diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index c6888b25d3ed7..68ba3a1d4e3e2 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,4 +1,12 @@ # dotnet-format -w Roslyn.sln on June 18, 2021 abce41d282ac631be5217140f1bd46d0e250ad02 fbdb56063e761643707f6bc1e1ba469f6fb9a31a -57278e7dcbf7bffb310e8b14105f657f0fdbab78 \ No newline at end of file +57278e7dcbf7bffb310e8b14105f657f0fdbab78 +# Converting to file scoped namespaces on 2/27/2024. https://github.com/dotnet/roslyn/pull/72294 +ec6c8b40e23a595b1e75aec920e3fb29e55f61cf +de42965cf9b64d71bccc549dd9940210cda5abf1 +4262648cadff59cc703b6be8c00b9814a6b13c5a +17e64d5773140ae5813664932bbfa4722a983805 +3f632c70e8e28a5ede881784600d0f3d06d3138d +27b50978f636ac0588ea7c19664fac4502e964a7 +f125c2664d3bffb59536f4d3dfee7fa5323c7721 diff --git a/.github/fabricbot.json b/.github/fabricbot.json index 50a680d2df295..8834535bc1088 100644 --- a/.github/fabricbot.json +++ b/.github/fabricbot.json @@ -1,43 +1,6 @@ { "version": "1.0", "tasks": [ - { - "taskType": "trigger", - "capabilityId": "AutoMerge", - "subCapability": "AutoMerge", - "version": "1.0", - "config": { - "taskName": "Automatically merge PR once CI passes", - "label": "auto-merge", - "minMinutesOpen": "12", - "mergeType": "merge", - "deleteBranches": true, - "requireAllStatuses": false, - "minimumNumberOfCheckRuns": 0, - "removeLabelOnPush": true, - "silentMode": true, - "conditionalMergeTypes": [ - { - "condition": { - "placeholder": "" - } - } - ], - "requireAllStatuses_exemptList": [ - "codecov", - "msftbot", - "dependabot", - "DotNet Maestro", - "DotNet Maestro - Int", - ".NET Helix (Int)" - ], - "requireSpecificCheckRuns": true, - "requireSpecificCheckRunsList": [ - "roslyn-CI" - ] - }, - "disabled": false - }, { "taskType": "trigger", "capabilityId": "IssueResponder", @@ -47,6 +10,10 @@ "conditions": { "operator": "and", "operands": [ + { + "name": "isPr", + "parameters": {} + }, { "name": "labelAdded", "parameters": { @@ -76,8 +43,7 @@ "eventType": "pull_request", "eventNames": [ "pull_request", - "issues", - "project_card" + "issues" ], "taskName": "Auto-approve auto-merge PRs", "actions": [ @@ -99,6 +65,10 @@ "conditions": { "operator": "and", "operands": [ + { + "name": "isPr", + "parameters": {} + }, { "name": "isActivitySender", "parameters": { @@ -127,8 +97,7 @@ "eventType": "pull_request", "eventNames": [ "pull_request", - "issues", - "project_card" + "issues" ], "taskName": "Auto-approve maestro PRs", "actions": [ @@ -150,6 +119,10 @@ "conditions": { "operator": "and", "operands": [ + { + "name": "isPr", + "parameters": {} + }, { "operator": "or", "operands": [ @@ -178,8 +151,7 @@ "eventType": "pull_request", "eventNames": [ "pull_request", - "issues", - "project_card" + "issues" ], "taskName": "Milestone tracking", "actions": [ @@ -206,51 +178,9 @@ "operator": "and", "operands": [ { - "name": "isActivitySender", - "parameters": { - "user": "dotnet-maestro[bot]" - } - }, - { - "name": "isAction", - "parameters": { - "action": "opened" - } + "name": "isPr", + "parameters": {} }, - { - "name": "bodyContains", - "parameters": { - "bodyPattern": "Updates sdk.version" - } - } - ] - }, - "eventType": "pull_request", - "eventNames": [ - "pull_request", - "issues", - "project_card" - ], - "actions": [ - { - "name": "requestChangesPullRequest", - "parameters": { - "comment": "This PR changes the .NET SDK version. Review required from @dotnet/roslyn-infrastructure-current-swat before merging.\nTasks:\n- [ ] Getting Started Documentation has been updated\n- [ ] NuGet dependency version updated to match version shipping in SDK" - } - } - ], - "taskName": "Require PR approval before merging SDK change" - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "config": { - "conditions": { - "operator": "and", - "operands": [ { "name": "isActivitySender", "parameters": { @@ -271,10 +201,9 @@ } ] }, - "eventType": "issue", + "eventType": "pull_request", "eventNames": [ - "issues", - "project_card" + "issues" ], "actions": [ { @@ -439,6 +368,10 @@ "conditions": { "operator": "and", "operands": [ + { + "name": "isIssue", + "parameters": {} + }, { "name": "isOpen", "parameters": {} @@ -481,6 +414,10 @@ "conditions": { "operator": "and", "operands": [ + { + "name": "isPr", + "parameters": {} + }, { "name": "isAction", "parameters": { @@ -585,8 +522,7 @@ "eventType": "pull_request", "eventNames": [ "pull_request", - "issues", - "project_card" + "issues" ], "taskName": "Label Community Pull Requests", "actions": [ @@ -608,6 +544,10 @@ "conditions": { "operator": "and", "operands": [ + { + "name": "isPr", + "parameters": {} + }, { "name": "prMatchesPattern", "parameters": { @@ -730,8 +670,7 @@ "eventType": "pull_request", "eventNames": [ "pull_request", - "issues", - "project_card" + "issues" ], "actions": [ { @@ -744,247 +683,6 @@ "taskName": "Add \"Needs UX Triage\" on PRs" } }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestResponder", - "version": "1.0", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "operator": "or", - "operands": [ - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/16.11-vs-deps" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.0-vs-deps" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.1" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.1-vs-deps" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.2" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.3" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.4" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.5" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.6" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.7" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.8" - } - } - ] - }, - { - "operator": "and", - "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "isActivitySender", - "parameters": { - "user": "dotnet-bot" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "infraswat-approved" - } - } - ] - } - ] - } - ] - }, - "eventType": "pull_request", - "eventNames": [ - "pull_request", - "issues", - "project_card" - ], - "taskName": "Require InfraSwat approval on PRs to release branches", - "actions": [ - { - "name": "requestReviewer", - "parameters": { - "comment": "InfraSwat needs to sign off on PRs to release branches.", - "reRequest": true, - "reviewer": "@dotnet/roslyn-infrastructure-current-swat" - } - }, - { - "name": "requestChangesPullRequest", - "parameters": { - "comment": "InfraSwat needs to sign off on PRs to release branches." - } - } - ] - }, - "disabled": true - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestResponder", - "version": "1.0", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "labelAdded", - "parameters": { - "label": "infraswat-approved" - } - }, - { - "operator": "or", - "operands": [ - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/16.11-vs-deps" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.0-vs-deps" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.1" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.1-vs-deps" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.2" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.3" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.4" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.5" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.6" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.7" - } - }, - { - "name": "prTargetsBranch", - "parameters": { - "branchName": "release/17.8" - } - } - ] - } - ] - }, - "eventType": "pull_request", - "eventNames": [ - "pull_request", - "issues", - "project_card" - ], - "actions": [ - { - "name": "approvePullRequest", - "parameters": { - "comment": "InfraSwat approved the PR." - } - } - ], - "taskName": "Approve PRs to release branches that are approved by InfraSwat" - }, - "disabled": true - }, { "taskType": "trigger", "capabilityId": "IssueResponder", @@ -1060,8 +758,7 @@ "eventType": "pull_request", "eventNames": [ "pull_request", - "issues", - "project_card" + "issues" ], "taskName": "Adds \"Needs API Review\" on PRs that touch public APIs", "actions": [ @@ -1089,6 +786,10 @@ "conditions": { "operator": "and", "operands": [ + { + "name": "isIssue", + "parameters": {} + }, { "name": "hasLabel", "parameters": { @@ -1105,8 +806,7 @@ }, "eventType": "issue", "eventNames": [ - "issues", - "project_card" + "issues" ], "taskName": "Close automatically generated PR tagger issues", "actions": [ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bbbb1c4d21ad7..79e00ef37e4c9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -49,7 +49,20 @@ "msbuild", "-p:RunAnalyzersDuringBuild=false", "-p:GenerateFullPaths=true", - "src/Compilers/CSharp/csc/csc.csproj" + "src/Compilers/CSharp/csc/AnyCpu/csc.csproj" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "build Compilers.slnf", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "-p:RunAnalyzersDuringBuild=false", + "-p:GenerateFullPaths=true", + "Compilers.slnf" ], "problemMatcher": "$msCompile", "group": "build" diff --git a/azure-pipelines-compliance.yml b/azure-pipelines-compliance.yml index ade0fedc1db29..18b415693f5b2 100644 --- a/azure-pipelines-compliance.yml +++ b/azure-pipelines-compliance.yml @@ -23,6 +23,10 @@ variables: value: true - name: _DevDivDropAccessToken value: $(System.AccessToken) + - name: Codeql.Enabled + value: false + - name: Codeql.SkipTaskAutoInjection + value: true steps: - template: eng/pipelines/checkout-windows-task.yml diff --git a/azure-pipelines-integration-corehost.yml b/azure-pipelines-integration-corehost.yml index 09b7766625b0a..710a212c7e06c 100644 --- a/azure-pipelines-integration-corehost.yml +++ b/azure-pipelines-integration-corehost.yml @@ -48,6 +48,12 @@ pr: - CONTRIBUTING.md - README.md +variables: +- name: Codeql.Enabled + value: false +- name: Codeql.SkipTaskAutoInjection + value: true + parameters: - name: poolName displayName: Pool Name diff --git a/azure-pipelines-integration-dartlab.yml b/azure-pipelines-integration-dartlab.yml index 429d052c25f7b..1f6926648b39f 100644 --- a/azure-pipelines-integration-dartlab.yml +++ b/azure-pipelines-integration-dartlab.yml @@ -27,6 +27,10 @@ resources: variables: - name: XUNIT_LOGS value: $(Build.SourcesDirectory)\artifacts\log\$(_configuration) +- name: Codeql.Enabled + value: false +- name: Codeql.SkipTaskAutoInjection + value: true stages: - template: \stages\visual-studio\agent.yml@DartLabTemplates diff --git a/azure-pipelines-integration-lsp.yml b/azure-pipelines-integration-lsp.yml index 25063253d0e0a..1176304615af8 100644 --- a/azure-pipelines-integration-lsp.yml +++ b/azure-pipelines-integration-lsp.yml @@ -34,6 +34,12 @@ pr: - release/dev17.2 - release/dev17.3 +variables: +- name: Codeql.Enabled + value: false +- name: Codeql.SkipTaskAutoInjection + value: true + parameters: - name: poolName displayName: Pool Name diff --git a/azure-pipelines-integration-scouting.yml b/azure-pipelines-integration-scouting.yml index 72b4d1cf71bb8..edddff358055f 100644 --- a/azure-pipelines-integration-scouting.yml +++ b/azure-pipelines-integration-scouting.yml @@ -11,6 +11,12 @@ schedules: - main - main-vs-deps +variables: +- name: Codeql.Enabled + value: false +- name: Codeql.SkipTaskAutoInjection + value: true + parameters: - name: poolName displayName: Pool Name diff --git a/azure-pipelines-integration.yml b/azure-pipelines-integration.yml index 60ee9b5a40e61..8e9e05ea61b22 100644 --- a/azure-pipelines-integration.yml +++ b/azure-pipelines-integration.yml @@ -44,7 +44,13 @@ pr: - README.md - src/Compilers/* - src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/README.md - + +variables: +- name: Codeql.Enabled + value: false +- name: Codeql.SkipTaskAutoInjection + value: true + parameters: - name: poolName displayName: Pool Name diff --git a/azure-pipelines-pr-validation.yml b/azure-pipelines-pr-validation.yml index 03df16351880a..d918f22289781 100644 --- a/azure-pipelines-pr-validation.yml +++ b/azure-pipelines-pr-validation.yml @@ -42,6 +42,11 @@ variables: - name: _DevDivDropAccessToken value: $(System.AccessToken) + - name: Codeql.Enabled + value: false + - name: Codeql.SkipTaskAutoInjection + value: true + stages: - stage: build displayName: Build and Test diff --git a/azure-pipelines-richnav.yml b/azure-pipelines-richnav.yml index 9a699c286ceec..26079c6b43d89 100644 --- a/azure-pipelines-richnav.yml +++ b/azure-pipelines-richnav.yml @@ -16,6 +16,12 @@ pr: none # - features/* # - demos/* +variables: +- name: Codeql.Enabled + value: false +- name: Codeql.SkipTaskAutoInjection + value: true + parameters: - name: poolName displayName: Pool Name diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b691df4dfa0b7..2878fa4a84802 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -35,6 +35,11 @@ variables: - name: HelixApiAccessToken value: '' + - name: Codeql.Enabled + value: false + - name: Codeql.SkipTaskAutoInjection + value: true + # Set pool / queue name variables depending on which instance we're running in. - name: PoolName ${{ if eq(variables['System.TeamProject'], 'public') }}: @@ -339,17 +344,17 @@ stages: # https://github.com/dotnet/runtime/issues/97186 # Disabled until runtime can track down the crash - # - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: - # - template: eng/pipelines/test-unix-job.yml - # parameters: - # testRunName: 'Test macOS Debug' - # jobName: Test_macOS_Debug - # testArtifactName: Transport_Artifacts_Unix_Debug - # configuration: Debug - # testArguments: --testCoreClr - # helixQueueName: $(HelixMacOsQueueName) - # helixApiAccessToken: $(HelixApiAccessToken) - # poolParameters: ${{ parameters.ubuntuPool }} + - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: + - template: eng/pipelines/test-unix-job.yml + parameters: + testRunName: 'Test macOS Debug' + jobName: Test_macOS_Debug + testArtifactName: Transport_Artifacts_Unix_Debug + configuration: Debug + testArguments: --testCoreClr + helixQueueName: $(HelixMacOsQueueName) + helixApiAccessToken: $(HelixApiAccessToken) + poolParameters: ${{ parameters.ubuntuPool }} - stage: Correctness dependsOn: [] @@ -383,19 +388,22 @@ stages: - task: PowerShell@2 displayName: Restore inputs: + pwsh: true filePath: eng/build.ps1 arguments: -configuration Release -prepareMachine -ci -restore -binaryLogName Restore.binlog - task: PowerShell@2 displayName: Build inputs: + pwsh: true filePath: eng/build.ps1 arguments: -configuration Release -prepareMachine -ci -build -pack -publish -sign -binaryLogName Build.binlog - script: $(Build.SourcesDirectory)\artifacts\bin\BuildBoss\Release\net472\BuildBoss.exe -r "$(Build.SourcesDirectory)/" -c Release -p Roslyn.sln displayName: Validate Build Artifacts - - script: eng/validate-rules-missing-documentation.cmd -ci + - pwsh: | + ./eng/validate-rules-missing-documentation.ps1 -ci displayName: Validate rules missing documentation - pwsh: | @@ -404,7 +412,7 @@ stages: condition: or(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['compilerChange'], 'true')) - pwsh: | - ./eng/validate-code-formatting.ps1 -rootDirectory $(Build.SourcesDirectory)\src -includeDirectories Compilers\CSharp\Portable\Generated\, Compilers\VisualBasic\Portable\Generated\, ExpressionEvaluator\VisualBasic\Source\ResultProvider\Generated\ + ./eng/validate-code-formatting.ps1 -ci -rootDirectory $(Build.SourcesDirectory)\src -includeDirectories Compilers\CSharp\Portable\Generated\, Compilers\VisualBasic\Portable\Generated\, ExpressionEvaluator\VisualBasic\Source\ResultProvider\Generated\ displayName: Validate Generated Syntax Files condition: or(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['compilerChange'], 'true')) @@ -422,13 +430,15 @@ stages: - template: eng/pipelines/variables-build.yml parameters: configuration: Debug + - name: bootstrapDir + value: $(Build.SourcesDirectory)/artifacts/bootstrap/determinism steps: - template: eng/pipelines/checkout-windows-task.yml - - script: eng/make-bootstrap.cmd -name determinism + - pwsh: eng/make-bootstrap.ps1 -output $(bootstrapDir) displayName: Build Bootstrap Compiler - - script: eng/test-determinism.cmd -configuration Debug -bootstrapDir $(Build.SourcesDirectory)/artifacts/bootstrap/determinism + - pwsh: eng/test-determinism.ps1 -configuration Debug -bootstrapDir $(bootstrapDir) displayName: Build - Validate determinism - template: eng/pipelines/publish-logs.yml diff --git a/docs/Language Feature Status.md b/docs/Language Feature Status.md index 4d4c5f1309032..5a638bb3ccf9c 100644 --- a/docs/Language Feature Status.md +++ b/docs/Language Feature Status.md @@ -10,6 +10,7 @@ efforts behind them. | Feature | Branch | State | Developer | Reviewer | IDE Buddy | LDM Champ | | ------- | ------ | ----- | --------- | -------- | --------- | --------- | +| [Ref Struct Interfaces](https://github.com/dotnet/csharplang/issues/7608) | [RefStructInterfaces](https://github.com/dotnet/roslyn/tree/features/RefStructInterfaces) | [In Progress](https://github.com/dotnet/roslyn/issues/72124) | [AlekseyTs](https://github.com/AlekseyTs) | [cston](https://github.com/cston), [jjonescz](https://github.com/jjonescz) | | [agocke](https://github.com/agocke), [jaredpar](https://github.com/jaredpar) | | [Params-collections](https://github.com/dotnet/csharplang/issues/7700) | [ParamsCollections](https://github.com/dotnet/roslyn/tree/features/ParamsCollections) | [In Progress](https://github.com/dotnet/roslyn/issues/71137) | [AlekseyTs](https://github.com/AlekseyTs) | [RikkiGibson](https://github.com/RikkiGibson), [333fred](https://github.com/333fred) | | [MadsTorgersen](https://github.com/MadsTorgersen), [AlekseyTs](https://github.com/AlekseyTs) | | [Semi-auto-properties](https://github.com/dotnet/csharplang/issues/140) | [semi-auto-props](https://github.com/dotnet/roslyn/tree/features/semi-auto-props) | [In Progress](https://github.com/dotnet/roslyn/issues/57012) | [Youssef1313](https://github.com/Youssef1313) | [333fred](https://github.com/333fred), [RikkiGibson](https://github.com/RikkiGibson) | | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | | [Params Span\ + Stackalloc any array type](https://github.com/dotnet/csharplang/issues/1757) | [params-span](https://github.com/dotnet/roslyn/tree/features/params-span) | [In Progress](https://github.com/dotnet/roslyn/issues/57049) | [cston](https://github.com/cston) | TBD | | [jaredpar](https://github.com/jaredpar) | @@ -17,7 +18,7 @@ efforts behind them. | [Roles/Extensions](https://github.com/dotnet/csharplang/issues/5497) | [roles](https://github.com/dotnet/roslyn/tree/features/roles) | [In Progress](https://github.com/dotnet/roslyn/issues/66722) | [jcouv](https://github.com/jcouv) | [AlekseyTs](https://github.com/AlekseyTs), [jjonescz](https://github.com/jjonescz) | | [MadsTorgersen](https://github.com/MadsTorgersen) | | [Escape character](https://github.com/dotnet/csharplang/issues/7400) | N/A | [In Progress](https://github.com/dotnet/roslyn/pull/70497) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | [jcouv](https://github.com/jcouv), [RikkiGibson](https://github.com/RikkiGibson) | | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | | [Method group natural type improvements](https://github.com/dotnet/csharplang/blob/main/proposals/method-group-natural-type-improvements.md) | main | In Progress | [jcouv](https://github.com/jcouv) | [AlekseyTs](https://github.com/AlekseyTs), [cston](https://github.com/cston) | | [jcouv](https://github.com/jcouv) | -| [`Lock` object](https://github.com/dotnet/csharplang/issues/7104) | [LockObject](https://github.com/dotnet/roslyn/tree/features/LockObject) | [In Progress](https://github.com/dotnet/roslyn/issues/71888) | [jjonescz](https://github.com/jjonescz) | [cston](https://github.com/cston), [RikkiGibson](https://github.com/RikkiGibson) | | [stephentoub](https://github.com/stephentoub) | +| [`Lock` object](https://github.com/dotnet/csharplang/issues/7104) | [LockObject](https://github.com/dotnet/roslyn/tree/features/LockObject) | [Merged into 17.10p2](https://github.com/dotnet/roslyn/issues/71888) | [jjonescz](https://github.com/jjonescz) | [cston](https://github.com/cston), [RikkiGibson](https://github.com/RikkiGibson) | | [stephentoub](https://github.com/stephentoub) | | Implicit indexer access in object initializers | main | [Merged into 17.9p3](https://github.com/dotnet/roslyn/pull/70649) | [jcouv](https://github.com/jcouv) | [AlekseyTs](https://github.com/AlekseyTs), [cston](https://github.com/cston) | | | # C# 12.0 diff --git a/docs/contributing/Compiler Test Plan.md b/docs/contributing/Compiler Test Plan.md index 5813a6884f9db..dbd74ee0c8c74 100644 --- a/docs/contributing/Compiler Test Plan.md +++ b/docs/contributing/Compiler Test Plan.md @@ -183,7 +183,7 @@ return … ; try  { … } catch (…) when (…) { … } finally { … } checked { … } unchecked { … } -lock(…) … +lock(…) … // including variant on an instance of the `System.Threading.Lock` type using (…) … // including `await` variant yield return …; yield break; diff --git a/docs/contributing/Target Framework Strategy.md b/docs/contributing/Target Framework Strategy.md index be6de4f06a091..a6545072f1920 100644 --- a/docs/contributing/Target Framework Strategy.md +++ b/docs/contributing/Target Framework Strategy.md @@ -17,14 +17,15 @@ It is not reasonable for us to take the union of all TFM and multi-target every Projects in our repository should include the following values in `` based on the rules below: -1. `$(NetRoslynSourceBuild)`: code that needs to be part of source build. This property will change based on whether the code is building in a source build context or official builds. In official builds this will include the TFMs for `$(NetVSShared)` +1. `$(NetRoslynSourceBuild)`: code that needs to be part of source build. This property will change based on whether the code is building in a source build context or official builds. + a. In official builds this will include the TFMs for `$(NetVSShared)` + b. In source builds this will include `$(NetRoslyn)` 2. `$(NetVS)`: code that needs to execute on the private runtime of Visual Studio. 3. `$(NetVSCode)`: code that needs to execute in DevKit host 4. `$(NetVSShared)`: code that needs to execute in both Visual Studio and VS Code but does not need to be source built. -5. `$(NetRoslynToolset)`: packages that ship the Roslyn toolset. The compiler often builds against multiple target frameworks. This property controls which of those frameworks are shipped in the toolset packages. This value will potentially change in source builds. +5. `$(NetRoslyn)`: code that needs to execute on .NET but does not have any specific product deployment requirements. For example utilities that are used by our infra, compiler unit tests, etc ... This property also controls which of the frameworks the compiler builds against are shipped in the toolset packages. This value will potentially change in source builds. 6. `$(NetRoslynAll)`: code, generally test utilities, that need to build for all .NET runtimes that we support. -7. `$(NetRoslyn)`: code that needs to execute on .NET but does not have any specific product deployment requirements. For example utilities that are used by our infra, compiler unit tests, etc ... -8. `$(NetRoslynBuildHostNetCoreVersion)`: the target used for the .NET Core BuildHost process used by MSBuildWorkspace. +7. `$(NetRoslynBuildHostNetCoreVersion)`: the target used for the .NET Core BuildHost process used by MSBuildWorkspace. This properties `$(NetCurrent)`, `$(NetPrevious)` and `$(NetMinimum)` are not used in our project files because they change in ways that make it hard for us to maintain corect product deployments. Our product ships on VS and VS Code which are not captured by arcade `$(Net...)` macros. Further as the arcade properties change it's very easy for us to end up with duplicate entries in a `` setting. Instead our repo uses the above values and when inside source build or VMR our properties are initialized with arcade properties. diff --git a/docs/features/incremental-generators.cookbook.md b/docs/features/incremental-generators.cookbook.md new file mode 100644 index 0000000000000..9bbb0b9134c9d --- /dev/null +++ b/docs/features/incremental-generators.cookbook.md @@ -0,0 +1,653 @@ +# Incremental Generators Cookbook + +## Summary + +This document aims to be a guide to help the creation of source generators by providing a series of guidelines for common patterns. +It also aims to set out what types of generators are possible under the current design, and what is expected to be explicitly out +of scope in the final design of the shipping feature. + +**This document expands on the details in the [full design document](incremental-generators.md), please ensure you have read that first.** + +## Table of contents + +- [Incremental Generators Cookbook](#incremental-generators-cookbook) + - [Summary](#summary) + - [Table of contents](#table-of-contents) + - [Proposal](#proposal) + - [Out of scope designs](#out-of-scope-designs) + - [Language features](#language-features) + - [Code rewriting](#code-rewriting) + - [Conventions](#conventions) + - [Pipeline model design](#pipeline-model-design) + - [Use `ForAttributeWithMetadataName`](#use-forattributewithmetadataname) + - [Use an indented text writer, not `SyntaxNode`s, for generation](#use-an-indented-text-writer-not-syntaxnodes-for-generation) + - [Designs](#designs) + - [Generated class](#generated-class) + - [Additional file transformation](#additional-file-transformation) + - [Augment user code](#augment-user-code) + - [Issue Diagnostics](#issue-diagnostics) + - [INotifyPropertyChanged](#inotifypropertychanged) + - [Package a generator as a NuGet package](#package-a-generator-as-a-nuget-package) + - [Use functionality from NuGet packages](#use-functionality-from-nuget-packages) + - [Access Analyzer Config properties](#access-analyzer-config-properties) + - [Consume MSBuild properties and metadata](#consume-msbuild-properties-and-metadata) + - [Unit Testing of Generators](#unit-testing-of-generators) + - [Auto interface implementation](#auto-interface-implementation) + - [Breaking Changes:](#breaking-changes) + - [Open Issues](#open-issues) + +## Proposal + +As a reminder, the high level design goals of source generators are: + +- Generators produce one or more strings that represent C# source code to be added to the compilation. +- Explicitly _additive_ only. Generators can add new source code to a compilation but may **not** modify existing user code. +- May access _additional files_, that is, non-C# source texts. +- Run _un-ordered_, each generator will see the same input compilation, with no access to files created by other source generators. +- A user specifies the generators to run via list of assemblies, much like analyzers. +- Generators create a pipeline, starting from base input sources and mapping them to the output they wish to produce. The more exposed, + properly equatable states exist, the earlier the compiler will be able to cut off changes and reuse the same output. + +## Out of scope designs + +We will briefly look at the non-solvable problems as examples of the kind of problems source generators are *not* designed to solve: + +### Language features + +Source generators are not designed to replace new language features: for instance one could imagine [records](records.md) being implemented as a source generator +that converts the specified syntax to a compilable C# representation. + +We explicitly consider this to be an anti-pattern; the language will continue to evolve and add new features, and we don't expect source generators to be +a way to enable this. Doing so would create new 'dialects' of C# that are incompatible with the compiler without generators. Further, because generators, +by design, cannot interact with each other, language features implemented in this way would quickly become incompatible with other additions to the language. + +### Code rewriting + +There are many post-processing tasks that users perform on their assemblies today, which here we define broadly as 'code rewriting'. These include, but +are not limited to: + +- Optimization +- Logging injection +- IL Weaving +- Call site re-writing + +While these techniques have many valuable use cases, they do not fit into the idea of *source generation*. They are, by definition, code altering operations +which are explicitly ruled out by the source generators proposal. + +There are already well supported tools and techniques for achieving these kinds of operations, and the source generators proposal is not aimed at replacing them. +We are exploring approaches for call site rewriting (see [interceptors.md](interceptors.md)), but those features are experimental and may change significantly +or even be removed. + +## Conventions + +### Pipeline model design + +As a general guideline, source generator pipelines need to pass along models that are _value equatable_. This is critical to the incrementality of an +`IIncrementalGenerator`; as soon as a pipeline step returns the same information that it returned in the previous run, the generator driver can stop running +the generator and reuse the same cached data that the generator produced in the previous run. Most times a generator is triggered (particularly generators that +need to look at type or method definitions, using `ForAttributeWithMetadataName`) the edit that triggered the generator will not actually have affected the +things that your generator is looking at. However, because semantics can change on basically any edit, the generator driver _must_ rerun your generator again +to ensure that this is the case. If your generator then produces a model with the same values as it did previously, this short-circuits the pipeline and allows +us to avoid a lot of work. Here are some general guidelines around designing your models to ensure that you maintain this equality: + +* Use `record`s, rather than `class`es, so that value equality is generated for you. +* Symbols (`ISymbol` and anything that inherits from that interface) are never equatable, and including them in your model can potentially root old compilations + and force Roslyn to hold onto lots of memory that it could otherwise free. Never put these in your model types. Instead, extract the information you need from + the symbols you inspect to an equatable representation: `string`s often work quite well here. +* `SyntaxNode`s are also usually not equatable between runs. They're not as strongly discouraged from the initial stages of a pipeline as symbols, and an example + [later down](#access-analyzer-config-properties) shows a case where you will need to include a `SyntaxNode` in model. They also don't potentially root as much + memory as symbols will. However, any edit in a file will ensure that all `SyntaxNode`s from that file are no longer equatable, so they should be removed from + your models as soon as possible. +* The previous bullet applies to `Location`s as well. +* Be careful of collection types in your models. Most built-in collection types in .NET do not do value equality by default. Arrays, `ImmutableArray` and + `List`, for example, use reference equality, not value equality. We suggest that most generator authors use a wrapper type around arrays to augment them + with value-based equality. + +### Use `ForAttributeWithMetadataName` + +We highly recommend that all generator authors that need to inspect syntax do so by using a marker attribute to indicate the types or members that need to be +inspected. This has multiple benefits, both for you as an author, and also for your users: + +* As an author, you can use `SyntaxProvider.ForAttributeWithMetadataName`. This utility method is at least 99x more efficient than `SyntaxProvider.CreateSyntaxProvider`, + and in many cases even more efficient. This will help you avoid causing performance issues for your users in editors. +* Your users can clearly indicate that they _intend_ to use your source generator. This intention is extremely helpful for designing a good user experience; it + means that you can author Roslyn analyzers to help your users when they intended to use your generator but violated your rules in some fashion. For example, if + you are generating some method body, and your generator requires that the user return a specific type, the presence of a `GenerateMe` attribute means you can + write an analyzer to tell the user if their method declaration returns something that it shouldn't. + +### Use an indented text writer, not `SyntaxNode`s, for generation + +We do not recommend generating `SyntaxNode`s when generating syntax for `AddSource`. Doing so can be complex, and it can be difficult to format it well; calling +`NormalizeWhitespace` is often quite expensive, and the API is not really designed for this use-case. Additionally, to ensure immutability guarantees, `AddSource` does +not accept `SyntaxNode`s. It instead requires getting the `string` representation and putting that into a `SourceText`. Instead of `SyntaxNode`, we recommend using a +wrapper around `StringBuilder` that will keep track of indent level and prepend the right amount of indentation when `AppendLine` is called. See +[this](https://github.com/dotnet/roslyn/issues/52914#issuecomment-1732680995) conversation on the performance of `NormalizeWhitespace` for more examples, performance +measurements, and discussion on why we don't believe that `SyntaxNode`s are a good abstraction for this use case. + +## Designs + +This section is broken down by user scenarios, with general solutions listed first, and more specific examples later on. + +### Generated class + +**User scenario:** As a generator author I want to be able to add a type to the compilation, that can be referenced by the user's code. Common use cases include +creating an attribute that will be used to drive other source generator steps. + +**Solution:** Have the user write the code as if the type was already present. Generate the missing type based on information available in the compilation using +the `RegisterPostInitializationOutput` step. + +**Example:** + +Given the following user code: + +```csharp +public partial class UserClass +{ + [GeneratedNamespace.GeneratedAttribute] + public partial void UserMethod(); +} +``` + +Create a generator that will create the missing type when run: + +```csharp +[Generator] +public class CustomGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(static postInitializationContext => { + postInitializationContext.AddSource("myGeneratedFile.cs", SourceText.From(""" + using System; + + namespace GeneratedNamespace + { + internal sealed class GeneratedAttribute : Attribute + { + } + } + """, Encoding.UTF8)); + }); + } +} +``` + +**Alternative Solution**: If you are also providing a library to your users, in addition to a source generator, simply have that library include the +attribute definition. + +### Additional file transformation + +**User scenario:** As a generator author I want to be able to transform an external non-C# file into an equivalent C# representation. + +**Solution:** Use the `AdditionalTextsProvider` to filter for and retrieve your files. Transform them into the code you care about, then +register that code with the solution. + +**Example:** + +```csharp +[Generator] +public class FileTransformGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var pipeline = context.AdditionalTextsProvider + .Where(static (text, cancellationToken) => text.Path.EndsWith(".xml")) + .Select(static (text, cancellationToken) => + { + var name = Path.GetFileName(text.Path); + var code = MyXmlToCSharpCompiler.Compile(text.GetText(cancellationToken)); + return (name, code); + }); + + context.RegisterSourceOutput(pipeline, + static (context, pair) => + // Note: this AddSource is simplified. You will likely want to include the path in the name of the file to avoid + // issues with duplicate file names in different paths in the same project. + context.AddSource($"{pair.name}generated.cs", SourceText.From(pair.code, Encoding.UTF8))); + } +} +``` + +### Augment user code + +**User scenario:** As a generator author I want to be able to inspect and augment a user's code with new functionality. + +**Solution:** Require the user to make the class you want to augment be a `partial class`, and mark it with a unique attribute. +Provide that attribute in a `RegisterPostInitializationOutput` step. Register for callbacks on that attribute with +`ForAttributeWithMetadataName` to collect the information needed to generate code, and use tuples (or create an equatable model) + to pass along that information. That information should be extracted from syntax and symbols; **do not put syntax or symbols into + your models**. + +**Example:** + +```csharp +public partial class UserClass +{ + [Generate] + public partial void UserMethod(); +} +``` + +```csharp +[Generator] +public class AugmentingGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(static postInitializationContext => + postInitializationContext.AddSource("myGeneratedFile.cs", SourceText.From(""" + using System; + namespace GeneratedNamespace + { + [AttributeUsage(AttributeTargets.Method)] + internal sealed class GeneratedAttribute : Attribute + { + } + } + """, Encoding.UTF8)); + + var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName: "GeneratedNamespace.GeneratedAttribute", + predicate: static (syntaxNode, cancellationToken) => syntaxNode is BaseMethodDeclarationSyntax, + transform: static (context, cancellationToken) => + { + var containingClass = context.TargetSymbol.ContainingType; + return new Model( + // Note: this is a simplified example. You will also need to handle the case where the type is in a global namespace, nested, etc. + Namespace: containingClass.ContainingNamespace?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)), + ClassName: containingClass.Name, + MethodName: context.TargetSymbol.Name); + } + ); + + context.RegisterSourceOutput(pipeline, static (context, model) => + { + var sourceText = SourceText.From($$""" + namespace {{model.Namespace}}; + partial class {{model.ClassName}} + { + partial void {{model.MethodName}}() + { + // generated code + } + } + """, Encoding.UTF8); + + context.AddSource($"{model.ClassName}_{model.MethodName}.g.cs", sourceText); + }); + } + + private record Model(string Namespace, string ClassName, string MethodName); +} +``` + +### Issue Diagnostics + +**User Scenario:** As a generator author I want to be able to add diagnostics to the user's compilation. + +**Solution:** We do not recommend issuing diagnostics within generators. It is possible, but doing so without breaking incrementality is advanced topic +beyond the scope of this cookbook. Instead, we suggest writing a separate analyzer for reporting diagnostics. + +### INotifyPropertyChanged + +**User scenario:** As a generator author I want to be able to implement the `INotifyPropertyChanged` pattern automatically for a user. + +**Solution:** The design tenant 'Explicitly additive only' seems to be at direct odds with the ability to implement this, and appears to call for user code modification. +However we can instead take advantage of explicit fields and instead of *editing* the users properties, directly provide them for listed fields. + +**Example:** + +Given a user class such as: + +```csharp +using AutoNotify; + +public partial class UserClass +{ + [AutoNotify] + private bool _boolProp; + + [AutoNotify(PropertyName = "Count")] + private int _intProp; +} +``` + +A generator could produce the following: + +```csharp +using System; +using System.ComponentModel; + +namespace AutoNotify +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + sealed class AutoNotifyAttribute : Attribute + { + public AutoNotifyAttribute() + { + } + public string PropertyName { get; set; } + } +} + + +public partial class UserClass : INotifyPropertyChanged +{ + public bool BoolProp + { + get => _boolProp; + set + { + _boolProp = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("UserBool")); + } + } + + public int Count + { + get => _intProp; + set + { + _intProp = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count")); + } + } + + public event PropertyChangedEventHandler PropertyChanged; +} +``` + +### Package a generator as a NuGet package + +**User scenario**: As a generator author I want to package my generator as a NuGet package for consumption. + +**Solution:** Generators can be packaged using the same method as an Analyzer would. +Ensure the generator is placed in the `analyzers\dotnet\cs` folder of the package for it to be automatically added to the users project on install. + +For example, to turn your generator project into a NuGet package at build, add the following to your project file: + +```xml + + true + false + + + + + + +``` + +### Use functionality from NuGet packages + +**User Scenario:** As a generator author I want to rely on functionality provided in NuGet packages inside my generator. + +**Solution:** It is possible to depend on NuGet packages inside of a generator, but special consideration has to be taken for distribution. + +Any *runtime* dependencies, that is, code that the end users program will need to rely on, can simply be added as a dependency of the generator NuGet package via the usual referencing mechanism. + +For example, consider a generator that creates code that relies on `Newtonsoft.Json`. The generator does not directly use the dependency, it just emits code that relies on the library being referenced in the users compilation. The author would add a reference to `Newtonsoft.Json` as a public dependency, and when the user adds the generator package it will referenced automatically. + +```xml + + + true + false + + + + + + + + + + +``` + +However, any *generation-time* dependencies, that is, used by the generator while it is is running and generating code, must be packaged directly alongside the generator assembly inside the generator NuGet package. There are no automatic facilities for this, and you will need to manually specify the dependencies to include. + +Consider a generator that uses `Newtonsoft.Json` to encode something to json during the generation pass, but does not emit any code the relies on it being present at runtime. The author would add a reference to `Newtonsoft.Json` but make all of its assets *private*; this ensures the consumer of the generator does not inherit a dependency on the library. + +The author would then have to package the `Newtonsoft.Json` library alongside the generator inside of the NuGet package. This can be achieved in the following way: set the dependency to generate a path property by adding `GeneratePathProperty="true"`. This will create a new MSBuild property of the format `PKG` where `` is the package name with `.` replaced by `_`. In our example there would be an MSBuild property called `PKGNewtonsoft_Json` with a value that points to the path on disk of the binary contents of the NuGet files. We can then use that to add the binaries to the resulting NuGet package as we do with the generator itself: + +```xml + + + true + false + + + + + + + + + + + + + +``` + +```C# +[Generator] +public class JsonUsingGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var pipeline = context.AdditionalTextsProvider.Select(static (text, cancellationToken) => + { + if (!text.Path.EndsWith("*.json")) + { + return default; + } + + return (Name: Path.GetFileName(text.Path), Value: Newtonsoft.Json.JsonConvert.DeserializeObject(text.GetText(cancellationToken).ToString())); + }) + .Where((pair) => pair is not ((_, null) or (null, _))); + + context.RegisterSourceOutput(pipeline, static (context, pair) => + { + var sourceText = SourceText.From($$""" + namespace GeneratedNamespace + { + internal sealed class GeneratedClass + { + public static const (int A, int B) SerializedContent = ({{pair.A}}, {{pair.B}}); + } + } + """, Encoding.UTF8); + + context.AddSource($"{pair.Name}generated.cs", sourceText) + }); + } + + record MyObject(int A, int B); +} +``` + +### Access Analyzer Config properties + +**User Scenarios:** + +- As a generator author I want to access the analyzer config properties for a syntax tree or additional file. +- As a generator author I want to access key-value pairs that customize the generator output. +- As a user of a generator I want to be able to customize the generated code and override defaults. + +**Solution**: Generators can access analyzer config values via the `AnalyzerConfigOptionsProvider`. Analyzer config values can either be accessed in the context of a `SyntaxTree`, `AdditionalFile` or globally via `GlobalOptions`. Global options are 'ambient' in that they don't apply to any specific context, but will be included when requesting option within a specific context. + +Note that this is one of the few cases that it is necessary to put a `SyntaxNode` into a pipeline, as you need the tree in order to get the generator option. +Try to get the `SyntaxNode` out of the pipeline as fast as possible to avoid making the model not correctly equatable. + +A generator is free to use a global option to customize its output. For example, consider a generator that can optionally emit logging. The author may choose to check the value of a global analyzer config value in order to control whether or not to emit the logging code. A user can then choose to enable the setting per project via an `.globalconfig` file: + +```.globalconfig +mygenerator_emit_logging = true +``` + +```csharp +[Generator] +public class MyGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + + var userCodePipeline = context.SyntaxProvider.ForAttributeWithMetadataName(... /* collect user code info */); + var emitLoggingPipeline = context.AnalyzerConfigOptionsProvider.Select(static (options, cancellationToken) => + options.GlobalOptions.TryGetValue("mygenerator_emit_logging", out var emitLoggingSwitch) + ? emitLoggingSwitch.Equals("true", StringComparison.InvariantCultureIgnoreCase) + : false); // Default + + context.RegisterSourceOutput(userCodePipeline.Combine(emitLoggingPipeline), (context, pair) => /* emit code */); + } +} +``` + +### Consume MSBuild properties and metadata + +**User Scenarios:** + +- As a generator author I want to make decisions based on the values contained in the project file +- As a user of a generator I want to be able to customize the generated code and override defaults. + +**Solution:** MSBuild will automatically translate specified properties and metadata into a global analyzer config that can be read by a generator. A generator author specifies the properties and metadata they want to make available by adding items to the `CompilerVisibleProperty` and `CompilerVisibleItemMetadata` item groups. These can be added via a props or targets file when packaging the generator as a NuGet package. + +For example, consider a generator that creates source based on additional files, and wants to allow a user to enable or disable logging via the project file. The author would specify in their props file that they want to make the specified MSBuild property visible to the compiler: + +```xml + + + +``` + +The value of `MyGenerator_EnableLogging` property will then be emitted to a generated analyzer config file before build, with a name of `build_property.MyGenerator_EnableLogging`. The generator is then able read this property from via the `GlobalOptions` property of the `AnalyzerConfigOptionsProvider` pipeline: + +```c# +context.AnalyzerConfigOptionsProvider.Select((provider, ct) => + provider.GlobalOptions.TryGetValue("build_property.MyGenerator_EnableLogging", out var emitLoggingSwitch) + ? emitLoggingSwitch.Equals("true", StringComparison.InvariantCultureIgnoreCase) : false); +``` + +A user can thus enable, or disable logging, by setting a property in their project file. + +Now, consider that the generator author wants to optionally allow opting in/out of logging on a per-additional file basis. The author can request that MSBuild emit the value of metadata for the specified file, by adding to the `CompilerVisibleItemMetadata` item group. The author specifies both the MSBuild itemType they want to read the metadata from, in this case `AdditionalFiles`, and the name of the metadata that they want to retrieve for them. + +```xml + + + +``` + +This value of `MyGenerator_EnableLogging` will be emitted to a generated analyzer config file, for each of the additional files in the compilation, with an item name of `build_metadata.AdditionalFiles.MyGenerator_EnableLogging`. The generator can read this value in the context of each additional file: + +```cs +context.AdditionalFilesProvider + .Combine(context.AnalyzerConfigOptionsProvider) + .Select((pair, ctx) => + pair.Right.GetOptions(pair.Left).TryGetValue("build_metadata.AdditionalFiles.MyGenerator_EnableLogging", out var perFileLoggingSwitch) + ? perFileLoggingSwitch : false); +``` + +In the users project file, the user can now annotate the individual additional files to say whether or not they want to enable logging: + +```xml + + + + + +``` + +**Full Example:** + +MyGenerator.props: + +```xml + + + + + + +``` + +MyGenerator.csproj: + +```xml + + + true + false + + + + + + + + + + +``` + +MyGenerator.cs: + +```csharp +[Generator] +public class MyGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var emitLoggingPipeline = context.AdditionalTextsProvider + .Combine(context.AnalyzerConfigOptionsProvider) + .Select((pair, ctx) => + pair.Right.GetOptions(pair.Left).TryGetValue("build_metadata.AdditionalFiles.MyGenerator_EnableLogging", out var perFileLoggingSwitch) + ? perFileLoggingSwitch.Equals("true", StringComparison.OrdinalIgnoreCase) + : pair.Right.GlobalOptions.TryGetValue("build_property.MyGenerator_EnableLogging", out var emitLoggingSwitch) + ? emitLoggingSwitch.Equals("true", StringComparison.OrdinalIgnoreCase) + : false); + + var sourcePipeline = context.AdditionalTextsProvider.Select((file, ctx) => /* Gather build info */); + + context.RegisterSourceOutput(sourcePipeline.Combine(emitLoggingPipeline), (context, pair) => /* Add source */); + } +} +``` + +### Unit Testing of Generators + +**User scenario**: As a generator author, I want to be able to unit test my generators to make development easier and ensure correctness. + +**Solution A**: + +The recommended approach is to use [Microsoft.CodeAnalysis.Testing](https://github.com/dotnet/roslyn-sdk/tree/main/src/Microsoft.CodeAnalysis.Testing#microsoftcodeanalysistesting) packages: + +- `Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.MSTest` +- `Microsoft.CodeAnalysis.VisualBasic.SourceGenerators.Testing.MSTest` +- `Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.NUnit` +- `Microsoft.CodeAnalysis.VisualBasic.SourceGenerators.Testing.NUnit` +- `Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit` +- `Microsoft.CodeAnalysis.VisualBasic.SourceGenerators.Testing.XUnit` + +TODO: https://github.com/dotnet/roslyn/issues/72149 + +### Auto interface implementation + +TODO: https://github.com/dotnet/roslyn/issues/72149 + +## Breaking Changes: + +* None currently + +## Open Issues + +This section track other miscellaneous TODO items: + +**Framework targets**: May want to mention if we have framework requirements for the generators, e.g. they must target netstandard2.0 or similar. + +**Conventions**: (See TODO in [conventions](#conventions) section above). What standard conventions are we suggesting to users? + +**Feature detection**: Show how to create a generator that relies on specific target framework features, without depending on the TargetFramework property. diff --git a/docs/features/incremental-generators.md b/docs/features/incremental-generators.md index 54b16cf4a150e..4e312a8f6c1a4 100644 --- a/docs/features/incremental-generators.md +++ b/docs/features/incremental-generators.md @@ -686,7 +686,7 @@ dedicated input node that instead exposes a sub-set of the syntax they are interested in. The syntax provider is specialized in this way to achieve a desired level of performance. -**CreateSyntaxProvider**: +#### CreateSyntaxProvider Currently the provider exposes a single method `CreateSyntaxProvider` that allows the author to construct an input node. @@ -781,6 +781,48 @@ allows the driver to substantially filter the number of nodes on which the semantic checks have to be re-run, significantly improved performance characteristics are still observed when editing a syntax tree. +#### ForAttributeWithMetadataName (FAWMN) + +One extremely common action we observe generators being written for is taking +actions driven on attributes applied to specific syntax constructs. + +```csharp +public readonly struct SyntaxValueProvider +{ + public IncrementalValuesProvider ForAttributeWithMetadataName( + string fullyQualifiedMetadataName, + Func predicate, + Func transform); +} +``` + +This area is particularly nice for optimization, as we can efficiently eliminate +a significant number of syntax nodes and edits before even needing to call the +provided `predicate` from the user, avoiding realizing a significant number of +`SyntaxNode` instances. Roslyn can even further optimize this by tracking whether or +not a given attribute could possibly be the attribute the generator cares about by +maintaining a small index and comparing type names as an initial heuristic. +This index is cheap to maintain and, importantly, can only have false positives, not +false negatives. This allows us to eliminate 99% of syntax in a Compilation from ever +needing to be checked for semantic information (to eliminate false positives from the +heuristic cache) or by the user `predicate` function (saving a significant number of +allocations of `SyntaxNode` instances). + +Given this, when at all possible, it is recommended to use attributes to drive source +generators, rather than other syntax constructs. Real world testing has indicated this +approach is usually 99x more efficient than `CreateSyntaxProvider`, even when the +generator is otherwise not well-behaved; some pathological scenarios are even more efficient +than that. + +Attributes are provided by the user as the fully-qualified metadata name, without the +assembly name portion. For example, given the C# type `My.Namespace.MyAttribute`, +the fully-qualified metadata-name would be ``My.Namespace.MyAttribute`1``. Given that +attributes are usually restricted to specific constructs by an `AttributeUsage` +attribute, it is common that the `predicate` a user provides will simply return `true`. +For the transformation step, everything stated in the [previous section](#createsyntaxprovider) +is still relevant; that step will still be rerun with every change to ensure that changed +semantics are observed. + ## Outputting values At some point in the pipeline the author will want to actually use the diff --git a/docs/features/interceptors.md b/docs/features/interceptors.md index f33c400202393..b96fe71e646b6 100644 --- a/docs/features/interceptors.md +++ b/docs/features/interceptors.md @@ -68,20 +68,15 @@ File-local declarations of this type (`file class InterceptsLocationAttribute`) #### File paths -File paths used in `[InterceptsLocation]` are expected to have `/pathmap` substitution already applied. Generators should accomplish this by locally recreating the file path transformation performed by the compiler: +The *referenced syntax tree* of an `[InterceptsLocation]` is determined by normalizing the `filePath` argument value relative to the path of the containing syntax tree of the `[InterceptsLocation]` usage, similar to how paths in `#line` directives are normalized. Let this normalized path be called `normalizedInterceptorPath`. If exactly one syntax tree in the compilation has a normalized path which matches `normalizedInterceptorPath` by ordinal string comparison, that is the *referenced syntax tree*. Otherwise, an error occurs. -```cs -using Microsoft.CodeAnalysis; - -string GetInterceptorFilePath(SyntaxTree tree, Compilation compilation) -{ - return compilation.Options.SourceReferenceResolver?.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath; -} -``` +`#line` directives are not considered when determining the call referenced by an `[InterceptsLocation]` attribute. In other words, the file path, line and column numbers used in `[InterceptsLocation]` are expected to refer to *unmapped* source locations. -The file path given in the attribute must be equal by ordinal comparison to the value given by the above function. +Temporarily, for compatibility purposes, when the initial matching strategy outlined above fails to match any syntax trees, we will fall back to a "compat" matching strategy which works in the following way: +- A *mapped path* of each syntax tree is determined by applying [`/pathmap`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.commandlinearguments.pathmap?view=roslyn-dotnet-4.7.0) substitution to `SyntaxTree.FilePath`. +- For a given `[InterceptsLocation]` usage, the `filePath` argument value is compared to the *mapped path* of each syntax tree using ordinal string comparison. If exactly one syntax tree matches under this comparison, that is the *referenced syntax tree*. Otherwise, an error occurs. -The compiler does not map `#line` directives when determining if an `[InterceptsLocation]` attribute intercepts a particular call in syntax. +Support for the "compat" strategy will be dropped prior to stable release. Tracked by https://github.com/dotnet/roslyn/issues/72265. #### Position diff --git a/docs/features/refout.md b/docs/features/refout.md index 4487d9f006353..0bba8504a30d4 100644 --- a/docs/features/refout.md +++ b/docs/features/refout.md @@ -100,6 +100,6 @@ As mentioned above, there may be further refinements after C# 7.1: ## Related issues - Produce ref assemblies from command-line and msbuild (https://github.com/dotnet/roslyn/issues/2184) - Refine what is in reference assemblies and what diagnostics prevent generating one (https://github.com/dotnet/roslyn/issues/17612) -- [Are private members part of the API surface?](http://blog.paranoidcoding.com/2016/02/15/are-private-members-api-surface.html) +- [Are private members part of the API surface?](https://blog.paranoidcoding.org/2016/02/15/are-private-members-api-surface.html) - MSBuild work items and design notes (https://github.com/Microsoft/msbuild/issues/1986) - Fast up-to-date check in project-system (https://github.com/dotnet/project-system/issues/2254) diff --git a/docs/features/source-generators.cookbook.md b/docs/features/source-generators.cookbook.md index ce05e5e1ebd0a..31a8722c78052 100644 --- a/docs/features/source-generators.cookbook.md +++ b/docs/features/source-generators.cookbook.md @@ -4,7 +4,7 @@ > **Warning**: Source generators implementing `ISourceGenerator` have been deprecated > in favor of [incremental generators](incremental-generators.md). -> This document has not been fully updated to reflect that. +> The incremental version of this document is [here](incremental-generators.cookbook.md). > You should implement `IIncrementalGenerator` instead of `ISourceGenerator`. This document aims to be a guide to help the creation of source generators by providing a series of guidelines for common patterns. @@ -835,39 +835,9 @@ Note: the above example uses MSTest, but the contents of the test are easily ada ### Participate in the IDE experience -**Implementation Status**: Not Implemented. - **User scenario:** As a generator author I want to be able to interactively regenerate code as the user is editing files. -**Solution:** We expect there to be an opt-in set of interactive callbacks that can be implemented to allow for progressively more complex generation strategies. -It is anticipated there will be a mechanism for providing symbol mapping for lighting up features such as 'Find all references'. - -```csharp -[Generator] -public class InteractiveGenerator : ISourceGenerator -{ - public void Initialize(GeneratorInitializationContext context) - { - // Register for additional file callbacks - context.RegisterForAdditionalFileChanges(OnAdditionalFilesChanged); - } - - public void Execute(GeneratorExecutionContext context) - { - // generators must always support a total generation pass - } - - public void OnAdditionalFilesChanged(AdditionalFilesChangedContext context) - { - // determine which file changed, and if it affects this generator - // regenerate only the parts that are affected by this change. - } -} -``` - -*Note*: Until these interfaces are made available, generator authors should not try and emulate 'incrementality' with caching to disk and custom up-to-date checks. -The compiler currently provides no reliable way for a generator to detect if it is suitable to use a previous run, and any attempt to do so will -likely lead to hard to diagnose bugs for consumers. Generator authors should always assume this is a 'full' generation, happening for the first time. +**Solution:** See [incremental generators](incremental-generators.md). ### Serialization diff --git a/eng/Directory.Packages.props b/eng/Directory.Packages.props index f5de301ea0523..007be63e2f975 100644 --- a/eng/Directory.Packages.props +++ b/eng/Directory.Packages.props @@ -170,10 +170,14 @@ + + + + @@ -182,9 +186,11 @@ - + + + @@ -192,11 +198,14 @@ + + + @@ -208,6 +217,7 @@ + diff --git a/eng/SourceBuildPrebuiltBaseline.xml b/eng/SourceBuildPrebuiltBaseline.xml index 36303b9c44860..81d5528e6ec92 100644 --- a/eng/SourceBuildPrebuiltBaseline.xml +++ b/eng/SourceBuildPrebuiltBaseline.xml @@ -22,10 +22,21 @@ + + + + + + - - - + + + + + + + + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 6017bf8fd7d3b..80553ac6bc4ec 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -8,51 +8,55 @@ - + https://github.com/dotnet/source-build-reference-packages - 2f79f97b7a6a0cf2ca3297a8fa526e6f4ea98ce2 + 6d28b5a26876f8f22dadb8f6cdee878faaa88464 - + https://github.com/dotnet/command-line-api - e9ac4ff4293cf853f3d07eb9e747aef27f5be965 + 94ef1b035c73904f2240b3b3b0de6c4890dab6d8 - + https://github.com/dotnet/command-line-api - e9ac4ff4293cf853f3d07eb9e747aef27f5be965 + 94ef1b035c73904f2240b3b3b0de6c4890dab6d8 - + https://github.com/dotnet/runtime - d099f075e45d2aa6007a22b71b45a08758559f80 + 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://github.com/dotnet/runtime - d099f075e45d2aa6007a22b71b45a08758559f80 + 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://github.com/dotnet/runtime - d099f075e45d2aa6007a22b71b45a08758559f80 + 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://github.com/dotnet/runtime - d099f075e45d2aa6007a22b71b45a08758559f80 + 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://github.com/dotnet/runtime - d099f075e45d2aa6007a22b71b45a08758559f80 + 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://github.com/dotnet/runtime - d099f075e45d2aa6007a22b71b45a08758559f80 + 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://github.com/dotnet/runtime - 0a2bda10e81d901396c3cff95533529e3a93ad47 + 5535e31a712343a63f5d7d796cd874e563e5ac14 - + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + + https://github.com/dotnet/runtime 5535e31a712343a63f5d7d796cd874e563e5ac14 @@ -60,16 +64,56 @@ https://github.com/dotnet/runtime 5535e31a712343a63f5d7d796cd874e563e5ac14 + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + - + https://github.com/dotnet/arcade - 61ae141d2bf3534619265c8f691fd55dc3e75147 + da98edc4c3ea539f109ea320672136ceb32591a7 - + https://github.com/dotnet/arcade - 61ae141d2bf3534619265c8f691fd55dc3e75147 + da98edc4c3ea539f109ea320672136ceb32591a7 @@ -96,9 +140,9 @@ https://github.com/dotnet/roslyn 5d10d428050c0d6afef30a072c4ae68776621877 - + https://github.com/dotnet/arcade - 61ae141d2bf3534619265c8f691fd55dc3e75147 + da98edc4c3ea539f109ea320672136ceb32591a7 https://github.com/dotnet/roslyn-analyzers @@ -106,9 +150,9 @@ - + https://github.com/dotnet/runtime - d099f075e45d2aa6007a22b71b45a08758559f80 + 5535e31a712343a63f5d7d796cd874e563e5ac14 https://github.com/dotnet/roslyn-analyzers diff --git a/eng/Versions.props b/eng/Versions.props index 3f9d4458ee396..adefbe8ba116e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,7 +8,7 @@ 4 10 0 - 2 + 3 $(MajorVersion).$(MinorVersion).$(PatchVersion) - 2.0.0-beta4.24112.1 - 7.0.0 - 7.0.0 + 2.0.0-beta4.24123.1 + 8.0.0 + 8.0.0 + 8.0.0 8.0.0 + 8.0.0 + 8.0.0 8.0.0 + 8.0.0 + 8.0.0 - 7.0.3 - 7.0.0 + 8.0.0 + 8.0.0 + 8.0.0 + 8.0.0 3.3.4 3.3.0 8.0.0-preview.23468.1 2.0.0 - 7.0.0 - 7.0.0 - 7.0.0 - 7.0.0 + 8.0.0 + 8.0.0 + 8.0.0 + 8.0.0 + 8.0.0 + 8.0.0 + 8.0.0 + 8.0.0 4.10.0-1.24061.4 17.9.3137-preview3 2.4.1 @@ -53,7 +64,6 @@ false true true - true true true true diff --git a/eng/build.ps1 b/eng/build.ps1 index 43fbdfba4db4c..b720e0ab7021c 100644 --- a/eng/build.ps1 +++ b/eng/build.ps1 @@ -751,8 +751,8 @@ try { if ($bootstrap -and $bootstrapDir -eq "") { Write-Host "Building bootstrap Compiler" - Exec-Script (Join-Path $PSScriptRoot "make-bootstrap.ps1") "-name build -force -ci:$ci" - $bootstrapDir = Join-Path $ArtifactsDir "bootstrap" "build" + $bootstrapDir = Join-Path (Join-Path $ArtifactsDir "bootstrap") "build" + Exec-Script (Join-Path $PSScriptRoot "make-bootstrap.ps1") "-output $bootstrapDir -force -ci:$ci" } if ($restore -or $build -or $rebuild -or $pack -or $sign -or $publish) { diff --git a/eng/build.sh b/eng/build.sh index 284714df4b517..976487bc9db3c 100755 --- a/eng/build.sh +++ b/eng/build.sh @@ -26,6 +26,7 @@ usage() echo "Test actions:" echo " --testCoreClr Run unit tests on .NET Core (short: --test, -t)" echo " --testMono Run unit tests on Mono" + echo " --testCompilerOnly Run only the compiler unit tests" echo " --testIOperation Run unit tests with the IOperation test hook" echo "" echo "Advanced settings:" @@ -61,6 +62,7 @@ publish=false test_core_clr=false test_mono=false test_ioperation=false +test_compiler_only=false configuration="Debug" verbosity='minimal' @@ -127,6 +129,9 @@ while [[ $# > 0 ]]; do --testmono) test_mono=true ;; + --testcompileronly) + test_compiler_only=true + ;; --testioperation) test_ioperation=true ;; @@ -301,6 +306,25 @@ function BuildSolution { $properties } +function GetCompilerTestAssembliesIncludePaths { + assemblies="--include '^Microsoft\.CodeAnalysis\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.CompilerServer\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.CSharp\.Syntax\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.CSharp\.Symbol\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.CSharp\.Semantic\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.CSharp\.Emit\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.CSharp\.Emit2\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.CSharp\.IOperation\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.CSharp\.CommandLine\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.VisualBasic\.Syntax\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.VisualBasic\.Symbol\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.VisualBasic\.Semantic\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.VisualBasic\.Emit\.UnitTests$'" + assemblies+=" --include '^Roslyn\.Compilers\.VisualBasic\.IOperation\.UnitTests$'" + assemblies+=" --include '^Microsoft\.CodeAnalysis\.VisualBasic\.CommandLine\.UnitTests$'" + echo "$assemblies" +} + install=false if [[ "$restore" == true || "$test_core_clr" == true ]]; then install=true @@ -322,6 +346,11 @@ fi if [[ "$test_core_clr" == true ]]; then runtests_args="" + + if [[ -n "$test_compiler_only" ]]; then + runtests_args="$runtests_args $(GetCompilerTestAssembliesIncludePaths)" + fi + if [[ -n "$helix_queue_name" ]]; then runtests_args="$runtests_args --helixQueueName $helix_queue_name" fi diff --git a/eng/common/post-build/publish-using-darc.ps1 b/eng/common/post-build/publish-using-darc.ps1 index 1e779fec4dd1e..5a3a32ea8d75b 100644 --- a/eng/common/post-build/publish-using-darc.ps1 +++ b/eng/common/post-build/publish-using-darc.ps1 @@ -12,7 +12,7 @@ param( try { . $PSScriptRoot\post-build-utils.ps1 - $darc = Get-Darc + $darc = Get-Darc $optionalParams = [System.Collections.ArrayList]::new() @@ -46,7 +46,7 @@ try { } Write-Host 'done.' -} +} catch { Write-Host $_ Write-PipelineTelemetryError -Category 'PromoteBuild' -Message "There was an error while trying to publish build '$BuildId' to default channels." diff --git a/eng/common/templates/job/publish-build-assets.yml b/eng/common/templates/job/publish-build-assets.yml index fa5446c093dd1..8ec0151def21a 100644 --- a/eng/common/templates/job/publish-build-assets.yml +++ b/eng/common/templates/job/publish-build-assets.yml @@ -58,7 +58,7 @@ jobs: demands: Cmd # If it's not devdiv, it's dnceng ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: - name: $(DncEngInternalBuildPool) + name: NetCore1ESPool-Publishing-Internal demands: ImageOverride -equals windows.vs2019.amd64 steps: @@ -71,7 +71,7 @@ jobs: checkDownloadedFiles: true condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} - + - task: NuGetAuthenticate@1 - task: PowerShell@2 @@ -86,7 +86,7 @@ jobs: /p:OfficialBuildId=$(Build.BuildNumber) condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} - + - task: powershell@2 displayName: Create ReleaseConfigs Artifact inputs: @@ -95,7 +95,7 @@ jobs: Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value $(BARBuildId) Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value "$(DefaultChannels)" Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value $(IsStableBuild) - + - task: PublishBuildArtifacts@1 displayName: Publish ReleaseConfigs Artifact inputs: @@ -121,7 +121,7 @@ jobs: - task: PublishBuildArtifacts@1 displayName: Publish SymbolPublishingExclusionsFile Artifact - condition: eq(variables['SymbolExclusionFile'], 'true') + condition: eq(variables['SymbolExclusionFile'], 'true') inputs: PathtoPublish: '$(Build.SourcesDirectory)/eng/SymbolPublishingExclusionsFile.txt' PublishLocation: Container @@ -137,7 +137,7 @@ jobs: displayName: Publish Using Darc inputs: filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-using-darc.ps1 - arguments: -BuildId $(BARBuildId) + arguments: -BuildId $(BARBuildId) -PublishingInfraVersion 3 -AzdoToken '$(publishing-dnceng-devdiv-code-r-build-re)' -MaestroToken '$(MaestroApiAccessToken)' @@ -148,4 +148,4 @@ jobs: - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: - template: /eng/common/templates/steps/publish-logs.yml parameters: - JobLabel: 'Publish_Artifacts_Logs' + JobLabel: 'Publish_Artifacts_Logs' diff --git a/eng/common/templates/post-build/post-build.yml b/eng/common/templates/post-build/post-build.yml index 3f74abf7ce0f8..aba44a25a3387 100644 --- a/eng/common/templates/post-build/post-build.yml +++ b/eng/common/templates/post-build/post-build.yml @@ -39,7 +39,7 @@ parameters: displayName: Enable NuGet validation type: boolean default: true - + - name: publishInstallersAndChecksums displayName: Publish installers and checksums type: boolean @@ -131,8 +131,8 @@ stages: displayName: Validate inputs: filePath: $(Build.SourcesDirectory)/eng/common/post-build/nuget-validation.ps1 - arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ - -ToolDestinationPath $(Agent.BuildDirectory)/Extract/ + arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ + -ToolDestinationPath $(Agent.BuildDirectory)/Extract/ - job: displayName: Signing Validation @@ -221,9 +221,9 @@ stages: displayName: Validate inputs: filePath: $(Build.SourcesDirectory)/eng/common/post-build/sourcelink-validation.ps1 - arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ - -ExtractPath $(Agent.BuildDirectory)/Extract/ - -GHRepoName $(Build.Repository.Name) + arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -ExtractPath $(Agent.BuildDirectory)/Extract/ + -GHRepoName $(Build.Repository.Name) -GHCommit $(Build.SourceVersion) -SourcelinkCliVersion $(SourceLinkCLIVersion) continueOnError: true @@ -258,7 +258,7 @@ stages: demands: Cmd # If it's not devdiv, it's dnceng ${{ else }}: - name: $(DncEngInternalBuildPool) + name: NetCore1ESPool-Publishing-Internal demands: ImageOverride -equals windows.vs2019.amd64 steps: - template: setup-maestro-vars.yml @@ -272,7 +272,7 @@ stages: displayName: Publish Using Darc inputs: filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-using-darc.ps1 - arguments: -BuildId $(BARBuildId) + arguments: -BuildId $(BARBuildId) -PublishingInfraVersion ${{ parameters.publishingInfraVersion }} -AzdoToken '$(publishing-dnceng-devdiv-code-r-build-re)' -MaestroToken '$(MaestroApiAccessToken)' diff --git a/eng/common/templates/variables/pool-providers.yml b/eng/common/templates/variables/pool-providers.yml index 9cc5c550d3b36..d236f9fdbb153 100644 --- a/eng/common/templates/variables/pool-providers.yml +++ b/eng/common/templates/variables/pool-providers.yml @@ -1,15 +1,15 @@ -# Select a pool provider based off branch name. Anything with branch name containing 'release' must go into an -Svc pool, +# Select a pool provider based off branch name. Anything with branch name containing 'release' must go into an -Svc pool, # otherwise it should go into the "normal" pools. This separates out the queueing and billing of released branches. -# Motivation: +# Motivation: # Once a given branch of a repository's output has been officially "shipped" once, it is then considered to be COGS # (Cost of goods sold) and should be moved to a servicing pool provider. This allows both separation of queueing # (allowing release builds and main PR builds to not intefere with each other) and billing (required for COGS. -# Additionally, the pool provider name itself may be subject to change when the .NET Core Engineering Services -# team needs to move resources around and create new and potentially differently-named pools. Using this template +# Additionally, the pool provider name itself may be subject to change when the .NET Core Engineering Services +# team needs to move resources around and create new and potentially differently-named pools. Using this template # file from an Arcade-ified repo helps guard against both having to update one's release/* branches and renaming. -# How to use: +# How to use: # This yaml assumes your shipped product branches use the naming convention "release/..." (which many do). # If we find alternate naming conventions in broad usage it can be added to the condition below. # @@ -54,4 +54,4 @@ variables: False, 'NetCore1ESPool-Internal' ) - ] \ No newline at end of file + ] diff --git a/eng/config/PublishData.json b/eng/config/PublishData.json index 993d930a7cc2d..7653ce3b21a5e 100644 --- a/eng/config/PublishData.json +++ b/eng/config/PublishData.json @@ -196,15 +196,6 @@ "insertionTitlePrefix": "[d17.9]" }, "release/dev17.10": { - "nugetKind": [ - "Shipping", - "NonShipping" - ], - "vsBranch": "rel/d17.10", - "vsMajorVersion": 17, - "insertionTitlePrefix": "[d17.10]" - }, - "main": { "nugetKind": [ "Shipping", "NonShipping" @@ -214,7 +205,7 @@ "insertionCreateDraftPR": false, "insertionTitlePrefix": "[d17.10 P2]" }, - "features/vscode_net8": { + "main": { "nugetKind": [ "Shipping", "NonShipping" @@ -222,9 +213,9 @@ "vsBranch": "main", "vsMajorVersion": 17, "insertionCreateDraftPR": true, - "insertionTitlePrefix": "[Validation]" + "insertionTitlePrefix": "[d17.10 P3]" }, - "dev/jjonescz/razor-ea": { + "features/vscode_net8": { "nugetKind": [ "Shipping", "NonShipping" diff --git a/eng/generate-compiler-code.ps1 b/eng/generate-compiler-code.ps1 index 8d8bb403d96eb..3e323530f30c3 100644 --- a/eng/generate-compiler-code.ps1 +++ b/eng/generate-compiler-code.ps1 @@ -3,7 +3,8 @@ # the generator source files. [CmdletBinding(PositionalBinding=$false)] param ([string]$configuration = "Debug", - [switch]$test = $false) + [switch]$test = $false, + [switch]$ci = $false) Set-StrictMode -version 2.0 $ErrorActionPreference="Stop" @@ -115,6 +116,7 @@ function Get-ToolPath($projectRelativePath) { try { . (Join-Path $PSScriptRoot "build-utils.ps1") Push-Location $RepoRoot + $prepareMachine = $ci $dotnet = Ensure-DotnetSdk $boundTreeGenProject = Get-ToolPath 'BoundTreeGenerator\CompilersBoundTreeGenerator.csproj' @@ -136,11 +138,11 @@ try { Run-IOperation $coreDir $operationsProject Run-GetText - exit 0 + ExitWithExitCode 0 } catch { Write-Host $_ - exit 1 + ExitWithExitCode 1 } finally { Pop-Location diff --git a/eng/make-bootstrap.ps1 b/eng/make-bootstrap.ps1 index b9452046dc41a..64100d029cdfa 100644 --- a/eng/make-bootstrap.ps1 +++ b/eng/make-bootstrap.ps1 @@ -1,8 +1,8 @@ # Make a bootstrap compiler and install it into artifacts/bootstrap folder -[CmdletBinding(PositionalBinding=$false)] +[CmdletBinding(PositionalBinding=$true)] param ( - [string]$name = "local", + [string]$output = "", [string]$toolset = "Default", [string]$configuration = "Release", [switch]$force = $false, @@ -15,14 +15,18 @@ $ErrorActionPreference="Stop" try { . (Join-Path $PSScriptRoot "build-utils.ps1") + $prepareMachine = $ci - $bootstrapDir = Join-Path $ArtifactsDir "bootstrap" $name - Write-Host "Building bootstrap compiler into $bootstrapDir" + if ($output -eq "") { + $output = Join-Path $ArtifactsDir "bootstrap" "local" + } + + Write-Host "Building bootstrap compiler into $output" - if (Test-Path $bootstrapDir) { + if (Test-Path $output) { if ($force) { Write-Host "Removing existing bootstrap compiler" - Remove-Item -Recurse -Force $bootstrapDir + Remove-Item -Recurse -Force $output } else { Write-Host "Bootstrap compiler already exists. Use -force to rebuild" @@ -42,6 +46,7 @@ try { throw "Unsupported bootstrap toolset $toolset" } + $name = Split-Path -Leaf $output $binaryLogFilePath = Join-Path $LogDir "bootstrap-$($name).binlog" # Because we override the C#/VB toolset to build against our LKG package, it is important @@ -51,7 +56,7 @@ try { $args = "/p:TreatWarningsAsErrors=true /warnaserror /nologo /nodeReuse:false /p:Configuration=$configuration /v:m"; $args += " /p:RunAnalyzersDuringBuild=false /bl:$binaryLogFilePath" $args += " /t:Pack /p:RoslynEnforceCodeStyle=false /p:DotNetUseShippingVersions=true /p:InitialDefineConstants=BOOTSTRAP" - $args += " /p:PackageOutputPath=$bootstrapDir /p:NgenOptimization=false /p:PublishWindowsPdb=false" + $args += " /p:PackageOutputPath=$output /p:NgenOptimization=false /p:PublishWindowsPdb=false" if ($ci) { $args += " /p:ContinuousIntegrationBuild=true" @@ -59,21 +64,21 @@ try { Exec-DotNet "build $args $projectPath" - $packageFilePath = Get-ChildItem -Path $bootstrapDir -Filter "$packageName.*.nupkg" + $packageFilePath = Get-ChildItem -Path $output -Filter "$packageName.*.nupkg" Write-Host "Found package $packageFilePath" - Unzip $packageFilePath.FullName $bootstrapDir + Unzip $packageFilePath.FullName $output Write-Host "Cleaning up artifacts" Exec-DotNet "build --no-restore /t:Clean $projectPath" Exec-DotNet "build-server shutdown" - exit 0 + ExitWithExitCode 0 } catch { Write-Host $_ Write-Host $_.Exception Write-Host $_.ScriptStackTrace - exit 1 + ExitWithExitCode 1 } finally { Pop-Location diff --git a/eng/pipelines/build-bootstrap.yml b/eng/pipelines/build-bootstrap.yml index bb3678676d99c..7b68ac40bbded 100644 --- a/eng/pipelines/build-bootstrap.yml +++ b/eng/pipelines/build-bootstrap.yml @@ -7,10 +7,13 @@ parameters: steps: - template: checkout-windows-task.yml - - script: eng\make-bootstrap.cmd -ci -toolset ${{parameters.toolset}} -name "ci-bootstrap" + - pwsh: | + ./eng/make-bootstrap.ps1 -ci -toolset ${{parameters.toolset}} -output '$(Build.SourcesDirectory)/artifacts/bootstrap/ci-bootstrap' + displayName: Build Bootstrap Compiler - - script: eng/test-build-correctness.cmd -configuration Release -enableDumps -bootstrapDir $(Build.SourcesDirectory)/artifacts/bootstrap/ci-bootstrap + - pwsh: | + ./eng/test-build-correctness.ps1 -ci -configuration Release -enableDumps -bootstrapDir '$(Build.SourcesDirectory)/artifacts/bootstrap/ci-bootstrap' displayName: Build - Validate correctness - template: publish-logs.yml diff --git a/eng/targets/Imports.BeforeArcade.targets b/eng/targets/Imports.BeforeArcade.targets index 0ad98f1dbfe29..b24d5c05bc401 100644 --- a/eng/targets/Imports.BeforeArcade.targets +++ b/eng/targets/Imports.BeforeArcade.targets @@ -13,16 +13,6 @@ $(AssemblyName).$(PlatformTarget).Symbols - - - - 8.0.0 - - - 8.0.0 - - - false diff --git a/eng/targets/TargetFrameworks.props b/eng/targets/TargetFrameworks.props index a7643fcbcfd9e..20b3ce78a455f 100644 --- a/eng/targets/TargetFrameworks.props +++ b/eng/targets/TargetFrameworks.props @@ -8,14 +8,14 @@ Requirements: - NetVSShared must include both NetVS and NetVSCode - - NetRoslynSourceBuild must include NetRoslynToolset + - NetRoslynSourceBuild must include NetRoslyn - NetRoslynAll must include all .NET Core TFMS in any property below --> net8.0 net7.0;net8.0 net8.0 - net7.0 + net8.0 net7.0;net8.0 net6.0 @@ -36,7 +36,7 @@ --> - $(NetPrevious) + $(NetPrevious) $(NetCurrent);$(NetPrevious) $(NetCurrent);$(NetPrevious) $(NetPrevious) @@ -48,7 +48,7 @@ --> - $(NetCurrent) + $(NetCurrent) $(NetCurrent);$(NetPrevious) $(NetCurrent);$(NetPrevious) $(NetCurrent) @@ -62,7 +62,7 @@ --> - net8.0 + net8.0 net7.0;net8.0 diff --git a/eng/test-build-correctness.ps1 b/eng/test-build-correctness.ps1 index 0e13c46e8f4f1..ca0bd28d8c718 100644 --- a/eng/test-build-correctness.ps1 +++ b/eng/test-build-correctness.ps1 @@ -33,6 +33,7 @@ try { . (Join-Path $PSScriptRoot "build-utils.ps1") Push-Location $RepoRoot + $prepareMachine = $ci if ($enableDumps) { $key = "HKLM:\\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps" @@ -44,8 +45,8 @@ try { if ($bootstrapDir -eq "") { Write-Host "Building bootstrap compiler" - Exec-Script (Join-Path $PSScriptRoot "make-bootstrap.ps1") "-name correctness -ci:$ci" $bootstrapDir = Join-Path $ArtifactsDir "bootstrap" "correctness" + Exec-Script (Join-Path $PSScriptRoot "make-bootstrap.ps1") "-output $bootstrapDir -ci:$ci" } Write-Host "Building Roslyn" @@ -62,17 +63,17 @@ try { # Verify the state of our generated syntax files Write-Host "Checking generated compiler files" Exec-Script (Join-Path $PSScriptRoot "generate-compiler-code.ps1") "-test -configuration:$configuration" - Exec-Command dotnet "tool run dotnet-format whitespace . --folder --include-generated --include src/Compilers/CSharp/Portable/Generated/ src/Compilers/VisualBasic/Portable/Generated/ src/ExpressionEvaluator/VisualBasic/Source/ResultProvider/Generated/ --verify-no-changes" + Exec-DotNet "tool run dotnet-format whitespace . --folder --include-generated --include src/Compilers/CSharp/Portable/Generated/ src/Compilers/VisualBasic/Portable/Generated/ src/ExpressionEvaluator/VisualBasic/Source/ResultProvider/Generated/ --verify-no-changes" Write-Host "" - exit 0 + ExitWithExitCode 0 } catch { Write-Host $_ Write-Host $_.Exception Write-Host $_.ScriptStackTrace Write-Host "##vso[task.logissue type=error]How to investigate bootstrap failures: https://github.com/dotnet/roslyn/blob/main/docs/compilers/Bootstrap%20Builds.md#Investigating" - exit 1 + ExitWithExitCode 1 } finally { if ($enableDumps) { diff --git a/eng/test-determinism.ps1 b/eng/test-determinism.ps1 index 9643e4b60729f..da25ef2586abf 100644 --- a/eng/test-determinism.ps1 +++ b/eng/test-determinism.ps1 @@ -285,8 +285,8 @@ try { if ($bootstrapDir -eq "") { Write-Host "Building bootstrap compiler" - Exec-Script (Join-Path $PSScriptRoot "make-bootstrap.ps1") "-name determinism -ci:$ci" $bootstrapDir = Join-Path $ArtifactsDir "bootstrap" "determinism" + Exec-Script (Join-Path $PSScriptRoot "make-bootstrap.ps1") "-output $bootstrapDir -ci:$ci" } Run-Test diff --git a/eng/validate-code-formatting.ps1 b/eng/validate-code-formatting.ps1 index 89ef02ba22a86..fcfc7327dbcba 100644 --- a/eng/validate-code-formatting.ps1 +++ b/eng/validate-code-formatting.ps1 @@ -1,6 +1,7 @@ param ( [string]$rootDirectory, - [string[]]$includeDirectories + [string[]]$includeDirectories, + [switch]$ci = $false ) Set-StrictMode -version 2.0 $ErrorActionPreference="Stop" @@ -8,14 +9,15 @@ $ErrorActionPreference="Stop" try { . (Join-Path $PSScriptRoot "build-utils.ps1") Push-Location $RepoRoot + $prepareMachine = $ci Exec-DotNet "tool run dotnet-format -v detailed whitespace $rootDirectory --folder --include-generated --include $includeDirectories --verify-no-changes" - exit 0 + ExitWithExitCode 0 } catch { Write-Host $_ - exit 1 + ExitWithExitCode 1 } finally { Pop-Location diff --git a/global.json b/global.json index 5db0afcd2453a..5d9202097815b 100644 --- a/global.json +++ b/global.json @@ -1,19 +1,19 @@ { "sdk": { - "version": "8.0.100", + "version": "8.0.101", "allowPrerelease": false, "rollForward": "patch" }, "tools": { - "dotnet": "8.0.100", + "dotnet": "8.0.101", "vs": { "version": "17.8.0" }, "xcopy-msbuild": "17.8.1-2" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.24059.4", - "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.24059.4", + "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.24113.2", + "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.24113.2", "Microsoft.Build.Traversal": "3.4.0" } } diff --git a/src/Analyzers/CSharp/Analyzers/AddAccessibilityModifiers/CSharpAddAccessibilityModifiers.cs b/src/Analyzers/CSharp/Analyzers/AddAccessibilityModifiers/CSharpAddAccessibilityModifiers.cs index 953e5ec2d995f..daeeaea7ec829 100644 --- a/src/Analyzers/CSharp/Analyzers/AddAccessibilityModifiers/CSharpAddAccessibilityModifiers.cs +++ b/src/Analyzers/CSharp/Analyzers/AddAccessibilityModifiers/CSharpAddAccessibilityModifiers.cs @@ -9,86 +9,85 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.AddAccessibilityModifiers +namespace Microsoft.CodeAnalysis.CSharp.AddAccessibilityModifiers; + +internal class CSharpAddAccessibilityModifiers : AbstractAddAccessibilityModifiers { - internal class CSharpAddAccessibilityModifiers : AbstractAddAccessibilityModifiers + public static readonly CSharpAddAccessibilityModifiers Instance = new(); + + protected CSharpAddAccessibilityModifiers() { - public static readonly CSharpAddAccessibilityModifiers Instance = new(); + } - protected CSharpAddAccessibilityModifiers() - { - } + public override bool ShouldUpdateAccessibilityModifier( + IAccessibilityFacts accessibilityFacts, + MemberDeclarationSyntax member, + AccessibilityModifiersRequired option, + out SyntaxToken name, + out bool modifierAdded) + { + modifierAdded = false; - public override bool ShouldUpdateAccessibilityModifier( - IAccessibilityFacts accessibilityFacts, - MemberDeclarationSyntax member, - AccessibilityModifiersRequired option, - out SyntaxToken name, - out bool modifierAdded) - { - modifierAdded = false; + // Have to have a name to report the issue on. + name = member.GetNameToken(); + if (name.Kind() == SyntaxKind.None) + return false; - // Have to have a name to report the issue on. - name = member.GetNameToken(); - if (name.Kind() == SyntaxKind.None) - return false; + // Certain members never have accessibility. Don't bother reporting on them. + if (!accessibilityFacts.CanHaveAccessibility(member)) + return false; - // Certain members never have accessibility. Don't bother reporting on them. - if (!accessibilityFacts.CanHaveAccessibility(member)) - return false; + // This analyzer bases all of its decisions on the accessibility + var accessibility = accessibilityFacts.GetAccessibility(member); - // This analyzer bases all of its decisions on the accessibility - var accessibility = accessibilityFacts.GetAccessibility(member); + // Omit will flag any accessibility values that exist and are default + // The other options will remove or ignore accessibility + var isOmit = option == AccessibilityModifiersRequired.OmitIfDefault; + modifierAdded = !isOmit; - // Omit will flag any accessibility values that exist and are default - // The other options will remove or ignore accessibility - var isOmit = option == AccessibilityModifiersRequired.OmitIfDefault; - modifierAdded = !isOmit; + if (isOmit) + { + if (accessibility == Accessibility.NotApplicable) + return false; - if (isOmit) + var parentKind = member.GetRequiredParent().Kind(); + switch (parentKind) { - if (accessibility == Accessibility.NotApplicable) - return false; - - var parentKind = member.GetRequiredParent().Kind(); - switch (parentKind) - { - // Check for default modifiers in namespace and outside of namespace - case SyntaxKind.CompilationUnit: - case SyntaxKind.FileScopedNamespaceDeclaration: - case SyntaxKind.NamespaceDeclaration: - { - // Default is internal - if (accessibility != Accessibility.Internal) - return false; - } - - break; - - case SyntaxKind.ClassDeclaration: - case SyntaxKind.RecordDeclaration: - case SyntaxKind.StructDeclaration: - case SyntaxKind.RecordStructDeclaration: - { - // Inside a type, default is private - if (accessibility != Accessibility.Private) - return false; - } - - break; - - default: - return false; // Unknown parent kind, don't do anything - } - } - else - { - // Mode is always, so we have to flag missing modifiers - if (accessibility != Accessibility.NotApplicable) - return false; - } + // Check for default modifiers in namespace and outside of namespace + case SyntaxKind.CompilationUnit: + case SyntaxKind.FileScopedNamespaceDeclaration: + case SyntaxKind.NamespaceDeclaration: + { + // Default is internal + if (accessibility != Accessibility.Internal) + return false; + } + + break; + + case SyntaxKind.ClassDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.RecordStructDeclaration: + { + // Inside a type, default is private + if (accessibility != Accessibility.Private) + return false; + } + + break; - return true; + default: + return false; // Unknown parent kind, don't do anything + } } + else + { + // Mode is always, so we have to flag missing modifiers + if (accessibility != Accessibility.NotApplicable) + return false; + } + + return true; } } diff --git a/src/Analyzers/CSharp/Analyzers/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersDiagnosticAnalyzer.cs index 01ca82c1fa73c..44ba3293252da 100644 --- a/src/Analyzers/CSharp/Analyzers/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersDiagnosticAnalyzer.cs @@ -11,73 +11,73 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.AddAccessibilityModifiers +namespace Microsoft.CodeAnalysis.CSharp.AddAccessibilityModifiers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpAddAccessibilityModifiersDiagnosticAnalyzer + : AbstractAddAccessibilityModifiersDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpAddAccessibilityModifiersDiagnosticAnalyzer - : AbstractAddAccessibilityModifiersDiagnosticAnalyzer + protected override void ProcessCompilationUnit( + SyntaxTreeAnalysisContext context, + CodeStyleOption2 option, CompilationUnitSyntax compilationUnit) { - protected override void ProcessCompilationUnit( - SyntaxTreeAnalysisContext context, - CodeStyleOption2 option, CompilationUnitSyntax compilationUnit) - { - ProcessMembers(context, option, compilationUnit.Members); - } + ProcessMembers(context, option, compilationUnit.Members); + } - private void ProcessMembers( - SyntaxTreeAnalysisContext context, - CodeStyleOption2 option, - SyntaxList members) - { - foreach (var memberDeclaration in members) - ProcessMemberDeclaration(context, option, memberDeclaration); - } + private void ProcessMembers( + SyntaxTreeAnalysisContext context, + CodeStyleOption2 option, + SyntaxList members) + { + foreach (var memberDeclaration in members) + ProcessMemberDeclaration(context, option, memberDeclaration); + } - private void ProcessMemberDeclaration( - SyntaxTreeAnalysisContext context, - CodeStyleOption2 option, MemberDeclarationSyntax member) - { - if (!context.ShouldAnalyzeSpan(member.Span)) - return; + private void ProcessMemberDeclaration( + SyntaxTreeAnalysisContext context, + CodeStyleOption2 option, MemberDeclarationSyntax member) + { + if (!context.ShouldAnalyzeSpan(member.Span)) + return; - if (member is BaseNamespaceDeclarationSyntax namespaceDeclaration) - ProcessMembers(context, option, namespaceDeclaration.Members); + if (member is BaseNamespaceDeclarationSyntax namespaceDeclaration) + ProcessMembers(context, option, namespaceDeclaration.Members); - // If we have a class or struct, recurse inwards. - if (member is TypeDeclarationSyntax( - SyntaxKind.ClassDeclaration or - SyntaxKind.StructDeclaration or - SyntaxKind.RecordDeclaration or - SyntaxKind.RecordStructDeclaration) typeDeclaration) - { - ProcessMembers(context, option, typeDeclaration.Members); - } + // If we have a class or struct, recurse inwards. + if (member is TypeDeclarationSyntax( + SyntaxKind.ClassDeclaration or + SyntaxKind.StructDeclaration or + SyntaxKind.RecordDeclaration or + SyntaxKind.RecordStructDeclaration) typeDeclaration) + { + ProcessMembers(context, option, typeDeclaration.Members); + } #if false - // Add this once we have the language version for C# that supports accessibility - // modifiers on interface methods. - if (option.Value == AccessibilityModifiersRequired.Always && - member.IsKind(SyntaxKind.InterfaceDeclaration, out typeDeclaration)) - { - // Only recurse into an interface if the user wants accessibility modifiers on - ProcessTypeDeclaration(context, generator, option, typeDeclaration); - } + // Add this once we have the language version for C# that supports accessibility + // modifiers on interface methods. + if (option.Value == AccessibilityModifiersRequired.Always && + member.IsKind(SyntaxKind.InterfaceDeclaration, out typeDeclaration)) + { + // Only recurse into an interface if the user wants accessibility modifiers on + ProcessTypeDeclaration(context, generator, option, typeDeclaration); + } #endif - if (!CSharpAddAccessibilityModifiers.Instance.ShouldUpdateAccessibilityModifier( - CSharpAccessibilityFacts.Instance, member, option.Value, out var name, out var modifiersAdded)) - { - return; - } - - // Have an issue to flag, either add or remove. Report issue to user. - var additionalLocations = ImmutableArray.Create(member.GetLocation()); - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - name.GetLocation(), - option.Notification, - additionalLocations: additionalLocations, - modifiersAdded ? ModifiersAddedProperties : null)); + if (!CSharpAddAccessibilityModifiers.Instance.ShouldUpdateAccessibilityModifier( + CSharpAccessibilityFacts.Instance, member, option.Value, out var name, out var modifiersAdded)) + { + return; } + + // Have an issue to flag, either add or remove. Report issue to user. + var additionalLocations = ImmutableArray.Create(member.GetLocation()); + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + name.GetLocation(), + option.Notification, + context.Options, + additionalLocations: additionalLocations, + modifiersAdded ? ModifiersAddedProperties : null)); } } diff --git a/src/Analyzers/CSharp/Analyzers/AddBraces/CSharpAddBracesDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/AddBraces/CSharpAddBracesDiagnosticAnalyzer.cs index ba15457d6b539..f5abee1dd2a0c 100644 --- a/src/Analyzers/CSharp/Analyzers/AddBraces/CSharpAddBracesDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/AddBraces/CSharpAddBracesDiagnosticAnalyzer.cs @@ -14,292 +14,292 @@ using Roslyn.Utilities; using FormattingRangeHelper = Microsoft.CodeAnalysis.CSharp.Utilities.FormattingRangeHelper; -namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.AddBraces +namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.AddBraces; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpAddBracesDiagnosticAnalyzer : + AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpAddBracesDiagnosticAnalyzer : - AbstractBuiltInCodeStyleDiagnosticAnalyzer + public CSharpAddBracesDiagnosticAnalyzer() + : base(IDEDiagnosticIds.AddBracesDiagnosticId, + EnforceOnBuildValues.AddBraces, + CSharpCodeStyleOptions.PreferBraces, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Add_braces), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Add_braces_to_0_statement), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + { + } + + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeNode, + SyntaxKind.IfStatement, + SyntaxKind.ElseClause, + SyntaxKind.ForStatement, + SyntaxKind.ForEachStatement, + SyntaxKind.ForEachVariableStatement, + SyntaxKind.WhileStatement, + SyntaxKind.DoStatement, + SyntaxKind.UsingStatement, + SyntaxKind.LockStatement, + SyntaxKind.FixedStatement); + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + public void AnalyzeNode(SyntaxNodeAnalysisContext context) { - public CSharpAddBracesDiagnosticAnalyzer() - : base(IDEDiagnosticIds.AddBracesDiagnosticId, - EnforceOnBuildValues.AddBraces, - CSharpCodeStyleOptions.PreferBraces, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Add_braces), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Add_braces_to_0_statement), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + var statement = context.Node; + + var option = context.GetCSharpAnalyzerOptions().PreferBraces; + if (option.Value == PreferBracesPreference.None || + ShouldSkipAnalysis(context, option.Notification)) { + return; } - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeNode, - SyntaxKind.IfStatement, - SyntaxKind.ElseClause, - SyntaxKind.ForStatement, - SyntaxKind.ForEachStatement, - SyntaxKind.ForEachVariableStatement, - SyntaxKind.WhileStatement, - SyntaxKind.DoStatement, - SyntaxKind.UsingStatement, - SyntaxKind.LockStatement, - SyntaxKind.FixedStatement); - - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - - public void AnalyzeNode(SyntaxNodeAnalysisContext context) - { - var statement = context.Node; + var embeddedStatement = statement.GetEmbeddedStatement(); + Contract.ThrowIfNull(embeddedStatement); - var option = context.GetCSharpAnalyzerOptions().PreferBraces; - if (option.Value == PreferBracesPreference.None || - ShouldSkipAnalysis(context, option.Notification)) - { + switch (embeddedStatement.Kind()) + { + case SyntaxKind.Block: + // The embedded statement already has braces, which is always allowed. return; - } - var embeddedStatement = statement.GetEmbeddedStatement(); - Contract.ThrowIfNull(embeddedStatement); + case SyntaxKind.IfStatement when statement.Kind() == SyntaxKind.ElseClause: + // Constructs like the following are always allowed: + // + // if (something) + // { + // } + // else if (somethingElse) // <-- 'if' nested in an 'else' clause + // { + // } + return; - switch (embeddedStatement.Kind()) - { - case SyntaxKind.Block: - // The embedded statement already has braces, which is always allowed. + case SyntaxKind.LockStatement: + case SyntaxKind.UsingStatement: + case SyntaxKind.FixedStatement: + // If we have something like this: + // + // using (...) + // using (...) + // { + // } + // + // The first statement needs no block as it formatted with the same indentation. + if (statement.Kind() == embeddedStatement.Kind()) + { return; + } - case SyntaxKind.IfStatement when statement.Kind() == SyntaxKind.ElseClause: - // Constructs like the following are always allowed: - // - // if (something) - // { - // } - // else if (somethingElse) // <-- 'if' nested in an 'else' clause - // { - // } - return; + break; + } - case SyntaxKind.LockStatement: - case SyntaxKind.UsingStatement: - case SyntaxKind.FixedStatement: - // If we have something like this: - // - // using (...) - // using (...) - // { - // } - // - // The first statement needs no block as it formatted with the same indentation. - if (statement.Kind() == embeddedStatement.Kind()) - { - return; - } + if (option.Value == PreferBracesPreference.WhenMultiline + && !IsConsideredMultiLine(statement, embeddedStatement) + && !RequiresBracesToMatchContext(statement)) + { + return; + } - break; - } + if (ContainsInterleavedDirective(statement, embeddedStatement, context.CancellationToken)) + { + return; + } - if (option.Value == PreferBracesPreference.WhenMultiline - && !IsConsideredMultiLine(statement, embeddedStatement) - && !RequiresBracesToMatchContext(statement)) - { - return; - } + var firstToken = statement.GetFirstToken(); + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + firstToken.GetLocation(), + option.Notification, + context.Options, + additionalLocations: null, + properties: null, + SyntaxFacts.GetText(firstToken.Kind()))); + } - if (ContainsInterleavedDirective(statement, embeddedStatement, context.CancellationToken)) + /// + /// Check if there are interleaved directives on the statement. + /// Handles special case with if/else. + /// + private static bool ContainsInterleavedDirective(SyntaxNode statement, StatementSyntax embeddedStatement, CancellationToken cancellationToken) + { + if (statement is IfStatementSyntax ifStatementNode) + { + var elseNode = ifStatementNode.Else; + if (elseNode != null && !embeddedStatement.IsMissing) { - return; + // For IF/ELSE statements, only the IF part should be checked for interleaved directives when the diagnostic is triggered on the IF. + // A separate diagnostic will be triggered to handle the ELSE part. + var ifStatementSpanWithoutElse = TextSpan.FromBounds(statement.Span.Start, embeddedStatement.Span.End); + return statement.ContainsInterleavedDirective(ifStatementSpanWithoutElse, cancellationToken); } - - var firstToken = statement.GetFirstToken(); - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - firstToken.GetLocation(), - option.Notification, - additionalLocations: null, - properties: null, - SyntaxFacts.GetText(firstToken.Kind()))); } - /// - /// Check if there are interleaved directives on the statement. - /// Handles special case with if/else. - /// - private static bool ContainsInterleavedDirective(SyntaxNode statement, StatementSyntax embeddedStatement, CancellationToken cancellationToken) + return statement.ContainsInterleavedDirective(cancellationToken); + } + + /// + /// In general, statements are considered multiline if any of the following span more than one line: + /// + /// The part of the statement preceding the embedded statement + /// The embedded statement itself + /// The part of the statement following the embedded statement, for example the + /// while (...); portion of a do ... while (...); statement + /// + /// The third condition is not checked for else clauses because they are only considered multiline + /// when their embedded statement is multiline. + /// + private static bool IsConsideredMultiLine(SyntaxNode statement, SyntaxNode embeddedStatement) + { + // Early return if syntax errors prevent analysis + if (embeddedStatement.IsMissing) { - if (statement is IfStatementSyntax ifStatementNode) - { - var elseNode = ifStatementNode.Else; - if (elseNode != null && !embeddedStatement.IsMissing) - { - // For IF/ELSE statements, only the IF part should be checked for interleaved directives when the diagnostic is triggered on the IF. - // A separate diagnostic will be triggered to handle the ELSE part. - var ifStatementSpanWithoutElse = TextSpan.FromBounds(statement.Span.Start, embeddedStatement.Span.End); - return statement.ContainsInterleavedDirective(ifStatementSpanWithoutElse, cancellationToken); - } - } + // The embedded statement was added by the compiler during recovery from a syntax error + return false; + } - return statement.ContainsInterleavedDirective(cancellationToken); + // Early return if the entire statement fits on one line + if (FormattingRangeHelper.AreTwoTokensOnSameLine(statement.GetFirstToken(), statement.GetLastToken())) + { + // The entire statement fits on one line. Examples: + // + // if (something) return; + // + // while (true) something(); + return false; } - /// - /// In general, statements are considered multiline if any of the following span more than one line: - /// - /// The part of the statement preceding the embedded statement - /// The embedded statement itself - /// The part of the statement following the embedded statement, for example the - /// while (...); portion of a do ... while (...); statement - /// - /// The third condition is not checked for else clauses because they are only considered multiline - /// when their embedded statement is multiline. - /// - private static bool IsConsideredMultiLine(SyntaxNode statement, SyntaxNode embeddedStatement) + // Check the part of the statement preceding the embedded statement (bullet 1) + var lastTokenBeforeEmbeddedStatement = embeddedStatement.GetFirstToken().GetPreviousToken(); + if (!FormattingRangeHelper.AreTwoTokensOnSameLine(statement.GetFirstToken(), lastTokenBeforeEmbeddedStatement)) { - // Early return if syntax errors prevent analysis - if (embeddedStatement.IsMissing) - { - // The embedded statement was added by the compiler during recovery from a syntax error - return false; - } + // The part of the statement preceding the embedded statement does not fit on one line. Examples: + // + // for (int i = 0; // <-- The initializer/condition/increment are on separate lines + // i < 10; + // i++) + // SomeMethod(); + return true; + } - // Early return if the entire statement fits on one line - if (FormattingRangeHelper.AreTwoTokensOnSameLine(statement.GetFirstToken(), statement.GetLastToken())) - { - // The entire statement fits on one line. Examples: - // - // if (something) return; - // - // while (true) something(); - return false; - } + // Check the embedded statement itself (bullet 2) + if (!FormattingRangeHelper.AreTwoTokensOnSameLine(embeddedStatement.GetFirstToken(), embeddedStatement.GetLastToken())) + { + // The embedded statement does not fit on one line. Examples: + // + // if (something) + // obj.Method( // <-- This embedded statement spans two lines. + // arg); + return true; + } - // Check the part of the statement preceding the embedded statement (bullet 1) - var lastTokenBeforeEmbeddedStatement = embeddedStatement.GetFirstToken().GetPreviousToken(); - if (!FormattingRangeHelper.AreTwoTokensOnSameLine(statement.GetFirstToken(), lastTokenBeforeEmbeddedStatement)) + // Check the part of the statement following the embedded statement, but only if it exists and is not an + // 'else' clause (bullet 3) + if (statement.GetLastToken() != embeddedStatement.GetLastToken()) + { + if (statement is IfStatementSyntax ifStatement && ifStatement.Statement == embeddedStatement) { - // The part of the statement preceding the embedded statement does not fit on one line. Examples: + // The embedded statement is followed by an 'else' clause, which may span multiple lines without + // triggering a braces requirement, such as this: // - // for (int i = 0; // <-- The initializer/condition/increment are on separate lines - // i < 10; - // i++) - // SomeMethod(); - return true; - } - - // Check the embedded statement itself (bullet 2) - if (!FormattingRangeHelper.AreTwoTokensOnSameLine(embeddedStatement.GetFirstToken(), embeddedStatement.GetLastToken())) - { - // The embedded statement does not fit on one line. Examples: + // if (true) + // return; + // else // <-- this else clause is two lines, but is not considered a multiline context + // return; // - // if (something) - // obj.Method( // <-- This embedded statement spans two lines. - // arg); - return true; + // --- + // INTENTIONAL FALLTHROUGH } - - // Check the part of the statement following the embedded statement, but only if it exists and is not an - // 'else' clause (bullet 3) - if (statement.GetLastToken() != embeddedStatement.GetLastToken()) + else { - if (statement is IfStatementSyntax ifStatement && ifStatement.Statement == embeddedStatement) + var firstTokenAfterEmbeddedStatement = embeddedStatement.GetLastToken().GetNextToken(); + if (!FormattingRangeHelper.AreTwoTokensOnSameLine(firstTokenAfterEmbeddedStatement, statement.GetLastToken())) { - // The embedded statement is followed by an 'else' clause, which may span multiple lines without - // triggering a braces requirement, such as this: - // - // if (true) - // return; - // else // <-- this else clause is two lines, but is not considered a multiline context - // return; + // The part of the statement following the embedded statement does not fit on one line. Examples: // - // --- - // INTENTIONAL FALLTHROUGH - } - else - { - var firstTokenAfterEmbeddedStatement = embeddedStatement.GetLastToken().GetNextToken(); - if (!FormattingRangeHelper.AreTwoTokensOnSameLine(firstTokenAfterEmbeddedStatement, statement.GetLastToken())) - { - // The part of the statement following the embedded statement does not fit on one line. Examples: - // - // do - // SomeMethod(); - // while (x < 0 || // <-- This condition is split across multiple lines. - // x > 10); - return true; - } + // do + // SomeMethod(); + // while (x < 0 || // <-- This condition is split across multiple lines. + // x > 10); + return true; } } + } + + return false; + } + /// + /// Determines whether should use braces under a + /// preference due to the presence of braces on one or more + /// sibling statements (the "context"). + /// + private static bool RequiresBracesToMatchContext(SyntaxNode statement) + { + if (statement.Kind() is not (SyntaxKind.IfStatement or SyntaxKind.ElseClause)) + { + // 'if' statements are the only statements that can have multiple embedded statements which are + // considered relative to each other. return false; } - /// - /// Determines whether should use braces under a - /// preference due to the presence of braces on one or more - /// sibling statements (the "context"). - /// - private static bool RequiresBracesToMatchContext(SyntaxNode statement) + var outermostIfStatement = GetOutermostIfStatementOfSequence(statement); + if (AnyPartOfIfSequenceUsesBraces(outermostIfStatement)) { - if (statement.Kind() is not (SyntaxKind.IfStatement or SyntaxKind.ElseClause)) - { - // 'if' statements are the only statements that can have multiple embedded statements which are - // considered relative to each other. - return false; - } + return true; + } - var outermostIfStatement = GetOutermostIfStatementOfSequence(statement); - if (AnyPartOfIfSequenceUsesBraces(outermostIfStatement)) - { - return true; - } + return false; + } - return false; + /// + /// Gets the top-most for which is + /// part of the if/else if/else sequence. + /// + /// + /// For the purpose of brace usage analysis, the embedded statements of an if/else if/else + /// sequence are considered sibling statements, even though they don't appear as immediate siblings in the + /// syntax tree. This method walks up the syntax tree to find the if statement that starts the + /// sequence. + /// + private static IfStatementSyntax GetOutermostIfStatementOfSequence(SyntaxNode ifStatementOrElseClause) + { + IfStatementSyntax result; + if (ifStatementOrElseClause.IsKind(SyntaxKind.ElseClause)) + { + result = (IfStatementSyntax)ifStatementOrElseClause.GetRequiredParent(); } - - /// - /// Gets the top-most for which is - /// part of the if/else if/else sequence. - /// - /// - /// For the purpose of brace usage analysis, the embedded statements of an if/else if/else - /// sequence are considered sibling statements, even though they don't appear as immediate siblings in the - /// syntax tree. This method walks up the syntax tree to find the if statement that starts the - /// sequence. - /// - private static IfStatementSyntax GetOutermostIfStatementOfSequence(SyntaxNode ifStatementOrElseClause) + else { - IfStatementSyntax result; - if (ifStatementOrElseClause.IsKind(SyntaxKind.ElseClause)) - { - result = (IfStatementSyntax)ifStatementOrElseClause.GetRequiredParent(); - } - else - { - Debug.Assert(ifStatementOrElseClause.IsKind(SyntaxKind.IfStatement)); - result = (IfStatementSyntax)ifStatementOrElseClause; - } + Debug.Assert(ifStatementOrElseClause.IsKind(SyntaxKind.IfStatement)); + result = (IfStatementSyntax)ifStatementOrElseClause; + } - while (result.IsParentKind(SyntaxKind.ElseClause)) - result = (IfStatementSyntax)result.GetRequiredParent().GetRequiredParent(); + while (result.IsParentKind(SyntaxKind.ElseClause)) + result = (IfStatementSyntax)result.GetRequiredParent().GetRequiredParent(); - return result; - } + return result; + } - /// - /// Determines if any embedded statement of an if/else if/else sequence uses braces. Only - /// the embedded statements falling immediately under one of these nodes are checked. - /// - private static bool AnyPartOfIfSequenceUsesBraces(IfStatementSyntax? statement) + /// + /// Determines if any embedded statement of an if/else if/else sequence uses braces. Only + /// the embedded statements falling immediately under one of these nodes are checked. + /// + private static bool AnyPartOfIfSequenceUsesBraces(IfStatementSyntax? statement) + { + // Iterative instead of recursive to avoid stack depth problems + while (statement != null) { - // Iterative instead of recursive to avoid stack depth problems - while (statement != null) - { - if (statement.Statement.IsKind(SyntaxKind.Block)) - return true; - - var elseStatement = statement.Else?.Statement; - if (elseStatement.IsKind(SyntaxKind.Block)) - return true; + if (statement.Statement.IsKind(SyntaxKind.Block)) + return true; - statement = elseStatement as IfStatementSyntax; - } + var elseStatement = statement.Else?.Statement; + if (elseStatement.IsKind(SyntaxKind.Block)) + return true; - return false; + statement = elseStatement as IfStatementSyntax; } + + return false; } } diff --git a/src/Analyzers/CSharp/Analyzers/AddRequiredParentheses/CSharpAddRequiredExpressionParenthesesDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/AddRequiredParentheses/CSharpAddRequiredExpressionParenthesesDiagnosticAnalyzer.cs index 32fc1f059bb92..4979dc2c8f9ea 100644 --- a/src/Analyzers/CSharp/Analyzers/AddRequiredParentheses/CSharpAddRequiredExpressionParenthesesDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/AddRequiredParentheses/CSharpAddRequiredExpressionParenthesesDiagnosticAnalyzer.cs @@ -11,73 +11,72 @@ using Microsoft.CodeAnalysis.Diagnostics; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.AddRequiredParentheses +namespace Microsoft.CodeAnalysis.CSharp.AddRequiredParentheses; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpAddRequiredExpressionParenthesesDiagnosticAnalyzer : + AbstractAddRequiredParenthesesDiagnosticAnalyzer< + ExpressionSyntax, ExpressionSyntax, SyntaxKind> { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpAddRequiredExpressionParenthesesDiagnosticAnalyzer : - AbstractAddRequiredParenthesesDiagnosticAnalyzer< - ExpressionSyntax, ExpressionSyntax, SyntaxKind> + public CSharpAddRequiredExpressionParenthesesDiagnosticAnalyzer() + : base(CSharpExpressionPrecedenceService.Instance) { - public CSharpAddRequiredExpressionParenthesesDiagnosticAnalyzer() - : base(CSharpExpressionPrecedenceService.Instance) - { - } + } - private static readonly ImmutableArray s_kinds = - [ - SyntaxKind.AddExpression, - SyntaxKind.SubtractExpression, - SyntaxKind.MultiplyExpression, - SyntaxKind.DivideExpression, - SyntaxKind.ModuloExpression, - SyntaxKind.LeftShiftExpression, - SyntaxKind.RightShiftExpression, - SyntaxKind.LogicalOrExpression, - SyntaxKind.LogicalAndExpression, - SyntaxKind.BitwiseOrExpression, - SyntaxKind.BitwiseAndExpression, - SyntaxKind.ExclusiveOrExpression, - SyntaxKind.EqualsExpression, - SyntaxKind.NotEqualsExpression, - SyntaxKind.LessThanExpression, - SyntaxKind.LessThanOrEqualExpression, - SyntaxKind.GreaterThanExpression, - SyntaxKind.GreaterThanOrEqualExpression, - SyntaxKind.IsExpression, - SyntaxKind.AsExpression, - SyntaxKind.CoalesceExpression, - SyntaxKind.IsPatternExpression, - ]; + private static readonly ImmutableArray s_kinds = + [ + SyntaxKind.AddExpression, + SyntaxKind.SubtractExpression, + SyntaxKind.MultiplyExpression, + SyntaxKind.DivideExpression, + SyntaxKind.ModuloExpression, + SyntaxKind.LeftShiftExpression, + SyntaxKind.RightShiftExpression, + SyntaxKind.LogicalOrExpression, + SyntaxKind.LogicalAndExpression, + SyntaxKind.BitwiseOrExpression, + SyntaxKind.BitwiseAndExpression, + SyntaxKind.ExclusiveOrExpression, + SyntaxKind.EqualsExpression, + SyntaxKind.NotEqualsExpression, + SyntaxKind.LessThanExpression, + SyntaxKind.LessThanOrEqualExpression, + SyntaxKind.GreaterThanExpression, + SyntaxKind.GreaterThanOrEqualExpression, + SyntaxKind.IsExpression, + SyntaxKind.AsExpression, + SyntaxKind.CoalesceExpression, + SyntaxKind.IsPatternExpression, + ]; - protected override ImmutableArray GetSyntaxNodeKinds() - => s_kinds; + protected override ImmutableArray GetSyntaxNodeKinds() + => s_kinds; - protected override int GetPrecedence(ExpressionSyntax binaryLike) - => (int)binaryLike.GetOperatorPrecedence(); + protected override int GetPrecedence(ExpressionSyntax binaryLike) + => (int)binaryLike.GetOperatorPrecedence(); - protected override bool IsBinaryLike(ExpressionSyntax node) - => node is BinaryExpressionSyntax || - node is IsPatternExpressionSyntax isPattern && isPattern.Pattern is ConstantPatternSyntax; + protected override bool IsBinaryLike(ExpressionSyntax node) + => node is BinaryExpressionSyntax || + node is IsPatternExpressionSyntax isPattern && isPattern.Pattern is ConstantPatternSyntax; - protected override (ExpressionSyntax, SyntaxToken, ExpressionSyntax) GetPartsOfBinaryLike(ExpressionSyntax binaryLike) + protected override (ExpressionSyntax, SyntaxToken, ExpressionSyntax) GetPartsOfBinaryLike(ExpressionSyntax binaryLike) + { + Debug.Assert(IsBinaryLike(binaryLike)); + switch (binaryLike) { - Debug.Assert(IsBinaryLike(binaryLike)); - switch (binaryLike) - { - case BinaryExpressionSyntax binaryExpression: - return (binaryExpression.Left, binaryExpression.OperatorToken, binaryExpression.Right); + case BinaryExpressionSyntax binaryExpression: + return (binaryExpression.Left, binaryExpression.OperatorToken, binaryExpression.Right); - case IsPatternExpressionSyntax isPatternExpression: - return (isPatternExpression.Expression, isPatternExpression.IsKeyword, ((ConstantPatternSyntax)isPatternExpression.Pattern).Expression); + case IsPatternExpressionSyntax isPatternExpression: + return (isPatternExpression.Expression, isPatternExpression.IsKeyword, ((ConstantPatternSyntax)isPatternExpression.Pattern).Expression); - default: - throw ExceptionUtilities.UnexpectedValue(binaryLike); - } + default: + throw ExceptionUtilities.UnexpectedValue(binaryLike); } - - protected override ExpressionSyntax? TryGetAppropriateParent(ExpressionSyntax binaryLike) - => binaryLike.Parent is ConstantPatternSyntax - ? binaryLike.Parent.Parent as ExpressionSyntax - : binaryLike.Parent as ExpressionSyntax; } + + protected override ExpressionSyntax? TryGetAppropriateParent(ExpressionSyntax binaryLike) + => binaryLike.Parent is ConstantPatternSyntax + ? binaryLike.Parent.Parent as ExpressionSyntax + : binaryLike.Parent as ExpressionSyntax; } diff --git a/src/Analyzers/CSharp/Analyzers/AddRequiredParentheses/CSharpAddRequiredPatternParenthesesDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/AddRequiredParentheses/CSharpAddRequiredPatternParenthesesDiagnosticAnalyzer.cs index 1e0bd2ed565bd..a3729c9617d8f 100644 --- a/src/Analyzers/CSharp/Analyzers/AddRequiredParentheses/CSharpAddRequiredPatternParenthesesDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/AddRequiredParentheses/CSharpAddRequiredPatternParenthesesDiagnosticAnalyzer.cs @@ -10,36 +10,35 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.AddRequiredParentheses +namespace Microsoft.CodeAnalysis.CSharp.AddRequiredParentheses; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpAddRequiredPatternParenthesesDiagnosticAnalyzer : + AbstractAddRequiredParenthesesDiagnosticAnalyzer< + PatternSyntax, BinaryPatternSyntax, SyntaxKind> { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpAddRequiredPatternParenthesesDiagnosticAnalyzer : - AbstractAddRequiredParenthesesDiagnosticAnalyzer< - PatternSyntax, BinaryPatternSyntax, SyntaxKind> + public CSharpAddRequiredPatternParenthesesDiagnosticAnalyzer() + : base(CSharpPatternPrecedenceService.Instance) { - public CSharpAddRequiredPatternParenthesesDiagnosticAnalyzer() - : base(CSharpPatternPrecedenceService.Instance) - { - } - - private static readonly ImmutableArray s_kinds = [SyntaxKind.AndPattern, SyntaxKind.OrPattern]; + } - protected override ImmutableArray GetSyntaxNodeKinds() - => s_kinds; + private static readonly ImmutableArray s_kinds = [SyntaxKind.AndPattern, SyntaxKind.OrPattern]; - protected override int GetPrecedence(BinaryPatternSyntax pattern) - => (int)pattern.GetOperatorPrecedence(); + protected override ImmutableArray GetSyntaxNodeKinds() + => s_kinds; - protected override bool IsBinaryLike(PatternSyntax node) - => node is BinaryPatternSyntax; + protected override int GetPrecedence(BinaryPatternSyntax pattern) + => (int)pattern.GetOperatorPrecedence(); - protected override (PatternSyntax, SyntaxToken, PatternSyntax) GetPartsOfBinaryLike(BinaryPatternSyntax binaryPattern) - { - Debug.Assert(IsBinaryLike(binaryPattern)); - return (binaryPattern.Left, binaryPattern.OperatorToken, binaryPattern.Right); - } + protected override bool IsBinaryLike(PatternSyntax node) + => node is BinaryPatternSyntax; - protected override PatternSyntax? TryGetAppropriateParent(BinaryPatternSyntax binaryLike) - => binaryLike.Parent as PatternSyntax; + protected override (PatternSyntax, SyntaxToken, PatternSyntax) GetPartsOfBinaryLike(BinaryPatternSyntax binaryPattern) + { + Debug.Assert(IsBinaryLike(binaryPattern)); + return (binaryPattern.Left, binaryPattern.OperatorToken, binaryPattern.Right); } + + protected override PatternSyntax? TryGetAppropriateParent(BinaryPatternSyntax binaryLike) + => binaryLike.Parent as PatternSyntax; } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertNamespaceAnalysis.cs b/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertNamespaceAnalysis.cs index 4b7d04312d6bf..9f4356c16c022 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertNamespaceAnalysis.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertNamespaceAnalysis.cs @@ -10,77 +10,76 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace -{ - internal static class ConvertNamespaceAnalysis - { - public static (string title, string equivalenceKey) GetInfo(NamespaceDeclarationPreference preference) - => preference switch - { - NamespaceDeclarationPreference.BlockScoped => (CSharpAnalyzersResources.Convert_to_block_scoped_namespace, nameof(CSharpAnalyzersResources.Convert_to_block_scoped_namespace)), - NamespaceDeclarationPreference.FileScoped => (CSharpAnalyzersResources.Convert_to_file_scoped_namespace, nameof(CSharpAnalyzersResources.Convert_to_file_scoped_namespace)), - _ => throw ExceptionUtilities.UnexpectedValue(preference), - }; +namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace; - public static bool CanOfferUseBlockScoped(CodeStyleOption2 option, [NotNullWhen(true)] BaseNamespaceDeclarationSyntax? declaration, bool forAnalyzer) +internal static class ConvertNamespaceAnalysis +{ + public static (string title, string equivalenceKey) GetInfo(NamespaceDeclarationPreference preference) + => preference switch { - if (declaration is not FileScopedNamespaceDeclarationSyntax) - return false; + NamespaceDeclarationPreference.BlockScoped => (CSharpAnalyzersResources.Convert_to_block_scoped_namespace, nameof(CSharpAnalyzersResources.Convert_to_block_scoped_namespace)), + NamespaceDeclarationPreference.FileScoped => (CSharpAnalyzersResources.Convert_to_file_scoped_namespace, nameof(CSharpAnalyzersResources.Convert_to_file_scoped_namespace)), + _ => throw ExceptionUtilities.UnexpectedValue(preference), + }; + + public static bool CanOfferUseBlockScoped(CodeStyleOption2 option, [NotNullWhen(true)] BaseNamespaceDeclarationSyntax? declaration, bool forAnalyzer) + { + if (declaration is not FileScopedNamespaceDeclarationSyntax) + return false; - var userPrefersRegularNamespaces = option.Value == NamespaceDeclarationPreference.BlockScoped; - var analyzerDisabled = option.Notification.Severity == ReportDiagnostic.Suppress; - var forRefactoring = !forAnalyzer; + var userPrefersRegularNamespaces = option.Value == NamespaceDeclarationPreference.BlockScoped; + var analyzerDisabled = option.Notification.Severity == ReportDiagnostic.Suppress; + var forRefactoring = !forAnalyzer; - // If the user likes regular namespaces, then we offer regular namespaces from the diagnostic analyzer. - // If the user does not like regular namespaces then we offer regular namespaces bodies from the refactoring provider. - // If the analyzer is disabled completely, the refactoring is enabled in both directions. - var canOffer = userPrefersRegularNamespaces == forAnalyzer || (forRefactoring && analyzerDisabled); - return canOffer; - } + // If the user likes regular namespaces, then we offer regular namespaces from the diagnostic analyzer. + // If the user does not like regular namespaces then we offer regular namespaces bodies from the refactoring provider. + // If the analyzer is disabled completely, the refactoring is enabled in both directions. + var canOffer = userPrefersRegularNamespaces == forAnalyzer || (forRefactoring && analyzerDisabled); + return canOffer; + } - internal static bool CanOfferUseFileScoped(CodeStyleOption2 option, CompilationUnitSyntax root, [NotNullWhen(true)] BaseNamespaceDeclarationSyntax? declaration, bool forAnalyzer) - => CanOfferUseFileScoped(option, root, declaration, forAnalyzer, root.SyntaxTree.Options.LanguageVersion()); + internal static bool CanOfferUseFileScoped(CodeStyleOption2 option, CompilationUnitSyntax root, [NotNullWhen(true)] BaseNamespaceDeclarationSyntax? declaration, bool forAnalyzer) + => CanOfferUseFileScoped(option, root, declaration, forAnalyzer, root.SyntaxTree.Options.LanguageVersion()); - internal static bool CanOfferUseFileScoped( - CodeStyleOption2 option, - CompilationUnitSyntax root, - BaseNamespaceDeclarationSyntax? declaration, - bool forAnalyzer, - LanguageVersion version) - { - if (declaration is not NamespaceDeclarationSyntax namespaceDeclaration) - return false; + internal static bool CanOfferUseFileScoped( + CodeStyleOption2 option, + CompilationUnitSyntax root, + BaseNamespaceDeclarationSyntax? declaration, + bool forAnalyzer, + LanguageVersion version) + { + if (declaration is not NamespaceDeclarationSyntax namespaceDeclaration) + return false; - if (namespaceDeclaration.OpenBraceToken.IsMissing) - return false; + if (namespaceDeclaration.OpenBraceToken.IsMissing) + return false; - if (version < LanguageVersion.CSharp10) - return false; + if (version < LanguageVersion.CSharp10) + return false; - var userPrefersFileScopedNamespaces = option.Value == NamespaceDeclarationPreference.FileScoped; - var analyzerDisabled = option.Notification.Severity == ReportDiagnostic.Suppress; - var forRefactoring = !forAnalyzer; + var userPrefersFileScopedNamespaces = option.Value == NamespaceDeclarationPreference.FileScoped; + var analyzerDisabled = option.Notification.Severity == ReportDiagnostic.Suppress; + var forRefactoring = !forAnalyzer; - // If the user likes file scoped namespaces, then we offer file scoped namespaces from the diagnostic analyzer. - // If the user does not like file scoped namespaces then we offer file scoped namespaces from the refactoring provider. - // If the analyzer is disabled completely, the refactoring is enabled in both directions. - var canOffer = userPrefersFileScopedNamespaces == forAnalyzer || (forRefactoring && analyzerDisabled); - if (!canOffer) - return false; + // If the user likes file scoped namespaces, then we offer file scoped namespaces from the diagnostic analyzer. + // If the user does not like file scoped namespaces then we offer file scoped namespaces from the refactoring provider. + // If the analyzer is disabled completely, the refactoring is enabled in both directions. + var canOffer = userPrefersFileScopedNamespaces == forAnalyzer || (forRefactoring && analyzerDisabled); + if (!canOffer) + return false; - // even if we could offer this here, we have to make sure it would be legal. A file scoped namespace is - // only legal if it's the only namespace in the file and there are no top level statements. - var tooManyNamespaces = root.DescendantNodesAndSelf(n => n is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax) - .OfType() - .Take(2) - .Count() != 1; - if (tooManyNamespaces) - return false; + // even if we could offer this here, we have to make sure it would be legal. A file scoped namespace is + // only legal if it's the only namespace in the file and there are no top level statements. + var tooManyNamespaces = root.DescendantNodesAndSelf(n => n is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax) + .OfType() + .Take(2) + .Count() != 1; + if (tooManyNamespaces) + return false; - if (root.Members.Any(m => m is GlobalStatementSyntax)) - return false; + if (root.Members.Any(m => m is GlobalStatementSyntax)) + return false; - return true; - } + return true; } } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertToBlockScopedNamespaceDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertToBlockScopedNamespaceDiagnosticAnalyzer.cs index c8b94b86a648e..99894891d8e70 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertToBlockScopedNamespaceDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertToBlockScopedNamespaceDiagnosticAnalyzer.cs @@ -10,56 +10,56 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace +namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class ConvertToBlockScopedNamespaceDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class ConvertToBlockScopedNamespaceDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public ConvertToBlockScopedNamespaceDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseBlockScopedNamespaceDiagnosticId, + EnforceOnBuildValues.UseBlockScopedNamespace, + CSharpCodeStyleOptions.NamespaceDeclarations, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_to_block_scoped_namespace), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public ConvertToBlockScopedNamespaceDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseBlockScopedNamespaceDiagnosticId, - EnforceOnBuildValues.UseBlockScopedNamespace, - CSharpCodeStyleOptions.NamespaceDeclarations, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_to_block_scoped_namespace), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeNamespace, SyntaxKind.FileScopedNamespaceDeclaration); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeNamespace, SyntaxKind.FileScopedNamespaceDeclaration); - private void AnalyzeNamespace(SyntaxNodeAnalysisContext context) - { - var namespaceDeclaration = (FileScopedNamespaceDeclarationSyntax)context.Node; + private void AnalyzeNamespace(SyntaxNodeAnalysisContext context) + { + var namespaceDeclaration = (FileScopedNamespaceDeclarationSyntax)context.Node; - var diagnostic = AnalyzeNamespace(context, namespaceDeclaration); - if (diagnostic != null) - context.ReportDiagnostic(diagnostic); - } + var diagnostic = AnalyzeNamespace(context, namespaceDeclaration); + if (diagnostic != null) + context.ReportDiagnostic(diagnostic); + } - private Diagnostic? AnalyzeNamespace(SyntaxNodeAnalysisContext context, FileScopedNamespaceDeclarationSyntax declaration) + private Diagnostic? AnalyzeNamespace(SyntaxNodeAnalysisContext context, FileScopedNamespaceDeclarationSyntax declaration) + { + var option = context.GetCSharpAnalyzerOptions().NamespaceDeclarations; + if (ShouldSkipAnalysis(context, option.Notification) + || !ConvertNamespaceAnalysis.CanOfferUseBlockScoped(option, declaration, forAnalyzer: true)) { - var option = context.GetCSharpAnalyzerOptions().NamespaceDeclarations; - if (ShouldSkipAnalysis(context, option.Notification) - || !ConvertNamespaceAnalysis.CanOfferUseBlockScoped(option, declaration, forAnalyzer: true)) - { - return null; - } + return null; + } - // if the diagnostic is hidden, show it anywhere from the `namespace` keyword through the name. - // otherwise, if it's not hidden, just squiggle the name. - var severity = option.Notification.Severity; - var diagnosticLocation = severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) != ReportDiagnostic.Hidden - ? declaration.Name.GetLocation() - : declaration.SyntaxTree.GetLocation(TextSpan.FromBounds(declaration.SpanStart, declaration.SemicolonToken.Span.End)); + // if the diagnostic is hidden, show it anywhere from the `namespace` keyword through the name. + // otherwise, if it's not hidden, just squiggle the name. + var severity = option.Notification.Severity; + var diagnosticLocation = severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) != ReportDiagnostic.Hidden + ? declaration.Name.GetLocation() + : declaration.SyntaxTree.GetLocation(TextSpan.FromBounds(declaration.SpanStart, declaration.SemicolonToken.Span.End)); - return DiagnosticHelper.Create( - this.Descriptor, - diagnosticLocation, - option.Notification, - ImmutableArray.Create(declaration.GetLocation()), - ImmutableDictionary.Empty); - } + return DiagnosticHelper.Create( + this.Descriptor, + diagnosticLocation, + option.Notification, + context.Options, + ImmutableArray.Create(declaration.GetLocation()), + ImmutableDictionary.Empty); } } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertToFileScopedNamespaceDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertToFileScopedNamespaceDiagnosticAnalyzer.cs index bf83729284f71..b03695a315784 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertToFileScopedNamespaceDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertNamespace/ConvertToFileScopedNamespaceDiagnosticAnalyzer.cs @@ -10,59 +10,59 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace +namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class ConvertToFileScopedNamespaceDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class ConvertToFileScopedNamespaceDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public ConvertToFileScopedNamespaceDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseFileScopedNamespaceDiagnosticId, + EnforceOnBuildValues.UseFileScopedNamespace, + CSharpCodeStyleOptions.NamespaceDeclarations, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_to_file_scoped_namespace), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public ConvertToFileScopedNamespaceDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseFileScopedNamespaceDiagnosticId, - EnforceOnBuildValues.UseFileScopedNamespace, - CSharpCodeStyleOptions.NamespaceDeclarations, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_to_file_scoped_namespace), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeNamespace, SyntaxKind.NamespaceDeclaration); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeNamespace, SyntaxKind.NamespaceDeclaration); - private void AnalyzeNamespace(SyntaxNodeAnalysisContext context) - { - var namespaceDeclaration = (NamespaceDeclarationSyntax)context.Node; - var syntaxTree = namespaceDeclaration.SyntaxTree; + private void AnalyzeNamespace(SyntaxNodeAnalysisContext context) + { + var namespaceDeclaration = (NamespaceDeclarationSyntax)context.Node; + var syntaxTree = namespaceDeclaration.SyntaxTree; - var cancellationToken = context.CancellationToken; - var root = (CompilationUnitSyntax)syntaxTree.GetRoot(cancellationToken); + var cancellationToken = context.CancellationToken; + var root = (CompilationUnitSyntax)syntaxTree.GetRoot(cancellationToken); - var diagnostic = AnalyzeNamespace(context, root, namespaceDeclaration); - if (diagnostic != null) - context.ReportDiagnostic(diagnostic); - } + var diagnostic = AnalyzeNamespace(context, root, namespaceDeclaration); + if (diagnostic != null) + context.ReportDiagnostic(diagnostic); + } - private Diagnostic? AnalyzeNamespace(SyntaxNodeAnalysisContext context, CompilationUnitSyntax root, BaseNamespaceDeclarationSyntax declaration) + private Diagnostic? AnalyzeNamespace(SyntaxNodeAnalysisContext context, CompilationUnitSyntax root, BaseNamespaceDeclarationSyntax declaration) + { + var option = context.GetCSharpAnalyzerOptions().NamespaceDeclarations; + if (ShouldSkipAnalysis(context, option.Notification) + || !ConvertNamespaceAnalysis.CanOfferUseFileScoped(option, root, declaration, forAnalyzer: true)) { - var option = context.GetCSharpAnalyzerOptions().NamespaceDeclarations; - if (ShouldSkipAnalysis(context, option.Notification) - || !ConvertNamespaceAnalysis.CanOfferUseFileScoped(option, root, declaration, forAnalyzer: true)) - { - return null; - } + return null; + } - // if the diagnostic is hidden, show it anywhere from the `namespace` keyword through the name. - // otherwise, if it's not hidden, just squiggle the name. - var diagnosticLocation = option.Notification.Severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) != ReportDiagnostic.Hidden - ? declaration.Name.GetLocation() - : declaration.SyntaxTree.GetLocation(TextSpan.FromBounds(declaration.SpanStart, declaration.Name.Span.End)); + // if the diagnostic is hidden, show it anywhere from the `namespace` keyword through the name. + // otherwise, if it's not hidden, just squiggle the name. + var diagnosticLocation = option.Notification.Severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) != ReportDiagnostic.Hidden + ? declaration.Name.GetLocation() + : declaration.SyntaxTree.GetLocation(TextSpan.FromBounds(declaration.SpanStart, declaration.Name.Span.End)); - return DiagnosticHelper.Create( - this.Descriptor, - diagnosticLocation, - option.Notification, - ImmutableArray.Create(declaration.GetLocation()), - ImmutableDictionary.Empty); - } + return DiagnosticHelper.Create( + this.Descriptor, + diagnosticLocation, + option.Notification, + context.Options, + ImmutableArray.Create(declaration.GetLocation()), + ImmutableDictionary.Empty); } } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_ProgramMain.cs b/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_ProgramMain.cs index 32923d505e8aa..160c519dc0e26 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_ProgramMain.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_ProgramMain.cs @@ -10,66 +10,65 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Analyzers.ConvertProgram +namespace Microsoft.CodeAnalysis.CSharp.Analyzers.ConvertProgram; + +internal static partial class ConvertProgramAnalysis { - internal static partial class ConvertProgramAnalysis - { - public static bool IsApplication(Compilation compilation) - => IsApplication(compilation.Options); + public static bool IsApplication(Compilation compilation) + => IsApplication(compilation.Options); - public static bool IsApplication(CompilationOptions options) - => options.OutputKind is OutputKind.ConsoleApplication or OutputKind.WindowsApplication; + public static bool IsApplication(CompilationOptions options) + => options.OutputKind is OutputKind.ConsoleApplication or OutputKind.WindowsApplication; - public static bool CanOfferUseProgramMain( - CodeStyleOption2 option, - CompilationUnitSyntax root, - Compilation compilation, - bool forAnalyzer) - { - // We only have to check if the first member is a global statement. Global statements following anything - // else is not legal. - if (!root.IsTopLevelProgram()) - return false; + public static bool CanOfferUseProgramMain( + CodeStyleOption2 option, + CompilationUnitSyntax root, + Compilation compilation, + bool forAnalyzer) + { + // We only have to check if the first member is a global statement. Global statements following anything + // else is not legal. + if (!root.IsTopLevelProgram()) + return false; - if (!CanOfferUseProgramMain(option, forAnalyzer)) - return false; + if (!CanOfferUseProgramMain(option, forAnalyzer)) + return false; - // Ensure that top-level method actually exists. - if (compilation.GetTopLevelStatementsMethod() is null) - return false; + // Ensure that top-level method actually exists. + if (compilation.GetTopLevelStatementsMethod() is null) + return false; - return true; - } + return true; + } - private static bool CanOfferUseProgramMain(CodeStyleOption2 option, bool forAnalyzer) - { - var userPrefersProgramMain = option.Value == false; - var analyzerDisabled = option.Notification.Severity == ReportDiagnostic.Suppress; - var forRefactoring = !forAnalyzer; + private static bool CanOfferUseProgramMain(CodeStyleOption2 option, bool forAnalyzer) + { + var userPrefersProgramMain = option.Value == false; + var analyzerDisabled = option.Notification.Severity == ReportDiagnostic.Suppress; + var forRefactoring = !forAnalyzer; - // If the user likes Program.Main, then we offer to convert to Program.Main from the diagnostic analyzer. - // If the user prefers Top-level-statements then we offer to use Program.Main from the refactoring provider. - // If the analyzer is disabled completely, the refactoring is enabled in both directions. - var canOffer = userPrefersProgramMain == forAnalyzer || (forRefactoring && analyzerDisabled); - return canOffer; - } + // If the user likes Program.Main, then we offer to convert to Program.Main from the diagnostic analyzer. + // If the user prefers Top-level-statements then we offer to use Program.Main from the refactoring provider. + // If the analyzer is disabled completely, the refactoring is enabled in both directions. + var canOffer = userPrefersProgramMain == forAnalyzer || (forRefactoring && analyzerDisabled); + return canOffer; + } - public static Location GetUseProgramMainDiagnosticLocation(CompilationUnitSyntax root, bool isHidden) - { - // if the diagnostic is hidden, show it anywhere from the top of the file through the end of the last global - // statement. That way the user can make the change anywhere in teh top level code. Otherwise, just put - // the diagnostic on the start of the first global statement. - if (!isHidden) - return root.Members.OfType().First().GetFirstToken().GetLocation(); + public static Location GetUseProgramMainDiagnosticLocation(CompilationUnitSyntax root, bool isHidden) + { + // if the diagnostic is hidden, show it anywhere from the top of the file through the end of the last global + // statement. That way the user can make the change anywhere in teh top level code. Otherwise, just put + // the diagnostic on the start of the first global statement. + if (!isHidden) + return root.Members.OfType().First().GetFirstToken().GetLocation(); - // note: the legal start has to come after any #pragma directives. We don't want this to be suppressed, but - // then have the span of the diagnostic end up outside the suppression. - var lastPragma = root.GetFirstToken().LeadingTrivia.LastOrDefault(t => t.Kind() is SyntaxKind.PragmaWarningDirectiveTrivia); - var start = lastPragma == default ? 0 : lastPragma.FullSpan.End; + // note: the legal start has to come after any #pragma directives. We don't want this to be suppressed, but + // then have the span of the diagnostic end up outside the suppression. + var lastPragma = root.GetFirstToken().LeadingTrivia.LastOrDefault(t => t.Kind() is SyntaxKind.PragmaWarningDirectiveTrivia); + var start = lastPragma == default ? 0 : lastPragma.FullSpan.End; - return Location.Create( - root.SyntaxTree, - TextSpan.FromBounds(start, root.Members.OfType().Last().FullSpan.End)); - } + return Location.Create( + root.SyntaxTree, + TextSpan.FromBounds(start, root.Members.OfType().Last().FullSpan.End)); } } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_TopLevelStatements.cs b/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_TopLevelStatements.cs index befba6416e778..3901f59629ae4 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_TopLevelStatements.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_TopLevelStatements.cs @@ -10,144 +10,143 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Analyzers.ConvertProgram +namespace Microsoft.CodeAnalysis.CSharp.Analyzers.ConvertProgram; + +internal static partial class ConvertProgramAnalysis { - internal static partial class ConvertProgramAnalysis + public static bool CanOfferUseTopLevelStatements(CodeStyleOption2 option, bool forAnalyzer) { - public static bool CanOfferUseTopLevelStatements(CodeStyleOption2 option, bool forAnalyzer) - { - var userPrefersTopLevelStatements = option.Value == true; - var analyzerDisabled = option.Notification.Severity == ReportDiagnostic.Suppress; - var forRefactoring = !forAnalyzer; - - // If the user likes top level statements, then we offer to convert to them from the diagnostic analyzer. - // If the user prefers Program.Main then we offer to use top-level-statements from the refactoring provider. - // If the analyzer is disabled completely, the refactoring is enabled in both directions. - var canOffer = userPrefersTopLevelStatements == forAnalyzer || (forRefactoring && analyzerDisabled); - return canOffer; - } + var userPrefersTopLevelStatements = option.Value == true; + var analyzerDisabled = option.Notification.Severity == ReportDiagnostic.Suppress; + var forRefactoring = !forAnalyzer; + + // If the user likes top level statements, then we offer to convert to them from the diagnostic analyzer. + // If the user prefers Program.Main then we offer to use top-level-statements from the refactoring provider. + // If the analyzer is disabled completely, the refactoring is enabled in both directions. + var canOffer = userPrefersTopLevelStatements == forAnalyzer || (forRefactoring && analyzerDisabled); + return canOffer; + } - public static Location GetUseTopLevelStatementsDiagnosticLocation(MethodDeclarationSyntax methodDeclaration, bool isHidden) - { - // if the diagnostic is hidden, show it anywhere on the main method. Otherwise, just put the diagnostic on - // the the 'Main' identifier. - return isHidden ? methodDeclaration.GetLocation() : methodDeclaration.Identifier.GetLocation(); - } + public static Location GetUseTopLevelStatementsDiagnosticLocation(MethodDeclarationSyntax methodDeclaration, bool isHidden) + { + // if the diagnostic is hidden, show it anywhere on the main method. Otherwise, just put the diagnostic on + // the the 'Main' identifier. + return isHidden ? methodDeclaration.GetLocation() : methodDeclaration.Identifier.GetLocation(); + } + + public static string? GetMainTypeName(Compilation compilation) + { + var mainTypeFullName = compilation.Options.MainTypeName; + var mainTypeName = mainTypeFullName?.Split('.').Last(); + return mainTypeName; + } - public static string? GetMainTypeName(Compilation compilation) + public static bool IsProgramMainMethod( + SemanticModel semanticModel, + MethodDeclarationSyntax methodDeclaration, + string? mainTypeName, + CancellationToken cancellationToken, + out bool canConvertToTopLevelStatements) + { + canConvertToTopLevelStatements = false; + + // Quick syntactic checks to allow us to avoid most methods. We basically filter out anything that isn't + // `static Main` immediately. + // + // For simplicity, we require the method to have a body so that we don't have to care about + // expression-bodied members later. + if (!methodDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword) || + methodDeclaration.TypeParameterList is not null || + methodDeclaration.Identifier.ValueText != WellKnownMemberNames.EntryPointMethodName || + methodDeclaration.Parent is not TypeDeclarationSyntax containingTypeDeclaration || + methodDeclaration.Body == null) { - var mainTypeFullName = compilation.Options.MainTypeName; - var mainTypeName = mainTypeFullName?.Split('.').Last(); - return mainTypeName; + return false; } - public static bool IsProgramMainMethod( - SemanticModel semanticModel, - MethodDeclarationSyntax methodDeclaration, - string? mainTypeName, - CancellationToken cancellationToken, - out bool canConvertToTopLevelStatements) - { - canConvertToTopLevelStatements = false; - - // Quick syntactic checks to allow us to avoid most methods. We basically filter out anything that isn't - // `static Main` immediately. - // - // For simplicity, we require the method to have a body so that we don't have to care about - // expression-bodied members later. - if (!methodDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword) || - methodDeclaration.TypeParameterList is not null || - methodDeclaration.Identifier.ValueText != WellKnownMemberNames.EntryPointMethodName || - methodDeclaration.Parent is not TypeDeclarationSyntax containingTypeDeclaration || - methodDeclaration.Body == null) - { - return false; - } + // If the compilation options specified a type name that Main should be found in, then do a quick check that + // our containing type matches that. + if (mainTypeName != null && containingTypeDeclaration.Identifier.ValueText != mainTypeName) + return false; - // If the compilation options specified a type name that Main should be found in, then do a quick check that - // our containing type matches that. - if (mainTypeName != null && containingTypeDeclaration.Identifier.ValueText != mainTypeName) - return false; + // If the user renamed the 'args' parameter, we can't convert to top level statements. + if (methodDeclaration.ParameterList.Parameters is [{ Identifier.ValueText: not "args" }]) + return false; - // If the user renamed the 'args' parameter, we can't convert to top level statements. - if (methodDeclaration.ParameterList.Parameters is [{ Identifier.ValueText: not "args" }]) - return false; + // Found a suitable candidate. See if this matches the entrypoint the compiler has actually chosen. + var entryPointMethod = semanticModel.Compilation.GetEntryPoint(cancellationToken); + if (entryPointMethod == null) + return false; - // Found a suitable candidate. See if this matches the entrypoint the compiler has actually chosen. - var entryPointMethod = semanticModel.Compilation.GetEntryPoint(cancellationToken); - if (entryPointMethod == null) - return false; + var thisMethod = semanticModel.GetDeclaredSymbol(methodDeclaration); + if (!entryPointMethod.Equals(thisMethod)) + return false; - var thisMethod = semanticModel.GetDeclaredSymbol(methodDeclaration); - if (!entryPointMethod.Equals(thisMethod)) - return false; + // We found the entrypoint. However, we can only effectively convert this to top-level-statements + // if the existing type is amenable to that. + canConvertToTopLevelStatements = TypeCanBeConverted(entryPointMethod.ContainingType, containingTypeDeclaration); + return true; + } - // We found the entrypoint. However, we can only effectively convert this to top-level-statements - // if the existing type is amenable to that. - canConvertToTopLevelStatements = TypeCanBeConverted(entryPointMethod.ContainingType, containingTypeDeclaration); - return true; - } + private static bool TypeCanBeConverted(INamedTypeSymbol containingType, TypeDeclarationSyntax typeDeclaration) + { + // Can't convert if our Program type derives or implements anything special. + if (containingType.BaseType?.SpecialType != SpecialType.System_Object) + return false; - private static bool TypeCanBeConverted(INamedTypeSymbol containingType, TypeDeclarationSyntax typeDeclaration) - { - // Can't convert if our Program type derives or implements anything special. - if (containingType.BaseType?.SpecialType != SpecialType.System_Object) - return false; + if (containingType.AllInterfaces.Length > 0) + return false; + + // Too complex to convert many parts to top-level statements. Just bail on this for now. + if (containingType.DeclaringSyntaxReferences.Length > 1) + return false; + + // Too complex to support converting a nested type. + if (containingType.ContainingType != null) + return false; - if (containingType.AllInterfaces.Length > 0) + // If the type wasn't internal it might have been public and something outside this assembly might be using it. + if (containingType.DeclaredAccessibility == Accessibility.Public) + return false; + + // type can't be converted with attributes. + if (typeDeclaration.AttributeLists.Count > 0) + return false; + + // can't convert doc comments to top level statements. + if (typeDeclaration.GetLeadingTrivia().Any(t => t.IsDocComment())) + return false; + + // All the members of the type need to be private/static. And we can only have fields or methods. that's to + // ensure that no one else was calling into this type, and that we can convert everything in the type to + // either locals or local-functions. + + foreach (var member in typeDeclaration.Members) + { + // method can't be converted with attributes. While a local function could support it, it would likely + // change the meaning of the program if reflection is being used to try to find this method. + if (member.AttributeLists.Count > 0) return false; - // Too complex to convert many parts to top-level statements. Just bail on this for now. - if (containingType.DeclaringSyntaxReferences.Length > 1) + // if not private, can't convert as something may be referencing it. + if (member.Modifiers.Any(m => m.Kind() is SyntaxKind.PublicKeyword or SyntaxKind.ProtectedKeyword or SyntaxKind.InternalKeyword)) return false; - // Too complex to support converting a nested type. - if (containingType.ContainingType != null) + if (!member.Modifiers.Any(SyntaxKind.StaticKeyword)) return false; - // If the type wasn't internal it might have been public and something outside this assembly might be using it. - if (containingType.DeclaredAccessibility == Accessibility.Public) + if (member is not FieldDeclarationSyntax and not MethodDeclarationSyntax) return false; - // type can't be converted with attributes. - if (typeDeclaration.AttributeLists.Count > 0) + // if a method, it has to actually have a body so we can convert it to a local function. + if (member is MethodDeclarationSyntax { Body: null, ExpressionBody: null }) return false; // can't convert doc comments to top level statements. - if (typeDeclaration.GetLeadingTrivia().Any(t => t.IsDocComment())) + if (member.GetLeadingTrivia().Any(t => t.IsDocComment())) return false; - - // All the members of the type need to be private/static. And we can only have fields or methods. that's to - // ensure that no one else was calling into this type, and that we can convert everything in the type to - // either locals or local-functions. - - foreach (var member in typeDeclaration.Members) - { - // method can't be converted with attributes. While a local function could support it, it would likely - // change the meaning of the program if reflection is being used to try to find this method. - if (member.AttributeLists.Count > 0) - return false; - - // if not private, can't convert as something may be referencing it. - if (member.Modifiers.Any(m => m.Kind() is SyntaxKind.PublicKeyword or SyntaxKind.ProtectedKeyword or SyntaxKind.InternalKeyword)) - return false; - - if (!member.Modifiers.Any(SyntaxKind.StaticKeyword)) - return false; - - if (member is not FieldDeclarationSyntax and not MethodDeclarationSyntax) - return false; - - // if a method, it has to actually have a body so we can convert it to a local function. - if (member is MethodDeclarationSyntax { Body: null, ExpressionBody: null }) - return false; - - // can't convert doc comments to top level statements. - if (member.GetLeadingTrivia().Any(t => t.IsDocComment())) - return false; - } - - return true; } + + return true; } } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertToProgramMainDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertToProgramMainDiagnosticAnalyzer.cs index 9b034f0eedd5a..14947a709a8f8 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertToProgramMainDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertToProgramMainDiagnosticAnalyzer.cs @@ -9,56 +9,56 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.TopLevelStatements -{ - using static ConvertProgramAnalysis; +namespace Microsoft.CodeAnalysis.CSharp.TopLevelStatements; + +using static ConvertProgramAnalysis; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class ConvertToProgramMainDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class ConvertToProgramMainDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer +{ + public ConvertToProgramMainDiagnosticAnalyzer() + : base( + IDEDiagnosticIds.UseProgramMainId, + EnforceOnBuildValues.UseProgramMain, + CSharpCodeStyleOptions.PreferTopLevelStatements, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_to_Program_Main_style_program), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public ConvertToProgramMainDiagnosticAnalyzer() - : base( - IDEDiagnosticIds.UseProgramMainId, - EnforceOnBuildValues.UseProgramMain, - CSharpCodeStyleOptions.PreferTopLevelStatements, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_to_Program_Main_style_program), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; - protected override void InitializeWorker(AnalysisContext context) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - context.RegisterCompilationStartAction(context => - { - if (!IsApplication(context.Compilation)) - return; + if (!IsApplication(context.Compilation)) + return; - context.RegisterSyntaxNodeAction(ProcessCompilationUnit, SyntaxKind.CompilationUnit); - }); - } + context.RegisterSyntaxNodeAction(ProcessCompilationUnit, SyntaxKind.CompilationUnit); + }); + } - private void ProcessCompilationUnit(SyntaxNodeAnalysisContext context) - { - var root = (CompilationUnitSyntax)context.Node; - var option = context.GetCSharpAnalyzerOptions().PreferTopLevelStatements; + private void ProcessCompilationUnit(SyntaxNodeAnalysisContext context) + { + var root = (CompilationUnitSyntax)context.Node; + var option = context.GetCSharpAnalyzerOptions().PreferTopLevelStatements; - if (ShouldSkipAnalysis(context, option.Notification) - || !CanOfferUseProgramMain(option, root, context.Compilation, forAnalyzer: true)) - { - return; - } + if (ShouldSkipAnalysis(context, option.Notification) + || !CanOfferUseProgramMain(option, root, context.Compilation, forAnalyzer: true)) + { + return; + } - var severity = option.Notification.Severity; + var severity = option.Notification.Severity; - context.ReportDiagnostic(DiagnosticHelper.Create( - this.Descriptor, - GetUseProgramMainDiagnosticLocation( - root, isHidden: severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden), - option.Notification, - [], - ImmutableDictionary.Empty)); - } + context.ReportDiagnostic(DiagnosticHelper.Create( + this.Descriptor, + GetUseProgramMainDiagnosticLocation( + root, isHidden: severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden), + option.Notification, + context.Options, + [], + ImmutableDictionary.Empty)); } } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertToTopLevelStatementsDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertToTopLevelStatementsDiagnosticAnalyzer.cs index b95afa1d48f68..5935fb2a83e02 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertToTopLevelStatementsDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertToTopLevelStatementsDiagnosticAnalyzer.cs @@ -11,79 +11,79 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.TopLevelStatements -{ - using static ConvertProgramAnalysis; +namespace Microsoft.CodeAnalysis.CSharp.TopLevelStatements; + +using static ConvertProgramAnalysis; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class ConvertToTopLevelStatementsDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class ConvertToTopLevelStatementsDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer +{ + public ConvertToTopLevelStatementsDiagnosticAnalyzer() + : base( + IDEDiagnosticIds.UseTopLevelStatementsId, + EnforceOnBuildValues.UseTopLevelStatements, + CSharpCodeStyleOptions.PreferTopLevelStatements, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_to_top_level_statements), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public ConvertToTopLevelStatementsDiagnosticAnalyzer() - : base( - IDEDiagnosticIds.UseTopLevelStatementsId, - EnforceOnBuildValues.UseTopLevelStatements, - CSharpCodeStyleOptions.PreferTopLevelStatements, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_to_top_level_statements), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; - protected override void InitializeWorker(AnalysisContext context) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - context.RegisterCompilationStartAction(context => + // can only suggest moving to top level statement on c# 9 or above. + if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp9 || + !IsApplication(context.Compilation)) { - // can only suggest moving to top level statement on c# 9 or above. - if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp9 || - !IsApplication(context.Compilation)) - { - return; - } + return; + } - context.RegisterSyntaxNodeAction(ProcessCompilationUnit, SyntaxKind.CompilationUnit); - }); - } + context.RegisterSyntaxNodeAction(ProcessCompilationUnit, SyntaxKind.CompilationUnit); + }); + } - private void ProcessCompilationUnit(SyntaxNodeAnalysisContext context) + private void ProcessCompilationUnit(SyntaxNodeAnalysisContext context) + { + // Don't want to suggest moving if the user doesn't have a preference for top-level-statements. + var option = context.GetCSharpAnalyzerOptions().PreferTopLevelStatements; + if (ShouldSkipAnalysis(context, option.Notification) + || !CanOfferUseTopLevelStatements(option, forAnalyzer: true)) { - // Don't want to suggest moving if the user doesn't have a preference for top-level-statements. - var option = context.GetCSharpAnalyzerOptions().PreferTopLevelStatements; - if (ShouldSkipAnalysis(context, option.Notification) - || !CanOfferUseTopLevelStatements(option, forAnalyzer: true)) - { - return; - } + return; + } - var cancellationToken = context.CancellationToken; - var semanticModel = context.SemanticModel; - var compilation = semanticModel.Compilation; - var mainTypeName = GetMainTypeName(compilation); + var cancellationToken = context.CancellationToken; + var semanticModel = context.SemanticModel; + var compilation = semanticModel.Compilation; + var mainTypeName = GetMainTypeName(compilation); - // Ok, the user does like top level statements. Check if we can find a suitable hit in this type that - // indicates we're on the entrypoint of the program. - var root = (CompilationUnitSyntax)context.Node; - var methodDeclarations = root.DescendantNodes(n => n is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax or ClassDeclarationSyntax).OfType(); - foreach (var methodDeclaration in methodDeclarations) + // Ok, the user does like top level statements. Check if we can find a suitable hit in this type that + // indicates we're on the entrypoint of the program. + var root = (CompilationUnitSyntax)context.Node; + var methodDeclarations = root.DescendantNodes(n => n is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax or ClassDeclarationSyntax).OfType(); + foreach (var methodDeclaration in methodDeclarations) + { + if (IsProgramMainMethod( + semanticModel, methodDeclaration, mainTypeName, cancellationToken, out var canConvertToTopLevelStatement)) { - if (IsProgramMainMethod( - semanticModel, methodDeclaration, mainTypeName, cancellationToken, out var canConvertToTopLevelStatement)) + if (canConvertToTopLevelStatement) { - if (canConvertToTopLevelStatement) - { - // Looks good. Let the user know this type/method can be converted to a top level program. - context.ReportDiagnostic(DiagnosticHelper.Create( - this.Descriptor, - GetUseTopLevelStatementsDiagnosticLocation( - methodDeclaration, isHidden: option.Notification.Severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden), - option.Notification, - ImmutableArray.Create(methodDeclaration.GetLocation()), - ImmutableDictionary.Empty)); - } - - // We found the main method, but it's not convertible, bail out as we have nothing else to do. - return; + // Looks good. Let the user know this type/method can be converted to a top level program. + context.ReportDiagnostic(DiagnosticHelper.Create( + this.Descriptor, + GetUseTopLevelStatementsDiagnosticLocation( + methodDeclaration, isHidden: option.Notification.Severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden), + option.Notification, + context.Options, + ImmutableArray.Create(methodDeclaration.GetLocation()), + ImmutableDictionary.Empty)); } + + // We found the main method, but it's not convertible, bail out as we have nothing else to do. + return; } } } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionConstants.cs b/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionConstants.cs index 38d5831c840fe..54b6a32f8732d 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionConstants.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionConstants.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression +namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression; + +internal static class ConvertSwitchStatementToExpressionConstants { - internal static class ConvertSwitchStatementToExpressionConstants - { - public const string NodeToGenerateKey = nameof(NodeToGenerateKey); - public const string ShouldRemoveNextStatementKey = nameof(ShouldRemoveNextStatementKey); - } + public const string NodeToGenerateKey = nameof(NodeToGenerateKey); + public const string ShouldRemoveNextStatementKey = nameof(ShouldRemoveNextStatementKey); } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionDiagnosticAnalyzer.Analyzer.cs b/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionDiagnosticAnalyzer.Analyzer.cs index 04c82dc556a6d..81a4794e13a00 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionDiagnosticAnalyzer.Analyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionDiagnosticAnalyzer.Analyzer.cs @@ -11,287 +11,286 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression -{ - using static ConvertSwitchStatementToExpressionHelpers; +namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression; + +using static ConvertSwitchStatementToExpressionHelpers; - internal sealed partial class ConvertSwitchStatementToExpressionDiagnosticAnalyzer +internal sealed partial class ConvertSwitchStatementToExpressionDiagnosticAnalyzer +{ + private sealed class Analyzer : CSharpSyntaxVisitor { - private sealed class Analyzer : CSharpSyntaxVisitor - { - private readonly bool _supportsOrPatterns; + private readonly bool _supportsOrPatterns; - private ExpressionSyntax? _assignmentTargetOpt; + private ExpressionSyntax? _assignmentTargetOpt; - private Analyzer(bool supportsOrPatterns) - { - _supportsOrPatterns = supportsOrPatterns; - } + private Analyzer(bool supportsOrPatterns) + { + _supportsOrPatterns = supportsOrPatterns; + } - public static (SyntaxKind nodeToGenerate, VariableDeclaratorSyntax? declaratorToRemoveOpt) Analyze( - SwitchStatementSyntax node, - SemanticModel semanticModel, - out bool shouldRemoveNextStatement) - { - var analyzer = new Analyzer(supportsOrPatterns: semanticModel.SyntaxTree.Options.LanguageVersion() >= LanguageVersion.CSharp9); - var nodeToGenerate = analyzer.AnalyzeSwitchStatement(node, out shouldRemoveNextStatement); + public static (SyntaxKind nodeToGenerate, VariableDeclaratorSyntax? declaratorToRemoveOpt) Analyze( + SwitchStatementSyntax node, + SemanticModel semanticModel, + out bool shouldRemoveNextStatement) + { + var analyzer = new Analyzer(supportsOrPatterns: semanticModel.SyntaxTree.Options.LanguageVersion() >= LanguageVersion.CSharp9); + var nodeToGenerate = analyzer.AnalyzeSwitchStatement(node, out shouldRemoveNextStatement); - if (nodeToGenerate == SyntaxKind.SimpleAssignmentExpression && - analyzer.TryGetVariableDeclaratorAndSymbol(semanticModel) is var (declarator, symbol)) + if (nodeToGenerate == SyntaxKind.SimpleAssignmentExpression && + analyzer.TryGetVariableDeclaratorAndSymbol(semanticModel) is var (declarator, symbol)) + { + if (shouldRemoveNextStatement && node.GetNextStatement() is StatementSyntax nextStatement) { - if (shouldRemoveNextStatement && node.GetNextStatement() is StatementSyntax nextStatement) + var dataFlow = semanticModel.AnalyzeDataFlow(nextStatement); + Contract.ThrowIfNull(dataFlow); + if (dataFlow.DataFlowsIn.Contains(symbol)) { - var dataFlow = semanticModel.AnalyzeDataFlow(nextStatement); - Contract.ThrowIfNull(dataFlow); - if (dataFlow.DataFlowsIn.Contains(symbol)) - { - // Bail out if data flows into the next statement that we want to move - // For example: - // - // string name = ""; - // switch (index) - // { - // case 0: name = "0"; break; - // case 1: name = "1"; break; - // } - // throw new Exception(name); - // - return default; - } + // Bail out if data flows into the next statement that we want to move + // For example: + // + // string name = ""; + // switch (index) + // { + // case 0: name = "0"; break; + // case 1: name = "1"; break; + // } + // throw new Exception(name); + // + return default; } + } - var declaration = declarator.GetAncestor(); - Contract.ThrowIfNull(declaration); - if (declaration.Parent == node.Parent && declarator.Initializer is null) + var declaration = declarator.GetAncestor(); + Contract.ThrowIfNull(declaration); + if (declaration.Parent == node.Parent && declarator.Initializer is null) + { + var beforeSwitch = node.GetPreviousStatement() is StatementSyntax previousStatement + ? semanticModel.AnalyzeDataFlow(declaration, previousStatement) + : semanticModel.AnalyzeDataFlow(declaration); + Contract.ThrowIfNull(beforeSwitch); + if (!beforeSwitch.WrittenInside.Contains(symbol)) { - var beforeSwitch = node.GetPreviousStatement() is StatementSyntax previousStatement - ? semanticModel.AnalyzeDataFlow(declaration, previousStatement) - : semanticModel.AnalyzeDataFlow(declaration); - Contract.ThrowIfNull(beforeSwitch); - if (!beforeSwitch.WrittenInside.Contains(symbol)) - { - // Move declarator only if it has no initializer and it's not used before switch - return (nodeToGenerate, declaratorToRemoveOpt: declarator); - } + // Move declarator only if it has no initializer and it's not used before switch + return (nodeToGenerate, declaratorToRemoveOpt: declarator); } } - - return (nodeToGenerate, declaratorToRemoveOpt: null); } - private (VariableDeclaratorSyntax, ISymbol)? TryGetVariableDeclaratorAndSymbol(SemanticModel semanticModel) - { - if (!_assignmentTargetOpt.IsKind(SyntaxKind.IdentifierName)) - { - return null; - } - - var symbol = semanticModel.GetSymbolInfo(_assignmentTargetOpt).Symbol; - if (symbol is not - { Kind: SymbolKind.Local, DeclaringSyntaxReferences: { Length: 1 } syntaxRefs }) - { - return null; - } - - if (syntaxRefs[0].GetSyntax() is not VariableDeclaratorSyntax declarator) - { - return null; - } + return (nodeToGenerate, declaratorToRemoveOpt: null); + } - return (declarator, symbol); + private (VariableDeclaratorSyntax, ISymbol)? TryGetVariableDeclaratorAndSymbol(SemanticModel semanticModel) + { + if (!_assignmentTargetOpt.IsKind(SyntaxKind.IdentifierName)) + { + return null; } - public override SyntaxKind VisitSwitchStatement(SwitchStatementSyntax node) - => AnalyzeSwitchStatement(node, out _); + var symbol = semanticModel.GetSymbolInfo(_assignmentTargetOpt).Symbol; + if (symbol is not + { Kind: SymbolKind.Local, DeclaringSyntaxReferences: { Length: 1 } syntaxRefs }) + { + return null; + } - private SyntaxKind AnalyzeSwitchStatement(SwitchStatementSyntax switchStatement, out bool shouldRemoveNextStatement) + if (syntaxRefs[0].GetSyntax() is not VariableDeclaratorSyntax declarator) { - // Fail if the switch statement is empty. - var sections = switchStatement.Sections; - if (sections.Count == 0) - { - shouldRemoveNextStatement = false; - return default; - } + return null; + } - if (!sections.All(s => CanConvertLabelsToArms(s.Labels))) - { - shouldRemoveNextStatement = false; - return default; - } + return (declarator, symbol); + } - // If there's no "default" case, we look at the next statement. - // For instance, it could be a "return" statement which we'll use - // as the default case in the switch expression. - var nextStatement = AnalyzeNextStatement(switchStatement, out shouldRemoveNextStatement); + public override SyntaxKind VisitSwitchStatement(SwitchStatementSyntax node) + => AnalyzeSwitchStatement(node, out _); - // We do need to intersect the next statement analysis result to catch possible - // arm kind mismatch, e.g. a "return" after a non-exhaustive assignment switch. - return Aggregate(nextStatement, sections, (result, section) => Intersect(result, AnalyzeSwitchSection(section))); + private SyntaxKind AnalyzeSwitchStatement(SwitchStatementSyntax switchStatement, out bool shouldRemoveNextStatement) + { + // Fail if the switch statement is empty. + var sections = switchStatement.Sections; + if (sections.Count == 0) + { + shouldRemoveNextStatement = false; + return default; } - private bool CanConvertLabelsToArms(SyntaxList labels) + if (!sections.All(s => CanConvertLabelsToArms(s.Labels))) { - Debug.Assert(labels.Count >= 1); - if (labels.Count == 1) - { - // Single label can always be converted to a single arm. - return true; - } - - if (labels.Any(label => IsDefaultSwitchLabel(label))) - { - // if any of the labels are a default/_/var (catch-all) then we can convert this set of labels into - // a single `_` arm. - return true; - } - - // We have multiple labels and none of them are a 'catch-all'. + shouldRemoveNextStatement = false; + return default; + } - if (!_supportsOrPatterns) - { - // We don't support 'or' patterns, so no way to convert this to arms. - return false; - } + // If there's no "default" case, we look at the next statement. + // For instance, it could be a "return" statement which we'll use + // as the default case in the switch expression. + var nextStatement = AnalyzeNextStatement(switchStatement, out shouldRemoveNextStatement); - // If any of the cases have when-clauses, like so: - // - // case ... when Goo(): - // case ... when Bar(): - // - // Then we can't convert into a single arm. - foreach (var label in labels) - { - if (label is CasePatternSwitchLabelSyntax casePattern && - casePattern.WhenClause != null) - { - return false; - } - } + // We do need to intersect the next statement analysis result to catch possible + // arm kind mismatch, e.g. a "return" after a non-exhaustive assignment switch. + return Aggregate(nextStatement, sections, (result, section) => Intersect(result, AnalyzeSwitchSection(section))); + } - // We have multiple labels that can be combined together using an 'or' pattern. + private bool CanConvertLabelsToArms(SyntaxList labels) + { + Debug.Assert(labels.Count >= 1); + if (labels.Count == 1) + { + // Single label can always be converted to a single arm. return true; } - private SyntaxKind AnalyzeNextStatement(SwitchStatementSyntax switchStatement, out bool shouldRemoveNextStatement) + if (labels.Any(label => IsDefaultSwitchLabel(label))) { - // Check if we have a catch-all label anywhere. If so we don't need to pull in the next statements. - if (switchStatement.Sections.Any(section => section.Labels.Any(label => IsDefaultSwitchLabel(label)))) - { - // Throw can be overridden by other section bodies, therefore it has no effect on the result. - shouldRemoveNextStatement = false; - return SyntaxKind.ThrowStatement; - } - - // Didn't have a default case, see if we can pull in the statement following the switch to become our default. - shouldRemoveNextStatement = true; - return AnalyzeNextStatement(switchStatement.GetNextStatement()); + // if any of the labels are a default/_/var (catch-all) then we can convert this set of labels into + // a single `_` arm. + return true; } - private static SyntaxKind Intersect(SyntaxKind left, SyntaxKind right) + // We have multiple labels and none of them are a 'catch-all'. + + if (!_supportsOrPatterns) { - if (left == SyntaxKind.ThrowStatement) - { - return right; - } + // We don't support 'or' patterns, so no way to convert this to arms. + return false; + } - if (right == SyntaxKind.ThrowStatement) + // If any of the cases have when-clauses, like so: + // + // case ... when Goo(): + // case ... when Bar(): + // + // Then we can't convert into a single arm. + foreach (var label in labels) + { + if (label is CasePatternSwitchLabelSyntax casePattern && + casePattern.WhenClause != null) { - return left; + return false; } + } - if (left == right) - { - return left; - } + // We have multiple labels that can be combined together using an 'or' pattern. + return true; + } - return default; + private SyntaxKind AnalyzeNextStatement(SwitchStatementSyntax switchStatement, out bool shouldRemoveNextStatement) + { + // Check if we have a catch-all label anywhere. If so we don't need to pull in the next statements. + if (switchStatement.Sections.Any(section => section.Labels.Any(label => IsDefaultSwitchLabel(label)))) + { + // Throw can be overridden by other section bodies, therefore it has no effect on the result. + shouldRemoveNextStatement = false; + return SyntaxKind.ThrowStatement; } - private SyntaxKind AnalyzeNextStatement(StatementSyntax? nextStatement) + // Didn't have a default case, see if we can pull in the statement following the switch to become our default. + shouldRemoveNextStatement = true; + return AnalyzeNextStatement(switchStatement.GetNextStatement()); + } + + private static SyntaxKind Intersect(SyntaxKind left, SyntaxKind right) + { + if (left == SyntaxKind.ThrowStatement) { - // Only the following "throw" and "return" can be moved into the switch expression. - return nextStatement is (kind: SyntaxKind.ThrowStatement or SyntaxKind.ReturnStatement) - ? Visit(nextStatement) - : default; + return right; } - private SyntaxKind AnalyzeSwitchSection(SwitchSectionSyntax section) + if (right == SyntaxKind.ThrowStatement) { - switch (section.Statements.Count) - { - case 1: - case 2 when section.Statements[1].IsKind(SyntaxKind.BreakStatement) || section.Statements[0].IsKind(SyntaxKind.SwitchStatement): - return Visit(section.Statements[0]); - default: - return default; - } + return left; } - private static SyntaxKind Aggregate(SyntaxKind seed, SyntaxList nodes, Func func) - where T : SyntaxNode + if (left == right) { - var result = seed; - foreach (var node in nodes) - { - result = func(result, node); - if (result == default) - { - // No point to continue if any node was not - // convertible to a switch arm's expression - break; - } - } - - return result; + return left; } - public override SyntaxKind VisitAssignmentExpression(AssignmentExpressionSyntax node) + return default; + } + + private SyntaxKind AnalyzeNextStatement(StatementSyntax? nextStatement) + { + // Only the following "throw" and "return" can be moved into the switch expression. + return nextStatement is (kind: SyntaxKind.ThrowStatement or SyntaxKind.ReturnStatement) + ? Visit(nextStatement) + : default; + } + + private SyntaxKind AnalyzeSwitchSection(SwitchSectionSyntax section) + { + switch (section.Statements.Count) { - if (node.Right is RefExpressionSyntax) + case 1: + case 2 when section.Statements[1].IsKind(SyntaxKind.BreakStatement) || section.Statements[0].IsKind(SyntaxKind.SwitchStatement): + return Visit(section.Statements[0]); + default: return default; + } + } - if (_assignmentTargetOpt != null) - { - if (!SyntaxFactory.AreEquivalent(node.Left, _assignmentTargetOpt)) - { - return default; - } - } - else + private static SyntaxKind Aggregate(SyntaxKind seed, SyntaxList nodes, Func func) + where T : SyntaxNode + { + var result = seed; + foreach (var node in nodes) + { + result = func(result, node); + if (result == default) { - _assignmentTargetOpt = node.Left; + // No point to continue if any node was not + // convertible to a switch arm's expression + break; } - - return node.Kind(); } - public override SyntaxKind VisitExpressionStatement(ExpressionStatementSyntax node) - => Visit(node.Expression); + return result; + } - public override SyntaxKind VisitReturnStatement(ReturnStatementSyntax node) - { - // A "return" statement's expression will be placed in the switch arm expression. We - // also can't convert a switch statement with ref-returns to a switch-expression - // (currently). Until the language supports ref-switch-expressions, we just disable - // things. - return node.Expression is null or RefExpressionSyntax - ? default - : SyntaxKind.ReturnStatement; - } + public override SyntaxKind VisitAssignmentExpression(AssignmentExpressionSyntax node) + { + if (node.Right is RefExpressionSyntax) + return default; - public override SyntaxKind VisitThrowStatement(ThrowStatementSyntax node) + if (_assignmentTargetOpt != null) { - // A "throw" statement can be converted to a throw expression. - // Gives Failure if Expression is null because a throw expression needs one. - return node.Expression is null ? default : SyntaxKind.ThrowStatement; + if (!SyntaxFactory.AreEquivalent(node.Left, _assignmentTargetOpt)) + { + return default; + } } - - public override SyntaxKind DefaultVisit(SyntaxNode node) + else { - // In all other cases we return failure result. - return default; + _assignmentTargetOpt = node.Left; } + + return node.Kind(); + } + + public override SyntaxKind VisitExpressionStatement(ExpressionStatementSyntax node) + => Visit(node.Expression); + + public override SyntaxKind VisitReturnStatement(ReturnStatementSyntax node) + { + // A "return" statement's expression will be placed in the switch arm expression. We + // also can't convert a switch statement with ref-returns to a switch-expression + // (currently). Until the language supports ref-switch-expressions, we just disable + // things. + return node.Expression is null or RefExpressionSyntax + ? default + : SyntaxKind.ReturnStatement; + } + + public override SyntaxKind VisitThrowStatement(ThrowStatementSyntax node) + { + // A "throw" statement can be converted to a throw expression. + // Gives Failure if Expression is null because a throw expression needs one. + return node.Expression is null ? default : SyntaxKind.ThrowStatement; + } + + public override SyntaxKind DefaultVisit(SyntaxNode node) + { + // In all other cases we return failure result. + return default; } } } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionDiagnosticAnalyzer.cs index 919846f335924..67e5ccb33901f 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionDiagnosticAnalyzer.cs @@ -13,70 +13,70 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression -{ - using Constants = ConvertSwitchStatementToExpressionConstants; - - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed partial class ConvertSwitchStatementToExpressionDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer - { - public ConvertSwitchStatementToExpressionDiagnosticAnalyzer() - : base(IDEDiagnosticIds.ConvertSwitchStatementToExpressionDiagnosticId, - EnforceOnBuildValues.ConvertSwitchStatementToExpression, - CSharpCodeStyleOptions.PreferSwitchExpression, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_switch_statement_to_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_switch_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } +namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - { - if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp8) - return; +using Constants = ConvertSwitchStatementToExpressionConstants; - context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.SwitchStatement); - }); +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed partial class ConvertSwitchStatementToExpressionDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer +{ + public ConvertSwitchStatementToExpressionDiagnosticAnalyzer() + : base(IDEDiagnosticIds.ConvertSwitchStatementToExpressionDiagnosticId, + EnforceOnBuildValues.ConvertSwitchStatementToExpression, + CSharpCodeStyleOptions.PreferSwitchExpression, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Convert_switch_statement_to_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_switch_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + { + } - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => { - var styleOption = context.GetCSharpAnalyzerOptions().PreferSwitchExpression; - if (!styleOption.Value || ShouldSkipAnalysis(context, styleOption.Notification)) - { - // User has disabled this feature. + if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp8) return; - } - var switchStatement = context.Node; - if (switchStatement.GetDiagnostics().Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)) - { - return; - } + context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.SwitchStatement); + }); - var (nodeToGenerate, declaratorToRemoveOpt) = Analyzer.Analyze( - (SwitchStatementSyntax)switchStatement, - context.SemanticModel, - out var shouldRemoveNextStatement); - if (nodeToGenerate == default) - { - return; - } + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var styleOption = context.GetCSharpAnalyzerOptions().PreferSwitchExpression; + if (!styleOption.Value || ShouldSkipAnalysis(context, styleOption.Notification)) + { + // User has disabled this feature. + return; + } - var additionalLocations = ArrayBuilder.GetInstance(); - additionalLocations.Add(switchStatement.GetLocation()); - additionalLocations.AddOptional(declaratorToRemoveOpt?.GetLocation()); + var switchStatement = context.Node; + if (switchStatement.GetDiagnostics().Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)) + { + return; + } - context.ReportDiagnostic(DiagnosticHelper.Create(Descriptor, - // Report the diagnostic on the "switch" keyword. - location: switchStatement.GetFirstToken().GetLocation(), - notificationOption: styleOption.Notification, - additionalLocations: additionalLocations.ToArrayAndFree(), - properties: ImmutableDictionary.Empty - .Add(Constants.NodeToGenerateKey, ((int)nodeToGenerate).ToString(CultureInfo.InvariantCulture)) - .Add(Constants.ShouldRemoveNextStatementKey, shouldRemoveNextStatement.ToString(CultureInfo.InvariantCulture)))); + var (nodeToGenerate, declaratorToRemoveOpt) = Analyzer.Analyze( + (SwitchStatementSyntax)switchStatement, + context.SemanticModel, + out var shouldRemoveNextStatement); + if (nodeToGenerate == default) + { + return; } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + var additionalLocations = ArrayBuilder.GetInstance(); + additionalLocations.Add(switchStatement.GetLocation()); + additionalLocations.AddOptional(declaratorToRemoveOpt?.GetLocation()); + + context.ReportDiagnostic(DiagnosticHelper.Create(Descriptor, + // Report the diagnostic on the "switch" keyword. + location: switchStatement.GetFirstToken().GetLocation(), + notificationOption: styleOption.Notification, + context.Options, + additionalLocations: additionalLocations.ToArrayAndFree(), + properties: ImmutableDictionary.Empty + .Add(Constants.NodeToGenerateKey, ((int)nodeToGenerate).ToString(CultureInfo.InvariantCulture)) + .Add(Constants.ShouldRemoveNextStatementKey, shouldRemoveNextStatement.ToString(CultureInfo.InvariantCulture)))); } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionHelpers.cs b/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionHelpers.cs index 5fcfb8f4f7e83..3a26494309d74 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionHelpers.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionHelpers.cs @@ -5,36 +5,35 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression +namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression; + +internal static class ConvertSwitchStatementToExpressionHelpers { - internal static class ConvertSwitchStatementToExpressionHelpers + public static bool IsDefaultSwitchLabel(SwitchLabelSyntax node) { - public static bool IsDefaultSwitchLabel(SwitchLabelSyntax node) + // default: + if (node.IsKind(SyntaxKind.DefaultSwitchLabel)) + { + return true; + } + + if (node is CasePatternSwitchLabelSyntax @case) { - // default: - if (node.IsKind(SyntaxKind.DefaultSwitchLabel)) + // case _: + if (@case.Pattern.IsKind(SyntaxKind.DiscardPattern)) { - return true; + return @case.WhenClause == null; } - if (node is CasePatternSwitchLabelSyntax @case) + // case var _: + // case var x: + if (@case.Pattern is VarPatternSyntax varPattern && + varPattern.Designation.Kind() is SyntaxKind.DiscardDesignation or SyntaxKind.SingleVariableDesignation) { - // case _: - if (@case.Pattern.IsKind(SyntaxKind.DiscardPattern)) - { - return @case.WhenClause == null; - } - - // case var _: - // case var x: - if (@case.Pattern is VarPatternSyntax varPattern && - varPattern.Designation.Kind() is SyntaxKind.DiscardDesignation or SyntaxKind.SingleVariableDesignation) - { - return @case.WhenClause == null; - } + return @case.WhenClause == null; } - - return false; } + + return false; } } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertTypeofToNameof/CSharpConvertTypeOfToNameOfDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/ConvertTypeofToNameof/CSharpConvertTypeOfToNameOfDiagnosticAnalyzer.cs index 4ab6f7f0ac1db..f6c3d60969c21 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertTypeofToNameof/CSharpConvertTypeOfToNameOfDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertTypeofToNameof/CSharpConvertTypeOfToNameOfDiagnosticAnalyzer.cs @@ -7,36 +7,35 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.ConvertTypeOfToNameOf +namespace Microsoft.CodeAnalysis.CSharp.ConvertTypeOfToNameOf; + +/// +/// Finds code like typeof(someType).Name and determines whether it can be changed to nameof(someType), if yes then it offers a diagnostic +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpConvertTypeOfToNameOfDiagnosticAnalyzer : AbstractConvertTypeOfToNameOfDiagnosticAnalyzer { - /// - /// Finds code like typeof(someType).Name and determines whether it can be changed to nameof(someType), if yes then it offers a diagnostic - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpConvertTypeOfToNameOfDiagnosticAnalyzer : AbstractConvertTypeOfToNameOfDiagnosticAnalyzer + private static readonly string s_title = CSharpAnalyzersResources.typeof_can_be_converted__to_nameof; + + public CSharpConvertTypeOfToNameOfDiagnosticAnalyzer() : base(s_title) { - private static readonly string s_title = CSharpAnalyzersResources.typeof_can_be_converted__to_nameof; + } - public CSharpConvertTypeOfToNameOfDiagnosticAnalyzer() : base(s_title) - { - } + protected override bool IsValidTypeofAction(OperationAnalysisContext context) + { + var node = context.Operation.Syntax; - protected override bool IsValidTypeofAction(OperationAnalysisContext context) + // nameof was added in CSharp 6.0, so don't offer it for any languages before that time + if (node.GetLanguageVersion() < LanguageVersion.CSharp6) { - var node = context.Operation.Syntax; - - // nameof was added in CSharp 6.0, so don't offer it for any languages before that time - if (node.GetLanguageVersion() < LanguageVersion.CSharp6) - { - return false; - } - - // Make sure that the syntax that we're looking at is actually a typeof expression and that - // the parent syntax is a member access expression otherwise the syntax is not the kind of - // expression that we want to analyze - return node is TypeOfExpressionSyntax { Parent: MemberAccessExpressionSyntax } typeofExpression && - // nameof(System.Void) isn't allowed in C#. - typeofExpression is not { Type: PredefinedTypeSyntax { Keyword.RawKind: (int)SyntaxKind.VoidKeyword } }; + return false; } + + // Make sure that the syntax that we're looking at is actually a typeof expression and that + // the parent syntax is a member access expression otherwise the syntax is not the kind of + // expression that we want to analyze + return node is TypeOfExpressionSyntax { Parent: MemberAccessExpressionSyntax } typeofExpression && + // nameof(System.Void) isn't allowed in C#. + typeofExpression is not { Type: PredefinedTypeSyntax { Keyword.RawKind: (int)SyntaxKind.VoidKeyword } }; } } diff --git a/src/Analyzers/CSharp/Analyzers/FileHeaders/CSharpFileHeaderDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/FileHeaders/CSharpFileHeaderDiagnosticAnalyzer.cs index 90c114c6f80e3..f87bbbb3d76b7 100644 --- a/src/Analyzers/CSharp/Analyzers/FileHeaders/CSharpFileHeaderDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/FileHeaders/CSharpFileHeaderDiagnosticAnalyzer.cs @@ -5,11 +5,10 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.FileHeaders; -namespace Microsoft.CodeAnalysis.CSharp.FileHeaders +namespace Microsoft.CodeAnalysis.CSharp.FileHeaders; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpFileHeaderDiagnosticAnalyzer : AbstractFileHeaderDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpFileHeaderDiagnosticAnalyzer : AbstractFileHeaderDiagnosticAnalyzer - { - protected override AbstractFileHeaderHelper FileHeaderHelper => CSharpFileHeaderHelper.Instance; - } + protected override AbstractFileHeaderHelper FileHeaderHelper => CSharpFileHeaderHelper.Instance; } diff --git a/src/Analyzers/CSharp/Analyzers/FileHeaders/CSharpFileHeaderHelper.cs b/src/Analyzers/CSharp/Analyzers/FileHeaders/CSharpFileHeaderHelper.cs index 6cfb29f0d122a..12209ee97040f 100644 --- a/src/Analyzers/CSharp/Analyzers/FileHeaders/CSharpFileHeaderHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/FileHeaders/CSharpFileHeaderHelper.cs @@ -7,46 +7,45 @@ using Microsoft.CodeAnalysis.FileHeaders; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.FileHeaders +namespace Microsoft.CodeAnalysis.CSharp.FileHeaders; + +/// +/// Helper class used for working with file headers. +/// +internal sealed class CSharpFileHeaderHelper : AbstractFileHeaderHelper { - /// - /// Helper class used for working with file headers. - /// - internal sealed class CSharpFileHeaderHelper : AbstractFileHeaderHelper + public static readonly CSharpFileHeaderHelper Instance = new(); + + private CSharpFileHeaderHelper() + : base(CSharpSyntaxKinds.Instance) { - public static readonly CSharpFileHeaderHelper Instance = new(); + } + + public override string CommentPrefix => "//"; - private CSharpFileHeaderHelper() - : base(CSharpSyntaxKinds.Instance) + protected override ReadOnlyMemory GetTextContextOfComment(SyntaxTrivia commentTrivia) + { + if (commentTrivia.IsKind(SyntaxKind.SingleLineCommentTrivia)) { + return commentTrivia.ToFullString().AsMemory()[2..]; } - - public override string CommentPrefix => "//"; - - protected override ReadOnlyMemory GetTextContextOfComment(SyntaxTrivia commentTrivia) + else if (commentTrivia.IsKind(SyntaxKind.MultiLineCommentTrivia)) { - if (commentTrivia.IsKind(SyntaxKind.SingleLineCommentTrivia)) - { - return commentTrivia.ToFullString().AsMemory()[2..]; - } - else if (commentTrivia.IsKind(SyntaxKind.MultiLineCommentTrivia)) - { - var triviaString = commentTrivia.ToFullString(); + var triviaString = commentTrivia.ToFullString(); - var startIndex = triviaString.IndexOf("/*", StringComparison.Ordinal) + 2; - var endIndex = triviaString.LastIndexOf("*/", StringComparison.Ordinal); - if (endIndex < startIndex) - { - // While editing, it is possible to have a multiline comment trivia that does not contain the closing '*/' yet. - return triviaString.AsMemory()[startIndex..]; - } - - return triviaString.AsMemory()[startIndex..endIndex]; - } - else + var startIndex = triviaString.IndexOf("/*", StringComparison.Ordinal) + 2; + var endIndex = triviaString.LastIndexOf("*/", StringComparison.Ordinal); + if (endIndex < startIndex) { - throw ExceptionUtilities.UnexpectedValue(commentTrivia.Kind()); + // While editing, it is possible to have a multiline comment trivia that does not contain the closing '*/' yet. + return triviaString.AsMemory()[startIndex..]; } + + return triviaString.AsMemory()[startIndex..endIndex]; + } + else + { + throw ExceptionUtilities.UnexpectedValue(commentTrivia.Kind()); } } } diff --git a/src/Analyzers/CSharp/Analyzers/ForEachCast/CSharpForEachCastDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/ForEachCast/CSharpForEachCastDiagnosticAnalyzer.cs index a0b966f177c7f..f036475e60334 100644 --- a/src/Analyzers/CSharp/Analyzers/ForEachCast/CSharpForEachCastDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/ForEachCast/CSharpForEachCastDiagnosticAnalyzer.cs @@ -10,24 +10,23 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.CSharp.Analyzers.ForEachCast +namespace Microsoft.CodeAnalysis.CSharp.Analyzers.ForEachCast; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpForEachCastDiagnosticAnalyzer : AbstractForEachCastDiagnosticAnalyzer< + SyntaxKind, + CommonForEachStatementSyntax> { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpForEachCastDiagnosticAnalyzer : AbstractForEachCastDiagnosticAnalyzer< - SyntaxKind, - CommonForEachStatementSyntax> - { - protected override ISyntaxFacts SyntaxFacts - => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts SyntaxFacts + => CSharpSyntaxFacts.Instance; - protected override ImmutableArray GetSyntaxKinds() - => [SyntaxKind.ForEachStatement, SyntaxKind.ForEachVariableStatement]; + protected override ImmutableArray GetSyntaxKinds() + => [SyntaxKind.ForEachStatement, SyntaxKind.ForEachVariableStatement]; - protected override (CommonConversion conversion, ITypeSymbol? collectionElementType) GetForEachInfo( - SemanticModel semanticModel, CommonForEachStatementSyntax node) - { - var info = semanticModel.GetForEachStatementInfo(node); - return (info.ElementConversion.ToCommonConversion(), info.ElementType); - } + protected override (CommonConversion conversion, ITypeSymbol? collectionElementType) GetForEachInfo( + SemanticModel semanticModel, CommonForEachStatementSyntax node) + { + var info = semanticModel.GetForEachStatementInfo(node); + return (info.ElementConversion.ToCommonConversion(), info.ElementType); } } diff --git a/src/Analyzers/CSharp/Analyzers/Formatting/CSharpFormattingAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/Formatting/CSharpFormattingAnalyzer.cs index 3f31dd0f3a08f..9d0948b7525c0 100644 --- a/src/Analyzers/CSharp/Analyzers/Formatting/CSharpFormattingAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/Formatting/CSharpFormattingAnalyzer.cs @@ -6,12 +6,11 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Formatting; -namespace Microsoft.CodeAnalysis.CodeStyle +namespace Microsoft.CodeAnalysis.CodeStyle; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpFormattingAnalyzer : AbstractFormattingAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpFormattingAnalyzer : AbstractFormattingAnalyzer - { - protected override ISyntaxFormatting SyntaxFormatting - => CSharpSyntaxFormatting.Instance; - } + protected override ISyntaxFormatting SyntaxFormatting + => CSharpSyntaxFormatting.Instance; } diff --git a/src/Analyzers/CSharp/Analyzers/InlineDeclaration/CSharpInlineDeclarationDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/InlineDeclaration/CSharpInlineDeclarationDiagnosticAnalyzer.cs index e234815776770..cc1998d92fddc 100644 --- a/src/Analyzers/CSharp/Analyzers/InlineDeclaration/CSharpInlineDeclarationDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/InlineDeclaration/CSharpInlineDeclarationDiagnosticAnalyzer.cs @@ -15,369 +15,369 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.InlineDeclaration +namespace Microsoft.CodeAnalysis.CSharp.InlineDeclaration; + +/// +/// Looks for code of the form: +/// +/// int i; +/// if (int.TryParse(s, out i)) { } +/// +/// And offers to convert it to: +/// +/// if (int.TryParse(s, out var i)) { } or +/// if (int.TryParse(s, out int i)) { } or +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpInlineDeclarationDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - /// - /// Looks for code of the form: - /// - /// int i; - /// if (int.TryParse(s, out i)) { } - /// - /// And offers to convert it to: - /// - /// if (int.TryParse(s, out var i)) { } or - /// if (int.TryParse(s, out int i)) { } or - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpInlineDeclarationDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + private const string CS0165 = nameof(CS0165); // Use of unassigned local variable 's' + + public CSharpInlineDeclarationDiagnosticAnalyzer() + : base(IDEDiagnosticIds.InlineDeclarationDiagnosticId, + EnforceOnBuildValues.InlineDeclaration, + CSharpCodeStyleOptions.PreferInlinedVariableDeclaration, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Inline_variable_declaration), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Variable_declaration_can_be_inlined), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - private const string CS0165 = nameof(CS0165); // Use of unassigned local variable 's' - - public CSharpInlineDeclarationDiagnosticAnalyzer() - : base(IDEDiagnosticIds.InlineDeclarationDiagnosticId, - EnforceOnBuildValues.InlineDeclaration, - CSharpCodeStyleOptions.PreferInlinedVariableDeclaration, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Inline_variable_declaration), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Variable_declaration_can_be_inlined), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(compilationContext => { + var compilation = compilationContext.Compilation; + var expressionType = compilation.GetTypeByMetadataName(typeof(Expression<>).FullName!); + + // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to + // get callbacks for Argument nodes, but analyze nodes across the entire code block + // and eventually report a diagnostic on the local declaration node. + // Without the containing CodeBlockStartAction, our reported diagnostic would be classified + // as a non-local diagnostic and would not participate in lightbulb for computing code fixes. + compilationContext.RegisterCodeBlockStartAction(blockStartContext => + blockStartContext.RegisterSyntaxNodeAction( + syntaxContext => AnalyzeSyntaxNode(syntaxContext, expressionType), SyntaxKind.Argument)); + }); + } + + private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context, INamedTypeSymbol? expressionType) + { + var syntaxTree = context.Node.SyntaxTree; + var csOptions = (CSharpParseOptions)syntaxTree.Options; + if (csOptions.LanguageVersion < LanguageVersion.CSharp7) + { + // out-vars are not supported prior to C# 7.0. + return; } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + var option = context.GetCSharpAnalyzerOptions().PreferInlinedVariableDeclaration; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + { + // Don't bother doing any work if the user doesn't even have this preference set. + return; + } - protected override void InitializeWorker(AnalysisContext context) + var argumentNode = (ArgumentSyntax)context.Node; + if (argumentNode.RefOrOutKeyword.Kind() != SyntaxKind.OutKeyword) { - context.RegisterCompilationStartAction(compilationContext => - { - var compilation = compilationContext.Compilation; - var expressionType = compilation.GetTypeByMetadataName(typeof(Expression<>).FullName!); - - // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to - // get callbacks for Argument nodes, but analyze nodes across the entire code block - // and eventually report a diagnostic on the local declaration node. - // Without the containing CodeBlockStartAction, our reported diagnostic would be classified - // as a non-local diagnostic and would not participate in lightbulb for computing code fixes. - compilationContext.RegisterCodeBlockStartAction(blockStartContext => - blockStartContext.RegisterSyntaxNodeAction( - syntaxContext => AnalyzeSyntaxNode(syntaxContext, expressionType), SyntaxKind.Argument)); - }); + // Immediately bail if this is not an out-argument. If it's not an out-argument + // we clearly can't convert it to be an out-variable-declaration. + return; } - private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context, INamedTypeSymbol? expressionType) + var argumentExpression = argumentNode.Expression; + if (argumentExpression is not IdentifierNameSyntax identifierName) { - var syntaxTree = context.Node.SyntaxTree; - var csOptions = (CSharpParseOptions)syntaxTree.Options; - if (csOptions.LanguageVersion < LanguageVersion.CSharp7) - { - // out-vars are not supported prior to C# 7.0. - return; - } + // has to be exactly the form "out i". i.e. "out this.i" or "out v[i]" are legal + // cases for out-arguments, but could not be converted to an out-variable-declaration. + return; + } - var option = context.GetCSharpAnalyzerOptions().PreferInlinedVariableDeclaration; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - { - // Don't bother doing any work if the user doesn't even have this preference set. - return; - } + if (argumentNode.Parent is not ArgumentListSyntax argumentList) + { + return; + } - var argumentNode = (ArgumentSyntax)context.Node; - if (argumentNode.RefOrOutKeyword.Kind() != SyntaxKind.OutKeyword) - { - // Immediately bail if this is not an out-argument. If it's not an out-argument - // we clearly can't convert it to be an out-variable-declaration. - return; - } + var invocationOrCreation = argumentList.Parent; + if (invocationOrCreation?.Kind() is not SyntaxKind.InvocationExpression and not SyntaxKind.ObjectCreationExpression) + { + // Out-variables are only legal with invocations and object creations. + // If we don't have one of those bail. Note: we need hte parent to be + // one of these forms so we can accurately verify that inlining the + // variable doesn't change semantics. + return; + } - var argumentExpression = argumentNode.Expression; - if (argumentExpression is not IdentifierNameSyntax identifierName) - { - // has to be exactly the form "out i". i.e. "out this.i" or "out v[i]" are legal - // cases for out-arguments, but could not be converted to an out-variable-declaration. - return; - } + // Don't offer to inline variables named "_". It can cause is to create a discard symbol + // which would cause a break. + if (identifierName.Identifier.ValueText == "_") + { + return; + } - if (argumentNode.Parent is not ArgumentListSyntax argumentList) - { - return; - } + var containingStatement = argumentExpression.FirstAncestorOrSelf(); + if (containingStatement == null) + { + return; + } - var invocationOrCreation = argumentList.Parent; - if (invocationOrCreation?.Kind() is not SyntaxKind.InvocationExpression and not SyntaxKind.ObjectCreationExpression) - { - // Out-variables are only legal with invocations and object creations. - // If we don't have one of those bail. Note: we need hte parent to be - // one of these forms so we can accurately verify that inlining the - // variable doesn't change semantics. - return; - } + var cancellationToken = context.CancellationToken; - // Don't offer to inline variables named "_". It can cause is to create a discard symbol - // which would cause a break. - if (identifierName.Identifier.ValueText == "_") - { - return; - } + var semanticModel = context.SemanticModel; + if (semanticModel.GetSymbolInfo(argumentExpression, cancellationToken).Symbol is not ILocalSymbol outLocalSymbol) + { + // The out-argument wasn't referencing a local. So we don't have an local + // declaration that we can attempt to inline here. + return; + } - var containingStatement = argumentExpression.FirstAncestorOrSelf(); - if (containingStatement == null) - { - return; - } + // Ensure that the local-symbol actually belongs to LocalDeclarationStatement. + // Trying to do things like inline a var-decl in a for-statement is just too + // esoteric and would make us have to write a lot more complex code to support + // that scenario. + var localReference = outLocalSymbol.DeclaringSyntaxReferences.FirstOrDefault(); + if (localReference?.GetSyntax(cancellationToken) is not VariableDeclaratorSyntax localDeclarator) + { + return; + } - var cancellationToken = context.CancellationToken; + var localDeclaration = localDeclarator.Parent as VariableDeclarationSyntax; + if (localDeclaration?.Parent is not LocalDeclarationStatementSyntax localStatement) + { + return; + } - var semanticModel = context.SemanticModel; - if (semanticModel.GetSymbolInfo(argumentExpression, cancellationToken).Symbol is not ILocalSymbol outLocalSymbol) - { - // The out-argument wasn't referencing a local. So we don't have an local - // declaration that we can attempt to inline here. - return; - } + // Bail out early if the localDeclaration is outside the context's analysis span. + if (!context.ShouldAnalyzeSpan(localDeclaration.Span)) + return; - // Ensure that the local-symbol actually belongs to LocalDeclarationStatement. - // Trying to do things like inline a var-decl in a for-statement is just too - // esoteric and would make us have to write a lot more complex code to support - // that scenario. - var localReference = outLocalSymbol.DeclaringSyntaxReferences.FirstOrDefault(); - if (localReference?.GetSyntax(cancellationToken) is not VariableDeclaratorSyntax localDeclarator) - { - return; - } + if (localDeclarator.SpanStart >= argumentNode.SpanStart) + { + // We have an error situation where the local was declared after the out-var. + // Don't even bother offering anything here. + return; + } - var localDeclaration = localDeclarator.Parent as VariableDeclarationSyntax; - if (localDeclaration?.Parent is not LocalDeclarationStatementSyntax localStatement) + // If the local has an initializer, only allow the refactoring if it is initialized + // with a simple literal or 'default' expression. i.e. it's ok to inline "var v = 0" + // since there are no side-effects of the initialization. However something like + // "var v = M()" should not be inlined as that could break program semantics. + if (localDeclarator.Initializer != null) + { + if (localDeclarator.Initializer.Value is not LiteralExpressionSyntax and + not DefaultExpressionSyntax) { return; } + } - // Bail out early if the localDeclaration is outside the context's analysis span. - if (!context.ShouldAnalyzeSpan(localDeclaration.Span)) - return; + // Get the block that the local is scoped inside of. We'll search that block + // for references to the local to make sure that no reads/writes happen before + // the out-argument. If there are any reads/writes we can't inline as those + // accesses will become invalid. + if (localStatement.Parent is not BlockSyntax enclosingBlockOfLocalStatement) + { + return; + } - if (localDeclarator.SpanStart >= argumentNode.SpanStart) - { - // We have an error situation where the local was declared after the out-var. - // Don't even bother offering anything here. - return; - } + if (argumentExpression.IsInExpressionTree(semanticModel, expressionType, cancellationToken)) + { + // out-vars are not allowed inside expression-trees. So don't offer to + // fix if we're inside one. + return; + } - // If the local has an initializer, only allow the refactoring if it is initialized - // with a simple literal or 'default' expression. i.e. it's ok to inline "var v = 0" - // since there are no side-effects of the initialization. However something like - // "var v = M()" should not be inlined as that could break program semantics. - if (localDeclarator.Initializer != null) - { - if (localDeclarator.Initializer.Value is not LiteralExpressionSyntax and - not DefaultExpressionSyntax) - { - return; - } - } + // Find the scope that the out-declaration variable will live in after we + // rewrite things. + var outArgumentScope = GetOutArgumentScope(argumentExpression); + if (outArgumentScope == null) + return; - // Get the block that the local is scoped inside of. We'll search that block - // for references to the local to make sure that no reads/writes happen before - // the out-argument. If there are any reads/writes we can't inline as those - // accesses will become invalid. - if (localStatement.Parent is not BlockSyntax enclosingBlockOfLocalStatement) - { - return; - } + if (!outLocalSymbol.CanSafelyMoveLocalToBlock(enclosingBlockOfLocalStatement, outArgumentScope)) + { + return; + } - if (argumentExpression.IsInExpressionTree(semanticModel, expressionType, cancellationToken)) - { - // out-vars are not allowed inside expression-trees. So don't offer to - // fix if we're inside one. - return; - } + // Make sure that variable is not accessed outside of that scope. + var dataFlow = semanticModel.AnalyzeDataFlow(outArgumentScope); + if (dataFlow.ReadOutside.Contains(outLocalSymbol) || dataFlow.WrittenOutside.Contains(outLocalSymbol)) + { + // The variable is read or written from outside the block that the new variable + // would be scoped in. This would cause a break. + // + // Note(cyrusn): We could still offer the refactoring, but just show an error in the + // preview in this case. + return; + } - // Find the scope that the out-declaration variable will live in after we - // rewrite things. - var outArgumentScope = GetOutArgumentScope(argumentExpression); - if (outArgumentScope == null) - return; + // Make sure the variable isn't ever accessed before the usage in this out-var. + if (IsAccessed(semanticModel, outLocalSymbol, enclosingBlockOfLocalStatement, + localStatement, argumentNode, cancellationToken)) + { + return; + } - if (!outLocalSymbol.CanSafelyMoveLocalToBlock(enclosingBlockOfLocalStatement, outArgumentScope)) - { - return; - } + // See if inlining this variable would make it so that some variables were no + // longer definitely assigned. + if (WouldCauseDefiniteAssignmentErrors(semanticModel, localStatement, + enclosingBlockOfLocalStatement, outLocalSymbol)) + { + return; + } + + // Collect some useful nodes for the fix provider to use so it doesn't have to + // find them again. + var allLocations = ImmutableArray.Create( + localDeclarator.GetLocation(), + identifierName.GetLocation(), + invocationOrCreation.GetLocation()); + + // If the local variable only has one declarator, then report the suggestion on the whole + // declaration. Otherwise, return the suggestion only on the single declarator. + var reportNode = localDeclaration.Variables.Count == 1 + ? (SyntaxNode)localDeclaration + : localDeclarator; + + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + reportNode.GetLocation(), + option.Notification, + context.Options, + additionalLocations: allLocations, + properties: null)); + } - // Make sure that variable is not accessed outside of that scope. - var dataFlow = semanticModel.AnalyzeDataFlow(outArgumentScope); - if (dataFlow.ReadOutside.Contains(outLocalSymbol) || dataFlow.WrittenOutside.Contains(outLocalSymbol)) + private static bool WouldCauseDefiniteAssignmentErrors( + SemanticModel semanticModel, + LocalDeclarationStatementSyntax localStatement, + BlockSyntax enclosingBlock, + ILocalSymbol outLocalSymbol) + { + // See if we have something like: + // + // int i = 0; + // if (Goo() || Bar(out i)) + // { + // Console.WriteLine(i); + // } + // + // In this case, inlining the 'i' would cause it to longer be definitely + // assigned in the WriteLine invocation. + var nextStatement = localStatement.GetNextStatement(); + Contract.ThrowIfNull(nextStatement); + + var dataFlow = semanticModel.AnalyzeDataFlow( + nextStatement, + enclosingBlock.Statements.Last()); + Contract.ThrowIfNull(dataFlow); + return dataFlow.DataFlowsIn.Contains(outLocalSymbol); + } + + private static SyntaxNode? GetOutArgumentScope(SyntaxNode argumentExpression) + { + for (var current = argumentExpression; current != null; current = current.Parent) + { + if (current.Parent is LambdaExpressionSyntax lambda && + current == lambda.Body) { - // The variable is read or written from outside the block that the new variable - // would be scoped in. This would cause a break. - // - // Note(cyrusn): We could still offer the refactoring, but just show an error in the - // preview in this case. - return; + // We were in a lambda. The lambda body will be the new scope of the + // out var. + return current; } - // Make sure the variable isn't ever accessed before the usage in this out-var. - if (IsAccessed(semanticModel, outLocalSymbol, enclosingBlockOfLocalStatement, - localStatement, argumentNode, cancellationToken)) + // Any loop construct defines a scope for out-variables, as well as each of the following: + // * Using statements + // * Fixed statements + // * Try statements (specifically for exception filters) + switch (current.Kind()) { - return; + case SyntaxKind.WhileStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForEachStatement: + case SyntaxKind.UsingStatement: + case SyntaxKind.FixedStatement: + case SyntaxKind.TryStatement: + return current; } - // See if inlining this variable would make it so that some variables were no - // longer definitely assigned. - if (WouldCauseDefiniteAssignmentErrors(semanticModel, localStatement, - enclosingBlockOfLocalStatement, outLocalSymbol)) + if (current is StatementSyntax) { - return; + // We hit a statement containing the out-argument. Statements can have one of + // two forms. They're either parented by a block, or by another statement + // (i.e. they're an embedded statement). If we're parented by a block, then + // that block will be the scope of the new out-var. + // + // However, if our containing statement is not parented by a block, then that + // means we have something like: + // + // if (x) + // if (Try(out y)) + // + // In this case, there is a 'virtual' block scope surrounding the embedded 'if' + // statement, and that will be the scope the out-var goes into. + return current.IsParentKind(SyntaxKind.Block) + ? current.Parent + : current; } - - // Collect some useful nodes for the fix provider to use so it doesn't have to - // find them again. - var allLocations = ImmutableArray.Create( - localDeclarator.GetLocation(), - identifierName.GetLocation(), - invocationOrCreation.GetLocation()); - - // If the local variable only has one declarator, then report the suggestion on the whole - // declaration. Otherwise, return the suggestion only on the single declarator. - var reportNode = localDeclaration.Variables.Count == 1 - ? (SyntaxNode)localDeclaration - : localDeclarator; - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - reportNode.GetLocation(), - option.Notification, - additionalLocations: allLocations, - properties: null)); } - private static bool WouldCauseDefiniteAssignmentErrors( - SemanticModel semanticModel, - LocalDeclarationStatementSyntax localStatement, - BlockSyntax enclosingBlock, - ILocalSymbol outLocalSymbol) - { - // See if we have something like: - // - // int i = 0; - // if (Goo() || Bar(out i)) - // { - // Console.WriteLine(i); - // } - // - // In this case, inlining the 'i' would cause it to longer be definitely - // assigned in the WriteLine invocation. - var nextStatement = localStatement.GetNextStatement(); - Contract.ThrowIfNull(nextStatement); - - var dataFlow = semanticModel.AnalyzeDataFlow( - nextStatement, - enclosingBlock.Statements.Last()); - Contract.ThrowIfNull(dataFlow); - return dataFlow.DataFlowsIn.Contains(outLocalSymbol); - } + return null; + } - private static SyntaxNode? GetOutArgumentScope(SyntaxNode argumentExpression) + private static bool IsAccessed( + SemanticModel semanticModel, + ISymbol outSymbol, + BlockSyntax enclosingBlockOfLocalStatement, + LocalDeclarationStatementSyntax localStatement, + ArgumentSyntax argumentNode, + CancellationToken cancellationToken) + { + var localStatementStart = localStatement.Span.Start; + var argumentNodeStart = argumentNode.Span.Start; + var variableName = outSymbol.Name; + + // Walk the block that the local is declared in looking for accesses. + // We can ignore anything prior to the actual local declaration point, + // and we only need to check up until we reach the out-argument. + foreach (var descendentNode in enclosingBlockOfLocalStatement.DescendantNodes()) { - for (var current = argumentExpression; current != null; current = current.Parent) + var descendentStart = descendentNode.Span.Start; + if (descendentStart <= localStatementStart) { - if (current.Parent is LambdaExpressionSyntax lambda && - current == lambda.Body) - { - // We were in a lambda. The lambda body will be the new scope of the - // out var. - return current; - } - - // Any loop construct defines a scope for out-variables, as well as each of the following: - // * Using statements - // * Fixed statements - // * Try statements (specifically for exception filters) - switch (current.Kind()) - { - case SyntaxKind.WhileStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.ForEachStatement: - case SyntaxKind.UsingStatement: - case SyntaxKind.FixedStatement: - case SyntaxKind.TryStatement: - return current; - } - - if (current is StatementSyntax) - { - // We hit a statement containing the out-argument. Statements can have one of - // two forms. They're either parented by a block, or by another statement - // (i.e. they're an embedded statement). If we're parented by a block, then - // that block will be the scope of the new out-var. - // - // However, if our containing statement is not parented by a block, then that - // means we have something like: - // - // if (x) - // if (Try(out y)) - // - // In this case, there is a 'virtual' block scope surrounding the embedded 'if' - // statement, and that will be the scope the out-var goes into. - return current.IsParentKind(SyntaxKind.Block) - ? current.Parent - : current; - } + // This node is before the local declaration. Can ignore it entirely as it could + // not be an access to the local. + continue; } - return null; - } - - private static bool IsAccessed( - SemanticModel semanticModel, - ISymbol outSymbol, - BlockSyntax enclosingBlockOfLocalStatement, - LocalDeclarationStatementSyntax localStatement, - ArgumentSyntax argumentNode, - CancellationToken cancellationToken) - { - var localStatementStart = localStatement.Span.Start; - var argumentNodeStart = argumentNode.Span.Start; - var variableName = outSymbol.Name; - - // Walk the block that the local is declared in looking for accesses. - // We can ignore anything prior to the actual local declaration point, - // and we only need to check up until we reach the out-argument. - foreach (var descendentNode in enclosingBlockOfLocalStatement.DescendantNodes()) + if (descendentStart >= argumentNodeStart) { - var descendentStart = descendentNode.Span.Start; - if (descendentStart <= localStatementStart) - { - // This node is before the local declaration. Can ignore it entirely as it could - // not be an access to the local. - continue; - } - - if (descendentStart >= argumentNodeStart) - { - // We reached the out-var. We can stop searching entirely. - break; - } + // We reached the out-var. We can stop searching entirely. + break; + } - if (descendentNode is IdentifierNameSyntax identifierName) + if (descendentNode is IdentifierNameSyntax identifierName) + { + // See if this looks like an accessor to the local variable syntactically. + if (identifierName.Identifier.ValueText == variableName) { - // See if this looks like an accessor to the local variable syntactically. - if (identifierName.Identifier.ValueText == variableName) + // Confirm that it is a access of the local. + var symbol = semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol; + if (outSymbol.Equals(symbol)) { - // Confirm that it is a access of the local. - var symbol = semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol; - if (outSymbol.Equals(symbol)) - { - // We definitely accessed the local before the out-argument. We - // can't inline this local. - return true; - } + // We definitely accessed the local before the out-argument. We + // can't inline this local. + return true; } } } - - // No accesses detected - return false; } + + // No accesses detected + return false; } } diff --git a/src/Analyzers/CSharp/Analyzers/InvokeDelegateWithConditionalAccess/InvokeDelegateWithConditionalAccessAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/InvokeDelegateWithConditionalAccess/InvokeDelegateWithConditionalAccessAnalyzer.cs index 49ffd3759fa49..34e8fe6152d2a 100644 --- a/src/Analyzers/CSharp/Analyzers/InvokeDelegateWithConditionalAccess/InvokeDelegateWithConditionalAccessAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/InvokeDelegateWithConditionalAccess/InvokeDelegateWithConditionalAccessAnalyzer.cs @@ -13,323 +13,325 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.InvokeDelegateWithConditionalAccess +namespace Microsoft.CodeAnalysis.CSharp.InvokeDelegateWithConditionalAccess; + +internal static class Constants +{ + public const string Kind = nameof(Kind); + public const string VariableAndIfStatementForm = nameof(VariableAndIfStatementForm); + public const string SingleIfStatementForm = nameof(SingleIfStatementForm); +} + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class InvokeDelegateWithConditionalAccessAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - internal static class Constants + public InvokeDelegateWithConditionalAccessAnalyzer() + : base(IDEDiagnosticIds.InvokeDelegateWithConditionalAccessId, + EnforceOnBuildValues.InvokeDelegateWithConditionalAccess, + CSharpCodeStyleOptions.PreferConditionalDelegateCall, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Delegate_invocation_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public const string Kind = nameof(Kind); - public const string VariableAndIfStatementForm = nameof(VariableAndIfStatementForm); - public const string SingleIfStatementForm = nameof(SingleIfStatementForm); } - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class InvokeDelegateWithConditionalAccessAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(SyntaxNodeAction, SyntaxKind.IfStatement); + + private void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext) { - public InvokeDelegateWithConditionalAccessAnalyzer() - : base(IDEDiagnosticIds.InvokeDelegateWithConditionalAccessId, - EnforceOnBuildValues.InvokeDelegateWithConditionalAccess, - CSharpCodeStyleOptions.PreferConditionalDelegateCall, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Delegate_invocation_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferConditionalDelegateCall; + if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) { + // Bail if the user has disabled this feature. + return; } - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(SyntaxNodeAction, SyntaxKind.IfStatement); + // look for the form "if (a != null)" or "if (null != a)" + var ifStatement = (IfStatementSyntax)syntaxContext.Node; - private void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext) + // ?. is only available in C# 6.0 and above. Don't offer this refactoring + // in projects targeting a lesser version. + if (ifStatement.SyntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp6) { - var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferConditionalDelegateCall; - if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) - { - // Bail if the user has disabled this feature. - return; - } - - // look for the form "if (a != null)" or "if (null != a)" - var ifStatement = (IfStatementSyntax)syntaxContext.Node; + return; + } - // ?. is only available in C# 6.0 and above. Don't offer this refactoring - // in projects targeting a lesser version. - if (ifStatement.SyntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp6) - { - return; - } + if (!ifStatement.Condition.IsKind(SyntaxKind.NotEqualsExpression)) + { + return; + } - if (!ifStatement.Condition.IsKind(SyntaxKind.NotEqualsExpression)) - { - return; - } + if (ifStatement.Else != null) + { + return; + } - if (ifStatement.Else != null) + // Check for both: "if (...) { a(); }" and "if (...) a();" + var innerStatement = ifStatement.Statement; + if (innerStatement is BlockSyntax block) + { + if (block.Statements.Count != 1) { return; } - // Check for both: "if (...) { a(); }" and "if (...) a();" - var innerStatement = ifStatement.Statement; - if (innerStatement is BlockSyntax block) - { - if (block.Statements.Count != 1) - { - return; - } - - innerStatement = block.Statements[0]; - } - - if (innerStatement is not ExpressionStatementSyntax expressionStatement) - { - return; - } + innerStatement = block.Statements[0]; + } - // Check that it's of the form: "if (a != null) { a(); } - if (expressionStatement.Expression is not InvocationExpressionSyntax invocationExpression) - { - return; - } + if (innerStatement is not ExpressionStatementSyntax expressionStatement) + { + return; + } - // Function pointers can only be invoked directly via the "()" operator, not "?.Invoke()". - if (syntaxContext.SemanticModel.GetTypeInfo(invocationExpression.Expression, syntaxContext.CancellationToken).Type is { TypeKind: TypeKind.FunctionPointer }) - { - return; - } + // Check that it's of the form: "if (a != null) { a(); } + if (expressionStatement.Expression is not InvocationExpressionSyntax invocationExpression) + { + return; + } - var condition = (BinaryExpressionSyntax)ifStatement.Condition; - if (TryCheckVariableAndIfStatementForm( - syntaxContext, ifStatement, condition, - expressionStatement, invocationExpression, - styleOption.Notification)) - { - return; - } + // Function pointers can only be invoked directly via the "()" operator, not "?.Invoke()". + if (syntaxContext.SemanticModel.GetTypeInfo(invocationExpression.Expression, syntaxContext.CancellationToken).Type is { TypeKind: TypeKind.FunctionPointer }) + { + return; + } - TryCheckSingleIfStatementForm( + var condition = (BinaryExpressionSyntax)ifStatement.Condition; + if (TryCheckVariableAndIfStatementForm( syntaxContext, ifStatement, condition, expressionStatement, invocationExpression, - styleOption.Notification); + styleOption.Notification)) + { + return; } - private bool TryCheckSingleIfStatementForm( - SyntaxNodeAnalysisContext syntaxContext, - IfStatementSyntax ifStatement, - BinaryExpressionSyntax condition, - ExpressionStatementSyntax expressionStatement, - InvocationExpressionSyntax invocationExpression, - NotificationOption2 notificationOption) - { - // Look for the form: "if (someExpr != null) someExpr()" - if (condition.Left.IsKind(SyntaxKind.NullLiteralExpression) || - condition.Right.IsKind(SyntaxKind.NullLiteralExpression)) - { - var expr = condition.Left.IsKind(SyntaxKind.NullLiteralExpression) - ? condition.Right - : condition.Left; - - if (InvocationExpressionIsEquivalent(expr, invocationExpression)) - { - // Looks good! - var tree = syntaxContext.SemanticModel.SyntaxTree; - var additionalLocations = ImmutableArray.Create( - Location.Create(tree, ifStatement.Span), - Location.Create(tree, expressionStatement.Span)); - - ReportDiagnostics( - syntaxContext, ifStatement, ifStatement, - expressionStatement, notificationOption, additionalLocations, - Constants.SingleIfStatementForm); - - return true; - } - } + TryCheckSingleIfStatementForm( + syntaxContext, ifStatement, condition, + expressionStatement, invocationExpression, + styleOption.Notification); + } - return false; + private bool TryCheckSingleIfStatementForm( + SyntaxNodeAnalysisContext syntaxContext, + IfStatementSyntax ifStatement, + BinaryExpressionSyntax condition, + ExpressionStatementSyntax expressionStatement, + InvocationExpressionSyntax invocationExpression, + NotificationOption2 notificationOption) + { + // Look for the form: "if (someExpr != null) someExpr()" + if (condition.Left.IsKind(SyntaxKind.NullLiteralExpression) || + condition.Right.IsKind(SyntaxKind.NullLiteralExpression)) + { + var expr = condition.Left.IsKind(SyntaxKind.NullLiteralExpression) + ? condition.Right + : condition.Left; - static bool InvocationExpressionIsEquivalent(ExpressionSyntax expression, InvocationExpressionSyntax invocationExpression) + if (InvocationExpressionIsEquivalent(expr, invocationExpression)) { - // expr(...) - if (SyntaxFactory.AreEquivalent(expression, invocationExpression.Expression, topLevel: false)) - return true; - - // expr.Invoke(...); - if (invocationExpression.Expression is MemberAccessExpressionSyntax { Name: IdentifierNameSyntax { Identifier.ValueText: nameof(Action.Invoke) } } memberAccessExpression && - SyntaxFactory.AreEquivalent(expression, memberAccessExpression.Expression, topLevel: false)) - { - return true; - } - - return false; + // Looks good! + var tree = syntaxContext.SemanticModel.SyntaxTree; + var additionalLocations = ImmutableArray.Create( + Location.Create(tree, ifStatement.Span), + Location.Create(tree, expressionStatement.Span)); + + ReportDiagnostics( + syntaxContext, ifStatement, ifStatement, + expressionStatement, notificationOption, additionalLocations, + Constants.SingleIfStatementForm); + + return true; } } - private void ReportDiagnostics( - SyntaxNodeAnalysisContext syntaxContext, - StatementSyntax firstStatement, - IfStatementSyntax ifStatement, - ExpressionStatementSyntax expressionStatement, - NotificationOption2 notificationOption, - ImmutableArray additionalLocations, - string kind) + return false; + + static bool InvocationExpressionIsEquivalent(ExpressionSyntax expression, InvocationExpressionSyntax invocationExpression) { - var tree = syntaxContext.Node.SyntaxTree; + // expr(...) + if (SyntaxFactory.AreEquivalent(expression, invocationExpression.Expression, topLevel: false)) + return true; - var properties = ImmutableDictionary.Empty.Add( - Constants.Kind, kind); + // expr.Invoke(...); + if (invocationExpression.Expression is MemberAccessExpressionSyntax { Name: IdentifierNameSyntax { Identifier.ValueText: nameof(Action.Invoke) } } memberAccessExpression && + SyntaxFactory.AreEquivalent(expression, memberAccessExpression.Expression, topLevel: false)) + { + return true; + } - var previousToken = expressionStatement.GetFirstToken().GetPreviousToken(); - var nextToken = expressionStatement.GetLastToken().GetNextToken(); + return false; + } + } - // Fade out the code up to the expression statement. - var fadeLocation = Location.Create(tree, TextSpan.FromBounds(firstStatement.SpanStart, previousToken.Span.End)); + private void ReportDiagnostics( + SyntaxNodeAnalysisContext syntaxContext, + StatementSyntax firstStatement, + IfStatementSyntax ifStatement, + ExpressionStatementSyntax expressionStatement, + NotificationOption2 notificationOption, + ImmutableArray additionalLocations, + string kind) + { + var tree = syntaxContext.Node.SyntaxTree; + + var properties = ImmutableDictionary.Empty.Add( + Constants.Kind, kind); + + var previousToken = expressionStatement.GetFirstToken().GetPreviousToken(); + var nextToken = expressionStatement.GetLastToken().GetNextToken(); + + // Fade out the code up to the expression statement. + var fadeLocation = Location.Create(tree, TextSpan.FromBounds(firstStatement.SpanStart, previousToken.Span.End)); + syntaxContext.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( + Descriptor, + fadeLocation, + NotificationOption2.ForSeverity(Descriptor.DefaultSeverity), + syntaxContext.Options, + additionalLocations, + additionalUnnecessaryLocations: [fadeLocation], + properties)); + + // Put a diagnostic with the appropriate severity on the expression-statement itself. + syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + expressionStatement.GetLocation(), + notificationOption, + syntaxContext.Options, + additionalLocations, properties)); + + // If the if-statement extends past the expression statement, then fade out the rest. + if (nextToken.Span.Start < ifStatement.Span.End) + { + fadeLocation = Location.Create(tree, TextSpan.FromBounds(nextToken.Span.Start, ifStatement.Span.End)); syntaxContext.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( Descriptor, fadeLocation, NotificationOption2.ForSeverity(Descriptor.DefaultSeverity), + syntaxContext.Options, additionalLocations, additionalUnnecessaryLocations: [fadeLocation], properties)); + } + } - // Put a diagnostic with the appropriate severity on the expression-statement itself. - syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - expressionStatement.GetLocation(), - notificationOption, - additionalLocations, properties)); + private bool TryCheckVariableAndIfStatementForm( + SyntaxNodeAnalysisContext syntaxContext, + IfStatementSyntax ifStatement, + BinaryExpressionSyntax condition, + ExpressionStatementSyntax expressionStatement, + InvocationExpressionSyntax invocationExpression, + NotificationOption2 notificationOption) + { + var cancellationToken = syntaxContext.CancellationToken; + cancellationToken.ThrowIfCancellationRequested(); - // If the if-statement extends past the expression statement, then fade out the rest. - if (nextToken.Span.Start < ifStatement.Span.End) - { - fadeLocation = Location.Create(tree, TextSpan.FromBounds(nextToken.Span.Start, ifStatement.Span.End)); - syntaxContext.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( - Descriptor, - fadeLocation, - NotificationOption2.ForSeverity(Descriptor.DefaultSeverity), - additionalLocations, - additionalUnnecessaryLocations: [fadeLocation], - properties)); - } + // look for the form "if (a != null)" or "if (null != a)" + if (!ifStatement.Parent.IsKind(SyntaxKind.Block)) + { + return false; } - private bool TryCheckVariableAndIfStatementForm( - SyntaxNodeAnalysisContext syntaxContext, - IfStatementSyntax ifStatement, - BinaryExpressionSyntax condition, - ExpressionStatementSyntax expressionStatement, - InvocationExpressionSyntax invocationExpression, - NotificationOption2 notificationOption) + if (!IsNullCheckExpression(condition.Left, condition.Right) && + !IsNullCheckExpression(condition.Right, condition.Left)) { - var cancellationToken = syntaxContext.CancellationToken; - cancellationToken.ThrowIfCancellationRequested(); + return false; + } - // look for the form "if (a != null)" or "if (null != a)" - if (!ifStatement.Parent.IsKind(SyntaxKind.Block)) + var invocationName = invocationExpression.Expression switch + { + IdentifierNameSyntax identifier => identifier, + MemberAccessExpressionSyntax { - return false; - } + Name: IdentifierNameSyntax { Identifier.ValueText: nameof(Action.Invoke) }, + Expression: IdentifierNameSyntax identifier + } => identifier, + _ => null + }; - if (!IsNullCheckExpression(condition.Left, condition.Right) && - !IsNullCheckExpression(condition.Right, condition.Left)) - { - return false; - } - - var invocationName = invocationExpression.Expression switch - { - IdentifierNameSyntax identifier => identifier, - MemberAccessExpressionSyntax - { - Name: IdentifierNameSyntax { Identifier.ValueText: nameof(Action.Invoke) }, - Expression: IdentifierNameSyntax identifier - } => identifier, - _ => null - }; - - if (invocationName is null) - return false; - - var conditionName = condition.Left is IdentifierNameSyntax - ? (IdentifierNameSyntax)condition.Left - : (IdentifierNameSyntax)condition.Right; - - if (!Equals(conditionName.Identifier.ValueText, invocationName.Identifier.ValueText)) - { - return false; - } + if (invocationName is null) + return false; - // Now make sure the previous statement is "var a = ..." - var parentBlock = (BlockSyntax)ifStatement.Parent; - var ifIndex = parentBlock.Statements.IndexOf(ifStatement); - if (ifIndex == 0) - { - return false; - } + var conditionName = condition.Left is IdentifierNameSyntax + ? (IdentifierNameSyntax)condition.Left + : (IdentifierNameSyntax)condition.Right; - var previousStatement = parentBlock.Statements[ifIndex - 1]; - if (previousStatement is not LocalDeclarationStatementSyntax localDeclarationStatement) - { - return false; - } + if (!Equals(conditionName.Identifier.ValueText, invocationName.Identifier.ValueText)) + { + return false; + } - var variableDeclaration = localDeclarationStatement.Declaration; + // Now make sure the previous statement is "var a = ..." + var parentBlock = (BlockSyntax)ifStatement.Parent; + var ifIndex = parentBlock.Statements.IndexOf(ifStatement); + if (ifIndex == 0) + { + return false; + } - if (variableDeclaration.Variables.Count != 1) - { - return false; - } + var previousStatement = parentBlock.Statements[ifIndex - 1]; + if (previousStatement is not LocalDeclarationStatementSyntax localDeclarationStatement) + { + return false; + } - var declarator = variableDeclaration.Variables[0]; - if (declarator.Initializer == null) - { - return false; - } + var variableDeclaration = localDeclarationStatement.Declaration; - cancellationToken.ThrowIfCancellationRequested(); - if (!Equals(declarator.Identifier.ValueText, conditionName.Identifier.ValueText)) - { - return false; - } + if (variableDeclaration.Variables.Count != 1) + { + return false; + } - // Syntactically this looks good. Now make sure that the local is a delegate type. - var semanticModel = syntaxContext.SemanticModel; + var declarator = variableDeclaration.Variables[0]; + if (declarator.Initializer == null) + { + return false; + } - // The initializer can't be inlined if it's an actual lambda/method reference. - // These cannot be invoked with `?.` (only delegate *values* can be). - var initializer = declarator.Initializer.Value.WalkDownParentheses(); - if (initializer is AnonymousFunctionExpressionSyntax) - return false; + cancellationToken.ThrowIfCancellationRequested(); + if (!Equals(declarator.Identifier.ValueText, conditionName.Identifier.ValueText)) + { + return false; + } - var initializerSymbol = semanticModel.GetSymbolInfo(initializer, cancellationToken).GetAnySymbol(); - if (initializerSymbol is IMethodSymbol) - { - return false; - } + // Syntactically this looks good. Now make sure that the local is a delegate type. + var semanticModel = syntaxContext.SemanticModel; - var localSymbol = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(declarator, cancellationToken); + // The initializer can't be inlined if it's an actual lambda/method reference. + // These cannot be invoked with `?.` (only delegate *values* can be). + var initializer = declarator.Initializer.Value.WalkDownParentheses(); + if (initializer is AnonymousFunctionExpressionSyntax) + return false; - // Ok, we made a local just to check it for null and invoke it. Looks like something - // we can suggest an improvement for! - // But first make sure we're only using the local only within the body of this if statement. - var analysis = semanticModel.AnalyzeDataFlow(localDeclarationStatement, ifStatement); - if (analysis == null || analysis.ReadOutside.Contains(localSymbol) || analysis.WrittenOutside.Contains(localSymbol)) - return false; + var initializerSymbol = semanticModel.GetSymbolInfo(initializer, cancellationToken).GetAnySymbol(); + if (initializerSymbol is IMethodSymbol) + { + return false; + } - // Looks good! - var tree = semanticModel.SyntaxTree; - var additionalLocations = ImmutableArray.Create( - Location.Create(tree, localDeclarationStatement.Span), - Location.Create(tree, ifStatement.Span), - Location.Create(tree, expressionStatement.Span)); + var localSymbol = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(declarator, cancellationToken); - ReportDiagnostics(syntaxContext, - localDeclarationStatement, ifStatement, expressionStatement, - notificationOption, additionalLocations, Constants.VariableAndIfStatementForm); + // Ok, we made a local just to check it for null and invoke it. Looks like something + // we can suggest an improvement for! + // But first make sure we're only using the local only within the body of this if statement. + var analysis = semanticModel.AnalyzeDataFlow(localDeclarationStatement, ifStatement); + if (analysis == null || analysis.ReadOutside.Contains(localSymbol) || analysis.WrittenOutside.Contains(localSymbol)) + return false; - return true; - } + // Looks good! + var tree = semanticModel.SyntaxTree; + var additionalLocations = ImmutableArray.Create( + Location.Create(tree, localDeclarationStatement.Span), + Location.Create(tree, ifStatement.Span), + Location.Create(tree, expressionStatement.Span)); - private static bool IsNullCheckExpression(ExpressionSyntax left, ExpressionSyntax right) - => left.IsKind(SyntaxKind.IdentifierName) && right.IsKind(SyntaxKind.NullLiteralExpression); + ReportDiagnostics(syntaxContext, + localDeclarationStatement, ifStatement, expressionStatement, + notificationOption, additionalLocations, Constants.VariableAndIfStatementForm); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + return true; } + + private static bool IsNullCheckExpression(ExpressionSyntax left, ExpressionSyntax right) + => left.IsKind(SyntaxKind.IdentifierName) && right.IsKind(SyntaxKind.NullLiteralExpression); + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; } diff --git a/src/Analyzers/CSharp/Analyzers/MakeFieldReadonly/CSharpMakeFieldReadonlyDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/MakeFieldReadonly/CSharpMakeFieldReadonlyDiagnosticAnalyzer.cs index 96e84a44f4034..4ac357e262353 100644 --- a/src/Analyzers/CSharp/Analyzers/MakeFieldReadonly/CSharpMakeFieldReadonlyDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/MakeFieldReadonly/CSharpMakeFieldReadonlyDiagnosticAnalyzer.cs @@ -10,15 +10,14 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.MakeFieldReadonly; -namespace Microsoft.CodeAnalysis.CSharp.Analyzers.MakeFieldReadonly +namespace Microsoft.CodeAnalysis.CSharp.Analyzers.MakeFieldReadonly; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpMakeFieldReadonlyDiagnosticAnalyzer + : AbstractMakeFieldReadonlyDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpMakeFieldReadonlyDiagnosticAnalyzer - : AbstractMakeFieldReadonlyDiagnosticAnalyzer - { - protected override ISyntaxKinds SyntaxKinds => CSharpSyntaxKinds.Instance; + protected override ISyntaxKinds SyntaxKinds => CSharpSyntaxKinds.Instance; - protected override bool IsWrittenTo(SemanticModel semanticModel, ThisExpressionSyntax expression, CancellationToken cancellationToken) - => expression.IsWrittenTo(semanticModel, cancellationToken); - } + protected override bool IsWrittenTo(SemanticModel semanticModel, ThisExpressionSyntax expression, CancellationToken cancellationToken) + => expression.IsWrittenTo(semanticModel, cancellationToken); } diff --git a/src/Analyzers/CSharp/Analyzers/MakeLocalFunctionStatic/MakeLocalFunctionStaticDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/MakeLocalFunctionStatic/MakeLocalFunctionStaticDiagnosticAnalyzer.cs index ff1666e250414..e5543f6aebef7 100644 --- a/src/Analyzers/CSharp/Analyzers/MakeLocalFunctionStatic/MakeLocalFunctionStaticDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/MakeLocalFunctionStatic/MakeLocalFunctionStaticDiagnosticAnalyzer.cs @@ -9,54 +9,54 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic +namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class MakeLocalFunctionStaticDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class MakeLocalFunctionStaticDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public MakeLocalFunctionStaticDiagnosticAnalyzer() + : base(IDEDiagnosticIds.MakeLocalFunctionStaticDiagnosticId, + EnforceOnBuildValues.MakeLocalFunctionStatic, + CSharpCodeStyleOptions.PreferStaticLocalFunction, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Make_local_function_static), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Local_function_can_be_made_static), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public MakeLocalFunctionStaticDiagnosticAnalyzer() - : base(IDEDiagnosticIds.MakeLocalFunctionStaticDiagnosticId, - EnforceOnBuildValues.MakeLocalFunctionStatic, - CSharpCodeStyleOptions.PreferStaticLocalFunction, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Make_local_function_static), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Local_function_can_be_made_static), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => { - } + if (MakeLocalFunctionStaticHelper.IsStaticLocalFunctionSupported(context.Compilation.LanguageVersion())) + context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.LocalFunctionStatement); + }); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var localFunction = (LocalFunctionStatementSyntax)context.Node; + if (localFunction.Modifiers.Any(SyntaxKind.StaticKeyword)) + { + return; + } - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - { - if (MakeLocalFunctionStaticHelper.IsStaticLocalFunctionSupported(context.Compilation.LanguageVersion())) - context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.LocalFunctionStatement); - }); + var option = context.GetCSharpAnalyzerOptions().PreferStaticLocalFunction; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + { + return; + } - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + var semanticModel = context.SemanticModel; + if (MakeLocalFunctionStaticHelper.CanMakeLocalFunctionStaticBecauseNoCaptures(localFunction, semanticModel)) { - var localFunction = (LocalFunctionStatementSyntax)context.Node; - if (localFunction.Modifiers.Any(SyntaxKind.StaticKeyword)) - { - return; - } - - var option = context.GetCSharpAnalyzerOptions().PreferStaticLocalFunction; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - { - return; - } - - var semanticModel = context.SemanticModel; - if (MakeLocalFunctionStaticHelper.CanMakeLocalFunctionStaticBecauseNoCaptures(localFunction, semanticModel)) - { - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - localFunction.Identifier.GetLocation(), - option.Notification, - additionalLocations: ImmutableArray.Create(localFunction.GetLocation()), - properties: null)); - } + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + localFunction.Identifier.GetLocation(), + option.Notification, + context.Options, + additionalLocations: ImmutableArray.Create(localFunction.GetLocation()), + properties: null)); } } } diff --git a/src/Analyzers/CSharp/Analyzers/MakeLocalFunctionStatic/MakeLocalFunctionStaticHelper.cs b/src/Analyzers/CSharp/Analyzers/MakeLocalFunctionStatic/MakeLocalFunctionStaticHelper.cs index dda14ac9cc879..ef5c0a426f96f 100644 --- a/src/Analyzers/CSharp/Analyzers/MakeLocalFunctionStatic/MakeLocalFunctionStaticHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/MakeLocalFunctionStatic/MakeLocalFunctionStaticHelper.cs @@ -7,51 +7,50 @@ using System.Linq; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic +namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic; + +internal static class MakeLocalFunctionStaticHelper { - internal static class MakeLocalFunctionStaticHelper + public static bool IsStaticLocalFunctionSupported(LanguageVersion languageVersion) + => languageVersion >= LanguageVersion.CSharp8; + + private static bool TryGetDataFlowAnalysis(LocalFunctionStatementSyntax localFunction, SemanticModel semanticModel, [NotNullWhen(returnValue: true)] out DataFlowAnalysis? dataFlow) { - public static bool IsStaticLocalFunctionSupported(LanguageVersion languageVersion) - => languageVersion >= LanguageVersion.CSharp8; + dataFlow = semanticModel.AnalyzeDataFlow(localFunction); + return dataFlow is { Succeeded: true }; + } - private static bool TryGetDataFlowAnalysis(LocalFunctionStatementSyntax localFunction, SemanticModel semanticModel, [NotNullWhen(returnValue: true)] out DataFlowAnalysis? dataFlow) - { - dataFlow = semanticModel.AnalyzeDataFlow(localFunction); - return dataFlow is { Succeeded: true }; - } + private static bool CanBeCalledFromStaticContext(LocalFunctionStatementSyntax localFunction, DataFlowAnalysis dataFlow) + { + // If other local functions are called the it can't be made static unless the are static, or the local + // function is recursive, or its calling a child local function + return !dataFlow.UsedLocalFunctions.Any(static (usedLocalFunction, localFunctionStatement) => + !usedLocalFunction.IsStatic && !IsChildOrSelf(localFunctionStatement, usedLocalFunction), localFunction); - private static bool CanBeCalledFromStaticContext(LocalFunctionStatementSyntax localFunction, DataFlowAnalysis dataFlow) + static bool IsChildOrSelf(LocalFunctionStatementSyntax containingLocalFunction, ISymbol calledLocationFunction) { - // If other local functions are called the it can't be made static unless the are static, or the local - // function is recursive, or its calling a child local function - return !dataFlow.UsedLocalFunctions.Any(static (usedLocalFunction, localFunctionStatement) => - !usedLocalFunction.IsStatic && !IsChildOrSelf(localFunctionStatement, usedLocalFunction), localFunction); - - static bool IsChildOrSelf(LocalFunctionStatementSyntax containingLocalFunction, ISymbol calledLocationFunction) - { - var node = calledLocationFunction.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(); - // Contains also returns true if node is equal to the containing local function - return containingLocalFunction.Contains(node); - } + var node = calledLocationFunction.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(); + // Contains also returns true if node is equal to the containing local function + return containingLocalFunction.Contains(node); } + } - public static bool CanMakeLocalFunctionStaticBecauseNoCaptures(LocalFunctionStatementSyntax localFunction, SemanticModel semanticModel) - => TryGetDataFlowAnalysis(localFunction, semanticModel, out var dataFlow) - && CanBeCalledFromStaticContext(localFunction, dataFlow) - && dataFlow.CapturedInside.IsEmpty; + public static bool CanMakeLocalFunctionStaticBecauseNoCaptures(LocalFunctionStatementSyntax localFunction, SemanticModel semanticModel) + => TryGetDataFlowAnalysis(localFunction, semanticModel, out var dataFlow) + && CanBeCalledFromStaticContext(localFunction, dataFlow) + && dataFlow.CapturedInside.IsEmpty; - public static bool CanMakeLocalFunctionStaticByRefactoringCaptures(LocalFunctionStatementSyntax localFunction, SemanticModel semanticModel, out ImmutableArray captures) + public static bool CanMakeLocalFunctionStaticByRefactoringCaptures(LocalFunctionStatementSyntax localFunction, SemanticModel semanticModel, out ImmutableArray captures) + { + if (TryGetDataFlowAnalysis(localFunction, semanticModel, out var dataFlow) && + CanBeCalledFromStaticContext(localFunction, dataFlow) && + !dataFlow.CapturedInside.IsEmpty) { - if (TryGetDataFlowAnalysis(localFunction, semanticModel, out var dataFlow) && - CanBeCalledFromStaticContext(localFunction, dataFlow) && - !dataFlow.CapturedInside.IsEmpty) - { - captures = dataFlow.CapturedInside; - return true; - } - - captures = default; - return false; + captures = dataFlow.CapturedInside; + return true; } + + captures = default; + return false; } } diff --git a/src/Analyzers/CSharp/Analyzers/MakeStructFieldsWritable/CSharpMakeStructFieldsWritableDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/MakeStructFieldsWritable/CSharpMakeStructFieldsWritableDiagnosticAnalyzer.cs index 15fdcff9dd2bd..d32f40f33f558 100644 --- a/src/Analyzers/CSharp/Analyzers/MakeStructFieldsWritable/CSharpMakeStructFieldsWritableDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/MakeStructFieldsWritable/CSharpMakeStructFieldsWritableDiagnosticAnalyzer.cs @@ -9,112 +9,111 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.CSharp.MakeStructFieldsWritable +namespace Microsoft.CodeAnalysis.CSharp.MakeStructFieldsWritable; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpMakeStructFieldsWritableDiagnosticAnalyzer : AbstractCodeQualityDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpMakeStructFieldsWritableDiagnosticAnalyzer : AbstractCodeQualityDiagnosticAnalyzer + private static readonly DiagnosticDescriptor s_diagnosticDescriptor = CreateDescriptor( + IDEDiagnosticIds.MakeStructFieldsWritable, + EnforceOnBuildValues.MakeStructFieldsWritable, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Make_readonly_fields_writable), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Struct_contains_assignment_to_this_outside_of_constructor_Make_readonly_fields_writable), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + hasAnyCodeStyleOption: false, isUnnecessary: false); + + public CSharpMakeStructFieldsWritableDiagnosticAnalyzer() + : base([s_diagnosticDescriptor], GeneratedCodeAnalysisFlags.ReportDiagnostics) { - private static readonly DiagnosticDescriptor s_diagnosticDescriptor = CreateDescriptor( - IDEDiagnosticIds.MakeStructFieldsWritable, - EnforceOnBuildValues.MakeStructFieldsWritable, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Make_readonly_fields_writable), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Struct_contains_assignment_to_this_outside_of_constructor_Make_readonly_fields_writable), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - hasAnyCodeStyleOption: false, isUnnecessary: false); - - public CSharpMakeStructFieldsWritableDiagnosticAnalyzer() - : base([s_diagnosticDescriptor], GeneratedCodeAnalysisFlags.ReportDiagnostics) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; - protected override void InitializeWorker(AnalysisContext context) - { - context.RegisterCompilationStartAction(context - => SymbolAnalyzer.CreateAndRegisterActions(context)); - } + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context + => SymbolAnalyzer.CreateAndRegisterActions(context)); + } - private sealed class SymbolAnalyzer - { - private readonly INamedTypeSymbol _namedTypeSymbol; - private bool _hasTypeInstanceAssignment; + private sealed class SymbolAnalyzer + { + private readonly INamedTypeSymbol _namedTypeSymbol; + private bool _hasTypeInstanceAssignment; - private SymbolAnalyzer(INamedTypeSymbol namedTypeSymbol) - => _namedTypeSymbol = namedTypeSymbol; + private SymbolAnalyzer(INamedTypeSymbol namedTypeSymbol) + => _namedTypeSymbol = namedTypeSymbol; - public static void CreateAndRegisterActions(CompilationStartAnalysisContext context) + public static void CreateAndRegisterActions(CompilationStartAnalysisContext context) + { + context.RegisterSymbolStartAction(context => { - context.RegisterSymbolStartAction(context => - { - // We report diagnostic only if these requirements are met: - // 1. The type is struct - // 2. Struct contains at least one 'readonly' field - // 3. Struct contains assignment to 'this' outside the scope of constructor - var namedTypeSymbol = (INamedTypeSymbol)context.Symbol; - - // We are only interested in struct declarations - if (namedTypeSymbol.TypeKind != TypeKind.Struct) - return; - - // We check if struct contains any 'readonly' fields - if (!HasReadonlyField(namedTypeSymbol)) - return; - - // Check if diagnostic location is within the analysis span - if (!context.ShouldAnalyzeLocation(GetDiagnosticLocation(namedTypeSymbol))) - return; - - var symbolAnalyzer = new SymbolAnalyzer(namedTypeSymbol); - symbolAnalyzer.RegisterActions(context); - }, SymbolKind.NamedType); - } + // We report diagnostic only if these requirements are met: + // 1. The type is struct + // 2. Struct contains at least one 'readonly' field + // 3. Struct contains assignment to 'this' outside the scope of constructor + var namedTypeSymbol = (INamedTypeSymbol)context.Symbol; + + // We are only interested in struct declarations + if (namedTypeSymbol.TypeKind != TypeKind.Struct) + return; + + // We check if struct contains any 'readonly' fields + if (!HasReadonlyField(namedTypeSymbol)) + return; + + // Check if diagnostic location is within the analysis span + if (!context.ShouldAnalyzeLocation(GetDiagnosticLocation(namedTypeSymbol))) + return; + + var symbolAnalyzer = new SymbolAnalyzer(namedTypeSymbol); + symbolAnalyzer.RegisterActions(context); + }, SymbolKind.NamedType); + } - private static Location GetDiagnosticLocation(INamedTypeSymbol namedTypeSymbol) - => namedTypeSymbol.Locations[0]; + private static Location GetDiagnosticLocation(INamedTypeSymbol namedTypeSymbol) + => namedTypeSymbol.Locations[0]; - private static bool HasReadonlyField(INamedTypeSymbol namedTypeSymbol) - { - return namedTypeSymbol - .GetMembers() - .OfType() - .Any(field => field is { AssociatedSymbol: null, IsStatic: false, IsReadOnly: true }); - } + private static bool HasReadonlyField(INamedTypeSymbol namedTypeSymbol) + { + return namedTypeSymbol + .GetMembers() + .OfType() + .Any(field => field is { AssociatedSymbol: null, IsStatic: false, IsReadOnly: true }); + } - private void RegisterActions(SymbolStartAnalysisContext context) + private void RegisterActions(SymbolStartAnalysisContext context) + { + context.RegisterOperationBlockStartAction(context => { - context.RegisterOperationBlockStartAction(context => + if (context.OwningSymbol is IMethodSymbol { MethodKind: MethodKind.Constructor }) { - if (context.OwningSymbol is IMethodSymbol { MethodKind: MethodKind.Constructor }) - { - // We are looking for assignment to 'this' outside the constructor scope - return; - } + // We are looking for assignment to 'this' outside the constructor scope + return; + } - context.RegisterOperationAction(AnalyzeAssignment, OperationKind.SimpleAssignment); - }); + context.RegisterOperationAction(AnalyzeAssignment, OperationKind.SimpleAssignment); + }); - context.RegisterSymbolEndAction(SymbolEndAction); - } + context.RegisterSymbolEndAction(SymbolEndAction); + } - private void AnalyzeAssignment(OperationAnalysisContext context) + private void AnalyzeAssignment(OperationAnalysisContext context) + { + var operationAssigmnent = (IAssignmentOperation)context.Operation; + if (operationAssigmnent.Target is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance }) { - var operationAssigmnent = (IAssignmentOperation)context.Operation; - if (operationAssigmnent.Target is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance }) - { - Volatile.Write(ref _hasTypeInstanceAssignment, true); - } + Volatile.Write(ref _hasTypeInstanceAssignment, true); } + } - private void SymbolEndAction(SymbolAnalysisContext context) + private void SymbolEndAction(SymbolAnalysisContext context) + { + if (_hasTypeInstanceAssignment) { - if (_hasTypeInstanceAssignment) - { - var diagnostic = Diagnostic.Create( - s_diagnosticDescriptor, - GetDiagnosticLocation(_namedTypeSymbol)); - context.ReportDiagnostic(diagnostic); - } + var diagnostic = Diagnostic.Create( + s_diagnosticDescriptor, + GetDiagnosticLocation(_namedTypeSymbol)); + context.ReportDiagnostic(diagnostic); } } } diff --git a/src/Analyzers/CSharp/Analyzers/MakeStructMemberReadOnly/CSharpMakeStructMemberReadOnlyAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/MakeStructMemberReadOnly/CSharpMakeStructMemberReadOnlyAnalyzer.cs index 096bfae2e60d1..1340fef8dc43c 100644 --- a/src/Analyzers/CSharp/Analyzers/MakeStructMemberReadOnly/CSharpMakeStructMemberReadOnlyAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/MakeStructMemberReadOnly/CSharpMakeStructMemberReadOnlyAnalyzer.cs @@ -162,6 +162,7 @@ private void AnalyzeBlock( Descriptor, location, notificationOption, + context.Options, additionalLocations: ImmutableArray.Create(additionalLocation), properties: null); } diff --git a/src/Analyzers/CSharp/Analyzers/MakeStructReadOnly/CSharpMakeStructReadOnlyDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/MakeStructReadOnly/CSharpMakeStructReadOnlyDiagnosticAnalyzer.cs index dd771d772a0e9..323a32372339a 100644 --- a/src/Analyzers/CSharp/Analyzers/MakeStructReadOnly/CSharpMakeStructReadOnlyDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/MakeStructReadOnly/CSharpMakeStructReadOnlyDiagnosticAnalyzer.cs @@ -61,6 +61,7 @@ protected override void InitializeWorker(AnalysisContext context) Descriptor, location, option.Notification, + context.Options, additionalLocations: ImmutableArray.Create(additionalLocation), properties: null)); }); diff --git a/src/Analyzers/CSharp/Analyzers/MatchFolderAndNamespace/CSharpMatchFolderAndNamespaceDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/MatchFolderAndNamespace/CSharpMatchFolderAndNamespaceDiagnosticAnalyzer.cs index 9b0d82b0fca8d..7817fbb3d477c 100644 --- a/src/Analyzers/CSharp/Analyzers/MatchFolderAndNamespace/CSharpMatchFolderAndNamespaceDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/MatchFolderAndNamespace/CSharpMatchFolderAndNamespaceDiagnosticAnalyzer.cs @@ -14,26 +14,25 @@ using Microsoft.CodeAnalysis.Host.Mef; #endif -namespace Microsoft.CodeAnalysis.CSharp.Analyzers.MatchFolderAndNamespace -{ +namespace Microsoft.CodeAnalysis.CSharp.Analyzers.MatchFolderAndNamespace; + #if !CODE_STYLE - [Export(typeof(CSharpMatchFolderAndNamespaceDiagnosticAnalyzer)), Shared] +[Export(typeof(CSharpMatchFolderAndNamespaceDiagnosticAnalyzer)), Shared] #endif - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpMatchFolderAndNamespaceDiagnosticAnalyzer - : AbstractMatchFolderAndNamespaceDiagnosticAnalyzer - { +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpMatchFolderAndNamespaceDiagnosticAnalyzer + : AbstractMatchFolderAndNamespaceDiagnosticAnalyzer +{ #if !CODE_STYLE - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpMatchFolderAndNamespaceDiagnosticAnalyzer() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpMatchFolderAndNamespaceDiagnosticAnalyzer() + { + } #endif - protected override ISyntaxFacts GetSyntaxFacts() => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts GetSyntaxFacts() => CSharpSyntaxFacts.Instance; - protected override ImmutableArray GetSyntaxKindsToAnalyze() - => [SyntaxKind.NamespaceDeclaration, SyntaxKind.FileScopedNamespaceDeclaration]; - } + protected override ImmutableArray GetSyntaxKindsToAnalyze() + => [SyntaxKind.NamespaceDeclaration, SyntaxKind.FileScopedNamespaceDeclaration]; } diff --git a/src/Analyzers/CSharp/Analyzers/MisplacedUsingDirectives/MisplacedUsingDirectivesDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/MisplacedUsingDirectives/MisplacedUsingDirectivesDiagnosticAnalyzer.cs index 16f348afeaf75..0295d3a9908cc 100644 --- a/src/Analyzers/CSharp/Analyzers/MisplacedUsingDirectives/MisplacedUsingDirectivesDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/MisplacedUsingDirectives/MisplacedUsingDirectivesDiagnosticAnalyzer.cs @@ -13,102 +13,102 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.CSharp.MisplacedUsingDirectives +namespace Microsoft.CodeAnalysis.CSharp.MisplacedUsingDirectives; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class MisplacedUsingDirectivesDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class MisplacedUsingDirectivesDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + private static readonly LocalizableResourceString s_localizableTitle = new( + nameof(CSharpAnalyzersResources.Misplaced_using_directive), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); + + private static readonly LocalizableResourceString s_localizableOutsideMessage = new( + nameof(CSharpAnalyzersResources.Using_directives_must_be_placed_outside_of_a_namespace_declaration), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); + + private static readonly DiagnosticDescriptor s_outsideDiagnosticDescriptor = CreateDescriptorWithId( + IDEDiagnosticIds.MoveMisplacedUsingDirectivesDiagnosticId, + EnforceOnBuildValues.MoveMisplacedUsingDirectives, + hasAnyCodeStyleOption: true, + s_localizableTitle, s_localizableOutsideMessage); + + private static readonly LocalizableResourceString s_localizableInsideMessage = new( + nameof(CSharpAnalyzersResources.Using_directives_must_be_placed_inside_of_a_namespace_declaration), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); + + private static readonly DiagnosticDescriptor s_insideDiagnosticDescriptor = CreateDescriptorWithId( + IDEDiagnosticIds.MoveMisplacedUsingDirectivesDiagnosticId, + EnforceOnBuildValues.MoveMisplacedUsingDirectives, + hasAnyCodeStyleOption: true, + s_localizableTitle, s_localizableInsideMessage); + + public MisplacedUsingDirectivesDiagnosticAnalyzer() + : base(ImmutableDictionary.Empty + .Add(s_outsideDiagnosticDescriptor, CSharpCodeStyleOptions.PreferredUsingDirectivePlacement) + .Add(s_insideDiagnosticDescriptor, CSharpCodeStyleOptions.PreferredUsingDirectivePlacement)) { - private static readonly LocalizableResourceString s_localizableTitle = new( - nameof(CSharpAnalyzersResources.Misplaced_using_directive), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); - - private static readonly LocalizableResourceString s_localizableOutsideMessage = new( - nameof(CSharpAnalyzersResources.Using_directives_must_be_placed_outside_of_a_namespace_declaration), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); - - private static readonly DiagnosticDescriptor s_outsideDiagnosticDescriptor = CreateDescriptorWithId( - IDEDiagnosticIds.MoveMisplacedUsingDirectivesDiagnosticId, - EnforceOnBuildValues.MoveMisplacedUsingDirectives, - hasAnyCodeStyleOption: true, - s_localizableTitle, s_localizableOutsideMessage); - - private static readonly LocalizableResourceString s_localizableInsideMessage = new( - nameof(CSharpAnalyzersResources.Using_directives_must_be_placed_inside_of_a_namespace_declaration), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); - - private static readonly DiagnosticDescriptor s_insideDiagnosticDescriptor = CreateDescriptorWithId( - IDEDiagnosticIds.MoveMisplacedUsingDirectivesDiagnosticId, - EnforceOnBuildValues.MoveMisplacedUsingDirectives, - hasAnyCodeStyleOption: true, - s_localizableTitle, s_localizableInsideMessage); - - public MisplacedUsingDirectivesDiagnosticAnalyzer() - : base(ImmutableDictionary.Empty - .Add(s_outsideDiagnosticDescriptor, CSharpCodeStyleOptions.PreferredUsingDirectivePlacement) - .Add(s_insideDiagnosticDescriptor, CSharpCodeStyleOptions.PreferredUsingDirectivePlacement)) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; - protected override void InitializeWorker(AnalysisContext context) - { - context.RegisterSyntaxNodeAction(AnalyzeNamespaceNode, SyntaxKind.NamespaceDeclaration, SyntaxKind.FileScopedNamespaceDeclaration); - context.RegisterSyntaxNodeAction(AnalyzeCompilationUnitNode, SyntaxKind.CompilationUnit); - } + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeNamespaceNode, SyntaxKind.NamespaceDeclaration, SyntaxKind.FileScopedNamespaceDeclaration); + context.RegisterSyntaxNodeAction(AnalyzeCompilationUnitNode, SyntaxKind.CompilationUnit); + } - private void AnalyzeNamespaceNode(SyntaxNodeAnalysisContext context) + private void AnalyzeNamespaceNode(SyntaxNodeAnalysisContext context) + { + var option = context.GetCSharpAnalyzerOptions().UsingDirectivePlacement; + if (option.Value != AddImportPlacement.OutsideNamespace + || ShouldSkipAnalysis(context, option.Notification)) { - var option = context.GetCSharpAnalyzerOptions().UsingDirectivePlacement; - if (option.Value != AddImportPlacement.OutsideNamespace - || ShouldSkipAnalysis(context, option.Notification)) - { - return; - } - - var namespaceDeclaration = (BaseNamespaceDeclarationSyntax)context.Node; - ReportDiagnostics(context, s_outsideDiagnosticDescriptor, namespaceDeclaration.Usings, option); + return; } - private void AnalyzeCompilationUnitNode(SyntaxNodeAnalysisContext context) - { - var option = context.GetCSharpAnalyzerOptions().UsingDirectivePlacement; - var compilationUnit = (CompilationUnitSyntax)context.Node; - - if (option.Value != AddImportPlacement.InsideNamespace - || ShouldSkipAnalysis(context, option.Notification) - || ShouldSuppressDiagnostic(compilationUnit)) - { - return; - } - - // Only report for non-global usings. Global usings must stay at the compilation unit level. - var nonGlobalUsings = compilationUnit.Usings.Where(u => u.GlobalKeyword == default); - - // Note: We will report diagnostics when a code file contains multiple namespaces even though we will not - // offer a code fix in these cases. - ReportDiagnostics(context, s_insideDiagnosticDescriptor, nonGlobalUsings, option); - } + var namespaceDeclaration = (BaseNamespaceDeclarationSyntax)context.Node; + ReportDiagnostics(context, s_outsideDiagnosticDescriptor, namespaceDeclaration.Usings, option); + } + + private void AnalyzeCompilationUnitNode(SyntaxNodeAnalysisContext context) + { + var option = context.GetCSharpAnalyzerOptions().UsingDirectivePlacement; + var compilationUnit = (CompilationUnitSyntax)context.Node; - private static bool ShouldSuppressDiagnostic(CompilationUnitSyntax compilationUnit) + if (option.Value != AddImportPlacement.InsideNamespace + || ShouldSkipAnalysis(context, option.Notification) + || ShouldSuppressDiagnostic(compilationUnit)) { - // Suppress if there are nodes other than usings and namespaces in the - // compilation unit (including ExternAlias). - return compilationUnit.ChildNodes().Any( - t => t.Kind() is not (SyntaxKind.UsingDirective or SyntaxKind.NamespaceDeclaration or SyntaxKind.FileScopedNamespaceDeclaration)); + return; } - private static void ReportDiagnostics( - SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, - IEnumerable usingDirectives, CodeStyleOption2 option) + // Only report for non-global usings. Global usings must stay at the compilation unit level. + var nonGlobalUsings = compilationUnit.Usings.Where(u => u.GlobalKeyword == default); + + // Note: We will report diagnostics when a code file contains multiple namespaces even though we will not + // offer a code fix in these cases. + ReportDiagnostics(context, s_insideDiagnosticDescriptor, nonGlobalUsings, option); + } + + private static bool ShouldSuppressDiagnostic(CompilationUnitSyntax compilationUnit) + { + // Suppress if there are nodes other than usings and namespaces in the + // compilation unit (including ExternAlias). + return compilationUnit.ChildNodes().Any( + t => t.Kind() is not (SyntaxKind.UsingDirective or SyntaxKind.NamespaceDeclaration or SyntaxKind.FileScopedNamespaceDeclaration)); + } + + private static void ReportDiagnostics( + SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, + IEnumerable usingDirectives, CodeStyleOption2 option) + { + foreach (var usingDirective in usingDirectives) { - foreach (var usingDirective in usingDirectives) - { - context.ReportDiagnostic(DiagnosticHelper.Create( - descriptor, - usingDirective.GetLocation(), - option.Notification, - additionalLocations: null, - properties: null)); - } + context.ReportDiagnostic(DiagnosticHelper.Create( + descriptor, + usingDirective.GetLocation(), + option.Notification, + context.Options, + additionalLocations: null, + properties: null)); } } } diff --git a/src/Analyzers/CSharp/Analyzers/NamingStyle/CSharpNamingStyleDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/NamingStyle/CSharpNamingStyleDiagnosticAnalyzer.cs index 15fbc04c053be..40049a2c657ea 100644 --- a/src/Analyzers/CSharp/Analyzers/NamingStyle/CSharpNamingStyleDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/NamingStyle/CSharpNamingStyleDiagnosticAnalyzer.cs @@ -9,54 +9,53 @@ using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.NamingStyles -{ - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpNamingStyleDiagnosticAnalyzer : NamingStyleDiagnosticAnalyzerBase - { - protected override ImmutableArray SupportedSyntaxKinds { get; } = - [ - SyntaxKind.VariableDeclarator, - SyntaxKind.ForEachStatement, - SyntaxKind.CatchDeclaration, - SyntaxKind.SingleVariableDesignation, - SyntaxKind.LocalFunctionStatement, - SyntaxKind.Parameter, - SyntaxKind.TypeParameter, - ]; +namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.NamingStyles; - protected override bool ShouldIgnore(ISymbol symbol) - { - if (symbol.IsKind(SymbolKind.Parameter) - && symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is ParameterSyntax - { - Parent: ParameterListSyntax - { - Parent: RecordDeclarationSyntax - } - }) - { - // Parameters of positional record declarations should be ignored because they also - // considered properties, and that naming style makes more sense - return true; - } +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpNamingStyleDiagnosticAnalyzer : NamingStyleDiagnosticAnalyzerBase +{ + protected override ImmutableArray SupportedSyntaxKinds { get; } = + [ + SyntaxKind.VariableDeclarator, + SyntaxKind.ForEachStatement, + SyntaxKind.CatchDeclaration, + SyntaxKind.SingleVariableDesignation, + SyntaxKind.LocalFunctionStatement, + SyntaxKind.Parameter, + SyntaxKind.TypeParameter, + ]; - if (!symbol.CanBeReferencedByName) + protected override bool ShouldIgnore(ISymbol symbol) + { + if (symbol.IsKind(SymbolKind.Parameter) + && symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is ParameterSyntax { - // Explicit interface implementation falls into here, as they don't own their names - // Two symbols are involved here, and symbol.ExplicitInterfaceImplementations only applies for one - return true; - } + Parent: ParameterListSyntax + { + Parent: RecordDeclarationSyntax + } + }) + { + // Parameters of positional record declarations should be ignored because they also + // considered properties, and that naming style makes more sense + return true; + } - if (symbol.IsExtern) - { - // Extern symbols are mainly P/Invoke and runtime invoke, probably requiring their name - // to match external definition exactly. - // Simply ignoring them. - return true; - } + if (!symbol.CanBeReferencedByName) + { + // Explicit interface implementation falls into here, as they don't own their names + // Two symbols are involved here, and symbol.ExplicitInterfaceImplementations only applies for one + return true; + } - return false; + if (symbol.IsExtern) + { + // Extern symbols are mainly P/Invoke and runtime invoke, probably requiring their name + // to match external definition exactly. + // Simply ignoring them. + return true; } + + return false; } } diff --git a/src/Analyzers/CSharp/Analyzers/NewLines/ArrowExpressionClausePlacement/ArrowExpressionClausePlacementDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/NewLines/ArrowExpressionClausePlacement/ArrowExpressionClausePlacementDiagnosticAnalyzer.cs index 50633537154ad..8972833f5689d 100644 --- a/src/Analyzers/CSharp/Analyzers/NewLines/ArrowExpressionClausePlacement/ArrowExpressionClausePlacementDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/NewLines/ArrowExpressionClausePlacement/ArrowExpressionClausePlacementDiagnosticAnalyzer.cs @@ -10,105 +10,105 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.NewLines.ArrowExpressionClausePlacement +namespace Microsoft.CodeAnalysis.CSharp.NewLines.ArrowExpressionClausePlacement; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class ArrowExpressionClausePlacementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class ArrowExpressionClausePlacementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public ArrowExpressionClausePlacementDiagnosticAnalyzer() + : base(IDEDiagnosticIds.ArrowExpressionClausePlacementDiagnosticId, + EnforceOnBuildValues.ArrowExpressionClausePlacement, + CSharpCodeStyleOptions.AllowBlankLineAfterTokenInArrowExpressionClause, + new LocalizableResourceString( + nameof(CSharpAnalyzersResources.Blank_line_not_allowed_after_arrow_expression_clause_token), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public ArrowExpressionClausePlacementDiagnosticAnalyzer() - : base(IDEDiagnosticIds.ArrowExpressionClausePlacementDiagnosticId, - EnforceOnBuildValues.ArrowExpressionClausePlacement, - CSharpCodeStyleOptions.AllowBlankLineAfterTokenInArrowExpressionClause, - new LocalizableResourceString( - nameof(CSharpAnalyzersResources.Blank_line_not_allowed_after_arrow_expression_clause_token), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); - private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) - { - var option = context.GetCSharpAnalyzerOptions().AllowBlankLineAfterTokenInArrowExpressionClause; - if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) - return; + private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + { + var option = context.GetCSharpAnalyzerOptions().AllowBlankLineAfterTokenInArrowExpressionClause; + if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) + return; - Recurse(context, option.Notification, context.GetAnalysisRoot(findInTrivia: false)); - } + Recurse(context, option.Notification, context.GetAnalysisRoot(findInTrivia: false)); + } - private void Recurse(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxNode node) - { - context.CancellationToken.ThrowIfCancellationRequested(); + private void Recurse(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxNode node) + { + context.CancellationToken.ThrowIfCancellationRequested(); - if (node is ArrowExpressionClauseSyntax arrowExpressionClause) - ProcessArrowExpressionClause(context, notificationOption, arrowExpressionClause); + if (node is ArrowExpressionClauseSyntax arrowExpressionClause) + ProcessArrowExpressionClause(context, notificationOption, arrowExpressionClause); - foreach (var child in node.ChildNodesAndTokens()) - { - if (!context.ShouldAnalyzeSpan(child.Span)) - continue; + foreach (var child in node.ChildNodesAndTokens()) + { + if (!context.ShouldAnalyzeSpan(child.Span)) + continue; - if (child.IsNode) - Recurse(context, notificationOption, child.AsNode()!); - } + if (child.IsNode) + Recurse(context, notificationOption, child.AsNode()!); } + } - private void ProcessArrowExpressionClause( - SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, ArrowExpressionClauseSyntax arrowExpressionClause) - { - // get - // => 1 + 2; - // - // Never looks good. So we don't process in that case. - if (arrowExpressionClause.Parent is AccessorDeclarationSyntax) - return; - - // Don't bother analyzing nodes that have syntax errors in them. - if (arrowExpressionClause.GetDiagnostics().Any(static d => d.Severity == DiagnosticSeverity.Error)) - return; - - if (IsOk(arrowExpressionClause.ArrowToken)) - return; - - context.ReportDiagnostic(DiagnosticHelper.Create( - this.Descriptor, - arrowExpressionClause.ArrowToken.GetLocation(), - notificationOption, - additionalLocations: null, - properties: null)); + private void ProcessArrowExpressionClause( + SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, ArrowExpressionClauseSyntax arrowExpressionClause) + { + // get + // => 1 + 2; + // + // Never looks good. So we don't process in that case. + if (arrowExpressionClause.Parent is AccessorDeclarationSyntax) + return; + // Don't bother analyzing nodes that have syntax errors in them. + if (arrowExpressionClause.GetDiagnostics().Any(static d => d.Severity == DiagnosticSeverity.Error)) return; - static bool IsOk(SyntaxToken token) + if (IsOk(arrowExpressionClause.ArrowToken)) + return; + + context.ReportDiagnostic(DiagnosticHelper.Create( + this.Descriptor, + arrowExpressionClause.ArrowToken.GetLocation(), + notificationOption, + context.Options, + additionalLocations: null, + properties: null)); + + return; + + static bool IsOk(SyntaxToken token) + { + // Only care about tokens that are actually present. Missing ones mean the code is incomplete and we + // don't want to complain about those. + if (token.IsMissing) + return true; + + // Arrow has to be at the end of the line for us to actually care. + if (token.TrailingTrivia is not [.., SyntaxTrivia(SyntaxKind.EndOfLineTrivia)]) + return true; + + // if the next token has pp-directives on it, we don't want to move the token around as we may screw + // things up in different pp-contexts. + var nextToken = token.GetNextToken(); + if (nextToken == default) + return true; + + if (nextToken.LeadingTrivia.Any(static t => t.Kind() is + SyntaxKind.IfDirectiveTrivia or SyntaxKind.ElseDirectiveTrivia or SyntaxKind.ElifDirectiveTrivia or SyntaxKind.EndIfDirectiveTrivia)) { - // Only care about tokens that are actually present. Missing ones mean the code is incomplete and we - // don't want to complain about those. - if (token.IsMissing) - return true; - - // Arrow has to be at the end of the line for us to actually care. - if (token.TrailingTrivia is not [.., SyntaxTrivia(SyntaxKind.EndOfLineTrivia)]) - return true; - - // if the next token has pp-directives on it, we don't want to move the token around as we may screw - // things up in different pp-contexts. - var nextToken = token.GetNextToken(); - if (nextToken == default) - return true; - - if (nextToken.LeadingTrivia.Any(static t => t.Kind() is - SyntaxKind.IfDirectiveTrivia or SyntaxKind.ElseDirectiveTrivia or SyntaxKind.ElifDirectiveTrivia or SyntaxKind.EndIfDirectiveTrivia)) - { - return true; - } - - // Not ok. - return false; + return true; } + + // Not ok. + return false; } } } diff --git a/src/Analyzers/CSharp/Analyzers/NewLines/ConditionalExpressionPlacement/ConditionalExpressionPlacementDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/NewLines/ConditionalExpressionPlacement/ConditionalExpressionPlacementDiagnosticAnalyzer.cs index 328432293ba13..35e99380792a9 100644 --- a/src/Analyzers/CSharp/Analyzers/NewLines/ConditionalExpressionPlacement/ConditionalExpressionPlacementDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/NewLines/ConditionalExpressionPlacement/ConditionalExpressionPlacementDiagnosticAnalyzer.cs @@ -60,6 +60,7 @@ private void ProcessConditionalExpression(SyntaxNodeAnalysisContext context) this.Descriptor, conditionalExpression.QuestionToken.GetLocation(), option.Notification, + context.Options, additionalLocations: null, properties: null)); diff --git a/src/Analyzers/CSharp/Analyzers/NewLines/ConsecutiveBracePlacement/ConsecutiveBracePlacementDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/NewLines/ConsecutiveBracePlacement/ConsecutiveBracePlacementDiagnosticAnalyzer.cs index 77c02af2d4a1d..82381f588feb8 100644 --- a/src/Analyzers/CSharp/Analyzers/NewLines/ConsecutiveBracePlacement/ConsecutiveBracePlacementDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/NewLines/ConsecutiveBracePlacement/ConsecutiveBracePlacementDiagnosticAnalyzer.cs @@ -11,133 +11,133 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConsecutiveBracePlacement +namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConsecutiveBracePlacement; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class ConsecutiveBracePlacementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class ConsecutiveBracePlacementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public ConsecutiveBracePlacementDiagnosticAnalyzer() + : base(IDEDiagnosticIds.ConsecutiveBracePlacementDiagnosticId, + EnforceOnBuildValues.ConsecutiveBracePlacement, + CSharpCodeStyleOptions.AllowBlankLinesBetweenConsecutiveBraces, + new LocalizableResourceString( + nameof(CSharpAnalyzersResources.Consecutive_braces_must_not_have_a_blank_between_them), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public ConsecutiveBracePlacementDiagnosticAnalyzer() - : base(IDEDiagnosticIds.ConsecutiveBracePlacementDiagnosticId, - EnforceOnBuildValues.ConsecutiveBracePlacement, - CSharpCodeStyleOptions.AllowBlankLinesBetweenConsecutiveBraces, - new LocalizableResourceString( - nameof(CSharpAnalyzersResources.Consecutive_braces_must_not_have_a_blank_between_them), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); - private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) - { - var option = context.GetCSharpAnalyzerOptions().AllowBlankLinesBetweenConsecutiveBraces; - if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) - return; + private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + { + var option = context.GetCSharpAnalyzerOptions().AllowBlankLinesBetweenConsecutiveBraces; + if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) + return; - using var _ = ArrayBuilder.GetInstance(out var stack); - Recurse(context, option.Notification, stack); - } + using var _ = ArrayBuilder.GetInstance(out var stack); + Recurse(context, option.Notification, stack); + } - private void Recurse(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, ArrayBuilder stack) - { - var tree = context.Tree; - var cancellationToken = context.CancellationToken; + private void Recurse(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, ArrayBuilder stack) + { + var tree = context.Tree; + var cancellationToken = context.CancellationToken; - var root = context.GetAnalysisRoot(findInTrivia: false); - var text = tree.GetText(cancellationToken); + var root = context.GetAnalysisRoot(findInTrivia: false); + var text = tree.GetText(cancellationToken); - stack.Add(root); - while (stack.Count > 0) - { - cancellationToken.ThrowIfCancellationRequested(); + stack.Add(root); + while (stack.Count > 0) + { + cancellationToken.ThrowIfCancellationRequested(); - var current = stack.Last(); - stack.RemoveLast(); + var current = stack.Last(); + stack.RemoveLast(); - // Don't bother analyzing nodes that have syntax errors in them. - if (current.ContainsDiagnostics && current.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) - continue; + // Don't bother analyzing nodes that have syntax errors in them. + if (current.ContainsDiagnostics && current.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + continue; - foreach (var child in current.ChildNodesAndTokens()) - { - if (!context.ShouldAnalyzeSpan(child.FullSpan)) - continue; + foreach (var child in current.ChildNodesAndTokens()) + { + if (!context.ShouldAnalyzeSpan(child.FullSpan)) + continue; - if (child.IsNode) - stack.Add(child.AsNode()!); - else if (child.IsToken) - ProcessToken(context, notificationOption, text, child.AsToken()); - } + if (child.IsNode) + stack.Add(child.AsNode()!); + else if (child.IsToken) + ProcessToken(context, notificationOption, text, child.AsToken()); } } + } - private void ProcessToken(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SourceText text, SyntaxToken token) - { - if (!HasExcessBlankLinesAfter(text, token, out var secondBrace, out _)) - return; - - context.ReportDiagnostic(DiagnosticHelper.Create( - this.Descriptor, - secondBrace.GetLocation(), - notificationOption, - additionalLocations: null, - properties: null)); - } - - public static bool HasExcessBlankLinesAfter( - SourceText text, SyntaxToken token, - out SyntaxToken secondBrace, - out SyntaxTrivia endOfLineTrivia) - { - secondBrace = default; - endOfLineTrivia = default; - if (!token.IsKind(SyntaxKind.CloseBraceToken)) - return false; + private void ProcessToken(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SourceText text, SyntaxToken token) + { + if (!HasExcessBlankLinesAfter(text, token, out var secondBrace, out _)) + return; + + context.ReportDiagnostic(DiagnosticHelper.Create( + this.Descriptor, + secondBrace.GetLocation(), + notificationOption, + context.Options, + additionalLocations: null, + properties: null)); + } - var nextToken = token.GetNextToken(); - if (!nextToken.IsKind(SyntaxKind.CloseBraceToken)) - return false; + public static bool HasExcessBlankLinesAfter( + SourceText text, SyntaxToken token, + out SyntaxToken secondBrace, + out SyntaxTrivia endOfLineTrivia) + { + secondBrace = default; + endOfLineTrivia = default; + if (!token.IsKind(SyntaxKind.CloseBraceToken)) + return false; - var firstBrace = token; - secondBrace = nextToken; + var nextToken = token.GetNextToken(); + if (!nextToken.IsKind(SyntaxKind.CloseBraceToken)) + return false; - // two } tokens. They need to be on the same line, or if they are not on subsequent lines, then there needs - // to be more than whitespace between them. - var lines = text.Lines; - var firstBraceLine = lines.GetLineFromPosition(firstBrace.SpanStart).LineNumber; - var secondBraceLine = lines.GetLineFromPosition(secondBrace.SpanStart).LineNumber; + var firstBrace = token; + secondBrace = nextToken; - var lineCount = secondBraceLine - firstBraceLine; + // two } tokens. They need to be on the same line, or if they are not on subsequent lines, then there needs + // to be more than whitespace between them. + var lines = text.Lines; + var firstBraceLine = lines.GetLineFromPosition(firstBrace.SpanStart).LineNumber; + var secondBraceLine = lines.GetLineFromPosition(secondBrace.SpanStart).LineNumber; - // if they're both on the same line, or one line apart, then there's no problem. - if (lineCount <= 1) - return false; + var lineCount = secondBraceLine - firstBraceLine; - // they're multiple lines apart. This i not ok if those lines are all whitespace. - for (var currentLine = firstBraceLine + 1; currentLine < secondBraceLine; currentLine++) - { - if (!IsAllWhitespace(lines[currentLine])) - return false; - } + // if they're both on the same line, or one line apart, then there's no problem. + if (lineCount <= 1) + return false; - endOfLineTrivia = secondBrace.LeadingTrivia.Last(t => t.IsKind(SyntaxKind.EndOfLineTrivia)); - return endOfLineTrivia != default; + // they're multiple lines apart. This i not ok if those lines are all whitespace. + for (var currentLine = firstBraceLine + 1; currentLine < secondBraceLine; currentLine++) + { + if (!IsAllWhitespace(lines[currentLine])) + return false; } - private static bool IsAllWhitespace(TextLine textLine) - { - var text = textLine.Text!; - for (var i = textLine.Start; i < textLine.End; i++) - { - if (!SyntaxFacts.IsWhitespace(text[i])) - return false; - } + endOfLineTrivia = secondBrace.LeadingTrivia.Last(t => t.IsKind(SyntaxKind.EndOfLineTrivia)); + return endOfLineTrivia != default; + } - return true; + private static bool IsAllWhitespace(TextLine textLine) + { + var text = textLine.Text!; + for (var i = textLine.Start; i < textLine.End; i++) + { + if (!SyntaxFacts.IsWhitespace(text[i])) + return false; } + + return true; } } diff --git a/src/Analyzers/CSharp/Analyzers/NewLines/ConsecutiveStatementPlacement/CSharpConsecutiveStatementPlacementDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/NewLines/ConsecutiveStatementPlacement/CSharpConsecutiveStatementPlacementDiagnosticAnalyzer.cs index 8a94cb595ed38..6ba10c19bf9d0 100644 --- a/src/Analyzers/CSharp/Analyzers/NewLines/ConsecutiveStatementPlacement/CSharpConsecutiveStatementPlacementDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/NewLines/ConsecutiveStatementPlacement/CSharpConsecutiveStatementPlacementDiagnosticAnalyzer.cs @@ -7,40 +7,39 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.NewLines.ConsecutiveStatementPlacement; -namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConsecutiveStatementPlacement +namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConsecutiveStatementPlacement; + +/// +/// Analyzer that finds code of the form: +/// +/// if (cond) +/// { +/// } +/// NextStatement(); +/// +/// +/// And requires it to be of the form: +/// +/// if (cond) +/// { +/// } +/// +/// NextStatement(); +/// +/// +/// Specifically, all blocks followed by another statement must have a blank line between them. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpConsecutiveStatementPlacementDiagnosticAnalyzer : AbstractConsecutiveStatementPlacementDiagnosticAnalyzer { - /// - /// Analyzer that finds code of the form: - /// - /// if (cond) - /// { - /// } - /// NextStatement(); - /// - /// - /// And requires it to be of the form: - /// - /// if (cond) - /// { - /// } - /// - /// NextStatement(); - /// - /// - /// Specifically, all blocks followed by another statement must have a blank line between them. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpConsecutiveStatementPlacementDiagnosticAnalyzer : AbstractConsecutiveStatementPlacementDiagnosticAnalyzer + public CSharpConsecutiveStatementPlacementDiagnosticAnalyzer() + : base(CSharpSyntaxFacts.Instance) { - public CSharpConsecutiveStatementPlacementDiagnosticAnalyzer() - : base(CSharpSyntaxFacts.Instance) - { - } + } - protected override bool IsBlockLikeStatement(SyntaxNode node) - => node is BlockSyntax or SwitchStatementSyntax; + protected override bool IsBlockLikeStatement(SyntaxNode node) + => node is BlockSyntax or SwitchStatementSyntax; - protected override Location GetDiagnosticLocation(SyntaxNode block) - => block.GetLastToken().GetLocation(); - } + protected override Location GetDiagnosticLocation(SyntaxNode block) + => block.GetLastToken().GetLocation(); } diff --git a/src/Analyzers/CSharp/Analyzers/NewLines/ConstructorInitializerPlacement/ConstructorInitializerPlacementDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/NewLines/ConstructorInitializerPlacement/ConstructorInitializerPlacementDiagnosticAnalyzer.cs index adeb292d3a366..d43ac7418c66e 100644 --- a/src/Analyzers/CSharp/Analyzers/NewLines/ConstructorInitializerPlacement/ConstructorInitializerPlacementDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/NewLines/ConstructorInitializerPlacement/ConstructorInitializerPlacementDiagnosticAnalyzer.cs @@ -12,88 +12,88 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConstructorInitializerPlacement +namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConstructorInitializerPlacement; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class ConstructorInitializerPlacementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class ConstructorInitializerPlacementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public ConstructorInitializerPlacementDiagnosticAnalyzer() + : base(IDEDiagnosticIds.ConstructorInitializerPlacementDiagnosticId, + EnforceOnBuildValues.ConstructorInitializerPlacement, + CSharpCodeStyleOptions.AllowBlankLineAfterColonInConstructorInitializer, + new LocalizableResourceString( + nameof(CSharpAnalyzersResources.Blank_line_not_allowed_after_constructor_initializer_colon), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public ConstructorInitializerPlacementDiagnosticAnalyzer() - : base(IDEDiagnosticIds.ConstructorInitializerPlacementDiagnosticId, - EnforceOnBuildValues.ConstructorInitializerPlacement, - CSharpCodeStyleOptions.AllowBlankLineAfterColonInConstructorInitializer, - new LocalizableResourceString( - nameof(CSharpAnalyzersResources.Blank_line_not_allowed_after_constructor_initializer_colon), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); - private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) - { - var option = context.GetCSharpAnalyzerOptions().AllowBlankLineAfterColonInConstructorInitializer; - if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) - return; + private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + { + var option = context.GetCSharpAnalyzerOptions().AllowBlankLineAfterColonInConstructorInitializer; + if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) + return; - Recurse(context, option.Notification, context.GetAnalysisRoot(findInTrivia: false)); - } + Recurse(context, option.Notification, context.GetAnalysisRoot(findInTrivia: false)); + } - private void Recurse(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxNode node) - { - context.CancellationToken.ThrowIfCancellationRequested(); + private void Recurse(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxNode node) + { + context.CancellationToken.ThrowIfCancellationRequested(); - // Don't bother analyzing nodes that have syntax errors in them. - if (node.ContainsDiagnostics) - return; + // Don't bother analyzing nodes that have syntax errors in them. + if (node.ContainsDiagnostics) + return; - if (node is ConstructorInitializerSyntax initializer) - ProcessConstructorInitializer(context, notificationOption, initializer); + if (node is ConstructorInitializerSyntax initializer) + ProcessConstructorInitializer(context, notificationOption, initializer); - foreach (var child in node.ChildNodesAndTokens()) - { - if (!context.ShouldAnalyzeSpan(child.Span)) - continue; + foreach (var child in node.ChildNodesAndTokens()) + { + if (!context.ShouldAnalyzeSpan(child.Span)) + continue; - if (child.IsNode) - Recurse(context, notificationOption, child.AsNode()!); - } + if (child.IsNode) + Recurse(context, notificationOption, child.AsNode()!); } + } - private void ProcessConstructorInitializer( - SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, ConstructorInitializerSyntax initializer) - { - var sourceText = context.Tree.GetText(context.CancellationToken); + private void ProcessConstructorInitializer( + SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, ConstructorInitializerSyntax initializer) + { + var sourceText = context.Tree.GetText(context.CancellationToken); - var colonToken = initializer.ColonToken; - var thisOrBaseKeyword = initializer.ThisOrBaseKeyword; + var colonToken = initializer.ColonToken; + var thisOrBaseKeyword = initializer.ThisOrBaseKeyword; - var colonLine = sourceText.Lines.GetLineFromPosition(colonToken.SpanStart); - var thisBaseLine = sourceText.Lines.GetLineFromPosition(thisOrBaseKeyword.SpanStart); - if (colonLine == thisBaseLine) - return; + var colonLine = sourceText.Lines.GetLineFromPosition(colonToken.SpanStart); + var thisBaseLine = sourceText.Lines.GetLineFromPosition(thisOrBaseKeyword.SpanStart); + if (colonLine == thisBaseLine) + return; - if (colonToken.TrailingTrivia.Count == 0) - return; + if (colonToken.TrailingTrivia.Count == 0) + return; - if (colonToken.TrailingTrivia.Last().Kind() != SyntaxKind.EndOfLineTrivia) - return; + if (colonToken.TrailingTrivia.Last().Kind() != SyntaxKind.EndOfLineTrivia) + return; - if (colonToken.TrailingTrivia.Any(t => !t.IsWhitespaceOrEndOfLine())) - return; + if (colonToken.TrailingTrivia.Any(t => !t.IsWhitespaceOrEndOfLine())) + return; - if (thisOrBaseKeyword.LeadingTrivia.Any(t => !t.IsWhitespaceOrEndOfLine() && !t.IsSingleOrMultiLineComment())) - return; + if (thisOrBaseKeyword.LeadingTrivia.Any(t => !t.IsWhitespaceOrEndOfLine() && !t.IsSingleOrMultiLineComment())) + return; - context.ReportDiagnostic(DiagnosticHelper.Create( - this.Descriptor, - colonToken.GetLocation(), - notificationOption, - additionalLocations: ImmutableArray.Create(initializer.GetLocation()), - properties: null)); - } + context.ReportDiagnostic(DiagnosticHelper.Create( + this.Descriptor, + colonToken.GetLocation(), + notificationOption, + context.Options, + additionalLocations: ImmutableArray.Create(initializer.GetLocation()), + properties: null)); } } diff --git a/src/Analyzers/CSharp/Analyzers/NewLines/EmbeddedStatementPlacement/EmbeddedStatementPlacementDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/NewLines/EmbeddedStatementPlacement/EmbeddedStatementPlacementDiagnosticAnalyzer.cs index b66a06589aa60..8aa7f141c6568 100644 --- a/src/Analyzers/CSharp/Analyzers/NewLines/EmbeddedStatementPlacement/EmbeddedStatementPlacementDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/NewLines/EmbeddedStatementPlacement/EmbeddedStatementPlacementDiagnosticAnalyzer.cs @@ -11,129 +11,129 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.NewLines.EmbeddedStatementPlacement +namespace Microsoft.CodeAnalysis.CSharp.NewLines.EmbeddedStatementPlacement; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class EmbeddedStatementPlacementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class EmbeddedStatementPlacementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public EmbeddedStatementPlacementDiagnosticAnalyzer() + : base(IDEDiagnosticIds.EmbeddedStatementPlacementDiagnosticId, + EnforceOnBuildValues.EmbeddedStatementPlacement, + CSharpCodeStyleOptions.AllowEmbeddedStatementsOnSameLine, + new LocalizableResourceString( + nameof(CSharpAnalyzersResources.Embedded_statements_must_be_on_their_own_line), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public EmbeddedStatementPlacementDiagnosticAnalyzer() - : base(IDEDiagnosticIds.EmbeddedStatementPlacementDiagnosticId, - EnforceOnBuildValues.EmbeddedStatementPlacement, - CSharpCodeStyleOptions.AllowEmbeddedStatementsOnSameLine, - new LocalizableResourceString( - nameof(CSharpAnalyzersResources.Embedded_statements_must_be_on_their_own_line), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } - - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; - - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); + } - private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) - { - var option = context.GetCSharpAnalyzerOptions().AllowEmbeddedStatementsOnSameLine; - if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) - return; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; - Recurse(context, option.Notification, context.GetAnalysisRoot(findInTrivia: false)); - } - - private void Recurse(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxNode node) - { - context.CancellationToken.ThrowIfCancellationRequested(); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); - // Don't bother analyzing nodes that have syntax errors in them. - if (node.ContainsDiagnostics) - return; + private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + { + var option = context.GetCSharpAnalyzerOptions().AllowEmbeddedStatementsOnSameLine; + if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) + return; - // Report on the topmost statement that has an issue. No need to recurse further at that point. Note: the - // fixer will fix up all statements, but we don't want to clutter things with lots of diagnostics on the - // same line. - if (node is StatementSyntax statement && - CheckStatementSyntax(context, notificationOption, statement)) - { - return; - } + Recurse(context, option.Notification, context.GetAnalysisRoot(findInTrivia: false)); + } - foreach (var child in node.ChildNodesAndTokens()) - { - if (!context.ShouldAnalyzeSpan(child.Span)) - continue; + private void Recurse(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxNode node) + { + context.CancellationToken.ThrowIfCancellationRequested(); - if (child.IsNode) - Recurse(context, notificationOption, child.AsNode()!); - } - } + // Don't bother analyzing nodes that have syntax errors in them. + if (node.ContainsDiagnostics) + return; - private bool CheckStatementSyntax(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, StatementSyntax statement) + // Report on the topmost statement that has an issue. No need to recurse further at that point. Note: the + // fixer will fix up all statements, but we don't want to clutter things with lots of diagnostics on the + // same line. + if (node is StatementSyntax statement && + CheckStatementSyntax(context, notificationOption, statement)) { - if (!StatementNeedsWrapping(statement)) - return false; - - var additionalLocations = ImmutableArray.Create(statement.GetLocation()); - context.ReportDiagnostic(DiagnosticHelper.Create( - this.Descriptor, - statement.GetFirstToken().GetLocation(), - notificationOption, - additionalLocations, - properties: null)); - return true; + return; } - public static bool StatementNeedsWrapping(StatementSyntax statement) + foreach (var child in node.ChildNodesAndTokens()) { - // Statement has to be parented by another statement (or an else-clause) to count. - var parent = statement.Parent; - var parentIsElseClause = parent.IsKind(SyntaxKind.ElseClause); + if (!context.ShouldAnalyzeSpan(child.Span)) + continue; - if (!(parent is StatementSyntax || parentIsElseClause)) - return false; + if (child.IsNode) + Recurse(context, notificationOption, child.AsNode()!); + } + } - // `else if` is always allowed. - if (statement.IsKind(SyntaxKind.IfStatement) && parentIsElseClause) - return false; + private bool CheckStatementSyntax(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, StatementSyntax statement) + { + if (!StatementNeedsWrapping(statement)) + return false; - var statementStartToken = statement.GetFirstToken(); + var additionalLocations = ImmutableArray.Create(statement.GetLocation()); + context.ReportDiagnostic(DiagnosticHelper.Create( + this.Descriptor, + statement.GetFirstToken().GetLocation(), + notificationOption, + context.Options, + additionalLocations, + properties: null)); + return true; + } - // we have to have a newline between the start of this statement and the previous statement. - if (ContainsEndOfLineBetween(statementStartToken.GetPreviousToken(), statementStartToken)) - return false; + public static bool StatementNeedsWrapping(StatementSyntax statement) + { + // Statement has to be parented by another statement (or an else-clause) to count. + var parent = statement.Parent; + var parentIsElseClause = parent.IsKind(SyntaxKind.ElseClause); - // Looks like a statement that might need wrapping. However, we do suppress wrapping for a few well known - // acceptable cases. + if (!(parent is StatementSyntax || parentIsElseClause)) + return false; - if (parent.IsKind(SyntaxKind.Block)) - { - // Blocks can be on a single line if parented by a member/accessor/lambda. - // And if they only contain a single statement at most within them. - var blockParent = parent.Parent; - if (blockParent is MemberDeclarationSyntax or - AccessorDeclarationSyntax or - AnonymousFunctionExpressionSyntax) - { - if (parent.DescendantNodes().OfType().Count() <= 1) - return false; - } - } + // `else if` is always allowed. + if (statement.IsKind(SyntaxKind.IfStatement) && parentIsElseClause) + return false; - return true; - } + var statementStartToken = statement.GetFirstToken(); - public static bool ContainsEndOfLineBetween(SyntaxToken previous, SyntaxToken next) - => ContainsEndOfLine(previous.TrailingTrivia) || ContainsEndOfLine(next.LeadingTrivia); + // we have to have a newline between the start of this statement and the previous statement. + if (ContainsEndOfLineBetween(statementStartToken.GetPreviousToken(), statementStartToken)) + return false; - private static bool ContainsEndOfLine(SyntaxTriviaList triviaList) + // Looks like a statement that might need wrapping. However, we do suppress wrapping for a few well known + // acceptable cases. + + if (parent.IsKind(SyntaxKind.Block)) { - foreach (var trivia in triviaList) + // Blocks can be on a single line if parented by a member/accessor/lambda. + // And if they only contain a single statement at most within them. + var blockParent = parent.Parent; + if (blockParent is MemberDeclarationSyntax or + AccessorDeclarationSyntax or + AnonymousFunctionExpressionSyntax) { - if (trivia.IsKind(SyntaxKind.EndOfLineTrivia)) - return true; + if (parent.DescendantNodes().OfType().Count() <= 1) + return false; } + } - return false; + return true; + } + + public static bool ContainsEndOfLineBetween(SyntaxToken previous, SyntaxToken next) + => ContainsEndOfLine(previous.TrailingTrivia) || ContainsEndOfLine(next.LeadingTrivia); + + private static bool ContainsEndOfLine(SyntaxTriviaList triviaList) + { + foreach (var trivia in triviaList) + { + if (trivia.IsKind(SyntaxKind.EndOfLineTrivia)) + return true; } + + return false; } } diff --git a/src/Analyzers/CSharp/Analyzers/NewLines/MultipleBlankLines/CSharpMultipleBlankLinesDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/NewLines/MultipleBlankLines/CSharpMultipleBlankLinesDiagnosticAnalyzer.cs index 193881b1fbe6f..9d53165229acb 100644 --- a/src/Analyzers/CSharp/Analyzers/NewLines/MultipleBlankLines/CSharpMultipleBlankLinesDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/NewLines/MultipleBlankLines/CSharpMultipleBlankLinesDiagnosticAnalyzer.cs @@ -6,14 +6,13 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.NewLines.MultipleBlankLines; -namespace Microsoft.CodeAnalysis.CSharp.NewLines.MultipleBlankLines +namespace Microsoft.CodeAnalysis.CSharp.NewLines.MultipleBlankLines; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpMultipleBlankLinesDiagnosticAnalyzer : AbstractMultipleBlankLinesDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpMultipleBlankLinesDiagnosticAnalyzer : AbstractMultipleBlankLinesDiagnosticAnalyzer + public CSharpMultipleBlankLinesDiagnosticAnalyzer() + : base(CSharpSyntaxFacts.Instance) { - public CSharpMultipleBlankLinesDiagnosticAnalyzer() - : base(CSharpSyntaxFacts.Instance) - { - } } } diff --git a/src/Analyzers/CSharp/Analyzers/OrderModifiers/CSharpOrderModifiersDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/OrderModifiers/CSharpOrderModifiersDiagnosticAnalyzer.cs index 48ccff2a88026..b4220b4071083 100644 --- a/src/Analyzers/CSharp/Analyzers/OrderModifiers/CSharpOrderModifiersDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/OrderModifiers/CSharpOrderModifiersDiagnosticAnalyzer.cs @@ -11,48 +11,47 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.OrderModifiers; -namespace Microsoft.CodeAnalysis.CSharp.OrderModifiers +namespace Microsoft.CodeAnalysis.CSharp.OrderModifiers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpOrderModifiersDiagnosticAnalyzer : AbstractOrderModifiersDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpOrderModifiersDiagnosticAnalyzer : AbstractOrderModifiersDiagnosticAnalyzer + public CSharpOrderModifiersDiagnosticAnalyzer() + : base(CSharpSyntaxFacts.Instance, + CSharpCodeStyleOptions.PreferredModifierOrder, + CSharpOrderModifiersHelper.Instance) { - public CSharpOrderModifiersDiagnosticAnalyzer() - : base(CSharpSyntaxFacts.Instance, - CSharpCodeStyleOptions.PreferredModifierOrder, - CSharpOrderModifiersHelper.Instance) - { - } + } - protected override CodeStyleOption2 GetPreferredOrderStyle(SyntaxTreeAnalysisContext context) - => context.GetCSharpAnalyzerOptions().PreferredModifierOrder; + protected override CodeStyleOption2 GetPreferredOrderStyle(SyntaxTreeAnalysisContext context) + => context.GetCSharpAnalyzerOptions().PreferredModifierOrder; - protected override void Recurse( - SyntaxTreeAnalysisContext context, - Dictionary preferredOrder, - NotificationOption2 notificationOption, - SyntaxNode root) + protected override void Recurse( + SyntaxTreeAnalysisContext context, + Dictionary preferredOrder, + NotificationOption2 notificationOption, + SyntaxNode root) + { + foreach (var child in root.ChildNodesAndTokens()) { - foreach (var child in root.ChildNodesAndTokens()) + if (child.IsNode && context.ShouldAnalyzeSpan(child.Span)) { - if (child.IsNode && context.ShouldAnalyzeSpan(child.Span)) + var node = child.AsNode(); + if (node is MemberDeclarationSyntax memberDeclaration) { - var node = child.AsNode(); - if (node is MemberDeclarationSyntax memberDeclaration) - { - CheckModifiers(context, preferredOrder, notificationOption, memberDeclaration); + CheckModifiers(context, preferredOrder, notificationOption, memberDeclaration); - // Recurse and check children. Note: we only do this if we're on an actual - // member declaration. Once we hit something that isn't, we don't need to - // keep recursing. This prevents us from actually entering things like method - // bodies. - Recurse(context, preferredOrder, notificationOption, node); - } - else if (node is AccessorListSyntax accessorList) + // Recurse and check children. Note: we only do this if we're on an actual + // member declaration. Once we hit something that isn't, we don't need to + // keep recursing. This prevents us from actually entering things like method + // bodies. + Recurse(context, preferredOrder, notificationOption, node); + } + else if (node is AccessorListSyntax accessorList) + { + foreach (var accessor in accessorList.Accessors) { - foreach (var accessor in accessorList.Accessors) - { - CheckModifiers(context, preferredOrder, notificationOption, accessor); - } + CheckModifiers(context, preferredOrder, notificationOption, accessor); } } } diff --git a/src/Analyzers/CSharp/Analyzers/OrderModifiers/CSharpOrderModifiersHelper.cs b/src/Analyzers/CSharp/Analyzers/OrderModifiers/CSharpOrderModifiersHelper.cs index ebbce44a21f13..71b9959203eec 100644 --- a/src/Analyzers/CSharp/Analyzers/OrderModifiers/CSharpOrderModifiersHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/OrderModifiers/CSharpOrderModifiersHelper.cs @@ -6,30 +6,29 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.OrderModifiers; -namespace Microsoft.CodeAnalysis.CSharp.OrderModifiers +namespace Microsoft.CodeAnalysis.CSharp.OrderModifiers; + +internal class CSharpOrderModifiersHelper : AbstractOrderModifiersHelpers { - internal class CSharpOrderModifiersHelper : AbstractOrderModifiersHelpers - { - public static readonly CSharpOrderModifiersHelper Instance = new(); + public static readonly CSharpOrderModifiersHelper Instance = new(); - private CSharpOrderModifiersHelper() - { - } + private CSharpOrderModifiersHelper() + { + } - protected override int GetKeywordKind(string trimmed) - { - var kind = SyntaxFacts.GetKeywordKind(trimmed); - return (int)(kind == SyntaxKind.None ? SyntaxFacts.GetContextualKeywordKind(trimmed) : kind); - } + protected override int GetKeywordKind(string trimmed) + { + var kind = SyntaxFacts.GetKeywordKind(trimmed); + return (int)(kind == SyntaxKind.None ? SyntaxFacts.GetContextualKeywordKind(trimmed) : kind); + } - protected override bool TryParse(string value, [NotNullWhen(true)] out Dictionary? parsed) - { - if (!base.TryParse(value, out parsed)) - return false; + protected override bool TryParse(string value, [NotNullWhen(true)] out Dictionary? parsed) + { + if (!base.TryParse(value, out parsed)) + return false; - // 'partial' must always go at the end in C#. - parsed[(int)SyntaxKind.PartialKeyword] = int.MaxValue; - return true; - } + // 'partial' must always go at the end in C#. + parsed[(int)SyntaxKind.PartialKeyword] = int.MaxValue; + return true; } } diff --git a/src/Analyzers/CSharp/Analyzers/PopulateSwitch/CSharpPopulateSwitchExpressionDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/PopulateSwitch/CSharpPopulateSwitchExpressionDiagnosticAnalyzer.cs index 4beaac97177ef..b8d50a704727a 100644 --- a/src/Analyzers/CSharp/Analyzers/PopulateSwitch/CSharpPopulateSwitchExpressionDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/PopulateSwitch/CSharpPopulateSwitchExpressionDiagnosticAnalyzer.cs @@ -6,13 +6,12 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.PopulateSwitch; -namespace Microsoft.CodeAnalysis.CSharp.PopulateSwitch +namespace Microsoft.CodeAnalysis.CSharp.PopulateSwitch; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpPopulateSwitchExpressionDiagnosticAnalyzer : + AbstractPopulateSwitchExpressionDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpPopulateSwitchExpressionDiagnosticAnalyzer : - AbstractPopulateSwitchExpressionDiagnosticAnalyzer - { - protected override Location GetDiagnosticLocation(SwitchExpressionSyntax switchBlock) - => switchBlock.SwitchKeyword.GetLocation(); - } + protected override Location GetDiagnosticLocation(SwitchExpressionSyntax switchBlock) + => switchBlock.SwitchKeyword.GetLocation(); } diff --git a/src/Analyzers/CSharp/Analyzers/QualifyMemberAccess/CSharpQualifyMemberAccessDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/QualifyMemberAccess/CSharpQualifyMemberAccessDiagnosticAnalyzer.cs index c4e6ed659c3ec..bf4cd6a0a96f0 100644 --- a/src/Analyzers/CSharp/Analyzers/QualifyMemberAccess/CSharpQualifyMemberAccessDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/QualifyMemberAccess/CSharpQualifyMemberAccessDiagnosticAnalyzer.cs @@ -11,59 +11,58 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Simplification; -namespace Microsoft.CodeAnalysis.CSharp.QualifyMemberAccess -{ - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpQualifyMemberAccessDiagnosticAnalyzer - : AbstractQualifyMemberAccessDiagnosticAnalyzer - { - protected override ISimplification Simplification - => CSharpSimplification.Instance; +namespace Microsoft.CodeAnalysis.CSharp.QualifyMemberAccess; - protected override bool IsAlreadyQualifiedMemberAccess(ExpressionSyntax node) - => node.IsKind(SyntaxKind.ThisExpression); - - // If the member is already qualified with `base.`, - // or member is in object initialization context, - // or member in property or field initialization, - // or member in constructor initializer, it cannot be qualified. - protected override bool CanMemberAccessBeQualified(ISymbol containingSymbol, SyntaxNode node) - { - if (node.GetAncestorOrThis() != null) - return false; +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpQualifyMemberAccessDiagnosticAnalyzer + : AbstractQualifyMemberAccessDiagnosticAnalyzer +{ + protected override ISimplification Simplification + => CSharpSimplification.Instance; - if (node.GetAncestorOrThis() != null) - return false; + protected override bool IsAlreadyQualifiedMemberAccess(ExpressionSyntax node) + => node.IsKind(SyntaxKind.ThisExpression); - if (node.IsKind(SyntaxKind.BaseExpression)) - return false; + // If the member is already qualified with `base.`, + // or member is in object initialization context, + // or member in property or field initialization, + // or member in constructor initializer, it cannot be qualified. + protected override bool CanMemberAccessBeQualified(ISymbol containingSymbol, SyntaxNode node) + { + if (node.GetAncestorOrThis() != null) + return false; - if (IsInPropertyOrFieldInitialization(containingSymbol, node)) - return false; + if (node.GetAncestorOrThis() != null) + return false; - if (node.Parent is AssignmentExpressionSyntax { Parent: InitializerExpressionSyntax(SyntaxKind.ObjectInitializerExpression), Left: var left } && - left == node) - { - return false; - } + if (node.IsKind(SyntaxKind.BaseExpression)) + return false; - return true; - } + if (IsInPropertyOrFieldInitialization(containingSymbol, node)) + return false; - private static bool IsInPropertyOrFieldInitialization(ISymbol containingSymbol, SyntaxNode node) + if (node.Parent is AssignmentExpressionSyntax { Parent: InitializerExpressionSyntax(SyntaxKind.ObjectInitializerExpression), Left: var left } && + left == node) { - return (containingSymbol.Kind is SymbolKind.Field or SymbolKind.Property) && - containingSymbol.DeclaringSyntaxReferences - .Select(declaringSyntaxReferences => declaringSyntaxReferences.GetSyntax()) - .Any(declaringSyntax => IsInPropertyInitialization(declaringSyntax, node) || IsInFieldInitialization(declaringSyntax, node)); + return false; } - private static bool IsInPropertyInitialization(SyntaxNode declarationSyntax, SyntaxNode node) - => declarationSyntax.IsKind(SyntaxKind.PropertyDeclaration) && declarationSyntax.Contains(node); - - private static bool IsInFieldInitialization(SyntaxNode declarationSyntax, SyntaxNode node) - => declarationSyntax.GetAncestorsOrThis(n => n.IsKind(SyntaxKind.FieldDeclaration) && n.Contains(node)).Any(); + return true; + } - protected override Location GetLocation(IOperation operation) => operation.Syntax.GetLocation(); + private static bool IsInPropertyOrFieldInitialization(ISymbol containingSymbol, SyntaxNode node) + { + return (containingSymbol.Kind is SymbolKind.Field or SymbolKind.Property) && + containingSymbol.DeclaringSyntaxReferences + .Select(declaringSyntaxReferences => declaringSyntaxReferences.GetSyntax()) + .Any(declaringSyntax => IsInPropertyInitialization(declaringSyntax, node) || IsInFieldInitialization(declaringSyntax, node)); } + + private static bool IsInPropertyInitialization(SyntaxNode declarationSyntax, SyntaxNode node) + => declarationSyntax.IsKind(SyntaxKind.PropertyDeclaration) && declarationSyntax.Contains(node); + + private static bool IsInFieldInitialization(SyntaxNode declarationSyntax, SyntaxNode node) + => declarationSyntax.GetAncestorsOrThis(n => n.IsKind(SyntaxKind.FieldDeclaration) && n.Contains(node)).Any(); + + protected override Location GetLocation(IOperation operation) => operation.Syntax.GetLocation(); } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveConfusingSuppression/CSharpRemoveConfusingSuppressionDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveConfusingSuppression/CSharpRemoveConfusingSuppressionDiagnosticAnalyzer.cs index 9343736934418..5574d045c2e13 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveConfusingSuppression/CSharpRemoveConfusingSuppressionDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveConfusingSuppression/CSharpRemoveConfusingSuppressionDiagnosticAnalyzer.cs @@ -8,44 +8,44 @@ using Microsoft.CodeAnalysis.Diagnostics; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.RemoveConfusingSuppression +namespace Microsoft.CodeAnalysis.CSharp.RemoveConfusingSuppression; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpRemoveConfusingSuppressionDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpRemoveConfusingSuppressionDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public CSharpRemoveConfusingSuppressionDiagnosticAnalyzer() + : base(IDEDiagnosticIds.RemoveConfusingSuppressionForIsExpressionDiagnosticId, + EnforceOnBuildValues.RemoveConfusingSuppressionForIsExpression, + option: null, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_unnecessary_suppression_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Suppression_operator_has_no_effect_and_can_be_misinterpreted), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpRemoveConfusingSuppressionDiagnosticAnalyzer() - : base(IDEDiagnosticIds.RemoveConfusingSuppressionForIsExpressionDiagnosticId, - EnforceOnBuildValues.RemoveConfusingSuppressionForIsExpression, - option: null, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_unnecessary_suppression_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Suppression_operator_has_no_effect_and_can_be_misinterpreted), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.IsExpression, SyntaxKind.IsPatternExpression); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.IsExpression, SyntaxKind.IsPatternExpression); - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var node = context.Node; + var left = node switch { - var node = context.Node; - var left = node switch - { - BinaryExpressionSyntax binary => binary.Left, - IsPatternExpressionSyntax isPattern => isPattern.Expression, - _ => throw ExceptionUtilities.UnexpectedValue(node), - }; - - if (left.Kind() != SyntaxKind.SuppressNullableWarningExpression) - return; - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - ((PostfixUnaryExpressionSyntax)left).OperatorToken.GetLocation(), - NotificationOption2.Warning, - ImmutableArray.Create(node.GetLocation()), - properties: null)); - } + BinaryExpressionSyntax binary => binary.Left, + IsPatternExpressionSyntax isPattern => isPattern.Expression, + _ => throw ExceptionUtilities.UnexpectedValue(node), + }; + + if (left.Kind() != SyntaxKind.SuppressNullableWarningExpression) + return; + + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + ((PostfixUnaryExpressionSyntax)left).OperatorToken.GetLocation(), + NotificationOption2.Warning, + context.Options, + ImmutableArray.Create(node.GetLocation()), + properties: null)); } } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveRedundantEquality/CSharpRemoveRedundantEqualityDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveRedundantEquality/CSharpRemoveRedundantEqualityDiagnosticAnalyzer.cs index 6c8ed97298237..6a953341dc596 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveRedundantEquality/CSharpRemoveRedundantEqualityDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveRedundantEquality/CSharpRemoveRedundantEqualityDiagnosticAnalyzer.cs @@ -6,14 +6,13 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.RemoveRedundantEquality; -namespace Microsoft.CodeAnalysis.CSharp.RemoveRedundantEquality +namespace Microsoft.CodeAnalysis.CSharp.RemoveRedundantEquality; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpRemoveRedundantEqualityDiagnosticAnalyzer + : AbstractRemoveRedundantEqualityDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpRemoveRedundantEqualityDiagnosticAnalyzer - : AbstractRemoveRedundantEqualityDiagnosticAnalyzer + public CSharpRemoveRedundantEqualityDiagnosticAnalyzer() : base(CSharpSyntaxFacts.Instance) { - public CSharpRemoveRedundantEqualityDiagnosticAnalyzer() : base(CSharpSyntaxFacts.Instance) - { - } } } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryDiscardDesignation/CSharpRemoveUnnecessaryDiscardDesignationDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryDiscardDesignation/CSharpRemoveUnnecessaryDiscardDesignationDiagnosticAnalyzer.cs index bd6d10302386c..ff48a216f5b1e 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryDiscardDesignation/CSharpRemoveUnnecessaryDiscardDesignationDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryDiscardDesignation/CSharpRemoveUnnecessaryDiscardDesignationDiagnosticAnalyzer.cs @@ -9,95 +9,94 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryDiscardDesignation +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryDiscardDesignation; + +/// +/// Supports code like o switch { int _ => ... } to just o switch { int => ... } in C# 9 and above. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpRemoveUnnecessaryDiscardDesignationDiagnosticAnalyzer + : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer { - /// - /// Supports code like o switch { int _ => ... } to just o switch { int => ... } in C# 9 and above. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpRemoveUnnecessaryDiscardDesignationDiagnosticAnalyzer - : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + public CSharpRemoveUnnecessaryDiscardDesignationDiagnosticAnalyzer() + : base(IDEDiagnosticIds.RemoveUnnecessaryDiscardDesignationDiagnosticId, + EnforceOnBuildValues.RemoveUnnecessaryDiscardDesignation, + option: null, + fadingOption: null, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_unnessary_discard), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Discard_can_be_removed), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpRemoveUnnecessaryDiscardDesignationDiagnosticAnalyzer() - : base(IDEDiagnosticIds.RemoveUnnecessaryDiscardDesignationDiagnosticId, - EnforceOnBuildValues.RemoveUnnecessaryDiscardDesignation, - option: null, - fadingOption: null, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_unnessary_discard), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Discard_can_be_removed), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - context.RegisterCompilationStartAction(context => - { - if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp9) - return; + if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp9) + return; - context.RegisterSyntaxNodeAction(AnalyzeDiscardDesignation, SyntaxKind.DiscardDesignation); - }); - } + context.RegisterSyntaxNodeAction(AnalyzeDiscardDesignation, SyntaxKind.DiscardDesignation); + }); + } - private void AnalyzeDiscardDesignation(SyntaxNodeAnalysisContext context) - { - if (ShouldSkipAnalysis(context, notification: null)) - return; + private void AnalyzeDiscardDesignation(SyntaxNodeAnalysisContext context) + { + if (ShouldSkipAnalysis(context, notification: null)) + return; - var semanticModel = context.SemanticModel; - var cancellationToken = context.CancellationToken; + var semanticModel = context.SemanticModel; + var cancellationToken = context.CancellationToken; - var discard = (DiscardDesignationSyntax)context.Node; + var discard = (DiscardDesignationSyntax)context.Node; - if (discard.Parent is DeclarationPatternSyntax declarationPattern) + if (discard.Parent is DeclarationPatternSyntax declarationPattern) + { + // Don't perform semantic checks if we are in a 'simple' pattern like `x is A _`. + // Since single identifier in cases like `x is A` binds stronger to type name for back compat reasons, we can safely remove discard anyway. + if (declarationPattern.Parent is not IsPatternExpressionSyntax) { - // Don't perform semantic checks if we are in a 'simple' pattern like `x is A _`. - // Since single identifier in cases like `x is A` binds stronger to type name for back compat reasons, we can safely remove discard anyway. - if (declarationPattern.Parent is not IsPatternExpressionSyntax) - { - var typeSyntax = declarationPattern.Type; - - while (typeSyntax is QualifiedNameSyntax qualifiedName) - typeSyntax = qualifiedName.Left; - - if (typeSyntax is IdentifierNameSyntax identifierName && - identifierName.GetAncestor() is { } containingTypeSyntax) - { - var typeSymbol = semanticModel.GetRequiredDeclaredSymbol(containingTypeSyntax, cancellationToken); - - // If we find other symbols with the same name in the type we are currently in, removing discard can lead to a compiler error. - // For instance, we can have a property in the type we are currently in with the same name as an identifier in the discard designation. - // Since a single identifier binds stronger to property name, we cannot remove discard. - if (typeSymbol.GetMembers(identifierName.Identifier.ValueText).Any()) - return; - } - } + var typeSyntax = declarationPattern.Type; - Report(discard); - } - else if (discard.Parent is RecursivePatternSyntax recursivePattern) - { - // can't remove from `(int i) _` as `(int i)` is not a legal pattern itself. - if (recursivePattern.PositionalPatternClause != null && - recursivePattern.PositionalPatternClause.Subpatterns.Count == 1) + while (typeSyntax is QualifiedNameSyntax qualifiedName) + typeSyntax = qualifiedName.Left; + + if (typeSyntax is IdentifierNameSyntax identifierName && + identifierName.GetAncestor() is { } containingTypeSyntax) { - return; - } + var typeSymbol = semanticModel.GetRequiredDeclaredSymbol(containingTypeSyntax, cancellationToken); - Report(discard); + // If we find other symbols with the same name in the type we are currently in, removing discard can lead to a compiler error. + // For instance, we can have a property in the type we are currently in with the same name as an identifier in the discard designation. + // Since a single identifier binds stronger to property name, we cannot remove discard. + if (typeSymbol.GetMembers(identifierName.Identifier.ValueText).Any()) + return; + } } - return; - - void Report(DiscardDesignationSyntax discard) + Report(discard); + } + else if (discard.Parent is RecursivePatternSyntax recursivePattern) + { + // can't remove from `(int i) _` as `(int i)` is not a legal pattern itself. + if (recursivePattern.PositionalPatternClause != null && + recursivePattern.PositionalPatternClause.Subpatterns.Count == 1) { - context.ReportDiagnostic(Diagnostic.Create( - this.Descriptor, - discard.GetLocation())); + return; } + + Report(discard); + } + + return; + + void Report(DiscardDesignationSyntax discard) + { + context.ReportDiagnostic(Diagnostic.Create( + this.Descriptor, + discard.GetLocation())); } } } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer.cs index bcfe97f392cde..c750fbf86a3d3 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer.cs @@ -17,54 +17,53 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryImports +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryImports; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer : + AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer : - AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer + public CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer() + : base(new LocalizableResourceString(nameof(CSharpAnalyzersResources.Using_directive_is_unnecessary), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer() - : base(new LocalizableResourceString(nameof(CSharpAnalyzersResources.Using_directive_is_unnecessary), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - protected override ISyntaxFacts SyntaxFacts - => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts SyntaxFacts + => CSharpSyntaxFacts.Instance; - // C# has no need to do any merging of using statements. Only VB needs to - // merge import clauses to an import statement if it all the import clauses - // are unnecessary. - protected override ImmutableArray MergeImports(ImmutableArray unnecessaryImports) - => ImmutableArray.CastUp(unnecessaryImports); + // C# has no need to do any merging of using statements. Only VB needs to + // merge import clauses to an import statement if it all the import clauses + // are unnecessary. + protected override ImmutableArray MergeImports(ImmutableArray unnecessaryImports) + => ImmutableArray.CastUp(unnecessaryImports); - protected override IUnnecessaryImportsProvider UnnecessaryImportsProvider - => CSharpUnnecessaryImportsProvider.Instance; + protected override IUnnecessaryImportsProvider UnnecessaryImportsProvider + => CSharpUnnecessaryImportsProvider.Instance; - protected override bool IsRegularCommentOrDocComment(SyntaxTrivia trivia) - => trivia.IsRegularComment() || trivia.IsDocComment(); + protected override bool IsRegularCommentOrDocComment(SyntaxTrivia trivia) + => trivia.IsRegularComment() || trivia.IsDocComment(); - protected override SyntaxToken? TryGetLastToken(SyntaxNode node) - // No special behavior needed for C# - => null; + protected override SyntaxToken? TryGetLastToken(SyntaxNode node) + // No special behavior needed for C# + => null; - protected override IEnumerable GetFixableDiagnosticSpans( - IEnumerable nodes, SyntaxTree tree, CancellationToken cancellationToken) - { - Contract.ThrowIfFalse(nodes.Any()); + protected override IEnumerable GetFixableDiagnosticSpans( + IEnumerable nodes, SyntaxTree tree, CancellationToken cancellationToken) + { + Contract.ThrowIfFalse(nodes.Any()); - var nodesContainingUnnecessaryUsings = new HashSet(); - foreach (var node in nodes) + var nodesContainingUnnecessaryUsings = new HashSet(); + foreach (var node in nodes) + { + var nodeContainingUnnecessaryUsings = node.GetAncestors().First(n => n is BaseNamespaceDeclarationSyntax or CompilationUnitSyntax); + if (!nodesContainingUnnecessaryUsings.Add(nodeContainingUnnecessaryUsings)) { - var nodeContainingUnnecessaryUsings = node.GetAncestors().First(n => n is BaseNamespaceDeclarationSyntax or CompilationUnitSyntax); - if (!nodesContainingUnnecessaryUsings.Add(nodeContainingUnnecessaryUsings)) - { - continue; - } - - yield return nodeContainingUnnecessaryUsings is BaseNamespaceDeclarationSyntax namespaceDeclaration - ? namespaceDeclaration.Usings.GetContainedSpan() - : ((CompilationUnitSyntax)nodeContainingUnnecessaryUsings).Usings.GetContainedSpan(); + continue; } + + yield return nodeContainingUnnecessaryUsings is BaseNamespaceDeclarationSyntax namespaceDeclaration + ? namespaceDeclaration.Usings.GetContainedSpan() + : ((CompilationUnitSyntax)nodeContainingUnnecessaryUsings).Usings.GetContainedSpan(); } } } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryLambdaExpression/CSharpRemoveUnnecessaryLambdaExpressionDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryLambdaExpression/CSharpRemoveUnnecessaryLambdaExpressionDiagnosticAnalyzer.cs index a5cc14b6c0d05..089f5c0521045 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryLambdaExpression/CSharpRemoveUnnecessaryLambdaExpressionDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryLambdaExpression/CSharpRemoveUnnecessaryLambdaExpressionDiagnosticAnalyzer.cs @@ -18,330 +18,330 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryLambdaExpression +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryLambdaExpression; + +/// +/// DiagnosticAnalyzer that looks code like Goo(() => Bar()) and offers to convert it to Goo(Bar). +/// This is only offered on C# 11 and above where this delegate can be cached and will not cause allocations each +/// time. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpRemoveUnnecessaryLambdaExpressionDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer { - /// - /// DiagnosticAnalyzer that looks code like Goo(() => Bar()) and offers to convert it to Goo(Bar). - /// This is only offered on C# 11 and above where this delegate can be cached and will not cause allocations each - /// time. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpRemoveUnnecessaryLambdaExpressionDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + public CSharpRemoveUnnecessaryLambdaExpressionDiagnosticAnalyzer() + : base(IDEDiagnosticIds.RemoveUnnecessaryLambdaExpressionDiagnosticId, + EnforceOnBuildValues.RemoveUnnecessaryLambdaExpression, + CSharpCodeStyleOptions.PreferMethodGroupConversion, + fadingOption: null, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_unnecessary_lambda_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Lambda_expression_can_be_removed), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpRemoveUnnecessaryLambdaExpressionDiagnosticAnalyzer() - : base(IDEDiagnosticIds.RemoveUnnecessaryLambdaExpressionDiagnosticId, - EnforceOnBuildValues.RemoveUnnecessaryLambdaExpression, - CSharpCodeStyleOptions.PreferMethodGroupConversion, - fadingOption: null, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_unnecessary_lambda_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Lambda_expression_can_be_removed), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - context.RegisterCompilationStartAction(context => + if (context.Compilation.LanguageVersion().IsCSharp11OrAbove()) { - if (context.Compilation.LanguageVersion().IsCSharp11OrAbove()) - { - var expressionType = context.Compilation.ExpressionOfTType(); - var conditionalAttributeType = context.Compilation.ConditionalAttribute(); - - context.RegisterSyntaxNodeAction( - c => AnalyzeSyntax(c, expressionType, conditionalAttributeType), - SyntaxKind.SimpleLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.AnonymousMethodExpression); - } - }); - } - - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context, INamedTypeSymbol? expressionType, INamedTypeSymbol? conditionalAttributeType) - { - var cancellationToken = context.CancellationToken; - var semanticModel = context.SemanticModel; - var syntaxTree = semanticModel.SyntaxTree; + var expressionType = context.Compilation.ExpressionOfTType(); + var conditionalAttributeType = context.Compilation.ConditionalAttribute(); - var preference = context.GetCSharpAnalyzerOptions().PreferMethodGroupConversion; - if (ShouldSkipAnalysis(context, preference.Notification)) - { - // User doesn't care about this rule. - return; + context.RegisterSyntaxNodeAction( + c => AnalyzeSyntax(c, expressionType, conditionalAttributeType), + SyntaxKind.SimpleLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.AnonymousMethodExpression); } + }); + } - var anonymousFunction = (AnonymousFunctionExpressionSyntax)context.Node; + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context, INamedTypeSymbol? expressionType, INamedTypeSymbol? conditionalAttributeType) + { + var cancellationToken = context.CancellationToken; + var semanticModel = context.SemanticModel; + var syntaxTree = semanticModel.SyntaxTree; - // Syntax checks first. + var preference = context.GetCSharpAnalyzerOptions().PreferMethodGroupConversion; + if (ShouldSkipAnalysis(context, preference.Notification)) + { + // User doesn't care about this rule. + return; + } - // Don't simplify static lambdas. The user made them explicitly static to make it clear it must only cause - // a single allocation for the cached delegate. If we get rid of the lambda (and thus the static-keyword) it - // won't be clear anymore if the member-group-conversion allocation is cached or not. - if (anonymousFunction.Modifiers.Any(SyntaxKind.StaticKeyword)) - return; + var anonymousFunction = (AnonymousFunctionExpressionSyntax)context.Node; - if (!TryGetAnonymousFunctionInvocation(anonymousFunction, out var invocation, out var wasAwaited)) - return; + // Syntax checks first. - // If we had an async function, but we didn't await the expression inside then we can't convert this. The - // underlying value was wrapped into a task, and that won't work if directly referencing the function. - if (wasAwaited != anonymousFunction.Modifiers.Any(SyntaxKind.AsyncKeyword)) - return; + // Don't simplify static lambdas. The user made them explicitly static to make it clear it must only cause + // a single allocation for the cached delegate. If we get rid of the lambda (and thus the static-keyword) it + // won't be clear anymore if the member-group-conversion allocation is cached or not. + if (anonymousFunction.Modifiers.Any(SyntaxKind.StaticKeyword)) + return; - // We have to have an invocation in the lambda like `() => X()` or `() => expr.X()`. - var invokedExpression = invocation.Expression; - if (invokedExpression is not SimpleNameSyntax and not MemberAccessExpressionSyntax) - return; + if (!TryGetAnonymousFunctionInvocation(anonymousFunction, out var invocation, out var wasAwaited)) + return; - // lambda and invocation have to agree on number of parameters. - var parameters = GetParameters(anonymousFunction); - if (parameters.Count != invocation.ArgumentList.Arguments.Count) - return; + // If we had an async function, but we didn't await the expression inside then we can't convert this. The + // underlying value was wrapped into a task, and that won't work if directly referencing the function. + if (wasAwaited != anonymousFunction.Modifiers.Any(SyntaxKind.AsyncKeyword)) + return; - // parameters must be passed 1:1 from lambda to invocation. - for (int i = 0, n = parameters.Count; i < n; i++) - { - var parameter = parameters[i]; - var argument = invocation.ArgumentList.Arguments[i]; + // We have to have an invocation in the lambda like `() => X()` or `() => expr.X()`. + var invokedExpression = invocation.Expression; + if (invokedExpression is not SimpleNameSyntax and not MemberAccessExpressionSyntax) + return; - if (argument.Expression is not IdentifierNameSyntax argumentIdentifier) - return; + // lambda and invocation have to agree on number of parameters. + var parameters = GetParameters(anonymousFunction); + if (parameters.Count != invocation.ArgumentList.Arguments.Count) + return; - if (parameter.Identifier.ValueText != argumentIdentifier.Identifier.ValueText) - return; - } + // parameters must be passed 1:1 from lambda to invocation. + for (int i = 0, n = parameters.Count; i < n; i++) + { + var parameter = parameters[i]; + var argument = invocation.ArgumentList.Arguments[i]; - // if we have `() => new C().X()` then converting to `new C().X` very much changes the meaning. - if (MayHaveSideEffects(invokedExpression)) + if (argument.Expression is not IdentifierNameSyntax argumentIdentifier) return; - // Looks like a reasonable candidate to simplify. Now switch to semantics to check for sure. - - if (CSharpSemanticFacts.Instance.IsInExpressionTree(semanticModel, anonymousFunction, expressionType, cancellationToken)) + if (parameter.Identifier.ValueText != argumentIdentifier.Identifier.ValueText) return; + } - // If we have `object obj = x => Goo(x);` we don't want to simplify. The compiler warns if you write - // `object obj = Goo;` because of the conversion to a non-delegate type. While we could insert a cast here - // to make this work, that goes against the spirit of this analyzer/fixer just removing code. - var lambdaTypeInfo = semanticModel.GetTypeInfo(anonymousFunction, cancellationToken); - if (lambdaTypeInfo.ConvertedType == null || lambdaTypeInfo.ConvertedType.SpecialType is SpecialType.System_Object) - return; + // if we have `() => new C().X()` then converting to `new C().X` very much changes the meaning. + if (MayHaveSideEffects(invokedExpression)) + return; - var lambdaSymbolInfo = semanticModel.GetSymbolInfo(anonymousFunction, cancellationToken); - if (lambdaSymbolInfo.Symbol is not IMethodSymbol lambdaMethod) - return; + // Looks like a reasonable candidate to simplify. Now switch to semantics to check for sure. - var invokedSymbolInfo = semanticModel.GetSymbolInfo(invokedExpression, cancellationToken); - if (invokedSymbolInfo.Symbol is not IMethodSymbol invokedMethod) - return; + if (CSharpSemanticFacts.Instance.IsInExpressionTree(semanticModel, anonymousFunction, expressionType, cancellationToken)) + return; - // cannot convert a partial-definition to a delegate (unless there's an existing implementation part that can be used). - if (invokedMethod.IsPartialDefinition && invokedMethod.PartialImplementationPart is null) - return; + // If we have `object obj = x => Goo(x);` we don't want to simplify. The compiler warns if you write + // `object obj = Goo;` because of the conversion to a non-delegate type. While we could insert a cast here + // to make this work, that goes against the spirit of this analyzer/fixer just removing code. + var lambdaTypeInfo = semanticModel.GetTypeInfo(anonymousFunction, cancellationToken); + if (lambdaTypeInfo.ConvertedType == null || lambdaTypeInfo.ConvertedType.SpecialType is SpecialType.System_Object) + return; - // If we're calling a generic method, we have to have supplied type arguments. They cannot be inferred once - // we remove the arguments during simplification. - var invokedTypeArguments = invokedExpression.GetRightmostName() is GenericNameSyntax genericName - ? genericName.TypeArgumentList.Arguments - : default; + var lambdaSymbolInfo = semanticModel.GetSymbolInfo(anonymousFunction, cancellationToken); + if (lambdaSymbolInfo.Symbol is not IMethodSymbol lambdaMethod) + return; - if (invokedMethod.TypeArguments.Length != invokedTypeArguments.Count) - return; + var invokedSymbolInfo = semanticModel.GetSymbolInfo(invokedExpression, cancellationToken); + if (invokedSymbolInfo.Symbol is not IMethodSymbol invokedMethod) + return; - // Methods have to be complimentary. That means the same number of parameters, with proper - // co-contravariance for the parameters and return type. - if (lambdaMethod.Parameters.Length != invokedMethod.Parameters.Length) - return; + // cannot convert a partial-definition to a delegate (unless there's an existing implementation part that can be used). + if (invokedMethod.IsPartialDefinition && invokedMethod.PartialImplementationPart is null) + return; - var compilation = semanticModel.Compilation; + // If we're calling a generic method, we have to have supplied type arguments. They cannot be inferred once + // we remove the arguments during simplification. + var invokedTypeArguments = invokedExpression.GetRightmostName() is GenericNameSyntax genericName + ? genericName.TypeArgumentList.Arguments + : default; - // Must be able to convert the invoked method return type to the lambda's return type. - if (!IsIdentityOrImplicitConversion(compilation, invokedMethod.ReturnType, lambdaMethod.ReturnType)) - return; + if (invokedMethod.TypeArguments.Length != invokedTypeArguments.Count) + return; - for (int i = 0, n = lambdaMethod.Parameters.Length; i < n; i++) - { - var lambdaParameter = lambdaMethod.Parameters[i]; - var invokedParameter = invokedMethod.Parameters[i]; + // Methods have to be complimentary. That means the same number of parameters, with proper + // co-contravariance for the parameters and return type. + if (lambdaMethod.Parameters.Length != invokedMethod.Parameters.Length) + return; - if (lambdaParameter.RefKind != invokedParameter.RefKind) - return; + var compilation = semanticModel.Compilation; - // All the lambda parameters must be convertible to the invoked method parameters. - if (!IsIdentityOrImplicitConversion(compilation, lambdaParameter.Type, invokedParameter.Type)) - return; - } + // Must be able to convert the invoked method return type to the lambda's return type. + if (!IsIdentityOrImplicitConversion(compilation, invokedMethod.ReturnType, lambdaMethod.ReturnType)) + return; - // If invoked method is conditional, converting lambda to method group produces compiler error - if (invokedMethod.GetAttributes().Any(a => Equals(a.AttributeClass, conditionalAttributeType))) + for (int i = 0, n = lambdaMethod.Parameters.Length; i < n; i++) + { + var lambdaParameter = lambdaMethod.Parameters[i]; + var invokedParameter = invokedMethod.Parameters[i]; + + if (lambdaParameter.RefKind != invokedParameter.RefKind) + return; + + // All the lambda parameters must be convertible to the invoked method parameters. + if (!IsIdentityOrImplicitConversion(compilation, lambdaParameter.Type, invokedParameter.Type)) return; + } + + // If invoked method is conditional, converting lambda to method group produces compiler error + if (invokedMethod.GetAttributes().Any(a => Equals(a.AttributeClass, conditionalAttributeType))) + return; - // In the case where we have `() => expr.m()`, check if `expr` is overwritten anywhere. If so then we do not - // want to remove the lambda, as that will bind eagerly to the original `expr` and will not see the write - // that later happens - if (invokedExpression is MemberAccessExpressionSyntax { Expression: var accessedExpression }) + // In the case where we have `() => expr.m()`, check if `expr` is overwritten anywhere. If so then we do not + // want to remove the lambda, as that will bind eagerly to the original `expr` and will not see the write + // that later happens + if (invokedExpression is MemberAccessExpressionSyntax { Expression: var accessedExpression }) + { + // Limit the search space to the outermost code block that could contain references to this expr (or + // fall back to compilation unit for top level statements). + var outermostBody = invokedExpression.AncestorsAndSelf().LastOrDefault( + n => n is BlockSyntax or ArrowExpressionClauseSyntax or AnonymousFunctionExpressionSyntax or GlobalStatementSyntax); + if (outermostBody is null or GlobalStatementSyntax) + outermostBody = syntaxTree.GetRoot(cancellationToken); + + foreach (var candidate in outermostBody.DescendantNodes().OfType()) { - // Limit the search space to the outermost code block that could contain references to this expr (or - // fall back to compilation unit for top level statements). - var outermostBody = invokedExpression.AncestorsAndSelf().LastOrDefault( - n => n is BlockSyntax or ArrowExpressionClauseSyntax or AnonymousFunctionExpressionSyntax or GlobalStatementSyntax); - if (outermostBody is null or GlobalStatementSyntax) - outermostBody = syntaxTree.GetRoot(cancellationToken); - - foreach (var candidate in outermostBody.DescendantNodes().OfType()) + if (candidate != accessedExpression && + SemanticEquivalence.AreEquivalent(semanticModel, candidate, accessedExpression) && + candidate.IsWrittenTo(semanticModel, cancellationToken)) { - if (candidate != accessedExpression && - SemanticEquivalence.AreEquivalent(semanticModel, candidate, accessedExpression) && - candidate.IsWrittenTo(semanticModel, cancellationToken)) - { - return; - } + return; } } + } - // Semantically, this looks good to go. Now, do an actual speculative replacement to ensure that the - // non-invoked method reference refers to the same method symbol, and that it converts to the same type that - // the lambda was. - var analyzer = new SpeculationAnalyzer(anonymousFunction, invokedExpression, semanticModel, cancellationToken); + // Semantically, this looks good to go. Now, do an actual speculative replacement to ensure that the + // non-invoked method reference refers to the same method symbol, and that it converts to the same type that + // the lambda was. + var analyzer = new SpeculationAnalyzer(anonymousFunction, invokedExpression, semanticModel, cancellationToken); - var rewrittenExpression = analyzer.ReplacedExpression; - var rewrittenSemanticModel = analyzer.SpeculativeSemanticModel; + var rewrittenExpression = analyzer.ReplacedExpression; + var rewrittenSemanticModel = analyzer.SpeculativeSemanticModel; - var rewrittenSymbolInfo = rewrittenSemanticModel.GetSymbolInfo(rewrittenExpression, cancellationToken); - if (rewrittenSymbolInfo.Symbol is not IMethodSymbol rewrittenMethod || - !invokedMethod.Equals(rewrittenMethod)) - { - return; - } + var rewrittenSymbolInfo = rewrittenSemanticModel.GetSymbolInfo(rewrittenExpression, cancellationToken); + if (rewrittenSymbolInfo.Symbol is not IMethodSymbol rewrittenMethod || + !invokedMethod.Equals(rewrittenMethod)) + { + return; + } - var rewrittenConvertedType = rewrittenSemanticModel.GetTypeInfo(rewrittenExpression, cancellationToken).ConvertedType; - if (!lambdaTypeInfo.ConvertedType.Equals(rewrittenConvertedType)) - return; + var rewrittenConvertedType = rewrittenSemanticModel.GetTypeInfo(rewrittenExpression, cancellationToken).ConvertedType; + if (!lambdaTypeInfo.ConvertedType.Equals(rewrittenConvertedType)) + return; - if (OverloadsChanged( - semanticModel, anonymousFunction.GetRequiredParent(), - rewrittenSemanticModel, rewrittenExpression.GetRequiredParent(), cancellationToken)) - { - return; - } + if (OverloadsChanged( + semanticModel, anonymousFunction.GetRequiredParent(), + rewrittenSemanticModel, rewrittenExpression.GetRequiredParent(), cancellationToken)) + { + return; + } - var startReportSpan = TextSpan.FromBounds(anonymousFunction.SpanStart, invokedExpression.SpanStart); - var endReportSpan = TextSpan.FromBounds(invokedExpression.Span.End, anonymousFunction.Span.End); + var startReportSpan = TextSpan.FromBounds(anonymousFunction.SpanStart, invokedExpression.SpanStart); + var endReportSpan = TextSpan.FromBounds(invokedExpression.Span.End, anonymousFunction.Span.End); - context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( - Descriptor, - syntaxTree.GetLocation(startReportSpan), - preference.Notification, - additionalLocations: [anonymousFunction.GetLocation()], - additionalUnnecessaryLocations: [syntaxTree.GetLocation(startReportSpan), syntaxTree.GetLocation(endReportSpan)])); - } + context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( + Descriptor, + syntaxTree.GetLocation(startReportSpan), + preference.Notification, + context.Options, + additionalLocations: [anonymousFunction.GetLocation()], + additionalUnnecessaryLocations: [syntaxTree.GetLocation(startReportSpan), syntaxTree.GetLocation(endReportSpan)])); + } - private static bool OverloadsChanged( - SemanticModel semanticModel1, - SyntaxNode? node1, - SemanticModel semanticModel2, - SyntaxNode? node2, - CancellationToken cancellationToken) + private static bool OverloadsChanged( + SemanticModel semanticModel1, + SyntaxNode? node1, + SemanticModel semanticModel2, + SyntaxNode? node2, + CancellationToken cancellationToken) + { + while (node1 != null && node2 != null) { - while (node1 != null && node2 != null) - { - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - var method1 = semanticModel1.GetSymbolInfo(node1, cancellationToken).Symbol as IMethodSymbol; - var method2 = semanticModel2.GetSymbolInfo(node2, cancellationToken).Symbol as IMethodSymbol; + var method1 = semanticModel1.GetSymbolInfo(node1, cancellationToken).Symbol as IMethodSymbol; + var method2 = semanticModel2.GetSymbolInfo(node2, cancellationToken).Symbol as IMethodSymbol; - if (method1 is null != method2 is null) - return true; + if (method1 is null != method2 is null) + return true; - if (method1 is not null && !method1.Equals(method2, SymbolEqualityComparer.IncludeNullability)) - return true; + if (method1 is not null && !method1.Equals(method2, SymbolEqualityComparer.IncludeNullability)) + return true; - node1 = node1.Parent; - node2 = node2.Parent; - } + node1 = node1.Parent; + node2 = node2.Parent; + } + return false; + } + + private static bool IsIdentityOrImplicitConversion(Compilation compilation, ITypeSymbol type1, ITypeSymbol type2) + { + // Dynamic can have an identity conversion between types. But it can have a very different effect on the + // generated code. Do not allow the change if these are not in agreement. + if (type1 is IDynamicTypeSymbol != type2 is IDynamicTypeSymbol) return false; - } - private static bool IsIdentityOrImplicitConversion(Compilation compilation, ITypeSymbol type1, ITypeSymbol type2) - { - // Dynamic can have an identity conversion between types. But it can have a very different effect on the - // generated code. Do not allow the change if these are not in agreement. - if (type1 is IDynamicTypeSymbol != type2 is IDynamicTypeSymbol) - return false; + var conversion = compilation.ClassifyConversion(type1, type2); + return conversion.IsIdentityOrImplicitReference(); + } - var conversion = compilation.ClassifyConversion(type1, type2); - return conversion.IsIdentityOrImplicitReference(); - } + private static bool MayHaveSideEffects(ExpressionSyntax expression) + { + // Checks to see if the expression being invoked looks side-effect free. If so, changing from executing + // each time in the lambda to only executing it once could have impact on the program. + + return !expression.DescendantNodesAndSelf().All( + n => n is TypeSyntax or + TypeArgumentListSyntax or + MemberAccessExpressionSyntax or + InstanceExpressionSyntax or + LiteralExpressionSyntax); + } - private static bool MayHaveSideEffects(ExpressionSyntax expression) + private static SeparatedSyntaxList GetParameters(AnonymousFunctionExpressionSyntax expression) + => expression switch { - // Checks to see if the expression being invoked looks side-effect free. If so, changing from executing - // each time in the lambda to only executing it once could have impact on the program. - - return !expression.DescendantNodesAndSelf().All( - n => n is TypeSyntax or - TypeArgumentListSyntax or - MemberAccessExpressionSyntax or - InstanceExpressionSyntax or - LiteralExpressionSyntax); - } + AnonymousMethodExpressionSyntax anonymousMethod => anonymousMethod.ParameterList?.Parameters ?? default, + SimpleLambdaExpressionSyntax simpleLambda => [simpleLambda.Parameter], + ParenthesizedLambdaExpressionSyntax parenthesizedLambda => parenthesizedLambda.ParameterList.Parameters, + _ => throw ExceptionUtilities.UnexpectedValue(expression.Kind()), + }; + + public static bool TryGetAnonymousFunctionInvocation( + AnonymousFunctionExpressionSyntax anonymousFunction, + [NotNullWhen(true)] out InvocationExpressionSyntax? invocation, + out bool wasAwaited) + { + if (anonymousFunction.ExpressionBody != null) + return TryGetInvocation(anonymousFunction.ExpressionBody, out invocation, out wasAwaited); - private static SeparatedSyntaxList GetParameters(AnonymousFunctionExpressionSyntax expression) - => expression switch - { - AnonymousMethodExpressionSyntax anonymousMethod => anonymousMethod.ParameterList?.Parameters ?? default, - SimpleLambdaExpressionSyntax simpleLambda => [simpleLambda.Parameter], - ParenthesizedLambdaExpressionSyntax parenthesizedLambda => parenthesizedLambda.ParameterList.Parameters, - _ => throw ExceptionUtilities.UnexpectedValue(expression.Kind()), - }; - - public static bool TryGetAnonymousFunctionInvocation( - AnonymousFunctionExpressionSyntax anonymousFunction, - [NotNullWhen(true)] out InvocationExpressionSyntax? invocation, - out bool wasAwaited) + if (anonymousFunction.Block != null && anonymousFunction.Block.Statements.Count == 1) { - if (anonymousFunction.ExpressionBody != null) - return TryGetInvocation(anonymousFunction.ExpressionBody, out invocation, out wasAwaited); + var statement = anonymousFunction.Block.Statements[0]; + if (statement is ReturnStatementSyntax { Expression: { } expression }) + return TryGetInvocation(expression, out invocation, out wasAwaited); - if (anonymousFunction.Block != null && anonymousFunction.Block.Statements.Count == 1) - { - var statement = anonymousFunction.Block.Statements[0]; - if (statement is ReturnStatementSyntax { Expression: { } expression }) - return TryGetInvocation(expression, out invocation, out wasAwaited); + if (statement is ExpressionStatementSyntax expressionStatement) + return TryGetInvocation(expressionStatement.Expression, out invocation, out wasAwaited); + } - if (statement is ExpressionStatementSyntax expressionStatement) - return TryGetInvocation(expressionStatement.Expression, out invocation, out wasAwaited); - } + invocation = null; + wasAwaited = false; + return false; + } - invocation = null; - wasAwaited = false; - return false; - } + private static bool TryGetInvocation( + ExpressionSyntax expression, + [NotNullWhen(true)] out InvocationExpressionSyntax? invocation, + out bool wasAwaited) + { + wasAwaited = false; - private static bool TryGetInvocation( - ExpressionSyntax expression, - [NotNullWhen(true)] out InvocationExpressionSyntax? invocation, - out bool wasAwaited) + // if we have either `await Goo()` or `await Goo().ConfigureAwait` then unwrap to get at `Goo()`. + if (expression is AwaitExpressionSyntax awaitExpression) { - wasAwaited = false; - - // if we have either `await Goo()` or `await Goo().ConfigureAwait` then unwrap to get at `Goo()`. - if (expression is AwaitExpressionSyntax awaitExpression) - { - wasAwaited = true; - expression = awaitExpression.Expression; - if (expression is InvocationExpressionSyntax - { - Expression: MemberAccessExpressionSyntax { Name.Identifier.ValueText: nameof(Task.ConfigureAwait), Expression: var underlying } - }) + wasAwaited = true; + expression = awaitExpression.Expression; + if (expression is InvocationExpressionSyntax { - expression = underlying; - } + Expression: MemberAccessExpressionSyntax { Name.Identifier.ValueText: nameof(Task.ConfigureAwait), Expression: var underlying } + }) + { + expression = underlying; } - - invocation = expression as InvocationExpressionSyntax; - return invocation != null; } + + invocation = expression as InvocationExpressionSyntax; + return invocation != null; } } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer.cs index f8186cced4fe0..eccfa38c27a86 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer.cs @@ -16,318 +16,317 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.RemoveUnnecessaryNullableDirective +namespace Microsoft.CodeAnalysis.RemoveUnnecessaryNullableDirective; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer + : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer - : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + public CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer() + : base(IDEDiagnosticIds.RemoveUnnecessaryNullableDirectiveDiagnosticId, + EnforceOnBuildValues.RemoveUnnecessaryNullableDirective, + option: null, + fadingOption: null, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_unnecessary_nullable_directive), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Nullable_directive_is_unnecessary), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer() - : base(IDEDiagnosticIds.RemoveUnnecessaryNullableDirectiveDiagnosticId, - EnforceOnBuildValues.RemoveUnnecessaryNullableDirective, - option: null, - fadingOption: null, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_unnecessary_nullable_directive), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Nullable_directive_is_unnecessary), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; - protected override void InitializeWorker(AnalysisContext context) - { - context.RegisterCompilationStartAction(AnalyzeCompilation); - } + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(AnalyzeCompilation); + } - private void AnalyzeCompilation(CompilationStartAnalysisContext context) - { - var analyzer = new AnalyzerImpl(this); - context.RegisterCodeBlockAction(analyzer.AnalyzeCodeBlock); - context.RegisterSemanticModelAction(analyzer.AnalyzeSemanticModel); - } + private void AnalyzeCompilation(CompilationStartAnalysisContext context) + { + var analyzer = new AnalyzerImpl(this); + context.RegisterCodeBlockAction(analyzer.AnalyzeCodeBlock); + context.RegisterSemanticModelAction(analyzer.AnalyzeSemanticModel); + } - /// - /// Determine if a code block is eligible for analysis by . - /// - /// The syntax node provided via . - /// if the code block should be analyzed by ; - /// otherwise, to skip analysis of the block. If a block is skipped, one or more child - /// blocks may be analyzed by , and any remaining spans can be analyzed by - /// . - private static bool IsIgnoredCodeBlock(SyntaxNode codeBlock) - { - // Avoid analysis of compilation units and types in AnalyzeCodeBlock. These nodes appear in code block - // callbacks when they include attributes, but analysis of the node at this level would block more efficient - // analysis of descendant members. - return codeBlock.Kind() is - SyntaxKind.CompilationUnit or - SyntaxKind.ClassDeclaration or - SyntaxKind.RecordDeclaration or - SyntaxKind.StructDeclaration or - SyntaxKind.RecordStructDeclaration or - SyntaxKind.InterfaceDeclaration or - SyntaxKind.DelegateDeclaration or - SyntaxKind.EnumDeclaration; - } + /// + /// Determine if a code block is eligible for analysis by . + /// + /// The syntax node provided via . + /// if the code block should be analyzed by ; + /// otherwise, to skip analysis of the block. If a block is skipped, one or more child + /// blocks may be analyzed by , and any remaining spans can be analyzed by + /// . + private static bool IsIgnoredCodeBlock(SyntaxNode codeBlock) + { + // Avoid analysis of compilation units and types in AnalyzeCodeBlock. These nodes appear in code block + // callbacks when they include attributes, but analysis of the node at this level would block more efficient + // analysis of descendant members. + return codeBlock.Kind() is + SyntaxKind.CompilationUnit or + SyntaxKind.ClassDeclaration or + SyntaxKind.RecordDeclaration or + SyntaxKind.StructDeclaration or + SyntaxKind.RecordStructDeclaration or + SyntaxKind.InterfaceDeclaration or + SyntaxKind.DelegateDeclaration or + SyntaxKind.EnumDeclaration; + } - private static bool IsReducing([NotNullWhen(true)] NullableContextOptions? oldOptions, [NotNullWhen(true)] NullableContextOptions? newOptions) - { - return oldOptions is { } oldOptionsValue - && newOptions is { } newOptionsValue - && newOptionsValue != oldOptionsValue - && (oldOptionsValue & newOptionsValue) == newOptionsValue; - } + private static bool IsReducing([NotNullWhen(true)] NullableContextOptions? oldOptions, [NotNullWhen(true)] NullableContextOptions? newOptions) + { + return oldOptions is { } oldOptionsValue + && newOptions is { } newOptionsValue + && newOptionsValue != oldOptionsValue + && (oldOptionsValue & newOptionsValue) == newOptionsValue; + } - private static ImmutableArray AnalyzeCodeBlock(CodeBlockAnalysisContext context, int positionOfFirstReducingNullableDirective) - { - using var simplifier = new NullableImpactingSpanWalker(context.SemanticModel, positionOfFirstReducingNullableDirective, ignoredSpans: null, context.CancellationToken); - simplifier.Visit(context.CodeBlock); - return simplifier.Spans; - } + private static ImmutableArray AnalyzeCodeBlock(CodeBlockAnalysisContext context, int positionOfFirstReducingNullableDirective) + { + using var simplifier = new NullableImpactingSpanWalker(context.SemanticModel, positionOfFirstReducingNullableDirective, ignoredSpans: null, context.CancellationToken); + simplifier.Visit(context.CodeBlock); + return simplifier.Spans; + } - private ImmutableArray AnalyzeSemanticModel(SemanticModelAnalysisContext context, int positionOfFirstReducingNullableDirective, TextSpanIntervalTree? codeBlockIntervalTree, TextSpanIntervalTree? possibleNullableImpactIntervalTree) - { - var root = context.SemanticModel.SyntaxTree.GetCompilationUnitRoot(context.CancellationToken); + private ImmutableArray AnalyzeSemanticModel(SemanticModelAnalysisContext context, int positionOfFirstReducingNullableDirective, TextSpanIntervalTree? codeBlockIntervalTree, TextSpanIntervalTree? possibleNullableImpactIntervalTree) + { + var root = context.SemanticModel.SyntaxTree.GetCompilationUnitRoot(context.CancellationToken); - using (var simplifier = new NullableImpactingSpanWalker(context.SemanticModel, positionOfFirstReducingNullableDirective, ignoredSpans: codeBlockIntervalTree, context.CancellationToken)) + using (var simplifier = new NullableImpactingSpanWalker(context.SemanticModel, positionOfFirstReducingNullableDirective, ignoredSpans: codeBlockIntervalTree, context.CancellationToken)) + { + simplifier.Visit(root); + possibleNullableImpactIntervalTree ??= new TextSpanIntervalTree(); + foreach (var interval in simplifier.Spans) { - simplifier.Visit(root); - possibleNullableImpactIntervalTree ??= new TextSpanIntervalTree(); - foreach (var interval in simplifier.Spans) - { - possibleNullableImpactIntervalTree.AddIntervalInPlace(interval); - } + possibleNullableImpactIntervalTree.AddIntervalInPlace(interval); } + } - using var diagnostics = TemporaryArray.Empty; + using var diagnostics = TemporaryArray.Empty; - var compilationOptions = ((CSharpCompilationOptions)context.SemanticModel.Compilation.Options).NullableContextOptions; + var compilationOptions = ((CSharpCompilationOptions)context.SemanticModel.Compilation.Options).NullableContextOptions; - NullableDirectiveTriviaSyntax? previousRetainedDirective = null; - NullableContextOptions? retainedOptions = compilationOptions; + NullableDirectiveTriviaSyntax? previousRetainedDirective = null; + NullableContextOptions? retainedOptions = compilationOptions; - NullableDirectiveTriviaSyntax? currentOptionsDirective = null; - var currentOptions = retainedOptions; + NullableDirectiveTriviaSyntax? currentOptionsDirective = null; + var currentOptions = retainedOptions; - for (var directive = root.GetFirstDirective(); directive is not null; directive = directive.GetNextDirective()) - { - context.CancellationToken.ThrowIfCancellationRequested(); + for (var directive = root.GetFirstDirective(); directive is not null; directive = directive.GetNextDirective()) + { + context.CancellationToken.ThrowIfCancellationRequested(); - if (directive is NullableDirectiveTriviaSyntax nullableDirectiveTrivia) + if (directive is NullableDirectiveTriviaSyntax nullableDirectiveTrivia) + { + // Once we reach a new directive, check to see if we can remove the previous directive + var removedCurrent = false; + if (IsReducing(retainedOptions, currentOptions)) { - // Once we reach a new directive, check to see if we can remove the previous directive - var removedCurrent = false; - if (IsReducing(retainedOptions, currentOptions)) - { - // We can't have found a reducing directive and not know which directive it was - Contract.ThrowIfNull(currentOptionsDirective); - - if (possibleNullableImpactIntervalTree is null - || !possibleNullableImpactIntervalTree.HasIntervalThatOverlapsWith(currentOptionsDirective.Span.End, nullableDirectiveTrivia.SpanStart - currentOptionsDirective.Span.End)) - { - diagnostics.Add(Diagnostic.Create(Descriptor, currentOptionsDirective.GetLocation())); - } - } + // We can't have found a reducing directive and not know which directive it was + Contract.ThrowIfNull(currentOptionsDirective); - if (!removedCurrent) + if (possibleNullableImpactIntervalTree is null + || !possibleNullableImpactIntervalTree.HasIntervalThatOverlapsWith(currentOptionsDirective.Span.End, nullableDirectiveTrivia.SpanStart - currentOptionsDirective.Span.End)) { - previousRetainedDirective = currentOptionsDirective; - retainedOptions = currentOptions; + diagnostics.Add(Diagnostic.Create(Descriptor, currentOptionsDirective.GetLocation())); } - - currentOptionsDirective = nullableDirectiveTrivia; - currentOptions = CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer.GetNullableContextOptions(compilationOptions, currentOptions, nullableDirectiveTrivia); } - else if (directive.Kind() is - SyntaxKind.IfDirectiveTrivia or - SyntaxKind.ElifDirectiveTrivia or - SyntaxKind.ElseDirectiveTrivia) + + if (!removedCurrent) { - possibleNullableImpactIntervalTree ??= new TextSpanIntervalTree(); - possibleNullableImpactIntervalTree.AddIntervalInPlace(directive.Span); + previousRetainedDirective = currentOptionsDirective; + retainedOptions = currentOptions; } - } - // Once we reach the end of the file, check to see if we can remove the last directive - if (IsReducing(retainedOptions, currentOptions)) + currentOptionsDirective = nullableDirectiveTrivia; + currentOptions = CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer.GetNullableContextOptions(compilationOptions, currentOptions, nullableDirectiveTrivia); + } + else if (directive.Kind() is + SyntaxKind.IfDirectiveTrivia or + SyntaxKind.ElifDirectiveTrivia or + SyntaxKind.ElseDirectiveTrivia) { - // We can't have found a reducing directive and not know which directive it was - Contract.ThrowIfNull(currentOptionsDirective); - - if (possibleNullableImpactIntervalTree is null - || !possibleNullableImpactIntervalTree.HasIntervalThatOverlapsWith(currentOptionsDirective.Span.End, root.Span.End - currentOptionsDirective.Span.End)) - { - diagnostics.Add(Diagnostic.Create(Descriptor, currentOptionsDirective.GetLocation())); - } + possibleNullableImpactIntervalTree ??= new TextSpanIntervalTree(); + possibleNullableImpactIntervalTree.AddIntervalInPlace(directive.Span); } - - return diagnostics.ToImmutableAndClear(); } - private sealed class SyntaxTreeState + // Once we reach the end of the file, check to see if we can remove the last directive + if (IsReducing(retainedOptions, currentOptions)) { - private SyntaxTreeState(bool completed, int? positionOfFirstReducingNullableDirective) + // We can't have found a reducing directive and not know which directive it was + Contract.ThrowIfNull(currentOptionsDirective); + + if (possibleNullableImpactIntervalTree is null + || !possibleNullableImpactIntervalTree.HasIntervalThatOverlapsWith(currentOptionsDirective.Span.End, root.Span.End - currentOptionsDirective.Span.End)) { - Completed = completed; - PositionOfFirstReducingNullableDirective = positionOfFirstReducingNullableDirective; - if (!completed) - { - IntervalTree = new TextSpanIntervalTree(); - PossibleNullableImpactIntervalTree = new TextSpanIntervalTree(); - } + diagnostics.Add(Diagnostic.Create(Descriptor, currentOptionsDirective.GetLocation())); } + } - [MemberNotNullWhen(false, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))] - public bool Completed { get; private set; } - public int? PositionOfFirstReducingNullableDirective { get; } - public TextSpanIntervalTree? IntervalTree { get; } - public TextSpanIntervalTree? PossibleNullableImpactIntervalTree { get; } + return diagnostics.ToImmutableAndClear(); + } - public static SyntaxTreeState Create(bool defaultCompleted, NullableContextOptions compilationOptions, SyntaxTree tree, CancellationToken cancellationToken) + private sealed class SyntaxTreeState + { + private SyntaxTreeState(bool completed, int? positionOfFirstReducingNullableDirective) + { + Completed = completed; + PositionOfFirstReducingNullableDirective = positionOfFirstReducingNullableDirective; + if (!completed) { - var root = tree.GetCompilationUnitRoot(cancellationToken); - - // This analyzer only needs to process syntax trees that contain at least one #nullable directive that - // reduces the nullable analysis scope. - int? positionOfFirstReducingNullableDirective = null; - - NullableContextOptions? currentOptions = compilationOptions; - for (var directive = root.GetFirstDirective(); directive is not null; directive = directive.GetNextDirective()) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (directive is NullableDirectiveTriviaSyntax nullableDirectiveTrivia) - { - var newOptions = CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer.GetNullableContextOptions(compilationOptions, currentOptions, nullableDirectiveTrivia); - if (IsReducing(currentOptions, newOptions)) - { - positionOfFirstReducingNullableDirective = directive.SpanStart; - break; - } - - currentOptions = newOptions; - } - } - - return new SyntaxTreeState(completed: defaultCompleted || positionOfFirstReducingNullableDirective is null, positionOfFirstReducingNullableDirective); + IntervalTree = new TextSpanIntervalTree(); + PossibleNullableImpactIntervalTree = new TextSpanIntervalTree(); } + } - [MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))] - public bool TryProceedWithInterval(TextSpan span) - => TryProceedOrReportNullableImpactingSpans(span, nullableImpactingSpans: null); + [MemberNotNullWhen(false, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))] + public bool Completed { get; private set; } + public int? PositionOfFirstReducingNullableDirective { get; } + public TextSpanIntervalTree? IntervalTree { get; } + public TextSpanIntervalTree? PossibleNullableImpactIntervalTree { get; } - [MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))] - public bool TryReportNullableImpactingSpans(TextSpan span, ImmutableArray nullableImpactingSpans) - => TryProceedOrReportNullableImpactingSpans(span, nullableImpactingSpans); + public static SyntaxTreeState Create(bool defaultCompleted, NullableContextOptions compilationOptions, SyntaxTree tree, CancellationToken cancellationToken) + { + var root = tree.GetCompilationUnitRoot(cancellationToken); + + // This analyzer only needs to process syntax trees that contain at least one #nullable directive that + // reduces the nullable analysis scope. + int? positionOfFirstReducingNullableDirective = null; - [MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))] - private bool TryProceedOrReportNullableImpactingSpans(TextSpan span, ImmutableArray? nullableImpactingSpans) + NullableContextOptions? currentOptions = compilationOptions; + for (var directive = root.GetFirstDirective(); directive is not null; directive = directive.GetNextDirective()) { - if (Completed) - return false; + cancellationToken.ThrowIfCancellationRequested(); - lock (this) + if (directive is NullableDirectiveTriviaSyntax nullableDirectiveTrivia) { - if (Completed) - return false; - - if (IntervalTree.HasIntervalThatOverlapsWith(span.Start, span.End)) - return false; - - if (nullableImpactingSpans is { } spans) + var newOptions = CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer.GetNullableContextOptions(compilationOptions, currentOptions, nullableDirectiveTrivia); + if (IsReducing(currentOptions, newOptions)) { - foreach (var nullableImpactingSpan in spans) - PossibleNullableImpactIntervalTree.AddIntervalInPlace(nullableImpactingSpan); + positionOfFirstReducingNullableDirective = directive.SpanStart; + break; } - return true; + currentOptions = newOptions; } } - internal void MarkComplete() + return new SyntaxTreeState(completed: defaultCompleted || positionOfFirstReducingNullableDirective is null, positionOfFirstReducingNullableDirective); + } + + [MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))] + public bool TryProceedWithInterval(TextSpan span) + => TryProceedOrReportNullableImpactingSpans(span, nullableImpactingSpans: null); + + [MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))] + public bool TryReportNullableImpactingSpans(TextSpan span, ImmutableArray nullableImpactingSpans) + => TryProceedOrReportNullableImpactingSpans(span, nullableImpactingSpans); + + [MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))] + private bool TryProceedOrReportNullableImpactingSpans(TextSpan span, ImmutableArray? nullableImpactingSpans) + { + if (Completed) + return false; + + lock (this) { if (Completed) - return; + return false; - lock (this) + if (IntervalTree.HasIntervalThatOverlapsWith(span.Start, span.End)) + return false; + + if (nullableImpactingSpans is { } spans) { - Completed = true; + foreach (var nullableImpactingSpan in spans) + PossibleNullableImpactIntervalTree.AddIntervalInPlace(nullableImpactingSpan); } + + return true; } } - private class AnalyzerImpl(CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer analyzer) + internal void MarkComplete() { - private readonly CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer _analyzer = analyzer; + if (Completed) + return; - /// - /// Tracks the analysis state of syntax trees in a compilation. - /// - private readonly ConcurrentDictionary _codeBlockIntervals = []; - - public void AnalyzeCodeBlock(CodeBlockAnalysisContext context) + lock (this) { - if (_analyzer.ShouldSkipAnalysis(context, notification: null) - || IsIgnoredCodeBlock(context.CodeBlock)) - { - return; - } + Completed = true; + } + } + } + + private class AnalyzerImpl(CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer analyzer) + { + private readonly CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer _analyzer = analyzer; + + /// + /// Tracks the analysis state of syntax trees in a compilation. + /// + private readonly ConcurrentDictionary _codeBlockIntervals = []; - var root = context.GetAnalysisRoot(findInTrivia: true); + public void AnalyzeCodeBlock(CodeBlockAnalysisContext context) + { + if (_analyzer.ShouldSkipAnalysis(context, notification: null) + || IsIgnoredCodeBlock(context.CodeBlock)) + { + return; + } - // Bail out if the root contains no nullable directives. - if (!root.ContainsDirective(SyntaxKind.NullableDirectiveTrivia)) - return; + var root = context.GetAnalysisRoot(findInTrivia: true); - var syntaxTreeState = GetOrCreateSyntaxTreeState(context.CodeBlock.SyntaxTree, defaultCompleted: false, context.SemanticModel, context.CancellationToken); - if (!syntaxTreeState.TryProceedWithInterval(context.CodeBlock.FullSpan)) - return; + // Bail out if the root contains no nullable directives. + if (!root.ContainsDirective(SyntaxKind.NullableDirectiveTrivia)) + return; - var nullableImpactingSpans = CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer.AnalyzeCodeBlock(context, syntaxTreeState.PositionOfFirstReducingNullableDirective.Value); + var syntaxTreeState = GetOrCreateSyntaxTreeState(context.CodeBlock.SyntaxTree, defaultCompleted: false, context.SemanticModel, context.CancellationToken); + if (!syntaxTreeState.TryProceedWithInterval(context.CodeBlock.FullSpan)) + return; - // After this point, cancellation is not allowed due to possible state alteration - syntaxTreeState.TryReportNullableImpactingSpans(context.CodeBlock.FullSpan, nullableImpactingSpans); - } + var nullableImpactingSpans = CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer.AnalyzeCodeBlock(context, syntaxTreeState.PositionOfFirstReducingNullableDirective.Value); - public void AnalyzeSemanticModel(SemanticModelAnalysisContext context) - { - if (_analyzer.ShouldSkipAnalysis(context, notification: null)) - return; + // After this point, cancellation is not allowed due to possible state alteration + syntaxTreeState.TryReportNullableImpactingSpans(context.CodeBlock.FullSpan, nullableImpactingSpans); + } - var root = context.GetAnalysisRoot(findInTrivia: true); + public void AnalyzeSemanticModel(SemanticModelAnalysisContext context) + { + if (_analyzer.ShouldSkipAnalysis(context, notification: null)) + return; - // Bail out if the root contains no nullable directives. - if (!root.ContainsDirective(SyntaxKind.NullableDirectiveTrivia)) - return; + var root = context.GetAnalysisRoot(findInTrivia: true); - // Get the state information for the syntax tree. If the state information is not available, it is - // initialized directly to a completed state, ensuring that concurrent (or future) calls to - // AnalyzeCodeBlock will always read completed==true, and intervalTree does not need to be initialized - // to a non-null value. - var syntaxTreeState = GetOrCreateSyntaxTreeState(context.SemanticModel.SyntaxTree, defaultCompleted: true, context.SemanticModel, context.CancellationToken); + // Bail out if the root contains no nullable directives. + if (!root.ContainsDirective(SyntaxKind.NullableDirectiveTrivia)) + return; - syntaxTreeState.MarkComplete(); + // Get the state information for the syntax tree. If the state information is not available, it is + // initialized directly to a completed state, ensuring that concurrent (or future) calls to + // AnalyzeCodeBlock will always read completed==true, and intervalTree does not need to be initialized + // to a non-null value. + var syntaxTreeState = GetOrCreateSyntaxTreeState(context.SemanticModel.SyntaxTree, defaultCompleted: true, context.SemanticModel, context.CancellationToken); - if (syntaxTreeState.PositionOfFirstReducingNullableDirective is not { } positionOfFirstReducingNullableDirective) - return; + syntaxTreeState.MarkComplete(); - var diagnostics = _analyzer.AnalyzeSemanticModel(context, positionOfFirstReducingNullableDirective, syntaxTreeState.IntervalTree, syntaxTreeState.PossibleNullableImpactIntervalTree); + if (syntaxTreeState.PositionOfFirstReducingNullableDirective is not { } positionOfFirstReducingNullableDirective) + return; - // After this point, cancellation is not allowed due to possible state alteration - foreach (var diagnostic in diagnostics) - { - context.ReportDiagnostic(diagnostic); - } - } + var diagnostics = _analyzer.AnalyzeSemanticModel(context, positionOfFirstReducingNullableDirective, syntaxTreeState.IntervalTree, syntaxTreeState.PossibleNullableImpactIntervalTree); - private SyntaxTreeState GetOrCreateSyntaxTreeState(SyntaxTree tree, bool defaultCompleted, SemanticModel semanticModel, CancellationToken cancellationToken) + // After this point, cancellation is not allowed due to possible state alteration + foreach (var diagnostic in diagnostics) { - return _codeBlockIntervals.GetOrAdd( - tree, - static (tree, arg) => SyntaxTreeState.Create(arg.defaultCompleted, arg.options, tree, arg.cancellationToken), - (defaultCompleted, options: ((CSharpCompilationOptions)semanticModel.Compilation.Options).NullableContextOptions, cancellationToken)); + context.ReportDiagnostic(diagnostic); } } + + private SyntaxTreeState GetOrCreateSyntaxTreeState(SyntaxTree tree, bool defaultCompleted, SemanticModel semanticModel, CancellationToken cancellationToken) + { + return _codeBlockIntervals.GetOrAdd( + tree, + static (tree, arg) => SyntaxTreeState.Create(arg.defaultCompleted, arg.options, tree, arg.cancellationToken), + (defaultCompleted, options: ((CSharpCompilationOptions)semanticModel.Compilation.Options).NullableContextOptions, cancellationToken)); + } } } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryNullableDirective/NullableImpactingSpanWalker.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryNullableDirective/NullableImpactingSpanWalker.cs index c190a0d8b9eac..5cc1092c91878 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryNullableDirective/NullableImpactingSpanWalker.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryNullableDirective/NullableImpactingSpanWalker.cs @@ -12,149 +12,148 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Analyzers.RemoveUnnecessaryNullableDirective +namespace Microsoft.CodeAnalysis.CSharp.Analyzers.RemoveUnnecessaryNullableDirective; + +internal sealed class NullableImpactingSpanWalker( + SemanticModel semanticModel, + int positionOfFirstReducingNullableDirective, + TextSpanIntervalTree? ignoredSpans, + CancellationToken cancellationToken) : CSharpSyntaxWalker(SyntaxWalkerDepth.StructuredTrivia), IDisposable { - internal sealed class NullableImpactingSpanWalker( - SemanticModel semanticModel, - int positionOfFirstReducingNullableDirective, - TextSpanIntervalTree? ignoredSpans, - CancellationToken cancellationToken) : CSharpSyntaxWalker(SyntaxWalkerDepth.StructuredTrivia), IDisposable - { - private readonly SemanticModel _semanticModel = semanticModel; - private readonly int _positionOfFirstReducingNullableDirective = positionOfFirstReducingNullableDirective; - private readonly TextSpanIntervalTree? _ignoredSpans = ignoredSpans; - private readonly CancellationToken _cancellationToken = cancellationToken; + private readonly SemanticModel _semanticModel = semanticModel; + private readonly int _positionOfFirstReducingNullableDirective = positionOfFirstReducingNullableDirective; + private readonly TextSpanIntervalTree? _ignoredSpans = ignoredSpans; + private readonly CancellationToken _cancellationToken = cancellationToken; - private ImmutableArray.Builder? _spans; + private ImmutableArray.Builder? _spans; - public bool HasSpans => _spans?.Count > 0; + public bool HasSpans => _spans?.Count > 0; - public ImmutableArray Spans => _spans?.ToImmutable() ?? []; + public ImmutableArray Spans => _spans?.ToImmutable() ?? []; - public ImmutableArray.Builder SpansBuilder + public ImmutableArray.Builder SpansBuilder + { + get { - get - { - if (_spans is null) - Interlocked.CompareExchange(ref _spans, ImmutableArray.CreateBuilder(), null); + if (_spans is null) + Interlocked.CompareExchange(ref _spans, ImmutableArray.CreateBuilder(), null); - return _spans; - } + return _spans; } + } - private bool IsIgnored(SyntaxNode node) - { - if (node.Span.End < _positionOfFirstReducingNullableDirective) - return true; + private bool IsIgnored(SyntaxNode node) + { + if (node.Span.End < _positionOfFirstReducingNullableDirective) + return true; - if (_ignoredSpans is not null) + if (_ignoredSpans is not null) + { + if (_ignoredSpans.HasIntervalThatContains(node.SpanStart, node.Span.Length)) { - if (_ignoredSpans.HasIntervalThatContains(node.SpanStart, node.Span.Length)) - { - return true; - } + return true; } + } + + return false; + } - return false; + private static bool IsLanguageRestrictedToNonNullForm(TypeSyntax node) + { + // Simplify syntax checks by walking up qualified names to an equivalent parent node. + node = WalkUpCurrentQualifiedName(node); + + if (node?.Parent is QualifiedNameSyntax qualifiedName + && qualifiedName.Left == node) + { + // Cannot dot off a nullable reference type + return true; } - private static bool IsLanguageRestrictedToNonNullForm(TypeSyntax node) + if (node.IsParentKind(SyntaxKind.UsingDirective)) { - // Simplify syntax checks by walking up qualified names to an equivalent parent node. - node = WalkUpCurrentQualifiedName(node); + // Using directives cannot directly reference a nullable reference type + return true; + } - if (node?.Parent is QualifiedNameSyntax qualifiedName - && qualifiedName.Left == node) - { - // Cannot dot off a nullable reference type - return true; - } + if (node.IsParentKind(SyntaxKind.SimpleBaseType)) + { + // Cannot derive directly from a nullable reference type + return true; + } - if (node.IsParentKind(SyntaxKind.UsingDirective)) - { - // Using directives cannot directly reference a nullable reference type - return true; - } + if (node?.Parent is BaseNamespaceDeclarationSyntax) + { + // Namespace names cannot be nullable reference types + return true; + } - if (node.IsParentKind(SyntaxKind.SimpleBaseType)) - { - // Cannot derive directly from a nullable reference type - return true; - } + if (node.IsParentKind(SyntaxKind.NameEquals) && node.Parent.IsParentKind(SyntaxKind.UsingDirective)) + { + // This is the alias or the target type of a using alias directive, neither of which can be nullable + // + // using CustomException = System.Exception; + // ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ + return true; + } - if (node?.Parent is BaseNamespaceDeclarationSyntax) - { - // Namespace names cannot be nullable reference types - return true; - } + return false; - if (node.IsParentKind(SyntaxKind.NameEquals) && node.Parent.IsParentKind(SyntaxKind.UsingDirective)) + // If this is Y in X.Y, walk up to X.Y + static TypeSyntax WalkUpCurrentQualifiedName(TypeSyntax node) + { + while (node.Parent is QualifiedNameSyntax qualifiedName + && qualifiedName.Right == node) { - // This is the alias or the target type of a using alias directive, neither of which can be nullable - // - // using CustomException = System.Exception; - // ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ - return true; + node = qualifiedName; } - return false; - - // If this is Y in X.Y, walk up to X.Y - static TypeSyntax WalkUpCurrentQualifiedName(TypeSyntax node) - { - while (node.Parent is QualifiedNameSyntax qualifiedName - && qualifiedName.Right == node) - { - node = qualifiedName; - } - - return node; - } + return node; } + } - public void Dispose() - { - } + public void Dispose() + { + } + + public override void DefaultVisit(SyntaxNode node) + { + if (IsIgnored(node)) + return; - public override void DefaultVisit(SyntaxNode node) + if (node is TypeSyntax typeSyntax + && !IsLanguageRestrictedToNonNullForm(typeSyntax)) { - if (IsIgnored(node)) + if (typeSyntax.IsVar) return; - if (node is TypeSyntax typeSyntax - && !IsLanguageRestrictedToNonNullForm(typeSyntax)) + if (typeSyntax is PredefinedTypeSyntax predefinedType + && CSharpSyntaxFacts.Instance.TryGetPredefinedType(predefinedType.Keyword, out var type)) { - if (typeSyntax.IsVar) - return; - - if (typeSyntax is PredefinedTypeSyntax predefinedType - && CSharpSyntaxFacts.Instance.TryGetPredefinedType(predefinedType.Keyword, out var type)) + if (type is CodeAnalysis.LanguageService.PredefinedType.Object or CodeAnalysis.LanguageService.PredefinedType.String) { - if (type is CodeAnalysis.LanguageService.PredefinedType.Object or CodeAnalysis.LanguageService.PredefinedType.String) - { - SpansBuilder.Add(predefinedType.Span); - } - - // All other predefined types are value types - return; + SpansBuilder.Add(predefinedType.Span); } - var symbolInfo = _semanticModel.GetSymbolInfo(typeSyntax, _cancellationToken); - if (symbolInfo.Symbol.IsKind(SymbolKind.Namespace)) - { - // Namespaces cannot be nullable - return; - } - else if (symbolInfo.Symbol is INamedTypeSymbol { IsValueType: true, IsGenericType: false }) - { - return; - } + // All other predefined types are value types + return; + } - SpansBuilder.Add(node.Span); + var symbolInfo = _semanticModel.GetSymbolInfo(typeSyntax, _cancellationToken); + if (symbolInfo.Symbol.IsKind(SymbolKind.Namespace)) + { + // Namespaces cannot be nullable + return; + } + else if (symbolInfo.Symbol is INamedTypeSymbol { IsValueType: true, IsGenericType: false }) + { return; } - base.DefaultVisit(node); + SpansBuilder.Add(node.Span); + return; } + + base.DefaultVisit(node); } } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryExpressionParenthesesDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryExpressionParenthesesDiagnosticAnalyzer.cs index 832c34fd0fd6d..7e75cfc1109e9 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryExpressionParenthesesDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryExpressionParenthesesDiagnosticAnalyzer.cs @@ -12,97 +12,96 @@ using Microsoft.CodeAnalysis.Precedence; using Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryParentheses +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryParentheses; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpRemoveUnnecessaryExpressionParenthesesDiagnosticAnalyzer + : AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpRemoveUnnecessaryExpressionParenthesesDiagnosticAnalyzer - : AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer - { - protected override SyntaxKind GetSyntaxKind() - => SyntaxKind.ParenthesizedExpression; + protected override SyntaxKind GetSyntaxKind() + => SyntaxKind.ParenthesizedExpression; + + protected override ISyntaxFacts GetSyntaxFacts() + => CSharpSyntaxFacts.Instance; - protected override ISyntaxFacts GetSyntaxFacts() - => CSharpSyntaxFacts.Instance; + protected override bool CanRemoveParentheses( + ParenthesizedExpressionSyntax parenthesizedExpression, + SemanticModel semanticModel, CancellationToken cancellationToken, + out PrecedenceKind precedence, out bool clarifiesPrecedence) + { + return CanRemoveParenthesesHelper( + parenthesizedExpression, semanticModel, cancellationToken, + out precedence, out clarifiesPrecedence); + } - protected override bool CanRemoveParentheses( - ParenthesizedExpressionSyntax parenthesizedExpression, - SemanticModel semanticModel, CancellationToken cancellationToken, - out PrecedenceKind precedence, out bool clarifiesPrecedence) + public static bool CanRemoveParenthesesHelper( + ParenthesizedExpressionSyntax parenthesizedExpression, SemanticModel semanticModel, CancellationToken cancellationToken, + out PrecedenceKind parentPrecedenceKind, out bool clarifiesPrecedence) + { + var result = parenthesizedExpression.CanRemoveParentheses(semanticModel, cancellationToken); + if (!result) { - return CanRemoveParenthesesHelper( - parenthesizedExpression, semanticModel, cancellationToken, - out precedence, out clarifiesPrecedence); + parentPrecedenceKind = default; + clarifiesPrecedence = false; + return false; } - public static bool CanRemoveParenthesesHelper( - ParenthesizedExpressionSyntax parenthesizedExpression, SemanticModel semanticModel, CancellationToken cancellationToken, - out PrecedenceKind parentPrecedenceKind, out bool clarifiesPrecedence) + var inner = parenthesizedExpression.Expression; + var innerPrecedence = inner.GetOperatorPrecedence(); + var innerIsSimple = innerPrecedence is OperatorPrecedence.Primary or + OperatorPrecedence.None; + + ExpressionSyntax parentExpression; + switch (parenthesizedExpression.Parent) { - var result = parenthesizedExpression.CanRemoveParentheses(semanticModel, cancellationToken); - if (!result) - { - parentPrecedenceKind = default; + case ConditionalExpressionSyntax _: + // If our parent is a conditional, then only remove parens if the inner + // expression is a primary. i.e. it's ok to remove any of the following: + // + // (a()) ? (b.length) : (c[0]) + // + // But we shouldn't remove parens for anything more complex like: + // + // ++a ? b + c : d << e + // + parentPrecedenceKind = PrecedenceKind.Other; clarifiesPrecedence = false; - return false; - } - - var inner = parenthesizedExpression.Expression; - var innerPrecedence = inner.GetOperatorPrecedence(); - var innerIsSimple = innerPrecedence is OperatorPrecedence.Primary or - OperatorPrecedence.None; - - ExpressionSyntax parentExpression; - switch (parenthesizedExpression.Parent) - { - case ConditionalExpressionSyntax _: - // If our parent is a conditional, then only remove parens if the inner - // expression is a primary. i.e. it's ok to remove any of the following: - // - // (a()) ? (b.length) : (c[0]) - // - // But we shouldn't remove parens for anything more complex like: - // - // ++a ? b + c : d << e - // - parentPrecedenceKind = PrecedenceKind.Other; - clarifiesPrecedence = false; - return innerIsSimple; + return innerIsSimple; - case BinaryExpressionSyntax binaryExpression: - parentExpression = binaryExpression; - break; + case BinaryExpressionSyntax binaryExpression: + parentExpression = binaryExpression; + break; - case IsPatternExpressionSyntax isPatternExpression: - // on the left side of an 'x is pat' expression - parentExpression = isPatternExpression; - break; + case IsPatternExpressionSyntax isPatternExpression: + // on the left side of an 'x is pat' expression + parentExpression = isPatternExpression; + break; - case ConstantPatternSyntax constantPattern when constantPattern.Parent is IsPatternExpressionSyntax isPatternExpression: - // on the right side of an 'x is const_pattern' expression - parentExpression = isPatternExpression; - break; + case ConstantPatternSyntax constantPattern when constantPattern.Parent is IsPatternExpressionSyntax isPatternExpression: + // on the right side of an 'x is const_pattern' expression + parentExpression = isPatternExpression; + break; - default: - parentPrecedenceKind = PrecedenceKind.Other; - clarifiesPrecedence = false; - return true; - } + default: + parentPrecedenceKind = PrecedenceKind.Other; + clarifiesPrecedence = false; + return true; + } - // We're parented by something binary-like. - parentPrecedenceKind = CSharpExpressionPrecedenceService.Instance.GetPrecedenceKind(parentExpression); + // We're parented by something binary-like. + parentPrecedenceKind = CSharpExpressionPrecedenceService.Instance.GetPrecedenceKind(parentExpression); - // Precedence is clarified any time we have expression with different precedence - // (and the inner expression is not a primary expression). in other words, this - // is helps clarify precedence: - // - // a + (b * c) - // - // However, this does not: - // - // a + (b.Length) - clarifiesPrecedence = !innerIsSimple && - parentExpression.GetOperatorPrecedence() != innerPrecedence; - return true; - } + // Precedence is clarified any time we have expression with different precedence + // (and the inner expression is not a primary expression). in other words, this + // is helps clarify precedence: + // + // a + (b * c) + // + // However, this does not: + // + // a + (b.Length) + clarifiesPrecedence = !innerIsSimple && + parentExpression.GetOperatorPrecedence() != innerPrecedence; + return true; } } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryPatternParenthesesDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryPatternParenthesesDiagnosticAnalyzer.cs index 52c75eb367fb7..3fd91e1fadc4e 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryPatternParenthesesDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryPatternParenthesesDiagnosticAnalyzer.cs @@ -12,75 +12,74 @@ using Microsoft.CodeAnalysis.Precedence; using Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryParentheses +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryParentheses; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpRemoveUnnecessaryPatternParenthesesDiagnosticAnalyzer + : AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpRemoveUnnecessaryPatternParenthesesDiagnosticAnalyzer - : AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer - { - protected override SyntaxKind GetSyntaxKind() - => SyntaxKind.ParenthesizedPattern; + protected override SyntaxKind GetSyntaxKind() + => SyntaxKind.ParenthesizedPattern; - protected override ISyntaxFacts GetSyntaxFacts() - => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts GetSyntaxFacts() + => CSharpSyntaxFacts.Instance; - protected override bool CanRemoveParentheses( - ParenthesizedPatternSyntax parenthesizedExpression, - SemanticModel semanticModel, CancellationToken cancellationToken, - out PrecedenceKind precedence, out bool clarifiesPrecedence) - { - return CanRemoveParenthesesHelper(parenthesizedExpression, out precedence, out clarifiesPrecedence); - } + protected override bool CanRemoveParentheses( + ParenthesizedPatternSyntax parenthesizedExpression, + SemanticModel semanticModel, CancellationToken cancellationToken, + out PrecedenceKind precedence, out bool clarifiesPrecedence) + { + return CanRemoveParenthesesHelper(parenthesizedExpression, out precedence, out clarifiesPrecedence); + } - public static bool CanRemoveParenthesesHelper( - ParenthesizedPatternSyntax parenthesizedPattern, out PrecedenceKind parentPrecedenceKind, out bool clarifiesPrecedence) + public static bool CanRemoveParenthesesHelper( + ParenthesizedPatternSyntax parenthesizedPattern, out PrecedenceKind parentPrecedenceKind, out bool clarifiesPrecedence) + { + var result = parenthesizedPattern.CanRemoveParentheses(); + if (!result) { - var result = parenthesizedPattern.CanRemoveParentheses(); - if (!result) - { - parentPrecedenceKind = default; - clarifiesPrecedence = false; - return false; - } - - var inner = parenthesizedPattern.Pattern; - var innerPrecedence = inner.GetOperatorPrecedence(); - var innerIsSimple = innerPrecedence is OperatorPrecedence.Primary or - OperatorPrecedence.None; - - if (parenthesizedPattern.Parent is not PatternSyntax) - { - // We're parented by something not a pattern. i.e. `x is (...)` or `case (...)`. - // These parentheses are never needed for clarity and can always be removed. - parentPrecedenceKind = PrecedenceKind.Other; - clarifiesPrecedence = false; - return true; - } + parentPrecedenceKind = default; + clarifiesPrecedence = false; + return false; + } - if (parenthesizedPattern.Parent is not BinaryPatternSyntax parentPattern) - { - // We're parented by something other than a BinaryPattern. These parentheses are never needed for - // clarity and can always be removed. - parentPrecedenceKind = PrecedenceKind.Other; - clarifiesPrecedence = false; - return true; - } + var inner = parenthesizedPattern.Pattern; + var innerPrecedence = inner.GetOperatorPrecedence(); + var innerIsSimple = innerPrecedence is OperatorPrecedence.Primary or + OperatorPrecedence.None; - // We're parented by something binary-like. - parentPrecedenceKind = CSharpPatternPrecedenceService.Instance.GetPrecedenceKind(parentPattern); + if (parenthesizedPattern.Parent is not PatternSyntax) + { + // We're parented by something not a pattern. i.e. `x is (...)` or `case (...)`. + // These parentheses are never needed for clarity and can always be removed. + parentPrecedenceKind = PrecedenceKind.Other; + clarifiesPrecedence = false; + return true; + } - // Precedence is clarified any time we have expression with different precedence - // (and the inner expression is not a primary expression). in other words, this - // is helps clarify precedence: - // - // a or (b and c) - // - // However, this does not: - // - // a or (b) - clarifiesPrecedence = !innerIsSimple && - parentPattern.GetOperatorPrecedence() != innerPrecedence; + if (parenthesizedPattern.Parent is not BinaryPatternSyntax parentPattern) + { + // We're parented by something other than a BinaryPattern. These parentheses are never needed for + // clarity and can always be removed. + parentPrecedenceKind = PrecedenceKind.Other; + clarifiesPrecedence = false; return true; } + + // We're parented by something binary-like. + parentPrecedenceKind = CSharpPatternPrecedenceService.Instance.GetPrecedenceKind(parentPattern); + + // Precedence is clarified any time we have expression with different precedence + // (and the inner expression is not a primary expression). in other words, this + // is helps clarify precedence: + // + // a or (b and c) + // + // However, this does not: + // + // a or (b) + clarifiesPrecedence = !innerIsSimple && + parentPattern.GetOperatorPrecedence() != innerPrecedence; + return true; } } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer.cs index da5897017be44..f7d161c8a19aa 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer.cs @@ -5,29 +5,28 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessarySuppressions +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessarySuppressions; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer + : AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer - : AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer + protected override void RegisterAttributeSyntaxAction(CompilationStartAnalysisContext context, CompilationAnalyzer compilationAnalyzer) { - protected override void RegisterAttributeSyntaxAction(CompilationStartAnalysisContext context, CompilationAnalyzer compilationAnalyzer) + context.RegisterSyntaxNodeAction(context => { - context.RegisterSyntaxNodeAction(context => + var attributeList = (AttributeListSyntax)context.Node; + switch (attributeList.Target?.Identifier.Kind()) { - var attributeList = (AttributeListSyntax)context.Node; - switch (attributeList.Target?.Identifier.Kind()) - { - case SyntaxKind.AssemblyKeyword: - case SyntaxKind.ModuleKeyword: - foreach (var attribute in attributeList.Attributes) - { - compilationAnalyzer.AnalyzeAssemblyOrModuleAttribute(attribute, context.SemanticModel, context.ReportDiagnostic, context.CancellationToken); - } + case SyntaxKind.AssemblyKeyword: + case SyntaxKind.ModuleKeyword: + foreach (var attribute in attributeList.Attributes) + { + compilationAnalyzer.AnalyzeAssemblyOrModuleAttribute(attribute, context.SemanticModel, context.ReportDiagnostic, context.CancellationToken); + } - break; - } - }, SyntaxKind.AttributeList); - } + break; + } + }, SyntaxKind.AttributeList); } } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs index a2a594b8f9f08..8e412e9bfd506 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs @@ -8,21 +8,20 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessarySuppressions -{ +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessarySuppressions; + #if !CODE_STYLE // Not exported in CodeStyle layer: https://github.com/dotnet/roslyn/issues/47942 - [DiagnosticAnalyzer(LanguageNames.CSharp)] +[DiagnosticAnalyzer(LanguageNames.CSharp)] #endif - internal sealed class CSharpRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer - : AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer - { - protected override string CompilerErrorCodePrefix => "CS"; - protected override int CompilerErrorCodeDigitCount => 4; - protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; - protected override ISemanticFacts SemanticFacts => CSharpSemanticFacts.Instance; - protected override (Assembly assembly, string typeName) GetCompilerDiagnosticAnalyzerInfo() - => (typeof(SyntaxKind).Assembly, CompilerDiagnosticAnalyzerNames.CSharpCompilerAnalyzerTypeName); - protected override bool ContainsPragmaDirective(SyntaxNode root) - => root.ContainsDirective(SyntaxKind.PragmaWarningDirectiveTrivia); - } +internal sealed class CSharpRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer + : AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer +{ + protected override string CompilerErrorCodePrefix => "CS"; + protected override int CompilerErrorCodeDigitCount => 4; + protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; + protected override ISemanticFacts SemanticFacts => CSharpSemanticFacts.Instance; + protected override (Assembly assembly, string typeName) GetCompilerDiagnosticAnalyzerInfo() + => (typeof(SyntaxKind).Assembly, CompilerDiagnosticAnalyzerNames.CSharpCompilerAnalyzerTypeName); + protected override bool ContainsPragmaDirective(SyntaxNode root) + => root.ContainsDirective(SyntaxKind.PragmaWarningDirectiveTrivia); } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnreachableCode/CSharpRemoveUnreachableCodeDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnreachableCode/CSharpRemoveUnreachableCodeDiagnosticAnalyzer.cs index 0399a28368ea6..d6ca179cb1e46 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnreachableCode/CSharpRemoveUnreachableCodeDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnreachableCode/CSharpRemoveUnreachableCodeDiagnosticAnalyzer.cs @@ -124,6 +124,7 @@ private void ProcessUnreachableDiagnostic( Descriptor, firstStatementLocation, NotificationOption2.ForSeverity(Descriptor.DefaultSeverity), + context.Options, additionalLocations: [], additionalUnnecessaryLocations: additionalLocations)); @@ -142,6 +143,7 @@ private void ProcessUnreachableDiagnostic( Descriptor, location, NotificationOption2.ForSeverity(Descriptor.DefaultSeverity), + context.Options, additionalLocations, additionalUnnecessaryLocations, s_subsequentSectionProperties)); diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnreachableCode/RemoveUnreachableCodeHelpers.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnreachableCode/RemoveUnreachableCodeHelpers.cs index cbccfa9308476..7e6162c613e82 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnreachableCode/RemoveUnreachableCodeHelpers.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnreachableCode/RemoveUnreachableCodeHelpers.cs @@ -8,100 +8,99 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnreachableCode +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnreachableCode; + +internal static class RemoveUnreachableCodeHelpers { - internal static class RemoveUnreachableCodeHelpers + public static ImmutableArray> GetSubsequentUnreachableSections(StatementSyntax firstUnreachableStatement) { - public static ImmutableArray> GetSubsequentUnreachableSections(StatementSyntax firstUnreachableStatement) + ImmutableArray siblingStatements; + switch (firstUnreachableStatement.Parent) { - ImmutableArray siblingStatements; - switch (firstUnreachableStatement.Parent) - { - case BlockSyntax block: - siblingStatements = ImmutableArray.CreateRange(block.Statements); - break; + case BlockSyntax block: + siblingStatements = ImmutableArray.CreateRange(block.Statements); + break; - case SwitchSectionSyntax switchSection: - siblingStatements = ImmutableArray.CreateRange(switchSection.Statements); - break; + case SwitchSectionSyntax switchSection: + siblingStatements = ImmutableArray.CreateRange(switchSection.Statements); + break; - case GlobalStatementSyntax globalStatement: - if (globalStatement.Parent is not CompilationUnitSyntax compilationUnit) - { - return []; - } + case GlobalStatementSyntax globalStatement: + if (globalStatement.Parent is not CompilationUnitSyntax compilationUnit) + { + return []; + } + { + // Can't use `SyntaxList` here since the retrieving the added node will result + // in a different reference. + using var _ = ArrayBuilder.GetInstance(out var builder); + foreach (var member in compilationUnit.Members) { - // Can't use `SyntaxList` here since the retrieving the added node will result - // in a different reference. - using var _ = ArrayBuilder.GetInstance(out var builder); - foreach (var member in compilationUnit.Members) + if (member is not GlobalStatementSyntax currentGlobalStatement) { - if (member is not GlobalStatementSyntax currentGlobalStatement) - { - continue; - } - - builder.Add(currentGlobalStatement.Statement); + continue; } - siblingStatements = builder.ToImmutable(); + builder.Add(currentGlobalStatement.Statement); } - break; + siblingStatements = builder.ToImmutable(); + } - default: - // We're an embedded statement. So the unreachable section is just us. - return []; - } + break; - var sections = ArrayBuilder>.GetInstance(); + default: + // We're an embedded statement. So the unreachable section is just us. + return []; + } - var currentSection = ArrayBuilder.GetInstance(); - var firstUnreachableStatementIndex = siblingStatements.IndexOf(firstUnreachableStatement); + var sections = ArrayBuilder>.GetInstance(); - for (int i = firstUnreachableStatementIndex + 1, n = siblingStatements.Length; i < n; i++) - { - var currentStatement = siblingStatements[i]; - if (currentStatement.IsKind(SyntaxKind.LabeledStatement)) - { - // In the case of a subsequent labeled statement, we don't want to consider it - // unreachable as there may be a 'goto' somewhere else to that label. If the - // compiler actually thinks that label is unreachable, it will give an diagnostic - // on that label itself and we can use that diagnostic to handle it and any - // subsequent sections. - break; - } - - if (currentStatement.IsKind(SyntaxKind.LocalFunctionStatement)) - { - // In the case of local functions, it is legal for a local function to be declared - // in code that is otherwise unreachable. It can still be called elsewhere. If - // the local function itself is not called, there will be a particular diagnostic - // for that ("The variable XXX is declared but never used") and the user can choose - // if they want to remove it or not. - var section = currentSection.ToImmutableAndFree(); - AddIfNonEmpty(sections, section); - - currentSection = ArrayBuilder.GetInstance(); - continue; - } + var currentSection = ArrayBuilder.GetInstance(); + var firstUnreachableStatementIndex = siblingStatements.IndexOf(firstUnreachableStatement); - currentSection.Add(currentStatement); + for (int i = firstUnreachableStatementIndex + 1, n = siblingStatements.Length; i < n; i++) + { + var currentStatement = siblingStatements[i]; + if (currentStatement.IsKind(SyntaxKind.LabeledStatement)) + { + // In the case of a subsequent labeled statement, we don't want to consider it + // unreachable as there may be a 'goto' somewhere else to that label. If the + // compiler actually thinks that label is unreachable, it will give an diagnostic + // on that label itself and we can use that diagnostic to handle it and any + // subsequent sections. + break; } - var lastSection = currentSection.ToImmutableAndFree(); - AddIfNonEmpty(sections, lastSection); + if (currentStatement.IsKind(SyntaxKind.LocalFunctionStatement)) + { + // In the case of local functions, it is legal for a local function to be declared + // in code that is otherwise unreachable. It can still be called elsewhere. If + // the local function itself is not called, there will be a particular diagnostic + // for that ("The variable XXX is declared but never used") and the user can choose + // if they want to remove it or not. + var section = currentSection.ToImmutableAndFree(); + AddIfNonEmpty(sections, section); + + currentSection = ArrayBuilder.GetInstance(); + continue; + } - return sections.ToImmutableAndFree(); + currentSection.Add(currentStatement); } - private static void AddIfNonEmpty(ArrayBuilder> sections, ImmutableArray lastSection) + var lastSection = currentSection.ToImmutableAndFree(); + AddIfNonEmpty(sections, lastSection); + + return sections.ToImmutableAndFree(); + } + + private static void AddIfNonEmpty(ArrayBuilder> sections, ImmutableArray lastSection) + { + if (!lastSection.IsEmpty) { - if (!lastSection.IsEmpty) - { - sections.Add(lastSection); - } + sections.Add(lastSection); } } } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs index d64dc8753d576..2bb616b174829 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs @@ -9,24 +9,23 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.RemoveUnusedMembers; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedMembers +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedMembers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpRemoveUnusedMembersDiagnosticAnalyzer + : AbstractRemoveUnusedMembersDiagnosticAnalyzer< + DocumentationCommentTriviaSyntax, + IdentifierNameSyntax, + TypeDeclarationSyntax, + MemberDeclarationSyntax> { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpRemoveUnusedMembersDiagnosticAnalyzer - : AbstractRemoveUnusedMembersDiagnosticAnalyzer< - DocumentationCommentTriviaSyntax, - IdentifierNameSyntax, - TypeDeclarationSyntax, - MemberDeclarationSyntax> + protected override IEnumerable GetTypeDeclarations(INamedTypeSymbol namedType, CancellationToken cancellationToken) { - protected override IEnumerable GetTypeDeclarations(INamedTypeSymbol namedType, CancellationToken cancellationToken) - { - return namedType.DeclaringSyntaxReferences - .Select(r => r.GetSyntax(cancellationToken)) - .OfType(); - } - - protected override SyntaxList GetMembers(TypeDeclarationSyntax typeDeclaration) - => typeDeclaration.Members; + return namedType.DeclaringSyntaxReferences + .Select(r => r.GetSyntax(cancellationToken)) + .OfType(); } + + protected override SyntaxList GetMembers(TypeDeclarationSyntax typeDeclaration) + => typeDeclaration.Members; } diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnusedParametersAndValues/CSharpRemoveUnusedParametersAndValuesDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnusedParametersAndValues/CSharpRemoveUnusedParametersAndValuesDiagnosticAnalyzer.cs index b5831f3f2a73f..be757787c32e5 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnusedParametersAndValues/CSharpRemoveUnusedParametersAndValuesDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnusedParametersAndValues/CSharpRemoveUnusedParametersAndValuesDiagnosticAnalyzer.cs @@ -13,101 +13,100 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedParametersAndValues +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedParametersAndValues; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer + public CSharpRemoveUnusedParametersAndValuesDiagnosticAnalyzer() + : base(unusedValueExpressionStatementOption: CSharpCodeStyleOptions.UnusedValueExpressionStatement, + unusedValueAssignmentOption: CSharpCodeStyleOptions.UnusedValueAssignment) { - public CSharpRemoveUnusedParametersAndValuesDiagnosticAnalyzer() - : base(unusedValueExpressionStatementOption: CSharpCodeStyleOptions.UnusedValueExpressionStatement, - unusedValueAssignmentOption: CSharpCodeStyleOptions.UnusedValueAssignment) - { - } + } - protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; - protected override bool SupportsDiscard(SyntaxTree tree) - => tree.Options.LanguageVersion() >= LanguageVersion.CSharp7; + protected override bool SupportsDiscard(SyntaxTree tree) + => tree.Options.LanguageVersion() >= LanguageVersion.CSharp7; - protected override bool MethodHasHandlesClause(IMethodSymbol method) - => false; + protected override bool MethodHasHandlesClause(IMethodSymbol method) + => false; - protected override bool IsIfConditionalDirective(SyntaxNode node) - => node is IfDirectiveTriviaSyntax; + protected override bool IsIfConditionalDirective(SyntaxNode node) + => node is IfDirectiveTriviaSyntax; - protected override bool ReturnsThrow(SyntaxNode node) + protected override bool ReturnsThrow(SyntaxNode node) + { + if (node is not BaseMethodDeclarationSyntax methodSyntax) { - if (node is not BaseMethodDeclarationSyntax methodSyntax) - { - return false; - } - - if (methodSyntax.ExpressionBody is not null) - { - return methodSyntax.ExpressionBody.Expression is ThrowExpressionSyntax; - } + return false; + } - return methodSyntax.Body is { Statements: [ThrowStatementSyntax] }; + if (methodSyntax.ExpressionBody is not null) + { + return methodSyntax.ExpressionBody.Expression is ThrowExpressionSyntax; } - protected override CodeStyleOption2 GetUnusedValueExpressionStatementOption(AnalyzerOptionsProvider provider) - => ((CSharpAnalyzerOptionsProvider)provider).UnusedValueExpressionStatement; + return methodSyntax.Body is { Statements: [ThrowStatementSyntax] }; + } - protected override CodeStyleOption2 GetUnusedValueAssignmentOption(AnalyzerOptionsProvider provider) - => ((CSharpAnalyzerOptionsProvider)provider).UnusedValueAssignment; + protected override CodeStyleOption2 GetUnusedValueExpressionStatementOption(AnalyzerOptionsProvider provider) + => ((CSharpAnalyzerOptionsProvider)provider).UnusedValueExpressionStatement; - protected override bool ShouldBailOutFromRemovableAssignmentAnalysis(IOperation unusedSymbolWriteOperation) - { - // We don't want to recommend removing the write operation if it is within a statement - // that is not parented by an explicit block with curly braces. - // For example, assignment to 'x' in 'if (...) x = 1;' - // Replacing 'x = 1' with an empty statement ';' is not useful, and user is most likely - // going to remove the entire if statement in this case. However, we don't - // want to suggest removing the entire if statement as that might lead to change of semantics. - // So, we conservatively bail out from removable assignment analysis for such cases. - - var statementAncestor = unusedSymbolWriteOperation.Syntax.FirstAncestorOrSelf()?.Parent; - return statementAncestor is not (BlockSyntax or SwitchSectionSyntax); - } + protected override CodeStyleOption2 GetUnusedValueAssignmentOption(AnalyzerOptionsProvider provider) + => ((CSharpAnalyzerOptionsProvider)provider).UnusedValueAssignment; - // C# does not have an explicit "call" statement syntax for invocations with explicit value discard. - protected override bool IsCallStatement(IExpressionStatementOperation expressionStatement) - => false; + protected override bool ShouldBailOutFromRemovableAssignmentAnalysis(IOperation unusedSymbolWriteOperation) + { + // We don't want to recommend removing the write operation if it is within a statement + // that is not parented by an explicit block with curly braces. + // For example, assignment to 'x' in 'if (...) x = 1;' + // Replacing 'x = 1' with an empty statement ';' is not useful, and user is most likely + // going to remove the entire if statement in this case. However, we don't + // want to suggest removing the entire if statement as that might lead to change of semantics. + // So, we conservatively bail out from removable assignment analysis for such cases. + + var statementAncestor = unusedSymbolWriteOperation.Syntax.FirstAncestorOrSelf()?.Parent; + return statementAncestor is not (BlockSyntax or SwitchSectionSyntax); + } + + // C# does not have an explicit "call" statement syntax for invocations with explicit value discard. + protected override bool IsCallStatement(IExpressionStatementOperation expressionStatement) + => false; - protected override bool IsExpressionOfExpressionBody(IExpressionStatementOperation expressionStatementOperation) - => expressionStatementOperation.Parent is IBlockOperation blockOperation && - !blockOperation.Syntax.IsKind(SyntaxKind.Block); + protected override bool IsExpressionOfExpressionBody(IExpressionStatementOperation expressionStatementOperation) + => expressionStatementOperation.Parent is IBlockOperation blockOperation && + !blockOperation.Syntax.IsKind(SyntaxKind.Block); - protected override Location GetDefinitionLocationToFade(IOperation unusedDefinition) + protected override Location GetDefinitionLocationToFade(IOperation unusedDefinition) + { + switch (unusedDefinition.Syntax) { - switch (unusedDefinition.Syntax) - { - case VariableDeclaratorSyntax variableDeclarator: - return variableDeclarator.Identifier.GetLocation(); - - case DeclarationPatternSyntax declarationPattern: - return declarationPattern.Designation.GetLocation(); - - case RecursivePatternSyntax recursivePattern: - Debug.Assert(recursivePattern.Designation is not null, "If we got to this point variable designation cannot be null"); - return recursivePattern.Designation!.GetLocation(); - - case ListPatternSyntax listPattern: - Debug.Assert(listPattern.Designation is not null, "If we got to this point variable designation cannot be null"); - return listPattern.Designation!.GetLocation(); - - default: - // C# syntax node for foreach statement has no syntax node for the loop control variable declaration, - // so the operation tree has an IVariableDeclaratorOperation with the syntax mapped to the type node syntax instead of variable declarator syntax. - // Check if the unused definition syntax is the foreach statement's type node. - if (unusedDefinition.Syntax.Parent is ForEachStatementSyntax forEachStatement && - forEachStatement.Type == unusedDefinition.Syntax) - { - return forEachStatement.Identifier.GetLocation(); - } - - return unusedDefinition.Syntax.GetLocation(); - } + case VariableDeclaratorSyntax variableDeclarator: + return variableDeclarator.Identifier.GetLocation(); + + case DeclarationPatternSyntax declarationPattern: + return declarationPattern.Designation.GetLocation(); + + case RecursivePatternSyntax recursivePattern: + Debug.Assert(recursivePattern.Designation is not null, "If we got to this point variable designation cannot be null"); + return recursivePattern.Designation!.GetLocation(); + + case ListPatternSyntax listPattern: + Debug.Assert(listPattern.Designation is not null, "If we got to this point variable designation cannot be null"); + return listPattern.Designation!.GetLocation(); + + default: + // C# syntax node for foreach statement has no syntax node for the loop control variable declaration, + // so the operation tree has an IVariableDeclaratorOperation with the syntax mapped to the type node syntax instead of variable declarator syntax. + // Check if the unused definition syntax is the foreach statement's type node. + if (unusedDefinition.Syntax.Parent is ForEachStatementSyntax forEachStatement && + forEachStatement.Type == unusedDefinition.Syntax) + { + return forEachStatement.Identifier.GetLocation(); + } + + return unusedDefinition.Syntax.GetLocation(); } } } diff --git a/src/Analyzers/CSharp/Analyzers/SimplifyBooleanExpression/CSharpSimplifyConditionalDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/SimplifyBooleanExpression/CSharpSimplifyConditionalDiagnosticAnalyzer.cs index 73bce14a8be8a..8208e5c02c88d 100644 --- a/src/Analyzers/CSharp/Analyzers/SimplifyBooleanExpression/CSharpSimplifyConditionalDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/SimplifyBooleanExpression/CSharpSimplifyConditionalDiagnosticAnalyzer.cs @@ -10,19 +10,18 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.SimplifyBooleanExpression; -namespace Microsoft.CodeAnalysis.CSharp.SimplifyBooleanExpression +namespace Microsoft.CodeAnalysis.CSharp.SimplifyBooleanExpression; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpSimplifyConditionalDiagnosticAnalyzer : + AbstractSimplifyConditionalDiagnosticAnalyzer< + SyntaxKind, + ExpressionSyntax, + ConditionalExpressionSyntax> { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpSimplifyConditionalDiagnosticAnalyzer : - AbstractSimplifyConditionalDiagnosticAnalyzer< - SyntaxKind, - ExpressionSyntax, - ConditionalExpressionSyntax> - { - protected override ISyntaxFacts SyntaxFacts - => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts SyntaxFacts + => CSharpSyntaxFacts.Instance; - protected override CommonConversion GetConversion(SemanticModel semanticModel, ExpressionSyntax node, CancellationToken cancellationToken) - => semanticModel.GetConversion(node, cancellationToken).ToCommonConversion(); - } + protected override CommonConversion GetConversion(SemanticModel semanticModel, ExpressionSyntax node, CancellationToken cancellationToken) + => semanticModel.GetConversion(node, cancellationToken).ToCommonConversion(); } diff --git a/src/Analyzers/CSharp/Analyzers/SimplifyInterpolation/CSharpSimplifyInterpolationDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/SimplifyInterpolation/CSharpSimplifyInterpolationDiagnosticAnalyzer.cs index 44a39dfdd463f..87984409aeaef 100644 --- a/src/Analyzers/CSharp/Analyzers/SimplifyInterpolation/CSharpSimplifyInterpolationDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/SimplifyInterpolation/CSharpSimplifyInterpolationDiagnosticAnalyzer.cs @@ -11,18 +11,17 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.SimplifyInterpolation; -namespace Microsoft.CodeAnalysis.CSharp.SimplifyInterpolation +namespace Microsoft.CodeAnalysis.CSharp.SimplifyInterpolation; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpSimplifyInterpolationDiagnosticAnalyzer : AbstractSimplifyInterpolationDiagnosticAnalyzer< + InterpolationSyntax, ExpressionSyntax> { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpSimplifyInterpolationDiagnosticAnalyzer : AbstractSimplifyInterpolationDiagnosticAnalyzer< - InterpolationSyntax, ExpressionSyntax> - { - protected override IVirtualCharService GetVirtualCharService() - => CSharpVirtualCharService.Instance; + protected override IVirtualCharService GetVirtualCharService() + => CSharpVirtualCharService.Instance; - protected override ISyntaxFacts GetSyntaxFacts() - => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts GetSyntaxFacts() + => CSharpSyntaxFacts.Instance; - protected override AbstractSimplifyInterpolationHelpers GetHelpers() => CSharpSimplifyInterpolationHelpers.Instance; - } + protected override AbstractSimplifyInterpolationHelpers GetHelpers() => CSharpSimplifyInterpolationHelpers.Instance; } diff --git a/src/Analyzers/CSharp/Analyzers/SimplifyInterpolation/CSharpSimplifyInterpolationHelpers.cs b/src/Analyzers/CSharp/Analyzers/SimplifyInterpolation/CSharpSimplifyInterpolationHelpers.cs index 11f479e03f0b4..d7eb94abecf58 100644 --- a/src/Analyzers/CSharp/Analyzers/SimplifyInterpolation/CSharpSimplifyInterpolationHelpers.cs +++ b/src/Analyzers/CSharp/Analyzers/SimplifyInterpolation/CSharpSimplifyInterpolationHelpers.cs @@ -5,23 +5,22 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.SimplifyInterpolation; -namespace Microsoft.CodeAnalysis.CSharp.Analyzers.SimplifyInterpolation +namespace Microsoft.CodeAnalysis.CSharp.Analyzers.SimplifyInterpolation; + +internal sealed class CSharpSimplifyInterpolationHelpers : AbstractSimplifyInterpolationHelpers { - internal sealed class CSharpSimplifyInterpolationHelpers : AbstractSimplifyInterpolationHelpers - { - public static CSharpSimplifyInterpolationHelpers Instance { get; } = new(); + public static CSharpSimplifyInterpolationHelpers Instance { get; } = new(); - private CSharpSimplifyInterpolationHelpers() { } + private CSharpSimplifyInterpolationHelpers() { } - protected override bool PermitNonLiteralAlignmentComponents => true; + protected override bool PermitNonLiteralAlignmentComponents => true; - protected override SyntaxNode GetPreservedInterpolationExpressionSyntax(IOperation operation) + protected override SyntaxNode GetPreservedInterpolationExpressionSyntax(IOperation operation) + { + return operation.Syntax switch { - return operation.Syntax switch - { - ConditionalExpressionSyntax { Parent: ParenthesizedExpressionSyntax parent } => parent, - var syntax => syntax, - }; - } + ConditionalExpressionSyntax { Parent: ParenthesizedExpressionSyntax parent } => parent, + var syntax => syntax, + }; } } diff --git a/src/Analyzers/CSharp/Analyzers/SimplifyLinqExpression/CSharpSimplifyLinqExpressionDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/SimplifyLinqExpression/CSharpSimplifyLinqExpressionDiagnosticAnalyzer.cs index 0c180bd50690d..eae5ea736e8b4 100644 --- a/src/Analyzers/CSharp/Analyzers/SimplifyLinqExpression/CSharpSimplifyLinqExpressionDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/SimplifyLinqExpression/CSharpSimplifyLinqExpressionDiagnosticAnalyzer.cs @@ -9,19 +9,18 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.SimplifyLinqExpression; -namespace Microsoft.CodeAnalysis.CSharp.SimplifyLinqExpression +namespace Microsoft.CodeAnalysis.CSharp.SimplifyLinqExpression; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpSimplifyLinqExpressionDiagnosticAnalyzer : AbstractSimplifyLinqExpressionDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpSimplifyLinqExpressionDiagnosticAnalyzer : AbstractSimplifyLinqExpressionDiagnosticAnalyzer - { - protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; - protected override IInvocationOperation? TryGetNextInvocationInChain(IInvocationOperation invocation) - // In C#, exention methods contain the methods they are being called from in the `this` parameter - // So in the case of A().ExensionB() to get to ExensionB from A we do the following: - => invocation.Parent is IArgumentOperation argument && - argument.Parent is IInvocationOperation nextInvocation - ? nextInvocation - : null; - } + protected override IInvocationOperation? TryGetNextInvocationInChain(IInvocationOperation invocation) + // In C#, exention methods contain the methods they are being called from in the `this` parameter + // So in the case of A().ExensionB() to get to ExensionB from A we do the following: + => invocation.Parent is IArgumentOperation argument && + argument.Parent is IInvocationOperation nextInvocation + ? nextInvocation + : null; } diff --git a/src/Analyzers/CSharp/Analyzers/SimplifyPropertyPattern/CSharpSimplifyPropertyPatternDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/SimplifyPropertyPattern/CSharpSimplifyPropertyPatternDiagnosticAnalyzer.cs index c107829c01fa8..71ffc34d40403 100644 --- a/src/Analyzers/CSharp/Analyzers/SimplifyPropertyPattern/CSharpSimplifyPropertyPatternDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/SimplifyPropertyPattern/CSharpSimplifyPropertyPatternDiagnosticAnalyzer.cs @@ -9,65 +9,65 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.SimplifyPropertyPattern +namespace Microsoft.CodeAnalysis.CSharp.SimplifyPropertyPattern; + +/// +/// Looks for code of the form: +/// +/// x is { a: { b: ... } } +/// +/// and converts it to: +/// +/// x is { a.b: ... } +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpSimplifyPropertyPatternDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - /// - /// Looks for code of the form: - /// - /// x is { a: { b: ... } } - /// - /// and converts it to: - /// - /// x is { a.b: ... } - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpSimplifyPropertyPatternDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public CSharpSimplifyPropertyPatternDiagnosticAnalyzer() + : base(IDEDiagnosticIds.SimplifyPropertyPatternDiagnosticId, + EnforceOnBuildValues.SimplifyPropertyPattern, + CSharpCodeStyleOptions.PreferExtendedPropertyPattern, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Property_pattern_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Simplify_property_pattern), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpSimplifyPropertyPatternDiagnosticAnalyzer() - : base(IDEDiagnosticIds.SimplifyPropertyPatternDiagnosticId, - EnforceOnBuildValues.SimplifyPropertyPattern, - CSharpCodeStyleOptions.PreferExtendedPropertyPattern, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Property_pattern_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Simplify_property_pattern), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(compilationContext => { - context.RegisterCompilationStartAction(compilationContext => - { - // Dotted property patterns are only available in C# 10.0 and above. Don't offer this refactoring - // in projects targeting a lesser version. + // Dotted property patterns are only available in C# 10.0 and above. Don't offer this refactoring + // in projects targeting a lesser version. - if (compilationContext.Compilation.LanguageVersion() < LanguageVersion.CSharp10) - return; + if (compilationContext.Compilation.LanguageVersion() < LanguageVersion.CSharp10) + return; - context.RegisterSyntaxNodeAction(AnalyzeSubpattern, SyntaxKind.Subpattern); - }); - } + context.RegisterSyntaxNodeAction(AnalyzeSubpattern, SyntaxKind.Subpattern); + }); + } - private void AnalyzeSubpattern(SyntaxNodeAnalysisContext syntaxContext) - { - // Bail immediately if the user has disabled this feature. - var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferExtendedPropertyPattern; - if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) - return; + private void AnalyzeSubpattern(SyntaxNodeAnalysisContext syntaxContext) + { + // Bail immediately if the user has disabled this feature. + var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferExtendedPropertyPattern; + if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) + return; - var subpattern = (SubpatternSyntax)syntaxContext.Node; - if (!SimplifyPropertyPatternHelpers.IsSimplifiable(subpattern, out _, out var expressionColon)) - return; + var subpattern = (SubpatternSyntax)syntaxContext.Node; + if (!SimplifyPropertyPatternHelpers.IsSimplifiable(subpattern, out _, out var expressionColon)) + return; - // If the diagnostic is not hidden, then just place the user visible part - // on the local being initialized with the lambda. - syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - expressionColon.GetLocation(), - styleOption.Notification, - ImmutableArray.Create(subpattern.GetLocation()), - properties: null)); - } + // If the diagnostic is not hidden, then just place the user visible part + // on the local being initialized with the lambda. + syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + expressionColon.GetLocation(), + styleOption.Notification, + syntaxContext.Options, + ImmutableArray.Create(subpattern.GetLocation()), + properties: null)); } } diff --git a/src/Analyzers/CSharp/Analyzers/SimplifyPropertyPattern/SimplifyPropertyPatternHelpers.cs b/src/Analyzers/CSharp/Analyzers/SimplifyPropertyPattern/SimplifyPropertyPatternHelpers.cs index 388276ad945cd..02ebe5f6963b3 100644 --- a/src/Analyzers/CSharp/Analyzers/SimplifyPropertyPattern/SimplifyPropertyPatternHelpers.cs +++ b/src/Analyzers/CSharp/Analyzers/SimplifyPropertyPattern/SimplifyPropertyPatternHelpers.cs @@ -6,56 +6,55 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.SimplifyPropertyPattern +namespace Microsoft.CodeAnalysis.CSharp.SimplifyPropertyPattern; + +internal static class SimplifyPropertyPatternHelpers { - internal static class SimplifyPropertyPatternHelpers + public static bool IsSimplifiable( + SubpatternSyntax subpattern, + [NotNullWhen(true)] out SubpatternSyntax? innerSubpattern, + [NotNullWhen(true)] out BaseExpressionColonSyntax? outerExpressionColon) { - public static bool IsSimplifiable( - SubpatternSyntax subpattern, - [NotNullWhen(true)] out SubpatternSyntax? innerSubpattern, - [NotNullWhen(true)] out BaseExpressionColonSyntax? outerExpressionColon) - { - // can't simplify if parent pattern is not a property pattern - // - // can't simplify if we have anything inside other than a property pattern clause. i.e. - // `a: { b: ... } x` is not simplifiable as we'll lose the `x` binding for the `a` property. - // - // can't simplify `a: { }` or `a: { b: ..., c: ... }` - if (subpattern is - { - Parent: PropertyPatternClauseSyntax, - ExpressionColon: { } outer, - Pattern: RecursivePatternSyntax - { - Type: null, - PositionalPatternClause: null, - Designation: null, - PropertyPatternClause.Subpatterns: { Count: 1 } subpatterns - } - } && - subpatterns[0] is { ExpressionColon: { } inner } && - IsMergable(outer.Expression) && - IsMergable(inner.Expression)) + // can't simplify if parent pattern is not a property pattern + // + // can't simplify if we have anything inside other than a property pattern clause. i.e. + // `a: { b: ... } x` is not simplifiable as we'll lose the `x` binding for the `a` property. + // + // can't simplify `a: { }` or `a: { b: ..., c: ... }` + if (subpattern is { - innerSubpattern = subpatterns[0]; - outerExpressionColon = outer; - return true; - } - - innerSubpattern = null; - outerExpressionColon = null; - return false; + Parent: PropertyPatternClauseSyntax, + ExpressionColon: { } outer, + Pattern: RecursivePatternSyntax + { + Type: null, + PositionalPatternClause: null, + Designation: null, + PropertyPatternClause.Subpatterns: { Count: 1 } subpatterns + } + } && + subpatterns[0] is { ExpressionColon: { } inner } && + IsMergable(outer.Expression) && + IsMergable(inner.Expression)) + { + innerSubpattern = subpatterns[0]; + outerExpressionColon = outer; + return true; } - public static bool IsMergable([NotNullWhen(true)] ExpressionSyntax? expression) - { - if (expression is SimpleNameSyntax) - return true; + innerSubpattern = null; + outerExpressionColon = null; + return false; + } - if (expression is MemberAccessExpressionSyntax memberAccessExpression && IsMergable(memberAccessExpression.Expression)) - return true; + public static bool IsMergable([NotNullWhen(true)] ExpressionSyntax? expression) + { + if (expression is SimpleNameSyntax) + return true; - return false; - } + if (expression is MemberAccessExpressionSyntax memberAccessExpression && IsMergable(memberAccessExpression.Expression)) + return true; + + return false; } } diff --git a/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer.cs index 0534fb9884ed0..0985e494ee4d2 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer.cs @@ -14,62 +14,61 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.CSharp.Analyzers.UseCoalesceExpression +namespace Microsoft.CodeAnalysis.CSharp.Analyzers.UseCoalesceExpression; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer : + AbstractUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer< + SyntaxKind, + ExpressionSyntax, + StatementSyntax, + VariableDeclaratorSyntax, + IfStatementSyntax> { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer : - AbstractUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer< - SyntaxKind, - ExpressionSyntax, - StatementSyntax, - VariableDeclaratorSyntax, - IfStatementSyntax> - { - protected override SyntaxKind IfStatementKind - => SyntaxKind.IfStatement; + protected override SyntaxKind IfStatementKind + => SyntaxKind.IfStatement; - protected override ISyntaxFacts SyntaxFacts - => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts SyntaxFacts + => CSharpSyntaxFacts.Instance; - protected override bool IsSingle(VariableDeclaratorSyntax declarator) - => true; + protected override bool IsSingle(VariableDeclaratorSyntax declarator) + => true; - protected override SyntaxNode GetDeclarationNode(VariableDeclaratorSyntax declarator) - => declarator; + protected override SyntaxNode GetDeclarationNode(VariableDeclaratorSyntax declarator) + => declarator; - protected override ExpressionSyntax GetConditionOfIfStatement(IfStatementSyntax ifStatement) - => ifStatement.Condition; + protected override ExpressionSyntax GetConditionOfIfStatement(IfStatementSyntax ifStatement) + => ifStatement.Condition; - protected override bool IsNullCheck(ExpressionSyntax condition, [NotNullWhen(true)] out ExpressionSyntax? checkedExpression) + protected override bool IsNullCheck(ExpressionSyntax condition, [NotNullWhen(true)] out ExpressionSyntax? checkedExpression) + { + if (condition is BinaryExpressionSyntax(SyntaxKind.EqualsExpression) { Right: LiteralExpressionSyntax(SyntaxKind.NullLiteralExpression) } binary) { - if (condition is BinaryExpressionSyntax(SyntaxKind.EqualsExpression) { Right: LiteralExpressionSyntax(SyntaxKind.NullLiteralExpression) } binary) - { - checkedExpression = binary.Left; - return true; - } - else if (condition is IsPatternExpressionSyntax { Pattern: ConstantPatternSyntax { Expression: LiteralExpressionSyntax(SyntaxKind.NullLiteralExpression) } } isPattern) - { - checkedExpression = isPattern.Expression; - return true; - } - - checkedExpression = null; - return false; + checkedExpression = binary.Left; + return true; } - - protected override bool TryGetEmbeddedStatement(IfStatementSyntax ifStatement, [NotNullWhen(true)] out StatementSyntax? whenTrueStatement) + else if (condition is IsPatternExpressionSyntax { Pattern: ConstantPatternSyntax { Expression: LiteralExpressionSyntax(SyntaxKind.NullLiteralExpression) } } isPattern) { - whenTrueStatement = ifStatement.Statement is BlockSyntax { Statements.Count: 1 } block - ? block.Statements[0] - : ifStatement.Statement; - + checkedExpression = isPattern.Expression; return true; } - protected override bool HasElseBlock(IfStatementSyntax ifStatement) - => ifStatement.Else != null; + checkedExpression = null; + return false; + } + + protected override bool TryGetEmbeddedStatement(IfStatementSyntax ifStatement, [NotNullWhen(true)] out StatementSyntax? whenTrueStatement) + { + whenTrueStatement = ifStatement.Statement is BlockSyntax { Statements.Count: 1 } block + ? block.Statements[0] + : ifStatement.Statement; - protected override StatementSyntax? TryGetPreviousStatement(IfStatementSyntax ifStatement) - => ifStatement.GetPreviousStatement(); + return true; } + + protected override bool HasElseBlock(IfStatementSyntax ifStatement) + => ifStatement.Else != null; + + protected override StatementSyntax? TryGetPreviousStatement(IfStatementSyntax ifStatement) + => ifStatement.GetPreviousStatement(); } diff --git a/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer.cs index 85ea079015262..309eafc7750a8 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer.cs @@ -9,22 +9,21 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.UseCoalesceExpression; -namespace Microsoft.CodeAnalysis.CSharp.UseCoalesceExpression +namespace Microsoft.CodeAnalysis.CSharp.UseCoalesceExpression; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer : + AbstractUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer< + SyntaxKind, + ExpressionSyntax, + ConditionalExpressionSyntax, + BinaryExpressionSyntax, + MemberAccessExpressionSyntax, + PrefixUnaryExpressionSyntax> { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer : - AbstractUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer< - SyntaxKind, - ExpressionSyntax, - ConditionalExpressionSyntax, - BinaryExpressionSyntax, - MemberAccessExpressionSyntax, - PrefixUnaryExpressionSyntax> - { - protected override ISyntaxFacts GetSyntaxFacts() - => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts GetSyntaxFacts() + => CSharpSyntaxFacts.Instance; - protected override bool IsTargetTyped(SemanticModel semanticModel, ConditionalExpressionSyntax conditional, CancellationToken cancellationToken) - => UseCoalesceExpressionHelpers.IsTargetTyped(semanticModel, conditional, cancellationToken); - } + protected override bool IsTargetTyped(SemanticModel semanticModel, ConditionalExpressionSyntax conditional, CancellationToken cancellationToken) + => UseCoalesceExpressionHelpers.IsTargetTyped(semanticModel, conditional, cancellationToken); } diff --git a/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer.cs index 248283d2309a6..218fe7fd26c04 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/CSharpUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer.cs @@ -9,20 +9,19 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.UseCoalesceExpression; -namespace Microsoft.CodeAnalysis.CSharp.UseCoalesceExpression +namespace Microsoft.CodeAnalysis.CSharp.UseCoalesceExpression; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer : + AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer< + SyntaxKind, + ExpressionSyntax, + ConditionalExpressionSyntax, + BinaryExpressionSyntax> { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer : - AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer< - SyntaxKind, - ExpressionSyntax, - ConditionalExpressionSyntax, - BinaryExpressionSyntax> - { - protected override ISyntaxFacts GetSyntaxFacts() - => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts GetSyntaxFacts() + => CSharpSyntaxFacts.Instance; - protected override bool IsTargetTyped(SemanticModel semanticModel, ConditionalExpressionSyntax conditional, CancellationToken cancellationToken) - => UseCoalesceExpressionHelpers.IsTargetTyped(semanticModel, conditional, cancellationToken); - } + protected override bool IsTargetTyped(SemanticModel semanticModel, ConditionalExpressionSyntax conditional, CancellationToken cancellationToken) + => UseCoalesceExpressionHelpers.IsTargetTyped(semanticModel, conditional, cancellationToken); } diff --git a/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/UseCoalesceExpressionHelpers.cs b/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/UseCoalesceExpressionHelpers.cs index 528a1f975aec7..63cf5e107f8c9 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/UseCoalesceExpressionHelpers.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCoalesceExpression/UseCoalesceExpressionHelpers.cs @@ -5,14 +5,13 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.UseCoalesceExpression +namespace Microsoft.CodeAnalysis.CSharp.UseCoalesceExpression; + +internal static class UseCoalesceExpressionHelpers { - internal static class UseCoalesceExpressionHelpers + public static bool IsTargetTyped(SemanticModel semanticModel, ConditionalExpressionSyntax conditional, CancellationToken cancellationToken) { - public static bool IsTargetTyped(SemanticModel semanticModel, ConditionalExpressionSyntax conditional, CancellationToken cancellationToken) - { - var conversion = semanticModel.GetConversion(conditional, cancellationToken); - return conversion.IsConditionalExpression; - } + var conversion = semanticModel.GetConversion(conditional, cancellationToken); + return conversion.IsConditionalExpression; } } diff --git a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.cs index 86ab0bebb8503..887b530668f8c 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.cs @@ -191,6 +191,7 @@ private void AnalyzeArrayInitializerExpression(SyntaxNodeAnalysisContext context Descriptor, initializer.OpenBraceToken.GetLocation(), option.Notification, + context.Options, additionalLocations: ImmutableArray.Create(initializer.GetLocation()), properties: changesSemantics ? ChangesSemantics : null)); } @@ -205,6 +206,7 @@ private void ReportArrayCreationDiagnostics( Descriptor, expression.GetFirstToken().GetLocation(), notification, + context.Options, additionalLocations: locations, properties: properties)); @@ -219,6 +221,7 @@ expression is ArrayCreationExpressionSyntax arrayCreationExpression UnnecessaryCodeDescriptor, additionalUnnecessaryLocations[0], NotificationOption2.ForSeverity(UnnecessaryCodeDescriptor.DefaultSeverity), + context.Options, additionalLocations: locations, additionalUnnecessaryLocations: additionalUnnecessaryLocations, properties: properties)); diff --git a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForBuilderDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForBuilderDiagnosticAnalyzer.cs index e2901f3eb0a27..ab5493c28229e 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForBuilderDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForBuilderDiagnosticAnalyzer.cs @@ -54,6 +54,7 @@ private void AnalyzeInvocationExpression( Descriptor, analysisResult.DiagnosticLocation, option.Notification, + context.Options, additionalLocations: locations, properties: properties)); @@ -70,6 +71,7 @@ void FadeOutCode(SyntaxNodeAnalysisContext context, AnalysisResult analysisResul UnnecessaryCodeDescriptor, additionalUnnecessaryLocations[0], NotificationOption2.ForSeverity(UnnecessaryCodeDescriptor.DefaultSeverity), + context.Options, additionalLocations: locations, additionalUnnecessaryLocations: additionalUnnecessaryLocations, properties: properties)); @@ -87,6 +89,7 @@ void FadeOutCode(SyntaxNodeAnalysisContext context, AnalysisResult analysisResul UnnecessaryCodeDescriptor, additionalUnnecessaryLocations[0], NotificationOption2.ForSeverity(UnnecessaryCodeDescriptor.DefaultSeverity), + context.Options, additionalLocations: locations, additionalUnnecessaryLocations: additionalUnnecessaryLocations, properties: properties)); diff --git a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForCreateDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForCreateDiagnosticAnalyzer.cs index c2be19e736cfc..de116646207c7 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForCreateDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForCreateDiagnosticAnalyzer.cs @@ -60,6 +60,7 @@ private void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context, INam Descriptor, memberAccess.Name.Identifier.GetLocation(), option.Notification, + context.Options, additionalLocations: locations, properties)); @@ -73,6 +74,7 @@ private void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context, INam UnnecessaryCodeDescriptor, additionalUnnecessaryLocations[0], NotificationOption2.ForSeverity(UnnecessaryCodeDescriptor.DefaultSeverity), + context.Options, additionalLocations: locations, additionalUnnecessaryLocations: additionalUnnecessaryLocations, properties)); diff --git a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForEmptyDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForEmptyDiagnosticAnalyzer.cs index 86f9b0ff8b2c1..2c3b3fd61d2c0 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForEmptyDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForEmptyDiagnosticAnalyzer.cs @@ -56,6 +56,7 @@ private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context, INamedTypeSy Descriptor, memberAccess.Name.Identifier.GetLocation(), option.Notification, + context.Options, additionalLocations: ImmutableArray.Create(nodeToReplace.GetLocation()), properties: changesSemantics ? ChangesSemantics : null)); } diff --git a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForFluentDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForFluentDiagnosticAnalyzer.cs index 5679abe17fe0a..a91372467c795 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForFluentDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForFluentDiagnosticAnalyzer.cs @@ -110,6 +110,7 @@ private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context, INamedTypeSy Descriptor, memberAccess.Name.Identifier.GetLocation(), option.Notification, + context.Options, additionalLocations: ImmutableArray.Create(invocation.GetLocation()), properties: analysisResult.Value.ChangesSemantics ? ChangesSemantics : null)); diff --git a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForStackAllocDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForStackAllocDiagnosticAnalyzer.cs index d619178c2d0f0..dc8fc2a87fc37 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForStackAllocDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForStackAllocDiagnosticAnalyzer.cs @@ -59,6 +59,7 @@ private void AnalyzeImplicitStackAllocExpression(SyntaxNodeAnalysisContext conte Descriptor, expression.GetFirstToken().GetLocation(), option.Notification, + context.Options, additionalLocations: locations, properties: null)); @@ -71,6 +72,7 @@ private void AnalyzeImplicitStackAllocExpression(SyntaxNodeAnalysisContext conte UnnecessaryCodeDescriptor, additionalUnnecessaryLocations[0], NotificationOption2.ForSeverity(UnnecessaryCodeDescriptor.DefaultSeverity), + context.Options, additionalLocations: locations, additionalUnnecessaryLocations: additionalUnnecessaryLocations)); } @@ -97,6 +99,7 @@ private void AnalyzeExplicitStackAllocExpression(SyntaxNodeAnalysisContext conte Descriptor, expression.GetFirstToken().GetLocation(), option.Notification, + context.Options, additionalLocations: locations, properties: null)); @@ -109,6 +112,7 @@ private void AnalyzeExplicitStackAllocExpression(SyntaxNodeAnalysisContext conte UnnecessaryCodeDescriptor, additionalUnnecessaryLocations[0], NotificationOption2.ForSeverity(UnnecessaryCodeDescriptor.DefaultSeverity), + context.Options, additionalLocations: locations, additionalUnnecessaryLocations: additionalUnnecessaryLocations)); } diff --git a/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/CSharpUseCompoundAssignmentDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/CSharpUseCompoundAssignmentDiagnosticAnalyzer.cs index c7007dbdda23f..ff11d8fa4b805 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/CSharpUseCompoundAssignmentDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/CSharpUseCompoundAssignmentDiagnosticAnalyzer.cs @@ -8,51 +8,50 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.UseCompoundAssignment; -namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment +namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseCompoundAssignmentDiagnosticAnalyzer + : AbstractUseCompoundAssignmentDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseCompoundAssignmentDiagnosticAnalyzer - : AbstractUseCompoundAssignmentDiagnosticAnalyzer + public CSharpUseCompoundAssignmentDiagnosticAnalyzer() + : base(CSharpSyntaxFacts.Instance, Utilities.Kinds) { - public CSharpUseCompoundAssignmentDiagnosticAnalyzer() - : base(CSharpSyntaxFacts.Instance, Utilities.Kinds) - { - } + } - protected override SyntaxKind GetAnalysisKind() - => SyntaxKind.SimpleAssignmentExpression; + protected override SyntaxKind GetAnalysisKind() + => SyntaxKind.SimpleAssignmentExpression; - protected override bool IsSupported(SyntaxKind assignmentKind, ParseOptions options) - => assignmentKind != SyntaxKind.CoalesceExpression || - options.LanguageVersion() >= LanguageVersion.CSharp8; + protected override bool IsSupported(SyntaxKind assignmentKind, ParseOptions options) + => assignmentKind != SyntaxKind.CoalesceExpression || + options.LanguageVersion() >= LanguageVersion.CSharp8; - protected override int TryGetIncrementOrDecrement(SyntaxKind opKind, object constantValue) + protected override int TryGetIncrementOrDecrement(SyntaxKind opKind, object constantValue) + { + if (constantValue is + (sbyte)1 or (short)1 or (int)1 or (long)1 or + (byte)1 or (ushort)1 or (uint)1 or (ulong)1 or + 1.0 or 1.0f or 1.0m) { - if (constantValue is - (sbyte)1 or (short)1 or (int)1 or (long)1 or - (byte)1 or (ushort)1 or (uint)1 or (ulong)1 or - 1.0 or 1.0f or 1.0m) + return opKind switch { - return opKind switch - { - SyntaxKind.AddExpression => 1, - SyntaxKind.SubtractExpression => -1, - _ => 0 - }; - } - else if (constantValue is - (sbyte)-1 or (short)-1 or (int)-1 or (long)-1 or - -1.0 or -1.0f or -1.0m) + SyntaxKind.AddExpression => 1, + SyntaxKind.SubtractExpression => -1, + _ => 0 + }; + } + else if (constantValue is + (sbyte)-1 or (short)-1 or (int)-1 or (long)-1 or + -1.0 or -1.0f or -1.0m) + { + return opKind switch { - return opKind switch - { - SyntaxKind.AddExpression => -1, - SyntaxKind.SubtractExpression => 1, - _ => 0 - }; - } - - return 0; + SyntaxKind.AddExpression => -1, + SyntaxKind.SubtractExpression => 1, + _ => 0 + }; } + + return 0; } } diff --git a/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer.cs index 0a773b0323d14..73386c61df9b8 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer.cs @@ -15,202 +15,203 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.UseCompoundAssignment; -namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment +namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment; + +/// +/// Looks for expressions of the form: +/// +/// expr ?? (expr = value) and converts it to expr ??= value. +/// if (expr is null) expr = value and converts it to expr ??= value. +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer + : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - /// - /// Looks for expressions of the form: - /// - /// expr ?? (expr = value) and converts it to expr ??= value. - /// if (expr is null) expr = value and converts it to expr ??= value. - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer - : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseCoalesceCompoundAssignmentDiagnosticId, + EnforceOnBuildValues.UseCoalesceCompoundAssignment, + CodeStyleOptions2.PreferCompoundAssignment, + new LocalizableResourceString(nameof(AnalyzersResources.Use_compound_assignment), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - public CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseCoalesceCompoundAssignmentDiagnosticId, - EnforceOnBuildValues.UseCoalesceCompoundAssignment, - CodeStyleOptions2.PreferCompoundAssignment, - new LocalizableResourceString(nameof(AnalyzersResources.Use_compound_assignment), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - context.RegisterCompilationStartAction(context => - { - if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp8) - return; + if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp8) + return; - context.RegisterSyntaxNodeAction(AnalyzeCoalesceExpression, SyntaxKind.CoalesceExpression); - context.RegisterSyntaxNodeAction(AnalyzeIfStatement, SyntaxKind.IfStatement); - }); - } + context.RegisterSyntaxNodeAction(AnalyzeCoalesceExpression, SyntaxKind.CoalesceExpression); + context.RegisterSyntaxNodeAction(AnalyzeIfStatement, SyntaxKind.IfStatement); + }); + } - private void AnalyzeCoalesceExpression(SyntaxNodeAnalysisContext context) - { - var cancellationToken = context.CancellationToken; - var semanticModel = context.SemanticModel; + private void AnalyzeCoalesceExpression(SyntaxNodeAnalysisContext context) + { + var cancellationToken = context.CancellationToken; + var semanticModel = context.SemanticModel; - var coalesceExpression = (BinaryExpressionSyntax)context.Node; + var coalesceExpression = (BinaryExpressionSyntax)context.Node; - var option = context.GetAnalyzerOptions().PreferCompoundAssignment; + var option = context.GetAnalyzerOptions().PreferCompoundAssignment; - // Bail immediately if the user has disabled this feature. - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; + // Bail immediately if the user has disabled this feature. + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; - var coalesceLeft = coalesceExpression.Left; - var coalesceRight = coalesceExpression.Right; + var coalesceLeft = coalesceExpression.Left; + var coalesceRight = coalesceExpression.Right; - if (coalesceRight is not ParenthesizedExpressionSyntax parenthesizedExpr) - return; + if (coalesceRight is not ParenthesizedExpressionSyntax parenthesizedExpr) + return; - if (parenthesizedExpr.Expression is not AssignmentExpressionSyntax assignment) - return; + if (parenthesizedExpr.Expression is not AssignmentExpressionSyntax assignment) + return; - if (assignment.Kind() != SyntaxKind.SimpleAssignmentExpression) - return; + if (assignment.Kind() != SyntaxKind.SimpleAssignmentExpression) + return; - // have x ?? (y = z) - // ensure that 'x' and 'y' are suitably equivalent. - var syntaxFacts = CSharpSyntaxFacts.Instance; - if (!syntaxFacts.AreEquivalent(coalesceLeft, assignment.Left)) - return; + // have x ?? (y = z) + // ensure that 'x' and 'y' are suitably equivalent. + var syntaxFacts = CSharpSyntaxFacts.Instance; + if (!syntaxFacts.AreEquivalent(coalesceLeft, assignment.Left)) + return; - // Syntactically looks promising. But we can only safely do this if 'expr' - // is side-effect-free since we will be changing the number of times it is - // executed from twice to once. - if (!UseCompoundAssignmentUtilities.IsSideEffectFree( - syntaxFacts, coalesceLeft, semanticModel, cancellationToken)) - { - return; - } - - // Good match. - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - coalesceExpression.OperatorToken.GetLocation(), - option.Notification, - additionalLocations: ImmutableArray.Create(coalesceExpression.GetLocation()), - properties: null)); + // Syntactically looks promising. But we can only safely do this if 'expr' + // is side-effect-free since we will be changing the number of times it is + // executed from twice to once. + if (!UseCompoundAssignmentUtilities.IsSideEffectFree( + syntaxFacts, coalesceLeft, semanticModel, cancellationToken)) + { + return; } - public static bool GetWhenTrueAssignment( - IfStatementSyntax ifStatement, - [NotNullWhen(true)] out StatementSyntax? whenTrue, - [NotNullWhen(true)] out AssignmentExpressionSyntax? assignment) - { - whenTrue = ifStatement.Statement is BlockSyntax block - ? block.Statements.Count == 1 ? block.Statements[0] : null - : ifStatement.Statement; + // Good match. + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + coalesceExpression.OperatorToken.GetLocation(), + option.Notification, + context.Options, + additionalLocations: ImmutableArray.Create(coalesceExpression.GetLocation()), + properties: null)); + } - assignment = whenTrue is ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignmentTemp } - ? assignmentTemp - : null; + public static bool GetWhenTrueAssignment( + IfStatementSyntax ifStatement, + [NotNullWhen(true)] out StatementSyntax? whenTrue, + [NotNullWhen(true)] out AssignmentExpressionSyntax? assignment) + { + whenTrue = ifStatement.Statement is BlockSyntax block + ? block.Statements.Count == 1 ? block.Statements[0] : null + : ifStatement.Statement; - return whenTrue != null && assignment != null; - } + assignment = whenTrue is ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignmentTemp } + ? assignmentTemp + : null; - private void AnalyzeIfStatement(SyntaxNodeAnalysisContext context) - { - var cancellationToken = context.CancellationToken; - var semanticModel = context.SemanticModel; + return whenTrue != null && assignment != null; + } - var ifStatement = (IfStatementSyntax)context.Node; + private void AnalyzeIfStatement(SyntaxNodeAnalysisContext context) + { + var cancellationToken = context.CancellationToken; + var semanticModel = context.SemanticModel; - var option = context.GetAnalyzerOptions().PreferCompoundAssignment; + var ifStatement = (IfStatementSyntax)context.Node; - // Bail immediately if the user has disabled this feature. - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; + var option = context.GetAnalyzerOptions().PreferCompoundAssignment; - if (ifStatement.Else != null) - return; + // Bail immediately if the user has disabled this feature. + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; - if (!GetWhenTrueAssignment(ifStatement, out var whenTrue, out var assignment)) - return; + if (ifStatement.Else != null) + return; - if (!IsReferenceEqualsNullCheck(semanticModel, ifStatement.Condition, cancellationToken, out var testedExpression)) - return; + if (!GetWhenTrueAssignment(ifStatement, out var whenTrue, out var assignment)) + return; - // have if (x == null) x = y; - // ensure that 'x' in both locations are suitably equivalent. - var syntaxFacts = CSharpSyntaxFacts.Instance; - if (!syntaxFacts.AreEquivalent(testedExpression, assignment.Left)) - return; + if (!IsReferenceEqualsNullCheck(semanticModel, ifStatement.Condition, cancellationToken, out var testedExpression)) + return; - // Syntactically looks promising. But we can only safely do this if 'expr' - // is side-effect-free since we will be changing the number of times it is - // executed from twice to once. - if (!UseCompoundAssignmentUtilities.IsSideEffectFree( - syntaxFacts, testedExpression, semanticModel, cancellationToken)) - { - return; - } + // have if (x == null) x = y; + // ensure that 'x' in both locations are suitably equivalent. + var syntaxFacts = CSharpSyntaxFacts.Instance; + if (!syntaxFacts.AreEquivalent(testedExpression, assignment.Left)) + return; - // Don't want to offer anything if our if-statement body has any conditional directives in it. That - // means there's some other code that may run under some other conditions, that we do not want to now - // run conditionally outside of the 'if' statement itself. - if (whenTrue.GetLeadingTrivia().Any(static t => t.GetStructure() is ConditionalDirectiveTriviaSyntax)) - return; + // Syntactically looks promising. But we can only safely do this if 'expr' + // is side-effect-free since we will be changing the number of times it is + // executed from twice to once. + if (!UseCompoundAssignmentUtilities.IsSideEffectFree( + syntaxFacts, testedExpression, semanticModel, cancellationToken)) + { + return; + } - // pointers cannot use ??= - if (semanticModel.GetTypeInfo(testedExpression, cancellationToken).Type is IPointerTypeSymbol or IFunctionPointerTypeSymbol) - return; + // Don't want to offer anything if our if-statement body has any conditional directives in it. That + // means there's some other code that may run under some other conditions, that we do not want to now + // run conditionally outside of the 'if' statement itself. + if (whenTrue.GetLeadingTrivia().Any(static t => t.GetStructure() is ConditionalDirectiveTriviaSyntax)) + return; + + // pointers cannot use ??= + if (semanticModel.GetTypeInfo(testedExpression, cancellationToken).Type is IPointerTypeSymbol or IFunctionPointerTypeSymbol) + return; + + // Good match. + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + ifStatement.IfKeyword.GetLocation(), + option.Notification, + context.Options, + additionalLocations: ImmutableArray.Create(ifStatement.GetLocation()), + properties: null)); + } - // Good match. - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - ifStatement.IfKeyword.GetLocation(), - option.Notification, - additionalLocations: ImmutableArray.Create(ifStatement.GetLocation()), - properties: null)); + private bool IsReferenceEqualsNullCheck( + SemanticModel semanticModel, + ExpressionSyntax condition, + CancellationToken cancellationToken, + [NotNullWhen(true)] out ExpressionSyntax? testedExpression) + { + testedExpression = null; + if (condition is BinaryExpressionSyntax(SyntaxKind.EqualsExpression) { Right: LiteralExpressionSyntax(SyntaxKind.NullLiteralExpression) } binaryExpression) + { + // Ensure that if we are using `==` that it's not an overloaded operator. One known exception is + // System.String. Even though `==` is overloaded, it has the same semantics as ReferenceEquals(null) so + // it's safe to convert. + var symbol = semanticModel.GetSymbolInfo(binaryExpression, cancellationToken).Symbol; + if (symbol is null || !symbol.IsUserDefinedOperator() || symbol.ContainingType.SpecialType == SpecialType.System_String) + testedExpression = binaryExpression.Left; } - - private bool IsReferenceEqualsNullCheck( - SemanticModel semanticModel, - ExpressionSyntax condition, - CancellationToken cancellationToken, - [NotNullWhen(true)] out ExpressionSyntax? testedExpression) + else if (condition is IsPatternExpressionSyntax { Pattern: ConstantPatternSyntax { Expression: LiteralExpressionSyntax(SyntaxKind.NullLiteralExpression) } } isPattern) { - testedExpression = null; - if (condition is BinaryExpressionSyntax(SyntaxKind.EqualsExpression) { Right: LiteralExpressionSyntax(SyntaxKind.NullLiteralExpression) } binaryExpression) - { - // Ensure that if we are using `==` that it's not an overloaded operator. One known exception is - // System.String. Even though `==` is overloaded, it has the same semantics as ReferenceEquals(null) so - // it's safe to convert. - var symbol = semanticModel.GetSymbolInfo(binaryExpression, cancellationToken).Symbol; - if (symbol is null || !symbol.IsUserDefinedOperator() || symbol.ContainingType.SpecialType == SpecialType.System_String) - testedExpression = binaryExpression.Left; - } - else if (condition is IsPatternExpressionSyntax { Pattern: ConstantPatternSyntax { Expression: LiteralExpressionSyntax(SyntaxKind.NullLiteralExpression) } } isPattern) - { - // x is null. always a valid null check. - testedExpression = isPattern.Expression; - } - else if (condition is InvocationExpressionSyntax { ArgumentList.Arguments.Count: 2 } invocation) - { - var arg0 = invocation.ArgumentList.Arguments[0].Expression; - var arg1 = invocation.ArgumentList.Arguments[1].Expression; + // x is null. always a valid null check. + testedExpression = isPattern.Expression; + } + else if (condition is InvocationExpressionSyntax { ArgumentList.Arguments.Count: 2 } invocation) + { + var arg0 = invocation.ArgumentList.Arguments[0].Expression; + var arg1 = invocation.ArgumentList.Arguments[1].Expression; - if (arg0.Kind() == SyntaxKind.NullLiteralExpression || - arg1.Kind() == SyntaxKind.NullLiteralExpression) + if (arg0.Kind() == SyntaxKind.NullLiteralExpression || + arg1.Kind() == SyntaxKind.NullLiteralExpression) + { + var symbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol; + if (symbol?.Name == nameof(ReferenceEquals) && + symbol.ContainingType?.SpecialType == SpecialType.System_Object) { - var symbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol; - if (symbol?.Name == nameof(ReferenceEquals) && - symbol.ContainingType?.SpecialType == SpecialType.System_Object) - { - testedExpression = arg0.Kind() == SyntaxKind.NullLiteralExpression ? arg1 : arg0; - } + testedExpression = arg0.Kind() == SyntaxKind.NullLiteralExpression ? arg1 : arg0; } } - - return testedExpression != null; } + + return testedExpression != null; } } diff --git a/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/Utilities.cs b/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/Utilities.cs index 80357bd238fc7..3a2bed7c95e77 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/Utilities.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCompoundAssignment/Utilities.cs @@ -5,36 +5,35 @@ using System.Collections.Immutable; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment +namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment; + +internal static class Utilities { - internal static class Utilities - { - public static readonly ImmutableArray<(SyntaxKind, SyntaxKind, SyntaxKind)> Kinds = - ImmutableArray.Create( - (SyntaxKind.AddExpression, SyntaxKind.AddAssignmentExpression), - (SyntaxKind.SubtractExpression, SyntaxKind.SubtractAssignmentExpression), - (SyntaxKind.MultiplyExpression, SyntaxKind.MultiplyAssignmentExpression), - (SyntaxKind.DivideExpression, SyntaxKind.DivideAssignmentExpression), - (SyntaxKind.ModuloExpression, SyntaxKind.ModuloAssignmentExpression), - (SyntaxKind.BitwiseAndExpression, SyntaxKind.AndAssignmentExpression), - (SyntaxKind.ExclusiveOrExpression, SyntaxKind.ExclusiveOrAssignmentExpression), - (SyntaxKind.BitwiseOrExpression, SyntaxKind.OrAssignmentExpression), - (SyntaxKind.LeftShiftExpression, SyntaxKind.LeftShiftAssignmentExpression), - (SyntaxKind.RightShiftExpression, SyntaxKind.RightShiftAssignmentExpression), - (SyntaxKind.CoalesceExpression, SyntaxKind.CoalesceAssignmentExpression)).SelectAsArray( - tuple => (tuple.Item1, tuple.Item2, FindOperatorToken(tuple.Item2))); + public static readonly ImmutableArray<(SyntaxKind, SyntaxKind, SyntaxKind)> Kinds = + ImmutableArray.Create( + (SyntaxKind.AddExpression, SyntaxKind.AddAssignmentExpression), + (SyntaxKind.SubtractExpression, SyntaxKind.SubtractAssignmentExpression), + (SyntaxKind.MultiplyExpression, SyntaxKind.MultiplyAssignmentExpression), + (SyntaxKind.DivideExpression, SyntaxKind.DivideAssignmentExpression), + (SyntaxKind.ModuloExpression, SyntaxKind.ModuloAssignmentExpression), + (SyntaxKind.BitwiseAndExpression, SyntaxKind.AndAssignmentExpression), + (SyntaxKind.ExclusiveOrExpression, SyntaxKind.ExclusiveOrAssignmentExpression), + (SyntaxKind.BitwiseOrExpression, SyntaxKind.OrAssignmentExpression), + (SyntaxKind.LeftShiftExpression, SyntaxKind.LeftShiftAssignmentExpression), + (SyntaxKind.RightShiftExpression, SyntaxKind.RightShiftAssignmentExpression), + (SyntaxKind.CoalesceExpression, SyntaxKind.CoalesceAssignmentExpression)).SelectAsArray( + tuple => (tuple.Item1, tuple.Item2, FindOperatorToken(tuple.Item2))); - private static SyntaxKind FindOperatorToken(SyntaxKind assignmentExpressionKind) + private static SyntaxKind FindOperatorToken(SyntaxKind assignmentExpressionKind) + { + for (var current = SyntaxKind.None; current <= SyntaxKind.ThrowExpression; current++) { - for (var current = SyntaxKind.None; current <= SyntaxKind.ThrowExpression; current++) + if (SyntaxFacts.GetAssignmentExpression(current) == assignmentExpressionKind) { - if (SyntaxFacts.GetAssignmentExpression(current) == assignmentExpressionKind) - { - return current; - } + return current; } - - throw ExceptionUtilities.UnexpectedValue(assignmentExpressionKind); } + + throw ExceptionUtilities.UnexpectedValue(assignmentExpressionKind); } } diff --git a/src/Analyzers/CSharp/Analyzers/UseConditionalExpression/CSharpUseConditionalExpressionForAssignmentDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseConditionalExpression/CSharpUseConditionalExpressionForAssignmentDiagnosticAnalyzer.cs index a4492173aa7db..70a739ffa9292 100644 --- a/src/Analyzers/CSharp/Analyzers/UseConditionalExpression/CSharpUseConditionalExpressionForAssignmentDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseConditionalExpression/CSharpUseConditionalExpressionForAssignmentDiagnosticAnalyzer.cs @@ -8,18 +8,17 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.UseConditionalExpression; -namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression +namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseConditionalExpressionForAssignmentDiagnosticAnalyzer + : AbstractUseConditionalExpressionForAssignmentDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseConditionalExpressionForAssignmentDiagnosticAnalyzer - : AbstractUseConditionalExpressionForAssignmentDiagnosticAnalyzer + public CSharpUseConditionalExpressionForAssignmentDiagnosticAnalyzer() + : base(new LocalizableResourceString(nameof(CSharpAnalyzersResources.if_statement_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpUseConditionalExpressionForAssignmentDiagnosticAnalyzer() - : base(new LocalizableResourceString(nameof(CSharpAnalyzersResources.if_statement_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } - - protected override ISyntaxFacts GetSyntaxFacts() - => CSharpSyntaxFacts.Instance; } + + protected override ISyntaxFacts GetSyntaxFacts() + => CSharpSyntaxFacts.Instance; } diff --git a/src/Analyzers/CSharp/Analyzers/UseConditionalExpression/CSharpUseConditionalExpressionForReturnDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseConditionalExpression/CSharpUseConditionalExpressionForReturnDiagnosticAnalyzer.cs index 4562648667351..3803642694dc4 100644 --- a/src/Analyzers/CSharp/Analyzers/UseConditionalExpression/CSharpUseConditionalExpressionForReturnDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseConditionalExpression/CSharpUseConditionalExpressionForReturnDiagnosticAnalyzer.cs @@ -9,37 +9,36 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.UseConditionalExpression; -namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression +namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseConditionalExpressionForReturnDiagnosticAnalyzer + : AbstractUseConditionalExpressionForReturnDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseConditionalExpressionForReturnDiagnosticAnalyzer - : AbstractUseConditionalExpressionForReturnDiagnosticAnalyzer + public CSharpUseConditionalExpressionForReturnDiagnosticAnalyzer() + : base(new LocalizableResourceString(nameof(CSharpAnalyzersResources.if_statement_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpUseConditionalExpressionForReturnDiagnosticAnalyzer() - : base(new LocalizableResourceString(nameof(CSharpAnalyzersResources.if_statement_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } - - protected override ISyntaxFacts GetSyntaxFacts() - => CSharpSyntaxFacts.Instance; - - protected override bool IsStatementSupported(IOperation statement) - { - // Return statements wrapped in an unsafe, checked or unchecked block are not supported - // because having these enclosing blocks makes it difficult or impossible to convert - // the blocks to expressions - return !IsWrappedByCheckedOrUnsafe(statement); - } - - private static bool IsWrappedByCheckedOrUnsafe(IOperation statement) - { - if (statement is not IReturnOperation { Parent: IBlockOperation block }) - return false; - - if (block.Syntax.Parent is UnsafeStatementSyntax or CheckedStatementSyntax) - return true; + } + + protected override ISyntaxFacts GetSyntaxFacts() + => CSharpSyntaxFacts.Instance; + protected override bool IsStatementSupported(IOperation statement) + { + // Return statements wrapped in an unsafe, checked or unchecked block are not supported + // because having these enclosing blocks makes it difficult or impossible to convert + // the blocks to expressions + return !IsWrappedByCheckedOrUnsafe(statement); + } + + private static bool IsWrappedByCheckedOrUnsafe(IOperation statement) + { + if (statement is not IReturnOperation { Parent: IBlockOperation block }) return false; - } + + if (block.Syntax.Parent is UnsafeStatementSyntax or CheckedStatementSyntax) + return true; + + return false; } } diff --git a/src/Analyzers/CSharp/Analyzers/UseDeconstruction/CSharpUseDeconstructionDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseDeconstruction/CSharpUseDeconstructionDiagnosticAnalyzer.cs index 8cc9f6bdca879..7dbbf2d82a353 100644 --- a/src/Analyzers/CSharp/Analyzers/UseDeconstruction/CSharpUseDeconstructionDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseDeconstruction/CSharpUseDeconstructionDiagnosticAnalyzer.cs @@ -16,279 +16,280 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseDeconstruction +namespace Microsoft.CodeAnalysis.CSharp.UseDeconstruction; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpUseDeconstructionDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpUseDeconstructionDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public CSharpUseDeconstructionDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseDeconstructionDiagnosticId, + EnforceOnBuildValues.UseDeconstruction, + CSharpCodeStyleOptions.PreferDeconstructedVariableDeclaration, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Deconstruct_variable_declaration), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Variable_declaration_can_be_deconstructed), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpUseDeconstructionDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseDeconstructionDiagnosticId, - EnforceOnBuildValues.UseDeconstruction, - CSharpCodeStyleOptions.PreferDeconstructedVariableDeclaration, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Deconstruct_variable_declaration), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Variable_declaration_can_be_deconstructed), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } - - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + } - protected override void InitializeWorker(AnalysisContext context) - { - context.RegisterSyntaxNodeAction(AnalyzeNode, - SyntaxKind.VariableDeclaration, SyntaxKind.ForEachStatement); - } + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - private void AnalyzeNode(SyntaxNodeAnalysisContext context) - { - var option = context.GetCSharpAnalyzerOptions().PreferDeconstructedVariableDeclaration; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeNode, + SyntaxKind.VariableDeclaration, SyntaxKind.ForEachStatement); + } - switch (context.Node) - { - case VariableDeclarationSyntax variableDeclaration: - AnalyzeVariableDeclaration(context, variableDeclaration, option.Notification); - return; - case ForEachStatementSyntax forEachStatement: - AnalyzeForEachStatement(context, forEachStatement, option.Notification); - return; - } - } + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var option = context.GetCSharpAnalyzerOptions().PreferDeconstructedVariableDeclaration; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; - private void AnalyzeVariableDeclaration( - SyntaxNodeAnalysisContext context, VariableDeclarationSyntax variableDeclaration, NotificationOption2 notificationOption) + switch (context.Node) { - if (!TryAnalyzeVariableDeclaration(context.SemanticModel, variableDeclaration, out _, out _, context.CancellationToken)) + case VariableDeclarationSyntax variableDeclaration: + AnalyzeVariableDeclaration(context, variableDeclaration, option.Notification); return; - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - variableDeclaration.Variables[0].Identifier.GetLocation(), - notificationOption, - additionalLocations: null, - properties: null)); - } - - private void AnalyzeForEachStatement( - SyntaxNodeAnalysisContext context, ForEachStatementSyntax forEachStatement, NotificationOption2 notificationOption) - { - if (!TryAnalyzeForEachStatement(context.SemanticModel, forEachStatement, out _, out _, context.CancellationToken)) + case ForEachStatementSyntax forEachStatement: + AnalyzeForEachStatement(context, forEachStatement, option.Notification); return; - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - forEachStatement.Identifier.GetLocation(), - notificationOption, - additionalLocations: null, - properties: null)); } + } - public static bool TryAnalyzeVariableDeclaration( - SemanticModel semanticModel, - VariableDeclarationSyntax variableDeclaration, - [NotNullWhen(true)] out INamedTypeSymbol? tupleType, - out ImmutableArray memberAccessExpressions, - CancellationToken cancellationToken) - { - tupleType = null; - memberAccessExpressions = default; - - // Only support code of the form: - // - // var t = ...; or - // (T1 e1, ..., TN eN) t = ... - if (variableDeclaration is not { Parent: LocalDeclarationStatementSyntax localDeclaration, Variables: [{ Initializer.Value: { } initializerValue } declarator] }) - return false; + private void AnalyzeVariableDeclaration( + SyntaxNodeAnalysisContext context, VariableDeclarationSyntax variableDeclaration, NotificationOption2 notificationOption) + { + if (!TryAnalyzeVariableDeclaration(context.SemanticModel, variableDeclaration, out _, out _, context.CancellationToken)) + return; + + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + variableDeclaration.Variables[0].Identifier.GetLocation(), + notificationOption, + context.Options, + additionalLocations: null, + properties: null)); + } - var local = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(declarator, cancellationToken); + private void AnalyzeForEachStatement( + SyntaxNodeAnalysisContext context, ForEachStatementSyntax forEachStatement, NotificationOption2 notificationOption) + { + if (!TryAnalyzeForEachStatement(context.SemanticModel, forEachStatement, out _, out _, context.CancellationToken)) + return; + + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + forEachStatement.Identifier.GetLocation(), + notificationOption, + context.Options, + additionalLocations: null, + properties: null)); + } - var initializerConversion = semanticModel.GetConversion(initializerValue, cancellationToken); + public static bool TryAnalyzeVariableDeclaration( + SemanticModel semanticModel, + VariableDeclarationSyntax variableDeclaration, + [NotNullWhen(true)] out INamedTypeSymbol? tupleType, + out ImmutableArray memberAccessExpressions, + CancellationToken cancellationToken) + { + tupleType = null; + memberAccessExpressions = default; + + // Only support code of the form: + // + // var t = ...; or + // (T1 e1, ..., TN eN) t = ... + if (variableDeclaration is not { Parent: LocalDeclarationStatementSyntax localDeclaration, Variables: [{ Initializer.Value: { } initializerValue } declarator] }) + return false; - var searchScope = localDeclaration.Parent is GlobalStatementSyntax globalStatement - ? globalStatement.GetRequiredParent() - : localDeclaration.GetRequiredParent(); + var local = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(declarator, cancellationToken); - return TryAnalyze( - semanticModel, local, variableDeclaration.Type, declarator.Identifier, initializerConversion, searchScope, - out tupleType, out memberAccessExpressions, cancellationToken); - } + var initializerConversion = semanticModel.GetConversion(initializerValue, cancellationToken); - public static bool TryAnalyzeForEachStatement( - SemanticModel semanticModel, - ForEachStatementSyntax forEachStatement, - [NotNullWhen(true)] out INamedTypeSymbol? tupleType, - out ImmutableArray memberAccessExpressions, - CancellationToken cancellationToken) - { - var local = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(forEachStatement, cancellationToken); - var elementConversion = semanticModel.GetForEachStatementInfo(forEachStatement).ElementConversion; + var searchScope = localDeclaration.Parent is GlobalStatementSyntax globalStatement + ? globalStatement.GetRequiredParent() + : localDeclaration.GetRequiredParent(); - return TryAnalyze( - semanticModel, local, forEachStatement.Type, forEachStatement.Identifier, elementConversion, - forEachStatement, out tupleType, out memberAccessExpressions, cancellationToken); - } + return TryAnalyze( + semanticModel, local, variableDeclaration.Type, declarator.Identifier, initializerConversion, searchScope, + out tupleType, out memberAccessExpressions, cancellationToken); + } - private static bool TryAnalyze( - SemanticModel semanticModel, - ILocalSymbol local, - TypeSyntax typeNode, - SyntaxToken identifier, - Conversion conversion, - SyntaxNode searchScope, - [NotNullWhen(true)] out INamedTypeSymbol? tupleType, - out ImmutableArray memberAccessExpressions, - CancellationToken cancellationToken) - { - tupleType = null; - memberAccessExpressions = default; + public static bool TryAnalyzeForEachStatement( + SemanticModel semanticModel, + ForEachStatementSyntax forEachStatement, + [NotNullWhen(true)] out INamedTypeSymbol? tupleType, + out ImmutableArray memberAccessExpressions, + CancellationToken cancellationToken) + { + var local = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(forEachStatement, cancellationToken); + var elementConversion = semanticModel.GetForEachStatementInfo(forEachStatement).ElementConversion; - if (identifier.IsMissing) - return false; + return TryAnalyze( + semanticModel, local, forEachStatement.Type, forEachStatement.Identifier, elementConversion, + forEachStatement, out tupleType, out memberAccessExpressions, cancellationToken); + } - if (!IsViableTupleTypeSyntax(typeNode)) - return false; + private static bool TryAnalyze( + SemanticModel semanticModel, + ILocalSymbol local, + TypeSyntax typeNode, + SyntaxToken identifier, + Conversion conversion, + SyntaxNode searchScope, + [NotNullWhen(true)] out INamedTypeSymbol? tupleType, + out ImmutableArray memberAccessExpressions, + CancellationToken cancellationToken) + { + tupleType = null; + memberAccessExpressions = default; - if (conversion.Exists && - !conversion.IsIdentity && - !conversion.IsTupleConversion && - !conversion.IsTupleLiteralConversion) - { - // If there is any other conversion, we bail out because the source type might not be a tuple - // or it is a tuple but only thanks to target type inference, which won't occur in a deconstruction. - // Interesting case that illustrates this is initialization with a default literal: - // (int a, int b) t = default; - // This is classified as conversion.IsNullLiteral. - return false; - } + if (identifier.IsMissing) + return false; - var type = semanticModel.GetTypeInfo(typeNode, cancellationToken).Type; - if (type is not INamedTypeSymbol { IsTupleType: true, TupleElements.Length: >= 2 } tupleTypeOpt) - return false; + if (!IsViableTupleTypeSyntax(typeNode)) + return false; - tupleType = tupleTypeOpt; - // All tuple elements must have been explicitly provided by the user. - foreach (var element in tupleType.TupleElements) - { - if (element.IsImplicitlyDeclared) - return false; - } + if (conversion.Exists && + !conversion.IsIdentity && + !conversion.IsTupleConversion && + !conversion.IsTupleLiteralConversion) + { + // If there is any other conversion, we bail out because the source type might not be a tuple + // or it is a tuple but only thanks to target type inference, which won't occur in a deconstruction. + // Interesting case that illustrates this is initialization with a default literal: + // (int a, int b) t = default; + // This is classified as conversion.IsNullLiteral. + return false; + } - using var _ = ArrayBuilder.GetInstance(out var references); + var type = semanticModel.GetTypeInfo(typeNode, cancellationToken).Type; + if (type is not INamedTypeSymbol { IsTupleType: true, TupleElements.Length: >= 2 } tupleTypeOpt) + return false; - // If the user actually uses the tuple local for anything other than accessing - // fields off of it, then we can't deconstruct this tuple into locals. - if (!OnlyUsedToAccessTupleFields( - semanticModel, searchScope, local, references, cancellationToken)) - { + tupleType = tupleTypeOpt; + // All tuple elements must have been explicitly provided by the user. + foreach (var element in tupleType.TupleElements) + { + if (element.IsImplicitlyDeclared) return false; - } + } - // Can only deconstruct the tuple if the names we introduce won't collide - // with anything else in scope (either outside, or inside the method). - if (AnyTupleFieldNamesCollideWithExistingNames( - semanticModel, tupleType, searchScope, cancellationToken)) - { - return false; - } + using var _ = ArrayBuilder.GetInstance(out var references); - memberAccessExpressions = references.ToImmutableAndClear(); - return true; + // If the user actually uses the tuple local for anything other than accessing + // fields off of it, then we can't deconstruct this tuple into locals. + if (!OnlyUsedToAccessTupleFields( + semanticModel, searchScope, local, references, cancellationToken)) + { + return false; } - private static bool AnyTupleFieldNamesCollideWithExistingNames( - SemanticModel semanticModel, INamedTypeSymbol tupleType, - SyntaxNode container, CancellationToken cancellationToken) + // Can only deconstruct the tuple if the names we introduce won't collide + // with anything else in scope (either outside, or inside the method). + if (AnyTupleFieldNamesCollideWithExistingNames( + semanticModel, tupleType, searchScope, cancellationToken)) { - var existingSymbols = GetExistingSymbols(semanticModel, container, cancellationToken); + return false; + } - var reservedNames = semanticModel.LookupSymbols(container.SpanStart) - .Select(s => s.Name) - .Concat(existingSymbols.Select(s => s.Name)) - .ToSet(); + memberAccessExpressions = references.ToImmutableAndClear(); + return true; + } - foreach (var element in tupleType.TupleElements) - { - if (reservedNames.Contains(element.Name)) - return true; - } + private static bool AnyTupleFieldNamesCollideWithExistingNames( + SemanticModel semanticModel, INamedTypeSymbol tupleType, + SyntaxNode container, CancellationToken cancellationToken) + { + var existingSymbols = GetExistingSymbols(semanticModel, container, cancellationToken); - return false; - } + var reservedNames = semanticModel.LookupSymbols(container.SpanStart) + .Select(s => s.Name) + .Concat(existingSymbols.Select(s => s.Name)) + .ToSet(); - private static bool IsViableTupleTypeSyntax(TypeSyntax type) + foreach (var element in tupleType.TupleElements) { - if (type.IsVar) - { - // 'var t' can be converted to 'var (x, y, z)' + if (reservedNames.Contains(element.Name)) return true; - } + } - if (type is TupleTypeSyntax tupleType) - { - // '(int x, int y) t' can be converted to '(int x, int y)'. So all the elements - // need names. + return false; + } - foreach (var element in tupleType.Elements) - { - if (element.Identifier.IsKind(SyntaxKind.None)) - return false; - } + private static bool IsViableTupleTypeSyntax(TypeSyntax type) + { + if (type.IsVar) + { + // 'var t' can be converted to 'var (x, y, z)' + return true; + } - return true; + if (type is TupleTypeSyntax tupleType) + { + // '(int x, int y) t' can be converted to '(int x, int y)'. So all the elements + // need names. + + foreach (var element in tupleType.Elements) + { + if (element.Identifier.IsKind(SyntaxKind.None)) + return false; } - return false; + return true; } - private static bool OnlyUsedToAccessTupleFields( - SemanticModel semanticModel, SyntaxNode searchScope, ILocalSymbol local, - ArrayBuilder memberAccessLocations, CancellationToken cancellationToken) - { - var localName = local.Name; + return false; + } - foreach (var identifierName in searchScope.DescendantNodes().OfType()) + private static bool OnlyUsedToAccessTupleFields( + SemanticModel semanticModel, SyntaxNode searchScope, ILocalSymbol local, + ArrayBuilder memberAccessLocations, CancellationToken cancellationToken) + { + var localName = local.Name; + + foreach (var identifierName in searchScope.DescendantNodes().OfType()) + { + if (identifierName.Identifier.ValueText == localName) { - if (identifierName.Identifier.ValueText == localName) + var symbol = semanticModel.GetSymbolInfo(identifierName, cancellationToken).GetAnySymbol(); + if (local.Equals(symbol)) { - var symbol = semanticModel.GetSymbolInfo(identifierName, cancellationToken).GetAnySymbol(); - if (local.Equals(symbol)) + if (identifierName.Parent is not MemberAccessExpressionSyntax memberAccess) { - if (identifierName.Parent is not MemberAccessExpressionSyntax memberAccess) - { - // We referenced the local in a location where we're not accessing a - // field off of it. i.e. Console.WriteLine(tupleLocal); - return false; - } - - var member = semanticModel.GetSymbolInfo(memberAccess, cancellationToken).GetAnySymbol(); - if (member is not IFieldSymbol field) - { - // Accessed some non-field member of it (like .ToString()). - return false; - } - - if (field.IsImplicitlyDeclared) - { - // They're referring to .Item1-.ItemN. We can't update this to refer to the local - return false; - } - - memberAccessLocations.Add(memberAccess); + // We referenced the local in a location where we're not accessing a + // field off of it. i.e. Console.WriteLine(tupleLocal); + return false; } + + var member = semanticModel.GetSymbolInfo(memberAccess, cancellationToken).GetAnySymbol(); + if (member is not IFieldSymbol field) + { + // Accessed some non-field member of it (like .ToString()). + return false; + } + + if (field.IsImplicitlyDeclared) + { + // They're referring to .Item1-.ItemN. We can't update this to refer to the local + return false; + } + + memberAccessLocations.Add(memberAccess); } } - - return true; } - private static IEnumerable GetExistingSymbols( - SemanticModel semanticModel, SyntaxNode container, CancellationToken cancellationToken) - { - // Ignore an anonymous type property. It's ok if they have a name that - // matches the name of the local we're introducing. - return semanticModel.GetAllDeclaredSymbols(container, cancellationToken) - .Where(s => !s.IsAnonymousTypeProperty() && !s.IsTupleField()); - } + return true; + } + + private static IEnumerable GetExistingSymbols( + SemanticModel semanticModel, SyntaxNode container, CancellationToken cancellationToken) + { + // Ignore an anonymous type property. It's ok if they have a name that + // matches the name of the local we're introducing. + return semanticModel.GetAllDeclaredSymbols(container, cancellationToken) + .Where(s => !s.IsAnonymousTypeProperty() && !s.IsTupleField()); } } diff --git a/src/Analyzers/CSharp/Analyzers/UseDefaultLiteral/CSharpUseDefaultLiteralDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseDefaultLiteral/CSharpUseDefaultLiteralDiagnosticAnalyzer.cs index 9504adc9fda11..ecc03d73a3d9f 100644 --- a/src/Analyzers/CSharp/Analyzers/UseDefaultLiteral/CSharpUseDefaultLiteralDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseDefaultLiteral/CSharpUseDefaultLiteralDiagnosticAnalyzer.cs @@ -10,50 +10,50 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.UseDefaultLiteral +namespace Microsoft.CodeAnalysis.CSharp.UseDefaultLiteral; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseDefaultLiteralDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseDefaultLiteralDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + public CSharpUseDefaultLiteralDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseDefaultLiteralDiagnosticId, + EnforceOnBuildValues.UseDefaultLiteral, + CSharpCodeStyleOptions.PreferSimpleDefaultExpression, + fadingOption: null, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Simplify_default_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.default_expression_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + { + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.DefaultExpression); + + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) { - public CSharpUseDefaultLiteralDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseDefaultLiteralDiagnosticId, - EnforceOnBuildValues.UseDefaultLiteral, - CSharpCodeStyleOptions.PreferSimpleDefaultExpression, - fadingOption: null, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Simplify_default_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.default_expression_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } - - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.DefaultExpression); - - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) - { - var preference = context.GetCSharpAnalyzerOptions().PreferSimpleDefaultExpression; - if (ShouldSkipAnalysis(context, preference.Notification)) - return; - - var cancellationToken = context.CancellationToken; - var syntaxTree = context.Node.SyntaxTree; - var parseOptions = (CSharpParseOptions)syntaxTree.Options; - var defaultExpression = (DefaultExpressionSyntax)context.Node; - if (!defaultExpression.CanReplaceWithDefaultLiteral(parseOptions, preference.Value, context.SemanticModel, cancellationToken)) - return; - - var fadeSpan = TextSpan.FromBounds(defaultExpression.OpenParenToken.SpanStart, defaultExpression.CloseParenToken.Span.End); - - // Create a normal diagnostic that covers the entire default expression. - context.ReportDiagnostic( - DiagnosticHelper.CreateWithLocationTags( - Descriptor, - defaultExpression.GetLocation(), - preference.Notification, - additionalLocations: [], - additionalUnnecessaryLocations: [defaultExpression.SyntaxTree.GetLocation(fadeSpan)])); - } + var preference = context.GetCSharpAnalyzerOptions().PreferSimpleDefaultExpression; + if (ShouldSkipAnalysis(context, preference.Notification)) + return; + + var cancellationToken = context.CancellationToken; + var syntaxTree = context.Node.SyntaxTree; + var parseOptions = (CSharpParseOptions)syntaxTree.Options; + var defaultExpression = (DefaultExpressionSyntax)context.Node; + if (!defaultExpression.CanReplaceWithDefaultLiteral(parseOptions, preference.Value, context.SemanticModel, cancellationToken)) + return; + + var fadeSpan = TextSpan.FromBounds(defaultExpression.OpenParenToken.SpanStart, defaultExpression.CloseParenToken.Span.End); + + // Create a normal diagnostic that covers the entire default expression. + context.ReportDiagnostic( + DiagnosticHelper.CreateWithLocationTags( + Descriptor, + defaultExpression.GetLocation(), + preference.Notification, + context.Options, + additionalLocations: [], + additionalUnnecessaryLocations: [defaultExpression.SyntaxTree.GetLocation(fadeSpan)])); } } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForAccessorsHelper.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForAccessorsHelper.cs index 0df4cfd543305..673ba458f2b97 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForAccessorsHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForAccessorsHelper.cs @@ -9,51 +9,50 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +internal class UseExpressionBodyForAccessorsHelper : + UseExpressionBodyHelper { - internal class UseExpressionBodyForAccessorsHelper : - UseExpressionBodyHelper + public static readonly UseExpressionBodyForAccessorsHelper Instance = new(); + + private UseExpressionBodyForAccessorsHelper() + : base(IDEDiagnosticIds.UseExpressionBodyForAccessorsDiagnosticId, + EnforceOnBuildValues.UseExpressionBodyForAccessors, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_accessor), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_accessor), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + CSharpCodeStyleOptions.PreferExpressionBodiedAccessors, + [ + SyntaxKind.GetAccessorDeclaration, + SyntaxKind.SetAccessorDeclaration, + SyntaxKind.InitAccessorDeclaration, + SyntaxKind.AddAccessorDeclaration, + SyntaxKind.RemoveAccessorDeclaration, + ]) { - public static readonly UseExpressionBodyForAccessorsHelper Instance = new(); - - private UseExpressionBodyForAccessorsHelper() - : base(IDEDiagnosticIds.UseExpressionBodyForAccessorsDiagnosticId, - EnforceOnBuildValues.UseExpressionBodyForAccessors, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_accessor), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_accessor), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - CSharpCodeStyleOptions.PreferExpressionBodiedAccessors, - [ - SyntaxKind.GetAccessorDeclaration, - SyntaxKind.SetAccessorDeclaration, - SyntaxKind.InitAccessorDeclaration, - SyntaxKind.AddAccessorDeclaration, - SyntaxKind.RemoveAccessorDeclaration, - ]) - { - } - - public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) - => options.PreferExpressionBodiedAccessors; - - protected override BlockSyntax? GetBody(AccessorDeclarationSyntax declaration) - => declaration.Body; - - protected override ArrowExpressionClauseSyntax? GetExpressionBody(AccessorDeclarationSyntax declaration) - => declaration.ExpressionBody; - - protected override SyntaxToken GetSemicolonToken(AccessorDeclarationSyntax declaration) - => declaration.SemicolonToken; - - protected override AccessorDeclarationSyntax WithSemicolonToken(AccessorDeclarationSyntax declaration, SyntaxToken token) - => declaration.WithSemicolonToken(token); - - protected override AccessorDeclarationSyntax WithExpressionBody(AccessorDeclarationSyntax declaration, ArrowExpressionClauseSyntax? expressionBody) - => declaration.WithExpressionBody(expressionBody); - - protected override AccessorDeclarationSyntax WithBody(AccessorDeclarationSyntax declaration, BlockSyntax? body) - => declaration.WithBody(body); - - protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, AccessorDeclarationSyntax declaration) - => declaration.IsKind(SyntaxKind.GetAccessorDeclaration); } + + public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) + => options.PreferExpressionBodiedAccessors; + + protected override BlockSyntax? GetBody(AccessorDeclarationSyntax declaration) + => declaration.Body; + + protected override ArrowExpressionClauseSyntax? GetExpressionBody(AccessorDeclarationSyntax declaration) + => declaration.ExpressionBody; + + protected override SyntaxToken GetSemicolonToken(AccessorDeclarationSyntax declaration) + => declaration.SemicolonToken; + + protected override AccessorDeclarationSyntax WithSemicolonToken(AccessorDeclarationSyntax declaration, SyntaxToken token) + => declaration.WithSemicolonToken(token); + + protected override AccessorDeclarationSyntax WithExpressionBody(AccessorDeclarationSyntax declaration, ArrowExpressionClauseSyntax? expressionBody) + => declaration.WithExpressionBody(expressionBody); + + protected override AccessorDeclarationSyntax WithBody(AccessorDeclarationSyntax declaration, BlockSyntax? body) + => declaration.WithBody(body); + + protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, AccessorDeclarationSyntax declaration) + => declaration.IsKind(SyntaxKind.GetAccessorDeclaration); } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForConstructorsHelper.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForConstructorsHelper.cs index 37c6708a704e0..cedba7091590f 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForConstructorsHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForConstructorsHelper.cs @@ -9,44 +9,43 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +internal class UseExpressionBodyForConstructorsHelper : + UseExpressionBodyHelper { - internal class UseExpressionBodyForConstructorsHelper : - UseExpressionBodyHelper + public static readonly UseExpressionBodyForConstructorsHelper Instance = new(); + + private UseExpressionBodyForConstructorsHelper() + : base(IDEDiagnosticIds.UseExpressionBodyForConstructorsDiagnosticId, + EnforceOnBuildValues.UseExpressionBodyForConstructors, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_constructor), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_constructor), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + CSharpCodeStyleOptions.PreferExpressionBodiedConstructors, + [SyntaxKind.ConstructorDeclaration]) { - public static readonly UseExpressionBodyForConstructorsHelper Instance = new(); - - private UseExpressionBodyForConstructorsHelper() - : base(IDEDiagnosticIds.UseExpressionBodyForConstructorsDiagnosticId, - EnforceOnBuildValues.UseExpressionBodyForConstructors, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_constructor), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_constructor), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - CSharpCodeStyleOptions.PreferExpressionBodiedConstructors, - [SyntaxKind.ConstructorDeclaration]) - { - } + } - public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) - => options.PreferExpressionBodiedConstructors; + public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) + => options.PreferExpressionBodiedConstructors; - protected override BlockSyntax? GetBody(ConstructorDeclarationSyntax declaration) - => declaration.Body; + protected override BlockSyntax? GetBody(ConstructorDeclarationSyntax declaration) + => declaration.Body; - protected override ArrowExpressionClauseSyntax? GetExpressionBody(ConstructorDeclarationSyntax declaration) - => declaration.ExpressionBody; + protected override ArrowExpressionClauseSyntax? GetExpressionBody(ConstructorDeclarationSyntax declaration) + => declaration.ExpressionBody; - protected override SyntaxToken GetSemicolonToken(ConstructorDeclarationSyntax declaration) - => declaration.SemicolonToken; + protected override SyntaxToken GetSemicolonToken(ConstructorDeclarationSyntax declaration) + => declaration.SemicolonToken; - protected override ConstructorDeclarationSyntax WithSemicolonToken(ConstructorDeclarationSyntax declaration, SyntaxToken token) - => declaration.WithSemicolonToken(token); + protected override ConstructorDeclarationSyntax WithSemicolonToken(ConstructorDeclarationSyntax declaration, SyntaxToken token) + => declaration.WithSemicolonToken(token); - protected override ConstructorDeclarationSyntax WithExpressionBody(ConstructorDeclarationSyntax declaration, ArrowExpressionClauseSyntax? expressionBody) - => declaration.WithExpressionBody(expressionBody); + protected override ConstructorDeclarationSyntax WithExpressionBody(ConstructorDeclarationSyntax declaration, ArrowExpressionClauseSyntax? expressionBody) + => declaration.WithExpressionBody(expressionBody); - protected override ConstructorDeclarationSyntax WithBody(ConstructorDeclarationSyntax declaration, BlockSyntax? body) - => declaration.WithBody(body); + protected override ConstructorDeclarationSyntax WithBody(ConstructorDeclarationSyntax declaration, BlockSyntax? body) + => declaration.WithBody(body); - protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, ConstructorDeclarationSyntax declaration) => false; - } + protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, ConstructorDeclarationSyntax declaration) => false; } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForConversionOperatorsHelper.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForConversionOperatorsHelper.cs index d4bf1b7e38736..d963bbcd567ec 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForConversionOperatorsHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForConversionOperatorsHelper.cs @@ -9,45 +9,44 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +internal class UseExpressionBodyForConversionOperatorsHelper : + UseExpressionBodyHelper { - internal class UseExpressionBodyForConversionOperatorsHelper : - UseExpressionBodyHelper + public static readonly UseExpressionBodyForConversionOperatorsHelper Instance = new(); + + private UseExpressionBodyForConversionOperatorsHelper() + : base(IDEDiagnosticIds.UseExpressionBodyForConversionOperatorsDiagnosticId, + EnforceOnBuildValues.UseExpressionBodyForConversionOperators, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_conversion_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_conversion_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + CSharpCodeStyleOptions.PreferExpressionBodiedOperators, + [SyntaxKind.ConversionOperatorDeclaration]) { - public static readonly UseExpressionBodyForConversionOperatorsHelper Instance = new(); - - private UseExpressionBodyForConversionOperatorsHelper() - : base(IDEDiagnosticIds.UseExpressionBodyForConversionOperatorsDiagnosticId, - EnforceOnBuildValues.UseExpressionBodyForConversionOperators, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_conversion_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_conversion_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - CSharpCodeStyleOptions.PreferExpressionBodiedOperators, - [SyntaxKind.ConversionOperatorDeclaration]) - { - } + } - public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) - => options.PreferExpressionBodiedOperators; + public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) + => options.PreferExpressionBodiedOperators; - protected override BlockSyntax? GetBody(ConversionOperatorDeclarationSyntax declaration) - => declaration.Body; + protected override BlockSyntax? GetBody(ConversionOperatorDeclarationSyntax declaration) + => declaration.Body; - protected override ArrowExpressionClauseSyntax? GetExpressionBody(ConversionOperatorDeclarationSyntax declaration) - => declaration.ExpressionBody; + protected override ArrowExpressionClauseSyntax? GetExpressionBody(ConversionOperatorDeclarationSyntax declaration) + => declaration.ExpressionBody; - protected override SyntaxToken GetSemicolonToken(ConversionOperatorDeclarationSyntax declaration) - => declaration.SemicolonToken; + protected override SyntaxToken GetSemicolonToken(ConversionOperatorDeclarationSyntax declaration) + => declaration.SemicolonToken; - protected override ConversionOperatorDeclarationSyntax WithSemicolonToken(ConversionOperatorDeclarationSyntax declaration, SyntaxToken token) - => declaration.WithSemicolonToken(token); + protected override ConversionOperatorDeclarationSyntax WithSemicolonToken(ConversionOperatorDeclarationSyntax declaration, SyntaxToken token) + => declaration.WithSemicolonToken(token); - protected override ConversionOperatorDeclarationSyntax WithExpressionBody(ConversionOperatorDeclarationSyntax declaration, ArrowExpressionClauseSyntax? expressionBody) - => declaration.WithExpressionBody(expressionBody); + protected override ConversionOperatorDeclarationSyntax WithExpressionBody(ConversionOperatorDeclarationSyntax declaration, ArrowExpressionClauseSyntax? expressionBody) + => declaration.WithExpressionBody(expressionBody); - protected override ConversionOperatorDeclarationSyntax WithBody(ConversionOperatorDeclarationSyntax declaration, BlockSyntax? body) - => declaration.WithBody(body); + protected override ConversionOperatorDeclarationSyntax WithBody(ConversionOperatorDeclarationSyntax declaration, BlockSyntax? body) + => declaration.WithBody(body); - protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, ConversionOperatorDeclarationSyntax declaration) - => true; - } + protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, ConversionOperatorDeclarationSyntax declaration) + => true; } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForIndexersHelper.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForIndexersHelper.cs index e9a274b5fbfb3..1037084de70a6 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForIndexersHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForIndexersHelper.cs @@ -13,79 +13,78 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +internal class UseExpressionBodyForIndexersHelper : + UseExpressionBodyHelper { - internal class UseExpressionBodyForIndexersHelper : - UseExpressionBodyHelper + public static readonly UseExpressionBodyForIndexersHelper Instance = new(); + + private UseExpressionBodyForIndexersHelper() + : base(IDEDiagnosticIds.UseExpressionBodyForIndexersDiagnosticId, + EnforceOnBuildValues.UseExpressionBodyForIndexers, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_indexer), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_indexer), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + CSharpCodeStyleOptions.PreferExpressionBodiedIndexers, + [SyntaxKind.IndexerDeclaration]) { - public static readonly UseExpressionBodyForIndexersHelper Instance = new(); - - private UseExpressionBodyForIndexersHelper() - : base(IDEDiagnosticIds.UseExpressionBodyForIndexersDiagnosticId, - EnforceOnBuildValues.UseExpressionBodyForIndexers, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_indexer), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_indexer), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - CSharpCodeStyleOptions.PreferExpressionBodiedIndexers, - [SyntaxKind.IndexerDeclaration]) - { - } + } - public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) - => options.PreferExpressionBodiedIndexers; + public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) + => options.PreferExpressionBodiedIndexers; - protected override BlockSyntax GetBody(IndexerDeclarationSyntax declaration) - => GetBodyFromSingleGetAccessor(declaration.AccessorList); + protected override BlockSyntax GetBody(IndexerDeclarationSyntax declaration) + => GetBodyFromSingleGetAccessor(declaration.AccessorList); - protected override ArrowExpressionClauseSyntax GetExpressionBody(IndexerDeclarationSyntax declaration) - => declaration.ExpressionBody; + protected override ArrowExpressionClauseSyntax GetExpressionBody(IndexerDeclarationSyntax declaration) + => declaration.ExpressionBody; - protected override SyntaxToken GetSemicolonToken(IndexerDeclarationSyntax declaration) - => declaration.SemicolonToken; + protected override SyntaxToken GetSemicolonToken(IndexerDeclarationSyntax declaration) + => declaration.SemicolonToken; - protected override IndexerDeclarationSyntax WithSemicolonToken(IndexerDeclarationSyntax declaration, SyntaxToken token) - => declaration.WithSemicolonToken(token); + protected override IndexerDeclarationSyntax WithSemicolonToken(IndexerDeclarationSyntax declaration, SyntaxToken token) + => declaration.WithSemicolonToken(token); - protected override IndexerDeclarationSyntax WithExpressionBody(IndexerDeclarationSyntax declaration, ArrowExpressionClauseSyntax expressionBody) - => declaration.WithExpressionBody(expressionBody); + protected override IndexerDeclarationSyntax WithExpressionBody(IndexerDeclarationSyntax declaration, ArrowExpressionClauseSyntax expressionBody) + => declaration.WithExpressionBody(expressionBody); - protected override IndexerDeclarationSyntax WithAccessorList(IndexerDeclarationSyntax declaration, AccessorListSyntax accessorList) - => declaration.WithAccessorList(accessorList); + protected override IndexerDeclarationSyntax WithAccessorList(IndexerDeclarationSyntax declaration, AccessorListSyntax accessorList) + => declaration.WithAccessorList(accessorList); - protected override IndexerDeclarationSyntax WithBody(IndexerDeclarationSyntax declaration, BlockSyntax body) + protected override IndexerDeclarationSyntax WithBody(IndexerDeclarationSyntax declaration, BlockSyntax body) + { + if (body == null) { - if (body == null) - { - return declaration.WithAccessorList(null); - } - - throw new InvalidOperationException(); + return declaration.WithAccessorList(null); } - protected override IndexerDeclarationSyntax WithGenerateBody(SemanticModel semanticModel, IndexerDeclarationSyntax declaration) - => WithAccessorList(semanticModel, declaration); + throw new InvalidOperationException(); + } + + protected override IndexerDeclarationSyntax WithGenerateBody(SemanticModel semanticModel, IndexerDeclarationSyntax declaration) + => WithAccessorList(semanticModel, declaration); - protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, IndexerDeclarationSyntax declaration) => true; + protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, IndexerDeclarationSyntax declaration) => true; - protected override bool TryConvertToExpressionBody( - IndexerDeclarationSyntax declaration, - ExpressionBodyPreference conversionPreference, - CancellationToken cancellationToken, - out ArrowExpressionClauseSyntax arrowExpression, - out SyntaxToken semicolonToken) - { - return TryConvertToExpressionBodyForBaseProperty(declaration, conversionPreference, cancellationToken, out arrowExpression, out semicolonToken); - } + protected override bool TryConvertToExpressionBody( + IndexerDeclarationSyntax declaration, + ExpressionBodyPreference conversionPreference, + CancellationToken cancellationToken, + out ArrowExpressionClauseSyntax arrowExpression, + out SyntaxToken semicolonToken) + { + return TryConvertToExpressionBodyForBaseProperty(declaration, conversionPreference, cancellationToken, out arrowExpression, out semicolonToken); + } - protected override Location GetDiagnosticLocation(IndexerDeclarationSyntax declaration) + protected override Location GetDiagnosticLocation(IndexerDeclarationSyntax declaration) + { + var body = GetBody(declaration); + if (body != null) { - var body = GetBody(declaration); - if (body != null) - { - return body.Statements[0].GetLocation(); - } - - var getAccessor = GetSingleGetAccessor(declaration.AccessorList); - return getAccessor.ExpressionBody.GetLocation(); + return body.Statements[0].GetLocation(); } + + var getAccessor = GetSingleGetAccessor(declaration.AccessorList); + return getAccessor.ExpressionBody.GetLocation(); } } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForLocalFunctionHelper.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForLocalFunctionHelper.cs index a8df4ae7b9968..d09df06b807fb 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForLocalFunctionHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForLocalFunctionHelper.cs @@ -12,58 +12,57 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +internal class UseExpressionBodyForLocalFunctionHelper : + UseExpressionBodyHelper { - internal class UseExpressionBodyForLocalFunctionHelper : - UseExpressionBodyHelper - { - public static readonly UseExpressionBodyForLocalFunctionHelper Instance = new(); + public static readonly UseExpressionBodyForLocalFunctionHelper Instance = new(); - private UseExpressionBodyForLocalFunctionHelper() - : base(IDEDiagnosticIds.UseExpressionBodyForLocalFunctionsDiagnosticId, - EnforceOnBuildValues.UseExpressionBodyForLocalFunctions, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_local_function), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_local_function), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - CSharpCodeStyleOptions.PreferExpressionBodiedLocalFunctions, - [SyntaxKind.LocalFunctionStatement]) - { - } + private UseExpressionBodyForLocalFunctionHelper() + : base(IDEDiagnosticIds.UseExpressionBodyForLocalFunctionsDiagnosticId, + EnforceOnBuildValues.UseExpressionBodyForLocalFunctions, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_local_function), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_local_function), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + CSharpCodeStyleOptions.PreferExpressionBodiedLocalFunctions, + [SyntaxKind.LocalFunctionStatement]) + { + } - public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) - => options.PreferExpressionBodiedLocalFunctions; + public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) + => options.PreferExpressionBodiedLocalFunctions; - protected override BlockSyntax GetBody(LocalFunctionStatementSyntax statement) - => statement.Body; + protected override BlockSyntax GetBody(LocalFunctionStatementSyntax statement) + => statement.Body; - protected override ArrowExpressionClauseSyntax GetExpressionBody(LocalFunctionStatementSyntax statement) - => statement.ExpressionBody; + protected override ArrowExpressionClauseSyntax GetExpressionBody(LocalFunctionStatementSyntax statement) + => statement.ExpressionBody; - protected override SyntaxToken GetSemicolonToken(LocalFunctionStatementSyntax statement) - => statement.SemicolonToken; + protected override SyntaxToken GetSemicolonToken(LocalFunctionStatementSyntax statement) + => statement.SemicolonToken; - protected override LocalFunctionStatementSyntax WithSemicolonToken(LocalFunctionStatementSyntax statement, SyntaxToken token) - => statement.WithSemicolonToken(token); + protected override LocalFunctionStatementSyntax WithSemicolonToken(LocalFunctionStatementSyntax statement, SyntaxToken token) + => statement.WithSemicolonToken(token); - protected override LocalFunctionStatementSyntax WithExpressionBody(LocalFunctionStatementSyntax statement, ArrowExpressionClauseSyntax expressionBody) - => statement.WithExpressionBody(expressionBody); + protected override LocalFunctionStatementSyntax WithExpressionBody(LocalFunctionStatementSyntax statement, ArrowExpressionClauseSyntax expressionBody) + => statement.WithExpressionBody(expressionBody); - protected override LocalFunctionStatementSyntax WithBody(LocalFunctionStatementSyntax statement, BlockSyntax body) - => statement.WithBody(body); + protected override LocalFunctionStatementSyntax WithBody(LocalFunctionStatementSyntax statement, BlockSyntax body) + => statement.WithBody(body); - protected override bool CreateReturnStatementForExpression( - SemanticModel semanticModel, LocalFunctionStatementSyntax statement) + protected override bool CreateReturnStatementForExpression( + SemanticModel semanticModel, LocalFunctionStatementSyntax statement) + { + if (statement.Modifiers.Any(SyntaxKind.AsyncKeyword)) { - if (statement.Modifiers.Any(SyntaxKind.AsyncKeyword)) - { - // if it's 'async TaskLike' (where TaskLike is non-generic) we do *not* want to - // create a return statement. This is just the 'async' version of a 'void' local function. - var symbol = semanticModel.GetDeclaredSymbol(statement); - return symbol is IMethodSymbol methodSymbol && - methodSymbol.ReturnType is INamedTypeSymbol namedType && - namedType.Arity != 0; - } - - return !statement.ReturnType.IsVoid(); + // if it's 'async TaskLike' (where TaskLike is non-generic) we do *not* want to + // create a return statement. This is just the 'async' version of a 'void' local function. + var symbol = semanticModel.GetDeclaredSymbol(statement); + return symbol is IMethodSymbol methodSymbol && + methodSymbol.ReturnType is INamedTypeSymbol namedType && + namedType.Arity != 0; } + + return !statement.ReturnType.IsVoid(); } } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForMethodsHelper.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForMethodsHelper.cs index 824de0a103b5a..f7b57bc77ca5a 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForMethodsHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForMethodsHelper.cs @@ -12,56 +12,55 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +internal class UseExpressionBodyForMethodsHelper : + UseExpressionBodyHelper { - internal class UseExpressionBodyForMethodsHelper : - UseExpressionBodyHelper - { - public static readonly UseExpressionBodyForMethodsHelper Instance = new(); + public static readonly UseExpressionBodyForMethodsHelper Instance = new(); - private UseExpressionBodyForMethodsHelper() - : base(IDEDiagnosticIds.UseExpressionBodyForMethodsDiagnosticId, - EnforceOnBuildValues.UseExpressionBodyForMethods, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_method), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_method), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - CSharpCodeStyleOptions.PreferExpressionBodiedMethods, - [SyntaxKind.MethodDeclaration]) - { - } + private UseExpressionBodyForMethodsHelper() + : base(IDEDiagnosticIds.UseExpressionBodyForMethodsDiagnosticId, + EnforceOnBuildValues.UseExpressionBodyForMethods, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_method), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_method), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + CSharpCodeStyleOptions.PreferExpressionBodiedMethods, + [SyntaxKind.MethodDeclaration]) + { + } - public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) - => options.PreferExpressionBodiedMethods; + public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) + => options.PreferExpressionBodiedMethods; - protected override BlockSyntax GetBody(MethodDeclarationSyntax declaration) - => declaration.Body; + protected override BlockSyntax GetBody(MethodDeclarationSyntax declaration) + => declaration.Body; - protected override ArrowExpressionClauseSyntax GetExpressionBody(MethodDeclarationSyntax declaration) - => declaration.ExpressionBody; + protected override ArrowExpressionClauseSyntax GetExpressionBody(MethodDeclarationSyntax declaration) + => declaration.ExpressionBody; - protected override SyntaxToken GetSemicolonToken(MethodDeclarationSyntax declaration) - => declaration.SemicolonToken; + protected override SyntaxToken GetSemicolonToken(MethodDeclarationSyntax declaration) + => declaration.SemicolonToken; - protected override MethodDeclarationSyntax WithSemicolonToken(MethodDeclarationSyntax declaration, SyntaxToken token) - => declaration.WithSemicolonToken(token); + protected override MethodDeclarationSyntax WithSemicolonToken(MethodDeclarationSyntax declaration, SyntaxToken token) + => declaration.WithSemicolonToken(token); - protected override MethodDeclarationSyntax WithExpressionBody(MethodDeclarationSyntax declaration, ArrowExpressionClauseSyntax expressionBody) - => declaration.WithExpressionBody(expressionBody); + protected override MethodDeclarationSyntax WithExpressionBody(MethodDeclarationSyntax declaration, ArrowExpressionClauseSyntax expressionBody) + => declaration.WithExpressionBody(expressionBody); - protected override MethodDeclarationSyntax WithBody(MethodDeclarationSyntax declaration, BlockSyntax body) - => declaration.WithBody(body); + protected override MethodDeclarationSyntax WithBody(MethodDeclarationSyntax declaration, BlockSyntax body) + => declaration.WithBody(body); - protected override bool CreateReturnStatementForExpression( - SemanticModel semanticModel, MethodDeclarationSyntax declaration) + protected override bool CreateReturnStatementForExpression( + SemanticModel semanticModel, MethodDeclarationSyntax declaration) + { + if (declaration.Modifiers.Any(SyntaxKind.AsyncKeyword)) { - if (declaration.Modifiers.Any(SyntaxKind.AsyncKeyword)) - { - // if it's 'async TaskLike' (where TaskLike is non-generic) we do *not* want to - // create a return statement. This is just the 'async' version of a 'void' method. - var method = semanticModel.GetDeclaredSymbol(declaration); - return method.ReturnType is INamedTypeSymbol namedType && namedType.Arity != 0; - } - - return !declaration.ReturnType.IsVoid(); + // if it's 'async TaskLike' (where TaskLike is non-generic) we do *not* want to + // create a return statement. This is just the 'async' version of a 'void' method. + var method = semanticModel.GetDeclaredSymbol(declaration); + return method.ReturnType is INamedTypeSymbol namedType && namedType.Arity != 0; } + + return !declaration.ReturnType.IsVoid(); } } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForOperatorsHelper.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForOperatorsHelper.cs index 65674133bf5df..dd75fabe5d456 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForOperatorsHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForOperatorsHelper.cs @@ -11,45 +11,44 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +internal class UseExpressionBodyForOperatorsHelper : + UseExpressionBodyHelper { - internal class UseExpressionBodyForOperatorsHelper : - UseExpressionBodyHelper + public static readonly UseExpressionBodyForOperatorsHelper Instance = new(); + + private UseExpressionBodyForOperatorsHelper() + : base(IDEDiagnosticIds.UseExpressionBodyForOperatorsDiagnosticId, + EnforceOnBuildValues.UseExpressionBodyForOperators, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + CSharpCodeStyleOptions.PreferExpressionBodiedOperators, + [SyntaxKind.OperatorDeclaration]) { - public static readonly UseExpressionBodyForOperatorsHelper Instance = new(); - - private UseExpressionBodyForOperatorsHelper() - : base(IDEDiagnosticIds.UseExpressionBodyForOperatorsDiagnosticId, - EnforceOnBuildValues.UseExpressionBodyForOperators, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - CSharpCodeStyleOptions.PreferExpressionBodiedOperators, - [SyntaxKind.OperatorDeclaration]) - { - } + } - public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) - => options.PreferExpressionBodiedOperators; + public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) + => options.PreferExpressionBodiedOperators; - protected override BlockSyntax GetBody(OperatorDeclarationSyntax declaration) - => declaration.Body; + protected override BlockSyntax GetBody(OperatorDeclarationSyntax declaration) + => declaration.Body; - protected override ArrowExpressionClauseSyntax GetExpressionBody(OperatorDeclarationSyntax declaration) - => declaration.ExpressionBody; + protected override ArrowExpressionClauseSyntax GetExpressionBody(OperatorDeclarationSyntax declaration) + => declaration.ExpressionBody; - protected override SyntaxToken GetSemicolonToken(OperatorDeclarationSyntax declaration) - => declaration.SemicolonToken; + protected override SyntaxToken GetSemicolonToken(OperatorDeclarationSyntax declaration) + => declaration.SemicolonToken; - protected override OperatorDeclarationSyntax WithSemicolonToken(OperatorDeclarationSyntax declaration, SyntaxToken token) - => declaration.WithSemicolonToken(token); + protected override OperatorDeclarationSyntax WithSemicolonToken(OperatorDeclarationSyntax declaration, SyntaxToken token) + => declaration.WithSemicolonToken(token); - protected override OperatorDeclarationSyntax WithExpressionBody(OperatorDeclarationSyntax declaration, ArrowExpressionClauseSyntax expressionBody) - => declaration.WithExpressionBody(expressionBody); + protected override OperatorDeclarationSyntax WithExpressionBody(OperatorDeclarationSyntax declaration, ArrowExpressionClauseSyntax expressionBody) + => declaration.WithExpressionBody(expressionBody); - protected override OperatorDeclarationSyntax WithBody(OperatorDeclarationSyntax declaration, BlockSyntax body) - => declaration.WithBody(body); + protected override OperatorDeclarationSyntax WithBody(OperatorDeclarationSyntax declaration, BlockSyntax body) + => declaration.WithBody(body); - protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, OperatorDeclarationSyntax declaration) - => true; - } + protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, OperatorDeclarationSyntax declaration) + => true; } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForPropertiesHelper.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForPropertiesHelper.cs index 7280a5ee05847..5681617779e31 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForPropertiesHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyForPropertiesHelper.cs @@ -13,79 +13,78 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +internal class UseExpressionBodyForPropertiesHelper : + UseExpressionBodyHelper { - internal class UseExpressionBodyForPropertiesHelper : - UseExpressionBodyHelper + public static readonly UseExpressionBodyForPropertiesHelper Instance = new(); + + private UseExpressionBodyForPropertiesHelper() + : base(IDEDiagnosticIds.UseExpressionBodyForPropertiesDiagnosticId, + EnforceOnBuildValues.UseExpressionBodyForProperties, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_property), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_property), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + CSharpCodeStyleOptions.PreferExpressionBodiedProperties, + [SyntaxKind.PropertyDeclaration]) { - public static readonly UseExpressionBodyForPropertiesHelper Instance = new(); - - private UseExpressionBodyForPropertiesHelper() - : base(IDEDiagnosticIds.UseExpressionBodyForPropertiesDiagnosticId, - EnforceOnBuildValues.UseExpressionBodyForProperties, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_property), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_property), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - CSharpCodeStyleOptions.PreferExpressionBodiedProperties, - [SyntaxKind.PropertyDeclaration]) - { - } + } - public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) - => options.PreferExpressionBodiedProperties; + public override CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options) + => options.PreferExpressionBodiedProperties; - protected override BlockSyntax GetBody(PropertyDeclarationSyntax declaration) - => GetBodyFromSingleGetAccessor(declaration.AccessorList); + protected override BlockSyntax GetBody(PropertyDeclarationSyntax declaration) + => GetBodyFromSingleGetAccessor(declaration.AccessorList); - protected override ArrowExpressionClauseSyntax GetExpressionBody(PropertyDeclarationSyntax declaration) - => declaration.ExpressionBody; + protected override ArrowExpressionClauseSyntax GetExpressionBody(PropertyDeclarationSyntax declaration) + => declaration.ExpressionBody; - protected override SyntaxToken GetSemicolonToken(PropertyDeclarationSyntax declaration) - => declaration.SemicolonToken; + protected override SyntaxToken GetSemicolonToken(PropertyDeclarationSyntax declaration) + => declaration.SemicolonToken; - protected override PropertyDeclarationSyntax WithSemicolonToken(PropertyDeclarationSyntax declaration, SyntaxToken token) - => declaration.WithSemicolonToken(token); + protected override PropertyDeclarationSyntax WithSemicolonToken(PropertyDeclarationSyntax declaration, SyntaxToken token) + => declaration.WithSemicolonToken(token); - protected override PropertyDeclarationSyntax WithExpressionBody(PropertyDeclarationSyntax declaration, ArrowExpressionClauseSyntax expressionBody) - => declaration.WithExpressionBody(expressionBody); + protected override PropertyDeclarationSyntax WithExpressionBody(PropertyDeclarationSyntax declaration, ArrowExpressionClauseSyntax expressionBody) + => declaration.WithExpressionBody(expressionBody); - protected override PropertyDeclarationSyntax WithAccessorList(PropertyDeclarationSyntax declaration, AccessorListSyntax accessorListSyntax) - => declaration.WithAccessorList(accessorListSyntax); + protected override PropertyDeclarationSyntax WithAccessorList(PropertyDeclarationSyntax declaration, AccessorListSyntax accessorListSyntax) + => declaration.WithAccessorList(accessorListSyntax); - protected override PropertyDeclarationSyntax WithBody(PropertyDeclarationSyntax declaration, BlockSyntax body) + protected override PropertyDeclarationSyntax WithBody(PropertyDeclarationSyntax declaration, BlockSyntax body) + { + if (body == null) { - if (body == null) - { - return declaration.WithAccessorList(null); - } - - throw new InvalidOperationException(); + return declaration.WithAccessorList(null); } - protected override PropertyDeclarationSyntax WithGenerateBody(SemanticModel semanticModel, PropertyDeclarationSyntax declaration) - => WithAccessorList(semanticModel, declaration); + throw new InvalidOperationException(); + } + + protected override PropertyDeclarationSyntax WithGenerateBody(SemanticModel semanticModel, PropertyDeclarationSyntax declaration) + => WithAccessorList(semanticModel, declaration); - protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, PropertyDeclarationSyntax declaration) => true; + protected override bool CreateReturnStatementForExpression(SemanticModel semanticModel, PropertyDeclarationSyntax declaration) => true; - protected override bool TryConvertToExpressionBody( - PropertyDeclarationSyntax declaration, - ExpressionBodyPreference conversionPreference, - CancellationToken cancellationToken, - out ArrowExpressionClauseSyntax arrowExpression, - out SyntaxToken semicolonToken) - { - return TryConvertToExpressionBodyForBaseProperty(declaration, conversionPreference, cancellationToken, out arrowExpression, out semicolonToken); - } + protected override bool TryConvertToExpressionBody( + PropertyDeclarationSyntax declaration, + ExpressionBodyPreference conversionPreference, + CancellationToken cancellationToken, + out ArrowExpressionClauseSyntax arrowExpression, + out SyntaxToken semicolonToken) + { + return TryConvertToExpressionBodyForBaseProperty(declaration, conversionPreference, cancellationToken, out arrowExpression, out semicolonToken); + } - protected override Location GetDiagnosticLocation(PropertyDeclarationSyntax declaration) + protected override Location GetDiagnosticLocation(PropertyDeclarationSyntax declaration) + { + var body = GetBody(declaration); + if (body != null) { - var body = GetBody(declaration); - if (body != null) - { - return base.GetDiagnosticLocation(declaration); - } - - var getAccessor = GetSingleGetAccessor(declaration.AccessorList); - return getAccessor.ExpressionBody.GetLocation(); + return base.GetDiagnosticLocation(declaration); } + + var getAccessor = GetSingleGetAccessor(declaration.AccessorList); + return getAccessor.ExpressionBody.GetLocation(); } } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyHelper.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyHelper.cs index ecf836bb6eb7c..b1b692a21fb52 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyHelper.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyHelper.cs @@ -10,38 +10,37 @@ using Microsoft.CodeAnalysis.CSharp.CodeGeneration; using System.Threading; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +internal abstract class UseExpressionBodyHelper { - internal abstract class UseExpressionBodyHelper - { - public abstract Option2> Option { get; } - public abstract LocalizableString UseExpressionBodyTitle { get; } - public abstract LocalizableString UseBlockBodyTitle { get; } - public abstract string DiagnosticId { get; } - public abstract EnforceOnBuild EnforceOnBuild { get; } - public abstract ImmutableArray SyntaxKinds { get; } + public abstract Option2> Option { get; } + public abstract LocalizableString UseExpressionBodyTitle { get; } + public abstract LocalizableString UseBlockBodyTitle { get; } + public abstract string DiagnosticId { get; } + public abstract EnforceOnBuild EnforceOnBuild { get; } + public abstract ImmutableArray SyntaxKinds { get; } - public abstract CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options); - public abstract BlockSyntax? GetBody(SyntaxNode declaration); - public abstract ArrowExpressionClauseSyntax? GetExpressionBody(SyntaxNode declaration); - public abstract bool IsRelevantDeclarationNode(SyntaxNode node); + public abstract CodeStyleOption2 GetExpressionBodyPreference(CSharpCodeGenerationOptions options); + public abstract BlockSyntax? GetBody(SyntaxNode declaration); + public abstract ArrowExpressionClauseSyntax? GetExpressionBody(SyntaxNode declaration); + public abstract bool IsRelevantDeclarationNode(SyntaxNode node); - public abstract bool CanOfferUseExpressionBody(CodeStyleOption2 preference, SyntaxNode declaration, bool forAnalyzer, CancellationToken cancellationToken); - public abstract bool CanOfferUseBlockBody(CodeStyleOption2 preference, SyntaxNode declaration, bool forAnalyzer, out bool fixesError, [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionBody); - public abstract SyntaxNode Update(SemanticModel semanticModel, SyntaxNode declaration, bool useExpressionBody, CancellationToken cancellationToken); + public abstract bool CanOfferUseExpressionBody(CodeStyleOption2 preference, SyntaxNode declaration, bool forAnalyzer, CancellationToken cancellationToken); + public abstract bool CanOfferUseBlockBody(CodeStyleOption2 preference, SyntaxNode declaration, bool forAnalyzer, out bool fixesError, [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionBody); + public abstract SyntaxNode Update(SemanticModel semanticModel, SyntaxNode declaration, bool useExpressionBody, CancellationToken cancellationToken); - public abstract Location GetDiagnosticLocation(SyntaxNode declaration); + public abstract Location GetDiagnosticLocation(SyntaxNode declaration); - public static readonly ImmutableArray Helpers = - [ - UseExpressionBodyForConstructorsHelper.Instance, - UseExpressionBodyForConversionOperatorsHelper.Instance, - UseExpressionBodyForIndexersHelper.Instance, - UseExpressionBodyForMethodsHelper.Instance, - UseExpressionBodyForOperatorsHelper.Instance, - UseExpressionBodyForPropertiesHelper.Instance, - UseExpressionBodyForAccessorsHelper.Instance, - UseExpressionBodyForLocalFunctionHelper.Instance, - ]; - } + public static readonly ImmutableArray Helpers = + [ + UseExpressionBodyForConstructorsHelper.Instance, + UseExpressionBodyForConversionOperatorsHelper.Instance, + UseExpressionBodyForIndexersHelper.Instance, + UseExpressionBodyForMethodsHelper.Instance, + UseExpressionBodyForOperatorsHelper.Instance, + UseExpressionBodyForPropertiesHelper.Instance, + UseExpressionBodyForAccessorsHelper.Instance, + UseExpressionBodyForLocalFunctionHelper.Instance, + ]; } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyHelper`1.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyHelper`1.cs index 1a7e18d796933..6873b47873a82 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyHelper`1.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/Helpers/UseExpressionBodyHelper`1.cs @@ -15,297 +15,296 @@ using Microsoft.CodeAnalysis.Options; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +/// +/// Helper class that allows us to share lots of logic between the diagnostic analyzer and the +/// code refactoring provider. Those can't share a common base class due to their own inheritance +/// requirements with and "CodeRefactoringProvider". +/// +internal abstract class UseExpressionBodyHelper : UseExpressionBodyHelper + where TDeclaration : SyntaxNode { - /// - /// Helper class that allows us to share lots of logic between the diagnostic analyzer and the - /// code refactoring provider. Those can't share a common base class due to their own inheritance - /// requirements with and "CodeRefactoringProvider". - /// - internal abstract class UseExpressionBodyHelper : UseExpressionBodyHelper - where TDeclaration : SyntaxNode + public override Option2> Option { get; } + public override LocalizableString UseExpressionBodyTitle { get; } + public override LocalizableString UseBlockBodyTitle { get; } + public override string DiagnosticId { get; } + public override EnforceOnBuild EnforceOnBuild { get; } + public override ImmutableArray SyntaxKinds { get; } + + protected UseExpressionBodyHelper( + string diagnosticId, + EnforceOnBuild enforceOnBuild, + LocalizableString useExpressionBodyTitle, + LocalizableString useBlockBodyTitle, + Option2> option, + ImmutableArray syntaxKinds) { - public override Option2> Option { get; } - public override LocalizableString UseExpressionBodyTitle { get; } - public override LocalizableString UseBlockBodyTitle { get; } - public override string DiagnosticId { get; } - public override EnforceOnBuild EnforceOnBuild { get; } - public override ImmutableArray SyntaxKinds { get; } - - protected UseExpressionBodyHelper( - string diagnosticId, - EnforceOnBuild enforceOnBuild, - LocalizableString useExpressionBodyTitle, - LocalizableString useBlockBodyTitle, - Option2> option, - ImmutableArray syntaxKinds) - { - DiagnosticId = diagnosticId; - EnforceOnBuild = enforceOnBuild; - Option = option; - UseExpressionBodyTitle = useExpressionBodyTitle; - UseBlockBodyTitle = useBlockBodyTitle; - SyntaxKinds = syntaxKinds; - } + DiagnosticId = diagnosticId; + EnforceOnBuild = enforceOnBuild; + Option = option; + UseExpressionBodyTitle = useExpressionBodyTitle; + UseBlockBodyTitle = useBlockBodyTitle; + SyntaxKinds = syntaxKinds; + } - protected static AccessorDeclarationSyntax? GetSingleGetAccessor(AccessorListSyntax? accessorList) - { - return accessorList is { Accessors: [{ AttributeLists.Count: 0, RawKind: (int)SyntaxKind.GetAccessorDeclaration } accessor] } - ? accessor - : null; - } + protected static AccessorDeclarationSyntax? GetSingleGetAccessor(AccessorListSyntax? accessorList) + { + return accessorList is { Accessors: [{ AttributeLists.Count: 0, RawKind: (int)SyntaxKind.GetAccessorDeclaration } accessor] } + ? accessor + : null; + } - protected static BlockSyntax? GetBodyFromSingleGetAccessor(AccessorListSyntax accessorList) - => GetSingleGetAccessor(accessorList)?.Body; + protected static BlockSyntax? GetBodyFromSingleGetAccessor(AccessorListSyntax accessorList) + => GetSingleGetAccessor(accessorList)?.Body; - public override BlockSyntax? GetBody(SyntaxNode declaration) - => GetBody((TDeclaration)declaration); + public override BlockSyntax? GetBody(SyntaxNode declaration) + => GetBody((TDeclaration)declaration); - public override ArrowExpressionClauseSyntax? GetExpressionBody(SyntaxNode declaration) - => GetExpressionBody((TDeclaration)declaration); + public override ArrowExpressionClauseSyntax? GetExpressionBody(SyntaxNode declaration) + => GetExpressionBody((TDeclaration)declaration); - public override bool IsRelevantDeclarationNode(SyntaxNode node) - => node is TDeclaration; + public override bool IsRelevantDeclarationNode(SyntaxNode node) + => node is TDeclaration; - public override bool CanOfferUseExpressionBody(CodeStyleOption2 preference, SyntaxNode declaration, bool forAnalyzer, CancellationToken cancellationToken) - => CanOfferUseExpressionBody(preference, (TDeclaration)declaration, forAnalyzer, cancellationToken); + public override bool CanOfferUseExpressionBody(CodeStyleOption2 preference, SyntaxNode declaration, bool forAnalyzer, CancellationToken cancellationToken) + => CanOfferUseExpressionBody(preference, (TDeclaration)declaration, forAnalyzer, cancellationToken); - public override bool CanOfferUseBlockBody(CodeStyleOption2 preference, SyntaxNode declaration, bool forAnalyzer, out bool fixesError, [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionBody) - => CanOfferUseBlockBody(preference, (TDeclaration)declaration, forAnalyzer, out fixesError, out expressionBody); + public override bool CanOfferUseBlockBody(CodeStyleOption2 preference, SyntaxNode declaration, bool forAnalyzer, out bool fixesError, [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionBody) + => CanOfferUseBlockBody(preference, (TDeclaration)declaration, forAnalyzer, out fixesError, out expressionBody); - public sealed override SyntaxNode Update(SemanticModel semanticModel, SyntaxNode declaration, bool useExpressionBody, CancellationToken cancellationToken) - => Update(semanticModel, (TDeclaration)declaration, useExpressionBody, cancellationToken); + public sealed override SyntaxNode Update(SemanticModel semanticModel, SyntaxNode declaration, bool useExpressionBody, CancellationToken cancellationToken) + => Update(semanticModel, (TDeclaration)declaration, useExpressionBody, cancellationToken); - public override Location GetDiagnosticLocation(SyntaxNode declaration) - => GetDiagnosticLocation((TDeclaration)declaration); + public override Location GetDiagnosticLocation(SyntaxNode declaration) + => GetDiagnosticLocation((TDeclaration)declaration); - protected virtual Location GetDiagnosticLocation(TDeclaration declaration) - { - var body = GetBody(declaration); - Contract.ThrowIfNull(body); - return body.Statements[0].GetLocation(); - } + protected virtual Location GetDiagnosticLocation(TDeclaration declaration) + { + var body = GetBody(declaration); + Contract.ThrowIfNull(body); + return body.Statements[0].GetLocation(); + } - public bool CanOfferUseExpressionBody( - CodeStyleOption2 preference, TDeclaration declaration, bool forAnalyzer, CancellationToken cancellationToken) - { - var userPrefersExpressionBodies = preference.Value != ExpressionBodyPreference.Never; - var analyzerDisabled = preference.Notification.Severity == ReportDiagnostic.Suppress; + public bool CanOfferUseExpressionBody( + CodeStyleOption2 preference, TDeclaration declaration, bool forAnalyzer, CancellationToken cancellationToken) + { + var userPrefersExpressionBodies = preference.Value != ExpressionBodyPreference.Never; + var analyzerDisabled = preference.Notification.Severity == ReportDiagnostic.Suppress; - // If the user likes expression bodies, then we offer expression bodies from the diagnostic analyzer. - // If the user does not like expression bodies then we offer expression bodies from the refactoring provider. - // If the analyzer is disabled completely, the refactoring is enabled in both directions. - if (userPrefersExpressionBodies == forAnalyzer || (!forAnalyzer && analyzerDisabled)) + // If the user likes expression bodies, then we offer expression bodies from the diagnostic analyzer. + // If the user does not like expression bodies then we offer expression bodies from the refactoring provider. + // If the analyzer is disabled completely, the refactoring is enabled in both directions. + if (userPrefersExpressionBodies == forAnalyzer || (!forAnalyzer && analyzerDisabled)) + { + var expressionBody = GetExpressionBody(declaration); + if (expressionBody == null) { - var expressionBody = GetExpressionBody(declaration); - if (expressionBody == null) - { - // They don't have an expression body. See if we could convert the block they - // have into one. + // They don't have an expression body. See if we could convert the block they + // have into one. - var conversionPreference = forAnalyzer ? preference.Value : ExpressionBodyPreference.WhenPossible; + var conversionPreference = forAnalyzer ? preference.Value : ExpressionBodyPreference.WhenPossible; - return TryConvertToExpressionBody(declaration, conversionPreference, cancellationToken, - expressionWhenOnSingleLine: out _, semicolonWhenOnSingleLine: out _); - } + return TryConvertToExpressionBody(declaration, conversionPreference, cancellationToken, + expressionWhenOnSingleLine: out _, semicolonWhenOnSingleLine: out _); } - - return false; } - protected virtual bool TryConvertToExpressionBody( - TDeclaration declaration, - ExpressionBodyPreference conversionPreference, - CancellationToken cancellationToken, - [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionWhenOnSingleLine, - out SyntaxToken semicolonWhenOnSingleLine) + return false; + } + + protected virtual bool TryConvertToExpressionBody( + TDeclaration declaration, + ExpressionBodyPreference conversionPreference, + CancellationToken cancellationToken, + [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionWhenOnSingleLine, + out SyntaxToken semicolonWhenOnSingleLine) + { + return TryConvertToExpressionBodyWorker( + declaration, conversionPreference, cancellationToken, + out expressionWhenOnSingleLine, out semicolonWhenOnSingleLine); + } + + private bool TryConvertToExpressionBodyWorker( + SyntaxNode declaration, ExpressionBodyPreference conversionPreference, CancellationToken cancellationToken, + [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionWhenOnSingleLine, out SyntaxToken semicolonWhenOnSingleLine) + { + var body = GetBody(declaration); + if (body is null) { - return TryConvertToExpressionBodyWorker( - declaration, conversionPreference, cancellationToken, - out expressionWhenOnSingleLine, out semicolonWhenOnSingleLine); + expressionWhenOnSingleLine = null; + semicolonWhenOnSingleLine = default; + return false; } - private bool TryConvertToExpressionBodyWorker( - SyntaxNode declaration, ExpressionBodyPreference conversionPreference, CancellationToken cancellationToken, - [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionWhenOnSingleLine, out SyntaxToken semicolonWhenOnSingleLine) - { - var body = GetBody(declaration); - if (body is null) - { - expressionWhenOnSingleLine = null; - semicolonWhenOnSingleLine = default; - return false; - } + var languageVersion = body.SyntaxTree.Options.LanguageVersion(); - var languageVersion = body.SyntaxTree.Options.LanguageVersion(); + return body.TryConvertToArrowExpressionBody( + declaration.Kind(), languageVersion, conversionPreference, cancellationToken, + out expressionWhenOnSingleLine, out semicolonWhenOnSingleLine); + } - return body.TryConvertToArrowExpressionBody( - declaration.Kind(), languageVersion, conversionPreference, cancellationToken, - out expressionWhenOnSingleLine, out semicolonWhenOnSingleLine); + protected bool TryConvertToExpressionBodyForBaseProperty( + BasePropertyDeclarationSyntax declaration, + ExpressionBodyPreference conversionPreference, + CancellationToken cancellationToken, + [NotNullWhen(true)] out ArrowExpressionClauseSyntax? arrowExpression, + out SyntaxToken semicolonToken) + { + if (TryConvertToExpressionBodyWorker(declaration, conversionPreference, cancellationToken, out arrowExpression, out semicolonToken)) + { + return true; } - protected bool TryConvertToExpressionBodyForBaseProperty( - BasePropertyDeclarationSyntax declaration, - ExpressionBodyPreference conversionPreference, - CancellationToken cancellationToken, - [NotNullWhen(true)] out ArrowExpressionClauseSyntax? arrowExpression, - out SyntaxToken semicolonToken) + var getAccessor = GetSingleGetAccessor(declaration.AccessorList); + if (getAccessor?.ExpressionBody != null && + BlockSyntaxExtensions.MatchesPreference(getAccessor.ExpressionBody.Expression, conversionPreference)) { - if (TryConvertToExpressionBodyWorker(declaration, conversionPreference, cancellationToken, out arrowExpression, out semicolonToken)) - { - return true; - } + arrowExpression = SyntaxFactory.ArrowExpressionClause(getAccessor.ExpressionBody.Expression); + semicolonToken = getAccessor.SemicolonToken; + return true; + } - var getAccessor = GetSingleGetAccessor(declaration.AccessorList); - if (getAccessor?.ExpressionBody != null && - BlockSyntaxExtensions.MatchesPreference(getAccessor.ExpressionBody.Expression, conversionPreference)) - { - arrowExpression = SyntaxFactory.ArrowExpressionClause(getAccessor.ExpressionBody.Expression); - semicolonToken = getAccessor.SemicolonToken; - return true; - } + return false; + } + + public bool CanOfferUseBlockBody( + CodeStyleOption2 preference, + TDeclaration declaration, + bool forAnalyzer, + out bool fixesError, + [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionBody) + { + var userPrefersBlockBodies = preference.Value == ExpressionBodyPreference.Never; + var analyzerDisabled = preference.Notification.Severity == ReportDiagnostic.Suppress; + expressionBody = GetExpressionBody(declaration); + if (expressionBody?.TryConvertToBlock( + SyntaxFactory.Token(SyntaxKind.SemicolonToken), false, block: out _) != true) + { + fixesError = false; return false; } - public bool CanOfferUseBlockBody( - CodeStyleOption2 preference, - TDeclaration declaration, - bool forAnalyzer, - out bool fixesError, - [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionBody) + var languageVersion = declaration.GetLanguageVersion(); + if (languageVersion < LanguageVersion.CSharp7) { - var userPrefersBlockBodies = preference.Value == ExpressionBodyPreference.Never; - var analyzerDisabled = preference.Notification.Severity == ReportDiagnostic.Suppress; - - expressionBody = GetExpressionBody(declaration); - if (expressionBody?.TryConvertToBlock( - SyntaxFactory.Token(SyntaxKind.SemicolonToken), false, block: out _) != true) - { - fixesError = false; - return false; - } - - var languageVersion = declaration.GetLanguageVersion(); - if (languageVersion < LanguageVersion.CSharp7) + if (expressionBody!.Expression.IsKind(SyntaxKind.ThrowExpression)) { - if (expressionBody!.Expression.IsKind(SyntaxKind.ThrowExpression)) - { - // If they're using a throw expression in a declaration and it's prior to C# 7 - // then always mark this as something that can be fixed by the analyzer. This way - // we'll also get 'fix all' working to fix all these cases. - fixesError = true; - return true; - } - - if (declaration is AccessorDeclarationSyntax or ConstructorDeclarationSyntax) - { - // If they're using expression bodies for accessors/constructors and it's prior to C# 7 - // then always mark this as something that can be fixed by the analyzer. This way - // we'll also get 'fix all' working to fix all these cases. - fixesError = true; - return true; - } + // If they're using a throw expression in a declaration and it's prior to C# 7 + // then always mark this as something that can be fixed by the analyzer. This way + // we'll also get 'fix all' working to fix all these cases. + fixesError = true; + return true; } - if (languageVersion < LanguageVersion.CSharp6) + if (declaration is AccessorDeclarationSyntax or ConstructorDeclarationSyntax) { - // If they're using expression bodies prior to C# 6, then always mark this as something - // that can be fixed by the analyzer. This way we'll also get 'fix all' working to fix - // all these cases. + // If they're using expression bodies for accessors/constructors and it's prior to C# 7 + // then always mark this as something that can be fixed by the analyzer. This way + // we'll also get 'fix all' working to fix all these cases. fixesError = true; return true; } - - // If the user likes block bodies, then we offer block bodies from the diagnostic analyzer. - // If the user does not like block bodies then we offer block bodies from the refactoring provider. - // If the analyzer is disabled completely, the refactoring is enabled in both directions. - fixesError = false; - return userPrefersBlockBodies == forAnalyzer || (!forAnalyzer && analyzerDisabled); } - public TDeclaration Update(SemanticModel semanticModel, TDeclaration declaration, bool useExpressionBody, CancellationToken cancellationToken) + if (languageVersion < LanguageVersion.CSharp6) { - if (useExpressionBody) - { - TryConvertToExpressionBody(declaration, ExpressionBodyPreference.WhenPossible, cancellationToken, out var expressionBody, out var semicolonToken); - - var trailingTrivia = semicolonToken.TrailingTrivia - .Where(t => t.Kind() != SyntaxKind.EndOfLineTrivia) - .Concat(declaration.GetTrailingTrivia()); - semicolonToken = semicolonToken.WithTrailingTrivia(trailingTrivia); - - return WithSemicolonToken( - WithExpressionBody( - WithBody(declaration, body: null), - expressionBody), - semicolonToken); - } - else - { - return WithSemicolonToken( - WithExpressionBody( - WithGenerateBody(semanticModel, declaration), - expressionBody: null), - default); - } + // If they're using expression bodies prior to C# 6, then always mark this as something + // that can be fixed by the analyzer. This way we'll also get 'fix all' working to fix + // all these cases. + fixesError = true; + return true; } - protected abstract BlockSyntax? GetBody(TDeclaration declaration); - - protected abstract ArrowExpressionClauseSyntax? GetExpressionBody(TDeclaration declaration); - - protected abstract bool CreateReturnStatementForExpression(SemanticModel semanticModel, TDeclaration declaration); + // If the user likes block bodies, then we offer block bodies from the diagnostic analyzer. + // If the user does not like block bodies then we offer block bodies from the refactoring provider. + // If the analyzer is disabled completely, the refactoring is enabled in both directions. + fixesError = false; + return userPrefersBlockBodies == forAnalyzer || (!forAnalyzer && analyzerDisabled); + } - protected abstract SyntaxToken GetSemicolonToken(TDeclaration declaration); + public TDeclaration Update(SemanticModel semanticModel, TDeclaration declaration, bool useExpressionBody, CancellationToken cancellationToken) + { + if (useExpressionBody) + { + TryConvertToExpressionBody(declaration, ExpressionBodyPreference.WhenPossible, cancellationToken, out var expressionBody, out var semicolonToken); + + var trailingTrivia = semicolonToken.TrailingTrivia + .Where(t => t.Kind() != SyntaxKind.EndOfLineTrivia) + .Concat(declaration.GetTrailingTrivia()); + semicolonToken = semicolonToken.WithTrailingTrivia(trailingTrivia); + + return WithSemicolonToken( + WithExpressionBody( + WithBody(declaration, body: null), + expressionBody), + semicolonToken); + } + else + { + return WithSemicolonToken( + WithExpressionBody( + WithGenerateBody(semanticModel, declaration), + expressionBody: null), + default); + } + } - protected abstract TDeclaration WithSemicolonToken(TDeclaration declaration, SyntaxToken token); - protected abstract TDeclaration WithExpressionBody(TDeclaration declaration, ArrowExpressionClauseSyntax? expressionBody); - protected abstract TDeclaration WithBody(TDeclaration declaration, BlockSyntax? body); + protected abstract BlockSyntax? GetBody(TDeclaration declaration); - protected virtual TDeclaration WithGenerateBody(SemanticModel semanticModel, TDeclaration declaration) - { - var expressionBody = GetExpressionBody(declaration); + protected abstract ArrowExpressionClauseSyntax? GetExpressionBody(TDeclaration declaration); - if (expressionBody.TryConvertToBlock( - GetSemicolonToken(declaration), - CreateReturnStatementForExpression(semanticModel, declaration), - out var block)) - { - return WithBody(declaration, block); - } + protected abstract bool CreateReturnStatementForExpression(SemanticModel semanticModel, TDeclaration declaration); - return declaration; - } + protected abstract SyntaxToken GetSemicolonToken(TDeclaration declaration); - protected TDeclaration WithAccessorList(SemanticModel semanticModel, TDeclaration declaration) - { - var expressionBody = GetExpressionBody(declaration); - var semicolonToken = GetSemicolonToken(declaration); + protected abstract TDeclaration WithSemicolonToken(TDeclaration declaration, SyntaxToken token); + protected abstract TDeclaration WithExpressionBody(TDeclaration declaration, ArrowExpressionClauseSyntax? expressionBody); + protected abstract TDeclaration WithBody(TDeclaration declaration, BlockSyntax? body); - // When converting an expression-bodied property to a block body, always attempt to - // create an accessor with a block body (even if the user likes expression bodied - // accessors. While this technically doesn't match their preferences, it fits with - // the far more likely scenario that the user wants to convert this property into - // a full property so that they can flesh out the body contents. If we keep around - // an expression bodied accessor they'll just have to convert that to a block as well - // and that means two steps to take instead of one. + protected virtual TDeclaration WithGenerateBody(SemanticModel semanticModel, TDeclaration declaration) + { + var expressionBody = GetExpressionBody(declaration); - expressionBody.TryConvertToBlock( + if (expressionBody.TryConvertToBlock( GetSemicolonToken(declaration), CreateReturnStatementForExpression(semanticModel, declaration), - out var block); - - var accessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration); - accessor = block != null - ? accessor.WithBody(block) - : accessor.WithExpressionBody(expressionBody) - .WithSemicolonToken(semicolonToken); - - return WithAccessorList(declaration, SyntaxFactory.AccessorList([accessor])); + out var block)) + { + return WithBody(declaration, block); } - protected virtual TDeclaration WithAccessorList(TDeclaration declaration, AccessorListSyntax accessorListSyntax) - => throw new NotImplementedException(); + return declaration; } + + protected TDeclaration WithAccessorList(SemanticModel semanticModel, TDeclaration declaration) + { + var expressionBody = GetExpressionBody(declaration); + var semicolonToken = GetSemicolonToken(declaration); + + // When converting an expression-bodied property to a block body, always attempt to + // create an accessor with a block body (even if the user likes expression bodied + // accessors. While this technically doesn't match their preferences, it fits with + // the far more likely scenario that the user wants to convert this property into + // a full property so that they can flesh out the body contents. If we keep around + // an expression bodied accessor they'll just have to convert that to a block as well + // and that means two steps to take instead of one. + + expressionBody.TryConvertToBlock( + GetSemicolonToken(declaration), + CreateReturnStatementForExpression(semanticModel, declaration), + out var block); + + var accessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration); + accessor = block != null + ? accessor.WithBody(block) + : accessor.WithExpressionBody(expressionBody) + .WithSemicolonToken(semicolonToken); + + return WithAccessorList(declaration, SyntaxFactory.AccessorList([accessor])); + } + + protected virtual TDeclaration WithAccessorList(TDeclaration declaration, AccessorListSyntax accessorListSyntax) + => throw new NotImplementedException(); } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/UseExpressionBodyDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/UseExpressionBodyDiagnosticAnalyzer.cs index dd3fd823b2403..bed23c5b6d1a6 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/UseExpressionBodyDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/UseExpressionBodyDiagnosticAnalyzer.cs @@ -11,123 +11,122 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class UseExpressionBodyDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class UseExpressionBodyDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer - { - public const string FixesError = nameof(FixesError); + public const string FixesError = nameof(FixesError); - private readonly ImmutableArray _syntaxKinds; + private readonly ImmutableArray _syntaxKinds; - private static readonly ImmutableArray _helpers = UseExpressionBodyHelper.Helpers; + private static readonly ImmutableArray _helpers = UseExpressionBodyHelper.Helpers; - public UseExpressionBodyDiagnosticAnalyzer() - : base(GetSupportedDescriptorsWithOptions()) - { - _syntaxKinds = _helpers.SelectManyAsArray(h => h.SyntaxKinds); - } + public UseExpressionBodyDiagnosticAnalyzer() + : base(GetSupportedDescriptorsWithOptions()) + { + _syntaxKinds = _helpers.SelectManyAsArray(h => h.SyntaxKinds); + } - private static ImmutableDictionary GetSupportedDescriptorsWithOptions() + private static ImmutableDictionary GetSupportedDescriptorsWithOptions() + { + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var helper in _helpers) { - var builder = ImmutableDictionary.CreateBuilder(); - foreach (var helper in _helpers) - { - var descriptor = CreateDescriptorWithId(helper.DiagnosticId, helper.EnforceOnBuild, hasAnyCodeStyleOption: true, helper.UseExpressionBodyTitle, helper.UseExpressionBodyTitle); - builder.Add(descriptor, helper.Option); - } - - return builder.ToImmutable(); + var descriptor = CreateDescriptorWithId(helper.DiagnosticId, helper.EnforceOnBuild, hasAnyCodeStyleOption: true, helper.UseExpressionBodyTitle, helper.UseExpressionBodyTitle); + builder.Add(descriptor, helper.Option); } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + return builder.ToImmutable(); + } - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeSyntax, _syntaxKinds); + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) - { - var cancellationToken = context.CancellationToken; - var options = context.GetCSharpAnalyzerOptions().GetCodeGenerationOptions(); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeSyntax, _syntaxKinds); - var nodeKind = context.Node.Kind(); + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var cancellationToken = context.CancellationToken; + var options = context.GetCSharpAnalyzerOptions().GetCodeGenerationOptions(); - // Don't offer a fix on an accessor, if we would also offer it on the property/indexer. - if (UseExpressionBodyForAccessorsHelper.Instance.SyntaxKinds.Contains(nodeKind)) - { - var grandparent = context.Node.GetRequiredParent().GetRequiredParent(); + var nodeKind = context.Node.Kind(); - if (grandparent.Kind() == SyntaxKind.PropertyDeclaration && - AnalyzeSyntax(options, grandparent, context, UseExpressionBodyForPropertiesHelper.Instance, cancellationToken) != null) - { - return; - } + // Don't offer a fix on an accessor, if we would also offer it on the property/indexer. + if (UseExpressionBodyForAccessorsHelper.Instance.SyntaxKinds.Contains(nodeKind)) + { + var grandparent = context.Node.GetRequiredParent().GetRequiredParent(); - if (grandparent.Kind() == SyntaxKind.IndexerDeclaration && - AnalyzeSyntax(options, grandparent, context, UseExpressionBodyForIndexersHelper.Instance, cancellationToken) != null) - { - return; - } + if (grandparent.Kind() == SyntaxKind.PropertyDeclaration && + AnalyzeSyntax(options, grandparent, context, UseExpressionBodyForPropertiesHelper.Instance, cancellationToken) != null) + { + return; } - foreach (var helper in _helpers) + if (grandparent.Kind() == SyntaxKind.IndexerDeclaration && + AnalyzeSyntax(options, grandparent, context, UseExpressionBodyForIndexersHelper.Instance, cancellationToken) != null) { - cancellationToken.ThrowIfCancellationRequested(); - - if (helper.SyntaxKinds.Contains(nodeKind)) - { - var diagnostic = AnalyzeSyntax(options, context.Node, context, helper, cancellationToken); - if (diagnostic != null) - { - context.ReportDiagnostic(diagnostic); - return; - } - } + return; } } - private Diagnostic? AnalyzeSyntax( - CSharpCodeGenerationOptions options, SyntaxNode declaration, SyntaxNodeAnalysisContext context, UseExpressionBodyHelper helper, CancellationToken cancellationToken) + foreach (var helper in _helpers) { - var preference = helper.GetExpressionBodyPreference(options); - if (ShouldSkipAnalysis(context, preference.Notification)) - return null; - - var severity = preference.Notification.Severity; + cancellationToken.ThrowIfCancellationRequested(); - if (helper.CanOfferUseExpressionBody(preference, declaration, forAnalyzer: true, cancellationToken)) + if (helper.SyntaxKinds.Contains(nodeKind)) { - var location = severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden - ? declaration.GetLocation() - : helper.GetDiagnosticLocation(declaration); - - var additionalLocations = ImmutableArray.Create(declaration.GetLocation()); - var properties = ImmutableDictionary.Empty.Add(nameof(UseExpressionBody), ""); - return DiagnosticHelper.Create( - CreateDescriptorWithId(helper.DiagnosticId, helper.EnforceOnBuild, hasAnyCodeStyleOption: true, helper.UseExpressionBodyTitle, helper.UseExpressionBodyTitle), - location, preference.Notification, additionalLocations: additionalLocations, properties: properties); - } - - if (helper.CanOfferUseBlockBody(preference, declaration, forAnalyzer: true, out var fixesError, out var expressionBody)) - { - // They have an expression body. Create a diagnostic to convert it to a block - // if they don't want expression bodies for this member. - var location = severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden - ? declaration.GetLocation() - : expressionBody.GetLocation(); - - var properties = ImmutableDictionary.Empty; - if (fixesError) - properties = properties.Add(FixesError, ""); - - var additionalLocations = ImmutableArray.Create(declaration.GetLocation()); - return DiagnosticHelper.Create( - CreateDescriptorWithId(helper.DiagnosticId, helper.EnforceOnBuild, hasAnyCodeStyleOption: true, helper.UseBlockBodyTitle, helper.UseBlockBodyTitle), - location, preference.Notification, additionalLocations: additionalLocations, properties: properties); + var diagnostic = AnalyzeSyntax(options, context.Node, context, helper, cancellationToken); + if (diagnostic != null) + { + context.ReportDiagnostic(diagnostic); + return; + } } + } + } + private Diagnostic? AnalyzeSyntax( + CSharpCodeGenerationOptions options, SyntaxNode declaration, SyntaxNodeAnalysisContext context, UseExpressionBodyHelper helper, CancellationToken cancellationToken) + { + var preference = helper.GetExpressionBodyPreference(options); + if (ShouldSkipAnalysis(context, preference.Notification)) return null; + + var severity = preference.Notification.Severity; + + if (helper.CanOfferUseExpressionBody(preference, declaration, forAnalyzer: true, cancellationToken)) + { + var location = severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden + ? declaration.GetLocation() + : helper.GetDiagnosticLocation(declaration); + + var additionalLocations = ImmutableArray.Create(declaration.GetLocation()); + var properties = ImmutableDictionary.Empty.Add(nameof(UseExpressionBody), ""); + return DiagnosticHelper.Create( + CreateDescriptorWithId(helper.DiagnosticId, helper.EnforceOnBuild, hasAnyCodeStyleOption: true, helper.UseExpressionBodyTitle, helper.UseExpressionBodyTitle), + location, preference.Notification, context.Options, additionalLocations: additionalLocations, properties: properties); } + + if (helper.CanOfferUseBlockBody(preference, declaration, forAnalyzer: true, out var fixesError, out var expressionBody)) + { + // They have an expression body. Create a diagnostic to convert it to a block + // if they don't want expression bodies for this member. + var location = severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden + ? declaration.GetLocation() + : expressionBody.GetLocation(); + + var properties = ImmutableDictionary.Empty; + if (fixesError) + properties = properties.Add(FixesError, ""); + + var additionalLocations = ImmutableArray.Create(declaration.GetLocation()); + return DiagnosticHelper.Create( + CreateDescriptorWithId(helper.DiagnosticId, helper.EnforceOnBuild, hasAnyCodeStyleOption: true, helper.UseBlockBodyTitle, helper.UseBlockBodyTitle), + location, preference.Notification, context.Options, additionalLocations: additionalLocations, properties: properties); + } + + return null; } } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaDiagnosticAnalyzer.cs index fb93c81e6a55e..d4248991e4b42 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaDiagnosticAnalyzer.cs @@ -12,100 +12,101 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBodyForLambda +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBodyForLambda; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class UseExpressionBodyForLambdaDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class UseExpressionBodyForLambdaDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer - { - private static readonly DiagnosticDescriptor s_useExpressionBodyForLambda = CreateDescriptorWithId(UseExpressionBodyForLambdaHelpers.UseExpressionBodyTitle, UseExpressionBodyForLambdaHelpers.UseExpressionBodyTitle); - private static readonly DiagnosticDescriptor s_useBlockBodyForLambda = CreateDescriptorWithId(UseExpressionBodyForLambdaHelpers.UseBlockBodyTitle, UseExpressionBodyForLambdaHelpers.UseBlockBodyTitle); + private static readonly DiagnosticDescriptor s_useExpressionBodyForLambda = CreateDescriptorWithId(UseExpressionBodyForLambdaHelpers.UseExpressionBodyTitle, UseExpressionBodyForLambdaHelpers.UseExpressionBodyTitle); + private static readonly DiagnosticDescriptor s_useBlockBodyForLambda = CreateDescriptorWithId(UseExpressionBodyForLambdaHelpers.UseBlockBodyTitle, UseExpressionBodyForLambdaHelpers.UseBlockBodyTitle); - public UseExpressionBodyForLambdaDiagnosticAnalyzer() : base( - ImmutableDictionary.Empty - .Add(s_useExpressionBodyForLambda, CSharpCodeStyleOptions.PreferExpressionBodiedLambdas) - .Add(s_useBlockBodyForLambda, CSharpCodeStyleOptions.PreferExpressionBodiedLambdas)) - { - } + public UseExpressionBodyForLambdaDiagnosticAnalyzer() : base( + ImmutableDictionary.Empty + .Add(s_useExpressionBodyForLambda, CSharpCodeStyleOptions.PreferExpressionBodiedLambdas) + .Add(s_useBlockBodyForLambda, CSharpCodeStyleOptions.PreferExpressionBodiedLambdas)) + { + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeIfEnabled, - SyntaxKind.SimpleLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeIfEnabled, + SyntaxKind.SimpleLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression); - private void AnalyzeIfEnabled(SyntaxNodeAnalysisContext context) + private void AnalyzeIfEnabled(SyntaxNodeAnalysisContext context) + { + var analyzerOptions = context.Options; + var syntaxTree = context.SemanticModel.SyntaxTree; + var optionValue = UseExpressionBodyForLambdaHelpers.GetCodeStyleOption(analyzerOptions.GetAnalyzerOptions(syntaxTree)); + if (ShouldSkipAnalysis(context, optionValue.Notification)) + return; + + var severity = UseExpressionBodyForLambdaHelpers.GetOptionSeverity(optionValue); + switch (severity) { - var analyzerOptions = context.Options; - var syntaxTree = context.SemanticModel.SyntaxTree; - var optionValue = UseExpressionBodyForLambdaHelpers.GetCodeStyleOption(analyzerOptions.GetAnalyzerOptions(syntaxTree)); - if (ShouldSkipAnalysis(context, optionValue.Notification)) + case ReportDiagnostic.Error: + case ReportDiagnostic.Warn: + case ReportDiagnostic.Info: + break; + default: + // don't analyze if it's any other value. return; - - var severity = UseExpressionBodyForLambdaHelpers.GetOptionSeverity(optionValue); - switch (severity) - { - case ReportDiagnostic.Error: - case ReportDiagnostic.Warn: - case ReportDiagnostic.Info: - break; - default: - // don't analyze if it's any other value. - return; - } - - AnalyzeSyntax(context, optionValue); } - private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context, CodeStyleOption2 option) + AnalyzeSyntax(context, optionValue); + } + + private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context, CodeStyleOption2 option) + { + var declaration = (LambdaExpressionSyntax)context.Node; + var diagnostic = AnalyzeSyntax(context.SemanticModel, option, declaration, context.Options, context.CancellationToken); + if (diagnostic != null) { - var declaration = (LambdaExpressionSyntax)context.Node; - var diagnostic = AnalyzeSyntax(context.SemanticModel, option, declaration, context.CancellationToken); - if (diagnostic != null) - { - context.ReportDiagnostic(diagnostic); - } + context.ReportDiagnostic(diagnostic); } + } - private static Diagnostic? AnalyzeSyntax( - SemanticModel semanticModel, CodeStyleOption2 option, - LambdaExpressionSyntax declaration, CancellationToken cancellationToken) + private static Diagnostic? AnalyzeSyntax( + SemanticModel semanticModel, CodeStyleOption2 option, + LambdaExpressionSyntax declaration, AnalyzerOptions analyzerOptions, CancellationToken cancellationToken) + { + if (UseExpressionBodyForLambdaHelpers.CanOfferUseExpressionBody(option.Value, declaration, declaration.GetLanguageVersion(), cancellationToken)) { - if (UseExpressionBodyForLambdaHelpers.CanOfferUseExpressionBody(option.Value, declaration, declaration.GetLanguageVersion(), cancellationToken)) - { - var location = GetDiagnosticLocation(declaration); - - var additionalLocations = ImmutableArray.Create(declaration.GetLocation()); - var properties = ImmutableDictionary.Empty; - return DiagnosticHelper.Create( - s_useExpressionBodyForLambda, - location, option.Notification, additionalLocations, properties); - } - - if (UseExpressionBodyForLambdaHelpers.CanOfferUseBlockBody(semanticModel, option.Value, declaration, cancellationToken)) - { - // They have an expression body. Create a diagnostic to convert it to a block - // if they don't want expression bodies for this member. - var location = GetDiagnosticLocation(declaration); - - var properties = ImmutableDictionary.Empty; - var additionalLocations = ImmutableArray.Create(declaration.GetLocation()); - return DiagnosticHelper.Create( - s_useBlockBodyForLambda, - location, option.Notification, additionalLocations, properties); - } - - return null; + var location = GetDiagnosticLocation(declaration); + + var additionalLocations = ImmutableArray.Create(declaration.GetLocation()); + var properties = ImmutableDictionary.Empty; + return DiagnosticHelper.Create( + s_useExpressionBodyForLambda, + location, option.Notification, + analyzerOptions, additionalLocations, properties); } - private static Location GetDiagnosticLocation(LambdaExpressionSyntax declaration) - => Location.Create(declaration.SyntaxTree, - TextSpan.FromBounds(declaration.SpanStart, declaration.ArrowToken.Span.End)); - - private static DiagnosticDescriptor CreateDescriptorWithId( - LocalizableString title, LocalizableString message) + if (UseExpressionBodyForLambdaHelpers.CanOfferUseBlockBody(semanticModel, option.Value, declaration, cancellationToken)) { - return CreateDescriptorWithId(IDEDiagnosticIds.UseExpressionBodyForLambdaExpressionsDiagnosticId, EnforceOnBuildValues.UseExpressionBodyForLambdaExpressions, hasAnyCodeStyleOption: true, title, message); + // They have an expression body. Create a diagnostic to convert it to a block + // if they don't want expression bodies for this member. + var location = GetDiagnosticLocation(declaration); + + var properties = ImmutableDictionary.Empty; + var additionalLocations = ImmutableArray.Create(declaration.GetLocation()); + return DiagnosticHelper.Create( + s_useBlockBodyForLambda, + location, option.Notification, + analyzerOptions, additionalLocations, properties); } + + return null; + } + + private static Location GetDiagnosticLocation(LambdaExpressionSyntax declaration) + => Location.Create(declaration.SyntaxTree, + TextSpan.FromBounds(declaration.SpanStart, declaration.ArrowToken.Span.End)); + + private static DiagnosticDescriptor CreateDescriptorWithId( + LocalizableString title, LocalizableString message) + { + return CreateDescriptorWithId(IDEDiagnosticIds.UseExpressionBodyForLambdaExpressionsDiagnosticId, EnforceOnBuildValues.UseExpressionBodyForLambdaExpressions, hasAnyCodeStyleOption: true, title, message); } } diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaHelpers.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaHelpers.cs index 68410111223e6..bb2a81c2f53ba 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaHelpers.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaHelpers.cs @@ -9,110 +9,109 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBodyForLambda +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBodyForLambda; + +internal static class UseExpressionBodyForLambdaHelpers { - internal static class UseExpressionBodyForLambdaHelpers - { - internal static readonly LocalizableString UseExpressionBodyTitle = new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_lambda_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); - internal static readonly LocalizableString UseBlockBodyTitle = new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_lambda_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); + internal static readonly LocalizableString UseExpressionBodyTitle = new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_expression_body_for_lambda_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); + internal static readonly LocalizableString UseBlockBodyTitle = new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_block_body_for_lambda_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); - internal static bool CanOfferUseBlockBody( - SemanticModel semanticModel, ExpressionBodyPreference preference, - LambdaExpressionSyntax declaration, CancellationToken cancellationToken) + internal static bool CanOfferUseBlockBody( + SemanticModel semanticModel, ExpressionBodyPreference preference, + LambdaExpressionSyntax declaration, CancellationToken cancellationToken) + { + var userPrefersBlockBodies = preference == ExpressionBodyPreference.Never; + if (!userPrefersBlockBodies) { - var userPrefersBlockBodies = preference == ExpressionBodyPreference.Never; - if (!userPrefersBlockBodies) - { - // If the user doesn't even want block bodies, then certainly do not offer. - return false; - } - - var expressionBodyOpt = GetBodyAsExpression(declaration); - if (expressionBodyOpt == null) - { - // they already have a block body. - return false; - } - - // We need to know what sort of lambda this is (void returning or not) in order to be - // able to create the right sort of block body (i.e. with a return-statement or - // expr-statement). So, if we can't figure out what lambda type this is, we should not - // proceed. - if (semanticModel.GetTypeInfo(declaration, cancellationToken).ConvertedType is not INamedTypeSymbol lambdaType || lambdaType.DelegateInvokeMethod == null) - { - return false; - } - - var canOffer = expressionBodyOpt.TryConvertToStatement( - semicolonTokenOpt: null, createReturnStatementForExpression: false, out _); - if (!canOffer) - { - // Couldn't even convert the expression into statement form. - return false; - } - - var languageVersion = declaration.SyntaxTree.Options.LanguageVersion(); - if (expressionBodyOpt.IsKind(SyntaxKind.ThrowExpression) && - languageVersion < LanguageVersion.CSharp7) - { - // Can't convert this prior to C# 7 because ```a => throw ...``` isn't allowed. - return false; - } - - return true; + // If the user doesn't even want block bodies, then certainly do not offer. + return false; } - internal static bool CanOfferUseExpressionBody( - ExpressionBodyPreference preference, LambdaExpressionSyntax declaration, LanguageVersion languageVersion, CancellationToken cancellationToken) + var expressionBodyOpt = GetBodyAsExpression(declaration); + if (expressionBodyOpt == null) { - var userPrefersExpressionBodies = preference != ExpressionBodyPreference.Never; - if (!userPrefersExpressionBodies) - { - // If the user doesn't even want expression bodies, then certainly do not offer. - return false; - } - - var expressionBody = GetBodyAsExpression(declaration); - if (expressionBody != null) - { - // they already have an expression body. so nothing to do here. - return false; - } - - // They don't have an expression body. See if we could convert the block they - // have into one. - return TryConvertToExpressionBody(declaration, languageVersion, preference, cancellationToken, out _); + // they already have a block body. + return false; } - internal static ExpressionSyntax? GetBodyAsExpression(LambdaExpressionSyntax declaration) - => declaration.Body as ExpressionSyntax; + // We need to know what sort of lambda this is (void returning or not) in order to be + // able to create the right sort of block body (i.e. with a return-statement or + // expr-statement). So, if we can't figure out what lambda type this is, we should not + // proceed. + if (semanticModel.GetTypeInfo(declaration, cancellationToken).ConvertedType is not INamedTypeSymbol lambdaType || lambdaType.DelegateInvokeMethod == null) + { + return false; + } - internal static CodeStyleOption2 GetCodeStyleOption(AnalyzerOptionsProvider provider) - => ((CSharpAnalyzerOptionsProvider)provider).PreferExpressionBodiedLambdas; + var canOffer = expressionBodyOpt.TryConvertToStatement( + semicolonTokenOpt: null, createReturnStatementForExpression: false, out _); + if (!canOffer) + { + // Couldn't even convert the expression into statement form. + return false; + } - /// - /// Helper to get the true ReportDiagnostic severity for a given option. Importantly, this - /// handle ReportDiagnostic.Default and will map that back to the appropriate value in that - /// case. - /// - internal static ReportDiagnostic GetOptionSeverity(CodeStyleOption2 optionValue) + var languageVersion = declaration.SyntaxTree.Options.LanguageVersion(); + if (expressionBodyOpt.IsKind(SyntaxKind.ThrowExpression) && + languageVersion < LanguageVersion.CSharp7) { - var severity = optionValue.Notification.Severity; - return severity == ReportDiagnostic.Default - ? severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) - : severity; + // Can't convert this prior to C# 7 because ```a => throw ...``` isn't allowed. + return false; } - internal static bool TryConvertToExpressionBody( - LambdaExpressionSyntax declaration, - LanguageVersion languageVersion, - ExpressionBodyPreference conversionPreference, - CancellationToken cancellationToken, - [NotNullWhen(true)] out ExpressionSyntax? expression) + return true; + } + + internal static bool CanOfferUseExpressionBody( + ExpressionBodyPreference preference, LambdaExpressionSyntax declaration, LanguageVersion languageVersion, CancellationToken cancellationToken) + { + var userPrefersExpressionBodies = preference != ExpressionBodyPreference.Never; + if (!userPrefersExpressionBodies) { - var body = declaration.Body as BlockSyntax; + // If the user doesn't even want expression bodies, then certainly do not offer. + return false; + } - return body.TryConvertToExpressionBody(languageVersion, conversionPreference, cancellationToken, out expression, out _); + var expressionBody = GetBodyAsExpression(declaration); + if (expressionBody != null) + { + // they already have an expression body. so nothing to do here. + return false; } + + // They don't have an expression body. See if we could convert the block they + // have into one. + return TryConvertToExpressionBody(declaration, languageVersion, preference, cancellationToken, out _); + } + + internal static ExpressionSyntax? GetBodyAsExpression(LambdaExpressionSyntax declaration) + => declaration.Body as ExpressionSyntax; + + internal static CodeStyleOption2 GetCodeStyleOption(AnalyzerOptionsProvider provider) + => ((CSharpAnalyzerOptionsProvider)provider).PreferExpressionBodiedLambdas; + + /// + /// Helper to get the true ReportDiagnostic severity for a given option. Importantly, this + /// handle ReportDiagnostic.Default and will map that back to the appropriate value in that + /// case. + /// + internal static ReportDiagnostic GetOptionSeverity(CodeStyleOption2 optionValue) + { + var severity = optionValue.Notification.Severity; + return severity == ReportDiagnostic.Default + ? severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) + : severity; + } + + internal static bool TryConvertToExpressionBody( + LambdaExpressionSyntax declaration, + LanguageVersion languageVersion, + ExpressionBodyPreference conversionPreference, + CancellationToken cancellationToken, + [NotNullWhen(true)] out ExpressionSyntax? expression) + { + var body = declaration.Body as BlockSyntax; + + return body.TryConvertToExpressionBody(languageVersion, conversionPreference, cancellationToken, out expression, out _); } } diff --git a/src/Analyzers/CSharp/Analyzers/UseImplicitObjectCreation/CSharpUseImplicitObjectCreationDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseImplicitObjectCreation/CSharpUseImplicitObjectCreationDiagnosticAnalyzer.cs index 82f94314d908e..03ff8fd87aa41 100644 --- a/src/Analyzers/CSharp/Analyzers/UseImplicitObjectCreation/CSharpUseImplicitObjectCreationDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseImplicitObjectCreation/CSharpUseImplicitObjectCreationDiagnosticAnalyzer.cs @@ -59,6 +59,7 @@ private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) Descriptor, objectCreation.Type.GetLocation(), styleOption.Notification, + context.Options, ImmutableArray.Create(objectCreation.GetLocation()), properties: null)); } diff --git a/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpTypeStyleDiagnosticAnalyzerBase.cs b/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpTypeStyleDiagnosticAnalyzerBase.cs index 4e9f79b864997..1fda7fdc6fdb5 100644 --- a/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpTypeStyleDiagnosticAnalyzerBase.cs +++ b/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpTypeStyleDiagnosticAnalyzerBase.cs @@ -14,68 +14,67 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.TypeStyle +namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.TypeStyle; + +internal abstract partial class CSharpTypeStyleDiagnosticAnalyzerBase : + AbstractBuiltInCodeStyleDiagnosticAnalyzer { - internal abstract partial class CSharpTypeStyleDiagnosticAnalyzerBase : - AbstractBuiltInCodeStyleDiagnosticAnalyzer + protected abstract CSharpTypeStyleHelper Helper { get; } + + protected CSharpTypeStyleDiagnosticAnalyzerBase( + string diagnosticId, EnforceOnBuild enforceOnBuild, LocalizableString title, LocalizableString message) + : base(diagnosticId, + enforceOnBuild, + [CSharpCodeStyleOptions.VarForBuiltInTypes, CSharpCodeStyleOptions.VarWhenTypeIsApparent, CSharpCodeStyleOptions.VarElsewhere], + title, message) { - protected abstract CSharpTypeStyleHelper Helper { get; } + } - protected CSharpTypeStyleDiagnosticAnalyzerBase( - string diagnosticId, EnforceOnBuild enforceOnBuild, LocalizableString title, LocalizableString message) - : base(diagnosticId, - enforceOnBuild, - [CSharpCodeStyleOptions.VarForBuiltInTypes, CSharpCodeStyleOptions.VarWhenTypeIsApparent, CSharpCodeStyleOptions.VarElsewhere], - title, message) - { - } + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override bool OpenFileOnly(SimplifierOptions? options) + { + // analyzer is only active in C# projects + Contract.ThrowIfNull(options); - public override bool OpenFileOnly(SimplifierOptions? options) - { - // analyzer is only active in C# projects - Contract.ThrowIfNull(options); + var csOptions = (CSharpSimplifierOptions)options; + return !(csOptions.VarForBuiltInTypes.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || + csOptions.VarWhenTypeIsApparent.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || + csOptions.VarElsewhere.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error); + } - var csOptions = (CSharpSimplifierOptions)options; - return !(csOptions.VarForBuiltInTypes.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || - csOptions.VarWhenTypeIsApparent.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || - csOptions.VarElsewhere.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error); - } + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction( + HandleVariableDeclaration, SyntaxKind.VariableDeclaration, SyntaxKind.ForEachStatement, SyntaxKind.DeclarationExpression); - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction( - HandleVariableDeclaration, SyntaxKind.VariableDeclaration, SyntaxKind.ForEachStatement, SyntaxKind.DeclarationExpression); + private void HandleVariableDeclaration(SyntaxNodeAnalysisContext context) + { + var declarationStatement = context.Node; + var cancellationToken = context.CancellationToken; - private void HandleVariableDeclaration(SyntaxNodeAnalysisContext context) + var semanticModel = context.SemanticModel; + var declaredType = Helper.FindAnalyzableType(declarationStatement, semanticModel, cancellationToken); + if (declaredType == null) { - var declarationStatement = context.Node; - var cancellationToken = context.CancellationToken; - - var semanticModel = context.SemanticModel; - var declaredType = Helper.FindAnalyzableType(declarationStatement, semanticModel, cancellationToken); - if (declaredType == null) - { - return; - } - - var simplifierOptions = context.GetCSharpAnalyzerOptions().GetSimplifierOptions(); + return; + } - var typeStyle = Helper.AnalyzeTypeName( - declaredType, semanticModel, simplifierOptions, cancellationToken); - if (!typeStyle.IsStylePreferred - || ShouldSkipAnalysis(context, typeStyle.Notification) - || !typeStyle.CanConvert()) - { - return; - } + var simplifierOptions = context.GetCSharpAnalyzerOptions().GetSimplifierOptions(); - // The severity preference is not Hidden, as indicated by IsStylePreferred. - var descriptor = Descriptor; - context.ReportDiagnostic(CreateDiagnostic(descriptor, declarationStatement, declaredType.StripRefIfNeeded().Span, typeStyle.Notification)); + var typeStyle = Helper.AnalyzeTypeName( + declaredType, semanticModel, simplifierOptions, cancellationToken); + if (!typeStyle.IsStylePreferred + || ShouldSkipAnalysis(context, typeStyle.Notification) + || !typeStyle.CanConvert()) + { + return; } - private static Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, SyntaxNode declaration, TextSpan diagnosticSpan, NotificationOption2 notificationOption) - => DiagnosticHelper.Create(descriptor, declaration.SyntaxTree.GetLocation(diagnosticSpan), notificationOption, additionalLocations: null, properties: null); + // The severity preference is not Hidden, as indicated by IsStylePreferred. + var descriptor = Descriptor; + context.ReportDiagnostic(CreateDiagnostic(descriptor, declarationStatement, declaredType.StripRefIfNeeded().Span, typeStyle.Notification, context.Options)); } + + private static Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, SyntaxNode declaration, TextSpan diagnosticSpan, NotificationOption2 notificationOption, AnalyzerOptions analyzerOptions) + => DiagnosticHelper.Create(descriptor, declaration.SyntaxTree.GetLocation(diagnosticSpan), notificationOption, analyzerOptions, additionalLocations: null, properties: null); } diff --git a/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpUseExplicitTypeDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpUseExplicitTypeDiagnosticAnalyzer.cs index b339dd29f3abe..3abc9eb52609d 100644 --- a/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpUseExplicitTypeDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpUseExplicitTypeDiagnosticAnalyzer.cs @@ -5,25 +5,24 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.TypeStyle +namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.TypeStyle; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpUseExplicitTypeDiagnosticAnalyzer : CSharpTypeStyleDiagnosticAnalyzerBase { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpUseExplicitTypeDiagnosticAnalyzer : CSharpTypeStyleDiagnosticAnalyzerBase - { - private static readonly LocalizableString s_Title = - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_explicit_type), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); + private static readonly LocalizableString s_Title = + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_explicit_type), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); - private static readonly LocalizableString s_Message = - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_explicit_type_instead_of_var), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); + private static readonly LocalizableString s_Message = + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_explicit_type_instead_of_var), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); - protected override CSharpTypeStyleHelper Helper => CSharpUseExplicitTypeHelper.Instance; + protected override CSharpTypeStyleHelper Helper => CSharpUseExplicitTypeHelper.Instance; - public CSharpUseExplicitTypeDiagnosticAnalyzer() - : base(diagnosticId: IDEDiagnosticIds.UseExplicitTypeDiagnosticId, - enforceOnBuild: EnforceOnBuildValues.UseExplicitType, - title: s_Title, - message: s_Message) - { - } + public CSharpUseExplicitTypeDiagnosticAnalyzer() + : base(diagnosticId: IDEDiagnosticIds.UseExplicitTypeDiagnosticId, + enforceOnBuild: EnforceOnBuildValues.UseExplicitType, + title: s_Title, + message: s_Message) + { } } diff --git a/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpUseImplicitTypeDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpUseImplicitTypeDiagnosticAnalyzer.cs index 86fe4a9af918a..72886ca920f33 100644 --- a/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpUseImplicitTypeDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseImplicitOrExplicitType/CSharpUseImplicitTypeDiagnosticAnalyzer.cs @@ -5,25 +5,24 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.TypeStyle +namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.TypeStyle; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpUseImplicitTypeDiagnosticAnalyzer : CSharpTypeStyleDiagnosticAnalyzerBase { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpUseImplicitTypeDiagnosticAnalyzer : CSharpTypeStyleDiagnosticAnalyzerBase - { - private static readonly LocalizableString s_Title = - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_implicit_type), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); + private static readonly LocalizableString s_Title = + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_implicit_type), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); - private static readonly LocalizableString s_Message = - new LocalizableResourceString(nameof(CSharpAnalyzersResources.use_var_instead_of_explicit_type), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); + private static readonly LocalizableString s_Message = + new LocalizableResourceString(nameof(CSharpAnalyzersResources.use_var_instead_of_explicit_type), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); - protected override CSharpTypeStyleHelper Helper => CSharpUseImplicitTypeHelper.Instance; + protected override CSharpTypeStyleHelper Helper => CSharpUseImplicitTypeHelper.Instance; - public CSharpUseImplicitTypeDiagnosticAnalyzer() - : base(diagnosticId: IDEDiagnosticIds.UseImplicitTypeDiagnosticId, - enforceOnBuild: EnforceOnBuildValues.UseImplicitType, - title: s_Title, - message: s_Message) - { - } + public CSharpUseImplicitTypeDiagnosticAnalyzer() + : base(diagnosticId: IDEDiagnosticIds.UseImplicitTypeDiagnosticId, + enforceOnBuild: EnforceOnBuildValues.UseImplicitType, + title: s_Title, + message: s_Message) + { } } diff --git a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseIndexOperatorDiagnosticAnalyzer.InfoCache.cs b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseIndexOperatorDiagnosticAnalyzer.InfoCache.cs index 7623c8991b7ce..f6bc6e568722a 100644 --- a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseIndexOperatorDiagnosticAnalyzer.InfoCache.cs +++ b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseIndexOperatorDiagnosticAnalyzer.InfoCache.cs @@ -8,99 +8,98 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator -{ - using static Helpers; +namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator; + +using static Helpers; - internal partial class CSharpUseIndexOperatorDiagnosticAnalyzer +internal partial class CSharpUseIndexOperatorDiagnosticAnalyzer +{ + /// + /// Helper type to cache information about types while analyzing the compilation. + /// + private class InfoCache { /// - /// Helper type to cache information about types while analyzing the compilation. + /// The type. Needed so that we only fixup code if we see the type + /// we're using has an indexer that takes an . + /// + [SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")] + public readonly INamedTypeSymbol IndexType; + + public readonly INamedTypeSymbol? ExpressionOfTType; + + /// + /// Mapping from a method like MyType.Get(int) to the Length/Count property for + /// MyType as well as the optional MyType.Get(System.Index) member if it exists. /// - private class InfoCache + private readonly ConcurrentDictionary _methodToMemberInfo = new(); + + private InfoCache(INamedTypeSymbol indexType, INamedTypeSymbol? expressionOfTType) + { + IndexType = indexType; + ExpressionOfTType = expressionOfTType; + } + + public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out InfoCache? infoCache) { - /// - /// The type. Needed so that we only fixup code if we see the type - /// we're using has an indexer that takes an . - /// - [SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")] - public readonly INamedTypeSymbol IndexType; - - public readonly INamedTypeSymbol? ExpressionOfTType; - - /// - /// Mapping from a method like MyType.Get(int) to the Length/Count property for - /// MyType as well as the optional MyType.Get(System.Index) member if it exists. - /// - private readonly ConcurrentDictionary _methodToMemberInfo = new(); - - private InfoCache(INamedTypeSymbol indexType, INamedTypeSymbol? expressionOfTType) + var indexType = compilation.GetBestTypeByMetadataName(typeof(Index).FullName!); + if (indexType == null || !indexType.IsAccessibleWithin(compilation.Assembly)) { - IndexType = indexType; - ExpressionOfTType = expressionOfTType; + infoCache = null; + return false; } - public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out InfoCache? infoCache) + infoCache = new InfoCache(indexType, compilation.ExpressionOfTType()); + return true; + } + + public bool TryGetMemberInfo(IMethodSymbol methodSymbol, out MemberInfo memberInfo) + { + memberInfo = default; + + if (IsIntIndexingMethod(methodSymbol)) { - var indexType = compilation.GetBestTypeByMetadataName(typeof(Index).FullName!); - if (indexType == null || !indexType.IsAccessibleWithin(compilation.Assembly)) - { - infoCache = null; - return false; - } - - infoCache = new InfoCache(indexType, compilation.ExpressionOfTType()); - return true; + memberInfo = _methodToMemberInfo.GetOrAdd(methodSymbol, m => ComputeMemberInfo(m)); } - public bool TryGetMemberInfo(IMethodSymbol methodSymbol, out MemberInfo memberInfo) - { - memberInfo = default; + return memberInfo.LengthLikeProperty != null; + } + + private MemberInfo ComputeMemberInfo(IMethodSymbol method) + { + Debug.Assert(IsIntIndexingMethod(method)); - if (IsIntIndexingMethod(methodSymbol)) - { - memberInfo = _methodToMemberInfo.GetOrAdd(methodSymbol, m => ComputeMemberInfo(m)); - } + // Check that the type has an int32 'Length' or 'Count' property. If not, we don't + // consider it something indexable. + var containingType = method.ContainingType; + var lengthLikeProperty = TryGetLengthOrCountProperty(containingType); + if (lengthLikeProperty == null) + return default; - return memberInfo.LengthLikeProperty != null; + if (method.MethodKind == MethodKind.PropertyGet) + { + // this is the getter for an indexer. i.e. the user is calling something + // like s[...]. + // + // These can always be converted to use a System.Index. Either because the + // type itself has a System.Index-based indexer, or because the language just + // allows types to implicitly seem like they support this through: + // + // https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-index-support + return new MemberInfo(lengthLikeProperty, overloadedMethodOpt: null); } - - private MemberInfo ComputeMemberInfo(IMethodSymbol method) + else { - Debug.Assert(IsIntIndexingMethod(method)); - - // Check that the type has an int32 'Length' or 'Count' property. If not, we don't - // consider it something indexable. - var containingType = method.ContainingType; - var lengthLikeProperty = TryGetLengthOrCountProperty(containingType); - if (lengthLikeProperty == null) - return default; - - if (method.MethodKind == MethodKind.PropertyGet) - { - // this is the getter for an indexer. i.e. the user is calling something - // like s[...]. - // - // These can always be converted to use a System.Index. Either because the - // type itself has a System.Index-based indexer, or because the language just - // allows types to implicitly seem like they support this through: - // - // https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-index-support - return new MemberInfo(lengthLikeProperty, overloadedMethodOpt: null); - } - else - { - Debug.Assert(method.MethodKind == MethodKind.Ordinary); - // it's a method like: `SomeType MyType.Get(int index)`. Look - // for an overload like: `SomeType MyType.Get(Range)` - var overloadedIndexMethod = GetOverload(method, IndexType); - if (overloadedIndexMethod != null) - return new MemberInfo(lengthLikeProperty, overloadedIndexMethod); - } - - // A index-like method that we can't convert. - return default; + Debug.Assert(method.MethodKind == MethodKind.Ordinary); + // it's a method like: `SomeType MyType.Get(int index)`. Look + // for an overload like: `SomeType MyType.Get(Range)` + var overloadedIndexMethod = GetOverload(method, IndexType); + if (overloadedIndexMethod != null) + return new MemberInfo(lengthLikeProperty, overloadedIndexMethod); } + + // A index-like method that we can't convert. + return default; } } } diff --git a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseIndexOperatorDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseIndexOperatorDiagnosticAnalyzer.cs index 2e462a8866114..3999c3accfdc2 100644 --- a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseIndexOperatorDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseIndexOperatorDiagnosticAnalyzer.cs @@ -13,207 +13,207 @@ using Microsoft.CodeAnalysis.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator +namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator; + +using static Helpers; + +/// +/// Analyzer that looks for code like: +/// +/// +/// s[s.Length - n] and offers to change that to s[^n] +/// s.Get(s.Length - n) and offers to change that to s.Get(^n) +/// +/// +/// In order to do convert between indexers, the type must look 'indexable'. Meaning, it must +/// have an -returning property called Length or Count, and it must have both an +/// -indexer, and a -indexer. In order to convert between methods, the type +/// must have identical overloads except that one takes an , and the other a . +/// +/// It is assumed that if the type follows this shape that it is well behaved and that this +/// transformation will preserve semantics. If this assumption is not good in practice, we +/// could always limit the feature to only work on an allow list of known safe types. +/// +/// Note that this feature only works if the code literally has expr1.Length - expr2. If +/// code has this, and is calling into a method that takes either an or a , +/// it feels very safe to assume this is well behaved and switching to ^expr2 is going to +/// preserve semantics. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +[SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")] +internal sealed partial class CSharpUseIndexOperatorDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - using static Helpers; - - /// - /// Analyzer that looks for code like: - /// - /// - /// s[s.Length - n] and offers to change that to s[^n] - /// s.Get(s.Length - n) and offers to change that to s.Get(^n) - /// - /// - /// In order to do convert between indexers, the type must look 'indexable'. Meaning, it must - /// have an -returning property called Length or Count, and it must have both an - /// -indexer, and a -indexer. In order to convert between methods, the type - /// must have identical overloads except that one takes an , and the other a . - /// - /// It is assumed that if the type follows this shape that it is well behaved and that this - /// transformation will preserve semantics. If this assumption is not good in practice, we - /// could always limit the feature to only work on an allow list of known safe types. - /// - /// Note that this feature only works if the code literally has expr1.Length - expr2. If - /// code has this, and is calling into a method that takes either an or a , - /// it feels very safe to assume this is well behaved and switching to ^expr2 is going to - /// preserve semantics. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - [SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")] - internal sealed partial class CSharpUseIndexOperatorDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public CSharpUseIndexOperatorDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseIndexOperatorDiagnosticId, + EnforceOnBuildValues.UseIndexOperator, + CSharpCodeStyleOptions.PreferIndexOperator, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_index_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Indexing_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpUseIndexOperatorDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseIndexOperatorDiagnosticId, - EnforceOnBuildValues.UseIndexOperator, - CSharpCodeStyleOptions.PreferIndexOperator, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_index_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Indexing_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } - - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - - protected override void InitializeWorker(AnalysisContext context) - { - context.RegisterCompilationStartAction(context => - { - var compilation = (CSharpCompilation)context.Compilation; - - // Only supported on C# 8 and above. - if (compilation.LanguageVersion < LanguageVersion.CSharp8) - return; - - // We're going to be checking every property-reference and invocation in the - // compilation. Cache information we compute in this object so we don't have to - // continually recompute it. - if (!InfoCache.TryCreate(compilation, out var infoCache)) - return; - - // Register to hear property references, so we can hear about calls to indexers - // like: s[s.Length - n] - context.RegisterOperationAction( - c => AnalyzePropertyReference(c, infoCache), - OperationKind.PropertyReference); - - // Register to hear about methods for: s.Get(s.Length - n) - context.RegisterOperationAction( - c => AnalyzeInvocation(c, infoCache), - OperationKind.Invocation); - - var arrayType = compilation.GetSpecialType(SpecialType.System_Array); - var arrayLengthProperty = TryGetNoArgInt32Property(arrayType, nameof(Array.Length)); - - if (arrayLengthProperty != null) - { - // Array indexing is represented with a different operation kind. Register - // specifically for that. - context.RegisterOperationAction( - c => AnalyzeArrayElementReference(c, infoCache, arrayLengthProperty), - OperationKind.ArrayElementReference); - } - }); - } - - private void AnalyzeInvocation( - OperationAnalysisContext context, InfoCache infoCache) - { - var cancellationToken = context.CancellationToken; - var invocationOperation = (IInvocationOperation)context.Operation; - - if (invocationOperation.Arguments.Length != 1) - return; + } - AnalyzeInvokedMember( - context, infoCache, - invocationOperation.Instance, - invocationOperation.TargetMethod, - invocationOperation.Arguments[0].Value, - lengthLikeProperty: null, - cancellationToken); - } + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - private void AnalyzePropertyReference( - OperationAnalysisContext context, InfoCache infoCache) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - var cancellationToken = context.CancellationToken; - var propertyReference = (IPropertyReferenceOperation)context.Operation; + var compilation = (CSharpCompilation)context.Compilation; - // Only analyze indexer calls. - if (!propertyReference.Property.IsIndexer) + // Only supported on C# 8 and above. + if (compilation.LanguageVersion < LanguageVersion.CSharp8) return; - if (propertyReference.Arguments.Length != 1) + // We're going to be checking every property-reference and invocation in the + // compilation. Cache information we compute in this object so we don't have to + // continually recompute it. + if (!InfoCache.TryCreate(compilation, out var infoCache)) return; - AnalyzeInvokedMember( - context, infoCache, - propertyReference.Instance, - propertyReference.Property.GetMethod, - propertyReference.Arguments[0].Value, - lengthLikeProperty: null, - cancellationToken); - } - - private void AnalyzeArrayElementReference( - OperationAnalysisContext context, InfoCache infoCache, IPropertySymbol arrayLengthProperty) - { - var cancellationToken = context.CancellationToken; - var arrayElementReference = (IArrayElementReferenceOperation)context.Operation; + // Register to hear property references, so we can hear about calls to indexers + // like: s[s.Length - n] + context.RegisterOperationAction( + c => AnalyzePropertyReference(c, infoCache), + OperationKind.PropertyReference); - // Has to be a single-dimensional element access. - if (arrayElementReference.Indices.Length != 1) - return; + // Register to hear about methods for: s.Get(s.Length - n) + context.RegisterOperationAction( + c => AnalyzeInvocation(c, infoCache), + OperationKind.Invocation); - AnalyzeInvokedMember( - context, infoCache, - arrayElementReference.ArrayReference, - targetMethod: null, - arrayElementReference.Indices[0], - lengthLikeProperty: arrayLengthProperty, - cancellationToken); - } + var arrayType = compilation.GetSpecialType(SpecialType.System_Array); + var arrayLengthProperty = TryGetNoArgInt32Property(arrayType, nameof(Array.Length)); - private void AnalyzeInvokedMember( - OperationAnalysisContext context, - InfoCache infoCache, - IOperation? instance, - IMethodSymbol? targetMethod, - IOperation argumentValue, - IPropertySymbol? lengthLikeProperty, - CancellationToken cancellationToken) - { - // look for `s[s.Length - value]` or `s.Get(s.Length- value)`. - - // Needs to have the one arg for `s.Length - value`, and that arg needs to be - // a subtraction. - if (instance is null || - !IsSubtraction(argumentValue, out var subtraction)) + if (arrayLengthProperty != null) { - return; + // Array indexing is represented with a different operation kind. Register + // specifically for that. + context.RegisterOperationAction( + c => AnalyzeArrayElementReference(c, infoCache, arrayLengthProperty), + OperationKind.ArrayElementReference); } + }); + } - if (subtraction.Syntax is not BinaryExpressionSyntax binaryExpression) - return; + private void AnalyzeInvocation( + OperationAnalysisContext context, InfoCache infoCache) + { + var cancellationToken = context.CancellationToken; + var invocationOperation = (IInvocationOperation)context.Operation; + + if (invocationOperation.Arguments.Length != 1) + return; + + AnalyzeInvokedMember( + context, infoCache, + invocationOperation.Instance, + invocationOperation.TargetMethod, + invocationOperation.Arguments[0].Value, + lengthLikeProperty: null, + cancellationToken); + } - // Don't bother analyzing if the user doesn't like using Index/Range operators. - var option = context.GetCSharpAnalyzerOptions().PreferIndexOperator; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; + private void AnalyzePropertyReference( + OperationAnalysisContext context, InfoCache infoCache) + { + var cancellationToken = context.CancellationToken; + var propertyReference = (IPropertyReferenceOperation)context.Operation; + + // Only analyze indexer calls. + if (!propertyReference.Property.IsIndexer) + return; + + if (propertyReference.Arguments.Length != 1) + return; + + AnalyzeInvokedMember( + context, infoCache, + propertyReference.Instance, + propertyReference.Property.GetMethod, + propertyReference.Arguments[0].Value, + lengthLikeProperty: null, + cancellationToken); + } + + private void AnalyzeArrayElementReference( + OperationAnalysisContext context, InfoCache infoCache, IPropertySymbol arrayLengthProperty) + { + var cancellationToken = context.CancellationToken; + var arrayElementReference = (IArrayElementReferenceOperation)context.Operation; + + // Has to be a single-dimensional element access. + if (arrayElementReference.Indices.Length != 1) + return; + + AnalyzeInvokedMember( + context, infoCache, + arrayElementReference.ArrayReference, + targetMethod: null, + arrayElementReference.Indices[0], + lengthLikeProperty: arrayLengthProperty, + cancellationToken); + } - // Ok, looks promising. We're indexing in with some subtraction expression. Examine the - // type this indexer is in to see if there's another member that takes a System.Index - // that we can convert to. - // - // Also ensure that the left side of the subtraction : `s.Length - value` is actually - // getting the length off the same instance we're indexing into. + private void AnalyzeInvokedMember( + OperationAnalysisContext context, + InfoCache infoCache, + IOperation? instance, + IMethodSymbol? targetMethod, + IOperation argumentValue, + IPropertySymbol? lengthLikeProperty, + CancellationToken cancellationToken) + { + // look for `s[s.Length - value]` or `s.Get(s.Length- value)`. - lengthLikeProperty ??= TryGetLengthLikeProperty(infoCache, targetMethod); - if (lengthLikeProperty == null || - !IsInstanceLengthCheck(lengthLikeProperty, instance, subtraction.LeftOperand)) - { - return; - } + // Needs to have the one arg for `s.Length - value`, and that arg needs to be + // a subtraction. + if (instance is null || + !IsSubtraction(argumentValue, out var subtraction)) + { + return; + } - var semanticModel = instance.SemanticModel; - Contract.ThrowIfNull(semanticModel); + if (subtraction.Syntax is not BinaryExpressionSyntax binaryExpression) + return; - if (CSharpSemanticFacts.Instance.IsInExpressionTree(semanticModel, instance.Syntax, infoCache.ExpressionOfTType, cancellationToken)) - return; + // Don't bother analyzing if the user doesn't like using Index/Range operators. + var option = context.GetCSharpAnalyzerOptions().PreferIndexOperator; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; - // Everything looks good. We can update this to use the System.Index member instead. - context.ReportDiagnostic( - DiagnosticHelper.Create( - Descriptor, - binaryExpression.GetLocation(), - option.Notification, - [], - ImmutableDictionary.Empty)); + // Ok, looks promising. We're indexing in with some subtraction expression. Examine the + // type this indexer is in to see if there's another member that takes a System.Index + // that we can convert to. + // + // Also ensure that the left side of the subtraction : `s.Length - value` is actually + // getting the length off the same instance we're indexing into. + + lengthLikeProperty ??= TryGetLengthLikeProperty(infoCache, targetMethod); + if (lengthLikeProperty == null || + !IsInstanceLengthCheck(lengthLikeProperty, instance, subtraction.LeftOperand)) + { + return; } - private static IPropertySymbol? TryGetLengthLikeProperty(InfoCache infoCache, IMethodSymbol? targetMethod) - => targetMethod != null && infoCache.TryGetMemberInfo(targetMethod, out var memberInfo) - ? memberInfo.LengthLikeProperty - : null; + var semanticModel = instance.SemanticModel; + Contract.ThrowIfNull(semanticModel); + + if (CSharpSemanticFacts.Instance.IsInExpressionTree(semanticModel, instance.Syntax, infoCache.ExpressionOfTType, cancellationToken)) + return; + + // Everything looks good. We can update this to use the System.Index member instead. + context.ReportDiagnostic( + DiagnosticHelper.Create( + Descriptor, + binaryExpression.GetLocation(), + option.Notification, + context.Options, + [], + ImmutableDictionary.Empty)); } + + private static IPropertySymbol? TryGetLengthLikeProperty(InfoCache infoCache, IMethodSymbol? targetMethod) + => targetMethod != null && infoCache.TryGetMemberInfo(targetMethod, out var memberInfo) + ? memberInfo.LengthLikeProperty + : null; } diff --git a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.InfoCache.cs b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.InfoCache.cs index 40804d8c83bb5..ddd2da262f1cd 100644 --- a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.InfoCache.cs +++ b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.InfoCache.cs @@ -9,171 +9,170 @@ using System.Linq; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator -{ - using static Helpers; +namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator; + +using static Helpers; - internal partial class CSharpUseRangeOperatorDiagnosticAnalyzer +internal partial class CSharpUseRangeOperatorDiagnosticAnalyzer +{ + /// + /// Helper type to cache information about types while analyzing the compilation. + /// + public class InfoCache { /// - /// Helper type to cache information about types while analyzing the compilation. + /// The type. Needed so that we only fixup code if we see the type + /// we're using has an indexer that takes a . /// - public class InfoCache - { - /// - /// The type. Needed so that we only fixup code if we see the type - /// we're using has an indexer that takes a . - /// - [SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")] - public readonly INamedTypeSymbol RangeType; - public readonly INamedTypeSymbol? ExpressionOfTType; + [SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")] + public readonly INamedTypeSymbol RangeType; + public readonly INamedTypeSymbol? ExpressionOfTType; - private readonly ConcurrentDictionary _methodToMemberInfo = new(); + private readonly ConcurrentDictionary _methodToMemberInfo = new(); - private InfoCache(INamedTypeSymbol rangeType, INamedTypeSymbol stringType, INamedTypeSymbol? expressionOfTType) + private InfoCache(INamedTypeSymbol rangeType, INamedTypeSymbol stringType, INamedTypeSymbol? expressionOfTType) + { + RangeType = rangeType; + ExpressionOfTType = expressionOfTType; + + // Always allow using System.Range indexers with System.String.Substring. The + // compiler has hard-coded knowledge on how to use this type, even if there is no + // this[Range] indexer declared on it directly. + // + // Ensure that we can actually get the 'string' type. We may fail if there is no + // proper mscorlib reference (for example, while a project is loading). + if (!stringType.IsErrorType()) { - RangeType = rangeType; - ExpressionOfTType = expressionOfTType; - - // Always allow using System.Range indexers with System.String.Substring. The - // compiler has hard-coded knowledge on how to use this type, even if there is no - // this[Range] indexer declared on it directly. - // - // Ensure that we can actually get the 'string' type. We may fail if there is no - // proper mscorlib reference (for example, while a project is loading). - if (!stringType.IsErrorType()) - { - var substringMethod = stringType.GetMembers(nameof(string.Substring)) - .OfType() - .FirstOrDefault(m => IsTwoArgumentSliceLikeMethod(m)); + var substringMethod = stringType.GetMembers(nameof(string.Substring)) + .OfType() + .FirstOrDefault(m => IsTwoArgumentSliceLikeMethod(m)); - if (substringMethod is not null) - _methodToMemberInfo[substringMethod] = ComputeMemberInfo(substringMethod, requireRangeMember: false); - } + if (substringMethod is not null) + _methodToMemberInfo[substringMethod] = ComputeMemberInfo(substringMethod, requireRangeMember: false); } + } - public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out InfoCache? infoCache) + public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out InfoCache? infoCache) + { + var rangeType = compilation.GetBestTypeByMetadataName(typeof(Range).FullName!); + if (rangeType == null || !rangeType.IsAccessibleWithin(compilation.Assembly)) { - var rangeType = compilation.GetBestTypeByMetadataName(typeof(Range).FullName!); - if (rangeType == null || !rangeType.IsAccessibleWithin(compilation.Assembly)) - { - infoCache = null; - return false; - } - - var stringType = compilation.GetSpecialType(SpecialType.System_String); - infoCache = new InfoCache(rangeType, stringType, compilation.ExpressionOfTType()); - return true; + infoCache = null; + return false; } - private static IMethodSymbol? GetSliceLikeMethod(INamedTypeSymbol namedType) - => namedType.GetMembers() - .OfType() - .Where(m => IsTwoArgumentSliceLikeMethod(m)) - .FirstOrDefault(); + var stringType = compilation.GetSpecialType(SpecialType.System_String); + infoCache = new InfoCache(rangeType, stringType, compilation.ExpressionOfTType()); + return true; + } + + private static IMethodSymbol? GetSliceLikeMethod(INamedTypeSymbol namedType) + => namedType.GetMembers() + .OfType() + .Where(m => IsTwoArgumentSliceLikeMethod(m)) + .FirstOrDefault(); - public bool TryGetMemberInfo(IMethodSymbol method, out MemberInfo memberInfo) + public bool TryGetMemberInfo(IMethodSymbol method, out MemberInfo memberInfo) + { + if (!IsTwoArgumentSliceLikeMethod(method)) { - if (!IsTwoArgumentSliceLikeMethod(method)) - { - memberInfo = default; - return false; - } + memberInfo = default; + return false; + } - memberInfo = _methodToMemberInfo.GetOrAdd(method, m => ComputeMemberInfo(m, requireRangeMember: true)); - return memberInfo.LengthLikeProperty != null; + memberInfo = _methodToMemberInfo.GetOrAdd(method, m => ComputeMemberInfo(m, requireRangeMember: true)); + return memberInfo.LengthLikeProperty != null; + } + + public bool TryGetMemberInfoOneArgument(IMethodSymbol method, out MemberInfo memberInfo) + { + if (!IsOneArgumentSliceLikeMethod(method)) + { + memberInfo = default; + return false; } - public bool TryGetMemberInfoOneArgument(IMethodSymbol method, out MemberInfo memberInfo) + if (!_methodToMemberInfo.TryGetValue(method, out memberInfo)) { - if (!IsOneArgumentSliceLikeMethod(method)) + // Find overload of our method that is a slice-like method with two arguments. + // Computing member info for this method will also check that the containing type + // has an int32 'Length' or 'Count' property, and has a suitable indexer, + // so we don't have to. + var overloadWithTwoArguments = method.ContainingType + .GetMembers(method.Name) + .OfType() + .FirstOrDefault(s => IsTwoArgumentSliceLikeMethod(s)); + if (overloadWithTwoArguments is null) { memberInfo = default; return false; } - if (!_methodToMemberInfo.TryGetValue(method, out memberInfo)) - { - // Find overload of our method that is a slice-like method with two arguments. - // Computing member info for this method will also check that the containing type - // has an int32 'Length' or 'Count' property, and has a suitable indexer, - // so we don't have to. - var overloadWithTwoArguments = method.ContainingType - .GetMembers(method.Name) - .OfType() - .FirstOrDefault(s => IsTwoArgumentSliceLikeMethod(s)); - if (overloadWithTwoArguments is null) - { - memberInfo = default; - return false; - } - - // Since the search is expensive, we keep both the original one-argument and - // two-arguments overload as keys in the cache, pointing to the same - // member information object. - var newMemberInfo = _methodToMemberInfo.GetOrAdd(overloadWithTwoArguments, _ => ComputeMemberInfo(overloadWithTwoArguments, requireRangeMember: true)); - _methodToMemberInfo.GetOrAdd(method, _ => newMemberInfo); - memberInfo = newMemberInfo; - } - - return memberInfo.LengthLikeProperty != null; + // Since the search is expensive, we keep both the original one-argument and + // two-arguments overload as keys in the cache, pointing to the same + // member information object. + var newMemberInfo = _methodToMemberInfo.GetOrAdd(overloadWithTwoArguments, _ => ComputeMemberInfo(overloadWithTwoArguments, requireRangeMember: true)); + _methodToMemberInfo.GetOrAdd(method, _ => newMemberInfo); + memberInfo = newMemberInfo; } - private MemberInfo ComputeMemberInfo(IMethodSymbol sliceLikeMethod, bool requireRangeMember) + return memberInfo.LengthLikeProperty != null; + } + + private MemberInfo ComputeMemberInfo(IMethodSymbol sliceLikeMethod, bool requireRangeMember) + { + Debug.Assert(IsTwoArgumentSliceLikeMethod(sliceLikeMethod)); + + // Check that the type has an int32 'Length' or 'Count' property. If not, we don't + // consider it something indexable. + var containingType = sliceLikeMethod.ContainingType; + var lengthLikeProperty = TryGetLengthOrCountProperty(containingType); + if (lengthLikeProperty == null) { - Debug.Assert(IsTwoArgumentSliceLikeMethod(sliceLikeMethod)); + return default; + } - // Check that the type has an int32 'Length' or 'Count' property. If not, we don't - // consider it something indexable. - var containingType = sliceLikeMethod.ContainingType; - var lengthLikeProperty = TryGetLengthOrCountProperty(containingType); - if (lengthLikeProperty == null) - { - return default; - } + if (!requireRangeMember) + { + return new MemberInfo(lengthLikeProperty, overloadedMethodOpt: null); + } - if (!requireRangeMember) + // A Slice method can either be paired with an Range-taking indexer on the type, or + // an Range-taking overload, or an explicit method called .Slice that takes two ints: + // + // https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-range-support + if (sliceLikeMethod.ReturnType.Equals(containingType)) + { + // it's a method like: MyType MyType.Get(int start, int length). Look for an + // indexer like `MyType MyType.this[Range range]`. + var indexer = GetIndexer(containingType, RangeType, containingType); + if (indexer != null) { return new MemberInfo(lengthLikeProperty, overloadedMethodOpt: null); } - // A Slice method can either be paired with an Range-taking indexer on the type, or - // an Range-taking overload, or an explicit method called .Slice that takes two ints: - // - // https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-range-support - if (sliceLikeMethod.ReturnType.Equals(containingType)) - { - // it's a method like: MyType MyType.Get(int start, int length). Look for an - // indexer like `MyType MyType.this[Range range]`. - var indexer = GetIndexer(containingType, RangeType, containingType); - if (indexer != null) - { - return new MemberInfo(lengthLikeProperty, overloadedMethodOpt: null); - } - - // Also, look to see if the type has a `.Slice(int start, int length)` method. - // This is also a method the compiler knows to look for when a user writes `x[a..b]` - var actualSliceMethod = - sliceLikeMethod.ContainingType.GetMembers(nameof(Span.Slice)) - .OfType() - .FirstOrDefault(s => IsTwoArgumentSliceLikeMethod(s)); - if (actualSliceMethod != null) - { - return new MemberInfo(lengthLikeProperty, overloadedMethodOpt: null); - } - } - - // it's a method like: `SomeType MyType.Get(int start, int length)`. Look - // for an overload like: `SomeType MyType.Get(Range)` - var overloadedRangeMethod = GetOverload(sliceLikeMethod, RangeType); - if (overloadedRangeMethod != null) + // Also, look to see if the type has a `.Slice(int start, int length)` method. + // This is also a method the compiler knows to look for when a user writes `x[a..b]` + var actualSliceMethod = + sliceLikeMethod.ContainingType.GetMembers(nameof(Span.Slice)) + .OfType() + .FirstOrDefault(s => IsTwoArgumentSliceLikeMethod(s)); + if (actualSliceMethod != null) { - return new MemberInfo(lengthLikeProperty, overloadedRangeMethod); + return new MemberInfo(lengthLikeProperty, overloadedMethodOpt: null); } + } - // A slice-like method that we can't convert. - return default; + // it's a method like: `SomeType MyType.Get(int start, int length)`. Look + // for an overload like: `SomeType MyType.Get(Range)` + var overloadedRangeMethod = GetOverload(sliceLikeMethod, RangeType); + if (overloadedRangeMethod != null) + { + return new MemberInfo(lengthLikeProperty, overloadedRangeMethod); } + + // A slice-like method that we can't convert. + return default; } } } diff --git a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.Result.cs b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.Result.cs index 356397ceaf1cc..0be90e13e761d 100644 --- a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.Result.cs +++ b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.Result.cs @@ -6,39 +6,38 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator +namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator; + +internal partial class CSharpUseRangeOperatorDiagnosticAnalyzer { - internal partial class CSharpUseRangeOperatorDiagnosticAnalyzer + public enum ResultKind { - public enum ResultKind - { - // like s.Substring(expr, s.Length - expr) or s.Substring(expr). 'expr' has to match on both sides. - Computed, + // like s.Substring(expr, s.Length - expr) or s.Substring(expr). 'expr' has to match on both sides. + Computed, - // like s.Substring(constant1, s.Length - constant2). the constants don't have to match. - Constant, - } + // like s.Substring(constant1, s.Length - constant2). the constants don't have to match. + Constant, + } - public readonly struct Result( - ResultKind kind, - IInvocationOperation invocationOperation, - InvocationExpressionSyntax invocation, - IMethodSymbol sliceLikeMethod, - MemberInfo memberInfo, - IOperation op1, - IOperation? op2) - { - public readonly ResultKind Kind = kind; - public readonly IInvocationOperation InvocationOperation = invocationOperation; - public readonly InvocationExpressionSyntax Invocation = invocation; - public readonly IMethodSymbol SliceLikeMethod = sliceLikeMethod; - public readonly MemberInfo MemberInfo = memberInfo; - public readonly IOperation Op1 = op1; + public readonly struct Result( + ResultKind kind, + IInvocationOperation invocationOperation, + InvocationExpressionSyntax invocation, + IMethodSymbol sliceLikeMethod, + MemberInfo memberInfo, + IOperation op1, + IOperation? op2) + { + public readonly ResultKind Kind = kind; + public readonly IInvocationOperation InvocationOperation = invocationOperation; + public readonly InvocationExpressionSyntax Invocation = invocation; + public readonly IMethodSymbol SliceLikeMethod = sliceLikeMethod; + public readonly MemberInfo MemberInfo = memberInfo; + public readonly IOperation Op1 = op1; - /// - /// Can be null, if we are dealing with one-argument call to a slice-like method. - /// - public readonly IOperation? Op2 = op2; - } + /// + /// Can be null, if we are dealing with one-argument call to a slice-like method. + /// + public readonly IOperation? Op2 = op2; } } diff --git a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.cs index 8f8fda2d1bc06..395afd70de3ec 100644 --- a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/CSharpUseRangeOperatorDiagnosticAnalyzer.cs @@ -16,267 +16,267 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator +namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator; + +using static Helpers; + +/// +/// Analyzer that looks for several variants of code like s.Slice(start, end - start) and +/// offers to update to s[start..end] or s.Slice(start..end). In order to convert to the +/// indexer, the type being called on needs a slice-like method that takes two ints, and returns +/// an instance of the same type. It also needs a Length/Count property, as well as an indexer +/// that takes a instance. In order to convert between methods, there need to be +/// two overloads that are equivalent except that one takes two ints, and the other takes a +/// . +/// +/// It is assumed that if the type follows this shape that it is well behaved and that this +/// transformation will preserve semantics. If this assumption is not good in practice, we +/// could always limit the feature to only work on an allow list of known safe types. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +[SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")] +internal sealed partial class CSharpUseRangeOperatorDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - using static Helpers; - - /// - /// Analyzer that looks for several variants of code like s.Slice(start, end - start) and - /// offers to update to s[start..end] or s.Slice(start..end). In order to convert to the - /// indexer, the type being called on needs a slice-like method that takes two ints, and returns - /// an instance of the same type. It also needs a Length/Count property, as well as an indexer - /// that takes a instance. In order to convert between methods, there need to be - /// two overloads that are equivalent except that one takes two ints, and the other takes a - /// . - /// - /// It is assumed that if the type follows this shape that it is well behaved and that this - /// transformation will preserve semantics. If this assumption is not good in practice, we - /// could always limit the feature to only work on an allow list of known safe types. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - [SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")] - internal sealed partial class CSharpUseRangeOperatorDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + // public const string UseIndexer = nameof(UseIndexer); + public const string ComputedRange = nameof(ComputedRange); + public const string ConstantRange = nameof(ConstantRange); + + public CSharpUseRangeOperatorDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseRangeOperatorDiagnosticId, + EnforceOnBuildValues.UseRangeOperator, + CSharpCodeStyleOptions.PreferRangeOperator, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_range_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources._0_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - // public const string UseIndexer = nameof(UseIndexer); - public const string ComputedRange = nameof(ComputedRange); - public const string ConstantRange = nameof(ConstantRange); - - public CSharpUseRangeOperatorDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseRangeOperatorDiagnosticId, - EnforceOnBuildValues.UseRangeOperator, - CSharpCodeStyleOptions.PreferRangeOperator, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_range_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources._0_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - context.RegisterCompilationStartAction(context => - { - var compilation = (CSharpCompilation)context.Compilation; - - // Check if we're at least on C# 8 - if (compilation.LanguageVersion < LanguageVersion.CSharp8) - return; - - // We're going to be checking every invocation in the compilation. Cache information - // we compute in this object so we don't have to continually recompute it. - if (!InfoCache.TryCreate(context.Compilation, out var infoCache)) - return; - - context.RegisterOperationAction( - c => AnalyzeInvocation(c, infoCache), - OperationKind.Invocation); - }); - } + var compilation = (CSharpCompilation)context.Compilation; - private void AnalyzeInvocation(OperationAnalysisContext context, InfoCache infoCache) - { - // Check if the user wants these operators. - var option = context.GetCSharpAnalyzerOptions().PreferRangeOperator; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + // Check if we're at least on C# 8 + if (compilation.LanguageVersion < LanguageVersion.CSharp8) return; - var operation = context.Operation; - var semanticModel = operation.SemanticModel; - Contract.ThrowIfNull(semanticModel); - - var result = AnalyzeInvocation((IInvocationOperation)operation, infoCache); - if (result == null) + // We're going to be checking every invocation in the compilation. Cache information + // we compute in this object so we don't have to continually recompute it. + if (!InfoCache.TryCreate(context.Compilation, out var infoCache)) return; - if (CSharpSemanticFacts.Instance.IsInExpressionTree(semanticModel, operation.Syntax, infoCache.ExpressionOfTType, context.CancellationToken)) - return; + context.RegisterOperationAction( + c => AnalyzeInvocation(c, infoCache), + OperationKind.Invocation); + }); + } - context.ReportDiagnostic(CreateDiagnostic(result.Value, option.Notification)); - } + private void AnalyzeInvocation(OperationAnalysisContext context, InfoCache infoCache) + { + // Check if the user wants these operators. + var option = context.GetCSharpAnalyzerOptions().PreferRangeOperator; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; - public static Result? AnalyzeInvocation(IInvocationOperation invocation, InfoCache infoCache) - { - // Validate we're on a piece of syntax we expect. While not necessary for analysis, we - // want to make sure we're on something the fixer will know how to actually fix. - if (invocation.Syntax is not InvocationExpressionSyntax invocationSyntax || - invocationSyntax.ArgumentList is null) - { - return null; - } - - // look for `s.Slice(e1, end - e2)` or `s.Slice(e1)` - if (invocation.Instance is null) - return null; - - return invocation.Arguments.Length switch - { - 1 => AnalyzeOneArgumentInvocation(invocation, infoCache, invocationSyntax), - 2 => AnalyzeTwoArgumentInvocation(invocation, infoCache, invocationSyntax), - _ => null, - }; - } + var operation = context.Operation; + var semanticModel = operation.SemanticModel; + Contract.ThrowIfNull(semanticModel); - private static Result? AnalyzeOneArgumentInvocation( - IInvocationOperation invocation, - InfoCache infoCache, - InvocationExpressionSyntax invocationSyntax) - { - var targetMethod = invocation.TargetMethod; + var result = AnalyzeInvocation((IInvocationOperation)operation, infoCache); + if (result == null) + return; - // We are dealing with a call like `.Substring(expr)`. - // Ensure that there is an overload with signature like `Substring(int start, int length)` - // and there is a suitable indexer to replace this with `[expr..]`. - if (!infoCache.TryGetMemberInfoOneArgument(targetMethod, out var memberInfo)) - return null; + if (CSharpSemanticFacts.Instance.IsInExpressionTree(semanticModel, operation.Syntax, infoCache.ExpressionOfTType, context.CancellationToken)) + return; - var startOperation = invocation.Arguments[0].Value; - return new Result( - ResultKind.Computed, - invocation, - invocationSyntax, - targetMethod, - memberInfo, - op1: startOperation, - op2: null); // The range will run to the end. + context.ReportDiagnostic(CreateDiagnostic(result.Value, option.Notification, context.Options)); + } + + public static Result? AnalyzeInvocation(IInvocationOperation invocation, InfoCache infoCache) + { + // Validate we're on a piece of syntax we expect. While not necessary for analysis, we + // want to make sure we're on something the fixer will know how to actually fix. + if (invocation.Syntax is not InvocationExpressionSyntax invocationSyntax || + invocationSyntax.ArgumentList is null) + { + return null; } - private static Result? AnalyzeTwoArgumentInvocation( - IInvocationOperation invocation, - InfoCache infoCache, - InvocationExpressionSyntax invocationSyntax) + // look for `s.Slice(e1, end - e2)` or `s.Slice(e1)` + if (invocation.Instance is null) + return null; + + return invocation.Arguments.Length switch { - Contract.ThrowIfNull(invocation.Instance); + 1 => AnalyzeOneArgumentInvocation(invocation, infoCache, invocationSyntax), + 2 => AnalyzeTwoArgumentInvocation(invocation, infoCache, invocationSyntax), + _ => null, + }; + } - // See if the call is to something slice-like. - var targetMethod = invocation.TargetMethod; - if (targetMethod == null) - return null; + private static Result? AnalyzeOneArgumentInvocation( + IInvocationOperation invocation, + InfoCache infoCache, + InvocationExpressionSyntax invocationSyntax) + { + var targetMethod = invocation.TargetMethod; - return AnalyzeTwoArgumentSubtractionInvocation(invocation, infoCache, invocationSyntax, targetMethod) ?? - AnalyzeTwoArgumentFromStartOrToEndInvocation(invocation, infoCache, invocationSyntax, targetMethod); - } + // We are dealing with a call like `.Substring(expr)`. + // Ensure that there is an overload with signature like `Substring(int start, int length)` + // and there is a suitable indexer to replace this with `[expr..]`. + if (!infoCache.TryGetMemberInfoOneArgument(targetMethod, out var memberInfo)) + return null; - private static Result? AnalyzeTwoArgumentSubtractionInvocation( - IInvocationOperation invocation, - InfoCache infoCache, - InvocationExpressionSyntax invocationSyntax, - IMethodSymbol targetMethod) - { - Contract.ThrowIfNull(invocation.Instance); - - // Second arg needs to be a subtraction for: `end - e2`. Once we've seen that we have - // that, try to see if we're calling into some sort of Slice method with a matching - // indexer or overload - if (!IsSubtraction(invocation.Arguments[1].Value, out var subtraction) || - !infoCache.TryGetMemberInfo(targetMethod, out var memberInfo)) - { - return null; - } - - if (!IsValidIndexing(invocation, infoCache, targetMethod)) - return null; - - // See if we have: (start, end - start). Specifically where the start operation it the - // same as the right side of the subtraction. - var startOperation = invocation.Arguments[0].Value; - - if (CSharpSyntaxFacts.Instance.AreEquivalent(startOperation.Syntax, subtraction.RightOperand.Syntax)) - { - return new Result( - ResultKind.Computed, - invocation, invocationSyntax, - targetMethod, memberInfo, - startOperation, subtraction.LeftOperand); - } - - // See if we have: (constant1, s.Length - constant2). The constants don't have to be - // the same value. This will convert over to s[constant1..(constant - constant1)] - if (IsConstantInt32(startOperation) && - IsConstantInt32(subtraction.RightOperand) && - IsInstanceLengthCheck(memberInfo.LengthLikeProperty, invocation.Instance, subtraction.LeftOperand)) - { - return new Result( - ResultKind.Constant, - invocation, invocationSyntax, - targetMethod, memberInfo, - startOperation, subtraction.RightOperand); - } + var startOperation = invocation.Arguments[0].Value; + return new Result( + ResultKind.Computed, + invocation, + invocationSyntax, + targetMethod, + memberInfo, + op1: startOperation, + op2: null); // The range will run to the end. + } + + private static Result? AnalyzeTwoArgumentInvocation( + IInvocationOperation invocation, + InfoCache infoCache, + InvocationExpressionSyntax invocationSyntax) + { + Contract.ThrowIfNull(invocation.Instance); + // See if the call is to something slice-like. + var targetMethod = invocation.TargetMethod; + if (targetMethod == null) return null; - } - private static Result? AnalyzeTwoArgumentFromStartOrToEndInvocation( - IInvocationOperation invocation, - InfoCache infoCache, - InvocationExpressionSyntax invocationSyntax, - IMethodSymbol targetMethod) - { - Contract.ThrowIfNull(invocation.Instance); + return AnalyzeTwoArgumentSubtractionInvocation(invocation, infoCache, invocationSyntax, targetMethod) ?? + AnalyzeTwoArgumentFromStartOrToEndInvocation(invocation, infoCache, invocationSyntax, targetMethod); + } - // if we have `x.Substring(0, end)` then that can just become `x[..end]` - // if we have `x.Substring(0, x.Length)` then that can just become `x[..]` - // if we have `x.Substring(0, x.Length - n)` then that is handled in AnalyzeTwoArgumentSubtractionInvocation + private static Result? AnalyzeTwoArgumentSubtractionInvocation( + IInvocationOperation invocation, + InfoCache infoCache, + InvocationExpressionSyntax invocationSyntax, + IMethodSymbol targetMethod) + { + Contract.ThrowIfNull(invocation.Instance); - var startOperation = invocation.Arguments[0].Value; - if (!IsConstantInt32(startOperation, value: 0) || - !infoCache.TryGetMemberInfo(targetMethod, out var memberInfo)) - { - return null; - } + // Second arg needs to be a subtraction for: `end - e2`. Once we've seen that we have + // that, try to see if we're calling into some sort of Slice method with a matching + // indexer or overload + if (!IsSubtraction(invocation.Arguments[1].Value, out var subtraction) || + !infoCache.TryGetMemberInfo(targetMethod, out var memberInfo)) + { + return null; + } - if (!IsValidIndexing(invocation, infoCache, targetMethod)) - return null; + if (!IsValidIndexing(invocation, infoCache, targetMethod)) + return null; + // See if we have: (start, end - start). Specifically where the start operation it the + // same as the right side of the subtraction. + var startOperation = invocation.Arguments[0].Value; + + if (CSharpSyntaxFacts.Instance.AreEquivalent(startOperation.Syntax, subtraction.RightOperand.Syntax)) + { return new Result( ResultKind.Computed, - invocation, - invocationSyntax, - targetMethod, - memberInfo, - startOperation, - invocation.Arguments[1].Value); + invocation, invocationSyntax, + targetMethod, memberInfo, + startOperation, subtraction.LeftOperand); } - private static bool IsValidIndexing(IInvocationOperation invocation, InfoCache infoCache, IMethodSymbol targetMethod) + // See if we have: (constant1, s.Length - constant2). The constants don't have to be + // the same value. This will convert over to s[constant1..(constant - constant1)] + if (IsConstantInt32(startOperation) && + IsConstantInt32(subtraction.RightOperand) && + IsInstanceLengthCheck(memberInfo.LengthLikeProperty, invocation.Instance, subtraction.LeftOperand)) { - var indexer = GetIndexer(targetMethod.ContainingType, infoCache.RangeType, targetMethod.ContainingType); - // Need to make sure that if the target method is being written to, that the indexer returns a ref, is a read/write property, - // or the syntax allows for the slice method to be run - return !invocation.Syntax.IsLeftSideOfAnyAssignExpression() || indexer == null || !IsWriteableIndexer(invocation, indexer); + return new Result( + ResultKind.Constant, + invocation, invocationSyntax, + targetMethod, memberInfo, + startOperation, subtraction.RightOperand); } - private Diagnostic CreateDiagnostic(Result result, NotificationOption2 notificationOption) - { - // Keep track of the invocation node - var invocation = result.Invocation; - var additionalLocations = ImmutableArray.Create( - invocation.GetLocation()); - - // Mark the span under the two arguments to .Slice(..., ...) as what we will be - // updating. - var arguments = invocation.ArgumentList.Arguments; - var location = Location.Create(invocation.SyntaxTree, - TextSpan.FromBounds(arguments.First().SpanStart, arguments.Last().Span.End)); - - return DiagnosticHelper.Create( - Descriptor, - location, - notificationOption, - additionalLocations, - ImmutableDictionary.Empty, - result.SliceLikeMethod.Name); - } + return null; + } + + private static Result? AnalyzeTwoArgumentFromStartOrToEndInvocation( + IInvocationOperation invocation, + InfoCache infoCache, + InvocationExpressionSyntax invocationSyntax, + IMethodSymbol targetMethod) + { + Contract.ThrowIfNull(invocation.Instance); - private static bool IsConstantInt32(IOperation operation, int? value = null) - => operation.ConstantValue.HasValue && - operation.ConstantValue.Value is int i && - (value == null || i == value); + // if we have `x.Substring(0, end)` then that can just become `x[..end]` + // if we have `x.Substring(0, x.Length)` then that can just become `x[..]` + // if we have `x.Substring(0, x.Length - n)` then that is handled in AnalyzeTwoArgumentSubtractionInvocation - private static bool IsWriteableIndexer(IInvocationOperation invocation, IPropertySymbol indexer) + var startOperation = invocation.Arguments[0].Value; + if (!IsConstantInt32(startOperation, value: 0) || + !infoCache.TryGetMemberInfo(targetMethod, out var memberInfo)) { - var refReturnMismatch = indexer.ReturnsByRef != invocation.TargetMethod.ReturnsByRef; - var indexerIsReadWrite = indexer.IsWriteableFieldOrProperty(); - return refReturnMismatch && !indexerIsReadWrite; + return null; } + + if (!IsValidIndexing(invocation, infoCache, targetMethod)) + return null; + + return new Result( + ResultKind.Computed, + invocation, + invocationSyntax, + targetMethod, + memberInfo, + startOperation, + invocation.Arguments[1].Value); + } + + private static bool IsValidIndexing(IInvocationOperation invocation, InfoCache infoCache, IMethodSymbol targetMethod) + { + var indexer = GetIndexer(targetMethod.ContainingType, infoCache.RangeType, targetMethod.ContainingType); + // Need to make sure that if the target method is being written to, that the indexer returns a ref, is a read/write property, + // or the syntax allows for the slice method to be run + return !invocation.Syntax.IsLeftSideOfAnyAssignExpression() || indexer == null || !IsWriteableIndexer(invocation, indexer); + } + + private Diagnostic CreateDiagnostic(Result result, NotificationOption2 notificationOption, AnalyzerOptions analyzerOptions) + { + // Keep track of the invocation node + var invocation = result.Invocation; + var additionalLocations = ImmutableArray.Create( + invocation.GetLocation()); + + // Mark the span under the two arguments to .Slice(..., ...) as what we will be + // updating. + var arguments = invocation.ArgumentList.Arguments; + var location = Location.Create(invocation.SyntaxTree, + TextSpan.FromBounds(arguments.First().SpanStart, arguments.Last().Span.End)); + + return DiagnosticHelper.Create( + Descriptor, + location, + notificationOption, + analyzerOptions, + additionalLocations, + ImmutableDictionary.Empty, + result.SliceLikeMethod.Name); + } + + private static bool IsConstantInt32(IOperation operation, int? value = null) + => operation.ConstantValue.HasValue && + operation.ConstantValue.Value is int i && + (value == null || i == value); + + private static bool IsWriteableIndexer(IInvocationOperation invocation, IPropertySymbol indexer) + { + var refReturnMismatch = indexer.ReturnsByRef != invocation.TargetMethod.ReturnsByRef; + var indexerIsReadWrite = indexer.IsWriteableFieldOrProperty(); + return refReturnMismatch && !indexerIsReadWrite; } } diff --git a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/Helpers.cs b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/Helpers.cs index 2df9459761a25..62dea09cde487 100644 --- a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/Helpers.cs +++ b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/Helpers.cs @@ -8,146 +8,145 @@ using Microsoft.CodeAnalysis.CSharp.LanguageService; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator +namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator; + +internal static class Helpers { - internal static class Helpers - { - /// - /// Find an `int MyType.Count` or `int MyType.Length` property. - /// - public static IPropertySymbol? TryGetLengthOrCountProperty(ITypeSymbol namedType) - => TryGetNoArgInt32Property(namedType, nameof(string.Length)) ?? - TryGetNoArgInt32Property(namedType, nameof(ICollection.Count)); + /// + /// Find an `int MyType.Count` or `int MyType.Length` property. + /// + public static IPropertySymbol? TryGetLengthOrCountProperty(ITypeSymbol namedType) + => TryGetNoArgInt32Property(namedType, nameof(string.Length)) ?? + TryGetNoArgInt32Property(namedType, nameof(ICollection.Count)); - /// - /// Tried to find a public, non-static, int-returning property in the given type with the - /// specified . - /// - public static IPropertySymbol? TryGetNoArgInt32Property(ITypeSymbol type, string name) - => type.GetMembers(name) - .OfType() - .Where(p => IsPublicInstance(p) && - p.Type.SpecialType == SpecialType.System_Int32) - .FirstOrDefault(); + /// + /// Tried to find a public, non-static, int-returning property in the given type with the + /// specified . + /// + public static IPropertySymbol? TryGetNoArgInt32Property(ITypeSymbol type, string name) + => type.GetMembers(name) + .OfType() + .Where(p => IsPublicInstance(p) && + p.Type.SpecialType == SpecialType.System_Int32) + .FirstOrDefault(); - public static bool IsPublicInstance(ISymbol symbol) - => !symbol.IsStatic && symbol.DeclaredAccessibility == Accessibility.Public; + public static bool IsPublicInstance(ISymbol symbol) + => !symbol.IsStatic && symbol.DeclaredAccessibility == Accessibility.Public; - /// - /// Checks if this is `expr.Length` where `expr` is equivalent - /// to the we were originally invoking an accessor/method off - /// of. - /// - public static bool IsInstanceLengthCheck(IPropertySymbol lengthLikeProperty, IOperation instance, IOperation operation) - => operation is IPropertyReferenceOperation propertyRef && - propertyRef.Instance != null && - lengthLikeProperty.Equals(propertyRef.Property) && - CSharpSyntaxFacts.Instance.AreEquivalent(instance.Syntax, propertyRef.Instance.Syntax); + /// + /// Checks if this is `expr.Length` where `expr` is equivalent + /// to the we were originally invoking an accessor/method off + /// of. + /// + public static bool IsInstanceLengthCheck(IPropertySymbol lengthLikeProperty, IOperation instance, IOperation operation) + => operation is IPropertyReferenceOperation propertyRef && + propertyRef.Instance != null && + lengthLikeProperty.Equals(propertyRef.Property) && + CSharpSyntaxFacts.Instance.AreEquivalent(instance.Syntax, propertyRef.Instance.Syntax); - /// - /// Checks if is a binary subtraction operator. If so, it - /// will be returned through . - /// - public static bool IsSubtraction(IOperation operation, [NotNullWhen(true)] out IBinaryOperation? subtraction) + /// + /// Checks if is a binary subtraction operator. If so, it + /// will be returned through . + /// + public static bool IsSubtraction(IOperation operation, [NotNullWhen(true)] out IBinaryOperation? subtraction) + { + if (operation is IBinaryOperation binaryOperation && + binaryOperation.OperatorKind == BinaryOperatorKind.Subtract) { - if (operation is IBinaryOperation binaryOperation && - binaryOperation.OperatorKind == BinaryOperatorKind.Subtract) - { - subtraction = binaryOperation; - return true; - } - - subtraction = null; - return false; + subtraction = binaryOperation; + return true; } - /// - /// Look for methods like "SomeType MyType.Get(int)". Also matches against the 'getter' - /// of an indexer like 'SomeType MyType.this[int]` - /// - public static bool IsIntIndexingMethod(IMethodSymbol method) - => method != null && - method.MethodKind is MethodKind.PropertyGet or MethodKind.Ordinary && - IsPublicInstance(method) && - method.Parameters.Length == 1 && - // From: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#decisions-made-during-implementation - // - // When looking for the pattern members, we look for original definitions, not - // constructed members - method.OriginalDefinition.Parameters[0].Type.SpecialType == SpecialType.System_Int32; + subtraction = null; + return false; + } - /// - /// Look for methods like "SomeType MyType.Slice(int start, int length)". Note that the - /// names of the parameters are checked to ensure they are appropriate slice-like. These - /// names were picked by examining the patterns in the BCL for slicing members. - /// - public static bool IsTwoArgumentSliceLikeMethod(IMethodSymbol method) - { - // From: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#decisions-made-during-implementation - // - // When looking for the pattern members, we look for original definitions, not - // constructed members - return method != null && - IsPublicInstance(method) && - method.Parameters.Length == 2 && - IsSliceFirstParameter(method.OriginalDefinition.Parameters[0]) && - IsSliceSecondParameter(method.OriginalDefinition.Parameters[1]); - } + /// + /// Look for methods like "SomeType MyType.Get(int)". Also matches against the 'getter' + /// of an indexer like 'SomeType MyType.this[int]` + /// + public static bool IsIntIndexingMethod(IMethodSymbol method) + => method != null && + method.MethodKind is MethodKind.PropertyGet or MethodKind.Ordinary && + IsPublicInstance(method) && + method.Parameters.Length == 1 && + // From: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#decisions-made-during-implementation + // + // When looking for the pattern members, we look for original definitions, not + // constructed members + method.OriginalDefinition.Parameters[0].Type.SpecialType == SpecialType.System_Int32; - /// - /// Look for methods like "SomeType MyType.Slice(int start)". Note that the - /// name of the parameter is checked to ensure it is appropriate slice-like. - /// This name was picked by examining the patterns in the BCL for slicing members. - /// - public static bool IsOneArgumentSliceLikeMethod(IMethodSymbol method) - { - // From: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#decisions-made-during-implementation - // - // When looking for the pattern members, we look for original definitions, not - // constructed members - return method != null && - IsPublicInstance(method) && - method.Parameters.Length == 1 && - IsSliceFirstParameter(method.OriginalDefinition.Parameters[0]); - } + /// + /// Look for methods like "SomeType MyType.Slice(int start, int length)". Note that the + /// names of the parameters are checked to ensure they are appropriate slice-like. These + /// names were picked by examining the patterns in the BCL for slicing members. + /// + public static bool IsTwoArgumentSliceLikeMethod(IMethodSymbol method) + { + // From: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#decisions-made-during-implementation + // + // When looking for the pattern members, we look for original definitions, not + // constructed members + return method != null && + IsPublicInstance(method) && + method.Parameters.Length == 2 && + IsSliceFirstParameter(method.OriginalDefinition.Parameters[0]) && + IsSliceSecondParameter(method.OriginalDefinition.Parameters[1]); + } - private static bool IsSliceFirstParameter(IParameterSymbol parameter) - => parameter.Type.SpecialType == SpecialType.System_Int32 && - (parameter.Name == "start" || parameter.Name == "startIndex"); + /// + /// Look for methods like "SomeType MyType.Slice(int start)". Note that the + /// name of the parameter is checked to ensure it is appropriate slice-like. + /// This name was picked by examining the patterns in the BCL for slicing members. + /// + public static bool IsOneArgumentSliceLikeMethod(IMethodSymbol method) + { + // From: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#decisions-made-during-implementation + // + // When looking for the pattern members, we look for original definitions, not + // constructed members + return method != null && + IsPublicInstance(method) && + method.Parameters.Length == 1 && + IsSliceFirstParameter(method.OriginalDefinition.Parameters[0]); + } - private static bool IsSliceSecondParameter(IParameterSymbol parameter) - => parameter.Type.SpecialType == SpecialType.System_Int32 && - (parameter.Name == "count" || parameter.Name == "length"); + private static bool IsSliceFirstParameter(IParameterSymbol parameter) + => parameter.Type.SpecialType == SpecialType.System_Int32 && + (parameter.Name == "start" || parameter.Name == "startIndex"); - /// - /// Finds a public, non-static indexer in the given type. The indexer has to accept the - /// provided and must return the provided . - /// - public static IPropertySymbol? GetIndexer(ITypeSymbol type, ITypeSymbol parameterType, ITypeSymbol returnType) - => type.GetMembers(WellKnownMemberNames.Indexer) - .OfType() - .Where(p => p.IsIndexer && - IsPublicInstance(p) && - returnType.Equals(p.Type) && - p.Parameters.Length == 1 && - p.Parameters[0].Type.Equals(parameterType)) - .FirstOrDefault(); + private static bool IsSliceSecondParameter(IParameterSymbol parameter) + => parameter.Type.SpecialType == SpecialType.System_Int32 && + (parameter.Name == "count" || parameter.Name == "length"); - /// - /// Finds a public, non-static overload of in the containing type. - /// The overload must have the same return type as . It must only - /// have a single parameter, with the provided . - /// - public static IMethodSymbol? GetOverload(IMethodSymbol method, ITypeSymbol parameterType) - => method.MethodKind != MethodKind.Ordinary - ? null - : method.ContainingType.GetMembers(method.Name) - .OfType() - .Where(m => IsPublicInstance(m) && - m.Parameters.Length == 1 && - m.Parameters[0].Type.Equals(parameterType) && - m.ReturnType.Equals(method.ReturnType)) - .FirstOrDefault(); - } + /// + /// Finds a public, non-static indexer in the given type. The indexer has to accept the + /// provided and must return the provided . + /// + public static IPropertySymbol? GetIndexer(ITypeSymbol type, ITypeSymbol parameterType, ITypeSymbol returnType) + => type.GetMembers(WellKnownMemberNames.Indexer) + .OfType() + .Where(p => p.IsIndexer && + IsPublicInstance(p) && + returnType.Equals(p.Type) && + p.Parameters.Length == 1 && + p.Parameters[0].Type.Equals(parameterType)) + .FirstOrDefault(); + + /// + /// Finds a public, non-static overload of in the containing type. + /// The overload must have the same return type as . It must only + /// have a single parameter, with the provided . + /// + public static IMethodSymbol? GetOverload(IMethodSymbol method, ITypeSymbol parameterType) + => method.MethodKind != MethodKind.Ordinary + ? null + : method.ContainingType.GetMembers(method.Name) + .OfType() + .Where(m => IsPublicInstance(m) && + m.Parameters.Length == 1 && + m.Parameters[0].Type.Equals(parameterType) && + m.ReturnType.Equals(method.ReturnType)) + .FirstOrDefault(); } diff --git a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/MemberInfo.cs b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/MemberInfo.cs index bdc115ac67762..3f3c287d20e6b 100644 --- a/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/MemberInfo.cs +++ b/src/Analyzers/CSharp/Analyzers/UseIndexOrRangeOperator/MemberInfo.cs @@ -4,22 +4,21 @@ using System.Diagnostics.CodeAnalysis; -namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator +namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator; + +internal readonly struct MemberInfo( + IPropertySymbol lengthLikeProperty, + IMethodSymbol? overloadedMethodOpt) { - internal readonly struct MemberInfo( - IPropertySymbol lengthLikeProperty, - IMethodSymbol? overloadedMethodOpt) - { - /// - /// The Length/Count property on the type. Must be public, non-static, no-parameter, - /// -returning. - /// - public readonly IPropertySymbol LengthLikeProperty = lengthLikeProperty; + /// + /// The Length/Count property on the type. Must be public, non-static, no-parameter, + /// -returning. + /// + public readonly IPropertySymbol LengthLikeProperty = lengthLikeProperty; - /// - /// Optional paired overload that takes a / parameter instead. - /// - [SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")] - public readonly IMethodSymbol? OverloadedMethodOpt = overloadedMethodOpt; - } + /// + /// Optional paired overload that takes a / parameter instead. + /// + [SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")] + public readonly IMethodSymbol? OverloadedMethodOpt = overloadedMethodOpt; } diff --git a/src/Analyzers/CSharp/Analyzers/UseInferredMemberName/CSharpUseInferredMemberNameDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseInferredMemberName/CSharpUseInferredMemberNameDiagnosticAnalyzer.cs index 45c0c4a37040f..6750377d7e3e8 100644 --- a/src/Analyzers/CSharp/Analyzers/UseInferredMemberName/CSharpUseInferredMemberNameDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseInferredMemberName/CSharpUseInferredMemberNameDiagnosticAnalyzer.cs @@ -13,78 +13,79 @@ using Microsoft.CodeAnalysis.UseInferredMemberName; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseInferredMemberName +namespace Microsoft.CodeAnalysis.CSharp.UseInferredMemberName; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpUseInferredMemberNameDiagnosticAnalyzer : AbstractUseInferredMemberNameDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpUseInferredMemberNameDiagnosticAnalyzer : AbstractUseInferredMemberNameDiagnosticAnalyzer + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.NameColon, SyntaxKind.NameEquals); + + protected override void AnalyzeSyntax(SyntaxNodeAnalysisContext context) { - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.NameColon, SyntaxKind.NameEquals); + switch (context.Node.Kind()) + { + case SyntaxKind.NameColon: + ReportDiagnosticsIfNeeded((NameColonSyntax)context.Node, context); + break; + case SyntaxKind.NameEquals: + ReportDiagnosticsIfNeeded((NameEqualsSyntax)context.Node, context); + break; + } + } - protected override void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + private void ReportDiagnosticsIfNeeded(NameColonSyntax nameColon, SyntaxNodeAnalysisContext context) + { + if (nameColon.Parent is not ArgumentSyntax argument) { - switch (context.Node.Kind()) - { - case SyntaxKind.NameColon: - ReportDiagnosticsIfNeeded((NameColonSyntax)context.Node, context); - break; - case SyntaxKind.NameEquals: - ReportDiagnosticsIfNeeded((NameEqualsSyntax)context.Node, context); - break; - } + return; } - private void ReportDiagnosticsIfNeeded(NameColonSyntax nameColon, SyntaxNodeAnalysisContext context) + var syntaxTree = context.Node.SyntaxTree; + var parseOptions = (CSharpParseOptions)syntaxTree.Options; + var preference = context.GetAnalyzerOptions().PreferInferredTupleNames; + if (!preference.Value + || ShouldSkipAnalysis(context, preference.Notification) + || !CSharpInferredMemberNameSimplifier.CanSimplifyTupleElementName(argument, parseOptions)) { - if (nameColon.Parent is not ArgumentSyntax argument) - { - return; - } + return; + } - var syntaxTree = context.Node.SyntaxTree; - var parseOptions = (CSharpParseOptions)syntaxTree.Options; - var preference = context.GetAnalyzerOptions().PreferInferredTupleNames; - if (!preference.Value - || ShouldSkipAnalysis(context, preference.Notification) - || !CSharpInferredMemberNameSimplifier.CanSimplifyTupleElementName(argument, parseOptions)) - { - return; - } + // Create a normal diagnostic + var fadeSpan = TextSpan.FromBounds(nameColon.Name.SpanStart, nameColon.ColonToken.Span.End); + context.ReportDiagnostic( + DiagnosticHelper.CreateWithLocationTags( + Descriptor, + nameColon.GetLocation(), + preference.Notification, + context.Options, + additionalLocations: [], + additionalUnnecessaryLocations: [syntaxTree.GetLocation(fadeSpan)])); + } - // Create a normal diagnostic - var fadeSpan = TextSpan.FromBounds(nameColon.Name.SpanStart, nameColon.ColonToken.Span.End); - context.ReportDiagnostic( - DiagnosticHelper.CreateWithLocationTags( - Descriptor, - nameColon.GetLocation(), - preference.Notification, - additionalLocations: [], - additionalUnnecessaryLocations: [syntaxTree.GetLocation(fadeSpan)])); + private void ReportDiagnosticsIfNeeded(NameEqualsSyntax nameEquals, SyntaxNodeAnalysisContext context) + { + if (nameEquals.Parent is not AnonymousObjectMemberDeclaratorSyntax anonCtor) + { + return; } - private void ReportDiagnosticsIfNeeded(NameEqualsSyntax nameEquals, SyntaxNodeAnalysisContext context) + var preference = context.GetAnalyzerOptions().PreferInferredAnonymousTypeMemberNames; + if (!preference.Value || + !CSharpInferredMemberNameSimplifier.CanSimplifyAnonymousTypeMemberName(anonCtor)) { - if (nameEquals.Parent is not AnonymousObjectMemberDeclaratorSyntax anonCtor) - { - return; - } - - var preference = context.GetAnalyzerOptions().PreferInferredAnonymousTypeMemberNames; - if (!preference.Value || - !CSharpInferredMemberNameSimplifier.CanSimplifyAnonymousTypeMemberName(anonCtor)) - { - return; - } - - // Create a normal diagnostic - var fadeSpan = TextSpan.FromBounds(nameEquals.Name.SpanStart, nameEquals.EqualsToken.Span.End); - context.ReportDiagnostic( - DiagnosticHelper.CreateWithLocationTags( - Descriptor, - nameEquals.GetLocation(), - preference.Notification, - additionalLocations: [], - additionalUnnecessaryLocations: [context.Node.SyntaxTree.GetLocation(fadeSpan)])); + return; } + + // Create a normal diagnostic + var fadeSpan = TextSpan.FromBounds(nameEquals.Name.SpanStart, nameEquals.EqualsToken.Span.End); + context.ReportDiagnostic( + DiagnosticHelper.CreateWithLocationTags( + Descriptor, + nameEquals.GetLocation(), + preference.Notification, + context.Options, + additionalLocations: [], + additionalUnnecessaryLocations: [context.Node.SyntaxTree.GetLocation(fadeSpan)])); } } diff --git a/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseIsNullCheckForCastAndEqualityOperatorDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseIsNullCheckForCastAndEqualityOperatorDiagnosticAnalyzer.cs index b9a4771d89aa4..e1dbbb180453a 100644 --- a/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseIsNullCheckForCastAndEqualityOperatorDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseIsNullCheckForCastAndEqualityOperatorDiagnosticAnalyzer.cs @@ -9,87 +9,86 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.UseIsNullCheck; -namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck -{ - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseIsNullCheckForCastAndEqualityOperatorDiagnosticAnalyzer - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - { - private static readonly ImmutableDictionary s_properties = - ImmutableDictionary.Empty.Add(UseIsNullConstants.Kind, UseIsNullConstants.CastAndEqualityKey); - private static readonly ImmutableDictionary s_NegatedProperties = - s_properties.Add(UseIsNullConstants.Negated, ""); +namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck; - public CSharpUseIsNullCheckForCastAndEqualityOperatorDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseIsNullCheckDiagnosticId, - EnforceOnBuildValues.UseIsNullCheck, - CodeStyleOptions2.PreferIsNullCheckOverReferenceEqualityMethod, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_is_null_check), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } - - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseIsNullCheckForCastAndEqualityOperatorDiagnosticAnalyzer + : AbstractBuiltInCodeStyleDiagnosticAnalyzer +{ + private static readonly ImmutableDictionary s_properties = + ImmutableDictionary.Empty.Add(UseIsNullConstants.Kind, UseIsNullConstants.CastAndEqualityKey); + private static readonly ImmutableDictionary s_NegatedProperties = + s_properties.Add(UseIsNullConstants.Negated, ""); - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - { - if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp7) - return; + public CSharpUseIsNullCheckForCastAndEqualityOperatorDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseIsNullCheckDiagnosticId, + EnforceOnBuildValues.UseIsNullCheck, + CodeStyleOptions2.PreferIsNullCheckOverReferenceEqualityMethod, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_is_null_check), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) + { + } - context.RegisterSyntaxNodeAction(n => AnalyzeSyntax(n), SyntaxKind.EqualsExpression, SyntaxKind.NotEqualsExpression); - }); + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => { - var option = context.GetAnalyzerOptions().PreferIsNullCheckOverReferenceEqualityMethod; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - { + if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp7) return; - } - var binaryExpression = (BinaryExpressionSyntax)context.Node; - var semanticModel = context.SemanticModel; + context.RegisterSyntaxNodeAction(n => AnalyzeSyntax(n), SyntaxKind.EqualsExpression, SyntaxKind.NotEqualsExpression); + }); - if (!IsObjectCastAndNullCheck(semanticModel, binaryExpression.Left, binaryExpression.Right) && - !IsObjectCastAndNullCheck(semanticModel, binaryExpression.Right, binaryExpression.Left)) - { - return; - } + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var option = context.GetAnalyzerOptions().PreferIsNullCheckOverReferenceEqualityMethod; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + { + return; + } - var properties = binaryExpression.Kind() == SyntaxKind.EqualsExpression - ? s_properties - : s_NegatedProperties; - context.ReportDiagnostic( - DiagnosticHelper.Create( - Descriptor, binaryExpression.GetLocation(), option.Notification, additionalLocations: null, properties)); + var binaryExpression = (BinaryExpressionSyntax)context.Node; + var semanticModel = context.SemanticModel; + + if (!IsObjectCastAndNullCheck(semanticModel, binaryExpression.Left, binaryExpression.Right) && + !IsObjectCastAndNullCheck(semanticModel, binaryExpression.Right, binaryExpression.Left)) + { + return; } - private static bool IsObjectCastAndNullCheck( - SemanticModel semanticModel, ExpressionSyntax left, ExpressionSyntax right) + var properties = binaryExpression.Kind() == SyntaxKind.EqualsExpression + ? s_properties + : s_NegatedProperties; + context.ReportDiagnostic( + DiagnosticHelper.Create( + Descriptor, binaryExpression.GetLocation(), option.Notification, context.Options, additionalLocations: null, properties)); + } + + private static bool IsObjectCastAndNullCheck( + SemanticModel semanticModel, ExpressionSyntax left, ExpressionSyntax right) + { + if (left is CastExpressionSyntax castExpression && + right.IsKind(SyntaxKind.NullLiteralExpression)) { - if (left is CastExpressionSyntax castExpression && - right.IsKind(SyntaxKind.NullLiteralExpression)) + // make sure it's a cast to object, and that the thing we're casting actually has a type. + if (semanticModel.GetTypeInfo(castExpression.Type).Type?.SpecialType == SpecialType.System_Object) { - // make sure it's a cast to object, and that the thing we're casting actually has a type. - if (semanticModel.GetTypeInfo(castExpression.Type).Type?.SpecialType == SpecialType.System_Object) + var expressionType = semanticModel.GetTypeInfo(castExpression.Expression).Type; + if (expressionType != null) { - var expressionType = semanticModel.GetTypeInfo(castExpression.Expression).Type; - if (expressionType != null) + if (expressionType is ITypeParameterSymbol typeParameter && + !typeParameter.HasReferenceTypeConstraint) { - if (expressionType is ITypeParameterSymbol typeParameter && - !typeParameter.HasReferenceTypeConstraint) - { - return false; - } - - return true; + return false; } + + return true; } } - - return false; } + + return false; } } diff --git a/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer.cs index 017666865f5d7..65d4b27d1fbb5 100644 --- a/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer.cs @@ -8,23 +8,22 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.UseIsNullCheck; -namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck +namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer : AbstractUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer : AbstractUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer + public CSharpUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer() + : base(CSharpAnalyzersResources.Use_is_null_check) { - public CSharpUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer() - : base(CSharpAnalyzersResources.Use_is_null_check) - { - } + } - protected override bool IsLanguageVersionSupported(Compilation compilation) - => compilation.LanguageVersion() >= LanguageVersion.CSharp7; + protected override bool IsLanguageVersionSupported(Compilation compilation) + => compilation.LanguageVersion() >= LanguageVersion.CSharp7; - protected override bool IsUnconstrainedGenericSupported(Compilation compilation) - => compilation.LanguageVersion() >= LanguageVersion.CSharp8; + protected override bool IsUnconstrainedGenericSupported(Compilation compilation) + => compilation.LanguageVersion() >= LanguageVersion.CSharp8; - protected override ISyntaxFacts GetSyntaxFacts() - => CSharpSyntaxFacts.Instance; - } + protected override ISyntaxFacts GetSyntaxFacts() + => CSharpSyntaxFacts.Instance; } diff --git a/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseNullCheckOverTypeCheckDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseNullCheckOverTypeCheckDiagnosticAnalyzer.cs index 3add5564a57df..ca11e6fbb1ee3 100644 --- a/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseNullCheckOverTypeCheckDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseIsNullCheck/CSharpUseNullCheckOverTypeCheckDiagnosticAnalyzer.cs @@ -11,105 +11,104 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck +namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpUseNullCheckOverTypeCheckDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpUseNullCheckOverTypeCheckDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public CSharpUseNullCheckOverTypeCheckDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseNullCheckOverTypeCheckDiagnosticId, + EnforceOnBuildValues.UseNullCheckOverTypeCheck, + CSharpCodeStyleOptions.PreferNullCheckOverTypeCheck, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Prefer_null_check_over_type_check), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Null_check_can_be_clarified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + { + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected override void InitializeWorker(AnalysisContext context) { - public CSharpUseNullCheckOverTypeCheckDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseNullCheckOverTypeCheckDiagnosticId, - EnforceOnBuildValues.UseNullCheckOverTypeCheck, - CSharpCodeStyleOptions.PreferNullCheckOverTypeCheck, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Prefer_null_check_over_type_check), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Null_check_can_be_clarified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + context.RegisterCompilationStartAction(context => { - } + var compilation = context.Compilation; + if (compilation.LanguageVersion() < LanguageVersion.CSharp9) + return; - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + var expressionType = compilation.ExpressionOfTType(); + context.RegisterOperationAction(c => AnalyzeIsTypeOperation(c, expressionType), OperationKind.IsType); + context.RegisterOperationAction(c => AnalyzeNegatedPatternOperation(c), OperationKind.NegatedPattern); + }); + } - protected override void InitializeWorker(AnalysisContext context) + private bool ShouldAnalyze(OperationAnalysisContext context, out NotificationOption2 notificationOption) + { + var option = context.GetCSharpAnalyzerOptions().PreferNullCheckOverTypeCheck; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) { - context.RegisterCompilationStartAction(context => - { - var compilation = context.Compilation; - if (compilation.LanguageVersion() < LanguageVersion.CSharp9) - return; - - var expressionType = compilation.ExpressionOfTType(); - context.RegisterOperationAction(c => AnalyzeIsTypeOperation(c, expressionType), OperationKind.IsType); - context.RegisterOperationAction(c => AnalyzeNegatedPatternOperation(c), OperationKind.NegatedPattern); - }); + notificationOption = NotificationOption2.Silent; + return false; } - private bool ShouldAnalyze(OperationAnalysisContext context, out NotificationOption2 notificationOption) + notificationOption = option.Notification; + return true; + } + + private void AnalyzeNegatedPatternOperation(OperationAnalysisContext context) + { + if (!ShouldAnalyze(context, out var notificationOption) || + context.Operation.Syntax is not UnaryPatternSyntax) { - var option = context.GetCSharpAnalyzerOptions().PreferNullCheckOverTypeCheck; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - { - notificationOption = NotificationOption2.Silent; - return false; - } - - notificationOption = option.Notification; - return true; + return; } - private void AnalyzeNegatedPatternOperation(OperationAnalysisContext context) + var negatedPattern = (INegatedPatternOperation)context.Operation; + // Matches 'x is not MyType' + // InputType is the type of 'x' + // MatchedType is 'MyType' + // We check InheritsFromOrEquals so that we report a diagnostic on the following: + // 1. x is not object (which is also equivalent to 'is null' check) + // 2. derivedObj is parentObj (which is the same as the previous point). + // 3. str is string (where str is a string, this is also equivalent to 'is null' check). + // This doesn't match `x is not MyType y` because in such case, negatedPattern.Pattern will + // be `DeclarationPattern`, not `TypePattern`. + if (negatedPattern.Pattern is ITypePatternOperation typePatternOperation && + typePatternOperation.InputType.InheritsFromOrEquals(typePatternOperation.MatchedType)) { - if (!ShouldAnalyze(context, out var notificationOption) || - context.Operation.Syntax is not UnaryPatternSyntax) - { - return; - } - - var negatedPattern = (INegatedPatternOperation)context.Operation; - // Matches 'x is not MyType' - // InputType is the type of 'x' - // MatchedType is 'MyType' - // We check InheritsFromOrEquals so that we report a diagnostic on the following: - // 1. x is not object (which is also equivalent to 'is null' check) - // 2. derivedObj is parentObj (which is the same as the previous point). - // 3. str is string (where str is a string, this is also equivalent to 'is null' check). - // This doesn't match `x is not MyType y` because in such case, negatedPattern.Pattern will - // be `DeclarationPattern`, not `TypePattern`. - if (negatedPattern.Pattern is ITypePatternOperation typePatternOperation && - typePatternOperation.InputType.InheritsFromOrEquals(typePatternOperation.MatchedType)) - { - context.ReportDiagnostic( - DiagnosticHelper.Create( - Descriptor, context.Operation.Syntax.GetLocation(), notificationOption, additionalLocations: null, properties: null)); - } + context.ReportDiagnostic( + DiagnosticHelper.Create( + Descriptor, context.Operation.Syntax.GetLocation(), notificationOption, context.Options, additionalLocations: null, properties: null)); } + } - private void AnalyzeIsTypeOperation(OperationAnalysisContext context, INamedTypeSymbol? expressionType) - { - var operation = context.Operation; - var semanticModel = operation.SemanticModel; - var syntax = operation.Syntax; + private void AnalyzeIsTypeOperation(OperationAnalysisContext context, INamedTypeSymbol? expressionType) + { + var operation = context.Operation; + var semanticModel = operation.SemanticModel; + var syntax = operation.Syntax; - Contract.ThrowIfNull(semanticModel); + Contract.ThrowIfNull(semanticModel); - if (!ShouldAnalyze(context, out var notificationOption) || syntax is not BinaryExpressionSyntax) - return; + if (!ShouldAnalyze(context, out var notificationOption) || syntax is not BinaryExpressionSyntax) + return; - if (CSharpSemanticFacts.Instance.IsInExpressionTree(semanticModel, syntax, expressionType, context.CancellationToken)) - return; + if (CSharpSemanticFacts.Instance.IsInExpressionTree(semanticModel, syntax, expressionType, context.CancellationToken)) + return; - var isTypeOperation = (IIsTypeOperation)operation; - - // Matches 'x is MyType' - // isTypeOperation.TypeOperand is 'MyType' - // isTypeOperation.ValueOperand.Type is the type of 'x'. - // We check InheritsFromOrEquals for the same reason as stated in AnalyzeNegatedPatternOperation. - // This doesn't match `x is MyType y` because in such case, we have an IsPattern instead of IsType operation. - if (isTypeOperation.ValueOperand.Type is not null && - isTypeOperation.ValueOperand.Type.InheritsFromOrEquals(isTypeOperation.TypeOperand)) - { - context.ReportDiagnostic( - DiagnosticHelper.Create( - Descriptor, syntax.GetLocation(), notificationOption, additionalLocations: null, properties: null)); - } + var isTypeOperation = (IIsTypeOperation)operation; + + // Matches 'x is MyType' + // isTypeOperation.TypeOperand is 'MyType' + // isTypeOperation.ValueOperand.Type is the type of 'x'. + // We check InheritsFromOrEquals for the same reason as stated in AnalyzeNegatedPatternOperation. + // This doesn't match `x is MyType y` because in such case, we have an IsPattern instead of IsType operation. + if (isTypeOperation.ValueOperand.Type is not null && + isTypeOperation.ValueOperand.Type.InheritsFromOrEquals(isTypeOperation.TypeOperand)) + { + context.ReportDiagnostic( + DiagnosticHelper.Create( + Descriptor, syntax.GetLocation(), notificationOption, context.Options, additionalLocations: null, properties: null)); } } } diff --git a/src/Analyzers/CSharp/Analyzers/UseLocalFunction/CSharpUseLocalFunctionDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseLocalFunction/CSharpUseLocalFunctionDiagnosticAnalyzer.cs index c26c008588748..7ca4c2037d1d1 100644 --- a/src/Analyzers/CSharp/Analyzers/UseLocalFunction/CSharpUseLocalFunctionDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseLocalFunction/CSharpUseLocalFunctionDiagnosticAnalyzer.cs @@ -15,410 +15,412 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UseLocalFunction +namespace Microsoft.CodeAnalysis.CSharp.UseLocalFunction; + +/// +/// Looks for code of the form: +/// +/// Func<int, int> fib = n => +/// { +/// if (n <= 2) +/// return 1 +/// +/// return fib(n - 1) + fib(n - 2); +/// } +/// +/// and converts it to: +/// +/// int fib(int n) +/// { +/// if (n <= 2) +/// return 1 +/// +/// return fib(n - 1) + fib(n - 2); +/// } +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseLocalFunctionDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - /// - /// Looks for code of the form: - /// - /// Func<int, int> fib = n => - /// { - /// if (n <= 2) - /// return 1 - /// - /// return fib(n - 1) + fib(n - 2); - /// } - /// - /// and converts it to: - /// - /// int fib(int n) - /// { - /// if (n <= 2) - /// return 1 - /// - /// return fib(n - 1) + fib(n - 2); - /// } - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseLocalFunctionDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public CSharpUseLocalFunctionDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseLocalFunctionDiagnosticId, + EnforceOnBuildValues.UseLocalFunction, + CSharpCodeStyleOptions.PreferLocalOverAnonymousFunction, + new LocalizableResourceString( + nameof(CSharpAnalyzersResources.Use_local_function), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpUseLocalFunctionDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseLocalFunctionDiagnosticId, - EnforceOnBuildValues.UseLocalFunction, - CSharpCodeStyleOptions.PreferLocalOverAnonymousFunction, - new LocalizableResourceString( - nameof(CSharpAnalyzersResources.Use_local_function), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } - - protected override void InitializeWorker(AnalysisContext context) - { - context.RegisterCompilationStartAction(compilationContext => - { - var compilation = compilationContext.Compilation; - - // Local functions are only available in C# 7.0 and above. Don't offer this refactoring - // in projects targeting a lesser version. - if (compilation.LanguageVersion() < LanguageVersion.CSharp7) - return; - - var expressionType = compilation.GetTypeByMetadataName(typeof(Expression<>).FullName!); - - // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to - // get callbacks for lambda expression nodes, but analyze nodes across the entire code block - // and eventually report a diagnostic on the local declaration node. - // Without the containing CodeBlockStartAction, our reported diagnostic would be classified - // as a non-local diagnostic and would not participate in lightbulb for computing code fixes. - context.RegisterCodeBlockStartAction(blockStartContext => - blockStartContext.RegisterSyntaxNodeAction(ctx => SyntaxNodeAction(ctx, expressionType), - SyntaxKind.SimpleLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.AnonymousMethodExpression)); - }); - } + } - private void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext, INamedTypeSymbol? expressionType) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(compilationContext => { - var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferLocalOverAnonymousFunction; - // Bail immediately if the user has disabled this feature. - if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) - return; + var compilation = compilationContext.Compilation; - var anonymousFunction = (AnonymousFunctionExpressionSyntax)syntaxContext.Node; - - var semanticModel = syntaxContext.SemanticModel; - if (!CheckForPattern(anonymousFunction, out var localDeclaration)) - return; - - if (localDeclaration.Declaration.Variables.Count != 1) - return; - - if (localDeclaration.Parent is not BlockSyntax block) + // Local functions are only available in C# 7.0 and above. Don't offer this refactoring + // in projects targeting a lesser version. + if (compilation.LanguageVersion() < LanguageVersion.CSharp7) return; - // If there are compiler error on the declaration we can't reliably - // tell that the refactoring will be accurate, so don't provide any - // code diagnostics - if (localDeclaration.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) - return; + var expressionType = compilation.GetTypeByMetadataName(typeof(Expression<>).FullName!); + + // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to + // get callbacks for lambda expression nodes, but analyze nodes across the entire code block + // and eventually report a diagnostic on the local declaration node. + // Without the containing CodeBlockStartAction, our reported diagnostic would be classified + // as a non-local diagnostic and would not participate in lightbulb for computing code fixes. + context.RegisterCodeBlockStartAction(blockStartContext => + blockStartContext.RegisterSyntaxNodeAction(ctx => SyntaxNodeAction(ctx, expressionType), + SyntaxKind.SimpleLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.AnonymousMethodExpression)); + }); + } - // Bail out if none of possible diagnostic locations are within the analysis span - var anonymousFunctionStatement = anonymousFunction.GetAncestor(); - var shouldReportOnAnonymousFunctionStatement = anonymousFunctionStatement != null - && localDeclaration != anonymousFunctionStatement; - if (!IsInAnalysisSpan(syntaxContext, localDeclaration, anonymousFunctionStatement, shouldReportOnAnonymousFunctionStatement)) - return; + private void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext, INamedTypeSymbol? expressionType) + { + var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferLocalOverAnonymousFunction; + // Bail immediately if the user has disabled this feature. + if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) + return; + + var anonymousFunction = (AnonymousFunctionExpressionSyntax)syntaxContext.Node; + + var semanticModel = syntaxContext.SemanticModel; + if (!CheckForPattern(anonymousFunction, out var localDeclaration)) + return; + + if (localDeclaration.Declaration.Variables.Count != 1) + return; + + if (localDeclaration.Parent is not BlockSyntax block) + return; + + // If there are compiler error on the declaration we can't reliably + // tell that the refactoring will be accurate, so don't provide any + // code diagnostics + if (localDeclaration.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + return; + + // Bail out if none of possible diagnostic locations are within the analysis span + var anonymousFunctionStatement = anonymousFunction.GetAncestor(); + var shouldReportOnAnonymousFunctionStatement = anonymousFunctionStatement != null + && localDeclaration != anonymousFunctionStatement; + if (!IsInAnalysisSpan(syntaxContext, localDeclaration, anonymousFunctionStatement, shouldReportOnAnonymousFunctionStatement)) + return; + + var cancellationToken = syntaxContext.CancellationToken; + var local = semanticModel.GetDeclaredSymbol(localDeclaration.Declaration.Variables[0], cancellationToken); + if (local == null) + return; + + var delegateType = semanticModel.GetTypeInfo(anonymousFunction, cancellationToken).ConvertedType as INamedTypeSymbol; + if (!delegateType.IsDelegateType() || + delegateType.DelegateInvokeMethod == null || + !CanReplaceDelegateWithLocalFunction(delegateType, localDeclaration, semanticModel, cancellationToken)) + { + return; + } - var cancellationToken = syntaxContext.CancellationToken; - var local = semanticModel.GetDeclaredSymbol(localDeclaration.Declaration.Variables[0], cancellationToken); - if (local == null) - return; + if (!CanReplaceAnonymousWithLocalFunction(semanticModel, expressionType, local, block, anonymousFunction, out var referenceLocations, cancellationToken)) + return; - var delegateType = semanticModel.GetTypeInfo(anonymousFunction, cancellationToken).ConvertedType as INamedTypeSymbol; - if (!delegateType.IsDelegateType() || - delegateType.DelegateInvokeMethod == null || - !CanReplaceDelegateWithLocalFunction(delegateType, localDeclaration, semanticModel, cancellationToken)) - { + if (localDeclaration.Declaration.Type.IsVar) + { + var options = semanticModel.SyntaxTree.Options; + if (options.LanguageVersion() < LanguageVersion.CSharp10) return; - } + } - if (!CanReplaceAnonymousWithLocalFunction(semanticModel, expressionType, local, block, anonymousFunction, out var referenceLocations, cancellationToken)) - return; + // Looks good! + var additionalLocations = ImmutableArray.Create( + localDeclaration.GetLocation(), + anonymousFunction.GetLocation()); - if (localDeclaration.Declaration.Type.IsVar) - { - var options = semanticModel.SyntaxTree.Options; - if (options.LanguageVersion() < LanguageVersion.CSharp10) - return; - } + additionalLocations = additionalLocations.AddRange(referenceLocations); - // Looks good! - var additionalLocations = ImmutableArray.Create( + if (styleOption.Notification.Severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) < ReportDiagnostic.Hidden) + { + // If the diagnostic is not hidden, then just place the user visible part + // on the local being initialized with the lambda. + syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + localDeclaration.Declaration.Variables[0].Identifier.GetLocation(), + styleOption.Notification, + syntaxContext.Options, + additionalLocations, + properties: null)); + } + else + { + // If the diagnostic is hidden, place it on the entire construct. + syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, localDeclaration.GetLocation(), - anonymousFunction.GetLocation()); - - additionalLocations = additionalLocations.AddRange(referenceLocations); + styleOption.Notification, + syntaxContext.Options, + additionalLocations, + properties: null)); - if (styleOption.Notification.Severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) < ReportDiagnostic.Hidden) + if (shouldReportOnAnonymousFunctionStatement) { - // If the diagnostic is not hidden, then just place the user visible part - // on the local being initialized with the lambda. syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( Descriptor, - localDeclaration.Declaration.Variables[0].Identifier.GetLocation(), + anonymousFunctionStatement!.GetLocation(), styleOption.Notification, + syntaxContext.Options, additionalLocations, properties: null)); } - else - { - // If the diagnostic is hidden, place it on the entire construct. - syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - localDeclaration.GetLocation(), - styleOption.Notification, - additionalLocations, - properties: null)); + } - if (shouldReportOnAnonymousFunctionStatement) - { - syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - anonymousFunctionStatement!.GetLocation(), - styleOption.Notification, - additionalLocations, - properties: null)); - } - } + static bool IsInAnalysisSpan( + SyntaxNodeAnalysisContext context, + LocalDeclarationStatementSyntax localDeclaration, + StatementSyntax? anonymousFunctionStatement, + bool shouldReportOnAnonymousFunctionStatement) + { + if (context.ShouldAnalyzeSpan(localDeclaration.Span)) + return true; - static bool IsInAnalysisSpan( - SyntaxNodeAnalysisContext context, - LocalDeclarationStatementSyntax localDeclaration, - StatementSyntax? anonymousFunctionStatement, - bool shouldReportOnAnonymousFunctionStatement) + if (shouldReportOnAnonymousFunctionStatement + && context.ShouldAnalyzeSpan(anonymousFunctionStatement!.Span)) { - if (context.ShouldAnalyzeSpan(localDeclaration.Span)) - return true; - - if (shouldReportOnAnonymousFunctionStatement - && context.ShouldAnalyzeSpan(anonymousFunctionStatement!.Span)) - { - return true; - } - - return false; + return true; } - } - private static bool CheckForPattern( - AnonymousFunctionExpressionSyntax anonymousFunction, - [NotNullWhen(true)] out LocalDeclarationStatementSyntax? localDeclaration) - { - // Look for: - // - // Type t = - // var t = (Type)() - // - // Type t = null; - // t = - return CheckForSimpleLocalDeclarationPattern(anonymousFunction, out localDeclaration) || - CheckForCastedLocalDeclarationPattern(anonymousFunction, out localDeclaration) || - CheckForLocalDeclarationAndAssignment(anonymousFunction, out localDeclaration); + return false; } + } - private static bool CheckForSimpleLocalDeclarationPattern( - AnonymousFunctionExpressionSyntax anonymousFunction, - [NotNullWhen(true)] out LocalDeclarationStatementSyntax? localDeclaration) - { - // Type t = - if (anonymousFunction is + private static bool CheckForPattern( + AnonymousFunctionExpressionSyntax anonymousFunction, + [NotNullWhen(true)] out LocalDeclarationStatementSyntax? localDeclaration) + { + // Look for: + // + // Type t = + // var t = (Type)() + // + // Type t = null; + // t = + return CheckForSimpleLocalDeclarationPattern(anonymousFunction, out localDeclaration) || + CheckForCastedLocalDeclarationPattern(anonymousFunction, out localDeclaration) || + CheckForLocalDeclarationAndAssignment(anonymousFunction, out localDeclaration); + } + + private static bool CheckForSimpleLocalDeclarationPattern( + AnonymousFunctionExpressionSyntax anonymousFunction, + [NotNullWhen(true)] out LocalDeclarationStatementSyntax? localDeclaration) + { + // Type t = + if (anonymousFunction is + { + Parent: EqualsValueClauseSyntax { - Parent: EqualsValueClauseSyntax + Parent: VariableDeclaratorSyntax { - Parent: VariableDeclaratorSyntax + Parent: VariableDeclarationSyntax { - Parent: VariableDeclarationSyntax - { - Parent: LocalDeclarationStatementSyntax declaration, - } + Parent: LocalDeclarationStatementSyntax declaration, } } - }) - { - localDeclaration = declaration; - return true; - } - - localDeclaration = null; - return false; + } + }) + { + localDeclaration = declaration; + return true; } - private static bool CanReplaceAnonymousWithLocalFunction( - SemanticModel semanticModel, INamedTypeSymbol? expressionTypeOpt, ISymbol local, BlockSyntax block, - AnonymousFunctionExpressionSyntax anonymousFunction, out ImmutableArray referenceLocations, CancellationToken cancellationToken) + localDeclaration = null; + return false; + } + + private static bool CanReplaceAnonymousWithLocalFunction( + SemanticModel semanticModel, INamedTypeSymbol? expressionTypeOpt, ISymbol local, BlockSyntax block, + AnonymousFunctionExpressionSyntax anonymousFunction, out ImmutableArray referenceLocations, CancellationToken cancellationToken) + { + // Check all the references to the anonymous function and disallow the conversion if + // they're used in certain ways. + using var _ = ArrayBuilder.GetInstance(out var references); + referenceLocations = []; + var anonymousFunctionStart = anonymousFunction.SpanStart; + foreach (var descendentNode in block.DescendantNodes()) { - // Check all the references to the anonymous function and disallow the conversion if - // they're used in certain ways. - using var _ = ArrayBuilder.GetInstance(out var references); - referenceLocations = []; - var anonymousFunctionStart = anonymousFunction.SpanStart; - foreach (var descendentNode in block.DescendantNodes()) + var descendentStart = descendentNode.Span.Start; + if (descendentStart <= anonymousFunctionStart) { - var descendentStart = descendentNode.Span.Start; - if (descendentStart <= anonymousFunctionStart) - { - // This node is before the local declaration. Can ignore it entirely as it could - // not be an access to the local. - continue; - } + // This node is before the local declaration. Can ignore it entirely as it could + // not be an access to the local. + continue; + } - if (descendentNode is IdentifierNameSyntax identifierName) + if (descendentNode is IdentifierNameSyntax identifierName) + { + if (identifierName.Identifier.ValueText == local.Name && + local.Equals(semanticModel.GetSymbolInfo(identifierName, cancellationToken).GetAnySymbol())) { - if (identifierName.Identifier.ValueText == local.Name && - local.Equals(semanticModel.GetSymbolInfo(identifierName, cancellationToken).GetAnySymbol())) + if (identifierName.IsWrittenTo(semanticModel, cancellationToken)) { - if (identifierName.IsWrittenTo(semanticModel, cancellationToken)) - { - // Can't change this to a local function if it is assigned to. - return false; - } + // Can't change this to a local function if it is assigned to. + return false; + } - var nodeToCheck = identifierName.WalkUpParentheses(); - if (nodeToCheck.Parent is BinaryExpressionSyntax) - { - // Can't change this if they're doing things like delegate addition with - // the lambda. - return false; - } + var nodeToCheck = identifierName.WalkUpParentheses(); + if (nodeToCheck.Parent is BinaryExpressionSyntax) + { + // Can't change this if they're doing things like delegate addition with + // the lambda. + return false; + } - if (nodeToCheck.Parent is InvocationExpressionSyntax invocationExpression) - { - references.Add(invocationExpression.GetLocation()); - } - else if (nodeToCheck.Parent is MemberAccessExpressionSyntax memberAccessExpression) + if (nodeToCheck.Parent is InvocationExpressionSyntax invocationExpression) + { + references.Add(invocationExpression.GetLocation()); + } + else if (nodeToCheck.Parent is MemberAccessExpressionSyntax memberAccessExpression) + { + if (memberAccessExpression.Parent is InvocationExpressionSyntax explicitInvocationExpression && + memberAccessExpression.Name.Identifier.ValueText == WellKnownMemberNames.DelegateInvokeName) { - if (memberAccessExpression.Parent is InvocationExpressionSyntax explicitInvocationExpression && - memberAccessExpression.Name.Identifier.ValueText == WellKnownMemberNames.DelegateInvokeName) - { - references.Add(explicitInvocationExpression.GetLocation()); - } - else - { - // They're doing something like "del.ToString()". Can't do this with a - // local function. - return false; - } + references.Add(explicitInvocationExpression.GetLocation()); } else { - references.Add(nodeToCheck.GetLocation()); - } - - var convertedType = semanticModel.GetTypeInfo(nodeToCheck, cancellationToken).ConvertedType; - if (!convertedType.IsDelegateType()) - { - // We can't change this anonymous function into a local function if it is - // converted to a non-delegate type (i.e. converted to 'object' or - // 'System.Delegate'). Local functions are not convertible to these types. - // They're only convertible to other delegate types. + // They're doing something like "del.ToString()". Can't do this with a + // local function. return false; } + } + else + { + references.Add(nodeToCheck.GetLocation()); + } - if (nodeToCheck.IsInExpressionTree(semanticModel, expressionTypeOpt, cancellationToken)) - { - // Can't reference a local function inside an expression tree. - return false; - } + var convertedType = semanticModel.GetTypeInfo(nodeToCheck, cancellationToken).ConvertedType; + if (!convertedType.IsDelegateType()) + { + // We can't change this anonymous function into a local function if it is + // converted to a non-delegate type (i.e. converted to 'object' or + // 'System.Delegate'). Local functions are not convertible to these types. + // They're only convertible to other delegate types. + return false; + } + + if (nodeToCheck.IsInExpressionTree(semanticModel, expressionTypeOpt, cancellationToken)) + { + // Can't reference a local function inside an expression tree. + return false; } } } - - referenceLocations = references.ToImmutableAndClear(); - return true; } - private static bool CheckForCastedLocalDeclarationPattern( - AnonymousFunctionExpressionSyntax anonymousFunction, - [NotNullWhen(true)] out LocalDeclarationStatementSyntax? localDeclaration) + referenceLocations = references.ToImmutableAndClear(); + return true; + } + + private static bool CheckForCastedLocalDeclarationPattern( + AnonymousFunctionExpressionSyntax anonymousFunction, + [NotNullWhen(true)] out LocalDeclarationStatementSyntax? localDeclaration) + { + // var t = (Type)() + var containingStatement = anonymousFunction.GetAncestor(); + if (containingStatement.IsKind(SyntaxKind.LocalDeclarationStatement, out localDeclaration) && + localDeclaration.Declaration.Variables.Count == 1) { - // var t = (Type)() - var containingStatement = anonymousFunction.GetAncestor(); - if (containingStatement.IsKind(SyntaxKind.LocalDeclarationStatement, out localDeclaration) && - localDeclaration.Declaration.Variables.Count == 1) + var variableDeclarator = localDeclaration.Declaration.Variables[0]; + if (variableDeclarator.Initializer != null) { - var variableDeclarator = localDeclaration.Declaration.Variables[0]; - if (variableDeclarator.Initializer != null) + var value = variableDeclarator.Initializer.Value.WalkDownParentheses(); + if (value is CastExpressionSyntax castExpression) { - var value = variableDeclarator.Initializer.Value.WalkDownParentheses(); - if (value is CastExpressionSyntax castExpression) + if (castExpression.Expression.WalkDownParentheses() == anonymousFunction) { - if (castExpression.Expression.WalkDownParentheses() == anonymousFunction) - { - return true; - } + return true; } } } - - localDeclaration = null; - return false; } - private static bool CheckForLocalDeclarationAndAssignment( - AnonymousFunctionExpressionSyntax anonymousFunction, - [NotNullWhen(true)] out LocalDeclarationStatementSyntax? localDeclaration) + localDeclaration = null; + return false; + } + + private static bool CheckForLocalDeclarationAndAssignment( + AnonymousFunctionExpressionSyntax anonymousFunction, + [NotNullWhen(true)] out LocalDeclarationStatementSyntax? localDeclaration) + { + // Type t = null; + // t = + if (anonymousFunction?.Parent is AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignment && + assignment.Parent is ExpressionStatementSyntax expressionStatement && + expressionStatement.Parent is BlockSyntax block) { - // Type t = null; - // t = - if (anonymousFunction?.Parent is AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignment && - assignment.Parent is ExpressionStatementSyntax expressionStatement && - expressionStatement.Parent is BlockSyntax block) + if (assignment.Left.IsKind(SyntaxKind.IdentifierName)) { - if (assignment.Left.IsKind(SyntaxKind.IdentifierName)) + var expressionStatementIndex = block.Statements.IndexOf(expressionStatement); + if (expressionStatementIndex >= 1) { - var expressionStatementIndex = block.Statements.IndexOf(expressionStatement); - if (expressionStatementIndex >= 1) + var previousStatement = block.Statements[expressionStatementIndex - 1]; + if (previousStatement.IsKind(SyntaxKind.LocalDeclarationStatement, out localDeclaration) && + localDeclaration.Declaration.Variables.Count == 1) { - var previousStatement = block.Statements[expressionStatementIndex - 1]; - if (previousStatement.IsKind(SyntaxKind.LocalDeclarationStatement, out localDeclaration) && - localDeclaration.Declaration.Variables.Count == 1) + var variableDeclarator = localDeclaration.Declaration.Variables[0]; + if (variableDeclarator.Initializer == null || + variableDeclarator.Initializer.Value.Kind() is + SyntaxKind.NullLiteralExpression or + SyntaxKind.DefaultLiteralExpression or + SyntaxKind.DefaultExpression) { - var variableDeclarator = localDeclaration.Declaration.Variables[0]; - if (variableDeclarator.Initializer == null || - variableDeclarator.Initializer.Value.Kind() is - SyntaxKind.NullLiteralExpression or - SyntaxKind.DefaultLiteralExpression or - SyntaxKind.DefaultExpression) + var identifierName = (IdentifierNameSyntax)assignment.Left; + if (variableDeclarator.Identifier.ValueText == identifierName.Identifier.ValueText) { - var identifierName = (IdentifierNameSyntax)assignment.Left; - if (variableDeclarator.Identifier.ValueText == identifierName.Identifier.ValueText) - { - return true; - } + return true; } } } } } + } - localDeclaration = null; - return false; + localDeclaration = null; + return false; + } + + private static bool CanReplaceDelegateWithLocalFunction( + INamedTypeSymbol delegateType, + LocalDeclarationStatementSyntax localDeclaration, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var delegateContainingType = delegateType.ContainingType; + if (delegateContainingType is null || !delegateContainingType.IsGenericType) + { + return true; } - private static bool CanReplaceDelegateWithLocalFunction( - INamedTypeSymbol delegateType, - LocalDeclarationStatementSyntax localDeclaration, - SemanticModel semanticModel, - CancellationToken cancellationToken) + var delegateTypeParamNames = delegateType.GetAllTypeParameters().Select(p => p.Name).ToImmutableHashSet(); + var localEnclosingSymbol = semanticModel.GetEnclosingSymbol(localDeclaration.SpanStart, cancellationToken); + while (localEnclosingSymbol != null) { - var delegateContainingType = delegateType.ContainingType; - if (delegateContainingType is null || !delegateContainingType.IsGenericType) + if (localEnclosingSymbol.Equals(delegateContainingType)) { return true; } - var delegateTypeParamNames = delegateType.GetAllTypeParameters().Select(p => p.Name).ToImmutableHashSet(); - var localEnclosingSymbol = semanticModel.GetEnclosingSymbol(localDeclaration.SpanStart, cancellationToken); - while (localEnclosingSymbol != null) + var typeParams = localEnclosingSymbol.GetTypeParameters(); + if (typeParams.Any()) { - if (localEnclosingSymbol.Equals(delegateContainingType)) + if (typeParams.Any(static (p, delegateTypeParamNames) => delegateTypeParamNames.Contains(p.Name), delegateTypeParamNames)) { - return true; + return false; } - - var typeParams = localEnclosingSymbol.GetTypeParameters(); - if (typeParams.Any()) - { - if (typeParams.Any(static (p, delegateTypeParamNames) => delegateTypeParamNames.Contains(p.Name), delegateTypeParamNames)) - { - return false; - } - } - - localEnclosingSymbol = localEnclosingSymbol.ContainingType; } - return true; + localEnclosingSymbol = localEnclosingSymbol.ContainingType; } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + return true; } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; } diff --git a/src/Analyzers/CSharp/Analyzers/UseNameofInNullableAttribute/CSharpUseNameofInNullableAttributeDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseNameofInNullableAttribute/CSharpUseNameofInNullableAttributeDiagnosticAnalyzer.cs index 5ed7c8fede1b4..14504a2b6eb16 100644 --- a/src/Analyzers/CSharp/Analyzers/UseNameofInNullableAttribute/CSharpUseNameofInNullableAttributeDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseNameofInNullableAttribute/CSharpUseNameofInNullableAttributeDiagnosticAnalyzer.cs @@ -107,6 +107,7 @@ and not "CallerArgumentExpression" this.Descriptor, argument.Expression.GetLocation(), NotificationOption2.Suggestion, + context.Options, additionalLocations: null, ImmutableDictionary.Empty.Add(NameKey, stringValue))); } diff --git a/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/AnalyzedPattern.cs b/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/AnalyzedPattern.cs index 989f5fb0da30e..63ad0ed28673d 100644 --- a/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/AnalyzedPattern.cs +++ b/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/AnalyzedPattern.cs @@ -7,189 +7,188 @@ using Microsoft.CodeAnalysis.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators +namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators; + +/// +/// Base class to represent a pattern constructed from various checks +/// +internal abstract class AnalyzedPattern { + public readonly IOperation Target; + + private AnalyzedPattern(IOperation target) + => Target = target; + /// - /// Base class to represent a pattern constructed from various checks + /// Represents a type-pattern, constructed from an is-expression /// - internal abstract class AnalyzedPattern + internal sealed class Type : AnalyzedPattern { - public readonly IOperation Target; - - private AnalyzedPattern(IOperation target) - => Target = target; + private static readonly SyntaxAnnotation s_annotation = new(); - /// - /// Represents a type-pattern, constructed from an is-expression - /// - internal sealed class Type : AnalyzedPattern + public static Type? TryCreate(BinaryExpressionSyntax binaryExpression, IIsTypeOperation operation) { - private static readonly SyntaxAnnotation s_annotation = new(); + Contract.ThrowIfNull(operation.SemanticModel); + if (binaryExpression.Right is not TypeSyntax typeSyntax) + { + return null; + } - public static Type? TryCreate(BinaryExpressionSyntax binaryExpression, IIsTypeOperation operation) + // We are coming from a type pattern, which likes to bind to types, but converting to + // patters which like to bind to expressions. For example, given: + // + // if (T is C.X || T is Y) { } + // + // we would want to convert to: + // + // if (T is C.X or Y) + // + // In the first case the compiler will bind to types named C or Y that are in scope + // but in the second it will also bind to a fields, methods etc. which for 'Y' changes + // semantics, and for 'C.X' could be a compile error. + // + // So lets create a pattern syntax and make sure the result is the same + var dummyStatement = SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName("_"), + SyntaxFactory.IsPatternExpression( + binaryExpression.Left, + SyntaxFactory.ConstantPattern(SyntaxFactory.ParenthesizedExpression(binaryExpression.Right.WithAdditionalAnnotations(s_annotation))) + ) + )); + + if (operation.SemanticModel.TryGetSpeculativeSemanticModel(typeSyntax.SpanStart, dummyStatement, out var speculativeModel)) { - Contract.ThrowIfNull(operation.SemanticModel); - if (binaryExpression.Right is not TypeSyntax typeSyntax) + var originalInfo = operation.SemanticModel.GetTypeInfo(binaryExpression.Right); + var newInfo = speculativeModel.GetTypeInfo(dummyStatement.GetAnnotatedNodes(s_annotation).Single()); + if (!originalInfo.Equals(newInfo)) { return null; } + } - // We are coming from a type pattern, which likes to bind to types, but converting to - // patters which like to bind to expressions. For example, given: - // - // if (T is C.X || T is Y) { } - // - // we would want to convert to: - // - // if (T is C.X or Y) - // - // In the first case the compiler will bind to types named C or Y that are in scope - // but in the second it will also bind to a fields, methods etc. which for 'Y' changes - // semantics, and for 'C.X' could be a compile error. - // - // So lets create a pattern syntax and make sure the result is the same - var dummyStatement = SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - SyntaxFactory.IdentifierName("_"), - SyntaxFactory.IsPatternExpression( - binaryExpression.Left, - SyntaxFactory.ConstantPattern(SyntaxFactory.ParenthesizedExpression(binaryExpression.Right.WithAdditionalAnnotations(s_annotation))) - ) - )); - - if (operation.SemanticModel.TryGetSpeculativeSemanticModel(typeSyntax.SpanStart, dummyStatement, out var speculativeModel)) - { - var originalInfo = operation.SemanticModel.GetTypeInfo(binaryExpression.Right); - var newInfo = speculativeModel.GetTypeInfo(dummyStatement.GetAnnotatedNodes(s_annotation).Single()); - if (!originalInfo.Equals(newInfo)) - { - return null; - } - } + return new Type(typeSyntax, operation.ValueOperand); + } - return new Type(typeSyntax, operation.ValueOperand); - } + public readonly TypeSyntax TypeSyntax; - public readonly TypeSyntax TypeSyntax; + private Type(TypeSyntax type, IOperation target) : base(target) + => TypeSyntax = type; + } - private Type(TypeSyntax type, IOperation target) : base(target) - => TypeSyntax = type; - } + /// + /// Represents a source-pattern, constructed from C# patterns + /// + internal sealed class Source(PatternSyntax patternSyntax, IOperation target) : AnalyzedPattern(target) + { + public readonly PatternSyntax PatternSyntax = patternSyntax; + } - /// - /// Represents a source-pattern, constructed from C# patterns - /// - internal sealed class Source(PatternSyntax patternSyntax, IOperation target) : AnalyzedPattern(target) - { - public readonly PatternSyntax PatternSyntax = patternSyntax; - } + /// + /// Represents a constant-pattern, constructed from an equality check + /// + internal sealed class Constant(ExpressionSyntax expression, IOperation target) : AnalyzedPattern(target) + { + public readonly ExpressionSyntax ExpressionSyntax = expression; + } - /// - /// Represents a constant-pattern, constructed from an equality check - /// - internal sealed class Constant(ExpressionSyntax expression, IOperation target) : AnalyzedPattern(target) - { - public readonly ExpressionSyntax ExpressionSyntax = expression; - } + /// + /// Represents a relational-pattern, constructed from relational operators + /// + internal sealed class Relational(BinaryOperatorKind operatorKind, ExpressionSyntax value, IOperation target) : AnalyzedPattern(target) + { + public readonly BinaryOperatorKind OperatorKind = operatorKind; + public readonly ExpressionSyntax Value = value; + } - /// - /// Represents a relational-pattern, constructed from relational operators - /// - internal sealed class Relational(BinaryOperatorKind operatorKind, ExpressionSyntax value, IOperation target) : AnalyzedPattern(target) + /// + /// Represents an and/or pattern, constructed from a logical and/or expression. + /// + internal sealed class Binary : AnalyzedPattern + { + public readonly AnalyzedPattern Left; + public readonly AnalyzedPattern Right; + public readonly bool IsDisjunctive; + public readonly SyntaxToken Token; + + private Binary(AnalyzedPattern leftPattern, AnalyzedPattern rightPattern, bool isDisjunctive, SyntaxToken token, IOperation target) : base(target) { - public readonly BinaryOperatorKind OperatorKind = operatorKind; - public readonly ExpressionSyntax Value = value; + Left = leftPattern; + Right = rightPattern; + IsDisjunctive = isDisjunctive; + Token = token; } - /// - /// Represents an and/or pattern, constructed from a logical and/or expression. - /// - internal sealed class Binary : AnalyzedPattern + public static AnalyzedPattern? TryCreate(AnalyzedPattern leftPattern, AnalyzedPattern rightPattern, bool isDisjunctive, SyntaxToken token) { - public readonly AnalyzedPattern Left; - public readonly AnalyzedPattern Right; - public readonly bool IsDisjunctive; - public readonly SyntaxToken Token; + var leftTarget = leftPattern.Target; + var rightTarget = rightPattern.Target; - private Binary(AnalyzedPattern leftPattern, AnalyzedPattern rightPattern, bool isDisjunctive, SyntaxToken token, IOperation target) : base(target) - { - Left = leftPattern; - Right = rightPattern; - IsDisjunctive = isDisjunctive; - Token = token; - } + var leftConv = (leftTarget as IConversionOperation)?.Conversion; + var rightConv = (rightTarget as IConversionOperation)?.Conversion; - public static AnalyzedPattern? TryCreate(AnalyzedPattern leftPattern, AnalyzedPattern rightPattern, bool isDisjunctive, SyntaxToken token) + var target = (leftConv, rightConv) switch { - var leftTarget = leftPattern.Target; - var rightTarget = rightPattern.Target; - - var leftConv = (leftTarget as IConversionOperation)?.Conversion; - var rightConv = (rightTarget as IConversionOperation)?.Conversion; - - var target = (leftConv, rightConv) switch - { - ({ IsUserDefined: true }, _) or - (_, { IsUserDefined: true }) => null, + ({ IsUserDefined: true }, _) or + (_, { IsUserDefined: true }) => null, - // If the original targets are implicitly converted due to usage of operators, - // both targets must have been converted to the same type, otherwise we bail. - ({ IsImplicit: true }, { IsImplicit: true }) when !Equals(leftTarget.Type, rightTarget.Type) => null, + // If the original targets are implicitly converted due to usage of operators, + // both targets must have been converted to the same type, otherwise we bail. + ({ IsImplicit: true }, { IsImplicit: true }) when !Equals(leftTarget.Type, rightTarget.Type) => null, - // If either of targets are implicitly converted but not both, - // we take the conversion node so that we can generate a cast off of it. - (null, { IsImplicit: true }) => rightTarget, - ({ IsImplicit: true }, null) => leftTarget, + // If either of targets are implicitly converted but not both, + // we take the conversion node so that we can generate a cast off of it. + (null, { IsImplicit: true }) => rightTarget, + ({ IsImplicit: true }, null) => leftTarget, - // If no implicit conversion is present, we just pick either side and continue. - _ => leftTarget, - }; + // If no implicit conversion is present, we just pick either side and continue. + _ => leftTarget, + }; - if (target is null) - return null; + if (target is null) + return null; - var compareTarget = target == leftTarget ? rightTarget : leftTarget; - if (!SyntaxFactory.AreEquivalent(target.Syntax, compareTarget.Syntax)) - return null; + var compareTarget = target == leftTarget ? rightTarget : leftTarget; + if (!SyntaxFactory.AreEquivalent(target.Syntax, compareTarget.Syntax)) + return null; - return new Binary(leftPattern, rightPattern, isDisjunctive, token, target); - } + return new Binary(leftPattern, rightPattern, isDisjunctive, token, target); } + } - /// - /// Represents a not-pattern, constructed from inequality check or a logical-not expression. - /// - internal sealed class Not : AnalyzedPattern - { - public readonly AnalyzedPattern Pattern; + /// + /// Represents a not-pattern, constructed from inequality check or a logical-not expression. + /// + internal sealed class Not : AnalyzedPattern + { + public readonly AnalyzedPattern Pattern; - private Not(AnalyzedPattern pattern, IOperation target) : base(target) - => Pattern = pattern; + private Not(AnalyzedPattern pattern, IOperation target) : base(target) + => Pattern = pattern; - private static BinaryOperatorKind Negate(BinaryOperatorKind kind) + private static BinaryOperatorKind Negate(BinaryOperatorKind kind) + { + return kind switch { - return kind switch - { - BinaryOperatorKind.LessThan => BinaryOperatorKind.GreaterThanOrEqual, - BinaryOperatorKind.GreaterThan => BinaryOperatorKind.LessThanOrEqual, - BinaryOperatorKind.LessThanOrEqual => BinaryOperatorKind.GreaterThan, - BinaryOperatorKind.GreaterThanOrEqual => BinaryOperatorKind.LessThan, - var v => throw ExceptionUtilities.UnexpectedValue(v) - }; - } + BinaryOperatorKind.LessThan => BinaryOperatorKind.GreaterThanOrEqual, + BinaryOperatorKind.GreaterThan => BinaryOperatorKind.LessThanOrEqual, + BinaryOperatorKind.LessThanOrEqual => BinaryOperatorKind.GreaterThan, + BinaryOperatorKind.GreaterThanOrEqual => BinaryOperatorKind.LessThan, + var v => throw ExceptionUtilities.UnexpectedValue(v) + }; + } - public static AnalyzedPattern? TryCreate(AnalyzedPattern? pattern) + public static AnalyzedPattern? TryCreate(AnalyzedPattern? pattern) + { + return pattern switch { - return pattern switch - { - null => null, - Not p => p.Pattern, // Avoid double negative - Relational p => new Relational(Negate(p.OperatorKind), p.Value, p.Target), - Binary { Left: Not left, Right: Not right } p // Apply demorgans's law - => Binary.TryCreate(left.Pattern, right.Pattern, !p.IsDisjunctive, p.Token), - _ => new Not(pattern, pattern.Target) - }; - } + null => null, + Not p => p.Pattern, // Avoid double negative + Relational p => new Relational(Negate(p.OperatorKind), p.Value, p.Target), + Binary { Left: Not left, Right: Not right } p // Apply demorgans's law + => Binary.TryCreate(left.Pattern, right.Pattern, !p.IsDisjunctive, p.Token), + _ => new Not(pattern, pattern.Target) + }; } } } diff --git a/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/CSharpUsePatternCombinatorsAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/CSharpUsePatternCombinatorsAnalyzer.cs index e8afa1654b21b..31f759e0dffe3 100644 --- a/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/CSharpUsePatternCombinatorsAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/CSharpUsePatternCombinatorsAnalyzer.cs @@ -6,162 +6,161 @@ using Microsoft.CodeAnalysis.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators +namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators; + +using static BinaryOperatorKind; +using static AnalyzedPattern; + +internal static class CSharpUsePatternCombinatorsAnalyzer { - using static BinaryOperatorKind; - using static AnalyzedPattern; + public static AnalyzedPattern? Analyze(IOperation operation) + { + var pattern = ParsePattern(operation); + return pattern?.Target.Syntax is ExpressionSyntax ? pattern : null; + } - internal static class CSharpUsePatternCombinatorsAnalyzer + private enum ConstantResult { - public static AnalyzedPattern? Analyze(IOperation operation) - { - var pattern = ParsePattern(operation); - return pattern?.Target.Syntax is ExpressionSyntax ? pattern : null; - } + /// + /// None of operands were constant. + /// + None, - private enum ConstantResult - { - /// - /// None of operands were constant. - /// - None, - - /// - /// The left operand is the constant. - /// - Left, - - /// - /// The right operand is the constant. - /// - Right, - } + /// + /// The left operand is the constant. + /// + Left, - private static AnalyzedPattern? ParsePattern(IOperation operation) - { - switch (operation) - { - case IBinaryOperation { OperatorKind: BinaryOperatorKind.Equals } op: - return ParseConstantPattern(op); + /// + /// The right operand is the constant. + /// + Right, + } - case IBinaryOperation { OperatorKind: NotEquals } op: - return Not.TryCreate(ParseConstantPattern(op)); + private static AnalyzedPattern? ParsePattern(IOperation operation) + { + switch (operation) + { + case IBinaryOperation { OperatorKind: BinaryOperatorKind.Equals } op: + return ParseConstantPattern(op); - case IBinaryOperation { OperatorKind: ConditionalOr, Syntax: BinaryExpressionSyntax syntax } op: - return ParseBinaryPattern(op, isDisjunctive: true, syntax.OperatorToken); + case IBinaryOperation { OperatorKind: NotEquals } op: + return Not.TryCreate(ParseConstantPattern(op)); - case IBinaryOperation { OperatorKind: ConditionalAnd, Syntax: BinaryExpressionSyntax syntax } op: - return ParseBinaryPattern(op, isDisjunctive: false, syntax.OperatorToken); + case IBinaryOperation { OperatorKind: ConditionalOr, Syntax: BinaryExpressionSyntax syntax } op: + return ParseBinaryPattern(op, isDisjunctive: true, syntax.OperatorToken); - case IBinaryOperation op when IsRelationalOperator(op.OperatorKind): - return ParseRelationalPattern(op); + case IBinaryOperation { OperatorKind: ConditionalAnd, Syntax: BinaryExpressionSyntax syntax } op: + return ParseBinaryPattern(op, isDisjunctive: false, syntax.OperatorToken); - case IUnaryOperation { OperatorKind: UnaryOperatorKind.Not } op: - return Not.TryCreate(ParsePattern(op.Operand)); + case IBinaryOperation op when IsRelationalOperator(op.OperatorKind): + return ParseRelationalPattern(op); - case IIsTypeOperation { Syntax: BinaryExpressionSyntax binaryExpression } op: - return Type.TryCreate(binaryExpression, op); + case IUnaryOperation { OperatorKind: UnaryOperatorKind.Not } op: + return Not.TryCreate(ParsePattern(op.Operand)); - case IIsPatternOperation { Pattern.Syntax: PatternSyntax pattern } op: - return new Source(pattern, op.Value); + case IIsTypeOperation { Syntax: BinaryExpressionSyntax binaryExpression } op: + return Type.TryCreate(binaryExpression, op); - case IParenthesizedOperation op: - return ParsePattern(op.Operand); - } + case IIsPatternOperation { Pattern.Syntax: PatternSyntax pattern } op: + return new Source(pattern, op.Value); - return null; + case IParenthesizedOperation op: + return ParsePattern(op.Operand); } - private static AnalyzedPattern? ParseBinaryPattern(IBinaryOperation op, bool isDisjunctive, SyntaxToken token) - { - var leftPattern = ParsePattern(op.LeftOperand); - if (leftPattern is null) - return null; + return null; + } + + private static AnalyzedPattern? ParseBinaryPattern(IBinaryOperation op, bool isDisjunctive, SyntaxToken token) + { + var leftPattern = ParsePattern(op.LeftOperand); + if (leftPattern is null) + return null; - var rightPattern = ParsePattern(op.RightOperand); - if (rightPattern is null) - return null; + var rightPattern = ParsePattern(op.RightOperand); + if (rightPattern is null) + return null; - return Binary.TryCreate(leftPattern, rightPattern, isDisjunctive, token); - } + return Binary.TryCreate(leftPattern, rightPattern, isDisjunctive, token); + } - private static ConstantResult DetermineConstant(IBinaryOperation op) + private static ConstantResult DetermineConstant(IBinaryOperation op) + { + return (op.LeftOperand, op.RightOperand) switch { - return (op.LeftOperand, op.RightOperand) switch - { - var (_, v) when IsConstant(v) => ConstantResult.Right, - var (v, _) when IsConstant(v) => ConstantResult.Left, - _ => ConstantResult.None, - }; - } + var (_, v) when IsConstant(v) => ConstantResult.Right, + var (v, _) when IsConstant(v) => ConstantResult.Left, + _ => ConstantResult.None, + }; + } - private static AnalyzedPattern? ParseRelationalPattern(IBinaryOperation op) + private static AnalyzedPattern? ParseRelationalPattern(IBinaryOperation op) + { + return DetermineConstant(op) switch { - return DetermineConstant(op) switch - { - ConstantResult.Left when op.LeftOperand.Syntax is ExpressionSyntax left - // We need to flip the operator if the constant is on the left-hand-side. - // This is because relational patterns only come in the prefix form. - // For instance: `123 > x` would be rewritten as `x is < 123`. - => new Relational(Flip(op.OperatorKind), left, op.RightOperand), - ConstantResult.Right when op.RightOperand.Syntax is ExpressionSyntax right - => new Relational(op.OperatorKind, right, op.LeftOperand), - _ => null - }; - } + ConstantResult.Left when op.LeftOperand.Syntax is ExpressionSyntax left + // We need to flip the operator if the constant is on the left-hand-side. + // This is because relational patterns only come in the prefix form. + // For instance: `123 > x` would be rewritten as `x is < 123`. + => new Relational(Flip(op.OperatorKind), left, op.RightOperand), + ConstantResult.Right when op.RightOperand.Syntax is ExpressionSyntax right + => new Relational(op.OperatorKind, right, op.LeftOperand), + _ => null + }; + } - private static AnalyzedPattern? ParseConstantPattern(IBinaryOperation op) + private static AnalyzedPattern? ParseConstantPattern(IBinaryOperation op) + { + return DetermineConstant(op) switch { - return DetermineConstant(op) switch - { - ConstantResult.Left when op.LeftOperand.Syntax is ExpressionSyntax left - => new Constant(left, op.RightOperand), - ConstantResult.Right when op.RightOperand.Syntax is ExpressionSyntax right - => new Constant(right, op.LeftOperand), - _ => null - }; - } + ConstantResult.Left when op.LeftOperand.Syntax is ExpressionSyntax left + => new Constant(left, op.RightOperand), + ConstantResult.Right when op.RightOperand.Syntax is ExpressionSyntax right + => new Constant(right, op.LeftOperand), + _ => null + }; + } - private static bool IsRelationalOperator(BinaryOperatorKind operatorKind) + private static bool IsRelationalOperator(BinaryOperatorKind operatorKind) + { + switch (operatorKind) { - switch (operatorKind) - { - case LessThan: - case LessThanOrEqual: - case GreaterThanOrEqual: - case GreaterThan: - return true; - default: - return false; - } + case LessThan: + case LessThanOrEqual: + case GreaterThanOrEqual: + case GreaterThan: + return true; + default: + return false; } + } - /// - /// Changes the direction the operator is pointing at. - /// - /// - /// Relational patterns only come in the prefix form so we'll have to - /// flip the operator if the constant happens to be on the left-hand-side. - /// For instance: `123 > x` would be rewritten as `x is < 123`. - /// - public static BinaryOperatorKind Flip(BinaryOperatorKind operatorKind) + /// + /// Changes the direction the operator is pointing at. + /// + /// + /// Relational patterns only come in the prefix form so we'll have to + /// flip the operator if the constant happens to be on the left-hand-side. + /// For instance: `123 > x` would be rewritten as `x is < 123`. + /// + public static BinaryOperatorKind Flip(BinaryOperatorKind operatorKind) + { + return operatorKind switch { - return operatorKind switch - { - LessThan => GreaterThan, - LessThanOrEqual => GreaterThanOrEqual, - GreaterThanOrEqual => LessThanOrEqual, - GreaterThan => LessThan, - var v => throw ExceptionUtilities.UnexpectedValue(v) - }; - } + LessThan => GreaterThan, + LessThanOrEqual => GreaterThanOrEqual, + GreaterThanOrEqual => LessThanOrEqual, + GreaterThan => LessThan, + var v => throw ExceptionUtilities.UnexpectedValue(v) + }; + } - private static bool IsConstant(IOperation operation) - { - // By-design, constants will not propagate to conversions. - return operation is IConversionOperation { Conversion.IsUserDefined: false } op - ? IsConstant(op.Operand) - : operation.ConstantValue.HasValue; - } + private static bool IsConstant(IOperation operation) + { + // By-design, constants will not propagate to conversions. + return operation is IConversionOperation { Conversion.IsUserDefined: false } op + ? IsConstant(op.Operand) + : operation.ConstantValue.HasValue; } } diff --git a/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/CSharpUsePatternCombinatorsDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/CSharpUsePatternCombinatorsDiagnosticAnalyzer.cs index a439ff0149a8f..6fd5116709f92 100644 --- a/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/CSharpUsePatternCombinatorsDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UsePatternCombinators/CSharpUsePatternCombinatorsDiagnosticAnalyzer.cs @@ -12,144 +12,144 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators +namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators; + +using static AnalyzedPattern; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpUsePatternCombinatorsDiagnosticAnalyzer : + AbstractBuiltInCodeStyleDiagnosticAnalyzer { - using static AnalyzedPattern; + private const string SafeKey = "safe"; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpUsePatternCombinatorsDiagnosticAnalyzer : - AbstractBuiltInCodeStyleDiagnosticAnalyzer - { - private const string SafeKey = "safe"; + private static readonly LocalizableResourceString s_safePatternTitle = new(nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); + private static readonly LocalizableResourceString s_unsafePatternTitle = new(nameof(CSharpAnalyzersResources.Use_pattern_matching_may_change_code_meaning), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); - private static readonly LocalizableResourceString s_safePatternTitle = new(nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); - private static readonly LocalizableResourceString s_unsafePatternTitle = new(nameof(CSharpAnalyzersResources.Use_pattern_matching_may_change_code_meaning), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)); + private static readonly ImmutableDictionary s_safeProperties = ImmutableDictionary.Empty.Add(SafeKey, ""); + private static readonly DiagnosticDescriptor s_unsafeDescriptor = CreateDescriptorWithId( + IDEDiagnosticIds.UsePatternCombinatorsDiagnosticId, + EnforceOnBuildValues.UsePatternCombinators, + hasAnyCodeStyleOption: true, + s_unsafePatternTitle); - private static readonly ImmutableDictionary s_safeProperties = ImmutableDictionary.Empty.Add(SafeKey, ""); - private static readonly DiagnosticDescriptor s_unsafeDescriptor = CreateDescriptorWithId( - IDEDiagnosticIds.UsePatternCombinatorsDiagnosticId, + public CSharpUsePatternCombinatorsDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UsePatternCombinatorsDiagnosticId, EnforceOnBuildValues.UsePatternCombinators, - hasAnyCodeStyleOption: true, - s_unsafePatternTitle); - - public CSharpUsePatternCombinatorsDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UsePatternCombinatorsDiagnosticId, - EnforceOnBuildValues.UsePatternCombinators, - CSharpCodeStyleOptions.PreferPatternMatching, - s_safePatternTitle, - s_safePatternTitle) - { - } + CSharpCodeStyleOptions.PreferPatternMatching, + s_safePatternTitle, + s_safePatternTitle) + { + } - public static bool IsSafe(Diagnostic diagnostic) - => diagnostic.Properties.ContainsKey(SafeKey); + public static bool IsSafe(Diagnostic diagnostic) + => diagnostic.Properties.ContainsKey(SafeKey); - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeNode, - SyntaxKind.LogicalAndExpression, - SyntaxKind.LogicalOrExpression, - SyntaxKind.LogicalNotExpression); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeNode, + SyntaxKind.LogicalAndExpression, + SyntaxKind.LogicalOrExpression, + SyntaxKind.LogicalNotExpression); - private void AnalyzeNode(SyntaxNodeAnalysisContext context) - { - var expression = (ExpressionSyntax)context.Node; - if (expression.GetDiagnostics().Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)) - return; - - // Bail if this is not a topmost expression - // to avoid overlapping diagnostics. - if (!IsTopmostExpression(expression)) - return; - - var syntaxTree = expression.SyntaxTree; - if (syntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp9) - return; - - var cancellationToken = context.CancellationToken; - var styleOption = context.GetCSharpAnalyzerOptions().PreferPatternMatching; - if (!styleOption.Value || ShouldSkipAnalysis(context, styleOption.Notification)) - return; - - var semanticModel = context.SemanticModel; - var expressionType = semanticModel.Compilation.ExpressionOfTType(); - if (expression.IsInExpressionTree(semanticModel, expressionType, cancellationToken)) - return; - - var operation = semanticModel.GetOperation(expression, cancellationToken); - if (operation is null) - return; - - var pattern = CSharpUsePatternCombinatorsAnalyzer.Analyze(operation); - if (pattern is null) - return; - - // Avoid rewriting trivial patterns, such as a single relational or a constant pattern. - if (IsTrivial(pattern)) - return; - - // C# 9.0 does not support pattern variables under `not` and `or` combinators, - // except for top-level `not` patterns. - if (HasIllegalPatternVariables(pattern, isTopLevel: true)) - return; - - // if the target (the common expression in the pattern) is a method call, - // then we can't guarantee that the rewritting won't have side-effects, - // so we should warn the user - var isSafe = pattern.Target.UnwrapImplicitConversion() is not Operations.IInvocationOperation; - - context.ReportDiagnostic(DiagnosticHelper.Create( - descriptor: isSafe ? this.Descriptor : s_unsafeDescriptor, - expression.GetLocation(), - styleOption.Notification, - additionalLocations: null, - properties: isSafe ? s_safeProperties : null)); - } + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var expression = (ExpressionSyntax)context.Node; + if (expression.GetDiagnostics().Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)) + return; + + // Bail if this is not a topmost expression + // to avoid overlapping diagnostics. + if (!IsTopmostExpression(expression)) + return; + + var syntaxTree = expression.SyntaxTree; + if (syntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp9) + return; + + var cancellationToken = context.CancellationToken; + var styleOption = context.GetCSharpAnalyzerOptions().PreferPatternMatching; + if (!styleOption.Value || ShouldSkipAnalysis(context, styleOption.Notification)) + return; + + var semanticModel = context.SemanticModel; + var expressionType = semanticModel.Compilation.ExpressionOfTType(); + if (expression.IsInExpressionTree(semanticModel, expressionType, cancellationToken)) + return; + + var operation = semanticModel.GetOperation(expression, cancellationToken); + if (operation is null) + return; + + var pattern = CSharpUsePatternCombinatorsAnalyzer.Analyze(operation); + if (pattern is null) + return; + + // Avoid rewriting trivial patterns, such as a single relational or a constant pattern. + if (IsTrivial(pattern)) + return; + + // C# 9.0 does not support pattern variables under `not` and `or` combinators, + // except for top-level `not` patterns. + if (HasIllegalPatternVariables(pattern, isTopLevel: true)) + return; + + // if the target (the common expression in the pattern) is a method call, + // then we can't guarantee that the rewritting won't have side-effects, + // so we should warn the user + var isSafe = pattern.Target.UnwrapImplicitConversion() is not Operations.IInvocationOperation; + + context.ReportDiagnostic(DiagnosticHelper.Create( + descriptor: isSafe ? this.Descriptor : s_unsafeDescriptor, + expression.GetLocation(), + styleOption.Notification, + context.Options, + additionalLocations: null, + properties: isSafe ? s_safeProperties : null)); + } - private static bool HasIllegalPatternVariables(AnalyzedPattern pattern, bool permitDesignations = true, bool isTopLevel = false) + private static bool HasIllegalPatternVariables(AnalyzedPattern pattern, bool permitDesignations = true, bool isTopLevel = false) + { + switch (pattern) { - switch (pattern) - { - case Not p: - return HasIllegalPatternVariables(p.Pattern, permitDesignations: isTopLevel); - case Binary p: - if (p.IsDisjunctive) - permitDesignations = false; - return HasIllegalPatternVariables(p.Left, permitDesignations) || - HasIllegalPatternVariables(p.Right, permitDesignations); - case Source p when !permitDesignations: - return p.PatternSyntax.DescendantNodes() - .OfType() - .Any(variable => !variable.Identifier.IsMissing); - default: - return false; - } + case Not p: + return HasIllegalPatternVariables(p.Pattern, permitDesignations: isTopLevel); + case Binary p: + if (p.IsDisjunctive) + permitDesignations = false; + return HasIllegalPatternVariables(p.Left, permitDesignations) || + HasIllegalPatternVariables(p.Right, permitDesignations); + case Source p when !permitDesignations: + return p.PatternSyntax.DescendantNodes() + .OfType() + .Any(variable => !variable.Identifier.IsMissing); + default: + return false; } + } - private static bool IsTopmostExpression(ExpressionSyntax node) + private static bool IsTopmostExpression(ExpressionSyntax node) + { + return node.WalkUpParentheses().Parent switch { - return node.WalkUpParentheses().Parent switch - { - LambdaExpressionSyntax _ => true, - AssignmentExpressionSyntax _ => true, - ConditionalExpressionSyntax _ => true, - ExpressionSyntax _ => false, - _ => true - }; - } + LambdaExpressionSyntax _ => true, + AssignmentExpressionSyntax _ => true, + ConditionalExpressionSyntax _ => true, + ExpressionSyntax _ => false, + _ => true + }; + } - private static bool IsTrivial(AnalyzedPattern pattern) + private static bool IsTrivial(AnalyzedPattern pattern) + { + return pattern switch { - return pattern switch - { - Not { Pattern: Constant _ } => true, - Not { Pattern: Source { PatternSyntax: ConstantPatternSyntax _ } } => true, - Not _ => false, - Binary _ => false, - _ => true - }; - } - - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + Not { Pattern: Constant _ } => true, + Not { Pattern: Source { PatternSyntax: ConstantPatternSyntax _ } } => true, + Not _ => false, + Binary _ => false, + _ => true + }; } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; } diff --git a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndMemberAccessDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndMemberAccessDiagnosticAnalyzer.cs index 3648ab3047ee3..3c75b1e322679 100644 --- a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndMemberAccessDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndMemberAccessDiagnosticAnalyzer.cs @@ -10,167 +10,166 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching +namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching; + +/// +/// Looks for code of the forms: +/// +/// (x as Y)?.Prop == constant +/// +/// and converts it to: +/// +/// x is Y { Prop: constant } +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal partial class CSharpAsAndMemberAccessDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { + public CSharpAsAndMemberAccessDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UsePatternMatchingAsAndMemberAccessDiagnosticId, + EnforceOnBuildValues.UsePatternMatchingAsAndMemberAccess, + CSharpCodeStyleOptions.PreferPatternMatchingOverAsWithNullCheck, + new LocalizableResourceString( + nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + { + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - /// - /// Looks for code of the forms: - /// - /// (x as Y)?.Prop == constant - /// - /// and converts it to: - /// - /// x is Y { Prop: constant } - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal partial class CSharpAsAndMemberAccessDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + protected override void InitializeWorker(AnalysisContext context) { - public CSharpAsAndMemberAccessDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UsePatternMatchingAsAndMemberAccessDiagnosticId, - EnforceOnBuildValues.UsePatternMatchingAsAndMemberAccess, - CSharpCodeStyleOptions.PreferPatternMatchingOverAsWithNullCheck, - new LocalizableResourceString( - nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + context.RegisterCompilationStartAction(context => { - } + // Recursive patterns (`is X { Prop: Y }`) is only available in C# 8.0 and above. Don't offer this + // refactoring in projects targeting a lesser version. + if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp8) + return; - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + context.RegisterSyntaxNodeAction(context => AnalyzeAsExpression(context), SyntaxKind.AsExpression); + }); + } - protected override void InitializeWorker(AnalysisContext context) + private void AnalyzeAsExpression(SyntaxNodeAnalysisContext context) + { + var styleOption = context.GetCSharpAnalyzerOptions().PreferPatternMatchingOverAsWithNullCheck; + if (!styleOption.Value || ShouldSkipAnalysis(context, styleOption.Notification)) { - context.RegisterCompilationStartAction(context => - { - // Recursive patterns (`is X { Prop: Y }`) is only available in C# 8.0 and above. Don't offer this - // refactoring in projects targeting a lesser version. - if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp8) - return; - - context.RegisterSyntaxNodeAction(context => AnalyzeAsExpression(context), SyntaxKind.AsExpression); - }); + // Bail immediately if the user has disabled this feature. + return; } - private void AnalyzeAsExpression(SyntaxNodeAnalysisContext context) + var cancellationToken = context.CancellationToken; + var semanticModel = context.SemanticModel; + var asExpression = (BinaryExpressionSyntax)context.Node; + + if (!UsePatternMatchingHelpers.TryGetPartsOfAsAndMemberAccessCheck( + asExpression, out var conditionalAccessExpression, out var binaryExpression, out var isPatternExpression, out var requiredLanguageVersion)) { - var styleOption = context.GetCSharpAnalyzerOptions().PreferPatternMatchingOverAsWithNullCheck; - if (!styleOption.Value || ShouldSkipAnalysis(context, styleOption.Notification)) - { - // Bail immediately if the user has disabled this feature. - return; - } + return; + } - var cancellationToken = context.CancellationToken; - var semanticModel = context.SemanticModel; - var asExpression = (BinaryExpressionSyntax)context.Node; + if (context.Compilation.LanguageVersion() < requiredLanguageVersion) + return; - if (!UsePatternMatchingHelpers.TryGetPartsOfAsAndMemberAccessCheck( - asExpression, out var conditionalAccessExpression, out var binaryExpression, out var isPatternExpression, out var requiredLanguageVersion)) - { - return; - } + if (!IsSafeToConvert()) + return; - if (context.Compilation.LanguageVersion() < requiredLanguageVersion) - return; + // Looks good! - if (!IsSafeToConvert()) - return; + // Put a diagnostic with the appropriate severity on the declaration-statement itself. + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + asExpression.GetLocation(), + styleOption.Notification, + context.Options, + additionalLocations: null, + properties: null)); - // Looks good! + bool IsSafeToConvert() + { + if (binaryExpression != null) + { + // `(expr as T)?... == other_expr + // + // in this case we can only convert if other_expr is a constant. + var constantValue = semanticModel.GetConstantValue(binaryExpression.Right, cancellationToken); + if (!constantValue.HasValue) + return false; + + if (binaryExpression.Kind() is SyntaxKind.EqualsExpression) + { + // `(a as T)?.Prop == null` does *not* have the same semantics as `a is T { Prop: null }` + // (specifically, when the type check fails) + if (constantValue.Value is null) + return false; - // Put a diagnostic with the appropriate severity on the declaration-statement itself. - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - asExpression.GetLocation(), - styleOption.Notification, - additionalLocations: null, - properties: null)); + // `(a as T)?.Prop == constant` does* have the same semantics as `a is T { Prop: constant }` + return true; + } + else if (binaryExpression.Kind() is SyntaxKind.NotEqualsExpression) + { + // `(a as T)?.Prop != constant` *does not* have the same semantics as `a is T { Prop: not constant }` + // (specifically, when the type check fails) + if (constantValue.Value is not null) + return false; + + // `(a as T)?.Prop != null` *does* have the same semantics as `a is T { Prop: not null }`. + // + // However, that's still only allowed if `Prop` is not a value type. + var symbol = semanticModel.GetSymbolInfo(conditionalAccessExpression.WhenNotNull, cancellationToken).GetAnySymbol(); + if (symbol.GetMemberType().IsNonNullableValueType()) + return false; - bool IsSafeToConvert() + return true; + } + + // don't need to check the other relational comparisons. These comparisons do a null check themselves, + // so it's safe to add a null-check with the 'is'. + return true; + } + else { - if (binaryExpression != null) + Contract.ThrowIfNull(isPatternExpression); + + // similar to the binary cases above. + + if (isPatternExpression.Pattern is ConstantPatternSyntax { Expression: var expression1 }) { - // `(expr as T)?... == other_expr - // - // in this case we can only convert if other_expr is a constant. - var constantValue = semanticModel.GetConstantValue(binaryExpression.Right, cancellationToken); + var constantValue = semanticModel.GetConstantValue(expression1, cancellationToken); if (!constantValue.HasValue) return false; - if (binaryExpression.Kind() is SyntaxKind.EqualsExpression) - { - // `(a as T)?.Prop == null` does *not* have the same semantics as `a is T { Prop: null }` - // (specifically, when the type check fails) - if (constantValue.Value is null) - return false; - - // `(a as T)?.Prop == constant` does* have the same semantics as `a is T { Prop: constant }` - return true; - } - else if (binaryExpression.Kind() is SyntaxKind.NotEqualsExpression) - { - // `(a as T)?.Prop != constant` *does not* have the same semantics as `a is T { Prop: not constant }` - // (specifically, when the type check fails) - if (constantValue.Value is not null) - return false; - - // `(a as T)?.Prop != null` *does* have the same semantics as `a is T { Prop: not null }`. - // - // However, that's still only allowed if `Prop` is not a value type. - var symbol = semanticModel.GetSymbolInfo(conditionalAccessExpression.WhenNotNull, cancellationToken).GetAnySymbol(); - if (symbol.GetMemberType().IsNonNullableValueType()) - return false; - - return true; - } - - // don't need to check the other relational comparisons. These comparisons do a null check themselves, - // so it's safe to add a null-check with the 'is'. + // `(a as T)?.Prop is null` does *not* have the same semantics as `a is T { Prop: null }` + // (specifically, when the type check fails) + if (constantValue.Value is null) + return false; + + // `(a as T)?.Prop is constant` does* have the same semantics as `a is T { Prop: constant }` return true; } - else + else if (isPatternExpression.Pattern is UnaryPatternSyntax { Pattern: ConstantPatternSyntax { Expression: var expression2 } }) { - Contract.ThrowIfNull(isPatternExpression); - - // similar to the binary cases above. - - if (isPatternExpression.Pattern is ConstantPatternSyntax { Expression: var expression1 }) - { - var constantValue = semanticModel.GetConstantValue(expression1, cancellationToken); - if (!constantValue.HasValue) - return false; - - // `(a as T)?.Prop is null` does *not* have the same semantics as `a is T { Prop: null }` - // (specifically, when the type check fails) - if (constantValue.Value is null) - return false; - - // `(a as T)?.Prop is constant` does* have the same semantics as `a is T { Prop: constant }` - return true; - } - else if (isPatternExpression.Pattern is UnaryPatternSyntax { Pattern: ConstantPatternSyntax { Expression: var expression2 } }) - { - var constantValue = semanticModel.GetConstantValue(expression2, cancellationToken); - if (!constantValue.HasValue) - return false; - - // `(a as T)?.Prop is not constant` *does not* have the same semantics as `a is T { Prop: not constant }` - // (specifically, when the type check fails) - if (constantValue.Value is not null) - return false; - - // `(a as T)?.Prop is not null` *does* have the same semantics as `a is T { Prop: not null }`. - // - // However, that's still only allowed if `Prop` is not a value type. - var symbol = semanticModel.GetSymbolInfo(conditionalAccessExpression.WhenNotNull, cancellationToken).GetAnySymbol(); - if (symbol.GetMemberType().IsNonNullableValueType()) - return false; - - return true; - } + var constantValue = semanticModel.GetConstantValue(expression2, cancellationToken); + if (!constantValue.HasValue) + return false; + + // `(a as T)?.Prop is not constant` *does not* have the same semantics as `a is T { Prop: not constant }` + // (specifically, when the type check fails) + if (constantValue.Value is not null) + return false; + + // `(a as T)?.Prop is not null` *does* have the same semantics as `a is T { Prop: not null }`. + // + // However, that's still only allowed if `Prop` is not a value type. + var symbol = semanticModel.GetSymbolInfo(conditionalAccessExpression.WhenNotNull, cancellationToken).GetAnySymbol(); + if (symbol.GetMemberType().IsNonNullableValueType()) + return false; return true; } + + return true; } } } diff --git a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndNullCheckDiagnosticAnalyzer.Analyzer.cs b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndNullCheckDiagnosticAnalyzer.Analyzer.cs index 2b64cc22acfb4..f1ca27d987501 100644 --- a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndNullCheckDiagnosticAnalyzer.Analyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndNullCheckDiagnosticAnalyzer.Analyzer.cs @@ -9,374 +9,373 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching +namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching; + +internal partial class CSharpAsAndNullCheckDiagnosticAnalyzer { - internal partial class CSharpAsAndNullCheckDiagnosticAnalyzer + private readonly struct Analyzer { - private readonly struct Analyzer + private readonly SemanticModel _semanticModel; + private readonly ILocalSymbol _localSymbol; + private readonly ExpressionSyntax _comparison; + private readonly ExpressionSyntax _operand; + private readonly SyntaxNode _localStatement; + private readonly SyntaxNode _enclosingBlock; + private readonly CancellationToken _cancellationToken; + + private Analyzer( + SemanticModel semanticModel, + ILocalSymbol localSymbol, + ExpressionSyntax comparison, + ExpressionSyntax operand, + SyntaxNode localStatement, + SyntaxNode enclosingBlock, + CancellationToken cancellationToken) { - private readonly SemanticModel _semanticModel; - private readonly ILocalSymbol _localSymbol; - private readonly ExpressionSyntax _comparison; - private readonly ExpressionSyntax _operand; - private readonly SyntaxNode _localStatement; - private readonly SyntaxNode _enclosingBlock; - private readonly CancellationToken _cancellationToken; - - private Analyzer( - SemanticModel semanticModel, - ILocalSymbol localSymbol, - ExpressionSyntax comparison, - ExpressionSyntax operand, - SyntaxNode localStatement, - SyntaxNode enclosingBlock, - CancellationToken cancellationToken) - { - Contract.ThrowIfNull(semanticModel); - Contract.ThrowIfNull(localSymbol); - Contract.ThrowIfNull(comparison); - Contract.ThrowIfNull(operand); - Debug.Assert(localStatement.IsKind(SyntaxKind.LocalDeclarationStatement)); - Debug.Assert(enclosingBlock.Kind() is SyntaxKind.Block or SyntaxKind.SwitchSection); - - _semanticModel = semanticModel; - _comparison = comparison; - _localSymbol = localSymbol; - _operand = operand; - _localStatement = localStatement; - _enclosingBlock = enclosingBlock; - _cancellationToken = cancellationToken; - } + Contract.ThrowIfNull(semanticModel); + Contract.ThrowIfNull(localSymbol); + Contract.ThrowIfNull(comparison); + Contract.ThrowIfNull(operand); + Debug.Assert(localStatement.IsKind(SyntaxKind.LocalDeclarationStatement)); + Debug.Assert(enclosingBlock.Kind() is SyntaxKind.Block or SyntaxKind.SwitchSection); + + _semanticModel = semanticModel; + _comparison = comparison; + _localSymbol = localSymbol; + _operand = operand; + _localStatement = localStatement; + _enclosingBlock = enclosingBlock; + _cancellationToken = cancellationToken; + } - public static bool CanSafelyConvertToPatternMatching( - SemanticModel semanticModel, - ILocalSymbol localSymbol, - ExpressionSyntax comparison, - ExpressionSyntax operand, - SyntaxNode localStatement, - SyntaxNode enclosingBlock, - CancellationToken cancellationToken) - { - var analyzer = new Analyzer(semanticModel, localSymbol, comparison, operand, localStatement, enclosingBlock, cancellationToken); - return analyzer.CanSafelyConvertToPatternMatching(); - } + public static bool CanSafelyConvertToPatternMatching( + SemanticModel semanticModel, + ILocalSymbol localSymbol, + ExpressionSyntax comparison, + ExpressionSyntax operand, + SyntaxNode localStatement, + SyntaxNode enclosingBlock, + CancellationToken cancellationToken) + { + var analyzer = new Analyzer(semanticModel, localSymbol, comparison, operand, localStatement, enclosingBlock, cancellationToken); + return analyzer.CanSafelyConvertToPatternMatching(); + } - // To convert a null-check to pattern-matching, we should make sure of a few things: - // - // (1) The pattern variable may not be used before the point of declaration. - // - // { - // var use = t; - // if (x is T t) {} - // } - // - // (2) The pattern variable may not be used outside of the new scope which - // is determined by the parent statement. - // - // { - // if (x is T t) {} - // } - // - // var use = t; - // - // (3) The pattern variable may not be used before assignment in opposite - // branches, if any. - // - // { - // if (x is T t) {} - // var use = t; - // } - // - // We walk up the tree from the point of null-check and see if any of the above is violated. - private bool CanSafelyConvertToPatternMatching() - { - // Keep track of whether the pattern variable is definitely assigned when false/true. - // We start by the null-check itself, if it's compared with '==', the pattern variable - // will be definitely assigned when false, because we wrap the is-operator in a !-operator. - var defAssignedWhenTrue = _comparison.Kind() is SyntaxKind.NotEqualsExpression or SyntaxKind.IsExpression; + // To convert a null-check to pattern-matching, we should make sure of a few things: + // + // (1) The pattern variable may not be used before the point of declaration. + // + // { + // var use = t; + // if (x is T t) {} + // } + // + // (2) The pattern variable may not be used outside of the new scope which + // is determined by the parent statement. + // + // { + // if (x is T t) {} + // } + // + // var use = t; + // + // (3) The pattern variable may not be used before assignment in opposite + // branches, if any. + // + // { + // if (x is T t) {} + // var use = t; + // } + // + // We walk up the tree from the point of null-check and see if any of the above is violated. + private bool CanSafelyConvertToPatternMatching() + { + // Keep track of whether the pattern variable is definitely assigned when false/true. + // We start by the null-check itself, if it's compared with '==', the pattern variable + // will be definitely assigned when false, because we wrap the is-operator in a !-operator. + var defAssignedWhenTrue = _comparison.Kind() is SyntaxKind.NotEqualsExpression or SyntaxKind.IsExpression; - foreach (var current in _comparison.Ancestors()) + foreach (var current in _comparison.Ancestors()) + { + // Checking for any conditional statement or expression that could possibly + // affect or determine the state of definite-assignment of the pattern variable. + switch (current.Kind()) { - // Checking for any conditional statement or expression that could possibly - // affect or determine the state of definite-assignment of the pattern variable. - switch (current.Kind()) - { - case SyntaxKind.LogicalAndExpression when !defAssignedWhenTrue: - case SyntaxKind.LogicalOrExpression when defAssignedWhenTrue: - // Since the pattern variable is only definitely assigned if the pattern - // succeeded, in the following cases it would not be safe to use pattern-matching. - // For example: - // - // if ((x = o as string) == null && SomeExpression) - // if ((x = o as string) != null || SomeExpression) - // - // Here, x would never be definitely assigned if pattern-matching were used. - return false; + case SyntaxKind.LogicalAndExpression when !defAssignedWhenTrue: + case SyntaxKind.LogicalOrExpression when defAssignedWhenTrue: + // Since the pattern variable is only definitely assigned if the pattern + // succeeded, in the following cases it would not be safe to use pattern-matching. + // For example: + // + // if ((x = o as string) == null && SomeExpression) + // if ((x = o as string) != null || SomeExpression) + // + // Here, x would never be definitely assigned if pattern-matching were used. + return false; - case SyntaxKind.LogicalAndExpression: - case SyntaxKind.LogicalOrExpression: - - // Parentheses and cast expressions do not contribute to the flow analysis. - case SyntaxKind.ParenthesizedExpression: - case SyntaxKind.CastExpression: - - // Skip over declaration parts to get to the parenting statement - // which might be a for-statement or a local declaration statement. - case SyntaxKind.EqualsValueClause: - case SyntaxKind.VariableDeclarator: - case SyntaxKind.VariableDeclaration: - continue; - - case SyntaxKind.LogicalNotExpression: - // The !-operator negates the definitive assignment state. - defAssignedWhenTrue = !defAssignedWhenTrue; - continue; - - case SyntaxKind.ConditionalExpression: - var conditionalExpression = (ConditionalExpressionSyntax)current; - if (LocalFlowsIn(defAssignedWhenTrue - ? conditionalExpression.WhenFalse - : conditionalExpression.WhenTrue)) - { - // In a conditional expression, the pattern variable - // would not be definitely assigned in the opposite branch. - return false; - } + case SyntaxKind.LogicalAndExpression: + case SyntaxKind.LogicalOrExpression: - return CheckExpression(conditionalExpression); + // Parentheses and cast expressions do not contribute to the flow analysis. + case SyntaxKind.ParenthesizedExpression: + case SyntaxKind.CastExpression: - case SyntaxKind.ForStatement: - var forStatement = (ForStatementSyntax)current; - if (forStatement.Condition is null || !forStatement.Condition.Span.Contains(_comparison.Span)) - { - // In a for-statement, only the condition expression - // can make this definitely assigned in the loop body. - return false; - } + // Skip over declaration parts to get to the parenting statement + // which might be a for-statement or a local declaration statement. + case SyntaxKind.EqualsValueClause: + case SyntaxKind.VariableDeclarator: + case SyntaxKind.VariableDeclaration: + continue; + + case SyntaxKind.LogicalNotExpression: + // The !-operator negates the definitive assignment state. + defAssignedWhenTrue = !defAssignedWhenTrue; + continue; - return CheckLoop(forStatement, forStatement.Statement, defAssignedWhenTrue); + case SyntaxKind.ConditionalExpression: + var conditionalExpression = (ConditionalExpressionSyntax)current; + if (LocalFlowsIn(defAssignedWhenTrue + ? conditionalExpression.WhenFalse + : conditionalExpression.WhenTrue)) + { + // In a conditional expression, the pattern variable + // would not be definitely assigned in the opposite branch. + return false; + } - case SyntaxKind.WhileStatement: - var whileStatement = (WhileStatementSyntax)current; - return CheckLoop(whileStatement, whileStatement.Statement, defAssignedWhenTrue); + return CheckExpression(conditionalExpression); - case SyntaxKind.IfStatement: - var ifStatement = (IfStatementSyntax)current; - var oppositeStatement = defAssignedWhenTrue - ? ifStatement.Else?.Statement - : ifStatement.Statement; + case SyntaxKind.ForStatement: + var forStatement = (ForStatementSyntax)current; + if (forStatement.Condition is null || !forStatement.Condition.Span.Contains(_comparison.Span)) + { + // In a for-statement, only the condition expression + // can make this definitely assigned in the loop body. + return false; + } - if (oppositeStatement != null) + return CheckLoop(forStatement, forStatement.Statement, defAssignedWhenTrue); + + case SyntaxKind.WhileStatement: + var whileStatement = (WhileStatementSyntax)current; + return CheckLoop(whileStatement, whileStatement.Statement, defAssignedWhenTrue); + + case SyntaxKind.IfStatement: + var ifStatement = (IfStatementSyntax)current; + var oppositeStatement = defAssignedWhenTrue + ? ifStatement.Else?.Statement + : ifStatement.Statement; + + if (oppositeStatement != null) + { + var dataFlow = _semanticModel.AnalyzeRequiredDataFlow(oppositeStatement); + if (dataFlow.DataFlowsIn.Contains(_localSymbol)) { - var dataFlow = _semanticModel.AnalyzeRequiredDataFlow(oppositeStatement); - if (dataFlow.DataFlowsIn.Contains(_localSymbol)) - { - // Access before assignment is not safe in the opposite branch - // as the variable is not definitely assigned at this point. - // For example: - // - // if (o is string x) { } - // else { Use(x); } - // - return false; - } - - if (dataFlow.AlwaysAssigned.Contains(_localSymbol)) - { - // If the variable is always assigned here, we don't need to check - // subsequent statements as it's definitely assigned afterwards. - // For example: - // - // if (o is string x) { } - // else { x = null; } - // - return true; - } + // Access before assignment is not safe in the opposite branch + // as the variable is not definitely assigned at this point. + // For example: + // + // if (o is string x) { } + // else { Use(x); } + // + return false; } - if (!defAssignedWhenTrue && - !_semanticModel.AnalyzeRequiredControlFlow(ifStatement.Statement).EndPointIsReachable) + if (dataFlow.AlwaysAssigned.Contains(_localSymbol)) { - // Access before assignment here is only valid if we have a negative - // pattern-matching in an if-statement with an unreachable endpoint. + // If the variable is always assigned here, we don't need to check + // subsequent statements as it's definitely assigned afterwards. // For example: // - // if (!(o is string x)) { - // return; - // } - // - // // The 'return' statement above ensures x is definitely assigned here - // Console.WriteLine(x); + // if (o is string x) { } + // else { x = null; } // return true; } + } - return CheckStatement(ifStatement); - } - - switch (current) - { - case ExpressionSyntax expression: - // If we reached here, it means we have a sub-expression that - // does not guarantee definite assignment. We should make sure that - // the pattern variable is not used outside of the expression boundaries. - return CheckExpression(expression); - - case StatementSyntax statement: - // If we reached here, it means that the null-check is appeared in - // a statement. In that case, the variable would be actually in the - // scope in subsequent statements, but not definitely assigned. - // Therefore, we should ensure that there is no use before assignment. - return CheckStatement(statement); - } - - // Bail out for error cases and unhandled cases. - break; - } - - return false; - } + if (!defAssignedWhenTrue && + !_semanticModel.AnalyzeRequiredControlFlow(ifStatement.Statement).EndPointIsReachable) + { + // Access before assignment here is only valid if we have a negative + // pattern-matching in an if-statement with an unreachable endpoint. + // For example: + // + // if (!(o is string x)) { + // return; + // } + // + // // The 'return' statement above ensures x is definitely assigned here + // Console.WriteLine(x); + // + return true; + } - private bool CheckLoop(SyntaxNode statement, StatementSyntax body, bool defAssignedWhenTrue) - { - if (_operand.Kind() == SyntaxKind.IdentifierName) - { - // We have something like: - // - // var x = e as T; - // while (b != null) { ... } - // - // It's not necessarily safe to convert this to: - // - // while (x is T b) { ... } - // - // That's because in this case, unlike the original code, we're - // type-checking in every iteration, so we do not replace a - // simple null check with the "is" operator if it's in a loop. - return false; + return CheckStatement(ifStatement); } - if (!defAssignedWhenTrue && LocalFlowsIn(body)) + switch (current) { - // If the local is accessed before assignment - // in the loop body, we should make sure that - // the variable is definitely assigned by then. - return false; + case ExpressionSyntax expression: + // If we reached here, it means we have a sub-expression that + // does not guarantee definite assignment. We should make sure that + // the pattern variable is not used outside of the expression boundaries. + return CheckExpression(expression); + + case StatementSyntax statement: + // If we reached here, it means that the null-check is appeared in + // a statement. In that case, the variable would be actually in the + // scope in subsequent statements, but not definitely assigned. + // Therefore, we should ensure that there is no use before assignment. + return CheckStatement(statement); } - // The scope of the pattern variables for loops - // does not leak out of the loop statement. - return !IsAccessedOutOfScope(scope: statement); + // Bail out for error cases and unhandled cases. + break; } - private bool CheckExpression(ExpressionSyntax exprsesion) + return false; + } + + private bool CheckLoop(SyntaxNode statement, StatementSyntax body, bool defAssignedWhenTrue) + { + if (_operand.Kind() == SyntaxKind.IdentifierName) { - // It wouldn't be safe to read after the pattern variable is - // declared inside a sub-expression, because it would not be - // definitely assigned after this point. It's possible to allow - // use after assignment but it's rather unlikely to happen. - return !IsAccessedOutOfScope(scope: exprsesion); + // We have something like: + // + // var x = e as T; + // while (b != null) { ... } + // + // It's not necessarily safe to convert this to: + // + // while (x is T b) { ... } + // + // That's because in this case, unlike the original code, we're + // type-checking in every iteration, so we do not replace a + // simple null check with the "is" operator if it's in a loop. + return false; } - private bool CheckStatement(StatementSyntax statement) + if (!defAssignedWhenTrue && LocalFlowsIn(body)) { - Contract.ThrowIfNull(statement); + // If the local is accessed before assignment + // in the loop body, we should make sure that + // the variable is definitely assigned by then. + return false; + } - // This is either an embedded statement or parented by a block. - // If we're parented by a block, then that block will be the scope - // of the new variable. Otherwise the scope is the statement itself. - if (statement.Parent is BlockSyntax block) - { - // Check if the local is accessed before assignment - // in the subsequent statements. If so, this can't - // be converted to pattern-matching. - if (LocalFlowsIn(firstStatement: statement.GetNextStatement(), - lastStatement: block.Statements.Last())) - { - return false; - } + // The scope of the pattern variables for loops + // does not leak out of the loop statement. + return !IsAccessedOutOfScope(scope: statement); + } - return !IsAccessedOutOfScope(scope: block); - } - else + private bool CheckExpression(ExpressionSyntax exprsesion) + { + // It wouldn't be safe to read after the pattern variable is + // declared inside a sub-expression, because it would not be + // definitely assigned after this point. It's possible to allow + // use after assignment but it's rather unlikely to happen. + return !IsAccessedOutOfScope(scope: exprsesion); + } + + private bool CheckStatement(StatementSyntax statement) + { + Contract.ThrowIfNull(statement); + + // This is either an embedded statement or parented by a block. + // If we're parented by a block, then that block will be the scope + // of the new variable. Otherwise the scope is the statement itself. + if (statement.Parent is BlockSyntax block) + { + // Check if the local is accessed before assignment + // in the subsequent statements. If so, this can't + // be converted to pattern-matching. + if (LocalFlowsIn(firstStatement: statement.GetNextStatement(), + lastStatement: block.Statements.Last())) { - return !IsAccessedOutOfScope(scope: statement); + return false; } - } - private bool IsAccessedOutOfScope(SyntaxNode scope) + return !IsAccessedOutOfScope(scope: block); + } + else { - Contract.ThrowIfNull(scope); + return !IsAccessedOutOfScope(scope: statement); + } + } - var localStatementStart = _localStatement.SpanStart; - var comparisonSpanStart = _comparison.SpanStart; - var variableName = _localSymbol.Name; - var scopeSpan = scope.Span; + private bool IsAccessedOutOfScope(SyntaxNode scope) + { + Contract.ThrowIfNull(scope); - // Iterate over all descendant nodes to find possible out-of-scope references. - foreach (var descendentNode in _enclosingBlock.DescendantNodes()) - { - var descendentNodeSpanStart = descendentNode.SpanStart; - if (descendentNodeSpanStart <= localStatementStart) - { - // We're not interested in nodes that are apeared before - // the local declaration statement. It's either an error - // or not the local reference we're looking for. - continue; - } + var localStatementStart = _localStatement.SpanStart; + var comparisonSpanStart = _comparison.SpanStart; + var variableName = _localSymbol.Name; + var scopeSpan = scope.Span; - if (descendentNodeSpanStart >= comparisonSpanStart && scopeSpan.Contains(descendentNode.Span)) - { - // If this is in the scope and after null-check, we don't bother checking the symbol. - continue; - } - - if (descendentNode is IdentifierNameSyntax identifierName && - identifierName.Identifier.ValueText == variableName && - _localSymbol.Equals(_semanticModel.GetSymbolInfo(identifierName, _cancellationToken).Symbol)) - { - // If we got here, it means we have a local - // reference out of scope of the pattern variable. - return true; - } + // Iterate over all descendant nodes to find possible out-of-scope references. + foreach (var descendentNode in _enclosingBlock.DescendantNodes()) + { + var descendentNodeSpanStart = descendentNode.SpanStart; + if (descendentNodeSpanStart <= localStatementStart) + { + // We're not interested in nodes that are apeared before + // the local declaration statement. It's either an error + // or not the local reference we're looking for. + continue; } - // Either no reference were found, or all - // references were inside the given scope. - return false; - } - - private bool LocalFlowsIn(SyntaxNode statementOrExpression) - { - if (statementOrExpression == null) + if (descendentNodeSpanStart >= comparisonSpanStart && scopeSpan.Contains(descendentNode.Span)) { - return false; + // If this is in the scope and after null-check, we don't bother checking the symbol. + continue; } - if (statementOrExpression.ContainsDiagnostics) + if (descendentNode is IdentifierNameSyntax identifierName && + identifierName.Identifier.ValueText == variableName && + _localSymbol.Equals(_semanticModel.GetSymbolInfo(identifierName, _cancellationToken).Symbol)) { - return false; + // If we got here, it means we have a local + // reference out of scope of the pattern variable. + return true; } + } - return _semanticModel.AnalyzeDataFlow(statementOrExpression).DataFlowsIn.Contains(_localSymbol); + // Either no reference were found, or all + // references were inside the given scope. + return false; + } + + private bool LocalFlowsIn(SyntaxNode statementOrExpression) + { + if (statementOrExpression == null) + { + return false; } - private bool LocalFlowsIn(StatementSyntax? firstStatement, StatementSyntax? lastStatement) + if (statementOrExpression.ContainsDiagnostics) { - if (firstStatement == null || lastStatement == null) - { - return false; - } + return false; + } - if (firstStatement.ContainsDiagnostics || lastStatement.ContainsDiagnostics) - { - return false; - } + return _semanticModel.AnalyzeDataFlow(statementOrExpression).DataFlowsIn.Contains(_localSymbol); + } - var dataFlow = _semanticModel.AnalyzeDataFlow(firstStatement, lastStatement); - Contract.ThrowIfNull(dataFlow); - return dataFlow.DataFlowsIn.Contains(_localSymbol); + private bool LocalFlowsIn(StatementSyntax? firstStatement, StatementSyntax? lastStatement) + { + if (firstStatement == null || lastStatement == null) + { + return false; + } + + if (firstStatement.ContainsDiagnostics || lastStatement.ContainsDiagnostics) + { + return false; } + + var dataFlow = _semanticModel.AnalyzeDataFlow(firstStatement, lastStatement); + Contract.ThrowIfNull(dataFlow); + return dataFlow.DataFlowsIn.Contains(_localSymbol); } } } diff --git a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndNullCheckDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndNullCheckDiagnosticAnalyzer.cs index d5f25e0ccc586..366b558711928 100644 --- a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndNullCheckDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpAsAndNullCheckDiagnosticAnalyzer.cs @@ -13,359 +13,358 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching +namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching; + +/// +/// Looks for code of the forms: +/// +/// var x = o as Type; +/// if (x != null) ... +/// +/// and converts it to: +/// +/// if (o is Type x) ... +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal partial class CSharpAsAndNullCheckDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { + public CSharpAsAndNullCheckDiagnosticAnalyzer() + : base(IDEDiagnosticIds.InlineAsTypeCheckId, + EnforceOnBuildValues.InlineAsType, + CSharpCodeStyleOptions.PreferPatternMatchingOverAsWithNullCheck, + new LocalizableResourceString( + nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + { + } - /// - /// Looks for code of the forms: - /// - /// var x = o as Type; - /// if (x != null) ... - /// - /// and converts it to: - /// - /// if (o is Type x) ... - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal partial class CSharpAsAndNullCheckDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected override void InitializeWorker(AnalysisContext context) + // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to + // get callbacks for expression nodes, but analyze nodes across the entire code block + // and eventually report a diagnostic on the local declaration statement node. + // Without the containing CodeBlockStartAction, our reported diagnostic would be classified + // as a non-local diagnostic and would not participate in lightbulb for computing code fixes. + => context.RegisterCodeBlockStartAction(blockStartContext => + blockStartContext.RegisterSyntaxNodeAction(SyntaxNodeAction, + SyntaxKind.EqualsExpression, + SyntaxKind.NotEqualsExpression, + SyntaxKind.IsExpression, + SyntaxKind.IsPatternExpression)); + + private void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext) { - public CSharpAsAndNullCheckDiagnosticAnalyzer() - : base(IDEDiagnosticIds.InlineAsTypeCheckId, - EnforceOnBuildValues.InlineAsType, - CSharpCodeStyleOptions.PreferPatternMatchingOverAsWithNullCheck, - new LocalizableResourceString( - nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + var node = syntaxContext.Node; + var syntaxTree = node.SyntaxTree; + + // "x is Type y" is only available in C# 7.0 and above. Don't offer this refactoring + // in projects targeting a lesser version. + if (syntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp7) + return; + + var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferPatternMatchingOverAsWithNullCheck; + if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) { + // Bail immediately if the user has disabled this feature. + return; } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - - protected override void InitializeWorker(AnalysisContext context) - // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to - // get callbacks for expression nodes, but analyze nodes across the entire code block - // and eventually report a diagnostic on the local declaration statement node. - // Without the containing CodeBlockStartAction, our reported diagnostic would be classified - // as a non-local diagnostic and would not participate in lightbulb for computing code fixes. - => context.RegisterCodeBlockStartAction(blockStartContext => - blockStartContext.RegisterSyntaxNodeAction(SyntaxNodeAction, - SyntaxKind.EqualsExpression, - SyntaxKind.NotEqualsExpression, - SyntaxKind.IsExpression, - SyntaxKind.IsPatternExpression)); - - private void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext) + var comparison = (ExpressionSyntax)node; + var (comparisonLeft, comparisonRight) = comparison switch { - var node = syntaxContext.Node; - var syntaxTree = node.SyntaxTree; + BinaryExpressionSyntax binaryExpression => (binaryExpression.Left, (SyntaxNode)binaryExpression.Right), + IsPatternExpressionSyntax isPattern => (isPattern.Expression, isPattern.Pattern), + _ => throw ExceptionUtilities.Unreachable(), + }; - // "x is Type y" is only available in C# 7.0 and above. Don't offer this refactoring - // in projects targeting a lesser version. - if (syntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp7) - return; + var operand = GetNullCheckOperand(comparisonLeft, comparison.Kind(), comparisonRight)?.WalkDownParentheses(); + if (operand == null) + return; - var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferPatternMatchingOverAsWithNullCheck; - if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) - { - // Bail immediately if the user has disabled this feature. - return; - } + var semanticModel = syntaxContext.SemanticModel; + if (operand is CastExpressionSyntax castExpression) + { + // Unwrap object cast + var castType = semanticModel.GetTypeInfo(castExpression.Type).Type; + if (castType?.SpecialType == SpecialType.System_Object) + operand = castExpression.Expression; + } - var comparison = (ExpressionSyntax)node; - var (comparisonLeft, comparisonRight) = comparison switch - { - BinaryExpressionSyntax binaryExpression => (binaryExpression.Left, (SyntaxNode)binaryExpression.Right), - IsPatternExpressionSyntax isPattern => (isPattern.Expression, isPattern.Pattern), - _ => throw ExceptionUtilities.Unreachable(), - }; + var cancellationToken = syntaxContext.CancellationToken; + if (semanticModel.GetSymbolInfo(comparison, cancellationToken).GetAnySymbol().IsUserDefinedOperator()) + return; - var operand = GetNullCheckOperand(comparisonLeft, comparison.Kind(), comparisonRight)?.WalkDownParentheses(); - if (operand == null) - return; + if (!TryGetTypeCheckParts(semanticModel, operand, + out var declarator, + out var asExpression, + out var localSymbol)) + { + return; + } - var semanticModel = syntaxContext.SemanticModel; - if (operand is CastExpressionSyntax castExpression) - { - // Unwrap object cast - var castType = semanticModel.GetTypeInfo(castExpression.Type).Type; - if (castType?.SpecialType == SpecialType.System_Object) - operand = castExpression.Expression; - } + var localStatement = declarator.Parent?.Parent; + var enclosingBlock = localStatement?.Parent; + if (localStatement == null || + enclosingBlock == null) + { + return; + } - var cancellationToken = syntaxContext.CancellationToken; - if (semanticModel.GetSymbolInfo(comparison, cancellationToken).GetAnySymbol().IsUserDefinedOperator()) - return; + // Bail out if the potential diagnostic location is outside the analysis span. + if (!syntaxContext.ShouldAnalyzeSpan(localStatement.Span)) + return; - if (!TryGetTypeCheckParts(semanticModel, operand, - out var declarator, - out var asExpression, - out var localSymbol)) - { - return; - } + // Don't convert if the as is part of a using statement + // eg using (var x = y as MyObject) { } + if (localStatement is UsingStatementSyntax) + return; - var localStatement = declarator.Parent?.Parent; - var enclosingBlock = localStatement?.Parent; - if (localStatement == null || - enclosingBlock == null) - { - return; - } + // Don't convert if the as is part of a local declaration with a using keyword + // eg using var x = y as MyObject; + if (localStatement is LocalDeclarationStatementSyntax localDecl && localDecl.UsingKeyword != default) + return; + + var typeNode = asExpression.Right; + var asType = semanticModel.GetTypeInfo(typeNode, cancellationToken).Type; + if (asType.IsNullable()) + { + // Not legal to write "x is int? y" + return; + } + + if (asType?.TypeKind == TypeKind.Dynamic) + { + // Not legal to use dynamic in a pattern. + return; + } - // Bail out if the potential diagnostic location is outside the analysis span. - if (!syntaxContext.ShouldAnalyzeSpan(localStatement.Span)) - return; + if (!localSymbol.Type.Equals(asType)) + { + // We have something like: + // + // BaseType b = x as DerivedType; + // if (b != null) { ... } + // + // It's not necessarily safe to convert this to: + // + // if (x is DerivedType b) { ... } + // + // That's because there may be later code that wants to do something like assign a + // 'BaseType' into 'b'. As we've now claimed that it must be DerivedType, that + // won't work. This might also cause unintended changes like changing overload + // resolution. So, we conservatively do not offer the change in a situation like this. + return; + } - // Don't convert if the as is part of a using statement - // eg using (var x = y as MyObject) { } - if (localStatement is UsingStatementSyntax) - return; + // Check if the as operand is ever written up to the point of null check. + // + // var s = field as string; + // field = null; + // if (s != null) { ... } + // + // It's no longer safe to use pattern-matching because 'field is string s' would never be true. + // + // Additionally, also bail out if the assigned local is referenced (i.e. read/write/nameof) up to the point of null check. + // var s = field as string; + // MethodCall(flag: s == null); + // if (s != null) { ... } + // + var asOperand = semanticModel.GetSymbolInfo(asExpression.Left, cancellationToken).Symbol; + var localStatementStart = localStatement.SpanStart; + var comparisonSpanStart = comparison.SpanStart; + + foreach (var descendentNode in enclosingBlock.DescendantNodes()) + { + var descendentNodeSpanStart = descendentNode.SpanStart; + if (descendentNodeSpanStart <= localStatementStart) + continue; - // Don't convert if the as is part of a local declaration with a using keyword - // eg using var x = y as MyObject; - if (localStatement is LocalDeclarationStatementSyntax localDecl && localDecl.UsingKeyword != default) - return; + if (descendentNodeSpanStart >= comparisonSpanStart) + break; - var typeNode = asExpression.Right; - var asType = semanticModel.GetTypeInfo(typeNode, cancellationToken).Type; - if (asType.IsNullable()) + if (descendentNode is IdentifierNameSyntax identifierName) { - // Not legal to write "x is int? y" - return; - } + // Check if this is a 'write' to the asOperand. + if (identifierName.Identifier.ValueText == asOperand?.Name && + asOperand.Equals(semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol) && + identifierName.IsWrittenTo(semanticModel, cancellationToken)) + { + return; + } - if (asType?.TypeKind == TypeKind.Dynamic) - { - // Not legal to use dynamic in a pattern. - return; + // Check is a reference of any sort (i.e. read/write/nameof) to the local. + if (identifierName.Identifier.ValueText == localSymbol.Name) + return; } + } - if (!localSymbol.Type.Equals(asType)) - { - // We have something like: - // - // BaseType b = x as DerivedType; - // if (b != null) { ... } - // - // It's not necessarily safe to convert this to: - // - // if (x is DerivedType b) { ... } - // - // That's because there may be later code that wants to do something like assign a - // 'BaseType' into 'b'. As we've now claimed that it must be DerivedType, that - // won't work. This might also cause unintended changes like changing overload - // resolution. So, we conservatively do not offer the change in a situation like this. - return; - } + if (!Analyzer.CanSafelyConvertToPatternMatching( + semanticModel, localSymbol, comparison, operand, + localStatement, enclosingBlock, cancellationToken)) + { + return; + } - // Check if the as operand is ever written up to the point of null check. - // - // var s = field as string; - // field = null; - // if (s != null) { ... } - // - // It's no longer safe to use pattern-matching because 'field is string s' would never be true. - // - // Additionally, also bail out if the assigned local is referenced (i.e. read/write/nameof) up to the point of null check. - // var s = field as string; - // MethodCall(flag: s == null); - // if (s != null) { ... } - // - var asOperand = semanticModel.GetSymbolInfo(asExpression.Left, cancellationToken).Symbol; - var localStatementStart = localStatement.SpanStart; - var comparisonSpanStart = comparison.SpanStart; + var comparisonEnclosingBlock = comparison.AncestorsAndSelf().FirstOrDefault(n => n is BlockSyntax); + if (comparisonEnclosingBlock is null) + return; + if (comparisonEnclosingBlock != enclosingBlock) + { + // ok, the local variable is defined in a different block than the block that the `x != null` is in. If + // we then update the `x != null` to `o is X x` we may break scoping if the variable is referenced + // before/after that scope. foreach (var descendentNode in enclosingBlock.DescendantNodes()) { var descendentNodeSpanStart = descendentNode.SpanStart; if (descendentNodeSpanStart <= localStatementStart) continue; - if (descendentNodeSpanStart >= comparisonSpanStart) - break; - - if (descendentNode is IdentifierNameSyntax identifierName) + if (descendentNode is IdentifierNameSyntax identifierName && ! + descendentNode.Span.IntersectsWith(comparisonEnclosingBlock.Span) && + identifierName.Identifier.ValueText == localSymbol.Name && + localSymbol.Equals(semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol)) { - // Check if this is a 'write' to the asOperand. - if (identifierName.Identifier.ValueText == asOperand?.Name && - asOperand.Equals(semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol) && - identifierName.IsWrittenTo(semanticModel, cancellationToken)) - { - return; - } - - // Check is a reference of any sort (i.e. read/write/nameof) to the local. - if (identifierName.Identifier.ValueText == localSymbol.Name) - return; + return; } } + } - if (!Analyzer.CanSafelyConvertToPatternMatching( - semanticModel, localSymbol, comparison, operand, - localStatement, enclosingBlock, cancellationToken)) - { - return; - } - - var comparisonEnclosingBlock = comparison.AncestorsAndSelf().FirstOrDefault(n => n is BlockSyntax); - if (comparisonEnclosingBlock is null) - return; - - if (comparisonEnclosingBlock != enclosingBlock) + // If we have an annotated local (like `string? s = o as string`) we can't convert this to `o is string s` + // if there are any assignments to `s` that end up assigning a `string?`. These will now give a nullable + // warning. + if (localSymbol.Type.NullableAnnotation == NullableAnnotation.Annotated) + { + foreach (var descendentNode in enclosingBlock.DescendantNodes()) { - // ok, the local variable is defined in a different block than the block that the `x != null` is in. If - // we then update the `x != null` to `o is X x` we may break scoping if the variable is referenced - // before/after that scope. - foreach (var descendentNode in enclosingBlock.DescendantNodes()) - { - var descendentNodeSpanStart = descendentNode.SpanStart; - if (descendentNodeSpanStart <= localStatementStart) - continue; - - if (descendentNode is IdentifierNameSyntax identifierName && ! - descendentNode.Span.IntersectsWith(comparisonEnclosingBlock.Span) && - identifierName.Identifier.ValueText == localSymbol.Name && - localSymbol.Equals(semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol)) - { - return; - } - } - } + var descendentNodeSpanStart = descendentNode.SpanStart; + if (descendentNodeSpanStart <= localStatementStart) + continue; - // If we have an annotated local (like `string? s = o as string`) we can't convert this to `o is string s` - // if there are any assignments to `s` that end up assigning a `string?`. These will now give a nullable - // warning. - if (localSymbol.Type.NullableAnnotation == NullableAnnotation.Annotated) - { - foreach (var descendentNode in enclosingBlock.DescendantNodes()) + if (descendentNode is IdentifierNameSyntax identifierName && + identifierName.Identifier.ValueText == localSymbol.Name && + localSymbol.Equals(semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol)) { - var descendentNodeSpanStart = descendentNode.SpanStart; - if (descendentNodeSpanStart <= localStatementStart) - continue; - - if (descendentNode is IdentifierNameSyntax identifierName && - identifierName.Identifier.ValueText == localSymbol.Name && - localSymbol.Equals(semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol)) + if (identifierName.Parent is AssignmentExpressionSyntax assignmentExpression && + assignmentExpression.Left == identifierName) { - if (identifierName.Parent is AssignmentExpressionSyntax assignmentExpression && - assignmentExpression.Left == identifierName) - { - var rightType = semanticModel.GetTypeInfo(assignmentExpression.Right); - if (rightType.Type is null or { NullableAnnotation: NullableAnnotation.Annotated }) - return; - } + var rightType = semanticModel.GetTypeInfo(assignmentExpression.Right); + if (rightType.Type is null or { NullableAnnotation: NullableAnnotation.Annotated }) + return; } } } - - // Looks good! - var additionalLocations = ImmutableArray.Create( - declarator.GetLocation(), - comparison.GetLocation(), - asExpression.GetLocation()); - - // Put a diagnostic with the appropriate severity on the declaration-statement itself. - syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - localStatement.GetLocation(), - styleOption.Notification, - additionalLocations, - properties: null)); } - private static bool TryGetTypeCheckParts( - SemanticModel semanticModel, - SyntaxNode operand, - [NotNullWhen(true)] out VariableDeclaratorSyntax? declarator, - [NotNullWhen(true)] out BinaryExpressionSyntax? asExpression, - [NotNullWhen(true)] out ILocalSymbol? localSymbol) + // Looks good! + var additionalLocations = ImmutableArray.Create( + declarator.GetLocation(), + comparison.GetLocation(), + asExpression.GetLocation()); + + // Put a diagnostic with the appropriate severity on the declaration-statement itself. + syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + localStatement.GetLocation(), + styleOption.Notification, + syntaxContext.Options, + additionalLocations, + properties: null)); + } + + private static bool TryGetTypeCheckParts( + SemanticModel semanticModel, + SyntaxNode operand, + [NotNullWhen(true)] out VariableDeclaratorSyntax? declarator, + [NotNullWhen(true)] out BinaryExpressionSyntax? asExpression, + [NotNullWhen(true)] out ILocalSymbol? localSymbol) + { + switch (operand.Kind()) { - switch (operand.Kind()) - { - case SyntaxKind.IdentifierName: - { - // var x = e as T; - // if (x != null) F(x); - var identifier = (IdentifierNameSyntax)operand; - if (!TryFindVariableDeclarator(semanticModel, identifier, out localSymbol, out declarator)) - break; + case SyntaxKind.IdentifierName: + { + // var x = e as T; + // if (x != null) F(x); + var identifier = (IdentifierNameSyntax)operand; + if (!TryFindVariableDeclarator(semanticModel, identifier, out localSymbol, out declarator)) + break; - var initializerValue = declarator.Initializer?.Value; - if (!initializerValue.IsKind(SyntaxKind.AsExpression, out asExpression)) - break; + var initializerValue = declarator.Initializer?.Value; + if (!initializerValue.IsKind(SyntaxKind.AsExpression, out asExpression)) + break; - return true; - } + return true; + } - case SyntaxKind.SimpleAssignmentExpression: + case SyntaxKind.SimpleAssignmentExpression: + { + // T x; + // if ((x = e as T) != null) F(x); + var assignment = (AssignmentExpressionSyntax)operand; + if (!assignment.Right.IsKind(SyntaxKind.AsExpression, out asExpression) || + assignment.Left is not IdentifierNameSyntax identifier) { - // T x; - // if ((x = e as T) != null) F(x); - var assignment = (AssignmentExpressionSyntax)operand; - if (!assignment.Right.IsKind(SyntaxKind.AsExpression, out asExpression) || - assignment.Left is not IdentifierNameSyntax identifier) - { - break; - } - - if (!TryFindVariableDeclarator(semanticModel, identifier, out localSymbol, out declarator)) - break; - - return true; + break; } - } - declarator = null; - asExpression = null; - localSymbol = null; - return false; + if (!TryFindVariableDeclarator(semanticModel, identifier, out localSymbol, out declarator)) + break; + + return true; + } } - private static bool TryFindVariableDeclarator( - SemanticModel semanticModel, - IdentifierNameSyntax identifier, - [NotNullWhen(true)] out ILocalSymbol? localSymbol, - [NotNullWhen(true)] out VariableDeclaratorSyntax? declarator) + declarator = null; + asExpression = null; + localSymbol = null; + return false; + } + + private static bool TryFindVariableDeclarator( + SemanticModel semanticModel, + IdentifierNameSyntax identifier, + [NotNullWhen(true)] out ILocalSymbol? localSymbol, + [NotNullWhen(true)] out VariableDeclaratorSyntax? declarator) + { + localSymbol = semanticModel.GetSymbolInfo(identifier).Symbol as ILocalSymbol; + declarator = localSymbol?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as VariableDeclaratorSyntax; + return localSymbol != null && declarator != null; + } + + private static ExpressionSyntax? GetNullCheckOperand(ExpressionSyntax left, SyntaxKind comparisonKind, SyntaxNode right) + { + if (left.IsKind(SyntaxKind.NullLiteralExpression)) { - localSymbol = semanticModel.GetSymbolInfo(identifier).Symbol as ILocalSymbol; - declarator = localSymbol?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as VariableDeclaratorSyntax; - return localSymbol != null && declarator != null; + // null == x + // null != x + return (ExpressionSyntax)right; } - private static ExpressionSyntax? GetNullCheckOperand(ExpressionSyntax left, SyntaxKind comparisonKind, SyntaxNode right) + if (right.IsKind(SyntaxKind.NullLiteralExpression)) { - if (left.IsKind(SyntaxKind.NullLiteralExpression)) - { - // null == x - // null != x - return (ExpressionSyntax)right; - } - - if (right.IsKind(SyntaxKind.NullLiteralExpression)) - { - // x == null - // x != null - return left; - } - - if (right is PredefinedTypeSyntax predefinedType - && predefinedType.Keyword.IsKind(SyntaxKind.ObjectKeyword) - && comparisonKind == SyntaxKind.IsExpression) - { - // x is object - return left; - } + // x == null + // x != null + return left; + } - if (right is ConstantPatternSyntax constantPattern - && constantPattern.Expression.IsKind(SyntaxKind.NullLiteralExpression) - && comparisonKind == SyntaxKind.IsPatternExpression) - { - // x is null - return left; - } + if (right is PredefinedTypeSyntax predefinedType + && predefinedType.Keyword.IsKind(SyntaxKind.ObjectKeyword) + && comparisonKind == SyntaxKind.IsExpression) + { + // x is object + return left; + } - return null; + if (right is ConstantPatternSyntax constantPattern + && constantPattern.Expression.IsKind(SyntaxKind.NullLiteralExpression) + && comparisonKind == SyntaxKind.IsPatternExpression) + { + // x is null + return left; } + + return null; } } diff --git a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpIsAndCastCheckDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpIsAndCastCheckDiagnosticAnalyzer.cs index 0cf22ae638448..f14bdc9df2d5f 100644 --- a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpIsAndCastCheckDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpIsAndCastCheckDiagnosticAnalyzer.cs @@ -13,236 +13,236 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching +namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching; + +/// +/// Looks for code of the form: +/// +/// if (expr is Type) +/// { +/// var v = (Type)expr; +/// } +/// +/// and converts it to: +/// +/// if (expr is Type v) +/// { +/// } +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpIsAndCastCheckDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - /// - /// Looks for code of the form: - /// - /// if (expr is Type) - /// { - /// var v = (Type)expr; - /// } - /// - /// and converts it to: - /// - /// if (expr is Type v) - /// { - /// } - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpIsAndCastCheckDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public static readonly CSharpIsAndCastCheckDiagnosticAnalyzer Instance = new(); + + public CSharpIsAndCastCheckDiagnosticAnalyzer() + : base(IDEDiagnosticIds.InlineIsTypeCheckId, + EnforceOnBuildValues.InlineIsType, + CSharpCodeStyleOptions.PreferPatternMatchingOverIsWithCastCheck, + new LocalizableResourceString( + nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public static readonly CSharpIsAndCastCheckDiagnosticAnalyzer Instance = new(); - - public CSharpIsAndCastCheckDiagnosticAnalyzer() - : base(IDEDiagnosticIds.InlineIsTypeCheckId, - EnforceOnBuildValues.InlineIsType, - CSharpCodeStyleOptions.PreferPatternMatchingOverIsWithCastCheck, - new LocalizableResourceString( - nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } - - protected override void InitializeWorker(AnalysisContext context) - { - context.RegisterCompilationStartAction(context => - { - // "x is Type y" is only available in C# 7.0 and above. Don't offer this refactoring - // in projects targeting a lesser version. - if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp7) - return; - - // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to - // get callbacks for 'is' expression nodes, but analyze nodes across the entire code block - // and eventually report a diagnostic on the local declaration statement node. - // Without the containing CodeBlockStartAction, our reported diagnostic would be classified - // as a non-local diagnostic and would not participate in lightbulb for computing code fixes. - context.RegisterCodeBlockStartAction(blockStartContext => - blockStartContext.RegisterSyntaxNodeAction(SyntaxNodeAction, SyntaxKind.IsExpression)); - }); - } + } - private void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferPatternMatchingOverIsWithCastCheck; - if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) - { - // Bail immediately if the user has disabled this feature. + // "x is Type y" is only available in C# 7.0 and above. Don't offer this refactoring + // in projects targeting a lesser version. + if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp7) return; - } - var isExpression = (BinaryExpressionSyntax)syntaxContext.Node; + // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to + // get callbacks for 'is' expression nodes, but analyze nodes across the entire code block + // and eventually report a diagnostic on the local declaration statement node. + // Without the containing CodeBlockStartAction, our reported diagnostic would be classified + // as a non-local diagnostic and would not participate in lightbulb for computing code fixes. + context.RegisterCodeBlockStartAction(blockStartContext => + blockStartContext.RegisterSyntaxNodeAction(SyntaxNodeAction, SyntaxKind.IsExpression)); + }); + } - if (!TryGetPatternPieces(isExpression, - out var ifStatement, out var localDeclarationStatement, - out var declarator, out var castExpression)) - { - return; - } + private void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext) + { + var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferPatternMatchingOverIsWithCastCheck; + if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) + { + // Bail immediately if the user has disabled this feature. + return; + } - // Bail out if the potential diagnostic location is outside the analysis span. - if (!syntaxContext.ShouldAnalyzeSpan(localDeclarationStatement.Span)) - return; + var isExpression = (BinaryExpressionSyntax)syntaxContext.Node; - // It's of the form: - // - // if (expr is Type) - // { - // var v = (Type)expr; - // } + if (!TryGetPatternPieces(isExpression, + out var ifStatement, out var localDeclarationStatement, + out var declarator, out var castExpression)) + { + return; + } - // Make sure that moving 'v' to the outer scope won't cause any conflicts. + // Bail out if the potential diagnostic location is outside the analysis span. + if (!syntaxContext.ShouldAnalyzeSpan(localDeclarationStatement.Span)) + return; - var ifStatementScope = ifStatement.Parent.IsKind(SyntaxKind.Block) - ? ifStatement.Parent - : ifStatement; + // It's of the form: + // + // if (expr is Type) + // { + // var v = (Type)expr; + // } - if (ContainsVariableDeclaration(ifStatementScope, declarator)) - { - // can't switch to using a pattern here as it would cause a scoping - // problem. - // - // TODO(cyrusn): Consider allowing the user to do this, but giving - // them an error preview. - return; - } + // Make sure that moving 'v' to the outer scope won't cause any conflicts. - var cancellationToken = syntaxContext.CancellationToken; - var semanticModel = syntaxContext.SemanticModel; - var localSymbol = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(declarator, cancellationToken); - var isType = semanticModel.GetTypeInfo(castExpression.Type).Type; + var ifStatementScope = ifStatement.Parent.IsKind(SyntaxKind.Block) + ? ifStatement.Parent + : ifStatement; - if (isType.IsNullable()) - { - // not legal to write "if (x is int? y)" - return; - } + if (ContainsVariableDeclaration(ifStatementScope, declarator)) + { + // can't switch to using a pattern here as it would cause a scoping + // problem. + // + // TODO(cyrusn): Consider allowing the user to do this, but giving + // them an error preview. + return; + } - if (isType?.TypeKind == TypeKind.Dynamic) - { - // Not legal to use dynamic in a pattern. - return; - } + var cancellationToken = syntaxContext.CancellationToken; + var semanticModel = syntaxContext.SemanticModel; + var localSymbol = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(declarator, cancellationToken); + var isType = semanticModel.GetTypeInfo(castExpression.Type).Type; - if (!localSymbol.Type.Equals(isType)) - { - // we have something like: - // - // if (x is DerivedType) - // { - // BaseType b = (DerivedType)x; - // } - // - // It's not necessarily safe to convert this to: - // - // if (x is DerivedType b) { ... } - // - // That's because there may be later code that wants to do something like assign a - // 'BaseType' into 'b'. As we've now claimed that it must be DerivedType, that - // won't work. This might also cause unintended changes like changing overload - // resolution. So, we conservatively do not offer the change in a situation like this. - return; - } + if (isType.IsNullable()) + { + // not legal to write "if (x is int? y)" + return; + } - // Looks good! - var additionalLocations = ImmutableArray.Create( - ifStatement.GetLocation(), - localDeclarationStatement.GetLocation()); - - // Put a diagnostic with the appropriate severity on the declaration-statement itself. - syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - localDeclarationStatement.GetLocation(), - styleOption.Notification, - additionalLocations, - properties: null)); + if (isType?.TypeKind == TypeKind.Dynamic) + { + // Not legal to use dynamic in a pattern. + return; } - public static bool TryGetPatternPieces( - BinaryExpressionSyntax isExpression, - [NotNullWhen(true)] out IfStatementSyntax? ifStatement, - [NotNullWhen(true)] out LocalDeclarationStatementSyntax? localDeclarationStatement, - [NotNullWhen(true)] out VariableDeclaratorSyntax? declarator, - [NotNullWhen(true)] out CastExpressionSyntax? castExpression) + if (!localSymbol.Type.Equals(isType)) { - localDeclarationStatement = null; - declarator = null; - castExpression = null; + // we have something like: + // + // if (x is DerivedType) + // { + // BaseType b = (DerivedType)x; + // } + // + // It's not necessarily safe to convert this to: + // + // if (x is DerivedType b) { ... } + // + // That's because there may be later code that wants to do something like assign a + // 'BaseType' into 'b'. As we've now claimed that it must be DerivedType, that + // won't work. This might also cause unintended changes like changing overload + // resolution. So, we conservatively do not offer the change in a situation like this. + return; + } - // The is check has to be in an if check: "if (x is Type) - if (!isExpression.Parent.IsKind(SyntaxKind.IfStatement, out ifStatement)) - return false; + // Looks good! + var additionalLocations = ImmutableArray.Create( + ifStatement.GetLocation(), + localDeclarationStatement.GetLocation()); + + // Put a diagnostic with the appropriate severity on the declaration-statement itself. + syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + localDeclarationStatement.GetLocation(), + styleOption.Notification, + syntaxContext.Options, + additionalLocations, + properties: null)); + } - if (ifStatement.Statement is not BlockSyntax block) - return false; + public static bool TryGetPatternPieces( + BinaryExpressionSyntax isExpression, + [NotNullWhen(true)] out IfStatementSyntax? ifStatement, + [NotNullWhen(true)] out LocalDeclarationStatementSyntax? localDeclarationStatement, + [NotNullWhen(true)] out VariableDeclaratorSyntax? declarator, + [NotNullWhen(true)] out CastExpressionSyntax? castExpression) + { + localDeclarationStatement = null; + declarator = null; + castExpression = null; - if (block.Statements is [TryStatementSyntax tryStatement, ..]) - block = tryStatement.Block; + // The is check has to be in an if check: "if (x is Type) + if (!isExpression.Parent.IsKind(SyntaxKind.IfStatement, out ifStatement)) + return false; - if (block.Statements.Count == 0) - return false; + if (ifStatement.Statement is not BlockSyntax block) + return false; - var firstStatement = block.Statements[0]; - if (!firstStatement.IsKind(SyntaxKind.LocalDeclarationStatement, out localDeclarationStatement)) - return false; + if (block.Statements is [TryStatementSyntax tryStatement, ..]) + block = tryStatement.Block; - if (localDeclarationStatement.Declaration.Variables.Count != 1) - return false; + if (block.Statements.Count == 0) + return false; - declarator = localDeclarationStatement.Declaration.Variables[0]; - if (declarator.Initializer == null) - return false; + var firstStatement = block.Statements[0]; + if (!firstStatement.IsKind(SyntaxKind.LocalDeclarationStatement, out localDeclarationStatement)) + return false; - var declaratorValue = declarator.Initializer.Value.WalkDownParentheses(); - if (!declaratorValue.IsKind(SyntaxKind.CastExpression, out castExpression)) - return false; + if (localDeclarationStatement.Declaration.Variables.Count != 1) + return false; - if (!SyntaxFactory.AreEquivalent(isExpression.Left.WalkDownParentheses(), castExpression.Expression.WalkDownParentheses(), topLevel: false) || - !SyntaxFactory.AreEquivalent(isExpression.Right.WalkDownParentheses(), castExpression.Type, topLevel: false)) - { - return false; - } + declarator = localDeclarationStatement.Declaration.Variables[0]; + if (declarator.Initializer == null) + return false; - return true; + var declaratorValue = declarator.Initializer.Value.WalkDownParentheses(); + if (!declaratorValue.IsKind(SyntaxKind.CastExpression, out castExpression)) + return false; + + if (!SyntaxFactory.AreEquivalent(isExpression.Left.WalkDownParentheses(), castExpression.Expression.WalkDownParentheses(), topLevel: false) || + !SyntaxFactory.AreEquivalent(isExpression.Right.WalkDownParentheses(), castExpression.Type, topLevel: false)) + { + return false; } - private static bool ContainsVariableDeclaration( - SyntaxNode scope, VariableDeclaratorSyntax variable) + return true; + } + + private static bool ContainsVariableDeclaration( + SyntaxNode scope, VariableDeclaratorSyntax variable) + { + var variableName = variable.Identifier.ValueText; + + using var _ = ArrayBuilder.GetInstance(out var stack); + stack.Push(scope); + + while (stack.Count > 0) { - var variableName = variable.Identifier.ValueText; + var current = stack.Pop(); - using var _ = ArrayBuilder.GetInstance(out var stack); - stack.Push(scope); + if (current == variable) + continue; - while (stack.Count > 0) + if (current is VariableDeclaratorSyntax declarator && + declarator.Identifier.ValueText.Equals(variableName)) + { + return true; + } + else if (current is SingleVariableDesignationSyntax designation && + designation.Identifier.ValueText.Equals(variableName)) { - var current = stack.Pop(); - - if (current == variable) - continue; - - if (current is VariableDeclaratorSyntax declarator && - declarator.Identifier.ValueText.Equals(variableName)) - { - return true; - } - else if (current is SingleVariableDesignationSyntax designation && - designation.Identifier.ValueText.Equals(variableName)) - { - return true; - } - - foreach (var child in current.ChildNodesAndTokens()) - { - if (child.IsNode) - stack.Push(child.AsNode()!); - } + return true; } - return false; + foreach (var child in current.ChildNodesAndTokens()) + { + if (child.IsNode) + stack.Push(child.AsNode()!); + } } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + return false; } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; } diff --git a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpUseNotPatternDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpUseNotPatternDiagnosticAnalyzer.cs index 29d1d3fede004..da2601fd3a508 100644 --- a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpUseNotPatternDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/CSharpUseNotPatternDiagnosticAnalyzer.cs @@ -97,6 +97,7 @@ private void SyntaxNodeAction( Descriptor, isKeyword.GetLocation(), styleOption.Notification, + context.Options, ImmutableArray.Create(node.GetLocation()), properties: null)); } diff --git a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/UsePatternMatchingHelpers.cs b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/UsePatternMatchingHelpers.cs index 686d88681263c..0241d39f8572c 100644 --- a/src/Analyzers/CSharp/Analyzers/UsePatternMatching/UsePatternMatchingHelpers.cs +++ b/src/Analyzers/CSharp/Analyzers/UsePatternMatching/UsePatternMatchingHelpers.cs @@ -7,99 +7,98 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching +namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching; + +internal static class UsePatternMatchingHelpers { - internal static class UsePatternMatchingHelpers + public static bool TryGetPartsOfAsAndMemberAccessCheck( + BinaryExpressionSyntax asExpression, + [NotNullWhen(true)] out ConditionalAccessExpressionSyntax? conditionalAccessExpression, + out BinaryExpressionSyntax? binaryExpression, + out IsPatternExpressionSyntax? isPatternExpression, + out LanguageVersion requiredLanguageVersion) { - public static bool TryGetPartsOfAsAndMemberAccessCheck( - BinaryExpressionSyntax asExpression, - [NotNullWhen(true)] out ConditionalAccessExpressionSyntax? conditionalAccessExpression, - out BinaryExpressionSyntax? binaryExpression, - out IsPatternExpressionSyntax? isPatternExpression, - out LanguageVersion requiredLanguageVersion) - { - conditionalAccessExpression = null; - binaryExpression = null; - isPatternExpression = null; - requiredLanguageVersion = LanguageVersion.CSharp8; + conditionalAccessExpression = null; + binaryExpression = null; + isPatternExpression = null; + requiredLanguageVersion = LanguageVersion.CSharp8; - if (asExpression.Kind() == SyntaxKind.AsExpression) - { - // has to be `(expr as T)` - if (asExpression.Parent is not ParenthesizedExpressionSyntax - { - // Has to be `(expr as T)?...` - Parent: ConditionalAccessExpressionSyntax parentConditionalAccess - }) + if (asExpression.Kind() == SyntaxKind.AsExpression) + { + // has to be `(expr as T)` + if (asExpression.Parent is not ParenthesizedExpressionSyntax { - return false; - } + // Has to be `(expr as T)?...` + Parent: ConditionalAccessExpressionSyntax parentConditionalAccess + }) + { + return false; + } - conditionalAccessExpression = parentConditionalAccess; + conditionalAccessExpression = parentConditionalAccess; - // After the `?` has to be `.X.Y.Z` + // After the `?` has to be `.X.Y.Z` - var whenNotNull = parentConditionalAccess.WhenNotNull; - while (whenNotNull is MemberAccessExpressionSyntax memberAccess) - { - // Extended property patterns are only in 10 and up. - requiredLanguageVersion = LanguageVersion.CSharp10; - whenNotNull = memberAccess.Expression; - } + var whenNotNull = parentConditionalAccess.WhenNotNull; + while (whenNotNull is MemberAccessExpressionSyntax memberAccess) + { + // Extended property patterns are only in 10 and up. + requiredLanguageVersion = LanguageVersion.CSharp10; + whenNotNull = memberAccess.Expression; + } - // Ensure we have `.X` - if (whenNotNull is not MemberBindingExpressionSyntax) - return false; + // Ensure we have `.X` + if (whenNotNull is not MemberBindingExpressionSyntax) + return false; - if (conditionalAccessExpression.Parent is BinaryExpressionSyntax(SyntaxKind.EqualsExpression) parentBinaryExpression1 && - parentBinaryExpression1.Left == conditionalAccessExpression) - { - // `(expr as T)?... == other_expr - // - // Can convert if other_expr is a constant (checked by caller). - binaryExpression = parentBinaryExpression1; - return true; - } - else if (conditionalAccessExpression.Parent is - BinaryExpressionSyntax( - SyntaxKind.NotEqualsExpression or - SyntaxKind.GreaterThanExpression or - SyntaxKind.GreaterThanOrEqualExpression or - SyntaxKind.LessThanExpression or - SyntaxKind.LessThanOrEqualExpression) parentBinaryExpression2 && - parentBinaryExpression2.Left == conditionalAccessExpression) - { - // `(expr as T)?... != other_expr - // - // Can convert if other_expr is a constant (checked by caller). - binaryExpression = parentBinaryExpression2; + if (conditionalAccessExpression.Parent is BinaryExpressionSyntax(SyntaxKind.EqualsExpression) parentBinaryExpression1 && + parentBinaryExpression1.Left == conditionalAccessExpression) + { + // `(expr as T)?... == other_expr + // + // Can convert if other_expr is a constant (checked by caller). + binaryExpression = parentBinaryExpression1; + return true; + } + else if (conditionalAccessExpression.Parent is + BinaryExpressionSyntax( + SyntaxKind.NotEqualsExpression or + SyntaxKind.GreaterThanExpression or + SyntaxKind.GreaterThanOrEqualExpression or + SyntaxKind.LessThanExpression or + SyntaxKind.LessThanOrEqualExpression) parentBinaryExpression2 && + parentBinaryExpression2.Left == conditionalAccessExpression) + { + // `(expr as T)?... != other_expr + // + // Can convert if other_expr is a constant (checked by caller). + binaryExpression = parentBinaryExpression2; - // relational patterns need c# 9 or above. - requiredLanguageVersion = (LanguageVersion)Math.Max((int)requiredLanguageVersion, (int)LanguageVersion.CSharp9); - return true; - } - else if (conditionalAccessExpression.Parent is IsPatternExpressionSyntax parentIsPatternExpression) - { - // `(expr as T)?... is pattern` - // - // We can convert this to a pattern in most cases except for: - // - // `(expr as T)?... is not X y`. As it is not legal to transform into `expr is T { Member: not X y }`. - // Specifically, `not variable` patterns are not legal except at the top level of a pattern. - if (parentIsPatternExpression.Pattern is UnaryPatternSyntax - { - Pattern: DeclarationPatternSyntax or VarPatternSyntax or RecursivePatternSyntax { Designation: not null } - }) + // relational patterns need c# 9 or above. + requiredLanguageVersion = (LanguageVersion)Math.Max((int)requiredLanguageVersion, (int)LanguageVersion.CSharp9); + return true; + } + else if (conditionalAccessExpression.Parent is IsPatternExpressionSyntax parentIsPatternExpression) + { + // `(expr as T)?... is pattern` + // + // We can convert this to a pattern in most cases except for: + // + // `(expr as T)?... is not X y`. As it is not legal to transform into `expr is T { Member: not X y }`. + // Specifically, `not variable` patterns are not legal except at the top level of a pattern. + if (parentIsPatternExpression.Pattern is UnaryPatternSyntax { - return false; - } - - isPatternExpression = parentIsPatternExpression; - return true; + Pattern: DeclarationPatternSyntax or VarPatternSyntax or RecursivePatternSyntax { Designation: not null } + }) + { + return false; } - } - return false; + isPatternExpression = parentIsPatternExpression; + return true; + } } + + return false; } } diff --git a/src/Analyzers/CSharp/Analyzers/UsePrimaryConstructor/CSharpUsePrimaryConstructorDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UsePrimaryConstructor/CSharpUsePrimaryConstructorDiagnosticAnalyzer.cs index f85b079cb6aef..68983f7a69938 100644 --- a/src/Analyzers/CSharp/Analyzers/UsePrimaryConstructor/CSharpUsePrimaryConstructorDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UsePrimaryConstructor/CSharpUsePrimaryConstructorDiagnosticAnalyzer.cs @@ -188,6 +188,7 @@ private void OnSymbolEnd(SymbolAnalysisContext context) _diagnosticAnalyzer.Descriptor, _primaryConstructorDeclaration.Identifier.GetLocation(), _styleOption.Notification, + context.Options, ImmutableArray.Create(_primaryConstructorDeclaration.GetLocation()), properties)); diff --git a/src/Analyzers/CSharp/Analyzers/UseSimpleUsingStatement/UseSimpleUsingStatementDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseSimpleUsingStatement/UseSimpleUsingStatementDiagnosticAnalyzer.cs index d3ecfa4d1a01d..6535e0e24d206 100644 --- a/src/Analyzers/CSharp/Analyzers/UseSimpleUsingStatement/UseSimpleUsingStatementDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseSimpleUsingStatement/UseSimpleUsingStatementDiagnosticAnalyzer.cs @@ -13,244 +13,244 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UseSimpleUsingStatement +namespace Microsoft.CodeAnalysis.CSharp.UseSimpleUsingStatement; + +/// +/// Looks for code like: +/// +/// ```c# +/// using (var a = b) +/// using (var c = d) +/// using (var e = f) +/// { +/// } +/// ``` +/// +/// And offers to convert it to: +/// +/// ```c# +/// using var a = b; +/// using var c = d; +/// using var e = f; +/// ``` +/// +/// (this of course works in the case where there is only one using). +/// +/// A few design decisions: +/// +/// 1. We only offer this if the entire group of usings in a nested stack can be +/// converted. We don't want to take a nice uniform group and break it into +/// a combination of using-statements and using-declarations. That may feel +/// less pleasant to the user than just staying uniform. +/// +/// 2. We're conservative about converting. Because `using`s may be critical for +/// program correctness, we only convert when we're absolutely *certain* that +/// semantics will not change. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class UseSimpleUsingStatementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - /// - /// Looks for code like: - /// - /// ```c# - /// using (var a = b) - /// using (var c = d) - /// using (var e = f) - /// { - /// } - /// ``` - /// - /// And offers to convert it to: - /// - /// ```c# - /// using var a = b; - /// using var c = d; - /// using var e = f; - /// ``` - /// - /// (this of course works in the case where there is only one using). - /// - /// A few design decisions: - /// - /// 1. We only offer this if the entire group of usings in a nested stack can be - /// converted. We don't want to take a nice uniform group and break it into - /// a combination of using-statements and using-declarations. That may feel - /// less pleasant to the user than just staying uniform. - /// - /// 2. We're conservative about converting. Because `using`s may be critical for - /// program correctness, we only convert when we're absolutely *certain* that - /// semantics will not change. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class UseSimpleUsingStatementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public UseSimpleUsingStatementDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseSimpleUsingStatementDiagnosticId, + EnforceOnBuildValues.UseSimpleUsingStatement, + CSharpCodeStyleOptions.PreferSimpleUsingStatement, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_simple_using_statement), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), + new LocalizableResourceString(nameof(CSharpAnalyzersResources.using_statement_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public UseSimpleUsingStatementDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseSimpleUsingStatementDiagnosticId, - EnforceOnBuildValues.UseSimpleUsingStatement, - CSharpCodeStyleOptions.PreferSimpleUsingStatement, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_simple_using_statement), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)), - new LocalizableResourceString(nameof(CSharpAnalyzersResources.using_statement_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => + { + if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp8) + return; + + context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.UsingStatement); + }); + } + + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var option = context.GetCSharpAnalyzerOptions().PreferSimpleUsingStatement; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; + + var cancellationToken = context.CancellationToken; + var outermostUsing = (UsingStatementSyntax)context.Node; + var semanticModel = context.SemanticModel; + + if (outermostUsing.Parent is not BlockSyntax parentBlock) { + // Don't offer on a using statement that is parented by another using statement. We'll just offer on the + // topmost using statement. + return; } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + var innermostUsing = outermostUsing; - protected override void InitializeWorker(AnalysisContext context) + // Check that all the immediately nested usings are convertible as well. + // We don't want take a sequence of nested-using and only convert some of them. + for (var current = outermostUsing; current != null; current = current.Statement as UsingStatementSyntax) { - context.RegisterCompilationStartAction(context => - { - if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp8) - return; + innermostUsing = current; + if (current.Declaration == null) + return; + } - context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.UsingStatement); - }); + // Verify that changing this using-statement into a using-declaration will not change semantics. + if (!PreservesSemantics(semanticModel, parentBlock, outermostUsing, innermostUsing, cancellationToken)) + return; + + // Converting a using-statement to a using-variable-declaration will cause the using's variables to now be + // pushed up to the parent block's scope. This is also true for any local variables in the innermost using's + // block. These may then collide with other variables in the block, causing an error. Check for that and + // bail if this happens. + if (CausesVariableCollision( + context.SemanticModel, parentBlock, + outermostUsing, innermostUsing, cancellationToken)) + { + return; } - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + // Good to go! + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + outermostUsing.UsingKeyword.GetLocation(), + option.Notification, + context.Options, + additionalLocations: ImmutableArray.Create(outermostUsing.GetLocation()), + properties: null)); + } + + private static bool CausesVariableCollision( + SemanticModel semanticModel, BlockSyntax parentBlock, + UsingStatementSyntax outermostUsing, UsingStatementSyntax innermostUsing, + CancellationToken cancellationToken) + { + var symbolNameToExistingSymbol = semanticModel.GetExistingSymbols(parentBlock, cancellationToken).ToLookup(s => s.Name); + + for (var current = outermostUsing; current != null; current = current.Statement as UsingStatementSyntax) { - var option = context.GetCSharpAnalyzerOptions().PreferSimpleUsingStatement; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; + // Check if the using statement itself contains variables that will collide with other variables in the + // block. + var usingOperation = (IUsingOperation)semanticModel.GetRequiredOperation(current, cancellationToken); + if (DeclaredLocalCausesCollision(symbolNameToExistingSymbol, usingOperation.Locals)) + return true; + } - var cancellationToken = context.CancellationToken; - var outermostUsing = (UsingStatementSyntax)context.Node; - var semanticModel = context.SemanticModel; + var innerUsingOperation = (IUsingOperation)semanticModel.GetRequiredOperation(innermostUsing, cancellationToken); + if (innerUsingOperation.Body is IBlockOperation innerUsingBlock) + return DeclaredLocalCausesCollision(symbolNameToExistingSymbol, innerUsingBlock.Locals); - if (outermostUsing.Parent is not BlockSyntax parentBlock) - { - // Don't offer on a using statement that is parented by another using statement. We'll just offer on the - // topmost using statement. - return; - } + return false; + } - var innermostUsing = outermostUsing; + private static bool DeclaredLocalCausesCollision(ILookup symbolNameToExistingSymbol, ImmutableArray locals) + => locals.Any(static (local, symbolNameToExistingSymbol) => symbolNameToExistingSymbol[local.Name].Any(otherLocal => !local.Equals(otherLocal)), symbolNameToExistingSymbol); - // Check that all the immediately nested usings are convertible as well. - // We don't want take a sequence of nested-using and only convert some of them. - for (var current = outermostUsing; current != null; current = current.Statement as UsingStatementSyntax) - { - innermostUsing = current; - if (current.Declaration == null) - return; - } + private static bool PreservesSemantics( + SemanticModel semanticModel, + BlockSyntax parentBlock, + UsingStatementSyntax outermostUsing, + UsingStatementSyntax innermostUsing, + CancellationToken cancellationToken) + { + var statements = parentBlock.Statements; + var index = statements.IndexOf(outermostUsing); - // Verify that changing this using-statement into a using-declaration will not change semantics. - if (!PreservesSemantics(semanticModel, parentBlock, outermostUsing, innermostUsing, cancellationToken)) - return; + return UsingValueDoesNotLeakToFollowingStatements(semanticModel, statements, index, cancellationToken) && + UsingStatementDoesNotInvolveJumps(statements, index, innermostUsing); + } - // Converting a using-statement to a using-variable-declaration will cause the using's variables to now be - // pushed up to the parent block's scope. This is also true for any local variables in the innermost using's - // block. These may then collide with other variables in the block, causing an error. Check for that and - // bail if this happens. - if (CausesVariableCollision( - context.SemanticModel, parentBlock, - outermostUsing, innermostUsing, cancellationToken)) - { - return; - } - - // Good to go! - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - outermostUsing.UsingKeyword.GetLocation(), - option.Notification, - additionalLocations: ImmutableArray.Create(outermostUsing.GetLocation()), - properties: null)); + private static bool UsingStatementDoesNotInvolveJumps( + SyntaxList parentStatements, int index, UsingStatementSyntax innermostUsing) + { + // Jumps are not allowed to cross a using declaration in the forward direction, and can't go back unless + // there is a curly brace between the using and the label. + // + // We conservatively implement this by disallowing the change if there are gotos/labels + // in the containing block, or inside the using body. + + // Note: we only have to check up to the `using`, since the checks below in + // UsingValueDoesNotLeakToFollowingStatements ensure that there would be no labels/gotos *after* the using + // statement. + for (var i = 0; i < index; i++) + { + var priorStatement = parentStatements[i]; + if (IsGotoOrLabeledStatement(priorStatement)) + return false; } - private static bool CausesVariableCollision( - SemanticModel semanticModel, BlockSyntax parentBlock, - UsingStatementSyntax outermostUsing, UsingStatementSyntax innermostUsing, - CancellationToken cancellationToken) + var innerStatements = innermostUsing.Statement is BlockSyntax block + ? block.Statements + : new SyntaxList(innermostUsing.Statement); + + foreach (var statement in innerStatements) { - var symbolNameToExistingSymbol = semanticModel.GetExistingSymbols(parentBlock, cancellationToken).ToLookup(s => s.Name); - - for (var current = outermostUsing; current != null; current = current.Statement as UsingStatementSyntax) - { - // Check if the using statement itself contains variables that will collide with other variables in the - // block. - var usingOperation = (IUsingOperation)semanticModel.GetRequiredOperation(current, cancellationToken); - if (DeclaredLocalCausesCollision(symbolNameToExistingSymbol, usingOperation.Locals)) - return true; - } - - var innerUsingOperation = (IUsingOperation)semanticModel.GetRequiredOperation(innermostUsing, cancellationToken); - if (innerUsingOperation.Body is IBlockOperation innerUsingBlock) - return DeclaredLocalCausesCollision(symbolNameToExistingSymbol, innerUsingBlock.Locals); - - return false; + if (IsGotoOrLabeledStatement(statement)) + return false; } - private static bool DeclaredLocalCausesCollision(ILookup symbolNameToExistingSymbol, ImmutableArray locals) - => locals.Any(static (local, symbolNameToExistingSymbol) => symbolNameToExistingSymbol[local.Name].Any(otherLocal => !local.Equals(otherLocal)), symbolNameToExistingSymbol); + return true; + } - private static bool PreservesSemantics( - SemanticModel semanticModel, - BlockSyntax parentBlock, - UsingStatementSyntax outermostUsing, - UsingStatementSyntax innermostUsing, - CancellationToken cancellationToken) - { - var statements = parentBlock.Statements; - var index = statements.IndexOf(outermostUsing); + private static bool IsGotoOrLabeledStatement(StatementSyntax priorStatement) + => priorStatement.Kind() is SyntaxKind.GotoStatement or + SyntaxKind.LabeledStatement; - return UsingValueDoesNotLeakToFollowingStatements(semanticModel, statements, index, cancellationToken) && - UsingStatementDoesNotInvolveJumps(statements, index, innermostUsing); - } + private static bool UsingValueDoesNotLeakToFollowingStatements( + SemanticModel semanticModel, + SyntaxList statements, + int index, + CancellationToken cancellationToken) + { + // Has to be one of the following forms: + // 1. Using statement is the last statement in the parent. + // 2. Using statement is not the last statement in parent, but is followed by something that is unaffected + // by simplifying the using statement. i.e. `return`/`break`/`continue`. *Note*. `return expr` would + // *not* be ok. In that case, `expr` would now be evaluated *before* the using disposed the resource, + // instead of afterwards. Effectively, the statement following cannot actually execute any code that + // might depend on the .Dispose method being called or not. + + // Note: we can skip local functions as moving their scope inside the using doesn't change anything. + while (index + 1 < statements.Count && statements[index + 1] is LocalFunctionStatementSyntax) + index++; + + // if we got to the end of the the block then this can be converted. + if (index == statements.Count - 1) + return true; - private static bool UsingStatementDoesNotInvolveJumps( - SyntaxList parentStatements, int index, UsingStatementSyntax innermostUsing) + // Not the last statement, get the next statement and examine that. + var nextStatement = statements[index + 1]; + if (nextStatement is BreakStatementSyntax or ContinueStatementSyntax) { - // Jumps are not allowed to cross a using declaration in the forward direction, and can't go back unless - // there is a curly brace between the using and the label. - // - // We conservatively implement this by disallowing the change if there are gotos/labels - // in the containing block, or inside the using body. - - // Note: we only have to check up to the `using`, since the checks below in - // UsingValueDoesNotLeakToFollowingStatements ensure that there would be no labels/gotos *after* the using - // statement. - for (var i = 0; i < index; i++) - { - var priorStatement = parentStatements[i]; - if (IsGotoOrLabeledStatement(priorStatement)) - return false; - } - - var innerStatements = innermostUsing.Statement is BlockSyntax block - ? block.Statements - : new SyntaxList(innermostUsing.Statement); - - foreach (var statement in innerStatements) - { - if (IsGotoOrLabeledStatement(statement)) - return false; - } - + // using statement followed by break/continue. Can convert this as executing the break/continue will + // cause the code to exit the using scope, causing Dispose to be called at the same place as before. return true; } - private static bool IsGotoOrLabeledStatement(StatementSyntax priorStatement) - => priorStatement.Kind() is SyntaxKind.GotoStatement or - SyntaxKind.LabeledStatement; - - private static bool UsingValueDoesNotLeakToFollowingStatements( - SemanticModel semanticModel, - SyntaxList statements, - int index, - CancellationToken cancellationToken) + if (nextStatement is ReturnStatementSyntax returnStatement) { - // Has to be one of the following forms: - // 1. Using statement is the last statement in the parent. - // 2. Using statement is not the last statement in parent, but is followed by something that is unaffected - // by simplifying the using statement. i.e. `return`/`break`/`continue`. *Note*. `return expr` would - // *not* be ok. In that case, `expr` would now be evaluated *before* the using disposed the resource, - // instead of afterwards. Effectively, the statement following cannot actually execute any code that - // might depend on the .Dispose method being called or not. - - // Note: we can skip local functions as moving their scope inside the using doesn't change anything. - while (index + 1 < statements.Count && statements[index + 1] is LocalFunctionStatementSyntax) - index++; - - // if we got to the end of the the block then this can be converted. - if (index == statements.Count - 1) + // using statement followed by `return`. Can convert this as executing the `return` will cause the code + // to exit the using scope, causing Dispose to be called at the same place as before. + // + // Note: the expr has to be null. If it was non-null, then the expr would now execute before hte using + // called 'Dispose' instead of after, potentially changing semantics. + if (returnStatement.Expression is null) return true; - // Not the last statement, get the next statement and examine that. - var nextStatement = statements[index + 1]; - if (nextStatement is BreakStatementSyntax or ContinueStatementSyntax) - { - // using statement followed by break/continue. Can convert this as executing the break/continue will - // cause the code to exit the using scope, causing Dispose to be called at the same place as before. - return true; - } - - if (nextStatement is ReturnStatementSyntax returnStatement) - { - // using statement followed by `return`. Can convert this as executing the `return` will cause the code - // to exit the using scope, causing Dispose to be called at the same place as before. - // - // Note: the expr has to be null. If it was non-null, then the expr would now execute before hte using - // called 'Dispose' instead of after, potentially changing semantics. - if (returnStatement.Expression is null) - return true; - - // return constant; - // - // This is also safe to return as constants could not be affected by being inside or outside the using block. - var constantValue = semanticModel.GetConstantValue(returnStatement.Expression, cancellationToken); - return constantValue.HasValue; - } - - // Add any additional cases here in the future. - return false; + // return constant; + // + // This is also safe to return as constants could not be affected by being inside or outside the using block. + var constantValue = semanticModel.GetConstantValue(returnStatement.Expression, cancellationToken); + return constantValue.HasValue; } + + // Add any additional cases here in the future. + return false; } } diff --git a/src/Analyzers/CSharp/Analyzers/UseTupleSwap/CSharpUseTupleSwapDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseTupleSwap/CSharpUseTupleSwapDiagnosticAnalyzer.cs index b7143fe04da1a..a34b0bc2ba03d 100644 --- a/src/Analyzers/CSharp/Analyzers/UseTupleSwap/CSharpUseTupleSwapDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseTupleSwap/CSharpUseTupleSwapDiagnosticAnalyzer.cs @@ -11,137 +11,137 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UseTupleSwap +namespace Microsoft.CodeAnalysis.CSharp.UseTupleSwap; + +/// +/// Looks for code of the form: +/// +/// +/// var temp = expr_a; +/// expr_a = expr_b; +/// expr_b = temp; +/// +/// +/// and converts it to: +/// +/// +/// (expr_b, expr_a) = (expr_a, expr_b); +/// +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpUseTupleSwapDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - /// - /// Looks for code of the form: - /// - /// - /// var temp = expr_a; - /// expr_a = expr_b; - /// expr_b = temp; - /// - /// - /// and converts it to: - /// - /// - /// (expr_b, expr_a) = (expr_a, expr_b); - /// - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpUseTupleSwapDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public CSharpUseTupleSwapDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseTupleSwapDiagnosticId, + EnforceOnBuildValues.UseTupleSwap, + CSharpCodeStyleOptions.PreferTupleSwap, + new LocalizableResourceString( + nameof(CSharpAnalyzersResources.Use_tuple_to_swap_values), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - public CSharpUseTupleSwapDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseTupleSwapDiagnosticId, - EnforceOnBuildValues.UseTupleSwap, - CSharpCodeStyleOptions.PreferTupleSwap, - new LocalizableResourceString( - nameof(CSharpAnalyzersResources.Use_tuple_to_swap_values), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - { - context.RegisterCompilationStartAction(context => - { - // Tuples are only available in C# 7 and above. - var compilation = context.Compilation; - if (compilation.LanguageVersion() < LanguageVersion.CSharp7) - return; - - context.RegisterSyntaxNodeAction( - AnalyzeLocalDeclarationStatement, - SyntaxKind.LocalDeclarationStatement); - }); - } - - private void AnalyzeLocalDeclarationStatement(SyntaxNodeAnalysisContext syntaxContext) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - var cancellationToken = syntaxContext.CancellationToken; - var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferTupleSwap; - if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) - return; - - // `var expr_temp = expr_a`; - var localDeclarationStatement = (LocalDeclarationStatementSyntax)syntaxContext.Node; - if (localDeclarationStatement.UsingKeyword != default || - localDeclarationStatement.AwaitKeyword != default) - { - return; - } - - if (localDeclarationStatement.Declaration.Variables.Count != 1) - return; - - var variableDeclarator = localDeclarationStatement.Declaration.Variables.First(); - var localDeclarationExprA = variableDeclarator.Initializer?.Value.WalkDownParentheses(); - if (localDeclarationExprA == null) - return; - - // `expr_a = expr_b`; - var firstAssignmentStatement = localDeclarationStatement.GetNextStatement(); - if (!IsSimpleAssignment(firstAssignmentStatement, out var firstAssignmentExprA, out var firstAssignmentExprB)) - return; - - // `expr_b = expr_temp;` - var secondAssignmentStatement = firstAssignmentStatement.GetNextStatement(); - if (!IsSimpleAssignment(secondAssignmentStatement, out var secondAssignmentExprB, out var secondAssignmentExprTemp)) + // Tuples are only available in C# 7 and above. + var compilation = context.Compilation; + if (compilation.LanguageVersion() < LanguageVersion.CSharp7) return; - if (!localDeclarationExprA.IsEquivalentTo(firstAssignmentExprA, topLevel: false)) - return; - - if (!firstAssignmentExprB.IsEquivalentTo(secondAssignmentExprB, topLevel: false)) - return; - - if (secondAssignmentExprTemp is not IdentifierNameSyntax { Identifier: var secondAssignmentExprTempIdentifier }) - return; - - if (variableDeclarator.Identifier.ValueText != secondAssignmentExprTempIdentifier.ValueText) - return; - - // Can't swap ref-structs. - var semanticModel = syntaxContext.SemanticModel; - var local = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(variableDeclarator, cancellationToken); - if (local.Type.IsRefLikeType || local.Type.RequiresUnsafeModifier()) - return; + context.RegisterSyntaxNodeAction( + AnalyzeLocalDeclarationStatement, + SyntaxKind.LocalDeclarationStatement); + }); + } - var additionalLocations = ImmutableArray.Create( - localDeclarationStatement.GetLocation(), - firstAssignmentStatement.GetLocation(), - secondAssignmentStatement.GetLocation()); - - // If the diagnostic is not hidden, then just place the user visible part - // on the local being initialized with the lambda. - syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - localDeclarationStatement.GetFirstToken().GetLocation(), - styleOption.Notification, - additionalLocations, - properties: null)); + private void AnalyzeLocalDeclarationStatement(SyntaxNodeAnalysisContext syntaxContext) + { + var cancellationToken = syntaxContext.CancellationToken; + var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferTupleSwap; + if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification)) + return; + + // `var expr_temp = expr_a`; + var localDeclarationStatement = (LocalDeclarationStatementSyntax)syntaxContext.Node; + if (localDeclarationStatement.UsingKeyword != default || + localDeclarationStatement.AwaitKeyword != default) + { + return; } - private static bool IsSimpleAssignment( - [NotNullWhen(true)] StatementSyntax? assignmentStatement, - [NotNullWhen(true)] out ExpressionSyntax? left, - [NotNullWhen(true)] out ExpressionSyntax? right) - { - left = null; - right = null; - if (assignmentStatement == null) - return false; + if (localDeclarationStatement.Declaration.Variables.Count != 1) + return; + + var variableDeclarator = localDeclarationStatement.Declaration.Variables.First(); + var localDeclarationExprA = variableDeclarator.Initializer?.Value.WalkDownParentheses(); + if (localDeclarationExprA == null) + return; + + // `expr_a = expr_b`; + var firstAssignmentStatement = localDeclarationStatement.GetNextStatement(); + if (!IsSimpleAssignment(firstAssignmentStatement, out var firstAssignmentExprA, out var firstAssignmentExprB)) + return; + + // `expr_b = expr_temp;` + var secondAssignmentStatement = firstAssignmentStatement.GetNextStatement(); + if (!IsSimpleAssignment(secondAssignmentStatement, out var secondAssignmentExprB, out var secondAssignmentExprTemp)) + return; + + if (!localDeclarationExprA.IsEquivalentTo(firstAssignmentExprA, topLevel: false)) + return; + + if (!firstAssignmentExprB.IsEquivalentTo(secondAssignmentExprB, topLevel: false)) + return; + + if (secondAssignmentExprTemp is not IdentifierNameSyntax { Identifier: var secondAssignmentExprTempIdentifier }) + return; + + if (variableDeclarator.Identifier.ValueText != secondAssignmentExprTempIdentifier.ValueText) + return; + + // Can't swap ref-structs. + var semanticModel = syntaxContext.SemanticModel; + var local = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(variableDeclarator, cancellationToken); + if (local.Type.IsRefLikeType || local.Type.RequiresUnsafeModifier()) + return; + + var additionalLocations = ImmutableArray.Create( + localDeclarationStatement.GetLocation(), + firstAssignmentStatement.GetLocation(), + secondAssignmentStatement.GetLocation()); + + // If the diagnostic is not hidden, then just place the user visible part + // on the local being initialized with the lambda. + syntaxContext.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + localDeclarationStatement.GetFirstToken().GetLocation(), + styleOption.Notification, + syntaxContext.Options, + additionalLocations, + properties: null)); + } + + private static bool IsSimpleAssignment( + [NotNullWhen(true)] StatementSyntax? assignmentStatement, + [NotNullWhen(true)] out ExpressionSyntax? left, + [NotNullWhen(true)] out ExpressionSyntax? right) + { + left = null; + right = null; + if (assignmentStatement == null) + return false; - if (assignmentStatement is not ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignment }) - return false; + if (assignmentStatement is not ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignment }) + return false; - left = assignment.Left.WalkDownParentheses(); - right = assignment.Right.WalkDownParentheses(); + left = assignment.Left.WalkDownParentheses(); + right = assignment.Right.WalkDownParentheses(); - return left is not RefExpressionSyntax && right is not RefExpressionSyntax; - } + return left is not RefExpressionSyntax && right is not RefExpressionSyntax; } } diff --git a/src/Analyzers/CSharp/Analyzers/UseUtf8StringLiteral/UseUtf8StringLiteralDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseUtf8StringLiteral/UseUtf8StringLiteralDiagnosticAnalyzer.cs index 25f7f93b51b4e..18076827975d7 100644 --- a/src/Analyzers/CSharp/Analyzers/UseUtf8StringLiteral/UseUtf8StringLiteralDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseUtf8StringLiteral/UseUtf8StringLiteralDiagnosticAnalyzer.cs @@ -20,198 +20,197 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseUtf8StringLiteral +namespace Microsoft.CodeAnalysis.CSharp.UseUtf8StringLiteral; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class UseUtf8StringLiteralDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class UseUtf8StringLiteralDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public enum ArrayCreationOperationLocation { - public enum ArrayCreationOperationLocation - { - Ancestors, - Descendants, - Current - } + Ancestors, + Descendants, + Current + } - public UseUtf8StringLiteralDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseUtf8StringLiteralDiagnosticId, - EnforceOnBuildValues.UseUtf8StringLiteral, - CSharpCodeStyleOptions.PreferUtf8StringLiterals, - new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_Utf8_string_literal), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) - { - } + public UseUtf8StringLiteralDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseUtf8StringLiteralDiagnosticId, + EnforceOnBuildValues.UseUtf8StringLiteral, + CSharpCodeStyleOptions.PreferUtf8StringLiterals, + new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_Utf8_string_literal), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + { + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - { - if (!context.Compilation.LanguageVersion().IsCSharp11OrAbove()) - return; + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + { + if (!context.Compilation.LanguageVersion().IsCSharp11OrAbove()) + return; - if (context.Compilation.GetBestTypeByMetadataName(typeof(ReadOnlySpan<>).FullName!) is null) - return; + if (context.Compilation.GetBestTypeByMetadataName(typeof(ReadOnlySpan<>).FullName!) is null) + return; - var expressionType = context.Compilation.GetTypeByMetadataName(typeof(System.Linq.Expressions.Expression<>).FullName!); + var expressionType = context.Compilation.GetTypeByMetadataName(typeof(System.Linq.Expressions.Expression<>).FullName!); - context.RegisterOperationAction(c => AnalyzeOperation(c, expressionType), OperationKind.ArrayCreation); - }); + context.RegisterOperationAction(c => AnalyzeOperation(c, expressionType), OperationKind.ArrayCreation); + }); - private void AnalyzeOperation(OperationAnalysisContext context, INamedTypeSymbol? expressionType) - { - var arrayCreationOperation = (IArrayCreationOperation)context.Operation; + private void AnalyzeOperation(OperationAnalysisContext context, INamedTypeSymbol? expressionType) + { + var arrayCreationOperation = (IArrayCreationOperation)context.Operation; - // Don't offer if the user doesn't want it - var option = context.GetCSharpAnalyzerOptions().PreferUtf8StringLiterals; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; + // Don't offer if the user doesn't want it + var option = context.GetCSharpAnalyzerOptions().PreferUtf8StringLiterals; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; - // Only replace arrays with initializers - if (arrayCreationOperation.Initializer is null) - return; + // Only replace arrays with initializers + if (arrayCreationOperation.Initializer is null) + return; - // Using UTF-8 string literals as nested array initializers is invalid - if (arrayCreationOperation.DimensionSizes.Length > 1) - return; + // Using UTF-8 string literals as nested array initializers is invalid + if (arrayCreationOperation.DimensionSizes.Length > 1) + return; - // Must be a byte array - if (arrayCreationOperation.Type is not IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }) - return; + // Must be a byte array + if (arrayCreationOperation.Type is not IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }) + return; - // UTF-8 strings are not valid to use in attributes - if (arrayCreationOperation.Syntax.Ancestors().OfType().Any()) - return; + // UTF-8 strings are not valid to use in attributes + if (arrayCreationOperation.Syntax.Ancestors().OfType().Any()) + return; - // Can't use a UTF-8 string inside an expression tree. - var semanticModel = context.Operation.SemanticModel; - Contract.ThrowIfNull(semanticModel); - if (arrayCreationOperation.Syntax.IsInExpressionTree(semanticModel, expressionType, context.CancellationToken)) - return; + // Can't use a UTF-8 string inside an expression tree. + var semanticModel = context.Operation.SemanticModel; + Contract.ThrowIfNull(semanticModel); + if (arrayCreationOperation.Syntax.IsInExpressionTree(semanticModel, expressionType, context.CancellationToken)) + return; - var elements = arrayCreationOperation.Initializer.ElementValues; + var elements = arrayCreationOperation.Initializer.ElementValues; - // If the compiler has constructed this array creation, then we don't want to do anything - // if there aren't any elements, as we could just end up inserting ""u8 somewhere. - if (arrayCreationOperation.IsImplicit && elements.Length == 0) - return; + // If the compiler has constructed this array creation, then we don't want to do anything + // if there aren't any elements, as we could just end up inserting ""u8 somewhere. + if (arrayCreationOperation.IsImplicit && elements.Length == 0) + return; - if (!TryConvertToUtf8String(builder: null, elements)) - return; + if (!TryConvertToUtf8String(builder: null, elements)) + return; - if (arrayCreationOperation.Syntax is ImplicitArrayCreationExpressionSyntax or ArrayCreationExpressionSyntax) - { - ReportArrayCreationDiagnostic(context, arrayCreationOperation.Syntax, option.Notification); - } - else if (elements is [{ Syntax.Parent: ArgumentSyntax }, ..]) - { - // For regular parameter arrays the code fix will need to search down - ReportParameterArrayDiagnostic(context, arrayCreationOperation.Syntax, elements, option.Notification, ArrayCreationOperationLocation.Descendants); - } - else if (elements is [{ Syntax.Parent: (kind: SyntaxKind.CollectionInitializerExpression) }, ..]) - { - // For collection initializers where the Add method takes a parameter array, the code fix - // will have to search up - ReportParameterArrayDiagnostic(context, arrayCreationOperation.Syntax, elements, option.Notification, ArrayCreationOperationLocation.Ancestors); - } + if (arrayCreationOperation.Syntax is ImplicitArrayCreationExpressionSyntax or ArrayCreationExpressionSyntax) + { + ReportArrayCreationDiagnostic(context, arrayCreationOperation.Syntax, option.Notification); } - - private void ReportParameterArrayDiagnostic(OperationAnalysisContext context, SyntaxNode syntaxNode, ImmutableArray elements, NotificationOption2 notificationOption, ArrayCreationOperationLocation operationLocation) + else if (elements is [{ Syntax.Parent: ArgumentSyntax }, ..]) { - // When the first elements parent is as argument, or an edge case for collection - // initializers where the Add method takes a param array, it means we have a parameter array. - // We raise the diagnostic on all of the parameters that make up the array. We could do just - // the first element, but that might be odd seeing: M(1, 2, [|3|], 4, 5) - var span = TextSpan.FromBounds(elements[0].Syntax.SpanStart, elements[^1].Syntax.Span.End); - var location = Location.Create(syntaxNode.SyntaxTree, span); - - ReportDiagnostic(context, syntaxNode, notificationOption, location, operationLocation); + // For regular parameter arrays the code fix will need to search down + ReportParameterArrayDiagnostic(context, arrayCreationOperation.Syntax, elements, option.Notification, ArrayCreationOperationLocation.Descendants); } - - private void ReportArrayCreationDiagnostic(OperationAnalysisContext context, SyntaxNode syntaxNode, NotificationOption2 notificationOption) + else if (elements is [{ Syntax.Parent: (kind: SyntaxKind.CollectionInitializerExpression) }, ..]) { - // When the user writes the array creation we raise the diagnostic on the first token, which will be the "new" keyword - var location = syntaxNode.GetFirstToken().GetLocation(); - - ReportDiagnostic(context, syntaxNode, notificationOption, location, ArrayCreationOperationLocation.Current); + // For collection initializers where the Add method takes a parameter array, the code fix + // will have to search up + ReportParameterArrayDiagnostic(context, arrayCreationOperation.Syntax, elements, option.Notification, ArrayCreationOperationLocation.Ancestors); } + } - private void ReportDiagnostic(OperationAnalysisContext context, SyntaxNode syntaxNode, NotificationOption2 notificationOption, Location location, ArrayCreationOperationLocation operationLocation) - { - // Store the original syntax location so the code fix can find the operation again - var additionalLocations = ImmutableArray.Create(syntaxNode.GetLocation()); + private void ReportParameterArrayDiagnostic(OperationAnalysisContext context, SyntaxNode syntaxNode, ImmutableArray elements, NotificationOption2 notificationOption, ArrayCreationOperationLocation operationLocation) + { + // When the first elements parent is as argument, or an edge case for collection + // initializers where the Add method takes a param array, it means we have a parameter array. + // We raise the diagnostic on all of the parameters that make up the array. We could do just + // the first element, but that might be odd seeing: M(1, 2, [|3|], 4, 5) + var span = TextSpan.FromBounds(elements[0].Syntax.SpanStart, elements[^1].Syntax.Span.End); + var location = Location.Create(syntaxNode.SyntaxTree, span); + + ReportDiagnostic(context, syntaxNode, notificationOption, location, operationLocation); + } - // Also let the code fix where to look to find the operation that originally trigger this diagnostic - var properties = ImmutableDictionary.Empty.Add(nameof(ArrayCreationOperationLocation), operationLocation.ToString()); + private void ReportArrayCreationDiagnostic(OperationAnalysisContext context, SyntaxNode syntaxNode, NotificationOption2 notificationOption) + { + // When the user writes the array creation we raise the diagnostic on the first token, which will be the "new" keyword + var location = syntaxNode.GetFirstToken().GetLocation(); - context.ReportDiagnostic( - DiagnosticHelper.Create(Descriptor, location, notificationOption, additionalLocations, properties)); - } + ReportDiagnostic(context, syntaxNode, notificationOption, location, ArrayCreationOperationLocation.Current); + } - internal static bool TryConvertToUtf8String(StringBuilder? builder, ImmutableArray arrayCreationElements) + private void ReportDiagnostic(OperationAnalysisContext context, SyntaxNode syntaxNode, NotificationOption2 notificationOption, Location location, ArrayCreationOperationLocation operationLocation) + { + // Store the original syntax location so the code fix can find the operation again + var additionalLocations = ImmutableArray.Create(syntaxNode.GetLocation()); + + // Also let the code fix where to look to find the operation that originally trigger this diagnostic + var properties = ImmutableDictionary.Empty.Add(nameof(ArrayCreationOperationLocation), operationLocation.ToString()); + + context.ReportDiagnostic( + DiagnosticHelper.Create(Descriptor, location, notificationOption, context.Options, additionalLocations, properties)); + } + + internal static bool TryConvertToUtf8String(StringBuilder? builder, ImmutableArray arrayCreationElements) + { + for (var i = 0; i < arrayCreationElements.Length;) { - for (var i = 0; i < arrayCreationElements.Length;) + // Need to call a method to do the actual rune decoding as it uses stackalloc, and stackalloc + // in a loop is a bad idea. We also exclude any characters that are control or format chars + if (!TryGetNextRune(arrayCreationElements, i, out var rune, out var bytesConsumed) || + IsControlOrFormatRune(rune)) { - // Need to call a method to do the actual rune decoding as it uses stackalloc, and stackalloc - // in a loop is a bad idea. We also exclude any characters that are control or format chars - if (!TryGetNextRune(arrayCreationElements, i, out var rune, out var bytesConsumed) || - IsControlOrFormatRune(rune)) - { - return false; - } + return false; + } - i += bytesConsumed; + i += bytesConsumed; - if (builder is not null) + if (builder is not null) + { + if (rune.TryGetEscapeCharacter(out var escapeChar)) + { + builder.Append('\\'); + builder.Append(escapeChar); + } + else { - if (rune.TryGetEscapeCharacter(out var escapeChar)) - { - builder.Append('\\'); - builder.Append(escapeChar); - } - else - { - builder.Append(rune.ToString()); - } + builder.Append(rune.ToString()); } } - - return true; - - // We allow the three control characters that users are familiar with and wouldn't be surprised to - // see in a string literal - static bool IsControlOrFormatRune(Rune rune) - => Rune.GetUnicodeCategory(rune) is UnicodeCategory.Control or UnicodeCategory.Format - && rune.Value switch - { - '\r' => false, - '\n' => false, - '\t' => false, - _ => true - }; } - private static bool TryGetNextRune(ImmutableArray arrayCreationElements, int startIndex, out Rune rune, out int bytesConsumed) - { - rune = default; - bytesConsumed = 0; + return true; - // We only need max 4 elements for a single Rune - var length = Math.Min(arrayCreationElements.Length - startIndex, 4); + // We allow the three control characters that users are familiar with and wouldn't be surprised to + // see in a string literal + static bool IsControlOrFormatRune(Rune rune) + => Rune.GetUnicodeCategory(rune) is UnicodeCategory.Control or UnicodeCategory.Format + && rune.Value switch + { + '\r' => false, + '\n' => false, + '\t' => false, + _ => true + }; + } - Span array = stackalloc byte[length]; - for (var i = 0; i < length; i++) - { - var element = arrayCreationElements[startIndex + i]; + private static bool TryGetNextRune(ImmutableArray arrayCreationElements, int startIndex, out Rune rune, out int bytesConsumed) + { + rune = default; + bytesConsumed = 0; - // First basic check is that the array element is actually a byte - if (element.ConstantValue.Value is not byte b) - return false; + // We only need max 4 elements for a single Rune + var length = Math.Min(arrayCreationElements.Length - startIndex, 4); - array[i] = b; - } + Span array = stackalloc byte[length]; + for (var i = 0; i < length; i++) + { + var element = arrayCreationElements[startIndex + i]; + + // First basic check is that the array element is actually a byte + if (element.ConstantValue.Value is not byte b) + return false; - // If we can't decode a rune from the array then it can't be represented as a string - return Rune.DecodeFromUtf8(array, out rune, out bytesConsumed) == System.Buffers.OperationStatus.Done; + array[i] = b; } + + // If we can't decode a rune from the array then it can't be represented as a string + return Rune.DecodeFromUtf8(array, out rune, out bytesConsumed) == System.Buffers.OperationStatus.Done; } } diff --git a/src/Analyzers/CSharp/Analyzers/ValidateFormatString/CSharpValidateFormatStringDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/ValidateFormatString/CSharpValidateFormatStringDiagnosticAnalyzer.cs index f7ee9ca2af9dc..f0067c97557ee 100644 --- a/src/Analyzers/CSharp/Analyzers/ValidateFormatString/CSharpValidateFormatStringDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/ValidateFormatString/CSharpValidateFormatStringDiagnosticAnalyzer.cs @@ -9,31 +9,30 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.ValidateFormatString; -namespace Microsoft.CodeAnalysis.CSharp.ValidateFormatString +namespace Microsoft.CodeAnalysis.CSharp.ValidateFormatString; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpValidateFormatStringDiagnosticAnalyzer : + AbstractValidateFormatStringDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpValidateFormatStringDiagnosticAnalyzer : - AbstractValidateFormatStringDiagnosticAnalyzer - { - protected override ISyntaxFacts GetSyntaxFacts() - => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts GetSyntaxFacts() + => CSharpSyntaxFacts.Instance; - protected override SyntaxNode? TryGetMatchingNamedArgument( - SeparatedSyntaxList arguments, - string searchArgumentName) + protected override SyntaxNode? TryGetMatchingNamedArgument( + SeparatedSyntaxList arguments, + string searchArgumentName) + { + foreach (var argument in arguments.Cast()) { - foreach (var argument in arguments.Cast()) + if (argument.NameColon != null && argument.NameColon.Name.Identifier.ValueText.Equals(searchArgumentName)) { - if (argument.NameColon != null && argument.NameColon.Name.Identifier.ValueText.Equals(searchArgumentName)) - { - return argument; - } + return argument; } - - return null; } - protected override SyntaxNode GetArgumentExpression(SyntaxNode syntaxNode) - => ((ArgumentSyntax)syntaxNode).Expression; + return null; } + + protected override SyntaxNode GetArgumentExpression(SyntaxNode syntaxNode) + => ((ArgumentSyntax)syntaxNode).Expression; } diff --git a/src/Analyzers/CSharp/CodeFixes/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersCodeFixProvider.cs index ec8d1021aef3e..539d47eb743e1 100644 --- a/src/Analyzers/CSharp/CodeFixes/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersCodeFixProvider.cs @@ -10,30 +10,29 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.AddAccessibilityModifiers +namespace Microsoft.CodeAnalysis.CSharp.AddAccessibilityModifiers; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddAccessibilityModifiers), Shared] +internal class CSharpAddAccessibilityModifiersCodeFixProvider : AbstractAddAccessibilityModifiersCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddAccessibilityModifiers), Shared] - internal class CSharpAddAccessibilityModifiersCodeFixProvider : AbstractAddAccessibilityModifiersCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddAccessibilityModifiersCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddAccessibilityModifiersCodeFixProvider() - { - } + } - protected override SyntaxNode MapToDeclarator(SyntaxNode node) + protected override SyntaxNode MapToDeclarator(SyntaxNode node) + { + switch (node) { - switch (node) - { - case FieldDeclarationSyntax field: - return field.Declaration.Variables[0]; + case FieldDeclarationSyntax field: + return field.Declaration.Variables[0]; - case EventFieldDeclarationSyntax eventField: - return eventField.Declaration.Variables[0]; + case EventFieldDeclarationSyntax eventField: + return eventField.Declaration.Variables[0]; - default: - return node; - } + default: + return node; } } } diff --git a/src/Analyzers/CSharp/CodeFixes/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersService.cs b/src/Analyzers/CSharp/CodeFixes/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersService.cs index 65fd2072bd996..265d06d8b08c1 100644 --- a/src/Analyzers/CSharp/CodeFixes/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersService.cs +++ b/src/Analyzers/CSharp/CodeFixes/AddAccessibilityModifiers/CSharpAddAccessibilityModifiersService.cs @@ -7,15 +7,14 @@ using Microsoft.CodeAnalysis.AddAccessibilityModifiers; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.AddAccessibilityModifiers +namespace Microsoft.CodeAnalysis.CSharp.AddAccessibilityModifiers; + +[ExportLanguageService(typeof(IAddAccessibilityModifiersService), LanguageNames.CSharp), Shared] +internal class CSharpAddAccessibilityModifiersService : CSharpAddAccessibilityModifiers, IAddAccessibilityModifiersService { - [ExportLanguageService(typeof(IAddAccessibilityModifiersService), LanguageNames.CSharp), Shared] - internal class CSharpAddAccessibilityModifiersService : CSharpAddAccessibilityModifiers, IAddAccessibilityModifiersService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpAddAccessibilityModifiersService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpAddAccessibilityModifiersService() - { - } } } diff --git a/src/Analyzers/CSharp/CodeFixes/AddAnonymousTypeMemberName/CSharpAddAnonymousTypeMemberNameCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/AddAnonymousTypeMemberName/CSharpAddAnonymousTypeMemberNameCodeFixProvider.cs index 256a36e928309..d5bf25d66e50a 100644 --- a/src/Analyzers/CSharp/CodeFixes/AddAnonymousTypeMemberName/CSharpAddAnonymousTypeMemberNameCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/AddAnonymousTypeMemberName/CSharpAddAnonymousTypeMemberNameCodeFixProvider.cs @@ -11,38 +11,37 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.AddAnonymousTypeMemberName +namespace Microsoft.CodeAnalysis.CSharp.AddAnonymousTypeMemberName; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddAnonymousTypeMemberName), Shared] +internal class CSharpAddAnonymousTypeMemberNameCodeFixProvider + : AbstractAddAnonymousTypeMemberNameCodeFixProvider< + ExpressionSyntax, + AnonymousObjectCreationExpressionSyntax, + AnonymousObjectMemberDeclaratorSyntax> { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddAnonymousTypeMemberName), Shared] - internal class CSharpAddAnonymousTypeMemberNameCodeFixProvider - : AbstractAddAnonymousTypeMemberNameCodeFixProvider< - ExpressionSyntax, - AnonymousObjectCreationExpressionSyntax, - AnonymousObjectMemberDeclaratorSyntax> - { - private const string CS0746 = nameof(CS0746); // Invalid anonymous type member declarator. Anonymous type members must be declared with a member assignment, simple name or member access. + private const string CS0746 = nameof(CS0746); // Invalid anonymous type member declarator. Anonymous type members must be declared with a member assignment, simple name or member access. - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddAnonymousTypeMemberNameCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddAnonymousTypeMemberNameCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds { get; } - = [CS0746]; + public override ImmutableArray FixableDiagnosticIds { get; } + = [CS0746]; - protected override bool HasName(AnonymousObjectMemberDeclaratorSyntax declarator) - => declarator.NameEquals != null; + protected override bool HasName(AnonymousObjectMemberDeclaratorSyntax declarator) + => declarator.NameEquals != null; - protected override ExpressionSyntax GetExpression(AnonymousObjectMemberDeclaratorSyntax declarator) - => declarator.Expression; + protected override ExpressionSyntax GetExpression(AnonymousObjectMemberDeclaratorSyntax declarator) + => declarator.Expression; - protected override AnonymousObjectMemberDeclaratorSyntax WithName(AnonymousObjectMemberDeclaratorSyntax declarator, SyntaxToken name) - => declarator.WithNameEquals( - SyntaxFactory.NameEquals( - SyntaxFactory.IdentifierName(name))); + protected override AnonymousObjectMemberDeclaratorSyntax WithName(AnonymousObjectMemberDeclaratorSyntax declarator, SyntaxToken name) + => declarator.WithNameEquals( + SyntaxFactory.NameEquals( + SyntaxFactory.IdentifierName(name))); - protected override IEnumerable GetAnonymousObjectMemberNames(AnonymousObjectCreationExpressionSyntax initializer) - => initializer.Initializers.Where(i => i.NameEquals != null).Select(i => i.NameEquals!.Name.Identifier.ValueText); - } + protected override IEnumerable GetAnonymousObjectMemberNames(AnonymousObjectCreationExpressionSyntax initializer) + => initializer.Initializers.Where(i => i.NameEquals != null).Select(i => i.NameEquals!.Name.Identifier.ValueText); } diff --git a/src/Analyzers/CSharp/CodeFixes/AddBraces/CSharpAddBracesCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/AddBraces/CSharpAddBracesCodeFixProvider.cs index ccc672cefc8e9..0d7122a551717 100644 --- a/src/Analyzers/CSharp/CodeFixes/AddBraces/CSharpAddBracesCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/AddBraces/CSharpAddBracesCodeFixProvider.cs @@ -13,47 +13,46 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editing; -namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.AddBraces +namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.AddBraces; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddBraces), Shared] +internal sealed class CSharpAddBracesCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddBraces), Shared] - internal sealed class CSharpAddBracesCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddBracesCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddBracesCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.AddBracesDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.AddBracesDiagnosticId]; - public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Add_braces, nameof(CSharpAnalyzersResources.Add_braces)); - return Task.CompletedTask; - } + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Add_braces, nameof(CSharpAnalyzersResources.Add_braces)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var root = editor.OriginalRoot; + foreach (var diagnostic in diagnostics) { - var root = editor.OriginalRoot; - foreach (var diagnostic in diagnostics) + var statement = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + + // Use the callback version of ReplaceNode so that we see the effects + // of other replace calls. i.e. we may have statements nested in statements, + // we need to make sure that any inner edits are seen when we make the outer + // replacement. + editor.ReplaceNode(statement, (currentStatement, g) => { - var statement = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); - - // Use the callback version of ReplaceNode so that we see the effects - // of other replace calls. i.e. we may have statements nested in statements, - // we need to make sure that any inner edits are seen when we make the outer - // replacement. - editor.ReplaceNode(statement, (currentStatement, g) => - { - var embeddedStatement = currentStatement.GetEmbeddedStatement(); - return embeddedStatement is null ? currentStatement : currentStatement.ReplaceNode(embeddedStatement, SyntaxFactory.Block(embeddedStatement)); - }); - } - - return Task.CompletedTask; + var embeddedStatement = currentStatement.GetEmbeddedStatement(); + return embeddedStatement is null ? currentStatement : currentStatement.ReplaceNode(embeddedStatement, SyntaxFactory.Block(embeddedStatement)); + }); } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/ArgumentFixer.cs b/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/ArgumentFixer.cs index 6bfa6a612e6cc..eb797b789e497 100644 --- a/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/ArgumentFixer.cs +++ b/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/ArgumentFixer.cs @@ -8,29 +8,28 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.AddExplicitCast +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.AddExplicitCast; + +internal sealed partial class CSharpAddExplicitCastCodeFixProvider { - internal sealed partial class CSharpAddExplicitCastCodeFixProvider + private class ArgumentFixer : Fixer { - private class ArgumentFixer : Fixer - { - protected override ExpressionSyntax GetExpressionOfArgument(ArgumentSyntax argument) - => argument.Expression; + protected override ExpressionSyntax GetExpressionOfArgument(ArgumentSyntax argument) + => argument.Expression; - protected override ArgumentSyntax GenerateNewArgument(ArgumentSyntax oldArgument, ITypeSymbol conversionType) - => oldArgument.WithExpression(oldArgument.Expression.Cast(conversionType)); + protected override ArgumentSyntax GenerateNewArgument(ArgumentSyntax oldArgument, ITypeSymbol conversionType) + => oldArgument.WithExpression(oldArgument.Expression.Cast(conversionType)); - protected override ArgumentListSyntax GenerateNewArgumentList(ArgumentListSyntax oldArgumentList, ArrayBuilder newArguments) - => oldArgumentList.WithArguments([.. newArguments]); + protected override ArgumentListSyntax GenerateNewArgumentList(ArgumentListSyntax oldArgumentList, ArrayBuilder newArguments) + => oldArgumentList.WithArguments([.. newArguments]); - protected override SeparatedSyntaxList GetArgumentsOfArgumentList(ArgumentListSyntax argumentList) - => argumentList.Arguments; + protected override SeparatedSyntaxList GetArgumentsOfArgumentList(ArgumentListSyntax argumentList) + => argumentList.Arguments; - protected override SymbolInfo GetSpeculativeSymbolInfo(SemanticModel semanticModel, ArgumentListSyntax newArgumentList) - { - var newInvocation = newArgumentList.Parent!; - return semanticModel.GetSpeculativeSymbolInfo(newInvocation.SpanStart, newInvocation, SpeculativeBindingOption.BindAsExpression); - } + protected override SymbolInfo GetSpeculativeSymbolInfo(SemanticModel semanticModel, ArgumentListSyntax newArgumentList) + { + var newInvocation = newArgumentList.Parent!; + return semanticModel.GetSpeculativeSymbolInfo(newInvocation.SpanStart, newInvocation, SpeculativeBindingOption.BindAsExpression); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/AttributeArgumentFixer.cs b/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/AttributeArgumentFixer.cs index 8a2a1a97aa73a..7c5161643cc42 100644 --- a/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/AttributeArgumentFixer.cs +++ b/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/AttributeArgumentFixer.cs @@ -8,29 +8,28 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.AddExplicitCast +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.AddExplicitCast; + +internal sealed partial class CSharpAddExplicitCastCodeFixProvider { - internal sealed partial class CSharpAddExplicitCastCodeFixProvider + private class AttributeArgumentFixer : Fixer { - private class AttributeArgumentFixer : Fixer - { - protected override ExpressionSyntax GetExpressionOfArgument(AttributeArgumentSyntax argument) - => argument.Expression; + protected override ExpressionSyntax GetExpressionOfArgument(AttributeArgumentSyntax argument) + => argument.Expression; - protected override AttributeArgumentSyntax GenerateNewArgument(AttributeArgumentSyntax oldArgument, ITypeSymbol conversionType) - => oldArgument.WithExpression(oldArgument.Expression.Cast(conversionType)); + protected override AttributeArgumentSyntax GenerateNewArgument(AttributeArgumentSyntax oldArgument, ITypeSymbol conversionType) + => oldArgument.WithExpression(oldArgument.Expression.Cast(conversionType)); - protected override AttributeArgumentListSyntax GenerateNewArgumentList(AttributeArgumentListSyntax oldArgumentList, ArrayBuilder newArguments) - => oldArgumentList.WithArguments([.. newArguments]); + protected override AttributeArgumentListSyntax GenerateNewArgumentList(AttributeArgumentListSyntax oldArgumentList, ArrayBuilder newArguments) + => oldArgumentList.WithArguments([.. newArguments]); - protected override SeparatedSyntaxList GetArgumentsOfArgumentList(AttributeArgumentListSyntax argumentList) - => argumentList.Arguments; + protected override SeparatedSyntaxList GetArgumentsOfArgumentList(AttributeArgumentListSyntax argumentList) + => argumentList.Arguments; - protected override SymbolInfo GetSpeculativeSymbolInfo(SemanticModel semanticModel, AttributeArgumentListSyntax newArgumentList) - { - var newAttribute = (AttributeSyntax)newArgumentList.Parent!; - return semanticModel.GetSpeculativeSymbolInfo(newAttribute.SpanStart, newAttribute); - } + protected override SymbolInfo GetSpeculativeSymbolInfo(SemanticModel semanticModel, AttributeArgumentListSyntax newArgumentList) + { + var newAttribute = (AttributeSyntax)newArgumentList.Parent!; + return semanticModel.GetSpeculativeSymbolInfo(newAttribute.SpanStart, newAttribute); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/CSharpAddExplicitCastCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/CSharpAddExplicitCastCodeFixProvider.cs index 8e61544a8698e..74e82c814d2a6 100644 --- a/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/CSharpAddExplicitCastCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/AddExplicitCast/CSharpAddExplicitCastCodeFixProvider.cs @@ -14,89 +14,88 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.AddExplicitCast +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.AddExplicitCast; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddExplicitCast), Shared] +internal sealed partial class CSharpAddExplicitCastCodeFixProvider + : AbstractAddExplicitCastCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddExplicitCast), Shared] - internal sealed partial class CSharpAddExplicitCastCodeFixProvider - : AbstractAddExplicitCastCodeFixProvider - { - /// - /// CS0266: Cannot implicitly convert from type 'x' to 'y'. An explicit conversion exists (are you missing a cast?) - /// - private const string CS0266 = nameof(CS0266); + /// + /// CS0266: Cannot implicitly convert from type 'x' to 'y'. An explicit conversion exists (are you missing a cast?) + /// + private const string CS0266 = nameof(CS0266); - /// - /// CS1503: Argument 1: cannot convert from 'x' to 'y' - /// - private const string CS1503 = nameof(CS1503); + /// + /// CS1503: Argument 1: cannot convert from 'x' to 'y' + /// + private const string CS1503 = nameof(CS1503); - private readonly ArgumentFixer _argumentFixer; - private readonly AttributeArgumentFixer _attributeArgumentFixer; + private readonly ArgumentFixer _argumentFixer; + private readonly AttributeArgumentFixer _attributeArgumentFixer; - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddExplicitCastCodeFixProvider() - { - _argumentFixer = new ArgumentFixer(); - _attributeArgumentFixer = new AttributeArgumentFixer(); - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddExplicitCastCodeFixProvider() + { + _argumentFixer = new ArgumentFixer(); + _attributeArgumentFixer = new AttributeArgumentFixer(); + } - public override ImmutableArray FixableDiagnosticIds => [CS0266, CS1503]; + public override ImmutableArray FixableDiagnosticIds => [CS0266, CS1503]; - protected override void GetPartsOfCastOrConversionExpression(ExpressionSyntax expression, out SyntaxNode type, out SyntaxNode castedExpression) - { - var castExpression = (CastExpressionSyntax)expression; - type = castExpression.Type; - castedExpression = castExpression.Expression; - } + protected override void GetPartsOfCastOrConversionExpression(ExpressionSyntax expression, out SyntaxNode type, out SyntaxNode castedExpression) + { + var castExpression = (CastExpressionSyntax)expression; + type = castExpression.Type; + castedExpression = castExpression.Expression; + } + + protected override ExpressionSyntax Cast(ExpressionSyntax expression, ITypeSymbol type) + => expression.Cast(type); - protected override ExpressionSyntax Cast(ExpressionSyntax expression, ITypeSymbol type) - => expression.Cast(type); + protected override bool TryGetTargetTypeInfo( + Document document, + SemanticModel semanticModel, + SyntaxNode root, + string diagnosticId, + ExpressionSyntax spanNode, + CancellationToken cancellationToken, + out ImmutableArray<(ExpressionSyntax, ITypeSymbol)> potentialConversionTypes) + { + potentialConversionTypes = []; + using var _ = ArrayBuilder<(ExpressionSyntax, ITypeSymbol)>.GetInstance(out var mutablePotentialConversionTypes); - protected override bool TryGetTargetTypeInfo( - Document document, - SemanticModel semanticModel, - SyntaxNode root, - string diagnosticId, - ExpressionSyntax spanNode, - CancellationToken cancellationToken, - out ImmutableArray<(ExpressionSyntax, ITypeSymbol)> potentialConversionTypes) + if (diagnosticId == CS0266) { - potentialConversionTypes = []; - using var _ = ArrayBuilder<(ExpressionSyntax, ITypeSymbol)>.GetInstance(out var mutablePotentialConversionTypes); + var inferenceService = document.GetRequiredLanguageService(); + var conversionType = inferenceService.InferType(semanticModel, spanNode, objectAsDefault: false, cancellationToken); + if (conversionType is null) + return false; - if (diagnosticId == CS0266) + mutablePotentialConversionTypes.Add((spanNode, conversionType)); + } + else if (diagnosticId == CS1503) + { + if (spanNode.GetAncestorOrThis() is ArgumentSyntax targetArgument + && targetArgument.Parent is ArgumentListSyntax argumentList + && argumentList.Parent is SyntaxNode invocationNode) { - var inferenceService = document.GetRequiredLanguageService(); - var conversionType = inferenceService.InferType(semanticModel, spanNode, objectAsDefault: false, cancellationToken); - if (conversionType is null) - return false; - - mutablePotentialConversionTypes.Add((spanNode, conversionType)); + // invocationNode could be Invocation Expression, Object Creation, Base Constructor...) + mutablePotentialConversionTypes.AddRange(_argumentFixer.GetPotentialConversionTypes( + document, semanticModel, root, targetArgument, argumentList, invocationNode, cancellationToken)); } - else if (diagnosticId == CS1503) + else if (spanNode.GetAncestorOrThis() is AttributeArgumentSyntax targetAttributeArgument + && targetAttributeArgument.Parent is AttributeArgumentListSyntax attributeArgumentList + && attributeArgumentList.Parent is AttributeSyntax attributeNode) { - if (spanNode.GetAncestorOrThis() is ArgumentSyntax targetArgument - && targetArgument.Parent is ArgumentListSyntax argumentList - && argumentList.Parent is SyntaxNode invocationNode) - { - // invocationNode could be Invocation Expression, Object Creation, Base Constructor...) - mutablePotentialConversionTypes.AddRange(_argumentFixer.GetPotentialConversionTypes( - document, semanticModel, root, targetArgument, argumentList, invocationNode, cancellationToken)); - } - else if (spanNode.GetAncestorOrThis() is AttributeArgumentSyntax targetAttributeArgument - && targetAttributeArgument.Parent is AttributeArgumentListSyntax attributeArgumentList - && attributeArgumentList.Parent is AttributeSyntax attributeNode) - { - // attribute node - mutablePotentialConversionTypes.AddRange(_attributeArgumentFixer.GetPotentialConversionTypes( - document, semanticModel, root, targetAttributeArgument, attributeArgumentList, attributeNode, cancellationToken)); - } + // attribute node + mutablePotentialConversionTypes.AddRange(_attributeArgumentFixer.GetPotentialConversionTypes( + document, semanticModel, root, targetAttributeArgument, attributeArgumentList, attributeNode, cancellationToken)); } - - // clear up duplicate types - potentialConversionTypes = FilterValidPotentialConversionTypes(document, semanticModel, mutablePotentialConversionTypes); - return !potentialConversionTypes.IsEmpty; } + + // clear up duplicate types + potentialConversionTypes = FilterValidPotentialConversionTypes(document, semanticModel, mutablePotentialConversionTypes); + return !potentialConversionTypes.IsEmpty; } } diff --git a/src/Analyzers/CSharp/CodeFixes/AddInheritdoc/AddInheritdocCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/AddInheritdoc/AddInheritdocCodeFixProvider.cs index 3a5f1986c49f5..8ecb0f5a16653 100644 --- a/src/Analyzers/CSharp/CodeFixes/AddInheritdoc/AddInheritdocCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/AddInheritdoc/AddInheritdocCodeFixProvider.cs @@ -25,111 +25,110 @@ using Microsoft.CodeAnalysis.Text; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.AddInheritdoc +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.AddInheritdoc; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddInheritdoc), Shared] +internal sealed class AddInheritdocCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddInheritdoc), Shared] - internal sealed class AddInheritdocCodeFixProvider : SyntaxEditorBasedCodeFixProvider + /// + /// CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' + /// + private const string CS1591 = nameof(CS1591); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public AddInheritdocCodeFixProvider() { - /// - /// CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' - /// - private const string CS1591 = nameof(CS1591); - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public AddInheritdocCodeFixProvider() + } + + public override ImmutableArray FixableDiagnosticIds => [CS1591]; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var cancellationToken = context.CancellationToken; + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) { + return; } - public override ImmutableArray FixableDiagnosticIds => [CS1591]; - - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + SemanticModel? semanticModel = null; + foreach (var diagnostic in context.Diagnostics) { - var document = context.Document; - var cancellationToken = context.CancellationToken; - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (root is null) + var node = root.FindNode(diagnostic.Location.SourceSpan); + if (node.Kind() is not SyntaxKind.MethodDeclaration and not SyntaxKind.PropertyDeclaration and not SyntaxKind.VariableDeclarator) { - return; + continue; } - SemanticModel? semanticModel = null; - foreach (var diagnostic in context.Diagnostics) + if (node.IsKind(SyntaxKind.VariableDeclarator) && node.Parent?.Parent?.IsKind(SyntaxKind.EventFieldDeclaration) == false) { - var node = root.FindNode(diagnostic.Location.SourceSpan); - if (node.Kind() is not SyntaxKind.MethodDeclaration and not SyntaxKind.PropertyDeclaration and not SyntaxKind.VariableDeclarator) - { - continue; - } - - if (node.IsKind(SyntaxKind.VariableDeclarator) && node.Parent?.Parent?.IsKind(SyntaxKind.EventFieldDeclaration) == false) - { - continue; - } + continue; + } - semanticModel ??= await context.Document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + semanticModel ??= await context.Document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken); - if (symbol is null) - { - continue; - } + var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken); + if (symbol is null) + { + continue; + } - if (symbol.Kind is SymbolKind.Method or SymbolKind.Property or SymbolKind.Event) + if (symbol.Kind is SymbolKind.Method or SymbolKind.Property or SymbolKind.Event) + { + if (symbol.IsOverride || + symbol.ImplicitInterfaceImplementations().Any()) { - if (symbol.IsOverride || - symbol.ImplicitInterfaceImplementations().Any()) - { - RegisterCodeFix(context, CSharpCodeFixesResources.Explicitly_inherit_documentation, nameof(CSharpCodeFixesResources.Explicitly_inherit_documentation), diagnostic); - } + RegisterCodeFix(context, CSharpCodeFixesResources.Explicitly_inherit_documentation, nameof(CSharpCodeFixesResources.Explicitly_inherit_documentation), diagnostic); } } } + } - protected override async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + string? newLine = null; + SourceText? sourceText = null; + foreach (var diagnostic in diagnostics) { - string? newLine = null; - SourceText? sourceText = null; - foreach (var diagnostic in diagnostics) + var node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan); + if (node.IsKind(SyntaxKind.VariableDeclarator) && !(node = node.Parent?.Parent).IsKind(SyntaxKind.EventFieldDeclaration)) { - var node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan); - if (node.IsKind(SyntaxKind.VariableDeclarator) && !(node = node.Parent?.Parent).IsKind(SyntaxKind.EventFieldDeclaration)) - { - continue; - } - - if (newLine == null) - { - var optionsProvider = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - newLine = optionsProvider.GetLineFormattingOptions().NewLine; - } + continue; + } - // We can safely assume, that there is no leading doc comment, because that is what CS1591 is telling us. - // So we create a new /// comment. - var xmlSpaceAfterTripleSlash = Token(leading: [DocumentationCommentExterior("///")], SyntaxKind.XmlTextLiteralToken, text: " ", valueText: " ", trailing: default); - var lessThanToken = Token(SyntaxKind.LessThanToken).WithoutTrivia(); - var inheritdocTagName = XmlName("inheritdoc").WithoutTrivia(); - var slashGreaterThanToken = Token(SyntaxKind.SlashGreaterThanToken).WithoutTrivia(); - var xmlNewLineToken = Token(leading: default, SyntaxKind.XmlTextLiteralNewLineToken, text: newLine, valueText: newLine, trailing: default); - - var singleLineInheritdocComment = DocumentationCommentTrivia( - kind: SyntaxKind.SingleLineDocumentationCommentTrivia, - content: - [ - XmlText(xmlSpaceAfterTripleSlash), - XmlEmptyElement(lessThanToken, inheritdocTagName, attributes: default, slashGreaterThanToken), - XmlText(xmlNewLineToken), - ], - endOfComment: Token(SyntaxKind.EndOfDocumentationCommentToken).WithoutTrivia()); - - sourceText ??= await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var indentation = sourceText.GetLeadingWhitespaceOfLineAtPosition(node.FullSpan.Start); - var newLeadingTrivia = TriviaList( - Whitespace(indentation), - Trivia(singleLineInheritdocComment)); - - editor.ReplaceNode(node, node.WithPrependedLeadingTrivia(newLeadingTrivia)); + if (newLine == null) + { + var optionsProvider = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + newLine = optionsProvider.GetLineFormattingOptions().NewLine; } + + // We can safely assume, that there is no leading doc comment, because that is what CS1591 is telling us. + // So we create a new /// comment. + var xmlSpaceAfterTripleSlash = Token(leading: [DocumentationCommentExterior("///")], SyntaxKind.XmlTextLiteralToken, text: " ", valueText: " ", trailing: default); + var lessThanToken = Token(SyntaxKind.LessThanToken).WithoutTrivia(); + var inheritdocTagName = XmlName("inheritdoc").WithoutTrivia(); + var slashGreaterThanToken = Token(SyntaxKind.SlashGreaterThanToken).WithoutTrivia(); + var xmlNewLineToken = Token(leading: default, SyntaxKind.XmlTextLiteralNewLineToken, text: newLine, valueText: newLine, trailing: default); + + var singleLineInheritdocComment = DocumentationCommentTrivia( + kind: SyntaxKind.SingleLineDocumentationCommentTrivia, + content: + [ + XmlText(xmlSpaceAfterTripleSlash), + XmlEmptyElement(lessThanToken, inheritdocTagName, attributes: default, slashGreaterThanToken), + XmlText(xmlNewLineToken), + ], + endOfComment: Token(SyntaxKind.EndOfDocumentationCommentToken).WithoutTrivia()); + + sourceText ??= await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var indentation = sourceText.GetLeadingWhitespaceOfLineAtPosition(node.FullSpan.Start); + var newLeadingTrivia = TriviaList( + Whitespace(indentation), + Trivia(singleLineInheritdocComment)); + + editor.ReplaceNode(node, node.WithPrependedLeadingTrivia(newLeadingTrivia)); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/AddObsoleteAttribute/CSharpAddObsoleteAttributeCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/AddObsoleteAttribute/CSharpAddObsoleteAttributeCodeFixProvider.cs index aa4d7ac5ab382..da69e0b653a9e 100644 --- a/src/Analyzers/CSharp/CodeFixes/AddObsoleteAttribute/CSharpAddObsoleteAttributeCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/AddObsoleteAttribute/CSharpAddObsoleteAttributeCodeFixProvider.cs @@ -10,27 +10,26 @@ using Microsoft.CodeAnalysis.CSharp.LanguageService; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.AddObsoleteAttribute +namespace Microsoft.CodeAnalysis.CSharp.AddObsoleteAttribute; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddObsoleteAttribute), Shared] +internal class CSharpAddObsoleteAttributeCodeFixProvider + : AbstractAddObsoleteAttributeCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddObsoleteAttribute), Shared] - internal class CSharpAddObsoleteAttributeCodeFixProvider - : AbstractAddObsoleteAttributeCodeFixProvider - { - public override ImmutableArray FixableDiagnosticIds { get; } = - [ - "CS0612", - "CS0618", - "CS0672", - "CS1062", - "CS1064" + public override ImmutableArray FixableDiagnosticIds { get; } = + [ + "CS0612", + "CS0618", + "CS0672", + "CS1062", + "CS1064" , // The best overloaded Add method 'MyCollection.Add(int)' for the collection initializer element is obsolete" - ]; + ]; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpAddObsoleteAttributeCodeFixProvider() - : base(CSharpSyntaxFacts.Instance, CSharpCodeFixesResources.Add_Obsolete) - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpAddObsoleteAttributeCodeFixProvider() + : base(CSharpSyntaxFacts.Instance, CSharpCodeFixesResources.Add_Obsolete) + { } } diff --git a/src/Analyzers/CSharp/CodeFixes/AddParameter/CSharpAddParameterCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/AddParameter/CSharpAddParameterCodeFixProvider.cs index 42d0af8f5c9bc..339b59bf7270f 100644 --- a/src/Analyzers/CSharp/CodeFixes/AddParameter/CSharpAddParameterCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/AddParameter/CSharpAddParameterCodeFixProvider.cs @@ -14,71 +14,70 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.AddParameter +namespace Microsoft.CodeAnalysis.CSharp.AddParameter; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddParameter), Shared] +[ExtensionOrder(Before = PredefinedCodeFixProviderNames.GenerateConstructor)] +internal class CSharpAddParameterCodeFixProvider : AbstractAddParameterCodeFixProvider< + ArgumentSyntax, + AttributeArgumentSyntax, + ArgumentListSyntax, + AttributeArgumentListSyntax, + InvocationExpressionSyntax, + BaseObjectCreationExpressionSyntax> { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddParameter), Shared] - [ExtensionOrder(Before = PredefinedCodeFixProviderNames.GenerateConstructor)] - internal class CSharpAddParameterCodeFixProvider : AbstractAddParameterCodeFixProvider< - ArgumentSyntax, - AttributeArgumentSyntax, - ArgumentListSyntax, - AttributeArgumentListSyntax, - InvocationExpressionSyntax, - BaseObjectCreationExpressionSyntax> - { - private const string CS1501 = nameof(CS1501); // error CS1501: No overload for method 'M' takes 1 arguments - private const string CS1503 = nameof(CS1503); // error CS1503: Argument 1: cannot convert from 'double' to 'int' - private const string CS1660 = nameof(CS1660); // error CS1660: Cannot convert lambda expression to type 'string[]' because it is not a delegate type - private const string CS1729 = nameof(CS1729); // error CS1729: 'C' does not contain a constructor that takes n arguments - private const string CS1739 = nameof(CS1739); // error CS1739: The best overload for 'M' does not have a parameter named 'x' + private const string CS1501 = nameof(CS1501); // error CS1501: No overload for method 'M' takes 1 arguments + private const string CS1503 = nameof(CS1503); // error CS1503: Argument 1: cannot convert from 'double' to 'int' + private const string CS1660 = nameof(CS1660); // error CS1660: Cannot convert lambda expression to type 'string[]' because it is not a delegate type + private const string CS1729 = nameof(CS1729); // error CS1729: 'C' does not contain a constructor that takes n arguments + private const string CS1739 = nameof(CS1739); // error CS1739: The best overload for 'M' does not have a parameter named 'x' - private static readonly ImmutableArray AddParameterFixableDiagnosticIds = [CS1501, CS1503, CS1660, CS1729, CS1739]; + private static readonly ImmutableArray AddParameterFixableDiagnosticIds = [CS1501, CS1503, CS1660, CS1729, CS1739]; - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddParameterCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddParameterCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds - => AddParameterFixableDiagnosticIds; + public override ImmutableArray FixableDiagnosticIds + => AddParameterFixableDiagnosticIds; - protected override ImmutableArray TooManyArgumentsDiagnosticIds - => GenerateConstructorDiagnosticIds.TooManyArgumentsDiagnosticIds; + protected override ImmutableArray TooManyArgumentsDiagnosticIds + => GenerateConstructorDiagnosticIds.TooManyArgumentsDiagnosticIds; - protected override ImmutableArray CannotConvertDiagnosticIds - => GenerateConstructorDiagnosticIds.CannotConvertDiagnosticIds; + protected override ImmutableArray CannotConvertDiagnosticIds + => GenerateConstructorDiagnosticIds.CannotConvertDiagnosticIds; - protected override ITypeSymbol GetArgumentType(SyntaxNode argumentNode, SemanticModel semanticModel, CancellationToken cancellationToken) - => ((ArgumentSyntax)argumentNode).DetermineParameterType(semanticModel, cancellationToken); + protected override ITypeSymbol GetArgumentType(SyntaxNode argumentNode, SemanticModel semanticModel, CancellationToken cancellationToken) + => ((ArgumentSyntax)argumentNode).DetermineParameterType(semanticModel, cancellationToken); - protected override RegisterFixData TryGetLanguageSpecificFixInfo( - SemanticModel semanticModel, - SyntaxNode node, - CancellationToken cancellationToken) + protected override RegisterFixData TryGetLanguageSpecificFixInfo( + SemanticModel semanticModel, + SyntaxNode node, + CancellationToken cancellationToken) + { + if (node is ConstructorInitializerSyntax constructorInitializer) { - if (node is ConstructorInitializerSyntax constructorInitializer) + var constructorDeclaration = constructorInitializer.Parent; + if (semanticModel.GetDeclaredSymbol(constructorDeclaration, cancellationToken) is IMethodSymbol constructorSymbol) { - var constructorDeclaration = constructorInitializer.Parent; - if (semanticModel.GetDeclaredSymbol(constructorDeclaration, cancellationToken) is IMethodSymbol constructorSymbol) + var type = constructorSymbol.ContainingType; + if (constructorInitializer.IsKind(SyntaxKind.BaseConstructorInitializer)) { - var type = constructorSymbol.ContainingType; - if (constructorInitializer.IsKind(SyntaxKind.BaseConstructorInitializer)) - { - // Search for fixable constructors in the base class. - type = type?.BaseType; - } + // Search for fixable constructors in the base class. + type = type?.BaseType; + } - if (type != null && type.IsFromSource()) - { - var methodCandidates = type.InstanceConstructors; - var arguments = constructorInitializer.ArgumentList.Arguments; - return new RegisterFixData(arguments, methodCandidates, isConstructorInitializer: true); - } + if (type != null && type.IsFromSource()) + { + var methodCandidates = type.InstanceConstructors; + var arguments = constructorInitializer.ArgumentList.Arguments; + return new RegisterFixData(arguments, methodCandidates, isConstructorInitializer: true); } } - - return null; } + + return null; } } diff --git a/src/Analyzers/CSharp/CodeFixes/AliasAmbiguousType/CSharpAliasAmbiguousTypeCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/AliasAmbiguousType/CSharpAliasAmbiguousTypeCodeFixProvider.cs index b5c4c8b6c5ab5..80d6a95e75451 100644 --- a/src/Analyzers/CSharp/CodeFixes/AliasAmbiguousType/CSharpAliasAmbiguousTypeCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/AliasAmbiguousType/CSharpAliasAmbiguousTypeCodeFixProvider.cs @@ -9,27 +9,26 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.AliasAmbiguousType +namespace Microsoft.CodeAnalysis.CSharp.AliasAmbiguousType; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AliasAmbiguousType), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.FullyQualify)] +internal class CSharpAliasAmbiguousTypeCodeFixProvider : AbstractAliasAmbiguousTypeCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AliasAmbiguousType), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.FullyQualify)] - internal class CSharpAliasAmbiguousTypeCodeFixProvider : AbstractAliasAmbiguousTypeCodeFixProvider - { - /// - /// 'reference' is an ambiguous reference between 'identifier' and 'identifier' - /// - private const string CS0104 = nameof(CS0104); + /// + /// 'reference' is an ambiguous reference between 'identifier' and 'identifier' + /// + private const string CS0104 = nameof(CS0104); - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAliasAmbiguousTypeCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAliasAmbiguousTypeCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds - => [CS0104]; + public override ImmutableArray FixableDiagnosticIds + => [CS0104]; - protected override string GetTextPreviewOfChange(string alias, ITypeSymbol typeSymbol) - => $"using {alias} = {typeSymbol.ToNameDisplayString()};"; - } + protected override string GetTextPreviewOfChange(string alias, ITypeSymbol typeSymbol) + => $"using {alias} = {typeSymbol.ToNameDisplayString()};"; } diff --git a/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AbstractAssignOutParametersCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AbstractAssignOutParametersCodeFixProvider.cs index 355bbf84c049b..85ae6f8ccefe8 100644 --- a/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AbstractAssignOutParametersCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AbstractAssignOutParametersCodeFixProvider.cs @@ -17,153 +17,152 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.AssignOutParameters +namespace Microsoft.CodeAnalysis.CSharp.AssignOutParameters; + +internal abstract class AbstractAssignOutParametersCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - internal abstract class AbstractAssignOutParametersCodeFixProvider : SyntaxEditorBasedCodeFixProvider - { - private const string CS0177 = nameof(CS0177); // The out parameter 'x' must be assigned to before control leaves the current method + private const string CS0177 = nameof(CS0177); // The out parameter 'x' must be assigned to before control leaves the current method - public sealed override ImmutableArray FixableDiagnosticIds { get; } = - [CS0177]; + public sealed override ImmutableArray FixableDiagnosticIds { get; } = + [CS0177]; - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var cancellationToken = context.CancellationToken; - var document = context.Document; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var cancellationToken = context.CancellationToken; + var document = context.Document; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var (container, location) = GetContainer(root, context.Span); - if (container != null) + var (container, location) = GetContainer(root, context.Span); + if (container != null) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var dataFlow = semanticModel.AnalyzeDataFlow(location); + if (dataFlow.Succeeded) { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var dataFlow = semanticModel.AnalyzeDataFlow(location); - if (dataFlow.Succeeded) - { - TryRegisterFix(context, document, container, location); - } + TryRegisterFix(context, document, container, location); } } + } - protected abstract void TryRegisterFix(CodeFixContext context, Document document, SyntaxNode container, SyntaxNode location); + protected abstract void TryRegisterFix(CodeFixContext context, Document document, SyntaxNode container, SyntaxNode location); - private static (SyntaxNode container, SyntaxNode exprOrStatement) GetContainer(SyntaxNode root, TextSpan span) + private static (SyntaxNode container, SyntaxNode exprOrStatement) GetContainer(SyntaxNode root, TextSpan span) + { + var location = root.FindNode(span); + if (IsValidLocation(location)) { - var location = root.FindNode(span); - if (IsValidLocation(location)) + var container = GetContainer(location); + if (container != null) { - var container = GetContainer(location); - if (container != null) - { - return (container, location); - } + return (container, location); } - - return default; } - private static bool IsValidLocation(SyntaxNode location) - { - if (location is StatementSyntax) - { - return location.Parent is BlockSyntax - || location.Parent is SwitchSectionSyntax - || location.Parent.IsEmbeddedStatementOwner(); - } + return default; + } - if (location is ExpressionSyntax) - { - return location.Parent is ArrowExpressionClauseSyntax or LambdaExpressionSyntax; - } + private static bool IsValidLocation(SyntaxNode location) + { + if (location is StatementSyntax) + { + return location.Parent is BlockSyntax + || location.Parent is SwitchSectionSyntax + || location.Parent.IsEmbeddedStatementOwner(); + } - return false; + if (location is ExpressionSyntax) + { + return location.Parent is ArrowExpressionClauseSyntax or LambdaExpressionSyntax; } - private static SyntaxNode? GetContainer(SyntaxNode node) + return false; + } + + private static SyntaxNode? GetContainer(SyntaxNode node) + { + for (var current = node; current != null; current = current.Parent) { - for (var current = node; current != null; current = current.Parent) + var parameterList = current.GetParameterList(); + if (parameterList != null) { - var parameterList = current.GetParameterList(); - if (parameterList != null) - { - return current; - } + return current; } - - return null; } - private static async Task)>> GetUnassignedParametersAsync( - Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + return null; + } + + private static async Task)>> GetUnassignedParametersAsync( + Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var containersAndLocations = - diagnostics.SelectAsArray(d => GetContainer(root, d.Location.SourceSpan)) - .WhereAsArray(t => t.container != null); + var containersAndLocations = + diagnostics.SelectAsArray(d => GetContainer(root, d.Location.SourceSpan)) + .WhereAsArray(t => t.container != null); - var result = new MultiDictionary)>(); - foreach (var group in containersAndLocations.GroupBy(t => t.container)) - { - var container = group.Key; + var result = new MultiDictionary)>(); + foreach (var group in containersAndLocations.GroupBy(t => t.container)) + { + var container = group.Key; - var parameterList = container.GetParameterList(); - Contract.ThrowIfNull(parameterList); + var parameterList = container.GetParameterList(); + Contract.ThrowIfNull(parameterList); - var outParameters = parameterList.Parameters - .Select(p => semanticModel.GetRequiredDeclaredSymbol(p, cancellationToken)) - .Where(p => p.RefKind == RefKind.Out) - .ToImmutableArray(); + var outParameters = parameterList.Parameters + .Select(p => semanticModel.GetRequiredDeclaredSymbol(p, cancellationToken)) + .Where(p => p.RefKind == RefKind.Out) + .ToImmutableArray(); - var distinctExprsOrStatements = group.Select(t => t.exprOrStatement).Distinct(); - foreach (var exprOrStatement in distinctExprsOrStatements) + var distinctExprsOrStatements = group.Select(t => t.exprOrStatement).Distinct(); + foreach (var exprOrStatement in distinctExprsOrStatements) + { + var dataFlow = semanticModel.AnalyzeDataFlow(exprOrStatement); + var unassignedParameters = outParameters.WhereAsArray( + p => !dataFlow.DefinitelyAssignedOnExit.Contains(p)); + + if (unassignedParameters.Length > 0) { - var dataFlow = semanticModel.AnalyzeDataFlow(exprOrStatement); - var unassignedParameters = outParameters.WhereAsArray( - p => !dataFlow.DefinitelyAssignedOnExit.Contains(p)); - - if (unassignedParameters.Length > 0) - { - result.Add(container, (exprOrStatement, unassignedParameters)); - } + result.Add(container, (exprOrStatement, unassignedParameters)); } } - - return result; } - protected sealed override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var unassignedParameters = await GetUnassignedParametersAsync( - document, diagnostics, cancellationToken).ConfigureAwait(false); - - foreach (var container in unassignedParameters.Keys.OrderByDescending(n => n.Span.Start)) - { - AssignOutParameters( - editor, container, unassignedParameters[container], cancellationToken); - } - } + return result; + } - protected abstract void AssignOutParameters( - SyntaxEditor editor, SyntaxNode container, - MultiDictionary unassignedParameters)>.ValueSet values, - CancellationToken cancellationToken); + protected sealed override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var unassignedParameters = await GetUnassignedParametersAsync( + document, diagnostics, cancellationToken).ConfigureAwait(false); - protected static ImmutableArray GenerateAssignmentStatements( - SyntaxGenerator generator, ImmutableArray unassignedParameters) + foreach (var container in unassignedParameters.Keys.OrderByDescending(n => n.Span.Start)) { - var result = ArrayBuilder.GetInstance(); + AssignOutParameters( + editor, container, unassignedParameters[container], cancellationToken); + } + } - foreach (var parameter in unassignedParameters) - { - result.Add(generator.ExpressionStatement(generator.AssignmentStatement( - generator.IdentifierName(parameter.Name), - ExpressionGenerator.GenerateExpression(generator, parameter.Type, value: null, canUseFieldReference: false)))); - } + protected abstract void AssignOutParameters( + SyntaxEditor editor, SyntaxNode container, + MultiDictionary unassignedParameters)>.ValueSet values, + CancellationToken cancellationToken); - return result.ToImmutableAndFree(); + protected static ImmutableArray GenerateAssignmentStatements( + SyntaxGenerator generator, ImmutableArray unassignedParameters) + { + var result = ArrayBuilder.GetInstance(); + + foreach (var parameter in unassignedParameters) + { + result.Add(generator.ExpressionStatement(generator.AssignmentStatement( + generator.IdentifierName(parameter.Name), + ExpressionGenerator.GenerateExpression(generator, parameter.Type, value: null, canUseFieldReference: false)))); } + + return result.ToImmutableAndFree(); } } diff --git a/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AssignOutParametersAboveReturnCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AssignOutParametersAboveReturnCodeFixProvider.cs index 34566480f779b..074030de7ec6c 100644 --- a/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AssignOutParametersAboveReturnCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AssignOutParametersAboveReturnCodeFixProvider.cs @@ -16,76 +16,75 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.AssignOutParameters +namespace Microsoft.CodeAnalysis.CSharp.AssignOutParameters; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AssignOutParametersAboveReturn), Shared] +internal class AssignOutParametersAboveReturnCodeFixProvider : AbstractAssignOutParametersCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AssignOutParametersAboveReturn), Shared] - internal class AssignOutParametersAboveReturnCodeFixProvider : AbstractAssignOutParametersCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public AssignOutParametersAboveReturnCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public AssignOutParametersAboveReturnCodeFixProvider() + } + + protected override void TryRegisterFix(CodeFixContext context, Document document, SyntaxNode container, SyntaxNode location) + { + RegisterCodeFix(context, CSharpCodeFixesResources.Assign_out_parameters, nameof(CSharpCodeFixesResources.Assign_out_parameters)); + } + + protected override void AssignOutParameters( + SyntaxEditor editor, SyntaxNode container, + MultiDictionary unassignedParameters)>.ValueSet values, + CancellationToken cancellationToken) + { + foreach (var (exprOrStatement, unassignedParameters) in values) { + var statements = GenerateAssignmentStatements(editor.Generator, unassignedParameters); + AddAssignmentStatements(editor, exprOrStatement, statements); } + } - protected override void TryRegisterFix(CodeFixContext context, Document document, SyntaxNode container, SyntaxNode location) + private static void AddAssignmentStatements( + SyntaxEditor editor, SyntaxNode exprOrStatement, ImmutableArray statements) + { + var generator = editor.Generator; + + if (exprOrStatement is LocalFunctionStatementSyntax { ExpressionBody: { } localFunctionExpressionBody }) { - RegisterCodeFix(context, CSharpCodeFixesResources.Assign_out_parameters, nameof(CSharpCodeFixesResources.Assign_out_parameters)); + // Expression-bodied local functions report CS0177 on the method name instead of the expression. + // Reassign exprOrStatement so the code fix implementation works as it does for other expression-bodied + // members. + exprOrStatement = localFunctionExpressionBody.Expression; } - protected override void AssignOutParameters( - SyntaxEditor editor, SyntaxNode container, - MultiDictionary unassignedParameters)>.ValueSet values, - CancellationToken cancellationToken) + var parent = exprOrStatement.GetRequiredParent(); + if (parent.IsEmbeddedStatementOwner()) { - foreach (var (exprOrStatement, unassignedParameters) in values) - { - var statements = GenerateAssignmentStatements(editor.Generator, unassignedParameters); - AddAssignmentStatements(editor, exprOrStatement, statements); - } + var newBody = SyntaxFactory.Block(statements.Add(exprOrStatement).Cast()); + editor.ReplaceNode(exprOrStatement, newBody); + editor.ReplaceNode( + exprOrStatement.GetRequiredParent(), + (c, _) => c.WithAdditionalAnnotations(Formatter.Annotation)); } - - private static void AddAssignmentStatements( - SyntaxEditor editor, SyntaxNode exprOrStatement, ImmutableArray statements) + else if (parent is BlockSyntax or SwitchSectionSyntax) { - var generator = editor.Generator; - - if (exprOrStatement is LocalFunctionStatementSyntax { ExpressionBody: { } localFunctionExpressionBody }) - { - // Expression-bodied local functions report CS0177 on the method name instead of the expression. - // Reassign exprOrStatement so the code fix implementation works as it does for other expression-bodied - // members. - exprOrStatement = localFunctionExpressionBody.Expression; - } - - var parent = exprOrStatement.GetRequiredParent(); - if (parent.IsEmbeddedStatementOwner()) - { - var newBody = SyntaxFactory.Block(statements.Add(exprOrStatement).Cast()); - editor.ReplaceNode(exprOrStatement, newBody); - editor.ReplaceNode( - exprOrStatement.GetRequiredParent(), - (c, _) => c.WithAdditionalAnnotations(Formatter.Annotation)); - } - else if (parent is BlockSyntax or SwitchSectionSyntax) - { - editor.InsertBefore(exprOrStatement, statements); - } - else if (parent is ArrowExpressionClauseSyntax) - { - statements = statements.Add(generator.ReturnStatement(exprOrStatement)); - editor.ReplaceNode( - parent.GetRequiredParent(), - generator.WithStatements(parent.GetRequiredParent(), statements)); - } - else - { - var lambda = (LambdaExpressionSyntax)parent; - var newBody = SyntaxFactory.Block(statements.Add(generator.ReturnStatement(exprOrStatement)).Cast()); - editor.ReplaceNode( - lambda, - lambda.WithBody(newBody) - .WithAdditionalAnnotations(Formatter.Annotation)); - } + editor.InsertBefore(exprOrStatement, statements); + } + else if (parent is ArrowExpressionClauseSyntax) + { + statements = statements.Add(generator.ReturnStatement(exprOrStatement)); + editor.ReplaceNode( + parent.GetRequiredParent(), + generator.WithStatements(parent.GetRequiredParent(), statements)); + } + else + { + var lambda = (LambdaExpressionSyntax)parent; + var newBody = SyntaxFactory.Block(statements.Add(generator.ReturnStatement(exprOrStatement)).Cast()); + editor.ReplaceNode( + lambda, + lambda.WithBody(newBody) + .WithAdditionalAnnotations(Formatter.Annotation)); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AssignOutParametersAtStartCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AssignOutParametersAtStartCodeFixProvider.cs index fe7e7b32e6aae..68e6c53f9fa52 100644 --- a/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AssignOutParametersAtStartCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/AssignOutParameters/AssignOutParametersAtStartCodeFixProvider.cs @@ -14,67 +14,66 @@ using Microsoft.CodeAnalysis.Host.Mef; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.AssignOutParameters +namespace Microsoft.CodeAnalysis.CSharp.AssignOutParameters; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AssignOutParametersAtStart), Shared] +internal class AssignOutParametersAtStartCodeFixProvider : AbstractAssignOutParametersCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AssignOutParametersAtStart), Shared] - internal class AssignOutParametersAtStartCodeFixProvider : AbstractAssignOutParametersCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public AssignOutParametersAtStartCodeFixProvider() + { + } + + protected override void TryRegisterFix(CodeFixContext context, Document document, SyntaxNode container, SyntaxNode location) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public AssignOutParametersAtStartCodeFixProvider() + // Don't offer if we're already the starting statement of the container. This case will + // be handled by the AssignOutParametersAboveReturnCodeFixProvider class. + if (location is ExpressionSyntax) { + return; } - protected override void TryRegisterFix(CodeFixContext context, Document document, SyntaxNode container, SyntaxNode location) + if (location is LocalFunctionStatementSyntax { ExpressionBody: { } }) { - // Don't offer if we're already the starting statement of the container. This case will - // be handled by the AssignOutParametersAboveReturnCodeFixProvider class. - if (location is ExpressionSyntax) - { - return; - } - - if (location is LocalFunctionStatementSyntax { ExpressionBody: { } }) - { - // This is an expression-bodied local function, which is also handled by the other code fix. - return; - } - - if (location is StatementSyntax statement && - statement.Parent is BlockSyntax block && - block.Statements[0] == statement && - block.Parent == container) - { - return; - } - - context.RegisterCodeFix( - CodeAction.Create( - CSharpCodeFixesResources.Assign_out_parameters_at_start, - GetDocumentUpdater(context), - nameof(CSharpCodeFixesResources.Assign_out_parameters_at_start)), - context.Diagnostics); + // This is an expression-bodied local function, which is also handled by the other code fix. + return; } - protected override void AssignOutParameters( - SyntaxEditor editor, SyntaxNode container, - MultiDictionary unassignedParameters)>.ValueSet values, - CancellationToken cancellationToken) + if (location is StatementSyntax statement && + statement.Parent is BlockSyntax block && + block.Statements[0] == statement && + block.Parent == container) { - var generator = editor.Generator; - var unassignedParameters = - values.SelectMany(t => t.unassignedParameters) - .Distinct() - .OrderBy(p => p.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken).SpanStart) - .ToImmutableArray(); + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + CSharpCodeFixesResources.Assign_out_parameters_at_start, + GetDocumentUpdater(context), + nameof(CSharpCodeFixesResources.Assign_out_parameters_at_start)), + context.Diagnostics); + } - var statements = GenerateAssignmentStatements(generator, unassignedParameters); - var originalStatements = generator.GetStatements(container).ToImmutableArray(); + protected override void AssignOutParameters( + SyntaxEditor editor, SyntaxNode container, + MultiDictionary unassignedParameters)>.ValueSet values, + CancellationToken cancellationToken) + { + var generator = editor.Generator; + var unassignedParameters = + values.SelectMany(t => t.unassignedParameters) + .Distinct() + .OrderBy(p => p.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken).SpanStart) + .ToImmutableArray(); - var finalStatements = statements.AddRange(originalStatements); - var updatedContainer = generator.WithStatements(container, finalStatements); + var statements = GenerateAssignmentStatements(generator, unassignedParameters); + var originalStatements = generator.GetStatements(container).ToImmutableArray(); - editor.ReplaceNode(container, updatedContainer); - } + var finalStatements = statements.AddRange(originalStatements); + var updatedContainer = generator.WithStatements(container, finalStatements); + + editor.ReplaceNode(container, updatedContainer); } } diff --git a/src/Analyzers/CSharp/CodeFixes/ConditionalExpressionInStringInterpolation/CSharpAddParenthesesAroundConditionalExpressionInInterpolatedStringCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/ConditionalExpressionInStringInterpolation/CSharpAddParenthesesAroundConditionalExpressionInInterpolatedStringCodeFixProvider.cs index 888e5a87a6ddc..33d5c2a114886 100644 --- a/src/Analyzers/CSharp/CodeFixes/ConditionalExpressionInStringInterpolation/CSharpAddParenthesesAroundConditionalExpressionInInterpolatedStringCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/ConditionalExpressionInStringInterpolation/CSharpAddParenthesesAroundConditionalExpressionInInterpolatedStringCodeFixProvider.cs @@ -16,113 +16,112 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConditionalExpressionInStringInterpolation +namespace Microsoft.CodeAnalysis.CSharp.ConditionalExpressionInStringInterpolation; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddParenthesesAroundConditionalExpressionInInterpolatedString), Shared] +internal class CSharpAddParenthesesAroundConditionalExpressionInInterpolatedStringCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddParenthesesAroundConditionalExpressionInInterpolatedString), Shared] - internal class CSharpAddParenthesesAroundConditionalExpressionInInterpolatedStringCodeFixProvider : CodeFixProvider - { - private const string CS8361 = nameof(CS8361); //A conditional expression cannot be used directly in a string interpolation because the ':' ends the interpolation.Parenthesize the conditional expression. + private const string CS8361 = nameof(CS8361); //A conditional expression cannot be used directly in a string interpolation because the ':' ends the interpolation.Parenthesize the conditional expression. - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddParenthesesAroundConditionalExpressionInInterpolatedStringCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddParenthesesAroundConditionalExpressionInInterpolatedStringCodeFixProvider() + { + } - // CS8361 is a syntax error and it is unlikely that there is more than one CS8361 at a time. - public override FixAllProvider? GetFixAllProvider() => null; + // CS8361 is a syntax error and it is unlikely that there is more than one CS8361 at a time. + public override FixAllProvider? GetFixAllProvider() => null; - public override ImmutableArray FixableDiagnosticIds => [CS8361]; + public override ImmutableArray FixableDiagnosticIds => [CS8361]; - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + var token = root.FindToken(diagnosticSpan.Start); + var conditionalExpression = token.GetAncestor(); + if (conditionalExpression != null) { - var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var diagnostic = context.Diagnostics.First(); - var diagnosticSpan = diagnostic.Location.SourceSpan; - var token = root.FindToken(diagnosticSpan.Start); - var conditionalExpression = token.GetAncestor(); - if (conditionalExpression != null) - { - var documentChangeAction = CodeAction.Create( - CSharpCodeFixesResources.Add_parentheses_around_conditional_expression_in_interpolated_string, - c => GetChangedDocumentAsync(context.Document, conditionalExpression.SpanStart, c), - nameof(CSharpCodeFixesResources.Add_parentheses_around_conditional_expression_in_interpolated_string)); - context.RegisterCodeFix(documentChangeAction, diagnostic); - } + var documentChangeAction = CodeAction.Create( + CSharpCodeFixesResources.Add_parentheses_around_conditional_expression_in_interpolated_string, + c => GetChangedDocumentAsync(context.Document, conditionalExpression.SpanStart, c), + nameof(CSharpCodeFixesResources.Add_parentheses_around_conditional_expression_in_interpolated_string)); + context.RegisterCodeFix(documentChangeAction, diagnostic); } + } - private static async Task GetChangedDocumentAsync(Document document, int conditionalExpressionSyntaxStartPosition, CancellationToken cancellationToken) - { - // The usual SyntaxTree transformations are complicated if string literals are present in the false part as in - // $"{ condition ? "Success": "Failure" }" - // The colon starts a FormatClause and the double quote left to 'F' therefore ends the interpolated string. - // The text starting with 'F' is parsed as code and the resulting syntax tree is impractical. - // The same problem arises if a } is present in the false part. - // To circumvent these problems this solution - // 1. Inserts an opening parenthesis - // 2. Re-parses the resulting document (now the colon isn't treated as starting a FormatClause anymore) - // 3. Replaces the missing CloseParenToken with a new one - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var openParenthesisPosition = conditionalExpressionSyntaxStartPosition; - var textWithOpenParenthesis = text.Replace(openParenthesisPosition, 0, "("); - var documentWithOpenParenthesis = document.WithText(textWithOpenParenthesis); - - var syntaxRoot = await documentWithOpenParenthesis.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var nodeAtInsertPosition = syntaxRoot.FindNode(new TextSpan(openParenthesisPosition, 0)); + private static async Task GetChangedDocumentAsync(Document document, int conditionalExpressionSyntaxStartPosition, CancellationToken cancellationToken) + { + // The usual SyntaxTree transformations are complicated if string literals are present in the false part as in + // $"{ condition ? "Success": "Failure" }" + // The colon starts a FormatClause and the double quote left to 'F' therefore ends the interpolated string. + // The text starting with 'F' is parsed as code and the resulting syntax tree is impractical. + // The same problem arises if a } is present in the false part. + // To circumvent these problems this solution + // 1. Inserts an opening parenthesis + // 2. Re-parses the resulting document (now the colon isn't treated as starting a FormatClause anymore) + // 3. Replaces the missing CloseParenToken with a new one + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var openParenthesisPosition = conditionalExpressionSyntaxStartPosition; + var textWithOpenParenthesis = text.Replace(openParenthesisPosition, 0, "("); + var documentWithOpenParenthesis = document.WithText(textWithOpenParenthesis); - if (nodeAtInsertPosition is not ParenthesizedExpressionSyntax parenthesizedExpression || - !parenthesizedExpression.CloseParenToken.IsMissing) - { - return documentWithOpenParenthesis; - } + var syntaxRoot = await documentWithOpenParenthesis.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var nodeAtInsertPosition = syntaxRoot.FindNode(new TextSpan(openParenthesisPosition, 0)); - return await InsertCloseParenthesisAsync( - documentWithOpenParenthesis, parenthesizedExpression, cancellationToken).ConfigureAwait(false); + if (nodeAtInsertPosition is not ParenthesizedExpressionSyntax parenthesizedExpression || + !parenthesizedExpression.CloseParenToken.IsMissing) + { + return documentWithOpenParenthesis; } - private static async Task InsertCloseParenthesisAsync( - Document document, - ParenthesizedExpressionSyntax parenthesizedExpression, - CancellationToken cancellationToken) - { - var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (parenthesizedExpression.Expression is ConditionalExpressionSyntax conditional && - parenthesizedExpression.GetAncestor()?.StringStartToken.Kind() == SyntaxKind.InterpolatedStringStartToken) - { - // If they have something like: - // - // var s3 = $""Text1 { true ? ""Text2""[|:|] - // NextLineOfCode(); - // - // We will update this initially to: - // - // var s3 = $""Text1 { (true ? ""Text2""[|:|] - // NextLineOfCode(); - // - // And we have to decide where the close paren should go. Based on the parse tree, the - // 'NextLineOfCode()' expression will be pulled into the WhenFalse portion of the conditional. - // So placing the close paren after the conditional woudl result in: 'NextLineOfCode())'. - // - // However, the user intent is likely that NextLineOfCode is not part of the conditional - // So instead find the colon and place the close paren after that, producing: - // - // var s3 = $""Text1 { (true ? ""Text2"":) - // NextLineOfCode(); + return await InsertCloseParenthesisAsync( + documentWithOpenParenthesis, parenthesizedExpression, cancellationToken).ConfigureAwait(false); + } - var endToken = sourceText.AreOnSameLine(conditional.ColonToken, conditional.WhenFalse.GetFirstToken()) - ? conditional.WhenFalse.GetLastToken() - : conditional.ColonToken; + private static async Task InsertCloseParenthesisAsync( + Document document, + ParenthesizedExpressionSyntax parenthesizedExpression, + CancellationToken cancellationToken) + { + var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (parenthesizedExpression.Expression is ConditionalExpressionSyntax conditional && + parenthesizedExpression.GetAncestor()?.StringStartToken.Kind() == SyntaxKind.InterpolatedStringStartToken) + { + // If they have something like: + // + // var s3 = $""Text1 { true ? ""Text2""[|:|] + // NextLineOfCode(); + // + // We will update this initially to: + // + // var s3 = $""Text1 { (true ? ""Text2""[|:|] + // NextLineOfCode(); + // + // And we have to decide where the close paren should go. Based on the parse tree, the + // 'NextLineOfCode()' expression will be pulled into the WhenFalse portion of the conditional. + // So placing the close paren after the conditional woudl result in: 'NextLineOfCode())'. + // + // However, the user intent is likely that NextLineOfCode is not part of the conditional + // So instead find the colon and place the close paren after that, producing: + // + // var s3 = $""Text1 { (true ? ""Text2"":) + // NextLineOfCode(); - var closeParenPosition = endToken.Span.End; - var textWithCloseParenthesis = sourceText.Replace(closeParenPosition, 0, ")"); - return document.WithText(textWithCloseParenthesis); - } + var endToken = sourceText.AreOnSameLine(conditional.ColonToken, conditional.WhenFalse.GetFirstToken()) + ? conditional.WhenFalse.GetLastToken() + : conditional.ColonToken; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newCloseParen = SyntaxFactory.Token(SyntaxKind.CloseParenToken).WithTriviaFrom(parenthesizedExpression.CloseParenToken); - var parenthesizedExpressionWithClosingParen = parenthesizedExpression.WithCloseParenToken(newCloseParen); - var newRoot = root.ReplaceNode(parenthesizedExpression, parenthesizedExpressionWithClosingParen); - return document.WithSyntaxRoot(newRoot); + var closeParenPosition = endToken.Span.End; + var textWithCloseParenthesis = sourceText.Replace(closeParenPosition, 0, ")"); + return document.WithText(textWithCloseParenthesis); } + + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newCloseParen = SyntaxFactory.Token(SyntaxKind.CloseParenToken).WithTriviaFrom(parenthesizedExpression.CloseParenToken); + var parenthesizedExpressionWithClosingParen = parenthesizedExpression.WithCloseParenToken(newCloseParen); + var newRoot = root.ReplaceNode(parenthesizedExpression, parenthesizedExpressionWithClosingParen); + return document.WithSyntaxRoot(newRoot); } } diff --git a/src/Analyzers/CSharp/CodeFixes/ConflictMarkerResolution/CSharpResolveConflictMarkerCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/ConflictMarkerResolution/CSharpResolveConflictMarkerCodeFixProvider.cs index 8b00238d88891..6ec47426307a4 100644 --- a/src/Analyzers/CSharp/CodeFixes/ConflictMarkerResolution/CSharpResolveConflictMarkerCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/ConflictMarkerResolution/CSharpResolveConflictMarkerCodeFixProvider.cs @@ -9,18 +9,17 @@ using Microsoft.CodeAnalysis.CSharp.LanguageService; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.ConflictMarkerResolution +namespace Microsoft.CodeAnalysis.CSharp.ConflictMarkerResolution; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConflictMarkerResolution), Shared] +internal class CSharpResolveConflictMarkerCodeFixProvider : AbstractResolveConflictMarkerCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConflictMarkerResolution), Shared] - internal class CSharpResolveConflictMarkerCodeFixProvider : AbstractResolveConflictMarkerCodeFixProvider - { - private const string CS8300 = nameof(CS8300); // Merge conflict marker encountered + private const string CS8300 = nameof(CS8300); // Merge conflict marker encountered - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpResolveConflictMarkerCodeFixProvider() - : base(CSharpSyntaxKinds.Instance, CS8300) - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpResolveConflictMarkerCodeFixProvider() + : base(CSharpSyntaxKinds.Instance, CS8300) + { } } diff --git a/src/Analyzers/CSharp/CodeFixes/ConvertNamespace/ConvertNamespaceCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/ConvertNamespace/ConvertNamespaceCodeFixProvider.cs index 2db0b7f008a53..a6ac61e24b48f 100644 --- a/src/Analyzers/CSharp/CodeFixes/ConvertNamespace/ConvertNamespaceCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/ConvertNamespace/ConvertNamespaceCodeFixProvider.cs @@ -18,52 +18,51 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace +namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertNamespace), Shared] +internal class ConvertNamespaceCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertNamespace), Shared] - internal class ConvertNamespaceCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConvertNamespaceCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConvertNamespaceCodeFixProvider() - { - } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseBlockScopedNamespaceDiagnosticId, IDEDiagnosticIds.UseFileScopedNamespaceDiagnosticId]; + } + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseBlockScopedNamespaceDiagnosticId, IDEDiagnosticIds.UseFileScopedNamespaceDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var diagnostic = context.Diagnostics.First(); + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); - var (title, equivalenceKey) = ConvertNamespaceAnalysis.GetInfo( - diagnostic.Id switch - { - IDEDiagnosticIds.UseBlockScopedNamespaceDiagnosticId => NamespaceDeclarationPreference.BlockScoped, - IDEDiagnosticIds.UseFileScopedNamespaceDiagnosticId => NamespaceDeclarationPreference.FileScoped, - _ => throw ExceptionUtilities.UnexpectedValue(diagnostic.Id), - }); + var (title, equivalenceKey) = ConvertNamespaceAnalysis.GetInfo( + diagnostic.Id switch + { + IDEDiagnosticIds.UseBlockScopedNamespaceDiagnosticId => NamespaceDeclarationPreference.BlockScoped, + IDEDiagnosticIds.UseFileScopedNamespaceDiagnosticId => NamespaceDeclarationPreference.FileScoped, + _ => throw ExceptionUtilities.UnexpectedValue(diagnostic.Id), + }); - context.RegisterCodeFix( - CodeAction.Create(title, GetDocumentUpdater(context), equivalenceKey), - context.Diagnostics); + context.RegisterCodeFix( + CodeAction.Create(title, GetDocumentUpdater(context), equivalenceKey), + context.Diagnostics); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var diagnostic = diagnostics.First(); + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var diagnostic = diagnostics.First(); - var namespaceDecl = (BaseNamespaceDeclarationSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken); + var namespaceDecl = (BaseNamespaceDeclarationSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken); - var options = await document.GetCSharpCodeFixOptionsProviderAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var converted = await ConvertNamespaceTransform.ConvertAsync(document, namespaceDecl, options.GetFormattingOptions(), cancellationToken).ConfigureAwait(false); + var options = await document.GetCSharpCodeFixOptionsProviderAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var converted = await ConvertNamespaceTransform.ConvertAsync(document, namespaceDecl, options.GetFormattingOptions(), cancellationToken).ConfigureAwait(false); - editor.ReplaceNode( - editor.OriginalRoot, - await converted.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false)); - } + editor.ReplaceNode( + editor.OriginalRoot, + await converted.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false)); } } diff --git a/src/Analyzers/CSharp/CodeFixes/ConvertNamespace/ConvertNamespaceTransform.cs b/src/Analyzers/CSharp/CodeFixes/ConvertNamespace/ConvertNamespaceTransform.cs index 0aab8426fbafb..40175de4cf443 100644 --- a/src/Analyzers/CSharp/CodeFixes/ConvertNamespace/ConvertNamespaceTransform.cs +++ b/src/Analyzers/CSharp/CodeFixes/ConvertNamespace/ConvertNamespaceTransform.cs @@ -113,7 +113,7 @@ private static (SourceText text, TextSpan semicolonSpan) DedentNamespace( var semicolonLine = text.Lines.GetLineFromPosition(fileScopedNamespace.SemicolonToken.SpanStart).LineNumber; // Cache what we compute so we don't have to recompute it for every line of a raw string literal. - (SyntaxToken stringLiteral, int closeTerminatorIndentationLength) lastRawStringLiteralData = default; + (SyntaxNode stringNode, int closeTerminatorIndentationLength) lastRawStringLiteralData = default; using var _ = ArrayBuilder.GetInstance(out var changes); for (var line = semicolonLine + 1; line < text.Lines.Count; line++) @@ -129,16 +129,37 @@ private static (SourceText text, TextSpan semicolonSpan) DedentNamespace( // dedented safely depending on the position of their close terminator. if (syntaxTree.IsEntirelyWithinStringLiteral(textLine.Span.Start, out var stringLiteral, cancellationToken)) { - if (stringLiteral.Kind() is not SyntaxKind.MultiLineRawStringLiteralToken and not SyntaxKind.Utf8MultiLineRawStringLiteralToken) + SyntaxNode stringNode; + if (stringLiteral.Kind() is SyntaxKind.InterpolatedStringTextToken) + { + if (stringLiteral.GetRequiredParent() is not InterpolatedStringTextSyntax { Parent: InterpolatedStringExpressionSyntax { StringStartToken: (kind: SyntaxKind.InterpolatedMultiLineRawStringStartToken) } interpolatedString }) + return null; + + stringNode = interpolatedString; + } + else if (stringLiteral.Kind() is SyntaxKind.InterpolatedRawStringEndToken) + { + if (stringLiteral.GetRequiredParent() is not InterpolatedStringExpressionSyntax { StringStartToken: (kind: SyntaxKind.InterpolatedMultiLineRawStringStartToken) } interpolatedString) + return null; + + stringNode = interpolatedString; + } + else if (stringLiteral.Kind() is SyntaxKind.MultiLineRawStringLiteralToken or SyntaxKind.Utf8MultiLineRawStringLiteralToken) + { + stringNode = stringLiteral.GetRequiredParent(); + } + else + { return null; + } // Don't touch the raw string if it already has issues. - if (stringLiteral.ContainsDiagnostics) + if (stringNode.ContainsDiagnostics) return null; // Ok, only dedent the raw string contents if we can dedent the closing terminator of the raw string. - if (lastRawStringLiteralData.stringLiteral != stringLiteral) - lastRawStringLiteralData = (stringLiteral, ComputeCommonIndentationLength(text.Lines.GetLineFromPosition(stringLiteral.Span.End))); + if (lastRawStringLiteralData.stringNode != stringNode) + lastRawStringLiteralData = (stringNode, ComputeCommonIndentationLength(text.Lines.GetLineFromPosition(stringNode.Span.End))); // If we can't dedent the close terminator the right amount, don't dedent any contents. if (lastRawStringLiteralData.closeTerminatorIndentationLength != indentation.Length) diff --git a/src/Analyzers/CSharp/CodeFixes/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionCodeFixProvider.Rewriter.cs b/src/Analyzers/CSharp/CodeFixes/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionCodeFixProvider.Rewriter.cs index 3aab5ed719250..020adab72164d 100644 --- a/src/Analyzers/CSharp/CodeFixes/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionCodeFixProvider.Rewriter.cs +++ b/src/Analyzers/CSharp/CodeFixes/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionCodeFixProvider.Rewriter.cs @@ -11,291 +11,290 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression -{ - using static ConvertSwitchStatementToExpressionHelpers; - using static SyntaxFactory; +namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression; + +using static ConvertSwitchStatementToExpressionHelpers; +using static SyntaxFactory; - internal sealed partial class ConvertSwitchStatementToExpressionCodeFixProvider +internal sealed partial class ConvertSwitchStatementToExpressionCodeFixProvider +{ + private sealed class Rewriter : CSharpSyntaxVisitor { - private sealed class Rewriter : CSharpSyntaxVisitor - { - private readonly SemanticModel _semanticModel; - private readonly bool _isAllThrowStatements; - private readonly CancellationToken _cancellationToken; + private readonly SemanticModel _semanticModel; + private readonly bool _isAllThrowStatements; + private readonly CancellationToken _cancellationToken; + + private ExpressionSyntax? _assignmentTarget; - private ExpressionSyntax? _assignmentTarget; + private Rewriter(SemanticModel semanticModel, bool isAllThrowStatements, CancellationToken cancellationToken) + { + _semanticModel = semanticModel; + _isAllThrowStatements = isAllThrowStatements; + _cancellationToken = cancellationToken; + } - private Rewriter(SemanticModel semanticModel, bool isAllThrowStatements, CancellationToken cancellationToken) + public static StatementSyntax Rewrite( + SwitchStatementSyntax switchStatement, + SemanticModel model, + ITypeSymbol? declaratorToRemoveType, + SyntaxKind nodeToGenerate, + bool shouldMoveNextStatementToSwitchExpression, + bool generateDeclaration, + CancellationToken cancellationToken) + { + if (switchStatement.ContainsDirectives) { - _semanticModel = semanticModel; - _isAllThrowStatements = isAllThrowStatements; - _cancellationToken = cancellationToken; + // Do not rewrite statements with preprocessor directives + return switchStatement; } - public static StatementSyntax Rewrite( - SwitchStatementSyntax switchStatement, - SemanticModel model, - ITypeSymbol? declaratorToRemoveType, - SyntaxKind nodeToGenerate, - bool shouldMoveNextStatementToSwitchExpression, - bool generateDeclaration, - CancellationToken cancellationToken) - { - if (switchStatement.ContainsDirectives) - { - // Do not rewrite statements with preprocessor directives - return switchStatement; - } + var rewriter = new Rewriter(model, isAllThrowStatements: nodeToGenerate == SyntaxKind.ThrowStatement, cancellationToken); - var rewriter = new Rewriter(model, isAllThrowStatements: nodeToGenerate == SyntaxKind.ThrowStatement, cancellationToken); + // Rewrite the switch statement as a switch expression. + var switchExpression = rewriter.RewriteSwitchStatement( + switchStatement, topLevel: true, + allowMoveNextStatementToSwitchExpression: shouldMoveNextStatementToSwitchExpression); - // Rewrite the switch statement as a switch expression. - var switchExpression = rewriter.RewriteSwitchStatement( - switchStatement, topLevel: true, - allowMoveNextStatementToSwitchExpression: shouldMoveNextStatementToSwitchExpression); + // Generate the final statement to wrap the switch expression, e.g. a "return" or an assignment. + return rewriter.GetFinalStatement( + switchExpression, switchStatement.SwitchKeyword.LeadingTrivia, declaratorToRemoveType, nodeToGenerate, generateDeclaration); + } - // Generate the final statement to wrap the switch expression, e.g. a "return" or an assignment. - return rewriter.GetFinalStatement( - switchExpression, switchStatement.SwitchKeyword.LeadingTrivia, declaratorToRemoveType, nodeToGenerate, generateDeclaration); + private StatementSyntax GetFinalStatement( + ExpressionSyntax switchExpression, + SyntaxTriviaList leadingTrivia, + ITypeSymbol? declaratorToRemoveType, + SyntaxKind nodeToGenerate, + bool generateDeclaration) + { + switch (nodeToGenerate) + { + case SyntaxKind.ReturnStatement: + return ReturnStatement( + Token(leadingTrivia, SyntaxKind.ReturnKeyword, trailing: default), + switchExpression, + Token(SyntaxKind.SemicolonToken)); + case SyntaxKind.ThrowStatement: + return ThrowStatement( + Token(leadingTrivia, SyntaxKind.ThrowKeyword, trailing: default), + switchExpression, + Token(SyntaxKind.SemicolonToken)); } - private StatementSyntax GetFinalStatement( - ExpressionSyntax switchExpression, - SyntaxTriviaList leadingTrivia, - ITypeSymbol? declaratorToRemoveType, - SyntaxKind nodeToGenerate, - bool generateDeclaration) - { - switch (nodeToGenerate) - { - case SyntaxKind.ReturnStatement: - return ReturnStatement( - Token(leadingTrivia, SyntaxKind.ReturnKeyword, trailing: default), - switchExpression, - Token(SyntaxKind.SemicolonToken)); - case SyntaxKind.ThrowStatement: - return ThrowStatement( - Token(leadingTrivia, SyntaxKind.ThrowKeyword, trailing: default), - switchExpression, - Token(SyntaxKind.SemicolonToken)); - } + Debug.Assert(SyntaxFacts.IsAssignmentExpression(nodeToGenerate)); + Debug.Assert(_assignmentTarget != null); - Debug.Assert(SyntaxFacts.IsAssignmentExpression(nodeToGenerate)); - Debug.Assert(_assignmentTarget != null); + return generateDeclaration + ? GenerateVariableDeclaration(switchExpression, declaratorToRemoveType) + : GenerateAssignment(switchExpression, nodeToGenerate, leadingTrivia); + } - return generateDeclaration - ? GenerateVariableDeclaration(switchExpression, declaratorToRemoveType) - : GenerateAssignment(switchExpression, nodeToGenerate, leadingTrivia); - } + private ExpressionStatementSyntax GenerateAssignment(ExpressionSyntax switchExpression, SyntaxKind assignmentKind, SyntaxTriviaList leadingTrivia) + { + Contract.ThrowIfNull(_assignmentTarget); - private ExpressionStatementSyntax GenerateAssignment(ExpressionSyntax switchExpression, SyntaxKind assignmentKind, SyntaxTriviaList leadingTrivia) - { - Contract.ThrowIfNull(_assignmentTarget); + return ExpressionStatement( + AssignmentExpression(assignmentKind, + left: _assignmentTarget, + right: switchExpression)) + .WithLeadingTrivia(leadingTrivia); + } - return ExpressionStatement( - AssignmentExpression(assignmentKind, - left: _assignmentTarget, - right: switchExpression)) - .WithLeadingTrivia(leadingTrivia); - } + private StatementSyntax GenerateVariableDeclaration(ExpressionSyntax switchExpression, ITypeSymbol? declaratorToRemoveType) + { + Contract.ThrowIfFalse(_assignmentTarget is IdentifierNameSyntax); + + // There is a probability that we cannot use var if the declaration type is a reference type or nullable type. + // In these cases, we generate the explicit type for now and decide later whether or not to use var. + var cannotUseVar = declaratorToRemoveType != null && (declaratorToRemoveType.IsReferenceType || declaratorToRemoveType.IsNullable()); + var type = cannotUseVar ? declaratorToRemoveType!.GenerateTypeSyntax() : IdentifierName("var"); + + return LocalDeclarationStatement( + VariableDeclaration( + type, + variables: [VariableDeclarator( + identifier: ((IdentifierNameSyntax)_assignmentTarget).Identifier, + argumentList: null, + initializer: EqualsValueClause(switchExpression))])); + } - private StatementSyntax GenerateVariableDeclaration(ExpressionSyntax switchExpression, ITypeSymbol? declaratorToRemoveType) - { - Contract.ThrowIfFalse(_assignmentTarget is IdentifierNameSyntax); - - // There is a probability that we cannot use var if the declaration type is a reference type or nullable type. - // In these cases, we generate the explicit type for now and decide later whether or not to use var. - var cannotUseVar = declaratorToRemoveType != null && (declaratorToRemoveType.IsReferenceType || declaratorToRemoveType.IsNullable()); - var type = cannotUseVar ? declaratorToRemoveType!.GenerateTypeSyntax() : IdentifierName("var"); - - return LocalDeclarationStatement( - VariableDeclaration( - type, - variables: [VariableDeclarator( - identifier: ((IdentifierNameSyntax)_assignmentTarget).Identifier, - argumentList: null, - initializer: EqualsValueClause(switchExpression))])); - } + private SwitchExpressionArmSyntax GetSwitchExpressionArm(SwitchSectionSyntax node) + { + return SwitchExpressionArm( + pattern: GetPattern(node.Labels, out var whenClauseOpt), + whenClause: whenClauseOpt, + expression: RewriteStatements(node.Statements)); + } - private SwitchExpressionArmSyntax GetSwitchExpressionArm(SwitchSectionSyntax node) - { - return SwitchExpressionArm( - pattern: GetPattern(node.Labels, out var whenClauseOpt), - whenClause: whenClauseOpt, - expression: RewriteStatements(node.Statements)); - } + private static PatternSyntax GetPattern(SyntaxList switchLabels, out WhenClauseSyntax? whenClause) + { + if (switchLabels.Count == 1) + return GetPattern(switchLabels[0], out whenClause); - private static PatternSyntax GetPattern(SyntaxList switchLabels, out WhenClauseSyntax? whenClause) + if (switchLabels.Any(label => IsDefaultSwitchLabel(label))) { - if (switchLabels.Count == 1) - return GetPattern(switchLabels[0], out whenClause); + // original group had a catch-all label. just convert to a discard _ to indicate the same. + whenClause = null; + return DiscardPattern(); + } - if (switchLabels.Any(label => IsDefaultSwitchLabel(label))) - { - // original group had a catch-all label. just convert to a discard _ to indicate the same. - whenClause = null; - return DiscardPattern(); - } + // Multiple labels, and no catch-all merge them using an 'or' pattern. + var totalPattern = GetPattern(switchLabels[0], out var whenClauseUnused); + Debug.Assert(whenClauseUnused == null, "We should not have offered to convert multiple cases if any have a when clause"); - // Multiple labels, and no catch-all merge them using an 'or' pattern. - var totalPattern = GetPattern(switchLabels[0], out var whenClauseUnused); + for (var i = 1; i < switchLabels.Count; i++) + { + var nextPatternPart = GetPattern(switchLabels[i], out whenClauseUnused); Debug.Assert(whenClauseUnused == null, "We should not have offered to convert multiple cases if any have a when clause"); - for (var i = 1; i < switchLabels.Count; i++) - { - var nextPatternPart = GetPattern(switchLabels[i], out whenClauseUnused); - Debug.Assert(whenClauseUnused == null, "We should not have offered to convert multiple cases if any have a when clause"); - - totalPattern = BinaryPattern(SyntaxKind.OrPattern, totalPattern.Parenthesize(), nextPatternPart.Parenthesize()); - } - - whenClause = null; - return totalPattern; + totalPattern = BinaryPattern(SyntaxKind.OrPattern, totalPattern.Parenthesize(), nextPatternPart.Parenthesize()); } - private static PatternSyntax GetPattern(SwitchLabelSyntax switchLabel, out WhenClauseSyntax? whenClause) + whenClause = null; + return totalPattern; + } + + private static PatternSyntax GetPattern(SwitchLabelSyntax switchLabel, out WhenClauseSyntax? whenClause) + { + switch (switchLabel.Kind()) { - switch (switchLabel.Kind()) - { - case SyntaxKind.CasePatternSwitchLabel: - var node = (CasePatternSwitchLabelSyntax)switchLabel; - whenClause = node.WhenClause; - return node.Pattern; + case SyntaxKind.CasePatternSwitchLabel: + var node = (CasePatternSwitchLabelSyntax)switchLabel; + whenClause = node.WhenClause; + return node.Pattern; - case SyntaxKind.CaseSwitchLabel: - whenClause = null; - return ConstantPattern(((CaseSwitchLabelSyntax)switchLabel).Value); + case SyntaxKind.CaseSwitchLabel: + whenClause = null; + return ConstantPattern(((CaseSwitchLabelSyntax)switchLabel).Value); - case SyntaxKind.DefaultSwitchLabel: - whenClause = null; - return DiscardPattern(); + case SyntaxKind.DefaultSwitchLabel: + whenClause = null; + return DiscardPattern(); - case var value: - throw ExceptionUtilities.UnexpectedValue(value); - } + case var value: + throw ExceptionUtilities.UnexpectedValue(value); } + } - public override ExpressionSyntax VisitAssignmentExpression(AssignmentExpressionSyntax node) - { - _assignmentTarget ??= node.Left; - return CastIfChangeInRuntimeRepresentation(node.Right); - } + public override ExpressionSyntax VisitAssignmentExpression(AssignmentExpressionSyntax node) + { + _assignmentTarget ??= node.Left; + return CastIfChangeInRuntimeRepresentation(node.Right); + } - private ExpressionSyntax CastIfChangeInRuntimeRepresentation(ExpressionSyntax node) + private ExpressionSyntax CastIfChangeInRuntimeRepresentation(ExpressionSyntax node) + { + // If the existing return/assign had an conversion involved that changed the runtime representation of + // the type (or value), then insert that same cast explicitly in the final result. This is needed as + // switch statements do not use best-common-type, but switch expressions can use it. We don't want the + // original conversion to be lost because a new best-common-type conversion is added. + var typeInfo = _semanticModel.GetTypeInfo(node, _cancellationToken); + if (typeInfo.ConvertedType is not null && + typeInfo.Type is not null && + !Equals(typeInfo.ConvertedType, typeInfo.Type)) { - // If the existing return/assign had an conversion involved that changed the runtime representation of - // the type (or value), then insert that same cast explicitly in the final result. This is needed as - // switch statements do not use best-common-type, but switch expressions can use it. We don't want the - // original conversion to be lost because a new best-common-type conversion is added. - var typeInfo = _semanticModel.GetTypeInfo(node, _cancellationToken); - if (typeInfo.ConvertedType is not null && - typeInfo.Type is not null && - !Equals(typeInfo.ConvertedType, typeInfo.Type)) - { - var conversion = _semanticModel.Compilation.ClassifyConversion(typeInfo.Type, typeInfo.ConvertedType); - if (!conversion.IsIdentityOrImplicitReference()) - return node.Cast(typeInfo.ConvertedType); - } - - return node; + var conversion = _semanticModel.Compilation.ClassifyConversion(typeInfo.Type, typeInfo.ConvertedType); + if (!conversion.IsIdentityOrImplicitReference()) + return node.Cast(typeInfo.ConvertedType); } - private ExpressionSyntax RewriteStatements(SyntaxList statements) - { - Debug.Assert(statements.Count is 1 or 2); - Debug.Assert(!statements[0].IsKind(SyntaxKind.BreakStatement)); - var result = Visit(statements[0]); - Contract.ThrowIfNull(result); - return result; - } + return node; + } - public override ExpressionSyntax VisitSwitchStatement(SwitchStatementSyntax node) - => RewriteSwitchStatement(node, topLevel: false); + private ExpressionSyntax RewriteStatements(SyntaxList statements) + { + Debug.Assert(statements.Count is 1 or 2); + Debug.Assert(!statements[0].IsKind(SyntaxKind.BreakStatement)); + var result = Visit(statements[0]); + Contract.ThrowIfNull(result); + return result; + } - private ExpressionSyntax RewriteSwitchStatement( - SwitchStatementSyntax node, - bool topLevel, - bool allowMoveNextStatementToSwitchExpression = true) - { - var switchArms = node.Sections - // The default label must come last in the switch expression. - .OrderBy(section => section.Labels.Any(label => IsDefaultSwitchLabel(label))) - .Select(s => - (tokensForLeadingTrivia: new[] { s.Labels[0].GetFirstToken(), s.Labels[0].GetLastToken() }, - tokensForTrailingTrivia: new[] { s.Statements[0].GetFirstToken(), s.Statements[0].GetLastToken() }, - armExpression: GetSwitchExpressionArm(s))) - .ToList(); - - if (allowMoveNextStatementToSwitchExpression) - { - var nextStatement = node.GetNextStatement(); - if (nextStatement is (kind: SyntaxKind.ThrowStatement or SyntaxKind.ReturnStatement)) - { - var armExpression = Visit(nextStatement); - Contract.ThrowIfNull(armExpression); - - switchArms.Add( - (tokensForLeadingTrivia: new[] { nextStatement.GetFirstToken() }, - tokensForTrailingTrivia: new[] { nextStatement.GetLastToken() }, - SwitchExpressionArm(DiscardPattern(), armExpression))); - } - } - // add explicit cast if necessary - var switchStatement = topLevel ? AddCastIfNecessary(node) : node; - - return SwitchExpression( - switchStatement.Expression.Parenthesize(), - Token(leading: default, SyntaxKind.SwitchKeyword, node.CloseParenToken.TrailingTrivia), - Token(leading: default, SyntaxKind.OpenBraceToken, node.OpenBraceToken.TrailingTrivia), - SeparatedList( - switchArms.Select(t => t.armExpression.WithLeadingTrivia(t.tokensForLeadingTrivia.GetTrivia().FilterComments(addElasticMarker: false))), - switchArms.Select(t => Token(SyntaxKind.CommaToken).WithTrailingTrivia(t.tokensForTrailingTrivia.GetTrivia().FilterComments(addElasticMarker: true)))), - Token(SyntaxKind.CloseBraceToken)); - } + public override ExpressionSyntax VisitSwitchStatement(SwitchStatementSyntax node) + => RewriteSwitchStatement(node, topLevel: false); - private SwitchStatementSyntax AddCastIfNecessary(SwitchStatementSyntax node) + private ExpressionSyntax RewriteSwitchStatement( + SwitchStatementSyntax node, + bool topLevel, + bool allowMoveNextStatementToSwitchExpression = true) + { + var switchArms = node.Sections + // The default label must come last in the switch expression. + .OrderBy(section => section.Labels.Any(label => IsDefaultSwitchLabel(label))) + .Select(s => + (tokensForLeadingTrivia: new[] { s.Labels[0].GetFirstToken(), s.Labels[0].GetLastToken() }, + tokensForTrailingTrivia: new[] { s.Statements[0].GetFirstToken(), s.Statements[0].GetLastToken() }, + armExpression: GetSwitchExpressionArm(s))) + .ToList(); + + if (allowMoveNextStatementToSwitchExpression) { - // If the switch statement expression is being implicitly converted then we need to explicitly cast the - // expression before rewriting as a switch expression - var expressionType = _semanticModel.GetSymbolInfo(node.Expression).Symbol.GetSymbolType(); - var expressionConvertedType = _semanticModel.GetTypeInfo(node.Expression).ConvertedType; - - if (expressionConvertedType != null && - !SymbolEqualityComparer.Default.Equals(expressionConvertedType, expressionType)) + var nextStatement = node.GetNextStatement(); + if (nextStatement is (kind: SyntaxKind.ThrowStatement or SyntaxKind.ReturnStatement)) { - return node.Update(node.SwitchKeyword, node.OpenParenToken, - node.Expression.Cast(expressionConvertedType).WithAdditionalAnnotations(Formatter.Annotation), - node.CloseParenToken, node.OpenBraceToken, - node.Sections, node.CloseBraceToken); - } + var armExpression = Visit(nextStatement); + Contract.ThrowIfNull(armExpression); - return node; + switchArms.Add( + (tokensForLeadingTrivia: new[] { nextStatement.GetFirstToken() }, + tokensForTrailingTrivia: new[] { nextStatement.GetLastToken() }, + SwitchExpressionArm(DiscardPattern(), armExpression))); + } } + // add explicit cast if necessary + var switchStatement = topLevel ? AddCastIfNecessary(node) : node; + + return SwitchExpression( + switchStatement.Expression.Parenthesize(), + Token(leading: default, SyntaxKind.SwitchKeyword, node.CloseParenToken.TrailingTrivia), + Token(leading: default, SyntaxKind.OpenBraceToken, node.OpenBraceToken.TrailingTrivia), + SeparatedList( + switchArms.Select(t => t.armExpression.WithLeadingTrivia(t.tokensForLeadingTrivia.GetTrivia().FilterComments(addElasticMarker: false))), + switchArms.Select(t => Token(SyntaxKind.CommaToken).WithTrailingTrivia(t.tokensForTrailingTrivia.GetTrivia().FilterComments(addElasticMarker: true)))), + Token(SyntaxKind.CloseBraceToken)); + } - public override ExpressionSyntax VisitReturnStatement(ReturnStatementSyntax node) - { - Contract.ThrowIfNull(node.Expression); - return CastIfChangeInRuntimeRepresentation(node.Expression); - } + private SwitchStatementSyntax AddCastIfNecessary(SwitchStatementSyntax node) + { + // If the switch statement expression is being implicitly converted then we need to explicitly cast the + // expression before rewriting as a switch expression + var expressionType = _semanticModel.GetSymbolInfo(node.Expression).Symbol.GetSymbolType(); + var expressionConvertedType = _semanticModel.GetTypeInfo(node.Expression).ConvertedType; - public override ExpressionSyntax VisitThrowStatement(ThrowStatementSyntax node) + if (expressionConvertedType != null && + !SymbolEqualityComparer.Default.Equals(expressionConvertedType, expressionType)) { - Contract.ThrowIfNull(node.Expression); - // If this is an all-throw switch statement, we return the expression rather than - // creating a throw expression so we can wrap the switch expression inside a throw expression. - return _isAllThrowStatements ? node.Expression : ThrowExpression(node.Expression); + return node.Update(node.SwitchKeyword, node.OpenParenToken, + node.Expression.Cast(expressionConvertedType).WithAdditionalAnnotations(Formatter.Annotation), + node.CloseParenToken, node.OpenBraceToken, + node.Sections, node.CloseBraceToken); } - public override ExpressionSyntax VisitExpressionStatement(ExpressionStatementSyntax node) - { - var result = Visit(node.Expression); - Contract.ThrowIfNull(result); - return result; - } + return node; + } - public override ExpressionSyntax DefaultVisit(SyntaxNode node) - => throw ExceptionUtilities.UnexpectedValue(node.Kind()); + public override ExpressionSyntax VisitReturnStatement(ReturnStatementSyntax node) + { + Contract.ThrowIfNull(node.Expression); + return CastIfChangeInRuntimeRepresentation(node.Expression); } + + public override ExpressionSyntax VisitThrowStatement(ThrowStatementSyntax node) + { + Contract.ThrowIfNull(node.Expression); + // If this is an all-throw switch statement, we return the expression rather than + // creating a throw expression so we can wrap the switch expression inside a throw expression. + return _isAllThrowStatements ? node.Expression : ThrowExpression(node.Expression); + } + + public override ExpressionSyntax VisitExpressionStatement(ExpressionStatementSyntax node) + { + var result = Visit(node.Expression); + Contract.ThrowIfNull(result); + return result; + } + + public override ExpressionSyntax DefaultVisit(SyntaxNode node) + => throw ExceptionUtilities.UnexpectedValue(node.Kind()); } } diff --git a/src/Analyzers/CSharp/CodeFixes/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionCodeFixProvider.cs index 7ca9e3d077a6a..4c00de4eef8b5 100644 --- a/src/Analyzers/CSharp/CodeFixes/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/ConvertSwitchStatementToExpression/ConvertSwitchStatementToExpressionCodeFixProvider.cs @@ -22,98 +22,97 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression +namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression; + +using Constants = ConvertSwitchStatementToExpressionConstants; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertSwitchStatementToExpression), Shared] +internal sealed partial class ConvertSwitchStatementToExpressionCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - using Constants = ConvertSwitchStatementToExpressionConstants; + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConvertSwitchStatementToExpressionCodeFixProvider() + { + } - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertSwitchStatementToExpression), Shared] - internal sealed partial class ConvertSwitchStatementToExpressionCodeFixProvider : SyntaxEditorBasedCodeFixProvider + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.ConvertSwitchStatementToExpressionDiagnosticId]; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConvertSwitchStatementToExpressionCodeFixProvider() + var switchLocation = context.Diagnostics.First().AdditionalLocations[0]; + var switchStatement = (SwitchStatementSyntax)switchLocation.FindNode(getInnermostNodeForTie: true, context.CancellationToken); + if (switchStatement.ContainsDirectives) { + // Avoid providing code fixes for switch statements containing directives + return Task.CompletedTask; } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.ConvertSwitchStatementToExpressionDiagnosticId]; + RegisterCodeFix(context, CSharpAnalyzersResources.Convert_switch_statement_to_expression, nameof(CSharpAnalyzersResources.Convert_switch_statement_to_expression)); + return Task.CompletedTask; + } - public override Task RegisterCodeFixesAsync(CodeFixContext context) + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(diagnostics.Length, out var spans); + foreach (var diagnostic in diagnostics) { - var switchLocation = context.Diagnostics.First().AdditionalLocations[0]; - var switchStatement = (SwitchStatementSyntax)switchLocation.FindNode(getInnermostNodeForTie: true, context.CancellationToken); - if (switchStatement.ContainsDirectives) + cancellationToken.ThrowIfCancellationRequested(); + + var switchLocation = diagnostic.AdditionalLocations[0]; + if (spans.Any((s, nodeSpan) => s.Contains(nodeSpan), switchLocation.SourceSpan)) { - // Avoid providing code fixes for switch statements containing directives - return Task.CompletedTask; + // Skip nested switch expressions in case of a fix-all operation. + continue; } - RegisterCodeFix(context, CSharpAnalyzersResources.Convert_switch_statement_to_expression, nameof(CSharpAnalyzersResources.Convert_switch_statement_to_expression)); - return Task.CompletedTask; - } + spans.Add(switchLocation.SourceSpan); - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - using var _ = ArrayBuilder.GetInstance(diagnostics.Length, out var spans); - foreach (var diagnostic in diagnostics) + var properties = diagnostic.Properties; + var nodeToGenerate = (SyntaxKind)int.Parse(properties[Constants.NodeToGenerateKey]!); + var shouldRemoveNextStatement = bool.Parse(properties[Constants.ShouldRemoveNextStatementKey]!); + + var declaratorToRemoveLocation = diagnostic.AdditionalLocations.ElementAtOrDefault(1); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + VariableDeclaratorSyntax? declaratorToRemoveNode = null; + ITypeSymbol? declaratorToRemoveType = null; + + if (declaratorToRemoveLocation != null) + { + declaratorToRemoveNode = (VariableDeclaratorSyntax)declaratorToRemoveLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); + declaratorToRemoveType = semanticModel.GetDeclaredSymbol(declaratorToRemoveNode, cancellationToken).GetSymbolType(); + } + + var switchStatement = (SwitchStatementSyntax)switchLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); + + var switchExpressionStatement = Rewriter.Rewrite( + switchStatement, semanticModel, declaratorToRemoveType, nodeToGenerate, + shouldMoveNextStatementToSwitchExpression: shouldRemoveNextStatement, + generateDeclaration: declaratorToRemoveLocation is not null, + cancellationToken); + + if (declaratorToRemoveNode is not null) + { + editor.RemoveNode(declaratorToRemoveNode); + + // If we are removing the declarator statement entirely, transfer its leading trivia to the + // expression-statement are converting to. + if (declaratorToRemoveNode.Parent is VariableDeclarationSyntax { Parent: LocalDeclarationStatementSyntax declStatement, Variables.Count: 1 }) + switchExpressionStatement = switchExpressionStatement.WithPrependedLeadingTrivia(declStatement.GetLeadingTrivia()); + } + + editor.ReplaceNode(switchStatement, switchExpressionStatement.WithAdditionalAnnotations(Formatter.Annotation)); + + if (shouldRemoveNextStatement) { - cancellationToken.ThrowIfCancellationRequested(); - - var switchLocation = diagnostic.AdditionalLocations[0]; - if (spans.Any((s, nodeSpan) => s.Contains(nodeSpan), switchLocation.SourceSpan)) - { - // Skip nested switch expressions in case of a fix-all operation. - continue; - } - - spans.Add(switchLocation.SourceSpan); - - var properties = diagnostic.Properties; - var nodeToGenerate = (SyntaxKind)int.Parse(properties[Constants.NodeToGenerateKey]!); - var shouldRemoveNextStatement = bool.Parse(properties[Constants.ShouldRemoveNextStatementKey]!); - - var declaratorToRemoveLocation = diagnostic.AdditionalLocations.ElementAtOrDefault(1); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - VariableDeclaratorSyntax? declaratorToRemoveNode = null; - ITypeSymbol? declaratorToRemoveType = null; - - if (declaratorToRemoveLocation != null) - { - declaratorToRemoveNode = (VariableDeclaratorSyntax)declaratorToRemoveLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); - declaratorToRemoveType = semanticModel.GetDeclaredSymbol(declaratorToRemoveNode, cancellationToken).GetSymbolType(); - } - - var switchStatement = (SwitchStatementSyntax)switchLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); - - var switchExpressionStatement = Rewriter.Rewrite( - switchStatement, semanticModel, declaratorToRemoveType, nodeToGenerate, - shouldMoveNextStatementToSwitchExpression: shouldRemoveNextStatement, - generateDeclaration: declaratorToRemoveLocation is not null, - cancellationToken); - - if (declaratorToRemoveNode is not null) - { - editor.RemoveNode(declaratorToRemoveNode); - - // If we are removing the declarator statement entirely, transfer its leading trivia to the - // expression-statement are converting to. - if (declaratorToRemoveNode.Parent is VariableDeclarationSyntax { Parent: LocalDeclarationStatementSyntax declStatement, Variables.Count: 1 }) - switchExpressionStatement = switchExpressionStatement.WithPrependedLeadingTrivia(declStatement.GetLeadingTrivia()); - } - - editor.ReplaceNode(switchStatement, switchExpressionStatement.WithAdditionalAnnotations(Formatter.Annotation)); - - if (shouldRemoveNextStatement) - { - // Already morphed into the top-level switch expression. - var nextStatement = switchStatement.GetNextStatement(); - Contract.ThrowIfNull(nextStatement); - Debug.Assert(nextStatement.Kind() is SyntaxKind.ThrowStatement or SyntaxKind.ReturnStatement); - editor.RemoveNode(nextStatement.IsParentKind(SyntaxKind.GlobalStatement) ? nextStatement.GetRequiredParent() : nextStatement); - } + // Already morphed into the top-level switch expression. + var nextStatement = switchStatement.GetNextStatement(); + Contract.ThrowIfNull(nextStatement); + Debug.Assert(nextStatement.Kind() is SyntaxKind.ThrowStatement or SyntaxKind.ReturnStatement); + editor.RemoveNode(nextStatement.IsParentKind(SyntaxKind.GlobalStatement) ? nextStatement.GetRequiredParent() : nextStatement); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/ConvertToAsync/CSharpConvertToAsyncMethodCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/ConvertToAsync/CSharpConvertToAsyncMethodCodeFixProvider.cs index 997993e41be79..c5cc94a864422 100644 --- a/src/Analyzers/CSharp/CodeFixes/ConvertToAsync/CSharpConvertToAsyncMethodCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/ConvertToAsync/CSharpConvertToAsyncMethodCodeFixProvider.cs @@ -13,94 +13,93 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.ConvertToAsync +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.ConvertToAsync; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertToAsync), Shared] +internal class CSharpConvertToAsyncMethodCodeFixProvider : AbstractConvertToAsyncCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertToAsync), Shared] - internal class CSharpConvertToAsyncMethodCodeFixProvider : AbstractConvertToAsyncCodeFixProvider + /// + /// Cannot await void. + /// + private const string CS4008 = nameof(CS4008); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpConvertToAsyncMethodCodeFixProvider() + { + } + + public override ImmutableArray FixableDiagnosticIds => [CS4008]; + + protected override async Task GetDescriptionAsync( + Diagnostic diagnostic, + SyntaxNode node, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var methodNode = await GetMethodDeclarationAsync(node, semanticModel, cancellationToken).ConfigureAwait(false); + + // We only call GetDescription when we already know that we succeeded (so it's safe to + // assume we have a methodNode here). + return string.Format(CSharpCodeFixesResources.Make_0_return_Task_instead_of_void, methodNode!.WithBody(null)); + } + + protected override async Task?> GetRootInOtherSyntaxTreeAsync( + SyntaxNode node, + SemanticModel semanticModel, + Diagnostic diagnostic, + CancellationToken cancellationToken) { - /// - /// Cannot await void. - /// - private const string CS4008 = nameof(CS4008); - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpConvertToAsyncMethodCodeFixProvider() + var methodDeclaration = await GetMethodDeclarationAsync(node, semanticModel, cancellationToken).ConfigureAwait(false); + if (methodDeclaration == null) { + return null; } - public override ImmutableArray FixableDiagnosticIds => [CS4008]; + var oldRoot = await methodDeclaration.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = oldRoot.ReplaceNode(methodDeclaration, ConvertToAsyncFunction(methodDeclaration)); + return Tuple.Create(oldRoot.SyntaxTree, newRoot); + } - protected override async Task GetDescriptionAsync( - Diagnostic diagnostic, - SyntaxNode node, - SemanticModel semanticModel, - CancellationToken cancellationToken) + private static async Task GetMethodDeclarationAsync( + SyntaxNode node, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var invocationExpression = node.ChildNodes().FirstOrDefault(n => n.IsKind(SyntaxKind.InvocationExpression)); + if (invocationExpression == null) { - var methodNode = await GetMethodDeclarationAsync(node, semanticModel, cancellationToken).ConfigureAwait(false); + return null; + } - // We only call GetDescription when we already know that we succeeded (so it's safe to - // assume we have a methodNode here). - return string.Format(CSharpCodeFixesResources.Make_0_return_Task_instead_of_void, methodNode!.WithBody(null)); + if (semanticModel.GetSymbolInfo(invocationExpression, cancellationToken).Symbol is not IMethodSymbol methodSymbol) + { + return null; } - protected override async Task?> GetRootInOtherSyntaxTreeAsync( - SyntaxNode node, - SemanticModel semanticModel, - Diagnostic diagnostic, - CancellationToken cancellationToken) + var methodReference = methodSymbol.DeclaringSyntaxReferences.FirstOrDefault(); + if (methodReference == null) { - var methodDeclaration = await GetMethodDeclarationAsync(node, semanticModel, cancellationToken).ConfigureAwait(false); - if (methodDeclaration == null) - { - return null; - } - - var oldRoot = await methodDeclaration.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var newRoot = oldRoot.ReplaceNode(methodDeclaration, ConvertToAsyncFunction(methodDeclaration)); - return Tuple.Create(oldRoot.SyntaxTree, newRoot); + return null; } - private static async Task GetMethodDeclarationAsync( - SyntaxNode node, - SemanticModel semanticModel, - CancellationToken cancellationToken) + if ((await methodReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false)) is not MethodDeclarationSyntax methodDeclaration) { - var invocationExpression = node.ChildNodes().FirstOrDefault(n => n.IsKind(SyntaxKind.InvocationExpression)); - if (invocationExpression == null) - { - return null; - } - - if (semanticModel.GetSymbolInfo(invocationExpression, cancellationToken).Symbol is not IMethodSymbol methodSymbol) - { - return null; - } - - var methodReference = methodSymbol.DeclaringSyntaxReferences.FirstOrDefault(); - if (methodReference == null) - { - return null; - } - - if ((await methodReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false)) is not MethodDeclarationSyntax methodDeclaration) - { - return null; - } - - if (!methodDeclaration.Modifiers.Any(SyntaxKind.AsyncKeyword)) - { - return null; - } - - return methodDeclaration; + return null; } - private static MethodDeclarationSyntax ConvertToAsyncFunction(MethodDeclarationSyntax methodDeclaration) + if (!methodDeclaration.Modifiers.Any(SyntaxKind.AsyncKeyword)) { - return methodDeclaration.WithReturnType( - SyntaxFactory.ParseTypeName("Task") - .WithTriviaFrom(methodDeclaration)); + return null; } + + return methodDeclaration; + } + + private static MethodDeclarationSyntax ConvertToAsyncFunction(MethodDeclarationSyntax methodDeclaration) + { + return methodDeclaration.WithReturnType( + SyntaxFactory.ParseTypeName("Task") + .WithTriviaFrom(methodDeclaration)); } } diff --git a/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/CSharpConvertToRecordCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/CSharpConvertToRecordCodeFixProvider.cs index 52cf1861c0704..c6ba051fba87a 100644 --- a/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/CSharpConvertToRecordCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/CSharpConvertToRecordCodeFixProvider.cs @@ -12,44 +12,43 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.ConvertToRecord +namespace Microsoft.CodeAnalysis.CSharp.ConvertToRecord; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertToRecord), Shared] +internal class CSharpConvertToRecordCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertToRecord), Shared] - internal class CSharpConvertToRecordCodeFixProvider : CodeFixProvider - { - private const string CS8865 = nameof(CS8865); // Only records may inherit from records. + private const string CS8865 = nameof(CS8865); // Only records may inherit from records. - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpConvertToRecordCodeFixProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpConvertToRecordCodeFixProvider() + { + } - public override FixAllProvider? GetFixAllProvider() - => null; + public override FixAllProvider? GetFixAllProvider() + => null; - public override ImmutableArray FixableDiagnosticIds - => [CS8865]; + public override ImmutableArray FixableDiagnosticIds + => [CS8865]; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var span = context.Span; - var cancellationToken = context.CancellationToken; + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var span = context.Span; + var cancellationToken = context.CancellationToken; - // get the class declaration. The span should be on the base type in the base list - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var baseTypeSyntax = root.FindNode(span) as BaseTypeSyntax; + // get the class declaration. The span should be on the base type in the base list + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var baseTypeSyntax = root.FindNode(span) as BaseTypeSyntax; - var typeDeclaration = baseTypeSyntax?.GetAncestor(); - if (typeDeclaration == null) - return; + var typeDeclaration = baseTypeSyntax?.GetAncestor(); + if (typeDeclaration == null) + return; - var action = await ConvertToRecordEngine.GetCodeActionAsync( - document, typeDeclaration, context.GetOptionsProvider(), cancellationToken).ConfigureAwait(false); + var action = await ConvertToRecordEngine.GetCodeActionAsync( + document, typeDeclaration, context.GetOptionsProvider(), cancellationToken).ConfigureAwait(false); - if (action != null) - context.RegisterCodeFix(action, context.Diagnostics); - } + if (action != null) + context.RegisterCodeFix(action, context.Diagnostics); } } diff --git a/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/ConvertToRecordHelpers.cs b/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/ConvertToRecordHelpers.cs index d35aaf94132cb..6bbb4a68f93a3 100644 --- a/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/ConvertToRecordHelpers.cs +++ b/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/ConvertToRecordHelpers.cs @@ -15,1126 +15,1125 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertToRecord +namespace Microsoft.CodeAnalysis.CSharp.ConvertToRecord; + +internal static class ConvertToRecordHelpers { - internal static class ConvertToRecordHelpers + public static bool IsSimpleEqualsMethod( + Compilation compilation, + IMethodSymbol methodSymbol, + IMethodBodyOperation methodBodyOperation, + ImmutableArray expectedComparedFields) { - public static bool IsSimpleEqualsMethod( - Compilation compilation, - IMethodSymbol methodSymbol, - IMethodBodyOperation methodBodyOperation, - ImmutableArray expectedComparedFields) + if (methodSymbol.Name == nameof(Equals) && + methodSymbol.ReturnType.SpecialType == SpecialType.System_Boolean && + methodSymbol.Parameters.IsSingle()) { - if (methodSymbol.Name == nameof(Equals) && - methodSymbol.ReturnType.SpecialType == SpecialType.System_Boolean && - methodSymbol.Parameters.IsSingle()) + var type = methodSymbol.ContainingType; + var equatableType = GetIEquatableType(compilation, type); + if (OverridesEquals(compilation, methodSymbol, equatableType)) { - var type = methodSymbol.ContainingType; - var equatableType = GetIEquatableType(compilation, type); - if (OverridesEquals(compilation, methodSymbol, equatableType)) - { - if (equatableType != null && - methodSymbol.Parameters.First().Type.SpecialType == SpecialType.System_Object && - GetBlockOfMethodBody(methodBodyOperation) is IBlockOperation + if (equatableType != null && + methodSymbol.Parameters.First().Type.SpecialType == SpecialType.System_Object && + GetBlockOfMethodBody(methodBodyOperation) is IBlockOperation + { + Operations: [IReturnOperation { - Operations: [IReturnOperation + ReturnedValue: IInvocationOperation { - ReturnedValue: IInvocationOperation - { - Instance: IInstanceReferenceOperation, - TargetMethod: IMethodSymbol { Name: nameof(Equals) }, - Arguments: [IArgumentOperation { Value: IOperation arg }] - } - }] - } && arg.WalkDownConversion() is IParameterReferenceOperation { Parameter: IParameterSymbol param } - && param.Equals(methodSymbol.Parameters.First())) - { - // in this case where we have an Equals(C? other) from IEquatable but the current one - // is Equals(object? other), we accept something of the form: - // return Equals(other as C); - return true; - } - - // otherwise we check to see which fields are compared (either by themselves or through properties) - var actualFields = GetEqualizedFields(methodBodyOperation, methodSymbol); - return actualFields.SetEquals(expectedComparedFields); + Instance: IInstanceReferenceOperation, + TargetMethod: IMethodSymbol { Name: nameof(Equals) }, + Arguments: [IArgumentOperation { Value: IOperation arg }] + } + }] + } && arg.WalkDownConversion() is IParameterReferenceOperation { Parameter: IParameterSymbol param } + && param.Equals(methodSymbol.Parameters.First())) + { + // in this case where we have an Equals(C? other) from IEquatable but the current one + // is Equals(object? other), we accept something of the form: + // return Equals(other as C); + return true; } - } - return false; + // otherwise we check to see which fields are compared (either by themselves or through properties) + var actualFields = GetEqualizedFields(methodBodyOperation, methodSymbol); + return actualFields.SetEquals(expectedComparedFields); + } } - public static INamedTypeSymbol? GetIEquatableType(Compilation compilation, INamedTypeSymbol containingType) - { - // can't use nameof since it's generic and we need the type parameter - var equatable = compilation.GetBestTypeByMetadataName("System.IEquatable`1")?.Construct(containingType); - return containingType.Interfaces.FirstOrDefault(iface => iface.Equals(equatable)); - } + return false; + } - public static bool IsSimpleHashCodeMethod( - Compilation compilation, - IMethodSymbol methodSymbol, - IMethodBodyOperation methodOperation, - ImmutableArray expectedHashedFields) + public static INamedTypeSymbol? GetIEquatableType(Compilation compilation, INamedTypeSymbol containingType) + { + // can't use nameof since it's generic and we need the type parameter + var equatable = compilation.GetBestTypeByMetadataName("System.IEquatable`1")?.Construct(containingType); + return containingType.Interfaces.FirstOrDefault(iface => iface.Equals(equatable)); + } + + public static bool IsSimpleHashCodeMethod( + Compilation compilation, + IMethodSymbol methodSymbol, + IMethodBodyOperation methodOperation, + ImmutableArray expectedHashedFields) + { + if (methodSymbol.Name == nameof(GetHashCode) && + methodSymbol.Parameters.IsEmpty && + HashCodeAnalyzer.TryGetAnalyzer(compilation, out var analyzer)) { - if (methodSymbol.Name == nameof(GetHashCode) && - methodSymbol.Parameters.IsEmpty && - HashCodeAnalyzer.TryGetAnalyzer(compilation, out var analyzer)) + // Hash Code method, see if it would be a default implementation that we can remove + var (_, members, _) = analyzer.GetHashedMembers( + methodSymbol, methodOperation.BlockBody ?? methodOperation.ExpressionBody); + if (members != default) { - // Hash Code method, see if it would be a default implementation that we can remove - var (_, members, _) = analyzer.GetHashedMembers( - methodSymbol, methodOperation.BlockBody ?? methodOperation.ExpressionBody); - if (members != default) - { - // the user could access a member using either the property or the underlying field - // so anytime they access a property instead of the underlying field we convert it to the - // corresponding underlying field - var actualMembers = members - .SelectAsArray(UnwrapPropertyToField).WhereNotNull().AsImmutable(); + // the user could access a member using either the property or the underlying field + // so anytime they access a property instead of the underlying field we convert it to the + // corresponding underlying field + var actualMembers = members + .SelectAsArray(UnwrapPropertyToField).WhereNotNull().AsImmutable(); - return actualMembers.SetEquals(expectedHashedFields); - } + return actualMembers.SetEquals(expectedHashedFields); } - return false; } + return false; + } - /// - /// Returns true if the method contents match a simple reference to the equals method - /// which would be the compiler generated implementation - /// - public static bool IsDefaultEqualsOperator(IMethodBodyOperation operation) + /// + /// Returns true if the method contents match a simple reference to the equals method + /// which would be the compiler generated implementation + /// + public static bool IsDefaultEqualsOperator(IMethodBodyOperation operation) + { + // must look like + // public static operator ==(C c1, object? c2) + // { + // return c1.Equals(c2); + // } + // or + // public static operator ==(C c1, object? c2) => c1.Equals(c2); + return GetBlockOfMethodBody(operation) is IBlockOperation { - // must look like - // public static operator ==(C c1, object? c2) - // { - // return c1.Equals(c2); - // } - // or - // public static operator ==(C c1, object? c2) => c1.Equals(c2); - return GetBlockOfMethodBody(operation) is IBlockOperation - { - // look for only one operation, a return operation that consists of an equals invocation - Operations: [IReturnOperation { ReturnedValue: IOperation returnedValue }] - } && - IsDotEqualsInvocation(returnedValue); - } + // look for only one operation, a return operation that consists of an equals invocation + Operations: [IReturnOperation { ReturnedValue: IOperation returnedValue }] + } && + IsDotEqualsInvocation(returnedValue); + } - /// - /// Whether the method simply returns !(equals), where "equals" is - /// c1 == c2 or c1.Equals(c2) - /// - internal static bool IsDefaultNotEqualsOperator( - IMethodBodyOperation operation) - { - // looking for: - // return !(operand); - // or: - // => !(operand); - if (GetBlockOfMethodBody(operation) is not IBlockOperation + /// + /// Whether the method simply returns !(equals), where "equals" is + /// c1 == c2 or c1.Equals(c2) + /// + internal static bool IsDefaultNotEqualsOperator( + IMethodBodyOperation operation) + { + // looking for: + // return !(operand); + // or: + // => !(operand); + if (GetBlockOfMethodBody(operation) is not IBlockOperation + { + Operations: [IReturnOperation { - Operations: [IReturnOperation + ReturnedValue: IUnaryOperation { - ReturnedValue: IUnaryOperation - { - OperatorKind: UnaryOperatorKind.Not, - Operand: IOperation operand - } - }] - }) - { - return false; - } + OperatorKind: UnaryOperatorKind.Not, + Operand: IOperation operand + } + }] + }) + { + return false; + } - // check to see if operand is an equals invocation that references the parameters - if (IsDotEqualsInvocation(operand)) - return true; + // check to see if operand is an equals invocation that references the parameters + if (IsDotEqualsInvocation(operand)) + return true; - // we accept an == operator, for example - // return !(obj1 == obj2); - // since this would call our == operator, which would in turn call .Equals (or equivalent) - // but we need to make sure that the operands are parameter references - if (operand is not IBinaryOperation - { - OperatorKind: BinaryOperatorKind.Equals, - LeftOperand: IOperation leftOperand, - RightOperand: IOperation rightOperand, - }) + // we accept an == operator, for example + // return !(obj1 == obj2); + // since this would call our == operator, which would in turn call .Equals (or equivalent) + // but we need to make sure that the operands are parameter references + if (operand is not IBinaryOperation { - return false; - } - - // now we know we have an == comparison, but we want to make sure these actually reference parameters - var left = GetParamFromArgument(leftOperand); - var right = GetParamFromArgument(rightOperand); - // make sure we're not referencing the same parameter twice - return left != null && right != null && !left.Equals(right); + OperatorKind: BinaryOperatorKind.Equals, + LeftOperand: IOperation leftOperand, + RightOperand: IOperation rightOperand, + }) + { + return false; } - /// - /// Matches constructors where each statement simply assigns one of the provided parameters to one of the provided properties - /// with no duplicate assignment or any other type of statement - /// - /// Constructor body - /// Properties expected to be assigned (would be replaced with positional constructor). - /// Will re-order this list to match parameter order if successful. - /// Constructor parameters - /// Whether the constructor body matches the pattern described - public static bool IsSimplePrimaryConstructor( - IConstructorBodyOperation operation, - ImmutableArray properties, - ImmutableArray parameters, - out ImmutableArray orderedProperties) - { - orderedProperties = default; - if (GetBlockOfMethodBody(operation) is not { Operations.Length: int opLength } || - opLength != properties.Length) - { - return false; - } + // now we know we have an == comparison, but we want to make sure these actually reference parameters + var left = GetParamFromArgument(leftOperand); + var right = GetParamFromArgument(rightOperand); + // make sure we're not referencing the same parameter twice + return left != null && right != null && !left.Equals(right); + } - var assignmentValues = GetAssignmentValuesForConstructor(operation, - assignment => (assignment as IParameterReferenceOperation)?.Parameter); + /// + /// Matches constructors where each statement simply assigns one of the provided parameters to one of the provided properties + /// with no duplicate assignment or any other type of statement + /// + /// Constructor body + /// Properties expected to be assigned (would be replaced with positional constructor). + /// Will re-order this list to match parameter order if successful. + /// Constructor parameters + /// Whether the constructor body matches the pattern described + public static bool IsSimplePrimaryConstructor( + IConstructorBodyOperation operation, + ImmutableArray properties, + ImmutableArray parameters, + out ImmutableArray orderedProperties) + { + orderedProperties = default; + if (GetBlockOfMethodBody(operation) is not { Operations.Length: int opLength } || + opLength != properties.Length) + { + return false; + } - // we must assign to all the properties (keys) and use all the parameters (values) - if (!assignmentValues.Keys.SetEquals(properties) || - !assignmentValues.Values.SetEquals(parameters)) - { - return false; - } + var assignmentValues = GetAssignmentValuesForConstructor(operation, + assignment => (assignment as IParameterReferenceOperation)?.Parameter); - // order properties in order of the parameters that they were assigned to - // e.g if we originally have Properties: [int Y, int X] - // and constructor: - // public C(int x, int y) - // { - // X = x; - // Y = y; - // } - // then we would re-order the properties to: [int X, int Y] - orderedProperties = properties - .OrderBy(property => parameters.IndexOf(assignmentValues[property])) - .AsImmutable(); - return true; + // we must assign to all the properties (keys) and use all the parameters (values) + if (!assignmentValues.Keys.SetEquals(properties) || + !assignmentValues.Values.SetEquals(parameters)) + { + return false; } - /// - /// Checks to see if all fields/properties were assigned from the parameter - /// - /// constructor body - /// all instance fields, including backing fields of constructors - /// parameter to copy constructor - public static bool IsSimpleCopyConstructor( - IConstructorBodyOperation operation, - ImmutableArray fields, - IParameterSymbol parameter) + // order properties in order of the parameters that they were assigned to + // e.g if we originally have Properties: [int Y, int X] + // and constructor: + // public C(int x, int y) + // { + // X = x; + // Y = y; + // } + // then we would re-order the properties to: [int X, int Y] + orderedProperties = properties + .OrderBy(property => parameters.IndexOf(assignmentValues[property])) + .AsImmutable(); + return true; + } + + /// + /// Checks to see if all fields/properties were assigned from the parameter + /// + /// constructor body + /// all instance fields, including backing fields of constructors + /// parameter to copy constructor + public static bool IsSimpleCopyConstructor( + IConstructorBodyOperation operation, + ImmutableArray fields, + IParameterSymbol parameter) + { + if (GetBlockOfMethodBody(operation) is not { Operations.Length: int opLength } || + opLength != fields.Length) { - if (GetBlockOfMethodBody(operation) is not { Operations.Length: int opLength } || - opLength != fields.Length) - { - return false; - } + return false; + } - var assignmentValues = GetAssignmentValuesForConstructor(operation, - assignment => assignment switch + var assignmentValues = GetAssignmentValuesForConstructor(operation, + assignment => assignment switch + { + IPropertyReferenceOperation { - IPropertyReferenceOperation - { - Instance: IParameterReferenceOperation { Parameter: IParameterSymbol referencedParameter }, - Property: IPropertySymbol referencedProperty - } => - referencedParameter.Equals(parameter) ? referencedProperty.GetBackingFieldIfAny() : null, - IFieldReferenceOperation - { - Instance: IParameterReferenceOperation { Parameter: IParameterSymbol referencedParameter }, - Field: IFieldSymbol referencedField - } => - referencedParameter.Equals(parameter) ? referencedField : null, - _ => null - }); - - // left hand side of each assignment - var assignedUnderlyingFields = assignmentValues.Keys.SelectAsArray(UnwrapPropertyToField); - - // Each right hand assignment should assign the same property. - // All assigned properties should be equal (in potentially a different order) - // to all the properties we would be moving - return assignedUnderlyingFields.SequenceEqual(assignmentValues.Values) && - assignedUnderlyingFields.SetEquals(fields); - } + Instance: IParameterReferenceOperation { Parameter: IParameterSymbol referencedParameter }, + Property: IPropertySymbol referencedProperty + } => + referencedParameter.Equals(parameter) ? referencedProperty.GetBackingFieldIfAny() : null, + IFieldReferenceOperation + { + Instance: IParameterReferenceOperation { Parameter: IParameterSymbol referencedParameter }, + Field: IFieldSymbol referencedField + } => + referencedParameter.Equals(parameter) ? referencedField : null, + _ => null + }); + + // left hand side of each assignment + var assignedUnderlyingFields = assignmentValues.Keys.SelectAsArray(UnwrapPropertyToField); - /// - /// Given a non-primary, non-copy constructor, get expressions that are assigned to - /// primary constructor properties via simple assignment. - /// - /// The constructor body operation - /// the primary constructor parameters - /// - /// Expressions that were assigned to a primary constructor property in the constructor, - /// or default/null if there wasn't an assignment found. Returned in order of primary parameters. - /// - /// - /// Example (assume we decided on positional parameters int Foo, bool Bar, int Baz): - /// - /// public C(int foo, bool bar) - /// { - /// Bar = bar; - /// Foo = foo; - /// Mumble = 0; - /// } - /// - /// we would return: [foo, bar, default] - /// where foo and bar are the nodes in the assignment, and default is factory constructed. - /// - public static ImmutableArray GetAssignmentValuesForNonPrimaryConstructor( - IConstructorBodyOperation operation, - ImmutableArray positionalParams) + // Each right hand assignment should assign the same property. + // All assigned properties should be equal (in potentially a different order) + // to all the properties we would be moving + return assignedUnderlyingFields.SequenceEqual(assignmentValues.Values) && + assignedUnderlyingFields.SetEquals(fields); + } + + /// + /// Given a non-primary, non-copy constructor, get expressions that are assigned to + /// primary constructor properties via simple assignment. + /// + /// The constructor body operation + /// the primary constructor parameters + /// + /// Expressions that were assigned to a primary constructor property in the constructor, + /// or default/null if there wasn't an assignment found. Returned in order of primary parameters. + /// + /// + /// Example (assume we decided on positional parameters int Foo, bool Bar, int Baz): + /// + /// public C(int foo, bool bar) + /// { + /// Bar = bar; + /// Foo = foo; + /// Mumble = 0; + /// } + /// + /// we would return: [foo, bar, default] + /// where foo and bar are the nodes in the assignment, and default is factory constructed. + /// + public static ImmutableArray GetAssignmentValuesForNonPrimaryConstructor( + IConstructorBodyOperation operation, + ImmutableArray positionalParams) + { + // make sure the assignment wouldn't reference local variables we may have declared + var assignmentValues = GetAssignmentValuesForConstructor(operation, + assignment => IsSafeAssignment(assignment) + ? assignment.Syntax as ExpressionSyntax + : null); + + if (operation.Initializer is + IInvocationOperation { Arguments: ImmutableArray args }) { - // make sure the assignment wouldn't reference local variables we may have declared - var assignmentValues = GetAssignmentValuesForConstructor(operation, - assignment => IsSafeAssignment(assignment) - ? assignment.Syntax as ExpressionSyntax - : null); - - if (operation.Initializer is - IInvocationOperation { Arguments: ImmutableArray args }) + var additionalValuesBuilder = assignmentValues.ToBuilder(); + // in a "base" or "this" initializer + foreach (var arg in args) { - var additionalValuesBuilder = assignmentValues.ToBuilder(); - // in a "base" or "this" initializer - foreach (var arg in args) + if (arg is { Parameter: IParameterSymbol param, Value.Syntax: ExpressionSyntax captured }) { - if (arg is { Parameter: IParameterSymbol param, Value.Syntax: ExpressionSyntax captured }) + // We're looking to see if this initializer is a primary constructor, + // i.e. the parameters are declared as auto-implemented properties in the record definition. + // Since there's no way to associate positional parameters (from the primary constructor) + // to the properties that they declare other than by comparing their declaration locations, + // we have to do this rather convoluted comparison. + // Note: We can use AssociatedSymbol once this is implemented: + // https://github.com/dotnet/roslyn/issues/54286 + var positionalParam = param.ContainingSymbol.ContainingType.GetMembers().FirstOrDefault(member + => member.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() == + param.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax()); + if (positionalParam is IPropertySymbol property) { - // We're looking to see if this initializer is a primary constructor, - // i.e. the parameters are declared as auto-implemented properties in the record definition. - // Since there's no way to associate positional parameters (from the primary constructor) - // to the properties that they declare other than by comparing their declaration locations, - // we have to do this rather convoluted comparison. - // Note: We can use AssociatedSymbol once this is implemented: - // https://github.com/dotnet/roslyn/issues/54286 - var positionalParam = param.ContainingSymbol.ContainingType.GetMembers().FirstOrDefault(member - => member.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() == - param.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax()); - if (positionalParam is IPropertySymbol property) + if (additionalValuesBuilder.ContainsKey(property)) { - if (additionalValuesBuilder.ContainsKey(property)) - { - // don't allow assignment to the same property more than once - return []; - } - - additionalValuesBuilder.Add(property, captured); + // don't allow assignment to the same property more than once + return []; } + + additionalValuesBuilder.Add(property, captured); } } - - assignmentValues = additionalValuesBuilder.ToImmutable(); } - var expressions = GetAssignmentExpressionsFromValuesMap(positionalParams, assignmentValues); - - return expressions; + assignmentValues = additionalValuesBuilder.ToImmutable(); } - /// - /// Given an object creation with a block initializer and a parameterless constructor declaration, - /// finds values that were assigned to the primary constructor parameters - /// - /// Object creation expression operation - /// primary constructor parameters - /// - /// values that were assigned to primary constructor parameters, in order of the passed in primary constructor - /// - /// - /// Example (assume we decided on positional parameters int Foo, bool Bar, int Baz): - /// - /// var c = new C - /// { - /// Bar = true; - /// Foo = 10; - /// Mumble = 0; - /// }; - /// - /// We would return [10, true, default] - /// where 10 and true are the actual nodes and default was a constructed node - /// - public static ImmutableArray GetAssignmentValuesFromObjectCreation( - IObjectCreationOperation operation, - ImmutableArray positionalParams) - { - // we want to be very careful about when we refactor because if the user has a constructor - // and initializer it could be what they intend. Since we gave initializers to all non-primary - // constructors they already have, any calls to an explicit constructor with additional block initialization - // still work absolutely fine. Further, we can't necessarily associate their constructor args to - // primary constructor args or any other constructor args. Therefore, - // the only time we want to actually make a change is if they use the default no-param constructor, - // and a block initializer. - if (operation is IObjectCreationOperation - { - Arguments: ImmutableArray { IsEmpty: true }, - Initializer: IObjectOrCollectionInitializerOperation initializer, - Constructor: IMethodSymbol { IsImplicitlyDeclared: true } - }) + var expressions = GetAssignmentExpressionsFromValuesMap(positionalParams, assignmentValues); + + return expressions; + } + + /// + /// Given an object creation with a block initializer and a parameterless constructor declaration, + /// finds values that were assigned to the primary constructor parameters + /// + /// Object creation expression operation + /// primary constructor parameters + /// + /// values that were assigned to primary constructor parameters, in order of the passed in primary constructor + /// + /// + /// Example (assume we decided on positional parameters int Foo, bool Bar, int Baz): + /// + /// var c = new C + /// { + /// Bar = true; + /// Foo = 10; + /// Mumble = 0; + /// }; + /// + /// We would return [10, true, default] + /// where 10 and true are the actual nodes and default was a constructed node + /// + public static ImmutableArray GetAssignmentValuesFromObjectCreation( + IObjectCreationOperation operation, + ImmutableArray positionalParams) + { + // we want to be very careful about when we refactor because if the user has a constructor + // and initializer it could be what they intend. Since we gave initializers to all non-primary + // constructors they already have, any calls to an explicit constructor with additional block initialization + // still work absolutely fine. Further, we can't necessarily associate their constructor args to + // primary constructor args or any other constructor args. Therefore, + // the only time we want to actually make a change is if they use the default no-param constructor, + // and a block initializer. + if (operation is IObjectCreationOperation { - var dictionaryBuilder = ImmutableDictionary.Empty.ToBuilder(); + Arguments: ImmutableArray { IsEmpty: true }, + Initializer: IObjectOrCollectionInitializerOperation initializer, + Constructor: IMethodSymbol { IsImplicitlyDeclared: true } + }) + { + var dictionaryBuilder = ImmutableDictionary.Empty.ToBuilder(); - foreach (var assignment in initializer.Initializers) - { - if (assignment is ISimpleAssignmentOperation - { - Target: IPropertyReferenceOperation { Property: IPropertySymbol property }, - Value: IOperation { Syntax: ExpressionSyntax syntax } - }) + foreach (var assignment in initializer.Initializers) + { + if (assignment is ISimpleAssignmentOperation { - dictionaryBuilder.Add(property, syntax); - } + Target: IPropertyReferenceOperation { Property: IPropertySymbol property }, + Value: IOperation { Syntax: ExpressionSyntax syntax } + }) + { + dictionaryBuilder.Add(property, syntax); } - - var expressions = GetAssignmentExpressionsFromValuesMap(positionalParams, dictionaryBuilder.ToImmutable()); - - return expressions; } - // no initializer or uses explicit constructor, no need to make a change - return []; + var expressions = GetAssignmentExpressionsFromValuesMap(positionalParams, dictionaryBuilder.ToImmutable()); + + return expressions; } - private static ImmutableArray GetAssignmentExpressionsFromValuesMap( - ImmutableArray positionalParams, - ImmutableDictionary assignmentValues) - => positionalParams.SelectAsArray(property => + // no initializer or uses explicit constructor, no need to make a change + return []; + } + + private static ImmutableArray GetAssignmentExpressionsFromValuesMap( + ImmutableArray positionalParams, + ImmutableDictionary assignmentValues) + => positionalParams.SelectAsArray(property => + { + if (assignmentValues.ContainsKey(property)) { - if (assignmentValues.ContainsKey(property)) - { - return assignmentValues[property]; - } - else - { - return SyntaxFactory.LiteralExpression( - property.Type.NullableAnnotation == NullableAnnotation.Annotated - ? SyntaxKind.NullLiteralExpression - : SyntaxKind.DefaultLiteralExpression); - } - }); + return assignmentValues[property]; + } + else + { + return SyntaxFactory.LiteralExpression( + property.Type.NullableAnnotation == NullableAnnotation.Annotated + ? SyntaxKind.NullLiteralExpression + : SyntaxKind.DefaultLiteralExpression); + } + }); + + private static ImmutableDictionary GetAssignmentValuesForConstructor( + IConstructorBodyOperation constructorOperation, + Func captureAssignedSymbol) + { + var body = GetBlockOfMethodBody(constructorOperation); + var dictionaryBuilder = ImmutableDictionary.Empty.ToBuilder(); - private static ImmutableDictionary GetAssignmentValuesForConstructor( - IConstructorBodyOperation constructorOperation, - Func captureAssignedSymbol) + // We expect the constructor to have exactly one statement per property, + // where the statement is a simple assignment from the parameter's property to the property + if (body == null) { - var body = GetBlockOfMethodBody(constructorOperation); - var dictionaryBuilder = ImmutableDictionary.Empty.ToBuilder(); + return ImmutableDictionary.Empty; + } - // We expect the constructor to have exactly one statement per property, - // where the statement is a simple assignment from the parameter's property to the property - if (body == null) + foreach (var operation in body.Operations) + { + if (operation is IExpressionStatementOperation + { + Operation: ISimpleAssignmentOperation + { + Target: IOperation assignee, + Value: IOperation assignment + } + } && + captureAssignedSymbol(assignment) is T captured) { - return ImmutableDictionary.Empty; - } + ISymbol? symbol = assignee switch + { + IFieldReferenceOperation + { Instance: IInstanceReferenceOperation, Field: IFieldSymbol field } + => field, + IPropertyReferenceOperation + { Instance: IInstanceReferenceOperation, Property: IPropertySymbol property } + => property, + _ => null, + }; - foreach (var operation in body.Operations) - { - if (operation is IExpressionStatementOperation - { - Operation: ISimpleAssignmentOperation - { - Target: IOperation assignee, - Value: IOperation assignment - } - } && - captureAssignedSymbol(assignment) is T captured) + if (symbol != null) { - ISymbol? symbol = assignee switch - { - IFieldReferenceOperation - { Instance: IInstanceReferenceOperation, Field: IFieldSymbol field } - => field, - IPropertyReferenceOperation - { Instance: IInstanceReferenceOperation, Property: IPropertySymbol property } - => property, - _ => null, - }; - - if (symbol != null) + if (dictionaryBuilder.ContainsKey(symbol)) { - if (dictionaryBuilder.ContainsKey(symbol)) - { - // don't allow assignment to the same property more than once - return ImmutableDictionary.Empty; - } - - dictionaryBuilder.Add(symbol, captured); + // don't allow assignment to the same property more than once + return ImmutableDictionary.Empty; } + + dictionaryBuilder.Add(symbol, captured); } } - - return dictionaryBuilder.ToImmutable(); } - /// - /// Determines whether the operation is safe to move into the "this(...)" initializer - /// i.e. Doesn't reference any other created variables but the parameters - /// - private static bool IsSafeAssignment(IOperation operation) - { - if (operation is ILocalReferenceOperation) - { - return false; - } + return dictionaryBuilder.ToImmutable(); + } - return operation.ChildOperations.All(IsSafeAssignment); + /// + /// Determines whether the operation is safe to move into the "this(...)" initializer + /// i.e. Doesn't reference any other created variables but the parameters + /// + private static bool IsSafeAssignment(IOperation operation) + { + if (operation is ILocalReferenceOperation) + { + return false; } - /// - /// Get all the fields (including implicit fields underlying properties) that this - /// equals method compares - /// - /// - /// the symbol of the equals method - /// - private static ImmutableArray GetEqualizedFields( - IMethodBodyOperation operation, - IMethodSymbol methodSymbol) - { - var type = methodSymbol.ContainingType; + return operation.ChildOperations.All(IsSafeAssignment); + } - var body = GetBlockOfMethodBody(operation); + /// + /// Get all the fields (including implicit fields underlying properties) that this + /// equals method compares + /// + /// + /// the symbol of the equals method + /// + private static ImmutableArray GetEqualizedFields( + IMethodBodyOperation operation, + IMethodSymbol methodSymbol) + { + var type = methodSymbol.ContainingType; - if (body == null || !methodSymbol.Parameters.IsSingle()) - return []; + var body = GetBlockOfMethodBody(operation); - var bodyOps = body.Operations; - var parameter = methodSymbol.Parameters.First(); - using var _1 = ArrayBuilder.GetInstance(out var fields); - ISymbol? otherC = null; - IEnumerable? statementsToCheck = null; + if (body == null || !methodSymbol.Parameters.IsSingle()) + return []; + + var bodyOps = body.Operations; + var parameter = methodSymbol.Parameters.First(); + using var _1 = ArrayBuilder.GetInstance(out var fields); + ISymbol? otherC = null; + IEnumerable? statementsToCheck = null; - // see whether we are calling on a param of the same type or of object - if (parameter.Type.Equals(type)) + // see whether we are calling on a param of the same type or of object + if (parameter.Type.Equals(type)) + { + // we need to check all the statements, and we already have the + // variable that is used to access the members + otherC = parameter; + statementsToCheck = bodyOps; + } + else if (parameter.Type.SpecialType == SpecialType.System_Object) + { + // we could look for some cast which rebinds the parameter + // to a local of the type such as any of the following: + // var otherc = other as C; *null and additional equality checks* + // if (other is C otherc) { *additional equality checks* } (optional else) return false; + // if (other is not C otherc) { return false; } (optional else) { *additional equality checks* } + // if (other is C) { otherc = (C) other; *additional equality checks* } (optional else) return false; + // if (other is not C) { return false; } (optional else) { otherc = (C) other; *additional equality checks* } + // return other is C otherC && ... + // return !(other is not C otherC || ... + + // check for single return operation which binds the variable as the first condition in a sequence + if (bodyOps is [IReturnOperation { ReturnedValue: IOperation value }] && + TryAddEqualizedFieldsForConditionWithoutTypedVariable( + value, successRequirement: true, type, fields, out var _2)) { - // we need to check all the statements, and we already have the - // variable that is used to access the members - otherC = parameter; - statementsToCheck = bodyOps; + // we're done, no more statements to check + return fields.ToImmutable(); } - else if (parameter.Type.SpecialType == SpecialType.System_Object) + // check for the first statement as an explicit cast to a variable declaration + // like: var otherC = other as C; + else if (TryGetAssignmentFromParameterWithExplicitCast(bodyOps.FirstOrDefault(), parameter, out otherC)) { - // we could look for some cast which rebinds the parameter - // to a local of the type such as any of the following: - // var otherc = other as C; *null and additional equality checks* - // if (other is C otherc) { *additional equality checks* } (optional else) return false; - // if (other is not C otherc) { return false; } (optional else) { *additional equality checks* } - // if (other is C) { otherc = (C) other; *additional equality checks* } (optional else) return false; - // if (other is not C) { return false; } (optional else) { otherc = (C) other; *additional equality checks* } - // return other is C otherC && ... - // return !(other is not C otherC || ... - - // check for single return operation which binds the variable as the first condition in a sequence - if (bodyOps is [IReturnOperation { ReturnedValue: IOperation value }] && - TryAddEqualizedFieldsForConditionWithoutTypedVariable( - value, successRequirement: true, type, fields, out var _2)) - { - // we're done, no more statements to check - return fields.ToImmutable(); - } - // check for the first statement as an explicit cast to a variable declaration - // like: var otherC = other as C; - else if (TryGetAssignmentFromParameterWithExplicitCast(bodyOps.FirstOrDefault(), parameter, out otherC)) - { - // ignore the first statement as we just ensured it was a cast - statementsToCheck = bodyOps.Skip(1); - } - // check for the first statement as an if statement where the cast check occurs - // and a variable assignment happens (either in the if or in a following statement) - else if (!TryGetBindingCastInFirstIfStatement(bodyOps, parameter, type, fields, out otherC, out statementsToCheck)) - { - return []; - } + // ignore the first statement as we just ensured it was a cast + statementsToCheck = bodyOps.Skip(1); } - - if (otherC == null || statementsToCheck == null || - !TryAddEqualizedFieldsForStatements(statementsToCheck, otherC, type, fields)) + // check for the first statement as an if statement where the cast check occurs + // and a variable assignment happens (either in the if or in a following statement) + else if (!TryGetBindingCastInFirstIfStatement(bodyOps, parameter, type, fields, out otherC, out statementsToCheck)) { - // no patterns matched to bind variable or statements didn't match expectation return []; } - - return fields.ToImmutable(); } - /// - /// Matches: var otherC = (C) other; - /// or: var otherC = other as C; - /// - private static bool TryGetAssignmentFromParameterWithExplicitCast( - IOperation? operation, - IParameterSymbol parameter, - [NotNullWhen(true)] out ISymbol? assignedVariable) + if (otherC == null || statementsToCheck == null || + !TryAddEqualizedFieldsForStatements(statementsToCheck, otherC, type, fields)) { - assignedVariable = null; - if (operation is IVariableDeclarationGroupOperation + // no patterns matched to bind variable or statements didn't match expectation + return []; + } + + return fields.ToImmutable(); + } + + /// + /// Matches: var otherC = (C) other; + /// or: var otherC = other as C; + /// + private static bool TryGetAssignmentFromParameterWithExplicitCast( + IOperation? operation, + IParameterSymbol parameter, + [NotNullWhen(true)] out ISymbol? assignedVariable) + { + assignedVariable = null; + if (operation is IVariableDeclarationGroupOperation + { + Declarations: [IVariableDeclarationOperation { - Declarations: [IVariableDeclarationOperation + Declarators: [IVariableDeclaratorOperation { - Declarators: [IVariableDeclaratorOperation + Symbol: ILocalSymbol castOther, + Initializer: IVariableInitializerOperation { - Symbol: ILocalSymbol castOther, - Initializer: IVariableInitializerOperation + Value: IConversionOperation { - Value: IConversionOperation + IsImplicit: false, + Operand: IParameterReferenceOperation { - IsImplicit: false, - Operand: IParameterReferenceOperation - { - Parameter: IParameterSymbol referencedParameter1 - } + Parameter: IParameterSymbol referencedParameter1 } } - }] + } }] - } && referencedParameter1.Equals(parameter)) - { - assignedVariable = castOther; - return true; - } - - return false; + }] + } && referencedParameter1.Equals(parameter)) + { + assignedVariable = castOther; + return true; } - /// - /// Get the referenced parameter (and unwraps implicit cast if necessary) or null if a parameter wasn't referenced - /// - /// The operation for which to get the parameter - /// the referenced parameter or null if unable to find - private static IParameterSymbol? GetParamFromArgument(IOperation operation) - => (operation.WalkDownConversion() as IParameterReferenceOperation)?.Parameter; + return false; + } + + /// + /// Get the referenced parameter (and unwraps implicit cast if necessary) or null if a parameter wasn't referenced + /// + /// The operation for which to get the parameter + /// the referenced parameter or null if unable to find + private static IParameterSymbol? GetParamFromArgument(IOperation operation) + => (operation.WalkDownConversion() as IParameterReferenceOperation)?.Parameter; - private static ISymbol? GetReferencedSymbolObject(IOperation reference) + private static ISymbol? GetReferencedSymbolObject(IOperation reference) + { + return reference.WalkDownConversion() switch { - return reference.WalkDownConversion() switch + IInstanceReferenceOperation thisReference => thisReference.Type, + ILocalReferenceOperation localReference => localReference.Local, + IParameterReferenceOperation paramReference => paramReference.Parameter, + _ => null, + }; + } + + /// + /// matches form: + /// c1.Equals(c2) + /// where c1 and c2 are parameter references + /// + private static bool IsDotEqualsInvocation(IOperation operation) + { + // must be called on one of the parameters + if (operation is not IInvocationOperation { - IInstanceReferenceOperation thisReference => thisReference.Type, - ILocalReferenceOperation localReference => localReference.Local, - IParameterReferenceOperation paramReference => paramReference.Parameter, - _ => null, - }; + TargetMethod.Name: nameof(Equals), + Instance: IOperation instance, + Arguments: [IArgumentOperation { Value: IOperation arg }] + }) + { + return false; } - /// - /// matches form: - /// c1.Equals(c2) - /// where c1 and c2 are parameter references - /// - private static bool IsDotEqualsInvocation(IOperation operation) - { - // must be called on one of the parameters - if (operation is not IInvocationOperation - { - TargetMethod.Name: nameof(Equals), - Instance: IOperation instance, - Arguments: [IArgumentOperation { Value: IOperation arg }] - }) - { - return false; - } + // get the (potential) parameters, uwrapping any potential implicit casts + var invokedOn = GetParamFromArgument(instance); + var param = GetParamFromArgument(arg); + // make sure we're not referencing the same parameter twice + return param != null && invokedOn != null && !invokedOn.Equals(param); + } - // get the (potential) parameters, uwrapping any potential implicit casts - var invokedOn = GetParamFromArgument(instance); - var param = GetParamFromArgument(arg); - // make sure we're not referencing the same parameter twice - return param != null && invokedOn != null && !invokedOn.Equals(param); + /// + /// checks for binary expressions of the type otherC == null or null == otherC + /// or a pattern against null like otherC is (not) null + /// and "otherC" is a reference to otherObject. + /// + /// Operation to check for + /// if we're in a context where the operation evaluating to true + /// would end up being false within the equals method, we look for != instead + /// Object to be compared to null + private static bool IsNullCheck( + IOperation operation, + bool successRequirement, + ISymbol otherObject) + { + if (operation is IBinaryOperation binOp) + { + // if success would return true, then we want the checked object to not be null + // so we expect a notEquals operator + var expectedKind = successRequirement + ? BinaryOperatorKind.NotEquals + : BinaryOperatorKind.Equals; + + return binOp.OperatorKind == expectedKind && + // one of the objects must be a reference to the "otherObject" + // and the other must be a constant null literal + AreConditionsSatisfiedEitherOrder(binOp.LeftOperand, binOp.RightOperand, + op => op.WalkDownConversion().IsNullLiteral(), + op => otherObject.Equals(GetReferencedSymbolObject(op))); } - - /// - /// checks for binary expressions of the type otherC == null or null == otherC - /// or a pattern against null like otherC is (not) null - /// and "otherC" is a reference to otherObject. - /// - /// Operation to check for - /// if we're in a context where the operation evaluating to true - /// would end up being false within the equals method, we look for != instead - /// Object to be compared to null - private static bool IsNullCheck( - IOperation operation, - bool successRequirement, - ISymbol otherObject) + else if (operation is IIsPatternOperation patternOp) { - if (operation is IBinaryOperation binOp) + // matches: otherC is null + // or: otherC is not null + // based on successRequirement + IConstantPatternOperation? constantPattern; + if (successRequirement) { - // if success would return true, then we want the checked object to not be null - // so we expect a notEquals operator - var expectedKind = successRequirement - ? BinaryOperatorKind.NotEquals - : BinaryOperatorKind.Equals; - - return binOp.OperatorKind == expectedKind && - // one of the objects must be a reference to the "otherObject" - // and the other must be a constant null literal - AreConditionsSatisfiedEitherOrder(binOp.LeftOperand, binOp.RightOperand, - op => op.WalkDownConversion().IsNullLiteral(), - op => otherObject.Equals(GetReferencedSymbolObject(op))); + constantPattern = (patternOp.Pattern as INegatedPatternOperation)?. + Pattern as IConstantPatternOperation; } - else if (operation is IIsPatternOperation patternOp) + else { - // matches: otherC is null - // or: otherC is not null - // based on successRequirement - IConstantPatternOperation? constantPattern; - if (successRequirement) - { - constantPattern = (patternOp.Pattern as INegatedPatternOperation)?. - Pattern as IConstantPatternOperation; - } - else - { - constantPattern = patternOp.Pattern as IConstantPatternOperation; - } - - return constantPattern != null && - otherObject.Equals(GetReferencedSymbolObject(patternOp.Value)) && - constantPattern.Value.WalkDownConversion().IsNullLiteral(); + constantPattern = patternOp.Pattern as IConstantPatternOperation; } - // neither of the expected forms - return false; + return constantPattern != null && + otherObject.Equals(GetReferencedSymbolObject(patternOp.Value)) && + constantPattern.Value.WalkDownConversion().IsNullLiteral(); } - private static bool ReturnsFalseImmediately(IEnumerable operation) - { - return operation.FirstOrDefault() is IReturnOperation - { - ReturnedValue: ILiteralOperation - { - ConstantValue.HasValue: true, - ConstantValue.Value: false, - } - }; - } + // neither of the expected forms + return false; + } - /// - /// looks just at conditional expressions such as "A == other.A && B == other.B..." - /// To determine which members were accessed and compared - /// - /// Condition to look at, should be a boolean expression - /// Whether to look for operators that would indicate equality success - /// (==, .Equals, &&) or inequality operators (!=, ||) - /// Symbol that would be referenced with this - /// symbol representing other object, either from a param or cast as a local - /// Builder to add members to - /// true if addition was successful, false if we see something odd - /// (equality checking in the wrong order, side effects, etc) - private static bool TryAddEqualizedFieldsForCondition( - IOperation condition, - bool successRequirement, - ISymbol currentObject, - ISymbol otherObject, - ArrayBuilder builder) - => (successRequirement, condition) switch + private static bool ReturnsFalseImmediately(IEnumerable operation) + { + return operation.FirstOrDefault() is IReturnOperation { - // if we see a not we want to invert the current success status - // e.g !(A != other.A || B != other.B) is equivalent to (A == other.A && B == other.B) - // using DeMorgans law. We recurse to see any members accessed - (_, IUnaryOperation { OperatorKind: UnaryOperatorKind.Not, Operand: IOperation newCondition }) - => TryAddEqualizedFieldsForCondition(newCondition, !successRequirement, currentObject, otherObject, builder), - // We want our equality check to be exhaustive, i.e. all checks must pass for the condition to pass - // we recurse into each operand to try to find some props to bind - (true, IBinaryOperation { OperatorKind: BinaryOperatorKind.ConditionalAnd } andOp) - => TryAddEqualizedFieldsForCondition(andOp.LeftOperand, successRequirement, currentObject, otherObject, builder) && - TryAddEqualizedFieldsForCondition(andOp.RightOperand, successRequirement, currentObject, otherObject, builder), - // Exhaustive binary operator for inverted checks via DeMorgan's law - // We see an or here, but we're in a context where this being true will return false - // for example: return !(expr || expr) - // or: if (expr || expr) return false; - (false, IBinaryOperation { OperatorKind: BinaryOperatorKind.ConditionalOr } orOp) - => TryAddEqualizedFieldsForCondition(orOp.LeftOperand, successRequirement, currentObject, otherObject, builder) && - TryAddEqualizedFieldsForCondition(orOp.RightOperand, successRequirement, currentObject, otherObject, builder), - // we are actually comparing two values that are potentially members, - // e.g: return A == other.A; - (true, IBinaryOperation - { - OperatorKind: BinaryOperatorKind.Equals, - LeftOperand: IMemberReferenceOperation leftMemberReference, - RightOperand: IMemberReferenceOperation rightMemberReference, - }) => TryAddFieldFromComparison(leftMemberReference, rightMemberReference, currentObject, otherObject, builder), - // we are comparing two potential members, but in a context where if the expression is true, we return false - // e.g: return !(A != other.A); - (false, IBinaryOperation + ReturnedValue: ILiteralOperation { - OperatorKind: BinaryOperatorKind.NotEquals, - LeftOperand: IMemberReferenceOperation leftMemberReference, - RightOperand: IMemberReferenceOperation rightMemberReference, - }) => TryAddFieldFromComparison(leftMemberReference, rightMemberReference, currentObject, otherObject, builder), - // equals invocation, something like: A.Equals(other.A) - (true, IInvocationOperation - { - TargetMethod.Name: nameof(Equals), - Instance: IMemberReferenceOperation invokedOn, - Arguments: [IMemberReferenceOperation arg] - }) => TryAddFieldFromComparison(invokedOn, arg, currentObject, otherObject, builder), - // some other operation, or an incorrect operation (!= when we expect == based on context, etc). - // If one of the conditions is just a null check on the "otherObject", then it's valid but doesn't check any members - // Otherwise we fail as it has unknown behavior - _ => IsNullCheck(condition, successRequirement, otherObject) + ConstantValue.HasValue: true, + ConstantValue.Value: false, + } }; + } - /// - /// Same as but we're looking for - /// a variable binding through an "is" pattern first/> - /// - /// the cast parameter symbol if found, null if not - private static bool TryAddEqualizedFieldsForConditionWithoutTypedVariable( - IOperation condition, - bool successRequirement, - ISymbol currentObject, - ArrayBuilder builder, - [NotNullWhen(true)] out ISymbol? boundVariable, - IEnumerable? additionalConditions = null) + /// + /// looks just at conditional expressions such as "A == other.A && B == other.B..." + /// To determine which members were accessed and compared + /// + /// Condition to look at, should be a boolean expression + /// Whether to look for operators that would indicate equality success + /// (==, .Equals, &&) or inequality operators (!=, ||) + /// Symbol that would be referenced with this + /// symbol representing other object, either from a param or cast as a local + /// Builder to add members to + /// true if addition was successful, false if we see something odd + /// (equality checking in the wrong order, side effects, etc) + private static bool TryAddEqualizedFieldsForCondition( + IOperation condition, + bool successRequirement, + ISymbol currentObject, + ISymbol otherObject, + ArrayBuilder builder) + => (successRequirement, condition) switch + { + // if we see a not we want to invert the current success status + // e.g !(A != other.A || B != other.B) is equivalent to (A == other.A && B == other.B) + // using DeMorgans law. We recurse to see any members accessed + (_, IUnaryOperation { OperatorKind: UnaryOperatorKind.Not, Operand: IOperation newCondition }) + => TryAddEqualizedFieldsForCondition(newCondition, !successRequirement, currentObject, otherObject, builder), + // We want our equality check to be exhaustive, i.e. all checks must pass for the condition to pass + // we recurse into each operand to try to find some props to bind + (true, IBinaryOperation { OperatorKind: BinaryOperatorKind.ConditionalAnd } andOp) + => TryAddEqualizedFieldsForCondition(andOp.LeftOperand, successRequirement, currentObject, otherObject, builder) && + TryAddEqualizedFieldsForCondition(andOp.RightOperand, successRequirement, currentObject, otherObject, builder), + // Exhaustive binary operator for inverted checks via DeMorgan's law + // We see an or here, but we're in a context where this being true will return false + // for example: return !(expr || expr) + // or: if (expr || expr) return false; + (false, IBinaryOperation { OperatorKind: BinaryOperatorKind.ConditionalOr } orOp) + => TryAddEqualizedFieldsForCondition(orOp.LeftOperand, successRequirement, currentObject, otherObject, builder) && + TryAddEqualizedFieldsForCondition(orOp.RightOperand, successRequirement, currentObject, otherObject, builder), + // we are actually comparing two values that are potentially members, + // e.g: return A == other.A; + (true, IBinaryOperation { - boundVariable = null; - additionalConditions ??= []; - return (successRequirement, condition) switch + OperatorKind: BinaryOperatorKind.Equals, + LeftOperand: IMemberReferenceOperation leftMemberReference, + RightOperand: IMemberReferenceOperation rightMemberReference, + }) => TryAddFieldFromComparison(leftMemberReference, rightMemberReference, currentObject, otherObject, builder), + // we are comparing two potential members, but in a context where if the expression is true, we return false + // e.g: return !(A != other.A); + (false, IBinaryOperation + { + OperatorKind: BinaryOperatorKind.NotEquals, + LeftOperand: IMemberReferenceOperation leftMemberReference, + RightOperand: IMemberReferenceOperation rightMemberReference, + }) => TryAddFieldFromComparison(leftMemberReference, rightMemberReference, currentObject, otherObject, builder), + // equals invocation, something like: A.Equals(other.A) + (true, IInvocationOperation + { + TargetMethod.Name: nameof(Equals), + Instance: IMemberReferenceOperation invokedOn, + Arguments: [IMemberReferenceOperation arg] + }) => TryAddFieldFromComparison(invokedOn, arg, currentObject, otherObject, builder), + // some other operation, or an incorrect operation (!= when we expect == based on context, etc). + // If one of the conditions is just a null check on the "otherObject", then it's valid but doesn't check any members + // Otherwise we fail as it has unknown behavior + _ => IsNullCheck(condition, successRequirement, otherObject) + }; + + /// + /// Same as but we're looking for + /// a variable binding through an "is" pattern first/> + /// + /// the cast parameter symbol if found, null if not + private static bool TryAddEqualizedFieldsForConditionWithoutTypedVariable( + IOperation condition, + bool successRequirement, + ISymbol currentObject, + ArrayBuilder builder, + [NotNullWhen(true)] out ISymbol? boundVariable, + IEnumerable? additionalConditions = null) + { + boundVariable = null; + additionalConditions ??= []; + return (successRequirement, condition) switch + { + (_, IUnaryOperation { OperatorKind: UnaryOperatorKind.Not, Operand: IOperation newCondition }) + => TryAddEqualizedFieldsForConditionWithoutTypedVariable( + newCondition, + !successRequirement, + currentObject, + builder, + out boundVariable, + additionalConditions), + (true, IBinaryOperation { - (_, IUnaryOperation { OperatorKind: UnaryOperatorKind.Not, Operand: IOperation newCondition }) - => TryAddEqualizedFieldsForConditionWithoutTypedVariable( - newCondition, - !successRequirement, - currentObject, - builder, - out boundVariable, - additionalConditions), - (true, IBinaryOperation - { - OperatorKind: BinaryOperatorKind.ConditionalAnd, - LeftOperand: IOperation leftOperation, - RightOperand: IOperation rightOperation, - }) => TryAddEqualizedFieldsForConditionWithoutTypedVariable( - leftOperation, - successRequirement, - currentObject, - builder, - out boundVariable, - additionalConditions.Append(rightOperation)), - (false, IBinaryOperation - { - OperatorKind: BinaryOperatorKind.ConditionalOr, - LeftOperand: IOperation leftOperation, - RightOperand: IOperation rightOperation, - }) => TryAddEqualizedFieldsForConditionWithoutTypedVariable( + OperatorKind: BinaryOperatorKind.ConditionalAnd, + LeftOperand: IOperation leftOperation, + RightOperand: IOperation rightOperation, + }) => TryAddEqualizedFieldsForConditionWithoutTypedVariable( leftOperation, successRequirement, currentObject, builder, out boundVariable, additionalConditions.Append(rightOperation)), - (_, IIsPatternOperation - { - Pattern: IPatternOperation isPattern - }) => TryGetBoundVariableForIsPattern(isPattern, out boundVariable), - _ => false, - }; + (false, IBinaryOperation + { + OperatorKind: BinaryOperatorKind.ConditionalOr, + LeftOperand: IOperation leftOperation, + RightOperand: IOperation rightOperation, + }) => TryAddEqualizedFieldsForConditionWithoutTypedVariable( + leftOperation, + successRequirement, + currentObject, + builder, + out boundVariable, + additionalConditions.Append(rightOperation)), + (_, IIsPatternOperation + { + Pattern: IPatternOperation isPattern + }) => TryGetBoundVariableForIsPattern(isPattern, out boundVariable), + _ => false, + }; - bool TryGetBoundVariableForIsPattern(IPatternOperation isPattern, [NotNullWhen(true)] out ISymbol? boundVariable) + bool TryGetBoundVariableForIsPattern(IPatternOperation isPattern, [NotNullWhen(true)] out ISymbol? boundVariable) + { + boundVariable = null; + // got to the leftmost condition and it is an "is" pattern + if (!successRequirement) { - boundVariable = null; - // got to the leftmost condition and it is an "is" pattern - if (!successRequirement) + // we could be in an "expect false for successful equality" condition + // and so we would want the pattern to be an "is not" pattern instead of an "is" pattern + if (isPattern is INegatedPatternOperation negatedPattern) { - // we could be in an "expect false for successful equality" condition - // and so we would want the pattern to be an "is not" pattern instead of an "is" pattern - if (isPattern is INegatedPatternOperation negatedPattern) - { - isPattern = negatedPattern.Pattern; - } - else - { - // if we don't see "is not" then the pattern sequence is incorrect - return false; - } + isPattern = negatedPattern.Pattern; } - - if (isPattern is IDeclarationPatternOperation - { - DeclaredSymbol: ISymbol otherVar, - MatchedType: INamedTypeSymbol matchedType, - } && - matchedType.Equals(currentObject.GetSymbolType()) && - // found the correct binding, add any members we equate in the rest of the binary condition - // if we were in a binary condition at all, and signal failure if any condition is bad - additionalConditions.All(otherCondition => TryAddEqualizedFieldsForCondition( - otherCondition, successRequirement, currentObject, otherVar, builder))) + else { - boundVariable = otherVar; - return true; + // if we don't see "is not" then the pattern sequence is incorrect + return false; } - return false; } - } - /// - /// Match a list of statements and add members that are compared - /// - /// operations which should compare members - /// non-this comparison of the type we're equating - /// the this symbol - /// builder for property/field comparisons we encounter - /// whether every statment was one of the expected forms - private static bool TryAddEqualizedFieldsForStatements( - IEnumerable statementsToCheck, - ISymbol otherC, - INamedTypeSymbol type, - ArrayBuilder builder) - => statementsToCheck.FirstOrDefault() switch - { - IReturnOperation - { - ReturnedValue: ILiteralOperation - { - ConstantValue.HasValue: true, - ConstantValue.Value: true, - } - } - // we are done with the comparison, the final statment does no checks - => true, - IReturnOperation { ReturnedValue: IOperation value } => TryAddEqualizedFieldsForCondition( - value, successRequirement: true, currentObject: type, otherObject: otherC, builder: builder), - IConditionalOperation + if (isPattern is IDeclarationPatternOperation { - Condition: IOperation condition, - WhenTrue: IOperation whenTrue, - WhenFalse: var whenFalse, - } - // 1. Check structure of if statment, get success requirement - // and any potential statments in the non failure block - // 2. Check condition for compared members - // 3. Check remaining members in non failure block - => TryGetSuccessCondition(whenTrue, whenFalse, statementsToCheck.Skip(1), - out var successRequirement, out var remainingStatements) && - TryAddEqualizedFieldsForCondition( - condition, successRequirement, type, otherC, builder) && - TryAddEqualizedFieldsForStatements(remainingStatements, otherC, type, builder), - _ => false - }; - - private static bool TryAddFieldFromComparison( - IMemberReferenceOperation memberReference1, - IMemberReferenceOperation memberReference2, - ISymbol currentObject, - ISymbol otherObject, - ArrayBuilder builder) - { - var leftObject = GetReferencedSymbolObject(memberReference1.Instance!); - var rightObject = GetReferencedSymbolObject(memberReference2.Instance!); - if (memberReference1.Member.Equals(memberReference2.Member) && - leftObject != null && - rightObject != null && - !leftObject.Equals(rightObject) && - AreConditionsSatisfiedEitherOrder(leftObject, rightObject, currentObject.Equals, otherObject.Equals)) + DeclaredSymbol: ISymbol otherVar, + MatchedType: INamedTypeSymbol matchedType, + } && + matchedType.Equals(currentObject.GetSymbolType()) && + // found the correct binding, add any members we equate in the rest of the binary condition + // if we were in a binary condition at all, and signal failure if any condition is bad + additionalConditions.All(otherCondition => TryAddEqualizedFieldsForCondition( + otherCondition, successRequirement, currentObject, otherVar, builder))) { - var field = UnwrapPropertyToField(memberReference1.Member); - if (field == null) - // not a field or no backing field for property so member is invalid - return false; - - builder.Add(field); + boundVariable = otherVar; return true; } return false; } + } - /// - /// Matches a pattern where the first statement is an if statement that ensures a cast - /// of the parameter to the correct type, and either binds it through an "is" pattern - /// or later assigns it to a local varaiable - /// - /// method body to search in - /// uncast object parameter - /// type which is being called - /// members that may have been incidentally checked - /// if matched, the variable that the cast parameter was assigned to - /// remaining non-check, non-assignment operations - /// to look for additional compared members. This can have statements even if there was no binding - /// as long as it found an if check that checks the correct type - /// whether or not the pattern matched - private static bool TryGetBindingCastInFirstIfStatement( - ImmutableArray bodyOps, - IParameterSymbol parameter, - INamedTypeSymbol type, - ArrayBuilder builder, - [NotNullWhen(true)] out ISymbol? otherC, - [NotNullWhen(true)] out IEnumerable? statementsToCheck) + /// + /// Match a list of statements and add members that are compared + /// + /// operations which should compare members + /// non-this comparison of the type we're equating + /// the this symbol + /// builder for property/field comparisons we encounter + /// whether every statment was one of the expected forms + private static bool TryAddEqualizedFieldsForStatements( + IEnumerable statementsToCheck, + ISymbol otherC, + INamedTypeSymbol type, + ArrayBuilder builder) + => statementsToCheck.FirstOrDefault() switch { - otherC = null; - statementsToCheck = null; - - // we require the if statement (with or without a cast) to be the first operation in the body - if (bodyOps.FirstOrDefault() is not IConditionalOperation - { - Condition: IOperation condition, - WhenTrue: IOperation whenTrue, - WhenFalse: var whenFalse, - }) + IReturnOperation { - return false; - } - - // find out if we return false after the condition is true or false and get the statements - // corresponding to the other branch - if (!TryGetSuccessCondition( - whenTrue, whenFalse, bodyOps.Skip(1).AsImmutable(), out var successRequirement, out var remainingStatments)) - { - return false; - } - - // if we have no else block, we could get no remaining statements, in that case we take all the - // statments after the if condition operation - statementsToCheck = !remainingStatments.IsEmpty() ? remainingStatments : bodyOps.Skip(1); - - // checks for simple "is" or "is not" statement without a variable binding - ITypeSymbol? testType = null; - IParameterSymbol? referencedParameter = null; - if (successRequirement) - { - if (condition is IIsTypeOperation typeCondition) + ReturnedValue: ILiteralOperation { - testType = typeCondition.TypeOperand; - referencedParameter = (typeCondition.ValueOperand as IParameterReferenceOperation)?.Parameter; + ConstantValue.HasValue: true, + ConstantValue.Value: true, } } - else + // we are done with the comparison, the final statment does no checks + => true, + IReturnOperation { ReturnedValue: IOperation value } => TryAddEqualizedFieldsForCondition( + value, successRequirement: true, currentObject: type, otherObject: otherC, builder: builder), + IConditionalOperation { - if (condition is IIsPatternOperation - { - Value: IParameterReferenceOperation parameterReference, - Pattern: INegatedPatternOperation - { - Pattern: ITypePatternOperation typePattern - } - }) - { - testType = typePattern.MatchedType; - referencedParameter = parameterReference.Parameter; - } + Condition: IOperation condition, + WhenTrue: IOperation whenTrue, + WhenFalse: var whenFalse, } + // 1. Check structure of if statment, get success requirement + // and any potential statments in the non failure block + // 2. Check condition for compared members + // 3. Check remaining members in non failure block + => TryGetSuccessCondition(whenTrue, whenFalse, statementsToCheck.Skip(1), + out var successRequirement, out var remainingStatements) && + TryAddEqualizedFieldsForCondition( + condition, successRequirement, type, otherC, builder) && + TryAddEqualizedFieldsForStatements(remainingStatements, otherC, type, builder), + _ => false + }; - if (testType != null && referencedParameter != null && - testType.Equals(type) && referencedParameter.Equals(parameter)) - { - // found correct pattern/type check, so we know we have something equivalent to - // if (other is C) { ... } else return false; - // we look for an explicit cast to assign a variable like: - // var otherC = (C)other; - // var otherC = other as C; - if (TryGetAssignmentFromParameterWithExplicitCast(statementsToCheck.FirstOrDefault(), parameter, out otherC)) - { - statementsToCheck = statementsToCheck.Skip(1); - return true; - } - + private static bool TryAddFieldFromComparison( + IMemberReferenceOperation memberReference1, + IMemberReferenceOperation memberReference2, + ISymbol currentObject, + ISymbol otherObject, + ArrayBuilder builder) + { + var leftObject = GetReferencedSymbolObject(memberReference1.Instance!); + var rightObject = GetReferencedSymbolObject(memberReference2.Instance!); + if (memberReference1.Member.Equals(memberReference2.Member) && + leftObject != null && + rightObject != null && + !leftObject.Equals(rightObject) && + AreConditionsSatisfiedEitherOrder(leftObject, rightObject, currentObject.Equals, otherObject.Equals)) + { + var field = UnwrapPropertyToField(memberReference1.Member); + if (field == null) + // not a field or no backing field for property so member is invalid return false; - } - // look for the condition to also contain a binding to a variable and optionally additional - // checks based on that assigned variable - return TryAddEqualizedFieldsForConditionWithoutTypedVariable( - condition, successRequirement, type, builder, out otherC); + + builder.Add(field); + return true; } + return false; + } + + /// + /// Matches a pattern where the first statement is an if statement that ensures a cast + /// of the parameter to the correct type, and either binds it through an "is" pattern + /// or later assigns it to a local varaiable + /// + /// method body to search in + /// uncast object parameter + /// type which is being called + /// members that may have been incidentally checked + /// if matched, the variable that the cast parameter was assigned to + /// remaining non-check, non-assignment operations + /// to look for additional compared members. This can have statements even if there was no binding + /// as long as it found an if check that checks the correct type + /// whether or not the pattern matched + private static bool TryGetBindingCastInFirstIfStatement( + ImmutableArray bodyOps, + IParameterSymbol parameter, + INamedTypeSymbol type, + ArrayBuilder builder, + [NotNullWhen(true)] out ISymbol? otherC, + [NotNullWhen(true)] out IEnumerable? statementsToCheck) + { + otherC = null; + statementsToCheck = null; - /// - /// Attempts to get information about an if operation in an equals method, - /// such as whether the condition being true would cause the method to return false - /// and the remaining statments in the branch not returning false (if any) - /// - /// "then" branch - /// "else" branch (if any) - /// whether the condition being true would cause the method to return false - /// or the condition being false would cause the method to return false - /// Potential remaining statements of the branch that does not return false - /// whether the pattern was matched (one of the branches must have a simple "return false") - private static bool TryGetSuccessCondition( - IOperation whenTrue, - IOperation? whenFalse, - IEnumerable otherOps, - out bool successRequirement, - out IEnumerable remainingStatements) + // we require the if statement (with or without a cast) to be the first operation in the body + if (bodyOps.FirstOrDefault() is not IConditionalOperation + { + Condition: IOperation condition, + WhenTrue: IOperation whenTrue, + WhenFalse: var whenFalse, + }) { - // this will be changed if we successfully match the pattern - successRequirement = default; - // this could be empty even if we match, if there is no else block - remainingStatements = []; - - // all the operations that would happen after the condition is true or false - // branches can either be block bodies or single statements - // each branch is followed by statements outside the branch either way - var trueOps = ((whenTrue as IBlockOperation)?.Operations ?? [whenTrue]) - .Concat(otherOps); - var falseOps = ((whenFalse as IBlockOperation)?.Operations ?? - (whenFalse != null - ? [whenFalse] - : ImmutableArray.Empty)) - .Concat(otherOps); - - // We expect one of the true or false branch to have exactly one statement: return false. - // If we don't find that, it indicates complex behavior such as - // extra statments, backup equality if one condition fails, or something else. - // We don't necessarily expect a return true because we could see - // a final return statement that checks a last set of conditions such as: - // if (other is C otherC) - // { - // return otherC.A == A; - // } - // return false; - // We should never have a case where both branches could potentially return true; - // at least one branch must simply return false. - if (ReturnsFalseImmediately(trueOps) == ReturnsFalseImmediately(falseOps)) - // both or neither fit the return false pattern, this if statement either doesn't do - // anything or does something too complex for us, so we assume it's not a default. - return false; + return false; + } - // when condition succeeds (evaluates to true), we return false - // so for equality the condition should not succeed - successRequirement = !ReturnsFalseImmediately(trueOps); - remainingStatements = successRequirement ? trueOps : falseOps; - return true; + // find out if we return false after the condition is true or false and get the statements + // corresponding to the other branch + if (!TryGetSuccessCondition( + whenTrue, whenFalse, bodyOps.Skip(1).AsImmutable(), out var successRequirement, out var remainingStatments)) + { + return false; } - /// - /// Whether the equals method overrides object or IEquatable Equals method - /// - private static bool OverridesEquals(Compilation compilation, IMethodSymbol equals, INamedTypeSymbol? equatableType) + // if we have no else block, we could get no remaining statements, in that case we take all the + // statments after the if condition operation + statementsToCheck = !remainingStatments.IsEmpty() ? remainingStatments : bodyOps.Skip(1); + + // checks for simple "is" or "is not" statement without a variable binding + ITypeSymbol? testType = null; + IParameterSymbol? referencedParameter = null; + if (successRequirement) { - if (equatableType != null && - equatableType.GetMembers(nameof(Equals)).FirstOrDefault() is IMethodSymbol equatableEquals && - equals.Equals(equals.ContainingType.FindImplementationForInterfaceMember(equatableEquals))) + if (condition is IIsTypeOperation typeCondition) { - return true; + testType = typeCondition.TypeOperand; + referencedParameter = (typeCondition.ValueOperand as IParameterReferenceOperation)?.Parameter; } + } + else + { + if (condition is IIsPatternOperation + { + Value: IParameterReferenceOperation parameterReference, + Pattern: INegatedPatternOperation + { + Pattern: ITypePatternOperation typePattern + } + }) + { + testType = typePattern.MatchedType; + referencedParameter = parameterReference.Parameter; + } + } - var objectType = compilation.GetSpecialType(SpecialType.System_Object); - var objectEquals = objectType?.GetMembers(nameof(Equals)).FirstOrDefault() as IMethodSymbol; - var curr = equals; - while (curr != null) + if (testType != null && referencedParameter != null && + testType.Equals(type) && referencedParameter.Equals(parameter)) + { + // found correct pattern/type check, so we know we have something equivalent to + // if (other is C) { ... } else return false; + // we look for an explicit cast to assign a variable like: + // var otherC = (C)other; + // var otherC = other as C; + if (TryGetAssignmentFromParameterWithExplicitCast(statementsToCheck.FirstOrDefault(), parameter, out otherC)) { - if (curr.Equals(objectEquals)) - return true; - curr = curr.OverriddenMethod; + statementsToCheck = statementsToCheck.Skip(1); + return true; } return false; } + // look for the condition to also contain a binding to a variable and optionally additional + // checks based on that assigned variable + return TryAddEqualizedFieldsForConditionWithoutTypedVariable( + condition, successRequirement, type, builder, out otherC); + } - private static IBlockOperation? GetBlockOfMethodBody(IMethodBodyBaseOperation body) - => body.BlockBody ?? body.ExpressionBody; + /// + /// Attempts to get information about an if operation in an equals method, + /// such as whether the condition being true would cause the method to return false + /// and the remaining statments in the branch not returning false (if any) + /// + /// "then" branch + /// "else" branch (if any) + /// whether the condition being true would cause the method to return false + /// or the condition being false would cause the method to return false + /// Potential remaining statements of the branch that does not return false + /// whether the pattern was matched (one of the branches must have a simple "return false") + private static bool TryGetSuccessCondition( + IOperation whenTrue, + IOperation? whenFalse, + IEnumerable otherOps, + out bool successRequirement, + out IEnumerable remainingStatements) + { + // this will be changed if we successfully match the pattern + successRequirement = default; + // this could be empty even if we match, if there is no else block + remainingStatements = []; + + // all the operations that would happen after the condition is true or false + // branches can either be block bodies or single statements + // each branch is followed by statements outside the branch either way + var trueOps = ((whenTrue as IBlockOperation)?.Operations ?? [whenTrue]) + .Concat(otherOps); + var falseOps = ((whenFalse as IBlockOperation)?.Operations ?? + (whenFalse != null + ? [whenFalse] + : ImmutableArray.Empty)) + .Concat(otherOps); + + // We expect one of the true or false branch to have exactly one statement: return false. + // If we don't find that, it indicates complex behavior such as + // extra statments, backup equality if one condition fails, or something else. + // We don't necessarily expect a return true because we could see + // a final return statement that checks a last set of conditions such as: + // if (other is C otherC) + // { + // return otherC.A == A; + // } + // return false; + // We should never have a case where both branches could potentially return true; + // at least one branch must simply return false. + if (ReturnsFalseImmediately(trueOps) == ReturnsFalseImmediately(falseOps)) + // both or neither fit the return false pattern, this if statement either doesn't do + // anything or does something too complex for us, so we assume it's not a default. + return false; - private static IFieldSymbol? UnwrapPropertyToField(ISymbol propertyOrField) - => propertyOrField switch - { - IPropertySymbol prop => prop.GetBackingFieldIfAny(), - IFieldSymbol field => field, - _ => null - }; + // when condition succeeds (evaluates to true), we return false + // so for equality the condition should not succeed + successRequirement = !ReturnsFalseImmediately(trueOps); + remainingStatements = successRequirement ? trueOps : falseOps; + return true; + } + + /// + /// Whether the equals method overrides object or IEquatable Equals method + /// + private static bool OverridesEquals(Compilation compilation, IMethodSymbol equals, INamedTypeSymbol? equatableType) + { + if (equatableType != null && + equatableType.GetMembers(nameof(Equals)).FirstOrDefault() is IMethodSymbol equatableEquals && + equals.Equals(equals.ContainingType.FindImplementationForInterfaceMember(equatableEquals))) + { + return true; + } - private static bool AreConditionsSatisfiedEitherOrder(T firstItem, T secondItem, - Func firstCondition, Func secondCondition) + var objectType = compilation.GetSpecialType(SpecialType.System_Object); + var objectEquals = objectType?.GetMembers(nameof(Equals)).FirstOrDefault() as IMethodSymbol; + var curr = equals; + while (curr != null) { - return (firstCondition(firstItem) && secondCondition(secondItem)) - || (firstCondition(secondItem) && secondCondition(firstItem)); + if (curr.Equals(objectEquals)) + return true; + curr = curr.OverriddenMethod; } + + return false; + } + + private static IBlockOperation? GetBlockOfMethodBody(IMethodBodyBaseOperation body) + => body.BlockBody ?? body.ExpressionBody; + + private static IFieldSymbol? UnwrapPropertyToField(ISymbol propertyOrField) + => propertyOrField switch + { + IPropertySymbol prop => prop.GetBackingFieldIfAny(), + IFieldSymbol field => field, + _ => null + }; + + private static bool AreConditionsSatisfiedEitherOrder(T firstItem, T secondItem, + Func firstCondition, Func secondCondition) + { + return (firstCondition(firstItem) && secondCondition(secondItem)) + || (firstCondition(secondItem) && secondCondition(firstItem)); } } diff --git a/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/PositionalParameterInfo.cs b/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/PositionalParameterInfo.cs index 90908b3136967..67af4f91edf23 100644 --- a/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/PositionalParameterInfo.cs +++ b/src/Analyzers/CSharp/CodeFixes/ConvertToRecord/PositionalParameterInfo.cs @@ -12,212 +12,211 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertToRecord +namespace Microsoft.CodeAnalysis.CSharp.ConvertToRecord; + +/// +/// Represents a property that should be added as a positional parameter +/// +/// Original declaration, null iff IsInherited is true +/// Null iff is true +/// Symbol of the property +/// Whether we should keep the original declaration present +internal record PositionalParameterInfo( + PropertyDeclarationSyntax? Declaration, + IPropertySymbol Symbol, + bool KeepAsOverride) { /// - /// Represents a property that should be added as a positional parameter + /// Whether this property is inherited from another base record /// - /// Original declaration, null iff IsInherited is true - /// Null iff is true - /// Symbol of the property - /// Whether we should keep the original declaration present - internal record PositionalParameterInfo( - PropertyDeclarationSyntax? Declaration, - IPropertySymbol Symbol, - bool KeepAsOverride) + [MemberNotNullWhen(false, nameof(Declaration))] + public bool IsInherited => Declaration == null; + + public static ImmutableArray GetPropertiesForPositionalParameters( + ImmutableArray properties, + INamedTypeSymbol type, + SemanticModel semanticModel, + CancellationToken cancellationToken) { - /// - /// Whether this property is inherited from another base record - /// - [MemberNotNullWhen(false, nameof(Declaration))] - public bool IsInherited => Declaration == null; - - public static ImmutableArray GetPropertiesForPositionalParameters( - ImmutableArray properties, - INamedTypeSymbol type, - SemanticModel semanticModel, - CancellationToken cancellationToken) - { - using var _ = ArrayBuilder.GetInstance(out var resultBuilder); + using var _ = ArrayBuilder.GetInstance(out var resultBuilder); - // get all declared property symbols, put inherited property symbols first - var symbols = properties.SelectAsArray(p => semanticModel.GetRequiredDeclaredSymbol(p, cancellationToken)); + // get all declared property symbols, put inherited property symbols first + var symbols = properties.SelectAsArray(p => semanticModel.GetRequiredDeclaredSymbol(p, cancellationToken)); - // add inherited properties from a potential base record first - var inheritedProperties = GetInheritedPositionalParams(type, cancellationToken); - resultBuilder.AddRange(inheritedProperties.Select(property => new PositionalParameterInfo( - Declaration: null, - property, - KeepAsOverride: false))); + // add inherited properties from a potential base record first + var inheritedProperties = GetInheritedPositionalParams(type, cancellationToken); + resultBuilder.AddRange(inheritedProperties.Select(property => new PositionalParameterInfo( + Declaration: null, + property, + KeepAsOverride: false))); -            // The user may not know about init or be converting code from before init was introduced. - // In this case we can convert set properties to init ones - var allowSetToInitConversion = !symbols - .Any(symbol => symbol.SetMethod is IMethodSymbol { IsInitOnly: true }); +        // The user may not know about init or be converting code from before init was introduced. + // In this case we can convert set properties to init ones + var allowSetToInitConversion = !symbols + .Any(symbol => symbol.SetMethod is IMethodSymbol { IsInitOnly: true }); - resultBuilder.AddRange(properties.Zip(symbols, (syntax, symbol) - => ShouldConvertProperty(syntax, symbol, type) switch - { - ConvertStatus.DoNotConvert => null, - ConvertStatus.Override - => new PositionalParameterInfo(syntax, symbol, KeepAsOverride: true), - ConvertStatus.OverrideIfConvertingSetToInit - => new PositionalParameterInfo(syntax, symbol, !allowSetToInitConversion), - ConvertStatus.AlwaysConvert - => new PositionalParameterInfo(syntax, symbol, KeepAsOverride: false), - _ => throw ExceptionUtilities.Unreachable(), - }).WhereNotNull()); - - return resultBuilder.ToImmutable(); - } - - public static ImmutableArray GetInheritedPositionalParams( - INamedTypeSymbol currentType, - CancellationToken cancellationToken) - { - var baseType = currentType.BaseType; - if (baseType != null && baseType.TryGetPrimaryConstructor(out var basePrimary)) + resultBuilder.AddRange(properties.Zip(symbols, (syntax, symbol) + => ShouldConvertProperty(syntax, symbol, type) switch { - return basePrimary.Parameters - .Select(param => param.GetAssociatedSynthesizedRecordProperty(cancellationToken)) - .WhereNotNull() - .AsImmutable(); - } + ConvertStatus.DoNotConvert => null, + ConvertStatus.Override + => new PositionalParameterInfo(syntax, symbol, KeepAsOverride: true), + ConvertStatus.OverrideIfConvertingSetToInit + => new PositionalParameterInfo(syntax, symbol, !allowSetToInitConversion), + ConvertStatus.AlwaysConvert + => new PositionalParameterInfo(syntax, symbol, KeepAsOverride: false), + _ => throw ExceptionUtilities.Unreachable(), + }).WhereNotNull()); + + return resultBuilder.ToImmutable(); + } - return []; + public static ImmutableArray GetInheritedPositionalParams( + INamedTypeSymbol currentType, + CancellationToken cancellationToken) + { + var baseType = currentType.BaseType; + if (baseType != null && baseType.TryGetPrimaryConstructor(out var basePrimary)) + { + return basePrimary.Parameters + .Select(param => param.GetAssociatedSynthesizedRecordProperty(cancellationToken)) + .WhereNotNull() + .AsImmutable(); } + return []; + } + + /// + /// for each property, say whether we can convert + /// to primary constructor parameter or not (and whether it would imply changes) + /// + private enum ConvertStatus + { + /// + /// no way we can convert this + /// + DoNotConvert, + /// + /// we can convert this because we feel it would be used in a primary constructor, + /// but some accessibility is non-default and we want to override + /// + Override, + /// + /// we can convert this if we see that the user only ever uses set (not init) + /// otherwise we should give an override + /// + OverrideIfConvertingSetToInit, /// - /// for each property, say whether we can convert - /// to primary constructor parameter or not (and whether it would imply changes) + /// we can convert this without changing the meaning /// - private enum ConvertStatus + AlwaysConvert + } + + private static ConvertStatus ShouldConvertProperty( + PropertyDeclarationSyntax property, + IPropertySymbol propertySymbol, + INamedTypeSymbol containingType) + { + // properties with identifiers or expression bodies are too complex to move. + // unimplemented or static properties shouldn't be in a constructor. + if (property.Initializer != null || + property.ExpressionBody != null || + propertySymbol.IsAbstract || + propertySymbol.IsStatic) { - /// - /// no way we can convert this - /// - DoNotConvert, - /// - /// we can convert this because we feel it would be used in a primary constructor, - /// but some accessibility is non-default and we want to override - /// - Override, - /// - /// we can convert this if we see that the user only ever uses set (not init) - /// otherwise we should give an override - /// - OverrideIfConvertingSetToInit, - /// - /// we can convert this without changing the meaning - /// - AlwaysConvert + return ConvertStatus.DoNotConvert; } - private static ConvertStatus ShouldConvertProperty( - PropertyDeclarationSyntax property, - IPropertySymbol propertySymbol, - INamedTypeSymbol containingType) + // more restrictive than internal (protected, private, private protected, or unspecified (private by default)). + // We allow internal props to be converted to public auto-generated ones + // because it's still as accessible as a constructor would be from outside the class. + if (propertySymbol.DeclaredAccessibility < Accessibility.Internal) { - // properties with identifiers or expression bodies are too complex to move. - // unimplemented or static properties shouldn't be in a constructor. - if (property.Initializer != null || - property.ExpressionBody != null || - propertySymbol.IsAbstract || - propertySymbol.IsStatic) - { - return ConvertStatus.DoNotConvert; - } + return ConvertStatus.DoNotConvert; + } - // more restrictive than internal (protected, private, private protected, or unspecified (private by default)). - // We allow internal props to be converted to public auto-generated ones - // because it's still as accessible as a constructor would be from outside the class. - if (propertySymbol.DeclaredAccessibility < Accessibility.Internal) - { - return ConvertStatus.DoNotConvert; - } + // no accessors declared + if (property.AccessorList == null || property.AccessorList.Accessors.IsEmpty()) + { + return ConvertStatus.DoNotConvert; + } + + // When we convert to primary constructor parameters, the auto-generated properties have default behavior + // Here are the cases where we wouldn't substantially change default behavior + // - No accessors can have any explicit implementation or modifiers + // - This is because it would indicate complex functionality or explicit hiding which is not default + // - class records and readonly struct records must have: + // - public get accessor + // - optionally a public init accessor + // - note: if this is not provided the user can still initialize the property in the constructor, + // so it's like init but without the user ability to initialize outside the constructor + // - for non-readonly structs, we must have: + // - public get accessor + // - public set accessor + // If the user has a private/protected set method, it could still make sense to be a primary constructor + // but we should provide the override in case the user sets the property from within the class + var getAccessor = propertySymbol.GetMethod; + var setAccessor = propertySymbol.SetMethod; + var accessors = property.AccessorList.Accessors; + if (accessors.Any(a => a.Body != null || a.ExpressionBody != null) || + getAccessor == null || + // private get means they probably don't want it seen from the constructor + getAccessor.DeclaredAccessibility < Accessibility.Internal) + { + return ConvertStatus.DoNotConvert; + } - // no accessors declared - if (property.AccessorList == null || property.AccessorList.Accessors.IsEmpty()) + // we consider a internal (by default) get on an internal property as public + // but if the user specifically declares a more restrictive accessibility + // it would indicate they want to keep it safer than the rest of the property + // and we should respect that + if (getAccessor.DeclaredAccessibility < propertySymbol.DeclaredAccessibility) + { + return ConvertStatus.Override; + } + + if (containingType.TypeKind == TypeKind.Struct && !containingType.IsReadOnly) + { + // in a struct, our default is to have a public set + // but anything else we can still convert and override + if (setAccessor == null) { - return ConvertStatus.DoNotConvert; + return ConvertStatus.Override; } - // When we convert to primary constructor parameters, the auto-generated properties have default behavior - // Here are the cases where we wouldn't substantially change default behavior - // - No accessors can have any explicit implementation or modifiers - // - This is because it would indicate complex functionality or explicit hiding which is not default - // - class records and readonly struct records must have: - // - public get accessor - // - optionally a public init accessor - // - note: if this is not provided the user can still initialize the property in the constructor, - // so it's like init but without the user ability to initialize outside the constructor - // - for non-readonly structs, we must have: - // - public get accessor - // - public set accessor - // If the user has a private/protected set method, it could still make sense to be a primary constructor - // but we should provide the override in case the user sets the property from within the class - var getAccessor = propertySymbol.GetMethod; - var setAccessor = propertySymbol.SetMethod; - var accessors = property.AccessorList.Accessors; - if (accessors.Any(a => a.Body != null || a.ExpressionBody != null) || - getAccessor == null || - // private get means they probably don't want it seen from the constructor - getAccessor.DeclaredAccessibility < Accessibility.Internal) + // if the user had their property as internal then we are fine with completely moving + // an internal (by default) set method, but if they explicitly mark the set as internal + // while the property is public we want to keep that behavior + if (setAccessor.DeclaredAccessibility != Accessibility.Public && + setAccessor.DeclaredAccessibility != propertySymbol.DeclaredAccessibility) { - return ConvertStatus.DoNotConvert; + return ConvertStatus.Override; } - // we consider a internal (by default) get on an internal property as public - // but if the user specifically declares a more restrictive accessibility - // it would indicate they want to keep it safer than the rest of the property - // and we should respect that - if (getAccessor.DeclaredAccessibility < propertySymbol.DeclaredAccessibility) + if (setAccessor.IsInitOnly) { return ConvertStatus.Override; } - - if (containingType.TypeKind == TypeKind.Struct && !containingType.IsReadOnly) + } + else + { + // either we are a class or readonly struct, the default is no set or init only set + if (setAccessor != null) { - // in a struct, our default is to have a public set - // but anything else we can still convert and override - if (setAccessor == null) - { - return ConvertStatus.Override; - } - - // if the user had their property as internal then we are fine with completely moving - // an internal (by default) set method, but if they explicitly mark the set as internal - // while the property is public we want to keep that behavior if (setAccessor.DeclaredAccessibility != Accessibility.Public && - setAccessor.DeclaredAccessibility != propertySymbol.DeclaredAccessibility) + setAccessor.DeclaredAccessibility != propertySymbol.DeclaredAccessibility) { return ConvertStatus.Override; } - if (setAccessor.IsInitOnly) + if (!setAccessor.IsInitOnly) { - return ConvertStatus.Override; + return ConvertStatus.OverrideIfConvertingSetToInit; } } - else - { - // either we are a class or readonly struct, the default is no set or init only set - if (setAccessor != null) - { - if (setAccessor.DeclaredAccessibility != Accessibility.Public && - setAccessor.DeclaredAccessibility != propertySymbol.DeclaredAccessibility) - { - return ConvertStatus.Override; - } - - if (!setAccessor.IsInitOnly) - { - return ConvertStatus.OverrideIfConvertingSetToInit; - } - } - } - - return ConvertStatus.AlwaysConvert; } + + return ConvertStatus.AlwaysConvert; } } diff --git a/src/Analyzers/CSharp/CodeFixes/ConvertTypeOfToNameOf/CSharpConvertTypeOfToNameOfCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/ConvertTypeOfToNameOf/CSharpConvertTypeOfToNameOfCodeFixProvider.cs index 4467de88057ab..715736b25f3a1 100644 --- a/src/Analyzers/CSharp/CodeFixes/ConvertTypeOfToNameOf/CSharpConvertTypeOfToNameOfCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/ConvertTypeOfToNameOf/CSharpConvertTypeOfToNameOfCodeFixProvider.cs @@ -12,28 +12,27 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertTypeOfToNameOf +namespace Microsoft.CodeAnalysis.CSharp.ConvertTypeOfToNameOf; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertTypeOfToNameOf), Shared] +internal class CSharpConvertTypeOfToNameOfCodeFixProvider : AbstractConvertTypeOfToNameOfCodeFixProvider< + MemberAccessExpressionSyntax> { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertTypeOfToNameOf), Shared] - internal class CSharpConvertTypeOfToNameOfCodeFixProvider : AbstractConvertTypeOfToNameOfCodeFixProvider< - MemberAccessExpressionSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertTypeOfToNameOfCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertTypeOfToNameOfCodeFixProvider() - { - } + } - protected override string GetCodeFixTitle() - => CSharpCodeFixesResources.Convert_typeof_to_nameof; + protected override string GetCodeFixTitle() + => CSharpCodeFixesResources.Convert_typeof_to_nameof; - protected override SyntaxNode GetSymbolTypeExpression(SemanticModel model, MemberAccessExpressionSyntax node, CancellationToken cancellationToken) - { - // The corresponding analyzer (CSharpConvertTypeOfToNameOfDiagnosticAnalyzer) validated the syntax - var typeOfExpression = (TypeOfExpressionSyntax)node.Expression; - var typeSymbol = model.GetSymbolInfo(typeOfExpression.Type, cancellationToken).Symbol.GetSymbolType(); - Contract.ThrowIfNull(typeSymbol); - return typeSymbol.GenerateExpressionSyntax(nameSyntax: true); - } + protected override SyntaxNode GetSymbolTypeExpression(SemanticModel model, MemberAccessExpressionSyntax node, CancellationToken cancellationToken) + { + // The corresponding analyzer (CSharpConvertTypeOfToNameOfDiagnosticAnalyzer) validated the syntax + var typeOfExpression = (TypeOfExpressionSyntax)node.Expression; + var typeSymbol = model.GetSymbolInfo(typeOfExpression.Type, cancellationToken).Symbol.GetSymbolType(); + Contract.ThrowIfNull(typeSymbol); + return typeSymbol.GenerateExpressionSyntax(nameSyntax: true); } } diff --git a/src/Analyzers/CSharp/CodeFixes/DisambiguateSameVariable/CSharpDisambiguateSameVariableCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/DisambiguateSameVariable/CSharpDisambiguateSameVariableCodeFixProvider.cs index b3d3751861f23..1b6f217aa2ba8 100644 --- a/src/Analyzers/CSharp/CodeFixes/DisambiguateSameVariable/CSharpDisambiguateSameVariableCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/DisambiguateSameVariable/CSharpDisambiguateSameVariableCodeFixProvider.cs @@ -21,147 +21,146 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.DisambiguateSameVariable +namespace Microsoft.CodeAnalysis.CSharp.DisambiguateSameVariable; + +using static SyntaxFactory; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.DisambiguateSameVariable), Shared] +internal class CSharpDisambiguateSameVariableCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - using static SyntaxFactory; + private const string CS1717 = nameof(CS1717); // Assignment made to same variable; did you mean to assign something else? + private const string CS1718 = nameof(CS1718); // Comparison made to same variable; did you mean to compare something else? - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.DisambiguateSameVariable), Shared] - internal class CSharpDisambiguateSameVariableCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpDisambiguateSameVariableCodeFixProvider() { - private const string CS1717 = nameof(CS1717); // Assignment made to same variable; did you mean to assign something else? - private const string CS1718 = nameof(CS1718); // Comparison made to same variable; did you mean to compare something else? + } - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpDisambiguateSameVariableCodeFixProvider() - { - } + public override ImmutableArray FixableDiagnosticIds { get; } + = [CS1717, CS1718]; - public override ImmutableArray FixableDiagnosticIds { get; } - = [CS1717, CS1718]; + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var diagnostic = context.Diagnostics.First(); + var cancellationToken = context.CancellationToken; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (CanFix(semanticModel, diagnostic, cancellationToken, out _, out _, out var title)) { - var document = context.Document; - var diagnostic = context.Diagnostics.First(); - var cancellationToken = context.CancellationToken; - - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (CanFix(semanticModel, diagnostic, cancellationToken, out _, out _, out var title)) - { - RegisterCodeFix(context, title, nameof(CSharpDisambiguateSameVariableCodeFixProvider)); - } + RegisterCodeFix(context, title, nameof(CSharpDisambiguateSameVariableCodeFixProvider)); } + } + + private static bool CanFix( + SemanticModel semanticModel, Diagnostic diagnostic, CancellationToken cancellationToken, + [NotNullWhen(true)] out SimpleNameSyntax? leftName, + [NotNullWhen(true)] out ISymbol? matchingMember, + [NotNullWhen(true)] out string? title) + { + leftName = null; + matchingMember = null; + title = null; - private static bool CanFix( - SemanticModel semanticModel, Diagnostic diagnostic, CancellationToken cancellationToken, - [NotNullWhen(true)] out SimpleNameSyntax? leftName, - [NotNullWhen(true)] out ISymbol? matchingMember, - [NotNullWhen(true)] out string? title) + var span = diagnostic.Location.SourceSpan; + var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); + var (left, right, titleFormat) = node switch { - leftName = null; - matchingMember = null; - title = null; + BinaryExpressionSyntax binary => (binary.Left, binary.Right, CSharpCodeFixesResources.Compare_to_0), + AssignmentExpressionSyntax assignment => (assignment.Left, assignment.Right, CSharpCodeFixesResources.Assign_to_0), + _ => default, + }; - var span = diagnostic.Location.SourceSpan; - var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - var (left, right, titleFormat) = node switch - { - BinaryExpressionSyntax binary => (binary.Left, binary.Right, CSharpCodeFixesResources.Compare_to_0), - AssignmentExpressionSyntax assignment => (assignment.Left, assignment.Right, CSharpCodeFixesResources.Assign_to_0), - _ => default, - }; + if (left == null || right == null) + return false; - if (left == null || right == null) - return false; + if (left is not IdentifierNameSyntax and not MemberAccessExpressionSyntax) + return false; - if (left is not IdentifierNameSyntax and not MemberAccessExpressionSyntax) - return false; + var leftSymbol = semanticModel.GetSymbolInfo(left, cancellationToken).GetAnySymbol(); + var rightSymbol = semanticModel.GetSymbolInfo(right, cancellationToken).GetAnySymbol(); - var leftSymbol = semanticModel.GetSymbolInfo(left, cancellationToken).GetAnySymbol(); - var rightSymbol = semanticModel.GetSymbolInfo(right, cancellationToken).GetAnySymbol(); + if (leftSymbol == null || rightSymbol == null) + return false; - if (leftSymbol == null || rightSymbol == null) - return false; + // Since this is a self assignment/compare, these symbols should be the same. + Debug.Assert(leftSymbol.Equals(rightSymbol)); - // Since this is a self assignment/compare, these symbols should be the same. - Debug.Assert(leftSymbol.Equals(rightSymbol)); + if (leftSymbol.Kind is not SymbolKind.Local and + not SymbolKind.Parameter and + not SymbolKind.Field and + not SymbolKind.Property) + { + return false; + } - if (leftSymbol.Kind is not SymbolKind.Local and - not SymbolKind.Parameter and - not SymbolKind.Field and - not SymbolKind.Property) - { - return false; - } + var localOrParamName = leftSymbol.Name; + if (string.IsNullOrWhiteSpace(localOrParamName)) + return false; + + var enclosingType = semanticModel.GetEnclosingNamedType(span.Start, cancellationToken); + if (enclosingType == null) + return false; + + // Given a local/param called 'x' See if we have an instance field or prop in the containing type + // called 'x' or 'X' or '_x'. + + var pascalName = localOrParamName.ToPascalCase(); + var camelName = localOrParamName.ToCamelCase(); + var underscoreName = "_" + localOrParamName; + var members = from t in enclosingType.GetBaseTypesAndThis() + from m in t.GetMembers() + where !m.IsStatic + where m is IFieldSymbol or IPropertySymbol + where !m.Equals(leftSymbol) + where m.Name == localOrParamName || + m.Name == pascalName || + m.Name == camelName || + m.Name == underscoreName + where m.IsAccessibleWithin(enclosingType) + select m; + + matchingMember = members.FirstOrDefault(); + if (matchingMember == null) + return false; + + var memberContainer = matchingMember.ContainingType.ToMinimalDisplayString(semanticModel, span.Start); + title = string.Format(titleFormat, $"{memberContainer}.{matchingMember.Name}"); + + leftName = left is MemberAccessExpressionSyntax memberAccess + ? memberAccess.Name + : (IdentifierNameSyntax)left; + + return true; + } - var localOrParamName = leftSymbol.Name; - if (string.IsNullOrWhiteSpace(localOrParamName)) - return false; - - var enclosingType = semanticModel.GetEnclosingNamedType(span.Start, cancellationToken); - if (enclosingType == null) - return false; - - // Given a local/param called 'x' See if we have an instance field or prop in the containing type - // called 'x' or 'X' or '_x'. - - var pascalName = localOrParamName.ToPascalCase(); - var camelName = localOrParamName.ToCamelCase(); - var underscoreName = "_" + localOrParamName; - var members = from t in enclosingType.GetBaseTypesAndThis() - from m in t.GetMembers() - where !m.IsStatic - where m is IFieldSymbol or IPropertySymbol - where !m.Equals(leftSymbol) - where m.Name == localOrParamName || - m.Name == pascalName || - m.Name == camelName || - m.Name == underscoreName - where m.IsAccessibleWithin(enclosingType) - select m; - - matchingMember = members.FirstOrDefault(); - if (matchingMember == null) - return false; - - var memberContainer = matchingMember.ContainingType.ToMinimalDisplayString(semanticModel, span.Start); - title = string.Format(titleFormat, $"{memberContainer}.{matchingMember.Name}"); - - leftName = left is MemberAccessExpressionSyntax memberAccess - ? memberAccess.Name - : (IdentifierNameSyntax)left; - - return true; - } + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var syntaxFacts = CSharpSyntaxFacts.Instance; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + foreach (var diagnostic in diagnostics) { - var syntaxFacts = CSharpSyntaxFacts.Instance; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (!CanFix(semanticModel, diagnostic, cancellationToken, + out var nameNode, out var matchingMember, out _)) + { + continue; + } - foreach (var diagnostic in diagnostics) + var newNameNode = matchingMember.Name.ToIdentifierName(); + var newExpr = (ExpressionSyntax)newNameNode; + if (!syntaxFacts.IsNameOfSimpleMemberAccessExpression(nameNode) && + !syntaxFacts.IsNameOfMemberBindingExpression(nameNode)) { - if (!CanFix(semanticModel, diagnostic, cancellationToken, - out var nameNode, out var matchingMember, out _)) - { - continue; - } - - var newNameNode = matchingMember.Name.ToIdentifierName(); - var newExpr = (ExpressionSyntax)newNameNode; - if (!syntaxFacts.IsNameOfSimpleMemberAccessExpression(nameNode) && - !syntaxFacts.IsNameOfMemberBindingExpression(nameNode)) - { - newExpr = MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), newNameNode).WithAdditionalAnnotations(Simplifier.Annotation); - } - - newExpr = newExpr.WithTriviaFrom(nameNode); - editor.ReplaceNode(nameNode, newExpr); + newExpr = MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), newNameNode).WithAdditionalAnnotations(Simplifier.Annotation); } + + newExpr = newExpr.WithTriviaFrom(nameNode); + editor.ReplaceNode(nameNode, newExpr); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/DocumentationComments/CSharpAddDocCommentNodesCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/DocumentationComments/CSharpAddDocCommentNodesCodeFixProvider.cs index 3d1fdebcbc1cc..9ddd508ced16f 100644 --- a/src/Analyzers/CSharp/CodeFixes/DocumentationComments/CSharpAddDocCommentNodesCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/DocumentationComments/CSharpAddDocCommentNodesCodeFixProvider.cs @@ -13,81 +13,80 @@ using Microsoft.CodeAnalysis.DocumentationComments; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments +namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddDocCommentNodes), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.ImplementInterface)] +internal class CSharpAddDocCommentNodesCodeFixProvider + : AbstractAddDocCommentNodesCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddDocCommentNodes), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.ImplementInterface)] - internal class CSharpAddDocCommentNodesCodeFixProvider - : AbstractAddDocCommentNodesCodeFixProvider + /// + /// Parameter has no matching param tag in XML comment + /// + private const string CS1573 = nameof(CS1573); + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddDocCommentNodesCodeFixProvider() { - /// - /// Parameter has no matching param tag in XML comment - /// - private const string CS1573 = nameof(CS1573); - - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddDocCommentNodesCodeFixProvider() - { - } - - public override ImmutableArray FixableDiagnosticIds { get; } = [CS1573]; + } - protected override string NodeName { get; } = "param"; + public override ImmutableArray FixableDiagnosticIds { get; } = [CS1573]; - protected override List GetNameAttributes(XmlElementSyntax node) - => node.StartTag.Attributes.OfType().ToList(); + protected override string NodeName { get; } = "param"; - protected override string GetValueFromNameAttribute(XmlNameAttributeSyntax attribute) - => attribute.Identifier.Identifier.ValueText; + protected override List GetNameAttributes(XmlElementSyntax node) + => node.StartTag.Attributes.OfType().ToList(); - protected override SyntaxNode? TryGetDocCommentNode(SyntaxTriviaList leadingTrivia) - { - var docCommentNodes = leadingTrivia.Where(f => f.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)); + protected override string GetValueFromNameAttribute(XmlNameAttributeSyntax attribute) + => attribute.Identifier.Identifier.ValueText; - foreach (var node in docCommentNodes) - { - var nodeStructure = node.GetStructure()!; - var descendentXmlElements = nodeStructure.DescendantNodes().OfType(); + protected override SyntaxNode? TryGetDocCommentNode(SyntaxTriviaList leadingTrivia) + { + var docCommentNodes = leadingTrivia.Where(f => f.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)); - if (descendentXmlElements.Any(element => GetXmlElementLocalName(element) == NodeName)) - return nodeStructure; - } + foreach (var node in docCommentNodes) + { + var nodeStructure = node.GetStructure()!; + var descendentXmlElements = nodeStructure.DescendantNodes().OfType(); - return null; + if (descendentXmlElements.Any(element => GetXmlElementLocalName(element) == NodeName)) + return nodeStructure; } - protected override string GetXmlElementLocalName(XmlElementSyntax element) - => element.StartTag.Name.LocalName.ValueText; + return null; + } - protected override ImmutableArray GetParameterNames(MemberDeclarationSyntax member) - { - var parameterList = (ParameterListSyntax?)member - .DescendantNodes(descendIntoChildren: _ => true, descendIntoTrivia: false) - .FirstOrDefault(f => f is ParameterListSyntax); + protected override string GetXmlElementLocalName(XmlElementSyntax element) + => element.StartTag.Name.LocalName.ValueText; - return parameterList == null - ? [] - : parameterList.Parameters.SelectAsArray(s => s.Identifier.ValueText); - } + protected override ImmutableArray GetParameterNames(MemberDeclarationSyntax member) + { + var parameterList = (ParameterListSyntax?)member + .DescendantNodes(descendIntoChildren: _ => true, descendIntoTrivia: false) + .FirstOrDefault(f => f is ParameterListSyntax); - protected override XmlElementSyntax GetNewNode(string parameterName, bool isFirstNodeInComment) + return parameterList == null + ? [] + : parameterList.Parameters.SelectAsArray(s => s.Identifier.ValueText); + } + + protected override XmlElementSyntax GetNewNode(string parameterName, bool isFirstNodeInComment) + { + // This is the simplest way of getting the XML node with the correct leading trivia + // However, trying to add a `DocumentationCommentTriviaSyntax` to the node in the abstract + // implementation causes an exception, so we have to add an XmlElementSyntax + var newDocCommentNode = SyntaxFactory.DocumentationComment(SyntaxFactory.XmlParamElement(parameterName)); + var elementNode = (XmlElementSyntax)newDocCommentNode.ChildNodes().ElementAt(0); + + // return node on new line + if (isFirstNodeInComment) { - // This is the simplest way of getting the XML node with the correct leading trivia - // However, trying to add a `DocumentationCommentTriviaSyntax` to the node in the abstract - // implementation causes an exception, so we have to add an XmlElementSyntax - var newDocCommentNode = SyntaxFactory.DocumentationComment(SyntaxFactory.XmlParamElement(parameterName)); - var elementNode = (XmlElementSyntax)newDocCommentNode.ChildNodes().ElementAt(0); - - // return node on new line - if (isFirstNodeInComment) - { - return elementNode.WithTrailingTrivia(SyntaxFactory.ParseTrailingTrivia(Environment.NewLine)); - } - - return elementNode.WithLeadingTrivia( - SyntaxFactory.ParseLeadingTrivia(Environment.NewLine) - .AddRange(elementNode.GetLeadingTrivia())); + return elementNode.WithTrailingTrivia(SyntaxFactory.ParseTrailingTrivia(Environment.NewLine)); } + + return elementNode.WithLeadingTrivia( + SyntaxFactory.ParseLeadingTrivia(Environment.NewLine) + .AddRange(elementNode.GetLeadingTrivia())); } } diff --git a/src/Analyzers/CSharp/CodeFixes/DocumentationComments/CSharpRemoveDocCommentNodeCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/DocumentationComments/CSharpRemoveDocCommentNodeCodeFixProvider.cs index 49cdea8b875db..f17292e1b14a3 100644 --- a/src/Analyzers/CSharp/CodeFixes/DocumentationComments/CSharpRemoveDocCommentNodeCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/DocumentationComments/CSharpRemoveDocCommentNodeCodeFixProvider.cs @@ -10,61 +10,60 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.DocumentationComments; -namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments +namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveDocCommentNode), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.ImplementInterface)] +internal class CSharpRemoveDocCommentNodeCodeFixProvider : + AbstractRemoveDocCommentNodeCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveDocCommentNode), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.ImplementInterface)] - internal class CSharpRemoveDocCommentNodeCodeFixProvider : - AbstractRemoveDocCommentNodeCodeFixProvider - { - /// - /// Duplicate param tag - /// - private const string CS1571 = nameof(CS1571); + /// + /// Duplicate param tag + /// + private const string CS1571 = nameof(CS1571); - /// - /// Param tag with no matching parameter - /// - private const string CS1572 = nameof(CS1572); + /// + /// Param tag with no matching parameter + /// + private const string CS1572 = nameof(CS1572); - /// - /// Duplicate typeparam tag - /// - private const string CS1710 = nameof(CS1710); + /// + /// Duplicate typeparam tag + /// + private const string CS1710 = nameof(CS1710); - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpRemoveDocCommentNodeCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpRemoveDocCommentNodeCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds { get; } = [CS1571, CS1572, CS1710]; + public override ImmutableArray FixableDiagnosticIds { get; } = [CS1571, CS1572, CS1710]; - protected override string DocCommentSignifierToken { get; } = "///"; + protected override string DocCommentSignifierToken { get; } = "///"; - protected override SyntaxTriviaList GetRevisedDocCommentTrivia(string docCommentText) - => SyntaxFactory.ParseLeadingTrivia(docCommentText); + protected override SyntaxTriviaList GetRevisedDocCommentTrivia(string docCommentText) + => SyntaxFactory.ParseLeadingTrivia(docCommentText); - protected override SyntaxTokenList GetTextTokens(XmlTextSyntax xmlText) - => xmlText.TextTokens; + protected override SyntaxTokenList GetTextTokens(XmlTextSyntax xmlText) + => xmlText.TextTokens; - protected override bool IsXmlWhitespaceToken(SyntaxToken token) - => token.Kind() == SyntaxKind.XmlTextLiteralToken && IsWhitespace(token.Text); + protected override bool IsXmlWhitespaceToken(SyntaxToken token) + => token.Kind() == SyntaxKind.XmlTextLiteralToken && IsWhitespace(token.Text); - protected override bool IsXmlNewLineToken(SyntaxToken token) - => token.Kind() == SyntaxKind.XmlTextLiteralNewLineToken; + protected override bool IsXmlNewLineToken(SyntaxToken token) + => token.Kind() == SyntaxKind.XmlTextLiteralNewLineToken; - private static bool IsWhitespace(string text) + private static bool IsWhitespace(string text) + { + foreach (var c in text) { - foreach (var c in text) + if (!SyntaxFacts.IsWhitespace(c)) { - if (!SyntaxFacts.IsWhitespace(c)) - { - return false; - } + return false; } - - return true; } + + return true; } } diff --git a/src/Analyzers/CSharp/CodeFixes/FileHeaders/CSharpFileHeaderCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/FileHeaders/CSharpFileHeaderCodeFixProvider.cs index 29f8991978ec2..d549187a6f015 100644 --- a/src/Analyzers/CSharp/CodeFixes/FileHeaders/CSharpFileHeaderCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/FileHeaders/CSharpFileHeaderCodeFixProvider.cs @@ -8,21 +8,20 @@ using Microsoft.CodeAnalysis.FileHeaders; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.FileHeaders +namespace Microsoft.CodeAnalysis.CSharp.FileHeaders; + +/// +/// Implements a code fix for file header diagnostics. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.FileHeader)] +[Shared] +internal class CSharpFileHeaderCodeFixProvider : AbstractFileHeaderCodeFixProvider { - /// - /// Implements a code fix for file header diagnostics. - /// - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.FileHeader)] - [Shared] - internal class CSharpFileHeaderCodeFixProvider : AbstractFileHeaderCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpFileHeaderCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpFileHeaderCodeFixProvider() - { - } - - protected override AbstractFileHeaderHelper FileHeaderHelper => CSharpFileHeaderHelper.Instance; } + + protected override AbstractFileHeaderHelper FileHeaderHelper => CSharpFileHeaderHelper.Instance; } diff --git a/src/Analyzers/CSharp/CodeFixes/FixIncorrectConstraint/CSharpFixIncorrectConstraintCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/FixIncorrectConstraint/CSharpFixIncorrectConstraintCodeFixProvider.cs index 0369d5154c362..e613465d2b23a 100644 --- a/src/Analyzers/CSharp/CodeFixes/FixIncorrectConstraint/CSharpFixIncorrectConstraintCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/FixIncorrectConstraint/CSharpFixIncorrectConstraintCodeFixProvider.cs @@ -16,102 +16,101 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.FixIncorrectConstraint +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.FixIncorrectConstraint; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.FixIncorrectConstraint), Shared] +internal class CSharpFixIncorrectConstraintCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.FixIncorrectConstraint), Shared] - internal class CSharpFixIncorrectConstraintCodeFixProvider : SyntaxEditorBasedCodeFixProvider + private const string CS9010 = nameof(CS9010); // Keyword 'enum' cannot be used as a constraint.Did you mean 'struct, System.Enum'? Net6 C:\github\repo_find_refs\Net6\Class1.cs 1 Active + private const string CS9011 = nameof(CS9011); // 'delegate' cannot be used as a constraint.Did you mean 'System.Delegate'? Net6 C:\github\repo_find_refs\Net6\Class1.cs 1 Active + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpFixIncorrectConstraintCodeFixProvider() { - private const string CS9010 = nameof(CS9010); // Keyword 'enum' cannot be used as a constraint.Did you mean 'struct, System.Enum'? Net6 C:\github\repo_find_refs\Net6\Class1.cs 1 Active - private const string CS9011 = nameof(CS9011); // 'delegate' cannot be used as a constraint.Did you mean 'System.Delegate'? Net6 C:\github\repo_find_refs\Net6\Class1.cs 1 Active + } - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpFixIncorrectConstraintCodeFixProvider() - { - } + public override ImmutableArray FixableDiagnosticIds + => [CS9010, CS9011]; - public override ImmutableArray FixableDiagnosticIds - => [CS9010, CS9011]; + private static bool TryGetConstraint( + Diagnostic diagnostic, + CancellationToken cancellationToken, + [NotNullWhen(true)] out TypeConstraintSyntax? constraint, + out SyntaxToken enumOrDelegateKeyword) + { + enumOrDelegateKeyword = default; + constraint = diagnostic.Location.FindNode(cancellationToken) as TypeConstraintSyntax; + if (constraint == null) + return false; - private static bool TryGetConstraint( - Diagnostic diagnostic, - CancellationToken cancellationToken, - [NotNullWhen(true)] out TypeConstraintSyntax? constraint, - out SyntaxToken enumOrDelegateKeyword) - { - enumOrDelegateKeyword = default; - constraint = diagnostic.Location.FindNode(cancellationToken) as TypeConstraintSyntax; - if (constraint == null) - return false; + if (constraint.Parent is not TypeParameterConstraintClauseSyntax) + return false; - if (constraint.Parent is not TypeParameterConstraintClauseSyntax) - return false; + if (constraint.Type is not IdentifierNameSyntax { Identifier.IsMissing: true } type) + return false; - if (constraint.Type is not IdentifierNameSyntax { Identifier.IsMissing: true } type) - return false; + var trailingTrivia = type.GetTrailingTrivia(); + if (trailingTrivia.Count == 0) + return false; - var trailingTrivia = type.GetTrailingTrivia(); - if (trailingTrivia.Count == 0) - return false; + var firstTrivia = trailingTrivia[0]; + if (firstTrivia.Kind() != SyntaxKind.SkippedTokensTrivia) + return false; - var firstTrivia = trailingTrivia[0]; - if (firstTrivia.Kind() != SyntaxKind.SkippedTokensTrivia) - return false; + var structure = (SkippedTokensTriviaSyntax)firstTrivia.GetStructure()!; + if (structure.Tokens.Count != 1) + return false; - var structure = (SkippedTokensTriviaSyntax)firstTrivia.GetStructure()!; - if (structure.Tokens.Count != 1) - return false; + enumOrDelegateKeyword = structure.Tokens[0]; + return enumOrDelegateKeyword.Kind() is SyntaxKind.EnumKeyword or SyntaxKind.DelegateKeyword; + } - enumOrDelegateKeyword = structure.Tokens[0]; - return enumOrDelegateKeyword.Kind() is SyntaxKind.EnumKeyword or SyntaxKind.DelegateKeyword; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var cancellationToken = context.CancellationToken; - public override Task RegisterCodeFixesAsync(CodeFixContext context) + var diagnostic = context.Diagnostics.First(); + if (TryGetConstraint(diagnostic, cancellationToken, out _, out _)) { - var cancellationToken = context.CancellationToken; + RegisterCodeFix(context, CSharpCodeFixesResources.Fix_constraint, nameof(CSharpFixIncorrectConstraintCodeFixProvider)); + } - var diagnostic = context.Diagnostics.First(); - if (TryGetConstraint(diagnostic, cancellationToken, out _, out _)) - { - RegisterCodeFix(context, CSharpCodeFixesResources.Fix_constraint, nameof(CSharpFixIncorrectConstraintCodeFixProvider)); - } + return Task.CompletedTask; + } - return Task.CompletedTask; - } + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + foreach (var diagnostic in diagnostics) { - var generator = SyntaxGenerator.GetGenerator(document); - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - - foreach (var diagnostic in diagnostics) + if (TryGetConstraint(diagnostic, cancellationToken, out var constraintSyntax, out var enumOrDelegateKeyword)) { - if (TryGetConstraint(diagnostic, cancellationToken, out var constraintSyntax, out var enumOrDelegateKeyword)) + var isEnumConstraint = enumOrDelegateKeyword.Kind() is SyntaxKind.EnumKeyword; + var newType = generator.TypeExpression(compilation.GetSpecialType( + isEnumConstraint ? SpecialType.System_Enum : SpecialType.System_Delegate)); + + // Skip the first trailing trivia as that's the skipped enum/delegate keyword. + editor.ReplaceNode( + constraintSyntax.Type, newType + .WithLeadingTrivia(constraintSyntax.GetLeadingTrivia()) + .WithTrailingTrivia(constraintSyntax.GetTrailingTrivia().Skip(1))); + + // if they added the `enum` constraint, also add `struct` along with `System.Enum` to properly + // reflect what they meant (and what the diagnostic says). + if (isEnumConstraint) { - var isEnumConstraint = enumOrDelegateKeyword.Kind() is SyntaxKind.EnumKeyword; - var newType = generator.TypeExpression(compilation.GetSpecialType( - isEnumConstraint ? SpecialType.System_Enum : SpecialType.System_Delegate)); - - // Skip the first trailing trivia as that's the skipped enum/delegate keyword. - editor.ReplaceNode( - constraintSyntax.Type, newType - .WithLeadingTrivia(constraintSyntax.GetLeadingTrivia()) - .WithTrailingTrivia(constraintSyntax.GetTrailingTrivia().Skip(1))); - - // if they added the `enum` constraint, also add `struct` along with `System.Enum` to properly - // reflect what they meant (and what the diagnostic says). - if (isEnumConstraint) + editor.ReplaceNode(constraintSyntax.GetRequiredParent(), (parent, _) => { - editor.ReplaceNode(constraintSyntax.GetRequiredParent(), (parent, _) => - { - var clause = (TypeParameterConstraintClauseSyntax)parent; - return clause.WithConstraints( - clause.Constraints.Insert(0, SyntaxFactory.ClassOrStructConstraint( - SyntaxKind.StructConstraint))); - }); - } + var clause = (TypeParameterConstraintClauseSyntax)parent; + return clause.WithConstraints( + clause.Constraints.Insert(0, SyntaxFactory.ClassOrStructConstraint( + SyntaxKind.StructConstraint))); + }); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/FixReturnType/CSharpFixReturnTypeCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/FixReturnType/CSharpFixReturnTypeCodeFixProvider.cs index a3a171112f75c..5d1de16881a7b 100644 --- a/src/Analyzers/CSharp/CodeFixes/FixReturnType/CSharpFixReturnTypeCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/FixReturnType/CSharpFixReturnTypeCodeFixProvider.cs @@ -18,171 +18,170 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Simplification; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.FixReturnType +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.FixReturnType; + +/// +/// Helps fix void-returning methods or local functions to return a correct type. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.FixReturnType), Shared] +internal class CSharpFixReturnTypeCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - /// - /// Helps fix void-returning methods or local functions to return a correct type. - /// - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.FixReturnType), Shared] - internal class CSharpFixReturnTypeCodeFixProvider : SyntaxEditorBasedCodeFixProvider + // error CS0127: Since 'M()' returns void, a return keyword must not be followed by an object expression + // error CS1997: Since 'M()' is an async method that returns 'Task', a return keyword must not be followed by an object expression + // error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement + public override ImmutableArray FixableDiagnosticIds => ["CS0127", "CS1997", "CS0201"]; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpFixReturnTypeCodeFixProvider() + : base(supportsFixAll: false) { - // error CS0127: Since 'M()' returns void, a return keyword must not be followed by an object expression - // error CS1997: Since 'M()' is an async method that returns 'Task', a return keyword must not be followed by an object expression - // error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement - public override ImmutableArray FixableDiagnosticIds => ["CS0127", "CS1997", "CS0201"]; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpFixReturnTypeCodeFixProvider() - : base(supportsFixAll: false) - { - } + } - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var diagnostics = context.Diagnostics; - var cancellationToken = context.CancellationToken; + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var diagnostics = context.Diagnostics; + var cancellationToken = context.CancellationToken; - var analyzedTypes = await TryGetOldAndNewReturnTypeAsync(document, diagnostics, cancellationToken).ConfigureAwait(false); - if (analyzedTypes == default) - return; + var analyzedTypes = await TryGetOldAndNewReturnTypeAsync(document, diagnostics, cancellationToken).ConfigureAwait(false); + if (analyzedTypes == default) + return; - if (IsVoid(analyzedTypes.declarationToFix) && IsVoid(analyzedTypes.fixedDeclaration)) - { - // Don't offer a code fix if the return type is void and return is followed by a void expression. - // See https://github.com/dotnet/roslyn/issues/47089 - return; - } + if (IsVoid(analyzedTypes.declarationToFix) && IsVoid(analyzedTypes.fixedDeclaration)) + { + // Don't offer a code fix if the return type is void and return is followed by a void expression. + // See https://github.com/dotnet/roslyn/issues/47089 + return; + } - RegisterCodeFix(context, CSharpCodeFixesResources.Fix_return_type, nameof(CSharpCodeFixesResources.Fix_return_type)); + RegisterCodeFix(context, CSharpCodeFixesResources.Fix_return_type, nameof(CSharpCodeFixesResources.Fix_return_type)); - return; + return; - static bool IsVoid(TypeSyntax typeSyntax) - => typeSyntax is PredefinedTypeSyntax { Keyword.RawKind: (int)SyntaxKind.VoidKeyword }; - } + static bool IsVoid(TypeSyntax typeSyntax) + => typeSyntax is PredefinedTypeSyntax { Keyword.RawKind: (int)SyntaxKind.VoidKeyword }; + } - private static async Task<(TypeSyntax declarationToFix, TypeSyntax fixedDeclaration)> TryGetOldAndNewReturnTypeAsync( - Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + private static async Task<(TypeSyntax declarationToFix, TypeSyntax fixedDeclaration)> TryGetOldAndNewReturnTypeAsync( + Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + { + Debug.Assert(diagnostics.Length == 1); + var location = diagnostics[0].Location; + var node = location.FindNode(getInnermostNodeForTie: true, cancellationToken); + var returnedValue = node is ReturnStatementSyntax returnStatement ? returnStatement.Expression : node; + if (returnedValue is null) + return default; + + var (declarationTypeToFix, isAsync) = TryGetDeclarationTypeToFix(node); + if (declarationTypeToFix is null) + return default; + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var returnedType = semanticModel.GetTypeInfo(returnedValue, cancellationToken).Type; + + // Special case when tuple has elements with unknown type, e.g. `(null, default)` + // Need to replace this unknown elements with default `object`s + if (returnedType is null && + returnedValue is TupleExpressionSyntax tuple) { - Debug.Assert(diagnostics.Length == 1); - var location = diagnostics[0].Location; - var node = location.FindNode(getInnermostNodeForTie: true, cancellationToken); - var returnedValue = node is ReturnStatementSyntax returnStatement ? returnStatement.Expression : node; - if (returnedValue is null) - return default; + returnedType = InferTupleType(tuple, semanticModel, cancellationToken); + } - var (declarationTypeToFix, isAsync) = TryGetDeclarationTypeToFix(node); - if (declarationTypeToFix is null) - return default; + returnedType ??= semanticModel.Compilation.ObjectType; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var returnedType = semanticModel.GetTypeInfo(returnedValue, cancellationToken).Type; + TypeSyntax fixedDeclaration; + if (isAsync) + { + var previousReturnType = semanticModel.GetTypeInfo(declarationTypeToFix, cancellationToken).Type; + if (previousReturnType is null) + return default; - // Special case when tuple has elements with unknown type, e.g. `(null, default)` - // Need to replace this unknown elements with default `object`s - if (returnedType is null && - returnedValue is TupleExpressionSyntax tuple) - { - returnedType = InferTupleType(tuple, semanticModel, cancellationToken); - } + var compilation = semanticModel.Compilation; - returnedType ??= semanticModel.Compilation.ObjectType; + INamedTypeSymbol? taskType = null; - TypeSyntax fixedDeclaration; - if (isAsync) + // void, Task -> Task + // ValueTask -> ValueTask + // other type -> we cannot infer anything + if (previousReturnType.SpecialType is SpecialType.System_Void || + Equals(previousReturnType, compilation.TaskType())) { - var previousReturnType = semanticModel.GetTypeInfo(declarationTypeToFix, cancellationToken).Type; - if (previousReturnType is null) - return default; - - var compilation = semanticModel.Compilation; - - INamedTypeSymbol? taskType = null; - - // void, Task -> Task - // ValueTask -> ValueTask - // other type -> we cannot infer anything - if (previousReturnType.SpecialType is SpecialType.System_Void || - Equals(previousReturnType, compilation.TaskType())) - { - taskType = compilation.TaskOfTType(); - } - else if (Equals(previousReturnType, compilation.ValueTaskType())) - { - taskType = compilation.ValueTaskOfTType(); - } - - if (taskType is null) - return default; - - fixedDeclaration = taskType.Construct(returnedType).GenerateTypeSyntax(allowVar: false); + taskType = compilation.TaskOfTType(); } - else + else if (Equals(previousReturnType, compilation.ValueTaskType())) { - fixedDeclaration = returnedType.GenerateTypeSyntax(allowVar: false); + taskType = compilation.ValueTaskOfTType(); } - fixedDeclaration = fixedDeclaration.WithAdditionalAnnotations(Simplifier.Annotation).WithTriviaFrom(declarationTypeToFix); + if (taskType is null) + return default; - return (declarationTypeToFix, fixedDeclaration); + fixedDeclaration = taskType.Construct(returnedType).GenerateTypeSyntax(allowVar: false); } - - protected override async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + else { - var (declarationTypeToFix, fixedDeclaration) = - await TryGetOldAndNewReturnTypeAsync(document, diagnostics, cancellationToken).ConfigureAwait(false); - - editor.ReplaceNode(declarationTypeToFix, fixedDeclaration); + fixedDeclaration = returnedType.GenerateTypeSyntax(allowVar: false); } - private static (TypeSyntax type, bool isAsync) TryGetDeclarationTypeToFix(SyntaxNode node) - { - return node.GetAncestors().Select(TryGetReturnTypeToFix).FirstOrDefault(p => p.type != null); + fixedDeclaration = fixedDeclaration.WithAdditionalAnnotations(Simplifier.Annotation).WithTriviaFrom(declarationTypeToFix); - static (TypeSyntax type, bool isAsync) TryGetReturnTypeToFix(SyntaxNode containingMember) - { - return containingMember switch - { - // void M() { return 1; } - // async Task M() { return 1; } - MethodDeclarationSyntax method => (method.ReturnType, method.Modifiers.Any(SyntaxKind.AsyncKeyword)), - // void local() { return 1; } - // async Task local() { return 1; } - LocalFunctionStatementSyntax localFunction => (localFunction.ReturnType, localFunction.Modifiers.Any(SyntaxKind.AsyncKeyword)), - _ => default, - }; - } - } + return (declarationTypeToFix, fixedDeclaration); + } - private static ITypeSymbol? InferTupleType(TupleExpressionSyntax tuple, SemanticModel semanticModel, CancellationToken cancellationToken) - { - var compilation = semanticModel.Compilation; - var argCount = tuple.Arguments.Count; + protected override async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var (declarationTypeToFix, fixedDeclaration) = + await TryGetOldAndNewReturnTypeAsync(document, diagnostics, cancellationToken).ConfigureAwait(false); - var baseTupleType = compilation.ValueTupleType(argCount); - if (baseTupleType is null) - return null; + editor.ReplaceNode(declarationTypeToFix, fixedDeclaration); + } - var inferredTupleTypes = new ITypeSymbol[argCount]; + private static (TypeSyntax type, bool isAsync) TryGetDeclarationTypeToFix(SyntaxNode node) + { + return node.GetAncestors().Select(TryGetReturnTypeToFix).FirstOrDefault(p => p.type != null); - for (var i = 0; i < argCount; i++) + static (TypeSyntax type, bool isAsync) TryGetReturnTypeToFix(SyntaxNode containingMember) + { + return containingMember switch { - var argumentExpression = tuple.Arguments[i].Expression; - var type = semanticModel.GetTypeInfo(argumentExpression, cancellationToken).Type; + // void M() { return 1; } + // async Task M() { return 1; } + MethodDeclarationSyntax method => (method.ReturnType, method.Modifiers.Any(SyntaxKind.AsyncKeyword)), + // void local() { return 1; } + // async Task local() { return 1; } + LocalFunctionStatementSyntax localFunction => (localFunction.ReturnType, localFunction.Modifiers.Any(SyntaxKind.AsyncKeyword)), + _ => default, + }; + } + } + + private static ITypeSymbol? InferTupleType(TupleExpressionSyntax tuple, SemanticModel semanticModel, CancellationToken cancellationToken) + { + var compilation = semanticModel.Compilation; + var argCount = tuple.Arguments.Count; - // Nested tuple with unknown type, e.g. `(string.Empty, (2, null))` - if (type is null && - argumentExpression is TupleExpressionSyntax nestedTuple) - { - type = InferTupleType(nestedTuple, semanticModel, cancellationToken); - } + var baseTupleType = compilation.ValueTupleType(argCount); + if (baseTupleType is null) + return null; - inferredTupleTypes[i] = type is null ? semanticModel.Compilation.ObjectType : type; + var inferredTupleTypes = new ITypeSymbol[argCount]; + + for (var i = 0; i < argCount; i++) + { + var argumentExpression = tuple.Arguments[i].Expression; + var type = semanticModel.GetTypeInfo(argumentExpression, cancellationToken).Type; + + // Nested tuple with unknown type, e.g. `(string.Empty, (2, null))` + if (type is null && + argumentExpression is TupleExpressionSyntax nestedTuple) + { + type = InferTupleType(nestedTuple, semanticModel, cancellationToken); } - return baseTupleType.Construct(inferredTupleTypes); + inferredTupleTypes[i] = type is null ? semanticModel.Compilation.ObjectType : type; } + + return baseTupleType.Construct(inferredTupleTypes); } } diff --git a/src/Analyzers/CSharp/CodeFixes/ForEachCast/CSharpForEachCastCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/ForEachCast/CSharpForEachCastCodeFixProvider.cs index f6e9325c1b4e2..1a10b3216eec0 100644 --- a/src/Analyzers/CSharp/CodeFixes/ForEachCast/CSharpForEachCastCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/ForEachCast/CSharpForEachCastCodeFixProvider.cs @@ -12,26 +12,25 @@ using Microsoft.CodeAnalysis.Host.Mef; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.ForEachCast +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.ForEachCast; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ForEachCast), Shared] +internal class CSharpForEachCastCodeFixProvider : AbstractForEachCastCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ForEachCast), Shared] - internal class CSharpForEachCastCodeFixProvider : AbstractForEachCastCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpForEachCastCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpForEachCastCodeFixProvider() - { - } + } - protected override ITypeSymbol GetForEachElementType( - SemanticModel semanticModel, CommonForEachStatementSyntax forEachStatement) - { - var forEachInfo = semanticModel.GetForEachStatementInfo(forEachStatement); - var result = forEachInfo.ElementType; - // We should only get here if our analyzer found an issue, and it already checked that this property - // was non-null. So we can safely know it's non-null at fix time as well. - Contract.ThrowIfNull(result); - return result; - } + protected override ITypeSymbol GetForEachElementType( + SemanticModel semanticModel, CommonForEachStatementSyntax forEachStatement) + { + var forEachInfo = semanticModel.GetForEachStatementInfo(forEachStatement); + var result = forEachInfo.ElementType; + // We should only get here if our analyzer found an issue, and it already checked that this property + // was non-null. So we can safely know it's non-null at fix time as well. + Contract.ThrowIfNull(result); + return result; } } diff --git a/src/Analyzers/CSharp/CodeFixes/Formatting/CSharpFormattingCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/Formatting/CSharpFormattingCodeFixProvider.cs index 9476008efaaa6..78aede7a1ed01 100644 --- a/src/Analyzers/CSharp/CodeFixes/Formatting/CSharpFormattingCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/Formatting/CSharpFormattingCodeFixProvider.cs @@ -9,17 +9,16 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CodeStyle +namespace Microsoft.CodeAnalysis.CodeStyle; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.FixFormatting), Shared] +internal class CSharpFormattingCodeFixProvider : AbstractFormattingCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.FixFormatting), Shared] - internal class CSharpFormattingCodeFixProvider : AbstractFormattingCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpFormattingCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpFormattingCodeFixProvider() - { - } - - protected override ISyntaxFormatting SyntaxFormatting => CSharpSyntaxFormatting.Instance; } + + protected override ISyntaxFormatting SyntaxFormatting => CSharpSyntaxFormatting.Instance; } diff --git a/src/Analyzers/CSharp/CodeFixes/GenerateConstructor/GenerateConstructorDiagnosticIds.cs b/src/Analyzers/CSharp/CodeFixes/GenerateConstructor/GenerateConstructorDiagnosticIds.cs index 351adbffab84f..b821a7fcfd42c 100644 --- a/src/Analyzers/CSharp/CodeFixes/GenerateConstructor/GenerateConstructorDiagnosticIds.cs +++ b/src/Analyzers/CSharp/CodeFixes/GenerateConstructor/GenerateConstructorDiagnosticIds.cs @@ -6,24 +6,23 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.CSharp.GenerateConstructor +namespace Microsoft.CodeAnalysis.CSharp.GenerateConstructor; + +internal static class GenerateConstructorDiagnosticIds { - internal static class GenerateConstructorDiagnosticIds - { - public const string CS0122 = nameof(CS0122); // CS0122: 'C' is inaccessible due to its protection level - public const string CS1729 = nameof(CS1729); // CS1729: 'C' does not contain a constructor that takes n arguments - public const string CS1739 = nameof(CS1739); // CS1739: The best overload for 'Program' does not have a parameter named 'v' - public const string CS1503 = nameof(CS1503); // CS1503: Argument 1: cannot convert from 'T1' to 'T2' - public const string CS1660 = nameof(CS1660); // CS1660: Cannot convert lambda expression to type 'string[]' because it is not a delegate type - public const string CS7036 = nameof(CS7036); // CS7036: There is no argument given that corresponds to the required parameter 'v' of 'C.C(int)' + public const string CS0122 = nameof(CS0122); // CS0122: 'C' is inaccessible due to its protection level + public const string CS1729 = nameof(CS1729); // CS1729: 'C' does not contain a constructor that takes n arguments + public const string CS1739 = nameof(CS1739); // CS1739: The best overload for 'Program' does not have a parameter named 'v' + public const string CS1503 = nameof(CS1503); // CS1503: Argument 1: cannot convert from 'T1' to 'T2' + public const string CS1660 = nameof(CS1660); // CS1660: Cannot convert lambda expression to type 'string[]' because it is not a delegate type + public const string CS7036 = nameof(CS7036); // CS7036: There is no argument given that corresponds to the required parameter 'v' of 'C.C(int)' - public static readonly ImmutableArray AllDiagnosticIds = - [CS0122, CS1729, CS1739, CS1503, CS1660, CS7036]; + public static readonly ImmutableArray AllDiagnosticIds = + [CS0122, CS1729, CS1739, CS1503, CS1660, CS7036]; - public static readonly ImmutableArray TooManyArgumentsDiagnosticIds = - [CS1729]; + public static readonly ImmutableArray TooManyArgumentsDiagnosticIds = + [CS1729]; - public static readonly ImmutableArray CannotConvertDiagnosticIds = - [CS1503, CS1660]; - } + public static readonly ImmutableArray CannotConvertDiagnosticIds = + [CS1503, CS1660]; } diff --git a/src/Analyzers/CSharp/CodeFixes/HideBase/HideBaseCodeFixProvider.AddNewKeywordAction.cs b/src/Analyzers/CSharp/CodeFixes/HideBase/HideBaseCodeFixProvider.AddNewKeywordAction.cs index 50b4fb19f2865..20561b64cab2a 100644 --- a/src/Analyzers/CSharp/CodeFixes/HideBase/HideBaseCodeFixProvider.AddNewKeywordAction.cs +++ b/src/Analyzers/CSharp/CodeFixes/HideBase/HideBaseCodeFixProvider.AddNewKeywordAction.cs @@ -14,52 +14,51 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.CodeActions; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.HideBase +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.HideBase; + +internal partial class HideBaseCodeFixProvider { - internal partial class HideBaseCodeFixProvider + private class AddNewKeywordAction(Document document, SyntaxNode node, CodeActionOptionsProvider fallbackOptions) : CodeAction { - private class AddNewKeywordAction(Document document, SyntaxNode node, CodeActionOptionsProvider fallbackOptions) : CodeAction - { - private readonly Document _document = document; - private readonly SyntaxNode _node = node; - private readonly CodeActionOptionsProvider _fallbackOptions = fallbackOptions; + private readonly Document _document = document; + private readonly SyntaxNode _node = node; + private readonly CodeActionOptionsProvider _fallbackOptions = fallbackOptions; - public override string Title => CSharpCodeFixesResources.Hide_base_member; + public override string Title => CSharpCodeFixesResources.Hide_base_member; - protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) - { - var root = await _document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var options = await _document.GetCSharpCodeFixOptionsProviderAsync(_fallbackOptions, cancellationToken).ConfigureAwait(false); + protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) + { + var root = await _document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var options = await _document.GetCSharpCodeFixOptionsProviderAsync(_fallbackOptions, cancellationToken).ConfigureAwait(false); - var newNode = GetNewNode(_node, options.PreferredModifierOrder.Value); - var newRoot = root.ReplaceNode(_node, newNode); + var newNode = GetNewNode(_node, options.PreferredModifierOrder.Value); + var newRoot = root.ReplaceNode(_node, newNode); - return _document.WithSyntaxRoot(newRoot); - } + return _document.WithSyntaxRoot(newRoot); + } - private static SyntaxNode GetNewNode(SyntaxNode node, string preferredModifierOrder) - { - var syntaxFacts = CSharpSyntaxFacts.Instance; - var modifiers = syntaxFacts.GetModifiers(node); - var newModifiers = modifiers.Add(SyntaxFactory.Token(SyntaxKind.NewKeyword)); + private static SyntaxNode GetNewNode(SyntaxNode node, string preferredModifierOrder) + { + var syntaxFacts = CSharpSyntaxFacts.Instance; + var modifiers = syntaxFacts.GetModifiers(node); + var newModifiers = modifiers.Add(SyntaxFactory.Token(SyntaxKind.NewKeyword)); - if (!CSharpOrderModifiersHelper.Instance.TryGetOrComputePreferredOrder(preferredModifierOrder, out var preferredOrder) || - !AbstractOrderModifiersHelpers.IsOrdered(preferredOrder, modifiers)) - { - return syntaxFacts.WithModifiers(node, newModifiers); - } + if (!CSharpOrderModifiersHelper.Instance.TryGetOrComputePreferredOrder(preferredModifierOrder, out var preferredOrder) || + !AbstractOrderModifiersHelpers.IsOrdered(preferredOrder, modifiers)) + { + return syntaxFacts.WithModifiers(node, newModifiers); + } - var orderedModifiers = new SyntaxTokenList( - newModifiers.OrderBy(CompareModifiers)); + var orderedModifiers = new SyntaxTokenList( + newModifiers.OrderBy(CompareModifiers)); - return syntaxFacts.WithModifiers(node, orderedModifiers); + return syntaxFacts.WithModifiers(node, orderedModifiers); - int CompareModifiers(SyntaxToken left, SyntaxToken right) - => GetOrder(left) - GetOrder(right); + int CompareModifiers(SyntaxToken left, SyntaxToken right) + => GetOrder(left) - GetOrder(right); - int GetOrder(SyntaxToken token) - => preferredOrder.TryGetValue(token.RawKind, out var value) ? value : int.MaxValue; - } + int GetOrder(SyntaxToken token) + => preferredOrder.TryGetValue(token.RawKind, out var value) ? value : int.MaxValue; } } } diff --git a/src/Analyzers/CSharp/CodeFixes/HideBase/HideBaseCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/HideBase/HideBaseCodeFixProvider.cs index 468bda32b7696..23104d5089746 100644 --- a/src/Analyzers/CSharp/CodeFixes/HideBase/HideBaseCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/HideBase/HideBaseCodeFixProvider.cs @@ -12,41 +12,40 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.HideBase +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.HideBase; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddNew), Shared] +internal partial class HideBaseCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddNew), Shared] - internal partial class HideBaseCodeFixProvider : CodeFixProvider - { - internal const string CS0108 = nameof(CS0108); // 'SomeClass.SomeMember' hides inherited member 'SomeClass.SomeMember'. Use the new keyword if hiding was intended. + internal const string CS0108 = nameof(CS0108); // 'SomeClass.SomeMember' hides inherited member 'SomeClass.SomeMember'. Use the new keyword if hiding was intended. - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public HideBaseCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public HideBaseCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds => [CS0108]; + public override ImmutableArray FixableDiagnosticIds => [CS0108]; - public override FixAllProvider GetFixAllProvider() - => WellKnownFixAllProviders.BatchFixer; + public override FixAllProvider GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var diagnostic = context.Diagnostics.First(); - var diagnosticSpan = diagnostic.Location.SourceSpan; + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; - var token = root.FindToken(diagnosticSpan.Start); + var token = root.FindToken(diagnosticSpan.Start); - var originalNode = token.GetAncestor() ?? - token.GetAncestor() ?? - (SyntaxNode?)token.GetAncestor(); + var originalNode = token.GetAncestor() ?? + token.GetAncestor() ?? + (SyntaxNode?)token.GetAncestor(); - if (originalNode == null) - return; + if (originalNode == null) + return; - context.RegisterCodeFix(new AddNewKeywordAction(context.Document, originalNode, context.GetOptionsProvider()), context.Diagnostics); - } + context.RegisterCodeFix(new AddNewKeywordAction(context.Document, originalNode, context.GetOptionsProvider()), context.Diagnostics); } } diff --git a/src/Analyzers/CSharp/CodeFixes/InlineDeclaration/CSharpInlineDeclarationCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/InlineDeclaration/CSharpInlineDeclarationCodeFixProvider.cs index 3b5f60185e6b2..baa760caa134a 100644 --- a/src/Analyzers/CSharp/CodeFixes/InlineDeclaration/CSharpInlineDeclarationCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/InlineDeclaration/CSharpInlineDeclarationCodeFixProvider.cs @@ -27,391 +27,390 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.InlineDeclaration +namespace Microsoft.CodeAnalysis.CSharp.InlineDeclaration; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.InlineDeclaration), Shared] +internal partial class CSharpInlineDeclarationCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.InlineDeclaration), Shared] - internal partial class CSharpInlineDeclarationCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpInlineDeclarationCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpInlineDeclarationCodeFixProvider() - { - } + } + + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.InlineDeclarationDiagnosticId]; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Inline_variable_declaration, nameof(CSharpAnalyzersResources.Inline_variable_declaration)); + return Task.CompletedTask; + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.InlineDeclarationDiagnosticId]; + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var options = await document.GetCSharpCodeFixOptionsProviderAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - public override Task RegisterCodeFixesAsync(CodeFixContext context) + // Gather all statements to be removed + // We need this to find the statements we can safely attach trivia to + var declarationsToRemove = new HashSet(); + foreach (var diagnostic in diagnostics) { - RegisterCodeFix(context, CSharpAnalyzersResources.Inline_variable_declaration, nameof(CSharpAnalyzersResources.Inline_variable_declaration)); - return Task.CompletedTask; + declarationsToRemove.Add((LocalDeclarationStatementSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken).Parent.Parent); } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var options = await document.GetCSharpCodeFixOptionsProviderAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + // Attempt to use an out-var declaration if that's the style the user prefers. + // Note: if using 'var' would cause a problem, we will use the actual type + // of the local. This is necessary in some cases (for example, when the + // type of the out-var-decl affects overload resolution or generic instantiation). + var originalRoot = editor.OriginalRoot; + + var originalNodes = diagnostics.SelectAsArray(diagnostic => FindDiagnosticNodes(diagnostic, cancellationToken)); - // Gather all statements to be removed - // We need this to find the statements we can safely attach trivia to - var declarationsToRemove = new HashSet(); - foreach (var diagnostic in diagnostics) + await editor.ApplyExpressionLevelSemanticEditsAsync( + document, + originalNodes, + t => { - declarationsToRemove.Add((LocalDeclarationStatementSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken).Parent.Parent); - } + using var _ = ArrayBuilder.GetInstance(capacity: 2, out var additionalNodesToTrack); + additionalNodesToTrack.Add(t.identifier); + additionalNodesToTrack.Add(t.declarator); + + return (t.invocationOrCreation, additionalNodesToTrack.ToImmutable()); + }, + (_, _, _) => true, + (semanticModel, currentRoot, t, currentNode) + => ReplaceIdentifierWithInlineDeclaration( + document, options, semanticModel, currentRoot, t.declarator, + t.identifier, currentNode, declarationsToRemove, cancellationToken), + cancellationToken).ConfigureAwait(false); + } - // Attempt to use an out-var declaration if that's the style the user prefers. - // Note: if using 'var' would cause a problem, we will use the actual type - // of the local. This is necessary in some cases (for example, when the - // type of the out-var-decl affects overload resolution or generic instantiation). - var originalRoot = editor.OriginalRoot; + private static (VariableDeclaratorSyntax declarator, IdentifierNameSyntax identifier, SyntaxNode invocationOrCreation) FindDiagnosticNodes( + Diagnostic diagnostic, CancellationToken cancellationToken) + { + // Recover the nodes we care about. + var declaratorLocation = diagnostic.AdditionalLocations[0]; + var identifierLocation = diagnostic.AdditionalLocations[1]; + var invocationOrCreationLocation = diagnostic.AdditionalLocations[2]; - var originalNodes = diagnostics.SelectAsArray(diagnostic => FindDiagnosticNodes(diagnostic, cancellationToken)); + var declarator = (VariableDeclaratorSyntax)declaratorLocation.FindNode(cancellationToken); + var identifier = (IdentifierNameSyntax)identifierLocation.FindNode(cancellationToken); + var invocationOrCreation = (ExpressionSyntax)invocationOrCreationLocation.FindNode( + getInnermostNodeForTie: true, cancellationToken: cancellationToken); - await editor.ApplyExpressionLevelSemanticEditsAsync( - document, - originalNodes, - t => - { - using var _ = ArrayBuilder.GetInstance(capacity: 2, out var additionalNodesToTrack); - additionalNodesToTrack.Add(t.identifier); - additionalNodesToTrack.Add(t.declarator); - - return (t.invocationOrCreation, additionalNodesToTrack.ToImmutable()); - }, - (_, _, _) => true, - (semanticModel, currentRoot, t, currentNode) - => ReplaceIdentifierWithInlineDeclaration( - document, options, semanticModel, currentRoot, t.declarator, - t.identifier, currentNode, declarationsToRemove, cancellationToken), - cancellationToken).ConfigureAwait(false); - } + return (declarator, identifier, invocationOrCreation); + } - private static (VariableDeclaratorSyntax declarator, IdentifierNameSyntax identifier, SyntaxNode invocationOrCreation) FindDiagnosticNodes( - Diagnostic diagnostic, CancellationToken cancellationToken) - { - // Recover the nodes we care about. - var declaratorLocation = diagnostic.AdditionalLocations[0]; - var identifierLocation = diagnostic.AdditionalLocations[1]; - var invocationOrCreationLocation = diagnostic.AdditionalLocations[2]; + private static SyntaxNode ReplaceIdentifierWithInlineDeclaration( + Document document, + CSharpCodeFixOptionsProvider options, SemanticModel semanticModel, + SyntaxNode currentRoot, VariableDeclaratorSyntax declarator, + IdentifierNameSyntax identifier, SyntaxNode currentNode, + HashSet declarationsToRemove, + CancellationToken cancellationToken) + { + declarator = currentRoot.GetCurrentNode(declarator); + identifier = currentRoot.GetCurrentNode(identifier); - var declarator = (VariableDeclaratorSyntax)declaratorLocation.FindNode(cancellationToken); - var identifier = (IdentifierNameSyntax)identifierLocation.FindNode(cancellationToken); - var invocationOrCreation = (ExpressionSyntax)invocationOrCreationLocation.FindNode( - getInnermostNodeForTie: true, cancellationToken: cancellationToken); + var editor = new SyntaxEditor(currentRoot, document.Project.Solution.Services); + var sourceText = currentRoot.GetText(); - return (declarator, identifier, invocationOrCreation); - } + var declaration = (VariableDeclarationSyntax)declarator.Parent; + var singleDeclarator = declaration.Variables.Count == 1; - private static SyntaxNode ReplaceIdentifierWithInlineDeclaration( - Document document, - CSharpCodeFixOptionsProvider options, SemanticModel semanticModel, - SyntaxNode currentRoot, VariableDeclaratorSyntax declarator, - IdentifierNameSyntax identifier, SyntaxNode currentNode, - HashSet declarationsToRemove, - CancellationToken cancellationToken) + if (singleDeclarator) { - declarator = currentRoot.GetCurrentNode(declarator); - identifier = currentRoot.GetCurrentNode(identifier); - - var editor = new SyntaxEditor(currentRoot, document.Project.Solution.Services); - var sourceText = currentRoot.GetText(); - - var declaration = (VariableDeclarationSyntax)declarator.Parent; - var singleDeclarator = declaration.Variables.Count == 1; - - if (singleDeclarator) + // This was a local statement with a single variable in it. Just Remove + // the entire local declaration statement. Note that comments belonging to + // this local statement will be moved to be above the statement containing + // the out-var. + var localDeclarationStatement = (LocalDeclarationStatementSyntax)declaration.Parent; + var block = (BlockSyntax)localDeclarationStatement.Parent; + var declarationIndex = block.Statements.IndexOf(localDeclarationStatement); + + // Try to find a predecessor Statement on the same line that isn't going to be removed + StatementSyntax priorStatementSyntax = null; + var localDeclarationToken = localDeclarationStatement.GetFirstToken(); + for (var i = declarationIndex - 1; i >= 0; i--) { - // This was a local statement with a single variable in it. Just Remove - // the entire local declaration statement. Note that comments belonging to - // this local statement will be moved to be above the statement containing - // the out-var. - var localDeclarationStatement = (LocalDeclarationStatementSyntax)declaration.Parent; - var block = (BlockSyntax)localDeclarationStatement.Parent; - var declarationIndex = block.Statements.IndexOf(localDeclarationStatement); - - // Try to find a predecessor Statement on the same line that isn't going to be removed - StatementSyntax priorStatementSyntax = null; - var localDeclarationToken = localDeclarationStatement.GetFirstToken(); - for (var i = declarationIndex - 1; i >= 0; i--) + var statementSyntax = block.Statements[i]; + if (declarationsToRemove.Contains(statementSyntax)) { - var statementSyntax = block.Statements[i]; - if (declarationsToRemove.Contains(statementSyntax)) - { - continue; - } - - if (sourceText.AreOnSameLine(statementSyntax.GetLastToken(), localDeclarationToken)) - { - priorStatementSyntax = statementSyntax; - } - - break; + continue; } - if (priorStatementSyntax != null) + if (sourceText.AreOnSameLine(statementSyntax.GetLastToken(), localDeclarationToken)) { - // There's another statement on the same line as this declaration statement. - // i.e. int a; int b; - // - // Just move all trivia from our statement to be trailing trivia of the previous - // statement - editor.ReplaceNode( - priorStatementSyntax, - (s, g) => s.WithAppendedTrailingTrivia(localDeclarationStatement.GetTrailingTrivia())); + priorStatementSyntax = statementSyntax; } - else - { - // Trivia on the local declaration will move to the next statement. - // use the callback form as the next statement may be the place where we're - // inlining the declaration, and thus need to see the effects of that change. - - // Find the next Statement that isn't going to be removed. - // We initialize this to null here but we must see at least the statement - // into which the declaration is going to be inlined so this will be not null - StatementSyntax nextStatementSyntax = null; - for (var i = declarationIndex + 1; i < block.Statements.Count; i++) - { - var statement = block.Statements[i]; - if (!declarationsToRemove.Contains(statement)) - { - nextStatementSyntax = statement; - break; - } - } - editor.ReplaceNode( - nextStatementSyntax, - (s, g) => s.WithPrependedNonIndentationTriviaFrom(localDeclarationStatement) - .WithAdditionalAnnotations(Formatter.Annotation)); - } + break; + } - // The above code handled the moving of trivia. So remove the node, keeping around no trivia from it. - editor.RemoveNode(localDeclarationStatement, SyntaxRemoveOptions.KeepNoTrivia); + if (priorStatementSyntax != null) + { + // There's another statement on the same line as this declaration statement. + // i.e. int a; int b; + // + // Just move all trivia from our statement to be trailing trivia of the previous + // statement + editor.ReplaceNode( + priorStatementSyntax, + (s, g) => s.WithAppendedTrailingTrivia(localDeclarationStatement.GetTrailingTrivia())); } else { - // Otherwise, just remove the single declarator. Note: we'll move the comments - // 'on' the declarator to the out-var location. This is a little bit trickier - // than normal due to how our comment-association rules work. i.e. if you have: - // - // var /*c1*/ i /*c2*/, /*c3*/ j /*c4*/; - // - // In this case 'c1' is owned by the 'var' token, not 'i', and 'c3' is owned by - // the comment token not 'j'. - - editor.RemoveNode(declarator); - if (declarator == declaration.Variables[0]) + // Trivia on the local declaration will move to the next statement. + // use the callback form as the next statement may be the place where we're + // inlining the declaration, and thus need to see the effects of that change. + + // Find the next Statement that isn't going to be removed. + // We initialize this to null here but we must see at least the statement + // into which the declaration is going to be inlined so this will be not null + StatementSyntax nextStatementSyntax = null; + for (var i = declarationIndex + 1; i < block.Statements.Count; i++) { - // If we're removing the first declarator, and it's on the same line - // as the previous token, then we want to remove all the trivia belonging - // to the previous token. We're going to move it along with this declarator. - // If we don't, then the comment will stay with the previous token. - // - // Note that the moving of the comment happens later on when we make the - // declaration expression. - if (sourceText.AreOnSameLine(declarator.GetFirstToken(), declarator.GetFirstToken().GetPreviousToken(includeSkipped: true))) + var statement = block.Statements[i]; + if (!declarationsToRemove.Contains(statement)) { - editor.ReplaceNode( - declaration.Type, - (t, g) => t.WithTrailingTrivia(SyntaxFactory.ElasticSpace).WithoutAnnotations(Formatter.Annotation)); + nextStatementSyntax = statement; + break; } } + + editor.ReplaceNode( + nextStatementSyntax, + (s, g) => s.WithPrependedNonIndentationTriviaFrom(localDeclarationStatement) + .WithAdditionalAnnotations(Formatter.Annotation)); } - // get the type that we want to put in the out-var-decl based on the user's options. - // i.e. prefer 'out var' if that is what the user wants. Note: if we have: + // The above code handled the moving of trivia. So remove the node, keeping around no trivia from it. + editor.RemoveNode(localDeclarationStatement, SyntaxRemoveOptions.KeepNoTrivia); + } + else + { + // Otherwise, just remove the single declarator. Note: we'll move the comments + // 'on' the declarator to the out-var location. This is a little bit trickier + // than normal due to how our comment-association rules work. i.e. if you have: // - // Method(out var x) + // var /*c1*/ i /*c2*/, /*c3*/ j /*c4*/; // - // Then the type is not-apparent, and we should not use var if the user only wants - // it for apparent types + // In this case 'c1' is owned by the 'var' token, not 'i', and 'c3' is owned by + // the comment token not 'j'. - var local = (ILocalSymbol)semanticModel.GetDeclaredSymbol(declarator, cancellationToken); - var newType = GenerateTypeSyntaxOrVar(local.Type, options); + editor.RemoveNode(declarator); + if (declarator == declaration.Variables[0]) + { + // If we're removing the first declarator, and it's on the same line + // as the previous token, then we want to remove all the trivia belonging + // to the previous token. We're going to move it along with this declarator. + // If we don't, then the comment will stay with the previous token. + // + // Note that the moving of the comment happens later on when we make the + // declaration expression. + if (sourceText.AreOnSameLine(declarator.GetFirstToken(), declarator.GetFirstToken().GetPreviousToken(includeSkipped: true))) + { + editor.ReplaceNode( + declaration.Type, + (t, g) => t.WithTrailingTrivia(SyntaxFactory.ElasticSpace).WithoutAnnotations(Formatter.Annotation)); + } + } + } - var declarationExpression = GetDeclarationExpression( - sourceText, identifier, newType, singleDeclarator ? null : declarator); + // get the type that we want to put in the out-var-decl based on the user's options. + // i.e. prefer 'out var' if that is what the user wants. Note: if we have: + // + // Method(out var x) + // + // Then the type is not-apparent, and we should not use var if the user only wants + // it for apparent types - // Check if using out-var changed problem semantics. - var semanticsChanged = SemanticsChanged(semanticModel, currentNode, identifier, declarationExpression, cancellationToken); - if (semanticsChanged) - { - // Switching to 'var' changed semantics. Just use the original type of the local. + var local = (ILocalSymbol)semanticModel.GetDeclaredSymbol(declarator, cancellationToken); + var newType = GenerateTypeSyntaxOrVar(local.Type, options); - // If the user originally wrote it something other than 'var', then use what they - // wrote. Otherwise, synthesize the actual type of the local. - var explicitType = declaration.Type.IsVar ? local.Type?.GenerateTypeSyntax() : declaration.Type; - declarationExpression = SyntaxFactory.DeclarationExpression(explicitType, declarationExpression.Designation); - } + var declarationExpression = GetDeclarationExpression( + sourceText, identifier, newType, singleDeclarator ? null : declarator); - editor.ReplaceNode(identifier, declarationExpression); + // Check if using out-var changed problem semantics. + var semanticsChanged = SemanticsChanged(semanticModel, currentNode, identifier, declarationExpression, cancellationToken); + if (semanticsChanged) + { + // Switching to 'var' changed semantics. Just use the original type of the local. - return editor.GetChangedRoot(); + // If the user originally wrote it something other than 'var', then use what they + // wrote. Otherwise, synthesize the actual type of the local. + var explicitType = declaration.Type.IsVar ? local.Type?.GenerateTypeSyntax() : declaration.Type; + declarationExpression = SyntaxFactory.DeclarationExpression(explicitType, declarationExpression.Designation); } - public static TypeSyntax GenerateTypeSyntaxOrVar( - ITypeSymbol symbol, CSharpCodeFixOptionsProvider options) + editor.ReplaceNode(identifier, declarationExpression); + + return editor.GetChangedRoot(); + } + + public static TypeSyntax GenerateTypeSyntaxOrVar( + ITypeSymbol symbol, CSharpCodeFixOptionsProvider options) + { + var useVar = IsVarDesired(symbol, options); + + // Note: we cannot use ".GenerateTypeSyntax()" only here. that's because we're + // actually creating a DeclarationExpression and currently the Simplifier cannot + // analyze those due to limitations between how it uses Speculative SemanticModels + // and how those don't handle new declarations well. + return useVar + ? SyntaxFactory.IdentifierName("var") + : symbol.GenerateTypeSyntax(); + } + + private static bool IsVarDesired(ITypeSymbol type, CSharpCodeFixOptionsProvider options) + { + // If they want it for intrinsics, and this is an intrinsic, then use var. + if (type.IsSpecialType() == true) { - var useVar = IsVarDesired(symbol, options); - - // Note: we cannot use ".GenerateTypeSyntax()" only here. that's because we're - // actually creating a DeclarationExpression and currently the Simplifier cannot - // analyze those due to limitations between how it uses Speculative SemanticModels - // and how those don't handle new declarations well. - return useVar - ? SyntaxFactory.IdentifierName("var") - : symbol.GenerateTypeSyntax(); + return options.VarForBuiltInTypes.Value; } - private static bool IsVarDesired(ITypeSymbol type, CSharpCodeFixOptionsProvider options) - { - // If they want it for intrinsics, and this is an intrinsic, then use var. - if (type.IsSpecialType() == true) - { - return options.VarForBuiltInTypes.Value; - } + // If they want "var" whenever possible, then use "var". + return options.VarElsewhere.Value; + } - // If they want "var" whenever possible, then use "var". - return options.VarElsewhere.Value; - } + private static DeclarationExpressionSyntax GetDeclarationExpression( + SourceText sourceText, IdentifierNameSyntax identifier, + TypeSyntax newType, VariableDeclaratorSyntax declaratorOpt) + { + var designation = SyntaxFactory.SingleVariableDesignation(identifier.Identifier); - private static DeclarationExpressionSyntax GetDeclarationExpression( - SourceText sourceText, IdentifierNameSyntax identifier, - TypeSyntax newType, VariableDeclaratorSyntax declaratorOpt) + if (declaratorOpt != null) { - var designation = SyntaxFactory.SingleVariableDesignation(identifier.Identifier); - - if (declaratorOpt != null) + // We're removing a single declarator. Copy any comments it has to the out-var. + // + // Note: this is tricky due to comment ownership. We want the comments that logically + // belong to the declarator, even if our syntax model attaches them to other tokens. + var precedingTrivia = declaratorOpt.GetAllPrecedingTriviaToPreviousToken( + sourceText, includePreviousTokenTrailingTriviaOnlyIfOnSameLine: true); + if (precedingTrivia.Any(t => t.IsSingleOrMultiLineComment())) { - // We're removing a single declarator. Copy any comments it has to the out-var. - // - // Note: this is tricky due to comment ownership. We want the comments that logically - // belong to the declarator, even if our syntax model attaches them to other tokens. - var precedingTrivia = declaratorOpt.GetAllPrecedingTriviaToPreviousToken( - sourceText, includePreviousTokenTrailingTriviaOnlyIfOnSameLine: true); - if (precedingTrivia.Any(t => t.IsSingleOrMultiLineComment())) - { - designation = designation.WithPrependedLeadingTrivia(MassageTrivia(precedingTrivia)); - } - - if (declaratorOpt.GetTrailingTrivia().Any(t => t.IsSingleOrMultiLineComment())) - { - designation = designation.WithAppendedTrailingTrivia(MassageTrivia(declaratorOpt.GetTrailingTrivia())); - } + designation = designation.WithPrependedLeadingTrivia(MassageTrivia(precedingTrivia)); } - newType = newType.WithoutTrivia().WithAdditionalAnnotations(Formatter.Annotation); - // We need trivia between the type declaration and designation or this will generate - // "out inti", but we might have trivia in the form of comments etc from the original - // designation and in those cases adding elastic trivia will break formatting. - if (!designation.HasLeadingTrivia) + if (declaratorOpt.GetTrailingTrivia().Any(t => t.IsSingleOrMultiLineComment())) { - newType = newType.WithAppendedTrailingTrivia(SyntaxFactory.ElasticSpace); + designation = designation.WithAppendedTrailingTrivia(MassageTrivia(declaratorOpt.GetTrailingTrivia())); } + } - return SyntaxFactory.DeclarationExpression(newType, designation); + newType = newType.WithoutTrivia().WithAdditionalAnnotations(Formatter.Annotation); + // We need trivia between the type declaration and designation or this will generate + // "out inti", but we might have trivia in the form of comments etc from the original + // designation and in those cases adding elastic trivia will break formatting. + if (!designation.HasLeadingTrivia) + { + newType = newType.WithAppendedTrailingTrivia(SyntaxFactory.ElasticSpace); } - private static IEnumerable MassageTrivia(IEnumerable triviaList) + return SyntaxFactory.DeclarationExpression(newType, designation); + } + + private static IEnumerable MassageTrivia(IEnumerable triviaList) + { + foreach (var trivia in triviaList) { - foreach (var trivia in triviaList) + if (trivia.IsSingleOrMultiLineComment()) { - if (trivia.IsSingleOrMultiLineComment()) - { - yield return trivia; - } - else if (trivia.IsWhitespace()) - { - // Condense whitespace down to single spaces. We don't want things like - // indentation spaces to be inserted in the out-var location. It is appropriate - // though to have single spaces to help separate out things like comments and - // tokens though. - yield return SyntaxFactory.Space; - } + yield return trivia; + } + else if (trivia.IsWhitespace()) + { + // Condense whitespace down to single spaces. We don't want things like + // indentation spaces to be inserted in the out-var location. It is appropriate + // though to have single spaces to help separate out things like comments and + // tokens though. + yield return SyntaxFactory.Space; } } + } - private static bool SemanticsChanged( - SemanticModel semanticModel, - SyntaxNode nodeToReplace, - IdentifierNameSyntax identifier, - DeclarationExpressionSyntax declarationExpression, - CancellationToken cancellationToken) + private static bool SemanticsChanged( + SemanticModel semanticModel, + SyntaxNode nodeToReplace, + IdentifierNameSyntax identifier, + DeclarationExpressionSyntax declarationExpression, + CancellationToken cancellationToken) + { + if (declarationExpression.Type.IsVar) { - if (declarationExpression.Type.IsVar) - { - // Options want us to use 'var' if we can. Make sure we didn't change - // the semantics of the call by doing this. + // Options want us to use 'var' if we can. Make sure we didn't change + // the semantics of the call by doing this. - // Find the symbol that the existing invocation points to. - var previousSymbol = semanticModel.GetSymbolInfo(nodeToReplace, cancellationToken).Symbol; + // Find the symbol that the existing invocation points to. + var previousSymbol = semanticModel.GetSymbolInfo(nodeToReplace, cancellationToken).Symbol; - // Now, create a speculative model in which we make the change. Make sure - // we still point to the same symbol afterwards. + // Now, create a speculative model in which we make the change. Make sure + // we still point to the same symbol afterwards. - var topmostContainer = GetTopmostContainer(nodeToReplace); - if (topmostContainer == null) - { - // Couldn't figure out what we were contained in. Have to assume that semantics - // Are changing. - return true; - } + var topmostContainer = GetTopmostContainer(nodeToReplace); + if (topmostContainer == null) + { + // Couldn't figure out what we were contained in. Have to assume that semantics + // Are changing. + return true; + } - var annotation = new SyntaxAnnotation(); - var updatedTopmostContainer = topmostContainer.ReplaceNode( - nodeToReplace, nodeToReplace.ReplaceNode(identifier, declarationExpression) - .WithAdditionalAnnotations(annotation)); + var annotation = new SyntaxAnnotation(); + var updatedTopmostContainer = topmostContainer.ReplaceNode( + nodeToReplace, nodeToReplace.ReplaceNode(identifier, declarationExpression) + .WithAdditionalAnnotations(annotation)); - if (!TryGetSpeculativeSemanticModel(semanticModel, - topmostContainer.SpanStart, updatedTopmostContainer, out var speculativeModel)) - { - // Couldn't figure out the new semantics. Assume semantics changed. - return true; - } + if (!TryGetSpeculativeSemanticModel(semanticModel, + topmostContainer.SpanStart, updatedTopmostContainer, out var speculativeModel)) + { + // Couldn't figure out the new semantics. Assume semantics changed. + return true; + } - var updatedInvocationOrCreation = updatedTopmostContainer.GetAnnotatedNodes(annotation).Single(); - var updatedSymbolInfo = speculativeModel.GetSymbolInfo(updatedInvocationOrCreation, cancellationToken); + var updatedInvocationOrCreation = updatedTopmostContainer.GetAnnotatedNodes(annotation).Single(); + var updatedSymbolInfo = speculativeModel.GetSymbolInfo(updatedInvocationOrCreation, cancellationToken); - if (!SymbolEquivalenceComparer.Instance.Equals(previousSymbol, updatedSymbolInfo.Symbol)) - { - // We're pointing at a new symbol now. Semantic have changed. - return true; - } + if (!SymbolEquivalenceComparer.Instance.Equals(previousSymbol, updatedSymbolInfo.Symbol)) + { + // We're pointing at a new symbol now. Semantic have changed. + return true; } - - return false; } - private static SyntaxNode GetTopmostContainer(SyntaxNode expression) - { - return expression.GetAncestorsOrThis( - a => a is StatementSyntax or - EqualsValueClauseSyntax or - ArrowExpressionClauseSyntax or - ConstructorInitializerSyntax).LastOrDefault(); - } + return false; + } - private static bool TryGetSpeculativeSemanticModel( - SemanticModel semanticModel, - int position, SyntaxNode topmostContainer, - out SemanticModel speculativeModel) - { - switch (topmostContainer) - { - case StatementSyntax statement: - return semanticModel.TryGetSpeculativeSemanticModel(position, statement, out speculativeModel); - case EqualsValueClauseSyntax equalsValue: - return semanticModel.TryGetSpeculativeSemanticModel(position, equalsValue, out speculativeModel); - case ArrowExpressionClauseSyntax arrowExpression: - return semanticModel.TryGetSpeculativeSemanticModel(position, arrowExpression, out speculativeModel); - case ConstructorInitializerSyntax constructorInitializer: - return semanticModel.TryGetSpeculativeSemanticModel(position, constructorInitializer, out speculativeModel); - } + private static SyntaxNode GetTopmostContainer(SyntaxNode expression) + { + return expression.GetAncestorsOrThis( + a => a is StatementSyntax or + EqualsValueClauseSyntax or + ArrowExpressionClauseSyntax or + ConstructorInitializerSyntax).LastOrDefault(); + } - speculativeModel = null; - return false; + private static bool TryGetSpeculativeSemanticModel( + SemanticModel semanticModel, + int position, SyntaxNode topmostContainer, + out SemanticModel speculativeModel) + { + switch (topmostContainer) + { + case StatementSyntax statement: + return semanticModel.TryGetSpeculativeSemanticModel(position, statement, out speculativeModel); + case EqualsValueClauseSyntax equalsValue: + return semanticModel.TryGetSpeculativeSemanticModel(position, equalsValue, out speculativeModel); + case ArrowExpressionClauseSyntax arrowExpression: + return semanticModel.TryGetSpeculativeSemanticModel(position, arrowExpression, out speculativeModel); + case ConstructorInitializerSyntax constructorInitializer: + return semanticModel.TryGetSpeculativeSemanticModel(position, constructorInitializer, out speculativeModel); } + + speculativeModel = null; + return false; } } diff --git a/src/Analyzers/CSharp/CodeFixes/InvokeDelegateWithConditionalAccess/InvokeDelegateWithConditionalAccessCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/InvokeDelegateWithConditionalAccess/InvokeDelegateWithConditionalAccessCodeFixProvider.cs index 82b08e6d06f60..d7a0179652e19 100644 --- a/src/Analyzers/CSharp/CodeFixes/InvokeDelegateWithConditionalAccess/InvokeDelegateWithConditionalAccessCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/InvokeDelegateWithConditionalAccess/InvokeDelegateWithConditionalAccessCodeFixProvider.cs @@ -20,151 +20,150 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.InvokeDelegateWithConditionalAccess +namespace Microsoft.CodeAnalysis.CSharp.InvokeDelegateWithConditionalAccess; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.InvokeDelegateWithConditionalAccess), Shared] +internal partial class InvokeDelegateWithConditionalAccessCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.InvokeDelegateWithConditionalAccess), Shared] - internal partial class InvokeDelegateWithConditionalAccessCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public InvokeDelegateWithConditionalAccessCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public InvokeDelegateWithConditionalAccessCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } = [IDEDiagnosticIds.InvokeDelegateWithConditionalAccessId]; + public override ImmutableArray FixableDiagnosticIds { get; } = [IDEDiagnosticIds.InvokeDelegateWithConditionalAccessId]; - // Filter out the diagnostics we created for the faded out code. We don't want - // to try to fix those as well as the normal diagnostics we created. - protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) - => !diagnostic.Properties.ContainsKey(WellKnownDiagnosticTags.Unnecessary); + // Filter out the diagnostics we created for the faded out code. We don't want + // to try to fix those as well as the normal diagnostics we created. + protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) + => !diagnostic.Properties.ContainsKey(WellKnownDiagnosticTags.Unnecessary); - public override Task RegisterCodeFixesAsync(CodeFixContext context) + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Simplify_delegate_invocation, nameof(CSharpAnalyzersResources.Simplify_delegate_invocation)); + return Task.CompletedTask; + } + + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) { - RegisterCodeFix(context, CSharpAnalyzersResources.Simplify_delegate_invocation, nameof(CSharpAnalyzersResources.Simplify_delegate_invocation)); - return Task.CompletedTask; + cancellationToken.ThrowIfCancellationRequested(); + AddEdits(editor, diagnostic, cancellationToken); } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - foreach (var diagnostic in diagnostics) - { - cancellationToken.ThrowIfCancellationRequested(); - AddEdits(editor, diagnostic, cancellationToken); - } + return Task.CompletedTask; + } - return Task.CompletedTask; + private static void AddEdits( + SyntaxEditor editor, Diagnostic diagnostic, CancellationToken cancellationToken) + { + if (diagnostic.Properties[Constants.Kind] == Constants.VariableAndIfStatementForm) + { + HandleVariableAndIfStatementForm(editor, diagnostic, cancellationToken); } - - private static void AddEdits( - SyntaxEditor editor, Diagnostic diagnostic, CancellationToken cancellationToken) + else { - if (diagnostic.Properties[Constants.Kind] == Constants.VariableAndIfStatementForm) - { - HandleVariableAndIfStatementForm(editor, diagnostic, cancellationToken); - } - else - { - Debug.Assert(diagnostic.Properties[Constants.Kind] == Constants.SingleIfStatementForm); - HandleSingleIfStatementForm(editor, diagnostic, cancellationToken); - } + Debug.Assert(diagnostic.Properties[Constants.Kind] == Constants.SingleIfStatementForm); + HandleSingleIfStatementForm(editor, diagnostic, cancellationToken); } + } - private static void HandleSingleIfStatementForm( - SyntaxEditor editor, - Diagnostic diagnostic, - CancellationToken cancellationToken) - { - var root = editor.OriginalRoot; + private static void HandleSingleIfStatementForm( + SyntaxEditor editor, + Diagnostic diagnostic, + CancellationToken cancellationToken) + { + var root = editor.OriginalRoot; - var ifStatementLocation = diagnostic.AdditionalLocations[0]; - var expressionStatementLocation = diagnostic.AdditionalLocations[1]; + var ifStatementLocation = diagnostic.AdditionalLocations[0]; + var expressionStatementLocation = diagnostic.AdditionalLocations[1]; - var ifStatement = (IfStatementSyntax)root.FindNode(ifStatementLocation.SourceSpan); - cancellationToken.ThrowIfCancellationRequested(); + var ifStatement = (IfStatementSyntax)root.FindNode(ifStatementLocation.SourceSpan); + cancellationToken.ThrowIfCancellationRequested(); - var expressionStatement = (ExpressionStatementSyntax)root.FindNode(expressionStatementLocation.SourceSpan); - cancellationToken.ThrowIfCancellationRequested(); + var expressionStatement = (ExpressionStatementSyntax)root.FindNode(expressionStatementLocation.SourceSpan); + cancellationToken.ThrowIfCancellationRequested(); - var invocationExpression = (InvocationExpressionSyntax)expressionStatement.Expression; - cancellationToken.ThrowIfCancellationRequested(); + var invocationExpression = (InvocationExpressionSyntax)expressionStatement.Expression; + cancellationToken.ThrowIfCancellationRequested(); - var (invokedExpression, invokeName) = - invocationExpression.Expression is MemberAccessExpressionSyntax { Name: IdentifierNameSyntax { Identifier.ValueText: nameof(Action.Invoke) } } memberAccessExpression - ? (memberAccessExpression.Expression, memberAccessExpression.Name) - : (invocationExpression.Expression, SyntaxFactory.IdentifierName(nameof(Action.Invoke))); + var (invokedExpression, invokeName) = + invocationExpression.Expression is MemberAccessExpressionSyntax { Name: IdentifierNameSyntax { Identifier.ValueText: nameof(Action.Invoke) } } memberAccessExpression + ? (memberAccessExpression.Expression, memberAccessExpression.Name) + : (invocationExpression.Expression, SyntaxFactory.IdentifierName(nameof(Action.Invoke))); - StatementSyntax newStatement = expressionStatement.WithExpression( - SyntaxFactory.ConditionalAccessExpression( - invokedExpression, - SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberBindingExpression(invokeName), invocationExpression.ArgumentList))); - newStatement = newStatement.WithPrependedLeadingTrivia(ifStatement.GetLeadingTrivia()); + StatementSyntax newStatement = expressionStatement.WithExpression( + SyntaxFactory.ConditionalAccessExpression( + invokedExpression, + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberBindingExpression(invokeName), invocationExpression.ArgumentList))); + newStatement = newStatement.WithPrependedLeadingTrivia(ifStatement.GetLeadingTrivia()); - if (ifStatement.Parent.IsKind(SyntaxKind.ElseClause) && - ifStatement.Statement is BlockSyntax block) - { - newStatement = block.WithStatements([newStatement]); - } + if (ifStatement.Parent.IsKind(SyntaxKind.ElseClause) && + ifStatement.Statement is BlockSyntax block) + { + newStatement = block.WithStatements([newStatement]); + } - newStatement = newStatement.WithAdditionalAnnotations(Formatter.Annotation); - newStatement = AppendTriviaWithoutEndOfLines(newStatement, ifStatement); + newStatement = newStatement.WithAdditionalAnnotations(Formatter.Annotation); + newStatement = AppendTriviaWithoutEndOfLines(newStatement, ifStatement); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - editor.ReplaceNode(ifStatement, newStatement); - } + editor.ReplaceNode(ifStatement, newStatement); + } - private static void HandleVariableAndIfStatementForm( - SyntaxEditor editor, Diagnostic diagnostic, CancellationToken cancellationToken) - { - var root = editor.OriginalRoot; + private static void HandleVariableAndIfStatementForm( + SyntaxEditor editor, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = editor.OriginalRoot; - var localDeclarationLocation = diagnostic.AdditionalLocations[0]; - var ifStatementLocation = diagnostic.AdditionalLocations[1]; - var expressionStatementLocation = diagnostic.AdditionalLocations[2]; + var localDeclarationLocation = diagnostic.AdditionalLocations[0]; + var ifStatementLocation = diagnostic.AdditionalLocations[1]; + var expressionStatementLocation = diagnostic.AdditionalLocations[2]; - var localDeclarationStatement = (LocalDeclarationStatementSyntax)root.FindNode(localDeclarationLocation.SourceSpan); - cancellationToken.ThrowIfCancellationRequested(); + var localDeclarationStatement = (LocalDeclarationStatementSyntax)root.FindNode(localDeclarationLocation.SourceSpan); + cancellationToken.ThrowIfCancellationRequested(); - var ifStatement = (IfStatementSyntax)root.FindNode(ifStatementLocation.SourceSpan); - cancellationToken.ThrowIfCancellationRequested(); + var ifStatement = (IfStatementSyntax)root.FindNode(ifStatementLocation.SourceSpan); + cancellationToken.ThrowIfCancellationRequested(); - var expressionStatement = (ExpressionStatementSyntax)root.FindNode(expressionStatementLocation.SourceSpan); - cancellationToken.ThrowIfCancellationRequested(); + var expressionStatement = (ExpressionStatementSyntax)root.FindNode(expressionStatementLocation.SourceSpan); + cancellationToken.ThrowIfCancellationRequested(); - var invocationExpression = (InvocationExpressionSyntax)expressionStatement.Expression; - var parentBlock = (BlockSyntax)localDeclarationStatement.GetRequiredParent(); + var invocationExpression = (InvocationExpressionSyntax)expressionStatement.Expression; + var parentBlock = (BlockSyntax)localDeclarationStatement.GetRequiredParent(); - var invokeName = - invocationExpression.Expression is MemberAccessExpressionSyntax { Name: IdentifierNameSyntax { Identifier.ValueText: nameof(Action.Invoke) } } memberAccessExpression - ? memberAccessExpression.Name - : SyntaxFactory.IdentifierName(nameof(Action.Invoke)); + var invokeName = + invocationExpression.Expression is MemberAccessExpressionSyntax { Name: IdentifierNameSyntax { Identifier.ValueText: nameof(Action.Invoke) } } memberAccessExpression + ? memberAccessExpression.Name + : SyntaxFactory.IdentifierName(nameof(Action.Invoke)); - var newStatement = expressionStatement.WithExpression( - SyntaxFactory.ConditionalAccessExpression( - localDeclarationStatement.Declaration.Variables[0].Initializer!.Value.Parenthesize(), - SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberBindingExpression(invokeName), invocationExpression.ArgumentList))); + var newStatement = expressionStatement.WithExpression( + SyntaxFactory.ConditionalAccessExpression( + localDeclarationStatement.Declaration.Variables[0].Initializer!.Value.Parenthesize(), + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberBindingExpression(invokeName), invocationExpression.ArgumentList))); - newStatement = newStatement.WithAdditionalAnnotations(Formatter.Annotation); - newStatement = AppendTriviaWithoutEndOfLines(newStatement, ifStatement); + newStatement = newStatement.WithAdditionalAnnotations(Formatter.Annotation); + newStatement = AppendTriviaWithoutEndOfLines(newStatement, ifStatement); - editor.ReplaceNode(ifStatement, newStatement); - editor.RemoveNode(localDeclarationStatement, SyntaxRemoveOptions.KeepLeadingTrivia | SyntaxRemoveOptions.AddElasticMarker); - cancellationToken.ThrowIfCancellationRequested(); - } + editor.ReplaceNode(ifStatement, newStatement); + editor.RemoveNode(localDeclarationStatement, SyntaxRemoveOptions.KeepLeadingTrivia | SyntaxRemoveOptions.AddElasticMarker); + cancellationToken.ThrowIfCancellationRequested(); + } - private static T AppendTriviaWithoutEndOfLines(T newStatement, IfStatementSyntax ifStatement) where T : SyntaxNode - { - // We're combining trivia from the delegate invocation and the end of the if statement - // but we don't want two EndOfLines so we ignore the one on the invocation (if it exists) - var expressionTrivia = newStatement.GetTrailingTrivia(); - var expressionTriviaWithoutEndOfLine = expressionTrivia.Where(t => !t.IsKind(SyntaxKind.EndOfLineTrivia)); - var ifStatementTrivia = ifStatement.GetTrailingTrivia(); + private static T AppendTriviaWithoutEndOfLines(T newStatement, IfStatementSyntax ifStatement) where T : SyntaxNode + { + // We're combining trivia from the delegate invocation and the end of the if statement + // but we don't want two EndOfLines so we ignore the one on the invocation (if it exists) + var expressionTrivia = newStatement.GetTrailingTrivia(); + var expressionTriviaWithoutEndOfLine = expressionTrivia.Where(t => !t.IsKind(SyntaxKind.EndOfLineTrivia)); + var ifStatementTrivia = ifStatement.GetTrailingTrivia(); - return newStatement.WithTrailingTrivia(expressionTriviaWithoutEndOfLine.Concat(ifStatementTrivia)); - } + return newStatement.WithTrailingTrivia(expressionTriviaWithoutEndOfLine.Concat(ifStatementTrivia)); } } diff --git a/src/Analyzers/CSharp/CodeFixes/Iterator/CSharpAddYieldCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/Iterator/CSharpAddYieldCodeFixProvider.cs index a0b3fc9fc9b84..dba1cfc6890d8 100644 --- a/src/Analyzers/CSharp/CodeFixes/Iterator/CSharpAddYieldCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/Iterator/CSharpAddYieldCodeFixProvider.cs @@ -19,209 +19,208 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.Iterator +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.Iterator; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ChangeToYield), Shared] +internal class CSharpAddYieldCodeFixProvider : AbstractIteratorCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ChangeToYield), Shared] - internal class CSharpAddYieldCodeFixProvider : AbstractIteratorCodeFixProvider + /// + /// CS0029: Cannot implicitly convert from type 'x' to 'y' + /// + private const string CS0029 = nameof(CS0029); + + /// + /// CS0266: Cannot implicitly convert from type 'x' to 'y'. An explicit conversion exists (are you missing a cast?) + /// + private const string CS0266 = nameof(CS0266); + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddYieldCodeFixProvider() { - /// - /// CS0029: Cannot implicitly convert from type 'x' to 'y' - /// - private const string CS0029 = nameof(CS0029); + } - /// - /// CS0266: Cannot implicitly convert from type 'x' to 'y'. An explicit conversion exists (are you missing a cast?) - /// - private const string CS0266 = nameof(CS0266); + public override ImmutableArray FixableDiagnosticIds + { + get { return [CS0029, CS0266]; } + } - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddYieldCodeFixProvider() + protected override async Task GetCodeFixAsync(SyntaxNode root, SyntaxNode node, Document document, Diagnostic diagnostics, CancellationToken cancellationToken) + { + // Check if node is return statement + if (node is not ReturnStatementSyntax returnStatement) { + return null; } - public override ImmutableArray FixableDiagnosticIds + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (!TryGetMethodReturnType(node, model, cancellationToken, out var methodReturnType)) { - get { return [CS0029, CS0266]; } + return null; } - protected override async Task GetCodeFixAsync(SyntaxNode root, SyntaxNode node, Document document, Diagnostic diagnostics, CancellationToken cancellationToken) + if (!TryGetExpressionType(model, returnStatement.Expression, out var returnExpressionType)) { - // Check if node is return statement - if (node is not ReturnStatementSyntax returnStatement) - { - return null; - } - - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (!TryGetMethodReturnType(node, model, cancellationToken, out var methodReturnType)) - { - return null; - } + return null; + } - if (!TryGetExpressionType(model, returnStatement.Expression, out var returnExpressionType)) - { - return null; - } + var typeArguments = methodReturnType.GetAllTypeArguments(); - var typeArguments = methodReturnType.GetAllTypeArguments(); + var shouldOfferYieldReturn = typeArguments.Length != 1 + ? IsCorrectTypeForYieldReturn(methodReturnType, model) + : IsCorrectTypeForYieldReturn(typeArguments.Single(), returnExpressionType, methodReturnType, model); - var shouldOfferYieldReturn = typeArguments.Length != 1 - ? IsCorrectTypeForYieldReturn(methodReturnType, model) - : IsCorrectTypeForYieldReturn(typeArguments.Single(), returnExpressionType, methodReturnType, model); + if (!shouldOfferYieldReturn) + { + return null; + } - if (!shouldOfferYieldReturn) - { - return null; - } + var yieldStatement = SyntaxFactory.YieldStatement( + SyntaxKind.YieldReturnStatement, + returnStatement.Expression) + .WithAdditionalAnnotations(Formatter.Annotation); - var yieldStatement = SyntaxFactory.YieldStatement( - SyntaxKind.YieldReturnStatement, - returnStatement.Expression) - .WithAdditionalAnnotations(Formatter.Annotation); + root = root.ReplaceNode(returnStatement, yieldStatement); - root = root.ReplaceNode(returnStatement, yieldStatement); + return CodeAction.Create( + CSharpCodeFixesResources.Replace_return_with_yield_return, + _ => Task.FromResult(document.WithSyntaxRoot(root)), + nameof(CSharpCodeFixesResources.Replace_return_with_yield_return)); + } - return CodeAction.Create( - CSharpCodeFixesResources.Replace_return_with_yield_return, - _ => Task.FromResult(document.WithSyntaxRoot(root)), - nameof(CSharpCodeFixesResources.Replace_return_with_yield_return)); + private static bool TryGetExpressionType( + SemanticModel model, ExpressionSyntax? expression, [NotNullWhen(true)] out ITypeSymbol? returnExpressionType) + { + if (expression == null) + { + returnExpressionType = null; + return false; } - private static bool TryGetExpressionType( - SemanticModel model, ExpressionSyntax? expression, [NotNullWhen(true)] out ITypeSymbol? returnExpressionType) - { - if (expression == null) - { - returnExpressionType = null; - return false; - } + var info = model.GetTypeInfo(expression); + returnExpressionType = info.Type; + return returnExpressionType != null; + } - var info = model.GetTypeInfo(expression); - returnExpressionType = info.Type; - return returnExpressionType != null; + private static bool TryGetMethodReturnType( + SyntaxNode node, SemanticModel model, CancellationToken cancellationToken, + [NotNullWhen(true)] out ITypeSymbol? methodReturnType) + { + methodReturnType = null; + var symbol = model.GetEnclosingSymbol(node.Span.Start, cancellationToken); + if (symbol is not IMethodSymbol method || method.ReturnsVoid) + { + return false; } - private static bool TryGetMethodReturnType( - SyntaxNode node, SemanticModel model, CancellationToken cancellationToken, - [NotNullWhen(true)] out ITypeSymbol? methodReturnType) + methodReturnType = method.ReturnType; + return methodReturnType != null; + } + + private static bool IsCorrectTypeForYieldReturn(ITypeSymbol typeArgument, ITypeSymbol returnExpressionType, ITypeSymbol methodReturnType, SemanticModel model) + { + var ienumerableSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName!); + var ienumeratorSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerator).FullName!); + var ienumerableGenericSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerable<>).FullName!); + var ienumeratorGenericSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerator<>).FullName!); + + if (ienumerableGenericSymbol == null || + ienumerableSymbol == null || + ienumeratorGenericSymbol == null || + ienumeratorSymbol == null) { - methodReturnType = null; - var symbol = model.GetEnclosingSymbol(node.Span.Start, cancellationToken); - if (symbol is not IMethodSymbol method || method.ReturnsVoid) - { - return false; - } + return false; + } + + ienumerableGenericSymbol = ienumerableGenericSymbol.Construct(typeArgument); + ienumeratorGenericSymbol = ienumeratorGenericSymbol.Construct(typeArgument); - methodReturnType = method.ReturnType; - return methodReturnType != null; + if (!CanConvertTypes(typeArgument, returnExpressionType, model)) + { + return false; } - private static bool IsCorrectTypeForYieldReturn(ITypeSymbol typeArgument, ITypeSymbol returnExpressionType, ITypeSymbol methodReturnType, SemanticModel model) + if (!(methodReturnType.Equals(ienumerableGenericSymbol) || + methodReturnType.Equals(ienumerableSymbol) || + methodReturnType.Equals(ienumeratorGenericSymbol) || + methodReturnType.Equals(ienumeratorSymbol))) { - var ienumerableSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName!); - var ienumeratorSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerator).FullName!); - var ienumerableGenericSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerable<>).FullName!); - var ienumeratorGenericSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerator<>).FullName!); + return false; + } - if (ienumerableGenericSymbol == null || - ienumerableSymbol == null || - ienumeratorGenericSymbol == null || - ienumeratorSymbol == null) - { - return false; - } + return true; + } - ienumerableGenericSymbol = ienumerableGenericSymbol.Construct(typeArgument); - ienumeratorGenericSymbol = ienumeratorGenericSymbol.Construct(typeArgument); + private static bool CanConvertTypes(ITypeSymbol typeArgument, ITypeSymbol returnExpressionType, SemanticModel model) + { + // return false if there is no conversion for the top level type + if (!model.Compilation.ClassifyConversion(typeArgument, returnExpressionType).Exists) + { + return false; + } - if (!CanConvertTypes(typeArgument, returnExpressionType, model)) - { - return false; - } + // Classify conversion does not consider type parameters on its own so we will have to recurse through them + var leftArguments = typeArgument.GetTypeArguments(); + var rightArguments = returnExpressionType.GetTypeArguments(); - if (!(methodReturnType.Equals(ienumerableGenericSymbol) || - methodReturnType.Equals(ienumerableSymbol) || - methodReturnType.Equals(ienumeratorGenericSymbol) || - methodReturnType.Equals(ienumeratorSymbol))) - { - return false; - } + // If we have a mismatch in the number of type arguments we can immediately return as there is no way the types are convertible + if (leftArguments != null && + rightArguments != null && + leftArguments.Length != rightArguments.Length) + { + return false; + } + // If there are no more type arguments we assume they are convertible since the outer generic types are convertible + if (leftArguments == null || !leftArguments.Any()) + { return true; } - private static bool CanConvertTypes(ITypeSymbol typeArgument, ITypeSymbol returnExpressionType, SemanticModel model) + // Check if all the type arguments are convertible + for (var i = 0; i < leftArguments.Length; i++) { - // return false if there is no conversion for the top level type - if (!model.Compilation.ClassifyConversion(typeArgument, returnExpressionType).Exists) - { - return false; - } - - // Classify conversion does not consider type parameters on its own so we will have to recurse through them - var leftArguments = typeArgument.GetTypeArguments(); - var rightArguments = returnExpressionType.GetTypeArguments(); - - // If we have a mismatch in the number of type arguments we can immediately return as there is no way the types are convertible - if (leftArguments != null && - rightArguments != null && - leftArguments.Length != rightArguments.Length) + if (!CanConvertTypes(leftArguments[i], rightArguments[i], model)) { return false; } + } - // If there are no more type arguments we assume they are convertible since the outer generic types are convertible - if (leftArguments == null || !leftArguments.Any()) - { - return true; - } + // Type argument comparisons have all succeeded, return true + return true; + } - // Check if all the type arguments are convertible - for (var i = 0; i < leftArguments.Length; i++) - { - if (!CanConvertTypes(leftArguments[i], rightArguments[i], model)) - { - return false; - } - } + private static bool IsCorrectTypeForYieldReturn(ITypeSymbol methodReturnType, SemanticModel model) + { + var ienumerableSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName!); + var ienumeratorSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerator).FullName!); - // Type argument comparisons have all succeeded, return true - return true; + if (ienumerableSymbol == null || + ienumeratorSymbol == null) + { + return false; } - private static bool IsCorrectTypeForYieldReturn(ITypeSymbol methodReturnType, SemanticModel model) + if (!(methodReturnType.Equals(ienumerableSymbol) || + methodReturnType.Equals(ienumeratorSymbol))) { - var ienumerableSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName!); - var ienumeratorSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerator).FullName!); - - if (ienumerableSymbol == null || - ienumeratorSymbol == null) - { - return false; - } - - if (!(methodReturnType.Equals(ienumerableSymbol) || - methodReturnType.Equals(ienumeratorSymbol))) - { - return false; - } - - return true; + return false; } - protected override bool TryGetNode( - SyntaxNode root, TextSpan span, [NotNullWhen(true)] out SyntaxNode? node) - { - node = null; - var ancestors = root.FindToken(span.Start).GetAncestors(); - if (!ancestors.Any()) - { - return false; - } + return true; + } - node = ancestors.FirstOrDefault(n => n.Span.Contains(span) && n != root && n.IsKind(SyntaxKind.ReturnStatement)); - return node != null; + protected override bool TryGetNode( + SyntaxNode root, TextSpan span, [NotNullWhen(true)] out SyntaxNode? node) + { + node = null; + var ancestors = root.FindToken(span.Start).GetAncestors(); + if (!ancestors.Any()) + { + return false; } + + node = ancestors.FirstOrDefault(n => n.Span.Contains(span) && n != root && n.IsKind(SyntaxKind.ReturnStatement)); + return node != null; } } diff --git a/src/Analyzers/CSharp/CodeFixes/Iterator/CSharpChangeToIEnumerableCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/Iterator/CSharpChangeToIEnumerableCodeFixProvider.cs index 9b84a4ed453c1..3043fe7f66fa2 100644 --- a/src/Analyzers/CSharp/CodeFixes/Iterator/CSharpChangeToIEnumerableCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/Iterator/CSharpChangeToIEnumerableCodeFixProvider.cs @@ -18,112 +18,111 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.Iterator +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.Iterator; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ChangeReturnType), Shared] +internal class CSharpChangeToIEnumerableCodeFixProvider : AbstractIteratorCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ChangeReturnType), Shared] - internal class CSharpChangeToIEnumerableCodeFixProvider : AbstractIteratorCodeFixProvider + /// + /// CS1624: The body of 'x' cannot be an iterator block because 'y' is not an iterator interface type + /// + private const string CS1624 = nameof(CS1624); + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpChangeToIEnumerableCodeFixProvider() + { + } + + public override ImmutableArray FixableDiagnosticIds { - /// - /// CS1624: The body of 'x' cannot be an iterator block because 'y' is not an iterator interface type - /// - private const string CS1624 = nameof(CS1624); - - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpChangeToIEnumerableCodeFixProvider() + get { return [CS1624]; } + } + + protected override async Task GetCodeFixAsync(SyntaxNode root, SyntaxNode node, Document document, Diagnostic diagnostics, CancellationToken cancellationToken) + { + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var methodSymbol = model.GetDeclaredSymbol(node, cancellationToken) as IMethodSymbol; + // IMethod symbol can either be a regular method or an accessor + if (methodSymbol?.ReturnType == null || methodSymbol.ReturnsVoid) { + return null; } - public override ImmutableArray FixableDiagnosticIds + var type = methodSymbol.ReturnType; + if (!TryGetIEnumerableSymbols(model, out var ienumerableSymbol, out var ienumerableGenericSymbol)) { - get { return [CS1624]; } + return null; } - protected override async Task GetCodeFixAsync(SyntaxNode root, SyntaxNode node, Document document, Diagnostic diagnostics, CancellationToken cancellationToken) + if (type.InheritsFromOrEquals(ienumerableSymbol, includeInterfaces: true)) { - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var methodSymbol = model.GetDeclaredSymbol(node, cancellationToken) as IMethodSymbol; - // IMethod symbol can either be a regular method or an accessor - if (methodSymbol?.ReturnType == null || methodSymbol.ReturnsVoid) - { - return null; - } - - var type = methodSymbol.ReturnType; - if (!TryGetIEnumerableSymbols(model, out var ienumerableSymbol, out var ienumerableGenericSymbol)) + var arity = type.GetArity(); + if (arity == 1) { - return null; + var typeArg = type.GetTypeArguments().First(); + ienumerableGenericSymbol = ienumerableGenericSymbol.Construct(typeArg); } - - if (type.InheritsFromOrEquals(ienumerableSymbol, includeInterfaces: true)) + else if (arity == 0 && type is IArrayTypeSymbol arrayType) { - var arity = type.GetArity(); - if (arity == 1) - { - var typeArg = type.GetTypeArguments().First(); - ienumerableGenericSymbol = ienumerableGenericSymbol.Construct(typeArg); - } - else if (arity == 0 && type is IArrayTypeSymbol arrayType) - { - ienumerableGenericSymbol = ienumerableGenericSymbol.Construct(arrayType.ElementType); - } - else - { - return null; - } + ienumerableGenericSymbol = ienumerableGenericSymbol.Construct(arrayType.ElementType); } else { - ienumerableGenericSymbol = ienumerableGenericSymbol.Construct(type); + return null; } + } + else + { + ienumerableGenericSymbol = ienumerableGenericSymbol.Construct(type); + } - var newReturnType = ienumerableGenericSymbol.GenerateTypeSyntax(); - Document? newDocument = null; - var newMethodDeclarationSyntax = (node as MethodDeclarationSyntax)?.WithReturnType(newReturnType); - if (newMethodDeclarationSyntax != null) - { - newDocument = document.WithSyntaxRoot(root.ReplaceNode(node, newMethodDeclarationSyntax)); - } + var newReturnType = ienumerableGenericSymbol.GenerateTypeSyntax(); + Document? newDocument = null; + var newMethodDeclarationSyntax = (node as MethodDeclarationSyntax)?.WithReturnType(newReturnType); + if (newMethodDeclarationSyntax != null) + { + newDocument = document.WithSyntaxRoot(root.ReplaceNode(node, newMethodDeclarationSyntax)); + } - var newOperator = (node as OperatorDeclarationSyntax)?.WithReturnType(newReturnType); - if (newOperator != null) - { - newDocument = document.WithSyntaxRoot(root.ReplaceNode(node, newOperator)); - } + var newOperator = (node as OperatorDeclarationSyntax)?.WithReturnType(newReturnType); + if (newOperator != null) + { + newDocument = document.WithSyntaxRoot(root.ReplaceNode(node, newOperator)); + } - var oldAccessor = node.Parent?.Parent as PropertyDeclarationSyntax; - if (oldAccessor != null) - { - newDocument = document.WithSyntaxRoot(root.ReplaceNode(oldAccessor, oldAccessor.WithType(newReturnType))); - } + var oldAccessor = node.Parent?.Parent as PropertyDeclarationSyntax; + if (oldAccessor != null) + { + newDocument = document.WithSyntaxRoot(root.ReplaceNode(oldAccessor, oldAccessor.WithType(newReturnType))); + } - var oldIndexer = node.Parent?.Parent as IndexerDeclarationSyntax; - if (oldIndexer != null) - { - newDocument = document.WithSyntaxRoot(root.ReplaceNode(oldIndexer, oldIndexer.WithType(newReturnType))); - } + var oldIndexer = node.Parent?.Parent as IndexerDeclarationSyntax; + if (oldIndexer != null) + { + newDocument = document.WithSyntaxRoot(root.ReplaceNode(oldIndexer, oldIndexer.WithType(newReturnType))); + } - if (newDocument == null) - { - return null; - } + if (newDocument == null) + { + return null; + } - var title = string.Format(CSharpCodeFixesResources.Change_return_type_from_0_to_1, - type.ToMinimalDisplayString(model, node.SpanStart), - ienumerableGenericSymbol.ToMinimalDisplayString(model, node.SpanStart)); + var title = string.Format(CSharpCodeFixesResources.Change_return_type_from_0_to_1, + type.ToMinimalDisplayString(model, node.SpanStart), + ienumerableGenericSymbol.ToMinimalDisplayString(model, node.SpanStart)); - return CodeAction.Create(title, _ => Task.FromResult(newDocument), title); - } + return CodeAction.Create(title, _ => Task.FromResult(newDocument), title); + } - private static bool TryGetIEnumerableSymbols( - SemanticModel model, - [NotNullWhen(true)] out INamedTypeSymbol? ienumerableSymbol, - [NotNullWhen(true)] out INamedTypeSymbol? ienumerableGenericSymbol) - { - ienumerableSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName!); - ienumerableGenericSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerable<>).FullName!); + private static bool TryGetIEnumerableSymbols( + SemanticModel model, + [NotNullWhen(true)] out INamedTypeSymbol? ienumerableSymbol, + [NotNullWhen(true)] out INamedTypeSymbol? ienumerableGenericSymbol) + { + ienumerableSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName!); + ienumerableGenericSymbol = model.Compilation.GetTypeByMetadataName(typeof(IEnumerable<>).FullName!); - return ienumerableGenericSymbol != null && ienumerableSymbol != null; - } + return ienumerableGenericSymbol != null && ienumerableSymbol != null; } } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeFieldReadonly/CSharpMakeFieldReadonlyCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MakeFieldReadonly/CSharpMakeFieldReadonlyCodeFixProvider.cs index f4d6fcf13e19a..233c98d80d709 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeFieldReadonly/CSharpMakeFieldReadonlyCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeFieldReadonly/CSharpMakeFieldReadonlyCodeFixProvider.cs @@ -10,21 +10,20 @@ using Microsoft.CodeAnalysis.MakeFieldReadonly; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.MakeFieldReadonly +namespace Microsoft.CodeAnalysis.CSharp.MakeFieldReadonly; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeFieldReadonly), Shared] +internal class CSharpMakeFieldReadonlyCodeFixProvider : AbstractMakeFieldReadonlyCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeFieldReadonly), Shared] - internal class CSharpMakeFieldReadonlyCodeFixProvider : AbstractMakeFieldReadonlyCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpMakeFieldReadonlyCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpMakeFieldReadonlyCodeFixProvider() - { - } + } - protected override SyntaxNode? GetInitializerNode(VariableDeclaratorSyntax declaration) - => declaration.Initializer?.Value; + protected override SyntaxNode? GetInitializerNode(VariableDeclaratorSyntax declaration) + => declaration.Initializer?.Value; - protected override ImmutableList GetVariableDeclarators(FieldDeclarationSyntax fieldDeclaration) - => fieldDeclaration.Declaration.Variables.ToImmutableListOrEmpty(); - } + protected override ImmutableList GetVariableDeclarators(FieldDeclarationSyntax fieldDeclaration) + => fieldDeclaration.Declaration.Variables.ToImmutableListOrEmpty(); } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeFixHelper.cs b/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeFixHelper.cs index 77bc273b5683f..d2240442f6abf 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeFixHelper.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeFixHelper.cs @@ -19,211 +19,210 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic -{ - using static SyntaxFactory; +namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic; + +using static SyntaxFactory; - internal static class MakeLocalFunctionStaticCodeFixHelper +internal static class MakeLocalFunctionStaticCodeFixHelper +{ + public static async Task MakeLocalFunctionStaticAsync( + Document document, + LocalFunctionStatementSyntax localFunction, + ImmutableArray captures, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) { - public static async Task MakeLocalFunctionStaticAsync( - Document document, - LocalFunctionStatementSyntax localFunction, - ImmutableArray captures, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var syntaxEditor = new SyntaxEditor(root, document.Project.Solution.Services); - await MakeLocalFunctionStaticAsync(document, localFunction, captures, syntaxEditor, fallbackOptions, cancellationToken).ConfigureAwait(false); - return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); - } + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var syntaxEditor = new SyntaxEditor(root, document.Project.Solution.Services); + await MakeLocalFunctionStaticAsync(document, localFunction, captures, syntaxEditor, fallbackOptions, cancellationToken).ConfigureAwait(false); + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } - public static async Task MakeLocalFunctionStaticAsync( - Document document, - LocalFunctionStatementSyntax localFunction, - ImmutableArray captures, - SyntaxEditor syntaxEditor, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var localFunctionSymbol = semanticModel.GetRequiredDeclaredSymbol(localFunction, cancellationToken); - var documentImmutableSet = ImmutableHashSet.Create(document); + public static async Task MakeLocalFunctionStaticAsync( + Document document, + LocalFunctionStatementSyntax localFunction, + ImmutableArray captures, + SyntaxEditor syntaxEditor, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var localFunctionSymbol = semanticModel.GetRequiredDeclaredSymbol(localFunction, cancellationToken); + var documentImmutableSet = ImmutableHashSet.Create(document); - // Finds all the call sites of the local function - var referencedSymbols = await SymbolFinder.FindReferencesAsync( - localFunctionSymbol, document.Project.Solution, documentImmutableSet, cancellationToken).ConfigureAwait(false); + // Finds all the call sites of the local function + var referencedSymbols = await SymbolFinder.FindReferencesAsync( + localFunctionSymbol, document.Project.Solution, documentImmutableSet, cancellationToken).ConfigureAwait(false); - // Now we need to find all the references to the local function that we might need to fix. - var shouldWarn = false; - using var _ = ArrayBuilder.GetInstance(out var invocations); + // Now we need to find all the references to the local function that we might need to fix. + var shouldWarn = false; + using var _ = ArrayBuilder.GetInstance(out var invocations); - foreach (var referencedSymbol in referencedSymbols) + foreach (var referencedSymbol in referencedSymbols) + { + foreach (var location in referencedSymbol.Locations) { - foreach (var location in referencedSymbol.Locations) + // We limited the search scope to the single document, + // so all reference should be in the same tree. + var referenceNode = root.FindNode(location.Location.SourceSpan); + if (referenceNode is not IdentifierNameSyntax identifierNode) { - // We limited the search scope to the single document, - // so all reference should be in the same tree. - var referenceNode = root.FindNode(location.Location.SourceSpan); - if (referenceNode is not IdentifierNameSyntax identifierNode) - { - // Unexpected scenario, skip and warn. - shouldWarn = true; - continue; - } + // Unexpected scenario, skip and warn. + shouldWarn = true; + continue; + } - if (identifierNode.Parent is InvocationExpressionSyntax invocation) - { - invocations.Add(invocation); - } - else - { - // We won't be able to fix non-invocation references, - // e.g. creating a delegate. - shouldWarn = true; - } + if (identifierNode.Parent is InvocationExpressionSyntax invocation) + { + invocations.Add(invocation); + } + else + { + // We won't be able to fix non-invocation references, + // e.g. creating a delegate. + shouldWarn = true; } } + } - var thisParameter = (IParameterSymbol?)captures.FirstOrDefault(c => c.IsThisParameter()); + var thisParameter = (IParameterSymbol?)captures.FirstOrDefault(c => c.IsThisParameter()); - var parameterAndCapturedSymbols = CreateParameterSymbols(captures.WhereAsArray(c => !c.IsThisParameter())); + var parameterAndCapturedSymbols = CreateParameterSymbols(captures.WhereAsArray(c => !c.IsThisParameter())); - // Fix all invocations by passing in additional arguments. - foreach (var invocation in invocations) - { - syntaxEditor.ReplaceNode( - invocation, - (node, generator) => - { - var currentInvocation = (InvocationExpressionSyntax)node; - var seenNamedArgument = currentInvocation.ArgumentList.Arguments.Any(a => a.NameColon != null); - var seenDefaultArgumentValue = currentInvocation.ArgumentList.Arguments.Count < localFunction.ParameterList.Parameters.Count; - - // Add all the non-this parameters to the end. If there is a 'this' parameter, add it to the start. - var newArguments = parameterAndCapturedSymbols.Where(p => !p.symbol.IsThisParameter()).Select( - symbolAndCapture => (ArgumentSyntax)generator.Argument( - seenNamedArgument || seenDefaultArgumentValue ? symbolAndCapture.symbol.Name : null, - symbolAndCapture.symbol.RefKind, - symbolAndCapture.capture.Name.ToIdentifierName())); - - var newArgumentsList = currentInvocation.ArgumentList.Arguments.AddRange(newArguments); - if (thisParameter != null) - newArgumentsList = newArgumentsList.Insert(0, (ArgumentSyntax)generator.Argument(generator.ThisExpression())); - - var newArgList = currentInvocation.ArgumentList.WithArguments(newArgumentsList); - return currentInvocation.WithArgumentList(newArgList); - }); - } + // Fix all invocations by passing in additional arguments. + foreach (var invocation in invocations) + { + syntaxEditor.ReplaceNode( + invocation, + (node, generator) => + { + var currentInvocation = (InvocationExpressionSyntax)node; + var seenNamedArgument = currentInvocation.ArgumentList.Arguments.Any(a => a.NameColon != null); + var seenDefaultArgumentValue = currentInvocation.ArgumentList.Arguments.Count < localFunction.ParameterList.Parameters.Count; + + // Add all the non-this parameters to the end. If there is a 'this' parameter, add it to the start. + var newArguments = parameterAndCapturedSymbols.Where(p => !p.symbol.IsThisParameter()).Select( + symbolAndCapture => (ArgumentSyntax)generator.Argument( + seenNamedArgument || seenDefaultArgumentValue ? symbolAndCapture.symbol.Name : null, + symbolAndCapture.symbol.RefKind, + symbolAndCapture.capture.Name.ToIdentifierName())); + + var newArgumentsList = currentInvocation.ArgumentList.Arguments.AddRange(newArguments); + if (thisParameter != null) + newArgumentsList = newArgumentsList.Insert(0, (ArgumentSyntax)generator.Argument(generator.ThisExpression())); - // In case any of the captured variable isn't camel-cased, - // we need to change the referenced name inside local function to use the new parameter's name. - foreach (var (parameter, capture) in parameterAndCapturedSymbols) - { - if (parameter.Name == capture.Name) - continue; + var newArgList = currentInvocation.ArgumentList.WithArguments(newArgumentsList); + return currentInvocation.WithArgumentList(newArgList); + }); + } - var referencedCaptureSymbols = await SymbolFinder.FindReferencesAsync( - capture, document.Project.Solution, documentImmutableSet, cancellationToken).ConfigureAwait(false); + // In case any of the captured variable isn't camel-cased, + // we need to change the referenced name inside local function to use the new parameter's name. + foreach (var (parameter, capture) in parameterAndCapturedSymbols) + { + if (parameter.Name == capture.Name) + continue; - foreach (var referencedSymbol in referencedCaptureSymbols) + var referencedCaptureSymbols = await SymbolFinder.FindReferencesAsync( + capture, document.Project.Solution, documentImmutableSet, cancellationToken).ConfigureAwait(false); + + foreach (var referencedSymbol in referencedCaptureSymbols) + { + foreach (var location in referencedSymbol.Locations) { - foreach (var location in referencedSymbol.Locations) + var referenceSpan = location.Location.SourceSpan; + if (!localFunction.FullSpan.Contains(referenceSpan)) + continue; + + var referenceNode = root.FindNode(referenceSpan); + if (referenceNode is IdentifierNameSyntax identifierNode) { - var referenceSpan = location.Location.SourceSpan; - if (!localFunction.FullSpan.Contains(referenceSpan)) - continue; - - var referenceNode = root.FindNode(referenceSpan); - if (referenceNode is IdentifierNameSyntax identifierNode) - { - syntaxEditor.ReplaceNode( - identifierNode, - (node, generator) => IdentifierName(parameter.Name.ToIdentifierToken()).WithTriviaFrom(node)); - } + syntaxEditor.ReplaceNode( + identifierNode, + (node, generator) => IdentifierName(parameter.Name.ToIdentifierToken()).WithTriviaFrom(node)); } } } + } - // If we captured 'this', then we have to go through and rewrite all usages of it to @this. Note that - // 'this' may be used explicitly or implicitly. - if (thisParameter != null) + // If we captured 'this', then we have to go through and rewrite all usages of it to @this. Note that + // 'this' may be used explicitly or implicitly. + if (thisParameter != null) + { + var localFunctionBodyOperation = semanticModel.GetOperation(localFunction.Body ?? (SyntaxNode)localFunction.ExpressionBody!.Expression, cancellationToken); + foreach (var descendent in localFunctionBodyOperation.DescendantsAndSelf()) { - var localFunctionBodyOperation = semanticModel.GetOperation(localFunction.Body ?? (SyntaxNode)localFunction.ExpressionBody!.Expression, cancellationToken); - foreach (var descendent in localFunctionBodyOperation.DescendantsAndSelf()) + if (descendent is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } instanceReference) { - if (descendent is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } instanceReference) + if (!instanceReference.IsImplicit) + { + syntaxEditor.ReplaceNode(instanceReference.Syntax, IdentifierName("@this")); + } + else if (instanceReference.Syntax is SimpleNameSyntax name) { - if (!instanceReference.IsImplicit) - { - syntaxEditor.ReplaceNode(instanceReference.Syntax, IdentifierName("@this")); - } - else if (instanceReference.Syntax is SimpleNameSyntax name) - { - syntaxEditor.ReplaceNode(name, MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IdentifierName("@this"), name)); - } + syntaxEditor.ReplaceNode(name, MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IdentifierName("@this"), name)); } } } + } #if CODE_STYLE - var info = new CSharpCodeGenerationContextInfo( - CodeGenerationContext.Default, CSharpCodeGenerationOptions.Default, new CSharpCodeGenerationService(document.Project.GetExtendedLanguageServices().LanguageServices), root.SyntaxTree.Options.LanguageVersion()); + var info = new CSharpCodeGenerationContextInfo( + CodeGenerationContext.Default, CSharpCodeGenerationOptions.Default, new CSharpCodeGenerationService(document.Project.GetExtendedLanguageServices().LanguageServices), root.SyntaxTree.Options.LanguageVersion()); #else - var info = await document.GetCodeGenerationInfoAsync(CodeGenerationContext.Default, fallbackOptions, cancellationToken).ConfigureAwait(false); + var info = await document.GetCodeGenerationInfoAsync(CodeGenerationContext.Default, fallbackOptions, cancellationToken).ConfigureAwait(false); #endif - // Updates the local function declaration with variables passed in as parameters - syntaxEditor.ReplaceNode( - localFunction, - (node, generator) => + // Updates the local function declaration with variables passed in as parameters + syntaxEditor.ReplaceNode( + localFunction, + (node, generator) => + { + var localFunctionWithNewParameters = (LocalFunctionStatementSyntax)info.Service.AddParameters( + node, + parameterAndCapturedSymbols.SelectAsArray(p => p.symbol), + info, + cancellationToken); + + // Add @this parameter as the first parameter to the local function. + if (thisParameter != null) { - var localFunctionWithNewParameters = (LocalFunctionStatementSyntax)info.Service.AddParameters( - node, - parameterAndCapturedSymbols.SelectAsArray(p => p.symbol), - info, - cancellationToken); - - // Add @this parameter as the first parameter to the local function. - if (thisParameter != null) - { - var parameterList = localFunctionWithNewParameters.ParameterList; - var parameters = parameterList.Parameters; - localFunctionWithNewParameters = localFunctionWithNewParameters.ReplaceNode( - parameterList, parameterList.WithParameters(parameters.Insert(0, Parameter(Identifier("@this")).WithType(thisParameter.Type.GenerateTypeSyntax())))); - } - - if (shouldWarn) - { - var annotation = WarningAnnotation.Create(CSharpCodeFixesResources.Warning_colon_Adding_parameters_to_local_function_declaration_may_produce_invalid_code); - localFunctionWithNewParameters = localFunctionWithNewParameters.WithAdditionalAnnotations(annotation); - } - - return AddStaticModifier(localFunctionWithNewParameters, generator); - }); - } + var parameterList = localFunctionWithNewParameters.ParameterList; + var parameters = parameterList.Parameters; + localFunctionWithNewParameters = localFunctionWithNewParameters.ReplaceNode( + parameterList, parameterList.WithParameters(parameters.Insert(0, Parameter(Identifier("@this")).WithType(thisParameter.Type.GenerateTypeSyntax())))); + } - public static SyntaxNode AddStaticModifier(SyntaxNode localFunction, SyntaxGenerator generator) - => generator.WithModifiers( - localFunction, - generator.GetModifiers(localFunction).WithIsStatic(true)); + if (shouldWarn) + { + var annotation = WarningAnnotation.Create(CSharpCodeFixesResources.Warning_colon_Adding_parameters_to_local_function_declaration_may_produce_invalid_code); + localFunctionWithNewParameters = localFunctionWithNewParameters.WithAdditionalAnnotations(annotation); + } - /// - /// Creates a new parameter symbol paired with the original captured symbol for each captured variables. - /// - private static ImmutableArray<(IParameterSymbol symbol, ISymbol capture)> CreateParameterSymbols(ImmutableArray captures) - => captures.SelectAsArray(static capture => - { - var symbolType = capture.GetSymbolType(); - Contract.ThrowIfNull(symbolType); - - return (CodeGenerationSymbolFactory.CreateParameterSymbol( - attributes: default, - refKind: RefKind.None, - isParams: false, - type: symbolType, - name: capture.Name.ToCamelCase()), capture); + return AddStaticModifier(localFunctionWithNewParameters, generator); }); } + + public static SyntaxNode AddStaticModifier(SyntaxNode localFunction, SyntaxGenerator generator) + => generator.WithModifiers( + localFunction, + generator.GetModifiers(localFunction).WithIsStatic(true)); + + /// + /// Creates a new parameter symbol paired with the original captured symbol for each captured variables. + /// + private static ImmutableArray<(IParameterSymbol symbol, ISymbol capture)> CreateParameterSymbols(ImmutableArray captures) + => captures.SelectAsArray(static capture => + { + var symbolType = capture.GetSymbolType(); + Contract.ThrowIfNull(symbolType); + + return (CodeGenerationSymbolFactory.CreateParameterSymbol( + attributes: default, + refKind: RefKind.None, + isParams: false, + type: symbolType, + name: capture.Name.ToCamelCase()), capture); + }); } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeFixProvider.cs index 023504216ee80..59747d0d61848 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeFixProvider.cs @@ -14,34 +14,33 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic +namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeLocalFunctionStatic), Shared] +internal class MakeLocalFunctionStaticCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeLocalFunctionStatic), Shared] - internal class MakeLocalFunctionStaticCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public MakeLocalFunctionStaticCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public MakeLocalFunctionStaticCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } = - [IDEDiagnosticIds.MakeLocalFunctionStaticDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds { get; } = + [IDEDiagnosticIds.MakeLocalFunctionStaticDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Make_local_function_static, nameof(CSharpAnalyzersResources.Make_local_function_static)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Make_local_function_static, nameof(CSharpAnalyzersResources.Make_local_function_static)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var localFunctions = diagnostics.SelectAsArray(d => d.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken)); - foreach (var localFunction in localFunctions) - editor.ReplaceNode(localFunction, MakeLocalFunctionStaticCodeFixHelper.AddStaticModifier); + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var localFunctions = diagnostics.SelectAsArray(d => d.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken)); + foreach (var localFunction in localFunctions) + editor.ReplaceNode(localFunction, MakeLocalFunctionStaticCodeFixHelper.AddStaticModifier); - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/PassInCapturedVariablesAsArgumentsCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/PassInCapturedVariablesAsArgumentsCodeFixProvider.cs index e473d8d69b417..ac4f5e1e8cbe0 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/PassInCapturedVariablesAsArgumentsCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeLocalFunctionStatic/PassInCapturedVariablesAsArgumentsCodeFixProvider.cs @@ -17,82 +17,81 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic +namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.PassInCapturedVariables), Shared] +internal sealed class PassInCapturedVariablesAsArgumentsCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.PassInCapturedVariables), Shared] - internal sealed class PassInCapturedVariablesAsArgumentsCodeFixProvider : SyntaxEditorBasedCodeFixProvider - { - private const string CS8421 = nameof(CS8421); // A static local function can't contain a reference to . + private const string CS8421 = nameof(CS8421); // A static local function can't contain a reference to . - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public PassInCapturedVariablesAsArgumentsCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public PassInCapturedVariablesAsArgumentsCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds { get; } = [CS8421]; + public override ImmutableArray FixableDiagnosticIds { get; } = [CS8421]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var diagnostic = context.Diagnostics.First(); + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); - return WrapFixAsync( - context.Document, - [diagnostic], - (document, localFunction, captures) => - { - context.RegisterCodeFix( - CodeAction.Create( - CSharpCodeFixesResources.Pass_in_captured_variables_as_arguments, - cancellationToken => MakeLocalFunctionStaticCodeFixHelper.MakeLocalFunctionStaticAsync(document, localFunction, captures, context.GetOptionsProvider(), cancellationToken), - nameof(CSharpCodeFixesResources.Pass_in_captured_variables_as_arguments)), - diagnostic); + return WrapFixAsync( + context.Document, + [diagnostic], + (document, localFunction, captures) => + { + context.RegisterCodeFix( + CodeAction.Create( + CSharpCodeFixesResources.Pass_in_captured_variables_as_arguments, + cancellationToken => MakeLocalFunctionStaticCodeFixHelper.MakeLocalFunctionStaticAsync(document, localFunction, captures, context.GetOptionsProvider(), cancellationToken), + nameof(CSharpCodeFixesResources.Pass_in_captured_variables_as_arguments)), + diagnostic); - return Task.CompletedTask; - }, - context.CancellationToken); - } + return Task.CompletedTask; + }, + context.CancellationToken); + } - protected override Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - => WrapFixAsync( - document, - diagnostics, - (document, localFunction, captures) => MakeLocalFunctionStaticCodeFixHelper.MakeLocalFunctionStaticAsync( - document, localFunction, captures, editor, fallbackOptions, cancellationToken), - cancellationToken); + protected override Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + => WrapFixAsync( + document, + diagnostics, + (document, localFunction, captures) => MakeLocalFunctionStaticCodeFixHelper.MakeLocalFunctionStaticAsync( + document, localFunction, captures, editor, fallbackOptions, cancellationToken), + cancellationToken); - // The purpose of this wrapper is to share some common logic between FixOne and FixAll. The main reason we chose - // this approach over the typical "FixOne calls FixAll" approach is to avoid duplicate code. - private static async Task WrapFixAsync( - Document document, - ImmutableArray diagnostics, - Func, Task> fixer, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + // The purpose of this wrapper is to share some common logic between FixOne and FixAll. The main reason we chose + // this approach over the typical "FixOne calls FixAll" approach is to avoid duplicate code. + private static async Task WrapFixAsync( + Document document, + ImmutableArray diagnostics, + Func, Task> fixer, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // Even when the language version doesn't support static local function, the compiler will still generate - // this error. So we need to check to make sure we don't provide incorrect fix. - if (!MakeLocalFunctionStaticHelper.IsStaticLocalFunctionSupported(root.SyntaxTree.Options.LanguageVersion())) - return; + // Even when the language version doesn't support static local function, the compiler will still generate + // this error. So we need to check to make sure we don't provide incorrect fix. + if (!MakeLocalFunctionStaticHelper.IsStaticLocalFunctionSupported(root.SyntaxTree.Options.LanguageVersion())) + return; - // Find all unique local functions that contain the error. - var localFunctions = diagnostics - .Select(d => root.FindNode(d.Location.SourceSpan).AncestorsAndSelf().OfType().FirstOrDefault()) - .WhereNotNull() - .Distinct() - .ToImmutableArrayOrEmpty(); + // Find all unique local functions that contain the error. + var localFunctions = diagnostics + .Select(d => root.FindNode(d.Location.SourceSpan).AncestorsAndSelf().OfType().FirstOrDefault()) + .WhereNotNull() + .Distinct() + .ToImmutableArrayOrEmpty(); - if (localFunctions.Length == 0) - return; + if (localFunctions.Length == 0) + return; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - foreach (var localFunction in localFunctions) - { - if (MakeLocalFunctionStaticHelper.CanMakeLocalFunctionStaticByRefactoringCaptures(localFunction, semanticModel, out var captures)) - await fixer(document, localFunction, captures).ConfigureAwait(false); - } + foreach (var localFunction in localFunctions) + { + if (MakeLocalFunctionStaticHelper.CanMakeLocalFunctionStaticByRefactoringCaptures(localFunction, semanticModel, out var captures)) + await fixer(document, localFunction, captures).ConfigureAwait(false); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeMemberStatic/CSharpMakeMemberStaticCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MakeMemberStatic/CSharpMakeMemberStaticCodeFixProvider.cs index 27c25ee8f59fd..8c747b4a83ddd 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeMemberStatic/CSharpMakeMemberStaticCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeMemberStatic/CSharpMakeMemberStaticCodeFixProvider.cs @@ -12,36 +12,35 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.MakeMemberStatic; -namespace Microsoft.CodeAnalysis.CSharp.MakeMemberStatic +namespace Microsoft.CodeAnalysis.CSharp.MakeMemberStatic; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeMemberStatic), Shared] +internal sealed class CSharpMakeMemberStaticCodeFixProvider : AbstractMakeMemberStaticCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeMemberStatic), Shared] - internal sealed class CSharpMakeMemberStaticCodeFixProvider : AbstractMakeMemberStaticCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpMakeMemberStaticCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpMakeMemberStaticCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } = - ["CS0708"]; + public override ImmutableArray FixableDiagnosticIds { get; } = + ["CS0708"]; - protected override bool TryGetMemberDeclaration(SyntaxNode node, [NotNullWhen(true)] out SyntaxNode? memberDeclaration) + protected override bool TryGetMemberDeclaration(SyntaxNode node, [NotNullWhen(true)] out SyntaxNode? memberDeclaration) + { + if (node is MemberDeclarationSyntax) { - if (node is MemberDeclarationSyntax) - { - memberDeclaration = node; - return true; - } - - if (node.IsKind(SyntaxKind.VariableDeclarator) && node.Parent is VariableDeclarationSyntax { Parent: FieldDeclarationSyntax or EventFieldDeclarationSyntax }) - { - memberDeclaration = node.Parent.Parent; - return true; - } + memberDeclaration = node; + return true; + } - memberDeclaration = null; - return false; + if (node.IsKind(SyntaxKind.VariableDeclarator) && node.Parent is VariableDeclarationSyntax { Parent: FieldDeclarationSyntax or EventFieldDeclarationSyntax }) + { + memberDeclaration = node.Parent.Parent; + return true; } + + memberDeclaration = null; + return false; } } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeMethodAsynchronous/CSharpMakeMethodAsynchronousCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MakeMethodAsynchronous/CSharpMakeMethodAsynchronousCodeFixProvider.cs index b56172cd6c846..40fd90e48f730 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeMethodAsynchronous/CSharpMakeMethodAsynchronousCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeMethodAsynchronous/CSharpMakeMethodAsynchronousCodeFixProvider.cs @@ -16,180 +16,179 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.MakeMethodAsynchronous +namespace Microsoft.CodeAnalysis.CSharp.MakeMethodAsynchronous; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddAsync), Shared] +internal class CSharpMakeMethodAsynchronousCodeFixProvider : AbstractMakeMethodAsynchronousCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddAsync), Shared] - internal class CSharpMakeMethodAsynchronousCodeFixProvider : AbstractMakeMethodAsynchronousCodeFixProvider + private const string CS4032 = nameof(CS4032); // The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'. + private const string CS4033 = nameof(CS4033); // The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'. + private const string CS4034 = nameof(CS4034); // The 'await' operator can only be used within an async lambda expression. Consider marking this method with the 'async' modifier. + private const string CS0246 = nameof(CS0246); // The type or namespace name 'await' could not be found + + private static readonly SyntaxToken s_asyncToken = SyntaxFactory.Token(SyntaxKind.AsyncKeyword); + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpMakeMethodAsynchronousCodeFixProvider() { - private const string CS4032 = nameof(CS4032); // The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'. - private const string CS4033 = nameof(CS4033); // The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'. - private const string CS4034 = nameof(CS4034); // The 'await' operator can only be used within an async lambda expression. Consider marking this method with the 'async' modifier. - private const string CS0246 = nameof(CS0246); // The type or namespace name 'await' could not be found + } - private static readonly SyntaxToken s_asyncToken = SyntaxFactory.Token(SyntaxKind.AsyncKeyword); + public override ImmutableArray FixableDiagnosticIds { get; } = + [CS4032, CS4033, CS4034, CS0246]; - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpMakeMethodAsynchronousCodeFixProvider() + protected override bool IsSupportedDiagnostic(Diagnostic diagnostic, CancellationToken cancellationToken) + { + if (diagnostic.Id == CS0246) { + // "The type or namespace name '{0}' could not be found" + // Needs to be reported on an identifier caller 'await'. + if (diagnostic.Location.SourceTree is null) + return false; + + var root = diagnostic.Location.SourceTree.GetRoot(cancellationToken); + var token = root.FindToken(diagnostic.Location.SourceSpan.Start); + return token.Kind() == SyntaxKind.IdentifierToken && token.Text == "await"; } - public override ImmutableArray FixableDiagnosticIds { get; } = - [CS4032, CS4033, CS4034, CS0246]; + // All the other diagnostics IDs are fine to use without additional checks. + return true; + } - protected override bool IsSupportedDiagnostic(Diagnostic diagnostic, CancellationToken cancellationToken) - { - if (diagnostic.Id == CS0246) - { - // "The type or namespace name '{0}' could not be found" - // Needs to be reported on an identifier caller 'await'. - if (diagnostic.Location.SourceTree is null) - return false; - - var root = diagnostic.Location.SourceTree.GetRoot(cancellationToken); - var token = root.FindToken(diagnostic.Location.SourceSpan.Start); - return token.Kind() == SyntaxKind.IdentifierToken && token.Text == "await"; - } + protected override string GetMakeAsyncTaskFunctionResource() + => CSharpCodeFixesResources.Make_method_async; - // All the other diagnostics IDs are fine to use without additional checks. - return true; - } + protected override string GetMakeAsyncVoidFunctionResource() + => CSharpCodeFixesResources.Make_method_async_remain_void; - protected override string GetMakeAsyncTaskFunctionResource() - => CSharpCodeFixesResources.Make_method_async; + protected override bool IsAsyncSupportingFunctionSyntax(SyntaxNode node) + => node.IsAsyncSupportingFunctionSyntax(); - protected override string GetMakeAsyncVoidFunctionResource() - => CSharpCodeFixesResources.Make_method_async_remain_void; + protected override bool IsAsyncReturnType(ITypeSymbol type, KnownTaskTypes knownTypes) + => IsIAsyncEnumerableOrEnumerator(type, knownTypes) || + knownTypes.IsTaskLike(type); - protected override bool IsAsyncSupportingFunctionSyntax(SyntaxNode node) - => node.IsAsyncSupportingFunctionSyntax(); + protected override SyntaxNode AddAsyncTokenAndFixReturnType( + bool keepVoid, + IMethodSymbol methodSymbol, + SyntaxNode node, + KnownTaskTypes knownTypes, + CancellationToken cancellationToken) + { + return node switch + { + MethodDeclarationSyntax method => FixMethod(keepVoid, methodSymbol, method, knownTypes, cancellationToken), + LocalFunctionStatementSyntax localFunction => FixLocalFunction(keepVoid, methodSymbol, localFunction, knownTypes, cancellationToken), + AnonymousFunctionExpressionSyntax anonymous => FixAnonymousFunction(anonymous), + _ => node, + }; + } - protected override bool IsAsyncReturnType(ITypeSymbol type, KnownTaskTypes knownTypes) - => IsIAsyncEnumerableOrEnumerator(type, knownTypes) || - knownTypes.IsTaskLike(type); + private static MethodDeclarationSyntax FixMethod( + bool keepVoid, + IMethodSymbol methodSymbol, + MethodDeclarationSyntax method, + KnownTaskTypes knownTypes, + CancellationToken cancellationToken) + { + var newReturnType = FixMethodReturnType(keepVoid, methodSymbol, method.ReturnType, knownTypes, cancellationToken); + var newModifiers = AddAsyncModifierWithCorrectedTrivia(method.Modifiers, ref newReturnType); + return method.WithReturnType(newReturnType).WithModifiers(newModifiers); + } - protected override SyntaxNode AddAsyncTokenAndFixReturnType( - bool keepVoid, - IMethodSymbol methodSymbol, - SyntaxNode node, - KnownTaskTypes knownTypes, - CancellationToken cancellationToken) - { - return node switch - { - MethodDeclarationSyntax method => FixMethod(keepVoid, methodSymbol, method, knownTypes, cancellationToken), - LocalFunctionStatementSyntax localFunction => FixLocalFunction(keepVoid, methodSymbol, localFunction, knownTypes, cancellationToken), - AnonymousFunctionExpressionSyntax anonymous => FixAnonymousFunction(anonymous), - _ => node, - }; - } + private static LocalFunctionStatementSyntax FixLocalFunction( + bool keepVoid, + IMethodSymbol methodSymbol, + LocalFunctionStatementSyntax localFunction, + KnownTaskTypes knownTypes, + CancellationToken cancellationToken) + { + var newReturnType = FixMethodReturnType(keepVoid, methodSymbol, localFunction.ReturnType, knownTypes, cancellationToken); + var newModifiers = AddAsyncModifierWithCorrectedTrivia(localFunction.Modifiers, ref newReturnType); + return localFunction.WithReturnType(newReturnType).WithModifiers(newModifiers); + } - private static MethodDeclarationSyntax FixMethod( - bool keepVoid, - IMethodSymbol methodSymbol, - MethodDeclarationSyntax method, - KnownTaskTypes knownTypes, - CancellationToken cancellationToken) - { - var newReturnType = FixMethodReturnType(keepVoid, methodSymbol, method.ReturnType, knownTypes, cancellationToken); - var newModifiers = AddAsyncModifierWithCorrectedTrivia(method.Modifiers, ref newReturnType); - return method.WithReturnType(newReturnType).WithModifiers(newModifiers); - } + private static TypeSyntax FixMethodReturnType( + bool keepVoid, + IMethodSymbol methodSymbol, + TypeSyntax returnTypeSyntax, + KnownTaskTypes knownTypes, + CancellationToken cancellationToken) + { + var newReturnType = returnTypeSyntax.WithAdditionalAnnotations(Formatter.Annotation); - private static LocalFunctionStatementSyntax FixLocalFunction( - bool keepVoid, - IMethodSymbol methodSymbol, - LocalFunctionStatementSyntax localFunction, - KnownTaskTypes knownTypes, - CancellationToken cancellationToken) + if (methodSymbol.ReturnsVoid) { - var newReturnType = FixMethodReturnType(keepVoid, methodSymbol, localFunction.ReturnType, knownTypes, cancellationToken); - var newModifiers = AddAsyncModifierWithCorrectedTrivia(localFunction.Modifiers, ref newReturnType); - return localFunction.WithReturnType(newReturnType).WithModifiers(newModifiers); + if (!keepVoid) + { + newReturnType = knownTypes.TaskType!.GenerateTypeSyntax(); + } } - - private static TypeSyntax FixMethodReturnType( - bool keepVoid, - IMethodSymbol methodSymbol, - TypeSyntax returnTypeSyntax, - KnownTaskTypes knownTypes, - CancellationToken cancellationToken) + else { - var newReturnType = returnTypeSyntax.WithAdditionalAnnotations(Formatter.Annotation); - - if (methodSymbol.ReturnsVoid) + var returnType = methodSymbol.ReturnType; + if (IsIEnumerable(returnType, knownTypes) && IsIterator(methodSymbol, cancellationToken)) { - if (!keepVoid) - { - newReturnType = knownTypes.TaskType!.GenerateTypeSyntax(); - } + newReturnType = knownTypes.IAsyncEnumerableOfTType is null + ? MakeGenericType(nameof(IAsyncEnumerable), methodSymbol.ReturnType) + : knownTypes.IAsyncEnumerableOfTType.Construct(methodSymbol.ReturnType.GetTypeArguments()[0]).GenerateTypeSyntax(); } - else + else if (IsIEnumerator(returnType, knownTypes) && IsIterator(methodSymbol, cancellationToken)) { - var returnType = methodSymbol.ReturnType; - if (IsIEnumerable(returnType, knownTypes) && IsIterator(methodSymbol, cancellationToken)) - { - newReturnType = knownTypes.IAsyncEnumerableOfTType is null - ? MakeGenericType(nameof(IAsyncEnumerable), methodSymbol.ReturnType) - : knownTypes.IAsyncEnumerableOfTType.Construct(methodSymbol.ReturnType.GetTypeArguments()[0]).GenerateTypeSyntax(); - } - else if (IsIEnumerator(returnType, knownTypes) && IsIterator(methodSymbol, cancellationToken)) - { - newReturnType = knownTypes.IAsyncEnumeratorOfTType is null - ? MakeGenericType(nameof(IAsyncEnumerator), methodSymbol.ReturnType) - : knownTypes.IAsyncEnumeratorOfTType.Construct(methodSymbol.ReturnType.GetTypeArguments()[0]).GenerateTypeSyntax(); - } - else if (IsIAsyncEnumerableOrEnumerator(returnType, knownTypes)) - { - // Leave the return type alone - } - else if (!knownTypes.IsTaskLike(returnType)) - { - // If it's not already Task-like, then wrap the existing return type - // in Task<>. - newReturnType = knownTypes.TaskOfTType!.Construct(methodSymbol.ReturnType).GenerateTypeSyntax(); - } + newReturnType = knownTypes.IAsyncEnumeratorOfTType is null + ? MakeGenericType(nameof(IAsyncEnumerator), methodSymbol.ReturnType) + : knownTypes.IAsyncEnumeratorOfTType.Construct(methodSymbol.ReturnType.GetTypeArguments()[0]).GenerateTypeSyntax(); } - - return newReturnType.WithTriviaFrom(returnTypeSyntax).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); - - static TypeSyntax MakeGenericType(string type, ITypeSymbol typeArgumentFrom) + else if (IsIAsyncEnumerableOrEnumerator(returnType, knownTypes)) { - var result = SyntaxFactory.GenericName( - SyntaxFactory.Identifier(type), - SyntaxFactory.TypeArgumentList([typeArgumentFrom.GetTypeArguments()[0].GenerateTypeSyntax()])); - - return result.WithAdditionalAnnotations(Simplifier.Annotation); + // Leave the return type alone + } + else if (!knownTypes.IsTaskLike(returnType)) + { + // If it's not already Task-like, then wrap the existing return type + // in Task<>. + newReturnType = knownTypes.TaskOfTType!.Construct(methodSymbol.ReturnType).GenerateTypeSyntax(); } } - private static bool IsIterator(IMethodSymbol method, CancellationToken cancellationToken) - => method.Locations.Any(static (loc, cancellationToken) => loc.FindNode(cancellationToken).ContainsYield(), cancellationToken); + return newReturnType.WithTriviaFrom(returnTypeSyntax).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); - private static bool IsIAsyncEnumerableOrEnumerator(ITypeSymbol returnType, KnownTaskTypes knownTypes) - => returnType.OriginalDefinition.Equals(knownTypes.IAsyncEnumerableOfTType) || - returnType.OriginalDefinition.Equals(knownTypes.IAsyncEnumeratorOfTType); + static TypeSyntax MakeGenericType(string type, ITypeSymbol typeArgumentFrom) + { + var result = SyntaxFactory.GenericName( + SyntaxFactory.Identifier(type), + SyntaxFactory.TypeArgumentList([typeArgumentFrom.GetTypeArguments()[0].GenerateTypeSyntax()])); - private static bool IsIEnumerable(ITypeSymbol returnType, KnownTaskTypes knownTypes) - => returnType.OriginalDefinition.Equals(knownTypes.IEnumerableOfTType); + return result.WithAdditionalAnnotations(Simplifier.Annotation); + } + } - private static bool IsIEnumerator(ITypeSymbol returnType, KnownTaskTypes knownTypes) - => returnType.OriginalDefinition.Equals(knownTypes.IEnumeratorOfTType); + private static bool IsIterator(IMethodSymbol method, CancellationToken cancellationToken) + => method.Locations.Any(static (loc, cancellationToken) => loc.FindNode(cancellationToken).ContainsYield(), cancellationToken); - private static SyntaxTokenList AddAsyncModifierWithCorrectedTrivia(SyntaxTokenList modifiers, ref TypeSyntax newReturnType) - { - if (modifiers.Any()) - return modifiers.Add(s_asyncToken); + private static bool IsIAsyncEnumerableOrEnumerator(ITypeSymbol returnType, KnownTaskTypes knownTypes) + => returnType.OriginalDefinition.Equals(knownTypes.IAsyncEnumerableOfTType) || + returnType.OriginalDefinition.Equals(knownTypes.IAsyncEnumeratorOfTType); - // Move the leading trivia from the return type to the new modifiers list. - var result = SyntaxFactory.TokenList(s_asyncToken.WithLeadingTrivia(newReturnType.GetLeadingTrivia())); - newReturnType = newReturnType.WithoutLeadingTrivia(); - return result; - } + private static bool IsIEnumerable(ITypeSymbol returnType, KnownTaskTypes knownTypes) + => returnType.OriginalDefinition.Equals(knownTypes.IEnumerableOfTType); + + private static bool IsIEnumerator(ITypeSymbol returnType, KnownTaskTypes knownTypes) + => returnType.OriginalDefinition.Equals(knownTypes.IEnumeratorOfTType); - private static AnonymousFunctionExpressionSyntax FixAnonymousFunction(AnonymousFunctionExpressionSyntax anonymous) - => anonymous - .WithoutLeadingTrivia() - .WithAsyncKeyword(s_asyncToken.WithPrependedLeadingTrivia(anonymous.GetLeadingTrivia())); + private static SyntaxTokenList AddAsyncModifierWithCorrectedTrivia(SyntaxTokenList modifiers, ref TypeSyntax newReturnType) + { + if (modifiers.Any()) + return modifiers.Add(s_asyncToken); + + // Move the leading trivia from the return type to the new modifiers list. + var result = SyntaxFactory.TokenList(s_asyncToken.WithLeadingTrivia(newReturnType.GetLeadingTrivia())); + newReturnType = newReturnType.WithoutLeadingTrivia(); + return result; } + + private static AnonymousFunctionExpressionSyntax FixAnonymousFunction(AnonymousFunctionExpressionSyntax anonymous) + => anonymous + .WithoutLeadingTrivia() + .WithAsyncKeyword(s_asyncToken.WithPrependedLeadingTrivia(anonymous.GetLeadingTrivia())); } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeMethodSynchronous/CSharpMakeMethodSynchronousCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MakeMethodSynchronous/CSharpMakeMethodSynchronousCodeFixProvider.cs index 61fe87e7b8dcc..71ae670f8bc24 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeMethodSynchronous/CSharpMakeMethodSynchronousCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeMethodSynchronous/CSharpMakeMethodSynchronousCodeFixProvider.cs @@ -12,78 +12,77 @@ using Microsoft.CodeAnalysis.MakeMethodSynchronous; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.MakeMethodSynchronous +namespace Microsoft.CodeAnalysis.CSharp.MakeMethodSynchronous; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeMethodSynchronous), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)] +internal class CSharpMakeMethodSynchronousCodeFixProvider : AbstractMakeMethodSynchronousCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeMethodSynchronous), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)] - internal class CSharpMakeMethodSynchronousCodeFixProvider : AbstractMakeMethodSynchronousCodeFixProvider + private const string CS1998 = nameof(CS1998); // This async method lacks 'await' operators and will run synchronously. + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpMakeMethodSynchronousCodeFixProvider() { - private const string CS1998 = nameof(CS1998); // This async method lacks 'await' operators and will run synchronously. + } + + public override ImmutableArray FixableDiagnosticIds { get; } = [CS1998]; + + protected override bool IsAsyncSupportingFunctionSyntax(SyntaxNode node) + => node.IsAsyncSupportingFunctionSyntax(); - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpMakeMethodSynchronousCodeFixProvider() + protected override SyntaxNode RemoveAsyncTokenAndFixReturnType(IMethodSymbol methodSymbol, SyntaxNode node, KnownTaskTypes knownTypes) + { + switch (node) { + case MethodDeclarationSyntax method: return FixMethod(methodSymbol, method, knownTypes); + case LocalFunctionStatementSyntax localFunction: return FixLocalFunction(methodSymbol, localFunction, knownTypes); + case AnonymousMethodExpressionSyntax method: return RemoveAsyncModifierHelpers.WithoutAsyncModifier(method); + case ParenthesizedLambdaExpressionSyntax lambda: return RemoveAsyncModifierHelpers.WithoutAsyncModifier(lambda); + case SimpleLambdaExpressionSyntax lambda: return RemoveAsyncModifierHelpers.WithoutAsyncModifier(lambda); + default: return node; } + } + private static SyntaxNode FixMethod(IMethodSymbol methodSymbol, MethodDeclarationSyntax method, KnownTaskTypes knownTypes) + { + var newReturnType = FixMethodReturnType(methodSymbol, method.ReturnType, knownTypes); + return RemoveAsyncModifierHelpers.WithoutAsyncModifier(method, newReturnType); + } - public override ImmutableArray FixableDiagnosticIds { get; } = [CS1998]; + private static SyntaxNode FixLocalFunction(IMethodSymbol methodSymbol, LocalFunctionStatementSyntax localFunction, KnownTaskTypes knownTypes) + { + var newReturnType = FixMethodReturnType(methodSymbol, localFunction.ReturnType, knownTypes); + return RemoveAsyncModifierHelpers.WithoutAsyncModifier(localFunction, newReturnType); + } - protected override bool IsAsyncSupportingFunctionSyntax(SyntaxNode node) - => node.IsAsyncSupportingFunctionSyntax(); + private static TypeSyntax FixMethodReturnType(IMethodSymbol methodSymbol, TypeSyntax returnTypeSyntax, KnownTaskTypes knownTypes) + { + var newReturnType = returnTypeSyntax; - protected override SyntaxNode RemoveAsyncTokenAndFixReturnType(IMethodSymbol methodSymbol, SyntaxNode node, KnownTaskTypes knownTypes) + var returnType = methodSymbol.ReturnType; + if (returnType.OriginalDefinition.Equals(knownTypes.TaskType)) { - switch (node) - { - case MethodDeclarationSyntax method: return FixMethod(methodSymbol, method, knownTypes); - case LocalFunctionStatementSyntax localFunction: return FixLocalFunction(methodSymbol, localFunction, knownTypes); - case AnonymousMethodExpressionSyntax method: return RemoveAsyncModifierHelpers.WithoutAsyncModifier(method); - case ParenthesizedLambdaExpressionSyntax lambda: return RemoveAsyncModifierHelpers.WithoutAsyncModifier(lambda); - case SimpleLambdaExpressionSyntax lambda: return RemoveAsyncModifierHelpers.WithoutAsyncModifier(lambda); - default: return node; - } + // If the return type is Task, then make the new return type "void". + newReturnType = SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)).WithTriviaFrom(returnTypeSyntax); } - private static SyntaxNode FixMethod(IMethodSymbol methodSymbol, MethodDeclarationSyntax method, KnownTaskTypes knownTypes) + else if (returnType.OriginalDefinition.Equals(knownTypes.TaskOfTType)) { - var newReturnType = FixMethodReturnType(methodSymbol, method.ReturnType, knownTypes); - return RemoveAsyncModifierHelpers.WithoutAsyncModifier(method, newReturnType); + // If the return type is Task, then make the new return type "T". + newReturnType = returnType.GetTypeArguments()[0].GenerateTypeSyntax().WithTriviaFrom(returnTypeSyntax); } - - private static SyntaxNode FixLocalFunction(IMethodSymbol methodSymbol, LocalFunctionStatementSyntax localFunction, KnownTaskTypes knownTypes) + else if (returnType.OriginalDefinition.Equals(knownTypes.IAsyncEnumerableOfTType) && + knownTypes.IEnumerableOfTType != null) { - var newReturnType = FixMethodReturnType(methodSymbol, localFunction.ReturnType, knownTypes); - return RemoveAsyncModifierHelpers.WithoutAsyncModifier(localFunction, newReturnType); + // If the return type is IAsyncEnumerable, then make the new return type IEnumerable. + newReturnType = knownTypes.IEnumerableOfTType.Construct(methodSymbol.ReturnType.GetTypeArguments()[0]).GenerateTypeSyntax(); } - - private static TypeSyntax FixMethodReturnType(IMethodSymbol methodSymbol, TypeSyntax returnTypeSyntax, KnownTaskTypes knownTypes) + else if (returnType.OriginalDefinition.Equals(knownTypes.IAsyncEnumeratorOfTType) && + knownTypes.IEnumeratorOfTType != null) { - var newReturnType = returnTypeSyntax; - - var returnType = methodSymbol.ReturnType; - if (returnType.OriginalDefinition.Equals(knownTypes.TaskType)) - { - // If the return type is Task, then make the new return type "void". - newReturnType = SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)).WithTriviaFrom(returnTypeSyntax); - } - else if (returnType.OriginalDefinition.Equals(knownTypes.TaskOfTType)) - { - // If the return type is Task, then make the new return type "T". - newReturnType = returnType.GetTypeArguments()[0].GenerateTypeSyntax().WithTriviaFrom(returnTypeSyntax); - } - else if (returnType.OriginalDefinition.Equals(knownTypes.IAsyncEnumerableOfTType) && - knownTypes.IEnumerableOfTType != null) - { - // If the return type is IAsyncEnumerable, then make the new return type IEnumerable. - newReturnType = knownTypes.IEnumerableOfTType.Construct(methodSymbol.ReturnType.GetTypeArguments()[0]).GenerateTypeSyntax(); - } - else if (returnType.OriginalDefinition.Equals(knownTypes.IAsyncEnumeratorOfTType) && - knownTypes.IEnumeratorOfTType != null) - { - // If the return type is IAsyncEnumerator, then make the new return type IEnumerator. - newReturnType = knownTypes.IEnumeratorOfTType.Construct(methodSymbol.ReturnType.GetTypeArguments()[0]).GenerateTypeSyntax(); - } - - return newReturnType; + // If the return type is IAsyncEnumerator, then make the new return type IEnumerator. + newReturnType = knownTypes.IEnumeratorOfTType.Construct(methodSymbol.ReturnType.GetTypeArguments()[0]).GenerateTypeSyntax(); } + + return newReturnType; } } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeRefStruct/MakeRefStructCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MakeRefStruct/MakeRefStructCodeFixProvider.cs index d46004d2ff8a0..61ca1362d13fe 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeRefStruct/MakeRefStructCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeRefStruct/MakeRefStructCodeFixProvider.cs @@ -15,71 +15,70 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.MakeRefStruct +namespace Microsoft.CodeAnalysis.CSharp.MakeRefStruct; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeRefStruct), Shared] +internal class MakeRefStructCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeRefStruct), Shared] - internal class MakeRefStructCodeFixProvider : CodeFixProvider - { - // Error CS8345: Field or auto-implemented property cannot be of certain type unless it is an instance member of a ref struct. - private const string CS8345 = nameof(CS8345); + // Error CS8345: Field or auto-implemented property cannot be of certain type unless it is an instance member of a ref struct. + private const string CS8345 = nameof(CS8345); - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public MakeRefStructCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public MakeRefStructCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds - => [CS8345]; + public override ImmutableArray FixableDiagnosticIds + => [CS8345]; - public override FixAllProvider? GetFixAllProvider() - { - // The chance of needing fix-all in these cases is super low - return null; - } + public override FixAllProvider? GetFixAllProvider() + { + // The chance of needing fix-all in these cases is super low + return null; + } - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var cancellationToken = context.CancellationToken; - var span = context.Span; + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var cancellationToken = context.CancellationToken; + var span = context.Span; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // Could be declared in a class or even in a nested class inside a struct, - // so find only the first parent declaration - if (root.FindNode(span).GetAncestor() is not StructDeclarationSyntax structDeclaration) - return; + // Could be declared in a class or even in a nested class inside a struct, + // so find only the first parent declaration + if (root.FindNode(span).GetAncestor() is not StructDeclarationSyntax structDeclaration) + return; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var structDeclarationSymbol = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(structDeclaration, cancellationToken); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var structDeclarationSymbol = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(structDeclaration, cancellationToken); - // CS8345 could be triggered when struct is already marked with `ref` but a property is static - if (!structDeclarationSymbol.IsRefLikeType) - { - context.RegisterCodeFix( - CodeAction.Create( - CSharpCodeFixesResources.Make_ref_struct, - c => FixCodeAsync(document, structDeclaration, c), - nameof(CSharpCodeFixesResources.Make_ref_struct)), - context.Diagnostics); - } + // CS8345 could be triggered when struct is already marked with `ref` but a property is static + if (!structDeclarationSymbol.IsRefLikeType) + { + context.RegisterCodeFix( + CodeAction.Create( + CSharpCodeFixesResources.Make_ref_struct, + c => FixCodeAsync(document, structDeclaration, c), + nameof(CSharpCodeFixesResources.Make_ref_struct)), + context.Diagnostics); } + } - private static async Task FixCodeAsync( - Document document, - StructDeclarationSyntax structDeclaration, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var generator = SyntaxGenerator.GetGenerator(document); + private static async Task FixCodeAsync( + Document document, + StructDeclarationSyntax structDeclaration, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var generator = SyntaxGenerator.GetGenerator(document); - var newStruct = generator.WithModifiers( - structDeclaration, - generator.GetModifiers(structDeclaration).WithIsRef(true)); - var newRoot = root.ReplaceNode(structDeclaration, newStruct); + var newStruct = generator.WithModifiers( + structDeclaration, + generator.GetModifiers(structDeclaration).WithIsRef(true)); + var newRoot = root.ReplaceNode(structDeclaration, newStruct); - return document.WithSyntaxRoot(newRoot); - } + return document.WithSyntaxRoot(newRoot); } } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeStatementAsynchronous/CSharpMakeStatementAsynchronousCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MakeStatementAsynchronous/CSharpMakeStatementAsynchronousCodeFixProvider.cs index 24eacde32087a..5b08afa5ecbd9 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeStatementAsynchronous/CSharpMakeStatementAsynchronousCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeStatementAsynchronous/CSharpMakeStatementAsynchronousCodeFixProvider.cs @@ -17,101 +17,100 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.MakeStatementAsynchronous +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.MakeStatementAsynchronous; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeStatementAsynchronous), Shared] +internal class CSharpMakeStatementAsynchronousCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeStatementAsynchronous), Shared] - internal class CSharpMakeStatementAsynchronousCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpMakeStatementAsynchronousCodeFixProvider() + { + } + + // error CS8414: foreach statement cannot operate on variables of type 'IAsyncEnumerable' because 'IAsyncEnumerable' does not contain a public instance definition for 'GetEnumerator'. Did you mean 'await foreach'? + // error CS8418: 'IAsyncDisposable': type used in a using statement must be implicitly convertible to 'System.IDisposable'. Did you mean 'await using' rather than 'using'? + public sealed override ImmutableArray FixableDiagnosticIds => ["CS8414", "CS8418"]; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpMakeStatementAsynchronousCodeFixProvider() + var diagnostic = context.Diagnostics.First(); + var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + + var constructToFix = TryGetStatementToFix(node); + if (constructToFix == null) { + return; } - // error CS8414: foreach statement cannot operate on variables of type 'IAsyncEnumerable' because 'IAsyncEnumerable' does not contain a public instance definition for 'GetEnumerator'. Did you mean 'await foreach'? - // error CS8418: 'IAsyncDisposable': type used in a using statement must be implicitly convertible to 'System.IDisposable'. Did you mean 'await using' rather than 'using'? - public sealed override ImmutableArray FixableDiagnosticIds => ["CS8414", "CS8418"]; + RegisterCodeFix(context, CSharpCodeFixesResources.Add_await, nameof(CSharpCodeFixesResources.Add_await)); + } - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) { - var diagnostic = context.Diagnostics.First(); - var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); - - var constructToFix = TryGetStatementToFix(node); - if (constructToFix == null) + var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); + var statementToFix = TryGetStatementToFix(node); + if (statementToFix != null) { - return; + MakeStatementAsynchronous(editor, statementToFix); } - - RegisterCodeFix(context, CSharpCodeFixesResources.Add_await, nameof(CSharpCodeFixesResources.Add_await)); } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - foreach (var diagnostic in diagnostics) - { - var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - var statementToFix = TryGetStatementToFix(node); - if (statementToFix != null) - { - MakeStatementAsynchronous(editor, statementToFix); - } - } + return Task.CompletedTask; + } - return Task.CompletedTask; + private static void MakeStatementAsynchronous(SyntaxEditor editor, SyntaxNode statementToFix) + { + SyntaxNode newStatement; + switch (statementToFix) + { + case ForEachStatementSyntax forEach: + newStatement = forEach + .WithForEachKeyword(forEach.ForEachKeyword.WithLeadingTrivia()) + .WithAwaitKeyword(SyntaxFactory.Token(SyntaxKind.AwaitKeyword).WithLeadingTrivia(forEach.GetLeadingTrivia())); + break; + case ForEachVariableStatementSyntax forEachDeconstruction: + newStatement = forEachDeconstruction + .WithForEachKeyword(forEachDeconstruction.ForEachKeyword.WithLeadingTrivia()) + .WithAwaitKeyword(SyntaxFactory.Token(SyntaxKind.AwaitKeyword).WithLeadingTrivia(forEachDeconstruction.GetLeadingTrivia())); + break; + case UsingStatementSyntax usingStatement: + newStatement = usingStatement + .WithUsingKeyword(usingStatement.UsingKeyword.WithLeadingTrivia()) + .WithAwaitKeyword(SyntaxFactory.Token(SyntaxKind.AwaitKeyword).WithLeadingTrivia(usingStatement.GetLeadingTrivia())); + break; + case LocalDeclarationStatementSyntax localDeclaration: + newStatement = localDeclaration + .WithUsingKeyword(localDeclaration.UsingKeyword.WithLeadingTrivia()) + .WithAwaitKeyword(SyntaxFactory.Token(SyntaxKind.AwaitKeyword).WithLeadingTrivia(localDeclaration.GetLeadingTrivia())); + break; + default: + return; } - private static void MakeStatementAsynchronous(SyntaxEditor editor, SyntaxNode statementToFix) - { - SyntaxNode newStatement; - switch (statementToFix) - { - case ForEachStatementSyntax forEach: - newStatement = forEach - .WithForEachKeyword(forEach.ForEachKeyword.WithLeadingTrivia()) - .WithAwaitKeyword(SyntaxFactory.Token(SyntaxKind.AwaitKeyword).WithLeadingTrivia(forEach.GetLeadingTrivia())); - break; - case ForEachVariableStatementSyntax forEachDeconstruction: - newStatement = forEachDeconstruction - .WithForEachKeyword(forEachDeconstruction.ForEachKeyword.WithLeadingTrivia()) - .WithAwaitKeyword(SyntaxFactory.Token(SyntaxKind.AwaitKeyword).WithLeadingTrivia(forEachDeconstruction.GetLeadingTrivia())); - break; - case UsingStatementSyntax usingStatement: - newStatement = usingStatement - .WithUsingKeyword(usingStatement.UsingKeyword.WithLeadingTrivia()) - .WithAwaitKeyword(SyntaxFactory.Token(SyntaxKind.AwaitKeyword).WithLeadingTrivia(usingStatement.GetLeadingTrivia())); - break; - case LocalDeclarationStatementSyntax localDeclaration: - newStatement = localDeclaration - .WithUsingKeyword(localDeclaration.UsingKeyword.WithLeadingTrivia()) - .WithAwaitKeyword(SyntaxFactory.Token(SyntaxKind.AwaitKeyword).WithLeadingTrivia(localDeclaration.GetLeadingTrivia())); - break; - default: - return; - } + editor.ReplaceNode(statementToFix, newStatement); + } - editor.ReplaceNode(statementToFix, newStatement); + private static SyntaxNode? TryGetStatementToFix(SyntaxNode node) + { + if (node.Parent is (kind: + SyntaxKind.ForEachStatement or + SyntaxKind.ForEachVariableStatement or + SyntaxKind.UsingStatement)) + { + return node.Parent; } - private static SyntaxNode? TryGetStatementToFix(SyntaxNode node) + if (node is LocalDeclarationStatementSyntax localDeclaration && localDeclaration.UsingKeyword != default) { - if (node.Parent is (kind: - SyntaxKind.ForEachStatement or - SyntaxKind.ForEachVariableStatement or - SyntaxKind.UsingStatement)) - { - return node.Parent; - } - - if (node is LocalDeclarationStatementSyntax localDeclaration && localDeclaration.UsingKeyword != default) - { - return node; - } - - return null; + return node; } + + return null; } } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeStructFieldsWritable/CSharpMakeStructFieldsWritableCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MakeStructFieldsWritable/CSharpMakeStructFieldsWritableCodeFixProvider.cs index c8e1199389fc3..8a328a197cb16 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeStructFieldsWritable/CSharpMakeStructFieldsWritableCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeStructFieldsWritable/CSharpMakeStructFieldsWritableCodeFixProvider.cs @@ -16,58 +16,57 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.MakeStructFieldsWritable +namespace Microsoft.CodeAnalysis.CSharp.MakeStructFieldsWritable; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeStructFieldsWritable), Shared] +internal class CSharpMakeStructFieldsWritableCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeStructFieldsWritable), Shared] - internal class CSharpMakeStructFieldsWritableCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpMakeStructFieldsWritableCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpMakeStructFieldsWritableCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.MakeStructFieldsWritable]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.MakeStructFieldsWritable]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Make_readonly_fields_writable, nameof(CSharpAnalyzersResources.Make_readonly_fields_writable)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Make_readonly_fields_writable, nameof(CSharpAnalyzersResources.Make_readonly_fields_writable)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, - ImmutableArray diagnostics, - SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) { - foreach (var diagnostic in diagnostics) + var diagnosticNode = diagnostic.Location.FindNode(cancellationToken); + + if (diagnosticNode is not StructDeclarationSyntax structDeclaration) { - var diagnosticNode = diagnostic.Location.FindNode(cancellationToken); + continue; + } - if (diagnosticNode is not StructDeclarationSyntax structDeclaration) - { - continue; - } + var fieldDeclarations = structDeclaration.Members + .OfType(); - var fieldDeclarations = structDeclaration.Members - .OfType(); + foreach (var fieldDeclaration in fieldDeclarations) + { + var fieldDeclarationModifiers = editor.Generator.GetModifiers(fieldDeclaration); + var containsReadonlyModifier = + (fieldDeclarationModifiers & DeclarationModifiers.ReadOnly) == DeclarationModifiers.ReadOnly; - foreach (var fieldDeclaration in fieldDeclarations) + if (containsReadonlyModifier) { - var fieldDeclarationModifiers = editor.Generator.GetModifiers(fieldDeclaration); - var containsReadonlyModifier = - (fieldDeclarationModifiers & DeclarationModifiers.ReadOnly) == DeclarationModifiers.ReadOnly; - - if (containsReadonlyModifier) - { - editor.SetModifiers(fieldDeclaration, fieldDeclarationModifiers.WithIsReadOnly(false)); - } + editor.SetModifiers(fieldDeclaration, fieldDeclarationModifiers.WithIsReadOnly(false)); } } - - return Task.CompletedTask; } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeTypeAbstract/CSharpMakeTypeAbstractCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MakeTypeAbstract/CSharpMakeTypeAbstractCodeFixProvider.cs index a31c8d9d4801e..3cf6b45ddf1e2 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeTypeAbstract/CSharpMakeTypeAbstractCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeTypeAbstract/CSharpMakeTypeAbstractCodeFixProvider.cs @@ -10,57 +10,56 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.MakeTypeAbstract; -namespace Microsoft.CodeAnalysis.CSharp.MakeTypeAbstract +namespace Microsoft.CodeAnalysis.CSharp.MakeTypeAbstract; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeTypeAbstract), Shared] +internal sealed class CSharpMakeTypeAbstractCodeFixProvider : AbstractMakeTypeAbstractCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeTypeAbstract), Shared] - internal sealed class CSharpMakeTypeAbstractCodeFixProvider : AbstractMakeTypeAbstractCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpMakeTypeAbstractCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpMakeTypeAbstractCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } = - ["CS0513"]; + public override ImmutableArray FixableDiagnosticIds { get; } = + ["CS0513"]; - protected override bool IsValidRefactoringContext(SyntaxNode? node, [NotNullWhen(true)] out TypeDeclarationSyntax? typeDeclaration) + protected override bool IsValidRefactoringContext(SyntaxNode? node, [NotNullWhen(true)] out TypeDeclarationSyntax? typeDeclaration) + { + switch (node) { - switch (node) - { - case MethodDeclarationSyntax method: - if (method.Body != null || method.ExpressionBody != null) - { - typeDeclaration = null; - return false; - } - - break; - - case AccessorDeclarationSyntax accessor: - if (accessor.Body != null || accessor.ExpressionBody != null) - { - typeDeclaration = null; - return false; - } + case MethodDeclarationSyntax method: + if (method.Body != null || method.ExpressionBody != null) + { + typeDeclaration = null; + return false; + } - break; + break; - default: + case AccessorDeclarationSyntax accessor: + if (accessor.Body != null || accessor.ExpressionBody != null) + { typeDeclaration = null; return false; - } + } + + break; - var enclosingType = node.FirstAncestorOrSelf(); - if (enclosingType?.Kind() is SyntaxKind.ClassDeclaration or SyntaxKind.RecordDeclaration && - !enclosingType.Modifiers.Any(SyntaxKind.AbstractKeyword) && !enclosingType.Modifiers.Any(SyntaxKind.StaticKeyword)) - { - typeDeclaration = enclosingType; - return true; - } + default: + typeDeclaration = null; + return false; + } - typeDeclaration = null; - return false; + var enclosingType = node.FirstAncestorOrSelf(); + if (enclosingType?.Kind() is SyntaxKind.ClassDeclaration or SyntaxKind.RecordDeclaration && + !enclosingType.Modifiers.Any(SyntaxKind.AbstractKeyword) && !enclosingType.Modifiers.Any(SyntaxKind.StaticKeyword)) + { + typeDeclaration = enclosingType; + return true; } + + typeDeclaration = null; + return false; } } diff --git a/src/Analyzers/CSharp/CodeFixes/MakeTypePartial/CSharpMakeTypePartialCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MakeTypePartial/CSharpMakeTypePartialCodeFixProvider.cs index dbb2da5bbbfe0..be903b4c152b7 100644 --- a/src/Analyzers/CSharp/CodeFixes/MakeTypePartial/CSharpMakeTypePartialCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MakeTypePartial/CSharpMakeTypePartialCodeFixProvider.cs @@ -9,20 +9,19 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.MakeTypePartial; -namespace Microsoft.CodeAnalysis.CSharp.MakeTypePartial -{ - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeTypePartial), Shared] - internal sealed class CSharpMakeTypePartialCodeFixProvider : AbstractMakeTypePartialCodeFixProvider - { - private const string CS0260 = nameof(CS0260); // Missing partial modifier on declaration of type 'C'; another partial declaration of this type exists +namespace Microsoft.CodeAnalysis.CSharp.MakeTypePartial; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpMakeTypePartialCodeFixProvider() - { - } +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MakeTypePartial), Shared] +internal sealed class CSharpMakeTypePartialCodeFixProvider : AbstractMakeTypePartialCodeFixProvider +{ + private const string CS0260 = nameof(CS0260); // Missing partial modifier on declaration of type 'C'; another partial declaration of this type exists - public override ImmutableArray FixableDiagnosticIds { get; } = - [CS0260]; + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpMakeTypePartialCodeFixProvider() + { } + + public override ImmutableArray FixableDiagnosticIds { get; } = + [CS0260]; } diff --git a/src/Analyzers/CSharp/CodeFixes/MatchFolderAndNamespace/CSharpChangeNamespaceToMatchFolderCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MatchFolderAndNamespace/CSharpChangeNamespaceToMatchFolderCodeFixProvider.cs index e8c485655cf0e..e5d5a6a13936a 100644 --- a/src/Analyzers/CSharp/CodeFixes/MatchFolderAndNamespace/CSharpChangeNamespaceToMatchFolderCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MatchFolderAndNamespace/CSharpChangeNamespaceToMatchFolderCodeFixProvider.cs @@ -8,16 +8,15 @@ using Microsoft.CodeAnalysis.CodeFixes.MatchFolderAndNamespace; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.MatchFolderAndNamespace +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.MatchFolderAndNamespace; + +[Export(typeof(CSharpChangeNamespaceToMatchFolderCodeFixProvider))] +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ChangeNamespaceToMatchFolder), Shared] +internal class CSharpChangeNamespaceToMatchFolderCodeFixProvider : AbstractChangeNamespaceToMatchFolderCodeFixProvider { - [Export(typeof(CSharpChangeNamespaceToMatchFolderCodeFixProvider))] - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ChangeNamespaceToMatchFolder), Shared] - internal class CSharpChangeNamespaceToMatchFolderCodeFixProvider : AbstractChangeNamespaceToMatchFolderCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpChangeNamespaceToMatchFolderCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpChangeNamespaceToMatchFolderCodeFixProvider() - { - } } } diff --git a/src/Analyzers/CSharp/CodeFixes/MisplacedUsingDirectives/MisplacedUsingDirectivesCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MisplacedUsingDirectives/MisplacedUsingDirectivesCodeFixProvider.cs index 93b46d5f65dba..c829b08232f00 100644 --- a/src/Analyzers/CSharp/CodeFixes/MisplacedUsingDirectives/MisplacedUsingDirectivesCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MisplacedUsingDirectives/MisplacedUsingDirectivesCodeFixProvider.cs @@ -23,418 +23,417 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.MisplacedUsingDirectives +namespace Microsoft.CodeAnalysis.CSharp.MisplacedUsingDirectives; + +/// +/// Implements a code fix for all misplaced using statements. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MoveMisplacedUsingDirectives)] +[Shared] +internal sealed partial class MisplacedUsingDirectivesCodeFixProvider : CodeFixProvider { + private static readonly SyntaxAnnotation s_usingPlacementCodeFixAnnotation = new(nameof(s_usingPlacementCodeFixAnnotation)); + /// - /// Implements a code fix for all misplaced using statements. + /// A blanket warning that this codefix may change code so that it does not compile. /// - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.MoveMisplacedUsingDirectives)] - [Shared] - internal sealed partial class MisplacedUsingDirectivesCodeFixProvider : CodeFixProvider - { - private static readonly SyntaxAnnotation s_usingPlacementCodeFixAnnotation = new(nameof(s_usingPlacementCodeFixAnnotation)); + private static readonly SyntaxAnnotation s_warningAnnotation = WarningAnnotation.Create( + CSharpAnalyzersResources.Warning_colon_Moving_using_directives_may_change_code_meaning); - /// - /// A blanket warning that this codefix may change code so that it does not compile. - /// - private static readonly SyntaxAnnotation s_warningAnnotation = WarningAnnotation.Create( - CSharpAnalyzersResources.Warning_colon_Moving_using_directives_may_change_code_meaning); - - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public MisplacedUsingDirectivesCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public MisplacedUsingDirectivesCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds => [IDEDiagnosticIds.MoveMisplacedUsingDirectivesDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds => [IDEDiagnosticIds.MoveMisplacedUsingDirectivesDiagnosticId]; - public override FixAllProvider GetFixAllProvider() - { - // Since we work on an entire document at a time fixing all contained diagnostics, the batch fixer should not have merge conflicts. - return WellKnownFixAllProviders.BatchFixer; - } + public override FixAllProvider GetFixAllProvider() + { + // Since we work on an entire document at a time fixing all contained diagnostics, the batch fixer should not have merge conflicts. + return WellKnownFixAllProviders.BatchFixer; + } - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var cancellationToken = context.CancellationToken; + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var cancellationToken = context.CancellationToken; - var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var compilationUnit = (CompilationUnitSyntax)syntaxRoot; + var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var compilationUnit = (CompilationUnitSyntax)syntaxRoot; - var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.GetOptionsProvider(), cancellationToken).ConfigureAwait(false); - var simplifierOptions = options.GetSimplifierOptions(); + var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.GetOptionsProvider(), cancellationToken).ConfigureAwait(false); + var simplifierOptions = options.GetSimplifierOptions(); - // Read the preferred placement option and verify if it can be applied to this code file. There are cases - // where we will not be able to fix the diagnostic and the user will need to resolve it manually. - var (placement, preferPreservation) = DeterminePlacement(compilationUnit, options.UsingDirectivePlacement); - if (preferPreservation) - return; + // Read the preferred placement option and verify if it can be applied to this code file. There are cases + // where we will not be able to fix the diagnostic and the user will need to resolve it manually. + var (placement, preferPreservation) = DeterminePlacement(compilationUnit, options.UsingDirectivePlacement); + if (preferPreservation) + return; - foreach (var diagnostic in context.Diagnostics) - { - context.RegisterCodeFix( - CodeAction.Create( - CSharpAnalyzersResources.Move_misplaced_using_directives, - token => GetTransformedDocumentAsync(document, compilationUnit, GetAllUsingDirectives(compilationUnit), placement, simplifierOptions, token), - nameof(CSharpAnalyzersResources.Move_misplaced_using_directives)), - diagnostic); - } + foreach (var diagnostic in context.Diagnostics) + { + context.RegisterCodeFix( + CodeAction.Create( + CSharpAnalyzersResources.Move_misplaced_using_directives, + token => GetTransformedDocumentAsync(document, compilationUnit, GetAllUsingDirectives(compilationUnit), placement, simplifierOptions, token), + nameof(CSharpAnalyzersResources.Move_misplaced_using_directives)), + diagnostic); } + } - internal static async Task TransformDocumentIfRequiredAsync( - Document document, - SimplifierOptions simplifierOptions, - CodeStyleOption2 importPlacementStyleOption, - CancellationToken cancellationToken) - { - var compilationUnit = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + internal static async Task TransformDocumentIfRequiredAsync( + Document document, + SimplifierOptions simplifierOptions, + CodeStyleOption2 importPlacementStyleOption, + CancellationToken cancellationToken) + { + var compilationUnit = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var (placement, preferPreservation) = DeterminePlacement(compilationUnit, importPlacementStyleOption); - if (preferPreservation) - return document; + var (placement, preferPreservation) = DeterminePlacement(compilationUnit, importPlacementStyleOption); + if (preferPreservation) + return document; - // We are called from a diagnostic, but also for all new documents, so check if there are any usings at all - // otherwise there is nothing to do. - var allUsingDirectives = GetAllUsingDirectives(compilationUnit); - if (allUsingDirectives.Length == 0) - return document; + // We are called from a diagnostic, but also for all new documents, so check if there are any usings at all + // otherwise there is nothing to do. + var allUsingDirectives = GetAllUsingDirectives(compilationUnit); + if (allUsingDirectives.Length == 0) + return document; - return await GetTransformedDocumentAsync( - document, compilationUnit, allUsingDirectives, placement, simplifierOptions, cancellationToken).ConfigureAwait(false); - } + return await GetTransformedDocumentAsync( + document, compilationUnit, allUsingDirectives, placement, simplifierOptions, cancellationToken).ConfigureAwait(false); + } - private static ImmutableArray GetAllUsingDirectives(CompilationUnitSyntax compilationUnit) - { - using var _ = ArrayBuilder.GetInstance(out var result); + private static ImmutableArray GetAllUsingDirectives(CompilationUnitSyntax compilationUnit) + { + using var _ = ArrayBuilder.GetInstance(out var result); - foreach (var usingDirective in compilationUnit.Usings) - { - // ignore global usings in teh compilation unit, they cannot be moved. - if (usingDirective.GlobalKeyword == default) - result.Add(usingDirective); - } + foreach (var usingDirective in compilationUnit.Usings) + { + // ignore global usings in teh compilation unit, they cannot be moved. + if (usingDirective.GlobalKeyword == default) + result.Add(usingDirective); + } - Recurse(compilationUnit.Members); + Recurse(compilationUnit.Members); - return result.ToImmutable(); + return result.ToImmutable(); - void Recurse(SyntaxList members) + void Recurse(SyntaxList members) + { + foreach (var member in members) { - foreach (var member in members) + if (member is NamespaceDeclarationSyntax namespaceDeclaration) { - if (member is NamespaceDeclarationSyntax namespaceDeclaration) - { - result.AddRange(namespaceDeclaration.Usings); - Recurse(namespaceDeclaration.Members); - } + result.AddRange(namespaceDeclaration.Usings); + Recurse(namespaceDeclaration.Members); } } } + } - private static async Task GetTransformedDocumentAsync( - Document document, - CompilationUnitSyntax compilationUnit, - ImmutableArray allUsingDirectives, - AddImportPlacement placement, - SimplifierOptions simplifierOptions, - CancellationToken cancellationToken) - { - var bannerService = document.GetRequiredLanguageService(); + private static async Task GetTransformedDocumentAsync( + Document document, + CompilationUnitSyntax compilationUnit, + ImmutableArray allUsingDirectives, + AddImportPlacement placement, + SimplifierOptions simplifierOptions, + CancellationToken cancellationToken) + { + var bannerService = document.GetRequiredLanguageService(); - // Expand usings so that they can be properly simplified after they are relocated. - var compilationUnitWithExpandedUsings = await ExpandUsingDirectivesAsync( - document, compilationUnit, allUsingDirectives, cancellationToken).ConfigureAwait(false); + // Expand usings so that they can be properly simplified after they are relocated. + var compilationUnitWithExpandedUsings = await ExpandUsingDirectivesAsync( + document, compilationUnit, allUsingDirectives, cancellationToken).ConfigureAwait(false); - // Remove the file header from the compilation unit so that we do not lose it when making changes to usings. - var (compilationUnitWithoutHeader, fileHeader) = RemoveFileHeader(compilationUnitWithExpandedUsings, bannerService); + // Remove the file header from the compilation unit so that we do not lose it when making changes to usings. + var (compilationUnitWithoutHeader, fileHeader) = RemoveFileHeader(compilationUnitWithExpandedUsings, bannerService); - var newCompilationUnit = placement == AddImportPlacement.InsideNamespace - ? MoveUsingsInsideNamespace(compilationUnitWithoutHeader) - : MoveUsingsOutsideNamespaces(compilationUnitWithoutHeader); + var newCompilationUnit = placement == AddImportPlacement.InsideNamespace + ? MoveUsingsInsideNamespace(compilationUnitWithoutHeader) + : MoveUsingsOutsideNamespaces(compilationUnitWithoutHeader); - // Re-attach the header now that using have been moved and LeadingTrivia is no longer being altered. - var newCompilationUnitWithHeader = AddFileHeader(newCompilationUnit, fileHeader); - var newDocument = document.WithSyntaxRoot(newCompilationUnitWithHeader); + // Re-attach the header now that using have been moved and LeadingTrivia is no longer being altered. + var newCompilationUnitWithHeader = AddFileHeader(newCompilationUnit, fileHeader); + var newDocument = document.WithSyntaxRoot(newCompilationUnitWithHeader); - // Simplify usings now that they have been moved and are in the proper context. + // Simplify usings now that they have been moved and are in the proper context. #if CODE_STYLE #pragma warning disable RS0030 // Do not used banned APIs (ReduceAsync with SimplifierOptions isn't public) - return await Simplifier.ReduceAsync(newDocument, Simplifier.Annotation, optionSet: null, cancellationToken).ConfigureAwait(false); + return await Simplifier.ReduceAsync(newDocument, Simplifier.Annotation, optionSet: null, cancellationToken).ConfigureAwait(false); #pragma warning restore #else - return await Simplifier.ReduceAsync(newDocument, Simplifier.Annotation, simplifierOptions, cancellationToken).ConfigureAwait(false); + return await Simplifier.ReduceAsync(newDocument, Simplifier.Annotation, simplifierOptions, cancellationToken).ConfigureAwait(false); #endif - } - - private static async Task ExpandUsingDirectivesAsync( - Document document, CompilationUnitSyntax compilationUnit, ImmutableArray allUsingDirectives, CancellationToken cancellationToken) - { - // Create a map between the original node and the future expanded node. - var expandUsingDirectiveTasks = allUsingDirectives.ToDictionary( - usingDirective => (SyntaxNode)usingDirective, - usingDirective => ExpandUsingDirectiveAsync(document, usingDirective, cancellationToken)); - - // Wait for all using directives to be expanded - await Task.WhenAll(expandUsingDirectiveTasks.Values).ConfigureAwait(false); - - // Replace using directives with their expanded version. - return compilationUnit.ReplaceNodes( - expandUsingDirectiveTasks.Keys, - (node, _) => expandUsingDirectiveTasks[node].Result); - } + } - private static async Task ExpandUsingDirectiveAsync(Document document, UsingDirectiveSyntax usingDirective, CancellationToken cancellationToken) - { - var newType = await Simplifier.ExpandAsync(usingDirective.NamespaceOrType, document, cancellationToken: cancellationToken).ConfigureAwait(false); - return usingDirective.WithNamespaceOrType(newType); - } + private static async Task ExpandUsingDirectivesAsync( + Document document, CompilationUnitSyntax compilationUnit, ImmutableArray allUsingDirectives, CancellationToken cancellationToken) + { + // Create a map between the original node and the future expanded node. + var expandUsingDirectiveTasks = allUsingDirectives.ToDictionary( + usingDirective => (SyntaxNode)usingDirective, + usingDirective => ExpandUsingDirectiveAsync(document, usingDirective, cancellationToken)); + + // Wait for all using directives to be expanded + await Task.WhenAll(expandUsingDirectiveTasks.Values).ConfigureAwait(false); + + // Replace using directives with their expanded version. + return compilationUnit.ReplaceNodes( + expandUsingDirectiveTasks.Keys, + (node, _) => expandUsingDirectiveTasks[node].Result); + } - private static CompilationUnitSyntax MoveUsingsInsideNamespace(CompilationUnitSyntax compilationUnit) - { - // Get the compilation unit usings and set them up to format when moved. - var usingsToAdd = compilationUnit.Usings - .Where(u => u.GlobalKeyword == default) - .Select(d => d.WithAdditionalAnnotations(Formatter.Annotation, s_warningAnnotation)); - - // Remove usings and fix leading trivia for compilation unit. - var compilationUnitWithoutUsings = compilationUnit.WithUsings([.. compilationUnit.Usings.Where(u => u.GlobalKeyword != default)]); - var compilationUnitWithoutBlankLine = compilationUnitWithoutUsings.Usings.Count == 0 - ? RemoveLeadingBlankLinesFromFirstMember(compilationUnitWithoutUsings) - : compilationUnitWithoutUsings; - - // Fix the leading trivia for the namespace declaration. - var namespaceDeclaration = (BaseNamespaceDeclarationSyntax)compilationUnitWithoutBlankLine.Members[0]; - var namespaceDeclarationWithBlankLine = EnsureLeadingBlankLineBeforeFirstMember(namespaceDeclaration); - - // Update the namespace declaration with the usings from the compilation unit. - var newUsings = namespaceDeclarationWithBlankLine.Usings.InsertRange(0, usingsToAdd); - var namespaceDeclarationWithUsings = namespaceDeclarationWithBlankLine.WithUsings(newUsings); - - // Update the compilation unit with the new namespace declaration - return compilationUnitWithoutBlankLine.ReplaceNode(namespaceDeclaration, namespaceDeclarationWithUsings); - } + private static async Task ExpandUsingDirectiveAsync(Document document, UsingDirectiveSyntax usingDirective, CancellationToken cancellationToken) + { + var newType = await Simplifier.ExpandAsync(usingDirective.NamespaceOrType, document, cancellationToken: cancellationToken).ConfigureAwait(false); + return usingDirective.WithNamespaceOrType(newType); + } - private static CompilationUnitSyntax MoveUsingsOutsideNamespaces(CompilationUnitSyntax compilationUnit) - { - var namespaceDeclarations = compilationUnit.Members.OfType(); - var namespaceDeclarationMap = namespaceDeclarations.ToDictionary( - namespaceDeclaration => namespaceDeclaration, RemoveUsingsFromNamespace); + private static CompilationUnitSyntax MoveUsingsInsideNamespace(CompilationUnitSyntax compilationUnit) + { + // Get the compilation unit usings and set them up to format when moved. + var usingsToAdd = compilationUnit.Usings + .Where(u => u.GlobalKeyword == default) + .Select(d => d.WithAdditionalAnnotations(Formatter.Annotation, s_warningAnnotation)); + + // Remove usings and fix leading trivia for compilation unit. + var compilationUnitWithoutUsings = compilationUnit.WithUsings([.. compilationUnit.Usings.Where(u => u.GlobalKeyword != default)]); + var compilationUnitWithoutBlankLine = compilationUnitWithoutUsings.Usings.Count == 0 + ? RemoveLeadingBlankLinesFromFirstMember(compilationUnitWithoutUsings) + : compilationUnitWithoutUsings; + + // Fix the leading trivia for the namespace declaration. + var namespaceDeclaration = (BaseNamespaceDeclarationSyntax)compilationUnitWithoutBlankLine.Members[0]; + var namespaceDeclarationWithBlankLine = EnsureLeadingBlankLineBeforeFirstMember(namespaceDeclaration); + + // Update the namespace declaration with the usings from the compilation unit. + var newUsings = namespaceDeclarationWithBlankLine.Usings.InsertRange(0, usingsToAdd); + var namespaceDeclarationWithUsings = namespaceDeclarationWithBlankLine.WithUsings(newUsings); + + // Update the compilation unit with the new namespace declaration + return compilationUnitWithoutBlankLine.ReplaceNode(namespaceDeclaration, namespaceDeclarationWithUsings); + } - // Replace the namespace declarations in the compilation with the ones without using directives. - var compilationUnitWithReplacedNamespaces = compilationUnit.ReplaceNodes( - namespaceDeclarations, (node, _) => namespaceDeclarationMap[node].namespaceWithoutUsings); + private static CompilationUnitSyntax MoveUsingsOutsideNamespaces(CompilationUnitSyntax compilationUnit) + { + var namespaceDeclarations = compilationUnit.Members.OfType(); + var namespaceDeclarationMap = namespaceDeclarations.ToDictionary( + namespaceDeclaration => namespaceDeclaration, RemoveUsingsFromNamespace); - // Get the using directives from the namespaces and set them up to format when moved. - var usingsToAdd = namespaceDeclarationMap.Values.SelectMany(result => result.usingsFromNamespace) - .Select(directive => directive.WithAdditionalAnnotations(Formatter.Annotation, s_warningAnnotation)); + // Replace the namespace declarations in the compilation with the ones without using directives. + var compilationUnitWithReplacedNamespaces = compilationUnit.ReplaceNodes( + namespaceDeclarations, (node, _) => namespaceDeclarationMap[node].namespaceWithoutUsings); - var (deduplicatedUsings, orphanedTrivia) = RemoveDuplicateUsings(compilationUnit.Usings, usingsToAdd.ToImmutableArray()); + // Get the using directives from the namespaces and set them up to format when moved. + var usingsToAdd = namespaceDeclarationMap.Values.SelectMany(result => result.usingsFromNamespace) + .Select(directive => directive.WithAdditionalAnnotations(Formatter.Annotation, s_warningAnnotation)); - // Update the compilation unit with the usings from the namespace declaration. - var newUsings = compilationUnitWithReplacedNamespaces.Usings.AddRange(deduplicatedUsings); - var compilationUnitWithUsings = compilationUnitWithReplacedNamespaces.WithUsings(newUsings); + var (deduplicatedUsings, orphanedTrivia) = RemoveDuplicateUsings(compilationUnit.Usings, usingsToAdd.ToImmutableArray()); - // Fix the leading trivia for the compilation unit. - var compilationUnitWithSeparatorLine = EnsureLeadingBlankLineBeforeFirstMember(compilationUnitWithUsings); + // Update the compilation unit with the usings from the namespace declaration. + var newUsings = compilationUnitWithReplacedNamespaces.Usings.AddRange(deduplicatedUsings); + var compilationUnitWithUsings = compilationUnitWithReplacedNamespaces.WithUsings(newUsings); - if (!orphanedTrivia.Any()) - { - return compilationUnitWithSeparatorLine; - } + // Fix the leading trivia for the compilation unit. + var compilationUnitWithSeparatorLine = EnsureLeadingBlankLineBeforeFirstMember(compilationUnitWithUsings); - // Add leading trivia that was orphaned from removing duplicate using directives to the first member in the compilation unit. - var firstMember = compilationUnitWithSeparatorLine.Members[0]; - return compilationUnitWithSeparatorLine.ReplaceNode(firstMember, firstMember.WithPrependedLeadingTrivia(orphanedTrivia)); + if (!orphanedTrivia.Any()) + { + return compilationUnitWithSeparatorLine; } - private static (BaseNamespaceDeclarationSyntax namespaceWithoutUsings, ImmutableArray usingsFromNamespace) RemoveUsingsFromNamespace( - BaseNamespaceDeclarationSyntax usingContainer) - { - var namespaceDeclarations = usingContainer.Members.OfType(); - var namespaceDeclarationMap = namespaceDeclarations.ToDictionary( - namespaceDeclaration => namespaceDeclaration, namespaceDeclaration => RemoveUsingsFromNamespace(namespaceDeclaration)); + // Add leading trivia that was orphaned from removing duplicate using directives to the first member in the compilation unit. + var firstMember = compilationUnitWithSeparatorLine.Members[0]; + return compilationUnitWithSeparatorLine.ReplaceNode(firstMember, firstMember.WithPrependedLeadingTrivia(orphanedTrivia)); + } - // Get the using directives from the namespaces. - var usingsFromNamespaces = namespaceDeclarationMap.Values.SelectMany(result => result.usingsFromNamespace); - var allUsings = usingContainer.Usings.AsEnumerable().Concat(usingsFromNamespaces).ToImmutableArray(); + private static (BaseNamespaceDeclarationSyntax namespaceWithoutUsings, ImmutableArray usingsFromNamespace) RemoveUsingsFromNamespace( + BaseNamespaceDeclarationSyntax usingContainer) + { + var namespaceDeclarations = usingContainer.Members.OfType(); + var namespaceDeclarationMap = namespaceDeclarations.ToDictionary( + namespaceDeclaration => namespaceDeclaration, namespaceDeclaration => RemoveUsingsFromNamespace(namespaceDeclaration)); - // Replace the namespace declarations in the compilation with the ones without using directives. - var namespaceDeclarationWithReplacedNamespaces = usingContainer.ReplaceNodes( - namespaceDeclarations, (node, _) => namespaceDeclarationMap[node].namespaceWithoutUsings); + // Get the using directives from the namespaces. + var usingsFromNamespaces = namespaceDeclarationMap.Values.SelectMany(result => result.usingsFromNamespace); + var allUsings = usingContainer.Usings.AsEnumerable().Concat(usingsFromNamespaces).ToImmutableArray(); - // Remove usings and fix leading trivia for namespace declaration. - var namespaceDeclarationWithoutUsings = namespaceDeclarationWithReplacedNamespaces.WithUsings(default); - var namespaceDeclarationWithoutBlankLine = RemoveLeadingBlankLinesFromFirstMember(namespaceDeclarationWithoutUsings); + // Replace the namespace declarations in the compilation with the ones without using directives. + var namespaceDeclarationWithReplacedNamespaces = usingContainer.ReplaceNodes( + namespaceDeclarations, (node, _) => namespaceDeclarationMap[node].namespaceWithoutUsings); - return (namespaceDeclarationWithoutBlankLine, allUsings); - } + // Remove usings and fix leading trivia for namespace declaration. + var namespaceDeclarationWithoutUsings = namespaceDeclarationWithReplacedNamespaces.WithUsings(default); + var namespaceDeclarationWithoutBlankLine = RemoveLeadingBlankLinesFromFirstMember(namespaceDeclarationWithoutUsings); - private static (IEnumerable deduplicatedUsings, IEnumerable orphanedTrivia) RemoveDuplicateUsings( - IEnumerable existingUsings, - ImmutableArray usingsToAdd) - { - var seenUsings = existingUsings.ToList(); + return (namespaceDeclarationWithoutBlankLine, allUsings); + } + + private static (IEnumerable deduplicatedUsings, IEnumerable orphanedTrivia) RemoveDuplicateUsings( + IEnumerable existingUsings, + ImmutableArray usingsToAdd) + { + var seenUsings = existingUsings.ToList(); - var deduplicatedUsingsBuilder = ImmutableArray.CreateBuilder(); - var orphanedTrivia = Enumerable.Empty(); + var deduplicatedUsingsBuilder = ImmutableArray.CreateBuilder(); + var orphanedTrivia = Enumerable.Empty(); - foreach (var usingDirective in usingsToAdd) + foreach (var usingDirective in usingsToAdd) + { + // Check is the node is a duplicate. + if (seenUsings.Any(seenUsingDirective => seenUsingDirective.IsEquivalentTo(usingDirective, topLevel: false))) { - // Check is the node is a duplicate. - if (seenUsings.Any(seenUsingDirective => seenUsingDirective.IsEquivalentTo(usingDirective, topLevel: false))) - { - // If there was trivia from the duplicate node, check if any of the trivia is necessary to keep. - var leadingTrivia = usingDirective.GetLeadingTrivia(); - if (leadingTrivia.Any(trivia => !trivia.IsWhitespaceOrEndOfLine())) - { - // Capture the meaningful trivia so we can prepend it to the next kept node. - orphanedTrivia = orphanedTrivia.Concat(leadingTrivia); - } - } - else + // If there was trivia from the duplicate node, check if any of the trivia is necessary to keep. + var leadingTrivia = usingDirective.GetLeadingTrivia(); + if (leadingTrivia.Any(trivia => !trivia.IsWhitespaceOrEndOfLine())) { - seenUsings.Add(usingDirective); - - // Add any orphaned trivia to this node. - deduplicatedUsingsBuilder.Add(usingDirective.WithPrependedLeadingTrivia(orphanedTrivia)); - orphanedTrivia = []; + // Capture the meaningful trivia so we can prepend it to the next kept node. + orphanedTrivia = orphanedTrivia.Concat(leadingTrivia); } } + else + { + seenUsings.Add(usingDirective); - return (deduplicatedUsingsBuilder.ToImmutable(), orphanedTrivia); + // Add any orphaned trivia to this node. + deduplicatedUsingsBuilder.Add(usingDirective.WithPrependedLeadingTrivia(orphanedTrivia)); + orphanedTrivia = []; + } } - private static SyntaxList GetMembers(SyntaxNode node) - => node switch - { - CompilationUnitSyntax compilationUnit => compilationUnit.Members, - BaseNamespaceDeclarationSyntax namespaceDeclaration => namespaceDeclaration.Members, - _ => throw ExceptionUtilities.UnexpectedValue(node) - }; + return (deduplicatedUsingsBuilder.ToImmutable(), orphanedTrivia); + } - private static TSyntaxNode RemoveLeadingBlankLinesFromFirstMember(TSyntaxNode node) where TSyntaxNode : SyntaxNode + private static SyntaxList GetMembers(SyntaxNode node) + => node switch { - var members = GetMembers(node); - if (members.Count == 0) - return node; + CompilationUnitSyntax compilationUnit => compilationUnit.Members, + BaseNamespaceDeclarationSyntax namespaceDeclaration => namespaceDeclaration.Members, + _ => throw ExceptionUtilities.UnexpectedValue(node) + }; - var firstMember = members.First(); - var firstMemberTrivia = firstMember.GetLeadingTrivia(); + private static TSyntaxNode RemoveLeadingBlankLinesFromFirstMember(TSyntaxNode node) where TSyntaxNode : SyntaxNode + { + var members = GetMembers(node); + if (members.Count == 0) + return node; - // If there is no leading trivia, then return the node as it is. - if (firstMemberTrivia.Count == 0) - return node; + var firstMember = members.First(); + var firstMemberTrivia = firstMember.GetLeadingTrivia(); - var newTrivia = SplitIntoLines(firstMemberTrivia) - .SkipWhile(trivia => trivia.All(t => t.IsWhitespaceOrEndOfLine()) && trivia.Last().IsKind(SyntaxKind.EndOfLineTrivia)) - .SelectMany(t => t); + // If there is no leading trivia, then return the node as it is. + if (firstMemberTrivia.Count == 0) + return node; - var newFirstMember = firstMember.WithLeadingTrivia(newTrivia); - return node.ReplaceNode(firstMember, newFirstMember); - } + var newTrivia = SplitIntoLines(firstMemberTrivia) + .SkipWhile(trivia => trivia.All(t => t.IsWhitespaceOrEndOfLine()) && trivia.Last().IsKind(SyntaxKind.EndOfLineTrivia)) + .SelectMany(t => t); - private static IEnumerable> SplitIntoLines(SyntaxTriviaList triviaList) - { - var index = 0; - for (var i = 0; i < triviaList.Count; i++) - { - if (triviaList[i].IsEndOfLine()) - { - yield return triviaList.TakeRange(index, i); - index = i + 1; - } - } + var newFirstMember = firstMember.WithLeadingTrivia(newTrivia); + return node.ReplaceNode(firstMember, newFirstMember); + } - if (index < triviaList.Count) + private static IEnumerable> SplitIntoLines(SyntaxTriviaList triviaList) + { + var index = 0; + for (var i = 0; i < triviaList.Count; i++) + { + if (triviaList[i].IsEndOfLine()) { - yield return triviaList.TakeRange(index, triviaList.Count - 1); + yield return triviaList.TakeRange(index, i); + index = i + 1; } } - private static TSyntaxNode EnsureLeadingBlankLineBeforeFirstMember(TSyntaxNode node) where TSyntaxNode : SyntaxNode + if (index < triviaList.Count) { - var members = GetMembers(node); - if (members.Count == 0) - return node; - - var firstMember = members.First(); - var firstMemberTrivia = firstMember.GetLeadingTrivia(); + yield return triviaList.TakeRange(index, triviaList.Count - 1); + } + } - // If the first member already contains a leading new line then, this will already break up the usings from these members. - if (firstMemberTrivia is [(kind: SyntaxKind.EndOfLineTrivia), ..]) - return node; + private static TSyntaxNode EnsureLeadingBlankLineBeforeFirstMember(TSyntaxNode node) where TSyntaxNode : SyntaxNode + { + var members = GetMembers(node); + if (members.Count == 0) + return node; - var newFirstMember = firstMember.WithLeadingTrivia(firstMemberTrivia.Insert(0, SyntaxFactory.CarriageReturnLineFeed)); - return node.ReplaceNode(firstMember, newFirstMember); - } + var firstMember = members.First(); + var firstMemberTrivia = firstMember.GetLeadingTrivia(); - private static (AddImportPlacement placement, bool preferPreservation) DeterminePlacement(CompilationUnitSyntax compilationUnit, CodeStyleOption2 styleOption) - { - var placement = styleOption.Value; - var preferPreservation = styleOption.Notification == NotificationOption2.None; + // If the first member already contains a leading new line then, this will already break up the usings from these members. + if (firstMemberTrivia is [(kind: SyntaxKind.EndOfLineTrivia), ..]) + return node; - if (preferPreservation || placement == AddImportPlacement.OutsideNamespace) - return (placement, preferPreservation); + var newFirstMember = firstMember.WithLeadingTrivia(firstMemberTrivia.Insert(0, SyntaxFactory.CarriageReturnLineFeed)); + return node.ReplaceNode(firstMember, newFirstMember); + } - // Determine if we can safely apply the InsideNamespace preference. + private static (AddImportPlacement placement, bool preferPreservation) DeterminePlacement(CompilationUnitSyntax compilationUnit, CodeStyleOption2 styleOption) + { + var placement = styleOption.Value; + var preferPreservation = styleOption.Notification == NotificationOption2.None; - // Do not offer a code fix when there are multiple namespaces in the source file. When there are - // nested namespaces it is not clear if inner usings can be moved outwards without causing - // collisions. Also, when moving usings inwards it is complex to determine which namespaces they - // should be moved into. + if (preferPreservation || placement == AddImportPlacement.OutsideNamespace) + return (placement, preferPreservation); - // Only move using declarations inside the namespace when - // - There are no global attributes - // - There are no type definitions outside of the single top level namespace - // - There is only a single namespace declared at the top level - var forcePreservation = compilationUnit.AttributeLists.Any() - || compilationUnit.Members.Count > 1 - || !HasOneNamespace(compilationUnit); + // Determine if we can safely apply the InsideNamespace preference. - return (AddImportPlacement.InsideNamespace, forcePreservation); - } + // Do not offer a code fix when there are multiple namespaces in the source file. When there are + // nested namespaces it is not clear if inner usings can be moved outwards without causing + // collisions. Also, when moving usings inwards it is complex to determine which namespaces they + // should be moved into. - private static bool HasOneNamespace(CompilationUnitSyntax compilationUnit) - { - // Find all the NamespaceDeclarations - var allNamespaces = compilationUnit - .DescendantNodes(node => node is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax) - .OfType(); + // Only move using declarations inside the namespace when + // - There are no global attributes + // - There are no type definitions outside of the single top level namespace + // - There is only a single namespace declared at the top level + var forcePreservation = compilationUnit.AttributeLists.Any() + || compilationUnit.Members.Count > 1 + || !HasOneNamespace(compilationUnit); - // To determine if there are multiple namespaces we only need to look for at least two. - return allNamespaces.Take(2).Count() == 1; - } + return (AddImportPlacement.InsideNamespace, forcePreservation); + } - private static (CompilationUnitSyntax compilationUnitWithoutHeader, ImmutableArray header) RemoveFileHeader( - CompilationUnitSyntax syntaxRoot, IFileBannerFactsService bannerService) - { - var fileHeader = bannerService.GetFileBanner(syntaxRoot); - var leadingTrivia = syntaxRoot.GetLeadingTrivia(); + private static bool HasOneNamespace(CompilationUnitSyntax compilationUnit) + { + // Find all the NamespaceDeclarations + var allNamespaces = compilationUnit + .DescendantNodes(node => node is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax) + .OfType(); - for (var i = fileHeader.Length - 1; i >= 0; i--) - { - leadingTrivia = leadingTrivia.RemoveAt(i); - } + // To determine if there are multiple namespaces we only need to look for at least two. + return allNamespaces.Take(2).Count() == 1; + } - var newCompilationUnit = syntaxRoot.WithLeadingTrivia(leadingTrivia); + private static (CompilationUnitSyntax compilationUnitWithoutHeader, ImmutableArray header) RemoveFileHeader( + CompilationUnitSyntax syntaxRoot, IFileBannerFactsService bannerService) + { + var fileHeader = bannerService.GetFileBanner(syntaxRoot); + var leadingTrivia = syntaxRoot.GetLeadingTrivia(); - return (newCompilationUnit, fileHeader); + for (var i = fileHeader.Length - 1; i >= 0; i--) + { + leadingTrivia = leadingTrivia.RemoveAt(i); } - private static CompilationUnitSyntax AddFileHeader(CompilationUnitSyntax compilationUnit, ImmutableArray fileHeader) - { - if (fileHeader.IsEmpty) - { - return compilationUnit; - } + var newCompilationUnit = syntaxRoot.WithLeadingTrivia(leadingTrivia); - // Add leading trivia to the first token. - var firstToken = compilationUnit.GetFirstToken(includeZeroWidth: true); - var newLeadingTrivia = firstToken.LeadingTrivia.InsertRange(0, fileHeader); - var newFirstToken = firstToken.WithLeadingTrivia(newLeadingTrivia); + return (newCompilationUnit, fileHeader); + } - return compilationUnit.ReplaceToken(firstToken, newFirstToken); + private static CompilationUnitSyntax AddFileHeader(CompilationUnitSyntax compilationUnit, ImmutableArray fileHeader) + { + if (fileHeader.IsEmpty) + { + return compilationUnit; } + + // Add leading trivia to the first token. + var firstToken = compilationUnit.GetFirstToken(includeZeroWidth: true); + var newLeadingTrivia = firstToken.LeadingTrivia.InsertRange(0, fileHeader); + var newFirstToken = firstToken.WithLeadingTrivia(newLeadingTrivia); + + return compilationUnit.ReplaceToken(firstToken, newFirstToken); } } diff --git a/src/Analyzers/CSharp/CodeFixes/NewLines/ArrowExpressionClausePlacement/ArrowExpressionClausePlacementCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/NewLines/ArrowExpressionClausePlacement/ArrowExpressionClausePlacementCodeFixProvider.cs index a61caac6e5faf..0efae742c6ca0 100644 --- a/src/Analyzers/CSharp/CodeFixes/NewLines/ArrowExpressionClausePlacement/ArrowExpressionClausePlacementCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/NewLines/ArrowExpressionClausePlacement/ArrowExpressionClausePlacementCodeFixProvider.cs @@ -19,87 +19,86 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.NewLines.ArrowExpressionClausePlacement +namespace Microsoft.CodeAnalysis.CSharp.NewLines.ArrowExpressionClausePlacement; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ArrowExpressionClausePlacement), Shared] +internal sealed class ArrowExpressionClausePlacementCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ArrowExpressionClausePlacement), Shared] - internal sealed class ArrowExpressionClausePlacementCodeFixProvider : CodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ArrowExpressionClausePlacementCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ArrowExpressionClausePlacementCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.ArrowExpressionClausePlacementDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.ArrowExpressionClausePlacementDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var diagnostic = context.Diagnostics.First(); - context.RegisterCodeFix( - CodeAction.Create( - CSharpCodeFixesResources.Place_token_on_following_line, - c => UpdateDocumentAsync(document, [diagnostic], c), - nameof(CSharpCodeFixesResources.Place_token_on_following_line)), - context.Diagnostics); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var diagnostic = context.Diagnostics.First(); + context.RegisterCodeFix( + CodeAction.Create( + CSharpCodeFixesResources.Place_token_on_following_line, + c => UpdateDocumentAsync(document, [diagnostic], c), + nameof(CSharpCodeFixesResources.Place_token_on_following_line)), + context.Diagnostics); + return Task.CompletedTask; + } - private static async Task UpdateDocumentAsync( - Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + private static async Task UpdateDocumentAsync( + Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + using var _ = ArrayBuilder.GetInstance(out var edits); + + foreach (var diagnostic in diagnostics) { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var edits); + var arrowToken = root.FindToken(diagnostic.Location.SourceSpan.Start); + Contract.ThrowIfTrue(arrowToken.Kind() != SyntaxKind.EqualsGreaterThanToken); - foreach (var diagnostic in diagnostics) - { - var arrowToken = root.FindToken(diagnostic.Location.SourceSpan.Start); - Contract.ThrowIfTrue(arrowToken.Kind() != SyntaxKind.EqualsGreaterThanToken); + var arrowExpression = (ArrowExpressionClauseSyntax)arrowToken.GetRequiredParent(); - var arrowExpression = (ArrowExpressionClauseSyntax)arrowToken.GetRequiredParent(); + AddEdits(text, arrowExpression.ArrowToken, arrowExpression.Expression, edits); + } - AddEdits(text, arrowExpression.ArrowToken, arrowExpression.Expression, edits); - } + var changedText = text.WithChanges(edits); + return document.WithText(changedText); + } - var changedText = text.WithChanges(edits); - return document.WithText(changedText); - } + private static void AddEdits( + SourceText text, + SyntaxToken token, + ExpressionSyntax nextExpression, + ArrayBuilder edits) + { + // Cases to consider + // x => + // x => + // x => /* comment */ + // x /* comment */ => + // x /* comment */ => /* comment */ + // + // in all these cases, we want to grab the arrow, and any spaces that follow and remove that, but we + // leave the rest where it is. We then move the arrow right before the start of the next token. + + var start = token.SpanStart; + var end = token.Span.End; - private static void AddEdits( - SourceText text, - SyntaxToken token, - ExpressionSyntax nextExpression, - ArrayBuilder edits) + while (end < text.Length && text[end] == ' ') + end++; + + if (end < text.Length && SyntaxFacts.IsNewLine(text[end])) { - // Cases to consider - // x => - // x => - // x => /* comment */ - // x /* comment */ => - // x /* comment */ => /* comment */ - // - // in all these cases, we want to grab the arrow, and any spaces that follow and remove that, but we - // leave the rest where it is. We then move the arrow right before the start of the next token. - - var start = token.SpanStart; - var end = token.Span.End; - - while (end < text.Length && text[end] == ' ') - end++; - - if (end < text.Length && SyntaxFacts.IsNewLine(text[end])) - { - while (start > 0 && text[start - 1] == ' ') - start--; - } - - edits.Add(new TextChange(TextSpan.FromBounds(start, end), "")); - edits.Add(new TextChange(new TextSpan(nextExpression.SpanStart, 0), token.Text + " ")); + while (start > 0 && text[start - 1] == ' ') + start--; } - public override FixAllProvider? GetFixAllProvider() - => FixAllProvider.Create(async (context, document, diagnostics) => await UpdateDocumentAsync(document, diagnostics, context.CancellationToken).ConfigureAwait(false)); + edits.Add(new TextChange(TextSpan.FromBounds(start, end), "")); + edits.Add(new TextChange(new TextSpan(nextExpression.SpanStart, 0), token.Text + " ")); } + + public override FixAllProvider? GetFixAllProvider() + => FixAllProvider.Create(async (context, document, diagnostics) => await UpdateDocumentAsync(document, diagnostics, context.CancellationToken).ConfigureAwait(false)); } diff --git a/src/Analyzers/CSharp/CodeFixes/NewLines/ConditionalExpressionPlacement/ConditionalExpressionPlacementCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/NewLines/ConditionalExpressionPlacement/ConditionalExpressionPlacementCodeFixProvider.cs index 361b4b8e8b07a..8889157d918c2 100644 --- a/src/Analyzers/CSharp/CodeFixes/NewLines/ConditionalExpressionPlacement/ConditionalExpressionPlacementCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/NewLines/ConditionalExpressionPlacement/ConditionalExpressionPlacementCodeFixProvider.cs @@ -19,89 +19,88 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConditionalExpressionPlacement +namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConditionalExpressionPlacement; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConditionalExpressionPlacement), Shared] +internal sealed class ConditionalExpressionPlacementCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConditionalExpressionPlacement), Shared] - internal sealed class ConditionalExpressionPlacementCodeFixProvider : CodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConditionalExpressionPlacementCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConditionalExpressionPlacementCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.ConditionalExpressionPlacementDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.ConditionalExpressionPlacementDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var diagnostic = context.Diagnostics.First(); - context.RegisterCodeFix( - CodeAction.Create( - CSharpCodeFixesResources.Place_token_on_following_line, - c => UpdateDocumentAsync(document, [diagnostic], c), - nameof(CSharpCodeFixesResources.Place_token_on_following_line)), - context.Diagnostics); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var diagnostic = context.Diagnostics.First(); + context.RegisterCodeFix( + CodeAction.Create( + CSharpCodeFixesResources.Place_token_on_following_line, + c => UpdateDocumentAsync(document, [diagnostic], c), + nameof(CSharpCodeFixesResources.Place_token_on_following_line)), + context.Diagnostics); + return Task.CompletedTask; + } - private static async Task UpdateDocumentAsync( - Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + private static async Task UpdateDocumentAsync( + Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + using var _ = ArrayBuilder.GetInstance(out var edits); + + foreach (var diagnostic in diagnostics) { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var edits); + var questionToken = root.FindToken(diagnostic.Location.SourceSpan.Start); + Contract.ThrowIfTrue(questionToken.Kind() != SyntaxKind.QuestionToken); - foreach (var diagnostic in diagnostics) - { - var questionToken = root.FindToken(diagnostic.Location.SourceSpan.Start); - Contract.ThrowIfTrue(questionToken.Kind() != SyntaxKind.QuestionToken); + var conditional = (ConditionalExpressionSyntax)questionToken.GetRequiredParent(); - var conditional = (ConditionalExpressionSyntax)questionToken.GetRequiredParent(); + AddEdits(text, conditional.QuestionToken, conditional.WhenTrue, edits); + AddEdits(text, conditional.ColonToken, conditional.WhenFalse, edits); + } - AddEdits(text, conditional.QuestionToken, conditional.WhenTrue, edits); - AddEdits(text, conditional.ColonToken, conditional.WhenFalse, edits); - } + var changedText = text.WithChanges(edits); + return document.WithText(changedText); + } - var changedText = text.WithChanges(edits); - return document.WithText(changedText); - } + private static void AddEdits( + SourceText text, + SyntaxToken token, + ExpressionSyntax nextExpression, + ArrayBuilder edits) + { + // Cases to consider + // x ? + // x ? + // x ? /* comment */ + // x /* comment */ ? + // x /* comment */ ? /* comment */ + // + // in all these cases, we want to grab the question, and any spaces that follow and remove that, but we + // leave the rest where it is. We then move the question right before the start of the next token. The + // same logic applies to the colon token. + + var start = token.SpanStart; + var end = token.Span.End; - private static void AddEdits( - SourceText text, - SyntaxToken token, - ExpressionSyntax nextExpression, - ArrayBuilder edits) + while (end < text.Length && text[end] == ' ') + end++; + + if (end < text.Length && SyntaxFacts.IsNewLine(text[end])) { - // Cases to consider - // x ? - // x ? - // x ? /* comment */ - // x /* comment */ ? - // x /* comment */ ? /* comment */ - // - // in all these cases, we want to grab the question, and any spaces that follow and remove that, but we - // leave the rest where it is. We then move the question right before the start of the next token. The - // same logic applies to the colon token. - - var start = token.SpanStart; - var end = token.Span.End; - - while (end < text.Length && text[end] == ' ') - end++; - - if (end < text.Length && SyntaxFacts.IsNewLine(text[end])) - { - while (start > 0 && text[start - 1] == ' ') - start--; - } - - edits.Add(new TextChange(TextSpan.FromBounds(start, end), "")); - edits.Add(new TextChange(new TextSpan(nextExpression.SpanStart, 0), token.Text + " ")); + while (start > 0 && text[start - 1] == ' ') + start--; } - public override FixAllProvider? GetFixAllProvider() - => FixAllProvider.Create(async (context, document, diagnostics) => await UpdateDocumentAsync(document, diagnostics, context.CancellationToken).ConfigureAwait(false)); + edits.Add(new TextChange(TextSpan.FromBounds(start, end), "")); + edits.Add(new TextChange(new TextSpan(nextExpression.SpanStart, 0), token.Text + " ")); } + + public override FixAllProvider? GetFixAllProvider() + => FixAllProvider.Create(async (context, document, diagnostics) => await UpdateDocumentAsync(document, diagnostics, context.CancellationToken).ConfigureAwait(false)); } diff --git a/src/Analyzers/CSharp/CodeFixes/NewLines/ConsecutiveBracePlacement/ConsecutiveBracePlacementCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/NewLines/ConsecutiveBracePlacement/ConsecutiveBracePlacementCodeFixProvider.cs index e68d162dda74e..8fbb8bcdb29fc 100644 --- a/src/Analyzers/CSharp/CodeFixes/NewLines/ConsecutiveBracePlacement/ConsecutiveBracePlacementCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/NewLines/ConsecutiveBracePlacement/ConsecutiveBracePlacementCodeFixProvider.cs @@ -18,84 +18,83 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConsecutiveBracePlacement +namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConsecutiveBracePlacement; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConsecutiveBracePlacement), Shared] +internal sealed class ConsecutiveBracePlacementCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConsecutiveBracePlacement), Shared] - internal sealed class ConsecutiveBracePlacementCodeFixProvider : CodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConsecutiveBracePlacementCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConsecutiveBracePlacementCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.ConsecutiveBracePlacementDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.ConsecutiveBracePlacementDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var diagnostic = context.Diagnostics.First(); - context.RegisterCodeFix( - CodeAction.Create( - CSharpCodeFixesResources.Remove_blank_lines_between_braces, - c => UpdateDocumentAsync(document, diagnostic, c), - nameof(CSharpCodeFixesResources.Remove_blank_lines_between_braces)), - context.Diagnostics); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var diagnostic = context.Diagnostics.First(); + context.RegisterCodeFix( + CodeAction.Create( + CSharpCodeFixesResources.Remove_blank_lines_between_braces, + c => UpdateDocumentAsync(document, diagnostic, c), + nameof(CSharpCodeFixesResources.Remove_blank_lines_between_braces)), + context.Diagnostics); + return Task.CompletedTask; + } - private static Task UpdateDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) - => FixAllAsync(document, [diagnostic], cancellationToken); + private static Task UpdateDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + => FixAllAsync(document, [diagnostic], cancellationToken); - public static async Task FixAllAsync(Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) - { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - using var _ = PooledDictionary.GetInstance(out var tokenToToken); + public static async Task FixAllAsync(Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + using var _ = PooledDictionary.GetInstance(out var tokenToToken); - foreach (var diagnostic in diagnostics) - FixOne(root, text, tokenToToken, diagnostic, cancellationToken); + foreach (var diagnostic in diagnostics) + FixOne(root, text, tokenToToken, diagnostic, cancellationToken); - var newRoot = root.ReplaceTokens(tokenToToken.Keys, (t1, _) => tokenToToken[t1]); + var newRoot = root.ReplaceTokens(tokenToToken.Keys, (t1, _) => tokenToToken[t1]); - return document.WithSyntaxRoot(newRoot); + return document.WithSyntaxRoot(newRoot); + } + + private static void FixOne( + SyntaxNode root, SourceText text, + Dictionary tokenToToken, + Diagnostic diagnostic, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var token = root.FindToken(diagnostic.Location.SourceSpan.Start); + if (!token.IsKind(SyntaxKind.CloseBraceToken)) + { + Debug.Fail("Could not find close brace in fixer"); + return; } - private static void FixOne( - SyntaxNode root, SourceText text, - Dictionary tokenToToken, - Diagnostic diagnostic, CancellationToken cancellationToken) + var firstBrace = token.GetPreviousToken(); + if (!firstBrace.IsKind(SyntaxKind.CloseBraceToken)) { - cancellationToken.ThrowIfCancellationRequested(); - - var token = root.FindToken(diagnostic.Location.SourceSpan.Start); - if (!token.IsKind(SyntaxKind.CloseBraceToken)) - { - Debug.Fail("Could not find close brace in fixer"); - return; - } - - var firstBrace = token.GetPreviousToken(); - if (!firstBrace.IsKind(SyntaxKind.CloseBraceToken)) - { - Debug.Fail("Could not find previous close brace in fixer"); - return; - } - - if (!ConsecutiveBracePlacementDiagnosticAnalyzer.HasExcessBlankLinesAfter( - text, firstBrace, out var secondBrace, out var lastEndOfLineTrivia)) - { - Debug.Fail("Could not match analyzer pattern"); - return; - } - - var updatedSecondBrace = secondBrace.WithLeadingTrivia( - secondBrace.LeadingTrivia.SkipWhile(t => t != lastEndOfLineTrivia).Skip(1)); - tokenToToken[secondBrace] = updatedSecondBrace; + Debug.Fail("Could not find previous close brace in fixer"); + return; } - public override FixAllProvider GetFixAllProvider() - => FixAllProvider.Create(async (context, document, diagnostics) => await FixAllAsync(document, diagnostics, context.CancellationToken).ConfigureAwait(false)); + if (!ConsecutiveBracePlacementDiagnosticAnalyzer.HasExcessBlankLinesAfter( + text, firstBrace, out var secondBrace, out var lastEndOfLineTrivia)) + { + Debug.Fail("Could not match analyzer pattern"); + return; + } + + var updatedSecondBrace = secondBrace.WithLeadingTrivia( + secondBrace.LeadingTrivia.SkipWhile(t => t != lastEndOfLineTrivia).Skip(1)); + tokenToToken[secondBrace] = updatedSecondBrace; } + + public override FixAllProvider GetFixAllProvider() + => FixAllProvider.Create(async (context, document, diagnostics) => await FixAllAsync(document, diagnostics, context.CancellationToken).ConfigureAwait(false)); } diff --git a/src/Analyzers/CSharp/CodeFixes/NewLines/ConstructorInitializerPlacement/ConstructorInitializerPlacementCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/NewLines/ConstructorInitializerPlacement/ConstructorInitializerPlacementCodeFixProvider.cs index e45217d16fff3..7e012daf50346 100644 --- a/src/Analyzers/CSharp/CodeFixes/NewLines/ConstructorInitializerPlacement/ConstructorInitializerPlacementCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/NewLines/ConstructorInitializerPlacement/ConstructorInitializerPlacementCodeFixProvider.cs @@ -17,103 +17,102 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConstructorInitializerPlacement +namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConstructorInitializerPlacement; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConstructorInitializerPlacement), Shared] +internal sealed class ConstructorInitializerPlacementCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConstructorInitializerPlacement), Shared] - internal sealed class ConstructorInitializerPlacementCodeFixProvider : CodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConstructorInitializerPlacementCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConstructorInitializerPlacementCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.ConstructorInitializerPlacementDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.ConstructorInitializerPlacementDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var diagnostic = context.Diagnostics.First(); - context.RegisterCodeFix( - CodeAction.Create( - CSharpCodeFixesResources.Place_token_on_following_line, - c => UpdateDocumentAsync(document, [diagnostic], c), - nameof(CSharpCodeFixesResources.Place_token_on_following_line)), - context.Diagnostics); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var diagnostic = context.Diagnostics.First(); + context.RegisterCodeFix( + CodeAction.Create( + CSharpCodeFixesResources.Place_token_on_following_line, + c => UpdateDocumentAsync(document, [diagnostic], c), + nameof(CSharpCodeFixesResources.Place_token_on_following_line)), + context.Diagnostics); + return Task.CompletedTask; + } - private static async Task UpdateDocumentAsync( - Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + private static async Task UpdateDocumentAsync( + Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + using var _ = PooledDictionary.GetInstance(out var replacementMap); + + foreach (var diagnostic in diagnostics) { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - using var _ = PooledDictionary.GetInstance(out var replacementMap); + var initializer = (ConstructorInitializerSyntax)diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); + var colonToken = initializer.ColonToken; + var thisBaseKeyword = initializer.ThisOrBaseKeyword; + var parenToken = colonToken.GetPreviousToken(); - foreach (var diagnostic in diagnostics) + if (text.AreOnSameLine(parenToken, colonToken)) + { + // something like: + // + // public C() : + // base() + // + // Move the trivia from the : to the preceding ) and move the trivia on 'base' to the colon, and + // add a space after it. + MoveTriviaWhenOnSameLine(replacementMap, colonToken, thisBaseKeyword); + } + else { - var initializer = (ConstructorInitializerSyntax)diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); - var colonToken = initializer.ColonToken; - var thisBaseKeyword = initializer.ThisOrBaseKeyword; - var parenToken = colonToken.GetPreviousToken(); - - if (text.AreOnSameLine(parenToken, colonToken)) - { - // something like: - // - // public C() : - // base() - // - // Move the trivia from the : to the preceding ) and move the trivia on 'base' to the colon, and - // add a space after it. - MoveTriviaWhenOnSameLine(replacementMap, colonToken, thisBaseKeyword); - } - else - { - // something like: - // - // public C() - // : - // base() - // - // Just add a space after the colon, and remove all leading trivia from this/base - replacementMap[colonToken] = colonToken.WithLeadingTrivia(colonToken.LeadingTrivia.AddRange(colonToken.TrailingTrivia).AddRange(thisBaseKeyword.LeadingTrivia)) - .WithTrailingTrivia(SyntaxFactory.Space); - replacementMap[thisBaseKeyword] = thisBaseKeyword.WithoutLeadingTrivia(); - } + // something like: + // + // public C() + // : + // base() + // + // Just add a space after the colon, and remove all leading trivia from this/base + replacementMap[colonToken] = colonToken.WithLeadingTrivia(colonToken.LeadingTrivia.AddRange(colonToken.TrailingTrivia).AddRange(thisBaseKeyword.LeadingTrivia)) + .WithTrailingTrivia(SyntaxFactory.Space); + replacementMap[thisBaseKeyword] = thisBaseKeyword.WithoutLeadingTrivia(); } + } - var newRoot = root.ReplaceTokens(replacementMap.Keys, (original, _) => replacementMap[original]); + var newRoot = root.ReplaceTokens(replacementMap.Keys, (original, _) => replacementMap[original]); - return document.WithSyntaxRoot(newRoot); - } + return document.WithSyntaxRoot(newRoot); + } - private static void MoveTriviaWhenOnSameLine( - Dictionary replacementMap, SyntaxToken colonToken, SyntaxToken thisBaseKeyword) - { - // colonToken has the unnecessary newline. Move all of it's trivia to the previous token so nothing belongs to it. - var closeParen = colonToken.GetPreviousToken(); - replacementMap[closeParen] = ComputeNewCloseParen(colonToken, closeParen); + private static void MoveTriviaWhenOnSameLine( + Dictionary replacementMap, SyntaxToken colonToken, SyntaxToken thisBaseKeyword) + { + // colonToken has the unnecessary newline. Move all of it's trivia to the previous token so nothing belongs to it. + var closeParen = colonToken.GetPreviousToken(); + replacementMap[closeParen] = ComputeNewCloseParen(colonToken, closeParen); - // Now, take all the trivia from the this/base keyword, and move before the colon, and add a space after it - // this will place it properly before the this/base keyword. - replacementMap[colonToken] = colonToken.WithLeadingTrivia(thisBaseKeyword.LeadingTrivia).WithTrailingTrivia(SyntaxFactory.Space); + // Now, take all the trivia from the this/base keyword, and move before the colon, and add a space after it + // this will place it properly before the this/base keyword. + replacementMap[colonToken] = colonToken.WithLeadingTrivia(thisBaseKeyword.LeadingTrivia).WithTrailingTrivia(SyntaxFactory.Space); - // Finally, remove all leading trivia from the this/base keyword. It was moved to the colon - replacementMap[thisBaseKeyword] = thisBaseKeyword.WithoutLeadingTrivia(); + // Finally, remove all leading trivia from the this/base keyword. It was moved to the colon + replacementMap[thisBaseKeyword] = thisBaseKeyword.WithoutLeadingTrivia(); - static SyntaxToken ComputeNewCloseParen(SyntaxToken colonToken, SyntaxToken previousToken) - { - var allColonTrivia = colonToken.LeadingTrivia.AddRange(colonToken.TrailingTrivia); + static SyntaxToken ComputeNewCloseParen(SyntaxToken colonToken, SyntaxToken previousToken) + { + var allColonTrivia = colonToken.LeadingTrivia.AddRange(colonToken.TrailingTrivia); - return previousToken.TrailingTrivia.All(t => t.Kind() == SyntaxKind.WhitespaceTrivia) - ? previousToken.WithTrailingTrivia(allColonTrivia) - : previousToken.WithAppendedTrailingTrivia(allColonTrivia); - } + return previousToken.TrailingTrivia.All(t => t.Kind() == SyntaxKind.WhitespaceTrivia) + ? previousToken.WithTrailingTrivia(allColonTrivia) + : previousToken.WithAppendedTrailingTrivia(allColonTrivia); } - - public override FixAllProvider? GetFixAllProvider() - => FixAllProvider.Create(async (context, document, diagnostics) => await UpdateDocumentAsync(document, diagnostics, context.CancellationToken).ConfigureAwait(false)); } + + public override FixAllProvider? GetFixAllProvider() + => FixAllProvider.Create(async (context, document, diagnostics) => await UpdateDocumentAsync(document, diagnostics, context.CancellationToken).ConfigureAwait(false)); } diff --git a/src/Analyzers/CSharp/CodeFixes/NewLines/EmbeddedStatementPlacement/EmbeddedStatementPlacementCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/NewLines/EmbeddedStatementPlacement/EmbeddedStatementPlacementCodeFixProvider.cs index 0c421102dbb68..3d54e0573d54d 100644 --- a/src/Analyzers/CSharp/CodeFixes/NewLines/EmbeddedStatementPlacement/EmbeddedStatementPlacementCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/NewLines/EmbeddedStatementPlacement/EmbeddedStatementPlacementCodeFixProvider.cs @@ -18,123 +18,122 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.NewLines.EmbeddedStatementPlacement +namespace Microsoft.CodeAnalysis.CSharp.NewLines.EmbeddedStatementPlacement; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.EmbeddedStatementPlacement), Shared] +internal sealed class EmbeddedStatementPlacementCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.EmbeddedStatementPlacement), Shared] - internal sealed class EmbeddedStatementPlacementCodeFixProvider : CodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public EmbeddedStatementPlacementCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EmbeddedStatementPlacementCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.EmbeddedStatementPlacementDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.EmbeddedStatementPlacementDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var diagnostic = context.Diagnostics.First(); - context.RegisterCodeFix( - CodeAction.Create( - CSharpCodeFixesResources.Place_statement_on_following_line, - c => FixAllAsync(document, [diagnostic], context.GetOptionsProvider(), c), - nameof(CSharpCodeFixesResources.Place_statement_on_following_line)), - context.Diagnostics); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var diagnostic = context.Diagnostics.First(); + context.RegisterCodeFix( + CodeAction.Create( + CSharpCodeFixesResources.Place_statement_on_following_line, + c => FixAllAsync(document, [diagnostic], context.GetOptionsProvider(), c), + nameof(CSharpCodeFixesResources.Place_statement_on_following_line)), + context.Diagnostics); + return Task.CompletedTask; + } - public static async Task FixAllAsync(Document document, ImmutableArray diagnostics, CodeActionOptionsProvider codeActionOptionsProvider, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var editor = new SyntaxEditor(root, document.Project.Solution.Services); + public static async Task FixAllAsync(Document document, ImmutableArray diagnostics, CodeActionOptionsProvider codeActionOptionsProvider, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var editor = new SyntaxEditor(root, document.Project.Solution.Services); + + var options = await document.GetCSharpCodeFixOptionsProviderAsync(codeActionOptionsProvider, cancellationToken).ConfigureAwait(false); - var options = await document.GetCSharpCodeFixOptionsProviderAsync(codeActionOptionsProvider, cancellationToken).ConfigureAwait(false); + var endOfLineTrivia = SyntaxFactory.ElasticEndOfLine(options.NewLine); - var endOfLineTrivia = SyntaxFactory.ElasticEndOfLine(options.NewLine); + foreach (var diagnostic in diagnostics) + FixOne(editor, diagnostic, endOfLineTrivia, cancellationToken); - foreach (var diagnostic in diagnostics) - FixOne(editor, diagnostic, endOfLineTrivia, cancellationToken); + return document.WithSyntaxRoot(editor.GetChangedRoot()); + } - return document.WithSyntaxRoot(editor.GetChangedRoot()); + private static void FixOne( + SyntaxEditor editor, + Diagnostic diagnostic, + SyntaxTrivia endOfLineTrivia, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var root = editor.OriginalRoot; + var node = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan); + if (node is not StatementSyntax startStatement) + { + Debug.Fail("Couldn't find statement in fixer"); + return; } - private static void FixOne( - SyntaxEditor editor, - Diagnostic diagnostic, - SyntaxTrivia endOfLineTrivia, - CancellationToken cancellationToken) + // fixup this statement and all nested statements that have an issue. + var descendentStatements = startStatement.DescendantNodesAndSelf().OfType(); + var badStatements = descendentStatements.Where(s => EmbeddedStatementPlacementDiagnosticAnalyzer.StatementNeedsWrapping(s)); + + // Walk from lower statements to higher so the higher up changes see the changes below. + foreach (var badStatement in badStatements.OrderByDescending(s => s.SpanStart)) { - cancellationToken.ThrowIfCancellationRequested(); - - var root = editor.OriginalRoot; - var node = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan); - if (node is not StatementSyntax startStatement) - { - Debug.Fail("Couldn't find statement in fixer"); - return; - } - - // fixup this statement and all nested statements that have an issue. - var descendentStatements = startStatement.DescendantNodesAndSelf().OfType(); - var badStatements = descendentStatements.Where(s => EmbeddedStatementPlacementDiagnosticAnalyzer.StatementNeedsWrapping(s)); - - // Walk from lower statements to higher so the higher up changes see the changes below. - foreach (var badStatement in badStatements.OrderByDescending(s => s.SpanStart)) - { - editor.ReplaceNode( - badStatement, - (currentBadStatement, _) => + editor.ReplaceNode( + badStatement, + (currentBadStatement, _) => + { + // Ensure a newline between the statement and the statement that preceded it. + var updatedStatement = AddLeadingTrivia(currentBadStatement, endOfLineTrivia); + + // Ensure that if we wrap an empty block that the trailing brace is on a new line as well. + if (updatedStatement is BlockSyntax blockSyntax && + blockSyntax.Statements.Count == 0) { - // Ensure a newline between the statement and the statement that preceded it. - var updatedStatement = AddLeadingTrivia(currentBadStatement, endOfLineTrivia); - - // Ensure that if we wrap an empty block that the trailing brace is on a new line as well. - if (updatedStatement is BlockSyntax blockSyntax && - blockSyntax.Statements.Count == 0) - { - updatedStatement = blockSyntax.WithCloseBraceToken( - AddLeadingTrivia(blockSyntax.CloseBraceToken, SyntaxFactory.ElasticMarker)); - } - - return updatedStatement; - }); - } - - // Now walk up all our containing blocks ensuring that they wrap over multiple lines - var ancestorBlocks = startStatement.AncestorsAndSelf().OfType(); - foreach (var block in ancestorBlocks) - { - var openBrace = block.OpenBraceToken; - var previousToken = openBrace.GetPreviousToken(); - - editor.ReplaceNode( - block, - (current, _) => + updatedStatement = blockSyntax.WithCloseBraceToken( + AddLeadingTrivia(blockSyntax.CloseBraceToken, SyntaxFactory.ElasticMarker)); + } + + return updatedStatement; + }); + } + + // Now walk up all our containing blocks ensuring that they wrap over multiple lines + var ancestorBlocks = startStatement.AncestorsAndSelf().OfType(); + foreach (var block in ancestorBlocks) + { + var openBrace = block.OpenBraceToken; + var previousToken = openBrace.GetPreviousToken(); + + editor.ReplaceNode( + block, + (current, _) => + { + // If the block's open { is not already on a new line, add an elastic marker so it will be placed there. + var currentBlock = (BlockSyntax)current; + if (!EmbeddedStatementPlacementDiagnosticAnalyzer.ContainsEndOfLineBetween(previousToken, openBrace)) { - // If the block's open { is not already on a new line, add an elastic marker so it will be placed there. - var currentBlock = (BlockSyntax)current; - if (!EmbeddedStatementPlacementDiagnosticAnalyzer.ContainsEndOfLineBetween(previousToken, openBrace)) - { - currentBlock = currentBlock.WithOpenBraceToken( - AddLeadingTrivia(currentBlock.OpenBraceToken, SyntaxFactory.ElasticMarker)); - } - - return currentBlock.WithCloseBraceToken( - AddLeadingTrivia(currentBlock.CloseBraceToken, SyntaxFactory.ElasticMarker)); - }); - } + currentBlock = currentBlock.WithOpenBraceToken( + AddLeadingTrivia(currentBlock.OpenBraceToken, SyntaxFactory.ElasticMarker)); + } + + return currentBlock.WithCloseBraceToken( + AddLeadingTrivia(currentBlock.CloseBraceToken, SyntaxFactory.ElasticMarker)); + }); } + } - private static SyntaxNode AddLeadingTrivia(SyntaxNode node, SyntaxTrivia trivia) - => node.WithLeadingTrivia(node.GetLeadingTrivia().Insert(0, trivia)); + private static SyntaxNode AddLeadingTrivia(SyntaxNode node, SyntaxTrivia trivia) + => node.WithLeadingTrivia(node.GetLeadingTrivia().Insert(0, trivia)); - private static SyntaxToken AddLeadingTrivia(SyntaxToken token, SyntaxTrivia trivia) - => token.WithLeadingTrivia(token.LeadingTrivia.Insert(0, trivia)); + private static SyntaxToken AddLeadingTrivia(SyntaxToken token, SyntaxTrivia trivia) + => token.WithLeadingTrivia(token.LeadingTrivia.Insert(0, trivia)); - public override FixAllProvider GetFixAllProvider() - => FixAllProvider.Create( - async (context, document, diagnostics) => await FixAllAsync(document, diagnostics, context.GetOptionsProvider(), context.CancellationToken).ConfigureAwait(false)); - } + public override FixAllProvider GetFixAllProvider() + => FixAllProvider.Create( + async (context, document, diagnostics) => await FixAllAsync(document, diagnostics, context.GetOptionsProvider(), context.CancellationToken).ConfigureAwait(false)); } diff --git a/src/Analyzers/CSharp/CodeFixes/Nullable/CSharpDeclareAsNullableCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/Nullable/CSharpDeclareAsNullableCodeFixProvider.cs index b308cc455943c..29af1616f6c1b 100644 --- a/src/Analyzers/CSharp/CodeFixes/Nullable/CSharpDeclareAsNullableCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/Nullable/CSharpDeclareAsNullableCodeFixProvider.cs @@ -19,326 +19,325 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.DeclareAsNullable +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.DeclareAsNullable; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.DeclareAsNullable), Shared] +internal class CSharpDeclareAsNullableCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.DeclareAsNullable), Shared] - internal class CSharpDeclareAsNullableCodeFixProvider : SyntaxEditorBasedCodeFixProvider + // We want to distinguish different situations: + // 1. local null assignments: `return null;`, `local = null;`, `parameter = null;` (high confidence that the null is introduced deliberately and the API should be updated) + // 2. invocation with null: `M(null);`, or assigning null to field or property (test code might do this even though the API should remain not-nullable, so FixAll should be invoked with care) + // 3. conditional: `return x?.ToString();` + private const string AssigningNullLiteralLocallyEquivalenceKey = nameof(AssigningNullLiteralLocallyEquivalenceKey); + private const string AssigningNullLiteralRemotelyEquivalenceKey = nameof(AssigningNullLiteralRemotelyEquivalenceKey); + private const string ConditionalOperatorEquivalenceKey = nameof(ConditionalOperatorEquivalenceKey); + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpDeclareAsNullableCodeFixProvider() { - // We want to distinguish different situations: - // 1. local null assignments: `return null;`, `local = null;`, `parameter = null;` (high confidence that the null is introduced deliberately and the API should be updated) - // 2. invocation with null: `M(null);`, or assigning null to field or property (test code might do this even though the API should remain not-nullable, so FixAll should be invoked with care) - // 3. conditional: `return x?.ToString();` - private const string AssigningNullLiteralLocallyEquivalenceKey = nameof(AssigningNullLiteralLocallyEquivalenceKey); - private const string AssigningNullLiteralRemotelyEquivalenceKey = nameof(AssigningNullLiteralRemotelyEquivalenceKey); - private const string ConditionalOperatorEquivalenceKey = nameof(ConditionalOperatorEquivalenceKey); - - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpDeclareAsNullableCodeFixProvider() - { - } + } - // warning CS8603: Possible null reference return. - // warning CS8600: Converting null literal or possible null value to non-nullable type. - // warning CS8625: Cannot convert null literal to non-nullable reference type. - // warning CS8618: Non-nullable property is uninitialized - public sealed override ImmutableArray FixableDiagnosticIds => ["CS8603", "CS8600", "CS8625", "CS8618"]; + // warning CS8603: Possible null reference return. + // warning CS8600: Converting null literal or possible null value to non-nullable type. + // warning CS8625: Cannot convert null literal to non-nullable reference type. + // warning CS8618: Non-nullable property is uninitialized + public sealed override ImmutableArray FixableDiagnosticIds => ["CS8603", "CS8600", "CS8625", "CS8618"]; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var cancellationToken = context.CancellationToken; + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var cancellationToken = context.CancellationToken; - var model = await context.Document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var node = context.Diagnostics.First().Location.FindNode(getInnermostNodeForTie: true, cancellationToken); + var model = await context.Document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var node = context.Diagnostics.First().Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - var declarationTypeToFix = TryGetDeclarationTypeToFix(model, node, cancellationToken); - if (declarationTypeToFix == null) - return; + var declarationTypeToFix = TryGetDeclarationTypeToFix(model, node, cancellationToken); + if (declarationTypeToFix == null) + return; - RegisterCodeFix(context, CSharpCodeFixesResources.Declare_as_nullable, GetEquivalenceKey(node, model)); - } + RegisterCodeFix(context, CSharpCodeFixesResources.Declare_as_nullable, GetEquivalenceKey(node, model)); + } - private static string GetEquivalenceKey(SyntaxNode node, SemanticModel model) + private static string GetEquivalenceKey(SyntaxNode node, SemanticModel model) + { + return IsRemoteApiUsage(node, model) + ? AssigningNullLiteralRemotelyEquivalenceKey + : node.IsKind(SyntaxKind.ConditionalAccessExpression) + ? ConditionalOperatorEquivalenceKey + : AssigningNullLiteralLocallyEquivalenceKey; + + static bool IsRemoteApiUsage(SyntaxNode node, SemanticModel model) { - return IsRemoteApiUsage(node, model) - ? AssigningNullLiteralRemotelyEquivalenceKey - : node.IsKind(SyntaxKind.ConditionalAccessExpression) - ? ConditionalOperatorEquivalenceKey - : AssigningNullLiteralLocallyEquivalenceKey; + if (node.IsParentKind(SyntaxKind.Argument)) + { + // M(null) could be used in a test + return true; + } - static bool IsRemoteApiUsage(SyntaxNode node, SemanticModel model) + if (node.Parent is AssignmentExpressionSyntax assignment) { - if (node.IsParentKind(SyntaxKind.Argument)) + var symbol = model.GetSymbolInfo(assignment.Left).Symbol; + if (symbol is IFieldSymbol) { - // M(null) could be used in a test + // x.field could be used in a test return true; } - - if (node.Parent is AssignmentExpressionSyntax assignment) + else if (symbol is IPropertySymbol) { - var symbol = model.GetSymbolInfo(assignment.Left).Symbol; - if (symbol is IFieldSymbol) - { - // x.field could be used in a test - return true; - } - else if (symbol is IPropertySymbol) - { - // x.Property could be used in a test - return true; - } + // x.Property could be used in a test + return true; } - - return false; } + + return false; } + } - protected override async Task FixAllAsync( - Document document, - ImmutableArray diagnostics, - SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - // a method can have multiple `return null;` statements, but we should only fix its return type once - using var _ = PooledHashSet.GetInstance(out var alreadyHandled); + protected override async Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + // a method can have multiple `return null;` statements, but we should only fix its return type once + using var _ = PooledHashSet.GetInstance(out var alreadyHandled); - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - foreach (var diagnostic in diagnostics) - { - var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - MakeDeclarationNullable(editor, model, node, alreadyHandled, cancellationToken); - } + foreach (var diagnostic in diagnostics) + { + var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); + MakeDeclarationNullable(editor, model, node, alreadyHandled, cancellationToken); } + } - protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic, Document document, SemanticModel model, string? equivalenceKey, CancellationToken cancellationToken) + protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic, Document document, SemanticModel model, string? equivalenceKey, CancellationToken cancellationToken) + { + var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); + return equivalenceKey == GetEquivalenceKey(node, model); + } + + private static void MakeDeclarationNullable( + SyntaxEditor editor, SemanticModel model, SyntaxNode node, HashSet alreadyHandled, CancellationToken cancellationToken) + { + var declarationTypeToFix = TryGetDeclarationTypeToFix(model, node, cancellationToken); + if (declarationTypeToFix != null && alreadyHandled.Add(declarationTypeToFix)) { - var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - return equivalenceKey == GetEquivalenceKey(node, model); + var fixedDeclaration = SyntaxFactory.NullableType(declarationTypeToFix.WithoutTrivia()).WithTriviaFrom(declarationTypeToFix); + editor.ReplaceNode(declarationTypeToFix, fixedDeclaration); } + } - private static void MakeDeclarationNullable( - SyntaxEditor editor, SemanticModel model, SyntaxNode node, HashSet alreadyHandled, CancellationToken cancellationToken) + private static TypeSyntax? TryGetDeclarationTypeToFix( + SemanticModel model, SyntaxNode node, CancellationToken cancellationToken) + { + if (!IsExpressionSupported(node)) + return null; + + if (node.Parent is (kind: SyntaxKind.ReturnStatement or SyntaxKind.YieldReturnStatement)) { - var declarationTypeToFix = TryGetDeclarationTypeToFix(model, node, cancellationToken); - if (declarationTypeToFix != null && alreadyHandled.Add(declarationTypeToFix)) + var containingMember = node.GetAncestors().FirstOrDefault( + a => a.Kind() is + SyntaxKind.MethodDeclaration or + SyntaxKind.PropertyDeclaration or + SyntaxKind.ParenthesizedLambdaExpression or + SyntaxKind.SimpleLambdaExpression or + SyntaxKind.LocalFunctionStatement or + SyntaxKind.AnonymousMethodExpression or + SyntaxKind.ConstructorDeclaration or + SyntaxKind.DestructorDeclaration or + SyntaxKind.OperatorDeclaration or + SyntaxKind.IndexerDeclaration or + SyntaxKind.EventDeclaration); + + if (containingMember == null) + return null; + + var onYield = node.IsParentKind(SyntaxKind.YieldReturnStatement); + + return containingMember switch { - var fixedDeclaration = SyntaxFactory.NullableType(declarationTypeToFix.WithoutTrivia()).WithTriviaFrom(declarationTypeToFix); - editor.ReplaceNode(declarationTypeToFix, fixedDeclaration); - } + MethodDeclarationSyntax method => + // string M() { return null; } + // async Task M() { return null; } + // IEnumerable M() { yield return null; } + TryGetReturnType(method.ReturnType, method.Modifiers, onYield), + + LocalFunctionStatementSyntax localFunction => + // string local() { return null; } + // async Task local() { return null; } + // IEnumerable local() { yield return null; } + TryGetReturnType(localFunction.ReturnType, localFunction.Modifiers, onYield), + + PropertyDeclarationSyntax property => + // string x { get { return null; } } + // IEnumerable Property { get { yield return null; } } + TryGetReturnType(property.Type, modifiers: default, onYield), + + _ => null, + }; } - private static TypeSyntax? TryGetDeclarationTypeToFix( - SemanticModel model, SyntaxNode node, CancellationToken cancellationToken) + // string x = null; + if (node.Parent?.Parent?.Parent is VariableDeclarationSyntax variableDeclaration) { - if (!IsExpressionSupported(node)) - return null; + // string x = null, y = null; + return variableDeclaration.Variables.Count == 1 ? variableDeclaration.Type : null; + } - if (node.Parent is (kind: SyntaxKind.ReturnStatement or SyntaxKind.YieldReturnStatement)) + // x = null; + if (node.Parent is AssignmentExpressionSyntax assignment) + { + var symbol = model.GetSymbolInfo(assignment.Left, cancellationToken).Symbol; + if (symbol is ILocalSymbol { DeclaringSyntaxReferences.Length: > 0 } local) { - var containingMember = node.GetAncestors().FirstOrDefault( - a => a.Kind() is - SyntaxKind.MethodDeclaration or - SyntaxKind.PropertyDeclaration or - SyntaxKind.ParenthesizedLambdaExpression or - SyntaxKind.SimpleLambdaExpression or - SyntaxKind.LocalFunctionStatement or - SyntaxKind.AnonymousMethodExpression or - SyntaxKind.ConstructorDeclaration or - SyntaxKind.DestructorDeclaration or - SyntaxKind.OperatorDeclaration or - SyntaxKind.IndexerDeclaration or - SyntaxKind.EventDeclaration); - - if (containingMember == null) - return null; - - var onYield = node.IsParentKind(SyntaxKind.YieldReturnStatement); - - return containingMember switch - { - MethodDeclarationSyntax method => - // string M() { return null; } - // async Task M() { return null; } - // IEnumerable M() { yield return null; } - TryGetReturnType(method.ReturnType, method.Modifiers, onYield), - - LocalFunctionStatementSyntax localFunction => - // string local() { return null; } - // async Task local() { return null; } - // IEnumerable local() { yield return null; } - TryGetReturnType(localFunction.ReturnType, localFunction.Modifiers, onYield), - - PropertyDeclarationSyntax property => - // string x { get { return null; } } - // IEnumerable Property { get { yield return null; } } - TryGetReturnType(property.Type, modifiers: default, onYield), - - _ => null, - }; + var syntax = local.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); + if (syntax is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Variables.Count: 1 } declaration }) + return declaration.Type; } - - // string x = null; - if (node.Parent?.Parent?.Parent is VariableDeclarationSyntax variableDeclaration) + else if (symbol is IParameterSymbol parameter) { - // string x = null, y = null; - return variableDeclaration.Variables.Count == 1 ? variableDeclaration.Type : null; + return TryGetParameterTypeSyntax(parameter, cancellationToken); } - - // x = null; - if (node.Parent is AssignmentExpressionSyntax assignment) + else if (symbol is IFieldSymbol { IsImplicitlyDeclared: false, DeclaringSyntaxReferences.Length: > 0 } field) { - var symbol = model.GetSymbolInfo(assignment.Left, cancellationToken).Symbol; - if (symbol is ILocalSymbol { DeclaringSyntaxReferences.Length: > 0 } local) - { - var syntax = local.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); - if (syntax is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Variables.Count: 1 } declaration }) - return declaration.Type; - } - else if (symbol is IParameterSymbol parameter) - { - return TryGetParameterTypeSyntax(parameter, cancellationToken); - } - else if (symbol is IFieldSymbol { IsImplicitlyDeclared: false, DeclaringSyntaxReferences.Length: > 0 } field) - { - // implicitly declared fields don't have DeclaringSyntaxReferences so filter them out - var syntax = field.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); - if (syntax is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Variables.Count: 1 } declaration }) - return declaration.Type; - - if (syntax is TupleElementSyntax tupleElement) - return tupleElement.Type; - } - else if (symbol is IFieldSymbol { CorrespondingTupleField: IFieldSymbol { Locations: [{ IsInSource: true } location] } }) - { - // Assigning a tuple field, eg. foo.Item1 = null - // The tupleField won't have DeclaringSyntaxReferences because it's implicitly declared, otherwise it - // would have fallen into the branch above. We can use the Locations instead, if there is one and it's in source - if (location.FindNode(cancellationToken) is TupleElementSyntax tupleElement) - return tupleElement.Type; - } - else if (symbol is IPropertySymbol { DeclaringSyntaxReferences.Length: > 0 } property) - { - var syntax = property.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); - if (syntax is PropertyDeclarationSyntax declaration) - return declaration.Type; - } + // implicitly declared fields don't have DeclaringSyntaxReferences so filter them out + var syntax = field.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); + if (syntax is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Variables.Count: 1 } declaration }) + return declaration.Type; - return null; + if (syntax is TupleElementSyntax tupleElement) + return tupleElement.Type; } - - // Method(null) - if (node.Parent is ArgumentSyntax argument && argument.Parent?.Parent is InvocationExpressionSyntax invocation) + else if (symbol is IFieldSymbol { CorrespondingTupleField: IFieldSymbol { Locations: [{ IsInSource: true } location] } }) { - var symbol = model.GetSymbolInfo(invocation.Expression, cancellationToken).Symbol; - if (symbol is not IMethodSymbol method || method.PartialImplementationPart is not null) - { - // We don't handle partial methods yet - return null; - } - - if (argument.NameColon?.Name is IdentifierNameSyntax { Identifier: var identifier }) - { - var parameter = method.Parameters.Where(p => p.Name == identifier.Text).FirstOrDefault(); - return TryGetParameterTypeSyntax(parameter, cancellationToken); - } + // Assigning a tuple field, eg. foo.Item1 = null + // The tupleField won't have DeclaringSyntaxReferences because it's implicitly declared, otherwise it + // would have fallen into the branch above. We can use the Locations instead, if there is one and it's in source + if (location.FindNode(cancellationToken) is TupleElementSyntax tupleElement) + return tupleElement.Type; + } + else if (symbol is IPropertySymbol { DeclaringSyntaxReferences.Length: > 0 } property) + { + var syntax = property.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); + if (syntax is PropertyDeclarationSyntax declaration) + return declaration.Type; + } - var index = invocation.ArgumentList.Arguments.IndexOf(argument); - if (index >= 0 && index < method.Parameters.Length) - { - var parameter = method.Parameters[index]; - return TryGetParameterTypeSyntax(parameter, cancellationToken); - } + return null; + } + // Method(null) + if (node.Parent is ArgumentSyntax argument && argument.Parent?.Parent is InvocationExpressionSyntax invocation) + { + var symbol = model.GetSymbolInfo(invocation.Expression, cancellationToken).Symbol; + if (symbol is not IMethodSymbol method || method.PartialImplementationPart is not null) + { + // We don't handle partial methods yet return null; } - // string x { get; set; } = null; - if (node.Parent?.Parent is PropertyDeclarationSyntax propertyDeclaration) - return propertyDeclaration.Type; - - // string x { get; } - // Unassigned value that's not marked as null - if (node is PropertyDeclarationSyntax propertyDeclarationSyntax) - return propertyDeclarationSyntax.Type; - - // string x; - // Unassigned value that's not marked as null - if (node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax, Variables.Count: 1 } declarationSyntax }) - return declarationSyntax.Type; - - // void M(string x = null) { } - if (node.Parent?.Parent is ParameterSyntax optionalParameter) + if (argument.NameColon?.Name is IdentifierNameSyntax { Identifier: var identifier }) { - var parameterSymbol = model.GetDeclaredSymbol(optionalParameter, cancellationToken); - return TryGetParameterTypeSyntax(parameterSymbol, cancellationToken); + var parameter = method.Parameters.Where(p => p.Name == identifier.Text).FirstOrDefault(); + return TryGetParameterTypeSyntax(parameter, cancellationToken); } - // static string M() => null; - if (node.IsParentKind(SyntaxKind.ArrowExpressionClause) && - node.Parent?.Parent is MethodDeclarationSyntax arrowMethod) + var index = invocation.ArgumentList.Arguments.IndexOf(argument); + if (index >= 0 && index < method.Parameters.Length) { - return arrowMethod.ReturnType; + var parameter = method.Parameters[index]; + return TryGetParameterTypeSyntax(parameter, cancellationToken); } return null; + } - // local functions - static TypeSyntax? TryGetReturnType(TypeSyntax returnType, SyntaxTokenList modifiers, bool onYield) - { - if (modifiers.Any(SyntaxKind.AsyncKeyword) || onYield) - { - // async Task M() { return null; } - // async IAsyncEnumerable M() { yield return null; } - // IEnumerable M() { yield return null; } - return TryGetSingleTypeArgument(returnType); - } + // string x { get; set; } = null; + if (node.Parent?.Parent is PropertyDeclarationSyntax propertyDeclaration) + return propertyDeclaration.Type; - // string M() { return null; } - return returnType; - } + // string x { get; } + // Unassigned value that's not marked as null + if (node is PropertyDeclarationSyntax propertyDeclarationSyntax) + return propertyDeclarationSyntax.Type; - static TypeSyntax? TryGetSingleTypeArgument(TypeSyntax type) - { - switch (type) - { - case QualifiedNameSyntax qualified: - return TryGetSingleTypeArgument(qualified.Right); + // string x; + // Unassigned value that's not marked as null + if (node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax, Variables.Count: 1 } declarationSyntax }) + return declarationSyntax.Type; - case GenericNameSyntax generic: - var typeArguments = generic.TypeArgumentList.Arguments; - if (typeArguments.Count == 1) - return typeArguments[0]; + // void M(string x = null) { } + if (node.Parent?.Parent is ParameterSyntax optionalParameter) + { + var parameterSymbol = model.GetDeclaredSymbol(optionalParameter, cancellationToken); + return TryGetParameterTypeSyntax(parameterSymbol, cancellationToken); + } - break; - } + // static string M() => null; + if (node.IsParentKind(SyntaxKind.ArrowExpressionClause) && + node.Parent?.Parent is MethodDeclarationSyntax arrowMethod) + { + return arrowMethod.ReturnType; + } - return null; + return null; + + // local functions + static TypeSyntax? TryGetReturnType(TypeSyntax returnType, SyntaxTokenList modifiers, bool onYield) + { + if (modifiers.Any(SyntaxKind.AsyncKeyword) || onYield) + { + // async Task M() { return null; } + // async IAsyncEnumerable M() { yield return null; } + // IEnumerable M() { yield return null; } + return TryGetSingleTypeArgument(returnType); } - static TypeSyntax? TryGetParameterTypeSyntax(IParameterSymbol? parameterSymbol, CancellationToken cancellationToken) + // string M() { return null; } + return returnType; + } + + static TypeSyntax? TryGetSingleTypeArgument(TypeSyntax type) + { + switch (type) { - if (parameterSymbol?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken) is ParameterSyntax parameterSyntax && - parameterSymbol.ContainingSymbol is IMethodSymbol method && - method.GetAllMethodSymbolsOfPartialParts().Length == 1) - { - return parameterSyntax.Type; - } + case QualifiedNameSyntax qualified: + return TryGetSingleTypeArgument(qualified.Right); - return null; + case GenericNameSyntax generic: + var typeArguments = generic.TypeArgumentList.Arguments; + if (typeArguments.Count == 1) + return typeArguments[0]; + + break; } + + return null; } - private static bool IsExpressionSupported(SyntaxNode node) - => node.Kind() is - SyntaxKind.NullLiteralExpression or - SyntaxKind.AsExpression or - SyntaxKind.DefaultExpression or - SyntaxKind.DefaultLiteralExpression or - SyntaxKind.ConditionalExpression or - SyntaxKind.ConditionalAccessExpression or - SyntaxKind.PropertyDeclaration or - SyntaxKind.VariableDeclarator; + static TypeSyntax? TryGetParameterTypeSyntax(IParameterSymbol? parameterSymbol, CancellationToken cancellationToken) + { + if (parameterSymbol?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken) is ParameterSyntax parameterSyntax && + parameterSymbol.ContainingSymbol is IMethodSymbol method && + method.GetAllMethodSymbolsOfPartialParts().Length == 1) + { + return parameterSyntax.Type; + } + + return null; + } } + + private static bool IsExpressionSupported(SyntaxNode node) + => node.Kind() is + SyntaxKind.NullLiteralExpression or + SyntaxKind.AsExpression or + SyntaxKind.DefaultExpression or + SyntaxKind.DefaultLiteralExpression or + SyntaxKind.ConditionalExpression or + SyntaxKind.ConditionalAccessExpression or + SyntaxKind.PropertyDeclaration or + SyntaxKind.VariableDeclarator; } diff --git a/src/Analyzers/CSharp/CodeFixes/OrderModifiers/CSharpOrderModifiersCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/OrderModifiers/CSharpOrderModifiersCodeFixProvider.cs index 504ef90d6d7ac..85de50c8f00fc 100644 --- a/src/Analyzers/CSharp/CodeFixes/OrderModifiers/CSharpOrderModifiersCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/OrderModifiers/CSharpOrderModifiersCodeFixProvider.cs @@ -14,23 +14,22 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.OrderModifiers; -namespace Microsoft.CodeAnalysis.CSharp.OrderModifiers +namespace Microsoft.CodeAnalysis.CSharp.OrderModifiers; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.OrderModifiers), Shared] +internal sealed class CSharpOrderModifiersCodeFixProvider : AbstractOrderModifiersCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.OrderModifiers), Shared] - internal sealed class CSharpOrderModifiersCodeFixProvider : AbstractOrderModifiersCodeFixProvider - { - private const string CS0267 = nameof(CS0267); // The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or 'void' + private const string CS0267 = nameof(CS0267); // The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or 'void' - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpOrderModifiersCodeFixProvider() - : base(CSharpSyntaxFacts.Instance, CSharpOrderModifiersHelper.Instance) - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpOrderModifiersCodeFixProvider() + : base(CSharpSyntaxFacts.Instance, CSharpOrderModifiersHelper.Instance) + { + } - protected override CodeStyleOption2 GetCodeStyleOption(AnalyzerOptionsProvider options) - => ((CSharpAnalyzerOptionsProvider)options).PreferredModifierOrder; + protected override CodeStyleOption2 GetCodeStyleOption(AnalyzerOptionsProvider options) + => ((CSharpAnalyzerOptionsProvider)options).PreferredModifierOrder; - protected override ImmutableArray FixableCompilerErrorIds { get; } = [CS0267]; - } + protected override ImmutableArray FixableCompilerErrorIds { get; } = [CS0267]; } diff --git a/src/Analyzers/CSharp/CodeFixes/PopulateSwitch/CSharpPopulateSwitchExpressionCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/PopulateSwitch/CSharpPopulateSwitchExpressionCodeFixProvider.cs index 885529c603c0a..b1691743e92e2 100644 --- a/src/Analyzers/CSharp/CodeFixes/PopulateSwitch/CSharpPopulateSwitchExpressionCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/PopulateSwitch/CSharpPopulateSwitchExpressionCodeFixProvider.cs @@ -11,47 +11,46 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.PopulateSwitch; -namespace Microsoft.CodeAnalysis.CSharp.PopulateSwitch +namespace Microsoft.CodeAnalysis.CSharp.PopulateSwitch; + +using static SyntaxFactory; + +[ExportCodeFixProvider(LanguageNames.CSharp, + Name = PredefinedCodeFixProviderNames.PopulateSwitchExpression), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.ImplementInterface)] +internal class CSharpPopulateSwitchExpressionCodeFixProvider + : AbstractPopulateSwitchExpressionCodeFixProvider< + ExpressionSyntax, + SwitchExpressionSyntax, + SwitchExpressionArmSyntax, + MemberAccessExpressionSyntax> { - using static SyntaxFactory; - - [ExportCodeFixProvider(LanguageNames.CSharp, - Name = PredefinedCodeFixProviderNames.PopulateSwitchExpression), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.ImplementInterface)] - internal class CSharpPopulateSwitchExpressionCodeFixProvider - : AbstractPopulateSwitchExpressionCodeFixProvider< - ExpressionSyntax, - SwitchExpressionSyntax, - SwitchExpressionArmSyntax, - MemberAccessExpressionSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpPopulateSwitchExpressionCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpPopulateSwitchExpressionCodeFixProvider() - { - } + } - protected override SwitchExpressionArmSyntax CreateDefaultSwitchArm(SyntaxGenerator generator, Compilation compilation) - => SwitchExpressionArm(DiscardPattern(), Exception(generator, compilation)); + protected override SwitchExpressionArmSyntax CreateDefaultSwitchArm(SyntaxGenerator generator, Compilation compilation) + => SwitchExpressionArm(DiscardPattern(), Exception(generator, compilation)); - protected override SwitchExpressionArmSyntax CreateSwitchArm(SyntaxGenerator generator, Compilation compilation, MemberAccessExpressionSyntax caseLabel) - => SwitchExpressionArm(ConstantPattern(caseLabel), Exception(generator, compilation)); + protected override SwitchExpressionArmSyntax CreateSwitchArm(SyntaxGenerator generator, Compilation compilation, MemberAccessExpressionSyntax caseLabel) + => SwitchExpressionArm(ConstantPattern(caseLabel), Exception(generator, compilation)); - protected override SwitchExpressionArmSyntax CreateNullSwitchArm(SyntaxGenerator generator, Compilation compilation) - => SwitchExpressionArm(ConstantPattern((LiteralExpressionSyntax)generator.NullLiteralExpression()), Exception(generator, compilation)); + protected override SwitchExpressionArmSyntax CreateNullSwitchArm(SyntaxGenerator generator, Compilation compilation) + => SwitchExpressionArm(ConstantPattern((LiteralExpressionSyntax)generator.NullLiteralExpression()), Exception(generator, compilation)); - protected override SwitchExpressionSyntax InsertSwitchArms(SyntaxGenerator generator, SwitchExpressionSyntax switchNode, int insertLocation, List newArms) + protected override SwitchExpressionSyntax InsertSwitchArms(SyntaxGenerator generator, SwitchExpressionSyntax switchNode, int insertLocation, List newArms) + { + // If the existing switch expression ends with a comma, then ensure that we preserve + // that. Also do this for an empty switch statement. + if (switchNode.Arms.Count == 0 || + !switchNode.Arms.GetWithSeparators().LastOrDefault().IsNode) { - // If the existing switch expression ends with a comma, then ensure that we preserve - // that. Also do this for an empty switch statement. - if (switchNode.Arms.Count == 0 || - !switchNode.Arms.GetWithSeparators().LastOrDefault().IsNode) - { - return switchNode.WithArms(switchNode.Arms.InsertRangeWithTrailingSeparator( - insertLocation, newArms, SyntaxKind.CommaToken)); - } - - return switchNode.WithArms(switchNode.Arms.InsertRange(insertLocation, newArms)); + return switchNode.WithArms(switchNode.Arms.InsertRangeWithTrailingSeparator( + insertLocation, newArms, SyntaxKind.CommaToken)); } + + return switchNode.WithArms(switchNode.Arms.InsertRange(insertLocation, newArms)); } } diff --git a/src/Analyzers/CSharp/CodeFixes/QualifyMemberAccess/CSharpQualifyMemberAccessCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/QualifyMemberAccess/CSharpQualifyMemberAccessCodeFixProvider.cs index 81c2b7d769bdc..dd3003ee9a437 100644 --- a/src/Analyzers/CSharp/CodeFixes/QualifyMemberAccess/CSharpQualifyMemberAccessCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/QualifyMemberAccess/CSharpQualifyMemberAccessCodeFixProvider.cs @@ -10,32 +10,31 @@ using Microsoft.CodeAnalysis.QualifyMemberAccess; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.QualifyMemberAccess +namespace Microsoft.CodeAnalysis.CSharp.QualifyMemberAccess; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.QualifyMemberAccess), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.RemoveUnnecessaryCast)] +internal class CSharpQualifyMemberAccessCodeFixProvider : AbstractQualifyMemberAccessCodeFixprovider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.QualifyMemberAccess), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.RemoveUnnecessaryCast)] - internal class CSharpQualifyMemberAccessCodeFixProvider : AbstractQualifyMemberAccessCodeFixprovider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpQualifyMemberAccessCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpQualifyMemberAccessCodeFixProvider() - { - } + } - protected override SimpleNameSyntax? GetNode(Diagnostic diagnostic, CancellationToken cancellationToken) + protected override SimpleNameSyntax? GetNode(Diagnostic diagnostic, CancellationToken cancellationToken) + { + var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); + switch (node) { - var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - switch (node) - { - case SimpleNameSyntax simpleNameSyntax: - return simpleNameSyntax; - case InvocationExpressionSyntax invocationExpressionSyntax: - return invocationExpressionSyntax.Expression as SimpleNameSyntax; - default: - return null; - } + case SimpleNameSyntax simpleNameSyntax: + return simpleNameSyntax; + case InvocationExpressionSyntax invocationExpressionSyntax: + return invocationExpressionSyntax.Expression as SimpleNameSyntax; + default: + return null; } - - protected override string GetTitle() => CSharpCodeFixesResources.Add_this; } + + protected override string GetTitle() => CSharpCodeFixesResources.Add_this; } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveAsyncModifier/CSharpRemoveAsyncModifierCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveAsyncModifier/CSharpRemoveAsyncModifierCodeFixProvider.cs index 27ee5d8e43857..5e29cca8ceb24 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveAsyncModifier/CSharpRemoveAsyncModifierCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveAsyncModifier/CSharpRemoveAsyncModifierCodeFixProvider.cs @@ -13,58 +13,57 @@ using Microsoft.CodeAnalysis.RemoveAsyncModifier; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.RemoveAsyncModifier +namespace Microsoft.CodeAnalysis.CSharp.RemoveAsyncModifier; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveAsyncModifier), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.MakeMethodSynchronous)] +internal partial class CSharpRemoveAsyncModifierCodeFixProvider : AbstractRemoveAsyncModifierCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveAsyncModifier), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.MakeMethodSynchronous)] - internal partial class CSharpRemoveAsyncModifierCodeFixProvider : AbstractRemoveAsyncModifierCodeFixProvider - { - private const string CS1998 = nameof(CS1998); // This async method lacks 'await' operators and will run synchronously. + private const string CS1998 = nameof(CS1998); // This async method lacks 'await' operators and will run synchronously. - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpRemoveAsyncModifierCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpRemoveAsyncModifierCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds { get; } = [CS1998]; + public override ImmutableArray FixableDiagnosticIds { get; } = [CS1998]; - protected override bool IsAsyncSupportingFunctionSyntax(SyntaxNode node) - => node.IsAsyncSupportingFunctionSyntax(); + protected override bool IsAsyncSupportingFunctionSyntax(SyntaxNode node) + => node.IsAsyncSupportingFunctionSyntax(); - protected override SyntaxNode? ConvertToBlockBody(SyntaxNode node, ExpressionSyntax expressionBody) + protected override SyntaxNode? ConvertToBlockBody(SyntaxNode node, ExpressionSyntax expressionBody) + { + var semicolonToken = SyntaxFactory.Token(SyntaxKind.SemicolonToken); + if (expressionBody.TryConvertToStatement(semicolonToken, createReturnStatementForExpression: false, out var statement)) { - var semicolonToken = SyntaxFactory.Token(SyntaxKind.SemicolonToken); - if (expressionBody.TryConvertToStatement(semicolonToken, createReturnStatementForExpression: false, out var statement)) + var block = SyntaxFactory.Block(statement); + return node switch { - var block = SyntaxFactory.Block(statement); - return node switch - { - MethodDeclarationSyntax method => method.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default), - LocalFunctionStatementSyntax localFunction => localFunction.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default), - AnonymousFunctionExpressionSyntax anonymousFunction => anonymousFunction.WithBody(block).WithExpressionBody(null), - _ => throw ExceptionUtilities.Unreachable() - }; - } - - return null; - } - - protected override SyntaxNode RemoveAsyncModifier(SyntaxGenerator generator, SyntaxNode methodLikeNode) - => methodLikeNode switch - { - MethodDeclarationSyntax method => RemoveAsyncModifierHelpers.WithoutAsyncModifier(method, method.ReturnType), - LocalFunctionStatementSyntax localFunction => RemoveAsyncModifierHelpers.WithoutAsyncModifier(localFunction, localFunction.ReturnType), - AnonymousMethodExpressionSyntax method => AnnotateBlock(generator, RemoveAsyncModifierHelpers.WithoutAsyncModifier(method)), - ParenthesizedLambdaExpressionSyntax lambda => AnnotateBlock(generator, RemoveAsyncModifierHelpers.WithoutAsyncModifier(lambda)), - SimpleLambdaExpressionSyntax lambda => AnnotateBlock(generator, RemoveAsyncModifierHelpers.WithoutAsyncModifier(lambda)), - _ => methodLikeNode, + MethodDeclarationSyntax method => method.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default), + LocalFunctionStatementSyntax localFunction => localFunction.WithBody(block).WithExpressionBody(null).WithSemicolonToken(default), + AnonymousFunctionExpressionSyntax anonymousFunction => anonymousFunction.WithBody(block).WithExpressionBody(null), + _ => throw ExceptionUtilities.Unreachable() }; + } - // Block bodied lambdas and anonymous methods need to be formatted after changing their modifiers, or their indentation is broken - private static SyntaxNode AnnotateBlock(SyntaxGenerator generator, SyntaxNode node) - => generator.GetExpression(node) == null - ? node.WithAdditionalAnnotations(Formatter.Annotation) - : node; + return null; } + + protected override SyntaxNode RemoveAsyncModifier(SyntaxGenerator generator, SyntaxNode methodLikeNode) + => methodLikeNode switch + { + MethodDeclarationSyntax method => RemoveAsyncModifierHelpers.WithoutAsyncModifier(method, method.ReturnType), + LocalFunctionStatementSyntax localFunction => RemoveAsyncModifierHelpers.WithoutAsyncModifier(localFunction, localFunction.ReturnType), + AnonymousMethodExpressionSyntax method => AnnotateBlock(generator, RemoveAsyncModifierHelpers.WithoutAsyncModifier(method)), + ParenthesizedLambdaExpressionSyntax lambda => AnnotateBlock(generator, RemoveAsyncModifierHelpers.WithoutAsyncModifier(lambda)), + SimpleLambdaExpressionSyntax lambda => AnnotateBlock(generator, RemoveAsyncModifierHelpers.WithoutAsyncModifier(lambda)), + _ => methodLikeNode, + }; + + // Block bodied lambdas and anonymous methods need to be formatted after changing their modifiers, or their indentation is broken + private static SyntaxNode AnnotateBlock(SyntaxGenerator generator, SyntaxNode node) + => generator.GetExpression(node) == null + ? node.WithAdditionalAnnotations(Formatter.Annotation) + : node; } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveAsyncModifier/RemoveAsyncModifierHelpers.cs b/src/Analyzers/CSharp/CodeFixes/RemoveAsyncModifier/RemoveAsyncModifierHelpers.cs index 35a2d9e25169d..1df3eefea5e0c 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveAsyncModifier/RemoveAsyncModifierHelpers.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveAsyncModifier/RemoveAsyncModifierHelpers.cs @@ -5,61 +5,60 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.RemoveAsyncModifier +namespace Microsoft.CodeAnalysis.CSharp.RemoveAsyncModifier; + +internal static class RemoveAsyncModifierHelpers { - internal static class RemoveAsyncModifierHelpers + internal static SyntaxNode WithoutAsyncModifier(MethodDeclarationSyntax method, TypeSyntax returnType) { - internal static SyntaxNode WithoutAsyncModifier(MethodDeclarationSyntax method, TypeSyntax returnType) - { - var newModifiers = RemoveAsyncModifier(method.Modifiers, ref returnType); - return method.WithReturnType(returnType).WithModifiers(newModifiers); - } + var newModifiers = RemoveAsyncModifier(method.Modifiers, ref returnType); + return method.WithReturnType(returnType).WithModifiers(newModifiers); + } - internal static SyntaxNode WithoutAsyncModifier(LocalFunctionStatementSyntax localFunction, TypeSyntax returnType) - { - var newModifiers = RemoveAsyncModifier(localFunction.Modifiers, ref returnType); - return localFunction.WithReturnType(returnType).WithModifiers(newModifiers); - } + internal static SyntaxNode WithoutAsyncModifier(LocalFunctionStatementSyntax localFunction, TypeSyntax returnType) + { + var newModifiers = RemoveAsyncModifier(localFunction.Modifiers, ref returnType); + return localFunction.WithReturnType(returnType).WithModifiers(newModifiers); + } - internal static SyntaxNode WithoutAsyncModifier(ParenthesizedLambdaExpressionSyntax lambda) - => lambda.WithAsyncKeyword(default).WithPrependedLeadingTrivia(lambda.AsyncKeyword.LeadingTrivia); + internal static SyntaxNode WithoutAsyncModifier(ParenthesizedLambdaExpressionSyntax lambda) + => lambda.WithAsyncKeyword(default).WithPrependedLeadingTrivia(lambda.AsyncKeyword.LeadingTrivia); - internal static SyntaxNode WithoutAsyncModifier(SimpleLambdaExpressionSyntax lambda) - => lambda.WithAsyncKeyword(default).WithPrependedLeadingTrivia(lambda.AsyncKeyword.LeadingTrivia); + internal static SyntaxNode WithoutAsyncModifier(SimpleLambdaExpressionSyntax lambda) + => lambda.WithAsyncKeyword(default).WithPrependedLeadingTrivia(lambda.AsyncKeyword.LeadingTrivia); - internal static SyntaxNode WithoutAsyncModifier(AnonymousMethodExpressionSyntax method) - => method.WithAsyncKeyword(default).WithPrependedLeadingTrivia(method.AsyncKeyword.LeadingTrivia); + internal static SyntaxNode WithoutAsyncModifier(AnonymousMethodExpressionSyntax method) + => method.WithAsyncKeyword(default).WithPrependedLeadingTrivia(method.AsyncKeyword.LeadingTrivia); - private static SyntaxTokenList RemoveAsyncModifier(SyntaxTokenList modifiers, ref TypeSyntax newReturnType) + private static SyntaxTokenList RemoveAsyncModifier(SyntaxTokenList modifiers, ref TypeSyntax newReturnType) + { + var asyncTokenIndex = modifiers.IndexOf(SyntaxKind.AsyncKeyword); + SyntaxTokenList newModifiers; + if (asyncTokenIndex == 0) { - var asyncTokenIndex = modifiers.IndexOf(SyntaxKind.AsyncKeyword); - SyntaxTokenList newModifiers; - if (asyncTokenIndex == 0) - { - // Have to move the trivia on the async token appropriately. - var asyncLeadingTrivia = modifiers[0].LeadingTrivia; + // Have to move the trivia on the async token appropriately. + var asyncLeadingTrivia = modifiers[0].LeadingTrivia; - if (modifiers.Count > 1) - { - // Move the trivia to the next modifier; - newModifiers = modifiers.Replace( - modifiers[1], - modifiers[1].WithPrependedLeadingTrivia(asyncLeadingTrivia)); - newModifiers = newModifiers.RemoveAt(0); - } - else - { - // move it to the return type. - newModifiers = default; - newReturnType = newReturnType.WithPrependedLeadingTrivia(asyncLeadingTrivia); - } + if (modifiers.Count > 1) + { + // Move the trivia to the next modifier; + newModifiers = modifiers.Replace( + modifiers[1], + modifiers[1].WithPrependedLeadingTrivia(asyncLeadingTrivia)); + newModifiers = newModifiers.RemoveAt(0); } else { - newModifiers = modifiers.RemoveAt(asyncTokenIndex); + // move it to the return type. + newModifiers = default; + newReturnType = newReturnType.WithPrependedLeadingTrivia(asyncLeadingTrivia); } - - return newModifiers; } + else + { + newModifiers = modifiers.RemoveAt(asyncTokenIndex); + } + + return newModifiers; } } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveConfusingSuppression/CSharpRemoveConfusingSuppressionCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveConfusingSuppression/CSharpRemoveConfusingSuppressionCodeFixProvider.cs index 5db3412e84e76..daab59954822b 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveConfusingSuppression/CSharpRemoveConfusingSuppressionCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveConfusingSuppression/CSharpRemoveConfusingSuppressionCodeFixProvider.cs @@ -18,91 +18,90 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.RemoveConfusingSuppression +namespace Microsoft.CodeAnalysis.CSharp.RemoveConfusingSuppression; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveConfusingSuppression), Shared] +internal sealed partial class CSharpRemoveConfusingSuppressionCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveConfusingSuppression), Shared] - internal sealed partial class CSharpRemoveConfusingSuppressionCodeFixProvider : CodeFixProvider + public const string RemoveOperator = nameof(RemoveOperator); + public const string NegateExpression = nameof(NegateExpression); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpRemoveConfusingSuppressionCodeFixProvider() { - public const string RemoveOperator = nameof(RemoveOperator); - public const string NegateExpression = nameof(NegateExpression); + } - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpRemoveConfusingSuppressionCodeFixProvider() - { - } + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.RemoveConfusingSuppressionForIsExpressionDiagnosticId]; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var diagnostics = context.Diagnostics; + var cancellationToken = context.CancellationToken; + + context.RegisterCodeFix( + CodeAction.Create( + CSharpAnalyzersResources.Remove_operator_preserves_semantics, + c => FixAllAsync(document, diagnostics, negate: false, c), + RemoveOperator), + context.Diagnostics); + + context.RegisterCodeFix( + CodeAction.Create( + CSharpAnalyzersResources.Negate_expression_changes_semantics, + c => FixAllAsync(document, diagnostics, negate: true, c), + NegateExpression), + context.Diagnostics); + + return Task.CompletedTask; + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.RemoveConfusingSuppressionForIsExpressionDiagnosticId]; + private static async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + bool negate, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var editor = new SyntaxEditor(root, document.Project.Solution.Services); + var generator = editor.Generator; + var generatorInternal = document.GetRequiredLanguageService(); - public override Task RegisterCodeFixesAsync(CodeFixContext context) + foreach (var diagnostic in diagnostics) { - var document = context.Document; - var diagnostics = context.Diagnostics; - var cancellationToken = context.CancellationToken; - - context.RegisterCodeFix( - CodeAction.Create( - CSharpAnalyzersResources.Remove_operator_preserves_semantics, - c => FixAllAsync(document, diagnostics, negate: false, c), - RemoveOperator), - context.Diagnostics); - - context.RegisterCodeFix( - CodeAction.Create( - CSharpAnalyzersResources.Negate_expression_changes_semantics, - c => FixAllAsync(document, diagnostics, negate: true, c), - NegateExpression), - context.Diagnostics); - - return Task.CompletedTask; - } + var node = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); + Debug.Assert(node.Kind() is SyntaxKind.IsExpression or SyntaxKind.IsPatternExpression); - private static async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - bool negate, CancellationToken cancellationToken) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var editor = new SyntaxEditor(root, document.Project.Solution.Services); - var generator = editor.Generator; - var generatorInternal = document.GetRequiredLanguageService(); + // Negate the result if requested. + var updatedNode = negate + ? generator.Negate(generatorInternal, node, semanticModel, cancellationToken) + : node; - foreach (var diagnostic in diagnostics) + var isNode = updatedNode.DescendantNodesAndSelf().First( + n => n.Kind() is SyntaxKind.IsExpression or SyntaxKind.IsPatternExpression); + var left = isNode switch { - var node = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); - Debug.Assert(node.Kind() is SyntaxKind.IsExpression or SyntaxKind.IsPatternExpression); - - // Negate the result if requested. - var updatedNode = negate - ? generator.Negate(generatorInternal, node, semanticModel, cancellationToken) - : node; - - var isNode = updatedNode.DescendantNodesAndSelf().First( - n => n.Kind() is SyntaxKind.IsExpression or SyntaxKind.IsPatternExpression); - var left = isNode switch - { - BinaryExpressionSyntax binary => binary.Left, - IsPatternExpressionSyntax isPattern => isPattern.Expression, - _ => throw ExceptionUtilities.UnexpectedValue(node), - }; - - // Remove the suppression operator. - var suppression = (PostfixUnaryExpressionSyntax)left; - var withoutSuppression = suppression.Operand.WithAppendedTrailingTrivia(suppression.OperatorToken.GetAllTrivia()); - var isWithoutSuppression = updatedNode.ReplaceNode(suppression, withoutSuppression); - - editor.ReplaceNode(node, isWithoutSuppression); - } - - return document.WithSyntaxRoot(editor.GetChangedRoot()); + BinaryExpressionSyntax binary => binary.Left, + IsPatternExpressionSyntax isPattern => isPattern.Expression, + _ => throw ExceptionUtilities.UnexpectedValue(node), + }; + + // Remove the suppression operator. + var suppression = (PostfixUnaryExpressionSyntax)left; + var withoutSuppression = suppression.Operand.WithAppendedTrailingTrivia(suppression.OperatorToken.GetAllTrivia()); + var isWithoutSuppression = updatedNode.ReplaceNode(suppression, withoutSuppression); + + editor.ReplaceNode(node, isWithoutSuppression); } - public override FixAllProvider GetFixAllProvider() - => FixAllProvider.Create(async (context, document, diagnostics) => - await FixAllAsync( - document, diagnostics, - context.CodeActionEquivalenceKey == NegateExpression, - context.CancellationToken).ConfigureAwait(false)); + return document.WithSyntaxRoot(editor.GetChangedRoot()); } + + public override FixAllProvider GetFixAllProvider() + => FixAllProvider.Create(async (context, document, diagnostics) => + await FixAllAsync( + document, diagnostics, + context.CodeActionEquivalenceKey == NegateExpression, + context.CancellationToken).ConfigureAwait(false)); } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveInKeyword/RemoveInKeywordCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveInKeyword/RemoveInKeywordCodeFixProvider.cs index 690a041b41efb..8d1d8c756f031 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveInKeyword/RemoveInKeywordCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveInKeyword/RemoveInKeywordCodeFixProvider.cs @@ -17,57 +17,56 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.RemoveInKeyword +namespace Microsoft.CodeAnalysis.CSharp.RemoveInKeyword; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveIn), Shared] +internal class RemoveInKeywordCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveIn), Shared] - internal class RemoveInKeywordCodeFixProvider : CodeFixProvider - { - private const string CS1615 = nameof(CS1615); // Argument 1 may not be passed with the 'in' keyword + private const string CS1615 = nameof(CS1615); // Argument 1 may not be passed with the 'in' keyword - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public RemoveInKeywordCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public RemoveInKeywordCodeFixProvider() + { + } - public override FixAllProvider GetFixAllProvider() - => WellKnownFixAllProviders.BatchFixer; + public override FixAllProvider GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; - public override ImmutableArray FixableDiagnosticIds => [CS1615]; + public override ImmutableArray FixableDiagnosticIds => [CS1615]; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var diagnostic = context.Diagnostics.First(); - var diagnosticSpan = diagnostic.Location.SourceSpan; + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; - var token = root.FindToken(diagnosticSpan.Start); + var token = root.FindToken(diagnosticSpan.Start); - var argumentSyntax = token.GetAncestor(); - if (argumentSyntax == null || argumentSyntax.GetRefKind() != RefKind.In) - return; + var argumentSyntax = token.GetAncestor(); + if (argumentSyntax == null || argumentSyntax.GetRefKind() != RefKind.In) + return; - context.RegisterCodeFix( - CodeAction.Create( - CSharpCodeFixesResources.Remove_in_keyword, - cancellationToken => FixAsync(context.Document, argumentSyntax, cancellationToken), - nameof(CSharpCodeFixesResources.Remove_in_keyword)), - context.Diagnostics); - } + context.RegisterCodeFix( + CodeAction.Create( + CSharpCodeFixesResources.Remove_in_keyword, + cancellationToken => FixAsync(context.Document, argumentSyntax, cancellationToken), + nameof(CSharpCodeFixesResources.Remove_in_keyword)), + context.Diagnostics); + } - private static async Task FixAsync( - Document document, - ArgumentSyntax argumentSyntax, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var generator = document.GetRequiredLanguageService(); - var syntaxFacts = document.GetRequiredLanguageService(); + private static async Task FixAsync( + Document document, + ArgumentSyntax argumentSyntax, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var generator = document.GetRequiredLanguageService(); + var syntaxFacts = document.GetRequiredLanguageService(); - return document.WithSyntaxRoot(root.ReplaceNode( - argumentSyntax, - generator.Argument(syntaxFacts.GetExpressionOfArgument(argumentSyntax)))); - } + return document.WithSyntaxRoot(root.ReplaceNode( + argumentSyntax, + generator.Argument(syntaxFacts.GetExpressionOfArgument(argumentSyntax)))); } } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveNewModifier/RemoveNewModifierCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveNewModifier/RemoveNewModifierCodeFixProvider.cs index 9092e7d622f42..6fb3710f7ea06 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveNewModifier/RemoveNewModifierCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveNewModifier/RemoveNewModifierCodeFixProvider.cs @@ -15,61 +15,60 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.RemoveNewModifier +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.RemoveNewModifier; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveNew), Shared] +internal class RemoveNewModifierCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveNew), Shared] - internal class RemoveNewModifierCodeFixProvider : CodeFixProvider - { - private const string CS0109 = nameof(CS0109); // The member 'SomeClass.SomeMember' does not hide an accessible member. The new keyword is not required. + private const string CS0109 = nameof(CS0109); // The member 'SomeClass.SomeMember' does not hide an accessible member. The new keyword is not required. - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public RemoveNewModifierCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public RemoveNewModifierCodeFixProvider() + { + } - public override FixAllProvider GetFixAllProvider() - => WellKnownFixAllProviders.BatchFixer; + public override FixAllProvider GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; - public override ImmutableArray FixableDiagnosticIds => [CS0109]; + public override ImmutableArray FixableDiagnosticIds => [CS0109]; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var diagnostic = context.Diagnostics.First(); - var diagnosticSpan = diagnostic.Location.SourceSpan; + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; - var token = root.FindToken(diagnosticSpan.Start); + var token = root.FindToken(diagnosticSpan.Start); - var memberDeclarationSyntax = token.GetAncestor(); - if (memberDeclarationSyntax == null) - return; + var memberDeclarationSyntax = token.GetAncestor(); + if (memberDeclarationSyntax == null) + return; - var generator = context.Document.GetRequiredLanguageService(); - if (!generator.GetModifiers(memberDeclarationSyntax).IsNew) - return; + var generator = context.Document.GetRequiredLanguageService(); + if (!generator.GetModifiers(memberDeclarationSyntax).IsNew) + return; - context.RegisterCodeFix( - CodeAction.Create( - CSharpCodeFixesResources.Remove_new_modifier, - ct => FixAsync(context.Document, generator, memberDeclarationSyntax, ct), - nameof(CSharpCodeFixesResources.Remove_new_modifier)), - context.Diagnostics); - } + context.RegisterCodeFix( + CodeAction.Create( + CSharpCodeFixesResources.Remove_new_modifier, + ct => FixAsync(context.Document, generator, memberDeclarationSyntax, ct), + nameof(CSharpCodeFixesResources.Remove_new_modifier)), + context.Diagnostics); + } - private static async Task FixAsync( - Document document, - SyntaxGenerator generator, - MemberDeclarationSyntax memberDeclaration, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + private static async Task FixAsync( + Document document, + SyntaxGenerator generator, + MemberDeclarationSyntax memberDeclaration, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - return document.WithSyntaxRoot(root.ReplaceNode( - memberDeclaration, - generator.WithModifiers( - memberDeclaration, generator.GetModifiers(memberDeclaration).WithIsNew(false)))); - } + return document.WithSyntaxRoot(root.ReplaceNode( + memberDeclaration, + generator.WithModifiers( + memberDeclaration, generator.GetModifiers(memberDeclaration).WithIsNew(false)))); } } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryCast/CSharpRemoveUnnecessaryCastCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryCast/CSharpRemoveUnnecessaryCastCodeFixProvider.cs index 01067ee202f6b..6be9261f0e31b 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryCast/CSharpRemoveUnnecessaryCastCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryCast/CSharpRemoveUnnecessaryCastCodeFixProvider.cs @@ -21,74 +21,73 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryCast +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryCast; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryCast), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.ImplementInterface)] +internal partial class CSharpRemoveUnnecessaryCastCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryCast), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.ImplementInterface)] - internal partial class CSharpRemoveUnnecessaryCastCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpRemoveUnnecessaryCastCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpRemoveUnnecessaryCastCodeFixProvider() - { - } + } - public sealed override ImmutableArray FixableDiagnosticIds { get; } = - [IDEDiagnosticIds.RemoveUnnecessaryCastDiagnosticId]; + public sealed override ImmutableArray FixableDiagnosticIds { get; } = + [IDEDiagnosticIds.RemoveUnnecessaryCastDiagnosticId]; - public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, AnalyzersResources.Remove_Unnecessary_Cast, nameof(AnalyzersResources.Remove_Unnecessary_Cast)); - return Task.CompletedTask; - } + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, AnalyzersResources.Remove_Unnecessary_Cast, nameof(AnalyzersResources.Remove_Unnecessary_Cast)); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var castNodes = diagnostics.SelectAsArray( - d => (ExpressionSyntax)d.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken)); + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var castNodes = diagnostics.SelectAsArray( + d => (ExpressionSyntax)d.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken)); - await editor.ApplyExpressionLevelSemanticEditsAsync( - document, castNodes, - (semanticModel, castExpression) => CastSimplifier.IsUnnecessaryCast(castExpression, semanticModel, cancellationToken), - (_, currentRoot, castExpression) => - { - var oldParent = castExpression.WalkUpParentheses(); - var newParent = Recurse(oldParent); + await editor.ApplyExpressionLevelSemanticEditsAsync( + document, castNodes, + (semanticModel, castExpression) => CastSimplifier.IsUnnecessaryCast(castExpression, semanticModel, cancellationToken), + (_, currentRoot, castExpression) => + { + var oldParent = castExpression.WalkUpParentheses(); + var newParent = Recurse(oldParent); - return currentRoot.ReplaceNode(oldParent, newParent); - }, - cancellationToken).ConfigureAwait(false); - } + return currentRoot.ReplaceNode(oldParent, newParent); + }, + cancellationToken).ConfigureAwait(false); + } - private static ExpressionSyntax Recurse(ExpressionSyntax old) + private static ExpressionSyntax Recurse(ExpressionSyntax old) + { + if (old is ParenthesizedExpressionSyntax parenthesizedExpression) { - if (old is ParenthesizedExpressionSyntax parenthesizedExpression) - { - // It's common in C# to have to write ((Goo)expr).Etc(). we don't just want to - // remove the cast and produce (expr).Etc(). So we mark all parent parenthesized - // expressions as worthy of simplification. The simplifier will remove these - // if possible, or leave them alone if not. - return parenthesizedExpression.ReplaceNode(parenthesizedExpression.Expression, Recurse(parenthesizedExpression.Expression)) - .WithAdditionalAnnotations(Simplifier.Annotation); - } - else if (old is CastExpressionSyntax castExpression) - { - // parenthesize the uncasted value to help ensure any proper parsing. The excess - // parens will be removed if unnecessary. - return castExpression.Uncast().WithAdditionalAnnotations(Formatter.Annotation) - .Parenthesize(); - } - else if (old is BinaryExpressionSyntax binaryExpression) - { - return binaryExpression.Left.WithTrailingTrivia(binaryExpression.GetTrailingTrivia()) - .WithAdditionalAnnotations(Simplifier.Annotation); - } - else - { - throw ExceptionUtilities.UnexpectedValue(old); - } + // It's common in C# to have to write ((Goo)expr).Etc(). we don't just want to + // remove the cast and produce (expr).Etc(). So we mark all parent parenthesized + // expressions as worthy of simplification. The simplifier will remove these + // if possible, or leave them alone if not. + return parenthesizedExpression.ReplaceNode(parenthesizedExpression.Expression, Recurse(parenthesizedExpression.Expression)) + .WithAdditionalAnnotations(Simplifier.Annotation); + } + else if (old is CastExpressionSyntax castExpression) + { + // parenthesize the uncasted value to help ensure any proper parsing. The excess + // parens will be removed if unnecessary. + return castExpression.Uncast().WithAdditionalAnnotations(Formatter.Annotation) + .Parenthesize(); + } + else if (old is BinaryExpressionSyntax binaryExpression) + { + return binaryExpression.Left.WithTrailingTrivia(binaryExpression.GetTrailingTrivia()) + .WithAdditionalAnnotations(Simplifier.Annotation); + } + else + { + throw ExceptionUtilities.UnexpectedValue(old); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryDiscardDesignation/CSharpRemoveUnnecessaryDiscardDesignationCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryDiscardDesignation/CSharpRemoveUnnecessaryDiscardDesignationCodeFixProvider.cs index c17a22f846788..334cfead66dc8 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryDiscardDesignation/CSharpRemoveUnnecessaryDiscardDesignationCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryDiscardDesignation/CSharpRemoveUnnecessaryDiscardDesignationCodeFixProvider.cs @@ -19,74 +19,73 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryDiscardDesignation +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryDiscardDesignation; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryDiscardDesignation), Shared] +internal partial class CSharpRemoveUnnecessaryDiscardDesignationCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryDiscardDesignation), Shared] - internal partial class CSharpRemoveUnnecessaryDiscardDesignationCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpRemoveUnnecessaryDiscardDesignationCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpRemoveUnnecessaryDiscardDesignationCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.RemoveUnnecessaryDiscardDesignationDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.RemoveUnnecessaryDiscardDesignationDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Remove_unnessary_discard, nameof(CSharpAnalyzersResources.Remove_unnessary_discard)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Remove_unnessary_discard, nameof(CSharpAnalyzersResources.Remove_unnessary_discard)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var generator = editor.Generator; + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var generator = editor.Generator; - foreach (var diagnostic in diagnostics) + foreach (var diagnostic in diagnostics) + { + var discard = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); + switch (discard.Parent) { - var discard = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - switch (discard.Parent) - { - case DeclarationPatternSyntax declarationPattern: - if (declarationPattern.Parent is IsPatternExpressionSyntax isPattern) - { - editor.ReplaceNode( - isPattern, - (current, _) => - { - var currentIsPattern = (IsPatternExpressionSyntax)current; - return SyntaxFactory.BinaryExpression( - SyntaxKind.IsExpression, - currentIsPattern.Expression, - currentIsPattern.IsKeyword, - ((DeclarationPatternSyntax)isPattern.Pattern).Type) - .WithAdditionalAnnotations(Formatter.Annotation); - }); - } - else - { - editor.ReplaceNode( - declarationPattern, - (current, _) => - SyntaxFactory.TypePattern(((DeclarationPatternSyntax)current).Type) - .WithAdditionalAnnotations(Formatter.Annotation)); - } - - break; - case RecursivePatternSyntax recursivePattern: + case DeclarationPatternSyntax declarationPattern: + if (declarationPattern.Parent is IsPatternExpressionSyntax isPattern) + { editor.ReplaceNode( - recursivePattern, + isPattern, (current, _) => - ((RecursivePatternSyntax)current).WithDesignation(null) - .WithAdditionalAnnotations(Formatter.Annotation)); - break; - } - } + { + var currentIsPattern = (IsPatternExpressionSyntax)current; + return SyntaxFactory.BinaryExpression( + SyntaxKind.IsExpression, + currentIsPattern.Expression, + currentIsPattern.IsKeyword, + ((DeclarationPatternSyntax)isPattern.Pattern).Type) + .WithAdditionalAnnotations(Formatter.Annotation); + }); + } + else + { + editor.ReplaceNode( + declarationPattern, + (current, _) => + SyntaxFactory.TypePattern(((DeclarationPatternSyntax)current).Type) + .WithAdditionalAnnotations(Formatter.Annotation)); + } - return Task.CompletedTask; + break; + case RecursivePatternSyntax recursivePattern: + editor.ReplaceNode( + recursivePattern, + (current, _) => + ((RecursivePatternSyntax)current).WithDesignation(null) + .WithAdditionalAnnotations(Formatter.Annotation)); + break; + } } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsCodeFixProvider.cs index 38ee7beeb961b..e1e0760dbc711 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsCodeFixProvider.cs @@ -11,22 +11,21 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.RemoveUnnecessaryImports; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryImports +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryImports; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryImports), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.AddMissingReference)] +internal class CSharpRemoveUnnecessaryImportsCodeFixProvider : AbstractRemoveUnnecessaryImportsCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryImports), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.AddMissingReference)] - internal class CSharpRemoveUnnecessaryImportsCodeFixProvider : AbstractRemoveUnnecessaryImportsCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpRemoveUnnecessaryImportsCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpRemoveUnnecessaryImportsCodeFixProvider() - { - } + } - protected override string GetTitle() - => CSharpCodeFixesResources.Remove_unnecessary_usings; + protected override string GetTitle() + => CSharpCodeFixesResources.Remove_unnecessary_usings; - protected override ISyntaxFormatting GetSyntaxFormatting() - => CSharpSyntaxFormatting.Instance; - } + protected override ISyntaxFormatting GetSyntaxFormatting() + => CSharpSyntaxFormatting.Instance; } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryLambdaExpression/CSharpRemoveUnnecessaryLambdaExpressionCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryLambdaExpression/CSharpRemoveUnnecessaryLambdaExpressionCodeFixProvider.cs index 43a7196915637..fc63163451ef6 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryLambdaExpression/CSharpRemoveUnnecessaryLambdaExpressionCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryLambdaExpression/CSharpRemoveUnnecessaryLambdaExpressionCodeFixProvider.cs @@ -18,50 +18,49 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryLambdaExpression -{ - using static CSharpRemoveUnnecessaryLambdaExpressionDiagnosticAnalyzer; +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryLambdaExpression; + +using static CSharpRemoveUnnecessaryLambdaExpressionDiagnosticAnalyzer; - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryLambdaExpression), Shared] - internal partial class CSharpRemoveUnnecessaryLambdaExpressionCodeFixProvider : SyntaxEditorBasedCodeFixProvider +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryLambdaExpression), Shared] +internal partial class CSharpRemoveUnnecessaryLambdaExpressionCodeFixProvider : SyntaxEditorBasedCodeFixProvider +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpRemoveUnnecessaryLambdaExpressionCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpRemoveUnnecessaryLambdaExpressionCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.RemoveUnnecessaryLambdaExpressionDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.RemoveUnnecessaryLambdaExpressionDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Remove_unnecessary_lambda_expression, nameof(CSharpAnalyzersResources.Remove_unnecessary_lambda_expression)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Remove_unnecessary_lambda_expression, nameof(CSharpAnalyzersResources.Remove_unnecessary_lambda_expression)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) { - foreach (var diagnostic in diagnostics) - { - var anonymousFunction = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); + var anonymousFunction = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); - editor.ReplaceNode(anonymousFunction, - (current, generator) => + editor.ReplaceNode(anonymousFunction, + (current, generator) => + { + if (current is AnonymousFunctionExpressionSyntax anonymousFunction && + TryGetAnonymousFunctionInvocation(anonymousFunction, out var invocation, out _)) { - if (current is AnonymousFunctionExpressionSyntax anonymousFunction && - TryGetAnonymousFunctionInvocation(anonymousFunction, out var invocation, out _)) - { - return invocation.Expression.WithTriviaFrom(current).Parenthesize(); - } - - return current; - }); - } + return invocation.Expression.WithTriviaFrom(current).Parenthesize(); + } - return Task.CompletedTask; + return current; + }); } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveCodeFixProvider.cs index 1f5780bd27a77..7b21ee94e9510 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveCodeFixProvider.cs @@ -5,61 +5,110 @@ using System; using System.Collections.Immutable; using System.Composition; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.RemoveUnnecessaryNullableDirective +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.RemoveUnnecessaryNullableDirective; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryNullableDirective)] +[Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpRemoveUnnecessaryNullableDirectiveCodeFixProvider() + : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryNullableDirective)] - [Shared] - internal sealed class CSharpRemoveUnnecessaryNullableDirectiveCodeFixProvider - : SyntaxEditorBasedCodeFixProvider + public override ImmutableArray FixableDiagnosticIds + => [ + IDEDiagnosticIds.RemoveRedundantNullableDirectiveDiagnosticId, + IDEDiagnosticIds.RemoveUnnecessaryNullableDirectiveDiagnosticId, + ]; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpRemoveUnnecessaryNullableDirectiveCodeFixProvider() + foreach (var diagnostic in context.Diagnostics) { + if (diagnostic.Id == IDEDiagnosticIds.RemoveRedundantNullableDirectiveDiagnosticId) + RegisterCodeFix(context, CSharpAnalyzersResources.Remove_redundant_nullable_directive, nameof(CSharpAnalyzersResources.Remove_redundant_nullable_directive), diagnostic); + else + RegisterCodeFix(context, CSharpAnalyzersResources.Remove_unnecessary_nullable_directive, nameof(CSharpAnalyzersResources.Remove_unnecessary_nullable_directive), diagnostic); } - public override ImmutableArray FixableDiagnosticIds - => [ - IDEDiagnosticIds.RemoveRedundantNullableDirectiveDiagnosticId, - IDEDiagnosticIds.RemoveUnnecessaryNullableDirectiveDiagnosticId, - ]; + return Task.CompletedTask; + } + + protected override Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + // We first group the nullable directives by the token they are attached This allows to replace each token + // separately even if they have multiple nullable directives. + var nullableDirectives = diagnostics + .Select(d => d.Location.FindNode(findInsideTrivia: true, getInnermostNodeForTie: true, cancellationToken)) + .OfType(); - public override Task RegisterCodeFixesAsync(CodeFixContext context) + foreach (var (token, directives) in nullableDirectives.GroupBy(d => d.ParentTrivia.Token)) { - foreach (var diagnostic in context.Diagnostics) + var leadingTrivia = token.LeadingTrivia; + var nullableDirectiveIndices = nullableDirectives + .Select(x => leadingTrivia.IndexOf(x.ParentTrivia)); + + // Walk backwards through the nullable directives on the token. That way our indices stay correct as we + // are removing later directives. + foreach (var index in nullableDirectiveIndices.OrderByDescending(x => x)) { - if (diagnostic.Id == IDEDiagnosticIds.RemoveRedundantNullableDirectiveDiagnosticId) - RegisterCodeFix(context, CSharpAnalyzersResources.Remove_redundant_nullable_directive, nameof(CSharpAnalyzersResources.Remove_redundant_nullable_directive), diagnostic); - else - RegisterCodeFix(context, CSharpAnalyzersResources.Remove_unnecessary_nullable_directive, nameof(CSharpAnalyzersResources.Remove_unnecessary_nullable_directive), diagnostic); - } + // Remove the directive itself. + leadingTrivia = leadingTrivia.RemoveAt(index); - return Task.CompletedTask; - } + // If we have a blank line both before and after the directive, then remove the one that follows to + // keep the code clean. + if (HasPrecedingBlankLine(leadingTrivia, index - 1) && + HasFollowingBlankLine(leadingTrivia, index)) + { + // Delete optional following whitespace. + if (leadingTrivia[index].IsWhitespace()) + leadingTrivia = leadingTrivia.RemoveAt(index); - protected override Task FixAllAsync( - Document document, - ImmutableArray diagnostics, - SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - foreach (var diagnostic in diagnostics) - { - var nullableDirective = diagnostic.Location.FindNode(findInsideTrivia: true, getInnermostNodeForTie: true, cancellationToken); - editor.RemoveNode(nullableDirective, SyntaxRemoveOptions.KeepNoTrivia); + // Then the following blank line. + leadingTrivia = leadingTrivia.RemoveAt(index); + } } - return Task.CompletedTask; + // Update the token and replace it within its parent. + var node = token.GetRequiredParent(); + editor.ReplaceNode( + node, + node.ReplaceToken(token, token.WithLeadingTrivia(leadingTrivia))); } + + return Task.CompletedTask; + } + + private static bool HasPrecedingBlankLine(SyntaxTriviaList leadingTrivia, int index) + { + if (index >= 0 && leadingTrivia[index].IsWhitespace()) + index--; + + return index >= 0 && leadingTrivia[index].IsEndOfLine(); + } + + private static bool HasFollowingBlankLine(SyntaxTriviaList leadingTrivia, int index) + { + if (index < leadingTrivia.Count && leadingTrivia[index].IsWhitespace()) + index++; + + return index < leadingTrivia.Count && leadingTrivia[index].IsEndOfLine(); } } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryParenthesesCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryParenthesesCodeFixProvider.cs index 0912edfa4b341..204c4c17a38f3 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryParenthesesCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveUnnecessaryParentheses/CSharpRemoveUnnecessaryParenthesesCodeFixProvider.cs @@ -11,24 +11,23 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryParentheses +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryParentheses; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryParentheses), Shared] +internal class CSharpRemoveUnnecessaryParenthesesCodeFixProvider : + AbstractRemoveUnnecessaryParenthesesCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryParentheses), Shared] - internal class CSharpRemoveUnnecessaryParenthesesCodeFixProvider : - AbstractRemoveUnnecessaryParenthesesCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpRemoveUnnecessaryParenthesesCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpRemoveUnnecessaryParenthesesCodeFixProvider() - { - } - - protected override bool CanRemoveParentheses(SyntaxNode current, SemanticModel semanticModel, CancellationToken cancellationToken) - => current switch - { - ParenthesizedExpressionSyntax p => CSharpRemoveUnnecessaryExpressionParenthesesDiagnosticAnalyzer.CanRemoveParenthesesHelper(p, semanticModel, cancellationToken, out _, out _), - ParenthesizedPatternSyntax p => CSharpRemoveUnnecessaryPatternParenthesesDiagnosticAnalyzer.CanRemoveParenthesesHelper(p, out _, out _), - _ => false, - }; } + + protected override bool CanRemoveParentheses(SyntaxNode current, SemanticModel semanticModel, CancellationToken cancellationToken) + => current switch + { + ParenthesizedExpressionSyntax p => CSharpRemoveUnnecessaryExpressionParenthesesDiagnosticAnalyzer.CanRemoveParenthesesHelper(p, semanticModel, cancellationToken, out _, out _), + ParenthesizedPatternSyntax p => CSharpRemoveUnnecessaryPatternParenthesesDiagnosticAnalyzer.CanRemoveParenthesesHelper(p, out _, out _), + _ => false, + }; } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveUnusedLocalFunction/CSharpRemoveUnusedLocalFunctionCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveUnusedLocalFunction/CSharpRemoveUnusedLocalFunctionCodeFixProvider.cs index d5879ee9fc8e2..134f003d50746 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveUnusedLocalFunction/CSharpRemoveUnusedLocalFunctionCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveUnusedLocalFunction/CSharpRemoveUnusedLocalFunctionCodeFixProvider.cs @@ -15,53 +15,52 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedLocalFunction +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedLocalFunction; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnusedLocalFunction), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)] +internal class CSharpRemoveUnusedLocalFunctionCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnusedLocalFunction), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)] - internal class CSharpRemoveUnusedLocalFunctionCodeFixProvider : SyntaxEditorBasedCodeFixProvider - { - private const string CS8321 = nameof(CS8321); // The local function 'X' is declared but never used + private const string CS8321 = nameof(CS8321); // The local function 'X' is declared but never used - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpRemoveUnusedLocalFunctionCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpRemoveUnusedLocalFunctionCodeFixProvider() + { + } - public sealed override ImmutableArray FixableDiagnosticIds - => [CS8321]; + public sealed override ImmutableArray FixableDiagnosticIds + => [CS8321]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - context.RegisterCodeFix( - CodeAction.Create( - CSharpCodeFixesResources.Remove_unused_function, - GetDocumentUpdater(context), - nameof(CSharpCodeFixesResources.Remove_unused_function)), - context.Diagnostics); - return Task.CompletedTask; - } - - protected override Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var root = editor.OriginalRoot; + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + context.RegisterCodeFix( + CodeAction.Create( + CSharpCodeFixesResources.Remove_unused_function, + GetDocumentUpdater(context), + nameof(CSharpCodeFixesResources.Remove_unused_function)), + context.Diagnostics); + return Task.CompletedTask; + } - // Order diagnostics in reverse (from latest in file to earliest) so that we process - // all inner local functions before processing outer local functions. If we don't - // do this, then SyntaxEditor will fail if it tries to remove an inner local function - // after already removing the outer one. - var localFunctions = diagnostics.OrderBy(static (d1, d2) => d2.Location.SourceSpan.Start - d1.Location.SourceSpan.Start) - .Select(d => root.FindToken(d.Location.SourceSpan.Start)) - .Select(t => t.GetAncestor()); + protected override Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var root = editor.OriginalRoot; - foreach (var localFunction in localFunctions) - { - if (localFunction != null) - editor.RemoveNode(localFunction.Parent is GlobalStatementSyntax globalStatement ? globalStatement : localFunction); - } + // Order diagnostics in reverse (from latest in file to earliest) so that we process + // all inner local functions before processing outer local functions. If we don't + // do this, then SyntaxEditor will fail if it tries to remove an inner local function + // after already removing the outer one. + var localFunctions = diagnostics.OrderBy(static (d1, d2) => d2.Location.SourceSpan.Start - d1.Location.SourceSpan.Start) + .Select(d => root.FindToken(d.Location.SourceSpan.Start)) + .Select(t => t.GetAncestor()); - return Task.CompletedTask; + foreach (var localFunction in localFunctions) + { + if (localFunction != null) + editor.RemoveNode(localFunction.Parent is GlobalStatementSyntax globalStatement ? globalStatement : localFunction); } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveUnusedMembers/CSharpRemoveUnusedMembersCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveUnusedMembers/CSharpRemoveUnusedMembersCodeFixProvider.cs index 29b238265c1d1..a9d4d39ed68d7 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveUnusedMembers/CSharpRemoveUnusedMembersCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveUnusedMembers/CSharpRemoveUnusedMembersCodeFixProvider.cs @@ -12,29 +12,28 @@ using System; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedMembers +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedMembers; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnusedMembers), Shared] +internal class CSharpRemoveUnusedMembersCodeFixProvider : AbstractRemoveUnusedMembersCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnusedMembers), Shared] - internal class CSharpRemoveUnusedMembersCodeFixProvider : AbstractRemoveUnusedMembersCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpRemoveUnusedMembersCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpRemoveUnusedMembersCodeFixProvider() - { - } + } - /// - /// This method adjusts the to remove based on whether or not all variable declarators - /// within a field declaration should be removed, - /// i.e. if all the fields declared within a field declaration are unused, - /// we can remove the entire field declaration instead of individual variable declarators. - /// - protected override void AdjustAndAddAppropriateDeclaratorsToRemove(HashSet fieldDeclarators, HashSet declarators) + /// + /// This method adjusts the to remove based on whether or not all variable declarators + /// within a field declaration should be removed, + /// i.e. if all the fields declared within a field declaration are unused, + /// we can remove the entire field declaration instead of individual variable declarators. + /// + protected override void AdjustAndAddAppropriateDeclaratorsToRemove(HashSet fieldDeclarators, HashSet declarators) + { + foreach (var fieldDeclarator in fieldDeclarators) { - foreach (var fieldDeclarator in fieldDeclarators) - { - AdjustAndAddAppropriateDeclaratorsToRemove(fieldDeclarator, fieldDeclarator.Declaration.Variables, declarators); - } + AdjustAndAddAppropriateDeclaratorsToRemove(fieldDeclarator, fieldDeclarator.Declaration.Variables, declarators); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/RemoveUnusedParametersAndValues/CSharpRemoveUnusedValuesCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/RemoveUnusedParametersAndValues/CSharpRemoveUnusedValuesCodeFixProvider.cs index 804fbabf917c4..703e7c6d2210d 100644 --- a/src/Analyzers/CSharp/CodeFixes/RemoveUnusedParametersAndValues/CSharpRemoveUnusedValuesCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/RemoveUnusedParametersAndValues/CSharpRemoveUnusedValuesCodeFixProvider.cs @@ -19,245 +19,244 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedParametersAndValues +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedParametersAndValues; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnusedValues), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)] +internal class CSharpRemoveUnusedValuesCodeFixProvider : + AbstractRemoveUnusedValuesCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnusedValues), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)] - internal class CSharpRemoveUnusedValuesCodeFixProvider : - AbstractRemoveUnusedValuesCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpRemoveUnusedValuesCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpRemoveUnusedValuesCodeFixProvider() - { - } - - protected override ISyntaxFormatting GetSyntaxFormatting() - => CSharpSyntaxFormatting.Instance; + } - protected override BlockSyntax WrapWithBlockIfNecessary(IEnumerable statements) - => SyntaxFactory.Block(statements); + protected override ISyntaxFormatting GetSyntaxFormatting() + => CSharpSyntaxFormatting.Instance; - protected override SyntaxToken GetForEachStatementIdentifier(ForEachStatementSyntax node) - => node.Identifier; + protected override BlockSyntax WrapWithBlockIfNecessary(IEnumerable statements) + => SyntaxFactory.Block(statements); - protected override LocalDeclarationStatementSyntax GetCandidateLocalDeclarationForRemoval(VariableDeclaratorSyntax declarator) - => declarator.Parent?.Parent as LocalDeclarationStatementSyntax; + protected override SyntaxToken GetForEachStatementIdentifier(ForEachStatementSyntax node) + => node.Identifier; - protected override SyntaxNode TryUpdateNameForFlaggedNode(SyntaxNode node, SyntaxToken newName) - { - switch (node.Kind()) - { - case SyntaxKind.IdentifierName: - var identifierName = (IdentifierNameSyntax)node; - return identifierName.WithIdentifier(newName.WithTriviaFrom(identifierName.Identifier)); - - case SyntaxKind.VariableDeclarator: - var variableDeclarator = (VariableDeclaratorSyntax)node; - if (newName.ValueText == AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName && - variableDeclarator.Initializer?.Value is ImplicitObjectCreationExpressionSyntax implicitObjectCreation && - variableDeclarator.Parent is VariableDeclarationSyntax parent) - { - // If we are generating a discard on the left of an initialization with an implicit object creation on the right, - // then we need to replace the implicit object creation with an explicit one. - // For example: 'TypeName v = new();' must be changed to '_ = new TypeName();' - var objectCreationNode = SyntaxFactory.ObjectCreationExpression( - newKeyword: implicitObjectCreation.NewKeyword, - type: parent.Type, - argumentList: implicitObjectCreation.ArgumentList, - initializer: implicitObjectCreation.Initializer); - variableDeclarator = variableDeclarator.WithInitializer(variableDeclarator.Initializer.WithValue(objectCreationNode)); - } - - return variableDeclarator.WithIdentifier(newName.WithTriviaFrom(variableDeclarator.Identifier)); - - case SyntaxKind.SingleVariableDesignation: - return newName.ValueText == AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName - ? SyntaxFactory.DiscardDesignation().WithTriviaFrom(node) - : SyntaxFactory.SingleVariableDesignation(newName).WithTriviaFrom(node); - - case SyntaxKind.CatchDeclaration: - var catchDeclaration = (CatchDeclarationSyntax)node; - return catchDeclaration.WithIdentifier(newName.WithTriviaFrom(catchDeclaration.Identifier)); - - case SyntaxKind.VarPattern: - return node.IsParentKind(SyntaxKind.Subpattern) - ? SyntaxFactory.DiscardPattern().WithTriviaFrom(node) - : SyntaxFactory.DiscardDesignation(); - - default: - Debug.Fail($"Unexpected node kind for local/parameter declaration or reference: '{node.Kind()}'"); - return null; - } - } + protected override LocalDeclarationStatementSyntax GetCandidateLocalDeclarationForRemoval(VariableDeclaratorSyntax declarator) + => declarator.Parent?.Parent as LocalDeclarationStatementSyntax; - protected override SyntaxNode TryUpdateParentOfUpdatedNode(SyntaxNode parent, SyntaxNode newNameNode, SyntaxEditor editor, ISyntaxFacts syntaxFacts, SemanticModel semanticModel) + protected override SyntaxNode TryUpdateNameForFlaggedNode(SyntaxNode node, SyntaxToken newName) + { + switch (node.Kind()) { - if (newNameNode.IsKind(SyntaxKind.DiscardDesignation)) - { - var triviaToAppend = newNameNode.GetLeadingTrivia().AddRange(newNameNode.GetTrailingTrivia()); + case SyntaxKind.IdentifierName: + var identifierName = (IdentifierNameSyntax)node; + return identifierName.WithIdentifier(newName.WithTriviaFrom(identifierName.Identifier)); - // 1) `... is MyType variable` -> `... is MyType` - // 2) `... is MyType /*1*/ variable /*2*/` -> `... is MyType /*1*/ /*2*/` - if (parent is DeclarationPatternSyntax declarationPattern && - parent.SyntaxTree.Options.LanguageVersion() >= LanguageVersion.CSharp9) + case SyntaxKind.VariableDeclarator: + var variableDeclarator = (VariableDeclaratorSyntax)node; + if (newName.ValueText == AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName && + variableDeclarator.Initializer?.Value is ImplicitObjectCreationExpressionSyntax implicitObjectCreation && + variableDeclarator.Parent is VariableDeclarationSyntax parent) { - var trailingTrivia = declarationPattern.Type.GetTrailingTrivia().AddRange(triviaToAppend); - return SyntaxFactory.TypePattern(declarationPattern.Type).WithTrailingTrivia(trailingTrivia); + // If we are generating a discard on the left of an initialization with an implicit object creation on the right, + // then we need to replace the implicit object creation with an explicit one. + // For example: 'TypeName v = new();' must be changed to '_ = new TypeName();' + var objectCreationNode = SyntaxFactory.ObjectCreationExpression( + newKeyword: implicitObjectCreation.NewKeyword, + type: parent.Type, + argumentList: implicitObjectCreation.ArgumentList, + initializer: implicitObjectCreation.Initializer); + variableDeclarator = variableDeclarator.WithInitializer(variableDeclarator.Initializer.WithValue(objectCreationNode)); } - // 1) `... is { } variable` -> `... is { }` - // 2) `... is { } /*1*/ variable /*2*/` -> `... is { } /*1*/ /*2*/` - if (parent is RecursivePatternSyntax recursivePattern) - { - var withoutDesignation = recursivePattern.WithDesignation(null); - return withoutDesignation.WithAppendedTrailingTrivia(triviaToAppend); - } + return variableDeclarator.WithIdentifier(newName.WithTriviaFrom(variableDeclarator.Identifier)); - // 1) `... is [] variable` -> `... is []` - // 2) `... is [] /*1*/ variable /*2*/` -> `... is [] /*1*/ /*2*/` - if (parent is ListPatternSyntax listPattern) - { - var withoutDesignation = listPattern.WithDesignation(null); - return withoutDesignation.WithAppendedTrailingTrivia(triviaToAppend); - } - } - else if (parent is AssignmentExpressionSyntax assignment && - assignment.Right is ImplicitObjectCreationExpressionSyntax implicitObjectCreation && - newNameNode is IdentifierNameSyntax { Identifier.ValueText: AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName } && - semanticModel.GetTypeInfo(implicitObjectCreation).Type is { } type) - { - // If we are generating a discard on the left of an assignment with an implicit object creation on the right, - // then we need to replace the implicit object creation with an explicit one. - // For example: 'v = new();' must be changed to '_ = new TypeOfV();' - var objectCreationNode = SyntaxFactory.ObjectCreationExpression( - newKeyword: implicitObjectCreation.NewKeyword, - type: type.GenerateTypeSyntax(allowVar: false), - argumentList: implicitObjectCreation.ArgumentList, - initializer: implicitObjectCreation.Initializer); - return assignment.Update((ExpressionSyntax)newNameNode, assignment.OperatorToken, objectCreationNode); - } + case SyntaxKind.SingleVariableDesignation: + return newName.ValueText == AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName + ? SyntaxFactory.DiscardDesignation().WithTriviaFrom(node) + : SyntaxFactory.SingleVariableDesignation(newName).WithTriviaFrom(node); - return null; + case SyntaxKind.CatchDeclaration: + var catchDeclaration = (CatchDeclarationSyntax)node; + return catchDeclaration.WithIdentifier(newName.WithTriviaFrom(catchDeclaration.Identifier)); + + case SyntaxKind.VarPattern: + return node.IsParentKind(SyntaxKind.Subpattern) + ? SyntaxFactory.DiscardPattern().WithTriviaFrom(node) + : SyntaxFactory.DiscardDesignation(); + + default: + Debug.Fail($"Unexpected node kind for local/parameter declaration or reference: '{node.Kind()}'"); + return null; } + } - protected override SyntaxNode ComputeReplacementNode(SyntaxNode originalOldNode, SyntaxNode changedOldNode, SyntaxNode proposedReplacementNode) + protected override SyntaxNode TryUpdateParentOfUpdatedNode(SyntaxNode parent, SyntaxNode newNameNode, SyntaxEditor editor, ISyntaxFacts syntaxFacts, SemanticModel semanticModel) + { + if (newNameNode.IsKind(SyntaxKind.DiscardDesignation)) { - // Check for the following change: `{ ... } variable` -> `{ ... }` - // Since the internals of this patterns might be changed during previous iterations - // we apply the same change (remove `variable` declaration) to the `changedOldNode` - if (originalOldNode is RecursivePatternSyntax originalOldRecursivePattern && - proposedReplacementNode is RecursivePatternSyntax proposedReplacementRecursivePattern && - proposedReplacementRecursivePattern.IsEquivalentTo(originalOldRecursivePattern.WithDesignation(null))) + var triviaToAppend = newNameNode.GetLeadingTrivia().AddRange(newNameNode.GetTrailingTrivia()); + + // 1) `... is MyType variable` -> `... is MyType` + // 2) `... is MyType /*1*/ variable /*2*/` -> `... is MyType /*1*/ /*2*/` + if (parent is DeclarationPatternSyntax declarationPattern && + parent.SyntaxTree.Options.LanguageVersion() >= LanguageVersion.CSharp9) { - proposedReplacementNode = ((RecursivePatternSyntax)changedOldNode).WithDesignation(null) - .WithTrailingTrivia(proposedReplacementNode.GetTrailingTrivia()); + var trailingTrivia = declarationPattern.Type.GetTrailingTrivia().AddRange(triviaToAppend); + return SyntaxFactory.TypePattern(declarationPattern.Type).WithTrailingTrivia(trailingTrivia); } - // Check for the following change: `[...] variable` -> `[...]` - // Since the internals of this patterns might be changed during previous iterations - // we apply the same change (remove `variable` declaration) to the `changedOldNode` - if (originalOldNode is ListPatternSyntax originalOldListPattern && - proposedReplacementNode is ListPatternSyntax proposedReplacementListPattern && - proposedReplacementListPattern.IsEquivalentTo(originalOldListPattern.WithDesignation(null))) + // 1) `... is { } variable` -> `... is { }` + // 2) `... is { } /*1*/ variable /*2*/` -> `... is { } /*1*/ /*2*/` + if (parent is RecursivePatternSyntax recursivePattern) { - proposedReplacementNode = ((ListPatternSyntax)changedOldNode).WithDesignation(null) - .WithTrailingTrivia(proposedReplacementNode.GetTrailingTrivia()); + var withoutDesignation = recursivePattern.WithDesignation(null); + return withoutDesignation.WithAppendedTrailingTrivia(triviaToAppend); } - return proposedReplacementNode.WithAdditionalAnnotations(Formatter.Annotation); + // 1) `... is [] variable` -> `... is []` + // 2) `... is [] /*1*/ variable /*2*/` -> `... is [] /*1*/ /*2*/` + if (parent is ListPatternSyntax listPattern) + { + var withoutDesignation = listPattern.WithDesignation(null); + return withoutDesignation.WithAppendedTrailingTrivia(triviaToAppend); + } + } + else if (parent is AssignmentExpressionSyntax assignment && + assignment.Right is ImplicitObjectCreationExpressionSyntax implicitObjectCreation && + newNameNode is IdentifierNameSyntax { Identifier.ValueText: AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName } && + semanticModel.GetTypeInfo(implicitObjectCreation).Type is { } type) + { + // If we are generating a discard on the left of an assignment with an implicit object creation on the right, + // then we need to replace the implicit object creation with an explicit one. + // For example: 'v = new();' must be changed to '_ = new TypeOfV();' + var objectCreationNode = SyntaxFactory.ObjectCreationExpression( + newKeyword: implicitObjectCreation.NewKeyword, + type: type.GenerateTypeSyntax(allowVar: false), + argumentList: implicitObjectCreation.ArgumentList, + initializer: implicitObjectCreation.Initializer); + return assignment.Update((ExpressionSyntax)newNameNode, assignment.OperatorToken, objectCreationNode); } - protected override void InsertAtStartOfSwitchCaseBlockForDeclarationInCaseLabelOrClause(SwitchSectionSyntax switchCaseBlock, SyntaxEditor editor, LocalDeclarationStatementSyntax declarationStatement) + return null; + } + + protected override SyntaxNode ComputeReplacementNode(SyntaxNode originalOldNode, SyntaxNode changedOldNode, SyntaxNode proposedReplacementNode) + { + // Check for the following change: `{ ... } variable` -> `{ ... }` + // Since the internals of this patterns might be changed during previous iterations + // we apply the same change (remove `variable` declaration) to the `changedOldNode` + if (originalOldNode is RecursivePatternSyntax originalOldRecursivePattern && + proposedReplacementNode is RecursivePatternSyntax proposedReplacementRecursivePattern && + proposedReplacementRecursivePattern.IsEquivalentTo(originalOldRecursivePattern.WithDesignation(null))) { - var firstStatement = switchCaseBlock.Statements.FirstOrDefault(); - if (firstStatement != null) - { - editor.InsertBefore(firstStatement, declarationStatement); - } - else - { - // Switch section without any statements is an error case. - // Insert before containing switch statement. - editor.InsertBefore(switchCaseBlock.Parent, declarationStatement); - } + proposedReplacementNode = ((RecursivePatternSyntax)changedOldNode).WithDesignation(null) + .WithTrailingTrivia(proposedReplacementNode.GetTrailingTrivia()); } - protected override SyntaxNode GetReplacementNodeForCompoundAssignment( - SyntaxNode originalCompoundAssignment, - SyntaxNode newAssignmentTarget, - SyntaxEditor editor, - ISyntaxFactsService syntaxFacts) + // Check for the following change: `[...] variable` -> `[...]` + // Since the internals of this patterns might be changed during previous iterations + // we apply the same change (remove `variable` declaration) to the `changedOldNode` + if (originalOldNode is ListPatternSyntax originalOldListPattern && + proposedReplacementNode is ListPatternSyntax proposedReplacementListPattern && + proposedReplacementListPattern.IsEquivalentTo(originalOldListPattern.WithDesignation(null))) { - // 1. Compound assignment is changed to simple assignment. - // For example, "x += MethodCall();", where assignment to 'x' is redundant - // is replaced with "_ = MethodCall();" or "var unused = MethodCall(); - // - // 2. Null coalesce assignment is changed to assignment with null coalesce - // expression on the right. - // For example, "x ??= MethodCall();", where assignment to 'x' is redundant - // is replaced with "_ = x ?? MethodCall();" or "var unused = x ?? MethodCall(); - // - // 3. However, if the node is not parented by an expression statement then we - // don't generate an assignment, but just the expression. - // For example, "return x += MethodCall();" is replaced with "return x + MethodCall();" - // and "return x ??= MethodCall();" is replaced with "return x ?? MethodCall();" - - if (originalCompoundAssignment is not AssignmentExpressionSyntax assignmentExpression) - { - Debug.Fail($"Unexpected kind for originalCompoundAssignment: {originalCompoundAssignment.Kind()}"); - return originalCompoundAssignment; - } + proposedReplacementNode = ((ListPatternSyntax)changedOldNode).WithDesignation(null) + .WithTrailingTrivia(proposedReplacementNode.GetTrailingTrivia()); + } + + return proposedReplacementNode.WithAdditionalAnnotations(Formatter.Annotation); + } + + protected override void InsertAtStartOfSwitchCaseBlockForDeclarationInCaseLabelOrClause(SwitchSectionSyntax switchCaseBlock, SyntaxEditor editor, LocalDeclarationStatementSyntax declarationStatement) + { + var firstStatement = switchCaseBlock.Statements.FirstOrDefault(); + if (firstStatement != null) + { + editor.InsertBefore(firstStatement, declarationStatement); + } + else + { + // Switch section without any statements is an error case. + // Insert before containing switch statement. + editor.InsertBefore(switchCaseBlock.Parent, declarationStatement); + } + } - var leftOfAssignment = assignmentExpression.Left; - var rightOfAssignment = assignmentExpression.Right; + protected override SyntaxNode GetReplacementNodeForCompoundAssignment( + SyntaxNode originalCompoundAssignment, + SyntaxNode newAssignmentTarget, + SyntaxEditor editor, + ISyntaxFactsService syntaxFacts) + { + // 1. Compound assignment is changed to simple assignment. + // For example, "x += MethodCall();", where assignment to 'x' is redundant + // is replaced with "_ = MethodCall();" or "var unused = MethodCall(); + // + // 2. Null coalesce assignment is changed to assignment with null coalesce + // expression on the right. + // For example, "x ??= MethodCall();", where assignment to 'x' is redundant + // is replaced with "_ = x ?? MethodCall();" or "var unused = x ?? MethodCall(); + // + // 3. However, if the node is not parented by an expression statement then we + // don't generate an assignment, but just the expression. + // For example, "return x += MethodCall();" is replaced with "return x + MethodCall();" + // and "return x ??= MethodCall();" is replaced with "return x ?? MethodCall();" - if (originalCompoundAssignment.Parent.IsKind(SyntaxKind.ExpressionStatement)) + if (originalCompoundAssignment is not AssignmentExpressionSyntax assignmentExpression) + { + Debug.Fail($"Unexpected kind for originalCompoundAssignment: {originalCompoundAssignment.Kind()}"); + return originalCompoundAssignment; + } + + var leftOfAssignment = assignmentExpression.Left; + var rightOfAssignment = assignmentExpression.Right; + + if (originalCompoundAssignment.Parent.IsKind(SyntaxKind.ExpressionStatement)) + { + if (!originalCompoundAssignment.IsKind(SyntaxKind.CoalesceAssignmentExpression)) { - if (!originalCompoundAssignment.IsKind(SyntaxKind.CoalesceAssignmentExpression)) - { - // Case 1. Simple compound assignment parented by an expression statement. - return editor.Generator.AssignmentStatement(newAssignmentTarget, rightOfAssignment); - } - else - { - // Case 2. Null coalescing compound assignment parented by an expression statement. - // Remove leading trivia from 'leftOfAssignment' as it should have been moved to 'newAssignmentTarget'. - leftOfAssignment = leftOfAssignment.WithoutLeadingTrivia(); - return editor.Generator.AssignmentStatement(newAssignmentTarget, - SyntaxFactory.BinaryExpression(SyntaxKind.CoalesceExpression, leftOfAssignment, rightOfAssignment)); - } + // Case 1. Simple compound assignment parented by an expression statement. + return editor.Generator.AssignmentStatement(newAssignmentTarget, rightOfAssignment); } else { - // Case 3. Compound assignment not parented by an expression statement. - var mappedBinaryExpressionKind = originalCompoundAssignment.Kind().MapCompoundAssignmentKindToBinaryExpressionKind(); - if (mappedBinaryExpressionKind == SyntaxKind.None) - { - return originalCompoundAssignment; - } - - return SyntaxFactory.BinaryExpression(mappedBinaryExpressionKind, leftOfAssignment, rightOfAssignment); + // Case 2. Null coalescing compound assignment parented by an expression statement. + // Remove leading trivia from 'leftOfAssignment' as it should have been moved to 'newAssignmentTarget'. + leftOfAssignment = leftOfAssignment.WithoutLeadingTrivia(); + return editor.Generator.AssignmentStatement(newAssignmentTarget, + SyntaxFactory.BinaryExpression(SyntaxKind.CoalesceExpression, leftOfAssignment, rightOfAssignment)); } } - - protected override SyntaxNode GetReplacementNodeForVarPattern(SyntaxNode originalVarPattern, SyntaxNode newNameNode) + else { - if (originalVarPattern is not VarPatternSyntax pattern) - throw ExceptionUtilities.Unreachable(); - - // If the replacement node is DiscardDesignationSyntax - // then we need to just change the incoming var's pattern designation - if (newNameNode is DiscardDesignationSyntax discardDesignation) + // Case 3. Compound assignment not parented by an expression statement. + var mappedBinaryExpressionKind = originalCompoundAssignment.Kind().MapCompoundAssignmentKindToBinaryExpressionKind(); + if (mappedBinaryExpressionKind == SyntaxKind.None) { - return pattern.WithDesignation(discardDesignation.WithTriviaFrom(pattern.Designation)); + return originalCompoundAssignment; } - // Otherwise just return new node as a replacement. - // This would be the default behavior if there was no special case described above - return newNameNode; + return SyntaxFactory.BinaryExpression(mappedBinaryExpressionKind, leftOfAssignment, rightOfAssignment); + } + } + + protected override SyntaxNode GetReplacementNodeForVarPattern(SyntaxNode originalVarPattern, SyntaxNode newNameNode) + { + if (originalVarPattern is not VarPatternSyntax pattern) + throw ExceptionUtilities.Unreachable(); + + // If the replacement node is DiscardDesignationSyntax + // then we need to just change the incoming var's pattern designation + if (newNameNode is DiscardDesignationSyntax discardDesignation) + { + return pattern.WithDesignation(discardDesignation.WithTriviaFrom(pattern.Designation)); } + + // Otherwise just return new node as a replacement. + // This would be the default behavior if there was no special case described above + return newNameNode; } } diff --git a/src/Analyzers/CSharp/CodeFixes/ReplaceDefaultLiteral/CSharpReplaceDefaultLiteralCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/ReplaceDefaultLiteral/CSharpReplaceDefaultLiteralCodeFixProvider.cs index b1e3eb2aa5e76..5d042bc0fe210 100644 --- a/src/Analyzers/CSharp/CodeFixes/ReplaceDefaultLiteral/CSharpReplaceDefaultLiteralCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/ReplaceDefaultLiteral/CSharpReplaceDefaultLiteralCodeFixProvider.cs @@ -17,136 +17,135 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.ReplaceDefaultLiteral +namespace Microsoft.CodeAnalysis.CSharp.ReplaceDefaultLiteral; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ReplaceDefaultLiteral), Shared] +internal sealed class CSharpReplaceDefaultLiteralCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ReplaceDefaultLiteral), Shared] - internal sealed class CSharpReplaceDefaultLiteralCodeFixProvider : CodeFixProvider + private const string CS8313 = nameof(CS8313); // A default literal 'default' is not valid as a case constant. Use another literal (e.g. '0' or 'null') as appropriate. If you intended to write the default label, use 'default:' without 'case'. + private const string CS8505 = nameof(CS8505); // A default literal 'default' is not valid as a pattern. Use another literal (e.g. '0' or 'null') as appropriate. To match everything, use a discard pattern 'var _'. + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpReplaceDefaultLiteralCodeFixProvider() { - private const string CS8313 = nameof(CS8313); // A default literal 'default' is not valid as a case constant. Use another literal (e.g. '0' or 'null') as appropriate. If you intended to write the default label, use 'default:' without 'case'. - private const string CS8505 = nameof(CS8505); // A default literal 'default' is not valid as a pattern. Use another literal (e.g. '0' or 'null') as appropriate. To match everything, use a discard pattern 'var _'. + } - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpReplaceDefaultLiteralCodeFixProvider() - { - } + public override ImmutableArray FixableDiagnosticIds { get; } = + [CS8313, CS8505]; - public override ImmutableArray FixableDiagnosticIds { get; } = - [CS8313, CS8505]; + public override FixAllProvider? GetFixAllProvider() + { + // This code fix addresses very specific compiler errors. It's unlikely there will be more than 1 of them at a time. + return null; + } - public override FixAllProvider? GetFixAllProvider() - { - // This code fix addresses very specific compiler errors. It's unlikely there will be more than 1 of them at a time. - return null; - } + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var syntaxRoot = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var token = syntaxRoot.FindToken(context.Span.Start); - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + if (token.Span == context.Span && + token.IsKind(SyntaxKind.DefaultKeyword) && + token.Parent is LiteralExpressionSyntax(SyntaxKind.DefaultLiteralExpression) defaultLiteral) { - var syntaxRoot = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var token = syntaxRoot.FindToken(context.Span.Start); + var semanticModel = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + + var (newExpression, displayText) = GetReplacementExpressionAndText( + context.Document, semanticModel, defaultLiteral, context.CancellationToken); - if (token.Span == context.Span && - token.IsKind(SyntaxKind.DefaultKeyword) && - token.Parent is LiteralExpressionSyntax(SyntaxKind.DefaultLiteralExpression) defaultLiteral) + if (newExpression != null) { - var semanticModel = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); - - var (newExpression, displayText) = GetReplacementExpressionAndText( - context.Document, semanticModel, defaultLiteral, context.CancellationToken); - - if (newExpression != null) - { - context.RegisterCodeFix( - CodeAction.Create( - string.Format(CSharpCodeFixesResources.Use_0, displayText), - c => ReplaceAsync(context.Document, context.Span, newExpression, c), - nameof(CSharpCodeFixesResources.Use_0)), - context.Diagnostics); - } + context.RegisterCodeFix( + CodeAction.Create( + string.Format(CSharpCodeFixesResources.Use_0, displayText), + c => ReplaceAsync(context.Document, context.Span, newExpression, c), + nameof(CSharpCodeFixesResources.Use_0)), + context.Diagnostics); } } + } - private static async Task ReplaceAsync( - Document document, TextSpan span, SyntaxNode newExpression, CancellationToken cancellationToken) - { - var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + private static async Task ReplaceAsync( + Document document, TextSpan span, SyntaxNode newExpression, CancellationToken cancellationToken) + { + var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var defaultToken = syntaxRoot.FindToken(span.Start); - var defaultLiteral = (LiteralExpressionSyntax)defaultToken.GetRequiredParent(); + var defaultToken = syntaxRoot.FindToken(span.Start); + var defaultLiteral = (LiteralExpressionSyntax)defaultToken.GetRequiredParent(); - var newRoot = syntaxRoot.ReplaceNode(defaultLiteral, newExpression.WithTriviaFrom(defaultLiteral)); - return document.WithSyntaxRoot(newRoot); - } + var newRoot = syntaxRoot.ReplaceNode(defaultLiteral, newExpression.WithTriviaFrom(defaultLiteral)); + return document.WithSyntaxRoot(newRoot); + } - private static (SyntaxNode newExpression, string displayText) GetReplacementExpressionAndText( - Document document, - SemanticModel semanticModel, - LiteralExpressionSyntax defaultLiteral, - CancellationToken cancellationToken) - { - var generator = SyntaxGenerator.GetGenerator(document); + private static (SyntaxNode newExpression, string displayText) GetReplacementExpressionAndText( + Document document, + SemanticModel semanticModel, + LiteralExpressionSyntax defaultLiteral, + CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); - var type = semanticModel.GetTypeInfo(defaultLiteral, cancellationToken).ConvertedType; - if (type != null && type.TypeKind != TypeKind.Error) + var type = semanticModel.GetTypeInfo(defaultLiteral, cancellationToken).ConvertedType; + if (type != null && type.TypeKind != TypeKind.Error) + { + if (IsFlagsEnum(type, semanticModel.Compilation) && + type.GetMembers("None").FirstOrDefault() is IFieldSymbol field && IsZero(field.ConstantValue)) { - if (IsFlagsEnum(type, semanticModel.Compilation) && - type.GetMembers("None").FirstOrDefault() is IFieldSymbol field && IsZero(field.ConstantValue)) - { - return GenerateMemberAccess("None"); - } - else if (type.Equals(semanticModel.Compilation.GetTypeByMetadataName(typeof(CancellationToken).FullName!))) - { - return GenerateMemberAccess(nameof(CancellationToken.None)); - } - else if (type.SpecialType is SpecialType.System_IntPtr or SpecialType.System_UIntPtr) - { - return GenerateMemberAccess(nameof(IntPtr.Zero)); - } - else if (semanticModel.GetConstantValue(defaultLiteral, cancellationToken) is var constant && constant.HasValue) - { - var newLiteral = generator.LiteralExpression(constant.Value); - return (newLiteral, newLiteral.ToString()); - } - else if (!type.ContainsAnonymousType()) - { - var defaultExpression = generator.DefaultExpression(type); - return (defaultExpression, $"default({type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)})"); - } + return GenerateMemberAccess("None"); } - - return default; - - (SyntaxNode newExpression, string displayText) GenerateMemberAccess(string memberName) + else if (type.Equals(semanticModel.Compilation.GetTypeByMetadataName(typeof(CancellationToken).FullName!))) + { + return GenerateMemberAccess(nameof(CancellationToken.None)); + } + else if (type.SpecialType is SpecialType.System_IntPtr or SpecialType.System_UIntPtr) + { + return GenerateMemberAccess(nameof(IntPtr.Zero)); + } + else if (semanticModel.GetConstantValue(defaultLiteral, cancellationToken) is var constant && constant.HasValue) + { + var newLiteral = generator.LiteralExpression(constant.Value); + return (newLiteral, newLiteral.ToString()); + } + else if (!type.ContainsAnonymousType()) { - var memberAccess = generator.MemberAccessExpression(generator.TypeExpression(type), memberName); - return (memberAccess, $"{type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}.{memberName}"); + var defaultExpression = generator.DefaultExpression(type); + return (defaultExpression, $"default({type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)})"); } } - private static bool IsFlagsEnum(ITypeSymbol type, Compilation compilation) + return default; + + (SyntaxNode newExpression, string displayText) GenerateMemberAccess(string memberName) { - var flagsAttribute = compilation.GetTypeByMetadataName(typeof(FlagsAttribute).FullName!); - return type.TypeKind == TypeKind.Enum && - flagsAttribute != null && - type.GetAttributes().Any(static (attribute, flagsAttribute) => flagsAttribute.Equals(attribute.AttributeClass), flagsAttribute); + var memberAccess = generator.MemberAccessExpression(generator.TypeExpression(type), memberName); + return (memberAccess, $"{type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}.{memberName}"); } + } + + private static bool IsFlagsEnum(ITypeSymbol type, Compilation compilation) + { + var flagsAttribute = compilation.GetTypeByMetadataName(typeof(FlagsAttribute).FullName!); + return type.TypeKind == TypeKind.Enum && + flagsAttribute != null && + type.GetAttributes().Any(static (attribute, flagsAttribute) => flagsAttribute.Equals(attribute.AttributeClass), flagsAttribute); + } - private static bool IsZero(object? o) + private static bool IsZero(object? o) + { + switch (o) { - switch (o) - { - case default(int): - case default(uint): - case default(byte): - case default(sbyte): - case default(short): - case default(ushort): - case default(long): - case default(ulong): - return true; - default: - return false; - } + case default(int): + case default(uint): + case default(byte): + case default(sbyte): + case default(short): + case default(ushort): + case default(long): + case default(ulong): + return true; + default: + return false; } } } diff --git a/src/Analyzers/CSharp/CodeFixes/SimplifyInterpolation/CSharpSimplifyInterpolationCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/SimplifyInterpolation/CSharpSimplifyInterpolationCodeFixProvider.cs index 618f4d11dc790..e701351b66dc5 100644 --- a/src/Analyzers/CSharp/CodeFixes/SimplifyInterpolation/CSharpSimplifyInterpolationCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/SimplifyInterpolation/CSharpSimplifyInterpolationCodeFixProvider.cs @@ -12,62 +12,61 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.SimplifyInterpolation; -namespace Microsoft.CodeAnalysis.CSharp.SimplifyInterpolation +namespace Microsoft.CodeAnalysis.CSharp.SimplifyInterpolation; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SimplifyInterpolation), Shared] +internal class CSharpSimplifyInterpolationCodeFixProvider : AbstractSimplifyInterpolationCodeFixProvider< + InterpolationSyntax, ExpressionSyntax, InterpolationAlignmentClauseSyntax, + InterpolationFormatClauseSyntax, InterpolatedStringExpressionSyntax> { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SimplifyInterpolation), Shared] - internal class CSharpSimplifyInterpolationCodeFixProvider : AbstractSimplifyInterpolationCodeFixProvider< - InterpolationSyntax, ExpressionSyntax, InterpolationAlignmentClauseSyntax, - InterpolationFormatClauseSyntax, InterpolatedStringExpressionSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpSimplifyInterpolationCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpSimplifyInterpolationCodeFixProvider() - { - } + } - protected override AbstractSimplifyInterpolationHelpers GetHelpers() => CSharpSimplifyInterpolationHelpers.Instance; + protected override AbstractSimplifyInterpolationHelpers GetHelpers() => CSharpSimplifyInterpolationHelpers.Instance; - protected override InterpolationSyntax WithExpression(InterpolationSyntax interpolation, ExpressionSyntax expression) - => interpolation.WithExpression(expression); + protected override InterpolationSyntax WithExpression(InterpolationSyntax interpolation, ExpressionSyntax expression) + => interpolation.WithExpression(expression); - protected override InterpolationSyntax WithAlignmentClause(InterpolationSyntax interpolation, InterpolationAlignmentClauseSyntax alignmentClause) - => interpolation.WithAlignmentClause(alignmentClause); + protected override InterpolationSyntax WithAlignmentClause(InterpolationSyntax interpolation, InterpolationAlignmentClauseSyntax alignmentClause) + => interpolation.WithAlignmentClause(alignmentClause); - protected override InterpolationSyntax WithFormatClause(InterpolationSyntax interpolation, InterpolationFormatClauseSyntax formatClause) - => interpolation.WithFormatClause(formatClause); + protected override InterpolationSyntax WithFormatClause(InterpolationSyntax interpolation, InterpolationFormatClauseSyntax formatClause) + => interpolation.WithFormatClause(formatClause); - protected override string Escape(InterpolatedStringExpressionSyntax interpolatedString, string formatString) + protected override string Escape(InterpolatedStringExpressionSyntax interpolatedString, string formatString) + { + var result = new StringBuilder(); + if (interpolatedString.StringStartToken.Kind() == SyntaxKind.InterpolatedVerbatimStringStartToken) { - var result = new StringBuilder(); - if (interpolatedString.StringStartToken.Kind() == SyntaxKind.InterpolatedVerbatimStringStartToken) + foreach (var c in formatString) { - foreach (var c in formatString) + // in a verbatim string, the only char we have to escape is the double-quote char + if (c == '"') { - // in a verbatim string, the only char we have to escape is the double-quote char - if (c == '"') - { - result.Append(c); - } - result.Append(c); } + + result.Append(c); } - else + } + else + { + // In a normal string we have to escape quotes and we have to escape an + // escape character itself. + foreach (var c in formatString) { - // In a normal string we have to escape quotes and we have to escape an - // escape character itself. - foreach (var c in formatString) + if (c is '"' or '\\') { - if (c is '"' or '\\') - { - result.Append('\\'); - } - - result.Append(c); + result.Append('\\'); } - } - return result.ToString(); + result.Append(c); + } } + + return result.ToString(); } } diff --git a/src/Analyzers/CSharp/CodeFixes/SimplifyLinqExpression/CSharpSimplifyLinqExpressionCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/SimplifyLinqExpression/CSharpSimplifyLinqExpressionCodeFixProvider.cs index 2639ac89f87d4..6d70c1b191fa6 100644 --- a/src/Analyzers/CSharp/CodeFixes/SimplifyLinqExpression/CSharpSimplifyLinqExpressionCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/SimplifyLinqExpression/CSharpSimplifyLinqExpressionCodeFixProvider.cs @@ -10,17 +10,16 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.SimplifyLinqExpression; -namespace Microsoft.CodeAnalysis.CSharp.SimplifyLinqExpression +namespace Microsoft.CodeAnalysis.CSharp.SimplifyLinqExpression; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SimplifyLinqExpression), Shared] +internal sealed class CSharpSimplifyLinqExpressionCodeFixProvider : AbstractSimplifyLinqExpressionCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SimplifyLinqExpression), Shared] - internal sealed class CSharpSimplifyLinqExpressionCodeFixProvider : AbstractSimplifyLinqExpressionCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpSimplifyLinqExpressionCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpSimplifyLinqExpressionCodeFixProvider() - { - } - - protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; } + + protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; } diff --git a/src/Analyzers/CSharp/CodeFixes/SimplifyPropertyPattern/CSharpSimplifyPropertyPatternCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/SimplifyPropertyPattern/CSharpSimplifyPropertyPatternCodeFixProvider.cs index 472bb44e657c0..49e1cd6eb64fc 100644 --- a/src/Analyzers/CSharp/CodeFixes/SimplifyPropertyPattern/CSharpSimplifyPropertyPatternCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/SimplifyPropertyPattern/CSharpSimplifyPropertyPatternCodeFixProvider.cs @@ -18,104 +18,103 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SimplifyPropertyPattern +namespace Microsoft.CodeAnalysis.CSharp.SimplifyPropertyPattern; + +using static SyntaxFactory; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SimplifyPropertyPattern), Shared] +internal class CSharpSimplifyPropertyPatternCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - using static SyntaxFactory; + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpSimplifyPropertyPatternCodeFixProvider() + { + } - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SimplifyPropertyPattern), Shared] - internal class CSharpSimplifyPropertyPatternCodeFixProvider : SyntaxEditorBasedCodeFixProvider + public override ImmutableArray FixableDiagnosticIds { get; } = + [IDEDiagnosticIds.SimplifyPropertyPatternDiagnosticId]; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpSimplifyPropertyPatternCodeFixProvider() - { - } + RegisterCodeFix(context, CSharpAnalyzersResources.Simplify_property_pattern, nameof(CSharpAnalyzersResources.Simplify_property_pattern)); + return Task.CompletedTask; + } - public override ImmutableArray FixableDiagnosticIds { get; } = - [IDEDiagnosticIds.SimplifyPropertyPatternDiagnosticId]; + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + // Process subpatterns in reverse order so we rewrite from inside-to-outside with nested + // patterns. + var subpatterns = diagnostics.Select(d => (SubpatternSyntax)d.AdditionalLocations[0].FindNode(cancellationToken)) + .OrderByDescending(s => s.SpanStart) + .ToImmutableArray(); - public override Task RegisterCodeFixesAsync(CodeFixContext context) + foreach (var subpattern in subpatterns) { - RegisterCodeFix(context, CSharpAnalyzersResources.Simplify_property_pattern, nameof(CSharpAnalyzersResources.Simplify_property_pattern)); - return Task.CompletedTask; + editor.ReplaceNode( + subpattern, + (current, _) => + { + var currentSubpattern = (SubpatternSyntax)current; + var simplified = TrySimplify(currentSubpattern); + return simplified ?? currentSubpattern; + }); } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - // Process subpatterns in reverse order so we rewrite from inside-to-outside with nested - // patterns. - var subpatterns = diagnostics.Select(d => (SubpatternSyntax)d.AdditionalLocations[0].FindNode(cancellationToken)) - .OrderByDescending(s => s.SpanStart) - .ToImmutableArray(); - - foreach (var subpattern in subpatterns) - { - editor.ReplaceNode( - subpattern, - (current, _) => - { - var currentSubpattern = (SubpatternSyntax)current; - var simplified = TrySimplify(currentSubpattern); - return simplified ?? currentSubpattern; - }); - } - - return Task.CompletedTask; - } + return Task.CompletedTask; + } - private static SubpatternSyntax? TrySimplify(SubpatternSyntax currentSubpattern) - { - if (!SimplifyPropertyPatternHelpers.IsSimplifiable(currentSubpattern, out var innerSubpattern, out var outerExpressionColon)) - return null; + private static SubpatternSyntax? TrySimplify(SubpatternSyntax currentSubpattern) + { + if (!SimplifyPropertyPatternHelpers.IsSimplifiable(currentSubpattern, out var innerSubpattern, out var outerExpressionColon)) + return null; - // attempt to simplify the inner pattern we're pointing at as well (that way if the user - // invokes the fix on a top level property, we collapse as far inwards as possible). - innerSubpattern = TrySimplify(innerSubpattern) ?? innerSubpattern; + // attempt to simplify the inner pattern we're pointing at as well (that way if the user + // invokes the fix on a top level property, we collapse as far inwards as possible). + innerSubpattern = TrySimplify(innerSubpattern) ?? innerSubpattern; - var innerExpressionColon = innerSubpattern.ExpressionColon; + var innerExpressionColon = innerSubpattern.ExpressionColon; - if (!SimplifyPropertyPatternHelpers.IsMergable(outerExpressionColon.Expression) || - !SimplifyPropertyPatternHelpers.IsMergable(innerExpressionColon?.Expression)) - { - return null; - } + if (!SimplifyPropertyPatternHelpers.IsMergable(outerExpressionColon.Expression) || + !SimplifyPropertyPatternHelpers.IsMergable(innerExpressionColon?.Expression)) + { + return null; + } - var merged = Merge(outerExpressionColon, innerExpressionColon); - if (merged == null) - return null; + var merged = Merge(outerExpressionColon, innerExpressionColon); + if (merged == null) + return null; - return currentSubpattern.WithExpressionColon(merged) - .WithPattern(innerSubpattern.Pattern) - .WithAdditionalAnnotations(Formatter.Annotation); - } + return currentSubpattern.WithExpressionColon(merged) + .WithPattern(innerSubpattern.Pattern) + .WithAdditionalAnnotations(Formatter.Annotation); + } - private static BaseExpressionColonSyntax? Merge(BaseExpressionColonSyntax outerExpressionColon, BaseExpressionColonSyntax innerExpressionColon) - { - var merged = Merge(outerExpressionColon.Expression, innerExpressionColon.Expression); - if (merged == null) - return null; + private static BaseExpressionColonSyntax? Merge(BaseExpressionColonSyntax outerExpressionColon, BaseExpressionColonSyntax innerExpressionColon) + { + var merged = Merge(outerExpressionColon.Expression, innerExpressionColon.Expression); + if (merged == null) + return null; - return outerExpressionColon.WithExpression(merged); - } + return outerExpressionColon.WithExpression(merged); + } - private static MemberAccessExpressionSyntax? Merge(ExpressionSyntax? outerExpression, ExpressionSyntax? innerExpression) - { - if (outerExpression == null || innerExpression == null) - return null; + private static MemberAccessExpressionSyntax? Merge(ExpressionSyntax? outerExpression, ExpressionSyntax? innerExpression) + { + if (outerExpression == null || innerExpression == null) + return null; - // if the inner name is simple (i.e. just 'X') we can trivially form the final member - // access by joining the two names together. - if (innerExpression is SimpleNameSyntax innerName) - return MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, outerExpression, innerName); + // if the inner name is simple (i.e. just 'X') we can trivially form the final member + // access by joining the two names together. + if (innerExpression is SimpleNameSyntax innerName) + return MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, outerExpression, innerName); - if (innerExpression is not MemberAccessExpressionSyntax innerMemberAccess) - return null; + if (innerExpression is not MemberAccessExpressionSyntax innerMemberAccess) + return null; - // otherwise, attempt to decompose the inner expression, joining that with the outer until we get - // the result. - return Merge(Merge(outerExpression, innerMemberAccess.Expression), innerMemberAccess.Name); - } + // otherwise, attempt to decompose the inner expression, joining that with the outer until we get + // the result. + return Merge(Merge(outerExpression, innerMemberAccess.Expression), innerMemberAccess.Name); } } diff --git a/src/Analyzers/CSharp/CodeFixes/TransposeRecordKeyword/CSharpTransposeRecordKeywordCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/TransposeRecordKeyword/CSharpTransposeRecordKeywordCodeFixProvider.cs index 5b70074f3bf24..37d5bea62bf4d 100644 --- a/src/Analyzers/CSharp/CodeFixes/TransposeRecordKeyword/CSharpTransposeRecordKeywordCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/TransposeRecordKeyword/CSharpTransposeRecordKeywordCodeFixProvider.cs @@ -16,112 +16,111 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.TransposeRecordKeyword +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.TransposeRecordKeyword; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.TransposeRecordKeyword), Shared] +internal class CSharpTransposeRecordKeywordCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.TransposeRecordKeyword), Shared] - internal class CSharpTransposeRecordKeywordCodeFixProvider : SyntaxEditorBasedCodeFixProvider - { - private const string CS9012 = nameof(CS9012); // Unexpected keyword 'record'. Did you mean 'record struct' or 'record class'? + private const string CS9012 = nameof(CS9012); // Unexpected keyword 'record'. Did you mean 'record struct' or 'record class'? - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpTransposeRecordKeywordCodeFixProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpTransposeRecordKeywordCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds - => [CS9012]; + public override ImmutableArray FixableDiagnosticIds + => [CS9012]; - private static bool TryGetRecordDeclaration( - Diagnostic diagnostic, CancellationToken cancellationToken, [NotNullWhen(true)] out RecordDeclarationSyntax? recordDeclaration) - { - recordDeclaration = diagnostic.Location.FindNode(cancellationToken) as RecordDeclarationSyntax; - return recordDeclaration != null; - } + private static bool TryGetRecordDeclaration( + Diagnostic diagnostic, CancellationToken cancellationToken, [NotNullWhen(true)] out RecordDeclarationSyntax? recordDeclaration) + { + recordDeclaration = diagnostic.Location.FindNode(cancellationToken) as RecordDeclarationSyntax; + return recordDeclaration != null; + } - private static bool TryGetTokens( - RecordDeclarationSyntax recordDeclaration, - out SyntaxToken classOrStructKeyword, - out SyntaxToken recordKeyword) + private static bool TryGetTokens( + RecordDeclarationSyntax recordDeclaration, + out SyntaxToken classOrStructKeyword, + out SyntaxToken recordKeyword) + { + recordKeyword = recordDeclaration.Keyword; + if (!recordKeyword.IsMissing) { - recordKeyword = recordDeclaration.Keyword; - if (!recordKeyword.IsMissing) + var leadingTrivia = recordKeyword.LeadingTrivia; + var skippedTriviaIndex = leadingTrivia.IndexOf(SyntaxKind.SkippedTokensTrivia); + if (skippedTriviaIndex >= 0) { - var leadingTrivia = recordKeyword.LeadingTrivia; - var skippedTriviaIndex = leadingTrivia.IndexOf(SyntaxKind.SkippedTokensTrivia); - if (skippedTriviaIndex >= 0) + var skippedTrivia = leadingTrivia[skippedTriviaIndex]; + var structure = (SkippedTokensTriviaSyntax)skippedTrivia.GetStructure()!; + var tokens = structure.Tokens; + if (tokens.Count == 1) { - var skippedTrivia = leadingTrivia[skippedTriviaIndex]; - var structure = (SkippedTokensTriviaSyntax)skippedTrivia.GetStructure()!; - var tokens = structure.Tokens; - if (tokens.Count == 1) + classOrStructKeyword = tokens.Single(); + if (classOrStructKeyword.Kind() is SyntaxKind.ClassKeyword or SyntaxKind.StructKeyword) { - classOrStructKeyword = tokens.Single(); - if (classOrStructKeyword.Kind() is SyntaxKind.ClassKeyword or SyntaxKind.StructKeyword) - { - // Because the class/struct keyword is skipped trivia on the record keyword, it will - // not have trivia of it's own. So we need to move the other trivia appropriate trivia - // on the record keyword to it. - var remainingLeadingTrivia = SyntaxFactory.TriviaList(leadingTrivia.Skip(skippedTriviaIndex + 1)); - var trailingTriviaTakeUntil = remainingLeadingTrivia.IndexOf(SyntaxKind.EndOfLineTrivia) is >= 0 and var eolIndex - ? eolIndex + 1 - : remainingLeadingTrivia.Count; - - classOrStructKeyword = classOrStructKeyword - .WithLeadingTrivia(SyntaxFactory.TriviaList(remainingLeadingTrivia.Skip(trailingTriviaTakeUntil))) - .WithTrailingTrivia(recordKeyword.TrailingTrivia); - recordKeyword = recordKeyword - .WithLeadingTrivia(leadingTrivia.Take(skippedTriviaIndex)) - .WithTrailingTrivia(SyntaxFactory.TriviaList(remainingLeadingTrivia.Take(trailingTriviaTakeUntil))); - - return true; - } + // Because the class/struct keyword is skipped trivia on the record keyword, it will + // not have trivia of it's own. So we need to move the other trivia appropriate trivia + // on the record keyword to it. + var remainingLeadingTrivia = SyntaxFactory.TriviaList(leadingTrivia.Skip(skippedTriviaIndex + 1)); + var trailingTriviaTakeUntil = remainingLeadingTrivia.IndexOf(SyntaxKind.EndOfLineTrivia) is >= 0 and var eolIndex + ? eolIndex + 1 + : remainingLeadingTrivia.Count; + + classOrStructKeyword = classOrStructKeyword + .WithLeadingTrivia(SyntaxFactory.TriviaList(remainingLeadingTrivia.Skip(trailingTriviaTakeUntil))) + .WithTrailingTrivia(recordKeyword.TrailingTrivia); + recordKeyword = recordKeyword + .WithLeadingTrivia(leadingTrivia.Take(skippedTriviaIndex)) + .WithTrailingTrivia(SyntaxFactory.TriviaList(remainingLeadingTrivia.Take(trailingTriviaTakeUntil))); + + return true; } } } - - classOrStructKeyword = default; - return false; } - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var cancellationToken = context.CancellationToken; + classOrStructKeyword = default; + return false; + } - var diagnostic = context.Diagnostics.First(); - if (TryGetRecordDeclaration(diagnostic, cancellationToken, out var recordDeclaration) && - TryGetTokens(recordDeclaration, out _, out _)) - { - RegisterCodeFix(context, CSharpCodeFixesResources.Fix_record_declaration, nameof(CSharpCodeFixesResources.Fix_record_declaration)); - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var cancellationToken = context.CancellationToken; - return Task.CompletedTask; + var diagnostic = context.Diagnostics.First(); + if (TryGetRecordDeclaration(diagnostic, cancellationToken, out var recordDeclaration) && + TryGetTokens(recordDeclaration, out _, out _)) + { + RegisterCodeFix(context, CSharpCodeFixesResources.Fix_record_declaration, nameof(CSharpCodeFixesResources.Fix_record_declaration)); } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + return Task.CompletedTask; + } + + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) { - foreach (var diagnostic in diagnostics) + if (TryGetRecordDeclaration(diagnostic, cancellationToken, out var recordDeclaration)) { - if (TryGetRecordDeclaration(diagnostic, cancellationToken, out var recordDeclaration)) - { - editor.ReplaceNode( - recordDeclaration, - (current, _) => - { - var currentRecordDeclaration = (RecordDeclarationSyntax)current; - if (!TryGetTokens(currentRecordDeclaration, out var classOrStructKeyword, out var recordKeyword)) - return currentRecordDeclaration; - - return currentRecordDeclaration - .WithClassOrStructKeyword(classOrStructKeyword) - .WithKeyword(recordKeyword); - }); - } - } + editor.ReplaceNode( + recordDeclaration, + (current, _) => + { + var currentRecordDeclaration = (RecordDeclarationSyntax)current; + if (!TryGetTokens(currentRecordDeclaration, out var classOrStructKeyword, out var recordKeyword)) + return currentRecordDeclaration; - return Task.CompletedTask; + return currentRecordDeclaration + .WithClassOrStructKeyword(classOrStructKeyword) + .WithKeyword(recordKeyword); + }); + } } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/CSharp/CodeFixes/UnsealClass/CSharpUnsealClassCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UnsealClass/CSharpUnsealClassCodeFixProvider.cs index 1af8b898310a9..fa04bf91c8e1f 100644 --- a/src/Analyzers/CSharp/CodeFixes/UnsealClass/CSharpUnsealClassCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UnsealClass/CSharpUnsealClassCodeFixProvider.cs @@ -9,22 +9,21 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.UnsealClass; -namespace Microsoft.CodeAnalysis.CSharp.UnsealClass +namespace Microsoft.CodeAnalysis.CSharp.UnsealClass; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UnsealClass), Shared] +internal sealed class CSharpUnsealClassCodeFixProvider : AbstractUnsealClassCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UnsealClass), Shared] - internal sealed class CSharpUnsealClassCodeFixProvider : AbstractUnsealClassCodeFixProvider - { - private const string CS0509 = nameof(CS0509); // 'D': cannot derive from sealed type 'C' + private const string CS0509 = nameof(CS0509); // 'D': cannot derive from sealed type 'C' - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpUnsealClassCodeFixProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpUnsealClassCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds { get; } = - [CS0509]; + public override ImmutableArray FixableDiagnosticIds { get; } = + [CS0509]; - protected override string TitleFormat => CSharpCodeFixesResources.Unseal_class_0; - } + protected override string TitleFormat => CSharpCodeFixesResources.Unseal_class_0; } diff --git a/src/Analyzers/CSharp/CodeFixes/UpdateProjectToAllowUnsafe/CSharpUpdateProjectToAllowUnsafeCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UpdateProjectToAllowUnsafe/CSharpUpdateProjectToAllowUnsafeCodeFixProvider.cs index 36e6c5e6216ef..5cc5d41e433ad 100644 --- a/src/Analyzers/CSharp/CodeFixes/UpdateProjectToAllowUnsafe/CSharpUpdateProjectToAllowUnsafeCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UpdateProjectToAllowUnsafe/CSharpUpdateProjectToAllowUnsafeCodeFixProvider.cs @@ -10,41 +10,40 @@ using Microsoft.CodeAnalysis.UpgradeProject; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UpdateProjectToAllowUnsafe +namespace Microsoft.CodeAnalysis.CSharp.UpdateProjectToAllowUnsafe; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UpdateProjectToAllowUnsafe), Shared] +internal class CSharpUpdateProjectToAllowUnsafeCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UpdateProjectToAllowUnsafe), Shared] - internal class CSharpUpdateProjectToAllowUnsafeCodeFixProvider : CodeFixProvider + private const string CS0227 = nameof(CS0227); // error CS0227: Unsafe code may only appear if compiling with /unsafe + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUpdateProjectToAllowUnsafeCodeFixProvider() + { + } + + public override ImmutableArray FixableDiagnosticIds { get; } = + [CS0227]; + + public override FixAllProvider? GetFixAllProvider() + { + // We're OK with users having to explicitly opt in each project. Unsafe code is itself an edge case and we don't + // need to make it easier to convert to it on a larger scale. It's also unlikely that anyone will need this. + return null; + } + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + context.RegisterCodeFix(ProjectOptionsChangeAction.Create(CSharpCodeFixesResources.Allow_unsafe_code_in_this_project, + _ => Task.FromResult(AllowUnsafeOnProject(context.Document.Project))), context.Diagnostics); + return Task.CompletedTask; + } + + private static Solution AllowUnsafeOnProject(Project project) { - private const string CS0227 = nameof(CS0227); // error CS0227: Unsafe code may only appear if compiling with /unsafe - - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUpdateProjectToAllowUnsafeCodeFixProvider() - { - } - - public override ImmutableArray FixableDiagnosticIds { get; } = - [CS0227]; - - public override FixAllProvider? GetFixAllProvider() - { - // We're OK with users having to explicitly opt in each project. Unsafe code is itself an edge case and we don't - // need to make it easier to convert to it on a larger scale. It's also unlikely that anyone will need this. - return null; - } - - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - context.RegisterCodeFix(ProjectOptionsChangeAction.Create(CSharpCodeFixesResources.Allow_unsafe_code_in_this_project, - _ => Task.FromResult(AllowUnsafeOnProject(context.Document.Project))), context.Diagnostics); - return Task.CompletedTask; - } - - private static Solution AllowUnsafeOnProject(Project project) - { - var compilationOptions = (CSharpCompilationOptions?)project.CompilationOptions; - Contract.ThrowIfNull(compilationOptions); - return project.Solution.WithProjectCompilationOptions(project.Id, compilationOptions.WithAllowUnsafe(true)); - } + var compilationOptions = (CSharpCompilationOptions?)project.CompilationOptions; + Contract.ThrowIfNull(compilationOptions); + return project.Solution.WithProjectCompilationOptions(project.Id, compilationOptions.WithAllowUnsafe(true)); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UpgradeProject/CSharpUpgradeProjectCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UpgradeProject/CSharpUpgradeProjectCodeFixProvider.cs index 7bb53834b8c15..32e6be6d7bc8b 100644 --- a/src/Analyzers/CSharp/CodeFixes/UpgradeProject/CSharpUpgradeProjectCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UpgradeProject/CSharpUpgradeProjectCodeFixProvider.cs @@ -10,111 +10,110 @@ using Microsoft.CodeAnalysis.UpgradeProject; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UpgradeProject +namespace Microsoft.CodeAnalysis.CSharp.UpgradeProject; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UpgradeProject), Shared] +internal class CSharpUpgradeProjectCodeFixProvider : AbstractUpgradeProjectCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UpgradeProject), Shared] - internal class CSharpUpgradeProjectCodeFixProvider : AbstractUpgradeProjectCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUpgradeProjectCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUpgradeProjectCodeFixProvider() - { - } - - public override ImmutableArray FixableDiagnosticIds { get; } = - [ - "CS8022", - "CS8023", - "CS8024", - "CS8025", - "CS8026", - "CS8059", - "CS8107", - "CS8302", - "CS8306", - "CS8314", - "CS8320", - "CS1738", - "CS8370", - "CS8371", - "CS8400", - "CS8401", - "CS8511", - "CS8627", - "CS8652", - "CS8773", - "CS8703", - "CS8706", - "CS8904", - "CS8912", - "CS8704", - "CS8957", - "CS8967", - "CS0171", - "CS0188", - "CS0843", - "CS8880", - "CS8881", - "CS8885", - "CS8936", - "CS9058", - "CS9194", - "CS9202", - ]; - - public override string UpgradeThisProjectResource => CSharpCodeFixesResources.Upgrade_this_project_to_csharp_language_version_0; - public override string UpgradeAllProjectsResource => CSharpCodeFixesResources.Upgrade_all_csharp_projects_to_language_version_0; + } - public override string SuggestedVersion(ImmutableArray diagnostics) - => RequiredVersion(diagnostics).ToDisplayString(); + public override ImmutableArray FixableDiagnosticIds { get; } = + [ + "CS8022", + "CS8023", + "CS8024", + "CS8025", + "CS8026", + "CS8059", + "CS8107", + "CS8302", + "CS8306", + "CS8314", + "CS8320", + "CS1738", + "CS8370", + "CS8371", + "CS8400", + "CS8401", + "CS8511", + "CS8627", + "CS8652", + "CS8773", + "CS8703", + "CS8706", + "CS8904", + "CS8912", + "CS8704", + "CS8957", + "CS8967", + "CS0171", + "CS0188", + "CS0843", + "CS8880", + "CS8881", + "CS8885", + "CS8936", + "CS9058", + "CS9194", + "CS9202", + ]; - private static LanguageVersion RequiredVersion(ImmutableArray diagnostics) - { - LanguageVersion max = 0; - foreach (var diagnostic in diagnostics) - { - if (diagnostic.Properties.TryGetValue(DiagnosticPropertyConstants.RequiredLanguageVersion, out var requiredVersion) && - LanguageVersionFacts.TryParse(requiredVersion, out var required)) - { - max = max > required ? max : required; - } - else if (diagnostic.Id == "CS8652") - { - max = LanguageVersion.Preview; - break; - } - } + public override string UpgradeThisProjectResource => CSharpCodeFixesResources.Upgrade_this_project_to_csharp_language_version_0; + public override string UpgradeAllProjectsResource => CSharpCodeFixesResources.Upgrade_all_csharp_projects_to_language_version_0; - return max; - } + public override string SuggestedVersion(ImmutableArray diagnostics) + => RequiredVersion(diagnostics).ToDisplayString(); - public override Solution UpgradeProject(Project project, string newVersion) + private static LanguageVersion RequiredVersion(ImmutableArray diagnostics) + { + LanguageVersion max = 0; + foreach (var diagnostic in diagnostics) { - if (IsUpgrade(project, newVersion)) + if (diagnostic.Properties.TryGetValue(DiagnosticPropertyConstants.RequiredLanguageVersion, out var requiredVersion) && + LanguageVersionFacts.TryParse(requiredVersion, out var required)) { - Contract.ThrowIfFalse(LanguageVersionFacts.TryParse(newVersion, out var parsedNewVersion)); - var parseOptions = (CSharpParseOptions)project.ParseOptions!; - - return project.Solution.WithProjectParseOptions(project.Id, parseOptions.WithLanguageVersion(parsedNewVersion)); + max = max > required ? max : required; } - else + else if (diagnostic.Id == "CS8652") { - // when fixing all projects in a solution, don't downgrade those with newer language versions - return project.Solution; + max = LanguageVersion.Preview; + break; } } - public override bool IsUpgrade(Project project, string newVersion) + return max; + } + + public override Solution UpgradeProject(Project project, string newVersion) + { + if (IsUpgrade(project, newVersion)) { Contract.ThrowIfFalse(LanguageVersionFacts.TryParse(newVersion, out var parsedNewVersion)); - var parseOptions = (CSharpParseOptions)project.ParseOptions!; - var mappedVersion = parsedNewVersion.MapSpecifiedToEffectiveVersion(); - // treat equivalent versions (one generic and one specific) to be a valid upgrade - return mappedVersion >= parseOptions.LanguageVersion && - parseOptions.SpecifiedLanguageVersion.ToDisplayString() != newVersion && - project.CanApplyParseOptionChange(parseOptions, parseOptions.WithLanguageVersion(parsedNewVersion)); + return project.Solution.WithProjectParseOptions(project.Id, parseOptions.WithLanguageVersion(parsedNewVersion)); + } + else + { + // when fixing all projects in a solution, don't downgrade those with newer language versions + return project.Solution; } } + + public override bool IsUpgrade(Project project, string newVersion) + { + Contract.ThrowIfFalse(LanguageVersionFacts.TryParse(newVersion, out var parsedNewVersion)); + + var parseOptions = (CSharpParseOptions)project.ParseOptions!; + var mappedVersion = parsedNewVersion.MapSpecifiedToEffectiveVersion(); + + // treat equivalent versions (one generic and one specific) to be a valid upgrade + return mappedVersion >= parseOptions.LanguageVersion && + parseOptions.SpecifiedLanguageVersion.ToDisplayString() != newVersion && + project.CanApplyParseOptionChange(parseOptions, parseOptions.WithLanguageVersion(parsedNewVersion)); + } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseCollectionInitializer/CSharpUseCollectionInitializerCodeFixProvider_CollectionExpression.cs b/src/Analyzers/CSharp/CodeFixes/UseCollectionInitializer/CSharpUseCollectionInitializerCodeFixProvider_CollectionExpression.cs index 19c3f29b3662f..7104850f27cd6 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseCollectionInitializer/CSharpUseCollectionInitializerCodeFixProvider_CollectionExpression.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseCollectionInitializer/CSharpUseCollectionInitializerCodeFixProvider_CollectionExpression.cs @@ -10,29 +10,28 @@ using Microsoft.CodeAnalysis.CSharp.UseCollectionExpression; using Microsoft.CodeAnalysis.UseCollectionInitializer; -namespace Microsoft.CodeAnalysis.CSharp.UseCollectionInitializer +namespace Microsoft.CodeAnalysis.CSharp.UseCollectionInitializer; + +internal partial class CSharpUseCollectionInitializerCodeFixProvider { - internal partial class CSharpUseCollectionInitializerCodeFixProvider + /// + /// Creates the final collection-expression [...] that will replace the given expression. + /// + private static Task CreateCollectionExpressionAsync( + Document document, + CodeActionOptionsProvider fallbackOptions, + BaseObjectCreationExpressionSyntax objectCreation, + ImmutableArray> matches, + CancellationToken cancellationToken) { - /// - /// Creates the final collection-expression [...] that will replace the given expression. - /// - private static Task CreateCollectionExpressionAsync( - Document document, - CodeActionOptionsProvider fallbackOptions, - BaseObjectCreationExpressionSyntax objectCreation, - ImmutableArray> matches, - CancellationToken cancellationToken) - { - return CSharpCollectionExpressionRewriter.CreateCollectionExpressionAsync( - document, - fallbackOptions, - objectCreation, - matches.SelectAsArray(m => new CollectionExpressionMatch(m.Statement, m.UseSpread)), - static objectCreation => objectCreation.Initializer, - static (objectCreation, initializer) => objectCreation.WithInitializer(initializer), - cancellationToken); - } + return CSharpCollectionExpressionRewriter.CreateCollectionExpressionAsync( + document, + fallbackOptions, + objectCreation, + matches.SelectAsArray(m => new CollectionExpressionMatch(m.Statement, m.UseSpread)), + static objectCreation => objectCreation.Initializer, + static (objectCreation, initializer) => objectCreation.WithInitializer(initializer), + cancellationToken); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseCompoundAssignment/CSharpUseCompoundAssignmentCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseCompoundAssignment/CSharpUseCompoundAssignmentCodeFixProvider.cs index 388e5743b8920..1539083103c96 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseCompoundAssignment/CSharpUseCompoundAssignmentCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseCompoundAssignment/CSharpUseCompoundAssignmentCodeFixProvider.cs @@ -11,56 +11,55 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.UseCompoundAssignment; -namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment +namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseCompoundAssignment), Shared] +internal class CSharpUseCompoundAssignmentCodeFixProvider + : AbstractUseCompoundAssignmentCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseCompoundAssignment), Shared] - internal class CSharpUseCompoundAssignmentCodeFixProvider - : AbstractUseCompoundAssignmentCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpUseCompoundAssignmentCodeFixProvider() + : base(Utilities.Kinds) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpUseCompoundAssignmentCodeFixProvider() - : base(Utilities.Kinds) - { - } + } - protected override SyntaxToken Token(SyntaxKind kind) - => SyntaxFactory.Token(kind); + protected override SyntaxToken Token(SyntaxKind kind) + => SyntaxFactory.Token(kind); - protected override AssignmentExpressionSyntax Assignment( - SyntaxKind assignmentOpKind, ExpressionSyntax left, SyntaxToken syntaxToken, ExpressionSyntax right) - { - return SyntaxFactory.AssignmentExpression(assignmentOpKind, left, syntaxToken, right); - } + protected override AssignmentExpressionSyntax Assignment( + SyntaxKind assignmentOpKind, ExpressionSyntax left, SyntaxToken syntaxToken, ExpressionSyntax right) + { + return SyntaxFactory.AssignmentExpression(assignmentOpKind, left, syntaxToken, right); + } - protected override ExpressionSyntax Increment(ExpressionSyntax left, bool postfix) - => postfix - ? Postfix(SyntaxKind.PostIncrementExpression, left) - : Prefix(SyntaxKind.PreIncrementExpression, left); + protected override ExpressionSyntax Increment(ExpressionSyntax left, bool postfix) + => postfix + ? Postfix(SyntaxKind.PostIncrementExpression, left) + : Prefix(SyntaxKind.PreIncrementExpression, left); - protected override ExpressionSyntax Decrement(ExpressionSyntax left, bool postfix) - => postfix - ? Postfix(SyntaxKind.PostDecrementExpression, left) - : Prefix(SyntaxKind.PreDecrementExpression, left); + protected override ExpressionSyntax Decrement(ExpressionSyntax left, bool postfix) + => postfix + ? Postfix(SyntaxKind.PostDecrementExpression, left) + : Prefix(SyntaxKind.PreDecrementExpression, left); - private static ExpressionSyntax Postfix(SyntaxKind kind, ExpressionSyntax operand) - => SyntaxFactory.PostfixUnaryExpression(kind, operand); + private static ExpressionSyntax Postfix(SyntaxKind kind, ExpressionSyntax operand) + => SyntaxFactory.PostfixUnaryExpression(kind, operand); - private static ExpressionSyntax Prefix(SyntaxKind kind, ExpressionSyntax operand) - => SyntaxFactory.PrefixUnaryExpression(kind, operand); + private static ExpressionSyntax Prefix(SyntaxKind kind, ExpressionSyntax operand) + => SyntaxFactory.PrefixUnaryExpression(kind, operand); - protected override SyntaxTriviaList PrepareRightExpressionLeadingTrivia(SyntaxTriviaList initialTrivia) => initialTrivia.SkipWhile(el => el.Kind() is SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia).ToSyntaxTriviaList(); + protected override SyntaxTriviaList PrepareRightExpressionLeadingTrivia(SyntaxTriviaList initialTrivia) => initialTrivia.SkipWhile(el => el.Kind() is SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia).ToSyntaxTriviaList(); - protected override bool PreferPostfix(ISyntaxFactsService syntaxFacts, AssignmentExpressionSyntax currentAssignment) + protected override bool PreferPostfix(ISyntaxFactsService syntaxFacts, AssignmentExpressionSyntax currentAssignment) + { + // in `for (...; x = x + 1)` we prefer to translate that idiomatically as `for (...; x++)` + if (currentAssignment.Parent is ForStatementSyntax forStatement && + forStatement.Incrementors.Contains(currentAssignment)) { - // in `for (...; x = x + 1)` we prefer to translate that idiomatically as `for (...; x++)` - if (currentAssignment.Parent is ForStatementSyntax forStatement && - forStatement.Incrementors.Contains(currentAssignment)) - { - return true; - } - - return base.PreferPostfix(syntaxFacts, currentAssignment); + return true; } + + return base.PreferPostfix(syntaxFacts, currentAssignment); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseCompoundAssignment/CSharpUseCompoundCoalesceAssignmentCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseCompoundAssignment/CSharpUseCompoundCoalesceAssignmentCodeFixProvider.cs index bca62522f64d7..d35d0cdf512cf 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseCompoundAssignment/CSharpUseCompoundCoalesceAssignmentCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseCompoundAssignment/CSharpUseCompoundCoalesceAssignmentCodeFixProvider.cs @@ -19,104 +19,103 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment +namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseCompoundCoalesceAssignment), Shared] +internal class CSharpUseCompoundCoalesceAssignmentCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseCompoundCoalesceAssignment), Shared] - internal class CSharpUseCompoundCoalesceAssignmentCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseCompoundCoalesceAssignmentCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseCompoundCoalesceAssignmentCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } = - [IDEDiagnosticIds.UseCoalesceCompoundAssignmentDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds { get; } = + [IDEDiagnosticIds.UseCoalesceCompoundAssignmentDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, AnalyzersResources.Use_compound_assignment, nameof(AnalyzersResources.Use_compound_assignment)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, AnalyzersResources.Use_compound_assignment, nameof(AnalyzersResources.Use_compound_assignment)); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); - var syntaxKinds = syntaxFacts.SyntaxKinds; + var syntaxFacts = document.GetRequiredLanguageService(); + var syntaxKinds = syntaxFacts.SyntaxKinds; - foreach (var diagnostic in diagnostics) - { - var coalesceOrIfStatement = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); + foreach (var diagnostic in diagnostics) + { + var coalesceOrIfStatement = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); - if (coalesceOrIfStatement is IfStatementSyntax ifStatement) + if (coalesceOrIfStatement is IfStatementSyntax ifStatement) + { + Contract.ThrowIfFalse(CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer.GetWhenTrueAssignment( + ifStatement, out var whenTrueStatement, out var assignment)); + + // we have `if (x is null) x = y;`. Update `x = y` to be `x ??= y`, then replace the entire + // if-statement with that assignment statement. + var newAssignment = SyntaxFactory.AssignmentExpression( + SyntaxKind.CoalesceAssignmentExpression, + assignment.Left, + SyntaxFactory.Token(SyntaxKind.QuestionQuestionEqualsToken).WithTriviaFrom(assignment.OperatorToken), + assignment.Right).WithTriviaFrom(assignment); + + var newWhenTrueStatement = whenTrueStatement.ReplaceNode(assignment, newAssignment); + + // If there's leading trivia on the original inner statement, then combine that with the leading + // trivia on the if-statement. We'll need to add a formatting annotation so that the leading comments + // are put in the right location. + if (newWhenTrueStatement.GetLeadingTrivia().Any(t => t.IsSingleOrMultiLineComment())) { - Contract.ThrowIfFalse(CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer.GetWhenTrueAssignment( - ifStatement, out var whenTrueStatement, out var assignment)); - - // we have `if (x is null) x = y;`. Update `x = y` to be `x ??= y`, then replace the entire - // if-statement with that assignment statement. - var newAssignment = SyntaxFactory.AssignmentExpression( - SyntaxKind.CoalesceAssignmentExpression, - assignment.Left, - SyntaxFactory.Token(SyntaxKind.QuestionQuestionEqualsToken).WithTriviaFrom(assignment.OperatorToken), - assignment.Right).WithTriviaFrom(assignment); - - var newWhenTrueStatement = whenTrueStatement.ReplaceNode(assignment, newAssignment); - - // If there's leading trivia on the original inner statement, then combine that with the leading - // trivia on the if-statement. We'll need to add a formatting annotation so that the leading comments - // are put in the right location. - if (newWhenTrueStatement.GetLeadingTrivia().Any(t => t.IsSingleOrMultiLineComment())) - { - newWhenTrueStatement = newWhenTrueStatement - .WithPrependedLeadingTrivia(ifStatement.GetLeadingTrivia()) - .WithAdditionalAnnotations(Formatter.Annotation); - } - else - { - newWhenTrueStatement = newWhenTrueStatement.WithLeadingTrivia(ifStatement.GetLeadingTrivia()); - } - - // If there's trailing comments on the original inner statement, then preserve that. Otherwise, - // replace it with the trailing trivia of hte original if-statement. - if (!newWhenTrueStatement.GetTrailingTrivia().Any(t => t.IsSingleOrMultiLineComment())) - newWhenTrueStatement = newWhenTrueStatement.WithTrailingTrivia(ifStatement.GetTrailingTrivia()); - - editor.ReplaceNode(ifStatement, newWhenTrueStatement); + newWhenTrueStatement = newWhenTrueStatement + .WithPrependedLeadingTrivia(ifStatement.GetLeadingTrivia()) + .WithAdditionalAnnotations(Formatter.Annotation); } else { - var coalesce = coalesceOrIfStatement; - // changing from `x ?? (x = y)` to `x ??= y` can change the type. Specifically, - // with nullable value types (`int?`) it could change from `int?` to `int`. - // - // Add an explicit cast to the original type to ensure semantics are preserved. - // Simplification engine can then remove it if it's not necessary. - var type = semanticModel.GetTypeInfo(coalesce, cancellationToken).Type; - - editor.ReplaceNode(coalesce, - (currentCoalesceNode, generator) => - { - var currentCoalesce = (BinaryExpressionSyntax)currentCoalesceNode; - var coalesceRight = (ParenthesizedExpressionSyntax)currentCoalesce.Right; - var assignment = (AssignmentExpressionSyntax)coalesceRight.Expression; - - var compoundOperator = SyntaxFactory.Token(SyntaxKind.QuestionQuestionEqualsToken); - var finalAssignment = SyntaxFactory.AssignmentExpression( - SyntaxKind.CoalesceAssignmentExpression, - assignment.Left, - compoundOperator.WithTriviaFrom(assignment.OperatorToken), - assignment.Right); - - return type == null || type.IsErrorType() - ? finalAssignment - : generator.CastExpression(type, finalAssignment); - }); + newWhenTrueStatement = newWhenTrueStatement.WithLeadingTrivia(ifStatement.GetLeadingTrivia()); } + + // If there's trailing comments on the original inner statement, then preserve that. Otherwise, + // replace it with the trailing trivia of hte original if-statement. + if (!newWhenTrueStatement.GetTrailingTrivia().Any(t => t.IsSingleOrMultiLineComment())) + newWhenTrueStatement = newWhenTrueStatement.WithTrailingTrivia(ifStatement.GetTrailingTrivia()); + + editor.ReplaceNode(ifStatement, newWhenTrueStatement); + } + else + { + var coalesce = coalesceOrIfStatement; + // changing from `x ?? (x = y)` to `x ??= y` can change the type. Specifically, + // with nullable value types (`int?`) it could change from `int?` to `int`. + // + // Add an explicit cast to the original type to ensure semantics are preserved. + // Simplification engine can then remove it if it's not necessary. + var type = semanticModel.GetTypeInfo(coalesce, cancellationToken).Type; + + editor.ReplaceNode(coalesce, + (currentCoalesceNode, generator) => + { + var currentCoalesce = (BinaryExpressionSyntax)currentCoalesceNode; + var coalesceRight = (ParenthesizedExpressionSyntax)currentCoalesce.Right; + var assignment = (AssignmentExpressionSyntax)coalesceRight.Expression; + + var compoundOperator = SyntaxFactory.Token(SyntaxKind.QuestionQuestionEqualsToken); + var finalAssignment = SyntaxFactory.AssignmentExpression( + SyntaxKind.CoalesceAssignmentExpression, + assignment.Left, + compoundOperator.WithTriviaFrom(assignment.OperatorToken), + assignment.Right); + + return type == null || type.IsErrorType() + ? finalAssignment + : generator.CastExpression(type, finalAssignment); + }); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionForAssignmentCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionForAssignmentCodeFixProvider.cs index 271230e3b04bb..a894c26ed3cc6 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionForAssignmentCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionForAssignmentCodeFixProvider.cs @@ -17,52 +17,51 @@ using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.UseConditionalExpression; -namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression +namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseConditionalExpressionForAssignment), Shared] +internal partial class CSharpUseConditionalExpressionForAssignmentCodeFixProvider + : AbstractUseConditionalExpressionForAssignmentCodeFixProvider< + StatementSyntax, IfStatementSyntax, LocalDeclarationStatementSyntax, VariableDeclaratorSyntax, ExpressionSyntax, ConditionalExpressionSyntax> { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseConditionalExpressionForAssignment), Shared] - internal partial class CSharpUseConditionalExpressionForAssignmentCodeFixProvider - : AbstractUseConditionalExpressionForAssignmentCodeFixProvider< - StatementSyntax, IfStatementSyntax, LocalDeclarationStatementSyntax, VariableDeclaratorSyntax, ExpressionSyntax, ConditionalExpressionSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseConditionalExpressionForAssignmentCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseConditionalExpressionForAssignmentCodeFixProvider() - { - } + } - protected override ISyntaxFacts SyntaxFacts - => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts SyntaxFacts + => CSharpSyntaxFacts.Instance; - protected override AbstractFormattingRule GetMultiLineFormattingRule() - => MultiLineConditionalExpressionFormattingRule.Instance; + protected override AbstractFormattingRule GetMultiLineFormattingRule() + => MultiLineConditionalExpressionFormattingRule.Instance; - protected override VariableDeclaratorSyntax WithInitializer(VariableDeclaratorSyntax variable, ExpressionSyntax value) - => variable.WithInitializer(SyntaxFactory.EqualsValueClause(value)); + protected override VariableDeclaratorSyntax WithInitializer(VariableDeclaratorSyntax variable, ExpressionSyntax value) + => variable.WithInitializer(SyntaxFactory.EqualsValueClause(value)); - protected override VariableDeclaratorSyntax GetDeclaratorSyntax(IVariableDeclaratorOperation declarator) - => (VariableDeclaratorSyntax)declarator.Syntax; + protected override VariableDeclaratorSyntax GetDeclaratorSyntax(IVariableDeclaratorOperation declarator) + => (VariableDeclaratorSyntax)declarator.Syntax; - protected override LocalDeclarationStatementSyntax AddSimplificationToType(LocalDeclarationStatementSyntax statement) - => statement.WithDeclaration(statement.Declaration.WithType( - statement.Declaration.Type.WithAdditionalAnnotations(Simplifier.Annotation))); + protected override LocalDeclarationStatementSyntax AddSimplificationToType(LocalDeclarationStatementSyntax statement) + => statement.WithDeclaration(statement.Declaration.WithType( + statement.Declaration.Type.WithAdditionalAnnotations(Simplifier.Annotation))); - protected override StatementSyntax WrapWithBlockIfAppropriate( - IfStatementSyntax ifStatement, StatementSyntax statement) + protected override StatementSyntax WrapWithBlockIfAppropriate( + IfStatementSyntax ifStatement, StatementSyntax statement) + { + if (ifStatement.Parent is ElseClauseSyntax && + ifStatement.Statement is BlockSyntax block) { - if (ifStatement.Parent is ElseClauseSyntax && - ifStatement.Statement is BlockSyntax block) - { - return block.WithStatements([statement]) - .WithAdditionalAnnotations(Formatter.Annotation); - } - - return statement; + return block.WithStatements([statement]) + .WithAdditionalAnnotations(Formatter.Annotation); } - protected override ExpressionSyntax ConvertToExpression(IThrowOperation throwOperation) - => CSharpUseConditionalExpressionHelpers.ConvertToExpression(throwOperation); - - protected override ISyntaxFormatting GetSyntaxFormatting() - => CSharpSyntaxFormatting.Instance; + return statement; } + + protected override ExpressionSyntax ConvertToExpression(IThrowOperation throwOperation) + => CSharpUseConditionalExpressionHelpers.ConvertToExpression(throwOperation); + + protected override ISyntaxFormatting GetSyntaxFormatting() + => CSharpSyntaxFormatting.Instance; } diff --git a/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionForReturnCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionForReturnCodeFixProvider.cs index f00b94fe540ab..f92caeab2fe45 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionForReturnCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionForReturnCodeFixProvider.cs @@ -14,57 +14,56 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.UseConditionalExpression; -namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression +namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseConditionalExpressionForReturn), Shared] +internal partial class CSharpUseConditionalExpressionForReturnCodeFixProvider + : AbstractUseConditionalExpressionForReturnCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseConditionalExpressionForReturn), Shared] - internal partial class CSharpUseConditionalExpressionForReturnCodeFixProvider - : AbstractUseConditionalExpressionForReturnCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseConditionalExpressionForReturnCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseConditionalExpressionForReturnCodeFixProvider() - { - } + } - protected override ISyntaxFacts SyntaxFacts - => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts SyntaxFacts + => CSharpSyntaxFacts.Instance; - protected override AbstractFormattingRule GetMultiLineFormattingRule() - => MultiLineConditionalExpressionFormattingRule.Instance; + protected override AbstractFormattingRule GetMultiLineFormattingRule() + => MultiLineConditionalExpressionFormattingRule.Instance; - protected override StatementSyntax WrapWithBlockIfAppropriate( - IfStatementSyntax ifStatement, StatementSyntax statement) + protected override StatementSyntax WrapWithBlockIfAppropriate( + IfStatementSyntax ifStatement, StatementSyntax statement) + { + if (ifStatement.Parent is ElseClauseSyntax && + ifStatement.Statement is BlockSyntax block) { - if (ifStatement.Parent is ElseClauseSyntax && - ifStatement.Statement is BlockSyntax block) - { - return block.WithStatements([statement]) - .WithAdditionalAnnotations(Formatter.Annotation); - } - - return statement; + return block.WithStatements([statement]) + .WithAdditionalAnnotations(Formatter.Annotation); } - protected override SyntaxNode WrapIfStatementIfNecessary(IConditionalOperation operation) - { - if (operation.Syntax is IfStatementSyntax { Condition: CheckedExpressionSyntax exp }) - return exp; - - return base.WrapIfStatementIfNecessary(operation); - } + return statement; + } - protected override ExpressionSyntax WrapReturnExpressionIfNecessary(ExpressionSyntax returnExpression, IOperation returnOperation) - { - if (returnOperation.Syntax is ReturnStatementSyntax { Expression: CheckedExpressionSyntax exp }) - return exp; + protected override SyntaxNode WrapIfStatementIfNecessary(IConditionalOperation operation) + { + if (operation.Syntax is IfStatementSyntax { Condition: CheckedExpressionSyntax exp }) + return exp; - return base.WrapReturnExpressionIfNecessary(returnExpression, returnOperation); - } + return base.WrapIfStatementIfNecessary(operation); + } - protected override ExpressionSyntax ConvertToExpression(IThrowOperation throwOperation) - => CSharpUseConditionalExpressionHelpers.ConvertToExpression(throwOperation); + protected override ExpressionSyntax WrapReturnExpressionIfNecessary(ExpressionSyntax returnExpression, IOperation returnOperation) + { + if (returnOperation.Syntax is ReturnStatementSyntax { Expression: CheckedExpressionSyntax exp }) + return exp; - protected override ISyntaxFormatting GetSyntaxFormatting() - => CSharpSyntaxFormatting.Instance; + return base.WrapReturnExpressionIfNecessary(returnExpression, returnOperation); } + + protected override ExpressionSyntax ConvertToExpression(IThrowOperation throwOperation) + => CSharpUseConditionalExpressionHelpers.ConvertToExpression(throwOperation); + + protected override ISyntaxFormatting GetSyntaxFormatting() + => CSharpSyntaxFormatting.Instance; } diff --git a/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionHelpers.cs b/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionHelpers.cs index 6b63a5d54ae71..2bb85ee1332c3 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionHelpers.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/CSharpUseConditionalExpressionHelpers.cs @@ -6,15 +6,14 @@ using Microsoft.CodeAnalysis.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression +namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression; + +internal static class CSharpUseConditionalExpressionHelpers { - internal static class CSharpUseConditionalExpressionHelpers + public static ExpressionSyntax ConvertToExpression(IThrowOperation throwOperation) { - public static ExpressionSyntax ConvertToExpression(IThrowOperation throwOperation) - { - var throwStatement = (ThrowStatementSyntax)throwOperation.Syntax; - RoslynDebug.Assert(throwStatement.Expression != null); - return SyntaxFactory.ThrowExpression(throwStatement.ThrowKeyword, throwStatement.Expression); - } + var throwStatement = (ThrowStatementSyntax)throwOperation.Syntax; + RoslynDebug.Assert(throwStatement.Expression != null); + return SyntaxFactory.ThrowExpression(throwStatement.ThrowKeyword, throwStatement.Expression); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/MultiLineConditionalExpressionFormattingRule.cs b/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/MultiLineConditionalExpressionFormattingRule.cs index 12f3a88dccbe9..7b9bdb36fc3ee 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/MultiLineConditionalExpressionFormattingRule.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/MultiLineConditionalExpressionFormattingRule.cs @@ -9,66 +9,65 @@ using Microsoft.CodeAnalysis.Formatting.Rules; using static Microsoft.CodeAnalysis.UseConditionalExpression.UseConditionalExpressionCodeFixHelpers; -namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression +namespace Microsoft.CodeAnalysis.CSharp.UseConditionalExpression; + +/// +/// Special formatting rule that will convert a conditional expression into the following +/// form if it has the on it: +/// +/// +/// var v = expr +/// ? whenTrue +/// : whenFalse +/// +/// +/// i.e. both branches will be on a newline, indented once from the parent indentation. +/// +internal class MultiLineConditionalExpressionFormattingRule : AbstractFormattingRule { - /// - /// Special formatting rule that will convert a conditional expression into the following - /// form if it has the on it: - /// - /// - /// var v = expr - /// ? whenTrue - /// : whenFalse - /// - /// - /// i.e. both branches will be on a newline, indented once from the parent indentation. - /// - internal class MultiLineConditionalExpressionFormattingRule : AbstractFormattingRule - { - public static readonly AbstractFormattingRule Instance = new MultiLineConditionalExpressionFormattingRule(); + public static readonly AbstractFormattingRule Instance = new MultiLineConditionalExpressionFormattingRule(); - private MultiLineConditionalExpressionFormattingRule() - { - } + private MultiLineConditionalExpressionFormattingRule() + { + } - private static bool IsQuestionOrColonOfNewConditional(SyntaxToken token) - => token.Kind() is SyntaxKind.QuestionToken or SyntaxKind.ColonToken && token.Parent.HasAnnotation(SpecializedFormattingAnnotation); + private static bool IsQuestionOrColonOfNewConditional(SyntaxToken token) + => token.Kind() is SyntaxKind.QuestionToken or SyntaxKind.ColonToken && token.Parent.HasAnnotation(SpecializedFormattingAnnotation); - public override AdjustNewLinesOperation GetAdjustNewLinesOperation( - in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + public override AdjustNewLinesOperation GetAdjustNewLinesOperation( + in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + { + if (IsQuestionOrColonOfNewConditional(currentToken)) { - if (IsQuestionOrColonOfNewConditional(currentToken)) - { - // We want to force the ? and : to each be put onto the following line. - return FormattingOperations.CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.ForceLines); - } - - return nextOperation.Invoke(in previousToken, in currentToken); + // We want to force the ? and : to each be put onto the following line. + return FormattingOperations.CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.ForceLines); } - public override void AddIndentBlockOperations( - List list, SyntaxNode node, in NextIndentBlockOperationAction nextOperation) + return nextOperation.Invoke(in previousToken, in currentToken); + } + + public override void AddIndentBlockOperations( + List list, SyntaxNode node, in NextIndentBlockOperationAction nextOperation) + { + if (node.HasAnnotation(SpecializedFormattingAnnotation) && + node is ConditionalExpressionSyntax conditional) { - if (node.HasAnnotation(SpecializedFormattingAnnotation) && - node is ConditionalExpressionSyntax conditional) + var statement = conditional.FirstAncestorOrSelf(); + if (statement != null) { - var statement = conditional.FirstAncestorOrSelf(); - if (statement != null) - { - var baseToken = statement.GetFirstToken(); + var baseToken = statement.GetFirstToken(); - // we want to indent the ? and : in one level from the containing statement. - list.Add(FormattingOperations.CreateRelativeIndentBlockOperation( - baseToken, conditional.QuestionToken, conditional.WhenTrue.GetLastToken(), - indentationDelta: 1, IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine)); - list.Add(FormattingOperations.CreateRelativeIndentBlockOperation( - baseToken, conditional.ColonToken, conditional.WhenFalse.GetLastToken(), - indentationDelta: 1, IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine)); - return; - } + // we want to indent the ? and : in one level from the containing statement. + list.Add(FormattingOperations.CreateRelativeIndentBlockOperation( + baseToken, conditional.QuestionToken, conditional.WhenTrue.GetLastToken(), + indentationDelta: 1, IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine)); + list.Add(FormattingOperations.CreateRelativeIndentBlockOperation( + baseToken, conditional.ColonToken, conditional.WhenFalse.GetLastToken(), + indentationDelta: 1, IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine)); + return; } - - nextOperation.Invoke(); } + + nextOperation.Invoke(); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseDeconstruction/CSharpUseDeconstructionCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseDeconstruction/CSharpUseDeconstructionCodeFixProvider.cs index 43d713b432ccb..388f25d9c52c8 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseDeconstruction/CSharpUseDeconstructionCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseDeconstruction/CSharpUseDeconstructionCodeFixProvider.cs @@ -19,173 +19,172 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UseDeconstruction +namespace Microsoft.CodeAnalysis.CSharp.UseDeconstruction; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseDeconstruction), Shared] +internal sealed class CSharpUseDeconstructionCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseDeconstruction), Shared] - internal sealed class CSharpUseDeconstructionCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpUseDeconstructionCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpUseDeconstructionCodeFixProvider() - { - } - - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseDeconstructionDiagnosticId]; + } - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Deconstruct_variable_declaration, nameof(CSharpAnalyzersResources.Deconstruct_variable_declaration)); - return Task.CompletedTask; - } + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseDeconstructionDiagnosticId]; - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var nodesToProcess = diagnostics.SelectAsArray(d => d.Location.FindToken(cancellationToken).Parent); - - // When doing a fix all, we have to avoid introducing the same name multiple times - // into the same scope. However, checking results after each change would be very - // expensive (lots of forking + new semantic models, etc.). So we use - // ApplyMethodBodySemanticEditsAsync to help out here. It will only do the forking - // if there are multiple results in the same method body. If there's only one - // result in a method body, we will just apply it without doing any extra analysis. - return editor.ApplyMethodBodySemanticEditsAsync( - document, nodesToProcess, - (semanticModel, node) => true, - (semanticModel, currentRoot, node) => UpdateRoot(document, semanticModel, currentRoot, node, cancellationToken), - cancellationToken); - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Deconstruct_variable_declaration, nameof(CSharpAnalyzersResources.Deconstruct_variable_declaration)); + return Task.CompletedTask; + } - private SyntaxNode UpdateRoot( - Document document, - SemanticModel semanticModel, - SyntaxNode root, - SyntaxNode node, - CancellationToken cancellationToken) - { - var editor = new SyntaxEditor(root, document.Project.Solution.Services); + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var nodesToProcess = diagnostics.SelectAsArray(d => d.Location.FindToken(cancellationToken).Parent); + + // When doing a fix all, we have to avoid introducing the same name multiple times + // into the same scope. However, checking results after each change would be very + // expensive (lots of forking + new semantic models, etc.). So we use + // ApplyMethodBodySemanticEditsAsync to help out here. It will only do the forking + // if there are multiple results in the same method body. If there's only one + // result in a method body, we will just apply it without doing any extra analysis. + return editor.ApplyMethodBodySemanticEditsAsync( + document, nodesToProcess, + (semanticModel, node) => true, + (semanticModel, currentRoot, node) => UpdateRoot(document, semanticModel, currentRoot, node, cancellationToken), + cancellationToken); + } - // We use the callback form of ReplaceNode because we may have nested code that - // needs to be updated in fix-all situations. For example, nested foreach statements. - // We need to see the results of the inner changes when making the outer changes. + private SyntaxNode UpdateRoot( + Document document, + SemanticModel semanticModel, + SyntaxNode root, + SyntaxNode node, + CancellationToken cancellationToken) + { + var editor = new SyntaxEditor(root, document.Project.Solution.Services); - ImmutableArray memberAccessExpressions = default; - if (node is VariableDeclaratorSyntax variableDeclarator) - { - var variableDeclaration = (VariableDeclarationSyntax)variableDeclarator.Parent; - if (CSharpUseDeconstructionDiagnosticAnalyzer.TryAnalyzeVariableDeclaration( - semanticModel, variableDeclaration, - out var tupleType, out memberAccessExpressions, - cancellationToken)) - { - editor.ReplaceNode( - variableDeclaration.Parent, - (current, _) => - { - var currentDeclarationStatement = (LocalDeclarationStatementSyntax)current; - return CreateDeconstructionStatement(tupleType, currentDeclarationStatement, currentDeclarationStatement.Declaration.Variables[0]); - }); - } - } - else if (node is ForEachStatementSyntax forEachStatement) - { - if (CSharpUseDeconstructionDiagnosticAnalyzer.TryAnalyzeForEachStatement( - semanticModel, forEachStatement, - out var tupleType, out memberAccessExpressions, - cancellationToken)) - { - editor.ReplaceNode( - forEachStatement, - (current, _) => CreateForEachVariableStatement(tupleType, (ForEachStatementSyntax)current)); - } - } + // We use the callback form of ReplaceNode because we may have nested code that + // needs to be updated in fix-all situations. For example, nested foreach statements. + // We need to see the results of the inner changes when making the outer changes. - foreach (var memberAccess in memberAccessExpressions.NullToEmpty()) + ImmutableArray memberAccessExpressions = default; + if (node is VariableDeclaratorSyntax variableDeclarator) + { + var variableDeclaration = (VariableDeclarationSyntax)variableDeclarator.Parent; + if (CSharpUseDeconstructionDiagnosticAnalyzer.TryAnalyzeVariableDeclaration( + semanticModel, variableDeclaration, + out var tupleType, out memberAccessExpressions, + cancellationToken)) { editor.ReplaceNode( - memberAccess, + variableDeclaration.Parent, (current, _) => { - var currentMemberAccess = (MemberAccessExpressionSyntax)current; - return currentMemberAccess.Name.WithTriviaFrom(currentMemberAccess); + var currentDeclarationStatement = (LocalDeclarationStatementSyntax)current; + return CreateDeconstructionStatement(tupleType, currentDeclarationStatement, currentDeclarationStatement.Declaration.Variables[0]); }); } - - return editor.GetChangedRoot(); } - - private ForEachVariableStatementSyntax CreateForEachVariableStatement(INamedTypeSymbol tupleType, ForEachStatementSyntax forEachStatement) + else if (node is ForEachStatementSyntax forEachStatement) { - // Copy all the tokens/nodes from the existing foreach statement to the new foreach statement. - // However, convert the existing declaration over to a "var (x, y)" declaration or (int x, int y) - // tuple expression. - return SyntaxFactory.ForEachVariableStatement( - forEachStatement.AttributeLists, - forEachStatement.AwaitKeyword, - forEachStatement.ForEachKeyword, - forEachStatement.OpenParenToken, - CreateTupleOrDeclarationExpression(tupleType, forEachStatement.Type), - forEachStatement.InKeyword, - forEachStatement.Expression, - forEachStatement.CloseParenToken, - forEachStatement.Statement); + if (CSharpUseDeconstructionDiagnosticAnalyzer.TryAnalyzeForEachStatement( + semanticModel, forEachStatement, + out var tupleType, out memberAccessExpressions, + cancellationToken)) + { + editor.ReplaceNode( + forEachStatement, + (current, _) => CreateForEachVariableStatement(tupleType, (ForEachStatementSyntax)current)); + } } - private ExpressionStatementSyntax CreateDeconstructionStatement( - INamedTypeSymbol tupleType, LocalDeclarationStatementSyntax declarationStatement, VariableDeclaratorSyntax variableDeclarator) + foreach (var memberAccess in memberAccessExpressions.NullToEmpty()) { - // Copy all the tokens/nodes from the existing declaration statement to the new assignment - // statement. However, convert the existing declaration over to a "var (x, y)" declaration - // or (int x, int y) tuple expression. - return SyntaxFactory.ExpressionStatement( - SyntaxFactory.AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - CreateTupleOrDeclarationExpression(tupleType, declarationStatement.Declaration.Type), - variableDeclarator.Initializer.EqualsToken, - variableDeclarator.Initializer.Value), - declarationStatement.SemicolonToken); + editor.ReplaceNode( + memberAccess, + (current, _) => + { + var currentMemberAccess = (MemberAccessExpressionSyntax)current; + return currentMemberAccess.Name.WithTriviaFrom(currentMemberAccess); + }); } - private ExpressionSyntax CreateTupleOrDeclarationExpression(INamedTypeSymbol tupleType, TypeSyntax typeNode) - { - // If we have an explicit tuple type in code, convert that over to a tuple expression. - // i.e. (int x, int y) t = ... will be converted to (int x, int y) = ... - // - // If we had the "var t" form we'll convert that to the declaration expression "var (x, y)" - return typeNode is TupleTypeSyntax tupleTypeSyntax - ? CreateTupleExpression(tupleTypeSyntax) - : CreateDeclarationExpression(tupleType, typeNode); - } + return editor.GetChangedRoot(); + } + + private ForEachVariableStatementSyntax CreateForEachVariableStatement(INamedTypeSymbol tupleType, ForEachStatementSyntax forEachStatement) + { + // Copy all the tokens/nodes from the existing foreach statement to the new foreach statement. + // However, convert the existing declaration over to a "var (x, y)" declaration or (int x, int y) + // tuple expression. + return SyntaxFactory.ForEachVariableStatement( + forEachStatement.AttributeLists, + forEachStatement.AwaitKeyword, + forEachStatement.ForEachKeyword, + forEachStatement.OpenParenToken, + CreateTupleOrDeclarationExpression(tupleType, forEachStatement.Type), + forEachStatement.InKeyword, + forEachStatement.Expression, + forEachStatement.CloseParenToken, + forEachStatement.Statement); + } - private static DeclarationExpressionSyntax CreateDeclarationExpression(INamedTypeSymbol tupleType, TypeSyntax typeNode) - => SyntaxFactory.DeclarationExpression( - typeNode, SyntaxFactory.ParenthesizedVariableDesignation( - [.. tupleType.TupleElements.Select( - e => SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier(e.Name.EscapeIdentifier())))])); + private ExpressionStatementSyntax CreateDeconstructionStatement( + INamedTypeSymbol tupleType, LocalDeclarationStatementSyntax declarationStatement, VariableDeclaratorSyntax variableDeclarator) + { + // Copy all the tokens/nodes from the existing declaration statement to the new assignment + // statement. However, convert the existing declaration over to a "var (x, y)" declaration + // or (int x, int y) tuple expression. + return SyntaxFactory.ExpressionStatement( + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + CreateTupleOrDeclarationExpression(tupleType, declarationStatement.Declaration.Type), + variableDeclarator.Initializer.EqualsToken, + variableDeclarator.Initializer.Value), + declarationStatement.SemicolonToken); + } - private TupleExpressionSyntax CreateTupleExpression(TupleTypeSyntax typeNode) - => SyntaxFactory.TupleExpression( - typeNode.OpenParenToken, - SyntaxFactory.SeparatedList(new SyntaxNodeOrTokenList(typeNode.Elements.GetWithSeparators().Select(ConvertTupleTypeElementComponent))), - typeNode.CloseParenToken); + private ExpressionSyntax CreateTupleOrDeclarationExpression(INamedTypeSymbol tupleType, TypeSyntax typeNode) + { + // If we have an explicit tuple type in code, convert that over to a tuple expression. + // i.e. (int x, int y) t = ... will be converted to (int x, int y) = ... + // + // If we had the "var t" form we'll convert that to the declaration expression "var (x, y)" + return typeNode is TupleTypeSyntax tupleTypeSyntax + ? CreateTupleExpression(tupleTypeSyntax) + : CreateDeclarationExpression(tupleType, typeNode); + } - private SyntaxNodeOrToken ConvertTupleTypeElementComponent(SyntaxNodeOrToken nodeOrToken) - { - if (nodeOrToken.IsToken) - { - // return commas directly as is. - return nodeOrToken; - } + private static DeclarationExpressionSyntax CreateDeclarationExpression(INamedTypeSymbol tupleType, TypeSyntax typeNode) + => SyntaxFactory.DeclarationExpression( + typeNode, SyntaxFactory.ParenthesizedVariableDesignation( + [.. tupleType.TupleElements.Select( + e => SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier(e.Name.EscapeIdentifier())))])); + + private TupleExpressionSyntax CreateTupleExpression(TupleTypeSyntax typeNode) + => SyntaxFactory.TupleExpression( + typeNode.OpenParenToken, + SyntaxFactory.SeparatedList(new SyntaxNodeOrTokenList(typeNode.Elements.GetWithSeparators().Select(ConvertTupleTypeElementComponent))), + typeNode.CloseParenToken); - // "int x" as a tuple element directly translates to "int x" (a declaration expression - // with a variable designation 'x'). - var node = (TupleElementSyntax)nodeOrToken.AsNode(); - return SyntaxFactory.Argument( - SyntaxFactory.DeclarationExpression( - node.Type, - SyntaxFactory.SingleVariableDesignation(node.Identifier))); + private SyntaxNodeOrToken ConvertTupleTypeElementComponent(SyntaxNodeOrToken nodeOrToken) + { + if (nodeOrToken.IsToken) + { + // return commas directly as is. + return nodeOrToken; } + + // "int x" as a tuple element directly translates to "int x" (a declaration expression + // with a variable designation 'x'). + var node = (TupleElementSyntax)nodeOrToken.AsNode(); + return SyntaxFactory.Argument( + SyntaxFactory.DeclarationExpression( + node.Type, + SyntaxFactory.SingleVariableDesignation(node.Identifier))); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseDefaultLiteral/CSharpUseDefaultLiteralCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseDefaultLiteral/CSharpUseDefaultLiteralCodeFixProvider.cs index 76ab20a69ff10..dc3da197868e1 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseDefaultLiteral/CSharpUseDefaultLiteralCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseDefaultLiteral/CSharpUseDefaultLiteralCodeFixProvider.cs @@ -16,54 +16,53 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UseDefaultLiteral +namespace Microsoft.CodeAnalysis.CSharp.UseDefaultLiteral; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseDefaultLiteral), Shared] +internal partial class CSharpUseDefaultLiteralCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseDefaultLiteral), Shared] - internal partial class CSharpUseDefaultLiteralCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpUseDefaultLiteralCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpUseDefaultLiteralCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } - = [IDEDiagnosticIds.UseDefaultLiteralDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds { get; } + = [IDEDiagnosticIds.UseDefaultLiteralDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Simplify_default_expression, nameof(CSharpAnalyzersResources.Simplify_default_expression)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Simplify_default_expression, nameof(CSharpAnalyzersResources.Simplify_default_expression)); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - // Fix-All for this feature is somewhat complicated. Each time we fix one case, it - // may make the next case unfixable. For example: - // - // 'var v = x ? default(string) : default(string)'. - // - // Here, we can replace either of the default expressions, but not both. So we have - // to replace one at a time, and only actually replace if it's still safe to do so. + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + // Fix-All for this feature is somewhat complicated. Each time we fix one case, it + // may make the next case unfixable. For example: + // + // 'var v = x ? default(string) : default(string)'. + // + // Here, we can replace either of the default expressions, but not both. So we have + // to replace one at a time, and only actually replace if it's still safe to do so. - var options = (CSharpAnalyzerOptionsProvider)await document.GetAnalyzerOptionsProviderAsync(cancellationToken).ConfigureAwait(false); - var preferSimpleDefaultExpression = options.PreferSimpleDefaultExpression.Value; + var options = (CSharpAnalyzerOptionsProvider)await document.GetAnalyzerOptionsProviderAsync(cancellationToken).ConfigureAwait(false); + var preferSimpleDefaultExpression = options.PreferSimpleDefaultExpression.Value; - var originalRoot = editor.OriginalRoot; - var parseOptions = (CSharpParseOptions)originalRoot.SyntaxTree.Options; + var originalRoot = editor.OriginalRoot; + var parseOptions = (CSharpParseOptions)originalRoot.SyntaxTree.Options; - var originalNodes = diagnostics.SelectAsArray( - d => (DefaultExpressionSyntax)originalRoot.FindNode(d.Location.SourceSpan, getInnermostNodeForTie: true)); + var originalNodes = diagnostics.SelectAsArray( + d => (DefaultExpressionSyntax)originalRoot.FindNode(d.Location.SourceSpan, getInnermostNodeForTie: true)); - await editor.ApplyExpressionLevelSemanticEditsAsync( - document, originalNodes, - (semanticModel, defaultExpression) => defaultExpression.CanReplaceWithDefaultLiteral(parseOptions, preferSimpleDefaultExpression, semanticModel, cancellationToken), - (_, currentRoot, defaultExpression) => currentRoot.ReplaceNode( - defaultExpression, - SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression).WithTriviaFrom(defaultExpression)), - cancellationToken).ConfigureAwait(false); - } + await editor.ApplyExpressionLevelSemanticEditsAsync( + document, originalNodes, + (semanticModel, defaultExpression) => defaultExpression.CanReplaceWithDefaultLiteral(parseOptions, preferSimpleDefaultExpression, semanticModel, cancellationToken), + (_, currentRoot, defaultExpression) => currentRoot.ReplaceNode( + defaultExpression, + SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression).WithTriviaFrom(defaultExpression)), + cancellationToken).ConfigureAwait(false); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseExplicitTypeForConst/UseExplicitTypeForConstCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseExplicitTypeForConst/UseExplicitTypeForConstCodeFixProvider.cs index 83f6c47e41be3..18f715432edf0 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseExplicitTypeForConst/UseExplicitTypeForConstCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseExplicitTypeForConst/UseExplicitTypeForConstCodeFixProvider.cs @@ -15,62 +15,61 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.UseExplicitTypeForConst +namespace Microsoft.CodeAnalysis.CSharp.UseExplicitTypeForConst; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseExplicitTypeForConst), Shared] +internal sealed class UseExplicitTypeForConstCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseExplicitTypeForConst), Shared] - internal sealed class UseExplicitTypeForConstCodeFixProvider : CodeFixProvider + private const string CS0822 = nameof(CS0822); // Implicitly-typed variables cannot be constant + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public UseExplicitTypeForConstCodeFixProvider() { - private const string CS0822 = nameof(CS0822); // Implicitly-typed variables cannot be constant + } - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public UseExplicitTypeForConstCodeFixProvider() - { - } + public override ImmutableArray FixableDiagnosticIds { get; } = [CS0822]; - public override ImmutableArray FixableDiagnosticIds { get; } = [CS0822]; + public override FixAllProvider? GetFixAllProvider() + { + // This code fix addresses a very specific compiler error. It's unlikely there will be more than 1 of them at a time. + return null; + } - public override FixAllProvider? GetFixAllProvider() - { - // This code fix addresses a very specific compiler error. It's unlikely there will be more than 1 of them at a time. - return null; - } + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var cancellationToken = context.CancellationToken; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var cancellationToken = context.CancellationToken; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root.FindNode(context.Span) is VariableDeclarationSyntax variableDeclaration && + variableDeclaration.Variables.Count == 1) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (root.FindNode(context.Span) is VariableDeclarationSyntax variableDeclaration && - variableDeclaration.Variables.Count == 1) + var type = semanticModel.GetTypeInfo(variableDeclaration.Type, cancellationToken).ConvertedType; + if (type == null || type.TypeKind == TypeKind.Error || type.IsAnonymousType) { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var type = semanticModel.GetTypeInfo(variableDeclaration.Type, cancellationToken).ConvertedType; - if (type == null || type.TypeKind == TypeKind.Error || type.IsAnonymousType) - { - return; - } - - context.RegisterCodeFix( - CodeAction.Create( - CSharpAnalyzersResources.Use_explicit_type_instead_of_var, - c => FixAsync(document, context.Span, type, c), - nameof(CSharpAnalyzersResources.Use_explicit_type_instead_of_var)), - context.Diagnostics); + return; } + + context.RegisterCodeFix( + CodeAction.Create( + CSharpAnalyzersResources.Use_explicit_type_instead_of_var, + c => FixAsync(document, context.Span, type, c), + nameof(CSharpAnalyzersResources.Use_explicit_type_instead_of_var)), + context.Diagnostics); } + } - private static async Task FixAsync( - Document document, TextSpan span, ITypeSymbol type, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var variableDeclaration = (VariableDeclarationSyntax)root.FindNode(span); + private static async Task FixAsync( + Document document, TextSpan span, ITypeSymbol type, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var variableDeclaration = (VariableDeclarationSyntax)root.FindNode(span); - var newRoot = root.ReplaceNode(variableDeclaration.Type, type.GenerateTypeSyntax(allowVar: false)); - return document.WithSyntaxRoot(newRoot); - } + var newRoot = root.ReplaceNode(variableDeclaration.Type, type.GenerateTypeSyntax(allowVar: false)); + return document.WithSyntaxRoot(newRoot); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseExpressionBody/UseExpressionBodyCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseExpressionBody/UseExpressionBodyCodeFixProvider.cs index 66b19132b4bb3..e26f88caadee0 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseExpressionBody/UseExpressionBodyCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseExpressionBody/UseExpressionBodyCodeFixProvider.cs @@ -20,79 +20,78 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseExpressionBody), Shared] +internal partial class UseExpressionBodyCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseExpressionBody), Shared] - internal partial class UseExpressionBodyCodeFixProvider : SyntaxEditorBasedCodeFixProvider - { - public sealed override ImmutableArray FixableDiagnosticIds { get; } + public sealed override ImmutableArray FixableDiagnosticIds { get; } - private static readonly ImmutableArray _helpers = UseExpressionBodyHelper.Helpers; + private static readonly ImmutableArray _helpers = UseExpressionBodyHelper.Helpers; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public UseExpressionBodyCodeFixProvider() - => FixableDiagnosticIds = _helpers.SelectAsArray(h => h.DiagnosticId); + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public UseExpressionBodyCodeFixProvider() + => FixableDiagnosticIds = _helpers.SelectAsArray(h => h.DiagnosticId); - protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) - => !diagnostic.IsSuppressed || - diagnostic.Properties.ContainsKey(UseExpressionBodyDiagnosticAnalyzer.FixesError); + protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) + => !diagnostic.IsSuppressed || + diagnostic.Properties.ContainsKey(UseExpressionBodyDiagnosticAnalyzer.FixesError); - public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var diagnostic = context.Diagnostics.First(); + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); - var priority = diagnostic.Severity == DiagnosticSeverity.Hidden - ? CodeActionPriority.Low - : CodeActionPriority.Default; + var priority = diagnostic.Severity == DiagnosticSeverity.Hidden + ? CodeActionPriority.Low + : CodeActionPriority.Default; - var title = diagnostic.GetMessage(); + var title = diagnostic.GetMessage(); - RegisterCodeFix(context, title, title, priority); - return Task.CompletedTask; - } + RegisterCodeFix(context, title, title, priority); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var accessorLists = new HashSet(); + foreach (var diagnostic in diagnostics) { - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var accessorLists = new HashSet(); - foreach (var diagnostic in diagnostics) - { - cancellationToken.ThrowIfCancellationRequested(); - AddEdits(semanticModel, editor, diagnostic, accessorLists, cancellationToken); - } - - // Ensure that if we changed any accessors that the accessor lists they're contained - // in are formatted properly as well. Do this as a last pass so that we see all - // individual changes made to the child accessors if we're doing a fix-all. - foreach (var accessorList in accessorLists) - { - editor.ReplaceNode(accessorList, (current, _) => current.WithAdditionalAnnotations(Formatter.Annotation)); - } + cancellationToken.ThrowIfCancellationRequested(); + AddEdits(semanticModel, editor, diagnostic, accessorLists, cancellationToken); } - private static void AddEdits( - SemanticModel semanticModel, SyntaxEditor editor, Diagnostic diagnostic, - HashSet accessorLists, - CancellationToken cancellationToken) + // Ensure that if we changed any accessors that the accessor lists they're contained + // in are formatted properly as well. Do this as a last pass so that we see all + // individual changes made to the child accessors if we're doing a fix-all. + foreach (var accessorList in accessorLists) { - var declarationLocation = diagnostic.AdditionalLocations[0]; - var helper = _helpers.Single(h => h.DiagnosticId == diagnostic.Id); - var declaration = declarationLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); - var useExpressionBody = diagnostic.Properties.ContainsKey(nameof(UseExpressionBody)); + editor.ReplaceNode(accessorList, (current, _) => current.WithAdditionalAnnotations(Formatter.Annotation)); + } + } - var updatedDeclaration = helper.Update(semanticModel, declaration, useExpressionBody, cancellationToken) - .WithAdditionalAnnotations(Formatter.Annotation); + private static void AddEdits( + SemanticModel semanticModel, SyntaxEditor editor, Diagnostic diagnostic, + HashSet accessorLists, + CancellationToken cancellationToken) + { + var declarationLocation = diagnostic.AdditionalLocations[0]; + var helper = _helpers.Single(h => h.DiagnosticId == diagnostic.Id); + var declaration = declarationLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); + var useExpressionBody = diagnostic.Properties.ContainsKey(nameof(UseExpressionBody)); + + var updatedDeclaration = helper.Update(semanticModel, declaration, useExpressionBody, cancellationToken) + .WithAdditionalAnnotations(Formatter.Annotation); - editor.ReplaceNode(declaration, updatedDeclaration); + editor.ReplaceNode(declaration, updatedDeclaration); - if (declaration.Parent is AccessorListSyntax accessorList) - { - accessorLists.Add(accessorList); - } + if (declaration.Parent is AccessorListSyntax accessorList) + { + accessorLists.Add(accessorList); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeActionHelpers.cs b/src/Analyzers/CSharp/CodeFixes/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeActionHelpers.cs index 1ee2dccfde90c..1eed151ee7e16 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeActionHelpers.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeActionHelpers.cs @@ -12,98 +12,97 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.UseExpressionBodyForLambda +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.UseExpressionBodyForLambda; + +internal static class UseExpressionBodyForLambdaCodeActionHelpers { - internal static class UseExpressionBodyForLambdaCodeActionHelpers + internal static LambdaExpressionSyntax Update(SemanticModel semanticModel, LambdaExpressionSyntax originalDeclaration, LambdaExpressionSyntax currentDeclaration, CancellationToken cancellationToken) + => UpdateWorker(semanticModel, originalDeclaration, currentDeclaration, cancellationToken).WithAdditionalAnnotations(Formatter.Annotation); + + private static LambdaExpressionSyntax UpdateWorker( + SemanticModel semanticModel, LambdaExpressionSyntax originalDeclaration, LambdaExpressionSyntax currentDeclaration, CancellationToken cancellationToken) { - internal static LambdaExpressionSyntax Update(SemanticModel semanticModel, LambdaExpressionSyntax originalDeclaration, LambdaExpressionSyntax currentDeclaration, CancellationToken cancellationToken) - => UpdateWorker(semanticModel, originalDeclaration, currentDeclaration, cancellationToken).WithAdditionalAnnotations(Formatter.Annotation); + var expressionBody = UseExpressionBodyForLambdaHelpers.GetBodyAsExpression(currentDeclaration); + return expressionBody == null + ? WithExpressionBody(currentDeclaration, originalDeclaration.GetLanguageVersion(), cancellationToken) + : WithBlockBody(semanticModel, originalDeclaration, currentDeclaration, expressionBody); + } - private static LambdaExpressionSyntax UpdateWorker( - SemanticModel semanticModel, LambdaExpressionSyntax originalDeclaration, LambdaExpressionSyntax currentDeclaration, CancellationToken cancellationToken) + private static LambdaExpressionSyntax WithExpressionBody(LambdaExpressionSyntax declaration, LanguageVersion languageVersion, CancellationToken cancellationToken) + { + if (!UseExpressionBodyForLambdaHelpers.TryConvertToExpressionBody( + declaration, languageVersion, ExpressionBodyPreference.WhenPossible, cancellationToken, out var expressionBody)) { - var expressionBody = UseExpressionBodyForLambdaHelpers.GetBodyAsExpression(currentDeclaration); - return expressionBody == null - ? WithExpressionBody(currentDeclaration, originalDeclaration.GetLanguageVersion(), cancellationToken) - : WithBlockBody(semanticModel, originalDeclaration, currentDeclaration, expressionBody); + return declaration; } - private static LambdaExpressionSyntax WithExpressionBody(LambdaExpressionSyntax declaration, LanguageVersion languageVersion, CancellationToken cancellationToken) - { - if (!UseExpressionBodyForLambdaHelpers.TryConvertToExpressionBody( - declaration, languageVersion, ExpressionBodyPreference.WhenPossible, cancellationToken, out var expressionBody)) - { - return declaration; - } + var updatedDecl = declaration.WithBody(expressionBody); - var updatedDecl = declaration.WithBody(expressionBody); + // If there will only be whitespace between the arrow and the body, then replace that + // with a single space so that the lambda doesn't have superfluous newlines in it. + if (declaration.ArrowToken.TrailingTrivia.All(t => t.IsWhitespaceOrEndOfLine()) && + expressionBody.GetLeadingTrivia().All(t => t.IsWhitespaceOrEndOfLine())) + { + updatedDecl = updatedDecl.WithArrowToken(updatedDecl.ArrowToken.WithTrailingTrivia(SyntaxFactory.ElasticSpace)); + } - // If there will only be whitespace between the arrow and the body, then replace that - // with a single space so that the lambda doesn't have superfluous newlines in it. - if (declaration.ArrowToken.TrailingTrivia.All(t => t.IsWhitespaceOrEndOfLine()) && - expressionBody.GetLeadingTrivia().All(t => t.IsWhitespaceOrEndOfLine())) - { - updatedDecl = updatedDecl.WithArrowToken(updatedDecl.ArrowToken.WithTrailingTrivia(SyntaxFactory.ElasticSpace)); - } + return updatedDecl; + } - return updatedDecl; - } + private static LambdaExpressionSyntax WithBlockBody( + SemanticModel semanticModel, LambdaExpressionSyntax originalDeclaration, LambdaExpressionSyntax currentDeclaration, ExpressionSyntax expressionBody) + { + var createReturnStatementForExpression = CreateReturnStatementForExpression( + semanticModel, originalDeclaration); - private static LambdaExpressionSyntax WithBlockBody( - SemanticModel semanticModel, LambdaExpressionSyntax originalDeclaration, LambdaExpressionSyntax currentDeclaration, ExpressionSyntax expressionBody) + if (!expressionBody.TryConvertToStatement( + semicolonTokenOpt: null, + createReturnStatementForExpression, + out var statement)) { - var createReturnStatementForExpression = CreateReturnStatementForExpression( - semanticModel, originalDeclaration); + return currentDeclaration; + } - if (!expressionBody.TryConvertToStatement( - semicolonTokenOpt: null, - createReturnStatementForExpression, - out var statement)) - { - return currentDeclaration; - } + // If the user is converting to a block, it's likely they intend to add multiple + // statements to it. So make a multi-line block so that things are formatted properly + // for them to do so. + return currentDeclaration.WithBody(SyntaxFactory.Block( + SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithAppendedTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed), + [statement], + SyntaxFactory.Token(SyntaxKind.CloseBraceToken))); + } - // If the user is converting to a block, it's likely they intend to add multiple - // statements to it. So make a multi-line block so that things are formatted properly - // for them to do so. - return currentDeclaration.WithBody(SyntaxFactory.Block( - SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithAppendedTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed), - [statement], - SyntaxFactory.Token(SyntaxKind.CloseBraceToken))); + private static bool CreateReturnStatementForExpression( + SemanticModel semanticModel, LambdaExpressionSyntax declaration) + { + var lambdaType = (INamedTypeSymbol)semanticModel.GetTypeInfo(declaration).ConvertedType!; + if (lambdaType.DelegateInvokeMethod!.ReturnsVoid) + { + return false; } - private static bool CreateReturnStatementForExpression( - SemanticModel semanticModel, LambdaExpressionSyntax declaration) + // 'async Task' is effectively a void-returning lambda. we do not want to create + // 'return statements' when converting. + if (declaration.AsyncKeyword != default) { - var lambdaType = (INamedTypeSymbol)semanticModel.GetTypeInfo(declaration).ConvertedType!; - if (lambdaType.DelegateInvokeMethod!.ReturnsVoid) + var returnType = lambdaType.DelegateInvokeMethod.ReturnType; + if (returnType.IsErrorType()) { - return false; + // "async Goo" where 'Goo' failed to bind. If 'Goo' is 'Task' then it's + // reasonable to assume this is just a missing 'using' and that this is a true + // "async Task" lambda. If the name isn't 'Task', then this looks like a + // real return type, and we should use return statements. + return returnType.Name != nameof(Task); } - // 'async Task' is effectively a void-returning lambda. we do not want to create - // 'return statements' when converting. - if (declaration.AsyncKeyword != default) + var taskType = semanticModel.Compilation.GetTypeByMetadataName(typeof(Task).FullName!); + if (returnType.Equals(taskType)) { - var returnType = lambdaType.DelegateInvokeMethod.ReturnType; - if (returnType.IsErrorType()) - { - // "async Goo" where 'Goo' failed to bind. If 'Goo' is 'Task' then it's - // reasonable to assume this is just a missing 'using' and that this is a true - // "async Task" lambda. If the name isn't 'Task', then this looks like a - // real return type, and we should use return statements. - return returnType.Name != nameof(Task); - } - - var taskType = semanticModel.Compilation.GetTypeByMetadataName(typeof(Task).FullName!); - if (returnType.Equals(taskType)) - { - // 'async Task'. definitely do not create a 'return' statement; - return false; - } + // 'async Task'. definitely do not create a 'return' statement; + return false; } - - return true; } + + return true; } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeFixProvider.cs index 7a3c6098bdff7..acd9a5861d81a 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeFixProvider.cs @@ -19,61 +19,60 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBodyForLambda +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBodyForLambda; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseExpressionBodyForLambda), Shared] +internal sealed class UseExpressionBodyForLambdaCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseExpressionBodyForLambda), Shared] - internal sealed class UseExpressionBodyForLambdaCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public UseExpressionBodyForLambdaCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public UseExpressionBodyForLambdaCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } = [IDEDiagnosticIds.UseExpressionBodyForLambdaExpressionsDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds { get; } = [IDEDiagnosticIds.UseExpressionBodyForLambdaExpressionsDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var diagnostic = context.Diagnostics[0]; + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var diagnostic = context.Diagnostics[0]; - var title = diagnostic.GetMessage(); - var codeAction = CodeAction.Create( - title, - c => FixWithSyntaxEditorAsync(document, diagnostic, c), - title); + var title = diagnostic.GetMessage(); + var codeAction = CodeAction.Create( + title, + c => FixWithSyntaxEditorAsync(document, diagnostic, c), + title); - context.RegisterCodeFix(codeAction, context.Diagnostics); - return Task.CompletedTask; - } + context.RegisterCodeFix(codeAction, context.Diagnostics); + return Task.CompletedTask; + } - protected override Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - => FixAllAsync(document, diagnostics, editor, cancellationToken); + protected override Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + => FixAllAsync(document, diagnostics, editor, cancellationToken); - private static async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CancellationToken cancellationToken) + private static async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + foreach (var diagnostic in diagnostics) { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - foreach (var diagnostic in diagnostics) - { - cancellationToken.ThrowIfCancellationRequested(); - AddEdits(editor, semanticModel, diagnostic, cancellationToken); - } + cancellationToken.ThrowIfCancellationRequested(); + AddEdits(editor, semanticModel, diagnostic, cancellationToken); } + } - private static Task FixWithSyntaxEditorAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) - => FixAllWithEditorAsync( - document, editor => FixAllAsync(document, [diagnostic], editor, cancellationToken), cancellationToken); + private static Task FixWithSyntaxEditorAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + => FixAllWithEditorAsync( + document, editor => FixAllAsync(document, [diagnostic], editor, cancellationToken), cancellationToken); - private static void AddEdits( - SyntaxEditor editor, SemanticModel semanticModel, - Diagnostic diagnostic, CancellationToken cancellationToken) - { - var declarationLocation = diagnostic.AdditionalLocations[0]; - var originalDeclaration = (LambdaExpressionSyntax)declarationLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); + private static void AddEdits( + SyntaxEditor editor, SemanticModel semanticModel, + Diagnostic diagnostic, CancellationToken cancellationToken) + { + var declarationLocation = diagnostic.AdditionalLocations[0]; + var originalDeclaration = (LambdaExpressionSyntax)declarationLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); - editor.ReplaceNode( - originalDeclaration, - (current, _) => UseExpressionBodyForLambdaCodeActionHelpers.Update(semanticModel, originalDeclaration, (LambdaExpressionSyntax)current, cancellationToken)); - } + editor.ReplaceNode( + originalDeclaration, + (current, _) => UseExpressionBodyForLambdaCodeActionHelpers.Update(semanticModel, originalDeclaration, (LambdaExpressionSyntax)current, cancellationToken)); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseImplicitOrExplicitType/UseExplicitTypeCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseImplicitOrExplicitType/UseExplicitTypeCodeFixProvider.cs index 5fed1288553b0..81e2b0a3a5647 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseImplicitOrExplicitType/UseExplicitTypeCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseImplicitOrExplicitType/UseExplicitTypeCodeFixProvider.cs @@ -20,213 +20,212 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.TypeStyle +namespace Microsoft.CodeAnalysis.CSharp.TypeStyle; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseExplicitType), Shared] +internal class UseExplicitTypeCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseExplicitType), Shared] - internal class UseExplicitTypeCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public UseExplicitTypeCodeFixProvider() + { + } + + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseExplicitTypeDiagnosticId]; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public UseExplicitTypeCodeFixProvider() + RegisterCodeFix(context, CSharpAnalyzersResources.Use_explicit_type_instead_of_var, nameof(CSharpAnalyzersResources.Use_explicit_type_instead_of_var)); + return Task.CompletedTask; + } + + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var root = editor.OriginalRoot; + + foreach (var diagnostic in diagnostics) { + var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + await HandleDeclarationAsync(document, editor, node, cancellationToken).ConfigureAwait(false); } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseExplicitTypeDiagnosticId]; + internal static async Task HandleDeclarationAsync( + Document document, SyntaxEditor editor, + SyntaxNode node, CancellationToken cancellationToken) + { + var declarationContext = node.Parent; - public override Task RegisterCodeFixesAsync(CodeFixContext context) + if (declarationContext is RefTypeSyntax) { - RegisterCodeFix(context, CSharpAnalyzersResources.Use_explicit_type_instead_of_var, nameof(CSharpAnalyzersResources.Use_explicit_type_instead_of_var)); - return Task.CompletedTask; + declarationContext = declarationContext.Parent; } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + if (declarationContext is VariableDeclarationSyntax varDecl) { - var root = editor.OriginalRoot; - - foreach (var diagnostic in diagnostics) - { - var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); - await HandleDeclarationAsync(document, editor, node, cancellationToken).ConfigureAwait(false); - } + await HandleVariableDeclarationAsync(document, editor, varDecl, cancellationToken).ConfigureAwait(false); } - - internal static async Task HandleDeclarationAsync( - Document document, SyntaxEditor editor, - SyntaxNode node, CancellationToken cancellationToken) + else if (declarationContext is ForEachStatementSyntax forEach) { - var declarationContext = node.Parent; - - if (declarationContext is RefTypeSyntax) - { - declarationContext = declarationContext.Parent; - } - - if (declarationContext is VariableDeclarationSyntax varDecl) - { - await HandleVariableDeclarationAsync(document, editor, varDecl, cancellationToken).ConfigureAwait(false); - } - else if (declarationContext is ForEachStatementSyntax forEach) - { - await HandleForEachStatementAsync(document, editor, forEach, cancellationToken).ConfigureAwait(false); - } - else if (declarationContext is DeclarationExpressionSyntax declarationExpression) - { - await HandleDeclarationExpressionAsync(document, editor, declarationExpression, cancellationToken).ConfigureAwait(false); - } - else - { - throw ExceptionUtilities.UnexpectedValue(declarationContext?.Kind()); - } + await HandleForEachStatementAsync(document, editor, forEach, cancellationToken).ConfigureAwait(false); } - - private static async Task HandleDeclarationExpressionAsync(Document document, SyntaxEditor editor, DeclarationExpressionSyntax declarationExpression, CancellationToken cancellationToken) + else if (declarationContext is DeclarationExpressionSyntax declarationExpression) + { + await HandleDeclarationExpressionAsync(document, editor, declarationExpression, cancellationToken).ConfigureAwait(false); + } + else { - var typeSyntax = declarationExpression.Type; - typeSyntax = typeSyntax.StripRefIfNeeded(); + throw ExceptionUtilities.UnexpectedValue(declarationContext?.Kind()); + } + } - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + private static async Task HandleDeclarationExpressionAsync(Document document, SyntaxEditor editor, DeclarationExpressionSyntax declarationExpression, CancellationToken cancellationToken) + { + var typeSyntax = declarationExpression.Type; + typeSyntax = typeSyntax.StripRefIfNeeded(); - if (declarationExpression.Designation is ParenthesizedVariableDesignationSyntax variableDesignation) - { - RoslynDebug.AssertNotNull(typeSyntax.Parent); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var tupleTypeSymbol = GetConvertedType(semanticModel, typeSyntax.Parent, cancellationToken); + if (declarationExpression.Designation is ParenthesizedVariableDesignationSyntax variableDesignation) + { + RoslynDebug.AssertNotNull(typeSyntax.Parent); - var leadingTrivia = declarationExpression.GetLeadingTrivia() - .Concat(variableDesignation.GetAllPrecedingTriviaToPreviousToken().Where(t => !t.IsWhitespace()).Select(t => t.WithoutAnnotations(SyntaxAnnotation.ElasticAnnotation))); + var tupleTypeSymbol = GetConvertedType(semanticModel, typeSyntax.Parent, cancellationToken); - var tupleDeclaration = GenerateTupleDeclaration(tupleTypeSymbol, variableDesignation).WithLeadingTrivia(leadingTrivia); + var leadingTrivia = declarationExpression.GetLeadingTrivia() + .Concat(variableDesignation.GetAllPrecedingTriviaToPreviousToken().Where(t => !t.IsWhitespace()).Select(t => t.WithoutAnnotations(SyntaxAnnotation.ElasticAnnotation))); - editor.ReplaceNode(declarationExpression, tupleDeclaration); - } - else - { - var typeSymbol = GetConvertedType(semanticModel, typeSyntax, cancellationToken); - editor.ReplaceNode(typeSyntax, GenerateTypeDeclaration(typeSyntax, typeSymbol)); - } - } + var tupleDeclaration = GenerateTupleDeclaration(tupleTypeSymbol, variableDesignation).WithLeadingTrivia(leadingTrivia); - private static Task HandleForEachStatementAsync(Document document, SyntaxEditor editor, ForEachStatementSyntax forEach, CancellationToken cancellationToken) - => UpdateTypeSyntaxAsync( - document, - editor, - forEach.Type, - forEach.Identifier.GetRequiredParent(), - cancellationToken); - - private static Task HandleVariableDeclarationAsync(Document document, SyntaxEditor editor, VariableDeclarationSyntax varDecl, CancellationToken cancellationToken) - => UpdateTypeSyntaxAsync( - document, - editor, - varDecl.Type, - // Since we're only dealing with variable declaration using var, we know - // that implicitly typed variables cannot have multiple declarators in - // a single declaration (CS0819). Only one variable should be present - varDecl.Variables.Single().Identifier.Parent!, - cancellationToken); - - private static async Task UpdateTypeSyntaxAsync(Document document, SyntaxEditor editor, TypeSyntax typeSyntax, SyntaxNode declarationSyntax, CancellationToken cancellationToken) + editor.ReplaceNode(declarationExpression, tupleDeclaration); + } + else { - typeSyntax = typeSyntax.StripRefIfNeeded(); - - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var typeSymbol = GetConvertedType(semanticModel, typeSyntax, cancellationToken); - - typeSymbol = AdjustNullabilityOfTypeSymbol( - typeSymbol, - semanticModel, - declarationSyntax, - cancellationToken); - editor.ReplaceNode(typeSyntax, GenerateTypeDeclaration(typeSyntax, typeSymbol)); } + } - private static ITypeSymbol AdjustNullabilityOfTypeSymbol( - ITypeSymbol typeSymbol, - SemanticModel semanticModel, - SyntaxNode declarationSyntax, - CancellationToken cancellationToken) + private static Task HandleForEachStatementAsync(Document document, SyntaxEditor editor, ForEachStatementSyntax forEach, CancellationToken cancellationToken) + => UpdateTypeSyntaxAsync( + document, + editor, + forEach.Type, + forEach.Identifier.GetRequiredParent(), + cancellationToken); + + private static Task HandleVariableDeclarationAsync(Document document, SyntaxEditor editor, VariableDeclarationSyntax varDecl, CancellationToken cancellationToken) + => UpdateTypeSyntaxAsync( + document, + editor, + varDecl.Type, + // Since we're only dealing with variable declaration using var, we know + // that implicitly typed variables cannot have multiple declarators in + // a single declaration (CS0819). Only one variable should be present + varDecl.Variables.Single().Identifier.Parent!, + cancellationToken); + + private static async Task UpdateTypeSyntaxAsync(Document document, SyntaxEditor editor, TypeSyntax typeSyntax, SyntaxNode declarationSyntax, CancellationToken cancellationToken) + { + typeSyntax = typeSyntax.StripRefIfNeeded(); + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var typeSymbol = GetConvertedType(semanticModel, typeSyntax, cancellationToken); + + typeSymbol = AdjustNullabilityOfTypeSymbol( + typeSymbol, + semanticModel, + declarationSyntax, + cancellationToken); + + editor.ReplaceNode(typeSyntax, GenerateTypeDeclaration(typeSyntax, typeSymbol)); + } + + private static ITypeSymbol AdjustNullabilityOfTypeSymbol( + ITypeSymbol typeSymbol, + SemanticModel semanticModel, + SyntaxNode declarationSyntax, + CancellationToken cancellationToken) + { + if (typeSymbol.NullableAnnotation == NullableAnnotation.Annotated) { - if (typeSymbol.NullableAnnotation == NullableAnnotation.Annotated) + // It's possible that the var shouldn't be annotated nullable, check assignments to the variable and + // determine if it needs to be null + var isPossiblyAssignedNull = NullableHelpers.IsDeclaredSymbolAssignedPossiblyNullValue(semanticModel, declarationSyntax, cancellationToken); + if (!isPossiblyAssignedNull) { - // It's possible that the var shouldn't be annotated nullable, check assignments to the variable and - // determine if it needs to be null - var isPossiblyAssignedNull = NullableHelpers.IsDeclaredSymbolAssignedPossiblyNullValue(semanticModel, declarationSyntax, cancellationToken); - if (!isPossiblyAssignedNull) - { - // If the symbol is never assigned null we can update the type symbol to also be non-null - return typeSymbol.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - } + // If the symbol is never assigned null we can update the type symbol to also be non-null + return typeSymbol.WithNullableAnnotation(NullableAnnotation.NotAnnotated); } - - return typeSymbol; } - private static ExpressionSyntax GenerateTupleDeclaration(ITypeSymbol typeSymbol, ParenthesizedVariableDesignationSyntax parensDesignation) - { - Debug.Assert(typeSymbol.IsTupleType); - var elements = ((INamedTypeSymbol)typeSymbol).TupleElements; - Debug.Assert(elements.Length == parensDesignation.Variables.Count); + return typeSymbol; + } - using var _ = ArrayBuilder.GetInstance(elements.Length, out var builder); - for (var i = 0; i < elements.Length; i++) + private static ExpressionSyntax GenerateTupleDeclaration(ITypeSymbol typeSymbol, ParenthesizedVariableDesignationSyntax parensDesignation) + { + Debug.Assert(typeSymbol.IsTupleType); + var elements = ((INamedTypeSymbol)typeSymbol).TupleElements; + Debug.Assert(elements.Length == parensDesignation.Variables.Count); + + using var _ = ArrayBuilder.GetInstance(elements.Length, out var builder); + for (var i = 0; i < elements.Length; i++) + { + var designation = parensDesignation.Variables[i]; + var type = elements[i].Type; + ExpressionSyntax newDeclaration; + switch (designation.Kind()) { - var designation = parensDesignation.Variables[i]; - var type = elements[i].Type; - ExpressionSyntax newDeclaration; - switch (designation.Kind()) - { - case SyntaxKind.SingleVariableDesignation: - case SyntaxKind.DiscardDesignation: - var typeName = type.GenerateTypeSyntax(allowVar: false); - newDeclaration = SyntaxFactory.DeclarationExpression(typeName, designation); - break; - case SyntaxKind.ParenthesizedVariableDesignation: - newDeclaration = GenerateTupleDeclaration(type, (ParenthesizedVariableDesignationSyntax)designation); - break; - default: - throw ExceptionUtilities.UnexpectedValue(designation.Kind()); - } - - newDeclaration = newDeclaration - .WithLeadingTrivia(designation.GetAllPrecedingTriviaToPreviousToken()) - .WithTrailingTrivia(designation.GetTrailingTrivia()); - - builder.Add(SyntaxFactory.Argument(newDeclaration)); + case SyntaxKind.SingleVariableDesignation: + case SyntaxKind.DiscardDesignation: + var typeName = type.GenerateTypeSyntax(allowVar: false); + newDeclaration = SyntaxFactory.DeclarationExpression(typeName, designation); + break; + case SyntaxKind.ParenthesizedVariableDesignation: + newDeclaration = GenerateTupleDeclaration(type, (ParenthesizedVariableDesignationSyntax)designation); + break; + default: + throw ExceptionUtilities.UnexpectedValue(designation.Kind()); } - var separatorBuilder = ArrayBuilder.GetInstance(builder.Count - 1, SyntaxFactory.Token(leading: default, SyntaxKind.CommaToken, trailing: default)); + newDeclaration = newDeclaration + .WithLeadingTrivia(designation.GetAllPrecedingTriviaToPreviousToken()) + .WithTrailingTrivia(designation.GetTrailingTrivia()); - return SyntaxFactory.TupleExpression( - SyntaxFactory.Token(SyntaxKind.OpenParenToken).WithTrailingTrivia(), - SyntaxFactory.SeparatedList(builder.ToImmutable(), separatorBuilder.ToImmutableAndFree()), - SyntaxFactory.Token(SyntaxKind.CloseParenToken)) - .WithTrailingTrivia(parensDesignation.GetTrailingTrivia()); + builder.Add(SyntaxFactory.Argument(newDeclaration)); } - private static SyntaxNode GenerateTypeDeclaration(TypeSyntax typeSyntax, ITypeSymbol newTypeSymbol) - { - // We're going to be passed through the simplifier. Tell it to not just convert this back to var (as - // that would defeat the purpose of this refactoring entirely). - var newTypeSyntax = newTypeSymbol - .GenerateTypeSyntax(allowVar: false) - .WithTriviaFrom(typeSyntax); + var separatorBuilder = ArrayBuilder.GetInstance(builder.Count - 1, SyntaxFactory.Token(leading: default, SyntaxKind.CommaToken, trailing: default)); - return newTypeSyntax; - } + return SyntaxFactory.TupleExpression( + SyntaxFactory.Token(SyntaxKind.OpenParenToken).WithTrailingTrivia(), + SyntaxFactory.SeparatedList(builder.ToImmutable(), separatorBuilder.ToImmutableAndFree()), + SyntaxFactory.Token(SyntaxKind.CloseParenToken)) + .WithTrailingTrivia(parensDesignation.GetTrailingTrivia()); + } - private static ITypeSymbol GetConvertedType(SemanticModel semanticModel, SyntaxNode typeSyntax, CancellationToken cancellationToken) - { - var typeSymbol = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).ConvertedType; - if (typeSymbol is null) - { - throw ExceptionUtilities.UnexpectedValue(typeSymbol); - } + private static SyntaxNode GenerateTypeDeclaration(TypeSyntax typeSyntax, ITypeSymbol newTypeSymbol) + { + // We're going to be passed through the simplifier. Tell it to not just convert this back to var (as + // that would defeat the purpose of this refactoring entirely). + var newTypeSyntax = newTypeSymbol + .GenerateTypeSyntax(allowVar: false) + .WithTriviaFrom(typeSyntax); + + return newTypeSyntax; + } - return typeSymbol; + private static ITypeSymbol GetConvertedType(SemanticModel semanticModel, SyntaxNode typeSyntax, CancellationToken cancellationToken) + { + var typeSymbol = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).ConvertedType; + if (typeSymbol is null) + { + throw ExceptionUtilities.UnexpectedValue(typeSymbol); } + + return typeSymbol; } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseImplicitOrExplicitType/UseImplicitTypeCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseImplicitOrExplicitType/UseImplicitTypeCodeFixProvider.cs index aa6fb88e69f06..95a7ff2e517f9 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseImplicitOrExplicitType/UseImplicitTypeCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseImplicitOrExplicitType/UseImplicitTypeCodeFixProvider.cs @@ -19,48 +19,47 @@ using Microsoft.CodeAnalysis.Editing; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.TypeStyle +namespace Microsoft.CodeAnalysis.CSharp.TypeStyle; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseImplicitType), Shared] +internal class UseImplicitTypeCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseImplicitType), Shared] - internal class UseImplicitTypeCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public UseImplicitTypeCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public UseImplicitTypeCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseImplicitTypeDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseImplicitTypeDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.use_var_instead_of_explicit_type, nameof(CSharpAnalyzersResources.use_var_instead_of_explicit_type)); - return Task.CompletedTask; - } - - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var root = editor.OriginalRoot; + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.use_var_instead_of_explicit_type, nameof(CSharpAnalyzersResources.use_var_instead_of_explicit_type)); + return Task.CompletedTask; + } - foreach (var diagnostic in diagnostics) - { - var typeSyntax = (TypeSyntax)root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); - ReplaceTypeWithVar(editor, typeSyntax); - } + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var root = editor.OriginalRoot; - return Task.CompletedTask; + foreach (var diagnostic in diagnostics) + { + var typeSyntax = (TypeSyntax)root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + ReplaceTypeWithVar(editor, typeSyntax); } - internal static void ReplaceTypeWithVar(SyntaxEditor editor, TypeSyntax type) - { - type = type.StripRefIfNeeded(); - var implicitType = SyntaxFactory.IdentifierName("var") - .WithTriviaFrom(type); + return Task.CompletedTask; + } - editor.ReplaceNode(type, implicitType); - } + internal static void ReplaceTypeWithVar(SyntaxEditor editor, TypeSyntax type) + { + type = type.StripRefIfNeeded(); + var implicitType = SyntaxFactory.IdentifierName("var") + .WithTriviaFrom(type); + + editor.ReplaceNode(type, implicitType); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/CSharpUseIndexOperatorCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/CSharpUseIndexOperatorCodeFixProvider.cs index ddc92233befc9..5206ffe65c3fc 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/CSharpUseIndexOperatorCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/CSharpUseIndexOperatorCodeFixProvider.cs @@ -17,44 +17,43 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator -{ - using System.Linq; - using static CodeFixHelpers; +namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator; + +using System.Linq; +using static CodeFixHelpers; - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseIndexOperator), Shared] - internal class CSharpUseIndexOperatorCodeFixProvider : SyntaxEditorBasedCodeFixProvider +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseIndexOperator), Shared] +internal class CSharpUseIndexOperatorCodeFixProvider : SyntaxEditorBasedCodeFixProvider +{ + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseIndexOperatorCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseIndexOperatorCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } = - [IDEDiagnosticIds.UseIndexOperatorDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds { get; } = + [IDEDiagnosticIds.UseIndexOperatorDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Use_index_operator, nameof(CSharpAnalyzersResources.Use_index_operator)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Use_index_operator, nameof(CSharpAnalyzersResources.Use_index_operator)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + // Process diagnostics from innermost to outermost in case any are nested. + foreach (var diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start)) { - // Process diagnostics from innermost to outermost in case any are nested. - foreach (var diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start)) - { - var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - - editor.ReplaceNode( - node, - (currentNode, _) => IndexExpression(((BinaryExpressionSyntax)currentNode).Right)); - } + var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - return Task.CompletedTask; + editor.ReplaceNode( + node, + (currentNode, _) => IndexExpression(((BinaryExpressionSyntax)currentNode).Right)); } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/CSharpUseRangeOperatorCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/CSharpUseRangeOperatorCodeFixProvider.cs index 78975c37d250c..c038856ac9110 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/CSharpUseRangeOperatorCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/CSharpUseRangeOperatorCodeFixProvider.cs @@ -21,213 +21,212 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator +namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator; + +using static CodeFixHelpers; +using static CSharpUseRangeOperatorDiagnosticAnalyzer; +using static Helpers; +using static SyntaxFactory; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseRangeOperator), Shared] +internal class CSharpUseRangeOperatorCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - using static CodeFixHelpers; - using static CSharpUseRangeOperatorDiagnosticAnalyzer; - using static Helpers; - using static SyntaxFactory; + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpUseRangeOperatorCodeFixProvider() + { + } - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseRangeOperator), Shared] - internal class CSharpUseRangeOperatorCodeFixProvider : SyntaxEditorBasedCodeFixProvider + public override ImmutableArray FixableDiagnosticIds { get; } = + [IDEDiagnosticIds.UseRangeOperatorDiagnosticId]; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpUseRangeOperatorCodeFixProvider() - { - } + RegisterCodeFix(context, CSharpAnalyzersResources.Use_range_operator, nameof(CSharpAnalyzersResources.Use_range_operator)); + return Task.CompletedTask; + } - public override ImmutableArray FixableDiagnosticIds { get; } = - [IDEDiagnosticIds.UseRangeOperatorDiagnosticId]; + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var invocationNodes = diagnostics.Select(d => GetInvocationExpression(d, cancellationToken)) + .OrderByDescending(i => i.SpanStart) + .ToImmutableArray(); + var syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + await editor.ApplyExpressionLevelSemanticEditsAsync( + document, invocationNodes, + canReplace: (_1, _2) => true, + (semanticModel, currentRoot, currentInvocation) => + UpdateInvocation(semanticModel, currentRoot, currentInvocation, syntaxGenerator, cancellationToken), + cancellationToken).ConfigureAwait(false); + } - public override Task RegisterCodeFixesAsync(CodeFixContext context) + private static SyntaxNode UpdateInvocation( + SemanticModel semanticModel, SyntaxNode currentRoot, + InvocationExpressionSyntax currentInvocation, + SyntaxGenerator generator, + CancellationToken cancellationToken) + { + if (semanticModel.GetOperation(currentInvocation, cancellationToken) is IInvocationOperation invocation && + InfoCache.TryCreate(semanticModel.Compilation, out var infoCache) && + AnalyzeInvocation(invocation, infoCache) is { } result) { - RegisterCodeFix(context, CSharpAnalyzersResources.Use_range_operator, nameof(CSharpAnalyzersResources.Use_range_operator)); - return Task.CompletedTask; + var updatedNode = FixOne(result, generator); + if (updatedNode != null) + return currentRoot.ReplaceNode(result.Invocation, updatedNode); } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var invocationNodes = diagnostics.Select(d => GetInvocationExpression(d, cancellationToken)) - .OrderByDescending(i => i.SpanStart) - .ToImmutableArray(); - var syntaxGenerator = SyntaxGenerator.GetGenerator(document); - - await editor.ApplyExpressionLevelSemanticEditsAsync( - document, invocationNodes, - canReplace: (_1, _2) => true, - (semanticModel, currentRoot, currentInvocation) => - UpdateInvocation(semanticModel, currentRoot, currentInvocation, syntaxGenerator, cancellationToken), - cancellationToken).ConfigureAwait(false); - } + return currentRoot; + } - private static SyntaxNode UpdateInvocation( - SemanticModel semanticModel, SyntaxNode currentRoot, - InvocationExpressionSyntax currentInvocation, - SyntaxGenerator generator, - CancellationToken cancellationToken) - { - if (semanticModel.GetOperation(currentInvocation, cancellationToken) is IInvocationOperation invocation && - InfoCache.TryCreate(semanticModel.Compilation, out var infoCache) && - AnalyzeInvocation(invocation, infoCache) is { } result) - { - var updatedNode = FixOne(result, generator); - if (updatedNode != null) - return currentRoot.ReplaceNode(result.Invocation, updatedNode); - } + private static InvocationExpressionSyntax GetInvocationExpression(Diagnostic d, CancellationToken cancellationToken) + => (InvocationExpressionSyntax)d.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); - return currentRoot; - } + private static ExpressionSyntax FixOne(Result result, SyntaxGenerator generator) + { + var invocation = result.Invocation; - private static InvocationExpressionSyntax GetInvocationExpression(Diagnostic d, CancellationToken cancellationToken) - => (InvocationExpressionSyntax)d.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); + var rangeExpression = CreateRangeExpression(result, generator); + var argument = Argument(rangeExpression).WithAdditionalAnnotations(Formatter.Annotation); + var arguments = SingletonSeparatedList(argument); - private static ExpressionSyntax FixOne(Result result, SyntaxGenerator generator) + if (result.MemberInfo.OverloadedMethodOpt == null) { - var invocation = result.Invocation; - - var rangeExpression = CreateRangeExpression(result, generator); - var argument = Argument(rangeExpression).WithAdditionalAnnotations(Formatter.Annotation); - var arguments = SingletonSeparatedList(argument); - - if (result.MemberInfo.OverloadedMethodOpt == null) - { - var argList = invocation.ArgumentList; - var argumentList = BracketedArgumentList( - Token(SyntaxKind.OpenBracketToken).WithTriviaFrom(argList.OpenParenToken), - arguments, - Token(SyntaxKind.CloseBracketToken).WithTriviaFrom(argList.CloseParenToken)); - if (invocation.Expression is MemberBindingExpressionSyntax) - { - // x?.Substring(...) -> x?[...] - return ElementBindingExpression(argumentList); - } - - if (invocation.Expression is IdentifierNameSyntax) - { - // Substring(...) -> this[...] - return ElementAccessExpression(ThisExpression(), argumentList); - } - - var expression = invocation.Expression is MemberAccessExpressionSyntax memberAccess - ? memberAccess.Expression // x.Substring(...) -> x[...] - : invocation.Expression; - return ElementAccessExpression(expression, argumentList); - } - else + var argList = invocation.ArgumentList; + var argumentList = BracketedArgumentList( + Token(SyntaxKind.OpenBracketToken).WithTriviaFrom(argList.OpenParenToken), + arguments, + Token(SyntaxKind.CloseBracketToken).WithTriviaFrom(argList.CloseParenToken)); + if (invocation.Expression is MemberBindingExpressionSyntax) { - return invocation.ReplaceNode( - invocation.ArgumentList, - invocation.ArgumentList.WithArguments(arguments)); + // x?.Substring(...) -> x?[...] + return ElementBindingExpression(argumentList); } - } - private static RangeExpressionSyntax CreateRangeExpression(Result result, SyntaxGenerator generator) - => result.Kind switch + if (invocation.Expression is IdentifierNameSyntax) { - ResultKind.Computed => CreateComputedRange(result), - ResultKind.Constant => CreateConstantRange(result, generator), - _ => throw ExceptionUtilities.Unreachable(), - }; + // Substring(...) -> this[...] + return ElementAccessExpression(ThisExpression(), argumentList); + } - private static RangeExpressionSyntax CreateComputedRange(Result result) + var expression = invocation.Expression is MemberAccessExpressionSyntax memberAccess + ? memberAccess.Expression // x.Substring(...) -> x[...] + : invocation.Expression; + return ElementAccessExpression(expression, argumentList); + } + else { - // We have enough information now to generate `start..end`. However, this will often - // not be what the user wants. For example, generating `start..expr.Length` is not as - // desirable as `start..`. Similarly, `start..(expr.Length - 1)` is not as desirable as - // `start..^1`. - - var startOperation = result.Op1; - var endOperation = result.Op2; - - var lengthLikeProperty = result.MemberInfo.LengthLikeProperty; - var instance = result.InvocationOperation.Instance; - Contract.ThrowIfNull(instance); + return invocation.ReplaceNode( + invocation.ArgumentList, + invocation.ArgumentList.WithArguments(arguments)); + } + } - // If our start-op is actually equivalent to `expr.Length - val`, then just change our - // start-op to be `val` and record that we should emit it as `^val`. - var startFromEnd = IsFromEnd(lengthLikeProperty, instance, ref startOperation); - var startExpr = (ExpressionSyntax)startOperation.Syntax; + private static RangeExpressionSyntax CreateRangeExpression(Result result, SyntaxGenerator generator) + => result.Kind switch + { + ResultKind.Computed => CreateComputedRange(result), + ResultKind.Constant => CreateConstantRange(result, generator), + _ => throw ExceptionUtilities.Unreachable(), + }; - var endFromEnd = false; - ExpressionSyntax? endExpr = null; + private static RangeExpressionSyntax CreateComputedRange(Result result) + { + // We have enough information now to generate `start..end`. However, this will often + // not be what the user wants. For example, generating `start..expr.Length` is not as + // desirable as `start..`. Similarly, `start..(expr.Length - 1)` is not as desirable as + // `start..^1`. - if (endOperation is not null) - { - // We need to do the same for the second argument, since it's present. - // Similarly, if our end-op is actually equivalent to `expr.Length - val`, then just - // change our end-op to be `val` and record that we should emit it as `^val`. - endFromEnd = IsFromEnd(lengthLikeProperty, instance, ref endOperation); - - // Check if the range goes to 'expr.Length'; if it does, we leave off - // the end part of the range, i.e. `start..`. - if (!IsInstanceLengthCheck(lengthLikeProperty, instance, endOperation)) - endExpr = (ExpressionSyntax)endOperation.Syntax; - } + var startOperation = result.Op1; + var endOperation = result.Op2; - // If we're starting the range operation from 0, then we can just leave off the start of - // the range. i.e. `..end` - if (startOperation.ConstantValue.HasValue && - startOperation.ConstantValue.Value is 0) - { - startExpr = null; - } + var lengthLikeProperty = result.MemberInfo.LengthLikeProperty; + var instance = result.InvocationOperation.Instance; + Contract.ThrowIfNull(instance); - // expressions that the iops point to may be skip certain expressions actually in source (like checked - // exprs). Walk upwards so we grab all of that when producing the final range expression. - startExpr = WalkUpCheckedExpressions(startExpr); - endExpr = WalkUpCheckedExpressions(endExpr); + // If our start-op is actually equivalent to `expr.Length - val`, then just change our + // start-op to be `val` and record that we should emit it as `^val`. + var startFromEnd = IsFromEnd(lengthLikeProperty, instance, ref startOperation); + var startExpr = (ExpressionSyntax)startOperation.Syntax; - return RangeExpression( - startExpr != null && startFromEnd ? IndexExpression(startExpr) : startExpr?.Parenthesize(), - endExpr != null && endFromEnd ? IndexExpression(endExpr) : endExpr?.Parenthesize()); - } + var endFromEnd = false; + ExpressionSyntax? endExpr = null; - [return: NotNullIfNotNull(nameof(expr))] - private static ExpressionSyntax? WalkUpCheckedExpressions(ExpressionSyntax? expr) + if (endOperation is not null) { - while (expr?.Parent is CheckedExpressionSyntax parent) - expr = parent; - - return expr; + // We need to do the same for the second argument, since it's present. + // Similarly, if our end-op is actually equivalent to `expr.Length - val`, then just + // change our end-op to be `val` and record that we should emit it as `^val`. + endFromEnd = IsFromEnd(lengthLikeProperty, instance, ref endOperation); + + // Check if the range goes to 'expr.Length'; if it does, we leave off + // the end part of the range, i.e. `start..`. + if (!IsInstanceLengthCheck(lengthLikeProperty, instance, endOperation)) + endExpr = (ExpressionSyntax)endOperation.Syntax; } - private static RangeExpressionSyntax CreateConstantRange(Result result, SyntaxGenerator generator) + // If we're starting the range operation from 0, then we can just leave off the start of + // the range. i.e. `..end` + if (startOperation.ConstantValue.HasValue && + startOperation.ConstantValue.Value is 0) { - Contract.ThrowIfNull(result.Op2); - - // the form is s.Slice(constant1, s.Length - constant2). Want to generate - // s[constant1..(constant2-constant1)] - var constant1 = GetInt32Value(result.Op1); - var constant2 = GetInt32Value(result.Op2); - - return RangeExpression( - // If we're starting the range operation from 0, then we can just leave off the start of - // the range. i.e. `..end` - constant1 == 0 ? null : WalkUpCheckedExpressions((ExpressionSyntax)result.Op1.Syntax), - IndexExpression((ExpressionSyntax)generator.LiteralExpression(constant2 - constant1))); + startExpr = null; } - private static int GetInt32Value(IOperation operation) - => (int)operation.ConstantValue.Value!; // Safe as we already confirmed this was an int when making the result. + // expressions that the iops point to may be skip certain expressions actually in source (like checked + // exprs). Walk upwards so we grab all of that when producing the final range expression. + startExpr = WalkUpCheckedExpressions(startExpr); + endExpr = WalkUpCheckedExpressions(endExpr); - /// - /// check if its the form: `expr.Length - value`. If so, update rangeOperation to then - /// point to 'value' so that we can generate '^value'. - /// - private static bool IsFromEnd( - IPropertySymbol lengthLikeProperty, IOperation instance, ref IOperation rangeOperation) - { - if (IsSubtraction(rangeOperation, out var subtraction) && - IsInstanceLengthCheck(lengthLikeProperty, instance, subtraction.LeftOperand)) - { - rangeOperation = subtraction.RightOperand; - return true; - } + return RangeExpression( + startExpr != null && startFromEnd ? IndexExpression(startExpr) : startExpr?.Parenthesize(), + endExpr != null && endFromEnd ? IndexExpression(endExpr) : endExpr?.Parenthesize()); + } - return false; + [return: NotNullIfNotNull(nameof(expr))] + private static ExpressionSyntax? WalkUpCheckedExpressions(ExpressionSyntax? expr) + { + while (expr?.Parent is CheckedExpressionSyntax parent) + expr = parent; + + return expr; + } + + private static RangeExpressionSyntax CreateConstantRange(Result result, SyntaxGenerator generator) + { + Contract.ThrowIfNull(result.Op2); + + // the form is s.Slice(constant1, s.Length - constant2). Want to generate + // s[constant1..(constant2-constant1)] + var constant1 = GetInt32Value(result.Op1); + var constant2 = GetInt32Value(result.Op2); + + return RangeExpression( + // If we're starting the range operation from 0, then we can just leave off the start of + // the range. i.e. `..end` + constant1 == 0 ? null : WalkUpCheckedExpressions((ExpressionSyntax)result.Op1.Syntax), + IndexExpression((ExpressionSyntax)generator.LiteralExpression(constant2 - constant1))); + } + + private static int GetInt32Value(IOperation operation) + => (int)operation.ConstantValue.Value!; // Safe as we already confirmed this was an int when making the result. + + /// + /// check if its the form: `expr.Length - value`. If so, update rangeOperation to then + /// point to 'value' so that we can generate '^value'. + /// + private static bool IsFromEnd( + IPropertySymbol lengthLikeProperty, IOperation instance, ref IOperation rangeOperation) + { + if (IsSubtraction(rangeOperation, out var subtraction) && + IsInstanceLengthCheck(lengthLikeProperty, instance, subtraction.LeftOperand)) + { + rangeOperation = subtraction.RightOperand; + return true; } + + return false; } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/Helpers.cs b/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/Helpers.cs index 5fb8e83f38655..62b9558db992e 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/Helpers.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseIndexOrRangeOperator/Helpers.cs @@ -7,16 +7,15 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator +namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator; + +internal static class CodeFixHelpers { - internal static class CodeFixHelpers - { - /// - /// Creates an `^expr` index expression from a given `expr`. - /// - public static PrefixUnaryExpressionSyntax IndexExpression(ExpressionSyntax expr) - => SyntaxFactory.PrefixUnaryExpression( - SyntaxKind.IndexExpression, - expr.Parenthesize()); - } + /// + /// Creates an `^expr` index expression from a given `expr`. + /// + public static PrefixUnaryExpressionSyntax IndexExpression(ExpressionSyntax expr) + => SyntaxFactory.PrefixUnaryExpression( + SyntaxKind.IndexExpression, + expr.Parenthesize()); } diff --git a/src/Analyzers/CSharp/CodeFixes/UseInferredMemberName/CSharpUseInferredMemberNameCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseInferredMemberName/CSharpUseInferredMemberNameCodeFixProvider.cs index fff91dba296ad..e9870962cfc1b 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseInferredMemberName/CSharpUseInferredMemberNameCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseInferredMemberName/CSharpUseInferredMemberNameCodeFixProvider.cs @@ -10,18 +10,17 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.UseInferredMemberName; -namespace Microsoft.CodeAnalysis.CSharp.UseInferredMemberName +namespace Microsoft.CodeAnalysis.CSharp.UseInferredMemberName; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseInferredMemberName), Shared] +internal sealed class CSharpUseInferredMemberNameCodeFixProvider : AbstractUseInferredMemberNameCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseInferredMemberName), Shared] - internal sealed class CSharpUseInferredMemberNameCodeFixProvider : AbstractUseInferredMemberNameCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseInferredMemberNameCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseInferredMemberNameCodeFixProvider() - { - } - - protected override void LanguageSpecificRemoveSuggestedNode(SyntaxEditor editor, SyntaxNode node) - => editor.RemoveNode(node, SyntaxRemoveOptions.KeepExteriorTrivia | SyntaxRemoveOptions.AddElasticMarker); } + + protected override void LanguageSpecificRemoveSuggestedNode(SyntaxEditor editor, SyntaxNode node) + => editor.RemoveNode(node, SyntaxRemoveOptions.KeepExteriorTrivia | SyntaxRemoveOptions.AddElasticMarker); } diff --git a/src/Analyzers/CSharp/CodeFixes/UseInterpolatedVerbatimString/CSharpUseInterpolatedVerbatimStringCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseInterpolatedVerbatimString/CSharpUseInterpolatedVerbatimStringCodeFixProvider.cs index f256a406edb88..fca0f8a0142e0 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseInterpolatedVerbatimString/CSharpUseInterpolatedVerbatimStringCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseInterpolatedVerbatimString/CSharpUseInterpolatedVerbatimStringCodeFixProvider.cs @@ -18,59 +18,58 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseInterpolatedVerbatimString +namespace Microsoft.CodeAnalysis.CSharp.UseInterpolatedVerbatimString; + +/// +/// Converts a verbatim interpolated string @$"" to an interpolated verbatim string $@"" +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseInterpolatedVerbatimString), Shared] +internal partial class CSharpUseInterpolatedVerbatimStringCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - /// - /// Converts a verbatim interpolated string @$"" to an interpolated verbatim string $@"" - /// - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseInterpolatedVerbatimString), Shared] - internal partial class CSharpUseInterpolatedVerbatimStringCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseInterpolatedVerbatimStringCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseInterpolatedVerbatimStringCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => ["CS8401"]; + public override ImmutableArray FixableDiagnosticIds + => ["CS8401"]; - private const string InterpolatedVerbatimText = "$@\""; + private const string InterpolatedVerbatimText = "$@\""; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpCodeFixesResources.Use_interpolated_verbatim_string, nameof(CSharpCodeFixesResources.Use_interpolated_verbatim_string)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpCodeFixesResources.Use_interpolated_verbatim_string, nameof(CSharpCodeFixesResources.Use_interpolated_verbatim_string)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) { - foreach (var diagnostic in diagnostics) - { - cancellationToken.ThrowIfCancellationRequested(); - AddEdits(editor, diagnostic, cancellationToken); - } - - return Task.CompletedTask; + cancellationToken.ThrowIfCancellationRequested(); + AddEdits(editor, diagnostic, cancellationToken); } - private static void AddEdits( - SyntaxEditor editor, - Diagnostic diagnostic, - CancellationToken cancellationToken) - { - var verbatimInterpolatedLocation = diagnostic.Location; - var verbatimInterpolated = (InterpolatedStringExpressionSyntax)verbatimInterpolatedLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); + return Task.CompletedTask; + } - var oldStartToken = verbatimInterpolated.StringStartToken; - var newStartToken = SyntaxFactory.Token(oldStartToken.LeadingTrivia, SyntaxKind.InterpolatedVerbatimStringStartToken, - InterpolatedVerbatimText, InterpolatedVerbatimText, oldStartToken.TrailingTrivia); + private static void AddEdits( + SyntaxEditor editor, + Diagnostic diagnostic, + CancellationToken cancellationToken) + { + var verbatimInterpolatedLocation = diagnostic.Location; + var verbatimInterpolated = (InterpolatedStringExpressionSyntax)verbatimInterpolatedLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); - var interpolatedVerbatim = verbatimInterpolated.WithStringStartToken(newStartToken); + var oldStartToken = verbatimInterpolated.StringStartToken; + var newStartToken = SyntaxFactory.Token(oldStartToken.LeadingTrivia, SyntaxKind.InterpolatedVerbatimStringStartToken, + InterpolatedVerbatimText, InterpolatedVerbatimText, oldStartToken.TrailingTrivia); - editor.ReplaceNode(verbatimInterpolated, interpolatedVerbatim); - } + var interpolatedVerbatim = verbatimInterpolated.WithStringStartToken(newStartToken); + + editor.ReplaceNode(verbatimInterpolated, interpolatedVerbatim); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseIsNullCheckForCastAndEqualityOperatorCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseIsNullCheckForCastAndEqualityOperatorCodeFixProvider.cs index 5ee262c2746d7..cd466f264297b 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseIsNullCheckForCastAndEqualityOperatorCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseIsNullCheckForCastAndEqualityOperatorCodeFixProvider.cs @@ -18,96 +18,95 @@ using Microsoft.CodeAnalysis.UseIsNullCheck; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck -{ - using static SyntaxFactory; - using static UseIsNullCheckHelpers; +namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck; + +using static SyntaxFactory; +using static UseIsNullCheckHelpers; - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseIsNullCheckForCastAndEqualityOperator), Shared] - internal class CSharpUseIsNullCheckForCastAndEqualityOperatorCodeFixProvider : SyntaxEditorBasedCodeFixProvider +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseIsNullCheckForCastAndEqualityOperator), Shared] +internal class CSharpUseIsNullCheckForCastAndEqualityOperatorCodeFixProvider : SyntaxEditorBasedCodeFixProvider +{ + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseIsNullCheckForCastAndEqualityOperatorCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseIsNullCheckForCastAndEqualityOperatorCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseIsNullCheckDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseIsNullCheckDiagnosticId]; - private static bool IsSupportedDiagnostic(Diagnostic diagnostic) - => diagnostic.Properties[UseIsNullConstants.Kind] == UseIsNullConstants.CastAndEqualityKey; + private static bool IsSupportedDiagnostic(Diagnostic diagnostic) + => diagnostic.Properties[UseIsNullConstants.Kind] == UseIsNullConstants.CastAndEqualityKey; - public override Task RegisterCodeFixesAsync(CodeFixContext context) + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); + if (IsSupportedDiagnostic(diagnostic)) { - var diagnostic = context.Diagnostics.First(); - if (IsSupportedDiagnostic(diagnostic)) - { - var negated = diagnostic.Properties.ContainsKey(UseIsNullConstants.Negated); - var title = GetTitle(negated, diagnostic.Location.SourceTree!.Options); - - context.RegisterCodeFix( - CodeAction.Create(title, GetDocumentUpdater(context), title), - context.Diagnostics); - } - - return Task.CompletedTask; + var negated = diagnostic.Properties.ContainsKey(UseIsNullConstants.Negated); + var title = GetTitle(negated, diagnostic.Location.SourceTree!.Options); + + context.RegisterCodeFix( + CodeAction.Create(title, GetDocumentUpdater(context), title), + context.Diagnostics); } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - foreach (var diagnostic in diagnostics) - { - if (!IsSupportedDiagnostic(diagnostic)) - continue; + return Task.CompletedTask; + } - var binary = (BinaryExpressionSyntax)diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken: cancellationToken); + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) + { + if (!IsSupportedDiagnostic(diagnostic)) + continue; - editor.ReplaceNode( - binary, - (current, g) => Rewrite((BinaryExpressionSyntax)current)); - } + var binary = (BinaryExpressionSyntax)diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken: cancellationToken); - return Task.CompletedTask; + editor.ReplaceNode( + binary, + (current, g) => Rewrite((BinaryExpressionSyntax)current)); } - private static ExpressionSyntax Rewrite(BinaryExpressionSyntax binary) - { - var isPattern = RewriteWorker(binary); - if (binary.IsKind(SyntaxKind.EqualsExpression)) - return isPattern; - - if (SupportsIsNotPattern(binary.SyntaxTree.Options)) - { - // convert: (object)expr != null to expr is not null - return isPattern.WithPattern( - UnaryPattern(isPattern.Pattern)); - } - else - { - // convert: (object)expr != null to expr is object - return BinaryExpression( - SyntaxKind.IsExpression, - isPattern.Expression, - PredefinedType(Token(SyntaxKind.ObjectKeyword))).WithTriviaFrom(isPattern); - } - } + return Task.CompletedTask; + } - private static IsPatternExpressionSyntax RewriteWorker(BinaryExpressionSyntax binary) - => binary.Right.IsKind(SyntaxKind.NullLiteralExpression) - ? Rewrite(binary, binary.Left, binary.Right) - : Rewrite(binary, binary.Right, binary.Left); + private static ExpressionSyntax Rewrite(BinaryExpressionSyntax binary) + { + var isPattern = RewriteWorker(binary); + if (binary.IsKind(SyntaxKind.EqualsExpression)) + return isPattern; - private static IsPatternExpressionSyntax Rewrite( - BinaryExpressionSyntax binary, ExpressionSyntax expr, ExpressionSyntax nullLiteral) + if (SupportsIsNotPattern(binary.SyntaxTree.Options)) { - var castExpr = (CastExpressionSyntax)expr; - return IsPatternExpression( - castExpr.Expression.WithTriviaFrom(binary.Left), - Token(SyntaxKind.IsKeyword).WithTriviaFrom(binary.OperatorToken), - ConstantPattern(nullLiteral).WithTriviaFrom(binary.Right)); + // convert: (object)expr != null to expr is not null + return isPattern.WithPattern( + UnaryPattern(isPattern.Pattern)); } + else + { + // convert: (object)expr != null to expr is object + return BinaryExpression( + SyntaxKind.IsExpression, + isPattern.Expression, + PredefinedType(Token(SyntaxKind.ObjectKeyword))).WithTriviaFrom(isPattern); + } + } + + private static IsPatternExpressionSyntax RewriteWorker(BinaryExpressionSyntax binary) + => binary.Right.IsKind(SyntaxKind.NullLiteralExpression) + ? Rewrite(binary, binary.Left, binary.Right) + : Rewrite(binary, binary.Right, binary.Left); + + private static IsPatternExpressionSyntax Rewrite( + BinaryExpressionSyntax binary, ExpressionSyntax expr, ExpressionSyntax nullLiteral) + { + var castExpr = (CastExpressionSyntax)expr; + return IsPatternExpression( + castExpr.Expression.WithTriviaFrom(binary.Left), + Token(SyntaxKind.IsKeyword).WithTriviaFrom(binary.OperatorToken), + ConstantPattern(nullLiteral).WithTriviaFrom(binary.Right)); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseIsNullCheckForReferenceEqualsCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseIsNullCheckForReferenceEqualsCodeFixProvider.cs index 068d58391e2eb..66be4c4f6f14e 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseIsNullCheckForReferenceEqualsCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseIsNullCheckForReferenceEqualsCodeFixProvider.cs @@ -11,61 +11,60 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.UseIsNullCheck; -namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck -{ - using static SyntaxFactory; - using static UseIsNullCheckHelpers; +namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck; - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseIsNullCheckForReferenceEquals), Shared] - internal class CSharpUseIsNullCheckForReferenceEqualsCodeFixProvider - : AbstractUseIsNullCheckForReferenceEqualsCodeFixProvider - { - private static readonly LiteralExpressionSyntax s_nullLiteralExpression - = LiteralExpression(SyntaxKind.NullLiteralExpression); +using static SyntaxFactory; +using static UseIsNullCheckHelpers; - private static readonly ConstantPatternSyntax s_nullLiteralPattern - = ConstantPattern(s_nullLiteralExpression); +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseIsNullCheckForReferenceEquals), Shared] +internal class CSharpUseIsNullCheckForReferenceEqualsCodeFixProvider + : AbstractUseIsNullCheckForReferenceEqualsCodeFixProvider +{ + private static readonly LiteralExpressionSyntax s_nullLiteralExpression + = LiteralExpression(SyntaxKind.NullLiteralExpression); - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseIsNullCheckForReferenceEqualsCodeFixProvider() - { - } + private static readonly ConstantPatternSyntax s_nullLiteralPattern + = ConstantPattern(s_nullLiteralExpression); - protected override string GetTitle(bool negated, ParseOptions options) - => UseIsNullCheckHelpers.GetTitle(negated, options); + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseIsNullCheckForReferenceEqualsCodeFixProvider() + { + } - private static SyntaxNode CreateEqualsNullCheck(ExpressionSyntax argument) - => BinaryExpression(SyntaxKind.EqualsExpression, argument, s_nullLiteralExpression).Parenthesize(); + protected override string GetTitle(bool negated, ParseOptions options) + => UseIsNullCheckHelpers.GetTitle(negated, options); - private static SyntaxNode CreateIsNullCheck(ExpressionSyntax argument) - => IsPatternExpression(argument, s_nullLiteralPattern).Parenthesize(); + private static SyntaxNode CreateEqualsNullCheck(ExpressionSyntax argument) + => BinaryExpression(SyntaxKind.EqualsExpression, argument, s_nullLiteralExpression).Parenthesize(); - private static SyntaxNode CreateIsNotNullCheck(ExpressionSyntax argument) + private static SyntaxNode CreateIsNullCheck(ExpressionSyntax argument) + => IsPatternExpression(argument, s_nullLiteralPattern).Parenthesize(); + + private static SyntaxNode CreateIsNotNullCheck(ExpressionSyntax argument) + { + if (SupportsIsNotPattern(argument.SyntaxTree.Options)) + { + return IsPatternExpression( + argument, + UnaryPattern( + Token(SyntaxKind.NotKeyword), + s_nullLiteralPattern)).Parenthesize(); + } + else { - if (SupportsIsNotPattern(argument.SyntaxTree.Options)) - { - return IsPatternExpression( - argument, - UnaryPattern( - Token(SyntaxKind.NotKeyword), - s_nullLiteralPattern)).Parenthesize(); - } - else - { - return BinaryExpression( - SyntaxKind.IsExpression, - argument, - PredefinedType(Token(SyntaxKind.ObjectKeyword))).Parenthesize(); - } + return BinaryExpression( + SyntaxKind.IsExpression, + argument, + PredefinedType(Token(SyntaxKind.ObjectKeyword))).Parenthesize(); } + } - protected override SyntaxNode CreateNullCheck(ExpressionSyntax argument, bool isUnconstrainedGeneric) - => isUnconstrainedGeneric - ? CreateEqualsNullCheck(argument) - : CreateIsNullCheck(argument); + protected override SyntaxNode CreateNullCheck(ExpressionSyntax argument, bool isUnconstrainedGeneric) + => isUnconstrainedGeneric + ? CreateEqualsNullCheck(argument) + : CreateIsNullCheck(argument); - protected override SyntaxNode CreateNotNullCheck(ExpressionSyntax argument) - => CreateIsNotNullCheck(argument); - } + protected override SyntaxNode CreateNotNullCheck(ExpressionSyntax argument) + => CreateIsNotNullCheck(argument); } diff --git a/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseNullCheckOverTypeCheckCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseNullCheckOverTypeCheckCodeFixProvider.cs index 584f8d1553a2c..eb784c182f703 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseNullCheckOverTypeCheckCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/CSharpUseNullCheckOverTypeCheckCodeFixProvider.cs @@ -19,52 +19,51 @@ using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck +namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseNullCheckOverTypeCheck), Shared] +internal sealed class CSharpUseNullCheckOverTypeCheckCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseNullCheckOverTypeCheck), Shared] - internal sealed class CSharpUseNullCheckOverTypeCheckCodeFixProvider : SyntaxEditorBasedCodeFixProvider - { - private static readonly ConstantPatternSyntax s_nullConstantPattern = ConstantPattern(LiteralExpression(SyntaxKind.NullLiteralExpression)); + private static readonly ConstantPatternSyntax s_nullConstantPattern = ConstantPattern(LiteralExpression(SyntaxKind.NullLiteralExpression)); - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpUseNullCheckOverTypeCheckCodeFixProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpUseNullCheckOverTypeCheckCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseNullCheckOverTypeCheckDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseNullCheckOverTypeCheckDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Prefer_null_check_over_type_check, nameof(CSharpAnalyzersResources.Prefer_null_check_over_type_check)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Prefer_null_check_over_type_check, nameof(CSharpAnalyzersResources.Prefer_null_check_over_type_check)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) { - foreach (var diagnostic in diagnostics) + var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken: cancellationToken); + SyntaxNode replacement = node switch { - var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken: cancellationToken); - SyntaxNode replacement = node switch - { - // Replace 'x is object' with 'x is not null' - BinaryExpressionSyntax binary => - IsPatternExpression( - expression: binary.Left, - pattern: UnaryPattern(s_nullConstantPattern)), - UnaryPatternSyntax => - s_nullConstantPattern, - // The analyzer reports diagnostic only on BinaryExpressionSyntax and UnaryPatternSyntax. - _ => throw ExceptionUtilities.Unreachable() - }; - - editor.ReplaceNode(node, replacement.WithTriviaFrom(node)); - } + // Replace 'x is object' with 'x is not null' + BinaryExpressionSyntax binary => + IsPatternExpression( + expression: binary.Left, + pattern: UnaryPattern(s_nullConstantPattern)), + UnaryPatternSyntax => + s_nullConstantPattern, + // The analyzer reports diagnostic only on BinaryExpressionSyntax and UnaryPatternSyntax. + _ => throw ExceptionUtilities.Unreachable() + }; - return Task.CompletedTask; + editor.ReplaceNode(node, replacement.WithTriviaFrom(node)); } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/UseIsNullCheckHelpers.cs b/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/UseIsNullCheckHelpers.cs index 40f03ad75bd56..77055e32f8108 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/UseIsNullCheckHelpers.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseIsNullCheck/UseIsNullCheckHelpers.cs @@ -4,25 +4,24 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck +namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck; + +internal static class UseIsNullCheckHelpers { - internal static class UseIsNullCheckHelpers + public static string GetTitle(bool negated, ParseOptions options) { - public static string GetTitle(bool negated, ParseOptions options) + if (negated) { - if (negated) - { - return SupportsIsNotPattern(options) - ? CSharpAnalyzersResources.Use_is_not_null_check - : CSharpAnalyzersResources.Use_is_object_check; - } - else - { - return CSharpAnalyzersResources.Use_is_null_check; - } + return SupportsIsNotPattern(options) + ? CSharpAnalyzersResources.Use_is_not_null_check + : CSharpAnalyzersResources.Use_is_object_check; + } + else + { + return CSharpAnalyzersResources.Use_is_null_check; } - - public static bool SupportsIsNotPattern(ParseOptions options) - => options.LanguageVersion() >= LanguageVersion.CSharp9; } + + public static bool SupportsIsNotPattern(ParseOptions options) + => options.LanguageVersion() >= LanguageVersion.CSharp9; } diff --git a/src/Analyzers/CSharp/CodeFixes/UseLocalFunction/CSharpUseLocalFunctionCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseLocalFunction/CSharpUseLocalFunctionCodeFixProvider.cs index 84db7b5e638ca..d49db09283500 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseLocalFunction/CSharpUseLocalFunctionCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseLocalFunction/CSharpUseLocalFunctionCodeFixProvider.cs @@ -26,301 +26,300 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseLocalFunction +namespace Microsoft.CodeAnalysis.CSharp.UseLocalFunction; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseLocalFunction), Shared] +internal class CSharpUseLocalFunctionCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseLocalFunction), Shared] - internal class CSharpUseLocalFunctionCodeFixProvider : SyntaxEditorBasedCodeFixProvider - { - private static readonly TypeSyntax s_objectType = SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword)); + private static readonly TypeSyntax s_objectType = SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword)); - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseLocalFunctionCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseLocalFunctionCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseLocalFunctionDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseLocalFunctionDiagnosticId]; - protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) - => !diagnostic.IsSuppressed; + protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) + => !diagnostic.IsSuppressed; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Use_local_function, nameof(CSharpAnalyzersResources.Use_local_function)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Use_local_function, nameof(CSharpAnalyzersResources.Use_local_function)); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var nodesFromDiagnostics = new List<( - LocalDeclarationStatementSyntax declaration, - AnonymousFunctionExpressionSyntax function, - List references)>(diagnostics.Length); + var nodesFromDiagnostics = new List<( + LocalDeclarationStatementSyntax declaration, + AnonymousFunctionExpressionSyntax function, + List references)>(diagnostics.Length); - var nodesToTrack = new HashSet(); + var nodesToTrack = new HashSet(); - foreach (var diagnostic in diagnostics) - { - var localDeclaration = (LocalDeclarationStatementSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken); - var anonymousFunction = (AnonymousFunctionExpressionSyntax)diagnostic.AdditionalLocations[1].FindNode(cancellationToken); + foreach (var diagnostic in diagnostics) + { + var localDeclaration = (LocalDeclarationStatementSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken); + var anonymousFunction = (AnonymousFunctionExpressionSyntax)diagnostic.AdditionalLocations[1].FindNode(cancellationToken); - var references = new List(diagnostic.AdditionalLocations.Count - 2); + var references = new List(diagnostic.AdditionalLocations.Count - 2); - for (var i = 2; i < diagnostic.AdditionalLocations.Count; i++) - { - references.Add((ExpressionSyntax)diagnostic.AdditionalLocations[i].FindNode(getInnermostNodeForTie: true, cancellationToken)); - } + for (var i = 2; i < diagnostic.AdditionalLocations.Count; i++) + { + references.Add((ExpressionSyntax)diagnostic.AdditionalLocations[i].FindNode(getInnermostNodeForTie: true, cancellationToken)); + } - nodesFromDiagnostics.Add((localDeclaration, anonymousFunction, references)); + nodesFromDiagnostics.Add((localDeclaration, anonymousFunction, references)); - nodesToTrack.Add(localDeclaration); - nodesToTrack.Add(anonymousFunction); - nodesToTrack.AddRange(references); - } + nodesToTrack.Add(localDeclaration); + nodesToTrack.Add(anonymousFunction); + nodesToTrack.AddRange(references); + } - var root = editor.OriginalRoot; - var currentRoot = root.TrackNodes(nodesToTrack); + var root = editor.OriginalRoot; + var currentRoot = root.TrackNodes(nodesToTrack); - var languageVersion = semanticModel.SyntaxTree.Options.LanguageVersion(); - var makeStaticIfPossible = false; + var languageVersion = semanticModel.SyntaxTree.Options.LanguageVersion(); + var makeStaticIfPossible = false; - if (languageVersion >= LanguageVersion.CSharp8) - { + if (languageVersion >= LanguageVersion.CSharp8) + { #if CODE_STYLE - var info = new CSharpCodeGenerationContextInfo( - CodeGenerationContext.Default, CSharpCodeGenerationOptions.Default, new CSharpCodeGenerationService(document.Project.GetExtendedLanguageServices().LanguageServices), root.SyntaxTree.Options.LanguageVersion()); + var info = new CSharpCodeGenerationContextInfo( + CodeGenerationContext.Default, CSharpCodeGenerationOptions.Default, new CSharpCodeGenerationService(document.Project.GetExtendedLanguageServices().LanguageServices), root.SyntaxTree.Options.LanguageVersion()); #else - var info = await document.GetCodeGenerationInfoAsync(CodeGenerationContext.Default, fallbackOptions, cancellationToken).ConfigureAwait(false); + var info = await document.GetCodeGenerationInfoAsync(CodeGenerationContext.Default, fallbackOptions, cancellationToken).ConfigureAwait(false); #endif - var options = (CSharpCodeGenerationOptions)info.Options; - makeStaticIfPossible = options.PreferStaticLocalFunction.Value; - } - - // Process declarations in reverse order so that we see the effects of nested - // declarations before processing the outer decls. - foreach (var (localDeclaration, anonymousFunction, references) in nodesFromDiagnostics.OrderByDescending(nodes => nodes.function.SpanStart)) - { - var delegateType = (INamedTypeSymbol)semanticModel.GetTypeInfo(anonymousFunction, cancellationToken).ConvertedType; - var parameterList = GenerateParameterList(editor.Generator, anonymousFunction, delegateType.DelegateInvokeMethod); - var makeStatic = MakeStatic(semanticModel, makeStaticIfPossible, localDeclaration, cancellationToken); - - var currentLocalDeclaration = currentRoot.GetCurrentNode(localDeclaration); - var currentAnonymousFunction = currentRoot.GetCurrentNode(anonymousFunction); - - currentRoot = ReplaceAnonymousWithLocalFunction( - document.Project.Solution.Services, currentRoot, - currentLocalDeclaration, currentAnonymousFunction, - delegateType.DelegateInvokeMethod, parameterList, makeStatic); - - // these invocations might actually be inside the local function! so we have to do this separately - currentRoot = ReplaceReferences( - document, currentRoot, - delegateType, parameterList, - references.Select(node => currentRoot.GetCurrentNode(node)).ToImmutableArray()); - } + var options = (CSharpCodeGenerationOptions)info.Options; + makeStaticIfPossible = options.PreferStaticLocalFunction.Value; + } - editor.ReplaceNode(root, currentRoot); + // Process declarations in reverse order so that we see the effects of nested + // declarations before processing the outer decls. + foreach (var (localDeclaration, anonymousFunction, references) in nodesFromDiagnostics.OrderByDescending(nodes => nodes.function.SpanStart)) + { + var delegateType = (INamedTypeSymbol)semanticModel.GetTypeInfo(anonymousFunction, cancellationToken).ConvertedType; + var parameterList = GenerateParameterList(editor.Generator, anonymousFunction, delegateType.DelegateInvokeMethod); + var makeStatic = MakeStatic(semanticModel, makeStaticIfPossible, localDeclaration, cancellationToken); + + var currentLocalDeclaration = currentRoot.GetCurrentNode(localDeclaration); + var currentAnonymousFunction = currentRoot.GetCurrentNode(anonymousFunction); + + currentRoot = ReplaceAnonymousWithLocalFunction( + document.Project.Solution.Services, currentRoot, + currentLocalDeclaration, currentAnonymousFunction, + delegateType.DelegateInvokeMethod, parameterList, makeStatic); + + // these invocations might actually be inside the local function! so we have to do this separately + currentRoot = ReplaceReferences( + document, currentRoot, + delegateType, parameterList, + references.Select(node => currentRoot.GetCurrentNode(node)).ToImmutableArray()); } - private static bool MakeStatic( - SemanticModel semanticModel, - bool makeStaticIfPossible, - LocalDeclarationStatementSyntax localDeclaration, - CancellationToken cancellationToken) + editor.ReplaceNode(root, currentRoot); + } + + private static bool MakeStatic( + SemanticModel semanticModel, + bool makeStaticIfPossible, + LocalDeclarationStatementSyntax localDeclaration, + CancellationToken cancellationToken) + { + // Determines if we can make the local function 'static'. We can make it static + // if the original lambda did not capture any variables (other than the local + // variable itself). it's ok for the lambda to capture itself as a static-local + // function can reference itself without any problems. + if (makeStaticIfPossible) { - // Determines if we can make the local function 'static'. We can make it static - // if the original lambda did not capture any variables (other than the local - // variable itself). it's ok for the lambda to capture itself as a static-local - // function can reference itself without any problems. - if (makeStaticIfPossible) - { - var localSymbol = semanticModel.GetDeclaredSymbol( - localDeclaration.Declaration.Variables[0], cancellationToken); + var localSymbol = semanticModel.GetDeclaredSymbol( + localDeclaration.Declaration.Variables[0], cancellationToken); - var dataFlow = semanticModel.AnalyzeDataFlow(localDeclaration); - if (dataFlow.Succeeded) + var dataFlow = semanticModel.AnalyzeDataFlow(localDeclaration); + if (dataFlow.Succeeded) + { + var capturedVariables = dataFlow.Captured.Remove(localSymbol); + if (capturedVariables.IsEmpty) { - var capturedVariables = dataFlow.Captured.Remove(localSymbol); - if (capturedVariables.IsEmpty) - { - return true; - } + return true; } } - - return false; } - private static SyntaxNode ReplaceAnonymousWithLocalFunction( - SolutionServices services, SyntaxNode currentRoot, - LocalDeclarationStatementSyntax localDeclaration, AnonymousFunctionExpressionSyntax anonymousFunction, - IMethodSymbol delegateMethod, ParameterListSyntax parameterList, bool makeStatic) - { - var newLocalFunctionStatement = CreateLocalFunctionStatement(localDeclaration, anonymousFunction, delegateMethod, parameterList, makeStatic) - .WithTriviaFrom(localDeclaration) - .WithAdditionalAnnotations(Formatter.Annotation); + return false; + } - var editor = new SyntaxEditor(currentRoot, services); - editor.ReplaceNode(localDeclaration, newLocalFunctionStatement); + private static SyntaxNode ReplaceAnonymousWithLocalFunction( + SolutionServices services, SyntaxNode currentRoot, + LocalDeclarationStatementSyntax localDeclaration, AnonymousFunctionExpressionSyntax anonymousFunction, + IMethodSymbol delegateMethod, ParameterListSyntax parameterList, bool makeStatic) + { + var newLocalFunctionStatement = CreateLocalFunctionStatement(localDeclaration, anonymousFunction, delegateMethod, parameterList, makeStatic) + .WithTriviaFrom(localDeclaration) + .WithAdditionalAnnotations(Formatter.Annotation); - var anonymousFunctionStatement = anonymousFunction.GetAncestor(); - if (anonymousFunctionStatement != localDeclaration) - { - // This is the split decl+init form. Remove the second statement as we're - // merging into the first one. - editor.RemoveNode(anonymousFunctionStatement); - } + var editor = new SyntaxEditor(currentRoot, services); + editor.ReplaceNode(localDeclaration, newLocalFunctionStatement); - return editor.GetChangedRoot(); + var anonymousFunctionStatement = anonymousFunction.GetAncestor(); + if (anonymousFunctionStatement != localDeclaration) + { + // This is the split decl+init form. Remove the second statement as we're + // merging into the first one. + editor.RemoveNode(anonymousFunctionStatement); } - private static SyntaxNode ReplaceReferences( - Document document, SyntaxNode currentRoot, - INamedTypeSymbol delegateType, ParameterListSyntax parameterList, - ImmutableArray references) + return editor.GetChangedRoot(); + } + + private static SyntaxNode ReplaceReferences( + Document document, SyntaxNode currentRoot, + INamedTypeSymbol delegateType, ParameterListSyntax parameterList, + ImmutableArray references) + { + return currentRoot.ReplaceNodes(references, (_ /* nested invocations! */, reference) => { - return currentRoot.ReplaceNodes(references, (_ /* nested invocations! */, reference) => + if (reference is InvocationExpressionSyntax invocation) { - if (reference is InvocationExpressionSyntax invocation) - { - var directInvocation = invocation.Expression is MemberAccessExpressionSyntax memberAccess // it's a .Invoke call - ? invocation.WithExpression(memberAccess.Expression).WithTriviaFrom(invocation) // remove it - : invocation; + var directInvocation = invocation.Expression is MemberAccessExpressionSyntax memberAccess // it's a .Invoke call + ? invocation.WithExpression(memberAccess.Expression).WithTriviaFrom(invocation) // remove it + : invocation; - return WithNewParameterNames(directInvocation, delegateType.DelegateInvokeMethod, parameterList); - } + return WithNewParameterNames(directInvocation, delegateType.DelegateInvokeMethod, parameterList); + } - // It's not an invocation. Wrap the identifier in a cast (which will be remove by the simplifier if unnecessary) - // to ensure we preserve semantics in cases like overload resolution or generic type inference. - return SyntaxGenerator.GetGenerator(document).CastExpression(delegateType, reference); - }); + // It's not an invocation. Wrap the identifier in a cast (which will be remove by the simplifier if unnecessary) + // to ensure we preserve semantics in cases like overload resolution or generic type inference. + return SyntaxGenerator.GetGenerator(document).CastExpression(delegateType, reference); + }); + } + + private static LocalFunctionStatementSyntax CreateLocalFunctionStatement( + LocalDeclarationStatementSyntax localDeclaration, + AnonymousFunctionExpressionSyntax anonymousFunction, + IMethodSymbol delegateMethod, + ParameterListSyntax parameterList, + bool makeStatic) + { + var modifiers = new SyntaxTokenList(); + if (makeStatic) + { + modifiers = modifiers.Add(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); } - private static LocalFunctionStatementSyntax CreateLocalFunctionStatement( - LocalDeclarationStatementSyntax localDeclaration, - AnonymousFunctionExpressionSyntax anonymousFunction, - IMethodSymbol delegateMethod, - ParameterListSyntax parameterList, - bool makeStatic) + if (anonymousFunction.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword)) { - var modifiers = new SyntaxTokenList(); - if (makeStatic) - { - modifiers = modifiers.Add(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); - } + modifiers = modifiers.Add(anonymousFunction.AsyncKeyword); + } - if (anonymousFunction.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword)) - { - modifiers = modifiers.Add(anonymousFunction.AsyncKeyword); - } + var returnType = delegateMethod.GenerateReturnTypeSyntax(); - var returnType = delegateMethod.GenerateReturnTypeSyntax(); + var identifier = localDeclaration.Declaration.Variables[0].Identifier; + var typeParameterList = (TypeParameterListSyntax)null; - var identifier = localDeclaration.Declaration.Variables[0].Identifier; - var typeParameterList = (TypeParameterListSyntax)null; + var constraintClauses = default(SyntaxList); - var constraintClauses = default(SyntaxList); + var body = anonymousFunction.Body is BlockSyntax block + ? block + : null; - var body = anonymousFunction.Body is BlockSyntax block - ? block - : null; + var expressionBody = anonymousFunction.Body is ExpressionSyntax expression + ? SyntaxFactory.ArrowExpressionClause(((LambdaExpressionSyntax)anonymousFunction).ArrowToken, expression) + : null; - var expressionBody = anonymousFunction.Body is ExpressionSyntax expression - ? SyntaxFactory.ArrowExpressionClause(((LambdaExpressionSyntax)anonymousFunction).ArrowToken, expression) - : null; + var semicolonToken = anonymousFunction.Body is ExpressionSyntax + ? localDeclaration.SemicolonToken + : default; - var semicolonToken = anonymousFunction.Body is ExpressionSyntax - ? localDeclaration.SemicolonToken - : default; + return SyntaxFactory.LocalFunctionStatement( + modifiers, returnType, identifier, typeParameterList, parameterList, + constraintClauses, body, expressionBody, semicolonToken); + } - return SyntaxFactory.LocalFunctionStatement( - modifiers, returnType, identifier, typeParameterList, parameterList, - constraintClauses, body, expressionBody, semicolonToken); - } + private static ParameterListSyntax GenerateParameterList( + SyntaxGenerator generator, AnonymousFunctionExpressionSyntax anonymousFunction, IMethodSymbol delegateMethod) + { + var parameterList = TryGetOrCreateParameterList(anonymousFunction); + var i = 0; - private static ParameterListSyntax GenerateParameterList( - SyntaxGenerator generator, AnonymousFunctionExpressionSyntax anonymousFunction, IMethodSymbol delegateMethod) - { - var parameterList = TryGetOrCreateParameterList(anonymousFunction); - var i = 0; + return parameterList != null + ? parameterList.ReplaceNodes(parameterList.Parameters, (parameterNode, _) => PromoteParameter(generator, parameterNode, delegateMethod.Parameters.ElementAtOrDefault(i++))) + : SyntaxFactory.ParameterList([.. delegateMethod.Parameters.Select(parameter => + PromoteParameter(generator, SyntaxFactory.Parameter(parameter.Name.ToIdentifierToken()), parameter))]); - return parameterList != null - ? parameterList.ReplaceNodes(parameterList.Parameters, (parameterNode, _) => PromoteParameter(generator, parameterNode, delegateMethod.Parameters.ElementAtOrDefault(i++))) - : SyntaxFactory.ParameterList([.. delegateMethod.Parameters.Select(parameter => - PromoteParameter(generator, SyntaxFactory.Parameter(parameter.Name.ToIdentifierToken()), parameter))]); + static ParameterSyntax PromoteParameter(SyntaxGenerator generator, ParameterSyntax parameterNode, IParameterSymbol delegateParameter) + { + // delegateParameter may be null, consider this case: Action x = (a, b) => { }; + // we will still fall back to object - static ParameterSyntax PromoteParameter(SyntaxGenerator generator, ParameterSyntax parameterNode, IParameterSymbol delegateParameter) + if (parameterNode.Type == null) { - // delegateParameter may be null, consider this case: Action x = (a, b) => { }; - // we will still fall back to object - - if (parameterNode.Type == null) - { - parameterNode = parameterNode.WithType(delegateParameter?.Type.GenerateTypeSyntax() ?? s_objectType); - } - - if (delegateParameter?.HasExplicitDefaultValue == true) - { - parameterNode = parameterNode.WithDefault(GetDefaultValue(generator, delegateParameter)); - } + parameterNode = parameterNode.WithType(delegateParameter?.Type.GenerateTypeSyntax() ?? s_objectType); + } - return parameterNode; + if (delegateParameter?.HasExplicitDefaultValue == true) + { + parameterNode = parameterNode.WithDefault(GetDefaultValue(generator, delegateParameter)); } + + return parameterNode; } + } - private static ParameterListSyntax TryGetOrCreateParameterList(AnonymousFunctionExpressionSyntax anonymousFunction) + private static ParameterListSyntax TryGetOrCreateParameterList(AnonymousFunctionExpressionSyntax anonymousFunction) + { + switch (anonymousFunction) { - switch (anonymousFunction) - { - case SimpleLambdaExpressionSyntax simpleLambda: - return SyntaxFactory.ParameterList([simpleLambda.Parameter]); - case ParenthesizedLambdaExpressionSyntax parenthesizedLambda: - return parenthesizedLambda.ParameterList; - case AnonymousMethodExpressionSyntax anonymousMethod: - return anonymousMethod.ParameterList; // may be null! - default: - throw ExceptionUtilities.UnexpectedValue(anonymousFunction); - } + case SimpleLambdaExpressionSyntax simpleLambda: + return SyntaxFactory.ParameterList([simpleLambda.Parameter]); + case ParenthesizedLambdaExpressionSyntax parenthesizedLambda: + return parenthesizedLambda.ParameterList; + case AnonymousMethodExpressionSyntax anonymousMethod: + return anonymousMethod.ParameterList; // may be null! + default: + throw ExceptionUtilities.UnexpectedValue(anonymousFunction); } + } - private static InvocationExpressionSyntax WithNewParameterNames(InvocationExpressionSyntax invocation, IMethodSymbol method, ParameterListSyntax newParameterList) + private static InvocationExpressionSyntax WithNewParameterNames(InvocationExpressionSyntax invocation, IMethodSymbol method, ParameterListSyntax newParameterList) + { + return invocation.ReplaceNodes(invocation.ArgumentList.Arguments, (argumentNode, _) => { - return invocation.ReplaceNodes(invocation.ArgumentList.Arguments, (argumentNode, _) => + if (argumentNode.NameColon == null) { - if (argumentNode.NameColon == null) - { - return argumentNode; - } - - var parameterIndex = TryDetermineParameterIndex(argumentNode.NameColon, method); - if (parameterIndex == -1) - { - return argumentNode; - } + return argumentNode; + } - var newParameter = newParameterList.Parameters.ElementAtOrDefault(parameterIndex); - if (newParameter == null || newParameter.Identifier.IsMissing) - { - return argumentNode; - } + var parameterIndex = TryDetermineParameterIndex(argumentNode.NameColon, method); + if (parameterIndex == -1) + { + return argumentNode; + } - return argumentNode.WithNameColon(argumentNode.NameColon.WithName(SyntaxFactory.IdentifierName(newParameter.Identifier))); - }); - } + var newParameter = newParameterList.Parameters.ElementAtOrDefault(parameterIndex); + if (newParameter == null || newParameter.Identifier.IsMissing) + { + return argumentNode; + } - private static int TryDetermineParameterIndex(NameColonSyntax argumentNameColon, IMethodSymbol method) - { - var name = argumentNameColon.Name.Identifier.ValueText; - return method.Parameters.IndexOf(p => p.Name == name); - } + return argumentNode.WithNameColon(argumentNode.NameColon.WithName(SyntaxFactory.IdentifierName(newParameter.Identifier))); + }); + } - private static EqualsValueClauseSyntax GetDefaultValue(SyntaxGenerator generator, IParameterSymbol parameter) - => SyntaxFactory.EqualsValueClause(ExpressionGenerator.GenerateExpression(generator, parameter.Type, parameter.ExplicitDefaultValue, canUseFieldReference: true)); + private static int TryDetermineParameterIndex(NameColonSyntax argumentNameColon, IMethodSymbol method) + { + var name = argumentNameColon.Name.Identifier.ValueText; + return method.Parameters.IndexOf(p => p.Name == name); } + + private static EqualsValueClauseSyntax GetDefaultValue(SyntaxGenerator generator, IParameterSymbol parameter) + => SyntaxFactory.EqualsValueClause(ExpressionGenerator.GenerateExpression(generator, parameter.Type, parameter.ExplicitDefaultValue, canUseFieldReference: true)); } diff --git a/src/Analyzers/CSharp/CodeFixes/UseNameofInAttribute/CSharpUseNameofInAttributeCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseNameofInAttribute/CSharpUseNameofInAttributeCodeFixProvider.cs index 07c5748d07759..e28a8b4ec7927 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseNameofInAttribute/CSharpUseNameofInAttributeCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseNameofInAttribute/CSharpUseNameofInAttributeCodeFixProvider.cs @@ -16,47 +16,46 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.UseNameofInAttribute +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.UseNameofInAttribute; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseNameofInAttribute), Shared] +internal sealed class CSharpUseNameofInAttributeCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseNameofInAttribute), Shared] - internal sealed class CSharpUseNameofInAttributeCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpUseNameofInAttributeCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpUseNameofInAttributeCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } = [IDEDiagnosticIds.UseNameofInAttributeDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds { get; } = [IDEDiagnosticIds.UseNameofInAttributeDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix( - context, - CSharpAnalyzersResources.Use_nameof, - nameof(CSharpAnalyzersResources.Use_nameof)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix( + context, + CSharpAnalyzersResources.Use_nameof, + nameof(CSharpAnalyzersResources.Use_nameof)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, - ImmutableArray diagnostics, - SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) + protected override Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) { - foreach (var diagnostic in diagnostics) - { - var expression = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - var name = diagnostic.Properties[CSharpUseNameofInAttributeDiagnosticAnalyzer.NameKey]; - Contract.ThrowIfNull(name); - - editor.ReplaceNode( - expression, - editor.Generator.NameOfExpression(editor.Generator.IdentifierName(name)).WithTriviaFrom(expression)); - } - - return Task.CompletedTask; + var expression = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); + var name = diagnostic.Properties[CSharpUseNameofInAttributeDiagnosticAnalyzer.NameKey]; + Contract.ThrowIfNull(name); + + editor.ReplaceNode( + expression, + editor.Generator.NameOfExpression(editor.Generator.IdentifierName(name)).WithTriviaFrom(expression)); } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseNullPropagation/CSharpUseNullPropagationCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseNullPropagation/CSharpUseNullPropagationCodeFixProvider.cs index 4c4185b410f4f..43774aabce8e5 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseNullPropagation/CSharpUseNullPropagationCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseNullPropagation/CSharpUseNullPropagationCodeFixProvider.cs @@ -9,54 +9,53 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.UseNullPropagation; -namespace Microsoft.CodeAnalysis.CSharp.UseNullPropagation +namespace Microsoft.CodeAnalysis.CSharp.UseNullPropagation; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseNullPropagation), Shared] +internal class CSharpUseNullPropagationCodeFixProvider : AbstractUseNullPropagationCodeFixProvider< + SyntaxKind, + ExpressionSyntax, + StatementSyntax, + ConditionalExpressionSyntax, + BinaryExpressionSyntax, + InvocationExpressionSyntax, + ConditionalAccessExpressionSyntax, + ElementAccessExpressionSyntax, + MemberAccessExpressionSyntax, + ElementBindingExpressionSyntax, + IfStatementSyntax, + ExpressionStatementSyntax, + BracketedArgumentListSyntax> { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseNullPropagation), Shared] - internal class CSharpUseNullPropagationCodeFixProvider : AbstractUseNullPropagationCodeFixProvider< - SyntaxKind, - ExpressionSyntax, - StatementSyntax, - ConditionalExpressionSyntax, - BinaryExpressionSyntax, - InvocationExpressionSyntax, - ConditionalAccessExpressionSyntax, - ElementAccessExpressionSyntax, - MemberAccessExpressionSyntax, - ElementBindingExpressionSyntax, - IfStatementSyntax, - ExpressionStatementSyntax, - BracketedArgumentListSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseNullPropagationCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseNullPropagationCodeFixProvider() - { - } + } - protected override bool TryGetBlock(SyntaxNode? statement, [NotNullWhen(true)] out StatementSyntax? block) + protected override bool TryGetBlock(SyntaxNode? statement, [NotNullWhen(true)] out StatementSyntax? block) + { + if (statement is BlockSyntax statementBlock) { - if (statement is BlockSyntax statementBlock) - { - block = statementBlock; - return true; - } - - block = null; - return false; + block = statementBlock; + return true; } - protected override StatementSyntax ReplaceBlockStatements(StatementSyntax block, StatementSyntax newInnerStatement) - => ((BlockSyntax)block).WithStatements([newInnerStatement]); + block = null; + return false; + } - protected override SyntaxNode PostProcessElseIf(IfStatementSyntax ifStatement, StatementSyntax newWhenTrueStatement) - { - var elseClauseSyntax = (ElseClauseSyntax)ifStatement.Parent!; - return elseClauseSyntax - .WithElseKeyword(elseClauseSyntax.ElseKeyword.WithTrailingTrivia()) - .WithStatement(newWhenTrueStatement.WithPrependedLeadingTrivia(ifStatement.CloseParenToken.TrailingTrivia)); - } + protected override StatementSyntax ReplaceBlockStatements(StatementSyntax block, StatementSyntax newInnerStatement) + => ((BlockSyntax)block).WithStatements([newInnerStatement]); - protected override ElementBindingExpressionSyntax ElementBindingExpression(BracketedArgumentListSyntax argumentList) - => SyntaxFactory.ElementBindingExpression(argumentList); + protected override SyntaxNode PostProcessElseIf(IfStatementSyntax ifStatement, StatementSyntax newWhenTrueStatement) + { + var elseClauseSyntax = (ElseClauseSyntax)ifStatement.Parent!; + return elseClauseSyntax + .WithElseKeyword(elseClauseSyntax.ElseKeyword.WithTrailingTrivia()) + .WithStatement(newWhenTrueStatement.WithPrependedLeadingTrivia(ifStatement.CloseParenToken.TrailingTrivia)); } + + protected override ElementBindingExpressionSyntax ElementBindingExpression(BracketedArgumentListSyntax argumentList) + => SyntaxFactory.ElementBindingExpression(argumentList); } diff --git a/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/UseInitializerHelpers.cs b/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/UseInitializerHelpers.cs index 7b6585f193692..f083978fdb53b 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/UseInitializerHelpers.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/UseInitializerHelpers.cs @@ -6,59 +6,58 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.CSharp.UseObjectInitializer -{ - using static SyntaxFactory; +namespace Microsoft.CodeAnalysis.CSharp.UseObjectInitializer; + +using static SyntaxFactory; - internal static class UseInitializerHelpers +internal static class UseInitializerHelpers +{ + public static BaseObjectCreationExpressionSyntax GetNewObjectCreation( + BaseObjectCreationExpressionSyntax baseObjectCreation, + SeparatedSyntaxList expressions) { - public static BaseObjectCreationExpressionSyntax GetNewObjectCreation( - BaseObjectCreationExpressionSyntax baseObjectCreation, - SeparatedSyntaxList expressions) + if (baseObjectCreation is ObjectCreationExpressionSyntax { ArgumentList.Arguments.Count: 0 } objectCreation) { - if (baseObjectCreation is ObjectCreationExpressionSyntax { ArgumentList.Arguments.Count: 0 } objectCreation) - { - baseObjectCreation = objectCreation - .WithType(objectCreation.Type.WithTrailingTrivia(objectCreation.ArgumentList.GetTrailingTrivia())) - .WithArgumentList(null); - } + baseObjectCreation = objectCreation + .WithType(objectCreation.Type.WithTrailingTrivia(objectCreation.ArgumentList.GetTrailingTrivia())) + .WithArgumentList(null); + } - var firstExpression = expressions.First(); - var initializerKind = firstExpression is AssignmentExpressionSyntax - ? SyntaxKind.ObjectInitializerExpression - : SyntaxKind.CollectionInitializerExpression; + var firstExpression = expressions.First(); + var initializerKind = firstExpression is AssignmentExpressionSyntax + ? SyntaxKind.ObjectInitializerExpression + : SyntaxKind.CollectionInitializerExpression; - return baseObjectCreation.WithInitializer(InitializerExpression(initializerKind, expressions)); - } + return baseObjectCreation.WithInitializer(InitializerExpression(initializerKind, expressions)); + } - public static void AddExistingItems( - BaseObjectCreationExpressionSyntax objectCreation, - ArrayBuilder nodesAndTokens, - bool addTrailingComma, - Func createElement) - where TMatch : struct - where TElementSyntax : SyntaxNode + public static void AddExistingItems( + BaseObjectCreationExpressionSyntax objectCreation, + ArrayBuilder nodesAndTokens, + bool addTrailingComma, + Func createElement) + where TMatch : struct + where TElementSyntax : SyntaxNode + { + if (objectCreation.Initializer != null) { - if (objectCreation.Initializer != null) + foreach (var nodeOrToken in objectCreation.Initializer.Expressions.GetWithSeparators()) { - foreach (var nodeOrToken in objectCreation.Initializer.Expressions.GetWithSeparators()) - { - if (nodeOrToken.IsToken) - nodesAndTokens.Add(nodeOrToken.AsToken()); - else - nodesAndTokens.Add(createElement(null, (ExpressionSyntax)nodeOrToken.AsNode()!)); - } + if (nodeOrToken.IsToken) + nodesAndTokens.Add(nodeOrToken.AsToken()); + else + nodesAndTokens.Add(createElement(null, (ExpressionSyntax)nodeOrToken.AsNode()!)); } + } - // If we have an odd number of elements already, add a comma at the end so that we can add the rest of the - // items afterwards without a syntax issue. - if (addTrailingComma && nodesAndTokens.Count % 2 == 1) - { - var last = nodesAndTokens.Last(); - nodesAndTokens.RemoveLast(); - nodesAndTokens.Add(last.WithTrailingTrivia()); - nodesAndTokens.Add(Token(SyntaxKind.CommaToken).WithTrailingTrivia(last.GetTrailingTrivia())); - } + // If we have an odd number of elements already, add a comma at the end so that we can add the rest of the + // items afterwards without a syntax issue. + if (addTrailingComma && nodesAndTokens.Count % 2 == 1) + { + var last = nodesAndTokens.Last(); + nodesAndTokens.RemoveLast(); + nodesAndTokens.Add(last.WithTrailingTrivia()); + nodesAndTokens.Add(Token(SyntaxKind.CommaToken).WithTrailingTrivia(last.GetTrailingTrivia())); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/UsePatternCombinators/CSharpUsePatternCombinatorsCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UsePatternCombinators/CSharpUsePatternCombinatorsCodeFixProvider.cs index 111d473e420ad..3a4b38255dff4 100644 --- a/src/Analyzers/CSharp/CodeFixes/UsePatternCombinators/CSharpUsePatternCombinatorsCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UsePatternCombinators/CSharpUsePatternCombinatorsCodeFixProvider.cs @@ -22,120 +22,119 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators +namespace Microsoft.CodeAnalysis.CSharp.UsePatternCombinators; + +using static SyntaxFactory; +using static AnalyzedPattern; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UsePatternCombinators), Shared] +internal class CSharpUsePatternCombinatorsCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - using static SyntaxFactory; - using static AnalyzedPattern; + private const string SafeEquivalenceKey = nameof(CSharpUsePatternCombinatorsCodeFixProvider) + "_safe"; + private const string UnsafeEquivalenceKey = nameof(CSharpUsePatternCombinatorsCodeFixProvider) + "_unsafe"; - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UsePatternCombinators), Shared] - internal class CSharpUsePatternCombinatorsCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUsePatternCombinatorsCodeFixProvider() { - private const string SafeEquivalenceKey = nameof(CSharpUsePatternCombinatorsCodeFixProvider) + "_safe"; - private const string UnsafeEquivalenceKey = nameof(CSharpUsePatternCombinatorsCodeFixProvider) + "_unsafe"; - - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUsePatternCombinatorsCodeFixProvider() - { - } + } - private static SyntaxKind MapToSyntaxKind(BinaryOperatorKind kind) + private static SyntaxKind MapToSyntaxKind(BinaryOperatorKind kind) + { + return kind switch { - return kind switch - { - BinaryOperatorKind.LessThan => SyntaxKind.LessThanToken, - BinaryOperatorKind.GreaterThan => SyntaxKind.GreaterThanToken, - BinaryOperatorKind.LessThanOrEqual => SyntaxKind.LessThanEqualsToken, - BinaryOperatorKind.GreaterThanOrEqual => SyntaxKind.GreaterThanEqualsToken, - _ => throw ExceptionUtilities.UnexpectedValue(kind) - }; - } + BinaryOperatorKind.LessThan => SyntaxKind.LessThanToken, + BinaryOperatorKind.GreaterThan => SyntaxKind.GreaterThanToken, + BinaryOperatorKind.LessThanOrEqual => SyntaxKind.LessThanEqualsToken, + BinaryOperatorKind.GreaterThanOrEqual => SyntaxKind.GreaterThanEqualsToken, + _ => throw ExceptionUtilities.UnexpectedValue(kind) + }; + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UsePatternCombinatorsDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UsePatternCombinatorsDiagnosticId]; - protected override bool IncludeDiagnosticDuringFixAll( - Diagnostic diagnostic, Document document, string? equivalenceKey, CancellationToken cancellationToken) - { - var isSafe = CSharpUsePatternCombinatorsDiagnosticAnalyzer.IsSafe(diagnostic); - return isSafe == (equivalenceKey == SafeEquivalenceKey); - } + protected override bool IncludeDiagnosticDuringFixAll( + Diagnostic diagnostic, Document document, string? equivalenceKey, CancellationToken cancellationToken) + { + var isSafe = CSharpUsePatternCombinatorsDiagnosticAnalyzer.IsSafe(diagnostic); + return isSafe == (equivalenceKey == SafeEquivalenceKey); + } - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var diagnostic = context.Diagnostics.First(); - var isSafe = CSharpUsePatternCombinatorsDiagnosticAnalyzer.IsSafe(diagnostic); + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); + var isSafe = CSharpUsePatternCombinatorsDiagnosticAnalyzer.IsSafe(diagnostic); - RegisterCodeFix( - context, - isSafe ? CSharpAnalyzersResources.Use_pattern_matching : CSharpAnalyzersResources.Use_pattern_matching_may_change_code_meaning, - isSafe ? SafeEquivalenceKey : UnsafeEquivalenceKey, - CodeActionPriority.Low); + RegisterCodeFix( + context, + isSafe ? CSharpAnalyzersResources.Use_pattern_matching : CSharpAnalyzersResources.Use_pattern_matching_may_change_code_meaning, + isSafe ? SafeEquivalenceKey : UnsafeEquivalenceKey, + CodeActionPriority.Low); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + foreach (var diagnostic in diagnostics) { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - foreach (var diagnostic in diagnostics) - { - var location = diagnostic.Location; - var expression = editor.OriginalRoot.FindNode(location.SourceSpan, getInnermostNodeForTie: true); - var operation = semanticModel.GetOperation(expression, cancellationToken); - RoslynDebug.AssertNotNull(operation); - var pattern = CSharpUsePatternCombinatorsAnalyzer.Analyze(operation); - RoslynDebug.AssertNotNull(pattern); - var patternSyntax = AsPatternSyntax(pattern).WithAdditionalAnnotations(Formatter.Annotation); - editor.ReplaceNode(expression, IsPatternExpression((ExpressionSyntax)pattern.Target.Syntax, patternSyntax)); - } + var location = diagnostic.Location; + var expression = editor.OriginalRoot.FindNode(location.SourceSpan, getInnermostNodeForTie: true); + var operation = semanticModel.GetOperation(expression, cancellationToken); + RoslynDebug.AssertNotNull(operation); + var pattern = CSharpUsePatternCombinatorsAnalyzer.Analyze(operation); + RoslynDebug.AssertNotNull(pattern); + var patternSyntax = AsPatternSyntax(pattern).WithAdditionalAnnotations(Formatter.Annotation); + editor.ReplaceNode(expression, IsPatternExpression((ExpressionSyntax)pattern.Target.Syntax, patternSyntax)); } + } - private static PatternSyntax AsPatternSyntax(AnalyzedPattern pattern) + private static PatternSyntax AsPatternSyntax(AnalyzedPattern pattern) + { + return pattern switch { - return pattern switch - { - Binary p => BinaryPattern( - p.IsDisjunctive ? SyntaxKind.OrPattern : SyntaxKind.AndPattern, - AsPatternSyntax(p.Left).Parenthesize(), - Token(p.Token.LeadingTrivia, p.IsDisjunctive ? SyntaxKind.OrKeyword : SyntaxKind.AndKeyword, - [.. p.Token.GetAllTrailingTrivia()]), - AsPatternSyntax(p.Right).Parenthesize()), - Constant p => ConstantPattern(AsExpressionSyntax(p.ExpressionSyntax, p)), - Source p => p.PatternSyntax, - Type p => TypePattern(p.TypeSyntax), - Relational p => RelationalPattern(Token(MapToSyntaxKind(p.OperatorKind)), AsExpressionSyntax(p.Value, p)), - Not p => UnaryPattern(AsPatternSyntax(p.Pattern).Parenthesize()), - var p => throw ExceptionUtilities.UnexpectedValue(p) - }; - } + Binary p => BinaryPattern( + p.IsDisjunctive ? SyntaxKind.OrPattern : SyntaxKind.AndPattern, + AsPatternSyntax(p.Left).Parenthesize(), + Token(p.Token.LeadingTrivia, p.IsDisjunctive ? SyntaxKind.OrKeyword : SyntaxKind.AndKeyword, + [.. p.Token.GetAllTrailingTrivia()]), + AsPatternSyntax(p.Right).Parenthesize()), + Constant p => ConstantPattern(AsExpressionSyntax(p.ExpressionSyntax, p)), + Source p => p.PatternSyntax, + Type p => TypePattern(p.TypeSyntax), + Relational p => RelationalPattern(Token(MapToSyntaxKind(p.OperatorKind)), AsExpressionSyntax(p.Value, p)), + Not p => UnaryPattern(AsPatternSyntax(p.Pattern).Parenthesize()), + var p => throw ExceptionUtilities.UnexpectedValue(p) + }; + } - private static ExpressionSyntax AsExpressionSyntax(ExpressionSyntax expr, AnalyzedPattern p) + private static ExpressionSyntax AsExpressionSyntax(ExpressionSyntax expr, AnalyzedPattern p) + { + var semanticModel = p.Target.SemanticModel; + RoslynDebug.Assert(semanticModel != null); + var type = semanticModel.GetTypeInfo(expr).Type; + if (type != null) { - var semanticModel = p.Target.SemanticModel; - RoslynDebug.Assert(semanticModel != null); - var type = semanticModel.GetTypeInfo(expr).Type; - if (type != null) - { - // default literals are not permitted in patterns - if (expr.IsKind(SyntaxKind.DefaultLiteralExpression)) - return DefaultExpression(type.GenerateTypeSyntax()); - - // 'null' is already the right form in a pattern, it does not need to be casted to anything else. - if (expr.IsKind(SyntaxKind.NullLiteralExpression)) - return expr; - - // if we have a nullable value type, only cast to the underlying type. - // - // `x is (long?)0` is not legal, only `x is (long)0` is. - var governingType = semanticModel.GetTypeInfo(p.Target.Syntax).Type.RemoveNullableIfPresent(); - if (governingType != null && !governingType.Equals(type)) - return CastExpression(governingType.GenerateTypeSyntax(), expr.Parenthesize()).WithAdditionalAnnotations(Simplifier.Annotation); - } - - return expr.Parenthesize(); + // default literals are not permitted in patterns + if (expr.IsKind(SyntaxKind.DefaultLiteralExpression)) + return DefaultExpression(type.GenerateTypeSyntax()); + + // 'null' is already the right form in a pattern, it does not need to be casted to anything else. + if (expr.IsKind(SyntaxKind.NullLiteralExpression)) + return expr; + + // if we have a nullable value type, only cast to the underlying type. + // + // `x is (long?)0` is not legal, only `x is (long)0` is. + var governingType = semanticModel.GetTypeInfo(p.Target.Syntax).Type.RemoveNullableIfPresent(); + if (governingType != null && !governingType.Equals(type)) + return CastExpression(governingType.GenerateTypeSyntax(), expr.Parenthesize()).WithAdditionalAnnotations(Simplifier.Annotation); } + + return expr.Parenthesize(); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpAsAndMemberAccessCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpAsAndMemberAccessCodeFixProvider.cs index 471814dd030c5..2fae3cf5660b0 100644 --- a/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpAsAndMemberAccessCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpAsAndMemberAccessCodeFixProvider.cs @@ -18,127 +18,126 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching +namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching; + +using static SyntaxFactory; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UsePatternMatchingAsAndMemberAccess), Shared] +internal partial class CSharpAsAndMemberAccessCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - using static SyntaxFactory; + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpAsAndMemberAccessCodeFixProvider() + { + } - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UsePatternMatchingAsAndMemberAccess), Shared] - internal partial class CSharpAsAndMemberAccessCodeFixProvider : SyntaxEditorBasedCodeFixProvider + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UsePatternMatchingAsAndMemberAccessDiagnosticId]; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpAsAndMemberAccessCodeFixProvider() - { - } + RegisterCodeFix(context, CSharpAnalyzersResources.Use_pattern_matching, nameof(CSharpAnalyzersResources.Use_pattern_matching)); + return Task.CompletedTask; + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UsePatternMatchingAsAndMemberAccessDiagnosticId]; + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start)) + FixOne(editor, diagnostic, cancellationToken); - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Use_pattern_matching, nameof(CSharpAnalyzersResources.Use_pattern_matching)); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - foreach (var diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start)) - FixOne(editor, diagnostic, cancellationToken); + private static void FixOne(SyntaxEditor editor, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); + if (node is not BinaryExpressionSyntax asExpression) + return; - return Task.CompletedTask; + if (!UsePatternMatchingHelpers.TryGetPartsOfAsAndMemberAccessCheck( + asExpression, out var conditionalAccessExpression, out var binaryExpression, out var isPatternExpression, out _)) + { + return; } - private static void FixOne(SyntaxEditor editor, Diagnostic diagnostic, CancellationToken cancellationToken) + var parent = binaryExpression ?? (ExpressionSyntax?)isPatternExpression; + Contract.ThrowIfNull(parent); + + // { X.Y: pattern } + var propertyPattern = PropertyPatternClause( + Token(SyntaxKind.OpenBraceToken).WithoutTrivia().WithAppendedTrailingTrivia(Space), + [Subpattern( + CreateExpressionColon(conditionalAccessExpression), + CreatePattern(binaryExpression, isPatternExpression).WithTrailingTrivia(Space))], + Token(SyntaxKind.CloseBraceToken).WithoutTrivia()); + + // T { X.Y: pattern } + var newPattern = RecursivePattern( + (TypeSyntax)asExpression.Right.WithAppendedTrailingTrivia(Space), + positionalPatternClause: null, + propertyPattern, + designation: null); + + // is T { X.Y: pattern } + var newIsExpression = IsPatternExpression( + asExpression.Left, + Token(SyntaxKind.IsKeyword).WithTriviaFrom(asExpression.OperatorToken), + newPattern); + + var toReplace = parent.WalkUpParentheses(); + editor.ReplaceNode( + toReplace, + newIsExpression.WithTriviaFrom(toReplace)); + + return; + + static BaseExpressionColonSyntax CreateExpressionColon(ConditionalAccessExpressionSyntax conditionalAccessExpression) { - var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - if (node is not BinaryExpressionSyntax asExpression) - return; + var whenNotNull = conditionalAccessExpression.WhenNotNull; - if (!UsePatternMatchingHelpers.TryGetPartsOfAsAndMemberAccessCheck( - asExpression, out var conditionalAccessExpression, out var binaryExpression, out var isPatternExpression, out _)) - { - return; - } - - var parent = binaryExpression ?? (ExpressionSyntax?)isPatternExpression; - Contract.ThrowIfNull(parent); - - // { X.Y: pattern } - var propertyPattern = PropertyPatternClause( - Token(SyntaxKind.OpenBraceToken).WithoutTrivia().WithAppendedTrailingTrivia(Space), - [Subpattern( - CreateExpressionColon(conditionalAccessExpression), - CreatePattern(binaryExpression, isPatternExpression).WithTrailingTrivia(Space))], - Token(SyntaxKind.CloseBraceToken).WithoutTrivia()); - - // T { X.Y: pattern } - var newPattern = RecursivePattern( - (TypeSyntax)asExpression.Right.WithAppendedTrailingTrivia(Space), - positionalPatternClause: null, - propertyPattern, - designation: null); - - // is T { X.Y: pattern } - var newIsExpression = IsPatternExpression( - asExpression.Left, - Token(SyntaxKind.IsKeyword).WithTriviaFrom(asExpression.OperatorToken), - newPattern); - - var toReplace = parent.WalkUpParentheses(); - editor.ReplaceNode( - toReplace, - newIsExpression.WithTriviaFrom(toReplace)); - - return; + if (whenNotNull is MemberBindingExpressionSyntax { Name: IdentifierNameSyntax identifierName }) + return NameColon(identifierName); - static BaseExpressionColonSyntax CreateExpressionColon(ConditionalAccessExpressionSyntax conditionalAccessExpression) - { - var whenNotNull = conditionalAccessExpression.WhenNotNull; + return ExpressionColon(RewriteMemberBindingToExpression(whenNotNull), Token(SyntaxKind.ColonToken)); + } - if (whenNotNull is MemberBindingExpressionSyntax { Name: IdentifierNameSyntax identifierName }) - return NameColon(identifierName); + static ExpressionSyntax RewriteMemberBindingToExpression(ExpressionSyntax expression) + { + // .X => X + if (expression is MemberBindingExpressionSyntax memberBinding) + return memberBinding.Name; - return ExpressionColon(RewriteMemberBindingToExpression(whenNotNull), Token(SyntaxKind.ColonToken)); - } + // .X.Y recurse down left side to produce: X.Y + if (expression is MemberAccessExpressionSyntax memberAccessExpression) + return memberAccessExpression.WithExpression(RewriteMemberBindingToExpression(memberAccessExpression.Expression)); - static ExpressionSyntax RewriteMemberBindingToExpression(ExpressionSyntax expression) - { - // .X => X - if (expression is MemberBindingExpressionSyntax memberBinding) - return memberBinding.Name; + return expression; + } - // .X.Y recurse down left side to produce: X.Y - if (expression is MemberAccessExpressionSyntax memberAccessExpression) - return memberAccessExpression.WithExpression(RewriteMemberBindingToExpression(memberAccessExpression.Expression)); + static PatternSyntax CreatePattern(BinaryExpressionSyntax? binaryExpression, IsPatternExpressionSyntax? isPatternExpression) + { + // if we had `.X.Y is some_pattern` we can just convert that to `X.Y: some_pattern` + if (isPatternExpression != null) + return isPatternExpression.Pattern; - return expression; - } + Contract.ThrowIfNull(binaryExpression); - static PatternSyntax CreatePattern(BinaryExpressionSyntax? binaryExpression, IsPatternExpressionSyntax? isPatternExpression) + return binaryExpression.Kind() switch { - // if we had `.X.Y is some_pattern` we can just convert that to `X.Y: some_pattern` - if (isPatternExpression != null) - return isPatternExpression.Pattern; - - Contract.ThrowIfNull(binaryExpression); - - return binaryExpression.Kind() switch - { - // `.X.Y == expr` => `X.Y: expr` - SyntaxKind.EqualsExpression => ConstantPattern(binaryExpression.Right), - // `.X.Y != expr` => `X.Y: not expr` - SyntaxKind.NotEqualsExpression => UnaryPattern(ConstantPattern(binaryExpression.Right)), - // `.X.Y > expr` => `X.Y: > expr` - // etc - SyntaxKind.GreaterThanExpression or - SyntaxKind.GreaterThanOrEqualExpression or - SyntaxKind.LessThanExpression or - SyntaxKind.LessThanOrEqualExpression => RelationalPattern(binaryExpression.OperatorToken, binaryExpression.Right), - _ => throw ExceptionUtilities.Unreachable() - }; - } + // `.X.Y == expr` => `X.Y: expr` + SyntaxKind.EqualsExpression => ConstantPattern(binaryExpression.Right), + // `.X.Y != expr` => `X.Y: not expr` + SyntaxKind.NotEqualsExpression => UnaryPattern(ConstantPattern(binaryExpression.Right)), + // `.X.Y > expr` => `X.Y: > expr` + // etc + SyntaxKind.GreaterThanExpression or + SyntaxKind.GreaterThanOrEqualExpression or + SyntaxKind.LessThanExpression or + SyntaxKind.LessThanOrEqualExpression => RelationalPattern(binaryExpression.OperatorToken, binaryExpression.Right), + _ => throw ExceptionUtilities.Unreachable() + }; } } } diff --git a/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpAsAndNullCheckCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpAsAndNullCheckCodeFixProvider.cs index 3683adb154738..a9fd0a6291559 100644 --- a/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpAsAndNullCheckCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpAsAndNullCheckCodeFixProvider.cs @@ -18,168 +18,167 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching +namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UsePatternMatchingAsAndNullCheck), Shared] +internal partial class CSharpAsAndNullCheckCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UsePatternMatchingAsAndNullCheck), Shared] - internal partial class CSharpAsAndNullCheckCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAsAndNullCheckCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAsAndNullCheckCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.InlineAsTypeCheckId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.InlineAsTypeCheckId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Use_pattern_matching, nameof(CSharpAnalyzersResources.Use_pattern_matching)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Use_pattern_matching, nameof(CSharpAnalyzersResources.Use_pattern_matching)); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - using var _1 = PooledHashSet.GetInstance(out var declaratorLocations); - using var _2 = PooledHashSet.GetInstance(out var statementParentScopes); + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + using var _1 = PooledHashSet.GetInstance(out var declaratorLocations); + using var _2 = PooledHashSet.GetInstance(out var statementParentScopes); - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var languageVersion = tree.Options.LanguageVersion(); + var languageVersion = tree.Options.LanguageVersion(); - foreach (var diagnostic in diagnostics) - { - cancellationToken.ThrowIfCancellationRequested(); + foreach (var diagnostic in diagnostics) + { + cancellationToken.ThrowIfCancellationRequested(); - if (declaratorLocations.Add(diagnostic.AdditionalLocations[0])) - AddEdits(editor, semanticModel, diagnostic, languageVersion, RemoveStatement, cancellationToken); - } + if (declaratorLocations.Add(diagnostic.AdditionalLocations[0])) + AddEdits(editor, semanticModel, diagnostic, languageVersion, RemoveStatement, cancellationToken); + } - foreach (var parentScope in statementParentScopes) + foreach (var parentScope in statementParentScopes) + { + editor.ReplaceNode(parentScope, (newParentScope, syntaxGenerator) => { - editor.ReplaceNode(parentScope, (newParentScope, syntaxGenerator) => - { - var firstStatement = newParentScope is BlockSyntax - ? ((BlockSyntax)newParentScope).Statements.First() - : ((SwitchSectionSyntax)newParentScope).Statements.First(); - return syntaxGenerator.ReplaceNode(newParentScope, firstStatement, firstStatement.WithoutLeadingBlankLinesInTrivia()); - }); - } + var firstStatement = newParentScope is BlockSyntax + ? ((BlockSyntax)newParentScope).Statements.First() + : ((SwitchSectionSyntax)newParentScope).Statements.First(); + return syntaxGenerator.ReplaceNode(newParentScope, firstStatement, firstStatement.WithoutLeadingBlankLinesInTrivia()); + }); + } - return; + return; - void RemoveStatement(StatementSyntax statement) + void RemoveStatement(StatementSyntax statement) + { + editor.RemoveNode(statement, SyntaxRemoveOptions.KeepNoTrivia); + if (statement.Parent is BlockSyntax or SwitchSectionSyntax) { - editor.RemoveNode(statement, SyntaxRemoveOptions.KeepNoTrivia); - if (statement.Parent is BlockSyntax or SwitchSectionSyntax) - { - statementParentScopes.Add(statement.Parent); - } + statementParentScopes.Add(statement.Parent); } } + } - private static void AddEdits( - SyntaxEditor editor, - SemanticModel semanticModel, - Diagnostic diagnostic, - LanguageVersion languageVersion, - Action removeStatement, - CancellationToken cancellationToken) - { - var declaratorLocation = diagnostic.AdditionalLocations[0]; - var comparisonLocation = diagnostic.AdditionalLocations[1]; - var asExpressionLocation = diagnostic.AdditionalLocations[2]; + private static void AddEdits( + SyntaxEditor editor, + SemanticModel semanticModel, + Diagnostic diagnostic, + LanguageVersion languageVersion, + Action removeStatement, + CancellationToken cancellationToken) + { + var declaratorLocation = diagnostic.AdditionalLocations[0]; + var comparisonLocation = diagnostic.AdditionalLocations[1]; + var asExpressionLocation = diagnostic.AdditionalLocations[2]; - var declarator = (VariableDeclaratorSyntax)declaratorLocation.FindNode(cancellationToken); - var comparison = (ExpressionSyntax)comparisonLocation.FindNode(cancellationToken); - var asExpression = (BinaryExpressionSyntax)asExpressionLocation.FindNode(cancellationToken); + var declarator = (VariableDeclaratorSyntax)declaratorLocation.FindNode(cancellationToken); + var comparison = (ExpressionSyntax)comparisonLocation.FindNode(cancellationToken); + var asExpression = (BinaryExpressionSyntax)asExpressionLocation.FindNode(cancellationToken); - var rightSideOfComparison = comparison is BinaryExpressionSyntax binaryExpression - ? (SyntaxNode)binaryExpression.Right - : ((IsPatternExpressionSyntax)comparison).Pattern; - var newIdentifier = declarator.Identifier - .WithoutTrivia().WithTrailingTrivia(rightSideOfComparison.GetTrailingTrivia()); + var rightSideOfComparison = comparison is BinaryExpressionSyntax binaryExpression + ? (SyntaxNode)binaryExpression.Right + : ((IsPatternExpressionSyntax)comparison).Pattern; + var newIdentifier = declarator.Identifier + .WithoutTrivia().WithTrailingTrivia(rightSideOfComparison.GetTrailingTrivia()); - var declarationPattern = SyntaxFactory.DeclarationPattern( - GetPatternType().WithoutTrivia().WithTrailingTrivia(SyntaxFactory.ElasticMarker), - SyntaxFactory.SingleVariableDesignation(newIdentifier)); + var declarationPattern = SyntaxFactory.DeclarationPattern( + GetPatternType().WithoutTrivia().WithTrailingTrivia(SyntaxFactory.ElasticMarker), + SyntaxFactory.SingleVariableDesignation(newIdentifier)); - var condition = GetCondition(languageVersion, comparison, asExpression, declarationPattern); + var condition = GetCondition(languageVersion, comparison, asExpression, declarationPattern); - if (declarator.Parent is VariableDeclarationSyntax declaration && - declaration.Parent is LocalDeclarationStatementSyntax localDeclaration && - declaration.Variables.Count == 1) - { - // Trivia on the local declaration will move to the next statement. - // use the callback form as the next statement may be the place where we're - // inlining the declaration, and thus need to see the effects of that change. - editor.ReplaceNode( - localDeclaration.GetNextStatement()!, - (s, g) => s.WithPrependedNonIndentationTriviaFrom(localDeclaration) - .WithAdditionalAnnotations(Formatter.Annotation)); - - removeStatement(localDeclaration); - } - else - { - editor.RemoveNode(declarator, SyntaxRemoveOptions.KeepUnbalancedDirectives); - } + if (declarator.Parent is VariableDeclarationSyntax declaration && + declaration.Parent is LocalDeclarationStatementSyntax localDeclaration && + declaration.Variables.Count == 1) + { + // Trivia on the local declaration will move to the next statement. + // use the callback form as the next statement may be the place where we're + // inlining the declaration, and thus need to see the effects of that change. + editor.ReplaceNode( + localDeclaration.GetNextStatement()!, + (s, g) => s.WithPrependedNonIndentationTriviaFrom(localDeclaration) + .WithAdditionalAnnotations(Formatter.Annotation)); + + removeStatement(localDeclaration); + } + else + { + editor.RemoveNode(declarator, SyntaxRemoveOptions.KeepUnbalancedDirectives); + } - editor.ReplaceNode(comparison, condition.WithTriviaFrom(comparison)); + editor.ReplaceNode(comparison, condition.WithTriviaFrom(comparison)); - return; + return; - TypeSyntax GetPatternType() + TypeSyntax GetPatternType() + { + // Complex case: object?[]? arr = obj as object[]; + // + // Because of array variance, the above is legal. We want the `object?[]` from the LHS here. + if (semanticModel.GetDeclaredSymbol(declarator, cancellationToken) is ILocalSymbol local) { - // Complex case: object?[]? arr = obj as object[]; - // - // Because of array variance, the above is legal. We want the `object?[]` from the LHS here. - if (semanticModel.GetDeclaredSymbol(declarator, cancellationToken) is ILocalSymbol local) + var asExpressionTypeInfo = semanticModel.GetTypeInfo(asExpression, cancellationToken); + if (asExpressionTypeInfo.Type != null) { - var asExpressionTypeInfo = semanticModel.GetTypeInfo(asExpression, cancellationToken); - if (asExpressionTypeInfo.Type != null) + // Strip off the outer ? if present. But the inner ? will still be there. + var localType = local.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + var asType = asExpressionTypeInfo.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + + // If they're the same types, except for the inner ?, then use the local's type here. + if (SymbolEqualityComparer.Default.Equals(localType, asType) && + !SymbolEqualityComparer.IncludeNullability.Equals(localType, asType)) { - // Strip off the outer ? if present. But the inner ? will still be there. - var localType = local.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - var asType = asExpressionTypeInfo.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - - // If they're the same types, except for the inner ?, then use the local's type here. - if (SymbolEqualityComparer.Default.Equals(localType, asType) && - !SymbolEqualityComparer.IncludeNullability.Equals(localType, asType)) - { - return localType.GenerateTypeSyntax(allowVar: false); - } + return localType.GenerateTypeSyntax(allowVar: false); } } - - return (TypeSyntax)asExpression.Right; } - } - private static ExpressionSyntax GetCondition( - LanguageVersion languageVersion, - ExpressionSyntax comparison, - BinaryExpressionSyntax asExpression, - DeclarationPatternSyntax declarationPattern) - { - var isPatternExpression = SyntaxFactory.IsPatternExpression(asExpression.Left, declarationPattern); + return (TypeSyntax)asExpression.Right; + } + } - // We should negate the is-expression if we have something like "x == null" or "x is null" - if (comparison.Kind() is not (SyntaxKind.EqualsExpression or SyntaxKind.IsPatternExpression)) - return isPatternExpression; + private static ExpressionSyntax GetCondition( + LanguageVersion languageVersion, + ExpressionSyntax comparison, + BinaryExpressionSyntax asExpression, + DeclarationPatternSyntax declarationPattern) + { + var isPatternExpression = SyntaxFactory.IsPatternExpression(asExpression.Left, declarationPattern); - if (languageVersion >= LanguageVersion.CSharp9) - { - // In C# 9 and higher, convert to `x is not string s`. - return isPatternExpression.WithPattern( - SyntaxFactory.UnaryPattern(SyntaxFactory.Token(SyntaxKind.NotKeyword), isPatternExpression.Pattern)); - } + // We should negate the is-expression if we have something like "x == null" or "x is null" + if (comparison.Kind() is not (SyntaxKind.EqualsExpression or SyntaxKind.IsPatternExpression)) + return isPatternExpression; - // In C# 8 and lower, convert to `!(x is string s)` - return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, isPatternExpression.Parenthesize()); + if (languageVersion >= LanguageVersion.CSharp9) + { + // In C# 9 and higher, convert to `x is not string s`. + return isPatternExpression.WithPattern( + SyntaxFactory.UnaryPattern(SyntaxFactory.Token(SyntaxKind.NotKeyword), isPatternExpression.Pattern)); } + + // In C# 8 and lower, convert to `!(x is string s)` + return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, isPatternExpression.Parenthesize()); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpIsAndCastCheckCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpIsAndCastCheckCodeFixProvider.cs index f081c20e78544..cf0d065725f49 100644 --- a/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpIsAndCastCheckCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpIsAndCastCheckCodeFixProvider.cs @@ -21,86 +21,85 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching +namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UsePatternMatchingIsAndCastCheck), Shared] +internal partial class CSharpIsAndCastCheckCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UsePatternMatchingIsAndCastCheck), Shared] - internal partial class CSharpIsAndCastCheckCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpIsAndCastCheckCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpIsAndCastCheckCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.InlineIsTypeCheckId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.InlineIsTypeCheckId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Use_pattern_matching, nameof(CSharpAnalyzersResources.Use_pattern_matching)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Use_pattern_matching, nameof(CSharpAnalyzersResources.Use_pattern_matching)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) { - foreach (var diagnostic in diagnostics) - { - cancellationToken.ThrowIfCancellationRequested(); - AddEdits(editor, diagnostic, cancellationToken); - } - - return Task.CompletedTask; + cancellationToken.ThrowIfCancellationRequested(); + AddEdits(editor, diagnostic, cancellationToken); } - private static void AddEdits( - SyntaxEditor editor, - Diagnostic diagnostic, - CancellationToken cancellationToken) - { - var ifStatementLocation = diagnostic.AdditionalLocations[0]; - var localDeclarationLocation = diagnostic.AdditionalLocations[1]; + return Task.CompletedTask; + } - var ifStatement = (IfStatementSyntax)ifStatementLocation.FindNode(cancellationToken); - var localDeclaration = (LocalDeclarationStatementSyntax)localDeclarationLocation.FindNode(cancellationToken); - var isExpression = (BinaryExpressionSyntax)ifStatement.Condition; + private static void AddEdits( + SyntaxEditor editor, + Diagnostic diagnostic, + CancellationToken cancellationToken) + { + var ifStatementLocation = diagnostic.AdditionalLocations[0]; + var localDeclarationLocation = diagnostic.AdditionalLocations[1]; - var updatedCondition = SyntaxFactory.IsPatternExpression( - isExpression.Left, SyntaxFactory.DeclarationPattern( - ((TypeSyntax)isExpression.Right).WithoutTrivia(), - SyntaxFactory.SingleVariableDesignation( - localDeclaration.Declaration.Variables[0].Identifier.WithoutTrivia()))); + var ifStatement = (IfStatementSyntax)ifStatementLocation.FindNode(cancellationToken); + var localDeclaration = (LocalDeclarationStatementSyntax)localDeclarationLocation.FindNode(cancellationToken); + var isExpression = (BinaryExpressionSyntax)ifStatement.Condition; - var trivia = localDeclaration.GetLeadingTrivia().Concat(localDeclaration.GetTrailingTrivia()) - .Where(t => t.IsSingleOrMultiLineComment()) - .SelectMany(t => ImmutableArray.Create(SyntaxFactory.Space, t, SyntaxFactory.ElasticCarriageReturnLineFeed)) - .ToImmutableArray(); + var updatedCondition = SyntaxFactory.IsPatternExpression( + isExpression.Left, SyntaxFactory.DeclarationPattern( + ((TypeSyntax)isExpression.Right).WithoutTrivia(), + SyntaxFactory.SingleVariableDesignation( + localDeclaration.Declaration.Variables[0].Identifier.WithoutTrivia()))); - editor.RemoveNode(localDeclaration); - editor.ReplaceNode(ifStatement, - (i, g) => - { - // Because the local declaration is *inside* the 'if', we need to get the 'if' - // statement after it was already modified and *then* update the condition - // portion of it. - var currentIf = (IfStatementSyntax)i; - return GetUpdatedIfStatement(updatedCondition, trivia, ifStatement, currentIf); - }); - } + var trivia = localDeclaration.GetLeadingTrivia().Concat(localDeclaration.GetTrailingTrivia()) + .Where(t => t.IsSingleOrMultiLineComment()) + .SelectMany(t => ImmutableArray.Create(SyntaxFactory.Space, t, SyntaxFactory.ElasticCarriageReturnLineFeed)) + .ToImmutableArray(); - private static IfStatementSyntax GetUpdatedIfStatement( - IsPatternExpressionSyntax updatedCondition, - ImmutableArray trivia, - IfStatementSyntax originalIf, - IfStatementSyntax currentIf) - { - var newIf = currentIf.ReplaceNode(currentIf.Condition, updatedCondition); - newIf = originalIf.IsParentKind(SyntaxKind.ElseClause) - ? newIf.ReplaceToken(newIf.CloseParenToken, newIf.CloseParenToken.WithTrailingTrivia(trivia)) - : newIf.WithPrependedLeadingTrivia(trivia); + editor.RemoveNode(localDeclaration); + editor.ReplaceNode(ifStatement, + (i, g) => + { + // Because the local declaration is *inside* the 'if', we need to get the 'if' + // statement after it was already modified and *then* update the condition + // portion of it. + var currentIf = (IfStatementSyntax)i; + return GetUpdatedIfStatement(updatedCondition, trivia, ifStatement, currentIf); + }); + } - return newIf.WithAdditionalAnnotations(Formatter.Annotation); - } + private static IfStatementSyntax GetUpdatedIfStatement( + IsPatternExpressionSyntax updatedCondition, + ImmutableArray trivia, + IfStatementSyntax originalIf, + IfStatementSyntax currentIf) + { + var newIf = currentIf.ReplaceNode(currentIf.Condition, updatedCondition); + newIf = originalIf.IsParentKind(SyntaxKind.ElseClause) + ? newIf.ReplaceToken(newIf.CloseParenToken, newIf.CloseParenToken.WithTrailingTrivia(trivia)) + : newIf.WithPrependedLeadingTrivia(trivia); + + return newIf.WithAdditionalAnnotations(Formatter.Annotation); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpUseNotPatternCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpUseNotPatternCodeFixProvider.cs index 1436e5c9c8cbe..81b7a2ffe7553 100644 --- a/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpUseNotPatternCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UsePatternMatching/CSharpUseNotPatternCodeFixProvider.cs @@ -16,59 +16,58 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching +namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseNotPattern), Shared] +internal sealed class CSharpUseNotPatternCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseNotPattern), Shared] - internal sealed class CSharpUseNotPatternCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpUseNotPatternCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpUseNotPatternCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseNotPatternDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseNotPatternDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Use_pattern_matching, nameof(CSharpAnalyzersResources.Use_pattern_matching)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Use_pattern_matching, nameof(CSharpAnalyzersResources.Use_pattern_matching)); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + foreach (var diagnostic in diagnostics) { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - foreach (var diagnostic in diagnostics) - { - cancellationToken.ThrowIfCancellationRequested(); - ProcessDiagnostic(semanticModel, editor, diagnostic, cancellationToken); - } + cancellationToken.ThrowIfCancellationRequested(); + ProcessDiagnostic(semanticModel, editor, diagnostic, cancellationToken); } + } - private static void ProcessDiagnostic( - SemanticModel semanticModel, - SyntaxEditor editor, - Diagnostic diagnostic, - CancellationToken cancellationToken) - { - var notExpressionLocation = diagnostic.AdditionalLocations[0]; + private static void ProcessDiagnostic( + SemanticModel semanticModel, + SyntaxEditor editor, + Diagnostic diagnostic, + CancellationToken cancellationToken) + { + var notExpressionLocation = diagnostic.AdditionalLocations[0]; - var notExpression = (PrefixUnaryExpressionSyntax)notExpressionLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); - var parenthesizedExpression = (ParenthesizedExpressionSyntax)notExpression.Operand; + var notExpression = (PrefixUnaryExpressionSyntax)notExpressionLocation.FindNode(getInnermostNodeForTie: true, cancellationToken); + var parenthesizedExpression = (ParenthesizedExpressionSyntax)notExpression.Operand; - var negated = editor.Generator.Negate( - CSharpSyntaxGeneratorInternal.Instance, - parenthesizedExpression.Expression, - semanticModel, - cancellationToken); + var negated = editor.Generator.Negate( + CSharpSyntaxGeneratorInternal.Instance, + parenthesizedExpression.Expression, + semanticModel, + cancellationToken); - editor.ReplaceNode( - notExpression, - negated.WithPrependedLeadingTrivia(notExpression.GetLeadingTrivia()) - .WithAppendedTrailingTrivia(notExpression.GetTrailingTrivia())); - } + editor.ReplaceNode( + notExpression, + negated.WithPrependedLeadingTrivia(notExpression.GetLeadingTrivia()) + .WithAppendedTrailingTrivia(notExpression.GetTrailingTrivia())); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseSimpleUsingStatement/UseSimpleUsingStatementCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseSimpleUsingStatement/UseSimpleUsingStatementCodeFixProvider.cs index 5124f50a3fe73..9896daadd5990 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseSimpleUsingStatement/UseSimpleUsingStatementCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseSimpleUsingStatement/UseSimpleUsingStatementCodeFixProvider.cs @@ -25,159 +25,158 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseSimpleUsingStatement -{ - using static SyntaxFactory; +namespace Microsoft.CodeAnalysis.CSharp.UseSimpleUsingStatement; + +using static SyntaxFactory; - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseSimpleUsingStatement), Shared] - internal class UseSimpleUsingStatementCodeFixProvider : SyntaxEditorBasedCodeFixProvider +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseSimpleUsingStatement), Shared] +internal class UseSimpleUsingStatementCodeFixProvider : SyntaxEditorBasedCodeFixProvider +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public UseSimpleUsingStatementCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public UseSimpleUsingStatementCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } = - [IDEDiagnosticIds.UseSimpleUsingStatementDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds { get; } = + [IDEDiagnosticIds.UseSimpleUsingStatementDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Use_simple_using_statement, nameof(CSharpAnalyzersResources.Use_simple_using_statement)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Use_simple_using_statement, nameof(CSharpAnalyzersResources.Use_simple_using_statement)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var topmostUsingStatements = diagnostics.Select(d => (UsingStatementSyntax)d.AdditionalLocations[0].FindNode(cancellationToken)).ToSet(); - var blocks = topmostUsingStatements.Select(u => (BlockSyntax)u.Parent); + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var topmostUsingStatements = diagnostics.Select(d => (UsingStatementSyntax)d.AdditionalLocations[0].FindNode(cancellationToken)).ToSet(); + var blocks = topmostUsingStatements.Select(u => (BlockSyntax)u.Parent); - // Process blocks in reverse order so we rewrite from inside-to-outside with nested - // usings. - var root = editor.OriginalRoot; - var updatedRoot = root.ReplaceNodes( - blocks.OrderByDescending(b => b.SpanStart), - (original, current) => RewriteBlock(original, current, topmostUsingStatements)); + // Process blocks in reverse order so we rewrite from inside-to-outside with nested + // usings. + var root = editor.OriginalRoot; + var updatedRoot = root.ReplaceNodes( + blocks.OrderByDescending(b => b.SpanStart), + (original, current) => RewriteBlock(original, current, topmostUsingStatements)); - editor.ReplaceNode(root, updatedRoot); + editor.ReplaceNode(root, updatedRoot); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - private static BlockSyntax RewriteBlock( - BlockSyntax originalBlock, BlockSyntax currentBlock, - ISet topmostUsingStatements) + private static BlockSyntax RewriteBlock( + BlockSyntax originalBlock, BlockSyntax currentBlock, + ISet topmostUsingStatements) + { + if (originalBlock.Statements.Count == currentBlock.Statements.Count) { - if (originalBlock.Statements.Count == currentBlock.Statements.Count) - { - var statementToUpdateIndex = originalBlock.Statements.IndexOf(s => topmostUsingStatements.Contains(s)); - var statementToUpdate = currentBlock.Statements[statementToUpdateIndex]; + var statementToUpdateIndex = originalBlock.Statements.IndexOf(s => topmostUsingStatements.Contains(s)); + var statementToUpdate = currentBlock.Statements[statementToUpdateIndex]; - if (statementToUpdate is UsingStatementSyntax usingStatement && - usingStatement.Declaration != null) - { - var updatedStatements = currentBlock.Statements.ReplaceRange( - statementToUpdate, - Expand(usingStatement)); - return currentBlock.WithStatements(updatedStatements); - } + if (statementToUpdate is UsingStatementSyntax usingStatement && + usingStatement.Declaration != null) + { + var updatedStatements = currentBlock.Statements.ReplaceRange( + statementToUpdate, + Expand(usingStatement)); + return currentBlock.WithStatements(updatedStatements); } - - return currentBlock; } - private static ImmutableArray Expand(UsingStatementSyntax usingStatement) - { - using var _ = ArrayBuilder.GetInstance(out var result); - var remainingTrivia = Expand(result, usingStatement); - - if (remainingTrivia.Any(t => t.IsSingleOrMultiLineComment() || t.IsDirective)) - { - var lastStatement = result[^1]; - result[^1] = lastStatement.WithAppendedTrailingTrivia( - remainingTrivia.Insert(0, CSharpSyntaxFacts.Instance.ElasticCarriageReturnLineFeed)); - } + return currentBlock; + } - for (int i = 0, n = result.Count; i < n; i++) - result[i] = result[i].WithAdditionalAnnotations(Formatter.Annotation); + private static ImmutableArray Expand(UsingStatementSyntax usingStatement) + { + using var _ = ArrayBuilder.GetInstance(out var result); + var remainingTrivia = Expand(result, usingStatement); - return result.ToImmutable(); + if (remainingTrivia.Any(t => t.IsSingleOrMultiLineComment() || t.IsDirective)) + { + var lastStatement = result[^1]; + result[^1] = lastStatement.WithAppendedTrailingTrivia( + remainingTrivia.Insert(0, CSharpSyntaxFacts.Instance.ElasticCarriageReturnLineFeed)); } - private static SyntaxTriviaList Expand(ArrayBuilder result, UsingStatementSyntax usingStatement) + for (int i = 0, n = result.Count; i < n; i++) + result[i] = result[i].WithAdditionalAnnotations(Formatter.Annotation); + + return result.ToImmutable(); + } + + private static SyntaxTriviaList Expand(ArrayBuilder result, UsingStatementSyntax usingStatement) + { + // First, convert the using-statement into a using-declaration. + result.Add(Convert(usingStatement)); + switch (usingStatement.Statement) { - // First, convert the using-statement into a using-declaration. - result.Add(Convert(usingStatement)); - switch (usingStatement.Statement) - { - case BlockSyntax blockSyntax: - var statements = blockSyntax.Statements; - if (!statements.Any()) - { - return blockSyntax.CloseBraceToken.LeadingTrivia; - } - - var openBraceLeadingTrivia = blockSyntax.OpenBraceToken.LeadingTrivia; - var openBraceTrailingTrivia = blockSyntax.OpenBraceToken.TrailingTrivia; - var usingHasEndOfLineTrivia = usingStatement.CloseParenToken.TrailingTrivia - .Any(SyntaxKind.EndOfLineTrivia); - if (!usingHasEndOfLineTrivia) - { - var newFirstStatement = statements.First() - .WithPrependedLeadingTrivia(ElasticCarriageReturnLineFeed); - statements = statements.Replace(statements.First(), newFirstStatement); - } - - if (openBraceTrailingTrivia.Any(t => t.IsSingleOrMultiLineComment())) - { - var newFirstStatement = statements.First() - .WithPrependedLeadingTrivia(openBraceTrailingTrivia); - statements = statements.Replace(statements.First(), newFirstStatement); - } - - if (openBraceLeadingTrivia.Any(t => t.IsSingleOrMultiLineComment() || t.IsDirective)) - { - var newFirstStatement = statements.First() - .WithPrependedLeadingTrivia(openBraceLeadingTrivia); - statements = statements.Replace(statements.First(), newFirstStatement); - } - - var closeBraceTrailingTrivia = blockSyntax.CloseBraceToken.TrailingTrivia; - if (closeBraceTrailingTrivia.Any(t => t.IsSingleOrMultiLineComment())) - { - var newLastStatement = statements.Last() - .WithAppendedTrailingTrivia(closeBraceTrailingTrivia); - statements = statements.Replace(statements.Last(), newLastStatement); - } - - // if we hit a block, then inline all the statements in the block into - // the final list of statements. - result.AddRange(statements); + case BlockSyntax blockSyntax: + var statements = blockSyntax.Statements; + if (!statements.Any()) + { return blockSyntax.CloseBraceToken.LeadingTrivia; - case UsingStatementSyntax childUsing when childUsing.Declaration != null: - // If we have a directly nested using-statement, then recurse into that - // expanding it and handle its children as well. - return Expand(result, childUsing); - case StatementSyntax anythingElse: - // Any other statement should be untouched and just be placed next in the - // final list of statements. - result.Add(anythingElse); - return default; - } + } - return default; - } + var openBraceLeadingTrivia = blockSyntax.OpenBraceToken.LeadingTrivia; + var openBraceTrailingTrivia = blockSyntax.OpenBraceToken.TrailingTrivia; + var usingHasEndOfLineTrivia = usingStatement.CloseParenToken.TrailingTrivia + .Any(SyntaxKind.EndOfLineTrivia); + if (!usingHasEndOfLineTrivia) + { + var newFirstStatement = statements.First() + .WithPrependedLeadingTrivia(ElasticCarriageReturnLineFeed); + statements = statements.Replace(statements.First(), newFirstStatement); + } - private static LocalDeclarationStatementSyntax Convert(UsingStatementSyntax usingStatement) - { - return LocalDeclarationStatement( - usingStatement.AwaitKeyword, - usingStatement.UsingKeyword.WithAppendedTrailingTrivia(ElasticMarker), - modifiers: default, - usingStatement.Declaration, - Token(SyntaxKind.SemicolonToken)).WithTrailingTrivia(usingStatement.CloseParenToken.TrailingTrivia); + if (openBraceTrailingTrivia.Any(t => t.IsSingleOrMultiLineComment())) + { + var newFirstStatement = statements.First() + .WithPrependedLeadingTrivia(openBraceTrailingTrivia); + statements = statements.Replace(statements.First(), newFirstStatement); + } + + if (openBraceLeadingTrivia.Any(t => t.IsSingleOrMultiLineComment() || t.IsDirective)) + { + var newFirstStatement = statements.First() + .WithPrependedLeadingTrivia(openBraceLeadingTrivia); + statements = statements.Replace(statements.First(), newFirstStatement); + } + + var closeBraceTrailingTrivia = blockSyntax.CloseBraceToken.TrailingTrivia; + if (closeBraceTrailingTrivia.Any(t => t.IsSingleOrMultiLineComment())) + { + var newLastStatement = statements.Last() + .WithAppendedTrailingTrivia(closeBraceTrailingTrivia); + statements = statements.Replace(statements.Last(), newLastStatement); + } + + // if we hit a block, then inline all the statements in the block into + // the final list of statements. + result.AddRange(statements); + return blockSyntax.CloseBraceToken.LeadingTrivia; + case UsingStatementSyntax childUsing when childUsing.Declaration != null: + // If we have a directly nested using-statement, then recurse into that + // expanding it and handle its children as well. + return Expand(result, childUsing); + case StatementSyntax anythingElse: + // Any other statement should be untouched and just be placed next in the + // final list of statements. + result.Add(anythingElse); + return default; } + + return default; + } + + private static LocalDeclarationStatementSyntax Convert(UsingStatementSyntax usingStatement) + { + return LocalDeclarationStatement( + usingStatement.AwaitKeyword, + usingStatement.UsingKeyword.WithAppendedTrailingTrivia(ElasticMarker), + modifiers: default, + usingStatement.Declaration, + Token(SyntaxKind.SemicolonToken)).WithTrailingTrivia(usingStatement.CloseParenToken.TrailingTrivia); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseThrowExpression/UseThrowExpressionCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseThrowExpression/UseThrowExpressionCodeFixProvider.cs index 6eab4aef59965..713cbd464ba07 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseThrowExpression/UseThrowExpressionCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseThrowExpression/UseThrowExpressionCodeFixProvider.cs @@ -18,74 +18,73 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseThrowExpression +namespace Microsoft.CodeAnalysis.CSharp.UseThrowExpression; + +[ExportCodeFixProvider(LanguageNames.CSharp, + Name = PredefinedCodeFixProviderNames.UseThrowExpression), Shared] +internal partial class UseThrowExpressionCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, - Name = PredefinedCodeFixProviderNames.UseThrowExpression), Shared] - internal partial class UseThrowExpressionCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public UseThrowExpressionCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public UseThrowExpressionCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseThrowExpressionDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseThrowExpressionDiagnosticId]; - protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) - => !diagnostic.Descriptor.ImmutableCustomTags().Contains(WellKnownDiagnosticTags.Unnecessary); + protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) + => !diagnostic.Descriptor.ImmutableCustomTags().Contains(WellKnownDiagnosticTags.Unnecessary); - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, AnalyzersResources.Use_throw_expression, nameof(AnalyzersResources.Use_throw_expression)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, AnalyzersResources.Use_throw_expression, nameof(AnalyzersResources.Use_throw_expression)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var generator = editor.Generator; - var root = editor.OriginalRoot; + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var generator = editor.Generator; + var root = editor.OriginalRoot; - foreach (var diagnostic in diagnostics) - { - var ifStatement = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan); - var throwStatementExpression = root.FindNode(diagnostic.AdditionalLocations[1].SourceSpan); - var assignmentValue = root.FindNode(diagnostic.AdditionalLocations[2].SourceSpan); - var assignmentExpressionStatement = root.FindNode(diagnostic.AdditionalLocations[3].SourceSpan); + foreach (var diagnostic in diagnostics) + { + var ifStatement = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan); + var throwStatementExpression = root.FindNode(diagnostic.AdditionalLocations[1].SourceSpan); + var assignmentValue = root.FindNode(diagnostic.AdditionalLocations[2].SourceSpan); + var assignmentExpressionStatement = root.FindNode(diagnostic.AdditionalLocations[3].SourceSpan); - // First, remote the if-statement entirely. - editor.RemoveNode(ifStatement); + // First, remote the if-statement entirely. + editor.RemoveNode(ifStatement); - // Now, update the assignment value to go from 'a' to 'a ?? throw ...'. - editor.ReplaceNode(assignmentValue, - generator.CoalesceExpression(assignmentValue, - generator.ThrowExpression(throwStatementExpression))); + // Now, update the assignment value to go from 'a' to 'a ?? throw ...'. + editor.ReplaceNode(assignmentValue, + generator.CoalesceExpression(assignmentValue, + generator.ThrowExpression(throwStatementExpression))); - // Move any trailing trivia after the `throw new Exception(); // comment` + // Move any trailing trivia after the `throw new Exception(); // comment` - if (throwStatementExpression.Parent is ThrowStatementSyntax throwStatement && - throwStatement.GetTrailingTrivia().Any(t => t.IsSingleOrMultiLineComment())) + if (throwStatementExpression.Parent is ThrowStatementSyntax throwStatement && + throwStatement.GetTrailingTrivia().Any(t => t.IsSingleOrMultiLineComment())) + { + if (assignmentExpressionStatement.GetTrailingTrivia().Any(t => t.IsSingleOrMultiLineComment())) { - if (assignmentExpressionStatement.GetTrailingTrivia().Any(t => t.IsSingleOrMultiLineComment())) - { - // Assignment already has trailing trivia. Move the comments above it instead. - editor.ReplaceNode( - assignmentExpressionStatement, - (current, _) => current.WithLeadingTrivia(current.GetLeadingTrivia().Concat(throwStatement.GetTrailingTrivia()))); - } - else - { - editor.ReplaceNode( - assignmentExpressionStatement, - (current, _) => current.WithTrailingTrivia(throwStatement.GetTrailingTrivia())); - } + // Assignment already has trailing trivia. Move the comments above it instead. + editor.ReplaceNode( + assignmentExpressionStatement, + (current, _) => current.WithLeadingTrivia(current.GetLeadingTrivia().Concat(throwStatement.GetTrailingTrivia()))); + } + else + { + editor.ReplaceNode( + assignmentExpressionStatement, + (current, _) => current.WithTrailingTrivia(throwStatement.GetTrailingTrivia())); } } - - return Task.CompletedTask; } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseTupleSwap/CSharpUseTupleSwapCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseTupleSwap/CSharpUseTupleSwapCodeFixProvider.cs index be51632431f90..1a82de7b16c0a 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseTupleSwap/CSharpUseTupleSwapCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseTupleSwap/CSharpUseTupleSwapCodeFixProvider.cs @@ -19,59 +19,58 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseTupleSwap -{ - using static SyntaxFactory; +namespace Microsoft.CodeAnalysis.CSharp.UseTupleSwap; + +using static SyntaxFactory; - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseTupleSwap), Shared] - internal partial class CSharpUseTupleSwapCodeFixProvider : SyntaxEditorBasedCodeFixProvider +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseTupleSwap), Shared] +internal partial class CSharpUseTupleSwapCodeFixProvider : SyntaxEditorBasedCodeFixProvider +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpUseTupleSwapCodeFixProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpUseTupleSwapCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } - = [IDEDiagnosticIds.UseTupleSwapDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds { get; } + = [IDEDiagnosticIds.UseTupleSwapDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Use_tuple_to_swap_values, nameof(CSharpAnalyzersResources.Use_tuple_to_swap_values)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Use_tuple_to_swap_values, nameof(CSharpAnalyzersResources.Use_tuple_to_swap_values)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - foreach (var diagnostic in diagnostics) - FixOne(editor, diagnostic, cancellationToken); + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) + FixOne(editor, diagnostic, cancellationToken); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - private static void FixOne( - SyntaxEditor editor, Diagnostic diagnostic, CancellationToken cancellationToken) - { - var localDeclarationStatement = (LocalDeclarationStatementSyntax)diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); - // `expr_a = expr_b`; - var firstAssignmentStatement = (ExpressionStatementSyntax)diagnostic.AdditionalLocations[1].FindNode(getInnermostNodeForTie: true, cancellationToken); - var secondAssignmentStatement = (ExpressionStatementSyntax)diagnostic.AdditionalLocations[2].FindNode(getInnermostNodeForTie: true, cancellationToken); + private static void FixOne( + SyntaxEditor editor, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var localDeclarationStatement = (LocalDeclarationStatementSyntax)diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); + // `expr_a = expr_b`; + var firstAssignmentStatement = (ExpressionStatementSyntax)diagnostic.AdditionalLocations[1].FindNode(getInnermostNodeForTie: true, cancellationToken); + var secondAssignmentStatement = (ExpressionStatementSyntax)diagnostic.AdditionalLocations[2].FindNode(getInnermostNodeForTie: true, cancellationToken); - editor.RemoveNode(firstAssignmentStatement); - editor.RemoveNode(secondAssignmentStatement); + editor.RemoveNode(firstAssignmentStatement); + editor.RemoveNode(secondAssignmentStatement); - var assignment = (AssignmentExpressionSyntax)firstAssignmentStatement.Expression; - var exprA = assignment.Left.WalkDownParentheses().WithoutTrivia(); - var exprB = assignment.Right.WalkDownParentheses().WithoutTrivia(); + var assignment = (AssignmentExpressionSyntax)firstAssignmentStatement.Expression; + var exprA = assignment.Left.WalkDownParentheses().WithoutTrivia(); + var exprB = assignment.Right.WalkDownParentheses().WithoutTrivia(); - var tupleAssignmentStatement = ExpressionStatement(AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - TupleExpression([Argument(exprB), Argument(exprA)]), - TupleExpression([Argument(exprA), Argument(exprB)]))); + var tupleAssignmentStatement = ExpressionStatement(AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + TupleExpression([Argument(exprB), Argument(exprA)]), + TupleExpression([Argument(exprA), Argument(exprB)]))); - editor.ReplaceNode(localDeclarationStatement, tupleAssignmentStatement.WithTriviaFrom(localDeclarationStatement)); - } + editor.ReplaceNode(localDeclarationStatement, tupleAssignmentStatement.WithTriviaFrom(localDeclarationStatement)); } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseUtf8StringLiteral/UseUtf8StringLiteralCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseUtf8StringLiteral/UseUtf8StringLiteralCodeFixProvider.cs index db33092a4bd09..4ad6082755661 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseUtf8StringLiteral/UseUtf8StringLiteralCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseUtf8StringLiteral/UseUtf8StringLiteralCodeFixProvider.cs @@ -19,198 +19,197 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseUtf8StringLiteral +namespace Microsoft.CodeAnalysis.CSharp.UseUtf8StringLiteral; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseUtf8StringLiteral), Shared] +internal sealed class UseUtf8StringLiteralCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseUtf8StringLiteral), Shared] - internal sealed class UseUtf8StringLiteralCodeFixProvider : SyntaxEditorBasedCodeFixProvider - { - private const char QuoteCharacter = '"'; - private const string Suffix = "u8"; + private const char QuoteCharacter = '"'; + private const string Suffix = "u8"; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public UseUtf8StringLiteralCodeFixProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public UseUtf8StringLiteralCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds { get; } = - [IDEDiagnosticIds.UseUtf8StringLiteralDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds { get; } = + [IDEDiagnosticIds.UseUtf8StringLiteralDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpAnalyzersResources.Use_Utf8_string_literal, nameof(CSharpAnalyzersResources.Use_Utf8_string_literal)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CSharpAnalyzersResources.Use_Utf8_string_literal, nameof(CSharpAnalyzersResources.Use_Utf8_string_literal)); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider options, CancellationToken cancellationToken) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider options, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var readOnlySpanType = semanticModel.Compilation.GetBestTypeByMetadataName(typeof(ReadOnlySpan<>).FullName!); - // The analyzer wouldn't raise a diagnostic if this were null - Contract.ThrowIfNull(readOnlySpanType); + var readOnlySpanType = semanticModel.Compilation.GetBestTypeByMetadataName(typeof(ReadOnlySpan<>).FullName!); + // The analyzer wouldn't raise a diagnostic if this were null + Contract.ThrowIfNull(readOnlySpanType); - foreach (var diagnostic in diagnostics) + foreach (var diagnostic in diagnostics) + { + cancellationToken.ThrowIfCancellationRequested(); + + var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); + var arrayOp = GetArrayCreationOperation(semanticModel, diagnostic, cancellationToken); + Contract.ThrowIfNull(arrayOp.Initializer); + + var stringValue = GetUtf8StringValueFromArrayInitializer(arrayOp.Initializer); + + // If our array is parented by a conversion to ReadOnlySpan then we don't want to call + // ToArray after the string literal, or we'll be regressing perf. + var isConvertedToReadOnlySpan = arrayOp.Parent is IConversionOperation conversion && + conversion.Type is INamedTypeSymbol { IsGenericType: true } namedType && + namedType.OriginalDefinition.Equals(readOnlySpanType) && + namedType.TypeArguments[0].SpecialType == SpecialType.System_Byte; + + // If we're replacing a byte array that is passed to a parameter array, not and an explicit array creation + // then node will be the ArgumentListSyntax that the implicit array creation is just a part of, so we have + // to handle that separately, as we can't just replace node with a string literal + // + // eg given a method: + // M(string x, params byte[] b) + // our diagnostic would be reported on: + // M("hi", [|1, 2, 3, 4|]); + // but node will point to: + // M([|"hi", 1, 2, 3, 4|]); + + if (node is BaseArgumentListSyntax argumentList) { - cancellationToken.ThrowIfCancellationRequested(); - - var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - var arrayOp = GetArrayCreationOperation(semanticModel, diagnostic, cancellationToken); - Contract.ThrowIfNull(arrayOp.Initializer); - - var stringValue = GetUtf8StringValueFromArrayInitializer(arrayOp.Initializer); - - // If our array is parented by a conversion to ReadOnlySpan then we don't want to call - // ToArray after the string literal, or we'll be regressing perf. - var isConvertedToReadOnlySpan = arrayOp.Parent is IConversionOperation conversion && - conversion.Type is INamedTypeSymbol { IsGenericType: true } namedType && - namedType.OriginalDefinition.Equals(readOnlySpanType) && - namedType.TypeArguments[0].SpecialType == SpecialType.System_Byte; - - // If we're replacing a byte array that is passed to a parameter array, not and an explicit array creation - // then node will be the ArgumentListSyntax that the implicit array creation is just a part of, so we have - // to handle that separately, as we can't just replace node with a string literal - // - // eg given a method: - // M(string x, params byte[] b) - // our diagnostic would be reported on: - // M("hi", [|1, 2, 3, 4|]); - // but node will point to: - // M([|"hi", 1, 2, 3, 4|]); - - if (node is BaseArgumentListSyntax argumentList) - { - editor.ReplaceNode(node, CreateArgumentListWithUtf8String(argumentList, diagnostic.Location, stringValue, isConvertedToReadOnlySpan)); - } - else - { - editor.ReplaceNode(node, CreateUtf8String(node, stringValue, isConvertedToReadOnlySpan)); - } + editor.ReplaceNode(node, CreateArgumentListWithUtf8String(argumentList, diagnostic.Location, stringValue, isConvertedToReadOnlySpan)); + } + else + { + editor.ReplaceNode(node, CreateUtf8String(node, stringValue, isConvertedToReadOnlySpan)); } } + } - private static IArrayCreationOperation GetArrayCreationOperation(SemanticModel semanticModel, Diagnostic diagnostic, CancellationToken cancellationToken) - { - // For computing the UTF-8 string we need the original location of the array creation - // operation, which is stored in additional locations. - var location = diagnostic.AdditionalLocations[0]; - var node = location.FindNode(getInnermostNodeForTie: true, cancellationToken); + private static IArrayCreationOperation GetArrayCreationOperation(SemanticModel semanticModel, Diagnostic diagnostic, CancellationToken cancellationToken) + { + // For computing the UTF-8 string we need the original location of the array creation + // operation, which is stored in additional locations. + var location = diagnostic.AdditionalLocations[0]; + var node = location.FindNode(getInnermostNodeForTie: true, cancellationToken); - var operation = semanticModel.GetRequiredOperation(node, cancellationToken); + var operation = semanticModel.GetRequiredOperation(node, cancellationToken); - var operationLocationString = diagnostic.Properties[nameof(UseUtf8StringLiteralDiagnosticAnalyzer.ArrayCreationOperationLocation)]; - if (!Enum.TryParse(operationLocationString, out UseUtf8StringLiteralDiagnosticAnalyzer.ArrayCreationOperationLocation operationLocation)) - throw ExceptionUtilities.Unreachable(); + var operationLocationString = diagnostic.Properties[nameof(UseUtf8StringLiteralDiagnosticAnalyzer.ArrayCreationOperationLocation)]; + if (!Enum.TryParse(operationLocationString, out UseUtf8StringLiteralDiagnosticAnalyzer.ArrayCreationOperationLocation operationLocation)) + throw ExceptionUtilities.Unreachable(); - // Because we get the location from an IOperation.Syntax, sometimes we have to look a - // little harder to get back from syntax to the operation that triggered the diagnostic - if (operationLocation == UseUtf8StringLiteralDiagnosticAnalyzer.ArrayCreationOperationLocation.Ancestors) - { - // For collection initializers where the Add method takes a param array, and the array creation - // will be a parent of the operation - return FindArrayCreationOperationAncestor(operation); - } - else if (operationLocation == UseUtf8StringLiteralDiagnosticAnalyzer.ArrayCreationOperationLocation.Descendants) - { - // Otherwise, we must have an implicit array creation for a parameter array, so the location - // will be the invocation, or similar, that has the argument, and we need to descend child - // nodes to find the one we are interested in. To make sure we're finding the right one, - // we can use the diagnostic location for that, since the analyzer raises it on the first element. - return operation.DescendantsAndSelf() - .OfType() - .Where(a => a.Initializer?.ElementValues.FirstOrDefault()?.Syntax.SpanStart == diagnostic.Location.SourceSpan.Start) - .First(); - } + // Because we get the location from an IOperation.Syntax, sometimes we have to look a + // little harder to get back from syntax to the operation that triggered the diagnostic + if (operationLocation == UseUtf8StringLiteralDiagnosticAnalyzer.ArrayCreationOperationLocation.Ancestors) + { + // For collection initializers where the Add method takes a param array, and the array creation + // will be a parent of the operation + return FindArrayCreationOperationAncestor(operation); + } + else if (operationLocation == UseUtf8StringLiteralDiagnosticAnalyzer.ArrayCreationOperationLocation.Descendants) + { + // Otherwise, we must have an implicit array creation for a parameter array, so the location + // will be the invocation, or similar, that has the argument, and we need to descend child + // nodes to find the one we are interested in. To make sure we're finding the right one, + // we can use the diagnostic location for that, since the analyzer raises it on the first element. + return operation.DescendantsAndSelf() + .OfType() + .Where(a => a.Initializer?.ElementValues.FirstOrDefault()?.Syntax.SpanStart == diagnostic.Location.SourceSpan.Start) + .First(); + } - return (IArrayCreationOperation)operation; + return (IArrayCreationOperation)operation; - static IArrayCreationOperation FindArrayCreationOperationAncestor(IOperation operation) + static IArrayCreationOperation FindArrayCreationOperationAncestor(IOperation operation) + { + while (operation is not null) { - while (operation is not null) - { - if (operation is IArrayCreationOperation arrayOperation) - return arrayOperation; - - operation = operation.Parent!; - } + if (operation is IArrayCreationOperation arrayOperation) + return arrayOperation; - throw ExceptionUtilities.Unreachable(); + operation = operation.Parent!; } + + throw ExceptionUtilities.Unreachable(); } + } - private static string GetUtf8StringValueFromArrayInitializer(IArrayInitializerOperation initializer) - { - // Get our list of bytes from the array elements - using var _ = PooledStringBuilder.GetInstance(out var builder); - builder.Capacity = initializer.ElementValues.Length; + private static string GetUtf8StringValueFromArrayInitializer(IArrayInitializerOperation initializer) + { + // Get our list of bytes from the array elements + using var _ = PooledStringBuilder.GetInstance(out var builder); + builder.Capacity = initializer.ElementValues.Length; - // Can never fail as the analyzer already validated this would work. - Contract.ThrowIfFalse(UseUtf8StringLiteralDiagnosticAnalyzer.TryConvertToUtf8String(builder, initializer.ElementValues)); + // Can never fail as the analyzer already validated this would work. + Contract.ThrowIfFalse(UseUtf8StringLiteralDiagnosticAnalyzer.TryConvertToUtf8String(builder, initializer.ElementValues)); - return builder.ToString(); - } + return builder.ToString(); + } - private static SyntaxNode CreateArgumentListWithUtf8String(BaseArgumentListSyntax argumentList, Location location, string stringValue, bool isConvertedToReadOnlySpan) + private static SyntaxNode CreateArgumentListWithUtf8String(BaseArgumentListSyntax argumentList, Location location, string stringValue, bool isConvertedToReadOnlySpan) + { + // To construct our new argument list we add any existing tokens before the location + // and then once we hit the location, we add our string literal + // We can't just loop through the arguments, as we want to preserve trivia on the + // comma tokens, if any. + using var _ = ArrayBuilder.GetInstance(out var arguments); + foreach (var argument in argumentList.ChildNodesAndTokens()) { - // To construct our new argument list we add any existing tokens before the location - // and then once we hit the location, we add our string literal - // We can't just loop through the arguments, as we want to preserve trivia on the - // comma tokens, if any. - using var _ = ArrayBuilder.GetInstance(out var arguments); - foreach (var argument in argumentList.ChildNodesAndTokens()) + // Skip the open paren, its a child token but not an argument + if (argument.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.OpenBracketToken) { - // Skip the open paren, its a child token but not an argument - if (argument.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.OpenBracketToken) - { - continue; - } - - // See if we found our first argument - if (argument.Span.Start == location.SourceSpan.Start) - { - // We don't need to worry about leading trivia here, because anything before the current - // argument will have been trailing trivia on the previous comma. - var stringLiteral = CreateUtf8String(SyntaxTriviaList.Empty, stringValue, argumentList.Arguments.Last().GetTrailingTrivia(), isConvertedToReadOnlySpan); - arguments.Add(SyntaxFactory.Argument(stringLiteral)); - break; - } - - arguments.Add(argument); + continue; } - return argumentList.WithArguments(SyntaxFactory.SeparatedList(arguments)); - } + // See if we found our first argument + if (argument.Span.Start == location.SourceSpan.Start) + { + // We don't need to worry about leading trivia here, because anything before the current + // argument will have been trailing trivia on the previous comma. + var stringLiteral = CreateUtf8String(SyntaxTriviaList.Empty, stringValue, argumentList.Arguments.Last().GetTrailingTrivia(), isConvertedToReadOnlySpan); + arguments.Add(SyntaxFactory.Argument(stringLiteral)); + break; + } - private static ExpressionSyntax CreateUtf8String(SyntaxNode nodeToTakeTriviaFrom, string stringValue, bool isConvertedToReadOnlySpan) - { - return CreateUtf8String(nodeToTakeTriviaFrom.GetLeadingTrivia(), stringValue, nodeToTakeTriviaFrom.GetTrailingTrivia(), isConvertedToReadOnlySpan); + arguments.Add(argument); } - private static ExpressionSyntax CreateUtf8String(SyntaxTriviaList leadingTrivia, string stringValue, SyntaxTriviaList trailingTrivia, bool isConvertedToReadOnlySpan) - { - var stringLiteral = SyntaxFactory.LiteralExpression(SyntaxKind.Utf8StringLiteralExpression, - SyntaxFactory.Token( - leading: leadingTrivia, - kind: SyntaxKind.Utf8StringLiteralToken, - text: QuoteCharacter + stringValue + QuoteCharacter + Suffix, - valueText: "", - trailing: SyntaxTriviaList.Empty)); - - if (isConvertedToReadOnlySpan) - { - return stringLiteral.WithTrailingTrivia(trailingTrivia); - } + return argumentList.WithArguments(SyntaxFactory.SeparatedList(arguments)); + } - // We're replacing a byte array with a ReadOnlySpan, so if that byte array wasn't originally being - // converted to the same, then we need to call .ToArray() to get things back to a byte array. - return SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - stringLiteral, - SyntaxFactory.IdentifierName(nameof(ReadOnlySpan.ToArray)))) - .WithTrailingTrivia(trailingTrivia); + private static ExpressionSyntax CreateUtf8String(SyntaxNode nodeToTakeTriviaFrom, string stringValue, bool isConvertedToReadOnlySpan) + { + return CreateUtf8String(nodeToTakeTriviaFrom.GetLeadingTrivia(), stringValue, nodeToTakeTriviaFrom.GetTrailingTrivia(), isConvertedToReadOnlySpan); + } + + private static ExpressionSyntax CreateUtf8String(SyntaxTriviaList leadingTrivia, string stringValue, SyntaxTriviaList trailingTrivia, bool isConvertedToReadOnlySpan) + { + var stringLiteral = SyntaxFactory.LiteralExpression(SyntaxKind.Utf8StringLiteralExpression, + SyntaxFactory.Token( + leading: leadingTrivia, + kind: SyntaxKind.Utf8StringLiteralToken, + text: QuoteCharacter + stringValue + QuoteCharacter + Suffix, + valueText: "", + trailing: SyntaxTriviaList.Empty)); + + if (isConvertedToReadOnlySpan) + { + return stringLiteral.WithTrailingTrivia(trailingTrivia); } + + // We're replacing a byte array with a ReadOnlySpan, so if that byte array wasn't originally being + // converted to the same, then we need to call .ToArray() to get things back to a byte array. + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + stringLiteral, + SyntaxFactory.IdentifierName(nameof(ReadOnlySpan.ToArray)))) + .WithTrailingTrivia(trailingTrivia); } } diff --git a/src/Analyzers/CSharp/Tests/ConvertNamespace/ConvertToFileScopedNamespaceAnalyzerTests.cs b/src/Analyzers/CSharp/Tests/ConvertNamespace/ConvertToFileScopedNamespaceAnalyzerTests.cs index 69b72bf8229c8..f8649c1d1242d 100644 --- a/src/Analyzers/CSharp/Tests/ConvertNamespace/ConvertToFileScopedNamespaceAnalyzerTests.cs +++ b/src/Analyzers/CSharp/Tests/ConvertNamespace/ConvertToFileScopedNamespaceAnalyzerTests.cs @@ -16,7 +16,7 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.ConvertNamespace { using VerifyCS = CSharpCodeFixVerifier; - public class ConvertToFileScopedNamespaceAnalyzerTests + public sealed class ConvertToFileScopedNamespaceAnalyzerTests { [Fact] public async Task TestNoConvertToFileScopedInCSharp9() @@ -1100,5 +1100,449 @@ class goobar { } } }.RunAsync(); } + + [Fact] + public async Task TestInterpolatedRawString1() + { + await new VerifyCS.Test + { + TestCode = """" + [|namespace Microsoft.CodeAnalysis.SQLite.v2|] + { + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + insert or replace into {0}.{1} + ({2}) + """; + + return; + + string GetSelectRowIdQuery(string database) + => $""" + select rowid from {0}.{1} where + {2} + limit 1 + """; + } + } + } + } + """", + FixedCode = """" + namespace $$Microsoft.CodeAnalysis.SQLite.v2; + + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + insert or replace into {0}.{1} + ({2}) + """; + + return; + + string GetSelectRowIdQuery(string database) + => $""" + select rowid from {0}.{1} where + {2} + limit 1 + """; + } + } + } + """", + LanguageVersion = LanguageVersion.CSharp11, + Options = + { + { CSharpCodeStyleOptions.NamespaceDeclarations, NamespaceDeclarationPreference.FileScoped } + } + }.RunAsync(); + } + + [Fact] + public async Task TestInterpolatedRawString2() + { + await new VerifyCS.Test + { + TestCode = """" + [|namespace Microsoft.CodeAnalysis.SQLite.v2|] + { + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + insert or replace into {0}.{1} + ({2}) + """; + + return; + + string GetSelectRowIdQuery(string database) => $""" + select rowid from {0}.{1} where + {2} + limit 1 + """; + } + } + } + } + """", + FixedCode = """" + namespace $$Microsoft.CodeAnalysis.SQLite.v2; + + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + insert or replace into {0}.{1} + ({2}) + """; + + return; + + string GetSelectRowIdQuery(string database) => $""" + select rowid from {0}.{1} where + {2} + limit 1 + """; + } + } + } + """", + LanguageVersion = LanguageVersion.CSharp11, + Options = + { + { CSharpCodeStyleOptions.NamespaceDeclarations, NamespaceDeclarationPreference.FileScoped } + } + }.RunAsync(); + } + + [Fact] + public async Task TestInterpolatedRawString3() + { + await new VerifyCS.Test + { + TestCode = """" + [|namespace Microsoft.CodeAnalysis.SQLite.v2|] + { + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + insert or replace into {0}.{1} + ({2}) + """; + + return; + + string GetSelectRowIdQuery(string database) => + $""" + select rowid from {0}.{1} where + {2} + limit 1 + """; + } + } + } + } + """", + FixedCode = """" + namespace $$Microsoft.CodeAnalysis.SQLite.v2; + + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + insert or replace into {0}.{1} + ({2}) + """; + + return; + + string GetSelectRowIdQuery(string database) => + $""" + select rowid from {0}.{1} where + {2} + limit 1 + """; + } + } + } + """", + LanguageVersion = LanguageVersion.CSharp11, + Options = + { + { CSharpCodeStyleOptions.NamespaceDeclarations, NamespaceDeclarationPreference.FileScoped } + } + }.RunAsync(); + } + + [Fact] + public async Task TestInterpolatedRawString4() + { + await new VerifyCS.Test + { + TestCode = """" + [|namespace Microsoft.CodeAnalysis.SQLite.v2|] + { + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + + insert or replace into {0}.{1} + + ({2}) + + """; + + return; + + string GetSelectRowIdQuery(string database) + => $""" + + select rowid from {0}.{1} where + + {2} + + limit 1 + + """; + } + } + } + } + """", + FixedCode = """" + namespace $$Microsoft.CodeAnalysis.SQLite.v2; + + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + + insert or replace into {0}.{1} + + ({2}) + + """; + + return; + + string GetSelectRowIdQuery(string database) + => $""" + + select rowid from {0}.{1} where + + {2} + + limit 1 + + """; + } + } + } + """", + LanguageVersion = LanguageVersion.CSharp11, + Options = + { + { CSharpCodeStyleOptions.NamespaceDeclarations, NamespaceDeclarationPreference.FileScoped } + } + }.RunAsync(); + } + + [Fact] + public async Task TestInterpolatedRawString5() + { + await new VerifyCS.Test + { + TestCode = """" + [|namespace Microsoft.CodeAnalysis.SQLite.v2|] + { + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + insert or replace into {0}.{1} + ({2}) + """; + + return; + + string GetSelectRowIdQuery(string database) + => $""" + select rowid from {0}.{1} where + {2} + limit 1 + """; + } + } + } + } + """", + FixedCode = """" + namespace $$Microsoft.CodeAnalysis.SQLite.v2; + + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + insert or replace into {0}.{1} + ({2}) + """; + + return; + + string GetSelectRowIdQuery(string database) + => $""" + select rowid from {0}.{1} where + {2} + limit 1 + """; + } + } + } + """", + LanguageVersion = LanguageVersion.CSharp11, + Options = + { + { CSharpCodeStyleOptions.NamespaceDeclarations, NamespaceDeclarationPreference.FileScoped } + } + }.RunAsync(); + } + + [Fact] + public async Task TestInterpolatedRawString6() + { + await new VerifyCS.Test + { + TestCode = """" + [|namespace Microsoft.CodeAnalysis.SQLite.v2|] + { + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + insert or replace into {0}.{1} + ({2}) + """; + + return; + + string GetSelectRowIdQuery(string database) + => $""" + select rowid from {0}.{1} where + {2} + limit 1 + """; + } + } + } + } + """", + FixedCode = """" + namespace $$Microsoft.CodeAnalysis.SQLite.v2; + + internal partial class SQLitePersistentStorage + { + private abstract class Accessor + where TDatabaseKey : struct + { + private string _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data; + + public Accessor() + { + _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data = $""" + insert or replace into {0}.{1} + ({2}) + """; + + return; + + string GetSelectRowIdQuery(string database) + => $""" + select rowid from {0}.{1} where + {2} + limit 1 + """; + } + } + } + """", + LanguageVersion = LanguageVersion.CSharp11, + Options = + { + { CSharpCodeStyleOptions.NamespaceDeclarations, NamespaceDeclarationPreference.FileScoped } + } + }.RunAsync(); + } } } diff --git a/src/Analyzers/CSharp/Tests/RemoveUnnecessaryCast/RemoveUnnecessaryCastTests.cs b/src/Analyzers/CSharp/Tests/RemoveUnnecessaryCast/RemoveUnnecessaryCastTests.cs index c04f806482679..6ea90acdd4ec4 100644 --- a/src/Analyzers/CSharp/Tests/RemoveUnnecessaryCast/RemoveUnnecessaryCastTests.cs +++ b/src/Analyzers/CSharp/Tests/RemoveUnnecessaryCast/RemoveUnnecessaryCastTests.cs @@ -13757,7 +13757,7 @@ void M2() } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/71926")] - public async Task NecessaryDelegateCast() + public async Task NecessaryDelegateCast1() { await new VerifyCS.Test { @@ -13778,4 +13778,34 @@ static void Main(string[] args) ReferenceAssemblies = ReferenceAssemblies.Net.Net80, }.RunAsync(); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72134")] + public async Task NecessaryDelegateCast2() + { + await new VerifyCS.Test + { + TestCode = """ + using System; + + public class MyClass + { + static void Main() + { + Goo f = (Action)(() => { }); // IDE0004: Cast is redundant. + + } + } + + public class Goo + { + public static implicit operator Goo(Action value) + { + return default!; + } + } + """, + LanguageVersion = LanguageVersion.CSharp12, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }.RunAsync(); + } } diff --git a/src/Analyzers/CSharp/Tests/RemoveUnnecessaryNullableDirective/CSharpRemoveRedundantNullableDirectiveTests.cs b/src/Analyzers/CSharp/Tests/RemoveUnnecessaryNullableDirective/CSharpRemoveRedundantNullableDirectiveTests.cs index ca2d1ccd34d85..8e9539da0ed53 100644 --- a/src/Analyzers/CSharp/Tests/RemoveUnnecessaryNullableDirective/CSharpRemoveRedundantNullableDirectiveTests.cs +++ b/src/Analyzers/CSharp/Tests/RemoveUnnecessaryNullableDirective/CSharpRemoveRedundantNullableDirectiveTests.cs @@ -182,13 +182,97 @@ class Program """ // File Header - class Program { } """); } + [Fact] + public async Task TestRedundantDirectiveBetweenUsingAndNamespace() + { + await VerifyCodeFixAsync( + NullableContextOptions.Enable, + """ + using System; + + [|#nullable enable|] + + namespace MyNamespace + { + class MyClass + { + } + } + """, + """ + using System; + + namespace MyNamespace + { + class MyClass + { + } + } + """); + } + + [Fact] + public async Task TestRedundantDirectiveBetweenUsingAndNamespace2() + { + await VerifyCodeFixAsync( + NullableContextOptions.Enable, + """ + using System; + [|#nullable enable|] + + namespace MyNamespace + { + class MyClass + { + } + } + """, + """ + using System; + + namespace MyNamespace + { + class MyClass + { + } + } + """); + } + + [Fact] + public async Task TestRedundantDirectiveBetweenUsingAndNamespace3() + { + await VerifyCodeFixAsync( + NullableContextOptions.Enable, + """ + using System; + + [|#nullable enable|] + namespace MyNamespace + { + class MyClass + { + } + } + """, + """ + using System; + + namespace MyNamespace + { + class MyClass + { + } + } + """); + } + [Fact] public async Task TestRedundantDirectiveWithNamespaceAndDerivedType() { @@ -219,6 +303,155 @@ class ProgramException : Exception """); } + [Fact] + public async Task TestRedundantDirectiveMultiple1() + { + await VerifyCodeFixAsync( + NullableContextOptions.Enable, + """ + using System; + + [|#nullable enable|] + [|#nullable enable|] + + namespace MyNamespace + { + class MyClass + { + } + } + """, + """ + using System; + + namespace MyNamespace + { + class MyClass + { + } + } + """); + } + + [Fact] + public async Task TestRedundantDirectiveMultiple2() + { + await VerifyCodeFixAsync( + NullableContextOptions.Enable, + """ + using System; + + [|#nullable enable|] + + [|#nullable enable|] + + namespace MyNamespace + { + class MyClass + { + } + } + """, + """ + using System; + + namespace MyNamespace + { + class MyClass + { + } + } + """); + } + + [Fact] + public async Task TestRedundantDirectiveMultiple3() + { + await VerifyCodeFixAsync( + NullableContextOptions.Enable, + """ + using System; + [|#nullable enable|] + [|#nullable enable|] + + namespace MyNamespace + { + class MyClass + { + } + } + """, + """ + using System; + + namespace MyNamespace + { + class MyClass + { + } + } + """); + } + + [Fact] + public async Task TestRedundantDirectiveMultiple4() + { + await VerifyCodeFixAsync( + NullableContextOptions.Enable, + """ + using System; + [|#nullable enable|] + + [|#nullable enable|] + + namespace MyNamespace + { + class MyClass + { + } + } + """, + """ + using System; + + namespace MyNamespace + { + class MyClass + { + } + } + """); + } + + [Fact] + public async Task TestRedundantDirectiveMultiple5() + { + await VerifyCodeFixAsync( + NullableContextOptions.Enable, + """ + using System; + + [|#nullable enable|] + [|#nullable enable|] + namespace MyNamespace + { + class MyClass + { + } + } + """, + """ + using System; + + namespace MyNamespace + { + class MyClass + { + } + } + """); + } + private static string GetDisableDirectiveContext(NullableContextOptions options) { return options switch diff --git a/src/Analyzers/CSharp/Tests/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveTests.cs b/src/Analyzers/CSharp/Tests/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveTests.cs index 5d261d2930df2..36c8e57cbb8b8 100644 --- a/src/Analyzers/CSharp/Tests/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveTests.cs +++ b/src/Analyzers/CSharp/Tests/RemoveUnnecessaryNullableDirective/CSharpRemoveUnnecessaryNullableDirectiveTests.cs @@ -86,7 +86,6 @@ enum EnumName """ // File Header - enum EnumName { First, diff --git a/src/Analyzers/CSharp/Tests/UseObjectInitializer/UseObjectInitializerTests.cs b/src/Analyzers/CSharp/Tests/UseObjectInitializer/UseObjectInitializerTests.cs index b791cdf84f403..6450541ea6d43 100644 --- a/src/Analyzers/CSharp/Tests/UseObjectInitializer/UseObjectInitializerTests.cs +++ b/src/Analyzers/CSharp/Tests/UseObjectInitializer/UseObjectInitializerTests.cs @@ -1041,5 +1041,89 @@ class MyClass } """, OutputKind.ConsoleApplication); } + + [Theory] + [InlineData(8.0)] + [InlineData(9.0)] + [WorkItem("https://github.com/dotnet/roslyn/issues/72094")] + public async Task TestWithConflictingSeverityConfigurationEntries(double analysisLevel) + { + var expectFix = analysisLevel >= 9.0; + + string testCode, fixedCode; + if (expectFix) + { + testCode = + """ + class C + { + int i; + + void M() + { + var c = [|new|] C(); + c.i = 1; + } + } + """; + + fixedCode = + """ + class C + { + int i; + + void M() + { + var c = new C + { + i = 1 + }; + } + } + """; + } + else + { + testCode = + """ + class C + { + int i; + + void M() + { + var c = new C(); + c.i = 1; + } + } + """; + fixedCode = testCode; + } + + var globalConfig = + $""" + is_global = true + + dotnet_style_object_initializer = true:suggestion + dotnet_diagnostic.IDE0017.severity = none + + build_property.EffectiveAnalysisLevelStyle = {analysisLevel} + """; + + await new VerifyCS.Test + { + TestState = + { + Sources = { testCode }, + AnalyzerConfigFiles = + { + ("/.globalconfig", globalConfig), + } + }, + FixedState = { Sources = { fixedCode } }, + LanguageVersion = LanguageVersion.CSharp12, + }.RunAsync(); + } } } diff --git a/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer.cs index a73c5c65e0bc8..2a9338532b838 100644 --- a/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer.cs @@ -11,115 +11,114 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeStyle +namespace Microsoft.CodeAnalysis.CodeStyle; + +internal abstract partial class AbstractBuiltInCodeStyleDiagnosticAnalyzer { - internal abstract partial class AbstractBuiltInCodeStyleDiagnosticAnalyzer + /// + /// Constructor for a code style analyzer with a single diagnostic descriptor and + /// unique code style option. + /// + /// Diagnostic ID reported by this analyzer + /// Build enforcement recommendation for this analyzer + /// + /// Code style editorconfig option that can be used to configure the given . + /// , if there is no such option that can be set in an editorconfig. + /// + /// Title for the diagnostic descriptor + /// + /// Message for the diagnostic descriptor. + /// if the message is identical to the title. + /// + /// if the diagnostic is reported on unnecessary code; otherwise, . + /// Flag indicating if the reported diagnostics are configurable by the end users + protected AbstractBuiltInCodeStyleDiagnosticAnalyzer( + string diagnosticId, + EnforceOnBuild enforceOnBuild, + IOption2? option, + LocalizableString title, + LocalizableString? messageFormat = null, + bool isUnnecessary = false, + bool configurable = true) + : this(diagnosticId, enforceOnBuild, title, messageFormat, isUnnecessary, configurable, + hasAnyCodeStyleOption: option != null) { - /// - /// Constructor for a code style analyzer with a single diagnostic descriptor and - /// unique code style option. - /// - /// Diagnostic ID reported by this analyzer - /// Build enforcement recommendation for this analyzer - /// - /// Code style editorconfig option that can be used to configure the given . - /// , if there is no such option that can be set in an editorconfig. - /// - /// Title for the diagnostic descriptor - /// - /// Message for the diagnostic descriptor. - /// if the message is identical to the title. - /// - /// if the diagnostic is reported on unnecessary code; otherwise, . - /// Flag indicating if the reported diagnostics are configurable by the end users - protected AbstractBuiltInCodeStyleDiagnosticAnalyzer( - string diagnosticId, - EnforceOnBuild enforceOnBuild, - IOption2? option, - LocalizableString title, - LocalizableString? messageFormat = null, - bool isUnnecessary = false, - bool configurable = true) - : this(diagnosticId, enforceOnBuild, title, messageFormat, isUnnecessary, configurable, - hasAnyCodeStyleOption: option != null) - { - AddDiagnosticIdToOptionMapping(diagnosticId, option); - } + AddDiagnosticIdToOptionMapping(diagnosticId, option); + } - /// - /// Constructor for a code style analyzer with a single diagnostic descriptor and - /// two or more code style options. - /// - /// Diagnostic ID reported by this analyzer - /// Build enforcement recommendation for this analyzer - /// - /// Set of two or more code style editorconfig options that can be used to configure the diagnostic severity of the given diagnosticId. - /// - /// Title for the diagnostic descriptor - /// - /// Message for the diagnostic descriptor. - /// Null if the message is identical to the title. - /// - /// if the diagnostic is reported on unnecessary code; otherwise, . - /// Flag indicating if the reported diagnostics are configurable by the end users - protected AbstractBuiltInCodeStyleDiagnosticAnalyzer( - string diagnosticId, - EnforceOnBuild enforceOnBuild, - ImmutableHashSet options, - LocalizableString title, - LocalizableString? messageFormat = null, - bool isUnnecessary = false, - bool configurable = true) - : this(diagnosticId, enforceOnBuild, title, messageFormat, isUnnecessary, configurable, - hasAnyCodeStyleOption: true) - { - RoslynDebug.Assert(options != null); - Debug.Assert(options.Count > 1); - AddDiagnosticIdToOptionMapping(diagnosticId, options); - } + /// + /// Constructor for a code style analyzer with a single diagnostic descriptor and + /// two or more code style options. + /// + /// Diagnostic ID reported by this analyzer + /// Build enforcement recommendation for this analyzer + /// + /// Set of two or more code style editorconfig options that can be used to configure the diagnostic severity of the given diagnosticId. + /// + /// Title for the diagnostic descriptor + /// + /// Message for the diagnostic descriptor. + /// Null if the message is identical to the title. + /// + /// if the diagnostic is reported on unnecessary code; otherwise, . + /// Flag indicating if the reported diagnostics are configurable by the end users + protected AbstractBuiltInCodeStyleDiagnosticAnalyzer( + string diagnosticId, + EnforceOnBuild enforceOnBuild, + ImmutableHashSet options, + LocalizableString title, + LocalizableString? messageFormat = null, + bool isUnnecessary = false, + bool configurable = true) + : this(diagnosticId, enforceOnBuild, title, messageFormat, isUnnecessary, configurable, + hasAnyCodeStyleOption: true) + { + RoslynDebug.Assert(options != null); + Debug.Assert(options.Count > 1); + AddDiagnosticIdToOptionMapping(diagnosticId, options); + } - /// - /// Constructor for a code style analyzer with a multiple diagnostic descriptors with a code style editorconfig option that can be used to configure each descriptor. - /// - protected AbstractBuiltInCodeStyleDiagnosticAnalyzer(ImmutableDictionary supportedDiagnosticsWithOptions) - : this(supportedDiagnosticsWithOptions.Keys.ToImmutableArray()) + /// + /// Constructor for a code style analyzer with a multiple diagnostic descriptors with a code style editorconfig option that can be used to configure each descriptor. + /// + protected AbstractBuiltInCodeStyleDiagnosticAnalyzer(ImmutableDictionary supportedDiagnosticsWithOptions) + : this(supportedDiagnosticsWithOptions.Keys.ToImmutableArray()) + { + foreach (var (descriptor, option) in supportedDiagnosticsWithOptions) { - foreach (var (descriptor, option) in supportedDiagnosticsWithOptions) - { - Debug.Assert(option != null == descriptor.CustomTags.Contains(WellKnownDiagnosticTags.CustomSeverityConfigurable)); - AddDiagnosticIdToOptionMapping(descriptor.Id, option); - } + Debug.Assert(option != null == descriptor.CustomTags.Contains(WellKnownDiagnosticTags.CustomSeverityConfigurable)); + AddDiagnosticIdToOptionMapping(descriptor.Id, option); } + } - /// - /// Constructor for a code style analyzer with multiple diagnostic descriptors with zero or more code style editorconfig options that can be used to configure each descriptor. - /// - protected AbstractBuiltInCodeStyleDiagnosticAnalyzer(ImmutableDictionary> supportedDiagnosticsWithOptions) - : this(supportedDiagnosticsWithOptions.Keys.ToImmutableArray()) + /// + /// Constructor for a code style analyzer with multiple diagnostic descriptors with zero or more code style editorconfig options that can be used to configure each descriptor. + /// + protected AbstractBuiltInCodeStyleDiagnosticAnalyzer(ImmutableDictionary> supportedDiagnosticsWithOptions) + : this(supportedDiagnosticsWithOptions.Keys.ToImmutableArray()) + { + foreach (var (descriptor, options) in supportedDiagnosticsWithOptions) { - foreach (var (descriptor, options) in supportedDiagnosticsWithOptions) + if (!options.IsEmpty) { - if (!options.IsEmpty) - { - AddDiagnosticIdToOptionMapping(descriptor.Id, options); - } + AddDiagnosticIdToOptionMapping(descriptor.Id, options); } } + } - private static void AddDiagnosticIdToOptionMapping(string diagnosticId, IOption2? option) + private static void AddDiagnosticIdToOptionMapping(string diagnosticId, IOption2? option) + { + if (option != null) { - if (option != null) - { - AddDiagnosticIdToOptionMapping(diagnosticId, [option]); - } + AddDiagnosticIdToOptionMapping(diagnosticId, [option]); } + } - private static void AddDiagnosticIdToOptionMapping(string diagnosticId, ImmutableHashSet options) - => IDEDiagnosticIdToOptionMappingHelper.AddOptionMapping(diagnosticId, options); + private static void AddDiagnosticIdToOptionMapping(string diagnosticId, ImmutableHashSet options) + => IDEDiagnosticIdToOptionMappingHelper.AddOptionMapping(diagnosticId, options); - public abstract DiagnosticAnalyzerCategory GetAnalyzerCategory(); + public abstract DiagnosticAnalyzerCategory GetAnalyzerCategory(); - public virtual bool OpenFileOnly(SimplifierOptions? options) - => false; - } + public virtual bool OpenFileOnly(SimplifierOptions? options) + => false; } diff --git a/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer_Core.cs b/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer_Core.cs index 8ed6219520afc..59d80030b5808 100644 --- a/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer_Core.cs +++ b/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer_Core.cs @@ -12,256 +12,248 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeStyle +namespace Microsoft.CodeAnalysis.CodeStyle; + +internal abstract partial class AbstractBuiltInCodeStyleDiagnosticAnalyzer : DiagnosticAnalyzer, IBuiltInAnalyzer { - internal abstract partial class AbstractBuiltInCodeStyleDiagnosticAnalyzer : DiagnosticAnalyzer, IBuiltInAnalyzer + protected readonly DiagnosticDescriptor Descriptor; + private DiagnosticSeverity? _minimumReportedSeverity; + + private AbstractBuiltInCodeStyleDiagnosticAnalyzer( + string descriptorId, + EnforceOnBuild enforceOnBuild, + LocalizableString title, + LocalizableString? messageFormat, + bool isUnnecessary, + bool configurable, + bool hasAnyCodeStyleOption) { - protected readonly DiagnosticDescriptor Descriptor; - private DiagnosticSeverity? _minimumReportedSeverity; - - private AbstractBuiltInCodeStyleDiagnosticAnalyzer( - string descriptorId, - EnforceOnBuild enforceOnBuild, - LocalizableString title, - LocalizableString? messageFormat, - bool isUnnecessary, - bool configurable, - bool hasAnyCodeStyleOption) - { - // 'isUnnecessary' should be true only for sub-types of AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer. - Debug.Assert(!isUnnecessary || this is AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer); + // 'isUnnecessary' should be true only for sub-types of AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer. + Debug.Assert(!isUnnecessary || this is AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer); - Descriptor = CreateDescriptorWithId(descriptorId, enforceOnBuild, hasAnyCodeStyleOption, title, messageFormat ?? title, isUnnecessary: isUnnecessary, isConfigurable: configurable); - SupportedDiagnostics = [Descriptor]; - } + Descriptor = CreateDescriptorWithId(descriptorId, enforceOnBuild, hasAnyCodeStyleOption, title, messageFormat ?? title, isUnnecessary: isUnnecessary, isConfigurable: configurable); + SupportedDiagnostics = [Descriptor]; + } - /// - /// Constructor for a code style analyzer with a multiple diagnostic descriptors such that all the descriptors have no unique code style option to configure the descriptors. - /// - protected AbstractBuiltInCodeStyleDiagnosticAnalyzer(ImmutableArray supportedDiagnostics) - { - SupportedDiagnostics = supportedDiagnostics; + /// + /// Constructor for a code style analyzer with a multiple diagnostic descriptors such that all the descriptors have no unique code style option to configure the descriptors. + /// + protected AbstractBuiltInCodeStyleDiagnosticAnalyzer(ImmutableArray supportedDiagnostics) + { + SupportedDiagnostics = supportedDiagnostics; - Descriptor = SupportedDiagnostics[0]; - Debug.Assert(!supportedDiagnostics.Any(descriptor => descriptor.CustomTags.Any(t => t == WellKnownDiagnosticTags.Unnecessary)) || this is AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer); - } + Descriptor = SupportedDiagnostics[0]; + Debug.Assert(!supportedDiagnostics.Any(descriptor => descriptor.CustomTags.Any(t => t == WellKnownDiagnosticTags.Unnecessary)) || this is AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer); + } - public virtual bool IsHighPriority => false; - public sealed override ImmutableArray SupportedDiagnostics { get; } - - protected static DiagnosticDescriptor CreateDescriptorWithId( - string id, - EnforceOnBuild enforceOnBuild, - bool hasAnyCodeStyleOption, - LocalizableString title, - LocalizableString? messageFormat = null, - bool isUnnecessary = false, - bool isConfigurable = true, - LocalizableString? description = null) + public virtual bool IsHighPriority => false; + public sealed override ImmutableArray SupportedDiagnostics { get; } + + protected static DiagnosticDescriptor CreateDescriptorWithId( + string id, + EnforceOnBuild enforceOnBuild, + bool hasAnyCodeStyleOption, + LocalizableString title, + LocalizableString? messageFormat = null, + bool isUnnecessary = false, + bool isConfigurable = true, + LocalizableString? description = null) #pragma warning disable RS0030 // Do not used banned APIs - => new( - id, title, messageFormat ?? title, - DiagnosticCategory.Style, - DiagnosticSeverity.Hidden, - isEnabledByDefault: true, - description: description, - helpLinkUri: DiagnosticHelper.GetHelpLinkForDiagnosticId(id), - customTags: DiagnosticCustomTags.Create(isUnnecessary, isConfigurable, isCustomConfigurable: hasAnyCodeStyleOption, enforceOnBuild)); + => new( + id, title, messageFormat ?? title, + DiagnosticCategory.Style, + DiagnosticSeverity.Hidden, + isEnabledByDefault: true, + description: description, + helpLinkUri: DiagnosticHelper.GetHelpLinkForDiagnosticId(id), + customTags: DiagnosticCustomTags.Create(isUnnecessary, isConfigurable, isCustomConfigurable: hasAnyCodeStyleOption, enforceOnBuild)); #pragma warning restore RS0030 // Do not used banned APIs - /// - /// Flags to configure the analysis of generated code. - /// By default, code style analyzers should not analyze or report diagnostics on generated code, so the value is false. - /// - protected virtual GeneratedCodeAnalysisFlags GeneratedCodeAnalysisFlags => GeneratedCodeAnalysisFlags.None; + /// + /// Flags to configure the analysis of generated code. + /// By default, code style analyzers should not analyze or report diagnostics on generated code, so the value is false. + /// + protected virtual GeneratedCodeAnalysisFlags GeneratedCodeAnalysisFlags => GeneratedCodeAnalysisFlags.None; - public sealed override void Initialize(AnalysisContext context) - { - _minimumReportedSeverity = context.MinimumReportedSeverity; - - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags); - context.EnableConcurrentExecution(); + public sealed override void Initialize(AnalysisContext context) + { + _minimumReportedSeverity = context.MinimumReportedSeverity; - InitializeWorker(context); - } + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags); + context.EnableConcurrentExecution(); - protected abstract void InitializeWorker(AnalysisContext context); + InitializeWorker(context); + } - protected static bool IsAnalysisLevelGreaterThanOrEquals(int minAnalysisLevel, AnalyzerOptions analyzerOptions) - { - // See https://github.com/dotnet/roslyn/pull/70794 for details. - const string AnalysisLevelKey = "build_property.EffectiveAnalysisLevelStyle"; + protected abstract void InitializeWorker(AnalysisContext context); - return analyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(AnalysisLevelKey, out var value) - && double.TryParse(value, out var version) - && version >= minAnalysisLevel; - } + protected static bool IsAnalysisLevelGreaterThanOrEquals(int minAnalysisLevel, AnalyzerOptions analyzerOptions) + => analyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.IsAnalysisLevelGreaterThanOrEquals(minAnalysisLevel); - protected bool ShouldSkipAnalysis(SemanticModelAnalysisContext context, NotificationOption2? notification) - => ShouldSkipAnalysis(context.FilterTree, context.Options, context.SemanticModel.Compilation.Options, notification, context.CancellationToken); + protected bool ShouldSkipAnalysis(SemanticModelAnalysisContext context, NotificationOption2? notification) + => ShouldSkipAnalysis(context.FilterTree, context.Options, context.SemanticModel.Compilation.Options, notification, context.CancellationToken); - protected bool ShouldSkipAnalysis(SyntaxNodeAnalysisContext context, NotificationOption2? notification) - => ShouldSkipAnalysis(context.Node.SyntaxTree, context.Options, context.Compilation.Options, notification, context.CancellationToken); + protected bool ShouldSkipAnalysis(SyntaxNodeAnalysisContext context, NotificationOption2? notification) + => ShouldSkipAnalysis(context.Node.SyntaxTree, context.Options, context.Compilation.Options, notification, context.CancellationToken); - protected bool ShouldSkipAnalysis(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions, NotificationOption2? notification) - => ShouldSkipAnalysis(context.Tree, context.Options, compilationOptions, notification, context.CancellationToken); + protected bool ShouldSkipAnalysis(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions, NotificationOption2? notification) + => ShouldSkipAnalysis(context.Tree, context.Options, compilationOptions, notification, context.CancellationToken); - protected bool ShouldSkipAnalysis(CodeBlockAnalysisContext context, NotificationOption2? notification) - => ShouldSkipAnalysis(context.FilterTree, context.Options, context.SemanticModel.Compilation.Options, notification, context.CancellationToken); + protected bool ShouldSkipAnalysis(CodeBlockAnalysisContext context, NotificationOption2? notification) + => ShouldSkipAnalysis(context.FilterTree, context.Options, context.SemanticModel.Compilation.Options, notification, context.CancellationToken); - protected bool ShouldSkipAnalysis(OperationAnalysisContext context, NotificationOption2? notification) - => ShouldSkipAnalysis(context.FilterTree, context.Options, context.Compilation.Options, notification, context.CancellationToken); + protected bool ShouldSkipAnalysis(OperationAnalysisContext context, NotificationOption2? notification) + => ShouldSkipAnalysis(context.FilterTree, context.Options, context.Compilation.Options, notification, context.CancellationToken); - protected bool ShouldSkipAnalysis(OperationBlockAnalysisContext context, NotificationOption2? notification) - => ShouldSkipAnalysis(context.FilterTree, context.Options, context.Compilation.Options, notification, context.CancellationToken); + protected bool ShouldSkipAnalysis(OperationBlockAnalysisContext context, NotificationOption2? notification) + => ShouldSkipAnalysis(context.FilterTree, context.Options, context.Compilation.Options, notification, context.CancellationToken); - protected bool ShouldSkipAnalysis( - SyntaxTree tree, - AnalyzerOptions analyzerOptions, - CompilationOptions compilationOptions, - NotificationOption2? notification, - CancellationToken cancellationToken) - => ShouldSkipAnalysis(tree, analyzerOptions, compilationOptions, notification, performDescriptorsCheck: true, cancellationToken); + protected bool ShouldSkipAnalysis( + SyntaxTree tree, + AnalyzerOptions analyzerOptions, + CompilationOptions compilationOptions, + NotificationOption2? notification, + CancellationToken cancellationToken) + => ShouldSkipAnalysis(tree, analyzerOptions, compilationOptions, notification, performDescriptorsCheck: true, cancellationToken); - protected bool ShouldSkipAnalysis( - SyntaxTree tree, - AnalyzerOptions analyzerOptions, - CompilationOptions compilationOptions, - ImmutableArray notifications, - CancellationToken cancellationToken) + protected bool ShouldSkipAnalysis( + SyntaxTree tree, + AnalyzerOptions analyzerOptions, + CompilationOptions compilationOptions, + ImmutableArray notifications, + CancellationToken cancellationToken) + { + // We need to check if the analyzer's severity has been escalated either via 'option_name = option_value:severity' + // setting or 'dotnet_diagnostic.RuleId.severity = severity'. + // For the former, we check if any of the given notifications have been escalated via the ':severity' such + // that analysis cannot be skipped. For the latter, we perform descriptor-based checks. + // Descriptors check verifies if any of the diagnostic IDs reported by this analyzer + // have been escalated to a severity that they must be executed. + + // PERF: Execute the descriptors check only once for the analyzer, not once per each notification option. + var performDescriptorsCheck = true; + + // Check if any of the notifications are enabled, if so we need to execute analysis. + foreach (var notification in notifications) { - // We need to check if the analyzer's severity has been escalated either via 'option_name = option_value:severity' - // setting or 'dotnet_diagnostic.RuleId.severity = severity'. - // For the former, we check if any of the given notifications have been escalated via the ':severity' such - // that analysis cannot be skipped. For the latter, we perform descriptor-based checks. - // Descriptors check verifies if any of the diagnostic IDs reported by this analyzer - // have been escalated to a severity that they must be executed. - - // PERF: Execute the descriptors check only once for the analyzer, not once per each notification option. - var performDescriptorsCheck = true; - - // Check if any of the notifications are enabled, if so we need to execute analysis. - foreach (var notification in notifications) - { - if (!ShouldSkipAnalysis(tree, analyzerOptions, compilationOptions, notification, performDescriptorsCheck, cancellationToken)) - return false; - - if (performDescriptorsCheck) - performDescriptorsCheck = false; - } + if (!ShouldSkipAnalysis(tree, analyzerOptions, compilationOptions, notification, performDescriptorsCheck, cancellationToken)) + return false; - return true; + if (performDescriptorsCheck) + performDescriptorsCheck = false; } - private bool ShouldSkipAnalysis( - SyntaxTree tree, - AnalyzerOptions analyzerOptions, - CompilationOptions compilationOptions, - NotificationOption2? notification, - bool performDescriptorsCheck, - CancellationToken cancellationToken) - { - // We need to check if the analyzer's severity has been escalated either via 'option_name = option_value:severity' - // setting or 'dotnet_diagnostic.RuleId.severity = severity'. - // For the former, we check if the given notification have been escalated via the ':severity' such - // that analysis cannot be skipped. For the latter, we perform descriptor-based checks. - // Descriptors check verifies if any of the diagnostic IDs reported by this analyzer - // have been escalated to a severity that they must be executed. - - Debug.Assert(_minimumReportedSeverity != null); - - if (notification?.Severity == ReportDiagnostic.Suppress) - return true; + return true; + } - // If _minimumReportedSeverity is 'Hidden', then we are reporting diagnostics with all severities. - if (_minimumReportedSeverity!.Value == DiagnosticSeverity.Hidden) - return false; + private bool ShouldSkipAnalysis( + SyntaxTree tree, + AnalyzerOptions analyzerOptions, + CompilationOptions compilationOptions, + NotificationOption2? notification, + bool performDescriptorsCheck, + CancellationToken cancellationToken) + { + // We need to check if the analyzer's severity has been escalated either via 'option_name = option_value:severity' + // setting or 'dotnet_diagnostic.RuleId.severity = severity'. + // For the former, we check if the given notification have been escalated via the ':severity' such + // that analysis cannot be skipped. For the latter, we perform descriptor-based checks. + // Descriptors check verifies if any of the diagnostic IDs reported by this analyzer + // have been escalated to a severity that they must be executed. - // If the severity is explicitly configured with `option_name = option_value:severity`, - // we should skip analysis if the configured severity is lesser than the minimum reported severity. - // Additionally, notification based severity configuration is respected on build only for AnalysisLevel >= 9. - if (notification.HasValue - && notification.Value.IsExplicitlySpecified - && IsAnalysisLevelGreaterThanOrEquals(9, analyzerOptions)) - { - return notification.Value.Severity.ToDiagnosticSeverity() < _minimumReportedSeverity.Value; - } + Debug.Assert(_minimumReportedSeverity != null); - if (!performDescriptorsCheck) - return true; + if (notification?.Severity == ReportDiagnostic.Suppress) + return true; - // Otherwise, we check if any of the descriptors have been configured or bulk-configured - // in editorconfig/globalconfig options to a severity that is greater than or equal to - // the minimum reported severity. - // If so, we should execute analysis. Otherwise, analysis should be skipped. - // See https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope - // for precedence rules for configuring severity of a single rule ID, a category of rule IDs - // or all analyzer rule IDs. + // If _minimumReportedSeverity is 'Hidden', then we are reporting diagnostics with all severities. + if (_minimumReportedSeverity!.Value == DiagnosticSeverity.Hidden) + return false; - var severityOptionsProvider = compilationOptions.SyntaxTreeOptionsProvider!; - var globalOptions = analyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions; - var treeOptions = analyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree); + // If the severity is explicitly configured with `option_name = option_value:severity`, + // we should skip analysis if the configured severity is lesser than the minimum reported severity. + // Additionally, notification based severity configuration is respected on build only for AnalysisLevel >= 9. + if (notification.HasValue + && notification.Value.IsExplicitlySpecified + && IsAnalysisLevelGreaterThanOrEquals(9, analyzerOptions)) + { + return notification.Value.Severity.ToDiagnosticSeverity() < _minimumReportedSeverity.Value; + } - // See https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope - // for supported analyzer bulk configuration formats. - const string DotnetAnalyzerDiagnosticPrefix = "dotnet_analyzer_diagnostic"; - const string CategoryPrefix = "category"; - const string SeveritySuffix = "severity"; + if (!performDescriptorsCheck) + return true; - var allDiagnosticsBulkSeverityKey = $"{DotnetAnalyzerDiagnosticPrefix}.{SeveritySuffix}"; - var hasAllBulkSeverityConfiguration = treeOptions.TryGetValue(allDiagnosticsBulkSeverityKey, out var editorConfigBulkSeverity) - || globalOptions.TryGetValue(allDiagnosticsBulkSeverityKey, out editorConfigBulkSeverity); + // Otherwise, we check if any of the descriptors have been configured or bulk-configured + // in editorconfig/globalconfig options to a severity that is greater than or equal to + // the minimum reported severity. + // If so, we should execute analysis. Otherwise, analysis should be skipped. + // See https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope + // for precedence rules for configuring severity of a single rule ID, a category of rule IDs + // or all analyzer rule IDs. + + var severityOptionsProvider = compilationOptions.SyntaxTreeOptionsProvider!; + var globalOptions = analyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions; + var treeOptions = analyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree); + + // See https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope + // for supported analyzer bulk configuration formats. + const string DotnetAnalyzerDiagnosticPrefix = "dotnet_analyzer_diagnostic"; + const string CategoryPrefix = "category"; + const string SeveritySuffix = "severity"; + + var allDiagnosticsBulkSeverityKey = $"{DotnetAnalyzerDiagnosticPrefix}.{SeveritySuffix}"; + var hasAllBulkSeverityConfiguration = treeOptions.TryGetValue(allDiagnosticsBulkSeverityKey, out var editorConfigBulkSeverity) + || globalOptions.TryGetValue(allDiagnosticsBulkSeverityKey, out editorConfigBulkSeverity); + + foreach (var descriptor in SupportedDiagnostics) + { + if (descriptor.CustomTags.Contains(WellKnownDiagnosticTags.NotConfigurable)) + continue; - foreach (var descriptor in SupportedDiagnostics) + // First check if the diagnostic ID has been explicitly configured with `dotnet_diagnostic` entry. + if (severityOptionsProvider.TryGetDiagnosticValue(tree, descriptor.Id, cancellationToken, out var configuredReportDiagnostic) + || severityOptionsProvider.TryGetGlobalDiagnosticValue(descriptor.Id, cancellationToken, out configuredReportDiagnostic)) { - if (descriptor.CustomTags.Contains(WellKnownDiagnosticTags.NotConfigurable)) - continue; - - // First check if the diagnostic ID has been explicitly configured with `dotnet_diagnostic` entry. - if (severityOptionsProvider.TryGetDiagnosticValue(tree, descriptor.Id, cancellationToken, out var configuredReportDiagnostic) - || severityOptionsProvider.TryGetGlobalDiagnosticValue(descriptor.Id, cancellationToken, out configuredReportDiagnostic)) + if (configuredReportDiagnostic.ToDiagnosticSeverity() is { } configuredSeverity + && configuredSeverity >= _minimumReportedSeverity.Value) { - if (configuredReportDiagnostic.ToDiagnosticSeverity() is { } configuredSeverity - && configuredSeverity >= _minimumReportedSeverity.Value) - { - return false; - } - - continue; - } - - // Next, check if the descriptor's category has been bulk configured with `dotnet_analyzer_diagnostic.category-Category.severity` entry. - // or severity of all analyzer diagnostics has been bulk configured with `dotnet_analyzer_diagnostic.severity` entry. - var categoryConfigurationKey = $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{descriptor.Category}.{SeveritySuffix}"; - if (treeOptions.TryGetValue(categoryConfigurationKey, out var editorConfigSeverity) - || globalOptions.TryGetValue(categoryConfigurationKey, out editorConfigSeverity)) - { - } - else if (hasAllBulkSeverityConfiguration) - { - editorConfigSeverity = editorConfigBulkSeverity; + return false; } - else - { - // No diagnostic ID or bulk configuration for the descriptor. - // Check if the descriptor's default severity is greater than or equals the minimum reported severiity. - if (descriptor.IsEnabledByDefault && descriptor.DefaultSeverity >= _minimumReportedSeverity.Value) - return false; - // Otherwise, we can skip this descriptor as it cannot contribute a diagnostic that will be reported. - continue; - } + continue; + } - Debug.Assert(editorConfigSeverity != null); - if (EditorConfigSeverityStrings.TryParse(editorConfigSeverity!, out var effectiveReportDiagnostic) - && effectiveReportDiagnostic.ToDiagnosticSeverity() is { } effectiveSeverity - && effectiveSeverity >= _minimumReportedSeverity.Value) - { + // Next, check if the descriptor's category has been bulk configured with `dotnet_analyzer_diagnostic.category-Category.severity` entry. + // or severity of all analyzer diagnostics has been bulk configured with `dotnet_analyzer_diagnostic.severity` entry. + var categoryConfigurationKey = $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{descriptor.Category}.{SeveritySuffix}"; + if (treeOptions.TryGetValue(categoryConfigurationKey, out var editorConfigSeverity) + || globalOptions.TryGetValue(categoryConfigurationKey, out editorConfigSeverity)) + { + } + else if (hasAllBulkSeverityConfiguration) + { + editorConfigSeverity = editorConfigBulkSeverity; + } + else + { + // No diagnostic ID or bulk configuration for the descriptor. + // Check if the descriptor's default severity is greater than or equals the minimum reported severiity. + if (descriptor.IsEnabledByDefault && descriptor.DefaultSeverity >= _minimumReportedSeverity.Value) return false; - } + + // Otherwise, we can skip this descriptor as it cannot contribute a diagnostic that will be reported. + continue; } - return true; + Debug.Assert(editorConfigSeverity != null); + if (EditorConfigSeverityStrings.TryParse(editorConfigSeverity!, out var effectiveReportDiagnostic) + && effectiveReportDiagnostic.ToDiagnosticSeverity() is { } effectiveSeverity + && effectiveSeverity >= _minimumReportedSeverity.Value) + { + return false; + } } + + return true; } } diff --git a/src/Analyzers/Core/Analyzers/AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer.cs index bd1b77e9c2277..4ca8ddd49631f 100644 --- a/src/Analyzers/Core/Analyzers/AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer.cs @@ -8,131 +8,130 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.CodeStyle +namespace Microsoft.CodeAnalysis.CodeStyle; + +/// +/// Code style analyzer that reports at least one 'unnecessary' code diagnostic. +/// +internal abstract class AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { /// - /// Code style analyzer that reports at least one 'unnecessary' code diagnostic. + /// Constructor for an unnecessary code style analyzer with a single diagnostic descriptor and + /// unique code style option. /// - internal abstract class AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + /// Diagnostic ID reported by this analyzer + /// Build enforcement recommendation for this analyzer + /// + /// Code style option that can be used to configure the given . + /// , if there is no such unique option. + /// + /// + /// Per-language fading option that can be used to configure if the diagnostic should be faded in the IDE or not. + /// , if there is no such unique fading option. + /// + /// Title for the diagnostic descriptor + /// + /// Message for the diagnostic descriptor. + /// if the message is identical to the title. + /// + /// Flag indicating if the reported diagnostics are configurable by the end users + protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer( + string diagnosticId, + EnforceOnBuild enforceOnBuild, + IOption2? option, + PerLanguageOption2? fadingOption, + LocalizableString title, + LocalizableString? messageFormat = null, + bool configurable = true) + : base(diagnosticId, enforceOnBuild, option, title, messageFormat, isUnnecessary: true, configurable) { - /// - /// Constructor for an unnecessary code style analyzer with a single diagnostic descriptor and - /// unique code style option. - /// - /// Diagnostic ID reported by this analyzer - /// Build enforcement recommendation for this analyzer - /// - /// Code style option that can be used to configure the given . - /// , if there is no such unique option. - /// - /// - /// Per-language fading option that can be used to configure if the diagnostic should be faded in the IDE or not. - /// , if there is no such unique fading option. - /// - /// Title for the diagnostic descriptor - /// - /// Message for the diagnostic descriptor. - /// if the message is identical to the title. - /// - /// Flag indicating if the reported diagnostics are configurable by the end users - protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer( - string diagnosticId, - EnforceOnBuild enforceOnBuild, - IOption2? option, - PerLanguageOption2? fadingOption, - LocalizableString title, - LocalizableString? messageFormat = null, - bool configurable = true) - : base(diagnosticId, enforceOnBuild, option, title, messageFormat, isUnnecessary: true, configurable) - { - AddDiagnosticIdToFadingOptionMapping(diagnosticId, fadingOption); - } + AddDiagnosticIdToFadingOptionMapping(diagnosticId, fadingOption); + } - /// - /// Constructor for an unnecessary code style analyzer with a single diagnostic descriptor and - /// two or more code style options. - /// - /// Diagnostic ID reported by this analyzer - /// Build enforcement recommendation for this analyzer - /// - /// Set of two or more per-language options that can be used to configure the diagnostic severity of the given diagnosticId. - /// - /// - /// Per-language fading option that can be used to configure if the diagnostic should be faded in the IDE or not. - /// , if there is no such unique fading option. - /// - /// Title for the diagnostic descriptor - /// - /// Message for the diagnostic descriptor. - /// Null if the message is identical to the title. - /// - /// Flag indicating if the reported diagnostics are configurable by the end users - protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer( - string diagnosticId, - EnforceOnBuild enforceOnBuild, - ImmutableHashSet options, - PerLanguageOption2? fadingOption, - LocalizableString title, - LocalizableString? messageFormat = null, - bool configurable = true) - : base(diagnosticId, enforceOnBuild, options, title, messageFormat, isUnnecessary: true, configurable) - { - AddDiagnosticIdToFadingOptionMapping(diagnosticId, fadingOption); - } + /// + /// Constructor for an unnecessary code style analyzer with a single diagnostic descriptor and + /// two or more code style options. + /// + /// Diagnostic ID reported by this analyzer + /// Build enforcement recommendation for this analyzer + /// + /// Set of two or more per-language options that can be used to configure the diagnostic severity of the given diagnosticId. + /// + /// + /// Per-language fading option that can be used to configure if the diagnostic should be faded in the IDE or not. + /// , if there is no such unique fading option. + /// + /// Title for the diagnostic descriptor + /// + /// Message for the diagnostic descriptor. + /// Null if the message is identical to the title. + /// + /// Flag indicating if the reported diagnostics are configurable by the end users + protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer( + string diagnosticId, + EnforceOnBuild enforceOnBuild, + ImmutableHashSet options, + PerLanguageOption2? fadingOption, + LocalizableString title, + LocalizableString? messageFormat = null, + bool configurable = true) + : base(diagnosticId, enforceOnBuild, options, title, messageFormat, isUnnecessary: true, configurable) + { + AddDiagnosticIdToFadingOptionMapping(diagnosticId, fadingOption); + } - /// - /// Constructor for an unnecessary code style analyzer with multiple descriptors. All unnecessary descriptors will share the same - /// - /// Descriptors supported by this analyzer - /// The fading option used to control descriptors that are unnecessary. - protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer(ImmutableArray descriptors, PerLanguageOption2 fadingOption) - : base(descriptors) + /// + /// Constructor for an unnecessary code style analyzer with multiple descriptors. All unnecessary descriptors will share the same + /// + /// Descriptors supported by this analyzer + /// The fading option used to control descriptors that are unnecessary. + protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer(ImmutableArray descriptors, PerLanguageOption2 fadingOption) + : base(descriptors) + { + foreach (var descriptor in descriptors) { - foreach (var descriptor in descriptors) + if (descriptor.CustomTags.Any(t => t == WellKnownDiagnosticTags.Unnecessary)) { - if (descriptor.CustomTags.Any(t => t == WellKnownDiagnosticTags.Unnecessary)) - { - AddDiagnosticIdToFadingOptionMapping(descriptor.Id, fadingOption); - } + AddDiagnosticIdToFadingOptionMapping(descriptor.Id, fadingOption); } } + } - /// - /// Constructor for a code style analyzer with a multiple diagnostic descriptors with a code style options that can be used to configure each descriptor. - /// - protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer(ImmutableDictionary supportedDiagnosticsWithOptions, PerLanguageOption2? fadingOption) - : base(supportedDiagnosticsWithOptions) - { - AddDescriptorsToFadingOptionMapping(supportedDiagnosticsWithOptions.Keys, fadingOption); - } + /// + /// Constructor for a code style analyzer with a multiple diagnostic descriptors with a code style options that can be used to configure each descriptor. + /// + protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer(ImmutableDictionary supportedDiagnosticsWithOptions, PerLanguageOption2? fadingOption) + : base(supportedDiagnosticsWithOptions) + { + AddDescriptorsToFadingOptionMapping(supportedDiagnosticsWithOptions.Keys, fadingOption); + } - /// - /// Constructor for a code style analyzer with multiple diagnostic descriptors with zero or more code style options that can be used to configure each descriptor. - /// - protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer(ImmutableDictionary> supportedDiagnosticsWithOptions, PerLanguageOption2? fadingOption) - : base(supportedDiagnosticsWithOptions) - { - AddDescriptorsToFadingOptionMapping(supportedDiagnosticsWithOptions.Keys, fadingOption); - } + /// + /// Constructor for a code style analyzer with multiple diagnostic descriptors with zero or more code style options that can be used to configure each descriptor. + /// + protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer(ImmutableDictionary> supportedDiagnosticsWithOptions, PerLanguageOption2? fadingOption) + : base(supportedDiagnosticsWithOptions) + { + AddDescriptorsToFadingOptionMapping(supportedDiagnosticsWithOptions.Keys, fadingOption); + } - private static void AddDiagnosticIdToFadingOptionMapping(string diagnosticId, PerLanguageOption2? fadingOption) + private static void AddDiagnosticIdToFadingOptionMapping(string diagnosticId, PerLanguageOption2? fadingOption) + { + if (fadingOption != null) { - if (fadingOption != null) - { - IDEDiagnosticIdToOptionMappingHelper.AddFadingOptionMapping(diagnosticId, fadingOption); - } + IDEDiagnosticIdToOptionMappingHelper.AddFadingOptionMapping(diagnosticId, fadingOption); } + } - private static void AddDescriptorsToFadingOptionMapping(IEnumerable descriptors, PerLanguageOption2? fadingOption) + private static void AddDescriptorsToFadingOptionMapping(IEnumerable descriptors, PerLanguageOption2? fadingOption) + { + if (fadingOption != null) { - if (fadingOption != null) + foreach (var descriptor in descriptors) { - foreach (var descriptor in descriptors) + if (descriptor.CustomTags.Any(t => t == WellKnownDiagnosticTags.Unnecessary)) { - if (descriptor.CustomTags.Any(t => t == WellKnownDiagnosticTags.Unnecessary)) - { - IDEDiagnosticIdToOptionMappingHelper.AddFadingOptionMapping(descriptor.Id, fadingOption); - } + IDEDiagnosticIdToOptionMappingHelper.AddFadingOptionMapping(descriptor.Id, fadingOption); } } } diff --git a/src/Analyzers/Core/Analyzers/AbstractCodeQualityDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/AbstractCodeQualityDiagnosticAnalyzer.cs index 7ed692129e52f..48739b46c5a4b 100644 --- a/src/Analyzers/Core/Analyzers/AbstractCodeQualityDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/AbstractCodeQualityDiagnosticAnalyzer.cs @@ -7,57 +7,56 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Simplification; -namespace Microsoft.CodeAnalysis.CodeQuality +namespace Microsoft.CodeAnalysis.CodeQuality; + +internal abstract class AbstractCodeQualityDiagnosticAnalyzer : DiagnosticAnalyzer, IBuiltInAnalyzer { - internal abstract class AbstractCodeQualityDiagnosticAnalyzer : DiagnosticAnalyzer, IBuiltInAnalyzer + private readonly GeneratedCodeAnalysisFlags _generatedCodeAnalysisFlags; + + protected AbstractCodeQualityDiagnosticAnalyzer( + ImmutableArray descriptors, + GeneratedCodeAnalysisFlags generatedCodeAnalysisFlags) { - private readonly GeneratedCodeAnalysisFlags _generatedCodeAnalysisFlags; - - protected AbstractCodeQualityDiagnosticAnalyzer( - ImmutableArray descriptors, - GeneratedCodeAnalysisFlags generatedCodeAnalysisFlags) - { - SupportedDiagnostics = descriptors; - _generatedCodeAnalysisFlags = generatedCodeAnalysisFlags; - } - - public bool IsHighPriority => false; - public sealed override ImmutableArray SupportedDiagnostics { get; } - - public sealed override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(_generatedCodeAnalysisFlags); - context.EnableConcurrentExecution(); - - InitializeWorker(context); - } - - protected abstract void InitializeWorker(AnalysisContext context); - - public abstract DiagnosticAnalyzerCategory GetAnalyzerCategory(); - - public bool OpenFileOnly(SimplifierOptions? options) - => false; - - protected static DiagnosticDescriptor CreateDescriptor( - string id, - EnforceOnBuild enforceOnBuild, - LocalizableString title, - LocalizableString messageFormat, - bool hasAnyCodeStyleOption, - bool isUnnecessary, - bool isEnabledByDefault = true, - bool isConfigurable = true, - LocalizableString? description = null) + SupportedDiagnostics = descriptors; + _generatedCodeAnalysisFlags = generatedCodeAnalysisFlags; + } + + public bool IsHighPriority => false; + public sealed override ImmutableArray SupportedDiagnostics { get; } + + public sealed override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(_generatedCodeAnalysisFlags); + context.EnableConcurrentExecution(); + + InitializeWorker(context); + } + + protected abstract void InitializeWorker(AnalysisContext context); + + public abstract DiagnosticAnalyzerCategory GetAnalyzerCategory(); + + public bool OpenFileOnly(SimplifierOptions? options) + => false; + + protected static DiagnosticDescriptor CreateDescriptor( + string id, + EnforceOnBuild enforceOnBuild, + LocalizableString title, + LocalizableString messageFormat, + bool hasAnyCodeStyleOption, + bool isUnnecessary, + bool isEnabledByDefault = true, + bool isConfigurable = true, + LocalizableString? description = null) #pragma warning disable RS0030 // Do not use banned APIs - => new( - id, title, messageFormat, - DiagnosticCategory.CodeQuality, - DiagnosticSeverity.Info, - isEnabledByDefault, - description, - helpLinkUri: DiagnosticHelper.GetHelpLinkForDiagnosticId(id), - customTags: DiagnosticCustomTags.Create(isUnnecessary, isConfigurable, isCustomConfigurable: hasAnyCodeStyleOption, enforceOnBuild)); + => new( + id, title, messageFormat, + DiagnosticCategory.CodeQuality, + DiagnosticSeverity.Info, + isEnabledByDefault, + description, + helpLinkUri: DiagnosticHelper.GetHelpLinkForDiagnosticId(id), + customTags: DiagnosticCustomTags.Create(isUnnecessary, isConfigurable, isCustomConfigurable: hasAnyCodeStyleOption, enforceOnBuild)); #pragma warning restore RS0030 // Do not use banned APIs - } } diff --git a/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/AbstractAddAccessibilityModifiers.cs b/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/AbstractAddAccessibilityModifiers.cs index f6667a1abe0d5..334f0d2197a5a 100644 --- a/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/AbstractAddAccessibilityModifiers.cs +++ b/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/AbstractAddAccessibilityModifiers.cs @@ -5,29 +5,28 @@ using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers +namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers; + +internal abstract class AbstractAddAccessibilityModifiers : IAddAccessibilityModifiers + where TMemberDeclarationSyntax : SyntaxNode { - internal abstract class AbstractAddAccessibilityModifiers : IAddAccessibilityModifiers - where TMemberDeclarationSyntax : SyntaxNode + public bool ShouldUpdateAccessibilityModifier( + IAccessibilityFacts accessibilityFacts, + SyntaxNode member, + AccessibilityModifiersRequired option, + out SyntaxToken name, + out bool modifierAdded) { - public bool ShouldUpdateAccessibilityModifier( - IAccessibilityFacts accessibilityFacts, - SyntaxNode member, - AccessibilityModifiersRequired option, - out SyntaxToken name, - out bool modifierAdded) - { - name = default; - modifierAdded = false; - return member is TMemberDeclarationSyntax memberDecl && - ShouldUpdateAccessibilityModifier(accessibilityFacts, memberDecl, option, out name, out modifierAdded); - } - - public abstract bool ShouldUpdateAccessibilityModifier( - IAccessibilityFacts accessibilityFacts, - TMemberDeclarationSyntax member, - AccessibilityModifiersRequired option, - out SyntaxToken name, - out bool modifierAdded); + name = default; + modifierAdded = false; + return member is TMemberDeclarationSyntax memberDecl && + ShouldUpdateAccessibilityModifier(accessibilityFacts, memberDecl, option, out name, out modifierAdded); } + + public abstract bool ShouldUpdateAccessibilityModifier( + IAccessibilityFacts accessibilityFacts, + TMemberDeclarationSyntax member, + AccessibilityModifiersRequired option, + out SyntaxToken name, + out bool modifierAdded); } diff --git a/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/AbstractAddAccessibilityModifiersDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/AbstractAddAccessibilityModifiersDiagnosticAnalyzer.cs index 563c527a54539..b68865e3e20b6 100644 --- a/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/AbstractAddAccessibilityModifiersDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/AbstractAddAccessibilityModifiersDiagnosticAnalyzer.cs @@ -6,43 +6,42 @@ using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers +namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers; + +internal abstract class AbstractAddAccessibilityModifiersDiagnosticAnalyzer + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TCompilationUnitSyntax : SyntaxNode { - internal abstract class AbstractAddAccessibilityModifiersDiagnosticAnalyzer - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TCompilationUnitSyntax : SyntaxNode + protected static readonly ImmutableDictionary ModifiersAddedProperties = ImmutableDictionary.Empty.Add( + AddAccessibilityModifiersConstants.ModifiersAdded, AddAccessibilityModifiersConstants.ModifiersAdded); + + protected AbstractAddAccessibilityModifiersDiagnosticAnalyzer() + : base(IDEDiagnosticIds.AddAccessibilityModifiersDiagnosticId, + EnforceOnBuildValues.AddAccessibilityModifiers, + CodeStyleOptions2.AccessibilityModifiersRequired, + new LocalizableResourceString(nameof(AnalyzersResources.Add_accessibility_modifiers), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Accessibility_modifiers_required), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - protected static readonly ImmutableDictionary ModifiersAddedProperties = ImmutableDictionary.Empty.Add( - AddAccessibilityModifiersConstants.ModifiersAdded, AddAccessibilityModifiersConstants.ModifiersAdded); - - protected AbstractAddAccessibilityModifiersDiagnosticAnalyzer() - : base(IDEDiagnosticIds.AddAccessibilityModifiersDiagnosticId, - EnforceOnBuildValues.AddAccessibilityModifiers, - CodeStyleOptions2.AccessibilityModifiersRequired, - new LocalizableResourceString(nameof(AnalyzersResources.Add_accessibility_modifiers), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Accessibility_modifiers_required), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + } - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; - protected sealed override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); + protected sealed override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); - private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + { + var option = context.GetAnalyzerOptions().RequireAccessibilityModifiers; + if (option.Value == AccessibilityModifiersRequired.Never + || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) { - var option = context.GetAnalyzerOptions().RequireAccessibilityModifiers; - if (option.Value == AccessibilityModifiersRequired.Never - || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) - { - return; - } - - ProcessCompilationUnit(context, option, (TCompilationUnitSyntax)context.Tree.GetRoot(context.CancellationToken)); + return; } - protected abstract void ProcessCompilationUnit(SyntaxTreeAnalysisContext context, CodeStyleOption2 option, TCompilationUnitSyntax compilationUnitSyntax); + ProcessCompilationUnit(context, option, (TCompilationUnitSyntax)context.Tree.GetRoot(context.CancellationToken)); } + + protected abstract void ProcessCompilationUnit(SyntaxTreeAnalysisContext context, CodeStyleOption2 option, TCompilationUnitSyntax compilationUnitSyntax); } diff --git a/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/IAddAccessibilityModifiersService.cs b/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/IAddAccessibilityModifiersService.cs index db6d08786c238..d3847ccc77d66 100644 --- a/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/IAddAccessibilityModifiersService.cs +++ b/src/Analyzers/Core/Analyzers/AddAccessibilityModifiers/IAddAccessibilityModifiersService.cs @@ -5,20 +5,19 @@ using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers +namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers; + +internal static partial class AddAccessibilityModifiersConstants { - internal static partial class AddAccessibilityModifiersConstants - { - public const string ModifiersAdded = nameof(ModifiersAdded); - } + public const string ModifiersAdded = nameof(ModifiersAdded); +} - internal interface IAddAccessibilityModifiers - { - bool ShouldUpdateAccessibilityModifier( - IAccessibilityFacts accessibilityFacts, - SyntaxNode member, - AccessibilityModifiersRequired option, - out SyntaxToken name, - out bool modifiersAdded); - } +internal interface IAddAccessibilityModifiers +{ + bool ShouldUpdateAccessibilityModifier( + IAccessibilityFacts accessibilityFacts, + SyntaxNode member, + AccessibilityModifiersRequired option, + out SyntaxToken name, + out bool modifiersAdded); } diff --git a/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AbstractAddRequiredParenthesesDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AbstractAddRequiredParenthesesDiagnosticAnalyzer.cs index 544da68cf6351..7fd9f9a64c66e 100644 --- a/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AbstractAddRequiredParenthesesDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AbstractAddRequiredParenthesesDiagnosticAnalyzer.cs @@ -12,151 +12,151 @@ using Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddRequiredParentheses +namespace Microsoft.CodeAnalysis.AddRequiredParentheses; + +internal abstract class AbstractAddRequiredParenthesesDiagnosticAnalyzer< + TExpressionSyntax, TBinaryLikeExpressionSyntax, TLanguageKindEnum> + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TExpressionSyntax : SyntaxNode + where TBinaryLikeExpressionSyntax : TExpressionSyntax + where TLanguageKindEnum : struct { - internal abstract class AbstractAddRequiredParenthesesDiagnosticAnalyzer< - TExpressionSyntax, TBinaryLikeExpressionSyntax, TLanguageKindEnum> - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TExpressionSyntax : SyntaxNode - where TBinaryLikeExpressionSyntax : TExpressionSyntax - where TLanguageKindEnum : struct - { - private static readonly Dictionary<(bool includeInFixAll, string equivalenceKey), ImmutableDictionary> s_cachedProperties = []; + private static readonly Dictionary<(bool includeInFixAll, string equivalenceKey), ImmutableDictionary> s_cachedProperties = []; - private readonly IPrecedenceService _precedenceService; + private readonly IPrecedenceService _precedenceService; - static AbstractAddRequiredParenthesesDiagnosticAnalyzer() - { - var includeArray = new[] { false, true }; + static AbstractAddRequiredParenthesesDiagnosticAnalyzer() + { + var includeArray = new[] { false, true }; - foreach (var equivalenceKey in GetAllEquivalenceKeys()) + foreach (var equivalenceKey in GetAllEquivalenceKeys()) + { + foreach (var includeInFixAll in includeArray) { - foreach (var includeInFixAll in includeArray) + var properties = ImmutableDictionary.Empty; + if (includeInFixAll) { - var properties = ImmutableDictionary.Empty; - if (includeInFixAll) - { - properties = properties.Add(AddRequiredParenthesesConstants.IncludeInFixAll, ""); - } - - properties = properties.Add(AddRequiredParenthesesConstants.EquivalenceKey, equivalenceKey); - s_cachedProperties.Add((includeInFixAll, equivalenceKey), properties); + properties = properties.Add(AddRequiredParenthesesConstants.IncludeInFixAll, ""); } + + properties = properties.Add(AddRequiredParenthesesConstants.EquivalenceKey, equivalenceKey); + s_cachedProperties.Add((includeInFixAll, equivalenceKey), properties); } } + } - protected static string GetEquivalenceKey(PrecedenceKind precedenceKind) - => precedenceKind switch - { - PrecedenceKind.Arithmetic or PrecedenceKind.Shift or PrecedenceKind.Bitwise => "ArithmeticBinary", - PrecedenceKind.Relational or PrecedenceKind.Equality => "RelationalBinary", - PrecedenceKind.Logical or PrecedenceKind.Coalesce => "OtherBinary", - PrecedenceKind.Other => "Other", - _ => throw ExceptionUtilities.UnexpectedValue(precedenceKind), - }; - - protected static ImmutableArray GetAllEquivalenceKeys() - => ["ArithmeticBinary", "RelationalBinary", "OtherBinary", "Other"]; - - private static ImmutableDictionary GetProperties(bool includeInFixAll, string equivalenceKey) - => s_cachedProperties[(includeInFixAll, equivalenceKey)]; - - protected abstract int GetPrecedence(TBinaryLikeExpressionSyntax binaryLike); - protected abstract TExpressionSyntax? TryGetAppropriateParent(TBinaryLikeExpressionSyntax binaryLike); - protected abstract bool IsBinaryLike(TExpressionSyntax node); - protected abstract (TExpressionSyntax, SyntaxToken, TExpressionSyntax) GetPartsOfBinaryLike(TBinaryLikeExpressionSyntax binaryLike); - - protected AbstractAddRequiredParenthesesDiagnosticAnalyzer(IPrecedenceService precedenceService) - : base(IDEDiagnosticIds.AddRequiredParenthesesDiagnosticId, - EnforceOnBuildValues.AddRequiredParentheses, - options: ParenthesesDiagnosticAnalyzersHelper.Options, - new LocalizableResourceString(nameof(AnalyzersResources.Add_parentheses_for_clarity), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Parentheses_should_be_added_for_clarity), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) + protected static string GetEquivalenceKey(PrecedenceKind precedenceKind) + => precedenceKind switch { - _precedenceService = precedenceService; - } + PrecedenceKind.Arithmetic or PrecedenceKind.Shift or PrecedenceKind.Bitwise => "ArithmeticBinary", + PrecedenceKind.Relational or PrecedenceKind.Equality => "RelationalBinary", + PrecedenceKind.Logical or PrecedenceKind.Coalesce => "OtherBinary", + PrecedenceKind.Other => "Other", + _ => throw ExceptionUtilities.UnexpectedValue(precedenceKind), + }; + + protected static ImmutableArray GetAllEquivalenceKeys() + => ["ArithmeticBinary", "RelationalBinary", "OtherBinary", "Other"]; + + private static ImmutableDictionary GetProperties(bool includeInFixAll, string equivalenceKey) + => s_cachedProperties[(includeInFixAll, equivalenceKey)]; + + protected abstract int GetPrecedence(TBinaryLikeExpressionSyntax binaryLike); + protected abstract TExpressionSyntax? TryGetAppropriateParent(TBinaryLikeExpressionSyntax binaryLike); + protected abstract bool IsBinaryLike(TExpressionSyntax node); + protected abstract (TExpressionSyntax, SyntaxToken, TExpressionSyntax) GetPartsOfBinaryLike(TBinaryLikeExpressionSyntax binaryLike); + + protected AbstractAddRequiredParenthesesDiagnosticAnalyzer(IPrecedenceService precedenceService) + : base(IDEDiagnosticIds.AddRequiredParenthesesDiagnosticId, + EnforceOnBuildValues.AddRequiredParentheses, + options: ParenthesesDiagnosticAnalyzersHelper.Options, + new LocalizableResourceString(nameof(AnalyzersResources.Add_parentheses_for_clarity), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Parentheses_should_be_added_for_clarity), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) + { + _precedenceService = precedenceService; + } - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected sealed override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeSyntax, GetSyntaxNodeKinds()); + protected sealed override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeSyntax, GetSyntaxNodeKinds()); - protected abstract ImmutableArray GetSyntaxNodeKinds(); + protected abstract ImmutableArray GetSyntaxNodeKinds(); - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var binaryLike = (TBinaryLikeExpressionSyntax)context.Node; + var parent = TryGetAppropriateParent(binaryLike); + if (parent == null || !IsBinaryLike(parent)) { - var binaryLike = (TBinaryLikeExpressionSyntax)context.Node; - var parent = TryGetAppropriateParent(binaryLike); - if (parent == null || !IsBinaryLike(parent)) - { - return; - } + return; + } - var parentBinaryLike = (TBinaryLikeExpressionSyntax)parent; - if (GetPrecedence(binaryLike) == GetPrecedence(parentBinaryLike)) - { - return; - } + var parentBinaryLike = (TBinaryLikeExpressionSyntax)parent; + if (GetPrecedence(binaryLike) == GetPrecedence(parentBinaryLike)) + { + return; + } - var options = context.GetAnalyzerOptions(); - var childPrecedenceKind = _precedenceService.GetPrecedenceKind(binaryLike); - var parentPrecedenceKind = _precedenceService.GetPrecedenceKind(parentBinaryLike); + var options = context.GetAnalyzerOptions(); + var childPrecedenceKind = _precedenceService.GetPrecedenceKind(binaryLike); + var parentPrecedenceKind = _precedenceService.GetPrecedenceKind(parentBinaryLike); - var childEquivalenceKey = GetEquivalenceKey(childPrecedenceKind); - var parentEquivalenceKey = GetEquivalenceKey(parentPrecedenceKind); + var childEquivalenceKey = GetEquivalenceKey(childPrecedenceKind); + var parentEquivalenceKey = GetEquivalenceKey(parentPrecedenceKind); - // only add parentheses within the same precedence band. - if (childEquivalenceKey != parentEquivalenceKey) - { - return; - } + // only add parentheses within the same precedence band. + if (childEquivalenceKey != parentEquivalenceKey) + { + return; + } - var preference = ParenthesesDiagnosticAnalyzersHelper.GetLanguageOption(options, childPrecedenceKind); - if (preference.Value != ParenthesesPreference.AlwaysForClarity - || ShouldSkipAnalysis(context, preference.Notification)) - { - return; - } + var preference = ParenthesesDiagnosticAnalyzersHelper.GetLanguageOption(options, childPrecedenceKind); + if (preference.Value != ParenthesesPreference.AlwaysForClarity + || ShouldSkipAnalysis(context, preference.Notification)) + { + return; + } - var additionalLocations = ImmutableArray.Create(binaryLike.GetLocation()); - var precedence = GetPrecedence(binaryLike); + var additionalLocations = ImmutableArray.Create(binaryLike.GetLocation()); + var precedence = GetPrecedence(binaryLike); - // In a case like "a + b * c * d", we'll add parens to make "a + (b * c * d)". - // To make this user experience more pleasant, we will place the diagnostic on - // both *'s. - AddDiagnostics( - context, binaryLike, precedence, preference.Notification, - additionalLocations, childEquivalenceKey, includeInFixAll: true); - } + // In a case like "a + b * c * d", we'll add parens to make "a + (b * c * d)". + // To make this user experience more pleasant, we will place the diagnostic on + // both *'s. + AddDiagnostics( + context, binaryLike, precedence, preference.Notification, + additionalLocations, childEquivalenceKey, includeInFixAll: true); + } - private void AddDiagnostics( - SyntaxNodeAnalysisContext context, TBinaryLikeExpressionSyntax? binaryLikeOpt, int precedence, - NotificationOption2 notificationOption, ImmutableArray additionalLocations, - string equivalenceKey, bool includeInFixAll) + private void AddDiagnostics( + SyntaxNodeAnalysisContext context, TBinaryLikeExpressionSyntax? binaryLikeOpt, int precedence, + NotificationOption2 notificationOption, ImmutableArray additionalLocations, + string equivalenceKey, bool includeInFixAll) + { + if (binaryLikeOpt != null && + IsBinaryLike(binaryLikeOpt) && + GetPrecedence(binaryLikeOpt) == precedence) { - if (binaryLikeOpt != null && - IsBinaryLike(binaryLikeOpt) && - GetPrecedence(binaryLikeOpt) == precedence) - { - var (left, operatorToken, right) = GetPartsOfBinaryLike(binaryLikeOpt); - - var properties = GetProperties(includeInFixAll, equivalenceKey); - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - operatorToken.GetLocation(), - notificationOption, - additionalLocations, - properties)); - - // We're adding diagnostics for all subcomponents so that the user can get the - // lightbulb on any of the operator tokens. However, we don't actually want to - // 'fix' all of these if the user does a fix-all. if we did, we'd end up adding far - // too many parens to the same expr. - AddDiagnostics(context, left as TBinaryLikeExpressionSyntax, precedence, notificationOption, additionalLocations, equivalenceKey, includeInFixAll: false); - AddDiagnostics(context, right as TBinaryLikeExpressionSyntax, precedence, notificationOption, additionalLocations, equivalenceKey, includeInFixAll: false); - } + var (left, operatorToken, right) = GetPartsOfBinaryLike(binaryLikeOpt); + + var properties = GetProperties(includeInFixAll, equivalenceKey); + + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + operatorToken.GetLocation(), + notificationOption, + context.Options, + additionalLocations, + properties)); + + // We're adding diagnostics for all subcomponents so that the user can get the + // lightbulb on any of the operator tokens. However, we don't actually want to + // 'fix' all of these if the user does a fix-all. if we did, we'd end up adding far + // too many parens to the same expr. + AddDiagnostics(context, left as TBinaryLikeExpressionSyntax, precedence, notificationOption, additionalLocations, equivalenceKey, includeInFixAll: false); + AddDiagnostics(context, right as TBinaryLikeExpressionSyntax, precedence, notificationOption, additionalLocations, equivalenceKey, includeInFixAll: false); } } } diff --git a/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AddRequiredParenthesesConstants.cs b/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AddRequiredParenthesesConstants.cs index a40c765ed1170..44e34bfb74ecd 100644 --- a/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AddRequiredParenthesesConstants.cs +++ b/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AddRequiredParenthesesConstants.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.AddRequiredParentheses +namespace Microsoft.CodeAnalysis.AddRequiredParentheses; + +internal static class AddRequiredParenthesesConstants { - internal static class AddRequiredParenthesesConstants - { - public const string EquivalenceKey = nameof(EquivalenceKey); - public const string IncludeInFixAll = nameof(IncludeInFixAll); - } + public const string EquivalenceKey = nameof(EquivalenceKey); + public const string IncludeInFixAll = nameof(IncludeInFixAll); } diff --git a/src/Analyzers/Core/Analyzers/ConvertTypeofToNameof/AbstractConvertTypeOfToNameOfDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/ConvertTypeofToNameof/AbstractConvertTypeOfToNameOfDiagnosticAnalyzer.cs index ec0d2787d2396..9cd2d87b87ed9 100644 --- a/src/Analyzers/Core/Analyzers/ConvertTypeofToNameof/AbstractConvertTypeOfToNameOfDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/ConvertTypeofToNameof/AbstractConvertTypeOfToNameOfDiagnosticAnalyzer.cs @@ -6,77 +6,76 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.ConvertTypeOfToNameOf +namespace Microsoft.CodeAnalysis.ConvertTypeOfToNameOf; + +internal abstract class AbstractConvertTypeOfToNameOfDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - internal abstract class AbstractConvertTypeOfToNameOfDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + protected AbstractConvertTypeOfToNameOfDiagnosticAnalyzer(LocalizableString title) + : base(diagnosticId: IDEDiagnosticIds.ConvertTypeOfToNameOfDiagnosticId, + EnforceOnBuildValues.ConvertTypeOfToNameOf, + option: null, + title: title) { - protected AbstractConvertTypeOfToNameOfDiagnosticAnalyzer(LocalizableString title) - : base(diagnosticId: IDEDiagnosticIds.ConvertTypeOfToNameOfDiagnosticId, - EnforceOnBuildValues.ConvertTypeOfToNameOf, - option: null, - title: title) - { - } + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + protected abstract bool IsValidTypeofAction(OperationAnalysisContext context); - protected abstract bool IsValidTypeofAction(OperationAnalysisContext context); + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterOperationAction(AnalyzeAction, OperationKind.TypeOf); + } - protected override void InitializeWorker(AnalysisContext context) + protected void AnalyzeAction(OperationAnalysisContext context) + { + if (ShouldSkipAnalysis(context, notification: null)) + return; + + if (!IsValidTypeofAction(context) || !IsValidOperation(context.Operation)) { - context.RegisterOperationAction(AnalyzeAction, OperationKind.TypeOf); + return; } - protected void AnalyzeAction(OperationAnalysisContext context) + var node = context.Operation.Syntax; + var parent = node.Parent; + // If the parent node is null then it cannot be a member access, so do not report a diagnostic + if (parent is null) { - if (ShouldSkipAnalysis(context, notification: null)) - return; - - if (!IsValidTypeofAction(context) || !IsValidOperation(context.Operation)) - { - return; - } + return; + } - var node = context.Operation.Syntax; - var parent = node.Parent; - // If the parent node is null then it cannot be a member access, so do not report a diagnostic - if (parent is null) - { - return; - } + var location = parent.GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); - var location = parent.GetLocation(); - context.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); + } + private static bool IsValidOperation(IOperation operation) + { + // Cast to a typeof operation & check parent is a property reference and member access + var typeofOperation = (ITypeOfOperation)operation; + if (operation.Parent is not IPropertyReferenceOperation) + { + return false; } - private static bool IsValidOperation(IOperation operation) + // Check Parent is a .Name access + var operationParent = (IPropertyReferenceOperation)operation.Parent; + var parentProperty = operationParent.Property.Name; + if (parentProperty is not nameof(System.Type.Name)) { - // Cast to a typeof operation & check parent is a property reference and member access - var typeofOperation = (ITypeOfOperation)operation; - if (operation.Parent is not IPropertyReferenceOperation) - { - return false; - } - - // Check Parent is a .Name access - var operationParent = (IPropertyReferenceOperation)operation.Parent; - var parentProperty = operationParent.Property.Name; - if (parentProperty is not nameof(System.Type.Name)) - { - return false; - } - - // If it's a generic type, do not offer the fix because nameof(T) and typeof(T).Name are not - // semantically equivalent, the resulting string is formatted differently, where typeof(T).Name - // return "T`1" and nameof just returns "T" - if (typeofOperation.TypeOperand is IErrorTypeSymbol) - { - return false; - } + return false; + } - return typeofOperation.TypeOperand is INamedTypeSymbol namedType && namedType.TypeArguments.Length == 0; + // If it's a generic type, do not offer the fix because nameof(T) and typeof(T).Name are not + // semantically equivalent, the resulting string is formatted differently, where typeof(T).Name + // return "T`1" and nameof just returns "T" + if (typeofOperation.TypeOperand is IErrorTypeSymbol) + { + return false; } + + return typeofOperation.TypeOperand is INamedTypeSymbol namedType && namedType.TypeArguments.Length == 0; } } diff --git a/src/Analyzers/Core/Analyzers/DiagnosticCustomTags.cs b/src/Analyzers/Core/Analyzers/DiagnosticCustomTags.cs index b2603bd214f6b..05c5275fa1f7f 100644 --- a/src/Analyzers/Core/Analyzers/DiagnosticCustomTags.cs +++ b/src/Analyzers/Core/Analyzers/DiagnosticCustomTags.cs @@ -8,98 +8,97 @@ using System.Diagnostics; using Microsoft.CodeAnalysis.CodeStyle; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal static class DiagnosticCustomTags { - internal static class DiagnosticCustomTags - { - private static readonly string s_enforceOnBuildNeverTag = EnforceOnBuild.Never.ToCustomTag(); + private static readonly string s_enforceOnBuildNeverTag = EnforceOnBuild.Never.ToCustomTag(); - private static readonly string[] s_microsoftCustomTags = [WellKnownDiagnosticTags.Telemetry]; - private static readonly string[] s_editAndContinueCustomTags = [WellKnownDiagnosticTags.EditAndContinue, WellKnownDiagnosticTags.Telemetry, WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag]; - private static readonly string[] s_unnecessaryCustomTags = [WellKnownDiagnosticTags.Unnecessary, WellKnownDiagnosticTags.Telemetry]; - private static readonly string[] s_notConfigurableCustomTags = [WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag, WellKnownDiagnosticTags.Telemetry]; - private static readonly string[] s_unnecessaryAndNotConfigurableCustomTags = [WellKnownDiagnosticTags.Unnecessary, WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag, WellKnownDiagnosticTags.Telemetry]; + private static readonly string[] s_microsoftCustomTags = [WellKnownDiagnosticTags.Telemetry]; + private static readonly string[] s_editAndContinueCustomTags = [WellKnownDiagnosticTags.EditAndContinue, WellKnownDiagnosticTags.Telemetry, WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag]; + private static readonly string[] s_unnecessaryCustomTags = [WellKnownDiagnosticTags.Unnecessary, WellKnownDiagnosticTags.Telemetry]; + private static readonly string[] s_notConfigurableCustomTags = [WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag, WellKnownDiagnosticTags.Telemetry]; + private static readonly string[] s_unnecessaryAndNotConfigurableCustomTags = [WellKnownDiagnosticTags.Unnecessary, WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag, WellKnownDiagnosticTags.Telemetry]; - public static string[] Microsoft + public static string[] Microsoft + { + get { - get - { - Assert(s_microsoftCustomTags, WellKnownDiagnosticTags.Telemetry); - return s_microsoftCustomTags; - } + Assert(s_microsoftCustomTags, WellKnownDiagnosticTags.Telemetry); + return s_microsoftCustomTags; } + } - public static string[] EditAndContinue + public static string[] EditAndContinue + { + get { - get - { - Assert(s_editAndContinueCustomTags, WellKnownDiagnosticTags.EditAndContinue, WellKnownDiagnosticTags.Telemetry, WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag); - return s_editAndContinueCustomTags; - } + Assert(s_editAndContinueCustomTags, WellKnownDiagnosticTags.EditAndContinue, WellKnownDiagnosticTags.Telemetry, WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag); + return s_editAndContinueCustomTags; } + } - public static string[] Unnecessary + public static string[] Unnecessary + { + get { - get - { - Assert(s_unnecessaryCustomTags, WellKnownDiagnosticTags.Unnecessary, WellKnownDiagnosticTags.Telemetry); - return s_unnecessaryCustomTags; - } + Assert(s_unnecessaryCustomTags, WellKnownDiagnosticTags.Unnecessary, WellKnownDiagnosticTags.Telemetry); + return s_unnecessaryCustomTags; } + } - public static string[] NotConfigurable + public static string[] NotConfigurable + { + get { - get - { - Assert(s_notConfigurableCustomTags, WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag, WellKnownDiagnosticTags.Telemetry); - return s_notConfigurableCustomTags; - } + Assert(s_notConfigurableCustomTags, WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag, WellKnownDiagnosticTags.Telemetry); + return s_notConfigurableCustomTags; } + } - public static string[] UnnecessaryAndNotConfigurable + public static string[] UnnecessaryAndNotConfigurable + { + get { - get - { - Assert(s_unnecessaryAndNotConfigurableCustomTags, WellKnownDiagnosticTags.Unnecessary, WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag, WellKnownDiagnosticTags.Telemetry); - return s_unnecessaryAndNotConfigurableCustomTags; - } + Assert(s_unnecessaryAndNotConfigurableCustomTags, WellKnownDiagnosticTags.Unnecessary, WellKnownDiagnosticTags.NotConfigurable, s_enforceOnBuildNeverTag, WellKnownDiagnosticTags.Telemetry); + return s_unnecessaryAndNotConfigurableCustomTags; } + } - [Conditional("DEBUG")] - private static void Assert(string[] customTags, params string[] tags) - { - Debug.Assert(customTags.Length == tags.Length); - - for (var i = 0; i < tags.Length; i++) - { - Debug.Assert(customTags[i] == tags[i]); - } - } + [Conditional("DEBUG")] + private static void Assert(string[] customTags, params string[] tags) + { + Debug.Assert(customTags.Length == tags.Length); - internal static string[] Create(bool isUnnecessary, bool isConfigurable, bool isCustomConfigurable, EnforceOnBuild enforceOnBuild) + for (var i = 0; i < tags.Length; i++) { - Debug.Assert(isConfigurable || enforceOnBuild == EnforceOnBuild.Never); + Debug.Assert(customTags[i] == tags[i]); + } + } - var customTagsBuilder = ImmutableArray.CreateBuilder(); - customTagsBuilder.AddRange(Microsoft); + internal static string[] Create(bool isUnnecessary, bool isConfigurable, bool isCustomConfigurable, EnforceOnBuild enforceOnBuild) + { + Debug.Assert(isConfigurable || enforceOnBuild == EnforceOnBuild.Never); - customTagsBuilder.Add(enforceOnBuild.ToCustomTag()); + var customTagsBuilder = ImmutableArray.CreateBuilder(); + customTagsBuilder.AddRange(Microsoft); - if (!isConfigurable) - { - customTagsBuilder.Add(WellKnownDiagnosticTags.NotConfigurable); - } - else if (isCustomConfigurable) - { - customTagsBuilder.Add(WellKnownDiagnosticTags.CustomSeverityConfigurable); - } + customTagsBuilder.Add(enforceOnBuild.ToCustomTag()); - if (isUnnecessary) - { - customTagsBuilder.Add(WellKnownDiagnosticTags.Unnecessary); - } + if (!isConfigurable) + { + customTagsBuilder.Add(WellKnownDiagnosticTags.NotConfigurable); + } + else if (isCustomConfigurable) + { + customTagsBuilder.Add(WellKnownDiagnosticTags.CustomSeverityConfigurable); + } - return customTagsBuilder.ToArray(); + if (isUnnecessary) + { + customTagsBuilder.Add(WellKnownDiagnosticTags.Unnecessary); } + + return customTagsBuilder.ToArray(); } } diff --git a/src/Analyzers/Core/Analyzers/EnforceOnBuild.cs b/src/Analyzers/Core/Analyzers/EnforceOnBuild.cs index 8845265223587..8bbde74962a2b 100644 --- a/src/Analyzers/Core/Analyzers/EnforceOnBuild.cs +++ b/src/Analyzers/Core/Analyzers/EnforceOnBuild.cs @@ -2,41 +2,40 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.CodeStyle +namespace Microsoft.CodeAnalysis.CodeStyle; + +/// +/// Build enforcement recommendation for a code style analyzer. +/// +internal enum EnforceOnBuild { /// - /// Build enforcement recommendation for a code style analyzer. + /// Indicates that the code style diagnostic is an IDE-only diagnostic that cannot be enforced on build. /// - internal enum EnforceOnBuild - { - /// - /// Indicates that the code style diagnostic is an IDE-only diagnostic that cannot be enforced on build. - /// - Never, + Never, - /// - /// Indicates that the code style diagnostic can be enforced on build when explicitly enabled in a configuration file, - /// but is not part of the or group for build enforcement. - /// This is the suggested P3 bucket of code style diagnostics to enforce on build. - /// - WhenExplicitlyEnabled, + /// + /// Indicates that the code style diagnostic can be enforced on build when explicitly enabled in a configuration file, + /// but is not part of the or group for build enforcement. + /// This is the suggested P3 bucket of code style diagnostics to enforce on build. + /// + WhenExplicitlyEnabled, - /// - /// Indicates that the code style diagnostic can be enforced on build and is part of the recommended group for build enforcement. - /// This is the suggested P2 bucket of code style diagnostics to enforce on build. - /// - Recommended, + /// + /// Indicates that the code style diagnostic can be enforced on build and is part of the recommended group for build enforcement. + /// This is the suggested P2 bucket of code style diagnostics to enforce on build. + /// + Recommended, - /// - /// Indicates that the code style diagnostic can be enforced on build and is part of the highly recommended group for build enforcement. - /// This is the suggested P1 bucket of code style diagnostics to enforce on build. - /// - HighlyRecommended, - } + /// + /// Indicates that the code style diagnostic can be enforced on build and is part of the highly recommended group for build enforcement. + /// This is the suggested P1 bucket of code style diagnostics to enforce on build. + /// + HighlyRecommended, +} - internal static class EnforceOnBuildExtensions - { - public static string ToCustomTag(this EnforceOnBuild enforceOnBuild) - => $"{nameof(EnforceOnBuild)}_{enforceOnBuild}"; - } +internal static class EnforceOnBuildExtensions +{ + public static string ToCustomTag(this EnforceOnBuild enforceOnBuild) + => $"{nameof(EnforceOnBuild)}_{enforceOnBuild}"; } diff --git a/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs b/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs index b0feec48f47cc..65cd9ad9ee9e1 100644 --- a/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs +++ b/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs @@ -4,138 +4,137 @@ using Microsoft.CodeAnalysis.CodeStyle; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal static class EnforceOnBuildValues { - internal static class EnforceOnBuildValues - { - /* EnforceOnBuild.HighlyRecommended */ - public const EnforceOnBuild RemoveUnnecessaryImports = /*IDE0005*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild UseImplicitType = /*IDE0007*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild UseExplicitType = /*IDE0008*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild AddBraces = /*IDE0011*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild OrderModifiers = /*IDE0036*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild AddAccessibilityModifiers = /*IDE0040*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild ValidateFormatString = /*IDE0043*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild MakeFieldReadonly = /*IDE0044*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild RemoveUnusedMembers = /*IDE0051*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild RemoveUnreadMembers = /*IDE0052*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild Formatting = /*IDE0055*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild ValueAssignedIsUnused = /*IDE0059*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild UnusedParameter = /*IDE0060*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild FileHeaderMismatch = /*IDE0073*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild InvalidSuppressMessageAttribute = /*IDE0076*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild LegacyFormatSuppressMessageAttribute = /*IDE0077*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild RemoveConfusingSuppressionForIsExpression = /*IDE0080*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild UseBlockScopedNamespace = /*IDE0160*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild UseFileScopedNamespace = /*IDE0161*/ EnforceOnBuild.HighlyRecommended; - public const EnforceOnBuild UseTupleSwap = /*IDE0180*/ EnforceOnBuild.HighlyRecommended; + /* EnforceOnBuild.HighlyRecommended */ + public const EnforceOnBuild RemoveUnnecessaryImports = /*IDE0005*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild UseImplicitType = /*IDE0007*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild UseExplicitType = /*IDE0008*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild AddBraces = /*IDE0011*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild OrderModifiers = /*IDE0036*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild AddAccessibilityModifiers = /*IDE0040*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild ValidateFormatString = /*IDE0043*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild MakeFieldReadonly = /*IDE0044*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild RemoveUnusedMembers = /*IDE0051*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild RemoveUnreadMembers = /*IDE0052*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild Formatting = /*IDE0055*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild ValueAssignedIsUnused = /*IDE0059*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild UnusedParameter = /*IDE0060*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild FileHeaderMismatch = /*IDE0073*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild InvalidSuppressMessageAttribute = /*IDE0076*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild LegacyFormatSuppressMessageAttribute = /*IDE0077*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild RemoveConfusingSuppressionForIsExpression = /*IDE0080*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild UseBlockScopedNamespace = /*IDE0160*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild UseFileScopedNamespace = /*IDE0161*/ EnforceOnBuild.HighlyRecommended; + public const EnforceOnBuild UseTupleSwap = /*IDE0180*/ EnforceOnBuild.HighlyRecommended; - /* EnforceOnBuild.Recommended */ - public const EnforceOnBuild UseThrowExpression = /*IDE0016*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseObjectInitializer = /*IDE0017*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild InlineDeclaration = /*IDE0018*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild InlineAsType = /*IDE0019*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild InlineIsType = /*IDE0020*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseExpressionBodyForConstructors = /*IDE0021*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseExpressionBodyForMethods = /*IDE0022*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseExpressionBodyForConversionOperators = /*IDE0023*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseExpressionBodyForOperators = /*IDE0024*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseExpressionBodyForProperties = /*IDE0025*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseExpressionBodyForIndexers = /*IDE0026*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseExpressionBodyForAccessors = /*IDE0027*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCollectionInitializer = /*IDE0028*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCoalesceExpression = /*IDE0029*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCoalesceExpressionForNullable = /*IDE0030*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseNullPropagation = /*IDE0031*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseAutoProperty = /*IDE0032*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseExplicitTupleName = /*IDE0033*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseDefaultLiteral = /*IDE0034*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild InlineIsTypeWithoutName = /*IDE0038*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseLocalFunction = /*IDE0039*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseDeconstruction = /*IDE0042*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseConditionalExpressionForAssignment = /*IDE0045*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseConditionalExpressionForReturn = /*IDE0046*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild RemoveUnnecessaryParentheses = /*IDE0047*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseExpressionBodyForLambdaExpressions = /*IDE0053*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCompoundAssignment = /*IDE0054*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseIndexOperator = /*IDE0056*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseRangeOperator = /*IDE0057*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseExpressionBodyForLocalFunctions = /*IDE0061*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild MakeLocalFunctionStatic = /*IDE0062*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseSimpleUsingStatement = /*IDE0063*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild MoveMisplacedUsingDirectives = /*IDE0065*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseSystemHashCode = /*IDE0070*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild SimplifyInterpolation = /*IDE0071*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCoalesceCompoundAssignment = /*IDE0074*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild SimplifyConditionalExpression = /*IDE0075*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UsePatternCombinators = /*IDE0078*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild RemoveUnnecessaryByVal = /*IDE0081*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild ConvertTypeOfToNameOf = /*IDE0082*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseNotPattern = /*IDE0083*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseIsNotExpression = /*IDE0084*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseImplicitObjectCreation = /*IDE0090*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild RemoveRedundantEquality = /*IDE0100*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild RemoveUnnecessaryDiscardDesignation = /*IDE0110*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild RemoveUnnecessaryLambdaExpression = /*IDE0200*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild InvokeDelegateWithConditionalAccess = /*IDE1005*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild NamingRule = /*IDE1006*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild MatchFolderAndNamespace = /*IDE0130*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild SimplifyObjectCreation = /*IDE0140*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild SimplifyPropertyPattern = /*IDE0170*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild RemoveRedundantNullableDirective = /*IDE0240*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild RemoveUnnecessaryNullableDirective = /*IDE0241*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild MakeStructReadOnly = /*IDE0250*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild MakeStructMemberReadOnly = /*IDE0251*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UsePatternMatchingAsAndMemberAccess = /*IDE0260*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCoalesceExpressionForIfNullCheck = /*IDE0270*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseNameofInAttribute = /*IDE0280*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UsePrimaryConstructor = /*IDE0290*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCollectionExpressionForArray = /*IDE0300*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCollectionExpressionForEmpty = /*IDE0301*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCollectionExpressionForStackAlloc = /*IDE0302*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCollectionExpressionForCreate = /*IDE0303*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCollectionExpressionForBuilder = /*IDE0304*/ EnforceOnBuild.Recommended; - public const EnforceOnBuild UseCollectionExpressionForFluent = /*IDE0305*/ EnforceOnBuild.Recommended; + /* EnforceOnBuild.Recommended */ + public const EnforceOnBuild UseThrowExpression = /*IDE0016*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseObjectInitializer = /*IDE0017*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild InlineDeclaration = /*IDE0018*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild InlineAsType = /*IDE0019*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild InlineIsType = /*IDE0020*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseExpressionBodyForConstructors = /*IDE0021*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseExpressionBodyForMethods = /*IDE0022*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseExpressionBodyForConversionOperators = /*IDE0023*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseExpressionBodyForOperators = /*IDE0024*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseExpressionBodyForProperties = /*IDE0025*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseExpressionBodyForIndexers = /*IDE0026*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseExpressionBodyForAccessors = /*IDE0027*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCollectionInitializer = /*IDE0028*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCoalesceExpression = /*IDE0029*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCoalesceExpressionForNullable = /*IDE0030*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseNullPropagation = /*IDE0031*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseAutoProperty = /*IDE0032*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseExplicitTupleName = /*IDE0033*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseDefaultLiteral = /*IDE0034*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild InlineIsTypeWithoutName = /*IDE0038*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseLocalFunction = /*IDE0039*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseDeconstruction = /*IDE0042*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseConditionalExpressionForAssignment = /*IDE0045*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseConditionalExpressionForReturn = /*IDE0046*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild RemoveUnnecessaryParentheses = /*IDE0047*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseExpressionBodyForLambdaExpressions = /*IDE0053*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCompoundAssignment = /*IDE0054*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseIndexOperator = /*IDE0056*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseRangeOperator = /*IDE0057*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseExpressionBodyForLocalFunctions = /*IDE0061*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild MakeLocalFunctionStatic = /*IDE0062*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseSimpleUsingStatement = /*IDE0063*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild MoveMisplacedUsingDirectives = /*IDE0065*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseSystemHashCode = /*IDE0070*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild SimplifyInterpolation = /*IDE0071*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCoalesceCompoundAssignment = /*IDE0074*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild SimplifyConditionalExpression = /*IDE0075*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UsePatternCombinators = /*IDE0078*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild RemoveUnnecessaryByVal = /*IDE0081*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild ConvertTypeOfToNameOf = /*IDE0082*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseNotPattern = /*IDE0083*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseIsNotExpression = /*IDE0084*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseImplicitObjectCreation = /*IDE0090*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild RemoveRedundantEquality = /*IDE0100*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild RemoveUnnecessaryDiscardDesignation = /*IDE0110*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild RemoveUnnecessaryLambdaExpression = /*IDE0200*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild InvokeDelegateWithConditionalAccess = /*IDE1005*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild NamingRule = /*IDE1006*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild MatchFolderAndNamespace = /*IDE0130*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild SimplifyObjectCreation = /*IDE0140*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild SimplifyPropertyPattern = /*IDE0170*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild RemoveRedundantNullableDirective = /*IDE0240*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild RemoveUnnecessaryNullableDirective = /*IDE0241*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild MakeStructReadOnly = /*IDE0250*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild MakeStructMemberReadOnly = /*IDE0251*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UsePatternMatchingAsAndMemberAccess = /*IDE0260*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCoalesceExpressionForIfNullCheck = /*IDE0270*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseNameofInAttribute = /*IDE0280*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UsePrimaryConstructor = /*IDE0290*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCollectionExpressionForArray = /*IDE0300*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCollectionExpressionForEmpty = /*IDE0301*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCollectionExpressionForStackAlloc = /*IDE0302*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCollectionExpressionForCreate = /*IDE0303*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCollectionExpressionForBuilder = /*IDE0304*/ EnforceOnBuild.Recommended; + public const EnforceOnBuild UseCollectionExpressionForFluent = /*IDE0305*/ EnforceOnBuild.Recommended; - /* EnforceOnBuild.WhenExplicitlyEnabled */ - public const EnforceOnBuild RemoveUnnecessaryCast = /*IDE0004*/ EnforceOnBuild.WhenExplicitlyEnabled; // TODO: Move to 'Recommended' OR 'HighlyRecommended' bucket once performance problems are addressed: https://github.com/dotnet/roslyn/issues/43304 - public const EnforceOnBuild PopulateSwitchStatement = /*IDE0010*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild UseInferredMemberName = /*IDE0037*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild UseIsNullCheck = /*IDE0041*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild AddRequiredParentheses = /*IDE0048*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild ExpressionValueIsUnused = /*IDE0058*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild MakeStructFieldsWritable = /*IDE0064*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild ConvertSwitchStatementToExpression = /*IDE0066*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild PopulateSwitchExpression = /*IDE0072*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild SimplifyLinqExpression = /*IDE0120*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild UseNullCheckOverTypeCheck = /*IDE0150*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild UseTopLevelStatements = /*IDE0210*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild UseProgramMain = /*IDE0211*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild ForEachCast = /*IDE0220*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild UseUtf8StringLiteral = /*IDE0230*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild MultipleBlankLines = /*IDE2000*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild EmbeddedStatementPlacement = /*IDE2001*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild ConsecutiveBracePlacement = /*IDE2002*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild ConsecutiveStatementPlacement = /*IDE2003*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild ConstructorInitializerPlacement = /*IDE2004*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild ConditionalExpressionPlacement = /*IDE2005*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild ArrowExpressionClausePlacement = /*IDE2006*/ EnforceOnBuild.WhenExplicitlyEnabled; + /* EnforceOnBuild.WhenExplicitlyEnabled */ + public const EnforceOnBuild RemoveUnnecessaryCast = /*IDE0004*/ EnforceOnBuild.WhenExplicitlyEnabled; // TODO: Move to 'Recommended' OR 'HighlyRecommended' bucket once performance problems are addressed: https://github.com/dotnet/roslyn/issues/43304 + public const EnforceOnBuild PopulateSwitchStatement = /*IDE0010*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild UseInferredMemberName = /*IDE0037*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild UseIsNullCheck = /*IDE0041*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild AddRequiredParentheses = /*IDE0048*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild ExpressionValueIsUnused = /*IDE0058*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild MakeStructFieldsWritable = /*IDE0064*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild ConvertSwitchStatementToExpression = /*IDE0066*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild PopulateSwitchExpression = /*IDE0072*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild SimplifyLinqExpression = /*IDE0120*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild UseNullCheckOverTypeCheck = /*IDE0150*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild UseTopLevelStatements = /*IDE0210*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild UseProgramMain = /*IDE0211*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild ForEachCast = /*IDE0220*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild UseUtf8StringLiteral = /*IDE0230*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild MultipleBlankLines = /*IDE2000*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild EmbeddedStatementPlacement = /*IDE2001*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild ConsecutiveBracePlacement = /*IDE2002*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild ConsecutiveStatementPlacement = /*IDE2003*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild ConstructorInitializerPlacement = /*IDE2004*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild ConditionalExpressionPlacement = /*IDE2005*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild ArrowExpressionClausePlacement = /*IDE2006*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild Regex = /*RE0001*/ EnforceOnBuild.WhenExplicitlyEnabled; - public const EnforceOnBuild Json = /*JSON001*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild Regex = /*RE0001*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild Json = /*JSON001*/ EnforceOnBuild.WhenExplicitlyEnabled; - /* EnforceOnBuild.Never */ - // TODO: Allow enforcing simplify names and related diagnostics on build once we validate their performance charactericstics. - public const EnforceOnBuild SimplifyNames = /*IDE0001*/ EnforceOnBuild.Never; - public const EnforceOnBuild SimplifyMemberAccess = /*IDE0002*/ EnforceOnBuild.Never; - public const EnforceOnBuild RemoveQualification = /*IDE0003*/ EnforceOnBuild.Never; - public const EnforceOnBuild AddQualification = /*IDE0009*/ EnforceOnBuild.Never; - public const EnforceOnBuild PreferBuiltInOrFrameworkType = /*IDE0049*/ EnforceOnBuild.Never; - public const EnforceOnBuild ConvertAnonymousTypeToTuple = /*IDE0050*/ EnforceOnBuild.Never; - public const EnforceOnBuild RemoveUnreachableCode = /*IDE0035*/ EnforceOnBuild.Never; // Non-configurable fading diagnostic corresponding to CS0162. - public const EnforceOnBuild RemoveUnnecessarySuppression = /*IDE0079*/ EnforceOnBuild.Never; // IDE-only analyzer. + /* EnforceOnBuild.Never */ + // TODO: Allow enforcing simplify names and related diagnostics on build once we validate their performance charactericstics. + public const EnforceOnBuild SimplifyNames = /*IDE0001*/ EnforceOnBuild.Never; + public const EnforceOnBuild SimplifyMemberAccess = /*IDE0002*/ EnforceOnBuild.Never; + public const EnforceOnBuild RemoveQualification = /*IDE0003*/ EnforceOnBuild.Never; + public const EnforceOnBuild AddQualification = /*IDE0009*/ EnforceOnBuild.Never; + public const EnforceOnBuild PreferBuiltInOrFrameworkType = /*IDE0049*/ EnforceOnBuild.Never; + public const EnforceOnBuild ConvertAnonymousTypeToTuple = /*IDE0050*/ EnforceOnBuild.Never; + public const EnforceOnBuild RemoveUnreachableCode = /*IDE0035*/ EnforceOnBuild.Never; // Non-configurable fading diagnostic corresponding to CS0162. + public const EnforceOnBuild RemoveUnnecessarySuppression = /*IDE0079*/ EnforceOnBuild.Never; // IDE-only analyzer. - // Pure IDE feature for lighting up editor features. Do not enforce on build. - public const EnforceOnBuild DetectProbableJsonStrings = /*JSON002*/ EnforceOnBuild.Never; - } + // Pure IDE feature for lighting up editor features. Do not enforce on build. + public const EnforceOnBuild DetectProbableJsonStrings = /*JSON002*/ EnforceOnBuild.Never; } diff --git a/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderDiagnosticAnalyzer.cs index a04b1b7297706..e356bc1c9b206 100644 --- a/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderDiagnosticAnalyzer.cs @@ -8,99 +8,98 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.FileHeaders +namespace Microsoft.CodeAnalysis.FileHeaders; + +internal abstract class AbstractFileHeaderDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - internal abstract class AbstractFileHeaderDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer - { - private static readonly LocalizableString s_invalidHeaderTitle = new LocalizableResourceString(nameof(AnalyzersResources.The_file_header_does_not_match_the_required_text), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - private static readonly LocalizableString s_invalidHeaderMessage = new LocalizableResourceString(nameof(AnalyzersResources.A_source_file_contains_a_header_that_does_not_match_the_required_text), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - private static readonly DiagnosticDescriptor s_invalidHeaderDescriptor = CreateDescriptorForFileHeader(s_invalidHeaderTitle, s_invalidHeaderMessage); + private static readonly LocalizableString s_invalidHeaderTitle = new LocalizableResourceString(nameof(AnalyzersResources.The_file_header_does_not_match_the_required_text), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + private static readonly LocalizableString s_invalidHeaderMessage = new LocalizableResourceString(nameof(AnalyzersResources.A_source_file_contains_a_header_that_does_not_match_the_required_text), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + private static readonly DiagnosticDescriptor s_invalidHeaderDescriptor = CreateDescriptorForFileHeader(s_invalidHeaderTitle, s_invalidHeaderMessage); - private static readonly LocalizableString s_missingHeaderTitle = new LocalizableResourceString(nameof(AnalyzersResources.The_file_header_is_missing_or_not_located_at_the_top_of_the_file), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - private static readonly LocalizableString s_missingHeaderMessage = new LocalizableResourceString(nameof(AnalyzersResources.A_source_file_is_missing_a_required_header), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - private static readonly DiagnosticDescriptor s_missingHeaderDescriptor = CreateDescriptorForFileHeader(s_missingHeaderTitle, s_missingHeaderMessage); + private static readonly LocalizableString s_missingHeaderTitle = new LocalizableResourceString(nameof(AnalyzersResources.The_file_header_is_missing_or_not_located_at_the_top_of_the_file), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + private static readonly LocalizableString s_missingHeaderMessage = new LocalizableResourceString(nameof(AnalyzersResources.A_source_file_is_missing_a_required_header), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + private static readonly DiagnosticDescriptor s_missingHeaderDescriptor = CreateDescriptorForFileHeader(s_missingHeaderTitle, s_missingHeaderMessage); - private static DiagnosticDescriptor CreateDescriptorForFileHeader(LocalizableString title, LocalizableString message) - => CreateDescriptorWithId(IDEDiagnosticIds.FileHeaderMismatch, EnforceOnBuildValues.FileHeaderMismatch, hasAnyCodeStyleOption: true, title, message); + private static DiagnosticDescriptor CreateDescriptorForFileHeader(LocalizableString title, LocalizableString message) + => CreateDescriptorWithId(IDEDiagnosticIds.FileHeaderMismatch, EnforceOnBuildValues.FileHeaderMismatch, hasAnyCodeStyleOption: true, title, message); - protected AbstractFileHeaderDiagnosticAnalyzer() - : base(ImmutableDictionary.Empty - .Add(s_invalidHeaderDescriptor, CodeStyleOptions2.FileHeaderTemplate) - .Add(s_missingHeaderDescriptor, CodeStyleOptions2.FileHeaderTemplate)) - { - } + protected AbstractFileHeaderDiagnosticAnalyzer() + : base(ImmutableDictionary.Empty + .Add(s_invalidHeaderDescriptor, CodeStyleOptions2.FileHeaderTemplate) + .Add(s_missingHeaderDescriptor, CodeStyleOptions2.FileHeaderTemplate)) + { + } - protected abstract AbstractFileHeaderHelper FileHeaderHelper { get; } + protected abstract AbstractFileHeaderHelper FileHeaderHelper { get; } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - context.RegisterSyntaxTreeAction(treeContext => HandleSyntaxTree(treeContext, context.Compilation.Options))); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + context.RegisterSyntaxTreeAction(treeContext => HandleSyntaxTree(treeContext, context.Compilation.Options))); - private void HandleSyntaxTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) - { - if (ShouldSkipAnalysis(context, compilationOptions, notification: null)) - return; + private void HandleSyntaxTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + { + if (ShouldSkipAnalysis(context, compilationOptions, notification: null)) + return; - var tree = context.Tree; - var root = tree.GetRoot(context.CancellationToken); + var tree = context.Tree; + var root = tree.GetRoot(context.CancellationToken); - // don't process empty files - if (root.FullSpan.IsEmpty) - { - return; - } + // don't process empty files + if (root.FullSpan.IsEmpty) + { + return; + } - var fileHeaderTemplate = context.GetAnalyzerOptions().FileHeaderTemplate; - if (string.IsNullOrEmpty(fileHeaderTemplate)) - { - return; - } + var fileHeaderTemplate = context.GetAnalyzerOptions().FileHeaderTemplate; + if (string.IsNullOrEmpty(fileHeaderTemplate)) + { + return; + } - var fileHeader = FileHeaderHelper.ParseFileHeader(root); + var fileHeader = FileHeaderHelper.ParseFileHeader(root); - if (!context.ShouldAnalyzeSpan(fileHeader.GetLocation(tree).SourceSpan)) - { - return; - } + if (!context.ShouldAnalyzeSpan(fileHeader.GetLocation(tree).SourceSpan)) + { + return; + } - if (fileHeader.IsMissing) - { - context.ReportDiagnostic(Diagnostic.Create(s_missingHeaderDescriptor, fileHeader.GetLocation(tree))); - return; - } + if (fileHeader.IsMissing) + { + context.ReportDiagnostic(Diagnostic.Create(s_missingHeaderDescriptor, fileHeader.GetLocation(tree))); + return; + } - var expectedFileHeader = fileHeaderTemplate.Replace("{fileName}", Path.GetFileName(tree.FilePath)); - if (!CompareCopyrightText(expectedFileHeader, fileHeader.CopyrightText)) - { - context.ReportDiagnostic(Diagnostic.Create(s_invalidHeaderDescriptor, fileHeader.GetLocation(tree))); - return; - } + var expectedFileHeader = fileHeaderTemplate.Replace("{fileName}", Path.GetFileName(tree.FilePath)); + if (!CompareCopyrightText(expectedFileHeader, fileHeader.CopyrightText)) + { + context.ReportDiagnostic(Diagnostic.Create(s_invalidHeaderDescriptor, fileHeader.GetLocation(tree))); + return; } + } - private static bool CompareCopyrightText(string expectedFileHeader, string copyrightText) + private static bool CompareCopyrightText(string expectedFileHeader, string copyrightText) + { + // make sure that both \n and \r\n are accepted from the settings. + var reformattedCopyrightTextParts = expectedFileHeader.Replace("\r\n", "\n").Split('\n'); + var fileHeaderCopyrightTextParts = copyrightText.Replace("\r\n", "\n").Split('\n'); + + if (reformattedCopyrightTextParts.Length != fileHeaderCopyrightTextParts.Length) { - // make sure that both \n and \r\n are accepted from the settings. - var reformattedCopyrightTextParts = expectedFileHeader.Replace("\r\n", "\n").Split('\n'); - var fileHeaderCopyrightTextParts = copyrightText.Replace("\r\n", "\n").Split('\n'); + return false; + } - if (reformattedCopyrightTextParts.Length != fileHeaderCopyrightTextParts.Length) + // compare line by line, ignoring leading and trailing whitespace on each line. + for (var i = 0; i < reformattedCopyrightTextParts.Length; i++) + { + if (string.CompareOrdinal(reformattedCopyrightTextParts[i].Trim(), fileHeaderCopyrightTextParts[i].Trim()) != 0) { return false; } - - // compare line by line, ignoring leading and trailing whitespace on each line. - for (var i = 0; i < reformattedCopyrightTextParts.Length; i++) - { - if (string.CompareOrdinal(reformattedCopyrightTextParts[i].Trim(), fileHeaderCopyrightTextParts[i].Trim()) != 0) - { - return false; - } - } - - return true; } + + return true; } } diff --git a/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderHelper.cs b/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderHelper.cs index c8a0b80ebd099..7baa27f92252f 100644 --- a/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderHelper.cs +++ b/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderHelper.cs @@ -7,154 +7,153 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.FileHeaders +namespace Microsoft.CodeAnalysis.FileHeaders; + +internal abstract class AbstractFileHeaderHelper { - internal abstract class AbstractFileHeaderHelper + protected AbstractFileHeaderHelper(ISyntaxKinds syntaxKinds) { - protected AbstractFileHeaderHelper(ISyntaxKinds syntaxKinds) - { - SingleLineCommentTriviaKind = syntaxKinds.SingleLineCommentTrivia; - MultiLineCommentTriviaKind = syntaxKinds.MultiLineCommentTrivia; - WhitespaceTriviaKind = syntaxKinds.WhitespaceTrivia; - EndOfLineTriviaKind = syntaxKinds.EndOfLineTrivia; - } + SingleLineCommentTriviaKind = syntaxKinds.SingleLineCommentTrivia; + MultiLineCommentTriviaKind = syntaxKinds.MultiLineCommentTrivia; + WhitespaceTriviaKind = syntaxKinds.WhitespaceTrivia; + EndOfLineTriviaKind = syntaxKinds.EndOfLineTrivia; + } - /// - /// Gets the text prefix indicating a single-line comment. - /// - public abstract string CommentPrefix { get; } + /// + /// Gets the text prefix indicating a single-line comment. + /// + public abstract string CommentPrefix { get; } - protected abstract ReadOnlyMemory GetTextContextOfComment(SyntaxTrivia commentTrivia); + protected abstract ReadOnlyMemory GetTextContextOfComment(SyntaxTrivia commentTrivia); - /// - private int SingleLineCommentTriviaKind { get; } + /// + private int SingleLineCommentTriviaKind { get; } - /// - private int? MultiLineCommentTriviaKind { get; } + /// + private int? MultiLineCommentTriviaKind { get; } - /// - private int WhitespaceTriviaKind { get; } + /// + private int WhitespaceTriviaKind { get; } - /// - private int EndOfLineTriviaKind { get; } + /// + private int EndOfLineTriviaKind { get; } - public FileHeader ParseFileHeader(SyntaxNode root) + public FileHeader ParseFileHeader(SyntaxNode root) + { + var firstToken = root.GetFirstToken(includeZeroWidth: true); + var firstNonWhitespaceTrivia = IndexOfFirstNonWhitespaceTrivia(firstToken.LeadingTrivia); + + if (firstNonWhitespaceTrivia == -1) { - var firstToken = root.GetFirstToken(includeZeroWidth: true); - var firstNonWhitespaceTrivia = IndexOfFirstNonWhitespaceTrivia(firstToken.LeadingTrivia); + return FileHeader.MissingFileHeader(0); + } - if (firstNonWhitespaceTrivia == -1) - { - return FileHeader.MissingFileHeader(0); - } + using var _ = PooledStringBuilder.GetInstance(out var sb); + var endOfLineCount = 0; + var missingHeaderOffset = 0; + var fileHeaderStart = int.MaxValue; + var fileHeaderEnd = int.MinValue; - using var _ = PooledStringBuilder.GetInstance(out var sb); - var endOfLineCount = 0; - var missingHeaderOffset = 0; - var fileHeaderStart = int.MaxValue; - var fileHeaderEnd = int.MinValue; + for (var i = firstNonWhitespaceTrivia; i < firstToken.LeadingTrivia.Count; i++) + { + var trivia = firstToken.LeadingTrivia[i]; - for (var i = firstNonWhitespaceTrivia; i < firstToken.LeadingTrivia.Count; i++) + if (trivia.RawKind == WhitespaceTriviaKind) { - var trivia = firstToken.LeadingTrivia[i]; - - if (trivia.RawKind == WhitespaceTriviaKind) - { - endOfLineCount = 0; - } - else if (trivia.RawKind == SingleLineCommentTriviaKind) - { - endOfLineCount = 0; + endOfLineCount = 0; + } + else if (trivia.RawKind == SingleLineCommentTriviaKind) + { + endOfLineCount = 0; - var commentText = GetTextContextOfComment(trivia).Span.Trim(); + var commentText = GetTextContextOfComment(trivia).Span.Trim(); - fileHeaderStart = Math.Min(trivia.FullSpan.Start, fileHeaderStart); - fileHeaderEnd = trivia.FullSpan.End; + fileHeaderStart = Math.Min(trivia.FullSpan.Start, fileHeaderStart); + fileHeaderEnd = trivia.FullSpan.End; #if NETCOREAPP - sb.Append(commentText).AppendLine(); + sb.Append(commentText).AppendLine(); #else - sb.AppendLine(commentText.ToString()); + sb.AppendLine(commentText.ToString()); #endif - } - else if (trivia.RawKind == MultiLineCommentTriviaKind) + } + else if (trivia.RawKind == MultiLineCommentTriviaKind) + { + // only process a MultiLineCommentTrivia if no SingleLineCommentTrivia have been processed + if (sb.Length == 0) { - // only process a MultiLineCommentTrivia if no SingleLineCommentTrivia have been processed - if (sb.Length == 0) - { - var commentText = GetTextContextOfComment(trivia); - var triviaStringParts = commentText.Span.Trim().ToString().Replace("\r\n", "\n").Split('\n'); + var commentText = GetTextContextOfComment(trivia); + var triviaStringParts = commentText.Span.Trim().ToString().Replace("\r\n", "\n").Split('\n'); - foreach (var part in triviaStringParts) - { - var trimmedPart = part.TrimStart(' ', '*'); - sb.AppendLine(trimmedPart); - } - - fileHeaderStart = trivia.FullSpan.Start; - fileHeaderEnd = trivia.FullSpan.End; + foreach (var part in triviaStringParts) + { + var trimmedPart = part.TrimStart(' ', '*'); + sb.AppendLine(trimmedPart); } - break; + fileHeaderStart = trivia.FullSpan.Start; + fileHeaderEnd = trivia.FullSpan.End; } - else if (trivia.RawKind == EndOfLineTriviaKind) + + break; + } + else if (trivia.RawKind == EndOfLineTriviaKind) + { + endOfLineCount++; + if (endOfLineCount > 1) { - endOfLineCount++; - if (endOfLineCount > 1) - { - break; - } + break; } - else + } + else + { + if (trivia.IsDirective) { - if (trivia.IsDirective) - { - missingHeaderOffset = trivia.FullSpan.End; - } - - if ((fileHeaderStart < fileHeaderEnd) || !trivia.IsDirective) - { - break; - } + missingHeaderOffset = trivia.FullSpan.End; } - } - if (fileHeaderStart > fileHeaderEnd) - { - return FileHeader.MissingFileHeader(missingHeaderOffset); + if ((fileHeaderStart < fileHeaderEnd) || !trivia.IsDirective) + { + break; + } } + } - if (sb.Length > 0) - { - // remove the final newline - var eolLength = Environment.NewLine.Length; - sb.Remove(sb.Length - eolLength, eolLength); - } + if (fileHeaderStart > fileHeaderEnd) + { + return FileHeader.MissingFileHeader(missingHeaderOffset); + } - return new FileHeader(sb.ToString(), fileHeaderStart, fileHeaderEnd, CommentPrefix.Length); + if (sb.Length > 0) + { + // remove the final newline + var eolLength = Environment.NewLine.Length; + sb.Remove(sb.Length - eolLength, eolLength); } - /// - /// Returns the index of the first non-whitespace trivia in the given trivia list. - /// - /// The trivia list to process. - /// The type of the trivia list. - /// The index where the non-whitespace starts, or -1 if there is no non-whitespace trivia. - private int IndexOfFirstNonWhitespaceTrivia(T triviaList) - where T : IReadOnlyList + return new FileHeader(sb.ToString(), fileHeaderStart, fileHeaderEnd, CommentPrefix.Length); + } + + /// + /// Returns the index of the first non-whitespace trivia in the given trivia list. + /// + /// The trivia list to process. + /// The type of the trivia list. + /// The index where the non-whitespace starts, or -1 if there is no non-whitespace trivia. + private int IndexOfFirstNonWhitespaceTrivia(T triviaList) + where T : IReadOnlyList + { + for (var index = 0; index < triviaList.Count; index++) { - for (var index = 0; index < triviaList.Count; index++) + var currentTrivia = triviaList[index]; + if (currentTrivia.RawKind != EndOfLineTriviaKind + && currentTrivia.RawKind != WhitespaceTriviaKind) { - var currentTrivia = triviaList[index]; - if (currentTrivia.RawKind != EndOfLineTriviaKind - && currentTrivia.RawKind != WhitespaceTriviaKind) - { - // encountered non-whitespace trivia -> the search is done. - return index; - } + // encountered non-whitespace trivia -> the search is done. + return index; } - - return -1; } + + return -1; } } diff --git a/src/Analyzers/Core/Analyzers/FileHeaders/FileHeader.cs b/src/Analyzers/Core/Analyzers/FileHeaders/FileHeader.cs index 15e9229121417..7dc41aa1af01e 100644 --- a/src/Analyzers/Core/Analyzers/FileHeaders/FileHeader.cs +++ b/src/Analyzers/Core/Analyzers/FileHeaders/FileHeader.cs @@ -4,105 +4,104 @@ using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.FileHeaders +namespace Microsoft.CodeAnalysis.FileHeaders; + +/// +/// Contains the parsed file header information for a syntax tree. +/// +internal readonly struct FileHeader { /// - /// Contains the parsed file header information for a syntax tree. + /// The location in the source where the file header was expected to start. /// - internal readonly struct FileHeader - { - /// - /// The location in the source where the file header was expected to start. - /// - private readonly int _fileHeaderStart; + private readonly int _fileHeaderStart; - /// - /// The length of the prefix indicating the start of a comment. For example: - /// - /// - /// C# - /// 2, for the length of //. - /// - /// - /// Visual Basic - /// 1, for the length of '. - /// - /// - /// - private readonly int _commentPrefixLength; + /// + /// The length of the prefix indicating the start of a comment. For example: + /// + /// + /// C# + /// 2, for the length of //. + /// + /// + /// Visual Basic + /// 1, for the length of '. + /// + /// + /// + private readonly int _commentPrefixLength; - /// - /// Initializes a new instance of the struct. - /// - /// The copyright string, as parsed from the header. - /// The offset within the file at which the header started. - /// The offset within the file at which the header ended. - internal FileHeader(string copyrightText, int fileHeaderStart, int fileHeaderEnd, int commentPrefixLength) - : this(fileHeaderStart, isMissing: false) - { - // Currently unused - _ = fileHeaderEnd; + /// + /// Initializes a new instance of the struct. + /// + /// The copyright string, as parsed from the header. + /// The offset within the file at which the header started. + /// The offset within the file at which the header ended. + internal FileHeader(string copyrightText, int fileHeaderStart, int fileHeaderEnd, int commentPrefixLength) + : this(fileHeaderStart, isMissing: false) + { + // Currently unused + _ = fileHeaderEnd; - CopyrightText = copyrightText; - _commentPrefixLength = commentPrefixLength; - } + CopyrightText = copyrightText; + _commentPrefixLength = commentPrefixLength; + } - /// - /// Initializes a new instance of the struct. - /// - /// The offset within the file at which the header started, or was expected to start. - /// if the file header is missing; otherwise, . - private FileHeader(int fileHeaderStart, bool isMissing) - { - _fileHeaderStart = fileHeaderStart; - _commentPrefixLength = 0; + /// + /// Initializes a new instance of the struct. + /// + /// The offset within the file at which the header started, or was expected to start. + /// if the file header is missing; otherwise, . + private FileHeader(int fileHeaderStart, bool isMissing) + { + _fileHeaderStart = fileHeaderStart; + _commentPrefixLength = 0; - IsMissing = isMissing; - CopyrightText = ""; - } + IsMissing = isMissing; + CopyrightText = ""; + } - /// - /// Gets a value indicating whether the file header is missing. - /// - /// - /// if the file header is missing; otherwise, . - /// - internal bool IsMissing { get; } + /// + /// Gets a value indicating whether the file header is missing. + /// + /// + /// if the file header is missing; otherwise, . + /// + internal bool IsMissing { get; } - /// - /// Gets the copyright text, as parsed from the header. - /// - /// - /// The copyright text, as parsed from the header. - /// - internal string CopyrightText { get; } + /// + /// Gets the copyright text, as parsed from the header. + /// + /// + /// The copyright text, as parsed from the header. + /// + internal string CopyrightText { get; } - /// - /// Gets a instance representing a missing file header starting at the specified - /// position. - /// - /// The location at which a file header was expected. This will typically be the - /// start of the first line after any directive trivia () to account for - /// source suppressions. - /// - /// A instance representing a missing file header. - /// - internal static FileHeader MissingFileHeader(int fileHeaderStart) - => new(fileHeaderStart, isMissing: true); + /// + /// Gets a instance representing a missing file header starting at the specified + /// position. + /// + /// The location at which a file header was expected. This will typically be the + /// start of the first line after any directive trivia () to account for + /// source suppressions. + /// + /// A instance representing a missing file header. + /// + internal static FileHeader MissingFileHeader(int fileHeaderStart) + => new(fileHeaderStart, isMissing: true); - /// - /// Gets the location representing the start of the file header. - /// - /// The syntax tree to use for generating the location. - /// The location representing the start of the file header. - internal Location GetLocation(SyntaxTree syntaxTree) + /// + /// Gets the location representing the start of the file header. + /// + /// The syntax tree to use for generating the location. + /// The location representing the start of the file header. + internal Location GetLocation(SyntaxTree syntaxTree) + { + if (IsMissing) { - if (IsMissing) - { - return Location.Create(syntaxTree, new TextSpan(_fileHeaderStart, 0)); - } - - return Location.Create(syntaxTree, new TextSpan(_fileHeaderStart, _commentPrefixLength)); + return Location.Create(syntaxTree, new TextSpan(_fileHeaderStart, 0)); } + + return Location.Create(syntaxTree, new TextSpan(_fileHeaderStart, _commentPrefixLength)); } } diff --git a/src/Analyzers/Core/Analyzers/ForEachCast/AbstractForEachCastDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/ForEachCast/AbstractForEachCastDiagnosticAnalyzer.cs index 960ef96325f10..de67dfecef5ff 100644 --- a/src/Analyzers/Core/Analyzers/ForEachCast/AbstractForEachCastDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/ForEachCast/AbstractForEachCastDiagnosticAnalyzer.cs @@ -12,150 +12,150 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ForEachCast +namespace Microsoft.CodeAnalysis.ForEachCast; + +internal static class ForEachCastHelpers { - internal static class ForEachCastHelpers - { - public const string IsFixable = nameof(IsFixable); - } + public const string IsFixable = nameof(IsFixable); +} - internal abstract class AbstractForEachCastDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TSyntaxKind : struct, Enum - where TForEachStatementSyntax : SyntaxNode +internal abstract class AbstractForEachCastDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct, Enum + where TForEachStatementSyntax : SyntaxNode +{ + public static readonly ImmutableDictionary s_isFixableProperties = + ImmutableDictionary.Empty.Add(ForEachCastHelpers.IsFixable, ForEachCastHelpers.IsFixable); + + protected AbstractForEachCastDiagnosticAnalyzer() + : base( + diagnosticId: IDEDiagnosticIds.ForEachCastDiagnosticId, + EnforceOnBuildValues.ForEachCast, + CodeStyleOptions2.ForEachExplicitCastInSource, + title: new LocalizableResourceString(nameof(AnalyzersResources.Add_explicit_cast), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + messageFormat: new LocalizableResourceString(nameof(AnalyzersResources._0_statement_implicitly_converts_1_to_2_Add_an_explicit_cast_to_make_intent_clearer_as_it_may_fail_at_runtime), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - public static readonly ImmutableDictionary s_isFixableProperties = - ImmutableDictionary.Empty.Add(ForEachCastHelpers.IsFixable, ForEachCastHelpers.IsFixable); - - protected AbstractForEachCastDiagnosticAnalyzer() - : base( - diagnosticId: IDEDiagnosticIds.ForEachCastDiagnosticId, - EnforceOnBuildValues.ForEachCast, - CodeStyleOptions2.ForEachExplicitCastInSource, - title: new LocalizableResourceString(nameof(AnalyzersResources.Add_explicit_cast), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - messageFormat: new LocalizableResourceString(nameof(AnalyzersResources._0_statement_implicitly_converts_1_to_2_Add_an_explicit_cast_to_make_intent_clearer_as_it_may_fail_at_runtime), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } - - protected abstract ISyntaxFacts SyntaxFacts { get; } - protected abstract ImmutableArray GetSyntaxKinds(); - protected abstract (CommonConversion conversion, ITypeSymbol? collectionElementType) GetForEachInfo(SemanticModel semanticModel, TForEachStatementSyntax node); + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + protected abstract ISyntaxFacts SyntaxFacts { get; } + protected abstract ImmutableArray GetSyntaxKinds(); + protected abstract (CommonConversion conversion, ITypeSymbol? collectionElementType) GetForEachInfo(SemanticModel semanticModel, TForEachStatementSyntax node); - protected override void InitializeWorker(AnalysisContext context) - { - context.RegisterSyntaxNodeAction(context => AnalyzeSyntax(context), GetSyntaxKinds()); - } + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) - { - var semanticModel = context.SemanticModel; - var cancellationToken = context.CancellationToken; - if (context.Node is not TForEachStatementSyntax node) - return; + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(context => AnalyzeSyntax(context), GetSyntaxKinds()); + } - var option = context.GetAnalyzerOptions().ForEachExplicitCastInSource; - Contract.ThrowIfFalse(option.Value is ForEachExplicitCastInSourcePreference.Always or ForEachExplicitCastInSourcePreference.WhenStronglyTyped); + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var semanticModel = context.SemanticModel; + var cancellationToken = context.CancellationToken; + if (context.Node is not TForEachStatementSyntax node) + return; - if (ShouldSkipAnalysis(context, option.Notification)) - return; + var option = context.GetAnalyzerOptions().ForEachExplicitCastInSource; + Contract.ThrowIfFalse(option.Value is ForEachExplicitCastInSourcePreference.Always or ForEachExplicitCastInSourcePreference.WhenStronglyTyped); - if (semanticModel.GetOperation(node, cancellationToken) is not IForEachLoopOperation loopOperation) - return; + if (ShouldSkipAnalysis(context, option.Notification)) + return; - if (loopOperation.LoopControlVariable is not IVariableDeclaratorOperation variableDeclarator || - variableDeclarator.Symbol.Type is not ITypeSymbol iterationType) - { - return; - } + if (semanticModel.GetOperation(node, cancellationToken) is not IForEachLoopOperation loopOperation) + return; - var syntaxFacts = this.SyntaxFacts; - var collectionType = semanticModel.GetTypeInfo(syntaxFacts.GetExpressionOfForeachStatement(node), cancellationToken).Type; - if (collectionType is null) - return; - - var (conversion, collectionElementType) = GetForEachInfo(semanticModel, node); - if (collectionElementType is null) - return; - - // Don't bother checking conversions that are problematic for other reasons. The user will already have a - // compiler error telling them the foreach is in error. - if (!conversion.Exists) - return; - - // Consider: - // public class C : IEnumerable - // { - // public IEnumerator GetEnumerator() => null; // compiler picks this for the foreach loop. - // - // IEnumerator IEnumerable.GetEnumerator() => null; // compiler doesn't use this. - // } - - // This collection have GetEnumerator method that returns non-strongly-typed IEnumerator, but also implements strongly-typed IEnumerable explicitly. - // In this case, the compiler chooses the non-strongly-typed GetEnumerator and adds explicit cast for `foreach (Match m in new C())`. - // This cast can fail if the collection is badly implemented such that the strongly-typed and non-strongly-typed GetEnumerator implemetations return different types. - // Given it's very rare that implementation can be bad, and to reduce false positives, we adjust collectionElementType for the case above to be `Match` instead of object. - if (collectionElementType.SpecialType == SpecialType.System_Object) - { - var ienumerableOfT = collectionType.AllInterfaces.FirstOrDefault(i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T); - if (ienumerableOfT is not null) - { - collectionElementType = ienumerableOfT.TypeArguments[0]; - conversion = context.Compilation.ClassifyCommonConversion(collectionElementType, iterationType); - } - } + if (loopOperation.LoopControlVariable is not IVariableDeclaratorOperation variableDeclarator || + variableDeclarator.Symbol.Type is not ITypeSymbol iterationType) + { + return; + } - // If the conversion was implicit, then everything is ok. Implicit conversions are safe and do not throw at runtime. - if (conversion.IsImplicit) - return; - - // An implicit legal conversion still shows up as explicit conversion in the object model. But this is fine - // to keep as is since being an implicit-conversion means the API indicates it should always be safe to - // happen at runtime. - if (conversion.IsUserDefined && conversion.MethodSymbol is { Name: WellKnownMemberNames.ImplicitConversionName }) - return; - - // We had a conversion that was explicit. These are potentially unsafe as they can throw at runtime. - // Generally, we would like to notify the user about these. However, we have different policies depending - // on if we think this is a legacy API or not. Legacy APIs are those built before generics, and thus often - // have APIs that will just return `objects` and thus always need some sort of cast to get them to the right - // type. A good example of that is S.T.RegularExpressions.CaptureCollection. Users will almost always - // write this was `foreach (Capture capture in match.Captures)` and it would be annoying to force them to - // change this. - // - // What we do want to warn on are things like: `foreach (IUnrelatedInterface iface in stronglyTypedCollection)`. - // - // So, to detect if we're in a legacy situation, we look for iterations that are returning an object-type - // where the collection itself didn't implement `IEnumerable` in some way. - if (option.Value == ForEachExplicitCastInSourcePreference.WhenStronglyTyped && - !IsStronglyTyped(collectionType, collectionElementType)) + var syntaxFacts = this.SyntaxFacts; + var collectionType = semanticModel.GetTypeInfo(syntaxFacts.GetExpressionOfForeachStatement(node), cancellationToken).Type; + if (collectionType is null) + return; + + var (conversion, collectionElementType) = GetForEachInfo(semanticModel, node); + if (collectionElementType is null) + return; + + // Don't bother checking conversions that are problematic for other reasons. The user will already have a + // compiler error telling them the foreach is in error. + if (!conversion.Exists) + return; + + // Consider: + // public class C : IEnumerable + // { + // public IEnumerator GetEnumerator() => null; // compiler picks this for the foreach loop. + // + // IEnumerator IEnumerable.GetEnumerator() => null; // compiler doesn't use this. + // } + + // This collection have GetEnumerator method that returns non-strongly-typed IEnumerator, but also implements strongly-typed IEnumerable explicitly. + // In this case, the compiler chooses the non-strongly-typed GetEnumerator and adds explicit cast for `foreach (Match m in new C())`. + // This cast can fail if the collection is badly implemented such that the strongly-typed and non-strongly-typed GetEnumerator implemetations return different types. + // Given it's very rare that implementation can be bad, and to reduce false positives, we adjust collectionElementType for the case above to be `Match` instead of object. + if (collectionElementType.SpecialType == SpecialType.System_Object) + { + var ienumerableOfT = collectionType.AllInterfaces.FirstOrDefault(i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T); + if (ienumerableOfT is not null) { - return; + collectionElementType = ienumerableOfT.TypeArguments[0]; + conversion = context.Compilation.ClassifyCommonConversion(collectionElementType, iterationType); } + } - // The user either always wants to write these casts explicitly, or they were calling a non-legacy API. - // report the issue so they can insert the appropriate cast. - - // We can only fix this issue if the collection type implemented ienumerable and we have - // System.Linq.Enumerable available. Then we can add a .Cast call to their collection explicitly. - var isFixable = collectionType.SpecialType == SpecialType.System_Collections_IEnumerable || collectionType.AllInterfaces.Any(static i => i.SpecialType == SpecialType.System_Collections_IEnumerable) && - semanticModel.Compilation.GetBestTypeByMetadataName(typeof(Enumerable).FullName!) != null; - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - node.GetFirstToken().GetLocation(), - option.Notification, - additionalLocations: null, - properties: isFixable ? s_isFixableProperties : null, - node.GetFirstToken().ToString(), - collectionElementType.ToDisplayString(), - iterationType.ToDisplayString())); + // If the conversion was implicit, then everything is ok. Implicit conversions are safe and do not throw at runtime. + if (conversion.IsImplicit) + return; + + // An implicit legal conversion still shows up as explicit conversion in the object model. But this is fine + // to keep as is since being an implicit-conversion means the API indicates it should always be safe to + // happen at runtime. + if (conversion.IsUserDefined && conversion.MethodSymbol is { Name: WellKnownMemberNames.ImplicitConversionName }) + return; + + // We had a conversion that was explicit. These are potentially unsafe as they can throw at runtime. + // Generally, we would like to notify the user about these. However, we have different policies depending + // on if we think this is a legacy API or not. Legacy APIs are those built before generics, and thus often + // have APIs that will just return `objects` and thus always need some sort of cast to get them to the right + // type. A good example of that is S.T.RegularExpressions.CaptureCollection. Users will almost always + // write this was `foreach (Capture capture in match.Captures)` and it would be annoying to force them to + // change this. + // + // What we do want to warn on are things like: `foreach (IUnrelatedInterface iface in stronglyTypedCollection)`. + // + // So, to detect if we're in a legacy situation, we look for iterations that are returning an object-type + // where the collection itself didn't implement `IEnumerable` in some way. + if (option.Value == ForEachExplicitCastInSourcePreference.WhenStronglyTyped && + !IsStronglyTyped(collectionType, collectionElementType)) + { + return; } - private static bool IsStronglyTyped(ITypeSymbol collectionType, ITypeSymbol collectionElementType) - => collectionElementType.SpecialType != SpecialType.System_Object || - collectionType.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T || - collectionType.AllInterfaces.Any(static i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T); + // The user either always wants to write these casts explicitly, or they were calling a non-legacy API. + // report the issue so they can insert the appropriate cast. + + // We can only fix this issue if the collection type implemented ienumerable and we have + // System.Linq.Enumerable available. Then we can add a .Cast call to their collection explicitly. + var isFixable = collectionType.SpecialType == SpecialType.System_Collections_IEnumerable || collectionType.AllInterfaces.Any(static i => i.SpecialType == SpecialType.System_Collections_IEnumerable) && + semanticModel.Compilation.GetBestTypeByMetadataName(typeof(Enumerable).FullName!) != null; + + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + node.GetFirstToken().GetLocation(), + option.Notification, + context.Options, + additionalLocations: null, + properties: isFixable ? s_isFixableProperties : null, + node.GetFirstToken().ToString(), + collectionElementType.ToDisplayString(), + iterationType.ToDisplayString())); } + + private static bool IsStronglyTyped(ITypeSymbol collectionType, ITypeSymbol collectionElementType) + => collectionElementType.SpecialType != SpecialType.System_Object || + collectionType.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T || + collectionType.AllInterfaces.Any(static i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T); } diff --git a/src/Analyzers/Core/Analyzers/Formatting/AbstractFormattingAnalyzer.cs b/src/Analyzers/Core/Analyzers/Formatting/AbstractFormattingAnalyzer.cs index 5183fd1ca26f5..82238c1b92d24 100644 --- a/src/Analyzers/Core/Analyzers/Formatting/AbstractFormattingAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/Formatting/AbstractFormattingAnalyzer.cs @@ -5,44 +5,43 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Formatting; -namespace Microsoft.CodeAnalysis.CodeStyle +namespace Microsoft.CodeAnalysis.CodeStyle; + +internal abstract class AbstractFormattingAnalyzer + : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - internal abstract class AbstractFormattingAnalyzer - : AbstractBuiltInCodeStyleDiagnosticAnalyzer + protected AbstractFormattingAnalyzer() + : base( + IDEDiagnosticIds.FormattingDiagnosticId, + EnforceOnBuildValues.Formatting, + option: null, + new LocalizableResourceString(nameof(AnalyzersResources.Fix_formatting), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Fix_formatting), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - protected AbstractFormattingAnalyzer() - : base( - IDEDiagnosticIds.FormattingDiagnosticId, - EnforceOnBuildValues.Formatting, - option: null, - new LocalizableResourceString(nameof(AnalyzersResources.Fix_formatting), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Fix_formatting), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } - - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; - - protected abstract ISyntaxFormatting SyntaxFormatting { get; } - - protected sealed override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - context.RegisterSyntaxTreeAction(treeContext => AnalyzeSyntaxTree(treeContext, context.Compilation.Options))); - - /// - /// Fixing formatting is high priority. It's something the user wants to be able to fix quickly, is driven by - /// them acting on an error reported in code, and can be computed fast as it only uses syntax not semantics. - /// It's also the 8th most common fix that people use, and is picked almost all the times it is shown. - /// - public override bool IsHighPriority => true; - - private void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) - { - if (ShouldSkipAnalysis(context, compilationOptions, notification: null)) - return; - - var options = context.GetAnalyzerOptions().GetSyntaxFormattingOptions(SyntaxFormatting); - FormattingAnalyzerHelper.AnalyzeSyntaxTree(context, SyntaxFormatting, Descriptor, options); - } + } + + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; + + protected abstract ISyntaxFormatting SyntaxFormatting { get; } + + protected sealed override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + context.RegisterSyntaxTreeAction(treeContext => AnalyzeSyntaxTree(treeContext, context.Compilation.Options))); + + /// + /// Fixing formatting is high priority. It's something the user wants to be able to fix quickly, is driven by + /// them acting on an error reported in code, and can be computed fast as it only uses syntax not semantics. + /// It's also the 8th most common fix that people use, and is picked almost all the times it is shown. + /// + public override bool IsHighPriority => true; + + private void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + { + if (ShouldSkipAnalysis(context, compilationOptions, notification: null)) + return; + + var options = context.GetAnalyzerOptions().GetSyntaxFormattingOptions(SyntaxFormatting); + FormattingAnalyzerHelper.AnalyzeSyntaxTree(context, SyntaxFormatting, Descriptor, options); } } diff --git a/src/Analyzers/Core/Analyzers/Formatting/FormatterHelper.cs b/src/Analyzers/Core/Analyzers/Formatting/FormatterHelper.cs index eb90f80e93436..7b4a7640b7dde 100644 --- a/src/Analyzers/Core/Analyzers/Formatting/FormatterHelper.cs +++ b/src/Analyzers/Core/Analyzers/Formatting/FormatterHelper.cs @@ -9,60 +9,59 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.Formatting.FormattingExtensions; -namespace Microsoft.CodeAnalysis.Formatting +namespace Microsoft.CodeAnalysis.Formatting; + +/// +/// Formats whitespace in documents or syntax trees. +/// +internal static class FormatterHelper { /// - /// Formats whitespace in documents or syntax trees. + /// Gets the formatting rules that would be applied if left unspecified. /// - internal static class FormatterHelper - { - /// - /// Gets the formatting rules that would be applied if left unspecified. - /// - internal static IEnumerable GetDefaultFormattingRules(ISyntaxFormatting syntaxFormattingService) - => syntaxFormattingService.GetDefaultFormattingRules(); + internal static IEnumerable GetDefaultFormattingRules(ISyntaxFormatting syntaxFormattingService) + => syntaxFormattingService.GetDefaultFormattingRules(); - /// - /// Formats the whitespace of a syntax tree. - /// - /// The root node of a syntax tree to format. - /// An optional set of formatting options. If these options are not supplied the current set of options from the workspace will be used. - /// An optional cancellation token. - /// The formatted tree's root node. - public static SyntaxNode Format(SyntaxNode node, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, CancellationToken cancellationToken) - => Format(node, SpecializedCollections.SingletonEnumerable(node.FullSpan), syntaxFormattingService, options, rules: null, cancellationToken: cancellationToken); + /// + /// Formats the whitespace of a syntax tree. + /// + /// The root node of a syntax tree to format. + /// An optional set of formatting options. If these options are not supplied the current set of options from the workspace will be used. + /// An optional cancellation token. + /// The formatted tree's root node. + public static SyntaxNode Format(SyntaxNode node, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, CancellationToken cancellationToken) + => Format(node, SpecializedCollections.SingletonEnumerable(node.FullSpan), syntaxFormattingService, options, rules: null, cancellationToken: cancellationToken); - public static SyntaxNode Format(SyntaxNode node, TextSpan spanToFormat, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, CancellationToken cancellationToken) - => Format(node, SpecializedCollections.SingletonEnumerable(spanToFormat), syntaxFormattingService, options, rules: null, cancellationToken: cancellationToken); + public static SyntaxNode Format(SyntaxNode node, TextSpan spanToFormat, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, CancellationToken cancellationToken) + => Format(node, SpecializedCollections.SingletonEnumerable(spanToFormat), syntaxFormattingService, options, rules: null, cancellationToken: cancellationToken); - /// - /// Formats the whitespace of a syntax tree. - /// - /// The root node of a syntax tree. - /// The descendant nodes of the root to format. - /// An optional set of formatting options. If these options are not supplied the current set of options from the workspace will be used. - /// An optional cancellation token. - /// The formatted tree's root node. - public static SyntaxNode Format(SyntaxNode node, SyntaxAnnotation annotation, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, IEnumerable? rules, CancellationToken cancellationToken) - => Format(node, GetAnnotatedSpans(node, annotation), syntaxFormattingService, options, rules, cancellationToken: cancellationToken); + /// + /// Formats the whitespace of a syntax tree. + /// + /// The root node of a syntax tree. + /// The descendant nodes of the root to format. + /// An optional set of formatting options. If these options are not supplied the current set of options from the workspace will be used. + /// An optional cancellation token. + /// The formatted tree's root node. + public static SyntaxNode Format(SyntaxNode node, SyntaxAnnotation annotation, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, IEnumerable? rules, CancellationToken cancellationToken) + => Format(node, GetAnnotatedSpans(node, annotation), syntaxFormattingService, options, rules, cancellationToken: cancellationToken); - internal static SyntaxNode Format(SyntaxNode node, IEnumerable spans, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, IEnumerable? rules, CancellationToken cancellationToken) - => GetFormattingResult(node, spans, syntaxFormattingService, options, rules, cancellationToken).GetFormattedRoot(cancellationToken); + internal static SyntaxNode Format(SyntaxNode node, IEnumerable spans, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, IEnumerable? rules, CancellationToken cancellationToken) + => GetFormattingResult(node, spans, syntaxFormattingService, options, rules, cancellationToken).GetFormattedRoot(cancellationToken); - internal static IList GetFormattedTextChanges(SyntaxNode node, IEnumerable spans, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, IEnumerable? rules, CancellationToken cancellationToken) - => GetFormattingResult(node, spans, syntaxFormattingService, options, rules, cancellationToken).GetTextChanges(cancellationToken); + internal static IList GetFormattedTextChanges(SyntaxNode node, IEnumerable spans, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, IEnumerable? rules, CancellationToken cancellationToken) + => GetFormattingResult(node, spans, syntaxFormattingService, options, rules, cancellationToken).GetTextChanges(cancellationToken); - internal static IFormattingResult GetFormattingResult(SyntaxNode node, IEnumerable spans, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, IEnumerable? rules, CancellationToken cancellationToken) - => syntaxFormattingService.GetFormattingResult(node, spans, options, rules, cancellationToken); + internal static IFormattingResult GetFormattingResult(SyntaxNode node, IEnumerable spans, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, IEnumerable? rules, CancellationToken cancellationToken) + => syntaxFormattingService.GetFormattingResult(node, spans, options, rules, cancellationToken); - /// - /// Determines the changes necessary to format the whitespace of a syntax tree. - /// - /// The root node of a syntax tree to format. - /// An optional set of formatting options. If these options are not supplied the current set of options from the workspace will be used. - /// An optional cancellation token. - /// The changes necessary to format the tree. - public static IList GetFormattedTextChanges(SyntaxNode node, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, CancellationToken cancellationToken) - => GetFormattedTextChanges(node, SpecializedCollections.SingletonEnumerable(node.FullSpan), syntaxFormattingService, options, rules: null, cancellationToken: cancellationToken); - } + /// + /// Determines the changes necessary to format the whitespace of a syntax tree. + /// + /// The root node of a syntax tree to format. + /// An optional set of formatting options. If these options are not supplied the current set of options from the workspace will be used. + /// An optional cancellation token. + /// The changes necessary to format the tree. + public static IList GetFormattedTextChanges(SyntaxNode node, ISyntaxFormatting syntaxFormattingService, SyntaxFormattingOptions options, CancellationToken cancellationToken) + => GetFormattedTextChanges(node, SpecializedCollections.SingletonEnumerable(node.FullSpan), syntaxFormattingService, options, rules: null, cancellationToken: cancellationToken); } diff --git a/src/Analyzers/Core/Analyzers/Formatting/FormattingAnalyzerHelper.cs b/src/Analyzers/Core/Analyzers/Formatting/FormattingAnalyzerHelper.cs index b9097500891c0..572aa92beeb8c 100644 --- a/src/Analyzers/Core/Analyzers/Formatting/FormattingAnalyzerHelper.cs +++ b/src/Analyzers/Core/Analyzers/Formatting/FormattingAnalyzerHelper.cs @@ -11,66 +11,65 @@ using Formatter = Microsoft.CodeAnalysis.Formatting.FormatterHelper; using FormattingProvider = Microsoft.CodeAnalysis.Formatting.ISyntaxFormatting; -namespace Microsoft.CodeAnalysis.CodeStyle +namespace Microsoft.CodeAnalysis.CodeStyle; + +internal static class FormattingAnalyzerHelper { - internal static class FormattingAnalyzerHelper + internal static void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context, FormattingProvider formattingProvider, DiagnosticDescriptor descriptor, SyntaxFormattingOptions options) { - internal static void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context, FormattingProvider formattingProvider, DiagnosticDescriptor descriptor, SyntaxFormattingOptions options) - { - var tree = context.Tree; - var cancellationToken = context.CancellationToken; + var tree = context.Tree; + var cancellationToken = context.CancellationToken; - var oldText = tree.GetText(cancellationToken); - var root = tree.GetRoot(cancellationToken); - var span = context.FilterSpan.HasValue ? context.FilterSpan.GetValueOrDefault() : root.FullSpan; - var spans = SpecializedCollections.SingletonEnumerable(span); - var formattingChanges = Formatter.GetFormattedTextChanges(root, spans, formattingProvider, options, rules: null, cancellationToken); + var oldText = tree.GetText(cancellationToken); + var root = tree.GetRoot(cancellationToken); + var span = context.FilterSpan.HasValue ? context.FilterSpan.GetValueOrDefault() : root.FullSpan; + var spans = SpecializedCollections.SingletonEnumerable(span); + var formattingChanges = Formatter.GetFormattedTextChanges(root, spans, formattingProvider, options, rules: null, cancellationToken); - // formattingChanges could include changes that impact a larger section of the original document than - // necessary. Before reporting diagnostics, process the changes to minimize the span of individual - // diagnostics. - foreach (var formattingChange in formattingChanges) + // formattingChanges could include changes that impact a larger section of the original document than + // necessary. Before reporting diagnostics, process the changes to minimize the span of individual + // diagnostics. + foreach (var formattingChange in formattingChanges) + { + var change = formattingChange; + if (change.NewText.Length > 0 && !change.Span.IsEmpty) { - var change = formattingChange; - if (change.NewText.Length > 0 && !change.Span.IsEmpty) + // Handle cases where the change is a substring removal from the beginning. In these cases, we want + // the diagnostic span to cover the unwanted leading characters (which should be removed), and + // nothing more. + var offset = change.Span.Length - change.NewText.Length; + if (offset >= 0) { - // Handle cases where the change is a substring removal from the beginning. In these cases, we want - // the diagnostic span to cover the unwanted leading characters (which should be removed), and - // nothing more. - var offset = change.Span.Length - change.NewText.Length; - if (offset >= 0) + if (oldText.GetSubText(new TextSpan(change.Span.Start + offset, change.NewText.Length)).ContentEquals(SourceText.From(change.NewText))) { - if (oldText.GetSubText(new TextSpan(change.Span.Start + offset, change.NewText.Length)).ContentEquals(SourceText.From(change.NewText))) - { - change = new TextChange(new TextSpan(change.Span.Start, offset), ""); - } - else + change = new TextChange(new TextSpan(change.Span.Start, offset), ""); + } + else + { + // Handle cases where the change is a substring removal from the end. In these cases, we want + // the diagnostic span to cover the unwanted trailing characters (which should be removed), and + // nothing more. + if (oldText.GetSubText(new TextSpan(change.Span.Start, change.NewText.Length)).ContentEquals(SourceText.From(change.NewText))) { - // Handle cases where the change is a substring removal from the end. In these cases, we want - // the diagnostic span to cover the unwanted trailing characters (which should be removed), and - // nothing more. - if (oldText.GetSubText(new TextSpan(change.Span.Start, change.NewText.Length)).ContentEquals(SourceText.From(change.NewText))) - { - change = new TextChange(new TextSpan(change.Span.Start + change.NewText.Length, offset), ""); - } + change = new TextChange(new TextSpan(change.Span.Start + change.NewText.Length, offset), ""); } } } + } - if (change.NewText.Length == 0 && change.Span.IsEmpty) - { - // No actual change (allows for the formatter to report a NOP change without triggering a - // diagnostic that can't be fixed). - continue; - } - - var location = Location.Create(tree, change.Span); - context.ReportDiagnostic(Diagnostic.Create( - descriptor, - location, - additionalLocations: null, - properties: null)); + if (change.NewText.Length == 0 && change.Span.IsEmpty) + { + // No actual change (allows for the formatter to report a NOP change without triggering a + // diagnostic that can't be fixed). + continue; } + + var location = Location.Create(tree, change.Span); + context.ReportDiagnostic(Diagnostic.Create( + descriptor, + location, + additionalLocations: null, + properties: null)); } } } diff --git a/src/Analyzers/Core/Analyzers/Helpers/DeserializationConstructorCheck.cs b/src/Analyzers/Core/Analyzers/Helpers/DeserializationConstructorCheck.cs index ae3ae18eadf15..5138c7ea6d79b 100644 --- a/src/Analyzers/Core/Analyzers/Helpers/DeserializationConstructorCheck.cs +++ b/src/Analyzers/Core/Analyzers/Helpers/DeserializationConstructorCheck.cs @@ -4,22 +4,21 @@ using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Shared.Utilities +namespace Microsoft.CodeAnalysis.Shared.Utilities; + +internal readonly struct DeserializationConstructorCheck(Compilation compilation) { - internal readonly struct DeserializationConstructorCheck(Compilation compilation) - { - private readonly INamedTypeSymbol? _iSerializableType = compilation.ISerializableType(); - private readonly INamedTypeSymbol? _serializationInfoType = compilation.SerializationInfoType(); - private readonly INamedTypeSymbol? _streamingContextType = compilation.StreamingContextType(); + private readonly INamedTypeSymbol? _iSerializableType = compilation.ISerializableType(); + private readonly INamedTypeSymbol? _serializationInfoType = compilation.SerializationInfoType(); + private readonly INamedTypeSymbol? _streamingContextType = compilation.StreamingContextType(); - // True if the method is a constructor adhering to the pattern used for custom - // deserialization by types that implement System.Runtime.Serialization.ISerializable - public bool IsDeserializationConstructor(IMethodSymbol methodSymbol) - => _iSerializableType != null && - methodSymbol.MethodKind == MethodKind.Constructor && - methodSymbol.Parameters.Length == 2 && - methodSymbol.Parameters[0].Type.Equals(_serializationInfoType) && - methodSymbol.Parameters[1].Type.Equals(_streamingContextType) && - methodSymbol.ContainingType.AllInterfaces.Contains(_iSerializableType); - } + // True if the method is a constructor adhering to the pattern used for custom + // deserialization by types that implement System.Runtime.Serialization.ISerializable + public bool IsDeserializationConstructor(IMethodSymbol methodSymbol) + => _iSerializableType != null && + methodSymbol.MethodKind == MethodKind.Constructor && + methodSymbol.Parameters.Length == 2 && + methodSymbol.Parameters[0].Type.Equals(_serializationInfoType) && + methodSymbol.Parameters[1].Type.Equals(_streamingContextType) && + methodSymbol.ContainingType.AllInterfaces.Contains(_iSerializableType); } diff --git a/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs b/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs index cbde78dcec0f1..151a02031558b 100644 --- a/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs +++ b/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs @@ -14,336 +14,359 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal static class DiagnosticHelper { - internal static class DiagnosticHelper + /// + /// Creates a instance. + /// + /// A describing the diagnostic. + /// An optional primary location of the diagnostic. If null, will return . + /// Notification option for the diagnostic. + /// Analyzer options + /// + /// An optional set of additional locations related to the diagnostic. + /// Typically, these are locations of other items referenced in the message. + /// If null, will return an empty list. + /// + /// + /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic + /// can convey more detailed information to the fixer. If null, will return + /// . + /// + /// Arguments to the message of the diagnostic. + /// The instance. + public static Diagnostic Create( + DiagnosticDescriptor descriptor, + Location location, + NotificationOption2 notificationOption, + AnalyzerOptions analyzerOptions, + IEnumerable? additionalLocations, + ImmutableDictionary? properties, + params object[] messageArgs) { - /// - /// Creates a instance. - /// - /// A describing the diagnostic. - /// An optional primary location of the diagnostic. If null, will return . - /// Notification option for the diagnostic. - /// - /// An optional set of additional locations related to the diagnostic. - /// Typically, these are locations of other items referenced in the message. - /// If null, will return an empty list. - /// - /// - /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic - /// can convey more detailed information to the fixer. If null, will return - /// . - /// - /// Arguments to the message of the diagnostic. - /// The instance. - public static Diagnostic Create( - DiagnosticDescriptor descriptor, - Location location, - NotificationOption2 notificationOption, - IEnumerable? additionalLocations, - ImmutableDictionary? properties, - params object[] messageArgs) + if (descriptor == null) { - if (descriptor == null) - { - throw new ArgumentNullException(nameof(descriptor)); - } - - LocalizableString message; - if (messageArgs == null || messageArgs.Length == 0) - { - message = descriptor.MessageFormat; - } - else - { - message = new LocalizableStringWithArguments(descriptor.MessageFormat, messageArgs); - } - - return CreateWithMessage(descriptor, location, notificationOption, additionalLocations, properties, message); + throw new ArgumentNullException(nameof(descriptor)); } - /// - /// Create a diagnostic that adds properties specifying a tag for a set of locations. - /// - /// A describing the diagnostic. - /// An optional primary location of the diagnostic. If null, will return . - /// Notification option of the diagnostic. - /// - /// An optional set of additional locations related to the diagnostic. - /// Typically, these are locations of other items referenced in the message. - /// These locations are joined with to produce the value for - /// . - /// - /// - /// An optional set of additional locations indicating unnecessary code related to the diagnostic. - /// These locations are joined with to produce the value for - /// . - /// - /// Arguments to the message of the diagnostic. - /// The instance. - public static Diagnostic CreateWithLocationTags( - DiagnosticDescriptor descriptor, - Location location, - NotificationOption2 notificationOption, - ImmutableArray additionalLocations, - ImmutableArray additionalUnnecessaryLocations, - params object[] messageArgs) + LocalizableString message; + if (messageArgs == null || messageArgs.Length == 0) { - if (additionalUnnecessaryLocations.IsEmpty) - { - return Create(descriptor, location, notificationOption, additionalLocations, ImmutableDictionary.Empty, messageArgs); - } - - var tagIndices = ImmutableDictionary>.Empty - .Add(WellKnownDiagnosticTags.Unnecessary, Enumerable.Range(additionalLocations.Length, additionalUnnecessaryLocations.Length)); - return CreateWithLocationTags( - descriptor, - location, - notificationOption, - additionalLocations.AddRange(additionalUnnecessaryLocations), - tagIndices, - ImmutableDictionary.Empty, - messageArgs); + message = descriptor.MessageFormat; } - - /// - /// Create a diagnostic that adds properties specifying a tag for a set of locations. - /// - /// A describing the diagnostic. - /// An optional primary location of the diagnostic. If null, will return . - /// Notification option for the diagnostic. - /// - /// An optional set of additional locations related to the diagnostic. - /// Typically, these are locations of other items referenced in the message. - /// These locations are joined with to produce the value for - /// . - /// - /// - /// An optional set of additional locations indicating unnecessary code related to the diagnostic. - /// These locations are joined with to produce the value for - /// . - /// - /// - /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic - /// can convey more detailed information to the fixer. - /// - /// Arguments to the message of the diagnostic. - /// The instance. - public static Diagnostic CreateWithLocationTags( - DiagnosticDescriptor descriptor, - Location location, - NotificationOption2 notificationOption, - ImmutableArray additionalLocations, - ImmutableArray additionalUnnecessaryLocations, - ImmutableDictionary? properties, - params object[] messageArgs) + else { - if (additionalUnnecessaryLocations.IsEmpty) - { - return Create(descriptor, location, notificationOption, additionalLocations, properties, messageArgs); - } + message = new LocalizableStringWithArguments(descriptor.MessageFormat, messageArgs); + } + + return CreateWithMessage(descriptor, location, notificationOption, analyzerOptions, additionalLocations, properties, message); + } - var tagIndices = ImmutableDictionary>.Empty - .Add(WellKnownDiagnosticTags.Unnecessary, Enumerable.Range(additionalLocations.Length, additionalUnnecessaryLocations.Length)); - return CreateWithLocationTags( - descriptor, - location, - notificationOption, - additionalLocations.AddRange(additionalUnnecessaryLocations), - tagIndices, - properties, - messageArgs); + /// + /// Create a diagnostic that adds properties specifying a tag for a set of locations. + /// + /// A describing the diagnostic. + /// An optional primary location of the diagnostic. If null, will return . + /// Notification option of the diagnostic. + /// Analyzer options. + /// + /// An optional set of additional locations related to the diagnostic. + /// Typically, these are locations of other items referenced in the message. + /// These locations are joined with to produce the value for + /// . + /// + /// + /// An optional set of additional locations indicating unnecessary code related to the diagnostic. + /// These locations are joined with to produce the value for + /// . + /// + /// Arguments to the message of the diagnostic. + /// The instance. + public static Diagnostic CreateWithLocationTags( + DiagnosticDescriptor descriptor, + Location location, + NotificationOption2 notificationOption, + AnalyzerOptions analyzerOptions, + ImmutableArray additionalLocations, + ImmutableArray additionalUnnecessaryLocations, + params object[] messageArgs) + { + if (additionalUnnecessaryLocations.IsEmpty) + { + return Create(descriptor, location, notificationOption, analyzerOptions, additionalLocations, ImmutableDictionary.Empty, messageArgs); } - /// - /// Create a diagnostic that adds properties specifying a tag for a set of locations. - /// - /// A describing the diagnostic. - /// An optional primary location of the diagnostic. If null, will return . - /// Notification option for the diagnostic. - /// - /// An optional set of additional locations related to the diagnostic. - /// Typically, these are locations of other items referenced in the message. - /// - /// - /// a map of location tag to index in additional locations. - /// "AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer" for an example of usage. - /// - /// - /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic - /// can convey more detailed information to the fixer. - /// - /// Arguments to the message of the diagnostic. - /// The instance. - private static Diagnostic CreateWithLocationTags( - DiagnosticDescriptor descriptor, - Location location, - NotificationOption2 notificationOption, - IEnumerable additionalLocations, - IDictionary> tagIndices, - ImmutableDictionary? properties, - params object[] messageArgs) + var tagIndices = ImmutableDictionary>.Empty + .Add(WellKnownDiagnosticTags.Unnecessary, Enumerable.Range(additionalLocations.Length, additionalUnnecessaryLocations.Length)); + return CreateWithLocationTags( + descriptor, + location, + notificationOption, + analyzerOptions, + additionalLocations.AddRange(additionalUnnecessaryLocations), + tagIndices, + ImmutableDictionary.Empty, + messageArgs); + } + + /// + /// Create a diagnostic that adds properties specifying a tag for a set of locations. + /// + /// A describing the diagnostic. + /// An optional primary location of the diagnostic. If null, will return . + /// Notification option for the diagnostic. + /// Analyzer options. + /// + /// An optional set of additional locations related to the diagnostic. + /// Typically, these are locations of other items referenced in the message. + /// These locations are joined with to produce the value for + /// . + /// + /// + /// An optional set of additional locations indicating unnecessary code related to the diagnostic. + /// These locations are joined with to produce the value for + /// . + /// + /// + /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic + /// can convey more detailed information to the fixer. + /// + /// Arguments to the message of the diagnostic. + /// The instance. + public static Diagnostic CreateWithLocationTags( + DiagnosticDescriptor descriptor, + Location location, + NotificationOption2 notificationOption, + AnalyzerOptions analyzerOptions, + ImmutableArray additionalLocations, + ImmutableArray additionalUnnecessaryLocations, + ImmutableDictionary? properties, + params object[] messageArgs) + { + if (additionalUnnecessaryLocations.IsEmpty) { - Contract.ThrowIfTrue(additionalLocations.IsEmpty()); - Contract.ThrowIfTrue(tagIndices.IsEmpty()); + return Create(descriptor, location, notificationOption, analyzerOptions, additionalLocations, properties, messageArgs); + } - properties ??= ImmutableDictionary.Empty; - properties = properties.AddRange(tagIndices.Select(kvp => new KeyValuePair(kvp.Key, EncodeIndices(kvp.Value, additionalLocations.Count())))); + var tagIndices = ImmutableDictionary>.Empty + .Add(WellKnownDiagnosticTags.Unnecessary, Enumerable.Range(additionalLocations.Length, additionalUnnecessaryLocations.Length)); + return CreateWithLocationTags( + descriptor, + location, + notificationOption, + analyzerOptions, + additionalLocations.AddRange(additionalUnnecessaryLocations), + tagIndices, + properties, + messageArgs); + } - return Create(descriptor, location, notificationOption, additionalLocations, properties, messageArgs); + /// + /// Create a diagnostic that adds properties specifying a tag for a set of locations. + /// + /// A describing the diagnostic. + /// An optional primary location of the diagnostic. If null, will return . + /// Notification option for the diagnostic. + /// + /// An optional set of additional locations related to the diagnostic. + /// Typically, these are locations of other items referenced in the message. + /// + /// + /// a map of location tag to index in additional locations. + /// "AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer" for an example of usage. + /// + /// + /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic + /// can convey more detailed information to the fixer. + /// + /// Arguments to the message of the diagnostic. + /// The instance. + private static Diagnostic CreateWithLocationTags( + DiagnosticDescriptor descriptor, + Location location, + NotificationOption2 notificationOption, + AnalyzerOptions analyzerOptions, + IEnumerable additionalLocations, + IDictionary> tagIndices, + ImmutableDictionary? properties, + params object[] messageArgs) + { + Contract.ThrowIfTrue(additionalLocations.IsEmpty()); + Contract.ThrowIfTrue(tagIndices.IsEmpty()); - static string EncodeIndices(IEnumerable indices, int additionalLocationsLength) - { - // Ensure that the provided tag index is a valid index into additional locations. - Contract.ThrowIfFalse(indices.All(idx => idx >= 0 && idx < additionalLocationsLength)); + properties ??= ImmutableDictionary.Empty; + properties = properties.AddRange(tagIndices.Select(kvp => new KeyValuePair(kvp.Key, EncodeIndices(kvp.Value, additionalLocations.Count())))); - using var stream = new MemoryStream(); - var serializer = new DataContractJsonSerializer(typeof(IEnumerable)); - serializer.WriteObject(stream, indices); + return Create(descriptor, location, notificationOption, analyzerOptions, additionalLocations, properties, messageArgs); - var jsonBytes = stream.ToArray(); - stream.Close(); - return Encoding.UTF8.GetString(jsonBytes, 0, jsonBytes.Length); - } + static string EncodeIndices(IEnumerable indices, int additionalLocationsLength) + { + // Ensure that the provided tag index is a valid index into additional locations. + Contract.ThrowIfFalse(indices.All(idx => idx >= 0 && idx < additionalLocationsLength)); + + using var stream = new MemoryStream(); + var serializer = new DataContractJsonSerializer(typeof(IEnumerable)); + serializer.WriteObject(stream, indices); + + var jsonBytes = stream.ToArray(); + stream.Close(); + return Encoding.UTF8.GetString(jsonBytes, 0, jsonBytes.Length); } + } - /// - /// Creates a instance. - /// - /// A describing the diagnostic. - /// An optional primary location of the diagnostic. If null, will return . - /// Notification option for the diagnostic. - /// - /// An optional set of additional locations related to the diagnostic. - /// Typically, these are locations of other items referenced in the message. - /// If null, will return an empty list. - /// - /// - /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic - /// can convey more detailed information to the fixer. If null, will return - /// . - /// - /// Localizable message for the diagnostic. - /// The instance. - public static Diagnostic CreateWithMessage( - DiagnosticDescriptor descriptor, - Location location, - NotificationOption2 notificationOption, - IEnumerable? additionalLocations, - ImmutableDictionary? properties, - LocalizableString message) + /// + /// Creates a instance. + /// + /// A describing the diagnostic. + /// An optional primary location of the diagnostic. If null, will return . + /// Notification option for the diagnostic. + /// Analyzer options. + /// + /// An optional set of additional locations related to the diagnostic. + /// Typically, these are locations of other items referenced in the message. + /// If null, will return an empty list. + /// + /// + /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic + /// can convey more detailed information to the fixer. If null, will return + /// . + /// + /// Localizable message for the diagnostic. + /// The instance. + public static Diagnostic CreateWithMessage( + DiagnosticDescriptor descriptor, + Location location, + NotificationOption2 notificationOption, + AnalyzerOptions analyzerOptions, + IEnumerable? additionalLocations, + ImmutableDictionary? properties, + LocalizableString message) + { + if (descriptor == null) { - if (descriptor == null) - { - throw new ArgumentNullException(nameof(descriptor)); - } + throw new ArgumentNullException(nameof(descriptor)); + } - var effectiveSeverity = notificationOption.Severity; - return Diagnostic.Create( - descriptor.Id, - descriptor.Category, - message, - effectiveSeverity.ToDiagnosticSeverity() ?? descriptor.DefaultSeverity, - descriptor.DefaultSeverity, - descriptor.IsEnabledByDefault, - warningLevel: effectiveSeverity.WithDefaultSeverity(descriptor.DefaultSeverity) == ReportDiagnostic.Error ? 0 : 1, - effectiveSeverity == ReportDiagnostic.Suppress, - descriptor.Title, - descriptor.Description, - descriptor.HelpLinkUri, - location, - additionalLocations, - GetEffectiveCustomTags(descriptor, notificationOption), - properties); - - static IEnumerable GetEffectiveCustomTags(DiagnosticDescriptor descriptor, NotificationOption2 notificationOption) + var effectiveSeverity = notificationOption.Severity; + return Diagnostic.Create( + descriptor.Id, + descriptor.Category, + message, + effectiveSeverity.ToDiagnosticSeverity() ?? descriptor.DefaultSeverity, + descriptor.DefaultSeverity, + descriptor.IsEnabledByDefault, + warningLevel: effectiveSeverity.WithDefaultSeverity(descriptor.DefaultSeverity) == ReportDiagnostic.Error ? 0 : 1, + effectiveSeverity == ReportDiagnostic.Suppress, + descriptor.Title, + descriptor.Description, + descriptor.HelpLinkUri, + location, + additionalLocations, + GetEffectiveCustomTags(descriptor, notificationOption, analyzerOptions), + properties); + + static IEnumerable GetEffectiveCustomTags(DiagnosticDescriptor descriptor, NotificationOption2 notificationOption, AnalyzerOptions analyzerOptions) + { + // 'CustomSeverityConfigurable' is only enabled when AnalysisLevel >= 9. + var skipCustomConfiguration = !analyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.IsAnalysisLevelGreaterThanOrEquals(9); + if (skipCustomConfiguration) { - var isCustomConfigured = notificationOption.IsExplicitlySpecified; - var hasCustomConfigurableTag = false; foreach (var customTag in descriptor.CustomTags) { - if (customTag == WellKnownDiagnosticTags.CustomSeverityConfigurable) - { - hasCustomConfigurableTag = true; - if (!isCustomConfigured) - continue; - } - - yield return customTag; + if (customTag != WellKnownDiagnosticTags.CustomSeverityConfigurable) + yield return customTag; } - if (isCustomConfigured && !hasCustomConfigurableTag) - yield return WellKnownDiagnosticTags.CustomSeverityConfigurable; + yield break; } - } - - public static string? GetHelpLinkForDiagnosticId(string id) - { - // TODO: Add documentation for Regex and Json analyzer - // Tracked with https://github.com/dotnet/roslyn/issues/48530 - if (id == "RE0001") - return null; - if (id.StartsWith("JSON", StringComparison.Ordinal)) - return null; - - // These diagnostics are hidden and not configurable, so help link can never be shown and is not applicable. - if (id == RemoveUnnecessaryImports.RemoveUnnecessaryImportsConstants.DiagnosticFixableId || - id == "IDE0005_gen") + var isCustomConfigured = notificationOption.IsExplicitlySpecified; + var hasCustomConfigurableTag = false; + foreach (var customTag in descriptor.CustomTags) { - return null; + if (customTag == WellKnownDiagnosticTags.CustomSeverityConfigurable) + { + hasCustomConfigurableTag = true; + if (!isCustomConfigured) + continue; + } + + yield return customTag; } - Debug.Assert(id.StartsWith("IDE", StringComparison.Ordinal)); - return $"https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/{id.ToLowerInvariant()}"; + if (isCustomConfigured && !hasCustomConfigurableTag) + yield return WellKnownDiagnosticTags.CustomSeverityConfigurable; } + } - public sealed class LocalizableStringWithArguments : LocalizableString - { - private readonly LocalizableString _messageFormat; - private readonly string[] _formatArguments; + public static string? GetHelpLinkForDiagnosticId(string id) + { + // TODO: Add documentation for Regex and Json analyzer + // Tracked with https://github.com/dotnet/roslyn/issues/48530 + if (id == "RE0001") + return null; - public LocalizableStringWithArguments(LocalizableString messageFormat, params object[] formatArguments) - { - if (messageFormat == null) - { - throw new ArgumentNullException(nameof(messageFormat)); - } + if (id.StartsWith("JSON", StringComparison.Ordinal)) + return null; - if (formatArguments == null) - { - throw new ArgumentNullException(nameof(formatArguments)); - } + // These diagnostics are hidden and not configurable, so help link can never be shown and is not applicable. + if (id == RemoveUnnecessaryImports.RemoveUnnecessaryImportsConstants.DiagnosticFixableId || + id == "IDE0005_gen") + { + return null; + } - _messageFormat = messageFormat; - _formatArguments = new string[formatArguments.Length]; - for (var i = 0; i < formatArguments.Length; i++) - { - _formatArguments[i] = $"{formatArguments[i]}"; - } - } + Debug.Assert(id.StartsWith("IDE", StringComparison.Ordinal)); + return $"https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/{id.ToLowerInvariant()}"; + } - protected override string GetText(IFormatProvider? formatProvider) + public sealed class LocalizableStringWithArguments : LocalizableString + { + private readonly LocalizableString _messageFormat; + private readonly string[] _formatArguments; + + public LocalizableStringWithArguments(LocalizableString messageFormat, params object[] formatArguments) + { + if (messageFormat == null) { - var messageFormat = _messageFormat.ToString(formatProvider); - return messageFormat != null - ? (_formatArguments.Length > 0 ? string.Format(formatProvider, messageFormat, _formatArguments) : messageFormat) - : string.Empty; + throw new ArgumentNullException(nameof(messageFormat)); } - protected override bool AreEqual(object? other) + if (formatArguments == null) { - return other is LocalizableStringWithArguments otherResourceString && - _messageFormat.Equals(otherResourceString._messageFormat) && - _formatArguments.SequenceEqual(otherResourceString._formatArguments, (a, b) => a == b); + throw new ArgumentNullException(nameof(formatArguments)); } - protected override int GetHash() + _messageFormat = messageFormat; + _formatArguments = new string[formatArguments.Length]; + for (var i = 0; i < formatArguments.Length; i++) { - return Hash.Combine( - _messageFormat.GetHashCode(), - Hash.CombineValues(_formatArguments)); + _formatArguments[i] = $"{formatArguments[i]}"; } } + + protected override string GetText(IFormatProvider? formatProvider) + { + var messageFormat = _messageFormat.ToString(formatProvider); + return messageFormat != null + ? (_formatArguments.Length > 0 ? string.Format(formatProvider, messageFormat, _formatArguments) : messageFormat) + : string.Empty; + } + + protected override bool AreEqual(object? other) + { + return other is LocalizableStringWithArguments otherResourceString && + _messageFormat.Equals(otherResourceString._messageFormat) && + _formatArguments.SequenceEqual(otherResourceString._formatArguments, (a, b) => a == b); + } + + protected override int GetHash() + { + return Hash.Combine( + _messageFormat.GetHashCode(), + Hash.CombineValues(_formatArguments)); + } } } diff --git a/src/Analyzers/Core/Analyzers/Helpers/HashCodeAnalyzer/HashCodeAnalyzer.OperationDeconstructor.cs b/src/Analyzers/Core/Analyzers/Helpers/HashCodeAnalyzer/HashCodeAnalyzer.OperationDeconstructor.cs index 7aa098e81ba68..8e9d2758b6cf6 100644 --- a/src/Analyzers/Core/Analyzers/Helpers/HashCodeAnalyzer/HashCodeAnalyzer.OperationDeconstructor.cs +++ b/src/Analyzers/Core/Analyzers/Helpers/HashCodeAnalyzer/HashCodeAnalyzer.OperationDeconstructor.cs @@ -9,193 +9,192 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Shared.Utilities +namespace Microsoft.CodeAnalysis.Shared.Utilities; + +internal partial struct HashCodeAnalyzer { - internal partial struct HashCodeAnalyzer + /// + /// Breaks down complex trees, looking for particular + /// patterns and extracting out the field and property + /// symbols use to compute the hash. + /// + private struct OperationDeconstructor( + HashCodeAnalyzer analyzer, IMethodSymbol method, ILocalSymbol? hashCodeVariable) : IDisposable { + private readonly HashCodeAnalyzer _analyzer = analyzer; + private readonly IMethodSymbol _method = method; + private readonly ILocalSymbol? _hashCodeVariable = hashCodeVariable; + + private readonly ArrayBuilder _hashedSymbols = ArrayBuilder.GetInstance(); + private bool _accessesBase = false; + + public readonly void Dispose() + => _hashedSymbols.Free(); + + public readonly (bool accessesBase, ImmutableArray hashedSymbol) GetResult() + => (_accessesBase, _hashedSymbols.ToImmutable()); + /// - /// Breaks down complex trees, looking for particular - /// patterns and extracting out the field and property - /// symbols use to compute the hash. + /// Recursive function that decomposes , looking for particular + /// forms that VS or ReSharper generate to hash fields in the containing type. /// - private struct OperationDeconstructor( - HashCodeAnalyzer analyzer, IMethodSymbol method, ILocalSymbol? hashCodeVariable) : IDisposable + /// 'seenHash' is used to determine if we actually saw something + /// that indicates that we really hashed a field/property and weren't just simply + /// referencing it. This is used as we recurse down to make sure we've seen a + /// pattern we explicitly recognize by the time we hit a field/prop. + public bool TryAddHashedSymbol(IOperation value, bool seenHash) { - private readonly HashCodeAnalyzer _analyzer = analyzer; - private readonly IMethodSymbol _method = method; - private readonly ILocalSymbol? _hashCodeVariable = hashCodeVariable; - - private readonly ArrayBuilder _hashedSymbols = ArrayBuilder.GetInstance(); - private bool _accessesBase = false; - - public readonly void Dispose() - => _hashedSymbols.Free(); - - public readonly (bool accessesBase, ImmutableArray hashedSymbol) GetResult() - => (_accessesBase, _hashedSymbols.ToImmutable()); - - /// - /// Recursive function that decomposes , looking for particular - /// forms that VS or ReSharper generate to hash fields in the containing type. - /// - /// 'seenHash' is used to determine if we actually saw something - /// that indicates that we really hashed a field/property and weren't just simply - /// referencing it. This is used as we recurse down to make sure we've seen a - /// pattern we explicitly recognize by the time we hit a field/prop. - public bool TryAddHashedSymbol(IOperation value, bool seenHash) + value = Unwrap(value); + switch (value) { - value = Unwrap(value); - switch (value) - { - case IBinaryOperation topBinary: - // (hashCode op1 constant) op1 hashed_value + case IBinaryOperation topBinary: + // (hashCode op1 constant) op1 hashed_value + // + // This is generated by both VS and ReSharper. Though each use different mathematical + // ops to combine the values. + return _hashCodeVariable != null && + topBinary.LeftOperand is IBinaryOperation leftBinary && + IsLocalReference(leftBinary.LeftOperand, _hashCodeVariable) && + IsLiteralNumber(leftBinary.RightOperand) && + TryAddHashedSymbol(topBinary.RightOperand, seenHash: true); + + case IInvocationOperation invocation: + var targetMethod = invocation.TargetMethod; + if (_analyzer.OverridesSystemObject(targetMethod)) + { + // Either: + // + // a.GetHashCode() // - // This is generated by both VS and ReSharper. Though each use different mathematical - // ops to combine the values. - return _hashCodeVariable != null && - topBinary.LeftOperand is IBinaryOperation leftBinary && - IsLocalReference(leftBinary.LeftOperand, _hashCodeVariable) && - IsLiteralNumber(leftBinary.RightOperand) && - TryAddHashedSymbol(topBinary.RightOperand, seenHash: true); - - case IInvocationOperation invocation: - var targetMethod = invocation.TargetMethod; - if (_analyzer.OverridesSystemObject(targetMethod)) + // or + // + // (hashCode * -1521134295 + a.GetHashCode()).GetHashCode() + // + // recurse on the value we're calling GetHashCode on. + RoslynDebug.Assert(invocation.Instance is not null); + return TryAddHashedSymbol(invocation.Instance, seenHash: true); + } + + if (targetMethod.Name == nameof(GetHashCode) && + Equals(_analyzer._equalityComparerType, targetMethod.ContainingType.OriginalDefinition) && + invocation.Arguments.Length == 1) + { + // EqualityComparer.Default.GetHashCode(i) + // + // VS codegen only. + return TryAddHashedSymbol(invocation.Arguments[0].Value, seenHash: true); + } + + // No other invocations supported. + return false; + + case IConditionalOperation conditional: + // (StringProperty != null ? StringProperty.GetHashCode() : 0) + // + // ReSharper codegen only. + if (conditional.Condition is IBinaryOperation binary && + Unwrap(binary.RightOperand).IsNullLiteral() && + TryGetFieldOrProperty(binary.LeftOperand, out _)) + { + if (binary.OperatorKind == BinaryOperatorKind.Equals) { - // Either: - // - // a.GetHashCode() - // - // or - // - // (hashCode * -1521134295 + a.GetHashCode()).GetHashCode() - // - // recurse on the value we're calling GetHashCode on. - RoslynDebug.Assert(invocation.Instance is not null); - return TryAddHashedSymbol(invocation.Instance, seenHash: true); + // (StringProperty == null ? 0 : StringProperty.GetHashCode()) + RoslynDebug.Assert(conditional.WhenFalse is not null); + return TryAddHashedSymbol(conditional.WhenFalse, seenHash: true); } - - if (targetMethod.Name == nameof(GetHashCode) && - Equals(_analyzer._equalityComparerType, targetMethod.ContainingType.OriginalDefinition) && - invocation.Arguments.Length == 1) + else if (binary.OperatorKind == BinaryOperatorKind.NotEquals) { - // EqualityComparer.Default.GetHashCode(i) - // - // VS codegen only. - return TryAddHashedSymbol(invocation.Arguments[0].Value, seenHash: true); + // (StringProperty != null ? StringProperty.GetHashCode() : 0) + return TryAddHashedSymbol(conditional.WhenTrue, seenHash: true); } + } + + // no other conditional forms supported. + return false; + } - // No other invocations supported. + // Look to see if we're referencing some field/prop/base. However, we only accept + // this reference if we've at least been through something that indicates that we've + // hashed the value. + if (seenHash) + { + if (value is IInstanceReferenceOperation instanceReference && + instanceReference.ReferenceKind == InstanceReferenceKind.ContainingTypeInstance && + Equals(_method.ContainingType.BaseType, instanceReference.Type)) + { + if (_accessesBase) + { + // already had a reference to base.GetHashCode(); return false; + } - case IConditionalOperation conditional: - // (StringProperty != null ? StringProperty.GetHashCode() : 0) - // - // ReSharper codegen only. - if (conditional.Condition is IBinaryOperation binary && - Unwrap(binary.RightOperand).IsNullLiteral() && - TryGetFieldOrProperty(binary.LeftOperand, out _)) - { - if (binary.OperatorKind == BinaryOperatorKind.Equals) - { - // (StringProperty == null ? 0 : StringProperty.GetHashCode()) - RoslynDebug.Assert(conditional.WhenFalse is not null); - return TryAddHashedSymbol(conditional.WhenFalse, seenHash: true); - } - else if (binary.OperatorKind == BinaryOperatorKind.NotEquals) - { - // (StringProperty != null ? StringProperty.GetHashCode() : 0) - return TryAddHashedSymbol(conditional.WhenTrue, seenHash: true); - } - } + // reference to base. + // + // Happens with code like: `var hashCode = base.GetHashCode();` + _accessesBase = true; + return true; + } - // no other conditional forms supported. - return false; + // After decomposing all of the above patterns, we must end up with an operation that is + // a reference to an instance-field (or prop) in our type. If so, and this is the only + // time we've seen that field/prop, then we're good. + // + // We only do this if we actually did something that counts as hashing along the way. This + // way + if (TryGetFieldOrProperty(value, out var fieldOrProp) && + Equals(fieldOrProp.ContainingType.OriginalDefinition, _method.ContainingType)) + { + return TryAddSymbol(fieldOrProp); } - // Look to see if we're referencing some field/prop/base. However, we only accept - // this reference if we've at least been through something that indicates that we've - // hashed the value. - if (seenHash) + if (value is ITupleOperation tupleOperation) { - if (value is IInstanceReferenceOperation instanceReference && - instanceReference.ReferenceKind == InstanceReferenceKind.ContainingTypeInstance && - Equals(_method.ContainingType.BaseType, instanceReference.Type)) + foreach (var element in tupleOperation.Elements) { - if (_accessesBase) + if (!TryAddHashedSymbol(element, seenHash: true)) { - // already had a reference to base.GetHashCode(); return false; } - - // reference to base. - // - // Happens with code like: `var hashCode = base.GetHashCode();` - _accessesBase = true; - return true; } - // After decomposing all of the above patterns, we must end up with an operation that is - // a reference to an instance-field (or prop) in our type. If so, and this is the only - // time we've seen that field/prop, then we're good. - // - // We only do this if we actually did something that counts as hashing along the way. This - // way - if (TryGetFieldOrProperty(value, out var fieldOrProp) && - Equals(fieldOrProp.ContainingType.OriginalDefinition, _method.ContainingType)) - { - return TryAddSymbol(fieldOrProp); - } + return true; + } + } - if (value is ITupleOperation tupleOperation) - { - foreach (var element in tupleOperation.Elements) - { - if (!TryAddHashedSymbol(element, seenHash: true)) - { - return false; - } - } + // Anything else is not recognized. + return false; + } - return true; - } - } + private static bool TryGetFieldOrProperty(IOperation operation, [NotNullWhen(true)] out ISymbol? symbol) + { + operation = Unwrap(operation); - // Anything else is not recognized. - return false; + if (operation is IFieldReferenceOperation fieldReference) + { + symbol = fieldReference.Member; + return !symbol.IsStatic; } - private static bool TryGetFieldOrProperty(IOperation operation, [NotNullWhen(true)] out ISymbol? symbol) + if (operation is IPropertyReferenceOperation propertyReference) { - operation = Unwrap(operation); - - if (operation is IFieldReferenceOperation fieldReference) - { - symbol = fieldReference.Member; - return !symbol.IsStatic; - } + symbol = propertyReference.Member; + return !symbol.IsStatic; + } - if (operation is IPropertyReferenceOperation propertyReference) - { - symbol = propertyReference.Member; - return !symbol.IsStatic; - } + symbol = null; + return false; + } - symbol = null; + private readonly bool TryAddSymbol(ISymbol member) + { + // Not a legal GetHashCode to convert if we refer to members multiple times. + if (_hashedSymbols.Contains(member)) + { return false; } - private readonly bool TryAddSymbol(ISymbol member) - { - // Not a legal GetHashCode to convert if we refer to members multiple times. - if (_hashedSymbols.Contains(member)) - { - return false; - } - - _hashedSymbols.Add(member); - return true; - } + _hashedSymbols.Add(member); + return true; } } } diff --git a/src/Analyzers/Core/Analyzers/Helpers/HashCodeAnalyzer/HashCodeAnalyzer.cs b/src/Analyzers/Core/Analyzers/Helpers/HashCodeAnalyzer/HashCodeAnalyzer.cs index 9769ea1087043..4931395a53f80 100644 --- a/src/Analyzers/Core/Analyzers/Helpers/HashCodeAnalyzer/HashCodeAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/Helpers/HashCodeAnalyzer/HashCodeAnalyzer.cs @@ -9,256 +9,255 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Shared.Utilities +namespace Microsoft.CodeAnalysis.Shared.Utilities; + +/// +/// Helper code to support analysis of HashCode methods +/// +internal readonly partial struct HashCodeAnalyzer { - /// - /// Helper code to support analysis of HashCode methods - /// - internal readonly partial struct HashCodeAnalyzer - { - private readonly Compilation _compilation; - private readonly IMethodSymbol _objectGetHashCodeMethod; - private readonly INamedTypeSymbol? _equalityComparerType; + private readonly Compilation _compilation; + private readonly IMethodSymbol _objectGetHashCodeMethod; + private readonly INamedTypeSymbol? _equalityComparerType; - public readonly INamedTypeSymbol SystemHashCodeType; + public readonly INamedTypeSymbol SystemHashCodeType; - private HashCodeAnalyzer( - Compilation compilation, IMethodSymbol objectGetHashCodeMethod, - INamedTypeSymbol? equalityComparerType, INamedTypeSymbol systemHashCodeType) - { - _compilation = compilation; - _objectGetHashCodeMethod = objectGetHashCodeMethod; - _equalityComparerType = equalityComparerType; - SystemHashCodeType = systemHashCodeType; - } + private HashCodeAnalyzer( + Compilation compilation, IMethodSymbol objectGetHashCodeMethod, + INamedTypeSymbol? equalityComparerType, INamedTypeSymbol systemHashCodeType) + { + _compilation = compilation; + _objectGetHashCodeMethod = objectGetHashCodeMethod; + _equalityComparerType = equalityComparerType; + SystemHashCodeType = systemHashCodeType; + } - public static bool TryGetAnalyzer(Compilation compilation, [NotNullWhen(true)] out HashCodeAnalyzer analyzer) - { - analyzer = default; - var objectType = compilation.GetSpecialType(SpecialType.System_Object); - // This may not find anything. However, CanAnalyze checks for this. So - // we represent the value as non-nullable for all future code. - var equalityComparerType = compilation.GetBestTypeByMetadataName(typeof(EqualityComparer<>).FullName!); + public static bool TryGetAnalyzer(Compilation compilation, [NotNullWhen(true)] out HashCodeAnalyzer analyzer) + { + analyzer = default; + var objectType = compilation.GetSpecialType(SpecialType.System_Object); + // This may not find anything. However, CanAnalyze checks for this. So + // we represent the value as non-nullable for all future code. + var equalityComparerType = compilation.GetBestTypeByMetadataName(typeof(EqualityComparer<>).FullName!); - if (objectType?.GetMembers(nameof(GetHashCode)).FirstOrDefault() is not IMethodSymbol objectGetHashCodeMethod) - return false; + if (objectType?.GetMembers(nameof(GetHashCode)).FirstOrDefault() is not IMethodSymbol objectGetHashCodeMethod) + return false; - var systemHashCodeType = compilation.GetBestTypeByMetadataName("System.HashCode"); - if (systemHashCodeType == null) - return false; + var systemHashCodeType = compilation.GetBestTypeByMetadataName("System.HashCode"); + if (systemHashCodeType == null) + return false; - analyzer = new HashCodeAnalyzer(compilation, objectGetHashCodeMethod, equalityComparerType, systemHashCodeType); - return true; - } + analyzer = new HashCodeAnalyzer(compilation, objectGetHashCodeMethod, equalityComparerType, systemHashCodeType); + return true; + } - /// - /// Analyzes the containing GetHashCode method to determine which fields and - /// properties were combined to form a hash code for this type. - /// - public (bool accessesBase, ImmutableArray members, ImmutableArray statements) GetHashedMembers(ISymbol? owningSymbol, IOperation? operation) - { - if (operation is not IBlockOperation blockOperation) - return default; + /// + /// Analyzes the containing GetHashCode method to determine which fields and + /// properties were combined to form a hash code for this type. + /// + public (bool accessesBase, ImmutableArray members, ImmutableArray statements) GetHashedMembers(ISymbol? owningSymbol, IOperation? operation) + { + if (operation is not IBlockOperation blockOperation) + return default; - // Owning symbol has to be an override of Object.GetHashCode. - if (owningSymbol is not IMethodSymbol { Name: nameof(GetHashCode) } method) - return default; + // Owning symbol has to be an override of Object.GetHashCode. + if (owningSymbol is not IMethodSymbol { Name: nameof(GetHashCode) } method) + return default; - if (method.Locations.Length != 1 || method.DeclaringSyntaxReferences.Length != 1) - return default; + if (method.Locations.Length != 1 || method.DeclaringSyntaxReferences.Length != 1) + return default; - if (!method.Locations[0].IsInSource) - return default; + if (!method.Locations[0].IsInSource) + return default; - if (!OverridesSystemObject(method)) - return default; + if (!OverridesSystemObject(method)) + return default; - // Unwind through nested blocks. This also handles if we're in an 'unchecked' block in C# - while (blockOperation.Operations is [IBlockOperation childBlock]) - blockOperation = childBlock; + // Unwind through nested blocks. This also handles if we're in an 'unchecked' block in C# + while (blockOperation.Operations is [IBlockOperation childBlock]) + blockOperation = childBlock; - var statements = blockOperation.Operations.WhereAsArray(o => !o.IsImplicit); - var (accessesBase, members) = - MatchAccumulatorPattern(method, statements) ?? - MatchTuplePattern(method, statements) ?? - default; + var statements = blockOperation.Operations.WhereAsArray(o => !o.IsImplicit); + var (accessesBase, members) = + MatchAccumulatorPattern(method, statements) ?? + MatchTuplePattern(method, statements) ?? + default; - return (accessesBase, members, statements); + return (accessesBase, members, statements); + } + + private (bool accessesBase, ImmutableArray members)? MatchTuplePattern( + IMethodSymbol method, ImmutableArray statements) + { + // look for code of the form `return (a, b, c).GetHashCode()`. + if (statements.Length != 1) + { + return null; } - private (bool accessesBase, ImmutableArray members)? MatchTuplePattern( - IMethodSymbol method, ImmutableArray statements) + if (statements[0] is not IReturnOperation { ReturnedValue: { } returnedValue }) { - // look for code of the form `return (a, b, c).GetHashCode()`. - if (statements.Length != 1) - { - return null; - } + return null; + } - if (statements[0] is not IReturnOperation { ReturnedValue: { } returnedValue }) - { - return null; - } + using var analyzer = new OperationDeconstructor(this, method, hashCodeVariable: null); + if (!analyzer.TryAddHashedSymbol(returnedValue, seenHash: false)) + { + return null; + } - using var analyzer = new OperationDeconstructor(this, method, hashCodeVariable: null); - if (!analyzer.TryAddHashedSymbol(returnedValue, seenHash: false)) - { - return null; - } + return analyzer.GetResult(); + } - return analyzer.GetResult(); + private (bool accessesBase, ImmutableArray members)? MatchAccumulatorPattern( + IMethodSymbol method, ImmutableArray statements) + { + // Needs to be of the form: + // + // // accumulator + // var hashCode = + // + // // 1-N member hashes mixed into the accumulator. + // hashCode = (hashCode op constant) op Hash(member) + // + // // return of the value. + // return hashCode; + if (statements.Length < 3) + { + return null; } - private (bool accessesBase, ImmutableArray members)? MatchAccumulatorPattern( - IMethodSymbol method, ImmutableArray statements) + // First statement has to be the declaration of the accumulator. + // Last statement has to be the return of it. + if (statements.First() is not IVariableDeclarationGroupOperation varDeclStatement || + !(statements.Last() is IReturnOperation { ReturnedValue: { } returnedValue })) { - // Needs to be of the form: - // - // // accumulator - // var hashCode = - // - // // 1-N member hashes mixed into the accumulator. - // hashCode = (hashCode op constant) op Hash(member) - // - // // return of the value. - // return hashCode; - if (statements.Length < 3) - { - return null; - } + return null; + } - // First statement has to be the declaration of the accumulator. - // Last statement has to be the return of it. - if (statements.First() is not IVariableDeclarationGroupOperation varDeclStatement || - !(statements.Last() is IReturnOperation { ReturnedValue: { } returnedValue })) - { - return null; - } + var variables = varDeclStatement.GetDeclaredVariables(); + if (variables.Length != 1 || + varDeclStatement.Declarations.Length != 1) + { + return null; + } - var variables = varDeclStatement.GetDeclaredVariables(); - if (variables.Length != 1 || - varDeclStatement.Declarations.Length != 1) - { - return null; - } + var declaration = varDeclStatement.Declarations[0]; + if (declaration.Declarators.Length != 1) + { + return null; + } - var declaration = varDeclStatement.Declarations[0]; - if (declaration.Declarators.Length != 1) - { - return null; - } + var declarator = declaration.Declarators[0]; + var initializerValue = declaration.Initializer?.Value ?? declarator.Initializer?.Value; + if (initializerValue == null) + { + return null; + } - var declarator = declaration.Declarators[0]; - var initializerValue = declaration.Initializer?.Value ?? declarator.Initializer?.Value; - if (initializerValue == null) - { - return null; - } + var hashCodeVariable = declarator.Symbol; + if (!(IsLocalReference(returnedValue, hashCodeVariable))) + { + return null; + } - var hashCodeVariable = declarator.Symbol; - if (!(IsLocalReference(returnedValue, hashCodeVariable))) - { - return null; - } + using var valueAnalyzer = new OperationDeconstructor(this, method, hashCodeVariable); + + // Local declaration can be of the form: + // + // // VS code gen + // var hashCode = number; + // + // or + // + // // ReSharper code gen + // var hashCode = Hash(firstSymbol); + + // Note: we pass in `seenHash: true` here because ReSharper may just initialize things + // like `var hashCode = intField`. In this case, there won't be any specific hashing + // operations in the value that we have to look for. + if (!IsLiteralNumber(initializerValue) && + !valueAnalyzer.TryAddHashedSymbol(initializerValue, seenHash: true)) + { + return null; + } - using var valueAnalyzer = new OperationDeconstructor(this, method, hashCodeVariable); - - // Local declaration can be of the form: - // - // // VS code gen - // var hashCode = number; - // - // or - // - // // ReSharper code gen - // var hashCode = Hash(firstSymbol); - - // Note: we pass in `seenHash: true` here because ReSharper may just initialize things - // like `var hashCode = intField`. In this case, there won't be any specific hashing - // operations in the value that we have to look for. - if (!IsLiteralNumber(initializerValue) && - !valueAnalyzer.TryAddHashedSymbol(initializerValue, seenHash: true)) + // Now check all the intermediary statements. They all have to be of the form: + // + // hashCode = (hashCode op constant) op Hash(member) + // + // Or recursively built out of that. For example, in VB we sometimes generate: + // + // hashCode = Hash((hashCode op constant) op Hash(member)) + // + // So, after confirming we're assigning to our accumulator, we recursively break down + // the expression, looking for valid forms that only end up hashing a single field in + // some way. + for (var i = 1; i < statements.Length - 1; i++) + { + var statement = statements[i]; + if (statement is not IExpressionStatementOperation expressionStatement || + expressionStatement.Operation is not ISimpleAssignmentOperation simpleAssignment || + !IsLocalReference(simpleAssignment.Target, hashCodeVariable) || + !valueAnalyzer.TryAddHashedSymbol(simpleAssignment.Value, seenHash: false)) { return null; } - - // Now check all the intermediary statements. They all have to be of the form: - // - // hashCode = (hashCode op constant) op Hash(member) - // - // Or recursively built out of that. For example, in VB we sometimes generate: - // - // hashCode = Hash((hashCode op constant) op Hash(member)) - // - // So, after confirming we're assigning to our accumulator, we recursively break down - // the expression, looking for valid forms that only end up hashing a single field in - // some way. - for (var i = 1; i < statements.Length - 1; i++) - { - var statement = statements[i]; - if (statement is not IExpressionStatementOperation expressionStatement || - expressionStatement.Operation is not ISimpleAssignmentOperation simpleAssignment || - !IsLocalReference(simpleAssignment.Target, hashCodeVariable) || - !valueAnalyzer.TryAddHashedSymbol(simpleAssignment.Value, seenHash: false)) - { - return null; - } - } - - return valueAnalyzer.GetResult(); } - private bool OverridesSystemObject(IMethodSymbol? method) + return valueAnalyzer.GetResult(); + } + + private bool OverridesSystemObject(IMethodSymbol? method) + { + for (var current = method; current != null; current = current.OverriddenMethod) { - for (var current = method; current != null; current = current.OverriddenMethod) + if (Equals(_objectGetHashCodeMethod, current)) { - if (Equals(_objectGetHashCodeMethod, current)) - { - return true; - } + return true; } - - return false; } - private static bool IsLocalReference(IOperation value, ILocalSymbol accumulatorVariable) - => Unwrap(value) is ILocalReferenceOperation localReference && accumulatorVariable.Equals(localReference.Local); + return false; + } - /// - /// Matches positive and negative numeric literals. - /// - private static bool IsLiteralNumber(IOperation value) - { - value = Unwrap(value); - return value is IUnaryOperation unary - ? unary.OperatorKind == UnaryOperatorKind.Minus && IsLiteralNumber(unary.Operand) - : value.IsNumericLiteral(); - } + private static bool IsLocalReference(IOperation value, ILocalSymbol accumulatorVariable) + => Unwrap(value) is ILocalReferenceOperation localReference && accumulatorVariable.Equals(localReference.Local); + + /// + /// Matches positive and negative numeric literals. + /// + private static bool IsLiteralNumber(IOperation value) + { + value = Unwrap(value); + return value is IUnaryOperation unary + ? unary.OperatorKind == UnaryOperatorKind.Minus && IsLiteralNumber(unary.Operand) + : value.IsNumericLiteral(); + } - private static IOperation Unwrap(IOperation value) + private static IOperation Unwrap(IOperation value) + { + // ReSharper and VS generate different patterns for parentheses (which also depends on + // the particular parentheses settings the user has enabled). So just descend through + // any parentheses we see to create a uniform view of the code. + // + // Also, lots of operations in a GetHashCode impl will involve conversions all over the + // place (for example, some computations happen in 64bit, but convert to/from 32bit + // along the way). So we descend through conversions as well to create a uniform view + // of things. + while (true) { - // ReSharper and VS generate different patterns for parentheses (which also depends on - // the particular parentheses settings the user has enabled). So just descend through - // any parentheses we see to create a uniform view of the code. - // - // Also, lots of operations in a GetHashCode impl will involve conversions all over the - // place (for example, some computations happen in 64bit, but convert to/from 32bit - // along the way). So we descend through conversions as well to create a uniform view - // of things. - while (true) + if (value is IConversionOperation conversion) + { + value = conversion.Operand; + } + else if (value is IParenthesizedOperation parenthesized) + { + value = parenthesized.Operand; + } + else { - if (value is IConversionOperation conversion) - { - value = conversion.Operand; - } - else if (value is IParenthesizedOperation parenthesized) - { - value = parenthesized.Operand; - } - else - { - return value; - } + return value; } } } diff --git a/src/Analyzers/Core/Analyzers/IDEDiagnosticIdToOptionMappingHelper.cs b/src/Analyzers/Core/Analyzers/IDEDiagnosticIdToOptionMappingHelper.cs index d13318e87dfae..7c3a5d350984d 100644 --- a/src/Analyzers/Core/Analyzers/IDEDiagnosticIdToOptionMappingHelper.cs +++ b/src/Analyzers/Core/Analyzers/IDEDiagnosticIdToOptionMappingHelper.cs @@ -11,96 +11,95 @@ using System.Linq; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +/// +/// Helper type to map to an unique editorconfig code style option, if any, +/// such that diagnostic's severity can be configured in .editorconfig with an entry such as: +/// "%option_name% = %option_value%:%severity% +/// +internal static class IDEDiagnosticIdToOptionMappingHelper { - /// - /// Helper type to map to an unique editorconfig code style option, if any, - /// such that diagnostic's severity can be configured in .editorconfig with an entry such as: - /// "%option_name% = %option_value%:%severity% - /// - internal static class IDEDiagnosticIdToOptionMappingHelper - { - private static readonly ConcurrentDictionary> s_diagnosticIdToOptionMap = new(); - private static readonly ConcurrentDictionary>> s_diagnosticIdToLanguageSpecificOptionsMap = new(); - private static readonly ConcurrentDictionary> s_diagnosticIdToFadingOptionMap = new(); + private static readonly ConcurrentDictionary> s_diagnosticIdToOptionMap = new(); + private static readonly ConcurrentDictionary>> s_diagnosticIdToLanguageSpecificOptionsMap = new(); + private static readonly ConcurrentDictionary> s_diagnosticIdToFadingOptionMap = new(); - public static bool TryGetMappedOptions(string diagnosticId, string language, [NotNullWhen(true)] out ImmutableHashSet? options) - => s_diagnosticIdToOptionMap.TryGetValue(diagnosticId, out options) || - (s_diagnosticIdToLanguageSpecificOptionsMap.TryGetValue(language, out var map) && - map.TryGetValue(diagnosticId, out options)); + public static bool TryGetMappedOptions(string diagnosticId, string language, [NotNullWhen(true)] out ImmutableHashSet? options) + => s_diagnosticIdToOptionMap.TryGetValue(diagnosticId, out options) || + (s_diagnosticIdToLanguageSpecificOptionsMap.TryGetValue(language, out var map) && + map.TryGetValue(diagnosticId, out options)); - public static bool TryGetMappedFadingOption(string diagnosticId, [NotNullWhen(true)] out PerLanguageOption2? fadingOption) - => s_diagnosticIdToFadingOptionMap.TryGetValue(diagnosticId, out fadingOption); + public static bool TryGetMappedFadingOption(string diagnosticId, [NotNullWhen(true)] out PerLanguageOption2? fadingOption) + => s_diagnosticIdToFadingOptionMap.TryGetValue(diagnosticId, out fadingOption); - public static bool IsKnownIDEDiagnosticId(string diagnosticId) - => s_diagnosticIdToOptionMap.ContainsKey(diagnosticId) || - s_diagnosticIdToLanguageSpecificOptionsMap.Values.Any(map => map.ContainsKey(diagnosticId)); + public static bool IsKnownIDEDiagnosticId(string diagnosticId) + => s_diagnosticIdToOptionMap.ContainsKey(diagnosticId) || + s_diagnosticIdToLanguageSpecificOptionsMap.Values.Any(map => map.ContainsKey(diagnosticId)); - public static void AddOptionMapping(string diagnosticId, ImmutableHashSet options) - { - diagnosticId = diagnosticId ?? throw new ArgumentNullException(nameof(diagnosticId)); - options = options ?? throw new ArgumentNullException(nameof(options)); + public static void AddOptionMapping(string diagnosticId, ImmutableHashSet options) + { + diagnosticId = diagnosticId ?? throw new ArgumentNullException(nameof(diagnosticId)); + options = options ?? throw new ArgumentNullException(nameof(options)); - var groups = options.GroupBy(o => o.IsPerLanguage); - var multipleLanguagesOptionsBuilder = ImmutableHashSet.CreateBuilder(); - foreach (var group in groups) + var groups = options.GroupBy(o => o.IsPerLanguage); + var multipleLanguagesOptionsBuilder = ImmutableHashSet.CreateBuilder(); + foreach (var group in groups) + { + if (group.Key == true) { - if (group.Key == true) + foreach (var perLanguageValuedOption in group) { - foreach (var perLanguageValuedOption in group) - { - Debug.Assert(perLanguageValuedOption.IsPerLanguage); - multipleLanguagesOptionsBuilder.Add(perLanguageValuedOption); - } + Debug.Assert(perLanguageValuedOption.IsPerLanguage); + multipleLanguagesOptionsBuilder.Add(perLanguageValuedOption); } - else + } + else + { + var languageGroups = group.GroupBy(o => ((ISingleValuedOption)o).LanguageName); + foreach (var languageGroup in languageGroups) { - var languageGroups = group.GroupBy(o => ((ISingleValuedOption)o).LanguageName); - foreach (var languageGroup in languageGroups) + var language = languageGroup.Key; + if (language is null) { - var language = languageGroup.Key; - if (language is null) - { - foreach (var option in languageGroup) - { - multipleLanguagesOptionsBuilder.Add(option); - } - } - else + foreach (var option in languageGroup) { - var map = s_diagnosticIdToLanguageSpecificOptionsMap.GetOrAdd(language, _ => new ConcurrentDictionary>()); - AddOptionMapping(map, diagnosticId, languageGroup.ToImmutableHashSet()); + multipleLanguagesOptionsBuilder.Add(option); } } + else + { + var map = s_diagnosticIdToLanguageSpecificOptionsMap.GetOrAdd(language, _ => new ConcurrentDictionary>()); + AddOptionMapping(map, diagnosticId, languageGroup.ToImmutableHashSet()); + } } } - - if (multipleLanguagesOptionsBuilder.Count > 0) - { - AddOptionMapping(s_diagnosticIdToOptionMap, diagnosticId, multipleLanguagesOptionsBuilder.ToImmutableHashSet()); - } } - private static void AddOptionMapping(ConcurrentDictionary> map, string diagnosticId, ImmutableHashSet options) + if (multipleLanguagesOptionsBuilder.Count > 0) { - // Verify that the option is either being added for the first time, or the existing option is already the same. - // Latter can happen in tests as we re-instantiate the analyzer for every test, which attempts to add the mapping every time. - Debug.Assert(!map.TryGetValue(diagnosticId, out var existingOptions) || options.SetEquals(existingOptions)); - Debug.Assert(options.All(option => option.Definition.IsEditorConfigOption)); - - map.TryAdd(diagnosticId, options); + AddOptionMapping(s_diagnosticIdToOptionMap, diagnosticId, multipleLanguagesOptionsBuilder.ToImmutableHashSet()); } + } - public static void AddFadingOptionMapping(string diagnosticId, PerLanguageOption2 fadingOption) - { - diagnosticId = diagnosticId ?? throw new ArgumentNullException(nameof(diagnosticId)); - fadingOption = fadingOption ?? throw new ArgumentNullException(nameof(fadingOption)); + private static void AddOptionMapping(ConcurrentDictionary> map, string diagnosticId, ImmutableHashSet options) + { + // Verify that the option is either being added for the first time, or the existing option is already the same. + // Latter can happen in tests as we re-instantiate the analyzer for every test, which attempts to add the mapping every time. + Debug.Assert(!map.TryGetValue(diagnosticId, out var existingOptions) || options.SetEquals(existingOptions)); + Debug.Assert(options.All(option => option.Definition.IsEditorConfigOption)); - // Verify that the option is either being added for the first time, or the existing option is already the same. - // Latter can happen in tests as we re-instantiate the analyzer for every test, which attempts to add the mapping every time. - Debug.Assert(!s_diagnosticIdToFadingOptionMap.TryGetValue(diagnosticId, out var existingOption) || existingOption.Equals(fadingOption)); + map.TryAdd(diagnosticId, options); + } - s_diagnosticIdToFadingOptionMap.TryAdd(diagnosticId, fadingOption); - } + public static void AddFadingOptionMapping(string diagnosticId, PerLanguageOption2 fadingOption) + { + diagnosticId = diagnosticId ?? throw new ArgumentNullException(nameof(diagnosticId)); + fadingOption = fadingOption ?? throw new ArgumentNullException(nameof(fadingOption)); + + // Verify that the option is either being added for the first time, or the existing option is already the same. + // Latter can happen in tests as we re-instantiate the analyzer for every test, which attempts to add the mapping every time. + Debug.Assert(!s_diagnosticIdToFadingOptionMap.TryGetValue(diagnosticId, out var existingOption) || existingOption.Equals(fadingOption)); + + s_diagnosticIdToFadingOptionMap.TryAdd(diagnosticId, fadingOption); } } diff --git a/src/Analyzers/Core/Analyzers/IDEDiagnosticIds.cs b/src/Analyzers/Core/Analyzers/IDEDiagnosticIds.cs index 1153a4df4b803..fefb859cdeb09 100644 --- a/src/Analyzers/Core/Analyzers/IDEDiagnosticIds.cs +++ b/src/Analyzers/Core/Analyzers/IDEDiagnosticIds.cs @@ -4,221 +4,220 @@ using Microsoft.CodeAnalysis.Formatting; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal static class IDEDiagnosticIds { - internal static class IDEDiagnosticIds - { - public const string SimplifyNamesDiagnosticId = "IDE0001"; - public const string SimplifyMemberAccessDiagnosticId = "IDE0002"; - public const string RemoveThisOrMeQualificationDiagnosticId = "IDE0003"; - public const string RemoveUnnecessaryCastDiagnosticId = "IDE0004"; - public const string RemoveUnnecessaryImportsDiagnosticId = "IDE0005"; - public const string IntellisenseBuildFailedDiagnosticId = "IDE0006"; - public const string UseImplicitTypeDiagnosticId = "IDE0007"; - public const string UseExplicitTypeDiagnosticId = "IDE0008"; - public const string AddThisOrMeQualificationDiagnosticId = "IDE0009"; - public const string PopulateSwitchStatementDiagnosticId = "IDE0010"; - public const string AddBracesDiagnosticId = "IDE0011"; + public const string SimplifyNamesDiagnosticId = "IDE0001"; + public const string SimplifyMemberAccessDiagnosticId = "IDE0002"; + public const string RemoveThisOrMeQualificationDiagnosticId = "IDE0003"; + public const string RemoveUnnecessaryCastDiagnosticId = "IDE0004"; + public const string RemoveUnnecessaryImportsDiagnosticId = "IDE0005"; + public const string IntellisenseBuildFailedDiagnosticId = "IDE0006"; + public const string UseImplicitTypeDiagnosticId = "IDE0007"; + public const string UseExplicitTypeDiagnosticId = "IDE0008"; + public const string AddThisOrMeQualificationDiagnosticId = "IDE0009"; + public const string PopulateSwitchStatementDiagnosticId = "IDE0010"; + public const string AddBracesDiagnosticId = "IDE0011"; - // IDE0012-IDE0015 deprecated and replaced with PreferBuiltInOrFrameworkTypeDiagnosticId (IDE0049) - // public const string PreferIntrinsicPredefinedTypeInDeclarationsDiagnosticId = "IDE0012"; - // public const string PreferIntrinsicPredefinedTypeInMemberAccessDiagnosticId = "IDE0013"; - // public const string PreferFrameworkTypeInDeclarationsDiagnosticId = "IDE0014"; - // public const string PreferFrameworkTypeInMemberAccessDiagnosticId = "IDE0015"; + // IDE0012-IDE0015 deprecated and replaced with PreferBuiltInOrFrameworkTypeDiagnosticId (IDE0049) + // public const string PreferIntrinsicPredefinedTypeInDeclarationsDiagnosticId = "IDE0012"; + // public const string PreferIntrinsicPredefinedTypeInMemberAccessDiagnosticId = "IDE0013"; + // public const string PreferFrameworkTypeInDeclarationsDiagnosticId = "IDE0014"; + // public const string PreferFrameworkTypeInMemberAccessDiagnosticId = "IDE0015"; - public const string UseThrowExpressionDiagnosticId = "IDE0016"; - public const string UseObjectInitializerDiagnosticId = "IDE0017"; - public const string InlineDeclarationDiagnosticId = "IDE0018"; - public const string InlineAsTypeCheckId = "IDE0019"; - public const string InlineIsTypeCheckId = "IDE0020"; + public const string UseThrowExpressionDiagnosticId = "IDE0016"; + public const string UseObjectInitializerDiagnosticId = "IDE0017"; + public const string InlineDeclarationDiagnosticId = "IDE0018"; + public const string InlineAsTypeCheckId = "IDE0019"; + public const string InlineIsTypeCheckId = "IDE0020"; - public const string UseExpressionBodyForConstructorsDiagnosticId = "IDE0021"; - public const string UseExpressionBodyForMethodsDiagnosticId = "IDE0022"; - public const string UseExpressionBodyForConversionOperatorsDiagnosticId = "IDE0023"; - public const string UseExpressionBodyForOperatorsDiagnosticId = "IDE0024"; - public const string UseExpressionBodyForPropertiesDiagnosticId = "IDE0025"; - public const string UseExpressionBodyForIndexersDiagnosticId = "IDE0026"; - public const string UseExpressionBodyForAccessorsDiagnosticId = "IDE0027"; + public const string UseExpressionBodyForConstructorsDiagnosticId = "IDE0021"; + public const string UseExpressionBodyForMethodsDiagnosticId = "IDE0022"; + public const string UseExpressionBodyForConversionOperatorsDiagnosticId = "IDE0023"; + public const string UseExpressionBodyForOperatorsDiagnosticId = "IDE0024"; + public const string UseExpressionBodyForPropertiesDiagnosticId = "IDE0025"; + public const string UseExpressionBodyForIndexersDiagnosticId = "IDE0026"; + public const string UseExpressionBodyForAccessorsDiagnosticId = "IDE0027"; - public const string UseCollectionInitializerDiagnosticId = "IDE0028"; + public const string UseCollectionInitializerDiagnosticId = "IDE0028"; - public const string UseCoalesceExpressionForTernaryConditionalCheckDiagnosticId = "IDE0029"; - public const string UseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticId = "IDE0030"; + public const string UseCoalesceExpressionForTernaryConditionalCheckDiagnosticId = "IDE0029"; + public const string UseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticId = "IDE0030"; - public const string UseNullPropagationDiagnosticId = "IDE0031"; + public const string UseNullPropagationDiagnosticId = "IDE0031"; - public const string UseAutoPropertyDiagnosticId = "IDE0032"; + public const string UseAutoPropertyDiagnosticId = "IDE0032"; - public const string UseExplicitTupleNameDiagnosticId = "IDE0033"; + public const string UseExplicitTupleNameDiagnosticId = "IDE0033"; - public const string UseDefaultLiteralDiagnosticId = "IDE0034"; + public const string UseDefaultLiteralDiagnosticId = "IDE0034"; - public const string RemoveUnreachableCodeDiagnosticId = "IDE0035"; + public const string RemoveUnreachableCodeDiagnosticId = "IDE0035"; - public const string OrderModifiersDiagnosticId = "IDE0036"; + public const string OrderModifiersDiagnosticId = "IDE0036"; - public const string UseInferredMemberNameDiagnosticId = "IDE0037"; + public const string UseInferredMemberNameDiagnosticId = "IDE0037"; - public const string InlineIsTypeWithoutNameCheckDiagnosticsId = "IDE0038"; + public const string InlineIsTypeWithoutNameCheckDiagnosticsId = "IDE0038"; - public const string UseLocalFunctionDiagnosticId = "IDE0039"; + public const string UseLocalFunctionDiagnosticId = "IDE0039"; - public const string AddAccessibilityModifiersDiagnosticId = "IDE0040"; + public const string AddAccessibilityModifiersDiagnosticId = "IDE0040"; - public const string UseIsNullCheckDiagnosticId = "IDE0041"; + public const string UseIsNullCheckDiagnosticId = "IDE0041"; - public const string UseDeconstructionDiagnosticId = "IDE0042"; + public const string UseDeconstructionDiagnosticId = "IDE0042"; - public const string ValidateFormatStringDiagnosticID = "IDE0043"; + public const string ValidateFormatStringDiagnosticID = "IDE0043"; - public const string MakeFieldReadonlyDiagnosticId = "IDE0044"; + public const string MakeFieldReadonlyDiagnosticId = "IDE0044"; - public const string UseConditionalExpressionForAssignmentDiagnosticId = "IDE0045"; - public const string UseConditionalExpressionForReturnDiagnosticId = "IDE0046"; + public const string UseConditionalExpressionForAssignmentDiagnosticId = "IDE0045"; + public const string UseConditionalExpressionForReturnDiagnosticId = "IDE0046"; - public const string RemoveUnnecessaryParenthesesDiagnosticId = "IDE0047"; - public const string AddRequiredParenthesesDiagnosticId = "IDE0048"; + public const string RemoveUnnecessaryParenthesesDiagnosticId = "IDE0047"; + public const string AddRequiredParenthesesDiagnosticId = "IDE0048"; - public const string PreferBuiltInOrFrameworkTypeDiagnosticId = "IDE0049"; + public const string PreferBuiltInOrFrameworkTypeDiagnosticId = "IDE0049"; - // public const string ConvertAnonymousTypeToTupleDiagnosticId = "IDE0050"; + // public const string ConvertAnonymousTypeToTupleDiagnosticId = "IDE0050"; - public const string RemoveUnusedMembersDiagnosticId = "IDE0051"; - public const string RemoveUnreadMembersDiagnosticId = "IDE0052"; + public const string RemoveUnusedMembersDiagnosticId = "IDE0051"; + public const string RemoveUnreadMembersDiagnosticId = "IDE0052"; - public const string UseExpressionBodyForLambdaExpressionsDiagnosticId = "IDE0053"; + public const string UseExpressionBodyForLambdaExpressionsDiagnosticId = "IDE0053"; - public const string UseCompoundAssignmentDiagnosticId = "IDE0054"; + public const string UseCompoundAssignmentDiagnosticId = "IDE0054"; - public const string FormattingDiagnosticId = FormattingDiagnosticIds.FormattingDiagnosticId; + public const string FormattingDiagnosticId = FormattingDiagnosticIds.FormattingDiagnosticId; - public const string UseIndexOperatorDiagnosticId = "IDE0056"; - public const string UseRangeOperatorDiagnosticId = "IDE0057"; + public const string UseIndexOperatorDiagnosticId = "IDE0056"; + public const string UseRangeOperatorDiagnosticId = "IDE0057"; - public const string ExpressionValueIsUnusedDiagnosticId = "IDE0058"; - public const string ValueAssignedIsUnusedDiagnosticId = "IDE0059"; - public const string UnusedParameterDiagnosticId = "IDE0060"; + public const string ExpressionValueIsUnusedDiagnosticId = "IDE0058"; + public const string ValueAssignedIsUnusedDiagnosticId = "IDE0059"; + public const string UnusedParameterDiagnosticId = "IDE0060"; - // Conceptually belongs with IDE0021-IDE0027 & IDE0053, but is here because it was added later - public const string UseExpressionBodyForLocalFunctionsDiagnosticId = "IDE0061"; + // Conceptually belongs with IDE0021-IDE0027 & IDE0053, but is here because it was added later + public const string UseExpressionBodyForLocalFunctionsDiagnosticId = "IDE0061"; - public const string MakeLocalFunctionStaticDiagnosticId = "IDE0062"; - public const string UseSimpleUsingStatementDiagnosticId = "IDE0063"; + public const string MakeLocalFunctionStaticDiagnosticId = "IDE0062"; + public const string UseSimpleUsingStatementDiagnosticId = "IDE0063"; - public const string MakeStructFieldsWritable = "IDE0064"; + public const string MakeStructFieldsWritable = "IDE0064"; - public const string MoveMisplacedUsingDirectivesDiagnosticId = "IDE0065"; + public const string MoveMisplacedUsingDirectivesDiagnosticId = "IDE0065"; - public const string ConvertSwitchStatementToExpressionDiagnosticId = "IDE0066"; + public const string ConvertSwitchStatementToExpressionDiagnosticId = "IDE0066"; - // IDE0067-IDE0069 deprecated in favor of CA2000 and CA2213 - // public const string DisposeObjectsBeforeLosingScopeDiagnosticId = "IDE0067"; - // public const string UseRecommendedDisposePatternDiagnosticId = "IDE0068"; - // public const string DisposableFieldsShouldBeDisposedDiagnosticId = "IDE0069"; + // IDE0067-IDE0069 deprecated in favor of CA2000 and CA2213 + // public const string DisposeObjectsBeforeLosingScopeDiagnosticId = "IDE0067"; + // public const string UseRecommendedDisposePatternDiagnosticId = "IDE0068"; + // public const string DisposableFieldsShouldBeDisposedDiagnosticId = "IDE0069"; - public const string UseSystemHashCode = "IDE0070"; + public const string UseSystemHashCode = "IDE0070"; - public const string SimplifyInterpolationId = "IDE0071"; + public const string SimplifyInterpolationId = "IDE0071"; - public const string PopulateSwitchExpressionDiagnosticId = "IDE0072"; + public const string PopulateSwitchExpressionDiagnosticId = "IDE0072"; - /// - /// Reported when a file header is missing or does not match the expected string. - /// - public const string FileHeaderMismatch = "IDE0073"; + /// + /// Reported when a file header is missing or does not match the expected string. + /// + public const string FileHeaderMismatch = "IDE0073"; - public const string UseCoalesceCompoundAssignmentDiagnosticId = "IDE0074"; + public const string UseCoalesceCompoundAssignmentDiagnosticId = "IDE0074"; - public const string SimplifyConditionalExpressionDiagnosticId = "IDE0075"; + public const string SimplifyConditionalExpressionDiagnosticId = "IDE0075"; - public const string InvalidSuppressMessageAttributeDiagnosticId = "IDE0076"; - public const string LegacyFormatSuppressMessageAttributeDiagnosticId = "IDE0077"; + public const string InvalidSuppressMessageAttributeDiagnosticId = "IDE0076"; + public const string LegacyFormatSuppressMessageAttributeDiagnosticId = "IDE0077"; - public const string UsePatternCombinatorsDiagnosticId = "IDE0078"; + public const string UsePatternCombinatorsDiagnosticId = "IDE0078"; - public const string RemoveUnnecessarySuppressionDiagnosticId = "IDE0079"; + public const string RemoveUnnecessarySuppressionDiagnosticId = "IDE0079"; - public const string RemoveConfusingSuppressionForIsExpressionDiagnosticId = "IDE0080"; - public const string RemoveUnnecessaryByValDiagnosticId = "IDE0081"; + public const string RemoveConfusingSuppressionForIsExpressionDiagnosticId = "IDE0080"; + public const string RemoveUnnecessaryByValDiagnosticId = "IDE0081"; - public const string ConvertTypeOfToNameOfDiagnosticId = "IDE0082"; + public const string ConvertTypeOfToNameOfDiagnosticId = "IDE0082"; - public const string UseNotPatternDiagnosticId = "IDE0083"; - public const string UseIsNotExpressionDiagnosticId = "IDE0084"; + public const string UseNotPatternDiagnosticId = "IDE0083"; + public const string UseIsNotExpressionDiagnosticId = "IDE0084"; - public const string UseImplicitObjectCreationDiagnosticId = "IDE0090"; + public const string UseImplicitObjectCreationDiagnosticId = "IDE0090"; - public const string RemoveRedundantEqualityDiagnosticId = "IDE0100"; + public const string RemoveRedundantEqualityDiagnosticId = "IDE0100"; - public const string RemoveUnnecessaryDiscardDesignationDiagnosticId = "IDE0110"; + public const string RemoveUnnecessaryDiscardDesignationDiagnosticId = "IDE0110"; - public const string SimplifyLinqExpressionDiagnosticId = "IDE0120"; + public const string SimplifyLinqExpressionDiagnosticId = "IDE0120"; - public const string MatchFolderAndNamespaceDiagnosticId = "IDE0130"; + public const string MatchFolderAndNamespaceDiagnosticId = "IDE0130"; - public const string SimplifyObjectCreationDiagnosticId = "IDE0140"; + public const string SimplifyObjectCreationDiagnosticId = "IDE0140"; - public const string UseNullCheckOverTypeCheckDiagnosticId = "IDE0150"; + public const string UseNullCheckOverTypeCheckDiagnosticId = "IDE0150"; - public const string UseBlockScopedNamespaceDiagnosticId = "IDE0160"; - public const string UseFileScopedNamespaceDiagnosticId = "IDE0161"; + public const string UseBlockScopedNamespaceDiagnosticId = "IDE0160"; + public const string UseFileScopedNamespaceDiagnosticId = "IDE0161"; - public const string SimplifyPropertyPatternDiagnosticId = "IDE0170"; + public const string SimplifyPropertyPatternDiagnosticId = "IDE0170"; - public const string UseTupleSwapDiagnosticId = "IDE0180"; + public const string UseTupleSwapDiagnosticId = "IDE0180"; - // Don't use "IDE0190". It corresponds to the deleted field UseParameterNullCheckingId which was previously shipped. + // Don't use "IDE0190". It corresponds to the deleted field UseParameterNullCheckingId which was previously shipped. - public const string RemoveUnnecessaryLambdaExpressionDiagnosticId = "IDE0200"; + public const string RemoveUnnecessaryLambdaExpressionDiagnosticId = "IDE0200"; - public const string UseTopLevelStatementsId = "IDE0210"; - public const string UseProgramMainId = "IDE0211"; + public const string UseTopLevelStatementsId = "IDE0210"; + public const string UseProgramMainId = "IDE0211"; - public const string ForEachCastDiagnosticId = "IDE0220"; + public const string ForEachCastDiagnosticId = "IDE0220"; - public const string UseUtf8StringLiteralDiagnosticId = "IDE0230"; + public const string UseUtf8StringLiteralDiagnosticId = "IDE0230"; - public const string RemoveRedundantNullableDirectiveDiagnosticId = "IDE0240"; - public const string RemoveUnnecessaryNullableDirectiveDiagnosticId = "IDE0241"; + public const string RemoveRedundantNullableDirectiveDiagnosticId = "IDE0240"; + public const string RemoveUnnecessaryNullableDirectiveDiagnosticId = "IDE0241"; - public const string MakeStructReadOnlyDiagnosticId = "IDE0250"; - public const string MakeStructMemberReadOnlyDiagnosticId = "IDE0251"; + public const string MakeStructReadOnlyDiagnosticId = "IDE0250"; + public const string MakeStructMemberReadOnlyDiagnosticId = "IDE0251"; - public const string UsePatternMatchingAsAndMemberAccessDiagnosticId = "IDE0260"; + public const string UsePatternMatchingAsAndMemberAccessDiagnosticId = "IDE0260"; - public const string UseCoalesceExpressionForIfNullCheckDiagnosticId = "IDE0270"; + public const string UseCoalesceExpressionForIfNullCheckDiagnosticId = "IDE0270"; - public const string UseNameofInAttributeDiagnosticId = "IDE0280"; + public const string UseNameofInAttributeDiagnosticId = "IDE0280"; - public const string UsePrimaryConstructorDiagnosticId = "IDE0290"; + public const string UsePrimaryConstructorDiagnosticId = "IDE0290"; - public const string UseCollectionExpressionForArrayDiagnosticId = "IDE0300"; - public const string UseCollectionExpressionForEmptyDiagnosticId = "IDE0301"; - public const string UseCollectionExpressionForStackAllocDiagnosticId = "IDE0302"; - public const string UseCollectionExpressionForCreateDiagnosticId = "IDE0303"; - public const string UseCollectionExpressionForBuilderDiagnosticId = "IDE0304"; - public const string UseCollectionExpressionForFluentDiagnosticId = "IDE0305"; + public const string UseCollectionExpressionForArrayDiagnosticId = "IDE0300"; + public const string UseCollectionExpressionForEmptyDiagnosticId = "IDE0301"; + public const string UseCollectionExpressionForStackAllocDiagnosticId = "IDE0302"; + public const string UseCollectionExpressionForCreateDiagnosticId = "IDE0303"; + public const string UseCollectionExpressionForBuilderDiagnosticId = "IDE0304"; + public const string UseCollectionExpressionForFluentDiagnosticId = "IDE0305"; - // Analyzer error Ids - public const string AnalyzerChangedId = "IDE1001"; - public const string AnalyzerDependencyConflictId = "IDE1002"; - public const string MissingAnalyzerReferenceId = "IDE1003"; - // public const string ErrorReadingRulesetId = "IDE1004"; - public const string InvokeDelegateWithConditionalAccessId = "IDE1005"; - public const string NamingRuleId = "IDE1006"; - public const string UnboundIdentifierId = "IDE1007"; + // Analyzer error Ids + public const string AnalyzerChangedId = "IDE1001"; + public const string AnalyzerDependencyConflictId = "IDE1002"; + public const string MissingAnalyzerReferenceId = "IDE1003"; + // public const string ErrorReadingRulesetId = "IDE1004"; + public const string InvokeDelegateWithConditionalAccessId = "IDE1005"; + public const string NamingRuleId = "IDE1006"; + public const string UnboundIdentifierId = "IDE1007"; - // Reserved for workspace error ids IDE1100-IDE1200 (see WorkspaceDiagnosticDescriptors) + // Reserved for workspace error ids IDE1100-IDE1200 (see WorkspaceDiagnosticDescriptors) - // Experimental features + // Experimental features - // 2000 range for experimental formatting enforcement - public const string MultipleBlankLinesDiagnosticId = "IDE2000"; - public const string EmbeddedStatementPlacementDiagnosticId = "IDE2001"; - public const string ConsecutiveBracePlacementDiagnosticId = "IDE2002"; - public const string ConsecutiveStatementPlacementDiagnosticId = "IDE2003"; - public const string ConstructorInitializerPlacementDiagnosticId = "IDE2004"; - public const string ConditionalExpressionPlacementDiagnosticId = "IDE2005"; - public const string ArrowExpressionClausePlacementDiagnosticId = "IDE2006"; - } + // 2000 range for experimental formatting enforcement + public const string MultipleBlankLinesDiagnosticId = "IDE2000"; + public const string EmbeddedStatementPlacementDiagnosticId = "IDE2001"; + public const string ConsecutiveBracePlacementDiagnosticId = "IDE2002"; + public const string ConsecutiveStatementPlacementDiagnosticId = "IDE2003"; + public const string ConstructorInitializerPlacementDiagnosticId = "IDE2004"; + public const string ConditionalExpressionPlacementDiagnosticId = "IDE2005"; + public const string ArrowExpressionClausePlacementDiagnosticId = "IDE2006"; } diff --git a/src/Analyzers/Core/Analyzers/MakeFieldReadonly/AbstractMakeFieldReadonlyDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/MakeFieldReadonly/AbstractMakeFieldReadonlyDiagnosticAnalyzer.cs index a0aa4d80baf76..aa155a9283b4a 100644 --- a/src/Analyzers/Core/Analyzers/MakeFieldReadonly/AbstractMakeFieldReadonlyDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/MakeFieldReadonly/AbstractMakeFieldReadonlyDiagnosticAnalyzer.cs @@ -12,289 +12,289 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.MakeFieldReadonly +namespace Microsoft.CodeAnalysis.MakeFieldReadonly; + +internal abstract class AbstractMakeFieldReadonlyDiagnosticAnalyzer< + TSyntaxKind, TThisExpression> + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct, Enum + where TThisExpression : SyntaxNode { - internal abstract class AbstractMakeFieldReadonlyDiagnosticAnalyzer< - TSyntaxKind, TThisExpression> - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TSyntaxKind : struct, Enum - where TThisExpression : SyntaxNode + protected AbstractMakeFieldReadonlyDiagnosticAnalyzer() + : base( + IDEDiagnosticIds.MakeFieldReadonlyDiagnosticId, + EnforceOnBuildValues.MakeFieldReadonly, + CodeStyleOptions2.PreferReadonly, + new LocalizableResourceString(nameof(AnalyzersResources.Add_readonly_modifier), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Make_field_readonly), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - protected AbstractMakeFieldReadonlyDiagnosticAnalyzer() - : base( - IDEDiagnosticIds.MakeFieldReadonlyDiagnosticId, - EnforceOnBuildValues.MakeFieldReadonly, - CodeStyleOptions2.PreferReadonly, - new LocalizableResourceString(nameof(AnalyzersResources.Add_readonly_modifier), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Make_field_readonly), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + } - protected abstract ISyntaxKinds SyntaxKinds { get; } - protected abstract bool IsWrittenTo(SemanticModel semanticModel, TThisExpression expression, CancellationToken cancellationToken); + protected abstract ISyntaxKinds SyntaxKinds { get; } + protected abstract bool IsWrittenTo(SemanticModel semanticModel, TThisExpression expression, CancellationToken cancellationToken); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; - // We need to analyze generated code to get callbacks for read/writes to non-generated members in generated code. - protected override GeneratedCodeAnalysisFlags GeneratedCodeAnalysisFlags => GeneratedCodeAnalysisFlags.Analyze; + // We need to analyze generated code to get callbacks for read/writes to non-generated members in generated code. + protected override GeneratedCodeAnalysisFlags GeneratedCodeAnalysisFlags => GeneratedCodeAnalysisFlags.Analyze; - protected override void InitializeWorker(AnalysisContext context) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - context.RegisterCompilationStartAction(context => + // State map for fields: + // 'isCandidate' : Indicates whether the field is a candidate to be made readonly based on it's options. + // 'written' : Indicates if there are any writes to the field outside the constructor and field initializer. + var fieldStateMap = new ConcurrentDictionary(); + + var threadStaticAttribute = context.Compilation.ThreadStaticAttributeType(); + var dataContractAttribute = context.Compilation.DataContractAttribute(); + var dataMemberAttribute = context.Compilation.DataMemberAttribute(); + + // We register following actions in the compilation: + // 1. A symbol action for field symbols to ensure the field state is initialized for every field in + // the compilation. + // 2. An operation action for field references to detect if a candidate field is written outside + // constructor and field initializer, and update field state accordingly. + // 3. A symbol start/end action for named types to report diagnostics for candidate fields that were + // not written outside constructor and field initializer. + + context.RegisterSymbolAction(AnalyzeFieldSymbol, SymbolKind.Field); + + context.RegisterSymbolStartAction(context => { - // State map for fields: - // 'isCandidate' : Indicates whether the field is a candidate to be made readonly based on it's options. - // 'written' : Indicates if there are any writes to the field outside the constructor and field initializer. - var fieldStateMap = new ConcurrentDictionary(); - - var threadStaticAttribute = context.Compilation.ThreadStaticAttributeType(); - var dataContractAttribute = context.Compilation.DataContractAttribute(); - var dataMemberAttribute = context.Compilation.DataMemberAttribute(); - - // We register following actions in the compilation: - // 1. A symbol action for field symbols to ensure the field state is initialized for every field in - // the compilation. - // 2. An operation action for field references to detect if a candidate field is written outside - // constructor and field initializer, and update field state accordingly. - // 3. A symbol start/end action for named types to report diagnostics for candidate fields that were - // not written outside constructor and field initializer. - - context.RegisterSymbolAction(AnalyzeFieldSymbol, SymbolKind.Field); - - context.RegisterSymbolStartAction(context => - { - if (!ShouldAnalyze(context, (INamedTypeSymbol)context.Symbol)) - return; + if (!ShouldAnalyze(context, (INamedTypeSymbol)context.Symbol)) + return; - context.RegisterOperationAction(AnalyzeOperation, OperationKind.FieldReference); + context.RegisterOperationAction(AnalyzeOperation, OperationKind.FieldReference); - // Can't allow changing the fields to readonly if the struct overwrites itself. e.g. `this = default;` - var writesToThis = false; - context.RegisterSyntaxNodeAction(context => - { - writesToThis = writesToThis || IsWrittenTo(context.SemanticModel, (TThisExpression)context.Node, context.CancellationToken); - }, SyntaxKinds.Convert(SyntaxKinds.ThisExpression)); + // Can't allow changing the fields to readonly if the struct overwrites itself. e.g. `this = default;` + var writesToThis = false; + context.RegisterSyntaxNodeAction(context => + { + writesToThis = writesToThis || IsWrittenTo(context.SemanticModel, (TThisExpression)context.Node, context.CancellationToken); + }, SyntaxKinds.Convert(SyntaxKinds.ThisExpression)); - context.RegisterSymbolEndAction(context => - { - if (writesToThis) - return; + context.RegisterSymbolEndAction(context => + { + if (writesToThis) + return; - OnSymbolEnd(context); - }); - }, SymbolKind.NamedType); + OnSymbolEnd(context); + }); + }, SymbolKind.NamedType); - return; + return; - // Local functions. - void AnalyzeFieldSymbol(SymbolAnalysisContext symbolContext) - { - var field = (IFieldSymbol)symbolContext.Symbol; - if (!symbolContext.ShouldAnalyzeLocation(GetDiagnosticLocation(field))) - return; + // Local functions. + void AnalyzeFieldSymbol(SymbolAnalysisContext symbolContext) + { + var field = (IFieldSymbol)symbolContext.Symbol; + if (!symbolContext.ShouldAnalyzeLocation(GetDiagnosticLocation(field))) + return; - _ = TryGetOrInitializeFieldState(field, symbolContext.Options, symbolContext.CancellationToken); - } + _ = TryGetOrInitializeFieldState(field, symbolContext.Options, symbolContext.CancellationToken); + } - void AnalyzeOperation(OperationAnalysisContext operationContext) - { - var fieldReference = (IFieldReferenceOperation)operationContext.Operation; - var (isCandidate, written) = TryGetOrInitializeFieldState(fieldReference.Field, operationContext.Options, operationContext.CancellationToken); + void AnalyzeOperation(OperationAnalysisContext operationContext) + { + var fieldReference = (IFieldReferenceOperation)operationContext.Operation; + var (isCandidate, written) = TryGetOrInitializeFieldState(fieldReference.Field, operationContext.Options, operationContext.CancellationToken); - // Ignore fields that are not candidates or have already been written outside the constructor/field initializer. - if (!isCandidate || written) - return; + // Ignore fields that are not candidates or have already been written outside the constructor/field initializer. + if (!isCandidate || written) + return; - // Check if this is a field write outside constructor and field initializer, and update field state accordingly. - if (!IsFieldWrite(fieldReference, operationContext.ContainingSymbol)) - return; + // Check if this is a field write outside constructor and field initializer, and update field state accordingly. + if (!IsFieldWrite(fieldReference, operationContext.ContainingSymbol)) + return; - UpdateFieldStateOnWrite(fieldReference.Field); - } + UpdateFieldStateOnWrite(fieldReference.Field); + } - void OnSymbolEnd(SymbolAnalysisContext symbolEndContext) + void OnSymbolEnd(SymbolAnalysisContext symbolEndContext) + { + // Report diagnostics for candidate fields that are not written outside constructor and field initializer. + var members = ((INamedTypeSymbol)symbolEndContext.Symbol).GetMembers(); + foreach (var member in members) { - // Report diagnostics for candidate fields that are not written outside constructor and field initializer. - var members = ((INamedTypeSymbol)symbolEndContext.Symbol).GetMembers(); - foreach (var member in members) + if (member is IFieldSymbol field && fieldStateMap.TryRemove(field.OriginalDefinition, out var value)) { - if (member is IFieldSymbol field && fieldStateMap.TryRemove(field.OriginalDefinition, out var value)) + var (isCandidate, written) = value; + if (isCandidate && !written) { - var (isCandidate, written) = value; - if (isCandidate && !written) - { - var option = GetCodeStyleOption(field, symbolEndContext.Options, out var location); - var diagnostic = DiagnosticHelper.Create( - Descriptor, - location, - option.Notification, - additionalLocations: null, - properties: null); - symbolEndContext.ReportDiagnostic(diagnostic); - } + var option = GetCodeStyleOption(field, symbolEndContext.Options, out var location); + var diagnostic = DiagnosticHelper.Create( + Descriptor, + location, + option.Notification, + context.Options, + additionalLocations: null, + properties: null); + symbolEndContext.ReportDiagnostic(diagnostic); } } } + } - bool ShouldAnalyze(SymbolStartAnalysisContext context, INamedTypeSymbol namedType) + bool ShouldAnalyze(SymbolStartAnalysisContext context, INamedTypeSymbol namedType) + { + // Check if we have at least one candidate field in analysis scope. + foreach (var member in namedType.GetMembers()) { - // Check if we have at least one candidate field in analysis scope. - foreach (var member in namedType.GetMembers()) + if (member is IFieldSymbol field + && IsCandidateField(field, threadStaticAttribute, dataContractAttribute, dataMemberAttribute) + && GetDiagnosticLocation(field) is { } location + && context.ShouldAnalyzeLocation(location)) { - if (member is IFieldSymbol field - && IsCandidateField(field, threadStaticAttribute, dataContractAttribute, dataMemberAttribute) - && GetDiagnosticLocation(field) is { } location - && context.ShouldAnalyzeLocation(location)) - { - return true; - } + return true; } + } - // We have to analyze nested types if containing type contains a candidate field in analysis scope. - if (namedType.ContainingType is { } containingType) - return ShouldAnalyze(context, containingType); + // We have to analyze nested types if containing type contains a candidate field in analysis scope. + if (namedType.ContainingType is { } containingType) + return ShouldAnalyze(context, containingType); + + return false; + } + static bool IsCandidateField(IFieldSymbol symbol, INamedTypeSymbol? threadStaticAttribute, INamedTypeSymbol? dataContractAttribute, INamedTypeSymbol? dataMemberAttribute) + => symbol is + { + DeclaredAccessibility: Accessibility.Private, + IsReadOnly: false, + IsConst: false, + IsImplicitlyDeclared: false, + Locations.Length: 1, + IsFixedSizeBuffer: false, + } && + symbol.Type.IsMutableValueType() == false && + !symbol.GetAttributes().Any( + static (a, threadStaticAttribute) => SymbolEqualityComparer.Default.Equals(a.AttributeClass, threadStaticAttribute), + threadStaticAttribute) && + !IsDataContractSerializable(symbol, dataContractAttribute, dataMemberAttribute); + + static bool IsDataContractSerializable(IFieldSymbol symbol, INamedTypeSymbol? dataContractAttribute, INamedTypeSymbol? dataMemberAttribute) + { + if (dataContractAttribute is null || dataMemberAttribute is null) return false; - } - static bool IsCandidateField(IFieldSymbol symbol, INamedTypeSymbol? threadStaticAttribute, INamedTypeSymbol? dataContractAttribute, INamedTypeSymbol? dataMemberAttribute) - => symbol is - { - DeclaredAccessibility: Accessibility.Private, - IsReadOnly: false, - IsConst: false, - IsImplicitlyDeclared: false, - Locations.Length: 1, - IsFixedSizeBuffer: false, - } && - symbol.Type.IsMutableValueType() == false && - !symbol.GetAttributes().Any( - static (a, threadStaticAttribute) => SymbolEqualityComparer.Default.Equals(a.AttributeClass, threadStaticAttribute), - threadStaticAttribute) && - !IsDataContractSerializable(symbol, dataContractAttribute, dataMemberAttribute); - - static bool IsDataContractSerializable(IFieldSymbol symbol, INamedTypeSymbol? dataContractAttribute, INamedTypeSymbol? dataMemberAttribute) - { - if (dataContractAttribute is null || dataMemberAttribute is null) - return false; + return symbol.GetAttributes().Any(static (x, dataMemberAttribute) => SymbolEqualityComparer.Default.Equals(x.AttributeClass, dataMemberAttribute), dataMemberAttribute) + && symbol.ContainingType.GetAttributes().Any(static (x, dataContractAttribute) => SymbolEqualityComparer.Default.Equals(x.AttributeClass, dataContractAttribute), dataContractAttribute); + } - return symbol.GetAttributes().Any(static (x, dataMemberAttribute) => SymbolEqualityComparer.Default.Equals(x.AttributeClass, dataMemberAttribute), dataMemberAttribute) - && symbol.ContainingType.GetAttributes().Any(static (x, dataContractAttribute) => SymbolEqualityComparer.Default.Equals(x.AttributeClass, dataContractAttribute), dataContractAttribute); - } + // Method to update the field state for a candidate field written outside constructor and field initializer. + void UpdateFieldStateOnWrite(IFieldSymbol field) + { + Debug.Assert(IsCandidateField(field, threadStaticAttribute, dataContractAttribute, dataMemberAttribute)); + Debug.Assert(fieldStateMap.ContainsKey(field.OriginalDefinition)); - // Method to update the field state for a candidate field written outside constructor and field initializer. - void UpdateFieldStateOnWrite(IFieldSymbol field) - { - Debug.Assert(IsCandidateField(field, threadStaticAttribute, dataContractAttribute, dataMemberAttribute)); - Debug.Assert(fieldStateMap.ContainsKey(field.OriginalDefinition)); + fieldStateMap[field.OriginalDefinition] = (isCandidate: true, written: true); + } - fieldStateMap[field.OriginalDefinition] = (isCandidate: true, written: true); + // Method to get or initialize the field state. + (bool isCandidate, bool written) TryGetOrInitializeFieldState(IFieldSymbol fieldSymbol, AnalyzerOptions options, CancellationToken cancellationToken) + { + if (!IsCandidateField(fieldSymbol, threadStaticAttribute, dataContractAttribute, dataMemberAttribute)) + { + return default; } - // Method to get or initialize the field state. - (bool isCandidate, bool written) TryGetOrInitializeFieldState(IFieldSymbol fieldSymbol, AnalyzerOptions options, CancellationToken cancellationToken) + if (fieldStateMap.TryGetValue(fieldSymbol.OriginalDefinition, out var result)) { - if (!IsCandidateField(fieldSymbol, threadStaticAttribute, dataContractAttribute, dataMemberAttribute)) - { - return default; - } + return result; + } - if (fieldStateMap.TryGetValue(fieldSymbol.OriginalDefinition, out var result)) - { - return result; - } + result = ComputeInitialFieldState(fieldSymbol, options, threadStaticAttribute, dataContractAttribute, dataMemberAttribute, cancellationToken); + return fieldStateMap.GetOrAdd(fieldSymbol.OriginalDefinition, result); + } - result = ComputeInitialFieldState(fieldSymbol, options, threadStaticAttribute, dataContractAttribute, dataMemberAttribute, cancellationToken); - return fieldStateMap.GetOrAdd(fieldSymbol.OriginalDefinition, result); - } + // Method to compute the initial field state. + (bool isCandidate, bool written) ComputeInitialFieldState( + IFieldSymbol field, + AnalyzerOptions options, + INamedTypeSymbol? threadStaticAttribute, + INamedTypeSymbol? dataContractAttribute, + INamedTypeSymbol? dataMemberAttribute, + CancellationToken cancellationToken) + { + Debug.Assert(IsCandidateField(field, threadStaticAttribute, dataContractAttribute, dataMemberAttribute)); - // Method to compute the initial field state. - (bool isCandidate, bool written) ComputeInitialFieldState( - IFieldSymbol field, - AnalyzerOptions options, - INamedTypeSymbol? threadStaticAttribute, - INamedTypeSymbol? dataContractAttribute, - INamedTypeSymbol? dataMemberAttribute, - CancellationToken cancellationToken) + var option = GetCodeStyleOption(field, options, out var location); + if (option == null + || !option.Value + || ShouldSkipAnalysis(location.SourceTree!, options, context.Compilation.Options, option.Notification, cancellationToken)) { - Debug.Assert(IsCandidateField(field, threadStaticAttribute, dataContractAttribute, dataMemberAttribute)); + return default; + } - var option = GetCodeStyleOption(field, options, out var location); - if (option == null - || !option.Value - || ShouldSkipAnalysis(location.SourceTree!, options, context.Compilation.Options, option.Notification, cancellationToken)) - { - return default; - } + return (isCandidate: true, written: false); + } + }); + } - return (isCandidate: true, written: false); - } - }); - } + private static Location GetDiagnosticLocation(IFieldSymbol field) + => field.Locations[0]; - private static Location GetDiagnosticLocation(IFieldSymbol field) - => field.Locations[0]; + private static bool IsFieldWrite(IFieldReferenceOperation fieldReference, ISymbol owningSymbol) + { + // Check if the underlying member is being written or a writable reference to the member is taken. + var valueUsageInfo = fieldReference.GetValueUsageInfo(owningSymbol); + if (!valueUsageInfo.IsWrittenTo()) + return false; - private static bool IsFieldWrite(IFieldReferenceOperation fieldReference, ISymbol owningSymbol) - { - // Check if the underlying member is being written or a writable reference to the member is taken. - var valueUsageInfo = fieldReference.GetValueUsageInfo(owningSymbol); - if (!valueUsageInfo.IsWrittenTo()) - return false; + // Writes to fields inside constructor are ignored, except for the below cases: + // 1. Instance reference of an instance field being written is not the instance being initialized by the constructor. + // 2. Field is being written inside a lambda or local function. - // Writes to fields inside constructor are ignored, except for the below cases: - // 1. Instance reference of an instance field being written is not the instance being initialized by the constructor. - // 2. Field is being written inside a lambda or local function. + // Check if we are in the constructor of the containing type of the written field. + var isInInstanceConstructor = owningSymbol.IsConstructor(); + var isInStaticConstructor = owningSymbol.IsStaticConstructor(); - // Check if we are in the constructor of the containing type of the written field. - var isInInstanceConstructor = owningSymbol.IsConstructor(); - var isInStaticConstructor = owningSymbol.IsStaticConstructor(); + // If we're not in a constructor, this is definitely a write to the field that would prevent it from + // becoming readonly. + if (!isInInstanceConstructor && !isInStaticConstructor) + return true; - // If we're not in a constructor, this is definitely a write to the field that would prevent it from - // becoming readonly. - if (!isInInstanceConstructor && !isInStaticConstructor) - return true; + var field = fieldReference.Field; - var field = fieldReference.Field; + // Follow 'strict' C#/VB behavior. Has to be opted into by user with feature-flag "strict", but is + // actually what the languages specify as correct. Specifically the actual types of the containing + // type and field-reference-containing type must be the same (not just their OriginalDefinition + // types). + // + // https://github.com/dotnet/roslyn/blob/8770fb62a36157ed4ca38a16a0283d27321a01a7/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs#L1201-L1203 + // https//github.com/dotnet/roslyn/blob/93d3aa1a2cf1790b1a0fe2d120f00987d50445c0/src/Compilers/VisualBasic/Portable/Binding/Binder_Expressions.vb#L1868-L1871 + if (!field.ContainingType.Equals(owningSymbol.ContainingType)) + return true; - // Follow 'strict' C#/VB behavior. Has to be opted into by user with feature-flag "strict", but is - // actually what the languages specify as correct. Specifically the actual types of the containing - // type and field-reference-containing type must be the same (not just their OriginalDefinition - // types). - // - // https://github.com/dotnet/roslyn/blob/8770fb62a36157ed4ca38a16a0283d27321a01a7/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs#L1201-L1203 - // https//github.com/dotnet/roslyn/blob/93d3aa1a2cf1790b1a0fe2d120f00987d50445c0/src/Compilers/VisualBasic/Portable/Binding/Binder_Expressions.vb#L1868-L1871 - if (!field.ContainingType.Equals(owningSymbol.ContainingType)) + if (isInStaticConstructor) + { + // For static fields, ensure that we are in the static constructor. + if (!field.IsStatic) return true; - - if (isInStaticConstructor) - { - // For static fields, ensure that we are in the static constructor. - if (!field.IsStatic) - return true; - } - else + } + else + { + // For instance fields, ensure that the instance reference is being initialized by the constructor. + Debug.Assert(isInInstanceConstructor); + if (fieldReference.Instance?.Kind != OperationKind.InstanceReference || + fieldReference.IsTargetOfObjectMemberInitializer()) { - // For instance fields, ensure that the instance reference is being initialized by the constructor. - Debug.Assert(isInInstanceConstructor); - if (fieldReference.Instance?.Kind != OperationKind.InstanceReference || - fieldReference.IsTargetOfObjectMemberInitializer()) - { - return true; - } + return true; } + } - // Finally, ensure that the write is not inside a lambda or local function. - if (fieldReference.TryGetContainingAnonymousFunctionOrLocalFunction() is not null) - return true; + // Finally, ensure that the write is not inside a lambda or local function. + if (fieldReference.TryGetContainingAnonymousFunctionOrLocalFunction() is not null) + return true; - return false; - } + return false; + } - private static CodeStyleOption2 GetCodeStyleOption(IFieldSymbol field, AnalyzerOptions options, out Location diagnosticLocation) - { - diagnosticLocation = GetDiagnosticLocation(field); - return options.GetAnalyzerOptions(diagnosticLocation.SourceTree!).PreferReadonly; - } + private static CodeStyleOption2 GetCodeStyleOption(IFieldSymbol field, AnalyzerOptions options, out Location diagnosticLocation) + { + diagnosticLocation = GetDiagnosticLocation(field); + return options.GetAnalyzerOptions(diagnosticLocation.SourceTree!).PreferReadonly; } } diff --git a/src/Analyzers/Core/Analyzers/MatchFolderAndNamespace/AbstractMatchFolderAndNamespaceDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/MatchFolderAndNamespace/AbstractMatchFolderAndNamespaceDiagnosticAnalyzer.cs index 71206e91bba49..f7896c89abea6 100644 --- a/src/Analyzers/Core/Analyzers/MatchFolderAndNamespace/AbstractMatchFolderAndNamespaceDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/MatchFolderAndNamespace/AbstractMatchFolderAndNamespaceDiagnosticAnalyzer.cs @@ -14,176 +14,175 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Analyzers.MatchFolderAndNamespace +namespace Microsoft.CodeAnalysis.Analyzers.MatchFolderAndNamespace; + +internal abstract class AbstractMatchFolderAndNamespaceDiagnosticAnalyzer + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct + where TNamespaceSyntax : SyntaxNode { - internal abstract class AbstractMatchFolderAndNamespaceDiagnosticAnalyzer - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TSyntaxKind : struct - where TNamespaceSyntax : SyntaxNode + private static readonly LocalizableResourceString s_localizableTitle = new( + nameof(AnalyzersResources.Namespace_does_not_match_folder_structure), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + + private static readonly LocalizableResourceString s_localizableInsideMessage = new( + nameof(AnalyzersResources.Namespace_0_does_not_match_folder_structure_expected_1), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + + private static readonly SymbolDisplayFormat s_namespaceDisplayFormat = SymbolDisplayFormat + .FullyQualifiedFormat + .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted); + + protected AbstractMatchFolderAndNamespaceDiagnosticAnalyzer() + : base(IDEDiagnosticIds.MatchFolderAndNamespaceDiagnosticId, + EnforceOnBuildValues.MatchFolderAndNamespace, + CodeStyleOptions2.PreferNamespaceAndFolderMatchStructure, + s_localizableTitle, + s_localizableInsideMessage) { - private static readonly LocalizableResourceString s_localizableTitle = new( - nameof(AnalyzersResources.Namespace_does_not_match_folder_structure), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - - private static readonly LocalizableResourceString s_localizableInsideMessage = new( - nameof(AnalyzersResources.Namespace_0_does_not_match_folder_structure_expected_1), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - - private static readonly SymbolDisplayFormat s_namespaceDisplayFormat = SymbolDisplayFormat - .FullyQualifiedFormat - .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted); - - protected AbstractMatchFolderAndNamespaceDiagnosticAnalyzer() - : base(IDEDiagnosticIds.MatchFolderAndNamespaceDiagnosticId, - EnforceOnBuildValues.MatchFolderAndNamespace, - CodeStyleOptions2.PreferNamespaceAndFolderMatchStructure, - s_localizableTitle, - s_localizableInsideMessage) - { - } + } - protected abstract ISyntaxFacts GetSyntaxFacts(); - protected abstract ImmutableArray GetSyntaxKindsToAnalyze(); + protected abstract ISyntaxFacts GetSyntaxFacts(); + protected abstract ImmutableArray GetSyntaxKindsToAnalyze(); - protected sealed override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeNamespaceNode, GetSyntaxKindsToAnalyze()); + protected sealed override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeNamespaceNode, GetSyntaxKindsToAnalyze()); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - private void AnalyzeNamespaceNode(SyntaxNodeAnalysisContext context) + private void AnalyzeNamespaceNode(SyntaxNodeAnalysisContext context) + { + var option = context.GetAnalyzerOptions().PreferNamespaceAndFolderMatchStructure; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) { - var option = context.GetAnalyzerOptions().PreferNamespaceAndFolderMatchStructure; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - { - return; - } + return; + } - // It's ok to not have a rootnamespace property, but if it's there we want to use it correctly - context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(MatchFolderAndNamespaceConstants.RootNamespaceOption, out var rootNamespace); + // It's ok to not have a rootnamespace property, but if it's there we want to use it correctly + context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(MatchFolderAndNamespaceConstants.RootNamespaceOption, out var rootNamespace); - // Project directory is a must to correctly get the relative path and construct a namespace - if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(MatchFolderAndNamespaceConstants.ProjectDirOption, out var projectDir) - || string.IsNullOrEmpty(projectDir)) - { - return; - } + // Project directory is a must to correctly get the relative path and construct a namespace + if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(MatchFolderAndNamespaceConstants.ProjectDirOption, out var projectDir) + || string.IsNullOrEmpty(projectDir)) + { + return; + } - var namespaceDecl = (TNamespaceSyntax)context.Node; - var symbol = context.SemanticModel.GetDeclaredSymbol(namespaceDecl); - RoslynDebug.AssertNotNull(symbol); + var namespaceDecl = (TNamespaceSyntax)context.Node; + var symbol = context.SemanticModel.GetDeclaredSymbol(namespaceDecl); + RoslynDebug.AssertNotNull(symbol); - var currentNamespace = symbol.ToDisplayString(s_namespaceDisplayFormat); + var currentNamespace = symbol.ToDisplayString(s_namespaceDisplayFormat); - if (IsFileAndNamespaceMismatch(namespaceDecl, rootNamespace, projectDir, currentNamespace, out var targetNamespace) && - IsFixSupported(context.SemanticModel, namespaceDecl, context.CancellationToken)) - { - var nameSyntax = GetSyntaxFacts().GetNameOfBaseNamespaceDeclaration(namespaceDecl); - RoslynDebug.AssertNotNull(nameSyntax); - - context.ReportDiagnostic(Diagnostic.Create( - Descriptor, - nameSyntax.GetLocation(), - additionalLocations: null, - properties: ImmutableDictionary.Empty.Add(MatchFolderAndNamespaceConstants.TargetNamespace, targetNamespace), - messageArgs: new[] { currentNamespace, targetNamespace })); - } + if (IsFileAndNamespaceMismatch(namespaceDecl, rootNamespace, projectDir, currentNamespace, out var targetNamespace) && + IsFixSupported(context.SemanticModel, namespaceDecl, context.CancellationToken)) + { + var nameSyntax = GetSyntaxFacts().GetNameOfBaseNamespaceDeclaration(namespaceDecl); + RoslynDebug.AssertNotNull(nameSyntax); + + context.ReportDiagnostic(Diagnostic.Create( + Descriptor, + nameSyntax.GetLocation(), + additionalLocations: null, + properties: ImmutableDictionary.Empty.Add(MatchFolderAndNamespaceConstants.TargetNamespace, targetNamespace), + messageArgs: new[] { currentNamespace, targetNamespace })); } + } + + private bool IsFixSupported(SemanticModel semanticModel, TNamespaceSyntax namespaceDeclaration, CancellationToken cancellationToken) + { + var root = namespaceDeclaration.SyntaxTree.GetRoot(cancellationToken); - private bool IsFixSupported(SemanticModel semanticModel, TNamespaceSyntax namespaceDeclaration, CancellationToken cancellationToken) + // It should not be nested in other namespaces + if (namespaceDeclaration.Ancestors().OfType().Any()) { - var root = namespaceDeclaration.SyntaxTree.GetRoot(cancellationToken); + return false; + } - // It should not be nested in other namespaces - if (namespaceDeclaration.Ancestors().OfType().Any()) - { - return false; - } + // It should not contain a namespace + var containsNamespace = namespaceDeclaration + .DescendantNodes(n => n is TNamespaceSyntax) + .OfType().Any(); + if (containsNamespace) + { + return false; + } - // It should not contain a namespace - var containsNamespace = namespaceDeclaration - .DescendantNodes(n => n is TNamespaceSyntax) - .OfType().Any(); - if (containsNamespace) - { - return false; - } + // The current namespace should be valid + var isCurrentNamespaceInvalid = GetSyntaxFacts() + .GetNameOfBaseNamespaceDeclaration(namespaceDeclaration) + ?.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error) + ?? false; - // The current namespace should be valid - var isCurrentNamespaceInvalid = GetSyntaxFacts() - .GetNameOfBaseNamespaceDeclaration(namespaceDeclaration) - ?.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error) - ?? false; + if (isCurrentNamespaceInvalid) + { + return false; + } - if (isCurrentNamespaceInvalid) - { - return false; - } + // It should not contain partial classes with more than one instance in the semantic model. The + // fixer does not support this scenario. + var containsPartialType = ContainsPartialTypeWithMultipleDeclarations(namespaceDeclaration, semanticModel); + if (containsPartialType) + { + return false; + } - // It should not contain partial classes with more than one instance in the semantic model. The - // fixer does not support this scenario. - var containsPartialType = ContainsPartialTypeWithMultipleDeclarations(namespaceDeclaration, semanticModel); - if (containsPartialType) - { - return false; - } + return true; + } - return true; + private bool IsFileAndNamespaceMismatch( + TNamespaceSyntax namespaceDeclaration, + string? rootNamespace, + string projectDir, + string currentNamespace, + [NotNullWhen(returnValue: true)] out string? targetNamespace) + { + if (!PathUtilities.IsChildPath(projectDir, namespaceDeclaration.SyntaxTree.FilePath)) + { + // The file does not exist within the project directory + targetNamespace = null; + return false; } - private bool IsFileAndNamespaceMismatch( - TNamespaceSyntax namespaceDeclaration, - string? rootNamespace, - string projectDir, - string currentNamespace, - [NotNullWhen(returnValue: true)] out string? targetNamespace) - { - if (!PathUtilities.IsChildPath(projectDir, namespaceDeclaration.SyntaxTree.FilePath)) - { - // The file does not exist within the project directory - targetNamespace = null; - return false; - } + var relativeDirectoryPath = PathUtilities.GetRelativePath( + projectDir, + PathUtilities.GetDirectoryName(namespaceDeclaration.SyntaxTree.FilePath)!); + var folders = relativeDirectoryPath.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); - var relativeDirectoryPath = PathUtilities.GetRelativePath( - projectDir, - PathUtilities.GetDirectoryName(namespaceDeclaration.SyntaxTree.FilePath)!); - var folders = relativeDirectoryPath.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); + var expectedNamespace = PathMetadataUtilities.TryBuildNamespaceFromFolders(folders, GetSyntaxFacts(), rootNamespace); - var expectedNamespace = PathMetadataUtilities.TryBuildNamespaceFromFolders(folders, GetSyntaxFacts(), rootNamespace); + if (RoslynString.IsNullOrWhiteSpace(expectedNamespace) || expectedNamespace.Equals(currentNamespace, StringComparison.OrdinalIgnoreCase)) + { + // The namespace currently matches the folder structure or is invalid, in which case we don't want + // to provide a diagnostic. + targetNamespace = null; + return false; + } - if (RoslynString.IsNullOrWhiteSpace(expectedNamespace) || expectedNamespace.Equals(currentNamespace, StringComparison.OrdinalIgnoreCase)) - { - // The namespace currently matches the folder structure or is invalid, in which case we don't want - // to provide a diagnostic. - targetNamespace = null; - return false; - } + targetNamespace = expectedNamespace; + return true; + } - targetNamespace = expectedNamespace; - return true; - } + /// + /// Returns true if the namespace declaration contains one or more partial types with multiple declarations. + /// + protected bool ContainsPartialTypeWithMultipleDeclarations(TNamespaceSyntax namespaceDeclaration, SemanticModel semanticModel) + { + var syntaxFacts = GetSyntaxFacts(); - /// - /// Returns true if the namespace declaration contains one or more partial types with multiple declarations. - /// - protected bool ContainsPartialTypeWithMultipleDeclarations(TNamespaceSyntax namespaceDeclaration, SemanticModel semanticModel) - { - var syntaxFacts = GetSyntaxFacts(); + var typeDeclarations = syntaxFacts.GetMembersOfBaseNamespaceDeclaration(namespaceDeclaration) + .Where(syntaxFacts.IsTypeDeclaration); - var typeDeclarations = syntaxFacts.GetMembersOfBaseNamespaceDeclaration(namespaceDeclaration) - .Where(syntaxFacts.IsTypeDeclaration); + foreach (var typeDecl in typeDeclarations) + { + var symbol = semanticModel.GetDeclaredSymbol(typeDecl); - foreach (var typeDecl in typeDeclarations) + // Simplify the check by assuming no multiple partial declarations in one document + if (symbol is ITypeSymbol typeSymbol && typeSymbol.DeclaringSyntaxReferences.Length > 1) { - var symbol = semanticModel.GetDeclaredSymbol(typeDecl); - - // Simplify the check by assuming no multiple partial declarations in one document - if (symbol is ITypeSymbol typeSymbol && typeSymbol.DeclaringSyntaxReferences.Length > 1) - { - return true; - } + return true; } - - return false; } + + return false; } } diff --git a/src/Analyzers/Core/Analyzers/MatchFolderAndNamespace/MatchFolderAndNamespaceConstants.cs b/src/Analyzers/Core/Analyzers/MatchFolderAndNamespace/MatchFolderAndNamespaceConstants.cs index a2441d13e9b77..81282ef3ba06a 100644 --- a/src/Analyzers/Core/Analyzers/MatchFolderAndNamespace/MatchFolderAndNamespaceConstants.cs +++ b/src/Analyzers/Core/Analyzers/MatchFolderAndNamespace/MatchFolderAndNamespaceConstants.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Analyzers.MatchFolderAndNamespace +namespace Microsoft.CodeAnalysis.Analyzers.MatchFolderAndNamespace; + +internal static class MatchFolderAndNamespaceConstants { - internal static class MatchFolderAndNamespaceConstants - { - public const string RootNamespaceOption = "build_property.RootNamespace"; - public const string ProjectDirOption = "build_property.ProjectDir"; - public const string TargetNamespace = "TargetNamespace"; - } + public const string RootNamespaceOption = "build_property.RootNamespace"; + public const string ProjectDirOption = "build_property.ProjectDir"; + public const string TargetNamespace = "TargetNamespace"; } diff --git a/src/Analyzers/Core/Analyzers/NamingStyle/NamingStyleDiagnosticAnalyzerBase.cs b/src/Analyzers/Core/Analyzers/NamingStyle/NamingStyleDiagnosticAnalyzerBase.cs index ed82ec4ed26b9..ffd63b4027daf 100644 --- a/src/Analyzers/Core/Analyzers/NamingStyle/NamingStyleDiagnosticAnalyzerBase.cs +++ b/src/Analyzers/Core/Analyzers/NamingStyle/NamingStyleDiagnosticAnalyzerBase.cs @@ -13,166 +13,165 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles +namespace Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; + +internal abstract class NamingStyleDiagnosticAnalyzerBase + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TLanguageKindEnum : struct { - internal abstract class NamingStyleDiagnosticAnalyzerBase - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TLanguageKindEnum : struct + private static readonly LocalizableString s_localizableMessageFormat = new LocalizableResourceString(nameof(AnalyzersResources.Naming_rule_violation_0), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + private static readonly LocalizableString s_localizableTitleNamingStyle = new LocalizableResourceString(nameof(AnalyzersResources.Naming_Styles), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + + protected NamingStyleDiagnosticAnalyzerBase() + : base(IDEDiagnosticIds.NamingRuleId, + EnforceOnBuildValues.NamingRule, + option: null, // No unique option to configure the diagnosticId + s_localizableTitleNamingStyle, + s_localizableMessageFormat) { - private static readonly LocalizableString s_localizableMessageFormat = new LocalizableResourceString(nameof(AnalyzersResources.Naming_rule_violation_0), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - private static readonly LocalizableString s_localizableTitleNamingStyle = new LocalizableResourceString(nameof(AnalyzersResources.Naming_Styles), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - - protected NamingStyleDiagnosticAnalyzerBase() - : base(IDEDiagnosticIds.NamingRuleId, - EnforceOnBuildValues.NamingRule, - option: null, // No unique option to configure the diagnosticId - s_localizableTitleNamingStyle, - s_localizableMessageFormat) - { - } + } - // Applicable SymbolKind list is limited due to https://github.com/dotnet/roslyn/issues/8753. - // Locals and fields are handled by SupportedSyntaxKinds for now. - private static readonly ImmutableArray _symbolKinds = [SymbolKind.Event, SymbolKind.Method, SymbolKind.NamedType, SymbolKind.Namespace, SymbolKind.Property]; + // Applicable SymbolKind list is limited due to https://github.com/dotnet/roslyn/issues/8753. + // Locals and fields are handled by SupportedSyntaxKinds for now. + private static readonly ImmutableArray _symbolKinds = [SymbolKind.Event, SymbolKind.Method, SymbolKind.NamedType, SymbolKind.Namespace, SymbolKind.Property]; - // Workaround: RegisterSymbolAction doesn't work with locals, local functions, parameters, or type parameters. - // see https://github.com/dotnet/roslyn/issues/14061 - protected abstract ImmutableArray SupportedSyntaxKinds { get; } + // Workaround: RegisterSymbolAction doesn't work with locals, local functions, parameters, or type parameters. + // see https://github.com/dotnet/roslyn/issues/14061 + protected abstract ImmutableArray SupportedSyntaxKinds { get; } - protected abstract bool ShouldIgnore(ISymbol symbol); + protected abstract bool ShouldIgnore(ISymbol symbol); - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(CompilationStartAction); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(CompilationStartAction); - private void CompilationStartAction(CompilationStartAnalysisContext context) - { - var idToCachedResult = new ConcurrentDictionary>( - concurrencyLevel: 2, capacity: 0); + private void CompilationStartAction(CompilationStartAnalysisContext context) + { + var idToCachedResult = new ConcurrentDictionary>( + concurrencyLevel: 2, capacity: 0); - context.RegisterSymbolAction(SymbolAction, _symbolKinds); - context.RegisterSyntaxNodeAction(SyntaxNodeAction, SupportedSyntaxKinds); - return; + context.RegisterSymbolAction(SymbolAction, _symbolKinds); + context.RegisterSyntaxNodeAction(SyntaxNodeAction, SupportedSyntaxKinds); + return; - // Local functions + // Local functions - void SymbolAction(SymbolAnalysisContext symbolContext) + void SymbolAction(SymbolAnalysisContext symbolContext) + { + var sourceTree = symbolContext.Symbol.Locations.FirstOrDefault()?.SourceTree; + if (sourceTree == null + || ShouldSkipAnalysis(sourceTree, symbolContext.Options, symbolContext.Compilation.Options, notification: null, symbolContext.CancellationToken)) { - var sourceTree = symbolContext.Symbol.Locations.FirstOrDefault()?.SourceTree; - if (sourceTree == null - || ShouldSkipAnalysis(sourceTree, symbolContext.Options, symbolContext.Compilation.Options, notification: null, symbolContext.CancellationToken)) - { - return; - } - - var diagnostic = TryGetDiagnostic( - symbolContext.Compilation, - symbolContext.Symbol, - sourceTree, - symbolContext.Options, - idToCachedResult, - symbolContext.CancellationToken); - - if (diagnostic != null) - { - symbolContext.ReportDiagnostic(diagnostic); - } + return; } - void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext) + var diagnostic = TryGetDiagnostic( + symbolContext.Compilation, + symbolContext.Symbol, + sourceTree, + symbolContext.Options, + idToCachedResult, + symbolContext.CancellationToken); + + if (diagnostic != null) { - if (ShouldSkipAnalysis(syntaxContext, notification: null)) - { - return; - } - - var symbol = syntaxContext.SemanticModel.GetDeclaredSymbol(syntaxContext.Node, syntaxContext.CancellationToken); - if (symbol?.Locations.FirstOrDefault()?.SourceTree is not { } sourceTree) - { - // Catch clauses don't need to have a declaration. - return; - } - - var diagnostic = TryGetDiagnostic( - syntaxContext.Compilation, - symbol, - sourceTree, - syntaxContext.Options, - idToCachedResult, - syntaxContext.CancellationToken); - - if (diagnostic != null) - { - syntaxContext.ReportDiagnostic(diagnostic); - } + symbolContext.ReportDiagnostic(diagnostic); } } - private static readonly Func> s_createCache = - _ => new ConcurrentDictionary(concurrencyLevel: 2, capacity: 0); - - private Diagnostic? TryGetDiagnostic( - Compilation compilation, - ISymbol symbol, - SyntaxTree sourceTree, - AnalyzerOptions options, - ConcurrentDictionary> idToCachedResult, - CancellationToken cancellationToken) + void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext) { - if (string.IsNullOrEmpty(symbol.Name)) + if (ShouldSkipAnalysis(syntaxContext, notification: null)) { - return null; + return; } - if (symbol is IMethodSymbol methodSymbol && methodSymbol.IsEntryPoint(compilation.TaskType(), compilation.TaskOfTType())) + var symbol = syntaxContext.SemanticModel.GetDeclaredSymbol(syntaxContext.Node, syntaxContext.CancellationToken); + if (symbol?.Locations.FirstOrDefault()?.SourceTree is not { } sourceTree) { - return null; + // Catch clauses don't need to have a declaration. + return; } - if (ShouldIgnore(symbol)) - { - return null; - } + var diagnostic = TryGetDiagnostic( + syntaxContext.Compilation, + symbol, + sourceTree, + syntaxContext.Options, + idToCachedResult, + syntaxContext.CancellationToken); - if (symbol.IsSymbolWithSpecialDiscardName()) + if (diagnostic != null) { - return null; + syntaxContext.ReportDiagnostic(diagnostic); } + } + } - var namingPreferences = options.GetAnalyzerOptions(sourceTree).NamingPreferences; - var namingStyleRules = namingPreferences.Rules; + private static readonly Func> s_createCache = + _ => new ConcurrentDictionary(concurrencyLevel: 2, capacity: 0); - if (!namingStyleRules.TryGetApplicableRule(symbol, out var applicableRule) || - applicableRule.EnforcementLevel == ReportDiagnostic.Suppress) - { - return null; - } + private Diagnostic? TryGetDiagnostic( + Compilation compilation, + ISymbol symbol, + SyntaxTree sourceTree, + AnalyzerOptions options, + ConcurrentDictionary> idToCachedResult, + CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(symbol.Name)) + { + return null; + } - var cache = idToCachedResult.GetOrAdd(applicableRule.NamingStyle.ID, s_createCache); + if (symbol is IMethodSymbol methodSymbol && methodSymbol.IsEntryPoint(compilation.TaskType(), compilation.TaskOfTType())) + { + return null; + } - if (!cache.TryGetValue(symbol.Name, out var failureReason)) - { - if (applicableRule.NamingStyle.IsNameCompliant(symbol.Name, out failureReason)) - { - failureReason = null; - } + if (ShouldIgnore(symbol)) + { + return null; + } - cache.TryAdd(symbol.Name, failureReason); - } + if (symbol.IsSymbolWithSpecialDiscardName()) + { + return null; + } + + var namingPreferences = options.GetAnalyzerOptions(sourceTree).NamingPreferences; + var namingStyleRules = namingPreferences.Rules; - if (failureReason == null) + if (!namingStyleRules.TryGetApplicableRule(symbol, out var applicableRule) || + applicableRule.EnforcementLevel == ReportDiagnostic.Suppress) + { + return null; + } + + var cache = idToCachedResult.GetOrAdd(applicableRule.NamingStyle.ID, s_createCache); + + if (!cache.TryGetValue(symbol.Name, out var failureReason)) + { + if (applicableRule.NamingStyle.IsNameCompliant(symbol.Name, out failureReason)) { - return null; + failureReason = null; } - var builder = ImmutableDictionary.CreateBuilder(); - builder[nameof(NamingStyle)] = applicableRule.NamingStyle.CreateXElement().ToString(); - builder["OptionName"] = nameof(NamingStyleOptions.NamingPreferences); - builder["OptionLanguage"] = compilation.Language; + cache.TryAdd(symbol.Name, failureReason); + } - return DiagnosticHelper.Create(Descriptor, symbol.Locations.First(), NotificationOption2.ForSeverity(applicableRule.EnforcementLevel), additionalLocations: null, builder.ToImmutable(), failureReason); + if (failureReason == null) + { + return null; } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + var builder = ImmutableDictionary.CreateBuilder(); + builder[nameof(NamingStyle)] = applicableRule.NamingStyle.CreateXElement().ToString(); + builder["OptionName"] = nameof(NamingStyleOptions.NamingPreferences); + builder["OptionLanguage"] = compilation.Language; + + return DiagnosticHelper.Create(Descriptor, symbol.Locations.First(), NotificationOption2.ForSeverity(applicableRule.EnforcementLevel), options, additionalLocations: null, builder.ToImmutable(), failureReason); } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; } diff --git a/src/Analyzers/Core/Analyzers/NewLines/ConsecutiveStatementPlacement/AbstractConsecutiveStatementPlacementDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/NewLines/ConsecutiveStatementPlacement/AbstractConsecutiveStatementPlacementDiagnosticAnalyzer.cs index 764c688439c8e..6f2572311e6d6 100644 --- a/src/Analyzers/Core/Analyzers/NewLines/ConsecutiveStatementPlacement/AbstractConsecutiveStatementPlacementDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/NewLines/ConsecutiveStatementPlacement/AbstractConsecutiveStatementPlacementDiagnosticAnalyzer.cs @@ -9,106 +9,106 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.NewLines.ConsecutiveStatementPlacement +namespace Microsoft.CodeAnalysis.NewLines.ConsecutiveStatementPlacement; + +internal abstract class AbstractConsecutiveStatementPlacementDiagnosticAnalyzer + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TExecutableStatementSyntax : SyntaxNode { - internal abstract class AbstractConsecutiveStatementPlacementDiagnosticAnalyzer - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TExecutableStatementSyntax : SyntaxNode + private readonly ISyntaxFacts _syntaxFacts; + + protected AbstractConsecutiveStatementPlacementDiagnosticAnalyzer(ISyntaxFacts syntaxFacts) + : base(IDEDiagnosticIds.ConsecutiveStatementPlacementDiagnosticId, + EnforceOnBuildValues.ConsecutiveStatementPlacement, + CodeStyleOptions2.AllowStatementImmediatelyAfterBlock, + new LocalizableResourceString( + nameof(AnalyzersResources.Blank_line_required_between_block_and_subsequent_statement), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - private readonly ISyntaxFacts _syntaxFacts; - - protected AbstractConsecutiveStatementPlacementDiagnosticAnalyzer(ISyntaxFacts syntaxFacts) - : base(IDEDiagnosticIds.ConsecutiveStatementPlacementDiagnosticId, - EnforceOnBuildValues.ConsecutiveStatementPlacement, - CodeStyleOptions2.AllowStatementImmediatelyAfterBlock, - new LocalizableResourceString( - nameof(AnalyzersResources.Blank_line_required_between_block_and_subsequent_statement), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - _syntaxFacts = syntaxFacts; - } + _syntaxFacts = syntaxFacts; + } - protected abstract bool IsBlockLikeStatement(SyntaxNode node); - protected abstract Location GetDiagnosticLocation(SyntaxNode block); + protected abstract bool IsBlockLikeStatement(SyntaxNode node); + protected abstract Location GetDiagnosticLocation(SyntaxNode block); - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; - protected sealed override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); + protected sealed override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); - private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) - { - var option = context.GetAnalyzerOptions().AllowStatementImmediatelyAfterBlock; - if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) - return; + private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + { + var option = context.GetAnalyzerOptions().AllowStatementImmediatelyAfterBlock; + if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) + return; - Recurse(context, option.Notification, context.GetAnalysisRoot(findInTrivia: false), context.CancellationToken); - } + Recurse(context, option.Notification, context.GetAnalysisRoot(findInTrivia: false), context.CancellationToken); + } - private void Recurse(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxNode node, CancellationToken cancellationToken) - { - if (node.ContainsDiagnostics && node.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) - return; + private void Recurse(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxNode node, CancellationToken cancellationToken) + { + if (node.ContainsDiagnostics && node.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + return; - if (IsBlockLikeStatement(node)) - ProcessBlockLikeStatement(context, notificationOption, node); + if (IsBlockLikeStatement(node)) + ProcessBlockLikeStatement(context, notificationOption, node); - foreach (var child in node.ChildNodesAndTokens()) - { - if (!context.ShouldAnalyzeSpan(child.FullSpan)) - continue; + foreach (var child in node.ChildNodesAndTokens()) + { + if (!context.ShouldAnalyzeSpan(child.FullSpan)) + continue; - if (child.IsNode) - Recurse(context, notificationOption, child.AsNode()!, cancellationToken); - } + if (child.IsNode) + Recurse(context, notificationOption, child.AsNode()!, cancellationToken); } + } - private void ProcessBlockLikeStatement(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxNode block) - { - // Don't examine broken blocks. - var endToken = block.GetLastToken(); - if (endToken.IsMissing) - return; + private void ProcessBlockLikeStatement(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxNode block) + { + // Don't examine broken blocks. + var endToken = block.GetLastToken(); + if (endToken.IsMissing) + return; - // If the close brace itself doesn't have a newline, then ignore this. This is a case of series of - // statements on the same line. - if (!endToken.TrailingTrivia.Any()) - return; + // If the close brace itself doesn't have a newline, then ignore this. This is a case of series of + // statements on the same line. + if (!endToken.TrailingTrivia.Any()) + return; - if (!_syntaxFacts.IsEndOfLineTrivia(endToken.TrailingTrivia.Last())) - return; + if (!_syntaxFacts.IsEndOfLineTrivia(endToken.TrailingTrivia.Last())) + return; - // Grab whatever comes after the close brace. If it's not the start of a statement, ignore it. - var nextToken = endToken.GetNextToken(); - var nextTokenContainingStatement = nextToken.Parent?.FirstAncestorOrSelf(); - if (nextTokenContainingStatement == null) - return; + // Grab whatever comes after the close brace. If it's not the start of a statement, ignore it. + var nextToken = endToken.GetNextToken(); + var nextTokenContainingStatement = nextToken.Parent?.FirstAncestorOrSelf(); + if (nextTokenContainingStatement == null) + return; - if (nextToken != nextTokenContainingStatement.GetFirstToken()) - return; + if (nextToken != nextTokenContainingStatement.GetFirstToken()) + return; - // There has to be at least a blank line between the end of the block and the start of the next statement. + // There has to be at least a blank line between the end of the block and the start of the next statement. - foreach (var trivia in nextToken.LeadingTrivia) - { - // If there's a blank line between the brace and the next token, we're all set. - if (_syntaxFacts.IsEndOfLineTrivia(trivia)) - return; + foreach (var trivia in nextToken.LeadingTrivia) + { + // If there's a blank line between the brace and the next token, we're all set. + if (_syntaxFacts.IsEndOfLineTrivia(trivia)) + return; - if (_syntaxFacts.IsWhitespaceTrivia(trivia)) - continue; + if (_syntaxFacts.IsWhitespaceTrivia(trivia)) + continue; - // got something that wasn't whitespace. Bail out as we don't want to place any restrictions on this code. - return; - } - - context.ReportDiagnostic(DiagnosticHelper.Create( - this.Descriptor, - GetDiagnosticLocation(block), - notificationOption, - additionalLocations: ImmutableArray.Create(nextToken.GetLocation()), - properties: null)); + // got something that wasn't whitespace. Bail out as we don't want to place any restrictions on this code. + return; } + + context.ReportDiagnostic(DiagnosticHelper.Create( + this.Descriptor, + GetDiagnosticLocation(block), + notificationOption, + context.Options, + additionalLocations: ImmutableArray.Create(nextToken.GetLocation()), + properties: null)); } } diff --git a/src/Analyzers/Core/Analyzers/NewLines/MultipleBlankLines/AbstractMultipleBlankLinesDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/NewLines/MultipleBlankLines/AbstractMultipleBlankLinesDiagnosticAnalyzer.cs index 480fcdd62d42d..45b7cf0ac6921 100644 --- a/src/Analyzers/Core/Analyzers/NewLines/MultipleBlankLines/AbstractMultipleBlankLinesDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/NewLines/MultipleBlankLines/AbstractMultipleBlankLinesDiagnosticAnalyzer.cs @@ -10,127 +10,127 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.NewLines.MultipleBlankLines +namespace Microsoft.CodeAnalysis.NewLines.MultipleBlankLines; + +internal abstract class AbstractMultipleBlankLinesDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - internal abstract class AbstractMultipleBlankLinesDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + private readonly ISyntaxFacts _syntaxFacts; + + protected AbstractMultipleBlankLinesDiagnosticAnalyzer(ISyntaxFacts syntaxFacts) + : base(IDEDiagnosticIds.MultipleBlankLinesDiagnosticId, + EnforceOnBuildValues.MultipleBlankLines, + CodeStyleOptions2.AllowMultipleBlankLines, + new LocalizableResourceString( + nameof(AnalyzersResources.Avoid_multiple_blank_lines), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - private readonly ISyntaxFacts _syntaxFacts; - - protected AbstractMultipleBlankLinesDiagnosticAnalyzer(ISyntaxFacts syntaxFacts) - : base(IDEDiagnosticIds.MultipleBlankLinesDiagnosticId, - EnforceOnBuildValues.MultipleBlankLines, - CodeStyleOptions2.AllowMultipleBlankLines, - new LocalizableResourceString( - nameof(AnalyzersResources.Avoid_multiple_blank_lines), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - _syntaxFacts = syntaxFacts; - } + _syntaxFacts = syntaxFacts; + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options))); - private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) - { - var option = context.GetAnalyzerOptions().AllowMultipleBlankLines; - if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) - return; + private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + { + var option = context.GetAnalyzerOptions().AllowMultipleBlankLines; + if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification)) + return; - Recurse(context, option.Notification, context.GetAnalysisRoot(findInTrivia: false), context.CancellationToken); - } + Recurse(context, option.Notification, context.GetAnalysisRoot(findInTrivia: false), context.CancellationToken); + } - private void Recurse( - SyntaxTreeAnalysisContext context, - NotificationOption2 notificationOption, - SyntaxNode node, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + private void Recurse( + SyntaxTreeAnalysisContext context, + NotificationOption2 notificationOption, + SyntaxNode node, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - // Don't bother analyzing nodes that have syntax errors in them. - if (node.ContainsDiagnostics) - return; + // Don't bother analyzing nodes that have syntax errors in them. + if (node.ContainsDiagnostics) + return; - foreach (var child in node.ChildNodesAndTokens()) - { - if (!context.ShouldAnalyzeSpan(child.FullSpan)) - continue; + foreach (var child in node.ChildNodesAndTokens()) + { + if (!context.ShouldAnalyzeSpan(child.FullSpan)) + continue; - if (child.IsNode) - Recurse(context, notificationOption, child.AsNode()!, cancellationToken); - else if (child.IsToken) - CheckToken(context, notificationOption, child.AsToken()); - } + if (child.IsNode) + Recurse(context, notificationOption, child.AsNode()!, cancellationToken); + else if (child.IsToken) + CheckToken(context, notificationOption, child.AsToken()); } + } - private void CheckToken(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxToken token) - { - if (token.ContainsDiagnostics) - return; - - if (!ContainsMultipleBlankLines(token, out var badTrivia)) - return; - - context.ReportDiagnostic(DiagnosticHelper.Create( - this.Descriptor, - Location.Create(badTrivia.SyntaxTree!, new TextSpan(badTrivia.SpanStart, 0)), - notificationOption, - additionalLocations: ImmutableArray.Create(token.GetLocation()), - properties: null)); - } + private void CheckToken(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxToken token) + { + if (token.ContainsDiagnostics) + return; + + if (!ContainsMultipleBlankLines(token, out var badTrivia)) + return; + + context.ReportDiagnostic(DiagnosticHelper.Create( + this.Descriptor, + Location.Create(badTrivia.SyntaxTree!, new TextSpan(badTrivia.SpanStart, 0)), + notificationOption, + context.Options, + additionalLocations: ImmutableArray.Create(token.GetLocation()), + properties: null)); + } - private bool ContainsMultipleBlankLines(SyntaxToken token, out SyntaxTrivia firstBadTrivia) + private bool ContainsMultipleBlankLines(SyntaxToken token, out SyntaxTrivia firstBadTrivia) + { + var leadingTrivia = token.LeadingTrivia; + for (var i = 0; i < leadingTrivia.Count; i++) { - var leadingTrivia = token.LeadingTrivia; - for (var i = 0; i < leadingTrivia.Count; i++) + if (IsEndOfLine(leadingTrivia, i) && + IsEndOfLine(leadingTrivia, i + 1)) { - if (IsEndOfLine(leadingTrivia, i) && - IsEndOfLine(leadingTrivia, i + 1)) + // Three cases that end up with two blank lines. + // + // 1. the token starts with two newlines. This is definitely something to clean up. + // 2. we have two newlines after structured trivia (which itself ends with an newline). + // 3. we have three newlines (following non-structured trivia). + + if (i == 0 || + leadingTrivia[i - 1].HasStructure) + { + firstBadTrivia = leadingTrivia[i]; + return true; + } + + if (IsEndOfLine(leadingTrivia, i + 2)) { - // Three cases that end up with two blank lines. + // Report on the second newline. This is for cases like: + // + // // comment + // + // + // public // - // 1. the token starts with two newlines. This is definitely something to clean up. - // 2. we have two newlines after structured trivia (which itself ends with an newline). - // 3. we have three newlines (following non-structured trivia). - - if (i == 0 || - leadingTrivia[i - 1].HasStructure) - { - firstBadTrivia = leadingTrivia[i]; - return true; - } - - if (IsEndOfLine(leadingTrivia, i + 2)) - { - // Report on the second newline. This is for cases like: - // - // // comment - // - // - // public - // - // The first newline follows the comment. But we want to report the issue on the start of the - // next line. - firstBadTrivia = leadingTrivia[i + 1]; - return true; - } + // The first newline follows the comment. But we want to report the issue on the start of the + // next line. + firstBadTrivia = leadingTrivia[i + 1]; + return true; } } - - firstBadTrivia = default; - return false; } - private bool IsEndOfLine(SyntaxTriviaList triviaList, int index) - { - if (index >= triviaList.Count) - return false; + firstBadTrivia = default; + return false; + } - var trivia = triviaList[index]; - return _syntaxFacts.IsEndOfLineTrivia(trivia); - } + private bool IsEndOfLine(SyntaxTriviaList triviaList, int index) + { + if (index >= triviaList.Count) + return false; + + var trivia = triviaList[index]; + return _syntaxFacts.IsEndOfLineTrivia(trivia); } } diff --git a/src/Analyzers/Core/Analyzers/OrderModifiers/AbstractOrderModifiersDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/OrderModifiers/AbstractOrderModifiersDiagnosticAnalyzer.cs index 628c3a050ce7c..c35f16028c5b8 100644 --- a/src/Analyzers/Core/Analyzers/OrderModifiers/AbstractOrderModifiersDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/OrderModifiers/AbstractOrderModifiersDiagnosticAnalyzer.cs @@ -9,78 +9,77 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.OrderModifiers +namespace Microsoft.CodeAnalysis.OrderModifiers; + +internal abstract class AbstractOrderModifiersDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - internal abstract class AbstractOrderModifiersDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer - { - private readonly ISyntaxFacts _syntaxFacts; - private readonly AbstractOrderModifiersHelpers _helpers; + private readonly ISyntaxFacts _syntaxFacts; + private readonly AbstractOrderModifiersHelpers _helpers; - protected AbstractOrderModifiersDiagnosticAnalyzer( - ISyntaxFacts syntaxFacts, - Option2> option, - AbstractOrderModifiersHelpers helpers) - : base(IDEDiagnosticIds.OrderModifiersDiagnosticId, - EnforceOnBuildValues.OrderModifiers, - option, - new LocalizableResourceString(nameof(AnalyzersResources.Order_modifiers), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Modifiers_are_not_ordered), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - _syntaxFacts = syntaxFacts; - _helpers = helpers; - } + protected AbstractOrderModifiersDiagnosticAnalyzer( + ISyntaxFacts syntaxFacts, + Option2> option, + AbstractOrderModifiersHelpers helpers) + : base(IDEDiagnosticIds.OrderModifiersDiagnosticId, + EnforceOnBuildValues.OrderModifiers, + option, + new LocalizableResourceString(nameof(AnalyzersResources.Order_modifiers), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Modifiers_are_not_ordered), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) + { + _syntaxFacts = syntaxFacts; + _helpers = helpers; + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => - context.RegisterSyntaxTreeAction(treeContext => AnalyzeSyntaxTree(treeContext, context.Compilation.Options))); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + context.RegisterSyntaxTreeAction(treeContext => AnalyzeSyntaxTree(treeContext, context.Compilation.Options))); - protected abstract CodeStyleOption2 GetPreferredOrderStyle(SyntaxTreeAnalysisContext context); + protected abstract CodeStyleOption2 GetPreferredOrderStyle(SyntaxTreeAnalysisContext context); - private void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + private void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions) + { + var option = GetPreferredOrderStyle(context); + if (ShouldSkipAnalysis(context, compilationOptions, option.Notification) + || !_helpers.TryGetOrComputePreferredOrder(option.Value, out var preferredOrder)) { - var option = GetPreferredOrderStyle(context); - if (ShouldSkipAnalysis(context, compilationOptions, option.Notification) - || !_helpers.TryGetOrComputePreferredOrder(option.Value, out var preferredOrder)) - { - return; - } - - Recurse(context, preferredOrder, option.Notification, context.GetAnalysisRoot(findInTrivia: false)); + return; } - protected abstract void Recurse( - SyntaxTreeAnalysisContext context, - Dictionary preferredOrder, - NotificationOption2 notificationOption, - SyntaxNode root); + Recurse(context, preferredOrder, option.Notification, context.GetAnalysisRoot(findInTrivia: false)); + } + + protected abstract void Recurse( + SyntaxTreeAnalysisContext context, + Dictionary preferredOrder, + NotificationOption2 notificationOption, + SyntaxNode root); - protected void CheckModifiers( - SyntaxTreeAnalysisContext context, - Dictionary preferredOrder, - NotificationOption2 notificationOption, - SyntaxNode memberDeclaration) + protected void CheckModifiers( + SyntaxTreeAnalysisContext context, + Dictionary preferredOrder, + NotificationOption2 notificationOption, + SyntaxNode memberDeclaration) + { + var modifiers = _syntaxFacts.GetModifiers(memberDeclaration); + if (!AbstractOrderModifiersHelpers.IsOrdered(preferredOrder, modifiers)) { - var modifiers = _syntaxFacts.GetModifiers(memberDeclaration); - if (!AbstractOrderModifiersHelpers.IsOrdered(preferredOrder, modifiers)) + if (notificationOption.Severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden) + { + // If the severity is hidden, put the marker on all the modifiers so that the + // user can bring up the fix anywhere in the modifier list. + context.ReportDiagnostic( + Diagnostic.Create(Descriptor, context.Tree.GetLocation( + TextSpan.FromBounds(modifiers.First().SpanStart, modifiers.Last().Span.End)))); + } + else { - if (notificationOption.Severity.WithDefaultSeverity(DiagnosticSeverity.Hidden) == ReportDiagnostic.Hidden) - { - // If the severity is hidden, put the marker on all the modifiers so that the - // user can bring up the fix anywhere in the modifier list. - context.ReportDiagnostic( - Diagnostic.Create(Descriptor, context.Tree.GetLocation( - TextSpan.FromBounds(modifiers.First().SpanStart, modifiers.Last().Span.End)))); - } - else - { - // If the Severity is not hidden, then just put the user visible portion on the - // first token. That way we don't - context.ReportDiagnostic( - DiagnosticHelper.Create(Descriptor, modifiers.First().GetLocation(), notificationOption, additionalLocations: null, properties: null)); - } + // If the Severity is not hidden, then just put the user visible portion on the + // first token. That way we don't + context.ReportDiagnostic( + DiagnosticHelper.Create(Descriptor, modifiers.First().GetLocation(), notificationOption, context.Options, additionalLocations: null, properties: null)); } } } diff --git a/src/Analyzers/Core/Analyzers/OrderModifiers/OrderModifiersHelpers.cs b/src/Analyzers/Core/Analyzers/OrderModifiers/OrderModifiersHelpers.cs index 96a54aa606c7e..40ce57a18ede6 100644 --- a/src/Analyzers/Core/Analyzers/OrderModifiers/OrderModifiersHelpers.cs +++ b/src/Analyzers/Core/Analyzers/OrderModifiers/OrderModifiersHelpers.cs @@ -7,92 +7,91 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; -namespace Microsoft.CodeAnalysis.OrderModifiers +namespace Microsoft.CodeAnalysis.OrderModifiers; + +internal abstract class AbstractOrderModifiersHelpers { - internal abstract class AbstractOrderModifiersHelpers - { - private static readonly char[] s_comma = [',']; + private static readonly char[] s_comma = [',']; - /// - /// Reference type so we can read/write atomically. - /// - private Tuple>? _lastParsed; + /// + /// Reference type so we can read/write atomically. + /// + private Tuple>? _lastParsed; - protected abstract int GetKeywordKind(string trimmed); + protected abstract int GetKeywordKind(string trimmed); - public static bool IsOrdered(Dictionary preferredOrder, SyntaxTokenList modifiers) + public static bool IsOrdered(Dictionary preferredOrder, SyntaxTokenList modifiers) + { + if (modifiers.Count >= 2) { - if (modifiers.Count >= 2) + var lastOrder = int.MinValue; + foreach (var modifier in modifiers) { - var lastOrder = int.MinValue; - foreach (var modifier in modifiers) + var currentOrder = preferredOrder.TryGetValue(modifier.RawKind, out var value) ? value : int.MaxValue; + if (currentOrder < lastOrder) { - var currentOrder = preferredOrder.TryGetValue(modifier.RawKind, out var value) ? value : int.MaxValue; - if (currentOrder < lastOrder) - { - return false; - } - - lastOrder = currentOrder; + return false; } + + lastOrder = currentOrder; } + } + + return true; + } - return true; + public bool TryGetOrComputePreferredOrder(string value, [NotNullWhen(true)] out Dictionary? preferredOrder) + { + if (string.IsNullOrWhiteSpace(value)) + { + preferredOrder = null; + return false; } - public bool TryGetOrComputePreferredOrder(string value, [NotNullWhen(true)] out Dictionary? preferredOrder) + var lastParsed = Volatile.Read(ref _lastParsed); + if (lastParsed?.Item1 != value) { - if (string.IsNullOrWhiteSpace(value)) + if (!TryParse(value, out var parsed)) { preferredOrder = null; return false; } - var lastParsed = Volatile.Read(ref _lastParsed); - if (lastParsed?.Item1 != value) - { - if (!TryParse(value, out var parsed)) - { - preferredOrder = null; - return false; - } - - lastParsed = Tuple.Create(value, parsed); - Volatile.Write(ref _lastParsed, lastParsed); - } - - preferredOrder = lastParsed.Item2; - return true; + lastParsed = Tuple.Create(value, parsed); + Volatile.Write(ref _lastParsed, lastParsed); } - protected virtual bool TryParse(string value, [NotNullWhen(true)] out Dictionary? parsed) - { - var result = new Dictionary(); - - var index = 0; - foreach (var piece in value.Split(s_comma, StringSplitOptions.RemoveEmptyEntries)) - { - var trimmed = piece.Trim(); - var kind = GetKeywordKind(trimmed); + preferredOrder = lastParsed.Item2; + return true; + } - if (kind == 0) - { - parsed = null; - return false; - } + protected virtual bool TryParse(string value, [NotNullWhen(true)] out Dictionary? parsed) + { + var result = new Dictionary(); - result[kind] = index; - index++; - } + var index = 0; + foreach (var piece in value.Split(s_comma, StringSplitOptions.RemoveEmptyEntries)) + { + var trimmed = piece.Trim(); + var kind = GetKeywordKind(trimmed); - if (result.Count == 0) + if (kind == 0) { parsed = null; return false; } - parsed = result; - return true; + result[kind] = index; + index++; } + + if (result.Count == 0) + { + parsed = null; + return false; + } + + parsed = result; + return true; } } diff --git a/src/Analyzers/Core/Analyzers/ParenthesesDiagnosticAnalyzersHelper.cs b/src/Analyzers/Core/Analyzers/ParenthesesDiagnosticAnalyzersHelper.cs index 950df2228df1a..0e2a7ad3e5b88 100644 --- a/src/Analyzers/Core/Analyzers/ParenthesesDiagnosticAnalyzersHelper.cs +++ b/src/Analyzers/Core/Analyzers/ParenthesesDiagnosticAnalyzersHelper.cs @@ -9,26 +9,25 @@ using Microsoft.CodeAnalysis.Precedence; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses +namespace Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses; + +internal static class ParenthesesDiagnosticAnalyzersHelper { - internal static class ParenthesesDiagnosticAnalyzersHelper - { - internal static ImmutableHashSet Options = - [ - CodeStyleOptions2.ArithmeticBinaryParentheses, - CodeStyleOptions2.RelationalBinaryParentheses, - CodeStyleOptions2.OtherBinaryParentheses, - CodeStyleOptions2.OtherParentheses, - ]; + internal static ImmutableHashSet Options = + [ + CodeStyleOptions2.ArithmeticBinaryParentheses, + CodeStyleOptions2.RelationalBinaryParentheses, + CodeStyleOptions2.OtherBinaryParentheses, + CodeStyleOptions2.OtherParentheses, + ]; - internal static CodeStyleOption2 GetLanguageOption(AnalyzerOptionsProvider options, PrecedenceKind precedenceKind) - => precedenceKind switch - { - PrecedenceKind.Arithmetic or PrecedenceKind.Shift or PrecedenceKind.Bitwise => options.ArithmeticBinaryParentheses, - PrecedenceKind.Relational or PrecedenceKind.Equality => options.RelationalBinaryParentheses, - PrecedenceKind.Logical or PrecedenceKind.Coalesce => options.OtherBinaryParentheses, - PrecedenceKind.Other => options.OtherParentheses, - _ => throw ExceptionUtilities.UnexpectedValue(precedenceKind), - }; - } + internal static CodeStyleOption2 GetLanguageOption(AnalyzerOptionsProvider options, PrecedenceKind precedenceKind) + => precedenceKind switch + { + PrecedenceKind.Arithmetic or PrecedenceKind.Shift or PrecedenceKind.Bitwise => options.ArithmeticBinaryParentheses, + PrecedenceKind.Relational or PrecedenceKind.Equality => options.RelationalBinaryParentheses, + PrecedenceKind.Logical or PrecedenceKind.Coalesce => options.OtherBinaryParentheses, + PrecedenceKind.Other => options.OtherParentheses, + _ => throw ExceptionUtilities.UnexpectedValue(precedenceKind), + }; } diff --git a/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchDiagnosticAnalyzer.cs index 8dc3734753371..f7a0eb28c621a 100644 --- a/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchDiagnosticAnalyzer.cs @@ -10,115 +10,114 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.PopulateSwitch +namespace Microsoft.CodeAnalysis.PopulateSwitch; + +internal abstract class AbstractPopulateSwitchDiagnosticAnalyzer : + AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TSwitchOperation : IOperation + where TSwitchSyntax : SyntaxNode { - internal abstract class AbstractPopulateSwitchDiagnosticAnalyzer : - AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TSwitchOperation : IOperation - where TSwitchSyntax : SyntaxNode + private static readonly LocalizableString s_localizableTitle = new LocalizableResourceString(nameof(AnalyzersResources.Add_missing_cases), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + private static readonly LocalizableString s_localizableMessage = new LocalizableResourceString(nameof(AnalyzersResources.Populate_switch), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + + protected AbstractPopulateSwitchDiagnosticAnalyzer(string diagnosticId, EnforceOnBuild enforceOnBuild) + : base(diagnosticId, + enforceOnBuild, + option: null, + s_localizableTitle, s_localizableMessage) { - private static readonly LocalizableString s_localizableTitle = new LocalizableResourceString(nameof(AnalyzersResources.Add_missing_cases), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - private static readonly LocalizableString s_localizableMessage = new LocalizableResourceString(nameof(AnalyzersResources.Populate_switch), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - - protected AbstractPopulateSwitchDiagnosticAnalyzer(string diagnosticId, EnforceOnBuild enforceOnBuild) - : base(diagnosticId, - enforceOnBuild, - option: null, - s_localizableTitle, s_localizableMessage) - { - } + } + + #region Interface methods - #region Interface methods + protected abstract OperationKind OperationKind { get; } - protected abstract OperationKind OperationKind { get; } + protected abstract bool IsSwitchTypeUnknown(TSwitchOperation operation); + protected abstract IOperation GetValueOfSwitchOperation(TSwitchOperation operation); - protected abstract bool IsSwitchTypeUnknown(TSwitchOperation operation); - protected abstract IOperation GetValueOfSwitchOperation(TSwitchOperation operation); + protected abstract bool HasConstantCase(TSwitchOperation operation, object? value); + protected abstract ICollection GetMissingEnumMembers(TSwitchOperation operation); + protected abstract bool HasDefaultCase(TSwitchOperation operation); + protected abstract Location GetDiagnosticLocation(TSwitchSyntax switchBlock); - protected abstract bool HasConstantCase(TSwitchOperation operation, object? value); - protected abstract ICollection GetMissingEnumMembers(TSwitchOperation operation); - protected abstract bool HasDefaultCase(TSwitchOperation operation); - protected abstract Location GetDiagnosticLocation(TSwitchSyntax switchBlock); + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + protected sealed override void InitializeWorker(AnalysisContext context) + => context.RegisterOperationAction(AnalyzeOperation, OperationKind); - protected sealed override void InitializeWorker(AnalysisContext context) - => context.RegisterOperationAction(AnalyzeOperation, OperationKind); + private void AnalyzeOperation(OperationAnalysisContext context) + { + if (ShouldSkipAnalysis(context, notification: null)) + return; + + var switchOperation = (TSwitchOperation)context.Operation; + if (switchOperation.Syntax is not TSwitchSyntax switchBlock || IsSwitchTypeUnknown(switchOperation)) + return; + + var tree = switchBlock.SyntaxTree; - private void AnalyzeOperation(OperationAnalysisContext context) + if (SwitchIsIncomplete(switchOperation, out var missingCases, out var missingDefaultCase) && + !tree.OverlapsHiddenPosition(switchBlock.Span, context.CancellationToken)) { - if (ShouldSkipAnalysis(context, notification: null)) - return; - - var switchOperation = (TSwitchOperation)context.Operation; - if (switchOperation.Syntax is not TSwitchSyntax switchBlock || IsSwitchTypeUnknown(switchOperation)) - return; - - var tree = switchBlock.SyntaxTree; - - if (SwitchIsIncomplete(switchOperation, out var missingCases, out var missingDefaultCase) && - !tree.OverlapsHiddenPosition(switchBlock.Span, context.CancellationToken)) - { - Debug.Assert(missingCases || missingDefaultCase); - var properties = ImmutableDictionary.Empty - .Add(PopulateSwitchStatementHelpers.MissingCases, missingCases.ToString()) - .Add(PopulateSwitchStatementHelpers.MissingDefaultCase, missingDefaultCase.ToString()); - var diagnostic = Diagnostic.Create( - Descriptor, - GetDiagnosticLocation(switchBlock), - properties: properties, - additionalLocations: [switchBlock.GetLocation()]); - context.ReportDiagnostic(diagnostic); - } + Debug.Assert(missingCases || missingDefaultCase); + var properties = ImmutableDictionary.Empty + .Add(PopulateSwitchStatementHelpers.MissingCases, missingCases.ToString()) + .Add(PopulateSwitchStatementHelpers.MissingDefaultCase, missingDefaultCase.ToString()); + var diagnostic = Diagnostic.Create( + Descriptor, + GetDiagnosticLocation(switchBlock), + properties: properties, + additionalLocations: [switchBlock.GetLocation()]); + context.ReportDiagnostic(diagnostic); } + } - #endregion + #endregion - private bool SwitchIsIncomplete( - TSwitchOperation operation, - out bool missingCases, out bool missingDefaultCase) + private bool SwitchIsIncomplete( + TSwitchOperation operation, + out bool missingCases, out bool missingDefaultCase) + { + if (!IsBooleanSwitch(operation, out missingCases, out missingDefaultCase)) { - if (!IsBooleanSwitch(operation, out missingCases, out missingDefaultCase)) - { - var missingEnumMembers = GetMissingEnumMembers(operation); + var missingEnumMembers = GetMissingEnumMembers(operation); - missingCases = missingEnumMembers.Count > 0; - missingDefaultCase = !HasDefaultCase(operation); - } - - // The switch is incomplete if we're missing any cases or we're missing a default case. - return missingDefaultCase || missingCases; + missingCases = missingEnumMembers.Count > 0; + missingDefaultCase = !HasDefaultCase(operation); } - private bool IsBooleanSwitch(TSwitchOperation operation, out bool missingCases, out bool missingDefaultCase) + // The switch is incomplete if we're missing any cases or we're missing a default case. + return missingDefaultCase || missingCases; + } + + private bool IsBooleanSwitch(TSwitchOperation operation, out bool missingCases, out bool missingDefaultCase) + { + missingCases = false; + missingDefaultCase = false; + + var value = GetValueOfSwitchOperation(operation); + var type = value.Type.RemoveNullableIfPresent(); + if (type is not { SpecialType: SpecialType.System_Boolean }) + return false; + + // If the switch already has a default case, then we don't have to offer the user anything. + if (HasDefaultCase(operation)) { - missingCases = false; missingDefaultCase = false; + } + else + { + // Doesn't have a default. We don't want to offer that if they're already complete. + var hasAllCases = HasConstantCase(operation, true) && HasConstantCase(operation, false); + if (value.Type.IsNullable()) + hasAllCases = hasAllCases && HasConstantCase(operation, null); - var value = GetValueOfSwitchOperation(operation); - var type = value.Type.RemoveNullableIfPresent(); - if (type is not { SpecialType: SpecialType.System_Boolean }) - return false; - - // If the switch already has a default case, then we don't have to offer the user anything. - if (HasDefaultCase(operation)) - { - missingDefaultCase = false; - } - else - { - // Doesn't have a default. We don't want to offer that if they're already complete. - var hasAllCases = HasConstantCase(operation, true) && HasConstantCase(operation, false); - if (value.Type.IsNullable()) - hasAllCases = hasAllCases && HasConstantCase(operation, null); - - missingDefaultCase = !hasAllCases; - } - - return true; + missingDefaultCase = !hasAllCases; } - protected static bool ConstantValueEquals(Optional constantValue, object? value) - => constantValue.HasValue && Equals(constantValue.Value, value); + return true; } + + protected static bool ConstantValueEquals(Optional constantValue, object? value) + => constantValue.HasValue && Equals(constantValue.Value, value); } diff --git a/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchExpressionDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchExpressionDiagnosticAnalyzer.cs index 5e142311e6272..01d63183dba51 100644 --- a/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchExpressionDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchExpressionDiagnosticAnalyzer.cs @@ -6,44 +6,43 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.PopulateSwitch +namespace Microsoft.CodeAnalysis.PopulateSwitch; + +internal abstract class AbstractPopulateSwitchExpressionDiagnosticAnalyzer : + AbstractPopulateSwitchDiagnosticAnalyzer + where TSwitchSyntax : SyntaxNode { - internal abstract class AbstractPopulateSwitchExpressionDiagnosticAnalyzer : - AbstractPopulateSwitchDiagnosticAnalyzer - where TSwitchSyntax : SyntaxNode + protected AbstractPopulateSwitchExpressionDiagnosticAnalyzer() + : base(IDEDiagnosticIds.PopulateSwitchExpressionDiagnosticId, + EnforceOnBuildValues.PopulateSwitchExpression) { - protected AbstractPopulateSwitchExpressionDiagnosticAnalyzer() - : base(IDEDiagnosticIds.PopulateSwitchExpressionDiagnosticId, - EnforceOnBuildValues.PopulateSwitchExpression) - { - } + } - protected sealed override OperationKind OperationKind => OperationKind.SwitchExpression; + protected sealed override OperationKind OperationKind => OperationKind.SwitchExpression; - protected override IOperation GetValueOfSwitchOperation(ISwitchExpressionOperation operation) - => operation.Value; + protected override IOperation GetValueOfSwitchOperation(ISwitchExpressionOperation operation) + => operation.Value; - protected override bool IsSwitchTypeUnknown(ISwitchExpressionOperation operation) - => operation.Value.Type is null; + protected override bool IsSwitchTypeUnknown(ISwitchExpressionOperation operation) + => operation.Value.Type is null; - protected sealed override ICollection GetMissingEnumMembers(ISwitchExpressionOperation operation) - => PopulateSwitchExpressionHelpers.GetMissingEnumMembers(operation); + protected sealed override ICollection GetMissingEnumMembers(ISwitchExpressionOperation operation) + => PopulateSwitchExpressionHelpers.GetMissingEnumMembers(operation); - protected sealed override bool HasDefaultCase(ISwitchExpressionOperation operation) - => PopulateSwitchExpressionHelpers.HasDefaultCase(operation); + protected sealed override bool HasDefaultCase(ISwitchExpressionOperation operation) + => PopulateSwitchExpressionHelpers.HasDefaultCase(operation); - protected override bool HasConstantCase(ISwitchExpressionOperation operation, object? value) + protected override bool HasConstantCase(ISwitchExpressionOperation operation, object? value) + { + foreach (var arm in operation.Arms) { - foreach (var arm in operation.Arms) + if (arm is { Guard: null, Pattern: IConstantPatternOperation constantPattern } && + ConstantValueEquals(constantPattern.Value.ConstantValue, value)) { - if (arm is { Guard: null, Pattern: IConstantPatternOperation constantPattern } && - ConstantValueEquals(constantPattern.Value.ConstantValue, value)) - { - return true; - } + return true; } - - return false; } + + return false; } } diff --git a/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchStatementDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchStatementDiagnosticAnalyzer.cs index a45c480878b40..7dbaaf1bbef63 100644 --- a/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchStatementDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/PopulateSwitch/AbstractPopulateSwitchStatementDiagnosticAnalyzer.cs @@ -6,55 +6,54 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.PopulateSwitch +namespace Microsoft.CodeAnalysis.PopulateSwitch; + +internal abstract class AbstractPopulateSwitchStatementDiagnosticAnalyzer : + AbstractPopulateSwitchDiagnosticAnalyzer + where TSwitchSyntax : SyntaxNode { - internal abstract class AbstractPopulateSwitchStatementDiagnosticAnalyzer : - AbstractPopulateSwitchDiagnosticAnalyzer - where TSwitchSyntax : SyntaxNode + protected AbstractPopulateSwitchStatementDiagnosticAnalyzer() + : base(IDEDiagnosticIds.PopulateSwitchStatementDiagnosticId, + EnforceOnBuildValues.PopulateSwitchStatement) { - protected AbstractPopulateSwitchStatementDiagnosticAnalyzer() - : base(IDEDiagnosticIds.PopulateSwitchStatementDiagnosticId, - EnforceOnBuildValues.PopulateSwitchStatement) - { - } + } - protected sealed override OperationKind OperationKind => OperationKind.Switch; + protected sealed override OperationKind OperationKind => OperationKind.Switch; - protected override IOperation GetValueOfSwitchOperation(ISwitchOperation operation) - => operation.Value; + protected override IOperation GetValueOfSwitchOperation(ISwitchOperation operation) + => operation.Value; - protected sealed override bool IsSwitchTypeUnknown(ISwitchOperation operation) - => operation.Value.Type is null; + protected sealed override bool IsSwitchTypeUnknown(ISwitchOperation operation) + => operation.Value.Type is null; - protected sealed override ICollection GetMissingEnumMembers(ISwitchOperation operation) - => PopulateSwitchStatementHelpers.GetMissingEnumMembers(operation); + protected sealed override ICollection GetMissingEnumMembers(ISwitchOperation operation) + => PopulateSwitchStatementHelpers.GetMissingEnumMembers(operation); - protected sealed override bool HasDefaultCase(ISwitchOperation operation) - => PopulateSwitchStatementHelpers.HasDefaultCase(operation); + protected sealed override bool HasDefaultCase(ISwitchOperation operation) + => PopulateSwitchStatementHelpers.HasDefaultCase(operation); - protected sealed override Location GetDiagnosticLocation(TSwitchSyntax switchBlock) - => switchBlock.GetFirstToken().GetLocation(); + protected sealed override Location GetDiagnosticLocation(TSwitchSyntax switchBlock) + => switchBlock.GetFirstToken().GetLocation(); - protected override bool HasConstantCase(ISwitchOperation operation, object? value) + protected override bool HasConstantCase(ISwitchOperation operation, object? value) + { + foreach (var opCase in operation.Cases) { - foreach (var opCase in operation.Cases) + foreach (var clause in opCase.Clauses) { - foreach (var clause in opCase.Clauses) + if (clause is ISingleValueCaseClauseOperation singleValueCase && + ConstantValueEquals(singleValueCase.Value.ConstantValue, value)) { - if (clause is ISingleValueCaseClauseOperation singleValueCase && - ConstantValueEquals(singleValueCase.Value.ConstantValue, value)) - { - return true; - } - else if (clause is IPatternCaseClauseOperation { Guard: null, Pattern: IConstantPatternOperation constantPattern } && - ConstantValueEquals(constantPattern.Value.ConstantValue, value)) - { - return true; - } + return true; + } + else if (clause is IPatternCaseClauseOperation { Guard: null, Pattern: IConstantPatternOperation constantPattern } && + ConstantValueEquals(constantPattern.Value.ConstantValue, value)) + { + return true; } } - - return false; } + + return false; } } diff --git a/src/Analyzers/Core/Analyzers/PopulateSwitch/PopulateSwitchExpressionHelpers.cs b/src/Analyzers/Core/Analyzers/PopulateSwitch/PopulateSwitchExpressionHelpers.cs index 6a843f8956f3c..0404194fec0ce 100644 --- a/src/Analyzers/Core/Analyzers/PopulateSwitch/PopulateSwitchExpressionHelpers.cs +++ b/src/Analyzers/Core/Analyzers/PopulateSwitch/PopulateSwitchExpressionHelpers.cs @@ -10,98 +10,97 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.PopulateSwitch +namespace Microsoft.CodeAnalysis.PopulateSwitch; + +internal static class PopulateSwitchExpressionHelpers { - internal static class PopulateSwitchExpressionHelpers + public static ICollection GetMissingEnumMembers(ISwitchExpressionOperation operation) { - public static ICollection GetMissingEnumMembers(ISwitchExpressionOperation operation) - { - var switchExpression = operation.Value; - var switchExpressionType = switchExpression?.Type; + var switchExpression = operation.Value; + var switchExpressionType = switchExpression?.Type; - // Check if the type of the expression is a nullable INamedTypeSymbol - // if the type is both nullable and an INamedTypeSymbol extract the type argument from the nullable - // and check if it is of enum type - if (switchExpressionType != null) - switchExpressionType = switchExpressionType.IsNullable(out var underlyingType) ? underlyingType : switchExpressionType; + // Check if the type of the expression is a nullable INamedTypeSymbol + // if the type is both nullable and an INamedTypeSymbol extract the type argument from the nullable + // and check if it is of enum type + if (switchExpressionType != null) + switchExpressionType = switchExpressionType.IsNullable(out var underlyingType) ? underlyingType : switchExpressionType; - if (switchExpressionType?.TypeKind == TypeKind.Enum) + if (switchExpressionType?.TypeKind == TypeKind.Enum) + { + var enumMembers = new Dictionary(); + if (PopulateSwitchStatementHelpers.TryGetAllEnumMembers(switchExpressionType, enumMembers)) { - var enumMembers = new Dictionary(); - if (PopulateSwitchStatementHelpers.TryGetAllEnumMembers(switchExpressionType, enumMembers)) - { - RemoveExistingEnumMembers(operation, enumMembers); - return enumMembers.Values; - } + RemoveExistingEnumMembers(operation, enumMembers); + return enumMembers.Values; } - - return SpecializedCollections.EmptyCollection(); } - public static bool HasNullSwitchArm(ISwitchExpressionOperation operation) - { - foreach (var arm in operation.Arms) - { - if (arm.Pattern is IConstantPatternOperation { Value.ConstantValue: { HasValue: true, Value: null } }) - return true; - } + return SpecializedCollections.EmptyCollection(); + } - return false; + public static bool HasNullSwitchArm(ISwitchExpressionOperation operation) + { + foreach (var arm in operation.Arms) + { + if (arm.Pattern is IConstantPatternOperation { Value.ConstantValue: { HasValue: true, Value: null } }) + return true; } - private static void RemoveExistingEnumMembers( - ISwitchExpressionOperation operation, Dictionary enumMembers) + return false; + } + + private static void RemoveExistingEnumMembers( + ISwitchExpressionOperation operation, Dictionary enumMembers) + { + foreach (var arm in operation.Arms) { - foreach (var arm in operation.Arms) + RemoveIfConstantPatternHasValue(arm.Pattern, enumMembers); + if (arm.Pattern is IBinaryPatternOperation binaryPattern) { - RemoveIfConstantPatternHasValue(arm.Pattern, enumMembers); - if (arm.Pattern is IBinaryPatternOperation binaryPattern) - { - HandleBinaryPattern(binaryPattern, enumMembers); - } + HandleBinaryPattern(binaryPattern, enumMembers); } } + } - private static void HandleBinaryPattern(IBinaryPatternOperation? binaryPattern, Dictionary enumMembers) + private static void HandleBinaryPattern(IBinaryPatternOperation? binaryPattern, Dictionary enumMembers) + { + if (binaryPattern?.OperatorKind == BinaryOperatorKind.Or) { - if (binaryPattern?.OperatorKind == BinaryOperatorKind.Or) - { - RemoveIfConstantPatternHasValue(binaryPattern.LeftPattern, enumMembers); - RemoveIfConstantPatternHasValue(binaryPattern.RightPattern, enumMembers); + RemoveIfConstantPatternHasValue(binaryPattern.LeftPattern, enumMembers); + RemoveIfConstantPatternHasValue(binaryPattern.RightPattern, enumMembers); - HandleBinaryPattern(binaryPattern.LeftPattern as IBinaryPatternOperation, enumMembers); - HandleBinaryPattern(binaryPattern.RightPattern as IBinaryPatternOperation, enumMembers); - } + HandleBinaryPattern(binaryPattern.LeftPattern as IBinaryPatternOperation, enumMembers); + HandleBinaryPattern(binaryPattern.RightPattern as IBinaryPatternOperation, enumMembers); } + } - private static void RemoveIfConstantPatternHasValue(IOperation operation, Dictionary enumMembers) - { - if (operation is IConstantPatternOperation { Value.ConstantValue: { HasValue: true, Value: not null and var value } }) - enumMembers.Remove(IntegerUtilities.ToInt64(value)); - } + private static void RemoveIfConstantPatternHasValue(IOperation operation, Dictionary enumMembers) + { + if (operation is IConstantPatternOperation { Value.ConstantValue: { HasValue: true, Value: not null and var value } }) + enumMembers.Remove(IntegerUtilities.ToInt64(value)); + } - public static bool HasDefaultCase(ISwitchExpressionOperation operation) - => operation.Arms.Any(IsDefault); + public static bool HasDefaultCase(ISwitchExpressionOperation operation) + => operation.Arms.Any(IsDefault); - public static bool IsDefault(ISwitchExpressionArmOperation arm) - => IsDefault(arm.Pattern); + public static bool IsDefault(ISwitchExpressionArmOperation arm) + => IsDefault(arm.Pattern); - public static bool IsDefault(IPatternOperation pattern) - => pattern switch + public static bool IsDefault(IPatternOperation pattern) + => pattern switch + { + // _ => ... + IDiscardPatternOperation => true, + // var v => ... + IDeclarationPatternOperation declarationPattern => declarationPattern.MatchesNull, + IBinaryPatternOperation binaryPattern => binaryPattern.OperatorKind switch { - // _ => ... - IDiscardPatternOperation => true, - // var v => ... - IDeclarationPatternOperation declarationPattern => declarationPattern.MatchesNull, - IBinaryPatternOperation binaryPattern => binaryPattern.OperatorKind switch - { - // x or _ => ... - BinaryOperatorKind.Or => IsDefault(binaryPattern.LeftPattern) || IsDefault(binaryPattern.RightPattern), - // _ and var x => ... - BinaryOperatorKind.And => IsDefault(binaryPattern.LeftPattern) && IsDefault(binaryPattern.RightPattern), - _ => false, - }, - _ => false - }; - } + // x or _ => ... + BinaryOperatorKind.Or => IsDefault(binaryPattern.LeftPattern) || IsDefault(binaryPattern.RightPattern), + // _ and var x => ... + BinaryOperatorKind.And => IsDefault(binaryPattern.LeftPattern) && IsDefault(binaryPattern.RightPattern), + _ => false, + }, + _ => false + }; } diff --git a/src/Analyzers/Core/Analyzers/QualifyMemberAccess/AbstractQualifyMemberAccessDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/QualifyMemberAccess/AbstractQualifyMemberAccessDiagnosticAnalyzer.cs index 37756b5867194..b6b48991cf0c5 100644 --- a/src/Analyzers/Core/Analyzers/QualifyMemberAccess/AbstractQualifyMemberAccessDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/QualifyMemberAccess/AbstractQualifyMemberAccessDiagnosticAnalyzer.cs @@ -10,155 +10,155 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.QualifyMemberAccess +namespace Microsoft.CodeAnalysis.QualifyMemberAccess; + +internal abstract class AbstractQualifyMemberAccessDiagnosticAnalyzer< + TLanguageKindEnum, + TExpressionSyntax, + TSimpleNameSyntax> + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TLanguageKindEnum : struct + where TExpressionSyntax : SyntaxNode + where TSimpleNameSyntax : TExpressionSyntax { - internal abstract class AbstractQualifyMemberAccessDiagnosticAnalyzer< - TLanguageKindEnum, - TExpressionSyntax, - TSimpleNameSyntax> - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TLanguageKindEnum : struct - where TExpressionSyntax : SyntaxNode - where TSimpleNameSyntax : TExpressionSyntax + protected AbstractQualifyMemberAccessDiagnosticAnalyzer() + : base(IDEDiagnosticIds.AddThisOrMeQualificationDiagnosticId, + EnforceOnBuildValues.AddQualification, + options: + [ + CodeStyleOptions2.QualifyFieldAccess, + CodeStyleOptions2.QualifyPropertyAccess, + CodeStyleOptions2.QualifyMethodAccess, + CodeStyleOptions2.QualifyEventAccess, + ], + new LocalizableResourceString(nameof(AnalyzersResources.Member_access_should_be_qualified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Add_this_or_Me_qualification), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - protected AbstractQualifyMemberAccessDiagnosticAnalyzer() - : base(IDEDiagnosticIds.AddThisOrMeQualificationDiagnosticId, - EnforceOnBuildValues.AddQualification, - options: - [ - CodeStyleOptions2.QualifyFieldAccess, - CodeStyleOptions2.QualifyPropertyAccess, - CodeStyleOptions2.QualifyMethodAccess, - CodeStyleOptions2.QualifyEventAccess, - ], - new LocalizableResourceString(nameof(AnalyzersResources.Member_access_should_be_qualified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Add_this_or_Me_qualification), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) + } + + public override bool OpenFileOnly(SimplifierOptions? options) + { + // analyzer is only active in C# and VB projects + Contract.ThrowIfNull(options); + + return + !(options.QualifyFieldAccess.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || + options.QualifyPropertyAccess.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || + options.QualifyMethodAccess.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || + options.QualifyEventAccess.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error); + } + + /// + /// Reports on whether the specified member is suitable for qualification. Some member + /// access expressions cannot be qualified; for instance if they begin with base., + /// MyBase., or MyClass.. + /// + /// True if the member access can be qualified; otherwise, False. + protected abstract bool CanMemberAccessBeQualified(ISymbol containingSymbol, SyntaxNode node); + + protected abstract bool IsAlreadyQualifiedMemberAccess(TExpressionSyntax node); + + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterOperationAction(AnalyzeOperation, OperationKind.FieldReference, OperationKind.PropertyReference, OperationKind.MethodReference, OperationKind.Invocation); + + protected abstract Location GetLocation(IOperation operation); + protected abstract ISimplification Simplification { get; } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + private void AnalyzeOperation(OperationAnalysisContext context) + { + if (context.ContainingSymbol.IsStatic) { + return; } - public override bool OpenFileOnly(SimplifierOptions? options) + switch (context.Operation) { - // analyzer is only active in C# and VB projects - Contract.ThrowIfNull(options); - - return - !(options.QualifyFieldAccess.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || - options.QualifyPropertyAccess.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || - options.QualifyMethodAccess.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || - options.QualifyEventAccess.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error); + case IMemberReferenceOperation memberReferenceOperation: + AnalyzeOperation(context, memberReferenceOperation, memberReferenceOperation.Instance); + break; + case IInvocationOperation invocationOperation: + AnalyzeOperation(context, invocationOperation, invocationOperation.Instance); + break; + default: + throw ExceptionUtilities.UnexpectedValue(context.Operation); } + } + + private void AnalyzeOperation(OperationAnalysisContext context, IOperation operation, IOperation? instanceOperation) + { + // this is a static reference so we don't care if it's qualified + if (instanceOperation == null) + return; - /// - /// Reports on whether the specified member is suitable for qualification. Some member - /// access expressions cannot be qualified; for instance if they begin with base., - /// MyBase., or MyClass.. - /// - /// True if the member access can be qualified; otherwise, False. - protected abstract bool CanMemberAccessBeQualified(ISymbol containingSymbol, SyntaxNode node); + // if we're not referencing `this.` or `Me.` (e.g., a parameter, local, etc.) + if (instanceOperation.Kind != OperationKind.InstanceReference) + return; - protected abstract bool IsAlreadyQualifiedMemberAccess(TExpressionSyntax node); + // We shouldn't qualify if it is inside a property pattern + if (context.Operation.Parent?.Kind == OperationKind.PropertySubpattern) + return; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterOperationAction(AnalyzeOperation, OperationKind.FieldReference, OperationKind.PropertyReference, OperationKind.MethodReference, OperationKind.Invocation); + // Initializer lists are IInvocationOperation which if passed to GetApplicableOptionFromSymbolKind + // will incorrectly fetch the options for method call. + // We still want to handle InstanceReferenceKind.ContainingTypeInstance + if ((instanceOperation as IInstanceReferenceOperation)?.ReferenceKind == InstanceReferenceKind.ImplicitReceiver) + return; - protected abstract Location GetLocation(IOperation operation); - protected abstract ISimplification Simplification { get; } + // If we can't be qualified (e.g., because we're already qualified with `base.`), we're done. + if (!CanMemberAccessBeQualified(context.ContainingSymbol, instanceOperation.Syntax)) + return; - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + // if we can't find a member then we can't do anything. Also, we shouldn't qualify + // accesses to static members. + if (IsStaticMemberOrIsLocalFunction(operation)) + return; - private void AnalyzeOperation(OperationAnalysisContext context) + if (instanceOperation.Syntax is not TSimpleNameSyntax simpleName) + return; + + var symbolKind = operation switch { - if (context.ContainingSymbol.IsStatic) - { - return; - } - - switch (context.Operation) - { - case IMemberReferenceOperation memberReferenceOperation: - AnalyzeOperation(context, memberReferenceOperation, memberReferenceOperation.Instance); - break; - case IInvocationOperation invocationOperation: - AnalyzeOperation(context, invocationOperation, invocationOperation.Instance); - break; - default: - throw ExceptionUtilities.UnexpectedValue(context.Operation); - } + IMemberReferenceOperation memberReferenceOperation => memberReferenceOperation.Member.Kind, + IInvocationOperation invocationOperation => invocationOperation.TargetMethod.Kind, + _ => throw ExceptionUtilities.UnexpectedValue(operation), + }; + + var simplifierOptions = context.GetAnalyzerOptions().GetSimplifierOptions(Simplification); + if (!simplifierOptions.TryGetQualifyMemberAccessOption(symbolKind, out var optionValue)) + return; + + var shouldOptionBePresent = optionValue.Value; + if (!shouldOptionBePresent || ShouldSkipAnalysis(context, optionValue.Notification)) + { + return; } - private void AnalyzeOperation(OperationAnalysisContext context, IOperation operation, IOperation? instanceOperation) + if (!IsAlreadyQualifiedMemberAccess(simpleName)) { - // this is a static reference so we don't care if it's qualified - if (instanceOperation == null) - return; - - // if we're not referencing `this.` or `Me.` (e.g., a parameter, local, etc.) - if (instanceOperation.Kind != OperationKind.InstanceReference) - return; - - // We shouldn't qualify if it is inside a property pattern - if (context.Operation.Parent?.Kind == OperationKind.PropertySubpattern) - return; - - // Initializer lists are IInvocationOperation which if passed to GetApplicableOptionFromSymbolKind - // will incorrectly fetch the options for method call. - // We still want to handle InstanceReferenceKind.ContainingTypeInstance - if ((instanceOperation as IInstanceReferenceOperation)?.ReferenceKind == InstanceReferenceKind.ImplicitReceiver) - return; - - // If we can't be qualified (e.g., because we're already qualified with `base.`), we're done. - if (!CanMemberAccessBeQualified(context.ContainingSymbol, instanceOperation.Syntax)) - return; - - // if we can't find a member then we can't do anything. Also, we shouldn't qualify - // accesses to static members. - if (IsStaticMemberOrIsLocalFunction(operation)) - return; - - if (instanceOperation.Syntax is not TSimpleNameSyntax simpleName) - return; - - var symbolKind = operation switch - { - IMemberReferenceOperation memberReferenceOperation => memberReferenceOperation.Member.Kind, - IInvocationOperation invocationOperation => invocationOperation.TargetMethod.Kind, - _ => throw ExceptionUtilities.UnexpectedValue(operation), - }; - - var simplifierOptions = context.GetAnalyzerOptions().GetSimplifierOptions(Simplification); - if (!simplifierOptions.TryGetQualifyMemberAccessOption(symbolKind, out var optionValue)) - return; - - var shouldOptionBePresent = optionValue.Value; - if (!shouldOptionBePresent || ShouldSkipAnalysis(context, optionValue.Notification)) - { - return; - } - - if (!IsAlreadyQualifiedMemberAccess(simpleName)) - { - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - GetLocation(operation), - optionValue.Notification, - additionalLocations: null, - properties: null)); - } + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + GetLocation(operation), + optionValue.Notification, + context.Options, + additionalLocations: null, + properties: null)); } + } + + private static bool IsStaticMemberOrIsLocalFunction(IOperation operation) + { + return operation switch + { + IMemberReferenceOperation memberReferenceOperation => IsStaticMemberOrIsLocalFunctionHelper(memberReferenceOperation.Member), + IInvocationOperation invocationOperation => IsStaticMemberOrIsLocalFunctionHelper(invocationOperation.TargetMethod), + _ => throw ExceptionUtilities.UnexpectedValue(operation), + }; - private static bool IsStaticMemberOrIsLocalFunction(IOperation operation) + static bool IsStaticMemberOrIsLocalFunctionHelper(ISymbol symbol) { - return operation switch - { - IMemberReferenceOperation memberReferenceOperation => IsStaticMemberOrIsLocalFunctionHelper(memberReferenceOperation.Member), - IInvocationOperation invocationOperation => IsStaticMemberOrIsLocalFunctionHelper(invocationOperation.TargetMethod), - _ => throw ExceptionUtilities.UnexpectedValue(operation), - }; - - static bool IsStaticMemberOrIsLocalFunctionHelper(ISymbol symbol) - { - return symbol == null || symbol.IsStatic || symbol is IMethodSymbol { MethodKind: MethodKind.LocalFunction }; - } + return symbol == null || symbol.IsStatic || symbol is IMethodSymbol { MethodKind: MethodKind.LocalFunction }; } } } diff --git a/src/Analyzers/Core/Analyzers/RemoveRedundantEquality/AbstractRemoveRedundantEqualityDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveRedundantEquality/AbstractRemoveRedundantEqualityDiagnosticAnalyzer.cs index 3aa18b1e4f3da..de3de1f9883b1 100644 --- a/src/Analyzers/Core/Analyzers/RemoveRedundantEquality/AbstractRemoveRedundantEqualityDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveRedundantEquality/AbstractRemoveRedundantEqualityDiagnosticAnalyzer.cs @@ -8,99 +8,98 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.RemoveRedundantEquality +namespace Microsoft.CodeAnalysis.RemoveRedundantEquality; + +internal abstract class AbstractRemoveRedundantEqualityDiagnosticAnalyzer + : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - internal abstract class AbstractRemoveRedundantEqualityDiagnosticAnalyzer - : AbstractBuiltInCodeStyleDiagnosticAnalyzer + private readonly ISyntaxFacts _syntaxFacts; + + protected AbstractRemoveRedundantEqualityDiagnosticAnalyzer(ISyntaxFacts syntaxFacts) + : base(IDEDiagnosticIds.RemoveRedundantEqualityDiagnosticId, + EnforceOnBuildValues.RemoveRedundantEquality, + option: null, + new LocalizableResourceString(nameof(AnalyzersResources.Remove_redundant_equality), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - private readonly ISyntaxFacts _syntaxFacts; + _syntaxFacts = syntaxFacts; + } - protected AbstractRemoveRedundantEqualityDiagnosticAnalyzer(ISyntaxFacts syntaxFacts) - : base(IDEDiagnosticIds.RemoveRedundantEqualityDiagnosticId, - EnforceOnBuildValues.RemoveRedundantEquality, - option: null, - new LocalizableResourceString(nameof(AnalyzersResources.Remove_redundant_equality), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - _syntaxFacts = syntaxFacts; - } + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterOperationAction(AnalyzeBinaryOperator, OperationKind.BinaryOperator); - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterOperationAction(AnalyzeBinaryOperator, OperationKind.BinaryOperator); + private void AnalyzeBinaryOperator(OperationAnalysisContext context) + { + if (ShouldSkipAnalysis(context, notification: null)) + return; - private void AnalyzeBinaryOperator(OperationAnalysisContext context) + var operation = (IBinaryOperation)context.Operation; + if (operation.OperatorMethod is not null) { - if (ShouldSkipAnalysis(context, notification: null)) - return; - - var operation = (IBinaryOperation)context.Operation; - if (operation.OperatorMethod is not null) - { - // We shouldn't report diagnostic on overloaded operator as the behavior can change. - return; - } + // We shouldn't report diagnostic on overloaded operator as the behavior can change. + return; + } - if (operation.OperatorKind is not (BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals)) - { - return; - } + if (operation.OperatorKind is not (BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals)) + { + return; + } - if (!_syntaxFacts.IsBinaryExpression(operation.Syntax)) - { - return; - } + if (!_syntaxFacts.IsBinaryExpression(operation.Syntax)) + { + return; + } - var rightOperand = operation.RightOperand; - var leftOperand = operation.LeftOperand; + var rightOperand = operation.RightOperand; + var leftOperand = operation.LeftOperand; - if (rightOperand.Type is null || leftOperand.Type is null) - { - return; - } + if (rightOperand.Type is null || leftOperand.Type is null) + { + return; + } - if (rightOperand.Type.SpecialType != SpecialType.System_Boolean || - leftOperand.Type.SpecialType != SpecialType.System_Boolean) - { - return; - } + if (rightOperand.Type.SpecialType != SpecialType.System_Boolean || + leftOperand.Type.SpecialType != SpecialType.System_Boolean) + { + return; + } - var isOperatorEquals = operation.OperatorKind == BinaryOperatorKind.Equals; - _syntaxFacts.GetPartsOfBinaryExpression(operation.Syntax, out _, out var operatorToken, out _); - var properties = ImmutableDictionary.CreateBuilder(); - if (TryGetLiteralValue(rightOperand) == isOperatorEquals) - { - properties.Add(RedundantEqualityConstants.RedundantSide, RedundantEqualityConstants.Right); - } - else if (TryGetLiteralValue(leftOperand) == isOperatorEquals) - { - properties.Add(RedundantEqualityConstants.RedundantSide, RedundantEqualityConstants.Left); - } + var isOperatorEquals = operation.OperatorKind == BinaryOperatorKind.Equals; + _syntaxFacts.GetPartsOfBinaryExpression(operation.Syntax, out _, out var operatorToken, out _); + var properties = ImmutableDictionary.CreateBuilder(); + if (TryGetLiteralValue(rightOperand) == isOperatorEquals) + { + properties.Add(RedundantEqualityConstants.RedundantSide, RedundantEqualityConstants.Right); + } + else if (TryGetLiteralValue(leftOperand) == isOperatorEquals) + { + properties.Add(RedundantEqualityConstants.RedundantSide, RedundantEqualityConstants.Left); + } - if (properties.Count == 1) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptor, - operatorToken.GetLocation(), - additionalLocations: [operation.Syntax.GetLocation()], - properties: properties.ToImmutable())); - } + if (properties.Count == 1) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, + operatorToken.GetLocation(), + additionalLocations: [operation.Syntax.GetLocation()], + properties: properties.ToImmutable())); + } - return; + return; - static bool? TryGetLiteralValue(IOperation operand) + static bool? TryGetLiteralValue(IOperation operand) + { + // Make sure we only simplify literals to avoid changing + // something like the following example: + // const bool Activated = true; ... if (state == Activated) + if (operand.ConstantValue.HasValue && operand.Kind == OperationKind.Literal && + operand.ConstantValue.Value is bool constValue) { - // Make sure we only simplify literals to avoid changing - // something like the following example: - // const bool Activated = true; ... if (state == Activated) - if (operand.ConstantValue.HasValue && operand.Kind == OperationKind.Literal && - operand.ConstantValue.Value is bool constValue) - { - return constValue; - } - - return null; + return constValue; } + + return null; } } } diff --git a/src/Analyzers/Core/Analyzers/RemoveRedundantEquality/RedundantEqualityConstants.cs b/src/Analyzers/Core/Analyzers/RemoveRedundantEquality/RedundantEqualityConstants.cs index 089c18a935e5b..9303d7d6b89fc 100644 --- a/src/Analyzers/Core/Analyzers/RemoveRedundantEquality/RedundantEqualityConstants.cs +++ b/src/Analyzers/Core/Analyzers/RemoveRedundantEquality/RedundantEqualityConstants.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.RemoveRedundantEquality +namespace Microsoft.CodeAnalysis.RemoveRedundantEquality; + +internal static class RedundantEqualityConstants { - internal static class RedundantEqualityConstants - { - public const string RedundantSide = nameof(RedundantSide); - public const string Left = nameof(Left); - public const string Right = nameof(Right); - } + public const string RedundantSide = nameof(RedundantSide); + public const string Left = nameof(Left); + public const string Right = nameof(Right); } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryCast/AbstractRemoveUnnecessaryCastDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryCast/AbstractRemoveUnnecessaryCastDiagnosticAnalyzer.cs index 1654db0336d5e..540a073cf8501 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryCast/AbstractRemoveUnnecessaryCastDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryCast/AbstractRemoveUnnecessaryCastDiagnosticAnalyzer.cs @@ -9,69 +9,68 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.RemoveUnnecessaryCast +namespace Microsoft.CodeAnalysis.RemoveUnnecessaryCast; + +internal abstract class AbstractRemoveUnnecessaryCastDiagnosticAnalyzer< + TLanguageKindEnum, + TCastExpression> : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + where TLanguageKindEnum : struct + where TCastExpression : SyntaxNode { - internal abstract class AbstractRemoveUnnecessaryCastDiagnosticAnalyzer< - TLanguageKindEnum, - TCastExpression> : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer - where TLanguageKindEnum : struct - where TCastExpression : SyntaxNode + protected AbstractRemoveUnnecessaryCastDiagnosticAnalyzer() + : base(IDEDiagnosticIds.RemoveUnnecessaryCastDiagnosticId, + EnforceOnBuildValues.RemoveUnnecessaryCast, + option: null, + fadingOption: null, + new LocalizableResourceString(nameof(AnalyzersResources.Remove_Unnecessary_Cast), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(CompilerExtensionsResources.Cast_is_redundant), CompilerExtensionsResources.ResourceManager, typeof(CompilerExtensionsResources))) { - protected AbstractRemoveUnnecessaryCastDiagnosticAnalyzer() - : base(IDEDiagnosticIds.RemoveUnnecessaryCastDiagnosticId, - EnforceOnBuildValues.RemoveUnnecessaryCast, - option: null, - fadingOption: null, - new LocalizableResourceString(nameof(AnalyzersResources.Remove_Unnecessary_Cast), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(CompilerExtensionsResources.Cast_is_redundant), CompilerExtensionsResources.ResourceManager, typeof(CompilerExtensionsResources))) - { - } - - protected abstract ImmutableArray SyntaxKindsOfInterest { get; } - protected abstract TextSpan GetFadeSpan(TCastExpression node); - protected abstract bool IsUnnecessaryCast(SemanticModel model, TCastExpression node, CancellationToken cancellationToken); + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + protected abstract ImmutableArray SyntaxKindsOfInterest { get; } + protected abstract TextSpan GetFadeSpan(TCastExpression node); + protected abstract bool IsUnnecessaryCast(SemanticModel model, TCastExpression node, CancellationToken cancellationToken); - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKindsOfInterest); + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) - { - if (ShouldSkipAnalysis(context, notification: null)) - return; + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKindsOfInterest); - var diagnostic = TryRemoveCastExpression( - context.SemanticModel, - (TCastExpression)context.Node, - context.CancellationToken); + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + if (ShouldSkipAnalysis(context, notification: null)) + return; - if (diagnostic != null) - { - context.ReportDiagnostic(diagnostic); - } - } + var diagnostic = TryRemoveCastExpression( + context.SemanticModel, + (TCastExpression)context.Node, + context.CancellationToken); - private Diagnostic? TryRemoveCastExpression(SemanticModel model, TCastExpression node, CancellationToken cancellationToken) + if (diagnostic != null) { - cancellationToken.ThrowIfCancellationRequested(); + context.ReportDiagnostic(diagnostic); + } + } - if (!IsUnnecessaryCast(model, node, cancellationToken)) - { - return null; - } + private Diagnostic? TryRemoveCastExpression(SemanticModel model, TCastExpression node, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - var tree = model.SyntaxTree; - if (tree.OverlapsHiddenPosition(node.Span, cancellationToken)) - { - return null; - } + if (!IsUnnecessaryCast(model, node, cancellationToken)) + { + return null; + } - return Diagnostic.Create( - Descriptor, - node.SyntaxTree.GetLocation(GetFadeSpan(node)), - ImmutableArray.Create(node.GetLocation())); + var tree = model.SyntaxTree; + if (tree.OverlapsHiddenPosition(node.Span, cancellationToken)) + { + return null; } + + return Diagnostic.Create( + Descriptor, + node.SyntaxTree.GetLocation(GetFadeSpan(node)), + ImmutableArray.Create(node.GetLocation())); } } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs index 2d850002da88d..690ac016a84b8 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs @@ -13,213 +13,212 @@ using Roslyn.Utilities; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.RemoveUnnecessaryImports +namespace Microsoft.CodeAnalysis.RemoveUnnecessaryImports; + +internal abstract class AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer : + AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + where TSyntaxNode : SyntaxNode { - internal abstract class AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer : - AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer - where TSyntaxNode : SyntaxNode - { - // NOTE: This is a special helper diagnostic ID which is reported when the remove unnecesssary diagnostic ID (IDE0005) is - // ecalated to a warning or an error, but 'GenerateDocumentationFile' is false, which leads to IDE0005 not being reported - // on command line builds. See https://github.com/dotnet/roslyn/issues/41640 for more details. - internal const string EnableGenerateDocumentationFileId = "EnableGenerateDocumentationFile"; + // NOTE: This is a special helper diagnostic ID which is reported when the remove unnecesssary diagnostic ID (IDE0005) is + // ecalated to a warning or an error, but 'GenerateDocumentationFile' is false, which leads to IDE0005 not being reported + // on command line builds. See https://github.com/dotnet/roslyn/issues/41640 for more details. + internal const string EnableGenerateDocumentationFileId = "EnableGenerateDocumentationFile"; - // The NotConfigurable custom tag ensures that user can't turn this diagnostic into a warning / error via - // ruleset editor or solution explorer. Setting messageFormat to empty string ensures that we won't display - // this diagnostic in the preview pane header. - private static readonly DiagnosticDescriptor s_fixableIdDescriptor = CreateDescriptorWithId( - RemoveUnnecessaryImportsConstants.DiagnosticFixableId, EnforceOnBuild.Never, hasAnyCodeStyleOption: true, "", "", isConfigurable: false); + // The NotConfigurable custom tag ensures that user can't turn this diagnostic into a warning / error via + // ruleset editor or solution explorer. Setting messageFormat to empty string ensures that we won't display + // this diagnostic in the preview pane header. + private static readonly DiagnosticDescriptor s_fixableIdDescriptor = CreateDescriptorWithId( + RemoveUnnecessaryImportsConstants.DiagnosticFixableId, EnforceOnBuild.Never, hasAnyCodeStyleOption: true, "", "", isConfigurable: false); #pragma warning disable RS0030 // Do not used banned APIs - Special diagnostic with 'Warning' default severity. - private static readonly DiagnosticDescriptor s_enableGenerateDocumentationFileIdDescriptor = new( - EnableGenerateDocumentationFileId, - title: AnalyzersResources.Set_MSBuild_Property_GenerateDocumentationFile_to_true, - messageFormat: AnalyzersResources.Set_MSBuild_Property_GenerateDocumentationFile_to_true_in_project_file_to_enable_IDE0005_Remove_unnecessary_usings_imports_on_build, - category: DiagnosticCategory.Style, - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - helpLinkUri: "https://github.com/dotnet/roslyn/issues/41640", - description: AnalyzersResources.Add_the_following_PropertyGroup_to_your_MSBuild_project_file_to_enable_IDE0005_Remove_unnecessary_usings_imports_on_build, - customTags: DiagnosticCustomTags.Microsoft.Concat(EnforceOnBuild.Never.ToCustomTag()).ToArray()); + private static readonly DiagnosticDescriptor s_enableGenerateDocumentationFileIdDescriptor = new( + EnableGenerateDocumentationFileId, + title: AnalyzersResources.Set_MSBuild_Property_GenerateDocumentationFile_to_true, + messageFormat: AnalyzersResources.Set_MSBuild_Property_GenerateDocumentationFile_to_true_in_project_file_to_enable_IDE0005_Remove_unnecessary_usings_imports_on_build, + category: DiagnosticCategory.Style, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: "https://github.com/dotnet/roslyn/issues/41640", + description: AnalyzersResources.Add_the_following_PropertyGroup_to_your_MSBuild_project_file_to_enable_IDE0005_Remove_unnecessary_usings_imports_on_build, + customTags: DiagnosticCustomTags.Microsoft.Concat(EnforceOnBuild.Never.ToCustomTag()).ToArray()); #pragma warning restore RS0030 // Do not used banned APIs - private readonly DiagnosticDescriptor _classificationIdDescriptor; - private readonly DiagnosticDescriptor _generatedCodeClassificationIdDescriptor; + private readonly DiagnosticDescriptor _classificationIdDescriptor; + private readonly DiagnosticDescriptor _generatedCodeClassificationIdDescriptor; - protected AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer(LocalizableString titleAndMessage) - : base(GetDescriptors(titleAndMessage, out var classificationIdDescriptor, out var generatedCodeClassificationIdDescriptor), FadingOptions.FadeOutUnusedImports) - { - _classificationIdDescriptor = classificationIdDescriptor; - _generatedCodeClassificationIdDescriptor = generatedCodeClassificationIdDescriptor; - } + protected AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer(LocalizableString titleAndMessage) + : base(GetDescriptors(titleAndMessage, out var classificationIdDescriptor, out var generatedCodeClassificationIdDescriptor), FadingOptions.FadeOutUnusedImports) + { + _classificationIdDescriptor = classificationIdDescriptor; + _generatedCodeClassificationIdDescriptor = generatedCodeClassificationIdDescriptor; + } - private static ImmutableArray GetDescriptors(LocalizableString titleAndMessage, out DiagnosticDescriptor classificationIdDescriptor, out DiagnosticDescriptor generatedCodeClassificationIdDescriptor) - { - classificationIdDescriptor = CreateDescriptorWithId(IDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId, EnforceOnBuildValues.RemoveUnnecessaryImports, hasAnyCodeStyleOption: false, titleAndMessage, isUnnecessary: true); - generatedCodeClassificationIdDescriptor = CreateDescriptorWithId(IDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId + "_gen", EnforceOnBuild.Never, hasAnyCodeStyleOption: false, titleAndMessage, isUnnecessary: true, isConfigurable: false); - return - [ - s_fixableIdDescriptor, - s_enableGenerateDocumentationFileIdDescriptor, - classificationIdDescriptor, - generatedCodeClassificationIdDescriptor, - ]; - } + private static ImmutableArray GetDescriptors(LocalizableString titleAndMessage, out DiagnosticDescriptor classificationIdDescriptor, out DiagnosticDescriptor generatedCodeClassificationIdDescriptor) + { + classificationIdDescriptor = CreateDescriptorWithId(IDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId, EnforceOnBuildValues.RemoveUnnecessaryImports, hasAnyCodeStyleOption: false, titleAndMessage, isUnnecessary: true); + generatedCodeClassificationIdDescriptor = CreateDescriptorWithId(IDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId + "_gen", EnforceOnBuild.Never, hasAnyCodeStyleOption: false, titleAndMessage, isUnnecessary: true, isConfigurable: false); + return + [ + s_fixableIdDescriptor, + s_enableGenerateDocumentationFileIdDescriptor, + classificationIdDescriptor, + generatedCodeClassificationIdDescriptor, + ]; + } - protected abstract ISyntaxFacts SyntaxFacts { get; } - protected abstract ImmutableArray MergeImports(ImmutableArray unnecessaryImports); - protected abstract bool IsRegularCommentOrDocComment(SyntaxTrivia trivia); - protected abstract IUnnecessaryImportsProvider UnnecessaryImportsProvider { get; } + protected abstract ISyntaxFacts SyntaxFacts { get; } + protected abstract ImmutableArray MergeImports(ImmutableArray unnecessaryImports); + protected abstract bool IsRegularCommentOrDocComment(SyntaxTrivia trivia); + protected abstract IUnnecessaryImportsProvider UnnecessaryImportsProvider { get; } - protected override GeneratedCodeAnalysisFlags GeneratedCodeAnalysisFlags => GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics; + protected override GeneratedCodeAnalysisFlags GeneratedCodeAnalysisFlags => GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics; - protected abstract SyntaxToken? TryGetLastToken(SyntaxNode node); + protected abstract SyntaxToken? TryGetLastToken(SyntaxNode node); - protected override void InitializeWorker(AnalysisContext context) - { - context.RegisterSemanticModelAction(AnalyzeSemanticModel); - context.RegisterCompilationAction(AnalyzeCompilation); - } + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterSemanticModelAction(AnalyzeSemanticModel); + context.RegisterCompilationAction(AnalyzeCompilation); + } - private void AnalyzeSemanticModel(SemanticModelAnalysisContext context) - { - if (ShouldSkipAnalysis(context, notification: null)) - return; + private void AnalyzeSemanticModel(SemanticModelAnalysisContext context) + { + if (ShouldSkipAnalysis(context, notification: null)) + return; - var tree = context.SemanticModel.SyntaxTree; - var cancellationToken = context.CancellationToken; + var tree = context.SemanticModel.SyntaxTree; + var cancellationToken = context.CancellationToken; - var unnecessaryImports = UnnecessaryImportsProvider.GetUnnecessaryImports(context.SemanticModel, context.FilterSpan, cancellationToken); - if (unnecessaryImports.Any()) + var unnecessaryImports = UnnecessaryImportsProvider.GetUnnecessaryImports(context.SemanticModel, context.FilterSpan, cancellationToken); + if (unnecessaryImports.Any()) + { + // The IUnnecessaryImportsService will return individual import pieces that + // need to be removed. For example, it will return individual import-clauses + // from VB. However, we want to mark the entire import statement if we are + // going to remove all the clause. Defer to our subclass to stitch this up + // for us appropriately. + var mergedImports = MergeImports(unnecessaryImports); + + var descriptor = GeneratedCodeUtilities.IsGeneratedCode(tree, IsRegularCommentOrDocComment, cancellationToken) + ? _generatedCodeClassificationIdDescriptor + : _classificationIdDescriptor; + var contiguousSpans = GetContiguousSpans(mergedImports); + var diagnostics = + CreateClassificationDiagnostics(contiguousSpans, tree, descriptor, cancellationToken).Concat( + CreateFixableDiagnostics(mergedImports, tree, cancellationToken)); + + foreach (var diagnostic in diagnostics) { - // The IUnnecessaryImportsService will return individual import pieces that - // need to be removed. For example, it will return individual import-clauses - // from VB. However, we want to mark the entire import statement if we are - // going to remove all the clause. Defer to our subclass to stitch this up - // for us appropriately. - var mergedImports = MergeImports(unnecessaryImports); - - var descriptor = GeneratedCodeUtilities.IsGeneratedCode(tree, IsRegularCommentOrDocComment, cancellationToken) - ? _generatedCodeClassificationIdDescriptor - : _classificationIdDescriptor; - var contiguousSpans = GetContiguousSpans(mergedImports); - var diagnostics = - CreateClassificationDiagnostics(contiguousSpans, tree, descriptor, cancellationToken).Concat( - CreateFixableDiagnostics(mergedImports, tree, cancellationToken)); - - foreach (var diagnostic in diagnostics) - { - context.ReportDiagnostic(diagnostic); - } + context.ReportDiagnostic(diagnostic); } } + } - private void AnalyzeCompilation(CompilationAnalysisContext context) - { - // Due to https://github.com/dotnet/roslyn/issues/41640, enabling this analyzer (IDE0005) on build requires users - // to enable generation of XML documentation comments. We detect if generation of XML documentation comments - // is disabled for this tree and IDE0005 diagnostics are being reported with effective severity "Warning" or "Error". - // If so, we report a special diagnostic that recommends the users to set "GenerateDocumentationFile" to "true" - // in their project file to enable IDE0005 on build. + private void AnalyzeCompilation(CompilationAnalysisContext context) + { + // Due to https://github.com/dotnet/roslyn/issues/41640, enabling this analyzer (IDE0005) on build requires users + // to enable generation of XML documentation comments. We detect if generation of XML documentation comments + // is disabled for this tree and IDE0005 diagnostics are being reported with effective severity "Warning" or "Error". + // If so, we report a special diagnostic that recommends the users to set "GenerateDocumentationFile" to "true" + // in their project file to enable IDE0005 on build. - var compilation = context.Compilation; - if (!IsAnalysisLevelGreaterThanOrEquals(8, context.Options)) - return; + var compilation = context.Compilation; + if (!IsAnalysisLevelGreaterThanOrEquals(8, context.Options)) + return; - var tree = compilation.SyntaxTrees.FirstOrDefault(tree => !GeneratedCodeUtilities.IsGeneratedCode(tree, IsRegularCommentOrDocComment, context.CancellationToken)); - if (tree is null || tree.Options.DocumentationMode != DocumentationMode.None) - return; + var tree = compilation.SyntaxTrees.FirstOrDefault(tree => !GeneratedCodeUtilities.IsGeneratedCode(tree, IsRegularCommentOrDocComment, context.CancellationToken)); + if (tree is null || tree.Options.DocumentationMode != DocumentationMode.None) + return; - if (ShouldSkipAnalysis(tree, context.Options, compilation.Options, notification: null, context.CancellationToken)) - return; + if (ShouldSkipAnalysis(tree, context.Options, compilation.Options, notification: null, context.CancellationToken)) + return; - var effectiveSeverity = _classificationIdDescriptor.GetEffectiveSeverity(compilation.Options, tree, context.Options); - if (effectiveSeverity is ReportDiagnostic.Warn or ReportDiagnostic.Error) - { - context.ReportDiagnostic(Diagnostic.Create(s_enableGenerateDocumentationFileIdDescriptor, Location.None)); - } + var effectiveSeverity = _classificationIdDescriptor.GetEffectiveSeverity(compilation.Options, tree, context.Options); + if (effectiveSeverity is ReportDiagnostic.Warn or ReportDiagnostic.Error) + { + context.ReportDiagnostic(Diagnostic.Create(s_enableGenerateDocumentationFileIdDescriptor, Location.None)); } + } - private IEnumerable GetContiguousSpans(ImmutableArray nodes) - { - var syntaxFacts = this.SyntaxFacts; - (SyntaxNode node, TextSpan textSpan)? previous = null; + private IEnumerable GetContiguousSpans(ImmutableArray nodes) + { + var syntaxFacts = this.SyntaxFacts; + (SyntaxNode node, TextSpan textSpan)? previous = null; - // Sort the nodes in source location order. - foreach (var node in nodes.OrderBy(n => n.SpanStart)) + // Sort the nodes in source location order. + foreach (var node in nodes.OrderBy(n => n.SpanStart)) + { + TextSpan textSpan; + var nodeEnd = GetEnd(node); + if (previous == null) + { + textSpan = TextSpan.FromBounds(node.Span.Start, nodeEnd); + } + else { - TextSpan textSpan; - var nodeEnd = GetEnd(node); - if (previous == null) + var lastToken = TryGetLastToken(previous.Value.node) ?? previous.Value.node.GetLastToken(); + if (lastToken.GetNextToken(includeDirectives: true) == node.GetFirstToken()) { - textSpan = TextSpan.FromBounds(node.Span.Start, nodeEnd); + // Expand the span + textSpan = TextSpan.FromBounds(previous.Value.textSpan.Start, nodeEnd); } else { - var lastToken = TryGetLastToken(previous.Value.node) ?? previous.Value.node.GetLastToken(); - if (lastToken.GetNextToken(includeDirectives: true) == node.GetFirstToken()) - { - // Expand the span - textSpan = TextSpan.FromBounds(previous.Value.textSpan.Start, nodeEnd); - } - else - { - // Return the last span, and start a new one - yield return previous.Value.textSpan; - textSpan = TextSpan.FromBounds(node.Span.Start, nodeEnd); - } + // Return the last span, and start a new one + yield return previous.Value.textSpan; + textSpan = TextSpan.FromBounds(node.Span.Start, nodeEnd); } - - previous = (node, textSpan); } - if (previous.HasValue) - yield return previous.Value.textSpan; + previous = (node, textSpan); + } - yield break; + if (previous.HasValue) + yield return previous.Value.textSpan; - int GetEnd(SyntaxNode node) - { - var end = node.Span.End; - foreach (var trivia in node.GetTrailingTrivia()) - { - if (syntaxFacts.IsRegularComment(trivia)) - end = trivia.Span.End; - } + yield break; - return end; + int GetEnd(SyntaxNode node) + { + var end = node.Span.End; + foreach (var trivia in node.GetTrailingTrivia()) + { + if (syntaxFacts.IsRegularComment(trivia)) + end = trivia.Span.End; } + + return end; } + } - // Create one diagnostic for each unnecessary span that will be classified as Unnecessary - private static IEnumerable CreateClassificationDiagnostics( - IEnumerable contiguousSpans, SyntaxTree tree, - DiagnosticDescriptor descriptor, CancellationToken cancellationToken) + // Create one diagnostic for each unnecessary span that will be classified as Unnecessary + private static IEnumerable CreateClassificationDiagnostics( + IEnumerable contiguousSpans, SyntaxTree tree, + DiagnosticDescriptor descriptor, CancellationToken cancellationToken) + { + foreach (var span in contiguousSpans) { - foreach (var span in contiguousSpans) + if (tree.OverlapsHiddenPosition(span, cancellationToken)) { - if (tree.OverlapsHiddenPosition(span, cancellationToken)) - { - continue; - } - - yield return Diagnostic.Create(descriptor, tree.GetLocation(span)); + continue; } - } - protected abstract IEnumerable GetFixableDiagnosticSpans( - IEnumerable nodes, SyntaxTree tree, CancellationToken cancellationToken); + yield return Diagnostic.Create(descriptor, tree.GetLocation(span)); + } + } - private IEnumerable CreateFixableDiagnostics( - IEnumerable nodes, SyntaxTree tree, CancellationToken cancellationToken) - { - var spans = GetFixableDiagnosticSpans(nodes, tree, cancellationToken); + protected abstract IEnumerable GetFixableDiagnosticSpans( + IEnumerable nodes, SyntaxTree tree, CancellationToken cancellationToken); - foreach (var span in spans) - yield return Diagnostic.Create(s_fixableIdDescriptor, tree.GetLocation(span)); - } + private IEnumerable CreateFixableDiagnostics( + IEnumerable nodes, SyntaxTree tree, CancellationToken cancellationToken) + { + var spans = GetFixableDiagnosticSpans(nodes, tree, cancellationToken); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + foreach (var span in spans) + yield return Diagnostic.Create(s_fixableIdDescriptor, tree.GetLocation(span)); } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/RemoveUnnecessaryImportsConstants.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/RemoveUnnecessaryImportsConstants.cs index 469a4c1f8bceb..4e9b025ce46d0 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/RemoveUnnecessaryImportsConstants.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/RemoveUnnecessaryImportsConstants.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.RemoveUnnecessaryImports +namespace Microsoft.CodeAnalysis.RemoveUnnecessaryImports; + +internal static class RemoveUnnecessaryImportsConstants { - internal static class RemoveUnnecessaryImportsConstants - { - // NOTE: This is a trigger diagnostic, which doesn't show up in the ruleset editor and hence doesn't need a conventional IDE Diagnostic ID string. - public const string DiagnosticFixableId = "RemoveUnnecessaryImportsFixable"; - } + // NOTE: This is a trigger diagnostic, which doesn't show up in the ruleset editor and hence doesn't need a conventional IDE Diagnostic ID string. + public const string DiagnosticFixableId = "RemoveUnnecessaryImportsFixable"; } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer.cs index a8508216b86f9..301d28bc17682 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer.cs @@ -12,126 +12,126 @@ using Microsoft.CodeAnalysis.Precedence; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses +namespace Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses; + +internal abstract class AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer< + TLanguageKindEnum, + TParenthesizedExpressionSyntax> + : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + where TLanguageKindEnum : struct + where TParenthesizedExpressionSyntax : SyntaxNode { - internal abstract class AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer< - TLanguageKindEnum, - TParenthesizedExpressionSyntax> - : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer - where TLanguageKindEnum : struct - where TParenthesizedExpressionSyntax : SyntaxNode + protected AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer() + : base(IDEDiagnosticIds.RemoveUnnecessaryParenthesesDiagnosticId, + EnforceOnBuildValues.RemoveUnnecessaryParentheses, + options: ParenthesesDiagnosticAnalyzersHelper.Options, + fadingOption: null, + new LocalizableResourceString(nameof(AnalyzersResources.Remove_unnecessary_parentheses), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Parentheses_can_be_removed), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - protected AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer() - : base(IDEDiagnosticIds.RemoveUnnecessaryParenthesesDiagnosticId, - EnforceOnBuildValues.RemoveUnnecessaryParentheses, - options: ParenthesesDiagnosticAnalyzersHelper.Options, - fadingOption: null, - new LocalizableResourceString(nameof(AnalyzersResources.Remove_unnecessary_parentheses), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Parentheses_can_be_removed), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + } + + protected abstract TLanguageKindEnum GetSyntaxKind(); + protected abstract ISyntaxFacts GetSyntaxFacts(); - protected abstract TLanguageKindEnum GetSyntaxKind(); - protected abstract ISyntaxFacts GetSyntaxFacts(); + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + protected sealed override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeSyntax, GetSyntaxKind()); - protected sealed override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeSyntax, GetSyntaxKind()); + protected abstract bool CanRemoveParentheses( + TParenthesizedExpressionSyntax parenthesizedExpression, SemanticModel semanticModel, CancellationToken cancellationToken, + out PrecedenceKind precedence, out bool clarifiesPrecedence); - protected abstract bool CanRemoveParentheses( - TParenthesizedExpressionSyntax parenthesizedExpression, SemanticModel semanticModel, CancellationToken cancellationToken, - out PrecedenceKind precedence, out bool clarifiesPrecedence); + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var cancellationToken = context.CancellationToken; + var parenthesizedExpression = (TParenthesizedExpressionSyntax)context.Node; - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + if (!CanRemoveParentheses(parenthesizedExpression, context.SemanticModel, cancellationToken, + out var precedence, out var clarifiesPrecedence)) { - var cancellationToken = context.CancellationToken; - var parenthesizedExpression = (TParenthesizedExpressionSyntax)context.Node; - - if (!CanRemoveParentheses(parenthesizedExpression, context.SemanticModel, cancellationToken, - out var precedence, out var clarifiesPrecedence)) - { - return; - } - - // Do not remove parentheses from these expressions when there are different kinds - // between the parent and child of the parenthesized expr.. This is because removing - // these parens can significantly decrease readability and can confuse many people - // (including several people quizzed on Roslyn). For example, most people see - // "1 + 2 << 3" as "1 + (2 << 3)", when it's actually "(1 + 2) << 3". To avoid - // making code bases more confusing, we just do not touch parens for these constructs - // unless both the child and parent have the same kinds. - switch (precedence) - { - case PrecedenceKind.Shift: - case PrecedenceKind.Bitwise: - case PrecedenceKind.Coalesce: - var syntaxFacts = GetSyntaxFacts(); - var child = syntaxFacts.GetExpressionOfParenthesizedExpression(parenthesizedExpression); - - var parentKind = parenthesizedExpression.Parent?.RawKind; - var childKind = child.RawKind; - if (parentKind != childKind) - { - return; - } - - // Ok to remove if it was the exact same kind. i.e. ```(a | b) | c``` - // not ok to remove if kinds changed. i.e. ```(a + b) << c``` - break; - } - - var options = context.GetAnalyzerOptions(); - var preference = ParenthesesDiagnosticAnalyzersHelper.GetLanguageOption(options, precedence); - - if (ShouldSkipAnalysis(context, preference.Notification)) - { - // User doesn't care about these parens. So nothing for us to do. - return; - } - - if (preference.Value == ParenthesesPreference.AlwaysForClarity && - clarifiesPrecedence) - { - // User wants these parens if they clarify precedence, and these parens - // clarify precedence. So keep these around. - return; - } - - // either they don't want unnecessary parentheses, or they want them only for - // clarification purposes and this does not make things clear. - Debug.Assert(preference.Value == ParenthesesPreference.NeverIfUnnecessary || - !clarifiesPrecedence); - - var additionalLocations = ImmutableArray.Create( - parenthesizedExpression.GetLocation()); - var additionalUnnecessaryLocations = ImmutableArray.Create( - parenthesizedExpression.GetFirstToken().GetLocation(), - parenthesizedExpression.GetLastToken().GetLocation()); - - context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( - Descriptor, - AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer.GetDiagnosticSquiggleLocation(parenthesizedExpression, cancellationToken), - preference.Notification, - additionalLocations, - additionalUnnecessaryLocations)); + return; } - /// - /// Gets the span of text to squiggle underline. - /// If the expression is contained within a single line, the entire expression span is returned. - /// Otherwise it will return the span from the expression start to the end of the same line. - /// - private static Location GetDiagnosticSquiggleLocation(TParenthesizedExpressionSyntax parenthesizedExpression, CancellationToken cancellationToken) + // Do not remove parentheses from these expressions when there are different kinds + // between the parent and child of the parenthesized expr.. This is because removing + // these parens can significantly decrease readability and can confuse many people + // (including several people quizzed on Roslyn). For example, most people see + // "1 + 2 << 3" as "1 + (2 << 3)", when it's actually "(1 + 2) << 3". To avoid + // making code bases more confusing, we just do not touch parens for these constructs + // unless both the child and parent have the same kinds. + switch (precedence) { - var parenthesizedExpressionLocation = parenthesizedExpression.GetLocation(); + case PrecedenceKind.Shift: + case PrecedenceKind.Bitwise: + case PrecedenceKind.Coalesce: + var syntaxFacts = GetSyntaxFacts(); + var child = syntaxFacts.GetExpressionOfParenthesizedExpression(parenthesizedExpression); + + var parentKind = parenthesizedExpression.Parent?.RawKind; + var childKind = child.RawKind; + if (parentKind != childKind) + { + return; + } + + // Ok to remove if it was the exact same kind. i.e. ```(a | b) | c``` + // not ok to remove if kinds changed. i.e. ```(a + b) << c``` + break; + } - var lines = parenthesizedExpression.SyntaxTree.GetText(cancellationToken).Lines; - var expressionFirstLine = lines.GetLineFromPosition(parenthesizedExpressionLocation.SourceSpan.Start); + var options = context.GetAnalyzerOptions(); + var preference = ParenthesesDiagnosticAnalyzersHelper.GetLanguageOption(options, precedence); + + if (ShouldSkipAnalysis(context, preference.Notification)) + { + // User doesn't care about these parens. So nothing for us to do. + return; + } - var textSpanEndPosition = Math.Min(parenthesizedExpressionLocation.SourceSpan.End, expressionFirstLine.Span.End); - return Location.Create(parenthesizedExpression.SyntaxTree, TextSpan.FromBounds(parenthesizedExpressionLocation.SourceSpan.Start, textSpanEndPosition)); + if (preference.Value == ParenthesesPreference.AlwaysForClarity && + clarifiesPrecedence) + { + // User wants these parens if they clarify precedence, and these parens + // clarify precedence. So keep these around. + return; } + + // either they don't want unnecessary parentheses, or they want them only for + // clarification purposes and this does not make things clear. + Debug.Assert(preference.Value == ParenthesesPreference.NeverIfUnnecessary || + !clarifiesPrecedence); + + var additionalLocations = ImmutableArray.Create( + parenthesizedExpression.GetLocation()); + var additionalUnnecessaryLocations = ImmutableArray.Create( + parenthesizedExpression.GetFirstToken().GetLocation(), + parenthesizedExpression.GetLastToken().GetLocation()); + + context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( + Descriptor, + AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer.GetDiagnosticSquiggleLocation(parenthesizedExpression, cancellationToken), + preference.Notification, + context.Options, + additionalLocations, + additionalUnnecessaryLocations)); + } + + /// + /// Gets the span of text to squiggle underline. + /// If the expression is contained within a single line, the entire expression span is returned. + /// Otherwise it will return the span from the expression start to the end of the same line. + /// + private static Location GetDiagnosticSquiggleLocation(TParenthesizedExpressionSyntax parenthesizedExpression, CancellationToken cancellationToken) + { + var parenthesizedExpressionLocation = parenthesizedExpression.GetLocation(); + + var lines = parenthesizedExpression.SyntaxTree.GetText(cancellationToken).Lines; + var expressionFirstLine = lines.GetLineFromPosition(parenthesizedExpressionLocation.SourceSpan.Start); + + var textSpanEndPosition = Math.Min(parenthesizedExpressionLocation.SourceSpan.End, expressionFirstLine.Span.End); + return Location.Create(parenthesizedExpression.SyntaxTree, TextSpan.FromBounds(parenthesizedExpressionLocation.SourceSpan.Start, textSpanEndPosition)); } } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer.cs index 8290a1f87bb00..2db23be7731c2 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer.cs @@ -10,109 +10,108 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions +namespace Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions; + +internal abstract class AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer + : AbstractCodeQualityDiagnosticAnalyzer { - internal abstract class AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer - : AbstractCodeQualityDiagnosticAnalyzer + internal const string DocCommentIdKey = nameof(DocCommentIdKey); + + private static readonly LocalizableResourceString s_localizableTitle = new( + nameof(AnalyzersResources.Invalid_global_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + private static readonly LocalizableResourceString s_localizableInvalidScopeMessage = new( + nameof(AnalyzersResources.Invalid_scope_for_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + private static readonly LocalizableResourceString s_localizableInvalidOrMissingTargetMessage = new( + nameof(AnalyzersResources.Invalid_or_missing_target_for_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + + private static readonly DiagnosticDescriptor s_invalidScopeDescriptor = CreateDescriptor( + IDEDiagnosticIds.InvalidSuppressMessageAttributeDiagnosticId, + EnforceOnBuildValues.InvalidSuppressMessageAttribute, + s_localizableTitle, s_localizableInvalidScopeMessage, + hasAnyCodeStyleOption: false, isUnnecessary: true); + private static readonly DiagnosticDescriptor s_invalidOrMissingTargetDescriptor = CreateDescriptor( + IDEDiagnosticIds.InvalidSuppressMessageAttributeDiagnosticId, + EnforceOnBuildValues.InvalidSuppressMessageAttribute, + s_localizableTitle, s_localizableInvalidOrMissingTargetMessage, + hasAnyCodeStyleOption: false, isUnnecessary: true); + + private static readonly LocalizableResourceString s_localizableLegacyFormatTitle = new( + nameof(AnalyzersResources.Avoid_legacy_format_target_in_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + private static readonly LocalizableResourceString s_localizableLegacyFormatMessage = new( + nameof(AnalyzersResources.Avoid_legacy_format_target_0_in_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + internal static readonly DiagnosticDescriptor LegacyFormatTargetDescriptor = CreateDescriptor( + IDEDiagnosticIds.LegacyFormatSuppressMessageAttributeDiagnosticId, + EnforceOnBuildValues.LegacyFormatSuppressMessageAttribute, + s_localizableLegacyFormatTitle, s_localizableLegacyFormatMessage, + hasAnyCodeStyleOption: false, isUnnecessary: false); + + protected AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer() + : base([s_invalidScopeDescriptor, s_invalidOrMissingTargetDescriptor, LegacyFormatTargetDescriptor], GeneratedCodeAnalysisFlags.None) { - internal const string DocCommentIdKey = nameof(DocCommentIdKey); - - private static readonly LocalizableResourceString s_localizableTitle = new( - nameof(AnalyzersResources.Invalid_global_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - private static readonly LocalizableResourceString s_localizableInvalidScopeMessage = new( - nameof(AnalyzersResources.Invalid_scope_for_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - private static readonly LocalizableResourceString s_localizableInvalidOrMissingTargetMessage = new( - nameof(AnalyzersResources.Invalid_or_missing_target_for_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - - private static readonly DiagnosticDescriptor s_invalidScopeDescriptor = CreateDescriptor( - IDEDiagnosticIds.InvalidSuppressMessageAttributeDiagnosticId, - EnforceOnBuildValues.InvalidSuppressMessageAttribute, - s_localizableTitle, s_localizableInvalidScopeMessage, - hasAnyCodeStyleOption: false, isUnnecessary: true); - private static readonly DiagnosticDescriptor s_invalidOrMissingTargetDescriptor = CreateDescriptor( - IDEDiagnosticIds.InvalidSuppressMessageAttributeDiagnosticId, - EnforceOnBuildValues.InvalidSuppressMessageAttribute, - s_localizableTitle, s_localizableInvalidOrMissingTargetMessage, - hasAnyCodeStyleOption: false, isUnnecessary: true); - - private static readonly LocalizableResourceString s_localizableLegacyFormatTitle = new( - nameof(AnalyzersResources.Avoid_legacy_format_target_in_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - private static readonly LocalizableResourceString s_localizableLegacyFormatMessage = new( - nameof(AnalyzersResources.Avoid_legacy_format_target_0_in_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - internal static readonly DiagnosticDescriptor LegacyFormatTargetDescriptor = CreateDescriptor( - IDEDiagnosticIds.LegacyFormatSuppressMessageAttributeDiagnosticId, - EnforceOnBuildValues.LegacyFormatSuppressMessageAttribute, - s_localizableLegacyFormatTitle, s_localizableLegacyFormatMessage, - hasAnyCodeStyleOption: false, isUnnecessary: false); - - protected AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer() - : base([s_invalidScopeDescriptor, s_invalidOrMissingTargetDescriptor, LegacyFormatTargetDescriptor], GeneratedCodeAnalysisFlags.None) - { - } + } - protected abstract void RegisterAttributeSyntaxAction(CompilationStartAnalysisContext context, CompilationAnalyzer compilationAnalyzer); - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + protected abstract void RegisterAttributeSyntaxAction(CompilationStartAnalysisContext context, CompilationAnalyzer compilationAnalyzer); + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; - protected sealed override void InitializeWorker(AnalysisContext context) + protected sealed override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - context.RegisterCompilationStartAction(context => + var suppressMessageAttributeType = context.Compilation.SuppressMessageAttributeType(); + if (suppressMessageAttributeType == null) { - var suppressMessageAttributeType = context.Compilation.SuppressMessageAttributeType(); - if (suppressMessageAttributeType == null) - { - return; - } + return; + } - RegisterAttributeSyntaxAction(context, new CompilationAnalyzer(context.Compilation, suppressMessageAttributeType)); - }); - } + RegisterAttributeSyntaxAction(context, new CompilationAnalyzer(context.Compilation, suppressMessageAttributeType)); + }); + } - protected sealed class CompilationAnalyzer(Compilation compilation, INamedTypeSymbol suppressMessageAttributeType) + protected sealed class CompilationAnalyzer(Compilation compilation, INamedTypeSymbol suppressMessageAttributeType) + { + private readonly SuppressMessageAttributeState _state = new SuppressMessageAttributeState(compilation, suppressMessageAttributeType); + + public void AnalyzeAssemblyOrModuleAttribute(SyntaxNode attributeSyntax, SemanticModel model, Action reportDiagnostic, CancellationToken cancellationToken) { - private readonly SuppressMessageAttributeState _state = new SuppressMessageAttributeState(compilation, suppressMessageAttributeType); + if (!_state.IsSuppressMessageAttributeWithNamedArguments(attributeSyntax, model, cancellationToken, out var namedAttributeArguments)) + { + return; + } - public void AnalyzeAssemblyOrModuleAttribute(SyntaxNode attributeSyntax, SemanticModel model, Action reportDiagnostic, CancellationToken cancellationToken) + if (!SuppressMessageAttributeState.HasValidScope(namedAttributeArguments, out var targetScope)) { - if (!_state.IsSuppressMessageAttributeWithNamedArguments(attributeSyntax, model, cancellationToken, out var namedAttributeArguments)) - { - return; - } + reportDiagnostic(Diagnostic.Create(s_invalidScopeDescriptor, attributeSyntax.GetLocation())); + return; + } - if (!SuppressMessageAttributeState.HasValidScope(namedAttributeArguments, out var targetScope)) - { - reportDiagnostic(Diagnostic.Create(s_invalidScopeDescriptor, attributeSyntax.GetLocation())); - return; - } + if (!_state.HasValidTarget(namedAttributeArguments, targetScope, out var targetHasDocCommentIdFormat, + out var targetSymbolString, out var targetValueOperation, out var resolvedSymbols)) + { + reportDiagnostic(Diagnostic.Create(s_invalidOrMissingTargetDescriptor, attributeSyntax.GetLocation())); + return; + } - if (!_state.HasValidTarget(namedAttributeArguments, targetScope, out var targetHasDocCommentIdFormat, - out var targetSymbolString, out var targetValueOperation, out var resolvedSymbols)) - { - reportDiagnostic(Diagnostic.Create(s_invalidOrMissingTargetDescriptor, attributeSyntax.GetLocation())); - return; - } + // We want to flag valid target which uses legacy format to update to Roslyn based DocCommentId format. + if (resolvedSymbols.Length > 0 && !targetHasDocCommentIdFormat) + { + RoslynDebug.Assert(!string.IsNullOrEmpty(targetSymbolString)); + RoslynDebug.Assert(targetValueOperation != null); - // We want to flag valid target which uses legacy format to update to Roslyn based DocCommentId format. - if (resolvedSymbols.Length > 0 && !targetHasDocCommentIdFormat) + var properties = ImmutableDictionary.Empty; + if (resolvedSymbols is [var resolvedSymbol]) { - RoslynDebug.Assert(!string.IsNullOrEmpty(targetSymbolString)); - RoslynDebug.Assert(targetValueOperation != null); - - var properties = ImmutableDictionary.Empty; - if (resolvedSymbols is [var resolvedSymbol]) + // We provide a code fix for the case where the target resolved to a single symbol. + var docCommentId = DocumentationCommentId.CreateDeclarationId(resolvedSymbol); + if (!string.IsNullOrEmpty(docCommentId)) { - // We provide a code fix for the case where the target resolved to a single symbol. - var docCommentId = DocumentationCommentId.CreateDeclarationId(resolvedSymbol); - if (!string.IsNullOrEmpty(docCommentId)) - { - // Suppression target has an optional "~" prefix to distinguish it from legacy FxCop suppressions. - // IDE suppression code fixes emit this prefix, so we we also add this prefix to new suppression target string. - properties = properties.Add(DocCommentIdKey, "~" + docCommentId); - } + // Suppression target has an optional "~" prefix to distinguish it from legacy FxCop suppressions. + // IDE suppression code fixes emit this prefix, so we we also add this prefix to new suppression target string. + properties = properties.Add(DocCommentIdKey, "~" + docCommentId); } - - reportDiagnostic(Diagnostic.Create(LegacyFormatTargetDescriptor, targetValueOperation.Syntax.GetLocation(), properties!, targetSymbolString)); - return; } + + reportDiagnostic(Diagnostic.Create(LegacyFormatTargetDescriptor, targetValueOperation.Syntax.GetLocation(), properties!, targetSymbolString)); + return; } } } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs index d3ad28f150f77..f7e43a6d2853e 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs @@ -20,837 +20,836 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions +namespace Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions; + +internal abstract class AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer + : AbstractCodeQualityDiagnosticAnalyzer, IPragmaSuppressionsAnalyzer { - internal abstract class AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer - : AbstractCodeQualityDiagnosticAnalyzer, IPragmaSuppressionsAnalyzer + private static readonly LocalizableResourceString s_localizableRemoveUnnecessarySuppression = new( + nameof(AnalyzersResources.Remove_unnecessary_suppression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + internal static readonly DiagnosticDescriptor s_removeUnnecessarySuppressionDescriptor = CreateDescriptor( + IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId, + EnforceOnBuildValues.RemoveUnnecessarySuppression, + s_localizableRemoveUnnecessarySuppression, s_localizableRemoveUnnecessarySuppression, + hasAnyCodeStyleOption: false, isUnnecessary: true); + + private readonly Lazy> _lazySupportedCompilerErrorCodes; + + protected AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer() + : base([s_removeUnnecessarySuppressionDescriptor], GeneratedCodeAnalysisFlags.None) { - private static readonly LocalizableResourceString s_localizableRemoveUnnecessarySuppression = new( - nameof(AnalyzersResources.Remove_unnecessary_suppression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - internal static readonly DiagnosticDescriptor s_removeUnnecessarySuppressionDescriptor = CreateDescriptor( - IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId, - EnforceOnBuildValues.RemoveUnnecessarySuppression, - s_localizableRemoveUnnecessarySuppression, s_localizableRemoveUnnecessarySuppression, - hasAnyCodeStyleOption: false, isUnnecessary: true); - - private readonly Lazy> _lazySupportedCompilerErrorCodes; - - protected AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer() - : base([s_removeUnnecessarySuppressionDescriptor], GeneratedCodeAnalysisFlags.None) - { - _lazySupportedCompilerErrorCodes = new Lazy>(GetSupportedCompilerErrorCodes); - } + _lazySupportedCompilerErrorCodes = new Lazy>(GetSupportedCompilerErrorCodes); + } - protected abstract string CompilerErrorCodePrefix { get; } - protected abstract int CompilerErrorCodeDigitCount { get; } - protected abstract ISyntaxFacts SyntaxFacts { get; } - protected abstract ISemanticFacts SemanticFacts { get; } - protected abstract (Assembly assembly, string typeName) GetCompilerDiagnosticAnalyzerInfo(); - protected abstract bool ContainsPragmaDirective(SyntaxNode root); + protected abstract string CompilerErrorCodePrefix { get; } + protected abstract int CompilerErrorCodeDigitCount { get; } + protected abstract ISyntaxFacts SyntaxFacts { get; } + protected abstract ISemanticFacts SemanticFacts { get; } + protected abstract (Assembly assembly, string typeName) GetCompilerDiagnosticAnalyzerInfo(); + protected abstract bool ContainsPragmaDirective(SyntaxNode root); - private ImmutableHashSet GetSupportedCompilerErrorCodes() + private ImmutableHashSet GetSupportedCompilerErrorCodes() + { + try { - try - { - // Use reflection to fetch compiler diagnostic IDs that are supported in IDE live analysis. - // Note that the unit test projects have IVT access to compiler layer, and hence can access this API. - // We have unit tests that guard this reflection based logic and will fail if the API is changed - // without updating the below code. - - var (assembly, compilerAnalyzerTypeName) = GetCompilerDiagnosticAnalyzerInfo(); - var compilerAnalyzerType = assembly.GetType(compilerAnalyzerTypeName)!; - var methodInfo = compilerAnalyzerType.GetMethod("GetSupportedErrorCodes", BindingFlags.Instance | BindingFlags.NonPublic)!; - var compilerAnalyzerInstance = Activator.CreateInstance(compilerAnalyzerType); - var supportedCodes = methodInfo.Invoke(compilerAnalyzerInstance, []) as IEnumerable; - return supportedCodes?.ToImmutableHashSet() ?? []; - } - catch (Exception ex) - { - Debug.Fail(ex.Message); - return []; - } + // Use reflection to fetch compiler diagnostic IDs that are supported in IDE live analysis. + // Note that the unit test projects have IVT access to compiler layer, and hence can access this API. + // We have unit tests that guard this reflection based logic and will fail if the API is changed + // without updating the below code. + + var (assembly, compilerAnalyzerTypeName) = GetCompilerDiagnosticAnalyzerInfo(); + var compilerAnalyzerType = assembly.GetType(compilerAnalyzerTypeName)!; + var methodInfo = compilerAnalyzerType.GetMethod("GetSupportedErrorCodes", BindingFlags.Instance | BindingFlags.NonPublic)!; + var compilerAnalyzerInstance = Activator.CreateInstance(compilerAnalyzerType); + var supportedCodes = methodInfo.Invoke(compilerAnalyzerInstance, []) as IEnumerable; + return supportedCodes?.ToImmutableHashSet() ?? []; + } + catch (Exception ex) + { + Debug.Fail(ex.Message); + return []; } + } + + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + protected sealed override void InitializeWorker(AnalysisContext context) + { + // We do not register any normal analyzer actions as we need 'CompilationWithAnalyzers' + // context to analyze unused suppressions using reported compiler and analyzer diagnostics. + // Instead, the analyzer defines a special 'AnalyzeAsync' method that should be invoked + // by the host with CompilationWithAnalyzers input to compute unused suppression diagnostics. + } - protected sealed override void InitializeWorker(AnalysisContext context) + public async Task AnalyzeAsync( + SemanticModel semanticModel, + TextSpan? span, + CompilationWithAnalyzers compilationWithAnalyzers, + Func> getSupportedDiagnostics, + Action reportDiagnostic, + CancellationToken cancellationToken) + { + // We need compilation with suppressed diagnostics for this feature. + if (!compilationWithAnalyzers.Compilation.Options.ReportSuppressedDiagnostics) { - // We do not register any normal analyzer actions as we need 'CompilationWithAnalyzers' - // context to analyze unused suppressions using reported compiler and analyzer diagnostics. - // Instead, the analyzer defines a special 'AnalyzeAsync' method that should be invoked - // by the host with CompilationWithAnalyzers input to compute unused suppression diagnostics. + return; } - public async Task AnalyzeAsync( - SemanticModel semanticModel, - TextSpan? span, - CompilationWithAnalyzers compilationWithAnalyzers, - Func> getSupportedDiagnostics, - Action reportDiagnostic, - CancellationToken cancellationToken) + var tree = semanticModel.SyntaxTree; + + // Bail out if analyzer is suppressed on this file or project. + // NOTE: Normally, we would not require this check in the analyzer as the analyzer driver has this optimization. + // However, this is a special analyzer that is directly invoked by the analysis host (IDE), so we do this check here. + if (compilationWithAnalyzers.Compilation.Options.SyntaxTreeOptionsProvider != null && + compilationWithAnalyzers.Compilation.Options.SyntaxTreeOptionsProvider.TryGetDiagnosticValue(tree, IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId, cancellationToken, out var severity) || + compilationWithAnalyzers.Compilation.Options.SpecificDiagnosticOptions.TryGetValue(IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId, out severity)) { - // We need compilation with suppressed diagnostics for this feature. - if (!compilationWithAnalyzers.Compilation.Options.ReportSuppressedDiagnostics) + if (severity == ReportDiagnostic.Suppress) { return; } + } - var tree = semanticModel.SyntaxTree; + // Bail out if analyzer has been turned off through options. + var option = compilationWithAnalyzers.AnalysisOptions.Options?.GetAnalyzerOptions(tree).RemoveUnnecessarySuppressionExclusions.Trim(); + var (userIdExclusions, userCategoryExclusions, analyzerDisabled) = ParseUserExclusions(option); + if (analyzerDisabled) + { + return; + } - // Bail out if analyzer is suppressed on this file or project. - // NOTE: Normally, we would not require this check in the analyzer as the analyzer driver has this optimization. - // However, this is a special analyzer that is directly invoked by the analysis host (IDE), so we do this check here. - if (compilationWithAnalyzers.Compilation.Options.SyntaxTreeOptionsProvider != null && - compilationWithAnalyzers.Compilation.Options.SyntaxTreeOptionsProvider.TryGetDiagnosticValue(tree, IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId, cancellationToken, out var severity) || - compilationWithAnalyzers.Compilation.Options.SpecificDiagnosticOptions.TryGetValue(IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId, out severity)) - { - if (severity == ReportDiagnostic.Suppress) - { - return; - } - } + // Bail out for generated code. + if (tree.IsGeneratedCode(compilationWithAnalyzers.AnalysisOptions.Options, SyntaxFacts, cancellationToken)) + { + return; + } - // Bail out if analyzer has been turned off through options. - var option = compilationWithAnalyzers.AnalysisOptions.Options?.GetAnalyzerOptions(tree).RemoveUnnecessarySuppressionExclusions.Trim(); - var (userIdExclusions, userCategoryExclusions, analyzerDisabled) = ParseUserExclusions(option); - if (analyzerDisabled) - { - return; - } + var root = tree.GetRoot(cancellationToken); - // Bail out for generated code. - if (tree.IsGeneratedCode(compilationWithAnalyzers.AnalysisOptions.Options, SyntaxFacts, cancellationToken)) - { - return; - } + // Bail out if tree has syntax errors. + if (root.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + { + return; + } - var root = tree.GetRoot(cancellationToken); + // Process pragma directives and inline SuppressMessageAttributes in the tree. + // The core algorithm is as follows: + // 1. Iterate through all the active pragmas and local SuppressMessageAttributes in the source file and + // identify the pragmas and local SuppressMessageAttributes + // with diagnostics IDs for which we support unnecessary suppression analysis. + // 2. Build the following data structures during this loop: + // a. A map from diagnostic ID to list of pragmas for the ID. This map tracks supported diagnostic IDs for this tree's pragmas. + // b. A array of tuples of candidate pragmas sorted by span, along with associated IDs and enable/disable flag. + // This sorted array allows mapping an unnecessary pragma to the corresponding toggling pragma pair for removal. + // c. A map from pragmas to a boolean indicating if the pragma was used or not. + // d. A map from diagnostic ID to list of SuppressMessageAttribute nodes for the ID. + // This map tracks supported diagnostic IDs for this tree's SuppressMessageAttribute nodes. + // e. A map from SuppressMessageAttribute nodes to a boolean indicating if the attribute was used or not. + // f. A set of supported compiler diagnostic IDs that are used in pragmas or SuppressMessageAttributes in this file. + // 3. Map the set of candidate diagnostic IDs to the analyzers that can report diagnostics with these IDs. + // 4. Execute these analyzers to compute the diagnostics reported by these analyzers in this file. + // 5. Iterate through the suppressed diagnostics from this list and do the following: + // a. If the diagnostic was suppressed with a pragma, mark the closest preceding disable pragma + // which suppresses this ID as used/necessary. Also mark the matching restore pragma as used. + // b. Otherwise, if the diagnostic was suppressed with SuppressMessageAttribute, mark the attribute as used. + // 6. Finally, report a diagnostic all the pragmas and SuppressMessageAttributes which have not been marked as used. + + using var _1 = PooledDictionary>.GetInstance(out var idToPragmasMap); + using var _2 = ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)>.GetInstance(out var sortedPragmasWithIds); + using var _3 = PooledDictionary.GetInstance(out var pragmasToIsUsedMap); + using var _4 = PooledHashSet.GetInstance(out var compilerDiagnosticIds); + var hasPragmaInAnalysisSpan = ProcessPragmaDirectives(root, span, idToPragmasMap, + pragmasToIsUsedMap, sortedPragmasWithIds, compilerDiagnosticIds, userIdExclusions); + + cancellationToken.ThrowIfCancellationRequested(); + + using var _5 = PooledDictionary>.GetInstance(out var idToSuppressMessageAttributesMap); + using var _6 = PooledDictionary.GetInstance(out var suppressMessageAttributesToIsUsedMap); + var hasAttributeInAnalysisSpan = await ProcessSuppressMessageAttributesAsync(root, semanticModel, span, + idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, userIdExclusions, userCategoryExclusions, cancellationToken).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + // Bail out if we have no pragma directives or SuppressMessageAttributes to analyze. + if (!hasPragmaInAnalysisSpan && !hasAttributeInAnalysisSpan) + { + return; + } - // Bail out if tree has syntax errors. - if (root.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) - { - return; - } + using var _8 = PooledHashSet.GetInstance(out var idsToAnalyzeBuilder); + idsToAnalyzeBuilder.AddAll(idToPragmasMap.Keys); + idsToAnalyzeBuilder.AddAll(idToSuppressMessageAttributesMap.Keys); + var idsToAnalyze = idsToAnalyzeBuilder.ToImmutableHashSet(); - // Process pragma directives and inline SuppressMessageAttributes in the tree. - // The core algorithm is as follows: - // 1. Iterate through all the active pragmas and local SuppressMessageAttributes in the source file and - // identify the pragmas and local SuppressMessageAttributes - // with diagnostics IDs for which we support unnecessary suppression analysis. - // 2. Build the following data structures during this loop: - // a. A map from diagnostic ID to list of pragmas for the ID. This map tracks supported diagnostic IDs for this tree's pragmas. - // b. A array of tuples of candidate pragmas sorted by span, along with associated IDs and enable/disable flag. - // This sorted array allows mapping an unnecessary pragma to the corresponding toggling pragma pair for removal. - // c. A map from pragmas to a boolean indicating if the pragma was used or not. - // d. A map from diagnostic ID to list of SuppressMessageAttribute nodes for the ID. - // This map tracks supported diagnostic IDs for this tree's SuppressMessageAttribute nodes. - // e. A map from SuppressMessageAttribute nodes to a boolean indicating if the attribute was used or not. - // f. A set of supported compiler diagnostic IDs that are used in pragmas or SuppressMessageAttributes in this file. - // 3. Map the set of candidate diagnostic IDs to the analyzers that can report diagnostics with these IDs. - // 4. Execute these analyzers to compute the diagnostics reported by these analyzers in this file. - // 5. Iterate through the suppressed diagnostics from this list and do the following: - // a. If the diagnostic was suppressed with a pragma, mark the closest preceding disable pragma - // which suppresses this ID as used/necessary. Also mark the matching restore pragma as used. - // b. Otherwise, if the diagnostic was suppressed with SuppressMessageAttribute, mark the attribute as used. - // 6. Finally, report a diagnostic all the pragmas and SuppressMessageAttributes which have not been marked as used. - - using var _1 = PooledDictionary>.GetInstance(out var idToPragmasMap); - using var _2 = ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)>.GetInstance(out var sortedPragmasWithIds); - using var _3 = PooledDictionary.GetInstance(out var pragmasToIsUsedMap); - using var _4 = PooledHashSet.GetInstance(out var compilerDiagnosticIds); - var hasPragmaInAnalysisSpan = ProcessPragmaDirectives(root, span, idToPragmasMap, - pragmasToIsUsedMap, sortedPragmasWithIds, compilerDiagnosticIds, userIdExclusions); + // Compute all the reported compiler and analyzer diagnostics for diagnostic IDs corresponding to pragmas in the tree. + var (diagnostics, unhandledIds) = await GetReportedDiagnosticsForIdsAsync( + idsToAnalyze, root, semanticModel, compilationWithAnalyzers, + getSupportedDiagnostics, compilerDiagnosticIds, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - using var _5 = PooledDictionary>.GetInstance(out var idToSuppressMessageAttributesMap); - using var _6 = PooledDictionary.GetInstance(out var suppressMessageAttributesToIsUsedMap); - var hasAttributeInAnalysisSpan = await ProcessSuppressMessageAttributesAsync(root, semanticModel, span, - idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, userIdExclusions, userCategoryExclusions, cancellationToken).ConfigureAwait(false); + // Iterate through reported diagnostics which are suppressed in source through pragmas and mark the corresponding pragmas as used. + await ProcessReportedDiagnosticsAsync(diagnostics, tree, compilationWithAnalyzers, idToPragmasMap, + pragmasToIsUsedMap, idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - // Bail out if we have no pragma directives or SuppressMessageAttributes to analyze. - if (!hasPragmaInAnalysisSpan && !hasAttributeInAnalysisSpan) + // Remove entries for unhandled diagnostic ids. + foreach (var id in unhandledIds) + { + if (idToPragmasMap.TryGetValue(id, out var pragmas)) { - return; + foreach (var (pragma, _) in pragmas) + { + pragmasToIsUsedMap.Remove(pragma); + } } - using var _8 = PooledHashSet.GetInstance(out var idsToAnalyzeBuilder); - idsToAnalyzeBuilder.AddAll(idToPragmasMap.Keys); - idsToAnalyzeBuilder.AddAll(idToSuppressMessageAttributesMap.Keys); - var idsToAnalyze = idsToAnalyzeBuilder.ToImmutableHashSet(); - - // Compute all the reported compiler and analyzer diagnostics for diagnostic IDs corresponding to pragmas in the tree. - var (diagnostics, unhandledIds) = await GetReportedDiagnosticsForIdsAsync( - idsToAnalyze, root, semanticModel, compilationWithAnalyzers, - getSupportedDiagnostics, compilerDiagnosticIds, cancellationToken).ConfigureAwait(false); + if (idToSuppressMessageAttributesMap.TryGetValue(id, out var attributeNodes)) + { + foreach (var attributeNode in attributeNodes) + { + suppressMessageAttributesToIsUsedMap.Remove(attributeNode); + } - cancellationToken.ThrowIfCancellationRequested(); + idToSuppressMessageAttributesMap.Remove(id); + } + } - // Iterate through reported diagnostics which are suppressed in source through pragmas and mark the corresponding pragmas as used. - await ProcessReportedDiagnosticsAsync(diagnostics, tree, compilationWithAnalyzers, idToPragmasMap, - pragmasToIsUsedMap, idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, cancellationToken).ConfigureAwait(false); + // Finally, report the unnecessary suppressions. + var effectiveSeverity = severity.ToDiagnosticSeverity() ?? s_removeUnnecessarySuppressionDescriptor.DefaultSeverity; + ReportUnnecessarySuppressions(pragmasToIsUsedMap, sortedPragmasWithIds, + suppressMessageAttributesToIsUsedMap, reportDiagnostic, effectiveSeverity, compilationWithAnalyzers.Compilation); + } - cancellationToken.ThrowIfCancellationRequested(); + private bool ProcessPragmaDirectives( + SyntaxNode root, + TextSpan? span, + PooledDictionary> idToPragmasMap, + PooledDictionary pragmasToIsUsedMap, + ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)> sortedPragmasWithIds, + PooledHashSet compilerDiagnosticIds, + ImmutableArray userExclusions) + { + if (!ContainsPragmaDirective(root)) + { + return false; + } - // Remove entries for unhandled diagnostic ids. - foreach (var id in unhandledIds) + using var _ = ArrayBuilder.GetInstance(out var idsBuilder); + var hasPragmaInAnalysisSpan = false; + foreach (var trivia in root.DescendantTrivia(node => node.ContainsDirectives)) + { + // Check if this is an active pragma with at least one applicable diagnostic ID/error code. + // Note that a pragma can have multiple error codes, such as '#pragma warning disable ID0001, ID0002' + if (SyntaxFacts.IsPragmaDirective(trivia, out var isDisable, out var isActive, out var errorCodeNodes) && + isActive && + errorCodeNodes.Count > 0) { - if (idToPragmasMap.TryGetValue(id, out var pragmas)) + // Iterate through each ID for this pragma and build the supported IDs. + idsBuilder.Clear(); + foreach (var errorCodeNode in errorCodeNodes) { - foreach (var (pragma, _) in pragmas) + // Ignore unsupported IDs and those excluded through user option. + if (!IsSupportedId(errorCodeNode, out var id, out var isCompilerDiagnosticId) || + userExclusions.Contains(id, StringComparer.OrdinalIgnoreCase)) { - pragmasToIsUsedMap.Remove(pragma); + continue; } - } - if (idToSuppressMessageAttributesMap.TryGetValue(id, out var attributeNodes)) - { - foreach (var attributeNode in attributeNodes) + idsBuilder.Add(id); + if (isCompilerDiagnosticId) { - suppressMessageAttributesToIsUsedMap.Remove(attributeNode); + compilerDiagnosticIds.Add(id); } - idToSuppressMessageAttributesMap.Remove(id); + // Add entry to idToPragmasMap + // Insert the pragmas in reverse order for easier processing later. + if (!idToPragmasMap.TryGetValue(id, out var pragmasForIdInReverseOrder)) + { + pragmasForIdInReverseOrder = []; + idToPragmasMap.Add(id, pragmasForIdInReverseOrder); + } + + pragmasForIdInReverseOrder.Insert(0, (trivia, isDisable)); } - } - // Finally, report the unnecessary suppressions. - var effectiveSeverity = severity.ToDiagnosticSeverity() ?? s_removeUnnecessarySuppressionDescriptor.DefaultSeverity; - ReportUnnecessarySuppressions(pragmasToIsUsedMap, sortedPragmasWithIds, - suppressMessageAttributesToIsUsedMap, reportDiagnostic, effectiveSeverity, compilationWithAnalyzers.Compilation); - } + if (idsBuilder.Count == 0) + { + // No supported ID in this pragma. + continue; + } - private bool ProcessPragmaDirectives( - SyntaxNode root, - TextSpan? span, - PooledDictionary> idToPragmasMap, - PooledDictionary pragmasToIsUsedMap, - ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)> sortedPragmasWithIds, - PooledHashSet compilerDiagnosticIds, - ImmutableArray userExclusions) - { - if (!ContainsPragmaDirective(root)) - { - return false; + hasPragmaInAnalysisSpan = hasPragmaInAnalysisSpan || !span.HasValue || span.Value.OverlapsWith(trivia.Span); + + sortedPragmasWithIds.Add((trivia, idsBuilder.ToImmutable(), isDisable)); + + // Pragma directive is initialized as unnecessary at the start of the algorithm (value = false). + // We will subsequently find required/used pragmas and update the entries in this map (value = true). + pragmasToIsUsedMap.Add(trivia, false); } + } - using var _ = ArrayBuilder.GetInstance(out var idsBuilder); - var hasPragmaInAnalysisSpan = false; - foreach (var trivia in root.DescendantTrivia(node => node.ContainsDirectives)) - { - // Check if this is an active pragma with at least one applicable diagnostic ID/error code. - // Note that a pragma can have multiple error codes, such as '#pragma warning disable ID0001, ID0002' - if (SyntaxFacts.IsPragmaDirective(trivia, out var isDisable, out var isActive, out var errorCodeNodes) && - isActive && - errorCodeNodes.Count > 0) - { - // Iterate through each ID for this pragma and build the supported IDs. - idsBuilder.Clear(); - foreach (var errorCodeNode in errorCodeNodes) - { - // Ignore unsupported IDs and those excluded through user option. - if (!IsSupportedId(errorCodeNode, out var id, out var isCompilerDiagnosticId) || - userExclusions.Contains(id, StringComparer.OrdinalIgnoreCase)) - { - continue; - } + return hasPragmaInAnalysisSpan; + } - idsBuilder.Add(id); - if (isCompilerDiagnosticId) - { - compilerDiagnosticIds.Add(id); - } + private bool IsSupportedId( + SyntaxNode idNode, + [NotNullWhen(returnValue: true)] out string? id, + out bool isCompilerDiagnosticId) + { + id = idNode.ToString(); - // Add entry to idToPragmasMap - // Insert the pragmas in reverse order for easier processing later. - if (!idToPragmasMap.TryGetValue(id, out var pragmasForIdInReverseOrder)) - { - pragmasForIdInReverseOrder = []; - idToPragmasMap.Add(id, pragmasForIdInReverseOrder); - } + // Compiler diagnostic pragma suppressions allow specifying just the integral ID. + // For example: + // "#pragma warning disable 0168" OR "#pragma warning disable 168" + // is equivalent to + // "#pragma warning disable CS0168" + // We handle all the three supported formats for compiler diagnostic pragmas. - pragmasForIdInReverseOrder.Insert(0, (trivia, isDisable)); - } + var idWithoutPrefix = id.StartsWith(CompilerErrorCodePrefix) && id.Length == CompilerErrorCodePrefix.Length + CompilerErrorCodeDigitCount + ? id[CompilerErrorCodePrefix.Length..] + : id; - if (idsBuilder.Count == 0) - { - // No supported ID in this pragma. - continue; - } + // ID without prefix should parse as an integer for compiler diagnostics. + if (int.TryParse(idWithoutPrefix, out var errorCode)) + { + // Normalize the ID to always be in the format with prefix. + id = CompilerErrorCodePrefix + errorCode.ToString($"D{CompilerErrorCodeDigitCount}"); + isCompilerDiagnosticId = true; + return _lazySupportedCompilerErrorCodes.Value.Contains(errorCode); + } - hasPragmaInAnalysisSpan = hasPragmaInAnalysisSpan || !span.HasValue || span.Value.OverlapsWith(trivia.Span); + isCompilerDiagnosticId = false; + return IsSupportedAnalyzerDiagnosticId(id) && + idWithoutPrefix == id; + } - sortedPragmasWithIds.Add((trivia, idsBuilder.ToImmutable(), isDisable)); + private static bool IsSupportedAnalyzerDiagnosticId(string id) + { + switch (id) + { + case IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId: + // Not supported as this would lead to recursion in computation. + return false; - // Pragma directive is initialized as unnecessary at the start of the algorithm (value = false). - // We will subsequently find required/used pragmas and update the entries in this map (value = true). - pragmasToIsUsedMap.Add(trivia, false); - } - } + case "format": + case IDEDiagnosticIds.FormattingDiagnosticId: + // Formatting analyzer is not supported as the analyzer does not seem to return suppressed IDE0055 diagnostics. + return false; - return hasPragmaInAnalysisSpan; + default: + return true; } + } - private bool IsSupportedId( - SyntaxNode idNode, - [NotNullWhen(returnValue: true)] out string? id, - out bool isCompilerDiagnosticId) + private static (ImmutableArray userIdExclusions, ImmutableArray userCategoryExclusions, bool analyzerDisabled) ParseUserExclusions(string? userExclusions) + { + // Option value must be a comma separate list of diagnostic IDs or categories (with a "category:" prefix) to exclude from unnecessary pragma analysis. + // We also allow a special keyword "all" to disable the analyzer completely. + switch (userExclusions) { - id = idNode.ToString(); - - // Compiler diagnostic pragma suppressions allow specifying just the integral ID. - // For example: - // "#pragma warning disable 0168" OR "#pragma warning disable 168" - // is equivalent to - // "#pragma warning disable CS0168" - // We handle all the three supported formats for compiler diagnostic pragmas. + case "": + case null: + return (userIdExclusions: ImmutableArray.Empty, userCategoryExclusions: ImmutableArray.Empty, analyzerDisabled: false); - var idWithoutPrefix = id.StartsWith(CompilerErrorCodePrefix) && id.Length == CompilerErrorCodePrefix.Length + CompilerErrorCodeDigitCount - ? id[CompilerErrorCodePrefix.Length..] - : id; + case "all": + return (userIdExclusions: ImmutableArray.Empty, userCategoryExclusions: ImmutableArray.Empty, analyzerDisabled: true); - // ID without prefix should parse as an integer for compiler diagnostics. - if (int.TryParse(idWithoutPrefix, out var errorCode)) - { - // Normalize the ID to always be in the format with prefix. - id = CompilerErrorCodePrefix + errorCode.ToString($"D{CompilerErrorCodeDigitCount}"); - isCompilerDiagnosticId = true; - return _lazySupportedCompilerErrorCodes.Value.Contains(errorCode); - } + default: + // Default string representation for unconfigured option value should be treated as no exclusions. + if (userExclusions == CodeStyleOptions2.RemoveUnnecessarySuppressionExclusions.DefaultValue) + return (userIdExclusions: ImmutableArray.Empty, userCategoryExclusions: ImmutableArray.Empty, analyzerDisabled: false); - isCompilerDiagnosticId = false; - return IsSupportedAnalyzerDiagnosticId(id) && - idWithoutPrefix == id; + break; } - private static bool IsSupportedAnalyzerDiagnosticId(string id) + // We allow excluding category of diagnostics with a category prefix, for example "category: ExcludedCategory". + const string categoryPrefix = "category:"; + + using var _1 = ArrayBuilder.GetInstance(out var idBuilder); + using var _2 = ArrayBuilder.GetInstance(out var categoryBuilder); + foreach (var part in userExclusions.Split(',')) { - switch (id) + var trimmedPart = part.Trim(); + if (trimmedPart.StartsWith(categoryPrefix, StringComparison.OrdinalIgnoreCase)) { - case IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId: - // Not supported as this would lead to recursion in computation. - return false; - - case "format": - case IDEDiagnosticIds.FormattingDiagnosticId: - // Formatting analyzer is not supported as the analyzer does not seem to return suppressed IDE0055 diagnostics. - return false; - - default: - return true; + trimmedPart = trimmedPart[categoryPrefix.Length..].Trim(); + categoryBuilder.Add(trimmedPart); + } + else + { + idBuilder.Add(trimmedPart); } } - private static (ImmutableArray userIdExclusions, ImmutableArray userCategoryExclusions, bool analyzerDisabled) ParseUserExclusions(string? userExclusions) - { - // Option value must be a comma separate list of diagnostic IDs or categories (with a "category:" prefix) to exclude from unnecessary pragma analysis. - // We also allow a special keyword "all" to disable the analyzer completely. - switch (userExclusions) - { - case "": - case null: - return (userIdExclusions: ImmutableArray.Empty, userCategoryExclusions: ImmutableArray.Empty, analyzerDisabled: false); + return (userIdExclusions: idBuilder.ToImmutable(), userCategoryExclusions: categoryBuilder.ToImmutable(), analyzerDisabled: false); + } - case "all": - return (userIdExclusions: ImmutableArray.Empty, userCategoryExclusions: ImmutableArray.Empty, analyzerDisabled: true); + private static async Task<(ImmutableArray reportedDiagnostics, ImmutableArray unhandledIds)> GetReportedDiagnosticsForIdsAsync( + ImmutableHashSet idsToAnalyze, + SyntaxNode root, + SemanticModel semanticModel, + CompilationWithAnalyzers compilationWithAnalyzers, + Func> getSupportedDiagnostics, + PooledHashSet compilerDiagnosticIds, + CancellationToken cancellationToken) + { + using var _1 = ArrayBuilder.GetInstance(out var analyzersBuilder); + using var _2 = ArrayBuilder.GetInstance(out var unhandledIds); - default: - // Default string representation for unconfigured option value should be treated as no exclusions. - if (userExclusions == CodeStyleOptions2.RemoveUnnecessarySuppressionExclusions.DefaultValue) - return (userIdExclusions: ImmutableArray.Empty, userCategoryExclusions: ImmutableArray.Empty, analyzerDisabled: false); + // First, we compute the relevant analyzers whose reported diagnostics need to be computed. + var addedCompilerAnalyzer = false; + var hasNonCompilerAnalyzers = idsToAnalyze.Count > compilerDiagnosticIds.Count; + foreach (var analyzer in compilationWithAnalyzers.Analyzers) + { + if (!addedCompilerAnalyzer && + analyzer.IsCompilerAnalyzer()) + { + addedCompilerAnalyzer = true; + analyzersBuilder.Add(analyzer); + if (!hasNonCompilerAnalyzers) + { break; - } + } - // We allow excluding category of diagnostics with a category prefix, for example "category: ExcludedCategory". - const string categoryPrefix = "category:"; + continue; + } - using var _1 = ArrayBuilder.GetInstance(out var idBuilder); - using var _2 = ArrayBuilder.GetInstance(out var categoryBuilder); - foreach (var part in userExclusions.Split(',')) + if (hasNonCompilerAnalyzers) { - var trimmedPart = part.Trim(); - if (trimmedPart.StartsWith(categoryPrefix, StringComparison.OrdinalIgnoreCase)) + Debug.Assert(!analyzer.IsCompilerAnalyzer()); + + bool? lazyIsUnhandledAnalyzer = null; + foreach (var descriptor in getSupportedDiagnostics(analyzer)) { - trimmedPart = trimmedPart[categoryPrefix.Length..].Trim(); - categoryBuilder.Add(trimmedPart); + if (!idsToAnalyze.Contains(descriptor.Id)) + { + continue; + } + + lazyIsUnhandledAnalyzer ??= descriptor.IsCompilationEnd() || analyzer is IPragmaSuppressionsAnalyzer; + if (lazyIsUnhandledAnalyzer.Value) + { + unhandledIds.Add(descriptor.Id); + } } - else + + if (lazyIsUnhandledAnalyzer.HasValue && !lazyIsUnhandledAnalyzer.Value) { - idBuilder.Add(trimmedPart); + analyzersBuilder.Add(analyzer); } } + } + + // Then, we execute these analyzers on the current file to fetch these diagnostics. + // Note that if an analyzer has already executed, then this will be just a cache access + // as computed analyzer diagnostics are cached on CompilationWithAnalyzers instance. - return (userIdExclusions: idBuilder.ToImmutable(), userCategoryExclusions: categoryBuilder.ToImmutable(), analyzerDisabled: false); + using var _3 = ArrayBuilder.GetInstance(out var reportedDiagnostics); + if (!addedCompilerAnalyzer && compilerDiagnosticIds.Count > 0) + { + // Special case when compiler analyzer could not be found. + Debug.Assert(semanticModel.Compilation.Options.ReportSuppressedDiagnostics); + reportedDiagnostics.AddRange(root.GetDiagnostics()); + reportedDiagnostics.AddRange(semanticModel.GetDiagnostics(cancellationToken: cancellationToken)); + cancellationToken.ThrowIfCancellationRequested(); } - private static async Task<(ImmutableArray reportedDiagnostics, ImmutableArray unhandledIds)> GetReportedDiagnosticsForIdsAsync( - ImmutableHashSet idsToAnalyze, - SyntaxNode root, - SemanticModel semanticModel, - CompilationWithAnalyzers compilationWithAnalyzers, - Func> getSupportedDiagnostics, - PooledHashSet compilerDiagnosticIds, - CancellationToken cancellationToken) + if (analyzersBuilder.Count > 0) { - using var _1 = ArrayBuilder.GetInstance(out var analyzersBuilder); - using var _2 = ArrayBuilder.GetInstance(out var unhandledIds); + var analyzers = analyzersBuilder.ToImmutable(); - // First, we compute the relevant analyzers whose reported diagnostics need to be computed. - var addedCompilerAnalyzer = false; - var hasNonCompilerAnalyzers = idsToAnalyze.Count > compilerDiagnosticIds.Count; - foreach (var analyzer in compilationWithAnalyzers.Analyzers) + var analysisResult = await compilationWithAnalyzers.GetAnalysisResultAsync(semanticModel.SyntaxTree, analyzers, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + if (analysisResult.SyntaxDiagnostics.TryGetValue(semanticModel.SyntaxTree, out var diagnostics)) { - if (!addedCompilerAnalyzer && - analyzer.IsCompilerAnalyzer()) - { - addedCompilerAnalyzer = true; - analyzersBuilder.Add(analyzer); - - if (!hasNonCompilerAnalyzers) - { - break; - } + AddAllDiagnostics(diagnostics, reportedDiagnostics); + } - continue; - } + analysisResult = await compilationWithAnalyzers.GetAnalysisResultAsync(semanticModel, filterSpan: null, analyzers, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + if (analysisResult.SemanticDiagnostics.TryGetValue(semanticModel.SyntaxTree, out diagnostics)) + { + AddAllDiagnostics(diagnostics, reportedDiagnostics); + } - if (hasNonCompilerAnalyzers) - { - Debug.Assert(!analyzer.IsCompilerAnalyzer()); + AddAllCompilationDiagnosticsForTree(analysisResult, semanticModel.SyntaxTree, reportedDiagnostics); + } - bool? lazyIsUnhandledAnalyzer = null; - foreach (var descriptor in getSupportedDiagnostics(analyzer)) - { - if (!idsToAnalyze.Contains(descriptor.Id)) - { - continue; - } + return (reportedDiagnostics.ToImmutable(), unhandledIds.ToImmutable()); - lazyIsUnhandledAnalyzer ??= descriptor.IsCompilationEnd() || analyzer is IPragmaSuppressionsAnalyzer; - if (lazyIsUnhandledAnalyzer.Value) - { - unhandledIds.Add(descriptor.Id); - } - } + static void AddAllDiagnostics(ImmutableDictionary> diagnostics, ArrayBuilder reportedDiagnostics) + { + foreach (var perAnalyzerDiagnostics in diagnostics.Values) + { + reportedDiagnostics.AddRange(perAnalyzerDiagnostics); + } + } - if (lazyIsUnhandledAnalyzer.HasValue && !lazyIsUnhandledAnalyzer.Value) + static void AddAllCompilationDiagnosticsForTree(AnalysisResult analysisResult, SyntaxTree tree, ArrayBuilder reportedDiagnostics) + { + foreach (var perAnalyzerDiagnostics in analysisResult.CompilationDiagnostics.Values) + { + foreach (var diagnostic in perAnalyzerDiagnostics) + { + if (diagnostic.Location.SourceTree == tree) { - analyzersBuilder.Add(analyzer); + reportedDiagnostics.Add(diagnostic); } } } + } + } - // Then, we execute these analyzers on the current file to fetch these diagnostics. - // Note that if an analyzer has already executed, then this will be just a cache access - // as computed analyzer diagnostics are cached on CompilationWithAnalyzers instance. - - using var _3 = ArrayBuilder.GetInstance(out var reportedDiagnostics); - if (!addedCompilerAnalyzer && compilerDiagnosticIds.Count > 0) + private static async Task ProcessReportedDiagnosticsAsync( + ImmutableArray diagnostics, + SyntaxTree tree, + CompilationWithAnalyzers compilationWithAnalyzers, + PooledDictionary> idToPragmasMap, + PooledDictionary pragmasToIsUsedMap, + PooledDictionary> idToSuppressMessageAttributesMap, + PooledDictionary suppressMessageAttributesToIsUsedMap, + CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) + { + if (!diagnostic.IsSuppressed) { - // Special case when compiler analyzer could not be found. - Debug.Assert(semanticModel.Compilation.Options.ReportSuppressedDiagnostics); - reportedDiagnostics.AddRange(root.GetDiagnostics()); - reportedDiagnostics.AddRange(semanticModel.GetDiagnostics(cancellationToken: cancellationToken)); - cancellationToken.ThrowIfCancellationRequested(); + continue; } - if (analyzersBuilder.Count > 0) + var suppressionInfo = diagnostic.GetSuppressionInfo(compilationWithAnalyzers.Compilation); + if (suppressionInfo == null || !suppressionInfo.ProgrammaticSuppressions.IsEmpty) { - var analyzers = analyzersBuilder.ToImmutable(); - - var analysisResult = await compilationWithAnalyzers.GetAnalysisResultAsync(semanticModel.SyntaxTree, analyzers, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - if (analysisResult.SyntaxDiagnostics.TryGetValue(semanticModel.SyntaxTree, out var diagnostics)) - { - AddAllDiagnostics(diagnostics, reportedDiagnostics); - } - - analysisResult = await compilationWithAnalyzers.GetAnalysisResultAsync(semanticModel, filterSpan: null, analyzers, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - if (analysisResult.SemanticDiagnostics.TryGetValue(semanticModel.SyntaxTree, out diagnostics)) - { - AddAllDiagnostics(diagnostics, reportedDiagnostics); - } + // Skip diagnostics that are not suppressed in source or suppressed via programmatic suppressions from suppressors. + continue; + } - AddAllCompilationDiagnosticsForTree(analysisResult, semanticModel.SyntaxTree, reportedDiagnostics); + if (suppressionInfo.Attribute is { } attribute) + { + await ProcessAttributeSuppressionsAsync(diagnostic, attribute, + idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, cancellationToken).ConfigureAwait(false); + } + else + { + ProcessPragmaSuppressions(diagnostic, tree, idToPragmasMap, pragmasToIsUsedMap); } + } - return (reportedDiagnostics.ToImmutable(), unhandledIds.ToImmutable()); + return; - static void AddAllDiagnostics(ImmutableDictionary> diagnostics, ArrayBuilder reportedDiagnostics) + static void ProcessPragmaSuppressions( + Diagnostic diagnostic, + SyntaxTree tree, + PooledDictionary> idToPragmasMap, + PooledDictionary pragmasToIsUsedMap) + { + if (!idToPragmasMap.TryGetValue(diagnostic.Id, out var pragmasForIdInReverseOrder)) { - foreach (var perAnalyzerDiagnostics in diagnostics.Values) - { - reportedDiagnostics.AddRange(perAnalyzerDiagnostics); - } + return; } - static void AddAllCompilationDiagnosticsForTree(AnalysisResult analysisResult, SyntaxTree tree, ArrayBuilder reportedDiagnostics) + Debug.Assert(diagnostic.Location.IsInSource); + Debug.Assert(diagnostic.Location.SourceTree == tree); + + // Process the pragmas for the document bottom-up, + // finding the first disable pragma directive before the diagnostic span. + // Mark this pragma and the corresponding enable pragma directive as used. + SyntaxTrivia? lastEnablePragma = null; + foreach (var (pragma, isDisable) in pragmasForIdInReverseOrder) { - foreach (var perAnalyzerDiagnostics in analysisResult.CompilationDiagnostics.Values) + if (isDisable) { - foreach (var diagnostic in perAnalyzerDiagnostics) + if (pragma.Span.End <= diagnostic.Location.SourceSpan.Start) { - if (diagnostic.Location.SourceTree == tree) + pragmasToIsUsedMap[pragma] = true; + if (lastEnablePragma.HasValue) { - reportedDiagnostics.Add(diagnostic); + pragmasToIsUsedMap[lastEnablePragma.Value] = true; } + + break; } } + else + { + lastEnablePragma = pragma; + } } } - private static async Task ProcessReportedDiagnosticsAsync( - ImmutableArray diagnostics, - SyntaxTree tree, - CompilationWithAnalyzers compilationWithAnalyzers, - PooledDictionary> idToPragmasMap, - PooledDictionary pragmasToIsUsedMap, + static async Task ProcessAttributeSuppressionsAsync( + Diagnostic diagnostic, + AttributeData attribute, PooledDictionary> idToSuppressMessageAttributesMap, PooledDictionary suppressMessageAttributesToIsUsedMap, CancellationToken cancellationToken) { - foreach (var diagnostic in diagnostics) + if (attribute.ApplicationSyntaxReference == null || + !idToSuppressMessageAttributesMap.TryGetValue(diagnostic.Id, out var suppressMessageAttributesForId)) { - if (!diagnostic.IsSuppressed) - { - continue; - } - - var suppressionInfo = diagnostic.GetSuppressionInfo(compilationWithAnalyzers.Compilation); - if (suppressionInfo == null || !suppressionInfo.ProgrammaticSuppressions.IsEmpty) - { - // Skip diagnostics that are not suppressed in source or suppressed via programmatic suppressions from suppressors. - continue; - } - - if (suppressionInfo.Attribute is { } attribute) - { - await ProcessAttributeSuppressionsAsync(diagnostic, attribute, - idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, cancellationToken).ConfigureAwait(false); - } - else - { - ProcessPragmaSuppressions(diagnostic, tree, idToPragmasMap, pragmasToIsUsedMap); - } + return; } - return; - - static void ProcessPragmaSuppressions( - Diagnostic diagnostic, - SyntaxTree tree, - PooledDictionary> idToPragmasMap, - PooledDictionary pragmasToIsUsedMap) + var attributeNode = await attribute.ApplicationSyntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + foreach (var node in suppressMessageAttributesForId) { - if (!idToPragmasMap.TryGetValue(diagnostic.Id, out var pragmasForIdInReverseOrder)) + if (attributeNode == node) { + suppressMessageAttributesToIsUsedMap[attributeNode] = true; return; } + } + } + } + + private static void ReportUnnecessarySuppressions( + PooledDictionary pragmasToIsUsedMap, + ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)> sortedPragmasWithIds, + PooledDictionary suppressMessageAttributesToIsUsedMap, + Action reportDiagnostic, + DiagnosticSeverity severity, + Compilation compilation) + { + using var _ = ArrayBuilder.GetInstance(out var diagnosticsBuilder); + AddUnnecessaryPragmaDiagnostics(diagnosticsBuilder, pragmasToIsUsedMap, sortedPragmasWithIds, severity); + AddUnnecessarySuppressMessageAttributeDiagnostics(diagnosticsBuilder, suppressMessageAttributesToIsUsedMap, severity); + + // Apply the diagnostic filtering + var effectiveDiagnostics = CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnosticsBuilder, compilation); + foreach (var diagnostic in effectiveDiagnostics) + { + reportDiagnostic(diagnostic); + } - Debug.Assert(diagnostic.Location.IsInSource); - Debug.Assert(diagnostic.Location.SourceTree == tree); + return; - // Process the pragmas for the document bottom-up, - // finding the first disable pragma directive before the diagnostic span. - // Mark this pragma and the corresponding enable pragma directive as used. - SyntaxTrivia? lastEnablePragma = null; - foreach (var (pragma, isDisable) in pragmasForIdInReverseOrder) + static void AddUnnecessaryPragmaDiagnostics( + ArrayBuilder diagnosticsBuilder, + PooledDictionary pragmasToIsUsedMap, + ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)> sortedPragmasWithIds, + DiagnosticSeverity severity) + { + foreach (var (pragma, isUsed) in pragmasToIsUsedMap) + { + if (!isUsed) { - if (isDisable) + // We found an unnecessary pragma directive. + // Try to find a matching disable/restore counterpart that toggles the pragma state. + // This enables the code fix to simultaneously remove both the disable and restore directives. + // If we don't find a matching pragma, report just the current pragma. + ImmutableArray additionalLocations; + if (TryGetTogglingPragmaDirective(pragma, sortedPragmasWithIds, out var togglePragma) && + pragmasToIsUsedMap.TryGetValue(togglePragma, out var isToggleUsed) && + !isToggleUsed) { - if (pragma.Span.End <= diagnostic.Location.SourceSpan.Start) - { - pragmasToIsUsedMap[pragma] = true; - if (lastEnablePragma.HasValue) - { - pragmasToIsUsedMap[lastEnablePragma.Value] = true; - } - - break; - } + additionalLocations = [togglePragma.GetLocation()]; } else { - lastEnablePragma = pragma; + additionalLocations = []; } + + var diagnostic = Diagnostic.Create(s_removeUnnecessarySuppressionDescriptor, pragma.GetLocation(), severity, additionalLocations, properties: null); + diagnosticsBuilder.Add(diagnostic); } } + } - static async Task ProcessAttributeSuppressionsAsync( - Diagnostic diagnostic, - AttributeData attribute, - PooledDictionary> idToSuppressMessageAttributesMap, - PooledDictionary suppressMessageAttributesToIsUsedMap, - CancellationToken cancellationToken) + static void AddUnnecessarySuppressMessageAttributeDiagnostics( + ArrayBuilder diagnosticsBuilder, + PooledDictionary suppressMessageAttributesToIsUsedMap, + DiagnosticSeverity severity) + { + foreach (var (attribute, isUsed) in suppressMessageAttributesToIsUsedMap) { - if (attribute.ApplicationSyntaxReference == null || - !idToSuppressMessageAttributesMap.TryGetValue(diagnostic.Id, out var suppressMessageAttributesForId)) + if (!isUsed) { - return; - } - - var attributeNode = await attribute.ApplicationSyntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); - foreach (var node in suppressMessageAttributesForId) - { - if (attributeNode == node) - { - suppressMessageAttributesToIsUsedMap[attributeNode] = true; - return; - } + var diagnostic = Diagnostic.Create(s_removeUnnecessarySuppressionDescriptor, attribute.GetLocation(), severity, additionalLocations: null, properties: null); + diagnosticsBuilder.Add(diagnostic); } } } + } - private static void ReportUnnecessarySuppressions( - PooledDictionary pragmasToIsUsedMap, - ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)> sortedPragmasWithIds, - PooledDictionary suppressMessageAttributesToIsUsedMap, - Action reportDiagnostic, - DiagnosticSeverity severity, - Compilation compilation) + private static bool TryGetTogglingPragmaDirective( + SyntaxTrivia pragma, + ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)> sortedPragmasWithIds, + out SyntaxTrivia togglePragma) + { + var indexOfPragma = sortedPragmasWithIds.FindIndex(p => p.pragma == pragma); + var idsForPragma = sortedPragmasWithIds[indexOfPragma].ids; + var isDisable = sortedPragmasWithIds[indexOfPragma].isDisable; + var incrementOrDecrement = isDisable ? 1 : -1; + var matchingPragmaStackCount = 0; + for (var i = indexOfPragma + incrementOrDecrement; i >= 0 && i < sortedPragmasWithIds.Count; i += incrementOrDecrement) { - using var _ = ArrayBuilder.GetInstance(out var diagnosticsBuilder); - AddUnnecessaryPragmaDiagnostics(diagnosticsBuilder, pragmasToIsUsedMap, sortedPragmasWithIds, severity); - AddUnnecessarySuppressMessageAttributeDiagnostics(diagnosticsBuilder, suppressMessageAttributesToIsUsedMap, severity); - - // Apply the diagnostic filtering - var effectiveDiagnostics = CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnosticsBuilder, compilation); - foreach (var diagnostic in effectiveDiagnostics) + var (nextPragma, nextPragmaIds, nextPragmaIsDisable) = sortedPragmasWithIds[i]; + var intersect = nextPragmaIds.Intersect(idsForPragma).ToImmutableArray(); + if (intersect.IsEmpty) { - reportDiagnostic(diagnostic); + // Unrelated pragma + continue; } - return; - - static void AddUnnecessaryPragmaDiagnostics( - ArrayBuilder diagnosticsBuilder, - PooledDictionary pragmasToIsUsedMap, - ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)> sortedPragmasWithIds, - DiagnosticSeverity severity) + if (intersect.Length != idsForPragma.Length) { - foreach (var (pragma, isUsed) in pragmasToIsUsedMap) - { - if (!isUsed) - { - // We found an unnecessary pragma directive. - // Try to find a matching disable/restore counterpart that toggles the pragma state. - // This enables the code fix to simultaneously remove both the disable and restore directives. - // If we don't find a matching pragma, report just the current pragma. - ImmutableArray additionalLocations; - if (TryGetTogglingPragmaDirective(pragma, sortedPragmasWithIds, out var togglePragma) && - pragmasToIsUsedMap.TryGetValue(togglePragma, out var isToggleUsed) && - !isToggleUsed) - { - additionalLocations = [togglePragma.GetLocation()]; - } - else - { - additionalLocations = []; - } - - var diagnostic = Diagnostic.Create(s_removeUnnecessarySuppressionDescriptor, pragma.GetLocation(), severity, additionalLocations, properties: null); - diagnosticsBuilder.Add(diagnostic); - } - } + // Partial intersection of IDs - bail out. + togglePragma = default; + return false; } - static void AddUnnecessarySuppressMessageAttributeDiagnostics( - ArrayBuilder diagnosticsBuilder, - PooledDictionary suppressMessageAttributesToIsUsedMap, - DiagnosticSeverity severity) + // Found a pragma with same IDs. + // Check if this is a pragma of same kind (disable/restore) or not. + if (isDisable == nextPragmaIsDisable) { - foreach (var (attribute, isUsed) in suppressMessageAttributesToIsUsedMap) - { - if (!isUsed) - { - var diagnostic = Diagnostic.Create(s_removeUnnecessarySuppressionDescriptor, attribute.GetLocation(), severity, additionalLocations: null, properties: null); - diagnosticsBuilder.Add(diagnostic); - } - } + // Same pragma kind, increment the stack count + matchingPragmaStackCount++; } - } - - private static bool TryGetTogglingPragmaDirective( - SyntaxTrivia pragma, - ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)> sortedPragmasWithIds, - out SyntaxTrivia togglePragma) - { - var indexOfPragma = sortedPragmasWithIds.FindIndex(p => p.pragma == pragma); - var idsForPragma = sortedPragmasWithIds[indexOfPragma].ids; - var isDisable = sortedPragmasWithIds[indexOfPragma].isDisable; - var incrementOrDecrement = isDisable ? 1 : -1; - var matchingPragmaStackCount = 0; - for (var i = indexOfPragma + incrementOrDecrement; i >= 0 && i < sortedPragmasWithIds.Count; i += incrementOrDecrement) + else { - var (nextPragma, nextPragmaIds, nextPragmaIsDisable) = sortedPragmasWithIds[i]; - var intersect = nextPragmaIds.Intersect(idsForPragma).ToImmutableArray(); - if (intersect.IsEmpty) + // Found a pragma of opposite kind. + if (matchingPragmaStackCount > 0) { - // Unrelated pragma - continue; - } - - if (intersect.Length != idsForPragma.Length) - { - // Partial intersection of IDs - bail out. - togglePragma = default; - return false; - } - - // Found a pragma with same IDs. - // Check if this is a pragma of same kind (disable/restore) or not. - if (isDisable == nextPragmaIsDisable) - { - // Same pragma kind, increment the stack count - matchingPragmaStackCount++; + // Not matching one for the input pragma, decrement stack count + matchingPragmaStackCount--; } else { - // Found a pragma of opposite kind. - if (matchingPragmaStackCount > 0) - { - // Not matching one for the input pragma, decrement stack count - matchingPragmaStackCount--; - } - else - { - // Found the match. - togglePragma = nextPragma; - return true; - } + // Found the match. + togglePragma = nextPragma; + return true; } } + } + + togglePragma = default; + return false; + } - togglePragma = default; + private async Task ProcessSuppressMessageAttributesAsync( + SyntaxNode root, + SemanticModel semanticModel, + TextSpan? span, + PooledDictionary> idToSuppressMessageAttributesMap, + PooledDictionary suppressMessageAttributesToIsUsedMap, + ImmutableArray userIdExclusions, + ImmutableArray userCategoryExclusions, + CancellationToken cancellationToken) + { + var suppressMessageAttributeType = semanticModel.Compilation.SuppressMessageAttributeType(); + if (suppressMessageAttributeType == null) + { return false; } - private async Task ProcessSuppressMessageAttributesAsync( - SyntaxNode root, - SemanticModel semanticModel, - TextSpan? span, - PooledDictionary> idToSuppressMessageAttributesMap, - PooledDictionary suppressMessageAttributesToIsUsedMap, - ImmutableArray userIdExclusions, - ImmutableArray userCategoryExclusions, - CancellationToken cancellationToken) + var declarationNodes = SyntaxFacts.GetTopLevelAndMethodLevelMembers(root); + using var _ = PooledHashSet.GetInstance(out var processedPartialSymbols); + if (declarationNodes.Count > 0) { - var suppressMessageAttributeType = semanticModel.Compilation.SuppressMessageAttributeType(); - if (suppressMessageAttributeType == null) + foreach (var node in declarationNodes) { - return false; - } + if (span.HasValue && !node.FullSpan.Contains(span.Value)) + { + continue; + } - var declarationNodes = SyntaxFacts.GetTopLevelAndMethodLevelMembers(root); - using var _ = PooledHashSet.GetInstance(out var processedPartialSymbols); - if (declarationNodes.Count > 0) - { - foreach (var node in declarationNodes) + var symbols = SemanticFacts.GetDeclaredSymbols(semanticModel, node, cancellationToken); + foreach (var symbol in symbols) { - if (span.HasValue && !node.FullSpan.Contains(span.Value)) + switch (symbol?.Kind) + { + // Local SuppressMessageAttributes are only applicable for types and members. + case SymbolKind.NamedType: + case SymbolKind.Method: + case SymbolKind.Field: + case SymbolKind.Property: + case SymbolKind.Event: + break; + + default: + continue; + } + + // Skip already processed symbols from partial declarations + var isPartial = symbol.Locations.Length > 1; + if (isPartial && !processedPartialSymbols.Add(symbol)) { continue; } - var symbols = SemanticFacts.GetDeclaredSymbols(semanticModel, node, cancellationToken); - foreach (var symbol in symbols) + foreach (var attribute in symbol.GetAttributes()) { - switch (symbol?.Kind) + if (attribute.ApplicationSyntaxReference != null && + TryGetSuppressedDiagnosticId(attribute, suppressMessageAttributeType, out var id, out var category)) { - // Local SuppressMessageAttributes are only applicable for types and members. - case SymbolKind.NamedType: - case SymbolKind.Method: - case SymbolKind.Field: - case SymbolKind.Property: - case SymbolKind.Event: - break; - - default: + // Ignore unsupported IDs and those excluded through user option. + if (!IsSupportedAnalyzerDiagnosticId(id) || + userIdExclusions.Contains(id, StringComparer.OrdinalIgnoreCase) || + category?.Length > 0 && userCategoryExclusions.Contains(category, StringComparer.OrdinalIgnoreCase)) + { continue; - } - - // Skip already processed symbols from partial declarations - var isPartial = symbol.Locations.Length > 1; - if (isPartial && !processedPartialSymbols.Add(symbol)) - { - continue; - } + } - foreach (var attribute in symbol.GetAttributes()) - { - if (attribute.ApplicationSyntaxReference != null && - TryGetSuppressedDiagnosticId(attribute, suppressMessageAttributeType, out var id, out var category)) + if (!idToSuppressMessageAttributesMap.TryGetValue(id, out var nodesForId)) { - // Ignore unsupported IDs and those excluded through user option. - if (!IsSupportedAnalyzerDiagnosticId(id) || - userIdExclusions.Contains(id, StringComparer.OrdinalIgnoreCase) || - category?.Length > 0 && userCategoryExclusions.Contains(category, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - if (!idToSuppressMessageAttributesMap.TryGetValue(id, out var nodesForId)) - { - nodesForId = []; - idToSuppressMessageAttributesMap.Add(id, nodesForId); - } - - var attributeNode = await attribute.ApplicationSyntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); - nodesForId.Add(attributeNode); - - // Initialize the attribute node as unnecessary at the start of the algorithm. - // Later processing will identify attributes which are indeed responsible for suppressing diagnostics - // and mark them as used. - // NOTE: For attributes on partial symbols with multiple declarations, we conservatively - // consider them as used and avoid unnecessary attribute analysis because that would potentially - // require analysis across multiple files, which can be expensive from a performance standpoint. - suppressMessageAttributesToIsUsedMap.Add(attributeNode, isPartial); + nodesForId = []; + idToSuppressMessageAttributesMap.Add(id, nodesForId); } + + var attributeNode = await attribute.ApplicationSyntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + nodesForId.Add(attributeNode); + + // Initialize the attribute node as unnecessary at the start of the algorithm. + // Later processing will identify attributes which are indeed responsible for suppressing diagnostics + // and mark them as used. + // NOTE: For attributes on partial symbols with multiple declarations, we conservatively + // consider them as used and avoid unnecessary attribute analysis because that would potentially + // require analysis across multiple files, which can be expensive from a performance standpoint. + suppressMessageAttributesToIsUsedMap.Add(attributeNode, isPartial); } } } } - - return idToSuppressMessageAttributesMap.Count > 0; } - private static bool TryGetSuppressedDiagnosticId( - AttributeData attribute, - INamedTypeSymbol suppressMessageAttributeType, - [NotNullWhen(returnValue: true)] out string? id, - out string? category) - { - category = null; + return idToSuppressMessageAttributesMap.Count > 0; + } - if (suppressMessageAttributeType.Equals(attribute.AttributeClass) && - attribute.AttributeConstructor?.Parameters is [_, { Name: "checkId", Type.SpecialType: SpecialType.System_String }, ..] && - attribute.ConstructorArguments is [_, { Kind: TypedConstantKind.Primitive, Value: string checkId }, ..]) - { - // CheckId represents diagnostic ID, followed by an option ':' and name. - // For example, "CA1801:ReviewUnusedParameters" - var index = checkId.IndexOf(':'); - id = index > 0 ? checkId[..index] : checkId; - - if (attribute.AttributeConstructor.Parameters[0].Name == "category" && - attribute.AttributeConstructor.Parameters[0].Type.SpecialType == SpecialType.System_String && - attribute.ConstructorArguments[0] is - { - Kind: TypedConstantKind.Primitive, - Value: string categoryArg - }) - { - category = categoryArg; - } + private static bool TryGetSuppressedDiagnosticId( + AttributeData attribute, + INamedTypeSymbol suppressMessageAttributeType, + [NotNullWhen(returnValue: true)] out string? id, + out string? category) + { + category = null; - return id.Length > 0; + if (suppressMessageAttributeType.Equals(attribute.AttributeClass) && + attribute.AttributeConstructor?.Parameters is [_, { Name: "checkId", Type.SpecialType: SpecialType.System_String }, ..] && + attribute.ConstructorArguments is [_, { Kind: TypedConstantKind.Primitive, Value: string checkId }, ..]) + { + // CheckId represents diagnostic ID, followed by an option ':' and name. + // For example, "CA1801:ReviewUnusedParameters" + var index = checkId.IndexOf(':'); + id = index > 0 ? checkId[..index] : checkId; + + if (attribute.AttributeConstructor.Parameters[0].Name == "category" && + attribute.AttributeConstructor.Parameters[0].Type.SpecialType == SpecialType.System_String && + attribute.ConstructorArguments[0] is + { + Kind: TypedConstantKind.Primitive, + Value: string categoryArg + }) + { + category = categoryArg; } - id = null; - return false; + return id.Length > 0; } + + id = null; + return false; } } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/SuppressMessageAttributeState.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/SuppressMessageAttributeState.cs index ab865315ce350..ea9ce47b6e8fc 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/SuppressMessageAttributeState.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/SuppressMessageAttributeState.cs @@ -11,143 +11,142 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal partial class SuppressMessageAttributeState(Compilation compilation, INamedTypeSymbol suppressMessageAttributeType) { - internal partial class SuppressMessageAttributeState(Compilation compilation, INamedTypeSymbol suppressMessageAttributeType) - { - internal const string SuppressMessageScope = "Scope"; - internal const string SuppressMessageTarget = "Target"; + internal const string SuppressMessageScope = "Scope"; + internal const string SuppressMessageTarget = "Target"; - private static readonly ImmutableDictionary s_targetScopesMap = CreateTargetScopesMap(); + private static readonly ImmutableDictionary s_targetScopesMap = CreateTargetScopesMap(); - private readonly Compilation _compilation = compilation; - private readonly INamedTypeSymbol _suppressMessageAttributeType = suppressMessageAttributeType; + private readonly Compilation _compilation = compilation; + private readonly INamedTypeSymbol _suppressMessageAttributeType = suppressMessageAttributeType; - private static ImmutableDictionary CreateTargetScopesMap() - { - var builder = ImmutableDictionary.CreateBuilder(StringComparer.OrdinalIgnoreCase); + private static ImmutableDictionary CreateTargetScopesMap() + { + var builder = ImmutableDictionary.CreateBuilder(StringComparer.OrdinalIgnoreCase); #pragma warning disable CS8605 // Unboxing a possibly null value. - foreach (TargetScope targetScope in Enum.GetValues(typeof(TargetScope))) + foreach (TargetScope targetScope in Enum.GetValues(typeof(TargetScope))) #pragma warning restore CS8605 // Unboxing a possibly null value. + { + if (targetScope == TargetScope.None) { - if (targetScope == TargetScope.None) - { - continue; - } - - builder.Add(targetScope.ToString(), targetScope); + continue; } - return builder.ToImmutable(); + builder.Add(targetScope.ToString(), targetScope); } - public bool IsSuppressMessageAttributeWithNamedArguments( - SyntaxNode attributeSyntax, - SemanticModel model, - CancellationToken cancellationToken, - out ImmutableArray<(string name, IOperation value)> namedAttributeArguments) + return builder.ToImmutable(); + } + + public bool IsSuppressMessageAttributeWithNamedArguments( + SyntaxNode attributeSyntax, + SemanticModel model, + CancellationToken cancellationToken, + out ImmutableArray<(string name, IOperation value)> namedAttributeArguments) + { + var operation = (model.GetOperation(attributeSyntax, cancellationToken) as IAttributeOperation)?.Operation; + if (operation is not IObjectCreationOperation { Initializer: { } initializerOperation }) { - var operation = (model.GetOperation(attributeSyntax, cancellationToken) as IAttributeOperation)?.Operation; - if (operation is not IObjectCreationOperation { Initializer: { } initializerOperation }) - { - namedAttributeArguments = []; - return false; - } + namedAttributeArguments = []; + return false; + } - using var _ = ArrayBuilder<(string name, IOperation value)>.GetInstance(out var builder); - foreach (var initializer in initializerOperation.Initializers) + using var _ = ArrayBuilder<(string name, IOperation value)>.GetInstance(out var builder); + foreach (var initializer in initializerOperation.Initializers) + { + var simpleAssignment = (ISimpleAssignmentOperation)initializer; + if (simpleAssignment.Target is IPropertyReferenceOperation propertyReference && + _suppressMessageAttributeType.Equals(propertyReference.Property.ContainingType)) { - var simpleAssignment = (ISimpleAssignmentOperation)initializer; - if (simpleAssignment.Target is IPropertyReferenceOperation propertyReference && - _suppressMessageAttributeType.Equals(propertyReference.Property.ContainingType)) - { - builder.Add((propertyReference.Property.Name, simpleAssignment.Value)); - } + builder.Add((propertyReference.Property.Name, simpleAssignment.Value)); } - - namedAttributeArguments = builder.ToImmutable(); - return namedAttributeArguments.Length > 0; } - public static bool HasValidScope(ImmutableArray<(string name, IOperation value)> namedAttributeArguments, out TargetScope targetScope) + namedAttributeArguments = builder.ToImmutable(); + return namedAttributeArguments.Length > 0; + } + + public static bool HasValidScope(ImmutableArray<(string name, IOperation value)> namedAttributeArguments, out TargetScope targetScope) + { + if (!TryGetNamedArgument(namedAttributeArguments, SuppressMessageScope, out var scopeString, out _) || + RoslynString.IsNullOrEmpty(scopeString)) { - if (!TryGetNamedArgument(namedAttributeArguments, SuppressMessageScope, out var scopeString, out _) || - RoslynString.IsNullOrEmpty(scopeString)) - { - // Missing/Null/Empty scope values are treated equivalent to a compilation wide suppression. - targetScope = TargetScope.Module; - } - else if (!s_targetScopesMap.TryGetValue(scopeString, out targetScope)) - { - targetScope = TargetScope.None; - return false; - } + // Missing/Null/Empty scope values are treated equivalent to a compilation wide suppression. + targetScope = TargetScope.Module; + } + else if (!s_targetScopesMap.TryGetValue(scopeString, out targetScope)) + { + targetScope = TargetScope.None; + return false; + } + + return true; + } + + public bool HasValidTarget( + ImmutableArray<(string name, IOperation value)> namedAttributeArguments, + TargetScope targetScope, + out bool targetHasDocCommentIdFormat, + out string? targetSymbolString, + out IOperation? targetValueOperation, + out ImmutableArray resolvedSymbols) + { + targetHasDocCommentIdFormat = false; + targetSymbolString = null; + targetValueOperation = null; + resolvedSymbols = []; + if (targetScope == TargetScope.Resource) + { + // Legacy scope which we do not handle. return true; } - public bool HasValidTarget( - ImmutableArray<(string name, IOperation value)> namedAttributeArguments, - TargetScope targetScope, - out bool targetHasDocCommentIdFormat, - out string? targetSymbolString, - out IOperation? targetValueOperation, - out ImmutableArray resolvedSymbols) + if (!TryGetNamedArgument(namedAttributeArguments, SuppressMessageTarget, out targetSymbolString, out targetValueOperation)) { - targetHasDocCommentIdFormat = false; targetSymbolString = null; - targetValueOperation = null; - resolvedSymbols = []; - - if (targetScope == TargetScope.Resource) - { - // Legacy scope which we do not handle. - return true; - } - - if (!TryGetNamedArgument(namedAttributeArguments, SuppressMessageTarget, out targetSymbolString, out targetValueOperation)) - { - targetSymbolString = null; - } - - if (targetScope == TargetScope.Module) - { - // Compilation wide suppression with a non-null target is considered invalid. - return targetSymbolString == null; - } - else if (targetScope == TargetScope.NamespaceAndDescendants) - { - // TargetSymbolResolver expects the callers to normalize 'NamespaceAndDescendants' and 'Namespace' scopes to 'Namespace' scope. - targetScope = TargetScope.Namespace; - } + } - var resolver = new TargetSymbolResolver(_compilation, targetScope, targetSymbolString); - resolvedSymbols = resolver.Resolve(out targetHasDocCommentIdFormat); - return !resolvedSymbols.IsEmpty; + if (targetScope == TargetScope.Module) + { + // Compilation wide suppression with a non-null target is considered invalid. + return targetSymbolString == null; + } + else if (targetScope == TargetScope.NamespaceAndDescendants) + { + // TargetSymbolResolver expects the callers to normalize 'NamespaceAndDescendants' and 'Namespace' scopes to 'Namespace' scope. + targetScope = TargetScope.Namespace; } - private static bool TryGetNamedArgument( - ImmutableArray<(string name, IOperation value)> namedAttributeArguments, - string argumentName, - out string? argumentValue, - [NotNullWhen(returnValue: true)] out IOperation? argumentValueOperation) + var resolver = new TargetSymbolResolver(_compilation, targetScope, targetSymbolString); + resolvedSymbols = resolver.Resolve(out targetHasDocCommentIdFormat); + return !resolvedSymbols.IsEmpty; + } + + private static bool TryGetNamedArgument( + ImmutableArray<(string name, IOperation value)> namedAttributeArguments, + string argumentName, + out string? argumentValue, + [NotNullWhen(returnValue: true)] out IOperation? argumentValueOperation) + { + foreach (var (name, value) in namedAttributeArguments) { - foreach (var (name, value) in namedAttributeArguments) + if (name == argumentName && + value.ConstantValue.HasValue && + value.ConstantValue.Value is string stringValue) { - if (name == argumentName && - value.ConstantValue.HasValue && - value.ConstantValue.Value is string stringValue) - { - argumentValue = stringValue; - argumentValueOperation = value; - return true; - } + argumentValue = stringValue; + argumentValueOperation = value; + return true; } - - argumentValue = null; - argumentValueOperation = null; - return false; } + + argumentValue = null; + argumentValueOperation = null; + return false; } } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs index 5d21cdcd60aa0..ea5ce9e512387 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs @@ -19,841 +19,841 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.RemoveUnusedMembers +namespace Microsoft.CodeAnalysis.RemoveUnusedMembers; + +internal abstract class AbstractRemoveUnusedMembersDiagnosticAnalyzer< + TDocumentationCommentTriviaSyntax, + TIdentifierNameSyntax, + TTypeDeclarationSyntax, + TMemberDeclarationSyntax> + : AbstractCodeQualityDiagnosticAnalyzer + where TDocumentationCommentTriviaSyntax : SyntaxNode + where TIdentifierNameSyntax : SyntaxNode + where TTypeDeclarationSyntax : TMemberDeclarationSyntax + where TMemberDeclarationSyntax : SyntaxNode { - internal abstract class AbstractRemoveUnusedMembersDiagnosticAnalyzer< - TDocumentationCommentTriviaSyntax, - TIdentifierNameSyntax, - TTypeDeclarationSyntax, - TMemberDeclarationSyntax> - : AbstractCodeQualityDiagnosticAnalyzer - where TDocumentationCommentTriviaSyntax : SyntaxNode - where TIdentifierNameSyntax : SyntaxNode - where TTypeDeclarationSyntax : TMemberDeclarationSyntax - where TMemberDeclarationSyntax : SyntaxNode + /// + /// Produces names like TypeName.MemberName + /// + private static readonly SymbolDisplayFormat ContainingTypeAndNameOnlyFormat = new( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, + memberOptions: SymbolDisplayMemberOptions.IncludeContainingType); + + // IDE0051: "Remove unused members" (Symbol is declared but never referenced) + private static readonly DiagnosticDescriptor s_removeUnusedMembersRule = CreateDescriptor( + IDEDiagnosticIds.RemoveUnusedMembersDiagnosticId, + EnforceOnBuildValues.RemoveUnusedMembers, + new LocalizableResourceString(nameof(AnalyzersResources.Remove_unused_private_members), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Private_member_0_is_unused), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + hasAnyCodeStyleOption: false, isUnnecessary: true); + + // IDE0052: "Remove unread members" (Value is written and/or symbol is referenced, but the assigned value is never read) + // Internal for testing + internal static readonly DiagnosticDescriptor s_removeUnreadMembersRule = CreateDescriptor( + IDEDiagnosticIds.RemoveUnreadMembersDiagnosticId, + EnforceOnBuildValues.RemoveUnreadMembers, + new LocalizableResourceString(nameof(AnalyzersResources.Remove_unread_private_members), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Private_member_0_can_be_removed_as_the_value_assigned_to_it_is_never_read), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + hasAnyCodeStyleOption: false, isUnnecessary: true); + + protected AbstractRemoveUnusedMembersDiagnosticAnalyzer() + : base([s_removeUnusedMembersRule, s_removeUnreadMembersRule], + GeneratedCodeAnalysisFlags.Analyze) // We want to analyze references in generated code, but not report unused members in generated code. { - /// - /// Produces names like TypeName.MemberName - /// - private static readonly SymbolDisplayFormat ContainingTypeAndNameOnlyFormat = new( - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, - memberOptions: SymbolDisplayMemberOptions.IncludeContainingType); - - // IDE0051: "Remove unused members" (Symbol is declared but never referenced) - private static readonly DiagnosticDescriptor s_removeUnusedMembersRule = CreateDescriptor( - IDEDiagnosticIds.RemoveUnusedMembersDiagnosticId, - EnforceOnBuildValues.RemoveUnusedMembers, - new LocalizableResourceString(nameof(AnalyzersResources.Remove_unused_private_members), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Private_member_0_is_unused), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - hasAnyCodeStyleOption: false, isUnnecessary: true); - - // IDE0052: "Remove unread members" (Value is written and/or symbol is referenced, but the assigned value is never read) - // Internal for testing - internal static readonly DiagnosticDescriptor s_removeUnreadMembersRule = CreateDescriptor( - IDEDiagnosticIds.RemoveUnreadMembersDiagnosticId, - EnforceOnBuildValues.RemoveUnreadMembers, - new LocalizableResourceString(nameof(AnalyzersResources.Remove_unread_private_members), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Private_member_0_can_be_removed_as_the_value_assigned_to_it_is_never_read), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - hasAnyCodeStyleOption: false, isUnnecessary: true); - - protected AbstractRemoveUnusedMembersDiagnosticAnalyzer() - : base([s_removeUnusedMembersRule, s_removeUnreadMembersRule], - GeneratedCodeAnalysisFlags.Analyze) // We want to analyze references in generated code, but not report unused members in generated code. - { - } + } - protected abstract IEnumerable GetTypeDeclarations(INamedTypeSymbol namedType, CancellationToken cancellationToken); - protected abstract SyntaxList GetMembers(TTypeDeclarationSyntax typeDeclaration); + protected abstract IEnumerable GetTypeDeclarations(INamedTypeSymbol namedType, CancellationToken cancellationToken); + protected abstract SyntaxList GetMembers(TTypeDeclarationSyntax typeDeclaration); - // We need to analyze the whole document even for edits within a method body, - // because we might add or remove references to members in executable code. - // For example, if we had an unused field with no references, then editing any single method body - // to reference this field should clear the unused field diagnostic. - // Hence, we need to re-analyze the declarations in the whole file for any edits within the document. - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + // We need to analyze the whole document even for edits within a method body, + // because we might add or remove references to members in executable code. + // For example, if we had an unused field with no references, then editing any single method body + // to reference this field should clear the unused field diagnostic. + // Hence, we need to re-analyze the declarations in the whole file for any edits within the document. + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; - protected sealed override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(compilationStartContext - => CompilationAnalyzer.CreateAndRegisterActions(compilationStartContext, this)); + protected sealed override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(compilationStartContext + => CompilationAnalyzer.CreateAndRegisterActions(compilationStartContext, this)); + /// + /// Override this method to register custom language specific actions to find symbol usages. + /// + protected virtual void HandleNamedTypeSymbolStart(SymbolStartAnalysisContext context, Action onSymbolUsageFound) + { + } + + private sealed class CompilationAnalyzer + { + private readonly object _gate; /// - /// Override this method to register custom language specific actions to find symbol usages. + /// State map for candidate member symbols, with the value indicating how each symbol is used in executable code. /// - protected virtual void HandleNamedTypeSymbolStart(SymbolStartAnalysisContext context, Action onSymbolUsageFound) + private readonly Dictionary _symbolValueUsageStateMap = []; + /// + /// List of properties that have a 'get' accessor usage, while the value itself is not used, e.g.: + /// + /// class C + /// { + /// private int P { get; set; } + /// public void M() { P++; } + /// } + /// + /// Here, 'get' accessor is used in an increment operation, but the result of the increment operation isn't used and 'P' itself is not used anywhere else, so it can be safely removed + /// + private readonly HashSet _propertiesWithShadowGetAccessorUsages = []; + private readonly INamedTypeSymbol? _taskType, _genericTaskType, _debuggerDisplayAttributeType, _structLayoutAttributeType; + private readonly INamedTypeSymbol? _eventArgsType; + private readonly INamedTypeSymbol? _iNotifyCompletionType; + private readonly DeserializationConstructorCheck _deserializationConstructorCheck; + private readonly ImmutableHashSet _attributeSetForMethodsToIgnore; + private readonly AbstractRemoveUnusedMembersDiagnosticAnalyzer _analyzer; + + private CompilationAnalyzer( + Compilation compilation, + AbstractRemoveUnusedMembersDiagnosticAnalyzer analyzer) { + _gate = new object(); + _analyzer = analyzer; + + _taskType = compilation.TaskType(); + _genericTaskType = compilation.TaskOfTType(); + _debuggerDisplayAttributeType = compilation.DebuggerDisplayAttributeType(); + _structLayoutAttributeType = compilation.StructLayoutAttributeType(); + _eventArgsType = compilation.EventArgsType(); + _iNotifyCompletionType = compilation.GetBestTypeByMetadataName(typeof(INotifyCompletion).FullName!); + _deserializationConstructorCheck = new DeserializationConstructorCheck(compilation); + _attributeSetForMethodsToIgnore = ImmutableHashSet.CreateRange(GetAttributesForMethodsToIgnore(compilation)); } - private sealed class CompilationAnalyzer + private static Location GetDiagnosticLocation(ISymbol symbol) + => symbol.Locations[0]; + + private static IEnumerable GetAttributesForMethodsToIgnore(Compilation compilation) { - private readonly object _gate; - /// - /// State map for candidate member symbols, with the value indicating how each symbol is used in executable code. - /// - private readonly Dictionary _symbolValueUsageStateMap = []; - /// - /// List of properties that have a 'get' accessor usage, while the value itself is not used, e.g.: - /// - /// class C - /// { - /// private int P { get; set; } - /// public void M() { P++; } - /// } - /// - /// Here, 'get' accessor is used in an increment operation, but the result of the increment operation isn't used and 'P' itself is not used anywhere else, so it can be safely removed - /// - private readonly HashSet _propertiesWithShadowGetAccessorUsages = []; - private readonly INamedTypeSymbol? _taskType, _genericTaskType, _debuggerDisplayAttributeType, _structLayoutAttributeType; - private readonly INamedTypeSymbol? _eventArgsType; - private readonly INamedTypeSymbol? _iNotifyCompletionType; - private readonly DeserializationConstructorCheck _deserializationConstructorCheck; - private readonly ImmutableHashSet _attributeSetForMethodsToIgnore; - private readonly AbstractRemoveUnusedMembersDiagnosticAnalyzer _analyzer; - - private CompilationAnalyzer( - Compilation compilation, - AbstractRemoveUnusedMembersDiagnosticAnalyzer analyzer) + // Ignore methods with special serialization attributes, which are invoked by the runtime + // for deserialization. + var onDeserializingAttribute = compilation.OnDeserializingAttribute(); + if (onDeserializingAttribute != null) { - _gate = new object(); - _analyzer = analyzer; - - _taskType = compilation.TaskType(); - _genericTaskType = compilation.TaskOfTType(); - _debuggerDisplayAttributeType = compilation.DebuggerDisplayAttributeType(); - _structLayoutAttributeType = compilation.StructLayoutAttributeType(); - _eventArgsType = compilation.EventArgsType(); - _iNotifyCompletionType = compilation.GetBestTypeByMetadataName(typeof(INotifyCompletion).FullName!); - _deserializationConstructorCheck = new DeserializationConstructorCheck(compilation); - _attributeSetForMethodsToIgnore = ImmutableHashSet.CreateRange(GetAttributesForMethodsToIgnore(compilation)); + yield return onDeserializingAttribute; } - private static Location GetDiagnosticLocation(ISymbol symbol) - => symbol.Locations[0]; - - private static IEnumerable GetAttributesForMethodsToIgnore(Compilation compilation) + var onDeserializedAttribute = compilation.OnDeserializedAttribute(); + if (onDeserializedAttribute != null) { - // Ignore methods with special serialization attributes, which are invoked by the runtime - // for deserialization. - var onDeserializingAttribute = compilation.OnDeserializingAttribute(); - if (onDeserializingAttribute != null) - { - yield return onDeserializingAttribute; - } - - var onDeserializedAttribute = compilation.OnDeserializedAttribute(); - if (onDeserializedAttribute != null) - { - yield return onDeserializedAttribute; - } - - var onSerializingAttribute = compilation.OnSerializingAttribute(); - if (onSerializingAttribute != null) - { - yield return onSerializingAttribute; - } + yield return onDeserializedAttribute; + } - var onSerializedAttribute = compilation.OnSerializedAttribute(); - if (onSerializedAttribute != null) - { - yield return onSerializedAttribute; - } + var onSerializingAttribute = compilation.OnSerializingAttribute(); + if (onSerializingAttribute != null) + { + yield return onSerializingAttribute; + } - var comRegisterFunctionAttribute = compilation.ComRegisterFunctionAttribute(); - if (comRegisterFunctionAttribute != null) - { - yield return comRegisterFunctionAttribute; - } + var onSerializedAttribute = compilation.OnSerializedAttribute(); + if (onSerializedAttribute != null) + { + yield return onSerializedAttribute; + } - var comUnregisterFunctionAttribute = compilation.ComUnregisterFunctionAttribute(); - if (comUnregisterFunctionAttribute != null) - { - yield return comUnregisterFunctionAttribute; - } + var comRegisterFunctionAttribute = compilation.ComRegisterFunctionAttribute(); + if (comRegisterFunctionAttribute != null) + { + yield return comRegisterFunctionAttribute; } - public static void CreateAndRegisterActions( - CompilationStartAnalysisContext compilationStartContext, - AbstractRemoveUnusedMembersDiagnosticAnalyzer analyzer) + var comUnregisterFunctionAttribute = compilation.ComUnregisterFunctionAttribute(); + if (comUnregisterFunctionAttribute != null) { - var compilationAnalyzer = new CompilationAnalyzer(compilationStartContext.Compilation, analyzer); - compilationAnalyzer.RegisterActions(compilationStartContext); + yield return comUnregisterFunctionAttribute; } + } - private void RegisterActions(CompilationStartAnalysisContext compilationStartContext) + public static void CreateAndRegisterActions( + CompilationStartAnalysisContext compilationStartContext, + AbstractRemoveUnusedMembersDiagnosticAnalyzer analyzer) + { + var compilationAnalyzer = new CompilationAnalyzer(compilationStartContext.Compilation, analyzer); + compilationAnalyzer.RegisterActions(compilationStartContext); + } + + private void RegisterActions(CompilationStartAnalysisContext compilationStartContext) + { + // We register following actions in the compilation: + // 1. A symbol action for member symbols to ensure the member's unused state is initialized to true for every private member symbol. + // 2. Operation actions for member references, invocations and object creations to detect member usages, i.e. read or read reference taken. + // 3. Operation action for field initializers to detect non-constant initialization. + // 4. Operation action for invalid operations to bail out on erroneous code. + // 5. A symbol start/end action for named types to report diagnostics for candidate members that have no usage in executable code. + // + // Note that we need to register separately for OperationKind.Invocation and OperationKind.ObjectCreation due to https://github.com/dotnet/roslyn/issues/26206 + + compilationStartContext.RegisterSymbolAction(AnalyzeSymbolDeclaration, SymbolKind.Method, SymbolKind.Field, SymbolKind.Property, SymbolKind.Event); + + Action onSymbolUsageFound = OnSymbolUsage; + compilationStartContext.RegisterSymbolStartAction(symbolStartContext => { - // We register following actions in the compilation: - // 1. A symbol action for member symbols to ensure the member's unused state is initialized to true for every private member symbol. - // 2. Operation actions for member references, invocations and object creations to detect member usages, i.e. read or read reference taken. - // 3. Operation action for field initializers to detect non-constant initialization. - // 4. Operation action for invalid operations to bail out on erroneous code. - // 5. A symbol start/end action for named types to report diagnostics for candidate members that have no usage in executable code. - // - // Note that we need to register separately for OperationKind.Invocation and OperationKind.ObjectCreation due to https://github.com/dotnet/roslyn/issues/26206 - - compilationStartContext.RegisterSymbolAction(AnalyzeSymbolDeclaration, SymbolKind.Method, SymbolKind.Field, SymbolKind.Property, SymbolKind.Event); - - Action onSymbolUsageFound = OnSymbolUsage; - compilationStartContext.RegisterSymbolStartAction(symbolStartContext => - { - if (!ShouldAnalyze(symbolStartContext, (INamedTypeSymbol)symbolStartContext.Symbol)) - return; - - var hasUnsupportedOperation = false; - symbolStartContext.RegisterOperationAction(AnalyzeMemberReferenceOperation, OperationKind.FieldReference, OperationKind.MethodReference, OperationKind.PropertyReference, OperationKind.EventReference); - symbolStartContext.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer); - symbolStartContext.RegisterOperationAction(AnalyzeInvocationOperation, OperationKind.Invocation); - symbolStartContext.RegisterOperationAction(AnalyzeNameOfOperation, OperationKind.NameOf); - symbolStartContext.RegisterOperationAction(AnalyzeObjectCreationOperation, OperationKind.ObjectCreation); - - // We bail out reporting diagnostics for named types if it contains following kind of operations: - // 1. Invalid operations, i.e. erroneous code: - // We do so to ensure that we don't report false positives during editing scenarios in the IDE, where the user - // is still editing code and fixing unresolved references to symbols, such as overload resolution errors. - // 2. Dynamic operations, where we do not know the exact member being referenced at compile time. - // 3. Operations with OperationKind.None. - symbolStartContext.RegisterOperationAction(_ => hasUnsupportedOperation = true, OperationKind.Invalid, OperationKind.None, - OperationKind.DynamicIndexerAccess, OperationKind.DynamicInvocation, OperationKind.DynamicMemberReference, OperationKind.DynamicObjectCreation); - - symbolStartContext.RegisterSymbolEndAction(symbolEndContext => OnSymbolEnd(symbolEndContext, hasUnsupportedOperation)); - - // Register custom language-specific actions, if any. - _analyzer.HandleNamedTypeSymbolStart(symbolStartContext, onSymbolUsageFound); - }, SymbolKind.NamedType); - - bool ShouldAnalyze(SymbolStartAnalysisContext context, INamedTypeSymbol namedType) + if (!ShouldAnalyze(symbolStartContext, (INamedTypeSymbol)symbolStartContext.Symbol)) + return; + + var hasUnsupportedOperation = false; + symbolStartContext.RegisterOperationAction(AnalyzeMemberReferenceOperation, OperationKind.FieldReference, OperationKind.MethodReference, OperationKind.PropertyReference, OperationKind.EventReference); + symbolStartContext.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer); + symbolStartContext.RegisterOperationAction(AnalyzeInvocationOperation, OperationKind.Invocation); + symbolStartContext.RegisterOperationAction(AnalyzeNameOfOperation, OperationKind.NameOf); + symbolStartContext.RegisterOperationAction(AnalyzeObjectCreationOperation, OperationKind.ObjectCreation); + + // We bail out reporting diagnostics for named types if it contains following kind of operations: + // 1. Invalid operations, i.e. erroneous code: + // We do so to ensure that we don't report false positives during editing scenarios in the IDE, where the user + // is still editing code and fixing unresolved references to symbols, such as overload resolution errors. + // 2. Dynamic operations, where we do not know the exact member being referenced at compile time. + // 3. Operations with OperationKind.None. + symbolStartContext.RegisterOperationAction(_ => hasUnsupportedOperation = true, OperationKind.Invalid, OperationKind.None, + OperationKind.DynamicIndexerAccess, OperationKind.DynamicInvocation, OperationKind.DynamicMemberReference, OperationKind.DynamicObjectCreation); + + symbolStartContext.RegisterSymbolEndAction(symbolEndContext => OnSymbolEnd(symbolEndContext, hasUnsupportedOperation)); + + // Register custom language-specific actions, if any. + _analyzer.HandleNamedTypeSymbolStart(symbolStartContext, onSymbolUsageFound); + }, SymbolKind.NamedType); + + bool ShouldAnalyze(SymbolStartAnalysisContext context, INamedTypeSymbol namedType) + { + // Check if we have at least one candidate symbol in analysis scope. + foreach (var member in namedType.GetMembers()) { - // Check if we have at least one candidate symbol in analysis scope. - foreach (var member in namedType.GetMembers()) + if (IsCandidateSymbol(member.OriginalDefinition) + && context.ShouldAnalyzeLocation(GetDiagnosticLocation(member))) { - if (IsCandidateSymbol(member.OriginalDefinition) - && context.ShouldAnalyzeLocation(GetDiagnosticLocation(member))) - { - return true; - } + return true; } + } - // We have to analyze nested types if containing type contains a candidate field in analysis scope. - if (namedType.ContainingType is { } containingType) - return ShouldAnalyze(context, containingType); + // We have to analyze nested types if containing type contains a candidate field in analysis scope. + if (namedType.ContainingType is { } containingType) + return ShouldAnalyze(context, containingType); - return false; - } + return false; } + } - private void AnalyzeSymbolDeclaration(SymbolAnalysisContext symbolContext) + private void AnalyzeSymbolDeclaration(SymbolAnalysisContext symbolContext) + { + var symbol = symbolContext.Symbol.OriginalDefinition; + if (IsCandidateSymbol(symbol) + && symbolContext.ShouldAnalyzeLocation(GetDiagnosticLocation(symbol))) { - var symbol = symbolContext.Symbol.OriginalDefinition; - if (IsCandidateSymbol(symbol) - && symbolContext.ShouldAnalyzeLocation(GetDiagnosticLocation(symbol))) + lock (_gate) { - lock (_gate) + // Initialize unused state to 'ValueUsageInfo.None' to indicate that + // no read/write references have been encountered yet for this symbol. + // Note that we might receive a symbol reference (AnalyzeMemberOperation) callback before + // this symbol declaration callback, so even though we cannot receive duplicate callbacks for a symbol, + // an entry might already be present of the declared symbol here. + if (!_symbolValueUsageStateMap.ContainsKey(symbol)) { - // Initialize unused state to 'ValueUsageInfo.None' to indicate that - // no read/write references have been encountered yet for this symbol. - // Note that we might receive a symbol reference (AnalyzeMemberOperation) callback before - // this symbol declaration callback, so even though we cannot receive duplicate callbacks for a symbol, - // an entry might already be present of the declared symbol here. - if (!_symbolValueUsageStateMap.ContainsKey(symbol)) - { - _symbolValueUsageStateMap.Add(symbol, ValueUsageInfo.None); - } + _symbolValueUsageStateMap.Add(symbol, ValueUsageInfo.None); } } } + } - private void AnalyzeFieldInitializer(OperationAnalysisContext operationContext) + private void AnalyzeFieldInitializer(OperationAnalysisContext operationContext) + { + // Check if the initialized fields are being initialized a non-constant value. + // If so, we want to consider these fields as being written to, + // so that we conservatively report an "Unread member" diagnostic instead of an "Unused member" diagnostic. + // This ensures that we do not offer a code fix for these fields that silently removes the initializer, + // as a non-constant initializer might have side-effects, which need to be preserved. + // On the other hand, initialization with a constant value can have no side-effects, and is safe to be removed. + var initializer = (IFieldInitializerOperation)operationContext.Operation; + if (!initializer.Value.ConstantValue.HasValue) { - // Check if the initialized fields are being initialized a non-constant value. - // If so, we want to consider these fields as being written to, - // so that we conservatively report an "Unread member" diagnostic instead of an "Unused member" diagnostic. - // This ensures that we do not offer a code fix for these fields that silently removes the initializer, - // as a non-constant initializer might have side-effects, which need to be preserved. - // On the other hand, initialization with a constant value can have no side-effects, and is safe to be removed. - var initializer = (IFieldInitializerOperation)operationContext.Operation; - if (!initializer.Value.ConstantValue.HasValue) + foreach (var field in initializer.InitializedFields) { - foreach (var field in initializer.InitializedFields) - { - OnSymbolUsage(field, ValueUsageInfo.Write); - } + OnSymbolUsage(field, ValueUsageInfo.Write); } } + } - private void OnSymbolUsage(ISymbol? memberSymbol, ValueUsageInfo usageInfo) + private void OnSymbolUsage(ISymbol? memberSymbol, ValueUsageInfo usageInfo) + { + if (!IsCandidateSymbol(memberSymbol)) { - if (!IsCandidateSymbol(memberSymbol)) - { - return; - } + return; + } - lock (_gate) + lock (_gate) + { + // Update the usage info for the memberSymbol + if (_symbolValueUsageStateMap.TryGetValue(memberSymbol, out var currentUsageInfo)) { - // Update the usage info for the memberSymbol - if (_symbolValueUsageStateMap.TryGetValue(memberSymbol, out var currentUsageInfo)) - { - usageInfo = currentUsageInfo | usageInfo; - } - - _symbolValueUsageStateMap[memberSymbol] = usageInfo; + usageInfo = currentUsageInfo | usageInfo; } + + _symbolValueUsageStateMap[memberSymbol] = usageInfo; } + } - private bool TryRemove(ISymbol memberSymbol, out ValueUsageInfo valueUsageInfo) + private bool TryRemove(ISymbol memberSymbol, out ValueUsageInfo valueUsageInfo) + { + lock (_gate) { - lock (_gate) + if (_symbolValueUsageStateMap.TryGetValue(memberSymbol, out valueUsageInfo)) { - if (_symbolValueUsageStateMap.TryGetValue(memberSymbol, out valueUsageInfo)) - { - _symbolValueUsageStateMap.Remove(memberSymbol); - return true; - } - - return false; + _symbolValueUsageStateMap.Remove(memberSymbol); + return true; } + + return false; } + } - private void AnalyzeMemberReferenceOperation(OperationAnalysisContext operationContext) + private void AnalyzeMemberReferenceOperation(OperationAnalysisContext operationContext) + { + var memberReference = (IMemberReferenceOperation)operationContext.Operation; + var memberSymbol = memberReference.Member.OriginalDefinition; + if (IsCandidateSymbol(memberSymbol)) { - var memberReference = (IMemberReferenceOperation)operationContext.Operation; - var memberSymbol = memberReference.Member.OriginalDefinition; - if (IsCandidateSymbol(memberSymbol)) - { - // Get the value usage info. - var valueUsageInfo = memberReference.GetValueUsageInfo(operationContext.ContainingSymbol); + // Get the value usage info. + var valueUsageInfo = memberReference.GetValueUsageInfo(operationContext.ContainingSymbol); - if (valueUsageInfo == ValueUsageInfo.ReadWrite) + if (valueUsageInfo == ValueUsageInfo.ReadWrite) + { + Debug.Assert(memberReference.Parent is ICompoundAssignmentOperation compoundAssignment && + compoundAssignment.Target == memberReference || + memberReference.Parent is ICoalesceAssignmentOperation coalesceAssignment && + coalesceAssignment.Target == memberReference || + memberReference.Parent is IIncrementOrDecrementOperation || + memberReference.Parent is IReDimClauseOperation reDimClause && reDimClause.Operand == memberReference); + + // Compound assignment or increment whose value is being dropped (parent is an expression statement) + // is treated as a Write as the value was never actually 'read' in a way that is observable. + // + // Consider the following example: + // class C + // { + // private int _f1 = 0, _f2 = 0; + // public void M1() { _f1++; } + // public int M2() { return _f2++; } + // } + // + // Note that the increment operation '_f1++' is child of an expression statement, which drops the result of the increment. + // while the increment operation '_f2++' is child of a return statement, which uses the result of the increment. + // For the above test, '_f1' can be safely removed without affecting the semantics of the program, while '_f2' cannot be removed. + // Additionally, we special case ICoalesceAssignmentOperation (??=) and treat it as a read-write, + // see https://github.com/dotnet/roslyn/issues/66975 for more details + + if (memberReference?.Parent?.Parent is IExpressionStatementOperation && + memberReference.Parent is not ICoalesceAssignmentOperation) { - Debug.Assert(memberReference.Parent is ICompoundAssignmentOperation compoundAssignment && - compoundAssignment.Target == memberReference || - memberReference.Parent is ICoalesceAssignmentOperation coalesceAssignment && - coalesceAssignment.Target == memberReference || - memberReference.Parent is IIncrementOrDecrementOperation || - memberReference.Parent is IReDimClauseOperation reDimClause && reDimClause.Operand == memberReference); - - // Compound assignment or increment whose value is being dropped (parent is an expression statement) - // is treated as a Write as the value was never actually 'read' in a way that is observable. - // - // Consider the following example: - // class C - // { - // private int _f1 = 0, _f2 = 0; - // public void M1() { _f1++; } - // public int M2() { return _f2++; } - // } - // - // Note that the increment operation '_f1++' is child of an expression statement, which drops the result of the increment. - // while the increment operation '_f2++' is child of a return statement, which uses the result of the increment. - // For the above test, '_f1' can be safely removed without affecting the semantics of the program, while '_f2' cannot be removed. - // Additionally, we special case ICoalesceAssignmentOperation (??=) and treat it as a read-write, - // see https://github.com/dotnet/roslyn/issues/66975 for more details - - if (memberReference?.Parent?.Parent is IExpressionStatementOperation && - memberReference.Parent is not ICoalesceAssignmentOperation) - { - valueUsageInfo = ValueUsageInfo.Write; + valueUsageInfo = ValueUsageInfo.Write; - // If the symbol is a property, than mark it as having shadow 'get' accessor usages. - // Later we will produce message "Private member X can be removed as the value assigned to it is never read" - // rather than "Private property X can be converted to a method as its get accessor is never invoked" depending on this information. - if (memberSymbol is IPropertySymbol propertySymbol) + // If the symbol is a property, than mark it as having shadow 'get' accessor usages. + // Later we will produce message "Private member X can be removed as the value assigned to it is never read" + // rather than "Private property X can be converted to a method as its get accessor is never invoked" depending on this information. + if (memberSymbol is IPropertySymbol propertySymbol) + { + lock (_gate) { - lock (_gate) - { - _propertiesWithShadowGetAccessorUsages.Add(propertySymbol); - } + _propertiesWithShadowGetAccessorUsages.Add(propertySymbol); } } } - - OnSymbolUsage(memberSymbol, valueUsageInfo); } + + OnSymbolUsage(memberSymbol, valueUsageInfo); } + } - private void AnalyzeInvocationOperation(OperationAnalysisContext operationContext) - { - var targetMethod = ((IInvocationOperation)operationContext.Operation).TargetMethod.OriginalDefinition; + private void AnalyzeInvocationOperation(OperationAnalysisContext operationContext) + { + var targetMethod = ((IInvocationOperation)operationContext.Operation).TargetMethod.OriginalDefinition; - // A method invocation is considered as a read reference to the symbol - // to ensure that we consider the method as "used". - OnSymbolUsage(targetMethod, ValueUsageInfo.Read); + // A method invocation is considered as a read reference to the symbol + // to ensure that we consider the method as "used". + OnSymbolUsage(targetMethod, ValueUsageInfo.Read); - // If the invoked method is a reduced extension method, also mark the original - // method from which it was reduced as "used". - if (targetMethod.ReducedFrom != null) - { - OnSymbolUsage(targetMethod.ReducedFrom, ValueUsageInfo.Read); - } + // If the invoked method is a reduced extension method, also mark the original + // method from which it was reduced as "used". + if (targetMethod.ReducedFrom != null) + { + OnSymbolUsage(targetMethod.ReducedFrom, ValueUsageInfo.Read); } + } - private void AnalyzeNameOfOperation(OperationAnalysisContext operationContext) - { - // 'nameof(argument)' is very commonly used for reading/writing to 'argument' in following ways: - // 1. Reflection based usage: See https://github.com/dotnet/roslyn/issues/32488 - // 2. Custom/Test frameworks: See https://github.com/dotnet/roslyn/issues/32008 and https://github.com/dotnet/roslyn/issues/31581 - // We treat 'nameof(argument)' as ValueUsageInfo.ReadWrite instead of ValueUsageInfo.NameOnly to avoid such false positives. + private void AnalyzeNameOfOperation(OperationAnalysisContext operationContext) + { + // 'nameof(argument)' is very commonly used for reading/writing to 'argument' in following ways: + // 1. Reflection based usage: See https://github.com/dotnet/roslyn/issues/32488 + // 2. Custom/Test frameworks: See https://github.com/dotnet/roslyn/issues/32008 and https://github.com/dotnet/roslyn/issues/31581 + // We treat 'nameof(argument)' as ValueUsageInfo.ReadWrite instead of ValueUsageInfo.NameOnly to avoid such false positives. - var nameofArgument = ((INameOfOperation)operationContext.Operation).Argument; + var nameofArgument = ((INameOfOperation)operationContext.Operation).Argument; - if (nameofArgument is IMemberReferenceOperation memberReference) - { - OnSymbolUsage(memberReference.Member.OriginalDefinition, ValueUsageInfo.ReadWrite); - return; - } + if (nameofArgument is IMemberReferenceOperation memberReference) + { + OnSymbolUsage(memberReference.Member.OriginalDefinition, ValueUsageInfo.ReadWrite); + return; + } - // Workaround for https://github.com/dotnet/roslyn/issues/19965 - // IOperation API does not expose potential references to methods/properties within - // a bound method group/property group. - var symbolInfo = nameofArgument.SemanticModel!.GetSymbolInfo(nameofArgument.Syntax, operationContext.CancellationToken); - foreach (var symbol in symbolInfo.GetAllSymbols()) + // Workaround for https://github.com/dotnet/roslyn/issues/19965 + // IOperation API does not expose potential references to methods/properties within + // a bound method group/property group. + var symbolInfo = nameofArgument.SemanticModel!.GetSymbolInfo(nameofArgument.Syntax, operationContext.CancellationToken); + foreach (var symbol in symbolInfo.GetAllSymbols()) + { + switch (symbol.Kind) { - switch (symbol.Kind) - { - // Handle potential references to methods/properties from missing IOperation - // for method group/property group. - case SymbolKind.Method: - case SymbolKind.Property: - OnSymbolUsage(symbol.OriginalDefinition, ValueUsageInfo.ReadWrite); - break; - } + // Handle potential references to methods/properties from missing IOperation + // for method group/property group. + case SymbolKind.Method: + case SymbolKind.Property: + OnSymbolUsage(symbol.OriginalDefinition, ValueUsageInfo.ReadWrite); + break; } } + } - private void AnalyzeObjectCreationOperation(OperationAnalysisContext operationContext) - { - var constructor = ((IObjectCreationOperation)operationContext.Operation).Constructor?.OriginalDefinition; + private void AnalyzeObjectCreationOperation(OperationAnalysisContext operationContext) + { + var constructor = ((IObjectCreationOperation)operationContext.Operation).Constructor?.OriginalDefinition; + + // An object creation is considered as a read reference to the constructor + // to ensure that we consider the constructor as "used". + OnSymbolUsage(constructor, ValueUsageInfo.Read); + } - // An object creation is considered as a read reference to the constructor - // to ensure that we consider the constructor as "used". - OnSymbolUsage(constructor, ValueUsageInfo.Read); + private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsupportedOperation) + { + if (hasUnsupportedOperation) + { + return; } - private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsupportedOperation) + if (symbolEndContext.Symbol.GetAttributes().Any(static (a, self) => a.AttributeClass == self._structLayoutAttributeType, this)) { - if (hasUnsupportedOperation) - { - return; - } + // Bail out for types with 'StructLayoutAttribute' as the ordering of the members is critical, + // and removal of unused members might break semantics. + return; + } - if (symbolEndContext.Symbol.GetAttributes().Any(static (a, self) => a.AttributeClass == self._structLayoutAttributeType, this)) - { - // Bail out for types with 'StructLayoutAttribute' as the ordering of the members is critical, - // and removal of unused members might break semantics. - return; - } + // Report diagnostics for unused candidate members. + var first = true; + using var _1 = PooledHashSet.GetInstance(out var symbolsReferencedInDocComments); + using var _2 = ArrayBuilder.GetInstance(out var debuggerDisplayAttributeArguments); - // Report diagnostics for unused candidate members. - var first = true; - using var _1 = PooledHashSet.GetInstance(out var symbolsReferencedInDocComments); - using var _2 = ArrayBuilder.GetInstance(out var debuggerDisplayAttributeArguments); + var entryPoint = symbolEndContext.Compilation.GetEntryPoint(symbolEndContext.CancellationToken); - var entryPoint = symbolEndContext.Compilation.GetEntryPoint(symbolEndContext.CancellationToken); + var namedType = (INamedTypeSymbol)symbolEndContext.Symbol; + foreach (var member in namedType.GetMembers()) + { + if (SymbolEqualityComparer.Default.Equals(entryPoint, member)) + { + continue; + } - var namedType = (INamedTypeSymbol)symbolEndContext.Symbol; - foreach (var member in namedType.GetMembers()) + // Check if the underlying member is neither read nor a readable reference to the member is taken. + // If so, we flag the member as either unused (never written) or unread (written but not read). + if (TryRemove(member, out var valueUsageInfo) && + !valueUsageInfo.IsReadFrom()) { - if (SymbolEqualityComparer.Default.Equals(entryPoint, member)) - { - continue; - } + Debug.Assert(IsCandidateSymbol(member)); + Debug.Assert(!member.IsImplicitlyDeclared); - // Check if the underlying member is neither read nor a readable reference to the member is taken. - // If so, we flag the member as either unused (never written) or unread (written but not read). - if (TryRemove(member, out var valueUsageInfo) && - !valueUsageInfo.IsReadFrom()) + if (first) { - Debug.Assert(IsCandidateSymbol(member)); - Debug.Assert(!member.IsImplicitlyDeclared); - - if (first) + // Bail out if there are syntax errors in any of the declarations of the containing type. + // Note that we check this only for the first time that we report an unused or unread member for the containing type. + if (HasSyntaxErrors(namedType, symbolEndContext.CancellationToken)) { - // Bail out if there are syntax errors in any of the declarations of the containing type. - // Note that we check this only for the first time that we report an unused or unread member for the containing type. - if (HasSyntaxErrors(namedType, symbolEndContext.CancellationToken)) - { - return; - } - - // Compute the set of candidate symbols referenced in all the documentation comments within the named type declarations. - // This set is computed once and used for all the iterations of the loop. - AddCandidateSymbolsReferencedInDocComments( - namedType, symbolEndContext.Compilation, symbolsReferencedInDocComments, symbolEndContext.CancellationToken); + return; + } - // Compute the set of string arguments to DebuggerDisplay attributes applied to any symbol within the named type declaration. - // These strings may have an embedded reference to the symbol. - // This set is computed once and used for all the iterations of the loop. - AddDebuggerDisplayAttributeArguments(namedType, debuggerDisplayAttributeArguments); + // Compute the set of candidate symbols referenced in all the documentation comments within the named type declarations. + // This set is computed once and used for all the iterations of the loop. + AddCandidateSymbolsReferencedInDocComments( + namedType, symbolEndContext.Compilation, symbolsReferencedInDocComments, symbolEndContext.CancellationToken); - first = false; - } + // Compute the set of string arguments to DebuggerDisplay attributes applied to any symbol within the named type declaration. + // These strings may have an embedded reference to the symbol. + // This set is computed once and used for all the iterations of the loop. + AddDebuggerDisplayAttributeArguments(namedType, debuggerDisplayAttributeArguments); - // Simple heuristic for members referenced in DebuggerDisplayAttribute's string argument: - // bail out if any of the DebuggerDisplay string arguments contains the member name. - // In future, we can consider improving this heuristic to parse the embedded expression - // and resolve symbol references. - if (debuggerDisplayAttributeArguments.Any(arg => arg.Contains(member.Name))) - { - continue; - } + first = false; + } - // Report IDE0051 or IDE0052 based on whether the underlying member has any Write/WritableRef/NonReadWriteRef references or not. - var rule = !valueUsageInfo.IsWrittenTo() && !valueUsageInfo.IsNameOnly() && !symbolsReferencedInDocComments.Contains(member) - ? s_removeUnusedMembersRule - : s_removeUnreadMembersRule; - - // Do not flag write-only properties that are not read. - // Write-only properties are assumed to have side effects - // visible through other means than a property getter. - if (rule == s_removeUnreadMembersRule && - member is IPropertySymbol property && - property.IsWriteOnly) - { - continue; - } + // Simple heuristic for members referenced in DebuggerDisplayAttribute's string argument: + // bail out if any of the DebuggerDisplay string arguments contains the member name. + // In future, we can consider improving this heuristic to parse the embedded expression + // and resolve symbol references. + if (debuggerDisplayAttributeArguments.Any(arg => arg.Contains(member.Name))) + { + continue; + } - // Most of the members should have a single location, except for partial methods. - // We report the diagnostic on the first location of the member. - var diagnostic = DiagnosticHelper.CreateWithMessage( - rule, - GetDiagnosticLocation(member), - NotificationOption2.ForSeverity(rule.DefaultSeverity), - additionalLocations: null, - properties: null, - GetMessage(rule, member)); - symbolEndContext.ReportDiagnostic(diagnostic); + // Report IDE0051 or IDE0052 based on whether the underlying member has any Write/WritableRef/NonReadWriteRef references or not. + var rule = !valueUsageInfo.IsWrittenTo() && !valueUsageInfo.IsNameOnly() && !symbolsReferencedInDocComments.Contains(member) + ? s_removeUnusedMembersRule + : s_removeUnreadMembersRule; + + // Do not flag write-only properties that are not read. + // Write-only properties are assumed to have side effects + // visible through other means than a property getter. + if (rule == s_removeUnreadMembersRule && + member is IPropertySymbol property && + property.IsWriteOnly) + { + continue; } + + // Most of the members should have a single location, except for partial methods. + // We report the diagnostic on the first location of the member. + var diagnostic = DiagnosticHelper.CreateWithMessage( + rule, + GetDiagnosticLocation(member), + NotificationOption2.ForSeverity(rule.DefaultSeverity), + symbolEndContext.Options, + additionalLocations: null, + properties: null, + GetMessage(rule, member)); + symbolEndContext.ReportDiagnostic(diagnostic); } } + } - private LocalizableString GetMessage( - DiagnosticDescriptor rule, - ISymbol member) + private LocalizableString GetMessage( + DiagnosticDescriptor rule, + ISymbol member) + { + var messageFormat = rule.MessageFormat; + if (rule == s_removeUnreadMembersRule) { - var messageFormat = rule.MessageFormat; - if (rule == s_removeUnreadMembersRule) + // IDE0052 has a different message for method and property symbols. + switch (member) { - // IDE0052 has a different message for method and property symbols. - switch (member) - { - case IMethodSymbol _: - messageFormat = AnalyzersResources.Private_method_0_can_be_removed_as_it_is_never_invoked; - break; - - case IPropertySymbol property: - // We change the message only if both 'get' and 'set' accessors are present and - // there are no shadow 'get' accessor usages. Otherwise the message will be confusing - if (property.GetMethod != null && property.SetMethod != null && - !_propertiesWithShadowGetAccessorUsages.Contains(property)) - { - messageFormat = AnalyzersResources.Private_property_0_can_be_converted_to_a_method_as_its_get_accessor_is_never_invoked; - } + case IMethodSymbol _: + messageFormat = AnalyzersResources.Private_method_0_can_be_removed_as_it_is_never_invoked; + break; + + case IPropertySymbol property: + // We change the message only if both 'get' and 'set' accessors are present and + // there are no shadow 'get' accessor usages. Otherwise the message will be confusing + if (property.GetMethod != null && property.SetMethod != null && + !_propertiesWithShadowGetAccessorUsages.Contains(property)) + { + messageFormat = AnalyzersResources.Private_property_0_can_be_converted_to_a_method_as_its_get_accessor_is_never_invoked; + } - break; - } + break; } - - return new DiagnosticHelper.LocalizableStringWithArguments( - messageFormat, member.ToDisplayString(ContainingTypeAndNameOnlyFormat)); } - private static bool HasSyntaxErrors(INamedTypeSymbol namedTypeSymbol, CancellationToken cancellationToken) + return new DiagnosticHelper.LocalizableStringWithArguments( + messageFormat, member.ToDisplayString(ContainingTypeAndNameOnlyFormat)); + } + + private static bool HasSyntaxErrors(INamedTypeSymbol namedTypeSymbol, CancellationToken cancellationToken) + { + foreach (var tree in namedTypeSymbol.Locations.Select(l => l.SourceTree).Distinct().WhereNotNull()) { - foreach (var tree in namedTypeSymbol.Locations.Select(l => l.SourceTree).Distinct().WhereNotNull()) + if (tree.GetDiagnostics(cancellationToken).Any(d => d.Severity == DiagnosticSeverity.Error)) { - if (tree.GetDiagnostics(cancellationToken).Any(d => d.Severity == DiagnosticSeverity.Error)) - { - return true; - } + return true; } - - return false; } - private void AddCandidateSymbolsReferencedInDocComments( - INamedTypeSymbol namedTypeSymbol, - Compilation compilation, - HashSet builder, - CancellationToken cancellationToken) + return false; + } + + private void AddCandidateSymbolsReferencedInDocComments( + INamedTypeSymbol namedTypeSymbol, + Compilation compilation, + HashSet builder, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var documentationComments); + AddAllDocumentationComments(); + + // Group by syntax tree so we can process all partial types within a tree at once, using just a single + // semantic model. + foreach (var group in documentationComments.GroupBy(d => d.SyntaxTree)) { - using var _ = ArrayBuilder.GetInstance(out var documentationComments); - AddAllDocumentationComments(); + var syntaxTree = group.Key; + SemanticModel? lazyModel = null; - // Group by syntax tree so we can process all partial types within a tree at once, using just a single - // semantic model. - foreach (var group in documentationComments.GroupBy(d => d.SyntaxTree)) + foreach (var docComment in group) { - var syntaxTree = group.Key; - SemanticModel? lazyModel = null; - - foreach (var docComment in group) + // Note: we could likely optimize this further by only analyzing identifier nodes that have a + // matching name to one of the candidate symbols we care about. + foreach (var node in docComment.DescendantNodes().OfType()) { - // Note: we could likely optimize this further by only analyzing identifier nodes that have a - // matching name to one of the candidate symbols we care about. - foreach (var node in docComment.DescendantNodes().OfType()) - { - lazyModel ??= compilation.GetSemanticModel(syntaxTree); - var symbol = lazyModel.GetSymbolInfo(node, cancellationToken).Symbol?.OriginalDefinition; + lazyModel ??= compilation.GetSemanticModel(syntaxTree); + var symbol = lazyModel.GetSymbolInfo(node, cancellationToken).Symbol?.OriginalDefinition; - if (IsCandidateSymbol(symbol)) - builder.Add(symbol); - } + if (IsCandidateSymbol(symbol)) + builder.Add(symbol); } } + } - return; + return; + + void AddAllDocumentationComments() + { + using var _ = ArrayBuilder.GetInstance(out var stack); - void AddAllDocumentationComments() + // Defer to subclass to give us the type decl nodes for this named type. + foreach (var typeDeclaration in _analyzer.GetTypeDeclarations(namedTypeSymbol, cancellationToken)) { - using var _ = ArrayBuilder.GetInstance(out var stack); + stack.Clear(); + stack.Push(typeDeclaration); - // Defer to subclass to give us the type decl nodes for this named type. - foreach (var typeDeclaration in _analyzer.GetTypeDeclarations(namedTypeSymbol, cancellationToken)) + while (stack.Count > 0) { - stack.Clear(); - stack.Push(typeDeclaration); - - while (stack.Count > 0) - { - var currentType = stack.Pop(); + var currentType = stack.Pop(); - // Add the doc comments on the type itself. - AddDocumentationComments(currentType, documentationComments); + // Add the doc comments on the type itself. + AddDocumentationComments(currentType, documentationComments); - // Walk each member - foreach (var member in _analyzer.GetMembers(currentType)) + // Walk each member + foreach (var member in _analyzer.GetMembers(currentType)) + { + if (member is TTypeDeclarationSyntax childType) { - if (member is TTypeDeclarationSyntax childType) - { - // If the member is a nested type, recurse into it. - stack.Push(childType); - } - else - { - // Otherwise, add the doc comments on the member itself. - AddDocumentationComments(member, documentationComments); - } + // If the member is a nested type, recurse into it. + stack.Push(childType); + } + else + { + // Otherwise, add the doc comments on the member itself. + AddDocumentationComments(member, documentationComments); } } } } + } - static void AddDocumentationComments( - SyntaxNode memberDeclaration, ArrayBuilder documentationComments) - { - var firstToken = memberDeclaration.GetFirstToken(); - if (!firstToken.HasStructuredTrivia) - return; + static void AddDocumentationComments( + SyntaxNode memberDeclaration, ArrayBuilder documentationComments) + { + var firstToken = memberDeclaration.GetFirstToken(); + if (!firstToken.HasStructuredTrivia) + return; - foreach (var trivia in firstToken.LeadingTrivia) - { - if (trivia.HasStructure) - documentationComments.AddIfNotNull(trivia.GetStructure() as TDocumentationCommentTriviaSyntax); - } + foreach (var trivia in firstToken.LeadingTrivia) + { + if (trivia.HasStructure) + documentationComments.AddIfNotNull(trivia.GetStructure() as TDocumentationCommentTriviaSyntax); } } + } - private void AddDebuggerDisplayAttributeArguments(INamedTypeSymbol namedTypeSymbol, ArrayBuilder builder) - { - AddDebuggerDisplayAttributeArgumentsCore(namedTypeSymbol, builder); + private void AddDebuggerDisplayAttributeArguments(INamedTypeSymbol namedTypeSymbol, ArrayBuilder builder) + { + AddDebuggerDisplayAttributeArgumentsCore(namedTypeSymbol, builder); - foreach (var member in namedTypeSymbol.GetMembers()) + foreach (var member in namedTypeSymbol.GetMembers()) + { + switch (member) { - switch (member) - { - case INamedTypeSymbol nestedType: - AddDebuggerDisplayAttributeArguments(nestedType, builder); - break; - - case IPropertySymbol _: - case IFieldSymbol _: - AddDebuggerDisplayAttributeArgumentsCore(member, builder); - break; - } + case INamedTypeSymbol nestedType: + AddDebuggerDisplayAttributeArguments(nestedType, builder); + break; + + case IPropertySymbol _: + case IFieldSymbol _: + AddDebuggerDisplayAttributeArgumentsCore(member, builder); + break; } } + } - private void AddDebuggerDisplayAttributeArgumentsCore(ISymbol symbol, ArrayBuilder builder) + private void AddDebuggerDisplayAttributeArgumentsCore(ISymbol symbol, ArrayBuilder builder) + { + foreach (var attribute in symbol.GetAttributes()) { - foreach (var attribute in symbol.GetAttributes()) + if (attribute.AttributeClass == _debuggerDisplayAttributeType && + attribute.ConstructorArguments is [{ Kind: TypedConstantKind.Primitive, Type.SpecialType: SpecialType.System_String, Value: string value }]) { - if (attribute.AttributeClass == _debuggerDisplayAttributeType && - attribute.ConstructorArguments is [{ Kind: TypedConstantKind.Primitive, Type.SpecialType: SpecialType.System_String, Value: string value }]) - { - builder.Add(value); - } + builder.Add(value); } } + } - /// - /// Returns true if the given symbol meets the following criteria to be - /// a candidate for dead code analysis: - /// 1. It is marked as "private". - /// 2. It is not an implicitly declared symbol. - /// 3. It is either a method, field, property or an event. - /// 4. If method, then it is a constructor OR a method with , - /// such that is meets a few criteria (see implementation details below). - /// 5. If field, then it must not be a backing field for an auto property. - /// Backing fields have a non-null . - /// 6. If property, then it must not be an explicit interface property implementation - /// or the 'IsCompleted' property which is needed to make a type awaitable. - /// 7. If event, then it must not be an explicit interface event implementation. - /// - private bool IsCandidateSymbol([NotNullWhen(true)] ISymbol? memberSymbol) - { - if (memberSymbol is null) - return false; + /// + /// Returns true if the given symbol meets the following criteria to be + /// a candidate for dead code analysis: + /// 1. It is marked as "private". + /// 2. It is not an implicitly declared symbol. + /// 3. It is either a method, field, property or an event. + /// 4. If method, then it is a constructor OR a method with , + /// such that is meets a few criteria (see implementation details below). + /// 5. If field, then it must not be a backing field for an auto property. + /// Backing fields have a non-null . + /// 6. If property, then it must not be an explicit interface property implementation + /// or the 'IsCompleted' property which is needed to make a type awaitable. + /// 7. If event, then it must not be an explicit interface event implementation. + /// + private bool IsCandidateSymbol([NotNullWhen(true)] ISymbol? memberSymbol) + { + if (memberSymbol is null) + return false; - Debug.Assert(memberSymbol == memberSymbol.OriginalDefinition); + Debug.Assert(memberSymbol == memberSymbol.OriginalDefinition); - if (memberSymbol.DeclaredAccessibility == Accessibility.Private && - !memberSymbol.IsImplicitlyDeclared) + if (memberSymbol.DeclaredAccessibility == Accessibility.Private && + !memberSymbol.IsImplicitlyDeclared) + { + switch (memberSymbol.Kind) { - switch (memberSymbol.Kind) - { - case SymbolKind.Method: - var methodSymbol = (IMethodSymbol)memberSymbol; - switch (methodSymbol.MethodKind) - { - case MethodKind.Constructor: - // It is fine to have an unused private constructor - // without parameters. - // This is commonly used for static holder types - // that want to block instantiation of the type. - if (methodSymbol.Parameters.Length == 0) - { - return false; - } - - // Having a private copy constructor in a record means it's implicitly used by - // the record's clone method - if (methodSymbol.ContainingType.IsRecord && - methodSymbol.Parameters.Length == 1 && - methodSymbol.Parameters[0].RefKind == RefKind.None && - methodSymbol.Parameters[0].Type.Equals(memberSymbol.ContainingType)) - { - return false; - } - - // ISerializable constructor is invoked by the runtime for deserialization - // and it is a common pattern to have a private serialization constructor - // that is not explicitly referenced in code. - if (_deserializationConstructorCheck.IsDeserializationConstructor(methodSymbol)) - { - return false; - } - - return true; - - case MethodKind.Ordinary: - // Do not track accessors, as we will track/flag the associated symbol. - if (methodSymbol.AssociatedSymbol != null) - { - return false; - } - - // Do not flag unused entry point (Main) method. - if (methodSymbol.IsEntryPoint(_taskType, _genericTaskType)) - { - return false; - } - - // It is fine to have unused virtual/abstract/overrides/extern - // methods as they might be used in another type in the containing - // type's type hierarchy. - if (methodSymbol.IsAbstract || - methodSymbol.IsVirtual || - methodSymbol.IsOverride || - methodSymbol.IsExtern) - { - return false; - } - - // Explicit interface implementations are not referenced explicitly, - // but are still used. - if (!methodSymbol.ExplicitInterfaceImplementations.IsEmpty) - { - return false; - } - - // Ignore methods with special attributes that indicate special/reflection - // based access. - if (IsMethodWithSpecialAttribute(methodSymbol)) - { - return false; - } - - // ShouldSerializeXXX and ResetXXX are ok if there is a matching - // property XXX as they are used by the windows designer property grid - if (IsShouldSerializeOrResetPropertyMethod(methodSymbol)) - { - return false; - } - - // Ignore methods with event handler signature - // as lot of ASP.NET types have many special event handlers - // that are invoked with reflection (e.g. Application_XXX, Page_XXX, - // OnTransactionXXX, etc). - if (methodSymbol.HasEventHandlerSignature(_eventArgsType)) - { - return false; - } - - // Ignore methods which make a type awaitable. - if (_iNotifyCompletionType != null && Roslyn.Utilities.ImmutableArrayExtensions.Contains(methodSymbol.ContainingType.AllInterfaces, _iNotifyCompletionType, SymbolEqualityComparer.Default) - && methodSymbol.Name is "GetAwaiter" or "GetResult") - { - return false; - } - - return true; - - default: + case SymbolKind.Method: + var methodSymbol = (IMethodSymbol)memberSymbol; + switch (methodSymbol.MethodKind) + { + case MethodKind.Constructor: + // It is fine to have an unused private constructor + // without parameters. + // This is commonly used for static holder types + // that want to block instantiation of the type. + if (methodSymbol.Parameters.Length == 0) + { return false; - } + } + + // Having a private copy constructor in a record means it's implicitly used by + // the record's clone method + if (methodSymbol.ContainingType.IsRecord && + methodSymbol.Parameters.Length == 1 && + methodSymbol.Parameters[0].RefKind == RefKind.None && + methodSymbol.Parameters[0].Type.Equals(memberSymbol.ContainingType)) + { + return false; + } - case SymbolKind.Field: - return ((IFieldSymbol)memberSymbol).AssociatedSymbol == null; + // ISerializable constructor is invoked by the runtime for deserialization + // and it is a common pattern to have a private serialization constructor + // that is not explicitly referenced in code. + if (_deserializationConstructorCheck.IsDeserializationConstructor(methodSymbol)) + { + return false; + } - case SymbolKind.Property: - if (_iNotifyCompletionType != null && memberSymbol.ContainingType.AllInterfaces.Contains(_iNotifyCompletionType) && memberSymbol.Name == "IsCompleted") - { + return true; + + case MethodKind.Ordinary: + // Do not track accessors, as we will track/flag the associated symbol. + if (methodSymbol.AssociatedSymbol != null) + { + return false; + } + + // Do not flag unused entry point (Main) method. + if (methodSymbol.IsEntryPoint(_taskType, _genericTaskType)) + { + return false; + } + + // It is fine to have unused virtual/abstract/overrides/extern + // methods as they might be used in another type in the containing + // type's type hierarchy. + if (methodSymbol.IsAbstract || + methodSymbol.IsVirtual || + methodSymbol.IsOverride || + methodSymbol.IsExtern) + { + return false; + } + + // Explicit interface implementations are not referenced explicitly, + // but are still used. + if (!methodSymbol.ExplicitInterfaceImplementations.IsEmpty) + { + return false; + } + + // Ignore methods with special attributes that indicate special/reflection + // based access. + if (IsMethodWithSpecialAttribute(methodSymbol)) + { + return false; + } + + // ShouldSerializeXXX and ResetXXX are ok if there is a matching + // property XXX as they are used by the windows designer property grid + if (IsShouldSerializeOrResetPropertyMethod(methodSymbol)) + { + return false; + } + + // Ignore methods with event handler signature + // as lot of ASP.NET types have many special event handlers + // that are invoked with reflection (e.g. Application_XXX, Page_XXX, + // OnTransactionXXX, etc). + if (methodSymbol.HasEventHandlerSignature(_eventArgsType)) + { + return false; + } + + // Ignore methods which make a type awaitable. + if (_iNotifyCompletionType != null && Roslyn.Utilities.ImmutableArrayExtensions.Contains(methodSymbol.ContainingType.AllInterfaces, _iNotifyCompletionType, SymbolEqualityComparer.Default) + && methodSymbol.Name is "GetAwaiter" or "GetResult") + { + return false; + } + + return true; + + default: return false; - } + } - return ((IPropertySymbol)memberSymbol).ExplicitInterfaceImplementations.IsEmpty; + case SymbolKind.Field: + return ((IFieldSymbol)memberSymbol).AssociatedSymbol == null; - case SymbolKind.Event: - return ((IEventSymbol)memberSymbol).ExplicitInterfaceImplementations.IsEmpty; - } - } + case SymbolKind.Property: + if (_iNotifyCompletionType != null && memberSymbol.ContainingType.AllInterfaces.Contains(_iNotifyCompletionType) && memberSymbol.Name == "IsCompleted") + { + return false; + } - return false; + return ((IPropertySymbol)memberSymbol).ExplicitInterfaceImplementations.IsEmpty; + + case SymbolKind.Event: + return ((IEventSymbol)memberSymbol).ExplicitInterfaceImplementations.IsEmpty; + } } - private bool IsMethodWithSpecialAttribute(IMethodSymbol methodSymbol) - => methodSymbol.GetAttributes().Any(static (a, self) => self._attributeSetForMethodsToIgnore.Contains(a.AttributeClass), this); + return false; + } - private static bool IsShouldSerializeOrResetPropertyMethod(IMethodSymbol methodSymbol) - { - // "bool ShouldSerializeXXX()" and "void ResetXXX()" are ok if there is a matching - // property XXX as they are used by the windows designer property grid - // Note that we do a case sensitive compare for compatibility with legacy FxCop - // implementation of this rule. + private bool IsMethodWithSpecialAttribute(IMethodSymbol methodSymbol) + => methodSymbol.GetAttributes().Any(static (a, self) => self._attributeSetForMethodsToIgnore.Contains(a.AttributeClass), this); - return methodSymbol.Parameters.IsEmpty && - (IsSpecialMethodWithMatchingProperty("ShouldSerialize") && methodSymbol.ReturnType.SpecialType == SpecialType.System_Boolean || - IsSpecialMethodWithMatchingProperty("Reset") && methodSymbol.ReturnsVoid); + private static bool IsShouldSerializeOrResetPropertyMethod(IMethodSymbol methodSymbol) + { + // "bool ShouldSerializeXXX()" and "void ResetXXX()" are ok if there is a matching + // property XXX as they are used by the windows designer property grid + // Note that we do a case sensitive compare for compatibility with legacy FxCop + // implementation of this rule. - // Local functions. - bool IsSpecialMethodWithMatchingProperty(string prefix) - { - if (methodSymbol.Name.StartsWith(prefix)) - { - var suffix = methodSymbol.Name[prefix.Length..]; - return suffix.Length > 0 && - methodSymbol.ContainingType.GetMembers(suffix).Any(static m => m is IPropertySymbol); - } + return methodSymbol.Parameters.IsEmpty && + (IsSpecialMethodWithMatchingProperty("ShouldSerialize") && methodSymbol.ReturnType.SpecialType == SpecialType.System_Boolean || + IsSpecialMethodWithMatchingProperty("Reset") && methodSymbol.ReturnsVoid); - return false; + // Local functions. + bool IsSpecialMethodWithMatchingProperty(string prefix) + { + if (methodSymbol.Name.StartsWith(prefix)) + { + var suffix = methodSymbol.Name[prefix.Length..]; + return suffix.Length > 0 && + methodSymbol.ContainingType.GetMembers(suffix).Any(static m => m is IPropertySymbol); } + + return false; } } } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.BlockAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.BlockAnalyzer.cs index ef835ea2d231a..ee5c031247f99 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.BlockAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.BlockAnalyzer.cs @@ -16,762 +16,763 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues +namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues; + +internal abstract partial class AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer { - internal abstract partial class AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + private sealed partial class SymbolStartAnalyzer { - private sealed partial class SymbolStartAnalyzer + private sealed partial class BlockAnalyzer { - private sealed partial class BlockAnalyzer + private readonly SymbolStartAnalyzer _symbolStartAnalyzer; + private readonly Options _options; + + /// + /// Indicates if the operation block has an or an . + /// We use this value in to determine whether to bail from analysis or not. + /// + private bool _hasDelegateCreationOrAnonymousFunction; + + /// + /// Indicates if the operation block has an operation that leads to a delegate escaping the current block, + /// which would prevent us from performing accurate flow analysis of lambda/local function invocations + /// within this operation block. + /// Some examples: + /// 1. Delegate assigned to a field or property. + /// 2. Delegate passed as an argument to an invocation or object creation. + /// 3. Delegate added to an array or wrapped within a tuple. + /// 4. Delegate converted to a non-delegate type. + /// We use this value in to determine whether to bail from analysis or not. + /// + private bool _hasDelegateEscape; + + /// + /// Indicates if the operation block has an . + /// We use this value in to determine whether to bail from analysis or not. + /// + private bool _hasInvalidOperation; + + /// + /// Parameters which have at least one read/write reference. + /// + private readonly ConcurrentDictionary _referencedParameters; + + private BlockAnalyzer(SymbolStartAnalyzer symbolStartAnalyzer, Options options) { - private readonly SymbolStartAnalyzer _symbolStartAnalyzer; - private readonly Options _options; - - /// - /// Indicates if the operation block has an or an . - /// We use this value in to determine whether to bail from analysis or not. - /// - private bool _hasDelegateCreationOrAnonymousFunction; - - /// - /// Indicates if the operation block has an operation that leads to a delegate escaping the current block, - /// which would prevent us from performing accurate flow analysis of lambda/local function invocations - /// within this operation block. - /// Some examples: - /// 1. Delegate assigned to a field or property. - /// 2. Delegate passed as an argument to an invocation or object creation. - /// 3. Delegate added to an array or wrapped within a tuple. - /// 4. Delegate converted to a non-delegate type. - /// We use this value in to determine whether to bail from analysis or not. - /// - private bool _hasDelegateEscape; - - /// - /// Indicates if the operation block has an . - /// We use this value in to determine whether to bail from analysis or not. - /// - private bool _hasInvalidOperation; - - /// - /// Parameters which have at least one read/write reference. - /// - private readonly ConcurrentDictionary _referencedParameters; - - private BlockAnalyzer(SymbolStartAnalyzer symbolStartAnalyzer, Options options) - { - _symbolStartAnalyzer = symbolStartAnalyzer; - _options = options; - _referencedParameters = []; - } - - public static void Analyze(OperationBlockStartAnalysisContext context, SymbolStartAnalyzer symbolStartAnalyzer) - { - if (!ShouldAnalyze(context, symbolStartAnalyzer, out var options)) - return; - - var blockAnalyzer = new BlockAnalyzer(symbolStartAnalyzer, options); - context.RegisterOperationAction(blockAnalyzer.AnalyzeExpressionStatement, OperationKind.ExpressionStatement); - context.RegisterOperationAction(blockAnalyzer.AnalyzeDelegateCreationOrAnonymousFunction, OperationKind.DelegateCreation, OperationKind.AnonymousFunction); - context.RegisterOperationAction(blockAnalyzer.AnalyzeLocalOrParameterReference, OperationKind.LocalReference, OperationKind.ParameterReference); - context.RegisterOperationAction(_ => blockAnalyzer._hasInvalidOperation = true, OperationKind.Invalid); - context.RegisterOperationBlockEndAction(blockAnalyzer.AnalyzeOperationBlockEnd); - return; - - // Local Functions. - bool ShouldAnalyze( - OperationBlockStartAnalysisContext context, - SymbolStartAnalyzer symbolStartAnalyzer, - [NotNullWhen(true)] out Options? options) - { - options = null; - if (HasSyntaxErrors() || context.OperationBlocks.IsEmpty) - return false; + _symbolStartAnalyzer = symbolStartAnalyzer; + _options = options; + _referencedParameters = []; + } - // Bail out in presence of conditional directives - // This is a workaround for https://github.com/dotnet/roslyn/issues/31820 - // Issue https://github.com/dotnet/roslyn/issues/31821 tracks - // reverting this workaround. - if (HasConditionalDirectives()) - return false; + public static void Analyze(OperationBlockStartAnalysisContext context, SymbolStartAnalyzer symbolStartAnalyzer) + { + if (!ShouldAnalyze(context, symbolStartAnalyzer, out var options)) + return; - // All operation blocks for a symbol belong to the same tree. - var firstBlock = context.OperationBlocks[0]; - if (!symbolStartAnalyzer._compilationAnalyzer.TryGetOptions(firstBlock.Syntax.SyntaxTree, - context.Options, - context.Compilation.Options, - context.CancellationToken, - out options)) - { - return false; - } + var blockAnalyzer = new BlockAnalyzer(symbolStartAnalyzer, options); + context.RegisterOperationAction(blockAnalyzer.AnalyzeExpressionStatement, OperationKind.ExpressionStatement); + context.RegisterOperationAction(blockAnalyzer.AnalyzeDelegateCreationOrAnonymousFunction, OperationKind.DelegateCreation, OperationKind.AnonymousFunction); + context.RegisterOperationAction(blockAnalyzer.AnalyzeLocalOrParameterReference, OperationKind.LocalReference, OperationKind.ParameterReference); + context.RegisterOperationAction(_ => blockAnalyzer._hasInvalidOperation = true, OperationKind.Invalid); + context.RegisterOperationBlockEndAction(blockAnalyzer.AnalyzeOperationBlockEnd); + return; + + // Local Functions. + bool ShouldAnalyze( + OperationBlockStartAnalysisContext context, + SymbolStartAnalyzer symbolStartAnalyzer, + [NotNullWhen(true)] out Options? options) + { + options = null; + if (HasSyntaxErrors() || context.OperationBlocks.IsEmpty) + return false; - // Ignore methods that are just a single-throw method. These are often - // in-progress pieces of work and we don't want to force the user to fixup other - // issues before they've even gotten around to writing their code. - if (firstBlock.IsSingleThrowNotImplementedOperation()) - return false; + // Bail out in presence of conditional directives + // This is a workaround for https://github.com/dotnet/roslyn/issues/31820 + // Issue https://github.com/dotnet/roslyn/issues/31821 tracks + // reverting this workaround. + if (HasConditionalDirectives()) + return false; - // If we are analyzing a specific filter tree, skip operation blocks in unrelated trees. - if (symbolStartAnalyzer._symbolStartAnalysisContext.FilterTree is { } filterTree && - firstBlock.Syntax.SyntaxTree != filterTree) - { - return false; - } + // All operation blocks for a symbol belong to the same tree. + var firstBlock = context.OperationBlocks[0]; + if (!symbolStartAnalyzer._compilationAnalyzer.TryGetOptions(firstBlock.Syntax.SyntaxTree, + context.Options, + context.Compilation.Options, + context.CancellationToken, + out options)) + { + return false; + } - // If we are analyzing a specific filter span, skip operation blocks outside the filter span. - if (context.FilterSpan.HasValue) - { - Contract.ThrowIfFalse(context.FilterSpan != symbolStartAnalyzer._symbolStartAnalysisContext.FilterSpan); - Contract.ThrowIfNull(symbolStartAnalyzer._symbolStartAnalysisContext.FilterTree); - var root = firstBlock.Syntax.SyntaxTree.GetRoot(context.CancellationToken); - var spanStart = firstBlock.Syntax.SpanStart; - var memberDecl = symbolStartAnalyzer._compilationAnalyzer.SyntaxFacts.GetContainingMemberDeclaration(root, spanStart, useFullSpan: false); - if (memberDecl != null && !context.ShouldAnalyzeSpan(memberDecl.Span)) - return false; - } + // Ignore methods that are just a single-throw method. These are often + // in-progress pieces of work and we don't want to force the user to fixup other + // issues before they've even gotten around to writing their code. + if (firstBlock.IsSingleThrowNotImplementedOperation()) + return false; - return true; + // If we are analyzing a specific filter tree, skip operation blocks in unrelated trees. + if (symbolStartAnalyzer._symbolStartAnalysisContext.FilterTree is { } filterTree && + firstBlock.Syntax.SyntaxTree != filterTree) + { + return false; } - bool HasSyntaxErrors() + // If we are analyzing a specific filter span, skip operation blocks outside the filter span. + if (context.FilterSpan.HasValue) { - foreach (var operationBlock in context.OperationBlocks) - { - if (operationBlock.Syntax.GetDiagnostics().ToImmutableArrayOrEmpty().HasAnyErrors()) - return true; - } + Contract.ThrowIfFalse(context.FilterSpan != symbolStartAnalyzer._symbolStartAnalysisContext.FilterSpan); + Contract.ThrowIfNull(symbolStartAnalyzer._symbolStartAnalysisContext.FilterTree); + var root = firstBlock.Syntax.SyntaxTree.GetRoot(context.CancellationToken); + var spanStart = firstBlock.Syntax.SpanStart; + var memberDecl = symbolStartAnalyzer._compilationAnalyzer.SyntaxFacts.GetContainingMemberDeclaration(root, spanStart, useFullSpan: false); + if (memberDecl != null && !context.ShouldAnalyzeSpan(memberDecl.Span)) + return false; + } - return false; + return true; + } + + bool HasSyntaxErrors() + { + foreach (var operationBlock in context.OperationBlocks) + { + if (operationBlock.Syntax.GetDiagnostics().ToImmutableArrayOrEmpty().HasAnyErrors()) + return true; } - bool HasConditionalDirectives() + return false; + } + + bool HasConditionalDirectives() + { + foreach (var operationBlock in context.OperationBlocks) { - foreach (var operationBlock in context.OperationBlocks) + if (operationBlock.Syntax.DescendantNodes(descendIntoTrivia: true) + .Any(symbolStartAnalyzer._compilationAnalyzer.IsIfConditionalDirective)) { - if (operationBlock.Syntax.DescendantNodes(descendIntoTrivia: true) - .Any(symbolStartAnalyzer._compilationAnalyzer.IsIfConditionalDirective)) - { - return true; - } + return true; } - - return false; } + + return false; } + } - private void AnalyzeExpressionStatement(OperationAnalysisContext context) + private void AnalyzeExpressionStatement(OperationAnalysisContext context) + { + if (_options.UnusedValueExpressionStatementNotification.Severity == ReportDiagnostic.Suppress) { - if (_options.UnusedValueExpressionStatementNotification.Severity == ReportDiagnostic.Suppress) - { - return; - } + return; + } - var expressionStatement = (IExpressionStatementOperation)context.Operation; - var value = expressionStatement.Operation; + var expressionStatement = (IExpressionStatementOperation)context.Operation; + var value = expressionStatement.Operation; - // Bail out cases for report unused expression value: + // Bail out cases for report unused expression value: - // 1. Null type, error type and void returning method invocations: no value being dropped here. - if (value.Type == null || - value.Type.IsErrorType() || - value.Type.SpecialType == SpecialType.System_Void) - { - return; - } + // 1. Null type, error type and void returning method invocations: no value being dropped here. + if (value.Type == null || + value.Type.IsErrorType() || + value.Type.SpecialType == SpecialType.System_Void) + { + return; + } - // 2. Bail out if the return type is dynamic as it could actually be void returning, and throw at runtime - if (value.Type.TypeKind == TypeKind.Dynamic) - { - return; - } + // 2. Bail out if the return type is dynamic as it could actually be void returning, and throw at runtime + if (value.Type.TypeKind == TypeKind.Dynamic) + { + return; + } - // 3. Bail out for semantic error (invalid operation) cases. - // Also bail out for constant expressions in expression statement syntax, say as "1;", - // which do not seem to have an invalid operation in the operation tree. - if (value is IInvalidOperation || - value.ConstantValue.HasValue) - { - return; - } + // 3. Bail out for semantic error (invalid operation) cases. + // Also bail out for constant expressions in expression statement syntax, say as "1;", + // which do not seem to have an invalid operation in the operation tree. + if (value is IInvalidOperation || + value.ConstantValue.HasValue) + { + return; + } - // 4. Assignments, increment/decrement operations: value is actually being assigned. - if (value is IAssignmentOperation or - IIncrementOrDecrementOperation) - { - return; - } + // 4. Assignments, increment/decrement operations: value is actually being assigned. + if (value is IAssignmentOperation or + IIncrementOrDecrementOperation) + { + return; + } - // 5. Bail out if there is language specific syntax to indicate an explicit discard. - // For example, VB call statement is used to explicitly ignore the value returned by - // an invocation by prefixing the invocation with keyword "Call". - // Similarly, we do not want to flag an expression of a C# expression body. - if (_symbolStartAnalyzer._compilationAnalyzer.IsCallStatement(expressionStatement) || - _symbolStartAnalyzer._compilationAnalyzer.IsExpressionOfExpressionBody(expressionStatement)) - { - return; - } + // 5. Bail out if there is language specific syntax to indicate an explicit discard. + // For example, VB call statement is used to explicitly ignore the value returned by + // an invocation by prefixing the invocation with keyword "Call". + // Similarly, we do not want to flag an expression of a C# expression body. + if (_symbolStartAnalyzer._compilationAnalyzer.IsCallStatement(expressionStatement) || + _symbolStartAnalyzer._compilationAnalyzer.IsExpressionOfExpressionBody(expressionStatement)) + { + return; + } - var properties = s_propertiesMap[(_options.UnusedValueExpressionStatementPreference, isUnusedLocalAssignment: false, isRemovableAssignment: false)]; - var diagnostic = DiagnosticHelper.Create(s_expressionValueIsUnusedRule, - value.Syntax.GetLocation(), - _options.UnusedValueExpressionStatementNotification, - additionalLocations: null, - properties); - context.ReportDiagnostic(diagnostic); + var properties = s_propertiesMap[(_options.UnusedValueExpressionStatementPreference, isUnusedLocalAssignment: false, isRemovableAssignment: false)]; + var diagnostic = DiagnosticHelper.Create(s_expressionValueIsUnusedRule, + value.Syntax.GetLocation(), + _options.UnusedValueExpressionStatementNotification, + context.Options, + additionalLocations: null, + properties); + context.ReportDiagnostic(diagnostic); + } + + private void AnalyzeDelegateCreationOrAnonymousFunction(OperationAnalysisContext operationAnalysisContext) + { + _hasDelegateCreationOrAnonymousFunction = true; + if (!_hasDelegateEscape) + { + _hasDelegateEscape = !IsHandledDelegateCreationOrAnonymousFunctionTreeShape(operationAnalysisContext.Operation); } + } - private void AnalyzeDelegateCreationOrAnonymousFunction(OperationAnalysisContext operationAnalysisContext) + private void AnalyzeLocalOrParameterReference(OperationAnalysisContext operationAnalysisContext) + { + if (operationAnalysisContext.Operation is IParameterReferenceOperation parameterReference) { - _hasDelegateCreationOrAnonymousFunction = true; - if (!_hasDelegateEscape) - { - _hasDelegateEscape = !IsHandledDelegateCreationOrAnonymousFunctionTreeShape(operationAnalysisContext.Operation); - } + _referencedParameters.GetOrAdd(parameterReference.Parameter, true); } - private void AnalyzeLocalOrParameterReference(OperationAnalysisContext operationAnalysisContext) + if (!_hasDelegateEscape) { - if (operationAnalysisContext.Operation is IParameterReferenceOperation parameterReference) - { - _referencedParameters.GetOrAdd(parameterReference.Parameter, true); - } + _hasDelegateEscape = !IsHandledLocalOrParameterReferenceTreeShape(operationAnalysisContext.Operation); + } + } - if (!_hasDelegateEscape) - { - _hasDelegateEscape = !IsHandledLocalOrParameterReferenceTreeShape(operationAnalysisContext.Operation); - } + /// + /// We handle only certain operation tree shapes in flow analysis + /// when delegate creations are involved (lambdas/local functions). + /// We track assignments of lambdas/local functions to parameters/locals, + /// assignments of parameters/locals to other parameters/locals of delegate types, + /// and then delegate invocations through parameter/locals. + /// For the remaining unknown ones, we conservatively mark the operation as leading to + /// delegate escape, and corresponding bail out from flow analysis in . + /// This function checks the operation tree shape in context of + /// an or an . + /// + private static bool IsHandledDelegateCreationOrAnonymousFunctionTreeShape(IOperation operation) + { + Debug.Assert(operation.Kind is OperationKind.DelegateCreation or OperationKind.AnonymousFunction); + + // 1. Delegate creation or anonymous function variable initializer is handled. + // For example, for 'Action a = () => { ... };', the lambda is the variable initializer + // and we track that 'a' points to this lambda during flow analysis + // and analyze lambda body at invocation sites 'a();' + if (operation.Parent is IVariableInitializerOperation) + { + return true; } - /// - /// We handle only certain operation tree shapes in flow analysis - /// when delegate creations are involved (lambdas/local functions). - /// We track assignments of lambdas/local functions to parameters/locals, - /// assignments of parameters/locals to other parameters/locals of delegate types, - /// and then delegate invocations through parameter/locals. - /// For the remaining unknown ones, we conservatively mark the operation as leading to - /// delegate escape, and corresponding bail out from flow analysis in . - /// This function checks the operation tree shape in context of - /// an or an . - /// - private static bool IsHandledDelegateCreationOrAnonymousFunctionTreeShape(IOperation operation) - { - Debug.Assert(operation.Kind is OperationKind.DelegateCreation or OperationKind.AnonymousFunction); - - // 1. Delegate creation or anonymous function variable initializer is handled. - // For example, for 'Action a = () => { ... };', the lambda is the variable initializer - // and we track that 'a' points to this lambda during flow analysis - // and analyze lambda body at invocation sites 'a();' - if (operation.Parent is IVariableInitializerOperation) - { - return true; - } + // 2. Delegate creation or anonymous function assigned to a local or parameter are handled. + // For example, for 'Action a; a = () => { ... };', the lambda is assigned to local 'a' + // and we track that 'a' points to this lambda during flow analysis + // and analyze lambda body at invocation sites 'a();' + if (operation.Parent is ISimpleAssignmentOperation assignment && + (assignment.Target.Kind == OperationKind.LocalReference || + assignment.Target.Kind == OperationKind.ParameterReference)) + { + return true; + } - // 2. Delegate creation or anonymous function assigned to a local or parameter are handled. - // For example, for 'Action a; a = () => { ... };', the lambda is assigned to local 'a' - // and we track that 'a' points to this lambda during flow analysis - // and analyze lambda body at invocation sites 'a();' - if (operation.Parent is ISimpleAssignmentOperation assignment && - (assignment.Target.Kind == OperationKind.LocalReference || - assignment.Target.Kind == OperationKind.ParameterReference)) - { - return true; - } + // 3. For anonymous functions parented by delegate creation, we analyze the parent operation. + // For example, for 'Action a = () => { ... };', the lambda generates an anonymous function + // operation parented by a delegate creation. + if (operation.Kind == OperationKind.AnonymousFunction && + operation.Parent is IDelegateCreationOperation) + { + return IsHandledDelegateCreationOrAnonymousFunctionTreeShape(operation.Parent); + } - // 3. For anonymous functions parented by delegate creation, we analyze the parent operation. - // For example, for 'Action a = () => { ... };', the lambda generates an anonymous function - // operation parented by a delegate creation. - if (operation.Kind == OperationKind.AnonymousFunction && - operation.Parent is IDelegateCreationOperation) - { - return IsHandledDelegateCreationOrAnonymousFunctionTreeShape(operation.Parent); - } + // 4. Otherwise, conservatively consider this as an unhandled delegate escape. + return false; + } - // 4. Otherwise, conservatively consider this as an unhandled delegate escape. - return false; + /// + /// We handle only certain operation tree shapes in flow analysis + /// when delegate creations are involved (lambdas/local functions). + /// We track assignments of lambdas/local functions to parameters/locals, + /// assignments of parameters/locals to other parameters/locals of delegate types, + /// and then delegate invocations through parameter/locals. + /// For the remaining unknown ones, we conservatively mark the operation as leading to + /// delegate escape, and corresponding bail out from flow analysis in . + /// This function checks the operation tree shape in context of + /// an or an + /// of delegate type. + /// + private static bool IsHandledLocalOrParameterReferenceTreeShape(IOperation operation) + { + Debug.Assert(operation.Kind is OperationKind.LocalReference or OperationKind.ParameterReference); + + // 1. We are only interested in parameters or locals of delegate type. + if (!operation.Type.IsDelegateType()) + { + return true; } - /// - /// We handle only certain operation tree shapes in flow analysis - /// when delegate creations are involved (lambdas/local functions). - /// We track assignments of lambdas/local functions to parameters/locals, - /// assignments of parameters/locals to other parameters/locals of delegate types, - /// and then delegate invocations through parameter/locals. - /// For the remaining unknown ones, we conservatively mark the operation as leading to - /// delegate escape, and corresponding bail out from flow analysis in . - /// This function checks the operation tree shape in context of - /// an or an - /// of delegate type. - /// - private static bool IsHandledLocalOrParameterReferenceTreeShape(IOperation operation) - { - Debug.Assert(operation.Kind is OperationKind.LocalReference or OperationKind.ParameterReference); - - // 1. We are only interested in parameters or locals of delegate type. - if (!operation.Type.IsDelegateType()) - { - return true; - } + // 2. Delegate invocations are handled. + // For example, for 'Action a = () => { ... }; a();' + // we track that 'a' points to the lambda during flow analysis + // and analyze lambda body at invocation sites 'a();' + if (operation.Parent is IInvocationOperation) + { + return true; + } - // 2. Delegate invocations are handled. - // For example, for 'Action a = () => { ... }; a();' - // we track that 'a' points to the lambda during flow analysis - // and analyze lambda body at invocation sites 'a();' - if (operation.Parent is IInvocationOperation) + if (operation.Parent is ISimpleAssignmentOperation assignmentOperation) + { + // 3. Parameter/local as target of an assignment is handled. + // For example, for 'a = () => { ... }; a();' + // assignment of a lambda to a local/parameter 'a' is tracked during flow analysis + // and we analyze lambda body at invocation sites 'a();' + if (assignmentOperation.Target == operation) { return true; } - if (operation.Parent is ISimpleAssignmentOperation assignmentOperation) - { - // 3. Parameter/local as target of an assignment is handled. - // For example, for 'a = () => { ... }; a();' - // assignment of a lambda to a local/parameter 'a' is tracked during flow analysis - // and we analyze lambda body at invocation sites 'a();' - if (assignmentOperation.Target == operation) - { - return true; - } - - // 4. Assignment from a parameter or local is only handled if being - // assigned to some parameter or local of delegate type. - // For example, for 'a = () => { ... }; b = a; b();' - // assignment of a local/parameter 'b = a' is tracked during flow analysis - // and we analyze lambda body at invocation sites 'b();' - if (assignmentOperation.Target.Type.IsDelegateType() && - (assignmentOperation.Target.Kind == OperationKind.LocalReference || - assignmentOperation.Target.Kind == OperationKind.ParameterReference)) - { - return true; - } - } - - // 5. Binary operations on parameter/local are fine. - // For example, 'a = () => { ... }; if (a != null) { a(); }' - // the binary operation 'a != null' is fine and does not lead - // to a delegate escape. - if (operation.Parent is IBinaryOperation) + // 4. Assignment from a parameter or local is only handled if being + // assigned to some parameter or local of delegate type. + // For example, for 'a = () => { ... }; b = a; b();' + // assignment of a local/parameter 'b = a' is tracked during flow analysis + // and we analyze lambda body at invocation sites 'b();' + if (assignmentOperation.Target.Type.IsDelegateType() && + (assignmentOperation.Target.Kind == OperationKind.LocalReference || + assignmentOperation.Target.Kind == OperationKind.ParameterReference)) { return true; } + } - // 6. Otherwise, conservatively consider this as an unhandled delegate escape. - return false; + // 5. Binary operations on parameter/local are fine. + // For example, 'a = () => { ... }; if (a != null) { a(); }' + // the binary operation 'a != null' is fine and does not lead + // to a delegate escape. + if (operation.Parent is IBinaryOperation) + { + return true; } - /// - /// Method invoked in - /// for each operation block to determine if we should analyze the operation block or bail out. - /// - private bool ShouldAnalyze(IOperation operationBlock, ISymbol owningSymbol, ref bool hasUnknownOperationNoneDescendant) + // 6. Otherwise, conservatively consider this as an unhandled delegate escape. + return false; + } + + /// + /// Method invoked in + /// for each operation block to determine if we should analyze the operation block or bail out. + /// + private bool ShouldAnalyze(IOperation operationBlock, ISymbol owningSymbol, ref bool hasUnknownOperationNoneDescendant) + { + switch (operationBlock.Kind) { - switch (operationBlock.Kind) - { - case OperationKind.Attribute: - case OperationKind.ParameterInitializer: - // Skip blocks from attributes and parameter initializers. - // We don't have any unused values in such operation blocks. - return false; + case OperationKind.Attribute: + case OperationKind.ParameterInitializer: + // Skip blocks from attributes and parameter initializers. + // We don't have any unused values in such operation blocks. + return false; - default: - foreach (var operation in operationBlock.Descendants()) + default: + foreach (var operation in operationBlock.Descendants()) + { + switch (operation) { - switch (operation) - { - // Workaround for https://github.com/dotnet/roslyn/issues/31007 - // We cannot perform flow analysis correctly for a ref assignment operation or ref conditional operation until this compiler feature is implemented. - case IConditionalOperation conditional when conditional.IsRef: - case ISimpleAssignmentOperation assignment when assignment.IsRef: + // Workaround for https://github.com/dotnet/roslyn/issues/31007 + // We cannot perform flow analysis correctly for a ref assignment operation or ref conditional operation until this compiler feature is implemented. + case IConditionalOperation conditional when conditional.IsRef: + case ISimpleAssignmentOperation assignment when assignment.IsRef: + return false; + + default: + // Workaround for https://github.com/dotnet/roslyn/issues/27564 + // Bail out in presence of OperationKind.None - not implemented IOperation. + if (operation.Kind == OperationKind.None) + { + // `nameof(SomeTypeName)` is a well-known case where operation related to `SomeTypeName` syntax is of kind `None` + hasUnknownOperationNoneDescendant = operation.Parent is not INameOfOperation; return false; + } - default: - // Workaround for https://github.com/dotnet/roslyn/issues/27564 - // Bail out in presence of OperationKind.None - not implemented IOperation. - if (operation.Kind == OperationKind.None) - { - // `nameof(SomeTypeName)` is a well-known case where operation related to `SomeTypeName` syntax is of kind `None` - hasUnknownOperationNoneDescendant = operation.Parent is not INameOfOperation; - return false; - } - - break; - } + break; } + } - break; - } + break; + } - // We currently do not support points-to analysis, which is needed to accurately track locations of - // allocated objects and their aliasing, which enables us to determine if two symbols reference the - // same object instance at a given program point and also enables us to track the set of runtime objects - // that a variable can point to. - // Hence, we cannot accurately track the exact set of delegates that a symbol with delegate type - // can point to for all control flow cases. - // We attempt to do our best effort delegate invocation analysis as follows: + // We currently do not support points-to analysis, which is needed to accurately track locations of + // allocated objects and their aliasing, which enables us to determine if two symbols reference the + // same object instance at a given program point and also enables us to track the set of runtime objects + // that a variable can point to. + // Hence, we cannot accurately track the exact set of delegates that a symbol with delegate type + // can point to for all control flow cases. + // We attempt to do our best effort delegate invocation analysis as follows: + + // 1. If we have no delegate creations or lambdas, our current analysis works fine, + // return true. + if (!_hasDelegateCreationOrAnonymousFunction) + { + return true; + } - // 1. If we have no delegate creations or lambdas, our current analysis works fine, - // return true. - if (!_hasDelegateCreationOrAnonymousFunction) - { - return true; - } + // 2. Bail out if we have a delegate escape via operation tree shapes that we do not understand. + // This indicates the delegate targets (such as lambda/local functions) have escaped current method + // and can be invoked from a separate method, and these invocations can read values written + // to any local/parameter in the current method. We cannot reliably flag any write to a + // local/parameter as unused for such cases. + if (_hasDelegateEscape) + { + return false; + } - // 2. Bail out if we have a delegate escape via operation tree shapes that we do not understand. - // This indicates the delegate targets (such as lambda/local functions) have escaped current method - // and can be invoked from a separate method, and these invocations can read values written - // to any local/parameter in the current method. We cannot reliably flag any write to a - // local/parameter as unused for such cases. - if (_hasDelegateEscape) - { - return false; - } + // 3. Bail out for method returning delegates or ref/out parameters of delegate type. + // We can analyze this correctly when we do points-to-analysis. + if (owningSymbol is IMethodSymbol method && + (method.ReturnType.IsDelegateType() || + method.Parameters.Any(static p => p.IsRefOrOut() && p.Type.IsDelegateType()))) + { + return false; + } - // 3. Bail out for method returning delegates or ref/out parameters of delegate type. - // We can analyze this correctly when we do points-to-analysis. - if (owningSymbol is IMethodSymbol method && - (method.ReturnType.IsDelegateType() || - method.Parameters.Any(static p => p.IsRefOrOut() && p.Type.IsDelegateType()))) - { - return false; - } + // 4. Bail out on invalid operations, i.e. code with semantic errors. + // We are likely to have false positives from flow analysis results + // as we will not account for potential lambda/local function invocations. + if (_hasInvalidOperation) + { + return false; + } - // 4. Bail out on invalid operations, i.e. code with semantic errors. - // We are likely to have false positives from flow analysis results - // as we will not account for potential lambda/local function invocations. - if (_hasInvalidOperation) - { - return false; - } + // 5. Otherwise, we execute analysis by walking the reaching symbol write chain to attempt to + // find the target method being invoked. + // This works for most common and simple cases where a local is assigned a lambda and invoked later. + // If we are unable to find a target, we will conservatively mark all current symbol writes as read. + return true; + } - // 5. Otherwise, we execute analysis by walking the reaching symbol write chain to attempt to - // find the target method being invoked. - // This works for most common and simple cases where a local is assigned a lambda and invoked later. - // If we are unable to find a target, we will conservatively mark all current symbol writes as read. - return true; + private void AnalyzeOperationBlockEnd(OperationBlockAnalysisContext context) + { + // Bail out if we are neither computing unused parameters nor unused value assignments. + var isComputingUnusedParams = _options.IsComputingUnusedParams(context.OwningSymbol); + if (_options.UnusedValueAssignmentSeverity.Severity == ReportDiagnostic.Suppress && + !isComputingUnusedParams) + { + return; } - private void AnalyzeOperationBlockEnd(OperationBlockAnalysisContext context) - { - // Bail out if we are neither computing unused parameters nor unused value assignments. - var isComputingUnusedParams = _options.IsComputingUnusedParams(context.OwningSymbol); - if (_options.UnusedValueAssignmentSeverity.Severity == ReportDiagnostic.Suppress && - !isComputingUnusedParams) - { - return; - } + // We perform analysis to compute unused parameters and value assignments in two passes. + // Unused value assignments can be identified by analyzing each operation block independently in the first pass. + // However, to identify unused parameters we need to first analyze all operation blocks and then iterate + // through the parameters to identify unused ones - // We perform analysis to compute unused parameters and value assignments in two passes. - // Unused value assignments can be identified by analyzing each operation block independently in the first pass. - // However, to identify unused parameters we need to first analyze all operation blocks and then iterate - // through the parameters to identify unused ones + // Builder to store the symbol read/write usage result for each operation block computed during the first pass. + // These are later used to compute unused parameters in second pass. + using var _ = PooledHashSet.GetInstance(out var symbolUsageResultsBuilder); - // Builder to store the symbol read/write usage result for each operation block computed during the first pass. - // These are later used to compute unused parameters in second pass. - using var _ = PooledHashSet.GetInstance(out var symbolUsageResultsBuilder); + // Flag indicating if we found an operation block where all symbol writes were used. + AnalyzeUnusedValueAssignments(context, isComputingUnusedParams, symbolUsageResultsBuilder, out var hasBlockWithAllUsedWrites, out var hasUnknownOperationNoneDescendant); - // Flag indicating if we found an operation block where all symbol writes were used. - AnalyzeUnusedValueAssignments(context, isComputingUnusedParams, symbolUsageResultsBuilder, out var hasBlockWithAllUsedWrites, out var hasUnknownOperationNoneDescendant); + AnalyzeUnusedParameters(context, isComputingUnusedParams, symbolUsageResultsBuilder, hasBlockWithAllUsedWrites, hasUnknownOperationNoneDescendant); + } - AnalyzeUnusedParameters(context, isComputingUnusedParams, symbolUsageResultsBuilder, hasBlockWithAllUsedWrites, hasUnknownOperationNoneDescendant); - } + private void AnalyzeUnusedValueAssignments( + OperationBlockAnalysisContext context, + bool isComputingUnusedParams, + PooledHashSet symbolUsageResultsBuilder, + out bool hasBlockWithAllUsedSymbolWrites, + out bool hasUnknownOperationNoneDescendant) + { + hasBlockWithAllUsedSymbolWrites = false; + hasUnknownOperationNoneDescendant = false; - private void AnalyzeUnusedValueAssignments( - OperationBlockAnalysisContext context, - bool isComputingUnusedParams, - PooledHashSet symbolUsageResultsBuilder, - out bool hasBlockWithAllUsedSymbolWrites, - out bool hasUnknownOperationNoneDescendant) + foreach (var operationBlock in context.OperationBlocks) { - hasBlockWithAllUsedSymbolWrites = false; - hasUnknownOperationNoneDescendant = false; - - foreach (var operationBlock in context.OperationBlocks) + if (!ShouldAnalyze(operationBlock, context.OwningSymbol, ref hasUnknownOperationNoneDescendant)) { - if (!ShouldAnalyze(operationBlock, context.OwningSymbol, ref hasUnknownOperationNoneDescendant)) - { - continue; - } + continue; + } - // First perform the fast, aggressive, imprecise operation-tree based analysis. - // This analysis might flag some "used" symbol writes as "unused", but will not miss reporting any truly unused symbol writes. - // This initial pass helps us reduce the number of methods for which we perform the slower second pass. - // We perform the first fast pass only if there are no delegate creations/lambda methods. - // This is due to the fact that tracking which local/parameter points to which delegate creation target - // at any given program point needs needs flow analysis (second pass). - if (!_hasDelegateCreationOrAnonymousFunction) + // First perform the fast, aggressive, imprecise operation-tree based analysis. + // This analysis might flag some "used" symbol writes as "unused", but will not miss reporting any truly unused symbol writes. + // This initial pass helps us reduce the number of methods for which we perform the slower second pass. + // We perform the first fast pass only if there are no delegate creations/lambda methods. + // This is due to the fact that tracking which local/parameter points to which delegate creation target + // at any given program point needs needs flow analysis (second pass). + if (!_hasDelegateCreationOrAnonymousFunction) + { + var resultFromOperationBlockAnalysis = SymbolUsageAnalysis.Run(operationBlock, context.OwningSymbol, context.CancellationToken); + if (!resultFromOperationBlockAnalysis.HasUnreadSymbolWrites()) { - var resultFromOperationBlockAnalysis = SymbolUsageAnalysis.Run(operationBlock, context.OwningSymbol, context.CancellationToken); - if (!resultFromOperationBlockAnalysis.HasUnreadSymbolWrites()) - { - // Assert that even slow pass (dataflow analysis) would have yielded no unused symbol writes. - Debug.Assert(!SymbolUsageAnalysis.Run(context.GetControlFlowGraph(operationBlock), context.OwningSymbol, context.CancellationToken) - .HasUnreadSymbolWrites()); + // Assert that even slow pass (dataflow analysis) would have yielded no unused symbol writes. + Debug.Assert(!SymbolUsageAnalysis.Run(context.GetControlFlowGraph(operationBlock), context.OwningSymbol, context.CancellationToken) + .HasUnreadSymbolWrites()); - hasBlockWithAllUsedSymbolWrites = true; - continue; - } + hasBlockWithAllUsedSymbolWrites = true; + continue; } + } - // Now perform the slower, precise, CFG based dataflow analysis to identify the actual unused symbol writes. - var controlFlowGraph = context.GetControlFlowGraph(operationBlock); - var symbolUsageResult = SymbolUsageAnalysis.Run(controlFlowGraph, context.OwningSymbol, context.CancellationToken); - symbolUsageResultsBuilder.Add(symbolUsageResult); + // Now perform the slower, precise, CFG based dataflow analysis to identify the actual unused symbol writes. + var controlFlowGraph = context.GetControlFlowGraph(operationBlock); + var symbolUsageResult = SymbolUsageAnalysis.Run(controlFlowGraph, context.OwningSymbol, context.CancellationToken); + symbolUsageResultsBuilder.Add(symbolUsageResult); - foreach (var (symbol, unreadWriteOperation) in symbolUsageResult.GetUnreadSymbolWrites()) + foreach (var (symbol, unreadWriteOperation) in symbolUsageResult.GetUnreadSymbolWrites()) + { + if (unreadWriteOperation == null) { - if (unreadWriteOperation == null) + // Null operation is used for initial write for the parameter from method declaration. + // So, the initial value of the parameter is never read in this operation block. + // However, we do not report this as an unused parameter here as a different operation block + // might be reading the initial parameter value. + // For example, a constructor with both a constructor initializer and body will have two different operation blocks + // and a parameter must be unused across both these blocks to be marked unused. + + // However, we do report unused parameters for local function here. + // Local function parameters are completely scoped to this operation block, and should be reported per-operation block. + var unusedParameter = (IParameterSymbol)symbol; + if (isComputingUnusedParams && + unusedParameter.ContainingSymbol.IsLocalFunction()) { - // Null operation is used for initial write for the parameter from method declaration. - // So, the initial value of the parameter is never read in this operation block. - // However, we do not report this as an unused parameter here as a different operation block - // might be reading the initial parameter value. - // For example, a constructor with both a constructor initializer and body will have two different operation blocks - // and a parameter must be unused across both these blocks to be marked unused. - - // However, we do report unused parameters for local function here. - // Local function parameters are completely scoped to this operation block, and should be reported per-operation block. - var unusedParameter = (IParameterSymbol)symbol; - if (isComputingUnusedParams && - unusedParameter.ContainingSymbol.IsLocalFunction()) + var hasReference = symbolUsageResult.SymbolsRead.Contains(unusedParameter); + + bool shouldReport; + switch (unusedParameter.RefKind) { - var hasReference = symbolUsageResult.SymbolsRead.Contains(unusedParameter); + case RefKind.Out: + // Do not report out parameters of local functions. + // If they are unused in the caller, we will flag the + // out argument at the local function callsite. + shouldReport = false; + break; - bool shouldReport; - switch (unusedParameter.RefKind) - { - case RefKind.Out: - // Do not report out parameters of local functions. - // If they are unused in the caller, we will flag the - // out argument at the local function callsite. - shouldReport = false; - break; - - case RefKind.Ref: - // Report ref parameters only if they have no read/write references. - // Note that we always have one write for the parameter input value from the caller. - shouldReport = !hasReference && symbolUsageResult.GetSymbolWriteCount(unusedParameter) == 1; - break; - - default: - shouldReport = true; - break; - } + case RefKind.Ref: + // Report ref parameters only if they have no read/write references. + // Note that we always have one write for the parameter input value from the caller. + shouldReport = !hasReference && symbolUsageResult.GetSymbolWriteCount(unusedParameter) == 1; + break; - if (shouldReport) - { - _symbolStartAnalyzer.ReportUnusedParameterDiagnostic(unusedParameter, hasReference, context.ReportDiagnostic, context.Options, cancellationToken: context.CancellationToken); - } + default: + shouldReport = true; + break; } - continue; + if (shouldReport) + { + _symbolStartAnalyzer.ReportUnusedParameterDiagnostic(unusedParameter, hasReference, context.ReportDiagnostic, context.Options, cancellationToken: context.CancellationToken); + } } - if (ShouldReportUnusedValueDiagnostic(symbol, unreadWriteOperation, symbolUsageResult, out var properties)) - { - var diagnostic = DiagnosticHelper.Create(s_valueAssignedIsUnusedRule, - _symbolStartAnalyzer._compilationAnalyzer.GetDefinitionLocationToFade(unreadWriteOperation), - _options.UnusedValueAssignmentSeverity, - additionalLocations: null, - properties, - symbol.Name); - context.ReportDiagnostic(diagnostic); - } + continue; } - } - return; - - // Local functions. - bool ShouldReportUnusedValueDiagnostic( - ISymbol symbol, - IOperation unreadWriteOperation, - SymbolUsageResult resultFromFlowAnalysis, - out ImmutableDictionary? properties) - { - Debug.Assert(symbol is not ILocalSymbol local || !local.IsRef); - - properties = null; - - // Bail out in following cases: - // 1. End user has configured the diagnostic to be suppressed. - // 2. Symbol has error type, hence the diagnostic could be noised - // 3. Static local symbols. Assignment to static locals - // is not unnecessary as the assigned value can be used on the next invocation. - // 4. Ignore special discard symbol names (see https://github.com/dotnet/roslyn/issues/32923). - if (_options.UnusedValueAssignmentSeverity.Severity == ReportDiagnostic.Suppress || - symbol.GetSymbolType().IsErrorType() || - (symbol.IsStatic && symbol.Kind == SymbolKind.Local) || - symbol.IsSymbolWithSpecialDiscardName()) + if (ShouldReportUnusedValueDiagnostic(symbol, unreadWriteOperation, symbolUsageResult, out var properties)) { - return false; + var diagnostic = DiagnosticHelper.Create(s_valueAssignedIsUnusedRule, + _symbolStartAnalyzer._compilationAnalyzer.GetDefinitionLocationToFade(unreadWriteOperation), + _options.UnusedValueAssignmentSeverity, + context.Options, + additionalLocations: null, + properties, + symbol.Name); + context.ReportDiagnostic(diagnostic); } + } + } - // Flag to indicate if the symbol has no reads. - var isUnusedLocalAssignment = symbol is ILocalSymbol localSymbol && - !resultFromFlowAnalysis.SymbolsRead.Contains(localSymbol); - - var isRemovableAssignment = IsRemovableAssignmentWithoutSideEffects(unreadWriteOperation); + return; - if (isUnusedLocalAssignment && - !isRemovableAssignment && - _options.UnusedValueAssignmentPreference == UnusedValuePreference.UnusedLocalVariable) - { - // Meets current user preference of using unused local symbols for storing computation result. - // Skip reporting diagnostic. - return false; - } + // Local functions. + bool ShouldReportUnusedValueDiagnostic( + ISymbol symbol, + IOperation unreadWriteOperation, + SymbolUsageResult resultFromFlowAnalysis, + out ImmutableDictionary? properties) + { + Debug.Assert(symbol is not ILocalSymbol local || !local.IsRef); - properties = s_propertiesMap[(_options.UnusedValueAssignmentPreference, isUnusedLocalAssignment, isRemovableAssignment)]; - return true; - } + properties = null; - // Indicates if the given unused symbol write is a removable assignment. - // This is true if the expression for the assigned value has no side effects. - bool IsRemovableAssignmentWithoutSideEffects(IOperation unusedSymbolWriteOperation) + // Bail out in following cases: + // 1. End user has configured the diagnostic to be suppressed. + // 2. Symbol has error type, hence the diagnostic could be noised + // 3. Static local symbols. Assignment to static locals + // is not unnecessary as the assigned value can be used on the next invocation. + // 4. Ignore special discard symbol names (see https://github.com/dotnet/roslyn/issues/32923). + if (_options.UnusedValueAssignmentSeverity.Severity == ReportDiagnostic.Suppress || + symbol.GetSymbolType().IsErrorType() || + (symbol.IsStatic && symbol.Kind == SymbolKind.Local) || + symbol.IsSymbolWithSpecialDiscardName()) { - if (_symbolStartAnalyzer._compilationAnalyzer.ShouldBailOutFromRemovableAssignmentAnalysis(unusedSymbolWriteOperation)) - { - return false; - } - - if (unusedSymbolWriteOperation.Parent is IAssignmentOperation assignment && - assignment.Target == unusedSymbolWriteOperation) - { - return IsRemovableAssignmentValueWithoutSideEffects(assignment.Value); - } - else if (unusedSymbolWriteOperation.Parent is IIncrementOrDecrementOperation) - { - // As the new value assigned to the incremented/decremented variable is unused, - // it is safe to remove the entire increment/decrement operation, - // as it cannot have side effects on anything but the variable. - return true; - } - - // Assume all other operations can have side effects, and cannot be removed. return false; } - static bool IsRemovableAssignmentValueWithoutSideEffects(IOperation assignmentValue) - { - if (assignmentValue.ConstantValue.HasValue) - { - // Constant expressions have no side effects. - return true; - } + // Flag to indicate if the symbol has no reads. + var isUnusedLocalAssignment = symbol is ILocalSymbol localSymbol && + !resultFromFlowAnalysis.SymbolsRead.Contains(localSymbol); - switch (assignmentValue.Kind) - { - case OperationKind.ParameterReference: - case OperationKind.LocalReference: - // Parameter/local references have no side effects and can be removed. - return true; - - case OperationKind.FieldReference: - // Field references with null instance (static fields) or 'this' or 'Me' instance can - // have no side effects and can be removed. - var fieldReference = (IFieldReferenceOperation)assignmentValue; - return fieldReference.Instance == null || fieldReference.Instance.Kind == OperationKind.InstanceReference; - - case OperationKind.DefaultValue: - // Default value expressions have no side-effects. - return true; - - case OperationKind.Conversion: - // Conversions can theoretically have side-effects as the conversion can throw exception(s). - // However, for all practical purposes, we can assume that a non-user defined conversion whose operand - // has no side effects can be safely removed. - var conversion = (IConversionOperation)assignmentValue; - return conversion.OperatorMethod == null && - IsRemovableAssignmentValueWithoutSideEffects(conversion.Operand); - } + var isRemovableAssignment = IsRemovableAssignmentWithoutSideEffects(unreadWriteOperation); - // Assume all other operations can have side effects, and cannot be removed. + if (isUnusedLocalAssignment && + !isRemovableAssignment && + _options.UnusedValueAssignmentPreference == UnusedValuePreference.UnusedLocalVariable) + { + // Meets current user preference of using unused local symbols for storing computation result. + // Skip reporting diagnostic. return false; } + + properties = s_propertiesMap[(_options.UnusedValueAssignmentPreference, isUnusedLocalAssignment, isRemovableAssignment)]; + return true; } - private void AnalyzeUnusedParameters( - OperationBlockAnalysisContext context, - bool isComputingUnusedParams, - PooledHashSet symbolUsageResultsBuilder, - bool hasBlockWithAllUsedSymbolWrites, - bool hasUnknownOperationNoneDescendant) + // Indicates if the given unused symbol write is a removable assignment. + // This is true if the expression for the assigned value has no side effects. + bool IsRemovableAssignmentWithoutSideEffects(IOperation unusedSymbolWriteOperation) { - // Process parameters for the context's OwningSymbol that are unused across all operation blocks. - - // Bail out cases: - // 1. Skip analysis if we are not computing unused parameters based on user's option preference or have - // a descendant operation with OperationKind.None (not yet implemented operation) in not a well-known location. - if (!isComputingUnusedParams || hasUnknownOperationNoneDescendant) + if (_symbolStartAnalyzer._compilationAnalyzer.ShouldBailOutFromRemovableAssignmentAnalysis(unusedSymbolWriteOperation)) { - return; + return false; } - // 2. Report unused parameters only for method symbols. - if (context.OwningSymbol is not IMethodSymbol method) + if (unusedSymbolWriteOperation.Parent is IAssignmentOperation assignment && + assignment.Target == unusedSymbolWriteOperation) { - return; + return IsRemovableAssignmentValueWithoutSideEffects(assignment.Value); } - - // Mark all unreferenced parameters as unused parameters with no read reference. - // We do so prior to bail out cases 3. and 4. below - // so that we flag unreferenced parameters even when we bail out from flow analysis. - foreach (var parameter in method.Parameters) + else if (unusedSymbolWriteOperation.Parent is IIncrementOrDecrementOperation) { - if (!_referencedParameters.ContainsKey(parameter)) - { - // Unused parameter without a reference. - _symbolStartAnalyzer._unusedParameters[parameter] = false; - } + // As the new value assigned to the incremented/decremented variable is unused, + // it is safe to remove the entire increment/decrement operation, + // as it cannot have side effects on anything but the variable. + return true; } - // 3. Bail out if we found a single operation block where all symbol writes were used. - if (hasBlockWithAllUsedSymbolWrites) + // Assume all other operations can have side effects, and cannot be removed. + return false; + } + + static bool IsRemovableAssignmentValueWithoutSideEffects(IOperation assignmentValue) + { + if (assignmentValue.ConstantValue.HasValue) { - return; + // Constant expressions have no side effects. + return true; } - // 4. Bail out if symbolUsageResultsBuilder is empty, indicating we skipped analysis for all operation blocks. - if (symbolUsageResultsBuilder.Count == 0) + switch (assignmentValue.Kind) { - return; + case OperationKind.ParameterReference: + case OperationKind.LocalReference: + // Parameter/local references have no side effects and can be removed. + return true; + + case OperationKind.FieldReference: + // Field references with null instance (static fields) or 'this' or 'Me' instance can + // have no side effects and can be removed. + var fieldReference = (IFieldReferenceOperation)assignmentValue; + return fieldReference.Instance == null || fieldReference.Instance.Kind == OperationKind.InstanceReference; + + case OperationKind.DefaultValue: + // Default value expressions have no side-effects. + return true; + + case OperationKind.Conversion: + // Conversions can theoretically have side-effects as the conversion can throw exception(s). + // However, for all practical purposes, we can assume that a non-user defined conversion whose operand + // has no side effects can be safely removed. + var conversion = (IConversionOperation)assignmentValue; + return conversion.OperatorMethod == null && + IsRemovableAssignmentValueWithoutSideEffects(conversion.Operand); } - foreach (var parameter in method.Parameters) + // Assume all other operations can have side effects, and cannot be removed. + return false; + } + } + + private void AnalyzeUnusedParameters( + OperationBlockAnalysisContext context, + bool isComputingUnusedParams, + PooledHashSet symbolUsageResultsBuilder, + bool hasBlockWithAllUsedSymbolWrites, + bool hasUnknownOperationNoneDescendant) + { + // Process parameters for the context's OwningSymbol that are unused across all operation blocks. + + // Bail out cases: + // 1. Skip analysis if we are not computing unused parameters based on user's option preference or have + // a descendant operation with OperationKind.None (not yet implemented operation) in not a well-known location. + if (!isComputingUnusedParams || hasUnknownOperationNoneDescendant) + { + return; + } + + // 2. Report unused parameters only for method symbols. + if (context.OwningSymbol is not IMethodSymbol method) + { + return; + } + + // Mark all unreferenced parameters as unused parameters with no read reference. + // We do so prior to bail out cases 3. and 4. below + // so that we flag unreferenced parameters even when we bail out from flow analysis. + foreach (var parameter in method.Parameters) + { + if (!_referencedParameters.ContainsKey(parameter)) { - var isUsed = false; - var isSymbolRead = false; - var isRefOrOutParam = parameter.IsRefOrOut(); + // Unused parameter without a reference. + _symbolStartAnalyzer._unusedParameters[parameter] = false; + } + } - // Iterate through symbol usage results for each operation block. - foreach (var symbolUsageResult in symbolUsageResultsBuilder) - { - if (symbolUsageResult.IsInitialParameterValueUsed(parameter)) - { - // Parameter is used in this block. - isUsed = true; - break; - } + // 3. Bail out if we found a single operation block where all symbol writes were used. + if (hasBlockWithAllUsedSymbolWrites) + { + return; + } - isSymbolRead |= symbolUsageResult.SymbolsRead.Contains(parameter); + // 4. Bail out if symbolUsageResultsBuilder is empty, indicating we skipped analysis for all operation blocks. + if (symbolUsageResultsBuilder.Count == 0) + { + return; + } - // Ref/Out parameters are considered used if they have any reads or writes - // Note that we always have one write for the parameter input value from the caller. - if (isRefOrOutParam && - (isSymbolRead || - symbolUsageResult.GetSymbolWriteCount(parameter) > 1)) - { - isUsed = true; - break; - } + foreach (var parameter in method.Parameters) + { + var isUsed = false; + var isSymbolRead = false; + var isRefOrOutParam = parameter.IsRefOrOut(); + + // Iterate through symbol usage results for each operation block. + foreach (var symbolUsageResult in symbolUsageResultsBuilder) + { + if (symbolUsageResult.IsInitialParameterValueUsed(parameter)) + { + // Parameter is used in this block. + isUsed = true; + break; } - if (!isUsed) + isSymbolRead |= symbolUsageResult.SymbolsRead.Contains(parameter); + + // Ref/Out parameters are considered used if they have any reads or writes + // Note that we always have one write for the parameter input value from the caller. + if (isRefOrOutParam && + (isSymbolRead || + symbolUsageResult.GetSymbolWriteCount(parameter) > 1)) { - // Mark if the symbol's value is read or the symbol is referenced to ensure appropriate diagnostic message is given. - _symbolStartAnalyzer._unusedParameters[parameter] = isSymbolRead || - _referencedParameters.ContainsKey(parameter); + isUsed = true; + break; } } + + if (!isUsed) + { + // Mark if the symbol's value is read or the symbol is referenced to ensure appropriate diagnostic message is given. + _symbolStartAnalyzer._unusedParameters[parameter] = isSymbolRead || + _referencedParameters.ContainsKey(parameter); + } } } } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.cs index 90f4e5c67a62c..3b52cf46647e9 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.cs @@ -18,264 +18,263 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues +namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues; + +internal abstract partial class AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer { - internal abstract partial class AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + private sealed partial class SymbolStartAnalyzer( + AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer compilationAnalyzer, + INamedTypeSymbol eventArgsTypeOpt, + ImmutableHashSet attributeSetForMethodsToIgnore, + DeserializationConstructorCheck deserializationConstructorCheck, + INamedTypeSymbol iCustomMarshaler, + SymbolStartAnalysisContext symbolStartAnalysisContext) { - private sealed partial class SymbolStartAnalyzer( - AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer compilationAnalyzer, - INamedTypeSymbol eventArgsTypeOpt, - ImmutableHashSet attributeSetForMethodsToIgnore, - DeserializationConstructorCheck deserializationConstructorCheck, - INamedTypeSymbol iCustomMarshaler, - SymbolStartAnalysisContext symbolStartAnalysisContext) + private readonly AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer _compilationAnalyzer = compilationAnalyzer; + + private readonly INamedTypeSymbol _eventArgsTypeOpt = eventArgsTypeOpt; + private readonly ImmutableHashSet _attributeSetForMethodsToIgnore = attributeSetForMethodsToIgnore; + private readonly DeserializationConstructorCheck _deserializationConstructorCheck = deserializationConstructorCheck; + private readonly ConcurrentDictionary _methodsUsedAsDelegates = []; + private readonly INamedTypeSymbol _iCustomMarshaler = iCustomMarshaler; + private readonly SymbolStartAnalysisContext _symbolStartAnalysisContext = symbolStartAnalysisContext; + + /// + /// Map from unused parameters to a boolean value indicating if the parameter has a read reference or not. + /// For example, a parameter whose initial value is overwritten before any reads + /// is an unused parameter with read reference(s). + /// + private readonly ConcurrentDictionary _unusedParameters = []; + + public static void CreateAndRegisterActions( + CompilationStartAnalysisContext context, + AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer analyzer) { - private readonly AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer _compilationAnalyzer = compilationAnalyzer; - - private readonly INamedTypeSymbol _eventArgsTypeOpt = eventArgsTypeOpt; - private readonly ImmutableHashSet _attributeSetForMethodsToIgnore = attributeSetForMethodsToIgnore; - private readonly DeserializationConstructorCheck _deserializationConstructorCheck = deserializationConstructorCheck; - private readonly ConcurrentDictionary _methodsUsedAsDelegates = []; - private readonly INamedTypeSymbol _iCustomMarshaler = iCustomMarshaler; - private readonly SymbolStartAnalysisContext _symbolStartAnalysisContext = symbolStartAnalysisContext; - - /// - /// Map from unused parameters to a boolean value indicating if the parameter has a read reference or not. - /// For example, a parameter whose initial value is overwritten before any reads - /// is an unused parameter with read reference(s). - /// - private readonly ConcurrentDictionary _unusedParameters = []; - - public static void CreateAndRegisterActions( - CompilationStartAnalysisContext context, - AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer analyzer) - { - var attributeSetForMethodsToIgnore = ImmutableHashSet.CreateRange(GetAttributesForMethodsToIgnore(context.Compilation).WhereNotNull()); - var eventsArgType = context.Compilation.EventArgsType(); - var deserializationConstructorCheck = new DeserializationConstructorCheck(context.Compilation); - var iCustomMarshaler = context.Compilation.GetTypeByMetadataName(typeof(ICustomMarshaler).FullName!); + var attributeSetForMethodsToIgnore = ImmutableHashSet.CreateRange(GetAttributesForMethodsToIgnore(context.Compilation).WhereNotNull()); + var eventsArgType = context.Compilation.EventArgsType(); + var deserializationConstructorCheck = new DeserializationConstructorCheck(context.Compilation); + var iCustomMarshaler = context.Compilation.GetTypeByMetadataName(typeof(ICustomMarshaler).FullName!); - context.RegisterSymbolStartAction(symbolStartContext => + context.RegisterSymbolStartAction(symbolStartContext => + { + if (HasSyntaxErrors((INamedTypeSymbol)symbolStartContext.Symbol, symbolStartContext.CancellationToken)) { - if (HasSyntaxErrors((INamedTypeSymbol)symbolStartContext.Symbol, symbolStartContext.CancellationToken)) - { - // Bail out on syntax errors. - return; - } + // Bail out on syntax errors. + return; + } - // Create a new SymbolStartAnalyzer instance for every named type symbol - // to ensure there is no shared state (such as identified unused parameters within the type), - // as that would lead to duplicate diagnostics being reported from symbol end action callbacks - // for unrelated named types. - var symbolAnalyzer = new SymbolStartAnalyzer(analyzer, eventsArgType, attributeSetForMethodsToIgnore, - deserializationConstructorCheck, iCustomMarshaler, symbolStartContext); - symbolAnalyzer.OnSymbolStart(symbolStartContext); - }, SymbolKind.NamedType); + // Create a new SymbolStartAnalyzer instance for every named type symbol + // to ensure there is no shared state (such as identified unused parameters within the type), + // as that would lead to duplicate diagnostics being reported from symbol end action callbacks + // for unrelated named types. + var symbolAnalyzer = new SymbolStartAnalyzer(analyzer, eventsArgType, attributeSetForMethodsToIgnore, + deserializationConstructorCheck, iCustomMarshaler, symbolStartContext); + symbolAnalyzer.OnSymbolStart(symbolStartContext); + }, SymbolKind.NamedType); - return; + return; - // Local functions - static bool HasSyntaxErrors(INamedTypeSymbol namedTypeSymbol, CancellationToken cancellationToken) + // Local functions + static bool HasSyntaxErrors(INamedTypeSymbol namedTypeSymbol, CancellationToken cancellationToken) + { + foreach (var syntaxRef in namedTypeSymbol.DeclaringSyntaxReferences) { - foreach (var syntaxRef in namedTypeSymbol.DeclaringSyntaxReferences) + var syntax = syntaxRef.GetSyntax(cancellationToken); + if (syntax.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) { - var syntax = syntaxRef.GetSyntax(cancellationToken); - if (syntax.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) - { - return true; - } + return true; } - - return false; } - } - private void OnSymbolStart(SymbolStartAnalysisContext context) - { - context.RegisterOperationBlockStartAction(OnOperationBlock); - context.RegisterSymbolEndAction(OnSymbolEnd); + return false; } + } - private void OnOperationBlock(OperationBlockStartAnalysisContext context) - { - context.RegisterOperationAction(OnMethodReference, OperationKind.MethodReference); - BlockAnalyzer.Analyze(context, this); - } + private void OnSymbolStart(SymbolStartAnalysisContext context) + { + context.RegisterOperationBlockStartAction(OnOperationBlock); + context.RegisterSymbolEndAction(OnSymbolEnd); + } + + private void OnOperationBlock(OperationBlockStartAnalysisContext context) + { + context.RegisterOperationAction(OnMethodReference, OperationKind.MethodReference); + BlockAnalyzer.Analyze(context, this); + } + + private void OnMethodReference(OperationAnalysisContext context) + { + var methodBinding = (IMethodReferenceOperation)context.Operation; + _methodsUsedAsDelegates.GetOrAdd(methodBinding.Method.OriginalDefinition, true); + } - private void OnMethodReference(OperationAnalysisContext context) + private void OnSymbolEnd(SymbolAnalysisContext context) + { + foreach (var (parameter, hasReference) in _unusedParameters) { - var methodBinding = (IMethodReferenceOperation)context.Operation; - _methodsUsedAsDelegates.GetOrAdd(methodBinding.Method.OriginalDefinition, true); + ReportUnusedParameterDiagnostic(parameter, hasReference, context.ReportDiagnostic, context.Options, context.CancellationToken); } + } - private void OnSymbolEnd(SymbolAnalysisContext context) + private void ReportUnusedParameterDiagnostic( + IParameterSymbol parameter, + bool hasReference, + Action reportDiagnostic, + AnalyzerOptions analyzerOptions, + CancellationToken cancellationToken) + { + if (!IsUnusedParameterCandidate(parameter, cancellationToken)) { - foreach (var (parameter, hasReference) in _unusedParameters) - { - ReportUnusedParameterDiagnostic(parameter, hasReference, context.ReportDiagnostic, context.Options, context.CancellationToken); - } + return; } - private void ReportUnusedParameterDiagnostic( - IParameterSymbol parameter, - bool hasReference, - Action reportDiagnostic, - AnalyzerOptions analyzerOptions, - CancellationToken cancellationToken) + var location = parameter.Locations[0]; + var option = analyzerOptions.GetAnalyzerOptions(location.SourceTree).UnusedParameters; + if (option.Notification.Severity == ReportDiagnostic.Suppress || + !ShouldReportUnusedParameters(parameter.ContainingSymbol, option.Value, option.Notification.Severity)) { - if (!IsUnusedParameterCandidate(parameter, cancellationToken)) - { - return; - } + return; + } - var location = parameter.Locations[0]; - var option = analyzerOptions.GetAnalyzerOptions(location.SourceTree).UnusedParameters; - if (option.Notification.Severity == ReportDiagnostic.Suppress || - !ShouldReportUnusedParameters(parameter.ContainingSymbol, option.Value, option.Notification.Severity)) - { - return; - } + var message = GetMessageForUnusedParameterDiagnostic( + parameter.Name, + hasReference, + isPublicApiParameter: parameter.ContainingSymbol.HasPublicResultantVisibility(), + isLocalFunctionParameter: parameter.ContainingSymbol.IsLocalFunction()); - var message = GetMessageForUnusedParameterDiagnostic( - parameter.Name, - hasReference, - isPublicApiParameter: parameter.ContainingSymbol.HasPublicResultantVisibility(), - isLocalFunctionParameter: parameter.ContainingSymbol.IsLocalFunction()); + var diagnostic = DiagnosticHelper.CreateWithMessage(s_unusedParameterRule, location, + option.Notification, analyzerOptions, additionalLocations: null, properties: null, message); + reportDiagnostic(diagnostic); + } - var diagnostic = DiagnosticHelper.CreateWithMessage(s_unusedParameterRule, location, - option.Notification, additionalLocations: null, properties: null, message); - reportDiagnostic(diagnostic); + private static LocalizableString GetMessageForUnusedParameterDiagnostic( + string parameterName, + bool hasReference, + bool isPublicApiParameter, + bool isLocalFunctionParameter) + { + LocalizableString messageFormat; + if (isPublicApiParameter && + !isLocalFunctionParameter) + { + messageFormat = hasReference + ? AnalyzersResources.Parameter_0_can_be_removed_if_it_is_not_part_of_a_shipped_public_API_its_initial_value_is_never_used + : AnalyzersResources.Remove_unused_parameter_0_if_it_is_not_part_of_a_shipped_public_API; } - - private static LocalizableString GetMessageForUnusedParameterDiagnostic( - string parameterName, - bool hasReference, - bool isPublicApiParameter, - bool isLocalFunctionParameter) + else if (hasReference) { - LocalizableString messageFormat; - if (isPublicApiParameter && - !isLocalFunctionParameter) - { - messageFormat = hasReference - ? AnalyzersResources.Parameter_0_can_be_removed_if_it_is_not_part_of_a_shipped_public_API_its_initial_value_is_never_used - : AnalyzersResources.Remove_unused_parameter_0_if_it_is_not_part_of_a_shipped_public_API; - } - else if (hasReference) - { - messageFormat = AnalyzersResources.Parameter_0_can_be_removed_its_initial_value_is_never_used; - } - else - { - messageFormat = s_unusedParameterRule.MessageFormat; - } - - return new DiagnosticHelper.LocalizableStringWithArguments(messageFormat, parameterName); + messageFormat = AnalyzersResources.Parameter_0_can_be_removed_its_initial_value_is_never_used; } - - private static IEnumerable GetAttributesForMethodsToIgnore(Compilation compilation) + else { - // Ignore conditional methods (One conditional will often call another conditional method as its only use of a parameter) - yield return compilation.ConditionalAttribute(); + messageFormat = s_unusedParameterRule.MessageFormat; + } - // Ignore methods with special serialization attributes (All serialization methods need to take 'StreamingContext') - yield return compilation.OnDeserializingAttribute(); - yield return compilation.OnDeserializedAttribute(); - yield return compilation.OnSerializingAttribute(); - yield return compilation.OnSerializedAttribute(); + return new DiagnosticHelper.LocalizableStringWithArguments(messageFormat, parameterName); + } - // Don't flag obsolete methods. - yield return compilation.ObsoleteAttribute(); + private static IEnumerable GetAttributesForMethodsToIgnore(Compilation compilation) + { + // Ignore conditional methods (One conditional will often call another conditional method as its only use of a parameter) + yield return compilation.ConditionalAttribute(); - // Don't flag MEF import constructors with ImportingConstructor attribute. - yield return compilation.SystemCompositionImportingConstructorAttribute(); - yield return compilation.SystemComponentModelCompositionImportingConstructorAttribute(); - } + // Ignore methods with special serialization attributes (All serialization methods need to take 'StreamingContext') + yield return compilation.OnDeserializingAttribute(); + yield return compilation.OnDeserializedAttribute(); + yield return compilation.OnSerializingAttribute(); + yield return compilation.OnSerializedAttribute(); - private bool IsUnusedParameterCandidate(IParameterSymbol parameter, CancellationToken cancellationToken) - { - // Ignore certain special parameters/methods. - // Note that "method.ExplicitOrImplicitInterfaceImplementations" check below is not a complete check, - // as identifying this correctly requires analyzing referencing projects, which is not - // supported for analyzers. We believe this is still a good enough check for most cases so - // we don't have to bail out on reporting unused parameters for all public methods. - - if (parameter.IsImplicitlyDeclared || - parameter.Name == DiscardVariableName || - parameter.ContainingSymbol is not IMethodSymbol method || - method.IsImplicitlyDeclared || - method.IsExtern || - method.IsAbstract || - method.IsVirtual || - method.IsOverride || - method.PartialImplementationPart != null || - method.PartialDefinitionPart != null || - !method.ExplicitOrImplicitInterfaceImplementations().IsEmpty || - method.IsAccessor() || - method.IsAnonymousFunction() || - _compilationAnalyzer.MethodHasHandlesClause(method) || - _deserializationConstructorCheck.IsDeserializationConstructor(method)) - { - return false; - } + // Don't flag obsolete methods. + yield return compilation.ObsoleteAttribute(); - // Ignore parameters of type primary constructors since they map to public properties - if (parameter.IsPrimaryConstructor(cancellationToken)) - { - return false; - } + // Don't flag MEF import constructors with ImportingConstructor attribute. + yield return compilation.SystemCompositionImportingConstructorAttribute(); + yield return compilation.SystemComponentModelCompositionImportingConstructorAttribute(); + } - // Ignore event handler methods "Handler(object, MyEventArgs)" - // as event handlers are required to match this signature - // regardless of whether or not the parameters are used. - if (_eventArgsTypeOpt != null && - method.Parameters is [{ Type.SpecialType: SpecialType.System_Object }, var secondParam] && - secondParam.Type.InheritsFromOrEquals(_eventArgsTypeOpt)) - { - return false; - } + private bool IsUnusedParameterCandidate(IParameterSymbol parameter, CancellationToken cancellationToken) + { + // Ignore certain special parameters/methods. + // Note that "method.ExplicitOrImplicitInterfaceImplementations" check below is not a complete check, + // as identifying this correctly requires analyzing referencing projects, which is not + // supported for analyzers. We believe this is still a good enough check for most cases so + // we don't have to bail out on reporting unused parameters for all public methods. + + if (parameter.IsImplicitlyDeclared || + parameter.Name == DiscardVariableName || + parameter.ContainingSymbol is not IMethodSymbol method || + method.IsImplicitlyDeclared || + method.IsExtern || + method.IsAbstract || + method.IsVirtual || + method.IsOverride || + method.PartialImplementationPart != null || + method.PartialDefinitionPart != null || + !method.ExplicitOrImplicitInterfaceImplementations().IsEmpty || + method.IsAccessor() || + method.IsAnonymousFunction() || + _compilationAnalyzer.MethodHasHandlesClause(method) || + _deserializationConstructorCheck.IsDeserializationConstructor(method)) + { + return false; + } - // Ignore flagging parameters for methods with certain well-known attributes, - // which are known to have unused parameters in real world code. - if (method.GetAttributes().Any(static (a, self) => self._attributeSetForMethodsToIgnore.Contains(a.AttributeClass), this)) - { - return false; - } + // Ignore parameters of type primary constructors since they map to public properties + if (parameter.IsPrimaryConstructor(cancellationToken)) + { + return false; + } - // Methods used as delegates likely need to have unused parameters for signature compat. - if (_methodsUsedAsDelegates.ContainsKey(method)) - { - return false; - } + // Ignore event handler methods "Handler(object, MyEventArgs)" + // as event handlers are required to match this signature + // regardless of whether or not the parameters are used. + if (_eventArgsTypeOpt != null && + method.Parameters is [{ Type.SpecialType: SpecialType.System_Object }, var secondParam] && + secondParam.Type.InheritsFromOrEquals(_eventArgsTypeOpt)) + { + return false; + } - // Ignore special parameter names for methods that need a specific signature. - // For example, methods used as a delegate in a different type or project. - // This also serves as a convenient way to suppress instances of unused parameter diagnostic - // without disabling the diagnostic completely. - // We ignore parameter names that start with an underscore and are optionally followed by an integer, - // such as '_', '_1', '_2', etc. - if (parameter.IsSymbolWithSpecialDiscardName()) - { - return false; - } + // Ignore flagging parameters for methods with certain well-known attributes, + // which are known to have unused parameters in real world code. + if (method.GetAttributes().Any(static (a, self) => self._attributeSetForMethodsToIgnore.Contains(a.AttributeClass), this)) + { + return false; + } - var methodSyntax = method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken); - if (_compilationAnalyzer.ReturnsThrow(methodSyntax)) - { - return false; - } + // Methods used as delegates likely need to have unused parameters for signature compat. + if (_methodsUsedAsDelegates.ContainsKey(method)) + { + return false; + } - // Don't report on valid GetInstance method of ICustomMarshaler. - // See https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.icustommarshaler#implementing-the-getinstance-method - if (method is { MetadataName: "GetInstance", IsStatic: true, Parameters.Length: 1, ContainingType: { } containingType } methodSymbol && - methodSymbol.Parameters[0].Type.SpecialType == SpecialType.System_String && - containingType.AllInterfaces.Any((@interface, marshaler) => @interface.Equals(marshaler), _iCustomMarshaler)) - { - return false; - } + // Ignore special parameter names for methods that need a specific signature. + // For example, methods used as a delegate in a different type or project. + // This also serves as a convenient way to suppress instances of unused parameter diagnostic + // without disabling the diagnostic completely. + // We ignore parameter names that start with an underscore and are optionally followed by an integer, + // such as '_', '_1', '_2', etc. + if (parameter.IsSymbolWithSpecialDiscardName()) + { + return false; + } - return true; + var methodSyntax = method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken); + if (_compilationAnalyzer.ReturnsThrow(methodSyntax)) + { + return false; } + + // Don't report on valid GetInstance method of ICustomMarshaler. + // See https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.icustommarshaler#implementing-the-getinstance-method + if (method is { MetadataName: "GetInstance", IsStatic: true, Parameters.Length: 1, ContainingType: { } containingType } methodSymbol && + methodSymbol.Parameters[0].Type.SpecialType == SpecialType.System_String && + containingType.AllInterfaces.Any((@interface, marshaler) => @interface.Equals(marshaler), _iCustomMarshaler)) + { + return false; + } + + return true; } } } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.cs index 7e0af0ae5c9d4..0240858e09729 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.cs @@ -14,312 +14,311 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues +namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues; + +// Map from different combinations of diagnostic properties to a properties map that gets added to each diagnostic instance. +using PropertiesMap = ImmutableDictionary<(UnusedValuePreference preference, bool isUnusedLocalAssignment, bool isRemovableAssignment), + ImmutableDictionary>; + +/// +/// Analyzer to report unused expression values and parameters: +/// It flags the following cases: +/// 1. Expression statements that drop computed value, for example, "Computation();". +/// These should either be removed (redundant computation) or should be replaced +/// with explicit assignment to discard variable OR an unused local variable, +/// i.e. "_ = Computation();" or "var unused = Computation();" +/// This diagnostic configuration is controlled by language specific code style option "UnusedValueExpressionStatement". +/// 2. Value assignments to locals/parameters that are never used on any control flow path, +/// For example, value assigned to 'x' in first statement below is unused and will be flagged: +/// x = Computation(); +/// if (...) +/// x = Computation2(); +/// else +/// Computation3(out x); +/// ... = x; +/// Just as for case 1., these should either be removed (redundant computation) or +/// should be replaced with explicit assignment to discard variable OR an unused local variable, +/// i.e. "_ = Computation();" or "var unused = Computation();" +/// This diagnostic configuration is controlled by language specific code style option "UnusedValueAssignment". +/// 3. Redundant parameters that fall into one of the following two categories: +/// a. Have no references in the executable code block(s) for its containing method symbol. +/// b. Have one or more references but its initial value at start of code block is never used. +/// For example, if 'x' in the example for case 2. above was a parameter symbol with RefKind.None +/// and "x = Computation();" is the first statement in the method body, then its initial value +/// is never used. Such a parameter should be removed and 'x' should be converted into a local. +/// We provide additional information in the diagnostic message to clarify the above two categories +/// and also detect and mention about potential breaking change if the containing method is a public API. +/// Currently, we do not provide any code fix for removing unused parameters as it needs fixing the +/// call sites and any automated fix can lead to subtle overload resolution differences, +/// though this may change in future. +/// This diagnostic configuration is controlled by option. +/// +internal abstract partial class AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer { - // Map from different combinations of diagnostic properties to a properties map that gets added to each diagnostic instance. - using PropertiesMap = ImmutableDictionary<(UnusedValuePreference preference, bool isUnusedLocalAssignment, bool isRemovableAssignment), - ImmutableDictionary>; + public const string DiscardVariableName = "_"; + + private const string UnusedValuePreferenceKey = nameof(UnusedValuePreferenceKey); + private const string IsUnusedLocalAssignmentKey = nameof(IsUnusedLocalAssignmentKey); + private const string IsRemovableAssignmentKey = nameof(IsRemovableAssignmentKey); + + // Diagnostic reported for expression statements that drop computed value, for example, "Computation();". + // This is **not** an unnecessary (fading) diagnostic as the expression being flagged is not unnecessary, but the dropped value is. + private static readonly DiagnosticDescriptor s_expressionValueIsUnusedRule = CreateDescriptorWithId( + IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId, + EnforceOnBuildValues.ExpressionValueIsUnused, + hasAnyCodeStyleOption: true, + new LocalizableResourceString(nameof(AnalyzersResources.Expression_value_is_never_used), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Expression_value_is_never_used), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + isUnnecessary: false); + + // Diagnostic reported for value assignments to locals/parameters that are never used on any control flow path. + private static readonly DiagnosticDescriptor s_valueAssignedIsUnusedRule = CreateDescriptorWithId( + IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId, + EnforceOnBuildValues.ValueAssignedIsUnused, + hasAnyCodeStyleOption: true, + new LocalizableResourceString(nameof(AnalyzersResources.Unnecessary_assignment_of_a_value), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Unnecessary_assignment_of_a_value_to_0), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + description: new LocalizableResourceString(nameof(AnalyzersResources.Avoid_unnecessary_value_assignments_in_your_code_as_these_likely_indicate_redundant_value_computations_If_the_value_computation_is_not_redundant_and_you_intend_to_retain_the_assignmentcomma_then_change_the_assignment_target_to_a_local_variable_whose_name_starts_with_an_underscore_and_is_optionally_followed_by_an_integercomma_such_as___comma__1_comma__2_comma_etc), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + isUnnecessary: true); + + // Diagnostic reported for unnecessary parameters that can be removed. + private static readonly DiagnosticDescriptor s_unusedParameterRule = CreateDescriptorWithId( + IDEDiagnosticIds.UnusedParameterDiagnosticId, + EnforceOnBuildValues.UnusedParameter, + hasAnyCodeStyleOption: true, + new LocalizableResourceString(nameof(AnalyzersResources.Remove_unused_parameter), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Remove_unused_parameter_0), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + description: new LocalizableResourceString(nameof(AnalyzersResources.Avoid_unused_parameters_in_your_code_If_the_parameter_cannot_be_removed_then_change_its_name_so_it_starts_with_an_underscore_and_is_optionally_followed_by_an_integer_such_as__comma__1_comma__2_etc_These_are_treated_as_special_discard_symbol_names), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + isUnnecessary: true); + + private static readonly PropertiesMap s_propertiesMap = CreatePropertiesMap(); + + protected AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer( + Option2> unusedValueExpressionStatementOption, + Option2> unusedValueAssignmentOption) + : base(ImmutableDictionary.Empty + .Add(s_expressionValueIsUnusedRule, unusedValueExpressionStatementOption) + .Add(s_valueAssignedIsUnusedRule, unusedValueAssignmentOption) + .Add(s_unusedParameterRule, CodeStyleOptions2.UnusedParameters), + fadingOption: null) + { + } + + protected abstract ISyntaxFacts SyntaxFacts { get; } + protected abstract Location GetDefinitionLocationToFade(IOperation unusedDefinition); + protected abstract bool SupportsDiscard(SyntaxTree tree); + protected abstract bool MethodHasHandlesClause(IMethodSymbol method); + protected abstract bool IsIfConditionalDirective(SyntaxNode node); + protected abstract bool ReturnsThrow(SyntaxNode node); + protected abstract CodeStyleOption2 GetUnusedValueExpressionStatementOption(AnalyzerOptionsProvider provider); + protected abstract CodeStyleOption2 GetUnusedValueAssignmentOption(AnalyzerOptionsProvider provider); + + /// + /// Indicates if we should bail from removable assignment analysis for the given + /// symbol write operation. + /// Removable assignment analysis determines if the assigned value for the symbol write + /// has no side effects and can be removed without changing the semantics. + /// + protected virtual bool ShouldBailOutFromRemovableAssignmentAnalysis(IOperation unusedSymbolWriteOperation) + => false; /// - /// Analyzer to report unused expression values and parameters: - /// It flags the following cases: - /// 1. Expression statements that drop computed value, for example, "Computation();". - /// These should either be removed (redundant computation) or should be replaced - /// with explicit assignment to discard variable OR an unused local variable, - /// i.e. "_ = Computation();" or "var unused = Computation();" - /// This diagnostic configuration is controlled by language specific code style option "UnusedValueExpressionStatement". - /// 2. Value assignments to locals/parameters that are never used on any control flow path, - /// For example, value assigned to 'x' in first statement below is unused and will be flagged: - /// x = Computation(); - /// if (...) - /// x = Computation2(); - /// else - /// Computation3(out x); - /// ... = x; - /// Just as for case 1., these should either be removed (redundant computation) or - /// should be replaced with explicit assignment to discard variable OR an unused local variable, - /// i.e. "_ = Computation();" or "var unused = Computation();" - /// This diagnostic configuration is controlled by language specific code style option "UnusedValueAssignment". - /// 3. Redundant parameters that fall into one of the following two categories: - /// a. Have no references in the executable code block(s) for its containing method symbol. - /// b. Have one or more references but its initial value at start of code block is never used. - /// For example, if 'x' in the example for case 2. above was a parameter symbol with RefKind.None - /// and "x = Computation();" is the first statement in the method body, then its initial value - /// is never used. Such a parameter should be removed and 'x' should be converted into a local. - /// We provide additional information in the diagnostic message to clarify the above two categories - /// and also detect and mention about potential breaking change if the containing method is a public API. - /// Currently, we do not provide any code fix for removing unused parameters as it needs fixing the - /// call sites and any automated fix can lead to subtle overload resolution differences, - /// though this may change in future. - /// This diagnostic configuration is controlled by option. + /// Indicates if the given expression statement operation has an explicit "Call" statement syntax indicating explicit discard. + /// For example, VB "Call" statement. /// - internal abstract partial class AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + /// + protected abstract bool IsCallStatement(IExpressionStatementOperation expressionStatement); + + /// + /// Indicates if the given operation is an expression of an expression body. + /// + protected abstract bool IsExpressionOfExpressionBody(IExpressionStatementOperation expressionStatement); + + /// + /// Method to compute well-known diagnostic property maps for different combinations of diagnostic properties. + /// The property map is added to each instance of the reported diagnostic and is used by the code fixer to + /// compute the correct code fix. + /// It currently maps to three different properties of the diagnostic: + /// 1. The underlying for the reported diagnostic + /// 2. "isUnusedLocalAssignment": Flag indicating if the flagged local variable has no reads/uses. + /// 3. "isRemovableAssignment": Flag indicating if the assigned value is from an expression that has no side effects + /// and hence can be removed completely. For example, if the assigned value is a constant or a reference + /// to a local/parameter, then it has no side effects, but if it is method invocation, it may have side effects. + /// + /// + private static PropertiesMap CreatePropertiesMap() { - public const string DiscardVariableName = "_"; - - private const string UnusedValuePreferenceKey = nameof(UnusedValuePreferenceKey); - private const string IsUnusedLocalAssignmentKey = nameof(IsUnusedLocalAssignmentKey); - private const string IsRemovableAssignmentKey = nameof(IsRemovableAssignmentKey); - - // Diagnostic reported for expression statements that drop computed value, for example, "Computation();". - // This is **not** an unnecessary (fading) diagnostic as the expression being flagged is not unnecessary, but the dropped value is. - private static readonly DiagnosticDescriptor s_expressionValueIsUnusedRule = CreateDescriptorWithId( - IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId, - EnforceOnBuildValues.ExpressionValueIsUnused, - hasAnyCodeStyleOption: true, - new LocalizableResourceString(nameof(AnalyzersResources.Expression_value_is_never_used), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Expression_value_is_never_used), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - isUnnecessary: false); - - // Diagnostic reported for value assignments to locals/parameters that are never used on any control flow path. - private static readonly DiagnosticDescriptor s_valueAssignedIsUnusedRule = CreateDescriptorWithId( - IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId, - EnforceOnBuildValues.ValueAssignedIsUnused, - hasAnyCodeStyleOption: true, - new LocalizableResourceString(nameof(AnalyzersResources.Unnecessary_assignment_of_a_value), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Unnecessary_assignment_of_a_value_to_0), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - description: new LocalizableResourceString(nameof(AnalyzersResources.Avoid_unnecessary_value_assignments_in_your_code_as_these_likely_indicate_redundant_value_computations_If_the_value_computation_is_not_redundant_and_you_intend_to_retain_the_assignmentcomma_then_change_the_assignment_target_to_a_local_variable_whose_name_starts_with_an_underscore_and_is_optionally_followed_by_an_integercomma_such_as___comma__1_comma__2_comma_etc), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - isUnnecessary: true); - - // Diagnostic reported for unnecessary parameters that can be removed. - private static readonly DiagnosticDescriptor s_unusedParameterRule = CreateDescriptorWithId( - IDEDiagnosticIds.UnusedParameterDiagnosticId, - EnforceOnBuildValues.UnusedParameter, - hasAnyCodeStyleOption: true, - new LocalizableResourceString(nameof(AnalyzersResources.Remove_unused_parameter), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Remove_unused_parameter_0), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - description: new LocalizableResourceString(nameof(AnalyzersResources.Avoid_unused_parameters_in_your_code_If_the_parameter_cannot_be_removed_then_change_its_name_so_it_starts_with_an_underscore_and_is_optionally_followed_by_an_integer_such_as__comma__1_comma__2_etc_These_are_treated_as_special_discard_symbol_names), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - isUnnecessary: true); - - private static readonly PropertiesMap s_propertiesMap = CreatePropertiesMap(); - - protected AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer( - Option2> unusedValueExpressionStatementOption, - Option2> unusedValueAssignmentOption) - : base(ImmutableDictionary.Empty - .Add(s_expressionValueIsUnusedRule, unusedValueExpressionStatementOption) - .Add(s_valueAssignedIsUnusedRule, unusedValueAssignmentOption) - .Add(s_unusedParameterRule, CodeStyleOptions2.UnusedParameters), - fadingOption: null) + var builder = ImmutableDictionary.CreateBuilder<(UnusedValuePreference preference, bool isUnusedLocalAssignment, bool isRemovableAssignment), + ImmutableDictionary>(); + AddEntries(UnusedValuePreference.DiscardVariable); + AddEntries(UnusedValuePreference.UnusedLocalVariable); + return builder.ToImmutable(); + + void AddEntries(UnusedValuePreference preference) { + AddEntries2(preference, isUnusedLocalAssignment: true); + AddEntries2(preference, isUnusedLocalAssignment: false); } - protected abstract ISyntaxFacts SyntaxFacts { get; } - protected abstract Location GetDefinitionLocationToFade(IOperation unusedDefinition); - protected abstract bool SupportsDiscard(SyntaxTree tree); - protected abstract bool MethodHasHandlesClause(IMethodSymbol method); - protected abstract bool IsIfConditionalDirective(SyntaxNode node); - protected abstract bool ReturnsThrow(SyntaxNode node); - protected abstract CodeStyleOption2 GetUnusedValueExpressionStatementOption(AnalyzerOptionsProvider provider); - protected abstract CodeStyleOption2 GetUnusedValueAssignmentOption(AnalyzerOptionsProvider provider); - - /// - /// Indicates if we should bail from removable assignment analysis for the given - /// symbol write operation. - /// Removable assignment analysis determines if the assigned value for the symbol write - /// has no side effects and can be removed without changing the semantics. - /// - protected virtual bool ShouldBailOutFromRemovableAssignmentAnalysis(IOperation unusedSymbolWriteOperation) - => false; - - /// - /// Indicates if the given expression statement operation has an explicit "Call" statement syntax indicating explicit discard. - /// For example, VB "Call" statement. - /// - /// - protected abstract bool IsCallStatement(IExpressionStatementOperation expressionStatement); - - /// - /// Indicates if the given operation is an expression of an expression body. - /// - protected abstract bool IsExpressionOfExpressionBody(IExpressionStatementOperation expressionStatement); - - /// - /// Method to compute well-known diagnostic property maps for different combinations of diagnostic properties. - /// The property map is added to each instance of the reported diagnostic and is used by the code fixer to - /// compute the correct code fix. - /// It currently maps to three different properties of the diagnostic: - /// 1. The underlying for the reported diagnostic - /// 2. "isUnusedLocalAssignment": Flag indicating if the flagged local variable has no reads/uses. - /// 3. "isRemovableAssignment": Flag indicating if the assigned value is from an expression that has no side effects - /// and hence can be removed completely. For example, if the assigned value is a constant or a reference - /// to a local/parameter, then it has no side effects, but if it is method invocation, it may have side effects. - /// - /// - private static PropertiesMap CreatePropertiesMap() + void AddEntries2(UnusedValuePreference preference, bool isUnusedLocalAssignment) { - var builder = ImmutableDictionary.CreateBuilder<(UnusedValuePreference preference, bool isUnusedLocalAssignment, bool isRemovableAssignment), - ImmutableDictionary>(); - AddEntries(UnusedValuePreference.DiscardVariable); - AddEntries(UnusedValuePreference.UnusedLocalVariable); - return builder.ToImmutable(); + AddEntryCore(preference, isUnusedLocalAssignment, isRemovableAssignment: true); + AddEntryCore(preference, isUnusedLocalAssignment, isRemovableAssignment: false); + } - void AddEntries(UnusedValuePreference preference) - { - AddEntries2(preference, isUnusedLocalAssignment: true); - AddEntries2(preference, isUnusedLocalAssignment: false); - } + void AddEntryCore(UnusedValuePreference preference, bool isUnusedLocalAssignment, bool isRemovableAssignment) + { + var propertiesBuilder = ImmutableDictionary.CreateBuilder(); - void AddEntries2(UnusedValuePreference preference, bool isUnusedLocalAssignment) + propertiesBuilder.Add(UnusedValuePreferenceKey, preference.ToString()); + if (isUnusedLocalAssignment) { - AddEntryCore(preference, isUnusedLocalAssignment, isRemovableAssignment: true); - AddEntryCore(preference, isUnusedLocalAssignment, isRemovableAssignment: false); + propertiesBuilder.Add(IsUnusedLocalAssignmentKey, string.Empty); } - void AddEntryCore(UnusedValuePreference preference, bool isUnusedLocalAssignment, bool isRemovableAssignment) + if (isRemovableAssignment) { - var propertiesBuilder = ImmutableDictionary.CreateBuilder(); - - propertiesBuilder.Add(UnusedValuePreferenceKey, preference.ToString()); - if (isUnusedLocalAssignment) - { - propertiesBuilder.Add(IsUnusedLocalAssignmentKey, string.Empty); - } - - if (isRemovableAssignment) - { - propertiesBuilder.Add(IsRemovableAssignmentKey, string.Empty); - } - - builder.Add((preference, isUnusedLocalAssignment, isRemovableAssignment), propertiesBuilder.ToImmutable()); + propertiesBuilder.Add(IsRemovableAssignmentKey, string.Empty); } + + builder.Add((preference, isUnusedLocalAssignment, isRemovableAssignment), propertiesBuilder.ToImmutable()); } + } - // Our analysis is limited to unused expressions in a code block, hence is unaffected by changes outside the code block. - // Hence, we can support incremental span based method body analysis. - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + // Our analysis is limited to unused expressions in a code block, hence is unaffected by changes outside the code block. + // Hence, we can support incremental span based method body analysis. + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected sealed override GeneratedCodeAnalysisFlags GeneratedCodeAnalysisFlags => GeneratedCodeAnalysisFlags.Analyze; + protected sealed override GeneratedCodeAnalysisFlags GeneratedCodeAnalysisFlags => GeneratedCodeAnalysisFlags.Analyze; - protected sealed override void InitializeWorker(AnalysisContext context) - { - context.RegisterCompilationStartAction( - compilationContext => SymbolStartAnalyzer.CreateAndRegisterActions(compilationContext, this)); - } + protected sealed override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction( + compilationContext => SymbolStartAnalyzer.CreateAndRegisterActions(compilationContext, this)); + } - private bool TryGetOptions(SyntaxTree syntaxTree, AnalyzerOptions analyzerOptions, CompilationOptions compilationOptions, CancellationToken cancellationToken, out Options options) - { - options = null; + private bool TryGetOptions(SyntaxTree syntaxTree, AnalyzerOptions analyzerOptions, CompilationOptions compilationOptions, CancellationToken cancellationToken, out Options options) + { + options = null; - var optionsProvider = analyzerOptions.GetAnalyzerOptions(syntaxTree); + var optionsProvider = analyzerOptions.GetAnalyzerOptions(syntaxTree); - var unusedParametersOption = optionsProvider.UnusedParameters; - var (unusedValueExpressionStatementPreference, unusedValueExpressionStatementSeverity) = GetPreferenceAndSeverity(GetUnusedValueExpressionStatementOption(optionsProvider)); - var (unusedValueAssignmentPreference, unusedValueAssignmentSeverity) = GetPreferenceAndSeverity(GetUnusedValueAssignmentOption(optionsProvider)); + var unusedParametersOption = optionsProvider.UnusedParameters; + var (unusedValueExpressionStatementPreference, unusedValueExpressionStatementSeverity) = GetPreferenceAndSeverity(GetUnusedValueExpressionStatementOption(optionsProvider)); + var (unusedValueAssignmentPreference, unusedValueAssignmentSeverity) = GetPreferenceAndSeverity(GetUnusedValueAssignmentOption(optionsProvider)); - var notifications = ImmutableArray.Create(unusedParametersOption.Notification, unusedValueExpressionStatementSeverity, unusedValueAssignmentSeverity); - if (ShouldSkipAnalysis(syntaxTree, analyzerOptions, compilationOptions, notifications, cancellationToken)) - return false; + var notifications = ImmutableArray.Create(unusedParametersOption.Notification, unusedValueExpressionStatementSeverity, unusedValueAssignmentSeverity); + if (ShouldSkipAnalysis(syntaxTree, analyzerOptions, compilationOptions, notifications, cancellationToken)) + return false; - options = new Options(unusedValueExpressionStatementPreference, unusedValueExpressionStatementSeverity, - unusedValueAssignmentPreference, unusedValueAssignmentSeverity, - unusedParametersOption.Value, unusedParametersOption.Notification); - return true; + options = new Options(unusedValueExpressionStatementPreference, unusedValueExpressionStatementSeverity, + unusedValueAssignmentPreference, unusedValueAssignmentSeverity, + unusedParametersOption.Value, unusedParametersOption.Notification); + return true; - // Local functions. - (UnusedValuePreference preference, NotificationOption2 notification) GetPreferenceAndSeverity(CodeStyleOption2 option) + // Local functions. + (UnusedValuePreference preference, NotificationOption2 notification) GetPreferenceAndSeverity(CodeStyleOption2 option) + { + var preferenceOpt = option?.Value; + if (preferenceOpt == null || + option.Notification.Severity == ReportDiagnostic.Suppress) { - var preferenceOpt = option?.Value; - if (preferenceOpt == null || - option.Notification.Severity == ReportDiagnostic.Suppress) - { - // Prefer does not matter as the severity is suppressed - we will never report this diagnostic. - return (default(UnusedValuePreference), NotificationOption2.None); - } - - // If language or language version does not support discard, fall back to prefer unused local variable. - if (preferenceOpt.Value == UnusedValuePreference.DiscardVariable && - !SupportsDiscard(syntaxTree)) - { - preferenceOpt = UnusedValuePreference.UnusedLocalVariable; - } - - return (preferenceOpt.Value, option.Notification); + // Prefer does not matter as the severity is suppressed - we will never report this diagnostic. + return (default(UnusedValuePreference), NotificationOption2.None); } - } - private sealed class Options - { - private readonly UnusedParametersPreference _unusedParametersPreference; - private readonly NotificationOption2 _unusedParametersSeverity; - - public Options( - UnusedValuePreference unusedValueExpressionStatementPreference, - NotificationOption2 unusedValueExpressionStatementSeverity, - UnusedValuePreference unusedValueAssignmentPreference, - NotificationOption2 unusedValueAssignmentSeverity, - UnusedParametersPreference unusedParametersPreference, - NotificationOption2 unusedParametersSeverity) + // If language or language version does not support discard, fall back to prefer unused local variable. + if (preferenceOpt.Value == UnusedValuePreference.DiscardVariable && + !SupportsDiscard(syntaxTree)) { - Debug.Assert(unusedValueExpressionStatementSeverity.Severity != ReportDiagnostic.Suppress || - unusedValueAssignmentSeverity.Severity != ReportDiagnostic.Suppress || - unusedParametersSeverity.Severity != ReportDiagnostic.Suppress); - - UnusedValueExpressionStatementPreference = unusedValueExpressionStatementPreference; - UnusedValueExpressionStatementNotification = unusedValueExpressionStatementSeverity; - UnusedValueAssignmentPreference = unusedValueAssignmentPreference; - UnusedValueAssignmentSeverity = unusedValueAssignmentSeverity; - _unusedParametersPreference = unusedParametersPreference; - _unusedParametersSeverity = unusedParametersSeverity; + preferenceOpt = UnusedValuePreference.UnusedLocalVariable; } - public UnusedValuePreference UnusedValueExpressionStatementPreference { get; } - public NotificationOption2 UnusedValueExpressionStatementNotification { get; } - public UnusedValuePreference UnusedValueAssignmentPreference { get; } - public NotificationOption2 UnusedValueAssignmentSeverity { get; } - public bool IsComputingUnusedParams(ISymbol symbol) - => ShouldReportUnusedParameters(symbol, _unusedParametersPreference, _unusedParametersSeverity.Severity); + return (preferenceOpt.Value, option.Notification); } + } - public static bool ShouldReportUnusedParameters( - ISymbol symbol, + private sealed class Options + { + private readonly UnusedParametersPreference _unusedParametersPreference; + private readonly NotificationOption2 _unusedParametersSeverity; + + public Options( + UnusedValuePreference unusedValueExpressionStatementPreference, + NotificationOption2 unusedValueExpressionStatementSeverity, + UnusedValuePreference unusedValueAssignmentPreference, + NotificationOption2 unusedValueAssignmentSeverity, UnusedParametersPreference unusedParametersPreference, - ReportDiagnostic unusedParametersSeverity) + NotificationOption2 unusedParametersSeverity) { - if (unusedParametersSeverity == ReportDiagnostic.Suppress) - { - return false; - } - - if (unusedParametersPreference == UnusedParametersPreference.NonPublicMethods) - { - return !symbol.HasPublicResultantVisibility(); - } - - return true; + Debug.Assert(unusedValueExpressionStatementSeverity.Severity != ReportDiagnostic.Suppress || + unusedValueAssignmentSeverity.Severity != ReportDiagnostic.Suppress || + unusedParametersSeverity.Severity != ReportDiagnostic.Suppress); + + UnusedValueExpressionStatementPreference = unusedValueExpressionStatementPreference; + UnusedValueExpressionStatementNotification = unusedValueExpressionStatementSeverity; + UnusedValueAssignmentPreference = unusedValueAssignmentPreference; + UnusedValueAssignmentSeverity = unusedValueAssignmentSeverity; + _unusedParametersPreference = unusedParametersPreference; + _unusedParametersSeverity = unusedParametersSeverity; } - public static bool TryGetUnusedValuePreference(Diagnostic diagnostic, out UnusedValuePreference preference) - { - if (diagnostic.Properties != null && - diagnostic.Properties.TryGetValue(UnusedValuePreferenceKey, out var preferenceString)) - { - switch (preferenceString) - { - case nameof(UnusedValuePreference.DiscardVariable): - preference = UnusedValuePreference.DiscardVariable; - return true; - - case nameof(UnusedValuePreference.UnusedLocalVariable): - preference = UnusedValuePreference.UnusedLocalVariable; - return true; - } - } + public UnusedValuePreference UnusedValueExpressionStatementPreference { get; } + public NotificationOption2 UnusedValueExpressionStatementNotification { get; } + public UnusedValuePreference UnusedValueAssignmentPreference { get; } + public NotificationOption2 UnusedValueAssignmentSeverity { get; } + public bool IsComputingUnusedParams(ISymbol symbol) + => ShouldReportUnusedParameters(symbol, _unusedParametersPreference, _unusedParametersSeverity.Severity); + } - preference = default; + public static bool ShouldReportUnusedParameters( + ISymbol symbol, + UnusedParametersPreference unusedParametersPreference, + ReportDiagnostic unusedParametersSeverity) + { + if (unusedParametersSeverity == ReportDiagnostic.Suppress) + { return false; } - public static bool GetIsUnusedLocalDiagnostic(Diagnostic diagnostic) + if (unusedParametersPreference == UnusedParametersPreference.NonPublicMethods) { - Debug.Assert(TryGetUnusedValuePreference(diagnostic, out _)); - return diagnostic.Properties.ContainsKey(IsUnusedLocalAssignmentKey); + return !symbol.HasPublicResultantVisibility(); } - public static bool GetIsRemovableAssignmentDiagnostic(Diagnostic diagnostic) + return true; + } + + public static bool TryGetUnusedValuePreference(Diagnostic diagnostic, out UnusedValuePreference preference) + { + if (diagnostic.Properties != null && + diagnostic.Properties.TryGetValue(UnusedValuePreferenceKey, out var preferenceString)) { - Debug.Assert(TryGetUnusedValuePreference(diagnostic, out _)); - return diagnostic.Properties.ContainsKey(IsRemovableAssignmentKey); + switch (preferenceString) + { + case nameof(UnusedValuePreference.DiscardVariable): + preference = UnusedValuePreference.DiscardVariable; + return true; + + case nameof(UnusedValuePreference.UnusedLocalVariable): + preference = UnusedValuePreference.UnusedLocalVariable; + return true; + } } + + preference = default; + return false; + } + + public static bool GetIsUnusedLocalDiagnostic(Diagnostic diagnostic) + { + Debug.Assert(TryGetUnusedValuePreference(diagnostic, out _)); + return diagnostic.Properties.ContainsKey(IsUnusedLocalAssignmentKey); + } + + public static bool GetIsRemovableAssignmentDiagnostic(Diagnostic diagnostic) + { + Debug.Assert(TryGetUnusedValuePreference(diagnostic, out _)); + return diagnostic.Properties.ContainsKey(IsRemovableAssignmentKey); } } diff --git a/src/Analyzers/Core/Analyzers/SimplifyBooleanExpression/AbstractSimplifyConditionalDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/SimplifyBooleanExpression/AbstractSimplifyConditionalDiagnosticAnalyzer.cs index ca188bced0899..558c79e19bf31 100644 --- a/src/Analyzers/Core/Analyzers/SimplifyBooleanExpression/AbstractSimplifyConditionalDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/SimplifyBooleanExpression/AbstractSimplifyConditionalDiagnosticAnalyzer.cs @@ -10,159 +10,159 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.SimplifyBooleanExpression +namespace Microsoft.CodeAnalysis.SimplifyBooleanExpression; + +using static SimplifyBooleanExpressionConstants; + +internal abstract class AbstractSimplifyConditionalDiagnosticAnalyzer< + TSyntaxKind, + TExpressionSyntax, + TConditionalExpressionSyntax> : + AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct + where TExpressionSyntax : SyntaxNode + where TConditionalExpressionSyntax : TExpressionSyntax { - using static SimplifyBooleanExpressionConstants; - - internal abstract class AbstractSimplifyConditionalDiagnosticAnalyzer< - TSyntaxKind, - TExpressionSyntax, - TConditionalExpressionSyntax> : - AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TSyntaxKind : struct - where TExpressionSyntax : SyntaxNode - where TConditionalExpressionSyntax : TExpressionSyntax + private static readonly ImmutableDictionary s_takeCondition + = ImmutableDictionary.Empty; + private static readonly ImmutableDictionary s_negateCondition + = s_takeCondition.Add(Negate, Negate); + private static readonly ImmutableDictionary s_takeConditionOrWhenFalse + = s_takeCondition.Add(Or, Or).Add(WhenFalse, WhenFalse); + private static readonly ImmutableDictionary s_negateConditionAndWhenFalse + = s_negateCondition.Add(And, And).Add(WhenFalse, WhenFalse); + private static readonly ImmutableDictionary s_negateConditionOrWhenTrue + = s_negateCondition.Add(Or, Or).Add(WhenTrue, WhenTrue); + private static readonly ImmutableDictionary s_takeConditionAndWhenTrue + = s_takeCondition.Add(And, And).Add(WhenTrue, WhenTrue); + private static readonly ImmutableDictionary s_takeConditionAndWhenFalse + = s_takeCondition.Add(And, And).Add(WhenFalse, WhenFalse); + + protected AbstractSimplifyConditionalDiagnosticAnalyzer() + : base(IDEDiagnosticIds.SimplifyConditionalExpressionDiagnosticId, + EnforceOnBuildValues.SimplifyConditionalExpression, + CodeStyleOptions2.PreferSimplifiedBooleanExpressions, + new LocalizableResourceString(nameof(AnalyzersResources.Simplify_conditional_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Conditional_expression_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - private static readonly ImmutableDictionary s_takeCondition - = ImmutableDictionary.Empty; - private static readonly ImmutableDictionary s_negateCondition - = s_takeCondition.Add(Negate, Negate); - private static readonly ImmutableDictionary s_takeConditionOrWhenFalse - = s_takeCondition.Add(Or, Or).Add(WhenFalse, WhenFalse); - private static readonly ImmutableDictionary s_negateConditionAndWhenFalse - = s_negateCondition.Add(And, And).Add(WhenFalse, WhenFalse); - private static readonly ImmutableDictionary s_negateConditionOrWhenTrue - = s_negateCondition.Add(Or, Or).Add(WhenTrue, WhenTrue); - private static readonly ImmutableDictionary s_takeConditionAndWhenTrue - = s_takeCondition.Add(And, And).Add(WhenTrue, WhenTrue); - private static readonly ImmutableDictionary s_takeConditionAndWhenFalse - = s_takeCondition.Add(And, And).Add(WhenFalse, WhenFalse); - - protected AbstractSimplifyConditionalDiagnosticAnalyzer() - : base(IDEDiagnosticIds.SimplifyConditionalExpressionDiagnosticId, - EnforceOnBuildValues.SimplifyConditionalExpression, - CodeStyleOptions2.PreferSimplifiedBooleanExpressions, - new LocalizableResourceString(nameof(AnalyzersResources.Simplify_conditional_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Conditional_expression_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) + } + + protected abstract ISyntaxFacts SyntaxFacts { get; } + + protected abstract CommonConversion GetConversion(SemanticModel semanticModel, TExpressionSyntax node, CancellationToken cancellationToken); + + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected sealed override void InitializeWorker(AnalysisContext context) + { + var syntaxKinds = SyntaxFacts.SyntaxKinds; + context.RegisterSyntaxNodeAction( + AnalyzeConditionalExpression, syntaxKinds.Convert(syntaxKinds.ConditionalExpression)); + } + + private void AnalyzeConditionalExpression(SyntaxNodeAnalysisContext context) + { + var styleOption = context.GetAnalyzerOptions().PreferSimplifiedBooleanExpressions; + if (!styleOption.Value || ShouldSkipAnalysis(context, styleOption.Notification)) { + // Bail immediately if the user has disabled this feature. + return; } - protected abstract ISyntaxFacts SyntaxFacts { get; } + var semanticModel = context.SemanticModel; + var cancellationToken = context.CancellationToken; + var conditionalExpression = (TConditionalExpressionSyntax)context.Node; + SyntaxFacts.GetPartsOfConditionalExpression( + conditionalExpression, out var conditionNode, out var whenTrueNode, out var whenFalseNode); + var condition = (TExpressionSyntax)conditionNode; + var whenTrue = (TExpressionSyntax)whenTrueNode; + var whenFalse = (TExpressionSyntax)whenFalseNode; + + // Only offer when everything is a basic boolean type. That way we don't have to worry + // about any sort of subtle cases with implicit or bool conversions. + if (!IsSimpleBooleanType(condition) || + !IsSimpleBooleanType(whenTrue) || + !IsSimpleBooleanType(whenFalse)) + { + return; + } - protected abstract CommonConversion GetConversion(SemanticModel semanticModel, TExpressionSyntax node, CancellationToken cancellationToken); + var whenTrue_isTrue = IsTrue(whenTrue); + var whenTrue_isFalse = IsFalse(whenTrue); - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + var whenFalse_isTrue = IsTrue(whenFalse); + var whenFalse_isFalse = IsFalse(whenFalse); - protected sealed override void InitializeWorker(AnalysisContext context) + if (whenTrue_isTrue && whenFalse_isFalse) + { + // c ? true : false => c + ReportDiagnostic(s_takeCondition); + } + else if (whenTrue_isFalse && whenFalse_isTrue) + { + // c ? false : true => !c + ReportDiagnostic(s_negateCondition); + } + else if (whenTrue_isFalse && whenFalse_isFalse) + { + // c ? false : false => c && false + // Note: this is a slight optimization over the when `c ? false : wf` + // case below. It allows to generate `c && false` instead of `!c && false` + ReportDiagnostic(s_takeConditionAndWhenFalse); + } + else if (whenTrue_isTrue) + { + // c ? true : wf => c || wf + ReportDiagnostic(s_takeConditionOrWhenFalse); + } + else if (whenTrue_isFalse) + { + // c ? false : wf => !c && wf + ReportDiagnostic(s_negateConditionAndWhenFalse); + } + else if (whenFalse_isTrue) + { + // c ? wt : true => !c or wt + ReportDiagnostic(s_negateConditionOrWhenTrue); + } + else if (whenFalse_isFalse) { - var syntaxKinds = SyntaxFacts.SyntaxKinds; - context.RegisterSyntaxNodeAction( - AnalyzeConditionalExpression, syntaxKinds.Convert(syntaxKinds.ConditionalExpression)); + // c ? wt : false => c && wt + ReportDiagnostic(s_takeConditionAndWhenTrue); } - private void AnalyzeConditionalExpression(SyntaxNodeAnalysisContext context) + return; + + // local functions + + void ReportDiagnostic(ImmutableDictionary properties) + => context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + conditionalExpression.GetLocation(), + styleOption.Notification, + context.Options, + additionalLocations: null, + properties)); + + bool IsSimpleBooleanType(TExpressionSyntax node) { - var styleOption = context.GetAnalyzerOptions().PreferSimplifiedBooleanExpressions; - if (!styleOption.Value || ShouldSkipAnalysis(context, styleOption.Notification)) - { - // Bail immediately if the user has disabled this feature. - return; - } - - var semanticModel = context.SemanticModel; - var cancellationToken = context.CancellationToken; - var conditionalExpression = (TConditionalExpressionSyntax)context.Node; - SyntaxFacts.GetPartsOfConditionalExpression( - conditionalExpression, out var conditionNode, out var whenTrueNode, out var whenFalseNode); - var condition = (TExpressionSyntax)conditionNode; - var whenTrue = (TExpressionSyntax)whenTrueNode; - var whenFalse = (TExpressionSyntax)whenFalseNode; - - // Only offer when everything is a basic boolean type. That way we don't have to worry - // about any sort of subtle cases with implicit or bool conversions. - if (!IsSimpleBooleanType(condition) || - !IsSimpleBooleanType(whenTrue) || - !IsSimpleBooleanType(whenFalse)) - { - return; - } - - var whenTrue_isTrue = IsTrue(whenTrue); - var whenTrue_isFalse = IsFalse(whenTrue); - - var whenFalse_isTrue = IsTrue(whenFalse); - var whenFalse_isFalse = IsFalse(whenFalse); - - if (whenTrue_isTrue && whenFalse_isFalse) - { - // c ? true : false => c - ReportDiagnostic(s_takeCondition); - } - else if (whenTrue_isFalse && whenFalse_isTrue) - { - // c ? false : true => !c - ReportDiagnostic(s_negateCondition); - } - else if (whenTrue_isFalse && whenFalse_isFalse) - { - // c ? false : false => c && false - // Note: this is a slight optimization over the when `c ? false : wf` - // case below. It allows to generate `c && false` instead of `!c && false` - ReportDiagnostic(s_takeConditionAndWhenFalse); - } - else if (whenTrue_isTrue) - { - // c ? true : wf => c || wf - ReportDiagnostic(s_takeConditionOrWhenFalse); - } - else if (whenTrue_isFalse) - { - // c ? false : wf => !c && wf - ReportDiagnostic(s_negateConditionAndWhenFalse); - } - else if (whenFalse_isTrue) - { - // c ? wt : true => !c or wt - ReportDiagnostic(s_negateConditionOrWhenTrue); - } - else if (whenFalse_isFalse) - { - // c ? wt : false => c && wt - ReportDiagnostic(s_takeConditionAndWhenTrue); - } + var typeInfo = semanticModel.GetTypeInfo(node, cancellationToken); + var conversion = GetConversion(semanticModel, node, cancellationToken); - return; + return + conversion.MethodSymbol == null && + typeInfo.Type?.SpecialType == SpecialType.System_Boolean && + typeInfo.ConvertedType?.SpecialType == SpecialType.System_Boolean; + } - // local functions - - void ReportDiagnostic(ImmutableDictionary properties) - => context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - conditionalExpression.GetLocation(), - styleOption.Notification, - additionalLocations: null, - properties)); - - bool IsSimpleBooleanType(TExpressionSyntax node) - { - var typeInfo = semanticModel.GetTypeInfo(node, cancellationToken); - var conversion = GetConversion(semanticModel, node, cancellationToken); - - return - conversion.MethodSymbol == null && - typeInfo.Type?.SpecialType == SpecialType.System_Boolean && - typeInfo.ConvertedType?.SpecialType == SpecialType.System_Boolean; - } - - bool IsTrue(TExpressionSyntax node) => IsBoolValue(node, true); - bool IsFalse(TExpressionSyntax node) => IsBoolValue(node, false); - - bool IsBoolValue(TExpressionSyntax node, bool value) - { - var constantValue = semanticModel.GetConstantValue(node, cancellationToken); - return constantValue.HasValue && constantValue.Value is bool b && b == value; - } + bool IsTrue(TExpressionSyntax node) => IsBoolValue(node, true); + bool IsFalse(TExpressionSyntax node) => IsBoolValue(node, false); + + bool IsBoolValue(TExpressionSyntax node, bool value) + { + var constantValue = semanticModel.GetConstantValue(node, cancellationToken); + return constantValue.HasValue && constantValue.Value is bool b && b == value; } } } diff --git a/src/Analyzers/Core/Analyzers/SimplifyBooleanExpression/SimplifyBooleanExpressionConstants.cs b/src/Analyzers/Core/Analyzers/SimplifyBooleanExpression/SimplifyBooleanExpressionConstants.cs index 6de9284a15e6b..4b6d9508a05a1 100644 --- a/src/Analyzers/Core/Analyzers/SimplifyBooleanExpression/SimplifyBooleanExpressionConstants.cs +++ b/src/Analyzers/Core/Analyzers/SimplifyBooleanExpression/SimplifyBooleanExpressionConstants.cs @@ -2,14 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.SimplifyBooleanExpression +namespace Microsoft.CodeAnalysis.SimplifyBooleanExpression; + +internal static class SimplifyBooleanExpressionConstants { - internal static class SimplifyBooleanExpressionConstants - { - public const string Negate = nameof(Negate); - public const string Or = nameof(Or); - public const string And = nameof(And); - public const string WhenTrue = nameof(WhenTrue); - public const string WhenFalse = nameof(WhenFalse); - } + public const string Negate = nameof(Negate); + public const string Or = nameof(Or); + public const string And = nameof(And); + public const string WhenTrue = nameof(WhenTrue); + public const string WhenFalse = nameof(WhenFalse); } diff --git a/src/Analyzers/Core/Analyzers/SimplifyInterpolation/AbstractSimplifyInterpolationDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/SimplifyInterpolation/AbstractSimplifyInterpolationDiagnosticAnalyzer.cs index 634d699b8497d..dfe7d74ae8977 100644 --- a/src/Analyzers/Core/Analyzers/SimplifyInterpolation/AbstractSimplifyInterpolationDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/SimplifyInterpolation/AbstractSimplifyInterpolationDiagnosticAnalyzer.cs @@ -9,66 +9,66 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.SimplifyInterpolation +namespace Microsoft.CodeAnalysis.SimplifyInterpolation; + +internal abstract class AbstractSimplifyInterpolationDiagnosticAnalyzer< + TInterpolationSyntax, + TExpressionSyntax> : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + where TInterpolationSyntax : SyntaxNode + where TExpressionSyntax : SyntaxNode { - internal abstract class AbstractSimplifyInterpolationDiagnosticAnalyzer< - TInterpolationSyntax, - TExpressionSyntax> : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer - where TInterpolationSyntax : SyntaxNode - where TExpressionSyntax : SyntaxNode + protected AbstractSimplifyInterpolationDiagnosticAnalyzer() + : base(IDEDiagnosticIds.SimplifyInterpolationId, + EnforceOnBuildValues.SimplifyInterpolation, + CodeStyleOptions2.PreferSimplifiedInterpolation, + fadingOption: null, + new LocalizableResourceString(nameof(AnalyzersResources.Simplify_interpolation), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Interpolation_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - protected AbstractSimplifyInterpolationDiagnosticAnalyzer() - : base(IDEDiagnosticIds.SimplifyInterpolationId, - EnforceOnBuildValues.SimplifyInterpolation, - CodeStyleOptions2.PreferSimplifiedInterpolation, - fadingOption: null, - new LocalizableResourceString(nameof(AnalyzersResources.Simplify_interpolation), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Interpolation_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + } - protected abstract IVirtualCharService GetVirtualCharService(); + protected abstract IVirtualCharService GetVirtualCharService(); - protected abstract ISyntaxFacts GetSyntaxFacts(); + protected abstract ISyntaxFacts GetSyntaxFacts(); - protected abstract AbstractSimplifyInterpolationHelpers GetHelpers(); + protected abstract AbstractSimplifyInterpolationHelpers GetHelpers(); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterOperationAction(AnalyzeInterpolation, OperationKind.Interpolation); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterOperationAction(AnalyzeInterpolation, OperationKind.Interpolation); - private void AnalyzeInterpolation(OperationAnalysisContext context) + private void AnalyzeInterpolation(OperationAnalysisContext context) + { + var option = context.GetAnalyzerOptions().PreferSimplifiedInterpolation; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) { - var option = context.GetAnalyzerOptions().PreferSimplifiedInterpolation; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - { - // No point in analyzing if the option is off. - return; - } + // No point in analyzing if the option is off. + return; + } - var interpolation = (IInterpolationOperation)context.Operation; - GetHelpers().UnwrapInterpolation( - GetVirtualCharService(), GetSyntaxFacts(), interpolation, out _, out var alignment, out _, - out var formatString, out var unnecessaryLocations); + var interpolation = (IInterpolationOperation)context.Operation; + GetHelpers().UnwrapInterpolation( + GetVirtualCharService(), GetSyntaxFacts(), interpolation, out _, out var alignment, out _, + out var formatString, out var unnecessaryLocations); - if (alignment == null && formatString == null) - { - return; - } + if (alignment == null && formatString == null) + { + return; + } - // The diagnostic itself fades the first unnecessary location, and the remaining locations are passed as - // additional unnecessary locations. - var firstUnnecessaryLocation = unnecessaryLocations[0]; - var remainingUnnecessaryLocations = unnecessaryLocations.RemoveAt(0); + // The diagnostic itself fades the first unnecessary location, and the remaining locations are passed as + // additional unnecessary locations. + var firstUnnecessaryLocation = unnecessaryLocations[0]; + var remainingUnnecessaryLocations = unnecessaryLocations.RemoveAt(0); - context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( - Descriptor, - firstUnnecessaryLocation, - option.Notification, - additionalLocations: [interpolation.Syntax.GetLocation()], - additionalUnnecessaryLocations: remainingUnnecessaryLocations)); - } + context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( + Descriptor, + firstUnnecessaryLocation, + option.Notification, + context.Options, + additionalLocations: [interpolation.Syntax.GetLocation()], + additionalUnnecessaryLocations: remainingUnnecessaryLocations)); } } diff --git a/src/Analyzers/Core/Analyzers/SimplifyInterpolation/AbstractSimplifyInterpolationHelpers.cs b/src/Analyzers/Core/Analyzers/SimplifyInterpolation/AbstractSimplifyInterpolationHelpers.cs index c22ecba6d6adb..84005a5485373 100644 --- a/src/Analyzers/Core/Analyzers/SimplifyInterpolation/AbstractSimplifyInterpolationHelpers.cs +++ b/src/Analyzers/Core/Analyzers/SimplifyInterpolation/AbstractSimplifyInterpolationHelpers.cs @@ -15,236 +15,235 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.SimplifyInterpolation +namespace Microsoft.CodeAnalysis.SimplifyInterpolation; + +internal abstract class AbstractSimplifyInterpolationHelpers { - internal abstract class AbstractSimplifyInterpolationHelpers - { - protected abstract bool PermitNonLiteralAlignmentComponents { get; } + protected abstract bool PermitNonLiteralAlignmentComponents { get; } - protected abstract SyntaxNode GetPreservedInterpolationExpressionSyntax(IOperation operation); + protected abstract SyntaxNode GetPreservedInterpolationExpressionSyntax(IOperation operation); - public void UnwrapInterpolation( - IVirtualCharService virtualCharService, ISyntaxFacts syntaxFacts, IInterpolationOperation interpolation, - out TExpressionSyntax? unwrapped, out TExpressionSyntax? alignment, out bool negate, - out string? formatString, out ImmutableArray unnecessaryLocations) - where TInterpolationSyntax : SyntaxNode - where TExpressionSyntax : SyntaxNode - { - alignment = null; - negate = false; - formatString = null; + public void UnwrapInterpolation( + IVirtualCharService virtualCharService, ISyntaxFacts syntaxFacts, IInterpolationOperation interpolation, + out TExpressionSyntax? unwrapped, out TExpressionSyntax? alignment, out bool negate, + out string? formatString, out ImmutableArray unnecessaryLocations) + where TInterpolationSyntax : SyntaxNode + where TExpressionSyntax : SyntaxNode + { + alignment = null; + negate = false; + formatString = null; - var unnecessarySpans = new List(); + var unnecessarySpans = new List(); - var expression = Unwrap(interpolation.Expression); - if (interpolation.Alignment == null) - UnwrapAlignmentPadding(expression, out expression, out alignment, out negate, unnecessarySpans); + var expression = Unwrap(interpolation.Expression); + if (interpolation.Alignment == null) + UnwrapAlignmentPadding(expression, out expression, out alignment, out negate, unnecessarySpans); - if (interpolation.FormatString == null) - UnwrapFormatString(virtualCharService, syntaxFacts, expression, out expression, out formatString, unnecessarySpans); + if (interpolation.FormatString == null) + UnwrapFormatString(virtualCharService, syntaxFacts, expression, out expression, out formatString, unnecessarySpans); - unwrapped = GetPreservedInterpolationExpressionSyntax(expression) as TExpressionSyntax; + unwrapped = GetPreservedInterpolationExpressionSyntax(expression) as TExpressionSyntax; - unnecessaryLocations = unnecessarySpans - .OrderBy(t => t.Start) - .SelectAsArray(interpolation.Syntax.SyntaxTree.GetLocation); - } + unnecessaryLocations = unnecessarySpans + .OrderBy(t => t.Start) + .SelectAsArray(interpolation.Syntax.SyntaxTree.GetLocation); + } - [return: NotNullIfNotNull(nameof(expression))] - private static IOperation? Unwrap(IOperation? expression, bool towardsParent = false) + [return: NotNullIfNotNull(nameof(expression))] + private static IOperation? Unwrap(IOperation? expression, bool towardsParent = false) + { + while (true) { - while (true) + if (towardsParent && expression?.Parent is null) + return expression; + + switch (expression) { - if (towardsParent && expression?.Parent is null) + case IParenthesizedOperation parenthesized: + expression = towardsParent ? expression.Parent : parenthesized.Operand; + continue; + case IConversionOperation { IsImplicit: true } conversion: + expression = towardsParent ? expression.Parent : conversion.Operand; + continue; + default: return expression; - - switch (expression) - { - case IParenthesizedOperation parenthesized: - expression = towardsParent ? expression.Parent : parenthesized.Operand; - continue; - case IConversionOperation { IsImplicit: true } conversion: - expression = towardsParent ? expression.Parent : conversion.Operand; - continue; - default: - return expression; - } } } + } - private void UnwrapFormatString( - IVirtualCharService virtualCharService, ISyntaxFacts syntaxFacts, IOperation expression, out IOperation unwrapped, - out string? formatString, List unnecessarySpans) - { - Contract.ThrowIfNull(expression.SemanticModel); + private void UnwrapFormatString( + IVirtualCharService virtualCharService, ISyntaxFacts syntaxFacts, IOperation expression, out IOperation unwrapped, + out string? formatString, List unnecessarySpans) + { + Contract.ThrowIfNull(expression.SemanticModel); - if (expression is IInvocationOperation { TargetMethod.Name: nameof(ToString) } invocation && - HasNonImplicitInstance(invocation, out var instance) && - !syntaxFacts.IsBaseExpression(instance.Syntax) && - instance.Type != null && - !instance.Type.IsRefLikeType) + if (expression is IInvocationOperation { TargetMethod.Name: nameof(ToString) } invocation && + HasNonImplicitInstance(invocation, out var instance) && + !syntaxFacts.IsBaseExpression(instance.Syntax) && + instance.Type != null && + !instance.Type.IsRefLikeType) + { + if (invocation.Arguments.Length == 1 + || (invocation.Arguments.Length == 2 && UsesInvariantCultureReferenceInsideFormattableStringInvariant(invocation, formatProviderArgumentIndex: 1))) { - if (invocation.Arguments.Length == 1 - || (invocation.Arguments.Length == 2 && UsesInvariantCultureReferenceInsideFormattableStringInvariant(invocation, formatProviderArgumentIndex: 1))) + if (invocation.Arguments[0].Value is ILiteralOperation { ConstantValue: { HasValue: true, Value: string value } } literal && + FindType(expression.SemanticModel) is { } systemIFormattable && + instance.Type.Implements(systemIFormattable)) { - if (invocation.Arguments[0].Value is ILiteralOperation { ConstantValue: { HasValue: true, Value: string value } } literal && - FindType(expression.SemanticModel) is { } systemIFormattable && - instance.Type.Implements(systemIFormattable)) - { - unwrapped = instance; - formatString = value; - - unnecessarySpans.AddRange(invocation.Syntax.Span - .Subtract(GetPreservedInterpolationExpressionSyntax(instance).FullSpan) - .Subtract(GetSpanWithinLiteralQuotes(virtualCharService, literal.Syntax.GetFirstToken()))); - return; - } - } - - if (IsObjectToStringOverride(invocation.TargetMethod) - || (invocation.Arguments.Length == 1 && UsesInvariantCultureReferenceInsideFormattableStringInvariant(invocation, formatProviderArgumentIndex: 0))) - { - // A call to `.ToString()` at the end of the interpolation. This is unnecessary. - // Just remove entirely. unwrapped = instance; - formatString = ""; + formatString = value; unnecessarySpans.AddRange(invocation.Syntax.Span - .Subtract(GetPreservedInterpolationExpressionSyntax(instance).FullSpan)); + .Subtract(GetPreservedInterpolationExpressionSyntax(instance).FullSpan) + .Subtract(GetSpanWithinLiteralQuotes(virtualCharService, literal.Syntax.GetFirstToken()))); return; } } - unwrapped = expression; - formatString = null; + if (IsObjectToStringOverride(invocation.TargetMethod) + || (invocation.Arguments.Length == 1 && UsesInvariantCultureReferenceInsideFormattableStringInvariant(invocation, formatProviderArgumentIndex: 0))) + { + // A call to `.ToString()` at the end of the interpolation. This is unnecessary. + // Just remove entirely. + unwrapped = instance; + formatString = ""; + + unnecessarySpans.AddRange(invocation.Syntax.Span + .Subtract(GetPreservedInterpolationExpressionSyntax(instance).FullSpan)); + return; + } } - private static bool IsObjectToStringOverride(IMethodSymbol method) - { - while (method.OverriddenMethod is not null) - method = method.OverriddenMethod; + unwrapped = expression; + formatString = null; + } - return method.ContainingType.SpecialType == SpecialType.System_Object - && method.Name == nameof(ToString); - } + private static bool IsObjectToStringOverride(IMethodSymbol method) + { + while (method.OverriddenMethod is not null) + method = method.OverriddenMethod; - private static bool UsesInvariantCultureReferenceInsideFormattableStringInvariant(IInvocationOperation invocation, int formatProviderArgumentIndex) - { - return IsInvariantCultureReference(invocation.Arguments[formatProviderArgumentIndex].Value) - && IsInsideFormattableStringInvariant(invocation); - } + return method.ContainingType.SpecialType == SpecialType.System_Object + && method.Name == nameof(ToString); + } - private static bool IsInvariantCultureReference(IOperation operation) + private static bool UsesInvariantCultureReferenceInsideFormattableStringInvariant(IInvocationOperation invocation, int formatProviderArgumentIndex) + { + return IsInvariantCultureReference(invocation.Arguments[formatProviderArgumentIndex].Value) + && IsInsideFormattableStringInvariant(invocation); + } + + private static bool IsInvariantCultureReference(IOperation operation) + { + Contract.ThrowIfNull(operation.SemanticModel); + + if (Unwrap(operation) is IPropertyReferenceOperation { Member: { } member }) { - Contract.ThrowIfNull(operation.SemanticModel); + if (member.Name == nameof(CultureInfo.InvariantCulture)) + return IsType(member.ContainingType, operation.SemanticModel); - if (Unwrap(operation) is IPropertyReferenceOperation { Member: { } member }) + if (member.Name == "InvariantInfo") { - if (member.Name == nameof(CultureInfo.InvariantCulture)) - return IsType(member.ContainingType, operation.SemanticModel); - - if (member.Name == "InvariantInfo") - { - return IsType(member.ContainingType, operation.SemanticModel) - || IsType(member.ContainingType, operation.SemanticModel); - } + return IsType(member.ContainingType, operation.SemanticModel) + || IsType(member.ContainingType, operation.SemanticModel); } - - return false; } - private static bool IsInsideFormattableStringInvariant(IOperation operation) - { - Contract.ThrowIfNull(operation.SemanticModel); + return false; + } + + private static bool IsInsideFormattableStringInvariant(IOperation operation) + { + Contract.ThrowIfNull(operation.SemanticModel); - var interpolatedStringOperation = AncestorsAndSelf(operation).OfType().FirstOrDefault(); + var interpolatedStringOperation = AncestorsAndSelf(operation).OfType().FirstOrDefault(); - return Unwrap(interpolatedStringOperation?.Parent, towardsParent: true) is IArgumentOperation + return Unwrap(interpolatedStringOperation?.Parent, towardsParent: true) is IArgumentOperation + { + Parent: IInvocationOperation { - Parent: IInvocationOperation - { - TargetMethod: { Name: nameof(FormattableString.Invariant), ContainingType: var containingType }, - }, - } && IsType(containingType, operation.SemanticModel); - } + TargetMethod: { Name: nameof(FormattableString.Invariant), ContainingType: var containingType }, + }, + } && IsType(containingType, operation.SemanticModel); + } - private static bool IsType(INamedTypeSymbol type, SemanticModel semanticModel) - => SymbolEqualityComparer.Default.Equals(type, FindType(semanticModel)); + private static bool IsType(INamedTypeSymbol type, SemanticModel semanticModel) + => SymbolEqualityComparer.Default.Equals(type, FindType(semanticModel)); - private static INamedTypeSymbol? FindType(SemanticModel semanticModel) - => semanticModel.Compilation.GetTypeByMetadataName(typeof(T).FullName!); + private static INamedTypeSymbol? FindType(SemanticModel semanticModel) + => semanticModel.Compilation.GetTypeByMetadataName(typeof(T).FullName!); - private static IEnumerable AncestorsAndSelf(IOperation operation) - { - for (var current = operation; current is not null; current = current.Parent) - yield return current; - } + private static IEnumerable AncestorsAndSelf(IOperation operation) + { + for (var current = operation; current is not null; current = current.Parent) + yield return current; + } - private static TextSpan GetSpanWithinLiteralQuotes(IVirtualCharService virtualCharService, SyntaxToken formatToken) - { - var sequence = virtualCharService.TryConvertToVirtualChars(formatToken); - return sequence.IsDefaultOrEmpty - ? default - : TextSpan.FromBounds(sequence.First().Span.Start, sequence.Last().Span.End); - } + private static TextSpan GetSpanWithinLiteralQuotes(IVirtualCharService virtualCharService, SyntaxToken formatToken) + { + var sequence = virtualCharService.TryConvertToVirtualChars(formatToken); + return sequence.IsDefaultOrEmpty + ? default + : TextSpan.FromBounds(sequence.First().Span.Start, sequence.Last().Span.End); + } - private void UnwrapAlignmentPadding( - IOperation expression, out IOperation unwrapped, - out TExpressionSyntax? alignment, out bool negate, List unnecessarySpans) - where TExpressionSyntax : SyntaxNode + private void UnwrapAlignmentPadding( + IOperation expression, out IOperation unwrapped, + out TExpressionSyntax? alignment, out bool negate, List unnecessarySpans) + where TExpressionSyntax : SyntaxNode + { + if (expression is IInvocationOperation invocation && + HasNonImplicitInstance(invocation, out var instance)) { - if (expression is IInvocationOperation invocation && - HasNonImplicitInstance(invocation, out var instance)) + var targetName = invocation.TargetMethod.Name; + if (targetName is nameof(string.PadLeft) or nameof(string.PadRight)) { - var targetName = invocation.TargetMethod.Name; - if (targetName is nameof(string.PadLeft) or nameof(string.PadRight)) + var argCount = invocation.Arguments.Length; + if (argCount is 1 or 2) { - var argCount = invocation.Arguments.Length; - if (argCount is 1 or 2) + if (argCount == 1 || + IsSpaceChar(invocation.Arguments[1])) { - if (argCount == 1 || - IsSpaceChar(invocation.Arguments[1])) + var alignmentOp = invocation.Arguments[0].Value; + + if (PermitNonLiteralAlignmentComponents + ? alignmentOp is { ConstantValue.HasValue: true } + : alignmentOp is { Kind: OperationKind.Literal }) { - var alignmentOp = invocation.Arguments[0].Value; - - if (PermitNonLiteralAlignmentComponents - ? alignmentOp is { ConstantValue.HasValue: true } - : alignmentOp is { Kind: OperationKind.Literal }) - { - var alignmentSyntax = alignmentOp.Syntax; - - unwrapped = instance; - alignment = alignmentSyntax as TExpressionSyntax; - negate = targetName == nameof(string.PadRight); - - unnecessarySpans.AddRange(invocation.Syntax.Span - .Subtract(GetPreservedInterpolationExpressionSyntax(instance).FullSpan) - .Subtract(alignmentSyntax.FullSpan)); - return; - } + var alignmentSyntax = alignmentOp.Syntax; + + unwrapped = instance; + alignment = alignmentSyntax as TExpressionSyntax; + negate = targetName == nameof(string.PadRight); + + unnecessarySpans.AddRange(invocation.Syntax.Span + .Subtract(GetPreservedInterpolationExpressionSyntax(instance).FullSpan) + .Subtract(alignmentSyntax.FullSpan)); + return; } } } } - - unwrapped = expression; - alignment = null; - negate = false; } - private static bool HasNonImplicitInstance(IInvocationOperation invocation, [NotNullWhen(true)] out IOperation? instance) - { - if (invocation.Instance is { IsImplicit: false }) - { - instance = invocation.Instance; - return true; - } + unwrapped = expression; + alignment = null; + negate = false; + } - instance = null; - return false; + private static bool HasNonImplicitInstance(IInvocationOperation invocation, [NotNullWhen(true)] out IOperation? instance) + { + if (invocation.Instance is { IsImplicit: false }) + { + instance = invocation.Instance; + return true; } - private static bool IsSpaceChar(IArgumentOperation argument) - => argument.Value.ConstantValue is { HasValue: true, Value: ' ' }; + instance = null; + return false; } + + private static bool IsSpaceChar(IArgumentOperation argument) + => argument.Value.ConstantValue is { HasValue: true, Value: ' ' }; } diff --git a/src/Analyzers/Core/Analyzers/SimplifyLinqExpression/AbstractSimplifyLinqExpressionDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/SimplifyLinqExpression/AbstractSimplifyLinqExpressionDiagnosticAnalyzer.cs index 5392433954f53..a0ee7fca6c506 100644 --- a/src/Analyzers/Core/Analyzers/SimplifyLinqExpression/AbstractSimplifyLinqExpressionDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/SimplifyLinqExpression/AbstractSimplifyLinqExpressionDiagnosticAnalyzer.cs @@ -11,176 +11,175 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.SimplifyLinqExpression +namespace Microsoft.CodeAnalysis.SimplifyLinqExpression; + +internal abstract class AbstractSimplifyLinqExpressionDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TInvocationExpressionSyntax : SyntaxNode + where TMemberAccessExpressionSyntax : SyntaxNode { - internal abstract class AbstractSimplifyLinqExpressionDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TInvocationExpressionSyntax : SyntaxNode - where TMemberAccessExpressionSyntax : SyntaxNode + private static readonly IImmutableSet s_nonEnumerableReturningLinqMethodNames = + ImmutableHashSet.Create( + nameof(Enumerable.First), + nameof(Enumerable.Last), + nameof(Enumerable.Single), + nameof(Enumerable.Any), + nameof(Enumerable.Count), + nameof(Enumerable.SingleOrDefault), + nameof(Enumerable.FirstOrDefault), + nameof(Enumerable.LastOrDefault)); + + protected abstract ISyntaxFacts SyntaxFacts { get; } + + public AbstractSimplifyLinqExpressionDiagnosticAnalyzer() + : base(IDEDiagnosticIds.SimplifyLinqExpressionDiagnosticId, + EnforceOnBuildValues.SimplifyLinqExpression, + option: null, + title: new LocalizableResourceString(nameof(AnalyzersResources.Simplify_LINQ_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - private static readonly IImmutableSet s_nonEnumerableReturningLinqMethodNames = - ImmutableHashSet.Create( - nameof(Enumerable.First), - nameof(Enumerable.Last), - nameof(Enumerable.Single), - nameof(Enumerable.Any), - nameof(Enumerable.Count), - nameof(Enumerable.SingleOrDefault), - nameof(Enumerable.FirstOrDefault), - nameof(Enumerable.LastOrDefault)); - - protected abstract ISyntaxFacts SyntaxFacts { get; } - - public AbstractSimplifyLinqExpressionDiagnosticAnalyzer() - : base(IDEDiagnosticIds.SimplifyLinqExpressionDiagnosticId, - EnforceOnBuildValues.SimplifyLinqExpression, - option: null, - title: new LocalizableResourceString(nameof(AnalyzersResources.Simplify_LINQ_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + } - protected abstract IInvocationOperation? TryGetNextInvocationInChain(IInvocationOperation invocation); + protected abstract IInvocationOperation? TryGetNextInvocationInChain(IInvocationOperation invocation); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(OnCompilationStart); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(OnCompilationStart); - private void OnCompilationStart(CompilationStartAnalysisContext context) + private void OnCompilationStart(CompilationStartAnalysisContext context) + { + if (!TryGetEnumerableTypeSymbol(context.Compilation, out var enumerableType)) { - if (!TryGetEnumerableTypeSymbol(context.Compilation, out var enumerableType)) - { - return; - } + return; + } - if (!TryGetLinqWhereExtensionMethod(enumerableType, out var whereMethodSymbol)) - { - return; - } + if (!TryGetLinqWhereExtensionMethod(enumerableType, out var whereMethodSymbol)) + { + return; + } - if (!TryGetLinqMethodsThatDoNotReturnEnumerables(enumerableType, out var linqMethodSymbols)) - { - return; - } + if (!TryGetLinqMethodsThatDoNotReturnEnumerables(enumerableType, out var linqMethodSymbols)) + { + return; + } - context.RegisterOperationAction( - context => AnalyzeInvocationOperation(context, enumerableType, whereMethodSymbol, linqMethodSymbols), - OperationKind.Invocation); + context.RegisterOperationAction( + context => AnalyzeInvocationOperation(context, enumerableType, whereMethodSymbol, linqMethodSymbols), + OperationKind.Invocation); - return; + return; - static bool TryGetEnumerableTypeSymbol(Compilation compilation, [NotNullWhen(true)] out INamedTypeSymbol? enumerableType) - { - enumerableType = compilation.GetTypeByMetadataName(typeof(Enumerable)?.FullName!); - return enumerableType is not null; - } + static bool TryGetEnumerableTypeSymbol(Compilation compilation, [NotNullWhen(true)] out INamedTypeSymbol? enumerableType) + { + enumerableType = compilation.GetTypeByMetadataName(typeof(Enumerable)?.FullName!); + return enumerableType is not null; + } - static bool TryGetLinqWhereExtensionMethod(INamedTypeSymbol enumerableType, [NotNullWhen(true)] out IMethodSymbol? whereMethod) + static bool TryGetLinqWhereExtensionMethod(INamedTypeSymbol enumerableType, [NotNullWhen(true)] out IMethodSymbol? whereMethod) + { + foreach (var whereMethodSymbol in enumerableType.GetMembers(nameof(Enumerable.Where)).OfType()) { - foreach (var whereMethodSymbol in enumerableType.GetMembers(nameof(Enumerable.Where)).OfType()) + var parameters = whereMethodSymbol.Parameters; + + if (parameters is [_, { Type: INamedTypeSymbol { Arity: 2 } }]) { - var parameters = whereMethodSymbol.Parameters; - - if (parameters is [_, { Type: INamedTypeSymbol { Arity: 2 } }]) - { - // This is the where overload that does not take and index (i.e. Where(source, Func) vs Where(source, Func)) - whereMethod = whereMethodSymbol; - return true; - } + // This is the where overload that does not take and index (i.e. Where(source, Func) vs Where(source, Func)) + whereMethod = whereMethodSymbol; + return true; } - - whereMethod = null; - return false; } - static bool TryGetLinqMethodsThatDoNotReturnEnumerables(INamedTypeSymbol enumerableType, out ImmutableArray linqMethods) + whereMethod = null; + return false; + } + + static bool TryGetLinqMethodsThatDoNotReturnEnumerables(INamedTypeSymbol enumerableType, out ImmutableArray linqMethods) + { + using var _ = ArrayBuilder.GetInstance(out var linqMethodSymbolsBuilder); + foreach (var method in enumerableType.GetMembers().OfType()) { - using var _ = ArrayBuilder.GetInstance(out var linqMethodSymbolsBuilder); - foreach (var method in enumerableType.GetMembers().OfType()) + if (s_nonEnumerableReturningLinqMethodNames.Contains(method.Name) && + method.Parameters is { Length: 1 }) { - if (s_nonEnumerableReturningLinqMethodNames.Contains(method.Name) && - method.Parameters is { Length: 1 }) - { - linqMethodSymbolsBuilder.AddRange(method); - } + linqMethodSymbolsBuilder.AddRange(method); } - - linqMethods = linqMethodSymbolsBuilder.ToImmutable(); - return linqMethods.Any(); } + + linqMethods = linqMethodSymbolsBuilder.ToImmutable(); + return linqMethods.Any(); } + } - public void AnalyzeInvocationOperation(OperationAnalysisContext context, INamedTypeSymbol enumerableType, IMethodSymbol whereMethod, ImmutableArray linqMethods) - { - if (ShouldSkipAnalysis(context, notification: null)) - return; + public void AnalyzeInvocationOperation(OperationAnalysisContext context, INamedTypeSymbol enumerableType, IMethodSymbol whereMethod, ImmutableArray linqMethods) + { + if (ShouldSkipAnalysis(context, notification: null)) + return; - if (context.Operation.Syntax.GetDiagnostics().Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)) - { - // Do not analyze linq methods that contain diagnostics. - return; - } + if (context.Operation.Syntax.GetDiagnostics().Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)) + { + // Do not analyze linq methods that contain diagnostics. + return; + } - if (context.Operation is not IInvocationOperation invocation || - !IsWhereLinqMethod(invocation)) - { - // we only care about Where methods on linq expressions - return; - } + if (context.Operation is not IInvocationOperation invocation || + !IsWhereLinqMethod(invocation)) + { + // we only care about Where methods on linq expressions + return; + } - if (TryGetNextInvocationInChain(invocation) is not IInvocationOperation nextInvocation || - !IsInvocationNonEnumerableReturningLinqMethod(nextInvocation)) - { - // Invocation is not part of a chain of invocations (i.e. Where(x => x is not null).First()) - return; - } + if (TryGetNextInvocationInChain(invocation) is not IInvocationOperation nextInvocation || + !IsInvocationNonEnumerableReturningLinqMethod(nextInvocation)) + { + // Invocation is not part of a chain of invocations (i.e. Where(x => x is not null).First()) + return; + } - if (TryGetSymbolOfMemberAccess(invocation) is not INamedTypeSymbol targetTypeSymbol || - TryGetMethodName(nextInvocation) is not string name) - { - return; - } + if (TryGetSymbolOfMemberAccess(invocation) is not INamedTypeSymbol targetTypeSymbol || + TryGetMethodName(nextInvocation) is not string name) + { + return; + } - if (!targetTypeSymbol.Equals(enumerableType, SymbolEqualityComparer.Default) && - targetTypeSymbol.MemberNames.Contains(name)) - { - // Do not offer to transpose if there is already a member on the collection named the same as the linq extension method - // example: list.Where(x => x != null).Count() cannot be changed to list.Count(x => x != null) as List already has a member named Count - return; - } + if (!targetTypeSymbol.Equals(enumerableType, SymbolEqualityComparer.Default) && + targetTypeSymbol.MemberNames.Contains(name)) + { + // Do not offer to transpose if there is already a member on the collection named the same as the linq extension method + // example: list.Where(x => x != null).Count() cannot be changed to list.Count(x => x != null) as List already has a member named Count + return; + } - context.ReportDiagnostic(Diagnostic.Create(Descriptor, nextInvocation.Syntax.GetLocation())); + context.ReportDiagnostic(Diagnostic.Create(Descriptor, nextInvocation.Syntax.GetLocation())); - return; + return; - bool IsWhereLinqMethod(IInvocationOperation invocation) - => whereMethod.Equals(invocation.TargetMethod.ReducedFrom ?? invocation.TargetMethod.OriginalDefinition, SymbolEqualityComparer.Default); + bool IsWhereLinqMethod(IInvocationOperation invocation) + => whereMethod.Equals(invocation.TargetMethod.ReducedFrom ?? invocation.TargetMethod.OriginalDefinition, SymbolEqualityComparer.Default); - bool IsInvocationNonEnumerableReturningLinqMethod(IInvocationOperation invocation) - => linqMethods.Any(static (m, invocation) => m.Equals(invocation.TargetMethod.ReducedFrom ?? invocation.TargetMethod.OriginalDefinition, SymbolEqualityComparer.Default), invocation); + bool IsInvocationNonEnumerableReturningLinqMethod(IInvocationOperation invocation) + => linqMethods.Any(static (m, invocation) => m.Equals(invocation.TargetMethod.ReducedFrom ?? invocation.TargetMethod.OriginalDefinition, SymbolEqualityComparer.Default), invocation); - INamedTypeSymbol? TryGetSymbolOfMemberAccess(IInvocationOperation invocation) + INamedTypeSymbol? TryGetSymbolOfMemberAccess(IInvocationOperation invocation) + { + if (invocation.Syntax is TInvocationExpressionSyntax invocationNode && + SyntaxFacts.GetExpressionOfInvocationExpression(invocationNode) is TMemberAccessExpressionSyntax memberAccess && + SyntaxFacts.GetExpressionOfMemberAccessExpression(memberAccess) is SyntaxNode expression) { - if (invocation.Syntax is TInvocationExpressionSyntax invocationNode && - SyntaxFacts.GetExpressionOfInvocationExpression(invocationNode) is TMemberAccessExpressionSyntax memberAccess && - SyntaxFacts.GetExpressionOfMemberAccessExpression(memberAccess) is SyntaxNode expression) - { - return invocation.SemanticModel?.GetTypeInfo(expression).Type as INamedTypeSymbol; - } - - return null; + return invocation.SemanticModel?.GetTypeInfo(expression).Type as INamedTypeSymbol; } - string? TryGetMethodName(IInvocationOperation invocation) - { - if (invocation.Syntax is TInvocationExpressionSyntax invocationNode && - SyntaxFacts.GetExpressionOfInvocationExpression(invocationNode) is TMemberAccessExpressionSyntax memberAccess) - { - return SyntaxFacts.GetNameOfMemberAccessExpression(memberAccess).GetText().ToString(); - } + return null; + } - return null; + string? TryGetMethodName(IInvocationOperation invocation) + { + if (invocation.Syntax is TInvocationExpressionSyntax invocationNode && + SyntaxFacts.GetExpressionOfInvocationExpression(invocationNode) is TMemberAccessExpressionSyntax memberAccess) + { + return SyntaxFacts.GetNameOfMemberAccessExpression(memberAccess).GetText().ToString(); } + + return null; } } } diff --git a/src/Analyzers/Core/Analyzers/SimplifyTypeNames/SimplifyTypeNamesDiagnosticAnalyzerBase.cs b/src/Analyzers/Core/Analyzers/SimplifyTypeNames/SimplifyTypeNamesDiagnosticAnalyzerBase.cs index 12ee84f9f17e8..a37aa66a8d07f 100644 --- a/src/Analyzers/Core/Analyzers/SimplifyTypeNames/SimplifyTypeNamesDiagnosticAnalyzerBase.cs +++ b/src/Analyzers/Core/Analyzers/SimplifyTypeNames/SimplifyTypeNamesDiagnosticAnalyzerBase.cs @@ -25,290 +25,289 @@ using System.Text.RegularExpressions; #endif -namespace Microsoft.CodeAnalysis.SimplifyTypeNames +namespace Microsoft.CodeAnalysis.SimplifyTypeNames; + +internal abstract class SimplifyTypeNamesDiagnosticAnalyzerBase + : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer + where TLanguageKindEnum : struct + where TSimplifierOptions : SimplifierOptions { - internal abstract class SimplifyTypeNamesDiagnosticAnalyzerBase - : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer - where TLanguageKindEnum : struct - where TSimplifierOptions : SimplifierOptions - { #if LOG - private static string _logFile = @"c:\temp\simplifytypenames.txt"; - private static object _logGate = new object(); - private static readonly Regex s_newlinePattern = new Regex(@"[\r\n]+"); + private static string _logFile = @"c:\temp\simplifytypenames.txt"; + private static object _logGate = new object(); + private static readonly Regex s_newlinePattern = new Regex(@"[\r\n]+"); #endif - private static readonly LocalizableString s_localizableMessage = new LocalizableResourceString(nameof(AnalyzersResources.Name_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - - private static readonly LocalizableString s_localizableTitleSimplifyNames = new LocalizableResourceString(nameof(AnalyzersResources.Simplify_Names), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - private static readonly DiagnosticDescriptor s_descriptorSimplifyNames = CreateDescriptorWithId(IDEDiagnosticIds.SimplifyNamesDiagnosticId, - EnforceOnBuildValues.SimplifyNames, - hasAnyCodeStyleOption: false, - s_localizableTitleSimplifyNames, - s_localizableMessage, - isUnnecessary: true); - - private static readonly LocalizableString s_localizableTitleSimplifyMemberAccess = new LocalizableResourceString(nameof(AnalyzersResources.Simplify_Member_Access), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - private static readonly DiagnosticDescriptor s_descriptorSimplifyMemberAccess = CreateDescriptorWithId(IDEDiagnosticIds.SimplifyMemberAccessDiagnosticId, - EnforceOnBuildValues.SimplifyMemberAccess, - hasAnyCodeStyleOption: false, - s_localizableTitleSimplifyMemberAccess, - s_localizableMessage, - isUnnecessary: true); - - private static readonly DiagnosticDescriptor s_descriptorPreferBuiltinOrFrameworkType = CreateDescriptorWithId(IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId, - EnforceOnBuildValues.PreferBuiltInOrFrameworkType, - hasAnyCodeStyleOption: true, - s_localizableTitleSimplifyNames, - s_localizableMessage, - isUnnecessary: true); - - protected SimplifyTypeNamesDiagnosticAnalyzerBase() - : base(ImmutableDictionary>.Empty - .Add(s_descriptorSimplifyNames, []) - .Add(s_descriptorSimplifyMemberAccess, []) - .Add(s_descriptorPreferBuiltinOrFrameworkType, - [ - CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInDeclaration, - CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess, - ]), - fadingOption: null) - { - } + private static readonly LocalizableString s_localizableMessage = new LocalizableResourceString(nameof(AnalyzersResources.Name_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + + private static readonly LocalizableString s_localizableTitleSimplifyNames = new LocalizableResourceString(nameof(AnalyzersResources.Simplify_Names), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + private static readonly DiagnosticDescriptor s_descriptorSimplifyNames = CreateDescriptorWithId(IDEDiagnosticIds.SimplifyNamesDiagnosticId, + EnforceOnBuildValues.SimplifyNames, + hasAnyCodeStyleOption: false, + s_localizableTitleSimplifyNames, + s_localizableMessage, + isUnnecessary: true); + + private static readonly LocalizableString s_localizableTitleSimplifyMemberAccess = new LocalizableResourceString(nameof(AnalyzersResources.Simplify_Member_Access), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + private static readonly DiagnosticDescriptor s_descriptorSimplifyMemberAccess = CreateDescriptorWithId(IDEDiagnosticIds.SimplifyMemberAccessDiagnosticId, + EnforceOnBuildValues.SimplifyMemberAccess, + hasAnyCodeStyleOption: false, + s_localizableTitleSimplifyMemberAccess, + s_localizableMessage, + isUnnecessary: true); + + private static readonly DiagnosticDescriptor s_descriptorPreferBuiltinOrFrameworkType = CreateDescriptorWithId(IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId, + EnforceOnBuildValues.PreferBuiltInOrFrameworkType, + hasAnyCodeStyleOption: true, + s_localizableTitleSimplifyNames, + s_localizableMessage, + isUnnecessary: true); + + protected SimplifyTypeNamesDiagnosticAnalyzerBase() + : base(ImmutableDictionary>.Empty + .Add(s_descriptorSimplifyNames, []) + .Add(s_descriptorSimplifyMemberAccess, []) + .Add(s_descriptorPreferBuiltinOrFrameworkType, + [ + CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInDeclaration, + CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess, + ]), + fadingOption: null) + { + } - internal abstract bool IsCandidate(SyntaxNode node); - internal abstract bool CanSimplifyTypeNameExpression( - SemanticModel model, SyntaxNode node, TSimplifierOptions options, - out TextSpan issueSpan, out string diagnosticId, out bool inDeclaration, - CancellationToken cancellationToken); + internal abstract bool IsCandidate(SyntaxNode node); + internal abstract bool CanSimplifyTypeNameExpression( + SemanticModel model, SyntaxNode node, TSimplifierOptions options, + out TextSpan issueSpan, out string diagnosticId, out bool inDeclaration, + CancellationToken cancellationToken); - public override bool OpenFileOnly(SimplifierOptions? options) - { - // analyzer is only active in C# and VB projects - Contract.ThrowIfNull(options); + public override bool OpenFileOnly(SimplifierOptions? options) + { + // analyzer is only active in C# and VB projects + Contract.ThrowIfNull(options); - return - !(options.PreferPredefinedTypeKeywordInDeclaration.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || - options.PreferPredefinedTypeKeywordInMemberAccess.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error); - } + return + !(options.PreferPredefinedTypeKeywordInDeclaration.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error || + options.PreferPredefinedTypeKeywordInMemberAccess.Notification.Severity is ReportDiagnostic.Warn or ReportDiagnostic.Error); + } - protected static ImmutableArray GetAllNotifications(SimplifierOptions options) - => [ - options.PreferPredefinedTypeKeywordInDeclaration.Notification, - options.PreferPredefinedTypeKeywordInMemberAccess.Notification, - ]; + protected static ImmutableArray GetAllNotifications(SimplifierOptions options) + => [ + options.PreferPredefinedTypeKeywordInDeclaration.Notification, + options.PreferPredefinedTypeKeywordInMemberAccess.Notification, + ]; - protected sealed override void InitializeWorker(AnalysisContext context) - { - context.RegisterCompilationStartAction(AnalyzeCompilation); - } + protected sealed override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(AnalyzeCompilation); + } - private void AnalyzeCompilation(CompilationStartAnalysisContext context) + private void AnalyzeCompilation(CompilationStartAnalysisContext context) + { + var analyzer = new AnalyzerImpl(this); + context.RegisterCodeBlockAction(analyzer.AnalyzeCodeBlock); + context.RegisterSemanticModelAction(analyzer.AnalyzeSemanticModel); + } + + /// + /// Determine if a code block is eligible for analysis by . + /// + /// The syntax node provided via . + /// if the code block should be analyzed by ; + /// otherwise, to skip analysis of the block. If a block is skipped, one or more child + /// blocks may be analyzed by , and any remaining spans can be analyzed by + /// . + protected abstract bool IsIgnoredCodeBlock(SyntaxNode codeBlock); + protected abstract ImmutableArray AnalyzeCodeBlock(CodeBlockAnalysisContext context, SyntaxNode root); + protected abstract ImmutableArray AnalyzeSemanticModel(SemanticModelAnalysisContext context, SyntaxNode root, TextSpanIntervalTree? codeBlockIntervalTree); + + public bool TrySimplify(SemanticModel model, SyntaxNode node, [NotNullWhen(true)] out Diagnostic? diagnostic, TSimplifierOptions options, AnalyzerOptions analyzerOptions, CancellationToken cancellationToken) + { + if (!CanSimplifyTypeNameExpression( + model, node, options, + out var issueSpan, out var diagnosticId, out var inDeclaration, + cancellationToken)) { - var analyzer = new AnalyzerImpl(this); - context.RegisterCodeBlockAction(analyzer.AnalyzeCodeBlock); - context.RegisterSemanticModelAction(analyzer.AnalyzeSemanticModel); + diagnostic = null; + return false; } - /// - /// Determine if a code block is eligible for analysis by . - /// - /// The syntax node provided via . - /// if the code block should be analyzed by ; - /// otherwise, to skip analysis of the block. If a block is skipped, one or more child - /// blocks may be analyzed by , and any remaining spans can be analyzed by - /// . - protected abstract bool IsIgnoredCodeBlock(SyntaxNode codeBlock); - protected abstract ImmutableArray AnalyzeCodeBlock(CodeBlockAnalysisContext context, SyntaxNode root); - protected abstract ImmutableArray AnalyzeSemanticModel(SemanticModelAnalysisContext context, SyntaxNode root, TextSpanIntervalTree? codeBlockIntervalTree); - - public bool TrySimplify(SemanticModel model, SyntaxNode node, [NotNullWhen(true)] out Diagnostic? diagnostic, TSimplifierOptions options, CancellationToken cancellationToken) + if (model.SyntaxTree.OverlapsHiddenPosition(issueSpan, cancellationToken)) { - if (!CanSimplifyTypeNameExpression( - model, node, options, - out var issueSpan, out var diagnosticId, out var inDeclaration, - cancellationToken)) - { - diagnostic = null; - return false; - } - - if (model.SyntaxTree.OverlapsHiddenPosition(issueSpan, cancellationToken)) - { - diagnostic = null; - return false; - } - - diagnostic = CreateDiagnostic(model, options, issueSpan, diagnosticId, inDeclaration); - return true; + diagnostic = null; + return false; } - internal static Diagnostic CreateDiagnostic(SemanticModel model, TSimplifierOptions options, TextSpan issueSpan, string diagnosticId, bool inDeclaration) + diagnostic = CreateDiagnostic(model, options, analyzerOptions, issueSpan, diagnosticId, inDeclaration); + return true; + } + + internal static Diagnostic CreateDiagnostic(SemanticModel model, TSimplifierOptions options, AnalyzerOptions analyzerOptions, TextSpan issueSpan, string diagnosticId, bool inDeclaration) + { + DiagnosticDescriptor descriptor; + NotificationOption2 notificationOption; + switch (diagnosticId) { - DiagnosticDescriptor descriptor; - NotificationOption2 notificationOption; - switch (diagnosticId) - { - case IDEDiagnosticIds.SimplifyNamesDiagnosticId: - descriptor = s_descriptorSimplifyNames; - notificationOption = descriptor.DefaultSeverity.ToNotificationOption(isOverridenSeverity: false); - break; - - case IDEDiagnosticIds.SimplifyMemberAccessDiagnosticId: - descriptor = s_descriptorSimplifyMemberAccess; - notificationOption = descriptor.DefaultSeverity.ToNotificationOption(isOverridenSeverity: false); - break; - - case IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId: - var optionValue = inDeclaration - ? options.PreferPredefinedTypeKeywordInDeclaration - : options.PreferPredefinedTypeKeywordInMemberAccess; - - descriptor = s_descriptorPreferBuiltinOrFrameworkType; - notificationOption = optionValue.Notification; - break; - default: - throw ExceptionUtilities.UnexpectedValue(diagnosticId); - } + case IDEDiagnosticIds.SimplifyNamesDiagnosticId: + descriptor = s_descriptorSimplifyNames; + notificationOption = descriptor.DefaultSeverity.ToNotificationOption(isOverridenSeverity: false); + break; + + case IDEDiagnosticIds.SimplifyMemberAccessDiagnosticId: + descriptor = s_descriptorSimplifyMemberAccess; + notificationOption = descriptor.DefaultSeverity.ToNotificationOption(isOverridenSeverity: false); + break; + + case IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId: + var optionValue = inDeclaration + ? options.PreferPredefinedTypeKeywordInDeclaration + : options.PreferPredefinedTypeKeywordInMemberAccess; + + descriptor = s_descriptorPreferBuiltinOrFrameworkType; + notificationOption = optionValue.Notification; + break; + default: + throw ExceptionUtilities.UnexpectedValue(diagnosticId); + } - var tree = model.SyntaxTree; - var builder = ImmutableDictionary.CreateBuilder(); - builder["OptionName"] = nameof(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess); // TODO: need the actual one - builder["OptionLanguage"] = model.Language; - var diagnostic = DiagnosticHelper.Create(descriptor, tree.GetLocation(issueSpan), notificationOption, additionalLocations: null, builder.ToImmutable()); + var tree = model.SyntaxTree; + var builder = ImmutableDictionary.CreateBuilder(); + builder["OptionName"] = nameof(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess); // TODO: need the actual one + builder["OptionLanguage"] = model.Language; + var diagnostic = DiagnosticHelper.Create(descriptor, tree.GetLocation(issueSpan), notificationOption, analyzerOptions, additionalLocations: null, builder.ToImmutable()); #if LOG - var sourceText = tree.GetText(); - sourceText.GetLineAndOffset(issueSpan.Start, out var startLineNumber, out var startOffset); - sourceText.GetLineAndOffset(issueSpan.End, out var endLineNumber, out var endOffset); - var logLine = tree.FilePath + "," + startLineNumber + "\t" + diagnosticId + "\t" + inDeclaration + "\t"; + var sourceText = tree.GetText(); + sourceText.GetLineAndOffset(issueSpan.Start, out var startLineNumber, out var startOffset); + sourceText.GetLineAndOffset(issueSpan.End, out var endLineNumber, out var endOffset); + var logLine = tree.FilePath + "," + startLineNumber + "\t" + diagnosticId + "\t" + inDeclaration + "\t"; - var leading = sourceText.ToString(TextSpan.FromBounds( - sourceText.Lines[startLineNumber].Start, issueSpan.Start)); - var mid = sourceText.ToString(issueSpan); - var trailing = sourceText.ToString(TextSpan.FromBounds( - issueSpan.End, sourceText.Lines[endLineNumber].End)); + var leading = sourceText.ToString(TextSpan.FromBounds( + sourceText.Lines[startLineNumber].Start, issueSpan.Start)); + var mid = sourceText.ToString(issueSpan); + var trailing = sourceText.ToString(TextSpan.FromBounds( + issueSpan.End, sourceText.Lines[endLineNumber].End)); - var contents = leading + "[|" + s_newlinePattern.Replace(mid, " ") + "|]" + trailing; - logLine += contents + "\r\n"; + var contents = leading + "[|" + s_newlinePattern.Replace(mid, " ") + "|]" + trailing; + logLine += contents + "\r\n"; - lock (_logGate) - { - File.AppendAllText(_logFile, logLine); - } + lock (_logGate) + { + File.AppendAllText(_logFile, logLine); + } #endif - return diagnostic; - } + return diagnostic; + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - private class AnalyzerImpl(SimplifyTypeNamesDiagnosticAnalyzerBase analyzer) + private class AnalyzerImpl(SimplifyTypeNamesDiagnosticAnalyzerBase analyzer) + { + private readonly SimplifyTypeNamesDiagnosticAnalyzerBase _analyzer = analyzer; + + /// + /// Tracks the analysis state of syntax trees in a compilation. Each syntax tree has the properties: + /// + /// + /// completed: to indicate that intervalTree has been obtained + /// for use in a callback; otherwise, to + /// indicate that intervalTree may be updated by adding a new non-overlapping + /// for analysis performed by a callback. + /// + /// This field also serves as the lock object for updating both completed and + /// intervalTree. + /// + /// + /// intervalTree: the set of intervals analyzed by + /// callbacks, and therefore do not need to be analyzed again by a + /// callback. + /// + /// This field may only be accessed while completed is locked, and is not valid after + /// completed is . + /// + /// + /// + private readonly ConcurrentDictionary completed, TextSpanIntervalTree? intervalTree)> _codeBlockIntervals = []; + + public void AnalyzeCodeBlock(CodeBlockAnalysisContext context) { - private readonly SimplifyTypeNamesDiagnosticAnalyzerBase _analyzer = analyzer; - - /// - /// Tracks the analysis state of syntax trees in a compilation. Each syntax tree has the properties: - /// - /// - /// completed: to indicate that intervalTree has been obtained - /// for use in a callback; otherwise, to - /// indicate that intervalTree may be updated by adding a new non-overlapping - /// for analysis performed by a callback. - /// - /// This field also serves as the lock object for updating both completed and - /// intervalTree. - /// - /// - /// intervalTree: the set of intervals analyzed by - /// callbacks, and therefore do not need to be analyzed again by a - /// callback. - /// - /// This field may only be accessed while completed is locked, and is not valid after - /// completed is . - /// - /// - /// - private readonly ConcurrentDictionary completed, TextSpanIntervalTree? intervalTree)> _codeBlockIntervals = []; - - public void AnalyzeCodeBlock(CodeBlockAnalysisContext context) - { - if (_analyzer.IsIgnoredCodeBlock(context.CodeBlock)) - return; + if (_analyzer.IsIgnoredCodeBlock(context.CodeBlock)) + return; - var (completed, intervalTree) = _codeBlockIntervals.GetOrAdd(context.CodeBlock.SyntaxTree, _ => (new StrongBox(false), new TextSpanIntervalTree())); - if (completed.Value) - return; + var (completed, intervalTree) = _codeBlockIntervals.GetOrAdd(context.CodeBlock.SyntaxTree, _ => (new StrongBox(false), new TextSpanIntervalTree())); + if (completed.Value) + return; - RoslynDebug.AssertNotNull(intervalTree); - if (!TryProceedWithInterval(addIfAvailable: false, context.CodeBlock.FullSpan, completed, intervalTree)) - return; + RoslynDebug.AssertNotNull(intervalTree); + if (!TryProceedWithInterval(addIfAvailable: false, context.CodeBlock.FullSpan, completed, intervalTree)) + return; - var root = context.GetAnalysisRoot(findInTrivia: true); - var diagnostics = _analyzer.AnalyzeCodeBlock(context, root); + var root = context.GetAnalysisRoot(findInTrivia: true); + var diagnostics = _analyzer.AnalyzeCodeBlock(context, root); - // After this point, cancellation is not allowed due to possible state alteration - if (!TryProceedWithInterval(addIfAvailable: root == context.CodeBlock, context.CodeBlock.FullSpan, completed, intervalTree)) - return; + // After this point, cancellation is not allowed due to possible state alteration + if (!TryProceedWithInterval(addIfAvailable: root == context.CodeBlock, context.CodeBlock.FullSpan, completed, intervalTree)) + return; - foreach (var diagnostic in diagnostics) - { - context.ReportDiagnostic(diagnostic); - } + foreach (var diagnostic in diagnostics) + { + context.ReportDiagnostic(diagnostic); + } - static bool TryProceedWithInterval(bool addIfAvailable, TextSpan span, StrongBox completed, TextSpanIntervalTree intervalTree) + static bool TryProceedWithInterval(bool addIfAvailable, TextSpan span, StrongBox completed, TextSpanIntervalTree intervalTree) + { + lock (completed) { - lock (completed) - { - if (completed.Value) - return false; + if (completed.Value) + return false; - if (intervalTree.HasIntervalThatOverlapsWith(span.Start, span.End)) - return false; + if (intervalTree.HasIntervalThatOverlapsWith(span.Start, span.End)) + return false; - if (addIfAvailable) - intervalTree.AddIntervalInPlace(span); + if (addIfAvailable) + intervalTree.AddIntervalInPlace(span); - return true; - } + return true; } } + } - public void AnalyzeSemanticModel(SemanticModelAnalysisContext context) + public void AnalyzeSemanticModel(SemanticModelAnalysisContext context) + { + // Get the state information for the syntax tree. If the state information is not available, it is + // initialized directly to a completed state, ensuring that concurrent (or future) calls to + // AnalyzeCodeBlock will always read completed==true, and intervalTree does not need to be initialized + // to a non-null value. + var (completed, intervalTree) = _codeBlockIntervals.GetOrAdd(context.SemanticModel.SyntaxTree, syntaxTree => (new StrongBox(true), null)); + + // Since SemanticModel callbacks only occur once per syntax tree, the completed state can be safely read + // here. It will have one of the values: + // + // false: the state was initialized in AnalyzeCodeBlock, and intervalTree will be a non-null tree. + // true: the state was initialized on the previous line, and either intervalTree will be null, or + // a previous call to AnalyzeSemanticModel was cancelled and the new one will operate on the + // same interval tree presented during the previous call. + if (!completed.Value && !context.FilterSpan.HasValue) { - // Get the state information for the syntax tree. If the state information is not available, it is - // initialized directly to a completed state, ensuring that concurrent (or future) calls to - // AnalyzeCodeBlock will always read completed==true, and intervalTree does not need to be initialized - // to a non-null value. - var (completed, intervalTree) = _codeBlockIntervals.GetOrAdd(context.SemanticModel.SyntaxTree, syntaxTree => (new StrongBox(true), null)); - - // Since SemanticModel callbacks only occur once per syntax tree, the completed state can be safely read - // here. It will have one of the values: - // - // false: the state was initialized in AnalyzeCodeBlock, and intervalTree will be a non-null tree. - // true: the state was initialized on the previous line, and either intervalTree will be null, or - // a previous call to AnalyzeSemanticModel was cancelled and the new one will operate on the - // same interval tree presented during the previous call. - if (!completed.Value && !context.FilterSpan.HasValue) + // This lock ensures we do not use intervalTree while it is being updated by a concurrent call to + // AnalyzeCodeBlock. + lock (completed) { - // This lock ensures we do not use intervalTree while it is being updated by a concurrent call to - // AnalyzeCodeBlock. - lock (completed) - { - // Prevent future code block callbacks from analyzing more spans within this tree - completed.Value = true; - } + // Prevent future code block callbacks from analyzing more spans within this tree + completed.Value = true; } + } - var root = context.GetAnalysisRoot(findInTrivia: true); - var diagnostics = _analyzer.AnalyzeSemanticModel(context, root, intervalTree); + var root = context.GetAnalysisRoot(findInTrivia: true); + var diagnostics = _analyzer.AnalyzeSemanticModel(context, root, intervalTree); - // After this point, cancellation is not allowed due to possible state alteration - foreach (var diagnostic in diagnostics) - { - context.ReportDiagnostic(diagnostic); - } + // After this point, cancellation is not allowed due to possible state alteration + foreach (var diagnostic in diagnostics) + { + context.ReportDiagnostic(diagnostic); } } } diff --git a/src/Analyzers/Core/Analyzers/UseAutoProperty/AbstractUseAutoPropertyAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseAutoProperty/AbstractUseAutoPropertyAnalyzer.cs index b7b6902565537..34f7e41e03429 100644 --- a/src/Analyzers/Core/Analyzers/UseAutoProperty/AbstractUseAutoPropertyAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseAutoProperty/AbstractUseAutoPropertyAnalyzer.cs @@ -402,6 +402,7 @@ private void Process(AnalysisResult result, SymbolAnalysisContext context) Descriptor, fieldNode.GetLocation(), result.Notification, + context.Options, additionalLocations: additionalLocations, properties: null); diff --git a/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForIfNullCheckDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForIfNullCheckDiagnosticAnalyzer.cs index 1d6ea22d25606..ce789da6231f1 100644 --- a/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForIfNullCheckDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForIfNullCheckDiagnosticAnalyzer.cs @@ -13,249 +13,249 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Analyzers.UseCoalesceExpression +namespace Microsoft.CodeAnalysis.Analyzers.UseCoalesceExpression; + +internal abstract class AbstractUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer< + TSyntaxKind, + TExpressionSyntax, + TStatementSyntax, + TVariableDeclarator, + TIfStatementSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct + where TExpressionSyntax : SyntaxNode + where TStatementSyntax : SyntaxNode + where TVariableDeclarator : SyntaxNode + where TIfStatementSyntax : TStatementSyntax { - internal abstract class AbstractUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer< - TSyntaxKind, - TExpressionSyntax, - TStatementSyntax, - TVariableDeclarator, - TIfStatementSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TSyntaxKind : struct - where TExpressionSyntax : SyntaxNode - where TStatementSyntax : SyntaxNode - where TVariableDeclarator : SyntaxNode - where TIfStatementSyntax : TStatementSyntax + protected AbstractUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseCoalesceExpressionForIfNullCheckDiagnosticId, + EnforceOnBuildValues.UseCoalesceExpression, + CodeStyleOptions2.PreferCoalesceExpression, + new LocalizableResourceString(nameof(AnalyzersResources.Use_coalesce_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - protected AbstractUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseCoalesceExpressionForIfNullCheckDiagnosticId, - EnforceOnBuildValues.UseCoalesceExpression, - CodeStyleOptions2.PreferCoalesceExpression, - new LocalizableResourceString(nameof(AnalyzersResources.Use_coalesce_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + } - protected abstract TSyntaxKind IfStatementKind { get; } - protected abstract ISyntaxFacts SyntaxFacts { get; } + protected abstract TSyntaxKind IfStatementKind { get; } + protected abstract ISyntaxFacts SyntaxFacts { get; } - protected abstract bool IsSingle(TVariableDeclarator declarator); - protected abstract bool IsNullCheck(TExpressionSyntax condition, [NotNullWhen(true)] out TExpressionSyntax? checkedExpression); - protected abstract bool HasElseBlock(TIfStatementSyntax ifStatement); + protected abstract bool IsSingle(TVariableDeclarator declarator); + protected abstract bool IsNullCheck(TExpressionSyntax condition, [NotNullWhen(true)] out TExpressionSyntax? checkedExpression); + protected abstract bool HasElseBlock(TIfStatementSyntax ifStatement); - protected abstract SyntaxNode GetDeclarationNode(TVariableDeclarator declarator); - protected abstract TExpressionSyntax GetConditionOfIfStatement(TIfStatementSyntax ifStatement); - protected abstract bool TryGetEmbeddedStatement(TIfStatementSyntax ifStatement, [NotNullWhen(true)] out TStatementSyntax? whenTrueStatement); + protected abstract SyntaxNode GetDeclarationNode(TVariableDeclarator declarator); + protected abstract TExpressionSyntax GetConditionOfIfStatement(TIfStatementSyntax ifStatement); + protected abstract bool TryGetEmbeddedStatement(TIfStatementSyntax ifStatement, [NotNullWhen(true)] out TStatementSyntax? whenTrueStatement); - protected abstract TStatementSyntax? TryGetPreviousStatement(TIfStatementSyntax ifStatement); + protected abstract TStatementSyntax? TryGetPreviousStatement(TIfStatementSyntax ifStatement); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeSyntax, this.IfStatementKind); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeSyntax, this.IfStatementKind); - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) - { - var cancellationToken = context.CancellationToken; - var ifStatement = (TIfStatementSyntax)context.Node; - var semanticModel = context.SemanticModel; + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var cancellationToken = context.CancellationToken; + var ifStatement = (TIfStatementSyntax)context.Node; + var semanticModel = context.SemanticModel; - var option = context.GetAnalyzerOptions().PreferCoalesceExpression; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; + var option = context.GetAnalyzerOptions().PreferCoalesceExpression; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; - var syntaxFacts = this.SyntaxFacts; - var condition = GetConditionOfIfStatement(ifStatement); + var syntaxFacts = this.SyntaxFacts; + var condition = GetConditionOfIfStatement(ifStatement); - if (!IsNullCheck(condition, out var checkedExpression)) - return; + if (!IsNullCheck(condition, out var checkedExpression)) + return; - var previousStatement = TryGetPreviousStatement(ifStatement); - if (previousStatement is null) - return; + var previousStatement = TryGetPreviousStatement(ifStatement); + if (previousStatement is null) + return; - if (HasElseBlock(ifStatement)) - return; + if (HasElseBlock(ifStatement)) + return; - if (!TryGetEmbeddedStatement(ifStatement, out var whenTrueStatement)) - return; + if (!TryGetEmbeddedStatement(ifStatement, out var whenTrueStatement)) + return; - if (syntaxFacts.IsThrowStatement(whenTrueStatement)) - { - if (!syntaxFacts.SupportsThrowExpression(ifStatement.SyntaxTree.Options)) - return; + if (syntaxFacts.IsThrowStatement(whenTrueStatement)) + { + if (!syntaxFacts.SupportsThrowExpression(ifStatement.SyntaxTree.Options)) + return; - var thrownExpression = syntaxFacts.GetExpressionOfThrowStatement(whenTrueStatement); - if (thrownExpression is null) - return; - } + var thrownExpression = syntaxFacts.GetExpressionOfThrowStatement(whenTrueStatement); + if (thrownExpression is null) + return; + } - TExpressionSyntax? expressionToCoalesce; + TExpressionSyntax? expressionToCoalesce; - if (syntaxFacts.IsLocalDeclarationStatement(previousStatement)) - { - // var v = Expr(); - // if (v == null) - // ... + if (syntaxFacts.IsLocalDeclarationStatement(previousStatement)) + { + // var v = Expr(); + // if (v == null) + // ... - if (!AnalyzeLocalDeclarationForm(previousStatement, out expressionToCoalesce)) - return; - } - else if (syntaxFacts.IsAnyAssignmentStatement(previousStatement)) - { - // v = Expr(); - // if (v == null) - // ... - if (!AnalyzeAssignmentForm(previousStatement, out expressionToCoalesce)) - return; - } - else - { + if (!AnalyzeLocalDeclarationForm(previousStatement, out expressionToCoalesce)) return; - } - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - ifStatement.GetFirstToken().GetLocation(), - option.Notification, - ImmutableArray.Create( - expressionToCoalesce.GetLocation(), - ifStatement.GetLocation(), - whenTrueStatement.GetLocation()), - properties: null)); - - bool AnalyzeLocalDeclarationForm( - TStatementSyntax localDeclarationStatement, - [NotNullWhen(true)] out TExpressionSyntax? expressionToCoalesce) - { - expressionToCoalesce = null; - - // var v = Expr(); - // if (v == null) - // ... + } + else if (syntaxFacts.IsAnyAssignmentStatement(previousStatement)) + { + // v = Expr(); + // if (v == null) + // ... + if (!AnalyzeAssignmentForm(previousStatement, out expressionToCoalesce)) + return; + } + else + { + return; + } - if (!syntaxFacts.IsIdentifierName(checkedExpression)) - return false; + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + ifStatement.GetFirstToken().GetLocation(), + option.Notification, + context.Options, + ImmutableArray.Create( + expressionToCoalesce.GetLocation(), + ifStatement.GetLocation(), + whenTrueStatement.GetLocation()), + properties: null)); + + bool AnalyzeLocalDeclarationForm( + TStatementSyntax localDeclarationStatement, + [NotNullWhen(true)] out TExpressionSyntax? expressionToCoalesce) + { + expressionToCoalesce = null; - var conditionIdentifier = syntaxFacts.GetIdentifierOfIdentifierName(checkedExpression).ValueText; + // var v = Expr(); + // if (v == null) + // ... - var declarators = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); - if (declarators.Count != 1) - return false; + if (!syntaxFacts.IsIdentifierName(checkedExpression)) + return false; - var declarator = (TVariableDeclarator)declarators[0]; - if (!IsSingle(declarator)) - return false; + var conditionIdentifier = syntaxFacts.GetIdentifierOfIdentifierName(checkedExpression).ValueText; - var equalsValue = syntaxFacts.GetInitializerOfVariableDeclarator(declarator); - if (equalsValue is null) - return false; + var declarators = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); + if (declarators.Count != 1) + return false; - if (syntaxFacts.GetValueOfEqualsValueClause(equalsValue) is not TExpressionSyntax initializer) - return false; + var declarator = (TVariableDeclarator)declarators[0]; + if (!IsSingle(declarator)) + return false; - expressionToCoalesce = initializer; + var equalsValue = syntaxFacts.GetInitializerOfVariableDeclarator(declarator); + if (equalsValue is null) + return false; - var variableName = syntaxFacts.GetIdentifierOfVariableDeclarator(declarator).ValueText; - if (conditionIdentifier != variableName) - return false; + if (syntaxFacts.GetValueOfEqualsValueClause(equalsValue) is not TExpressionSyntax initializer) + return false; - // if 'Expr()' is a value type, we can't use `??` on it. - var exprType = semanticModel.GetTypeInfo(initializer, cancellationToken).Type; - if (exprType is null || exprType.IsNonNullableValueType()) - return false; + expressionToCoalesce = initializer; - if (!IsLegalWhenTrueStatementForAssignment(out var whenPartToAnalyze)) - return false; + var variableName = syntaxFacts.GetIdentifierOfVariableDeclarator(declarator).ValueText; + if (conditionIdentifier != variableName) + return false; - // Looks good. However, make sure the when-true part doesn't access this symbol. We can't merge - // with the assignment then. - var localSymbol = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(GetDeclarationNode(declarator), cancellationToken); - foreach (var identifier in whenPartToAnalyze.DescendantNodesAndSelf()) - { - if (syntaxFacts.IsIdentifierName(identifier) && - syntaxFacts.GetIdentifierOfIdentifierName(identifier).ValueText == localSymbol.Name) - { - var symbol = semanticModel.GetSymbolInfo(identifier, cancellationToken).GetAnySymbol(); - if (Equals(localSymbol, symbol)) - return false; - } - } + // if 'Expr()' is a value type, we can't use `??` on it. + var exprType = semanticModel.GetTypeInfo(initializer, cancellationToken).Type; + if (exprType is null || exprType.IsNonNullableValueType()) + return false; - return true; + if (!IsLegalWhenTrueStatementForAssignment(out var whenPartToAnalyze)) + return false; - bool IsLegalWhenTrueStatementForAssignment([NotNullWhen(true)] out SyntaxNode? whenPartToAnalyze) + // Looks good. However, make sure the when-true part doesn't access this symbol. We can't merge + // with the assignment then. + var localSymbol = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(GetDeclarationNode(declarator), cancellationToken); + foreach (var identifier in whenPartToAnalyze.DescendantNodesAndSelf()) + { + if (syntaxFacts.IsIdentifierName(identifier) && + syntaxFacts.GetIdentifierOfIdentifierName(identifier).ValueText == localSymbol.Name) { - whenPartToAnalyze = whenTrueStatement; - - // var v = Expr(); - // if (v == null) - // throw ... - // - // can always convert this to `var v = Expr() ?? throw - if (syntaxFacts.IsThrowStatement(whenTrueStatement)) - return true; - - // var v = Expr(); - // if (v == null) - // v = ... - // - // can convert if embedded statement is assigning to same variable - if (syntaxFacts.IsSimpleAssignmentStatement(whenTrueStatement)) - { - syntaxFacts.GetPartsOfAssignmentStatement(whenTrueStatement, out var left, out var right); - if (syntaxFacts.IsIdentifierName(left)) - { - whenPartToAnalyze = right; - var leftName = syntaxFacts.GetIdentifierOfIdentifierName(left).ValueText; - return leftName == variableName; - } - } - - return false; + var symbol = semanticModel.GetSymbolInfo(identifier, cancellationToken).GetAnySymbol(); + if (Equals(localSymbol, symbol)) + return false; } } - bool AnalyzeAssignmentForm( - TStatementSyntax assignmentStatement, - [NotNullWhen(true)] out TExpressionSyntax? expressionToCoalesce) - { - expressionToCoalesce = null; - - // expr = Expr(); - // if (expr == null) - // ... - - syntaxFacts.GetPartsOfAssignmentStatement(assignmentStatement, out var topAssignmentLeft, out var topAssignmentRight); - if (!syntaxFacts.AreEquivalent(topAssignmentLeft, checkedExpression)) - return false; + return true; - expressionToCoalesce = topAssignmentRight as TExpressionSyntax; - if (expressionToCoalesce is null) - return false; + bool IsLegalWhenTrueStatementForAssignment([NotNullWhen(true)] out SyntaxNode? whenPartToAnalyze) + { + whenPartToAnalyze = whenTrueStatement; - // expr = Expr(); - // if (expr == null) + // var v = Expr(); + // if (v == null) // throw ... // // can always convert this to `var v = Expr() ?? throw if (syntaxFacts.IsThrowStatement(whenTrueStatement)) return true; - // expr = Expr(); - // if (expr == null) - // expr = ... + // var v = Expr(); + // if (v == null) + // v = ... // // can convert if embedded statement is assigning to same variable if (syntaxFacts.IsSimpleAssignmentStatement(whenTrueStatement)) { - syntaxFacts.GetPartsOfAssignmentStatement(whenTrueStatement, out var innerAssignmentLeft, out _); - return syntaxFacts.AreEquivalent(innerAssignmentLeft, checkedExpression); + syntaxFacts.GetPartsOfAssignmentStatement(whenTrueStatement, out var left, out var right); + if (syntaxFacts.IsIdentifierName(left)) + { + whenPartToAnalyze = right; + var leftName = syntaxFacts.GetIdentifierOfIdentifierName(left).ValueText; + return leftName == variableName; + } } return false; } } + + bool AnalyzeAssignmentForm( + TStatementSyntax assignmentStatement, + [NotNullWhen(true)] out TExpressionSyntax? expressionToCoalesce) + { + expressionToCoalesce = null; + + // expr = Expr(); + // if (expr == null) + // ... + + syntaxFacts.GetPartsOfAssignmentStatement(assignmentStatement, out var topAssignmentLeft, out var topAssignmentRight); + if (!syntaxFacts.AreEquivalent(topAssignmentLeft, checkedExpression)) + return false; + + expressionToCoalesce = topAssignmentRight as TExpressionSyntax; + if (expressionToCoalesce is null) + return false; + + // expr = Expr(); + // if (expr == null) + // throw ... + // + // can always convert this to `var v = Expr() ?? throw + if (syntaxFacts.IsThrowStatement(whenTrueStatement)) + return true; + + // expr = Expr(); + // if (expr == null) + // expr = ... + // + // can convert if embedded statement is assigning to same variable + if (syntaxFacts.IsSimpleAssignmentStatement(whenTrueStatement)) + { + syntaxFacts.GetPartsOfAssignmentStatement(whenTrueStatement, out var innerAssignmentLeft, out _); + return syntaxFacts.AreEquivalent(innerAssignmentLeft, checkedExpression); + } + + return false; + } } } diff --git a/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer.cs index 3e3d0be9f2d8c..515eb7d8a2efa 100644 --- a/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer.cs @@ -8,125 +8,125 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.UseCoalesceExpression +namespace Microsoft.CodeAnalysis.UseCoalesceExpression; + +/// +/// Looks for code of the form "!x.HasValue ? y : x.Value" and offers to convert it to "x ?? y"; +/// +internal abstract class AbstractUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer< + TSyntaxKind, + TExpressionSyntax, + TConditionalExpressionSyntax, + TBinaryExpressionSyntax, + TMemberAccessExpression, + TPrefixUnaryExpressionSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct + where TExpressionSyntax : SyntaxNode + where TConditionalExpressionSyntax : TExpressionSyntax + where TBinaryExpressionSyntax : TExpressionSyntax + where TMemberAccessExpression : TExpressionSyntax + where TPrefixUnaryExpressionSyntax : TExpressionSyntax { - /// - /// Looks for code of the form "!x.HasValue ? y : x.Value" and offers to convert it to "x ?? y"; - /// - internal abstract class AbstractUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer< - TSyntaxKind, - TExpressionSyntax, - TConditionalExpressionSyntax, - TBinaryExpressionSyntax, - TMemberAccessExpression, - TPrefixUnaryExpressionSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TSyntaxKind : struct - where TExpressionSyntax : SyntaxNode - where TConditionalExpressionSyntax : TExpressionSyntax - where TBinaryExpressionSyntax : TExpressionSyntax - where TMemberAccessExpression : TExpressionSyntax - where TPrefixUnaryExpressionSyntax : TExpressionSyntax + protected AbstractUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticId, + EnforceOnBuildValues.UseCoalesceExpressionForNullable, + CodeStyleOptions2.PreferCoalesceExpression, + new LocalizableResourceString(nameof(AnalyzersResources.Use_coalesce_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - protected AbstractUseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseCoalesceExpressionForNullableTernaryConditionalCheckDiagnosticId, - EnforceOnBuildValues.UseCoalesceExpressionForNullable, - CodeStyleOptions2.PreferCoalesceExpression, - new LocalizableResourceString(nameof(AnalyzersResources.Use_coalesce_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected abstract ISyntaxFacts GetSyntaxFacts(); - protected abstract bool IsTargetTyped(SemanticModel semanticModel, TConditionalExpressionSyntax conditional, System.Threading.CancellationToken cancellationToken); + protected abstract ISyntaxFacts GetSyntaxFacts(); + protected abstract bool IsTargetTyped(SemanticModel semanticModel, TConditionalExpressionSyntax conditional, System.Threading.CancellationToken cancellationToken); - protected override void InitializeWorker(AnalysisContext context) - { - var syntaxKinds = GetSyntaxFacts().SyntaxKinds; - context.RegisterSyntaxNodeAction(AnalyzeSyntax, - syntaxKinds.Convert(syntaxKinds.TernaryConditionalExpression)); - } + protected override void InitializeWorker(AnalysisContext context) + { + var syntaxKinds = GetSyntaxFacts().SyntaxKinds; + context.RegisterSyntaxNodeAction(AnalyzeSyntax, + syntaxKinds.Convert(syntaxKinds.TernaryConditionalExpression)); + } + + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var conditionalExpression = (TConditionalExpressionSyntax)context.Node; + + var cancellationToken = context.CancellationToken; + + var option = context.GetAnalyzerOptions().PreferCoalesceExpression; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; + + var syntaxFacts = GetSyntaxFacts(); + syntaxFacts.GetPartsOfConditionalExpression( + conditionalExpression, out var conditionNode, out var whenTrueNodeHigh, out var whenFalseNodeHigh); + + conditionNode = syntaxFacts.WalkDownParentheses(conditionNode); + var whenTrueNodeLow = syntaxFacts.WalkDownParentheses(whenTrueNodeHigh); + var whenFalseNodeLow = syntaxFacts.WalkDownParentheses(whenFalseNodeHigh); - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + var notHasValueExpression = false; + if (syntaxFacts.IsLogicalNotExpression(conditionNode)) { - var conditionalExpression = (TConditionalExpressionSyntax)context.Node; - - var cancellationToken = context.CancellationToken; - - var option = context.GetAnalyzerOptions().PreferCoalesceExpression; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; - - var syntaxFacts = GetSyntaxFacts(); - syntaxFacts.GetPartsOfConditionalExpression( - conditionalExpression, out var conditionNode, out var whenTrueNodeHigh, out var whenFalseNodeHigh); - - conditionNode = syntaxFacts.WalkDownParentheses(conditionNode); - var whenTrueNodeLow = syntaxFacts.WalkDownParentheses(whenTrueNodeHigh); - var whenFalseNodeLow = syntaxFacts.WalkDownParentheses(whenFalseNodeHigh); - - var notHasValueExpression = false; - if (syntaxFacts.IsLogicalNotExpression(conditionNode)) - { - notHasValueExpression = true; - conditionNode = syntaxFacts.GetOperandOfPrefixUnaryExpression(conditionNode); - } - - if (conditionNode is not TMemberAccessExpression conditionMemberAccess) - return; - - syntaxFacts.GetPartsOfMemberAccessExpression(conditionMemberAccess, out var conditionExpression, out var conditionSimpleName); - syntaxFacts.GetNameAndArityOfSimpleName(conditionSimpleName, out var conditionName, out _); - - if (conditionName != nameof(Nullable.HasValue)) - return; - - var whenPartToCheck = notHasValueExpression ? whenFalseNodeLow : whenTrueNodeLow; - if (whenPartToCheck is not TMemberAccessExpression whenPartMemberAccess) - return; - - syntaxFacts.GetPartsOfMemberAccessExpression(whenPartMemberAccess, out var whenPartExpression, out var whenPartSimpleName); - syntaxFacts.GetNameAndArityOfSimpleName(whenPartSimpleName, out var whenPartName, out _); - - if (whenPartName != nameof(Nullable.Value)) - return; - - if (!syntaxFacts.AreEquivalent(conditionExpression, whenPartExpression)) - return; - - // Coalesce expression cannot be target typed. So if we had a ternary that was target typed - // that means the individual parts themselves had no best common type, which would not work - // for a coalesce expression. - var semanticModel = context.SemanticModel; - if (IsTargetTyped(semanticModel, conditionalExpression, cancellationToken)) - return; - - // Syntactically this looks like something we can simplify. Make sure we're - // actually looking at something Nullable (and not some type that uses a similar - // syntactic pattern). - var nullableType = semanticModel.Compilation.GetTypeByMetadataName(typeof(Nullable<>).FullName!); - if (nullableType == null) - return; - - var type = semanticModel.GetTypeInfo(conditionExpression, cancellationToken); - - if (!nullableType.Equals(type.Type?.OriginalDefinition)) - return; - - var whenPartToKeep = notHasValueExpression ? whenTrueNodeHigh : whenFalseNodeHigh; - var locations = ImmutableArray.Create( - conditionalExpression.GetLocation(), - conditionExpression.GetLocation(), - whenPartToKeep.GetLocation()); - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - conditionalExpression.GetLocation(), - option.Notification, - locations, - properties: null)); + notHasValueExpression = true; + conditionNode = syntaxFacts.GetOperandOfPrefixUnaryExpression(conditionNode); } + + if (conditionNode is not TMemberAccessExpression conditionMemberAccess) + return; + + syntaxFacts.GetPartsOfMemberAccessExpression(conditionMemberAccess, out var conditionExpression, out var conditionSimpleName); + syntaxFacts.GetNameAndArityOfSimpleName(conditionSimpleName, out var conditionName, out _); + + if (conditionName != nameof(Nullable.HasValue)) + return; + + var whenPartToCheck = notHasValueExpression ? whenFalseNodeLow : whenTrueNodeLow; + if (whenPartToCheck is not TMemberAccessExpression whenPartMemberAccess) + return; + + syntaxFacts.GetPartsOfMemberAccessExpression(whenPartMemberAccess, out var whenPartExpression, out var whenPartSimpleName); + syntaxFacts.GetNameAndArityOfSimpleName(whenPartSimpleName, out var whenPartName, out _); + + if (whenPartName != nameof(Nullable.Value)) + return; + + if (!syntaxFacts.AreEquivalent(conditionExpression, whenPartExpression)) + return; + + // Coalesce expression cannot be target typed. So if we had a ternary that was target typed + // that means the individual parts themselves had no best common type, which would not work + // for a coalesce expression. + var semanticModel = context.SemanticModel; + if (IsTargetTyped(semanticModel, conditionalExpression, cancellationToken)) + return; + + // Syntactically this looks like something we can simplify. Make sure we're + // actually looking at something Nullable (and not some type that uses a similar + // syntactic pattern). + var nullableType = semanticModel.Compilation.GetTypeByMetadataName(typeof(Nullable<>).FullName!); + if (nullableType == null) + return; + + var type = semanticModel.GetTypeInfo(conditionExpression, cancellationToken); + + if (!nullableType.Equals(type.Type?.OriginalDefinition)) + return; + + var whenPartToKeep = notHasValueExpression ? whenTrueNodeHigh : whenFalseNodeHigh; + var locations = ImmutableArray.Create( + conditionalExpression.GetLocation(), + conditionExpression.GetLocation(), + whenPartToKeep.GetLocation()); + + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + conditionalExpression.GetLocation(), + option.Notification, + context.Options, + locations, + properties: null)); } } diff --git a/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer.cs index 0dd556a087580..db4a5b51d0b26 100644 --- a/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseCoalesceExpression/AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer.cs @@ -7,130 +7,130 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.UseCoalesceExpression +namespace Microsoft.CodeAnalysis.UseCoalesceExpression; + +/// +/// Looks for code of the form "x == null ? y : x" and offers to convert it to "x ?? y"; +/// +internal abstract class AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer< + TSyntaxKind, + TExpressionSyntax, + TConditionalExpressionSyntax, + TBinaryExpressionSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct + where TExpressionSyntax : SyntaxNode + where TConditionalExpressionSyntax : TExpressionSyntax + where TBinaryExpressionSyntax : TExpressionSyntax { - /// - /// Looks for code of the form "x == null ? y : x" and offers to convert it to "x ?? y"; - /// - internal abstract class AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer< - TSyntaxKind, - TExpressionSyntax, - TConditionalExpressionSyntax, - TBinaryExpressionSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TSyntaxKind : struct - where TExpressionSyntax : SyntaxNode - where TConditionalExpressionSyntax : TExpressionSyntax - where TBinaryExpressionSyntax : TExpressionSyntax + protected AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseCoalesceExpressionForTernaryConditionalCheckDiagnosticId, + EnforceOnBuildValues.UseCoalesceExpression, + CodeStyleOptions2.PreferCoalesceExpression, + new LocalizableResourceString(nameof(AnalyzersResources.Use_coalesce_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - protected AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseCoalesceExpressionForTernaryConditionalCheckDiagnosticId, - EnforceOnBuildValues.UseCoalesceExpression, - CodeStyleOptions2.PreferCoalesceExpression, - new LocalizableResourceString(nameof(AnalyzersResources.Use_coalesce_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected abstract ISyntaxFacts GetSyntaxFacts(); + protected abstract bool IsTargetTyped(SemanticModel semanticModel, TConditionalExpressionSyntax conditional, System.Threading.CancellationToken cancellationToken); + + protected override void InitializeWorker(AnalysisContext context) + { + var syntaxKinds = GetSyntaxFacts().SyntaxKinds; + context.RegisterSyntaxNodeAction(AnalyzeSyntax, + syntaxKinds.Convert(syntaxKinds.TernaryConditionalExpression)); + } + + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var cancellationToken = context.CancellationToken; + var conditionalExpression = (TConditionalExpressionSyntax)context.Node; + + var option = context.GetAnalyzerOptions().PreferCoalesceExpression; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; + + var syntaxFacts = GetSyntaxFacts(); + syntaxFacts.GetPartsOfConditionalExpression( + conditionalExpression, out var conditionNode, out var whenTrueNodeHigh, out var whenFalseNodeHigh); + + conditionNode = syntaxFacts.WalkDownParentheses(conditionNode); + var whenTrueNodeLow = syntaxFacts.WalkDownParentheses(whenTrueNodeHigh); + var whenFalseNodeLow = syntaxFacts.WalkDownParentheses(whenFalseNodeHigh); + + if (conditionNode is not TBinaryExpressionSyntax condition) + return; + + var syntaxKinds = syntaxFacts.SyntaxKinds; + var isEquals = syntaxKinds.ReferenceEqualsExpression == condition.RawKind; + var isNotEquals = syntaxKinds.ReferenceNotEqualsExpression == condition.RawKind; + if (!isEquals && !isNotEquals) + return; + + syntaxFacts.GetPartsOfBinaryExpression(condition, out var conditionLeftHigh, out var conditionRightHigh); + + var conditionLeftLow = syntaxFacts.WalkDownParentheses(conditionLeftHigh); + var conditionRightLow = syntaxFacts.WalkDownParentheses(conditionRightHigh); + + var conditionLeftIsNull = syntaxFacts.IsNullLiteralExpression(conditionLeftLow); + var conditionRightIsNull = syntaxFacts.IsNullLiteralExpression(conditionRightLow); + + if (conditionRightIsNull && conditionLeftIsNull) { + // null == null nothing to do here. + return; } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + if (!conditionRightIsNull && !conditionLeftIsNull) + return; - protected abstract ISyntaxFacts GetSyntaxFacts(); - protected abstract bool IsTargetTyped(SemanticModel semanticModel, TConditionalExpressionSyntax conditional, System.Threading.CancellationToken cancellationToken); - - protected override void InitializeWorker(AnalysisContext context) + if (!syntaxFacts.AreEquivalent( + conditionRightIsNull ? conditionLeftLow : conditionRightLow, + isEquals ? whenFalseNodeLow : whenTrueNodeLow)) { - var syntaxKinds = GetSyntaxFacts().SyntaxKinds; - context.RegisterSyntaxNodeAction(AnalyzeSyntax, - syntaxKinds.Convert(syntaxKinds.TernaryConditionalExpression)); + return; } - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + // Coalesce expression cannot be target typed. So if we had a ternary that was target typed + // that means the individual parts themselves had no best common type, which would not work + // for a coalesce expression. + var semanticModel = context.SemanticModel; + if (IsTargetTyped(semanticModel, conditionalExpression, cancellationToken)) + return; + + var conditionType = semanticModel.GetTypeInfo( + conditionLeftIsNull ? conditionRightLow : conditionLeftLow, cancellationToken).Type; + if (conditionType != null && + !conditionType.IsReferenceType) { - var cancellationToken = context.CancellationToken; - var conditionalExpression = (TConditionalExpressionSyntax)context.Node; - - var option = context.GetAnalyzerOptions().PreferCoalesceExpression; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; - - var syntaxFacts = GetSyntaxFacts(); - syntaxFacts.GetPartsOfConditionalExpression( - conditionalExpression, out var conditionNode, out var whenTrueNodeHigh, out var whenFalseNodeHigh); - - conditionNode = syntaxFacts.WalkDownParentheses(conditionNode); - var whenTrueNodeLow = syntaxFacts.WalkDownParentheses(whenTrueNodeHigh); - var whenFalseNodeLow = syntaxFacts.WalkDownParentheses(whenFalseNodeHigh); - - if (conditionNode is not TBinaryExpressionSyntax condition) - return; - - var syntaxKinds = syntaxFacts.SyntaxKinds; - var isEquals = syntaxKinds.ReferenceEqualsExpression == condition.RawKind; - var isNotEquals = syntaxKinds.ReferenceNotEqualsExpression == condition.RawKind; - if (!isEquals && !isNotEquals) - return; - - syntaxFacts.GetPartsOfBinaryExpression(condition, out var conditionLeftHigh, out var conditionRightHigh); - - var conditionLeftLow = syntaxFacts.WalkDownParentheses(conditionLeftHigh); - var conditionRightLow = syntaxFacts.WalkDownParentheses(conditionRightHigh); - - var conditionLeftIsNull = syntaxFacts.IsNullLiteralExpression(conditionLeftLow); - var conditionRightIsNull = syntaxFacts.IsNullLiteralExpression(conditionRightLow); - - if (conditionRightIsNull && conditionLeftIsNull) - { - // null == null nothing to do here. - return; - } - - if (!conditionRightIsNull && !conditionLeftIsNull) - return; - - if (!syntaxFacts.AreEquivalent( - conditionRightIsNull ? conditionLeftLow : conditionRightLow, - isEquals ? whenFalseNodeLow : whenTrueNodeLow)) - { - return; - } - - // Coalesce expression cannot be target typed. So if we had a ternary that was target typed - // that means the individual parts themselves had no best common type, which would not work - // for a coalesce expression. - var semanticModel = context.SemanticModel; - if (IsTargetTyped(semanticModel, conditionalExpression, cancellationToken)) - return; - - var conditionType = semanticModel.GetTypeInfo( - conditionLeftIsNull ? conditionRightLow : conditionLeftLow, cancellationToken).Type; - if (conditionType != null && - !conditionType.IsReferenceType) - { - // Note: it is intentional that we do not support nullable types here. If you have: - // - // int? x; - // var z = x == null ? y : x; - // - // then that's not the same as: x ?? y. ?? will unwrap the nullable, producing a - // int and not an int? like we have in the above code. - // - // Note: we could look for: x == null ? y : x.Value, and simplify that in the future. - return; - } - - var conditionPartToCheck = conditionRightIsNull ? conditionLeftHigh : conditionRightHigh; - var whenPartToCheck = isEquals ? whenTrueNodeHigh : whenFalseNodeHigh; - var locations = ImmutableArray.Create( - conditionalExpression.GetLocation(), - conditionPartToCheck.GetLocation(), - whenPartToCheck.GetLocation()); - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - conditionalExpression.GetLocation(), - option.Notification, - locations, - properties: null)); + // Note: it is intentional that we do not support nullable types here. If you have: + // + // int? x; + // var z = x == null ? y : x; + // + // then that's not the same as: x ?? y. ?? will unwrap the nullable, producing a + // int and not an int? like we have in the above code. + // + // Note: we could look for: x == null ? y : x.Value, and simplify that in the future. + return; } + + var conditionPartToCheck = conditionRightIsNull ? conditionLeftHigh : conditionRightHigh; + var whenPartToCheck = isEquals ? whenTrueNodeHigh : whenFalseNodeHigh; + var locations = ImmutableArray.Create( + conditionalExpression.GetLocation(), + conditionPartToCheck.GetLocation(), + whenPartToCheck.GetLocation()); + + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + conditionalExpression.GetLocation(), + option.Notification, + context.Options, + locations, + properties: null)); } } diff --git a/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs index 0d3f0fc8f7293..ea645fb31ee2a 100644 --- a/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs @@ -197,6 +197,7 @@ private void AnalyzeNode( s_descriptor, objectCreationExpression.GetFirstToken().GetLocation(), notification, + context.Options, additionalLocations: locations, properties)); @@ -266,6 +267,7 @@ private void FadeOutCode( s_unnecessaryCodeDescriptor, additionalUnnecessaryLocations[0], NotificationOption2.ForSeverity(s_unnecessaryCodeDescriptor.DefaultSeverity), + context.Options, additionalLocations: locations, additionalUnnecessaryLocations: additionalUnnecessaryLocations, properties)); diff --git a/src/Analyzers/Core/Analyzers/UseCompoundAssignment/AbstractUseCompoundAssignmentDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseCompoundAssignment/AbstractUseCompoundAssignmentDiagnosticAnalyzer.cs index f8753b12cecf4..7beb9fcc9df88 100644 --- a/src/Analyzers/Core/Analyzers/UseCompoundAssignment/AbstractUseCompoundAssignmentDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseCompoundAssignment/AbstractUseCompoundAssignmentDiagnosticAnalyzer.cs @@ -9,189 +9,191 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.UseCompoundAssignment +namespace Microsoft.CodeAnalysis.UseCompoundAssignment; + +internal abstract class AbstractUseCompoundAssignmentDiagnosticAnalyzer< + TSyntaxKind, + TAssignmentSyntax, + TBinaryExpressionSyntax> + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct + where TAssignmentSyntax : SyntaxNode + where TBinaryExpressionSyntax : SyntaxNode { - internal abstract class AbstractUseCompoundAssignmentDiagnosticAnalyzer< - TSyntaxKind, - TAssignmentSyntax, - TBinaryExpressionSyntax> - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TSyntaxKind : struct - where TAssignmentSyntax : SyntaxNode - where TBinaryExpressionSyntax : SyntaxNode + private readonly ISyntaxFacts _syntaxFacts; + + /// + /// Maps from a binary expression kind (like AddExpression) to the corresponding assignment + /// form (like AddAssignmentExpression). + /// + private readonly ImmutableDictionary _binaryToAssignmentMap; + + private readonly DiagnosticDescriptor _incrementDescriptor; + + private readonly DiagnosticDescriptor _decrementDescriptor; + + protected AbstractUseCompoundAssignmentDiagnosticAnalyzer( + ISyntaxFacts syntaxFacts, + ImmutableArray<(TSyntaxKind exprKind, TSyntaxKind assignmentKind, TSyntaxKind tokenKind)> kinds) + : base(IDEDiagnosticIds.UseCompoundAssignmentDiagnosticId, + EnforceOnBuildValues.UseCompoundAssignment, + CodeStyleOptions2.PreferCompoundAssignment, + new LocalizableResourceString( + nameof(AnalyzersResources.Use_compound_assignment), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - private readonly ISyntaxFacts _syntaxFacts; - - /// - /// Maps from a binary expression kind (like AddExpression) to the corresponding assignment - /// form (like AddAssignmentExpression). - /// - private readonly ImmutableDictionary _binaryToAssignmentMap; - - private readonly DiagnosticDescriptor _incrementDescriptor; - - private readonly DiagnosticDescriptor _decrementDescriptor; - - protected AbstractUseCompoundAssignmentDiagnosticAnalyzer( - ISyntaxFacts syntaxFacts, - ImmutableArray<(TSyntaxKind exprKind, TSyntaxKind assignmentKind, TSyntaxKind tokenKind)> kinds) - : base(IDEDiagnosticIds.UseCompoundAssignmentDiagnosticId, - EnforceOnBuildValues.UseCompoundAssignment, - CodeStyleOptions2.PreferCompoundAssignment, - new LocalizableResourceString( - nameof(AnalyzersResources.Use_compound_assignment), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - _syntaxFacts = syntaxFacts; - UseCompoundAssignmentUtilities.GenerateMaps(kinds, out _binaryToAssignmentMap, out _); - - var useIncrementMessage = new LocalizableResourceString( - nameof(AnalyzersResources.Use_increment_operator), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - _incrementDescriptor = CreateDescriptorWithId( - IDEDiagnosticIds.UseCompoundAssignmentDiagnosticId, - EnforceOnBuildValues.UseCompoundAssignment, - hasAnyCodeStyleOption: true, - useIncrementMessage, useIncrementMessage); - - var useDecrementMessage = new LocalizableResourceString( - nameof(AnalyzersResources.Use_decrement_operator), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); - _decrementDescriptor = CreateDescriptorWithId( - IDEDiagnosticIds.UseCompoundAssignmentDiagnosticId, - EnforceOnBuildValues.UseCompoundAssignment, - hasAnyCodeStyleOption: true, - useDecrementMessage, useDecrementMessage); - } + _syntaxFacts = syntaxFacts; + UseCompoundAssignmentUtilities.GenerateMaps(kinds, out _binaryToAssignmentMap, out _); + + var useIncrementMessage = new LocalizableResourceString( + nameof(AnalyzersResources.Use_increment_operator), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + _incrementDescriptor = CreateDescriptorWithId( + IDEDiagnosticIds.UseCompoundAssignmentDiagnosticId, + EnforceOnBuildValues.UseCompoundAssignment, + hasAnyCodeStyleOption: true, + useIncrementMessage, useIncrementMessage); + + var useDecrementMessage = new LocalizableResourceString( + nameof(AnalyzersResources.Use_decrement_operator), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + _decrementDescriptor = CreateDescriptorWithId( + IDEDiagnosticIds.UseCompoundAssignmentDiagnosticId, + EnforceOnBuildValues.UseCompoundAssignment, + hasAnyCodeStyleOption: true, + useDecrementMessage, useDecrementMessage); + } - protected abstract TSyntaxKind GetAnalysisKind(); - protected abstract bool IsSupported(TSyntaxKind assignmentKind, ParseOptions options); - protected abstract int TryGetIncrementOrDecrement(TSyntaxKind opKind, object constantValue); + protected abstract TSyntaxKind GetAnalysisKind(); + protected abstract bool IsSupported(TSyntaxKind assignmentKind, ParseOptions options); + protected abstract int TryGetIncrementOrDecrement(TSyntaxKind opKind, object constantValue); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterSyntaxNodeAction(AnalyzeAssignment, GetAnalysisKind()); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(AnalyzeAssignment, GetAnalysisKind()); - private void AnalyzeAssignment(SyntaxNodeAnalysisContext context) + private void AnalyzeAssignment(SyntaxNodeAnalysisContext context) + { + var assignment = (TAssignmentSyntax)context.Node; + var cancellationToken = context.CancellationToken; + + var syntaxTree = assignment.SyntaxTree; + var option = context.GetAnalyzerOptions().PreferCompoundAssignment; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) { - var assignment = (TAssignmentSyntax)context.Node; - var cancellationToken = context.CancellationToken; + // Bail immediately if the user has disabled this feature. + return; + } - var syntaxTree = assignment.SyntaxTree; - var option = context.GetAnalyzerOptions().PreferCompoundAssignment; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - { - // Bail immediately if the user has disabled this feature. - return; - } + _syntaxFacts.GetPartsOfAssignmentExpressionOrStatement(assignment, + out var assignmentLeft, out var assignmentToken, out var assignmentRight); - _syntaxFacts.GetPartsOfAssignmentExpressionOrStatement(assignment, - out var assignmentLeft, out var assignmentToken, out var assignmentRight); + assignmentRight = _syntaxFacts.WalkDownParentheses(assignmentRight); - assignmentRight = _syntaxFacts.WalkDownParentheses(assignmentRight); + // has to be of the form: a = b op c + // op has to be a form we could convert into op= + if (assignmentRight is not TBinaryExpressionSyntax binaryExpression) + { + return; + } - // has to be of the form: a = b op c - // op has to be a form we could convert into op= - if (assignmentRight is not TBinaryExpressionSyntax binaryExpression) - { - return; - } + var binaryKind = _syntaxFacts.SyntaxKinds.Convert(binaryExpression.RawKind); + if (!_binaryToAssignmentMap.ContainsKey(binaryKind)) + { + return; + } - var binaryKind = _syntaxFacts.SyntaxKinds.Convert(binaryExpression.RawKind); - if (!_binaryToAssignmentMap.ContainsKey(binaryKind)) - { - return; - } + // Requires at least C# 8 for Coalesce compound expression + if (!IsSupported(binaryKind, syntaxTree.Options)) + { + return; + } - // Requires at least C# 8 for Coalesce compound expression - if (!IsSupported(binaryKind, syntaxTree.Options)) - { - return; - } + _syntaxFacts.GetPartsOfBinaryExpression(binaryExpression, + out var binaryLeft, out var binaryRight); - _syntaxFacts.GetPartsOfBinaryExpression(binaryExpression, - out var binaryLeft, out var binaryRight); + // has to be of the form: expr = expr op ... + if (!_syntaxFacts.AreEquivalent(assignmentLeft, binaryLeft)) + { + return; + } - // has to be of the form: expr = expr op ... - if (!_syntaxFacts.AreEquivalent(assignmentLeft, binaryLeft)) - { - return; - } + // Don't offer if this is `x = x + 1` inside an obj initializer like: + // `new Point { x = x + 1 }` or + // `new () { x = x + 1 }` or + // `p with { x = x + 1 }` + if (_syntaxFacts.IsMemberInitializerNamedAssignmentIdentifier(assignmentLeft)) + { + return; + } - // Don't offer if this is `x = x + 1` inside an obj initializer like: - // `new Point { x = x + 1 }` or - // `new () { x = x + 1 }` or - // `p with { x = x + 1 }` - if (_syntaxFacts.IsMemberInitializerNamedAssignmentIdentifier(assignmentLeft)) - { - return; - } + // Don't offer if this is `x = x ?? throw new Exception()` + if (_syntaxFacts.IsThrowExpression(binaryRight)) + { + return; + } - // Don't offer if this is `x = x ?? throw new Exception()` - if (_syntaxFacts.IsThrowExpression(binaryRight)) - { - return; - } + // Syntactically looks promising. But we can only safely do this if 'expr' + // is side-effect-free since we will be changing the number of times it is + // executed from twice to once. + var semanticModel = context.SemanticModel; + if (!UseCompoundAssignmentUtilities.IsSideEffectFree( + _syntaxFacts, assignmentLeft, semanticModel, cancellationToken)) + { + return; + } - // Syntactically looks promising. But we can only safely do this if 'expr' - // is side-effect-free since we will be changing the number of times it is - // executed from twice to once. - var semanticModel = context.SemanticModel; - if (!UseCompoundAssignmentUtilities.IsSideEffectFree( - _syntaxFacts, assignmentLeft, semanticModel, cancellationToken)) + var constant = semanticModel.GetConstantValue(binaryRight, cancellationToken).Value; + if (constant != null) + { + var incrementOrDecrement = TryGetIncrementOrDecrement(binaryKind, constant); + if (incrementOrDecrement == 1) { - return; - } + var operation = (IBinaryOperation)semanticModel.GetRequiredOperation(binaryExpression, cancellationToken); - var constant = semanticModel.GetConstantValue(binaryRight, cancellationToken).Value; - if (constant != null) - { - var incrementOrDecrement = TryGetIncrementOrDecrement(binaryKind, constant); - if (incrementOrDecrement == 1) + // We can suggest using increment operator only if it is a built-in one (in such case `OperatorMethod` is null) + // or if increment operator is defined in the containing type + if (operation.OperatorMethod is null || + operation.OperatorMethod.ContainingType.GetMembers(WellKnownMemberNames.IncrementOperatorName).Length > 0) { - var operation = (IBinaryOperation)semanticModel.GetRequiredOperation(binaryExpression, cancellationToken); - - // We can suggest using increment operator only if it is a built-in one (in such case `OperatorMethod` is null) - // or if increment operator is defined in the containing type - if (operation.OperatorMethod is null || - operation.OperatorMethod.ContainingType.GetMembers(WellKnownMemberNames.IncrementOperatorName).Length > 0) - { - context.ReportDiagnostic(DiagnosticHelper.Create( - _incrementDescriptor, - assignmentToken.GetLocation(), - option.Notification, - additionalLocations: ImmutableArray.Create(assignment.GetLocation()), - properties: ImmutableDictionary.Create() - .Add(UseCompoundAssignmentUtilities.Increment, UseCompoundAssignmentUtilities.Increment))); - return; - } + context.ReportDiagnostic(DiagnosticHelper.Create( + _incrementDescriptor, + assignmentToken.GetLocation(), + option.Notification, + context.Options, + additionalLocations: ImmutableArray.Create(assignment.GetLocation()), + properties: ImmutableDictionary.Create() + .Add(UseCompoundAssignmentUtilities.Increment, UseCompoundAssignmentUtilities.Increment))); + return; } - else if (incrementOrDecrement == -1) + } + else if (incrementOrDecrement == -1) + { + var operation = (IBinaryOperation)semanticModel.GetRequiredOperation(binaryExpression, cancellationToken); + + // We can suggest using decrement operator only if it is a built-in one (in such case `OperatorMethod` is null) + // or if decrement operator is defined in the containing type + if (operation.OperatorMethod is null || + operation.OperatorMethod.ContainingType.GetMembers(WellKnownMemberNames.DecrementOperatorName).Length > 0) { - var operation = (IBinaryOperation)semanticModel.GetRequiredOperation(binaryExpression, cancellationToken); - - // We can suggest using decrement operator only if it is a built-in one (in such case `OperatorMethod` is null) - // or if decrement operator is defined in the containing type - if (operation.OperatorMethod is null || - operation.OperatorMethod.ContainingType.GetMembers(WellKnownMemberNames.DecrementOperatorName).Length > 0) - { - context.ReportDiagnostic(DiagnosticHelper.Create( - _decrementDescriptor, - assignmentToken.GetLocation(), - option.Notification, - additionalLocations: ImmutableArray.Create(assignment.GetLocation()), - properties: ImmutableDictionary.Create() - .Add(UseCompoundAssignmentUtilities.Decrement, UseCompoundAssignmentUtilities.Decrement))); - return; - } + context.ReportDiagnostic(DiagnosticHelper.Create( + _decrementDescriptor, + assignmentToken.GetLocation(), + option.Notification, + context.Options, + additionalLocations: ImmutableArray.Create(assignment.GetLocation()), + properties: ImmutableDictionary.Create() + .Add(UseCompoundAssignmentUtilities.Decrement, UseCompoundAssignmentUtilities.Decrement))); + return; } } - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - assignmentToken.GetLocation(), - option.Notification, - additionalLocations: ImmutableArray.Create(assignment.GetLocation()), - properties: null)); } + + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + assignmentToken.GetLocation(), + option.Notification, + context.Options, + additionalLocations: ImmutableArray.Create(assignment.GetLocation()), + properties: null)); } } diff --git a/src/Analyzers/Core/Analyzers/UseCompoundAssignment/UseCompoundAssignmentUtilities.cs b/src/Analyzers/Core/Analyzers/UseCompoundAssignment/UseCompoundAssignmentUtilities.cs index 98c8ebd47018b..d485e896ec964 100644 --- a/src/Analyzers/Core/Analyzers/UseCompoundAssignment/UseCompoundAssignmentUtilities.cs +++ b/src/Analyzers/Core/Analyzers/UseCompoundAssignment/UseCompoundAssignmentUtilities.cs @@ -9,134 +9,133 @@ using Microsoft.CodeAnalysis.LanguageService; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.UseCompoundAssignment +namespace Microsoft.CodeAnalysis.UseCompoundAssignment; + +internal static class UseCompoundAssignmentUtilities { - internal static class UseCompoundAssignmentUtilities + internal const string Increment = nameof(Increment); + internal const string Decrement = nameof(Decrement); + + public static void GenerateMaps( + ImmutableArray<(TSyntaxKind exprKind, TSyntaxKind assignmentKind, TSyntaxKind tokenKind)> kinds, + out ImmutableDictionary binaryToAssignmentMap, + out ImmutableDictionary assignmentToTokenMap) where TSyntaxKind : struct { - internal const string Increment = nameof(Increment); - internal const string Decrement = nameof(Decrement); + var binaryToAssignmentBuilder = ImmutableDictionary.CreateBuilder(); + var assignmentToTokenBuilder = ImmutableDictionary.CreateBuilder(); - public static void GenerateMaps( - ImmutableArray<(TSyntaxKind exprKind, TSyntaxKind assignmentKind, TSyntaxKind tokenKind)> kinds, - out ImmutableDictionary binaryToAssignmentMap, - out ImmutableDictionary assignmentToTokenMap) where TSyntaxKind : struct + foreach (var (exprKind, assignmentKind, tokenKind) in kinds) { - var binaryToAssignmentBuilder = ImmutableDictionary.CreateBuilder(); - var assignmentToTokenBuilder = ImmutableDictionary.CreateBuilder(); + binaryToAssignmentBuilder[exprKind] = assignmentKind; + assignmentToTokenBuilder[assignmentKind] = tokenKind; + } - foreach (var (exprKind, assignmentKind, tokenKind) in kinds) - { - binaryToAssignmentBuilder[exprKind] = assignmentKind; - assignmentToTokenBuilder[assignmentKind] = tokenKind; - } + binaryToAssignmentMap = binaryToAssignmentBuilder.ToImmutable(); + assignmentToTokenMap = assignmentToTokenBuilder.ToImmutable(); - binaryToAssignmentMap = binaryToAssignmentBuilder.ToImmutable(); - assignmentToTokenMap = assignmentToTokenBuilder.ToImmutable(); + Debug.Assert(binaryToAssignmentMap.Count == assignmentToTokenMap.Count); + Debug.Assert(binaryToAssignmentMap.Values.All(assignmentToTokenMap.ContainsKey)); + } - Debug.Assert(binaryToAssignmentMap.Count == assignmentToTokenMap.Count); - Debug.Assert(binaryToAssignmentMap.Values.All(assignmentToTokenMap.ContainsKey)); - } + public static bool IsSideEffectFree( + ISyntaxFacts syntaxFacts, SyntaxNode expr, SemanticModel semanticModel, CancellationToken cancellationToken) + { + return IsSideEffectFreeRecurse(syntaxFacts, expr, semanticModel, isTopLevel: true, cancellationToken); + } - public static bool IsSideEffectFree( - ISyntaxFacts syntaxFacts, SyntaxNode expr, SemanticModel semanticModel, CancellationToken cancellationToken) + private static bool IsSideEffectFreeRecurse( + ISyntaxFacts syntaxFacts, SyntaxNode expr, SemanticModel semanticModel, + bool isTopLevel, CancellationToken cancellationToken) + { + if (expr == null) { - return IsSideEffectFreeRecurse(syntaxFacts, expr, semanticModel, isTopLevel: true, cancellationToken); + return false; } - private static bool IsSideEffectFreeRecurse( - ISyntaxFacts syntaxFacts, SyntaxNode expr, SemanticModel semanticModel, - bool isTopLevel, CancellationToken cancellationToken) - { - if (expr == null) - { - return false; - } + // it basically has to be of the form "a.b.c", where all components are locals, + // parameters or fields. Basically, nothing that can cause arbitrary user code + // execution when being evaluated by the compiler. - // it basically has to be of the form "a.b.c", where all components are locals, - // parameters or fields. Basically, nothing that can cause arbitrary user code - // execution when being evaluated by the compiler. + if (syntaxFacts.IsThisExpression(expr) || + syntaxFacts.IsBaseExpression(expr)) + { + // Referencing this/base like this.a.b.c causes no side effects itself. + return true; + } - if (syntaxFacts.IsThisExpression(expr) || - syntaxFacts.IsBaseExpression(expr)) - { - // Referencing this/base like this.a.b.c causes no side effects itself. - return true; - } + if (syntaxFacts.IsIdentifierName(expr)) + { + return IsSideEffectFreeSymbol(expr, semanticModel, isTopLevel, cancellationToken); + } - if (syntaxFacts.IsIdentifierName(expr)) - { - return IsSideEffectFreeSymbol(expr, semanticModel, isTopLevel, cancellationToken); - } + if (syntaxFacts.IsParenthesizedExpression(expr)) + { + syntaxFacts.GetPartsOfParenthesizedExpression(expr, + out _, out var expression, out _); - if (syntaxFacts.IsParenthesizedExpression(expr)) - { - syntaxFacts.GetPartsOfParenthesizedExpression(expr, - out _, out var expression, out _); + return IsSideEffectFreeRecurse(syntaxFacts, expression, semanticModel, isTopLevel, cancellationToken); + } - return IsSideEffectFreeRecurse(syntaxFacts, expression, semanticModel, isTopLevel, cancellationToken); - } + if (syntaxFacts.IsSimpleMemberAccessExpression(expr)) + { + syntaxFacts.GetPartsOfMemberAccessExpression(expr, + out var subExpr, out _); + return IsSideEffectFreeRecurse(syntaxFacts, subExpr, semanticModel, isTopLevel: false, cancellationToken) && + IsSideEffectFreeSymbol(expr, semanticModel, isTopLevel, cancellationToken); + } - if (syntaxFacts.IsSimpleMemberAccessExpression(expr)) - { - syntaxFacts.GetPartsOfMemberAccessExpression(expr, - out var subExpr, out _); - return IsSideEffectFreeRecurse(syntaxFacts, subExpr, semanticModel, isTopLevel: false, cancellationToken) && - IsSideEffectFreeSymbol(expr, semanticModel, isTopLevel, cancellationToken); - } + if (syntaxFacts.IsConditionalAccessExpression(expr)) + { + syntaxFacts.GetPartsOfConditionalAccessExpression(expr, + out var expression, out var whenNotNull); + return IsSideEffectFreeRecurse(syntaxFacts, expression, semanticModel, isTopLevel: false, cancellationToken) && + IsSideEffectFreeRecurse(syntaxFacts, whenNotNull, semanticModel, isTopLevel: false, cancellationToken); + } - if (syntaxFacts.IsConditionalAccessExpression(expr)) - { - syntaxFacts.GetPartsOfConditionalAccessExpression(expr, - out var expression, out var whenNotNull); - return IsSideEffectFreeRecurse(syntaxFacts, expression, semanticModel, isTopLevel: false, cancellationToken) && - IsSideEffectFreeRecurse(syntaxFacts, whenNotNull, semanticModel, isTopLevel: false, cancellationToken); - } + // Something we don't explicitly handle. Assume this may have side effects. + return false; + } - // Something we don't explicitly handle. Assume this may have side effects. + private static bool IsSideEffectFreeSymbol( + SyntaxNode expr, SemanticModel semanticModel, bool isTopLevel, CancellationToken cancellationToken) + { + var symbolInfo = semanticModel.GetSymbolInfo(expr, cancellationToken); + if (symbolInfo.CandidateSymbols.Length > 0 || + symbolInfo.Symbol == null) + { + // couldn't bind successfully, assume that this might have side-effects. return false; } - private static bool IsSideEffectFreeSymbol( - SyntaxNode expr, SemanticModel semanticModel, bool isTopLevel, CancellationToken cancellationToken) + var symbol = symbolInfo.Symbol; + switch (symbol.Kind) { - var symbolInfo = semanticModel.GetSymbolInfo(expr, cancellationToken); - if (symbolInfo.CandidateSymbols.Length > 0 || - symbolInfo.Symbol == null) - { - // couldn't bind successfully, assume that this might have side-effects. - return false; - } - - var symbol = symbolInfo.Symbol; - switch (symbol.Kind) - { - case SymbolKind.Namespace: - case SymbolKind.NamedType: - case SymbolKind.Field: - case SymbolKind.Parameter: - case SymbolKind.Local: - return true; - } + case SymbolKind.Namespace: + case SymbolKind.NamedType: + case SymbolKind.Field: + case SymbolKind.Parameter: + case SymbolKind.Local: + return true; + } - if (symbol.Kind == SymbolKind.Property && isTopLevel) + if (symbol.Kind == SymbolKind.Property && isTopLevel) + { + // If we have `this.Prop = this.Prop * 2`, then that's just a single read/write of + // the prop and we can safely make that `this.Prop *= 2` (since it will still be a + // single read/write). However, if we had `this.prop.x = this.prop.x * 2`, then + // that's multiple reads of `this.prop`, and it's not safe to convert that to + // `this.prop.x *= 2` in the case where calling 'prop' may have side effects. + // + // Note, this doesn't apply if the property is a ref-property. In that case, we'd + // go from a read and a write to to just a read (and a write to it's returned ref + // value). + var property = (IPropertySymbol)symbol; + if (property.RefKind == RefKind.None) { - // If we have `this.Prop = this.Prop * 2`, then that's just a single read/write of - // the prop and we can safely make that `this.Prop *= 2` (since it will still be a - // single read/write). However, if we had `this.prop.x = this.prop.x * 2`, then - // that's multiple reads of `this.prop`, and it's not safe to convert that to - // `this.prop.x *= 2` in the case where calling 'prop' may have side effects. - // - // Note, this doesn't apply if the property is a ref-property. In that case, we'd - // go from a read and a write to to just a read (and a write to it's returned ref - // value). - var property = (IPropertySymbol)symbol; - if (property.RefKind == RefKind.None) - { - return true; - } + return true; } - - return false; } + + return false; } } diff --git a/src/Analyzers/Core/Analyzers/UseConditionalExpression/AbstractUseConditionalExpressionDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseConditionalExpression/AbstractUseConditionalExpressionDiagnosticAnalyzer.cs index d050a28879c76..cfdae5ae14a19 100644 --- a/src/Analyzers/Core/Analyzers/UseConditionalExpression/AbstractUseConditionalExpressionDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseConditionalExpression/AbstractUseConditionalExpressionDiagnosticAnalyzer.cs @@ -9,55 +9,55 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.UseConditionalExpression +namespace Microsoft.CodeAnalysis.UseConditionalExpression; + +internal abstract class AbstractUseConditionalExpressionDiagnosticAnalyzer + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TIfStatementSyntax : SyntaxNode { - internal abstract class AbstractUseConditionalExpressionDiagnosticAnalyzer - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TIfStatementSyntax : SyntaxNode + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected AbstractUseConditionalExpressionDiagnosticAnalyzer( + string descriptorId, + EnforceOnBuild enforceOnBuild, + LocalizableResourceString message, + PerLanguageOption2> option) + : base(descriptorId, + enforceOnBuild, + option, + new LocalizableResourceString(nameof(AnalyzersResources.Convert_to_conditional_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + message) + { + } + + protected abstract ISyntaxFacts GetSyntaxFacts(); + protected abstract (bool matched, bool canSimplify) TryMatchPattern(IConditionalOperation ifOperation, ISymbol containingSymbol); + protected abstract CodeStyleOption2 GetStylePreference(OperationAnalysisContext context); + + protected sealed override void InitializeWorker(AnalysisContext context) + => context.RegisterOperationAction(AnalyzeOperation, OperationKind.Conditional); + + private void AnalyzeOperation(OperationAnalysisContext context) { - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - - protected AbstractUseConditionalExpressionDiagnosticAnalyzer( - string descriptorId, - EnforceOnBuild enforceOnBuild, - LocalizableResourceString message, - PerLanguageOption2> option) - : base(descriptorId, - enforceOnBuild, - option, - new LocalizableResourceString(nameof(AnalyzersResources.Convert_to_conditional_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - message) - { - } - - protected abstract ISyntaxFacts GetSyntaxFacts(); - protected abstract (bool matched, bool canSimplify) TryMatchPattern(IConditionalOperation ifOperation, ISymbol containingSymbol); - protected abstract CodeStyleOption2 GetStylePreference(OperationAnalysisContext context); - - protected sealed override void InitializeWorker(AnalysisContext context) - => context.RegisterOperationAction(AnalyzeOperation, OperationKind.Conditional); - - private void AnalyzeOperation(OperationAnalysisContext context) - { - var ifOperation = (IConditionalOperation)context.Operation; - if (ifOperation.Syntax is not TIfStatementSyntax ifStatement) - return; - - var option = GetStylePreference(context); - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; - - var (matched, canSimplify) = TryMatchPattern(ifOperation, context.ContainingSymbol); - if (!matched) - return; - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - ifStatement.GetFirstToken().GetLocation(), - option.Notification, - additionalLocations: ImmutableArray.Create(ifStatement.GetLocation()), - properties: canSimplify ? UseConditionalExpressionHelpers.CanSimplifyProperties : null)); - } + var ifOperation = (IConditionalOperation)context.Operation; + if (ifOperation.Syntax is not TIfStatementSyntax ifStatement) + return; + + var option = GetStylePreference(context); + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; + + var (matched, canSimplify) = TryMatchPattern(ifOperation, context.ContainingSymbol); + if (!matched) + return; + + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + ifStatement.GetFirstToken().GetLocation(), + option.Notification, + context.Options, + additionalLocations: ImmutableArray.Create(ifStatement.GetLocation()), + properties: canSimplify ? UseConditionalExpressionHelpers.CanSimplifyProperties : null)); } } diff --git a/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForAssignment/AbstractUseConditionalExpressionForAssignmentDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForAssignment/AbstractUseConditionalExpressionForAssignmentDiagnosticAnalyzer.cs index 41fd42118c027..4380e6ce2bb5b 100644 --- a/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForAssignment/AbstractUseConditionalExpressionForAssignmentDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForAssignment/AbstractUseConditionalExpressionForAssignmentDiagnosticAnalyzer.cs @@ -6,40 +6,39 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.UseConditionalExpression +namespace Microsoft.CodeAnalysis.UseConditionalExpression; + +internal abstract class AbstractUseConditionalExpressionForAssignmentDiagnosticAnalyzer< + TIfStatementSyntax> + : AbstractUseConditionalExpressionDiagnosticAnalyzer + where TIfStatementSyntax : SyntaxNode { - internal abstract class AbstractUseConditionalExpressionForAssignmentDiagnosticAnalyzer< - TIfStatementSyntax> - : AbstractUseConditionalExpressionDiagnosticAnalyzer - where TIfStatementSyntax : SyntaxNode + protected AbstractUseConditionalExpressionForAssignmentDiagnosticAnalyzer( + LocalizableResourceString message) + : base(IDEDiagnosticIds.UseConditionalExpressionForAssignmentDiagnosticId, + EnforceOnBuildValues.UseConditionalExpressionForAssignment, + message, + CodeStyleOptions2.PreferConditionalExpressionOverAssignment) { - protected AbstractUseConditionalExpressionForAssignmentDiagnosticAnalyzer( - LocalizableResourceString message) - : base(IDEDiagnosticIds.UseConditionalExpressionForAssignmentDiagnosticId, - EnforceOnBuildValues.UseConditionalExpressionForAssignment, - message, - CodeStyleOptions2.PreferConditionalExpressionOverAssignment) - { - } + } - protected sealed override CodeStyleOption2 GetStylePreference(OperationAnalysisContext context) - => context.GetAnalyzerOptions().PreferConditionalExpressionOverAssignment; + protected sealed override CodeStyleOption2 GetStylePreference(OperationAnalysisContext context) + => context.GetAnalyzerOptions().PreferConditionalExpressionOverAssignment; - protected override (bool matched, bool canSimplify) TryMatchPattern(IConditionalOperation ifOperation, ISymbol containingSymbol) + protected override (bool matched, bool canSimplify) TryMatchPattern(IConditionalOperation ifOperation, ISymbol containingSymbol) + { + if (!UseConditionalExpressionForAssignmentHelpers.TryMatchPattern( + GetSyntaxFacts(), ifOperation, out var isRef, out var trueStatement, out var falseStatement, out var trueAssignment, out var falseAssignment)) { - if (!UseConditionalExpressionForAssignmentHelpers.TryMatchPattern( - GetSyntaxFacts(), ifOperation, out var isRef, out var trueStatement, out var falseStatement, out var trueAssignment, out var falseAssignment)) - { - return default; - } + return default; + } - var canSimplify = UseConditionalExpressionHelpers.CanSimplify( - trueAssignment?.Value ?? trueStatement, - falseAssignment?.Value ?? falseStatement, - isRef, - out _); + var canSimplify = UseConditionalExpressionHelpers.CanSimplify( + trueAssignment?.Value ?? trueStatement, + falseAssignment?.Value ?? falseStatement, + isRef, + out _); - return (matched: true, canSimplify); - } + return (matched: true, canSimplify); } } diff --git a/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForAssignment/UseConditionalExpressionForAssignmentHelpers.cs b/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForAssignment/UseConditionalExpressionForAssignmentHelpers.cs index d4b38e5e4c038..fd95fea70eb59 100644 --- a/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForAssignment/UseConditionalExpressionForAssignmentHelpers.cs +++ b/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForAssignment/UseConditionalExpressionForAssignmentHelpers.cs @@ -9,158 +9,157 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.UseConditionalExpression +namespace Microsoft.CodeAnalysis.UseConditionalExpression; + +internal static class UseConditionalExpressionForAssignmentHelpers { - internal static class UseConditionalExpressionForAssignmentHelpers + public static bool TryMatchPattern( + ISyntaxFacts syntaxFacts, + IConditionalOperation ifOperation, + out bool isRef, + [NotNullWhen(true)] out IOperation trueStatement, + [NotNullWhen(true)] out IOperation? falseStatement, + out ISimpleAssignmentOperation? trueAssignment, + out ISimpleAssignmentOperation? falseAssignment) { - public static bool TryMatchPattern( - ISyntaxFacts syntaxFacts, - IConditionalOperation ifOperation, - out bool isRef, - [NotNullWhen(true)] out IOperation trueStatement, - [NotNullWhen(true)] out IOperation? falseStatement, - out ISimpleAssignmentOperation? trueAssignment, - out ISimpleAssignmentOperation? falseAssignment) - { - isRef = false; - falseAssignment = null; + isRef = false; + falseAssignment = null; - trueStatement = ifOperation.WhenTrue; - falseStatement = ifOperation.WhenFalse; + trueStatement = ifOperation.WhenTrue; + falseStatement = ifOperation.WhenFalse; - trueStatement = UseConditionalExpressionHelpers.UnwrapSingleStatementBlock(trueStatement); - falseStatement = UseConditionalExpressionHelpers.UnwrapSingleStatementBlock(falseStatement); + trueStatement = UseConditionalExpressionHelpers.UnwrapSingleStatementBlock(trueStatement); + falseStatement = UseConditionalExpressionHelpers.UnwrapSingleStatementBlock(falseStatement); - if (!TryGetAssignmentOrThrow(trueStatement, out trueAssignment, out var trueThrow) || - !TryGetAssignmentOrThrow(falseStatement, out falseAssignment, out var falseThrow)) - { - return false; - } + if (!TryGetAssignmentOrThrow(trueStatement, out trueAssignment, out var trueThrow) || + !TryGetAssignmentOrThrow(falseStatement, out falseAssignment, out var falseThrow)) + { + return false; + } - var anyAssignment = trueAssignment ?? falseAssignment; - if (UseConditionalExpressionHelpers.HasInconvertibleThrowStatement( - syntaxFacts, anyAssignment?.IsRef == true, trueThrow, falseThrow)) - { - return false; - } + var anyAssignment = trueAssignment ?? falseAssignment; + if (UseConditionalExpressionHelpers.HasInconvertibleThrowStatement( + syntaxFacts, anyAssignment?.IsRef == true, trueThrow, falseThrow)) + { + return false; + } - // The left side of both assignment statements has to be syntactically identical (modulo - // trivia differences). - if (trueAssignment != null && falseAssignment != null && - !syntaxFacts.AreEquivalent(trueAssignment.Target.Syntax, falseAssignment.Target.Syntax)) - { - return false; - } + // The left side of both assignment statements has to be syntactically identical (modulo + // trivia differences). + if (trueAssignment != null && falseAssignment != null && + !syntaxFacts.AreEquivalent(trueAssignment.Target.Syntax, falseAssignment.Target.Syntax)) + { + return false; + } - // If both assignments are discards type check is required. - // Since discard can discard value of any type, after converting if statement to a conditional expression - // it can produce compiler error if types are not the same and there is no implicit conversion between them, e.g.: - // if (flag) - // { - // _ = 5; - // } - // else - // { - // _ = ""; - // } - // This code can result in `_ = flag ? 5 : ""`, which immediately produces CS0173 - if (trueAssignment?.Target is IDiscardOperation && - falseAssignment?.Target is IDiscardOperation && - !AreEqualOrHaveImplicitConversion(trueAssignment.Type, falseAssignment.Type, trueAssignment.SemanticModel!.Compilation)) - { - return false; - } + // If both assignments are discards type check is required. + // Since discard can discard value of any type, after converting if statement to a conditional expression + // it can produce compiler error if types are not the same and there is no implicit conversion between them, e.g.: + // if (flag) + // { + // _ = 5; + // } + // else + // { + // _ = ""; + // } + // This code can result in `_ = flag ? 5 : ""`, which immediately produces CS0173 + if (trueAssignment?.Target is IDiscardOperation && + falseAssignment?.Target is IDiscardOperation && + !AreEqualOrHaveImplicitConversion(trueAssignment.Type, falseAssignment.Type, trueAssignment.SemanticModel!.Compilation)) + { + return false; + } - if (ReferencesDeclaredVariableInAssignment(ifOperation.Condition, trueAssignment?.Target, falseAssignment?.Target)) - return false; + if (ReferencesDeclaredVariableInAssignment(ifOperation.Condition, trueAssignment?.Target, falseAssignment?.Target)) + return false; - isRef = trueAssignment?.IsRef == true; - return UseConditionalExpressionHelpers.CanConvert( - syntaxFacts, ifOperation, trueStatement, falseStatement); + isRef = trueAssignment?.IsRef == true; + return UseConditionalExpressionHelpers.CanConvert( + syntaxFacts, ifOperation, trueStatement, falseStatement); - static bool AreEqualOrHaveImplicitConversion(ITypeSymbol? firstType, ITypeSymbol? secondType, Compilation compilation) - { - if (SymbolEqualityComparer.Default.Equals(firstType, secondType)) - return true; + static bool AreEqualOrHaveImplicitConversion(ITypeSymbol? firstType, ITypeSymbol? secondType, Compilation compilation) + { + if (SymbolEqualityComparer.Default.Equals(firstType, secondType)) + return true; - if (firstType is null || secondType is null) - return false; + if (firstType is null || secondType is null) + return false; - return compilation.ClassifyCommonConversion(firstType, secondType).IsImplicit - ^ compilation.ClassifyCommonConversion(secondType, firstType).IsImplicit; - } + return compilation.ClassifyCommonConversion(firstType, secondType).IsImplicit + ^ compilation.ClassifyCommonConversion(secondType, firstType).IsImplicit; + } - static bool ReferencesDeclaredVariableInAssignment(IOperation condition, IOperation? trueTarget, IOperation? falseTarget) + static bool ReferencesDeclaredVariableInAssignment(IOperation condition, IOperation? trueTarget, IOperation? falseTarget) + { + if (trueTarget is not null || falseTarget is not null) { - if (trueTarget is not null || falseTarget is not null) + using var _1 = PooledHashSet.GetInstance(out var symbolsDeclaredInConditional); + foreach (var operation in condition.DescendantsAndSelf()) { - using var _1 = PooledHashSet.GetInstance(out var symbolsDeclaredInConditional); - foreach (var operation in condition.DescendantsAndSelf()) - { - // `if (x is String s)` - if (operation is IDeclarationPatternOperation { DeclaredSymbol: ILocalSymbol local }) - symbolsDeclaredInConditional.AddIfNotNull(local); - - // `if (Goo(out String s))` - if (operation is IDeclarationExpressionOperation { Expression: ILocalReferenceOperation localReference }) - symbolsDeclaredInConditional.AddIfNotNull(localReference.Local); - } + // `if (x is String s)` + if (operation is IDeclarationPatternOperation { DeclaredSymbol: ILocalSymbol local }) + symbolsDeclaredInConditional.AddIfNotNull(local); - if (symbolsDeclaredInConditional.Count > 0) - { - return ContainsLocalReference(symbolsDeclaredInConditional, trueTarget) || - ContainsLocalReference(symbolsDeclaredInConditional, falseTarget); - } + // `if (Goo(out String s))` + if (operation is IDeclarationExpressionOperation { Expression: ILocalReferenceOperation localReference }) + symbolsDeclaredInConditional.AddIfNotNull(localReference.Local); } - return false; + if (symbolsDeclaredInConditional.Count > 0) + { + return ContainsLocalReference(symbolsDeclaredInConditional, trueTarget) || + ContainsLocalReference(symbolsDeclaredInConditional, falseTarget); + } } - static bool ContainsLocalReference(HashSet declaredPatternSymbols, IOperation? target) + return false; + } + + static bool ContainsLocalReference(HashSet declaredPatternSymbols, IOperation? target) + { + if (target is not null) { - if (target is not null) + foreach (var operation in target.DescendantsAndSelf()) { - foreach (var operation in target.DescendantsAndSelf()) + if (operation is ILocalReferenceOperation { Local: var local } && + declaredPatternSymbols.Contains(local)) { - if (operation is ILocalReferenceOperation { Local: var local } && - declaredPatternSymbols.Contains(local)) - { - return true; - } + return true; } } - - return false; } - } - private static bool TryGetAssignmentOrThrow( - [NotNullWhen(true)] IOperation? statement, - out ISimpleAssignmentOperation? assignment, - out IThrowOperation? throwOperation) - { - assignment = null; - throwOperation = null; + return false; + } + } - if (statement is IThrowOperation throwOp) - { - throwOperation = throwOp; + private static bool TryGetAssignmentOrThrow( + [NotNullWhen(true)] IOperation? statement, + out ISimpleAssignmentOperation? assignment, + out IThrowOperation? throwOperation) + { + assignment = null; + throwOperation = null; - // We can only convert a `throw expr` to a throw expression, not `throw;` - return throwOperation.Exception != null; - } + if (statement is IThrowOperation throwOp) + { + throwOperation = throwOp; - // Both the WhenTrue and WhenFalse statements must be of the form: - // target = value; - if (statement is IExpressionStatementOperation exprStatement && - exprStatement.Operation is ISimpleAssignmentOperation assignmentOp && - assignmentOp.Target != null) - { - assignment = assignmentOp; - return true; - } + // We can only convert a `throw expr` to a throw expression, not `throw;` + return throwOperation.Exception != null; + } - return false; + // Both the WhenTrue and WhenFalse statements must be of the form: + // target = value; + if (statement is IExpressionStatementOperation exprStatement && + exprStatement.Operation is ISimpleAssignmentOperation assignmentOp && + assignmentOp.Target != null) + { + assignment = assignmentOp; + return true; } + + return false; } } diff --git a/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForReturn/AbstractUseConditionalExpressionForReturnDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForReturn/AbstractUseConditionalExpressionForReturnDiagnosticAnalyzer.cs index fe0643d9799a7..b6c26745df06f 100644 --- a/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForReturn/AbstractUseConditionalExpressionForReturnDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForReturn/AbstractUseConditionalExpressionForReturnDiagnosticAnalyzer.cs @@ -6,49 +6,48 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.UseConditionalExpression +namespace Microsoft.CodeAnalysis.UseConditionalExpression; + +internal abstract class AbstractUseConditionalExpressionForReturnDiagnosticAnalyzer< + TIfStatementSyntax> + : AbstractUseConditionalExpressionDiagnosticAnalyzer + where TIfStatementSyntax : SyntaxNode { - internal abstract class AbstractUseConditionalExpressionForReturnDiagnosticAnalyzer< - TIfStatementSyntax> - : AbstractUseConditionalExpressionDiagnosticAnalyzer - where TIfStatementSyntax : SyntaxNode + protected AbstractUseConditionalExpressionForReturnDiagnosticAnalyzer( + LocalizableResourceString message) + : base(IDEDiagnosticIds.UseConditionalExpressionForReturnDiagnosticId, + EnforceOnBuildValues.UseConditionalExpressionForReturn, + message, + CodeStyleOptions2.PreferConditionalExpressionOverReturn) + { + } + + protected sealed override CodeStyleOption2 GetStylePreference(OperationAnalysisContext context) + => context.GetAnalyzerOptions().PreferConditionalExpressionOverReturn; + + protected sealed override (bool matched, bool canSimplify) TryMatchPattern(IConditionalOperation ifOperation, ISymbol containingSymbol) { - protected AbstractUseConditionalExpressionForReturnDiagnosticAnalyzer( - LocalizableResourceString message) - : base(IDEDiagnosticIds.UseConditionalExpressionForReturnDiagnosticId, - EnforceOnBuildValues.UseConditionalExpressionForReturn, - message, - CodeStyleOptions2.PreferConditionalExpressionOverReturn) + if (!UseConditionalExpressionForReturnHelpers.TryMatchPattern( + GetSyntaxFacts(), ifOperation, containingSymbol, out var isRef, out var trueStatement, out var falseStatement, out var trueReturn, out var falseReturn)) { + return default; } - protected sealed override CodeStyleOption2 GetStylePreference(OperationAnalysisContext context) - => context.GetAnalyzerOptions().PreferConditionalExpressionOverReturn; - - protected sealed override (bool matched, bool canSimplify) TryMatchPattern(IConditionalOperation ifOperation, ISymbol containingSymbol) + if (!IsStatementSupported(trueStatement) || + !IsStatementSupported(falseStatement)) { - if (!UseConditionalExpressionForReturnHelpers.TryMatchPattern( - GetSyntaxFacts(), ifOperation, containingSymbol, out var isRef, out var trueStatement, out var falseStatement, out var trueReturn, out var falseReturn)) - { - return default; - } - - if (!IsStatementSupported(trueStatement) || - !IsStatementSupported(falseStatement)) - { - return default; - } - - var canSimplify = UseConditionalExpressionHelpers.CanSimplify( - trueReturn?.ReturnedValue ?? trueStatement, - falseReturn?.ReturnedValue ?? falseStatement, - isRef, - out _); - - return (matched: true, canSimplify); + return default; } - protected virtual bool IsStatementSupported(IOperation statement) - => true; + var canSimplify = UseConditionalExpressionHelpers.CanSimplify( + trueReturn?.ReturnedValue ?? trueStatement, + falseReturn?.ReturnedValue ?? falseStatement, + isRef, + out _); + + return (matched: true, canSimplify); } + + protected virtual bool IsStatementSupported(IOperation statement) + => true; } diff --git a/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForReturn/UseConditionalExpressionForReturnHelpers.cs b/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForReturn/UseConditionalExpressionForReturnHelpers.cs index ab52a7c5d231e..f8f9f25dec95c 100644 --- a/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForReturn/UseConditionalExpressionForReturnHelpers.cs +++ b/src/Analyzers/Core/Analyzers/UseConditionalExpression/ForReturn/UseConditionalExpressionForReturnHelpers.cs @@ -6,131 +6,130 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.UseConditionalExpression +namespace Microsoft.CodeAnalysis.UseConditionalExpression; + +internal static class UseConditionalExpressionForReturnHelpers { - internal static class UseConditionalExpressionForReturnHelpers + public static bool TryMatchPattern( + ISyntaxFacts syntaxFacts, + IConditionalOperation ifOperation, + ISymbol containingSymbol, + out bool isRef, + [NotNullWhen(true)] out IOperation? trueStatement, + [NotNullWhen(true)] out IOperation? falseStatement, + out IReturnOperation? trueReturn, + out IReturnOperation? falseReturn) { - public static bool TryMatchPattern( - ISyntaxFacts syntaxFacts, - IConditionalOperation ifOperation, - ISymbol containingSymbol, - out bool isRef, - [NotNullWhen(true)] out IOperation? trueStatement, - [NotNullWhen(true)] out IOperation? falseStatement, - out IReturnOperation? trueReturn, - out IReturnOperation? falseReturn) + isRef = false; + + trueReturn = null; + falseReturn = null; + + trueStatement = ifOperation.WhenTrue; + falseStatement = ifOperation.WhenFalse; + + // we support: + // + // if (expr) + // return a; + // else + // return b; + // + // and + // + // if (expr) + // return a; + // + // return b; + // + // note: either (but not both) of these statements can be throw-statements. + + if (falseStatement == null) { - isRef = false; - - trueReturn = null; - falseReturn = null; - - trueStatement = ifOperation.WhenTrue; - falseStatement = ifOperation.WhenFalse; - - // we support: - // - // if (expr) - // return a; - // else - // return b; - // - // and - // - // if (expr) - // return a; - // - // return b; - // - // note: either (but not both) of these statements can be throw-statements. + if (ifOperation.Parent is not IBlockOperation parentBlock) + return false; - if (falseStatement == null) - { - if (ifOperation.Parent is not IBlockOperation parentBlock) - return false; + var ifIndex = parentBlock.Operations.IndexOf(ifOperation); + if (ifIndex < 0) + return false; - var ifIndex = parentBlock.Operations.IndexOf(ifOperation); - if (ifIndex < 0) - return false; + if (ifIndex + 1 >= parentBlock.Operations.Length) + return false; - if (ifIndex + 1 >= parentBlock.Operations.Length) - return false; + falseStatement = parentBlock.Operations[ifIndex + 1]; + if (falseStatement.IsImplicit) + return false; + } - falseStatement = parentBlock.Operations[ifIndex + 1]; - if (falseStatement.IsImplicit) - return false; - } + trueStatement = UseConditionalExpressionHelpers.UnwrapSingleStatementBlock(trueStatement); + falseStatement = UseConditionalExpressionHelpers.UnwrapSingleStatementBlock(falseStatement); - trueStatement = UseConditionalExpressionHelpers.UnwrapSingleStatementBlock(trueStatement); - falseStatement = UseConditionalExpressionHelpers.UnwrapSingleStatementBlock(falseStatement); + // Both return-statements must be of the form "return value" + if (!IsReturnExprOrThrow(trueStatement) || + !IsReturnExprOrThrow(falseStatement)) + { + return false; + } - // Both return-statements must be of the form "return value" - if (!IsReturnExprOrThrow(trueStatement) || - !IsReturnExprOrThrow(falseStatement)) - { - return false; - } - - trueReturn = trueStatement as IReturnOperation; - falseReturn = falseStatement as IReturnOperation; - var trueThrow = trueStatement as IThrowOperation; - var falseThrow = falseStatement as IThrowOperation; - - var anyReturn = trueReturn ?? falseReturn; - if (UseConditionalExpressionHelpers.HasInconvertibleThrowStatement( - syntaxFacts, anyReturn.GetRefKind(containingSymbol) != RefKind.None, - trueThrow, falseThrow)) - { - return false; - } - - if (trueReturn != null && - falseReturn != null && - trueReturn.Kind != falseReturn.Kind) - { - // Not allowed if these are different types of returns. i.e. - // "yield return ..." and "return ...". - return false; - } + trueReturn = trueStatement as IReturnOperation; + falseReturn = falseStatement as IReturnOperation; + var trueThrow = trueStatement as IThrowOperation; + var falseThrow = falseStatement as IThrowOperation; - if (trueReturn?.Kind == OperationKind.YieldBreak) - { - // This check is just paranoia. We likely shouldn't get here since we already - // checked if .ReturnedValue was null above. - return false; - } - - if (trueReturn?.Kind == OperationKind.YieldReturn && - ifOperation.WhenFalse == null) - { - // we have the following: - // - // if (...) { - // yield return ... - // } - // - // yield return ... - // - // It is *not* correct to replace this with: - // - // yield return ... ? ... ? ... - // - // as both yields need to be hit. - return false; - } + var anyReturn = trueReturn ?? falseReturn; + if (UseConditionalExpressionHelpers.HasInconvertibleThrowStatement( + syntaxFacts, anyReturn.GetRefKind(containingSymbol) != RefKind.None, + trueThrow, falseThrow)) + { + return false; + } - isRef = anyReturn.GetRefKind(containingSymbol) != RefKind.None; - return UseConditionalExpressionHelpers.CanConvert( - syntaxFacts, ifOperation, trueStatement, falseStatement); + if (trueReturn != null && + falseReturn != null && + trueReturn.Kind != falseReturn.Kind) + { + // Not allowed if these are different types of returns. i.e. + // "yield return ..." and "return ...". + return false; } - private static bool IsReturnExprOrThrow(IOperation? statement) + if (trueReturn?.Kind == OperationKind.YieldBreak) { - // We can only convert a `throw expr` to a throw expression, not `throw;` - if (statement is IThrowOperation throwOperation) - return throwOperation.Exception != null; + // This check is just paranoia. We likely shouldn't get here since we already + // checked if .ReturnedValue was null above. + return false; + } - return statement is IReturnOperation returnOp && returnOp.ReturnedValue != null; + if (trueReturn?.Kind == OperationKind.YieldReturn && + ifOperation.WhenFalse == null) + { + // we have the following: + // + // if (...) { + // yield return ... + // } + // + // yield return ... + // + // It is *not* correct to replace this with: + // + // yield return ... ? ... ? ... + // + // as both yields need to be hit. + return false; } + + isRef = anyReturn.GetRefKind(containingSymbol) != RefKind.None; + return UseConditionalExpressionHelpers.CanConvert( + syntaxFacts, ifOperation, trueStatement, falseStatement); + } + + private static bool IsReturnExprOrThrow(IOperation? statement) + { + // We can only convert a `throw expr` to a throw expression, not `throw;` + if (statement is IThrowOperation throwOperation) + return throwOperation.Exception != null; + + return statement is IReturnOperation returnOp && returnOp.ReturnedValue != null; } } diff --git a/src/Analyzers/Core/Analyzers/UseConditionalExpression/UseConditionalExpressionHelpers.cs b/src/Analyzers/Core/Analyzers/UseConditionalExpression/UseConditionalExpressionHelpers.cs index 51e31aa2fa0bd..4f08b9eb60459 100644 --- a/src/Analyzers/Core/Analyzers/UseConditionalExpression/UseConditionalExpressionHelpers.cs +++ b/src/Analyzers/Core/Analyzers/UseConditionalExpression/UseConditionalExpressionHelpers.cs @@ -7,127 +7,126 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.UseConditionalExpression +namespace Microsoft.CodeAnalysis.UseConditionalExpression; + +internal static partial class UseConditionalExpressionHelpers { - internal static partial class UseConditionalExpressionHelpers - { - public const string CanSimplifyName = nameof(CanSimplifyName); + public const string CanSimplifyName = nameof(CanSimplifyName); - public static readonly ImmutableDictionary CanSimplifyProperties = - ImmutableDictionary.Empty.Add(CanSimplifyName, CanSimplifyName); + public static readonly ImmutableDictionary CanSimplifyProperties = + ImmutableDictionary.Empty.Add(CanSimplifyName, CanSimplifyName); - public static bool CanConvert( - ISyntaxFacts syntaxFacts, IConditionalOperation ifOperation, - IOperation whenTrue, IOperation whenFalse) + public static bool CanConvert( + ISyntaxFacts syntaxFacts, IConditionalOperation ifOperation, + IOperation whenTrue, IOperation whenFalse) + { + // Will likely not work as intended if the if directive spans any preprocessor directives. So + // do not offer for now. Note: we pass in both the node for the ifOperation and the + // whenFalse portion. The whenFalse portion isn't necessary under the ifOperation. For + // example in: + // + // ```c# + // #if DEBUG + // if (check) + // return 3; + // #endif + // return 2; + // ``` + // + // In this case, we want to see that this cross the `#endif` + if (syntaxFacts.SpansPreprocessorDirective(ifOperation.Syntax, whenFalse.Syntax)) { - // Will likely not work as intended if the if directive spans any preprocessor directives. So - // do not offer for now. Note: we pass in both the node for the ifOperation and the - // whenFalse portion. The whenFalse portion isn't necessary under the ifOperation. For - // example in: - // - // ```c# - // #if DEBUG - // if (check) - // return 3; - // #endif - // return 2; - // ``` - // - // In this case, we want to see that this cross the `#endif` - if (syntaxFacts.SpansPreprocessorDirective(ifOperation.Syntax, whenFalse.Syntax)) - { - return false; - } - - // User may have comments on the when-true/when-false statements. These statements can - // be very important. Often they indicate why the true/false branches are important in - // the first place. We don't have any place to put these, so we don't offer here. - if (HasRegularComments(syntaxFacts, whenTrue.Syntax) || - HasRegularComments(syntaxFacts, whenFalse.Syntax)) - { - return false; - } - - return true; + return false; } - /// - /// Will unwrap a block with a single statement in it to just that block. Used so we can - /// support both if (expr) { statement } and if (expr) statement - /// - [return: NotNullIfNotNull(nameof(statement))] - public static IOperation? UnwrapSingleStatementBlock(IOperation? statement) - => statement is IBlockOperation { Operations: [var operationInBlock] } - ? operationInBlock - : statement; - - public static bool HasRegularComments(ISyntaxFacts syntaxFacts, SyntaxNode syntax) - => HasRegularCommentTrivia(syntaxFacts, syntax.GetLeadingTrivia()) || - HasRegularCommentTrivia(syntaxFacts, syntax.GetTrailingTrivia()); - - public static bool HasRegularCommentTrivia(ISyntaxFacts syntaxFacts, SyntaxTriviaList triviaList) + // User may have comments on the when-true/when-false statements. These statements can + // be very important. Often they indicate why the true/false branches are important in + // the first place. We don't have any place to put these, so we don't offer here. + if (HasRegularComments(syntaxFacts, whenTrue.Syntax) || + HasRegularComments(syntaxFacts, whenFalse.Syntax)) { - foreach (var trivia in triviaList) - { - if (syntaxFacts.IsRegularComment(trivia)) - return true; - } - return false; } - public static bool HasInconvertibleThrowStatement( - ISyntaxFacts syntaxFacts, bool isRef, - IThrowOperation? trueThrow, IThrowOperation? falseThrow) + return true; + } + + /// + /// Will unwrap a block with a single statement in it to just that block. Used so we can + /// support both if (expr) { statement } and if (expr) statement + /// + [return: NotNullIfNotNull(nameof(statement))] + public static IOperation? UnwrapSingleStatementBlock(IOperation? statement) + => statement is IBlockOperation { Operations: [var operationInBlock] } + ? operationInBlock + : statement; + + public static bool HasRegularComments(ISyntaxFacts syntaxFacts, SyntaxNode syntax) + => HasRegularCommentTrivia(syntaxFacts, syntax.GetLeadingTrivia()) || + HasRegularCommentTrivia(syntaxFacts, syntax.GetTrailingTrivia()); + + public static bool HasRegularCommentTrivia(ISyntaxFacts syntaxFacts, SyntaxTriviaList triviaList) + { + foreach (var trivia in triviaList) { - // Can't convert to `x ? throw ... : throw ...` as there's no best common type between the two (even when - // throwing the same exception type). - if (trueThrow != null && falseThrow != null) + if (syntaxFacts.IsRegularComment(trivia)) return true; + } - var anyThrow = trueThrow ?? falseThrow; + return false; + } - if (anyThrow != null) - { - // can only convert to a conditional expression if the lang supports throw-exprs. - if (!syntaxFacts.SupportsThrowExpression(anyThrow.Syntax.SyntaxTree.Options)) - return true; + public static bool HasInconvertibleThrowStatement( + ISyntaxFacts syntaxFacts, bool isRef, + IThrowOperation? trueThrow, IThrowOperation? falseThrow) + { + // Can't convert to `x ? throw ... : throw ...` as there's no best common type between the two (even when + // throwing the same exception type). + if (trueThrow != null && falseThrow != null) + return true; - // `ref` can't be used with `throw`. - if (isRef) - return true; - } + var anyThrow = trueThrow ?? falseThrow; - return false; + if (anyThrow != null) + { + // can only convert to a conditional expression if the lang supports throw-exprs. + if (!syntaxFacts.SupportsThrowExpression(anyThrow.Syntax.SyntaxTree.Options)) + return true; + + // `ref` can't be used with `throw`. + if (isRef) + return true; } - public static bool IsBooleanLiteral(IOperation trueValue, bool val) - => trueValue is ILiteralOperation { ConstantValue: { HasValue: true, Value: bool value } } && value == val; + return false; + } + + public static bool IsBooleanLiteral(IOperation trueValue, bool val) + => trueValue is ILiteralOperation { ConstantValue: { HasValue: true, Value: bool value } } && value == val; - public static bool CanSimplify(IOperation trueValue, IOperation falseValue, bool isRef, out bool negate) + public static bool CanSimplify(IOperation trueValue, IOperation falseValue, bool isRef, out bool negate) + { + // If we are going to generate "expr ? true : false" then just generate "expr" + // instead. + // + // If we are going to generate "expr ? false : true" then just generate "!expr" + // instead. + if (!isRef) { - // If we are going to generate "expr ? true : false" then just generate "expr" - // instead. - // - // If we are going to generate "expr ? false : true" then just generate "!expr" - // instead. - if (!isRef) + if (IsBooleanLiteral(trueValue, true) && IsBooleanLiteral(falseValue, false)) { - if (IsBooleanLiteral(trueValue, true) && IsBooleanLiteral(falseValue, false)) - { - negate = false; - return true; - } - - if (IsBooleanLiteral(trueValue, false) && IsBooleanLiteral(falseValue, true)) - { - negate = true; - return true; - } + negate = false; + return true; } - negate = false; - return false; + if (IsBooleanLiteral(trueValue, false) && IsBooleanLiteral(falseValue, true)) + { + negate = true; + return true; + } } + + negate = false; + return false; } } diff --git a/src/Analyzers/Core/Analyzers/UseExplicitTupleName/UseExplicitTupleNameDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseExplicitTupleName/UseExplicitTupleNameDiagnosticAnalyzer.cs index 36be395e816c2..0d23347c5b0ac 100644 --- a/src/Analyzers/Core/Analyzers/UseExplicitTupleName/UseExplicitTupleNameDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseExplicitTupleName/UseExplicitTupleNameDiagnosticAnalyzer.cs @@ -9,88 +9,88 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.UseExplicitTupleName +namespace Microsoft.CodeAnalysis.UseExplicitTupleName; + +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +internal class UseExplicitTupleNameDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - internal class UseExplicitTupleNameDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer - { - public const string ElementName = nameof(ElementName); + public const string ElementName = nameof(ElementName); - public UseExplicitTupleNameDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseExplicitTupleNameDiagnosticId, - EnforceOnBuildValues.UseExplicitTupleName, - CodeStyleOptions2.PreferExplicitTupleNames, - title: new LocalizableResourceString(nameof(AnalyzersResources.Use_explicitly_provided_tuple_name), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - messageFormat: new LocalizableResourceString(nameof(AnalyzersResources.Prefer_explicitly_provided_tuple_element_name), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + public UseExplicitTupleNameDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseExplicitTupleNameDiagnosticId, + EnforceOnBuildValues.UseExplicitTupleName, + CodeStyleOptions2.PreferExplicitTupleNames, + title: new LocalizableResourceString(nameof(AnalyzersResources.Use_explicitly_provided_tuple_name), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + messageFormat: new LocalizableResourceString(nameof(AnalyzersResources.Prefer_explicitly_provided_tuple_element_name), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) + { + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterOperationAction(AnalyzeOperation, OperationKind.FieldReference); + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterOperationAction(AnalyzeOperation, OperationKind.FieldReference); - private void AnalyzeOperation(OperationAnalysisContext context) + private void AnalyzeOperation(OperationAnalysisContext context) + { + // We only create a diagnostic if the option's value is set to true. + var option = context.GetAnalyzerOptions().PreferExplicitTupleNames; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) { - // We only create a diagnostic if the option's value is set to true. - var option = context.GetAnalyzerOptions().PreferExplicitTupleNames; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - { - return; - } + return; + } - if (option.Notification.Severity == ReportDiagnostic.Suppress) - { - return; - } + if (option.Notification.Severity == ReportDiagnostic.Suppress) + { + return; + } - var fieldReferenceOperation = (IFieldReferenceOperation)context.Operation; + var fieldReferenceOperation = (IFieldReferenceOperation)context.Operation; - var field = fieldReferenceOperation.Field; - if (field.ContainingType.IsTupleType) + var field = fieldReferenceOperation.Field; + if (field.ContainingType.IsTupleType) + { + if (field.CorrespondingTupleField?.Equals(field) == true) { - if (field.CorrespondingTupleField?.Equals(field) == true) + var namedField = GetNamedField(field.ContainingType, field, context.CancellationToken); + if (namedField != null) { - var namedField = GetNamedField(field.ContainingType, field, context.CancellationToken); - if (namedField != null) + var memberAccessSyntax = fieldReferenceOperation.Syntax; + var nameNode = memberAccessSyntax.ChildNodesAndTokens().Reverse().FirstOrDefault().AsNode(); + if (nameNode != null) { - var memberAccessSyntax = fieldReferenceOperation.Syntax; - var nameNode = memberAccessSyntax.ChildNodesAndTokens().Reverse().FirstOrDefault().AsNode(); - if (nameNode != null) - { - var properties = ImmutableDictionary.Empty.Add( - nameof(ElementName), namedField.Name); - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - nameNode.GetLocation(), - option.Notification, - additionalLocations: null, - properties)); - } + var properties = ImmutableDictionary.Empty.Add( + nameof(ElementName), namedField.Name); + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + nameNode.GetLocation(), + option.Notification, + context.Options, + additionalLocations: null, + properties)); } } } } + } - private static IFieldSymbol? GetNamedField( - INamedTypeSymbol containingType, IFieldSymbol unnamedField, CancellationToken cancellationToken) + private static IFieldSymbol? GetNamedField( + INamedTypeSymbol containingType, IFieldSymbol unnamedField, CancellationToken cancellationToken) + { + foreach (var member in containingType.GetMembers()) { - foreach (var member in containingType.GetMembers()) - { - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - if (member.Kind == SymbolKind.Field) + if (member.Kind == SymbolKind.Field) + { + var fieldSymbol = (IFieldSymbol)member; + if (unnamedField.Equals(fieldSymbol.CorrespondingTupleField) && + !fieldSymbol.Name.Equals(unnamedField.Name)) { - var fieldSymbol = (IFieldSymbol)member; - if (unnamedField.Equals(fieldSymbol.CorrespondingTupleField) && - !fieldSymbol.Name.Equals(unnamedField.Name)) - { - return fieldSymbol; - } + return fieldSymbol; } } - - return null; } + + return null; } } diff --git a/src/Analyzers/Core/Analyzers/UseInferredMemberName/AbstractUseInferredMemberNameDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseInferredMemberName/AbstractUseInferredMemberNameDiagnosticAnalyzer.cs index 633fbb8fdca2b..b4ab57390bc19 100644 --- a/src/Analyzers/Core/Analyzers/UseInferredMemberName/AbstractUseInferredMemberNameDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseInferredMemberName/AbstractUseInferredMemberNameDiagnosticAnalyzer.cs @@ -8,23 +8,22 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.UseInferredMemberName -{ - internal abstract class AbstractUseInferredMemberNameDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer - { - protected abstract void AnalyzeSyntax(SyntaxNodeAnalysisContext context); +namespace Microsoft.CodeAnalysis.UseInferredMemberName; - public AbstractUseInferredMemberNameDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseInferredMemberNameDiagnosticId, - EnforceOnBuildValues.UseInferredMemberName, - options: [CodeStyleOptions2.PreferInferredAnonymousTypeMemberNames, CodeStyleOptions2.PreferInferredTupleNames], - fadingOption: null, - new LocalizableResourceString(nameof(AnalyzersResources.Use_inferred_member_name), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Member_name_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } +internal abstract class AbstractUseInferredMemberNameDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer +{ + protected abstract void AnalyzeSyntax(SyntaxNodeAnalysisContext context); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public AbstractUseInferredMemberNameDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseInferredMemberNameDiagnosticId, + EnforceOnBuildValues.UseInferredMemberName, + options: [CodeStyleOptions2.PreferInferredAnonymousTypeMemberNames, CodeStyleOptions2.PreferInferredTupleNames], + fadingOption: null, + new LocalizableResourceString(nameof(AnalyzersResources.Use_inferred_member_name), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Member_name_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) + { } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; } diff --git a/src/Analyzers/Core/Analyzers/UseIsNullCheck/AbstractUseIsNullForReferenceEqualsDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseIsNullCheck/AbstractUseIsNullForReferenceEqualsDiagnosticAnalyzer.cs index 42af4c5304e0f..304ad7291b7f5 100644 --- a/src/Analyzers/Core/Analyzers/UseIsNullCheck/AbstractUseIsNullForReferenceEqualsDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseIsNullCheck/AbstractUseIsNullForReferenceEqualsDiagnosticAnalyzer.cs @@ -9,156 +9,156 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.UseIsNullCheck +namespace Microsoft.CodeAnalysis.UseIsNullCheck; + +internal abstract class AbstractUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer< + TLanguageKindEnum> + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TLanguageKindEnum : struct { - internal abstract class AbstractUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer< - TLanguageKindEnum> - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TLanguageKindEnum : struct + protected AbstractUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer(LocalizableString title) + : base(IDEDiagnosticIds.UseIsNullCheckDiagnosticId, + EnforceOnBuildValues.UseIsNullCheck, + CodeStyleOptions2.PreferIsNullCheckOverReferenceEqualityMethod, + title, + new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - protected AbstractUseIsNullCheckForReferenceEqualsDiagnosticAnalyzer(LocalizableString title) - : base(IDEDiagnosticIds.UseIsNullCheckDiagnosticId, - EnforceOnBuildValues.UseIsNullCheck, - CodeStyleOptions2.PreferIsNullCheckOverReferenceEqualityMethod, - title, - new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) - => context.RegisterCompilationStartAction(context => + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + { + var objectType = context.Compilation.GetSpecialType(SpecialType.System_Object); + if (objectType != null && IsLanguageVersionSupported(context.Compilation)) { - var objectType = context.Compilation.GetSpecialType(SpecialType.System_Object); - if (objectType != null && IsLanguageVersionSupported(context.Compilation)) + var referenceEqualsMethod = objectType.GetMembers(nameof(ReferenceEquals)) + .OfType() + .FirstOrDefault(m => m.DeclaredAccessibility == Accessibility.Public && + m.Parameters.Length == 2); + if (referenceEqualsMethod != null) { - var referenceEqualsMethod = objectType.GetMembers(nameof(ReferenceEquals)) - .OfType() - .FirstOrDefault(m => m.DeclaredAccessibility == Accessibility.Public && - m.Parameters.Length == 2); - if (referenceEqualsMethod != null) - { - var syntaxKinds = GetSyntaxFacts().SyntaxKinds; - var unconstraintedGenericSupported = IsUnconstrainedGenericSupported(context.Compilation); - context.RegisterSyntaxNodeAction( - c => AnalyzeSyntax(c, referenceEqualsMethod, unconstraintedGenericSupported), - syntaxKinds.Convert(syntaxKinds.InvocationExpression)); - } + var syntaxKinds = GetSyntaxFacts().SyntaxKinds; + var unconstraintedGenericSupported = IsUnconstrainedGenericSupported(context.Compilation); + context.RegisterSyntaxNodeAction( + c => AnalyzeSyntax(c, referenceEqualsMethod, unconstraintedGenericSupported), + syntaxKinds.Convert(syntaxKinds.InvocationExpression)); } - }); + } + }); + + protected abstract bool IsLanguageVersionSupported(Compilation compilation); + protected abstract bool IsUnconstrainedGenericSupported(Compilation compilation); + protected abstract ISyntaxFacts GetSyntaxFacts(); - protected abstract bool IsLanguageVersionSupported(Compilation compilation); - protected abstract bool IsUnconstrainedGenericSupported(Compilation compilation); - protected abstract ISyntaxFacts GetSyntaxFacts(); + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context, IMethodSymbol referenceEqualsMethod, bool unconstraintedGenericSupported) + { + var cancellationToken = context.CancellationToken; + + var semanticModel = context.SemanticModel; - private void AnalyzeSyntax(SyntaxNodeAnalysisContext context, IMethodSymbol referenceEqualsMethod, bool unconstraintedGenericSupported) + var option = context.GetAnalyzerOptions().PreferIsNullCheckOverReferenceEqualityMethod; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) { - var cancellationToken = context.CancellationToken; + return; + } - var semanticModel = context.SemanticModel; + var invocation = context.Node; + var syntaxFacts = GetSyntaxFacts(); - var option = context.GetAnalyzerOptions().PreferIsNullCheckOverReferenceEqualityMethod; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - { - return; - } + var expression = syntaxFacts.GetExpressionOfInvocationExpression(invocation); + var nameNode = syntaxFacts.IsIdentifierName(expression) + ? expression + : syntaxFacts.IsSimpleMemberAccessExpression(expression) + ? syntaxFacts.GetNameOfMemberAccessExpression(expression) + : null; - var invocation = context.Node; - var syntaxFacts = GetSyntaxFacts(); + if (!syntaxFacts.IsIdentifierName(nameNode)) + { + return; + } - var expression = syntaxFacts.GetExpressionOfInvocationExpression(invocation); - var nameNode = syntaxFacts.IsIdentifierName(expression) - ? expression - : syntaxFacts.IsSimpleMemberAccessExpression(expression) - ? syntaxFacts.GetNameOfMemberAccessExpression(expression) - : null; + syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out var name, out _); + if (!syntaxFacts.StringComparer.Equals(name, nameof(ReferenceEquals))) + { + return; + } - if (!syntaxFacts.IsIdentifierName(nameNode)) - { - return; - } + var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(invocation); + if (arguments.Count != 2) + { + return; + } - syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out var name, out _); - if (!syntaxFacts.StringComparer.Equals(name, nameof(ReferenceEquals))) - { - return; - } + if (!MatchesPattern(syntaxFacts, arguments[0], arguments[1]) && + !MatchesPattern(syntaxFacts, arguments[1], arguments[0])) + { + return; + } - var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(invocation); - if (arguments.Count != 2) - { - return; - } + var symbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol; + if (!referenceEqualsMethod.Equals(symbol)) + { + return; + } - if (!MatchesPattern(syntaxFacts, arguments[0], arguments[1]) && - !MatchesPattern(syntaxFacts, arguments[1], arguments[0])) - { - return; - } + var properties = ImmutableDictionary.Empty.Add( + UseIsNullConstants.Kind, UseIsNullConstants.ReferenceEqualsKey); - var symbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol; - if (!referenceEqualsMethod.Equals(symbol)) + var genericParameterSymbol = GetGenericParameterSymbol(syntaxFacts, semanticModel, arguments[0], arguments[1], cancellationToken); + if (genericParameterSymbol != null) + { + if (genericParameterSymbol.IsValueType) { + // 'is null' would generate error CS0403: Cannot convert null to type parameter 'T' because it could be a non-nullable value type. Consider using 'default(T)' instead. + // '== null' would generate error CS0019: Operator '==' cannot be applied to operands of type 'T' and '' + // 'Is Nothing' would generate error BC30020: 'Is' operator does not accept operands of type 'T'. Operands must be reference or nullable types. return; } - var properties = ImmutableDictionary.Empty.Add( - UseIsNullConstants.Kind, UseIsNullConstants.ReferenceEqualsKey); + // HasReferenceTypeConstraint returns false for base type constraint. + // IsReferenceType returns true. - var genericParameterSymbol = GetGenericParameterSymbol(syntaxFacts, semanticModel, arguments[0], arguments[1], cancellationToken); - if (genericParameterSymbol != null) + if (!genericParameterSymbol.IsReferenceType && !unconstraintedGenericSupported) { - if (genericParameterSymbol.IsValueType) - { - // 'is null' would generate error CS0403: Cannot convert null to type parameter 'T' because it could be a non-nullable value type. Consider using 'default(T)' instead. - // '== null' would generate error CS0019: Operator '==' cannot be applied to operands of type 'T' and '' - // 'Is Nothing' would generate error BC30020: 'Is' operator does not accept operands of type 'T'. Operands must be reference or nullable types. - return; - } - - // HasReferenceTypeConstraint returns false for base type constraint. - // IsReferenceType returns true. - - if (!genericParameterSymbol.IsReferenceType && !unconstraintedGenericSupported) - { - // Needs special casing for C# as long as - // 'is null' over unconstrained generic is implemented in C# 8. - properties = properties.Add(UseIsNullConstants.UnconstrainedGeneric, ""); - } + // Needs special casing for C# as long as + // 'is null' over unconstrained generic is implemented in C# 8. + properties = properties.Add(UseIsNullConstants.UnconstrainedGeneric, ""); } + } - var additionalLocations = ImmutableArray.Create(invocation.GetLocation()); + var additionalLocations = ImmutableArray.Create(invocation.GetLocation()); - var negated = syntaxFacts.IsLogicalNotExpression(invocation.Parent); - if (negated) - { - properties = properties.Add(UseIsNullConstants.Negated, ""); - } - - context.ReportDiagnostic( - DiagnosticHelper.Create( - Descriptor, nameNode.GetLocation(), - option.Notification, - additionalLocations, properties)); + var negated = syntaxFacts.IsLogicalNotExpression(invocation.Parent); + if (negated) + { + properties = properties.Add(UseIsNullConstants.Negated, ""); } - private static ITypeParameterSymbol? GetGenericParameterSymbol(ISyntaxFacts syntaxFacts, SemanticModel semanticModel, SyntaxNode node1, SyntaxNode node2, CancellationToken cancellationToken) - { - var valueNode = syntaxFacts.IsNullLiteralExpression(syntaxFacts.GetExpressionOfArgument(node1)) ? node2 : node1; - var argumentExpression = syntaxFacts.GetExpressionOfArgument(valueNode); - if (argumentExpression != null) - { - var parameterType = semanticModel.GetTypeInfo(argumentExpression, cancellationToken).Type; - return parameterType as ITypeParameterSymbol; - } + context.ReportDiagnostic( + DiagnosticHelper.Create( + Descriptor, nameNode.GetLocation(), + option.Notification, + context.Options, + additionalLocations, properties)); + } - return null; + private static ITypeParameterSymbol? GetGenericParameterSymbol(ISyntaxFacts syntaxFacts, SemanticModel semanticModel, SyntaxNode node1, SyntaxNode node2, CancellationToken cancellationToken) + { + var valueNode = syntaxFacts.IsNullLiteralExpression(syntaxFacts.GetExpressionOfArgument(node1)) ? node2 : node1; + var argumentExpression = syntaxFacts.GetExpressionOfArgument(valueNode); + if (argumentExpression != null) + { + var parameterType = semanticModel.GetTypeInfo(argumentExpression, cancellationToken).Type; + return parameterType as ITypeParameterSymbol; } - private static bool MatchesPattern(ISyntaxFacts syntaxFacts, SyntaxNode node1, SyntaxNode node2) - => syntaxFacts.IsNullLiteralExpression(syntaxFacts.GetExpressionOfArgument(node1)) && - !syntaxFacts.IsNullLiteralExpression(syntaxFacts.GetExpressionOfArgument(node2)); + return null; } + + private static bool MatchesPattern(ISyntaxFacts syntaxFacts, SyntaxNode node1, SyntaxNode node2) + => syntaxFacts.IsNullLiteralExpression(syntaxFacts.GetExpressionOfArgument(node1)) && + !syntaxFacts.IsNullLiteralExpression(syntaxFacts.GetExpressionOfArgument(node2)); } diff --git a/src/Analyzers/Core/Analyzers/UseIsNullCheck/UseIsNullConstants.cs b/src/Analyzers/Core/Analyzers/UseIsNullCheck/UseIsNullConstants.cs index cfba4fe5746ee..6f69adfe83beb 100644 --- a/src/Analyzers/Core/Analyzers/UseIsNullCheck/UseIsNullConstants.cs +++ b/src/Analyzers/Core/Analyzers/UseIsNullCheck/UseIsNullConstants.cs @@ -2,14 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.UseIsNullCheck +namespace Microsoft.CodeAnalysis.UseIsNullCheck; + +internal static class UseIsNullConstants { - internal static class UseIsNullConstants - { - public const string Kind = nameof(Kind); - public const string ReferenceEqualsKey = nameof(ReferenceEqualsKey); - public const string CastAndEqualityKey = nameof(CastAndEqualityKey); - public const string Negated = nameof(Negated); - public const string UnconstrainedGeneric = nameof(UnconstrainedGeneric); - } + public const string Kind = nameof(Kind); + public const string ReferenceEqualsKey = nameof(ReferenceEqualsKey); + public const string CastAndEqualityKey = nameof(CastAndEqualityKey); + public const string Negated = nameof(Negated); + public const string UnconstrainedGeneric = nameof(UnconstrainedGeneric); } diff --git a/src/Analyzers/Core/Analyzers/UseNullPropagation/AbstractUseNullPropagationDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseNullPropagation/AbstractUseNullPropagationDiagnosticAnalyzer.cs index 502e6ad9bea7d..dce6beb32c68c 100644 --- a/src/Analyzers/Core/Analyzers/UseNullPropagation/AbstractUseNullPropagationDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseNullPropagation/AbstractUseNullPropagationDiagnosticAnalyzer.cs @@ -11,376 +11,376 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.UseNullPropagation +namespace Microsoft.CodeAnalysis.UseNullPropagation; + +internal static class UseNullPropagationConstants +{ + public const string WhenPartIsNullable = nameof(WhenPartIsNullable); +} + +/// +/// Looks for code snippets similar to x == null ? null : x.Y() and converts it to x?.Y(). This form is also supported: +/// +/// if (x != null) +/// x.Y(); +/// +/// +internal abstract partial class AbstractUseNullPropagationDiagnosticAnalyzer< + TSyntaxKind, + TExpressionSyntax, + TStatementSyntax, + TConditionalExpressionSyntax, + TBinaryExpressionSyntax, + TInvocationExpressionSyntax, + TConditionalAccessExpressionSyntax, + TElementAccessExpressionSyntax, + TMemberAccessExpressionSyntax, + TIfStatementSyntax, + TExpressionStatementSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct + where TExpressionSyntax : SyntaxNode + where TStatementSyntax : SyntaxNode + where TConditionalExpressionSyntax : TExpressionSyntax + where TBinaryExpressionSyntax : TExpressionSyntax + where TInvocationExpressionSyntax : TExpressionSyntax + where TConditionalAccessExpressionSyntax : TExpressionSyntax + where TElementAccessExpressionSyntax : TExpressionSyntax + where TMemberAccessExpressionSyntax : TExpressionSyntax + where TIfStatementSyntax : TStatementSyntax + where TExpressionStatementSyntax : TStatementSyntax { - internal static class UseNullPropagationConstants + private static readonly ImmutableDictionary s_whenPartIsNullableProperties = + ImmutableDictionary.Empty.Add(UseNullPropagationConstants.WhenPartIsNullable, ""); + + protected AbstractUseNullPropagationDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseNullPropagationDiagnosticId, + EnforceOnBuildValues.UseNullPropagation, + CodeStyleOptions2.PreferNullPropagation, + new LocalizableResourceString(nameof(AnalyzersResources.Use_null_propagation), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - public const string WhenPartIsNullable = nameof(WhenPartIsNullable); } - /// - /// Looks for code snippets similar to x == null ? null : x.Y() and converts it to x?.Y(). This form is also supported: - /// - /// if (x != null) - /// x.Y(); - /// - /// - internal abstract partial class AbstractUseNullPropagationDiagnosticAnalyzer< - TSyntaxKind, - TExpressionSyntax, - TStatementSyntax, - TConditionalExpressionSyntax, - TBinaryExpressionSyntax, - TInvocationExpressionSyntax, - TConditionalAccessExpressionSyntax, - TElementAccessExpressionSyntax, - TMemberAccessExpressionSyntax, - TIfStatementSyntax, - TExpressionStatementSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TSyntaxKind : struct - where TExpressionSyntax : SyntaxNode - where TStatementSyntax : SyntaxNode - where TConditionalExpressionSyntax : TExpressionSyntax - where TBinaryExpressionSyntax : TExpressionSyntax - where TInvocationExpressionSyntax : TExpressionSyntax - where TConditionalAccessExpressionSyntax : TExpressionSyntax - where TElementAccessExpressionSyntax : TExpressionSyntax - where TMemberAccessExpressionSyntax : TExpressionSyntax - where TIfStatementSyntax : TStatementSyntax - where TExpressionStatementSyntax : TStatementSyntax + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected abstract bool ShouldAnalyze(Compilation compilation); + + protected abstract TSyntaxKind IfStatementSyntaxKind { get; } + protected abstract ISemanticFacts SemanticFacts { get; } + protected ISyntaxFacts SyntaxFacts => SemanticFacts.SyntaxFacts; + + protected abstract bool TryAnalyzePatternCondition( + ISyntaxFacts syntaxFacts, TExpressionSyntax conditionNode, + [NotNullWhen(true)] out TExpressionSyntax? conditionPartToCheck, out bool isEquals); + + protected override void InitializeWorker(AnalysisContext context) { - private static readonly ImmutableDictionary s_whenPartIsNullableProperties = - ImmutableDictionary.Empty.Add(UseNullPropagationConstants.WhenPartIsNullable, ""); - - protected AbstractUseNullPropagationDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseNullPropagationDiagnosticId, - EnforceOnBuildValues.UseNullPropagation, - CodeStyleOptions2.PreferNullPropagation, - new LocalizableResourceString(nameof(AnalyzersResources.Use_null_propagation), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) + context.RegisterCompilationStartAction(context => { - } + if (!ShouldAnalyze(context.Compilation)) + return; - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + var expressionType = context.Compilation.ExpressionOfTType(); + + var objectType = context.Compilation.GetSpecialType(SpecialType.System_Object); + var referenceEqualsMethod = objectType?.GetMembers(nameof(ReferenceEquals)) + .OfType() + .FirstOrDefault(m => m.DeclaredAccessibility == Accessibility.Public && + m.Parameters.Length == 2); + + var syntaxKinds = this.SyntaxFacts.SyntaxKinds; + context.RegisterSyntaxNodeAction( + context => AnalyzeTernaryConditionalExpression(context, expressionType, referenceEqualsMethod), + syntaxKinds.Convert(syntaxKinds.TernaryConditionalExpression)); + context.RegisterSyntaxNodeAction( + context => AnalyzeIfStatement(context, referenceEqualsMethod), + IfStatementSyntaxKind); + }); + } + + private void AnalyzeTernaryConditionalExpression( + SyntaxNodeAnalysisContext context, + INamedTypeSymbol? expressionType, + IMethodSymbol? referenceEqualsMethod) + { + var cancellationToken = context.CancellationToken; + var conditionalExpression = (TConditionalExpressionSyntax)context.Node; - protected abstract bool ShouldAnalyze(Compilation compilation); + var option = context.GetAnalyzerOptions().PreferNullPropagation; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; - protected abstract TSyntaxKind IfStatementSyntaxKind { get; } - protected abstract ISemanticFacts SemanticFacts { get; } - protected ISyntaxFacts SyntaxFacts => SemanticFacts.SyntaxFacts; + var syntaxFacts = this.SyntaxFacts; + syntaxFacts.GetPartsOfConditionalExpression( + conditionalExpression, out var condition, out var whenTrue, out var whenFalse); - protected abstract bool TryAnalyzePatternCondition( - ISyntaxFacts syntaxFacts, TExpressionSyntax conditionNode, - [NotNullWhen(true)] out TExpressionSyntax? conditionPartToCheck, out bool isEquals); + var conditionNode = (TExpressionSyntax)condition; - protected override void InitializeWorker(AnalysisContext context) + var whenTrueNode = (TExpressionSyntax)syntaxFacts.WalkDownParentheses(whenTrue); + var whenFalseNode = (TExpressionSyntax)syntaxFacts.WalkDownParentheses(whenFalse); + + if (!TryAnalyzeCondition( + context, syntaxFacts, referenceEqualsMethod, conditionNode, + out var conditionPartToCheck, out var isEquals)) { - context.RegisterCompilationStartAction(context => - { - if (!ShouldAnalyze(context.Compilation)) - return; - - var expressionType = context.Compilation.ExpressionOfTType(); - - var objectType = context.Compilation.GetSpecialType(SpecialType.System_Object); - var referenceEqualsMethod = objectType?.GetMembers(nameof(ReferenceEquals)) - .OfType() - .FirstOrDefault(m => m.DeclaredAccessibility == Accessibility.Public && - m.Parameters.Length == 2); - - var syntaxKinds = this.SyntaxFacts.SyntaxKinds; - context.RegisterSyntaxNodeAction( - context => AnalyzeTernaryConditionalExpression(context, expressionType, referenceEqualsMethod), - syntaxKinds.Convert(syntaxKinds.TernaryConditionalExpression)); - context.RegisterSyntaxNodeAction( - context => AnalyzeIfStatement(context, referenceEqualsMethod), - IfStatementSyntaxKind); - }); + return; } - private void AnalyzeTernaryConditionalExpression( - SyntaxNodeAnalysisContext context, - INamedTypeSymbol? expressionType, - IMethodSymbol? referenceEqualsMethod) - { - var cancellationToken = context.CancellationToken; - var conditionalExpression = (TConditionalExpressionSyntax)context.Node; + // Needs to be of the form: + // x == null ? null : ... or + // x != null ? ... : null; + if (isEquals && !syntaxFacts.IsNullLiteralExpression(whenTrueNode)) + return; - var option = context.GetAnalyzerOptions().PreferNullPropagation; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; + if (!isEquals && !syntaxFacts.IsNullLiteralExpression(whenFalseNode)) + return; - var syntaxFacts = this.SyntaxFacts; - syntaxFacts.GetPartsOfConditionalExpression( - conditionalExpression, out var condition, out var whenTrue, out var whenFalse); + var whenPartToCheck = isEquals ? whenFalseNode : whenTrueNode; - var conditionNode = (TExpressionSyntax)condition; + var semanticModel = context.SemanticModel; + var whenPartMatch = GetWhenPartMatch(syntaxFacts, semanticModel, conditionPartToCheck, whenPartToCheck, cancellationToken); + if (whenPartMatch == null) + return; - var whenTrueNode = (TExpressionSyntax)syntaxFacts.WalkDownParentheses(whenTrue); - var whenFalseNode = (TExpressionSyntax)syntaxFacts.WalkDownParentheses(whenFalse); + // can't use ?. on a pointer + var whenPartType = semanticModel.GetTypeInfo(whenPartMatch, cancellationToken).Type; + if (whenPartType is IPointerTypeSymbol) + return; - if (!TryAnalyzeCondition( - context, syntaxFacts, referenceEqualsMethod, conditionNode, - out var conditionPartToCheck, out var isEquals)) + var type = semanticModel.GetTypeInfo(conditionalExpression, cancellationToken).Type; + if (type?.IsValueType == true) + { + if (type is not INamedTypeSymbol namedType || namedType.ConstructedFrom.SpecialType != SpecialType.System_Nullable_T) { + // User has something like: If(str is nothing, nothing, str.Length) + // In this case, converting to str?.Length changes the type of this from + // int to int? return; } + // But for a nullable type, such as If(c is nothing, nothing, c.nullable) + // converting to c?.nullable doesn't affect the type + } - // Needs to be of the form: - // x == null ? null : ... or - // x != null ? ... : null; - if (isEquals && !syntaxFacts.IsNullLiteralExpression(whenTrueNode)) + if (syntaxFacts.IsSimpleMemberAccessExpression(whenPartToCheck)) + { + // `x == null ? x : x.M` cannot be converted to `x?.M` when M is a method symbol. + syntaxFacts.GetPartsOfMemberAccessExpression(whenPartToCheck, out _, out var name); + if (semanticModel.GetSymbolInfo(name, cancellationToken).GetAnySymbol() is IMethodSymbol) return; + } - if (!isEquals && !syntaxFacts.IsNullLiteralExpression(whenFalseNode)) - return; + // ?. is not available in expression-trees. Disallow the fix in that case. + if (this.SemanticFacts.IsInExpressionTree(semanticModel, conditionNode, expressionType, cancellationToken)) + return; + + var locations = ImmutableArray.Create( + conditionalExpression.GetLocation(), + conditionPartToCheck.GetLocation(), + whenPartToCheck.GetLocation()); + + var whenPartIsNullable = whenPartType?.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; + var properties = whenPartIsNullable + ? s_whenPartIsNullableProperties + : ImmutableDictionary.Empty; + + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + conditionalExpression.GetLocation(), + option.Notification, + context.Options, + locations, + properties)); + } - var whenPartToCheck = isEquals ? whenFalseNode : whenTrueNode; + private bool TryAnalyzeCondition( + SyntaxNodeAnalysisContext context, + ISyntaxFacts syntaxFacts, + IMethodSymbol? referenceEqualsMethod, + TExpressionSyntax condition, + [NotNullWhen(true)] out TExpressionSyntax? conditionPartToCheck, + out bool isEquals) + { + condition = (TExpressionSyntax)syntaxFacts.WalkDownParentheses(condition); + var conditionIsNegated = false; + if (syntaxFacts.IsLogicalNotExpression(condition)) + { + conditionIsNegated = true; + condition = (TExpressionSyntax)syntaxFacts.WalkDownParentheses( + syntaxFacts.GetOperandOfPrefixUnaryExpression(condition)); + } - var semanticModel = context.SemanticModel; - var whenPartMatch = GetWhenPartMatch(syntaxFacts, semanticModel, conditionPartToCheck, whenPartToCheck, cancellationToken); - if (whenPartMatch == null) - return; + var result = condition switch + { + TBinaryExpressionSyntax binaryExpression => TryAnalyzeBinaryExpressionCondition( + syntaxFacts, binaryExpression, out conditionPartToCheck, out isEquals), - // can't use ?. on a pointer - var whenPartType = semanticModel.GetTypeInfo(whenPartMatch, cancellationToken).Type; - if (whenPartType is IPointerTypeSymbol) - return; + TInvocationExpressionSyntax invocation => TryAnalyzeInvocationCondition( + context, syntaxFacts, referenceEqualsMethod, invocation, + out conditionPartToCheck, out isEquals), - var type = semanticModel.GetTypeInfo(conditionalExpression, cancellationToken).Type; - if (type?.IsValueType == true) - { - if (type is not INamedTypeSymbol namedType || namedType.ConstructedFrom.SpecialType != SpecialType.System_Nullable_T) - { - // User has something like: If(str is nothing, nothing, str.Length) - // In this case, converting to str?.Length changes the type of this from - // int to int? - return; - } - // But for a nullable type, such as If(c is nothing, nothing, c.nullable) - // converting to c?.nullable doesn't affect the type - } + _ => TryAnalyzePatternCondition(syntaxFacts, condition, out conditionPartToCheck, out isEquals), + }; - if (syntaxFacts.IsSimpleMemberAccessExpression(whenPartToCheck)) - { - // `x == null ? x : x.M` cannot be converted to `x?.M` when M is a method symbol. - syntaxFacts.GetPartsOfMemberAccessExpression(whenPartToCheck, out _, out var name); - if (semanticModel.GetSymbolInfo(name, cancellationToken).GetAnySymbol() is IMethodSymbol) - return; - } + if (conditionIsNegated) + isEquals = !isEquals; - // ?. is not available in expression-trees. Disallow the fix in that case. - if (this.SemanticFacts.IsInExpressionTree(semanticModel, conditionNode, expressionType, cancellationToken)) - return; + return result; + } - var locations = ImmutableArray.Create( - conditionalExpression.GetLocation(), - conditionPartToCheck.GetLocation(), - whenPartToCheck.GetLocation()); - - var whenPartIsNullable = whenPartType?.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; - var properties = whenPartIsNullable - ? s_whenPartIsNullableProperties - : ImmutableDictionary.Empty; - - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - conditionalExpression.GetLocation(), - option.Notification, - locations, - properties)); + private static bool TryAnalyzeBinaryExpressionCondition( + ISyntaxFacts syntaxFacts, TBinaryExpressionSyntax condition, + [NotNullWhen(true)] out TExpressionSyntax? conditionPartToCheck, out bool isEquals) + { + var syntaxKinds = syntaxFacts.SyntaxKinds; + isEquals = syntaxKinds.ReferenceEqualsExpression == condition.RawKind; + var isNotEquals = syntaxKinds.ReferenceNotEqualsExpression == condition.RawKind; + if (!isEquals && !isNotEquals) + { + conditionPartToCheck = null; + return false; } - - private bool TryAnalyzeCondition( - SyntaxNodeAnalysisContext context, - ISyntaxFacts syntaxFacts, - IMethodSymbol? referenceEqualsMethod, - TExpressionSyntax condition, - [NotNullWhen(true)] out TExpressionSyntax? conditionPartToCheck, - out bool isEquals) + else { - condition = (TExpressionSyntax)syntaxFacts.WalkDownParentheses(condition); - var conditionIsNegated = false; - if (syntaxFacts.IsLogicalNotExpression(condition)) - { - conditionIsNegated = true; - condition = (TExpressionSyntax)syntaxFacts.WalkDownParentheses( - syntaxFacts.GetOperandOfPrefixUnaryExpression(condition)); - } - - var result = condition switch - { - TBinaryExpressionSyntax binaryExpression => TryAnalyzeBinaryExpressionCondition( - syntaxFacts, binaryExpression, out conditionPartToCheck, out isEquals), + syntaxFacts.GetPartsOfBinaryExpression(condition, out var conditionLeft, out var conditionRight); + conditionPartToCheck = GetConditionPartToCheck(syntaxFacts, (TExpressionSyntax)conditionLeft, (TExpressionSyntax)conditionRight); + return conditionPartToCheck != null; + } + } - TInvocationExpressionSyntax invocation => TryAnalyzeInvocationCondition( - context, syntaxFacts, referenceEqualsMethod, invocation, - out conditionPartToCheck, out isEquals), + private static bool TryAnalyzeInvocationCondition( + SyntaxNodeAnalysisContext context, + ISyntaxFacts syntaxFacts, + IMethodSymbol? referenceEqualsMethod, + TInvocationExpressionSyntax invocation, + [NotNullWhen(true)] out TExpressionSyntax? conditionPartToCheck, + out bool isEquals) + { + conditionPartToCheck = null; + isEquals = true; - _ => TryAnalyzePatternCondition(syntaxFacts, condition, out conditionPartToCheck, out isEquals), - }; + if (referenceEqualsMethod == null) + return false; - if (conditionIsNegated) - isEquals = !isEquals; + var expression = syntaxFacts.GetExpressionOfInvocationExpression(invocation); + var nameNode = syntaxFacts.IsIdentifierName(expression) + ? expression + : syntaxFacts.IsSimpleMemberAccessExpression(expression) + ? syntaxFacts.GetNameOfMemberAccessExpression(expression) + : null; - return result; + if (!syntaxFacts.IsIdentifierName(nameNode)) + { + return false; } - private static bool TryAnalyzeBinaryExpressionCondition( - ISyntaxFacts syntaxFacts, TBinaryExpressionSyntax condition, - [NotNullWhen(true)] out TExpressionSyntax? conditionPartToCheck, out bool isEquals) + syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out var name, out _); + if (!syntaxFacts.StringComparer.Equals(name, nameof(ReferenceEquals))) { - var syntaxKinds = syntaxFacts.SyntaxKinds; - isEquals = syntaxKinds.ReferenceEqualsExpression == condition.RawKind; - var isNotEquals = syntaxKinds.ReferenceNotEqualsExpression == condition.RawKind; - if (!isEquals && !isNotEquals) - { - conditionPartToCheck = null; - return false; - } - else - { - syntaxFacts.GetPartsOfBinaryExpression(condition, out var conditionLeft, out var conditionRight); - conditionPartToCheck = GetConditionPartToCheck(syntaxFacts, (TExpressionSyntax)conditionLeft, (TExpressionSyntax)conditionRight); - return conditionPartToCheck != null; - } + return false; } - private static bool TryAnalyzeInvocationCondition( - SyntaxNodeAnalysisContext context, - ISyntaxFacts syntaxFacts, - IMethodSymbol? referenceEqualsMethod, - TInvocationExpressionSyntax invocation, - [NotNullWhen(true)] out TExpressionSyntax? conditionPartToCheck, - out bool isEquals) + var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(invocation); + if (arguments.Count != 2) { - conditionPartToCheck = null; - isEquals = true; - - if (referenceEqualsMethod == null) - return false; - - var expression = syntaxFacts.GetExpressionOfInvocationExpression(invocation); - var nameNode = syntaxFacts.IsIdentifierName(expression) - ? expression - : syntaxFacts.IsSimpleMemberAccessExpression(expression) - ? syntaxFacts.GetNameOfMemberAccessExpression(expression) - : null; - - if (!syntaxFacts.IsIdentifierName(nameNode)) - { - return false; - } + return false; + } - syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out var name, out _); - if (!syntaxFacts.StringComparer.Equals(name, nameof(ReferenceEquals))) - { - return false; - } + var conditionLeft = (TExpressionSyntax)syntaxFacts.GetExpressionOfArgument(arguments[0]); + var conditionRight = (TExpressionSyntax)syntaxFacts.GetExpressionOfArgument(arguments[1]); + if (conditionLeft == null || conditionRight == null) + { + return false; + } - var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(invocation); - if (arguments.Count != 2) - { - return false; - } + conditionPartToCheck = GetConditionPartToCheck(syntaxFacts, conditionLeft, conditionRight); + if (conditionPartToCheck == null) + { + return false; + } - var conditionLeft = (TExpressionSyntax)syntaxFacts.GetExpressionOfArgument(arguments[0]); - var conditionRight = (TExpressionSyntax)syntaxFacts.GetExpressionOfArgument(arguments[1]); - if (conditionLeft == null || conditionRight == null) - { - return false; - } + var semanticModel = context.SemanticModel; + var cancellationToken = context.CancellationToken; + var symbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol; + return referenceEqualsMethod.Equals(symbol); + } - conditionPartToCheck = GetConditionPartToCheck(syntaxFacts, conditionLeft, conditionRight); - if (conditionPartToCheck == null) - { - return false; - } + private static TExpressionSyntax? GetConditionPartToCheck( + ISyntaxFacts syntaxFacts, TExpressionSyntax conditionLeft, TExpressionSyntax conditionRight) + { + var conditionLeftIsNull = syntaxFacts.IsNullLiteralExpression(conditionLeft); + var conditionRightIsNull = syntaxFacts.IsNullLiteralExpression(conditionRight); - var semanticModel = context.SemanticModel; - var cancellationToken = context.CancellationToken; - var symbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol; - return referenceEqualsMethod.Equals(symbol); + if (conditionRightIsNull && conditionLeftIsNull) + { + // null == null nothing to do here. + return null; } - private static TExpressionSyntax? GetConditionPartToCheck( - ISyntaxFacts syntaxFacts, TExpressionSyntax conditionLeft, TExpressionSyntax conditionRight) + if (!conditionRightIsNull && !conditionLeftIsNull) { - var conditionLeftIsNull = syntaxFacts.IsNullLiteralExpression(conditionLeft); - var conditionRightIsNull = syntaxFacts.IsNullLiteralExpression(conditionRight); + return null; + } - if (conditionRightIsNull && conditionLeftIsNull) - { - // null == null nothing to do here. + return conditionRightIsNull ? conditionLeft : conditionRight; + } + + internal static TExpressionSyntax? GetWhenPartMatch( + ISyntaxFacts syntaxFacts, + SemanticModel semanticModel, + TExpressionSyntax expressionToMatch, + TExpressionSyntax whenPart, + CancellationToken cancellationToken) + { + expressionToMatch = RemoveObjectCastIfAny(syntaxFacts, semanticModel, expressionToMatch, cancellationToken); + var current = whenPart; + while (true) + { + var unwrapped = Unwrap(syntaxFacts, current); + if (unwrapped == null) return null; - } - if (!conditionRightIsNull && !conditionLeftIsNull) + if (syntaxFacts.IsSimpleMemberAccessExpression(current) || current is TElementAccessExpressionSyntax) { - return null; + if (syntaxFacts.AreEquivalent(unwrapped, expressionToMatch)) + return unwrapped; } - return conditionRightIsNull ? conditionLeft : conditionRight; + current = unwrapped; } + } - internal static TExpressionSyntax? GetWhenPartMatch( - ISyntaxFacts syntaxFacts, - SemanticModel semanticModel, - TExpressionSyntax expressionToMatch, - TExpressionSyntax whenPart, - CancellationToken cancellationToken) + private static TExpressionSyntax RemoveObjectCastIfAny( + ISyntaxFacts syntaxFacts, SemanticModel semanticModel, TExpressionSyntax node, CancellationToken cancellationToken) + { + if (syntaxFacts.IsCastExpression(node)) { - expressionToMatch = RemoveObjectCastIfAny(syntaxFacts, semanticModel, expressionToMatch, cancellationToken); - var current = whenPart; - while (true) - { - var unwrapped = Unwrap(syntaxFacts, current); - if (unwrapped == null) - return null; - - if (syntaxFacts.IsSimpleMemberAccessExpression(current) || current is TElementAccessExpressionSyntax) - { - if (syntaxFacts.AreEquivalent(unwrapped, expressionToMatch)) - return unwrapped; - } + syntaxFacts.GetPartsOfCastExpression(node, out var type, out var expression); + var typeSymbol = semanticModel.GetTypeInfo(type, cancellationToken).Type; - current = unwrapped; - } + if (typeSymbol?.SpecialType == SpecialType.System_Object) + return (TExpressionSyntax)expression; } - private static TExpressionSyntax RemoveObjectCastIfAny( - ISyntaxFacts syntaxFacts, SemanticModel semanticModel, TExpressionSyntax node, CancellationToken cancellationToken) - { - if (syntaxFacts.IsCastExpression(node)) - { - syntaxFacts.GetPartsOfCastExpression(node, out var type, out var expression); - var typeSymbol = semanticModel.GetTypeInfo(type, cancellationToken).Type; - - if (typeSymbol?.SpecialType == SpecialType.System_Object) - return (TExpressionSyntax)expression; - } - - return node; - } + return node; + } - private static TExpressionSyntax? Unwrap(ISyntaxFacts syntaxFacts, TExpressionSyntax node) - { - node = (TExpressionSyntax)syntaxFacts.WalkDownParentheses(node); + private static TExpressionSyntax? Unwrap(ISyntaxFacts syntaxFacts, TExpressionSyntax node) + { + node = (TExpressionSyntax)syntaxFacts.WalkDownParentheses(node); - if (node is TInvocationExpressionSyntax invocation) - return (TExpressionSyntax)syntaxFacts.GetExpressionOfInvocationExpression(invocation); + if (node is TInvocationExpressionSyntax invocation) + return (TExpressionSyntax)syntaxFacts.GetExpressionOfInvocationExpression(invocation); - if (syntaxFacts.IsSimpleMemberAccessExpression(node)) - return (TExpressionSyntax?)syntaxFacts.GetExpressionOfMemberAccessExpression(node); + if (syntaxFacts.IsSimpleMemberAccessExpression(node)) + return (TExpressionSyntax?)syntaxFacts.GetExpressionOfMemberAccessExpression(node); - if (node is TConditionalAccessExpressionSyntax conditionalAccess) - return (TExpressionSyntax)syntaxFacts.GetExpressionOfConditionalAccessExpression(conditionalAccess); + if (node is TConditionalAccessExpressionSyntax conditionalAccess) + return (TExpressionSyntax)syntaxFacts.GetExpressionOfConditionalAccessExpression(conditionalAccess); - if (node is TElementAccessExpressionSyntax elementAccess) - return (TExpressionSyntax?)syntaxFacts.GetExpressionOfElementAccessExpression(elementAccess); + if (node is TElementAccessExpressionSyntax elementAccess) + return (TExpressionSyntax?)syntaxFacts.GetExpressionOfElementAccessExpression(elementAccess); - return null; - } + return null; } } diff --git a/src/Analyzers/Core/Analyzers/UseNullPropagation/AbstractUseNullPropagationDiagnosticAnalyzer_IfStatement.cs b/src/Analyzers/Core/Analyzers/UseNullPropagation/AbstractUseNullPropagationDiagnosticAnalyzer_IfStatement.cs index dc7d31923ad8f..3a37e8aa52d27 100644 --- a/src/Analyzers/Core/Analyzers/UseNullPropagation/AbstractUseNullPropagationDiagnosticAnalyzer_IfStatement.cs +++ b/src/Analyzers/Core/Analyzers/UseNullPropagation/AbstractUseNullPropagationDiagnosticAnalyzer_IfStatement.cs @@ -95,6 +95,7 @@ private void AnalyzeIfStatement( Descriptor, ifStatement.GetFirstToken().GetLocation(), option.Notification, + context.Options, ImmutableArray.Create( ifStatement.GetLocation(), trueStatement.GetLocation(), diff --git a/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs index 687bf64c396e6..8e1c39d240270 100644 --- a/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs @@ -11,10 +11,28 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.UseObjectInitializer -{ - internal abstract partial class AbstractUseObjectInitializerDiagnosticAnalyzer< - TSyntaxKind, +namespace Microsoft.CodeAnalysis.UseObjectInitializer; + +internal abstract partial class AbstractUseObjectInitializerDiagnosticAnalyzer< + TSyntaxKind, + TExpressionSyntax, + TStatementSyntax, + TObjectCreationExpressionSyntax, + TMemberAccessExpressionSyntax, + TAssignmentStatementSyntax, + TLocalDeclarationStatementSyntax, + TVariableDeclaratorSyntax, + TAnalyzer> + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct + where TExpressionSyntax : SyntaxNode + where TStatementSyntax : SyntaxNode + where TObjectCreationExpressionSyntax : TExpressionSyntax + where TMemberAccessExpressionSyntax : TExpressionSyntax + where TAssignmentStatementSyntax : TStatementSyntax + where TLocalDeclarationStatementSyntax : TStatementSyntax + where TVariableDeclaratorSyntax : SyntaxNode + where TAnalyzer : AbstractUseNamedMemberInitializerAnalyzer< TExpressionSyntax, TStatementSyntax, TObjectCreationExpressionSyntax, @@ -22,160 +40,143 @@ internal abstract partial class AbstractUseObjectInitializerDiagnosticAnalyzer< TAssignmentStatementSyntax, TLocalDeclarationStatementSyntax, TVariableDeclaratorSyntax, - TAnalyzer> - : AbstractBuiltInCodeStyleDiagnosticAnalyzer - where TSyntaxKind : struct - where TExpressionSyntax : SyntaxNode - where TStatementSyntax : SyntaxNode - where TObjectCreationExpressionSyntax : TExpressionSyntax - where TMemberAccessExpressionSyntax : TExpressionSyntax - where TAssignmentStatementSyntax : TStatementSyntax - where TLocalDeclarationStatementSyntax : TStatementSyntax - where TVariableDeclaratorSyntax : SyntaxNode - where TAnalyzer : AbstractUseNamedMemberInitializerAnalyzer< - TExpressionSyntax, - TStatementSyntax, - TObjectCreationExpressionSyntax, - TMemberAccessExpressionSyntax, - TAssignmentStatementSyntax, - TLocalDeclarationStatementSyntax, - TVariableDeclaratorSyntax, - TAnalyzer>, new() + TAnalyzer>, new() +{ + private static readonly DiagnosticDescriptor s_descriptor = CreateDescriptorWithId( + IDEDiagnosticIds.UseObjectInitializerDiagnosticId, + EnforceOnBuildValues.UseObjectInitializer, + hasAnyCodeStyleOption: true, + new LocalizableResourceString(nameof(AnalyzersResources.Simplify_object_initialization), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Object_initialization_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + isUnnecessary: false); + + private static readonly DiagnosticDescriptor s_unnecessaryCodeDescriptor = CreateDescriptorWithId( + IDEDiagnosticIds.UseObjectInitializerDiagnosticId, + EnforceOnBuildValues.UseObjectInitializer, + hasAnyCodeStyleOption: true, + new LocalizableResourceString(nameof(AnalyzersResources.Simplify_object_initialization), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Object_initialization_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + isUnnecessary: true); + + protected abstract bool FadeOutOperatorToken { get; } + protected abstract TAnalyzer GetAnalyzer(); + + protected AbstractUseObjectInitializerDiagnosticAnalyzer() + : base(ImmutableDictionary.Empty + .Add(s_descriptor, CodeStyleOptions2.PreferObjectInitializer) + .Add(s_unnecessaryCodeDescriptor, CodeStyleOptions2.PreferObjectInitializer)) { - private static readonly DiagnosticDescriptor s_descriptor = CreateDescriptorWithId( - IDEDiagnosticIds.UseObjectInitializerDiagnosticId, - EnforceOnBuildValues.UseObjectInitializer, - hasAnyCodeStyleOption: true, - new LocalizableResourceString(nameof(AnalyzersResources.Simplify_object_initialization), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Object_initialization_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - isUnnecessary: false); - - private static readonly DiagnosticDescriptor s_unnecessaryCodeDescriptor = CreateDescriptorWithId( - IDEDiagnosticIds.UseObjectInitializerDiagnosticId, - EnforceOnBuildValues.UseObjectInitializer, - hasAnyCodeStyleOption: true, - new LocalizableResourceString(nameof(AnalyzersResources.Simplify_object_initialization), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Object_initialization_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - isUnnecessary: true); - - protected abstract bool FadeOutOperatorToken { get; } - protected abstract TAnalyzer GetAnalyzer(); - - protected AbstractUseObjectInitializerDiagnosticAnalyzer() - : base(ImmutableDictionary.Empty - .Add(s_descriptor, CodeStyleOptions2.PreferObjectInitializer) - .Add(s_unnecessaryCodeDescriptor, CodeStyleOptions2.PreferObjectInitializer)) - { - } + } - protected abstract ISyntaxFacts GetSyntaxFacts(); + protected abstract ISyntaxFacts GetSyntaxFacts(); - protected sealed override void InitializeWorker(AnalysisContext context) + protected sealed override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - context.RegisterCompilationStartAction(context => - { - if (!AreObjectInitializersSupported(context.Compilation)) - return; - - var syntaxKinds = GetSyntaxFacts().SyntaxKinds; - using var matchKinds = TemporaryArray.Empty; - matchKinds.Add(syntaxKinds.Convert(syntaxKinds.ObjectCreationExpression)); - if (syntaxKinds.ImplicitObjectCreationExpression != null) - matchKinds.Add(syntaxKinds.Convert(syntaxKinds.ImplicitObjectCreationExpression.Value)); - var matchKindsArray = matchKinds.ToImmutableAndClear(); - - // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to - // get callbacks for object creation expression nodes, but analyze nodes across the entire code block - // and eventually report fading diagnostics with location outside this node. - // Without the containing CodeBlockStartAction, our reported diagnostic would be classified - // as a non-local diagnostic and would not participate in lightbulb for computing code fixes. - context.RegisterCodeBlockStartAction(blockStartContext => - blockStartContext.RegisterSyntaxNodeAction(AnalyzeNode, matchKindsArray)); - }); - } + if (!AreObjectInitializersSupported(context.Compilation)) + return; + + var syntaxKinds = GetSyntaxFacts().SyntaxKinds; + using var matchKinds = TemporaryArray.Empty; + matchKinds.Add(syntaxKinds.Convert(syntaxKinds.ObjectCreationExpression)); + if (syntaxKinds.ImplicitObjectCreationExpression != null) + matchKinds.Add(syntaxKinds.Convert(syntaxKinds.ImplicitObjectCreationExpression.Value)); + var matchKindsArray = matchKinds.ToImmutableAndClear(); + + // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to + // get callbacks for object creation expression nodes, but analyze nodes across the entire code block + // and eventually report fading diagnostics with location outside this node. + // Without the containing CodeBlockStartAction, our reported diagnostic would be classified + // as a non-local diagnostic and would not participate in lightbulb for computing code fixes. + context.RegisterCodeBlockStartAction(blockStartContext => + blockStartContext.RegisterSyntaxNodeAction(AnalyzeNode, matchKindsArray)); + }); + } - protected abstract bool AreObjectInitializersSupported(Compilation compilation); + protected abstract bool AreObjectInitializersSupported(Compilation compilation); - protected abstract bool IsValidContainingStatement(TStatementSyntax node); + protected abstract bool IsValidContainingStatement(TStatementSyntax node); - private void AnalyzeNode(SyntaxNodeAnalysisContext context) + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var semanticModel = context.SemanticModel; + var objectCreationExpression = (TObjectCreationExpressionSyntax)context.Node; + var language = objectCreationExpression.Language; + var option = context.GetAnalyzerOptions().PreferObjectInitializer; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) { - var semanticModel = context.SemanticModel; - var objectCreationExpression = (TObjectCreationExpressionSyntax)context.Node; - var language = objectCreationExpression.Language; - var option = context.GetAnalyzerOptions().PreferObjectInitializer; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - { - // not point in analyzing if the option is off. - return; - } + // not point in analyzing if the option is off. + return; + } - var syntaxFacts = GetSyntaxFacts(); - using var analyzer = GetAnalyzer(); - var matches = analyzer.Analyze(semanticModel, syntaxFacts, objectCreationExpression, context.CancellationToken); + var syntaxFacts = GetSyntaxFacts(); + using var analyzer = GetAnalyzer(); + var matches = analyzer.Analyze(semanticModel, syntaxFacts, objectCreationExpression, context.CancellationToken); - if (matches.IsDefaultOrEmpty) - return; + if (matches.IsDefaultOrEmpty) + return; - var containingStatement = objectCreationExpression.FirstAncestorOrSelf(); - if (containingStatement == null) - return; + var containingStatement = objectCreationExpression.FirstAncestorOrSelf(); + if (containingStatement == null) + return; - if (!IsValidContainingStatement(containingStatement)) - return; + if (!IsValidContainingStatement(containingStatement)) + return; - var nodes = ImmutableArray.Create(containingStatement).AddRange(matches.Select(m => m.Statement)); - if (syntaxFacts.ContainsInterleavedDirective(nodes, context.CancellationToken)) - return; + var nodes = ImmutableArray.Create(containingStatement).AddRange(matches.Select(m => m.Statement)); + if (syntaxFacts.ContainsInterleavedDirective(nodes, context.CancellationToken)) + return; - var locations = ImmutableArray.Create(objectCreationExpression.GetLocation()); + var locations = ImmutableArray.Create(objectCreationExpression.GetLocation()); - context.ReportDiagnostic(DiagnosticHelper.Create( - s_descriptor, - objectCreationExpression.GetFirstToken().GetLocation(), - option.Notification, - locations, - properties: null)); + context.ReportDiagnostic(DiagnosticHelper.Create( + s_descriptor, + objectCreationExpression.GetFirstToken().GetLocation(), + option.Notification, + context.Options, + locations, + properties: null)); - FadeOutCode(context, matches, locations); - } + FadeOutCode(context, matches, locations); + } - private void FadeOutCode( - SyntaxNodeAnalysisContext context, - ImmutableArray> matches, - ImmutableArray locations) + private void FadeOutCode( + SyntaxNodeAnalysisContext context, + ImmutableArray> matches, + ImmutableArray locations) + { + var syntaxTree = context.Node.SyntaxTree; + + var syntaxFacts = GetSyntaxFacts(); + + foreach (var match in matches) { - var syntaxTree = context.Node.SyntaxTree; + var end = FadeOutOperatorToken + ? syntaxFacts.GetOperatorTokenOfMemberAccessExpression(match.MemberAccessExpression).Span.End + : syntaxFacts.GetExpressionOfMemberAccessExpression(match.MemberAccessExpression)!.Span.End; - var syntaxFacts = GetSyntaxFacts(); + var location1 = Location.Create(syntaxTree, TextSpan.FromBounds( + match.MemberAccessExpression.SpanStart, end)); - foreach (var match in matches) + if (match.Statement.Span.End > match.Initializer.FullSpan.End) { - var end = FadeOutOperatorToken - ? syntaxFacts.GetOperatorTokenOfMemberAccessExpression(match.MemberAccessExpression).Span.End - : syntaxFacts.GetExpressionOfMemberAccessExpression(match.MemberAccessExpression)!.Span.End; - - var location1 = Location.Create(syntaxTree, TextSpan.FromBounds( - match.MemberAccessExpression.SpanStart, end)); - - if (match.Statement.Span.End > match.Initializer.FullSpan.End) - { - context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( - s_unnecessaryCodeDescriptor, - location1, - NotificationOption2.ForSeverity(s_unnecessaryCodeDescriptor.DefaultSeverity), - additionalLocations: locations, - additionalUnnecessaryLocations: [syntaxTree.GetLocation(TextSpan.FromBounds(match.Initializer.FullSpan.End, match.Statement.Span.End))])); - } - else - { - context.ReportDiagnostic(Diagnostic.Create( - s_unnecessaryCodeDescriptor, location1, additionalLocations: locations)); - } + context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( + s_unnecessaryCodeDescriptor, + location1, + NotificationOption2.ForSeverity(s_unnecessaryCodeDescriptor.DefaultSeverity), + context.Options, + additionalLocations: locations, + additionalUnnecessaryLocations: [syntaxTree.GetLocation(TextSpan.FromBounds(match.Initializer.FullSpan.End, match.Statement.Span.End))])); + } + else + { + context.ReportDiagnostic(Diagnostic.Create( + s_unnecessaryCodeDescriptor, location1, additionalLocations: locations)); } } - - public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; } + + public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; } diff --git a/src/Analyzers/Core/Analyzers/UseSystemHashCode/UseSystemHashCodeDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseSystemHashCode/UseSystemHashCodeDiagnosticAnalyzer.cs index 9ae7ad8d620ba..773fc1694d26e 100644 --- a/src/Analyzers/Core/Analyzers/UseSystemHashCode/UseSystemHashCodeDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseSystemHashCode/UseSystemHashCodeDiagnosticAnalyzer.cs @@ -8,80 +8,80 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.UseSystemHashCode +namespace Microsoft.CodeAnalysis.UseSystemHashCode; + +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +internal class UseSystemHashCodeDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - internal class UseSystemHashCodeDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + public UseSystemHashCodeDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseSystemHashCode, + EnforceOnBuildValues.UseSystemHashCode, + option: null, + new LocalizableResourceString(nameof(AnalyzersResources.Use_System_HashCode), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.GetHashCode_implementation_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - public UseSystemHashCodeDiagnosticAnalyzer() - : base(IDEDiagnosticIds.UseSystemHashCode, - EnforceOnBuildValues.UseSystemHashCode, - option: null, - new LocalizableResourceString(nameof(AnalyzersResources.Use_System_HashCode), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.GetHashCode_implementation_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected override void InitializeWorker(AnalysisContext context) + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(c => { - context.RegisterCompilationStartAction(c => + // var hashCodeType = c.Compilation.GetTypeByMetadataName("System.HashCode"); + if (HashCodeAnalyzer.TryGetAnalyzer(c.Compilation, out var analyzer)) { - // var hashCodeType = c.Compilation.GetTypeByMetadataName("System.HashCode"); - if (HashCodeAnalyzer.TryGetAnalyzer(c.Compilation, out var analyzer)) - { - c.RegisterOperationBlockAction(ctx => AnalyzeOperationBlock(analyzer, ctx)); - } - }); - } + c.RegisterOperationBlockAction(ctx => AnalyzeOperationBlock(analyzer, ctx)); + } + }); + } - private void AnalyzeOperationBlock(HashCodeAnalyzer analyzer, OperationBlockAnalysisContext context) - { - if (context.OperationBlocks.Length != 1) - return; + private void AnalyzeOperationBlock(HashCodeAnalyzer analyzer, OperationBlockAnalysisContext context) + { + if (context.OperationBlocks.Length != 1) + return; - var owningSymbol = context.OwningSymbol; - var diagnosticLocation = owningSymbol.Locations[0]; - if (!context.ShouldAnalyzeSpan(diagnosticLocation.SourceSpan)) - return; + var owningSymbol = context.OwningSymbol; + var diagnosticLocation = owningSymbol.Locations[0]; + if (!context.ShouldAnalyzeSpan(diagnosticLocation.SourceSpan)) + return; - var operation = context.OperationBlocks[0]; - var (accessesBase, hashedMembers, statements) = analyzer.GetHashedMembers(owningSymbol, operation); - var elementCount = (accessesBase ? 1 : 0) + (hashedMembers.IsDefaultOrEmpty ? 0 : hashedMembers.Length); + var operation = context.OperationBlocks[0]; + var (accessesBase, hashedMembers, statements) = analyzer.GetHashedMembers(owningSymbol, operation); + var elementCount = (accessesBase ? 1 : 0) + (hashedMembers.IsDefaultOrEmpty ? 0 : hashedMembers.Length); - // No members to call into HashCode.Combine with. Don't offer anything here. - if (elementCount == 0) - return; + // No members to call into HashCode.Combine with. Don't offer anything here. + if (elementCount == 0) + return; - // Just one member to call into HashCode.Combine. Only offer this if we have multiple statements that we can - // reduce to a single statement. It's not worth it to offer to replace: - // - // `return x.GetHashCode();` with `return HashCode.Combine(x);` - // - // But it is work it to offer to replace: - // - // `return (a, b).GetHashCode();` with `return HashCode.Combine(a, b);` - if (elementCount == 1 && statements.Length < 2) - return; + // Just one member to call into HashCode.Combine. Only offer this if we have multiple statements that we can + // reduce to a single statement. It's not worth it to offer to replace: + // + // `return x.GetHashCode();` with `return HashCode.Combine(x);` + // + // But it is work it to offer to replace: + // + // `return (a, b).GetHashCode();` with `return HashCode.Combine(a, b);` + if (elementCount == 1 && statements.Length < 2) + return; - // We've got multiple members to hash, or multiple statements that can be reduced at this point. - Debug.Assert(elementCount >= 2 || statements.Length >= 2); + // We've got multiple members to hash, or multiple statements that can be reduced at this point. + Debug.Assert(elementCount >= 2 || statements.Length >= 2); - var option = context.Options.GetIdeOptions().PreferSystemHashCode; - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; + var option = context.Options.GetIdeOptions().PreferSystemHashCode; + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; - var cancellationToken = context.CancellationToken; - var operationLocation = operation.Syntax.GetLocation(); - var declarationLocation = context.OwningSymbol.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken).GetLocation(); - context.ReportDiagnostic(DiagnosticHelper.Create( - Descriptor, - diagnosticLocation, - option.Notification, - [operationLocation, declarationLocation], - ImmutableDictionary.Empty)); - } + var cancellationToken = context.CancellationToken; + var operationLocation = operation.Syntax.GetLocation(); + var declarationLocation = context.OwningSymbol.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken).GetLocation(); + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + diagnosticLocation, + option.Notification, + context.Options, + [operationLocation, declarationLocation], + ImmutableDictionary.Empty)); } } diff --git a/src/Analyzers/Core/Analyzers/UseThrowExpression/AbstractUseThrowExpressionDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseThrowExpression/AbstractUseThrowExpressionDiagnosticAnalyzer.cs index 7dd4789597e5b..2088447ffec98 100644 --- a/src/Analyzers/Core/Analyzers/UseThrowExpression/AbstractUseThrowExpressionDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseThrowExpression/AbstractUseThrowExpressionDiagnosticAnalyzer.cs @@ -13,286 +13,285 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.UseThrowExpression +namespace Microsoft.CodeAnalysis.UseThrowExpression; + +/// +/// Looks for patterns of the form: +/// +/// if (a == null) { +/// throw SomeException(); +/// } +/// +/// x = a; +/// +/// +/// and offers to change it to +/// +/// +/// x = a ?? throw SomeException(); +/// +/// +/// Note: this analyzer can be updated to run on VB once VB supports 'throw' +/// expressions as well. +/// +internal abstract class AbstractUseThrowExpressionDiagnosticAnalyzer : + AbstractBuiltInCodeStyleDiagnosticAnalyzer { - /// - /// Looks for patterns of the form: - /// - /// if (a == null) { - /// throw SomeException(); - /// } - /// - /// x = a; - /// - /// - /// and offers to change it to - /// - /// - /// x = a ?? throw SomeException(); - /// - /// - /// Note: this analyzer can be updated to run on VB once VB supports 'throw' - /// expressions as well. - /// - internal abstract class AbstractUseThrowExpressionDiagnosticAnalyzer : - AbstractBuiltInCodeStyleDiagnosticAnalyzer + protected AbstractUseThrowExpressionDiagnosticAnalyzer(Option2> preferThrowExpressionOption) + : base(IDEDiagnosticIds.UseThrowExpressionDiagnosticId, + EnforceOnBuildValues.UseThrowExpression, + preferThrowExpressionOption, + new LocalizableResourceString(nameof(AnalyzersResources.Use_throw_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), + new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) { - protected AbstractUseThrowExpressionDiagnosticAnalyzer(Option2> preferThrowExpressionOption) - : base(IDEDiagnosticIds.UseThrowExpressionDiagnosticId, - EnforceOnBuildValues.UseThrowExpression, - preferThrowExpressionOption, - new LocalizableResourceString(nameof(AnalyzersResources.Use_throw_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)), - new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) - { - } + } - protected abstract ISemanticFacts SemanticFacts { get; } - protected abstract CodeStyleOption2 PreferThrowExpressionStyle(OperationAnalysisContext context); - protected abstract bool IsSupported(Compilation compilation); + protected abstract ISemanticFacts SemanticFacts { get; } + protected abstract CodeStyleOption2 PreferThrowExpressionStyle(OperationAnalysisContext context); + protected abstract bool IsSupported(Compilation compilation); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - protected sealed override void InitializeWorker(AnalysisContext context) + protected sealed override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(startContext => { - context.RegisterCompilationStartAction(startContext => + if (!IsSupported(startContext.Compilation)) { - if (!IsSupported(startContext.Compilation)) - { - return; - } - - var expressionType = startContext.Compilation.ExpressionOfTType(); - startContext.RegisterOperationAction(operationContext => AnalyzeOperation(operationContext, expressionType), OperationKind.Throw); - }); - } - - private void AnalyzeOperation(OperationAnalysisContext context, INamedTypeSymbol? expressionType) - { - var cancellationToken = context.CancellationToken; - - var throwOperation = (IThrowOperation)context.Operation; - if (throwOperation.Exception == null) return; + } - var throwStatementSyntax = throwOperation.Syntax; - - var semanticModel = context.Operation.SemanticModel; - Contract.ThrowIfNull(semanticModel); + var expressionType = startContext.Compilation.ExpressionOfTType(); + startContext.RegisterOperationAction(operationContext => AnalyzeOperation(operationContext, expressionType), OperationKind.Throw); + }); + } - var ifOperation = GetContainingIfOperation( - semanticModel, throwOperation, cancellationToken); + private void AnalyzeOperation(OperationAnalysisContext context, INamedTypeSymbol? expressionType) + { + var cancellationToken = context.CancellationToken; - // This throw statement isn't parented by an if-statement. Nothing to - // do here. - if (ifOperation == null) - return; + var throwOperation = (IThrowOperation)context.Operation; + if (throwOperation.Exception == null) + return; - if (ifOperation.WhenFalse != null) - { - // Can't offer this if the 'if-statement' has an 'else-clause'. - return; - } + var throwStatementSyntax = throwOperation.Syntax; - var option = PreferThrowExpressionStyle(context); - if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) - return; + var semanticModel = context.Operation.SemanticModel; + Contract.ThrowIfNull(semanticModel); - if (this.SemanticFacts.IsInExpressionTree(semanticModel, throwStatementSyntax, expressionType, cancellationToken)) - return; + var ifOperation = GetContainingIfOperation( + semanticModel, throwOperation, cancellationToken); - if (ifOperation.Parent is not IBlockOperation containingBlock) - return; + // This throw statement isn't parented by an if-statement. Nothing to + // do here. + if (ifOperation == null) + return; - if (!TryDecomposeIfCondition(ifOperation, out var localOrParameter)) - return; + if (ifOperation.WhenFalse != null) + { + // Can't offer this if the 'if-statement' has an 'else-clause'. + return; + } - if (!TryFindAssignmentExpression(containingBlock, ifOperation, localOrParameter, - out var expressionStatement, out var assignmentExpression)) - { - return; - } + var option = PreferThrowExpressionStyle(context); + if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) + return; - if (!localOrParameter.GetSymbolType().CanAddNullCheck()) - return; + if (this.SemanticFacts.IsInExpressionTree(semanticModel, throwStatementSyntax, expressionType, cancellationToken)) + return; - // We found an assignment using this local/parameter. Now, just make sure there - // were no intervening accesses between the check and the assignment. - if (ValueIsAccessed( - semanticModel, ifOperation, containingBlock, - localOrParameter, expressionStatement, assignmentExpression)) - { - return; - } + if (ifOperation.Parent is not IBlockOperation containingBlock) + return; - // Ok, there were no intervening writes or accesses. This check+assignment can be simplified. - var allLocations = ImmutableArray.Create( - ifOperation.Syntax.GetLocation(), - throwOperation.Exception.Syntax.GetLocation(), - assignmentExpression.Value.Syntax.GetLocation(), - expressionStatement.Syntax.GetLocation()); + if (!TryDecomposeIfCondition(ifOperation, out var localOrParameter)) + return; - context.ReportDiagnostic( - DiagnosticHelper.Create(Descriptor, throwStatementSyntax.GetLocation(), option.Notification, additionalLocations: allLocations, properties: null)); + if (!TryFindAssignmentExpression(containingBlock, ifOperation, localOrParameter, + out var expressionStatement, out var assignmentExpression)) + { + return; } - private static bool ValueIsAccessed(SemanticModel semanticModel, IConditionalOperation ifOperation, IBlockOperation containingBlock, ISymbol localOrParameter, IExpressionStatementOperation expressionStatement, IAssignmentOperation assignmentExpression) + if (!localOrParameter.GetSymbolType().CanAddNullCheck()) + return; + + // We found an assignment using this local/parameter. Now, just make sure there + // were no intervening accesses between the check and the assignment. + if (ValueIsAccessed( + semanticModel, ifOperation, containingBlock, + localOrParameter, expressionStatement, assignmentExpression)) { - var statements = containingBlock.Operations; - var ifOperationIndex = statements.IndexOf(ifOperation); - var expressionStatementIndex = statements.IndexOf(expressionStatement); + return; + } - if (expressionStatementIndex > ifOperationIndex + 1) - { - // There are intermediary statements between the check and the assignment. - // Make sure they don't try to access the local. - var dataFlow = semanticModel.AnalyzeDataFlow( - statements[ifOperationIndex + 1].Syntax, - statements[expressionStatementIndex - 1].Syntax); - - if (dataFlow.ReadInside.Contains(localOrParameter) || - dataFlow.WrittenInside.Contains(localOrParameter)) - { - return true; - } - } + // Ok, there were no intervening writes or accesses. This check+assignment can be simplified. + var allLocations = ImmutableArray.Create( + ifOperation.Syntax.GetLocation(), + throwOperation.Exception.Syntax.GetLocation(), + assignmentExpression.Value.Syntax.GetLocation(), + expressionStatement.Syntax.GetLocation()); - // Also, have to make sure there is no read/write of the local/parameter on the left - // of the assignment. For example: map[val.Id] = val; - var exprDataFlow = semanticModel.AnalyzeDataFlow(assignmentExpression.Target.Syntax); - return exprDataFlow.ReadInside.Contains(localOrParameter) || - exprDataFlow.WrittenInside.Contains(localOrParameter); - } + context.ReportDiagnostic( + DiagnosticHelper.Create(Descriptor, throwStatementSyntax.GetLocation(), option.Notification, context.Options, additionalLocations: allLocations, properties: null)); + } - private static bool TryFindAssignmentExpression( - IBlockOperation containingBlock, IConditionalOperation ifOperation, ISymbol localOrParameter, - [NotNullWhen(true)] out IExpressionStatementOperation? expressionStatement, - [NotNullWhen(true)] out IAssignmentOperation? assignmentExpression) - { - var ifOperationIndex = containingBlock.Operations.IndexOf(ifOperation); + private static bool ValueIsAccessed(SemanticModel semanticModel, IConditionalOperation ifOperation, IBlockOperation containingBlock, ISymbol localOrParameter, IExpressionStatementOperation expressionStatement, IAssignmentOperation assignmentExpression) + { + var statements = containingBlock.Operations; + var ifOperationIndex = statements.IndexOf(ifOperation); + var expressionStatementIndex = statements.IndexOf(expressionStatement); - // walk forward until we find an assignment of this local/parameter into - // something else. - for (var i = ifOperationIndex + 1; i < containingBlock.Operations.Length; i++) + if (expressionStatementIndex > ifOperationIndex + 1) + { + // There are intermediary statements between the check and the assignment. + // Make sure they don't try to access the local. + var dataFlow = semanticModel.AnalyzeDataFlow( + statements[ifOperationIndex + 1].Syntax, + statements[expressionStatementIndex - 1].Syntax); + + if (dataFlow.ReadInside.Contains(localOrParameter) || + dataFlow.WrittenInside.Contains(localOrParameter)) { - expressionStatement = containingBlock.Operations[i] as IExpressionStatementOperation; - if (expressionStatement == null) - { - continue; - } - - assignmentExpression = expressionStatement.Operation as IAssignmentOperation; - if (assignmentExpression == null) - { - continue; - } - - if (!TryGetLocalOrParameterSymbol(assignmentExpression.Value, out var assignmentValue)) - { - continue; - } - - if (!Equals(localOrParameter, assignmentValue)) - { - continue; - } - return true; } - - expressionStatement = null; - assignmentExpression = null; - return false; } - private static bool TryDecomposeIfCondition( - IConditionalOperation ifStatement, - [NotNullWhen(true)] out ISymbol? localOrParameter) - { - localOrParameter = null; + // Also, have to make sure there is no read/write of the local/parameter on the left + // of the assignment. For example: map[val.Id] = val; + var exprDataFlow = semanticModel.AnalyzeDataFlow(assignmentExpression.Target.Syntax); + return exprDataFlow.ReadInside.Contains(localOrParameter) || + exprDataFlow.WrittenInside.Contains(localOrParameter); + } - var condition = ifStatement.Condition; - if (condition is not IBinaryOperation binaryOperator) + private static bool TryFindAssignmentExpression( + IBlockOperation containingBlock, IConditionalOperation ifOperation, ISymbol localOrParameter, + [NotNullWhen(true)] out IExpressionStatementOperation? expressionStatement, + [NotNullWhen(true)] out IAssignmentOperation? assignmentExpression) + { + var ifOperationIndex = containingBlock.Operations.IndexOf(ifOperation); + + // walk forward until we find an assignment of this local/parameter into + // something else. + for (var i = ifOperationIndex + 1; i < containingBlock.Operations.Length; i++) + { + expressionStatement = containingBlock.Operations[i] as IExpressionStatementOperation; + if (expressionStatement == null) { - return false; + continue; } - if (binaryOperator.OperatorKind != BinaryOperatorKind.Equals) + assignmentExpression = expressionStatement.Operation as IAssignmentOperation; + if (assignmentExpression == null) { - return false; + continue; } - if (IsNull(binaryOperator.LeftOperand)) + if (!TryGetLocalOrParameterSymbol(assignmentExpression.Value, out var assignmentValue)) { - return TryGetLocalOrParameterSymbol( - binaryOperator.RightOperand, out localOrParameter); + continue; } - if (IsNull(binaryOperator.RightOperand)) + if (!Equals(localOrParameter, assignmentValue)) { - return TryGetLocalOrParameterSymbol( - binaryOperator.LeftOperand, out localOrParameter); + continue; } - return false; + return true; } - private static bool TryGetLocalOrParameterSymbol( - IOperation operation, - [NotNullWhen(true)] out ISymbol? localOrParameter) + expressionStatement = null; + assignmentExpression = null; + return false; + } + + private static bool TryDecomposeIfCondition( + IConditionalOperation ifStatement, + [NotNullWhen(true)] out ISymbol? localOrParameter) + { + localOrParameter = null; + + var condition = ifStatement.Condition; + if (condition is not IBinaryOperation binaryOperator) { - if (operation is IConversionOperation conversion && conversion.IsImplicit) - { - return TryGetLocalOrParameterSymbol(conversion.Operand, out localOrParameter); - } - else if (operation is ILocalReferenceOperation localReference) - { - localOrParameter = localReference.Local; - return true; - } - else if (operation is IParameterReferenceOperation parameterReference) - { - localOrParameter = parameterReference.Parameter; - return true; - } + return false; + } - localOrParameter = null; + if (binaryOperator.OperatorKind != BinaryOperatorKind.Equals) + { return false; } - private static bool IsNull(IOperation operation) + if (IsNull(binaryOperator.LeftOperand)) { - return operation.ConstantValue.HasValue && - operation.ConstantValue.Value == null; + return TryGetLocalOrParameterSymbol( + binaryOperator.RightOperand, out localOrParameter); } - private static IConditionalOperation? GetContainingIfOperation( - SemanticModel semanticModel, IThrowOperation throwOperation, - CancellationToken cancellationToken) + if (IsNull(binaryOperator.RightOperand)) { - var throwStatement = throwOperation.Syntax; - var containingOperation = semanticModel.GetOperation(throwStatement.GetRequiredParent(), cancellationToken); + return TryGetLocalOrParameterSymbol( + binaryOperator.LeftOperand, out localOrParameter); + } - if (containingOperation is IBlockOperation block) - { - if (block.Operations.Length != 1) - { - // If we are in a block, then the block must only contain - // the throw statement. - return null; - } - - // C# may have an intermediary block between the throw-statement - // and the if-statement. Walk up one operation higher in that case. - containingOperation = semanticModel.GetOperation(throwStatement.GetRequiredParent().GetRequiredParent(), cancellationToken); - } + return false; + } + + private static bool TryGetLocalOrParameterSymbol( + IOperation operation, + [NotNullWhen(true)] out ISymbol? localOrParameter) + { + if (operation is IConversionOperation conversion && conversion.IsImplicit) + { + return TryGetLocalOrParameterSymbol(conversion.Operand, out localOrParameter); + } + else if (operation is ILocalReferenceOperation localReference) + { + localOrParameter = localReference.Local; + return true; + } + else if (operation is IParameterReferenceOperation parameterReference) + { + localOrParameter = parameterReference.Parameter; + return true; + } + + localOrParameter = null; + return false; + } - if (containingOperation is IConditionalOperation conditionalOperation) + private static bool IsNull(IOperation operation) + { + return operation.ConstantValue.HasValue && + operation.ConstantValue.Value == null; + } + + private static IConditionalOperation? GetContainingIfOperation( + SemanticModel semanticModel, IThrowOperation throwOperation, + CancellationToken cancellationToken) + { + var throwStatement = throwOperation.Syntax; + var containingOperation = semanticModel.GetOperation(throwStatement.GetRequiredParent(), cancellationToken); + + if (containingOperation is IBlockOperation block) + { + if (block.Operations.Length != 1) { - return conditionalOperation; + // If we are in a block, then the block must only contain + // the throw statement. + return null; } - return null; + // C# may have an intermediary block between the throw-statement + // and the if-statement. Walk up one operation higher in that case. + containingOperation = semanticModel.GetOperation(throwStatement.GetRequiredParent().GetRequiredParent(), cancellationToken); } + + if (containingOperation is IConditionalOperation conditionalOperation) + { + return conditionalOperation; + } + + return null; } } diff --git a/src/Analyzers/Core/Analyzers/ValidateFormatString/AbstractValidateFormatStringDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/ValidateFormatString/AbstractValidateFormatStringDiagnosticAnalyzer.cs index 19a43a75906ac..856265eaaea2c 100644 --- a/src/Analyzers/Core/Analyzers/ValidateFormatString/AbstractValidateFormatStringDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/ValidateFormatString/AbstractValidateFormatStringDiagnosticAnalyzer.cs @@ -10,394 +10,393 @@ using Microsoft.CodeAnalysis.LanguageService; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ValidateFormatString +namespace Microsoft.CodeAnalysis.ValidateFormatString; + +internal abstract class AbstractValidateFormatStringDiagnosticAnalyzer + : DiagnosticAnalyzer + where TSyntaxKind : struct { - internal abstract class AbstractValidateFormatStringDiagnosticAnalyzer - : DiagnosticAnalyzer - where TSyntaxKind : struct - { - private const string DiagnosticID = IDEDiagnosticIds.ValidateFormatStringDiagnosticID; + private const string DiagnosticID = IDEDiagnosticIds.ValidateFormatStringDiagnosticID; - private static readonly LocalizableString Title = new LocalizableResourceString( - nameof(AnalyzersResources.Invalid_format_string), - AnalyzersResources.ResourceManager, - typeof(AnalyzersResources)); + private static readonly LocalizableString Title = new LocalizableResourceString( + nameof(AnalyzersResources.Invalid_format_string), + AnalyzersResources.ResourceManager, + typeof(AnalyzersResources)); - private static readonly LocalizableString MessageFormat = new LocalizableResourceString( - nameof(AnalyzersResources.Format_string_contains_invalid_placeholder), - AnalyzersResources.ResourceManager, - typeof(AnalyzersResources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString( + nameof(AnalyzersResources.Format_string_contains_invalid_placeholder), + AnalyzersResources.ResourceManager, + typeof(AnalyzersResources)); #pragma warning disable RS0030 // Do not used banned APIs - We cannot use AbstractBuiltInCodeStyleDiagnosticAnalyzer nor AbstractCodeQualityDiagnosticAnalyzer. - // This analyzer is run against generated code while the abstract base classes mentioned doesn't. The rule is also not documented. - // There is even a current work to remove the rule completely in favor of CA2241. - private static readonly DiagnosticDescriptor Rule = new( - DiagnosticID, - Title, - MessageFormat, - DiagnosticCategory.Compiler, - DiagnosticSeverity.Info, - isEnabledByDefault: true, - customTags: EnforceOnBuildValues.ValidateFormatString.ToCustomTag()); + // This analyzer is run against generated code while the abstract base classes mentioned doesn't. The rule is also not documented. + // There is even a current work to remove the rule completely in favor of CA2241. + private static readonly DiagnosticDescriptor Rule = new( + DiagnosticID, + Title, + MessageFormat, + DiagnosticCategory.Compiler, + DiagnosticSeverity.Info, + isEnabledByDefault: true, + customTags: EnforceOnBuildValues.ValidateFormatString.ToCustomTag()); #pragma warning restore RS0030 // Do not used banned APIs - public override ImmutableArray SupportedDiagnostics - => [Rule]; + public override ImmutableArray SupportedDiagnostics + => [Rule]; - /// - /// this regex is used to remove escaped brackets from - /// the format string before looking for valid {} pairs - /// - private static readonly Regex s_removeEscapedBracketsRegex = new("{{"); + /// + /// this regex is used to remove escaped brackets from + /// the format string before looking for valid {} pairs + /// + private static readonly Regex s_removeEscapedBracketsRegex = new("{{"); - /// - /// this regex is used to extract the text between the - /// brackets and save the contents in a MatchCollection - /// - private static readonly Regex s_extractPlaceholdersRegex = new("{(.*?)}"); + /// + /// this regex is used to extract the text between the + /// brackets and save the contents in a MatchCollection + /// + private static readonly Regex s_extractPlaceholdersRegex = new("{(.*?)}"); - private const string NameOfArgsParameter = "args"; - private const string NameOfFormatStringParameter = "format"; + private const string NameOfArgsParameter = "args"; + private const string NameOfFormatStringParameter = "format"; - protected abstract ISyntaxFacts GetSyntaxFacts(); - protected abstract SyntaxNode GetArgumentExpression(SyntaxNode syntaxNode); - protected abstract SyntaxNode? TryGetMatchingNamedArgument(SeparatedSyntaxList arguments, string searchArgumentName); + protected abstract ISyntaxFacts GetSyntaxFacts(); + protected abstract SyntaxNode GetArgumentExpression(SyntaxNode syntaxNode); + protected abstract SyntaxNode? TryGetMatchingNamedArgument(SeparatedSyntaxList arguments, string searchArgumentName); - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - - context.RegisterCompilationStartAction(startContext => - { - var formatProviderType = startContext.Compilation.GetTypeByMetadataName(typeof(System.IFormatProvider).FullName!); - if (formatProviderType == null) - { - return; - } - - var syntaxKinds = GetSyntaxFacts().SyntaxKinds; - startContext.RegisterSyntaxNodeAction( - c => AnalyzeNode(c, formatProviderType), - syntaxKinds.Convert(syntaxKinds.InvocationExpression)); - }); - } + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - [PerformanceSensitive( - "https://github.com/dotnet/roslyn/issues/23583", - Constraint = "Reading editorconfig options is expensive and should be avoided if a syntax-based fast path exists.")] - private void AnalyzeNode(SyntaxNodeAnalysisContext context, INamedTypeSymbol formatProviderType) + context.RegisterCompilationStartAction(startContext => { - var syntaxFacts = GetSyntaxFacts(); - var expression = syntaxFacts.GetExpressionOfInvocationExpression(context.Node); - - if (!IsValidFormatMethod(syntaxFacts, expression)) - { - return; - } - - if (!context.GetIdeAnalyzerOptions().ReportInvalidPlaceholdersInStringDotFormatCalls) - { - return; - } - - var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(context.Node); - var symbolInfo = context.SemanticModel.GetSymbolInfo(expression, context.CancellationToken); - - var method = TryGetValidFormatMethodSymbol(symbolInfo); - if (method == null) + var formatProviderType = startContext.Compilation.GetTypeByMetadataName(typeof(System.IFormatProvider).FullName!); + if (formatProviderType == null) { return; } - var parameters = method.Parameters; + var syntaxKinds = GetSyntaxFacts().SyntaxKinds; + startContext.RegisterSyntaxNodeAction( + c => AnalyzeNode(c, formatProviderType), + syntaxKinds.Convert(syntaxKinds.InvocationExpression)); + }); + } - // If the chosen overload has a params object[] that is being passed an array of - // reference types then an actual array must be being passed instead of passing one - // argument at a time because we know the set of overloads and how overload resolution - // will work. This array may have been created somewhere we can't analyze, so never - // squiggle in this case. - if (ArgsIsArrayOfReferenceTypes(context.SemanticModel, arguments, parameters, syntaxFacts)) - { - return; - } + [PerformanceSensitive( + "https://github.com/dotnet/roslyn/issues/23583", + Constraint = "Reading editorconfig options is expensive and should be avoided if a syntax-based fast path exists.")] + private void AnalyzeNode(SyntaxNodeAnalysisContext context, INamedTypeSymbol formatProviderType) + { + var syntaxFacts = GetSyntaxFacts(); + var expression = syntaxFacts.GetExpressionOfInvocationExpression(context.Node); - var formatStringLiteralExpressionSyntax = TryGetFormatStringLiteralExpressionSyntax( - arguments, parameters, syntaxFacts); + if (!IsValidFormatMethod(syntaxFacts, expression)) + { + return; + } - if (formatStringLiteralExpressionSyntax == null) - { - return; - } + if (!context.GetIdeAnalyzerOptions().ReportInvalidPlaceholdersInStringDotFormatCalls) + { + return; + } - if (parameters.Length == 0) - { - return; - } + var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(context.Node); + var symbolInfo = context.SemanticModel.GetSymbolInfo(expression, context.CancellationToken); - var numberOfArguments = arguments.Count; - var hasIFormatProvider = parameters[0].Type.Equals(formatProviderType); + var method = TryGetValidFormatMethodSymbol(symbolInfo); + if (method == null) + { + return; + } - // We know the format string parameter exists so numberOfArguments is at least one, - // and at least 2 if there is also an IFormatProvider. The result can be zero if - // calling string.Format("String only with no placeholders"), where there is an - // empty params array. - var numberOfPlaceholderArguments = numberOfArguments - (hasIFormatProvider ? 2 : 1); + var parameters = method.Parameters; - var formatString = formatStringLiteralExpressionSyntax.ToString(); + // If the chosen overload has a params object[] that is being passed an array of + // reference types then an actual array must be being passed instead of passing one + // argument at a time because we know the set of overloads and how overload resolution + // will work. This array may have been created somewhere we can't analyze, so never + // squiggle in this case. + if (ArgsIsArrayOfReferenceTypes(context.SemanticModel, arguments, parameters, syntaxFacts)) + { + return; + } - if (FormatCallWorksAtRuntime(formatString, numberOfPlaceholderArguments)) - { - return; - } + var formatStringLiteralExpressionSyntax = TryGetFormatStringLiteralExpressionSyntax( + arguments, parameters, syntaxFacts); - ValidateAndReportDiagnostic(context, numberOfPlaceholderArguments, - formatString, formatStringLiteralExpressionSyntax.SpanStart); + if (formatStringLiteralExpressionSyntax == null) + { + return; } - private static bool IsValidFormatMethod(ISyntaxFacts syntaxFacts, SyntaxNode expression) + if (parameters.Length == 0) { - // When calling string.Format(...), the expression will be MemberAccessExpressionSyntax - if (syntaxFacts.IsSimpleMemberAccessExpression(expression)) - { - var nameOfMemberAccessExpression = syntaxFacts.GetNameOfMemberAccessExpression(expression); - return !syntaxFacts.IsGenericName(nameOfMemberAccessExpression) - && syntaxFacts.GetIdentifierOfSimpleName(nameOfMemberAccessExpression).ValueText - == (nameof(string.Format)); - } + return; + } - // When using static System.String and calling Format(...), the expression will be - // IdentifierNameSyntax - if (syntaxFacts.IsIdentifierName(expression)) - { - return syntaxFacts.GetIdentifierOfSimpleName(expression).ValueText - == (nameof(string.Format)); - } + var numberOfArguments = arguments.Count; + var hasIFormatProvider = parameters[0].Type.Equals(formatProviderType); - return false; - } + // We know the format string parameter exists so numberOfArguments is at least one, + // and at least 2 if there is also an IFormatProvider. The result can be zero if + // calling string.Format("String only with no placeholders"), where there is an + // empty params array. + var numberOfPlaceholderArguments = numberOfArguments - (hasIFormatProvider ? 2 : 1); + + var formatString = formatStringLiteralExpressionSyntax.ToString(); - private bool ArgsIsArrayOfReferenceTypes( - SemanticModel semanticModel, - SeparatedSyntaxList arguments, - ImmutableArray parameters, - ISyntaxFacts syntaxFacts) + if (FormatCallWorksAtRuntime(formatString, numberOfPlaceholderArguments)) { - var argsArgumentType = TryGetArgsArgumentType(semanticModel, arguments, parameters, syntaxFacts); - return argsArgumentType is IArrayTypeSymbol arrayType && arrayType.ElementType.IsReferenceType; + return; } - private ITypeSymbol? TryGetArgsArgumentType( - SemanticModel semanticModel, - SeparatedSyntaxList arguments, - ImmutableArray parameters, - ISyntaxFacts syntaxFacts) - { - var argsArgument = TryGetArgument(NameOfArgsParameter, arguments, parameters); - if (argsArgument == null) - { - return null; - } + ValidateAndReportDiagnostic(context, numberOfPlaceholderArguments, + formatString, formatStringLiteralExpressionSyntax.SpanStart); + } - Debug.Assert(syntaxFacts.IsArgument(argsArgument)); - var expression = syntaxFacts.GetExpressionOfArgument(argsArgument)!; - return semanticModel.GetTypeInfo(expression).ConvertedType; + private static bool IsValidFormatMethod(ISyntaxFacts syntaxFacts, SyntaxNode expression) + { + // When calling string.Format(...), the expression will be MemberAccessExpressionSyntax + if (syntaxFacts.IsSimpleMemberAccessExpression(expression)) + { + var nameOfMemberAccessExpression = syntaxFacts.GetNameOfMemberAccessExpression(expression); + return !syntaxFacts.IsGenericName(nameOfMemberAccessExpression) + && syntaxFacts.GetIdentifierOfSimpleName(nameOfMemberAccessExpression).ValueText + == (nameof(string.Format)); } - protected SyntaxNode? TryGetArgument( - string searchArgumentName, - SeparatedSyntaxList arguments, - ImmutableArray parameters) + // When using static System.String and calling Format(...), the expression will be + // IdentifierNameSyntax + if (syntaxFacts.IsIdentifierName(expression)) { - // First, look for a named argument that matches - var matchingNamedArgument = TryGetMatchingNamedArgument(arguments, searchArgumentName); - if (matchingNamedArgument != null) - { - return matchingNamedArgument; - } + return syntaxFacts.GetIdentifierOfSimpleName(expression).ValueText + == (nameof(string.Format)); + } - // If no named argument exists, look for the named parameter - // and return the corresponding argument - var parameterWithMatchingName = GetParameterWithMatchingName(parameters, searchArgumentName); - if (parameterWithMatchingName == null) - { - return null; - } + return false; + } - // For the case string.Format("Test string"), there is only one argument - // but the compiler created an empty parameter array to bind to an overload - if (parameterWithMatchingName.Ordinal >= arguments.Count) - { - return null; - } + private bool ArgsIsArrayOfReferenceTypes( + SemanticModel semanticModel, + SeparatedSyntaxList arguments, + ImmutableArray parameters, + ISyntaxFacts syntaxFacts) + { + var argsArgumentType = TryGetArgsArgumentType(semanticModel, arguments, parameters, syntaxFacts); + return argsArgumentType is IArrayTypeSymbol arrayType && arrayType.ElementType.IsReferenceType; + } - // Multiple arguments could have been converted to a single params array, - // so there wouldn't be a corresponding argument - if (parameterWithMatchingName.IsParams && parameters.Length != arguments.Count) - { - return null; - } + private ITypeSymbol? TryGetArgsArgumentType( + SemanticModel semanticModel, + SeparatedSyntaxList arguments, + ImmutableArray parameters, + ISyntaxFacts syntaxFacts) + { + var argsArgument = TryGetArgument(NameOfArgsParameter, arguments, parameters); + if (argsArgument == null) + { + return null; + } - return arguments[parameterWithMatchingName.Ordinal]; + Debug.Assert(syntaxFacts.IsArgument(argsArgument)); + var expression = syntaxFacts.GetExpressionOfArgument(argsArgument)!; + return semanticModel.GetTypeInfo(expression).ConvertedType; + } + + protected SyntaxNode? TryGetArgument( + string searchArgumentName, + SeparatedSyntaxList arguments, + ImmutableArray parameters) + { + // First, look for a named argument that matches + var matchingNamedArgument = TryGetMatchingNamedArgument(arguments, searchArgumentName); + if (matchingNamedArgument != null) + { + return matchingNamedArgument; } - private static IParameterSymbol? GetParameterWithMatchingName(ImmutableArray parameters, string searchArgumentName) + // If no named argument exists, look for the named parameter + // and return the corresponding argument + var parameterWithMatchingName = GetParameterWithMatchingName(parameters, searchArgumentName); + if (parameterWithMatchingName == null) { - foreach (var p in parameters) - { - if (p.Name == searchArgumentName) - { - return p; - } - } + return null; + } + // For the case string.Format("Test string"), there is only one argument + // but the compiler created an empty parameter array to bind to an overload + if (parameterWithMatchingName.Ordinal >= arguments.Count) + { return null; } - protected SyntaxNode? TryGetFormatStringLiteralExpressionSyntax( - SeparatedSyntaxList arguments, - ImmutableArray parameters, - ISyntaxFacts syntaxFacts) + // Multiple arguments could have been converted to a single params array, + // so there wouldn't be a corresponding argument + if (parameterWithMatchingName.IsParams && parameters.Length != arguments.Count) { - var formatArgumentSyntax = TryGetArgument( - NameOfFormatStringParameter, - arguments, - parameters); + return null; + } - if (formatArgumentSyntax == null) - { - return null; - } + return arguments[parameterWithMatchingName.Ordinal]; + } - if (!syntaxFacts.IsStringLiteralExpression(syntaxFacts.GetExpressionOfArgument(formatArgumentSyntax))) + private static IParameterSymbol? GetParameterWithMatchingName(ImmutableArray parameters, string searchArgumentName) + { + foreach (var p in parameters) + { + if (p.Name == searchArgumentName) { - return null; + return p; } + } + + return null; + } - return GetArgumentExpression(formatArgumentSyntax); + protected SyntaxNode? TryGetFormatStringLiteralExpressionSyntax( + SeparatedSyntaxList arguments, + ImmutableArray parameters, + ISyntaxFacts syntaxFacts) + { + var formatArgumentSyntax = TryGetArgument( + NameOfFormatStringParameter, + arguments, + parameters); + + if (formatArgumentSyntax == null) + { + return null; } - protected static IMethodSymbol? TryGetValidFormatMethodSymbol(SymbolInfo symbolInfo) + if (!syntaxFacts.IsStringLiteralExpression(syntaxFacts.GetExpressionOfArgument(formatArgumentSyntax))) { - if (symbolInfo.Symbol == null) - { - return null; - } + return null; + } - if (symbolInfo.Symbol.Kind != SymbolKind.Method) - { - return null; - } + return GetArgumentExpression(formatArgumentSyntax); + } - if (((IMethodSymbol)symbolInfo.Symbol).MethodKind == MethodKind.LocalFunction) - { - return null; - } + protected static IMethodSymbol? TryGetValidFormatMethodSymbol(SymbolInfo symbolInfo) + { + if (symbolInfo.Symbol == null) + { + return null; + } - var containingType = symbolInfo.Symbol.ContainingType; - if (containingType == null) - { - return null; - } + if (symbolInfo.Symbol.Kind != SymbolKind.Method) + { + return null; + } - if (containingType.SpecialType != SpecialType.System_String) - { - return null; - } + if (((IMethodSymbol)symbolInfo.Symbol).MethodKind == MethodKind.LocalFunction) + { + return null; + } - return (IMethodSymbol)symbolInfo.Symbol; + var containingType = symbolInfo.Symbol.ContainingType; + if (containingType == null) + { + return null; } - private static bool FormatCallWorksAtRuntime(string formatString, int numberOfPlaceholderArguments) + if (containingType.SpecialType != SpecialType.System_String) { - var testArray = new object[numberOfPlaceholderArguments]; - for (var i = 0; i < numberOfPlaceholderArguments; i++) - { - testArray[i] = "test"; - } + return null; + } - try - { - string.Format(formatString, testArray); - } - catch - { - return false; - } + return (IMethodSymbol)symbolInfo.Symbol; + } - return true; + private static bool FormatCallWorksAtRuntime(string formatString, int numberOfPlaceholderArguments) + { + var testArray = new object[numberOfPlaceholderArguments]; + for (var i = 0; i < numberOfPlaceholderArguments; i++) + { + testArray[i] = "test"; } - protected static void ValidateAndReportDiagnostic( - SyntaxNodeAnalysisContext context, - int numberOfPlaceholderArguments, - string formatString, - int formatStringPosition) + try { - // removing escaped left brackets and replacing with space characters so they won't - // impede the extraction of placeholders, yet the locations of the placeholders are - // the same as in the original string. - var formatStringWithEscapedBracketsChangedToSpaces = RemoveEscapedBrackets(formatString); - - var matches = s_extractPlaceholdersRegex.Matches(formatStringWithEscapedBracketsChangedToSpaces); - foreach (Match? match in matches) - { - RoslynDebug.AssertNotNull(match); - - var textInsideBrackets = match.Groups[1].Value; - - if (!PlaceholderIndexIsValid(textInsideBrackets, numberOfPlaceholderArguments)) - { - var invalidPlaceholderText = "{" + textInsideBrackets + "}"; - var invalidPlaceholderLocation = Location.Create( - context.Node.SyntaxTree, - new Text.TextSpan( - formatStringPosition + match.Index, - invalidPlaceholderText.Length)); - var diagnostic = Diagnostic.Create( - Rule, - invalidPlaceholderLocation, - invalidPlaceholderText); - context.ReportDiagnostic(diagnostic); - } - } + string.Format(formatString, testArray); + } + catch + { + return false; } - /// - /// removing escaped left brackets and replacing with space characters so they won't - /// impede the extraction of placeholders, yet the locations of the placeholders are - /// the same as in the original string. - /// - /// - /// string with left brackets removed and replaced by spaces - private static string RemoveEscapedBrackets(string formatString) - => s_removeEscapedBracketsRegex.Replace(formatString, " "); - - private static bool PlaceholderIndexIsValid( - string textInsideBrackets, - int numberOfPlaceholderArguments) + return true; + } + + protected static void ValidateAndReportDiagnostic( + SyntaxNodeAnalysisContext context, + int numberOfPlaceholderArguments, + string formatString, + int formatStringPosition) + { + // removing escaped left brackets and replacing with space characters so they won't + // impede the extraction of placeholders, yet the locations of the placeholders are + // the same as in the original string. + var formatStringWithEscapedBracketsChangedToSpaces = RemoveEscapedBrackets(formatString); + + var matches = s_extractPlaceholdersRegex.Matches(formatStringWithEscapedBracketsChangedToSpaces); + foreach (Match? match in matches) { - var placeholderIndexText = textInsideBrackets.IndexOf(",") > 0 - ? textInsideBrackets.Split(',')[0] - : textInsideBrackets.Split(':')[0]; + RoslynDebug.AssertNotNull(match); - // placeholders cannot begin with whitespace - if (placeholderIndexText.Length > 0 && char.IsWhiteSpace(placeholderIndexText, 0)) - { - return false; - } + var textInsideBrackets = match.Groups[1].Value; - if (!int.TryParse(placeholderIndexText, out var placeholderIndex)) + if (!PlaceholderIndexIsValid(textInsideBrackets, numberOfPlaceholderArguments)) { - return false; + var invalidPlaceholderText = "{" + textInsideBrackets + "}"; + var invalidPlaceholderLocation = Location.Create( + context.Node.SyntaxTree, + new Text.TextSpan( + formatStringPosition + match.Index, + invalidPlaceholderText.Length)); + var diagnostic = Diagnostic.Create( + Rule, + invalidPlaceholderLocation, + invalidPlaceholderText); + context.ReportDiagnostic(diagnostic); } + } + } - if (placeholderIndex >= numberOfPlaceholderArguments) - { - return false; - } + /// + /// removing escaped left brackets and replacing with space characters so they won't + /// impede the extraction of placeholders, yet the locations of the placeholders are + /// the same as in the original string. + /// + /// + /// string with left brackets removed and replaced by spaces + private static string RemoveEscapedBrackets(string formatString) + => s_removeEscapedBracketsRegex.Replace(formatString, " "); + + private static bool PlaceholderIndexIsValid( + string textInsideBrackets, + int numberOfPlaceholderArguments) + { + var placeholderIndexText = textInsideBrackets.IndexOf(",") > 0 + ? textInsideBrackets.Split(',')[0] + : textInsideBrackets.Split(':')[0]; - return true; + // placeholders cannot begin with whitespace + if (placeholderIndexText.Length > 0 && char.IsWhiteSpace(placeholderIndexText, 0)) + { + return false; } + + if (!int.TryParse(placeholderIndexText, out var placeholderIndex)) + { + return false; + } + + if (placeholderIndex >= numberOfPlaceholderArguments) + { + return false; + } + + return true; } } diff --git a/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/AbstractAddAccessibilityModifiersCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/AbstractAddAccessibilityModifiersCodeFixProvider.cs index a66d8b35f85ab..513b171f8c9d8 100644 --- a/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/AbstractAddAccessibilityModifiersCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/AbstractAddAccessibilityModifiersCodeFixProvider.cs @@ -14,46 +14,45 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers +namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers; + +internal abstract class AbstractAddAccessibilityModifiersCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - internal abstract class AbstractAddAccessibilityModifiersCodeFixProvider : SyntaxEditorBasedCodeFixProvider - { - protected abstract SyntaxNode MapToDeclarator(SyntaxNode declaration); + protected abstract SyntaxNode MapToDeclarator(SyntaxNode declaration); - public sealed override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.AddAccessibilityModifiersDiagnosticId]; + public sealed override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.AddAccessibilityModifiersDiagnosticId]; - public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var diagnostic = context.Diagnostics.First(); + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); - var priority = diagnostic.Severity == DiagnosticSeverity.Hidden - ? CodeActionPriority.Low - : CodeActionPriority.Default; + var priority = diagnostic.Severity == DiagnosticSeverity.Hidden + ? CodeActionPriority.Low + : CodeActionPriority.Default; - var (title, key) = diagnostic.Properties.ContainsKey(AddAccessibilityModifiersConstants.ModifiersAdded) - ? (AnalyzersResources.Add_accessibility_modifiers, nameof(AnalyzersResources.Add_accessibility_modifiers)) - : (AnalyzersResources.Remove_accessibility_modifiers, nameof(AnalyzersResources.Remove_accessibility_modifiers)); + var (title, key) = diagnostic.Properties.ContainsKey(AddAccessibilityModifiersConstants.ModifiersAdded) + ? (AnalyzersResources.Add_accessibility_modifiers, nameof(AnalyzersResources.Add_accessibility_modifiers)) + : (AnalyzersResources.Remove_accessibility_modifiers, nameof(AnalyzersResources.Remove_accessibility_modifiers)); - RegisterCodeFix(context, title, key, priority); + RegisterCodeFix(context, title, key, priority); - return Task.CompletedTask; - } + return Task.CompletedTask; + } + + protected sealed override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - protected sealed override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + foreach (var diagnostic in diagnostics) { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - foreach (var diagnostic in diagnostics) - { - var declaration = diagnostic.AdditionalLocations[0].FindNode(cancellationToken); - var declarator = MapToDeclarator(declaration); - var symbol = semanticModel.GetDeclaredSymbol(declarator, cancellationToken); - Contract.ThrowIfNull(symbol); - AddAccessibilityModifiersHelpers.UpdateDeclaration(editor, symbol, declaration); - } + var declaration = diagnostic.AdditionalLocations[0].FindNode(cancellationToken); + var declarator = MapToDeclarator(declaration); + var symbol = semanticModel.GetDeclaredSymbol(declarator, cancellationToken); + Contract.ThrowIfNull(symbol); + AddAccessibilityModifiersHelpers.UpdateDeclaration(editor, symbol, declaration); } } } diff --git a/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/AddAccessibilityModifiersHelpers.cs b/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/AddAccessibilityModifiersHelpers.cs index 2cd6856fe3a14..73dde711921c7 100644 --- a/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/AddAccessibilityModifiersHelpers.cs +++ b/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/AddAccessibilityModifiersHelpers.cs @@ -7,57 +7,56 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers +namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers; + +internal static partial class AddAccessibilityModifiersHelpers { - internal static partial class AddAccessibilityModifiersHelpers + public static void UpdateDeclaration( + SyntaxEditor editor, ISymbol symbol, SyntaxNode declaration) { - public static void UpdateDeclaration( - SyntaxEditor editor, ISymbol symbol, SyntaxNode declaration) - { - Contract.ThrowIfNull(symbol); + Contract.ThrowIfNull(symbol); - var preferredAccessibility = GetPreferredAccessibility(symbol); + var preferredAccessibility = GetPreferredAccessibility(symbol); - // Check to see if we need to add or remove - // If there's a modifier, then we need to remove it, otherwise no modifier, add it. - editor.ReplaceNode( - declaration, - (currentDeclaration, _) => UpdateAccessibility(currentDeclaration, preferredAccessibility)); + // Check to see if we need to add or remove + // If there's a modifier, then we need to remove it, otherwise no modifier, add it. + editor.ReplaceNode( + declaration, + (currentDeclaration, _) => UpdateAccessibility(currentDeclaration, preferredAccessibility)); - return; + return; - SyntaxNode UpdateAccessibility(SyntaxNode declaration, Accessibility preferredAccessibility) - { - var generator = editor.Generator; + SyntaxNode UpdateAccessibility(SyntaxNode declaration, Accessibility preferredAccessibility) + { + var generator = editor.Generator; - // If there was accessibility on the member, then remove it. If there was no accessibility, then add - // the preferred accessibility for this member. - return generator.GetAccessibility(declaration) == Accessibility.NotApplicable - ? generator.WithAccessibility(declaration, preferredAccessibility) - : generator.WithAccessibility(declaration, Accessibility.NotApplicable); - } + // If there was accessibility on the member, then remove it. If there was no accessibility, then add + // the preferred accessibility for this member. + return generator.GetAccessibility(declaration) == Accessibility.NotApplicable + ? generator.WithAccessibility(declaration, preferredAccessibility) + : generator.WithAccessibility(declaration, Accessibility.NotApplicable); } + } - private static Accessibility GetPreferredAccessibility(ISymbol symbol) + private static Accessibility GetPreferredAccessibility(ISymbol symbol) + { + // If we have an overridden member, then if we're adding an accessibility modifier, use the + // accessibility of the member we're overriding as both should be consistent here. + if (symbol.GetOverriddenMember() is { DeclaredAccessibility: var accessibility }) + return accessibility; + + // Default abstract members to be protected, and virtual members to be public. They can't be private as + // that's not legal. And these are reasonable default values for them. + if (symbol is IMethodSymbol or IPropertySymbol or IEventSymbol) { - // If we have an overridden member, then if we're adding an accessibility modifier, use the - // accessibility of the member we're overriding as both should be consistent here. - if (symbol.GetOverriddenMember() is { DeclaredAccessibility: var accessibility }) - return accessibility; - - // Default abstract members to be protected, and virtual members to be public. They can't be private as - // that's not legal. And these are reasonable default values for them. - if (symbol is IMethodSymbol or IPropertySymbol or IEventSymbol) - { - if (symbol.IsAbstract) - return Accessibility.Protected; - - if (symbol.IsVirtual) - return Accessibility.Public; - } - - // Otherwise, default to whatever accessibility no-accessibility means for this member; - return symbol.DeclaredAccessibility; + if (symbol.IsAbstract) + return Accessibility.Protected; + + if (symbol.IsVirtual) + return Accessibility.Public; } + + // Otherwise, default to whatever accessibility no-accessibility means for this member; + return symbol.DeclaredAccessibility; } } diff --git a/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/IAddAccessibilityModifiersService.cs b/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/IAddAccessibilityModifiersService.cs index 93c6f7ff23281..21dce6e78ca36 100644 --- a/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/IAddAccessibilityModifiersService.cs +++ b/src/Analyzers/Core/CodeFixes/AddAccessibilityModifiers/IAddAccessibilityModifiersService.cs @@ -4,9 +4,8 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers +namespace Microsoft.CodeAnalysis.AddAccessibilityModifiers; + +internal interface IAddAccessibilityModifiersService : IAddAccessibilityModifiers, ILanguageService { - internal interface IAddAccessibilityModifiersService : IAddAccessibilityModifiers, ILanguageService - { - } } diff --git a/src/Analyzers/Core/CodeFixes/AddAnonymousTypeMemberName/AbstractAddAnonymousTypeMemberNameCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/AddAnonymousTypeMemberName/AbstractAddAnonymousTypeMemberNameCodeFixProvider.cs index b073138af0ae1..a957390cda11e 100644 --- a/src/Analyzers/Core/CodeFixes/AddAnonymousTypeMemberName/AbstractAddAnonymousTypeMemberNameCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/AddAnonymousTypeMemberName/AbstractAddAnonymousTypeMemberNameCodeFixProvider.cs @@ -14,124 +14,123 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.AddAnonymousTypeMemberName +namespace Microsoft.CodeAnalysis.AddAnonymousTypeMemberName; + +internal abstract class AbstractAddAnonymousTypeMemberNameCodeFixProvider< + TExpressionSyntax, + TAnonymousObjectInitializer, + TAnonymousObjectMemberDeclaratorSyntax> + : SyntaxEditorBasedCodeFixProvider + where TExpressionSyntax : SyntaxNode + where TAnonymousObjectInitializer : SyntaxNode + where TAnonymousObjectMemberDeclaratorSyntax : SyntaxNode { - internal abstract class AbstractAddAnonymousTypeMemberNameCodeFixProvider< - TExpressionSyntax, - TAnonymousObjectInitializer, - TAnonymousObjectMemberDeclaratorSyntax> - : SyntaxEditorBasedCodeFixProvider - where TExpressionSyntax : SyntaxNode - where TAnonymousObjectInitializer : SyntaxNode - where TAnonymousObjectMemberDeclaratorSyntax : SyntaxNode + protected abstract bool HasName(TAnonymousObjectMemberDeclaratorSyntax declarator); + protected abstract TExpressionSyntax GetExpression(TAnonymousObjectMemberDeclaratorSyntax declarator); + protected abstract TAnonymousObjectMemberDeclaratorSyntax WithName(TAnonymousObjectMemberDeclaratorSyntax declarator, SyntaxToken name); + protected abstract IEnumerable GetAnonymousObjectMemberNames(TAnonymousObjectInitializer initializer); + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) { - protected abstract bool HasName(TAnonymousObjectMemberDeclaratorSyntax declarator); - protected abstract TExpressionSyntax GetExpression(TAnonymousObjectMemberDeclaratorSyntax declarator); - protected abstract TAnonymousObjectMemberDeclaratorSyntax WithName(TAnonymousObjectMemberDeclaratorSyntax declarator, SyntaxToken name); - protected abstract IEnumerable GetAnonymousObjectMemberNames(TAnonymousObjectInitializer initializer); + var document = context.Document; + var cancellationToken = context.CancellationToken; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + var diagnostic = context.Diagnostics[0]; + var declarator = await GetMemberDeclaratorAsync(document, diagnostic, cancellationToken).ConfigureAwait(false); + if (declarator == null) { - var document = context.Document; - var cancellationToken = context.CancellationToken; + return; + } - var diagnostic = context.Diagnostics[0]; - var declarator = await GetMemberDeclaratorAsync(document, diagnostic, cancellationToken).ConfigureAwait(false); - if (declarator == null) - { - return; - } - - context.RegisterCodeFix( - CodeAction.Create( - CodeFixesResources.Add_member_name, - GetDocumentUpdater(context), - nameof(CodeFixesResources.Add_member_name)), - context.Diagnostics); + context.RegisterCodeFix( + CodeAction.Create( + CodeFixesResources.Add_member_name, + GetDocumentUpdater(context), + nameof(CodeFixesResources.Add_member_name)), + context.Diagnostics); + } + + private async Task GetMemberDeclaratorAsync( + Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var span = diagnostic.Location.SourceSpan; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var node = root.FindNode(span, getInnermostNodeForTie: true) as TExpressionSyntax; + if (node?.Span != span) + { + return null; } - private async Task GetMemberDeclaratorAsync( - Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + if (node.Parent is not TAnonymousObjectMemberDeclaratorSyntax declarator) { - var span = diagnostic.Location.SourceSpan; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var node = root.FindNode(span, getInnermostNodeForTie: true) as TExpressionSyntax; - if (node?.Span != span) - { - return null; - } + return null; + } - if (node.Parent is not TAnonymousObjectMemberDeclaratorSyntax declarator) - { - return null; - } + // Can't add a name of the declarator already has a name. + if (HasName(declarator)) + { + return null; + } - // Can't add a name of the declarator already has a name. - if (HasName(declarator)) - { - return null; - } + if (declarator.Parent is not TAnonymousObjectInitializer) + { + return null; + } - if (declarator.Parent is not TAnonymousObjectInitializer) - { - return null; - } + return declarator; + } - return declarator; - } + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + // If we're only introducing one name, then add the rename annotation to + // it so the user can pick a better name if they want. + var annotation = diagnostics.Length == 1 ? RenameAnnotation.Create() : null; - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + foreach (var diagnostic in diagnostics) { - // If we're only introducing one name, then add the rename annotation to - // it so the user can pick a better name if they want. - var annotation = diagnostics.Length == 1 ? RenameAnnotation.Create() : null; + await FixOneAsync( + document, semanticModel, diagnostic, + editor, annotation, cancellationToken).ConfigureAwait(false); + } + } - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - foreach (var diagnostic in diagnostics) - { - await FixOneAsync( - document, semanticModel, diagnostic, - editor, annotation, cancellationToken).ConfigureAwait(false); - } + private async Task FixOneAsync( + Document document, SemanticModel semanticModel, Diagnostic diagnostic, + SyntaxEditor editor, SyntaxAnnotation? annotation, CancellationToken cancellationToken) + { + var declarator = await GetMemberDeclaratorAsync(document, diagnostic, cancellationToken).ConfigureAwait(false); + if (declarator == null) + { + return; } - private async Task FixOneAsync( - Document document, SemanticModel semanticModel, Diagnostic diagnostic, - SyntaxEditor editor, SyntaxAnnotation? annotation, CancellationToken cancellationToken) + var semanticFacts = document.GetRequiredLanguageService(); + var name = semanticFacts.GenerateNameForExpression(semanticModel, GetExpression(declarator), capitalize: true, cancellationToken); + if (string.IsNullOrEmpty(name)) { - var declarator = await GetMemberDeclaratorAsync(document, diagnostic, cancellationToken).ConfigureAwait(false); - if (declarator == null) - { - return; - } + return; + } - var semanticFacts = document.GetRequiredLanguageService(); - var name = semanticFacts.GenerateNameForExpression(semanticModel, GetExpression(declarator), capitalize: true, cancellationToken); - if (string.IsNullOrEmpty(name)) + var generator = document.GetRequiredLanguageService(); + var syntaxFacts = document.GetRequiredLanguageService(); + editor.ReplaceNode( + declarator, + (current, _) => { - return; - } - - var generator = document.GetRequiredLanguageService(); - var syntaxFacts = document.GetRequiredLanguageService(); - editor.ReplaceNode( - declarator, - (current, _) => - { - var currentDeclarator = (TAnonymousObjectMemberDeclaratorSyntax)current; - var initializer = (TAnonymousObjectInitializer)currentDeclarator.GetRequiredParent(); - var existingNames = GetAnonymousObjectMemberNames(initializer); - var anonymousType = current.Parent; - var uniqueName = NameGenerator.EnsureUniqueness(name, existingNames, syntaxFacts.IsCaseSensitive); - - var nameToken = generator.Identifier(uniqueName); - if (annotation != null) - nameToken = nameToken.WithAdditionalAnnotations(annotation); - - return WithName(currentDeclarator, nameToken); - }); - } + var currentDeclarator = (TAnonymousObjectMemberDeclaratorSyntax)current; + var initializer = (TAnonymousObjectInitializer)currentDeclarator.GetRequiredParent(); + var existingNames = GetAnonymousObjectMemberNames(initializer); + var anonymousType = current.Parent; + var uniqueName = NameGenerator.EnsureUniqueness(name, existingNames, syntaxFacts.IsCaseSensitive); + + var nameToken = generator.Identifier(uniqueName); + if (annotation != null) + nameToken = nameToken.WithAdditionalAnnotations(annotation); + + return WithName(currentDeclarator, nameToken); + }); } } diff --git a/src/Analyzers/Core/CodeFixes/AddExplicitCast/AbstractAddExplicitCastCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/AddExplicitCast/AbstractAddExplicitCastCodeFixProvider.cs index 7269662eeb8cc..addd607fea7e6 100644 --- a/src/Analyzers/Core/CodeFixes/AddExplicitCast/AbstractAddExplicitCastCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/AddExplicitCast/AbstractAddExplicitCastCodeFixProvider.cs @@ -17,206 +17,205 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.AddExplicitCast +namespace Microsoft.CodeAnalysis.CodeFixes.AddExplicitCast; + +internal abstract partial class AbstractAddExplicitCastCodeFixProvider : SyntaxEditorBasedCodeFixProvider + where TExpressionSyntax : SyntaxNode { - internal abstract partial class AbstractAddExplicitCastCodeFixProvider : SyntaxEditorBasedCodeFixProvider - where TExpressionSyntax : SyntaxNode + /// + /// Give a set of least specific types with a limit, and the part exceeding the limit doesn't show any code fix, + /// but logs telemetry + /// + private const int MaximumConversionOptions = 3; + + protected abstract TExpressionSyntax Cast(TExpressionSyntax expression, ITypeSymbol type); + protected abstract void GetPartsOfCastOrConversionExpression(TExpressionSyntax expression, out SyntaxNode type, out SyntaxNode castedExpression); + + /// + /// Output the current type information of the target node and the conversion type(s) that the target node is + /// going to be cast by. Implicit downcast can appear on Variable Declaration, Return Statement, Function + /// Invocation, Attribute + /// + /// For example: + /// Base b; Derived d = [||]b; + /// "b" is the current node with type "Base", and the potential conversion types list which "b" can be cast by + /// is {Derived} + /// + /// The Id of diagnostic + /// the innermost node that contains the span + /// Output (target expression, potential conversion type) pairs + /// + /// True, if there is at least one potential conversion pair, and they are assigned to + /// "potentialConversionTypes" False, if there is no potential conversion pair. + /// + protected abstract bool TryGetTargetTypeInfo( + Document document, SemanticModel semanticModel, SyntaxNode root, + string diagnosticId, TExpressionSyntax spanNode, CancellationToken cancellationToken, + out ImmutableArray<(TExpressionSyntax node, ITypeSymbol type)> potentialConversionTypes); + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) { - /// - /// Give a set of least specific types with a limit, and the part exceeding the limit doesn't show any code fix, - /// but logs telemetry - /// - private const int MaximumConversionOptions = 3; - - protected abstract TExpressionSyntax Cast(TExpressionSyntax expression, ITypeSymbol type); - protected abstract void GetPartsOfCastOrConversionExpression(TExpressionSyntax expression, out SyntaxNode type, out SyntaxNode castedExpression); - - /// - /// Output the current type information of the target node and the conversion type(s) that the target node is - /// going to be cast by. Implicit downcast can appear on Variable Declaration, Return Statement, Function - /// Invocation, Attribute - /// - /// For example: - /// Base b; Derived d = [||]b; - /// "b" is the current node with type "Base", and the potential conversion types list which "b" can be cast by - /// is {Derived} - /// - /// The Id of diagnostic - /// the innermost node that contains the span - /// Output (target expression, potential conversion type) pairs - /// - /// True, if there is at least one potential conversion pair, and they are assigned to - /// "potentialConversionTypes" False, if there is no potential conversion pair. - /// - protected abstract bool TryGetTargetTypeInfo( - Document document, SemanticModel semanticModel, SyntaxNode root, - string diagnosticId, TExpressionSyntax spanNode, CancellationToken cancellationToken, - out ImmutableArray<(TExpressionSyntax node, ITypeSymbol type)> potentialConversionTypes); - - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var cancellationToken = context.CancellationToken; - var diagnostic = context.Diagnostics.First(); - - var root = await document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var document = context.Document; + var cancellationToken = context.CancellationToken; + var diagnostic = context.Diagnostics.First(); - var spanNode = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) - .GetAncestorsOrThis().FirstOrDefault(); - if (spanNode == null) - return; + var root = await document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var hasSolution = TryGetTargetTypeInfo(document, - semanticModel, root, diagnostic.Id, spanNode, cancellationToken, - out var potentialConversionTypes); - if (!hasSolution) - return; + var spanNode = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) + .GetAncestorsOrThis().FirstOrDefault(); + if (spanNode == null) + return; - if (potentialConversionTypes.Length == 1) - { - RegisterCodeFix(context, CodeFixesResources.Add_explicit_cast, nameof(CodeFixesResources.Add_explicit_cast)); - return; - } + var hasSolution = TryGetTargetTypeInfo(document, + semanticModel, root, diagnostic.Id, spanNode, cancellationToken, + out var potentialConversionTypes); + if (!hasSolution) + return; - using var actions = TemporaryArray.Empty; + if (potentialConversionTypes.Length == 1) + { + RegisterCodeFix(context, CodeFixesResources.Add_explicit_cast, nameof(CodeFixesResources.Add_explicit_cast)); + return; + } - // MaximumConversionOptions: we show at most [MaximumConversionOptions] options for this code fixer - for (var i = 0; i < Math.Min(MaximumConversionOptions, potentialConversionTypes.Length); i++) - { - var targetNode = potentialConversionTypes[i].node; - var conversionType = potentialConversionTypes[i].type; - var title = GetSubItemName(semanticModel, targetNode.SpanStart, conversionType); - - actions.Add(CodeAction.Create( - title, - cancellationToken => Task.FromResult(document.WithSyntaxRoot( - ApplyFix(document, semanticModel, root, targetNode, conversionType, cancellationToken))), - title)); - } + using var actions = TemporaryArray.Empty; - context.RegisterCodeFix( - CodeAction.Create(CodeFixesResources.Add_explicit_cast, actions.ToImmutableAndClear(), isInlinable: false), - context.Diagnostics); + // MaximumConversionOptions: we show at most [MaximumConversionOptions] options for this code fixer + for (var i = 0; i < Math.Min(MaximumConversionOptions, potentialConversionTypes.Length); i++) + { + var targetNode = potentialConversionTypes[i].node; + var conversionType = potentialConversionTypes[i].type; + var title = GetSubItemName(semanticModel, targetNode.SpanStart, conversionType); + + actions.Add(CodeAction.Create( + title, + cancellationToken => Task.FromResult(document.WithSyntaxRoot( + ApplyFix(document, semanticModel, root, targetNode, conversionType, cancellationToken))), + title)); } - private SyntaxNode ApplyFix( - Document document, - SemanticModel semanticModel, - SyntaxNode currentRoot, - TExpressionSyntax targetNode, - ITypeSymbol conversionType, - CancellationToken cancellationToken) + context.RegisterCodeFix( + CodeAction.Create(CodeFixesResources.Add_explicit_cast, actions.ToImmutableAndClear(), isInlinable: false), + context.Diagnostics); + } + + private SyntaxNode ApplyFix( + Document document, + SemanticModel semanticModel, + SyntaxNode currentRoot, + TExpressionSyntax targetNode, + ITypeSymbol conversionType, + CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var semanticFacts = document.GetRequiredLanguageService(); + + // if the node we're about to cast already has a cast, replace that cast if both are reference-identity downcasts. + if (syntaxFacts.IsCastExpression(targetNode) || syntaxFacts.IsConversionExpression(targetNode)) { - var syntaxFacts = document.GetRequiredLanguageService(); - var semanticFacts = document.GetRequiredLanguageService(); + GetPartsOfCastOrConversionExpression(targetNode, out var castTypeNode, out var castedExpression); - // if the node we're about to cast already has a cast, replace that cast if both are reference-identity downcasts. - if (syntaxFacts.IsCastExpression(targetNode) || syntaxFacts.IsConversionExpression(targetNode)) + var castType = semanticModel.GetTypeInfo(castTypeNode, cancellationToken).Type; + if (castType != null) { - GetPartsOfCastOrConversionExpression(targetNode, out var castTypeNode, out var castedExpression); + var firstConversion = semanticFacts.ClassifyConversion(semanticModel, castedExpression, castType); + var secondConversion = semanticModel.Compilation.ClassifyCommonConversion(castType, conversionType); - var castType = semanticModel.GetTypeInfo(castTypeNode, cancellationToken).Type; - if (castType != null) + if (firstConversion is { IsImplicit: false, IsReference: true } or { IsIdentity: true } && + secondConversion is { IsImplicit: false, IsReference: true }) { - var firstConversion = semanticFacts.ClassifyConversion(semanticModel, castedExpression, castType); - var secondConversion = semanticModel.Compilation.ClassifyCommonConversion(castType, conversionType); - - if (firstConversion is { IsImplicit: false, IsReference: true } or { IsIdentity: true } && - secondConversion is { IsImplicit: false, IsReference: true }) - { - return currentRoot.ReplaceNode( - targetNode, - this.Cast((TExpressionSyntax)castedExpression, conversionType) - .WithTriviaFrom(targetNode) - .WithAdditionalAnnotations(Simplifier.Annotation)); - } + return currentRoot.ReplaceNode( + targetNode, + this.Cast((TExpressionSyntax)castedExpression, conversionType) + .WithTriviaFrom(targetNode) + .WithAdditionalAnnotations(Simplifier.Annotation)); } } - - return currentRoot.ReplaceNode( - targetNode, - this.Cast(targetNode, conversionType).WithAdditionalAnnotations(Simplifier.Annotation)); } - private static string GetSubItemName(SemanticModel semanticModel, int position, ITypeSymbol conversionType) - { - return string.Format( - CodeFixesResources.Convert_type_to_0, - conversionType.ToMinimalDisplayString(semanticModel, position)); - } + return currentRoot.ReplaceNode( + targetNode, + this.Cast(targetNode, conversionType).WithAdditionalAnnotations(Simplifier.Annotation)); + } - protected static ImmutableArray<(TExpressionSyntax, ITypeSymbol)> FilterValidPotentialConversionTypes( - Document document, - SemanticModel semanticModel, - ArrayBuilder<(TExpressionSyntax node, ITypeSymbol type)> mutablePotentialConversionTypes) - { - var syntaxFacts = document.GetRequiredLanguageService(); - var semanticFacts = document.GetRequiredLanguageService(); + private static string GetSubItemName(SemanticModel semanticModel, int position, ITypeSymbol conversionType) + { + return string.Format( + CodeFixesResources.Convert_type_to_0, + conversionType.ToMinimalDisplayString(semanticModel, position)); + } - using var _ = ArrayBuilder<(TExpressionSyntax, ITypeSymbol)>.GetInstance(out var validPotentialConversionTypes); - foreach (var conversionTuple in mutablePotentialConversionTypes) - { - var targetNode = conversionTuple.node; - var targetNodeConversionType = conversionTuple.type; - - // For cases like object creation expression. for example: - // Derived d = [||]new Base(); - // It is always invalid except the target node has explicit conversion operator or is numeric. - if (syntaxFacts.IsObjectCreationExpression(targetNode) && - !semanticFacts.ClassifyConversion(semanticModel, targetNode, targetNodeConversionType).IsUserDefined) - { - continue; - } + protected static ImmutableArray<(TExpressionSyntax, ITypeSymbol)> FilterValidPotentialConversionTypes( + Document document, + SemanticModel semanticModel, + ArrayBuilder<(TExpressionSyntax node, ITypeSymbol type)> mutablePotentialConversionTypes) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var semanticFacts = document.GetRequiredLanguageService(); - validPotentialConversionTypes.Add(conversionTuple); + using var _ = ArrayBuilder<(TExpressionSyntax, ITypeSymbol)>.GetInstance(out var validPotentialConversionTypes); + foreach (var conversionTuple in mutablePotentialConversionTypes) + { + var targetNode = conversionTuple.node; + var targetNodeConversionType = conversionTuple.type; + + // For cases like object creation expression. for example: + // Derived d = [||]new Base(); + // It is always invalid except the target node has explicit conversion operator or is numeric. + if (syntaxFacts.IsObjectCreationExpression(targetNode) && + !semanticFacts.ClassifyConversion(semanticModel, targetNode, targetNodeConversionType).IsUserDefined) + { + continue; } - return validPotentialConversionTypes.Distinct().ToImmutableArray(); + validPotentialConversionTypes.Add(conversionTuple); } - protected static bool FindCorrespondingParameterByName( - string argumentName, ImmutableArray parameters, ref int parameterIndex) + return validPotentialConversionTypes.Distinct().ToImmutableArray(); + } + + protected static bool FindCorrespondingParameterByName( + string argumentName, ImmutableArray parameters, ref int parameterIndex) + { + for (var j = 0; j < parameters.Length; j++) { - for (var j = 0; j < parameters.Length; j++) + if (argumentName.Equals(parameters[j].Name)) { - if (argumentName.Equals(parameters[j].Name)) - { - parameterIndex = j; - return true; - } + parameterIndex = j; + return true; } - - return false; } - protected override async Task FixAllAsync( - Document document, - ImmutableArray diagnostics, - SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var spanNodes = diagnostics.SelectAsArray( - d => root.FindNode(d.Location.SourceSpan, getInnermostNodeForTie: true) - .GetAncestorsOrThis().First()); - - await editor.ApplyExpressionLevelSemanticEditsAsync( - document, spanNodes, - (semanticModel, spanNode) => true, - (semanticModel, root, spanNode) => + return false; + } + + protected override async Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var spanNodes = diagnostics.SelectAsArray( + d => root.FindNode(d.Location.SourceSpan, getInnermostNodeForTie: true) + .GetAncestorsOrThis().First()); + + await editor.ApplyExpressionLevelSemanticEditsAsync( + document, spanNodes, + (semanticModel, spanNode) => true, + (semanticModel, root, spanNode) => + { + // All diagnostics have the same error code + if (TryGetTargetTypeInfo(document, semanticModel, root, diagnostics[0].Id, spanNode, cancellationToken, out var potentialConversionTypes) && + potentialConversionTypes.Length == 1) { - // All diagnostics have the same error code - if (TryGetTargetTypeInfo(document, semanticModel, root, diagnostics[0].Id, spanNode, cancellationToken, out var potentialConversionTypes) && - potentialConversionTypes.Length == 1) - { - return ApplyFix(document, semanticModel, root, potentialConversionTypes[0].node, potentialConversionTypes[0].type, cancellationToken); - } - - return root; - }, - cancellationToken).ConfigureAwait(false); - } + return ApplyFix(document, semanticModel, root, potentialConversionTypes[0].node, potentialConversionTypes[0].type, cancellationToken); + } + + return root; + }, + cancellationToken).ConfigureAwait(false); } } diff --git a/src/Analyzers/Core/CodeFixes/AddExplicitCast/Fixer.cs b/src/Analyzers/Core/CodeFixes/AddExplicitCast/Fixer.cs index a0184e372ad16..a75028498ced1 100644 --- a/src/Analyzers/Core/CodeFixes/AddExplicitCast/Fixer.cs +++ b/src/Analyzers/Core/CodeFixes/AddExplicitCast/Fixer.cs @@ -11,211 +11,210 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CodeFixes.AddExplicitCast +namespace Microsoft.CodeAnalysis.CodeFixes.AddExplicitCast; + +internal abstract partial class AbstractAddExplicitCastCodeFixProvider { - internal abstract partial class AbstractAddExplicitCastCodeFixProvider + protected abstract class Fixer + where TArgumentSyntax : SyntaxNode + where TArgumentListSyntax : SyntaxNode + where TInvocationSyntax : SyntaxNode { - protected abstract class Fixer - where TArgumentSyntax : SyntaxNode - where TArgumentListSyntax : SyntaxNode - where TInvocationSyntax : SyntaxNode + protected abstract TExpressionSyntax GetExpressionOfArgument(TArgumentSyntax argument); + protected abstract TArgumentSyntax GenerateNewArgument(TArgumentSyntax oldArgument, ITypeSymbol conversionType); + protected abstract TArgumentListSyntax GenerateNewArgumentList(TArgumentListSyntax oldArgumentList, ArrayBuilder newArguments); + protected abstract SeparatedSyntaxList GetArgumentsOfArgumentList(TArgumentListSyntax argumentList); + protected abstract SymbolInfo GetSpeculativeSymbolInfo(SemanticModel semanticModel, TArgumentListSyntax newArgumentList); + + /// + /// Collect all the available cast pairs, format is (target argument expression, potential conversion type) + /// + /// The argument that need to be cast + /// The argument list that contains the target argument to be cast + /// The invocation node that is the parent of "argumentList" + /// + /// Return all the available cast pairs, format is (target argument expression, potential conversion type) + /// + public ImmutableArray<(TExpressionSyntax, ITypeSymbol)> GetPotentialConversionTypes( + Document document, + SemanticModel semanticModel, + SyntaxNode root, + TArgumentSyntax targetArgument, + TArgumentListSyntax argumentList, + TInvocationSyntax invocationNode, + CancellationToken cancellationToken) { - protected abstract TExpressionSyntax GetExpressionOfArgument(TArgumentSyntax argument); - protected abstract TArgumentSyntax GenerateNewArgument(TArgumentSyntax oldArgument, ITypeSymbol conversionType); - protected abstract TArgumentListSyntax GenerateNewArgumentList(TArgumentListSyntax oldArgumentList, ArrayBuilder newArguments); - protected abstract SeparatedSyntaxList GetArgumentsOfArgumentList(TArgumentListSyntax argumentList); - protected abstract SymbolInfo GetSpeculativeSymbolInfo(SemanticModel semanticModel, TArgumentListSyntax newArgumentList); - - /// - /// Collect all the available cast pairs, format is (target argument expression, potential conversion type) - /// - /// The argument that need to be cast - /// The argument list that contains the target argument to be cast - /// The invocation node that is the parent of "argumentList" - /// - /// Return all the available cast pairs, format is (target argument expression, potential conversion type) - /// - public ImmutableArray<(TExpressionSyntax, ITypeSymbol)> GetPotentialConversionTypes( - Document document, - SemanticModel semanticModel, - SyntaxNode root, - TArgumentSyntax targetArgument, - TArgumentListSyntax argumentList, - TInvocationSyntax invocationNode, - CancellationToken cancellationToken) + // Implicit downcast appears on the argument of invocation node, + // get all candidate functions and extract potential conversion types + var symbolInfo = semanticModel.GetSymbolInfo(invocationNode, cancellationToken); + using var _ = ArrayBuilder.GetInstance(out var candidateSymbols); + if (symbolInfo.Symbol != null) // BC42016: the only candidate symbol is symbolInfo.Symbol { - // Implicit downcast appears on the argument of invocation node, - // get all candidate functions and extract potential conversion types - var symbolInfo = semanticModel.GetSymbolInfo(invocationNode, cancellationToken); - using var _ = ArrayBuilder.GetInstance(out var candidateSymbols); - if (symbolInfo.Symbol != null) // BC42016: the only candidate symbol is symbolInfo.Symbol - { - candidateSymbols.Add(symbolInfo.Symbol); - } - else - { - candidateSymbols.AddRange(symbolInfo.CandidateSymbols); - } + candidateSymbols.Add(symbolInfo.Symbol); + } + else + { + candidateSymbols.AddRange(symbolInfo.CandidateSymbols); + } - using var __ = ArrayBuilder<(TExpressionSyntax, ITypeSymbol)>.GetInstance(out var mutablePotentialConversionTypes); - foreach (var candidateSymbol in candidateSymbols.OfType()) + using var __ = ArrayBuilder<(TExpressionSyntax, ITypeSymbol)>.GetInstance(out var mutablePotentialConversionTypes); + foreach (var candidateSymbol in candidateSymbols.OfType()) + { + if (CanArgumentTypesBeConvertedToParameterTypes( + document, semanticModel, root, argumentList, candidateSymbol.Parameters, + targetArgument, cancellationToken, out var targetArgumentConversionType) + && GetExpressionOfArgument(targetArgument) is TExpressionSyntax argumentExpression) { - if (CanArgumentTypesBeConvertedToParameterTypes( - document, semanticModel, root, argumentList, candidateSymbol.Parameters, - targetArgument, cancellationToken, out var targetArgumentConversionType) - && GetExpressionOfArgument(targetArgument) is TExpressionSyntax argumentExpression) - { - mutablePotentialConversionTypes.Add((argumentExpression, targetArgumentConversionType)); - } + mutablePotentialConversionTypes.Add((argumentExpression, targetArgumentConversionType)); } + } - // Sort the potential conversion types by inheritance distance, so that - // operations are in order and user can choose least specific types(more accurate) - mutablePotentialConversionTypes.Sort(new InheritanceDistanceComparer(semanticModel)); + // Sort the potential conversion types by inheritance distance, so that + // operations are in order and user can choose least specific types(more accurate) + mutablePotentialConversionTypes.Sort(new InheritanceDistanceComparer(semanticModel)); - return mutablePotentialConversionTypes.ToImmutable(); - } + return mutablePotentialConversionTypes.ToImmutable(); + } - /// - /// Test if all argument types can be converted to corresponding parameter types. - /// - /// For example: - /// class Base { } - /// class Derived1 : Base { } - /// class Derived2 : Base { } - /// class Derived3 : Base { } - /// void DoSomething(int i, Derived1 d) { } - /// void DoSomething(string s, Derived2 d) { } - /// void DoSomething(int i, Derived3 d) { } - /// - /// Base b; - /// DoSomething(1, [||]b); - /// - /// *void DoSomething(string s, Derived2 d) { }* is not the perfect match candidate function for - /// *DoSomething(1, [||]b)* because int and string are not ancestor-descendant relationship. Thus, - /// Derived2 is not a potential conversion type. - /// - /// The argument list of invocation expression - /// The parameters of function - /// The argument need to be cast. - /// Output the corresponding parameter type of - /// "targetArgument" if function returns true - /// - /// True, if arguments and parameters match perfectly. - /// "targetArgumentConversionType" outputs the corresponding parameter type of "targetArgument" - /// False, otherwise. - /// - public bool CanArgumentTypesBeConvertedToParameterTypes( - Document document, - SemanticModel semanticModel, - SyntaxNode root, - TArgumentListSyntax argumentList, - ImmutableArray parameters, - TArgumentSyntax targetArgument, - CancellationToken cancellationToken, - [NotNullWhen(true)] out ITypeSymbol? targetArgumentConversionType) - { - targetArgumentConversionType = null; + /// + /// Test if all argument types can be converted to corresponding parameter types. + /// + /// For example: + /// class Base { } + /// class Derived1 : Base { } + /// class Derived2 : Base { } + /// class Derived3 : Base { } + /// void DoSomething(int i, Derived1 d) { } + /// void DoSomething(string s, Derived2 d) { } + /// void DoSomething(int i, Derived3 d) { } + /// + /// Base b; + /// DoSomething(1, [||]b); + /// + /// *void DoSomething(string s, Derived2 d) { }* is not the perfect match candidate function for + /// *DoSomething(1, [||]b)* because int and string are not ancestor-descendant relationship. Thus, + /// Derived2 is not a potential conversion type. + /// + /// The argument list of invocation expression + /// The parameters of function + /// The argument need to be cast. + /// Output the corresponding parameter type of + /// "targetArgument" if function returns true + /// + /// True, if arguments and parameters match perfectly. + /// "targetArgumentConversionType" outputs the corresponding parameter type of "targetArgument" + /// False, otherwise. + /// + public bool CanArgumentTypesBeConvertedToParameterTypes( + Document document, + SemanticModel semanticModel, + SyntaxNode root, + TArgumentListSyntax argumentList, + ImmutableArray parameters, + TArgumentSyntax targetArgument, + CancellationToken cancellationToken, + [NotNullWhen(true)] out ITypeSymbol? targetArgumentConversionType) + { + targetArgumentConversionType = null; - // No conversion happens under this case - if (parameters.Length == 0) - return false; + // No conversion happens under this case + if (parameters.Length == 0) + return false; - var syntaxFacts = document.GetRequiredLanguageService(); - var semanticFacts = document.GetRequiredLanguageService(); + var syntaxFacts = document.GetRequiredLanguageService(); + var semanticFacts = document.GetRequiredLanguageService(); - var arguments = GetArgumentsOfArgumentList(argumentList); - using var _ = ArrayBuilder.GetInstance(out var newArguments); + var arguments = GetArgumentsOfArgumentList(argumentList); + using var _ = ArrayBuilder.GetInstance(out var newArguments); - for (var i = 0; i < arguments.Count; i++) + for (var i = 0; i < arguments.Count; i++) + { + // Parameter index cannot out of its range, #arguments is larger than #parameter only if + // the last parameter with keyword params + var parameterIndex = Math.Min(i, parameters.Length - 1); + + // If the argument has a name, get the corresponding parameter index + if (syntaxFacts.GetNameForArgument(arguments[i]) is string name + && name != string.Empty + && !FindCorrespondingParameterByName(name, parameters, ref parameterIndex)) { - // Parameter index cannot out of its range, #arguments is larger than #parameter only if - // the last parameter with keyword params - var parameterIndex = Math.Min(i, parameters.Length - 1); - - // If the argument has a name, get the corresponding parameter index - if (syntaxFacts.GetNameForArgument(arguments[i]) is string name - && name != string.Empty - && !FindCorrespondingParameterByName(name, parameters, ref parameterIndex)) - { - return false; - } - - // The argument is either in order with parameters, or have a matched name with parameters. - var argumentExpression = GetExpressionOfArgument(arguments[i]); - if (argumentExpression == null) - { - // argumentExpression is null when it is an omitted argument in VB .NET - newArguments.Add(arguments[i]); - continue; - } - - var parameterType = parameters[parameterIndex].Type; - if (parameters[parameterIndex].IsParams - && parameterType is IArrayTypeSymbol paramsType - && semanticFacts.ClassifyConversion(semanticModel, argumentExpression, paramsType.ElementType).Exists) - { - newArguments.Add(GenerateNewArgument(arguments[i], paramsType.ElementType)); - if (arguments[i].Equals(targetArgument)) - targetArgumentConversionType = paramsType.ElementType; - } - else if (semanticFacts.ClassifyConversion(semanticModel, argumentExpression, parameterType).Exists) - { - newArguments.Add(GenerateNewArgument(arguments[i], parameterType)); - if (arguments[i].Equals(targetArgument)) - targetArgumentConversionType = parameterType; - } - else if (syntaxFacts.IsDeclarationExpression(argumentExpression) - && semanticModel.GetTypeInfo(argumentExpression, cancellationToken).Type is ITypeSymbol argumentType - && semanticModel.Compilation.ClassifyCommonConversion(argumentType, parameterType).IsIdentity) - { - // Direct conversion from a declaration expression to a type is unspecified, thus we classify the - // conversion from the type of declaration expression to the parameter type - // An example for this case: - // void Goo(out int i) { i = 1; } - // Goo([|out var i|]); - // "var i" is a declaration expression - // - // In addition, since this case is with keyword "out", the type of declaration expression and the - // parameter type must be identical in order to match. - newArguments.Add(arguments[i]); - } - else - { - return false; - } + return false; } - return targetArgumentConversionType != null - && IsInvocationExpressionWithNewArgumentsApplicable( - semanticModel, root, argumentList, newArguments, targetArgument); - } + // The argument is either in order with parameters, or have a matched name with parameters. + var argumentExpression = GetExpressionOfArgument(arguments[i]); + if (argumentExpression == null) + { + // argumentExpression is null when it is an omitted argument in VB .NET + newArguments.Add(arguments[i]); + continue; + } - /// - /// Check whether the invocation expression with new arguments is applicable. - /// - /// old argumentList node - /// new arguments that are cast by corresponding parameter types - /// The node needs to be cast. - /// - /// Return true if the invocation expression with new arguments is applicable. - /// Otherwise, return false - /// - private bool IsInvocationExpressionWithNewArgumentsApplicable(SemanticModel semanticModel, - SyntaxNode root, - TArgumentListSyntax oldArgumentList, - ArrayBuilder newArguments, - SyntaxNode targetNode) - { - var newRoot = root.ReplaceNode(oldArgumentList, GenerateNewArgumentList(oldArgumentList, newArguments)); - if (newRoot.FindNode(targetNode.Span).GetAncestorOrThis() is TArgumentListSyntax newArgumentList) + var parameterType = parameters[parameterIndex].Type; + if (parameters[parameterIndex].IsParams + && parameterType is IArrayTypeSymbol paramsType + && semanticFacts.ClassifyConversion(semanticModel, argumentExpression, paramsType.ElementType).Exists) + { + newArguments.Add(GenerateNewArgument(arguments[i], paramsType.ElementType)); + if (arguments[i].Equals(targetArgument)) + targetArgumentConversionType = paramsType.ElementType; + } + else if (semanticFacts.ClassifyConversion(semanticModel, argumentExpression, parameterType).Exists) + { + newArguments.Add(GenerateNewArgument(arguments[i], parameterType)); + if (arguments[i].Equals(targetArgument)) + targetArgumentConversionType = parameterType; + } + else if (syntaxFacts.IsDeclarationExpression(argumentExpression) + && semanticModel.GetTypeInfo(argumentExpression, cancellationToken).Type is ITypeSymbol argumentType + && semanticModel.Compilation.ClassifyCommonConversion(argumentType, parameterType).IsIdentity) { - var symbolInfo = GetSpeculativeSymbolInfo(semanticModel, newArgumentList); - return symbolInfo.Symbol != null; + // Direct conversion from a declaration expression to a type is unspecified, thus we classify the + // conversion from the type of declaration expression to the parameter type + // An example for this case: + // void Goo(out int i) { i = 1; } + // Goo([|out var i|]); + // "var i" is a declaration expression + // + // In addition, since this case is with keyword "out", the type of declaration expression and the + // parameter type must be identical in order to match. + newArguments.Add(arguments[i]); } + else + { + return false; + } + } - return false; + return targetArgumentConversionType != null + && IsInvocationExpressionWithNewArgumentsApplicable( + semanticModel, root, argumentList, newArguments, targetArgument); + } + + /// + /// Check whether the invocation expression with new arguments is applicable. + /// + /// old argumentList node + /// new arguments that are cast by corresponding parameter types + /// The node needs to be cast. + /// + /// Return true if the invocation expression with new arguments is applicable. + /// Otherwise, return false + /// + private bool IsInvocationExpressionWithNewArgumentsApplicable(SemanticModel semanticModel, + SyntaxNode root, + TArgumentListSyntax oldArgumentList, + ArrayBuilder newArguments, + SyntaxNode targetNode) + { + var newRoot = root.ReplaceNode(oldArgumentList, GenerateNewArgumentList(oldArgumentList, newArguments)); + if (newRoot.FindNode(targetNode.Span).GetAncestorOrThis() is TArgumentListSyntax newArgumentList) + { + var symbolInfo = GetSpeculativeSymbolInfo(semanticModel, newArgumentList); + return symbolInfo.Symbol != null; } + + return false; } } } diff --git a/src/Analyzers/Core/CodeFixes/AddExplicitCast/InheritanceDistanceComparer.cs b/src/Analyzers/Core/CodeFixes/AddExplicitCast/InheritanceDistanceComparer.cs index c695918c5dc59..dceec07c3217e 100644 --- a/src/Analyzers/Core/CodeFixes/AddExplicitCast/InheritanceDistanceComparer.cs +++ b/src/Analyzers/Core/CodeFixes/AddExplicitCast/InheritanceDistanceComparer.cs @@ -5,92 +5,91 @@ using System; using System.Collections.Generic; -namespace Microsoft.CodeAnalysis.CodeFixes.AddExplicitCast +namespace Microsoft.CodeAnalysis.CodeFixes.AddExplicitCast; + +/// +/// The item is the pair of target argument expression and its conversion type +/// +/// Sort pairs using conversion types by inheritance distance from the base type in ascending order, +/// i.e., less specific type has higher priority because it has less probability to make mistakes +/// +/// For example: +/// class Base { } +/// class Derived1 : Base { } +/// class Derived2 : Derived1 { } +/// +/// void Foo(Derived1 d1) { } +/// void Foo(Derived2 d2) { } +/// +/// Base b = new Derived1(); +/// Foo([||]b); +/// +/// operations: +/// 1. Convert type to 'Derived1' +/// 2. Convert type to 'Derived2' +/// +/// 'Derived1' is less specific than 'Derived2' compared to 'Base' +/// +internal sealed class InheritanceDistanceComparer(SemanticModel semanticModel) +: IComparer<(TExpressionSyntax syntax, ITypeSymbol symbol)> +where TExpressionSyntax : SyntaxNode { - /// - /// The item is the pair of target argument expression and its conversion type - /// - /// Sort pairs using conversion types by inheritance distance from the base type in ascending order, - /// i.e., less specific type has higher priority because it has less probability to make mistakes - /// - /// For example: - /// class Base { } - /// class Derived1 : Base { } - /// class Derived2 : Derived1 { } - /// - /// void Foo(Derived1 d1) { } - /// void Foo(Derived2 d2) { } - /// - /// Base b = new Derived1(); - /// Foo([||]b); - /// - /// operations: - /// 1. Convert type to 'Derived1' - /// 2. Convert type to 'Derived2' - /// - /// 'Derived1' is less specific than 'Derived2' compared to 'Base' - /// - internal sealed class InheritanceDistanceComparer(SemanticModel semanticModel) - : IComparer<(TExpressionSyntax syntax, ITypeSymbol symbol)> - where TExpressionSyntax : SyntaxNode - { - private readonly SemanticModel _semanticModel = semanticModel; + private readonly SemanticModel _semanticModel = semanticModel; - public int Compare((TExpressionSyntax syntax, ITypeSymbol symbol) x, - (TExpressionSyntax syntax, ITypeSymbol symbol) y) + public int Compare((TExpressionSyntax syntax, ITypeSymbol symbol) x, + (TExpressionSyntax syntax, ITypeSymbol symbol) y) + { + // if the argument is different, keep the original order + if (!x.syntax.Equals(y.syntax)) { - // if the argument is different, keep the original order - if (!x.syntax.Equals(y.syntax)) - { - return 0; - } - else - { - var baseType = _semanticModel.GetTypeInfo(x.syntax).Type; - var xDist = GetInheritanceDistance(baseType, x.symbol); - var yDist = GetInheritanceDistance(baseType, y.symbol); - return xDist.CompareTo(yDist); - } + return 0; } - - /// - /// Calculate the inheritance distance between baseType and derivedType. - /// - private static int GetInheritanceDistanceRecursive(ITypeSymbol baseType, ITypeSymbol? derivedType) + else { - if (derivedType == null) - return int.MaxValue; - if (derivedType.Equals(baseType)) - return 0; + var baseType = _semanticModel.GetTypeInfo(x.syntax).Type; + var xDist = GetInheritanceDistance(baseType, x.symbol); + var yDist = GetInheritanceDistance(baseType, y.symbol); + return xDist.CompareTo(yDist); + } + } - var distance = GetInheritanceDistanceRecursive(baseType, derivedType.BaseType); + /// + /// Calculate the inheritance distance between baseType and derivedType. + /// + private static int GetInheritanceDistanceRecursive(ITypeSymbol baseType, ITypeSymbol? derivedType) + { + if (derivedType == null) + return int.MaxValue; + if (derivedType.Equals(baseType)) + return 0; + + var distance = GetInheritanceDistanceRecursive(baseType, derivedType.BaseType); - if (derivedType.Interfaces.Length != 0) + if (derivedType.Interfaces.Length != 0) + { + foreach (var interfaceType in derivedType.Interfaces) { - foreach (var interfaceType in derivedType.Interfaces) - { - distance = Math.Min(GetInheritanceDistanceRecursive(baseType, interfaceType), distance); - } + distance = Math.Min(GetInheritanceDistanceRecursive(baseType, interfaceType), distance); } - - return distance == int.MaxValue ? distance : distance + 1; } - /// - /// Wrapper function of [GetInheritanceDistance], also consider the class with explicit conversion operator - /// has the highest priority. - /// - private int GetInheritanceDistance(ITypeSymbol? baseType, ITypeSymbol castType) - { - if (baseType is null) - return 0; + return distance == int.MaxValue ? distance : distance + 1; + } + + /// + /// Wrapper function of [GetInheritanceDistance], also consider the class with explicit conversion operator + /// has the highest priority. + /// + private int GetInheritanceDistance(ITypeSymbol? baseType, ITypeSymbol castType) + { + if (baseType is null) + return 0; - var conversion = _semanticModel.Compilation.ClassifyCommonConversion(baseType, castType); + var conversion = _semanticModel.Compilation.ClassifyCommonConversion(baseType, castType); - // If the node has the explicit conversion operator, then it has the shortest distance - // since explicit conversion operator is defined by users and has the highest priority - var distance = conversion.IsUserDefined ? 0 : GetInheritanceDistanceRecursive(baseType, castType); - return distance; - } + // If the node has the explicit conversion operator, then it has the shortest distance + // since explicit conversion operator is defined by users and has the highest priority + var distance = conversion.IsUserDefined ? 0 : GetInheritanceDistanceRecursive(baseType, castType); + return distance; } } diff --git a/src/Analyzers/Core/CodeFixes/AddObsoleteAttribute/AbstractAddObsoleteAttributeCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/AddObsoleteAttribute/AbstractAddObsoleteAttributeCodeFixProvider.cs index 354be560e66f0..d1246e968e40e 100644 --- a/src/Analyzers/Core/CodeFixes/AddObsoleteAttribute/AbstractAddObsoleteAttributeCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/AddObsoleteAttribute/AbstractAddObsoleteAttributeCodeFixProvider.cs @@ -14,81 +14,80 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddObsoleteAttribute +namespace Microsoft.CodeAnalysis.AddObsoleteAttribute; + +internal abstract class AbstractAddObsoleteAttributeCodeFixProvider + : SyntaxEditorBasedCodeFixProvider { - internal abstract class AbstractAddObsoleteAttributeCodeFixProvider - : SyntaxEditorBasedCodeFixProvider + private readonly ISyntaxFacts _syntaxFacts; + private readonly string _title; + + protected AbstractAddObsoleteAttributeCodeFixProvider( + ISyntaxFacts syntaxFacts, + string title) { - private readonly ISyntaxFacts _syntaxFacts; - private readonly string _title; + _syntaxFacts = syntaxFacts; + _title = title; + } - protected AbstractAddObsoleteAttributeCodeFixProvider( - ISyntaxFacts syntaxFacts, - string title) - { - _syntaxFacts = syntaxFacts; - _title = title; - } + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var cancellationToken = context.CancellationToken; + var document = context.Document; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + var attribute = await GetObsoleteAttributeAsync(document, cancellationToken).ConfigureAwait(false); + if (attribute == null) { - var cancellationToken = context.CancellationToken; - var document = context.Document; - - var attribute = await GetObsoleteAttributeAsync(document, cancellationToken).ConfigureAwait(false); - if (attribute == null) - { - return; - } - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - var node = context.Diagnostics[0].Location.FindNode(cancellationToken); + return; + } - var container = GetContainer(root, node); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (container == null) - { - return; - } + var node = context.Diagnostics[0].Location.FindNode(cancellationToken); - RegisterCodeFix(context, _title, _title); - } + var container = GetContainer(root, node); - private static async Task GetObsoleteAttributeAsync(Document document, CancellationToken cancellationToken) + if (container == null) { - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var attribute = compilation.GetTypeByMetadataName(typeof(ObsoleteAttribute).FullName!); - return attribute; + return; } - private SyntaxNode? GetContainer(SyntaxNode root, SyntaxNode node) - { - return _syntaxFacts.GetContainingMemberDeclaration(root, node.SpanStart) ?? - _syntaxFacts.GetContainingTypeDeclaration(root, node.SpanStart); - } + RegisterCodeFix(context, _title, _title); + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var obsoleteAttribute = await GetObsoleteAttributeAsync(document, cancellationToken).ConfigureAwait(false); + private static async Task GetObsoleteAttributeAsync(Document document, CancellationToken cancellationToken) + { + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var attribute = compilation.GetTypeByMetadataName(typeof(ObsoleteAttribute).FullName!); + return attribute; + } - // RegisterCodeFixesAsync checked for null - Contract.ThrowIfNull(obsoleteAttribute); + private SyntaxNode? GetContainer(SyntaxNode root, SyntaxNode node) + { + return _syntaxFacts.GetContainingMemberDeclaration(root, node.SpanStart) ?? + _syntaxFacts.GetContainingTypeDeclaration(root, node.SpanStart); + } + + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var obsoleteAttribute = await GetObsoleteAttributeAsync(document, cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + // RegisterCodeFixesAsync checked for null + Contract.ThrowIfNull(obsoleteAttribute); - var containers = diagnostics.Select(d => GetContainer(root, d.Location.FindNode(cancellationToken))) - .WhereNotNull() - .ToSet(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var generator = editor.Generator; - foreach (var container in containers) - { - editor.AddAttribute(container, - generator.Attribute(editor.Generator.TypeExpression(obsoleteAttribute))); - } + var containers = diagnostics.Select(d => GetContainer(root, d.Location.FindNode(cancellationToken))) + .WhereNotNull() + .ToSet(); + + var generator = editor.Generator; + foreach (var container in containers) + { + editor.AddAttribute(container, + generator.Attribute(editor.Generator.TypeExpression(obsoleteAttribute))); } } } diff --git a/src/Analyzers/Core/CodeFixes/AddParameter/AbstractAddParameterCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/AddParameter/AbstractAddParameterCodeFixProvider.cs index 4738a6b3314b2..117b30f2ccd8d 100644 --- a/src/Analyzers/Core/CodeFixes/AddParameter/AbstractAddParameterCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/AddParameter/AbstractAddParameterCodeFixProvider.cs @@ -16,553 +16,552 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddParameter +namespace Microsoft.CodeAnalysis.AddParameter; + +internal abstract class AbstractAddParameterCodeFixProvider< + TArgumentSyntax, + TAttributeArgumentSyntax, + TArgumentListSyntax, + TAttributeArgumentListSyntax, + TInvocationExpressionSyntax, + TObjectCreationExpressionSyntax> : CodeFixProvider + where TArgumentSyntax : SyntaxNode + where TArgumentListSyntax : SyntaxNode + where TAttributeArgumentListSyntax : SyntaxNode + where TInvocationExpressionSyntax : SyntaxNode + where TObjectCreationExpressionSyntax : SyntaxNode { - internal abstract class AbstractAddParameterCodeFixProvider< - TArgumentSyntax, - TAttributeArgumentSyntax, - TArgumentListSyntax, - TAttributeArgumentListSyntax, - TInvocationExpressionSyntax, - TObjectCreationExpressionSyntax> : CodeFixProvider - where TArgumentSyntax : SyntaxNode - where TArgumentListSyntax : SyntaxNode - where TAttributeArgumentListSyntax : SyntaxNode - where TInvocationExpressionSyntax : SyntaxNode - where TObjectCreationExpressionSyntax : SyntaxNode - { - protected abstract ImmutableArray TooManyArgumentsDiagnosticIds { get; } - protected abstract ImmutableArray CannotConvertDiagnosticIds { get; } + protected abstract ImmutableArray TooManyArgumentsDiagnosticIds { get; } + protected abstract ImmutableArray CannotConvertDiagnosticIds { get; } - protected abstract ITypeSymbol GetArgumentType(SyntaxNode argumentNode, SemanticModel semanticModel, CancellationToken cancellationToken); + protected abstract ITypeSymbol GetArgumentType(SyntaxNode argumentNode, SemanticModel semanticModel, CancellationToken cancellationToken); - public override FixAllProvider? GetFixAllProvider() - { - // Fix All is not supported for this code fix. - return null; - } + public override FixAllProvider? GetFixAllProvider() + { + // Fix All is not supported for this code fix. + return null; + } - protected virtual RegisterFixData? TryGetLanguageSpecificFixInfo( - SemanticModel semanticModel, - SyntaxNode node, - CancellationToken cancellationToken) - => null; + protected virtual RegisterFixData? TryGetLanguageSpecificFixInfo( + SemanticModel semanticModel, + SyntaxNode node, + CancellationToken cancellationToken) + => null; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var cancellationToken = context.CancellationToken; - var diagnostic = context.Diagnostics.First(); + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var cancellationToken = context.CancellationToken; + var diagnostic = context.Diagnostics.First(); - var document = context.Document; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var document = context.Document; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var initialNode = root.FindNode(diagnostic.Location.SourceSpan); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); + var initialNode = root.FindNode(diagnostic.Location.SourceSpan); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); - for (var node = initialNode; node != null; node = node.Parent) - { - var fixData = - TryGetInvocationExpressionFixInfo(semanticModel, syntaxFacts, node, cancellationToken) ?? - TryGetObjectCreationFixInfo(semanticModel, syntaxFacts, node, cancellationToken) ?? - TryGetLanguageSpecificFixInfo(semanticModel, node, cancellationToken); + for (var node = initialNode; node != null; node = node.Parent) + { + var fixData = + TryGetInvocationExpressionFixInfo(semanticModel, syntaxFacts, node, cancellationToken) ?? + TryGetObjectCreationFixInfo(semanticModel, syntaxFacts, node, cancellationToken) ?? + TryGetLanguageSpecificFixInfo(semanticModel, node, cancellationToken); - if (fixData != null) + if (fixData != null) + { + var candidates = fixData.MethodCandidates; + if (fixData.IsConstructorInitializer) { - var candidates = fixData.MethodCandidates; - if (fixData.IsConstructorInitializer) + // The invocation is a :this() or :base() call. In the 'this' case we need to exclude the + // method with the diagnostic because otherwise we might introduce a call to itself (which is forbidden). + if (semanticModel.GetEnclosingSymbol(node.SpanStart, cancellationToken) is IMethodSymbol methodWithDiagnostic) { - // The invocation is a :this() or :base() call. In the 'this' case we need to exclude the - // method with the diagnostic because otherwise we might introduce a call to itself (which is forbidden). - if (semanticModel.GetEnclosingSymbol(node.SpanStart, cancellationToken) is IMethodSymbol methodWithDiagnostic) - { - candidates = candidates.Remove(methodWithDiagnostic); - } + candidates = candidates.Remove(methodWithDiagnostic); } - - var argumentOpt = TryGetRelevantArgument(initialNode, node, diagnostic); - var argumentInsertPositionInMethodCandidates = GetArgumentInsertPositionForMethodCandidates( - argumentOpt, semanticModel, syntaxFacts, fixData.Arguments, candidates); - RegisterFixForMethodOverloads(context, fixData.Arguments, argumentInsertPositionInMethodCandidates); - return; } + + var argumentOpt = TryGetRelevantArgument(initialNode, node, diagnostic); + var argumentInsertPositionInMethodCandidates = GetArgumentInsertPositionForMethodCandidates( + argumentOpt, semanticModel, syntaxFacts, fixData.Arguments, candidates); + RegisterFixForMethodOverloads(context, fixData.Arguments, argumentInsertPositionInMethodCandidates); + return; } } + } - /// - /// If the diagnostic is on a argument, the argument is considered to be the argument to fix. - /// There are some exceptions to this rule. Returning null indicates that the fixer needs - /// to find the relevant argument by itself. - /// - private TArgumentSyntax? TryGetRelevantArgument( - SyntaxNode initialNode, SyntaxNode node, Diagnostic diagnostic) + /// + /// If the diagnostic is on a argument, the argument is considered to be the argument to fix. + /// There are some exceptions to this rule. Returning null indicates that the fixer needs + /// to find the relevant argument by itself. + /// + private TArgumentSyntax? TryGetRelevantArgument( + SyntaxNode initialNode, SyntaxNode node, Diagnostic diagnostic) + { + if (TooManyArgumentsDiagnosticIds.Contains(diagnostic.Id)) { - if (TooManyArgumentsDiagnosticIds.Contains(diagnostic.Id)) - { - return null; - } - - if (CannotConvertDiagnosticIds.Contains(diagnostic.Id)) - { - return null; - } - - return initialNode.GetAncestorsOrThis() - .LastOrDefault(a => a.AncestorsAndSelf().Contains(node)); + return null; } - private static RegisterFixData? TryGetInvocationExpressionFixInfo( - SemanticModel semanticModel, - ISyntaxFactsService syntaxFacts, - SyntaxNode node, - CancellationToken cancellationToken) + if (CannotConvertDiagnosticIds.Contains(diagnostic.Id)) { - if (node is TInvocationExpressionSyntax invocationExpression) - { - var expression = syntaxFacts.GetExpressionOfInvocationExpression(invocationExpression); - var candidates = semanticModel.GetMemberGroup(expression, cancellationToken).OfType().ToImmutableArray(); - var arguments = (SeparatedSyntaxList)syntaxFacts.GetArgumentsOfInvocationExpression(invocationExpression); - - // In VB a constructor calls other constructor overloads via a Me.New(..) invocation. - // If the candidates are MethodKind.Constructor than these are the equivalent the a C# ConstructorInitializer. - var isConstructorInitializer = candidates.All(m => m.MethodKind == MethodKind.Constructor); - return new RegisterFixData(arguments, candidates, isConstructorInitializer); - } - return null; } - private static RegisterFixData? TryGetObjectCreationFixInfo( - SemanticModel semanticModel, - ISyntaxFactsService syntaxFacts, - SyntaxNode node, - CancellationToken cancellationToken) + return initialNode.GetAncestorsOrThis() + .LastOrDefault(a => a.AncestorsAndSelf().Contains(node)); + } + + private static RegisterFixData? TryGetInvocationExpressionFixInfo( + SemanticModel semanticModel, + ISyntaxFactsService syntaxFacts, + SyntaxNode node, + CancellationToken cancellationToken) + { + if (node is TInvocationExpressionSyntax invocationExpression) { - if (node is TObjectCreationExpressionSyntax objectCreation) - { + var expression = syntaxFacts.GetExpressionOfInvocationExpression(invocationExpression); + var candidates = semanticModel.GetMemberGroup(expression, cancellationToken).OfType().ToImmutableArray(); + var arguments = (SeparatedSyntaxList)syntaxFacts.GetArgumentsOfInvocationExpression(invocationExpression); + + // In VB a constructor calls other constructor overloads via a Me.New(..) invocation. + // If the candidates are MethodKind.Constructor than these are the equivalent the a C# ConstructorInitializer. + var isConstructorInitializer = candidates.All(m => m.MethodKind == MethodKind.Constructor); + return new RegisterFixData(arguments, candidates, isConstructorInitializer); + } - // Not supported if this is "new { ... }" (as there are no parameters at all. - var typeNode = syntaxFacts.IsImplicitObjectCreationExpression(node) - ? node - : syntaxFacts.GetTypeOfObjectCreationExpression(objectCreation); - if (typeNode == null) - { - return new RegisterFixData(); - } + return null; + } - var symbol = semanticModel.GetSymbolInfo(typeNode, cancellationToken).GetAnySymbol(); - var type = symbol switch - { - IMethodSymbol methodSymbol => methodSymbol.ContainingType, // Implicit object creation expressions - INamedTypeSymbol namedTypeSymbol => namedTypeSymbol, // Standard object creation expressions - _ => null, - }; - - // If we can't figure out the type being created, or the type isn't in source, - // then there's nothing we can do. - if (type == null) - { - return new RegisterFixData(); - } + private static RegisterFixData? TryGetObjectCreationFixInfo( + SemanticModel semanticModel, + ISyntaxFactsService syntaxFacts, + SyntaxNode node, + CancellationToken cancellationToken) + { + if (node is TObjectCreationExpressionSyntax objectCreation) + { - if (!type.IsNonImplicitAndFromSource()) - { - return new RegisterFixData(); - } + // Not supported if this is "new { ... }" (as there are no parameters at all. + var typeNode = syntaxFacts.IsImplicitObjectCreationExpression(node) + ? node + : syntaxFacts.GetTypeOfObjectCreationExpression(objectCreation); + if (typeNode == null) + { + return new RegisterFixData(); + } - var arguments = (SeparatedSyntaxList)syntaxFacts.GetArgumentsOfObjectCreationExpression(objectCreation); - var methodCandidates = type.InstanceConstructors; + var symbol = semanticModel.GetSymbolInfo(typeNode, cancellationToken).GetAnySymbol(); + var type = symbol switch + { + IMethodSymbol methodSymbol => methodSymbol.ContainingType, // Implicit object creation expressions + INamedTypeSymbol namedTypeSymbol => namedTypeSymbol, // Standard object creation expressions + _ => null, + }; + + // If we can't figure out the type being created, or the type isn't in source, + // then there's nothing we can do. + if (type == null) + { + return new RegisterFixData(); + } - return new RegisterFixData(arguments, methodCandidates, isConstructorInitializer: false); + if (!type.IsNonImplicitAndFromSource()) + { + return new RegisterFixData(); } - return null; + var arguments = (SeparatedSyntaxList)syntaxFacts.GetArgumentsOfObjectCreationExpression(objectCreation); + var methodCandidates = type.InstanceConstructors; + + return new RegisterFixData(arguments, methodCandidates, isConstructorInitializer: false); } - private static ImmutableArray> GetArgumentInsertPositionForMethodCandidates( - TArgumentSyntax? argumentOpt, - SemanticModel semanticModel, - ISyntaxFactsService syntaxFacts, - SeparatedSyntaxList arguments, - ImmutableArray methodCandidates) - { - var comparer = syntaxFacts.StringComparer; - var methodsAndArgumentToAdd = ArrayBuilder>.GetInstance(); + return null; + } - foreach (var method in methodCandidates.OrderBy(m => m.Parameters.Length)) + private static ImmutableArray> GetArgumentInsertPositionForMethodCandidates( + TArgumentSyntax? argumentOpt, + SemanticModel semanticModel, + ISyntaxFactsService syntaxFacts, + SeparatedSyntaxList arguments, + ImmutableArray methodCandidates) + { + var comparer = syntaxFacts.StringComparer; + var methodsAndArgumentToAdd = ArrayBuilder>.GetInstance(); + + foreach (var method in methodCandidates.OrderBy(m => m.Parameters.Length)) + { + if (method.IsNonImplicitAndFromSource()) { - if (method.IsNonImplicitAndFromSource()) + var isNamedArgument = !string.IsNullOrWhiteSpace(syntaxFacts.GetNameForArgument(argumentOpt)); + + if (isNamedArgument || NonParamsParameterCount(method) < arguments.Count) { - var isNamedArgument = !string.IsNullOrWhiteSpace(syntaxFacts.GetNameForArgument(argumentOpt)); + var argumentToAdd = DetermineFirstArgumentToAdd( + semanticModel, syntaxFacts, comparer, method, + arguments); - if (isNamedArgument || NonParamsParameterCount(method) < arguments.Count) + if (argumentToAdd != null) { - var argumentToAdd = DetermineFirstArgumentToAdd( - semanticModel, syntaxFacts, comparer, method, - arguments); - - if (argumentToAdd != null) + if (argumentOpt != null && argumentToAdd != argumentOpt) { - if (argumentOpt != null && argumentToAdd != argumentOpt) - { - // We were trying to fix a specific argument, but the argument we want - // to fix is something different. That means there was an error earlier - // than this argument. Which means we're looking at a non-viable - // constructor or method. Skip this one. - continue; - } - - methodsAndArgumentToAdd.Add(new ArgumentInsertPositionData( - method, argumentToAdd, arguments.IndexOf(argumentToAdd))); + // We were trying to fix a specific argument, but the argument we want + // to fix is something different. That means there was an error earlier + // than this argument. Which means we're looking at a non-viable + // constructor or method. Skip this one. + continue; } + + methodsAndArgumentToAdd.Add(new ArgumentInsertPositionData( + method, argumentToAdd, arguments.IndexOf(argumentToAdd))); } } } - - return methodsAndArgumentToAdd.ToImmutableAndFree(); } - private static int NonParamsParameterCount(IMethodSymbol method) - => method.IsParams() ? method.Parameters.Length - 1 : method.Parameters.Length; + return methodsAndArgumentToAdd.ToImmutableAndFree(); + } - private void RegisterFixForMethodOverloads( - CodeFixContext context, - SeparatedSyntaxList arguments, - ImmutableArray> methodsAndArgumentsToAdd) - { - var codeFixData = PrepareCreationOfCodeActions(context.Document, arguments, methodsAndArgumentsToAdd); + private static int NonParamsParameterCount(IMethodSymbol method) + => method.IsParams() ? method.Parameters.Length - 1 : method.Parameters.Length; - // To keep the list of offered fixes short we create one menu entry per overload only - // as long as there are two or less overloads present. If there are more overloads we - // create two menu entries. One entry for non-cascading fixes and one with cascading fixes. - var fixes = codeFixData.Length <= 2 - ? NestByOverload() - : NestByCascading(); + private void RegisterFixForMethodOverloads( + CodeFixContext context, + SeparatedSyntaxList arguments, + ImmutableArray> methodsAndArgumentsToAdd) + { + var codeFixData = PrepareCreationOfCodeActions(context.Document, arguments, methodsAndArgumentsToAdd); + + // To keep the list of offered fixes short we create one menu entry per overload only + // as long as there are two or less overloads present. If there are more overloads we + // create two menu entries. One entry for non-cascading fixes and one with cascading fixes. + var fixes = codeFixData.Length <= 2 + ? NestByOverload() + : NestByCascading(); - context.RegisterFixes(fixes, context.Diagnostics); - return; + context.RegisterFixes(fixes, context.Diagnostics); + return; - ImmutableArray NestByOverload() + ImmutableArray NestByOverload() + { + using var _ = ArrayBuilder.GetInstance(codeFixData.Length, out var builder); + foreach (var data in codeFixData) { - using var _ = ArrayBuilder.GetInstance(codeFixData.Length, out var builder); - foreach (var data in codeFixData) + // We create the mandatory data.CreateChangedSolutionNonCascading fix first. + var title = GetCodeFixTitle(CodeFixesResources.Add_parameter_to_0, data.Method, includeParameters: true); + var codeAction = CodeAction.Create( + title, + data.CreateChangedSolutionNonCascading, + equivalenceKey: title); + if (data.CreateChangedSolutionCascading != null) { - // We create the mandatory data.CreateChangedSolutionNonCascading fix first. - var title = GetCodeFixTitle(CodeFixesResources.Add_parameter_to_0, data.Method, includeParameters: true); - var codeAction = CodeAction.Create( - title, - data.CreateChangedSolutionNonCascading, - equivalenceKey: title); - if (data.CreateChangedSolutionCascading != null) - { - // We have two fixes to offer. We nest the two fixes in an inlinable CodeAction - // so the IDE is free to either show both at once or to create a sub-menu. - var titleForNesting = GetCodeFixTitle(CodeFixesResources.Add_parameter_to_0, data.Method, includeParameters: true); - var titleCascading = GetCodeFixTitle(CodeFixesResources.Add_parameter_to_0_and_overrides_implementations, data.Method, - includeParameters: true); - codeAction = CodeAction.Create( - title: titleForNesting, - [ - codeAction, - CodeAction.Create( - titleCascading, - data.CreateChangedSolutionCascading, - equivalenceKey: titleCascading), - ], - isInlinable: true); - } - - // codeAction is now either a single fix or two fixes wrapped in a CodeActionWithNestedActions - builder.Add(codeAction); + // We have two fixes to offer. We nest the two fixes in an inlinable CodeAction + // so the IDE is free to either show both at once or to create a sub-menu. + var titleForNesting = GetCodeFixTitle(CodeFixesResources.Add_parameter_to_0, data.Method, includeParameters: true); + var titleCascading = GetCodeFixTitle(CodeFixesResources.Add_parameter_to_0_and_overrides_implementations, data.Method, + includeParameters: true); + codeAction = CodeAction.Create( + title: titleForNesting, + [ + codeAction, + CodeAction.Create( + titleCascading, + data.CreateChangedSolutionCascading, + equivalenceKey: titleCascading), + ], + isInlinable: true); } - return builder.ToImmutableAndClear(); + // codeAction is now either a single fix or two fixes wrapped in a CodeActionWithNestedActions + builder.Add(codeAction); } - ImmutableArray NestByCascading() + return builder.ToImmutableAndClear(); + } + + ImmutableArray NestByCascading() + { + using var _ = ArrayBuilder.GetInstance(capacity: 2, out var builder); + + var nonCascadingActions = codeFixData.SelectAsArray(data => { - using var _ = ArrayBuilder.GetInstance(capacity: 2, out var builder); + var title = GetCodeFixTitle(CodeFixesResources.Add_to_0, data.Method, includeParameters: true); + return CodeAction.Create(title, data.CreateChangedSolutionNonCascading, equivalenceKey: title); + }); - var nonCascadingActions = codeFixData.SelectAsArray(data => + var cascadingActions = codeFixData.SelectAsArray( + data => data.CreateChangedSolutionCascading != null, + data => { var title = GetCodeFixTitle(CodeFixesResources.Add_to_0, data.Method, includeParameters: true); - return CodeAction.Create(title, data.CreateChangedSolutionNonCascading, equivalenceKey: title); + return CodeAction.Create(title, data.CreateChangedSolutionCascading!, equivalenceKey: title); }); - var cascadingActions = codeFixData.SelectAsArray( - data => data.CreateChangedSolutionCascading != null, - data => - { - var title = GetCodeFixTitle(CodeFixesResources.Add_to_0, data.Method, includeParameters: true); - return CodeAction.Create(title, data.CreateChangedSolutionCascading!, equivalenceKey: title); - }); - - var aMethod = codeFixData.First().Method; // We need to term the MethodGroup and need an arbitrary IMethodSymbol to do so. - var nestedNonCascadingTitle = GetCodeFixTitle(CodeFixesResources.Add_parameter_to_0, aMethod, includeParameters: false); - - // Create a sub-menu entry with all the non-cascading CodeActions. - // We make sure the IDE does not inline. Otherwise the context menu gets flooded with our fixes. - builder.Add(CodeAction.Create(nestedNonCascadingTitle, nonCascadingActions, isInlinable: false)); + var aMethod = codeFixData.First().Method; // We need to term the MethodGroup and need an arbitrary IMethodSymbol to do so. + var nestedNonCascadingTitle = GetCodeFixTitle(CodeFixesResources.Add_parameter_to_0, aMethod, includeParameters: false); - if (cascadingActions.Length > 0) - { - // if there are cascading CodeActions create a second sub-menu. - var nestedCascadingTitle = GetCodeFixTitle(CodeFixesResources.Add_parameter_to_0_and_overrides_implementations, - aMethod, includeParameters: false); - builder.Add(CodeAction.Create(nestedCascadingTitle, cascadingActions, isInlinable: false)); - } + // Create a sub-menu entry with all the non-cascading CodeActions. + // We make sure the IDE does not inline. Otherwise the context menu gets flooded with our fixes. + builder.Add(CodeAction.Create(nestedNonCascadingTitle, nonCascadingActions, isInlinable: false)); - return builder.ToImmutable(); + if (cascadingActions.Length > 0) + { + // if there are cascading CodeActions create a second sub-menu. + var nestedCascadingTitle = GetCodeFixTitle(CodeFixesResources.Add_parameter_to_0_and_overrides_implementations, + aMethod, includeParameters: false); + builder.Add(CodeAction.Create(nestedCascadingTitle, cascadingActions, isInlinable: false)); } + + return builder.ToImmutable(); } + } - private ImmutableArray PrepareCreationOfCodeActions( - Document document, - SeparatedSyntaxList arguments, - ImmutableArray> methodsAndArgumentsToAdd) + private ImmutableArray PrepareCreationOfCodeActions( + Document document, + SeparatedSyntaxList arguments, + ImmutableArray> methodsAndArgumentsToAdd) + { + using var _ = ArrayBuilder.GetInstance(methodsAndArgumentsToAdd.Length, out var builder); + + // Order by the furthest argument index to the nearest argument index. The ones with + // larger argument indexes mean that we matched more earlier arguments (and thus are + // likely to be the correct match). + foreach (var argumentInsertPositionData in methodsAndArgumentsToAdd.OrderByDescending(t => t.ArgumentInsertionIndex)) { - using var _ = ArrayBuilder.GetInstance(methodsAndArgumentsToAdd.Length, out var builder); + var methodToUpdate = argumentInsertPositionData.MethodToUpdate; + var argumentToInsert = argumentInsertPositionData.ArgumentToInsert; - // Order by the furthest argument index to the nearest argument index. The ones with - // larger argument indexes mean that we matched more earlier arguments (and thus are - // likely to be the correct match). - foreach (var argumentInsertPositionData in methodsAndArgumentsToAdd.OrderByDescending(t => t.ArgumentInsertionIndex)) - { - var methodToUpdate = argumentInsertPositionData.MethodToUpdate; - var argumentToInsert = argumentInsertPositionData.ArgumentToInsert; + var cascadingFix = AddParameterService.HasCascadingDeclarations(methodToUpdate) + ? new Func>(c => FixAsync(document, methodToUpdate, argumentToInsert, arguments, fixAllReferences: true, c)) + : null; - var cascadingFix = AddParameterService.HasCascadingDeclarations(methodToUpdate) - ? new Func>(c => FixAsync(document, methodToUpdate, argumentToInsert, arguments, fixAllReferences: true, c)) - : null; + var codeFixData = new CodeFixData( + methodToUpdate, + c => FixAsync(document, methodToUpdate, argumentToInsert, arguments, fixAllReferences: false, c), + cascadingFix); - var codeFixData = new CodeFixData( - methodToUpdate, - c => FixAsync(document, methodToUpdate, argumentToInsert, arguments, fixAllReferences: false, c), - cascadingFix); + builder.Add(codeFixData); + } - builder.Add(codeFixData); - } + return builder.ToImmutable(); + } - return builder.ToImmutable(); - } + private static string GetCodeFixTitle(string resourceString, IMethodSymbol methodToUpdate, bool includeParameters) + { + var methodDisplay = methodToUpdate.ToDisplayString(new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, + extensionMethodStyle: SymbolDisplayExtensionMethodStyle.StaticMethod, + parameterOptions: SymbolDisplayParameterOptions.None, + memberOptions: SymbolDisplayMemberOptions.None)); + + var parameters = methodToUpdate.Parameters.Select(p => p.ToDisplayString(SimpleFormat)); + var signature = includeParameters + ? $"{methodDisplay}({string.Join(", ", parameters)})" + : methodDisplay; + var title = string.Format(resourceString, signature); + return title; + } - private static string GetCodeFixTitle(string resourceString, IMethodSymbol methodToUpdate, bool includeParameters) - { - var methodDisplay = methodToUpdate.ToDisplayString(new SymbolDisplayFormat( - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, - extensionMethodStyle: SymbolDisplayExtensionMethodStyle.StaticMethod, - parameterOptions: SymbolDisplayParameterOptions.None, - memberOptions: SymbolDisplayMemberOptions.None)); - - var parameters = methodToUpdate.Parameters.Select(p => p.ToDisplayString(SimpleFormat)); - var signature = includeParameters - ? $"{methodDisplay}({string.Join(", ", parameters)})" - : methodDisplay; - var title = string.Format(resourceString, signature); - return title; - } + private async Task FixAsync( + Document invocationDocument, + IMethodSymbol method, + TArgumentSyntax argument, + SeparatedSyntaxList argumentList, + bool fixAllReferences, + CancellationToken cancellationToken) + { + var (argumentType, refKind) = await GetArgumentTypeAndRefKindAsync(invocationDocument, argument, cancellationToken).ConfigureAwait(false); + + // The argumentNameSuggestion is the base for the parameter name. + // For each method declaration the name is made unique to avoid name collisions. + var (argumentNameSuggestion, isNamedArgument) = await GetNameSuggestionForArgumentAsync( + invocationDocument, argument, method.ContainingType, cancellationToken).ConfigureAwait(false); + + var newParameterIndex = isNamedArgument ? (int?)null : argumentList.IndexOf(argument); + return await AddParameterService.AddParameterAsync( + invocationDocument, + method, + argumentType, + refKind, + argumentNameSuggestion, + newParameterIndex, + fixAllReferences, + cancellationToken).ConfigureAwait(false); + } + + private async Task<(ITypeSymbol, RefKind)> GetArgumentTypeAndRefKindAsync(Document invocationDocument, TArgumentSyntax argument, CancellationToken cancellationToken) + { + var syntaxFacts = invocationDocument.GetRequiredLanguageService(); + var semanticModel = await invocationDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var argumentType = GetArgumentType(argument, semanticModel, cancellationToken); + var refKind = syntaxFacts.GetRefKindOfArgument(argument); + return (argumentType, refKind); + } - private async Task FixAsync( - Document invocationDocument, - IMethodSymbol method, - TArgumentSyntax argument, - SeparatedSyntaxList argumentList, - bool fixAllReferences, - CancellationToken cancellationToken) + private static async Task<(string argumentNameSuggestion, bool isNamed)> GetNameSuggestionForArgumentAsync( + Document invocationDocument, TArgumentSyntax argument, INamedTypeSymbol containingType, CancellationToken cancellationToken) + { + var syntaxFacts = invocationDocument.GetRequiredLanguageService(); + + var argumentName = syntaxFacts.GetNameForArgument(argument); + if (!string.IsNullOrWhiteSpace(argumentName)) { - var (argumentType, refKind) = await GetArgumentTypeAndRefKindAsync(invocationDocument, argument, cancellationToken).ConfigureAwait(false); - - // The argumentNameSuggestion is the base for the parameter name. - // For each method declaration the name is made unique to avoid name collisions. - var (argumentNameSuggestion, isNamedArgument) = await GetNameSuggestionForArgumentAsync( - invocationDocument, argument, method.ContainingType, cancellationToken).ConfigureAwait(false); - - var newParameterIndex = isNamedArgument ? (int?)null : argumentList.IndexOf(argument); - return await AddParameterService.AddParameterAsync( - invocationDocument, - method, - argumentType, - refKind, - argumentNameSuggestion, - newParameterIndex, - fixAllReferences, - cancellationToken).ConfigureAwait(false); + return (argumentNameSuggestion: argumentName, isNamed: true); } - - private async Task<(ITypeSymbol, RefKind)> GetArgumentTypeAndRefKindAsync(Document invocationDocument, TArgumentSyntax argument, CancellationToken cancellationToken) + else { - var syntaxFacts = invocationDocument.GetRequiredLanguageService(); var semanticModel = await invocationDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var argumentType = GetArgumentType(argument, semanticModel, cancellationToken); - var refKind = syntaxFacts.GetRefKindOfArgument(argument); - return (argumentType, refKind); + var expression = syntaxFacts.GetExpressionOfArgument(argument); + var semanticFacts = invocationDocument.GetRequiredLanguageService(); + argumentName = semanticFacts.GenerateNameForExpression( + semanticModel, expression, capitalize: containingType.IsRecord, cancellationToken: cancellationToken); + return (argumentNameSuggestion: argumentName, isNamed: false); } + } - private static async Task<(string argumentNameSuggestion, bool isNamed)> GetNameSuggestionForArgumentAsync( - Document invocationDocument, TArgumentSyntax argument, INamedTypeSymbol containingType, CancellationToken cancellationToken) - { - var syntaxFacts = invocationDocument.GetRequiredLanguageService(); + private static readonly SymbolDisplayFormat SimpleFormat = + new( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + parameterOptions: SymbolDisplayParameterOptions.IncludeParamsRefOut | SymbolDisplayParameterOptions.IncludeType, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + private static TArgumentSyntax? DetermineFirstArgumentToAdd( + SemanticModel semanticModel, + ISyntaxFactsService syntaxFacts, + StringComparer comparer, + IMethodSymbol method, + SeparatedSyntaxList arguments) + { + var compilation = semanticModel.Compilation; + var methodParameterNames = new HashSet(comparer); + methodParameterNames.AddRange(method.Parameters.Select(p => p.Name)); + for (int i = 0, n = arguments.Count; i < n; i++) + { + var argument = arguments[i]; var argumentName = syntaxFacts.GetNameForArgument(argument); + if (!string.IsNullOrWhiteSpace(argumentName)) { - return (argumentNameSuggestion: argumentName, isNamed: true); + // If the user provided an argument-name and we don't have any parameters that + // match, then this is the argument we want to add a parameter for. + if (!methodParameterNames.Contains(argumentName)) + { + return argument; + } } else { - var semanticModel = await invocationDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var expression = syntaxFacts.GetExpressionOfArgument(argument); - var semanticFacts = invocationDocument.GetRequiredLanguageService(); - argumentName = semanticFacts.GenerateNameForExpression( - semanticModel, expression, capitalize: containingType.IsRecord, cancellationToken: cancellationToken); - return (argumentNameSuggestion: argumentName, isNamed: false); - } - } - - private static readonly SymbolDisplayFormat SimpleFormat = - new( - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - parameterOptions: SymbolDisplayParameterOptions.IncludeParamsRefOut | SymbolDisplayParameterOptions.IncludeType, - miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - private static TArgumentSyntax? DetermineFirstArgumentToAdd( - SemanticModel semanticModel, - ISyntaxFactsService syntaxFacts, - StringComparer comparer, - IMethodSymbol method, - SeparatedSyntaxList arguments) - { - var compilation = semanticModel.Compilation; - var methodParameterNames = new HashSet(comparer); - methodParameterNames.AddRange(method.Parameters.Select(p => p.Name)); - - for (int i = 0, n = arguments.Count; i < n; i++) - { - var argument = arguments[i]; - var argumentName = syntaxFacts.GetNameForArgument(argument); - - if (!string.IsNullOrWhiteSpace(argumentName)) + // Positional argument. If the position is beyond what the method supports, + // then this definitely is an argument we could add. + if (i >= method.Parameters.Length) { - // If the user provided an argument-name and we don't have any parameters that - // match, then this is the argument we want to add a parameter for. - if (!methodParameterNames.Contains(argumentName)) + if (method.Parameters.LastOrDefault()?.IsParams == true) { - return argument; + // Last parameter is a params. We can't place any parameters past it. + return null; } - } - else - { - // Positional argument. If the position is beyond what the method supports, - // then this definitely is an argument we could add. - if (i >= method.Parameters.Length) - { - if (method.Parameters.LastOrDefault()?.IsParams == true) - { - // Last parameter is a params. We can't place any parameters past it. - return null; - } - return argument; - } + return argument; + } - // Now check the type of the argument versus the type of the parameter. If they - // don't match, then this is the argument we should make the parameter for. - var expressionOfArgument = syntaxFacts.GetExpressionOfArgument(argument); - if (expressionOfArgument is null) - { - return null; - } + // Now check the type of the argument versus the type of the parameter. If they + // don't match, then this is the argument we should make the parameter for. + var expressionOfArgument = syntaxFacts.GetExpressionOfArgument(argument); + if (expressionOfArgument is null) + { + return null; + } - var argumentTypeInfo = semanticModel.GetTypeInfo(expressionOfArgument); - var isNullLiteral = syntaxFacts.IsNullLiteralExpression(expressionOfArgument); - var isDefaultLiteral = syntaxFacts.IsDefaultLiteralExpression(expressionOfArgument); + var argumentTypeInfo = semanticModel.GetTypeInfo(expressionOfArgument); + var isNullLiteral = syntaxFacts.IsNullLiteralExpression(expressionOfArgument); + var isDefaultLiteral = syntaxFacts.IsDefaultLiteralExpression(expressionOfArgument); - if (argumentTypeInfo.Type == null && argumentTypeInfo.ConvertedType == null) + if (argumentTypeInfo.Type == null && argumentTypeInfo.ConvertedType == null) + { + // Didn't know the type of the argument. We shouldn't assume it doesn't + // match a parameter. However, if the user wrote 'null' and it didn't + // match anything, then this is the problem argument. + if (!isNullLiteral && !isDefaultLiteral) { - // Didn't know the type of the argument. We shouldn't assume it doesn't - // match a parameter. However, if the user wrote 'null' and it didn't - // match anything, then this is the problem argument. - if (!isNullLiteral && !isDefaultLiteral) - { - continue; - } + continue; } + } - var parameter = method.Parameters[i]; + var parameter = method.Parameters[i]; - if (!TypeInfoMatchesType( - compilation, argumentTypeInfo, parameter.Type, + if (!TypeInfoMatchesType( + compilation, argumentTypeInfo, parameter.Type, + isNullLiteral, isDefaultLiteral)) + { + if (TypeInfoMatchesWithParamsExpansion( + compilation, argumentTypeInfo, parameter, isNullLiteral, isDefaultLiteral)) { - if (TypeInfoMatchesWithParamsExpansion( - compilation, argumentTypeInfo, parameter, - isNullLiteral, isDefaultLiteral)) - { - // The argument matched if we expanded out the params-parameter. - // As the params-parameter has to be last, there's nothing else to - // do here. - return null; - } - - return argument; + // The argument matched if we expanded out the params-parameter. + // As the params-parameter has to be last, there's nothing else to + // do here. + return null; } + + return argument; } } - - return null; } - private static bool TypeInfoMatchesWithParamsExpansion( - Compilation compilation, TypeInfo argumentTypeInfo, IParameterSymbol parameter, - bool isNullLiteral, bool isDefaultLiteral) + return null; + } + + private static bool TypeInfoMatchesWithParamsExpansion( + Compilation compilation, TypeInfo argumentTypeInfo, IParameterSymbol parameter, + bool isNullLiteral, bool isDefaultLiteral) + { + if (parameter.IsParams && parameter.Type is IArrayTypeSymbol arrayType) { - if (parameter.IsParams && parameter.Type is IArrayTypeSymbol arrayType) + if (TypeInfoMatchesType( + compilation, argumentTypeInfo, arrayType.ElementType, + isNullLiteral, isDefaultLiteral)) { - if (TypeInfoMatchesType( - compilation, argumentTypeInfo, arrayType.ElementType, - isNullLiteral, isDefaultLiteral)) - { - return true; - } + return true; } - - return false; } - private static bool TypeInfoMatchesType( - Compilation compilation, TypeInfo argumentTypeInfo, ITypeSymbol parameterType, - bool isNullLiteral, bool isDefaultLiteral) - { - if (parameterType.Equals(argumentTypeInfo.Type) || parameterType.Equals(argumentTypeInfo.ConvertedType)) - return true; - - if (isDefaultLiteral) - return true; - - if (isNullLiteral) - return parameterType.IsReferenceType || parameterType.IsNullable(); + return false; + } - // Overload resolution couldn't resolve the actual type of the type parameter. We assume - // that the type parameter can be the argument's type (ignoring any type parameter constraints). - if (parameterType.Kind == SymbolKind.TypeParameter) + private static bool TypeInfoMatchesType( + Compilation compilation, TypeInfo argumentTypeInfo, ITypeSymbol parameterType, + bool isNullLiteral, bool isDefaultLiteral) + { + if (parameterType.Equals(argumentTypeInfo.Type) || parameterType.Equals(argumentTypeInfo.ConvertedType)) + return true; + + if (isDefaultLiteral) + return true; + + if (isNullLiteral) + return parameterType.IsReferenceType || parameterType.IsNullable(); + + // Overload resolution couldn't resolve the actual type of the type parameter. We assume + // that the type parameter can be the argument's type (ignoring any type parameter constraints). + if (parameterType.Kind == SymbolKind.TypeParameter) + return true; + + // If there's an implicit conversion from the arg type to the param type then + // count this as a match. This happens commonly with cases like: + // + // `Goo(derivedType)` + // `void Goo(BaseType baseType)`. + // + // We want this simple case to match. + if (argumentTypeInfo.Type != null) + { + var conversion = compilation.ClassifyCommonConversion(argumentTypeInfo.Type, parameterType); + if (conversion.IsImplicit) return true; - - // If there's an implicit conversion from the arg type to the param type then - // count this as a match. This happens commonly with cases like: - // - // `Goo(derivedType)` - // `void Goo(BaseType baseType)`. - // - // We want this simple case to match. - if (argumentTypeInfo.Type != null) - { - var conversion = compilation.ClassifyCommonConversion(argumentTypeInfo.Type, parameterType); - if (conversion.IsImplicit) - return true; - } - - return false; } + + return false; } } diff --git a/src/Analyzers/Core/CodeFixes/AddParameter/AddParameterService.cs b/src/Analyzers/Core/CodeFixes/AddParameter/AddParameterService.cs index b6056af8e2fdb..9f8cec634e46c 100644 --- a/src/Analyzers/Core/CodeFixes/AddParameter/AddParameterService.cs +++ b/src/Analyzers/Core/CodeFixes/AddParameter/AddParameterService.cs @@ -15,158 +15,157 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.AddParameter +namespace Microsoft.CodeAnalysis.AddParameter; + +internal static class AddParameterService { - internal static class AddParameterService + /// + /// Checks if there are indications that there might be more than one declarations that need to be fixed. + /// The check does not look-up if there are other declarations (this is done later in the CodeAction). + /// + public static bool HasCascadingDeclarations(IMethodSymbol method) { - /// - /// Checks if there are indications that there might be more than one declarations that need to be fixed. - /// The check does not look-up if there are other declarations (this is done later in the CodeAction). - /// - public static bool HasCascadingDeclarations(IMethodSymbol method) + // Don't cascade constructors + if (method.IsConstructor()) { - // Don't cascade constructors - if (method.IsConstructor()) - { - return false; - } - - // Virtual methods of all kinds might have overrides somewhere else that need to be fixed. - if (method.IsVirtual || method.IsOverride || method.IsAbstract) - { - return true; - } - - // If interfaces are involved we will fix those too - // Explicit interface implementations are easy to detect - if (method.ExplicitInterfaceImplementations.Length > 0) - { - return true; - } + return false; + } - // For implicit interface implementations lets check if the characteristic of the method - // allows it to implicit implement an interface member. - if (method.DeclaredAccessibility != Accessibility.Public) - { - return false; - } + // Virtual methods of all kinds might have overrides somewhere else that need to be fixed. + if (method.IsVirtual || method.IsOverride || method.IsAbstract) + { + return true; + } - if (method.IsStatic) - { - return false; - } + // If interfaces are involved we will fix those too + // Explicit interface implementations are easy to detect + if (method.ExplicitInterfaceImplementations.Length > 0) + { + return true; + } - // Now check if the method does implement an interface member - if (method.ExplicitOrImplicitInterfaceImplementations().Length > 0) - { - return true; - } + // For implicit interface implementations lets check if the characteristic of the method + // allows it to implicit implement an interface member. + if (method.DeclaredAccessibility != Accessibility.Public) + { + return false; + } + if (method.IsStatic) + { return false; } - /// - /// Adds a parameter to a method. - /// - /// to add as the final parameter - /// - public static async Task AddParameterAsync( - Document invocationDocument, - IMethodSymbol method, - ITypeSymbol newParameterType, - RefKind refKind, - string parameterName, - int? newParameterIndex, - bool fixAllReferences, - CancellationToken cancellationToken) + // Now check if the method does implement an interface member + if (method.ExplicitOrImplicitInterfaceImplementations().Length > 0) { - var solution = invocationDocument.Project.Solution; + return true; + } - var referencedSymbols = fixAllReferences - ? await FindMethodDeclarationReferencesAsync(invocationDocument, method, cancellationToken).ConfigureAwait(false) - : method.GetAllMethodSymbolsOfPartialParts(); + return false; + } - var anySymbolReferencesNotInSource = referencedSymbols.Any(static symbol => !symbol.IsFromSource()); - var locationsInSource = referencedSymbols.Where(symbol => symbol.IsFromSource()); + /// + /// Adds a parameter to a method. + /// + /// to add as the final parameter + /// + public static async Task AddParameterAsync( + Document invocationDocument, + IMethodSymbol method, + ITypeSymbol newParameterType, + RefKind refKind, + string parameterName, + int? newParameterIndex, + bool fixAllReferences, + CancellationToken cancellationToken) + { + var solution = invocationDocument.Project.Solution; - // Indexing Locations[0] is valid because IMethodSymbols have one location at most - // and IsFromSource() tests if there is at least one location. - var locationsByDocument = locationsInSource.ToLookup(declarationLocation - => solution.GetRequiredDocument(declarationLocation.Locations[0].SourceTree!)); + var referencedSymbols = fixAllReferences + ? await FindMethodDeclarationReferencesAsync(invocationDocument, method, cancellationToken).ConfigureAwait(false) + : method.GetAllMethodSymbolsOfPartialParts(); - foreach (var documentLookup in locationsByDocument) - { - var document = documentLookup.Key; + var anySymbolReferencesNotInSource = referencedSymbols.Any(static symbol => !symbol.IsFromSource()); + var locationsInSource = referencedSymbols.Where(symbol => symbol.IsFromSource()); - // May not have syntax facts for a different language in CodeStyle layer. - var syntaxFacts = document.GetLanguageService(); - if (syntaxFacts is null) - continue; + // Indexing Locations[0] is valid because IMethodSymbols have one location at most + // and IsFromSource() tests if there is at least one location. + var locationsByDocument = locationsInSource.ToLookup(declarationLocation + => solution.GetRequiredDocument(declarationLocation.Locations[0].SourceTree!)); + + foreach (var documentLookup in locationsByDocument) + { + var document = documentLookup.Key; + + // May not have syntax facts for a different language in CodeStyle layer. + var syntaxFacts = document.GetLanguageService(); + if (syntaxFacts is null) + continue; + + var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var editor = new SyntaxEditor(syntaxRoot, solution.Services); + var generator = editor.Generator; + foreach (var methodDeclaration in documentLookup) + { + var methodNode = syntaxRoot.FindNode(methodDeclaration.Locations[0].SourceSpan, getInnermostNodeForTie: true); + var existingParameters = generator.GetParameters(methodNode); + var insertionIndex = newParameterIndex ?? existingParameters.Count; + + // if the preceding parameter is optional, the new parameter must also be optional + // see also BC30202 and CS1737 + var parameterMustBeOptional = insertionIndex > 0 && + syntaxFacts.GetDefaultOfParameter(existingParameters[insertionIndex - 1]) != null; + + var parameterSymbol = CreateParameterSymbol( + methodDeclaration, newParameterType, refKind, parameterMustBeOptional, parameterName); + + var argumentInitializer = parameterMustBeOptional ? generator.DefaultExpression(newParameterType) : null; + var parameterDeclaration = generator.ParameterDeclaration(parameterSymbol, argumentInitializer) + .WithAdditionalAnnotations(Formatter.Annotation); + if (anySymbolReferencesNotInSource && methodDeclaration == method) + { + parameterDeclaration = parameterDeclaration.WithAdditionalAnnotations( + ConflictAnnotation.Create(CodeFixesResources.Related_method_signatures_found_in_metadata_will_not_be_updated)); + } - var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var editor = new SyntaxEditor(syntaxRoot, solution.Services); - var generator = editor.Generator; - foreach (var methodDeclaration in documentLookup) + if (method.MethodKind == MethodKind.ReducedExtension && insertionIndex < existingParameters.Count) { - var methodNode = syntaxRoot.FindNode(methodDeclaration.Locations[0].SourceSpan, getInnermostNodeForTie: true); - var existingParameters = generator.GetParameters(methodNode); - var insertionIndex = newParameterIndex ?? existingParameters.Count; - - // if the preceding parameter is optional, the new parameter must also be optional - // see also BC30202 and CS1737 - var parameterMustBeOptional = insertionIndex > 0 && - syntaxFacts.GetDefaultOfParameter(existingParameters[insertionIndex - 1]) != null; - - var parameterSymbol = CreateParameterSymbol( - methodDeclaration, newParameterType, refKind, parameterMustBeOptional, parameterName); - - var argumentInitializer = parameterMustBeOptional ? generator.DefaultExpression(newParameterType) : null; - var parameterDeclaration = generator.ParameterDeclaration(parameterSymbol, argumentInitializer) - .WithAdditionalAnnotations(Formatter.Annotation); - if (anySymbolReferencesNotInSource && methodDeclaration == method) - { - parameterDeclaration = parameterDeclaration.WithAdditionalAnnotations( - ConflictAnnotation.Create(CodeFixesResources.Related_method_signatures_found_in_metadata_will_not_be_updated)); - } - - if (method.MethodKind == MethodKind.ReducedExtension && insertionIndex < existingParameters.Count) - { - insertionIndex++; - } - - AddParameterEditor.AddParameter(syntaxFacts, editor, methodNode, insertionIndex, parameterDeclaration, cancellationToken); + insertionIndex++; } - var newRoot = editor.GetChangedRoot(); - solution = solution.WithDocumentSyntaxRoot(document.Id, newRoot); + AddParameterEditor.AddParameter(syntaxFacts, editor, methodNode, insertionIndex, parameterDeclaration, cancellationToken); } - return solution; + var newRoot = editor.GetChangedRoot(); + solution = solution.WithDocumentSyntaxRoot(document.Id, newRoot); } - private static async Task> FindMethodDeclarationReferencesAsync( - Document invocationDocument, IMethodSymbol method, CancellationToken cancellationToken) - { - var referencedSymbols = await SymbolFinder.FindReferencesAsync( - method, invocationDocument.Project.Solution, cancellationToken).ConfigureAwait(false); + return solution; + } - return referencedSymbols.Select(referencedSymbol => referencedSymbol.Definition) - .OfType() - .Distinct() - .ToImmutableArray(); - } + private static async Task> FindMethodDeclarationReferencesAsync( + Document invocationDocument, IMethodSymbol method, CancellationToken cancellationToken) + { + var referencedSymbols = await SymbolFinder.FindReferencesAsync( + method, invocationDocument.Project.Solution, cancellationToken).ConfigureAwait(false); - private static IParameterSymbol CreateParameterSymbol( - IMethodSymbol method, - ITypeSymbol parameterType, - RefKind refKind, - bool isOptional, - string argumentNameSuggestion) - { - var uniqueName = NameGenerator.EnsureUniqueness(argumentNameSuggestion, method.Parameters.Select(p => p.Name)); - var newParameterSymbol = CodeGenerationSymbolFactory.CreateParameterSymbol( - attributes: default, refKind: refKind, isOptional: isOptional, isParams: false, type: parameterType, name: uniqueName); - return newParameterSymbol; - } + return referencedSymbols.Select(referencedSymbol => referencedSymbol.Definition) + .OfType() + .Distinct() + .ToImmutableArray(); + } + + private static IParameterSymbol CreateParameterSymbol( + IMethodSymbol method, + ITypeSymbol parameterType, + RefKind refKind, + bool isOptional, + string argumentNameSuggestion) + { + var uniqueName = NameGenerator.EnsureUniqueness(argumentNameSuggestion, method.Parameters.Select(p => p.Name)); + var newParameterSymbol = CodeGenerationSymbolFactory.CreateParameterSymbol( + attributes: default, refKind: refKind, isOptional: isOptional, isParams: false, type: parameterType, name: uniqueName); + return newParameterSymbol; } } diff --git a/src/Analyzers/Core/CodeFixes/AddParameter/ArgumentInsertPositionData.cs b/src/Analyzers/Core/CodeFixes/AddParameter/ArgumentInsertPositionData.cs index 0079867c387e9..75d50953b0b96 100644 --- a/src/Analyzers/Core/CodeFixes/AddParameter/ArgumentInsertPositionData.cs +++ b/src/Analyzers/Core/CodeFixes/AddParameter/ArgumentInsertPositionData.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.AddParameter +namespace Microsoft.CodeAnalysis.AddParameter; + +internal readonly struct ArgumentInsertPositionData(IMethodSymbol methodToUpdate, TArgumentSyntax argumentToInsert, int argumentInsertionIndex) where TArgumentSyntax : SyntaxNode { - internal readonly struct ArgumentInsertPositionData(IMethodSymbol methodToUpdate, TArgumentSyntax argumentToInsert, int argumentInsertionIndex) where TArgumentSyntax : SyntaxNode - { - public IMethodSymbol MethodToUpdate { get; } = methodToUpdate; - public TArgumentSyntax ArgumentToInsert { get; } = argumentToInsert; - public int ArgumentInsertionIndex { get; } = argumentInsertionIndex; - } + public IMethodSymbol MethodToUpdate { get; } = methodToUpdate; + public TArgumentSyntax ArgumentToInsert { get; } = argumentToInsert; + public int ArgumentInsertionIndex { get; } = argumentInsertionIndex; } diff --git a/src/Analyzers/Core/CodeFixes/AddParameter/CodeFixData.cs b/src/Analyzers/Core/CodeFixes/AddParameter/CodeFixData.cs index 40fe39d9cf600..77cab725f4245 100644 --- a/src/Analyzers/Core/CodeFixes/AddParameter/CodeFixData.cs +++ b/src/Analyzers/Core/CodeFixes/AddParameter/CodeFixData.cs @@ -6,27 +6,26 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.AddParameter +namespace Microsoft.CodeAnalysis.AddParameter; + +internal readonly struct CodeFixData( + IMethodSymbol method, + Func> createChangedSolutionNonCascading, + Func>? createChangedSolutionCascading) { - internal readonly struct CodeFixData( - IMethodSymbol method, - Func> createChangedSolutionNonCascading, - Func>? createChangedSolutionCascading) - { - /// - /// The overload to fix. - /// - public IMethodSymbol Method { get; } = method ?? throw new ArgumentNullException(nameof(method)); + /// + /// The overload to fix. + /// + public IMethodSymbol Method { get; } = method ?? throw new ArgumentNullException(nameof(method)); - /// - /// A mandatory fix for the overload without cascading. - /// - public Func> CreateChangedSolutionNonCascading { get; } = createChangedSolutionNonCascading ?? throw new ArgumentNullException(nameof(createChangedSolutionNonCascading)); + /// + /// A mandatory fix for the overload without cascading. + /// + public Func> CreateChangedSolutionNonCascading { get; } = createChangedSolutionNonCascading ?? throw new ArgumentNullException(nameof(createChangedSolutionNonCascading)); - /// - /// An optional fix for the overload with cascading. - /// - public Func>? CreateChangedSolutionCascading { get; } = createChangedSolutionCascading; - } + /// + /// An optional fix for the overload with cascading. + /// + public Func>? CreateChangedSolutionCascading { get; } = createChangedSolutionCascading; } diff --git a/src/Analyzers/Core/CodeFixes/AddParameter/RegisterFixData.cs b/src/Analyzers/Core/CodeFixes/AddParameter/RegisterFixData.cs index 1617ef1e07cb9..033a326a6a5bc 100644 --- a/src/Analyzers/Core/CodeFixes/AddParameter/RegisterFixData.cs +++ b/src/Analyzers/Core/CodeFixes/AddParameter/RegisterFixData.cs @@ -4,17 +4,16 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.AddParameter +namespace Microsoft.CodeAnalysis.AddParameter; + +internal class RegisterFixData(SeparatedSyntaxList arguments, ImmutableArray methodCandidates, bool isConstructorInitializer) + where TArgumentSyntax : SyntaxNode { - internal class RegisterFixData(SeparatedSyntaxList arguments, ImmutableArray methodCandidates, bool isConstructorInitializer) - where TArgumentSyntax : SyntaxNode + public RegisterFixData() : this([], [], false) { - public RegisterFixData() : this([], [], false) - { - } - - public SeparatedSyntaxList Arguments { get; } = arguments; - public ImmutableArray MethodCandidates { get; } = methodCandidates; - public bool IsConstructorInitializer { get; } = isConstructorInitializer; } + + public SeparatedSyntaxList Arguments { get; } = arguments; + public ImmutableArray MethodCandidates { get; } = methodCandidates; + public bool IsConstructorInitializer { get; } = isConstructorInitializer; } diff --git a/src/Analyzers/Core/CodeFixes/AliasAmbiguousType/AbstractAliasAmbiguousTypeCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/AliasAmbiguousType/AbstractAliasAmbiguousTypeCodeFixProvider.cs index f75f4ec1980bb..ff015dabf6f0f 100644 --- a/src/Analyzers/Core/CodeFixes/AliasAmbiguousType/AbstractAliasAmbiguousTypeCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/AliasAmbiguousType/AbstractAliasAmbiguousTypeCodeFixProvider.cs @@ -19,142 +19,141 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AliasAmbiguousType +namespace Microsoft.CodeAnalysis.AliasAmbiguousType; + +internal abstract class AbstractAliasAmbiguousTypeCodeFixProvider : CodeFixProvider { - internal abstract class AbstractAliasAmbiguousTypeCodeFixProvider : CodeFixProvider - { - protected abstract string GetTextPreviewOfChange(string aliasName, ITypeSymbol typeSymbol); + protected abstract string GetTextPreviewOfChange(string aliasName, ITypeSymbol typeSymbol); - public override FixAllProvider? GetFixAllProvider() => null; + public override FixAllProvider? GetFixAllProvider() => null; - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var cancellationToken = context.CancellationToken; - var document = context.Document; - var syntaxFacts = document.GetRequiredLanguageService(); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var cancellationToken = context.CancellationToken; + var document = context.Document; + var syntaxFacts = document.GetRequiredLanguageService(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // Innermost: We are looking for an IdentifierName. IdentifierName is sometimes at the same span as its parent (e.g. SimpleBaseTypeSyntax). - var diagnosticNode = root.FindNode(context.Span, getInnermostNodeForTie: true); - if (!syntaxFacts.IsIdentifierName(diagnosticNode)) - return; + // Innermost: We are looking for an IdentifierName. IdentifierName is sometimes at the same span as its parent (e.g. SimpleBaseTypeSyntax). + var diagnosticNode = root.FindNode(context.Span, getInnermostNodeForTie: true); + if (!syntaxFacts.IsIdentifierName(diagnosticNode)) + return; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var symbolInfo = semanticModel.GetSymbolInfo(diagnosticNode, cancellationToken); - if (!SymbolCandidatesContainsSupportedSymbols(symbolInfo)) - return; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var symbolInfo = semanticModel.GetSymbolInfo(diagnosticNode, cancellationToken); + if (!SymbolCandidatesContainsSupportedSymbols(symbolInfo)) + return; - var addImportService = document.GetRequiredLanguageService(); - var syntaxGenerator = document.GetRequiredLanguageService(); - var compilation = semanticModel.Compilation; + var addImportService = document.GetRequiredLanguageService(); + var syntaxGenerator = document.GetRequiredLanguageService(); + var compilation = semanticModel.Compilation; - var placementOption = await document.GetAddImportPlacementOptionsAsync(addImportService, context.GetOptionsProvider(), cancellationToken).ConfigureAwait(false); + var placementOption = await document.GetAddImportPlacementOptionsAsync(addImportService, context.GetOptionsProvider(), cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var actions); - foreach (var symbol in Sort(symbolInfo.CandidateSymbols.Cast(), placementOption.PlaceSystemNamespaceFirst)) - { - var typeName = symbol.Name; - var title = GetTextPreviewOfChange(typeName, symbol); - - actions.Add(CodeAction.Create( - title, - cancellationToken => - { - var aliasDirective = syntaxGenerator.AliasImportDeclaration(typeName, symbol); - var newRoot = addImportService.AddImport(compilation, root, diagnosticNode, aliasDirective, syntaxGenerator, placementOption, cancellationToken); - return Task.FromResult(document.WithSyntaxRoot(newRoot)); - }, - title)); - } + using var _ = ArrayBuilder.GetInstance(out var actions); + foreach (var symbol in Sort(symbolInfo.CandidateSymbols.Cast(), placementOption.PlaceSystemNamespaceFirst)) + { + var typeName = symbol.Name; + var title = GetTextPreviewOfChange(typeName, symbol); - context.RegisterCodeFix( - CodeAction.Create( - string.Format(CodeFixesResources.Alias_ambiguous_type_0, diagnosticNode.ToString()), - actions.ToImmutable(), - isInlinable: true), - context.Diagnostics.First()); + actions.Add(CodeAction.Create( + title, + cancellationToken => + { + var aliasDirective = syntaxGenerator.AliasImportDeclaration(typeName, symbol); + var newRoot = addImportService.AddImport(compilation, root, diagnosticNode, aliasDirective, syntaxGenerator, placementOption, cancellationToken); + return Task.FromResult(document.WithSyntaxRoot(newRoot)); + }, + title)); } - private static IEnumerable Sort(IEnumerable types, bool sortSystemFirst) + context.RegisterCodeFix( + CodeAction.Create( + string.Format(CodeFixesResources.Alias_ambiguous_type_0, diagnosticNode.ToString()), + actions.ToImmutable(), + isInlinable: true), + context.Diagnostics.First()); + } + + private static IEnumerable Sort(IEnumerable types, bool sortSystemFirst) + { + // get all the name portions of the fully-qualified-names of the types in 'types'. + // cache these in this local dictionary so we only have to compute them once. + var typeToNameSegments = new Dictionary>(); + + return types.OrderBy((t1, t2) => { - // get all the name portions of the fully-qualified-names of the types in 'types'. - // cache these in this local dictionary so we only have to compute them once. - var typeToNameSegments = new Dictionary>(); + var t1NameSegments = GetNameSegments(t1); + var t2NameSegments = GetNameSegments(t2); - return types.OrderBy((t1, t2) => + // compare all the name segments the two types have in common. + for (int i = 0, n = Math.Min(t1NameSegments.Length, t2NameSegments.Length); i < n; i++) { - var t1NameSegments = GetNameSegments(t1); - var t2NameSegments = GetNameSegments(t2); + var t1NameSegment = t1NameSegments[i]; + var t2NameSegment = t2NameSegments[i]; - // compare all the name segments the two types have in common. - for (int i = 0, n = Math.Min(t1NameSegments.Length, t2NameSegments.Length); i < n; i++) - { - var t1NameSegment = t1NameSegments[i]; - var t2NameSegment = t2NameSegments[i]; - - // if we're on the first name segment, ensure we sort 'System' properly if the user - // prefers them coming first. - var comparer = i == 0 && sortSystemFirst ? SortSystemFirstComparer.Instance : StringComparer.Ordinal; + // if we're on the first name segment, ensure we sort 'System' properly if the user + // prefers them coming first. + var comparer = i == 0 && sortSystemFirst ? SortSystemFirstComparer.Instance : StringComparer.Ordinal; - var diff = comparer.Compare(t1NameSegment, t2NameSegment); - if (diff != 0) - return diff; - } + var diff = comparer.Compare(t1NameSegment, t2NameSegment); + if (diff != 0) + return diff; + } - // if all the names matched up to this point, then the type with the shorter number of segments comes first. - return t1NameSegments.Length - t2NameSegments.Length; - }); + // if all the names matched up to this point, then the type with the shorter number of segments comes first. + return t1NameSegments.Length - t2NameSegments.Length; + }); - ImmutableArray GetNameSegments(ITypeSymbol symbol) + ImmutableArray GetNameSegments(ITypeSymbol symbol) + { + return typeToNameSegments.GetOrAdd(symbol, static symbol => { - return typeToNameSegments.GetOrAdd(symbol, static symbol => - { - using var result = TemporaryArray.Empty; + using var result = TemporaryArray.Empty; - for (ISymbol current = symbol; current != null; current = current.ContainingSymbol) - { - if (string.IsNullOrEmpty(current.Name)) - break; + for (ISymbol current = symbol; current != null; current = current.ContainingSymbol) + { + if (string.IsNullOrEmpty(current.Name)) + break; - result.Add(current.Name); - } + result.Add(current.Name); + } - // We walked upwards to get the name segments. So reverse teh order here so it goes from outer-most to - // inner-most names. - result.ReverseContents(); - return result.ToImmutableAndClear(); - }); - } + // We walked upwards to get the name segments. So reverse teh order here so it goes from outer-most to + // inner-most names. + result.ReverseContents(); + return result.ToImmutableAndClear(); + }); } + } - private static bool SymbolCandidatesContainsSupportedSymbols(SymbolInfo symbolInfo) - => symbolInfo.CandidateReason == CandidateReason.Ambiguous && - // Arity: Aliases can only name closed constructed types. (See also proposal https://github.com/dotnet/csharplang/issues/1239) - // Aliasing as a closed constructed type is possible but would require to remove the type arguments from the diagnosed node. - // It is unlikely that the user wants that and so generic types are not supported. - symbolInfo.CandidateSymbols.All(symbol => symbol.IsKind(SymbolKind.NamedType) && - symbol.GetArity() == 0); + private static bool SymbolCandidatesContainsSupportedSymbols(SymbolInfo symbolInfo) + => symbolInfo.CandidateReason == CandidateReason.Ambiguous && + // Arity: Aliases can only name closed constructed types. (See also proposal https://github.com/dotnet/csharplang/issues/1239) + // Aliasing as a closed constructed type is possible but would require to remove the type arguments from the diagnosed node. + // It is unlikely that the user wants that and so generic types are not supported. + symbolInfo.CandidateSymbols.All(symbol => symbol.IsKind(SymbolKind.NamedType) && + symbol.GetArity() == 0); - private sealed class SortSystemFirstComparer : IComparer - { - public static readonly IComparer Instance = new SortSystemFirstComparer(); + private sealed class SortSystemFirstComparer : IComparer + { + public static readonly IComparer Instance = new SortSystemFirstComparer(); - public int Compare(string? x, string? y) - { - var xIsSystem = x == nameof(System); - var yIsSystem = y == nameof(System); + public int Compare(string? x, string? y) + { + var xIsSystem = x == nameof(System); + var yIsSystem = y == nameof(System); - if (xIsSystem && yIsSystem) - return 0; + if (xIsSystem && yIsSystem) + return 0; - if (xIsSystem) - return -1; + if (xIsSystem) + return -1; - if (yIsSystem) - return 1; + if (yIsSystem) + return 1; - return StringComparer.Ordinal.Compare(x, y); - } + return StringComparer.Ordinal.Compare(x, y); } } } diff --git a/src/Analyzers/Core/CodeFixes/ConflictMarkerResolution/AbstractConflictMarkerCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/ConflictMarkerResolution/AbstractConflictMarkerCodeFixProvider.cs index 5451f95435cec..7cc576637c8ec 100644 --- a/src/Analyzers/Core/CodeFixes/ConflictMarkerResolution/AbstractConflictMarkerCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/ConflictMarkerResolution/AbstractConflictMarkerCodeFixProvider.cs @@ -15,439 +15,438 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ConflictMarkerResolution +namespace Microsoft.CodeAnalysis.ConflictMarkerResolution; + +/// +/// This code fixer helps remove version conflict markers in code by offering the choice +/// of which version to keep and which version to discard. +/// +/// Conflict markers come in two flavors, diff3 and diff formats. +/// +/// diff3 has a start marker, followed by a first middle markers and a second middle marker, and terminate with an end marker. +/// The disabled text between the first and second middle markers is the baseline for the three-way diff. +/// The fixer always discards this baseline text. +/// +/// diff has a start marker, followed by a middle marker, and terminates with an end marker. +/// We treat the middle marker as both the first and second middle markers (degenerate case with no baseline). +/// +internal abstract partial class AbstractResolveConflictMarkerCodeFixProvider : CodeFixProvider { - /// - /// This code fixer helps remove version conflict markers in code by offering the choice - /// of which version to keep and which version to discard. - /// - /// Conflict markers come in two flavors, diff3 and diff formats. - /// - /// diff3 has a start marker, followed by a first middle markers and a second middle marker, and terminate with an end marker. - /// The disabled text between the first and second middle markers is the baseline for the three-way diff. - /// The fixer always discards this baseline text. - /// - /// diff has a start marker, followed by a middle marker, and terminates with an end marker. - /// We treat the middle marker as both the first and second middle markers (degenerate case with no baseline). - /// - internal abstract partial class AbstractResolveConflictMarkerCodeFixProvider : CodeFixProvider - { - internal const string TakeTopEquivalenceKey = nameof(TakeTopEquivalenceKey); - internal const string TakeBottomEquivalenceKey = nameof(TakeBottomEquivalenceKey); - internal const string TakeBothEquivalenceKey = nameof(TakeBothEquivalenceKey); + internal const string TakeTopEquivalenceKey = nameof(TakeTopEquivalenceKey); + internal const string TakeBottomEquivalenceKey = nameof(TakeBottomEquivalenceKey); + internal const string TakeBothEquivalenceKey = nameof(TakeBothEquivalenceKey); - private static readonly int s_mergeConflictLength = "<<<<<<<".Length; + private static readonly int s_mergeConflictLength = "<<<<<<<".Length; - private readonly ISyntaxKinds _syntaxKinds; + private readonly ISyntaxKinds _syntaxKinds; - protected AbstractResolveConflictMarkerCodeFixProvider( - ISyntaxKinds syntaxKinds, string diagnosticId) - { - FixableDiagnosticIds = [diagnosticId]; - _syntaxKinds = syntaxKinds; + protected AbstractResolveConflictMarkerCodeFixProvider( + ISyntaxKinds syntaxKinds, string diagnosticId) + { + FixableDiagnosticIds = [diagnosticId]; + _syntaxKinds = syntaxKinds; #if !CODE_STYLE - // Backdoor that allows this provider to use the high-priority bucket. - this.CustomTags = this.CustomTags.Add(CodeAction.CanBeHighPriorityTag); + // Backdoor that allows this provider to use the high-priority bucket. + this.CustomTags = this.CustomTags.Add(CodeAction.CanBeHighPriorityTag); #endif - } + } + + public override ImmutableArray FixableDiagnosticIds { get; } - public override ImmutableArray FixableDiagnosticIds { get; } + /// + /// 'Fix merge conflict markers' gets special privileges. A core user scenario around them is that a user does + /// a source control merge, gets conflicts, and then wants to open and edit them in the IDE very quickly. + /// Forcing their fixes to be gated behind the set of normal fixes (which also involves semantic analysis) just + /// slows the user down. As we can compute this syntactically, and the user is almost certainly trying to fix + /// them if they bring up the lightbulb on a <<<<<<< line, it should run ahead of + /// normal fix providers else so the user can quickly fix the conflict and move onto the next conflict. + /// + protected override CodeActionRequestPriority ComputeRequestPriority() + => CodeActionRequestPriority.High; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var cancellationToken = context.CancellationToken; + var document = context.Document; + + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - /// - /// 'Fix merge conflict markers' gets special privileges. A core user scenario around them is that a user does - /// a source control merge, gets conflicts, and then wants to open and edit them in the IDE very quickly. - /// Forcing their fixes to be gated behind the set of normal fixes (which also involves semantic analysis) just - /// slows the user down. As we can compute this syntactically, and the user is almost certainly trying to fix - /// them if they bring up the lightbulb on a <<<<<<< line, it should run ahead of - /// normal fix providers else so the user can quickly fix the conflict and move onto the next conflict. - /// - protected override CodeActionRequestPriority ComputeRequestPriority() - => CodeActionRequestPriority.High; + var position = context.Span.Start; + if (!ShouldFix(root, text, position, out var startLine, out var firstMiddleLine, out var secondMiddleLine, out var endLine)) + return; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + RegisterCodeFixes(context, startLine, firstMiddleLine, secondMiddleLine, endLine); + } + + private bool ShouldFix( + SyntaxNode root, SourceText text, int position, + out TextLine startLine, out TextLine firstMiddleLine, out TextLine secondMiddleLine, out TextLine endLine) + { + startLine = default; + firstMiddleLine = default; + secondMiddleLine = default; + endLine = default; + + var lines = text.Lines; + var conflictLine = lines.GetLineFromPosition(position); + if (position != conflictLine.Start) { - var cancellationToken = context.CancellationToken; - var document = context.Document; + Debug.Assert(false, "All conflict markers should start at the beginning of a line."); + return false; + } - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (!TryGetConflictLines(text, position, out startLine, out firstMiddleLine, out secondMiddleLine, out endLine)) + return false; - var position = context.Span.Start; - if (!ShouldFix(root, text, position, out var startLine, out var firstMiddleLine, out var secondMiddleLine, out var endLine)) - return; + var startTrivia = root.FindTrivia(startLine.Start); + var firstMiddleTrivia = root.FindTrivia(firstMiddleLine.Start); + var secondMiddleTrivia = root.FindTrivia(secondMiddleLine.Start); - RegisterCodeFixes(context, startLine, firstMiddleLine, secondMiddleLine, endLine); + if (position == firstMiddleLine.Start) + { + // We were on the ||||||| line. + // We don't want to report here if there was conflict trivia on the <<<<<<< line (since we would have already reported the issue there). + if (startTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) + return false; } + else if (position == secondMiddleLine.Start) + { + // We were on the ======= line. + // We don't want to report here if there was conflict trivia on the <<<<<<< line (since we would have already reported the issue there). + if (startTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) + return false; - private bool ShouldFix( - SyntaxNode root, SourceText text, int position, - out TextLine startLine, out TextLine firstMiddleLine, out TextLine secondMiddleLine, out TextLine endLine) + // We don't want to report here if there was conflict trivia on the ||||||| line (since we would have already reported the issue there). + if (firstMiddleLine != secondMiddleLine && firstMiddleTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) + return false; + } + else if (position == endLine.Start) { - startLine = default; - firstMiddleLine = default; - secondMiddleLine = default; - endLine = default; - - var lines = text.Lines; - var conflictLine = lines.GetLineFromPosition(position); - if (position != conflictLine.Start) - { - Debug.Assert(false, "All conflict markers should start at the beginning of a line."); + // We were on the >>>>>>> line. + // We don't want to report here if there was conflict trivia on the <<<<<<< line (since we would have already reported the issue there). + if (startTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) return false; - } - if (!TryGetConflictLines(text, position, out startLine, out firstMiddleLine, out secondMiddleLine, out endLine)) + // We don't want to report here if there was conflict trivia on the ||||||| line (since we would have already reported the issue there). + if (firstMiddleLine != secondMiddleLine && firstMiddleTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) return false; - var startTrivia = root.FindTrivia(startLine.Start); - var firstMiddleTrivia = root.FindTrivia(firstMiddleLine.Start); - var secondMiddleTrivia = root.FindTrivia(secondMiddleLine.Start); + // We don't want to report here if there was conflict trivia on the ======= line (since we would have already reported the issue there). + if (secondMiddleTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) + return false; + } - if (position == firstMiddleLine.Start) - { - // We were on the ||||||| line. - // We don't want to report here if there was conflict trivia on the <<<<<<< line (since we would have already reported the issue there). - if (startTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) - return false; - } - else if (position == secondMiddleLine.Start) - { - // We were on the ======= line. - // We don't want to report here if there was conflict trivia on the <<<<<<< line (since we would have already reported the issue there). - if (startTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) - return false; + return true; + } + + private static bool TryGetConflictLines( + SourceText text, int position, + out TextLine startLine, out TextLine firstMiddleLine, out TextLine secondMiddleLine, out TextLine endLine) + { + startLine = default; + firstMiddleLine = default; + secondMiddleLine = default; + endLine = default; + + var lines = text.Lines; + bool foundBarLine; + switch (text[position]) + { + case '<': + startLine = lines.GetLineFromPosition(position); + foundBarLine = TryFindLineForwards(startLine, '|', out firstMiddleLine); - // We don't want to report here if there was conflict trivia on the ||||||| line (since we would have already reported the issue there). - if (firstMiddleLine != secondMiddleLine && firstMiddleTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) + if (!TryFindLineForwards(foundBarLine ? firstMiddleLine : startLine, '=', out secondMiddleLine) || + !TryFindLineForwards(secondMiddleLine, '>', out endLine)) + { return false; - } - else if (position == endLine.Start) - { - // We were on the >>>>>>> line. - // We don't want to report here if there was conflict trivia on the <<<<<<< line (since we would have already reported the issue there). - if (startTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) + } + + break; + case '|': + firstMiddleLine = lines.GetLineFromPosition(position); + return TryFindLineBackwards(firstMiddleLine, '<', out startLine) && + TryFindLineForwards(firstMiddleLine, '=', out secondMiddleLine) && + TryFindLineForwards(secondMiddleLine, '>', out endLine); + case '=': + secondMiddleLine = lines.GetLineFromPosition(position); + foundBarLine = TryFindLineBackwards(secondMiddleLine, '|', out firstMiddleLine); + + if (!TryFindLineBackwards(foundBarLine ? firstMiddleLine : secondMiddleLine, '<', out startLine) || + !TryFindLineForwards(secondMiddleLine, '>', out endLine)) + { return false; + } - // We don't want to report here if there was conflict trivia on the ||||||| line (since we would have already reported the issue there). - if (firstMiddleLine != secondMiddleLine && firstMiddleTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) + break; + case '>': + endLine = lines.GetLineFromPosition(position); + if (!TryFindLineBackwards(endLine, '=', out secondMiddleLine)) + { return false; + } + + foundBarLine = TryFindLineBackwards(secondMiddleLine, '|', out firstMiddleLine); - // We don't want to report here if there was conflict trivia on the ======= line (since we would have already reported the issue there). - if (secondMiddleTrivia.RawKind == _syntaxKinds.ConflictMarkerTrivia) + if (!TryFindLineBackwards(foundBarLine ? firstMiddleLine : secondMiddleLine, '<', out startLine)) return false; - } - return true; + break; + default: + throw ExceptionUtilities.UnexpectedValue(text[position]); } - private static bool TryGetConflictLines( - SourceText text, int position, - out TextLine startLine, out TextLine firstMiddleLine, out TextLine secondMiddleLine, out TextLine endLine) - { - startLine = default; - firstMiddleLine = default; - secondMiddleLine = default; - endLine = default; - - var lines = text.Lines; - bool foundBarLine; - switch (text[position]) - { - case '<': - startLine = lines.GetLineFromPosition(position); - foundBarLine = TryFindLineForwards(startLine, '|', out firstMiddleLine); - - if (!TryFindLineForwards(foundBarLine ? firstMiddleLine : startLine, '=', out secondMiddleLine) || - !TryFindLineForwards(secondMiddleLine, '>', out endLine)) - { - return false; - } - - break; - case '|': - firstMiddleLine = lines.GetLineFromPosition(position); - return TryFindLineBackwards(firstMiddleLine, '<', out startLine) && - TryFindLineForwards(firstMiddleLine, '=', out secondMiddleLine) && - TryFindLineForwards(secondMiddleLine, '>', out endLine); - case '=': - secondMiddleLine = lines.GetLineFromPosition(position); - foundBarLine = TryFindLineBackwards(secondMiddleLine, '|', out firstMiddleLine); - - if (!TryFindLineBackwards(foundBarLine ? firstMiddleLine : secondMiddleLine, '<', out startLine) || - !TryFindLineForwards(secondMiddleLine, '>', out endLine)) - { - return false; - } - - break; - case '>': - endLine = lines.GetLineFromPosition(position); - if (!TryFindLineBackwards(endLine, '=', out secondMiddleLine)) - { - return false; - } - - foundBarLine = TryFindLineBackwards(secondMiddleLine, '|', out firstMiddleLine); - - if (!TryFindLineBackwards(foundBarLine ? firstMiddleLine : secondMiddleLine, '<', out startLine)) - return false; - - break; - default: - throw ExceptionUtilities.UnexpectedValue(text[position]); - } - - if (!foundBarLine) - firstMiddleLine = secondMiddleLine; + if (!foundBarLine) + firstMiddleLine = secondMiddleLine; - return true; - } + return true; + } - private static bool TryFindLineForwards(TextLine startLine, char ch, out TextLine foundLine) + private static bool TryFindLineForwards(TextLine startLine, char ch, out TextLine foundLine) + { + var text = startLine.Text!; + var lines = text.Lines; + for (var i = startLine.LineNumber + 1; i < lines.Count; i++) { - var text = startLine.Text!; - var lines = text.Lines; - for (var i = startLine.LineNumber + 1; i < lines.Count; i++) + var currentLine = lines[i]; + if (IsConflictMarker(currentLine, ch)) { - var currentLine = lines[i]; - if (IsConflictMarker(currentLine, ch)) - { - foundLine = currentLine; - return true; - } + foundLine = currentLine; + return true; } - - foundLine = default; - return false; } - private static bool TryFindLineBackwards(TextLine startLine, char ch, out TextLine foundLine) + foundLine = default; + return false; + } + + private static bool TryFindLineBackwards(TextLine startLine, char ch, out TextLine foundLine) + { + var text = startLine.Text!; + var lines = text.Lines; + for (var i = startLine.LineNumber - 1; i >= 0; i--) { - var text = startLine.Text!; - var lines = text.Lines; - for (var i = startLine.LineNumber - 1; i >= 0; i--) + var currentLine = lines[i]; + if (IsConflictMarker(currentLine, ch)) { - var currentLine = lines[i]; - if (IsConflictMarker(currentLine, ch)) - { - foundLine = currentLine; - return true; - } + foundLine = currentLine; + return true; } + } - foundLine = default; + foundLine = default; + return false; + } + + private static bool IsConflictMarker(TextLine currentLine, char ch) + { + var text = currentLine.Text!; + var currentLineStart = currentLine.Start; + var currentLineLength = currentLine.End - currentLine.Start; + if (currentLineLength < s_mergeConflictLength) + { return false; } - private static bool IsConflictMarker(TextLine currentLine, char ch) + for (var j = 0; j < s_mergeConflictLength; j++) { - var text = currentLine.Text!; - var currentLineStart = currentLine.Start; - var currentLineLength = currentLine.End - currentLine.Start; - if (currentLineLength < s_mergeConflictLength) + if (text[currentLineStart + j] != ch) { return false; } - - for (var j = 0; j < s_mergeConflictLength; j++) - { - if (text[currentLineStart + j] != ch) - { - return false; - } - } - - return true; } - private static void RegisterCodeFixes( - CodeFixContext context, TextLine startLine, TextLine firstMiddleLine, TextLine secondMiddleLine, TextLine endLine) + return true; + } + + private static void RegisterCodeFixes( + CodeFixContext context, TextLine startLine, TextLine firstMiddleLine, TextLine secondMiddleLine, TextLine endLine) + { + var document = context.Document; + + var topText = startLine.ToString()[s_mergeConflictLength..].Trim(); + var takeTopText = string.IsNullOrWhiteSpace(topText) + ? CodeFixesResources.Take_top + : string.Format(CodeFixesResources.Take_0, topText); + + var bottomText = endLine.ToString()[s_mergeConflictLength..].Trim(); + var takeBottomText = string.IsNullOrWhiteSpace(bottomText) + ? CodeFixesResources.Take_bottom + : string.Format(CodeFixesResources.Take_0, bottomText); + + var startPos = startLine.Start; + var firstMiddlePos = firstMiddleLine.Start; + var secondMiddlePos = secondMiddleLine.Start; + var endPos = endLine.Start; + + context.RegisterCodeFix( + CreateCodeAction(takeTopText, + c => TakeTopAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, c), + TakeTopEquivalenceKey), + context.Diagnostics); + context.RegisterCodeFix( + CreateCodeAction(takeBottomText, + c => TakeBottomAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, c), + TakeBottomEquivalenceKey), + context.Diagnostics); + context.RegisterCodeFix( + CreateCodeAction(CodeFixesResources.Take_both, + c => TakeBothAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, c), + TakeBothEquivalenceKey), + context.Diagnostics); + + static CodeAction CreateCodeAction(string title, Func> action, string equivalenceKey) { - var document = context.Document; + var codeAction = CodeAction.Create(title, action, equivalenceKey, CodeActionPriority.High); + +#if !CODE_STYLE + // Backdoor that allows this provider to use the high-priority bucket. + codeAction.CustomTags = codeAction.CustomTags.Add(CodeAction.CanBeHighPriorityTag); +#endif - var topText = startLine.ToString()[s_mergeConflictLength..].Trim(); - var takeTopText = string.IsNullOrWhiteSpace(topText) - ? CodeFixesResources.Take_top - : string.Format(CodeFixesResources.Take_0, topText); + return codeAction; + } + } - var bottomText = endLine.ToString()[s_mergeConflictLength..].Trim(); - var takeBottomText = string.IsNullOrWhiteSpace(bottomText) - ? CodeFixesResources.Take_bottom - : string.Format(CodeFixesResources.Take_0, bottomText); + private static async Task AddEditsAsync( + Document document, int startPos, int firstMiddlePos, int secondMiddlePos, int endPos, + Action, int, int, int, int> addEdits, + CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var startPos = startLine.Start; - var firstMiddlePos = firstMiddleLine.Start; - var secondMiddlePos = secondMiddleLine.Start; - var endPos = endLine.Start; + using var _ = ArrayBuilder.GetInstance(out var edits); + addEdits(text, edits, startPos, firstMiddlePos, secondMiddlePos, endPos); - context.RegisterCodeFix( - CreateCodeAction(takeTopText, - c => TakeTopAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, c), - TakeTopEquivalenceKey), - context.Diagnostics); - context.RegisterCodeFix( - CreateCodeAction(takeBottomText, - c => TakeBottomAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, c), - TakeBottomEquivalenceKey), - context.Diagnostics); - context.RegisterCodeFix( - CreateCodeAction(CodeFixesResources.Take_both, - c => TakeBothAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, c), - TakeBothEquivalenceKey), - context.Diagnostics); - - static CodeAction CreateCodeAction(string title, Func> action, string equivalenceKey) - { - var codeAction = CodeAction.Create(title, action, equivalenceKey, CodeActionPriority.High); + var finalText = text.WithChanges(edits); + return document.WithText(finalText); + } -#if !CODE_STYLE - // Backdoor that allows this provider to use the high-priority bucket. - codeAction.CustomTags = codeAction.CustomTags.Add(CodeAction.CanBeHighPriorityTag); -#endif + private static void AddTopEdits( + SourceText text, ArrayBuilder edits, + int startPos, int firstMiddlePos, int secondMiddlePos, int endPos) + { + // Delete the line containing <<<<<<< + var startEnd = GetEndIncludingLineBreak(text, startPos); + edits.Add(new TextChange(TextSpan.FromBounds(startPos, startEnd), "")); - return codeAction; - } - } + // Remove the chunk of text (inclusive) from ||||||| or ======= through >>>>>>> + var bottomEnd = GetEndIncludingLineBreak(text, endPos); + edits.Add(new TextChange(TextSpan.FromBounds(firstMiddlePos, bottomEnd), "")); + } - private static async Task AddEditsAsync( - Document document, int startPos, int firstMiddlePos, int secondMiddlePos, int endPos, - Action, int, int, int, int> addEdits, - CancellationToken cancellationToken) - { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + private static void AddBottomEdits( + SourceText text, ArrayBuilder edits, + int startPos, int firstMiddlePos, int secondMiddlePos, int endPos) + { + // Remove the chunk of text (inclusive) from <<<<<<< through ======= + var equalsEnd = GetEndIncludingLineBreak(text, secondMiddlePos); + edits.Add(new TextChange(TextSpan.FromBounds(startPos, equalsEnd), "")); - using var _ = ArrayBuilder.GetInstance(out var edits); - addEdits(text, edits, startPos, firstMiddlePos, secondMiddlePos, endPos); + // Delete the line containing >>>>>>> + var bottomEnd = GetEndIncludingLineBreak(text, endPos); + edits.Add(new TextChange(TextSpan.FromBounds(endPos, bottomEnd), "")); + } - var finalText = text.WithChanges(edits); - return document.WithText(finalText); - } + private static void AddBothEdits( + SourceText text, ArrayBuilder edits, + int startPos, int firstMiddlePos, int secondMiddlePos, int endPos) + { + // Delete the line containing <<<<<<< + var startEnd = GetEndIncludingLineBreak(text, startPos); + edits.Add(new TextChange(TextSpan.FromBounds(startPos, startEnd), "")); - private static void AddTopEdits( - SourceText text, ArrayBuilder edits, - int startPos, int firstMiddlePos, int secondMiddlePos, int endPos) + if (firstMiddlePos == secondMiddlePos) { - // Delete the line containing <<<<<<< - var startEnd = GetEndIncludingLineBreak(text, startPos); - edits.Add(new TextChange(TextSpan.FromBounds(startPos, startEnd), "")); - - // Remove the chunk of text (inclusive) from ||||||| or ======= through >>>>>>> - var bottomEnd = GetEndIncludingLineBreak(text, endPos); - edits.Add(new TextChange(TextSpan.FromBounds(firstMiddlePos, bottomEnd), "")); + // Delete the line containing ======= + var equalsEnd = GetEndIncludingLineBreak(text, secondMiddlePos); + edits.Add(new TextChange(TextSpan.FromBounds(secondMiddlePos, equalsEnd), "")); } - - private static void AddBottomEdits( - SourceText text, ArrayBuilder edits, - int startPos, int firstMiddlePos, int secondMiddlePos, int endPos) + else { - // Remove the chunk of text (inclusive) from <<<<<<< through ======= + // Remove the chunk of text (inclusive) from ||||||| through ======= var equalsEnd = GetEndIncludingLineBreak(text, secondMiddlePos); - edits.Add(new TextChange(TextSpan.FromBounds(startPos, equalsEnd), "")); - - // Delete the line containing >>>>>>> - var bottomEnd = GetEndIncludingLineBreak(text, endPos); - edits.Add(new TextChange(TextSpan.FromBounds(endPos, bottomEnd), "")); + edits.Add(new TextChange(TextSpan.FromBounds(firstMiddlePos, equalsEnd), "")); } - private static void AddBothEdits( - SourceText text, ArrayBuilder edits, - int startPos, int firstMiddlePos, int secondMiddlePos, int endPos) - { - // Delete the line containing <<<<<<< - var startEnd = GetEndIncludingLineBreak(text, startPos); - edits.Add(new TextChange(TextSpan.FromBounds(startPos, startEnd), "")); + // Delete the line containing >>>>>>> + var bottomEnd = GetEndIncludingLineBreak(text, endPos); + edits.Add(new TextChange(TextSpan.FromBounds(endPos, bottomEnd), "")); + } - if (firstMiddlePos == secondMiddlePos) - { - // Delete the line containing ======= - var equalsEnd = GetEndIncludingLineBreak(text, secondMiddlePos); - edits.Add(new TextChange(TextSpan.FromBounds(secondMiddlePos, equalsEnd), "")); - } - else - { - // Remove the chunk of text (inclusive) from ||||||| through ======= - var equalsEnd = GetEndIncludingLineBreak(text, secondMiddlePos); - edits.Add(new TextChange(TextSpan.FromBounds(firstMiddlePos, equalsEnd), "")); - } + private static Task TakeTopAsync(Document document, int startPos, int firstMiddlePos, int secondMiddlePos, int endPos, CancellationToken cancellationToken) + => AddEditsAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, AddTopEdits, cancellationToken); - // Delete the line containing >>>>>>> - var bottomEnd = GetEndIncludingLineBreak(text, endPos); - edits.Add(new TextChange(TextSpan.FromBounds(endPos, bottomEnd), "")); - } + private static Task TakeBottomAsync(Document document, int startPos, int firstMiddlePos, int secondMiddlePos, int endPos, CancellationToken cancellationToken) + => AddEditsAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, AddBottomEdits, cancellationToken); - private static Task TakeTopAsync(Document document, int startPos, int firstMiddlePos, int secondMiddlePos, int endPos, CancellationToken cancellationToken) - => AddEditsAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, AddTopEdits, cancellationToken); + private static Task TakeBothAsync(Document document, int startPos, int firstMiddlePos, int secondMiddlePos, int endPos, CancellationToken cancellationToken) + => AddEditsAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, AddBothEdits, cancellationToken); - private static Task TakeBottomAsync(Document document, int startPos, int firstMiddlePos, int secondMiddlePos, int endPos, CancellationToken cancellationToken) - => AddEditsAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, AddBottomEdits, cancellationToken); + private static int GetEndIncludingLineBreak(SourceText text, int position) + => text.Lines.GetLineFromPosition(position).SpanIncludingLineBreak.End; - private static Task TakeBothAsync(Document document, int startPos, int firstMiddlePos, int secondMiddlePos, int endPos, CancellationToken cancellationToken) - => AddEditsAsync(document, startPos, firstMiddlePos, secondMiddlePos, endPos, AddBothEdits, cancellationToken); + private async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + string? equivalenceKey, CancellationToken cancellationToken) + { + Debug.Assert( + equivalenceKey is TakeTopEquivalenceKey or + TakeBottomEquivalenceKey or + TakeBothEquivalenceKey); - private static int GetEndIncludingLineBreak(SourceText text, int position) - => text.Lines.GetLineFromPosition(position).SpanIncludingLineBreak.End; + // Process diagnostics in order so we produce edits in the right order. + var orderedDiagnostics = diagnostics.OrderBy( + (d1, d2) => d1.Location.SourceSpan.Start - d2.Location.SourceSpan.Start).ToImmutableArray(); - private async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - string? equivalenceKey, CancellationToken cancellationToken) - { - Debug.Assert( - equivalenceKey is TakeTopEquivalenceKey or - TakeBottomEquivalenceKey or - TakeBothEquivalenceKey); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // Process diagnostics in order so we produce edits in the right order. - var orderedDiagnostics = diagnostics.OrderBy( - (d1, d2) => d1.Location.SourceSpan.Start - d2.Location.SourceSpan.Start).ToImmutableArray(); + // Create a single array of edits to apply. Then walk over all the + // conflict-marker-regions we want to fix and add the edits for each + // region into that array. Then apply the array just once to get the + // final document. + using var _ = ArrayBuilder.GetInstance(out var edits); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + foreach (var diagnostic in diagnostics) + { + var position = diagnostic.Location.SourceSpan.Start; + if (!ShouldFix(root, text, position, out var startLine, out var firstMiddleLine, out var secondMiddleLine, out var endLine)) + continue; - // Create a single array of edits to apply. Then walk over all the - // conflict-marker-regions we want to fix and add the edits for each - // region into that array. Then apply the array just once to get the - // final document. - using var _ = ArrayBuilder.GetInstance(out var edits); + var startPos = startLine.Start; + var firstMiddlePos = firstMiddleLine.Start; + var secondMiddlePos = secondMiddleLine.Start; + var endPos = endLine.Start; - foreach (var diagnostic in diagnostics) + switch (equivalenceKey) { - var position = diagnostic.Location.SourceSpan.Start; - if (!ShouldFix(root, text, position, out var startLine, out var firstMiddleLine, out var secondMiddleLine, out var endLine)) + case TakeTopEquivalenceKey: + AddTopEdits(text, edits, startPos, firstMiddlePos, secondMiddlePos, endPos); continue; - var startPos = startLine.Start; - var firstMiddlePos = firstMiddleLine.Start; - var secondMiddlePos = secondMiddleLine.Start; - var endPos = endLine.Start; - - switch (equivalenceKey) - { - case TakeTopEquivalenceKey: - AddTopEdits(text, edits, startPos, firstMiddlePos, secondMiddlePos, endPos); - continue; - - case TakeBottomEquivalenceKey: - AddBottomEdits(text, edits, startPos, firstMiddlePos, secondMiddlePos, endPos); - continue; + case TakeBottomEquivalenceKey: + AddBottomEdits(text, edits, startPos, firstMiddlePos, secondMiddlePos, endPos); + continue; - case TakeBothEquivalenceKey: - AddBothEdits(text, edits, startPos, firstMiddlePos, secondMiddlePos, endPos); - continue; + case TakeBothEquivalenceKey: + AddBothEdits(text, edits, startPos, firstMiddlePos, secondMiddlePos, endPos); + continue; - default: - throw ExceptionUtilities.UnexpectedValue(equivalenceKey); - } + default: + throw ExceptionUtilities.UnexpectedValue(equivalenceKey); } - - var finalText = text.WithChanges(edits); - var finalDoc = document.WithText(finalText); - - return finalDoc; } - public override FixAllProvider GetFixAllProvider() - => FixAllProvider.Create(async (context, document, diagnostics) => - await this.FixAllAsync(document, diagnostics, context.CodeActionEquivalenceKey, context.CancellationToken).ConfigureAwait(false)); + var finalText = text.WithChanges(edits); + var finalDoc = document.WithText(finalText); + + return finalDoc; } + + public override FixAllProvider GetFixAllProvider() + => FixAllProvider.Create(async (context, document, diagnostics) => + await this.FixAllAsync(document, diagnostics, context.CodeActionEquivalenceKey, context.CancellationToken).ConfigureAwait(false)); } diff --git a/src/Analyzers/Core/CodeFixes/ConvertToAsync/AbstractConvertToAsyncCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/ConvertToAsync/AbstractConvertToAsyncCodeFixProvider.cs index e081ee14a7e36..29d317d48e772 100644 --- a/src/Analyzers/Core/CodeFixes/ConvertToAsync/AbstractConvertToAsyncCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/ConvertToAsync/AbstractConvertToAsyncCodeFixProvider.cs @@ -13,68 +13,67 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ConvertToAsync +namespace Microsoft.CodeAnalysis.ConvertToAsync; + +internal abstract partial class AbstractConvertToAsyncCodeFixProvider : CodeFixProvider { - internal abstract partial class AbstractConvertToAsyncCodeFixProvider : CodeFixProvider + protected abstract Task GetDescriptionAsync(Diagnostic diagnostic, SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken); + protected abstract Task> GetRootInOtherSyntaxTreeAsync(SyntaxNode node, SemanticModel semanticModel, Diagnostic diagnostic, CancellationToken cancellationToken); + + public override FixAllProvider GetFixAllProvider() { - protected abstract Task GetDescriptionAsync(Diagnostic diagnostic, SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken); - protected abstract Task> GetRootInOtherSyntaxTreeAsync(SyntaxNode node, SemanticModel semanticModel, Diagnostic diagnostic, CancellationToken cancellationToken); + // Fix All is not supported by this code fix + // https://github.com/dotnet/roslyn/issues/34463 + return null; + } - public override FixAllProvider GetFixAllProvider() + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (!TryGetNode(root, context.Span, out var node)) { - // Fix All is not supported by this code fix - // https://github.com/dotnet/roslyn/issues/34463 - return null; + return; } - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - if (!TryGetNode(root, context.Span, out var node)) - { - return; - } - - var diagnostic = context.Diagnostics.FirstOrDefault(); + var diagnostic = context.Diagnostics.FirstOrDefault(); - var codeAction = await GetCodeActionAsync( - node, context.Document, diagnostic, context.CancellationToken).ConfigureAwait(false); - if (codeAction != null) - { - context.RegisterCodeFix(codeAction, context.Diagnostics); - } + var codeAction = await GetCodeActionAsync( + node, context.Document, diagnostic, context.CancellationToken).ConfigureAwait(false); + if (codeAction != null) + { + context.RegisterCodeFix(codeAction, context.Diagnostics); } + } - private static bool TryGetNode(SyntaxNode root, TextSpan span, out SyntaxNode node) + private static bool TryGetNode(SyntaxNode root, TextSpan span, out SyntaxNode node) + { + node = null; + var ancestors = root.FindToken(span.Start).GetAncestors(); + if (!ancestors.Any()) { - node = null; - var ancestors = root.FindToken(span.Start).GetAncestors(); - if (!ancestors.Any()) - { - return false; - } - - node = ancestors.FirstOrDefault(n => n.Span.Contains(span) && n != root); - return node != null; + return false; } - private async Task GetCodeActionAsync( - SyntaxNode node, Document document, Diagnostic diagnostic, CancellationToken cancellationToken) - { - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + node = ancestors.FirstOrDefault(n => n.Span.Contains(span) && n != root); + return node != null; + } - var result = await GetRootInOtherSyntaxTreeAsync(node, semanticModel, diagnostic, cancellationToken).ConfigureAwait(false); - if (result == null) - return null; + private async Task GetCodeActionAsync( + SyntaxNode node, Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var syntaxTree = result.Item1; - var newRoot = result.Item2; - var otherDocument = document.Project.Solution.GetDocument(syntaxTree); - var title = await GetDescriptionAsync(diagnostic, node, semanticModel, cancellationToken).ConfigureAwait(false); - return CodeAction.Create( - title, - token => Task.FromResult(otherDocument.WithSyntaxRoot(newRoot)), - title); - } + var result = await GetRootInOtherSyntaxTreeAsync(node, semanticModel, diagnostic, cancellationToken).ConfigureAwait(false); + if (result == null) + return null; + + var syntaxTree = result.Item1; + var newRoot = result.Item2; + var otherDocument = document.Project.Solution.GetDocument(syntaxTree); + var title = await GetDescriptionAsync(diagnostic, node, semanticModel, cancellationToken).ConfigureAwait(false); + return CodeAction.Create( + title, + token => Task.FromResult(otherDocument.WithSyntaxRoot(newRoot)), + title); } } diff --git a/src/Analyzers/Core/CodeFixes/ConvertTypeOfToNameOf/AbstractConvertTypeOfToNameOfCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/ConvertTypeOfToNameOf/AbstractConvertTypeOfToNameOfCodeFixProvider.cs index 2eddd2656c54d..66d14ce2db6a4 100644 --- a/src/Analyzers/Core/CodeFixes/ConvertTypeOfToNameOf/AbstractConvertTypeOfToNameOfCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/ConvertTypeOfToNameOf/AbstractConvertTypeOfToNameOfCodeFixProvider.cs @@ -11,48 +11,47 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.ConvertTypeOfToNameOf +namespace Microsoft.CodeAnalysis.ConvertTypeOfToNameOf; + +internal abstract class AbstractConvertTypeOfToNameOfCodeFixProvider< + TMemberAccessExpressionSyntax> : SyntaxEditorBasedCodeFixProvider + where TMemberAccessExpressionSyntax : SyntaxNode { - internal abstract class AbstractConvertTypeOfToNameOfCodeFixProvider< - TMemberAccessExpressionSyntax> : SyntaxEditorBasedCodeFixProvider - where TMemberAccessExpressionSyntax : SyntaxNode - { - protected abstract string GetCodeFixTitle(); + protected abstract string GetCodeFixTitle(); - protected abstract SyntaxNode GetSymbolTypeExpression(SemanticModel model, TMemberAccessExpressionSyntax node, CancellationToken cancellationToken); + protected abstract SyntaxNode GetSymbolTypeExpression(SemanticModel model, TMemberAccessExpressionSyntax node, CancellationToken cancellationToken); - public sealed override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.ConvertTypeOfToNameOfDiagnosticId]; + public sealed override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.ConvertTypeOfToNameOfDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var title = GetCodeFixTitle(); - RegisterCodeFix(context, title, title); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var title = GetCodeFixTitle(); + RegisterCodeFix(context, title, title); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + foreach (var diagnostic in diagnostics) { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - foreach (var diagnostic in diagnostics) - { - if (editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is not TMemberAccessExpressionSyntax node) - continue; - - ConvertTypeOfToNameOf(semanticModel, editor, node, cancellationToken); - } - } + if (editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is not TMemberAccessExpressionSyntax node) + continue; - /// - /// Method converts typeof(...).Name to nameof(...) - /// - public void ConvertTypeOfToNameOf(SemanticModel semanticModel, SyntaxEditor editor, TMemberAccessExpressionSyntax nodeToReplace, CancellationToken cancellationToken) - { - var typeExpression = GetSymbolTypeExpression(semanticModel, nodeToReplace, cancellationToken); - var nameOfSyntax = editor.Generator.NameOfExpression(typeExpression); - editor.ReplaceNode(nodeToReplace, nameOfSyntax); + ConvertTypeOfToNameOf(semanticModel, editor, node, cancellationToken); } } + + /// + /// Method converts typeof(...).Name to nameof(...) + /// + public void ConvertTypeOfToNameOf(SemanticModel semanticModel, SyntaxEditor editor, TMemberAccessExpressionSyntax nodeToReplace, CancellationToken cancellationToken) + { + var typeExpression = GetSymbolTypeExpression(semanticModel, nodeToReplace, cancellationToken); + var nameOfSyntax = editor.Generator.NameOfExpression(typeExpression); + editor.ReplaceNode(nodeToReplace, nameOfSyntax); + } } diff --git a/src/Analyzers/Core/CodeFixes/DocumentationComments/AbstractAddDocCommentNodesCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/DocumentationComments/AbstractAddDocCommentNodesCodeFixProvider.cs index c0b32e180da5e..0305ba86a6e61 100644 --- a/src/Analyzers/Core/CodeFixes/DocumentationComments/AbstractAddDocCommentNodesCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/DocumentationComments/AbstractAddDocCommentNodesCodeFixProvider.cs @@ -15,160 +15,159 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.DocumentationComments +namespace Microsoft.CodeAnalysis.DocumentationComments; + +internal abstract class AbstractAddDocCommentNodesCodeFixProvider + : CodeFixProvider + where TXmlElementSyntax : SyntaxNode + where TXmlNameAttributeSyntax : SyntaxNode + where TXmlTextSyntax : SyntaxNode + where TMemberDeclarationSyntax : SyntaxNode { - internal abstract class AbstractAddDocCommentNodesCodeFixProvider - : CodeFixProvider - where TXmlElementSyntax : SyntaxNode - where TXmlNameAttributeSyntax : SyntaxNode - where TXmlTextSyntax : SyntaxNode - where TMemberDeclarationSyntax : SyntaxNode + public override FixAllProvider GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { - public override FixAllProvider GetFixAllProvider() - => WellKnownFixAllProviders.BatchFixer; + var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var parentMethod = root.FindNode(context.Span).FirstAncestorOrSelf(); + if (parentMethod is null) + return; + + var docCommentNode = TryGetDocCommentNode(parentMethod.GetLeadingTrivia()); + if (docCommentNode is null) + return; + + context.RegisterCodeFix( + CodeAction.Create( + CodeFixesResources.Add_missing_param_nodes, + cancellationToken => AddParamTagAsync(context.Document, parentMethod, docCommentNode, cancellationToken), + nameof(CodeFixesResources.Add_missing_param_nodes)), + context.Diagnostics); + } - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var parentMethod = root.FindNode(context.Span).FirstAncestorOrSelf(); - if (parentMethod is null) - return; - - var docCommentNode = TryGetDocCommentNode(parentMethod.GetLeadingTrivia()); - if (docCommentNode is null) - return; - - context.RegisterCodeFix( - CodeAction.Create( - CodeFixesResources.Add_missing_param_nodes, - cancellationToken => AddParamTagAsync(context.Document, parentMethod, docCommentNode, cancellationToken), - nameof(CodeFixesResources.Add_missing_param_nodes)), - context.Diagnostics); - } + protected abstract string NodeName { get; } + + protected abstract List GetNameAttributes(TXmlElementSyntax node); + protected abstract string GetValueFromNameAttribute(TXmlNameAttributeSyntax attribute); + protected abstract SyntaxNode? TryGetDocCommentNode(SyntaxTriviaList parameter); + protected abstract string GetXmlElementLocalName(TXmlElementSyntax element); + protected abstract ImmutableArray GetParameterNames(TMemberDeclarationSyntax method); + protected abstract TXmlElementSyntax GetNewNode(string parameterName, bool isFirstNodeInComment); - protected abstract string NodeName { get; } + protected async Task AddParamTagAsync( + Document document, TMemberDeclarationSyntax parentMethod, SyntaxNode docCommentNode, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - protected abstract List GetNameAttributes(TXmlElementSyntax node); - protected abstract string GetValueFromNameAttribute(TXmlNameAttributeSyntax attribute); - protected abstract SyntaxNode? TryGetDocCommentNode(SyntaxTriviaList parameter); - protected abstract string GetXmlElementLocalName(TXmlElementSyntax element); - protected abstract ImmutableArray GetParameterNames(TMemberDeclarationSyntax method); - protected abstract TXmlElementSyntax GetNewNode(string parameterName, bool isFirstNodeInComment); + var newDocComment = docCommentNode; + var parameterNames = GetParameterNames(parentMethod); - protected async Task AddParamTagAsync( - Document document, TMemberDeclarationSyntax parentMethod, SyntaxNode docCommentNode, CancellationToken cancellationToken) + for (var index = 0; index < parameterNames.Length; index++) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var parameterName = parameterNames[index]; + var paramNodes = GetElementNodes(newDocComment, NodeName); + if (NodeExists(paramNodes, parameterName)) + { + continue; + } - var newDocComment = docCommentNode; - var parameterNames = GetParameterNames(parentMethod); + var paramsBeforeCurrentParam = parameterNames.TakeWhile(t => t != parameterName).ToList(); + var paramsAfterCurrentParam = parameterNames.Except(paramsBeforeCurrentParam).ToList(); + paramsAfterCurrentParam.Remove(parameterName); - for (var index = 0; index < parameterNames.Length; index++) + // If the index is not `0`, there is a node before the current one for sure + // If the index is `0`, try to add the node after the `summary` node, + // only if any existing nodes are at the top level--this route will not + // be taken if the existing node is nested in another node + var summaryNode = GetElementNodes(newDocComment, "summary").FirstOrDefault(); + if (index != 0 || (!paramNodes.Any() && summaryNode != null)) { - var parameterName = parameterNames[index]; - var paramNodes = GetElementNodes(newDocComment, NodeName); - if (NodeExists(paramNodes, parameterName)) + // First, try to get the node before the param node so we know where to insert the new node + TXmlElementSyntax? nodeBeforeNewParamNode = null; + if (index > 0) { - continue; + nodeBeforeNewParamNode = GetParamNodeForParamName(paramNodes, parameterNames[index - 1]); } - var paramsBeforeCurrentParam = parameterNames.TakeWhile(t => t != parameterName).ToList(); - var paramsAfterCurrentParam = parameterNames.Except(paramsBeforeCurrentParam).ToList(); - paramsAfterCurrentParam.Remove(parameterName); - - // If the index is not `0`, there is a node before the current one for sure - // If the index is `0`, try to add the node after the `summary` node, - // only if any existing nodes are at the top level--this route will not - // be taken if the existing node is nested in another node - var summaryNode = GetElementNodes(newDocComment, "summary").FirstOrDefault(); - if (index != 0 || (!paramNodes.Any() && summaryNode != null)) - { - // First, try to get the node before the param node so we know where to insert the new node - TXmlElementSyntax? nodeBeforeNewParamNode = null; - if (index > 0) - { - nodeBeforeNewParamNode = GetParamNodeForParamName(paramNodes, parameterNames[index - 1]); - } + // This will be hit in the index is `0`, in which case the previous node is the summary node + nodeBeforeNewParamNode ??= summaryNode; - // This will be hit in the index is `0`, in which case the previous node is the summary node - nodeBeforeNewParamNode ??= summaryNode; + newDocComment = newDocComment.InsertNodesAfter(nodeBeforeNewParamNode!, + new[] { GetNewNode(parameterName, isFirstNodeInComment: false) }); - newDocComment = newDocComment.InsertNodesAfter(nodeBeforeNewParamNode!, - new[] { GetNewNode(parameterName, isFirstNodeInComment: false) }); + continue; + } - continue; - } + // At this point, the node has to go at the beginning of the comment + var nodeAfterNewParamNode = paramNodes.FirstOrDefault() ?? newDocComment.ChildNodes().First(); - // At this point, the node has to go at the beginning of the comment - var nodeAfterNewParamNode = paramNodes.FirstOrDefault() ?? newDocComment.ChildNodes().First(); + // Adjust for doc comment marker before the node + var paramNodeSiblings = nodeAfterNewParamNode.GetRequiredParent().ChildNodes().ToList(); + var indexOfNode = paramNodeSiblings.IndexOf(nodeAfterNewParamNode); - // Adjust for doc comment marker before the node - var paramNodeSiblings = nodeAfterNewParamNode.GetRequiredParent().ChildNodes().ToList(); - var indexOfNode = paramNodeSiblings.IndexOf(nodeAfterNewParamNode); + // set insert node to be the doc comment signifier of the closest param before the new node + if (indexOfNode > 0 && paramNodeSiblings[indexOfNode - 1] is TXmlTextSyntax previousSibling) + nodeAfterNewParamNode = previousSibling; - // set insert node to be the doc comment signifier of the closest param before the new node - if (indexOfNode > 0 && paramNodeSiblings[indexOfNode - 1] is TXmlTextSyntax previousSibling) - nodeAfterNewParamNode = previousSibling; + var newNodeList = new[] + { + // the last value will almost always be true, unless the node is embedded in another doc comment node + GetNewNode(parameterName, nodeAfterNewParamNode == newDocComment.ChildNodes().First()) + }; + newDocComment = newDocComment.InsertNodesBefore(nodeAfterNewParamNode, newNodeList); + } - var newNodeList = new[] - { - // the last value will almost always be true, unless the node is embedded in another doc comment node - GetNewNode(parameterName, nodeAfterNewParamNode == newDocComment.ChildNodes().First()) - }; - newDocComment = newDocComment.InsertNodesBefore(nodeAfterNewParamNode, newNodeList); - } + var newRoot = root.ReplaceNode(docCommentNode, newDocComment.WithAdditionalAnnotations(Formatter.Annotation)); + return document.WithSyntaxRoot(newRoot); + } - var newRoot = root.ReplaceNode(docCommentNode, newDocComment.WithAdditionalAnnotations(Formatter.Annotation)); - return document.WithSyntaxRoot(newRoot); - } + private List GetElementNodes(SyntaxNode docComment, string nodeName) + { + var nodes = docComment.ChildNodes().OfType() + .Where(w => GetXmlElementLocalName(w) == nodeName) + .ToList(); - private List GetElementNodes(SyntaxNode docComment, string nodeName) + // Prefer to return element nodes that are the top-level children of the DocComment. + // If we don't find any, then fallback to the first element node at any depth with the requested name. + if (!nodes.Any()) { - var nodes = docComment.ChildNodes().OfType() - .Where(w => GetXmlElementLocalName(w) == nodeName) - .ToList(); + nodes = docComment.DescendantNodes(descendIntoChildren: _ => true) + .OfType() + .Where(w => GetXmlElementLocalName(w) == nodeName) + .ToList(); + } - // Prefer to return element nodes that are the top-level children of the DocComment. - // If we don't find any, then fallback to the first element node at any depth with the requested name. - if (!nodes.Any()) - { - nodes = docComment.DescendantNodes(descendIntoChildren: _ => true) - .OfType() - .Where(w => GetXmlElementLocalName(w) == nodeName) - .ToList(); - } + return nodes; + } - return nodes; - } + private bool NodeExists(IEnumerable paramNodes, string name) + { + return paramNodes.Select(GetNameAttributes) + .Where(nameAttributes => nameAttributes.Count == 1) + .Any(nameAttributes => nameAttributes.Select(GetValueFromNameAttribute).Contains(name)); + } - private bool NodeExists(IEnumerable paramNodes, string name) + protected TXmlElementSyntax? GetParamNodeForParamName( + IEnumerable paramNodeList, + string name) + { + foreach (var paramNode in paramNodeList) { - return paramNodes.Select(GetNameAttributes) - .Where(nameAttributes => nameAttributes.Count == 1) - .Any(nameAttributes => nameAttributes.Select(GetValueFromNameAttribute).Contains(name)); - } + var paramNameAttributesForNode = GetNameAttributes(paramNode); - protected TXmlElementSyntax? GetParamNodeForParamName( - IEnumerable paramNodeList, - string name) - { - foreach (var paramNode in paramNodeList) + // param node is missing `name` attribute or there are multiple `name` attributes + if (paramNameAttributesForNode.Count != 1) { - var paramNameAttributesForNode = GetNameAttributes(paramNode); - - // param node is missing `name` attribute or there are multiple `name` attributes - if (paramNameAttributesForNode.Count != 1) - { - continue; - } - - if (GetValueFromNameAttribute(paramNameAttributesForNode.Single()) == name) - { - return paramNode; - } + continue; } - return null; + if (GetValueFromNameAttribute(paramNameAttributesForNode.Single()) == name) + { + return paramNode; + } } + + return null; } } diff --git a/src/Analyzers/Core/CodeFixes/DocumentationComments/AbstractRemoveDocCommentNodeCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/DocumentationComments/AbstractRemoveDocCommentNodeCodeFixProvider.cs index 7bc553d97a63a..ba25d7971bd2c 100644 --- a/src/Analyzers/Core/CodeFixes/DocumentationComments/AbstractRemoveDocCommentNodeCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/DocumentationComments/AbstractRemoveDocCommentNodeCodeFixProvider.cs @@ -13,115 +13,114 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.DocumentationComments +namespace Microsoft.CodeAnalysis.DocumentationComments; + +internal abstract class AbstractRemoveDocCommentNodeCodeFixProvider : CodeFixProvider + where TXmlElementSyntax : SyntaxNode + where TXmlTextSyntax : SyntaxNode { - internal abstract class AbstractRemoveDocCommentNodeCodeFixProvider : CodeFixProvider - where TXmlElementSyntax : SyntaxNode - where TXmlTextSyntax : SyntaxNode - { - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - public abstract override ImmutableArray FixableDiagnosticIds { get; } + public abstract override ImmutableArray FixableDiagnosticIds { get; } - protected abstract string DocCommentSignifierToken { get; } + protected abstract string DocCommentSignifierToken { get; } - protected abstract SyntaxTriviaList GetRevisedDocCommentTrivia(string docCommentText); + protected abstract SyntaxTriviaList GetRevisedDocCommentTrivia(string docCommentText); - protected abstract SyntaxTokenList GetTextTokens(TXmlTextSyntax xmlText); - protected abstract bool IsXmlNewLineToken(SyntaxToken token); - protected abstract bool IsXmlWhitespaceToken(SyntaxToken token); + protected abstract SyntaxTokenList GetTextTokens(TXmlTextSyntax xmlText); + protected abstract bool IsXmlNewLineToken(SyntaxToken token); + protected abstract bool IsXmlWhitespaceToken(SyntaxToken token); - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var paramNode = GetParamNode(root, context.Span); + if (paramNode != null) { - var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var paramNode = GetParamNode(root, context.Span); - if (paramNode != null) - { - context.RegisterCodeFix( - CodeAction.Create( - CodeFixesResources.Remove_tag, - cancellationToken => RemoveDuplicateParamTagAsync(context.Document, paramNode, cancellationToken), - nameof(CodeFixesResources.Remove_tag)), - context.Diagnostics); - } + context.RegisterCodeFix( + CodeAction.Create( + CodeFixesResources.Remove_tag, + cancellationToken => RemoveDuplicateParamTagAsync(context.Document, paramNode, cancellationToken), + nameof(CodeFixesResources.Remove_tag)), + context.Diagnostics); } + } - private static TXmlElementSyntax? GetParamNode(SyntaxNode root, TextSpan span) - { - // First, we get the node the diagnostic fired on - // Then, we climb the tree to the first parent that is of the type XMLElement - // This is to correctly handle XML nodes that are nested in other XML nodes, so we only - // remove the node the diagnostic fired on and its children, but no parent nodes - var paramNode = root.FindNode(span, findInsideTrivia: true); - return paramNode.FirstAncestorOrSelf(); - } + private static TXmlElementSyntax? GetParamNode(SyntaxNode root, TextSpan span) + { + // First, we get the node the diagnostic fired on + // Then, we climb the tree to the first parent that is of the type XMLElement + // This is to correctly handle XML nodes that are nested in other XML nodes, so we only + // remove the node the diagnostic fired on and its children, but no parent nodes + var paramNode = root.FindNode(span, findInsideTrivia: true); + return paramNode.FirstAncestorOrSelf(); + } - private async Task RemoveDuplicateParamTagAsync( - Document document, TXmlElementSyntax paramNode, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + private async Task RemoveDuplicateParamTagAsync( + Document document, TXmlElementSyntax paramNode, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var removedNodes = new List { paramNode }; - var paramNodeSiblings = paramNode.GetRequiredParent().ChildNodes().ToList(); + var removedNodes = new List { paramNode }; + var paramNodeSiblings = paramNode.GetRequiredParent().ChildNodes().ToList(); - // This should not cause a crash because the diagnostics are only thrown in - // doc comment XML nodes, which, by definition, start with `///` (C#) or `'''` (VB.NET) - // If, perhaps, this specific node is not directly preceded by the comment marker node, - // it will be preceded by another XML node - var paramNodeIndex = paramNodeSiblings.IndexOf(paramNode); + // This should not cause a crash because the diagnostics are only thrown in + // doc comment XML nodes, which, by definition, start with `///` (C#) or `'''` (VB.NET) + // If, perhaps, this specific node is not directly preceded by the comment marker node, + // it will be preceded by another XML node + var paramNodeIndex = paramNodeSiblings.IndexOf(paramNode); - if (ShouldRemovePreviousSibling(paramNodeSiblings, paramNodeIndex)) - { - removedNodes.Add(paramNodeSiblings[paramNodeIndex - 1]); - } - - // Remove all trivia attached to the nodes I am removing. - // Really, any option should work here because the leading/trailing text - // around these nodes are not attached to them as trivia. - var newRoot = root.RemoveNodes(removedNodes, SyntaxRemoveOptions.KeepNoTrivia); - Contract.ThrowIfNull(newRoot); - return document.WithSyntaxRoot(newRoot); + if (ShouldRemovePreviousSibling(paramNodeSiblings, paramNodeIndex)) + { + removedNodes.Add(paramNodeSiblings[paramNodeIndex - 1]); } - private bool ShouldRemovePreviousSibling(List paramNodeSiblings, int paramNodeIndex) + // Remove all trivia attached to the nodes I am removing. + // Really, any option should work here because the leading/trailing text + // around these nodes are not attached to them as trivia. + var newRoot = root.RemoveNodes(removedNodes, SyntaxRemoveOptions.KeepNoTrivia); + Contract.ThrowIfNull(newRoot); + return document.WithSyntaxRoot(newRoot); + } + + private bool ShouldRemovePreviousSibling(List paramNodeSiblings, int paramNodeIndex) + { + if (paramNodeIndex > 0) { - if (paramNodeIndex > 0) - { - var previousNodeTextTrimmed = paramNodeSiblings[paramNodeIndex - 1].ToFullString().Trim(); + var previousNodeTextTrimmed = paramNodeSiblings[paramNodeIndex - 1].ToFullString().Trim(); - if (previousNodeTextTrimmed == string.Empty || - previousNodeTextTrimmed == DocCommentSignifierToken) + if (previousNodeTextTrimmed == string.Empty || + previousNodeTextTrimmed == DocCommentSignifierToken) + { + // Only remove the preceding /// if this param node is also the only thing on this line. + if (paramNodeIndex + 1 < paramNodeSiblings.Count) { - // Only remove the preceding /// if this param node is also the only thing on this line. - if (paramNodeIndex + 1 < paramNodeSiblings.Count) + var nextSibling = paramNodeSiblings[paramNodeIndex + 1]; + if (nextSibling is TXmlTextSyntax textSyntax) { - var nextSibling = paramNodeSiblings[paramNodeIndex + 1]; - if (nextSibling is TXmlTextSyntax textSyntax) + // Walk the next text block forward, making sure we only see whitespace + // until we hit the next newline. If that's all we can remove the preceding + // '///'. Otherwise we'll want to keep it to keep whatever comes after + // this node valid. + foreach (var childToken in GetTextTokens(textSyntax)) { - // Walk the next text block forward, making sure we only see whitespace - // until we hit the next newline. If that's all we can remove the preceding - // '///'. Otherwise we'll want to keep it to keep whatever comes after - // this node valid. - foreach (var childToken in GetTextTokens(textSyntax)) + if (IsXmlWhitespaceToken(childToken)) { - if (IsXmlWhitespaceToken(childToken)) - { - continue; - } - - if (IsXmlNewLineToken(childToken)) - { - return true; - } + continue; + } - return false; + if (IsXmlNewLineToken(childToken)) + { + return true; } + + return false; } } } } - - return false; } + + return false; } } diff --git a/src/Analyzers/Core/CodeFixes/FileHeaders/AbstractFileHeaderCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/FileHeaders/AbstractFileHeaderCodeFixProvider.cs index 9a909ec96d0c3..55068c1eba0c3 100644 --- a/src/Analyzers/Core/CodeFixes/FileHeaders/AbstractFileHeaderCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/FileHeaders/AbstractFileHeaderCodeFixProvider.cs @@ -19,220 +19,219 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.FileHeaders +namespace Microsoft.CodeAnalysis.FileHeaders; + +internal abstract class AbstractFileHeaderCodeFixProvider : CodeFixProvider { - internal abstract class AbstractFileHeaderCodeFixProvider : CodeFixProvider - { - protected abstract AbstractFileHeaderHelper FileHeaderHelper { get; } + protected abstract AbstractFileHeaderHelper FileHeaderHelper { get; } - public override ImmutableArray FixableDiagnosticIds { get; } - = [IDEDiagnosticIds.FileHeaderMismatch]; + public override ImmutableArray FixableDiagnosticIds { get; } + = [IDEDiagnosticIds.FileHeaderMismatch]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics) { - foreach (var diagnostic in context.Diagnostics) - { - context.RegisterCodeFix( - CodeAction.Create( - CodeFixesResources.Add_file_header, - cancellationToken => GetTransformedDocumentAsync(context.Document, context.GetOptionsProvider(), cancellationToken), - nameof(AbstractFileHeaderCodeFixProvider)), - diagnostic); - } - - return Task.CompletedTask; + context.RegisterCodeFix( + CodeAction.Create( + CodeFixesResources.Add_file_header, + cancellationToken => GetTransformedDocumentAsync(context.Document, context.GetOptionsProvider(), cancellationToken), + nameof(AbstractFileHeaderCodeFixProvider)), + diagnostic); } - private async Task GetTransformedDocumentAsync(Document document, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - => document.WithSyntaxRoot(await GetTransformedSyntaxRootAsync(document, fallbackOptions, cancellationToken).ConfigureAwait(false)); + return Task.CompletedTask; + } - private async Task GetTransformedSyntaxRootAsync(Document document, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var generator = document.GetRequiredLanguageService(); - var newLineTrivia = generator.EndOfLine(options.NewLine); + private async Task GetTransformedDocumentAsync(Document document, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + => document.WithSyntaxRoot(await GetTransformedSyntaxRootAsync(document, fallbackOptions, cancellationToken).ConfigureAwait(false)); - return await GetTransformedSyntaxRootAsync(generator.SyntaxFacts, FileHeaderHelper, newLineTrivia, document, fileHeaderTemplate: null, cancellationToken).ConfigureAwait(false); - } + private async Task GetTransformedSyntaxRootAsync(Document document, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var generator = document.GetRequiredLanguageService(); + var newLineTrivia = generator.EndOfLine(options.NewLine); - internal static async Task GetTransformedSyntaxRootAsync(ISyntaxFacts syntaxFacts, AbstractFileHeaderHelper fileHeaderHelper, SyntaxTrivia newLineTrivia, Document document, string? fileHeaderTemplate, CancellationToken cancellationToken) - { - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); + return await GetTransformedSyntaxRootAsync(generator.SyntaxFacts, FileHeaderHelper, newLineTrivia, document, fileHeaderTemplate: null, cancellationToken).ConfigureAwait(false); + } - // If we weren't given a header lets get the one from editorconfig - if (fileHeaderTemplate is null && - !document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree).TryGetEditorConfigOption(CodeStyleOptions2.FileHeaderTemplate, out fileHeaderTemplate)) - { - // No header supplied, no editorconfig setting, nothing to do - return root; - } + internal static async Task GetTransformedSyntaxRootAsync(ISyntaxFacts syntaxFacts, AbstractFileHeaderHelper fileHeaderHelper, SyntaxTrivia newLineTrivia, Document document, string? fileHeaderTemplate, CancellationToken cancellationToken) + { + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - if (RoslynString.IsNullOrEmpty(fileHeaderTemplate)) - { - // Header template is empty, nothing to do. This shouldn't be possible if this method is called in - // reaction to a diagnostic, but this method is also used when creating new documents so lets be defensive. - return root; - } + // If we weren't given a header lets get the one from editorconfig + if (fileHeaderTemplate is null && + !document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree).TryGetEditorConfigOption(CodeStyleOptions2.FileHeaderTemplate, out fileHeaderTemplate)) + { + // No header supplied, no editorconfig setting, nothing to do + return root; + } - var expectedFileHeader = fileHeaderTemplate.Replace("{fileName}", Path.GetFileName(document.FilePath)); + if (RoslynString.IsNullOrEmpty(fileHeaderTemplate)) + { + // Header template is empty, nothing to do. This shouldn't be possible if this method is called in + // reaction to a diagnostic, but this method is also used when creating new documents so lets be defensive. + return root; + } - var fileHeader = fileHeaderHelper.ParseFileHeader(root); - SyntaxNode newSyntaxRoot; - if (fileHeader.IsMissing) - { - newSyntaxRoot = AddHeader(syntaxFacts, fileHeaderHelper, newLineTrivia, root, expectedFileHeader); - } - else - { - newSyntaxRoot = ReplaceHeader(syntaxFacts, fileHeaderHelper, newLineTrivia, root, expectedFileHeader); - } + var expectedFileHeader = fileHeaderTemplate.Replace("{fileName}", Path.GetFileName(document.FilePath)); - return newSyntaxRoot; + var fileHeader = fileHeaderHelper.ParseFileHeader(root); + SyntaxNode newSyntaxRoot; + if (fileHeader.IsMissing) + { + newSyntaxRoot = AddHeader(syntaxFacts, fileHeaderHelper, newLineTrivia, root, expectedFileHeader); } - - private static SyntaxNode ReplaceHeader(ISyntaxFacts syntaxFacts, AbstractFileHeaderHelper fileHeaderHelper, SyntaxTrivia newLineTrivia, SyntaxNode root, string expectedFileHeader) + else { - // Skip single line comments, whitespace, and end of line trivia until a blank line is encountered. - var triviaList = root.GetLeadingTrivia(); + newSyntaxRoot = ReplaceHeader(syntaxFacts, fileHeaderHelper, newLineTrivia, root, expectedFileHeader); + } - // True if the current line is blank so far (empty or whitespace); otherwise, false. The first line is - // assumed to not be blank, which allows the analysis to detect a file header which follows a blank line at - // the top of the file. - var onBlankLine = false; + return newSyntaxRoot; + } + + private static SyntaxNode ReplaceHeader(ISyntaxFacts syntaxFacts, AbstractFileHeaderHelper fileHeaderHelper, SyntaxTrivia newLineTrivia, SyntaxNode root, string expectedFileHeader) + { + // Skip single line comments, whitespace, and end of line trivia until a blank line is encountered. + var triviaList = root.GetLeadingTrivia(); - // The set of indexes to remove from 'triviaList'. After removing these indexes, the remaining trivia (if - // any) will be preserved in the document along with the replacement header. - var removalList = new List(); + // True if the current line is blank so far (empty or whitespace); otherwise, false. The first line is + // assumed to not be blank, which allows the analysis to detect a file header which follows a blank line at + // the top of the file. + var onBlankLine = false; - // The number of spaces to indent the new header. This is expected to match the indentation of the header - // which is being replaced. - var leadingSpaces = string.Empty; + // The set of indexes to remove from 'triviaList'. After removing these indexes, the remaining trivia (if + // any) will be preserved in the document along with the replacement header. + var removalList = new List(); - // The number of spaces found so far on the current line. This will become 'leadingSpaces' if the spaces are - // followed by a comment which is considered a header comment. - var possibleLeadingSpaces = string.Empty; + // The number of spaces to indent the new header. This is expected to match the indentation of the header + // which is being replaced. + var leadingSpaces = string.Empty; - // Need to do this with index so we get the line endings correct. - for (var i = 0; i < triviaList.Count; i++) + // The number of spaces found so far on the current line. This will become 'leadingSpaces' if the spaces are + // followed by a comment which is considered a header comment. + var possibleLeadingSpaces = string.Empty; + + // Need to do this with index so we get the line endings correct. + for (var i = 0; i < triviaList.Count; i++) + { + var triviaLine = triviaList[i]; + if (triviaLine.RawKind == syntaxFacts.SyntaxKinds.SingleLineCommentTrivia) { - var triviaLine = triviaList[i]; - if (triviaLine.RawKind == syntaxFacts.SyntaxKinds.SingleLineCommentTrivia) + if (possibleLeadingSpaces != string.Empty) { - if (possibleLeadingSpaces != string.Empty) - { - // One or more spaces precedes the comment. Keep track of these spaces so we can indent the new - // header by the same amount. - leadingSpaces = possibleLeadingSpaces; - } - - removalList.Add(i); - onBlankLine = false; + // One or more spaces precedes the comment. Keep track of these spaces so we can indent the new + // header by the same amount. + leadingSpaces = possibleLeadingSpaces; } - else if (triviaLine.RawKind == syntaxFacts.SyntaxKinds.WhitespaceTrivia) - { - if (leadingSpaces == string.Empty) - { - possibleLeadingSpaces = triviaLine.ToFullString(); - } - removalList.Add(i); + removalList.Add(i); + onBlankLine = false; + } + else if (triviaLine.RawKind == syntaxFacts.SyntaxKinds.WhitespaceTrivia) + { + if (leadingSpaces == string.Empty) + { + possibleLeadingSpaces = triviaLine.ToFullString(); } - else if (triviaLine.RawKind == syntaxFacts.SyntaxKinds.EndOfLineTrivia) + + removalList.Add(i); + } + else if (triviaLine.RawKind == syntaxFacts.SyntaxKinds.EndOfLineTrivia) + { + possibleLeadingSpaces = string.Empty; + removalList.Add(i); + + if (onBlankLine) { - possibleLeadingSpaces = string.Empty; - removalList.Add(i); - - if (onBlankLine) - { - break; - } - else - { - onBlankLine = true; - } + break; } else { - break; + onBlankLine = true; } } - - // Remove copyright lines in reverse order. - for (var i = removalList.Count - 1; i >= 0; i--) + else { - triviaList = triviaList.RemoveAt(removalList[i]); + break; } + } - var newHeaderTrivia = CreateNewHeader(syntaxFacts, leadingSpaces + fileHeaderHelper.CommentPrefix, expectedFileHeader, newLineTrivia.ToFullString()); + // Remove copyright lines in reverse order. + for (var i = removalList.Count - 1; i >= 0; i--) + { + triviaList = triviaList.RemoveAt(removalList[i]); + } - // Add a blank line and any remaining preserved trivia after the header. - newHeaderTrivia = newHeaderTrivia.Add(newLineTrivia).Add(newLineTrivia).AddRange(triviaList); + var newHeaderTrivia = CreateNewHeader(syntaxFacts, leadingSpaces + fileHeaderHelper.CommentPrefix, expectedFileHeader, newLineTrivia.ToFullString()); - // Insert header at top of the file. - return root.WithLeadingTrivia(newHeaderTrivia); - } + // Add a blank line and any remaining preserved trivia after the header. + newHeaderTrivia = newHeaderTrivia.Add(newLineTrivia).Add(newLineTrivia).AddRange(triviaList); - private static SyntaxNode AddHeader(ISyntaxFacts syntaxFacts, AbstractFileHeaderHelper fileHeaderHelper, SyntaxTrivia newLineTrivia, SyntaxNode root, string expectedFileHeader) - { - var newTrivia = CreateNewHeader(syntaxFacts, fileHeaderHelper.CommentPrefix, expectedFileHeader, newLineTrivia.ToFullString()).Add(newLineTrivia).Add(newLineTrivia); + // Insert header at top of the file. + return root.WithLeadingTrivia(newHeaderTrivia); + } - // Skip blank lines already at the beginning of the document, since we add our own - var leadingTrivia = root.GetLeadingTrivia(); - var skipCount = 0; - for (var i = 0; i < leadingTrivia.Count; i++) + private static SyntaxNode AddHeader(ISyntaxFacts syntaxFacts, AbstractFileHeaderHelper fileHeaderHelper, SyntaxTrivia newLineTrivia, SyntaxNode root, string expectedFileHeader) + { + var newTrivia = CreateNewHeader(syntaxFacts, fileHeaderHelper.CommentPrefix, expectedFileHeader, newLineTrivia.ToFullString()).Add(newLineTrivia).Add(newLineTrivia); + + // Skip blank lines already at the beginning of the document, since we add our own + var leadingTrivia = root.GetLeadingTrivia(); + var skipCount = 0; + for (var i = 0; i < leadingTrivia.Count; i++) + { + if (leadingTrivia[i].RawKind == syntaxFacts.SyntaxKinds.EndOfLineTrivia) { - if (leadingTrivia[i].RawKind == syntaxFacts.SyntaxKinds.EndOfLineTrivia) - { - skipCount = i + 1; - } - else if (leadingTrivia[i].RawKind != syntaxFacts.SyntaxKinds.WhitespaceTrivia) - { - break; - } + skipCount = i + 1; } + else if (leadingTrivia[i].RawKind != syntaxFacts.SyntaxKinds.WhitespaceTrivia) + { + break; + } + } - newTrivia = newTrivia.AddRange(leadingTrivia.Skip(skipCount)); + newTrivia = newTrivia.AddRange(leadingTrivia.Skip(skipCount)); - return root.WithLeadingTrivia(newTrivia); - } + return root.WithLeadingTrivia(newTrivia); + } - private static SyntaxTriviaList CreateNewHeader(ISyntaxFacts syntaxFacts, string prefixWithLeadingSpaces, string expectedFileHeader, string newLineText) - { - var copyrightText = GetCopyrightText(prefixWithLeadingSpaces, expectedFileHeader, newLineText); - var newHeader = copyrightText; - return syntaxFacts.ParseLeadingTrivia(newHeader); - } + private static SyntaxTriviaList CreateNewHeader(ISyntaxFacts syntaxFacts, string prefixWithLeadingSpaces, string expectedFileHeader, string newLineText) + { + var copyrightText = GetCopyrightText(prefixWithLeadingSpaces, expectedFileHeader, newLineText); + var newHeader = copyrightText; + return syntaxFacts.ParseLeadingTrivia(newHeader); + } - private static string GetCopyrightText(string prefixWithLeadingSpaces, string copyrightText, string newLineText) + private static string GetCopyrightText(string prefixWithLeadingSpaces, string copyrightText, string newLineText) + { + copyrightText = copyrightText.Replace("\r\n", "\n"); + var lines = copyrightText.Split('\n'); + return string.Join(newLineText, lines.Select(line => { - copyrightText = copyrightText.Replace("\r\n", "\n"); - var lines = copyrightText.Split('\n'); - return string.Join(newLineText, lines.Select(line => + // Rewrite the lines of the header as comments without trailing whitespace. + if (string.IsNullOrEmpty(line)) { - // Rewrite the lines of the header as comments without trailing whitespace. - if (string.IsNullOrEmpty(line)) - { - // This is a blank line of the header. We want the prefix indicating the line is a comment, but no - // additional trailing whitespace. - return prefixWithLeadingSpaces; - } - else - { - // This is a normal line of the header. We want the prefix, followed by a single space, and then the - // text of the header line. - return prefixWithLeadingSpaces + " " + line; - } - })); - } - - public override FixAllProvider GetFixAllProvider() - => FixAllProvider.Create(async (context, document, diagnostics) => + // This is a blank line of the header. We want the prefix indicating the line is a comment, but no + // additional trailing whitespace. + return prefixWithLeadingSpaces; + } + else { - if (diagnostics.IsEmpty) - return null; - - return await this.GetTransformedDocumentAsync(document, context.GetOptionsProvider(), context.CancellationToken).ConfigureAwait(false); - }); + // This is a normal line of the header. We want the prefix, followed by a single space, and then the + // text of the header line. + return prefixWithLeadingSpaces + " " + line; + } + })); } + + public override FixAllProvider GetFixAllProvider() + => FixAllProvider.Create(async (context, document, diagnostics) => + { + if (diagnostics.IsEmpty) + return null; + + return await this.GetTransformedDocumentAsync(document, context.GetOptionsProvider(), context.CancellationToken).ConfigureAwait(false); + }); } diff --git a/src/Analyzers/Core/CodeFixes/ForEachCast/AbstractForEachCastCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/ForEachCast/AbstractForEachCastCodeFixProvider.cs index 8d1baec53415d..5fc4bd8e9c582 100644 --- a/src/Analyzers/Core/CodeFixes/ForEachCast/AbstractForEachCastCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/ForEachCast/AbstractForEachCastCodeFixProvider.cs @@ -17,99 +17,98 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ForEachCast +namespace Microsoft.CodeAnalysis.ForEachCast; + +internal abstract class AbstractForEachCastCodeFixProvider : SyntaxEditorBasedCodeFixProvider + where TForEachStatementSyntax : SyntaxNode { - internal abstract class AbstractForEachCastCodeFixProvider : SyntaxEditorBasedCodeFixProvider - where TForEachStatementSyntax : SyntaxNode - { - protected abstract ITypeSymbol GetForEachElementType(SemanticModel semanticModel, TForEachStatementSyntax forEachStatement); + protected abstract ITypeSymbol GetForEachElementType(SemanticModel semanticModel, TForEachStatementSyntax forEachStatement); - public sealed override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.ForEachCastDiagnosticId]; + public sealed override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.ForEachCastDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + if (context.Diagnostics.First().Properties.ContainsKey(ForEachCastHelpers.IsFixable)) { - if (context.Diagnostics.First().Properties.ContainsKey(ForEachCastHelpers.IsFixable)) - { - RegisterCodeFix(context, AnalyzersResources.Add_explicit_cast, nameof(AbstractForEachCastCodeFixProvider)); - } - - return Task.CompletedTask; + RegisterCodeFix(context, AnalyzersResources.Add_explicit_cast, nameof(AbstractForEachCastCodeFixProvider)); } - protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) - => diagnostic.Properties.ContainsKey(ForEachCastHelpers.IsFixable); + return Task.CompletedTask; + } + + protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) + => diagnostic.Properties.ContainsKey(ForEachCastHelpers.IsFixable); - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + foreach (var diagnostic in diagnostics) { - var syntaxFacts = document.GetRequiredLanguageService(); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - foreach (var diagnostic in diagnostics) - { - var node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); - if (node is TForEachStatementSyntax foreachStatement) - AddCast(syntaxFacts, semanticModel, editor, foreachStatement, cancellationToken); - } + var node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + if (node is TForEachStatementSyntax foreachStatement) + AddCast(syntaxFacts, semanticModel, editor, foreachStatement, cancellationToken); } + } - public void AddCast( - ISyntaxFacts syntaxFacts, - SemanticModel semanticModel, - SyntaxEditor editor, - TForEachStatementSyntax forEachStatement, - CancellationToken cancellationToken) - { - var expression = syntaxFacts.GetExpressionOfForeachStatement(forEachStatement); - var loopOperation = (IForEachLoopOperation)semanticModel.GetRequiredOperation(forEachStatement, cancellationToken); - var variableDeclarator = (IVariableDeclaratorOperation)loopOperation.LoopControlVariable; - var enumerableType = semanticModel.Compilation.GetBestTypeByMetadataName(typeof(Enumerable).FullName!); + public void AddCast( + ISyntaxFacts syntaxFacts, + SemanticModel semanticModel, + SyntaxEditor editor, + TForEachStatementSyntax forEachStatement, + CancellationToken cancellationToken) + { + var expression = syntaxFacts.GetExpressionOfForeachStatement(forEachStatement); + var loopOperation = (IForEachLoopOperation)semanticModel.GetRequiredOperation(forEachStatement, cancellationToken); + var variableDeclarator = (IVariableDeclaratorOperation)loopOperation.LoopControlVariable; + var enumerableType = semanticModel.Compilation.GetBestTypeByMetadataName(typeof(Enumerable).FullName!); - // These were already verified to be non-null in the analyzer. - Contract.ThrowIfNull(variableDeclarator.Symbol.Type); - Contract.ThrowIfNull(enumerableType); + // These were already verified to be non-null in the analyzer. + Contract.ThrowIfNull(variableDeclarator.Symbol.Type); + Contract.ThrowIfNull(enumerableType); - var elementType = GetForEachElementType(semanticModel, forEachStatement); - var conversion = semanticModel.Compilation.ClassifyCommonConversion(elementType, variableDeclarator.Symbol.Type); + var elementType = GetForEachElementType(semanticModel, forEachStatement); + var conversion = semanticModel.Compilation.ClassifyCommonConversion(elementType, variableDeclarator.Symbol.Type); - var rewritten = GetRewrittenCollection(editor.Generator, expression, variableDeclarator.Symbol.Type, conversion); + var rewritten = GetRewrittenCollection(editor.Generator, expression, variableDeclarator.Symbol.Type, conversion); - // Add an annotation for System.Linq.Enumerable so that we add a `using System.Linq;` if not present. - rewritten = rewritten.WithAdditionalAnnotations( - Simplifier.Annotation, Simplifier.AddImportsAnnotation, SymbolAnnotation.Create(enumerableType)); + // Add an annotation for System.Linq.Enumerable so that we add a `using System.Linq;` if not present. + rewritten = rewritten.WithAdditionalAnnotations( + Simplifier.Annotation, Simplifier.AddImportsAnnotation, SymbolAnnotation.Create(enumerableType)); - editor.ReplaceNode(expression, rewritten); - } + editor.ReplaceNode(expression, rewritten); + } - private SyntaxNode GetRewrittenCollection( - SyntaxGenerator generator, - SyntaxNode collection, - ITypeSymbol iterationVariableType, - CommonConversion conversion) + private SyntaxNode GetRewrittenCollection( + SyntaxGenerator generator, + SyntaxNode collection, + ITypeSymbol iterationVariableType, + CommonConversion conversion) + { + if (conversion.Exists && conversion.IsReference) + { + // for a reference cast we can insert `.Cast()` + return generator.InvocationExpression( + generator.MemberAccessExpression( + collection, + generator.GenericName( + nameof(Enumerable.Cast), + new[] { iterationVariableType }))); + } + else { - if (conversion.Exists && conversion.IsReference) - { - // for a reference cast we can insert `.Cast()` - return generator.InvocationExpression( - generator.MemberAccessExpression( - collection, - generator.GenericName( - nameof(Enumerable.Cast), - new[] { iterationVariableType }))); - } - else - { - // otherwise we need to ensure a language specific conversion by emitting the conversion into the code - // like so: `.Select(v => (DestType)v)` - return generator.InvocationExpression( - generator.MemberAccessExpression( - collection, - generator.IdentifierName(nameof(Enumerable.Select))), - generator.ValueReturningLambdaExpression( - "v", - generator.ConvertExpression(iterationVariableType, generator.IdentifierName("v")))); - } + // otherwise we need to ensure a language specific conversion by emitting the conversion into the code + // like so: `.Select(v => (DestType)v)` + return generator.InvocationExpression( + generator.MemberAccessExpression( + collection, + generator.IdentifierName(nameof(Enumerable.Select))), + generator.ValueReturningLambdaExpression( + "v", + generator.ConvertExpression(iterationVariableType, generator.IdentifierName("v")))); } } } diff --git a/src/Analyzers/Core/CodeFixes/Formatting/FormattingCodeFixHelper.cs b/src/Analyzers/Core/CodeFixes/Formatting/FormattingCodeFixHelper.cs index f1308edf11c32..a20c0646ab33c 100644 --- a/src/Analyzers/Core/CodeFixes/Formatting/FormattingCodeFixHelper.cs +++ b/src/Analyzers/Core/CodeFixes/Formatting/FormattingCodeFixHelper.cs @@ -10,24 +10,23 @@ using Formatter = Microsoft.CodeAnalysis.Formatting.FormatterHelper; using FormattingProvider = Microsoft.CodeAnalysis.Formatting.ISyntaxFormatting; -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +internal static class FormattingCodeFixHelper { - internal static class FormattingCodeFixHelper + internal static async Task FixOneAsync(SyntaxTree syntaxTree, FormattingProvider formattingProvider, SyntaxFormattingOptions options, Diagnostic diagnostic, CancellationToken cancellationToken) { - internal static async Task FixOneAsync(SyntaxTree syntaxTree, FormattingProvider formattingProvider, SyntaxFormattingOptions options, Diagnostic diagnostic, CancellationToken cancellationToken) - { - // The span to format is the full line(s) containing the diagnostic - var text = await syntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false); - var diagnosticSpan = diagnostic.Location.SourceSpan; - var diagnosticLinePositionSpan = text.Lines.GetLinePositionSpan(diagnosticSpan); - var spanToFormat = TextSpan.FromBounds( - text.Lines[diagnosticLinePositionSpan.Start.Line].Start, - text.Lines[diagnosticLinePositionSpan.End.Line].End); + // The span to format is the full line(s) containing the diagnostic + var text = await syntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false); + var diagnosticSpan = diagnostic.Location.SourceSpan; + var diagnosticLinePositionSpan = text.Lines.GetLinePositionSpan(diagnosticSpan); + var spanToFormat = TextSpan.FromBounds( + text.Lines[diagnosticLinePositionSpan.Start.Line].Start, + text.Lines[diagnosticLinePositionSpan.End.Line].End); - var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var formattedRoot = Formatter.Format(root, spanToFormat, formattingProvider, options, cancellationToken); + var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var formattedRoot = Formatter.Format(root, spanToFormat, formattingProvider, options, cancellationToken); - return syntaxTree.WithRootAndOptions(formattedRoot, syntaxTree.Options); - } + return syntaxTree.WithRootAndOptions(formattedRoot, syntaxTree.Options); } } diff --git a/src/Analyzers/Core/CodeFixes/Formatting/FormattingCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/Formatting/FormattingCodeFixProvider.cs index 2f945465ecea4..2669f4355011c 100644 --- a/src/Analyzers/Core/CodeFixes/Formatting/FormattingCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/Formatting/FormattingCodeFixProvider.cs @@ -14,67 +14,66 @@ using Formatter = Microsoft.CodeAnalysis.Formatting.FormatterHelper; -namespace Microsoft.CodeAnalysis.CodeStyle +namespace Microsoft.CodeAnalysis.CodeStyle; + +internal abstract class AbstractFormattingCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - internal abstract class AbstractFormattingCodeFixProvider : SyntaxEditorBasedCodeFixProvider + protected AbstractFormattingCodeFixProvider() { - protected AbstractFormattingCodeFixProvider() - { #if !CODE_STYLE - // Backdoor that allows this provider to use the high-priority bucket. - this.CustomTags = this.CustomTags.Add(CodeAction.CanBeHighPriorityTag); + // Backdoor that allows this provider to use the high-priority bucket. + this.CustomTags = this.CustomTags.Add(CodeAction.CanBeHighPriorityTag); #endif - } + } - public sealed override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.FormattingDiagnosticId]; + public sealed override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.FormattingDiagnosticId]; - protected abstract ISyntaxFormatting SyntaxFormatting { get; } + protected abstract ISyntaxFormatting SyntaxFormatting { get; } - /// - /// Fixing formatting is high priority. It's something the user wants to be able to fix quickly, is driven by - /// them acting on an error reported in code, and can be computed fast as it only uses syntax not semantics. - /// It's also the 8th most common fix that people use, and is picked almost all the times it is shown. - /// - protected override CodeActionRequestPriority ComputeRequestPriority() - => CodeActionRequestPriority.High; + /// + /// Fixing formatting is high priority. It's something the user wants to be able to fix quickly, is driven by + /// them acting on an error reported in code, and can be computed fast as it only uses syntax not semantics. + /// It's also the 8th most common fix that people use, and is picked almost all the times it is shown. + /// + protected override CodeActionRequestPriority ComputeRequestPriority() + => CodeActionRequestPriority.High; - public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics) { - foreach (var diagnostic in context.Diagnostics) - { - var codeAction = CodeAction.Create( - AnalyzersResources.Fix_formatting, - c => FixOneAsync(context, diagnostic, c), - nameof(AbstractFormattingCodeFixProvider), - CodeActionPriority.High); + var codeAction = CodeAction.Create( + AnalyzersResources.Fix_formatting, + c => FixOneAsync(context, diagnostic, c), + nameof(AbstractFormattingCodeFixProvider), + CodeActionPriority.High); #if !CODE_STYLE - // Backdoor that allows this provider to use the high-priority bucket. - codeAction.CustomTags = codeAction.CustomTags.Add(CodeAction.CanBeHighPriorityTag); + // Backdoor that allows this provider to use the high-priority bucket. + codeAction.CustomTags = codeAction.CustomTags.Add(CodeAction.CanBeHighPriorityTag); #endif - context.RegisterCodeFix(codeAction, diagnostic); - } - - return Task.CompletedTask; + context.RegisterCodeFix(codeAction, diagnostic); } - private async Task FixOneAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken cancellationToken) - { - var options = await context.Document.GetCodeFixOptionsAsync(context.GetOptionsProvider(), cancellationToken).ConfigureAwait(false); - var formattingOptions = options.GetFormattingOptions(SyntaxFormatting); - var tree = await context.Document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var updatedTree = await FormattingCodeFixHelper.FixOneAsync(tree, SyntaxFormatting, formattingOptions, diagnostic, cancellationToken).ConfigureAwait(false); - return context.Document.WithText(await updatedTree.GetTextAsync(cancellationToken).ConfigureAwait(false)); - } + return Task.CompletedTask; + } - protected override async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var formattingOptions = options.GetFormattingOptions(SyntaxFormatting); - var updatedRoot = Formatter.Format(editor.OriginalRoot, SyntaxFormatting, formattingOptions, cancellationToken); - editor.ReplaceNode(editor.OriginalRoot, updatedRoot); - } + private async Task FixOneAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var options = await context.Document.GetCodeFixOptionsAsync(context.GetOptionsProvider(), cancellationToken).ConfigureAwait(false); + var formattingOptions = options.GetFormattingOptions(SyntaxFormatting); + var tree = await context.Document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var updatedTree = await FormattingCodeFixHelper.FixOneAsync(tree, SyntaxFormatting, formattingOptions, diagnostic, cancellationToken).ConfigureAwait(false); + return context.Document.WithText(await updatedTree.GetTextAsync(cancellationToken).ConfigureAwait(false)); + } + + protected override async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var formattingOptions = options.GetFormattingOptions(SyntaxFormatting); + var updatedRoot = Formatter.Format(editor.OriginalRoot, SyntaxFormatting, formattingOptions, cancellationToken); + editor.ReplaceNode(editor.OriginalRoot, updatedRoot); } } diff --git a/src/Analyzers/Core/CodeFixes/Iterator/AbstractIteratorCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/Iterator/AbstractIteratorCodeFixProvider.cs index 0359435b5db9c..e7dff05603827 100644 --- a/src/Analyzers/Core/CodeFixes/Iterator/AbstractIteratorCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/Iterator/AbstractIteratorCodeFixProvider.cs @@ -11,47 +11,46 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CodeFixes.Iterator +namespace Microsoft.CodeAnalysis.CodeFixes.Iterator; + +internal abstract class AbstractIteratorCodeFixProvider : CodeFixProvider { - internal abstract class AbstractIteratorCodeFixProvider : CodeFixProvider + public override FixAllProvider GetFixAllProvider() { - public override FixAllProvider GetFixAllProvider() - { - // Fix All is not supported by this code fix - return null; - } + // Fix All is not supported by this code fix + return null; + } - protected abstract Task GetCodeFixAsync(SyntaxNode root, SyntaxNode node, Document document, Diagnostic diagnostics, CancellationToken cancellationToken); + protected abstract Task GetCodeFixAsync(SyntaxNode root, SyntaxNode node, Document document, Diagnostic diagnostics, CancellationToken cancellationToken); - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (!TryGetNode(root, context.Span, out var node)) { - var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - if (!TryGetNode(root, context.Span, out var node)) - { - return; - } + return; + } - var diagnostic = context.Diagnostics.FirstOrDefault(); + var diagnostic = context.Diagnostics.FirstOrDefault(); - var codeAction = await GetCodeFixAsync(root, node, context.Document, diagnostic, context.CancellationToken).ConfigureAwait(false); + var codeAction = await GetCodeFixAsync(root, node, context.Document, diagnostic, context.CancellationToken).ConfigureAwait(false); - if (codeAction != null) - { - context.RegisterCodeFix(codeAction, diagnostic); - } + if (codeAction != null) + { + context.RegisterCodeFix(codeAction, diagnostic); } + } - protected virtual bool TryGetNode(SyntaxNode root, TextSpan span, out SyntaxNode node) + protected virtual bool TryGetNode(SyntaxNode root, TextSpan span, out SyntaxNode node) + { + node = null; + var ancestors = root.FindToken(span.Start).GetAncestors(); + if (!ancestors.Any()) { - node = null; - var ancestors = root.FindToken(span.Start).GetAncestors(); - if (!ancestors.Any()) - { - return false; - } - - node = ancestors.FirstOrDefault(n => n.Span.Contains(span) && n != root); - return node != null; + return false; } + + node = ancestors.FirstOrDefault(n => n.Span.Contains(span) && n != root); + return node != null; } } diff --git a/src/Analyzers/Core/CodeFixes/MakeFieldReadonly/AbstractMakeFieldReadonlyCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/MakeFieldReadonly/AbstractMakeFieldReadonlyCodeFixProvider.cs index 97be6e261af99..8ddf3bbe3d123 100644 --- a/src/Analyzers/Core/CodeFixes/MakeFieldReadonly/AbstractMakeFieldReadonlyCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/MakeFieldReadonly/AbstractMakeFieldReadonlyCodeFixProvider.cs @@ -16,91 +16,90 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.MakeFieldReadonly +namespace Microsoft.CodeAnalysis.MakeFieldReadonly; + +internal abstract class AbstractMakeFieldReadonlyCodeFixProvider + : SyntaxEditorBasedCodeFixProvider + where TSymbolSyntax : SyntaxNode + where TFieldDeclarationSyntax : SyntaxNode { - internal abstract class AbstractMakeFieldReadonlyCodeFixProvider - : SyntaxEditorBasedCodeFixProvider - where TSymbolSyntax : SyntaxNode - where TFieldDeclarationSyntax : SyntaxNode - { - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.MakeFieldReadonlyDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.MakeFieldReadonlyDiagnosticId]; - protected abstract SyntaxNode? GetInitializerNode(TSymbolSyntax declaration); - protected abstract ImmutableList GetVariableDeclarators(TFieldDeclarationSyntax declaration); + protected abstract SyntaxNode? GetInitializerNode(TSymbolSyntax declaration); + protected abstract ImmutableList GetVariableDeclarators(TFieldDeclarationSyntax declaration); - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, AnalyzersResources.Add_readonly_modifier, nameof(AnalyzersResources.Add_readonly_modifier)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, AnalyzersResources.Add_readonly_modifier, nameof(AnalyzersResources.Add_readonly_modifier)); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, - ImmutableArray diagnostics, - SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override async Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var declarators = new List(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + foreach (var diagnostic in diagnostics) { - var declarators = new List(); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - foreach (var diagnostic in diagnostics) - { - var diagnosticSpan = diagnostic.Location.SourceSpan; - - declarators.Add(root.FindNode(diagnosticSpan, getInnermostNodeForTie: true).FirstAncestorOrSelf()!); - } + var diagnosticSpan = diagnostic.Location.SourceSpan; - await MakeFieldReadonlyAsync(document, editor, declarators, cancellationToken).ConfigureAwait(false); + declarators.Add(root.FindNode(diagnosticSpan, getInnermostNodeForTie: true).FirstAncestorOrSelf()!); } - private async Task MakeFieldReadonlyAsync( - Document document, SyntaxEditor editor, List declarators, CancellationToken cancellationToken) + await MakeFieldReadonlyAsync(document, editor, declarators, cancellationToken).ConfigureAwait(false); + } + + private async Task MakeFieldReadonlyAsync( + Document document, SyntaxEditor editor, List declarators, CancellationToken cancellationToken) + { + var generator = editor.Generator; + var declaratorsByField = declarators.GroupBy(g => g.FirstAncestorOrSelf()!); + + foreach (var fieldDeclarators in declaratorsByField) { - var generator = editor.Generator; - var declaratorsByField = declarators.GroupBy(g => g.FirstAncestorOrSelf()!); + var fieldDeclaration = fieldDeclarators.Key; + var declarationDeclarators = GetVariableDeclarators(fieldDeclaration); - foreach (var fieldDeclarators in declaratorsByField) + if (declarationDeclarators.Count == fieldDeclarators.Count()) + { + var modifiers = WithReadOnly(editor.Generator.GetModifiers(fieldDeclaration)); + editor.ReplaceNode( + fieldDeclaration, + generator.WithModifiers(fieldDeclaration.WithoutTrivia(), modifiers).WithTriviaFrom(fieldDeclaration)); + } + else { - var fieldDeclaration = fieldDeclarators.Key; - var declarationDeclarators = GetVariableDeclarators(fieldDeclaration); + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (declarationDeclarators.Count == fieldDeclarators.Count()) - { - var modifiers = WithReadOnly(editor.Generator.GetModifiers(fieldDeclaration)); - editor.ReplaceNode( - fieldDeclaration, - generator.WithModifiers(fieldDeclaration.WithoutTrivia(), modifiers).WithTriviaFrom(fieldDeclaration)); - } - else + foreach (var declarator in declarationDeclarators.Reverse()) { - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - foreach (var declarator in declarationDeclarators.Reverse()) - { - var symbol = (IFieldSymbol?)model.GetDeclaredSymbol(declarator, cancellationToken); - Contract.ThrowIfNull(symbol); - var modifiers = generator.GetModifiers(fieldDeclaration); - - var newDeclaration = generator - .FieldDeclaration( - symbol.Name, - generator.TypeExpression(symbol.Type), - Accessibility.Private, - fieldDeclarators.Contains(declarator) - ? WithReadOnly(modifiers) - : modifiers, - GetInitializerNode(declarator)) - .WithAdditionalAnnotations(Formatter.Annotation); - - editor.InsertAfter(fieldDeclaration, newDeclaration); - } - - editor.RemoveNode(fieldDeclaration, SyntaxRemoveOptions.KeepLeadingTrivia); + var symbol = (IFieldSymbol?)model.GetDeclaredSymbol(declarator, cancellationToken); + Contract.ThrowIfNull(symbol); + var modifiers = generator.GetModifiers(fieldDeclaration); + + var newDeclaration = generator + .FieldDeclaration( + symbol.Name, + generator.TypeExpression(symbol.Type), + Accessibility.Private, + fieldDeclarators.Contains(declarator) + ? WithReadOnly(modifiers) + : modifiers, + GetInitializerNode(declarator)) + .WithAdditionalAnnotations(Formatter.Annotation); + + editor.InsertAfter(fieldDeclaration, newDeclaration); } + + editor.RemoveNode(fieldDeclaration, SyntaxRemoveOptions.KeepLeadingTrivia); } } - - private static DeclarationModifiers WithReadOnly(DeclarationModifiers modifiers) - => (modifiers - DeclarationModifiers.Volatile) | DeclarationModifiers.ReadOnly; } + + private static DeclarationModifiers WithReadOnly(DeclarationModifiers modifiers) + => (modifiers - DeclarationModifiers.Volatile) | DeclarationModifiers.ReadOnly; } diff --git a/src/Analyzers/Core/CodeFixes/MakeMemberStatic/AbstractMakeMemberStaticCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/MakeMemberStatic/AbstractMakeMemberStaticCodeFixProvider.cs index 476d9a8b533cd..def47e999fad0 100644 --- a/src/Analyzers/Core/CodeFixes/MakeMemberStatic/AbstractMakeMemberStaticCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/MakeMemberStatic/AbstractMakeMemberStaticCodeFixProvider.cs @@ -12,39 +12,38 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.MakeMemberStatic +namespace Microsoft.CodeAnalysis.MakeMemberStatic; + +internal abstract class AbstractMakeMemberStaticCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - internal abstract class AbstractMakeMemberStaticCodeFixProvider : SyntaxEditorBasedCodeFixProvider - { - protected abstract bool TryGetMemberDeclaration(SyntaxNode node, [NotNullWhen(true)] out SyntaxNode? memberDeclaration); + protected abstract bool TryGetMemberDeclaration(SyntaxNode node, [NotNullWhen(true)] out SyntaxNode? memberDeclaration); - public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + if (context.Diagnostics.Length == 1 && + TryGetMemberDeclaration(context.Diagnostics[0].Location.FindNode(context.CancellationToken), out _)) { - if (context.Diagnostics.Length == 1 && - TryGetMemberDeclaration(context.Diagnostics[0].Location.FindNode(context.CancellationToken), out _)) - { - RegisterCodeFix(context, CodeFixesResources.Make_member_static, nameof(AbstractMakeMemberStaticCodeFixProvider)); - } - - return Task.CompletedTask; + RegisterCodeFix(context, CodeFixesResources.Make_member_static, nameof(AbstractMakeMemberStaticCodeFixProvider)); } - protected sealed override Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + return Task.CompletedTask; + } + + protected sealed override Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + for (var i = 0; i < diagnostics.Length; i++) { - for (var i = 0; i < diagnostics.Length; i++) + var declaration = diagnostics[i].Location.FindNode(cancellationToken); + + if (TryGetMemberDeclaration(declaration, out var memberDeclaration)) { - var declaration = diagnostics[i].Location.FindNode(cancellationToken); - - if (TryGetMemberDeclaration(declaration, out var memberDeclaration)) - { - var generator = SyntaxGenerator.GetGenerator(document); - var newNode = generator.WithModifiers(memberDeclaration, generator.GetModifiers(declaration).WithIsStatic(true)); - editor.ReplaceNode(declaration, newNode); - } + var generator = SyntaxGenerator.GetGenerator(document); + var newNode = generator.WithModifiers(memberDeclaration, generator.GetModifiers(declaration).WithIsStatic(true)); + editor.ReplaceNode(declaration, newNode); } - - return Task.CompletedTask; } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/Core/CodeFixes/MakeMethodAsynchronous/AbstractMakeMethodAsynchronousCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/MakeMethodAsynchronous/AbstractMakeMethodAsynchronousCodeFixProvider.cs index 3daa7dbe11d34..73af02c92ae82 100644 --- a/src/Analyzers/Core/CodeFixes/MakeMethodAsynchronous/AbstractMakeMethodAsynchronousCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/MakeMethodAsynchronous/AbstractMakeMethodAsynchronousCodeFixProvider.cs @@ -13,189 +13,188 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.MakeMethodAsynchronous +namespace Microsoft.CodeAnalysis.MakeMethodAsynchronous; + +internal abstract partial class AbstractMakeMethodAsynchronousCodeFixProvider : CodeFixProvider { - internal abstract partial class AbstractMakeMethodAsynchronousCodeFixProvider : CodeFixProvider - { - protected abstract bool IsSupportedDiagnostic(Diagnostic diagnostic, CancellationToken cancellationToken); - protected abstract bool IsAsyncSupportingFunctionSyntax(SyntaxNode node); + protected abstract bool IsSupportedDiagnostic(Diagnostic diagnostic, CancellationToken cancellationToken); + protected abstract bool IsAsyncSupportingFunctionSyntax(SyntaxNode node); - protected abstract string GetMakeAsyncTaskFunctionResource(); - protected abstract string GetMakeAsyncVoidFunctionResource(); + protected abstract string GetMakeAsyncTaskFunctionResource(); + protected abstract string GetMakeAsyncVoidFunctionResource(); - protected abstract bool IsAsyncReturnType(ITypeSymbol type, KnownTaskTypes knownTypes); + protected abstract bool IsAsyncReturnType(ITypeSymbol type, KnownTaskTypes knownTypes); - protected abstract SyntaxNode AddAsyncTokenAndFixReturnType( - bool keepVoid, IMethodSymbol methodSymbol, SyntaxNode node, KnownTaskTypes knownTypes, CancellationToken cancellationToken); + protected abstract SyntaxNode AddAsyncTokenAndFixReturnType( + bool keepVoid, IMethodSymbol methodSymbol, SyntaxNode node, KnownTaskTypes knownTypes, CancellationToken cancellationToken); - 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) + { + var document = context.Document; + var diagnostic = context.Diagnostics.First(); + var cancellationToken = context.CancellationToken; + + if (!IsSupportedDiagnostic(diagnostic, cancellationToken)) + return; + + var node = GetContainingFunction(diagnostic, cancellationToken); + if (node == null) + return; + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var compilation = semanticModel.Compilation; + + // Find the symbols for Task, Task and ValueTask. Note that the first + // two are mandatory (since we need them to generate the return types for our + // method if we convert it. The last is optional. It is only needed to know + // if our member is already Task-Like, and that functionality recognizes + // ValueTask if it is available, but does not care if it is not. + var knownTypes = new KnownTaskTypes(compilation); + if (knownTypes.TaskType == null || knownTypes.TaskOfTType == null) + return; + + var methodSymbol = GetMethodSymbol(semanticModel, node, cancellationToken); + if (methodSymbol is null) + return; + + // Heuristic to recognize the common case for entry point method + var isEntryPoint = methodSymbol.IsStatic && IsLikelyEntryPointName(methodSymbol.Name, document); + + // Offer to convert to a Task return type. + var taskTitle = GetMakeAsyncTaskFunctionResource(); + context.RegisterCodeFix( + CodeAction.Create( + taskTitle, + cancellationToken => FixNodeAsync(document, diagnostic, keepVoid: false, isEntryPoint, cancellationToken), + taskTitle), + context.Diagnostics); + + // If it's a void returning method (and not an entry point), also offer to keep the void return type + if (methodSymbol.IsOrdinaryMethodOrLocalFunction() && methodSymbol.ReturnsVoid && !isEntryPoint) { - var document = context.Document; - var diagnostic = context.Diagnostics.First(); - var cancellationToken = context.CancellationToken; - - if (!IsSupportedDiagnostic(diagnostic, cancellationToken)) - return; - - var node = GetContainingFunction(diagnostic, cancellationToken); - if (node == null) - return; - - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var compilation = semanticModel.Compilation; - - // Find the symbols for Task, Task and ValueTask. Note that the first - // two are mandatory (since we need them to generate the return types for our - // method if we convert it. The last is optional. It is only needed to know - // if our member is already Task-Like, and that functionality recognizes - // ValueTask if it is available, but does not care if it is not. - var knownTypes = new KnownTaskTypes(compilation); - if (knownTypes.TaskType == null || knownTypes.TaskOfTType == null) - return; - - var methodSymbol = GetMethodSymbol(semanticModel, node, cancellationToken); - if (methodSymbol is null) - return; - - // Heuristic to recognize the common case for entry point method - var isEntryPoint = methodSymbol.IsStatic && IsLikelyEntryPointName(methodSymbol.Name, document); - - // Offer to convert to a Task return type. - var taskTitle = GetMakeAsyncTaskFunctionResource(); + var asyncVoidTitle = GetMakeAsyncVoidFunctionResource(); context.RegisterCodeFix( CodeAction.Create( - taskTitle, - cancellationToken => FixNodeAsync(document, diagnostic, keepVoid: false, isEntryPoint, cancellationToken), - taskTitle), + asyncVoidTitle, + cancellationToken => FixNodeAsync(document, diagnostic, keepVoid: true, isEntryPoint: false, cancellationToken), + asyncVoidTitle), context.Diagnostics); - - // If it's a void returning method (and not an entry point), also offer to keep the void return type - if (methodSymbol.IsOrdinaryMethodOrLocalFunction() && methodSymbol.ReturnsVoid && !isEntryPoint) - { - var asyncVoidTitle = GetMakeAsyncVoidFunctionResource(); - context.RegisterCodeFix( - CodeAction.Create( - asyncVoidTitle, - cancellationToken => FixNodeAsync(document, diagnostic, keepVoid: true, isEntryPoint: false, cancellationToken), - asyncVoidTitle), - context.Diagnostics); - } } + } - private static IMethodSymbol? GetMethodSymbol(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) - { - // GetDeclaredSymbol for methods/local-functions. GetSymbolInfo for lambdas. - var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) ?? semanticModel.GetSymbolInfo(node, cancellationToken).GetAnySymbol(); - return symbol as IMethodSymbol; - } + private static IMethodSymbol? GetMethodSymbol(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + { + // GetDeclaredSymbol for methods/local-functions. GetSymbolInfo for lambdas. + var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) ?? semanticModel.GetSymbolInfo(node, cancellationToken).GetAnySymbol(); + return symbol as IMethodSymbol; + } - private static bool IsLikelyEntryPointName(string name, Document document) - { - var syntaxFacts = document.GetRequiredLanguageService(); - return syntaxFacts.StringComparer.Equals(name, "Main"); - } + private static bool IsLikelyEntryPointName(string name, Document document) + { + var syntaxFacts = document.GetRequiredLanguageService(); + return syntaxFacts.StringComparer.Equals(name, "Main"); + } - private const string AsyncSuffix = "Async"; + private const string AsyncSuffix = "Async"; - private async Task FixNodeAsync( - Document document, - Diagnostic diagnostic, - bool keepVoid, - bool isEntryPoint, - CancellationToken cancellationToken) - { - var node = GetContainingFunction(diagnostic, cancellationToken); - Contract.ThrowIfNull(node); - - // See if we're on an actual method declaration (otherwise we're on a lambda declaration). - // If we're on a method declaration, we'll get an IMethodSymbol back. In that case, check - // if it has the 'Async' suffix, and remove that suffix if so. - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var methodSymbol = GetMethodSymbol(semanticModel, node, cancellationToken); - Contract.ThrowIfNull(methodSymbol); - - var knownTypes = new KnownTaskTypes(semanticModel.Compilation); - - return NeedsRename() - ? await RenameThenAddAsyncTokenAsync(keepVoid, document, node, methodSymbol, knownTypes, cancellationToken).ConfigureAwait(false) - : await AddAsyncTokenAsync(keepVoid, document, methodSymbol, knownTypes, node, cancellationToken).ConfigureAwait(false); - - bool NeedsRename() - { - // We don't need to rename methods that don't have a name - if (!methodSymbol.IsOrdinaryMethodOrLocalFunction()) - return false; - - // We don't need to rename methods that already have an Async suffix - if (methodSymbol.Name.EndsWith(AsyncSuffix)) - return false; - - // We don't need to rename entry point methods - if (isEntryPoint) - return false; - - // Only rename if the return type will change - if (methodSymbol.ReturnsVoid) - return !keepVoid; - - return !IsAsyncReturnType(methodSymbol.ReturnType, knownTypes); - } - } + private async Task FixNodeAsync( + Document document, + Diagnostic diagnostic, + bool keepVoid, + bool isEntryPoint, + CancellationToken cancellationToken) + { + var node = GetContainingFunction(diagnostic, cancellationToken); + Contract.ThrowIfNull(node); + + // See if we're on an actual method declaration (otherwise we're on a lambda declaration). + // If we're on a method declaration, we'll get an IMethodSymbol back. In that case, check + // if it has the 'Async' suffix, and remove that suffix if so. + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var methodSymbol = GetMethodSymbol(semanticModel, node, cancellationToken); + Contract.ThrowIfNull(methodSymbol); + + var knownTypes = new KnownTaskTypes(semanticModel.Compilation); - private SyntaxNode? GetContainingFunction(Diagnostic diagnostic, CancellationToken cancellationToken) + return NeedsRename() + ? await RenameThenAddAsyncTokenAsync(keepVoid, document, node, methodSymbol, knownTypes, cancellationToken).ConfigureAwait(false) + : await AddAsyncTokenAsync(keepVoid, document, methodSymbol, knownTypes, node, cancellationToken).ConfigureAwait(false); + + bool NeedsRename() { - var token = diagnostic.Location.FindToken(cancellationToken); - var node = token.GetAncestor(IsAsyncSupportingFunctionSyntax); - return node; + // We don't need to rename methods that don't have a name + if (!methodSymbol.IsOrdinaryMethodOrLocalFunction()) + return false; + + // We don't need to rename methods that already have an Async suffix + if (methodSymbol.Name.EndsWith(AsyncSuffix)) + return false; + + // We don't need to rename entry point methods + if (isEntryPoint) + return false; + + // Only rename if the return type will change + if (methodSymbol.ReturnsVoid) + return !keepVoid; + + return !IsAsyncReturnType(methodSymbol.ReturnType, knownTypes); } + } + + private SyntaxNode? GetContainingFunction(Diagnostic diagnostic, CancellationToken cancellationToken) + { + var token = diagnostic.Location.FindToken(cancellationToken); + var node = token.GetAncestor(IsAsyncSupportingFunctionSyntax); + return node; + } - private async Task RenameThenAddAsyncTokenAsync( - bool keepVoid, - Document document, - SyntaxNode node, - IMethodSymbol methodSymbol, - KnownTaskTypes knownTypes, - CancellationToken cancellationToken) + private async Task RenameThenAddAsyncTokenAsync( + bool keepVoid, + Document document, + SyntaxNode node, + IMethodSymbol methodSymbol, + KnownTaskTypes knownTypes, + CancellationToken cancellationToken) + { + var name = methodSymbol.Name; + var newName = name + AsyncSuffix; + var solution = document.Project.Solution; + + // Store the path to this node. That way we can find it post rename. + var syntaxPath = new SyntaxPath(node); + + // Rename the method to add the 'Async' suffix, then add the 'async' keyword. + var newSolution = await Renamer.RenameSymbolAsync(solution, methodSymbol, new SymbolRenameOptions(), newName, cancellationToken).ConfigureAwait(false); + + var newDocument = newSolution.GetRequiredDocument(document.Id); + var newRoot = await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (syntaxPath.TryResolve(newRoot, out SyntaxNode? newNode)) { - var name = methodSymbol.Name; - var newName = name + AsyncSuffix; - var solution = document.Project.Solution; - - // Store the path to this node. That way we can find it post rename. - var syntaxPath = new SyntaxPath(node); - - // Rename the method to add the 'Async' suffix, then add the 'async' keyword. - var newSolution = await Renamer.RenameSymbolAsync(solution, methodSymbol, new SymbolRenameOptions(), newName, cancellationToken).ConfigureAwait(false); - - var newDocument = newSolution.GetRequiredDocument(document.Id); - var newRoot = await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (syntaxPath.TryResolve(newRoot, out SyntaxNode? newNode)) - { - var semanticModel = await newDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var newMethod = (IMethodSymbol)semanticModel.GetRequiredDeclaredSymbol(newNode, cancellationToken); - return await AddAsyncTokenAsync(keepVoid, newDocument, newMethod, knownTypes, newNode, cancellationToken).ConfigureAwait(false); - } - - return newSolution; + var semanticModel = await newDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var newMethod = (IMethodSymbol)semanticModel.GetRequiredDeclaredSymbol(newNode, cancellationToken); + return await AddAsyncTokenAsync(keepVoid, newDocument, newMethod, knownTypes, newNode, cancellationToken).ConfigureAwait(false); } - private async Task AddAsyncTokenAsync( - bool keepVoid, - Document document, - IMethodSymbol methodSymbol, - KnownTaskTypes knownTypes, - SyntaxNode node, - CancellationToken cancellationToken) - { - var newNode = AddAsyncTokenAndFixReturnType(keepVoid, methodSymbol, node, knownTypes, cancellationToken); + return newSolution; + } + + private async Task AddAsyncTokenAsync( + bool keepVoid, + Document document, + IMethodSymbol methodSymbol, + KnownTaskTypes knownTypes, + SyntaxNode node, + CancellationToken cancellationToken) + { + var newNode = AddAsyncTokenAndFixReturnType(keepVoid, methodSymbol, node, knownTypes, cancellationToken); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newRoot = root.ReplaceNode(node, newNode); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = root.ReplaceNode(node, newNode); - var newDocument = document.WithSyntaxRoot(newRoot); - return newDocument.Project.Solution; - } + var newDocument = document.WithSyntaxRoot(newRoot); + return newDocument.Project.Solution; } } diff --git a/src/Analyzers/Core/CodeFixes/MakeMethodSynchronous/AbstractMakeMethodSynchronousCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/MakeMethodSynchronous/AbstractMakeMethodSynchronousCodeFixProvider.cs index 75097069a6f04..cbecadb7094d4 100644 --- a/src/Analyzers/Core/CodeFixes/MakeMethodSynchronous/AbstractMakeMethodSynchronousCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/MakeMethodSynchronous/AbstractMakeMethodSynchronousCodeFixProvider.cs @@ -17,247 +17,246 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.MakeMethodSynchronous +namespace Microsoft.CodeAnalysis.MakeMethodSynchronous; + +internal abstract class AbstractMakeMethodSynchronousCodeFixProvider : CodeFixProvider { - internal abstract class AbstractMakeMethodSynchronousCodeFixProvider : CodeFixProvider - { - protected abstract bool IsAsyncSupportingFunctionSyntax(SyntaxNode node); - protected abstract SyntaxNode RemoveAsyncTokenAndFixReturnType(IMethodSymbol methodSymbol, SyntaxNode node, KnownTaskTypes knownTypes); + protected abstract bool IsAsyncSupportingFunctionSyntax(SyntaxNode node); + protected abstract SyntaxNode RemoveAsyncTokenAndFixReturnType(IMethodSymbol methodSymbol, SyntaxNode node, KnownTaskTypes knownTypes); - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var cancellationToken = context.CancellationToken; - var diagnostic = context.Diagnostics.First(); - - var token = diagnostic.Location.FindToken(cancellationToken); - var node = token.GetAncestor(IsAsyncSupportingFunctionSyntax); - if (node != null) - { - context.RegisterCodeFix( - CodeAction.Create( - CodeFixesResources.Make_method_synchronous, - cancellationToken => FixNodeAsync(context.Document, node, cancellationToken), - nameof(CodeFixesResources.Make_method_synchronous)), - context.Diagnostics); - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var cancellationToken = context.CancellationToken; + var diagnostic = context.Diagnostics.First(); - return Task.CompletedTask; + var token = diagnostic.Location.FindToken(cancellationToken); + var node = token.GetAncestor(IsAsyncSupportingFunctionSyntax); + if (node != null) + { + context.RegisterCodeFix( + CodeAction.Create( + CodeFixesResources.Make_method_synchronous, + cancellationToken => FixNodeAsync(context.Document, node, cancellationToken), + nameof(CodeFixesResources.Make_method_synchronous)), + context.Diagnostics); } - private const string AsyncSuffix = "Async"; + return Task.CompletedTask; + } - private async Task FixNodeAsync( - Document document, SyntaxNode node, CancellationToken cancellationToken) - { - // See if we're on an actual method declaration (otherwise we're on a lambda declaration). - // If we're on a method declaration, we'll get an IMethodSymbol back. In that case, check - // if it has the 'Async' suffix, and remove that suffix if so. - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var methodSymbol = (IMethodSymbol?)(semanticModel.GetDeclaredSymbol(node, cancellationToken) ?? semanticModel.GetSymbolInfo(node, cancellationToken).GetAnySymbol()); - Contract.ThrowIfNull(methodSymbol); + private const string AsyncSuffix = "Async"; - if (methodSymbol.IsOrdinaryMethodOrLocalFunction() && - methodSymbol.Name.Length > AsyncSuffix.Length && - methodSymbol.Name.EndsWith(AsyncSuffix)) - { - return await RenameThenRemoveAsyncTokenAsync(document, node, methodSymbol, cancellationToken).ConfigureAwait(false); - } - else - { - return await RemoveAsyncTokenAsync(document, methodSymbol, node, cancellationToken).ConfigureAwait(false); - } + private async Task FixNodeAsync( + Document document, SyntaxNode node, CancellationToken cancellationToken) + { + // See if we're on an actual method declaration (otherwise we're on a lambda declaration). + // If we're on a method declaration, we'll get an IMethodSymbol back. In that case, check + // if it has the 'Async' suffix, and remove that suffix if so. + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var methodSymbol = (IMethodSymbol?)(semanticModel.GetDeclaredSymbol(node, cancellationToken) ?? semanticModel.GetSymbolInfo(node, cancellationToken).GetAnySymbol()); + Contract.ThrowIfNull(methodSymbol); + + if (methodSymbol.IsOrdinaryMethodOrLocalFunction() && + methodSymbol.Name.Length > AsyncSuffix.Length && + methodSymbol.Name.EndsWith(AsyncSuffix)) + { + return await RenameThenRemoveAsyncTokenAsync(document, node, methodSymbol, cancellationToken).ConfigureAwait(false); } - - private async Task RenameThenRemoveAsyncTokenAsync(Document document, SyntaxNode node, IMethodSymbol methodSymbol, CancellationToken cancellationToken) + else { - var name = methodSymbol.Name; - var newName = name[..^AsyncSuffix.Length]; - var solution = document.Project.Solution; - - // Store the path to this node. That way we can find it post rename. - var syntaxPath = new SyntaxPath(node); - - // Rename the method to remove the 'Async' suffix, then remove the 'async' keyword. - var newSolution = await Renamer.RenameSymbolAsync(solution, methodSymbol, new SymbolRenameOptions(), newName, cancellationToken).ConfigureAwait(false); - var newDocument = newSolution.GetRequiredDocument(document.Id); - var newRoot = await newDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (syntaxPath.TryResolve(newRoot, out SyntaxNode? newNode)) - { - var semanticModel = await newDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var newMethod = (IMethodSymbol)semanticModel.GetRequiredDeclaredSymbol(newNode, cancellationToken); - return await RemoveAsyncTokenAsync(newDocument, newMethod, newNode, cancellationToken).ConfigureAwait(false); - } - - return newSolution; + return await RemoveAsyncTokenAsync(document, methodSymbol, node, cancellationToken).ConfigureAwait(false); } + } - private async Task RemoveAsyncTokenAsync( - Document document, IMethodSymbol methodSymbol, SyntaxNode node, CancellationToken cancellationToken) + private async Task RenameThenRemoveAsyncTokenAsync(Document document, SyntaxNode node, IMethodSymbol methodSymbol, CancellationToken cancellationToken) + { + var name = methodSymbol.Name; + var newName = name[..^AsyncSuffix.Length]; + var solution = document.Project.Solution; + + // Store the path to this node. That way we can find it post rename. + var syntaxPath = new SyntaxPath(node); + + // Rename the method to remove the 'Async' suffix, then remove the 'async' keyword. + var newSolution = await Renamer.RenameSymbolAsync(solution, methodSymbol, new SymbolRenameOptions(), newName, cancellationToken).ConfigureAwait(false); + var newDocument = newSolution.GetRequiredDocument(document.Id); + var newRoot = await newDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (syntaxPath.TryResolve(newRoot, out SyntaxNode? newNode)) { - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var knownTypes = new KnownTaskTypes(compilation); + var semanticModel = await newDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var newMethod = (IMethodSymbol)semanticModel.GetRequiredDeclaredSymbol(newNode, cancellationToken); + return await RemoveAsyncTokenAsync(newDocument, newMethod, newNode, cancellationToken).ConfigureAwait(false); + } - var annotation = new SyntaxAnnotation(); - var newNode = RemoveAsyncTokenAndFixReturnType(methodSymbol, node, knownTypes) - .WithAdditionalAnnotations(Formatter.Annotation, annotation); + return newSolution; + } - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newRoot = root.ReplaceNode(node, newNode); + private async Task RemoveAsyncTokenAsync( + Document document, IMethodSymbol methodSymbol, SyntaxNode node, CancellationToken cancellationToken) + { + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var knownTypes = new KnownTaskTypes(compilation); - var newDocument = document.WithSyntaxRoot(newRoot); - var newSolution = newDocument.Project.Solution; + var annotation = new SyntaxAnnotation(); + var newNode = RemoveAsyncTokenAndFixReturnType(methodSymbol, node, knownTypes) + .WithAdditionalAnnotations(Formatter.Annotation, annotation); - if (!methodSymbol.IsOrdinaryMethodOrLocalFunction()) - return newSolution; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = root.ReplaceNode(node, newNode); - return await RemoveAwaitFromCallersAsync( - newDocument, annotation, cancellationToken).ConfigureAwait(false); - } + var newDocument = document.WithSyntaxRoot(newRoot); + var newSolution = newDocument.Project.Solution; + + if (!methodSymbol.IsOrdinaryMethodOrLocalFunction()) + return newSolution; - private static async Task RemoveAwaitFromCallersAsync( - Document document, SyntaxAnnotation annotation, CancellationToken cancellationToken) + return await RemoveAwaitFromCallersAsync( + newDocument, annotation, cancellationToken).ConfigureAwait(false); + } + + private static async Task RemoveAwaitFromCallersAsync( + Document document, SyntaxAnnotation annotation, CancellationToken cancellationToken) + { + var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var methodDeclaration = syntaxRoot.GetAnnotatedNodes(annotation).FirstOrDefault(); + if (methodDeclaration != null) { - var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var methodDeclaration = syntaxRoot.GetAnnotatedNodes(annotation).FirstOrDefault(); - if (methodDeclaration != null) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken) is IMethodSymbol methodSymbol) - { + if (semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken) is IMethodSymbol methodSymbol) + { #if CODE_STYLE - var references = await SymbolFinder.FindReferencesAsync( - methodSymbol, document.Project.Solution, cancellationToken).ConfigureAwait(false); + var references = await SymbolFinder.FindReferencesAsync( + methodSymbol, document.Project.Solution, cancellationToken).ConfigureAwait(false); #else - var references = await SymbolFinder.FindRenamableReferencesAsync( - [methodSymbol], document.Project.Solution, cancellationToken).ConfigureAwait(false); + var references = await SymbolFinder.FindRenamableReferencesAsync( + [methodSymbol], document.Project.Solution, cancellationToken).ConfigureAwait(false); #endif - var referencedSymbol = references.FirstOrDefault(r => Equals(r.Definition, methodSymbol)); - if (referencedSymbol != null) - { - return await RemoveAwaitFromCallersAsync( - document.Project.Solution, referencedSymbol.Locations.ToImmutableArray(), cancellationToken).ConfigureAwait(false); - } + var referencedSymbol = references.FirstOrDefault(r => Equals(r.Definition, methodSymbol)); + if (referencedSymbol != null) + { + return await RemoveAwaitFromCallersAsync( + document.Project.Solution, referencedSymbol.Locations.ToImmutableArray(), cancellationToken).ConfigureAwait(false); } } - - return document.Project.Solution; } - private static async Task RemoveAwaitFromCallersAsync( - Solution solution, ImmutableArray locations, CancellationToken cancellationToken) - { - var currentSolution = solution; + return document.Project.Solution; + } - var groupedLocations = locations.GroupBy(loc => loc.Document); + private static async Task RemoveAwaitFromCallersAsync( + Solution solution, ImmutableArray locations, CancellationToken cancellationToken) + { + var currentSolution = solution; - foreach (var group in groupedLocations) - { - currentSolution = await RemoveAwaitFromCallersAsync( - currentSolution, group, cancellationToken).ConfigureAwait(false); - } + var groupedLocations = locations.GroupBy(loc => loc.Document); - return currentSolution; + foreach (var group in groupedLocations) + { + currentSolution = await RemoveAwaitFromCallersAsync( + currentSolution, group, cancellationToken).ConfigureAwait(false); } - private static async Task RemoveAwaitFromCallersAsync( - Solution currentSolution, IGrouping group, CancellationToken cancellationToken) - { - var document = group.Key; - var syntaxFactsService = document.GetRequiredLanguageService(); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + return currentSolution; + } - var editor = new SyntaxEditor(root, currentSolution.Services); + private static async Task RemoveAwaitFromCallersAsync( + Solution currentSolution, IGrouping group, CancellationToken cancellationToken) + { + var document = group.Key; + var syntaxFactsService = document.GetRequiredLanguageService(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - foreach (var location in group) - { - RemoveAwaitFromCallerIfPresent(editor, syntaxFactsService, root, location, cancellationToken); - } + var editor = new SyntaxEditor(root, currentSolution.Services); - var newRoot = editor.GetChangedRoot(); - return currentSolution.WithDocumentSyntaxRoot(document.Id, newRoot); + foreach (var location in group) + { + RemoveAwaitFromCallerIfPresent(editor, syntaxFactsService, root, location, cancellationToken); } - private static void RemoveAwaitFromCallerIfPresent( - SyntaxEditor editor, ISyntaxFactsService syntaxFacts, - SyntaxNode root, ReferenceLocation referenceLocation, - CancellationToken cancellationToken) + var newRoot = editor.GetChangedRoot(); + return currentSolution.WithDocumentSyntaxRoot(document.Id, newRoot); + } + + private static void RemoveAwaitFromCallerIfPresent( + SyntaxEditor editor, ISyntaxFactsService syntaxFacts, + SyntaxNode root, ReferenceLocation referenceLocation, + CancellationToken cancellationToken) + { + if (referenceLocation.IsImplicit) { - if (referenceLocation.IsImplicit) - { - return; - } + return; + } - var location = referenceLocation.Location; - var token = location.FindToken(cancellationToken); + var location = referenceLocation.Location; + var token = location.FindToken(cancellationToken); - var nameNode = token.Parent; - if (nameNode == null) - { - return; - } + var nameNode = token.Parent; + if (nameNode == null) + { + return; + } - // Look for the following forms: - // await M(...) - // await .M(...) - // await M(...).ConfigureAwait(...) - // await .M(...).ConfigureAwait(...) + // Look for the following forms: + // await M(...) + // await .M(...) + // await M(...).ConfigureAwait(...) + // await .M(...).ConfigureAwait(...) - var expressionNode = nameNode; - if (syntaxFacts.IsNameOfSimpleMemberAccessExpression(nameNode) || - syntaxFacts.IsNameOfMemberBindingExpression(nameNode)) - { - expressionNode = nameNode.Parent; - } + var expressionNode = nameNode; + if (syntaxFacts.IsNameOfSimpleMemberAccessExpression(nameNode) || + syntaxFacts.IsNameOfMemberBindingExpression(nameNode)) + { + expressionNode = nameNode.Parent; + } - if (!syntaxFacts.IsExpressionOfInvocationExpression(expressionNode)) - { - return; - } + if (!syntaxFacts.IsExpressionOfInvocationExpression(expressionNode)) + { + return; + } - // We now either have M(...) or .M(...) + // We now either have M(...) or .M(...) - var invocationExpression = expressionNode.Parent; - Debug.Assert(syntaxFacts.IsInvocationExpression(invocationExpression)); + var invocationExpression = expressionNode.Parent; + Debug.Assert(syntaxFacts.IsInvocationExpression(invocationExpression)); - if (syntaxFacts.IsExpressionOfAwaitExpression(invocationExpression)) - { - // Handle the case where we're directly awaited. - var awaitExpression = invocationExpression.GetRequiredParent(); - editor.ReplaceNode(awaitExpression, (currentAwaitExpression, generator) => - syntaxFacts.GetExpressionOfAwaitExpression(currentAwaitExpression) - .WithTriviaFrom(currentAwaitExpression)); - } - else if (syntaxFacts.IsExpressionOfMemberAccessExpression(invocationExpression)) - { - // Check for the .ConfigureAwait case. - var parentMemberAccessExpression = invocationExpression.GetRequiredParent(); - var parentMemberAccessExpressionNameNode = syntaxFacts.GetNameOfMemberAccessExpression(parentMemberAccessExpression); + if (syntaxFacts.IsExpressionOfAwaitExpression(invocationExpression)) + { + // Handle the case where we're directly awaited. + var awaitExpression = invocationExpression.GetRequiredParent(); + editor.ReplaceNode(awaitExpression, (currentAwaitExpression, generator) => + syntaxFacts.GetExpressionOfAwaitExpression(currentAwaitExpression) + .WithTriviaFrom(currentAwaitExpression)); + } + else if (syntaxFacts.IsExpressionOfMemberAccessExpression(invocationExpression)) + { + // Check for the .ConfigureAwait case. + var parentMemberAccessExpression = invocationExpression.GetRequiredParent(); + var parentMemberAccessExpressionNameNode = syntaxFacts.GetNameOfMemberAccessExpression(parentMemberAccessExpression); - var parentMemberAccessExpressionName = syntaxFacts.GetIdentifierOfSimpleName(parentMemberAccessExpressionNameNode).ValueText; - if (parentMemberAccessExpressionName == nameof(Task.ConfigureAwait)) + var parentMemberAccessExpressionName = syntaxFacts.GetIdentifierOfSimpleName(parentMemberAccessExpressionNameNode).ValueText; + if (parentMemberAccessExpressionName == nameof(Task.ConfigureAwait)) + { + var parentExpression = parentMemberAccessExpression.Parent; + if (syntaxFacts.IsExpressionOfAwaitExpression(parentExpression)) { - var parentExpression = parentMemberAccessExpression.Parent; - if (syntaxFacts.IsExpressionOfAwaitExpression(parentExpression)) + var awaitExpression = parentExpression.GetRequiredParent(); + editor.ReplaceNode(awaitExpression, (currentAwaitExpression, generator) => { - var awaitExpression = parentExpression.GetRequiredParent(); - editor.ReplaceNode(awaitExpression, (currentAwaitExpression, generator) => - { - var currentConfigureAwaitInvocation = syntaxFacts.GetExpressionOfAwaitExpression(currentAwaitExpression); - var currentMemberAccess = syntaxFacts.GetExpressionOfInvocationExpression(currentConfigureAwaitInvocation); - var currentInvocationExpression = syntaxFacts.GetExpressionOfMemberAccessExpression(currentMemberAccess); - Contract.ThrowIfNull(currentInvocationExpression); - - return currentInvocationExpression.WithTriviaFrom(currentAwaitExpression); - }); - } + var currentConfigureAwaitInvocation = syntaxFacts.GetExpressionOfAwaitExpression(currentAwaitExpression); + var currentMemberAccess = syntaxFacts.GetExpressionOfInvocationExpression(currentConfigureAwaitInvocation); + var currentInvocationExpression = syntaxFacts.GetExpressionOfMemberAccessExpression(currentMemberAccess); + Contract.ThrowIfNull(currentInvocationExpression); + + return currentInvocationExpression.WithTriviaFrom(currentAwaitExpression); + }); } } } diff --git a/src/Analyzers/Core/CodeFixes/MakeTypeAbstract/AbstractMakeTypeAbstractCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/MakeTypeAbstract/AbstractMakeTypeAbstractCodeFixProvider.cs index f3b51b2dccb03..4d9aaad35bcb2 100644 --- a/src/Analyzers/Core/CodeFixes/MakeTypeAbstract/AbstractMakeTypeAbstractCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/MakeTypeAbstract/AbstractMakeTypeAbstractCodeFixProvider.cs @@ -12,36 +12,35 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.MakeTypeAbstract +namespace Microsoft.CodeAnalysis.MakeTypeAbstract; + +internal abstract class AbstractMakeTypeAbstractCodeFixProvider : SyntaxEditorBasedCodeFixProvider + where TTypeDeclarationSyntax : SyntaxNode { - internal abstract class AbstractMakeTypeAbstractCodeFixProvider : SyntaxEditorBasedCodeFixProvider - where TTypeDeclarationSyntax : SyntaxNode - { - protected abstract bool IsValidRefactoringContext(SyntaxNode? node, [NotNullWhen(true)] out TTypeDeclarationSyntax? typeDeclaration); + protected abstract bool IsValidRefactoringContext(SyntaxNode? node, [NotNullWhen(true)] out TTypeDeclarationSyntax? typeDeclaration); - public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + if (IsValidRefactoringContext(context.Diagnostics[0].Location?.FindNode(context.CancellationToken), out _)) { - if (IsValidRefactoringContext(context.Diagnostics[0].Location?.FindNode(context.CancellationToken), out _)) - { - RegisterCodeFix(context, CodeFixesResources.Make_class_abstract, CodeFixesResources.Make_class_abstract); - } - - return Task.CompletedTask; + RegisterCodeFix(context, CodeFixesResources.Make_class_abstract, CodeFixesResources.Make_class_abstract); } - protected sealed override Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + return Task.CompletedTask; + } + + protected sealed override Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + for (var i = 0; i < diagnostics.Length; i++) { - for (var i = 0; i < diagnostics.Length; i++) + if (IsValidRefactoringContext(diagnostics[i].Location?.FindNode(cancellationToken), out var typeDeclaration)) { - if (IsValidRefactoringContext(diagnostics[i].Location?.FindNode(cancellationToken), out var typeDeclaration)) - { - editor.ReplaceNode(typeDeclaration, - (currentTypeDeclaration, generator) => generator.WithModifiers(currentTypeDeclaration, generator.GetModifiers(currentTypeDeclaration).WithIsAbstract(true))); - } + editor.ReplaceNode(typeDeclaration, + (currentTypeDeclaration, generator) => generator.WithModifiers(currentTypeDeclaration, generator.GetModifiers(currentTypeDeclaration).WithIsAbstract(true))); } - - return Task.CompletedTask; } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/Core/CodeFixes/MakeTypePartial/AbstractMakeTypePartialCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/MakeTypePartial/AbstractMakeTypePartialCodeFixProvider.cs index 2956cdcd4f1a5..d72b9da4503cb 100644 --- a/src/Analyzers/Core/CodeFixes/MakeTypePartial/AbstractMakeTypePartialCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/MakeTypePartial/AbstractMakeTypePartialCodeFixProvider.cs @@ -11,48 +11,47 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.MakeTypePartial +namespace Microsoft.CodeAnalysis.MakeTypePartial; + +internal abstract class AbstractMakeTypePartialCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - internal abstract class AbstractMakeTypePartialCodeFixProvider : SyntaxEditorBasedCodeFixProvider + protected AbstractMakeTypePartialCodeFixProvider() + : base(supportsFixAll: false) { - protected AbstractMakeTypePartialCodeFixProvider() - : base(supportsFixAll: false) - { - } + } - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CodeFixesResources.Make_type_partial, nameof(CodeFixesResources.Make_type_partial)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, CodeFixesResources.Make_type_partial, nameof(CodeFixesResources.Make_type_partial)); + return Task.CompletedTask; + } + + protected override async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var syntaxRoot = editor.OriginalRoot; + var generator = editor.Generator; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - protected override async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + foreach (var diagnostic in diagnostics) { - var syntaxRoot = editor.OriginalRoot; - var generator = editor.Generator; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var declaration = syntaxRoot.FindNode(diagnostic.Location.SourceSpan); + var symbol = semanticModel.GetDeclaredSymbol(declaration, cancellationToken); - foreach (var diagnostic in diagnostics) + if (symbol is null) { - var declaration = syntaxRoot.FindNode(diagnostic.Location.SourceSpan); - var symbol = semanticModel.GetDeclaredSymbol(declaration, cancellationToken); + Debug.Fail("Declared symbol must never be null here"); + continue; + } - if (symbol is null) - { - Debug.Fail("Declared symbol must never be null here"); - continue; - } + foreach (var reference in symbol.DeclaringSyntaxReferences) + { + var node = await reference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + var modifiers = generator.GetModifiers(node); - foreach (var reference in symbol.DeclaringSyntaxReferences) + if (!modifiers.IsPartial) { - var node = await reference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); - var modifiers = generator.GetModifiers(node); - - if (!modifiers.IsPartial) - { - var fixedModifiers = modifiers.WithPartial(true); - editor.ReplaceNode(node, generator.WithModifiers(node, fixedModifiers)); - } + var fixedModifiers = modifiers.WithPartial(true); + editor.ReplaceNode(node, generator.WithModifiers(node, fixedModifiers)); } } } diff --git a/src/Analyzers/Core/CodeFixes/MatchFolderAndNamespace/AbstractChangeNamespaceToMatchFolderCodeFixProvider.CustomFixAllProvider.cs b/src/Analyzers/Core/CodeFixes/MatchFolderAndNamespace/AbstractChangeNamespaceToMatchFolderCodeFixProvider.CustomFixAllProvider.cs index 5c0b76d7d9e92..b8455ab05fb41 100644 --- a/src/Analyzers/Core/CodeFixes/MatchFolderAndNamespace/AbstractChangeNamespaceToMatchFolderCodeFixProvider.CustomFixAllProvider.cs +++ b/src/Analyzers/Core/CodeFixes/MatchFolderAndNamespace/AbstractChangeNamespaceToMatchFolderCodeFixProvider.CustomFixAllProvider.cs @@ -11,94 +11,93 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.MatchFolderAndNamespace +namespace Microsoft.CodeAnalysis.CodeFixes.MatchFolderAndNamespace; + +/// +/// Custom fix all provider for namespace sync. Does fix all on per document level. Since +/// multiple documents may be updated when changing a single namespace, it happens +/// on a sequential level instead of batch fixing and merging the changes. This prevents +/// collisions that the batch fixer won't handle correctly but is slower. +/// +internal abstract partial class AbstractChangeNamespaceToMatchFolderCodeFixProvider { - /// - /// Custom fix all provider for namespace sync. Does fix all on per document level. Since - /// multiple documents may be updated when changing a single namespace, it happens - /// on a sequential level instead of batch fixing and merging the changes. This prevents - /// collisions that the batch fixer won't handle correctly but is slower. - /// - internal abstract partial class AbstractChangeNamespaceToMatchFolderCodeFixProvider + private class CustomFixAllProvider : FixAllProvider { - private class CustomFixAllProvider : FixAllProvider - { - public static readonly CustomFixAllProvider Instance = new(); + public static readonly CustomFixAllProvider Instance = new(); - public override async Task GetFixAsync(FixAllContext fixAllContext) + public override async Task GetFixAsync(FixAllContext fixAllContext) + { + var diagnostics = fixAllContext.Scope switch { - var diagnostics = fixAllContext.Scope switch - { - FixAllScope.Document when fixAllContext.Document is not null => await fixAllContext.GetDocumentDiagnosticsAsync(fixAllContext.Document).ConfigureAwait(false), - FixAllScope.Project => await fixAllContext.GetAllDiagnosticsAsync(fixAllContext.Project).ConfigureAwait(false), - FixAllScope.Solution => await GetSolutionDiagnosticsAsync(fixAllContext).ConfigureAwait(false), - _ => default - }; + FixAllScope.Document when fixAllContext.Document is not null => await fixAllContext.GetDocumentDiagnosticsAsync(fixAllContext.Document).ConfigureAwait(false), + FixAllScope.Project => await fixAllContext.GetAllDiagnosticsAsync(fixAllContext.Project).ConfigureAwait(false), + FixAllScope.Solution => await GetSolutionDiagnosticsAsync(fixAllContext).ConfigureAwait(false), + _ => default + }; - if (diagnostics.IsDefaultOrEmpty) - return null; + if (diagnostics.IsDefaultOrEmpty) + return null; - var title = fixAllContext.GetDefaultFixAllTitle(); - return CodeAction.Create( - title, - cancellationToken => FixAllByDocumentAsync( - fixAllContext.Project.Solution, - diagnostics, - fixAllContext.Progress, + var title = fixAllContext.GetDefaultFixAllTitle(); + return CodeAction.Create( + title, + cancellationToken => FixAllByDocumentAsync( + fixAllContext.Project.Solution, + diagnostics, + fixAllContext.Progress, #if CODE_STYLE - CodeActionOptions.DefaultProvider, + CodeActionOptions.DefaultProvider, #else - fixAllContext.State.CodeActionOptionsProvider, + fixAllContext.State.CodeActionOptionsProvider, #endif - cancellationToken), - title); + cancellationToken), + title); - static async Task> GetSolutionDiagnosticsAsync(FixAllContext fixAllContext) - { - var diagnostics = ImmutableArray.CreateBuilder(); - - foreach (var project in fixAllContext.Solution.Projects) - { - var projectDiagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false); - diagnostics.AddRange(projectDiagnostics); - } + static async Task> GetSolutionDiagnosticsAsync(FixAllContext fixAllContext) + { + var diagnostics = ImmutableArray.CreateBuilder(); - return diagnostics.ToImmutable(); + foreach (var project in fixAllContext.Solution.Projects) + { + var projectDiagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false); + diagnostics.AddRange(projectDiagnostics); } - } - private static async Task FixAllByDocumentAsync( - Solution solution, - ImmutableArray diagnostics, - IProgress progressTracker, - CodeActionOptionsProvider options, - CancellationToken cancellationToken) - { - // Use documentId instead of tree here because the - // FixAsync call can modify more than one document per call. The - // important thing is that the fix works on fixing the namespaces in a single document, - // but references in other documents will be updated to be correct. Id will remain - // across this mutation, but lookup via SyntaxTree directly will not work because - // the tree won't be the same. - var documentIdToDiagnosticsMap = diagnostics - .GroupBy(diagnostic => diagnostic.Location.SourceTree) - .Where(group => group.Key is not null) - .SelectAsArray(group => (id: solution.GetRequiredDocument(group.Key!).Id, diagnostics: group.ToImmutableArray())); + return diagnostics.ToImmutable(); + } + } - var newSolution = solution; + private static async Task FixAllByDocumentAsync( + Solution solution, + ImmutableArray diagnostics, + IProgress progressTracker, + CodeActionOptionsProvider options, + CancellationToken cancellationToken) + { + // Use documentId instead of tree here because the + // FixAsync call can modify more than one document per call. The + // important thing is that the fix works on fixing the namespaces in a single document, + // but references in other documents will be updated to be correct. Id will remain + // across this mutation, but lookup via SyntaxTree directly will not work because + // the tree won't be the same. + var documentIdToDiagnosticsMap = diagnostics + .GroupBy(diagnostic => diagnostic.Location.SourceTree) + .Where(group => group.Key is not null) + .SelectAsArray(group => (id: solution.GetRequiredDocument(group.Key!).Id, diagnostics: group.ToImmutableArray())); - progressTracker.AddItems(documentIdToDiagnosticsMap.Length); + var newSolution = solution; - foreach (var (documentId, diagnosticsInTree) in documentIdToDiagnosticsMap) - { - var document = newSolution.GetRequiredDocument(documentId); - using var _ = progressTracker.ItemCompletedScope(document.Name); + progressTracker.AddItems(documentIdToDiagnosticsMap.Length); - newSolution = await FixAllInDocumentAsync(document, diagnosticsInTree, options, cancellationToken).ConfigureAwait(false); - } + foreach (var (documentId, diagnosticsInTree) in documentIdToDiagnosticsMap) + { + var document = newSolution.GetRequiredDocument(documentId); + using var _ = progressTracker.ItemCompletedScope(document.Name); - return newSolution; + newSolution = await FixAllInDocumentAsync(document, diagnosticsInTree, options, cancellationToken).ConfigureAwait(false); } + + return newSolution; } } } diff --git a/src/Analyzers/Core/CodeFixes/MatchFolderAndNamespace/AbstractChangeNamespaceToMatchFolderCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/MatchFolderAndNamespace/AbstractChangeNamespaceToMatchFolderCodeFixProvider.cs index a38f500e64abf..35e77aeaa8391 100644 --- a/src/Analyzers/Core/CodeFixes/MatchFolderAndNamespace/AbstractChangeNamespaceToMatchFolderCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/MatchFolderAndNamespace/AbstractChangeNamespaceToMatchFolderCodeFixProvider.cs @@ -16,60 +16,59 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.MatchFolderAndNamespace +namespace Microsoft.CodeAnalysis.CodeFixes.MatchFolderAndNamespace; + +internal abstract partial class AbstractChangeNamespaceToMatchFolderCodeFixProvider : CodeFixProvider { - internal abstract partial class AbstractChangeNamespaceToMatchFolderCodeFixProvider : CodeFixProvider - { - public override ImmutableArray FixableDiagnosticIds => [IDEDiagnosticIds.MatchFolderAndNamespaceDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds => [IDEDiagnosticIds.MatchFolderAndNamespaceDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var service = context.Document.Project.Solution.Services.GetRequiredService(); + if (service.CanApplyChange(ApplyChangesKind.ChangeDocumentInfo)) { - var service = context.Document.Project.Solution.Services.GetRequiredService(); - if (service.CanApplyChange(ApplyChangesKind.ChangeDocumentInfo)) - { - context.RegisterCodeFix( - CodeAction.Create( - AnalyzersResources.Change_namespace_to_match_folder_structure, - cancellationToken => FixAllInDocumentAsync(context.Document, context.Diagnostics, - context.GetOptionsProvider(), - cancellationToken), - nameof(AnalyzersResources.Change_namespace_to_match_folder_structure)), - context.Diagnostics); - } - - return Task.CompletedTask; + context.RegisterCodeFix( + CodeAction.Create( + AnalyzersResources.Change_namespace_to_match_folder_structure, + cancellationToken => FixAllInDocumentAsync(context.Document, context.Diagnostics, + context.GetOptionsProvider(), + cancellationToken), + nameof(AnalyzersResources.Change_namespace_to_match_folder_structure)), + context.Diagnostics); } - private static async Task FixAllInDocumentAsync(Document document, ImmutableArray diagnostics, CodeActionOptionsProvider options, CancellationToken cancellationToken) - { - // All the target namespaces should be the same for a given document - Debug.Assert(diagnostics.Select(diagnostic => diagnostic.Properties[MatchFolderAndNamespaceConstants.TargetNamespace]).Distinct().Count() == 1); + return Task.CompletedTask; + } - var targetNamespace = diagnostics.First().Properties[MatchFolderAndNamespaceConstants.TargetNamespace]; - RoslynDebug.AssertNotNull(targetNamespace); + private static async Task FixAllInDocumentAsync(Document document, ImmutableArray diagnostics, CodeActionOptionsProvider options, CancellationToken cancellationToken) + { + // All the target namespaces should be the same for a given document + Debug.Assert(diagnostics.Select(diagnostic => diagnostic.Properties[MatchFolderAndNamespaceConstants.TargetNamespace]).Distinct().Count() == 1); + + var targetNamespace = diagnostics.First().Properties[MatchFolderAndNamespaceConstants.TargetNamespace]; + RoslynDebug.AssertNotNull(targetNamespace); - // Use the Renamer.RenameDocumentAsync API to sync namespaces in the document. This allows - // us to keep in line with the sync methodology that we have as a public API and not have - // to rewrite or move the complex logic. RenameDocumentAsync is designed to behave the same - // as the intent of this analyzer/codefix pair. - var targetFolders = PathMetadataUtilities.BuildFoldersFromNamespace(targetNamespace, document.Project.DefaultNamespace); - var documentWithInvalidFolders = document.WithFolders(document.Folders.Concat("Force-Namespace-Change")); - var renameActionSet = await Renamer.RenameDocumentAsync( - documentWithInvalidFolders, - new DocumentRenameOptions(), + // Use the Renamer.RenameDocumentAsync API to sync namespaces in the document. This allows + // us to keep in line with the sync methodology that we have as a public API and not have + // to rewrite or move the complex logic. RenameDocumentAsync is designed to behave the same + // as the intent of this analyzer/codefix pair. + var targetFolders = PathMetadataUtilities.BuildFoldersFromNamespace(targetNamespace, document.Project.DefaultNamespace); + var documentWithInvalidFolders = document.WithFolders(document.Folders.Concat("Force-Namespace-Change")); + var renameActionSet = await Renamer.RenameDocumentAsync( + documentWithInvalidFolders, + new DocumentRenameOptions(), #if !CODE_STYLE - options, + options, #endif - documentWithInvalidFolders.Name, - newDocumentFolders: targetFolders, - cancellationToken: cancellationToken).ConfigureAwait(false); + documentWithInvalidFolders.Name, + newDocumentFolders: targetFolders, + cancellationToken: cancellationToken).ConfigureAwait(false); - var newSolution = await renameActionSet.UpdateSolutionAsync(documentWithInvalidFolders.Project.Solution, cancellationToken).ConfigureAwait(false); - Debug.Assert(newSolution != document.Project.Solution); - return newSolution; - } - - public override FixAllProvider? GetFixAllProvider() - => CustomFixAllProvider.Instance; + var newSolution = await renameActionSet.UpdateSolutionAsync(documentWithInvalidFolders.Project.Solution, cancellationToken).ConfigureAwait(false); + Debug.Assert(newSolution != document.Project.Solution); + return newSolution; } + + public override FixAllProvider? GetFixAllProvider() + => CustomFixAllProvider.Instance; } diff --git a/src/Analyzers/Core/CodeFixes/OrderModifiers/AbstractOrderModifiersCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/OrderModifiers/AbstractOrderModifiersCodeFixProvider.cs index c8581b249d229..b264cdb40a6c5 100644 --- a/src/Analyzers/Core/CodeFixes/OrderModifiers/AbstractOrderModifiersCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/OrderModifiers/AbstractOrderModifiersCodeFixProvider.cs @@ -17,73 +17,72 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.OrderModifiers +namespace Microsoft.CodeAnalysis.OrderModifiers; + +internal abstract class AbstractOrderModifiersCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - internal abstract class AbstractOrderModifiersCodeFixProvider : SyntaxEditorBasedCodeFixProvider + private readonly ISyntaxFacts _syntaxFacts; + private readonly AbstractOrderModifiersHelpers _helpers; + + protected AbstractOrderModifiersCodeFixProvider( + ISyntaxFacts syntaxFacts, + AbstractOrderModifiersHelpers helpers) { - private readonly ISyntaxFacts _syntaxFacts; - private readonly AbstractOrderModifiersHelpers _helpers; + _syntaxFacts = syntaxFacts; + _helpers = helpers; + } - protected AbstractOrderModifiersCodeFixProvider( - ISyntaxFacts syntaxFacts, - AbstractOrderModifiersHelpers helpers) - { - _syntaxFacts = syntaxFacts; - _helpers = helpers; - } + protected abstract ImmutableArray FixableCompilerErrorIds { get; } + protected abstract CodeStyleOption2 GetCodeStyleOption(AnalyzerOptionsProvider options); - protected abstract ImmutableArray FixableCompilerErrorIds { get; } - protected abstract CodeStyleOption2 GetCodeStyleOption(AnalyzerOptionsProvider options); + public sealed override ImmutableArray FixableDiagnosticIds + => FixableCompilerErrorIds.Add(IDEDiagnosticIds.OrderModifiersDiagnosticId); - public sealed override ImmutableArray FixableDiagnosticIds - => FixableCompilerErrorIds.Add(IDEDiagnosticIds.OrderModifiersDiagnosticId); + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var syntaxTree = await context.Document.GetRequiredSyntaxTreeAsync(context.CancellationToken).ConfigureAwait(false); + var syntaxNode = Location.Create(syntaxTree, context.Span).FindNode(context.CancellationToken); - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + if (_syntaxFacts.GetModifiers(syntaxNode) != default) { - var syntaxTree = await context.Document.GetRequiredSyntaxTreeAsync(context.CancellationToken).ConfigureAwait(false); - var syntaxNode = Location.Create(syntaxTree, context.Span).FindNode(context.CancellationToken); + RegisterCodeFix(context, AnalyzersResources.Order_modifiers, nameof(AnalyzersResources.Order_modifiers)); + } + } - if (_syntaxFacts.GetModifiers(syntaxNode) != default) - { - RegisterCodeFix(context, AnalyzersResources.Order_modifiers, nameof(AnalyzersResources.Order_modifiers)); - } + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var options = await document.GetAnalyzerOptionsProviderAsync(cancellationToken).ConfigureAwait(false); + var option = GetCodeStyleOption(options); + if (!_helpers.TryGetOrComputePreferredOrder(option.Value, out var preferredOrder)) + { + return; } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + foreach (var diagnostic in diagnostics) { - var options = await document.GetAnalyzerOptionsProviderAsync(cancellationToken).ConfigureAwait(false); - var option = GetCodeStyleOption(options); - if (!_helpers.TryGetOrComputePreferredOrder(option.Value, out var preferredOrder)) - { - return; - } + var memberDeclaration = diagnostic.Location.FindNode(cancellationToken); - foreach (var diagnostic in diagnostics) + editor.ReplaceNode(memberDeclaration, (currentNode, _) => { - var memberDeclaration = diagnostic.Location.FindNode(cancellationToken); + var modifiers = _syntaxFacts.GetModifiers(currentNode); + var orderedModifiers = new SyntaxTokenList( + modifiers.OrderBy(CompareModifiers) + .Select((t, i) => t.WithTriviaFrom(modifiers[i]))); - editor.ReplaceNode(memberDeclaration, (currentNode, _) => - { - var modifiers = _syntaxFacts.GetModifiers(currentNode); - var orderedModifiers = new SyntaxTokenList( - modifiers.OrderBy(CompareModifiers) - .Select((t, i) => t.WithTriviaFrom(modifiers[i]))); - - var updatedMemberDeclaration = _syntaxFacts.WithModifiers(currentNode, orderedModifiers); - return updatedMemberDeclaration; - }); - } + var updatedMemberDeclaration = _syntaxFacts.WithModifiers(currentNode, orderedModifiers); + return updatedMemberDeclaration; + }); + } - return; + return; - // Local functions + // Local functions - int CompareModifiers(SyntaxToken t1, SyntaxToken t2) - => GetOrder(t1) - GetOrder(t2); + int CompareModifiers(SyntaxToken t1, SyntaxToken t2) + => GetOrder(t1) - GetOrder(t2); - int GetOrder(SyntaxToken token) - => preferredOrder.TryGetValue(token.RawKind, out var value) ? value : int.MaxValue; - } + int GetOrder(SyntaxToken token) + => preferredOrder.TryGetValue(token.RawKind, out var value) ? value : int.MaxValue; } } diff --git a/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchCodeFixProvider.cs index 985259470c555..f11e3341e79c6 100644 --- a/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchCodeFixProvider.cs @@ -18,216 +18,215 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.PopulateSwitch +namespace Microsoft.CodeAnalysis.PopulateSwitch; + +internal abstract class AbstractPopulateSwitchCodeFixProvider< + TSwitchOperation, + TSwitchSyntax, + TSwitchArmSyntax, + TMemberAccessExpression> + : SyntaxEditorBasedCodeFixProvider + where TSwitchOperation : IOperation + where TSwitchSyntax : SyntaxNode + where TSwitchArmSyntax : SyntaxNode + where TMemberAccessExpression : SyntaxNode { - internal abstract class AbstractPopulateSwitchCodeFixProvider< - TSwitchOperation, - TSwitchSyntax, - TSwitchArmSyntax, - TMemberAccessExpression> - : SyntaxEditorBasedCodeFixProvider - where TSwitchOperation : IOperation - where TSwitchSyntax : SyntaxNode - where TSwitchArmSyntax : SyntaxNode - where TMemberAccessExpression : SyntaxNode - { - public sealed override ImmutableArray FixableDiagnosticIds { get; } + public sealed override ImmutableArray FixableDiagnosticIds { get; } - protected AbstractPopulateSwitchCodeFixProvider(string diagnosticId) - => FixableDiagnosticIds = [diagnosticId]; + protected AbstractPopulateSwitchCodeFixProvider(string diagnosticId) + => FixableDiagnosticIds = [diagnosticId]; - protected abstract ITypeSymbol GetSwitchType(TSwitchOperation switchStatement); - protected abstract ICollection GetMissingEnumMembers(TSwitchOperation switchOperation); - protected abstract bool HasNullSwitchArm(TSwitchOperation switchOperation); + protected abstract ITypeSymbol GetSwitchType(TSwitchOperation switchStatement); + protected abstract ICollection GetMissingEnumMembers(TSwitchOperation switchOperation); + protected abstract bool HasNullSwitchArm(TSwitchOperation switchOperation); - protected abstract TSwitchArmSyntax CreateSwitchArm(SyntaxGenerator generator, Compilation compilation, TMemberAccessExpression caseLabel); - protected abstract TSwitchArmSyntax CreateNullSwitchArm(SyntaxGenerator generator, Compilation compilation); - protected abstract TSwitchArmSyntax CreateDefaultSwitchArm(SyntaxGenerator generator, Compilation compilation); - protected abstract int InsertPosition(TSwitchOperation switchOperation); - protected abstract TSwitchSyntax InsertSwitchArms(SyntaxGenerator generator, TSwitchSyntax switchNode, int insertLocation, List newArms); + protected abstract TSwitchArmSyntax CreateSwitchArm(SyntaxGenerator generator, Compilation compilation, TMemberAccessExpression caseLabel); + protected abstract TSwitchArmSyntax CreateNullSwitchArm(SyntaxGenerator generator, Compilation compilation); + protected abstract TSwitchArmSyntax CreateDefaultSwitchArm(SyntaxGenerator generator, Compilation compilation); + protected abstract int InsertPosition(TSwitchOperation switchOperation); + protected abstract TSwitchSyntax InsertSwitchArms(SyntaxGenerator generator, TSwitchSyntax switchNode, int insertLocation, List newArms); - protected abstract void FixOneDiagnostic( - Document document, SyntaxEditor editor, SemanticModel semanticModel, - bool addCases, bool addDefaultCase, bool onlyOneDiagnostic, - bool hasMissingCases, bool hasMissingDefaultCase, - TSwitchSyntax switchNode, TSwitchOperation switchOperation); + protected abstract void FixOneDiagnostic( + Document document, SyntaxEditor editor, SemanticModel semanticModel, + bool addCases, bool addDefaultCase, bool onlyOneDiagnostic, + bool hasMissingCases, bool hasMissingDefaultCase, + TSwitchSyntax switchNode, TSwitchOperation switchOperation); - public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var diagnostic = context.Diagnostics.First(); - var properties = diagnostic.Properties; - var missingCases = bool.Parse(properties[PopulateSwitchStatementHelpers.MissingCases]!); - var missingDefaultCase = bool.Parse(properties[PopulateSwitchStatementHelpers.MissingDefaultCase]!); - - Debug.Assert(missingCases || missingDefaultCase); - - var document = context.Document; - if (missingCases) - { - context.RegisterCodeFix( - CodeAction.Create( - AnalyzersResources.Add_missing_cases, - c => FixAsync(document, diagnostic, - addCases: true, addDefaultCase: false, - cancellationToken: c), - nameof(AnalyzersResources.Add_missing_cases)), - context.Diagnostics); - } - - if (missingDefaultCase) - { - context.RegisterCodeFix( - CodeAction.Create( - CodeFixesResources.Add_default_case, - c => FixAsync(document, diagnostic, - addCases: false, addDefaultCase: true, - cancellationToken: c), - nameof(CodeFixesResources.Add_default_case)), - context.Diagnostics); - } - - if (missingCases && missingDefaultCase) - { - context.RegisterCodeFix( - CodeAction.Create( - CodeFixesResources.Add_both, - c => FixAsync(document, diagnostic, - addCases: true, addDefaultCase: true, - cancellationToken: c), - nameof(CodeFixesResources.Add_both)), - context.Diagnostics); - } - - return Task.CompletedTask; - } + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); + var properties = diagnostic.Properties; + var missingCases = bool.Parse(properties[PopulateSwitchStatementHelpers.MissingCases]!); + var missingDefaultCase = bool.Parse(properties[PopulateSwitchStatementHelpers.MissingDefaultCase]!); - private Task FixAsync( - Document document, Diagnostic diagnostic, - bool addCases, bool addDefaultCase, - CancellationToken cancellationToken) + Debug.Assert(missingCases || missingDefaultCase); + + var document = context.Document; + if (missingCases) { - return FixAllAsync(document, [diagnostic], - addCases, addDefaultCase, cancellationToken); + context.RegisterCodeFix( + CodeAction.Create( + AnalyzersResources.Add_missing_cases, + c => FixAsync(document, diagnostic, + addCases: true, addDefaultCase: false, + cancellationToken: c), + nameof(AnalyzersResources.Add_missing_cases)), + context.Diagnostics); } - private Task FixAllAsync( - Document document, ImmutableArray diagnostics, - bool addCases, bool addDefaultCase, - CancellationToken cancellationToken) + if (missingDefaultCase) { - return FixAllWithEditorAsync(document, - editor => FixWithEditorAsync(document, editor, diagnostics, addCases, addDefaultCase, cancellationToken), - cancellationToken); + context.RegisterCodeFix( + CodeAction.Create( + CodeFixesResources.Add_default_case, + c => FixAsync(document, diagnostic, + addCases: false, addDefaultCase: true, + cancellationToken: c), + nameof(CodeFixesResources.Add_default_case)), + context.Diagnostics); } - private async Task FixWithEditorAsync( - Document document, SyntaxEditor editor, ImmutableArray diagnostics, - bool addCases, bool addDefaultCase, - CancellationToken cancellationToken) + if (missingCases && missingDefaultCase) { - foreach (var diagnostic in diagnostics) - { - await FixOneDiagnosticAsync( - document, editor, diagnostic, addCases, addDefaultCase, - diagnostics.Length == 1, cancellationToken).ConfigureAwait(false); - } + context.RegisterCodeFix( + CodeAction.Create( + CodeFixesResources.Add_both, + c => FixAsync(document, diagnostic, + addCases: true, addDefaultCase: true, + cancellationToken: c), + nameof(CodeFixesResources.Add_both)), + context.Diagnostics); } - private async Task FixOneDiagnosticAsync( - Document document, SyntaxEditor editor, Diagnostic diagnostic, - bool addCases, bool addDefaultCase, bool onlyOneDiagnostic, - CancellationToken cancellationToken) - { - var hasMissingCases = bool.Parse(diagnostic.Properties[PopulateSwitchStatementHelpers.MissingCases]!); - var hasMissingDefaultCase = bool.Parse(diagnostic.Properties[PopulateSwitchStatementHelpers.MissingDefaultCase]!); + return Task.CompletedTask; + } - var switchLocation = diagnostic.AdditionalLocations[0]; - var switchNode = switchLocation.FindNode(getInnermostNodeForTie: true, cancellationToken) as TSwitchSyntax; - if (switchNode == null) - return; + private Task FixAsync( + Document document, Diagnostic diagnostic, + bool addCases, bool addDefaultCase, + CancellationToken cancellationToken) + { + return FixAllAsync(document, [diagnostic], + addCases, addDefaultCase, cancellationToken); + } - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - // https://github.com/dotnet/roslyn/issues/40505 - var switchStatement = (TSwitchOperation)model.GetOperation(switchNode, cancellationToken)!; + private Task FixAllAsync( + Document document, ImmutableArray diagnostics, + bool addCases, bool addDefaultCase, + CancellationToken cancellationToken) + { + return FixAllWithEditorAsync(document, + editor => FixWithEditorAsync(document, editor, diagnostics, addCases, addDefaultCase, cancellationToken), + cancellationToken); + } - FixOneDiagnostic( - document, editor, model, addCases, addDefaultCase, onlyOneDiagnostic, - hasMissingCases, hasMissingDefaultCase, switchNode, switchStatement); + private async Task FixWithEditorAsync( + Document document, SyntaxEditor editor, ImmutableArray diagnostics, + bool addCases, bool addDefaultCase, + CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) + { + await FixOneDiagnosticAsync( + document, editor, diagnostic, addCases, addDefaultCase, + diagnostics.Length == 1, cancellationToken).ConfigureAwait(false); } + } - protected TSwitchSyntax UpdateSwitchNode( - SyntaxEditor editor, SemanticModel semanticModel, - bool addCases, bool addDefaultCase, - bool hasMissingCases, bool hasMissingDefaultCase, - TSwitchSyntax switchNode, TSwitchOperation switchOperation) - { - var enumType = GetSwitchType(switchOperation); - var isNullable = false; + private async Task FixOneDiagnosticAsync( + Document document, SyntaxEditor editor, Diagnostic diagnostic, + bool addCases, bool addDefaultCase, bool onlyOneDiagnostic, + CancellationToken cancellationToken) + { + var hasMissingCases = bool.Parse(diagnostic.Properties[PopulateSwitchStatementHelpers.MissingCases]!); + var hasMissingDefaultCase = bool.Parse(diagnostic.Properties[PopulateSwitchStatementHelpers.MissingDefaultCase]!); - if (enumType.IsNullable(out var underlyingType)) - { - isNullable = true; - enumType = underlyingType; - } + var switchLocation = diagnostic.AdditionalLocations[0]; + var switchNode = switchLocation.FindNode(getInnermostNodeForTie: true, cancellationToken) as TSwitchSyntax; + if (switchNode == null) + return; - var generator = editor.Generator; + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + // https://github.com/dotnet/roslyn/issues/40505 + var switchStatement = (TSwitchOperation)model.GetOperation(switchNode, cancellationToken)!; - var newArms = new List(); + FixOneDiagnostic( + document, editor, model, addCases, addDefaultCase, onlyOneDiagnostic, + hasMissingCases, hasMissingDefaultCase, switchNode, switchStatement); + } + + protected TSwitchSyntax UpdateSwitchNode( + SyntaxEditor editor, SemanticModel semanticModel, + bool addCases, bool addDefaultCase, + bool hasMissingCases, bool hasMissingDefaultCase, + TSwitchSyntax switchNode, TSwitchOperation switchOperation) + { + var enumType = GetSwitchType(switchOperation); + var isNullable = false; - if (hasMissingCases && addCases) - { - var missingArms = - from e in GetMissingEnumMembers(switchOperation) - let caseLabel = (TMemberAccessExpression)generator.MemberAccessExpression(generator.TypeExpression(enumType), e.Name).WithAdditionalAnnotations(Simplifier.Annotation) - select CreateSwitchArm(generator, semanticModel.Compilation, caseLabel); + if (enumType.IsNullable(out var underlyingType)) + { + isNullable = true; + enumType = underlyingType; + } - newArms.AddRange(missingArms); + var generator = editor.Generator; - if (isNullable && !HasNullSwitchArm(switchOperation)) - newArms.Add(CreateNullSwitchArm(generator, semanticModel.Compilation)); - } + var newArms = new List(); - if (hasMissingDefaultCase && addDefaultCase) - { - // Always add the default clause at the end. - newArms.Add(CreateDefaultSwitchArm(generator, semanticModel.Compilation)); - } + if (hasMissingCases && addCases) + { + var missingArms = + from e in GetMissingEnumMembers(switchOperation) + let caseLabel = (TMemberAccessExpression)generator.MemberAccessExpression(generator.TypeExpression(enumType), e.Name).WithAdditionalAnnotations(Simplifier.Annotation) + select CreateSwitchArm(generator, semanticModel.Compilation, caseLabel); - var insertLocation = InsertPosition(switchOperation); + newArms.AddRange(missingArms); - var newSwitchNode = InsertSwitchArms(generator, switchNode, insertLocation, newArms) - .WithAdditionalAnnotations(Formatter.Annotation); - return newSwitchNode; + if (isNullable && !HasNullSwitchArm(switchOperation)) + newArms.Add(CreateNullSwitchArm(generator, semanticModel.Compilation)); } - protected static void AddMissingBraces( - Document document, - ref SyntaxNode root, - ref TSwitchSyntax switchNode) + if (hasMissingDefaultCase && addDefaultCase) { - // Parsing of the switch may have caused imbalanced braces. i.e. the switch - // may have consumed a brace that was intended for a higher level construct. - // So balance the tree first, then do the switch replacement. - var syntaxFacts = document.GetRequiredLanguageService(); - syntaxFacts.AddFirstMissingCloseBrace( - root, switchNode, out var newRoot, out var newSwitchNode); - - root = newRoot; - switchNode = newSwitchNode; + // Always add the default clause at the end. + newArms.Add(CreateDefaultSwitchArm(generator, semanticModel.Compilation)); } - protected override Task FixAllAsync( - Document document, - ImmutableArray diagnostics, - SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - // If the user is performing a fix-all, then fix up all the issues we see. i.e. - // add missing cases and missing 'default' cases for any switches we reported an - // issue on. - return FixWithEditorAsync(document, editor, diagnostics, - addCases: true, addDefaultCase: true, - cancellationToken: cancellationToken); - } + var insertLocation = InsertPosition(switchOperation); + + var newSwitchNode = InsertSwitchArms(generator, switchNode, insertLocation, newArms) + .WithAdditionalAnnotations(Formatter.Annotation); + return newSwitchNode; + } + + protected static void AddMissingBraces( + Document document, + ref SyntaxNode root, + ref TSwitchSyntax switchNode) + { + // Parsing of the switch may have caused imbalanced braces. i.e. the switch + // may have consumed a brace that was intended for a higher level construct. + // So balance the tree first, then do the switch replacement. + var syntaxFacts = document.GetRequiredLanguageService(); + syntaxFacts.AddFirstMissingCloseBrace( + root, switchNode, out var newRoot, out var newSwitchNode); + + root = newRoot; + switchNode = newSwitchNode; + } + + protected override Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + // If the user is performing a fix-all, then fix up all the issues we see. i.e. + // add missing cases and missing 'default' cases for any switches we reported an + // issue on. + return FixWithEditorAsync(document, editor, diagnostics, + addCases: true, addDefaultCase: true, + cancellationToken: cancellationToken); } } diff --git a/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchExpressionCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchExpressionCodeFixProvider.cs index 21fceb99e256f..5b452e4e68a46 100644 --- a/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchExpressionCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchExpressionCodeFixProvider.cs @@ -10,57 +10,56 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.PopulateSwitch +namespace Microsoft.CodeAnalysis.PopulateSwitch; + +internal abstract class AbstractPopulateSwitchExpressionCodeFixProvider< + TExpressionSyntax, TSwitchSyntax, TSwitchArmSyntax, TMemberAccessExpressionSyntax> + : AbstractPopulateSwitchCodeFixProvider< + ISwitchExpressionOperation, TSwitchSyntax, TSwitchArmSyntax, TMemberAccessExpressionSyntax> + where TExpressionSyntax : SyntaxNode + where TSwitchSyntax : TExpressionSyntax + where TSwitchArmSyntax : SyntaxNode + where TMemberAccessExpressionSyntax : TExpressionSyntax { - internal abstract class AbstractPopulateSwitchExpressionCodeFixProvider< - TExpressionSyntax, TSwitchSyntax, TSwitchArmSyntax, TMemberAccessExpressionSyntax> - : AbstractPopulateSwitchCodeFixProvider< - ISwitchExpressionOperation, TSwitchSyntax, TSwitchArmSyntax, TMemberAccessExpressionSyntax> - where TExpressionSyntax : SyntaxNode - where TSwitchSyntax : TExpressionSyntax - where TSwitchArmSyntax : SyntaxNode - where TMemberAccessExpressionSyntax : TExpressionSyntax + protected AbstractPopulateSwitchExpressionCodeFixProvider() + : base(IDEDiagnosticIds.PopulateSwitchExpressionDiagnosticId) { - protected AbstractPopulateSwitchExpressionCodeFixProvider() - : base(IDEDiagnosticIds.PopulateSwitchExpressionDiagnosticId) - { - } + } - protected sealed override void FixOneDiagnostic( - Document document, SyntaxEditor editor, SemanticModel semanticModel, - bool addCases, bool addDefaultCase, bool onlyOneDiagnostic, - bool hasMissingCases, bool hasMissingDefaultCase, - TSwitchSyntax switchNode, ISwitchExpressionOperation switchExpression) - { - var newSwitchNode = UpdateSwitchNode( - editor, semanticModel, addCases, addDefaultCase, - hasMissingCases, hasMissingDefaultCase, - switchNode, switchExpression).WithAdditionalAnnotations(Formatter.Annotation); + protected sealed override void FixOneDiagnostic( + Document document, SyntaxEditor editor, SemanticModel semanticModel, + bool addCases, bool addDefaultCase, bool onlyOneDiagnostic, + bool hasMissingCases, bool hasMissingDefaultCase, + TSwitchSyntax switchNode, ISwitchExpressionOperation switchExpression) + { + var newSwitchNode = UpdateSwitchNode( + editor, semanticModel, addCases, addDefaultCase, + hasMissingCases, hasMissingDefaultCase, + switchNode, switchExpression).WithAdditionalAnnotations(Formatter.Annotation); - editor.ReplaceNode(switchNode, newSwitchNode); - } + editor.ReplaceNode(switchNode, newSwitchNode); + } - protected sealed override ITypeSymbol GetSwitchType(ISwitchExpressionOperation switchExpression) - => switchExpression.Value.Type ?? throw ExceptionUtilities.Unreachable(); + protected sealed override ITypeSymbol GetSwitchType(ISwitchExpressionOperation switchExpression) + => switchExpression.Value.Type ?? throw ExceptionUtilities.Unreachable(); - protected sealed override ICollection GetMissingEnumMembers(ISwitchExpressionOperation switchOperation) - => PopulateSwitchExpressionHelpers.GetMissingEnumMembers(switchOperation); + protected sealed override ICollection GetMissingEnumMembers(ISwitchExpressionOperation switchOperation) + => PopulateSwitchExpressionHelpers.GetMissingEnumMembers(switchOperation); - protected override bool HasNullSwitchArm(ISwitchExpressionOperation switchOperation) - => PopulateSwitchExpressionHelpers.HasNullSwitchArm(switchOperation); + protected override bool HasNullSwitchArm(ISwitchExpressionOperation switchOperation) + => PopulateSwitchExpressionHelpers.HasNullSwitchArm(switchOperation); - protected static TExpressionSyntax Exception(SyntaxGenerator generator, Compilation compilation) - => (TExpressionSyntax)generator.CreateThrowNotImplementedExpression(compilation); + protected static TExpressionSyntax Exception(SyntaxGenerator generator, Compilation compilation) + => (TExpressionSyntax)generator.CreateThrowNotImplementedExpression(compilation); - protected sealed override int InsertPosition(ISwitchExpressionOperation switchExpression) - { - // If the last section has a default label, then we want to be above that. - // Otherwise, we just get inserted at the end. + protected sealed override int InsertPosition(ISwitchExpressionOperation switchExpression) + { + // If the last section has a default label, then we want to be above that. + // Otherwise, we just get inserted at the end. - var arms = switchExpression.Arms; - return arms.Length > 0 && PopulateSwitchExpressionHelpers.IsDefault(arms[^1]) - ? arms.Length - 1 - : arms.Length; - } + var arms = switchExpression.Arms; + return arms.Length > 0 && PopulateSwitchExpressionHelpers.IsDefault(arms[^1]) + ? arms.Length - 1 + : arms.Length; } } diff --git a/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchStatementCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchStatementCodeFixProvider.cs index a822e00a760ff..9b8fbb8ed8995 100644 --- a/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchStatementCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/PopulateSwitch/AbstractPopulateSwitchStatementCodeFixProvider.cs @@ -12,87 +12,86 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.PopulateSwitch +namespace Microsoft.CodeAnalysis.PopulateSwitch; + +internal abstract class AbstractPopulateSwitchStatementCodeFixProvider< + TSwitchSyntax, TSwitchArmSyntax, TMemberAccessExpression> : + AbstractPopulateSwitchCodeFixProvider< + ISwitchOperation, TSwitchSyntax, TSwitchArmSyntax, TMemberAccessExpression> + where TSwitchSyntax : SyntaxNode + where TSwitchArmSyntax : SyntaxNode + where TMemberAccessExpression : SyntaxNode { - internal abstract class AbstractPopulateSwitchStatementCodeFixProvider< - TSwitchSyntax, TSwitchArmSyntax, TMemberAccessExpression> : - AbstractPopulateSwitchCodeFixProvider< - ISwitchOperation, TSwitchSyntax, TSwitchArmSyntax, TMemberAccessExpression> - where TSwitchSyntax : SyntaxNode - where TSwitchArmSyntax : SyntaxNode - where TMemberAccessExpression : SyntaxNode + protected AbstractPopulateSwitchStatementCodeFixProvider() + : base(IDEDiagnosticIds.PopulateSwitchStatementDiagnosticId) { - protected AbstractPopulateSwitchStatementCodeFixProvider() - : base(IDEDiagnosticIds.PopulateSwitchStatementDiagnosticId) - { - } + } - protected sealed override void FixOneDiagnostic( - Document document, SyntaxEditor editor, SemanticModel semanticModel, - bool addCases, bool addDefaultCase, bool onlyOneDiagnostic, - bool hasMissingCases, bool hasMissingDefaultCase, - TSwitchSyntax switchNode, ISwitchOperation switchOperation) + protected sealed override void FixOneDiagnostic( + Document document, SyntaxEditor editor, SemanticModel semanticModel, + bool addCases, bool addDefaultCase, bool onlyOneDiagnostic, + bool hasMissingCases, bool hasMissingDefaultCase, + TSwitchSyntax switchNode, ISwitchOperation switchOperation) + { + var newSwitchNode = UpdateSwitchNode( + editor, semanticModel, addCases, addDefaultCase, + hasMissingCases, hasMissingDefaultCase, + switchNode, switchOperation).WithAdditionalAnnotations(Formatter.Annotation); + + if (onlyOneDiagnostic) { - var newSwitchNode = UpdateSwitchNode( - editor, semanticModel, addCases, addDefaultCase, - hasMissingCases, hasMissingDefaultCase, - switchNode, switchOperation).WithAdditionalAnnotations(Formatter.Annotation); + // If we're only fixing up one issue in this document, then also make sure we + // didn't cause any braces to be imbalanced when we added members to the switch. + // Note: i'm only doing this for the single case because it feels too complex + // to try to support this during fix-all. + var root = editor.OriginalRoot; + AddMissingBraces(document, ref root, ref switchNode); - if (onlyOneDiagnostic) - { - // If we're only fixing up one issue in this document, then also make sure we - // didn't cause any braces to be imbalanced when we added members to the switch. - // Note: i'm only doing this for the single case because it feels too complex - // to try to support this during fix-all. - var root = editor.OriginalRoot; - AddMissingBraces(document, ref root, ref switchNode); - - var newRoot = root.ReplaceNode(switchNode, newSwitchNode); - editor.ReplaceNode(editor.OriginalRoot, newRoot); - } - else - { - editor.ReplaceNode(switchNode, newSwitchNode); - } + var newRoot = root.ReplaceNode(switchNode, newSwitchNode); + editor.ReplaceNode(editor.OriginalRoot, newRoot); + } + else + { + editor.ReplaceNode(switchNode, newSwitchNode); } + } - protected sealed override ITypeSymbol GetSwitchType(ISwitchOperation switchOperation) - => switchOperation.Value.Type ?? throw ExceptionUtilities.Unreachable(); + protected sealed override ITypeSymbol GetSwitchType(ISwitchOperation switchOperation) + => switchOperation.Value.Type ?? throw ExceptionUtilities.Unreachable(); - protected sealed override ICollection GetMissingEnumMembers(ISwitchOperation switchOperation) - => PopulateSwitchStatementHelpers.GetMissingEnumMembers(switchOperation); + protected sealed override ICollection GetMissingEnumMembers(ISwitchOperation switchOperation) + => PopulateSwitchStatementHelpers.GetMissingEnumMembers(switchOperation); - protected sealed override bool HasNullSwitchArm(ISwitchOperation switchOperation) - => PopulateSwitchStatementHelpers.HasNullSwitchArm(switchOperation); + protected sealed override bool HasNullSwitchArm(ISwitchOperation switchOperation) + => PopulateSwitchStatementHelpers.HasNullSwitchArm(switchOperation); - protected sealed override TSwitchSyntax InsertSwitchArms(SyntaxGenerator generator, TSwitchSyntax switchNode, int insertLocation, List newArms) - => (TSwitchSyntax)generator.InsertSwitchSections(switchNode, insertLocation, newArms); + protected sealed override TSwitchSyntax InsertSwitchArms(SyntaxGenerator generator, TSwitchSyntax switchNode, int insertLocation, List newArms) + => (TSwitchSyntax)generator.InsertSwitchSections(switchNode, insertLocation, newArms); - protected sealed override TSwitchArmSyntax CreateDefaultSwitchArm(SyntaxGenerator generator, Compilation compilation) - => (TSwitchArmSyntax)generator.DefaultSwitchSection([generator.ExitSwitchStatement()]); + protected sealed override TSwitchArmSyntax CreateDefaultSwitchArm(SyntaxGenerator generator, Compilation compilation) + => (TSwitchArmSyntax)generator.DefaultSwitchSection([generator.ExitSwitchStatement()]); - protected sealed override TSwitchArmSyntax CreateSwitchArm(SyntaxGenerator generator, Compilation compilation, TMemberAccessExpression caseLabel) - => (TSwitchArmSyntax)generator.SwitchSection(caseLabel, [generator.ExitSwitchStatement()]); + protected sealed override TSwitchArmSyntax CreateSwitchArm(SyntaxGenerator generator, Compilation compilation, TMemberAccessExpression caseLabel) + => (TSwitchArmSyntax)generator.SwitchSection(caseLabel, [generator.ExitSwitchStatement()]); - protected override TSwitchArmSyntax CreateNullSwitchArm(SyntaxGenerator generator, Compilation compilation) - => (TSwitchArmSyntax)generator.SwitchSection(generator.NullLiteralExpression(), [generator.ExitSwitchStatement()]); + protected override TSwitchArmSyntax CreateNullSwitchArm(SyntaxGenerator generator, Compilation compilation) + => (TSwitchArmSyntax)generator.SwitchSection(generator.NullLiteralExpression(), [generator.ExitSwitchStatement()]); - protected sealed override int InsertPosition(ISwitchOperation switchStatement) - { - // If the last section has a default label, then we want to be above that. - // Otherwise, we just get inserted at the end. + protected sealed override int InsertPosition(ISwitchOperation switchStatement) + { + // If the last section has a default label, then we want to be above that. + // Otherwise, we just get inserted at the end. - var cases = switchStatement.Cases; - if (cases.Length > 0) + var cases = switchStatement.Cases; + if (cases.Length > 0) + { + var lastCase = cases.Last(); + if (lastCase.Clauses.Any(static c => c.CaseKind == CaseKind.Default)) { - var lastCase = cases.Last(); - if (lastCase.Clauses.Any(static c => c.CaseKind == CaseKind.Default)) - { - return cases.Length - 1; - } + return cases.Length - 1; } - - return cases.Length; } + + return cases.Length; } } diff --git a/src/Analyzers/Core/CodeFixes/PredefinedCodeFixProviderNames.cs b/src/Analyzers/Core/CodeFixes/PredefinedCodeFixProviderNames.cs index f39457433c597..e6069cc4b93c4 100644 --- a/src/Analyzers/Core/CodeFixes/PredefinedCodeFixProviderNames.cs +++ b/src/Analyzers/Core/CodeFixes/PredefinedCodeFixProviderNames.cs @@ -2,178 +2,177 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.CodeFixes +namespace Microsoft.CodeAnalysis.CodeFixes; + +internal static class PredefinedCodeFixProviderNames { - internal static class PredefinedCodeFixProviderNames - { - public const string AddAccessibilityModifiers = nameof(AddAccessibilityModifiers); - public const string AddAnonymousTypeMemberName = nameof(AddAnonymousTypeMemberName); - public const string AddAsync = nameof(AddAsync); - public const string AddBraces = nameof(AddBraces); - public const string AddDocCommentNodes = nameof(AddDocCommentNodes); - public const string AddExplicitCast = nameof(AddExplicitCast); - public const string AddImport = nameof(AddImport); - public const string AddInheritdoc = nameof(AddInheritdoc); - public const string AddMissingReference = nameof(AddMissingReference); - public const string AddNew = nameof(AddNew); - public const string AddObsoleteAttribute = nameof(AddObsoleteAttribute); - public const string AddOverloads = nameof(AddOverloads); - public const string AddPackage = nameof(AddPackage); - public const string AddParameter = nameof(AddParameter); - public const string AddParenthesesAroundConditionalExpressionInInterpolatedString = nameof(AddParenthesesAroundConditionalExpressionInInterpolatedString); - public const string AddRequiredParentheses = nameof(AddRequiredParentheses); - public const string AliasAmbiguousType = nameof(AliasAmbiguousType); - public const string ApplyNamingStyle = nameof(ApplyNamingStyle); - public const string ArrowExpressionClausePlacement = nameof(ArrowExpressionClausePlacement); - public const string AssignOutParametersAboveReturn = nameof(AssignOutParametersAboveReturn); - public const string AssignOutParametersAtStart = nameof(AssignOutParametersAtStart); - public const string ChangeNamespaceToMatchFolder = nameof(ChangeNamespaceToMatchFolder); - public const string ChangeReturnType = nameof(ChangeReturnType); - public const string ChangeToYield = nameof(ChangeToYield); - public const string ConditionalExpressionPlacement = nameof(ConditionalExpressionPlacement); - public const string ConflictMarkerResolution = nameof(ConflictMarkerResolution); - public const string ConsecutiveBracePlacement = nameof(ConsecutiveBracePlacement); - public const string ConsecutiveStatementPlacement = nameof(ConsecutiveStatementPlacement); - public const string ConstructorInitializerPlacement = nameof(ConstructorInitializerPlacement); - public const string ConvertNamespace = nameof(ConvertNamespace); - public const string ConvertSwitchStatementToExpression = nameof(ConvertSwitchStatementToExpression); - public const string ConvertToAsync = nameof(ConvertToAsync); - public const string ConvertToIterator = nameof(ConvertToIterator); - public const string ConvertToProgramMain = nameof(ConvertToProgramMain); - public const string ConvertToRecord = nameof(ConvertToRecord); - public const string ConvertToTopLevelStatements = nameof(ConvertToTopLevelStatements); - public const string ConvertTypeOfToNameOf = nameof(ConvertTypeOfToNameOf); - public const string CorrectNextControlVariable = nameof(CorrectNextControlVariable); - public const string DeclareAsNullable = nameof(DeclareAsNullable); - public const string DisambiguateSameVariable = nameof(DisambiguateSameVariable); - public const string EmbeddedStatementPlacement = nameof(EmbeddedStatementPlacement); - public const string FileHeader = nameof(FileHeader); - public const string FixFormatting = nameof(FixFormatting); - public const string FixIncorrectConstraint = nameof(FixIncorrectConstraint); - public const string FixIncorrectExitContinue = nameof(FixIncorrectExitContinue); - public const string FixIncorrectFunctionReturnType = nameof(FixIncorrectFunctionReturnType); - public const string FixReturnType = nameof(FixReturnType); - public const string ForEachCast = nameof(ForEachCast); - public const string FullyQualify = nameof(FullyQualify); - public const string GenerateConstructor = nameof(GenerateConstructor); - public const string GenerateConversion = nameof(GenerateConversion); - public const string GenerateDeconstructMethod = nameof(GenerateDeconstructMethod); - public const string GenerateDefaultConstructors = nameof(GenerateDefaultConstructors); - public const string GenerateEndConstruct = nameof(GenerateEndConstruct); - public const string GenerateEnumMember = nameof(GenerateEnumMember); - public const string GenerateEvent = nameof(GenerateEvent); - public const string GenerateMethod = nameof(GenerateMethod); - public const string GenerateType = nameof(GenerateType); - public const string GenerateVariable = nameof(GenerateVariable); - public const string ImplementAbstractClass = nameof(ImplementAbstractClass); - public const string ImplementInterface = nameof(ImplementInterface); - public const string InlineDeclaration = nameof(InlineDeclaration); - public const string InvokeDelegateWithConditionalAccess = nameof(InvokeDelegateWithConditionalAccess); - public const string JsonDetection = nameof(JsonDetection); - public const string MakeFieldReadonly = nameof(MakeFieldReadonly); - public const string MakeLocalFunctionStatic = nameof(MakeLocalFunctionStatic); - public const string MakeMemberRequired = nameof(MakeMemberRequired); - public const string MakeMemberStatic = nameof(MakeMemberStatic); - public const string MakeMethodSynchronous = nameof(MakeMethodSynchronous); - public const string MakeRefStruct = nameof(MakeRefStruct); - public const string MakeStatementAsynchronous = nameof(MakeStatementAsynchronous); - public const string MakeStructFieldsWritable = nameof(MakeStructFieldsWritable); - public const string MakeStructMemberReadOnly = nameof(MakeStructMemberReadOnly); - public const string MakeStructReadOnly = nameof(MakeStructReadOnly); - public const string MakeTypeAbstract = nameof(MakeTypeAbstract); - public const string MakeTypePartial = nameof(MakeTypePartial); - public const string MoveMisplacedUsingDirectives = nameof(MoveMisplacedUsingDirectives); - public const string MoveToTopOfFile = nameof(MoveToTopOfFile); - public const string OrderModifiers = nameof(OrderModifiers); - public const string PassInCapturedVariables = nameof(PassInCapturedVariables); - public const string PopulateSwitch = nameof(PopulateSwitch); - public const string PopulateSwitchExpression = nameof(PopulateSwitchExpression); - public const string PreferFrameworkType = nameof(PreferFrameworkType); - public const string QualifyMemberAccess = nameof(QualifyMemberAccess); - public const string RemoveAsyncModifier = nameof(RemoveAsyncModifier); - public const string RemoveBlankLines = nameof(RemoveBlankLines); - public const string RemoveConfusingSuppression = nameof(RemoveConfusingSuppression); - public const string RemoveDocCommentNode = nameof(RemoveDocCommentNode); - public const string RemoveIn = nameof(RemoveIn); - public const string RemoveNew = nameof(RemoveNew); - public const string RemoveRedundantEquality = nameof(RemoveRedundantEquality); - public const string RemoveSharedFromModuleMembers = nameof(RemoveSharedFromModuleMembers); - public const string RemoveUnnecessaryAttributeSuppressions = nameof(RemoveUnnecessaryAttributeSuppressions); - public const string RemoveUnnecessaryByVal = nameof(RemoveUnnecessaryByVal); - public const string RemoveUnnecessaryCast = nameof(RemoveUnnecessaryCast); - public const string RemoveUnnecessaryDiscardDesignation = nameof(RemoveUnnecessaryDiscardDesignation); - public const string RemoveUnnecessaryImports = nameof(RemoveUnnecessaryImports); - public const string RemoveUnnecessaryLambdaExpression = nameof(RemoveUnnecessaryLambdaExpression); - public const string RemoveUnnecessaryNullableDirective = nameof(RemoveUnnecessaryNullableDirective); - public const string RemoveUnnecessaryParentheses = nameof(RemoveUnnecessaryParentheses); - public const string RemoveUnnecessaryPragmaSuppressions = nameof(RemoveUnnecessaryPragmaSuppressions); - public const string RemoveUnreachableCode = nameof(RemoveUnreachableCode); - public const string RemoveUnusedLocalFunction = nameof(RemoveUnusedLocalFunction); - public const string RemoveUnusedMembers = nameof(RemoveUnusedMembers); - public const string RemoveUnusedValues = nameof(RemoveUnusedValues); - public const string RemoveUnusedVariable = nameof(RemoveUnusedVariable); - public const string ReplaceDefaultLiteral = nameof(ReplaceDefaultLiteral); - public const string SimplifyConditionalExpression = nameof(SimplifyConditionalExpression); - public const string SimplifyInterpolation = nameof(SimplifyInterpolation); - public const string SimplifyLinqExpression = nameof(SimplifyLinqExpression); - public const string SimplifyNames = nameof(SimplifyNames); - public const string SimplifyObjectCreation = nameof(SimplifyObjectCreation); - public const string SimplifyPropertyPattern = nameof(SimplifyPropertyPattern); - public const string SimplifyThisOrMe = nameof(SimplifyThisOrMe); - public const string SpellCheck = nameof(SpellCheck); - public const string TransposeRecordKeyword = nameof(TransposeRecordKeyword); - public const string UnsealClass = nameof(UnsealClass); - public const string UpdateLegacySuppressions = nameof(UpdateLegacySuppressions); - public const string UpdateProjectToAllowUnsafe = nameof(UpdateProjectToAllowUnsafe); - public const string UpgradeProject = nameof(UpgradeProject); - public const string UseAutoProperty = nameof(UseAutoProperty); - public const string UseCoalesceExpressionForIfNullStatementCheck = nameof(UseCoalesceExpressionForIfNullStatementCheck); - public const string UseCoalesceExpressionForNullableTernaryConditionalCheck = nameof(UseCoalesceExpressionForNullableTernaryConditionalCheck); - public const string UseCoalesceExpressionForTernaryConditionalCheck = nameof(UseCoalesceExpressionForTernaryConditionalCheck); - public const string UseCollectionExpressionForArray = nameof(UseCollectionExpressionForArray); - public const string UseCollectionExpressionForBuilder = nameof(UseCollectionExpressionForBuilder); - public const string UseCollectionExpressionForCreate = nameof(UseCollectionExpressionForCreate); - public const string UseCollectionExpressionForEmpty = nameof(UseCollectionExpressionForEmpty); - public const string UseCollectionExpressionForFluent = nameof(UseCollectionExpressionForFluent); - public const string UseCollectionExpressionForStackAlloc = nameof(UseCollectionExpressionForStackAlloc); - public const string UseCollectionInitializer = nameof(UseCollectionInitializer); - public const string UseCompoundAssignment = nameof(UseCompoundAssignment); - public const string UseCompoundCoalesceAssignment = nameof(UseCompoundCoalesceAssignment); - public const string UseConditionalExpressionForAssignment = nameof(UseConditionalExpressionForAssignment); - public const string UseConditionalExpressionForReturn = nameof(UseConditionalExpressionForReturn); - public const string UseDeconstruction = nameof(UseDeconstruction); - public const string UseDefaultLiteral = nameof(UseDefaultLiteral); - public const string UseExplicitTupleName = nameof(UseExplicitTupleName); - public const string UseExplicitType = nameof(UseExplicitType); - public const string UseExplicitTypeForConst = nameof(UseExplicitTypeForConst); - public const string UseExpressionBody = nameof(UseExpressionBody); - public const string UseExpressionBodyForLambda = nameof(UseExpressionBodyForLambda); - public const string UseImplicitObjectCreation = nameof(UseImplicitObjectCreation); - public const string UseImplicitType = nameof(UseImplicitType); - public const string UseIndexOperator = nameof(UseIndexOperator); - public const string UseInferredMemberName = nameof(UseInferredMemberName); - public const string UseInterpolatedVerbatimString = nameof(UseInterpolatedVerbatimString); - public const string UseIsNotExpression = nameof(UseIsNotExpression); - public const string UseIsNullCheck = nameof(UseIsNullCheck); - public const string UseIsNullCheckForCastAndEqualityOperator = nameof(UseIsNullCheckForCastAndEqualityOperator); - public const string UseIsNullCheckForReferenceEquals = nameof(UseIsNullCheckForReferenceEquals); - public const string UseLocalFunction = nameof(UseLocalFunction); - public const string UseNameofInAttribute = nameof(UseNameofInAttribute); - public const string UseNotPattern = nameof(UseNotPattern); - public const string UseNullCheckOverTypeCheck = nameof(UseNullCheckOverTypeCheck); - public const string UseNullPropagation = nameof(UseNullPropagation); - public const string UseObjectInitializer = nameof(UseObjectInitializer); - public const string UsePatternCombinators = nameof(UsePatternCombinators); - public const string UsePatternMatchingAsAndMemberAccess = nameof(UsePatternMatchingAsAndMemberAccess); - public const string UsePatternMatchingAsAndNullCheck = nameof(UsePatternMatchingAsAndNullCheck); - public const string UsePatternMatchingIsAndCastCheck = nameof(UsePatternMatchingIsAndCastCheck); - public const string UsePatternMatchingIsAndCastCheckWithoutName = nameof(UsePatternMatchingIsAndCastCheckWithoutName); - public const string UsePrimaryConstructor = nameof(UsePrimaryConstructor); - public const string UseRangeOperator = nameof(UseRangeOperator); - public const string UseSimpleUsingStatement = nameof(UseSimpleUsingStatement); - public const string UseSystemHashCode = nameof(UseSystemHashCode); - public const string UseThrowExpression = nameof(UseThrowExpression); - public const string UseTupleSwap = nameof(UseTupleSwap); - public const string UseUtf8StringLiteral = nameof(UseUtf8StringLiteral); - } + public const string AddAccessibilityModifiers = nameof(AddAccessibilityModifiers); + public const string AddAnonymousTypeMemberName = nameof(AddAnonymousTypeMemberName); + public const string AddAsync = nameof(AddAsync); + public const string AddBraces = nameof(AddBraces); + public const string AddDocCommentNodes = nameof(AddDocCommentNodes); + public const string AddExplicitCast = nameof(AddExplicitCast); + public const string AddImport = nameof(AddImport); + public const string AddInheritdoc = nameof(AddInheritdoc); + public const string AddMissingReference = nameof(AddMissingReference); + public const string AddNew = nameof(AddNew); + public const string AddObsoleteAttribute = nameof(AddObsoleteAttribute); + public const string AddOverloads = nameof(AddOverloads); + public const string AddPackage = nameof(AddPackage); + public const string AddParameter = nameof(AddParameter); + public const string AddParenthesesAroundConditionalExpressionInInterpolatedString = nameof(AddParenthesesAroundConditionalExpressionInInterpolatedString); + public const string AddRequiredParentheses = nameof(AddRequiredParentheses); + public const string AliasAmbiguousType = nameof(AliasAmbiguousType); + public const string ApplyNamingStyle = nameof(ApplyNamingStyle); + public const string ArrowExpressionClausePlacement = nameof(ArrowExpressionClausePlacement); + public const string AssignOutParametersAboveReturn = nameof(AssignOutParametersAboveReturn); + public const string AssignOutParametersAtStart = nameof(AssignOutParametersAtStart); + public const string ChangeNamespaceToMatchFolder = nameof(ChangeNamespaceToMatchFolder); + public const string ChangeReturnType = nameof(ChangeReturnType); + public const string ChangeToYield = nameof(ChangeToYield); + public const string ConditionalExpressionPlacement = nameof(ConditionalExpressionPlacement); + public const string ConflictMarkerResolution = nameof(ConflictMarkerResolution); + public const string ConsecutiveBracePlacement = nameof(ConsecutiveBracePlacement); + public const string ConsecutiveStatementPlacement = nameof(ConsecutiveStatementPlacement); + public const string ConstructorInitializerPlacement = nameof(ConstructorInitializerPlacement); + public const string ConvertNamespace = nameof(ConvertNamespace); + public const string ConvertSwitchStatementToExpression = nameof(ConvertSwitchStatementToExpression); + public const string ConvertToAsync = nameof(ConvertToAsync); + public const string ConvertToIterator = nameof(ConvertToIterator); + public const string ConvertToProgramMain = nameof(ConvertToProgramMain); + public const string ConvertToRecord = nameof(ConvertToRecord); + public const string ConvertToTopLevelStatements = nameof(ConvertToTopLevelStatements); + public const string ConvertTypeOfToNameOf = nameof(ConvertTypeOfToNameOf); + public const string CorrectNextControlVariable = nameof(CorrectNextControlVariable); + public const string DeclareAsNullable = nameof(DeclareAsNullable); + public const string DisambiguateSameVariable = nameof(DisambiguateSameVariable); + public const string EmbeddedStatementPlacement = nameof(EmbeddedStatementPlacement); + public const string FileHeader = nameof(FileHeader); + public const string FixFormatting = nameof(FixFormatting); + public const string FixIncorrectConstraint = nameof(FixIncorrectConstraint); + public const string FixIncorrectExitContinue = nameof(FixIncorrectExitContinue); + public const string FixIncorrectFunctionReturnType = nameof(FixIncorrectFunctionReturnType); + public const string FixReturnType = nameof(FixReturnType); + public const string ForEachCast = nameof(ForEachCast); + public const string FullyQualify = nameof(FullyQualify); + public const string GenerateConstructor = nameof(GenerateConstructor); + public const string GenerateConversion = nameof(GenerateConversion); + public const string GenerateDeconstructMethod = nameof(GenerateDeconstructMethod); + public const string GenerateDefaultConstructors = nameof(GenerateDefaultConstructors); + public const string GenerateEndConstruct = nameof(GenerateEndConstruct); + public const string GenerateEnumMember = nameof(GenerateEnumMember); + public const string GenerateEvent = nameof(GenerateEvent); + public const string GenerateMethod = nameof(GenerateMethod); + public const string GenerateType = nameof(GenerateType); + public const string GenerateVariable = nameof(GenerateVariable); + public const string ImplementAbstractClass = nameof(ImplementAbstractClass); + public const string ImplementInterface = nameof(ImplementInterface); + public const string InlineDeclaration = nameof(InlineDeclaration); + public const string InvokeDelegateWithConditionalAccess = nameof(InvokeDelegateWithConditionalAccess); + public const string JsonDetection = nameof(JsonDetection); + public const string MakeFieldReadonly = nameof(MakeFieldReadonly); + public const string MakeLocalFunctionStatic = nameof(MakeLocalFunctionStatic); + public const string MakeMemberRequired = nameof(MakeMemberRequired); + public const string MakeMemberStatic = nameof(MakeMemberStatic); + public const string MakeMethodSynchronous = nameof(MakeMethodSynchronous); + public const string MakeRefStruct = nameof(MakeRefStruct); + public const string MakeStatementAsynchronous = nameof(MakeStatementAsynchronous); + public const string MakeStructFieldsWritable = nameof(MakeStructFieldsWritable); + public const string MakeStructMemberReadOnly = nameof(MakeStructMemberReadOnly); + public const string MakeStructReadOnly = nameof(MakeStructReadOnly); + public const string MakeTypeAbstract = nameof(MakeTypeAbstract); + public const string MakeTypePartial = nameof(MakeTypePartial); + public const string MoveMisplacedUsingDirectives = nameof(MoveMisplacedUsingDirectives); + public const string MoveToTopOfFile = nameof(MoveToTopOfFile); + public const string OrderModifiers = nameof(OrderModifiers); + public const string PassInCapturedVariables = nameof(PassInCapturedVariables); + public const string PopulateSwitch = nameof(PopulateSwitch); + public const string PopulateSwitchExpression = nameof(PopulateSwitchExpression); + public const string PreferFrameworkType = nameof(PreferFrameworkType); + public const string QualifyMemberAccess = nameof(QualifyMemberAccess); + public const string RemoveAsyncModifier = nameof(RemoveAsyncModifier); + public const string RemoveBlankLines = nameof(RemoveBlankLines); + public const string RemoveConfusingSuppression = nameof(RemoveConfusingSuppression); + public const string RemoveDocCommentNode = nameof(RemoveDocCommentNode); + public const string RemoveIn = nameof(RemoveIn); + public const string RemoveNew = nameof(RemoveNew); + public const string RemoveRedundantEquality = nameof(RemoveRedundantEquality); + public const string RemoveSharedFromModuleMembers = nameof(RemoveSharedFromModuleMembers); + public const string RemoveUnnecessaryAttributeSuppressions = nameof(RemoveUnnecessaryAttributeSuppressions); + public const string RemoveUnnecessaryByVal = nameof(RemoveUnnecessaryByVal); + public const string RemoveUnnecessaryCast = nameof(RemoveUnnecessaryCast); + public const string RemoveUnnecessaryDiscardDesignation = nameof(RemoveUnnecessaryDiscardDesignation); + public const string RemoveUnnecessaryImports = nameof(RemoveUnnecessaryImports); + public const string RemoveUnnecessaryLambdaExpression = nameof(RemoveUnnecessaryLambdaExpression); + public const string RemoveUnnecessaryNullableDirective = nameof(RemoveUnnecessaryNullableDirective); + public const string RemoveUnnecessaryParentheses = nameof(RemoveUnnecessaryParentheses); + public const string RemoveUnnecessaryPragmaSuppressions = nameof(RemoveUnnecessaryPragmaSuppressions); + public const string RemoveUnreachableCode = nameof(RemoveUnreachableCode); + public const string RemoveUnusedLocalFunction = nameof(RemoveUnusedLocalFunction); + public const string RemoveUnusedMembers = nameof(RemoveUnusedMembers); + public const string RemoveUnusedValues = nameof(RemoveUnusedValues); + public const string RemoveUnusedVariable = nameof(RemoveUnusedVariable); + public const string ReplaceDefaultLiteral = nameof(ReplaceDefaultLiteral); + public const string SimplifyConditionalExpression = nameof(SimplifyConditionalExpression); + public const string SimplifyInterpolation = nameof(SimplifyInterpolation); + public const string SimplifyLinqExpression = nameof(SimplifyLinqExpression); + public const string SimplifyNames = nameof(SimplifyNames); + public const string SimplifyObjectCreation = nameof(SimplifyObjectCreation); + public const string SimplifyPropertyPattern = nameof(SimplifyPropertyPattern); + public const string SimplifyThisOrMe = nameof(SimplifyThisOrMe); + public const string SpellCheck = nameof(SpellCheck); + public const string TransposeRecordKeyword = nameof(TransposeRecordKeyword); + public const string UnsealClass = nameof(UnsealClass); + public const string UpdateLegacySuppressions = nameof(UpdateLegacySuppressions); + public const string UpdateProjectToAllowUnsafe = nameof(UpdateProjectToAllowUnsafe); + public const string UpgradeProject = nameof(UpgradeProject); + public const string UseAutoProperty = nameof(UseAutoProperty); + public const string UseCoalesceExpressionForIfNullStatementCheck = nameof(UseCoalesceExpressionForIfNullStatementCheck); + public const string UseCoalesceExpressionForNullableTernaryConditionalCheck = nameof(UseCoalesceExpressionForNullableTernaryConditionalCheck); + public const string UseCoalesceExpressionForTernaryConditionalCheck = nameof(UseCoalesceExpressionForTernaryConditionalCheck); + public const string UseCollectionExpressionForArray = nameof(UseCollectionExpressionForArray); + public const string UseCollectionExpressionForBuilder = nameof(UseCollectionExpressionForBuilder); + public const string UseCollectionExpressionForCreate = nameof(UseCollectionExpressionForCreate); + public const string UseCollectionExpressionForEmpty = nameof(UseCollectionExpressionForEmpty); + public const string UseCollectionExpressionForFluent = nameof(UseCollectionExpressionForFluent); + public const string UseCollectionExpressionForStackAlloc = nameof(UseCollectionExpressionForStackAlloc); + public const string UseCollectionInitializer = nameof(UseCollectionInitializer); + public const string UseCompoundAssignment = nameof(UseCompoundAssignment); + public const string UseCompoundCoalesceAssignment = nameof(UseCompoundCoalesceAssignment); + public const string UseConditionalExpressionForAssignment = nameof(UseConditionalExpressionForAssignment); + public const string UseConditionalExpressionForReturn = nameof(UseConditionalExpressionForReturn); + public const string UseDeconstruction = nameof(UseDeconstruction); + public const string UseDefaultLiteral = nameof(UseDefaultLiteral); + public const string UseExplicitTupleName = nameof(UseExplicitTupleName); + public const string UseExplicitType = nameof(UseExplicitType); + public const string UseExplicitTypeForConst = nameof(UseExplicitTypeForConst); + public const string UseExpressionBody = nameof(UseExpressionBody); + public const string UseExpressionBodyForLambda = nameof(UseExpressionBodyForLambda); + public const string UseImplicitObjectCreation = nameof(UseImplicitObjectCreation); + public const string UseImplicitType = nameof(UseImplicitType); + public const string UseIndexOperator = nameof(UseIndexOperator); + public const string UseInferredMemberName = nameof(UseInferredMemberName); + public const string UseInterpolatedVerbatimString = nameof(UseInterpolatedVerbatimString); + public const string UseIsNotExpression = nameof(UseIsNotExpression); + public const string UseIsNullCheck = nameof(UseIsNullCheck); + public const string UseIsNullCheckForCastAndEqualityOperator = nameof(UseIsNullCheckForCastAndEqualityOperator); + public const string UseIsNullCheckForReferenceEquals = nameof(UseIsNullCheckForReferenceEquals); + public const string UseLocalFunction = nameof(UseLocalFunction); + public const string UseNameofInAttribute = nameof(UseNameofInAttribute); + public const string UseNotPattern = nameof(UseNotPattern); + public const string UseNullCheckOverTypeCheck = nameof(UseNullCheckOverTypeCheck); + public const string UseNullPropagation = nameof(UseNullPropagation); + public const string UseObjectInitializer = nameof(UseObjectInitializer); + public const string UsePatternCombinators = nameof(UsePatternCombinators); + public const string UsePatternMatchingAsAndMemberAccess = nameof(UsePatternMatchingAsAndMemberAccess); + public const string UsePatternMatchingAsAndNullCheck = nameof(UsePatternMatchingAsAndNullCheck); + public const string UsePatternMatchingIsAndCastCheck = nameof(UsePatternMatchingIsAndCastCheck); + public const string UsePatternMatchingIsAndCastCheckWithoutName = nameof(UsePatternMatchingIsAndCastCheckWithoutName); + public const string UsePrimaryConstructor = nameof(UsePrimaryConstructor); + public const string UseRangeOperator = nameof(UseRangeOperator); + public const string UseSimpleUsingStatement = nameof(UseSimpleUsingStatement); + public const string UseSystemHashCode = nameof(UseSystemHashCode); + public const string UseThrowExpression = nameof(UseThrowExpression); + public const string UseTupleSwap = nameof(UseTupleSwap); + public const string UseUtf8StringLiteral = nameof(UseUtf8StringLiteral); } diff --git a/src/Analyzers/Core/CodeFixes/PredefinedConfigurationFixProviderNames.cs b/src/Analyzers/Core/CodeFixes/PredefinedConfigurationFixProviderNames.cs index 33c27846577c4..1817e69981dbd 100644 --- a/src/Analyzers/Core/CodeFixes/PredefinedConfigurationFixProviderNames.cs +++ b/src/Analyzers/Core/CodeFixes/PredefinedConfigurationFixProviderNames.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.CodeFixes +namespace Microsoft.CodeAnalysis.CodeFixes; + +internal static class PredefinedConfigurationFixProviderNames { - internal static class PredefinedConfigurationFixProviderNames - { - public const string ConfigureCodeStyleOption = nameof(ConfigureCodeStyleOption); - public const string ConfigureSeverity = nameof(ConfigureSeverity); - public const string Suppression = nameof(Suppression); - } + public const string ConfigureCodeStyleOption = nameof(ConfigureCodeStyleOption); + public const string ConfigureSeverity = nameof(ConfigureSeverity); + public const string Suppression = nameof(Suppression); } diff --git a/src/Analyzers/Core/CodeFixes/QualifyMemberAccess/AbstractQualifyMemberAccessCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/QualifyMemberAccess/AbstractQualifyMemberAccessCodeFixProvider.cs index 6d4d0d790f824..d57f8d9b8b3ce 100644 --- a/src/Analyzers/Core/CodeFixes/QualifyMemberAccess/AbstractQualifyMemberAccessCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/QualifyMemberAccess/AbstractQualifyMemberAccessCodeFixProvider.cs @@ -12,48 +12,47 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.QualifyMemberAccess +namespace Microsoft.CodeAnalysis.QualifyMemberAccess; + +internal abstract class AbstractQualifyMemberAccessCodeFixprovider + : SyntaxEditorBasedCodeFixProvider + where TSimpleNameSyntax : SyntaxNode + where TInvocationSyntax : SyntaxNode { - internal abstract class AbstractQualifyMemberAccessCodeFixprovider - : SyntaxEditorBasedCodeFixProvider - where TSimpleNameSyntax : SyntaxNode - where TInvocationSyntax : SyntaxNode - { - protected abstract string GetTitle(); - protected abstract TSimpleNameSyntax? GetNode(Diagnostic diagnostic, CancellationToken cancellationToken); + protected abstract string GetTitle(); + protected abstract TSimpleNameSyntax? GetNode(Diagnostic diagnostic, CancellationToken cancellationToken); - public sealed override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.AddThisOrMeQualificationDiagnosticId]; + public sealed override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.AddThisOrMeQualificationDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var title = GetTitle(); - RegisterCodeFix(context, title, title); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var title = GetTitle(); + RegisterCodeFix(context, title, title); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var generator = document.GetRequiredLanguageService(); + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var generator = document.GetRequiredLanguageService(); - foreach (var diagnostic in diagnostics) + foreach (var diagnostic in diagnostics) + { + var node = GetNode(diagnostic, cancellationToken); + if (node != null) { - var node = GetNode(diagnostic, cancellationToken); - if (node != null) - { - var qualifiedAccess = - generator.MemberAccessExpression( - generator.ThisExpression(), - node.WithLeadingTrivia()) - .WithLeadingTrivia(node.GetLeadingTrivia()); - - editor.ReplaceNode(node, qualifiedAccess); - } - } + var qualifiedAccess = + generator.MemberAccessExpression( + generator.ThisExpression(), + node.WithLeadingTrivia()) + .WithLeadingTrivia(node.GetLeadingTrivia()); - return Task.CompletedTask; + editor.ReplaceNode(node, qualifiedAccess); + } } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/Core/CodeFixes/RemoveAsyncModifier/AbstractRemoveAsyncModifierCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/RemoveAsyncModifier/AbstractRemoveAsyncModifierCodeFixProvider.cs index 8b5e199414026..250c040db27fe 100644 --- a/src/Analyzers/Core/CodeFixes/RemoveAsyncModifier/AbstractRemoveAsyncModifierCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/RemoveAsyncModifier/AbstractRemoveAsyncModifierCodeFixProvider.cs @@ -16,256 +16,255 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.RemoveAsyncModifier +namespace Microsoft.CodeAnalysis.RemoveAsyncModifier; + +internal abstract class AbstractRemoveAsyncModifierCodeFixProvider : SyntaxEditorBasedCodeFixProvider + where TReturnStatementSyntax : SyntaxNode + where TExpressionSyntax : SyntaxNode { - internal abstract class AbstractRemoveAsyncModifierCodeFixProvider : SyntaxEditorBasedCodeFixProvider - where TReturnStatementSyntax : SyntaxNode - where TExpressionSyntax : SyntaxNode + protected abstract bool IsAsyncSupportingFunctionSyntax(SyntaxNode node); + protected abstract SyntaxNode RemoveAsyncModifier(SyntaxGenerator generator, SyntaxNode methodLikeNode); + protected abstract SyntaxNode? ConvertToBlockBody(SyntaxNode node, TExpressionSyntax expressionBody); + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) { - protected abstract bool IsAsyncSupportingFunctionSyntax(SyntaxNode node); - protected abstract SyntaxNode RemoveAsyncModifier(SyntaxGenerator generator, SyntaxNode methodLikeNode); - protected abstract SyntaxNode? ConvertToBlockBody(SyntaxNode node, TExpressionSyntax expressionBody); + var document = context.Document; + var cancellationToken = context.CancellationToken; + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var knownTypes = new KnownTaskTypes(compilation); + + var diagnostic = context.Diagnostics.First(); + var token = diagnostic.Location.FindToken(cancellationToken); + var node = token.GetAncestor(IsAsyncSupportingFunctionSyntax); + if (node == null) + { + return; + } + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var methodSymbol = GetMethodSymbol(node, semanticModel, cancellationToken); - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + if (methodSymbol == null) { - var document = context.Document; - var cancellationToken = context.CancellationToken; - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var knownTypes = new KnownTaskTypes(compilation); + return; + } + + if (ShouldOfferFix(methodSymbol.ReturnType, knownTypes)) + { + context.RegisterCodeFix( + CodeAction.Create( + CodeFixesResources.Remove_async_modifier, + GetDocumentUpdater(context), + nameof(CodeFixesResources.Remove_async_modifier)), + context.Diagnostics); + } + } - var diagnostic = context.Diagnostics.First(); + protected sealed override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var solutionServices = document.Project.Solution.Services; + var generator = editor.Generator; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + var compilation = semanticModel.Compilation; + var knownTypes = new KnownTaskTypes(compilation); + + // For fix all we need to do nested locals or lambdas first, so order the diagnostics by location descending + foreach (var diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start)) + { var token = diagnostic.Location.FindToken(cancellationToken); var node = token.GetAncestor(IsAsyncSupportingFunctionSyntax); if (node == null) { - return; + Debug.Fail("We should always be able to find the node from the diagnostic."); + continue; } - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var methodSymbol = GetMethodSymbol(node, semanticModel, cancellationToken); - if (methodSymbol == null) { - return; + Debug.Fail("We should always be able to find the method symbol for the diagnostic."); + continue; } - if (ShouldOfferFix(methodSymbol.ReturnType, knownTypes)) - { - context.RegisterCodeFix( - CodeAction.Create( - CodeFixesResources.Remove_async_modifier, - GetDocumentUpdater(context), - nameof(CodeFixesResources.Remove_async_modifier)), - context.Diagnostics); - } - } + // We might need to perform control flow analysis as part of the fix, so we need to do it on the original node + // so do it up front. Nothing in the fixer changes the reachability of the end of the method so this is safe + var controlFlow = GetControlFlowAnalysis(generator, semanticModel, node); + // If control flow couldn't be computed then its probably an empty block, which means we need to add a return anyway + var needsReturnStatementAdded = controlFlow == null || controlFlow.EndPointIsReachable; - protected sealed override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var solutionServices = document.Project.Solution.Services; - var generator = editor.Generator; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); - var compilation = semanticModel.Compilation; - var knownTypes = new KnownTaskTypes(compilation); - - // For fix all we need to do nested locals or lambdas first, so order the diagnostics by location descending - foreach (var diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start)) - { - var token = diagnostic.Location.FindToken(cancellationToken); - var node = token.GetAncestor(IsAsyncSupportingFunctionSyntax); - if (node == null) - { - Debug.Fail("We should always be able to find the node from the diagnostic."); - continue; - } - - var methodSymbol = GetMethodSymbol(node, semanticModel, cancellationToken); - if (methodSymbol == null) - { - Debug.Fail("We should always be able to find the method symbol for the diagnostic."); - continue; - } - - // We might need to perform control flow analysis as part of the fix, so we need to do it on the original node - // so do it up front. Nothing in the fixer changes the reachability of the end of the method so this is safe - var controlFlow = GetControlFlowAnalysis(generator, semanticModel, node); - // If control flow couldn't be computed then its probably an empty block, which means we need to add a return anyway - var needsReturnStatementAdded = controlFlow == null || controlFlow.EndPointIsReachable; - - editor.ReplaceNode(node, - (updatedNode, generator) => RemoveAsyncModifier( - solutionServices, syntaxFacts, generator, updatedNode, methodSymbol.ReturnType, knownTypes, needsReturnStatementAdded)); - } + editor.ReplaceNode(node, + (updatedNode, generator) => RemoveAsyncModifier( + solutionServices, syntaxFacts, generator, updatedNode, methodSymbol.ReturnType, knownTypes, needsReturnStatementAdded)); } + } - private static IMethodSymbol? GetMethodSymbol(SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken) - => semanticModel.GetSymbolInfo(node, cancellationToken).Symbol as IMethodSymbol ?? - semanticModel.GetDeclaredSymbol(node, cancellationToken) as IMethodSymbol; - - private static bool ShouldOfferFix(ITypeSymbol returnType, KnownTaskTypes knownTypes) - => IsTaskType(returnType, knownTypes) - || returnType.OriginalDefinition.Equals(knownTypes.TaskOfTType) - || returnType.OriginalDefinition.Equals(knownTypes.ValueTaskOfTType); - - private static bool IsTaskType(ITypeSymbol returnType, KnownTaskTypes knownTypes) - => returnType.OriginalDefinition.Equals(knownTypes.TaskType) - || returnType.OriginalDefinition.Equals(knownTypes.ValueTaskType); - - private SyntaxNode RemoveAsyncModifier( - SolutionServices solutionServices, - ISyntaxFacts syntaxFacts, - SyntaxGenerator generator, - SyntaxNode node, - ITypeSymbol returnType, - KnownTaskTypes knownTypes, - bool needsReturnStatementAdded) - { - node = RemoveAsyncModifier(generator, node); + private static IMethodSymbol? GetMethodSymbol(SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken) + => semanticModel.GetSymbolInfo(node, cancellationToken).Symbol as IMethodSymbol ?? + semanticModel.GetDeclaredSymbol(node, cancellationToken) as IMethodSymbol; + + private static bool ShouldOfferFix(ITypeSymbol returnType, KnownTaskTypes knownTypes) + => IsTaskType(returnType, knownTypes) + || returnType.OriginalDefinition.Equals(knownTypes.TaskOfTType) + || returnType.OriginalDefinition.Equals(knownTypes.ValueTaskOfTType); + + private static bool IsTaskType(ITypeSymbol returnType, KnownTaskTypes knownTypes) + => returnType.OriginalDefinition.Equals(knownTypes.TaskType) + || returnType.OriginalDefinition.Equals(knownTypes.ValueTaskType); + + private SyntaxNode RemoveAsyncModifier( + SolutionServices solutionServices, + ISyntaxFacts syntaxFacts, + SyntaxGenerator generator, + SyntaxNode node, + ITypeSymbol returnType, + KnownTaskTypes knownTypes, + bool needsReturnStatementAdded) + { + node = RemoveAsyncModifier(generator, node); - var expression = generator.GetExpression(node); - if (expression is TExpressionSyntax expressionBody) + var expression = generator.GetExpression(node); + if (expression is TExpressionSyntax expressionBody) + { + if (IsTaskType(returnType, knownTypes)) { - if (IsTaskType(returnType, knownTypes)) - { - // We need to add a `return Task.CompletedTask;` so we have to convert to a block body - var blockBodiedNode = ConvertToBlockBody(node, expressionBody); - - // Expression bodied members can't have return statements so if we can't convert to a block - // body then we've done all we can - if (blockBodiedNode != null) - { - node = AddReturnStatement(generator, blockBodiedNode); - } - } - else + // We need to add a `return Task.CompletedTask;` so we have to convert to a block body + var blockBodiedNode = ConvertToBlockBody(node, expressionBody); + + // Expression bodied members can't have return statements so if we can't convert to a block + // body then we've done all we can + if (blockBodiedNode != null) { - // For Task returning expression bodied methods we can just wrap the whole expression - var newExpressionBody = WrapExpressionWithTaskFromResult(generator, expressionBody, returnType, knownTypes); - node = generator.WithExpression(node, newExpressionBody); + node = AddReturnStatement(generator, blockBodiedNode); } } else { - if (IsTaskType(returnType, knownTypes)) - { - // If the end of the method isn't reachable, or there were no statements to analyze, then we - // need to add an explicit return - if (needsReturnStatementAdded) - { - node = AddReturnStatement(generator, node); - } - } + // For Task returning expression bodied methods we can just wrap the whole expression + var newExpressionBody = WrapExpressionWithTaskFromResult(generator, expressionBody, returnType, knownTypes); + node = generator.WithExpression(node, newExpressionBody); } - - return ChangeReturnStatements(solutionServices, syntaxFacts, generator, node, returnType, knownTypes); } - - private static ControlFlowAnalysis? GetControlFlowAnalysis(SyntaxGenerator generator, SemanticModel semanticModel, SyntaxNode node) + else { - var statements = generator.GetStatements(node); - if (statements.Count > 0) + if (IsTaskType(returnType, knownTypes)) { - return semanticModel.AnalyzeControlFlow(statements[0], statements[statements.Count - 1]); + // If the end of the method isn't reachable, or there were no statements to analyze, then we + // need to add an explicit return + if (needsReturnStatementAdded) + { + node = AddReturnStatement(generator, node); + } } - - return null; } - private static SyntaxNode AddReturnStatement(SyntaxGenerator generator, SyntaxNode node) - => generator.WithStatements(node, generator.GetStatements(node).Concat(generator.ReturnStatement())); + return ChangeReturnStatements(solutionServices, syntaxFacts, generator, node, returnType, knownTypes); + } - private SyntaxNode ChangeReturnStatements( - SolutionServices solutionServices, - ISyntaxFacts syntaxFacts, - SyntaxGenerator generator, - SyntaxNode node, - ITypeSymbol returnType, - KnownTaskTypes knownTypes) + private static ControlFlowAnalysis? GetControlFlowAnalysis(SyntaxGenerator generator, SemanticModel semanticModel, SyntaxNode node) + { + var statements = generator.GetStatements(node); + if (statements.Count > 0) { - var editor = new SyntaxEditor(node, solutionServices); + return semanticModel.AnalyzeControlFlow(statements[0], statements[statements.Count - 1]); + } - // Look for all return statements, but if we find a new node that can have the async modifier we stop - // because that will have its own diagnostic and fix, if applicable - var returns = node.DescendantNodes(n => n == node || !IsAsyncSupportingFunctionSyntax(n)).OfType(); + return null; + } - foreach (var returnSyntax in returns) - { - var returnExpression = syntaxFacts.GetExpressionOfReturnStatement(returnSyntax); - if (returnExpression is null) - { - // Convert return; into return Task.CompletedTask; - var returnTaskCompletedTask = GetReturnTaskCompletedTaskStatement(generator, returnType, knownTypes); - editor.ReplaceNode(returnSyntax, returnTaskCompletedTask); - } - else - { - // Convert return ; into return Task.FromResult(); - var newExpression = WrapExpressionWithTaskFromResult(generator, returnExpression, returnType, knownTypes); - editor.ReplaceNode(returnExpression, newExpression); - } - } + private static SyntaxNode AddReturnStatement(SyntaxGenerator generator, SyntaxNode node) + => generator.WithStatements(node, generator.GetStatements(node).Concat(generator.ReturnStatement())); - return editor.GetChangedRoot(); - } + private SyntaxNode ChangeReturnStatements( + SolutionServices solutionServices, + ISyntaxFacts syntaxFacts, + SyntaxGenerator generator, + SyntaxNode node, + ITypeSymbol returnType, + KnownTaskTypes knownTypes) + { + var editor = new SyntaxEditor(node, solutionServices); - private static SyntaxNode GetReturnTaskCompletedTaskStatement(SyntaxGenerator generator, ITypeSymbol returnType, KnownTaskTypes knownTypes) + // Look for all return statements, but if we find a new node that can have the async modifier we stop + // because that will have its own diagnostic and fix, if applicable + var returns = node.DescendantNodes(n => n == node || !IsAsyncSupportingFunctionSyntax(n)).OfType(); + + foreach (var returnSyntax in returns) { - SyntaxNode invocation; - if (returnType.OriginalDefinition.Equals(knownTypes.TaskType)) + var returnExpression = syntaxFacts.GetExpressionOfReturnStatement(returnSyntax); + if (returnExpression is null) { - var taskTypeExpression = TypeExpressionForStaticMemberAccess(generator, knownTypes.TaskType); - invocation = generator.MemberAccessExpression(taskTypeExpression, nameof(Task.CompletedTask)); + // Convert return; into return Task.CompletedTask; + var returnTaskCompletedTask = GetReturnTaskCompletedTaskStatement(generator, returnType, knownTypes); + editor.ReplaceNode(returnSyntax, returnTaskCompletedTask); } else { - invocation = generator.ObjectCreationExpression(knownTypes.ValueTaskType!); + // Convert return ; into return Task.FromResult(); + var newExpression = WrapExpressionWithTaskFromResult(generator, returnExpression, returnType, knownTypes); + editor.ReplaceNode(returnExpression, newExpression); } - - var statement = generator.ReturnStatement(invocation); - return statement; } - private static SyntaxNode WrapExpressionWithTaskFromResult(SyntaxGenerator generator, SyntaxNode expression, ITypeSymbol returnType, KnownTaskTypes knownTypes) + return editor.GetChangedRoot(); + } + + private static SyntaxNode GetReturnTaskCompletedTaskStatement(SyntaxGenerator generator, ITypeSymbol returnType, KnownTaskTypes knownTypes) + { + SyntaxNode invocation; + if (returnType.OriginalDefinition.Equals(knownTypes.TaskType)) { - if (returnType.OriginalDefinition.Equals(knownTypes.TaskOfTType)) - { - var taskTypeExpression = TypeExpressionForStaticMemberAccess(generator, knownTypes.TaskType!); - var unwrappedReturnType = returnType.GetTypeArguments()[0]; - var memberName = generator.GenericName(nameof(Task.FromResult), unwrappedReturnType); - var taskFromResult = generator.MemberAccessExpression(taskTypeExpression, memberName); - return generator.InvocationExpression(taskFromResult, expression.WithoutTrivia()).WithTriviaFrom(expression); - } - else - { - return generator.ObjectCreationExpression(returnType, expression); - } + var taskTypeExpression = TypeExpressionForStaticMemberAccess(generator, knownTypes.TaskType); + invocation = generator.MemberAccessExpression(taskTypeExpression, nameof(Task.CompletedTask)); + } + else + { + invocation = generator.ObjectCreationExpression(knownTypes.ValueTaskType!); } - // Workaround for https://github.com/dotnet/roslyn/issues/43950 - // Copied from https://github.com/dotnet/roslyn-analyzers/blob/f24a5b42c85be6ee572f3a93bef223767fbefd75/src/Utilities/Workspaces/SyntaxGeneratorExtensions.cs#L68-L74 - private static SyntaxNode TypeExpressionForStaticMemberAccess(SyntaxGenerator generator, INamedTypeSymbol typeSymbol) + var statement = generator.ReturnStatement(invocation); + return statement; + } + + private static SyntaxNode WrapExpressionWithTaskFromResult(SyntaxGenerator generator, SyntaxNode expression, ITypeSymbol returnType, KnownTaskTypes knownTypes) + { + if (returnType.OriginalDefinition.Equals(knownTypes.TaskOfTType)) + { + var taskTypeExpression = TypeExpressionForStaticMemberAccess(generator, knownTypes.TaskType!); + var unwrappedReturnType = returnType.GetTypeArguments()[0]; + var memberName = generator.GenericName(nameof(Task.FromResult), unwrappedReturnType); + var taskFromResult = generator.MemberAccessExpression(taskTypeExpression, memberName); + return generator.InvocationExpression(taskFromResult, expression.WithoutTrivia()).WithTriviaFrom(expression); + } + else { - var qualifiedNameSyntaxKind = generator.QualifiedName(generator.IdentifierName("ignored"), generator.IdentifierName("ignored")).RawKind; - var memberAccessExpressionSyntaxKind = generator.MemberAccessExpression(generator.IdentifierName("ignored"), "ignored").RawKind; + return generator.ObjectCreationExpression(returnType, expression); + } + } - var typeExpression = generator.TypeExpression(typeSymbol); - return QualifiedNameToMemberAccess(qualifiedNameSyntaxKind, memberAccessExpressionSyntaxKind, typeExpression, generator); + // Workaround for https://github.com/dotnet/roslyn/issues/43950 + // Copied from https://github.com/dotnet/roslyn-analyzers/blob/f24a5b42c85be6ee572f3a93bef223767fbefd75/src/Utilities/Workspaces/SyntaxGeneratorExtensions.cs#L68-L74 + private static SyntaxNode TypeExpressionForStaticMemberAccess(SyntaxGenerator generator, INamedTypeSymbol typeSymbol) + { + var qualifiedNameSyntaxKind = generator.QualifiedName(generator.IdentifierName("ignored"), generator.IdentifierName("ignored")).RawKind; + var memberAccessExpressionSyntaxKind = generator.MemberAccessExpression(generator.IdentifierName("ignored"), "ignored").RawKind; - // Local function - static SyntaxNode QualifiedNameToMemberAccess(int qualifiedNameSyntaxKind, int memberAccessExpressionSyntaxKind, SyntaxNode expression, SyntaxGenerator generator) - { - if (expression.RawKind == qualifiedNameSyntaxKind) - { - var left = QualifiedNameToMemberAccess(qualifiedNameSyntaxKind, memberAccessExpressionSyntaxKind, expression.ChildNodes().First(), generator); - var right = expression.ChildNodes().Last(); - return generator.MemberAccessExpression(left, right); - } + var typeExpression = generator.TypeExpression(typeSymbol); + return QualifiedNameToMemberAccess(qualifiedNameSyntaxKind, memberAccessExpressionSyntaxKind, typeExpression, generator); - return expression; + // Local function + static SyntaxNode QualifiedNameToMemberAccess(int qualifiedNameSyntaxKind, int memberAccessExpressionSyntaxKind, SyntaxNode expression, SyntaxGenerator generator) + { + if (expression.RawKind == qualifiedNameSyntaxKind) + { + var left = QualifiedNameToMemberAccess(qualifiedNameSyntaxKind, memberAccessExpressionSyntaxKind, expression.ChildNodes().First(), generator); + var right = expression.ChildNodes().Last(); + return generator.MemberAccessExpression(left, right); } + + return expression; } } } diff --git a/src/Analyzers/Core/CodeFixes/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsCodeFixProvider.cs index 3f216df050960..31fe78427eca9 100644 --- a/src/Analyzers/Core/CodeFixes/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsCodeFixProvider.cs @@ -12,42 +12,41 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.RemoveUnnecessaryImports +namespace Microsoft.CodeAnalysis.RemoveUnnecessaryImports; + +internal abstract class AbstractRemoveUnnecessaryImportsCodeFixProvider : CodeFixProvider { - internal abstract class AbstractRemoveUnnecessaryImportsCodeFixProvider : CodeFixProvider + protected abstract ISyntaxFormatting GetSyntaxFormatting(); + + public sealed override ImmutableArray FixableDiagnosticIds + => [RemoveUnnecessaryImportsConstants.DiagnosticFixableId]; + + public sealed override FixAllProvider GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; + + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) { - protected abstract ISyntaxFormatting GetSyntaxFormatting(); - - public sealed override ImmutableArray FixableDiagnosticIds - => [RemoveUnnecessaryImportsConstants.DiagnosticFixableId]; - - public sealed override FixAllProvider GetFixAllProvider() - => WellKnownFixAllProviders.BatchFixer; - - public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var title = GetTitle(); - context.RegisterCodeFix( - CodeAction.Create( - title, - c => RemoveUnnecessaryImportsAsync(context.Document, context.GetOptionsProvider(), c), - title), - context.Diagnostics); - return Task.CompletedTask; - } - - protected abstract string GetTitle(); - - private async Task RemoveUnnecessaryImportsAsync( - Document document, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - var service = document.GetRequiredLanguageService(); - - var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var formattingOptions = options.GetFormattingOptions(GetSyntaxFormatting()); - return await service.RemoveUnnecessaryImportsAsync(document, formattingOptions, cancellationToken).ConfigureAwait(false); - } + var title = GetTitle(); + context.RegisterCodeFix( + CodeAction.Create( + title, + c => RemoveUnnecessaryImportsAsync(context.Document, context.GetOptionsProvider(), c), + title), + context.Diagnostics); + return Task.CompletedTask; + } + + protected abstract string GetTitle(); + + private async Task RemoveUnnecessaryImportsAsync( + Document document, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var service = document.GetRequiredLanguageService(); + + var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var formattingOptions = options.GetFormattingOptions(GetSyntaxFormatting()); + return await service.RemoveUnnecessaryImportsAsync(document, formattingOptions, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Analyzers/Core/CodeFixes/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesCodeFixProvider.cs index 6e6b6a83f0018..0e78d6cb60a46 100644 --- a/src/Analyzers/Core/CodeFixes/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesCodeFixProvider.cs @@ -13,38 +13,37 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses +namespace Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses; + +internal abstract class AbstractRemoveUnnecessaryParenthesesCodeFixProvider + : SyntaxEditorBasedCodeFixProvider + where TParenthesizedExpressionSyntax : SyntaxNode { - internal abstract class AbstractRemoveUnnecessaryParenthesesCodeFixProvider - : SyntaxEditorBasedCodeFixProvider - where TParenthesizedExpressionSyntax : SyntaxNode - { - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.RemoveUnnecessaryParenthesesDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.RemoveUnnecessaryParenthesesDiagnosticId]; - protected abstract bool CanRemoveParentheses( - TParenthesizedExpressionSyntax current, SemanticModel semanticModel, CancellationToken cancellationToken); + protected abstract bool CanRemoveParentheses( + TParenthesizedExpressionSyntax current, SemanticModel semanticModel, CancellationToken cancellationToken); - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, AnalyzersResources.Remove_unnecessary_parentheses, nameof(AnalyzersResources.Remove_unnecessary_parentheses)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, AnalyzersResources.Remove_unnecessary_parentheses, nameof(AnalyzersResources.Remove_unnecessary_parentheses)); + return Task.CompletedTask; + } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var syntaxFacts = document.GetRequiredLanguageService(); - var originalNodes = diagnostics.SelectAsArray( - d => (TParenthesizedExpressionSyntax)d.AdditionalLocations[0].FindNode( - findInsideTrivia: true, getInnermostNodeForTie: true, cancellationToken)); + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var originalNodes = diagnostics.SelectAsArray( + d => (TParenthesizedExpressionSyntax)d.AdditionalLocations[0].FindNode( + findInsideTrivia: true, getInnermostNodeForTie: true, cancellationToken)); - return editor.ApplyExpressionLevelSemanticEditsAsync( - document, originalNodes, - (semanticModel, current) => current != null && CanRemoveParentheses(current, semanticModel, cancellationToken), - (_, currentRoot, current) => currentRoot.ReplaceNode(current, syntaxFacts.Unparenthesize(current)), - cancellationToken); - } + return editor.ApplyExpressionLevelSemanticEditsAsync( + document, originalNodes, + (semanticModel, current) => current != null && CanRemoveParentheses(current, semanticModel, cancellationToken), + (_, currentRoot, current) => currentRoot.ReplaceNode(current, syntaxFacts.Unparenthesize(current)), + cancellationToken); } } diff --git a/src/Analyzers/Core/CodeFixes/RemoveUnusedMembers/AbstractRemoveUnusedMembersCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/RemoveUnusedMembers/AbstractRemoveUnusedMembersCodeFixProvider.cs index ca618a1e18f9f..69c2f4a991ade 100644 --- a/src/Analyzers/Core/CodeFixes/RemoveUnusedMembers/AbstractRemoveUnusedMembersCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/RemoveUnusedMembers/AbstractRemoveUnusedMembersCodeFixProvider.cs @@ -17,107 +17,106 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.RemoveUnusedMembers +namespace Microsoft.CodeAnalysis.RemoveUnusedMembers; + +internal abstract class AbstractRemoveUnusedMembersCodeFixProvider : SyntaxEditorBasedCodeFixProvider + where TFieldDeclarationSyntax : SyntaxNode { - internal abstract class AbstractRemoveUnusedMembersCodeFixProvider : SyntaxEditorBasedCodeFixProvider - where TFieldDeclarationSyntax : SyntaxNode - { - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.RemoveUnusedMembersDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.RemoveUnusedMembersDiagnosticId]; - /// - /// This method adjusts the to remove based on whether or not all variable declarators - /// within a field declaration should be removed, - /// i.e. if all the fields declared within a field declaration are unused, - /// we can remove the entire field declaration instead of individual variable declarators. - /// - protected abstract void AdjustAndAddAppropriateDeclaratorsToRemove(HashSet fieldDeclarators, HashSet declarators); + /// + /// This method adjusts the to remove based on whether or not all variable declarators + /// within a field declaration should be removed, + /// i.e. if all the fields declared within a field declaration are unused, + /// we can remove the entire field declaration instead of individual variable declarators. + /// + protected abstract void AdjustAndAddAppropriateDeclaratorsToRemove(HashSet fieldDeclarators, HashSet declarators); - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, AnalyzersResources.Remove_unused_member, nameof(AnalyzersResources.Remove_unused_member)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, AnalyzersResources.Remove_unused_member, nameof(AnalyzersResources.Remove_unused_member)); + return Task.CompletedTask; + } + + protected override async Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var declarators = new HashSet(); + var fieldDeclarators = new HashSet(); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var declarationService = document.GetRequiredLanguageService(); - protected override async Task FixAllAsync( - Document document, - ImmutableArray diagnostics, - SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + // Compute declarators to remove, and also track common field declarators. + foreach (var diagnostic in diagnostics) { - var declarators = new HashSet(); - var fieldDeclarators = new HashSet(); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var declarationService = document.GetRequiredLanguageService(); + // Get symbol to be removed. + var diagnosticNode = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); + var symbol = semanticModel.GetDeclaredSymbol(diagnosticNode, cancellationToken); + Contract.ThrowIfNull(symbol); - // Compute declarators to remove, and also track common field declarators. - foreach (var diagnostic in diagnostics) + // Get symbol declarations to be removed. + foreach (var declReference in declarationService.GetDeclarations(symbol)) { - // Get symbol to be removed. - var diagnosticNode = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken); - var symbol = semanticModel.GetDeclaredSymbol(diagnosticNode, cancellationToken); - Contract.ThrowIfNull(symbol); + var node = await declReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + declarators.Add(node); - // Get symbol declarations to be removed. - foreach (var declReference in declarationService.GetDeclarations(symbol)) + // For fields, the declaration node is the variable declarator. + // We also track the ancestor FieldDeclarationSyntax which may declare more then one field. + if (symbol.Kind == SymbolKind.Field) { - var node = await declReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); - declarators.Add(node); - - // For fields, the declaration node is the variable declarator. - // We also track the ancestor FieldDeclarationSyntax which may declare more then one field. - if (symbol.Kind == SymbolKind.Field) - { - var fieldDeclarator = node.FirstAncestorOrSelf(); - Contract.ThrowIfNull(fieldDeclarator); - fieldDeclarators.Add(fieldDeclarator); - } + var fieldDeclarator = node.FirstAncestorOrSelf(); + Contract.ThrowIfNull(fieldDeclarator); + fieldDeclarators.Add(fieldDeclarator); } } + } - // If all the fields declared within a field declaration are unused, - // we can remove the entire field declaration instead of individual variable declarators. - if (fieldDeclarators.Count > 0) - { - AdjustAndAddAppropriateDeclaratorsToRemove(fieldDeclarators, declarators); - } + // If all the fields declared within a field declaration are unused, + // we can remove the entire field declaration instead of individual variable declarators. + if (fieldDeclarators.Count > 0) + { + AdjustAndAddAppropriateDeclaratorsToRemove(fieldDeclarators, declarators); + } - // Remove all the symbol declarator nodes. - foreach (var declarator in declarators) - { - editor.RemoveNode(declarator); - } + // Remove all the symbol declarator nodes. + foreach (var declarator in declarators) + { + editor.RemoveNode(declarator); } + } - /// - /// If all the are contained in , - /// the removes the from , and - /// adds the to the . - /// - protected static void AdjustAndAddAppropriateDeclaratorsToRemove(SyntaxNode parentDeclaration, IEnumerable childDeclarators, HashSet declarators) + /// + /// If all the are contained in , + /// the removes the from , and + /// adds the to the . + /// + protected static void AdjustAndAddAppropriateDeclaratorsToRemove(SyntaxNode parentDeclaration, IEnumerable childDeclarators, HashSet declarators) + { + if (declarators.Contains(parentDeclaration)) { - if (declarators.Contains(parentDeclaration)) - { - Debug.Assert(childDeclarators.All(c => !declarators.Contains(c))); - return; - } + Debug.Assert(childDeclarators.All(c => !declarators.Contains(c))); + return; + } - var declaratorsContainsAllChildren = true; - foreach (var childDeclarator in childDeclarators) + var declaratorsContainsAllChildren = true; + foreach (var childDeclarator in childDeclarators) + { + if (!declarators.Contains(childDeclarator)) { - if (!declarators.Contains(childDeclarator)) - { - declaratorsContainsAllChildren = false; - break; - } + declaratorsContainsAllChildren = false; + break; } + } - if (declaratorsContainsAllChildren) - { - // Remove the entire parent declaration instead of individual child declarators within it. - declarators.Add(parentDeclaration); - declarators.RemoveAll(childDeclarators); - } + if (declaratorsContainsAllChildren) + { + // Remove the entire parent declaration instead of individual child declarators within it. + declarators.Add(parentDeclaration); + declarators.RemoveAll(childDeclarators); } } } diff --git a/src/Analyzers/Core/CodeFixes/RemoveUnusedParametersAndValues/AbstractRemoveUnusedValuesCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/RemoveUnusedParametersAndValues/AbstractRemoveUnusedValuesCodeFixProvider.cs index 58917176e3838..c176e52e9c84b 100644 --- a/src/Analyzers/Core/CodeFixes/RemoveUnusedParametersAndValues/AbstractRemoveUnusedValuesCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/RemoveUnusedParametersAndValues/AbstractRemoveUnusedValuesCodeFixProvider.cs @@ -26,929 +26,928 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues +namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues; + +/// +/// Code fixer for unused expression value diagnostics reported by . +/// We provide following code fixes: +/// 1. If the unused value assigned to a local/parameter has no side-effects, +/// we recommend removing the assignment. We consider an expression value to have no side effects +/// if one of the following is true: +/// 1. Value is a compile time constant. +/// 2. Value is a local or parameter reference. +/// 3. Value is a field reference with no or implicit this instance. +/// 2. Otherwise, if user preference is set to DiscardVariable, and project's +/// language version supports discard variable, we recommend assigning the value to discard. +/// 3. Otherwise, we recommend assigning the value to a new unused local variable which has no reads. +/// +internal abstract class AbstractRemoveUnusedValuesCodeFixProvider + : SyntaxEditorBasedCodeFixProvider + where TExpressionSyntax : SyntaxNode + where TStatementSyntax : SyntaxNode + where TBlockSyntax : TStatementSyntax + where TExpressionStatementSyntax : TStatementSyntax + where TLocalDeclarationStatementSyntax : TStatementSyntax + where TForEachStatementSyntax : TStatementSyntax + where TVariableDeclaratorSyntax : SyntaxNode + where TSwitchCaseBlockSyntax : SyntaxNode + where TSwitchCaseLabelOrClauseSyntax : SyntaxNode { - /// - /// Code fixer for unused expression value diagnostics reported by . - /// We provide following code fixes: - /// 1. If the unused value assigned to a local/parameter has no side-effects, - /// we recommend removing the assignment. We consider an expression value to have no side effects - /// if one of the following is true: - /// 1. Value is a compile time constant. - /// 2. Value is a local or parameter reference. - /// 3. Value is a field reference with no or implicit this instance. - /// 2. Otherwise, if user preference is set to DiscardVariable, and project's - /// language version supports discard variable, we recommend assigning the value to discard. - /// 3. Otherwise, we recommend assigning the value to a new unused local variable which has no reads. - /// - internal abstract class AbstractRemoveUnusedValuesCodeFixProvider - : SyntaxEditorBasedCodeFixProvider - where TExpressionSyntax : SyntaxNode - where TStatementSyntax : SyntaxNode - where TBlockSyntax : TStatementSyntax - where TExpressionStatementSyntax : TStatementSyntax - where TLocalDeclarationStatementSyntax : TStatementSyntax - where TForEachStatementSyntax : TStatementSyntax - where TVariableDeclaratorSyntax : SyntaxNode - where TSwitchCaseBlockSyntax : SyntaxNode - where TSwitchCaseLabelOrClauseSyntax : SyntaxNode - { - private static readonly SyntaxAnnotation s_memberAnnotation = new(); - private static readonly SyntaxAnnotation s_newLocalDeclarationStatementAnnotation = new(); - private static readonly SyntaxAnnotation s_unusedLocalDeclarationAnnotation = new(); - private static readonly SyntaxAnnotation s_existingLocalDeclarationWithoutInitializerAnnotation = new(); - - public sealed override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId, IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId]; - - protected abstract ISyntaxFormatting GetSyntaxFormatting(); - - /// - /// Method to update the identifier token for the local/parameter declaration or reference - /// that was flagged as an unused value write by the analyzer. - /// Returns null if the provided node is not one of the handled node kinds. - /// Otherwise, returns the new node with updated identifier. - /// - /// Flagged node containing the identifier token to be replaced. - /// New identifier token - protected abstract SyntaxNode TryUpdateNameForFlaggedNode(SyntaxNode node, SyntaxToken newName); - - /// - /// Gets the identifier token for the iteration variable of the given foreach statement node. - /// - protected abstract SyntaxToken GetForEachStatementIdentifier(TForEachStatementSyntax node); - - /// - /// Wraps the given statements within a block statement. - /// Note this method is invoked when replacing a statement that is parented by a non-block statement syntax. - /// - protected abstract TBlockSyntax WrapWithBlockIfNecessary(IEnumerable statements); - - /// - /// Inserts the given declaration statement at the start of the given switch case block. - /// - protected abstract void InsertAtStartOfSwitchCaseBlockForDeclarationInCaseLabelOrClause(TSwitchCaseBlockSyntax switchCaseBlock, SyntaxEditor editor, TLocalDeclarationStatementSyntax declarationStatement); - - /// - /// Gets the replacement node for a compound assignment expression whose - /// assigned value is redundant. - /// For example, "x += MethodCall()", where assignment to 'x' is redundant - /// is replaced with "_ = MethodCall()" or "var unused = MethodCall()" - /// - protected abstract SyntaxNode GetReplacementNodeForCompoundAssignment( - SyntaxNode originalCompoundAssignment, - SyntaxNode newAssignmentTarget, - SyntaxEditor editor, - ISyntaxFactsService syntaxFacts); - - /// - /// Gets the replacement node for a var pattern. - /// We need just to change the identifier of the pattern, not the whole node - /// - protected abstract SyntaxNode GetReplacementNodeForVarPattern(SyntaxNode originalVarPattern, SyntaxNode newNameNode); - - /// - /// Rewrite the parent of a node which was rewritten by . - /// - /// The original parent of the node rewritten by . - /// The rewritten node produced by . - /// The syntax editor for the code fix. - /// The syntax facts for the current language. - /// Semantic model for the tree. - /// The replacement node to use in the rewritten syntax tree; otherwise, to only - /// rewrite the node originally rewritten by . - protected virtual SyntaxNode? TryUpdateParentOfUpdatedNode(SyntaxNode parent, SyntaxNode newNameNode, SyntaxEditor editor, ISyntaxFacts syntaxFacts, SemanticModel semanticModel) => null; - - /// - /// Computes correct replacement node, including cases with recursive changes (e.g. recursive pattern node rewrite in fix-all scenario) - /// - /// The original node for replacement - /// Node for replacement transformed by previous replacements - /// Proposed replacement node with changes relative to - /// The final replacement for the node - protected abstract SyntaxNode ComputeReplacementNode(SyntaxNode originalOldNode, SyntaxNode changedOldNode, SyntaxNode proposedReplacementNode); - - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var diagnostic = context.Diagnostics[0]; - if (!AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostic, out var preference)) - { - return; - } + private static readonly SyntaxAnnotation s_memberAnnotation = new(); + private static readonly SyntaxAnnotation s_newLocalDeclarationStatementAnnotation = new(); + private static readonly SyntaxAnnotation s_unusedLocalDeclarationAnnotation = new(); + private static readonly SyntaxAnnotation s_existingLocalDeclarationWithoutInitializerAnnotation = new(); - var isRemovableAssignment = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostic); + public sealed override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId, IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId]; - string title; - if (isRemovableAssignment) - { - // Recommend removing the redundant constant value assignment. - title = CodeFixesResources.Remove_redundant_assignment; - } - else - { - // Recommend using discard/unused local for redundant non-constant assignment. - switch (preference) - { - case UnusedValuePreference.DiscardVariable: - if (IsForEachIterationVariableDiagnostic(diagnostic, context.Document, context.CancellationToken)) - { - // Do not offer a fix to replace unused foreach iteration variable with discard. - // User should probably replace it with a for loop based on the collection length. - return; - } + protected abstract ISyntaxFormatting GetSyntaxFormatting(); - title = CodeFixesResources.Use_discard_underscore; + /// + /// Method to update the identifier token for the local/parameter declaration or reference + /// that was flagged as an unused value write by the analyzer. + /// Returns null if the provided node is not one of the handled node kinds. + /// Otherwise, returns the new node with updated identifier. + /// + /// Flagged node containing the identifier token to be replaced. + /// New identifier token + protected abstract SyntaxNode TryUpdateNameForFlaggedNode(SyntaxNode node, SyntaxToken newName); - var syntaxFacts = context.Document.GetRequiredLanguageService(); - var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var node = root.FindNode(context.Span, getInnermostNodeForTie: true); + /// + /// Gets the identifier token for the iteration variable of the given foreach statement node. + /// + protected abstract SyntaxToken GetForEachStatementIdentifier(TForEachStatementSyntax node); - // Check if this is compound assignment which is not parented by an expression statement, - // for example "return x += M();" OR "=> x ??= new C();" - // If so, we will be replacing this compound assignment with the underlying binary operation. - // For the above examples, it will be "return x + M();" AND "=> x ?? new C();" respectively. - // For these cases, we want to show the title as "Remove redundant assignment" instead of "Use discard _". - if (syntaxFacts.IsLeftSideOfCompoundAssignment(node) && - !syntaxFacts.IsExpressionStatement(node.Parent)) - { - title = CodeFixesResources.Remove_redundant_assignment; - } - // Also we want to show "Remove redundant assignment" title for variable designation in pattern matching, - // since this assignment will be fully removed. Cases: - // 1) `if (obj is SomeType someType)` - // 2) `if (obj is { } someType)` - // 3) `if (obj is [] someType)` - else if (syntaxFacts.IsDeclarationPattern(node.Parent) || - syntaxFacts.IsRecursivePattern(node.Parent) || - syntaxFacts.IsListPattern(node.Parent)) - { - title = CodeFixesResources.Remove_redundant_assignment; - } + /// + /// Wraps the given statements within a block statement. + /// Note this method is invoked when replacing a statement that is parented by a non-block statement syntax. + /// + protected abstract TBlockSyntax WrapWithBlockIfNecessary(IEnumerable statements); - break; + /// + /// Inserts the given declaration statement at the start of the given switch case block. + /// + protected abstract void InsertAtStartOfSwitchCaseBlockForDeclarationInCaseLabelOrClause(TSwitchCaseBlockSyntax switchCaseBlock, SyntaxEditor editor, TLocalDeclarationStatementSyntax declarationStatement); - case UnusedValuePreference.UnusedLocalVariable: - title = CodeFixesResources.Use_discarded_local; - break; + /// + /// Gets the replacement node for a compound assignment expression whose + /// assigned value is redundant. + /// For example, "x += MethodCall()", where assignment to 'x' is redundant + /// is replaced with "_ = MethodCall()" or "var unused = MethodCall()" + /// + protected abstract SyntaxNode GetReplacementNodeForCompoundAssignment( + SyntaxNode originalCompoundAssignment, + SyntaxNode newAssignmentTarget, + SyntaxEditor editor, + ISyntaxFactsService syntaxFacts); - default: - return; - } - } + /// + /// Gets the replacement node for a var pattern. + /// We need just to change the identifier of the pattern, not the whole node + /// + protected abstract SyntaxNode GetReplacementNodeForVarPattern(SyntaxNode originalVarPattern, SyntaxNode newNameNode); - RegisterCodeFix(context, title, GetEquivalenceKey(preference, isRemovableAssignment)); - } + /// + /// Rewrite the parent of a node which was rewritten by . + /// + /// The original parent of the node rewritten by . + /// The rewritten node produced by . + /// The syntax editor for the code fix. + /// The syntax facts for the current language. + /// Semantic model for the tree. + /// The replacement node to use in the rewritten syntax tree; otherwise, to only + /// rewrite the node originally rewritten by . + protected virtual SyntaxNode? TryUpdateParentOfUpdatedNode(SyntaxNode parent, SyntaxNode newNameNode, SyntaxEditor editor, ISyntaxFacts syntaxFacts, SemanticModel semanticModel) => null; - private static bool IsForEachIterationVariableDiagnostic(Diagnostic diagnostic, Document document, CancellationToken cancellationToken) + /// + /// Computes correct replacement node, including cases with recursive changes (e.g. recursive pattern node rewrite in fix-all scenario) + /// + /// The original node for replacement + /// Node for replacement transformed by previous replacements + /// Proposed replacement node with changes relative to + /// The final replacement for the node + protected abstract SyntaxNode ComputeReplacementNode(SyntaxNode originalOldNode, SyntaxNode changedOldNode, SyntaxNode proposedReplacementNode); + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics[0]; + if (!AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostic, out var preference)) { - // Do not offer a fix to replace unused foreach iteration variable with discard. - // User should probably replace it with a for loop based on the collection length. - var syntaxFacts = document.GetRequiredLanguageService(); - return syntaxFacts.IsForEachStatement(diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken)); + return; } - private static string GetEquivalenceKey(UnusedValuePreference preference, bool isRemovableAssignment) - => preference.ToString() + isRemovableAssignment; + var isRemovableAssignment = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostic); - private static string GetEquivalenceKey(Diagnostic diagnostic) + string title; + if (isRemovableAssignment) { - if (!AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostic, out var preference)) + // Recommend removing the redundant constant value assignment. + title = CodeFixesResources.Remove_redundant_assignment; + } + else + { + // Recommend using discard/unused local for redundant non-constant assignment. + switch (preference) { - return string.Empty; - } + case UnusedValuePreference.DiscardVariable: + if (IsForEachIterationVariableDiagnostic(diagnostic, context.Document, context.CancellationToken)) + { + // Do not offer a fix to replace unused foreach iteration variable with discard. + // User should probably replace it with a for loop based on the collection length. + return; + } - var isRemovableAssignment = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostic); - return GetEquivalenceKey(preference, isRemovableAssignment); - } + title = CodeFixesResources.Use_discard_underscore; - /// - /// Flag to indicate if the code fix can introduce local declaration statements - /// that need to be moved closer to the first reference of the declared variable. - /// This is currently only possible for the unused value assignment fix. - /// - private static bool NeedsToMoveNewLocalDeclarationsNearReference(string diagnosticId) - => diagnosticId == IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId; + var syntaxFacts = context.Document.GetRequiredLanguageService(); + var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var node = root.FindNode(context.Span, getInnermostNodeForTie: true); - protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic, Document document, string? equivalenceKey, CancellationToken cancellationToken) - { - return equivalenceKey == GetEquivalenceKey(diagnostic) && - !IsForEachIterationVariableDiagnostic(diagnostic, document, cancellationToken); - } + // Check if this is compound assignment which is not parented by an expression statement, + // for example "return x += M();" OR "=> x ??= new C();" + // If so, we will be replacing this compound assignment with the underlying binary operation. + // For the above examples, it will be "return x + M();" AND "=> x ?? new C();" respectively. + // For these cases, we want to show the title as "Remove redundant assignment" instead of "Use discard _". + if (syntaxFacts.IsLeftSideOfCompoundAssignment(node) && + !syntaxFacts.IsExpressionStatement(node.Parent)) + { + title = CodeFixesResources.Remove_redundant_assignment; + } + // Also we want to show "Remove redundant assignment" title for variable designation in pattern matching, + // since this assignment will be fully removed. Cases: + // 1) `if (obj is SomeType someType)` + // 2) `if (obj is { } someType)` + // 3) `if (obj is [] someType)` + else if (syntaxFacts.IsDeclarationPattern(node.Parent) || + syntaxFacts.IsRecursivePattern(node.Parent) || + syntaxFacts.IsListPattern(node.Parent)) + { + title = CodeFixesResources.Remove_redundant_assignment; + } - private static IEnumerable> GetDiagnosticsGroupedByMember( - ImmutableArray diagnostics, - ISyntaxFactsService syntaxFacts, - SyntaxNode root, - out string diagnosticId, - out UnusedValuePreference preference, - out bool removeAssignments) - { - diagnosticId = diagnostics[0].Id; - var success = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostics[0], out preference); - Debug.Assert(success); - removeAssignments = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostics[0]); -#if DEBUG - foreach (var diagnostic in diagnostics) - { - Debug.Assert(diagnosticId == diagnostic.Id); - Debug.Assert(AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostic, out var diagnosticPreference) && - diagnosticPreference == preference); - Debug.Assert(removeAssignments == AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostic)); - } -#endif + break; - return GetDiagnosticsGroupedByMember(diagnostics, syntaxFacts, root); - } + case UnusedValuePreference.UnusedLocalVariable: + title = CodeFixesResources.Use_discarded_local; + break; - private static IEnumerable> GetDiagnosticsGroupedByMember( - ImmutableArray diagnostics, - ISyntaxFactsService syntaxFacts, - SyntaxNode root) - { - return diagnostics.GroupBy(d => syntaxFacts.GetContainingMemberDeclaration(root, d.Location.SourceSpan.Start) ?? root); + default: + return; + } } - private static async Task PreprocessDocumentAsync(Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + RegisterCodeFix(context, title, GetEquivalenceKey(preference, isRemovableAssignment)); + } + + private static bool IsForEachIterationVariableDiagnostic(Diagnostic diagnostic, Document document, CancellationToken cancellationToken) + { + // Do not offer a fix to replace unused foreach iteration variable with discard. + // User should probably replace it with a for loop based on the collection length. + var syntaxFacts = document.GetRequiredLanguageService(); + return syntaxFacts.IsForEachStatement(diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken)); + } + + private static string GetEquivalenceKey(UnusedValuePreference preference, bool isRemovableAssignment) + => preference.ToString() + isRemovableAssignment; + + private static string GetEquivalenceKey(Diagnostic diagnostic) + { + if (!AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostic, out var preference)) { - // Track all the member declaration nodes that have diagnostics. - // We will post process all these tracked nodes after applying the fix (see "PostProcessDocumentAsync" below in this source file). - - var syntaxFacts = document.GetRequiredLanguageService(); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var memberDeclarations = GetDiagnosticsGroupedByMember(diagnostics, syntaxFacts, root).Select(g => g.Key); - root = root.ReplaceNodes(memberDeclarations, computeReplacementNode: (_, n) => n.WithAdditionalAnnotations(s_memberAnnotation)); - return document.WithSyntaxRoot(root); + return string.Empty; } - protected sealed override async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + var isRemovableAssignment = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostic); + return GetEquivalenceKey(preference, isRemovableAssignment); + } + + /// + /// Flag to indicate if the code fix can introduce local declaration statements + /// that need to be moved closer to the first reference of the declared variable. + /// This is currently only possible for the unused value assignment fix. + /// + private static bool NeedsToMoveNewLocalDeclarationsNearReference(string diagnosticId) + => diagnosticId == IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId; + + protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic, Document document, string? equivalenceKey, CancellationToken cancellationToken) + { + return equivalenceKey == GetEquivalenceKey(diagnostic) && + !IsForEachIterationVariableDiagnostic(diagnostic, document, cancellationToken); + } + + private static IEnumerable> GetDiagnosticsGroupedByMember( + ImmutableArray diagnostics, + ISyntaxFactsService syntaxFacts, + SyntaxNode root, + out string diagnosticId, + out UnusedValuePreference preference, + out bool removeAssignments) + { + diagnosticId = diagnostics[0].Id; + var success = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostics[0], out preference); + Debug.Assert(success); + removeAssignments = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostics[0]); +#if DEBUG + foreach (var diagnostic in diagnostics) { - var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var formattingOptions = options.GetFormattingOptions(GetSyntaxFormatting()); - var preprocessedDocument = await PreprocessDocumentAsync(document, diagnostics, cancellationToken).ConfigureAwait(false); - var newRoot = await GetNewRootAsync(preprocessedDocument, formattingOptions, diagnostics, cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(editor.OriginalRoot, newRoot); + Debug.Assert(diagnosticId == diagnostic.Id); + Debug.Assert(AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostic, out var diagnosticPreference) && + diagnosticPreference == preference); + Debug.Assert(removeAssignments == AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostic)); } +#endif + + return GetDiagnosticsGroupedByMember(diagnostics, syntaxFacts, root); + } + + private static IEnumerable> GetDiagnosticsGroupedByMember( + ImmutableArray diagnostics, + ISyntaxFactsService syntaxFacts, + SyntaxNode root) + { + return diagnostics.GroupBy(d => syntaxFacts.GetContainingMemberDeclaration(root, d.Location.SourceSpan.Start) ?? root); + } + + private static async Task PreprocessDocumentAsync(Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + { + // Track all the member declaration nodes that have diagnostics. + // We will post process all these tracked nodes after applying the fix (see "PostProcessDocumentAsync" below in this source file). + + var syntaxFacts = document.GetRequiredLanguageService(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var memberDeclarations = GetDiagnosticsGroupedByMember(diagnostics, syntaxFacts, root).Select(g => g.Key); + root = root.ReplaceNodes(memberDeclarations, computeReplacementNode: (_, n) => n.WithAdditionalAnnotations(s_memberAnnotation)); + return document.WithSyntaxRoot(root); + } - private async Task GetNewRootAsync( - Document document, - SyntaxFormattingOptions options, - ImmutableArray diagnostics, - CancellationToken cancellationToken) + protected sealed override async Task FixAllAsync(Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var formattingOptions = options.GetFormattingOptions(GetSyntaxFormatting()); + var preprocessedDocument = await PreprocessDocumentAsync(document, diagnostics, cancellationToken).ConfigureAwait(false); + var newRoot = await GetNewRootAsync(preprocessedDocument, formattingOptions, diagnostics, cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(editor.OriginalRoot, newRoot); + } + + private async Task GetNewRootAsync( + Document document, + SyntaxFormattingOptions options, + ImmutableArray diagnostics, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + var semanticFacts = document.GetRequiredLanguageService(); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var editor = new SyntaxEditor(root, document.Project.Solution.Services); + + // We compute the code fix in two passes: + // 1. The first pass groups the diagnostics to fix by containing member declaration and + // computes and applies the core code fixes. Grouping is done to ensure we choose + // the most appropriate name for new unused local declarations, which can clash + // with existing local declarations in the method body. + // 2. Second pass (PostProcessDocumentAsync) performs additional syntax manipulations + // for the fixes produced from the first pass: + // a. Replace discard declarations, such as "var _ = M();" that conflict with newly added + // discard assignments, with discard assignments of the form "_ = M();" + // b. Move newly introduced local declaration statements closer to the local variable's + // first reference. + + // Get diagnostics grouped by member. + var diagnosticsGroupedByMember = GetDiagnosticsGroupedByMember(diagnostics, syntaxFacts, root, + out var diagnosticId, out var preference, out var removeAssignments); + + // First pass to compute and apply the core code fixes. + foreach (var diagnosticsToFix in diagnosticsGroupedByMember) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); - var semanticFacts = document.GetRequiredLanguageService(); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var editor = new SyntaxEditor(root, document.Project.Solution.Services); - - // We compute the code fix in two passes: - // 1. The first pass groups the diagnostics to fix by containing member declaration and - // computes and applies the core code fixes. Grouping is done to ensure we choose - // the most appropriate name for new unused local declarations, which can clash - // with existing local declarations in the method body. - // 2. Second pass (PostProcessDocumentAsync) performs additional syntax manipulations - // for the fixes produced from the first pass: - // a. Replace discard declarations, such as "var _ = M();" that conflict with newly added - // discard assignments, with discard assignments of the form "_ = M();" - // b. Move newly introduced local declaration statements closer to the local variable's - // first reference. - - // Get diagnostics grouped by member. - var diagnosticsGroupedByMember = GetDiagnosticsGroupedByMember(diagnostics, syntaxFacts, root, - out var diagnosticId, out var preference, out var removeAssignments); - - // First pass to compute and apply the core code fixes. - foreach (var diagnosticsToFix in diagnosticsGroupedByMember) - { - var containingMemberDeclaration = diagnosticsToFix.Key; - using var nameGenerator = new UniqueVariableNameGenerator(containingMemberDeclaration, semanticModel, semanticFacts, cancellationToken); + var containingMemberDeclaration = diagnosticsToFix.Key; + using var nameGenerator = new UniqueVariableNameGenerator(containingMemberDeclaration, semanticModel, semanticFacts, cancellationToken); - await FixAllAsync( - diagnosticId, diagnosticsToFix.Select(d => d), - document, semanticModel, root, containingMemberDeclaration, preference, - removeAssignments, nameGenerator, editor, cancellationToken).ConfigureAwait(false); - } + await FixAllAsync( + diagnosticId, diagnosticsToFix.Select(d => d), + document, semanticModel, root, containingMemberDeclaration, preference, + removeAssignments, nameGenerator, editor, cancellationToken).ConfigureAwait(false); + } + + // Second pass to post process the document. + var currentRoot = editor.GetChangedRoot(); + var newRoot = await PostProcessDocumentAsync(document, options, currentRoot, + diagnosticId, preference, cancellationToken).ConfigureAwait(false); - // Second pass to post process the document. - var currentRoot = editor.GetChangedRoot(); - var newRoot = await PostProcessDocumentAsync(document, options, currentRoot, - diagnosticId, preference, cancellationToken).ConfigureAwait(false); + if (currentRoot != newRoot) + editor.ReplaceNode(root, newRoot); - if (currentRoot != newRoot) - editor.ReplaceNode(root, newRoot); + return editor.GetChangedRoot(); + } - return editor.GetChangedRoot(); + private async Task FixAllAsync( + string diagnosticId, + IEnumerable diagnostics, + Document document, + SemanticModel semanticModel, + SyntaxNode root, + SyntaxNode containingMemberDeclaration, + UnusedValuePreference preference, + bool removeAssignments, + UniqueVariableNameGenerator nameGenerator, + SyntaxEditor editor, + CancellationToken cancellationToken) + { + switch (diagnosticId) + { + case IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId: + // Make sure the inner diagnostics are placed first + FixAllExpressionValueIsUnusedDiagnostics( + diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start), + document, semanticModel, root, preference, nameGenerator, editor); + break; + + case IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId: + // Make sure the diagnostics are placed in order. + // Example: + // int a = 0; int b = 1; + // After fix it would be int a; int b; + await FixAllValueAssignedIsUnusedDiagnosticsAsync( + diagnostics.OrderBy(d => d.Location.SourceSpan.Start), + document, semanticModel, root, containingMemberDeclaration, + preference, removeAssignments, nameGenerator, editor, cancellationToken).ConfigureAwait(false); + break; + + default: + throw ExceptionUtilities.Unreachable(); } + } - private async Task FixAllAsync( - string diagnosticId, - IEnumerable diagnostics, - Document document, - SemanticModel semanticModel, - SyntaxNode root, - SyntaxNode containingMemberDeclaration, - UnusedValuePreference preference, - bool removeAssignments, - UniqueVariableNameGenerator nameGenerator, - SyntaxEditor editor, - CancellationToken cancellationToken) + private static void FixAllExpressionValueIsUnusedDiagnostics( + IOrderedEnumerable diagnostics, + Document document, + SemanticModel semanticModel, + SyntaxNode root, + UnusedValuePreference preference, + UniqueVariableNameGenerator nameGenerator, + SyntaxEditor editor) + { + var syntaxFacts = document.GetRequiredLanguageService(); + + // This method applies the code fix for diagnostics reported for expression statement dropping values. + // We replace each flagged expression statement with an assignment to a discard variable or a new unused local, + // based on the user's preference. + // Note: The diagnostic order here should be inner first and outer second. + // Example: Foo1(() => { Foo2(); }) + // Foo2() should be the first in this case. + foreach (var diagnostic in diagnostics) { - switch (diagnosticId) + var expressionStatement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); + if (expressionStatement == null) { - case IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId: - // Make sure the inner diagnostics are placed first - FixAllExpressionValueIsUnusedDiagnostics( - diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start), - document, semanticModel, root, preference, nameGenerator, editor); - break; + continue; + } - case IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId: - // Make sure the diagnostics are placed in order. - // Example: - // int a = 0; int b = 1; - // After fix it would be int a; int b; - await FixAllValueAssignedIsUnusedDiagnosticsAsync( - diagnostics.OrderBy(d => d.Location.SourceSpan.Start), - document, semanticModel, root, containingMemberDeclaration, - preference, removeAssignments, nameGenerator, editor, cancellationToken).ConfigureAwait(false); + switch (preference) + { + case UnusedValuePreference.DiscardVariable: + Debug.Assert(semanticModel.Language != LanguageNames.VisualBasic); + var expression = syntaxFacts.GetExpressionOfExpressionStatement(expressionStatement); + editor.ReplaceNode(expression, (node, generator) => + { + var discardAssignmentExpression = (TExpressionSyntax)generator.AssignmentStatement( + left: generator.IdentifierName(AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName), + right: node.WithoutTrivia()) + .WithTriviaFrom(node) + .WithAdditionalAnnotations(Simplifier.Annotation, Formatter.Annotation); + return discardAssignmentExpression; + }); break; - default: - throw ExceptionUtilities.Unreachable(); + case UnusedValuePreference.UnusedLocalVariable: + var name = nameGenerator.GenerateUniqueNameAtSpanStart(expressionStatement).ValueText; + editor.ReplaceNode(expressionStatement, (node, generator) => + { + var expression = syntaxFacts.GetExpressionOfExpressionStatement(node); + // Add Simplifier annotation so that 'var'/explicit type is correctly added based on user options. + var localDecl = editor.Generator.LocalDeclarationStatement( + name: name, + initializer: expression.WithoutLeadingTrivia()) + .WithTriviaFrom(node) + .WithAdditionalAnnotations(Simplifier.Annotation, Formatter.Annotation); + return localDecl; + }); + break; } } + } - private static void FixAllExpressionValueIsUnusedDiagnostics( - IOrderedEnumerable diagnostics, - Document document, - SemanticModel semanticModel, - SyntaxNode root, - UnusedValuePreference preference, - UniqueVariableNameGenerator nameGenerator, - SyntaxEditor editor) + private async Task FixAllValueAssignedIsUnusedDiagnosticsAsync( + IOrderedEnumerable diagnostics, + Document document, + SemanticModel semanticModel, + SyntaxNode root, + SyntaxNode containingMemberDeclaration, + UnusedValuePreference preference, + bool removeAssignments, + UniqueVariableNameGenerator nameGenerator, + SyntaxEditor editor, + CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var blockFacts = document.GetRequiredLanguageService(); + + // This method applies the code fix for diagnostics reported for unused value assignments to local/parameter. + // The actual code fix depends on whether or not the right hand side of the assignment has side effects. + // For example, if the right hand side is a constant or a reference to a local/parameter, then it has no side effects. + // The lack of side effects is indicated by the "removeAssignments" parameter for this function. + + // If the right hand side has no side effects, then we can replace the assignments with variable declarations that have no initializer + // or completely remove the statement. + // If the right hand side does have side effects, we replace the identifier token for unused value assignment with + // a new identifier token (either discard '_' or new unused local variable name). + + // For both the above cases, if the original diagnostic was reported on a local declaration, i.e. redundant initialization + // at declaration, then we also add a new variable declaration statement without initializer for this local. + + using var _1 = PooledDictionary.GetInstance(out var nodeReplacementMap); + using var _2 = PooledHashSet.GetInstance(out var nodesToRemove); + using var _3 = PooledHashSet<(TLocalDeclarationStatementSyntax declarationStatement, SyntaxNode node)>.GetInstance(out var nodesToAdd); + // Indicates if the node's trivia was processed. + using var _4 = PooledHashSet.GetInstance(out var processedNodes); + using var _5 = PooledHashSet.GetInstance(out var candidateDeclarationStatementsForRemoval); + var hasAnyUnusedLocalAssignment = false; + + foreach (var (node, isUnusedLocalAssignment) in GetNodesToFix()) { - var syntaxFacts = document.GetRequiredLanguageService(); - - // This method applies the code fix for diagnostics reported for expression statement dropping values. - // We replace each flagged expression statement with an assignment to a discard variable or a new unused local, - // based on the user's preference. - // Note: The diagnostic order here should be inner first and outer second. - // Example: Foo1(() => { Foo2(); }) - // Foo2() should be the first in this case. - foreach (var diagnostic in diagnostics) - { - var expressionStatement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); - if (expressionStatement == null) - { - continue; - } + hasAnyUnusedLocalAssignment |= isUnusedLocalAssignment; - switch (preference) - { - case UnusedValuePreference.DiscardVariable: - Debug.Assert(semanticModel.Language != LanguageNames.VisualBasic); - var expression = syntaxFacts.GetExpressionOfExpressionStatement(expressionStatement); - editor.ReplaceNode(expression, (node, generator) => - { - var discardAssignmentExpression = (TExpressionSyntax)generator.AssignmentStatement( - left: generator.IdentifierName(AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName), - right: node.WithoutTrivia()) - .WithTriviaFrom(node) - .WithAdditionalAnnotations(Simplifier.Annotation, Formatter.Annotation); - return discardAssignmentExpression; - }); - break; - - case UnusedValuePreference.UnusedLocalVariable: - var name = nameGenerator.GenerateUniqueNameAtSpanStart(expressionStatement).ValueText; - editor.ReplaceNode(expressionStatement, (node, generator) => - { - var expression = syntaxFacts.GetExpressionOfExpressionStatement(node); - // Add Simplifier annotation so that 'var'/explicit type is correctly added based on user options. - var localDecl = editor.Generator.LocalDeclarationStatement( - name: name, - initializer: expression.WithoutLeadingTrivia()) - .WithTriviaFrom(node) - .WithAdditionalAnnotations(Simplifier.Annotation, Formatter.Annotation); - return localDecl; - }); - break; - } + var declaredLocal = semanticModel.GetDeclaredSymbol(node, cancellationToken) as ILocalSymbol; + if (declaredLocal == null && node.Parent is TCatchStatementSyntax) + { + declaredLocal = semanticModel.GetDeclaredSymbol(node.Parent, cancellationToken) as ILocalSymbol; } - } - private async Task FixAllValueAssignedIsUnusedDiagnosticsAsync( - IOrderedEnumerable diagnostics, - Document document, - SemanticModel semanticModel, - SyntaxNode root, - SyntaxNode containingMemberDeclaration, - UnusedValuePreference preference, - bool removeAssignments, - UniqueVariableNameGenerator nameGenerator, - SyntaxEditor editor, - CancellationToken cancellationToken) - { - var syntaxFacts = document.GetRequiredLanguageService(); - var blockFacts = document.GetRequiredLanguageService(); - - // This method applies the code fix for diagnostics reported for unused value assignments to local/parameter. - // The actual code fix depends on whether or not the right hand side of the assignment has side effects. - // For example, if the right hand side is a constant or a reference to a local/parameter, then it has no side effects. - // The lack of side effects is indicated by the "removeAssignments" parameter for this function. - - // If the right hand side has no side effects, then we can replace the assignments with variable declarations that have no initializer - // or completely remove the statement. - // If the right hand side does have side effects, we replace the identifier token for unused value assignment with - // a new identifier token (either discard '_' or new unused local variable name). - - // For both the above cases, if the original diagnostic was reported on a local declaration, i.e. redundant initialization - // at declaration, then we also add a new variable declaration statement without initializer for this local. - - using var _1 = PooledDictionary.GetInstance(out var nodeReplacementMap); - using var _2 = PooledHashSet.GetInstance(out var nodesToRemove); - using var _3 = PooledHashSet<(TLocalDeclarationStatementSyntax declarationStatement, SyntaxNode node)>.GetInstance(out var nodesToAdd); - // Indicates if the node's trivia was processed. - using var _4 = PooledHashSet.GetInstance(out var processedNodes); - using var _5 = PooledHashSet.GetInstance(out var candidateDeclarationStatementsForRemoval); - var hasAnyUnusedLocalAssignment = false; - - foreach (var (node, isUnusedLocalAssignment) in GetNodesToFix()) + string? newLocalNameOpt = null; + if (removeAssignments) { - hasAnyUnusedLocalAssignment |= isUnusedLocalAssignment; - - var declaredLocal = semanticModel.GetDeclaredSymbol(node, cancellationToken) as ILocalSymbol; - if (declaredLocal == null && node.Parent is TCatchStatementSyntax) - { - declaredLocal = semanticModel.GetDeclaredSymbol(node.Parent, cancellationToken) as ILocalSymbol; - } - - string? newLocalNameOpt = null; - if (removeAssignments) + // Removable assignment or initialization, such that right hand side has no side effects. + if (declaredLocal != null) { - // Removable assignment or initialization, such that right hand side has no side effects. - if (declaredLocal != null) + // Redundant initialization. + // For example, "int a = 0;" + var variableDeclarator = node.FirstAncestorOrSelf(); + Contract.ThrowIfNull(variableDeclarator); + nodesToRemove.Add(variableDeclarator); + + // Local declaration statement containing the declarator might be a candidate for removal if all its variables get marked for removal. + var candidate = GetCandidateLocalDeclarationForRemoval(variableDeclarator); + if (candidate != null) { - // Redundant initialization. - // For example, "int a = 0;" - var variableDeclarator = node.FirstAncestorOrSelf(); - Contract.ThrowIfNull(variableDeclarator); - nodesToRemove.Add(variableDeclarator); - - // Local declaration statement containing the declarator might be a candidate for removal if all its variables get marked for removal. - var candidate = GetCandidateLocalDeclarationForRemoval(variableDeclarator); - if (candidate != null) - { - candidateDeclarationStatementsForRemoval.Add(candidate); - } - } - else - { - // Redundant assignment or increment/decrement. - if (syntaxFacts.IsOperandOfIncrementOrDecrementExpression(node)) - { - // For example, C# increment operation "a++;" - Contract.ThrowIfFalse(node.GetRequiredParent().Parent is TExpressionStatementSyntax); - nodesToRemove.Add(node.GetRequiredParent().GetRequiredParent()); - } - else - { - Debug.Assert(syntaxFacts.IsLeftSideOfAnyAssignment(node)); - - if (node.Parent is TStatementSyntax) - { - // For example, VB simple assignment statement "a = 0" - nodesToRemove.Add(node.Parent); - } - else if (node.Parent is TExpressionSyntax && node.Parent.Parent is TExpressionStatementSyntax) - { - // For example, C# simple assignment statement "a = 0;" - nodesToRemove.Add(node.Parent.Parent); - } - else - { - // For example, C# nested assignment statement "a = b = 0;", where assignment to 'b' is redundant. - // We replace the node with "a = 0;" - nodeReplacementMap.Add(node.GetRequiredParent(), syntaxFacts.GetRightHandSideOfAssignment(node.GetRequiredParent())); - } - } + candidateDeclarationStatementsForRemoval.Add(candidate); } } else { - // Value initialization/assignment where the right hand side may have side effects, - // and hence needs to be preserved in fixed code. - // For example, "x = MethodCall();" is replaced with "_ = MethodCall();" or "var unused = MethodCall();" - - // Replace the flagged variable's identifier token with new named, based on user's preference. - var newNameToken = preference == UnusedValuePreference.DiscardVariable - ? document.GetRequiredLanguageService().Identifier(AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName) - : nameGenerator.GenerateUniqueNameAtSpanStart(node); - newLocalNameOpt = newNameToken.ValueText; - var newNameNode = TryUpdateNameForFlaggedNode(node, newNameToken); - if (newNameNode == null) + // Redundant assignment or increment/decrement. + if (syntaxFacts.IsOperandOfIncrementOrDecrementExpression(node)) { - continue; - } - - // Is this is compound assignment? - if (syntaxFacts.IsLeftSideOfAnyAssignment(node) && !syntaxFacts.IsLeftSideOfAssignment(node)) - { - // Compound assignment is changed to simple assignment. - // For example, "x += MethodCall();", where assignment to 'x' is redundant - // is replaced with "_ = MethodCall();" or "var unused = MethodCall();" - nodeReplacementMap.Add(node.GetRequiredParent(), GetReplacementNodeForCompoundAssignment(node.GetRequiredParent(), newNameNode, editor, syntaxFacts)); - } - else if (syntaxFacts.IsVarPattern(node)) - { - nodeReplacementMap.Add(node, GetReplacementNodeForVarPattern(node, newNameNode)); + // For example, C# increment operation "a++;" + Contract.ThrowIfFalse(node.GetRequiredParent().Parent is TExpressionStatementSyntax); + nodesToRemove.Add(node.GetRequiredParent().GetRequiredParent()); } else { - var newParentNode = TryUpdateParentOfUpdatedNode(node.GetRequiredParent(), newNameNode, editor, syntaxFacts, semanticModel); - if (newParentNode is not null) + Debug.Assert(syntaxFacts.IsLeftSideOfAnyAssignment(node)); + + if (node.Parent is TStatementSyntax) { - nodeReplacementMap.Add(node.GetRequiredParent(), newParentNode); + // For example, VB simple assignment statement "a = 0" + nodesToRemove.Add(node.Parent); + } + else if (node.Parent is TExpressionSyntax && node.Parent.Parent is TExpressionStatementSyntax) + { + // For example, C# simple assignment statement "a = 0;" + nodesToRemove.Add(node.Parent.Parent); } else { - nodeReplacementMap.Add(node, newNameNode); + // For example, C# nested assignment statement "a = b = 0;", where assignment to 'b' is redundant. + // We replace the node with "a = 0;" + nodeReplacementMap.Add(node.GetRequiredParent(), syntaxFacts.GetRightHandSideOfAssignment(node.GetRequiredParent())); } } } - - if (declaredLocal != null) + } + else + { + // Value initialization/assignment where the right hand side may have side effects, + // and hence needs to be preserved in fixed code. + // For example, "x = MethodCall();" is replaced with "_ = MethodCall();" or "var unused = MethodCall();" + + // Replace the flagged variable's identifier token with new named, based on user's preference. + var newNameToken = preference == UnusedValuePreference.DiscardVariable + ? document.GetRequiredLanguageService().Identifier(AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName) + : nameGenerator.GenerateUniqueNameAtSpanStart(node); + newLocalNameOpt = newNameToken.ValueText; + var newNameNode = TryUpdateNameForFlaggedNode(node, newNameToken); + if (newNameNode == null) { - // We have a dead initialization for a local declaration. - // Introduce a new local declaration statement without an initializer for this local. - var declarationStatement = CreateLocalDeclarationStatement(declaredLocal.Type, declaredLocal.Name); - if (isUnusedLocalAssignment) - { - declarationStatement = declarationStatement.WithAdditionalAnnotations(s_unusedLocalDeclarationAnnotation); - } + continue; + } - nodesToAdd.Add((declarationStatement, node)); + // Is this is compound assignment? + if (syntaxFacts.IsLeftSideOfAnyAssignment(node) && !syntaxFacts.IsLeftSideOfAssignment(node)) + { + // Compound assignment is changed to simple assignment. + // For example, "x += MethodCall();", where assignment to 'x' is redundant + // is replaced with "_ = MethodCall();" or "var unused = MethodCall();" + nodeReplacementMap.Add(node.GetRequiredParent(), GetReplacementNodeForCompoundAssignment(node.GetRequiredParent(), newNameNode, editor, syntaxFacts)); + } + else if (syntaxFacts.IsVarPattern(node)) + { + nodeReplacementMap.Add(node, GetReplacementNodeForVarPattern(node, newNameNode)); } else { - // We have a dead assignment to a local/parameter, which is not at the declaration site. - // Create a new local declaration for the unused local if both following conditions are met: - // 1. User prefers unused local variables for unused value assignment AND - // 2. Assignment value has side effects and hence cannot be removed. - if (preference == UnusedValuePreference.UnusedLocalVariable && !removeAssignments) + var newParentNode = TryUpdateParentOfUpdatedNode(node.GetRequiredParent(), newNameNode, editor, syntaxFacts, semanticModel); + if (newParentNode is not null) { - var type = semanticModel.GetTypeInfo(node, cancellationToken).Type; - Contract.ThrowIfNull(type); - Contract.ThrowIfNull(newLocalNameOpt); - var declarationStatement = CreateLocalDeclarationStatement(type, newLocalNameOpt); - nodesToAdd.Add((declarationStatement, node)); + nodeReplacementMap.Add(node.GetRequiredParent(), newParentNode); + } + else + { + nodeReplacementMap.Add(node, newNameNode); } } } - // Process candidate declaration statements for removal. - foreach (var localDeclarationStatement in candidateDeclarationStatementsForRemoval) + if (declaredLocal != null) { - // If all the variable declarators for the local declaration statement are being removed, - // we can remove the entire local declaration statement. - if (ShouldRemoveStatement(localDeclarationStatement, out var variables)) + // We have a dead initialization for a local declaration. + // Introduce a new local declaration statement without an initializer for this local. + var declarationStatement = CreateLocalDeclarationStatement(declaredLocal.Type, declaredLocal.Name); + if (isUnusedLocalAssignment) { - nodesToRemove.Add(localDeclarationStatement); - nodesToRemove.RemoveRange(variables); + declarationStatement = declarationStatement.WithAdditionalAnnotations(s_unusedLocalDeclarationAnnotation); } - } - foreach (var (declarationStatement, node) in nodesToAdd) - { - InsertLocalDeclarationStatement(declarationStatement, node); + nodesToAdd.Add((declarationStatement, node)); } - - if (hasAnyUnusedLocalAssignment) + else { - // Local declaration statements with no initializer, but non-zero references are candidates for removal - // if the code fix removes all these references. - // We annotate such declaration statements with no initializer and non-zero references here - // and remove them in post process document pass later, if the code fix did remove all these references. - foreach (var localDeclarationStatement in containingMemberDeclaration.DescendantNodes().OfType()) + // We have a dead assignment to a local/parameter, which is not at the declaration site. + // Create a new local declaration for the unused local if both following conditions are met: + // 1. User prefers unused local variables for unused value assignment AND + // 2. Assignment value has side effects and hence cannot be removed. + if (preference == UnusedValuePreference.UnusedLocalVariable && !removeAssignments) { - var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); - if (variables.Count == 1 && - syntaxFacts.GetInitializerOfVariableDeclarator(variables[0]) == null && - !(await IsLocalDeclarationWithNoReferencesAsync(localDeclarationStatement, document, cancellationToken).ConfigureAwait(false))) - { - nodeReplacementMap.Add(localDeclarationStatement, localDeclarationStatement.WithAdditionalAnnotations(s_existingLocalDeclarationWithoutInitializerAnnotation)); - } + var type = semanticModel.GetTypeInfo(node, cancellationToken).Type; + Contract.ThrowIfNull(type); + Contract.ThrowIfNull(newLocalNameOpt); + var declarationStatement = CreateLocalDeclarationStatement(type, newLocalNameOpt); + nodesToAdd.Add((declarationStatement, node)); } } + } - foreach (var node in nodesToRemove) + // Process candidate declaration statements for removal. + foreach (var localDeclarationStatement in candidateDeclarationStatementsForRemoval) + { + // If all the variable declarators for the local declaration statement are being removed, + // we can remove the entire local declaration statement. + if (ShouldRemoveStatement(localDeclarationStatement, out var variables)) { - var removeOptions = SyntaxGenerator.DefaultRemoveOptions; - // If the leading trivia was not added to a new node, process it now. - if (!processedNodes.Contains(node)) - { - // Don't keep trivia if the node is part of a multiple declaration statement. - // e.g. int x = 0, y = 0, z = 0; any white space left behind can cause problems if the declaration gets split apart. - var containingDeclaration = node.GetAncestor(); - if (containingDeclaration != null && candidateDeclarationStatementsForRemoval.Contains(containingDeclaration)) - { - removeOptions = SyntaxRemoveOptions.KeepNoTrivia; - } - else - { - removeOptions |= SyntaxRemoveOptions.KeepLeadingTrivia; - } - } - - editor.RemoveNode(node, removeOptions); + nodesToRemove.Add(localDeclarationStatement); + nodesToRemove.RemoveRange(variables); } + } - foreach (var (node, replacement) in nodeReplacementMap) - editor.ReplaceNode(node, (oldNode, _) => ComputeReplacementNode(node, oldNode, replacement)); - - return; + foreach (var (declarationStatement, node) in nodesToAdd) + { + InsertLocalDeclarationStatement(declarationStatement, node); + } - // Local functions. - IEnumerable<(SyntaxNode node, bool isUnusedLocalAssignment)> GetNodesToFix() + if (hasAnyUnusedLocalAssignment) + { + // Local declaration statements with no initializer, but non-zero references are candidates for removal + // if the code fix removes all these references. + // We annotate such declaration statements with no initializer and non-zero references here + // and remove them in post process document pass later, if the code fix did remove all these references. + foreach (var localDeclarationStatement in containingMemberDeclaration.DescendantNodes().OfType()) { - foreach (var diagnostic in diagnostics) + var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); + if (variables.Count == 1 && + syntaxFacts.GetInitializerOfVariableDeclarator(variables[0]) == null && + !(await IsLocalDeclarationWithNoReferencesAsync(localDeclarationStatement, document, cancellationToken).ConfigureAwait(false))) { - var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); - var isUnusedLocalAssignment = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsUnusedLocalDiagnostic(diagnostic); - yield return (node, isUnusedLocalAssignment); + nodeReplacementMap.Add(localDeclarationStatement, localDeclarationStatement.WithAdditionalAnnotations(s_existingLocalDeclarationWithoutInitializerAnnotation)); } } + } - // Mark generated local declaration statement with: - // 1. "s_newLocalDeclarationAnnotation" for post processing in "MoveNewLocalDeclarationsNearReference" below. - // 2. Simplifier annotation so that 'var'/explicit type is correctly added based on user options. - TLocalDeclarationStatementSyntax CreateLocalDeclarationStatement(ITypeSymbol type, string name) - => (TLocalDeclarationStatementSyntax)editor.Generator.LocalDeclarationStatement(type, name) - .WithLeadingTrivia(syntaxFacts.ElasticCarriageReturnLineFeed) - .WithAdditionalAnnotations(s_newLocalDeclarationStatementAnnotation, Simplifier.Annotation); - - void InsertLocalDeclarationStatement(TLocalDeclarationStatementSyntax declarationStatement, SyntaxNode node) + foreach (var node in nodesToRemove) + { + var removeOptions = SyntaxGenerator.DefaultRemoveOptions; + // If the leading trivia was not added to a new node, process it now. + if (!processedNodes.Contains(node)) { - // Find the correct place to insert the given declaration statement based on the node's ancestors. - var insertionNode = node.FirstAncestorOrSelf( - n => n.Parent is TSwitchCaseBlockSyntax || - blockFacts.IsExecutableBlock(n.Parent) && - n is not TCatchStatementSyntax && - n is not TCatchBlockSyntax); - if (insertionNode is TSwitchCaseLabelOrClauseSyntax) + // Don't keep trivia if the node is part of a multiple declaration statement. + // e.g. int x = 0, y = 0, z = 0; any white space left behind can cause problems if the declaration gets split apart. + var containingDeclaration = node.GetAncestor(); + if (containingDeclaration != null && candidateDeclarationStatementsForRemoval.Contains(containingDeclaration)) { - InsertAtStartOfSwitchCaseBlockForDeclarationInCaseLabelOrClause( - insertionNode.GetAncestor()!, editor, declarationStatement); + removeOptions = SyntaxRemoveOptions.KeepNoTrivia; } - else if (insertionNode is TStatementSyntax) + else { - // If the insertion node is being removed, keep the leading trivia (following any directives) with - // the new declaration. - if (nodesToRemove.Contains(insertionNode) && !processedNodes.Contains(insertionNode)) - { - // Fix 48070 - The Leading Trivia of the insertion node needs to be filtered - // to only include trivia after Directives (if there are any) - var leadingTrivia = insertionNode.GetLeadingTrivia(); - var lastDirective = leadingTrivia.LastOrDefault(t => t.IsDirective); - var lastDirectiveIndex = leadingTrivia.IndexOf(lastDirective); - declarationStatement = declarationStatement.WithLeadingTrivia(leadingTrivia.Skip(lastDirectiveIndex + 1)); - - // Mark the node as processed so that the trivia only gets added once. - processedNodes.Add(insertionNode); - } - - editor.InsertBefore(insertionNode, declarationStatement); + removeOptions |= SyntaxRemoveOptions.KeepLeadingTrivia; } } - bool ShouldRemoveStatement(TLocalDeclarationStatementSyntax localDeclarationStatement, out SeparatedSyntaxList variables) - { - Debug.Assert(removeAssignments); + editor.RemoveNode(node, removeOptions); + } - // We should remove the entire local declaration statement if all its variables are marked for removal. - variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); - foreach (var variable in variables) - { - if (!nodesToRemove.Contains(variable)) - { - return false; - } - } + foreach (var (node, replacement) in nodeReplacementMap) + editor.ReplaceNode(node, (oldNode, _) => ComputeReplacementNode(node, oldNode, replacement)); + + return; - return true; + // Local functions. + IEnumerable<(SyntaxNode node, bool isUnusedLocalAssignment)> GetNodesToFix() + { + foreach (var diagnostic in diagnostics) + { + var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + var isUnusedLocalAssignment = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsUnusedLocalDiagnostic(diagnostic); + yield return (node, isUnusedLocalAssignment); } } - protected abstract TLocalDeclarationStatementSyntax GetCandidateLocalDeclarationForRemoval(TVariableDeclaratorSyntax declarator); + // Mark generated local declaration statement with: + // 1. "s_newLocalDeclarationAnnotation" for post processing in "MoveNewLocalDeclarationsNearReference" below. + // 2. Simplifier annotation so that 'var'/explicit type is correctly added based on user options. + TLocalDeclarationStatementSyntax CreateLocalDeclarationStatement(ITypeSymbol type, string name) + => (TLocalDeclarationStatementSyntax)editor.Generator.LocalDeclarationStatement(type, name) + .WithLeadingTrivia(syntaxFacts.ElasticCarriageReturnLineFeed) + .WithAdditionalAnnotations(s_newLocalDeclarationStatementAnnotation, Simplifier.Annotation); - private async Task PostProcessDocumentAsync( - Document document, - SyntaxFormattingOptions options, - SyntaxNode currentRoot, - string diagnosticId, - UnusedValuePreference preference, - CancellationToken cancellationToken) + void InsertLocalDeclarationStatement(TLocalDeclarationStatementSyntax declarationStatement, SyntaxNode node) { - // If we added discard assignments, replace all discard variable declarations in - // this method with discard assignments, i.e. "var _ = M();" is replaced with "_ = M();" - // This is done to prevent compiler errors where the existing method has a discard - // variable declaration at a line following the one we added a discard assignment in our fix. - if (preference == UnusedValuePreference.DiscardVariable) + // Find the correct place to insert the given declaration statement based on the node's ancestors. + var insertionNode = node.FirstAncestorOrSelf( + n => n.Parent is TSwitchCaseBlockSyntax || + blockFacts.IsExecutableBlock(n.Parent) && + n is not TCatchStatementSyntax && + n is not TCatchBlockSyntax); + if (insertionNode is TSwitchCaseLabelOrClauseSyntax) { - currentRoot = await PostProcessDocumentCoreAsync( - ReplaceDiscardDeclarationsWithAssignmentsAsync, currentRoot, document, options, cancellationToken).ConfigureAwait(false); + InsertAtStartOfSwitchCaseBlockForDeclarationInCaseLabelOrClause( + insertionNode.GetAncestor()!, editor, declarationStatement); } - - // If we added new variable declaration statements, move these as close as possible to their - // first reference site. - if (NeedsToMoveNewLocalDeclarationsNearReference(diagnosticId)) + else if (insertionNode is TStatementSyntax) { - currentRoot = await PostProcessDocumentCoreAsync( - AdjustLocalDeclarationsAsync, currentRoot, document, options, cancellationToken).ConfigureAwait(false); - } + // If the insertion node is being removed, keep the leading trivia (following any directives) with + // the new declaration. + if (nodesToRemove.Contains(insertionNode) && !processedNodes.Contains(insertionNode)) + { + // Fix 48070 - The Leading Trivia of the insertion node needs to be filtered + // to only include trivia after Directives (if there are any) + var leadingTrivia = insertionNode.GetLeadingTrivia(); + var lastDirective = leadingTrivia.LastOrDefault(t => t.IsDirective); + var lastDirectiveIndex = leadingTrivia.IndexOf(lastDirective); + declarationStatement = declarationStatement.WithLeadingTrivia(leadingTrivia.Skip(lastDirectiveIndex + 1)); + + // Mark the node as processed so that the trivia only gets added once. + processedNodes.Add(insertionNode); + } - return currentRoot; + editor.InsertBefore(insertionNode, declarationStatement); + } } - private static async Task PostProcessDocumentCoreAsync( - Func> processMemberDeclarationAsync, - SyntaxNode currentRoot, - Document document, - SyntaxFormattingOptions options, - CancellationToken cancellationToken) + bool ShouldRemoveStatement(TLocalDeclarationStatementSyntax localDeclarationStatement, out SeparatedSyntaxList variables) { - // Process each member declaration which had at least one diagnostic reported in the original tree and hence - // was annotated with "s_memberAnnotation" for post processing. - - var newDocument = document.WithSyntaxRoot(currentRoot); - var newRoot = await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - using var _1 = PooledDictionary.GetInstance(out var memberDeclReplacementsMap); + Debug.Assert(removeAssignments); - foreach (var memberDecl in newRoot.DescendantNodes().Where(n => n.HasAnnotation(s_memberAnnotation))) + // We should remove the entire local declaration statement if all its variables are marked for removal. + variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); + foreach (var variable in variables) { - var newMemberDecl = await processMemberDeclarationAsync(memberDecl, newDocument, options, cancellationToken).ConfigureAwait(false); - memberDeclReplacementsMap.Add(memberDecl, newMemberDecl); + if (!nodesToRemove.Contains(variable)) + { + return false; + } } - return newRoot.ReplaceNodes(memberDeclReplacementsMap.Keys, - computeReplacementNode: (node, _) => memberDeclReplacementsMap[node]); + return true; } + } + + protected abstract TLocalDeclarationStatementSyntax GetCandidateLocalDeclarationForRemoval(TVariableDeclaratorSyntax declarator); - /// - /// Returns an updated with all the - /// local declarations named '_' converted to simple assignments to discard. - /// For example, int _ = Computation(); is converted to - /// _ = Computation();. - /// This is needed to prevent the code fix/FixAll from generating code with - /// multiple local variables named '_', which is a compiler error. - /// - private async Task ReplaceDiscardDeclarationsWithAssignmentsAsync(SyntaxNode memberDeclaration, Document document, SyntaxFormattingOptions options, CancellationToken cancellationToken) + private async Task PostProcessDocumentAsync( + Document document, + SyntaxFormattingOptions options, + SyntaxNode currentRoot, + string diagnosticId, + UnusedValuePreference preference, + CancellationToken cancellationToken) + { + // If we added discard assignments, replace all discard variable declarations in + // this method with discard assignments, i.e. "var _ = M();" is replaced with "_ = M();" + // This is done to prevent compiler errors where the existing method has a discard + // variable declaration at a line following the one we added a discard assignment in our fix. + if (preference == UnusedValuePreference.DiscardVariable) + { + currentRoot = await PostProcessDocumentCoreAsync( + ReplaceDiscardDeclarationsWithAssignmentsAsync, currentRoot, document, options, cancellationToken).ConfigureAwait(false); + } + + // If we added new variable declaration statements, move these as close as possible to their + // first reference site. + if (NeedsToMoveNewLocalDeclarationsNearReference(diagnosticId)) { - var service = document.GetLanguageService(); - if (service == null) - return memberDeclaration; + currentRoot = await PostProcessDocumentCoreAsync( + AdjustLocalDeclarationsAsync, currentRoot, document, options, cancellationToken).ConfigureAwait(false); + } + + return currentRoot; + } + + private static async Task PostProcessDocumentCoreAsync( + Func> processMemberDeclarationAsync, + SyntaxNode currentRoot, + Document document, + SyntaxFormattingOptions options, + CancellationToken cancellationToken) + { + // Process each member declaration which had at least one diagnostic reported in the original tree and hence + // was annotated with "s_memberAnnotation" for post processing. + + var newDocument = document.WithSyntaxRoot(currentRoot); + var newRoot = await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + using var _1 = PooledDictionary.GetInstance(out var memberDeclReplacementsMap); - return await service.ReplaceAsync(document, memberDeclaration, cancellationToken).ConfigureAwait(false); + foreach (var memberDecl in newRoot.DescendantNodes().Where(n => n.HasAnnotation(s_memberAnnotation))) + { + var newMemberDecl = await processMemberDeclarationAsync(memberDecl, newDocument, options, cancellationToken).ConfigureAwait(false); + memberDeclReplacementsMap.Add(memberDecl, newMemberDecl); } - /// - /// Returns an updated with all the new - /// local declaration statements annotated with - /// moved closer to first reference and all the existing - /// local declaration statements annotated with - /// whose declared local is no longer used removed. - /// - private async Task AdjustLocalDeclarationsAsync( - SyntaxNode memberDeclaration, - Document document, - SyntaxFormattingOptions options, - CancellationToken cancellationToken) + return newRoot.ReplaceNodes(memberDeclReplacementsMap.Keys, + computeReplacementNode: (node, _) => memberDeclReplacementsMap[node]); + } + + /// + /// Returns an updated with all the + /// local declarations named '_' converted to simple assignments to discard. + /// For example, int _ = Computation(); is converted to + /// _ = Computation();. + /// This is needed to prevent the code fix/FixAll from generating code with + /// multiple local variables named '_', which is a compiler error. + /// + private async Task ReplaceDiscardDeclarationsWithAssignmentsAsync(SyntaxNode memberDeclaration, Document document, SyntaxFormattingOptions options, CancellationToken cancellationToken) + { + var service = document.GetLanguageService(); + if (service == null) + return memberDeclaration; + + return await service.ReplaceAsync(document, memberDeclaration, cancellationToken).ConfigureAwait(false); + } + + /// + /// Returns an updated with all the new + /// local declaration statements annotated with + /// moved closer to first reference and all the existing + /// local declaration statements annotated with + /// whose declared local is no longer used removed. + /// + private async Task AdjustLocalDeclarationsAsync( + SyntaxNode memberDeclaration, + Document document, + SyntaxFormattingOptions options, + CancellationToken cancellationToken) + { + var moveDeclarationService = document.GetRequiredLanguageService(); + var syntaxFacts = document.GetRequiredLanguageService(); + var originalDocument = document; + var originalDeclStatementsToMoveOrRemove = + memberDeclaration.DescendantNodes() + .Where(n => n.HasAnnotation(s_newLocalDeclarationStatementAnnotation) || + n.HasAnnotation(s_existingLocalDeclarationWithoutInitializerAnnotation)) + .ToImmutableArray(); + if (originalDeclStatementsToMoveOrRemove.IsEmpty) { - var moveDeclarationService = document.GetRequiredLanguageService(); - var syntaxFacts = document.GetRequiredLanguageService(); - var originalDocument = document; - var originalDeclStatementsToMoveOrRemove = - memberDeclaration.DescendantNodes() - .Where(n => n.HasAnnotation(s_newLocalDeclarationStatementAnnotation) || - n.HasAnnotation(s_existingLocalDeclarationWithoutInitializerAnnotation)) - .ToImmutableArray(); - if (originalDeclStatementsToMoveOrRemove.IsEmpty) - { - return memberDeclaration; - } + return memberDeclaration; + } - // Moving declarations closer to a reference can lead to conflicting edits. - // So, we track all the declaration statements to be moved upfront, and update - // the root, document, editor and memberDeclaration for every edit. - // Finally, we apply replace the memberDeclaration in the originalEditor as a single edit. - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var rootWithTrackedNodes = root.TrackNodes(originalDeclStatementsToMoveOrRemove); + // Moving declarations closer to a reference can lead to conflicting edits. + // So, we track all the declaration statements to be moved upfront, and update + // the root, document, editor and memberDeclaration for every edit. + // Finally, we apply replace the memberDeclaration in the originalEditor as a single edit. + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var rootWithTrackedNodes = root.TrackNodes(originalDeclStatementsToMoveOrRemove); - // Run formatter prior to invoking IMoveDeclarationNearReferenceService. + // Run formatter prior to invoking IMoveDeclarationNearReferenceService. #if CODE_STYLE - var provider = GetSyntaxFormatting(); - rootWithTrackedNodes = FormatterHelper.Format(rootWithTrackedNodes, originalDeclStatementsToMoveOrRemove.Select(s => s.Span), provider, options, rules: null, cancellationToken); + var provider = GetSyntaxFormatting(); + rootWithTrackedNodes = FormatterHelper.Format(rootWithTrackedNodes, originalDeclStatementsToMoveOrRemove.Select(s => s.Span), provider, options, rules: null, cancellationToken); #else - var provider = document.Project.Solution.Services; - rootWithTrackedNodes = Formatter.Format(rootWithTrackedNodes, originalDeclStatementsToMoveOrRemove.Select(s => s.Span), provider, options, rules: null, cancellationToken); + var provider = document.Project.Solution.Services; + rootWithTrackedNodes = Formatter.Format(rootWithTrackedNodes, originalDeclStatementsToMoveOrRemove.Select(s => s.Span), provider, options, rules: null, cancellationToken); #endif - document = document.WithSyntaxRoot(rootWithTrackedNodes); - await OnDocumentUpdatedAsync().ConfigureAwait(false); + document = document.WithSyntaxRoot(rootWithTrackedNodes); + await OnDocumentUpdatedAsync().ConfigureAwait(false); - foreach (TLocalDeclarationStatementSyntax originalDeclStatement in originalDeclStatementsToMoveOrRemove) - { - // Get the current declaration statement. - var declStatement = memberDeclaration.GetCurrentNode(originalDeclStatement); - Contract.ThrowIfNull(declStatement); - - // Check if the variable declaration is unused after all the fixes, and hence can be removed. - if (await TryRemoveUnusedLocalAsync(declStatement, originalDeclStatement).ConfigureAwait(false)) - { - await OnDocumentUpdatedAsync().ConfigureAwait(false); - } - else if (declStatement.HasAnnotation(s_newLocalDeclarationStatementAnnotation)) - { - // Otherwise, move the declaration closer to the first reference if possible. - if (await moveDeclarationService.CanMoveDeclarationNearReferenceAsync(document, declStatement, cancellationToken).ConfigureAwait(false)) - { - document = await moveDeclarationService.MoveDeclarationNearReferenceAsync(document, declStatement, cancellationToken).ConfigureAwait(false); - await OnDocumentUpdatedAsync().ConfigureAwait(false); - } - } - } - - return memberDeclaration; + foreach (TLocalDeclarationStatementSyntax originalDeclStatement in originalDeclStatementsToMoveOrRemove) + { + // Get the current declaration statement. + var declStatement = memberDeclaration.GetCurrentNode(originalDeclStatement); + Contract.ThrowIfNull(declStatement); - // Local functions. - async Task OnDocumentUpdatedAsync() + // Check if the variable declaration is unused after all the fixes, and hence can be removed. + if (await TryRemoveUnusedLocalAsync(declStatement, originalDeclStatement).ConfigureAwait(false)) { - root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - memberDeclaration = syntaxFacts.GetContainingMemberDeclaration(root, memberDeclaration.SpanStart) ?? root; + await OnDocumentUpdatedAsync().ConfigureAwait(false); } - - async Task TryRemoveUnusedLocalAsync(TLocalDeclarationStatementSyntax newDecl, TLocalDeclarationStatementSyntax originalDecl) + else if (declStatement.HasAnnotation(s_newLocalDeclarationStatementAnnotation)) { - // If we introduced this new local declaration statement while computing the code fix, but all it's - // existing references were removed as part of FixAll, then we can remove the unnecessary local - // declaration statement. Additionally, if this is an existing local declaration without an initializer, - // such that the local has no references anymore, we can remove it. - - if (newDecl.HasAnnotation(s_unusedLocalDeclarationAnnotation) || - newDecl.HasAnnotation(s_existingLocalDeclarationWithoutInitializerAnnotation)) + // Otherwise, move the declaration closer to the first reference if possible. + if (await moveDeclarationService.CanMoveDeclarationNearReferenceAsync(document, declStatement, cancellationToken).ConfigureAwait(false)) { - // Check if we have no references to local in fixed code. - if (await IsLocalDeclarationWithNoReferencesAsync(newDecl, document, cancellationToken).ConfigureAwait(false)) - { - var rootWithRemovedDeclaration = root.RemoveNode(newDecl, SyntaxGenerator.DefaultRemoveOptions | SyntaxRemoveOptions.KeepLeadingTrivia); - Contract.ThrowIfNull(rootWithRemovedDeclaration); - document = document.WithSyntaxRoot(rootWithRemovedDeclaration); - return true; - } + document = await moveDeclarationService.MoveDeclarationNearReferenceAsync(document, declStatement, cancellationToken).ConfigureAwait(false); + await OnDocumentUpdatedAsync().ConfigureAwait(false); } - - return false; } } - private static async Task IsLocalDeclarationWithNoReferencesAsync( - TLocalDeclarationStatementSyntax declStatement, - Document document, - CancellationToken cancellationToken) + return memberDeclaration; + + // Local functions. + async Task OnDocumentUpdatedAsync() { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var localDeclarationOperation = (IVariableDeclarationGroupOperation)semanticModel.GetRequiredOperation(declStatement, cancellationToken); - var local = localDeclarationOperation.GetDeclaredVariables().Single(); - - // Check if the declared variable has no references in fixed code. - var referencedSymbols = await SymbolFinder.FindReferencesAsync(local, document.Project.Solution, cancellationToken).ConfigureAwait(false); - return referencedSymbols.Count() == 1 && - referencedSymbols.Single().Locations.IsEmpty(); + root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + memberDeclaration = syntaxFacts.GetContainingMemberDeclaration(root, memberDeclaration.SpanStart) ?? root; } - protected sealed class UniqueVariableNameGenerator( - SyntaxNode memberDeclaration, - SemanticModel semanticModel, - ISemanticFactsService semanticFacts, - CancellationToken cancellationToken) : IDisposable + async Task TryRemoveUnusedLocalAsync(TLocalDeclarationStatementSyntax newDecl, TLocalDeclarationStatementSyntax originalDecl) { - private readonly SyntaxNode _memberDeclaration = memberDeclaration; - private readonly SemanticModel _semanticModel = semanticModel; - private readonly ISemanticFactsService _semanticFacts = semanticFacts; - private readonly CancellationToken _cancellationToken = cancellationToken; - private readonly PooledHashSet _usedNames = PooledHashSet.GetInstance(); + // If we introduced this new local declaration statement while computing the code fix, but all it's + // existing references were removed as part of FixAll, then we can remove the unnecessary local + // declaration statement. Additionally, if this is an existing local declaration without an initializer, + // such that the local has no references anymore, we can remove it. - public SyntaxToken GenerateUniqueNameAtSpanStart(SyntaxNode node) + if (newDecl.HasAnnotation(s_unusedLocalDeclarationAnnotation) || + newDecl.HasAnnotation(s_existingLocalDeclarationWithoutInitializerAnnotation)) { - var nameToken = _semanticFacts.GenerateUniqueName(_semanticModel, node, _memberDeclaration, "unused", _usedNames, _cancellationToken); - _usedNames.Add(nameToken.ValueText); - return nameToken; + // Check if we have no references to local in fixed code. + if (await IsLocalDeclarationWithNoReferencesAsync(newDecl, document, cancellationToken).ConfigureAwait(false)) + { + var rootWithRemovedDeclaration = root.RemoveNode(newDecl, SyntaxGenerator.DefaultRemoveOptions | SyntaxRemoveOptions.KeepLeadingTrivia); + Contract.ThrowIfNull(rootWithRemovedDeclaration); + document = document.WithSyntaxRoot(rootWithRemovedDeclaration); + return true; + } } - public void Dispose() => _usedNames.Free(); + return false; } } + + private static async Task IsLocalDeclarationWithNoReferencesAsync( + TLocalDeclarationStatementSyntax declStatement, + Document document, + CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var localDeclarationOperation = (IVariableDeclarationGroupOperation)semanticModel.GetRequiredOperation(declStatement, cancellationToken); + var local = localDeclarationOperation.GetDeclaredVariables().Single(); + + // Check if the declared variable has no references in fixed code. + var referencedSymbols = await SymbolFinder.FindReferencesAsync(local, document.Project.Solution, cancellationToken).ConfigureAwait(false); + return referencedSymbols.Count() == 1 && + referencedSymbols.Single().Locations.IsEmpty(); + } + + protected sealed class UniqueVariableNameGenerator( + SyntaxNode memberDeclaration, + SemanticModel semanticModel, + ISemanticFactsService semanticFacts, + CancellationToken cancellationToken) : IDisposable + { + private readonly SyntaxNode _memberDeclaration = memberDeclaration; + private readonly SemanticModel _semanticModel = semanticModel; + private readonly ISemanticFactsService _semanticFacts = semanticFacts; + private readonly CancellationToken _cancellationToken = cancellationToken; + private readonly PooledHashSet _usedNames = PooledHashSet.GetInstance(); + + public SyntaxToken GenerateUniqueNameAtSpanStart(SyntaxNode node) + { + var nameToken = _semanticFacts.GenerateUniqueName(_semanticModel, node, _memberDeclaration, "unused", _usedNames, _cancellationToken); + _usedNames.Add(nameToken.ValueText); + return nameToken; + } + + public void Dispose() => _usedNames.Free(); + } } diff --git a/src/Analyzers/Core/CodeFixes/SimplifyInterpolation/AbstractSimplifyInterpolationCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/SimplifyInterpolation/AbstractSimplifyInterpolationCodeFixProvider.cs index adce47c7cc1d9..9543b0e29eea6 100644 --- a/src/Analyzers/Core/CodeFixes/SimplifyInterpolation/AbstractSimplifyInterpolationCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/SimplifyInterpolation/AbstractSimplifyInterpolationCodeFixProvider.cs @@ -16,93 +16,92 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.SimplifyInterpolation +namespace Microsoft.CodeAnalysis.SimplifyInterpolation; + +internal abstract class AbstractSimplifyInterpolationCodeFixProvider< + TInterpolationSyntax, + TExpressionSyntax, + TInterpolationAlignmentClause, + TInterpolationFormatClause, + TInterpolatedStringExpressionSyntax> : SyntaxEditorBasedCodeFixProvider + where TInterpolationSyntax : SyntaxNode + where TExpressionSyntax : SyntaxNode + where TInterpolationAlignmentClause : SyntaxNode + where TInterpolationFormatClause : SyntaxNode + where TInterpolatedStringExpressionSyntax : TExpressionSyntax { - internal abstract class AbstractSimplifyInterpolationCodeFixProvider< - TInterpolationSyntax, - TExpressionSyntax, - TInterpolationAlignmentClause, - TInterpolationFormatClause, - TInterpolatedStringExpressionSyntax> : SyntaxEditorBasedCodeFixProvider - where TInterpolationSyntax : SyntaxNode - where TExpressionSyntax : SyntaxNode - where TInterpolationAlignmentClause : SyntaxNode - where TInterpolationFormatClause : SyntaxNode - where TInterpolatedStringExpressionSyntax : TExpressionSyntax - { - public override ImmutableArray FixableDiagnosticIds { get; } = - [IDEDiagnosticIds.SimplifyInterpolationId]; + public override ImmutableArray FixableDiagnosticIds { get; } = + [IDEDiagnosticIds.SimplifyInterpolationId]; - protected abstract AbstractSimplifyInterpolationHelpers GetHelpers(); + protected abstract AbstractSimplifyInterpolationHelpers GetHelpers(); - protected abstract TInterpolationSyntax WithExpression(TInterpolationSyntax interpolation, TExpressionSyntax expression); - protected abstract TInterpolationSyntax WithAlignmentClause(TInterpolationSyntax interpolation, TInterpolationAlignmentClause alignmentClause); - protected abstract TInterpolationSyntax WithFormatClause(TInterpolationSyntax interpolation, TInterpolationFormatClause? formatClause); - protected abstract string Escape(TInterpolatedStringExpressionSyntax interpolatedString, string formatString); + protected abstract TInterpolationSyntax WithExpression(TInterpolationSyntax interpolation, TExpressionSyntax expression); + protected abstract TInterpolationSyntax WithAlignmentClause(TInterpolationSyntax interpolation, TInterpolationAlignmentClause alignmentClause); + protected abstract TInterpolationSyntax WithFormatClause(TInterpolationSyntax interpolation, TInterpolationFormatClause? formatClause); + protected abstract string Escape(TInterpolatedStringExpressionSyntax interpolatedString, string formatString); - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, AnalyzersResources.Simplify_interpolation, nameof(AnalyzersResources.Simplify_interpolation)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, AnalyzersResources.Simplify_interpolation, nameof(AnalyzersResources.Simplify_interpolation)); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var generator = editor.Generator; - var generatorInternal = document.GetRequiredLanguageService(); - var helpers = GetHelpers(); + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var generator = editor.Generator; + var generatorInternal = document.GetRequiredLanguageService(); + var helpers = GetHelpers(); - foreach (var diagnostic in diagnostics) + foreach (var diagnostic in diagnostics) + { + var loc = diagnostic.AdditionalLocations[0]; + var interpolation = semanticModel.GetOperation(loc.FindNode(getInnermostNodeForTie: true, cancellationToken), cancellationToken) as IInterpolationOperation; + if (interpolation?.Syntax is TInterpolationSyntax interpolationSyntax && + interpolationSyntax.Parent is TInterpolatedStringExpressionSyntax interpolatedString) { - var loc = diagnostic.AdditionalLocations[0]; - var interpolation = semanticModel.GetOperation(loc.FindNode(getInnermostNodeForTie: true, cancellationToken), cancellationToken) as IInterpolationOperation; - if (interpolation?.Syntax is TInterpolationSyntax interpolationSyntax && - interpolationSyntax.Parent is TInterpolatedStringExpressionSyntax interpolatedString) - { - helpers.UnwrapInterpolation( - document.GetRequiredLanguageService(), - document.GetRequiredLanguageService(), - interpolation, out var unwrapped, out var alignment, out var negate, out var formatString, out _); - - if (unwrapped == null) - continue; + helpers.UnwrapInterpolation( + document.GetRequiredLanguageService(), + document.GetRequiredLanguageService(), + interpolation, out var unwrapped, out var alignment, out var negate, out var formatString, out _); - if (alignment != null && negate) - { - alignment = (TExpressionSyntax)generator.NegateExpression(alignment); - } + if (unwrapped == null) + continue; - editor.ReplaceNode( - interpolationSyntax, - Update(generatorInternal, interpolatedString, interpolationSyntax, unwrapped, alignment, formatString)); + if (alignment != null && negate) + { + alignment = (TExpressionSyntax)generator.NegateExpression(alignment); } + + editor.ReplaceNode( + interpolationSyntax, + Update(generatorInternal, interpolatedString, interpolationSyntax, unwrapped, alignment, formatString)); } } + } - private TInterpolationSyntax Update( - SyntaxGeneratorInternal generator, TInterpolatedStringExpressionSyntax interpolatedString, - TInterpolationSyntax interpolation, TExpressionSyntax unwrapped, - TExpressionSyntax? alignment, string? formatString) + private TInterpolationSyntax Update( + SyntaxGeneratorInternal generator, TInterpolatedStringExpressionSyntax interpolatedString, + TInterpolationSyntax interpolation, TExpressionSyntax unwrapped, + TExpressionSyntax? alignment, string? formatString) + { + var result = WithExpression(interpolation, unwrapped); + if (alignment != null) { - var result = WithExpression(interpolation, unwrapped); - if (alignment != null) - { - result = WithAlignmentClause( - result, - (TInterpolationAlignmentClause)generator.InterpolationAlignmentClause(alignment)); - } - - if (!string.IsNullOrEmpty(formatString)) - { - result = WithFormatClause( - result, - (TInterpolationFormatClause?)generator.InterpolationFormatClause(Escape(interpolatedString, formatString!))); - } + result = WithAlignmentClause( + result, + (TInterpolationAlignmentClause)generator.InterpolationAlignmentClause(alignment)); + } - return result; + if (!string.IsNullOrEmpty(formatString)) + { + result = WithFormatClause( + result, + (TInterpolationFormatClause?)generator.InterpolationFormatClause(Escape(interpolatedString, formatString!))); } + + return result; } } diff --git a/src/Analyzers/Core/CodeFixes/SimplifyLinqExpression/AbstractSimplifyLinqExpressionCodeFixProvider`3.cs b/src/Analyzers/Core/CodeFixes/SimplifyLinqExpression/AbstractSimplifyLinqExpressionCodeFixProvider`3.cs index ac30f7818ebb5..cbe3bc882fa57 100644 --- a/src/Analyzers/Core/CodeFixes/SimplifyLinqExpression/AbstractSimplifyLinqExpressionCodeFixProvider`3.cs +++ b/src/Analyzers/Core/CodeFixes/SimplifyLinqExpression/AbstractSimplifyLinqExpressionCodeFixProvider`3.cs @@ -13,59 +13,58 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.SimplifyLinqExpression +namespace Microsoft.CodeAnalysis.SimplifyLinqExpression; + +internal abstract class AbstractSimplifyLinqExpressionCodeFixProvider : SyntaxEditorBasedCodeFixProvider + where TExpressionSyntax : SyntaxNode + where TInvocationExpressionSyntax : TExpressionSyntax + where TSimpleNameSyntax : TExpressionSyntax { - internal abstract class AbstractSimplifyLinqExpressionCodeFixProvider : SyntaxEditorBasedCodeFixProvider - where TExpressionSyntax : SyntaxNode - where TInvocationExpressionSyntax : TExpressionSyntax - where TSimpleNameSyntax : TExpressionSyntax - { - protected abstract ISyntaxFacts SyntaxFacts { get; } + protected abstract ISyntaxFacts SyntaxFacts { get; } - public sealed override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.SimplifyLinqExpressionDiagnosticId]; + public sealed override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.SimplifyLinqExpressionDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, AnalyzersResources.Simplify_LINQ_expression, nameof(AnalyzersResources.Simplify_LINQ_expression)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, AnalyzersResources.Simplify_LINQ_expression, nameof(AnalyzersResources.Simplify_LINQ_expression)); + return Task.CompletedTask; + } - protected override Task FixAllAsync(Document document, - ImmutableArray diagnostics, - SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override Task FixAllAsync(Document document, + ImmutableArray diagnostics, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var root = editor.OriginalRoot; + var expressionsToReWrite = diagnostics.Select(d => GetInvocation(root, d)).OrderByDescending(i => i.SpanStart); + foreach (var original in expressionsToReWrite) { - var root = editor.OriginalRoot; - var expressionsToReWrite = diagnostics.Select(d => GetInvocation(root, d)).OrderByDescending(i => i.SpanStart); - foreach (var original in expressionsToReWrite) + editor.ReplaceNode(original, (current, generator) => { - editor.ReplaceNode(original, (current, generator) => - { - var invocation = (TInvocationExpressionSyntax)current; - var (expression, name, arguments) = FindNodes(invocation); - return generator.InvocationExpression( - generator.MemberAccessExpression(expression, name), - arguments); - }); - } + var invocation = (TInvocationExpressionSyntax)current; + var (expression, name, arguments) = FindNodes(invocation); + return generator.InvocationExpression( + generator.MemberAccessExpression(expression, name), + arguments); + }); + } - return Task.CompletedTask; + return Task.CompletedTask; - static TInvocationExpressionSyntax GetInvocation(SyntaxNode root, Diagnostic diagnostic) - { - return (TInvocationExpressionSyntax)root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); - } + static TInvocationExpressionSyntax GetInvocation(SyntaxNode root, Diagnostic diagnostic) + { + return (TInvocationExpressionSyntax)root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + } - (TExpressionSyntax Expression, TSimpleNameSyntax Name, SeparatedSyntaxList Arguments) FindNodes(TInvocationExpressionSyntax current) - { - var memberAccess = SyntaxFacts.GetExpressionOfInvocationExpression(current); - var name = (TSimpleNameSyntax)SyntaxFacts.GetNameOfMemberAccessExpression(memberAccess); - var whereExpression = (TInvocationExpressionSyntax)SyntaxFacts.GetExpressionOfMemberAccessExpression(memberAccess)!; - var arguments = SyntaxFacts.GetArgumentsOfInvocationExpression(whereExpression); - var expression = (TExpressionSyntax)SyntaxFacts.GetExpressionOfMemberAccessExpression(SyntaxFacts.GetExpressionOfInvocationExpression(whereExpression))!; - return (expression, name, arguments); - } + (TExpressionSyntax Expression, TSimpleNameSyntax Name, SeparatedSyntaxList Arguments) FindNodes(TInvocationExpressionSyntax current) + { + var memberAccess = SyntaxFacts.GetExpressionOfInvocationExpression(current); + var name = (TSimpleNameSyntax)SyntaxFacts.GetNameOfMemberAccessExpression(memberAccess); + var whereExpression = (TInvocationExpressionSyntax)SyntaxFacts.GetExpressionOfMemberAccessExpression(memberAccess)!; + var arguments = SyntaxFacts.GetArgumentsOfInvocationExpression(whereExpression); + var expression = (TExpressionSyntax)SyntaxFacts.GetExpressionOfMemberAccessExpression(SyntaxFacts.GetExpressionOfInvocationExpression(whereExpression))!; + return (expression, name, arguments); } } } diff --git a/src/Analyzers/Core/CodeFixes/UnsealClass/AbstractUnsealClassCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UnsealClass/AbstractUnsealClassCodeFixProvider.cs index beea3f3371253..c9d06daf456f2 100644 --- a/src/Analyzers/Core/CodeFixes/UnsealClass/AbstractUnsealClassCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UnsealClass/AbstractUnsealClassCodeFixProvider.cs @@ -14,75 +14,74 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.UnsealClass +namespace Microsoft.CodeAnalysis.UnsealClass; + +internal abstract class AbstractUnsealClassCodeFixProvider : CodeFixProvider { - internal abstract class AbstractUnsealClassCodeFixProvider : CodeFixProvider - { - protected abstract string TitleFormat { get; } + protected abstract string TitleFormat { get; } - public override FixAllProvider? GetFixAllProvider() - { - // This code fix addresses a very specific compiler error. It's unlikely there will be more than 1 of them at a time. - return null; - } + public override FixAllProvider? GetFixAllProvider() + { + // This code fix addresses a very specific compiler error. It's unlikely there will be more than 1 of them at a time. + return null; + } - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var cancellationToken = context.CancellationToken; + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var cancellationToken = context.CancellationToken; - var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var node = syntaxRoot.FindNode(context.Span, getInnermostNodeForTie: true); + var node = syntaxRoot.FindNode(context.Span, getInnermostNodeForTie: true); - if (semanticModel.GetSymbolInfo(node, cancellationToken).Symbol is INamedTypeSymbol type && - type.TypeKind == TypeKind.Class && type.IsSealed && !type.IsStatic) + if (semanticModel.GetSymbolInfo(node, cancellationToken).Symbol is INamedTypeSymbol type && + type.TypeKind == TypeKind.Class && type.IsSealed && !type.IsStatic) + { + var definition = await SymbolFinder.FindSourceDefinitionAsync( + type, document.Project.Solution, cancellationToken).ConfigureAwait(false); + if (definition is not null && definition.DeclaringSyntaxReferences.Length > 0) { - var definition = await SymbolFinder.FindSourceDefinitionAsync( - type, document.Project.Solution, cancellationToken).ConfigureAwait(false); - if (definition is not null && definition.DeclaringSyntaxReferences.Length > 0) - { - var title = string.Format(TitleFormat, type.Name); - context.RegisterCodeFix( - CodeAction.Create( - title, - c => UnsealDeclarationsAsync(document.Project.Solution, definition.DeclaringSyntaxReferences, c), - title), - context.Diagnostics); - } + var title = string.Format(TitleFormat, type.Name); + context.RegisterCodeFix( + CodeAction.Create( + title, + c => UnsealDeclarationsAsync(document.Project.Solution, definition.DeclaringSyntaxReferences, c), + title), + context.Diagnostics); } } + } - private static async Task UnsealDeclarationsAsync( - Solution solution, ImmutableArray declarationReferences, CancellationToken cancellationToken) + private static async Task UnsealDeclarationsAsync( + Solution solution, ImmutableArray declarationReferences, CancellationToken cancellationToken) + { + foreach (var (documentId, syntaxReferences) in + declarationReferences.GroupBy(reference => solution.GetDocumentId(reference.SyntaxTree)!)) { - foreach (var (documentId, syntaxReferences) in - declarationReferences.GroupBy(reference => solution.GetDocumentId(reference.SyntaxTree)!)) - { - var document = solution.GetRequiredDocument(documentId); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var document = solution.GetRequiredDocument(documentId); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var editor = new SyntaxEditor(root, document.Project.Solution.Services); - var generator = editor.Generator; + var editor = new SyntaxEditor(root, document.Project.Solution.Services); + var generator = editor.Generator; - foreach (var syntaxReference in syntaxReferences) - { - var declaration = await syntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + foreach (var syntaxReference in syntaxReferences) + { + var declaration = await syntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); - var modifiers = generator.GetModifiers(declaration); - if (modifiers.IsSealed) - { - var newDeclaration = generator.WithModifiers(declaration, modifiers.WithIsSealed(false)); + var modifiers = generator.GetModifiers(declaration); + if (modifiers.IsSealed) + { + var newDeclaration = generator.WithModifiers(declaration, modifiers.WithIsSealed(false)); - editor.ReplaceNode(declaration, newDeclaration); - } + editor.ReplaceNode(declaration, newDeclaration); } - - solution = solution.WithDocumentSyntaxRoot(documentId, editor.GetChangedRoot()); } - return solution; + solution = solution.WithDocumentSyntaxRoot(documentId, editor.GetChangedRoot()); } + + return solution; } } diff --git a/src/Analyzers/Core/CodeFixes/UpgradeProject/AbstractUpgradeProjectCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UpgradeProject/AbstractUpgradeProjectCodeFixProvider.cs index 587257363a89f..b70f1d64ba778 100644 --- a/src/Analyzers/Core/CodeFixes/UpgradeProject/AbstractUpgradeProjectCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UpgradeProject/AbstractUpgradeProjectCodeFixProvider.cs @@ -14,127 +14,126 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.CodeActions.CodeAction; -namespace Microsoft.CodeAnalysis.UpgradeProject -{ - internal abstract partial class AbstractUpgradeProjectCodeFixProvider : CodeFixProvider - { - public abstract string SuggestedVersion(ImmutableArray diagnostics); - public abstract Solution UpgradeProject(Project project, string version); - public abstract bool IsUpgrade(Project project, string newVersion); - public abstract string UpgradeThisProjectResource { get; } - public abstract string UpgradeAllProjectsResource { get; } +namespace Microsoft.CodeAnalysis.UpgradeProject; - public override FixAllProvider? GetFixAllProvider() - { - // This code fix uses a dedicated action for fixing all instances in a solution - return null; - } +internal abstract partial class AbstractUpgradeProjectCodeFixProvider : CodeFixProvider +{ + public abstract string SuggestedVersion(ImmutableArray diagnostics); + public abstract Solution UpgradeProject(Project project, string version); + public abstract bool IsUpgrade(Project project, string newVersion); + public abstract string UpgradeThisProjectResource { get; } + public abstract string UpgradeAllProjectsResource { get; } - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var diagnostics = context.Diagnostics; + public override FixAllProvider? GetFixAllProvider() + { + // This code fix uses a dedicated action for fixing all instances in a solution + return null; + } - context.RegisterFixes(GetUpgradeProjectCodeActions(context), diagnostics); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostics = context.Diagnostics; - protected ImmutableArray GetUpgradeProjectCodeActions(CodeFixContext context) - { - var project = context.Document.Project; - var solution = project.Solution; - var newVersion = SuggestedVersion(context.Diagnostics); + context.RegisterFixes(GetUpgradeProjectCodeActions(context), diagnostics); + return Task.CompletedTask; + } - var result = new List(); - var language = project.Language; + protected ImmutableArray GetUpgradeProjectCodeActions(CodeFixContext context) + { + var project = context.Document.Project; + var solution = project.Solution; + var newVersion = SuggestedVersion(context.Diagnostics); - var upgradeableProjects = solution.Projects.Where(p => CanUpgrade(p, language, newVersion)).AsImmutable(); + var result = new List(); + var language = project.Language; - if (upgradeableProjects.Length == 0) - { - return []; - } + var upgradeableProjects = solution.Projects.Where(p => CanUpgrade(p, language, newVersion)).AsImmutable(); - var fixOneProjectTitle = string.Format(UpgradeThisProjectResource, newVersion); - var fixOneProject = ProjectOptionsChangeAction.Create(fixOneProjectTitle, - _ => Task.FromResult(UpgradeProject(project, newVersion))); + if (upgradeableProjects.Length == 0) + { + return []; + } - result.Add(fixOneProject); + var fixOneProjectTitle = string.Format(UpgradeThisProjectResource, newVersion); + var fixOneProject = ProjectOptionsChangeAction.Create(fixOneProjectTitle, + _ => Task.FromResult(UpgradeProject(project, newVersion))); - if (upgradeableProjects.Length > 1) - { - var fixAllProjectsTitle = string.Format(UpgradeAllProjectsResource, newVersion); + result.Add(fixOneProject); - var fixAllProjects = ProjectOptionsChangeAction.Create(fixAllProjectsTitle, - ct => Task.FromResult(UpgradeAllProjects(solution, language, newVersion, ct))); + if (upgradeableProjects.Length > 1) + { + var fixAllProjectsTitle = string.Format(UpgradeAllProjectsResource, newVersion); - result.Add(fixAllProjects); - } + var fixAllProjects = ProjectOptionsChangeAction.Create(fixAllProjectsTitle, + ct => Task.FromResult(UpgradeAllProjects(solution, language, newVersion, ct))); - return result.AsImmutable(); + result.Add(fixAllProjects); } - public Solution UpgradeAllProjects(Solution solution, string language, string version, CancellationToken cancellationToken) + return result.AsImmutable(); + } + + public Solution UpgradeAllProjects(Solution solution, string language, string version, CancellationToken cancellationToken) + { + var currentSolution = solution; + foreach (var projectId in solution.Projects.Select(p => p.Id)) { - var currentSolution = solution; - foreach (var projectId in solution.Projects.Select(p => p.Id)) - { - cancellationToken.ThrowIfCancellationRequested(); - var currentProject = currentSolution.GetRequiredProject(projectId); + cancellationToken.ThrowIfCancellationRequested(); + var currentProject = currentSolution.GetRequiredProject(projectId); - if (CanUpgrade(currentProject, language, version)) - { - currentSolution = UpgradeProject(currentProject, version); - } + if (CanUpgrade(currentProject, language, version)) + { + currentSolution = UpgradeProject(currentProject, version); } - - return currentSolution; } - private bool CanUpgrade(Project project, string language, string version) - => project.Language == language && IsUpgrade(project, version); + return currentSolution; } + private bool CanUpgrade(Project project, string language, string version) + => project.Language == language && IsUpgrade(project, version); +} + #if CODE_STYLE - internal sealed class ProjectOptionsChangeAction : CodeAction - { - public override string Title { get; } +internal sealed class ProjectOptionsChangeAction : CodeAction +{ + public override string Title { get; } - private readonly Func> _createChangedSolution; + private readonly Func> _createChangedSolution; - private ProjectOptionsChangeAction(string title, Func> createChangedSolution) - { - this.Title = title; - _createChangedSolution = createChangedSolution; - } + private ProjectOptionsChangeAction(string title, Func> createChangedSolution) + { + this.Title = title; + _createChangedSolution = createChangedSolution; + } - public static ProjectOptionsChangeAction Create(string title, Func> createChangedSolution) - => new(title, createChangedSolution); + public static ProjectOptionsChangeAction Create(string title, Func> createChangedSolution) + => new(title, createChangedSolution); - protected override Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) - => SpecializedTasks.EmptyEnumerable(); + protected override Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) + => SpecializedTasks.EmptyEnumerable(); - protected override async Task GetChangedSolutionAsync(CancellationToken cancellationToken) - => await _createChangedSolution(cancellationToken).ConfigureAwait(false); - } + protected override async Task GetChangedSolutionAsync(CancellationToken cancellationToken) + => await _createChangedSolution(cancellationToken).ConfigureAwait(false); +} #else - internal sealed class ProjectOptionsChangeAction : SolutionChangeAction - { - public override ImmutableArray Tags => RequiresNonDocumentChangeTags; +internal sealed class ProjectOptionsChangeAction : SolutionChangeAction +{ + public override ImmutableArray Tags => RequiresNonDocumentChangeTags; - private ProjectOptionsChangeAction(string title, Func, CancellationToken, Task> createChangedSolution) - : base(title, createChangedSolution, equivalenceKey: null, priority: CodeActionPriority.Default, createdFromFactoryMethod: true) - { - } + private ProjectOptionsChangeAction(string title, Func, CancellationToken, Task> createChangedSolution) + : base(title, createChangedSolution, equivalenceKey: null, priority: CodeActionPriority.Default, createdFromFactoryMethod: true) + { + } - public static ProjectOptionsChangeAction Create(string title, Func, CancellationToken, Task> createChangedSolution) - => new(title, createChangedSolution); + public static ProjectOptionsChangeAction Create(string title, Func, CancellationToken, Task> createChangedSolution) + => new(title, createChangedSolution); - protected override Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) - => SpecializedTasks.EmptyEnumerable(); - } + protected override Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) + => SpecializedTasks.EmptyEnumerable(); +} #endif -} diff --git a/src/Analyzers/Core/CodeFixes/UseCompoundAssignment/AbstractUseCompoundAssignmentCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UseCompoundAssignment/AbstractUseCompoundAssignmentCodeFixProvider.cs index 08aa14cb91d88..f4ed3a1fc1ba8 100644 --- a/src/Analyzers/Core/CodeFixes/UseCompoundAssignment/AbstractUseCompoundAssignmentCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UseCompoundAssignment/AbstractUseCompoundAssignmentCodeFixProvider.cs @@ -13,99 +13,98 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.UseCompoundAssignment +namespace Microsoft.CodeAnalysis.UseCompoundAssignment; + +internal abstract class AbstractUseCompoundAssignmentCodeFixProvider< + TSyntaxKind, TAssignmentSyntax, TExpressionSyntax> + : SyntaxEditorBasedCodeFixProvider + where TSyntaxKind : struct + where TAssignmentSyntax : SyntaxNode + where TExpressionSyntax : SyntaxNode { - internal abstract class AbstractUseCompoundAssignmentCodeFixProvider< - TSyntaxKind, TAssignmentSyntax, TExpressionSyntax> - : SyntaxEditorBasedCodeFixProvider - where TSyntaxKind : struct - where TAssignmentSyntax : SyntaxNode - where TExpressionSyntax : SyntaxNode - { - public override ImmutableArray FixableDiagnosticIds { get; } = - [IDEDiagnosticIds.UseCompoundAssignmentDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds { get; } = + [IDEDiagnosticIds.UseCompoundAssignmentDiagnosticId]; - // See comments in the analyzer for what these maps are for. + // See comments in the analyzer for what these maps are for. - private readonly ImmutableDictionary _binaryToAssignmentMap; - private readonly ImmutableDictionary _assignmentToTokenMap; + private readonly ImmutableDictionary _binaryToAssignmentMap; + private readonly ImmutableDictionary _assignmentToTokenMap; - protected AbstractUseCompoundAssignmentCodeFixProvider( - ImmutableArray<(TSyntaxKind exprKind, TSyntaxKind assignmentKind, TSyntaxKind tokenKind)> kinds) - { - UseCompoundAssignmentUtilities.GenerateMaps(kinds, out _binaryToAssignmentMap, out _assignmentToTokenMap); - } - - protected abstract SyntaxToken Token(TSyntaxKind kind); - protected abstract TAssignmentSyntax Assignment( - TSyntaxKind assignmentOpKind, TExpressionSyntax left, SyntaxToken syntaxToken, TExpressionSyntax right); - protected abstract TExpressionSyntax Increment(TExpressionSyntax left, bool postfix); - protected abstract TExpressionSyntax Decrement(TExpressionSyntax left, bool postfix); - protected abstract SyntaxTriviaList PrepareRightExpressionLeadingTrivia(SyntaxTriviaList initialTrivia); + protected AbstractUseCompoundAssignmentCodeFixProvider( + ImmutableArray<(TSyntaxKind exprKind, TSyntaxKind assignmentKind, TSyntaxKind tokenKind)> kinds) + { + UseCompoundAssignmentUtilities.GenerateMaps(kinds, out _binaryToAssignmentMap, out _assignmentToTokenMap); + } - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, AnalyzersResources.Use_compound_assignment, nameof(AnalyzersResources.Use_compound_assignment)); - return Task.CompletedTask; - } + protected abstract SyntaxToken Token(TSyntaxKind kind); + protected abstract TAssignmentSyntax Assignment( + TSyntaxKind assignmentOpKind, TExpressionSyntax left, SyntaxToken syntaxToken, TExpressionSyntax right); + protected abstract TExpressionSyntax Increment(TExpressionSyntax left, bool postfix); + protected abstract TExpressionSyntax Decrement(TExpressionSyntax left, bool postfix); + protected abstract SyntaxTriviaList PrepareRightExpressionLeadingTrivia(SyntaxTriviaList initialTrivia); - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var syntaxFacts = document.GetRequiredLanguageService(); - var syntaxKinds = syntaxFacts.SyntaxKinds; + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, AnalyzersResources.Use_compound_assignment, nameof(AnalyzersResources.Use_compound_assignment)); + return Task.CompletedTask; + } - foreach (var diagnostic in diagnostics) - { - var assignment = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var syntaxKinds = syntaxFacts.SyntaxKinds; - editor.ReplaceNode(assignment, - (current, generator) => - { - if (current is not TAssignmentSyntax currentAssignment) - return current; + foreach (var diagnostic in diagnostics) + { + var assignment = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); - syntaxFacts.GetPartsOfAssignmentExpressionOrStatement(currentAssignment, - out var leftOfAssign, out var equalsToken, out var rightOfAssign); + editor.ReplaceNode(assignment, + (current, generator) => + { + if (current is not TAssignmentSyntax currentAssignment) + return current; - while (syntaxFacts.IsParenthesizedExpression(rightOfAssign)) - rightOfAssign = syntaxFacts.Unparenthesize(rightOfAssign); + syntaxFacts.GetPartsOfAssignmentExpressionOrStatement(currentAssignment, + out var leftOfAssign, out var equalsToken, out var rightOfAssign); - syntaxFacts.GetPartsOfBinaryExpression(rightOfAssign, - out _, out var opToken, out var rightExpr); + while (syntaxFacts.IsParenthesizedExpression(rightOfAssign)) + rightOfAssign = syntaxFacts.Unparenthesize(rightOfAssign); - if (diagnostic.Properties.ContainsKey(UseCompoundAssignmentUtilities.Increment)) - return Increment((TExpressionSyntax)leftOfAssign, PreferPostfix(syntaxFacts, currentAssignment)).WithTriviaFrom(currentAssignment); + syntaxFacts.GetPartsOfBinaryExpression(rightOfAssign, + out _, out var opToken, out var rightExpr); - if (diagnostic.Properties.ContainsKey(UseCompoundAssignmentUtilities.Decrement)) - return Decrement((TExpressionSyntax)leftOfAssign, PreferPostfix(syntaxFacts, currentAssignment)).WithTriviaFrom(currentAssignment); + if (diagnostic.Properties.ContainsKey(UseCompoundAssignmentUtilities.Increment)) + return Increment((TExpressionSyntax)leftOfAssign, PreferPostfix(syntaxFacts, currentAssignment)).WithTriviaFrom(currentAssignment); - var assignmentOpKind = _binaryToAssignmentMap[syntaxKinds.Convert(rightOfAssign.RawKind)]; - var compoundOperator = Token(_assignmentToTokenMap[assignmentOpKind]); + if (diagnostic.Properties.ContainsKey(UseCompoundAssignmentUtilities.Decrement)) + return Decrement((TExpressionSyntax)leftOfAssign, PreferPostfix(syntaxFacts, currentAssignment)).WithTriviaFrom(currentAssignment); - rightExpr = rightExpr.WithLeadingTrivia(PrepareRightExpressionLeadingTrivia(rightExpr.GetLeadingTrivia())); + var assignmentOpKind = _binaryToAssignmentMap[syntaxKinds.Convert(rightOfAssign.RawKind)]; + var compoundOperator = Token(_assignmentToTokenMap[assignmentOpKind]); - return Assignment( - assignmentOpKind, - (TExpressionSyntax)leftOfAssign, - compoundOperator.WithTriviaFrom(equalsToken), - (TExpressionSyntax)rightExpr); - }); - } + rightExpr = rightExpr.WithLeadingTrivia(PrepareRightExpressionLeadingTrivia(rightExpr.GetLeadingTrivia())); - return Task.CompletedTask; + return Assignment( + assignmentOpKind, + (TExpressionSyntax)leftOfAssign, + compoundOperator.WithTriviaFrom(equalsToken), + (TExpressionSyntax)rightExpr); + }); } - protected virtual bool PreferPostfix(ISyntaxFactsService syntaxFacts, TAssignmentSyntax currentAssignment) - { - // If we have `x = x + 1;` on it's own, then we prefer `x++` as idiomatic. - if (syntaxFacts.IsSimpleAssignmentStatement(currentAssignment.Parent)) - return true; + return Task.CompletedTask; + } - // In any other circumstance, the value of the assignment might be read, so we need to transform to - // ++x to ensure that we preserve semantics. - return false; - } + protected virtual bool PreferPostfix(ISyntaxFactsService syntaxFacts, TAssignmentSyntax currentAssignment) + { + // If we have `x = x + 1;` on it's own, then we prefer `x++` as idiomatic. + if (syntaxFacts.IsSimpleAssignmentStatement(currentAssignment.Parent)) + return true; + + // In any other circumstance, the value of the assignment might be read, so we need to transform to + // ++x to ensure that we preserve semantics. + return false; } } diff --git a/src/Analyzers/Core/CodeFixes/UseConditionalExpression/AbstractUseConditionalExpressionCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UseConditionalExpression/AbstractUseConditionalExpressionCodeFixProvider.cs index 104a04b631717..5f39393f7b430 100644 --- a/src/Analyzers/Core/CodeFixes/UseConditionalExpression/AbstractUseConditionalExpressionCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UseConditionalExpression/AbstractUseConditionalExpressionCodeFixProvider.cs @@ -22,185 +22,184 @@ using Formatter = Microsoft.CodeAnalysis.Formatting.Formatter; #endif -namespace Microsoft.CodeAnalysis.UseConditionalExpression +namespace Microsoft.CodeAnalysis.UseConditionalExpression; + +using static UseConditionalExpressionCodeFixHelpers; +using static UseConditionalExpressionHelpers; + +internal abstract class AbstractUseConditionalExpressionCodeFixProvider< + TStatementSyntax, + TIfStatementSyntax, + TExpressionSyntax, + TConditionalExpressionSyntax> : SyntaxEditorBasedCodeFixProvider + where TStatementSyntax : SyntaxNode + where TIfStatementSyntax : TStatementSyntax + where TExpressionSyntax : SyntaxNode + where TConditionalExpressionSyntax : TExpressionSyntax { - using static UseConditionalExpressionCodeFixHelpers; - using static UseConditionalExpressionHelpers; - - internal abstract class AbstractUseConditionalExpressionCodeFixProvider< - TStatementSyntax, - TIfStatementSyntax, - TExpressionSyntax, - TConditionalExpressionSyntax> : SyntaxEditorBasedCodeFixProvider - where TStatementSyntax : SyntaxNode - where TIfStatementSyntax : TStatementSyntax - where TExpressionSyntax : SyntaxNode - where TConditionalExpressionSyntax : TExpressionSyntax - { - protected abstract ISyntaxFacts SyntaxFacts { get; } - protected abstract AbstractFormattingRule GetMultiLineFormattingRule(); + protected abstract ISyntaxFacts SyntaxFacts { get; } + protected abstract AbstractFormattingRule GetMultiLineFormattingRule(); - protected abstract ISyntaxFormatting GetSyntaxFormatting(); + protected abstract ISyntaxFormatting GetSyntaxFormatting(); - protected abstract TExpressionSyntax ConvertToExpression(IThrowOperation throwOperation); - protected abstract TStatementSyntax WrapWithBlockIfAppropriate(TIfStatementSyntax ifStatement, TStatementSyntax statement); + protected abstract TExpressionSyntax ConvertToExpression(IThrowOperation throwOperation); + protected abstract TStatementSyntax WrapWithBlockIfAppropriate(TIfStatementSyntax ifStatement, TStatementSyntax statement); - protected abstract Task FixOneAsync( - Document document, Diagnostic diagnostic, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken); + protected abstract Task FixOneAsync( + Document document, Diagnostic diagnostic, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken); - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, SyntaxEditor editor, - CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + // Defer to our callback to actually make the edits for each diagnostic. In turn, it + // will return 'true' if it made a multi-line conditional expression. In that case, + // we'll need to explicitly format this node so we can get our special multi-line + // formatting in VB and C#. + var nestedEditor = new SyntaxEditor(root, document.Project.Solution.Services); + foreach (var diagnostic in diagnostics) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - // Defer to our callback to actually make the edits for each diagnostic. In turn, it - // will return 'true' if it made a multi-line conditional expression. In that case, - // we'll need to explicitly format this node so we can get our special multi-line - // formatting in VB and C#. - var nestedEditor = new SyntaxEditor(root, document.Project.Solution.Services); - foreach (var diagnostic in diagnostics) - { - await FixOneAsync( - document, diagnostic, nestedEditor, fallbackOptions, cancellationToken).ConfigureAwait(false); - } + await FixOneAsync( + document, diagnostic, nestedEditor, fallbackOptions, cancellationToken).ConfigureAwait(false); + } - var changedRoot = nestedEditor.GetChangedRoot(); + var changedRoot = nestedEditor.GetChangedRoot(); - // Get the language specific rule for formatting this construct and call into the - // formatted to explicitly format things. Note: all we will format is the new - // conditional expression as that's the only node that has the appropriate - // annotation on it. - var rules = new List { GetMultiLineFormattingRule() }; + // Get the language specific rule for formatting this construct and call into the + // formatted to explicitly format things. Note: all we will format is the new + // conditional expression as that's the only node that has the appropriate + // annotation on it. + var rules = new List { GetMultiLineFormattingRule() }; #if CODE_STYLE - var provider = GetSyntaxFormatting(); + var provider = GetSyntaxFormatting(); #else - var provider = document.Project.Solution.Services; + var provider = document.Project.Solution.Services; #endif - var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var formattingOptions = options.GetFormattingOptions(GetSyntaxFormatting()); - var formattedRoot = Formatter.Format(changedRoot, SpecializedFormattingAnnotation, provider, formattingOptions, rules, cancellationToken); - - changedRoot = formattedRoot; + var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var formattingOptions = options.GetFormattingOptions(GetSyntaxFormatting()); + var formattedRoot = Formatter.Format(changedRoot, SpecializedFormattingAnnotation, provider, formattingOptions, rules, cancellationToken); - editor.ReplaceNode(root, changedRoot); - } + changedRoot = formattedRoot; - /// - /// Helper to create a conditional expression out of two original IOperation values - /// corresponding to the whenTrue and whenFalse parts. The helper will add the appropriate - /// annotations and casts to ensure that the conditional expression preserves semantics, but - /// is also properly simplified and formatted. - /// - protected async Task CreateConditionalExpressionAsync( - Document document, IConditionalOperation ifOperation, - IOperation trueStatement, IOperation falseStatement, - IOperation trueValue, IOperation falseValue, - bool isRef, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var generator = SyntaxGenerator.GetGenerator(document); - var generatorInternal = document.GetRequiredLanguageService(); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(root, changedRoot); + } - var condition = WrapIfStatementIfNecessary(ifOperation); - if (CanSimplify(trueValue, falseValue, isRef, out var negate)) - { - return negate - ? (TExpressionSyntax)generator.Negate(generatorInternal, condition, semanticModel, cancellationToken).WithoutTrivia() - : (TExpressionSyntax)condition.WithoutTrivia(); - } + /// + /// Helper to create a conditional expression out of two original IOperation values + /// corresponding to the whenTrue and whenFalse parts. The helper will add the appropriate + /// annotations and casts to ensure that the conditional expression preserves semantics, but + /// is also properly simplified and formatted. + /// + protected async Task CreateConditionalExpressionAsync( + Document document, IConditionalOperation ifOperation, + IOperation trueStatement, IOperation falseStatement, + IOperation trueValue, IOperation falseValue, + bool isRef, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + var generatorInternal = document.GetRequiredLanguageService(); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var trueExpression = MakeRef(generatorInternal, isRef, CastValueIfNecessary(generator, trueStatement, trueValue)); - var falseExpression = MakeRef(generatorInternal, isRef, CastValueIfNecessary(generator, falseStatement, falseValue)); - trueExpression = WrapReturnExpressionIfNecessary(trueExpression, trueStatement); - falseExpression = WrapReturnExpressionIfNecessary(falseExpression, falseStatement); - - var conditionalExpression = (TConditionalExpressionSyntax)generator.ConditionalExpression( - condition.WithoutTrivia(), - trueExpression, - falseExpression); - - conditionalExpression = conditionalExpression.WithAdditionalAnnotations(Simplifier.Annotation); - var makeMultiLine = await MakeMultiLineAsync( - document, condition, - trueValue.Syntax, falseValue.Syntax, fallbackOptions, cancellationToken).ConfigureAwait(false); - if (makeMultiLine) - { - conditionalExpression = conditionalExpression.WithAdditionalAnnotations( - SpecializedFormattingAnnotation); - } + var condition = WrapIfStatementIfNecessary(ifOperation); + if (CanSimplify(trueValue, falseValue, isRef, out var negate)) + { + return negate + ? (TExpressionSyntax)generator.Negate(generatorInternal, condition, semanticModel, cancellationToken).WithoutTrivia() + : (TExpressionSyntax)condition.WithoutTrivia(); + } - return MakeRef(generatorInternal, isRef, conditionalExpression); + var trueExpression = MakeRef(generatorInternal, isRef, CastValueIfNecessary(generator, trueStatement, trueValue)); + var falseExpression = MakeRef(generatorInternal, isRef, CastValueIfNecessary(generator, falseStatement, falseValue)); + trueExpression = WrapReturnExpressionIfNecessary(trueExpression, trueStatement); + falseExpression = WrapReturnExpressionIfNecessary(falseExpression, falseStatement); + + var conditionalExpression = (TConditionalExpressionSyntax)generator.ConditionalExpression( + condition.WithoutTrivia(), + trueExpression, + falseExpression); + + conditionalExpression = conditionalExpression.WithAdditionalAnnotations(Simplifier.Annotation); + var makeMultiLine = await MakeMultiLineAsync( + document, condition, + trueValue.Syntax, falseValue.Syntax, fallbackOptions, cancellationToken).ConfigureAwait(false); + if (makeMultiLine) + { + conditionalExpression = conditionalExpression.WithAdditionalAnnotations( + SpecializedFormattingAnnotation); } - protected virtual SyntaxNode WrapIfStatementIfNecessary(IConditionalOperation operation) - => operation.Condition.Syntax; + return MakeRef(generatorInternal, isRef, conditionalExpression); + } + + protected virtual SyntaxNode WrapIfStatementIfNecessary(IConditionalOperation operation) + => operation.Condition.Syntax; - protected virtual TExpressionSyntax WrapReturnExpressionIfNecessary(TExpressionSyntax returnExpression, IOperation returnOperation) - => returnExpression; + protected virtual TExpressionSyntax WrapReturnExpressionIfNecessary(TExpressionSyntax returnExpression, IOperation returnOperation) + => returnExpression; - private static TExpressionSyntax MakeRef(SyntaxGeneratorInternal generator, bool isRef, TExpressionSyntax syntaxNode) - => isRef ? (TExpressionSyntax)generator.RefExpression(syntaxNode) : syntaxNode; + private static TExpressionSyntax MakeRef(SyntaxGeneratorInternal generator, bool isRef, TExpressionSyntax syntaxNode) + => isRef ? (TExpressionSyntax)generator.RefExpression(syntaxNode) : syntaxNode; - /// - /// Checks if we should wrap the conditional expression over multiple lines. - /// - private static async Task MakeMultiLineAsync( - Document document, SyntaxNode condition, SyntaxNode trueSyntax, SyntaxNode falseSyntax, CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) + /// + /// Checks if we should wrap the conditional expression over multiple lines. + /// + private static async Task MakeMultiLineAsync( + Document document, SyntaxNode condition, SyntaxNode trueSyntax, SyntaxNode falseSyntax, CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (!sourceText.AreOnSameLine(condition.GetFirstToken(), condition.GetLastToken()) || + !sourceText.AreOnSameLine(trueSyntax.GetFirstToken(), trueSyntax.GetLastToken()) || + !sourceText.AreOnSameLine(falseSyntax.GetFirstToken(), falseSyntax.GetLastToken())) { - var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (!sourceText.AreOnSameLine(condition.GetFirstToken(), condition.GetLastToken()) || - !sourceText.AreOnSameLine(trueSyntax.GetFirstToken(), trueSyntax.GetLastToken()) || - !sourceText.AreOnSameLine(falseSyntax.GetFirstToken(), falseSyntax.GetLastToken())) - { - return true; - } + return true; + } - // the option is currently not an editorconfig option, so not available in code style layer - var wrappingLength = + // the option is currently not an editorconfig option, so not available in code style layer + var wrappingLength = #if !CODE_STYLE - fallbackOptions.GetOptions(document.Project.Services)?.ConditionalExpressionWrappingLength ?? + fallbackOptions.GetOptions(document.Project.Services)?.ConditionalExpressionWrappingLength ?? #endif - CodeActionOptions.DefaultConditionalExpressionWrappingLength; - - if (condition.Span.Length + trueSyntax.Span.Length + falseSyntax.Span.Length > wrappingLength) - { - return true; - } + CodeActionOptions.DefaultConditionalExpressionWrappingLength; - return false; + if (condition.Span.Length + trueSyntax.Span.Length + falseSyntax.Span.Length > wrappingLength) + { + return true; } - private TExpressionSyntax CastValueIfNecessary( - SyntaxGenerator generator, IOperation statement, IOperation value) + return false; + } + + private TExpressionSyntax CastValueIfNecessary( + SyntaxGenerator generator, IOperation statement, IOperation value) + { + if (statement is IThrowOperation throwOperation) + return ConvertToExpression(throwOperation); + + var sourceSyntax = value.Syntax.WithoutTrivia(); + + // If there was an implicit conversion generated by the compiler, then convert that to an + // explicit conversion inside the condition. This is needed as there is no type + // inference in conditional expressions, so we need to ensure that the same conversions + // that were occurring previously still occur after conversion. Note: the simplifier + // will remove any of these casts that are unnecessary. + if (value is IConversionOperation conversion && + conversion.IsImplicit && + conversion.Type != null && + conversion.Type.TypeKind != TypeKind.Error) { - if (statement is IThrowOperation throwOperation) - return ConvertToExpression(throwOperation); - - var sourceSyntax = value.Syntax.WithoutTrivia(); - - // If there was an implicit conversion generated by the compiler, then convert that to an - // explicit conversion inside the condition. This is needed as there is no type - // inference in conditional expressions, so we need to ensure that the same conversions - // that were occurring previously still occur after conversion. Note: the simplifier - // will remove any of these casts that are unnecessary. - if (value is IConversionOperation conversion && - conversion.IsImplicit && - conversion.Type != null && - conversion.Type.TypeKind != TypeKind.Error) + // Note we only add the cast if the source had no type (like the null literal), or a + // non-error type itself. We don't want to insert lots of casts in error code. + if (conversion.Operand.Type == null || conversion.Operand.Type.TypeKind != TypeKind.Error) { - // Note we only add the cast if the source had no type (like the null literal), or a - // non-error type itself. We don't want to insert lots of casts in error code. - if (conversion.Operand.Type == null || conversion.Operand.Type.TypeKind != TypeKind.Error) - { - return (TExpressionSyntax)generator.CastExpression(conversion.Type, sourceSyntax); - } + return (TExpressionSyntax)generator.CastExpression(conversion.Type, sourceSyntax); } - - return (TExpressionSyntax)sourceSyntax; } + + return (TExpressionSyntax)sourceSyntax; } } diff --git a/src/Analyzers/Core/CodeFixes/UseConditionalExpression/ForAssignment/AbstractUseConditionalExpressionForAssignmentCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UseConditionalExpression/ForAssignment/AbstractUseConditionalExpressionForAssignmentCodeFixProvider.cs index 13ecebf89b1ee..7f6a796257477 100644 --- a/src/Analyzers/Core/CodeFixes/UseConditionalExpression/ForAssignment/AbstractUseConditionalExpressionForAssignmentCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UseConditionalExpression/ForAssignment/AbstractUseConditionalExpressionForAssignmentCodeFixProvider.cs @@ -19,250 +19,249 @@ using static Microsoft.CodeAnalysis.UseConditionalExpression.UseConditionalExpressionCodeFixHelpers; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.CodeAnalysis.UseConditionalExpression +namespace Microsoft.CodeAnalysis.UseConditionalExpression; + +internal abstract class AbstractUseConditionalExpressionForAssignmentCodeFixProvider< + TStatementSyntax, + TIfStatementSyntax, + TLocalDeclarationStatementSyntax, + TVariableDeclaratorSyntax, + TExpressionSyntax, + TConditionalExpressionSyntax> + : AbstractUseConditionalExpressionCodeFixProvider + where TStatementSyntax : SyntaxNode + where TIfStatementSyntax : TStatementSyntax + where TLocalDeclarationStatementSyntax : TStatementSyntax + where TVariableDeclaratorSyntax : SyntaxNode + where TExpressionSyntax : SyntaxNode + where TConditionalExpressionSyntax : TExpressionSyntax { - internal abstract class AbstractUseConditionalExpressionForAssignmentCodeFixProvider< - TStatementSyntax, - TIfStatementSyntax, - TLocalDeclarationStatementSyntax, - TVariableDeclaratorSyntax, - TExpressionSyntax, - TConditionalExpressionSyntax> - : AbstractUseConditionalExpressionCodeFixProvider - where TStatementSyntax : SyntaxNode - where TIfStatementSyntax : TStatementSyntax - where TLocalDeclarationStatementSyntax : TStatementSyntax - where TVariableDeclaratorSyntax : SyntaxNode - where TExpressionSyntax : SyntaxNode - where TConditionalExpressionSyntax : TExpressionSyntax + protected abstract TVariableDeclaratorSyntax WithInitializer(TVariableDeclaratorSyntax variable, TExpressionSyntax value); + protected abstract TVariableDeclaratorSyntax GetDeclaratorSyntax(IVariableDeclaratorOperation declarator); + protected abstract TLocalDeclarationStatementSyntax AddSimplificationToType(TLocalDeclarationStatementSyntax updatedLocalDeclaration); + + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseConditionalExpressionForAssignmentDiagnosticId]; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) { - protected abstract TVariableDeclaratorSyntax WithInitializer(TVariableDeclaratorSyntax variable, TExpressionSyntax value); - protected abstract TVariableDeclaratorSyntax GetDeclaratorSyntax(IVariableDeclaratorOperation declarator); - protected abstract TLocalDeclarationStatementSyntax AddSimplificationToType(TLocalDeclarationStatementSyntax updatedLocalDeclaration); + var (title, key) = context.Diagnostics.First().Properties.ContainsKey(UseConditionalExpressionHelpers.CanSimplifyName) + ? (AnalyzersResources.Simplify_check, nameof(AnalyzersResources.Simplify_check)) + : (AnalyzersResources.Convert_to_conditional_expression, nameof(AnalyzersResources.Convert_to_conditional_expression)); - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseConditionalExpressionForAssignmentDiagnosticId]; + RegisterCodeFix(context, title, key); + return Task.CompletedTask; + } - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var (title, key) = context.Diagnostics.First().Properties.ContainsKey(UseConditionalExpressionHelpers.CanSimplifyName) - ? (AnalyzersResources.Simplify_check, nameof(AnalyzersResources.Simplify_check)) - : (AnalyzersResources.Convert_to_conditional_expression, nameof(AnalyzersResources.Convert_to_conditional_expression)); + /// + /// Returns 'true' if a multi-line conditional was created, and thus should be + /// formatted specially. + /// + protected override async Task FixOneAsync( + Document document, Diagnostic diagnostic, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var ifStatement = diagnostic.AdditionalLocations[0].FindNode(cancellationToken); - RegisterCodeFix(context, title, key); - return Task.CompletedTask; - } + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var ifOperation = (IConditionalOperation)semanticModel.GetOperation(ifStatement, cancellationToken)!; - /// - /// Returns 'true' if a multi-line conditional was created, and thus should be - /// formatted specially. - /// - protected override async Task FixOneAsync( - Document document, Diagnostic diagnostic, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + if (!UseConditionalExpressionForAssignmentHelpers.TryMatchPattern( + syntaxFacts, ifOperation, out var isRef, + out var trueStatement, out var falseStatement, + out var trueAssignment, out var falseAssignment)) { - var syntaxFacts = document.GetRequiredLanguageService(); - var ifStatement = diagnostic.AdditionalLocations[0].FindNode(cancellationToken); - - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var ifOperation = (IConditionalOperation)semanticModel.GetOperation(ifStatement, cancellationToken)!; + return; + } - if (!UseConditionalExpressionForAssignmentHelpers.TryMatchPattern( - syntaxFacts, ifOperation, out var isRef, - out var trueStatement, out var falseStatement, - out var trueAssignment, out var falseAssignment)) - { - return; - } + var conditionalExpression = await CreateConditionalExpressionAsync( + document, ifOperation, + trueStatement, falseStatement, + trueAssignment?.Value ?? trueStatement, + falseAssignment?.Value ?? falseStatement, + isRef, + fallbackOptions, + cancellationToken).ConfigureAwait(false); + + // See if we're assigning to a variable declared directly above the if statement. If so, + // try to inline the conditional directly into the initializer for that variable. + if (TryConvertWhenAssignmentToLocalDeclaredImmediateAbove( + syntaxFacts, editor, ifOperation, + trueAssignment, falseAssignment, conditionalExpression)) + { + return; - var conditionalExpression = await CreateConditionalExpressionAsync( - document, ifOperation, - trueStatement, falseStatement, - trueAssignment?.Value ?? trueStatement, - falseAssignment?.Value ?? falseStatement, - isRef, - fallbackOptions, - cancellationToken).ConfigureAwait(false); - - // See if we're assigning to a variable declared directly above the if statement. If so, - // try to inline the conditional directly into the initializer for that variable. - if (TryConvertWhenAssignmentToLocalDeclaredImmediateAbove( - syntaxFacts, editor, ifOperation, - trueAssignment, falseAssignment, conditionalExpression)) - { - return; + } - } + // If not, just replace the if-statement with a single assignment of the new + // conditional. + ConvertOnlyIfToConditionalExpression( + editor, ifOperation, (trueAssignment ?? falseAssignment)!, conditionalExpression); + } - // If not, just replace the if-statement with a single assignment of the new - // conditional. - ConvertOnlyIfToConditionalExpression( - editor, ifOperation, (trueAssignment ?? falseAssignment)!, conditionalExpression); - } + private void ConvertOnlyIfToConditionalExpression( + SyntaxEditor editor, + IConditionalOperation ifOperation, + ISimpleAssignmentOperation assignment, + TExpressionSyntax conditionalExpression) + { + var generator = editor.Generator; + var ifStatement = (TIfStatementSyntax)ifOperation.Syntax; + var expressionStatement = (TStatementSyntax)generator.ExpressionStatement( + generator.AssignmentStatement( + assignment.Target.Syntax, + conditionalExpression)).WithTriviaFrom(ifStatement); + + editor.ReplaceNode( + ifOperation.Syntax, + WrapWithBlockIfAppropriate(ifStatement, expressionStatement)); + } - private void ConvertOnlyIfToConditionalExpression( - SyntaxEditor editor, - IConditionalOperation ifOperation, - ISimpleAssignmentOperation assignment, - TExpressionSyntax conditionalExpression) + private bool TryConvertWhenAssignmentToLocalDeclaredImmediateAbove( + ISyntaxFactsService syntaxFacts, SyntaxEditor editor, IConditionalOperation ifOperation, + ISimpleAssignmentOperation? trueAssignment, + ISimpleAssignmentOperation? falseAssignment, + TExpressionSyntax conditionalExpression) + { + if (!TryFindMatchingLocalDeclarationImmediatelyAbove( + ifOperation, trueAssignment, falseAssignment, + out var localDeclarationOperation, out var declarator)) { - var generator = editor.Generator; - var ifStatement = (TIfStatementSyntax)ifOperation.Syntax; - var expressionStatement = (TStatementSyntax)generator.ExpressionStatement( - generator.AssignmentStatement( - assignment.Target.Syntax, - conditionalExpression)).WithTriviaFrom(ifStatement); - - editor.ReplaceNode( - ifOperation.Syntax, - WrapWithBlockIfAppropriate(ifStatement, expressionStatement)); + return false; } - private bool TryConvertWhenAssignmentToLocalDeclaredImmediateAbove( - ISyntaxFactsService syntaxFacts, SyntaxEditor editor, IConditionalOperation ifOperation, - ISimpleAssignmentOperation? trueAssignment, - ISimpleAssignmentOperation? falseAssignment, - TExpressionSyntax conditionalExpression) - { - if (!TryFindMatchingLocalDeclarationImmediatelyAbove( - ifOperation, trueAssignment, falseAssignment, - out var localDeclarationOperation, out var declarator)) - { - return false; - } + // We found a valid local declaration right above the if-statement. + var localDeclaration = localDeclarationOperation.Syntax; + var variable = GetDeclaratorSyntax(declarator); - // We found a valid local declaration right above the if-statement. - var localDeclaration = localDeclarationOperation.Syntax; - var variable = GetDeclaratorSyntax(declarator); + // Initialize that variable with the conditional expression. + var updatedVariable = WithInitializer(variable, conditionalExpression); - // Initialize that variable with the conditional expression. - var updatedVariable = WithInitializer(variable, conditionalExpression); + // Because we merged the initialization and the variable, the variable may now be able + // to use 'var' (c#), or elide its type (vb). Add the simplification annotation + // appropriately so that can happen later down the line. + var updatedLocalDeclaration = localDeclaration.ReplaceNode(variable, updatedVariable); + updatedLocalDeclaration = AddSimplificationToType( + (TLocalDeclarationStatementSyntax)updatedLocalDeclaration); - // Because we merged the initialization and the variable, the variable may now be able - // to use 'var' (c#), or elide its type (vb). Add the simplification annotation - // appropriately so that can happen later down the line. - var updatedLocalDeclaration = localDeclaration.ReplaceNode(variable, updatedVariable); - updatedLocalDeclaration = AddSimplificationToType( - (TLocalDeclarationStatementSyntax)updatedLocalDeclaration); + editor.ReplaceNode(localDeclaration, updatedLocalDeclaration); + editor.RemoveNode(ifOperation.Syntax, GetRemoveOptions(syntaxFacts, ifOperation.Syntax)); + return true; + } - editor.ReplaceNode(localDeclaration, updatedLocalDeclaration); - editor.RemoveNode(ifOperation.Syntax, GetRemoveOptions(syntaxFacts, ifOperation.Syntax)); - return true; - } + private static bool TryFindMatchingLocalDeclarationImmediatelyAbove( + IConditionalOperation ifOperation, + ISimpleAssignmentOperation? trueAssignment, + ISimpleAssignmentOperation? falseAssignment, + [NotNullWhen(true)] out IVariableDeclarationGroupOperation? localDeclaration, + [NotNullWhen(true)] out IVariableDeclaratorOperation? declarator) + { + localDeclaration = null; + declarator = null; - private static bool TryFindMatchingLocalDeclarationImmediatelyAbove( - IConditionalOperation ifOperation, - ISimpleAssignmentOperation? trueAssignment, - ISimpleAssignmentOperation? falseAssignment, - [NotNullWhen(true)] out IVariableDeclarationGroupOperation? localDeclaration, - [NotNullWhen(true)] out IVariableDeclaratorOperation? declarator) + ILocalSymbol? local = null; + if (trueAssignment != null) { - localDeclaration = null; - declarator = null; + if (trueAssignment.Target is not ILocalReferenceOperation trueLocal) + return false; - ILocalSymbol? local = null; - if (trueAssignment != null) - { - if (trueAssignment.Target is not ILocalReferenceOperation trueLocal) - return false; + local = trueLocal.Local; + } - local = trueLocal.Local; - } + if (falseAssignment != null) + { + if (falseAssignment.Target is not ILocalReferenceOperation falseLocal) + return false; - if (falseAssignment != null) - { - if (falseAssignment.Target is not ILocalReferenceOperation falseLocal) - return false; + // See if both assignments are to the same local. + if (local != null && !Equals(local, falseLocal.Local)) + return false; - // See if both assignments are to the same local. - if (local != null && !Equals(local, falseLocal.Local)) - return false; + local = falseLocal.Local; + } - local = falseLocal.Local; - } + // We weren't assigning to a local. + if (local == null) + return false; - // We weren't assigning to a local. - if (local == null) - return false; + // If so, see if that local was declared immediately above the if-statement. + if (ifOperation.Parent is not IBlockOperation parentBlock) + { + return false; + } - // If so, see if that local was declared immediately above the if-statement. - if (ifOperation.Parent is not IBlockOperation parentBlock) - { - return false; - } + var ifIndex = parentBlock.Operations.IndexOf(ifOperation); + if (ifIndex <= 0) + { + return false; + } - var ifIndex = parentBlock.Operations.IndexOf(ifOperation); - if (ifIndex <= 0) - { - return false; - } + localDeclaration = parentBlock.Operations[ifIndex - 1] as IVariableDeclarationGroupOperation; + if (localDeclaration == null) + { + return false; + } - localDeclaration = parentBlock.Operations[ifIndex - 1] as IVariableDeclarationGroupOperation; - if (localDeclaration == null) - { - return false; - } + if (localDeclaration.IsImplicit) + { + return false; + } - if (localDeclaration.IsImplicit) - { - return false; - } + if (localDeclaration.Declarations.Length != 1) + { + return false; + } - if (localDeclaration.Declarations.Length != 1) - { - return false; - } + var declaration = localDeclaration.Declarations[0]; + var declarators = declaration.Declarators; + if (declarators.Length != 1) + { + return false; + } - var declaration = localDeclaration.Declarations[0]; - var declarators = declaration.Declarators; - if (declarators.Length != 1) - { - return false; - } + declarator = declarators[0]; + var variable = declarator.Symbol; + if (!Equals(variable, local)) + { + // wasn't a declaration of the local we're assigning to. + return false; + } - declarator = declarators[0]; - var variable = declarator.Symbol; - if (!Equals(variable, local)) + var variableInitializer = declarator.Initializer ?? declaration.Initializer; + if (variableInitializer?.Value != null) + { + var unwrapped = variableInitializer.Value.UnwrapImplicitConversion(); + // the variable has to either not have an initializer, or it needs to be basic + // literal/default expression. + if (unwrapped is not ILiteralOperation and + not IDefaultValueOperation) { - // wasn't a declaration of the local we're assigning to. return false; } + } - var variableInitializer = declarator.Initializer ?? declaration.Initializer; - if (variableInitializer?.Value != null) - { - var unwrapped = variableInitializer.Value.UnwrapImplicitConversion(); - // the variable has to either not have an initializer, or it needs to be basic - // literal/default expression. - if (unwrapped is not ILiteralOperation and - not IDefaultValueOperation) - { - return false; - } - } + // If the variable is referenced in the condition of the 'if' block, we can't merge the + // declaration and assignments. + return !ReferencesLocalVariable(ifOperation.Condition, variable); + } - // If the variable is referenced in the condition of the 'if' block, we can't merge the - // declaration and assignments. - return !ReferencesLocalVariable(ifOperation.Condition, variable); + private static bool ReferencesLocalVariable(IOperation operation, ILocalSymbol variable) + { + if (operation is ILocalReferenceOperation localReference && + Equals(variable, localReference.Local)) + { + return true; } - private static bool ReferencesLocalVariable(IOperation operation, ILocalSymbol variable) + foreach (var child in operation.ChildOperations) { - if (operation is ILocalReferenceOperation localReference && - Equals(variable, localReference.Local)) + if (ReferencesLocalVariable(child, variable)) { return true; } - - foreach (var child in operation.ChildOperations) - { - if (ReferencesLocalVariable(child, variable)) - { - return true; - } - } - - return false; } + + return false; } } diff --git a/src/Analyzers/Core/CodeFixes/UseConditionalExpression/ForReturn/AbstractUseConditionalExpressionForReturnCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UseConditionalExpression/ForReturn/AbstractUseConditionalExpressionForReturnCodeFixProvider.cs index f2aec6031b030..5b2651a7a039d 100644 --- a/src/Analyzers/Core/CodeFixes/UseConditionalExpression/ForReturn/AbstractUseConditionalExpressionForReturnCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UseConditionalExpression/ForReturn/AbstractUseConditionalExpressionForReturnCodeFixProvider.cs @@ -17,78 +17,77 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.UseConditionalExpression.UseConditionalExpressionCodeFixHelpers; -namespace Microsoft.CodeAnalysis.UseConditionalExpression +namespace Microsoft.CodeAnalysis.UseConditionalExpression; + +internal abstract class AbstractUseConditionalExpressionForReturnCodeFixProvider< + TStatementSyntax, + TIfStatementSyntax, + TExpressionSyntax, + TConditionalExpressionSyntax> + : AbstractUseConditionalExpressionCodeFixProvider + where TStatementSyntax : SyntaxNode + where TIfStatementSyntax : TStatementSyntax + where TExpressionSyntax : SyntaxNode + where TConditionalExpressionSyntax : TExpressionSyntax { - internal abstract class AbstractUseConditionalExpressionForReturnCodeFixProvider< - TStatementSyntax, - TIfStatementSyntax, - TExpressionSyntax, - TConditionalExpressionSyntax> - : AbstractUseConditionalExpressionCodeFixProvider - where TStatementSyntax : SyntaxNode - where TIfStatementSyntax : TStatementSyntax - where TExpressionSyntax : SyntaxNode - where TConditionalExpressionSyntax : TExpressionSyntax - { - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseConditionalExpressionForReturnDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseConditionalExpressionForReturnDiagnosticId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - var (title, key) = context.Diagnostics.First().Properties.ContainsKey(UseConditionalExpressionHelpers.CanSimplifyName) - ? (AnalyzersResources.Simplify_check, nameof(AnalyzersResources.Simplify_check)) - : (AnalyzersResources.Convert_to_conditional_expression, nameof(AnalyzersResources.Convert_to_conditional_expression)); + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var (title, key) = context.Diagnostics.First().Properties.ContainsKey(UseConditionalExpressionHelpers.CanSimplifyName) + ? (AnalyzersResources.Simplify_check, nameof(AnalyzersResources.Simplify_check)) + : (AnalyzersResources.Convert_to_conditional_expression, nameof(AnalyzersResources.Convert_to_conditional_expression)); - RegisterCodeFix(context, title, key); - return Task.CompletedTask; - } + RegisterCodeFix(context, title, key); + return Task.CompletedTask; + } - protected override async Task FixOneAsync( - Document document, Diagnostic diagnostic, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var syntaxFacts = document.GetRequiredLanguageService(); - var ifStatement = (TIfStatementSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken); + protected override async Task FixOneAsync( + Document document, Diagnostic diagnostic, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var ifStatement = (TIfStatementSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var ifOperation = (IConditionalOperation)semanticModel.GetOperation(ifStatement, cancellationToken)!; - var containingSymbol = semanticModel.GetRequiredEnclosingSymbol(ifStatement.SpanStart, cancellationToken); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var ifOperation = (IConditionalOperation)semanticModel.GetOperation(ifStatement, cancellationToken)!; + var containingSymbol = semanticModel.GetRequiredEnclosingSymbol(ifStatement.SpanStart, cancellationToken); - if (!UseConditionalExpressionForReturnHelpers.TryMatchPattern( - syntaxFacts, ifOperation, containingSymbol, out var isRef, - out var trueStatement, out var falseStatement, - out var trueReturn, out var falseReturn)) - { - return; - } + if (!UseConditionalExpressionForReturnHelpers.TryMatchPattern( + syntaxFacts, ifOperation, containingSymbol, out var isRef, + out var trueStatement, out var falseStatement, + out var trueReturn, out var falseReturn)) + { + return; + } - var anyReturn = (trueReturn ?? falseReturn)!; - var conditionalExpression = await CreateConditionalExpressionAsync( - document, ifOperation, - trueStatement, falseStatement, - trueReturn?.ReturnedValue ?? trueStatement, - falseReturn?.ReturnedValue ?? falseStatement, - isRef, - fallbackOptions, - cancellationToken).ConfigureAwait(false); + var anyReturn = (trueReturn ?? falseReturn)!; + var conditionalExpression = await CreateConditionalExpressionAsync( + document, ifOperation, + trueStatement, falseStatement, + trueReturn?.ReturnedValue ?? trueStatement, + falseReturn?.ReturnedValue ?? falseStatement, + isRef, + fallbackOptions, + cancellationToken).ConfigureAwait(false); - var generatorInternal = document.GetRequiredLanguageService(); - var returnStatement = anyReturn.Kind == OperationKind.YieldReturn - ? (TStatementSyntax)generatorInternal.YieldReturnStatement(conditionalExpression) - : (TStatementSyntax)editor.Generator.ReturnStatement(conditionalExpression); + var generatorInternal = document.GetRequiredLanguageService(); + var returnStatement = anyReturn.Kind == OperationKind.YieldReturn + ? (TStatementSyntax)generatorInternal.YieldReturnStatement(conditionalExpression) + : (TStatementSyntax)editor.Generator.ReturnStatement(conditionalExpression); - returnStatement = returnStatement.WithTriviaFrom(ifStatement); + returnStatement = returnStatement.WithTriviaFrom(ifStatement); - editor.ReplaceNode( - ifStatement, - WrapWithBlockIfAppropriate(ifStatement, returnStatement)); + editor.ReplaceNode( + ifStatement, + WrapWithBlockIfAppropriate(ifStatement, returnStatement)); - // if the if-statement had no 'else' clause, then we were using the following statement - // as the 'false' statement. If so, remove it explicitly. - if (ifOperation.WhenFalse == null) - { - editor.RemoveNode(falseStatement.Syntax, GetRemoveOptions(syntaxFacts, falseStatement.Syntax)); - } + // if the if-statement had no 'else' clause, then we were using the following statement + // as the 'false' statement. If so, remove it explicitly. + if (ifOperation.WhenFalse == null) + { + editor.RemoveNode(falseStatement.Syntax, GetRemoveOptions(syntaxFacts, falseStatement.Syntax)); } } } diff --git a/src/Analyzers/Core/CodeFixes/UseConditionalExpression/UseConditionalExpressionHelpers.cs b/src/Analyzers/Core/CodeFixes/UseConditionalExpression/UseConditionalExpressionHelpers.cs index 1f3b251bb5345..5d0c5e89b87fa 100644 --- a/src/Analyzers/Core/CodeFixes/UseConditionalExpression/UseConditionalExpressionHelpers.cs +++ b/src/Analyzers/Core/CodeFixes/UseConditionalExpression/UseConditionalExpressionHelpers.cs @@ -6,27 +6,26 @@ using Microsoft.CodeAnalysis.LanguageService; using static Microsoft.CodeAnalysis.UseConditionalExpression.UseConditionalExpressionHelpers; -namespace Microsoft.CodeAnalysis.UseConditionalExpression +namespace Microsoft.CodeAnalysis.UseConditionalExpression; + +internal static class UseConditionalExpressionCodeFixHelpers { - internal static class UseConditionalExpressionCodeFixHelpers - { - public static readonly SyntaxAnnotation SpecializedFormattingAnnotation = new(); + public static readonly SyntaxAnnotation SpecializedFormattingAnnotation = new(); - public static SyntaxRemoveOptions GetRemoveOptions( - ISyntaxFactsService syntaxFacts, SyntaxNode syntax) + public static SyntaxRemoveOptions GetRemoveOptions( + ISyntaxFactsService syntaxFacts, SyntaxNode syntax) + { + var removeOptions = SyntaxGenerator.DefaultRemoveOptions; + if (HasRegularCommentTrivia(syntaxFacts, syntax.GetLeadingTrivia())) { - var removeOptions = SyntaxGenerator.DefaultRemoveOptions; - if (HasRegularCommentTrivia(syntaxFacts, syntax.GetLeadingTrivia())) - { - removeOptions |= SyntaxRemoveOptions.KeepLeadingTrivia; - } - - if (HasRegularCommentTrivia(syntaxFacts, syntax.GetTrailingTrivia())) - { - removeOptions |= SyntaxRemoveOptions.KeepTrailingTrivia; - } + removeOptions |= SyntaxRemoveOptions.KeepLeadingTrivia; + } - return removeOptions; + if (HasRegularCommentTrivia(syntaxFacts, syntax.GetTrailingTrivia())) + { + removeOptions |= SyntaxRemoveOptions.KeepTrailingTrivia; } + + return removeOptions; } } diff --git a/src/Analyzers/Core/CodeFixes/UseInferredMemberName/AbstractUseInferredMemberNameCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UseInferredMemberName/AbstractUseInferredMemberNameCodeFixProvider.cs index 976cc4327c50f..c97cc9c07be29 100644 --- a/src/Analyzers/Core/CodeFixes/UseInferredMemberName/AbstractUseInferredMemberNameCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UseInferredMemberName/AbstractUseInferredMemberNameCodeFixProvider.cs @@ -13,34 +13,33 @@ using Microsoft.CodeAnalysis.Editing; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.UseInferredMemberName +namespace Microsoft.CodeAnalysis.UseInferredMemberName; + +internal abstract class AbstractUseInferredMemberNameCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - internal abstract class AbstractUseInferredMemberNameCodeFixProvider : SyntaxEditorBasedCodeFixProvider + protected abstract void LanguageSpecificRemoveSuggestedNode(SyntaxEditor editor, SyntaxNode node); + + public override ImmutableArray FixableDiagnosticIds { get; } + = [IDEDiagnosticIds.UseInferredMemberNameDiagnosticId]; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) { - protected abstract void LanguageSpecificRemoveSuggestedNode(SyntaxEditor editor, SyntaxNode node); + RegisterCodeFix(context, AnalyzersResources.Use_inferred_member_name, nameof(AnalyzersResources.Use_inferred_member_name)); + return Task.CompletedTask; + } - public override ImmutableArray FixableDiagnosticIds { get; } - = [IDEDiagnosticIds.UseInferredMemberNameDiagnosticId]; + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var root = editor.OriginalRoot; - public override Task RegisterCodeFixesAsync(CodeFixContext context) + foreach (var diagnostic in diagnostics) { - RegisterCodeFix(context, AnalyzersResources.Use_inferred_member_name, nameof(AnalyzersResources.Use_inferred_member_name)); - return Task.CompletedTask; + var node = root.FindNode(diagnostic.Location.SourceSpan); + LanguageSpecificRemoveSuggestedNode(editor, node); } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var root = editor.OriginalRoot; - - foreach (var diagnostic in diagnostics) - { - var node = root.FindNode(diagnostic.Location.SourceSpan); - LanguageSpecificRemoveSuggestedNode(editor, node); - } - - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/src/Analyzers/Core/CodeFixes/UseIsNullCheck/AbstractUseIsNullForReferenceEqualsCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UseIsNullCheck/AbstractUseIsNullForReferenceEqualsCodeFixProvider.cs index c2bcbbc97b927..c2e2c9dfc0cd1 100644 --- a/src/Analyzers/Core/CodeFixes/UseIsNullCheck/AbstractUseIsNullForReferenceEqualsCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UseIsNullCheck/AbstractUseIsNullForReferenceEqualsCodeFixProvider.cs @@ -15,72 +15,71 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.UseIsNullCheck +namespace Microsoft.CodeAnalysis.UseIsNullCheck; + +internal abstract class AbstractUseIsNullCheckForReferenceEqualsCodeFixProvider + : SyntaxEditorBasedCodeFixProvider + where TExpressionSyntax : SyntaxNode { - internal abstract class AbstractUseIsNullCheckForReferenceEqualsCodeFixProvider - : SyntaxEditorBasedCodeFixProvider - where TExpressionSyntax : SyntaxNode - { - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseIsNullCheckDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseIsNullCheckDiagnosticId]; - protected abstract string GetTitle(bool negated, ParseOptions options); + protected abstract string GetTitle(bool negated, ParseOptions options); - protected abstract SyntaxNode CreateNullCheck(TExpressionSyntax argument, bool isUnconstrainedGeneric); - protected abstract SyntaxNode CreateNotNullCheck(TExpressionSyntax argument); + protected abstract SyntaxNode CreateNullCheck(TExpressionSyntax argument, bool isUnconstrainedGeneric); + protected abstract SyntaxNode CreateNotNullCheck(TExpressionSyntax argument); - private static bool IsSupportedDiagnostic(Diagnostic diagnostic) - => diagnostic.Properties[UseIsNullConstants.Kind] == UseIsNullConstants.ReferenceEqualsKey; + private static bool IsSupportedDiagnostic(Diagnostic diagnostic) + => diagnostic.Properties[UseIsNullConstants.Kind] == UseIsNullConstants.ReferenceEqualsKey; - public override Task RegisterCodeFixesAsync(CodeFixContext context) + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); + if (IsSupportedDiagnostic(diagnostic)) { - var diagnostic = context.Diagnostics.First(); - if (IsSupportedDiagnostic(diagnostic)) - { - var negated = diagnostic.Properties.ContainsKey(UseIsNullConstants.Negated); - var title = GetTitle(negated, diagnostic.Location.SourceTree!.Options); - context.RegisterCodeFix( - CodeAction.Create(title, GetDocumentUpdater(context), title), - context.Diagnostics); - } - - return Task.CompletedTask; + var negated = diagnostic.Properties.ContainsKey(UseIsNullConstants.Negated); + var title = GetTitle(negated, diagnostic.Location.SourceTree!.Options); + context.RegisterCodeFix( + CodeAction.Create(title, GetDocumentUpdater(context), title), + context.Diagnostics); } - protected override Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var syntaxFacts = document.GetRequiredLanguageService(); + return Task.CompletedTask; + } - // Order in reverse so we process inner diagnostics before outer diagnostics. - // Otherwise, we won't be able to find the nodes we want to replace if they're - // not there once their parent has been replaced. - foreach (var diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start)) - { - if (!IsSupportedDiagnostic(diagnostic)) - continue; + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); - var invocation = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken: cancellationToken); - var negate = diagnostic.Properties.ContainsKey(UseIsNullConstants.Negated); - var isUnconstrainedGeneric = diagnostic.Properties.ContainsKey(UseIsNullConstants.UnconstrainedGeneric); + // Order in reverse so we process inner diagnostics before outer diagnostics. + // Otherwise, we won't be able to find the nodes we want to replace if they're + // not there once their parent has been replaced. + foreach (var diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start)) + { + if (!IsSupportedDiagnostic(diagnostic)) + continue; - var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(invocation); - var argument = syntaxFacts.IsNullLiteralExpression(syntaxFacts.GetExpressionOfArgument(arguments[0])) - ? (TExpressionSyntax)syntaxFacts.GetExpressionOfArgument(arguments[1]) - : (TExpressionSyntax)syntaxFacts.GetExpressionOfArgument(arguments[0]); + var invocation = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken: cancellationToken); + var negate = diagnostic.Properties.ContainsKey(UseIsNullConstants.Negated); + var isUnconstrainedGeneric = diagnostic.Properties.ContainsKey(UseIsNullConstants.UnconstrainedGeneric); - var toReplace = negate ? invocation.GetRequiredParent() : invocation; - var replacement = negate - ? CreateNotNullCheck(argument) - : CreateNullCheck(argument, isUnconstrainedGeneric); + var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(invocation); + var argument = syntaxFacts.IsNullLiteralExpression(syntaxFacts.GetExpressionOfArgument(arguments[0])) + ? (TExpressionSyntax)syntaxFacts.GetExpressionOfArgument(arguments[1]) + : (TExpressionSyntax)syntaxFacts.GetExpressionOfArgument(arguments[0]); - editor.ReplaceNode( - toReplace, - replacement.WithTriviaFrom(toReplace)); - } + var toReplace = negate ? invocation.GetRequiredParent() : invocation; + var replacement = negate + ? CreateNotNullCheck(argument) + : CreateNullCheck(argument, isUnconstrainedGeneric); - return Task.CompletedTask; + editor.ReplaceNode( + toReplace, + replacement.WithTriviaFrom(toReplace)); } + + return Task.CompletedTask; } } diff --git a/src/Analyzers/Core/CodeFixes/UseNullPropagation/AbstractUseNullPropagationCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UseNullPropagation/AbstractUseNullPropagationCodeFixProvider.cs index de6ca1ca0af18..d4c1209b43121 100644 --- a/src/Analyzers/Core/CodeFixes/UseNullPropagation/AbstractUseNullPropagationCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UseNullPropagation/AbstractUseNullPropagationCodeFixProvider.cs @@ -18,251 +18,250 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.UseNullPropagation +namespace Microsoft.CodeAnalysis.UseNullPropagation; + +internal abstract class AbstractUseNullPropagationCodeFixProvider< + TSyntaxKind, + TExpressionSyntax, + TStatementSyntax, + TConditionalExpressionSyntax, + TBinaryExpressionSyntax, + TInvocationExpressionSyntax, + TConditionalAccessExpressionSyntax, + TElementAccessExpressionSyntax, + TMemberAccessExpressionSyntax, + TElementBindingExpressionSyntax, + TIfStatementSyntax, + TExpressionStatementSyntax, + TElementBindingArgumentListSyntax> : SyntaxEditorBasedCodeFixProvider + where TSyntaxKind : struct + where TExpressionSyntax : SyntaxNode + where TStatementSyntax : SyntaxNode + where TConditionalExpressionSyntax : TExpressionSyntax + where TBinaryExpressionSyntax : TExpressionSyntax + where TInvocationExpressionSyntax : TExpressionSyntax + where TConditionalAccessExpressionSyntax : TExpressionSyntax + where TElementAccessExpressionSyntax : TExpressionSyntax + where TMemberAccessExpressionSyntax : TExpressionSyntax + where TElementBindingExpressionSyntax : TExpressionSyntax + where TIfStatementSyntax : TStatementSyntax + where TExpressionStatementSyntax : TStatementSyntax + where TElementBindingArgumentListSyntax : SyntaxNode { - internal abstract class AbstractUseNullPropagationCodeFixProvider< - TSyntaxKind, - TExpressionSyntax, - TStatementSyntax, - TConditionalExpressionSyntax, - TBinaryExpressionSyntax, - TInvocationExpressionSyntax, - TConditionalAccessExpressionSyntax, - TElementAccessExpressionSyntax, - TMemberAccessExpressionSyntax, - TElementBindingExpressionSyntax, - TIfStatementSyntax, - TExpressionStatementSyntax, - TElementBindingArgumentListSyntax> : SyntaxEditorBasedCodeFixProvider - where TSyntaxKind : struct - where TExpressionSyntax : SyntaxNode - where TStatementSyntax : SyntaxNode - where TConditionalExpressionSyntax : TExpressionSyntax - where TBinaryExpressionSyntax : TExpressionSyntax - where TInvocationExpressionSyntax : TExpressionSyntax - where TConditionalAccessExpressionSyntax : TExpressionSyntax - where TElementAccessExpressionSyntax : TExpressionSyntax - where TMemberAccessExpressionSyntax : TExpressionSyntax - where TElementBindingExpressionSyntax : TExpressionSyntax - where TIfStatementSyntax : TStatementSyntax - where TExpressionStatementSyntax : TStatementSyntax - where TElementBindingArgumentListSyntax : SyntaxNode - { - protected abstract bool TryGetBlock(SyntaxNode? node, [NotNullWhen(true)] out TStatementSyntax? block); - protected abstract TStatementSyntax ReplaceBlockStatements(TStatementSyntax block, TStatementSyntax newInnerStatement); - protected abstract SyntaxNode PostProcessElseIf(TIfStatementSyntax ifStatement, TStatementSyntax newWhenTrueStatement); - protected abstract TElementBindingExpressionSyntax ElementBindingExpression(TElementBindingArgumentListSyntax argumentList); + protected abstract bool TryGetBlock(SyntaxNode? node, [NotNullWhen(true)] out TStatementSyntax? block); + protected abstract TStatementSyntax ReplaceBlockStatements(TStatementSyntax block, TStatementSyntax newInnerStatement); + protected abstract SyntaxNode PostProcessElseIf(TIfStatementSyntax ifStatement, TStatementSyntax newWhenTrueStatement); + protected abstract TElementBindingExpressionSyntax ElementBindingExpression(TElementBindingArgumentListSyntax argumentList); - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseNullPropagationDiagnosticId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseNullPropagationDiagnosticId]; - protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) - => !diagnostic.Descriptor.ImmutableCustomTags().Contains(WellKnownDiagnosticTags.Unnecessary); + protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) + => !diagnostic.Descriptor.ImmutableCustomTags().Contains(WellKnownDiagnosticTags.Unnecessary); - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, AnalyzersResources.Use_null_propagation, nameof(AnalyzersResources.Use_null_propagation)); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, AnalyzersResources.Use_null_propagation, nameof(AnalyzersResources.Use_null_propagation)); + return Task.CompletedTask; + } + + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var root = editor.OriginalRoot; - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + foreach (var diagnostic in diagnostics) { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var root = editor.OriginalRoot; + var conditionalExpressionOrIfStatement = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan, getInnermostNodeForTie: true); + if (conditionalExpressionOrIfStatement is TIfStatementSyntax ifStatement) + { + FixIfStatement(document, editor, diagnostic, ifStatement); + } + else + { + FixConditionalExpression(document, editor, semanticModel, diagnostic, conditionalExpressionOrIfStatement, cancellationToken); + } + } + } - foreach (var diagnostic in diagnostics) + private void FixConditionalExpression( + Document document, + SyntaxEditor editor, + SemanticModel semanticModel, + Diagnostic diagnostic, + SyntaxNode conditionalExpression, + CancellationToken cancellationToken) + { + var root = editor.OriginalRoot; + + var syntaxFacts = document.GetRequiredLanguageService(); + var generator = document.GetRequiredLanguageService(); + + var conditionalPart = root.FindNode(diagnostic.AdditionalLocations[1].SourceSpan, getInnermostNodeForTie: true); + var whenPart = root.FindNode(diagnostic.AdditionalLocations[2].SourceSpan, getInnermostNodeForTie: true); + syntaxFacts.GetPartsOfConditionalExpression( + conditionalExpression, out var condition, out var whenTrue, out var whenFalse); + whenTrue = syntaxFacts.WalkDownParentheses(whenTrue); + whenFalse = syntaxFacts.WalkDownParentheses(whenFalse); + + var whenPartIsNullable = diagnostic.Properties.ContainsKey(UseNullPropagationConstants.WhenPartIsNullable); + editor.ReplaceNode( + conditionalExpression, + (conditionalExpression, _) => { - var conditionalExpressionOrIfStatement = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan, getInnermostNodeForTie: true); - if (conditionalExpressionOrIfStatement is TIfStatementSyntax ifStatement) - { - FixIfStatement(document, editor, diagnostic, ifStatement); - } - else + syntaxFacts.GetPartsOfConditionalExpression( + conditionalExpression, out var currentCondition, out var currentWhenTrue, out var currentWhenFalse); + + var currentWhenPartToCheck = whenPart == whenTrue ? currentWhenTrue : currentWhenFalse; + + var match = AbstractUseNullPropagationDiagnosticAnalyzer< + TSyntaxKind, TExpressionSyntax, TStatementSyntax, + TConditionalExpressionSyntax, TBinaryExpressionSyntax, TInvocationExpressionSyntax, + TConditionalAccessExpressionSyntax, TElementAccessExpressionSyntax, TMemberAccessExpressionSyntax, + TIfStatementSyntax, TExpressionStatementSyntax>.GetWhenPartMatch( + syntaxFacts, semanticModel, (TExpressionSyntax)conditionalPart, (TExpressionSyntax)currentWhenPartToCheck, cancellationToken); + if (match == null) { - FixConditionalExpression(document, editor, semanticModel, diagnostic, conditionalExpressionOrIfStatement, cancellationToken); + return conditionalExpression; } - } - } - private void FixConditionalExpression( - Document document, - SyntaxEditor editor, - SemanticModel semanticModel, - Diagnostic diagnostic, - SyntaxNode conditionalExpression, - CancellationToken cancellationToken) + var newNode = CreateConditionalAccessExpression( + syntaxFacts, generator, whenPartIsNullable, currentWhenPartToCheck, match) ?? conditionalExpression; + + newNode = newNode.WithTriviaFrom(conditionalExpression); + return newNode; + }); + } + + private void FixIfStatement( + Document document, + SyntaxEditor editor, + Diagnostic diagnostic, + TIfStatementSyntax ifStatement) + { + var root = editor.OriginalRoot; + + var syntaxFacts = document.GetRequiredLanguageService(); + var generator = document.GetRequiredLanguageService(); + + var whenTrueStatement = (TStatementSyntax)root.FindNode(diagnostic.AdditionalLocations[1].SourceSpan); + var match = (TExpressionSyntax)root.FindNode(diagnostic.AdditionalLocations[2].SourceSpan, getInnermostNodeForTie: true); + + var whenPartIsNullable = diagnostic.Properties.ContainsKey(UseNullPropagationConstants.WhenPartIsNullable); + + SyntaxNode nodeToBeReplaced = ifStatement; + SyntaxNode? replacementNode = null; + + // we have `if (x != null) x.Y();`. Update `x.Y()` to be `x?.Y()`, then replace the entire + // if-statement with that expression statement. + var newWhenTrueStatement = CreateConditionalAccessExpression( + syntaxFacts, generator, whenPartIsNullable, whenTrueStatement, match); + Contract.ThrowIfNull(newWhenTrueStatement); + + var isElseIf = syntaxFacts.IsElseClause(ifStatement.Parent); + + // If we have code like: + // ... + // else if (v != null) + // { + // v.M(); + // } + // then we want to keep the result statement in a block: + // else + // { + // v?.M(); + // } + // Applies only to C# since VB doesn't have a general-purpose block syntax + if (isElseIf && + TryGetBlock(whenTrueStatement.Parent, out var block)) { - var root = editor.OriginalRoot; - - var syntaxFacts = document.GetRequiredLanguageService(); - var generator = document.GetRequiredLanguageService(); - - var conditionalPart = root.FindNode(diagnostic.AdditionalLocations[1].SourceSpan, getInnermostNodeForTie: true); - var whenPart = root.FindNode(diagnostic.AdditionalLocations[2].SourceSpan, getInnermostNodeForTie: true); - syntaxFacts.GetPartsOfConditionalExpression( - conditionalExpression, out var condition, out var whenTrue, out var whenFalse); - whenTrue = syntaxFacts.WalkDownParentheses(whenTrue); - whenFalse = syntaxFacts.WalkDownParentheses(whenFalse); - - var whenPartIsNullable = diagnostic.Properties.ContainsKey(UseNullPropagationConstants.WhenPartIsNullable); - editor.ReplaceNode( - conditionalExpression, - (conditionalExpression, _) => - { - syntaxFacts.GetPartsOfConditionalExpression( - conditionalExpression, out var currentCondition, out var currentWhenTrue, out var currentWhenFalse); - - var currentWhenPartToCheck = whenPart == whenTrue ? currentWhenTrue : currentWhenFalse; - - var match = AbstractUseNullPropagationDiagnosticAnalyzer< - TSyntaxKind, TExpressionSyntax, TStatementSyntax, - TConditionalExpressionSyntax, TBinaryExpressionSyntax, TInvocationExpressionSyntax, - TConditionalAccessExpressionSyntax, TElementAccessExpressionSyntax, TMemberAccessExpressionSyntax, - TIfStatementSyntax, TExpressionStatementSyntax>.GetWhenPartMatch( - syntaxFacts, semanticModel, (TExpressionSyntax)conditionalPart, (TExpressionSyntax)currentWhenPartToCheck, cancellationToken); - if (match == null) - { - return conditionalExpression; - } - - var newNode = CreateConditionalAccessExpression( - syntaxFacts, generator, whenPartIsNullable, currentWhenPartToCheck, match) ?? conditionalExpression; - - newNode = newNode.WithTriviaFrom(conditionalExpression); - return newNode; - }); + newWhenTrueStatement = ReplaceBlockStatements(block, newWhenTrueStatement); } - private void FixIfStatement( - Document document, - SyntaxEditor editor, - Diagnostic diagnostic, - TIfStatementSyntax ifStatement) + // If there's leading trivia on the original inner statement, then combine that with the leading + // trivia on the if-statement. We'll need to add a formatting annotation so that the leading comments + // are put in the right location. + if (newWhenTrueStatement.GetLeadingTrivia().Any(syntaxFacts.IsRegularComment)) { - var root = editor.OriginalRoot; - - var syntaxFacts = document.GetRequiredLanguageService(); - var generator = document.GetRequiredLanguageService(); - - var whenTrueStatement = (TStatementSyntax)root.FindNode(diagnostic.AdditionalLocations[1].SourceSpan); - var match = (TExpressionSyntax)root.FindNode(diagnostic.AdditionalLocations[2].SourceSpan, getInnermostNodeForTie: true); - - var whenPartIsNullable = diagnostic.Properties.ContainsKey(UseNullPropagationConstants.WhenPartIsNullable); - - SyntaxNode nodeToBeReplaced = ifStatement; - SyntaxNode? replacementNode = null; - - // we have `if (x != null) x.Y();`. Update `x.Y()` to be `x?.Y()`, then replace the entire - // if-statement with that expression statement. - var newWhenTrueStatement = CreateConditionalAccessExpression( - syntaxFacts, generator, whenPartIsNullable, whenTrueStatement, match); - Contract.ThrowIfNull(newWhenTrueStatement); - - var isElseIf = syntaxFacts.IsElseClause(ifStatement.Parent); - - // If we have code like: - // ... - // else if (v != null) - // { - // v.M(); - // } - // then we want to keep the result statement in a block: - // else - // { - // v?.M(); - // } - // Applies only to C# since VB doesn't have a general-purpose block syntax - if (isElseIf && - TryGetBlock(whenTrueStatement.Parent, out var block)) - { - newWhenTrueStatement = ReplaceBlockStatements(block, newWhenTrueStatement); - } - - // If there's leading trivia on the original inner statement, then combine that with the leading - // trivia on the if-statement. We'll need to add a formatting annotation so that the leading comments - // are put in the right location. - if (newWhenTrueStatement.GetLeadingTrivia().Any(syntaxFacts.IsRegularComment)) + newWhenTrueStatement = newWhenTrueStatement + .WithPrependedLeadingTrivia(ifStatement.GetLeadingTrivia()) + .WithAdditionalAnnotations(Formatter.Annotation); + } + else + { + if (isElseIf) { - newWhenTrueStatement = newWhenTrueStatement - .WithPrependedLeadingTrivia(ifStatement.GetLeadingTrivia()) - .WithAdditionalAnnotations(Formatter.Annotation); + nodeToBeReplaced = ifStatement.Parent!; + replacementNode = PostProcessElseIf(ifStatement, newWhenTrueStatement); } else { - if (isElseIf) - { - nodeToBeReplaced = ifStatement.Parent!; - replacementNode = PostProcessElseIf(ifStatement, newWhenTrueStatement); - } - else - { - newWhenTrueStatement = newWhenTrueStatement.WithLeadingTrivia(ifStatement.GetLeadingTrivia()); - } + newWhenTrueStatement = newWhenTrueStatement.WithLeadingTrivia(ifStatement.GetLeadingTrivia()); } + } - // If there's trailing comments on the original inner statement, then preserve that. Otherwise, - // replace it with the trailing trivia of hte original if-statement. - if (!newWhenTrueStatement.GetTrailingTrivia().Any(syntaxFacts.IsRegularComment) && !isElseIf) - newWhenTrueStatement = newWhenTrueStatement.WithTrailingTrivia(ifStatement.GetTrailingTrivia()); + // If there's trailing comments on the original inner statement, then preserve that. Otherwise, + // replace it with the trailing trivia of hte original if-statement. + if (!newWhenTrueStatement.GetTrailingTrivia().Any(syntaxFacts.IsRegularComment) && !isElseIf) + newWhenTrueStatement = newWhenTrueStatement.WithTrailingTrivia(ifStatement.GetTrailingTrivia()); - editor.ReplaceNode(nodeToBeReplaced, replacementNode ?? newWhenTrueStatement); - } + editor.ReplaceNode(nodeToBeReplaced, replacementNode ?? newWhenTrueStatement); + } - private TContainer? CreateConditionalAccessExpression( - ISyntaxFactsService syntaxFacts, SyntaxGeneratorInternal generator, bool whenPartIsNullable, - TContainer container, SyntaxNode match) where TContainer : SyntaxNode + private TContainer? CreateConditionalAccessExpression( + ISyntaxFactsService syntaxFacts, SyntaxGeneratorInternal generator, bool whenPartIsNullable, + TContainer container, SyntaxNode match) where TContainer : SyntaxNode + { + if (whenPartIsNullable) { - if (whenPartIsNullable) + if (syntaxFacts.IsSimpleMemberAccessExpression(match.Parent)) { - if (syntaxFacts.IsSimpleMemberAccessExpression(match.Parent)) + var memberAccess = match.Parent; + var nameNode = syntaxFacts.GetNameOfMemberAccessExpression(memberAccess); + syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out var name, out var arity); + var comparer = syntaxFacts.StringComparer; + + if (arity == 0 && comparer.Equals(name, nameof(Nullable.Value))) { - var memberAccess = match.Parent; - var nameNode = syntaxFacts.GetNameOfMemberAccessExpression(memberAccess); - syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out var name, out var arity); - var comparer = syntaxFacts.StringComparer; - - if (arity == 0 && comparer.Equals(name, nameof(Nullable.Value))) - { - // They're calling ".Value" off of a nullable. Because we're moving to ?. - // we want to remove the .Value as well. i.e. we should generate: - // - // goo?.Bar() not goo?.Value.Bar(); - return CreateConditionalAccessExpression( - syntaxFacts, generator, container, match, memberAccess.GetRequiredParent()); - } + // They're calling ".Value" off of a nullable. Because we're moving to ?. + // we want to remove the .Value as well. i.e. we should generate: + // + // goo?.Bar() not goo?.Value.Bar(); + return CreateConditionalAccessExpression( + syntaxFacts, generator, container, match, memberAccess.GetRequiredParent()); } } - - return CreateConditionalAccessExpression( - syntaxFacts, generator, container, match, match.GetRequiredParent()); } - private TContainer? CreateConditionalAccessExpression( - ISyntaxFactsService syntaxFacts, SyntaxGeneratorInternal generator, - TContainer whenPart, SyntaxNode match, SyntaxNode matchParent) where TContainer : SyntaxNode - { - if (syntaxFacts.IsSimpleMemberAccessExpression(matchParent)) - { - var memberAccess = matchParent; - return whenPart.ReplaceNode(memberAccess, - generator.ConditionalAccessExpression( - match, - generator.MemberBindingExpression( - syntaxFacts.GetNameOfMemberAccessExpression(memberAccess)))); - } + return CreateConditionalAccessExpression( + syntaxFacts, generator, container, match, match.GetRequiredParent()); + } - if (matchParent is TElementAccessExpressionSyntax elementAccess) - { - Debug.Assert(syntaxFacts.IsElementAccessExpression(elementAccess)); - var argumentList = (TElementBindingArgumentListSyntax)syntaxFacts.GetArgumentListOfElementAccessExpression(elementAccess)!; - return whenPart.ReplaceNode(elementAccess, - generator.ConditionalAccessExpression( - match, ElementBindingExpression(argumentList))); - } + private TContainer? CreateConditionalAccessExpression( + ISyntaxFactsService syntaxFacts, SyntaxGeneratorInternal generator, + TContainer whenPart, SyntaxNode match, SyntaxNode matchParent) where TContainer : SyntaxNode + { + if (syntaxFacts.IsSimpleMemberAccessExpression(matchParent)) + { + var memberAccess = matchParent; + return whenPart.ReplaceNode(memberAccess, + generator.ConditionalAccessExpression( + match, + generator.MemberBindingExpression( + syntaxFacts.GetNameOfMemberAccessExpression(memberAccess)))); + } - return null; + if (matchParent is TElementAccessExpressionSyntax elementAccess) + { + Debug.Assert(syntaxFacts.IsElementAccessExpression(elementAccess)); + var argumentList = (TElementBindingArgumentListSyntax)syntaxFacts.GetArgumentListOfElementAccessExpression(elementAccess)!; + return whenPart.ReplaceNode(elementAccess, + generator.ConditionalAccessExpression( + match, ElementBindingExpression(argumentList))); } + + return null; } } diff --git a/src/Analyzers/VisualBasic/Analyzers/AddAccessibilityModifiers/VisualBasicAddAccessibilityModifiersDiagnosticAnalyzer.vb b/src/Analyzers/VisualBasic/Analyzers/AddAccessibilityModifiers/VisualBasicAddAccessibilityModifiersDiagnosticAnalyzer.vb index 313528a0c46f4..0781477ba3750 100644 --- a/src/Analyzers/VisualBasic/Analyzers/AddAccessibilityModifiers/VisualBasicAddAccessibilityModifiersDiagnosticAnalyzer.vb +++ b/src/Analyzers/VisualBasic/Analyzers/AddAccessibilityModifiers/VisualBasicAddAccessibilityModifiersDiagnosticAnalyzer.vb @@ -67,6 +67,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.AddAccessibilityModifiers Descriptor, name.GetLocation(), [option].Notification, + context.Options, additionalLocations:=additionalLocations, If(modifiersAdded, ModifiersAddedProperties, Nothing))) End Sub diff --git a/src/Analyzers/VisualBasic/Analyzers/SimplifyObjectCreation/VisualBasicSimplifyObjectCreationDiagnosticAnalyzer.vb b/src/Analyzers/VisualBasic/Analyzers/SimplifyObjectCreation/VisualBasicSimplifyObjectCreationDiagnosticAnalyzer.vb index 1d1814686c485..2c569f920bc1d 100644 --- a/src/Analyzers/VisualBasic/Analyzers/SimplifyObjectCreation/VisualBasicSimplifyObjectCreationDiagnosticAnalyzer.vb +++ b/src/Analyzers/VisualBasic/Analyzers/SimplifyObjectCreation/VisualBasicSimplifyObjectCreationDiagnosticAnalyzer.vb @@ -55,7 +55,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.SimplifyObjectCreation Dim symbolInfo = context.SemanticModel.GetTypeInfo(objectCreation, cancellationToken) If symbolInfo.Type IsNot Nothing AndAlso symbolInfo.Type.Equals(symbolInfo.ConvertedType, SymbolEqualityComparer.Default) Then context.ReportDiagnostic(DiagnosticHelper.Create(Descriptor, variableDeclarator.GetLocation(), styleOption.Notification, - additionalLocations:=Nothing, + context.Options, additionalLocations:=Nothing, properties:=Nothing)) End If End Sub diff --git a/src/Analyzers/VisualBasic/Analyzers/UseInferredMemberName/VisualBasicUseInferredMemberNameDiagnosticAnalyzer.vb b/src/Analyzers/VisualBasic/Analyzers/UseInferredMemberName/VisualBasicUseInferredMemberNameDiagnosticAnalyzer.vb index 1f0a6b34dc20a..ea2a222b81b09 100644 --- a/src/Analyzers/VisualBasic/Analyzers/UseInferredMemberName/VisualBasicUseInferredMemberNameDiagnosticAnalyzer.vb +++ b/src/Analyzers/VisualBasic/Analyzers/UseInferredMemberName/VisualBasicUseInferredMemberNameDiagnosticAnalyzer.vb @@ -57,6 +57,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.UseInferredMemberName Descriptor, nameColonEquals.GetLocation(), preference.Notification, + context.Options, additionalLocations:=ImmutableArray(Of Location).Empty, additionalUnnecessaryLocations:=ImmutableArray.Create(syntaxTree.GetLocation(fadeSpan)))) End Sub @@ -80,6 +81,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.UseInferredMemberName Descriptor, syntaxTree.GetLocation(fadeSpan), preference.Notification, + context.Options, additionalLocations:=ImmutableArray(Of Location).Empty, additionalUnnecessaryLocations:=ImmutableArray.Create(syntaxTree.GetLocation(fadeSpan)))) End Sub diff --git a/src/Analyzers/VisualBasic/Analyzers/UseIsNotExpression/VisualBasicUseIsNotDiagnosticAnalyzer.vb b/src/Analyzers/VisualBasic/Analyzers/UseIsNotExpression/VisualBasicUseIsNotDiagnosticAnalyzer.vb index 1af7cbff46c55..8721b0068d708 100644 --- a/src/Analyzers/VisualBasic/Analyzers/UseIsNotExpression/VisualBasicUseIsNotDiagnosticAnalyzer.vb +++ b/src/Analyzers/VisualBasic/Analyzers/UseIsNotExpression/VisualBasicUseIsNotDiagnosticAnalyzer.vb @@ -72,6 +72,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.UseIsNotExpression Descriptor, isKeyword.GetLocation(), styleOption.Notification, + syntaxContext.Options, ImmutableArray.Create(notExpression.GetLocation()), properties:=Nothing)) End Sub diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs index e7d95a83aae0e..4678033b6d84a 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs @@ -279,6 +279,13 @@ BoundExpression createConversion( if (!hasErrors && conversion.Exists) { ensureAllUnderlyingConversionsChecked(syntax, source, conversion, wasCompilerGenerated, destination, diagnostics); + + if (conversion.Kind is ConversionKind.ImplicitReference or ConversionKind.ExplicitReference && + source.Type is { } sourceType && + sourceType.IsWellKnownTypeLock()) + { + diagnostics.Add(ErrorCode.WRN_ConvertingLock, source.Syntax); + } } return new BoundConversion( diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs index c627a235e3a7d..c8dfa66aacf90 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs @@ -3182,13 +3182,21 @@ private BoundExpression BindOutVariableDeclarationArgument( /// /// Reports an error when a bad special by-ref local was found. /// - internal static void CheckRestrictedTypeInAsyncMethod(Symbol containingSymbol, TypeSymbol type, BindingDiagnosticBag diagnostics, SyntaxNode syntax, bool forUsingExpression = false) + internal static void CheckRestrictedTypeInAsyncMethod(Symbol containingSymbol, TypeSymbol type, BindingDiagnosticBag diagnostics, SyntaxNode syntax, ErrorCode errorCode = ErrorCode.ERR_BadSpecialByRefLocal) { + Debug.Assert(errorCode is ErrorCode.ERR_BadSpecialByRefLocal or ErrorCode.ERR_BadSpecialByRefUsing or ErrorCode.ERR_BadSpecialByRefLock); if (containingSymbol.Kind == SymbolKind.Method && ((MethodSymbol)containingSymbol).IsAsync && type.IsRestrictedType()) { - Error(diagnostics, forUsingExpression ? ErrorCode.ERR_BadSpecialByRefUsing : ErrorCode.ERR_BadSpecialByRefLocal, syntax, type); + if (errorCode == ErrorCode.ERR_BadSpecialByRefLock) + { + Error(diagnostics, errorCode, syntax); + } + else + { + Error(diagnostics, errorCode, syntax, type); + } } } diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs index be162efefa3e5..c1fb6edb5e447 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs @@ -1349,7 +1349,6 @@ internal void BindDefaultArgumentsAndParamsArray( bool expanded, bool enableCallerInfo, BindingDiagnosticBag diagnostics, - bool assertMissingParametersAreOptional = true, Symbol? attributedMember = null) { int firstParamArrayArgument = -1; @@ -1485,7 +1484,7 @@ internal void BindDefaultArgumentsAndParamsArray( { if (!visitedParameters[parameter.Ordinal]) { - Debug.Assert(parameter.IsOptional || !assertMissingParametersAreOptional); + Debug.Assert(parameter.IsOptional); defaultArguments[argumentsBuilder.Count] = true; argumentsBuilder.Add(bindDefaultArgument(node, parameter, containingMember, enableCallerInfo, diagnostics, argumentsBuilder, argumentsCount, argsToParamsOpt)); diff --git a/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs b/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs index 719e192fc5e86..ec4b0a11eb9c0 100644 --- a/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs @@ -1110,11 +1110,7 @@ private EnumeratorResult SatisfiesIEnumerableInterfaces(ExpressionSyntax collect extensionReceiverOpt: null, expanded: false, collectionExpr.Syntax, - diagnostics, - // C# 8 shipped allowing the CancellationToken of `IAsyncEnumerable.GetAsyncEnumerator` to be non-optional, - // filling in a default value in that case. https://github.com/dotnet/roslyn/issues/50182 tracks making - // this an error and breaking the scenario. - assertMissingParametersAreOptional: false); + diagnostics); MethodSymbol currentPropertyGetter; if (isAsync) @@ -1203,7 +1199,8 @@ private void GetDisposalInfoForEnumerator(SyntaxNode syntax, ref ForEachEnumerat if (patternDisposeMethod is object) { Debug.Assert(!patternDisposeMethod.IsExtensionMethod); - Debug.Assert(patternDisposeMethod.ParameterRefKinds.IsDefaultOrEmpty); + Debug.Assert(patternDisposeMethod.ParameterRefKinds.IsDefaultOrEmpty || + patternDisposeMethod.ParameterRefKinds.All(static refKind => refKind is RefKind.None or RefKind.In or RefKind.RefReadOnlyParameter)); var argsBuilder = ArrayBuilder.GetInstance(patternDisposeMethod.ParameterCount); var argsToParams = default(ImmutableArray); @@ -1869,7 +1866,7 @@ private MethodArgumentInfo GetParameterlessSpecialTypeMemberInfo(SpecialMember m } /// If method is an extension method, this must be non-null. - private MethodArgumentInfo BindDefaultArguments(MethodSymbol method, BoundExpression extensionReceiverOpt, bool expanded, SyntaxNode syntax, BindingDiagnosticBag diagnostics, bool assertMissingParametersAreOptional = true) + private MethodArgumentInfo BindDefaultArguments(MethodSymbol method, BoundExpression extensionReceiverOpt, bool expanded, SyntaxNode syntax, BindingDiagnosticBag diagnostics) { Debug.Assert((extensionReceiverOpt != null) == method.IsExtensionMethod); @@ -1896,8 +1893,7 @@ private MethodArgumentInfo BindDefaultArguments(MethodSymbol method, BoundExpres defaultArguments: out BitVector defaultArguments, expanded, enableCallerInfo: true, - diagnostics, - assertMissingParametersAreOptional); + diagnostics); Debug.Assert(argsToParams.IsDefault); return new MethodArgumentInfo(method, argsBuilder.ToImmutableAndFree(), defaultArguments, expanded); diff --git a/src/Compilers/CSharp/Portable/Binder/LockBinder.cs b/src/Compilers/CSharp/Portable/Binder/LockBinder.cs index 7a050ef28f219..602daee16d162 100644 --- a/src/Compilers/CSharp/Portable/Binder/LockBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/LockBinder.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Immutable; using Microsoft.CodeAnalysis.CSharp.Symbols; @@ -13,6 +11,8 @@ namespace Microsoft.CodeAnalysis.CSharp { + using LockTypeInfo = (MethodSymbol EnterScopeMethod, TypeSymbol ScopeType, MethodSymbol ScopeDisposeMethod); + internal sealed class LockBinder : LockOrUsingBinder { private readonly LockStatementSyntax _syntax; @@ -37,11 +37,11 @@ internal override BoundStatement BindLockStatementParts(BindingDiagnosticBag dia // a reference type. ExpressionSyntax exprSyntax = TargetExpressionSyntax; BoundExpression expr = BindTargetExpression(diagnostics, originalBinder); - TypeSymbol exprType = expr.Type; + TypeSymbol? exprType = expr.Type; bool hasErrors = false; - if ((object)exprType == null) + if (exprType is null) { if (expr.ConstantValueOpt != ConstantValue.Null || Compilation.FeatureStrictEnabled) // Dev10 allows the null literal. { @@ -55,9 +55,91 @@ internal override BoundStatement BindLockStatementParts(BindingDiagnosticBag dia hasErrors = true; } + if (exprType?.IsWellKnownTypeLock() == true && + TryFindLockTypeInfo(exprType, diagnostics, exprSyntax) is { } lockTypeInfo) + { + CheckFeatureAvailability(exprSyntax, MessageID.IDS_LockObject, diagnostics); + + // Report use-site errors for members we will use in lowering. + _ = diagnostics.ReportUseSite(lockTypeInfo.EnterScopeMethod, exprSyntax) || + diagnostics.ReportUseSite(lockTypeInfo.ScopeType, exprSyntax) || + diagnostics.ReportUseSite(lockTypeInfo.ScopeDisposeMethod, exprSyntax); + + CheckRestrictedTypeInAsyncMethod( + originalBinder.ContainingMemberOrLambda, + lockTypeInfo.ScopeType, + diagnostics, + exprSyntax, + errorCode: ErrorCode.ERR_BadSpecialByRefLock); + } + BoundStatement stmt = originalBinder.BindPossibleEmbeddedStatement(_syntax.Statement, diagnostics); Debug.Assert(this.Locals.IsDefaultOrEmpty); return new BoundLockStatement(_syntax, expr, stmt, hasErrors); } + + // Keep consistent with ISymbolExtensions.TryFindLockTypeInfo. + internal static LockTypeInfo? TryFindLockTypeInfo(TypeSymbol lockType, BindingDiagnosticBag diagnostics, SyntaxNode syntax) + { + const string LockTypeFullName = $"{nameof(System)}.{nameof(System.Threading)}.{WellKnownMemberNames.LockTypeName}"; + + var enterScopeMethod = TryFindPublicVoidParameterlessMethod(lockType, WellKnownMemberNames.EnterScopeMethodName); + if (enterScopeMethod is not { ReturnsVoid: false, RefKind: RefKind.None }) + { + Error(diagnostics, ErrorCode.ERR_MissingPredefinedMember, syntax, LockTypeFullName, WellKnownMemberNames.EnterScopeMethodName); + return null; + } + + var scopeType = enterScopeMethod.ReturnType; + if (scopeType is not NamedTypeSymbol { Name: WellKnownMemberNames.LockScopeTypeName, Arity: 0, IsValueType: true, IsRefLikeType: true, DeclaredAccessibility: Accessibility.Public } || + !TypeSymbol.Equals(scopeType.ContainingType, lockType, TypeCompareKind.ConsiderEverything)) + { + Error(diagnostics, ErrorCode.ERR_MissingPredefinedMember, syntax, LockTypeFullName, WellKnownMemberNames.EnterScopeMethodName); + return null; + } + + var disposeMethod = TryFindPublicVoidParameterlessMethod(scopeType, WellKnownMemberNames.DisposeMethodName); + if (disposeMethod is not { ReturnsVoid: true }) + { + Error(diagnostics, ErrorCode.ERR_MissingPredefinedMember, syntax, $"{LockTypeFullName}+{WellKnownMemberNames.LockScopeTypeName}", WellKnownMemberNames.DisposeMethodName); + return null; + } + + return new LockTypeInfo + { + EnterScopeMethod = enterScopeMethod, + ScopeType = scopeType, + ScopeDisposeMethod = disposeMethod, + }; + } + + // Keep consistent with ISymbolExtensions.TryFindPublicVoidParameterlessMethod. + private static MethodSymbol? TryFindPublicVoidParameterlessMethod(TypeSymbol type, string name) + { + var members = type.GetMembers(name); + MethodSymbol? result = null; + foreach (var member in members) + { + if (member is MethodSymbol + { + ParameterCount: 0, + Arity: 0, + IsStatic: false, + DeclaredAccessibility: Accessibility.Public, + MethodKind: MethodKind.Ordinary, + } method) + { + if (result is not null) + { + // Ambiguous method found. + return null; + } + + result = method; + } + } + + return result; + } } } diff --git a/src/Compilers/CSharp/Portable/Binder/UsingStatementBinder.cs b/src/Compilers/CSharp/Portable/Binder/UsingStatementBinder.cs index 02b186c91a763..d1b9d601166b8 100644 --- a/src/Compilers/CSharp/Portable/Binder/UsingStatementBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/UsingStatementBinder.cs @@ -119,7 +119,7 @@ internal static BoundStatement BindUsingStatementOrDeclarationFromParts(SyntaxNo Debug.Assert(expressionOpt is not null); if (expressionOpt.Type is not null) { - CheckRestrictedTypeInAsyncMethod(originalBinder.ContainingMemberOrLambda, expressionOpt.Type, diagnostics, expressionOpt.Syntax, forUsingExpression: true); + CheckRestrictedTypeInAsyncMethod(originalBinder.ContainingMemberOrLambda, expressionOpt.Type, diagnostics, expressionOpt.Syntax, errorCode: ErrorCode.ERR_BadSpecialByRefUsing); } } else diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index 1a994db1fab0e..01eb58c1c5527 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -7845,6 +7845,18 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ implicit indexer initializer + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + + Lock object + ref struct interfaces diff --git a/src/Compilers/CSharp/Portable/CommandLine/CSharpCompiler.cs b/src/Compilers/CSharp/Portable/CommandLine/CSharpCompiler.cs index 4b62175029b82..7c7ab46535c77 100644 --- a/src/Compilers/CSharp/Portable/CommandLine/CSharpCompiler.cs +++ b/src/Compilers/CSharp/Portable/CommandLine/CSharpCompiler.cs @@ -373,9 +373,9 @@ protected override void ResolveEmbeddedFilesFromExternalSourceDirectives( } } - private protected override GeneratorDriver CreateGeneratorDriver(ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray additionalTexts) + private protected override GeneratorDriver CreateGeneratorDriver(string baseDirectory, ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray additionalTexts) { - return CSharpGeneratorDriver.Create(generators, additionalTexts, (CSharpParseOptions)parseOptions, analyzerConfigOptionsProvider); + return CSharpGeneratorDriver.Create(generators, additionalTexts, (CSharpParseOptions)parseOptions, analyzerConfigOptionsProvider, driverOptions: new GeneratorDriverOptions() { BaseDirectory = baseDirectory }); } private protected override void DiagnoseBadAccesses(TextWriter consoleOutput, ErrorLogger? errorLogger, Compilation compilation, ImmutableArray diagnostics) diff --git a/src/Compilers/CSharp/Portable/Compilation/AttributeSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/AttributeSemanticModel.cs index 83583bdb01dd3..8d3b41544e5ab 100644 --- a/src/Compilers/CSharp/Portable/Compilation/AttributeSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/AttributeSemanticModel.cs @@ -115,7 +115,7 @@ protected override void AnalyzeBoundNodeNullability(BoundNode boundRoot, Binder NullableWalker.AnalyzeWithoutRewrite(Compilation, symbol: null, boundRoot, binder, diagnostics, createSnapshots); } - protected override bool IsNullableAnalysisEnabled() + protected override bool IsNullableAnalysisEnabledCore() { return IsNullableAnalysisEnabledIn(Compilation, (AttributeSyntax)Root); } diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs index c4bea85b2c002..3bac2a10db5aa 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -154,9 +154,13 @@ internal Conversions Conversions /// private readonly ConcurrentCache _typeToNullableVersion = new ConcurrentCache(size: 100); - /// Lazily caches SyntaxTrees by their mapped path. Used to look up the syntax tree referenced by an interceptor. + /// Lazily caches SyntaxTrees by their mapped path. Used to look up the syntax tree referenced by an interceptor (temporary compat behavior). + /// Must be removed prior to interceptors stable release. private ImmutableSegmentedDictionary> _mappedPathToSyntaxTree; + /// Lazily caches SyntaxTrees by their path. Used to look up the syntax tree referenced by an interceptor. + private ImmutableSegmentedDictionary> _pathToSyntaxTree; + public override string Language { get @@ -1042,6 +1046,9 @@ internal override int GetSyntaxTreeOrdinal(SyntaxTree tree) internal OneOrMany GetSyntaxTreesByMappedPath(string mappedPath) { + // This method supports a "compat" behavior for interceptor file path resolution. + // It must be removed prior to stable release. + // We could consider storing this on SyntaxAndDeclarationManager instead, and updating it incrementally. // However, this would make it more difficult for it to be "pay-for-play", // i.e. only created in compilations where interceptors are used. @@ -1067,6 +1074,32 @@ ImmutableSegmentedDictionary> computeMappedPathToS } } + internal OneOrMany GetSyntaxTreesByPath(string path) + { + // We could consider storing this on SyntaxAndDeclarationManager instead, and updating it incrementally. + // However, this would make it more difficult for it to be "pay-for-play", + // i.e. only created in compilations where interceptors are used. + var pathToSyntaxTree = _pathToSyntaxTree; + if (pathToSyntaxTree.IsDefault) + { + RoslynImmutableInterlocked.InterlockedInitialize(ref _pathToSyntaxTree, computePathToSyntaxTree()); + pathToSyntaxTree = _pathToSyntaxTree; + } + + return pathToSyntaxTree.TryGetValue(path, out var value) ? value : OneOrMany.Empty; + + ImmutableSegmentedDictionary> computePathToSyntaxTree() + { + var builder = ImmutableSegmentedDictionary.CreateBuilder>(); + foreach (var tree in SyntaxTrees) + { + var path = FileUtilities.GetNormalizedPathOrOriginalPath(tree.FilePath, basePath: null); + builder[path] = builder.ContainsKey(path) ? builder[path].Add(tree) : OneOrMany.Create(tree); + } + return builder.ToImmutable(); + } + } + #endregion #region References @@ -2400,10 +2433,16 @@ internal void AddInterception(string filePath, int line, int character, Location #region Binding + public new SemanticModel GetSemanticModel(SyntaxTree syntaxTree, bool ignoreAccessibility) +#pragma warning disable RSEXPERIMENTAL001 // Internal usage of experimental API + => GetSemanticModel(syntaxTree, ignoreAccessibility ? SemanticModelOptions.IgnoreAccessibility : SemanticModelOptions.None); +#pragma warning restore RSEXPERIMENTAL001 + /// /// Gets a new SyntaxTreeSemanticModel for the specified syntax tree. /// - public new SemanticModel GetSemanticModel(SyntaxTree syntaxTree, bool ignoreAccessibility) + [Experimental(RoslynExperiments.NullableDisabledSemanticModel, UrlFormat = RoslynExperiments.NullableDisabledSemanticModel_Url)] + public new SemanticModel GetSemanticModel(SyntaxTree syntaxTree, SemanticModelOptions options) { if (syntaxTree == null) { @@ -2418,15 +2457,17 @@ internal void AddInterception(string filePath, int line, int character, Location SemanticModel? model = null; if (SemanticModelProvider != null) { - model = SemanticModelProvider.GetSemanticModel(syntaxTree, this, ignoreAccessibility); + model = SemanticModelProvider.GetSemanticModel(syntaxTree, this, options); Debug.Assert(model != null); } - return model ?? CreateSemanticModel(syntaxTree, ignoreAccessibility); + return model ?? CreateSemanticModel(syntaxTree, options); } - internal override SemanticModel CreateSemanticModel(SyntaxTree syntaxTree, bool ignoreAccessibility) - => new SyntaxTreeSemanticModel(this, syntaxTree, ignoreAccessibility); +#pragma warning disable RSEXPERIMENTAL001 // Internal usage of experimental API + internal override SemanticModel CreateSemanticModel(SyntaxTree syntaxTree, SemanticModelOptions options) + => new SyntaxTreeSemanticModel(this, syntaxTree, options); +#pragma warning restore RSEXPERIMENTAL001 // When building symbols from the declaration table (lazily), or inside a type, or when // compiling a method body, we may not have a BinderContext in hand for the enclosing @@ -3829,9 +3870,10 @@ protected override CompilationOptions CommonOptions get { return _options; } } - protected override SemanticModel CommonGetSemanticModel(SyntaxTree syntaxTree, bool ignoreAccessibility) + [Experimental(RoslynExperiments.NullableDisabledSemanticModel)] + protected override SemanticModel CommonGetSemanticModel(SyntaxTree syntaxTree, SemanticModelOptions options) { - return this.GetSemanticModel(syntaxTree, ignoreAccessibility); + return this.GetSemanticModel(syntaxTree, options); } protected internal override ImmutableArray CommonSyntaxTrees diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs index eff28b83f199b..638b978c601bc 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs @@ -1678,10 +1678,10 @@ private ImmutableArray LookupSymbolsInternal( lookupResult.Free(); } - ImmutableArray sealedResults = results.ToImmutableAndFree(); - return name == null - ? FilterNotReferencable(sealedResults) - : sealedResults; + if (name == null) + FilterNotReferenceable(results); + + return results.ToImmutableAndFree(); } private void AppendSymbolsWithName(ArrayBuilder results, string name, Binder binder, NamespaceOrTypeSymbol container, LookupOptions options, LookupSymbolsInfo info) @@ -1795,25 +1795,22 @@ private Symbol RemapSymbolIfNecessary(Symbol symbol) /// internal abstract Symbol RemapSymbolIfNecessaryCore(Symbol symbol); - private static ImmutableArray FilterNotReferencable(ImmutableArray sealedResults) + private static void FilterNotReferenceable(ArrayBuilder sealedResults) { - ArrayBuilder builder = null; - int pos = 0; - foreach (var result in sealedResults) + var writeIndex = 0; + for (var i = 0; i < sealedResults.Count; i++) { - if (result.CanBeReferencedByName) - { - builder?.Add(result); - } - else if (builder == null) + var symbol = sealedResults[i]; + if (symbol.CanBeReferencedByName) { - builder = ArrayBuilder.GetInstance(); - builder.AddRange(sealedResults, pos); + if (writeIndex != i) + sealedResults[writeIndex] = symbol; + + writeIndex++; } - pos++; } - return builder?.ToImmutableAndFree() ?? sealedResults; + sealedResults.Count = writeIndex; } /// @@ -4119,10 +4116,13 @@ private ImmutableArray GetIndexerGroupSemanticSymbols(BoundExpr return ImmutableArray.Empty; } - return FilterOverriddenOrHiddenIndexers(symbols.ToImmutableAndFree()); + var result = FilterOverriddenOrHiddenIndexers(symbols); + symbols.Free(); + + return result; } - private static ImmutableArray FilterOverriddenOrHiddenIndexers(ImmutableArray symbols) + private static ImmutableArray FilterOverriddenOrHiddenIndexers(ArrayBuilder symbols) { PooledHashSet hiddenSymbols = null; foreach (ISymbol iSymbol in symbols) diff --git a/src/Compilers/CSharp/Portable/Compilation/InitializerSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/InitializerSemanticModel.cs index c2ceb6136b97b..deaf026210b69 100644 --- a/src/Compilers/CSharp/Portable/Compilation/InitializerSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/InitializerSemanticModel.cs @@ -270,7 +270,7 @@ protected override void AnalyzeBoundNodeNullability(BoundNode boundRoot, Binder #nullable enable - protected override bool IsNullableAnalysisEnabled() + protected override bool IsNullableAnalysisEnabledCore() { switch (MemberSymbol.Kind) { diff --git a/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.SpeculativeMemberSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.SpeculativeMemberSemanticModel.cs index 8d4ed467b8c22..ba2e3b255e7bb 100644 --- a/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.SpeculativeMemberSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.SpeculativeMemberSemanticModel.cs @@ -57,7 +57,7 @@ protected override void AnalyzeBoundNodeNullability(BoundNode boundRoot, Binder NullableWalker.AnalyzeWithoutRewrite(Compilation, MemberSymbol as MethodSymbol, boundRoot, binder, diagnostics, createSnapshots); } - protected override bool IsNullableAnalysisEnabled() + protected override bool IsNullableAnalysisEnabledCore() { return ((SyntaxTreeSemanticModel)_containingPublicSemanticModel.ParentModel).IsNullableAnalysisEnabledAtSpeculativePosition(_containingPublicSemanticModel.OriginalPositionForSpeculation, Root); } diff --git a/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs index 28a318cf63c97..5d59391bf30bb 100644 --- a/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.CSharp.Symbols; @@ -104,6 +105,15 @@ public sealed override bool IgnoresAccessibility } } + [Experimental(RoslynExperiments.NullableDisabledSemanticModel, UrlFormat = RoslynExperiments.NullableDisabledSemanticModel_Url)] + public sealed override bool NullableAnalysisIsDisabled + { + get + { + return _containingPublicSemanticModel.NullableAnalysisIsDisabled; + } + } + public sealed override int OriginalPositionForSpeculation { get @@ -1900,7 +1910,20 @@ private static Binder GetLambdaEnclosingBinder(int position, CSharpSyntaxNode st /// protected void EnsureNullabilityAnalysisPerformedIfNecessary() { +#if !DEBUG + // In release mode, when the semantic model options include DisableNullableAnalysis, + // we want to completely avoid doing any work for nullable analysis. +#pragma warning disable RSEXPERIMENTAL001 // Internal usage of experimental API + if (NullableAnalysisIsDisabled) +#pragma warning restore RSEXPERIMENTAL001 + { + return; + } +#endif + bool isNullableAnalysisEnabled = IsNullableAnalysisEnabled(); + // When 'isNullableAnalysisEnabled' is false but 'Compilation.IsNullableAnalysisEnabledAlways' is true here, + // we still need to perform a nullable analysis whose results are discarded for debug verification purposes. if (!isNullableAnalysisEnabled && !Compilation.IsNullableAnalysisEnabledAlways) { return; @@ -2029,7 +2052,12 @@ protected abstract BoundNode RewriteNullableBoundNodesWithSnapshots( /// protected abstract void AnalyzeBoundNodeNullability(BoundNode boundRoot, Binder binder, DiagnosticBag diagnostics, bool createSnapshots); - protected abstract bool IsNullableAnalysisEnabled(); + protected abstract bool IsNullableAnalysisEnabledCore(); + + protected bool IsNullableAnalysisEnabled() +#pragma warning disable RSEXPERIMENTAL001 // internal use of experimental API + => !NullableAnalysisIsDisabled && IsNullableAnalysisEnabledCore(); +#pragma warning restore RSEXPERIMENTAL001 #nullable disable /// diff --git a/src/Compilers/CSharp/Portable/Compilation/MethodBodySemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/MethodBodySemanticModel.cs index ed57de2bd50d3..04d209c27c4be 100644 --- a/src/Compilers/CSharp/Portable/Compilation/MethodBodySemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/MethodBodySemanticModel.cs @@ -300,7 +300,7 @@ protected override void AnalyzeBoundNodeNullability(BoundNode boundRoot, Binder NullableWalker.AnalyzeWithoutRewrite(Compilation, MemberSymbol, boundRoot, binder, diagnostics, createSnapshots); } - protected override bool IsNullableAnalysisEnabled() + protected override bool IsNullableAnalysisEnabledCore() { return Compilation.IsNullableAnalysisEnabledIn((MethodSymbol)MemberSymbol); } diff --git a/src/Compilers/CSharp/Portable/Compilation/SpeculativeSemanticModelWithMemberModel.cs b/src/Compilers/CSharp/Portable/Compilation/SpeculativeSemanticModelWithMemberModel.cs index 7eb35f163c82a..0e65d7f3dcaea 100644 --- a/src/Compilers/CSharp/Portable/Compilation/SpeculativeSemanticModelWithMemberModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/SpeculativeSemanticModelWithMemberModel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Threading; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -127,6 +128,9 @@ public SpeculativeSemanticModelWithMemberModel( public override SyntaxTree SyntaxTree => _memberModel.SyntaxTree; + [Experimental(RoslynExperiments.NullableDisabledSemanticModel, UrlFormat = RoslynExperiments.NullableDisabledSemanticModel_Url)] + public override bool NullableAnalysisIsDisabled => _parentSemanticModel.NullableAnalysisIsDisabled; + public override bool IgnoresAccessibility => _parentSemanticModel.IgnoresAccessibility; private MemberSemanticModel GetEnclosingMemberModel(int position) diff --git a/src/Compilers/CSharp/Portable/Compilation/SpeculativeSyntaxTreeSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/SpeculativeSyntaxTreeSemanticModel.cs index b421d5132cb8c..6a91f23058a2e 100644 --- a/src/Compilers/CSharp/Portable/Compilation/SpeculativeSyntaxTreeSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/SpeculativeSyntaxTreeSemanticModel.cs @@ -47,7 +47,7 @@ private static SpeculativeSyntaxTreeSemanticModel CreateCore(SyntaxTreeSemanticM } private SpeculativeSyntaxTreeSemanticModel(SyntaxTreeSemanticModel parentSemanticModel, CSharpSyntaxNode root, Binder rootBinder, int position, SpeculativeBindingOption bindingOption) - : base(parentSemanticModel.Compilation, parentSemanticModel.SyntaxTree, root.SyntaxTree, parentSemanticModel.IgnoresAccessibility) + : base(parentSemanticModel.Compilation, parentSemanticModel.SyntaxTree, root.SyntaxTree, parentSemanticModel.Options) { _parentSemanticModel = parentSemanticModel; _root = root; diff --git a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs index 801e274644767..602946b93212e 100644 --- a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#pragma warning disable RSEXPERIMENTAL001 // Internal use of experimental API #nullable disable using System; @@ -35,27 +36,26 @@ internal partial class SyntaxTreeSemanticModel : PublicSemanticModel private readonly BinderFactory _binderFactory; private Func _createMemberModelFunction; - private readonly bool _ignoresAccessibility; + private readonly SemanticModelOptions _options; private ScriptLocalScopeBinder.Labels _globalStatementLabels; private static readonly Func s_isMemberDeclarationFunction = IsMemberDeclaration; -#nullable enable - internal SyntaxTreeSemanticModel(CSharpCompilation compilation, SyntaxTree syntaxTree, bool ignoreAccessibility = false) + internal SyntaxTreeSemanticModel(CSharpCompilation compilation, SyntaxTree syntaxTree, SemanticModelOptions options) { _compilation = compilation; _syntaxTree = syntaxTree; - _ignoresAccessibility = ignoreAccessibility; + _options = options; - _binderFactory = compilation.GetBinderFactory(SyntaxTree, ignoreAccessibility); + _binderFactory = compilation.GetBinderFactory(SyntaxTree, (options & SemanticModelOptions.IgnoreAccessibility) != 0); } - internal SyntaxTreeSemanticModel(CSharpCompilation parentCompilation, SyntaxTree parentSyntaxTree, SyntaxTree speculatedSyntaxTree, bool ignoreAccessibility) + internal SyntaxTreeSemanticModel(CSharpCompilation parentCompilation, SyntaxTree parentSyntaxTree, SyntaxTree speculatedSyntaxTree, SemanticModelOptions options) { _compilation = parentCompilation; _syntaxTree = speculatedSyntaxTree; - _binderFactory = _compilation.GetBinderFactory(parentSyntaxTree, ignoreAccessibility); - _ignoresAccessibility = ignoreAccessibility; + _binderFactory = _compilation.GetBinderFactory(parentSyntaxTree, ignoreAccessibility: (options & SemanticModelOptions.IgnoreAccessibility) != 0); + _options = options; } /// @@ -91,14 +91,18 @@ public override SyntaxTree SyntaxTree } } + internal SemanticModelOptions Options => _options; + /// /// Returns true if this is a SemanticModel that ignores accessibility rules when answering semantic questions. /// public override bool IgnoresAccessibility { - get { return _ignoresAccessibility; } + get { return (_options & SemanticModelOptions.IgnoreAccessibility) != 0; } } + public override bool NullableAnalysisIsDisabled => (_options & SemanticModelOptions.DisableNullableAnalysis) != 0; + private void VerifySpanForGetDiagnostics(TextSpan? span) { if (span.HasValue && !this.Root.FullSpan.Contains(span.Value)) diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index adb4541fcbe87..6d04b271f4206 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -2202,7 +2202,7 @@ internal enum ErrorCode ERR_InterceptorLineOutOfRange = 9142, ERR_InterceptorCharacterOutOfRange = 9143, ERR_InterceptorSignatureMismatch = 9144, - ERR_InterceptorPathNotInCompilationWithUnmappedCandidate = 9145, + // ERR_InterceptorPathNotInCompilationWithUnmappedCandidate = 9145, ERR_InterceptorMethodMustBeOrdinary = 9146, ERR_InterceptorMustReferToStartOfTokenPosition = 9147, ERR_InterceptorMustHaveMatchingThisParameter = 9148, @@ -2292,6 +2292,9 @@ internal enum ErrorCode #endregion + WRN_ConvertingLock = 9216, + ERR_BadSpecialByRefLock = 9217, + // Note: you will need to do the following after adding warnings: // 1) Re-generate compiler code (eng\generate-compiler-code.cmd). } diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index 8b9d4b6898fbe..0f4b0389b72af 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -553,6 +553,7 @@ internal static int GetWarningLevel(ErrorCode code) case ErrorCode.WRN_Experimental: case ErrorCode.WRN_CollectionExpressionRefStructMayAllocate: case ErrorCode.WRN_CollectionExpressionRefStructSpreadMayAllocate: + case ErrorCode.WRN_ConvertingLock: return 1; default: return 0; @@ -2348,7 +2349,6 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_InterceptorContainingTypeCannotBeGeneric: case ErrorCode.ERR_InterceptorPathNotInCompilation: case ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate: - case ErrorCode.ERR_InterceptorPathNotInCompilationWithUnmappedCandidate: case ErrorCode.ERR_InterceptorPositionBadToken: case ErrorCode.ERR_InterceptorLineOutOfRange: case ErrorCode.ERR_InterceptorCharacterOutOfRange: @@ -2413,6 +2413,8 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code) case ErrorCode.ERR_CollectionExpressionTargetNoElementType: case ErrorCode.ERR_CollectionExpressionMissingConstructor: case ErrorCode.ERR_CollectionExpressionMissingAdd: + case ErrorCode.WRN_ConvertingLock: + case ErrorCode.ERR_BadSpecialByRefLock: case ErrorCode.ERR_RuntimeDoesNotSupportByRefLikeGenerics: case ErrorCode.ERR_RefStructConstraintAlreadySpecified: case ErrorCode.ERR_AllowsClauseMustBeLast: diff --git a/src/Compilers/CSharp/Portable/Errors/MessageID.cs b/src/Compilers/CSharp/Portable/Errors/MessageID.cs index 4563a63e1e1f6..9aa42f402c943 100644 --- a/src/Compilers/CSharp/Portable/Errors/MessageID.cs +++ b/src/Compilers/CSharp/Portable/Errors/MessageID.cs @@ -279,6 +279,7 @@ internal enum MessageID IDS_StringEscapeCharacter = MessageBase + 12839, IDS_ImplicitIndexerInitializer = MessageBase + 12840, + IDS_LockObject = MessageBase + 12841, IDS_FeatureRefStructInterfaces = MessageBase + 12950, // PROTOTYPE(RefStructInterfaces): Pack numbers } @@ -463,6 +464,7 @@ internal static LanguageVersion RequiredVersion(this MessageID feature) // C# preview features. case MessageID.IDS_StringEscapeCharacter: case MessageID.IDS_ImplicitIndexerInitializer: + case MessageID.IDS_LockObject: case MessageID.IDS_FeatureRefStructInterfaces: return LanguageVersion.Preview; diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs index 61fb94f56681f..f7a16cc4bbae0 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs @@ -1607,7 +1607,8 @@ public override BoundNode VisitUtf8String(BoundUtf8String node) protected void SplitIfBooleanConstant(BoundExpression node) { - if (node.ConstantValueOpt is { IsBoolean: true, BooleanValue: bool booleanValue }) + if (node.ConstantValueOpt is { IsBoolean: true, BooleanValue: bool booleanValue } + && node.Type.SpecialType == SpecialType.System_Boolean) { var unreachable = UnreachableState(); Split(); diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs index dcd07a1dfdf3e..f0de65b711584 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs @@ -339,6 +339,12 @@ private void SetResult(BoundExpression? expression, TypeWithState resultType, Ty private void SetResult(BoundExpression? expression, VisitResult visitResult, bool updateAnalyzedNullability, bool? isLvalue) { + // As a general rule, the state should only be conditional for expressions of type bool, + // although there are a few exceptions. + Debug.Assert(TypeAllowsConditionalState(visitResult.RValueType.Type) + || !IsConditionalState + || expression is BoundTypeExpression); + _visitResult = visitResult; if (updateAnalyzedNullability) { @@ -3547,6 +3553,20 @@ private BoundNode VisitLValue(BoundNode node) return Visit(node, expressionIsRead: false); } + private static bool TypeAllowsConditionalState(TypeSymbol? type) + { + return type is not null + && (type.SpecialType == SpecialType.System_Boolean || type.IsDynamic() || type.IsErrorType()); + } + + private void UnsplitIfNeeded(TypeSymbol? type) + { + if (!TypeAllowsConditionalState(type)) + { + Unsplit(); + } + } + private BoundNode Visit(BoundNode? node, bool expressionIsRead) { #if DEBUG @@ -4490,36 +4510,25 @@ private TypeSymbol VisitArrayInitialization(TypeSymbol type, BoundArrayInitializ ? elementType.SetUnknownNullabilityForReferenceTypes() : TypeWithAnnotations.Create(bestType); - if (bestType is object) + // Convert elements to best type to determine element top-level nullability and to report nested nullability warnings + for (int i = 0; i < n; i++) { - // Convert elements to best type to determine element top-level nullability and to report nested nullability warnings - for (int i = 0; i < n; i++) - { - var expressionNoConversion = expressionsNoConversions[i]; - var expression = GetConversionIfApplicable(expressions[i], expressionNoConversion); - expressionTypes[i] = VisitConversion(expression, expressionNoConversion, conversions[i], inferredType, expressionTypes[i], checkConversion: true, - fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment, reportRemainingWarnings: true, reportTopLevelWarnings: false); - } + var expressionNoConversion = expressionsNoConversions[i]; + var expression = GetConversionIfApplicable(expressions[i], expressionNoConversion); + expressionTypes[i] = VisitConversion(expression, expressionNoConversion, conversions[i], inferredType, expressionTypes[i], checkConversion: true, + fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment, reportRemainingWarnings: true, reportTopLevelWarnings: false); + Unsplit(); + } - // Set top-level nullability on inferred element type - var elementState = BestTypeInferrer.GetNullableState(expressionTypes); - inferredType = TypeWithState.Create(inferredType.Type, elementState).ToTypeWithAnnotations(compilation); + // Set top-level nullability on inferred element type + var elementState = BestTypeInferrer.GetNullableState(expressionTypes); + inferredType = TypeWithState.Create(inferredType.Type, elementState).ToTypeWithAnnotations(compilation); - for (int i = 0; i < n; i++) - { - // Report top-level warnings - _ = VisitConversion(conversionOpt: null, conversionOperand: expressionsNoConversions[i], Conversion.Identity, targetTypeWithNullability: inferredType, operandType: expressionTypes[i], - checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment, reportRemainingWarnings: false); - } - } - else + for (int i = 0; i < n; i++) { - // We need to ensure that we're tracking the inferred type with nullability of any conversions that - // were stripped off. - for (int i = 0; i < n; i++) - { - TrackAnalyzedNullabilityThroughConversionGroup(inferredType.ToTypeWithState(), expressions[i] as BoundConversion, expressionsNoConversions[i]); - } + // Report top-level warnings + _ = VisitConversion(conversionOpt: null, conversionOperand: expressionsNoConversions[i], Conversion.Identity, targetTypeWithNullability: inferredType, operandType: expressionTypes[i], + checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment, reportRemainingWarnings: false); } expressionsNoConversions.Free(); @@ -5825,6 +5834,8 @@ void makeAndAdjustReceiverSlot(BoundExpression receiver) resultType ??= node.Type?.SetUnknownNullabilityForReferenceTypes(); + UnsplitIfNeeded(resultType); + TypeWithAnnotations resultTypeWithAnnotations; if (resultType is null) @@ -6435,7 +6446,33 @@ private FlowAnalysisAnnotations GetParameterAnnotations(ParameterSymbol paramete { // Annotations are ignored when binding an attribute to avoid cycles. (Members used // in attributes are error scenarios, so missing warnings should not be important.) - return IsAnalyzingAttribute ? FlowAnalysisAnnotations.None : parameter.FlowAnalysisAnnotations; + if (IsAnalyzingAttribute) + return FlowAnalysisAnnotations.None; + + var annotations = parameter.FlowAnalysisAnnotations; + + // Conditional annotations are ignored on parameters of non-boolean members. + if (parameter.ContainingSymbol.GetTypeOrReturnType().Type.SpecialType != SpecialType.System_Boolean) + { + // NotNull = NotNullWhenTrue + NotNullWhenFalse + bool hasNotNullWhenTrue = (annotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNullWhenTrue; + bool hasNotNullWhenFalse = (annotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNullWhenFalse; + if (hasNotNullWhenTrue ^ hasNotNullWhenFalse) + { + annotations &= ~FlowAnalysisAnnotations.NotNull; + } + + // MaybeNull = MaybeNullWhenTrue + MaybeNullWhenFalse + bool hasMaybeNullWhenTrue = (annotations & FlowAnalysisAnnotations.MaybeNull) == FlowAnalysisAnnotations.MaybeNullWhenTrue; + bool hasMaybeNullWhenFalse = (annotations & FlowAnalysisAnnotations.MaybeNull) == FlowAnalysisAnnotations.MaybeNullWhenFalse; + if (hasMaybeNullWhenTrue ^ hasMaybeNullWhenFalse) + { + annotations &= ~FlowAnalysisAnnotations.MaybeNull; + } + + } + + return annotations; } /// @@ -8004,10 +8041,9 @@ bool tryAsMemberOfSingleType(NamedTypeSymbol singleType, [NotNullWhen(true)] out (BoundExpression operand, Conversion conversion) = RemoveConversion(node, includeExplicitConversions: true); SnapshotWalkerThroughConversionGroup(node, operand); - if (targetType.SpecialType == SpecialType.System_Boolean && - (operand.Type?.SpecialType == SpecialType.System_Boolean || operand.Type?.IsErrorType() == true)) + if (TypeAllowsConditionalState(targetType.Type) && TypeAllowsConditionalState(operand.Type)) { - Visit(operand); + Visit(operand); // don't Unsplit } else { diff --git a/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Internal.Generated.cs b/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Internal.Generated.cs index 33160d05e24b2..ed09f4ccd3931 100644 --- a/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Internal.Generated.cs +++ b/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Internal.Generated.cs @@ -15864,6 +15864,7 @@ internal sealed partial class AttributeSyntax : CSharpSyntaxNode internal AttributeSyntax(SyntaxKind kind, NameSyntax name, AttributeArgumentListSyntax? argumentList, DiagnosticInfo[]? diagnostics, SyntaxAnnotation[]? annotations) : base(kind, diagnostics, annotations) { + SetFlags(NodeFlags.ContainsAttributes); this.SlotCount = 2; this.AdjustFlagsAndWidth(name); this.name = name; @@ -15878,6 +15879,7 @@ internal AttributeSyntax(SyntaxKind kind, NameSyntax name, AttributeArgumentList : base(kind) { this.SetFactoryContext(context); + SetFlags(NodeFlags.ContainsAttributes); this.SlotCount = 2; this.AdjustFlagsAndWidth(name); this.name = name; @@ -15891,6 +15893,7 @@ internal AttributeSyntax(SyntaxKind kind, NameSyntax name, AttributeArgumentList internal AttributeSyntax(SyntaxKind kind, NameSyntax name, AttributeArgumentListSyntax? argumentList) : base(kind) { + SetFlags(NodeFlags.ContainsAttributes); this.SlotCount = 2; this.AdjustFlagsAndWidth(name); this.name = name; @@ -24277,13 +24280,13 @@ internal abstract partial class DirectiveTriviaSyntax : StructuredTriviaSyntax internal DirectiveTriviaSyntax(SyntaxKind kind, DiagnosticInfo[]? diagnostics, SyntaxAnnotation[]? annotations) : base(kind, diagnostics, annotations) { - this.flags |= NodeFlags.ContainsDirectives; + SetFlags(NodeFlags.ContainsDirectives); } internal DirectiveTriviaSyntax(SyntaxKind kind) : base(kind) { - this.flags |= NodeFlags.ContainsDirectives; + SetFlags(NodeFlags.ContainsDirectives); } public abstract SyntaxToken HashToken { get; } @@ -30970,17 +30973,7 @@ public AttributeSyntax Attribute(NameSyntax name, AttributeArgumentListSyntax? a if (name == null) throw new ArgumentNullException(nameof(name)); #endif - int hash; - var cached = CSharpSyntaxNodeCache.TryGetNode((int)SyntaxKind.Attribute, name, argumentList, this.context, out hash); - if (cached != null) return (AttributeSyntax)cached; - - var result = new AttributeSyntax(SyntaxKind.Attribute, name, argumentList, this.context); - if (hash >= 0) - { - SyntaxNodeCache.AddNode(result, hash); - } - - return result; + return new AttributeSyntax(SyntaxKind.Attribute, name, argumentList, this.context); } public AttributeArgumentListSyntax AttributeArgumentList(SyntaxToken openParenToken, CoreSyntax.SeparatedSyntaxList arguments, SyntaxToken closeParenToken) @@ -36228,17 +36221,7 @@ public static AttributeSyntax Attribute(NameSyntax name, AttributeArgumentListSy if (name == null) throw new ArgumentNullException(nameof(name)); #endif - int hash; - var cached = SyntaxNodeCache.TryGetNode((int)SyntaxKind.Attribute, name, argumentList, out hash); - if (cached != null) return (AttributeSyntax)cached; - - var result = new AttributeSyntax(SyntaxKind.Attribute, name, argumentList); - if (hash >= 0) - { - SyntaxNodeCache.AddNode(result, hash); - } - - return result; + return new AttributeSyntax(SyntaxKind.Attribute, name, argumentList); } public static AttributeArgumentListSyntax AttributeArgumentList(SyntaxToken openParenToken, CoreSyntax.SeparatedSyntaxList arguments, SyntaxToken closeParenToken) diff --git a/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs b/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs index c8feebec6fef9..b07702f8e708f 100644 --- a/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs +++ b/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs @@ -337,6 +337,7 @@ public static bool IsWarning(ErrorCode code) case ErrorCode.WRN_Experimental: case ErrorCode.WRN_CollectionExpressionRefStructMayAllocate: case ErrorCode.WRN_CollectionExpressionRefStructSpreadMayAllocate: + case ErrorCode.WRN_ConvertingLock: return true; default: return false; diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ForEachStatement.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ForEachStatement.cs index 6432ac28779d7..aaef129e2ce72 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ForEachStatement.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ForEachStatement.cs @@ -179,10 +179,7 @@ private BoundStatement RewriteForEachEnumerator( // ((C)(x)).GetEnumerator(); OR (x).GetEnumerator(); OR async variants (which fill-in arguments for optional parameters) BoundExpression enumeratorVarInitValue = SynthesizeCall(getEnumeratorInfo, forEachSyntax, receiver, - allowExtensionAndOptionalParameters: isAsync || getEnumeratorInfo.Method.IsExtensionMethod, - // C# 8 shipped allowing the CancellationToken of `IAsyncEnumerable.GetAsyncEnumerator` to be non-optional. - // https://github.com/dotnet/roslyn/issues/50182 tracks making this an error and breaking the scenario. - assertParametersAreOptional: false); + allowExtensionAndOptionalParameters: isAsync || getEnumeratorInfo.Method.IsExtensionMethod); // E e = ((C)(x)).GetEnumerator(); BoundStatement enumeratorVarDecl = MakeLocalDeclaration(forEachSyntax, enumeratorVar, enumeratorVarInitValue); @@ -537,12 +534,12 @@ private BoundExpression ConvertReceiverForInvocation(CSharpSyntaxNode syntax, Bo return receiver; } - private BoundExpression SynthesizeCall(MethodArgumentInfo methodArgumentInfo, CSharpSyntaxNode syntax, BoundExpression? receiver, bool allowExtensionAndOptionalParameters, bool assertParametersAreOptional = true) + private BoundExpression SynthesizeCall(MethodArgumentInfo methodArgumentInfo, CSharpSyntaxNode syntax, BoundExpression? receiver, bool allowExtensionAndOptionalParameters) { if (allowExtensionAndOptionalParameters) { // Generate a call with zero explicit arguments, but with implicit arguments for optional and params parameters. - return MakeCallWithNoExplicitArgument(methodArgumentInfo, syntax, receiver, assertParametersAreOptional); + return MakeCallWithNoExplicitArgument(methodArgumentInfo, syntax, receiver); } // Generate a call with literally zero arguments diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_LockStatement.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_LockStatement.cs index a12604751d8b0..eebe254102294 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_LockStatement.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_LockStatement.cs @@ -13,8 +13,11 @@ namespace Microsoft.CodeAnalysis.CSharp internal sealed partial class LocalRewriter { /// - /// Lowers a lock statement to a try-finally block that calls Monitor.Enter and Monitor.Exit - /// before and after the body, respectively. + /// Lowers a lock statement to a try-finally block that calls (before and after the body, respectively): + /// + /// Lock.EnterScope and Lock+Scope.Dispose if the argument is of type Lock, or + /// Monitor.Enter and Monitor.Exit. + /// /// public override BoundNode VisitLockStatement(BoundLockStatement node) { @@ -37,6 +40,45 @@ public override BoundNode VisitLockStatement(BoundLockStatement node) argumentType); //need to have a non-null type here for TempHelpers.StoreToTemp. } + if (argumentType.IsWellKnownTypeLock()) + { + if (LockBinder.TryFindLockTypeInfo(argumentType, _diagnostics, rewrittenArgument.Syntax) is not { } lockTypeInfo) + { + Debug.Fail("We should have reported an error during binding if lock type info cannot be found."); + return node.Update(rewrittenArgument, rewrittenBody).WithHasErrors(); + } + + // lock (x) { body } -> using (x.EnterScope()) { body } + + var tryBlock = rewrittenBody is BoundBlock block ? block : BoundBlock.SynthesizedNoLocals(lockSyntax, rewrittenBody); + + var enterScopeCall = BoundCall.Synthesized( + rewrittenArgument.Syntax, + rewrittenArgument, + initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown, + lockTypeInfo.EnterScopeMethod); + + BoundLocal boundTemp = _factory.StoreToTemp(enterScopeCall, + out BoundAssignmentOperator tempAssignment, + syntaxOpt: rewrittenArgument.Syntax, + kind: SynthesizedLocalKind.Using); + var expressionStatement = new BoundExpressionStatement(rewrittenArgument.Syntax, tempAssignment); + + BoundStatement tryFinally = RewriteUsingStatementTryFinally( + rewrittenArgument.Syntax, + rewrittenArgument.Syntax, + tryBlock, + boundTemp, + awaitKeywordOpt: default, + awaitOpt: null, + patternDisposeInfo: MethodArgumentInfo.CreateParameterlessMethod(lockTypeInfo.ScopeDisposeMethod)); + + return new BoundBlock( + lockSyntax, + locals: ImmutableArray.Create(boundTemp.LocalSymbol), + statements: ImmutableArray.Create(expressionStatement, tryFinally)); + } + if (argumentType.Kind == SymbolKind.TypeParameter) { // If the argument has a type parameter type, then we'll box it right away diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_UsingStatement.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_UsingStatement.cs index d50d9cb5392fe..7d75606d623ba 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_UsingStatement.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_UsingStatement.cs @@ -489,7 +489,7 @@ private BoundExpression GenerateDisposeCall( /// Synthesize a call `expression.Method()`, but with some extra smarts to handle extension methods, and to fill-in optional and params parameters. This call expects that the /// receiver parameter has already been visited. /// - private BoundExpression MakeCallWithNoExplicitArgument(MethodArgumentInfo methodArgumentInfo, SyntaxNode syntax, BoundExpression? expression, bool assertParametersAreOptional = true) + private BoundExpression MakeCallWithNoExplicitArgument(MethodArgumentInfo methodArgumentInfo, SyntaxNode syntax, BoundExpression? expression) { MethodSymbol method = methodArgumentInfo.Method; @@ -497,15 +497,14 @@ private BoundExpression MakeCallWithNoExplicitArgument(MethodArgumentInfo method if (method.IsExtensionMethod) { Debug.Assert(expression == null); - Debug.Assert(method.Parameters.AsSpan()[1..].All(assertParametersAreOptional, (p, assertOptional) => (p.IsOptional || p.IsParams || !assertOptional) && p.RefKind == RefKind.None)); - Debug.Assert(method.ParameterRefKinds.IsDefaultOrEmpty || method.ParameterRefKinds[0] is RefKind.In or RefKind.RefReadOnlyParameter or RefKind.None); + Debug.Assert(method.Parameters.AsSpan()[1..].All(static (p) => (p.IsOptional || p.IsParams) && p.RefKind is RefKind.None or RefKind.In or RefKind.RefReadOnlyParameter)); } else { - Debug.Assert(!assertParametersAreOptional || method.Parameters.All(p => p.IsOptional || p.IsParams)); - Debug.Assert(method.ParameterRefKinds.IsDefaultOrEmpty); + Debug.Assert(method.Parameters.All(p => p.IsOptional || p.IsParams)); } + Debug.Assert(method.ParameterRefKinds.IsDefaultOrEmpty || method.ParameterRefKinds.All(static refKind => refKind is RefKind.In or RefKind.RefReadOnlyParameter or RefKind.None)); Debug.Assert(methodArgumentInfo.Arguments.All(arg => arg is not BoundConversion { ConversionKind: ConversionKind.InterpolatedStringHandler })); #endif diff --git a/src/Compilers/CSharp/Portable/Parser/Lexer.cs b/src/Compilers/CSharp/Portable/Parser/Lexer.cs index 346de7a0573bd..9e230a8483aeb 100644 --- a/src/Compilers/CSharp/Portable/Parser/Lexer.cs +++ b/src/Compilers/CSharp/Portable/Parser/Lexer.cs @@ -119,7 +119,6 @@ public Lexer(SourceText text, CSharpParseOptions options, bool allowPreprocessor _builder = new StringBuilder(); _identBuffer = new char[32]; _cache = new LexerCache(); - _createQuickTokenFunction = this.CreateQuickToken; _allowPreprocessorDirectives = allowPreprocessorDirectives; _interpolationFollowedByColon = interpolationFollowedByColon; } @@ -284,6 +283,7 @@ public SyntaxToken Lex(LexerMode mode) private SyntaxListBuilder _leadingTriviaCache = new SyntaxListBuilder(10); private SyntaxListBuilder _trailingTriviaCache = new SyntaxListBuilder(10); + private SyntaxListBuilder? _directiveTriviaCache; private static int GetFullWidth(SyntaxListBuilder? builder) { @@ -2225,11 +2225,6 @@ private void ScanToEndOfLine() /// A trivia node with the whitespace text private SyntaxTrivia ScanWhitespace() { - if (_createWhitespaceTriviaFunction == null) - { - _createWhitespaceTriviaFunction = this.CreateWhitespaceTrivia; - } - int hashCode = Hash.FnvOffsetBias; // FNV base bool onlySpaces = true; @@ -2278,20 +2273,19 @@ private SyntaxTrivia ScanWhitespace() TextWindow.LexemeRelativeStart, width, hashCode, - _createWhitespaceTriviaFunction); + CreateWhitespaceTrivia, + TextWindow); } else { - return _createWhitespaceTriviaFunction(); + return CreateWhitespaceTrivia(TextWindow); } } } - private Func? _createWhitespaceTriviaFunction; - - private SyntaxTrivia CreateWhitespaceTrivia() + private static SyntaxTrivia CreateWhitespaceTrivia(SlidingTextWindow textWindow) { - return SyntaxFactory.Whitespace(TextWindow.GetText(intern: true)); + return SyntaxFactory.Whitespace(textWindow.GetText(intern: true)); } private void LexDirectiveAndExcludedTrivia( @@ -2423,8 +2417,17 @@ private SyntaxToken LexDirectiveToken() TokenInfo info = default(TokenInfo); this.ScanDirectiveToken(ref info); var errors = this.GetErrors(leadingTriviaWidth: 0); - var trailing = this.LexDirectiveTrailingTrivia(info.Kind == SyntaxKind.EndOfDirectiveToken); - return Create(in info, null, trailing, errors); + + var directiveTriviaCache = _directiveTriviaCache; + directiveTriviaCache?.Clear(); + _directiveTriviaCache = null; + + this.LexDirectiveTrailingTrivia(info.Kind == SyntaxKind.EndOfDirectiveToken, ref directiveTriviaCache); + + var token = Create(in info, null, directiveTriviaCache, errors); + _directiveTriviaCache = directiveTriviaCache; + + return token; } public SyntaxToken LexEndOfDirectiveWithOptionalPreprocessingMessage() @@ -2456,7 +2459,14 @@ public SyntaxToken LexEndOfDirectiveWithOptionalPreprocessingMessage() : SyntaxFactory.PreprocessingMessage(builder.ToStringAndFree()); // now try to consume the EOL if there. - var trailing = this.LexDirectiveTrailingTrivia(includeEndOfLine: true)?.ToListNode(); + var directiveTriviaCache = _directiveTriviaCache; + directiveTriviaCache?.Clear(); + _directiveTriviaCache = null; + + this.LexDirectiveTrailingTrivia(includeEndOfLine: true, ref directiveTriviaCache); + var trailing = directiveTriviaCache?.ToListNode(); + _directiveTriviaCache = directiveTriviaCache; + var endOfDirective = SyntaxFactory.Token(leading, SyntaxKind.EndOfDirectiveToken, trailing); return endOfDirective; @@ -2628,10 +2638,8 @@ private bool ScanDirectiveToken(ref TokenInfo info) return info.Kind != SyntaxKind.None; } - private SyntaxListBuilder? LexDirectiveTrailingTrivia(bool includeEndOfLine) + private void LexDirectiveTrailingTrivia(bool includeEndOfLine, ref SyntaxListBuilder? trivia) { - SyntaxListBuilder? trivia = null; - CSharpSyntaxNode? tr; while (true) { @@ -2660,8 +2668,6 @@ private bool ScanDirectiveToken(ref TokenInfo info) AddTrivia(tr, ref trivia); } } - - return trivia; } private CSharpSyntaxNode? LexDirectiveTrivia() @@ -3739,7 +3745,7 @@ private bool ScanXmlCrefToken(ref TokenInfo info) // check to see if it is an actual keyword // NOTE: name attribute values don't respect keywords - everything is an identifier. SyntaxKind keywordKind; - if (!InXmlNameAttributeValue && !info.IsVerbatim && !info.HasIdentifierEscapeSequence && _cache.TryGetKeywordKind(info.StringValue, out keywordKind)) + if (!InXmlNameAttributeValue && !info.IsVerbatim && !info.HasIdentifierEscapeSequence && _cache.TryGetKeywordKind(info.StringValue!, out keywordKind)) { if (SyntaxFacts.IsContextualKeyword(keywordKind)) { diff --git a/src/Compilers/CSharp/Portable/Parser/LexerCache.cs b/src/Compilers/CSharp/Portable/Parser/LexerCache.cs index e72e483f343b0..10afad5da8947 100644 --- a/src/Compilers/CSharp/Portable/Parser/LexerCache.cs +++ b/src/Compilers/CSharp/Portable/Parser/LexerCache.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - // #define COLLECT_STATS using System; @@ -63,18 +61,19 @@ internal bool TryGetKeywordKind(string key, out SyntaxKind kind) return kind != SyntaxKind.None; } - internal SyntaxTrivia LookupTrivia( + internal SyntaxTrivia LookupTrivia( char[] textBuffer, int keyStart, int keyLength, int hashCode, - Func createTriviaFunction) + Func createTriviaFunction, + TArg data) { var value = _triviaMap.FindItem(textBuffer, keyStart, keyLength, hashCode); if (value == null) { - value = createTriviaFunction(); + value = createTriviaFunction(data); _triviaMap.AddItem(textBuffer, keyStart, keyLength, hashCode, value); } @@ -102,12 +101,13 @@ private static void Miss() } #endif - internal SyntaxToken LookupToken( + internal SyntaxToken LookupToken( char[] textBuffer, int keyStart, int keyLength, int hashCode, - Func createTokenFunction) + Func createTokenFunction, + TArg data) { var value = _tokenMap.FindItem(textBuffer, keyStart, keyLength, hashCode); @@ -116,7 +116,7 @@ internal SyntaxToken LookupToken( #if COLLECT_STATS Miss(); #endif - value = createTokenFunction(); + value = createTokenFunction(data); _tokenMap.AddItem(textBuffer, keyStart, keyLength, hashCode, value); } else diff --git a/src/Compilers/CSharp/Portable/Parser/QuickScanner.cs b/src/Compilers/CSharp/Portable/Parser/QuickScanner.cs index 7b57b67a17382..13d159ced8b5d 100644 --- a/src/Compilers/CSharp/Portable/Parser/QuickScanner.cs +++ b/src/Compilers/CSharp/Portable/Parser/QuickScanner.cs @@ -242,7 +242,8 @@ private enum CharFlags : byte TextWindow.LexemeRelativeStart, i - TextWindow.LexemeRelativeStart, hashCode, - _createQuickTokenFunction); + CreateQuickToken, + this); return token; } else @@ -252,15 +253,13 @@ private enum CharFlags : byte } } - private readonly Func _createQuickTokenFunction; - - private SyntaxToken CreateQuickToken() + private static SyntaxToken CreateQuickToken(Lexer lexer) { #if DEBUG - var quickWidth = TextWindow.Width; + var quickWidth = lexer.TextWindow.Width; #endif - TextWindow.Reset(TextWindow.LexemeStartPosition); - var token = this.LexSyntaxToken(); + lexer.TextWindow.Reset(lexer.TextWindow.LexemeStartPosition); + var token = lexer.LexSyntaxToken(); #if DEBUG Debug.Assert(quickWidth == token.FullWidth); #endif diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt index b8663e3812f3f..2c2ade38192fb 100644 --- a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt @@ -5,6 +5,7 @@ Microsoft.CodeAnalysis.CSharp.Syntax.CrefParameterSyntax.Update(Microsoft.CodeAn Microsoft.CodeAnalysis.CSharp.Syntax.CrefParameterSyntax.WithReadOnlyKeyword(Microsoft.CodeAnalysis.SyntaxToken readOnlyKeyword) -> Microsoft.CodeAnalysis.CSharp.Syntax.CrefParameterSyntax! static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetDeclaredSymbol(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.LocalFunctionStatementSyntax! node, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.IMethodSymbol? static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetElementConversion(this Microsoft.CodeAnalysis.Operations.ISpreadOperation! spread) -> Microsoft.CodeAnalysis.CSharp.Conversion +[RSEXPERIMENTAL001]Microsoft.CodeAnalysis.CSharp.CSharpCompilation.GetSemanticModel(Microsoft.CodeAnalysis.SyntaxTree! syntaxTree, Microsoft.CodeAnalysis.SemanticModelOptions options) -> Microsoft.CodeAnalysis.SemanticModel! static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.CrefParameter(Microsoft.CodeAnalysis.SyntaxToken refKindKeyword, Microsoft.CodeAnalysis.SyntaxToken readOnlyKeyword, Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax! type) -> Microsoft.CodeAnalysis.CSharp.Syntax.CrefParameterSyntax! Microsoft.CodeAnalysis.CSharp.SyntaxKind.AllowsConstraintClause = 8879 -> Microsoft.CodeAnalysis.CSharp.SyntaxKind Microsoft.CodeAnalysis.CSharp.SyntaxKind.AllowsKeyword = 8450 -> Microsoft.CodeAnalysis.CSharp.SyntaxKind diff --git a/src/Compilers/CSharp/Portable/SourceGeneration/CSharpSyntaxHelper.cs b/src/Compilers/CSharp/Portable/SourceGeneration/CSharpSyntaxHelper.cs index 4014712c69783..3be1c2ba29827 100644 --- a/src/Compilers/CSharp/Portable/SourceGeneration/CSharpSyntaxHelper.cs +++ b/src/Compilers/CSharp/Portable/SourceGeneration/CSharpSyntaxHelper.cs @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Threading; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.SourceGeneration; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp @@ -21,9 +19,6 @@ private CSharpSyntaxHelper() public override bool IsCaseSensitive => true; - protected override int AttributeListKind - => (int)SyntaxKind.AttributeList; - public override bool IsValidIdentifier(string name) => SyntaxFacts.IsValidIdentifier(name); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs index 8b3450a06e331..65a64fb3888e6 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs @@ -4663,7 +4663,9 @@ private void AddSynthesizedConstructorsIfNecessary(MembersAndInitializersBuilder } //kick out early if we've seen everything we're looking for - if (hasInstanceConstructor && hasStaticConstructor) + if (hasInstanceConstructor && + hasParameterlessInstanceConstructor && + hasStaticConstructor) { break; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index 461150c1449e8..301a71f6bedf4 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -1005,54 +1005,56 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments return; } - var syntaxTrees = DeclaringCompilation.SyntaxTrees; - var matchingTrees = DeclaringCompilation.GetSyntaxTreesByMappedPath(attributeFilePath); + var normalizedPath = FileUtilities.GetNormalizedPathOrOriginalPath(attributeFilePath, basePath: SyntaxTree.FilePath); + var matchingTrees = DeclaringCompilation.GetSyntaxTreesByPath(normalizedPath); + if (matchingTrees.Count > 1) + { + diagnostics.Add(ErrorCode.ERR_InterceptorNonUniquePath, attributeData.GetAttributeArgumentLocation(filePathParameterIndex), normalizedPath); + return; + } + if (matchingTrees.Count == 0) { - var referenceResolver = DeclaringCompilation.Options.SourceReferenceResolver; - // if we expect '/_/Program.cs': + // Temporary compat behavior: check if 'attributeFilePath' is equivalent to a mapped path of one of the syntax trees in the compilation. + matchingTrees = DeclaringCompilation.GetSyntaxTreesByMappedPath(attributeFilePath); - // we might get: 'C:\Project\Program.cs' <-- path not mapped - var unmappedMatch = syntaxTrees.FirstOrDefault(static (tree, filePath) => tree.FilePath == filePath, attributeFilePath); - if (unmappedMatch != null) + if (matchingTrees.Count > 1) { - diagnostics.Add( - ErrorCode.ERR_InterceptorPathNotInCompilationWithUnmappedCandidate, - attributeData.GetAttributeArgumentLocation(filePathParameterIndex), - attributeFilePath, - mapPath(referenceResolver, unmappedMatch)); + diagnostics.Add(ErrorCode.ERR_InterceptorNonUniquePath, attributeData.GetAttributeArgumentLocation(filePathParameterIndex), attributeFilePath); return; } + } - // we might get: '\_\Program.cs' <-- slashes not normalized - // we might get: '\_/Program.cs' <-- slashes don't match + // Neither the primary or compat methods of resolving the file path found any match. + if (matchingTrees.Count == 0) + { + // if we expect '/src/Program.cs': // we might get: 'Program.cs' <-- suffix match - // Force normalization of all '\' to '/', but when we recommend a path in the diagnostic message, ensure it will match what we expect if the user decides to use it. - var suffixMatch = syntaxTrees.FirstOrDefault(static (tree, pair) - => mapPath(pair.referenceResolver, tree) + var syntaxTrees = DeclaringCompilation.SyntaxTrees; + var suffixMatch = syntaxTrees.FirstOrDefault(static (tree, attributeFilePathWithForwardSlashes) + => tree.FilePath .Replace('\\', '/') - .EndsWith(pair.attributeFilePath), - (referenceResolver, attributeFilePath: attributeFilePath.Replace('\\', '/'))); + .EndsWith(attributeFilePathWithForwardSlashes), + attributeFilePath.Replace('\\', '/')); if (suffixMatch != null) { + var recommendedPath = PathUtilities.IsAbsolute(SyntaxTree.FilePath) + ? PathUtilities.GetRelativePath(PathUtilities.GetDirectoryName(SyntaxTree.FilePath), suffixMatch.FilePath) + : suffixMatch.FilePath; diagnostics.Add( ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, attributeData.GetAttributeArgumentLocation(filePathParameterIndex), attributeFilePath, - mapPath(referenceResolver, suffixMatch)); + recommendedPath); return; } - diagnostics.Add(ErrorCode.ERR_InterceptorPathNotInCompilation, attributeData.GetAttributeArgumentLocation(filePathParameterIndex), attributeFilePath); + diagnostics.Add(ErrorCode.ERR_InterceptorPathNotInCompilation, attributeData.GetAttributeArgumentLocation(filePathParameterIndex), normalizedPath); return; } - else if (matchingTrees.Count > 1) - { - diagnostics.Add(ErrorCode.ERR_InterceptorNonUniquePath, attributeData.GetAttributeArgumentLocation(filePathParameterIndex), attributeFilePath); - return; - } + Debug.Assert(matchingTrees.Count == 1); SyntaxTree? matchingTree = matchingTrees[0]; // Internally, line and character numbers are 0-indexed, but when they appear in code or diagnostic messages, they are 1-indexed. int lineNumberZeroBased = lineNumberOneBased - 1; @@ -1112,11 +1114,6 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments DeclaringCompilation.AddInterception(matchingTree.FilePath, lineNumberZeroBased, characterNumberZeroBased, attributeLocation, this); - static string mapPath(SourceReferenceResolver? referenceResolver, SyntaxTree tree) - { - return referenceResolver?.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath; - } - // Caller must free the returned builder. ArrayBuilder getNamespaceNames() { diff --git a/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs b/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs index 98b3b953330d2..e3bef07119fac 100644 --- a/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs +++ b/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs @@ -2078,6 +2078,13 @@ internal static bool IsWellKnownTypeIsExternalInit(this TypeSymbol typeSymbol) internal static bool IsWellKnownTypeOutAttribute(this TypeSymbol typeSymbol) => typeSymbol.IsWellKnownInteropServicesTopLevelType("OutAttribute"); + // Keep consistent with ISymbolExtensions.IsWellKnownTypeLock and VB equivalent. + internal static bool IsWellKnownTypeLock(this TypeSymbol typeSymbol) + { + return typeSymbol is NamedTypeSymbol { Name: WellKnownMemberNames.LockTypeName, Arity: 0, ContainingType: null } && + typeSymbol.IsContainedInNamespace(nameof(System), nameof(System.Threading)); + } + private static bool IsWellKnownInteropServicesTopLevelType(this TypeSymbol typeSymbol, string name) { if (typeSymbol.Name != name || typeSymbol.ContainingType is object) diff --git a/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/CSharpSyntaxNode.cs b/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/CSharpSyntaxNode.cs index b34e5ff4b5323..19869a60ef521 100644 --- a/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/CSharpSyntaxNode.cs +++ b/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/CSharpSyntaxNode.cs @@ -175,7 +175,7 @@ internal static DirectiveStack ApplyDirectivesToListOrNode(GreenNode listOrNode, internal virtual IList GetDirectives() { - if ((this.flags & NodeFlags.ContainsDirectives) != 0) + if (this.ContainsDirectives) { var list = new List(32); GetDirectives(this, list); @@ -223,12 +223,12 @@ protected void SetFactoryContext(SyntaxFactoryContext context) { if (context.IsInAsync) { - this.flags |= NodeFlags.FactoryContextIsInAsync; + SetFlags(NodeFlags.FactoryContextIsInAsync); } if (context.IsInQuery) { - this.flags |= NodeFlags.FactoryContextIsInQuery; + SetFlags(NodeFlags.FactoryContextIsInQuery); } } diff --git a/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/StructuredTriviaSyntax.cs b/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/StructuredTriviaSyntax.cs index 79fcbba920d3c..dee293d870c6f 100644 --- a/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/StructuredTriviaSyntax.cs +++ b/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/StructuredTriviaSyntax.cs @@ -13,11 +13,11 @@ internal abstract partial class StructuredTriviaSyntax : CSharpSyntaxNode internal StructuredTriviaSyntax(SyntaxKind kind, DiagnosticInfo[] diagnostics = null, SyntaxAnnotation[] annotations = null) : base(kind, diagnostics, annotations) { - this.flags |= NodeFlags.ContainsStructuredTrivia; + SetFlags(NodeFlags.ContainsStructuredTrivia); if (this.Kind == SyntaxKind.SkippedTokensTrivia) { - this.flags |= NodeFlags.ContainsSkippedText; + SetFlags(NodeFlags.ContainsSkippedText); } } diff --git a/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxToken.MissingTokenWithTrivia.cs b/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxToken.MissingTokenWithTrivia.cs index b3976b5113b00..d8dab91ec20ed 100644 --- a/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxToken.MissingTokenWithTrivia.cs +++ b/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxToken.MissingTokenWithTrivia.cs @@ -16,13 +16,13 @@ internal class MissingTokenWithTrivia : SyntaxTokenWithTrivia internal MissingTokenWithTrivia(SyntaxKind kind, GreenNode leading, GreenNode trailing) : base(kind, leading, trailing) { - this.flags &= ~NodeFlags.IsNotMissing; + ClearFlags(NodeFlags.IsNotMissing); } internal MissingTokenWithTrivia(SyntaxKind kind, GreenNode leading, GreenNode trailing, DiagnosticInfo[] diagnostics, SyntaxAnnotation[] annotations) : base(kind, leading, trailing, diagnostics, annotations) { - this.flags &= ~NodeFlags.IsNotMissing; + ClearFlags(NodeFlags.IsNotMissing); } public override string Text diff --git a/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxToken.cs b/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxToken.cs index b1f4740497d19..5306c96ff3d5c 100644 --- a/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxToken.cs +++ b/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxToken.cs @@ -23,39 +23,39 @@ internal SyntaxToken(SyntaxKind kind) : base(kind) { FullWidth = this.Text.Length; - this.flags |= NodeFlags.IsNotMissing; //note: cleared by subclasses representing missing tokens + SetFlags(NodeFlags.IsNotMissing); //note: cleared by subclasses representing missing tokens } internal SyntaxToken(SyntaxKind kind, DiagnosticInfo[] diagnostics) : base(kind, diagnostics) { FullWidth = this.Text.Length; - this.flags |= NodeFlags.IsNotMissing; //note: cleared by subclasses representing missing tokens + SetFlags(NodeFlags.IsNotMissing); //note: cleared by subclasses representing missing tokens } internal SyntaxToken(SyntaxKind kind, DiagnosticInfo[] diagnostics, SyntaxAnnotation[] annotations) : base(kind, diagnostics, annotations) { FullWidth = this.Text.Length; - this.flags |= NodeFlags.IsNotMissing; //note: cleared by subclasses representing missing tokens + SetFlags(NodeFlags.IsNotMissing); //note: cleared by subclasses representing missing tokens } internal SyntaxToken(SyntaxKind kind, int fullWidth) : base(kind, fullWidth) { - this.flags |= NodeFlags.IsNotMissing; //note: cleared by subclasses representing missing tokens + SetFlags(NodeFlags.IsNotMissing); //note: cleared by subclasses representing missing tokens } internal SyntaxToken(SyntaxKind kind, int fullWidth, DiagnosticInfo[] diagnostics) : base(kind, diagnostics, fullWidth) { - this.flags |= NodeFlags.IsNotMissing; //note: cleared by subclasses representing missing tokens + SetFlags(NodeFlags.IsNotMissing); //note: cleared by subclasses representing missing tokens } internal SyntaxToken(SyntaxKind kind, int fullWidth, DiagnosticInfo[] diagnostics, SyntaxAnnotation[] annotations) : base(kind, diagnostics, annotations, fullWidth) { - this.flags |= NodeFlags.IsNotMissing; //note: cleared by subclasses representing missing tokens + SetFlags(NodeFlags.IsNotMissing); //note: cleared by subclasses representing missing tokens } //==================== diff --git a/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxTrivia.cs b/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxTrivia.cs index 819f7d42227db..fd2f3a5a8763a 100644 --- a/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxTrivia.cs +++ b/src/Compilers/CSharp/Portable/Syntax/InternalSyntax/SyntaxTrivia.cs @@ -17,7 +17,7 @@ internal SyntaxTrivia(SyntaxKind kind, string text, DiagnosticInfo[]? diagnostic this.Text = text; if (kind == SyntaxKind.PreprocessingMessageTrivia) { - this.flags |= NodeFlags.ContainsSkippedText; + SetFlags(NodeFlags.ContainsSkippedText); } } diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index 2734d6f8697b4..74302674adfcb 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -272,6 +272,11 @@ Člen záznamu {0} musí být čitelná vlastnost instance nebo pole typu {1}, která se bude shodovat s pozičním parametrem {2}. + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. Prostředek použitého příkazu typu {0} nejde použít v asynchronních metodách ani v asynchronních výrazech lambda. @@ -2357,6 +2362,11 @@ implicitní inicializační výraz indexeru + + Lock object + Lock object + + <missing> <missing> @@ -2597,6 +2607,16 @@ Operace může při běhu přetéct (pro přepis použijte syntaxi unchecked) + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. Porovnání ukazatelů funkcí může přinést neočekávaný výsledek, protože ukazatele na stejnou funkci můžou být rozdílné. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index db53e7d2fa922..e4815886b5d30 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -272,6 +272,11 @@ Das Datensatzelement "{0}" muss eine lesbare Instanzeigenschaft oder ein Feld vom Typ "{1}" sein, um dem Positionsparameter "{2}" zu entsprechen. + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. Eine using-Anweisungsressource vom Typ „{0}“ kann nicht in asynchronen Methoden oder asynchronen Lambdaausdrücken verwendet werden. @@ -2357,6 +2362,11 @@ impliziter Indexerinitialisierer + + Lock object + Lock object + + <missing> <missing> @@ -2597,6 +2607,16 @@ Der Vorgang kann zur Laufzeit überlaufen (verwenden Sie zum Überschreiben die Syntax „unchecked“) + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. Der Vergleich von Funktionszeigern kann zu einem unerwarteten Ergebnis führen, weil Zeiger auf dieselbe Funktion möglicherweise unterschiedlich sind. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index eba0d3478a6b0..f40cdd6d3720c 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -272,6 +272,11 @@ El miembro de registro '{0}' debe ser una propiedad de instancia legible o un campo de tipo '{1}' para coincidir con el parámetro posicional '{2}'. + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. Un recurso de instrucción using de tipo '{0}' no se puede usar en métodos asincrónicos ni expresiones lambda asincrónicas. @@ -2357,6 +2362,11 @@ inicializador de indizador implícito + + Lock object + Lock object + + <missing> <missing> @@ -2597,6 +2607,16 @@ La operación puede desbordarse en tiempo de ejecución (use la sintaxis "sin activar" para invalidarla). + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. La comparación de los punteros de función puede proporcionar resultados inesperados, ya que los punteros a la misma función pueden ser distintos. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index aa0b07d94e235..e8edea5c7a000 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -272,6 +272,11 @@ Le membre d'enregistrement '{0}' doit être une propriété d'instance our champ lisible de type '{1}' pour correspondre au paramètre positionnel '{2}'. + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. Une ressource d’instruction d’utilisation de type '{0}' ne peut pas être utilisée dans des méthodes asynchrones ou des expressions lambda asynchrones. @@ -2357,6 +2362,11 @@ initialiseur d’indexeur implicite + + Lock object + Lock object + + <missing> <manquant> @@ -2597,6 +2607,16 @@ L'opération peut déborder au moment de l'exécution (utilisez la syntaxe 'unchecked' pour passer outre). + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. La comparaison des pointeurs de fonction peut donner un résultat inattendu, car les pointeurs vers la même fonction peuvent être distincts. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index 76c644a9f029e..7c620e4710d5e 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -272,6 +272,11 @@ Il membro di record '{0}' deve essere una proprietà di istanza leggibile o campo di tipo '{1}' per corrispondere al parametro posizionale '{2}'. + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. Non è possibile usare una risorsa di istruzione using di tipo '{0}' in metodi asincroni o espressioni lambda asincrone. @@ -2357,6 +2362,11 @@ inizializzatore indicizzatore implicito + + Lock object + Lock object + + <missing> <missing> @@ -2597,6 +2607,16 @@ Con l’operazione può verificarsi un overflow in fase di esecuzione. Usare la sintassi 'unchecked' per eseguire l'override + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. Il confronto dei puntatori a funzione potrebbe produrre un risultato imprevisto perché i puntatori alla stessa funzione possono essere distinti. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index b693bd1e01a40..9b1bbac57d392 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -272,6 +272,11 @@ レコード メンバー '{0}' は、位置指定パラメーター '{2}' に一致させるための型 '{1}' の読み取り可能なインスタンス プロパティまたはフィールドである必要があります。 + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. 型 '{0}' の using ステートメント リソースは、非同期メソッドまたは非同期ラムダ式では使用できません。 @@ -2357,6 +2362,11 @@ 暗黙的なインデクサー初期化子 + + Lock object + Lock object + + <missing> <missing> @@ -2597,6 +2607,16 @@ 実行時に操作がオーバーフローする可能性があります (オーバーライドするには 'unchecked' 構文を使用してください) + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. 同じ関数へのポインターがそれぞれ異なっている可能性があるため、関数ポインターの比較によって予期しない結果が生成されるおそれがあります。 diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index d96a1be636aa8..e7e15efc78fa4 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -272,6 +272,11 @@ 위치 매개 변수 '{0}'과(와) 일치하려면 레코드 멤버 '{1}'이(가) 유형 '{2}'의 읽을 수 있는 인스턴스 속성 또는 필드여야 합니다. + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. '{0}' 형식의 using 문 리소스는 비동기 메서드 또는 비동기 람다 식에 사용할 수 없습니다. @@ -2357,6 +2362,11 @@ 암시적 인덱서 이니셜라이저 + + Lock object + Lock object + + <missing> <missing> @@ -2597,6 +2607,16 @@ 작업이 런타임에 오버플로될 수 있습니다('선택되지 않은' 구문을 사용하여 재정의). + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. 같은 함수에 대한 포인터가 다를 수 있으므로 함수 포인터를 비교하면 예기치 않은 결과가 발생할 수 있습니다. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index b5ddafe9ce2f0..a285376f9a517 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -272,6 +272,11 @@ Składowa rekordu "{0}" musi być możliwą do odczytu właściwością wystąpienia typu "{1}", aby dopasować parametr pozycyjny "{2}". + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. Zasobu instrukcji przy użyciu typu '{0}' nie można używać w metodach asynchronicznych ani asynchronicznych wyrażeniach lambda. @@ -2357,6 +2362,11 @@ niejawny inicjator indeksatora + + Lock object + Lock object + + <missing> <missing> @@ -2597,6 +2607,16 @@ Operacja może się przepełnić w środowisku uruchomieniowym (użyj składni „niezaznaczone”, aby zastąpić) + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. Porównanie wskaźników funkcji może zwrócić nieoczekiwany wynik, ponieważ wskaźniki do tej samej funkcji mogą być różne. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 98496bb60a7ea..befb8fa39196b 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -272,6 +272,11 @@ O membro do registro '{0}' precisa ser uma propriedade de instância legível ou campo do tipo '{1}' para corresponder ao parâmetro posicional '{2}'. + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. Um recurso de instrução using do tipo "{0}" não pode ser usado em métodos assíncronos ou expressões lambda assíncronas. @@ -2357,6 +2362,11 @@ inicializador de indexador implícito + + Lock object + Lock object + + <missing> <ausente> @@ -2597,6 +2607,16 @@ A operação pode estourar em tempo de execução (use a sintaxe 'não verificada' para substituir) + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. A comparação de ponteiros de função pode gerar um resultado inesperado, pois os ponteiros para a mesma função podem ser diferentes. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index cac9864591514..a9161bf15b2cf 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -272,6 +272,11 @@ Элемент записи "{0}" должен быть доступным для чтения свойством экземпляра или полем типа "{1}", чтобы соответствовать позиционному параметру "{2}". + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. Ресурс оператора использования типа "{0}" нельзя применять в асинхронных методах или асинхронных лямбда-выражениях. @@ -2357,6 +2362,11 @@ неявный инициализатор индексатора + + Lock object + Lock object + + <missing> <missing> @@ -2597,6 +2607,16 @@ Операция может привести к переполнению в среде выполнения (для переопределения используйте синтаксис "unchecked") + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. Сравнение указателей на функции может привести к непредвиденному результату, так как указатели на одну и ту же функцию могут быть разными. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index ce9b06b7ed857..c29d6779b8e05 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -272,6 +272,11 @@ {0} kayıt üyesi, {1} konumsal parametresi ile eşleşmesi için {2} türünde okunabilir bir örnek özelliği veya alan olmalıdır. + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. '{0}' türündeki bir using deyimi kaynağı, asenkron yöntemlerde veya asenkron lambda ifadelerinde kullanılamaz. @@ -2357,6 +2362,11 @@ örtük dizin oluşturucu başlatıcısı + + Lock object + Lock object + + <missing> <missing> @@ -2597,6 +2607,16 @@ İşlem, çalışma zamanında taşabilir (geçersiz kılmak için “denetlenmemiş” sözdizimini kullanın) + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. Aynı işleve yönelik işaretçiler birbirinden farklı olabileceğinden işlev işaretçilerinin karşılaştırılması beklenmeyen bir sonuç verebilir. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index 7f237bded5080..a20ac59511b39 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -272,6 +272,11 @@ 记录成员 '{0}' 必须为类型 '{1}' 的可读实例属性或字段,以匹配位置参数 '{2}'。 + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. 无法在异步方法或异步 lambda 表达式中使用类型为“{0}”的 using 语句资源。 @@ -2357,6 +2362,11 @@ 隐式索引器初始值设定项 + + Lock object + Lock object + + <missing> <missing> @@ -2597,6 +2607,16 @@ 操作可能在运行时溢出(请使用“unchecked”语法替代) + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. 函数指针比较可能产生意外的结果,因为指向同一函数的指针可能是不同的。 diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index b1e857acbfe3f..6b74a9c4e4324 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -272,6 +272,11 @@ 記錄成員 '{0}' 必須是類型 '{1}' 的可讀取執行個體屬性或欄位,才能符合位置參數 '{2}'。 + + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + + A using statement resource of type '{0}' cannot be used in async methods or async lambda expressions. 類型 '{0}' 的 using 陳述式資源不能用於非同步方法或非同步 Lambda 運算式。 @@ -2357,6 +2362,11 @@ 隱含索引子初始設定式 + + Lock object + Lock object + + <missing> <missing> @@ -2597,6 +2607,16 @@ 作業在執行階段可能會溢位 (請使用 'unchecked' 語法覆寫) + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + + Comparison of function pointers might yield an unexpected result, since pointers to the same function may be distinct. 因為同一個函式的指標可能截然不同,所以比較函式指標可能會產生非預期的結果。 diff --git a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs index feb32ac0337e9..4b1adefbb858d 100644 --- a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs +++ b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs @@ -13773,7 +13773,7 @@ class C VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/debug:embedded", "/out:embed.exe" }, generators: new[] { generator }, analyzers: null); - var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator); + var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(dir.Path, generator); ValidateEmbeddedSources_Portable(new Dictionary { { Path.Combine(dir.Path, generatorPrefix, $"generatedSource.cs"), generatedSource } }, dir, true); // Clean up temp files @@ -13814,7 +13814,7 @@ class C generators: new[] { generator }); // Verify source generator was executed, regardless of the value of 'skipAnalyzers'. - var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator); + var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(dir.Path, generator); ValidateEmbeddedSources_Portable(new Dictionary { { Path.Combine(dir.Path, generatorPrefix, "generatedSource.cs"), generatedSource } }, dir, true); if (expectedAnalyzerExecution) @@ -13853,8 +13853,8 @@ class C VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/debug:embedded", "/out:embed.exe" }, generators: new[] { generator, generator2 }, analyzers: null); - var generator1Prefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator); - var generator2Prefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator2); + var generator1Prefix = GeneratorDriver.GetFilePathPrefixForGenerator(dir.Path, generator); + var generator2Prefix = GeneratorDriver.GetFilePathPrefixForGenerator(dir.Path, generator2); ValidateEmbeddedSources_Portable(new Dictionary { @@ -13939,7 +13939,7 @@ class C VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/generatedfilesout:" + generatedDir.Path, "/langversion:preview", "/out:embed.exe" }, generators: new[] { generator }, analyzers: null); - var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator); + var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generatedDir.Path, generator); ValidateWrittenSources(new() { { Path.Combine(generatedDir.Path, generatorPrefix, expectedDir), new() { { expectedFileName, generatedSource } } } @@ -13965,7 +13965,7 @@ class C VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/generatedfilesout:" + generatedDir.Path, "/langversion:preview", "/out:embed.exe" }, generators: new[] { generator1 }, analyzers: null); - var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator1); + var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generatedDir.Path, generator1); ValidateWrittenSources(new() { { Path.Combine(generatedDir.Path, generatorPrefix), new() { { "generatedSource.cs", generatedSource1 } } } }); var generatedSource2 = "public class D { }"; @@ -14000,13 +14000,13 @@ class C VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/generatedfilesout:" + generatedDir.Path, "/langversion:preview", "/out:embed.exe" }, generators: new[] { generator, generator2 }, analyzers: null); - var generator1Prefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator); - var generator2Prefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator2); + var generator1Prefix = GeneratorDriver.GetFilePathPrefixForGenerator(generatedDir.Path, generator); + var generator2Prefix = GeneratorDriver.GetFilePathPrefixForGenerator(generatedDir.Path, generator2); ValidateWrittenSources(new() { - { Path.Combine(generatedDir.Path, generator1Prefix), new() { { source1Name, source1 } } }, - { Path.Combine(generatedDir.Path, generator2Prefix), new() { { source2Name, source2 } } } + { generator1Prefix, new() { { source1Name, source1 } } }, + { generator2Prefix, new() { { source2Name, source2 } } } }); // Clean up temp files @@ -14041,7 +14041,7 @@ class C VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/generatedfilesout:" + generatedDir.Path, "/langversion:preview", "/out:embed.exe" }, generators: new[] { generator }, analyzers: null); - var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator); + var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generatedDir.Path, generator); ValidateWrittenSources(new() { { Path.Combine(generatedDir.Path, generatorPrefix, expectedDir), new() { { generatedFileName, generatedSource } } } @@ -14158,7 +14158,7 @@ class C VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/generatedfilesout:" + generatedDir.Path, "/langversion:preview", "/out:embed.exe" }, generators: new[] { generator }, analyzers: null); - var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator); + var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generatedDir.Path, generator); ValidateWrittenSources(new() { { Path.Combine(generatedDir.Path, generatorPrefix), new() { { "generatedSource.cs", generatedSource } } } }); // Clean up temp files @@ -14263,6 +14263,200 @@ class C Directory.Delete(dir.Path, true); } + [Fact] + public void Interceptors_RelativePath_GeneratedFiles_EndToEnd() + { + var dir = Temp.CreateDirectory(); + + var srcDir = dir.CreateDirectory("src"); + var src = srcDir.CreateFile("Program.cs").WriteAllText(""" + class Program + { + static void Main() + { + M(); + } + + public static void M() => throw null!; + } + """); + + // final path will look like: + // 'TempDir/{assemblyName}/{generatorName}/Generated.cs' + // Note that generator will have access to the full path of the generated file, before adding it to the compilation + // additionally we plan to add public API to determine a "correct relative path" to use here without any additional tricks + var generatedSource = """ + using System.Runtime.CompilerServices; + using System; + + namespace Generated + { + static class Interceptors + { + [InterceptsLocation("../../src/Program.cs", 5, 9)] + public static void M1() => Console.Write(1); + } + } + + namespace System.Runtime.CompilerServices + { + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string filePath, int line, int character) + { + } + } + } + """; + var generator = new SingleFileTestGenerator(generatedSource, "Generated.cs"); + + VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: ["/langversion:preview", "/out:embed.exe", "/features:InterceptorsPreviewNamespaces=Generated"], generators: [generator], analyzers: null); + ValidateWrittenSources([]); + + // Clean up temp files + CleanupAllGeneratedFiles(src.Path); + Directory.Delete(dir.Path, true); + } + + [Fact] + public void Interceptors_RelativePath_GeneratedFiles_EndToEnd_OutputDirectoryNested() + { + // Demonstrates the difference between defaulting the generated files base path to 'Arguments.OutputDirectory' + // versus 'Arguments.BaseDirectory' (which occurs implicitly for relative paths in command line arguments) + + var dir = Temp.CreateDirectory(); + var srcDir = dir.CreateDirectory("src"); + var src = srcDir.CreateFile("Program.cs").WriteAllText(""" + class Program + { + static void Main() + { + M(); + } + + public static void M() => throw null!; + } + """); + + // final path will look like: + // 'TempDir/obj/{assemblyName}/{generatorName}/Generated.cs' + // Note that generator will have access to the full path of the generated file, before adding it to the compilation + // additionally we plan to add public API to determine a "correct relative path" to use here without any additional tricks + var generatedSource = """ + using System.Runtime.CompilerServices; + using System; + + namespace Generated + { + static class Interceptors + { + [InterceptsLocation("../../../src/Program.cs", 5, 9)] + public static void M1() => Console.Write(1); + } + } + + namespace System.Runtime.CompilerServices + { + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string filePath, int line, int character) + { + } + } + } + """; + var generator = new SingleFileTestGenerator(generatedSource, "Generated.cs"); + var objDir = dir.CreateDirectory("obj"); + + VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: ["/langversion:preview", $"/out:{objDir.Path}/embed.exe", "/features:InterceptorsPreviewNamespaces=Generated"], generators: [generator], analyzers: null); + ValidateWrittenSources([]); + + // Clean up temp files + CleanupAllGeneratedFiles(src.Path); + Directory.Delete(dir.Path, true); + } + + [Theory] + [InlineData("")] + [InlineData($"/pathmap:DIRPATH=/_/")] + [InlineData($"/pathmap:SRCDIRPATH=a/,OBJDIRPATH=b/")] + public void Interceptors_RelativePath_GeneratedFiles_EndToEnd_GeneratedFilesOutSpecified(string pathMapArgument) + { + var dir = Temp.CreateDirectory(); + + var srcDir = dir.CreateDirectory("src"); + var src = srcDir.CreateFile("Program.cs").WriteAllText(""" + class Program + { + static void Main() + { + M(); + } + + public static void M() => throw null!; + } + """); + + var objDir = dir.CreateDirectory("obj"); + + // final path will look like: + // 'TempDir/obj/{assemblyName}/{generatorName}/Generated.cs' + // Note that generator will have access to the full path of the generated file, before adding it to the compilation + // additionally we plan to add public API to determine a "correct relative path" to use here without any additional tricks + var generatedSource = """ + using System.Runtime.CompilerServices; + using System; + + namespace Generated + { + static class Interceptors + { + [InterceptsLocation("../../../src/Program.cs", 5, 9)] + public static void M1() => Console.Write(1); + } + } + + namespace System.Runtime.CompilerServices + { + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string filePath, int line, int character) + { + } + } + } + """; + var generator = new SingleFileTestGenerator(generatedSource, "Generated.cs"); + + pathMapArgument = pathMapArgument.Replace("DIRPATH", dir.Path).Replace("SRCDIRPATH", srcDir.Path).Replace("OBJDIRPATH", objDir.Path); + VerifyOutput( + dir, + src, + includeCurrentAssemblyAsAnalyzerReference: false, + additionalFlags: [ + "/generatedfilesout:" + objDir.Path, + "/langversion:preview", + "/out:embed.exe", + "/features:InterceptorsPreviewNamespaces=Generated", + .. string.IsNullOrEmpty(pathMapArgument) ? default(Span) : [pathMapArgument] + ], + generators: [generator], + analyzers: null); + + var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(objDir.Path, generator); + ValidateWrittenSources(new() + { + { generatorPrefix, new() { { "Generated.cs", generatedSource } } }, + }); + + // Clean up temp files + CleanupAllGeneratedFiles(src.Path); + Directory.Delete(dir.Path, true); + } + [Fact] [WorkItem(44087, "https://github.com/dotnet/roslyn/issues/44087")] public void SourceGeneratorsAndAnalyzerConfig() diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenForEachTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenForEachTests.cs index 3e6025a6551ab..f1ee673602927 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenForEachTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenForEachTests.cs @@ -4306,6 +4306,119 @@ public static class Extensions CompileAndVerify(source, parseOptions: TestOptions.Regular9, expectedOutput: "123"); } + [Theory, CombinatorialData] + public void TestGetEnumeratorPatternViaInExtensionOnAssignableVariable_OptionalParameter( + [CombinatorialValues("ref", "in", "ref readonly", "")] string modifier) + { + var source = $$""" + using System; + public struct C + { + public static void Main() + { + var c = new C(); + foreach (var i in c) + { + Console.Write(i); + } + } + public struct Enumerator + { + public int Current { get; private set; } + public bool MoveNext() => Current++ != 3; + } + } + public static class Extensions + { + public static C.Enumerator GetEnumerator(this in C self, {{modifier}} int x = 9) + { + Console.Write(x); + return new C.Enumerator(); + } + } + """; + if (modifier == "ref") + { + CreateCompilation(source).VerifyDiagnostics( + // (7,27): error CS7036: There is no argument given that corresponds to the required parameter 'x' of 'Extensions.GetEnumerator(in C, ref int)' + // foreach (var i in c) + Diagnostic(ErrorCode.ERR_NoCorrespondingArgument, "c").WithArguments("x", "Extensions.GetEnumerator(in C, ref int)").WithLocation(7, 27), + // (7,27): error CS1579: foreach statement cannot operate on variables of type 'C' because 'C' does not contain a public instance or extension definition for 'GetEnumerator' + // foreach (var i in c) + Diagnostic(ErrorCode.ERR_ForEachMissingMember, "c").WithArguments("C", "GetEnumerator").WithLocation(7, 27), + // (20,62): error CS1741: A ref or out parameter cannot have a default value + // public static C.Enumerator GetEnumerator(this in C self, ref int x = 9) + Diagnostic(ErrorCode.ERR_RefOutDefaultValue, "ref").WithLocation(20, 62)); + } + else + { + var verifier = CompileAndVerify(source, expectedOutput: "9123"); + if (modifier == "ref readonly") + { + verifier.VerifyDiagnostics( + // (20,83): warning CS9200: A default value is specified for 'ref readonly' parameter 'x', but 'ref readonly' should be used only for references. Consider declaring the parameter as 'in'. + // public static C.Enumerator GetEnumerator(this in C self, ref readonly int x = 9) + Diagnostic(ErrorCode.WRN_RefReadonlyParameterDefaultValue, "9").WithArguments("x").WithLocation(20, 83)); + } + else + { + verifier.VerifyDiagnostics(); + } + } + } + + [Theory, CombinatorialData] + public void TestDisposePattern_OptionalParameter( + [CombinatorialValues("ref", "in", "ref readonly", "")] string modifier) + { + var source = $$""" + using System; + public struct C + { + public static void Main() + { + var c = new C(); + foreach (var i in c) + { + Console.Write(i); + } + } + public Enumerator GetEnumerator() + { + return new Enumerator(); + } + public ref struct Enumerator + { + public int Current { get; private set; } + public bool MoveNext() => Current++ != 3; + public void Dispose({{modifier}} int x = 5) { Console.Write(x); } + } + } + """; + if (modifier == "ref") + { + CreateCompilation(source).VerifyDiagnostics( + // (20,29): error CS1741: A ref or out parameter cannot have a default value + // public void Dispose(ref int x = 5) { Console.Write(x); } + Diagnostic(ErrorCode.ERR_RefOutDefaultValue, "ref").WithLocation(20, 29)); + } + else + { + var verifier = CompileAndVerify(source, expectedOutput: "1235", verify: Verification.FailsILVerify); + if (modifier == "ref readonly") + { + verifier.VerifyDiagnostics( + // (20,50): warning CS9200: A default value is specified for 'ref readonly' parameter 'x', but 'ref readonly' should be used only for references. Consider declaring the parameter as 'in'. + // public void Dispose(ref readonly int x = 5) { Console.Write(x); } + Diagnostic(ErrorCode.WRN_RefReadonlyParameterDefaultValue, "5").WithArguments("x").WithLocation(20, 50)); + } + else + { + verifier.VerifyDiagnostics(); + } + } + } + [Fact] public void TestGetEnumeratorPatternViaExtensionsCSharp8() { diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenUsingStatementTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenUsingStatementTests.cs index 92223943b5106..168fedc8068ee 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenUsingStatementTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenUsingStatementTests.cs @@ -3095,5 +3095,49 @@ static void Main() CompileAndVerify(source, expectedOutput: "0"); } + + [Theory, CombinatorialData] + public void OptionalParameter([CombinatorialValues("ref", "in", "ref readonly", "")] string modifier) + { + var source = $$""" + using System; + using (var s = new S()) + { + Console.Write("1"); + } + ref struct S + { + public void Dispose({{modifier}} int x = 2) => Console.Write(x); + } + """; + if (modifier == "ref") + { + CreateCompilation(source).VerifyDiagnostics( + // (2,8): error CS7036: There is no argument given that corresponds to the required parameter 'x' of 'S.Dispose(ref int)' + // using (var s = new S()) + Diagnostic(ErrorCode.ERR_NoCorrespondingArgument, "var s = new S()").WithArguments("x", "S.Dispose(ref int)").WithLocation(2, 8), + // (2,8): error CS1674: 'S': type used in a using statement must be implicitly convertible to 'System.IDisposable'. + // using (var s = new S()) + Diagnostic(ErrorCode.ERR_NoConvToIDisp, "var s = new S()").WithArguments("S").WithLocation(2, 8), + // (8,25): error CS1741: A ref or out parameter cannot have a default value + // public void Dispose(ref int x = 2) => Console.Write(x); + Diagnostic(ErrorCode.ERR_RefOutDefaultValue, "ref").WithLocation(8, 25)); + } + else + { + var verifier = CompileAndVerify(source, expectedOutput: "12"); + if (modifier == "ref readonly") + { + verifier.VerifyDiagnostics( + // (8,46): warning CS9200: A default value is specified for 'ref readonly' parameter 'x', but 'ref readonly' should be used only for references. Consider declaring the parameter as 'in'. + // public void Dispose(ref readonly int x = 2) => Console.Write(x); + Diagnostic(ErrorCode.WRN_RefReadonlyParameterDefaultValue, "2").WithArguments("x").WithLocation(8, 46)); + } + else + { + verifier.VerifyDiagnostics(); + } + } + } } } diff --git a/src/Compilers/CSharp/Test/Emit2/Diagnostics/DiagnosticAnalyzerTests.cs b/src/Compilers/CSharp/Test/Emit2/Diagnostics/DiagnosticAnalyzerTests.cs index bd0984e9a1354..a9163ca2c7467 100644 --- a/src/Compilers/CSharp/Test/Emit2/Diagnostics/DiagnosticAnalyzerTests.cs +++ b/src/Compilers/CSharp/Test/Emit2/Diagnostics/DiagnosticAnalyzerTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#pragma warning disable RSEXPERIMENTAL001 // Internal usage of experimental API #nullable disable using System; @@ -4006,9 +4007,9 @@ private sealed class MySemanticModelProvider : SemanticModelProvider { private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); - public override SemanticModel GetSemanticModel(SyntaxTree tree, Compilation compilation, bool ignoreAccessibility = false) + public override SemanticModel GetSemanticModel(SyntaxTree tree, Compilation compilation, SemanticModelOptions options) { - return _cache.GetOrAdd(tree, compilation.CreateSemanticModel(tree, ignoreAccessibility)); + return _cache.GetOrAdd(tree, compilation.CreateSemanticModel(tree, options)); } public void VerifyCachedModel(SyntaxTree tree, SemanticModel model) diff --git a/src/Compilers/CSharp/Test/Emit2/Semantics/LockTests.cs b/src/Compilers/CSharp/Test/Emit2/Semantics/LockTests.cs new file mode 100644 index 0000000000000..392bfa04527f5 --- /dev/null +++ b/src/Compilers/CSharp/Test/Emit2/Semantics/LockTests.cs @@ -0,0 +1,2740 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics; + +public class LockTests : CSharpTestBase +{ + private const string LockTypeDefinition = """ + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() + { + Console.Write("D"); + } + } + } + } + """; + + [Fact] + public void LockVsUsing() + { + var source = """ + using System; + using System.Threading; + + static class C + { + static readonly Lock _lock = new(); + + static void Main() + { + M1(); + M2(); + } + + static void M1() + { + Console.Write("1"); + lock (_lock) + { + Console.Write("2"); + } + Console.Write("3"); + } + + static void M2() + { + Console.Write("1"); + using (_lock.EnterScope()) + { + Console.Write("2"); + } + Console.Write("3"); + } + } + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], expectedOutput: "1E2D31E2D3", + verify: Verification.FailsILVerify); + verifier.VerifyDiagnostics(); + var il = """ + { + // Code size 52 (0x34) + .maxstack 1 + .locals init (System.Threading.Lock.Scope V_0) + IL_0000: ldstr "1" + IL_0005: call "void System.Console.Write(string)" + IL_000a: ldsfld "System.Threading.Lock C._lock" + IL_000f: callvirt "System.Threading.Lock.Scope System.Threading.Lock.EnterScope()" + IL_0014: stloc.0 + .try + { + IL_0015: ldstr "2" + IL_001a: call "void System.Console.Write(string)" + IL_001f: leave.s IL_0029 + } + finally + { + IL_0021: ldloca.s V_0 + IL_0023: call "void System.Threading.Lock.Scope.Dispose()" + IL_0028: endfinally + } + IL_0029: ldstr "3" + IL_002e: call "void System.Console.Write(string)" + IL_0033: ret + } + """; + verifier.VerifyIL("C.M2", il); + verifier.VerifyIL("C.M1", il); + } + + [Fact] + public void SemanticModel() + { + var source = """ + using System.Threading; + + Lock l = null; + lock (l) + { + } + """; + var compilation = CreateCompilation([source, LockTypeDefinition]); + compilation.VerifyDiagnostics(); + + var tree = compilation.SyntaxTrees[0]; + var model = compilation.GetSemanticModel(tree); + + var localDecl = tree.GetRoot().DescendantNodes().OfType().Single(); + var localSymbol = (ILocalSymbol)model.GetDeclaredSymbol(localDecl.Declaration.Variables.Single())!; + Assert.Equal("l", localSymbol.Name); + Assert.Equal("System.Threading.Lock", localSymbol.Type.ToTestDisplayString()); + + var lockStatement = tree.GetRoot().DescendantNodes().OfType().Single(); + var lockExprInfo = model.GetSymbolInfo(lockStatement.Expression); + Assert.Equal(localSymbol, lockExprInfo.Symbol); + } + + [Fact] + public void MissingEnterScope() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock { } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void EnterScopeReturnsVoid() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public void EnterScope() { } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void EnterScopeStatic() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public static Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void EnterScopeTakesParameters() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope(int arg) => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void EnterScopeTakesParameters_Optional() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope(int arg = 1) => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void EnterScopeTakesParameters_ParamsArray() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope(params int[] args) => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void EnterScopeMultipleOverloads_01() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7), + // (9,22): error CS0111: Type 'Lock' already defines a member called 'EnterScope' with the same parameter types + // public Scope EnterScope() => new Scope(); + Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "EnterScope").WithArguments("EnterScope", "System.Threading.Lock").WithLocation(9, 22)); + } + + [Fact] + public void EnterScopeMultipleOverloads_02() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { System.Console.Write("L"); } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public Scope EnterScope(int x) + { + Console.Write("X"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() => Console.Write("D"); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "ELD", verify: Verification.FailsILVerify).VerifyDiagnostics(); + } + + [Fact] + public void EnterScopeMultipleOverloads_03() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { System.Console.Write("L"); } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public Scope EnterScope() + { + Console.Write("T"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() => Console.Write("D"); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "ELD", verify: Verification.FailsILVerify).VerifyDiagnostics(); + } + + [Fact] + public void EnterScopeHidden() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { System.Console.Write("L"); } + + namespace System.Threading + { + public class LockBase + { + public Lock.Scope EnterScope() + { + Console.Write("B"); + return new(); + } + } + + public class Lock : LockBase + { + public new Scope EnterScope() + { + Console.Write("E"); + return new(); + } + + public ref struct Scope + { + public void Dispose() => Console.Write("D"); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "ELD", verify: Verification.FailsILVerify).VerifyDiagnostics(); + } + + [Fact] + public void EnterScopeOverride() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { System.Console.Write("L"); } + + namespace System.Threading + { + public class LockBase + { + public virtual Lock.Scope EnterScope() + { + Console.Write("B"); + return new(); + } + } + + public class Lock : LockBase + { + public override Scope EnterScope() + { + Console.Write("E"); + return new(); + } + + public ref struct Scope + { + public void Dispose() => Console.Write("D"); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "ELD", verify: Verification.FailsILVerify).VerifyDiagnostics(); + } + + [Fact] + public void EnterScopeVirtual() + { + var source = """ + System.Threading.Lock l = new System.Threading.LockDerived(); + lock (l) { System.Console.Write("L"); } + + namespace System.Threading + { + public class Lock + { + public virtual Scope EnterScope() + { + Console.Write("E"); + return new(); + } + + public ref struct Scope + { + public void Dispose() => Console.Write("D"); + } + } + + public class LockDerived : Lock + { + public override Scope EnterScope() + { + Console.Write("O"); + return new(); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "OLD", verify: Verification.FailsILVerify).VerifyDiagnostics(); + } + + [Fact] + public void EnterScopeExplicitImplementation() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public interface ILock + { + Lock.Scope EnterScope(); + } + + public class Lock : ILock + { + Scope ILock.EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void MissingScopeDispose() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope { } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock+Scope.Dispose' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock+Scope", "Dispose").WithLocation(2, 7)); + } + + [Fact] + public void ScopeDisposeStatic() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public static void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock+Scope.Dispose' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock+Scope", "Dispose").WithLocation(2, 7)); + } + + [Fact] + public void ScopeDisposeReturnsNonVoid() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public int Dispose() => 1; + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock+Scope.Dispose' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock+Scope", "Dispose").WithLocation(2, 7)); + } + + [Fact] + public void ScopeDisposeTakesParameters() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose(int x) { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock+Scope.Dispose' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock+Scope", "Dispose").WithLocation(2, 7)); + } + + [Fact] + public void ScopeDisposeTakesParameters_Optional() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose(int x = 1) { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock+Scope.Dispose' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock+Scope", "Dispose").WithLocation(2, 7)); + } + + [Fact] + public void ScopeDisposeTakesParameters_ParamsArray() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose(params int[] xs) { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock+Scope.Dispose' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock+Scope", "Dispose").WithLocation(2, 7)); + } + + [Fact] + public void ScopeDisposeMultipleOverloads_01() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock+Scope.Dispose' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock+Scope", "Dispose").WithLocation(2, 7), + // (13,25): error CS0111: Type 'Lock.Scope' already defines a member called 'Dispose' with the same parameter types + // public void Dispose() { } + Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "Dispose").WithArguments("Dispose", "System.Threading.Lock.Scope").WithLocation(13, 25)); + } + + [Fact] + public void ScopeDisposeMultipleOverloads_02() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { System.Console.Write("L"); } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() => Console.Write("D"); + public void Dispose(int x) => Console.Write("X"); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "ELD", verify: Verification.FailsILVerify).VerifyDiagnostics(); + } + + [Fact] + public void ScopeDisposeMultipleOverloads_03() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { System.Console.Write("L"); } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() => Console.Write("D"); + public void Dispose() => Console.Write("T"); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "ELD", verify: Verification.FailsILVerify).VerifyDiagnostics(); + } + + [Fact] + public void InternalLock() + { + var source = """ + static class Program + { + static void Main() + { + System.Threading.Lock l = new(); + lock (l) { System.Console.Write("L"); } + } + } + + namespace System.Threading + { + internal class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() + { + Console.Write("D"); + } + } + } + } + """; + var verifier = CompileAndVerify(source, expectedOutput: "ELD", verify: Verification.FailsILVerify); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void InternalScope() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + internal ref struct Scope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7), + // (8,22): error CS0050: Inconsistent accessibility: return type 'Lock.Scope' is less accessible than method 'Lock.EnterScope()' + // public Scope EnterScope() => new Scope(); + Diagnostic(ErrorCode.ERR_BadVisReturnType, "EnterScope").WithArguments("System.Threading.Lock.EnterScope()", "System.Threading.Lock.Scope").WithLocation(8, 22)); + } + + [Fact] + public void Obsolete_EnterScope() + { + var source = """ + using System; + using System.Threading; + + Lock l = new(); + lock (l) { Console.Write("1"); } + using (l.EnterScope()) { Console.Write("2"); } + + namespace System.Threading + { + public class Lock + { + [System.Obsolete] + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() => Console.Write("D"); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "E1DE2D", verify: Verification.FailsILVerify).VerifyDiagnostics( + // (6,8): warning CS0612: 'Lock.EnterScope()' is obsolete + // using (l.EnterScope()) { Console.Write("2"); } + Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "l.EnterScope()").WithArguments("System.Threading.Lock.EnterScope()").WithLocation(6, 8)); + } + + [Fact] + public void Obsolete_Lock() + { + var source = """ + using System; + using System.Threading; + + Lock l = new(); + lock (l) { Console.Write("1"); } + using (l.EnterScope()) { Console.Write("2"); } + + namespace System.Threading + { + [System.Obsolete] + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() => Console.Write("D"); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "E1DE2D", verify: Verification.FailsILVerify).VerifyDiagnostics( + // (4,1): warning CS0612: 'Lock' is obsolete + // Lock l = new(); + Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "Lock").WithArguments("System.Threading.Lock").WithLocation(4, 1)); + } + + [Fact] + public void Obsolete_Scope() + { + var source = """ + using System; + using System.Threading; + + Lock l = new(); + lock (l) { Console.Write("1"); } + using (l.EnterScope()) { Console.Write("2"); } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + [System.Obsolete] + public ref struct Scope + { + public void Dispose() => Console.Write("D"); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "E1DE2D", verify: Verification.FailsILVerify).VerifyDiagnostics( + // (12,16): warning CS0612: 'Lock.Scope' is obsolete + // public Scope EnterScope() + Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "Scope").WithArguments("System.Threading.Lock.Scope").WithLocation(12, 16), + // (15,24): warning CS0612: 'Lock.Scope' is obsolete + // return new Scope(); + Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "Scope").WithArguments("System.Threading.Lock.Scope").WithLocation(15, 24)); + } + + [Fact] + public void Obsolete_Dispose() + { + var source = """ + using System; + using System.Threading; + + Lock l = new(); + lock (l) { Console.Write("1"); } + using (l.EnterScope()) { Console.Write("2"); } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + [System.Obsolete] + public void Dispose() => Console.Write("D"); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "E1DE2D", verify: Verification.FailsILVerify).VerifyDiagnostics( + // (6,8): warning CS0612: 'Lock.Scope.Dispose()' is obsolete + // using (l.EnterScope()) { Console.Write("2"); } + Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "l.EnterScope()").WithArguments("System.Threading.Lock.Scope.Dispose()").WithLocation(6, 8)); + } + + [Fact] + public void GenericLock() + { + var source = """ + static class Program + { + static void Main() + { + System.Threading.Lock l = new(); + lock (l) { System.Console.Write("L"); } + } + } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() + { + Console.Write("D"); + } + } + } + } + """; + var verifier = CompileAndVerify(source, expectedOutput: "L", verify: Verification.FailsILVerify); + verifier.VerifyDiagnostics(); + // Should use Monitor locking. + verifier.VerifyIL("Program.Main", """ + { + // Code size 39 (0x27) + .maxstack 2 + .locals init (System.Threading.Lock V_0, + bool V_1) + IL_0000: newobj "System.Threading.Lock..ctor()" + IL_0005: stloc.0 + IL_0006: ldc.i4.0 + IL_0007: stloc.1 + .try + { + IL_0008: ldloc.0 + IL_0009: ldloca.s V_1 + IL_000b: call "void System.Threading.Monitor.Enter(object, ref bool)" + IL_0010: ldstr "L" + IL_0015: call "void System.Console.Write(string)" + IL_001a: leave.s IL_0026 + } + finally + { + IL_001c: ldloc.1 + IL_001d: brfalse.s IL_0025 + IL_001f: ldloc.0 + IL_0020: call "void System.Threading.Monitor.Exit(object)" + IL_0025: endfinally + } + IL_0026: ret + } + """); + } + + [Fact] + public void GenericEnterScope() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void GenericScope() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void LockStruct() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public struct Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0185: 'Lock' is not a reference type as required by the lock statement + // lock (l) { } + Diagnostic(ErrorCode.ERR_LockNeedsReference, "l").WithArguments("System.Threading.Lock").WithLocation(2, 7)); + } + + [Theory, CombinatorialData] + public void ScopeRegularStruct(bool implementsIDisposable) + { + var source = $$""" + static class Program + { + static void Main() + { + System.Threading.Lock l = new(); + lock (l) { System.Console.Write("L"); } + } + } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public struct Scope {{(implementsIDisposable ? ": IDisposable" : "")}} + { + public void Dispose() + { + Console.Write("D"); + } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (6,15): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { System.Console.Write("L"); } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(6, 15)); + } + + [Fact] + public void ScopeMisnamed() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public MyScope EnterScope() => new MyScope(); + + public ref struct MyScope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void ScopeNotNested() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + } + + public ref struct Scope + { + public void Dispose() { } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void ScopeClass() + { + var source = """ + System.Threading.Lock l = new(); + lock (l) { } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public class Scope + { + public void Dispose() { } + } + } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) { } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(2, 7)); + } + + [Fact] + public void LockInterface() + { + var source = """ + static class Program + { + static void Main() + { + System.Threading.Lock l = new System.Threading.MyLock(); + lock (l) { System.Console.Write("L"); } + } + } + + namespace System.Threading + { + public interface Lock + { + Scope EnterScope(); + + public ref struct Scope + { + public void Dispose() + { + Console.Write("D"); + } + } + } + + public class MyLock : Lock + { + public Lock.Scope EnterScope() + { + Console.Write("E"); + return new(); + } + } + } + """; + CompileAndVerify(source, expectedOutput: "ELD", verify: Verification.FailsILVerify).VerifyDiagnostics(); + } + + [Fact] + public void LockInterface_DefaultImplementation() + { + var source = """ + static class Program + { + static void Main() + { + System.Threading.Lock l = new System.Threading.MyLock(); + lock (l) { System.Console.Write("L"); } + } + } + + namespace System.Threading + { + public interface Lock + { + Scope EnterScope() + { + Console.Write("I"); + return new(); + } + + public ref struct Scope + { + public void Dispose() + { + Console.Write("D"); + } + } + } + + public class MyLock : Lock + { + public Lock.Scope EnterScope() + { + Console.Write("E"); + return new(); + } + } + } + """; + CompileAndVerify(source, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "ELD" : null, + verify: Verification.Fails, targetFramework: TargetFramework.Net60).VerifyDiagnostics(); + } + + [Fact] + public void LockNested() + { + var source = """ + static class Program + { + static void Main() + { + System.Threading.Container.Lock l = new(); + lock (l) { System.Console.Write("L"); } + } + } + + namespace System.Threading + { + public class Container + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() + { + Console.Write("D"); + } + } + } + } + } + """; + var verifier = CompileAndVerify(source, expectedOutput: "L", verify: Verification.FailsILVerify); + verifier.VerifyDiagnostics(); + // Should use Monitor locking. + verifier.VerifyIL("Program.Main", """ + { + // Code size 39 (0x27) + .maxstack 2 + .locals init (System.Threading.Container.Lock V_0, + bool V_1) + IL_0000: newobj "System.Threading.Container.Lock..ctor()" + IL_0005: stloc.0 + IL_0006: ldc.i4.0 + IL_0007: stloc.1 + .try + { + IL_0008: ldloc.0 + IL_0009: ldloca.s V_1 + IL_000b: call "void System.Threading.Monitor.Enter(object, ref bool)" + IL_0010: ldstr "L" + IL_0015: call "void System.Console.Write(string)" + IL_001a: leave.s IL_0026 + } + finally + { + IL_001c: ldloc.1 + IL_001d: brfalse.s IL_0025 + IL_001f: ldloc.0 + IL_0020: call "void System.Threading.Monitor.Exit(object)" + IL_0025: endfinally + } + IL_0026: ret + } + """); + } + + [Fact] + public void LockInWrongNamespace() + { + var source = """ + static class Program + { + static void Main() + { + Threading.Lock l = new(); + lock (l) { System.Console.Write("L"); } + } + } + + namespace Threading + { + public class Lock + { + public Scope EnterScope() + { + System.Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() + { + System.Console.Write("D"); + } + } + } + } + """; + var verifier = CompileAndVerify(source, expectedOutput: "L", verify: Verification.FailsILVerify); + verifier.VerifyDiagnostics(); + // Should use Monitor locking. + verifier.VerifyIL("Program.Main", """ + { + // Code size 39 (0x27) + .maxstack 2 + .locals init (Threading.Lock V_0, + bool V_1) + IL_0000: newobj "Threading.Lock..ctor()" + IL_0005: stloc.0 + IL_0006: ldc.i4.0 + IL_0007: stloc.1 + .try + { + IL_0008: ldloc.0 + IL_0009: ldloca.s V_1 + IL_000b: call "void System.Threading.Monitor.Enter(object, ref bool)" + IL_0010: ldstr "L" + IL_0015: call "void System.Console.Write(string)" + IL_001a: leave.s IL_0026 + } + finally + { + IL_001c: ldloc.1 + IL_001d: brfalse.s IL_0025 + IL_001f: ldloc.0 + IL_0020: call "void System.Threading.Monitor.Exit(object)" + IL_0025: endfinally + } + IL_0026: ret + } + """); + } + + [Fact] + public void ExternalAssembly() + { + var lib = CreateCompilation(LockTypeDefinition) + .VerifyDiagnostics() + .EmitToImageReference(); + var source = """ + using System; + using System.Threading; + + Lock l = new Lock(); + lock (l) { Console.Write("L"); } + """; + var verifier = CompileAndVerify(source, [lib], expectedOutput: "ELD"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void MultipleLockTypes() + { + var source1 = """ + public static class C1 + { + public static readonly System.Threading.Lock L = new(); + } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E1 "); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() + { + Console.Write("D1 "); + } + } + } + } + """; + var lib1 = CreateCompilation(source1) + .VerifyDiagnostics() + .EmitToImageReference(); + + var source2 = """ + public static class C2 + { + public static readonly System.Threading.Lock L = new(); + } + + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E2 "); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() + { + Console.Write("D2 "); + } + } + } + } + """; + var lib2 = CreateCompilation(source2) + .VerifyDiagnostics() + .EmitToImageReference(); + + var source = """ + using System; + + static class Program + { + static void Main() + { + M1(); + M2(); + } + + static void M1() + { + var l1 = C1.L; + lock (l1) { Console.Write("L1 "); } + } + + static void M2() + { + var l2 = C2.L; + lock (l2) { Console.Write("L2 "); } + } + } + """; + var verifier = CompileAndVerify(source, [lib1, lib2], expectedOutput: "E1 L1 D1 E2 L2 D2"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void LangVersion() + { + var source = """ + using System; + using System.Threading; + + Lock l = new Lock(); + lock (l) { Console.Write("L"); } + """; + + CSharpTestSource sources = [source, LockTypeDefinition]; + + CreateCompilation(sources, parseOptions: TestOptions.Regular12).VerifyDiagnostics( + // (5,7): error CS8652: The feature 'Lock object' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // lock (l) { Console.Write("L"); } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "l").WithArguments("Lock object").WithLocation(5, 7)); + + var expectedOutput = "ELD"; + + CompileAndVerify(sources, parseOptions: TestOptions.RegularPreview, expectedOutput: expectedOutput, + verify: Verification.FailsILVerify).VerifyDiagnostics(); + CompileAndVerify(sources, expectedOutput: expectedOutput, + verify: Verification.FailsILVerify).VerifyDiagnostics(); + } + + [Fact] + public void InPlace() + { + var source = """ + using System; + using System.Threading; + + lock (new Lock()) + { + Console.Write("L"); + } + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, + expectedOutput: "ELD"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void EmbeddedStatement() + { + var source = """ + using System; + using System.Threading; + + lock (new Lock()) Console.Write("L"); + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, + expectedOutput: "ELD"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void EmptyStatement() + { + var source = """ + using System.Threading; + + lock (new Lock()) ; + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, + expectedOutput: "ED"); + verifier.VerifyDiagnostics( + // (3,19): warning CS0642: Possible mistaken empty statement + // lock (new Lock()) ; + Diagnostic(ErrorCode.WRN_PossibleMistakenNullStatement, ";").WithLocation(3, 19)); + } + + [Fact] + public void Nullable_01() + { + var source = """ + #nullable enable + using System; + using System.Threading; + + static class C + { + static void Main() + { + M(new Lock()); + } + + static void M(Lock? l) + { + lock (l) { Console.Write("1"); } + lock (l) { Console.Write("2"); } + } + } + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, + expectedOutput: "E1DE2D"); + verifier.VerifyDiagnostics( + // (14,15): warning CS8602: Dereference of a possibly null reference. + // lock (l) { Console.Write("1"); } + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "l").WithLocation(14, 15)); + } + + [Fact] + public void Nullable_02() + { + var source = """ + #nullable enable + using System.Threading; + + static class C + { + static void Main() + { + M(new Lock()); + } + + static void M(Lock? l) + { + lock (l) + { + l.EnterScope(); + } + } + } + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, + expectedOutput: "EED"); + verifier.VerifyDiagnostics( + // (13,15): warning CS8602: Dereference of a possibly null reference. + // lock (l) + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "l").WithLocation(13, 15)); + } + + [Theory, CombinatorialData] + public void Null([CombinatorialValues("null", "default")] string expr) + { + var source = $$""" + #nullable enable + static class C + { + static void Main() + { + try + { + M(); + } + catch (System.NullReferenceException) + { + System.Console.Write("caught"); + } + } + static void M() + { + lock ((System.Threading.Lock){{expr}}) { } + } + } + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, + expectedOutput: "caught"); + verifier.VerifyDiagnostics( + // (17,15): warning CS8600: Converting null literal or possible null value to non-nullable type. + // lock ((System.Threading.Lock)null) { } + Diagnostic(ErrorCode.WRN_ConvertingNullableToNonNullable, $"(System.Threading.Lock){expr}").WithLocation(17, 15), + // (17,15): warning CS8602: Dereference of a possibly null reference. + // lock ((System.Threading.Lock)null) { } + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, $"(System.Threading.Lock){expr}").WithLocation(17, 15)); + verifier.VerifyIL("C.M", """ + { + // Code size 18 (0x12) + .maxstack 1 + .locals init (System.Threading.Lock.Scope V_0) + IL_0000: ldnull + IL_0001: callvirt "System.Threading.Lock.Scope System.Threading.Lock.EnterScope()" + IL_0006: stloc.0 + .try + { + IL_0007: leave.s IL_0011 + } + finally + { + IL_0009: ldloca.s V_0 + IL_000b: call "void System.Threading.Lock.Scope.Dispose()" + IL_0010: endfinally + } + IL_0011: ret + } + """); + } + + [Fact] + public void Await() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + + lock (new Lock()) + { + await Task.Yield(); + } + """; + CreateCompilation([source, LockTypeDefinition]).VerifyDiagnostics( + // (4,7): error CS9217: A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + // lock (new Lock()) + Diagnostic(ErrorCode.ERR_BadSpecialByRefLock, "new Lock()").WithLocation(4, 7), + // (6,5): error CS1996: Cannot await in the body of a lock statement + // await Task.Yield(); + Diagnostic(ErrorCode.ERR_BadAwaitInLock, "await Task.Yield()").WithLocation(6, 5)); + } + + [Fact] + public void AsyncMethod() + { + var source = """ + #pragma warning disable 1998 // async method lacks 'await' operators + using System.Threading; + + class C + { + async void M() + { + lock (new Lock()) { } + } + } + """; + CreateCompilation([source, LockTypeDefinition]).VerifyDiagnostics( + // (8,15): error CS9217: A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + // lock (new Lock()) { } + Diagnostic(ErrorCode.ERR_BadSpecialByRefLock, "new Lock()").WithLocation(8, 15)); + } + + [Fact] + public void AsyncMethod_WithAwait() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + + class C + { + async void M() + { + await Task.Yield(); + lock (new Lock()) { } + } + } + """; + CreateCompilation([source, LockTypeDefinition]).VerifyDiagnostics( + // (9,15): error CS9217: A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + // lock (new Lock()) { } + Diagnostic(ErrorCode.ERR_BadSpecialByRefLock, "new Lock()").WithLocation(9, 15)); + } + + [Fact] + public void AsyncLocalFunction() + { + var source = """ + #pragma warning disable 1998 // async method lacks 'await' operators + using System.Threading; + + async void local() + { + lock (new Lock()) { } + } + + local(); + """; + CreateCompilation([source, LockTypeDefinition]).VerifyDiagnostics( + // (6,11): error CS9217: A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + // lock (new Lock()) { } + Diagnostic(ErrorCode.ERR_BadSpecialByRefLock, "new Lock()").WithLocation(6, 11)); + } + + [Fact] + public void AsyncLocalFunction_WithAwait() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + + async void local() + { + await Task.Yield(); + lock (new Lock()) { } + } + + local(); + """; + CreateCompilation([source, LockTypeDefinition]).VerifyDiagnostics( + // (7,11): error CS9217: A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + // lock (new Lock()) { } + Diagnostic(ErrorCode.ERR_BadSpecialByRefLock, "new Lock()").WithLocation(7, 11)); + } + + [Fact] + public void AsyncLambda() + { + var source = """ + #pragma warning disable 1998 // async method lacks 'await' operators + using System.Threading; + + var lam = async () => + { + lock (new Lock()) { } + }; + """; + CreateCompilation([source, LockTypeDefinition]).VerifyDiagnostics( + // (6,11): error CS9217: A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + // lock (new Lock()) { } + Diagnostic(ErrorCode.ERR_BadSpecialByRefLock, "new Lock()").WithLocation(6, 11)); + } + + [Fact] + public void AsyncLambda_WithAwait() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + + var lam = async () => + { + await Task.Yield(); + lock (new Lock()) { } + }; + """; + CreateCompilation([source, LockTypeDefinition]).VerifyDiagnostics( + // (7,11): error CS9217: A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + // lock (new Lock()) { } + Diagnostic(ErrorCode.ERR_BadSpecialByRefLock, "new Lock()").WithLocation(7, 11)); + } + + [Fact] + public void Yield() + { + var source = """ + using System.Collections.Generic; + using System.Threading; + + class C + { + IEnumerable M() + { + yield return 1; + lock (new Lock()) + { + yield return 2; + } + yield return 3; + } + } + """; + CreateCompilation([source, LockTypeDefinition]).VerifyEmitDiagnostics( + // (9,15): error CS4013: Instance of type 'Lock.Scope' cannot be used inside a nested function, query expression, iterator block or async method + // lock (new Lock()) + Diagnostic(ErrorCode.ERR_SpecialByRefInLambda, "new Lock()").WithArguments("System.Threading.Lock.Scope").WithLocation(9, 15)); + } + + [Fact] + public void Yield_Async() + { + var source = """ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class C + { + async IAsyncEnumerable M() + { + yield return 1; + lock (new Lock()) + { + yield return 2; + } + await Task.Yield(); + yield return 3; + } + } + """; + CreateCompilationWithTasksExtensions([source, LockTypeDefinition, AsyncStreamsTypes]).VerifyDiagnostics( + // (10,15): error CS9217: A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions. + // lock (new Lock()) + Diagnostic(ErrorCode.ERR_BadSpecialByRefLock, "new Lock()").WithLocation(10, 15)); + } + + [Theory, CombinatorialData] + public void CastToObject([CombinatorialValues("object ", "dynamic")] string type) + { + var source = $$""" + using System; + using System.Threading; + + Lock l = new(); + + {{type}} o = l; + lock (o) { Console.Write("1"); } + + lock (({{type}})l) { Console.Write("2"); } + + lock (l as {{type}}) { Console.Write("3"); } + + o = l as {{type}}; + lock (o) { Console.Write("4"); } + + static {{type}} Cast1(T t) => t; + lock (Cast1(l)) { Console.Write("5"); } + + static {{type}} Cast2(T t) where T : class => t; + lock (Cast2(l)) { Console.Write("6"); } + + static {{type}} Cast3(T t) where T : Lock => t; + lock (Cast3(l)) { Console.Write("7"); } + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, + expectedOutput: "1234567"); + verifier.VerifyDiagnostics( + // 0.cs(6,13): warning CS9216: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // object o = l; + Diagnostic(ErrorCode.WRN_ConvertingLock, "l").WithLocation(6, 13), + // 0.cs(9,16): warning CS9216: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // lock ((object )l) { Console.Write("2"); } + Diagnostic(ErrorCode.WRN_ConvertingLock, "l").WithLocation(9, 16), + // 0.cs(11,7): warning CS9216: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // lock (l as object ) { Console.Write("3"); } + Diagnostic(ErrorCode.WRN_ConvertingLock, "l").WithLocation(11, 7), + // 0.cs(13,5): warning CS9216: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // o = l as object ; + Diagnostic(ErrorCode.WRN_ConvertingLock, "l").WithLocation(13, 5)); + } + + [Fact] + public void CastToSelf() + { + var source = """ + using System; + using System.Threading; + + Lock l = new(); + + Lock o = l; + lock (o) { Console.Write("1"); } + + lock ((Lock)l) { Console.Write("2"); } + + lock (l as Lock) { Console.Write("3"); } + + o = l as Lock; + lock (o) { Console.Write("4"); } + + static Lock Cast1(T t) => (Lock)(object)t; + lock (Cast1(l)) { Console.Write("5"); } + + static Lock Cast2(T t) where T : class => (Lock)(object)t; + lock (Cast2(l)) { Console.Write("6"); } + + static Lock Cast3(T t) where T : Lock => t; + lock (Cast3(l)) { Console.Write("7"); } + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, + expectedOutput: "E1DE2DE3DE4DE5DE6DE7D"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void CommonType() + { + var source = """ + using System; + using System.Threading; + + var array1 = new[] { new Lock(), new Lock() }; + Console.WriteLine(array1.GetType()); + + var array2 = new[] { new Lock(), new object() }; + Console.WriteLine(array2.GetType()); + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, expectedOutput: """ + System.Threading.Lock[] + System.Object[] + """); + verifier.VerifyDiagnostics( + // 0.cs(7,22): warning CS9216: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // var array2 = new[] { new Lock(), new object() }; + Diagnostic(ErrorCode.WRN_ConvertingLock, "new Lock()").WithLocation(7, 22)); + } + + [Theory, CombinatorialData] + public void CastToBase([CombinatorialValues("interface", "class")] string baseKind) + { + var source = $$""" + using System; + using System.Threading; + + static class Program + { + static void Main() + { + M1(); + M2(); + } + + static void M1() + { + ILock l1 = new Lock(); + lock (l1) { Console.Write("1"); } + } + + static void M2() + { + ILock l2 = new Lock(); + lock ((Lock)l2) { Console.Write("2"); } + } + } + + namespace System.Threading + { + public {{baseKind}} ILock { } + + public class Lock : ILock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() + { + Console.Write("D"); + } + } + } + } + """; + var verifier = CompileAndVerify(source, verify: Verification.FailsILVerify, + expectedOutput: "1E2D"); + verifier.VerifyDiagnostics( + // (14,20): warning CS9216: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // ILock l1 = new Lock(); + Diagnostic(ErrorCode.WRN_ConvertingLock, "new Lock()").WithLocation(14, 20), + // (20,20): warning CS9216: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // ILock l2 = new Lock(); + Diagnostic(ErrorCode.WRN_ConvertingLock, "new Lock()").WithLocation(20, 20)); + // Should use Monitor locking. + verifier.VerifyIL("Program.M1", """ + { + // Code size 39 (0x27) + .maxstack 2 + .locals init (System.Threading.ILock V_0, + bool V_1) + IL_0000: newobj "System.Threading.Lock..ctor()" + IL_0005: stloc.0 + IL_0006: ldc.i4.0 + IL_0007: stloc.1 + .try + { + IL_0008: ldloc.0 + IL_0009: ldloca.s V_1 + IL_000b: call "void System.Threading.Monitor.Enter(object, ref bool)" + IL_0010: ldstr "1" + IL_0015: call "void System.Console.Write(string)" + IL_001a: leave.s IL_0026 + } + finally + { + IL_001c: ldloc.1 + IL_001d: brfalse.s IL_0025 + IL_001f: ldloc.0 + IL_0020: call "void System.Threading.Monitor.Exit(object)" + IL_0025: endfinally + } + IL_0026: ret + } + """); + } + + [Fact] + public void DerivedLock() + { + var source = """ + using System; + using System.Threading; + + static class Program + { + static void Main() + { + var l1 = M1(); + var l2 = M2(l1); + M3(l2); + M4(l2); + } + + static DerivedLock M1() + { + DerivedLock l1 = new DerivedLock(); + lock (l1) { Console.Write("1"); } + return l1; + } + + static Lock M2(DerivedLock l1) + { + Lock l2 = l1; + lock (l2) { Console.Write("2"); } + return l2; + } + + static void M3(Lock l2) + { + DerivedLock l3 = (DerivedLock)l2; + lock (l3) { Console.Write("3"); } + } + + static void M4(Lock l2) + { + IDerivedLock l4 = (IDerivedLock)l2; + lock (l4) { Console.Write("4"); } + } + } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() + { + Console.Write("E"); + return new Scope(); + } + + public ref struct Scope + { + public void Dispose() + { + Console.Write("D"); + } + } + } + + public class DerivedLock : Lock, IDerivedLock { } + + public interface IDerivedLock { } + } + """; + var verifier = CompileAndVerify(source, verify: Verification.FailsILVerify, + expectedOutput: "1E2D34"); + verifier.VerifyDiagnostics( + // (30,39): warning CS9216: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // DerivedLock l3 = (DerivedLock)l2; + Diagnostic(ErrorCode.WRN_ConvertingLock, "l2").WithLocation(30, 39), + // (36,41): warning CS9216: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // IDerivedLock l4 = (IDerivedLock)l2; + Diagnostic(ErrorCode.WRN_ConvertingLock, "l2").WithLocation(36, 41)); + // Should use Monitor locking. + verifier.VerifyIL("Program.M1", """ + { + // Code size 42 (0x2a) + .maxstack 2 + .locals init (System.Threading.DerivedLock V_0, //l1 + System.Threading.DerivedLock V_1, + bool V_2) + IL_0000: newobj "System.Threading.DerivedLock..ctor()" + IL_0005: stloc.0 + IL_0006: ldloc.0 + IL_0007: stloc.1 + IL_0008: ldc.i4.0 + IL_0009: stloc.2 + .try + { + IL_000a: ldloc.1 + IL_000b: ldloca.s V_2 + IL_000d: call "void System.Threading.Monitor.Enter(object, ref bool)" + IL_0012: ldstr "1" + IL_0017: call "void System.Console.Write(string)" + IL_001c: leave.s IL_0028 + } + finally + { + IL_001e: ldloc.2 + IL_001f: brfalse.s IL_0027 + IL_0021: ldloc.1 + IL_0022: call "void System.Threading.Monitor.Exit(object)" + IL_0027: endfinally + } + IL_0028: ldloc.0 + IL_0029: ret + } + """); + // Should use Monitor locking. + verifier.VerifyIL("Program.M3", """ + { + // Code size 40 (0x28) + .maxstack 2 + .locals init (System.Threading.DerivedLock V_0, + bool V_1) + IL_0000: ldarg.0 + IL_0001: castclass "System.Threading.DerivedLock" + IL_0006: stloc.0 + IL_0007: ldc.i4.0 + IL_0008: stloc.1 + .try + { + IL_0009: ldloc.0 + IL_000a: ldloca.s V_1 + IL_000c: call "void System.Threading.Monitor.Enter(object, ref bool)" + IL_0011: ldstr "3" + IL_0016: call "void System.Console.Write(string)" + IL_001b: leave.s IL_0027 + } + finally + { + IL_001d: ldloc.1 + IL_001e: brfalse.s IL_0026 + IL_0020: ldloc.0 + IL_0021: call "void System.Threading.Monitor.Exit(object)" + IL_0026: endfinally + } + IL_0027: ret + } + """); + // Should use Monitor locking. + verifier.VerifyIL("Program.M4", """ + { + // Code size 40 (0x28) + .maxstack 2 + .locals init (System.Threading.IDerivedLock V_0, + bool V_1) + IL_0000: ldarg.0 + IL_0001: castclass "System.Threading.IDerivedLock" + IL_0006: stloc.0 + IL_0007: ldc.i4.0 + IL_0008: stloc.1 + .try + { + IL_0009: ldloc.0 + IL_000a: ldloca.s V_1 + IL_000c: call "void System.Threading.Monitor.Enter(object, ref bool)" + IL_0011: ldstr "4" + IL_0016: call "void System.Console.Write(string)" + IL_001b: leave.s IL_0027 + } + finally + { + IL_001d: ldloc.1 + IL_001e: brfalse.s IL_0026 + IL_0020: ldloc.0 + IL_0021: call "void System.Threading.Monitor.Exit(object)" + IL_0026: endfinally + } + IL_0027: ret + } + """); + } + + [Fact] + public void Downcast() + { + var source = """ + using System; + using System.Threading; + + object o = new Lock(); + lock ((Lock)o) { Console.Write("L"); } + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, + expectedOutput: "ELD"); + verifier.VerifyDiagnostics( + // 0.cs(4,12): warning CS9216: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // object o = new Lock(); + Diagnostic(ErrorCode.WRN_ConvertingLock, "new Lock()").WithLocation(4, 12)); + } + + [Fact] + public void OtherConversions() + { + var source = """ + #nullable enable + using System.Threading; + + I i = new Lock(); + Lock? l = new Lock(); + C c = new Lock(); + D d = new Lock(); + d = (D)new Lock(); + + interface I { } + + class C + { + public static implicit operator C(Lock l) => new C(); + } + + class D + { + public static explicit operator D(Lock l) => new D(); + } + """; + // No warnings about converting `Lock` expected. + CreateCompilation([source, LockTypeDefinition]).VerifyDiagnostics( + // 0.cs(4,7): error CS0266: Cannot implicitly convert type 'System.Threading.Lock' to 'I'. An explicit conversion exists (are you missing a cast?) + // I i = new Lock(); + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "new Lock()").WithArguments("System.Threading.Lock", "I").WithLocation(4, 7), + // 0.cs(7,7): error CS0266: Cannot implicitly convert type 'System.Threading.Lock' to 'D'. An explicit conversion exists (are you missing a cast?) + // D d = new Lock(); + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "new Lock()").WithArguments("System.Threading.Lock", "D").WithLocation(7, 7)); + } + + [Theory] + [InlineData("")] + [InlineData("where T : class")] + [InlineData("where T : Lock")] + public void GenericParameter(string constraint) + { + var source = $$""" + using System; + using System.Threading; + + C.M(new Lock()); + + static class C + { + public static void M(T t) {{constraint}} + { + lock (t) { Console.Write("L"); } + } + } + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, + expectedOutput: "L"); + verifier.VerifyDiagnostics(); + // Should use Monitor locking. + verifier.VerifyIL("C.M", """ + { + // Code size 40 (0x28) + .maxstack 2 + .locals init (object V_0, + bool V_1) + IL_0000: ldarg.0 + IL_0001: box "T" + IL_0006: stloc.0 + IL_0007: ldc.i4.0 + IL_0008: stloc.1 + .try + { + IL_0009: ldloc.0 + IL_000a: ldloca.s V_1 + IL_000c: call "void System.Threading.Monitor.Enter(object, ref bool)" + IL_0011: ldstr "L" + IL_0016: call "void System.Console.Write(string)" + IL_001b: leave.s IL_0027 + } + finally + { + IL_001d: ldloc.1 + IL_001e: brfalse.s IL_0026 + IL_0020: ldloc.0 + IL_0021: call "void System.Threading.Monitor.Exit(object)" + IL_0026: endfinally + } + IL_0027: ret + } + """); + } + + [Fact] + public void GenericParameter_Object() + { + var source = """ + using System; + using System.Threading; + + C.M(new Lock()); + + static class C + { + public static void M(T t) + { + lock (t) { Console.Write("L"); } + } + } + """; + var verifier = CompileAndVerify([source, LockTypeDefinition], verify: Verification.FailsILVerify, + expectedOutput: "L"); + verifier.VerifyDiagnostics( + // (4,13): warning CS9216: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // C.M(new Lock()); + Diagnostic(ErrorCode.WRN_ConvertingLock, "new Lock()").WithLocation(4, 13)); + // Should use Monitor locking. + verifier.VerifyIL("C.M", """ + { + // Code size 40 (0x28) + .maxstack 2 + .locals init (object V_0, + bool V_1) + IL_0000: ldarg.0 + IL_0001: box "T" + IL_0006: stloc.0 + IL_0007: ldc.i4.0 + IL_0008: stloc.1 + .try + { + IL_0009: ldloc.0 + IL_000a: ldloca.s V_1 + IL_000c: call "void System.Threading.Monitor.Enter(object, ref bool)" + IL_0011: ldstr "L" + IL_0016: call "void System.Console.Write(string)" + IL_001b: leave.s IL_0027 + } + finally + { + IL_001d: ldloc.1 + IL_001e: brfalse.s IL_0026 + IL_0020: ldloc.0 + IL_0021: call "void System.Threading.Monitor.Exit(object)" + IL_0026: endfinally + } + IL_0027: ret + } + """); + } + + [Fact] + public void UseSiteError_EnterScope() + { + // namespace System.Threading + // { + // public class Lock + // { + // [System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute("Test")] + // public Scope EnterScope() => throw null; + // + // public ref struct Scope + // { + // public void Dispose() { } + // } + // } + // } + var ilSource = """ + .class public auto ansi sealed beforefieldinit System.Threading.Lock extends System.Object + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ret + } + + .method public hidebysig instance class System.Threading.Lock/Scope EnterScope () cil managed + { + .custom instance void System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute::.ctor(string) = ( + 01 00 04 54 65 73 74 00 00 + ) + .maxstack 8 + ldnull + throw + } + + .class nested public sequential ansi sealed beforefieldinit Scope extends System.ValueType + { + .custom instance void System.Runtime.CompilerServices.IsByRefLikeAttribute::.ctor() = ( + 01 00 00 00 + ) + + .pack 0 + .size 1 + + .method public hidebysig instance void Dispose () cil managed + { + .maxstack 8 + ret + } + } + } + + .class public auto ansi sealed beforefieldinit System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute extends System.Object + { + .method public hidebysig specialname rtspecialname instance void .ctor(string s) cil managed + { + .maxstack 8 + ret + } + } + + .class public auto ansi sealed beforefieldinit System.Runtime.CompilerServices.IsByRefLikeAttribute extends System.Object + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ret + } + } + """; + var source = """ + class C + { + void M(System.Threading.Lock l) + { + lock (l) { } + } + } + """; + CreateCompilationWithIL(source, ilSource).VerifyDiagnostics( + // (5,15): error CS9041: 'Lock.EnterScope()' requires compiler feature 'Test', which is not supported by this version of the C# compiler. + // lock (l) { } + Diagnostic(ErrorCode.ERR_UnsupportedCompilerFeature, "l").WithArguments("System.Threading.Lock.EnterScope()", "Test").WithLocation(5, 15)); + } + + [Fact] + public void UseSiteError_Scope() + { + // namespace System.Threading + // { + // public class Lock + // { + // public Scope EnterScope() => throw null; + // + // [System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute("Test")] + // public ref struct Scope + // { + // public void Dispose() { } + // } + // } + // } + var ilSource = """ + .class public auto ansi sealed beforefieldinit System.Threading.Lock extends System.Object + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ret + } + + .method public hidebysig instance class System.Threading.Lock/Scope EnterScope () cil managed + { + .maxstack 8 + ldnull + throw + } + + .class nested public sequential ansi sealed beforefieldinit Scope extends System.ValueType + { + .custom instance void System.Runtime.CompilerServices.IsByRefLikeAttribute::.ctor() = ( + 01 00 00 00 + ) + + .custom instance void System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute::.ctor(string) = ( + 01 00 04 54 65 73 74 00 00 + ) + + .pack 0 + .size 1 + + .method public hidebysig instance void Dispose () cil managed + { + .maxstack 8 + ret + } + } + } + + .class public auto ansi sealed beforefieldinit System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute extends System.Object + { + .method public hidebysig specialname rtspecialname instance void .ctor(string s) cil managed + { + .maxstack 8 + ret + } + } + + .class public auto ansi sealed beforefieldinit System.Runtime.CompilerServices.IsByRefLikeAttribute extends System.Object + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ret + } + } + """; + var source = """ + class C + { + void M(System.Threading.Lock l) + { + lock (l) { } + } + } + """; + CreateCompilationWithIL(source, ilSource).VerifyDiagnostics( + // (5,15): error CS9041: 'Lock.Scope' requires compiler feature 'Test', which is not supported by this version of the C# compiler. + // lock (l) { } + Diagnostic(ErrorCode.ERR_UnsupportedCompilerFeature, "l").WithArguments("System.Threading.Lock.Scope", "Test").WithLocation(5, 15)); + } + + [Fact] + public void UseSiteError_Dispose() + { + // namespace System.Threading + // { + // public class Lock + // { + // public Scope EnterScope() => throw null; + // + // public ref struct Scope + // { + // [System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute("Test")] + // public void Dispose() { } + // } + // } + // } + var ilSource = """ + .class public auto ansi sealed beforefieldinit System.Threading.Lock extends System.Object + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ret + } + + .method public hidebysig instance class System.Threading.Lock/Scope EnterScope () cil managed + { + .maxstack 8 + ldnull + throw + } + + .class nested public sequential ansi sealed beforefieldinit Scope extends System.ValueType + { + .custom instance void System.Runtime.CompilerServices.IsByRefLikeAttribute::.ctor() = ( + 01 00 00 00 + ) + + .pack 0 + .size 1 + + .method public hidebysig instance void Dispose () cil managed + { + .custom instance void System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute::.ctor(string) = ( + 01 00 04 54 65 73 74 00 00 + ) + .maxstack 8 + ret + } + } + } + + .class public auto ansi sealed beforefieldinit System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute extends System.Object + { + .method public hidebysig specialname rtspecialname instance void .ctor(string s) cil managed + { + .maxstack 8 + ret + } + } + + .class public auto ansi sealed beforefieldinit System.Runtime.CompilerServices.IsByRefLikeAttribute extends System.Object + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ret + } + } + """; + var source = """ + class C + { + void M(System.Threading.Lock l) + { + lock (l) { } + } + } + """; + CreateCompilationWithIL(source, ilSource).VerifyDiagnostics( + // (5,15): error CS9041: 'Lock.Scope.Dispose()' requires compiler feature 'Test', which is not supported by this version of the C# compiler. + // lock (l) { } + Diagnostic(ErrorCode.ERR_UnsupportedCompilerFeature, "l").WithArguments("System.Threading.Lock.Scope.Dispose()", "Test").WithLocation(5, 15)); + } +} diff --git a/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_ILockStatement.cs b/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_ILockStatement.cs index a63d47c28e7b1..4843bb085fcf0 100644 --- a/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_ILockStatement.cs +++ b/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_ILockStatement.cs @@ -13,6 +13,21 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests { public class IOperationTests_ILockStatement : SemanticModelTestBase { + private const string LockTypeDefinition = """ + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + } + } + } + """; + [CompilerTrait(CompilerFeature.IOperation)] [Fact] public void ILockStatement_ObjectLock_FieldReference() @@ -378,6 +393,153 @@ public void M() VerifyOperationTreeAndDiagnosticsForTest(source, expectedOperationTree, expectedDiagnostics); } + [Fact, CompilerTrait(CompilerFeature.IOperation)] + public void ILockStatement_LockObject() + { + var source = """ + using System; + using System.Threading; + + Lock l = new Lock(); + /**/lock (l) + { + }/**/ + """; + + var expectedOperationTree = """ + ILockOperation (OperationKind.Lock, Type: null) (Syntax: 'lock (l) ... }') + Expression: + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock) (Syntax: 'l') + Body: + IBlockOperation (0 statements) (OperationKind.Block, Type: null) (Syntax: '{ ... }') + """; + + var expectedDiagnostics = DiagnosticDescription.None; + + VerifyOperationTreeAndDiagnosticsForTest([source, LockTypeDefinition], expectedOperationTree, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation)] + public void ILockStatement_LockObject_MissingEnterScope() + { + var source = """ + using System; + using System.Threading; + + Lock l = new Lock(); + /**/lock (l) + { + }/**/ + + namespace System.Threading + { + public class Lock { } + } + """; + + var expectedOperationTree = """ + ILockOperation (OperationKind.Lock, Type: null, IsInvalid) (Syntax: 'lock (l) ... }') + Expression: + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock, IsInvalid) (Syntax: 'l') + Body: + IBlockOperation (0 statements) (OperationKind.Block, Type: null) (Syntax: '{ ... }') + """; + + var expectedDiagnostics = new[] + { + // (5,17): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // /**/lock (l) + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(5, 17) + }; + + VerifyOperationTreeAndDiagnosticsForTest(source, expectedOperationTree, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation)] + public void ILockStatement_LockObject_MissingScopeDispose() + { + var source = """ + using System; + using System.Threading; + + Lock l = new Lock(); + /**/lock (l) + { + }/**/ + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope { } + } + } + """; + + var expectedOperationTree = """ + ILockOperation (OperationKind.Lock, Type: null, IsInvalid) (Syntax: 'lock (l) ... }') + Expression: + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock, IsInvalid) (Syntax: 'l') + Body: + IBlockOperation (0 statements) (OperationKind.Block, Type: null) (Syntax: '{ ... }') + """; + + var expectedDiagnostics = new[] + { + // (5,17): error CS0656: Missing compiler required member 'System.Threading.Lock+Scope.Dispose' + // /**/lock (l) + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock+Scope", "Dispose").WithLocation(5, 17) + }; + + VerifyOperationTreeAndDiagnosticsForTest(source, expectedOperationTree, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation)] + public void ILockStatement_LockObject_EnterScopeVirtual() + { + var source = """ + using System; + using System.Threading; + + Lock l = new LockDerived(); + /**/lock (l) + { + }/**/ + + namespace System.Threading + { + public class Lock + { + public virtual Scope EnterScope() => new(); + + public ref struct Scope + { + public void Dispose() { } + } + } + + public class LockDerived : Lock + { + public override Scope EnterScope() => new(); + } + } + """; + + var expectedOperationTree = """ + ILockOperation (OperationKind.Lock, Type: null) (Syntax: 'lock (l) ... }') + Expression: + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock) (Syntax: 'l') + Body: + IBlockOperation (0 statements) (OperationKind.Block, Type: null) (Syntax: '{ ... }') + """; + + var expectedDiagnostics = DiagnosticDescription.None; + + VerifyOperationTreeAndDiagnosticsForTest(source, expectedOperationTree, expectedDiagnostics); + } + [CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)] [Fact] public void LockFlow_01() @@ -1025,5 +1187,966 @@ void M(P input, bool b) VerifyFlowGraphAndDiagnosticsForTest(compilation, expectedGraph, expectedDiagnostics); } + + [Fact, CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)] + public void LockFlow_LockObject() + { + var source = """ + using System; + using System.Threading; + + class C + { + void M() + /**/{ + Lock l = new Lock(); + lock (l) + { + } + }/**/ + } + """; + + var expectedFlowGraph = """ + Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} + .locals {R1} + { + Locals: [System.Threading.Lock l] + Block[B1] - Block + Predecessors: [B0] + Statements (1) + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Left: + ILocalReferenceOperation: l (IsDeclaration: True) (OperationKind.LocalReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Right: + IObjectCreationOperation (Constructor: System.Threading.Lock..ctor()) (OperationKind.ObjectCreation, Type: System.Threading.Lock) (Syntax: 'new Lock()') + Arguments(0) + Initializer: + null + Next (Regular) Block[B2] + Entering: {R2} + .locals {R2} + { + CaptureIds: [0] + Block[B2] - Block + Predecessors: [B1] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'l') + Value: + IInvocationOperation ( System.Threading.Lock.Scope System.Threading.Lock.EnterScope()) (OperationKind.Invocation, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'l') + Instance Receiver: + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock) (Syntax: 'l') + Arguments(0) + Next (Regular) Block[B3] + Entering: {R3} {R4} + .try {R3, R4} + { + Block[B3] - Block + Predecessors: [B2] + Statements (0) + Next (Regular) Block[B5] + Finalizing: {R5} + Leaving: {R4} {R3} {R2} {R1} + } + .finally {R5} + { + Block[B4] - Block + Predecessors (0) + Statements (1) + IInvocationOperation ( void System.Threading.Lock.Scope.Dispose()) (OperationKind.Invocation, Type: System.Void, IsImplicit) (Syntax: 'l') + Instance Receiver: + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'l') + Arguments(0) + Next (StructuredExceptionHandling) Block[null] + } + } + } + Block[B5] - Exit + Predecessors: [B3] + Statements (0) + """; + + var expectedDiagnostics = DiagnosticDescription.None; + + VerifyFlowGraphAndDiagnosticsForTest([source, LockTypeDefinition], expectedFlowGraph, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)] + public void LockFlow_LockObject_NonEmptyBody() + { + var source = """ + using System; + using System.Threading; + + class C + { + void M() + /**/{ + Lock l = new Lock(); + lock (l) + { + Console.Write("Body"); + } + }/**/ + } + """; + + var expectedFlowGraph = """ + Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} + .locals {R1} + { + Locals: [System.Threading.Lock l] + Block[B1] - Block + Predecessors: [B0] + Statements (1) + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Left: + ILocalReferenceOperation: l (IsDeclaration: True) (OperationKind.LocalReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Right: + IObjectCreationOperation (Constructor: System.Threading.Lock..ctor()) (OperationKind.ObjectCreation, Type: System.Threading.Lock) (Syntax: 'new Lock()') + Arguments(0) + Initializer: + null + Next (Regular) Block[B2] + Entering: {R2} + .locals {R2} + { + CaptureIds: [0] + Block[B2] - Block + Predecessors: [B1] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'l') + Value: + IInvocationOperation ( System.Threading.Lock.Scope System.Threading.Lock.EnterScope()) (OperationKind.Invocation, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'l') + Instance Receiver: + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock) (Syntax: 'l') + Arguments(0) + Next (Regular) Block[B3] + Entering: {R3} {R4} + .try {R3, R4} + { + Block[B3] - Block + Predecessors: [B2] + Statements (1) + IExpressionStatementOperation (OperationKind.ExpressionStatement, Type: null) (Syntax: 'Console.Write("Body");') + Expression: + IInvocationOperation (void System.Console.Write(System.String value)) (OperationKind.Invocation, Type: System.Void) (Syntax: 'Console.Write("Body")') + Instance Receiver: + null + Arguments(1): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: value) (OperationKind.Argument, Type: null) (Syntax: '"Body"') + ILiteralOperation (OperationKind.Literal, Type: System.String, Constant: "Body") (Syntax: '"Body"') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Next (Regular) Block[B5] + Finalizing: {R5} + Leaving: {R4} {R3} {R2} {R1} + } + .finally {R5} + { + Block[B4] - Block + Predecessors (0) + Statements (1) + IInvocationOperation ( void System.Threading.Lock.Scope.Dispose()) (OperationKind.Invocation, Type: System.Void, IsImplicit) (Syntax: 'l') + Instance Receiver: + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'l') + Arguments(0) + Next (StructuredExceptionHandling) Block[null] + } + } + } + Block[B5] - Exit + Predecessors: [B3] + Statements (0) + """; + + var expectedDiagnostics = DiagnosticDescription.None; + + VerifyFlowGraphAndDiagnosticsForTest([source, LockTypeDefinition], expectedFlowGraph, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)] + public void LockFlow_LockObject_ConditionalBody() + { + var source = """ + using System; + using System.Threading; + + class C + { + void M(bool b) + /**/{ + Lock l = new Lock(); + lock (l) + { + if (b) + { + Console.Write("Body"); + } + else + { + Console.Write("Else"); + } + } + }/**/ + } + """; + + var expectedFlowGraph = """ + Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} + .locals {R1} + { + Locals: [System.Threading.Lock l] + Block[B1] - Block + Predecessors: [B0] + Statements (1) + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Left: + ILocalReferenceOperation: l (IsDeclaration: True) (OperationKind.LocalReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Right: + IObjectCreationOperation (Constructor: System.Threading.Lock..ctor()) (OperationKind.ObjectCreation, Type: System.Threading.Lock) (Syntax: 'new Lock()') + Arguments(0) + Initializer: + null + Next (Regular) Block[B2] + Entering: {R2} + .locals {R2} + { + CaptureIds: [0] + Block[B2] - Block + Predecessors: [B1] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'l') + Value: + IInvocationOperation ( System.Threading.Lock.Scope System.Threading.Lock.EnterScope()) (OperationKind.Invocation, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'l') + Instance Receiver: + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock) (Syntax: 'l') + Arguments(0) + Next (Regular) Block[B3] + Entering: {R3} {R4} + .try {R3, R4} + { + Block[B3] - Block + Predecessors: [B2] + Statements (0) + Jump if False (Regular) to Block[B5] + IParameterReferenceOperation: b (OperationKind.ParameterReference, Type: System.Boolean) (Syntax: 'b') + Next (Regular) Block[B4] + Block[B4] - Block + Predecessors: [B3] + Statements (1) + IExpressionStatementOperation (OperationKind.ExpressionStatement, Type: null) (Syntax: 'Console.Write("Body");') + Expression: + IInvocationOperation (void System.Console.Write(System.String value)) (OperationKind.Invocation, Type: System.Void) (Syntax: 'Console.Write("Body")') + Instance Receiver: + null + Arguments(1): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: value) (OperationKind.Argument, Type: null) (Syntax: '"Body"') + ILiteralOperation (OperationKind.Literal, Type: System.String, Constant: "Body") (Syntax: '"Body"') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Next (Regular) Block[B7] + Finalizing: {R5} + Leaving: {R4} {R3} {R2} {R1} + Block[B5] - Block + Predecessors: [B3] + Statements (1) + IExpressionStatementOperation (OperationKind.ExpressionStatement, Type: null) (Syntax: 'Console.Write("Else");') + Expression: + IInvocationOperation (void System.Console.Write(System.String value)) (OperationKind.Invocation, Type: System.Void) (Syntax: 'Console.Write("Else")') + Instance Receiver: + null + Arguments(1): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: value) (OperationKind.Argument, Type: null) (Syntax: '"Else"') + ILiteralOperation (OperationKind.Literal, Type: System.String, Constant: "Else") (Syntax: '"Else"') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Next (Regular) Block[B7] + Finalizing: {R5} + Leaving: {R4} {R3} {R2} {R1} + } + .finally {R5} + { + Block[B6] - Block + Predecessors (0) + Statements (1) + IInvocationOperation ( void System.Threading.Lock.Scope.Dispose()) (OperationKind.Invocation, Type: System.Void, IsImplicit) (Syntax: 'l') + Instance Receiver: + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'l') + Arguments(0) + Next (StructuredExceptionHandling) Block[null] + } + } + } + Block[B7] - Exit + Predecessors: [B4] [B5] + Statements (0) + """; + + var expectedDiagnostics = DiagnosticDescription.None; + + VerifyFlowGraphAndDiagnosticsForTest([source, LockTypeDefinition], expectedFlowGraph, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)] + public void LockFlow_LockObject_Conditional() + { + var source = """ + using System; + using System.Threading; + + class C + { + void M(bool b, Lock l1, Lock l2) + /**/{ + lock (b ? l1 : l2) + { + } + }/**/ + } + """; + + var expectedFlowGraph = """ + Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} {R2} + .locals {R1} + { + CaptureIds: [1] + .locals {R2} + { + CaptureIds: [0] + Block[B1] - Block + Predecessors: [B0] + Statements (0) + Jump if False (Regular) to Block[B3] + IParameterReferenceOperation: b (OperationKind.ParameterReference, Type: System.Boolean) (Syntax: 'b') + Next (Regular) Block[B2] + Block[B2] - Block + Predecessors: [B1] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'l1') + Value: + IParameterReferenceOperation: l1 (OperationKind.ParameterReference, Type: System.Threading.Lock) (Syntax: 'l1') + Next (Regular) Block[B4] + Block[B3] - Block + Predecessors: [B1] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'l2') + Value: + IParameterReferenceOperation: l2 (OperationKind.ParameterReference, Type: System.Threading.Lock) (Syntax: 'l2') + Next (Regular) Block[B4] + Block[B4] - Block + Predecessors: [B2] [B3] + Statements (1) + IFlowCaptureOperation: 1 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'b ? l1 : l2') + Value: + IInvocationOperation ( System.Threading.Lock.Scope System.Threading.Lock.EnterScope()) (OperationKind.Invocation, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'b ? l1 : l2') + Instance Receiver: + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'b ? l1 : l2') + Arguments(0) + Next (Regular) Block[B5] + Leaving: {R2} + Entering: {R3} {R4} + } + .try {R3, R4} + { + Block[B5] - Block + Predecessors: [B4] + Statements (0) + Next (Regular) Block[B7] + Finalizing: {R5} + Leaving: {R4} {R3} {R1} + } + .finally {R5} + { + Block[B6] - Block + Predecessors (0) + Statements (1) + IInvocationOperation ( void System.Threading.Lock.Scope.Dispose()) (OperationKind.Invocation, Type: System.Void, IsImplicit) (Syntax: 'b ? l1 : l2') + Instance Receiver: + IFlowCaptureReferenceOperation: 1 (OperationKind.FlowCaptureReference, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'b ? l1 : l2') + Arguments(0) + Next (StructuredExceptionHandling) Block[null] + } + } + Block[B7] - Exit + Predecessors: [B5] + Statements (0) + """; + + var expectedDiagnostics = DiagnosticDescription.None; + + VerifyFlowGraphAndDiagnosticsForTest([source, LockTypeDefinition], expectedFlowGraph, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)] + public void LockFlow_LockObject_Conditional_Object() + { + var source = """ + using System; + using System.Threading; + + class C + { + void M(bool b, object o) + /**/{ + Lock l = new Lock(); + lock (b ? l : o) + { + } + }/**/ + } + """; + + var expectedFlowGraph = """ + Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} + .locals {R1} + { + Locals: [System.Threading.Lock l] + Block[B1] - Block + Predecessors: [B0] + Statements (1) + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Left: + ILocalReferenceOperation: l (IsDeclaration: True) (OperationKind.LocalReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Right: + IObjectCreationOperation (Constructor: System.Threading.Lock..ctor()) (OperationKind.ObjectCreation, Type: System.Threading.Lock) (Syntax: 'new Lock()') + Arguments(0) + Initializer: + null + Next (Regular) Block[B2] + Entering: {R2} + .locals {R2} + { + Locals: [System.Boolean ?] + CaptureIds: [0] + Block[B2] - Block + Predecessors: [B1] + Statements (0) + Jump if False (Regular) to Block[B4] + IParameterReferenceOperation: b (OperationKind.ParameterReference, Type: System.Boolean) (Syntax: 'b') + Next (Regular) Block[B3] + Block[B3] - Block + Predecessors: [B2] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'l') + Value: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'l') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: True, IsUserDefined: False) (MethodSymbol: null) + (ImplicitReference) + Operand: + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock) (Syntax: 'l') + Next (Regular) Block[B5] + Entering: {R3} {R4} + Block[B4] - Block + Predecessors: [B2] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'o') + Value: + IParameterReferenceOperation: o (OperationKind.ParameterReference, Type: System.Object) (Syntax: 'o') + Next (Regular) Block[B5] + Entering: {R3} {R4} + .try {R3, R4} + { + Block[B5] - Block + Predecessors: [B3] [B4] + Statements (1) + IInvocationOperation (void System.Threading.Monitor.Enter(System.Object obj, ref System.Boolean lockTaken)) (OperationKind.Invocation, Type: System.Void, IsImplicit) (Syntax: 'b ? l : o') + Instance Receiver: + null + Arguments(2): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: obj) (OperationKind.Argument, Type: null, IsImplicit) (Syntax: 'b ? l : o') + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: System.Object, IsImplicit) (Syntax: 'b ? l : o') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: lockTaken) (OperationKind.Argument, Type: null, IsImplicit) (Syntax: 'b ? l : o') + ILocalReferenceOperation: (IsDeclaration: True) (OperationKind.LocalReference, Type: System.Boolean, IsImplicit) (Syntax: 'b ? l : o') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Next (Regular) Block[B9] + Finalizing: {R5} + Leaving: {R4} {R3} {R2} {R1} + } + .finally {R5} + { + Block[B6] - Block + Predecessors (0) + Statements (0) + Jump if False (Regular) to Block[B8] + ILocalReferenceOperation: (OperationKind.LocalReference, Type: System.Boolean, IsImplicit) (Syntax: 'b ? l : o') + Next (Regular) Block[B7] + Block[B7] - Block + Predecessors: [B6] + Statements (1) + IInvocationOperation (void System.Threading.Monitor.Exit(System.Object obj)) (OperationKind.Invocation, Type: System.Void, IsImplicit) (Syntax: 'b ? l : o') + Instance Receiver: + null + Arguments(1): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: obj) (OperationKind.Argument, Type: null, IsImplicit) (Syntax: 'b ? l : o') + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: System.Object, IsImplicit) (Syntax: 'b ? l : o') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Next (Regular) Block[B8] + Block[B8] - Block + Predecessors: [B6] [B7] + Statements (0) + Next (StructuredExceptionHandling) Block[null] + } + } + } + Block[B9] - Exit + Predecessors: [B5] + Statements (0) + """; + + var expectedDiagnostics = new[] + { + // (9,19): warning CS9214: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement. + // lock (b ? l : o) + Diagnostic(ErrorCode.WRN_ConvertingLock, "l").WithLocation(9, 19) + }; + + VerifyFlowGraphAndDiagnosticsForTest([source, LockTypeDefinition], expectedFlowGraph, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)] + public void LockFlow_LockObject_Coalesce() + { + var source = """ + using System; + using System.Threading; + + class C + { + void M(Lock l1, Lock l2) + /**/{ + lock (l1 ?? l2) + { + } + }/**/ + } + """; + + var expectedFlowGraph = """ + Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} {R2} {R3} + .locals {R1} + { + CaptureIds: [2] + .locals {R2} + { + CaptureIds: [1] + .locals {R3} + { + CaptureIds: [0] + Block[B1] - Block + Predecessors: [B0] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'l1') + Value: + IParameterReferenceOperation: l1 (OperationKind.ParameterReference, Type: System.Threading.Lock) (Syntax: 'l1') + Jump if True (Regular) to Block[B3] + IIsNullOperation (OperationKind.IsNull, Type: System.Boolean, IsImplicit) (Syntax: 'l1') + Operand: + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l1') + Leaving: {R3} + Next (Regular) Block[B2] + Block[B2] - Block + Predecessors: [B1] + Statements (1) + IFlowCaptureOperation: 1 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'l1') + Value: + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l1') + Next (Regular) Block[B4] + Leaving: {R3} + } + Block[B3] - Block + Predecessors: [B1] + Statements (1) + IFlowCaptureOperation: 1 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'l2') + Value: + IParameterReferenceOperation: l2 (OperationKind.ParameterReference, Type: System.Threading.Lock) (Syntax: 'l2') + Next (Regular) Block[B4] + Block[B4] - Block + Predecessors: [B2] [B3] + Statements (1) + IFlowCaptureOperation: 2 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'l1 ?? l2') + Value: + IInvocationOperation ( System.Threading.Lock.Scope System.Threading.Lock.EnterScope()) (OperationKind.Invocation, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'l1 ?? l2') + Instance Receiver: + IFlowCaptureReferenceOperation: 1 (OperationKind.FlowCaptureReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l1 ?? l2') + Arguments(0) + Next (Regular) Block[B5] + Leaving: {R2} + Entering: {R4} {R5} + } + .try {R4, R5} + { + Block[B5] - Block + Predecessors: [B4] + Statements (0) + Next (Regular) Block[B7] + Finalizing: {R6} + Leaving: {R5} {R4} {R1} + } + .finally {R6} + { + Block[B6] - Block + Predecessors (0) + Statements (1) + IInvocationOperation ( void System.Threading.Lock.Scope.Dispose()) (OperationKind.Invocation, Type: System.Void, IsImplicit) (Syntax: 'l1 ?? l2') + Instance Receiver: + IFlowCaptureReferenceOperation: 2 (OperationKind.FlowCaptureReference, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'l1 ?? l2') + Arguments(0) + Next (StructuredExceptionHandling) Block[null] + } + } + Block[B7] - Exit + Predecessors: [B5] + Statements (0) + """; + + var expectedDiagnostics = DiagnosticDescription.None; + + VerifyFlowGraphAndDiagnosticsForTest([source, LockTypeDefinition], expectedFlowGraph, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)] + public void LockFlow_LockObject_MissingEnterScope() + { + var source = """ + using System; + using System.Threading; + + class C + { + void M() + /**/{ + Lock l = new Lock(); + lock (l) + { + } + }/**/ + } + + namespace System.Threading + { + public class Lock { } + } + """; + + var expectedFlowGraph = """ + Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} + .locals {R1} + { + Locals: [System.Threading.Lock l] + Block[B1] - Block + Predecessors: [B0] + Statements (2) + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Left: + ILocalReferenceOperation: l (IsDeclaration: True) (OperationKind.LocalReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Right: + IObjectCreationOperation (Constructor: System.Threading.Lock..ctor()) (OperationKind.ObjectCreation, Type: System.Threading.Lock) (Syntax: 'new Lock()') + Arguments(0) + Initializer: + null + IExpressionStatementOperation (OperationKind.ExpressionStatement, Type: null, IsInvalid) (Syntax: 'lock (l) ... }') + Expression: + IInvalidOperation (OperationKind.Invalid, Type: null, IsInvalid, IsImplicit) (Syntax: 'l') + Children(1): + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock, IsInvalid) (Syntax: 'l') + Next (Regular) Block[B2] + Leaving: {R1} + } + Block[B2] - Exit + Predecessors: [B1] + Statements (0) + """; + + var expectedDiagnostics = new[] + { + // (9,15): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(9, 15) + }; + + VerifyFlowGraphAndDiagnosticsForTest(source, expectedFlowGraph, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)] + public void LockFlow_LockObject_MissingEnterScope_NonEmptyBody() + { + var source = """ + using System; + using System.Threading; + + class C + { + void M() + /**/{ + Lock l = new Lock(); + lock (l) + { + Console.Write("Body"); + } + }/**/ + } + + namespace System.Threading + { + public class Lock { } + } + """; + + var expectedFlowGraph = """ + Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} + .locals {R1} + { + Locals: [System.Threading.Lock l] + Block[B1] - Block + Predecessors: [B0] + Statements (3) + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Left: + ILocalReferenceOperation: l (IsDeclaration: True) (OperationKind.LocalReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Right: + IObjectCreationOperation (Constructor: System.Threading.Lock..ctor()) (OperationKind.ObjectCreation, Type: System.Threading.Lock) (Syntax: 'new Lock()') + Arguments(0) + Initializer: + null + IExpressionStatementOperation (OperationKind.ExpressionStatement, Type: null, IsInvalid) (Syntax: 'lock (l) ... }') + Expression: + IInvalidOperation (OperationKind.Invalid, Type: null, IsInvalid, IsImplicit) (Syntax: 'l') + Children(1): + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock, IsInvalid) (Syntax: 'l') + IExpressionStatementOperation (OperationKind.ExpressionStatement, Type: null) (Syntax: 'Console.Write("Body");') + Expression: + IInvocationOperation (void System.Console.Write(System.String value)) (OperationKind.Invocation, Type: System.Void) (Syntax: 'Console.Write("Body")') + Instance Receiver: + null + Arguments(1): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: value) (OperationKind.Argument, Type: null) (Syntax: '"Body"') + ILiteralOperation (OperationKind.Literal, Type: System.String, Constant: "Body") (Syntax: '"Body"') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Next (Regular) Block[B2] + Leaving: {R1} + } + Block[B2] - Exit + Predecessors: [B1] + Statements (0) + """; + + var expectedDiagnostics = new[] + { + // (9,15): error CS0656: Missing compiler required member 'System.Threading.Lock.EnterScope' + // lock (l) + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock", "EnterScope").WithLocation(9, 15) + }; + + VerifyFlowGraphAndDiagnosticsForTest(source, expectedFlowGraph, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)] + public void LockFlow_LockObject_MissingScopeDispose() + { + var source = """ + using System; + using System.Threading; + + class C + { + void M() + /**/{ + Lock l = new Lock(); + lock (l) + { + } + }/**/ + } + + namespace System.Threading + { + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope { } + } + } + """; + + var expectedFlowGraph = """ + Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} + .locals {R1} + { + Locals: [System.Threading.Lock l] + Block[B1] - Block + Predecessors: [B0] + Statements (2) + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Left: + ILocalReferenceOperation: l (IsDeclaration: True) (OperationKind.LocalReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new Lock()') + Right: + IObjectCreationOperation (Constructor: System.Threading.Lock..ctor()) (OperationKind.ObjectCreation, Type: System.Threading.Lock) (Syntax: 'new Lock()') + Arguments(0) + Initializer: + null + IExpressionStatementOperation (OperationKind.ExpressionStatement, Type: null, IsInvalid) (Syntax: 'lock (l) ... }') + Expression: + IInvalidOperation (OperationKind.Invalid, Type: null, IsInvalid, IsImplicit) (Syntax: 'l') + Children(1): + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock, IsInvalid) (Syntax: 'l') + Next (Regular) Block[B2] + Leaving: {R1} + } + Block[B2] - Exit + Predecessors: [B1] + Statements (0) + """; + + var expectedDiagnostics = new[] + { + // (9,15): error CS0656: Missing compiler required member 'System.Threading.Lock+Scope.Dispose' + // lock (l) + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "l").WithArguments("System.Threading.Lock+Scope", "Dispose").WithLocation(9, 15) + }; + + VerifyFlowGraphAndDiagnosticsForTest(source, expectedFlowGraph, expectedDiagnostics); + } + + [Fact, CompilerTrait(CompilerFeature.IOperation, CompilerFeature.Dataflow)] + public void LockFlow_LockObject_EnterScopeVirtual() + { + var source = """ + using System; + using System.Threading; + + class C + { + void M() + /**/{ + Lock l = new LockDerived(); + lock (l) + { + } + }/**/ + } + + namespace System.Threading + { + public class Lock + { + public virtual Scope EnterScope() => new(); + + public ref struct Scope + { + public void Dispose() { } + } + } + + public class LockDerived : Lock + { + public override Scope EnterScope() => new(); + } + } + """; + + var expectedFlowGraph = """ + Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} + .locals {R1} + { + Locals: [System.Threading.Lock l] + Block[B1] - Block + Predecessors: [B0] + Statements (1) + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new LockDerived()') + Left: + ILocalReferenceOperation: l (IsDeclaration: True) (OperationKind.LocalReference, Type: System.Threading.Lock, IsImplicit) (Syntax: 'l = new LockDerived()') + Right: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Threading.Lock, IsImplicit) (Syntax: 'new LockDerived()') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: True, IsUserDefined: False) (MethodSymbol: null) + (ImplicitReference) + Operand: + IObjectCreationOperation (Constructor: System.Threading.LockDerived..ctor()) (OperationKind.ObjectCreation, Type: System.Threading.LockDerived) (Syntax: 'new LockDerived()') + Arguments(0) + Initializer: + null + Next (Regular) Block[B2] + Entering: {R2} + .locals {R2} + { + CaptureIds: [0] + Block[B2] - Block + Predecessors: [B1] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsImplicit) (Syntax: 'l') + Value: + IInvocationOperation (virtual System.Threading.Lock.Scope System.Threading.Lock.EnterScope()) (OperationKind.Invocation, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'l') + Instance Receiver: + ILocalReferenceOperation: l (OperationKind.LocalReference, Type: System.Threading.Lock) (Syntax: 'l') + Arguments(0) + Next (Regular) Block[B3] + Entering: {R3} {R4} + .try {R3, R4} + { + Block[B3] - Block + Predecessors: [B2] + Statements (0) + Next (Regular) Block[B5] + Finalizing: {R5} + Leaving: {R4} {R3} {R2} {R1} + } + .finally {R5} + { + Block[B4] - Block + Predecessors (0) + Statements (1) + IInvocationOperation ( void System.Threading.Lock.Scope.Dispose()) (OperationKind.Invocation, Type: System.Void, IsImplicit) (Syntax: 'l') + Instance Receiver: + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: System.Threading.Lock.Scope, IsImplicit) (Syntax: 'l') + Arguments(0) + Next (StructuredExceptionHandling) Block[null] + } + } + } + Block[B5] - Exit + Predecessors: [B3] + Statements (0) + """; + + var expectedDiagnostics = DiagnosticDescription.None; + + VerifyFlowGraphAndDiagnosticsForTest(source, expectedFlowGraph, expectedDiagnostics); + } } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index f769e5bb7284d..6b013851422dc 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -2612,11 +2612,11 @@ static class D public static I1 Interceptor1(this I1 i1, string param) { Console.Write("interceptor " + param); return i1; } } """; - var comp = CreateCompilation(new[] { (source, "/Users/me/projects/Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + var comp = CreateCompilation(new[] { (source, PlatformInformation.IsWindows ? @"C:\Users\me\projects\Program.cs" : "/Users/me/projects/Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); comp.VerifyEmitDiagnostics( - // /Users/me/projects/Program.cs(21,25): error CS9140: Cannot intercept: compilation does not contain a file with path 'projects/Program.cs'. Did you mean to use path '/Users/me/projects/Program.cs'? + // C:\Users\me\projects\Program.cs(21,25): error CS9140: Cannot intercept: compilation does not contain a file with path 'projects/Program.cs'. Did you mean to use path 'Program.cs'? // [InterceptsLocation("projects/Program.cs", 15, 11)] - Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"""projects/Program.cs""").WithArguments("projects/Program.cs", "/Users/me/projects/Program.cs").WithLocation(21, 25) + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"""projects/Program.cs""").WithArguments("projects/Program.cs", "Program.cs").WithLocation(21, 25) ); } @@ -4556,8 +4556,8 @@ class C [Fact] public void PathMapping_02() { - // Attribute uses a physical path, but we expected a mapped path. - // Diagnostic can suggest using the mapped path instead. + // Attribute contains an unmapped path even though compilation uses a pathmap. + // Because normalizing to the path of the containing file also effectively applies the pathmap, we accept the given path var pathPrefix = PlatformInformation.IsWindows ? @"C:\My\Machine\Specific\Path\" : "/My/Machine/Specific/Path/"; var path = pathPrefix + "Program.cs"; var source = $$""" @@ -4577,16 +4577,13 @@ class C """; var pathMap = ImmutableArray.Create(new KeyValuePair(pathPrefix, "/_/")); - var comp = CreateCompilation( + var verifier = CompileAndVerify( new[] { (source, path), s_attributesSource }, parseOptions: RegularWithInterceptors, options: TestOptions.DebugExe.WithSourceReferenceResolver( - new SourceFileResolver(ImmutableArray.Empty, null, pathMap))); - comp.VerifyEmitDiagnostics( - // C:\My\Machine\Specific\Path\Program.cs(11,25): error CS9145: Cannot intercept: Path 'C:\My\Machine\Specific\Path\Program.cs' is unmapped. Expected mapped path '/_/Program.cs'. - // [InterceptsLocation(@"C:\My\Machine\Specific\Path\Program.cs", 5, 3)] - Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithUnmappedCandidate, $@"@""{path}""").WithArguments(path, "/_/Program.cs").WithLocation(11, 25) - ); + new SourceFileResolver(ImmutableArray.Empty, null, pathMap)), + expectedOutput: "1"); + verifier.VerifyDiagnostics(); } [Fact] @@ -4616,10 +4613,14 @@ class C parseOptions: RegularWithInterceptors, options: TestOptions.DebugExe.WithSourceReferenceResolver( new SourceFileResolver(ImmutableArray.Empty, null, pathMap))); - comp.VerifyEmitDiagnostics( - // C:\My\Machine\Specific\Path\Program.cs(11,25): error CS9140: Cannot intercept: compilation does not contain a file with path '\_\Program.cs'. Did you mean to use path '/_/Program.cs'? + comp.VerifyEmitDiagnostics(PlatformInformation.IsWindows + // C:\My\Machine\Specific\Path\Program.cs(11,25): error CS9139: Cannot intercept: compilation does not contain a file with path 'C:\_\Program.cs'. // [InterceptsLocation(@"\_\Program.cs", 5, 3)] - Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"@""\_\Program.cs""").WithArguments(@"\_\Program.cs", "/_/Program.cs").WithLocation(11, 25)); + ? Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"@""\_\Program.cs""").WithArguments(@"C:\_\Program.cs").WithLocation(11, 25) + + // /My/Machine/Specific/Path/Program.cs(11,25): error CS9139: Cannot intercept: compilation does not contain a file with path '/My/Machine/Specific/Path/\_\Program.cs'. + // [InterceptsLocation(@"\_\Program.cs", 5, 3)] + : Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"@""\_\Program.cs""").WithArguments(@"/My/Machine/Specific/Path/\_\Program.cs").WithLocation(11, 25)); } [Fact] @@ -4746,9 +4747,14 @@ class C options: TestOptions.DebugExe.WithSourceReferenceResolver( new SourceFileResolver(ImmutableArray.Empty, null, pathMap))); comp.VerifyEmitDiagnostics( - // C:\My\Machine\Specific\Path\Program.cs(11,25): error CS9140: Cannot intercept: compilation does not contain a file with path '/_/Program.cs'. Did you mean to use path '\_/Program.cs'? - // [InterceptsLocation(@"/_/Program.cs", 5, 3)] - Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"@""/_/Program.cs""").WithArguments("/_/Program.cs", @"\_/Program.cs").WithLocation(11, 25)); + PlatformInformation.IsWindows + // C:\My\Machine\Specific\Path\Program.cs(11,25): error CS9139: Cannot intercept: compilation does not contain a file with path 'C:\_\Program.cs'. + // [InterceptsLocation(@"/_/Program.cs", 5, 3)] + ? Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"@""/_/Program.cs""").WithArguments(PlatformInformation.IsWindows ? @"C:\_\Program.cs" : "/_/Program.cs").WithLocation(11, 25) + + // /My/Machine/Specific/Path/Program.cs(11,25): error CS9139: Cannot intercept: compilation does not contain a file with path '/_/Program.cs'. + // [InterceptsLocation(@"/_/Program.cs", 5, 3)] + : Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"@""/_/Program.cs""").WithArguments("/_/Program.cs").WithLocation(11, 25)); } [Fact] @@ -4876,8 +4882,6 @@ public static void Main() [Fact] public void PathNormalization_04() { - // Absolute paths do not have slashes normalized when no pathmap is present - // Note that any such normalization step would be specific to Windows var source = """ using System.Runtime.CompilerServices; using System; @@ -4892,16 +4896,401 @@ public static void Main() public void M() => throw null!; - [InterceptsLocation("C:/src/Program.cs", 9, 11)] // 1 + [InterceptsLocation("C:/src/Program.cs", 9, 11)] public void Interceptor() => Console.Write(1); } """; - var comp = CreateCompilation(new[] { (source, @"C:\src\Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + if (PlatformInformation.IsWindows) + { + var verifier = CompileAndVerify(new[] { (source, @"C:\src\Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors, expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + else + { + var comp = CreateCompilation(new[] { (source, @"/src/Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics( + // /src/Program.cs(14,25): error CS9139: Cannot intercept: compilation does not contain a file with path '/src/C:/src/Program.cs'. + // [InterceptsLocation("C:/src/Program.cs", 9, 11)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"""C:/src/Program.cs""").WithArguments("/src/C:/src/Program.cs").WithLocation(14, 25)); + } + } + + [Fact] + public void PathNormalization_05() + { + // paths in attribute as well as syntax tree have mixed slashes + var source = """ + using System.Runtime.CompilerServices; + using System; + + class C + { + public static void Main() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + + [InterceptsLocation(@"C:\src/Program.cs", 9, 11)] + public void Interceptor() => Console.Write(1); + } + """; + + if (PlatformInformation.IsWindows) + { + var verifier = CompileAndVerify(new[] { (source, @"C:/src\Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors, expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + else + { + var comp = CreateCompilation(new[] { (source, @"/src/Program.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics( + // /src/Program.cs(14,25): error CS9139: Cannot intercept: compilation does not contain a file with path '/src/C:\src/Program.cs'. + // [InterceptsLocation(@"C:\src/Program.cs", 9, 11)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"@""C:\src/Program.cs""").WithArguments(@"/src/C:\src/Program.cs").WithLocation(14, 25)); + } + } + + [Fact] + public void RelativePaths_01() + { + var source = """ + class C + { + public static void Main() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + } + """; + + var source2 = """ + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation("../src/Program.cs", 6, 11)] + internal static void Interceptor(this C c) => Console.Write(1); + } + """; + + var verifier = CompileAndVerify(new[] { (source, PlatformInformation.IsWindows ? @"C:\src\Program.cs" : "/src/Program.cs"), (source2, PlatformInformation.IsWindows ? @"C:\obj\Generated.cs" : "/obj/Generated.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors, expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void RelativePaths_02() + { + var source = """ + class C + { + public static void Main() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + } + """; + + // interceptor containing file does not have absolute path + // Therefore we don't resolve the relative path + var source2 = """ + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation("../src/Program.cs", 6, 11)] + internal static void Interceptor(this C c) => Console.Write(1); + } + """; + + var comp = CreateCompilation(new[] { (source, PlatformInformation.IsWindows ? @"C:\src\Program.cs" : "/src/Program.cs"), (source2, PlatformInformation.IsWindows ? @"Generator\Generated.cs" : "Generator/Generated.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics( + // Generator\Generated.cs(6,25): error CS9139: Cannot intercept: compilation does not contain a file with path '../src/Program.cs'. + // [InterceptsLocation("../src/Program.cs", 6, 11)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"""../src/Program.cs""").WithArguments("../src/Program.cs").WithLocation(6, 25)); + } + + [Fact] + public void RelativePaths_03() + { + // intercepted file does not have absolute path + var source = """ + class C + { + public static void Main() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + } + """; + + var source2 = """ + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation("../src/Program.cs", 6, 11)] + internal static void Interceptor(this C c) => Console.Write(1); + } + """; + + var comp = CreateCompilation(new[] { (source, PlatformInformation.IsWindows ? @"src\Program.cs" : "src/Program.cs"), (source2, PlatformInformation.IsWindows ? @"C:\obj\Generated.cs" : "/obj/Generated.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics( + // C:\obj\Generated.cs(6,25): error CS9139: Cannot intercept: compilation does not contain a file with path 'C:\src\Program.cs'. + // [InterceptsLocation("../src/Program.cs", 6, 11)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"""../src/Program.cs""").WithArguments(PlatformInformation.IsWindows ? @"C:\src\Program.cs" : "/src/Program.cs").WithLocation(6, 25) + ); + } + + [Fact] + public void RelativePaths_04() + { + var source = """ + class C + { + public static void Main() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + } + """; + + // The relative path resolution of `C:\..` is just `C:\` (and `/..` resolves to `/`). + var source2 = """ + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation("../../src/Program.cs", 6, 11)] + internal static void Interceptor(this C c) => Console.Write(1); + } + """; + + var comp = CreateCompilation(new[] { (source, PlatformInformation.IsWindows ? @"C:\src\Program.cs" : "/src/Program.cs"), (source2, PlatformInformation.IsWindows ? @"C:\obj\Generated.cs" : "/obj/Generated.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics(); + } + + [Fact] + public void RelativePaths_05() + { + var source = """ + class C + { + public static void Main() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + } + """; + + var source2 = """ + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation("../src/./Program.cs", 6, 11)] + internal static void Interceptor(this C c) => Console.Write(1); + } + """; + + var comp = CreateCompilation(new[] { (source, PlatformInformation.IsWindows ? @"C:\src\Program.cs" : "/src/Program.cs"), (source2, PlatformInformation.IsWindows ? @"C:\obj\Generated.cs" : "/obj/Generated.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics(); + } + + [Fact] + public void RelativePaths_06() + { + var source = """ + class C + { + public static void Main() + { + C c = new C(); + c.M(); + } + + public void M() => throw null!; + } + """; + + var source2 = """ + using System; + using System.Runtime.CompilerServices; + + static class Interceptors + { + [InterceptsLocation("../src/Program.cs/.", 6, 11)] + internal static void Interceptor(this C c) => Console.Write(1); + } + """; + + var comp = CreateCompilation(new[] { (source, PlatformInformation.IsWindows ? @"C:\src\Program.cs" : "/src/Program.cs"), (source2, PlatformInformation.IsWindows ? @"C:\obj\Generated.cs" : "/obj/Generated.cs"), s_attributesSource }, parseOptions: RegularWithInterceptors); + comp.VerifyEmitDiagnostics(); + } + + [Fact] + public void RelativePaths_07() + { + var source = """ + C c = new C(); + c.M(); + + class C + { + public void M() => throw null!; + } + """; + + var source2 = """ + using System.Runtime.CompilerServices; + using System; + + static class Interceptors + { + [InterceptsLocation("../src/Program.cs", 2, 3)] + public static void Interceptor(this C c) => Console.Write(1); + } + """; + var pathPrefix = PlatformInformation.IsWindows ? """C:\My\Machine\Specific\Path\""" : "/My/Machine/Specific/Path/"; + var path = pathPrefix + "src/Program.cs"; + var path2 = pathPrefix + "obj/Generated.cs"; + var pathMap = ImmutableArray.Create(new KeyValuePair(pathPrefix, "/_/")); + + var verifier = CompileAndVerify( + new[] { (source, path), (source2, path2), s_attributesSource }, + parseOptions: RegularWithInterceptors, + options: TestOptions.DebugExe.WithSourceReferenceResolver( + new SourceFileResolver(ImmutableArray.Empty, null, pathMap)), + expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void RelativePaths_08() + { + // SyntaxTree file paths are not absolute. Relative path resolution is not performed. + var source = """ + C c = new C(); + c.M(); + + class C + { + public void M() => throw null!; + } + """; + + var source2 = """ + using System.Runtime.CompilerServices; + using System; + + static class Interceptors + { + [InterceptsLocation("../src/Program.cs", 2, 3)] + public static void Interceptor(this C c) => Console.Write(1); + } + """; + var pathPrefix = PlatformInformation.IsWindows ? """My\Machine\Specific\Path\""" : "My/Machine/Specific/Path/"; + var path = pathPrefix + "src/Program.cs"; + var path2 = pathPrefix + "obj/Generated.cs"; + var pathMap = ImmutableArray.Create(new KeyValuePair(pathPrefix, "/_/")); + + var comp = CreateCompilation( + new[] { (source, path), (source2, path2), s_attributesSource }, + parseOptions: RegularWithInterceptors, + options: TestOptions.DebugExe.WithSourceReferenceResolver( + new SourceFileResolver(ImmutableArray.Empty, null, pathMap))); + comp.VerifyEmitDiagnostics( + // My\Machine\Specific\Path\obj/Generated.cs(6,25): error CS9139: Cannot intercept: compilation does not contain a file with path '../src/Program.cs'. + // [InterceptsLocation("../src/Program.cs", 2, 3)] + Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilation, @"""../src/Program.cs""").WithArguments("../src/Program.cs").WithLocation(6, 25)); + } + + [Fact] + public void OldVersusNewResolutionStrategy() + { + // relative path resolution will match a file (and the node referenced is not interceptable) + // exact mapped resolution will match a *different* file (and the node referenced is interceptable) + var source1 = (""" + class C1 + { + void M1() + { + var _ = + C.Interceptable; + } + } + """, PlatformInformation.IsWindows ? @"C:\src1\file1.cs" : "/src1/file1.cs"); + + var directory2 = PlatformInformation.IsWindows ? @"C:\src2\" : "/src2/"; + var path2 = PlatformInformation.IsWindows ? @"C:\src2\file1.cs" : "/src2/file1.cs"; + var source2 = (""" + class C2 + { + static void Main() + { + // var _ = + C.Interceptable(); + } + } + + class C + { + public static void Interceptable() => throw null!; + } + """, path2); + + var source3 = (""" + using System.Runtime.CompilerServices; + using System; + + class Interceptors + { + [InterceptsLocation("./file1.cs", 6, 15)] // 1 + public static void Interceptor() => Console.Write(1); + } + """, PlatformInformation.IsWindows ? @"C:\src1\interceptors.cs" : "/src1/interceptors.cs"); + + // Demonstrate that "relative path" resolution happens first by triggering the not interceptable error. + var pathMap = ImmutableArray.Create(new KeyValuePair(directory2, "./")); + var comp = CreateCompilation([source1, source2, source3, s_attributesSource], + parseOptions: RegularWithInterceptors, + options: TestOptions.DebugExe.WithSourceReferenceResolver( + new SourceFileResolver(ImmutableArray.Empty, null, pathMap))); comp.VerifyEmitDiagnostics( - // C:\src\Program.cs(14,25): error CS9140: Cannot intercept: compilation does not contain a file with path 'C:/src/Program.cs'. Did you mean to use path 'C:\src\Program.cs'? - // [InterceptsLocation("C:/src/Program.cs", 9, 11)] // 1 - Diagnostic(ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate, @"""C:/src/Program.cs""").WithArguments("C:/src/Program.cs", @"C:\src\Program.cs").WithLocation(14, 25)); + // C:\src1\interceptors.cs(6,6): error CS9151: Possible method name 'Interceptable' cannot be intercepted because it is not being invoked. + // [InterceptsLocation("./file1.cs", 6, 15)] // 1 + Diagnostic(ErrorCode.ERR_InterceptorNameNotInvoked, @"InterceptsLocation(""./file1.cs"", 6, 15)").WithArguments("Interceptable").WithLocation(6, 6)); + + // excluding 'source1' from the compilation, we fall back to exact match of mapped path, and interception is successful. + var verifier = CompileAndVerify([source2, source3, s_attributesSource], + parseOptions: RegularWithInterceptors, + options: TestOptions.DebugExe.WithSourceReferenceResolver( + new SourceFileResolver(ImmutableArray.Empty, null, pathMap)), + expectedOutput: "1"); + verifier.VerifyDiagnostics(); } [Fact] diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs index 22feb2d1fad04..016adb0c61305 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs @@ -25514,6 +25514,33 @@ public void M2() comp.VerifyDiagnostics(); } + [Fact, WorkItem(67880, "https://github.com/dotnet/roslyn/issues/67880")] + public void PatternOnTuple_IsExpression_DynamicCast() + { + var src = @" +#nullable enable +public class C +{ + public (string, string)? M1() => throw null!; + + public void M2() + { + if ((dynamic)(M1() is (var z, _))) + { + z.ToString(); + } + } +} +"; + + var comp = CreateCompilation(src); + comp.VerifyDiagnostics( + // (11,13): error CS0165: Use of unassigned local variable 'z' + // z.ToString(); + Diagnostic(ErrorCode.ERR_UseDefViolation, "z").WithArguments("z").WithLocation(11, 13) + ); + } + [Fact, WorkItem(51020, "https://github.com/dotnet/roslyn/issues/51020")] public void PatternOnTuple_IsExpression_ContradictoryTests() { @@ -36470,11 +36497,11 @@ void Main(string? s) { if (MyIsNullOrEmpty(s)) { - s.ToString(); // warn + s.ToString(); // 1 } else { - s.ToString(); + s.ToString(); // 2 } } public dynamic MyIsNullOrEmpty([NotNullWhen(false)] string? s) => throw null!; @@ -36482,9 +36509,46 @@ void Main(string? s) ", NotNullWhenAttributeDefinition }, options: WithNullableEnable()); c.VerifyDiagnostics( - // (9,13): warning CS8602: Dereference of a possibly null reference. - // s.ToString(); // warn - Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13) + // 0.cs(9,13): warning CS8602: Dereference of a possibly null reference. + // s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // 0.cs(13,13): warning CS8602: Dereference of a possibly null reference. + // s.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 13) + ); + + VerifyAnnotations(c, "C.MyIsNullOrEmpty", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_ReturningNullableBoolean() + { + CSharpCompilation c = CreateCompilation(new[] { @" +using System.Diagnostics.CodeAnalysis; +public class C +{ + void Main(string? s) + { + if (MyIsNullOrEmpty(s) == false) + { + s.ToString(); // 1 + } + else + { + s.ToString(); // 2 + } + } + public bool? MyIsNullOrEmpty([NotNullWhen(false)] string? s) => throw null!; +} +", NotNullWhenAttributeDefinition }, options: WithNullableEnable()); + + c.VerifyDiagnostics( + // 0.cs(9,13): warning CS8602: Dereference of a possibly null reference. + // s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // 0.cs(13,13): warning CS8602: Dereference of a possibly null reference. + // s.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 13) ); VerifyAnnotations(c, "C.MyIsNullOrEmpty", NotNullWhenFalse); @@ -36583,20 +36647,111 @@ class C { void Main(string? s) { - MyIsNullOrEmpty(s); - s.ToString(); // warn + if ((bool)MyIsNullOrEmpty(s)) + { + s.ToString(); // 1 + } + else + { + s.ToString(); // 2 + } } object MyIsNullOrEmpty([NotNullWhen(false)] string? s) => throw null!; } ", NotNullWhenAttributeDefinition }, options: WithNullableEnable()); c.VerifyDiagnostics( - // (8,9): warning CS8602: Dereference of a possibly null reference. - // s.ToString(); // warn - Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 9) + // 0.cs(9,13): warning CS8602: Dereference of a possibly null reference. + // s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // 0.cs(13,13): warning CS8602: Dereference of a possibly null reference. + // s.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 13) + ); + } + + [Fact] + public void NotNullWhenTrue_ReturningObject() + { + CSharpCompilation c = CreateCompilation(new[] { @" +using System.Diagnostics.CodeAnalysis; +class C +{ + void Main(string? s) + { + if ((bool)MyIsNullOrEmpty(s)) + { + s.ToString(); // 1 + } + else + { + s.ToString(); // 2 + } + } + object MyIsNullOrEmpty([NotNullWhen(true)] string? s) => throw null!; +} +", NotNullWhenAttributeDefinition }, options: WithNullableEnable()); + + c.VerifyDiagnostics( + // 0.cs(9,13): warning CS8602: Dereference of a possibly null reference. + // s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // 0.cs(13,13): warning CS8602: Dereference of a possibly null reference. + // s.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 13) ); } + [Fact] + public void MaybeNullWhenTrue_ReturningObject() + { + CSharpCompilation c = CreateCompilation(new[] { @" +using System.Diagnostics.CodeAnalysis; +class C +{ + void Main(string s) + { + if ((bool)M(s)) + { + s.ToString(); + } + else + { + s.ToString(); + } + } + object M([MaybeNullWhen(true)] string s) => throw null!; +} +", MaybeNullWhenAttributeDefinition }, options: WithNullableEnable()); + + c.VerifyDiagnostics(); + } + + [Fact] + public void MaybeNullWhenFalse_ReturningObject() + { + CSharpCompilation c = CreateCompilation(new[] { @" +using System.Diagnostics.CodeAnalysis; +class C +{ + void Main(string s) + { + if ((bool)M(s)) + { + s.ToString(); + } + else + { + s.ToString(); + } + } + object M([MaybeNullWhen(false)] string s) => throw null!; +} +", MaybeNullWhenAttributeDefinition }, options: WithNullableEnable()); + + c.VerifyDiagnostics(); + } + [Fact] public void NotNullWhenFalse_FollowedByNotNull() { @@ -61862,6 +62017,35 @@ static void M() ); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72056")] + public void Lambda_23() + { + var comp = CreateCompilation(""" + using System.Collections.Generic; + + string[]? e = null; + + var arr = new[] { + () => M2(e), + () => M1(e) + }; + + partial class Program + { + static void M1(IEnumerable param) {} + } + """, options: WithNullableEnable(TestOptions.ReleaseExe)); + + comp.VerifyDiagnostics( + // (6,11): error CS0103: The name 'M2' does not exist in the current context + // () => M2(e), + Diagnostic(ErrorCode.ERR_NameNotInContext, "M2").WithArguments("M2").WithLocation(6, 11), + // (7,14): warning CS8604: Possible null reference argument for parameter 'param' in 'void Program.M1(IEnumerable param)'. + // () => M1(e) + Diagnostic(ErrorCode.WRN_NullReferenceArgument, "e").WithArguments("param", "void Program.M1(IEnumerable param)").WithLocation(7, 14) + ); + } + [ConditionalFact(typeof(IsRelease))] [WorkItem(48174, "https://github.com/dotnet/roslyn/issues/48174")] public void Lambda_Nesting_Large_02() diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/StructConstructorTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/StructConstructorTests.cs index d972d3cffcbb7..6987f658c4acb 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/StructConstructorTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/StructConstructorTests.cs @@ -4792,5 +4792,111 @@ .maxstack 2 } "); } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1598252")] + public void StaticAndInstanceConstructors_01(string type) + { + string source = $$""" + using System; + {{type}} S + { + static S() + { + Console.WriteLine("static constructor"); + } + public S(int p) + { + Console.WriteLine("instance constructor: {0}", p); + } + public S() : this(0) + { + } + } + class Program + { + static void Main() + { + _ = new S(); + } + } + """; + + var comp = CreateCompilation(source, options: TestOptions.ReleaseExe); + VerifyExplicitlyDeclaredInstanceConstructors( + comp.GlobalNamespace.GetTypeMember("S"), + "S..ctor(System.Int32 p)", + "S..ctor()"); + VerifyExplicitlyDeclaredInstanceConstructors( + ((Compilation)comp).GlobalNamespace.GetTypeMember("S"), + "S..ctor(System.Int32 p)", + "S..ctor()"); + + CompileAndVerify(comp, expectedOutput: """ + static constructor + instance constructor: 0 + """); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + public void StaticAndInstanceConstructors_02(string type) + { + string source = $$""" + using System; + {{type}} S + { + static S() + { + Console.WriteLine("static constructor"); + } + public S() + { + Console.WriteLine("instance constructor"); + } + } + class Program + { + static void Main() + { + _ = new S(); + } + } + """; + + var comp = CreateCompilation(source, options: TestOptions.ReleaseExe); + VerifyExplicitlyDeclaredInstanceConstructors( + comp.GlobalNamespace.GetTypeMember("S"), + "S..ctor()"); + VerifyExplicitlyDeclaredInstanceConstructors( + ((Compilation)comp).GlobalNamespace.GetTypeMember("S"), + "S..ctor()"); + + CompileAndVerify(comp, expectedOutput: """ + static constructor + instance constructor + """); + } + + private static void VerifyExplicitlyDeclaredInstanceConstructors(NamedTypeSymbol type, params string[] expectedConstructors) + { + var constructors = type.InstanceConstructors; + var members = type.GetMembers(".ctor"); + Assert.True(members.SequenceEqual(constructors)); + Assert.True(constructors.All(c => c is { IsStatic: false, IsImplicitConstructor: false })); + Assert.Equal(expectedConstructors, constructors.ToTestDisplayStrings()); + } + + private static void VerifyExplicitlyDeclaredInstanceConstructors(INamedTypeSymbol type, params string[] expectedConstructors) + { + var constructors = type.InstanceConstructors; + var members = type.GetMembers(".ctor"); + Assert.True(members.SequenceEqual(constructors)); + Assert.True(constructors.All(c => c is { IsStatic: false, IsImplicitlyDeclared: false })); + Assert.Equal(expectedConstructors, constructors.ToTestDisplayStrings()); + } } } diff --git a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/StateTableTests.cs b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/StateTableTests.cs index 1078b8899e344..1422d3f9263e7 100644 --- a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/StateTableTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/StateTableTests.cs @@ -1343,7 +1343,8 @@ private DriverStateTable.Builder GetBuilder(DriverStateTable previous, bool trac { var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp10); var c = CSharpCompilation.Create("empty"); - var state = new GeneratorDriverState(options, + var state = new GeneratorDriverState( + options, CompilerAnalyzerConfigOptionsProvider.Empty, ImmutableArray.Empty, ImmutableArray.Empty, @@ -1351,9 +1352,8 @@ private DriverStateTable.Builder GetBuilder(DriverStateTable previous, bool trac ImmutableArray.Empty, previous, SyntaxStore.Empty, - disabledOutputs: IncrementalGeneratorOutputKind.None, + driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps), runtime: TimeSpan.Zero, - trackIncrementalGeneratorSteps: trackIncrementalGeneratorSteps, parseOptionsChanged: false); return new DriverStateTable.Builder(c, state, SyntaxStore.Empty.ToBuilder(c, ImmutableArray.Empty, trackIncrementalGeneratorSteps, cancellationToken: default)); diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/NullablePublicAPITests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/NullablePublicAPITests.cs index e2630d1982bc4..75ebd9324b0cc 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/NullablePublicAPITests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/NullablePublicAPITests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#pragma warning disable RSEXPERIMENTAL001 // Internal usage of experimental API #nullable disable using System; @@ -5160,5 +5161,79 @@ public static void M1(C? c, object? x) var binaryRightArgument = tree.GetRoot().DescendantNodes().OfType().Single().Right.DescendantNodes().OfType().Single().Expression; Assert.Equal("System.Object?", model.GetTypeInfo(binaryRightArgument).Type.ToTestDisplayString(includeNonNullable: true)); } + + [Theory] + [CombinatorialData] + public void NullableDisableSemanticModel_01(bool runNullableAnalysisAlways) + { + var source = """ + #nullable enable + class C + { + void M(string x) + { + if (x == null) + { + x.ToString(); + } + } + } + """; + + var comp = CreateCompilation(source, parseOptions: runNullableAnalysisAlways ? TestOptions.RegularPreview.WithFeature("run-nullable-analysis", "always") : TestOptions.RegularPreview); + comp.VerifyDiagnostics( + // (8,13): warning CS8602: Dereference of a possibly null reference. + // x.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "x").WithLocation(8, 13)); + + test(SemanticModelOptions.None, expectedAnnotation: PublicNullableAnnotation.Annotated); + test(SemanticModelOptions.DisableNullableAnalysis, expectedAnnotation: PublicNullableAnnotation.None); + + void test(SemanticModelOptions options, PublicNullableAnnotation expectedAnnotation) + { + var tree = comp.SyntaxTrees.Single(); + var model = comp.GetSemanticModel(tree, options); + var xUsage = tree.GetRoot().DescendantNodes().OfType().Single().Expression; + var typeInfo = model.GetTypeInfo(xUsage); + Assert.NotNull(typeInfo.Type); + Assert.Equal(SpecialType.System_String, typeInfo.Type.SpecialType); + Assert.Equal(expectedAnnotation, typeInfo.Type.NullableAnnotation); + } + } + + [Fact] + public void NullableDisableSemanticModel_02() + { + var source = """ + #nullable enable + class C + { + void M(string x) + { + if (x == null) + { + x.ToString(); + } + } + } + """; + + var comp = CreateCompilation(source, parseOptions: TestOptions.RegularPreview.WithFeature("run-nullable-analysis", "never")); + comp.VerifyDiagnostics(); + + test(SemanticModelOptions.None); + test(SemanticModelOptions.DisableNullableAnalysis); + + void test(SemanticModelOptions options) + { + var tree = comp.SyntaxTrees.Single(); + var model = comp.GetSemanticModel(tree, options); + var xUsage = tree.GetRoot().DescendantNodes().OfType().Single().Expression; + var typeInfo = model.GetTypeInfo(xUsage); + Assert.NotNull(typeInfo.Type); + Assert.Equal(SpecialType.System_String, typeInfo.Type.SpecialType); + Assert.Equal(PublicNullableAnnotation.None, typeInfo.Type.NullableAnnotation); + } + } } } diff --git a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs index 568b4af3f1ef7..ddee4aeacea4e 100644 --- a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs +++ b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs @@ -322,6 +322,7 @@ public void WarningLevel_2() case ErrorCode.WRN_TargetDifferentRefness: case ErrorCode.WRN_RefReadonlyParameterDefaultValue: case ErrorCode.WRN_Experimental: + case ErrorCode.WRN_ConvertingLock: Assert.Equal(1, ErrorFacts.GetWarningLevel(errorCode)); break; case ErrorCode.WRN_MainIgnored: diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/CollectionExpressionParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/CollectionExpressionParsingTests.cs index 53ac300301e9b..75cde7ce70fbc 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/CollectionExpressionParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/CollectionExpressionParsingTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Linq; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Roslyn.Test.Utilities; using Xunit; @@ -17096,4 +17097,35 @@ public void TreatKeywordAsCollectionExprElement() } EOF(); } + + [Theory, CombinatorialData] + public void CollectionExpressionParsingSlotCounts([CombinatorialRange(1, 20)] int count) + { + // Validate no errors for collections with small and large number of elements. Importantly, we want to test the + // boundary points where the slot count of the collection crosses over the amount that can be directly stored in + // the node, versus the slot count stored in subclass nodes. + var text = $"[{string.Join(", ", Enumerable.Range(1, count).Select(i => $"A{i}"))}]"; + + UsingExpression(text, TestOptions.Regular); + + N(SyntaxKind.CollectionExpression); + N(SyntaxKind.OpenBracketToken); + + for (var i = 1; i <= count; i++) + { + N(SyntaxKind.ExpressionElement); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, $"A{i}"); + } + + if (i < count) + { + N(SyntaxKind.CommaToken); + } + } + + N(SyntaxKind.CloseBracketToken); + EOF(); + } } diff --git a/src/Compilers/Core/CodeAnalysisTest/Collections/TemporaryArrayTests.cs b/src/Compilers/Core/CodeAnalysisTest/Collections/TemporaryArrayTests.cs index 07f981a25f40b..57069ea7a58d2 100644 --- a/src/Compilers/Core/CodeAnalysisTest/Collections/TemporaryArrayTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/Collections/TemporaryArrayTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis.Shared.Collections; @@ -205,6 +206,69 @@ public void TestReverseContents([CombinatorialRange(0, 6)] int initialItems) Assert.Equal(array[i], initialItems - 1 - i); } + [Fact] + public void TestSort() + { + // Create arrays with different lengths, making sure to exceed the number of inline elements to test all code paths. + for (int i = 0; i <= TemporaryArray.TestAccessor.InlineCapacity + 1; i++) + { + foreach (var permutation in permute(Enumerable.Range(0, i).ToArray())) + { + assertSort(permutation); + } + } + + static void assertSort(ImmutableArray inputArray) + { + var sortedArray = inputArray.Sort(); + Assert.Equal(inputArray.Length, sortedArray.Length); + using var array = TemporaryArray.Empty; + foreach (var num in inputArray) + array.Add(num); + + Assert.Equal(array.Count, sortedArray.Length); + array.Sort((x, y) => x.CompareTo(y)); + Assert.Equal(array.Count, sortedArray.Length); + for (int i = 0; i < array.Count; i++) + { + Assert.Equal(array[i], sortedArray[i]); + } + } + + // Almost copy from ServiceHubServicesTests + static List> permute(T[] values) + { + var result = new List>(); + if (values.Length == 0) + { + result.Add(ImmutableArray.Empty); + return result; + } + + doPermute(0, values.Length - 1); + return result; + + void doPermute(int start, int end) + { + if (start == end) + { + // We have one of our possible n! solutions, + // add it to the list. + result.Add(values.ToImmutableArray()); + } + else + { + for (var i = start; i <= end; i++) + { + (values[start], values[i]) = (values[i], values[start]); + doPermute(start + 1, end); + (values[start], values[i]) = (values[i], values[start]); + } + } + } + } + } + [Theory, CombinatorialData] public void TestRemoveLast([CombinatorialRange(0, 6)] int initialItems) { @@ -238,5 +302,23 @@ public void TestContains([CombinatorialRange(0, 6)] int initialItems) Assert.False(array.Contains(-1)); Assert.False(array.Contains(initialItems)); } + + [Fact] + public void TestSingleOrDefault() + { + using var array = TemporaryArray.Empty; + array.Add(1); + array.Add(2); + array.Add(3); + + Assert.Equal(3, array.SingleOrDefault(p => p > 2)); + Assert.Equal(3, array.SingleOrDefault((p, arg) => p > arg, arg: 2)); + + Assert.Equal(0, array.SingleOrDefault(p => p > 5)); + Assert.Equal(0, array.SingleOrDefault((p, arg) => p > arg, arg: 5)); + + Assert.Throws(() => array.SingleOrDefault(p => p > 1)); + Assert.Throws(() => array.SingleOrDefault((p, arg) => p > arg, arg: 1)); + } } } diff --git a/src/Compilers/Core/CommandLine/BuildProtocol.cs b/src/Compilers/Core/CommandLine/BuildProtocol.cs index c1faa26c50edd..b2fa1ec0b622a 100644 --- a/src/Compilers/Core/CommandLine/BuildProtocol.cs +++ b/src/Compilers/Core/CommandLine/BuildProtocol.cs @@ -53,7 +53,7 @@ internal sealed class BuildRequest /// private const int MaximumRequestSize = 0x500000; - public readonly Guid RequestId; + public readonly string RequestId; public readonly RequestLanguage Language; public readonly ReadOnlyCollection Arguments; public readonly string CompilerHash; @@ -61,9 +61,9 @@ internal sealed class BuildRequest public BuildRequest(RequestLanguage language, string compilerHash, IEnumerable arguments, - Guid? requestId = null) + string? requestId = null) { - RequestId = requestId ?? Guid.Empty; + RequestId = requestId ?? ""; Language = language; Arguments = new ReadOnlyCollection(arguments.ToList()); CompilerHash = compilerHash; @@ -76,7 +76,7 @@ public static BuildRequest Create(RequestLanguage language, string workingDirectory, string? tempDirectory, string compilerHash, - Guid? requestId = null, + string? requestId = null, string? keepAlive = null, string? libDirectory = null) { @@ -142,7 +142,7 @@ public static async Task ReadAsync(Stream inStream, CancellationTo // Parse the request into the Request data structure. using var reader = new BinaryReader(new MemoryStream(requestBuffer), Encoding.Unicode); - var requestId = readGuid(reader); + var requestId = reader.ReadString(); var language = (RequestLanguage)reader.ReadUInt32(); var compilerHash = reader.ReadString(); uint argumentCount = reader.ReadUInt32(); @@ -158,18 +158,6 @@ public static async Task ReadAsync(Stream inStream, CancellationTo compilerHash, argumentsBuilder, requestId); - - static Guid readGuid(BinaryReader reader) - { - const int size = 16; - var bytes = new byte[size]; - if (size != reader.Read(bytes, 0, size)) - { - throw new InvalidOperationException(); - } - - return new Guid(bytes); - } } /// @@ -179,7 +167,7 @@ static Guid readGuid(BinaryReader reader) { using var memoryStream = new MemoryStream(); using var writer = new BinaryWriter(memoryStream, Encoding.Unicode); - writer.Write(RequestId.ToByteArray()); + writer.Write(RequestId); writer.Write((uint)Language); writer.Write(CompilerHash); writer.Write(Arguments.Count); diff --git a/src/Compilers/Core/MSBuildTask/ManagedCompiler.cs b/src/Compilers/Core/MSBuildTask/ManagedCompiler.cs index f7ff0c9687b55..b2a9dfe48325f 100644 --- a/src/Compilers/Core/MSBuildTask/ManagedCompiler.cs +++ b/src/Compilers/Core/MSBuildTask/ManagedCompiler.cs @@ -297,6 +297,12 @@ public bool Prefer32Bit get { return _store.GetOrDefault(nameof(Prefer32Bit), false); } } + public string? ProjectName + { + set { _store[nameof(ProjectName)] = value; } + get { return (string?)_store[nameof(ProjectName)]; } + } + public bool ProvideCommandLineArgs { set { _store[nameof(ProvideCommandLineArgs)] = value; } @@ -377,6 +383,12 @@ public string? SubsystemVersion get { return (string?)_store[nameof(SubsystemVersion)]; } } + public string? TargetFramework + { + set { _store[nameof(TargetFramework)] = value; } + get { return (string?)_store[nameof(TargetFramework)]; } + } + public string? TargetType { set @@ -505,7 +517,7 @@ internal int ExecuteTool(string pathToTool, string responseFileCommands, string try { - var requestId = Guid.NewGuid(); + var requestId = getRequestId(); logger.Log($"Compilation request {requestId}, PathToTool={pathToTool}"); string workingDirectory = CurrentDirectoryToUse(); @@ -574,6 +586,20 @@ internal int ExecuteTool(string pathToTool, string responseFileCommands, string } return ExitCode; + + // Construct the friendly name for the compilation. This does not need to be unique. Instead + // it's used by developers to understand what compilation is running on the server. + string getRequestId() + { + if (!string.IsNullOrEmpty(ProjectName)) + { + return string.IsNullOrEmpty(TargetFramework) + ? ProjectName + : $"{ProjectName} ({TargetFramework})"; + } + + return $"Unnamed compilation {Guid.NewGuid()}"; + } } /// @@ -638,7 +664,7 @@ private string CurrentDirectoryToUse() /// Handle a response from the server, reporting messages and returning /// the appropriate exit code. /// - private int HandleResponse(Guid requestId, BuildResponse? response, string pathToTool, string responseFileCommands, string commandLineCommands, ICompilerServerLogger logger) + private int HandleResponse(string requestId, BuildResponse? response, string pathToTool, string responseFileCommands, string commandLineCommands, ICompilerServerLogger logger) { #if BOOTSTRAP if (!ValidateBootstrapResponse(response)) @@ -766,7 +792,7 @@ private bool ValidateBootstrapResponse(BuildResponse? response) /// These are intended to be processed by automation in the binlog hence do not change the structure of /// the messages here. /// - private void LogCompilationMessage(ICompilerServerLogger logger, Guid requestId, CompilationKind kind, string diagnostic) + private void LogCompilationMessage(ICompilerServerLogger logger, string requestId, CompilationKind kind, string diagnostic) { var category = kind switch { diff --git a/src/Compilers/Core/MSBuildTask/Microsoft.CSharp.Core.targets b/src/Compilers/Core/MSBuildTask/Microsoft.CSharp.Core.targets index 1f722afd4619b..241ad2307c945 100644 --- a/src/Compilers/Core/MSBuildTask/Microsoft.CSharp.Core.targets +++ b/src/Compilers/Core/MSBuildTask/Microsoft.CSharp.Core.targets @@ -50,7 +50,8 @@ @(AdditionalFiles); @(EmbeddedFiles); @(Analyzer); - @(EditorConfigFiles)" + @(EditorConfigFiles); + $(SourceLink)" Outputs="@(DocFileItem); @(IntermediateAssembly); @(IntermediateRefAssembly); @@ -134,6 +135,7 @@ Platform="$(PlatformTarget)" Prefer32Bit="$(Prefer32Bit)" PreferredUILang="$(PreferredUILang)" + ProjectName="$(MSBuildProjectName)" ProvideCommandLineArgs="$(ProvideCommandLineArgs)" References="@(ReferencePathWithRefAssemblies)" RefOnly="$(ProduceOnlyReferenceAssembly)" @@ -148,6 +150,7 @@ Sources="@(Compile)" SubsystemVersion="$(SubsystemVersion)" TargetType="$(OutputType)" + TargetFramework="$(TargetFramework)" ToolExe="$(CscToolExe)" ToolPath="$(CscToolPath)" TreatWarningsAsErrors="$(TreatWarningsAsErrors)" diff --git a/src/Compilers/Core/MSBuildTask/Microsoft.VisualBasic.Core.targets b/src/Compilers/Core/MSBuildTask/Microsoft.VisualBasic.Core.targets index 0e6b49ddc15ce..1b92edaabf983 100644 --- a/src/Compilers/Core/MSBuildTask/Microsoft.VisualBasic.Core.targets +++ b/src/Compilers/Core/MSBuildTask/Microsoft.VisualBasic.Core.targets @@ -20,7 +20,8 @@ @(AdditionalFiles); @(EmbeddedFiles); @(Analyzer); - @(EditorConfigFiles)" + @(EditorConfigFiles); + $(SourceLink)" Outputs="@(DocFileItem); @(IntermediateAssembly); @(IntermediateRefAssembly); @@ -94,6 +95,7 @@ Platform="$(PlatformTarget)" Prefer32Bit="$(Prefer32Bit)" PreferredUILang="$(PreferredUILang)" + ProjectName="$(MSBuildProjectName)" ProvideCommandLineArgs="$(ProvideCommandLineArgs)" References="@(ReferencePathWithRefAssemblies)" RefOnly="$(ProduceOnlyReferenceAssembly)" @@ -112,6 +114,7 @@ SubsystemVersion="$(SubsystemVersion)" TargetCompactFramework="$(TargetCompactFramework)" TargetType="$(OutputType)" + TargetFramework="$(TargetFramework)" ToolExe="$(VbcToolExe)" ToolPath="$(VbcToolPath)" TreatWarningsAsErrors="$(TreatWarningsAsErrors)" diff --git a/src/Compilers/Core/Portable/AssemblyUtilities.cs b/src/Compilers/Core/Portable/AssemblyUtilities.cs index 87a673277970d..42aa4adc4df52 100644 --- a/src/Compilers/Core/Portable/AssemblyUtilities.cs +++ b/src/Compilers/Core/Portable/AssemblyUtilities.cs @@ -13,28 +13,8 @@ namespace Roslyn.Utilities { - internal static class AssemblyUtilities + internal static partial class AssemblyUtilities { - /// - /// Given a path to an assembly, returns its MVID (Module Version ID). - /// May throw. - /// - /// If the file at does not exist or cannot be accessed. - /// If the file is not an assembly or is somehow corrupted. - public static Guid ReadMvid(string filePath) - { - RoslynDebug.Assert(PathUtilities.IsAbsolute(filePath)); - - using (var reader = new PEReader(FileUtilities.OpenRead(filePath))) - { - var metadataReader = reader.GetMetadataReader(); - var mvidHandle = metadataReader.GetModuleDefinition().Mvid; - var fileMvid = metadataReader.GetGuid(mvidHandle); - - return fileMvid; - } - } - /// /// Given a path to an assembly, finds the paths to all of its satellite /// assemblies. diff --git a/src/Compilers/Core/Portable/AssemblyUtilitiesCore.cs b/src/Compilers/Core/Portable/AssemblyUtilitiesCore.cs new file mode 100644 index 0000000000000..c7ad478d9bc1e --- /dev/null +++ b/src/Compilers/Core/Portable/AssemblyUtilitiesCore.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Microsoft.CodeAnalysis; + +namespace Roslyn.Utilities; + +/// +/// This partial contains methods that must be shared by source with the workspaces layer +/// +internal static partial class AssemblyUtilities +{ + /// + /// Given a path to an assembly, returns its MVID (Module Version ID). + /// May throw. + /// + /// If the file at does not exist or cannot be accessed. + /// If the file is not an assembly or is somehow corrupted. + public static Guid ReadMvid(string filePath) + { + RoslynDebug.Assert(PathUtilities.IsAbsolute(filePath)); + + using (var reader = new PEReader(FileUtilities.OpenRead(filePath))) + { + var metadataReader = reader.GetMetadataReader(); + var mvidHandle = metadataReader.GetModuleDefinition().Mvid; + var fileMvid = metadataReader.GetGuid(mvidHandle); + + return fileMvid; + } + } +} diff --git a/src/Compilers/Core/Portable/CodeAnalysisEventSource.cs b/src/Compilers/Core/Portable/CodeAnalysisEventSource.cs index 95bdff05062b1..0808ff7ce5515 100644 --- a/src/Compilers/Core/Portable/CodeAnalysisEventSource.cs +++ b/src/Compilers/Core/Portable/CodeAnalysisEventSource.cs @@ -23,6 +23,7 @@ public static class Tasks public const EventTask GeneratorDriverRunTime = (EventTask)1; public const EventTask SingleGeneratorRunTime = (EventTask)2; public const EventTask BuildStateTable = (EventTask)3; + public const EventTask Compilation = (EventTask)4; } private CodeAnalysisEventSource() { } @@ -95,6 +96,12 @@ internal unsafe void NodeTransform(int nodeHashCode, string name, string tableTy } } + [Event(7, Message = "Server compilation {0} started", Keywords = Keywords.Performance, Level = EventLevel.Informational, Opcode = EventOpcode.Start, Task = Tasks.Compilation)] + internal void StartServerCompilation(string name) => WriteEvent(7, name); + + [Event(8, Message = "Server compilation {0} completed", Keywords = Keywords.Performance, Level = EventLevel.Informational, Opcode = EventOpcode.Stop, Task = Tasks.Compilation)] + internal void StopServerCompilation(string name) => WriteEvent(8, name); + private static unsafe EventData GetEventDataForString(string value, char* ptr) { fixed (char* ptr2 = value) diff --git a/src/Compilers/Core/Portable/Collections/ImmutableArrayExtensions.cs b/src/Compilers/Core/Portable/Collections/ImmutableArrayExtensions.cs index 3da802ad40fb9..f837871a5f7dc 100644 --- a/src/Compilers/Core/Portable/Collections/ImmutableArrayExtensions.cs +++ b/src/Compilers/Core/Portable/Collections/ImmutableArrayExtensions.cs @@ -349,6 +349,9 @@ public static ImmutableArray SelectManyAsArray(this Imm /// public static async ValueTask> SelectAsArrayAsync(this ImmutableArray array, Func> selector, CancellationToken cancellationToken) { + if (array.IsEmpty) + return ImmutableArray.Empty; + var builder = ArrayBuilder.GetInstance(array.Length); foreach (var item in array) @@ -364,6 +367,9 @@ public static async ValueTask> SelectAsArrayAsync public static async ValueTask> SelectAsArrayAsync(this ImmutableArray array, Func> selector, TArg arg, CancellationToken cancellationToken) { + if (array.IsEmpty) + return ImmutableArray.Empty; + var builder = ArrayBuilder.GetInstance(array.Length); foreach (var item in array) @@ -864,7 +870,48 @@ internal static ImmutableArray AddRange(this ImmutableArray self, in Te return builder.ToImmutableAndFree(); } - internal static bool HasDuplicates(this ImmutableArray array, IEqualityComparer? comparer = null) + /// + /// Determines whether duplicates exist using default equality comparer. + /// + /// Array to search for duplicates + /// Whether duplicates were found + internal static bool HasDuplicates(this ImmutableArray array) + { + switch (array.Length) + { + case 0: + case 1: + return false; + + case 2: + return EqualityComparer.Default.Equals(array[0], array[1]); + + default: + var set = PooledHashSet.GetInstance(); + var foundDuplicate = false; + + foreach (var element in array) + { + if (!set.Add(element)) + { + foundDuplicate = true; + break; + } + } + + set.Free(); + return foundDuplicate; + } + } + + /// + /// Determines whether duplicates exist using . Use other override + /// if you don't need a custom comparer. + /// + /// Array to search for duplicates + /// Comparer to use in search + /// Whether duplicates were found + internal static bool HasDuplicates(this ImmutableArray array, IEqualityComparer comparer) { switch (array.Length) { @@ -878,9 +925,9 @@ internal static bool HasDuplicates(this ImmutableArray array, IEqualityCom default: var set = new HashSet(comparer); - foreach (var i in array) + foreach (var element in array) { - if (!set.Add(i)) + if (!set.Add(element)) { return true; } diff --git a/src/Compilers/Core/Portable/Collections/TemporaryArrayExtensions.cs b/src/Compilers/Core/Portable/Collections/TemporaryArrayExtensions.cs index b332be2cba044..62ef11b4bb4d0 100644 --- a/src/Compilers/Core/Portable/Collections/TemporaryArrayExtensions.cs +++ b/src/Compilers/Core/Portable/Collections/TemporaryArrayExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Linq; using System.Runtime.CompilerServices; namespace Microsoft.CodeAnalysis.Shared.Collections @@ -61,6 +62,51 @@ public static bool All(this in TemporaryArray array, Func predica return true; } + private static void ThrowSequenceContainsMoreThanOneElement() + => new[] { 0, 0 }.Single(); + + public static T? SingleOrDefault(this in TemporaryArray array, Func predicate) + { + var first = true; + T? result = default; + foreach (var item in array) + { + if (predicate(item)) + { + if (!first) + { + ThrowSequenceContainsMoreThanOneElement(); + } + + first = false; + result = item; + } + } + + return result; + } + + public static T? SingleOrDefault(this in TemporaryArray array, Func predicate, TArg arg) + { + var first = true; + T? result = default; + foreach (var item in array) + { + if (predicate(item, arg)) + { + if (!first) + { + ThrowSequenceContainsMoreThanOneElement(); + } + + first = false; + result = item; + } + } + + return result; + } + public static void AddIfNotNull(this ref TemporaryArray array, T? value) where T : struct { diff --git a/src/Compilers/Core/Portable/Collections/TemporaryArray`1.cs b/src/Compilers/Core/Portable/Collections/TemporaryArray`1.cs index 2a41e9c8aca37..72e9ac3c8196b 100644 --- a/src/Compilers/Core/Portable/Collections/TemporaryArray`1.cs +++ b/src/Compilers/Core/Portable/Collections/TemporaryArray`1.cs @@ -351,6 +351,59 @@ public void ReverseContents() } } + public void Sort(Comparison compare) + { + if (_builder is not null) + { + _builder.Sort(compare); + return; + } + + switch (_count) + { + case <= 1: + return; + case 2: + if (compare(_item0, _item1) > 0) + { + (_item0, _item1) = (_item1, _item0); + } + return; + case 3: + if (compare(_item0, _item1) > 0) + (_item0, _item1) = (_item1, _item0); + + if (compare(_item1, _item2) > 0) + { + (_item1, _item2) = (_item2, _item1); + + if (compare(_item0, _item1) > 0) + (_item0, _item1) = (_item1, _item0); + } + return; + case 4: + + if (compare(_item0, _item1) > 0) + (_item0, _item1) = (_item1, _item0); + + if (compare(_item2, _item3) > 0) + (_item2, _item3) = (_item3, _item2); + + if (compare(_item0, _item2) > 0) + (_item0, _item2) = (_item2, _item0); + + if (compare(_item1, _item3) > 0) + (_item1, _item3) = (_item3, _item1); + + if (compare(_item1, _item2) > 0) + (_item1, _item2) = (_item2, _item1); + + return; + default: + throw ExceptionUtilities.Unreachable(); + } + } + /// /// Throws . /// diff --git a/src/Compilers/Core/Portable/CommandLine/AnalyzerConfig.SectionNameMatching.cs b/src/Compilers/Core/Portable/CommandLine/AnalyzerConfig.SectionNameMatching.cs index 3817da8a8c5a9..674cbd93964c9 100644 --- a/src/Compilers/Core/Portable/CommandLine/AnalyzerConfig.SectionNameMatching.cs +++ b/src/Compilers/Core/Portable/CommandLine/AnalyzerConfig.SectionNameMatching.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Text; using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; @@ -15,6 +17,8 @@ namespace Microsoft.CodeAnalysis { public sealed partial class AnalyzerConfig { + private static readonly ConcurrentDictionary s_regexMap = []; + internal readonly struct SectionNameMatcher { private readonly ImmutableArray<(int minValue, int maxValue)> _numberRangePairs; @@ -108,10 +112,13 @@ public bool IsMatch(string s) numberRangePairs.Free(); return null; } + sb.Append('$'); - return new SectionNameMatcher( - new Regex(sb.ToString(), RegexOptions.Compiled), - numberRangePairs.ToImmutableAndFree()); + + var pattern = sb.ToString(); + var regex = s_regexMap.GetOrAdd(pattern, static pattern => new(pattern, RegexOptions.Compiled)); + + return new SectionNameMatcher(regex, numberRangePairs.ToImmutableAndFree()); } internal static string UnescapeSectionName(string sectionName) diff --git a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs index b7937a5c08ed5..a33725e092e09 100644 --- a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs +++ b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs @@ -802,6 +802,7 @@ public virtual int Run(TextWriter consoleOutput, CancellationToken cancellationT /// Perform source generation, if the compiler supports it. /// /// The compilation before any source generation has occurred. + /// The base directory for the of generated files. /// The to use when parsing any generated sources. /// The generators to run /// A provider that returns analyzer config options. @@ -810,12 +811,15 @@ public virtual int Run(TextWriter consoleOutput, CancellationToken cancellationT /// A compilation that represents the original compilation with any additional, generated texts added to it. private protected (Compilation Compilation, GeneratorDriverTimingInfo DriverTimingInfo) RunGenerators( Compilation input, + string generatedFilesBaseDirectory, ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray additionalTexts, DiagnosticBag generatorDiagnostics) { + Debug.Assert(generatedFilesBaseDirectory is not null); + GeneratorDriver? driver = null; string cacheKey = string.Empty; bool disableCache = @@ -830,7 +834,7 @@ private protected (Compilation Compilation, GeneratorDriverTimingInfo DriverTimi .ReplaceAdditionalTexts(additionalTexts); } - driver ??= CreateGeneratorDriver(parseOptions, generators, analyzerConfigOptionsProvider, additionalTexts); + driver ??= CreateGeneratorDriver(generatedFilesBaseDirectory, parseOptions, generators, analyzerConfigOptionsProvider, additionalTexts); driver = driver.RunGeneratorsAndUpdateCompilation(input, out var compilationOut, out var diagnostics); generatorDiagnostics.AddRange(diagnostics); @@ -865,7 +869,7 @@ string deriveCacheKey() } } - private protected abstract GeneratorDriver CreateGeneratorDriver(ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray additionalTexts); + private protected abstract GeneratorDriver CreateGeneratorDriver(string baseDirectory, ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray additionalTexts); private int RunCore(TextWriter consoleOutput, ErrorLogger? errorLogger, CancellationToken cancellationToken) { @@ -1136,10 +1140,12 @@ private void CompileAndEmit( { // At this point we have a compilation with nothing yet computed. // We pass it to the generators, which will realize any symbols they require. - (compilation, generatorTimingInfo) = RunGenerators(compilation, Arguments.ParseOptions, generators, analyzerConfigProvider, additionalTextFiles, diagnostics); + var explicitGeneratedOutDir = Arguments.GeneratedFilesOutputDirectory; + var hasExplicitGeneratedOutDir = !string.IsNullOrWhiteSpace(explicitGeneratedOutDir); + var baseDirectory = hasExplicitGeneratedOutDir ? explicitGeneratedOutDir! : Arguments.OutputDirectory; + (compilation, generatorTimingInfo) = RunGenerators(compilation, baseDirectory, Arguments.ParseOptions, generators, analyzerConfigProvider, additionalTextFiles, diagnostics); bool hasAnalyzerConfigs = !Arguments.AnalyzerConfigPaths.IsEmpty; - bool hasGeneratedOutputPath = !string.IsNullOrWhiteSpace(Arguments.GeneratedFilesOutputDirectory); var generatedSyntaxTrees = compilation.SyntaxTrees.Skip(Arguments.SourceFiles.Length).ToList(); var analyzerOptionsBuilder = hasAnalyzerConfigs ? ArrayBuilder.GetInstance(generatedSyntaxTrees.Count) : null; var embeddedTextBuilder = ArrayBuilder.GetInstance(generatedSyntaxTrees.Count); @@ -1159,11 +1165,12 @@ private void CompileAndEmit( analyzerOptionsBuilder.Add(analyzerConfigSet!.GetOptionsForSourcePath(tree.FilePath)); } - // write out the file if we have an output path - if (hasGeneratedOutputPath) + // write out the file if an output path was explicitly provided + if (hasExplicitGeneratedOutDir) { - var path = Path.Combine(Arguments.GeneratedFilesOutputDirectory!, tree.FilePath); - if (Directory.Exists(Arguments.GeneratedFilesOutputDirectory)) + var path = tree.FilePath; + Debug.Assert(path.StartsWith(explicitGeneratedOutDir!)); + if (Directory.Exists(explicitGeneratedOutDir)) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); } diff --git a/src/Compilers/Core/Portable/Compilation/Compilation.cs b/src/Compilers/Core/Portable/Compilation/Compilation.cs index e10e72b1aed2a..7dba509dcc2b7 100644 --- a/src/Compilers/Core/Portable/Compilation/Compilation.cs +++ b/src/Compilers/Core/Portable/Compilation/Compilation.cs @@ -303,28 +303,33 @@ public Compilation Clone() /// /// True if the SemanticModel should ignore accessibility rules when answering semantic questions. /// +#pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads public SemanticModel GetSemanticModel(SyntaxTree syntaxTree, bool ignoreAccessibility = false) - => CommonGetSemanticModel(syntaxTree, ignoreAccessibility); +#pragma warning restore RS0027 +#pragma warning disable RSEXPERIMENTAL001 // internal usage of experimental API + => GetSemanticModel(syntaxTree, ignoreAccessibility ? SemanticModelOptions.IgnoreAccessibility : SemanticModelOptions.None); +#pragma warning restore RSEXPERIMENTAL001 + + [Experimental(RoslynExperiments.NullableDisabledSemanticModel, UrlFormat = RoslynExperiments.NullableDisabledSemanticModel_Url)] + public SemanticModel GetSemanticModel(SyntaxTree syntaxTree, SemanticModelOptions options) + => CommonGetSemanticModel(syntaxTree, options); /// /// Gets a for the given . - /// If is non-null, it attempts to use - /// to get a semantic model. Otherwise, it creates a new semantic model using . + /// If is non-null, it attempts to use + /// to get a semantic model. Otherwise, it creates a new semantic model using . /// - /// - /// - /// - protected abstract SemanticModel CommonGetSemanticModel(SyntaxTree syntaxTree, bool ignoreAccessibility); + [Experimental(RoslynExperiments.NullableDisabledSemanticModel, UrlFormat = RoslynExperiments.NullableDisabledSemanticModel_Url)] + protected abstract SemanticModel CommonGetSemanticModel(SyntaxTree syntaxTree, SemanticModelOptions options); /// /// Creates a new for the given . - /// Unlike the and , + /// Unlike the and , /// it does not attempt to use the to get a semantic model, but instead always creates a new semantic model. /// - /// - /// - /// - internal abstract SemanticModel CreateSemanticModel(SyntaxTree syntaxTree, bool ignoreAccessibility); +#pragma warning disable RSEXPERIMENTAL001 // internal usage of experimental API + internal abstract SemanticModel CreateSemanticModel(SyntaxTree syntaxTree, SemanticModelOptions options); +#pragma warning restore RSEXPERIMENTAL001 // internal usage of experimental API /// /// Returns a new INamedTypeSymbol representing an error type with the given name and arity @@ -1134,11 +1139,9 @@ public INamedTypeSymbol CreateNativeIntegerTypeSymbol(bool signed) // hash code conflicts, but seems to do the trick. The size is mostly arbitrary. My guess // is that there are maybe a couple dozen analyzers in the solution and each one has // ~0-2 unique well-known types, and the chance of hash collision is very low. - private readonly ConcurrentCache _getTypeCache = - new ConcurrentCache(50, ReferenceEqualityComparer.Instance); + private ConcurrentCache? _getTypeCache; - private readonly ConcurrentCache> _getTypesCache = - new ConcurrentCache>(50, ReferenceEqualityComparer.Instance); + private ConcurrentCache>? _getTypesCache; /// /// Gets the type within the compilation's assembly and all referenced assemblies (other than @@ -1182,12 +1185,15 @@ public INamedTypeSymbol CreateNativeIntegerTypeSymbol(bool signed) /// public INamedTypeSymbol? GetTypeByMetadataName(string fullyQualifiedMetadataName) { - if (!_getTypeCache.TryGetValue(fullyQualifiedMetadataName, out INamedTypeSymbol? val)) + var getTypeCache = RoslynLazyInitializer.EnsureInitialized( + ref _getTypeCache, static () => new ConcurrentCache(50, ReferenceEqualityComparer.Instance)); + + if (!getTypeCache.TryGetValue(fullyQualifiedMetadataName, out INamedTypeSymbol? val)) { val = CommonGetTypeByMetadataName(fullyQualifiedMetadataName); - var result = _getTypeCache.TryAdd(fullyQualifiedMetadataName, val); + var result = getTypeCache.TryAdd(fullyQualifiedMetadataName, val); Debug.Assert(result - || !_getTypeCache.TryGetValue(fullyQualifiedMetadataName, out var addedType) // Could fail if the type was already evicted from the cache + || !getTypeCache.TryGetValue(fullyQualifiedMetadataName, out var addedType) // Could fail if the type was already evicted from the cache || ReferenceEquals(addedType, val)); } return val; @@ -1210,12 +1216,15 @@ public INamedTypeSymbol CreateNativeIntegerTypeSymbol(bool signed) /// public ImmutableArray GetTypesByMetadataName(string fullyQualifiedMetadataName) { - if (!_getTypesCache.TryGetValue(fullyQualifiedMetadataName, out ImmutableArray val)) + var getTypesCache = RoslynLazyInitializer.EnsureInitialized( + ref _getTypesCache, static () => new ConcurrentCache>(50, ReferenceEqualityComparer.Instance)); + + if (!getTypesCache.TryGetValue(fullyQualifiedMetadataName, out ImmutableArray val)) { val = getTypesByMetadataNameImpl(); - var result = _getTypesCache.TryAdd(fullyQualifiedMetadataName, val); + var result = getTypesCache.TryAdd(fullyQualifiedMetadataName, val); Debug.Assert(result - || !_getTypesCache.TryGetValue(fullyQualifiedMetadataName, out var addedArray) // Could fail if the type was already evicted from the cache + || !getTypesCache.TryGetValue(fullyQualifiedMetadataName, out var addedArray) // Could fail if the type was already evicted from the cache || Enumerable.SequenceEqual(addedArray, val, ReferenceEqualityComparer.Instance)); } diff --git a/src/Compilers/Core/Portable/Compilation/SemanticModel.cs b/src/Compilers/Core/Portable/Compilation/SemanticModel.cs index aa4c5cf22c76b..2df6f1d68b4c6 100644 --- a/src/Compilers/Core/Portable/Compilation/SemanticModel.cs +++ b/src/Compilers/Core/Portable/Compilation/SemanticModel.cs @@ -88,6 +88,9 @@ public virtual bool IgnoresAccessibility get { return false; } } + [Experimental(RoslynExperiments.NullableDisabledSemanticModel, UrlFormat = RoslynExperiments.NullableDisabledSemanticModel_Url)] + public abstract bool NullableAnalysisIsDisabled { get; } + /// /// Gets symbol information about a syntax node. /// diff --git a/src/Compilers/Core/Portable/Compilation/SemanticModelOptions.cs b/src/Compilers/Core/Portable/Compilation/SemanticModelOptions.cs new file mode 100644 index 0000000000000..1edbbd31a1440 --- /dev/null +++ b/src/Compilers/Core/Portable/Compilation/SemanticModelOptions.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.CodeAnalysis; + +[Experimental(RoslynExperiments.NullableDisabledSemanticModel, UrlFormat = RoslynExperiments.NullableDisabledSemanticModel_Url)] +[Flags] +public enum SemanticModelOptions +{ + None = 0, + IgnoreAccessibility = 1 << 0, + DisableNullableAnalysis = 1 << 1 +} diff --git a/src/Compilers/Core/Portable/Compilation/SemanticModelProvider.cs b/src/Compilers/Core/Portable/Compilation/SemanticModelProvider.cs index af6cbb5522abe..37956159d30e3 100644 --- a/src/Compilers/Core/Portable/Compilation/SemanticModelProvider.cs +++ b/src/Compilers/Core/Portable/Compilation/SemanticModelProvider.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#pragma warning disable RSEXPERIMENTAL001 // internal usage of experimental API + namespace Microsoft.CodeAnalysis { /// @@ -13,6 +15,6 @@ internal abstract class SemanticModelProvider /// /// Gets a for the given that belongs to the given . /// - public abstract SemanticModel GetSemanticModel(SyntaxTree tree, Compilation compilation, bool ignoreAccessibility = false); + public abstract SemanticModel GetSemanticModel(SyntaxTree tree, Compilation compilation, SemanticModelOptions options = default); } } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptionsExtensions.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptionsExtensions.cs index 11c36ca3dd671..e7b8ac7b8f72d 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptionsExtensions.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptionsExtensions.cs @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Threading; -using Roslyn.Utilities; +using Microsoft.CodeAnalysis.InternalUtilities; namespace Microsoft.CodeAnalysis.Diagnostics { @@ -16,9 +14,10 @@ internal static class AnalyzerOptionsExtensions private const string SeveritySuffix = "severity"; private const string DotnetAnalyzerDiagnosticSeverityKey = DotnetAnalyzerDiagnosticPrefix + "." + SeveritySuffix; + private static readonly ConcurrentLruCache s_categoryToSeverityKeyMap = new ConcurrentLruCache(50); private static string GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(string category) - => $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{category}.{SeveritySuffix}"; + => s_categoryToSeverityKeyMap.GetOrAdd(category, category, static category => $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{category}.{SeveritySuffix}"); /// /// Tries to get configured severity for the given diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/CachingSemanticModelProvider.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/CachingSemanticModelProvider.cs index 1f5b93d0583ee..573045bbf5733 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/CachingSemanticModelProvider.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/CachingSemanticModelProvider.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#pragma warning disable RSEXPERIMENTAL001 // internal usage of experimental API + using System; using System.Collections.Concurrent; using System.Runtime.CompilerServices; @@ -31,8 +33,8 @@ public CachingSemanticModelProvider() _providerCache = new ConditionalWeakTable(); } - public override SemanticModel GetSemanticModel(SyntaxTree tree, Compilation compilation, bool ignoreAccessibility = false) - => _providerCache.GetValue(compilation, s_createProviderCallback).GetSemanticModel(tree, ignoreAccessibility); + public override SemanticModel GetSemanticModel(SyntaxTree tree, Compilation compilation, SemanticModelOptions options = default) + => _providerCache.GetValue(compilation, s_createProviderCallback).GetSemanticModel(tree, options); internal void ClearCache(SyntaxTree tree, Compilation compilation) { @@ -60,15 +62,15 @@ public PerCompilationProvider(Compilation compilation) { _compilation = compilation; _semanticModelsMap = new ConcurrentDictionary(); - _createSemanticModel = tree => compilation.CreateSemanticModel(tree, ignoreAccessibility: false); + _createSemanticModel = tree => compilation.CreateSemanticModel(tree, options: default); } - public SemanticModel GetSemanticModel(SyntaxTree tree, bool ignoreAccessibility) + public SemanticModel GetSemanticModel(SyntaxTree tree, SemanticModelOptions options) { // We only care about caching semantic models for internal callers, which use the default 'ignoreAccessibility = false'. - return !ignoreAccessibility + return options == SemanticModelOptions.None ? _semanticModelsMap.GetOrAdd(tree, _createSemanticModel) - : _compilation.CreateSemanticModel(tree, ignoreAccessibility: true); + : _compilation.CreateSemanticModel(tree, options); } public void ClearCachedSemanticModel(SyntaxTree tree) diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs index 999339e2c254f..db53a1ae198fc 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs @@ -10,6 +10,9 @@ using System.Threading; using System.Threading.Tasks; using System.Collections.Immutable; +using Roslyn.Utilities; +using System.Reflection.PortableExecutable; +using System.Reflection.Metadata; #if NETCOREAPP using System.Runtime.Loader; @@ -35,14 +38,11 @@ internal sealed class ShadowCopyAnalyzerAssemblyLoader : AnalyzerAssemblyLoader /// private readonly Lazy<(string directory, Mutex)> _shadowCopyDirectoryAndMutex; - /// - /// Used to generate unique names for per-assembly directories. Should be updated with . - /// - private int _assemblyDirectoryId; + private readonly ConcurrentDictionary> _mvidPathMap = new ConcurrentDictionary>(); internal string BaseDirectory => _baseDirectory; - internal int CopyCount => _assemblyDirectoryId; + internal int CopyCount => _mvidPathMap.Count; #if NETCOREAPP public ShadowCopyAnalyzerAssemblyLoader(string baseDirectory) @@ -131,26 +131,58 @@ private void DeleteLeftoverDirectories() protected override string PreparePathToLoad(string originalAnalyzerPath, ImmutableHashSet cultureNames) { - var analyzerFileName = Path.GetFileName(originalAnalyzerPath); - var shadowDirectory = CreateUniqueDirectoryForAssembly(); - var shadowAnalyzerPath = Path.Combine(shadowDirectory, analyzerFileName); - copyFile(originalAnalyzerPath, shadowAnalyzerPath); - - if (cultureNames.IsEmpty) + var mvid = AssemblyUtilities.ReadMvid(originalAnalyzerPath); + if (_mvidPathMap.TryGetValue(mvid, out Task? copyTask)) { - return shadowAnalyzerPath; + return copyTask.Result; } - var originalDirectory = Path.GetDirectoryName(originalAnalyzerPath)!; - var satelliteFileName = GetSatelliteFileName(analyzerFileName); - foreach (var cultureName in cultureNames) + var tcs = new TaskCompletionSource(); + var task = _mvidPathMap.GetOrAdd(mvid, tcs.Task); + if (object.ReferenceEquals(task, tcs.Task)) + { + // This thread won and we need to do the copy. + try + { + var shadowAnalyzerPath = copyAnalyzerContents(); + tcs.SetResult(shadowAnalyzerPath); + return shadowAnalyzerPath; + } + catch (Exception ex) + { + tcs.SetException(ex); + throw; + } + } + else { - var originalSatellitePath = Path.Combine(originalDirectory, cultureName, satelliteFileName); - var shadowSatellitePath = Path.Combine(shadowDirectory, cultureName, satelliteFileName); - copyFile(originalSatellitePath, shadowSatellitePath); + // This thread lost and we need to wait for the winner to finish the copy. + return task.Result; } - return shadowAnalyzerPath; + string copyAnalyzerContents() + { + var analyzerFileName = Path.GetFileName(originalAnalyzerPath); + var shadowDirectory = Path.Combine(_shadowCopyDirectoryAndMutex.Value.directory, mvid.ToString()); + var shadowAnalyzerPath = Path.Combine(shadowDirectory, analyzerFileName); + copyFile(originalAnalyzerPath, shadowAnalyzerPath); + + if (cultureNames.IsEmpty) + { + return shadowAnalyzerPath; + } + + var originalDirectory = Path.GetDirectoryName(originalAnalyzerPath)!; + var satelliteFileName = GetSatelliteFileName(analyzerFileName); + foreach (var cultureName in cultureNames) + { + var originalSatellitePath = Path.Combine(originalDirectory, cultureName, satelliteFileName); + var shadowSatellitePath = Path.Combine(shadowDirectory, cultureName, satelliteFileName); + copyFile(originalSatellitePath, shadowSatellitePath); + } + + return shadowAnalyzerPath; + } static void copyFile(string originalPath, string shadowCopyPath) { @@ -191,16 +223,6 @@ private static void ClearReadOnlyFlagOnFile(FileInfo fileInfo) } } - private string CreateUniqueDirectoryForAssembly() - { - int directoryId = Interlocked.Increment(ref _assemblyDirectoryId); - - string directory = Path.Combine(_shadowCopyDirectoryAndMutex.Value.directory, directoryId.ToString()); - - Directory.CreateDirectory(directory); - return directory; - } - private (string directory, Mutex mutex) CreateUniqueDirectoryForProcess() { string guid = Guid.NewGuid().ToString("N").ToLowerInvariant(); diff --git a/src/Compilers/Core/Portable/FileSystem/FileUtilities.cs b/src/Compilers/Core/Portable/FileSystem/FileUtilities.cs index be09e82e36c70..66cd96adace05 100644 --- a/src/Compilers/Core/Portable/FileSystem/FileUtilities.cs +++ b/src/Compilers/Core/Portable/FileSystem/FileUtilities.cs @@ -209,6 +209,11 @@ internal static class FileUtilities private static readonly char[] s_invalidPathChars = Path.GetInvalidPathChars(); + internal static string GetNormalizedPathOrOriginalPath(string path, string? basePath) + { + return NormalizeRelativePath(path, basePath, baseDirectory: null) ?? path; + } + internal static string? NormalizeRelativePath(string path, string? basePath, string? baseDirectory) { // Does this look like a URI at all or does it have any invalid path characters? If so, just use it as is. @@ -268,7 +273,10 @@ internal static string NormalizeDirectoryPath(string path) internal static string? TryNormalizeAbsolutePath(string path) { - Debug.Assert(PathUtilities.IsAbsolute(path)); + if (!PathUtilities.IsAbsolute(path)) + { + return null; + } try { diff --git a/src/Compilers/Core/Portable/InternalUtilities/ExperimentalAttribute.cs b/src/Compilers/Core/Portable/InternalUtilities/ExperimentalAttribute.cs new file mode 100644 index 0000000000000..723361aa24f85 --- /dev/null +++ b/src/Compilers/Core/Portable/InternalUtilities/ExperimentalAttribute.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// This was copied from https://github.com/dotnet/runtime/blob/815953a12c822847095a843d69c610a9f895ae3f/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/ExperimentalAttribute.cs +// and updated to have the scope of the attributes be internal. + +#if NET8_0_OR_GREATER + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API) +[assembly: TypeForwardedTo(typeof(ExperimentalAttribute))] +#pragma warning restore RS0016 // Add public types and members to the declared API + +#else + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Indicates that an API is experimental and it may change in the future. + /// + /// + /// This attribute allows call sites to be flagged with a diagnostic that indicates that an experimental + /// feature is used. Authors can use this attribute to ship preview features in their assemblies. + /// + [AttributeUsage(AttributeTargets.Assembly | + AttributeTargets.Module | + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Constructor | + AttributeTargets.Method | + AttributeTargets.Property | + AttributeTargets.Field | + AttributeTargets.Event | + AttributeTargets.Interface | + AttributeTargets.Delegate, Inherited = false)] + internal sealed class ExperimentalAttribute : Attribute + { + /// + /// Initializes a new instance of the class, specifying the ID that the compiler will use + /// when reporting a use of the API the attribute applies to. + /// + /// The ID that the compiler will use when reporting a use of the API the attribute applies to. + public ExperimentalAttribute(string diagnosticId) + { + DiagnosticId = diagnosticId; + } + + /// + /// Gets the ID that the compiler will use when reporting a use of the API the attribute applies to. + /// + /// The unique diagnostic ID. + /// + /// The diagnostic ID is shown in build output for warnings and errors. + /// This property represents the unique ID that can be used to suppress the warnings or errors, if needed. + /// + public string DiagnosticId { get; } + + /// + /// Gets or sets the URL for corresponding documentation. + /// The API accepts a format string instead of an actual URL, creating a generic URL that includes the diagnostic ID. + /// + /// The format string that represents a URL to corresponding documentation. + /// An example format string is https://contoso.com/obsoletion-warnings/{0}. + public string? UrlFormat { get; set; } + } +} + +#endif diff --git a/src/Compilers/Core/Portable/InternalUtilities/RoslynExperiments.cs b/src/Compilers/Core/Portable/InternalUtilities/RoslynExperiments.cs new file mode 100644 index 0000000000000..b6b4b37d4490d --- /dev/null +++ b/src/Compilers/Core/Portable/InternalUtilities/RoslynExperiments.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis; + +/// +/// Defines diagnostic info for Roslyn experimental APIs. +/// +internal static class RoslynExperiments +{ + internal const string NullableDisabledSemanticModel = "RSEXPERIMENTAL001"; + internal const string NullableDisabledSemanticModel_Url = "https://github.com/dotnet/roslyn/issues/70609"; +} diff --git a/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.cs b/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.cs index b5b3ae0dfaad2..43c5435d0d2d7 100644 --- a/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.cs +++ b/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.cs @@ -3946,7 +3946,8 @@ private void LinkThrowStatement(IOperation? exception) return FinishVisitingStatement(operation); } - private void HandleUsingOperationParts(IOperation resources, IOperation body, IMethodSymbol? disposeMethod, ImmutableArray disposeArguments, ImmutableArray locals, bool isAsynchronous) + private void HandleUsingOperationParts(IOperation resources, IOperation body, IMethodSymbol? disposeMethod, ImmutableArray disposeArguments, ImmutableArray locals, bool isAsynchronous, + Func? visitResource = null) { var usingRegion = new RegionBuilder(ControlFlowRegionKind.LocalLifetime, locals: locals); EnterRegion(usingRegion); @@ -3957,6 +3958,8 @@ private void HandleUsingOperationParts(IOperation resources, IOperation body, IM if (resources is IVariableDeclarationGroupOperation declarationGroup) { + Debug.Assert(visitResource is null); + var resourceQueue = ArrayBuilder<(IVariableDeclarationOperation, IVariableDeclaratorOperation)>.GetInstance(declarationGroup.Declarations.Length); foreach (IVariableDeclarationOperation declaration in declarationGroup.Declarations) @@ -3977,7 +3980,7 @@ private void HandleUsingOperationParts(IOperation resources, IOperation body, IM Debug.Assert(resources.Kind != OperationKind.VariableDeclarator); EvalStackFrame frame = PushStackFrame(); - IOperation resource = VisitRequired(resources); + IOperation resource = visitResource != null ? visitResource(resources) : VisitRequired(resources); if (shouldConvertToIDisposableBeforeTry(resource)) { @@ -4200,6 +4203,57 @@ private IOperation ConvertToIDisposable(IOperation operand, ITypeSymbol iDisposa { StartVisitingStatement(operation); + // `lock (l) { }` on value of type `System.Threading.Lock` is lowered to `using (l.EnterScope()) { }`. + if (operation.LockedValue.Type?.IsWellKnownTypeLock() == true) + { + if (operation.LockedValue.Type.TryFindLockTypeInfo() is { } lockTypeInfo) + { + HandleUsingOperationParts( + resources: operation.LockedValue, + body: operation.Body, + disposeMethod: lockTypeInfo.ScopeDisposeMethod, + disposeArguments: ImmutableArray.Empty, + locals: ImmutableArray.Empty, + isAsynchronous: false, + visitResource: (resource) => + { + var lockObject = VisitRequired(resource); + + return new InvocationOperation( + targetMethod: lockTypeInfo.EnterScopeMethod, + constrainedToType: null, + instance: lockObject, + isVirtual: lockTypeInfo.EnterScopeMethod.IsVirtual || + lockTypeInfo.EnterScopeMethod.IsAbstract || + lockTypeInfo.EnterScopeMethod.IsOverride, + arguments: ImmutableArray.Empty, + semanticModel: null, + syntax: lockObject.Syntax, + type: lockTypeInfo.EnterScopeMethod.ReturnType, + isImplicit: true); + }); + + return FinishVisitingStatement(operation); + } + else + { + IOperation? underlying = Visit(operation.LockedValue); + + if (underlying is not null) + { + AddStatement(new ExpressionStatementOperation( + MakeInvalidOperation(type: null, underlying), + semanticModel: null, + operation.Syntax, + IsImplicit(operation))); + } + + VisitStatement(operation.Body); + + return FinishVisitingStatement(operation); + } + } + ITypeSymbol objectType = _compilation.GetSpecialType(SpecialType.System_Object); // If Monitor.Enter(object, ref bool) is available: diff --git a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt index 373c14b5b527a..9eae0fc50c55f 100644 --- a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt @@ -46,3 +46,9 @@ virtual Microsoft.CodeAnalysis.SyntaxContextReceiverCreator.Invoke() -> Microsof virtual Microsoft.CodeAnalysis.SyntaxReceiverCreator.Invoke() -> Microsoft.CodeAnalysis.ISyntaxReceiver! static Microsoft.CodeAnalysis.DocumentationCommentId.CreateDeclarationId(Microsoft.CodeAnalysis.ISymbol! symbol) -> string? *REMOVED*static Microsoft.CodeAnalysis.DocumentationCommentId.CreateDeclarationId(Microsoft.CodeAnalysis.ISymbol! symbol) -> string! +[RSEXPERIMENTAL001]Microsoft.CodeAnalysis.SemanticModelOptions +[RSEXPERIMENTAL001]Microsoft.CodeAnalysis.SemanticModelOptions.None = 0 -> Microsoft.CodeAnalysis.SemanticModelOptions +[RSEXPERIMENTAL001]Microsoft.CodeAnalysis.SemanticModelOptions.IgnoreAccessibility = 1 -> Microsoft.CodeAnalysis.SemanticModelOptions +[RSEXPERIMENTAL001]Microsoft.CodeAnalysis.SemanticModelOptions.DisableNullableAnalysis = 2 -> Microsoft.CodeAnalysis.SemanticModelOptions +[RSEXPERIMENTAL001]Microsoft.CodeAnalysis.Compilation.GetSemanticModel(Microsoft.CodeAnalysis.SyntaxTree! syntaxTree, Microsoft.CodeAnalysis.SemanticModelOptions options) -> Microsoft.CodeAnalysis.SemanticModel! +abstract Microsoft.CodeAnalysis.SemanticModel.NullableAnalysisIsDisabled.get -> bool diff --git a/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs index 843119b613b40..70b57c9ff6533 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs @@ -39,7 +39,7 @@ internal GeneratorDriver(GeneratorDriverState state) internal GeneratorDriver(ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider optionsProvider, ImmutableArray additionalTexts, GeneratorDriverOptions driverOptions) { var incrementalGenerators = GetIncrementalGenerators(generators, SourceExtension); - _state = new GeneratorDriverState(parseOptions, optionsProvider, generators, incrementalGenerators, additionalTexts, ImmutableArray.Create(new GeneratorState[generators.Length]), DriverStateTable.Empty, SyntaxStore.Empty, driverOptions.DisabledOutputs, runtime: TimeSpan.Zero, driverOptions.TrackIncrementalGeneratorSteps, parseOptionsChanged: true); + _state = new GeneratorDriverState(parseOptions, optionsProvider, generators, incrementalGenerators, additionalTexts, ImmutableArray.Create(new GeneratorState[generators.Length]), DriverStateTable.Empty, SyntaxStore.Empty, driverOptions, runtime: TimeSpan.Zero, parseOptionsChanged: true); } public GeneratorDriver RunGenerators(Compilation compilation, CancellationToken cancellationToken = default) @@ -341,8 +341,7 @@ private IncrementalExecutionContext UpdateOutputs(ImmutableArray ParseAdditionalSources(ISourceGenerator generator, ImmutableArray generatedSources, CancellationToken cancellationToken) { var trees = ArrayBuilder.GetInstance(generatedSources.Length); - var type = generator.GetGeneratorType(); - var prefix = GetFilePathPrefixForGenerator(generator); + var prefix = GetFilePathPrefixForGenerator(this._state.BaseDirectory, generator); foreach (var source in generatedSources) { var tree = ParseGeneratedSourceText(source, Path.Combine(prefix, source.HintName), cancellationToken); @@ -410,10 +409,10 @@ private static ImmutableArray FilterDiagnostics(Compilation compilat return filteredDiagnostics.ToImmutableAndFree(); } - internal static string GetFilePathPrefixForGenerator(ISourceGenerator generator) + internal static string GetFilePathPrefixForGenerator(string? baseDirectory, ISourceGenerator generator) { var type = generator.GetGeneratorType(); - return Path.Combine(type.Assembly.GetName().Name ?? string.Empty, type.FullName!); + return Path.Combine(baseDirectory ?? "", type.Assembly.GetName().Name ?? string.Empty, type.FullName!); } private static ImmutableArray GetIncrementalGenerators(ImmutableArray generators, string sourceExtension) diff --git a/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriverOptions.cs b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriverOptions.cs index 8d15f1804627c..b7f013cab5905 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriverOptions.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriverOptions.cs @@ -17,6 +17,8 @@ public readonly struct GeneratorDriverOptions public readonly bool TrackIncrementalGeneratorSteps; + internal string? BaseDirectory { get; init; } + public GeneratorDriverOptions(IncrementalGeneratorOutputKind disabledOutputs) : this(disabledOutputs, false) { diff --git a/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriverState.cs b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriverState.cs index 3cc44d2edaf5c..1a16777ae0d31 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriverState.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriverState.cs @@ -19,9 +19,8 @@ internal GeneratorDriverState(ParseOptions parseOptions, ImmutableArray generatorStates, DriverStateTable stateTable, SyntaxStore syntaxStore, - IncrementalGeneratorOutputKind disabledOutputs, + GeneratorDriverOptions driverOptions, TimeSpan runtime, - bool trackIncrementalGeneratorSteps, bool parseOptionsChanged) { Generators = sourceGenerators; @@ -32,9 +31,10 @@ internal GeneratorDriverState(ParseOptions parseOptions, OptionsProvider = optionsProvider; StateTable = stateTable; SyntaxStore = syntaxStore; - DisabledOutputs = disabledOutputs; + _driverOptions = driverOptions; + DisabledOutputs = driverOptions.DisabledOutputs; + TrackIncrementalSteps = driverOptions.TrackIncrementalGeneratorSteps; RunTime = runtime; - TrackIncrementalSteps = trackIncrementalGeneratorSteps; ParseOptionsChanged = parseOptionsChanged; Debug.Assert(Generators.Length == GeneratorStates.Length); Debug.Assert(IncrementalGenerators.Length == GeneratorStates.Length); @@ -77,6 +77,11 @@ internal GeneratorDriverState(ParseOptions parseOptions, /// internal readonly AnalyzerConfigOptionsProvider OptionsProvider; + /// + /// The base directory for the of generated files. + /// + internal string? BaseDirectory => _driverOptions.BaseDirectory; + /// /// ParseOptions to use when parsing generator provided source. /// @@ -86,13 +91,17 @@ internal GeneratorDriverState(ParseOptions parseOptions, internal readonly SyntaxStore SyntaxStore; + private readonly GeneratorDriverOptions _driverOptions; + /// /// A bit field containing the output kinds that should not be produced by this generator driver. /// + // https://github.com/dotnet/roslyn/issues/72129: Change from field to property once issue is addressed internal readonly IncrementalGeneratorOutputKind DisabledOutputs; internal readonly TimeSpan RunTime; + // https://github.com/dotnet/roslyn/issues/72129: Change from field to property once issue is addressed internal readonly bool TrackIncrementalSteps; /// @@ -109,7 +118,6 @@ internal GeneratorDriverState With( SyntaxStore? syntaxStore = null, ParseOptions? parseOptions = null, AnalyzerConfigOptionsProvider? optionsProvider = null, - IncrementalGeneratorOutputKind? disabledOutputs = null, TimeSpan? runTime = null, bool? parseOptionsChanged = null) { @@ -122,9 +130,8 @@ internal GeneratorDriverState With( generatorStates ?? this.GeneratorStates, stateTable ?? this.StateTable, syntaxStore ?? this.SyntaxStore, - disabledOutputs ?? this.DisabledOutputs, + this._driverOptions, runTime ?? this.RunTime, - this.TrackIncrementalSteps, parseOptionsChanged ?? this.ParseOptionsChanged ); } diff --git a/src/Compilers/Core/Portable/SourceGeneration/ISyntaxHelper.cs b/src/Compilers/Core/Portable/SourceGeneration/ISyntaxHelper.cs index 52906bd178683..9e9a46124b152 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/ISyntaxHelper.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/ISyntaxHelper.cs @@ -32,14 +32,12 @@ internal interface ISyntaxHelper void AddAliases(GreenNode node, ArrayBuilder<(string aliasName, string symbolName)> aliases, bool global); void AddAliases(CompilationOptions options, ArrayBuilder<(string aliasName, string symbolName)> aliases); - bool ContainsAttributeList(SyntaxNode root); bool ContainsGlobalAliases(SyntaxNode root); } internal abstract class AbstractSyntaxHelper : ISyntaxHelper { public abstract bool IsCaseSensitive { get; } - protected abstract int AttributeListKind { get; } public abstract bool IsValidIdentifier(string name); @@ -60,18 +58,5 @@ internal abstract class AbstractSyntaxHelper : ISyntaxHelper public abstract void AddAliases(CompilationOptions options, ArrayBuilder<(string aliasName, string symbolName)> aliases); public abstract bool ContainsGlobalAliases(SyntaxNode root); - - public bool ContainsAttributeList(SyntaxNode root) - { - var attributeListKind = this.AttributeListKind; - - foreach (var node in root.Green.EnumerateNodes()) - { - if (node.RawKind == attributeListKind) - return true; - } - - return false; - } } } diff --git a/src/Compilers/Core/Portable/SourceGeneration/Nodes/NodeStateTable.cs b/src/Compilers/Core/Portable/SourceGeneration/Nodes/NodeStateTable.cs index 2968c5166e36e..8758c9302a75a 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/Nodes/NodeStateTable.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/Nodes/NodeStateTable.cs @@ -204,7 +204,16 @@ public string GetPackedStates() { for (int i = 0; i < state.Count; i++) { - pooled.Builder.Append(state.GetState(i).ToString()[0]); + var packedChar = state.GetState(i) switch + { + EntryState.Added => 'A', + EntryState.Removed => 'R', + EntryState.Modified => 'M', + EntryState.Cached => 'C', + _ => throw ExceptionUtilities.Unreachable(), + }; + + pooled.Builder.Append(packedChar); } pooled.Builder.Append(','); } diff --git a/src/Compilers/Core/Portable/SourceGeneration/Nodes/SyntaxValueProvider_ForAttributeWithSimpleName.cs b/src/Compilers/Core/Portable/SourceGeneration/Nodes/SyntaxValueProvider_ForAttributeWithSimpleName.cs index c3b7b48cba0c1..f2fa22112ded2 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/Nodes/SyntaxValueProvider_ForAttributeWithSimpleName.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/Nodes/SyntaxValueProvider_ForAttributeWithSimpleName.cs @@ -234,6 +234,10 @@ void processMember(SyntaxNode member) { cancellationToken.ThrowIfCancellationRequested(); + // Don't bother descending into nodes that don't contain attributes. + if (!member.ContainsAttributes) + return; + // nodes can be arbitrarily deep. Use an explicit stack over recursion to prevent a stack-overflow. var nodeStack = s_nodeStackPool.Allocate(); nodeStack.Push(member); @@ -244,6 +248,10 @@ void processMember(SyntaxNode member) { var node = nodeStack.Pop(); + // Don't bother descending into nodes that don't contain attributes. + if (!node.ContainsAttributes) + continue; + if (syntaxHelper.IsAttributeList(node)) { foreach (var attribute in syntaxHelper.GetAttributesOfAttributeList(node)) diff --git a/src/Compilers/Core/Portable/Symbols/ISymbolExtensions.cs b/src/Compilers/Core/Portable/Symbols/ISymbolExtensions.cs index d1861841feac0..26af026a8e05e 100644 --- a/src/Compilers/Core/Portable/Symbols/ISymbolExtensions.cs +++ b/src/Compilers/Core/Portable/Symbols/ISymbolExtensions.cs @@ -6,6 +6,8 @@ namespace Microsoft.CodeAnalysis { + using LockTypeInfo = (IMethodSymbol EnterScopeMethod, IMethodSymbol ScopeDisposeMethod); + public static partial class ISymbolExtensions { /// @@ -116,5 +118,83 @@ internal static bool IsInSource(this ISymbol symbol) return false; } + + // Keep consistent with TypeSymbolExtensions.IsWellKnownTypeLock. + internal static bool IsWellKnownTypeLock(this ITypeSymbol type) + { + return type is INamedTypeSymbol + { + Name: WellKnownMemberNames.LockTypeName, + Arity: 0, + ContainingType: null, + ContainingNamespace: + { + Name: nameof(System.Threading), + ContainingNamespace: + { + Name: nameof(System), + ContainingNamespace.IsGlobalNamespace: true, + } + } + }; + } + + // Keep consistent with LockBinder.TryFindLockTypeInfo. + internal static LockTypeInfo? TryFindLockTypeInfo(this ITypeSymbol lockType) + { + IMethodSymbol? enterScopeMethod = TryFindPublicVoidParameterlessMethod(lockType, WellKnownMemberNames.EnterScopeMethodName); + if (enterScopeMethod is not { ReturnsVoid: false, RefKind: RefKind.None }) + { + return null; + } + + ITypeSymbol? scopeType = enterScopeMethod.ReturnType; + if (scopeType is not INamedTypeSymbol { Name: WellKnownMemberNames.LockScopeTypeName, Arity: 0, IsValueType: true, IsRefLikeType: true, DeclaredAccessibility: Accessibility.Public } || + !lockType.Equals(scopeType.ContainingType, SymbolEqualityComparer.ConsiderEverything)) + { + return null; + } + + IMethodSymbol? disposeMethod = TryFindPublicVoidParameterlessMethod(scopeType, WellKnownMemberNames.DisposeMethodName); + if (disposeMethod is not { ReturnsVoid: true }) + { + return null; + } + + return new LockTypeInfo + { + EnterScopeMethod = enterScopeMethod, + ScopeDisposeMethod = disposeMethod, + }; + } + + // Keep consistent with LockBinder.TryFindPublicVoidParameterlessMethod. + private static IMethodSymbol? TryFindPublicVoidParameterlessMethod(ITypeSymbol type, string name) + { + var members = type.GetMembers(name); + IMethodSymbol? result = null; + foreach (var member in members) + { + if (member is IMethodSymbol + { + Parameters: [], + Arity: 0, + IsStatic: false, + DeclaredAccessibility: Accessibility.Public, + MethodKind: MethodKind.Ordinary, + } method) + { + if (result is not null) + { + // Ambiguous method found. + return null; + } + + result = method; + } + } + + return result; + } } } diff --git a/src/Compilers/Core/Portable/Symbols/WellKnownMemberNames.cs b/src/Compilers/Core/Portable/Symbols/WellKnownMemberNames.cs index 090f5abb1fc50..f22c1902567b0 100644 --- a/src/Compilers/Core/Portable/Symbols/WellKnownMemberNames.cs +++ b/src/Compilers/Core/Portable/Symbols/WellKnownMemberNames.cs @@ -398,5 +398,9 @@ public static class WellKnownMemberNames /// The name of a type synthesized for a top-level statements entry point method. /// public const string TopLevelStatementsEntryPointTypeName = "Program"; + + internal const string LockTypeName = "Lock"; + internal const string EnterScopeMethodName = "EnterScope"; + internal const string LockScopeTypeName = "Scope"; } } diff --git a/src/Compilers/Core/Portable/Syntax/GreenNode.NodeFlagsAndSlotCount.cs b/src/Compilers/Core/Portable/Syntax/GreenNode.NodeFlagsAndSlotCount.cs new file mode 100644 index 0000000000000..f3249a121238c --- /dev/null +++ b/src/Compilers/Core/Portable/Syntax/GreenNode.NodeFlagsAndSlotCount.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace Microsoft.CodeAnalysis +{ + internal abstract partial class GreenNode + { + /// + /// Combination of and stored in a single 16bit value. + /// + private struct NodeFlagsAndSlotCount + { + /// + /// 4 bits for the SlotCount. This allows slot counts of 0-14 to be stored as a direct byte. All 1s + /// indicates that the slot count must be computed. + /// + private const ushort SlotCountMask = 0b1111000000000000; + private const ushort NodeFlagsMask = 0b0000111111111111; + + private const int SlotCountShift = 12; + + /// + /// 12 bits for the NodeFlags. This allows for up to 12 distinct bits to be stored to designate interesting + /// aspects of a node. + /// + + /// + /// CCCCFFFFFFFFFFFF for Count bits then Flag bits. + /// + private ushort _data; + + /// + /// Returns the slot count if it was small enough to be stored directly in this object. Otherwise, returns + /// to indicate it could not be directly stored. + /// + public byte SmallSlotCount + { + readonly get + { + var shifted = _data >> SlotCountShift; + Debug.Assert(shifted <= SlotCountTooLarge); + return (byte)shifted; + } + + set + { + if (value > SlotCountTooLarge) + value = SlotCountTooLarge; + + // Clear out everything but the node-flags, and then assign into the slot-count segment. + _data = (ushort)((_data & NodeFlagsMask) | (value << SlotCountShift)); + } + } + + public NodeFlags NodeFlags + { + readonly get + { + return (NodeFlags)(_data & NodeFlagsMask); + } + + set + { + Debug.Assert((ushort)value <= NodeFlagsMask); + + // Clear out everything but the slot-count, and then assign into the node-flags segment. + _data = (ushort)((_data & SlotCountMask) | (ushort)value); + } + } + } + } +} diff --git a/src/Compilers/Core/Portable/Syntax/GreenNode.cs b/src/Compilers/Core/Portable/Syntax/GreenNode.cs index ba2be0cb0b454..9ef6a04ef4c59 100644 --- a/src/Compilers/Core/Portable/Syntax/GreenNode.cs +++ b/src/Compilers/Core/Portable/Syntax/GreenNode.cs @@ -25,9 +25,18 @@ private string GetDebuggerDisplay() internal const int ListKind = 1; + // Pack the kind, node-flags, slot-count, and full-width into 64bits. Note: if we need more bits in the future + // (say for additional node-flags), we can always directly use a packed int64 here, and manage where all these + // bits go manually. + + /// + /// Value used to indicate the slot count was too large to be encoded directly in our + /// value. Callers will have to store the value elsewhere and retrieve the full value themselves. + /// + protected const int SlotCountTooLarge = 0b0000000000001111; + private readonly ushort _kind; - protected NodeFlags flags; - private byte _slotCount; + private NodeFlagsAndSlotCount _nodeFlagsAndSlotCount; private int _fullWidth; private static readonly ConditionalWeakTable s_diagnosticsTable = @@ -57,7 +66,7 @@ protected GreenNode(ushort kind, DiagnosticInfo[]? diagnostics, int fullWidth) _fullWidth = fullWidth; if (diagnostics?.Length > 0) { - this.flags |= NodeFlags.ContainsDiagnostics; + SetFlags(NodeFlags.ContainsDiagnostics); s_diagnosticsTable.Add(this, diagnostics); } } @@ -67,7 +76,7 @@ protected GreenNode(ushort kind, DiagnosticInfo[]? diagnostics) _kind = kind; if (diagnostics?.Length > 0) { - this.flags |= NodeFlags.ContainsDiagnostics; + SetFlags(NodeFlags.ContainsDiagnostics); s_diagnosticsTable.Add(this, diagnostics); } } @@ -82,7 +91,7 @@ protected GreenNode(ushort kind, DiagnosticInfo[]? diagnostics, SyntaxAnnotation if (annotation == null) throw new ArgumentException(paramName: nameof(annotations), message: "" /*CSharpResources.ElementsCannotBeNull*/); } - this.flags |= NodeFlags.ContainsAnnotations; + SetFlags(NodeFlags.HasAnnotationsDirectly | NodeFlags.ContainsAnnotations); s_annotationsTable.Add(this, annotations); } } @@ -97,7 +106,7 @@ protected GreenNode(ushort kind, DiagnosticInfo[]? diagnostics, SyntaxAnnotation if (annotation == null) throw new ArgumentException(paramName: nameof(annotations), message: "" /*CSharpResources.ElementsCannotBeNull*/); } - this.flags |= NodeFlags.ContainsAnnotations; + SetFlags(NodeFlags.HasAnnotationsDirectly | NodeFlags.ContainsAnnotations); s_annotationsTable.Add(this, annotations); } } @@ -105,7 +114,7 @@ protected GreenNode(ushort kind, DiagnosticInfo[]? diagnostics, SyntaxAnnotation protected void AdjustFlagsAndWidth(GreenNode node) { RoslynDebug.Assert(node != null, "PERF: caller must ensure that node!=null, we do not want to re-check that here."); - this.flags |= (node.flags & NodeFlags.InheritMask); + SetFlags(node.Flags & NodeFlags.InheritMask); _fullWidth += node._fullWidth; } @@ -141,18 +150,14 @@ public int SlotCount { get { - int count = _slotCount; - if (count == byte.MaxValue) - { - count = GetSlotCount(); - } - - return count; + var count = _nodeFlagsAndSlotCount.SmallSlotCount; + return count == SlotCountTooLarge ? GetSlotCount() : count; } protected set { - _slotCount = (byte)value; + Debug.Assert(value <= byte.MaxValue); + _nodeFlagsAndSlotCount.SmallSlotCount = (byte)value; } } @@ -165,10 +170,15 @@ internal GreenNode GetRequiredSlot(int index) return node; } - // for slot counts >= byte.MaxValue + /// + /// Called when returns a value of . + /// protected virtual int GetSlotCount() { - return _slotCount; + // This should only be called for nodes that couldn't store their slot count effectively in our + // _nodeFlagsAndSlotCount field. The only nodes that cannot do that are the `WithManyChildren` list types. + // All of which should be subclassing this method. + throw ExceptionUtilities.Unreachable(); } public virtual int GetSlotOffset(int index) @@ -234,37 +244,63 @@ public virtual int FindSlotIndexContainingOffset(int offset) #endregion #region Flags + + /// + /// Special flags a node can have. Note: while this is typed as being `ushort`, we can only practically use 12 + /// of those 16 bits as we use the remaining 4 bits to store the slot count of a node. + /// [Flags] - internal enum NodeFlags : byte + internal enum NodeFlags : ushort { None = 0, - ContainsDiagnostics = 1 << 0, - ContainsStructuredTrivia = 1 << 1, - ContainsDirectives = 1 << 2, - ContainsSkippedText = 1 << 3, - ContainsAnnotations = 1 << 4, - IsNotMissing = 1 << 5, - - FactoryContextIsInAsync = 1 << 6, - FactoryContextIsInQuery = 1 << 7, + /// + /// If this node is missing or not. We use a non-zero value for the not-missing case so that this value + /// automatically merges upwards when building parent nodes. In other words, once we have one node that is + /// not-missing, all nodes above it are definitely not-missing as well. + /// + IsNotMissing = 1 << 0, + /// + /// If this node directly has annotations (not its descendants). can be + /// used to determine if a node or any of its descendants has annotations. + /// + HasAnnotationsDirectly = 1 << 1, + + FactoryContextIsInAsync = 1 << 2, + FactoryContextIsInQuery = 1 << 3, FactoryContextIsInIterator = FactoryContextIsInQuery, // VB does not use "InQuery", but uses "InIterator" instead - InheritMask = ContainsDiagnostics | ContainsStructuredTrivia | ContainsDirectives | ContainsSkippedText | ContainsAnnotations | IsNotMissing, + // Flags that are inherited upwards when building parent nodes. They should all start with "Contains" to + // indicate that the information could be found on it or anywhere in its children. + + /// + /// If this node, or any of its descendants has annotations attached to them. + /// + ContainsAnnotations = 1 << 4, + /// + /// If this node, or any of its descendants has attributes attached to it. + /// + ContainsAttributes = 1 << 5, + ContainsDiagnostics = 1 << 6, + ContainsDirectives = 1 << 7, + ContainsSkippedText = 1 << 8, + ContainsStructuredTrivia = 1 << 9, + + InheritMask = IsNotMissing | ContainsAnnotations | ContainsAttributes | ContainsDiagnostics | ContainsDirectives | ContainsSkippedText | ContainsStructuredTrivia, } internal NodeFlags Flags { - get { return this.flags; } + get { return this._nodeFlagsAndSlotCount.NodeFlags; } } internal void SetFlags(NodeFlags flags) { - this.flags |= flags; + _nodeFlagsAndSlotCount.NodeFlags |= flags; } internal void ClearFlags(NodeFlags flags) { - this.flags &= ~flags; + _nodeFlagsAndSlotCount.NodeFlags &= ~flags; } internal bool IsMissing @@ -272,7 +308,7 @@ internal bool IsMissing get { // flag has reversed meaning hence "==" - return (this.flags & NodeFlags.IsNotMissing) == 0; + return (this.Flags & NodeFlags.IsNotMissing) == 0; } } @@ -280,7 +316,7 @@ internal bool ParsedInAsync { get { - return (this.flags & NodeFlags.FactoryContextIsInAsync) != 0; + return (this.Flags & NodeFlags.FactoryContextIsInAsync) != 0; } } @@ -288,7 +324,7 @@ internal bool ParsedInQuery { get { - return (this.flags & NodeFlags.FactoryContextIsInQuery) != 0; + return (this.Flags & NodeFlags.FactoryContextIsInQuery) != 0; } } @@ -296,7 +332,7 @@ internal bool ParsedInIterator { get { - return (this.flags & NodeFlags.FactoryContextIsInIterator) != 0; + return (this.Flags & NodeFlags.FactoryContextIsInIterator) != 0; } } @@ -304,7 +340,7 @@ public bool ContainsSkippedText { get { - return (this.flags & NodeFlags.ContainsSkippedText) != 0; + return (this.Flags & NodeFlags.ContainsSkippedText) != 0; } } @@ -312,7 +348,7 @@ public bool ContainsStructuredTrivia { get { - return (this.flags & NodeFlags.ContainsStructuredTrivia) != 0; + return (this.Flags & NodeFlags.ContainsStructuredTrivia) != 0; } } @@ -320,7 +356,15 @@ public bool ContainsDirectives { get { - return (this.flags & NodeFlags.ContainsDirectives) != 0; + return (this.Flags & NodeFlags.ContainsDirectives) != 0; + } + } + + public bool ContainsAttributes + { + get + { + return (this.Flags & NodeFlags.ContainsAttributes) != 0; } } @@ -328,7 +372,7 @@ public bool ContainsDiagnostics { get { - return (this.flags & NodeFlags.ContainsDiagnostics) != 0; + return (this.Flags & NodeFlags.ContainsDiagnostics) != 0; } } @@ -336,9 +380,18 @@ public bool ContainsAnnotations { get { - return (this.flags & NodeFlags.ContainsAnnotations) != 0; + return (this.Flags & NodeFlags.ContainsAnnotations) != 0; + } + } + + public bool HasAnnotationsDirectly + { + get + { + return (this.Flags & NodeFlags.HasAnnotationsDirectly) != 0; } } + #endregion #region Spans @@ -510,17 +563,15 @@ private static IEnumerable GetAnnotationsSlow(SyntaxAnnotation public SyntaxAnnotation[] GetAnnotations() { - if (this.ContainsAnnotations) - { - SyntaxAnnotation[]? annotations; - if (s_annotationsTable.TryGetValue(this, out annotations)) - { - System.Diagnostics.Debug.Assert(annotations.Length != 0, "we should return nonempty annotations or NoAnnotations"); - return annotations; - } - } + if (!this.HasAnnotationsDirectly) + return s_noAnnotations; - return s_noAnnotations; + var found = s_annotationsTable.TryGetValue(this, out var annotations); + Debug.Assert(found, "We must be able to find annotations since we had the bit set on ourselves"); + Debug.Assert(annotations != null, "annotations should not be null"); + Debug.Assert(annotations != s_noAnnotations, "annotations should not be s_noAnnotations"); + Debug.Assert(annotations.Length != 0, "annotations should be non-empty"); + return annotations; } internal abstract GreenNode SetAnnotations(SyntaxAnnotation[]? annotations); @@ -699,7 +750,11 @@ public virtual GreenNode WithTrailingTrivia(GreenNode? trivia) } } node = firstChild; - } while (node?._slotCount > 0); + } + // Note: it's ok to examine SmallSlotCount here. All we're trying to do is make sure we have at least one + // child. And SmallSlotCount works both for small counts and large counts. This avoids an unnecessary + // virtual call for large list nodes. + while (node?._nodeFlagsAndSlotCount.SmallSlotCount > 0); return node; } @@ -721,7 +776,11 @@ public virtual GreenNode WithTrailingTrivia(GreenNode? trivia) } } node = lastChild; - } while (node?._slotCount > 0); + } + // Note: it's ok to examine SmallSlotCount here. All we're trying to do is make sure we have at least one + // child. And SmallSlotCount works both for small counts and large counts. This avoids an unnecessary + // virtual call for large list nodes. + while (node?._nodeFlagsAndSlotCount.SmallSlotCount > 0); return node; } @@ -744,7 +803,10 @@ public virtual GreenNode WithTrailingTrivia(GreenNode? trivia) } node = nonmissingChild; } - while (node?._slotCount > 0); + // Note: it's ok to examine SmallSlotCount here. All we're trying to do is make sure we have at least one + // child. And SmallSlotCount works both for small counts and large counts. This avoids an unnecessary + // virtual call for large list nodes. + while (node?._nodeFlagsAndSlotCount.SmallSlotCount > 0); return node; } @@ -898,7 +960,7 @@ internal bool IsCacheable { get { - return ((this.flags & NodeFlags.InheritMask) == NodeFlags.IsNotMissing) && + return ((this.Flags & NodeFlags.InheritMask) == NodeFlags.IsNotMissing) && this.SlotCount <= GreenNode.MaxCachedChildNum; } } @@ -907,7 +969,7 @@ internal int GetCacheHash() { Debug.Assert(this.IsCacheable); - int code = (int)(this.flags) ^ this.RawKind; + int code = (int)(this.Flags) ^ this.RawKind; int cnt = this.SlotCount; for (int i = 0; i < cnt; i++) { @@ -926,7 +988,7 @@ internal bool IsCacheEquivalent(int kind, NodeFlags flags, GreenNode? child1) Debug.Assert(this.IsCacheable); return this.RawKind == kind && - this.flags == flags && + this.Flags == flags && this.SlotCount == 1 && this.GetSlot(0) == child1; } @@ -936,7 +998,7 @@ internal bool IsCacheEquivalent(int kind, NodeFlags flags, GreenNode? child1, Gr Debug.Assert(this.IsCacheable); return this.RawKind == kind && - this.flags == flags && + this.Flags == flags && this.SlotCount == 2 && this.GetSlot(0) == child1 && this.GetSlot(1) == child2; @@ -947,7 +1009,7 @@ internal bool IsCacheEquivalent(int kind, NodeFlags flags, GreenNode? child1, Gr Debug.Assert(this.IsCacheable); return this.RawKind == kind && - this.flags == flags && + this.Flags == flags && this.SlotCount == 3 && this.GetSlot(0) == child1 && this.GetSlot(1) == child2 && diff --git a/src/Compilers/Core/Portable/Syntax/InternalSyntax/SyntaxList.WithManyChildren.cs b/src/Compilers/Core/Portable/Syntax/InternalSyntax/SyntaxList.WithManyChildren.cs index 503d907700cec..842d3e8f3a727 100644 --- a/src/Compilers/Core/Portable/Syntax/InternalSyntax/SyntaxList.WithManyChildren.cs +++ b/src/Compilers/Core/Portable/Syntax/InternalSyntax/SyntaxList.WithManyChildren.cs @@ -29,14 +29,13 @@ internal WithManyChildrenBase(DiagnosticInfo[]? diagnostics, SyntaxAnnotation[]? private void InitializeChildren() { int n = children.Length; - if (n < byte.MaxValue) - { - this.SlotCount = (byte)n; - } - else - { - this.SlotCount = byte.MaxValue; - } + + // Attempt to store small lengths directly into the storage provided within GreenNode. If, however, the + // length is too long, we will store a special value in the space, which will `SlotCount` to call back + // into `GetSlotCount` to retrieve the true length. + this.SlotCount = n < SlotCountTooLarge + ? (byte)n + : SlotCountTooLarge; for (int i = 0; i < children.Length; i++) { diff --git a/src/Compilers/Core/Portable/Syntax/SyntaxNode.cs b/src/Compilers/Core/Portable/Syntax/SyntaxNode.cs index 9f77eb52eeb95..ba5ab7ed1b3dc 100644 --- a/src/Compilers/Core/Portable/Syntax/SyntaxNode.cs +++ b/src/Compilers/Core/Portable/Syntax/SyntaxNode.cs @@ -442,6 +442,8 @@ public bool ContainsDiagnostics /// public bool ContainsDirectives => this.Green.ContainsDirectives; + internal bool ContainsAttributes => this.Green.ContainsAttributes; + /// /// Returns true if this node contains any directives (e.g. #if, #nullable, etc.) within it with a matching kind. /// diff --git a/src/Compilers/Core/Portable/Syntax/SyntaxTree.cs b/src/Compilers/Core/Portable/Syntax/SyntaxTree.cs index f4e25ff87614f..6e9655d6fcf14 100644 --- a/src/Compilers/Core/Portable/Syntax/SyntaxTree.cs +++ b/src/Compilers/Core/Portable/Syntax/SyntaxTree.cs @@ -433,7 +433,7 @@ internal SourceGeneratorSyntaxTreeInfo GetSourceGeneratorInfo( if (syntaxHelper.ContainsGlobalAliases(root)) result |= SourceGeneratorSyntaxTreeInfo.ContainsGlobalAliases; - if (syntaxHelper.ContainsAttributeList(root)) + if (root.ContainsAttributes) result |= SourceGeneratorSyntaxTreeInfo.ContainsAttributeList; _sourceGeneratorInfo = result; diff --git a/src/Compilers/Server/VBCSCompiler/ClientConnectionHandler.cs b/src/Compilers/Server/VBCSCompiler/ClientConnectionHandler.cs index e3102302f1a73..1a4933a01eba4 100644 --- a/src/Compilers/Server/VBCSCompiler/ClientConnectionHandler.cs +++ b/src/Compilers/Server/VBCSCompiler/ClientConnectionHandler.cs @@ -96,7 +96,7 @@ async Task ProcessCoreAsync() } } - private async Task WriteBuildResponseAsync(IClientConnection clientConnection, Guid requestId, BuildResponse response, CompletionData completionData, CancellationToken cancellationToken) + private async Task WriteBuildResponseAsync(IClientConnection clientConnection, string requestId, BuildResponse response, CompletionData completionData, CancellationToken cancellationToken) { var message = response switch { diff --git a/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs b/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs index e706894495c7f..3b326885d17b9 100644 --- a/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs +++ b/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs @@ -21,14 +21,14 @@ namespace Microsoft.CodeAnalysis.CompilerServer { internal readonly struct RunRequest { - public Guid RequestId { get; } + public string RequestId { get; } public string Language { get; } public string? WorkingDirectory { get; } public string? TempDirectory { get; } public string? LibDirectory { get; } public string[] Arguments { get; } - public RunRequest(Guid requestId, string language, string? workingDirectory, string? tempDirectory, string? libDirectory, string[] arguments) + public RunRequest(string requestId, string language, string? workingDirectory, string? tempDirectory, string? libDirectory, string[] arguments) { RequestId = requestId; Language = language; @@ -146,6 +146,7 @@ public BuildResponse RunCompilation(in RunRequest request, CancellationToken can Logger.Log($"Begin {request.RequestId} {request.Language} compiler run"); try { + CodeAnalysisEventSource.Log.StartServerCompilation(request.RequestId); bool utf8output = compiler.Arguments.Utf8Output; TextWriter output = new StringWriter(CultureInfo.InvariantCulture); int returnCode = compiler.Run(output, cancellationToken); @@ -161,6 +162,10 @@ public BuildResponse RunCompilation(in RunRequest request, CancellationToken can Logger.LogException(ex, $"Running compilation for {request.RequestId}"); throw; } + finally + { + CodeAnalysisEventSource.Log.StopServerCompilation(request.RequestId); + } } } } diff --git a/src/Compilers/Shared/BuildClient.cs b/src/Compilers/Shared/BuildClient.cs index 7b2ed9a3ee2ff..c6b22edc22755 100644 --- a/src/Compilers/Shared/BuildClient.cs +++ b/src/Compilers/Shared/BuildClient.cs @@ -219,7 +219,7 @@ public static CompileOnServerFunc GetCompileOnServerFunc(ICompilerServerLogger l try { - var requestId = Guid.NewGuid(); + var requestId = Guid.NewGuid().ToString(); var buildRequest = BuildServerConnection.CreateBuildRequest( requestId, _language, diff --git a/src/Compilers/Shared/BuildServerConnection.cs b/src/Compilers/Shared/BuildServerConnection.cs index ae9385ea12e06..c858dd92b69ec 100644 --- a/src/Compilers/Shared/BuildServerConnection.cs +++ b/src/Compilers/Shared/BuildServerConnection.cs @@ -20,11 +20,30 @@ namespace Microsoft.CodeAnalysis.CommandLine { internal sealed class BuildServerConnection { - // Spend up to 1s connecting to existing process (existing processes should be always responsive). - internal const int TimeOutMsExistingProcess = 1000; + /// + /// The time to wait for a named pipe connection to complete to an existing server + /// process. + /// + /// + /// The compiler server is designed to be responsive to new connections so in ideal + /// circumstances a timeout as short as one second is fine. However, in practice the + /// server can become temporarily unresponsive if say the machine is under heavy load + /// or a connection occurs just as a gen2 GC occurs. + /// + /// In any of these cases abandoning the connection attempt means falling back to + /// starting csc.exe / vbc.exe which will likely make the above problems. That will + /// create a new process that adds more load to the system. + /// + /// As such this timeout should be significantly longer than the average gen2 pause + /// time for the server. When changing this value consider profiling building + /// Roslyn.sln and consulting the GC stats to see what a typical pause time is. + /// + internal const int TimeOutMsExistingProcess = 5_000; - // Spend up to 20s connecting to a new process, to allow time for it to start. - internal const int TimeOutMsNewProcess = 20000; + /// + /// The time to wait for a named pipe connection to complete for a newly started server + /// + internal const int TimeOutMsNewProcess = 20_000; // To share a mutex between processes the name should have the Global prefix private const string GlobalMutexPrefix = "Global\\"; @@ -38,7 +57,7 @@ internal sealed class BuildServerConnection /// Create a build request for processing on the server. /// internal static BuildRequest CreateBuildRequest( - Guid requestId, + string requestId, RequestLanguage language, List arguments, string workingDirectory, @@ -318,7 +337,7 @@ static async Task tryRunRequestAsync( /// internal static async Task MonitorDisconnectAsync( PipeStream pipeStream, - Guid requestId, + string requestId, ICompilerServerLogger logger, CancellationToken cancellationToken = default) { diff --git a/src/Compilers/Test/Core/Assert/AssertEx.cs b/src/Compilers/Test/Core/Assert/AssertEx.cs index 4c908c5b891d2..e5a4e66119b49 100644 --- a/src/Compilers/Test/Core/Assert/AssertEx.cs +++ b/src/Compilers/Test/Core/Assert/AssertEx.cs @@ -76,11 +76,6 @@ bool IEqualityComparer.Equals(T x, T y) } } - if (x.GetType() != y.GetType()) - { - return false; - } - if (x is IEquatable equatable) { return equatable.Equals(y); @@ -96,10 +91,7 @@ bool IEqualityComparer.Equals(T x, T y) return comparable.CompareTo(y) == 0; } - var enumerableX = x as IEnumerable; - var enumerableY = y as IEnumerable; - - if (enumerableX != null && enumerableY != null) + if (x is IEnumerable enumerableX && y is IEnumerable enumerableY) { var enumeratorX = enumerableX.GetEnumerator(); var enumeratorY = enumerableY.GetEnumerator(); @@ -959,5 +951,19 @@ public static void NotNull([NotNull] T value) Assert.NotNull(value); Debug.Assert(value is object); } + + public static void Contains(IEnumerable collection, Predicate filter, Func? itemInspector = null, string? itemSeparator = null) + { + foreach (var item in collection) + { + if (filter(item)) + { + return; + } + } + + Fail("Filter does not match any item in the collection: " + Environment.NewLine + + ToString(collection, itemSeparator ?? Environment.NewLine, itemInspector)); + } } } diff --git a/src/Compilers/Test/Utilities/CSharp/Microsoft.CodeAnalysis.CSharp.Test.Utilities.csproj b/src/Compilers/Test/Utilities/CSharp/Microsoft.CodeAnalysis.CSharp.Test.Utilities.csproj index aed021b5be1eb..357668d44d38f 100644 --- a/src/Compilers/Test/Utilities/CSharp/Microsoft.CodeAnalysis.CSharp.Test.Utilities.csproj +++ b/src/Compilers/Test/Utilities/CSharp/Microsoft.CodeAnalysis.CSharp.Test.Utilities.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Compilers/VisualBasic/Portable/Binding/Binder_Conversions.vb b/src/Compilers/VisualBasic/Portable/Binding/Binder_Conversions.vb index 1665f06b67d7f..75d4e36989ad4 100644 --- a/src/Compilers/VisualBasic/Portable/Binding/Binder_Conversions.vb +++ b/src/Compilers/VisualBasic/Portable/Binding/Binder_Conversions.vb @@ -158,6 +158,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic constantResult = Conversions.TryFoldNothingReferenceConversion(argument, conv, targetType) End If + If Not Conversions.IsIdentityConversion(conv) Then + WarnOnLockConversion(sourceType, argument.Syntax, diagnostics) + End If + Return New BoundDirectCast(node, argument, conv, constantResult, targetType) End Function @@ -257,6 +261,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Dim constantResult = Conversions.TryFoldNothingReferenceConversion(argument, conv, targetType) + If Not Conversions.IsIdentityConversion(conv) Then + WarnOnLockConversion(sourceType, argument.Syntax, diagnostics) + End If + Return New BoundTryCast(node, argument, conv, constantResult, targetType) End Function @@ -1042,6 +1050,11 @@ DoneWithDiagnostics: Dim tupleElements As BoundConvertedTupleElements = CreateConversionForTupleElements(tree, sourceType, targetType, convKind, isExplicit) + If (convKind = ConversionKind.WideningReference OrElse convKind = ConversionKind.NarrowingReference) AndAlso + sourceType.IsWellKnownTypeLock() Then + ReportDiagnostic(diagnostics, argument.Syntax, ERRID.WRN_ConvertingLock) + End If + Return New BoundConversion(tree, argument, convKind, CheckOverflow, isExplicit, constantResult, tupleElements, targetType) End Function @@ -1723,6 +1736,12 @@ DoneWithDiagnostics: End If End Sub + Private Shared Sub WarnOnLockConversion(sourceType As TypeSymbol, syntax As SyntaxNode, diagnostics As BindingDiagnosticBag) + If sourceType IsNot Nothing AndAlso sourceType.IsWellKnownTypeLock() Then + ReportDiagnostic(diagnostics, syntax, ERRID.WRN_ConvertingLock) + End If + End Sub + Private Function IsIEnumerableOfXElement(type As TypeSymbol, <[In], Out> ByRef useSiteInfo As CompoundUseSiteInfo(Of AssemblySymbol)) As Boolean Return type.IsOrImplementsIEnumerableOfXElement(Compilation, useSiteInfo) End Function diff --git a/src/Compilers/VisualBasic/Portable/Binding/Binder_Statements.vb b/src/Compilers/VisualBasic/Portable/Binding/Binder_Statements.vb index 3615c69c72311..8c239074929b9 100644 --- a/src/Compilers/VisualBasic/Portable/Binding/Binder_Statements.vb +++ b/src/Compilers/VisualBasic/Portable/Binding/Binder_Statements.vb @@ -4731,6 +4731,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic ReportDiagnostic(diagnostics, lockExpression.Syntax, ErrorFactory.ErrorInfo(ERRID.ERR_SyncLockRequiresReferenceType1, lockExpressionType)) + ElseIf lockExpressionType.IsWellKnownTypeLock() Then + ReportDiagnostic(diagnostics, lockExpression.Syntax, ERRID.ERR_LockTypeUnsupported) End If End If diff --git a/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCompiler.vb b/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCompiler.vb index 4cd8e1ca1b010..c43521edbc5ac 100644 --- a/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCompiler.vb +++ b/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCompiler.vb @@ -297,8 +297,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Next End Sub - Private Protected Overrides Function CreateGeneratorDriver(parseOptions As ParseOptions, generators As ImmutableArray(Of ISourceGenerator), analyzerConfigOptionsProvider As AnalyzerConfigOptionsProvider, additionalTexts As ImmutableArray(Of AdditionalText)) As GeneratorDriver - Return VisualBasicGeneratorDriver.Create(generators, additionalTexts, DirectCast(parseOptions, VisualBasicParseOptions), analyzerConfigOptionsProvider) + Private Protected Overrides Function CreateGeneratorDriver(baseDirectory As String, parseOptions As ParseOptions, generators As ImmutableArray(Of ISourceGenerator), analyzerConfigOptionsProvider As AnalyzerConfigOptionsProvider, additionalTexts As ImmutableArray(Of AdditionalText)) As GeneratorDriver + Return VisualBasicGeneratorDriver.Create(generators, additionalTexts, DirectCast(parseOptions, VisualBasicParseOptions), analyzerConfigOptionsProvider, driverOptions:=New GeneratorDriverOptions() With {.BaseDirectory = baseDirectory}) End Function Private Protected Overrides Sub DiagnoseBadAccesses(consoleOutput As TextWriter, errorLogger As ErrorLogger, compilation As Compilation, diagnostics As ImmutableArray(Of Diagnostic)) diff --git a/src/Compilers/VisualBasic/Portable/Compilation/SemanticModel.vb b/src/Compilers/VisualBasic/Portable/Compilation/SemanticModel.vb index 3865bb37099c6..32e09ae72b7f2 100644 --- a/src/Compilers/VisualBasic/Portable/Compilation/SemanticModel.vb +++ b/src/Compilers/VisualBasic/Portable/Compilation/SemanticModel.vb @@ -3,6 +3,7 @@ ' See the LICENSE file in the project root for more information. Imports System.Collections.Immutable +Imports System.Diagnostics.CodeAnalysis Imports System.Runtime.InteropServices Imports System.Threading Imports Microsoft.CodeAnalysis.PooledObjects @@ -46,6 +47,9 @@ Namespace Microsoft.CodeAnalysis.VisualBasic ''' Friend MustOverride Shadows ReadOnly Property Root As SyntaxNode + + Public NotOverridable Overrides ReadOnly Property NullableAnalysisIsDisabled As Boolean = False + ''' ''' Gets symbol information about an expression syntax node. This is the worker ''' function that is overridden in various derived kinds of Semantic Models. It can assume that diff --git a/src/Compilers/VisualBasic/Portable/Compilation/VisualBasicCompilation.vb b/src/Compilers/VisualBasic/Portable/Compilation/VisualBasicCompilation.vb index df30e764d29fc..75ecf3b00227e 100644 --- a/src/Compilers/VisualBasic/Portable/Compilation/VisualBasicCompilation.vb +++ b/src/Compilers/VisualBasic/Portable/Compilation/VisualBasicCompilation.vb @@ -4,6 +4,7 @@ Imports System.Collections.Concurrent Imports System.Collections.Immutable +Imports System.Diagnostics.CodeAnalysis Imports System.IO Imports System.Reflection.Emit Imports System.Reflection.Metadata @@ -2040,16 +2041,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Public Shadows Function GetSemanticModel(syntaxTree As SyntaxTree, Optional ignoreAccessibility As Boolean = False) As SemanticModel Dim model As SemanticModel = Nothing If SemanticModelProvider IsNot Nothing Then - model = SemanticModelProvider.GetSemanticModel(syntaxTree, Me, ignoreAccessibility) +#Disable Warning RSEXPERIMENTAL001 'internal use of experimental API + model = SemanticModelProvider.GetSemanticModel(syntaxTree, Me, If(ignoreAccessibility, SemanticModelOptions.IgnoreAccessibility, SemanticModelOptions.None)) Debug.Assert(model IsNot Nothing) End If - Return If(model, CreateSemanticModel(syntaxTree, ignoreAccessibility)) + Return If(model, CreateSemanticModel(syntaxTree, If(ignoreAccessibility, SemanticModelOptions.IgnoreAccessibility, SemanticModelOptions.None))) End Function - Friend Overrides Function CreateSemanticModel(syntaxTree As SyntaxTree, ignoreAccessibility As Boolean) As SemanticModel - Return New SyntaxTreeSemanticModel(Me, DirectCast(Me.SourceModule, SourceModuleSymbol), syntaxTree, ignoreAccessibility) + Friend Overrides Function CreateSemanticModel(syntaxTree As SyntaxTree, options As SemanticModelOptions) As SemanticModel + Return New SyntaxTreeSemanticModel(Me, DirectCast(Me.SourceModule, SourceModuleSymbol), syntaxTree, ignoreAccessibility:=(options And SemanticModelOptions.IgnoreAccessibility) <> 0) End Function +#Enable Warning RSEXPERIMENTAL001 Friend ReadOnly Property FeatureStrictEnabled As Boolean Get @@ -2740,8 +2743,9 @@ Namespace Microsoft.CodeAnalysis.VisualBasic End Get End Property - Protected Overrides Function CommonGetSemanticModel(syntaxTree As SyntaxTree, ignoreAccessibility As Boolean) As SemanticModel - Return Me.GetSemanticModel(syntaxTree, ignoreAccessibility) + + Protected Overrides Function CommonGetSemanticModel(syntaxTree As SyntaxTree, options As SemanticModelOptions) As SemanticModel + Return Me.GetSemanticModel(syntaxTree, ignoreAccessibility:=(options And SemanticModelOptions.IgnoreAccessibility) <> 0) End Function Protected Overrides ReadOnly Property CommonSyntaxTrees As ImmutableArray(Of SyntaxTree) diff --git a/src/Compilers/VisualBasic/Portable/Errors/ErrorFacts.vb b/src/Compilers/VisualBasic/Portable/Errors/ErrorFacts.vb index d8e41bd58db71..8c5a24af80636 100644 --- a/src/Compilers/VisualBasic/Portable/Errors/ErrorFacts.vb +++ b/src/Compilers/VisualBasic/Portable/Errors/ErrorFacts.vb @@ -1537,7 +1537,9 @@ Namespace Microsoft.CodeAnalysis.VisualBasic ERRID.WRN_CallerArgumentExpressionAttributeHasInvalidParameterName, ERRID.WRN_AnalyzerReferencesNewerCompiler, ERRID.WRN_DuplicateAnalyzerReference, - ERRID.ERR_InvalidExperimentalDiagID + ERRID.ERR_InvalidExperimentalDiagID, + ERRID.ERR_LockTypeUnsupported, + ERRID.WRN_ConvertingLock Return False Case Else ' NOTE: All error codes must be explicitly handled in the below select case statement diff --git a/src/Compilers/VisualBasic/Portable/Errors/Errors.vb b/src/Compilers/VisualBasic/Portable/Errors/Errors.vb index 6104e6c691811..0b86a4e99a8d7 100644 --- a/src/Compilers/VisualBasic/Portable/Errors/Errors.vb +++ b/src/Compilers/VisualBasic/Portable/Errors/Errors.vb @@ -1778,7 +1778,9 @@ Namespace Microsoft.CodeAnalysis.VisualBasic ERR_InvalidExperimentalDiagID = 37328 - ERR_NextAvailable = 37329 + ERR_LockTypeUnsupported = 37329 + + ERR_NextAvailable = 37330 '// WARNINGS BEGIN HERE WRN_UseOfObsoleteSymbol2 = 40000 @@ -2011,6 +2013,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic WRN_AnalyzerReferencesNewerCompiler = 42506 WRN_DuplicateAnalyzerReference = 42507 + WRN_ConvertingLock = 42508 + ' // AVAILABLE 42600 - 49998 WRN_NextAvailable = 42600 diff --git a/src/Compilers/VisualBasic/Portable/Generated/ErrorFacts.Generated.vb b/src/Compilers/VisualBasic/Portable/Generated/ErrorFacts.Generated.vb index aabe4cb5ddb3d..a325413f92031 100644 --- a/src/Compilers/VisualBasic/Portable/Generated/ErrorFacts.Generated.vb +++ b/src/Compilers/VisualBasic/Portable/Generated/ErrorFacts.Generated.vb @@ -177,7 +177,8 @@ ERRID.WRN_CallerArgumentExpressionAttributeSelfReferential, ERRID.WRN_CallerArgumentExpressionAttributeHasInvalidParameterName, ERRID.WRN_AnalyzerReferencesNewerCompiler, - ERRID.WRN_DuplicateAnalyzerReference + ERRID.WRN_DuplicateAnalyzerReference, + ERRID.WRN_ConvertingLock Return True Case Else Return False diff --git a/src/Compilers/VisualBasic/Portable/Generated/Syntax.xml.Internal.Generated.vb b/src/Compilers/VisualBasic/Portable/Generated/Syntax.xml.Internal.Generated.vb index 611c758ee9a67..5bd77043b6295 100644 --- a/src/Compilers/VisualBasic/Portable/Generated/Syntax.xml.Internal.Generated.vb +++ b/src/Compilers/VisualBasic/Portable/Generated/Syntax.xml.Internal.Generated.vb @@ -103,7 +103,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, empty As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(empty) Me._empty = empty @@ -112,7 +112,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, empty As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(empty) @@ -122,7 +122,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), empty As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(empty) Me._empty = empty @@ -177,7 +177,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, endKeyword As InternalSyntax.KeywordSyntax, blockKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(endKeyword) Me._endKeyword = endKeyword @@ -188,7 +188,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, endKeyword As InternalSyntax.KeywordSyntax, blockKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(endKeyword) @@ -200,7 +200,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), endKeyword As InternalSyntax.KeywordSyntax, blockKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(endKeyword) Me._endKeyword = endKeyword @@ -275,7 +275,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, options As GreenNode, [imports] As GreenNode, attributes As GreenNode, members As GreenNode, endOfFileToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 If options IsNot Nothing Then AdjustFlagsAndWidth(options) @@ -300,7 +300,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, options As GreenNode, [imports] As GreenNode, attributes As GreenNode, members As GreenNode, endOfFileToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) If options IsNot Nothing Then @@ -326,7 +326,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), options As GreenNode, [imports] As GreenNode, attributes As GreenNode, members As GreenNode, endOfFileToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 If options IsNot Nothing Then AdjustFlagsAndWidth(options) @@ -459,7 +459,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, optionKeyword As InternalSyntax.KeywordSyntax, nameKeyword As InternalSyntax.KeywordSyntax, valueKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(optionKeyword) Me._optionKeyword = optionKeyword @@ -474,7 +474,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, optionKeyword As InternalSyntax.KeywordSyntax, nameKeyword As InternalSyntax.KeywordSyntax, valueKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(optionKeyword) @@ -490,7 +490,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), optionKeyword As InternalSyntax.KeywordSyntax, nameKeyword As InternalSyntax.KeywordSyntax, valueKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(optionKeyword) Me._optionKeyword = optionKeyword @@ -579,7 +579,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, importsKeyword As InternalSyntax.KeywordSyntax, importsClauses As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(importsKeyword) Me._importsKeyword = importsKeyword @@ -592,7 +592,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, importsKeyword As InternalSyntax.KeywordSyntax, importsClauses As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(importsKeyword) @@ -606,7 +606,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), importsKeyword As InternalSyntax.KeywordSyntax, importsClauses As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(importsKeyword) Me._importsKeyword = importsKeyword @@ -704,7 +704,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, [alias] As ImportAliasClauseSyntax, name As NameSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 If [alias] IsNot Nothing Then AdjustFlagsAndWidth([alias]) @@ -717,7 +717,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, [alias] As ImportAliasClauseSyntax, name As NameSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) If [alias] IsNot Nothing Then @@ -731,7 +731,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), [alias] As ImportAliasClauseSyntax, name As NameSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 If [alias] IsNot Nothing Then AdjustFlagsAndWidth([alias]) @@ -805,7 +805,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax, equalsToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -816,7 +816,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax, equalsToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(identifier) @@ -828,7 +828,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), identifier As InternalSyntax.IdentifierTokenSyntax, equalsToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -898,7 +898,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanToken As InternalSyntax.PunctuationSyntax, xmlNamespace As XmlAttributeSyntax, greaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanToken) Me._lessThanToken = lessThanToken @@ -911,7 +911,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanToken As InternalSyntax.PunctuationSyntax, xmlNamespace As XmlAttributeSyntax, greaterThanToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lessThanToken) @@ -925,7 +925,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lessThanToken As InternalSyntax.PunctuationSyntax, xmlNamespace As XmlAttributeSyntax, greaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanToken) Me._lessThanToken = lessThanToken @@ -1002,7 +1002,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, namespaceStatement As NamespaceStatementSyntax, members As GreenNode, endNamespaceStatement As EndBlockStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(namespaceStatement) Me._namespaceStatement = namespaceStatement @@ -1017,7 +1017,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, namespaceStatement As NamespaceStatementSyntax, members As GreenNode, endNamespaceStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(namespaceStatement) @@ -1033,7 +1033,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), namespaceStatement As NamespaceStatementSyntax, members As GreenNode, endNamespaceStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(namespaceStatement) Me._namespaceStatement = namespaceStatement @@ -1121,7 +1121,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, namespaceKeyword As InternalSyntax.KeywordSyntax, name As NameSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(namespaceKeyword) Me._namespaceKeyword = namespaceKeyword @@ -1132,7 +1132,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, namespaceKeyword As InternalSyntax.KeywordSyntax, name As NameSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(namespaceKeyword) @@ -1144,7 +1144,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), namespaceKeyword As InternalSyntax.KeywordSyntax, name As NameSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(namespaceKeyword) Me._namespaceKeyword = namespaceKeyword @@ -1318,7 +1318,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, moduleStatement As ModuleStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endModuleStatement As EndBlockStatementSyntax) MyBase.New(kind, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(moduleStatement) Me._moduleStatement = moduleStatement @@ -1329,7 +1329,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, moduleStatement As ModuleStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endModuleStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(moduleStatement) @@ -1341,7 +1341,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), moduleStatement As ModuleStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endModuleStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(moduleStatement) Me._moduleStatement = moduleStatement @@ -1417,7 +1417,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, structureStatement As StructureStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endStructureStatement As EndBlockStatementSyntax) MyBase.New(kind, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(structureStatement) Me._structureStatement = structureStatement @@ -1428,7 +1428,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, structureStatement As StructureStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endStructureStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(structureStatement) @@ -1440,7 +1440,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), structureStatement As StructureStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endStructureStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(structureStatement) Me._structureStatement = structureStatement @@ -1516,7 +1516,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, interfaceStatement As InterfaceStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endInterfaceStatement As EndBlockStatementSyntax) MyBase.New(kind, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(interfaceStatement) Me._interfaceStatement = interfaceStatement @@ -1527,7 +1527,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, interfaceStatement As InterfaceStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endInterfaceStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(interfaceStatement) @@ -1539,7 +1539,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), interfaceStatement As InterfaceStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endInterfaceStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(interfaceStatement) Me._interfaceStatement = interfaceStatement @@ -1615,7 +1615,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, classStatement As ClassStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endClassStatement As EndBlockStatementSyntax) MyBase.New(kind, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(classStatement) Me._classStatement = classStatement @@ -1626,7 +1626,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, classStatement As ClassStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endClassStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(classStatement) @@ -1638,7 +1638,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), classStatement As ClassStatementSyntax, [inherits] As GreenNode, [implements] As GreenNode, members As GreenNode, endClassStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations, [inherits], [implements], members) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(classStatement) Me._classStatement = classStatement @@ -1715,7 +1715,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, enumStatement As EnumStatementSyntax, members As GreenNode, endEnumStatement As EndBlockStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(enumStatement) Me._enumStatement = enumStatement @@ -1730,7 +1730,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, enumStatement As EnumStatementSyntax, members As GreenNode, endEnumStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(enumStatement) @@ -1746,7 +1746,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), enumStatement As EnumStatementSyntax, members As GreenNode, endEnumStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(enumStatement) Me._enumStatement = enumStatement @@ -1856,7 +1856,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, inheritsKeyword As InternalSyntax.KeywordSyntax, types As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(inheritsKeyword) Me._inheritsKeyword = inheritsKeyword @@ -1869,7 +1869,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, inheritsKeyword As InternalSyntax.KeywordSyntax, types As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(inheritsKeyword) @@ -1883,7 +1883,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), inheritsKeyword As InternalSyntax.KeywordSyntax, types As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(inheritsKeyword) Me._inheritsKeyword = inheritsKeyword @@ -1954,7 +1954,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, implementsKeyword As InternalSyntax.KeywordSyntax, types As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(implementsKeyword) Me._implementsKeyword = implementsKeyword @@ -1967,7 +1967,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, implementsKeyword As InternalSyntax.KeywordSyntax, types As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(implementsKeyword) @@ -1981,7 +1981,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), implementsKeyword As InternalSyntax.KeywordSyntax, types As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(implementsKeyword) Me._implementsKeyword = implementsKeyword @@ -2177,7 +2177,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, moduleKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax) MyBase.New(kind, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(moduleKeyword) Me._moduleKeyword = moduleKeyword @@ -2186,7 +2186,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, moduleKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(moduleKeyword) @@ -2196,7 +2196,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, moduleKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(moduleKeyword) Me._moduleKeyword = moduleKeyword @@ -2260,7 +2260,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, structureKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax) MyBase.New(kind, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(structureKeyword) Me._structureKeyword = structureKeyword @@ -2269,7 +2269,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, structureKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(structureKeyword) @@ -2279,7 +2279,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, structureKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(structureKeyword) Me._structureKeyword = structureKeyword @@ -2343,7 +2343,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, interfaceKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax) MyBase.New(kind, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(interfaceKeyword) Me._interfaceKeyword = interfaceKeyword @@ -2352,7 +2352,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, interfaceKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(interfaceKeyword) @@ -2362,7 +2362,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, interfaceKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(interfaceKeyword) Me._interfaceKeyword = interfaceKeyword @@ -2426,7 +2426,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, classKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax) MyBase.New(kind, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(classKeyword) Me._classKeyword = classKeyword @@ -2435,7 +2435,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, classKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(classKeyword) @@ -2445,7 +2445,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, classKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, identifier, typeParameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(classKeyword) Me._classKeyword = classKeyword @@ -2513,7 +2513,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, enumKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, underlyingType As AsClauseSyntax) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -2536,7 +2536,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, enumKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, underlyingType As AsClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) If attributeLists IsNot Nothing Then @@ -2560,7 +2560,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, enumKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, underlyingType As AsClauseSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -2688,7 +2688,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, ofKeyword As InternalSyntax.KeywordSyntax, parameters As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -2705,7 +2705,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, ofKeyword As InternalSyntax.KeywordSyntax, parameters As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(openParenToken) @@ -2723,7 +2723,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), openParenToken As InternalSyntax.PunctuationSyntax, ofKeyword As InternalSyntax.KeywordSyntax, parameters As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -2822,7 +2822,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, varianceKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterConstraintClause As TypeParameterConstraintClauseSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 If varianceKeyword IsNot Nothing Then AdjustFlagsAndWidth(varianceKeyword) @@ -2839,7 +2839,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, varianceKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterConstraintClause As TypeParameterConstraintClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) If varianceKeyword IsNot Nothing Then @@ -2857,7 +2857,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), varianceKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterConstraintClause As TypeParameterConstraintClauseSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 If varianceKeyword IsNot Nothing Then AdjustFlagsAndWidth(varianceKeyword) @@ -2975,7 +2975,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, asKeyword As InternalSyntax.KeywordSyntax, constraint As ConstraintSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(asKeyword) Me._asKeyword = asKeyword @@ -2986,7 +2986,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, asKeyword As InternalSyntax.KeywordSyntax, constraint As ConstraintSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(asKeyword) @@ -2998,7 +2998,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), asKeyword As InternalSyntax.KeywordSyntax, constraint As ConstraintSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(asKeyword) Me._asKeyword = asKeyword @@ -3072,7 +3072,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, asKeyword As InternalSyntax.KeywordSyntax, openBraceToken As InternalSyntax.PunctuationSyntax, constraints As GreenNode, closeBraceToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(asKeyword) Me._asKeyword = asKeyword @@ -3089,7 +3089,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, asKeyword As InternalSyntax.KeywordSyntax, openBraceToken As InternalSyntax.PunctuationSyntax, constraints As GreenNode, closeBraceToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(asKeyword) @@ -3107,7 +3107,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), asKeyword As InternalSyntax.KeywordSyntax, openBraceToken As InternalSyntax.PunctuationSyntax, constraints As GreenNode, closeBraceToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(asKeyword) Me._asKeyword = asKeyword @@ -3229,7 +3229,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, constraintKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(constraintKeyword) Me._constraintKeyword = constraintKeyword @@ -3238,7 +3238,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, constraintKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(constraintKeyword) @@ -3248,7 +3248,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), constraintKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(constraintKeyword) Me._constraintKeyword = constraintKeyword @@ -3303,7 +3303,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, type As TypeSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(type) Me._type = type @@ -3312,7 +3312,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, type As TypeSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(type) @@ -3322,7 +3322,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), type As TypeSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(type) Me._type = type @@ -3378,7 +3378,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, identifier As InternalSyntax.IdentifierTokenSyntax, initializer As EqualsValueSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -3395,7 +3395,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, identifier As InternalSyntax.IdentifierTokenSyntax, initializer As EqualsValueSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) If attributeLists IsNot Nothing Then @@ -3413,7 +3413,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, identifier As InternalSyntax.IdentifierTokenSyntax, initializer As EqualsValueSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -3556,7 +3556,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, subOrFunctionStatement As MethodStatementSyntax, statements As GreenNode, endSubOrFunctionStatement As EndBlockStatementSyntax) MyBase.New(kind, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(subOrFunctionStatement) Me._subOrFunctionStatement = subOrFunctionStatement @@ -3567,7 +3567,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, subOrFunctionStatement As MethodStatementSyntax, statements As GreenNode, endSubOrFunctionStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(subOrFunctionStatement) @@ -3579,7 +3579,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), subOrFunctionStatement As MethodStatementSyntax, statements As GreenNode, endSubOrFunctionStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(subOrFunctionStatement) Me._subOrFunctionStatement = subOrFunctionStatement @@ -3651,7 +3651,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, subNewStatement As SubNewStatementSyntax, statements As GreenNode, endSubStatement As EndBlockStatementSyntax) MyBase.New(kind, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(subNewStatement) Me._subNewStatement = subNewStatement @@ -3662,7 +3662,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, subNewStatement As SubNewStatementSyntax, statements As GreenNode, endSubStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(subNewStatement) @@ -3674,7 +3674,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), subNewStatement As SubNewStatementSyntax, statements As GreenNode, endSubStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(subNewStatement) Me._subNewStatement = subNewStatement @@ -3746,7 +3746,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, operatorStatement As OperatorStatementSyntax, statements As GreenNode, endOperatorStatement As EndBlockStatementSyntax) MyBase.New(kind, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(operatorStatement) Me._operatorStatement = operatorStatement @@ -3757,7 +3757,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, operatorStatement As OperatorStatementSyntax, statements As GreenNode, endOperatorStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(operatorStatement) @@ -3769,7 +3769,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), operatorStatement As OperatorStatementSyntax, statements As GreenNode, endOperatorStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(operatorStatement) Me._operatorStatement = operatorStatement @@ -3842,7 +3842,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, accessorStatement As AccessorStatementSyntax, statements As GreenNode, endAccessorStatement As EndBlockStatementSyntax) MyBase.New(kind, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(accessorStatement) Me._accessorStatement = accessorStatement @@ -3853,7 +3853,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, accessorStatement As AccessorStatementSyntax, statements As GreenNode, endAccessorStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(accessorStatement) @@ -3865,7 +3865,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), accessorStatement As AccessorStatementSyntax, statements As GreenNode, endAccessorStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations, statements) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(accessorStatement) Me._accessorStatement = accessorStatement @@ -3940,7 +3940,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, propertyStatement As PropertyStatementSyntax, accessors As GreenNode, endPropertyStatement As EndBlockStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(propertyStatement) Me._propertyStatement = propertyStatement @@ -3955,7 +3955,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, propertyStatement As PropertyStatementSyntax, accessors As GreenNode, endPropertyStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(propertyStatement) @@ -3971,7 +3971,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), propertyStatement As PropertyStatementSyntax, accessors As GreenNode, endPropertyStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(propertyStatement) Me._propertyStatement = propertyStatement @@ -4058,7 +4058,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, eventStatement As EventStatementSyntax, accessors As GreenNode, endEventStatement As EndBlockStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(eventStatement) Me._eventStatement = eventStatement @@ -4073,7 +4073,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, eventStatement As EventStatementSyntax, accessors As GreenNode, endEventStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(eventStatement) @@ -4089,7 +4089,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), eventStatement As EventStatementSyntax, accessors As GreenNode, endEventStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(eventStatement) Me._eventStatement = eventStatement @@ -4286,7 +4286,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, parameters As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -4301,7 +4301,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, parameters As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(openParenToken) @@ -4317,7 +4317,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), openParenToken As InternalSyntax.PunctuationSyntax, parameters As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -4413,7 +4413,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax, handlesClause As HandlesClauseSyntax, implementsClause As ImplementsClauseSyntax) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 9 + Me.SlotCount = 9 AdjustFlagsAndWidth(subOrFunctionKeyword) Me._subOrFunctionKeyword = subOrFunctionKeyword @@ -4440,7 +4440,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax, handlesClause As HandlesClauseSyntax, implementsClause As ImplementsClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 9 + Me.SlotCount = 9 Me.SetFactoryContext(context) AdjustFlagsAndWidth(subOrFunctionKeyword) @@ -4468,7 +4468,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax, handlesClause As HandlesClauseSyntax, implementsClause As ImplementsClauseSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, parameterList) - MyBase._slotCount = 9 + Me.SlotCount = 9 AdjustFlagsAndWidth(subOrFunctionKeyword) Me._subOrFunctionKeyword = subOrFunctionKeyword @@ -4620,7 +4620,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, subKeyword As InternalSyntax.KeywordSyntax, newKeyword As InternalSyntax.KeywordSyntax, parameterList As ParameterListSyntax) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(subKeyword) Me._subKeyword = subKeyword @@ -4631,7 +4631,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, subKeyword As InternalSyntax.KeywordSyntax, newKeyword As InternalSyntax.KeywordSyntax, parameterList As ParameterListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(subKeyword) @@ -4643,7 +4643,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, subKeyword As InternalSyntax.KeywordSyntax, newKeyword As InternalSyntax.KeywordSyntax, parameterList As ParameterListSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, parameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(subKeyword) Me._subKeyword = subKeyword @@ -4725,7 +4725,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, declareKeyword As InternalSyntax.KeywordSyntax, charsetKeyword As InternalSyntax.KeywordSyntax, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, libKeyword As InternalSyntax.KeywordSyntax, libraryName As LiteralExpressionSyntax, aliasKeyword As InternalSyntax.KeywordSyntax, aliasName As LiteralExpressionSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 12 + Me.SlotCount = 12 AdjustFlagsAndWidth(declareKeyword) Me._declareKeyword = declareKeyword @@ -4758,7 +4758,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, declareKeyword As InternalSyntax.KeywordSyntax, charsetKeyword As InternalSyntax.KeywordSyntax, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, libKeyword As InternalSyntax.KeywordSyntax, libraryName As LiteralExpressionSyntax, aliasKeyword As InternalSyntax.KeywordSyntax, aliasName As LiteralExpressionSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 12 + Me.SlotCount = 12 Me.SetFactoryContext(context) AdjustFlagsAndWidth(declareKeyword) @@ -4792,7 +4792,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, declareKeyword As InternalSyntax.KeywordSyntax, charsetKeyword As InternalSyntax.KeywordSyntax, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, libKeyword As InternalSyntax.KeywordSyntax, libraryName As LiteralExpressionSyntax, aliasKeyword As InternalSyntax.KeywordSyntax, aliasName As LiteralExpressionSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, parameterList) - MyBase._slotCount = 12 + Me.SlotCount = 12 AdjustFlagsAndWidth(declareKeyword) Me._declareKeyword = declareKeyword @@ -4983,7 +4983,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, delegateKeyword As InternalSyntax.KeywordSyntax, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 8 + Me.SlotCount = 8 AdjustFlagsAndWidth(delegateKeyword) Me._delegateKeyword = delegateKeyword @@ -5004,7 +5004,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, delegateKeyword As InternalSyntax.KeywordSyntax, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 8 + Me.SlotCount = 8 Me.SetFactoryContext(context) AdjustFlagsAndWidth(delegateKeyword) @@ -5026,7 +5026,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, delegateKeyword As InternalSyntax.KeywordSyntax, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, typeParameterList As TypeParameterListSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, parameterList) - MyBase._slotCount = 8 + Me.SlotCount = 8 AdjustFlagsAndWidth(delegateKeyword) Me._delegateKeyword = delegateKeyword @@ -5157,7 +5157,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, customKeyword As InternalSyntax.KeywordSyntax, eventKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax, implementsClause As ImplementsClauseSyntax) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 8 + Me.SlotCount = 8 If customKeyword IsNot Nothing Then AdjustFlagsAndWidth(customKeyword) @@ -5180,7 +5180,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, customKeyword As InternalSyntax.KeywordSyntax, eventKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax, implementsClause As ImplementsClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 8 + Me.SlotCount = 8 Me.SetFactoryContext(context) If customKeyword IsNot Nothing Then @@ -5204,7 +5204,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, customKeyword As InternalSyntax.KeywordSyntax, eventKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax, implementsClause As ImplementsClauseSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, parameterList) - MyBase._slotCount = 8 + Me.SlotCount = 8 If customKeyword IsNot Nothing Then AdjustFlagsAndWidth(customKeyword) @@ -5339,7 +5339,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, operatorKeyword As InternalSyntax.KeywordSyntax, operatorToken As InternalSyntax.SyntaxToken, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 6 + Me.SlotCount = 6 AdjustFlagsAndWidth(operatorKeyword) Me._operatorKeyword = operatorKeyword @@ -5354,7 +5354,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, operatorKeyword As InternalSyntax.KeywordSyntax, operatorToken As InternalSyntax.SyntaxToken, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 6 + Me.SlotCount = 6 Me.SetFactoryContext(context) AdjustFlagsAndWidth(operatorKeyword) @@ -5370,7 +5370,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, operatorKeyword As InternalSyntax.KeywordSyntax, operatorToken As InternalSyntax.SyntaxToken, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, parameterList) - MyBase._slotCount = 6 + Me.SlotCount = 6 AdjustFlagsAndWidth(operatorKeyword) Me._operatorKeyword = operatorKeyword @@ -5470,7 +5470,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, propertyKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, parameterList As ParameterListSyntax, asClause As AsClauseSyntax, initializer As EqualsValueSyntax, implementsClause As ImplementsClauseSyntax) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 8 + Me.SlotCount = 8 AdjustFlagsAndWidth(propertyKeyword) Me._propertyKeyword = propertyKeyword @@ -5493,7 +5493,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, propertyKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, parameterList As ParameterListSyntax, asClause As AsClauseSyntax, initializer As EqualsValueSyntax, implementsClause As ImplementsClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 8 + Me.SlotCount = 8 Me.SetFactoryContext(context) AdjustFlagsAndWidth(propertyKeyword) @@ -5517,7 +5517,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, propertyKeyword As InternalSyntax.KeywordSyntax, identifier As InternalSyntax.IdentifierTokenSyntax, parameterList As ParameterListSyntax, asClause As AsClauseSyntax, initializer As EqualsValueSyntax, implementsClause As ImplementsClauseSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, parameterList) - MyBase._slotCount = 8 + Me.SlotCount = 8 AdjustFlagsAndWidth(propertyKeyword) Me._propertyKeyword = propertyKeyword @@ -5651,7 +5651,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, accessorKeyword As InternalSyntax.KeywordSyntax, parameterList As ParameterListSyntax) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(accessorKeyword) Me._accessorKeyword = accessorKeyword @@ -5660,7 +5660,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, accessorKeyword As InternalSyntax.KeywordSyntax, parameterList As ParameterListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(accessorKeyword) @@ -5670,7 +5670,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, accessorKeyword As InternalSyntax.KeywordSyntax, parameterList As ParameterListSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, parameterList) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(accessorKeyword) Me._accessorKeyword = accessorKeyword @@ -5734,7 +5734,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, implementsKeyword As InternalSyntax.KeywordSyntax, interfaceMembers As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(implementsKeyword) Me._implementsKeyword = implementsKeyword @@ -5747,7 +5747,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, implementsKeyword As InternalSyntax.KeywordSyntax, interfaceMembers As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(implementsKeyword) @@ -5761,7 +5761,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), implementsKeyword As InternalSyntax.KeywordSyntax, interfaceMembers As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(implementsKeyword) Me._implementsKeyword = implementsKeyword @@ -5833,7 +5833,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, handlesKeyword As InternalSyntax.KeywordSyntax, events As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(handlesKeyword) Me._handlesKeyword = handlesKeyword @@ -5846,7 +5846,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, handlesKeyword As InternalSyntax.KeywordSyntax, events As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(handlesKeyword) @@ -5860,7 +5860,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), handlesKeyword As InternalSyntax.KeywordSyntax, events As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(handlesKeyword) Me._handlesKeyword = handlesKeyword @@ -5953,7 +5953,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(keyword) Me._keyword = keyword @@ -5962,7 +5962,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(keyword) @@ -5972,7 +5972,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), keyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(keyword) Me._keyword = keyword @@ -6027,7 +6027,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -6036,7 +6036,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(identifier) @@ -6046,7 +6046,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), identifier As InternalSyntax.IdentifierTokenSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -6103,7 +6103,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, withEventsContainer As WithEventsEventContainerSyntax, dotToken As InternalSyntax.PunctuationSyntax, [property] As IdentifierNameSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(withEventsContainer) Me._withEventsContainer = withEventsContainer @@ -6116,7 +6116,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, withEventsContainer As WithEventsEventContainerSyntax, dotToken As InternalSyntax.PunctuationSyntax, [property] As IdentifierNameSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(withEventsContainer) @@ -6130,7 +6130,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), withEventsContainer As WithEventsEventContainerSyntax, dotToken As InternalSyntax.PunctuationSyntax, [property] As IdentifierNameSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(withEventsContainer) Me._withEventsContainer = withEventsContainer @@ -6214,7 +6214,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, eventContainer As EventContainerSyntax, dotToken As InternalSyntax.PunctuationSyntax, eventMember As IdentifierNameSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(eventContainer) Me._eventContainer = eventContainer @@ -6227,7 +6227,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, eventContainer As EventContainerSyntax, dotToken As InternalSyntax.PunctuationSyntax, eventMember As IdentifierNameSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(eventContainer) @@ -6241,7 +6241,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), eventContainer As EventContainerSyntax, dotToken As InternalSyntax.PunctuationSyntax, eventMember As IdentifierNameSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(eventContainer) Me._eventContainer = eventContainer @@ -6328,7 +6328,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, missingIdentifier As InternalSyntax.IdentifierTokenSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -6347,7 +6347,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, missingIdentifier As InternalSyntax.IdentifierTokenSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) If attributeLists IsNot Nothing Then @@ -6367,7 +6367,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, missingIdentifier As InternalSyntax.IdentifierTokenSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -6471,7 +6471,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, declarators As GreenNode) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -6490,7 +6490,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, declarators As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) If attributeLists IsNot Nothing Then @@ -6510,7 +6510,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, declarators As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -6609,7 +6609,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, names As GreenNode, asClause As AsClauseSyntax, initializer As EqualsValueSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 If names IsNot Nothing Then AdjustFlagsAndWidth(names) @@ -6628,7 +6628,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, names As GreenNode, asClause As AsClauseSyntax, initializer As EqualsValueSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) If names IsNot Nothing Then @@ -6648,7 +6648,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), names As GreenNode, asClause As AsClauseSyntax, initializer As EqualsValueSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 If names IsNot Nothing Then AdjustFlagsAndWidth(names) @@ -6794,7 +6794,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, asKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, type As TypeSyntax) MyBase.New(kind, asKeyword) - MyBase._slotCount = 3 + Me.SlotCount = 3 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -6807,7 +6807,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, asKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, type As TypeSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, asKeyword) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) If attributeLists IsNot Nothing Then @@ -6821,7 +6821,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), asKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, type As TypeSyntax) MyBase.New(kind, errors, annotations, asKeyword) - MyBase._slotCount = 3 + Me.SlotCount = 3 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -6900,7 +6900,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, asKeyword As InternalSyntax.KeywordSyntax, newExpression As NewExpressionSyntax) MyBase.New(kind, asKeyword) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(newExpression) Me._newExpression = newExpression @@ -6909,7 +6909,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, asKeyword As InternalSyntax.KeywordSyntax, newExpression As NewExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, asKeyword) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(newExpression) @@ -6919,7 +6919,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), asKeyword As InternalSyntax.KeywordSyntax, newExpression As NewExpressionSyntax) MyBase.New(kind, errors, annotations, asKeyword) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(newExpression) Me._newExpression = newExpression @@ -7002,7 +7002,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, withKeyword As InternalSyntax.KeywordSyntax, openBraceToken As InternalSyntax.PunctuationSyntax, initializers As GreenNode, closeBraceToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(withKeyword) Me._withKeyword = withKeyword @@ -7019,7 +7019,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, withKeyword As InternalSyntax.KeywordSyntax, openBraceToken As InternalSyntax.PunctuationSyntax, initializers As GreenNode, closeBraceToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(withKeyword) @@ -7037,7 +7037,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), withKeyword As InternalSyntax.KeywordSyntax, openBraceToken As InternalSyntax.PunctuationSyntax, initializers As GreenNode, closeBraceToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(withKeyword) Me._withKeyword = withKeyword @@ -7135,7 +7135,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, fromKeyword As InternalSyntax.KeywordSyntax, initializer As CollectionInitializerSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(fromKeyword) Me._fromKeyword = fromKeyword @@ -7146,7 +7146,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, fromKeyword As InternalSyntax.KeywordSyntax, initializer As CollectionInitializerSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(fromKeyword) @@ -7158,7 +7158,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), fromKeyword As InternalSyntax.KeywordSyntax, initializer As CollectionInitializerSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(fromKeyword) Me._fromKeyword = fromKeyword @@ -7281,7 +7281,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, keyKeyword) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(expression) Me._expression = expression @@ -7290,7 +7290,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, keyKeyword) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(expression) @@ -7300,7 +7300,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), keyKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations, keyKeyword) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(expression) Me._expression = expression @@ -7361,7 +7361,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyKeyword As InternalSyntax.KeywordSyntax, dotToken As InternalSyntax.PunctuationSyntax, name As IdentifierNameSyntax, equalsToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax) MyBase.New(kind, keyKeyword) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(dotToken) Me._dotToken = dotToken @@ -7376,7 +7376,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyKeyword As InternalSyntax.KeywordSyntax, dotToken As InternalSyntax.PunctuationSyntax, name As IdentifierNameSyntax, equalsToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, keyKeyword) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(dotToken) @@ -7392,7 +7392,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), keyKeyword As InternalSyntax.KeywordSyntax, dotToken As InternalSyntax.PunctuationSyntax, name As IdentifierNameSyntax, equalsToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations, keyKeyword) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(dotToken) Me._dotToken = dotToken @@ -7490,7 +7490,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, equalsToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(equalsToken) Me._equalsToken = equalsToken @@ -7501,7 +7501,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, equalsToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(equalsToken) @@ -7513,7 +7513,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), equalsToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(equalsToken) Me._equalsToken = equalsToken @@ -7585,7 +7585,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, identifier As ModifiedIdentifierSyntax, asClause As SimpleAsClauseSyntax, [default] As EqualsValueSyntax) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -7610,7 +7610,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, identifier As ModifiedIdentifierSyntax, asClause As SimpleAsClauseSyntax, [default] As EqualsValueSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) If attributeLists IsNot Nothing Then @@ -7636,7 +7636,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, identifier As ModifiedIdentifierSyntax, asClause As SimpleAsClauseSyntax, [default] As EqualsValueSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -7771,7 +7771,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax, nullable As InternalSyntax.PunctuationSyntax, arrayBounds As ArgumentListSyntax, arrayRankSpecifiers As GreenNode) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -7792,7 +7792,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax, nullable As InternalSyntax.PunctuationSyntax, arrayBounds As ArgumentListSyntax, arrayRankSpecifiers As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(identifier) @@ -7814,7 +7814,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), identifier As InternalSyntax.IdentifierTokenSyntax, nullable As InternalSyntax.PunctuationSyntax, arrayBounds As ArgumentListSyntax, arrayRankSpecifiers As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -7927,7 +7927,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, commaTokens As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -7942,7 +7942,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, commaTokens As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(openParenToken) @@ -7958,7 +7958,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), openParenToken As InternalSyntax.PunctuationSyntax, commaTokens As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -8046,7 +8046,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanToken As InternalSyntax.PunctuationSyntax, attributes As GreenNode, greaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanToken) Me._lessThanToken = lessThanToken @@ -8061,7 +8061,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanToken As InternalSyntax.PunctuationSyntax, attributes As GreenNode, greaterThanToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lessThanToken) @@ -8077,7 +8077,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lessThanToken As InternalSyntax.PunctuationSyntax, attributes As GreenNode, greaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanToken) Me._lessThanToken = lessThanToken @@ -8165,7 +8165,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, target As AttributeTargetSyntax, name As TypeSyntax, argumentList As ArgumentListSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 If target IsNot Nothing Then AdjustFlagsAndWidth(target) @@ -8178,11 +8178,12 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Me._argumentList = argumentList End If + SetFlags(NodeFlags.ContainsAttributes) End Sub Friend Sub New(ByVal kind As SyntaxKind, target As AttributeTargetSyntax, name As TypeSyntax, argumentList As ArgumentListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) If target IsNot Nothing Then @@ -8196,11 +8197,12 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Me._argumentList = argumentList End If + SetFlags(NodeFlags.ContainsAttributes) End Sub Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), target As AttributeTargetSyntax, name As TypeSyntax, argumentList As ArgumentListSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 If target IsNot Nothing Then AdjustFlagsAndWidth(target) @@ -8213,6 +8215,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Me._argumentList = argumentList End If + SetFlags(NodeFlags.ContainsAttributes) End Sub Friend Overrides Function CreateRed(ByVal parent As SyntaxNode, ByVal startLocation As Integer) As SyntaxNode @@ -8293,7 +8296,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeModifier As InternalSyntax.KeywordSyntax, colonToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(attributeModifier) Me._attributeModifier = attributeModifier @@ -8304,7 +8307,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeModifier As InternalSyntax.KeywordSyntax, colonToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(attributeModifier) @@ -8316,7 +8319,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeModifier As InternalSyntax.KeywordSyntax, colonToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(attributeModifier) Me._attributeModifier = attributeModifier @@ -8387,7 +8390,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -8398,7 +8401,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) If attributeLists IsNot Nothing Then @@ -8410,7 +8413,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 If attributeLists IsNot Nothing Then AdjustFlagsAndWidth(attributeLists) @@ -8471,7 +8474,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(expression) Me._expression = expression @@ -8480,7 +8483,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(expression) @@ -8490,7 +8493,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(expression) Me._expression = expression @@ -8545,7 +8548,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, questionToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(questionToken) Me._questionToken = questionToken @@ -8556,7 +8559,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, questionToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(questionToken) @@ -8568,7 +8571,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), questionToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(questionToken) Me._questionToken = questionToken @@ -8639,7 +8642,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, whileStatement As WhileStatementSyntax, statements As GreenNode, endWhileStatement As EndBlockStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(whileStatement) Me._whileStatement = whileStatement @@ -8654,7 +8657,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, whileStatement As WhileStatementSyntax, statements As GreenNode, endWhileStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(whileStatement) @@ -8670,7 +8673,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), whileStatement As WhileStatementSyntax, statements As GreenNode, endWhileStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(whileStatement) Me._whileStatement = whileStatement @@ -8759,7 +8762,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, usingStatement As UsingStatementSyntax, statements As GreenNode, endUsingStatement As EndBlockStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(usingStatement) Me._usingStatement = usingStatement @@ -8774,7 +8777,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, usingStatement As UsingStatementSyntax, statements As GreenNode, endUsingStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(usingStatement) @@ -8790,7 +8793,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), usingStatement As UsingStatementSyntax, statements As GreenNode, endUsingStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(usingStatement) Me._usingStatement = usingStatement @@ -8880,7 +8883,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, syncLockStatement As SyncLockStatementSyntax, statements As GreenNode, endSyncLockStatement As EndBlockStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(syncLockStatement) Me._syncLockStatement = syncLockStatement @@ -8895,7 +8898,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, syncLockStatement As SyncLockStatementSyntax, statements As GreenNode, endSyncLockStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(syncLockStatement) @@ -8911,7 +8914,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), syncLockStatement As SyncLockStatementSyntax, statements As GreenNode, endSyncLockStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(syncLockStatement) Me._syncLockStatement = syncLockStatement @@ -9001,7 +9004,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, withStatement As WithStatementSyntax, statements As GreenNode, endWithStatement As EndBlockStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(withStatement) Me._withStatement = withStatement @@ -9016,7 +9019,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, withStatement As WithStatementSyntax, statements As GreenNode, endWithStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(withStatement) @@ -9032,7 +9035,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), withStatement As WithStatementSyntax, statements As GreenNode, endWithStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(withStatement) Me._withStatement = withStatement @@ -9120,7 +9123,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, modifiers As GreenNode, declarators As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 If modifiers IsNot Nothing Then AdjustFlagsAndWidth(modifiers) @@ -9135,7 +9138,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, modifiers As GreenNode, declarators As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) If modifiers IsNot Nothing Then @@ -9151,7 +9154,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), modifiers As GreenNode, declarators As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 If modifiers IsNot Nothing Then AdjustFlagsAndWidth(modifiers) @@ -9226,7 +9229,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, labelToken As InternalSyntax.SyntaxToken, colonToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(labelToken) Me._labelToken = labelToken @@ -9237,7 +9240,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, labelToken As InternalSyntax.SyntaxToken, colonToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(labelToken) @@ -9249,7 +9252,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), labelToken As InternalSyntax.SyntaxToken, colonToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(labelToken) Me._labelToken = labelToken @@ -9319,7 +9322,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, goToKeyword As InternalSyntax.KeywordSyntax, label As LabelSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(goToKeyword) Me._goToKeyword = goToKeyword @@ -9330,7 +9333,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, goToKeyword As InternalSyntax.KeywordSyntax, label As LabelSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(goToKeyword) @@ -9342,7 +9345,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), goToKeyword As InternalSyntax.KeywordSyntax, label As LabelSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(goToKeyword) Me._goToKeyword = goToKeyword @@ -9412,7 +9415,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, labelToken As InternalSyntax.SyntaxToken) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(labelToken) Me._labelToken = labelToken @@ -9421,7 +9424,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, labelToken As InternalSyntax.SyntaxToken, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(labelToken) @@ -9431,7 +9434,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), labelToken As InternalSyntax.SyntaxToken) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(labelToken) Me._labelToken = labelToken @@ -9487,7 +9490,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, stopOrEndKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(stopOrEndKeyword) Me._stopOrEndKeyword = stopOrEndKeyword @@ -9496,7 +9499,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, stopOrEndKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(stopOrEndKeyword) @@ -9506,7 +9509,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), stopOrEndKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(stopOrEndKeyword) Me._stopOrEndKeyword = stopOrEndKeyword @@ -9562,7 +9565,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, exitKeyword As InternalSyntax.KeywordSyntax, blockKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(exitKeyword) Me._exitKeyword = exitKeyword @@ -9573,7 +9576,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, exitKeyword As InternalSyntax.KeywordSyntax, blockKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(exitKeyword) @@ -9585,7 +9588,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), exitKeyword As InternalSyntax.KeywordSyntax, blockKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(exitKeyword) Me._exitKeyword = exitKeyword @@ -9655,7 +9658,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, continueKeyword As InternalSyntax.KeywordSyntax, blockKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(continueKeyword) Me._continueKeyword = continueKeyword @@ -9666,7 +9669,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, continueKeyword As InternalSyntax.KeywordSyntax, blockKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(continueKeyword) @@ -9678,7 +9681,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), continueKeyword As InternalSyntax.KeywordSyntax, blockKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(continueKeyword) Me._continueKeyword = continueKeyword @@ -9748,7 +9751,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, returnKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(returnKeyword) Me._returnKeyword = returnKeyword @@ -9761,7 +9764,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, returnKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(returnKeyword) @@ -9775,7 +9778,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), returnKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(returnKeyword) Me._returnKeyword = returnKeyword @@ -9852,7 +9855,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ifKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax, statements As GreenNode, elseClause As SingleLineElseClauseSyntax) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(ifKeyword) Me._ifKeyword = ifKeyword @@ -9873,7 +9876,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ifKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax, statements As GreenNode, elseClause As SingleLineElseClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(ifKeyword) @@ -9895,7 +9898,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), ifKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax, statements As GreenNode, elseClause As SingleLineElseClauseSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(ifKeyword) Me._ifKeyword = ifKeyword @@ -10014,7 +10017,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseKeyword As InternalSyntax.KeywordSyntax, statements As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elseKeyword) Me._elseKeyword = elseKeyword @@ -10027,7 +10030,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseKeyword As InternalSyntax.KeywordSyntax, statements As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(elseKeyword) @@ -10041,7 +10044,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), elseKeyword As InternalSyntax.KeywordSyntax, statements As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elseKeyword) Me._elseKeyword = elseKeyword @@ -10120,7 +10123,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ifStatement As IfStatementSyntax, statements As GreenNode, elseIfBlocks As GreenNode, elseBlock As ElseBlockSyntax, endIfStatement As EndBlockStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(ifStatement) Me._ifStatement = ifStatement @@ -10143,7 +10146,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ifStatement As IfStatementSyntax, statements As GreenNode, elseIfBlocks As GreenNode, elseBlock As ElseBlockSyntax, endIfStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(ifStatement) @@ -10167,7 +10170,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), ifStatement As IfStatementSyntax, statements As GreenNode, elseIfBlocks As GreenNode, elseBlock As ElseBlockSyntax, endIfStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(ifStatement) Me._ifStatement = ifStatement @@ -10294,7 +10297,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ifKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(ifKeyword) Me._ifKeyword = ifKeyword @@ -10309,7 +10312,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ifKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(ifKeyword) @@ -10325,7 +10328,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), ifKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(ifKeyword) Me._ifKeyword = ifKeyword @@ -10412,7 +10415,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseIfStatement As ElseIfStatementSyntax, statements As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elseIfStatement) Me._elseIfStatement = elseIfStatement @@ -10425,7 +10428,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseIfStatement As ElseIfStatementSyntax, statements As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(elseIfStatement) @@ -10439,7 +10442,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), elseIfStatement As ElseIfStatementSyntax, statements As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elseIfStatement) Me._elseIfStatement = elseIfStatement @@ -10515,7 +10518,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseIfKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(elseIfKeyword) Me._elseIfKeyword = elseIfKeyword @@ -10530,7 +10533,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseIfKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(elseIfKeyword) @@ -10546,7 +10549,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), elseIfKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(elseIfKeyword) Me._elseIfKeyword = elseIfKeyword @@ -10633,7 +10636,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseStatement As ElseStatementSyntax, statements As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elseStatement) Me._elseStatement = elseStatement @@ -10646,7 +10649,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseStatement As ElseStatementSyntax, statements As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(elseStatement) @@ -10660,7 +10663,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), elseStatement As ElseStatementSyntax, statements As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elseStatement) Me._elseStatement = elseStatement @@ -10733,7 +10736,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(elseKeyword) Me._elseKeyword = elseKeyword @@ -10742,7 +10745,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(elseKeyword) @@ -10752,7 +10755,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), elseKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(elseKeyword) Me._elseKeyword = elseKeyword @@ -10810,7 +10813,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, tryStatement As TryStatementSyntax, statements As GreenNode, catchBlocks As GreenNode, finallyBlock As FinallyBlockSyntax, endTryStatement As EndBlockStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(tryStatement) Me._tryStatement = tryStatement @@ -10833,7 +10836,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, tryStatement As TryStatementSyntax, statements As GreenNode, catchBlocks As GreenNode, finallyBlock As FinallyBlockSyntax, endTryStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(tryStatement) @@ -10857,7 +10860,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), tryStatement As TryStatementSyntax, statements As GreenNode, catchBlocks As GreenNode, finallyBlock As FinallyBlockSyntax, endTryStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(tryStatement) Me._tryStatement = tryStatement @@ -10980,7 +10983,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, tryKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(tryKeyword) Me._tryKeyword = tryKeyword @@ -10989,7 +10992,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, tryKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(tryKeyword) @@ -10999,7 +11002,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), tryKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(tryKeyword) Me._tryKeyword = tryKeyword @@ -11054,7 +11057,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, catchStatement As CatchStatementSyntax, statements As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(catchStatement) Me._catchStatement = catchStatement @@ -11067,7 +11070,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, catchStatement As CatchStatementSyntax, statements As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(catchStatement) @@ -11081,7 +11084,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), catchStatement As CatchStatementSyntax, statements As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(catchStatement) Me._catchStatement = catchStatement @@ -11158,7 +11161,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, catchKeyword As InternalSyntax.KeywordSyntax, identifierName As IdentifierNameSyntax, asClause As SimpleAsClauseSyntax, whenClause As CatchFilterClauseSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(catchKeyword) Me._catchKeyword = catchKeyword @@ -11179,7 +11182,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, catchKeyword As InternalSyntax.KeywordSyntax, identifierName As IdentifierNameSyntax, asClause As SimpleAsClauseSyntax, whenClause As CatchFilterClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(catchKeyword) @@ -11201,7 +11204,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), catchKeyword As InternalSyntax.KeywordSyntax, identifierName As IdentifierNameSyntax, asClause As SimpleAsClauseSyntax, whenClause As CatchFilterClauseSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(catchKeyword) Me._catchKeyword = catchKeyword @@ -11312,7 +11315,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, whenKeyword As InternalSyntax.KeywordSyntax, filter As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(whenKeyword) Me._whenKeyword = whenKeyword @@ -11323,7 +11326,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, whenKeyword As InternalSyntax.KeywordSyntax, filter As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(whenKeyword) @@ -11335,7 +11338,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), whenKeyword As InternalSyntax.KeywordSyntax, filter As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(whenKeyword) Me._whenKeyword = whenKeyword @@ -11404,7 +11407,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, finallyStatement As FinallyStatementSyntax, statements As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(finallyStatement) Me._finallyStatement = finallyStatement @@ -11417,7 +11420,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, finallyStatement As FinallyStatementSyntax, statements As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(finallyStatement) @@ -11431,7 +11434,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), finallyStatement As FinallyStatementSyntax, statements As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(finallyStatement) Me._finallyStatement = finallyStatement @@ -11504,7 +11507,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, finallyKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(finallyKeyword) Me._finallyKeyword = finallyKeyword @@ -11513,7 +11516,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, finallyKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(finallyKeyword) @@ -11523,7 +11526,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), finallyKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(finallyKeyword) Me._finallyKeyword = finallyKeyword @@ -11578,7 +11581,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, errorKeyword As InternalSyntax.KeywordSyntax, errorNumber As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(errorKeyword) Me._errorKeyword = errorKeyword @@ -11589,7 +11592,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, errorKeyword As InternalSyntax.KeywordSyntax, errorNumber As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(errorKeyword) @@ -11601,7 +11604,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), errorKeyword As InternalSyntax.KeywordSyntax, errorNumber As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(errorKeyword) Me._errorKeyword = errorKeyword @@ -11673,7 +11676,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, onKeyword As InternalSyntax.KeywordSyntax, errorKeyword As InternalSyntax.KeywordSyntax, goToKeyword As InternalSyntax.KeywordSyntax, minus As InternalSyntax.PunctuationSyntax, label As LabelSyntax) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(onKeyword) Me._onKeyword = onKeyword @@ -11692,7 +11695,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, onKeyword As InternalSyntax.KeywordSyntax, errorKeyword As InternalSyntax.KeywordSyntax, goToKeyword As InternalSyntax.KeywordSyntax, minus As InternalSyntax.PunctuationSyntax, label As LabelSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(onKeyword) @@ -11712,7 +11715,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), onKeyword As InternalSyntax.KeywordSyntax, errorKeyword As InternalSyntax.KeywordSyntax, goToKeyword As InternalSyntax.KeywordSyntax, minus As InternalSyntax.PunctuationSyntax, label As LabelSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(onKeyword) Me._onKeyword = onKeyword @@ -11828,7 +11831,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, onKeyword As InternalSyntax.KeywordSyntax, errorKeyword As InternalSyntax.KeywordSyntax, resumeKeyword As InternalSyntax.KeywordSyntax, nextKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(onKeyword) Me._onKeyword = onKeyword @@ -11843,7 +11846,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, onKeyword As InternalSyntax.KeywordSyntax, errorKeyword As InternalSyntax.KeywordSyntax, resumeKeyword As InternalSyntax.KeywordSyntax, nextKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(onKeyword) @@ -11859,7 +11862,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), onKeyword As InternalSyntax.KeywordSyntax, errorKeyword As InternalSyntax.KeywordSyntax, resumeKeyword As InternalSyntax.KeywordSyntax, nextKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(onKeyword) Me._onKeyword = onKeyword @@ -11955,7 +11958,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, resumeKeyword As InternalSyntax.KeywordSyntax, label As LabelSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(resumeKeyword) Me._resumeKeyword = resumeKeyword @@ -11968,7 +11971,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, resumeKeyword As InternalSyntax.KeywordSyntax, label As LabelSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(resumeKeyword) @@ -11982,7 +11985,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), resumeKeyword As InternalSyntax.KeywordSyntax, label As LabelSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(resumeKeyword) Me._resumeKeyword = resumeKeyword @@ -12060,7 +12063,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, selectStatement As SelectStatementSyntax, caseBlocks As GreenNode, endSelectStatement As EndBlockStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(selectStatement) Me._selectStatement = selectStatement @@ -12075,7 +12078,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, selectStatement As SelectStatementSyntax, caseBlocks As GreenNode, endSelectStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(selectStatement) @@ -12091,7 +12094,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), selectStatement As SelectStatementSyntax, caseBlocks As GreenNode, endSelectStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(selectStatement) Me._selectStatement = selectStatement @@ -12180,7 +12183,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, selectKeyword As InternalSyntax.KeywordSyntax, caseKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(selectKeyword) Me._selectKeyword = selectKeyword @@ -12195,7 +12198,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, selectKeyword As InternalSyntax.KeywordSyntax, caseKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(selectKeyword) @@ -12211,7 +12214,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), selectKeyword As InternalSyntax.KeywordSyntax, caseKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(selectKeyword) Me._selectKeyword = selectKeyword @@ -12298,7 +12301,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, caseStatement As CaseStatementSyntax, statements As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(caseStatement) Me._caseStatement = caseStatement @@ -12311,7 +12314,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, caseStatement As CaseStatementSyntax, statements As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(caseStatement) @@ -12325,7 +12328,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), caseStatement As CaseStatementSyntax, statements As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(caseStatement) Me._caseStatement = caseStatement @@ -12401,7 +12404,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, caseKeyword As InternalSyntax.KeywordSyntax, cases As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(caseKeyword) Me._caseKeyword = caseKeyword @@ -12414,7 +12417,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, caseKeyword As InternalSyntax.KeywordSyntax, cases As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(caseKeyword) @@ -12428,7 +12431,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), caseKeyword As InternalSyntax.KeywordSyntax, cases As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(caseKeyword) Me._caseKeyword = caseKeyword @@ -12522,7 +12525,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(elseKeyword) Me._elseKeyword = elseKeyword @@ -12531,7 +12534,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elseKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(elseKeyword) @@ -12541,7 +12544,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), elseKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(elseKeyword) Me._elseKeyword = elseKeyword @@ -12595,7 +12598,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, value As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(value) Me._value = value @@ -12604,7 +12607,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, value As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(value) @@ -12614,7 +12617,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), value As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(value) Me._value = value @@ -12670,7 +12673,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lowerBound As ExpressionSyntax, toKeyword As InternalSyntax.KeywordSyntax, upperBound As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lowerBound) Me._lowerBound = lowerBound @@ -12683,7 +12686,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lowerBound As ExpressionSyntax, toKeyword As InternalSyntax.KeywordSyntax, upperBound As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lowerBound) @@ -12697,7 +12700,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lowerBound As ExpressionSyntax, toKeyword As InternalSyntax.KeywordSyntax, upperBound As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lowerBound) Me._lowerBound = lowerBound @@ -12780,7 +12783,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, isKeyword As InternalSyntax.KeywordSyntax, operatorToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 If isKeyword IsNot Nothing Then AdjustFlagsAndWidth(isKeyword) @@ -12795,7 +12798,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, isKeyword As InternalSyntax.KeywordSyntax, operatorToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) If isKeyword IsNot Nothing Then @@ -12811,7 +12814,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), isKeyword As InternalSyntax.KeywordSyntax, operatorToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 If isKeyword IsNot Nothing Then AdjustFlagsAndWidth(isKeyword) @@ -12900,7 +12903,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, syncLockKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(syncLockKeyword) Me._syncLockKeyword = syncLockKeyword @@ -12911,7 +12914,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, syncLockKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(syncLockKeyword) @@ -12923,7 +12926,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), syncLockKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(syncLockKeyword) Me._syncLockKeyword = syncLockKeyword @@ -12994,7 +12997,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, doStatement As DoStatementSyntax, statements As GreenNode, loopStatement As LoopStatementSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(doStatement) Me._doStatement = doStatement @@ -13009,7 +13012,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, doStatement As DoStatementSyntax, statements As GreenNode, loopStatement As LoopStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(doStatement) @@ -13025,7 +13028,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), doStatement As DoStatementSyntax, statements As GreenNode, loopStatement As LoopStatementSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(doStatement) Me._doStatement = doStatement @@ -13112,7 +13115,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, doKeyword As InternalSyntax.KeywordSyntax, whileOrUntilClause As WhileOrUntilClauseSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(doKeyword) Me._doKeyword = doKeyword @@ -13125,7 +13128,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, doKeyword As InternalSyntax.KeywordSyntax, whileOrUntilClause As WhileOrUntilClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(doKeyword) @@ -13139,7 +13142,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), doKeyword As InternalSyntax.KeywordSyntax, whileOrUntilClause As WhileOrUntilClauseSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(doKeyword) Me._doKeyword = doKeyword @@ -13214,7 +13217,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, loopKeyword As InternalSyntax.KeywordSyntax, whileOrUntilClause As WhileOrUntilClauseSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(loopKeyword) Me._loopKeyword = loopKeyword @@ -13227,7 +13230,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, loopKeyword As InternalSyntax.KeywordSyntax, whileOrUntilClause As WhileOrUntilClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(loopKeyword) @@ -13241,7 +13244,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), loopKeyword As InternalSyntax.KeywordSyntax, whileOrUntilClause As WhileOrUntilClauseSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(loopKeyword) Me._loopKeyword = loopKeyword @@ -13318,7 +13321,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, whileOrUntilKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(whileOrUntilKeyword) Me._whileOrUntilKeyword = whileOrUntilKeyword @@ -13329,7 +13332,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, whileOrUntilKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(whileOrUntilKeyword) @@ -13341,7 +13344,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), whileOrUntilKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(whileOrUntilKeyword) Me._whileOrUntilKeyword = whileOrUntilKeyword @@ -13411,7 +13414,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, whileKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(whileKeyword) Me._whileKeyword = whileKeyword @@ -13422,7 +13425,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, whileKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(whileKeyword) @@ -13434,7 +13437,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), whileKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(whileKeyword) Me._whileKeyword = whileKeyword @@ -13584,7 +13587,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, forStatement As ForStatementSyntax, statements As GreenNode, nextStatement As NextStatementSyntax) MyBase.New(kind, statements, nextStatement) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(forStatement) Me._forStatement = forStatement @@ -13593,7 +13596,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, forStatement As ForStatementSyntax, statements As GreenNode, nextStatement As NextStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, statements, nextStatement) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(forStatement) @@ -13603,7 +13606,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), forStatement As ForStatementSyntax, statements As GreenNode, nextStatement As NextStatementSyntax) MyBase.New(kind, errors, annotations, statements, nextStatement) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(forStatement) Me._forStatement = forStatement @@ -13664,7 +13667,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, forEachStatement As ForEachStatementSyntax, statements As GreenNode, nextStatement As NextStatementSyntax) MyBase.New(kind, statements, nextStatement) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(forEachStatement) Me._forEachStatement = forEachStatement @@ -13673,7 +13676,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, forEachStatement As ForEachStatementSyntax, statements As GreenNode, nextStatement As NextStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, statements, nextStatement) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(forEachStatement) @@ -13683,7 +13686,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), forEachStatement As ForEachStatementSyntax, statements As GreenNode, nextStatement As NextStatementSyntax) MyBase.New(kind, errors, annotations, statements, nextStatement) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(forEachStatement) Me._forEachStatement = forEachStatement @@ -13814,7 +13817,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, forKeyword As InternalSyntax.KeywordSyntax, controlVariable As VisualBasicSyntaxNode, equalsToken As InternalSyntax.PunctuationSyntax, fromValue As ExpressionSyntax, toKeyword As InternalSyntax.KeywordSyntax, toValue As ExpressionSyntax, stepClause As ForStepClauseSyntax) MyBase.New(kind, forKeyword, controlVariable) - MyBase._slotCount = 7 + Me.SlotCount = 7 AdjustFlagsAndWidth(equalsToken) Me._equalsToken = equalsToken @@ -13833,7 +13836,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, forKeyword As InternalSyntax.KeywordSyntax, controlVariable As VisualBasicSyntaxNode, equalsToken As InternalSyntax.PunctuationSyntax, fromValue As ExpressionSyntax, toKeyword As InternalSyntax.KeywordSyntax, toValue As ExpressionSyntax, stepClause As ForStepClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, forKeyword, controlVariable) - MyBase._slotCount = 7 + Me.SlotCount = 7 Me.SetFactoryContext(context) AdjustFlagsAndWidth(equalsToken) @@ -13853,7 +13856,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), forKeyword As InternalSyntax.KeywordSyntax, controlVariable As VisualBasicSyntaxNode, equalsToken As InternalSyntax.PunctuationSyntax, fromValue As ExpressionSyntax, toKeyword As InternalSyntax.KeywordSyntax, toValue As ExpressionSyntax, stepClause As ForStepClauseSyntax) MyBase.New(kind, errors, annotations, forKeyword, controlVariable) - MyBase._slotCount = 7 + Me.SlotCount = 7 AdjustFlagsAndWidth(equalsToken) Me._equalsToken = equalsToken @@ -13970,7 +13973,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, stepKeyword As InternalSyntax.KeywordSyntax, stepValue As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(stepKeyword) Me._stepKeyword = stepKeyword @@ -13981,7 +13984,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, stepKeyword As InternalSyntax.KeywordSyntax, stepValue As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(stepKeyword) @@ -13993,7 +13996,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), stepKeyword As InternalSyntax.KeywordSyntax, stepValue As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(stepKeyword) Me._stepKeyword = stepKeyword @@ -14069,7 +14072,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, forKeyword As InternalSyntax.KeywordSyntax, eachKeyword As InternalSyntax.KeywordSyntax, controlVariable As VisualBasicSyntaxNode, inKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, forKeyword, controlVariable) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(eachKeyword) Me._eachKeyword = eachKeyword @@ -14082,7 +14085,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, forKeyword As InternalSyntax.KeywordSyntax, eachKeyword As InternalSyntax.KeywordSyntax, controlVariable As VisualBasicSyntaxNode, inKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, forKeyword, controlVariable) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(eachKeyword) @@ -14096,7 +14099,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), forKeyword As InternalSyntax.KeywordSyntax, eachKeyword As InternalSyntax.KeywordSyntax, controlVariable As VisualBasicSyntaxNode, inKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations, forKeyword, controlVariable) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(eachKeyword) Me._eachKeyword = eachKeyword @@ -14185,7 +14188,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, nextKeyword As InternalSyntax.KeywordSyntax, controlVariables As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(nextKeyword) Me._nextKeyword = nextKeyword @@ -14198,7 +14201,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, nextKeyword As InternalSyntax.KeywordSyntax, controlVariables As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(nextKeyword) @@ -14212,7 +14215,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), nextKeyword As InternalSyntax.KeywordSyntax, controlVariables As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(nextKeyword) Me._nextKeyword = nextKeyword @@ -14289,7 +14292,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, usingKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, variables As GreenNode) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(usingKeyword) Me._usingKeyword = usingKeyword @@ -14306,7 +14309,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, usingKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, variables As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(usingKeyword) @@ -14324,7 +14327,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), usingKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, variables As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(usingKeyword) Me._usingKeyword = usingKeyword @@ -14418,7 +14421,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, throwKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(throwKeyword) Me._throwKeyword = throwKeyword @@ -14431,7 +14434,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, throwKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(throwKeyword) @@ -14445,7 +14448,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), throwKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(throwKeyword) Me._throwKeyword = throwKeyword @@ -14521,7 +14524,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, left As ExpressionSyntax, operatorToken As InternalSyntax.PunctuationSyntax, right As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(left) Me._left = left @@ -14534,7 +14537,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, left As ExpressionSyntax, operatorToken As InternalSyntax.PunctuationSyntax, right As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(left) @@ -14548,7 +14551,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), left As ExpressionSyntax, operatorToken As InternalSyntax.PunctuationSyntax, right As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(left) Me._left = left @@ -14631,7 +14634,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, mid As InternalSyntax.IdentifierTokenSyntax, argumentList As ArgumentListSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(mid) Me._mid = mid @@ -14642,7 +14645,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, mid As InternalSyntax.IdentifierTokenSyntax, argumentList As ArgumentListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(mid) @@ -14654,7 +14657,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), mid As InternalSyntax.IdentifierTokenSyntax, argumentList As ArgumentListSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(mid) Me._mid = mid @@ -14723,7 +14726,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, callKeyword As InternalSyntax.KeywordSyntax, invocation As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(callKeyword) Me._callKeyword = callKeyword @@ -14734,7 +14737,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, callKeyword As InternalSyntax.KeywordSyntax, invocation As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(callKeyword) @@ -14746,7 +14749,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), callKeyword As InternalSyntax.KeywordSyntax, invocation As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(callKeyword) Me._callKeyword = callKeyword @@ -14819,7 +14822,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, addHandlerOrRemoveHandlerKeyword As InternalSyntax.KeywordSyntax, eventExpression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, delegateExpression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(addHandlerOrRemoveHandlerKeyword) Me._addHandlerOrRemoveHandlerKeyword = addHandlerOrRemoveHandlerKeyword @@ -14834,7 +14837,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, addHandlerOrRemoveHandlerKeyword As InternalSyntax.KeywordSyntax, eventExpression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, delegateExpression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(addHandlerOrRemoveHandlerKeyword) @@ -14850,7 +14853,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), addHandlerOrRemoveHandlerKeyword As InternalSyntax.KeywordSyntax, eventExpression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, delegateExpression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(addHandlerOrRemoveHandlerKeyword) Me._addHandlerOrRemoveHandlerKeyword = addHandlerOrRemoveHandlerKeyword @@ -14946,7 +14949,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, raiseEventKeyword As InternalSyntax.KeywordSyntax, name As IdentifierNameSyntax, argumentList As ArgumentListSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(raiseEventKeyword) Me._raiseEventKeyword = raiseEventKeyword @@ -14961,7 +14964,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, raiseEventKeyword As InternalSyntax.KeywordSyntax, name As IdentifierNameSyntax, argumentList As ArgumentListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(raiseEventKeyword) @@ -14977,7 +14980,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), raiseEventKeyword As InternalSyntax.KeywordSyntax, name As IdentifierNameSyntax, argumentList As ArgumentListSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(raiseEventKeyword) Me._raiseEventKeyword = raiseEventKeyword @@ -15066,7 +15069,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, withKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(withKeyword) Me._withKeyword = withKeyword @@ -15077,7 +15080,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, withKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(withKeyword) @@ -15089,7 +15092,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), withKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(withKeyword) Me._withKeyword = withKeyword @@ -15159,7 +15162,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, reDimKeyword As InternalSyntax.KeywordSyntax, preserveKeyword As InternalSyntax.KeywordSyntax, clauses As GreenNode) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(reDimKeyword) Me._reDimKeyword = reDimKeyword @@ -15176,7 +15179,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, reDimKeyword As InternalSyntax.KeywordSyntax, preserveKeyword As InternalSyntax.KeywordSyntax, clauses As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(reDimKeyword) @@ -15194,7 +15197,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), reDimKeyword As InternalSyntax.KeywordSyntax, preserveKeyword As InternalSyntax.KeywordSyntax, clauses As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(reDimKeyword) Me._reDimKeyword = reDimKeyword @@ -15284,7 +15287,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax, arrayBounds As ArgumentListSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(expression) Me._expression = expression @@ -15295,7 +15298,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax, arrayBounds As ArgumentListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(expression) @@ -15307,7 +15310,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), expression As ExpressionSyntax, arrayBounds As ArgumentListSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(expression) Me._expression = expression @@ -15376,7 +15379,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, eraseKeyword As InternalSyntax.KeywordSyntax, expressions As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(eraseKeyword) Me._eraseKeyword = eraseKeyword @@ -15389,7 +15392,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, eraseKeyword As InternalSyntax.KeywordSyntax, expressions As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(eraseKeyword) @@ -15403,7 +15406,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), eraseKeyword As InternalSyntax.KeywordSyntax, expressions As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(eraseKeyword) Me._eraseKeyword = eraseKeyword @@ -15499,7 +15502,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, token As InternalSyntax.SyntaxToken) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(token) Me._token = token @@ -15508,7 +15511,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, token As InternalSyntax.SyntaxToken, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(token) @@ -15518,7 +15521,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), token As InternalSyntax.SyntaxToken) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(token) Me._token = token @@ -15582,7 +15585,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -15595,7 +15598,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(openParenToken) @@ -15609,7 +15612,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -15692,7 +15695,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, arguments As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -15707,7 +15710,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, arguments As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(openParenToken) @@ -15723,7 +15726,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), openParenToken As InternalSyntax.PunctuationSyntax, arguments As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -15808,7 +15811,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, elements As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -15823,7 +15826,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, elements As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(openParenToken) @@ -15839,7 +15842,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), openParenToken As InternalSyntax.PunctuationSyntax, elements As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -15944,7 +15947,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, type As TypeSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(type) Me._type = type @@ -15953,7 +15956,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, type As TypeSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(type) @@ -15963,7 +15966,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), type As TypeSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(type) Me._type = type @@ -16019,7 +16022,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -16032,7 +16035,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax, asClause As SimpleAsClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(identifier) @@ -16046,7 +16049,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), identifier As InternalSyntax.IdentifierTokenSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -16163,18 +16166,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, keyword) - MyBase._slotCount = 1 + Me.SlotCount = 1 End Sub Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, keyword) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) End Sub Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), keyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations, keyword) - MyBase._slotCount = 1 + Me.SlotCount = 1 End Sub Friend Overrides Function CreateRed(ByVal parent As SyntaxNode, ByVal startLocation As Integer) As SyntaxNode @@ -16214,18 +16217,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, keyword) - MyBase._slotCount = 1 + Me.SlotCount = 1 End Sub Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, keyword) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) End Sub Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), keyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations, keyword) - MyBase._slotCount = 1 + Me.SlotCount = 1 End Sub Friend Overrides Function CreateRed(ByVal parent As SyntaxNode, ByVal startLocation As Integer) As SyntaxNode @@ -16265,18 +16268,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, keyword) - MyBase._slotCount = 1 + Me.SlotCount = 1 End Sub Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, keyword) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) End Sub Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), keyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations, keyword) - MyBase._slotCount = 1 + Me.SlotCount = 1 End Sub Friend Overrides Function CreateRed(ByVal parent As SyntaxNode, ByVal startLocation As Integer) As SyntaxNode @@ -16320,7 +16323,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, getTypeKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(getTypeKeyword) Me._getTypeKeyword = getTypeKeyword @@ -16335,7 +16338,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, getTypeKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(getTypeKeyword) @@ -16351,7 +16354,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), getTypeKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(getTypeKeyword) Me._getTypeKeyword = getTypeKeyword @@ -16448,7 +16451,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, typeOfKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, operatorToken As InternalSyntax.KeywordSyntax, type As TypeSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(typeOfKeyword) Me._typeOfKeyword = typeOfKeyword @@ -16463,7 +16466,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, typeOfKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, operatorToken As InternalSyntax.KeywordSyntax, type As TypeSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(typeOfKeyword) @@ -16479,7 +16482,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), typeOfKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, operatorToken As InternalSyntax.KeywordSyntax, type As TypeSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(typeOfKeyword) Me._typeOfKeyword = typeOfKeyword @@ -16576,7 +16579,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, getXmlNamespaceKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, name As XmlPrefixNameSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(getXmlNamespaceKeyword) Me._getXmlNamespaceKeyword = getXmlNamespaceKeyword @@ -16593,7 +16596,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, getXmlNamespaceKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, name As XmlPrefixNameSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(getXmlNamespaceKeyword) @@ -16611,7 +16614,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), getXmlNamespaceKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, name As XmlPrefixNameSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(getXmlNamespaceKeyword) Me._getXmlNamespaceKeyword = getXmlNamespaceKeyword @@ -16713,7 +16716,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax, operatorToken As InternalSyntax.PunctuationSyntax, name As SimpleNameSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 If expression IsNot Nothing Then AdjustFlagsAndWidth(expression) @@ -16728,7 +16731,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax, operatorToken As InternalSyntax.PunctuationSyntax, name As SimpleNameSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) If expression IsNot Nothing Then @@ -16744,7 +16747,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), expression As ExpressionSyntax, operatorToken As InternalSyntax.PunctuationSyntax, name As SimpleNameSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 If expression IsNot Nothing Then AdjustFlagsAndWidth(expression) @@ -16836,7 +16839,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, base As ExpressionSyntax, token1 As InternalSyntax.PunctuationSyntax, token2 As InternalSyntax.PunctuationSyntax, token3 As InternalSyntax.PunctuationSyntax, name As XmlNodeSyntax) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 If base IsNot Nothing Then AdjustFlagsAndWidth(base) @@ -16859,7 +16862,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, base As ExpressionSyntax, token1 As InternalSyntax.PunctuationSyntax, token2 As InternalSyntax.PunctuationSyntax, token3 As InternalSyntax.PunctuationSyntax, name As XmlNodeSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) If base IsNot Nothing Then @@ -16883,7 +16886,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), base As ExpressionSyntax, token1 As InternalSyntax.PunctuationSyntax, token2 As InternalSyntax.PunctuationSyntax, token3 As InternalSyntax.PunctuationSyntax, name As XmlNodeSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 If base IsNot Nothing Then AdjustFlagsAndWidth(base) @@ -17008,7 +17011,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax, argumentList As ArgumentListSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 If expression IsNot Nothing Then AdjustFlagsAndWidth(expression) @@ -17023,7 +17026,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax, argumentList As ArgumentListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) If expression IsNot Nothing Then @@ -17039,7 +17042,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), expression As ExpressionSyntax, argumentList As ArgumentListSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 If expression IsNot Nothing Then AdjustFlagsAndWidth(expression) @@ -17190,7 +17193,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, newKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, type As TypeSyntax, argumentList As ArgumentListSyntax, initializer As ObjectCreationInitializerSyntax) MyBase.New(kind, newKeyword, attributeLists) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(type) Me._type = type @@ -17207,7 +17210,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, newKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, type As TypeSyntax, argumentList As ArgumentListSyntax, initializer As ObjectCreationInitializerSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, newKeyword, attributeLists) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(type) @@ -17225,7 +17228,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), newKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, type As TypeSyntax, argumentList As ArgumentListSyntax, initializer As ObjectCreationInitializerSyntax) MyBase.New(kind, errors, annotations, newKeyword, attributeLists) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(type) Me._type = type @@ -17321,7 +17324,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, newKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, initializer As ObjectMemberInitializerSyntax) MyBase.New(kind, newKeyword, attributeLists) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(initializer) Me._initializer = initializer @@ -17330,7 +17333,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, newKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, initializer As ObjectMemberInitializerSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, newKeyword, attributeLists) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(initializer) @@ -17340,7 +17343,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), newKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, initializer As ObjectMemberInitializerSyntax) MyBase.New(kind, errors, annotations, newKeyword, attributeLists) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(initializer) Me._initializer = initializer @@ -17402,7 +17405,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, newKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, type As TypeSyntax, arrayBounds As ArgumentListSyntax, rankSpecifiers As GreenNode, initializer As CollectionInitializerSyntax) MyBase.New(kind, newKeyword, attributeLists) - MyBase._slotCount = 6 + Me.SlotCount = 6 AdjustFlagsAndWidth(type) Me._type = type @@ -17421,7 +17424,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, newKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, type As TypeSyntax, arrayBounds As ArgumentListSyntax, rankSpecifiers As GreenNode, initializer As CollectionInitializerSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, newKeyword, attributeLists) - MyBase._slotCount = 6 + Me.SlotCount = 6 Me.SetFactoryContext(context) AdjustFlagsAndWidth(type) @@ -17441,7 +17444,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), newKeyword As InternalSyntax.KeywordSyntax, attributeLists As GreenNode, type As TypeSyntax, arrayBounds As ArgumentListSyntax, rankSpecifiers As GreenNode, initializer As CollectionInitializerSyntax) MyBase.New(kind, errors, annotations, newKeyword, attributeLists) - MyBase._slotCount = 6 + Me.SlotCount = 6 AdjustFlagsAndWidth(type) Me._type = type @@ -17553,7 +17556,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openBraceToken As InternalSyntax.PunctuationSyntax, initializers As GreenNode, closeBraceToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openBraceToken) Me._openBraceToken = openBraceToken @@ -17568,7 +17571,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openBraceToken As InternalSyntax.PunctuationSyntax, initializers As GreenNode, closeBraceToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(openBraceToken) @@ -17584,7 +17587,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), openBraceToken As InternalSyntax.PunctuationSyntax, initializers As GreenNode, closeBraceToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openBraceToken) Me._openBraceToken = openBraceToken @@ -17791,18 +17794,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, keyword, openParenToken, expression, commaToken, type, closeParenToken) - MyBase._slotCount = 6 + Me.SlotCount = 6 End Sub Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, keyword, openParenToken, expression, commaToken, type, closeParenToken) - MyBase._slotCount = 6 + Me.SlotCount = 6 Me.SetFactoryContext(context) End Sub Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations, keyword, openParenToken, expression, commaToken, type, closeParenToken) - MyBase._slotCount = 6 + Me.SlotCount = 6 End Sub Friend Overrides Function CreateRed(ByVal parent As SyntaxNode, ByVal startLocation As Integer) As SyntaxNode @@ -17850,18 +17853,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, keyword, openParenToken, expression, commaToken, type, closeParenToken) - MyBase._slotCount = 6 + Me.SlotCount = 6 End Sub Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, keyword, openParenToken, expression, commaToken, type, closeParenToken) - MyBase._slotCount = 6 + Me.SlotCount = 6 Me.SetFactoryContext(context) End Sub Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations, keyword, openParenToken, expression, commaToken, type, closeParenToken) - MyBase._slotCount = 6 + Me.SlotCount = 6 End Sub Friend Overrides Function CreateRed(ByVal parent As SyntaxNode, ByVal startLocation As Integer) As SyntaxNode @@ -17909,18 +17912,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, keyword, openParenToken, expression, commaToken, type, closeParenToken) - MyBase._slotCount = 6 + Me.SlotCount = 6 End Sub Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, keyword, openParenToken, expression, commaToken, type, closeParenToken) - MyBase._slotCount = 6 + Me.SlotCount = 6 Me.SetFactoryContext(context) End Sub Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, type As TypeSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations, keyword, openParenToken, expression, commaToken, type, closeParenToken) - MyBase._slotCount = 6 + Me.SlotCount = 6 End Sub Friend Overrides Function CreateRed(ByVal parent As SyntaxNode, ByVal startLocation As Integer) As SyntaxNode @@ -17976,7 +17979,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(keyword) Me._keyword = keyword @@ -17991,7 +17994,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(keyword) @@ -18007,7 +18010,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), keyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(keyword) Me._keyword = keyword @@ -18105,7 +18108,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, left As ExpressionSyntax, operatorToken As InternalSyntax.SyntaxToken, right As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(left) Me._left = left @@ -18118,7 +18121,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, left As ExpressionSyntax, operatorToken As InternalSyntax.SyntaxToken, right As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(left) @@ -18132,7 +18135,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), left As ExpressionSyntax, operatorToken As InternalSyntax.SyntaxToken, right As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(left) Me._left = left @@ -18211,7 +18214,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, operatorToken As InternalSyntax.SyntaxToken, operand As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(operatorToken) Me._operatorToken = operatorToken @@ -18222,7 +18225,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, operatorToken As InternalSyntax.SyntaxToken, operand As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(operatorToken) @@ -18234,7 +18237,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), operatorToken As InternalSyntax.SyntaxToken, operand As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(operatorToken) Me._operatorToken = operatorToken @@ -18308,7 +18311,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ifKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, firstExpression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, secondExpression As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 6 + Me.SlotCount = 6 AdjustFlagsAndWidth(ifKeyword) Me._ifKeyword = ifKeyword @@ -18327,7 +18330,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ifKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, firstExpression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, secondExpression As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 6 + Me.SlotCount = 6 Me.SetFactoryContext(context) AdjustFlagsAndWidth(ifKeyword) @@ -18347,7 +18350,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), ifKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, firstExpression As ExpressionSyntax, commaToken As InternalSyntax.PunctuationSyntax, secondExpression As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 6 + Me.SlotCount = 6 AdjustFlagsAndWidth(ifKeyword) Me._ifKeyword = ifKeyword @@ -18475,7 +18478,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ifKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, condition As ExpressionSyntax, firstCommaToken As InternalSyntax.PunctuationSyntax, whenTrue As ExpressionSyntax, secondCommaToken As InternalSyntax.PunctuationSyntax, whenFalse As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 8 + Me.SlotCount = 8 AdjustFlagsAndWidth(ifKeyword) Me._ifKeyword = ifKeyword @@ -18498,7 +18501,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ifKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, condition As ExpressionSyntax, firstCommaToken As InternalSyntax.PunctuationSyntax, whenTrue As ExpressionSyntax, secondCommaToken As InternalSyntax.PunctuationSyntax, whenFalse As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 8 + Me.SlotCount = 8 Me.SetFactoryContext(context) AdjustFlagsAndWidth(ifKeyword) @@ -18522,7 +18525,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), ifKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, condition As ExpressionSyntax, firstCommaToken As InternalSyntax.PunctuationSyntax, whenTrue As ExpressionSyntax, secondCommaToken As InternalSyntax.PunctuationSyntax, whenFalse As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 8 + Me.SlotCount = 8 AdjustFlagsAndWidth(ifKeyword) Me._ifKeyword = ifKeyword @@ -18713,7 +18716,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, subOrFunctionHeader As LambdaHeaderSyntax, body As VisualBasicSyntaxNode) MyBase.New(kind, subOrFunctionHeader) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(body) Me._body = body @@ -18722,7 +18725,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, subOrFunctionHeader As LambdaHeaderSyntax, body As VisualBasicSyntaxNode, context As ISyntaxFactoryContext) MyBase.New(kind, subOrFunctionHeader) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(body) @@ -18732,7 +18735,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), subOrFunctionHeader As LambdaHeaderSyntax, body As VisualBasicSyntaxNode) MyBase.New(kind, errors, annotations, subOrFunctionHeader) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(body) Me._body = body @@ -18791,7 +18794,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, subOrFunctionHeader As LambdaHeaderSyntax, statements As GreenNode, endSubOrFunctionStatement As EndBlockStatementSyntax) MyBase.New(kind, subOrFunctionHeader) - MyBase._slotCount = 3 + Me.SlotCount = 3 If statements IsNot Nothing Then AdjustFlagsAndWidth(statements) @@ -18804,7 +18807,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, subOrFunctionHeader As LambdaHeaderSyntax, statements As GreenNode, endSubOrFunctionStatement As EndBlockStatementSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, subOrFunctionHeader) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) If statements IsNot Nothing Then @@ -18818,7 +18821,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), subOrFunctionHeader As LambdaHeaderSyntax, statements As GreenNode, endSubOrFunctionStatement As EndBlockStatementSyntax) MyBase.New(kind, errors, annotations, subOrFunctionHeader) - MyBase._slotCount = 3 + Me.SlotCount = 3 If statements IsNot Nothing Then AdjustFlagsAndWidth(statements) @@ -18897,7 +18900,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(subOrFunctionKeyword) Me._subOrFunctionKeyword = subOrFunctionKeyword @@ -18910,7 +18913,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, attributeLists As GreenNode, modifiers As GreenNode, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, attributeLists, modifiers, parameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(subOrFunctionKeyword) @@ -18924,7 +18927,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), attributeLists As GreenNode, modifiers As GreenNode, subOrFunctionKeyword As InternalSyntax.KeywordSyntax, parameterList As ParameterListSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind, errors, annotations, attributeLists, modifiers, parameterList) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(subOrFunctionKeyword) Me._subOrFunctionKeyword = subOrFunctionKeyword @@ -19006,7 +19009,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, arguments As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -19021,7 +19024,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, arguments As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(openParenToken) @@ -19037,7 +19040,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), openParenToken As InternalSyntax.PunctuationSyntax, arguments As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -19148,7 +19151,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, empty As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(empty) Me._empty = empty @@ -19157,7 +19160,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, empty As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(empty) @@ -19167,7 +19170,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), empty As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(empty) Me._empty = empty @@ -19223,7 +19226,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, nameColonEquals As NameColonEqualsSyntax, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 If nameColonEquals IsNot Nothing Then AdjustFlagsAndWidth(nameColonEquals) @@ -19236,7 +19239,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, nameColonEquals As NameColonEqualsSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) If nameColonEquals IsNot Nothing Then @@ -19250,7 +19253,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), nameColonEquals As NameColonEqualsSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 If nameColonEquals IsNot Nothing Then AdjustFlagsAndWidth(nameColonEquals) @@ -19324,7 +19327,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As IdentifierNameSyntax, colonEqualsToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(name) Me._name = name @@ -19335,7 +19338,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As IdentifierNameSyntax, colonEqualsToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(name) @@ -19347,7 +19350,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), name As IdentifierNameSyntax, colonEqualsToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(name) Me._name = name @@ -19418,7 +19421,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lowerBound As ExpressionSyntax, toKeyword As InternalSyntax.KeywordSyntax, upperBound As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lowerBound) Me._lowerBound = lowerBound @@ -19431,7 +19434,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lowerBound As ExpressionSyntax, toKeyword As InternalSyntax.KeywordSyntax, upperBound As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lowerBound) @@ -19445,7 +19448,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lowerBound As ExpressionSyntax, toKeyword As InternalSyntax.KeywordSyntax, upperBound As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lowerBound) Me._lowerBound = lowerBound @@ -19528,7 +19531,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, clauses As GreenNode) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 If clauses IsNot Nothing Then AdjustFlagsAndWidth(clauses) @@ -19539,7 +19542,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, clauses As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) If clauses IsNot Nothing Then @@ -19551,7 +19554,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), clauses As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 If clauses IsNot Nothing Then AdjustFlagsAndWidth(clauses) @@ -19634,7 +19637,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As ModifiedIdentifierSyntax, asClause As SimpleAsClauseSyntax, inKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -19651,7 +19654,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As ModifiedIdentifierSyntax, asClause As SimpleAsClauseSyntax, inKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(identifier) @@ -19669,7 +19672,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), identifier As ModifiedIdentifierSyntax, asClause As SimpleAsClauseSyntax, inKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -19770,7 +19773,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, nameEquals As VariableNameEqualsSyntax, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 If nameEquals IsNot Nothing Then AdjustFlagsAndWidth(nameEquals) @@ -19783,7 +19786,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, nameEquals As VariableNameEqualsSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) If nameEquals IsNot Nothing Then @@ -19797,7 +19800,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), nameEquals As VariableNameEqualsSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 If nameEquals IsNot Nothing Then AdjustFlagsAndWidth(nameEquals) @@ -19874,7 +19877,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, nameEquals As VariableNameEqualsSyntax, aggregation As AggregationSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 If nameEquals IsNot Nothing Then AdjustFlagsAndWidth(nameEquals) @@ -19887,7 +19890,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, nameEquals As VariableNameEqualsSyntax, aggregation As AggregationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) If nameEquals IsNot Nothing Then @@ -19901,7 +19904,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), nameEquals As VariableNameEqualsSyntax, aggregation As AggregationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 If nameEquals IsNot Nothing Then AdjustFlagsAndWidth(nameEquals) @@ -19979,7 +19982,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As ModifiedIdentifierSyntax, asClause As SimpleAsClauseSyntax, equalsToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -19994,7 +19997,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As ModifiedIdentifierSyntax, asClause As SimpleAsClauseSyntax, equalsToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(identifier) @@ -20010,7 +20013,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), identifier As ModifiedIdentifierSyntax, asClause As SimpleAsClauseSyntax, equalsToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(identifier) Me._identifier = identifier @@ -20123,7 +20126,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, functionName As InternalSyntax.IdentifierTokenSyntax, openParenToken As InternalSyntax.PunctuationSyntax, argument As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(functionName) Me._functionName = functionName @@ -20144,7 +20147,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, functionName As InternalSyntax.IdentifierTokenSyntax, openParenToken As InternalSyntax.PunctuationSyntax, argument As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(functionName) @@ -20166,7 +20169,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), functionName As InternalSyntax.IdentifierTokenSyntax, openParenToken As InternalSyntax.PunctuationSyntax, argument As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(functionName) Me._functionName = functionName @@ -20277,7 +20280,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, groupKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(groupKeyword) Me._groupKeyword = groupKeyword @@ -20286,7 +20289,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, groupKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(groupKeyword) @@ -20296,7 +20299,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), groupKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(groupKeyword) Me._groupKeyword = groupKeyword @@ -20353,7 +20356,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, fromKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(fromKeyword) Me._fromKeyword = fromKeyword @@ -20366,7 +20369,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, fromKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(fromKeyword) @@ -20380,7 +20383,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), fromKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(fromKeyword) Me._fromKeyword = fromKeyword @@ -20451,7 +20454,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, letKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(letKeyword) Me._letKeyword = letKeyword @@ -20464,7 +20467,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, letKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(letKeyword) @@ -20478,7 +20481,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), letKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(letKeyword) Me._letKeyword = letKeyword @@ -20552,7 +20555,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, aggregateKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode, additionalQueryOperators As GreenNode, intoKeyword As InternalSyntax.KeywordSyntax, aggregationVariables As GreenNode) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(aggregateKeyword) Me._aggregateKeyword = aggregateKeyword @@ -20575,7 +20578,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, aggregateKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode, additionalQueryOperators As GreenNode, intoKeyword As InternalSyntax.KeywordSyntax, aggregationVariables As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(aggregateKeyword) @@ -20599,7 +20602,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), aggregateKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode, additionalQueryOperators As GreenNode, intoKeyword As InternalSyntax.KeywordSyntax, aggregationVariables As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(aggregateKeyword) Me._aggregateKeyword = aggregateKeyword @@ -20715,7 +20718,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, distinctKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(distinctKeyword) Me._distinctKeyword = distinctKeyword @@ -20724,7 +20727,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, distinctKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(distinctKeyword) @@ -20734,7 +20737,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), distinctKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(distinctKeyword) Me._distinctKeyword = distinctKeyword @@ -20789,7 +20792,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, whereKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(whereKeyword) Me._whereKeyword = whereKeyword @@ -20800,7 +20803,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, whereKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(whereKeyword) @@ -20812,7 +20815,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), whereKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(whereKeyword) Me._whereKeyword = whereKeyword @@ -20883,7 +20886,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, skipOrTakeKeyword As InternalSyntax.KeywordSyntax, whileKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(skipOrTakeKeyword) Me._skipOrTakeKeyword = skipOrTakeKeyword @@ -20896,7 +20899,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, skipOrTakeKeyword As InternalSyntax.KeywordSyntax, whileKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(skipOrTakeKeyword) @@ -20910,7 +20913,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), skipOrTakeKeyword As InternalSyntax.KeywordSyntax, whileKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(skipOrTakeKeyword) Me._skipOrTakeKeyword = skipOrTakeKeyword @@ -20992,7 +20995,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, skipOrTakeKeyword As InternalSyntax.KeywordSyntax, count As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(skipOrTakeKeyword) Me._skipOrTakeKeyword = skipOrTakeKeyword @@ -21003,7 +21006,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, skipOrTakeKeyword As InternalSyntax.KeywordSyntax, count As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(skipOrTakeKeyword) @@ -21015,7 +21018,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), skipOrTakeKeyword As InternalSyntax.KeywordSyntax, count As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(skipOrTakeKeyword) Me._skipOrTakeKeyword = skipOrTakeKeyword @@ -21088,7 +21091,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, groupKeyword As InternalSyntax.KeywordSyntax, items As GreenNode, byKeyword As InternalSyntax.KeywordSyntax, keys As GreenNode, intoKeyword As InternalSyntax.KeywordSyntax, aggregationVariables As GreenNode) MyBase.New(kind) - MyBase._slotCount = 6 + Me.SlotCount = 6 AdjustFlagsAndWidth(groupKeyword) Me._groupKeyword = groupKeyword @@ -21113,7 +21116,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, groupKeyword As InternalSyntax.KeywordSyntax, items As GreenNode, byKeyword As InternalSyntax.KeywordSyntax, keys As GreenNode, intoKeyword As InternalSyntax.KeywordSyntax, aggregationVariables As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 6 + Me.SlotCount = 6 Me.SetFactoryContext(context) AdjustFlagsAndWidth(groupKeyword) @@ -21139,7 +21142,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), groupKeyword As InternalSyntax.KeywordSyntax, items As GreenNode, byKeyword As InternalSyntax.KeywordSyntax, keys As GreenNode, intoKeyword As InternalSyntax.KeywordSyntax, aggregationVariables As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 6 + Me.SlotCount = 6 AdjustFlagsAndWidth(groupKeyword) Me._groupKeyword = groupKeyword @@ -21398,7 +21401,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, left As ExpressionSyntax, equalsKeyword As InternalSyntax.KeywordSyntax, right As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(left) Me._left = left @@ -21411,7 +21414,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, left As ExpressionSyntax, equalsKeyword As InternalSyntax.KeywordSyntax, right As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(left) @@ -21425,7 +21428,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), left As ExpressionSyntax, equalsKeyword As InternalSyntax.KeywordSyntax, right As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(left) Me._left = left @@ -21505,18 +21508,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, joinKeyword As InternalSyntax.KeywordSyntax, joinedVariables As GreenNode, additionalJoins As GreenNode, onKeyword As InternalSyntax.KeywordSyntax, joinConditions As GreenNode) MyBase.New(kind, joinKeyword, joinedVariables, additionalJoins, onKeyword, joinConditions) - MyBase._slotCount = 5 + Me.SlotCount = 5 End Sub Friend Sub New(ByVal kind As SyntaxKind, joinKeyword As InternalSyntax.KeywordSyntax, joinedVariables As GreenNode, additionalJoins As GreenNode, onKeyword As InternalSyntax.KeywordSyntax, joinConditions As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind, joinKeyword, joinedVariables, additionalJoins, onKeyword, joinConditions) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) End Sub Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), joinKeyword As InternalSyntax.KeywordSyntax, joinedVariables As GreenNode, additionalJoins As GreenNode, onKeyword As InternalSyntax.KeywordSyntax, joinConditions As GreenNode) MyBase.New(kind, errors, annotations, joinKeyword, joinedVariables, additionalJoins, onKeyword, joinConditions) - MyBase._slotCount = 5 + Me.SlotCount = 5 End Sub Friend Overrides Function CreateRed(ByVal parent As SyntaxNode, ByVal startLocation As Integer) As SyntaxNode @@ -21568,7 +21571,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, groupKeyword As InternalSyntax.KeywordSyntax, joinKeyword As InternalSyntax.KeywordSyntax, joinedVariables As GreenNode, additionalJoins As GreenNode, onKeyword As InternalSyntax.KeywordSyntax, joinConditions As GreenNode, intoKeyword As InternalSyntax.KeywordSyntax, aggregationVariables As GreenNode) MyBase.New(kind, joinKeyword, joinedVariables, additionalJoins, onKeyword, joinConditions) - MyBase._slotCount = 8 + Me.SlotCount = 8 AdjustFlagsAndWidth(groupKeyword) Me._groupKeyword = groupKeyword @@ -21583,7 +21586,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, groupKeyword As InternalSyntax.KeywordSyntax, joinKeyword As InternalSyntax.KeywordSyntax, joinedVariables As GreenNode, additionalJoins As GreenNode, onKeyword As InternalSyntax.KeywordSyntax, joinConditions As GreenNode, intoKeyword As InternalSyntax.KeywordSyntax, aggregationVariables As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind, joinKeyword, joinedVariables, additionalJoins, onKeyword, joinConditions) - MyBase._slotCount = 8 + Me.SlotCount = 8 Me.SetFactoryContext(context) AdjustFlagsAndWidth(groupKeyword) @@ -21599,7 +21602,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), groupKeyword As InternalSyntax.KeywordSyntax, joinKeyword As InternalSyntax.KeywordSyntax, joinedVariables As GreenNode, additionalJoins As GreenNode, onKeyword As InternalSyntax.KeywordSyntax, joinConditions As GreenNode, intoKeyword As InternalSyntax.KeywordSyntax, aggregationVariables As GreenNode) MyBase.New(kind, errors, annotations, joinKeyword, joinedVariables, additionalJoins, onKeyword, joinConditions) - MyBase._slotCount = 8 + Me.SlotCount = 8 AdjustFlagsAndWidth(groupKeyword) Me._groupKeyword = groupKeyword @@ -21694,7 +21697,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, orderKeyword As InternalSyntax.KeywordSyntax, byKeyword As InternalSyntax.KeywordSyntax, orderings As GreenNode) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(orderKeyword) Me._orderKeyword = orderKeyword @@ -21709,7 +21712,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, orderKeyword As InternalSyntax.KeywordSyntax, byKeyword As InternalSyntax.KeywordSyntax, orderings As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(orderKeyword) @@ -21725,7 +21728,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), orderKeyword As InternalSyntax.KeywordSyntax, byKeyword As InternalSyntax.KeywordSyntax, orderings As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(orderKeyword) Me._orderKeyword = orderKeyword @@ -21810,7 +21813,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax, ascendingOrDescendingKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(expression) Me._expression = expression @@ -21823,7 +21826,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax, ascendingOrDescendingKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(expression) @@ -21837,7 +21840,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), expression As ExpressionSyntax, ascendingOrDescendingKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(expression) Me._expression = expression @@ -21912,7 +21915,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, selectKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(selectKeyword) Me._selectKeyword = selectKeyword @@ -21925,7 +21928,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, selectKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(selectKeyword) @@ -21939,7 +21942,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), selectKeyword As InternalSyntax.KeywordSyntax, variables As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(selectKeyword) Me._selectKeyword = selectKeyword @@ -22036,7 +22039,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, declaration As XmlDeclarationSyntax, precedingMisc As GreenNode, root As XmlNodeSyntax, followingMisc As GreenNode) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(declaration) Me._declaration = declaration @@ -22055,7 +22058,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, declaration As XmlDeclarationSyntax, precedingMisc As GreenNode, root As XmlNodeSyntax, followingMisc As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(declaration) @@ -22075,7 +22078,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), declaration As XmlDeclarationSyntax, precedingMisc As GreenNode, root As XmlNodeSyntax, followingMisc As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(declaration) Me._declaration = declaration @@ -22172,7 +22175,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanQuestionToken As InternalSyntax.PunctuationSyntax, xmlKeyword As InternalSyntax.KeywordSyntax, version As XmlDeclarationOptionSyntax, encoding As XmlDeclarationOptionSyntax, standalone As XmlDeclarationOptionSyntax, questionGreaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 6 + Me.SlotCount = 6 AdjustFlagsAndWidth(lessThanQuestionToken) Me._lessThanQuestionToken = lessThanQuestionToken @@ -22195,7 +22198,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanQuestionToken As InternalSyntax.PunctuationSyntax, xmlKeyword As InternalSyntax.KeywordSyntax, version As XmlDeclarationOptionSyntax, encoding As XmlDeclarationOptionSyntax, standalone As XmlDeclarationOptionSyntax, questionGreaterThanToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 6 + Me.SlotCount = 6 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lessThanQuestionToken) @@ -22219,7 +22222,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lessThanQuestionToken As InternalSyntax.PunctuationSyntax, xmlKeyword As InternalSyntax.KeywordSyntax, version As XmlDeclarationOptionSyntax, encoding As XmlDeclarationOptionSyntax, standalone As XmlDeclarationOptionSyntax, questionGreaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 6 + Me.SlotCount = 6 AdjustFlagsAndWidth(lessThanQuestionToken) Me._lessThanQuestionToken = lessThanQuestionToken @@ -22334,7 +22337,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As InternalSyntax.XmlNameTokenSyntax, equals As InternalSyntax.PunctuationSyntax, value As XmlStringSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(name) Me._name = name @@ -22347,7 +22350,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As InternalSyntax.XmlNameTokenSyntax, equals As InternalSyntax.PunctuationSyntax, value As XmlStringSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(name) @@ -22361,7 +22364,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), name As InternalSyntax.XmlNameTokenSyntax, equals As InternalSyntax.PunctuationSyntax, value As XmlStringSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(name) Me._name = name @@ -22435,7 +22438,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, startTag As XmlElementStartTagSyntax, content As GreenNode, endTag As XmlElementEndTagSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(startTag) Me._startTag = startTag @@ -22450,7 +22453,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, startTag As XmlElementStartTagSyntax, content As GreenNode, endTag As XmlElementEndTagSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(startTag) @@ -22466,7 +22469,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), startTag As XmlElementStartTagSyntax, content As GreenNode, endTag As XmlElementEndTagSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(startTag) Me._startTag = startTag @@ -22543,7 +22546,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, textTokens As GreenNode) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 If textTokens IsNot Nothing Then AdjustFlagsAndWidth(textTokens) @@ -22554,7 +22557,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, textTokens As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) If textTokens IsNot Nothing Then @@ -22566,7 +22569,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), textTokens As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 If textTokens IsNot Nothing Then AdjustFlagsAndWidth(textTokens) @@ -22626,7 +22629,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanToken As InternalSyntax.PunctuationSyntax, name As XmlNodeSyntax, attributes As GreenNode, greaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(lessThanToken) Me._lessThanToken = lessThanToken @@ -22643,7 +22646,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanToken As InternalSyntax.PunctuationSyntax, name As XmlNodeSyntax, attributes As GreenNode, greaterThanToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lessThanToken) @@ -22661,7 +22664,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lessThanToken As InternalSyntax.PunctuationSyntax, name As XmlNodeSyntax, attributes As GreenNode, greaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(lessThanToken) Me._lessThanToken = lessThanToken @@ -22750,7 +22753,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanSlashToken As InternalSyntax.PunctuationSyntax, name As XmlNameSyntax, greaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanSlashToken) Me._lessThanSlashToken = lessThanSlashToken @@ -22765,7 +22768,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanSlashToken As InternalSyntax.PunctuationSyntax, name As XmlNameSyntax, greaterThanToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lessThanSlashToken) @@ -22781,7 +22784,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lessThanSlashToken As InternalSyntax.PunctuationSyntax, name As XmlNameSyntax, greaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanSlashToken) Me._lessThanSlashToken = lessThanSlashToken @@ -22861,7 +22864,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanToken As InternalSyntax.PunctuationSyntax, name As XmlNodeSyntax, attributes As GreenNode, slashGreaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(lessThanToken) Me._lessThanToken = lessThanToken @@ -22878,7 +22881,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanToken As InternalSyntax.PunctuationSyntax, name As XmlNodeSyntax, attributes As GreenNode, slashGreaterThanToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lessThanToken) @@ -22896,7 +22899,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lessThanToken As InternalSyntax.PunctuationSyntax, name As XmlNodeSyntax, attributes As GreenNode, slashGreaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(lessThanToken) Me._lessThanToken = lessThanToken @@ -22985,7 +22988,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As XmlNodeSyntax, equalsToken As InternalSyntax.PunctuationSyntax, value As XmlNodeSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(name) Me._name = name @@ -22998,7 +23001,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As XmlNodeSyntax, equalsToken As InternalSyntax.PunctuationSyntax, value As XmlNodeSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(name) @@ -23012,7 +23015,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), name As XmlNodeSyntax, equalsToken As InternalSyntax.PunctuationSyntax, value As XmlNodeSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(name) Me._name = name @@ -23109,7 +23112,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, startQuoteToken As InternalSyntax.PunctuationSyntax, textTokens As GreenNode, endQuoteToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(startQuoteToken) Me._startQuoteToken = startQuoteToken @@ -23124,7 +23127,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, startQuoteToken As InternalSyntax.PunctuationSyntax, textTokens As GreenNode, endQuoteToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(startQuoteToken) @@ -23140,7 +23143,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), startQuoteToken As InternalSyntax.PunctuationSyntax, textTokens As GreenNode, endQuoteToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(startQuoteToken) Me._startQuoteToken = startQuoteToken @@ -23217,7 +23220,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As InternalSyntax.XmlNameTokenSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(name) Me._name = name @@ -23226,7 +23229,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As InternalSyntax.XmlNameTokenSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(name) @@ -23236,7 +23239,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), name As InternalSyntax.XmlNameTokenSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(name) Me._name = name @@ -23290,7 +23293,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, prefix As XmlPrefixSyntax, localName As InternalSyntax.XmlNameTokenSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 If prefix IsNot Nothing Then AdjustFlagsAndWidth(prefix) @@ -23303,7 +23306,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, prefix As XmlPrefixSyntax, localName As InternalSyntax.XmlNameTokenSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) If prefix IsNot Nothing Then @@ -23317,7 +23320,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), prefix As XmlPrefixSyntax, localName As InternalSyntax.XmlNameTokenSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 If prefix IsNot Nothing Then AdjustFlagsAndWidth(prefix) @@ -23388,7 +23391,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanToken As InternalSyntax.PunctuationSyntax, name As XmlNameSyntax, greaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanToken) Me._lessThanToken = lessThanToken @@ -23401,7 +23404,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanToken As InternalSyntax.PunctuationSyntax, name As XmlNameSyntax, greaterThanToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lessThanToken) @@ -23415,7 +23418,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lessThanToken As InternalSyntax.PunctuationSyntax, name As XmlNameSyntax, greaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanToken) Me._lessThanToken = lessThanToken @@ -23488,7 +23491,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As InternalSyntax.XmlNameTokenSyntax, colonToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(name) Me._name = name @@ -23499,7 +23502,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As InternalSyntax.XmlNameTokenSyntax, colonToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(name) @@ -23511,7 +23514,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), name As InternalSyntax.XmlNameTokenSyntax, colonToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(name) Me._name = name @@ -23576,7 +23579,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanExclamationMinusMinusToken As InternalSyntax.PunctuationSyntax, textTokens As GreenNode, minusMinusGreaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanExclamationMinusMinusToken) Me._lessThanExclamationMinusMinusToken = lessThanExclamationMinusMinusToken @@ -23591,7 +23594,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanExclamationMinusMinusToken As InternalSyntax.PunctuationSyntax, textTokens As GreenNode, minusMinusGreaterThanToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lessThanExclamationMinusMinusToken) @@ -23607,7 +23610,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lessThanExclamationMinusMinusToken As InternalSyntax.PunctuationSyntax, textTokens As GreenNode, minusMinusGreaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanExclamationMinusMinusToken) Me._lessThanExclamationMinusMinusToken = lessThanExclamationMinusMinusToken @@ -23685,7 +23688,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanQuestionToken As InternalSyntax.PunctuationSyntax, name As InternalSyntax.XmlNameTokenSyntax, textTokens As GreenNode, questionGreaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(lessThanQuestionToken) Me._lessThanQuestionToken = lessThanQuestionToken @@ -23702,7 +23705,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanQuestionToken As InternalSyntax.PunctuationSyntax, name As InternalSyntax.XmlNameTokenSyntax, textTokens As GreenNode, questionGreaterThanToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lessThanQuestionToken) @@ -23720,7 +23723,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lessThanQuestionToken As InternalSyntax.PunctuationSyntax, name As InternalSyntax.XmlNameTokenSyntax, textTokens As GreenNode, questionGreaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(lessThanQuestionToken) Me._lessThanQuestionToken = lessThanQuestionToken @@ -23806,7 +23809,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, beginCDataToken As InternalSyntax.PunctuationSyntax, textTokens As GreenNode, endCDataToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(beginCDataToken) Me._beginCDataToken = beginCDataToken @@ -23821,7 +23824,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, beginCDataToken As InternalSyntax.PunctuationSyntax, textTokens As GreenNode, endCDataToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(beginCDataToken) @@ -23837,7 +23840,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), beginCDataToken As InternalSyntax.PunctuationSyntax, textTokens As GreenNode, endCDataToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(beginCDataToken) Me._beginCDataToken = beginCDataToken @@ -23914,7 +23917,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanPercentEqualsToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, percentGreaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanPercentEqualsToken) Me._lessThanPercentEqualsToken = lessThanPercentEqualsToken @@ -23927,7 +23930,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, lessThanPercentEqualsToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, percentGreaterThanToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(lessThanPercentEqualsToken) @@ -23941,7 +23944,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), lessThanPercentEqualsToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, percentGreaterThanToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(lessThanPercentEqualsToken) Me._lessThanPercentEqualsToken = lessThanPercentEqualsToken @@ -24039,7 +24042,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elementType As TypeSyntax, rankSpecifiers As GreenNode) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elementType) Me._elementType = elementType @@ -24052,7 +24055,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elementType As TypeSyntax, rankSpecifiers As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(elementType) @@ -24066,7 +24069,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), elementType As TypeSyntax, rankSpecifiers As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elementType) Me._elementType = elementType @@ -24137,7 +24140,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elementType As TypeSyntax, questionMarkToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elementType) Me._elementType = elementType @@ -24148,7 +24151,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, elementType As TypeSyntax, questionMarkToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(elementType) @@ -24160,7 +24163,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), elementType As TypeSyntax, questionMarkToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elementType) Me._elementType = elementType @@ -24230,7 +24233,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(keyword) Me._keyword = keyword @@ -24239,7 +24242,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, keyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(keyword) @@ -24249,7 +24252,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), keyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(keyword) Me._keyword = keyword @@ -24370,18 +24373,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax) MyBase.New(kind, identifier) - MyBase._slotCount = 1 + Me.SlotCount = 1 End Sub Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, identifier) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) End Sub Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), identifier As InternalSyntax.IdentifierTokenSyntax) MyBase.New(kind, errors, annotations, identifier) - MyBase._slotCount = 1 + Me.SlotCount = 1 End Sub Friend Overrides Function CreateRed(ByVal parent As SyntaxNode, ByVal startLocation As Integer) As SyntaxNode @@ -24423,7 +24426,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax, typeArgumentList As TypeArgumentListSyntax) MyBase.New(kind, identifier) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(typeArgumentList) Me._typeArgumentList = typeArgumentList @@ -24432,7 +24435,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, identifier As InternalSyntax.IdentifierTokenSyntax, typeArgumentList As TypeArgumentListSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, identifier) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(typeArgumentList) @@ -24442,7 +24445,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), identifier As InternalSyntax.IdentifierTokenSyntax, typeArgumentList As TypeArgumentListSyntax) MyBase.New(kind, errors, annotations, identifier) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(typeArgumentList) Me._typeArgumentList = typeArgumentList @@ -24501,7 +24504,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, left As NameSyntax, dotToken As InternalSyntax.PunctuationSyntax, right As SimpleNameSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(left) Me._left = left @@ -24514,7 +24517,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, left As NameSyntax, dotToken As InternalSyntax.PunctuationSyntax, right As SimpleNameSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(left) @@ -24528,7 +24531,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), left As NameSyntax, dotToken As InternalSyntax.PunctuationSyntax, right As SimpleNameSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(left) Me._left = left @@ -24611,7 +24614,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, globalKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(globalKeyword) Me._globalKeyword = globalKeyword @@ -24620,7 +24623,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, globalKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(globalKeyword) @@ -24630,7 +24633,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), globalKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(globalKeyword) Me._globalKeyword = globalKeyword @@ -24687,7 +24690,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, ofKeyword As InternalSyntax.KeywordSyntax, arguments As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -24704,7 +24707,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, ofKeyword As InternalSyntax.KeywordSyntax, arguments As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(openParenToken) @@ -24722,7 +24725,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), openParenToken As InternalSyntax.PunctuationSyntax, ofKeyword As InternalSyntax.KeywordSyntax, arguments As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -24821,7 +24824,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As TypeSyntax, signature As CrefSignatureSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(name) Me._name = name @@ -24838,7 +24841,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As TypeSyntax, signature As CrefSignatureSyntax, asClause As SimpleAsClauseSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(name) @@ -24856,7 +24859,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), name As TypeSyntax, signature As CrefSignatureSyntax, asClause As SimpleAsClauseSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(name) Me._name = name @@ -24941,7 +24944,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, argumentTypes As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -24956,7 +24959,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openParenToken As InternalSyntax.PunctuationSyntax, argumentTypes As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(openParenToken) @@ -24972,7 +24975,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), openParenToken As InternalSyntax.PunctuationSyntax, argumentTypes As GreenNode, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(openParenToken) Me._openParenToken = openParenToken @@ -25044,7 +25047,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, modifier As InternalSyntax.KeywordSyntax, type As TypeSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 If modifier IsNot Nothing Then AdjustFlagsAndWidth(modifier) @@ -25059,7 +25062,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, modifier As InternalSyntax.KeywordSyntax, type As TypeSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) If modifier IsNot Nothing Then @@ -25075,7 +25078,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), modifier As InternalSyntax.KeywordSyntax, type As TypeSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 If modifier IsNot Nothing Then AdjustFlagsAndWidth(modifier) @@ -25145,7 +25148,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, operatorKeyword As InternalSyntax.KeywordSyntax, operatorToken As InternalSyntax.SyntaxToken) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(operatorKeyword) Me._operatorKeyword = operatorKeyword @@ -25156,7 +25159,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, operatorKeyword As InternalSyntax.KeywordSyntax, operatorToken As InternalSyntax.SyntaxToken, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(operatorKeyword) @@ -25168,7 +25171,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), operatorKeyword As InternalSyntax.KeywordSyntax, operatorToken As InternalSyntax.SyntaxToken) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(operatorKeyword) Me._operatorKeyword = operatorKeyword @@ -25229,7 +25232,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, left As NameSyntax, dotToken As InternalSyntax.PunctuationSyntax, right As CrefOperatorReferenceSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(left) Me._left = left @@ -25242,7 +25245,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, left As NameSyntax, dotToken As InternalSyntax.PunctuationSyntax, right As CrefOperatorReferenceSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(left) @@ -25256,7 +25259,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), left As NameSyntax, dotToken As InternalSyntax.PunctuationSyntax, right As CrefOperatorReferenceSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(left) Me._left = left @@ -25329,7 +25332,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, yieldKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(yieldKeyword) Me._yieldKeyword = yieldKeyword @@ -25340,7 +25343,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, yieldKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(yieldKeyword) @@ -25352,7 +25355,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), yieldKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(yieldKeyword) Me._yieldKeyword = yieldKeyword @@ -25421,7 +25424,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, awaitKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(awaitKeyword) Me._awaitKeyword = awaitKeyword @@ -25432,7 +25435,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, awaitKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(awaitKeyword) @@ -25444,7 +25447,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), awaitKeyword As InternalSyntax.KeywordSyntax, expression As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(awaitKeyword) Me._awaitKeyword = awaitKeyword @@ -25982,7 +25985,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, tokens As GreenNode) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 If tokens IsNot Nothing Then AdjustFlagsAndWidth(tokens) @@ -25993,7 +25996,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, tokens As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) If tokens IsNot Nothing Then @@ -26005,7 +26008,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), tokens As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 If tokens IsNot Nothing Then AdjustFlagsAndWidth(tokens) @@ -26065,7 +26068,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, content As GreenNode) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 If content IsNot Nothing Then AdjustFlagsAndWidth(content) @@ -26076,7 +26079,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, content As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) If content IsNot Nothing Then @@ -26088,7 +26091,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), content As GreenNode) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 If content IsNot Nothing Then AdjustFlagsAndWidth(content) @@ -26150,7 +26153,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As XmlNameSyntax, equalsToken As InternalSyntax.PunctuationSyntax, startQuoteToken As InternalSyntax.PunctuationSyntax, reference As CrefReferenceSyntax, endQuoteToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(name) Me._name = name @@ -26167,7 +26170,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As XmlNameSyntax, equalsToken As InternalSyntax.PunctuationSyntax, startQuoteToken As InternalSyntax.PunctuationSyntax, reference As CrefReferenceSyntax, endQuoteToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(name) @@ -26185,7 +26188,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), name As XmlNameSyntax, equalsToken As InternalSyntax.PunctuationSyntax, startQuoteToken As InternalSyntax.PunctuationSyntax, reference As CrefReferenceSyntax, endQuoteToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(name) Me._name = name @@ -26283,7 +26286,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As XmlNameSyntax, equalsToken As InternalSyntax.PunctuationSyntax, startQuoteToken As InternalSyntax.PunctuationSyntax, reference As IdentifierNameSyntax, endQuoteToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(name) Me._name = name @@ -26300,7 +26303,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, name As XmlNameSyntax, equalsToken As InternalSyntax.PunctuationSyntax, startQuoteToken As InternalSyntax.PunctuationSyntax, reference As IdentifierNameSyntax, endQuoteToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(name) @@ -26318,7 +26321,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), name As XmlNameSyntax, equalsToken As InternalSyntax.PunctuationSyntax, startQuoteToken As InternalSyntax.PunctuationSyntax, reference As IdentifierNameSyntax, endQuoteToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(name) Me._name = name @@ -26412,7 +26415,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax, questionMarkToken As InternalSyntax.PunctuationSyntax, whenNotNull As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 If expression IsNot Nothing Then AdjustFlagsAndWidth(expression) @@ -26427,7 +26430,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, expression As ExpressionSyntax, questionMarkToken As InternalSyntax.PunctuationSyntax, whenNotNull As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) If expression IsNot Nothing Then @@ -26443,7 +26446,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), expression As ExpressionSyntax, questionMarkToken As InternalSyntax.PunctuationSyntax, whenNotNull As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 If expression IsNot Nothing Then AdjustFlagsAndWidth(expression) @@ -26533,7 +26536,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, nameOfKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, argument As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(nameOfKeyword) Me._nameOfKeyword = nameOfKeyword @@ -26548,7 +26551,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, nameOfKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, argument As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(nameOfKeyword) @@ -26564,7 +26567,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), nameOfKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, argument As ExpressionSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(nameOfKeyword) Me._nameOfKeyword = nameOfKeyword @@ -26660,7 +26663,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, dollarSignDoubleQuoteToken As InternalSyntax.PunctuationSyntax, contents As GreenNode, doubleQuoteToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(dollarSignDoubleQuoteToken) Me._dollarSignDoubleQuoteToken = dollarSignDoubleQuoteToken @@ -26675,7 +26678,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, dollarSignDoubleQuoteToken As InternalSyntax.PunctuationSyntax, contents As GreenNode, doubleQuoteToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(dollarSignDoubleQuoteToken) @@ -26691,7 +26694,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), dollarSignDoubleQuoteToken As InternalSyntax.PunctuationSyntax, contents As GreenNode, doubleQuoteToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(dollarSignDoubleQuoteToken) Me._dollarSignDoubleQuoteToken = dollarSignDoubleQuoteToken @@ -26796,7 +26799,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, textToken As InternalSyntax.InterpolatedStringTextTokenSyntax) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(textToken) Me._textToken = textToken @@ -26805,7 +26808,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, textToken As InternalSyntax.InterpolatedStringTextTokenSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) AdjustFlagsAndWidth(textToken) @@ -26815,7 +26818,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), textToken As InternalSyntax.InterpolatedStringTextTokenSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 1 + Me.SlotCount = 1 AdjustFlagsAndWidth(textToken) Me._textToken = textToken @@ -26874,7 +26877,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openBraceToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, alignmentClause As InterpolationAlignmentClauseSyntax, formatClause As InterpolationFormatClauseSyntax, closeBraceToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(openBraceToken) Me._openBraceToken = openBraceToken @@ -26895,7 +26898,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, openBraceToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, alignmentClause As InterpolationAlignmentClauseSyntax, formatClause As InterpolationFormatClauseSyntax, closeBraceToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(openBraceToken) @@ -26917,7 +26920,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), openBraceToken As InternalSyntax.PunctuationSyntax, expression As ExpressionSyntax, alignmentClause As InterpolationAlignmentClauseSyntax, formatClause As InterpolationFormatClauseSyntax, closeBraceToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(openBraceToken) Me._openBraceToken = openBraceToken @@ -27037,7 +27040,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, commaToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(commaToken) Me._commaToken = commaToken @@ -27048,7 +27051,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, commaToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(commaToken) @@ -27060,7 +27063,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), commaToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(commaToken) Me._commaToken = commaToken @@ -27130,7 +27133,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, colonToken As InternalSyntax.PunctuationSyntax, formatStringToken As InternalSyntax.InterpolatedStringTextTokenSyntax) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(colonToken) Me._colonToken = colonToken @@ -27141,7 +27144,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, colonToken As InternalSyntax.PunctuationSyntax, formatStringToken As InternalSyntax.InterpolatedStringTextTokenSyntax, context As ISyntaxFactoryContext) MyBase.New(kind) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(colonToken) @@ -27153,7 +27156,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), colonToken As InternalSyntax.PunctuationSyntax, formatStringToken As InternalSyntax.InterpolatedStringTextTokenSyntax) MyBase.New(kind, errors, annotations) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(colonToken) Me._colonToken = colonToken @@ -27291,7 +27294,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, constKeyword As InternalSyntax.KeywordSyntax, name As InternalSyntax.IdentifierTokenSyntax, equalsToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax) MyBase.New(kind, hashToken) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(constKeyword) Me._constKeyword = constKeyword @@ -27306,7 +27309,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, constKeyword As InternalSyntax.KeywordSyntax, name As InternalSyntax.IdentifierTokenSyntax, equalsToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) AdjustFlagsAndWidth(constKeyword) @@ -27322,7 +27325,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, constKeyword As InternalSyntax.KeywordSyntax, name As InternalSyntax.IdentifierTokenSyntax, equalsToken As InternalSyntax.PunctuationSyntax, value As ExpressionSyntax) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 5 + Me.SlotCount = 5 AdjustFlagsAndWidth(constKeyword) Me._constKeyword = constKeyword @@ -27423,7 +27426,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, elseKeyword As InternalSyntax.KeywordSyntax, ifOrElseIfKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, hashToken) - MyBase._slotCount = 5 + Me.SlotCount = 5 If elseKeyword IsNot Nothing Then AdjustFlagsAndWidth(elseKeyword) @@ -27442,7 +27445,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, elseKeyword As InternalSyntax.KeywordSyntax, ifOrElseIfKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 5 + Me.SlotCount = 5 Me.SetFactoryContext(context) If elseKeyword IsNot Nothing Then @@ -27462,7 +27465,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, elseKeyword As InternalSyntax.KeywordSyntax, ifOrElseIfKeyword As InternalSyntax.KeywordSyntax, condition As ExpressionSyntax, thenKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 5 + Me.SlotCount = 5 If elseKeyword IsNot Nothing Then AdjustFlagsAndWidth(elseKeyword) @@ -27556,7 +27559,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, elseKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, hashToken) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elseKeyword) Me._elseKeyword = elseKeyword @@ -27565,7 +27568,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, elseKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 2 + Me.SlotCount = 2 Me.SetFactoryContext(context) AdjustFlagsAndWidth(elseKeyword) @@ -27575,7 +27578,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, elseKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 2 + Me.SlotCount = 2 AdjustFlagsAndWidth(elseKeyword) Me._elseKeyword = elseKeyword @@ -27630,7 +27633,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, endKeyword As InternalSyntax.KeywordSyntax, ifKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(endKeyword) Me._endKeyword = endKeyword @@ -27641,7 +27644,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, endKeyword As InternalSyntax.KeywordSyntax, ifKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(endKeyword) @@ -27653,7 +27656,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, endKeyword As InternalSyntax.KeywordSyntax, ifKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(endKeyword) Me._endKeyword = endKeyword @@ -27718,7 +27721,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, regionKeyword As InternalSyntax.KeywordSyntax, name As InternalSyntax.StringLiteralTokenSyntax) MyBase.New(kind, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(regionKeyword) Me._regionKeyword = regionKeyword @@ -27729,7 +27732,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, regionKeyword As InternalSyntax.KeywordSyntax, name As InternalSyntax.StringLiteralTokenSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(regionKeyword) @@ -27741,7 +27744,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, regionKeyword As InternalSyntax.KeywordSyntax, name As InternalSyntax.StringLiteralTokenSyntax) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(regionKeyword) Me._regionKeyword = regionKeyword @@ -27812,7 +27815,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, endKeyword As InternalSyntax.KeywordSyntax, regionKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(endKeyword) Me._endKeyword = endKeyword @@ -27823,7 +27826,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, endKeyword As InternalSyntax.KeywordSyntax, regionKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(endKeyword) @@ -27835,7 +27838,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, endKeyword As InternalSyntax.KeywordSyntax, regionKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(endKeyword) Me._endKeyword = endKeyword @@ -27911,7 +27914,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, externalSourceKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, externalSource As InternalSyntax.StringLiteralTokenSyntax, commaToken As InternalSyntax.PunctuationSyntax, lineStart As InternalSyntax.IntegerLiteralTokenSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, hashToken) - MyBase._slotCount = 7 + Me.SlotCount = 7 AdjustFlagsAndWidth(externalSourceKeyword) Me._externalSourceKeyword = externalSourceKeyword @@ -27930,7 +27933,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, externalSourceKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, externalSource As InternalSyntax.StringLiteralTokenSyntax, commaToken As InternalSyntax.PunctuationSyntax, lineStart As InternalSyntax.IntegerLiteralTokenSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 7 + Me.SlotCount = 7 Me.SetFactoryContext(context) AdjustFlagsAndWidth(externalSourceKeyword) @@ -27950,7 +27953,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, externalSourceKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, externalSource As InternalSyntax.StringLiteralTokenSyntax, commaToken As InternalSyntax.PunctuationSyntax, lineStart As InternalSyntax.IntegerLiteralTokenSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 7 + Me.SlotCount = 7 AdjustFlagsAndWidth(externalSourceKeyword) Me._externalSourceKeyword = externalSourceKeyword @@ -28055,7 +28058,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, endKeyword As InternalSyntax.KeywordSyntax, externalSourceKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(endKeyword) Me._endKeyword = endKeyword @@ -28066,7 +28069,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, endKeyword As InternalSyntax.KeywordSyntax, externalSourceKeyword As InternalSyntax.KeywordSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(endKeyword) @@ -28078,7 +28081,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, endKeyword As InternalSyntax.KeywordSyntax, externalSourceKeyword As InternalSyntax.KeywordSyntax) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(endKeyword) Me._endKeyword = endKeyword @@ -28149,7 +28152,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, externalChecksumKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, externalSource As InternalSyntax.StringLiteralTokenSyntax, firstCommaToken As InternalSyntax.PunctuationSyntax, guid As InternalSyntax.StringLiteralTokenSyntax, secondCommaToken As InternalSyntax.PunctuationSyntax, checksum As InternalSyntax.StringLiteralTokenSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, hashToken) - MyBase._slotCount = 9 + Me.SlotCount = 9 AdjustFlagsAndWidth(externalChecksumKeyword) Me._externalChecksumKeyword = externalChecksumKeyword @@ -28172,7 +28175,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, externalChecksumKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, externalSource As InternalSyntax.StringLiteralTokenSyntax, firstCommaToken As InternalSyntax.PunctuationSyntax, guid As InternalSyntax.StringLiteralTokenSyntax, secondCommaToken As InternalSyntax.PunctuationSyntax, checksum As InternalSyntax.StringLiteralTokenSyntax, closeParenToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 9 + Me.SlotCount = 9 Me.SetFactoryContext(context) AdjustFlagsAndWidth(externalChecksumKeyword) @@ -28196,7 +28199,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, externalChecksumKeyword As InternalSyntax.KeywordSyntax, openParenToken As InternalSyntax.PunctuationSyntax, externalSource As InternalSyntax.StringLiteralTokenSyntax, firstCommaToken As InternalSyntax.PunctuationSyntax, guid As InternalSyntax.StringLiteralTokenSyntax, secondCommaToken As InternalSyntax.PunctuationSyntax, checksum As InternalSyntax.StringLiteralTokenSyntax, closeParenToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 9 + Me.SlotCount = 9 AdjustFlagsAndWidth(externalChecksumKeyword) Me._externalChecksumKeyword = externalChecksumKeyword @@ -28322,7 +28325,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, enableKeyword As InternalSyntax.KeywordSyntax, warningKeyword As InternalSyntax.KeywordSyntax, errorCodes As GreenNode) MyBase.New(kind, hashToken) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(enableKeyword) Me._enableKeyword = enableKeyword @@ -28337,7 +28340,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, enableKeyword As InternalSyntax.KeywordSyntax, warningKeyword As InternalSyntax.KeywordSyntax, errorCodes As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(enableKeyword) @@ -28353,7 +28356,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, enableKeyword As InternalSyntax.KeywordSyntax, warningKeyword As InternalSyntax.KeywordSyntax, errorCodes As GreenNode) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(enableKeyword) Me._enableKeyword = enableKeyword @@ -28431,7 +28434,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, disableKeyword As InternalSyntax.KeywordSyntax, warningKeyword As InternalSyntax.KeywordSyntax, errorCodes As GreenNode) MyBase.New(kind, hashToken) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(disableKeyword) Me._disableKeyword = disableKeyword @@ -28446,7 +28449,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, disableKeyword As InternalSyntax.KeywordSyntax, warningKeyword As InternalSyntax.KeywordSyntax, errorCodes As GreenNode, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 4 + Me.SlotCount = 4 Me.SetFactoryContext(context) AdjustFlagsAndWidth(disableKeyword) @@ -28462,7 +28465,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, disableKeyword As InternalSyntax.KeywordSyntax, warningKeyword As InternalSyntax.KeywordSyntax, errorCodes As GreenNode) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 4 + Me.SlotCount = 4 AdjustFlagsAndWidth(disableKeyword) Me._disableKeyword = disableKeyword @@ -28539,7 +28542,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, referenceKeyword As InternalSyntax.KeywordSyntax, file As InternalSyntax.StringLiteralTokenSyntax) MyBase.New(kind, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(referenceKeyword) Me._referenceKeyword = referenceKeyword @@ -28550,7 +28553,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, referenceKeyword As InternalSyntax.KeywordSyntax, file As InternalSyntax.StringLiteralTokenSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 Me.SetFactoryContext(context) AdjustFlagsAndWidth(referenceKeyword) @@ -28562,7 +28565,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax, referenceKeyword As InternalSyntax.KeywordSyntax, file As InternalSyntax.StringLiteralTokenSyntax) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 3 + Me.SlotCount = 3 AdjustFlagsAndWidth(referenceKeyword) Me._referenceKeyword = referenceKeyword @@ -28628,18 +28631,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, hashToken) - MyBase._slotCount = 1 + Me.SlotCount = 1 End Sub Friend Sub New(ByVal kind As SyntaxKind, hashToken As InternalSyntax.PunctuationSyntax, context As ISyntaxFactoryContext) MyBase.New(kind, hashToken) - MyBase._slotCount = 1 + Me.SlotCount = 1 Me.SetFactoryContext(context) End Sub Friend Sub New(ByVal kind As SyntaxKind, ByVal errors as DiagnosticInfo(), ByVal annotations as SyntaxAnnotation(), hashToken As InternalSyntax.PunctuationSyntax) MyBase.New(kind, errors, annotations, hashToken) - MyBase._slotCount = 1 + Me.SlotCount = 1 End Sub Friend Overrides Function CreateRed(ByVal parent As SyntaxNode, ByVal startLocation As Integer) As SyntaxNode @@ -37511,19 +37514,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax ''' Friend Shared Function Attribute(target As AttributeTargetSyntax, name As TypeSyntax, argumentList As ArgumentListSyntax) As AttributeSyntax Debug.Assert(name IsNot Nothing) - - Dim hash As Integer - Dim cached = SyntaxNodeCache.TryGetNode(SyntaxKind.Attribute, target, name, argumentList, hash) - If cached IsNot Nothing Then - Return DirectCast(cached, AttributeSyntax) - End If - - Dim result = New AttributeSyntax(SyntaxKind.Attribute, target, name, argumentList) - If hash >= 0 Then - SyntaxNodeCache.AddNode(result, hash) - End If - - Return result + Return New AttributeSyntax(SyntaxKind.Attribute, target, name, argumentList) End Function @@ -49589,19 +49580,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax ''' Friend Function Attribute(target As AttributeTargetSyntax, name As TypeSyntax, argumentList As ArgumentListSyntax) As AttributeSyntax Debug.Assert(name IsNot Nothing) - - Dim hash As Integer - Dim cached = VisualBasicSyntaxNodeCache.TryGetNode(SyntaxKind.Attribute, target, name, argumentList, _factoryContext, hash) - If cached IsNot Nothing Then - Return DirectCast(cached, AttributeSyntax) - End If - - Dim result = New AttributeSyntax(SyntaxKind.Attribute, target, name, argumentList, _factoryContext) - If hash >= 0 Then - SyntaxNodeCache.AddNode(result, hash) - End If - - Return result + Return New AttributeSyntax(SyntaxKind.Attribute, target, name, argumentList, _factoryContext) End Function diff --git a/src/Compilers/VisualBasic/Portable/SourceGeneration/VisualBasicSyntaxHelper.vb b/src/Compilers/VisualBasic/Portable/SourceGeneration/VisualBasicSyntaxHelper.vb index 6d3572dbb55f0..80f5312759192 100644 --- a/src/Compilers/VisualBasic/Portable/SourceGeneration/VisualBasicSyntaxHelper.vb +++ b/src/Compilers/VisualBasic/Portable/SourceGeneration/VisualBasicSyntaxHelper.vb @@ -2,11 +2,7 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. -Imports System.Runtime.CompilerServices -Imports System.Runtime.InteropServices -Imports System.Threading Imports Microsoft.CodeAnalysis.PooledObjects -Imports Microsoft.CodeAnalysis.SourceGeneration Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic @@ -20,8 +16,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Public Overrides ReadOnly Property IsCaseSensitive As Boolean = False - Protected Overrides ReadOnly Property AttributeListKind As Integer = SyntaxKind.AttributeList - Public Overrides Function IsValidIdentifier(name As String) As Boolean Return SyntaxFacts.IsValidIdentifier(name) End Function diff --git a/src/Compilers/VisualBasic/Portable/Symbols/TypeSymbolExtensions.vb b/src/Compilers/VisualBasic/Portable/Symbols/TypeSymbolExtensions.vb index 12fd14aad8096..8dbdcb3e68379 100644 --- a/src/Compilers/VisualBasic/Portable/Symbols/TypeSymbolExtensions.vb +++ b/src/Compilers/VisualBasic/Portable/Symbols/TypeSymbolExtensions.vb @@ -1297,6 +1297,17 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols Return typeSymbol.IsWellKnownCompilerServicesTopLevelType("IsExternalInit") End Function + ' Keep in sync with C# equivalent. + + Friend Function IsWellKnownTypeLock(typeSymbol As TypeSymbol) As Boolean + Dim namedTypeSymbol = TryCast(typeSymbol, NamedTypeSymbol) + Return namedTypeSymbol IsNot Nothing AndAlso + namedTypeSymbol.Name = WellKnownMemberNames.LockTypeName AndAlso + namedTypeSymbol.Arity = 0 AndAlso + namedTypeSymbol.ContainingType Is Nothing AndAlso + namedTypeSymbol.IsContainedInNamespace(NameOf(System), NameOf(System.Threading)) + End Function + Private Function IsWellKnownCompilerServicesTopLevelType(typeSymbol As TypeSymbol, name As String) As Boolean If Not String.Equals(typeSymbol.Name, name) Then @@ -1312,13 +1323,19 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols End Function - Private Function IsContainedInNamespace(typeSymbol As TypeSymbol, outerNS As String, midNS As String, innerNS As String) As Boolean - Dim innerNamespace = typeSymbol.ContainingNamespace - If Not String.Equals(innerNamespace?.Name, innerNS) Then - Return False + Private Function IsContainedInNamespace(typeSymbol As TypeSymbol, outerNS As String, midNS As String, Optional innerNS As String = Nothing) As Boolean + Dim midNamespace As NamespaceSymbol + + If innerNS IsNot Nothing Then + Dim innerNamespace = typeSymbol.ContainingNamespace + If Not String.Equals(innerNamespace?.Name, innerNS) Then + Return False + End If + midNamespace = innerNamespace.ContainingNamespace + Else + midNamespace = typeSymbol.ContainingNamespace End If - Dim midNamespace = innerNamespace.ContainingNamespace If Not String.Equals(midNamespace?.Name, midNS) Then Return False End If diff --git a/src/Compilers/VisualBasic/Portable/Syntax/InternalSyntax/SyntaxNode.vb b/src/Compilers/VisualBasic/Portable/Syntax/InternalSyntax/SyntaxNode.vb index 33c5b973bb165..68be901611ea3 100644 --- a/src/Compilers/VisualBasic/Portable/Syntax/InternalSyntax/SyntaxNode.vb +++ b/src/Compilers/VisualBasic/Portable/Syntax/InternalSyntax/SyntaxNode.vb @@ -83,20 +83,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax End Get End Property - Protected Overrides Function GetSlotCount() As Integer - Throw ExceptionUtilities.Unreachable - End Function - - Protected Property _slotCount As Integer - Get - Return Me.SlotCount - End Get - - Set(value As Integer) - Me.SlotCount = value - End Set - End Property - Friend Function GetFirstToken() As SyntaxToken Return DirectCast(Me.GetFirstTerminal(), SyntaxToken) End Function diff --git a/src/Compilers/VisualBasic/Portable/VBResources.resx b/src/Compilers/VisualBasic/Portable/VBResources.resx index 62fb8733c2c40..f2b931852c9dd 100644 --- a/src/Compilers/VisualBasic/Portable/VBResources.resx +++ b/src/Compilers/VisualBasic/Portable/VBResources.resx @@ -5695,4 +5695,13 @@ The diagnosticId argument to the 'Experimental' attribute must be a valid identifier + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.cs.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.cs.xlf index 14ac130b36227..dde635cf5a37c 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.cs.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.cs.xlf @@ -47,6 +47,11 @@ Argument diagnosticId atributu Experimental musí být platný identifikátor + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). Ve stejném adresáři nemůže být více konfiguračních souborů analyzátoru ({0}). @@ -608,6 +613,16 @@ CallerArgumentExpressionAttribute použitý u parametru nebude mít žádný vliv, protože odkazuje sám na sebe. + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times Odkaz analyzátoru {0} byl zadán vícekrát diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.de.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.de.xlf index 8d8ca797a6908..92bee2c30c622 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.de.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.de.xlf @@ -47,6 +47,11 @@ Das diagnosticId-Argument für das Experimental-Attribut muss ein gültiger Bezeichner sein. + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). Dasselbe Verzeichnis ({0}) darf nicht mehrere Konfigurationsdateien des Analysetools enthalten. @@ -608,6 +613,16 @@ Das auf den Parameter angewendete CallerArgumentExpressionAttribute hat keine Auswirkungen, da es selbstreferenziell ist. + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times Mehrfacher Verweis auf Analysetool "{0}" diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.es.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.es.xlf index 344213f27b1a6..4533a1ed53233 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.es.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.es.xlf @@ -47,6 +47,11 @@ El argumento diagnosticId del atributo "Experimental" debe ser un identificador válido. + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). No es posible que un mismo directorio ("{0}") contenga varios archivos de configuración del analizador. @@ -608,6 +613,16 @@ El atributo CallerArgumentExpressionAttribute aplicado al parámetro no tendrá ningún efecto porque es autorreferencial. + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times Referencia del analizador "{0}" especificada varias veces diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.fr.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.fr.xlf index ca0b81844f1dd..fcac41bae73fe 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.fr.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.fr.xlf @@ -47,6 +47,11 @@ L’argument diagnosticId de l’attribut « Experimental » doit être un identificateur valide + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). Plusieurs fichiers config d'analyseur ne peuvent pas figurer dans le même répertoire ('{0}'). @@ -608,6 +613,16 @@ Le CallerArgumentExpressionAttribute appliqué au paramètre n’aura aucun effet, car il est auto-référentiel. + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times Référence de l’analyseur '{0}' spécifiée plusieurs fois diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.it.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.it.xlf index 23337ccb8d548..b7a706ad51d69 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.it.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.it.xlf @@ -47,6 +47,11 @@ L'argomento diagnosticId dell'attributo 'Experimental' deve essere un identificatore valido + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). La stessa directory ('{0}') non può contenere più file di configurazione dell'analizzatore. @@ -609,6 +614,16 @@ CallerArgumentExpressionAttribute applicato al parametro non avrà alcun effetto perché è autoreferenziale. + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times Il riferimento '{0}' dell'analizzatore è stato specificato più volte diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ja.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ja.xlf index d5842e214f829..3db9ff53067e3 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ja.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ja.xlf @@ -47,6 +47,11 @@ 'Experimental' 属性への diagnosticId 引数は有効な識別子である必要があります + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). 複数のアナライザー構成ファイルを同じディレクトリに入れることはできません ('{0}')。 @@ -610,6 +615,16 @@ パラメーターに適用された CallerArgumentExpressionAttribute は自己参照であるため、無効となります。 + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times 複数回指定されたアナライザー参照 '{0}' diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ko.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ko.xlf index 7d578543ddcd9..50b92bd5a2033 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ko.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ko.xlf @@ -47,6 +47,11 @@ 'Experimental' 특성에 대한 diagnosticId 인수는 유효한 식별자여야 합니다. + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). 분석기 구성 파일 여러 개가 동일한 디렉터리('{0}')에 있을 수 없습니다. @@ -608,6 +613,16 @@ 매개 변수에 적용된 CallerArgumentExpressionAttribute는 자체 참조이기 때문에 효과가 없습니다. + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times 분석기 참조 '{0}'이(가) 여러 번 지정되었습니다. diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.pl.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.pl.xlf index fd7c3102370c0..71119e9928aaa 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.pl.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.pl.xlf @@ -47,6 +47,11 @@ Argument diagnosticId atrybutu „Experimental” musi być prawidłowym identyfikatorem + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). Wiele plików konfiguracji analizatora nie może znajdować się w tym samym katalogu („{0}”). @@ -608,6 +613,16 @@ Atrybut CallerArgumentExpressionAttribute zastosowany do parametru nie będzie działać, ponieważ jest to odwołanie samodzielne. + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times Odwołanie analizatora „{0}” określono wiele razy diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.pt-BR.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.pt-BR.xlf index 46759bf7c9895..fdde610723f8e 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.pt-BR.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.pt-BR.xlf @@ -47,6 +47,11 @@ O argumento diagnosticId para o atributo 'Experimental' deve ser um identificador válido + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). Não é possível que haja vários arquivos de configuração do analisador no mesmo diretório ('{0}'). @@ -608,6 +613,16 @@ O CallerArgumentExpressionAttribute aplicado ao parâmetro não terá efeito porque é de autorreferência. + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times Referência do analisador '{0}' especificada várias vezes diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ru.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ru.xlf index 5939a32a33d5d..e8db68f9da02c 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ru.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ru.xlf @@ -47,6 +47,11 @@ Аргумент diagnosticId атрибута "Experimental" должен быть допустимым идентификатором + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). В одном каталоге ("{0}") не может находиться несколько файлов конфигурации анализатора. @@ -608,6 +613,16 @@ optionstrict[+|-] Принудительное применени Применение класса CallerArgumentExpressionAttribute к параметру не подействует, так как он ссылается сам на себя. + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times Ссылка анализатора "{0}" указана несколько раз diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.tr.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.tr.xlf index 2697ba04404ef..15b539366c895 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.tr.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.tr.xlf @@ -47,6 +47,11 @@ 'Experimental' özniteliğinin diagnosticId bağımsız değişkeni geçerli bir tanımlayıcı olmalıdır + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). Birden çok çözümleyici yapılandırma dosyası aynı dizinde ('{0}') olamaz. @@ -609,6 +614,16 @@ Parametre kendini işaret ettiğinden, parametreye uygulanan CallerArgumentExpressionAttribute hiçbir etkiye sahip olmaz. + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times Çözümleyici referansı '{0}' birden çok kez belirtildi diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hans.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hans.xlf index 61e5014d87332..35aa90cfe0f0b 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hans.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hans.xlf @@ -47,6 +47,11 @@ “Experimental” 属性的 diagnosticId 参数必须是有效的标识符 + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). 多个分析器配置文件不能位于同一目录({0})中。 @@ -608,6 +613,16 @@ 应用于参数的 CallerArgumentExpressionAttribute 将不起任何作用,因为它是自引用的。 + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times 已多次指定分析器引用“{0}” diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hant.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hant.xlf index d807f96ff7314..2ea1daf390895 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hant.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hant.xlf @@ -47,6 +47,11 @@ 'Experimental' 屬性的 diagnosticId 引數必須是有效的識別碼 + + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + + Multiple analyzer config files cannot be in the same directory ('{0}'). 多個分析器組態檔無法處於相同目錄 ('{0}') 中。 @@ -609,6 +614,16 @@ 套用到參數的 CallerArgumentExpressionAttribute 將沒有效果,因為它是自我參考。 + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + + + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + + Analyzer reference '{0}' specified multiple times 已指定多次分析器參考 '{0}' diff --git a/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb b/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb index 29a54e8782665..6a03a96a948a7 100644 --- a/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb +++ b/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb @@ -10535,7 +10535,7 @@ End Class" Dim generator = New SingleFileTestGenerator(generatedSource, "generatedSource.vb") VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference:=False, generators:={generator}) - Dim generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator) + Dim generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(baseDirectory:=generatedDir.Path, generator) ValidateWrittenSources(New Dictionary(Of String, Dictionary(Of String, String))() From {{generatedDir.Path, New Dictionary(Of String, String)()}} ) @@ -10582,7 +10582,7 @@ End Class" Dim generator = New SingleFileTestGenerator(generatedSource, "generatedSource.vb") VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference:=False, additionalFlags:={"/generatedfilesout:" + generatedDir.Path}, generators:={generator}) - Dim generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator) + Dim generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(baseDirectory:=generatedDir.Path, generator) ValidateWrittenSources(New Dictionary(Of String, Dictionary(Of String, String))() From {{Path.Combine(generatedDir.Path, generatorPrefix), New Dictionary(Of String, String)() From {{"generatedSource.vb", generatedSource}} diff --git a/src/Compilers/VisualBasic/Test/IOperation/IOperation/IOperationTests_ILockStatement.vb b/src/Compilers/VisualBasic/Test/IOperation/IOperation/IOperationTests_ILockStatement.vb index c3451ce1593b6..3c9a999f1f902 100644 --- a/src/Compilers/VisualBasic/Test/IOperation/IOperation/IOperationTests_ILockStatement.vb +++ b/src/Compilers/VisualBasic/Test/IOperation/IOperation/IOperationTests_ILockStatement.vb @@ -9,6 +9,20 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.UnitTests.Semantics Partial Public Class IOperationTests Inherits SemanticModelTestBase + Private Const LockTypeDefinition As String = " +namespace System.Threading +{ + public class Lock + { + public Scope EnterScope() => new Scope(); + + public ref struct Scope + { + public void Dispose() { } + } + } +}" + Public Sub ILockStatement_ObjectLock_FieldReference() @@ -362,6 +376,86 @@ ILockOperation (OperationKind.Lock, Type: null) (Syntax: 'SyncLock o' ... nd Syn VerifyOperationTreeAndDiagnosticsForTest(Of SyncLockBlockSyntax)(source, expectedOperationTree, expectedDiagnostics) End Sub + + Public Sub ILockStatement_LockObject() + Dim source = .Value + + Dim expectedOperationTree = .Value + + Dim expectedDiagnostics = .Value + + VerifyOperationTreeAndDiagnosticsForTest(Of SyncLockBlockSyntax)(source, expectedOperationTree, expectedDiagnostics) + End Sub + + + Public Sub ILockStatement_LockObjectWithAllMembers() + Dim lockRef = CreateCSharpCompilation(LockTypeDefinition).VerifyDiagnostics().EmitToImageReference() + + Dim source = .Value + + Dim expectedOperationTree = .Value + + Dim expectedDiagnostics = .Value + + VerifyOperationTreeAndDiagnosticsForTest(Of SyncLockBlockSyntax)(source, expectedOperationTree, expectedDiagnostics, references:={lockRef}) + End Sub + Public Sub LockFlow_04() @@ -469,5 +563,235 @@ Block[B6] - Exit VerifyFlowGraphAndDiagnosticsForTest(Of MethodBlockSyntax)(source, expectedFlowGraph, expectedDiagnostics) End Sub + + Public Sub LockFlow_LockObject() + Dim source = .Value + + Dim expectedDiagnostics = .Value + + Dim expectedFlowGraph = .Value + + VerifyFlowGraphAndDiagnosticsForTest(Of MethodBlockSyntax)(source, expectedFlowGraph, expectedDiagnostics) + End Sub + + + Public Sub LockFlow_LockObject_NonEmptyBody() + Dim source = .Value + + Dim expectedDiagnostics = .Value + + Dim expectedFlowGraph = .Value + + VerifyFlowGraphAndDiagnosticsForTest(Of MethodBlockSyntax)(source, expectedFlowGraph, expectedDiagnostics) + End Sub + + + Public Sub LockFlow_LockObject_ConditionalBody() + Dim source = .Value + + Dim expectedDiagnostics = .Value + + Dim expectedFlowGraph = .Value + + VerifyFlowGraphAndDiagnosticsForTest(Of MethodBlockSyntax)(source, expectedFlowGraph, expectedDiagnostics) + End Sub + + + Public Sub LockFlow_LockObjectWithAllMembers() + Dim lockRef = CreateCSharpCompilation(LockTypeDefinition).VerifyDiagnostics().EmitToImageReference() + + Dim source = .Value + + Dim expectedDiagnostics = .Value + + Dim expectedFlowGraph = .Value + + VerifyFlowGraphAndDiagnosticsForTest(Of MethodBlockSyntax)(source, expectedFlowGraph, expectedDiagnostics, additionalReferences:={lockRef}) + End Sub + End Class End Namespace diff --git a/src/Compilers/VisualBasic/Test/Semantic/Binding/SyncLockTests.vb b/src/Compilers/VisualBasic/Test/Semantic/Binding/SyncLockTests.vb index 1923333cb4185..98f71f7808904 100644 --- a/src/Compilers/VisualBasic/Test/Semantic/Binding/SyncLockTests.vb +++ b/src/Compilers/VisualBasic/Test/Semantic/Binding/SyncLockTests.vb @@ -2,6 +2,7 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. +Imports Microsoft.CodeAnalysis.Test.Utilities Imports Roslyn.Test.Utilities Namespace Microsoft.CodeAnalysis.VisualBasic.UnitTests @@ -473,5 +474,515 @@ End Module ).VerifyDiagnostics( Diagnostic(ERRID.ERR_CaseElseNoSelect, "Case Else")) End Sub + + + Public Sub LockType_InSyncLock() + Dim source = " +Module Program + Sub Main() + Dim l = New System.Threading.Lock() + SyncLock l + End SyncLock + End Sub +End Module + +Namespace System.Threading + Public Class Lock + End Class +End Namespace +" + CreateCompilation(source).AssertTheseDiagnostics( +"BC37329: A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + SyncLock l + ~ +") + End Sub + + + Public Sub LockType_Generic() + Dim source = " +Module Program + Sub Main() + Dim l = New System.Threading.Lock(Of String)() + SyncLock l + End SyncLock + End Sub +End Module + +Namespace System.Threading + Public Class Lock(Of T) + End Class +End Namespace +" + CreateCompilation(source).AssertTheseDiagnostics() + End Sub + + + Public Sub LockType_Nested() + Dim source = " +Module Program + Sub Main() + Dim l = New System.Threading.Container.Lock() + SyncLock l + End SyncLock + End Sub +End Module + +Namespace System.Threading + Public Class Container + Public Class Lock + End Class + End Class +End Namespace +" + CreateCompilation(source).AssertTheseDiagnostics() + End Sub + + + Public Sub LockType_WrongNamespace() + Dim source = " +Module Program + Sub Main() + Dim l = New Threading.Lock() + SyncLock l + End SyncLock + End Sub +End Module + +Namespace Threading + Public Class Lock + End Class +End Namespace +" + CreateCompilation(source).AssertTheseDiagnostics() + End Sub + + + Public Sub LockType_WrongTypeName() + Dim source = " +Module Program + Sub Main() + Dim l = New System.Threading.Lock1() + SyncLock l + End SyncLock + End Sub +End Module + +Namespace System.Threading + Public Class Lock1 + End Class +End Namespace +" + CreateCompilation(source).AssertTheseDiagnostics() + End Sub + + + Public Sub LockType_LowercaseTypeName( + usage As String, + declaration As String) + Dim source = $" +Module Program + Sub Main() + Dim l = New System.Threading.{usage}() + SyncLock l + End SyncLock + End Sub +End Module + +Namespace System.Threading + Public Class {declaration} + End Class +End Namespace +" + Dim comp = CreateCompilation(source) + If declaration = "Lock" Then + comp.AssertTheseDiagnostics( +"BC37329: A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + SyncLock l + ~ +") + Else + Assert.Equal("lock", declaration) + comp.AssertTheseDiagnostics() + End If + End Sub + + + Public Sub LockType_CastToObject() + Dim source = " +Imports System.Threading + +Module Program + Sub Main() + Dim l = New Lock() + Dim o As Object = l + + o = DirectCast(l, Object) + SyncLock DirectCast(l, Object) + End SyncLock + + o = CType(l, Object) + SyncLock CType(l, Object) + End SyncLock + + o = TryCast(l, Object) + SyncLock TryCast(l, Object) + End SyncLock + + o = M1(l) + SyncLock M1(l) + End SyncLock + + o = M2(l) + SyncLock M2(l) + End SyncLock + + o = M3(l) + SyncLock M3(l) + End SyncLock + End Sub + + Function M1(Of T)(o as T) As Object + Return o + End Function + + Function M2(Of T As Class)(o as T) As Object + Return o + End Function + + Function M3(Of T As Lock)(o as T) As Object + Return o + End Function +End Module + +Namespace System.Threading + Public Class Lock + End Class +End Namespace +" + CreateCompilation(source).AssertTheseDiagnostics( +"BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + Dim o As Object = l + ~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + o = DirectCast(l, Object) + ~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + SyncLock DirectCast(l, Object) + ~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + o = CType(l, Object) + ~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + SyncLock CType(l, Object) + ~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + o = TryCast(l, Object) + ~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + SyncLock TryCast(l, Object) + ~ +") + End Sub + + + Public Sub LockType_CastToBase() + Dim source = " +Imports System.Threading + +Module Program + Sub Main() + Dim l = New Lock() + Dim o As LockBase = l + SyncLock o + End SyncLock + End Sub +End Module + +Namespace System.Threading + Public Class LockBase + End Class + + Public Class Lock + Inherits LockBase + End Class +End Namespace +" + CreateCompilation(source).AssertTheseDiagnostics( +"BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + Dim o As LockBase = l + ~ +") + End Sub + + + Public Sub LockType_CastToInterface() + Dim source = " +Imports System.Threading + +Module Program + Sub Main() + Dim l = New Lock() + Dim o As ILockBase = l + SyncLock o + End SyncLock + End Sub +End Module + +Namespace System.Threading + Public Interface ILockBase + End Interface + + Public Class Lock + Implements ILockBase + End Class +End Namespace +" + CreateCompilation(source).AssertTheseDiagnostics( +"BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + Dim o As ILockBase = l + ~ +") + End Sub + + + Public Sub LockType_CastToSelf() + Dim source = " +Imports System.Threading + +Module Program + Sub Main() + Dim l = New Lock() + Dim o As Lock = l + + o = DirectCast(l, Lock) + SyncLock DirectCast(l, Lock) + End SyncLock + + o = CType(l, Lock) + SyncLock CType(l, Lock) + End SyncLock + + o = TryCast(l, Lock) + SyncLock TryCast(l, Lock) + End SyncLock + + o = M1(l) + SyncLock M1(l) + End SyncLock + + o = M2(l) + SyncLock M2(l) + End SyncLock + + o = M3(l) + SyncLock M3(l) + End SyncLock + End Sub + + Function M1(Of T)(o as T) As Lock + Return CType(CType(o, Object), Lock) + End Function + + Function M2(Of T As Class)(o as T) As Lock + Return CType(CType(o, Object), Lock) + End Function + + Function M3(Of T As Lock)(o as T) As Lock + Return o + End Function +End Module + +Namespace System.Threading + Public Class Lock + End Class +End Namespace +" + CreateCompilation(source).AssertTheseDiagnostics( +"BC37329: A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + SyncLock DirectCast(l, Lock) + ~~~~~~~~~~~~~~~~~~~ +BC37329: A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + SyncLock CType(l, Lock) + ~~~~~~~~~~~~~~ +BC37329: A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + SyncLock TryCast(l, Lock) + ~~~~~~~~~~~~~~~~ +BC37329: A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + SyncLock M1(l) + ~~~~~ +BC37329: A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + SyncLock M2(l) + ~~~~~ +BC37329: A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + SyncLock M3(l) + ~~~~~ +") + End Sub + + + Public Sub LockType_Downcast() + Dim source = " +Imports System.Threading +Module Program + Sub Main() + Dim l = New Lock() + Dim o As Object = l + SyncLock CType(o, Lock) + End SyncLock + End Sub +End Module + +Namespace System.Threading + Public Class Lock + End Class +End Namespace +" + CreateCompilation(source).AssertTheseDiagnostics( +"BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + Dim o As Object = l + ~ +BC37329: A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + SyncLock CType(o, Lock) + ~~~~~~~~~~~~~~ +") + End Sub + + + Public Sub LockType_Derived() + Dim source = " +Imports System +Imports System.Threading + +Module Program + Private Sub Main() + Dim l1 As DerivedLock = New DerivedLock() + SyncLock l1 + End SyncLock + + Dim l2 As Lock = l1 + SyncLock l2 ' 1 + End SyncLock + + Dim l3 As DerivedLock = CType(l2, DerivedLock) ' 2 + l3 = DirectCast(l2, DerivedLock) ' 3 + l3 = TryCast(l2, DerivedLock) ' 4 + SyncLock l3 + End SyncLock + + Dim l4 As IDerivedLock = CType(l2, IDerivedLock) ' 5 + l4 = DirectCast(l2, IDerivedLock) ' 6 + l4 = TryCast(l2, IDerivedLock) ' 7 + SyncLock l4 + End SyncLock + End Sub +End Module + +Namespace System.Threading + Public Class Lock + End Class + + Public Class DerivedLock + Inherits Lock + Implements IDerivedLock + End Class + + Interface IDerivedLock + End Interface +End Namespace +" + CreateCompilation(source).AssertTheseDiagnostics( +"BC37329: A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead. + SyncLock l2 ' 1 + ~~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + Dim l3 As DerivedLock = CType(l2, DerivedLock) ' 2 + ~~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + l3 = DirectCast(l2, DerivedLock) ' 3 + ~~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + l3 = TryCast(l2, DerivedLock) ' 4 + ~~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + Dim l4 As IDerivedLock = CType(l2, IDerivedLock) ' 5 + ~~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + l4 = DirectCast(l2, IDerivedLock) ' 6 + ~~ +BC42508: A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in SyncLock statement. + l4 = TryCast(l2, IDerivedLock) ' 7 + ~~ +") + End Sub + + + Public Sub LockType_Derived_Execution() + Dim source = .Value + Dim comp = CreateCompilation(source, options:=TestOptions.ReleaseExe) + Dim verifier = CompileAndVerify(comp, expectedOutput:="locked") + verifier.Diagnostics.AssertTheseDiagnostics() + verifier.VerifyIL("Program.Main", ) + End Sub End Class End Namespace diff --git a/src/Compilers/VisualBasic/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.vb b/src/Compilers/VisualBasic/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.vb index 24721c44d70ba..d405f22faf4f2 100644 --- a/src/Compilers/VisualBasic/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.vb +++ b/src/Compilers/VisualBasic/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.vb @@ -1706,8 +1706,9 @@ End Class") Private ReadOnly _cache As New ConcurrentDictionary(Of SyntaxTree, SemanticModel)() - Public Overrides Function GetSemanticModel(tree As SyntaxTree, compilation As Compilation, Optional ignoreAccessibility As Boolean = False) As SemanticModel - Return _cache.GetOrAdd(tree, compilation.CreateSemanticModel(tree, ignoreAccessibility)) +#Disable Warning RSEXPERIMENTAL001 ' Test usage of experimental API + Public Overrides Function GetSemanticModel(tree As SyntaxTree, compilation As Compilation, Optional options As SemanticModelOptions = SemanticModelOptions.None) As SemanticModel + Return _cache.GetOrAdd(tree, compilation.CreateSemanticModel(tree, options)) End Function Public Sub VerifyCachedModel(tree As SyntaxTree, model As SemanticModel) diff --git a/src/EditorFeatures/CSharp/AddImports/CSharpAddImportsPasteCommandHandler.cs b/src/EditorFeatures/CSharp/AddImports/CSharpAddImportsPasteCommandHandler.cs index ae1cc8a3e340c..1020f93e3e778 100644 --- a/src/EditorFeatures/CSharp/AddImports/CSharpAddImportsPasteCommandHandler.cs +++ b/src/EditorFeatures/CSharp/AddImports/CSharpAddImportsPasteCommandHandler.cs @@ -12,25 +12,24 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.AddImports +namespace Microsoft.CodeAnalysis.Editor.CSharp.AddImports; + +[Export] +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(PredefinedCommandHandlerNames.AddImportsPaste)] +// Order is important here, this command needs to execute before PasteTracking +// since it may modify the pasted span. Paste tracking dismisses if +// the span is modified. It doesn't need to be before FormatDocument, but +// this helps the order of execution be more constant in case there +// are problems that arise. This command will always execute the next +// command before doing operations. +[Order(After = PredefinedCommandHandlerNames.PasteTrackingPaste)] +[Order(Before = PredefinedCommandHandlerNames.FormatDocument)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpAddImportsPasteCommandHandler(IThreadingContext threadingContext, IGlobalOptionService globalOptions, IAsynchronousOperationListenerProvider listnerProvider) : AbstractAddImportsPasteCommandHandler(threadingContext, globalOptions, listnerProvider) { - [Export] - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(PredefinedCommandHandlerNames.AddImportsPaste)] - // Order is important here, this command needs to execute before PasteTracking - // since it may modify the pasted span. Paste tracking dismisses if - // the span is modified. It doesn't need to be before FormatDocument, but - // this helps the order of execution be more constant in case there - // are problems that arise. This command will always execute the next - // command before doing operations. - [Order(After = PredefinedCommandHandlerNames.PasteTrackingPaste)] - [Order(Before = PredefinedCommandHandlerNames.FormatDocument)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpAddImportsPasteCommandHandler(IThreadingContext threadingContext, IGlobalOptionService globalOptions, IAsynchronousOperationListenerProvider listnerProvider) : AbstractAddImportsPasteCommandHandler(threadingContext, globalOptions, listnerProvider) - { - public override string DisplayName => CSharpEditorResources.Add_Missing_Usings_on_Paste; - protected override string DialogText => CSharpEditorResources.Adding_missing_usings; - } + public override string DisplayName => CSharpEditorResources.Add_Missing_Usings_on_Paste; + protected override string DialogText => CSharpEditorResources.Adding_missing_usings; } diff --git a/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler.cs b/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler.cs index f2f74e1551f39..3f3c83c5355ba 100644 --- a/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler.cs +++ b/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler.cs @@ -29,663 +29,662 @@ using Roslyn.Utilities; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Editor.CSharp.AutomaticCompletion +namespace Microsoft.CodeAnalysis.Editor.CSharp.AutomaticCompletion; + +/// +/// csharp automatic line ender command handler +/// +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(PredefinedCommandHandlerNames.AutomaticLineEnder)] +[Order(After = PredefinedCompletionNames.CompletionCommandHandler)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal partial class AutomaticLineEnderCommandHandler( + ITextUndoHistoryRegistry undoRegistry, + IEditorOperationsFactoryService editorOperations, + EditorOptionsService editorOptionsService) : AbstractAutomaticLineEnderCommandHandler(undoRegistry, editorOperations, editorOptionsService) { + private static readonly string s_semicolon = SyntaxFacts.GetText(SyntaxKind.SemicolonToken); + /// - /// csharp automatic line ender command handler + /// Annotation to locate the open brace token. /// - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(PredefinedCommandHandlerNames.AutomaticLineEnder)] - [Order(After = PredefinedCompletionNames.CompletionCommandHandler)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal partial class AutomaticLineEnderCommandHandler( - ITextUndoHistoryRegistry undoRegistry, - IEditorOperationsFactoryService editorOperations, - EditorOptionsService editorOptionsService) : AbstractAutomaticLineEnderCommandHandler(undoRegistry, editorOperations, editorOptionsService) - { - private static readonly string s_semicolon = SyntaxFacts.GetText(SyntaxKind.SemicolonToken); + private static readonly SyntaxAnnotation s_openBracePositionAnnotation = new(); - /// - /// Annotation to locate the open brace token. - /// - private static readonly SyntaxAnnotation s_openBracePositionAnnotation = new(); + /// + /// Annotation to locate the replacement node(with or without braces). + /// + private static readonly SyntaxAnnotation s_replacementNodeAnnotation = new(); - /// - /// Annotation to locate the replacement node(with or without braces). - /// - private static readonly SyntaxAnnotation s_replacementNodeAnnotation = new(); + protected override void NextAction(IEditorOperations editorOperation, Action nextAction) + => editorOperation.InsertNewLine(); - protected override void NextAction(IEditorOperations editorOperation, Action nextAction) - => editorOperation.InsertNewLine(); + protected override bool TreatAsReturn(ParsedDocument document, int caretPosition, CancellationToken cancellationToken) + { + var endToken = document.Root.FindToken(caretPosition); + if (endToken.IsMissing) + { + return false; + } - protected override bool TreatAsReturn(ParsedDocument document, int caretPosition, CancellationToken cancellationToken) + var tokenToLeft = document.Root.FindTokenOnLeftOfPosition(caretPosition); + var startToken = endToken.GetPreviousToken(); + + // case 1: + // Consider code like so: try {|} + // With auto brace completion on, user types `{` and `Return` in a hurry. + // During typing, it is possible that shift was still down and not released after typing `{`. + // So we've got an unintentional `shift + enter` and also we have nothing to complete this, + // so we put in a newline, + // which generates code like so : try { } + // | + // which is not useful as : try { + // | + // } + // To support this, we treat `shift + enter` like `enter` here. + var afterOpenBrace = startToken.Kind() == SyntaxKind.OpenBraceToken + && endToken.Kind() == SyntaxKind.CloseBraceToken + && tokenToLeft == startToken + && endToken.Parent.IsKind(SyntaxKind.Block) + && FormattingRangeHelper.AreTwoTokensOnSameLine(startToken, endToken); + + return afterOpenBrace; + } + + protected override IList FormatBasedOnEndToken(ParsedDocument document, int position, SyntaxFormattingOptions options, CancellationToken cancellationToken) + { + var root = document.Root; + var endToken = root.FindToken(position); + var span = GetFormattedTextSpan(root, endToken); + if (span == null) { - var endToken = document.Root.FindToken(caretPosition); - if (endToken.IsMissing) - { - return false; - } + return SpecializedCollections.EmptyList(); + } + + var formatter = document.LanguageServices.GetRequiredService(); + return formatter.GetFormattingResult( + root, + SpecializedCollections.SingletonCollection(CommonFormattingHelpers.GetFormattingSpan(root, span.Value)), + options, + rules: null, + cancellationToken).GetTextChanges(cancellationToken); + } - var tokenToLeft = document.Root.FindTokenOnLeftOfPosition(caretPosition); - var startToken = endToken.GetPreviousToken(); - - // case 1: - // Consider code like so: try {|} - // With auto brace completion on, user types `{` and `Return` in a hurry. - // During typing, it is possible that shift was still down and not released after typing `{`. - // So we've got an unintentional `shift + enter` and also we have nothing to complete this, - // so we put in a newline, - // which generates code like so : try { } - // | - // which is not useful as : try { - // | - // } - // To support this, we treat `shift + enter` like `enter` here. - var afterOpenBrace = startToken.Kind() == SyntaxKind.OpenBraceToken - && endToken.Kind() == SyntaxKind.CloseBraceToken - && tokenToLeft == startToken - && endToken.Parent.IsKind(SyntaxKind.Block) - && FormattingRangeHelper.AreTwoTokensOnSameLine(startToken, endToken); - - return afterOpenBrace; + private static TextSpan? GetFormattedTextSpan(SyntaxNode root, SyntaxToken endToken) + { + if (endToken.IsMissing) + { + return null; } - protected override IList FormatBasedOnEndToken(ParsedDocument document, int position, SyntaxFormattingOptions options, CancellationToken cancellationToken) + var ranges = FormattingRangeHelper.FindAppropriateRange(endToken, useDefaultRange: false); + if (ranges == null) { - var root = document.Root; - var endToken = root.FindToken(position); - var span = GetFormattedTextSpan(root, endToken); - if (span == null) - { - return SpecializedCollections.EmptyList(); - } + return null; + } - var formatter = document.LanguageServices.GetRequiredService(); - return formatter.GetFormattingResult( - root, - SpecializedCollections.SingletonCollection(CommonFormattingHelpers.GetFormattingSpan(root, span.Value)), - options, - rules: null, - cancellationToken).GetTextChanges(cancellationToken); + var startToken = ranges.Value.Item1; + if (startToken.IsMissing || startToken.Kind() == SyntaxKind.None) + { + return null; } - private static TextSpan? GetFormattedTextSpan(SyntaxNode root, SyntaxToken endToken) + return CommonFormattingHelpers.GetFormattingSpan(root, TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End)); + } + + #region SemicolonAppending + + protected override string? GetEndingString(ParsedDocument document, int position) + { + var root = document.Root; + var text = document.Text; + var tree = document.SyntaxTree; + + // Go through the set of owning nodes in leaf to root chain. + foreach (var owningNode in GetOwningNodes(root, position)) { - if (endToken.IsMissing) + if (!TryGetLastToken(text, position, owningNode, out var lastToken)) { + // If we can't get last token, there is nothing more to do, just skip + // the other owning nodes and return. return null; } - var ranges = FormattingRangeHelper.FindAppropriateRange(endToken, useDefaultRange: false); - if (ranges == null) + if (!CheckLocation(text, position, owningNode, lastToken)) { + // If we failed this check, we indeed got the intended owner node and + // inserting line ender here would introduce errors. return null; } - var startToken = ranges.Value.Item1; - if (startToken.IsMissing || startToken.Kind() == SyntaxKind.None) + // so far so good. we only add semi-colon if it makes statement syntax error free + var textToParse = owningNode.NormalizeWhitespace().ToFullString() + s_semicolon; + + // currently, Parsing a field is not supported. as a workaround, wrap the field in a type and parse + var node = ParseNode(tree, owningNode, textToParse); + + // Insert line ender if we didn't introduce any diagnostics, if not try the next owning node. + if (node != null && !node.ContainsDiagnostics) { - return null; + return s_semicolon; } - - return CommonFormattingHelpers.GetFormattingSpan(root, TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End)); } - #region SemicolonAppending + return null; + } - protected override string? GetEndingString(ParsedDocument document, int position) + private static SyntaxNode? ParseNode(SyntaxTree tree, SyntaxNode owningNode, string textToParse) + => owningNode switch { - var root = document.Root; - var text = document.Text; - var tree = document.SyntaxTree; + BaseFieldDeclarationSyntax => SyntaxFactory.ParseCompilationUnit(WrapInType(textToParse), options: (CSharpParseOptions)tree.Options), + BaseMethodDeclarationSyntax => SyntaxFactory.ParseCompilationUnit(WrapInType(textToParse), options: (CSharpParseOptions)tree.Options), + BasePropertyDeclarationSyntax => SyntaxFactory.ParseCompilationUnit(WrapInType(textToParse), options: (CSharpParseOptions)tree.Options), + StatementSyntax => SyntaxFactory.ParseStatement(textToParse, options: (CSharpParseOptions)tree.Options), + UsingDirectiveSyntax => SyntaxFactory.ParseCompilationUnit(textToParse, options: (CSharpParseOptions)tree.Options), + _ => null, + }; - // Go through the set of owning nodes in leaf to root chain. - foreach (var owningNode in GetOwningNodes(root, position)) - { - if (!TryGetLastToken(text, position, owningNode, out var lastToken)) - { - // If we can't get last token, there is nothing more to do, just skip - // the other owning nodes and return. - return null; - } - - if (!CheckLocation(text, position, owningNode, lastToken)) - { - // If we failed this check, we indeed got the intended owner node and - // inserting line ender here would introduce errors. - return null; - } - - // so far so good. we only add semi-colon if it makes statement syntax error free - var textToParse = owningNode.NormalizeWhitespace().ToFullString() + s_semicolon; - - // currently, Parsing a field is not supported. as a workaround, wrap the field in a type and parse - var node = ParseNode(tree, owningNode, textToParse); - - // Insert line ender if we didn't introduce any diagnostics, if not try the next owning node. - if (node != null && !node.ContainsDiagnostics) - { - return s_semicolon; - } - } + /// + /// wrap field in type + /// + private static string WrapInType(string textToParse) + => "class C { " + textToParse + " }"; - return null; - } + /// + /// make sure current location is okay to put semicolon + /// + private static bool CheckLocation(SourceText text, int position, SyntaxNode owningNode, SyntaxToken lastToken) + { + var line = text.Lines.GetLineFromPosition(position); - private static SyntaxNode? ParseNode(SyntaxTree tree, SyntaxNode owningNode, string textToParse) - => owningNode switch - { - BaseFieldDeclarationSyntax => SyntaxFactory.ParseCompilationUnit(WrapInType(textToParse), options: (CSharpParseOptions)tree.Options), - BaseMethodDeclarationSyntax => SyntaxFactory.ParseCompilationUnit(WrapInType(textToParse), options: (CSharpParseOptions)tree.Options), - BasePropertyDeclarationSyntax => SyntaxFactory.ParseCompilationUnit(WrapInType(textToParse), options: (CSharpParseOptions)tree.Options), - StatementSyntax => SyntaxFactory.ParseStatement(textToParse, options: (CSharpParseOptions)tree.Options), - UsingDirectiveSyntax => SyntaxFactory.ParseCompilationUnit(textToParse, options: (CSharpParseOptions)tree.Options), - _ => null, - }; - - /// - /// wrap field in type - /// - private static string WrapInType(string textToParse) - => "class C { " + textToParse + " }"; - - /// - /// make sure current location is okay to put semicolon - /// - private static bool CheckLocation(SourceText text, int position, SyntaxNode owningNode, SyntaxToken lastToken) + // if caret is at the end of the line and containing statement is expression statement + // don't do anything + if (position == line.End && owningNode is ExpressionStatementSyntax) { - var line = text.Lines.GetLineFromPosition(position); + return false; + } + + var locatedAtTheEndOfLine = LocatedAtTheEndOfLine(line, lastToken); - // if caret is at the end of the line and containing statement is expression statement - // don't do anything - if (position == line.End && owningNode is ExpressionStatementSyntax) + // make sure that there is no trailing text after last token on the line if it is not at the end of the line + if (!locatedAtTheEndOfLine) + { + var endingString = text.ToString(TextSpan.FromBounds(lastToken.Span.End, line.End)); + if (!string.IsNullOrWhiteSpace(endingString)) { return false; } + } - var locatedAtTheEndOfLine = LocatedAtTheEndOfLine(line, lastToken); + // check whether using has contents + if (owningNode is UsingDirectiveSyntax u && u.NamespaceOrType.IsMissing) + { + return false; + } - // make sure that there is no trailing text after last token on the line if it is not at the end of the line - if (!locatedAtTheEndOfLine) - { - var endingString = text.ToString(TextSpan.FromBounds(lastToken.Span.End, line.End)); - if (!string.IsNullOrWhiteSpace(endingString)) - { - return false; - } - } + // make sure there is no open string literals + var previousToken = lastToken.GetPreviousToken(); + if (previousToken.Kind() == SyntaxKind.StringLiteralToken && previousToken.ToString().Last() != '"') + { + return false; + } - // check whether using has contents - if (owningNode is UsingDirectiveSyntax u && u.NamespaceOrType.IsMissing) - { - return false; - } + if (previousToken.Kind() == SyntaxKind.CharacterLiteralToken && previousToken.ToString().Last() != '\'') + { + return false; + } - // make sure there is no open string literals - var previousToken = lastToken.GetPreviousToken(); - if (previousToken.Kind() == SyntaxKind.StringLiteralToken && previousToken.ToString().Last() != '"') + // now, check embedded statement case + if (owningNode.IsEmbeddedStatementOwner()) + { + var embeddedStatement = owningNode.GetEmbeddedStatement(); + if (embeddedStatement == null || embeddedStatement.Span.IsEmpty) { return false; } + } - if (previousToken.Kind() == SyntaxKind.CharacterLiteralToken && previousToken.ToString().Last() != '\'') - { - return false; - } + return true; + } - // now, check embedded statement case - if (owningNode.IsEmbeddedStatementOwner()) - { - var embeddedStatement = owningNode.GetEmbeddedStatement(); - if (embeddedStatement == null || embeddedStatement.Span.IsEmpty) - { - return false; - } - } + /// + /// get last token of the given using/field/statement/expression bodied member if one exists + /// + private static bool TryGetLastToken(SourceText text, int position, SyntaxNode owningNode, out SyntaxToken lastToken) + { + lastToken = owningNode.GetLastToken(includeZeroWidth: true); - return true; + // last token must be on the same line as the caret + var line = text.Lines.GetLineFromPosition(position); + var locatedAtTheEndOfLine = LocatedAtTheEndOfLine(line, lastToken); + if (!locatedAtTheEndOfLine && text.Lines.IndexOf(lastToken.Span.End) != line.LineNumber) + { + return false; } - /// - /// get last token of the given using/field/statement/expression bodied member if one exists - /// - private static bool TryGetLastToken(SourceText text, int position, SyntaxNode owningNode, out SyntaxToken lastToken) + // if we already have last semicolon, we don't need to do anything + if (!lastToken.IsMissing && lastToken.Kind() == SyntaxKind.SemicolonToken) { - lastToken = owningNode.GetLastToken(includeZeroWidth: true); + return false; + } - // last token must be on the same line as the caret - var line = text.Lines.GetLineFromPosition(position); - var locatedAtTheEndOfLine = LocatedAtTheEndOfLine(line, lastToken); - if (!locatedAtTheEndOfLine && text.Lines.IndexOf(lastToken.Span.End) != line.LineNumber) - { - return false; - } + return true; + } - // if we already have last semicolon, we don't need to do anything - if (!lastToken.IsMissing && lastToken.Kind() == SyntaxKind.SemicolonToken) - { - return false; - } + /// + /// check whether the line is located at the end of the line + /// + private static bool LocatedAtTheEndOfLine(TextLine line, SyntaxToken lastToken) + => lastToken.IsMissing && lastToken.Span.End == line.EndIncludingLineBreak; - return true; + /// + /// find owning usings/field/statement/expression-bodied member of the given position + /// + private static IEnumerable GetOwningNodes(SyntaxNode root, int position) + { + // make sure caret position is somewhere we can find a token + var token = root.FindTokenFromEnd(position); + if (token.Kind() == SyntaxKind.None) + { + return SpecializedCollections.EmptyEnumerable(); } - /// - /// check whether the line is located at the end of the line - /// - private static bool LocatedAtTheEndOfLine(TextLine line, SyntaxToken lastToken) - => lastToken.IsMissing && lastToken.Span.End == line.EndIncludingLineBreak; - - /// - /// find owning usings/field/statement/expression-bodied member of the given position - /// - private static IEnumerable GetOwningNodes(SyntaxNode root, int position) - { - // make sure caret position is somewhere we can find a token - var token = root.FindTokenFromEnd(position); - if (token.Kind() == SyntaxKind.None) - { - return SpecializedCollections.EmptyEnumerable(); - } + return token.GetAncestors() + .Where(AllowedConstructs) + .Select(OwningNode) + .WhereNotNull(); + } - return token.GetAncestors() - .Where(AllowedConstructs) - .Select(OwningNode) - .WhereNotNull(); - } + private static bool AllowedConstructs(SyntaxNode n) + => n is StatementSyntax + or BaseFieldDeclarationSyntax + or UsingDirectiveSyntax + or ArrowExpressionClauseSyntax; - private static bool AllowedConstructs(SyntaxNode n) - => n is StatementSyntax - or BaseFieldDeclarationSyntax - or UsingDirectiveSyntax - or ArrowExpressionClauseSyntax; + private static SyntaxNode? OwningNode(SyntaxNode n) + => n is ArrowExpressionClauseSyntax ? n.Parent : n; - private static SyntaxNode? OwningNode(SyntaxNode n) - => n is ArrowExpressionClauseSyntax ? n.Parent : n; + #endregion - #endregion + #region BraceModification - #region BraceModification + protected override void ModifySelectedNode( + AutomaticLineEnderCommandArgs args, + ParsedDocument document, + SyntaxNode selectedNode, + bool addBrace, + int caretPosition, + CancellationToken cancellationToken) + { + var formattingOptions = args.SubjectBuffer.GetSyntaxFormattingOptions(EditorOptionsService, document.LanguageServices, explicitFormat: false); - protected override void ModifySelectedNode( - AutomaticLineEnderCommandArgs args, - ParsedDocument document, - SyntaxNode selectedNode, - bool addBrace, - int caretPosition, - CancellationToken cancellationToken) + // Add braces for the selected node + if (addBrace) { - var formattingOptions = args.SubjectBuffer.GetSyntaxFormattingOptions(EditorOptionsService, document.LanguageServices, explicitFormat: false); - - // Add braces for the selected node - if (addBrace) - { - // For these syntax node, braces pair could be easily added by modify the syntax tree - if (selectedNode is BaseTypeDeclarationSyntax - or BaseMethodDeclarationSyntax - or LocalFunctionStatementSyntax - or AccessorDeclarationSyntax - or ObjectCreationExpressionSyntax - or WhileStatementSyntax - or ForEachStatementSyntax - or ForStatementSyntax - or LockStatementSyntax - or UsingStatementSyntax - or DoStatementSyntax - or IfStatementSyntax - or ElseClauseSyntax) - { - // Add the braces and get the next caretPosition - var (newRoot, nextCaretPosition) = AddBraceToSelectedNode(document.SolutionServices, document.Root, selectedNode, formattingOptions, cancellationToken); - - var newDocument = document.WithChangedRoot(newRoot, cancellationToken); - args.SubjectBuffer.ApplyChanges(newDocument.GetChanges(document)); - args.TextView.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(args.SubjectBuffer.CurrentSnapshot, nextCaretPosition)); - } - else - { - // For the rest of the syntax node, - // like try statement - // class Bar - // { - // void Main() - // { - // tr$$y - // } - // } - // In this case, the last close brace of 'void Main()' would be thought as a part of the try statement, - // and the last close brace of 'Bar' would be thought as a part of Main() - // For field and event declarations node because - // class A - // { - // int Hel$$lo - // - // [SomeAttribute] - // void Bar() { } - // } - // Parser would think '[SomeAttribute]' (BrackedArgumentList) is a part of the 'Hello' (VariableDeclarator). - // So for these cases, just find the missing open brace position and directly insert '()' to the document - - // 1. Find the position to insert braces. - var insertionPosition = GetBraceInsertionPosition(selectedNode); - - // 2. Insert the braces and move caret - InsertBraceAndMoveCaret(args.TextView, args.SubjectBuffer, document, formattingOptions, insertionPosition, cancellationToken); - } - } - else + // For these syntax node, braces pair could be easily added by modify the syntax tree + if (selectedNode is BaseTypeDeclarationSyntax + or BaseMethodDeclarationSyntax + or LocalFunctionStatementSyntax + or AccessorDeclarationSyntax + or ObjectCreationExpressionSyntax + or WhileStatementSyntax + or ForEachStatementSyntax + or ForStatementSyntax + or LockStatementSyntax + or UsingStatementSyntax + or DoStatementSyntax + or IfStatementSyntax + or ElseClauseSyntax) { - // Remove the braces and get the next caretPosition - var (newRoot, nextCaretPosition) = RemoveBraceFromSelectedNode( - document.SolutionServices, - document.Root, - selectedNode, - formattingOptions, - cancellationToken); + // Add the braces and get the next caretPosition + var (newRoot, nextCaretPosition) = AddBraceToSelectedNode(document.SolutionServices, document.Root, selectedNode, formattingOptions, cancellationToken); var newDocument = document.WithChangedRoot(newRoot, cancellationToken); args.SubjectBuffer.ApplyChanges(newDocument.GetChanges(document)); args.TextView.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(args.SubjectBuffer.CurrentSnapshot, nextCaretPosition)); } + else + { + // For the rest of the syntax node, + // like try statement + // class Bar + // { + // void Main() + // { + // tr$$y + // } + // } + // In this case, the last close brace of 'void Main()' would be thought as a part of the try statement, + // and the last close brace of 'Bar' would be thought as a part of Main() + // For field and event declarations node because + // class A + // { + // int Hel$$lo + // + // [SomeAttribute] + // void Bar() { } + // } + // Parser would think '[SomeAttribute]' (BrackedArgumentList) is a part of the 'Hello' (VariableDeclarator). + // So for these cases, just find the missing open brace position and directly insert '()' to the document + + // 1. Find the position to insert braces. + var insertionPosition = GetBraceInsertionPosition(selectedNode); + + // 2. Insert the braces and move caret + InsertBraceAndMoveCaret(args.TextView, args.SubjectBuffer, document, formattingOptions, insertionPosition, cancellationToken); + } } + else + { + // Remove the braces and get the next caretPosition + var (newRoot, nextCaretPosition) = RemoveBraceFromSelectedNode( + document.SolutionServices, + document.Root, + selectedNode, + formattingOptions, + cancellationToken); + + var newDocument = document.WithChangedRoot(newRoot, cancellationToken); + args.SubjectBuffer.ApplyChanges(newDocument.GetChanges(document)); + args.TextView.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(args.SubjectBuffer.CurrentSnapshot, nextCaretPosition)); + } + } - private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToSelectedNode( - SolutionServices services, - SyntaxNode root, - SyntaxNode selectedNode, - SyntaxFormattingOptions formattingOptions, - CancellationToken cancellationToken) + private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToSelectedNode( + SolutionServices services, + SyntaxNode root, + SyntaxNode selectedNode, + SyntaxFormattingOptions formattingOptions, + CancellationToken cancellationToken) + { + // For these nodes, directly modify the node and replace it. + if (selectedNode is BaseTypeDeclarationSyntax + or BaseMethodDeclarationSyntax + or LocalFunctionStatementSyntax + or AccessorDeclarationSyntax) { - // For these nodes, directly modify the node and replace it. - if (selectedNode is BaseTypeDeclarationSyntax - or BaseMethodDeclarationSyntax - or LocalFunctionStatementSyntax - or AccessorDeclarationSyntax) - { - var newRoot = ReplaceNodeAndFormat( - services, - root, - selectedNode, - WithBraces(selectedNode, formattingOptions), - formattingOptions, - cancellationToken); - // Locate the open brace token, and move the caret after it. - var nextCaretPosition = GetOpenBraceSpanEnd(newRoot); - return (newRoot, nextCaretPosition); - } + var newRoot = ReplaceNodeAndFormat( + services, + root, + selectedNode, + WithBraces(selectedNode, formattingOptions), + formattingOptions, + cancellationToken); + // Locate the open brace token, and move the caret after it. + var nextCaretPosition = GetOpenBraceSpanEnd(newRoot); + return (newRoot, nextCaretPosition); + } - // For ObjectCreationExpression, like new List() - // It requires - // 1. Add an initializer to it. - // 2. make sure it has '()' after the type, and if its next token is a missing semicolon, add that semicolon. e.g - // var c = new Obje$$ct() => var c = new Object(); - if (selectedNode is ObjectCreationExpressionSyntax objectCreationExpressionNode) - { - var (newNode, oldNode) = ModifyObjectCreationExpressionNode(objectCreationExpressionNode, addOrRemoveInitializer: true, formattingOptions); - var newRoot = ReplaceNodeAndFormat( - services, - root, - oldNode, - newNode, - formattingOptions, - cancellationToken); - - // Locate the open brace token, and move the caret after it. - var nextCaretPosition = GetOpenBraceSpanEnd(newRoot); - return (newRoot, nextCaretPosition); - } + // For ObjectCreationExpression, like new List() + // It requires + // 1. Add an initializer to it. + // 2. make sure it has '()' after the type, and if its next token is a missing semicolon, add that semicolon. e.g + // var c = new Obje$$ct() => var c = new Object(); + if (selectedNode is ObjectCreationExpressionSyntax objectCreationExpressionNode) + { + var (newNode, oldNode) = ModifyObjectCreationExpressionNode(objectCreationExpressionNode, addOrRemoveInitializer: true, formattingOptions); + var newRoot = ReplaceNodeAndFormat( + services, + root, + oldNode, + newNode, + formattingOptions, + cancellationToken); + + // Locate the open brace token, and move the caret after it. + var nextCaretPosition = GetOpenBraceSpanEnd(newRoot); + return (newRoot, nextCaretPosition); + } + + // For the embeddedStatementOwner node, like ifStatement/elseClause + // It requires: + // 1. Add a empty block as its statement. + // 2. Handle its previous statement if needed. + // case 1: + // if$$ (true) + // var c = 10; + // => + // if (true) + // { + // $$ + // } + // var c = 10; + // In this case, 'var c = 10;' is considered as the inner statement so we need to move it next to the if Statement + // + // case 2: + // if (true) + // { + // } + // else if$$ (false) + // Print("Bar"); + // else + // { + // } + // => + // if (true) + // { + // } + // else if (false) + // { + // $$ + // Print("Bar"); + // } + // else + // { + // } + // In this case 'Print("Bar")' is considered as the innerStatement so when we inserted the empty block, we need also insert that + if (selectedNode.IsEmbeddedStatementOwner()) + { + return AddBraceToEmbeddedStatementOwner(services, root, selectedNode, formattingOptions, cancellationToken); + } - // For the embeddedStatementOwner node, like ifStatement/elseClause - // It requires: - // 1. Add a empty block as its statement. - // 2. Handle its previous statement if needed. - // case 1: - // if$$ (true) - // var c = 10; + throw ExceptionUtilities.UnexpectedValue(selectedNode); + } + + private static (SyntaxNode newRoot, int nextCaretPosition) RemoveBraceFromSelectedNode( + SolutionServices services, + SyntaxNode root, + SyntaxNode selectedNode, + SyntaxFormattingOptions formattingOptions, + CancellationToken cancellationToken) + { + // Remove the initializer from ObjectCreationExpression + // Step 1. Remove the initializer + // e.g. var c = new Bar { $$ } => var c = new Bar + // + // Step 2. Add parenthesis + // e.g var c = new Bar => var c = new Bar() + // + // Step 3. Add semicolon if needed + // e.g. var c = new Bar() => var c = new Bar(); + if (selectedNode is BaseObjectCreationExpressionSyntax objectCreationExpressionNode) + { + var (newNode, oldNode) = ModifyObjectCreationExpressionNode(objectCreationExpressionNode, addOrRemoveInitializer: false, formattingOptions); + var newRoot = ReplaceNodeAndFormat( + services, + root, + oldNode, + newNode, + formattingOptions, + cancellationToken); + + // Find the replacement node, and move the caret to the end of line (where the last token is) + var replacementNode = newRoot.GetAnnotatedNodes(s_replacementNodeAnnotation).Single(); + var lastToken = replacementNode.GetLastToken(); + var lineEnd = newRoot.GetText().Lines.GetLineFromPosition(lastToken.Span.End).End; + return (newRoot, lineEnd); + } + else + { + // For all the other cases, include + // 1. Property declaration => Field Declaration. + // e.g. + // class Bar + // { + // int Bar {$$} + // } // => - // if (true) + // class Bar + // { + // int Bar; + // } + // 2. Event Declaration => Event Field Declaration + // class Bar // { - // $$ + // event EventHandler e { $$ } // } - // var c = 10; - // In this case, 'var c = 10;' is considered as the inner statement so we need to move it next to the if Statement - // - // case 2: - // if (true) + // => + // class Bar // { + // event EventHandler e; // } - // else if$$ (false) - // Print("Bar"); - // else + // 3. Accessor + // class Bar // { + // int Bar + // { + // get { $$ } + // } // } // => - // if (true) + // class Bar // { + // int Bar + // { + // get; + // } // } - // else if (false) + // Get its no-brace version of node and insert it into the root. + var newRoot = ReplaceNodeAndFormat( + services, + root, + selectedNode, + WithoutBraces(selectedNode), + formattingOptions, + cancellationToken); + + // Locate the replacement node, move the caret to the end. + // e.g. + // class Bar // { - // $$ - // Print("Bar"); + // event EventHandler e { $$ } // } - // else + // => + // class Bar // { + // event EventHandler e;$$ // } - // In this case 'Print("Bar")' is considered as the innerStatement so when we inserted the empty block, we need also insert that - if (selectedNode.IsEmbeddedStatementOwner()) - { - return AddBraceToEmbeddedStatementOwner(services, root, selectedNode, formattingOptions, cancellationToken); - } - - throw ExceptionUtilities.UnexpectedValue(selectedNode); + // and we need to move the caret after semicolon + var nextCaretPosition = newRoot.GetAnnotatedNodes(s_replacementNodeAnnotation).Single().GetLastToken().Span.End; + return (newRoot, nextCaretPosition); } + } - private static (SyntaxNode newRoot, int nextCaretPosition) RemoveBraceFromSelectedNode( - SolutionServices services, - SyntaxNode root, - SyntaxNode selectedNode, - SyntaxFormattingOptions formattingOptions, - CancellationToken cancellationToken) + private static int GetOpenBraceSpanEnd(SyntaxNode root) + { + // Use the annotation to find the end of the open brace. + var annotatedOpenBraceToken = root.GetAnnotatedTokens(s_openBracePositionAnnotation).Single(); + return annotatedOpenBraceToken.Span.End; + } + + private static int GetBraceInsertionPosition(SyntaxNode node) + { + if (node is SwitchStatementSyntax switchStatementNode) { - // Remove the initializer from ObjectCreationExpression - // Step 1. Remove the initializer - // e.g. var c = new Bar { $$ } => var c = new Bar - // - // Step 2. Add parenthesis - // e.g var c = new Bar => var c = new Bar() - // - // Step 3. Add semicolon if needed - // e.g. var c = new Bar() => var c = new Bar(); - if (selectedNode is BaseObjectCreationExpressionSyntax objectCreationExpressionNode) - { - var (newNode, oldNode) = ModifyObjectCreationExpressionNode(objectCreationExpressionNode, addOrRemoveInitializer: false, formattingOptions); - var newRoot = ReplaceNodeAndFormat( - services, - root, - oldNode, - newNode, - formattingOptions, - cancellationToken); - - // Find the replacement node, and move the caret to the end of line (where the last token is) - var replacementNode = newRoot.GetAnnotatedNodes(s_replacementNodeAnnotation).Single(); - var lastToken = replacementNode.GetLastToken(); - var lineEnd = newRoot.GetText().Lines.GetLineFromPosition(lastToken.Span.End).End; - return (newRoot, lineEnd); - } - else + // There is no parenthesis pair in the switchStatementNode, and the node before 'switch' is an expression + // e.g. + // void Foo(int i) + // { + // var c = (i + 1) swit$$ch + // } + // Consider this as a SwitchExpression, add the brace after 'switch' + if (switchStatementNode.OpenParenToken.IsMissing + && switchStatementNode.CloseParenToken.IsMissing + && IsTokenPartOfExpression(switchStatementNode.GetFirstToken().GetPreviousToken())) { - // For all the other cases, include - // 1. Property declaration => Field Declaration. - // e.g. - // class Bar - // { - // int Bar {$$} - // } - // => - // class Bar - // { - // int Bar; - // } - // 2. Event Declaration => Event Field Declaration - // class Bar - // { - // event EventHandler e { $$ } - // } - // => - // class Bar - // { - // event EventHandler e; - // } - // 3. Accessor - // class Bar - // { - // int Bar - // { - // get { $$ } - // } - // } - // => - // class Bar - // { - // int Bar - // { - // get; - // } - // } - // Get its no-brace version of node and insert it into the root. - var newRoot = ReplaceNodeAndFormat( - services, - root, - selectedNode, - WithoutBraces(selectedNode), - formattingOptions, - cancellationToken); - - // Locate the replacement node, move the caret to the end. - // e.g. - // class Bar - // { - // event EventHandler e { $$ } - // } - // => - // class Bar - // { - // event EventHandler e;$$ - // } - // and we need to move the caret after semicolon - var nextCaretPosition = newRoot.GetAnnotatedNodes(s_replacementNodeAnnotation).Single().GetLastToken().Span.End; - return (newRoot, nextCaretPosition); + return switchStatementNode.SwitchKeyword.Span.End; } - } - private static int GetOpenBraceSpanEnd(SyntaxNode root) - { - // Use the annotation to find the end of the open brace. - var annotatedOpenBraceToken = root.GetAnnotatedTokens(s_openBracePositionAnnotation).Single(); - return annotatedOpenBraceToken.Span.End; + // In all other case, think it is a switch statement, add brace after the close parenthesis. + return switchStatementNode.CloseParenToken.Span.End; } - private static int GetBraceInsertionPosition(SyntaxNode node) + return node switch { - if (node is SwitchStatementSyntax switchStatementNode) - { - // There is no parenthesis pair in the switchStatementNode, and the node before 'switch' is an expression - // e.g. - // void Foo(int i) - // { - // var c = (i + 1) swit$$ch - // } - // Consider this as a SwitchExpression, add the brace after 'switch' - if (switchStatementNode.OpenParenToken.IsMissing - && switchStatementNode.CloseParenToken.IsMissing - && IsTokenPartOfExpression(switchStatementNode.GetFirstToken().GetPreviousToken())) - { - return switchStatementNode.SwitchKeyword.Span.End; - } - - // In all other case, think it is a switch statement, add brace after the close parenthesis. - return switchStatementNode.CloseParenToken.Span.End; - } + NamespaceDeclarationSyntax => node.GetBraces().openBrace.SpanStart, + IndexerDeclarationSyntax indexerNode => indexerNode.ParameterList.Span.End, + TryStatementSyntax tryStatementNode => tryStatementNode.TryKeyword.Span.End, + CatchClauseSyntax catchClauseNode => catchClauseNode.Block.SpanStart, + FinallyClauseSyntax finallyClauseNode => finallyClauseNode.Block.SpanStart, + CheckedStatementSyntax checkedStatementNode => checkedStatementNode.Keyword.Span.End, + FieldDeclarationSyntax fieldDeclarationNode => fieldDeclarationNode.Declaration.Variables[0].Identifier.Span.End, + EventFieldDeclarationSyntax eventFieldDeclarationNode => eventFieldDeclarationNode.Declaration.Variables[0].Identifier.Span.End, + _ => throw ExceptionUtilities.Unreachable(), + }; + } - return node switch - { - NamespaceDeclarationSyntax => node.GetBraces().openBrace.SpanStart, - IndexerDeclarationSyntax indexerNode => indexerNode.ParameterList.Span.End, - TryStatementSyntax tryStatementNode => tryStatementNode.TryKeyword.Span.End, - CatchClauseSyntax catchClauseNode => catchClauseNode.Block.SpanStart, - FinallyClauseSyntax finallyClauseNode => finallyClauseNode.Block.SpanStart, - CheckedStatementSyntax checkedStatementNode => checkedStatementNode.Keyword.Span.End, - FieldDeclarationSyntax fieldDeclarationNode => fieldDeclarationNode.Declaration.Variables[0].Identifier.Span.End, - EventFieldDeclarationSyntax eventFieldDeclarationNode => eventFieldDeclarationNode.Declaration.Variables[0].Identifier.Span.End, - _ => throw ExceptionUtilities.Unreachable(), - }; + private static bool IsTokenPartOfExpression(SyntaxToken syntaxToken) + { + if (syntaxToken.IsMissing || syntaxToken.IsKind(SyntaxKind.None)) + { + return false; } - private static bool IsTokenPartOfExpression(SyntaxToken syntaxToken) - { - if (syntaxToken.IsMissing || syntaxToken.IsKind(SyntaxKind.None)) - { - return false; - } + return !syntaxToken.GetAncestors().IsEmpty(); + } - return !syntaxToken.GetAncestors().IsEmpty(); - } + private static string GetBracePairString(SyntaxFormattingOptions formattingOptions) + => string.Concat(SyntaxFacts.GetText(SyntaxKind.OpenBraceToken), + formattingOptions.NewLine, + SyntaxFacts.GetText(SyntaxKind.CloseBraceToken)); + + private void InsertBraceAndMoveCaret( + ITextView textView, + ITextBuffer buffer, + ParsedDocument document, + SyntaxFormattingOptions formattingOptions, + int insertionPosition, + CancellationToken cancellationToken) + { + var bracePair = GetBracePairString(formattingOptions); - private static string GetBracePairString(SyntaxFormattingOptions formattingOptions) - => string.Concat(SyntaxFacts.GetText(SyntaxKind.OpenBraceToken), - formattingOptions.NewLine, - SyntaxFacts.GetText(SyntaxKind.CloseBraceToken)); - - private void InsertBraceAndMoveCaret( - ITextView textView, - ITextBuffer buffer, - ParsedDocument document, - SyntaxFormattingOptions formattingOptions, - int insertionPosition, - CancellationToken cancellationToken) - { - var bracePair = GetBracePairString(formattingOptions); + // 1. Insert { }. + var insertChange = new TextChange(new TextSpan(insertionPosition, 0), bracePair); + buffer.ApplyChange(insertChange); + var newDocument = document.WithChange(insertChange, cancellationToken); - // 1. Insert { }. - var insertChange = new TextChange(new TextSpan(insertionPosition, 0), bracePair); - buffer.ApplyChange(insertChange); - var newDocument = document.WithChange(insertChange, cancellationToken); + // 2. Place caret between the braces. + textView.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(textView.TextSnapshot, insertionPosition + 1)); - // 2. Place caret between the braces. - textView.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(textView.TextSnapshot, insertionPosition + 1)); + // 3. Format the document using the close brace. + var changes = FormatBasedOnEndToken(newDocument, insertionPosition + bracePair.Length - 1, formattingOptions, cancellationToken); + buffer.ApplyChanges(changes); + } - // 3. Format the document using the close brace. - var changes = FormatBasedOnEndToken(newDocument, insertionPosition + bracePair.Length - 1, formattingOptions, cancellationToken); - buffer.ApplyChanges(changes); + protected override (SyntaxNode selectedNode, bool addBrace)? GetValidNodeToModifyBraces(ParsedDocument document, int caretPosition, CancellationToken cancellationToken) + { + var token = document.Root.FindTokenOnLeftOfPosition(caretPosition); + if (token.IsKind(SyntaxKind.None)) + { + return null; } - protected override (SyntaxNode selectedNode, bool addBrace)? GetValidNodeToModifyBraces(ParsedDocument document, int caretPosition, CancellationToken cancellationToken) + foreach (var node in token.GetAncestors()) { - var token = document.Root.FindTokenOnLeftOfPosition(caretPosition); - if (token.IsKind(SyntaxKind.None)) + if (ShouldAddBraces(node, caretPosition)) { - return null; + return (selectedNode: node, addBrace: true); } - foreach (var node in token.GetAncestors()) + if (ShouldRemoveBraces(node, caretPosition)) { - if (ShouldAddBraces(node, caretPosition)) - { - return (selectedNode: node, addBrace: true); - } - - if (ShouldRemoveBraces(node, caretPosition)) - { - return (selectedNode: node, addBrace: false); - } + return (selectedNode: node, addBrace: false); } - - return null; } - #endregion + return null; } + + #endregion } diff --git a/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler_Helpers.cs b/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler_Helpers.cs index aa6ffca873414..37655a312ed78 100644 --- a/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler_Helpers.cs +++ b/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler_Helpers.cs @@ -16,1019 +16,1018 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.AutomaticCompletion +namespace Microsoft.CodeAnalysis.Editor.CSharp.AutomaticCompletion; + +internal partial class AutomaticLineEnderCommandHandler { - internal partial class AutomaticLineEnderCommandHandler + #region NodeReplacementHelpers + + private static (SyntaxNode newRoot, int nextCaretPosition) ReplaceStatementOwnerAndInsertStatement( + SolutionServices services, + SyntaxNode root, + SyntaxNode oldNode, + SyntaxNode newNode, + SyntaxNode anchorNode, + ImmutableArray nodesToInsert, + SyntaxFormattingOptions formattingOptions, + CancellationToken cancellationToken) { - #region NodeReplacementHelpers - - private static (SyntaxNode newRoot, int nextCaretPosition) ReplaceStatementOwnerAndInsertStatement( - SolutionServices services, - SyntaxNode root, - SyntaxNode oldNode, - SyntaxNode newNode, - SyntaxNode anchorNode, - ImmutableArray nodesToInsert, - SyntaxFormattingOptions formattingOptions, - CancellationToken cancellationToken) - { - var rootEditor = new SyntaxEditor(root, services); + var rootEditor = new SyntaxEditor(root, services); - // 1. Insert the node before anchor node - rootEditor.InsertAfter(anchorNode, nodesToInsert); + // 1. Insert the node before anchor node + rootEditor.InsertAfter(anchorNode, nodesToInsert); - // 2. Replace the old node with newNode. (new node is the node with correct braces) - rootEditor.ReplaceNode(oldNode, newNode.WithAdditionalAnnotations(s_replacementNodeAnnotation)); - var newRoot = rootEditor.GetChangedRoot(); + // 2. Replace the old node with newNode. (new node is the node with correct braces) + rootEditor.ReplaceNode(oldNode, newNode.WithAdditionalAnnotations(s_replacementNodeAnnotation)); + var newRoot = rootEditor.GetChangedRoot(); - // 4. Format the new node so that the inserted braces/blocks would have correct indentation and formatting. - var newNodeAfterInsertion = newRoot.GetAnnotatedNodes(s_replacementNodeAnnotation).Single(); + // 4. Format the new node so that the inserted braces/blocks would have correct indentation and formatting. + var newNodeAfterInsertion = newRoot.GetAnnotatedNodes(s_replacementNodeAnnotation).Single(); - var formattedNewRoot = Formatter.Format( - newRoot, - newNodeAfterInsertion.Span, - services, - formattingOptions, - cancellationToken); + var formattedNewRoot = Formatter.Format( + newRoot, + newNodeAfterInsertion.Span, + services, + formattingOptions, + cancellationToken); - // 4. Use the annotation to find the end of the open brace, it would be the new caret position - var nextCaretPosition = formattedNewRoot.GetAnnotatedTokens(s_openBracePositionAnnotation).Single().Span.End; - return (formattedNewRoot, nextCaretPosition); - } + // 4. Use the annotation to find the end of the open brace, it would be the new caret position + var nextCaretPosition = formattedNewRoot.GetAnnotatedTokens(s_openBracePositionAnnotation).Single().Span.End; + return (formattedNewRoot, nextCaretPosition); + } - private static SyntaxNode ReplaceNodeAndFormat( - SolutionServices services, - SyntaxNode root, - SyntaxNode oldNode, - SyntaxNode newNode, - SyntaxFormattingOptions formattingOptions, - CancellationToken cancellationToken) - { - // 1. Tag the new node so that it could be found later. - var annotatedNewNode = newNode.WithAdditionalAnnotations(s_replacementNodeAnnotation); + private static SyntaxNode ReplaceNodeAndFormat( + SolutionServices services, + SyntaxNode root, + SyntaxNode oldNode, + SyntaxNode newNode, + SyntaxFormattingOptions formattingOptions, + CancellationToken cancellationToken) + { + // 1. Tag the new node so that it could be found later. + var annotatedNewNode = newNode.WithAdditionalAnnotations(s_replacementNodeAnnotation); + + // 2. Replace the old node with newNode. (new node is the node with correct braces) + var newRoot = root.ReplaceNode( + oldNode, + annotatedNewNode); + + // 3. Find the newNode in the new syntax root. + var newNodeAfterInsertion = newRoot.GetAnnotatedNodes(s_replacementNodeAnnotation).Single(); + + // 4. Format the new node so that the inserted braces/blocks would have correct indentation and formatting. + var formattedNewRoot = Formatter.Format( + newRoot, + newNodeAfterInsertion.Span, + services, + formattingOptions, + cancellationToken); + + return formattedNewRoot; + } - // 2. Replace the old node with newNode. (new node is the node with correct braces) - var newRoot = root.ReplaceNode( - oldNode, - annotatedNewNode); + #endregion - // 3. Find the newNode in the new syntax root. - var newNodeAfterInsertion = newRoot.GetAnnotatedNodes(s_replacementNodeAnnotation).Single(); + #region EmbeddedStatementModificationHelpers - // 4. Format the new node so that the inserted braces/blocks would have correct indentation and formatting. - var formattedNewRoot = Formatter.Format( - newRoot, - newNodeAfterInsertion.Span, + private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToEmbeddedStatementOwner( + SolutionServices services, + SyntaxNode root, + SyntaxNode embeddedStatementOwner, + SyntaxFormattingOptions formattingOptions, + CancellationToken cancellationToken) + { + // If there is no inner statement, just add an empty block to it. + // e.g. + // class Bar + // { + // if (true)$$ + // } + // => + // class Bar + // { + // if (true) + // { + // } + // } + var statement = embeddedStatementOwner.GetEmbeddedStatement(); + if (statement == null || statement.IsMissing) + { + var newRoot = ReplaceNodeAndFormat( services, + root, + embeddedStatementOwner, + WithBraces(embeddedStatementOwner, formattingOptions), formattingOptions, cancellationToken); - return formattedNewRoot; + // Locate the open brace token, and move the caret after it. + var nextCaretPosition = GetOpenBraceSpanEnd(newRoot); + return (newRoot, nextCaretPosition); } - #endregion - - #region EmbeddedStatementModificationHelpers + // There is an inner statement, it needs to be handled differently in addition to adding the block, - private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToEmbeddedStatementOwner( - SolutionServices services, - SyntaxNode root, - SyntaxNode embeddedStatementOwner, - SyntaxFormattingOptions formattingOptions, - CancellationToken cancellationToken) + // For while, ForEach, Lock and Using statement, + // If there is an statement in the embeddedStatementOwner, + // move the old statement next to the statementOwner, + // and insert a empty block into the statementOwner, + // e.g. + // before: + // whi$$le(true) + // var i = 1; + // for this case 'var i = 1;' is thought as the inner statement, + // + // after: + // while(true) + // { + // $$ + // } + // var i = 1; + return embeddedStatementOwner switch { - // If there is no inner statement, just add an empty block to it. - // e.g. - // class Bar - // { - // if (true)$$ - // } - // => - // class Bar - // { - // if (true) - // { - // } - // } - var statement = embeddedStatementOwner.GetEmbeddedStatement(); - if (statement == null || statement.IsMissing) - { - var newRoot = ReplaceNodeAndFormat( - services, - root, - embeddedStatementOwner, - WithBraces(embeddedStatementOwner, formattingOptions), - formattingOptions, - cancellationToken); - - // Locate the open brace token, and move the caret after it. - var nextCaretPosition = GetOpenBraceSpanEnd(newRoot); - return (newRoot, nextCaretPosition); - } - - // There is an inner statement, it needs to be handled differently in addition to adding the block, - - // For while, ForEach, Lock and Using statement, - // If there is an statement in the embeddedStatementOwner, - // move the old statement next to the statementOwner, - // and insert a empty block into the statementOwner, - // e.g. - // before: - // whi$$le(true) - // var i = 1; - // for this case 'var i = 1;' is thought as the inner statement, - // - // after: - // while(true) - // { - // $$ - // } - // var i = 1; - return embeddedStatementOwner switch - { - WhileStatementSyntax or ForEachStatementSyntax or ForStatementSyntax or LockStatementSyntax or UsingStatementSyntax - => ReplaceStatementOwnerAndInsertStatement( - services, - root, - oldNode: embeddedStatementOwner, - newNode: AddBlockToEmbeddedStatementOwner(embeddedStatementOwner, formattingOptions), - anchorNode: embeddedStatementOwner, - nodesToInsert: ImmutableArray.Empty.Add(statement), - formattingOptions, - cancellationToken), - DoStatementSyntax doStatementNode => AddBraceToDoStatement(services, root, doStatementNode, formattingOptions, statement, cancellationToken), - IfStatementSyntax ifStatementNode => AddBraceToIfStatement(services, root, ifStatementNode, formattingOptions, statement, cancellationToken), - ElseClauseSyntax elseClauseNode => AddBraceToElseClause(services, root, elseClauseNode, formattingOptions, statement, cancellationToken), - _ => throw ExceptionUtilities.UnexpectedValue(embeddedStatementOwner), - }; - } + WhileStatementSyntax or ForEachStatementSyntax or ForStatementSyntax or LockStatementSyntax or UsingStatementSyntax + => ReplaceStatementOwnerAndInsertStatement( + services, + root, + oldNode: embeddedStatementOwner, + newNode: AddBlockToEmbeddedStatementOwner(embeddedStatementOwner, formattingOptions), + anchorNode: embeddedStatementOwner, + nodesToInsert: ImmutableArray.Empty.Add(statement), + formattingOptions, + cancellationToken), + DoStatementSyntax doStatementNode => AddBraceToDoStatement(services, root, doStatementNode, formattingOptions, statement, cancellationToken), + IfStatementSyntax ifStatementNode => AddBraceToIfStatement(services, root, ifStatementNode, formattingOptions, statement, cancellationToken), + ElseClauseSyntax elseClauseNode => AddBraceToElseClause(services, root, elseClauseNode, formattingOptions, statement, cancellationToken), + _ => throw ExceptionUtilities.UnexpectedValue(embeddedStatementOwner), + }; + } - private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToDoStatement( - SolutionServices services, - SyntaxNode root, - DoStatementSyntax doStatementNode, - SyntaxFormattingOptions formattingOptions, - StatementSyntax innerStatement, - CancellationToken cancellationToken) + private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToDoStatement( + SolutionServices services, + SyntaxNode root, + DoStatementSyntax doStatementNode, + SyntaxFormattingOptions formattingOptions, + StatementSyntax innerStatement, + CancellationToken cancellationToken) + { + // If this do statement doesn't end with the 'while' parts + // e.g: + // before: + // d$$o + // Print("hello"); + // after: + // do + // { + // $$ + // } + // Print("hello"); + if (doStatementNode.WhileKeyword.IsMissing + && doStatementNode.SemicolonToken.IsMissing + && doStatementNode.OpenParenToken.IsMissing + && doStatementNode.CloseParenToken.IsMissing) { - // If this do statement doesn't end with the 'while' parts - // e.g: - // before: - // d$$o - // Print("hello"); - // after: - // do - // { - // $$ - // } - // Print("hello"); - if (doStatementNode.WhileKeyword.IsMissing - && doStatementNode.SemicolonToken.IsMissing - && doStatementNode.OpenParenToken.IsMissing - && doStatementNode.CloseParenToken.IsMissing) - { - return ReplaceStatementOwnerAndInsertStatement( - services, - root, - oldNode: doStatementNode, - newNode: AddBlockToEmbeddedStatementOwner(doStatementNode, formattingOptions), - anchorNode: doStatementNode, - nodesToInsert: ImmutableArray.Empty.Add(innerStatement), - formattingOptions, - cancellationToken); - } - - // if the do statement has 'while' as an end - // e.g: - // before: - // d$$o - // Print("hello"); - // while (true); - // after: - // do - // { - // $$ - // Print("hello"); - // } while(true); - var newRoot = ReplaceNodeAndFormat( + return ReplaceStatementOwnerAndInsertStatement( services, root, - doStatementNode, - AddBlockToEmbeddedStatementOwner(doStatementNode, formattingOptions, innerStatement), + oldNode: doStatementNode, + newNode: AddBlockToEmbeddedStatementOwner(doStatementNode, formattingOptions), + anchorNode: doStatementNode, + nodesToInsert: ImmutableArray.Empty.Add(innerStatement), formattingOptions, cancellationToken); - - var nextCaretPosition = GetOpenBraceSpanEnd(newRoot); - return (newRoot, nextCaretPosition); } - private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToIfStatement( - SolutionServices services, - SyntaxNode root, - IfStatementSyntax ifStatementNode, - SyntaxFormattingOptions formattingOptions, - StatementSyntax innerStatement, - CancellationToken cancellationToken) - { - // This ifStatement doesn't have an else clause, and its parent is a Block. - // Insert the innerStatement next to the ifStatement - // e.g. - // if ($$a) - // Print(); - // => - // if (a) - // { - // $$ - // } - // Print(); - if (ifStatementNode.Else == null && ifStatementNode.Parent is BlockSyntax) - { - return ReplaceStatementOwnerAndInsertStatement( - services, - root, - ifStatementNode, - AddBlockToEmbeddedStatementOwner(ifStatementNode, formattingOptions), - ifStatementNode, - ImmutableArray.Empty.Add(innerStatement), - formattingOptions, - cancellationToken); - } + // if the do statement has 'while' as an end + // e.g: + // before: + // d$$o + // Print("hello"); + // while (true); + // after: + // do + // { + // $$ + // Print("hello"); + // } while(true); + var newRoot = ReplaceNodeAndFormat( + services, + root, + doStatementNode, + AddBlockToEmbeddedStatementOwner(doStatementNode, formattingOptions, innerStatement), + formattingOptions, + cancellationToken); + + var nextCaretPosition = GetOpenBraceSpanEnd(newRoot); + return (newRoot, nextCaretPosition); + } - // If this IfStatement has an else statement after - // e.g. - // before: - // if $$(true) - // print("Hello"); - // else {} - // after: - // if (true) - // { - // $$ - // print("Hello"); - // } - // else {} - var newRoot = ReplaceNodeAndFormat( + private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToIfStatement( + SolutionServices services, + SyntaxNode root, + IfStatementSyntax ifStatementNode, + SyntaxFormattingOptions formattingOptions, + StatementSyntax innerStatement, + CancellationToken cancellationToken) + { + // This ifStatement doesn't have an else clause, and its parent is a Block. + // Insert the innerStatement next to the ifStatement + // e.g. + // if ($$a) + // Print(); + // => + // if (a) + // { + // $$ + // } + // Print(); + if (ifStatementNode.Else == null && ifStatementNode.Parent is BlockSyntax) + { + return ReplaceStatementOwnerAndInsertStatement( services, root, ifStatementNode, - AddBlockToEmbeddedStatementOwner(ifStatementNode, formattingOptions, innerStatement), + AddBlockToEmbeddedStatementOwner(ifStatementNode, formattingOptions), + ifStatementNode, + ImmutableArray.Empty.Add(innerStatement), formattingOptions, cancellationToken); - - var nextCaretPosition = GetOpenBraceSpanEnd(newRoot); - return (newRoot, nextCaretPosition); } - private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToElseClause( - SolutionServices services, - SyntaxNode root, - ElseClauseSyntax elseClauseNode, - SyntaxFormattingOptions formattingOptions, - StatementSyntax innerStatement, - CancellationToken cancellationToken) - { - // If this is an 'els$$e if(true)' statement, - // then treat it as the selected node is the nested if statement - if (elseClauseNode.Statement is IfStatementSyntax) - { - return AddBraceToEmbeddedStatementOwner(services, root, elseClauseNode.Statement, formattingOptions, cancellationToken); - } + // If this IfStatement has an else statement after + // e.g. + // before: + // if $$(true) + // print("Hello"); + // else {} + // after: + // if (true) + // { + // $$ + // print("Hello"); + // } + // else {} + var newRoot = ReplaceNodeAndFormat( + services, + root, + ifStatementNode, + AddBlockToEmbeddedStatementOwner(ifStatementNode, formattingOptions, innerStatement), + formattingOptions, + cancellationToken); + + var nextCaretPosition = GetOpenBraceSpanEnd(newRoot); + return (newRoot, nextCaretPosition); + } - // Otherwise, it is just an ending else clause. - // if its parent is an ifStatement and the parent of ifStatement is a block, insert the innerStatement after the ifStatement - // e.g. before: - // if (true) - // { - // } els$$e - // Print(); - // after: - // if (true) - // { - // } els$$e - // { - // $$ - // } - // Print(); - if (elseClauseNode.Parent is IfStatementSyntax { Parent: BlockSyntax }) - { - return ReplaceStatementOwnerAndInsertStatement( - services, - root, - elseClauseNode, - WithBraces(elseClauseNode, formattingOptions), - elseClauseNode.Parent!, - ImmutableArray.Empty.Add(innerStatement), - formattingOptions, - cancellationToken); - } + private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToElseClause( + SolutionServices services, + SyntaxNode root, + ElseClauseSyntax elseClauseNode, + SyntaxFormattingOptions formattingOptions, + StatementSyntax innerStatement, + CancellationToken cancellationToken) + { + // If this is an 'els$$e if(true)' statement, + // then treat it as the selected node is the nested if statement + if (elseClauseNode.Statement is IfStatementSyntax) + { + return AddBraceToEmbeddedStatementOwner(services, root, elseClauseNode.Statement, formattingOptions, cancellationToken); + } - // For all the other cases, - // Put the innerStatement into the block - // e.g. - // if (a) - // if (true) - // { - // } - // else - // { - // $$ - // Print(); - // } - // => - // if (a) - // if (true) - // { - // } - // els$$e - // Print(); - var formattedNewRoot = ReplaceNodeAndFormat( + // Otherwise, it is just an ending else clause. + // if its parent is an ifStatement and the parent of ifStatement is a block, insert the innerStatement after the ifStatement + // e.g. before: + // if (true) + // { + // } els$$e + // Print(); + // after: + // if (true) + // { + // } els$$e + // { + // $$ + // } + // Print(); + if (elseClauseNode.Parent is IfStatementSyntax { Parent: BlockSyntax }) + { + return ReplaceStatementOwnerAndInsertStatement( services, root, elseClauseNode, - AddBlockToEmbeddedStatementOwner(elseClauseNode, formattingOptions, innerStatement), + WithBraces(elseClauseNode, formattingOptions), + elseClauseNode.Parent!, + ImmutableArray.Empty.Add(innerStatement), formattingOptions, cancellationToken); - - var nextCaretPosition = formattedNewRoot.GetAnnotatedTokens(s_openBracePositionAnnotation).Single().Span.End; - return (formattedNewRoot, nextCaretPosition); } - #endregion + // For all the other cases, + // Put the innerStatement into the block + // e.g. + // if (a) + // if (true) + // { + // } + // else + // { + // $$ + // Print(); + // } + // => + // if (a) + // if (true) + // { + // } + // els$$e + // Print(); + var formattedNewRoot = ReplaceNodeAndFormat( + services, + root, + elseClauseNode, + AddBlockToEmbeddedStatementOwner(elseClauseNode, formattingOptions, innerStatement), + formattingOptions, + cancellationToken); + + var nextCaretPosition = formattedNewRoot.GetAnnotatedTokens(s_openBracePositionAnnotation).Single().Span.End; + return (formattedNewRoot, nextCaretPosition); + } + + #endregion - #region ObjectCreationExpressionModificationHelpers + #region ObjectCreationExpressionModificationHelpers - private static (SyntaxNode newNode, SyntaxNode oldNode) ModifyObjectCreationExpressionNode( - BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode, - bool addOrRemoveInitializer, - SyntaxFormattingOptions formattingOptions) + private static (SyntaxNode newNode, SyntaxNode oldNode) ModifyObjectCreationExpressionNode( + BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode, + bool addOrRemoveInitializer, + SyntaxFormattingOptions formattingOptions) + { + // 1. Add '()' after the type or new keyword. + // e.g. + // case 1: 'var c = new Bar' becomes 'var c = new Bar()' + // case 2: 'Bar b = new' becomes 'Bar b = new()' + var objectCreationNodeWithArgumentList = WithArgumentListIfNeeded(baseObjectCreationExpressionNode); + + // 2. Add or remove initializer + // e.g. var c = new Bar() => var c = new Bar() { } + var objectCreationNodeWithCorrectInitializer = addOrRemoveInitializer + ? WithBraces(objectCreationNodeWithArgumentList, formattingOptions) + : WithoutBraces(objectCreationNodeWithArgumentList); + + // 3. Handle the semicolon. + // If the next token is a semicolon, e.g. + // var l = new Ba$$r() { } => var l = new Ba$$r() { }; + var nextToken = baseObjectCreationExpressionNode.GetLastToken(includeZeroWidth: true).GetNextToken(includeZeroWidth: true); + if (nextToken.IsKind(SyntaxKind.SemicolonToken) + && nextToken.Parent != null + && nextToken.Parent.Contains(baseObjectCreationExpressionNode)) { - // 1. Add '()' after the type or new keyword. + var objectCreationNodeContainer = nextToken.Parent; + // Replace the old object creation node and add the semicolon token. + // Note: need to move the trailing trivia of the objectCreationExpressionNode after the semicolon token // e.g. - // case 1: 'var c = new Bar' becomes 'var c = new Bar()' - // case 2: 'Bar b = new' becomes 'Bar b = new()' - var objectCreationNodeWithArgumentList = WithArgumentListIfNeeded(baseObjectCreationExpressionNode); - - // 2. Add or remove initializer - // e.g. var c = new Bar() => var c = new Bar() { } - var objectCreationNodeWithCorrectInitializer = addOrRemoveInitializer - ? WithBraces(objectCreationNodeWithArgumentList, formattingOptions) - : WithoutBraces(objectCreationNodeWithArgumentList); - - // 3. Handle the semicolon. - // If the next token is a semicolon, e.g. - // var l = new Ba$$r() { } => var l = new Ba$$r() { }; - var nextToken = baseObjectCreationExpressionNode.GetLastToken(includeZeroWidth: true).GetNextToken(includeZeroWidth: true); - if (nextToken.IsKind(SyntaxKind.SemicolonToken) - && nextToken.Parent != null - && nextToken.Parent.Contains(baseObjectCreationExpressionNode)) - { - var objectCreationNodeContainer = nextToken.Parent; - // Replace the old object creation node and add the semicolon token. - // Note: need to move the trailing trivia of the objectCreationExpressionNode after the semicolon token - // e.g. - // var l = new Bar() {} // I am some comments - // => - // var l = new Bar() {}; // I am some comments - var replacementContainerNode = objectCreationNodeContainer.ReplaceSyntax( - nodes: SpecializedCollections.SingletonCollection(baseObjectCreationExpressionNode), - (_, _) => objectCreationNodeWithCorrectInitializer.WithoutTrailingTrivia(), - tokens: SpecializedCollections.SingletonCollection(nextToken), - computeReplacementToken: (_, _) => - SyntaxFactory.Token(SyntaxKind.SemicolonToken).WithTrailingTrivia(objectCreationNodeWithCorrectInitializer.GetTrailingTrivia()), - trivia: [], - computeReplacementTrivia: (_, syntaxTrivia) => syntaxTrivia); - return (replacementContainerNode, objectCreationNodeContainer); - } - else - { - // No need to change the semicolon, just return the objectCreationExpression with correct initializer - return (objectCreationNodeWithCorrectInitializer, baseObjectCreationExpressionNode); - } + // var l = new Bar() {} // I am some comments + // => + // var l = new Bar() {}; // I am some comments + var replacementContainerNode = objectCreationNodeContainer.ReplaceSyntax( + nodes: SpecializedCollections.SingletonCollection(baseObjectCreationExpressionNode), + (_, _) => objectCreationNodeWithCorrectInitializer.WithoutTrailingTrivia(), + tokens: SpecializedCollections.SingletonCollection(nextToken), + computeReplacementToken: (_, _) => + SyntaxFactory.Token(SyntaxKind.SemicolonToken).WithTrailingTrivia(objectCreationNodeWithCorrectInitializer.GetTrailingTrivia()), + trivia: [], + computeReplacementTrivia: (_, syntaxTrivia) => syntaxTrivia); + return (replacementContainerNode, objectCreationNodeContainer); } - - /// - /// Add argument list to the objectCreationExpression if needed. - /// e.g. new Bar; => new Bar(); - /// - private static BaseObjectCreationExpressionSyntax WithArgumentListIfNeeded(BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode) + else { - var argumentList = baseObjectCreationExpressionNode.ArgumentList; - if (argumentList is { IsMissing: false }) - { - return baseObjectCreationExpressionNode; - } + // No need to change the semicolon, just return the objectCreationExpression with correct initializer + return (objectCreationNodeWithCorrectInitializer, baseObjectCreationExpressionNode); + } + } - RoslynDebug.Assert(!baseObjectCreationExpressionNode.NewKeyword.IsMissing); - if (baseObjectCreationExpressionNode is ObjectCreationExpressionSyntax objectCreationExpressionNode) - { - var typeNode = objectCreationExpressionNode.Type; - if (typeNode.IsMissing) - { - // There is only 'new' keyword in the object creation expression. Treat it as an ImplicitObjectCreationExpression. - // This could happen because when only type 'new', parser would think it is an ObjectCreationExpression. - var newKeywordToken = baseObjectCreationExpressionNode.NewKeyword; - var newArgumentList = SyntaxFactory.ArgumentList().WithTrailingTrivia(newKeywordToken.TrailingTrivia); - return SyntaxFactory.ImplicitObjectCreationExpression(newKeywordToken.WithoutTrailingTrivia(), newArgumentList, baseObjectCreationExpressionNode.Initializer); - } - else - { - // Make sure the trailing trivia is passed to the argument list - // like var l = new List\r\n => - // var l = new List()\r\r - var newArgumentList = SyntaxFactory.ArgumentList().WithTrailingTrivia(typeNode.GetTrailingTrivia()); - var newTypeNode = typeNode.WithoutTrivia(); - return objectCreationExpressionNode.WithType(newTypeNode).WithArgumentList(newArgumentList); - } - } + /// + /// Add argument list to the objectCreationExpression if needed. + /// e.g. new Bar; => new Bar(); + /// + private static BaseObjectCreationExpressionSyntax WithArgumentListIfNeeded(BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode) + { + var argumentList = baseObjectCreationExpressionNode.ArgumentList; + if (argumentList is { IsMissing: false }) + { + return baseObjectCreationExpressionNode; + } - if (baseObjectCreationExpressionNode is ImplicitObjectCreationExpressionSyntax implicitObjectCreationExpressionNode) + RoslynDebug.Assert(!baseObjectCreationExpressionNode.NewKeyword.IsMissing); + if (baseObjectCreationExpressionNode is ObjectCreationExpressionSyntax objectCreationExpressionNode) + { + var typeNode = objectCreationExpressionNode.Type; + if (typeNode.IsMissing) { - var newKeywordToken = implicitObjectCreationExpressionNode.NewKeyword; + // There is only 'new' keyword in the object creation expression. Treat it as an ImplicitObjectCreationExpression. + // This could happen because when only type 'new', parser would think it is an ObjectCreationExpression. + var newKeywordToken = baseObjectCreationExpressionNode.NewKeyword; var newArgumentList = SyntaxFactory.ArgumentList().WithTrailingTrivia(newKeywordToken.TrailingTrivia); return SyntaxFactory.ImplicitObjectCreationExpression(newKeywordToken.WithoutTrailingTrivia(), newArgumentList, baseObjectCreationExpressionNode.Initializer); } + else + { + // Make sure the trailing trivia is passed to the argument list + // like var l = new List\r\n => + // var l = new List()\r\r + var newArgumentList = SyntaxFactory.ArgumentList().WithTrailingTrivia(typeNode.GetTrailingTrivia()); + var newTypeNode = typeNode.WithoutTrivia(); + return objectCreationExpressionNode.WithType(newTypeNode).WithArgumentList(newArgumentList); + } + } - RoslynDebug.Assert(false, $"New derived type of {nameof(BaseObjectCreationExpressionSyntax)} is added"); - return baseObjectCreationExpressionNode; + if (baseObjectCreationExpressionNode is ImplicitObjectCreationExpressionSyntax implicitObjectCreationExpressionNode) + { + var newKeywordToken = implicitObjectCreationExpressionNode.NewKeyword; + var newArgumentList = SyntaxFactory.ArgumentList().WithTrailingTrivia(newKeywordToken.TrailingTrivia); + return SyntaxFactory.ImplicitObjectCreationExpression(newKeywordToken.WithoutTrailingTrivia(), newArgumentList, baseObjectCreationExpressionNode.Initializer); } - #endregion + RoslynDebug.Assert(false, $"New derived type of {nameof(BaseObjectCreationExpressionSyntax)} is added"); + return baseObjectCreationExpressionNode; + } - #region ShouldAddBraceCheck + #endregion - private static bool ShouldAddBraces(SyntaxNode node, int caretPosition) - => node switch - { - NamespaceDeclarationSyntax namespaceDeclarationNode => ShouldAddBraceForNamespaceDeclaration(namespaceDeclarationNode, caretPosition), - BaseTypeDeclarationSyntax baseTypeDeclarationNode => ShouldAddBraceForBaseTypeDeclaration(baseTypeDeclarationNode, caretPosition), - BaseMethodDeclarationSyntax baseMethodDeclarationNode => ShouldAddBraceForBaseMethodDeclaration(baseMethodDeclarationNode, caretPosition), - LocalFunctionStatementSyntax localFunctionStatementNode => ShouldAddBraceForLocalFunctionStatement(localFunctionStatementNode, caretPosition), - ObjectCreationExpressionSyntax objectCreationExpressionNode => ShouldAddBraceForObjectCreationExpression(objectCreationExpressionNode), - BaseFieldDeclarationSyntax baseFieldDeclarationNode => ShouldAddBraceForBaseFieldDeclaration(baseFieldDeclarationNode), - AccessorDeclarationSyntax accessorDeclarationNode => ShouldAddBraceForAccessorDeclaration(accessorDeclarationNode), - IndexerDeclarationSyntax indexerDeclarationNode => ShouldAddBraceForIndexerDeclaration(indexerDeclarationNode, caretPosition), - SwitchStatementSyntax switchStatementNode => ShouldAddBraceForSwitchStatement(switchStatementNode), - TryStatementSyntax tryStatementNode => ShouldAddBraceForTryStatement(tryStatementNode, caretPosition), - CatchClauseSyntax catchClauseNode => ShouldAddBraceForCatchClause(catchClauseNode, caretPosition), - FinallyClauseSyntax finallyClauseNode => ShouldAddBraceForFinallyClause(finallyClauseNode, caretPosition), - DoStatementSyntax doStatementNode => ShouldAddBraceForDoStatement(doStatementNode, caretPosition), - CommonForEachStatementSyntax commonForEachStatementNode => ShouldAddBraceForCommonForEachStatement(commonForEachStatementNode, caretPosition), - ForStatementSyntax forStatementNode => ShouldAddBraceForForStatement(forStatementNode, caretPosition), - IfStatementSyntax ifStatementNode => ShouldAddBraceForIfStatement(ifStatementNode, caretPosition), - ElseClauseSyntax elseClauseNode => ShouldAddBraceForElseClause(elseClauseNode, caretPosition), - LockStatementSyntax lockStatementNode => ShouldAddBraceForLockStatement(lockStatementNode, caretPosition), - UsingStatementSyntax usingStatementNode => ShouldAddBraceForUsingStatement(usingStatementNode, caretPosition), - WhileStatementSyntax whileStatementNode => ShouldAddBraceForWhileStatement(whileStatementNode, caretPosition), - CheckedStatementSyntax checkedStatementNode => ShouldAddBraceForCheckedStatement(checkedStatementNode, caretPosition), - _ => false, - }; - - /// - /// For namespace, make sure it has name there is no braces - /// - private static bool ShouldAddBraceForNamespaceDeclaration(NamespaceDeclarationSyntax namespaceDeclarationNode, int caretPosition) - => !namespaceDeclarationNode.Name.IsMissing - && HasNoBrace(namespaceDeclarationNode) - && !WithinAttributeLists(namespaceDeclarationNode, caretPosition) - && !WithinBraces(namespaceDeclarationNode, caretPosition); - - /// - /// For class/struct/enum ..., make sure it has name and there is no braces. - /// - private static bool ShouldAddBraceForBaseTypeDeclaration(BaseTypeDeclarationSyntax baseTypeDeclarationNode, int caretPosition) - => !baseTypeDeclarationNode.Identifier.IsMissing - && HasNoBrace(baseTypeDeclarationNode) - && !WithinAttributeLists(baseTypeDeclarationNode, caretPosition) - && !WithinBraces(baseTypeDeclarationNode, caretPosition); - - /// - /// For method, make sure it has a ParameterList, because later braces would be inserted after the Parameterlist - /// - private static bool ShouldAddBraceForBaseMethodDeclaration(BaseMethodDeclarationSyntax baseMethodDeclarationNode, int caretPosition) - => baseMethodDeclarationNode.ExpressionBody == null - && baseMethodDeclarationNode.Body == null - && !baseMethodDeclarationNode.ParameterList.IsMissing - && baseMethodDeclarationNode.SemicolonToken.IsMissing - && !WithinAttributeLists(baseMethodDeclarationNode, caretPosition) - && !WithinMethodBody(baseMethodDeclarationNode, caretPosition) - // Make sure we don't insert braces for method in Interface. - && !baseMethodDeclarationNode.IsParentKind(SyntaxKind.InterfaceDeclaration); - - /// - /// For local Function, make sure it has a ParameterList, because later braces would be inserted after the Parameterlist - /// - private static bool ShouldAddBraceForLocalFunctionStatement(LocalFunctionStatementSyntax localFunctionStatementNode, int caretPosition) - => localFunctionStatementNode.ExpressionBody == null - && localFunctionStatementNode.Body == null - && !localFunctionStatementNode.ParameterList.IsMissing - && !WithinAttributeLists(localFunctionStatementNode, caretPosition) - && !WithinMethodBody(localFunctionStatementNode, caretPosition); - - /// - /// Add brace for ObjectCreationExpression if it doesn't have initializer - /// - private static bool ShouldAddBraceForObjectCreationExpression(ObjectCreationExpressionSyntax objectCreationExpressionNode) - => objectCreationExpressionNode.Initializer == null; - - /// - /// Add braces for field and event field if they only have one variable, semicolon is missing and don't have readonly keyword - /// Example: - /// public int Bar$$ => - /// public int Bar - /// { - /// $$ - /// } - /// This would change field to property, and change event field to event declaration. - /// - private static bool ShouldAddBraceForBaseFieldDeclaration(BaseFieldDeclarationSyntax baseFieldDeclarationNode) - => baseFieldDeclarationNode is { Declaration.Variables: [{ Initializer: null }], SemicolonToken.IsMissing: true } - && !baseFieldDeclarationNode.Modifiers.Any(SyntaxKind.ReadOnlyKeyword); - - private static bool ShouldAddBraceForAccessorDeclaration(AccessorDeclarationSyntax accessorDeclarationNode) + #region ShouldAddBraceCheck + + private static bool ShouldAddBraces(SyntaxNode node, int caretPosition) + => node switch { - if (accessorDeclarationNode.Body == null - && accessorDeclarationNode.ExpressionBody == null - && accessorDeclarationNode.SemicolonToken.IsMissing) + NamespaceDeclarationSyntax namespaceDeclarationNode => ShouldAddBraceForNamespaceDeclaration(namespaceDeclarationNode, caretPosition), + BaseTypeDeclarationSyntax baseTypeDeclarationNode => ShouldAddBraceForBaseTypeDeclaration(baseTypeDeclarationNode, caretPosition), + BaseMethodDeclarationSyntax baseMethodDeclarationNode => ShouldAddBraceForBaseMethodDeclaration(baseMethodDeclarationNode, caretPosition), + LocalFunctionStatementSyntax localFunctionStatementNode => ShouldAddBraceForLocalFunctionStatement(localFunctionStatementNode, caretPosition), + ObjectCreationExpressionSyntax objectCreationExpressionNode => ShouldAddBraceForObjectCreationExpression(objectCreationExpressionNode), + BaseFieldDeclarationSyntax baseFieldDeclarationNode => ShouldAddBraceForBaseFieldDeclaration(baseFieldDeclarationNode), + AccessorDeclarationSyntax accessorDeclarationNode => ShouldAddBraceForAccessorDeclaration(accessorDeclarationNode), + IndexerDeclarationSyntax indexerDeclarationNode => ShouldAddBraceForIndexerDeclaration(indexerDeclarationNode, caretPosition), + SwitchStatementSyntax switchStatementNode => ShouldAddBraceForSwitchStatement(switchStatementNode), + TryStatementSyntax tryStatementNode => ShouldAddBraceForTryStatement(tryStatementNode, caretPosition), + CatchClauseSyntax catchClauseNode => ShouldAddBraceForCatchClause(catchClauseNode, caretPosition), + FinallyClauseSyntax finallyClauseNode => ShouldAddBraceForFinallyClause(finallyClauseNode, caretPosition), + DoStatementSyntax doStatementNode => ShouldAddBraceForDoStatement(doStatementNode, caretPosition), + CommonForEachStatementSyntax commonForEachStatementNode => ShouldAddBraceForCommonForEachStatement(commonForEachStatementNode, caretPosition), + ForStatementSyntax forStatementNode => ShouldAddBraceForForStatement(forStatementNode, caretPosition), + IfStatementSyntax ifStatementNode => ShouldAddBraceForIfStatement(ifStatementNode, caretPosition), + ElseClauseSyntax elseClauseNode => ShouldAddBraceForElseClause(elseClauseNode, caretPosition), + LockStatementSyntax lockStatementNode => ShouldAddBraceForLockStatement(lockStatementNode, caretPosition), + UsingStatementSyntax usingStatementNode => ShouldAddBraceForUsingStatement(usingStatementNode, caretPosition), + WhileStatementSyntax whileStatementNode => ShouldAddBraceForWhileStatement(whileStatementNode, caretPosition), + CheckedStatementSyntax checkedStatementNode => ShouldAddBraceForCheckedStatement(checkedStatementNode, caretPosition), + _ => false, + }; + + /// + /// For namespace, make sure it has name there is no braces + /// + private static bool ShouldAddBraceForNamespaceDeclaration(NamespaceDeclarationSyntax namespaceDeclarationNode, int caretPosition) + => !namespaceDeclarationNode.Name.IsMissing + && HasNoBrace(namespaceDeclarationNode) + && !WithinAttributeLists(namespaceDeclarationNode, caretPosition) + && !WithinBraces(namespaceDeclarationNode, caretPosition); + + /// + /// For class/struct/enum ..., make sure it has name and there is no braces. + /// + private static bool ShouldAddBraceForBaseTypeDeclaration(BaseTypeDeclarationSyntax baseTypeDeclarationNode, int caretPosition) + => !baseTypeDeclarationNode.Identifier.IsMissing + && HasNoBrace(baseTypeDeclarationNode) + && !WithinAttributeLists(baseTypeDeclarationNode, caretPosition) + && !WithinBraces(baseTypeDeclarationNode, caretPosition); + + /// + /// For method, make sure it has a ParameterList, because later braces would be inserted after the Parameterlist + /// + private static bool ShouldAddBraceForBaseMethodDeclaration(BaseMethodDeclarationSyntax baseMethodDeclarationNode, int caretPosition) + => baseMethodDeclarationNode.ExpressionBody == null + && baseMethodDeclarationNode.Body == null + && !baseMethodDeclarationNode.ParameterList.IsMissing + && baseMethodDeclarationNode.SemicolonToken.IsMissing + && !WithinAttributeLists(baseMethodDeclarationNode, caretPosition) + && !WithinMethodBody(baseMethodDeclarationNode, caretPosition) + // Make sure we don't insert braces for method in Interface. + && !baseMethodDeclarationNode.IsParentKind(SyntaxKind.InterfaceDeclaration); + + /// + /// For local Function, make sure it has a ParameterList, because later braces would be inserted after the Parameterlist + /// + private static bool ShouldAddBraceForLocalFunctionStatement(LocalFunctionStatementSyntax localFunctionStatementNode, int caretPosition) + => localFunctionStatementNode.ExpressionBody == null + && localFunctionStatementNode.Body == null + && !localFunctionStatementNode.ParameterList.IsMissing + && !WithinAttributeLists(localFunctionStatementNode, caretPosition) + && !WithinMethodBody(localFunctionStatementNode, caretPosition); + + /// + /// Add brace for ObjectCreationExpression if it doesn't have initializer + /// + private static bool ShouldAddBraceForObjectCreationExpression(ObjectCreationExpressionSyntax objectCreationExpressionNode) + => objectCreationExpressionNode.Initializer == null; + + /// + /// Add braces for field and event field if they only have one variable, semicolon is missing and don't have readonly keyword + /// Example: + /// public int Bar$$ => + /// public int Bar + /// { + /// $$ + /// } + /// This would change field to property, and change event field to event declaration. + /// + private static bool ShouldAddBraceForBaseFieldDeclaration(BaseFieldDeclarationSyntax baseFieldDeclarationNode) + => baseFieldDeclarationNode is { Declaration.Variables: [{ Initializer: null }], SemicolonToken.IsMissing: true } + && !baseFieldDeclarationNode.Modifiers.Any(SyntaxKind.ReadOnlyKeyword); + + private static bool ShouldAddBraceForAccessorDeclaration(AccessorDeclarationSyntax accessorDeclarationNode) + { + if (accessorDeclarationNode.Body == null + && accessorDeclarationNode.ExpressionBody == null + && accessorDeclarationNode.SemicolonToken.IsMissing) + { + // If the accessor doesn't have body, expression body and semicolon, let's check this case + // for both event and property, + // e.g. + // int Bar + // { + // get; + // se$$t + // } + // because if the getter doesn't have a body then setter also shouldn't have any body. + // Don't check for indexer because the accessor for indexer should have body. + var parent = accessorDeclarationNode.Parent; + var parentOfParent = parent?.Parent; + if (parent is AccessorListSyntax accessorListNode + && parentOfParent is PropertyDeclarationSyntax) { - // If the accessor doesn't have body, expression body and semicolon, let's check this case - // for both event and property, - // e.g. - // int Bar - // { - // get; - // se$$t - // } - // because if the getter doesn't have a body then setter also shouldn't have any body. - // Don't check for indexer because the accessor for indexer should have body. - var parent = accessorDeclarationNode.Parent; - var parentOfParent = parent?.Parent; - if (parent is AccessorListSyntax accessorListNode - && parentOfParent is PropertyDeclarationSyntax) + var otherAccessors = accessorListNode.Accessors + .Except([accessorDeclarationNode]) + .ToImmutableArray(); + if (!otherAccessors.IsEmpty) { - var otherAccessors = accessorListNode.Accessors - .Except([accessorDeclarationNode]) - .ToImmutableArray(); - if (!otherAccessors.IsEmpty) - { - return !otherAccessors.Any( - static accessor => accessor.Body == null - && accessor.ExpressionBody == null - && !accessor.SemicolonToken.IsMissing); - } + return !otherAccessors.Any( + static accessor => accessor.Body == null + && accessor.ExpressionBody == null + && !accessor.SemicolonToken.IsMissing); } - - return true; } - return false; + return true; } - /// - /// For indexer, switch, try and catch syntax node without braces, if it is the last child of its parent, it would - /// use its parent's close brace as its own. - /// Example: - /// class Bar - /// { - /// int th$$is[int i] - /// } - /// In this case, parser would think the last '}' belongs to the indexer, not the class. - /// Therefore, only check if the open brace is missing for these 4 types of SyntaxNode - /// - private static bool ShouldAddBraceForIndexerDeclaration(IndexerDeclarationSyntax indexerDeclarationNode, int caretPosition) - { - if (WithinAttributeLists(indexerDeclarationNode, caretPosition) || - WithinBraces(indexerDeclarationNode.AccessorList, caretPosition)) - { - return false; - } - - // Make sure it has brackets - var (openBracket, closeBracket) = indexerDeclarationNode.ParameterList.GetBrackets(); - if (openBracket.IsMissing || closeBracket.IsMissing) - { - return false; - } - - // If both accessorList and body is empty - if ((indexerDeclarationNode.AccessorList == null || indexerDeclarationNode.AccessorList.IsMissing) - && indexerDeclarationNode.ExpressionBody == null) - { - return true; - } + return false; + } - return indexerDeclarationNode.AccessorList != null - && indexerDeclarationNode.AccessorList.OpenBraceToken.IsMissing; + /// + /// For indexer, switch, try and catch syntax node without braces, if it is the last child of its parent, it would + /// use its parent's close brace as its own. + /// Example: + /// class Bar + /// { + /// int th$$is[int i] + /// } + /// In this case, parser would think the last '}' belongs to the indexer, not the class. + /// Therefore, only check if the open brace is missing for these 4 types of SyntaxNode + /// + private static bool ShouldAddBraceForIndexerDeclaration(IndexerDeclarationSyntax indexerDeclarationNode, int caretPosition) + { + if (WithinAttributeLists(indexerDeclarationNode, caretPosition) || + WithinBraces(indexerDeclarationNode.AccessorList, caretPosition)) + { + return false; } - // For the Switch, Try, Catch, Finally node - // e.g. - // class Bar - // { - // void Main() - // { - // tr$$y - // } - // } - // In this case, the last close brace of 'void Main()' would be thought as a part of the try statement, - // and the last close brace of 'Bar' would be thought as a part of Main() - // So for these case, just check if the open brace is missing. - private static bool ShouldAddBraceForSwitchStatement(SwitchStatementSyntax switchStatementNode) - => !switchStatementNode.SwitchKeyword.IsMissing && switchStatementNode.OpenBraceToken.IsMissing; - - private static bool ShouldAddBraceForTryStatement(TryStatementSyntax tryStatementNode, int caretPosition) - => !tryStatementNode.TryKeyword.IsMissing - && tryStatementNode.Block.OpenBraceToken.IsMissing - && !tryStatementNode.Block.Span.Contains(caretPosition); - - private static bool ShouldAddBraceForCatchClause(CatchClauseSyntax catchClauseSyntax, int caretPosition) - => !catchClauseSyntax.CatchKeyword.IsMissing - && catchClauseSyntax.Block.OpenBraceToken.IsMissing - && !catchClauseSyntax.Block.Span.Contains(caretPosition); - - private static bool ShouldAddBraceForFinallyClause(FinallyClauseSyntax finallyClauseNode, int caretPosition) - => !finallyClauseNode.FinallyKeyword.IsMissing - && finallyClauseNode.Block.OpenBraceToken.IsMissing - && !finallyClauseNode.Block.Span.Contains(caretPosition); - - private static bool ShouldAddBraceForCheckedStatement(CheckedStatementSyntax checkedStatementNode, int caretPosition) - => checkedStatementNode.Block.OpenBraceToken.IsMissing - && !checkedStatementNode.Block.Span.Contains(caretPosition); - - // For all the embeddedStatementOwners, - // if the embeddedStatement is not block, insert the the braces if its statement is not block. - private static bool ShouldAddBraceForDoStatement(DoStatementSyntax doStatementNode, int caretPosition) - => !doStatementNode.DoKeyword.IsMissing - && doStatementNode.Statement is not BlockSyntax - && doStatementNode.DoKeyword.FullSpan.Contains(caretPosition); - - private static bool ShouldAddBraceForCommonForEachStatement(CommonForEachStatementSyntax commonForEachStatementNode, int caretPosition) - => commonForEachStatementNode.Statement is not BlockSyntax - && !commonForEachStatementNode.OpenParenToken.IsMissing - && !commonForEachStatementNode.CloseParenToken.IsMissing - && !WithinEmbeddedStatement(commonForEachStatementNode, caretPosition); - - private static bool ShouldAddBraceForForStatement(ForStatementSyntax forStatementNode, int caretPosition) - => forStatementNode.Statement is not BlockSyntax - && !forStatementNode.OpenParenToken.IsMissing - && !forStatementNode.CloseParenToken.IsMissing - && !WithinEmbeddedStatement(forStatementNode, caretPosition); - - private static bool ShouldAddBraceForIfStatement(IfStatementSyntax ifStatementNode, int caretPosition) - => ifStatementNode.Statement is not BlockSyntax - && !ifStatementNode.OpenParenToken.IsMissing - && !ifStatementNode.CloseParenToken.IsMissing - && !WithinEmbeddedStatement(ifStatementNode, caretPosition); - - private static bool ShouldAddBraceForElseClause(ElseClauseSyntax elseClauseNode, int caretPosition) + // Make sure it has brackets + var (openBracket, closeBracket) = indexerDeclarationNode.ParameterList.GetBrackets(); + if (openBracket.IsMissing || closeBracket.IsMissing) { - // In case it is an else-if clause, if the statement is IfStatement, use its insertion statement - // otherwise, use the end of the else keyword - // Example: - // Before: if (a) - // { - // } else i$$f (b) - // After: if (a) - // { - // } else if (b) - // { - // $$ - // } - if (elseClauseNode.Statement is IfStatementSyntax ifStatementNode) - { - return ShouldAddBraceForIfStatement(ifStatementNode, caretPosition); - } - else - { - // Here it should be an elseClause - // like: - // if (a) - // { - // } els$$e { - // } - // So only check the its statement - return elseClauseNode.Statement is not BlockSyntax && !WithinEmbeddedStatement(elseClauseNode, caretPosition); - } + return false; } - private static bool ShouldAddBraceForLockStatement(LockStatementSyntax lockStatementNode, int caretPosition) - => lockStatementNode.Statement is not BlockSyntax - && !lockStatementNode.OpenParenToken.IsMissing - && !lockStatementNode.CloseParenToken.IsMissing - && !WithinEmbeddedStatement(lockStatementNode, caretPosition); - - private static bool ShouldAddBraceForUsingStatement(UsingStatementSyntax usingStatementNode, int caretPosition) - => usingStatementNode.Statement is not BlockSyntax - && !usingStatementNode.OpenParenToken.IsMissing - && !usingStatementNode.CloseParenToken.IsMissing - && !WithinEmbeddedStatement(usingStatementNode, caretPosition); - - private static bool ShouldAddBraceForWhileStatement(WhileStatementSyntax whileStatementNode, int caretPosition) - => whileStatementNode.Statement is not BlockSyntax - && !whileStatementNode.OpenParenToken.IsMissing - && !whileStatementNode.CloseParenToken.IsMissing - && !WithinEmbeddedStatement(whileStatementNode, caretPosition); - - private static bool WithinAttributeLists(SyntaxNode node, int caretPosition) + // If both accessorList and body is empty + if ((indexerDeclarationNode.AccessorList == null || indexerDeclarationNode.AccessorList.IsMissing) + && indexerDeclarationNode.ExpressionBody == null) { - var attributeLists = node.GetAttributeLists(); - return attributeLists.Span.Contains(caretPosition); + return true; } - private static bool WithinBraces(SyntaxNode? node, int caretPosition) + return indexerDeclarationNode.AccessorList != null + && indexerDeclarationNode.AccessorList.OpenBraceToken.IsMissing; + } + + // For the Switch, Try, Catch, Finally node + // e.g. + // class Bar + // { + // void Main() + // { + // tr$$y + // } + // } + // In this case, the last close brace of 'void Main()' would be thought as a part of the try statement, + // and the last close brace of 'Bar' would be thought as a part of Main() + // So for these case, just check if the open brace is missing. + private static bool ShouldAddBraceForSwitchStatement(SwitchStatementSyntax switchStatementNode) + => !switchStatementNode.SwitchKeyword.IsMissing && switchStatementNode.OpenBraceToken.IsMissing; + + private static bool ShouldAddBraceForTryStatement(TryStatementSyntax tryStatementNode, int caretPosition) + => !tryStatementNode.TryKeyword.IsMissing + && tryStatementNode.Block.OpenBraceToken.IsMissing + && !tryStatementNode.Block.Span.Contains(caretPosition); + + private static bool ShouldAddBraceForCatchClause(CatchClauseSyntax catchClauseSyntax, int caretPosition) + => !catchClauseSyntax.CatchKeyword.IsMissing + && catchClauseSyntax.Block.OpenBraceToken.IsMissing + && !catchClauseSyntax.Block.Span.Contains(caretPosition); + + private static bool ShouldAddBraceForFinallyClause(FinallyClauseSyntax finallyClauseNode, int caretPosition) + => !finallyClauseNode.FinallyKeyword.IsMissing + && finallyClauseNode.Block.OpenBraceToken.IsMissing + && !finallyClauseNode.Block.Span.Contains(caretPosition); + + private static bool ShouldAddBraceForCheckedStatement(CheckedStatementSyntax checkedStatementNode, int caretPosition) + => checkedStatementNode.Block.OpenBraceToken.IsMissing + && !checkedStatementNode.Block.Span.Contains(caretPosition); + + // For all the embeddedStatementOwners, + // if the embeddedStatement is not block, insert the the braces if its statement is not block. + private static bool ShouldAddBraceForDoStatement(DoStatementSyntax doStatementNode, int caretPosition) + => !doStatementNode.DoKeyword.IsMissing + && doStatementNode.Statement is not BlockSyntax + && doStatementNode.DoKeyword.FullSpan.Contains(caretPosition); + + private static bool ShouldAddBraceForCommonForEachStatement(CommonForEachStatementSyntax commonForEachStatementNode, int caretPosition) + => commonForEachStatementNode.Statement is not BlockSyntax + && !commonForEachStatementNode.OpenParenToken.IsMissing + && !commonForEachStatementNode.CloseParenToken.IsMissing + && !WithinEmbeddedStatement(commonForEachStatementNode, caretPosition); + + private static bool ShouldAddBraceForForStatement(ForStatementSyntax forStatementNode, int caretPosition) + => forStatementNode.Statement is not BlockSyntax + && !forStatementNode.OpenParenToken.IsMissing + && !forStatementNode.CloseParenToken.IsMissing + && !WithinEmbeddedStatement(forStatementNode, caretPosition); + + private static bool ShouldAddBraceForIfStatement(IfStatementSyntax ifStatementNode, int caretPosition) + => ifStatementNode.Statement is not BlockSyntax + && !ifStatementNode.OpenParenToken.IsMissing + && !ifStatementNode.CloseParenToken.IsMissing + && !WithinEmbeddedStatement(ifStatementNode, caretPosition); + + private static bool ShouldAddBraceForElseClause(ElseClauseSyntax elseClauseNode, int caretPosition) + { + // In case it is an else-if clause, if the statement is IfStatement, use its insertion statement + // otherwise, use the end of the else keyword + // Example: + // Before: if (a) + // { + // } else i$$f (b) + // After: if (a) + // { + // } else if (b) + // { + // $$ + // } + if (elseClauseNode.Statement is IfStatementSyntax ifStatementNode) { - var (openBrace, closeBrace) = node.GetBraces(); - return TextSpan.FromBounds(openBrace.SpanStart, closeBrace.Span.End).Contains(caretPosition); + return ShouldAddBraceForIfStatement(ifStatementNode, caretPosition); } - - private static bool WithinMethodBody(SyntaxNode node, int caretPosition) + else { - if (node is BaseMethodDeclarationSyntax { Body: { } baseMethodBody }) - { - return baseMethodBody.Span.Contains(caretPosition); - } + // Here it should be an elseClause + // like: + // if (a) + // { + // } els$$e { + // } + // So only check the its statement + return elseClauseNode.Statement is not BlockSyntax && !WithinEmbeddedStatement(elseClauseNode, caretPosition); + } + } - if (node is LocalFunctionStatementSyntax { Body: { } localFunctionBody }) - { - return localFunctionBody.Span.Contains(caretPosition); - } + private static bool ShouldAddBraceForLockStatement(LockStatementSyntax lockStatementNode, int caretPosition) + => lockStatementNode.Statement is not BlockSyntax + && !lockStatementNode.OpenParenToken.IsMissing + && !lockStatementNode.CloseParenToken.IsMissing + && !WithinEmbeddedStatement(lockStatementNode, caretPosition); + + private static bool ShouldAddBraceForUsingStatement(UsingStatementSyntax usingStatementNode, int caretPosition) + => usingStatementNode.Statement is not BlockSyntax + && !usingStatementNode.OpenParenToken.IsMissing + && !usingStatementNode.CloseParenToken.IsMissing + && !WithinEmbeddedStatement(usingStatementNode, caretPosition); + + private static bool ShouldAddBraceForWhileStatement(WhileStatementSyntax whileStatementNode, int caretPosition) + => whileStatementNode.Statement is not BlockSyntax + && !whileStatementNode.OpenParenToken.IsMissing + && !whileStatementNode.CloseParenToken.IsMissing + && !WithinEmbeddedStatement(whileStatementNode, caretPosition); + + private static bool WithinAttributeLists(SyntaxNode node, int caretPosition) + { + var attributeLists = node.GetAttributeLists(); + return attributeLists.Span.Contains(caretPosition); + } - return false; + private static bool WithinBraces(SyntaxNode? node, int caretPosition) + { + var (openBrace, closeBrace) = node.GetBraces(); + return TextSpan.FromBounds(openBrace.SpanStart, closeBrace.Span.End).Contains(caretPosition); + } + + private static bool WithinMethodBody(SyntaxNode node, int caretPosition) + { + if (node is BaseMethodDeclarationSyntax { Body: { } baseMethodBody }) + { + return baseMethodBody.Span.Contains(caretPosition); } - private static bool HasNoBrace(SyntaxNode node) + if (node is LocalFunctionStatementSyntax { Body: { } localFunctionBody }) { - var (openBrace, closeBrace) = node.GetBraces(); - return openBrace.IsKind(SyntaxKind.None) && closeBrace.IsKind(SyntaxKind.None) - || openBrace.IsMissing && closeBrace.IsMissing; + return localFunctionBody.Span.Contains(caretPosition); } - private static bool WithinEmbeddedStatement(SyntaxNode node, int caretPosition) - => node.GetEmbeddedStatement()?.Span.Contains(caretPosition) ?? false; + return false; + } - #endregion + private static bool HasNoBrace(SyntaxNode node) + { + var (openBrace, closeBrace) = node.GetBraces(); + return openBrace.IsKind(SyntaxKind.None) && closeBrace.IsKind(SyntaxKind.None) + || openBrace.IsMissing && closeBrace.IsMissing; + } - #region ShouldRemoveBraceCheck + private static bool WithinEmbeddedStatement(SyntaxNode node, int caretPosition) + => node.GetEmbeddedStatement()?.Span.Contains(caretPosition) ?? false; - private static bool ShouldRemoveBraces(SyntaxNode node, int caretPosition) - => node switch - { - BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode => ShouldRemoveBraceForObjectCreationExpression(baseObjectCreationExpressionNode), - AccessorDeclarationSyntax accessorDeclarationNode => ShouldRemoveBraceForAccessorDeclaration(accessorDeclarationNode, caretPosition), - PropertyDeclarationSyntax propertyDeclarationNode => ShouldRemoveBraceForPropertyDeclaration(propertyDeclarationNode, caretPosition), - EventDeclarationSyntax eventDeclarationNode => ShouldRemoveBraceForEventDeclaration(eventDeclarationNode, caretPosition), - _ => false, - }; - - /// - /// Remove the braces if the BaseObjectCreationExpression has an empty Initializer. - /// - private static bool ShouldRemoveBraceForObjectCreationExpression(BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode) - { - var initializer = baseObjectCreationExpressionNode.Initializer; - return initializer != null && initializer.Expressions.IsEmpty(); - } + #endregion - // Only do this when it is an accessor in property - // Since it is illegal to have something like - // int this[int i] { get; set;} - // event EventHandler Bar {add; remove;} - private static bool ShouldRemoveBraceForAccessorDeclaration(AccessorDeclarationSyntax accessorDeclarationNode, int caretPosition) - => accessorDeclarationNode.Body != null - && accessorDeclarationNode.Body.Statements.IsEmpty() - && accessorDeclarationNode.ExpressionBody == null - && accessorDeclarationNode.Parent != null - && accessorDeclarationNode.Parent.IsParentKind(SyntaxKind.PropertyDeclaration) - && accessorDeclarationNode.Body.Span.Contains(caretPosition); - - private static bool ShouldRemoveBraceForPropertyDeclaration(PropertyDeclarationSyntax propertyDeclarationNode, int caretPosition) - { - // If a property just has an empty accessorList, like - // int i $${ } - // then remove the braces and change it to a field - // int i; - if (propertyDeclarationNode.AccessorList != null - && propertyDeclarationNode.ExpressionBody == null) - { - var accessorList = propertyDeclarationNode.AccessorList; - return accessorList.Span.Contains(caretPosition) && accessorList.Accessors.IsEmpty(); - } + #region ShouldRemoveBraceCheck - return false; - } + private static bool ShouldRemoveBraces(SyntaxNode node, int caretPosition) + => node switch + { + BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode => ShouldRemoveBraceForObjectCreationExpression(baseObjectCreationExpressionNode), + AccessorDeclarationSyntax accessorDeclarationNode => ShouldRemoveBraceForAccessorDeclaration(accessorDeclarationNode, caretPosition), + PropertyDeclarationSyntax propertyDeclarationNode => ShouldRemoveBraceForPropertyDeclaration(propertyDeclarationNode, caretPosition), + EventDeclarationSyntax eventDeclarationNode => ShouldRemoveBraceForEventDeclaration(eventDeclarationNode, caretPosition), + _ => false, + }; + + /// + /// Remove the braces if the BaseObjectCreationExpression has an empty Initializer. + /// + private static bool ShouldRemoveBraceForObjectCreationExpression(BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode) + { + var initializer = baseObjectCreationExpressionNode.Initializer; + return initializer != null && initializer.Expressions.IsEmpty(); + } - private static bool ShouldRemoveBraceForEventDeclaration(EventDeclarationSyntax eventDeclarationNode, int caretPosition) + // Only do this when it is an accessor in property + // Since it is illegal to have something like + // int this[int i] { get; set;} + // event EventHandler Bar {add; remove;} + private static bool ShouldRemoveBraceForAccessorDeclaration(AccessorDeclarationSyntax accessorDeclarationNode, int caretPosition) + => accessorDeclarationNode.Body != null + && accessorDeclarationNode.Body.Statements.IsEmpty() + && accessorDeclarationNode.ExpressionBody == null + && accessorDeclarationNode.Parent != null + && accessorDeclarationNode.Parent.IsParentKind(SyntaxKind.PropertyDeclaration) + && accessorDeclarationNode.Body.Span.Contains(caretPosition); + + private static bool ShouldRemoveBraceForPropertyDeclaration(PropertyDeclarationSyntax propertyDeclarationNode, int caretPosition) + { + // If a property just has an empty accessorList, like + // int i $${ } + // then remove the braces and change it to a field + // int i; + if (propertyDeclarationNode.AccessorList != null + && propertyDeclarationNode.ExpressionBody == null) { - // If an event declaration just has an empty accessorList, - // like - // event EventHandler e$$ { } - // then change it to a event field declaration - // event EventHandler e; - var accessorList = eventDeclarationNode.AccessorList; - return accessorList != null - && accessorList.Span.Contains(caretPosition) - && accessorList.Accessors.IsEmpty(); + var accessorList = propertyDeclarationNode.AccessorList; + return accessorList.Span.Contains(caretPosition) && accessorList.Accessors.IsEmpty(); } - #endregion + return false; + } + + private static bool ShouldRemoveBraceForEventDeclaration(EventDeclarationSyntax eventDeclarationNode, int caretPosition) + { + // If an event declaration just has an empty accessorList, + // like + // event EventHandler e$$ { } + // then change it to a event field declaration + // event EventHandler e; + var accessorList = eventDeclarationNode.AccessorList; + return accessorList != null + && accessorList.Span.Contains(caretPosition) + && accessorList.Accessors.IsEmpty(); + } - #region AddBrace + #endregion - private static AccessorListSyntax GetAccessorListNode(SyntaxFormattingOptions formattingOptions) - => SyntaxFactory.AccessorList().WithOpenBraceToken(GetOpenBrace(formattingOptions)).WithCloseBraceToken(GetCloseBrace(formattingOptions)); + #region AddBrace - private static InitializerExpressionSyntax GetInitializerExpressionNode(SyntaxFormattingOptions formattingOptions) - => SyntaxFactory.InitializerExpression(SyntaxKind.ObjectInitializerExpression) - .WithOpenBraceToken(GetOpenBrace(formattingOptions)); + private static AccessorListSyntax GetAccessorListNode(SyntaxFormattingOptions formattingOptions) + => SyntaxFactory.AccessorList().WithOpenBraceToken(GetOpenBrace(formattingOptions)).WithCloseBraceToken(GetCloseBrace(formattingOptions)); - private static BlockSyntax GetBlockNode(SyntaxFormattingOptions formattingOptions) - => SyntaxFactory.Block().WithOpenBraceToken(GetOpenBrace(formattingOptions)).WithCloseBraceToken(GetCloseBrace(formattingOptions)); + private static InitializerExpressionSyntax GetInitializerExpressionNode(SyntaxFormattingOptions formattingOptions) + => SyntaxFactory.InitializerExpression(SyntaxKind.ObjectInitializerExpression) + .WithOpenBraceToken(GetOpenBrace(formattingOptions)); - private static SyntaxToken GetOpenBrace(SyntaxFormattingOptions formattingOptions) - => SyntaxFactory.Token( - leading: SyntaxTriviaList.Empty, - kind: SyntaxKind.OpenBraceToken, - trailing: [GetNewLineTrivia(formattingOptions)]) - .WithAdditionalAnnotations(s_openBracePositionAnnotation); + private static BlockSyntax GetBlockNode(SyntaxFormattingOptions formattingOptions) + => SyntaxFactory.Block().WithOpenBraceToken(GetOpenBrace(formattingOptions)).WithCloseBraceToken(GetCloseBrace(formattingOptions)); - private static SyntaxToken GetCloseBrace(SyntaxFormattingOptions formattingOptions) - => SyntaxFactory.Token( + private static SyntaxToken GetOpenBrace(SyntaxFormattingOptions formattingOptions) + => SyntaxFactory.Token( leading: SyntaxTriviaList.Empty, - kind: SyntaxKind.CloseBraceToken, - trailing: [GetNewLineTrivia(formattingOptions)]); + kind: SyntaxKind.OpenBraceToken, + trailing: [GetNewLineTrivia(formattingOptions)]) + .WithAdditionalAnnotations(s_openBracePositionAnnotation); - private static SyntaxTrivia GetNewLineTrivia(SyntaxFormattingOptions formattingOptions) - { - var newLineString = formattingOptions.NewLine; - return SyntaxFactory.EndOfLine(newLineString); - } + private static SyntaxToken GetCloseBrace(SyntaxFormattingOptions formattingOptions) + => SyntaxFactory.Token( + leading: SyntaxTriviaList.Empty, + kind: SyntaxKind.CloseBraceToken, + trailing: [GetNewLineTrivia(formattingOptions)]); - /// - /// Add braces to the . - /// For FieldDeclaration and EventFieldDeclaration, it will change them to PropertyDeclaration and EventDeclaration - /// - private static SyntaxNode WithBraces(SyntaxNode node, SyntaxFormattingOptions formattingOptions) - => node switch - { - BaseTypeDeclarationSyntax baseTypeDeclarationNode => WithBracesForBaseTypeDeclaration(baseTypeDeclarationNode, formattingOptions), - BaseObjectCreationExpressionSyntax objectCreationExpressionNode => GetObjectCreationExpressionWithInitializer(objectCreationExpressionNode, formattingOptions), - BaseMethodDeclarationSyntax baseMethodDeclarationNode => AddBlockToBaseMethodDeclaration(baseMethodDeclarationNode, formattingOptions), - LocalFunctionStatementSyntax localFunctionStatementNode => AddBlockToLocalFunctionDeclaration(localFunctionStatementNode, formattingOptions), - AccessorDeclarationSyntax accessorDeclarationNode => AddBlockToAccessorDeclaration(accessorDeclarationNode, formattingOptions), - _ when node.IsEmbeddedStatementOwner() => AddBlockToEmbeddedStatementOwner(node, formattingOptions), - _ => throw ExceptionUtilities.UnexpectedValue(node), - }; - - /// - /// Add braces to . - /// - private static BaseTypeDeclarationSyntax WithBracesForBaseTypeDeclaration( - BaseTypeDeclarationSyntax baseTypeDeclarationNode, - SyntaxFormattingOptions formattingOptions) - => baseTypeDeclarationNode.WithOpenBraceToken(GetOpenBrace(formattingOptions)) - .WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken)); - - /// - /// Add an empty initializer to . - /// - private static BaseObjectCreationExpressionSyntax GetObjectCreationExpressionWithInitializer( - BaseObjectCreationExpressionSyntax objectCreationExpressionNode, - SyntaxFormattingOptions formattingOptions) - => objectCreationExpressionNode.WithInitializer(GetInitializerExpressionNode(formattingOptions)); - - /// - /// Add an empty block to . - /// - private static BaseMethodDeclarationSyntax AddBlockToBaseMethodDeclaration( - BaseMethodDeclarationSyntax baseMethodDeclarationNode, - SyntaxFormattingOptions formattingOptions) - => baseMethodDeclarationNode.WithBody(GetBlockNode(formattingOptions)) - // When the method declaration with no body is parsed, it has an invisible trailing semicolon. Make sure it is removed. - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)); - - /// - /// Add an empty block to . - /// - private static LocalFunctionStatementSyntax AddBlockToLocalFunctionDeclaration( - LocalFunctionStatementSyntax localFunctionStatementNode, - SyntaxFormattingOptions formattingOptions) - => localFunctionStatementNode.WithBody(GetBlockNode(formattingOptions)) - // When the local method declaration with no body is parsed, it has an invisible trailing semicolon. Make sure it is removed. - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)); - - /// - /// Add an empty block to . - /// - private static AccessorDeclarationSyntax AddBlockToAccessorDeclaration( - AccessorDeclarationSyntax accessorDeclarationNode, - SyntaxFormattingOptions formattingOptions) - => accessorDeclarationNode.WithBody(GetBlockNode(formattingOptions)) - // When the accessor with no body is parsed, it has an invisible trailing semicolon. Make sure it is removed. - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)); - - /// - /// Add a block with to - /// - private static SyntaxNode AddBlockToEmbeddedStatementOwner( - SyntaxNode embeddedStatementOwner, - SyntaxFormattingOptions formattingOptions, - StatementSyntax? extraNodeInsertedBetweenBraces = null) + private static SyntaxTrivia GetNewLineTrivia(SyntaxFormattingOptions formattingOptions) + { + var newLineString = formattingOptions.NewLine; + return SyntaxFactory.EndOfLine(newLineString); + } + + /// + /// Add braces to the . + /// For FieldDeclaration and EventFieldDeclaration, it will change them to PropertyDeclaration and EventDeclaration + /// + private static SyntaxNode WithBraces(SyntaxNode node, SyntaxFormattingOptions formattingOptions) + => node switch { - var block = extraNodeInsertedBetweenBraces != null - ? GetBlockNode(formattingOptions).WithStatements(new SyntaxList(extraNodeInsertedBetweenBraces)) - : GetBlockNode(formattingOptions); + BaseTypeDeclarationSyntax baseTypeDeclarationNode => WithBracesForBaseTypeDeclaration(baseTypeDeclarationNode, formattingOptions), + BaseObjectCreationExpressionSyntax objectCreationExpressionNode => GetObjectCreationExpressionWithInitializer(objectCreationExpressionNode, formattingOptions), + BaseMethodDeclarationSyntax baseMethodDeclarationNode => AddBlockToBaseMethodDeclaration(baseMethodDeclarationNode, formattingOptions), + LocalFunctionStatementSyntax localFunctionStatementNode => AddBlockToLocalFunctionDeclaration(localFunctionStatementNode, formattingOptions), + AccessorDeclarationSyntax accessorDeclarationNode => AddBlockToAccessorDeclaration(accessorDeclarationNode, formattingOptions), + _ when node.IsEmbeddedStatementOwner() => AddBlockToEmbeddedStatementOwner(node, formattingOptions), + _ => throw ExceptionUtilities.UnexpectedValue(node), + }; + + /// + /// Add braces to . + /// + private static BaseTypeDeclarationSyntax WithBracesForBaseTypeDeclaration( + BaseTypeDeclarationSyntax baseTypeDeclarationNode, + SyntaxFormattingOptions formattingOptions) + => baseTypeDeclarationNode.WithOpenBraceToken(GetOpenBrace(formattingOptions)) + .WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken)); + + /// + /// Add an empty initializer to . + /// + private static BaseObjectCreationExpressionSyntax GetObjectCreationExpressionWithInitializer( + BaseObjectCreationExpressionSyntax objectCreationExpressionNode, + SyntaxFormattingOptions formattingOptions) + => objectCreationExpressionNode.WithInitializer(GetInitializerExpressionNode(formattingOptions)); + + /// + /// Add an empty block to . + /// + private static BaseMethodDeclarationSyntax AddBlockToBaseMethodDeclaration( + BaseMethodDeclarationSyntax baseMethodDeclarationNode, + SyntaxFormattingOptions formattingOptions) + => baseMethodDeclarationNode.WithBody(GetBlockNode(formattingOptions)) + // When the method declaration with no body is parsed, it has an invisible trailing semicolon. Make sure it is removed. + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)); + + /// + /// Add an empty block to . + /// + private static LocalFunctionStatementSyntax AddBlockToLocalFunctionDeclaration( + LocalFunctionStatementSyntax localFunctionStatementNode, + SyntaxFormattingOptions formattingOptions) + => localFunctionStatementNode.WithBody(GetBlockNode(formattingOptions)) + // When the local method declaration with no body is parsed, it has an invisible trailing semicolon. Make sure it is removed. + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)); + + /// + /// Add an empty block to . + /// + private static AccessorDeclarationSyntax AddBlockToAccessorDeclaration( + AccessorDeclarationSyntax accessorDeclarationNode, + SyntaxFormattingOptions formattingOptions) + => accessorDeclarationNode.WithBody(GetBlockNode(formattingOptions)) + // When the accessor with no body is parsed, it has an invisible trailing semicolon. Make sure it is removed. + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)); + + /// + /// Add a block with to + /// + private static SyntaxNode AddBlockToEmbeddedStatementOwner( + SyntaxNode embeddedStatementOwner, + SyntaxFormattingOptions formattingOptions, + StatementSyntax? extraNodeInsertedBetweenBraces = null) + { + var block = extraNodeInsertedBetweenBraces != null + ? GetBlockNode(formattingOptions).WithStatements(new SyntaxList(extraNodeInsertedBetweenBraces)) + : GetBlockNode(formattingOptions); - return embeddedStatementOwner switch - { - DoStatementSyntax doStatementNode => doStatementNode.WithStatement(block), - ForEachStatementSyntax forEachStatementNode => forEachStatementNode.WithStatement(block), - ForStatementSyntax forStatementNode => forStatementNode.WithStatement(block), - IfStatementSyntax ifStatementNode => ifStatementNode.WithStatement(block), - ElseClauseSyntax elseClauseNode => elseClauseNode.WithStatement(block), - WhileStatementSyntax whileStatementNode => whileStatementNode.WithStatement(block), - UsingStatementSyntax usingStatementNode => usingStatementNode.WithStatement(block), - LockStatementSyntax lockStatementNode => lockStatementNode.WithStatement(block), - _ => throw ExceptionUtilities.UnexpectedValue(embeddedStatementOwner) - }; - } + return embeddedStatementOwner switch + { + DoStatementSyntax doStatementNode => doStatementNode.WithStatement(block), + ForEachStatementSyntax forEachStatementNode => forEachStatementNode.WithStatement(block), + ForStatementSyntax forStatementNode => forStatementNode.WithStatement(block), + IfStatementSyntax ifStatementNode => ifStatementNode.WithStatement(block), + ElseClauseSyntax elseClauseNode => elseClauseNode.WithStatement(block), + WhileStatementSyntax whileStatementNode => whileStatementNode.WithStatement(block), + UsingStatementSyntax usingStatementNode => usingStatementNode.WithStatement(block), + LockStatementSyntax lockStatementNode => lockStatementNode.WithStatement(block), + _ => throw ExceptionUtilities.UnexpectedValue(embeddedStatementOwner) + }; + } - #endregion + #endregion - #region RemoveBrace + #region RemoveBrace - /// - /// Remove the brace for the input syntax node - /// For ObjectCreationExpressionSyntax, it would remove the initializer - /// For PropertyDeclarationSyntax, it would change it to a FieldDeclaration - /// For EventDeclarationSyntax, it would change it to eventFieldDeclaration - /// For Accessor, it would change it to the empty version ending with semicolon. - /// e.g get {} => get; - /// - private static SyntaxNode WithoutBraces(SyntaxNode node) - => node switch - { - BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode => RemoveInitializerForBaseObjectCreationExpression(baseObjectCreationExpressionNode), - PropertyDeclarationSyntax propertyDeclarationNode => ConvertPropertyDeclarationToFieldDeclaration(propertyDeclarationNode), - EventDeclarationSyntax eventDeclarationNode => ConvertEventDeclarationToEventFieldDeclaration(eventDeclarationNode), - AccessorDeclarationSyntax accessorDeclarationNode => RemoveBodyForAccessorDeclarationNode(accessorDeclarationNode), - _ => throw ExceptionUtilities.UnexpectedValue(node), - }; - - /// - /// Remove the initializer for . - /// - private static BaseObjectCreationExpressionSyntax RemoveInitializerForBaseObjectCreationExpression( - BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode) + /// + /// Remove the brace for the input syntax node + /// For ObjectCreationExpressionSyntax, it would remove the initializer + /// For PropertyDeclarationSyntax, it would change it to a FieldDeclaration + /// For EventDeclarationSyntax, it would change it to eventFieldDeclaration + /// For Accessor, it would change it to the empty version ending with semicolon. + /// e.g get {} => get; + /// + private static SyntaxNode WithoutBraces(SyntaxNode node) + => node switch { - var objectCreationNodeWithoutInitializer = baseObjectCreationExpressionNode.WithInitializer(null); - // Filter the non-comments trivia - // e.g. - // Bar(new Foo() // I am some comments - // { - // $$ - // }); - // => - // Bar(new Foo() // I am some comments); - // In this case, 'I am somme comments' has an end of line triva, if not removed, it would make - // the final result becomes - // Bar(new Foo() // I am some comments - // ); - var trivia = objectCreationNodeWithoutInitializer.GetTrailingTrivia().Where(trivia => trivia.IsSingleOrMultiLineComment()); - return objectCreationNodeWithoutInitializer.WithTrailingTrivia(trivia); - } - - /// - /// Convert to fieldDeclaration. - /// - private static FieldDeclarationSyntax ConvertPropertyDeclarationToFieldDeclaration( - PropertyDeclarationSyntax propertyDeclarationNode) - => SyntaxFactory.FieldDeclaration( - propertyDeclarationNode.AttributeLists, - propertyDeclarationNode.Modifiers, - SyntaxFactory.VariableDeclaration( - propertyDeclarationNode.Type, - [SyntaxFactory.VariableDeclarator(propertyDeclarationNode.Identifier)]), - SyntaxFactory.Token(SyntaxKind.SemicolonToken)); - - /// - /// Convert to EventFieldDeclaration. - /// - private static EventFieldDeclarationSyntax ConvertEventDeclarationToEventFieldDeclaration( - EventDeclarationSyntax eventDeclarationNode) - => SyntaxFactory.EventFieldDeclaration( - eventDeclarationNode.AttributeLists, - eventDeclarationNode.Modifiers, - SyntaxFactory.VariableDeclaration( - eventDeclarationNode.Type, - [SyntaxFactory.VariableDeclarator(eventDeclarationNode.Identifier)])); - - /// - /// Remove the body of . - /// - private static AccessorDeclarationSyntax RemoveBodyForAccessorDeclarationNode(AccessorDeclarationSyntax accessorDeclarationNode) - => accessorDeclarationNode - .WithBody(null).WithoutTrailingTrivia().WithSemicolonToken( - SyntaxFactory.Token(SyntaxTriviaList.Empty, SyntaxKind.SemicolonToken, SyntaxTriviaList.Empty)); - - #endregion + BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode => RemoveInitializerForBaseObjectCreationExpression(baseObjectCreationExpressionNode), + PropertyDeclarationSyntax propertyDeclarationNode => ConvertPropertyDeclarationToFieldDeclaration(propertyDeclarationNode), + EventDeclarationSyntax eventDeclarationNode => ConvertEventDeclarationToEventFieldDeclaration(eventDeclarationNode), + AccessorDeclarationSyntax accessorDeclarationNode => RemoveBodyForAccessorDeclarationNode(accessorDeclarationNode), + _ => throw ExceptionUtilities.UnexpectedValue(node), + }; + + /// + /// Remove the initializer for . + /// + private static BaseObjectCreationExpressionSyntax RemoveInitializerForBaseObjectCreationExpression( + BaseObjectCreationExpressionSyntax baseObjectCreationExpressionNode) + { + var objectCreationNodeWithoutInitializer = baseObjectCreationExpressionNode.WithInitializer(null); + // Filter the non-comments trivia + // e.g. + // Bar(new Foo() // I am some comments + // { + // $$ + // }); + // => + // Bar(new Foo() // I am some comments); + // In this case, 'I am somme comments' has an end of line triva, if not removed, it would make + // the final result becomes + // Bar(new Foo() // I am some comments + // ); + var trivia = objectCreationNodeWithoutInitializer.GetTrailingTrivia().Where(trivia => trivia.IsSingleOrMultiLineComment()); + return objectCreationNodeWithoutInitializer.WithTrailingTrivia(trivia); } + + /// + /// Convert to fieldDeclaration. + /// + private static FieldDeclarationSyntax ConvertPropertyDeclarationToFieldDeclaration( + PropertyDeclarationSyntax propertyDeclarationNode) + => SyntaxFactory.FieldDeclaration( + propertyDeclarationNode.AttributeLists, + propertyDeclarationNode.Modifiers, + SyntaxFactory.VariableDeclaration( + propertyDeclarationNode.Type, + [SyntaxFactory.VariableDeclarator(propertyDeclarationNode.Identifier)]), + SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + + /// + /// Convert to EventFieldDeclaration. + /// + private static EventFieldDeclarationSyntax ConvertEventDeclarationToEventFieldDeclaration( + EventDeclarationSyntax eventDeclarationNode) + => SyntaxFactory.EventFieldDeclaration( + eventDeclarationNode.AttributeLists, + eventDeclarationNode.Modifiers, + SyntaxFactory.VariableDeclaration( + eventDeclarationNode.Type, + [SyntaxFactory.VariableDeclarator(eventDeclarationNode.Identifier)])); + + /// + /// Remove the body of . + /// + private static AccessorDeclarationSyntax RemoveBodyForAccessorDeclarationNode(AccessorDeclarationSyntax accessorDeclarationNode) + => accessorDeclarationNode + .WithBody(null).WithoutTrailingTrivia().WithSemicolonToken( + SyntaxFactory.Token(SyntaxTriviaList.Empty, SyntaxKind.SemicolonToken, SyntaxTriviaList.Empty)); + + #endregion } diff --git a/src/EditorFeatures/CSharp/AutomaticCompletion/CSharpBraceCompletionServiceFactory.cs b/src/EditorFeatures/CSharp/AutomaticCompletion/CSharpBraceCompletionServiceFactory.cs index 4adb143c7452b..00c1baaba6125 100644 --- a/src/EditorFeatures/CSharp/AutomaticCompletion/CSharpBraceCompletionServiceFactory.cs +++ b/src/EditorFeatures/CSharp/AutomaticCompletion/CSharpBraceCompletionServiceFactory.cs @@ -9,13 +9,12 @@ using Microsoft.CodeAnalysis.BraceCompletion; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.Editor.CSharp.AutomaticCompletion +namespace Microsoft.CodeAnalysis.Editor.CSharp.AutomaticCompletion; + +[ExportLanguageService(typeof(IBraceCompletionServiceFactory), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpBraceCompletionServiceFactory( + [ImportMany(LanguageNames.CSharp)] IEnumerable braceCompletionServices) : AbstractBraceCompletionServiceFactory(braceCompletionServices) { - [ExportLanguageService(typeof(IBraceCompletionServiceFactory), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpBraceCompletionServiceFactory( - [ImportMany(LanguageNames.CSharp)] IEnumerable braceCompletionServices) : AbstractBraceCompletionServiceFactory(braceCompletionServices) - { - } } diff --git a/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingCommandHandler.cs b/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingCommandHandler.cs index 09c9808fe87f4..9a445d550c844 100644 --- a/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingCommandHandler.cs +++ b/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingCommandHandler.cs @@ -23,297 +23,296 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.BlockCommentEditing +namespace Microsoft.CodeAnalysis.Editor.CSharp.BlockCommentEditing; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(nameof(BlockCommentEditingCommandHandler))] +[Order(After = PredefinedCompletionNames.CompletionCommandHandler)] +internal sealed class BlockCommentEditingCommandHandler : ICommandHandler { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(nameof(BlockCommentEditingCommandHandler))] - [Order(After = PredefinedCompletionNames.CompletionCommandHandler)] - internal sealed class BlockCommentEditingCommandHandler : ICommandHandler + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; + private readonly EditorOptionsService _editorOptionsService; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public BlockCommentEditingCommandHandler( + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService) { - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; - private readonly EditorOptionsService _editorOptionsService; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public BlockCommentEditingCommandHandler( - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService) - { - Contract.ThrowIfNull(undoHistoryRegistry); - Contract.ThrowIfNull(editorOperationsFactoryService); + Contract.ThrowIfNull(undoHistoryRegistry); + Contract.ThrowIfNull(editorOperationsFactoryService); - _undoHistoryRegistry = undoHistoryRegistry; - _editorOperationsFactoryService = editorOperationsFactoryService; - _editorOptionsService = editorOptionsService; - } + _undoHistoryRegistry = undoHistoryRegistry; + _editorOperationsFactoryService = editorOperationsFactoryService; + _editorOptionsService = editorOptionsService; + } - public string DisplayName => EditorFeaturesResources.Block_Comment_Editing; + public string DisplayName => EditorFeaturesResources.Block_Comment_Editing; - public CommandState GetCommandState(ReturnKeyCommandArgs args) - => CommandState.Unspecified; + public CommandState GetCommandState(ReturnKeyCommandArgs args) + => CommandState.Unspecified; - public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) - => TryHandleReturnKey(args.SubjectBuffer, args.TextView, context.OperationContext.UserCancellationToken); + public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) + => TryHandleReturnKey(args.SubjectBuffer, args.TextView, context.OperationContext.UserCancellationToken); - private bool TryHandleReturnKey(ITextBuffer subjectBuffer, ITextView textView, CancellationToken cancellationToken) - { - if (!_editorOptionsService.GlobalOptions.GetOption(BlockCommentEditingOptionsStorage.AutoInsertBlockCommentStartString, LanguageNames.CSharp)) - return false; + private bool TryHandleReturnKey(ITextBuffer subjectBuffer, ITextView textView, CancellationToken cancellationToken) + { + if (!_editorOptionsService.GlobalOptions.GetOption(BlockCommentEditingOptionsStorage.AutoInsertBlockCommentStartString, LanguageNames.CSharp)) + return false; - var caretPosition = textView.GetCaretPoint(subjectBuffer); - if (caretPosition == null) - return false; + var caretPosition = textView.GetCaretPoint(subjectBuffer); + if (caretPosition == null) + return false; - var textToInsert = GetTextToInsert(caretPosition.Value, subjectBuffer, _editorOptionsService, cancellationToken); - if (textToInsert == null) - return false; + var textToInsert = GetTextToInsert(caretPosition.Value, subjectBuffer, _editorOptionsService, cancellationToken); + if (textToInsert == null) + return false; - using var transaction = _undoHistoryRegistry.GetHistory(textView.TextBuffer).CreateTransaction(EditorFeaturesResources.Insert_new_line); + using var transaction = _undoHistoryRegistry.GetHistory(textView.TextBuffer).CreateTransaction(EditorFeaturesResources.Insert_new_line); - var editorOperations = _editorOperationsFactoryService.GetEditorOperations(textView); - editorOperations.ReplaceText(GetReplacementSpan(caretPosition.Value), textToInsert); + var editorOperations = _editorOperationsFactoryService.GetEditorOperations(textView); + editorOperations.ReplaceText(GetReplacementSpan(caretPosition.Value), textToInsert); - transaction.Complete(); - return true; - } + transaction.Complete(); + return true; + } - private static Span GetReplacementSpan(SnapshotPoint caretPosition) + private static Span GetReplacementSpan(SnapshotPoint caretPosition) + { + // We want to replace all the whitespace following the caret. This is standard behavior in VS that + // we want to mimic. + var snapshot = caretPosition.Snapshot; + var start = caretPosition.Position; + var end = caretPosition; + while (end < snapshot.Length && SyntaxFacts.IsWhitespace(end.GetChar()) && !SyntaxFacts.IsNewLine(end.GetChar())) + end = end + 1; + + return Span.FromBounds(start, end); + } + + private static string? GetTextToInsert(SnapshotPoint caretPosition, ITextBuffer buffer, EditorOptionsService editorOptionsService, CancellationToken cancellationToken) + { + var currentLine = caretPosition.GetContainingLine(); + var firstNonWhitespacePosition = currentLine.GetFirstNonWhitespacePosition() ?? -1; + if (firstNonWhitespacePosition == -1) + return null; + + // Do quick textual checks to see if it looks like we're inside a comment. That way we only do the expensive + // syntactic work when necessary. + // + // The line either has to contain `/*` or it has to start with `*`. The former looks like we're starting a + // comment in this line. The latter looks like the continuation of a block comment. + var containsBlockCommentStartString = currentLine.Contains(firstNonWhitespacePosition, "/*", ignoreCase: false); + var startsWithBlockCommentMiddleString = currentLine.StartsWith(firstNonWhitespacePosition, "*", ignoreCase: false); + + if (!containsBlockCommentStartString && + !startsWithBlockCommentMiddleString) { - // We want to replace all the whitespace following the caret. This is standard behavior in VS that - // we want to mimic. - var snapshot = caretPosition.Snapshot; - var start = caretPosition.Position; - var end = caretPosition; - while (end < snapshot.Length && SyntaxFacts.IsWhitespace(end.GetChar()) && !SyntaxFacts.IsNewLine(end.GetChar())) - end = end + 1; - - return Span.FromBounds(start, end); + return null; } - private static string? GetTextToInsert(SnapshotPoint caretPosition, ITextBuffer buffer, EditorOptionsService editorOptionsService, CancellationToken cancellationToken) - { - var currentLine = caretPosition.GetContainingLine(); - var firstNonWhitespacePosition = currentLine.GetFirstNonWhitespacePosition() ?? -1; - if (firstNonWhitespacePosition == -1) - return null; - - // Do quick textual checks to see if it looks like we're inside a comment. That way we only do the expensive - // syntactic work when necessary. - // - // The line either has to contain `/*` or it has to start with `*`. The former looks like we're starting a - // comment in this line. The latter looks like the continuation of a block comment. - var containsBlockCommentStartString = currentLine.Contains(firstNonWhitespacePosition, "/*", ignoreCase: false); - var startsWithBlockCommentMiddleString = currentLine.StartsWith(firstNonWhitespacePosition, "*", ignoreCase: false); - - if (!containsBlockCommentStartString && - !startsWithBlockCommentMiddleString) - { - return null; - } + // Now do more expensive syntactic check to see if we're actually in the block comment. + if (!IsCaretInsideBlockCommentSyntax(caretPosition, buffer, editorOptionsService, out var blockComment, out var newLine, cancellationToken)) + return null; - // Now do more expensive syntactic check to see if we're actually in the block comment. - if (!IsCaretInsideBlockCommentSyntax(caretPosition, buffer, editorOptionsService, out var blockComment, out var newLine, cancellationToken)) - return null; + var textSnapshot = caretPosition.Snapshot; - var textSnapshot = caretPosition.Snapshot; + // Now that we've found the real start of the comment, ensure that it's accurate with our quick textual check. + containsBlockCommentStartString = currentLine.LineNumber == textSnapshot.GetLineFromPosition(blockComment.FullSpan.Start).LineNumber; - // Now that we've found the real start of the comment, ensure that it's accurate with our quick textual check. - containsBlockCommentStartString = currentLine.LineNumber == textSnapshot.GetLineFromPosition(blockComment.FullSpan.Start).LineNumber; + // The whitespace indentation on the line where the block-comment starts. + var commentIndentation = GetCommentIndentation(); - // The whitespace indentation on the line where the block-comment starts. - var commentIndentation = GetCommentIndentation(); + // The whitespace indentation on the current line up to the first non-whitespace char. + var lineIndentation = textSnapshot.GetText(Span.FromBounds( + currentLine.Start, + firstNonWhitespacePosition)); - // The whitespace indentation on the current line up to the first non-whitespace char. - var lineIndentation = textSnapshot.GetText(Span.FromBounds( - currentLine.Start, - firstNonWhitespacePosition)); + var exteriorText = GetExteriorText(); + if (exteriorText == null) + return null; - var exteriorText = GetExteriorText(); - if (exteriorText == null) - return null; + return newLine + exteriorText; - return newLine + exteriorText; + string GetCommentIndentation() + { + var sb = PooledStringBuilder.GetInstance(); - string GetCommentIndentation() + var commentStart = blockComment.FullSpan.Start; + var commentLine = textSnapshot.GetLineFromPosition(commentStart); + for (var i = commentLine.Start.Position; i < commentStart; i++) { - var sb = PooledStringBuilder.GetInstance(); + var ch = textSnapshot[i]; + sb.Builder.Append(ch == '\t' ? ch : ' '); + } - var commentStart = blockComment.FullSpan.Start; - var commentLine = textSnapshot.GetLineFromPosition(commentStart); - for (var i = commentLine.Start.Position; i < commentStart; i++) - { - var ch = textSnapshot[i]; - sb.Builder.Append(ch == '\t' ? ch : ' '); - } + return sb.ToStringAndFree(); + } - return sb.ToStringAndFree(); - } + string? GetExteriorText() + { + if (containsBlockCommentStartString) + return GetExteriorTextAfterBlockCommentStart(); - string? GetExteriorText() - { - if (containsBlockCommentStartString) - return GetExteriorTextAfterBlockCommentStart(); + var startsWithBlockCommentEndString = currentLine.StartsWith(firstNonWhitespacePosition, "*/", ignoreCase: false); + if (startsWithBlockCommentEndString) + return GetExteriorTextBeforeBlockCommentEnd(); - var startsWithBlockCommentEndString = currentLine.StartsWith(firstNonWhitespacePosition, "*/", ignoreCase: false); - if (startsWithBlockCommentEndString) - return GetExteriorTextBeforeBlockCommentEnd(); + if (startsWithBlockCommentMiddleString) + return GetExteriorTextInBlockCommentMiddle(); - if (startsWithBlockCommentMiddleString) - return GetExteriorTextInBlockCommentMiddle(); + return null; + } - return null; + string? GetExteriorTextAfterBlockCommentStart() + { + if (BlockCommentEndsRightAfterCaret(caretPosition)) + { + // /*|*/ + return commentIndentation + " "; } - - string? GetExteriorTextAfterBlockCommentStart() + else if (caretPosition == firstNonWhitespacePosition + 1) { - if (BlockCommentEndsRightAfterCaret(caretPosition)) - { - // /*|*/ - return commentIndentation + " "; - } - else if (caretPosition == firstNonWhitespacePosition + 1) - { - // /|* - return null; // The newline inserted could break the syntax in a way that this handler cannot fix, let's leave it. - } - else - { - // /*| or /* | - // - // In the latter case, keep the whitespace the user has typed. in the former, insert at least one - // space. This is the idiomatic style for C#. - var whitespace = GetWhitespaceBetweenCommentAsteriskAndCaret(); - return commentIndentation + " *" + (whitespace == "" ? " " : whitespace); - } + // /|* + return null; // The newline inserted could break the syntax in a way that this handler cannot fix, let's leave it. } - - string? GetExteriorTextBeforeBlockCommentEnd() + else { - if (BlockCommentEndsRightAfterCaret(caretPosition)) - { - // /* - // |*/ - return commentIndentation + " "; - } - else if (caretPosition == firstNonWhitespacePosition + 1) - { - // *|/ - return lineIndentation + "*"; - } - else - { - // /* - // | */ - return commentIndentation + " "; - } + // /*| or /* | + // + // In the latter case, keep the whitespace the user has typed. in the former, insert at least one + // space. This is the idiomatic style for C#. + var whitespace = GetWhitespaceBetweenCommentAsteriskAndCaret(); + return commentIndentation + " *" + (whitespace == "" ? " " : whitespace); } + } - string? GetExteriorTextInBlockCommentMiddle() + string? GetExteriorTextBeforeBlockCommentEnd() + { + if (BlockCommentEndsRightAfterCaret(caretPosition)) + { + // /* + // |*/ + return commentIndentation + " "; + } + else if (caretPosition == firstNonWhitespacePosition + 1) + { + // *|/ + return lineIndentation + "*"; + } + else { - if (BlockCommentEndsRightAfterCaret(caretPosition)) - { - // *|*/ - return lineIndentation; - } - else if (caretPosition > firstNonWhitespacePosition) - { - // /* - // * - // *| - // - // We don't add a space here. If the user isn't adding spaces at this point, we respect that and - // continue with that style. - return lineIndentation + "*" + GetWhitespaceBetweenCommentAsteriskAndCaret(); - } - else - { - // /* - // | * - return commentIndentation + " "; - } + // /* + // | */ + return commentIndentation + " "; } + } - // Returns the whitespace after the * in either '/*' or just '*' and the caret. - string GetWhitespaceBetweenCommentAsteriskAndCaret() + string? GetExteriorTextInBlockCommentMiddle() + { + if (BlockCommentEndsRightAfterCaret(caretPosition)) { - var currentChar = containsBlockCommentStartString - ? blockComment.FullSpan.Start - : firstNonWhitespacePosition; + // *|*/ + return lineIndentation; + } + else if (caretPosition > firstNonWhitespacePosition) + { + // /* + // * + // *| + // + // We don't add a space here. If the user isn't adding spaces at this point, we respect that and + // continue with that style. + return lineIndentation + "*" + GetWhitespaceBetweenCommentAsteriskAndCaret(); + } + else + { + // /* + // | * + return commentIndentation + " "; + } + } - if (textSnapshot[currentChar] == '/') - currentChar++; + // Returns the whitespace after the * in either '/*' or just '*' and the caret. + string GetWhitespaceBetweenCommentAsteriskAndCaret() + { + var currentChar = containsBlockCommentStartString + ? blockComment.FullSpan.Start + : firstNonWhitespacePosition; - if (textSnapshot[currentChar] == '*') - currentChar++; + if (textSnapshot[currentChar] == '/') + currentChar++; - var start = currentChar; - while (currentChar < caretPosition && SyntaxFacts.IsWhitespace(textSnapshot[currentChar])) - currentChar++; + if (textSnapshot[currentChar] == '*') + currentChar++; - return textSnapshot.GetText(Span.FromBounds(start, currentChar)); - } - } + var start = currentChar; + while (currentChar < caretPosition && SyntaxFacts.IsWhitespace(textSnapshot[currentChar])) + currentChar++; - private static bool BlockCommentEndsRightAfterCaret(SnapshotPoint caretPosition) - { - var snapshot = caretPosition.Snapshot; - return (int)caretPosition + 2 <= snapshot.Length && snapshot.GetText(caretPosition, 2) == "*/"; + return textSnapshot.GetText(Span.FromBounds(start, currentChar)); } + } - public static bool IsCaretInsideBlockCommentSyntax( - SnapshotPoint caretPosition, - ITextBuffer buffer, - EditorOptionsService editorOptionsService, - out SyntaxTrivia trivia, - [NotNullWhen(true)] out string? newLine, - CancellationToken cancellationToken) - { - trivia = default; - newLine = null; + private static bool BlockCommentEndsRightAfterCaret(SnapshotPoint caretPosition) + { + var snapshot = caretPosition.Snapshot; + return (int)caretPosition + 2 <= snapshot.Length && snapshot.GetText(caretPosition, 2) == "*/"; + } - var snapshot = caretPosition.Snapshot; - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return false; + public static bool IsCaretInsideBlockCommentSyntax( + SnapshotPoint caretPosition, + ITextBuffer buffer, + EditorOptionsService editorOptionsService, + out SyntaxTrivia trivia, + [NotNullWhen(true)] out string? newLine, + CancellationToken cancellationToken) + { + trivia = default; + newLine = null; - var syntaxTree = document.GetRequiredSyntaxTreeSynchronously(cancellationToken); - trivia = syntaxTree.FindTriviaAndAdjustForEndOfFile(caretPosition, cancellationToken); + var snapshot = caretPosition.Snapshot; + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return false; - var isBlockComment = trivia.Kind() is SyntaxKind.MultiLineCommentTrivia or SyntaxKind.MultiLineDocumentationCommentTrivia; - if (isBlockComment) - { - newLine = buffer.GetLineFormattingOptions(editorOptionsService, explicitFormat: false).NewLine; + var syntaxTree = document.GetRequiredSyntaxTreeSynchronously(cancellationToken); + trivia = syntaxTree.FindTriviaAndAdjustForEndOfFile(caretPosition, cancellationToken); - var span = trivia.FullSpan; - if (span.Start < caretPosition && caretPosition < span.End) + var isBlockComment = trivia.Kind() is SyntaxKind.MultiLineCommentTrivia or SyntaxKind.MultiLineDocumentationCommentTrivia; + if (isBlockComment) + { + newLine = buffer.GetLineFormattingOptions(editorOptionsService, explicitFormat: false).NewLine; + + var span = trivia.FullSpan; + if (span.Start < caretPosition && caretPosition < span.End) + return true; + + // FindTriviaAndAdjustForEndOfFile always returns something if position is EOF, + // whether or not the result includes the position. + // And the SyntaxTrivia for block comments always ends on EOF, closed or not. + // So we need to handle + // /**/|EOF + // and + // /* |EOF + if (caretPosition == snapshot.Length) + { + if (span.Length < "/**/".Length) return true; - // FindTriviaAndAdjustForEndOfFile always returns something if position is EOF, - // whether or not the result includes the position. - // And the SyntaxTrivia for block comments always ends on EOF, closed or not. - // So we need to handle - // /**/|EOF - // and - // /* |EOF - if (caretPosition == snapshot.Length) - { - if (span.Length < "/**/".Length) - return true; - - // If the block comment is not closed, SyntaxTrivia contains diagnostics - // So when the SyntaxTrivia is clean, the block comment should be closed - if (!trivia.ContainsDiagnostics) - return false; - - var textBeforeCaret = snapshot.GetText(caretPosition.Position - 2, 2); - return textBeforeCaret != "*/"; - } - } + // If the block comment is not closed, SyntaxTrivia contains diagnostics + // So when the SyntaxTrivia is clean, the block comment should be closed + if (!trivia.ContainsDiagnostics) + return false; - return false; + var textBeforeCaret = snapshot.GetText(caretPosition.Position - 2, 2); + return textBeforeCaret != "*/"; + } } + + return false; } } diff --git a/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingOptionsStorage.cs b/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingOptionsStorage.cs index b3044473742cc..19b5412d600b3 100644 --- a/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingOptionsStorage.cs +++ b/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.CSharp.BlockCommentEditing +namespace Microsoft.CodeAnalysis.Editor.CSharp.BlockCommentEditing; + +internal class BlockCommentEditingOptionsStorage { - internal class BlockCommentEditingOptionsStorage - { - public static readonly PerLanguageOption2 AutoInsertBlockCommentStartString = new("csharp_insert_block_comment_start_string", defaultValue: true); - } + public static readonly PerLanguageOption2 AutoInsertBlockCommentStartString = new("csharp_insert_block_comment_start_string", defaultValue: true); } diff --git a/src/EditorFeatures/CSharp/BlockCommentEditing/CloseBlockCommentCommandHandler.cs b/src/EditorFeatures/CSharp/BlockCommentEditing/CloseBlockCommentCommandHandler.cs index 928a2981e9f3f..9e43ed817b702 100644 --- a/src/EditorFeatures/CSharp/BlockCommentEditing/CloseBlockCommentCommandHandler.cs +++ b/src/EditorFeatures/CSharp/BlockCommentEditing/CloseBlockCommentCommandHandler.cs @@ -12,54 +12,53 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.BlockCommentEditing +namespace Microsoft.CodeAnalysis.Editor.CSharp.BlockCommentEditing; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(nameof(CloseBlockCommentCommandHandler))] +[Order(After = nameof(BlockCommentEditingCommandHandler))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CloseBlockCommentCommandHandler(EditorOptionsService editorOptionsService) : ICommandHandler { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(nameof(CloseBlockCommentCommandHandler))] - [Order(After = nameof(BlockCommentEditingCommandHandler))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class CloseBlockCommentCommandHandler(EditorOptionsService editorOptionsService) : ICommandHandler - { - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - public string DisplayName => EditorFeaturesResources.Block_Comment_Editing; + public string DisplayName => EditorFeaturesResources.Block_Comment_Editing; - public bool ExecuteCommand(TypeCharCommandArgs args, CommandExecutionContext executionContext) + public bool ExecuteCommand(TypeCharCommandArgs args, CommandExecutionContext executionContext) + { + if (args.TypedChar == '/') { - if (args.TypedChar == '/') + var caret = args.TextView.GetCaretPoint(args.SubjectBuffer); + if (caret != null) { - var caret = args.TextView.GetCaretPoint(args.SubjectBuffer); - if (caret != null) - { - var (snapshot, position) = caret.Value; + var (snapshot, position) = caret.Value; - // Check that the line is all whitespace ending with an asterisk and a single space (| marks caret position): - // * | - if (position >= 2 && - snapshot[position - 1] == ' ' && - snapshot[position - 2] == '*') + // Check that the line is all whitespace ending with an asterisk and a single space (| marks caret position): + // * | + if (position >= 2 && + snapshot[position - 1] == ' ' && + snapshot[position - 2] == '*') + { + var line = snapshot.GetLineFromPosition(position); + if (line.End == position && + line.IsEmptyOrWhitespace(0, line.Length - 2)) { - var line = snapshot.GetLineFromPosition(position); - if (line.End == position && - line.IsEmptyOrWhitespace(0, line.Length - 2)) + if (_editorOptionsService.GlobalOptions.GetOption(BlockCommentEditingOptionsStorage.AutoInsertBlockCommentStartString, LanguageNames.CSharp) && + BlockCommentEditingCommandHandler.IsCaretInsideBlockCommentSyntax(caret.Value, args.SubjectBuffer, _editorOptionsService, out _, out _, executionContext.OperationContext.UserCancellationToken)) { - if (_editorOptionsService.GlobalOptions.GetOption(BlockCommentEditingOptionsStorage.AutoInsertBlockCommentStartString, LanguageNames.CSharp) && - BlockCommentEditingCommandHandler.IsCaretInsideBlockCommentSyntax(caret.Value, args.SubjectBuffer, _editorOptionsService, out _, out _, executionContext.OperationContext.UserCancellationToken)) - { - args.SubjectBuffer.Replace(new VisualStudio.Text.Span(position - 1, 1), "/"); - return true; - } + args.SubjectBuffer.Replace(new VisualStudio.Text.Span(position - 1, 1), "/"); + return true; } } } } - - return false; } - public CommandState GetCommandState(TypeCharCommandArgs args) - => CommandState.Unspecified; + return false; } + + public CommandState GetCommandState(TypeCharCommandArgs args) + => CommandState.Unspecified; } diff --git a/src/EditorFeatures/CSharp/ChangeSignature/CSharpChangeSignatureCommandHandler.cs b/src/EditorFeatures/CSharp/ChangeSignature/CSharpChangeSignatureCommandHandler.cs index 556eedf52fc95..2acb0a635722c 100644 --- a/src/EditorFeatures/CSharp/ChangeSignature/CSharpChangeSignatureCommandHandler.cs +++ b/src/EditorFeatures/CSharp/ChangeSignature/CSharpChangeSignatureCommandHandler.cs @@ -11,14 +11,13 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.ChangeSignature +namespace Microsoft.CodeAnalysis.Editor.CSharp.ChangeSignature; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(PredefinedCommandHandlerNames.ChangeSignature)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpChangeSignatureCommandHandler(IThreadingContext threadingContext, IGlobalOptionService globalOptions) : AbstractChangeSignatureCommandHandler(threadingContext, globalOptions) { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(PredefinedCommandHandlerNames.ChangeSignature)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class CSharpChangeSignatureCommandHandler(IThreadingContext threadingContext, IGlobalOptionService globalOptions) : AbstractChangeSignatureCommandHandler(threadingContext, globalOptions) - { - } } diff --git a/src/EditorFeatures/CSharp/CodeCleanup/CSharpCodeCleanupService.cs b/src/EditorFeatures/CSharp/CodeCleanup/CSharpCodeCleanupService.cs index 9c533a8549ef6..083c1ca0978b4 100644 --- a/src/EditorFeatures/CSharp/CodeCleanup/CSharpCodeCleanupService.cs +++ b/src/EditorFeatures/CSharp/CodeCleanup/CSharpCodeCleanupService.cs @@ -11,169 +11,168 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.CodeCleanup +namespace Microsoft.CodeAnalysis.CSharp.CodeCleanup; + +[ExportLanguageService(typeof(ICodeCleanupService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpCodeCleanupService(ICodeFixService codeFixService, IDiagnosticAnalyzerService diagnosticAnalyzerService) : AbstractCodeCleanupService(codeFixService, diagnosticAnalyzerService) { - [ExportLanguageService(typeof(ICodeCleanupService), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpCodeCleanupService(ICodeFixService codeFixService, IDiagnosticAnalyzerService diagnosticAnalyzerService) : AbstractCodeCleanupService(codeFixService, diagnosticAnalyzerService) - { - /// - /// Maps format document code cleanup options to DiagnosticId[] - /// - private static readonly ImmutableArray s_diagnosticSets = - [ - new DiagnosticSet(FeaturesResources.Apply_using_directive_placement_preferences, - IDEDiagnosticIds.MoveMisplacedUsingDirectivesDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_file_header_preferences, - IDEDiagnosticIds.FileHeaderMismatch), - new DiagnosticSet(AnalyzersResources.Add_this_or_Me_qualification, - IDEDiagnosticIds.AddThisOrMeQualificationDiagnosticId, - IDEDiagnosticIds.RemoveThisOrMeQualificationDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_language_framework_type_preferences, - IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_parentheses_preferences, - IDEDiagnosticIds.RemoveUnnecessaryParenthesesDiagnosticId, - IDEDiagnosticIds.AddRequiredParenthesesDiagnosticId), - new DiagnosticSet(AnalyzersResources.Add_accessibility_modifiers, - IDEDiagnosticIds.AddAccessibilityModifiersDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_coalesce_expression_preferences, - IDEDiagnosticIds.UseCoalesceExpressionForTernaryConditionalCheckDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_object_collection_initialization_preferences, - IDEDiagnosticIds.UseCollectionInitializerDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_tuple_name_preferences, - IDEDiagnosticIds.UseExplicitTupleNameDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_namespace_matches_folder_preferences, - IDEDiagnosticIds.MatchFolderAndNamespaceDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_null_propagation_preferences, - IDEDiagnosticIds.UseNullPropagationDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_object_initializer_preferences, - IDEDiagnosticIds.UseObjectInitializerDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_auto_property_preferences, - IDEDiagnosticIds.UseAutoPropertyDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_compound_assignment_preferences, - IDEDiagnosticIds.UseCoalesceCompoundAssignmentDiagnosticId, - IDEDiagnosticIds.UseCompoundAssignmentDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_conditional_expression_preferences, - // dotnet_style_prefer_conditional_expression_over_assignment - IDEDiagnosticIds.UseConditionalExpressionForAssignmentDiagnosticId, - // dotnet_style_prefer_conditional_expression_over_return - IDEDiagnosticIds.UseConditionalExpressionForReturnDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_inferred_anonymous_type_member_names_preferences, - IDEDiagnosticIds.UseInferredMemberNameDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_null_checking_preferences, - IDEDiagnosticIds.UseIsNullCheckDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_simplify_boolean_expression_preferences, - IDEDiagnosticIds.SimplifyConditionalExpressionDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_string_interpolation_preferences, - IDEDiagnosticIds.SimplifyInterpolationId), - new DiagnosticSet(CSharpFeaturesResources.Make_private_field_readonly_when_possible, - IDEDiagnosticIds.MakeFieldReadonlyDiagnosticId), - new DiagnosticSet(FeaturesResources.Remove_unused_parameters, - IDEDiagnosticIds.UnusedParameterDiagnosticId), - new DiagnosticSet(FeaturesResources.Remove_unused_suppressions, - IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_blank_line_preferences_experimental, - IDEDiagnosticIds.MultipleBlankLinesDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_statement_after_block_preferences_experimental, - IDEDiagnosticIds.ConsecutiveStatementPlacementDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_var_preferences, - IDEDiagnosticIds.UseImplicitTypeDiagnosticId, - IDEDiagnosticIds.UseExplicitTypeDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_expression_block_body_preferences, - // csharp_style_expression_bodied_accessors - IDEDiagnosticIds.UseExpressionBodyForAccessorsDiagnosticId, - // csharp_style_expression_bodied_constructors - IDEDiagnosticIds.UseExpressionBodyForConstructorsDiagnosticId, - // csharp_style_expression_bodied_indexers - IDEDiagnosticIds.UseExpressionBodyForIndexersDiagnosticId, - // csharp_style_expression_bodied_lambdas - IDEDiagnosticIds.UseExpressionBodyForLambdaExpressionsDiagnosticId, - // csharp_style_expression_bodied_local_functions - IDEDiagnosticIds.UseExpressionBodyForLocalFunctionsDiagnosticId, - // csharp_style_expression_bodied_methods - IDEDiagnosticIds.UseExpressionBodyForMethodsDiagnosticId, - // csharp_style_expression_bodied_operators - IDEDiagnosticIds.UseExpressionBodyForOperatorsDiagnosticId, - IDEDiagnosticIds.UseExpressionBodyForConversionOperatorsDiagnosticId, - // csharp_style_expression_bodied_properties - IDEDiagnosticIds.UseExpressionBodyForPropertiesDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_pattern_matching_preferences, - // csharp_style_pattern_matching_over_as_with_null_check - IDEDiagnosticIds.InlineAsTypeCheckId, - // csharp_style_pattern_matching_over_is_with_cast_check - IDEDiagnosticIds.InlineIsTypeCheckId, - // csharp_style_prefer_extended_property_pattern - IDEDiagnosticIds.SimplifyPropertyPatternDiagnosticId, - // csharp_style_prefer_not_pattern - IDEDiagnosticIds.UseNotPatternDiagnosticId, - // csharp_style_prefer_pattern_matching - IDEDiagnosticIds.UsePatternCombinatorsDiagnosticId, - // csharp_style_prefer_switch_expression - IDEDiagnosticIds.ConvertSwitchStatementToExpressionDiagnosticId, - // csharp_style_prefer_null_check_over_type_check - IDEDiagnosticIds.UseNullCheckOverTypeCheckDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_conditional_delegate_call_preferences, - IDEDiagnosticIds.InvokeDelegateWithConditionalAccessId), - new DiagnosticSet(CSharpFeaturesResources.Apply_static_local_function_preferences, - IDEDiagnosticIds.MakeLocalFunctionStaticDiagnosticId), - new DiagnosticSet(FeaturesResources.Sort_accessibility_modifiers, - IDEDiagnosticIds.OrderModifiersDiagnosticId, - "CS0267"), - new DiagnosticSet(CSharpFeaturesResources.Apply_readonly_struct_preferences, - IDEDiagnosticIds.MakeStructReadOnlyDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Add_required_braces_for_single_line_control_statements, - IDEDiagnosticIds.AddBracesDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_using_statement_preferences, - IDEDiagnosticIds.UseSimpleUsingStatementDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_namespace_preferences, - IDEDiagnosticIds.UseFileScopedNamespaceDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_method_group_conversion_preferences, - IDEDiagnosticIds.RemoveUnnecessaryLambdaExpressionDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_default_T_preferences, - IDEDiagnosticIds.UseDefaultLiteralDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_deconstruct_preferences, - // csharp_style_deconstructed_variable_declaration - IDEDiagnosticIds.UseDeconstructionDiagnosticId, - // csharp_style_prefer_tuple_swap - IDEDiagnosticIds.UseTupleSwapDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_new_preferences, - IDEDiagnosticIds.UseImplicitObjectCreationDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_inline_out_variable_preferences, - IDEDiagnosticIds.InlineDeclarationDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_range_preferences, - // csharp_style_prefer_index_operator - IDEDiagnosticIds.UseIndexOperatorDiagnosticId, - // csharp_style_prefer_range_operator - IDEDiagnosticIds.UseRangeOperatorDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_local_over_anonymous_function_preferences, - IDEDiagnosticIds.UseLocalFunctionDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_throw_expression_preferences, - IDEDiagnosticIds.UseThrowExpressionDiagnosticId), - new DiagnosticSet(FeaturesResources.Apply_unused_value_preferences, - IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId, - IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_blank_line_after_colon_in_constructor_initializer_preferences_experimental, - IDEDiagnosticIds.ConstructorInitializerPlacementDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_blank_lines_between_consecutive_braces_preferences_experimental, - IDEDiagnosticIds.ConsecutiveBracePlacementDiagnosticId), - new DiagnosticSet(CSharpFeaturesResources.Apply_embedded_statements_on_same_line_preferences_experimental, - IDEDiagnosticIds.EmbeddedStatementPlacementDiagnosticId), - new DiagnosticSet(FeaturesResources.Remove_unnecessary_casts, - IDEDiagnosticIds.RemoveUnnecessaryCastDiagnosticId), - new DiagnosticSet(FeaturesResources.Remove_unused_variables, - CSharpRemoveUnusedVariableCodeFixProvider.CS0168, - CSharpRemoveUnusedVariableCodeFixProvider.CS0219), - new DiagnosticSet(CSharpAnalyzersResources.Remove_unnecessary_nullable_directive, - IDEDiagnosticIds.RemoveRedundantNullableDirectiveDiagnosticId, - IDEDiagnosticIds.RemoveUnnecessaryNullableDirectiveDiagnosticId) + /// + /// Maps format document code cleanup options to DiagnosticId[] + /// + private static readonly ImmutableArray s_diagnosticSets = + [ + new DiagnosticSet(FeaturesResources.Apply_using_directive_placement_preferences, + IDEDiagnosticIds.MoveMisplacedUsingDirectivesDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_file_header_preferences, + IDEDiagnosticIds.FileHeaderMismatch), + new DiagnosticSet(AnalyzersResources.Add_this_or_Me_qualification, + IDEDiagnosticIds.AddThisOrMeQualificationDiagnosticId, + IDEDiagnosticIds.RemoveThisOrMeQualificationDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_language_framework_type_preferences, + IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_parentheses_preferences, + IDEDiagnosticIds.RemoveUnnecessaryParenthesesDiagnosticId, + IDEDiagnosticIds.AddRequiredParenthesesDiagnosticId), + new DiagnosticSet(AnalyzersResources.Add_accessibility_modifiers, + IDEDiagnosticIds.AddAccessibilityModifiersDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_coalesce_expression_preferences, + IDEDiagnosticIds.UseCoalesceExpressionForTernaryConditionalCheckDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_object_collection_initialization_preferences, + IDEDiagnosticIds.UseCollectionInitializerDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_tuple_name_preferences, + IDEDiagnosticIds.UseExplicitTupleNameDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_namespace_matches_folder_preferences, + IDEDiagnosticIds.MatchFolderAndNamespaceDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_null_propagation_preferences, + IDEDiagnosticIds.UseNullPropagationDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_object_initializer_preferences, + IDEDiagnosticIds.UseObjectInitializerDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_auto_property_preferences, + IDEDiagnosticIds.UseAutoPropertyDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_compound_assignment_preferences, + IDEDiagnosticIds.UseCoalesceCompoundAssignmentDiagnosticId, + IDEDiagnosticIds.UseCompoundAssignmentDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_conditional_expression_preferences, + // dotnet_style_prefer_conditional_expression_over_assignment + IDEDiagnosticIds.UseConditionalExpressionForAssignmentDiagnosticId, + // dotnet_style_prefer_conditional_expression_over_return + IDEDiagnosticIds.UseConditionalExpressionForReturnDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_inferred_anonymous_type_member_names_preferences, + IDEDiagnosticIds.UseInferredMemberNameDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_null_checking_preferences, + IDEDiagnosticIds.UseIsNullCheckDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_simplify_boolean_expression_preferences, + IDEDiagnosticIds.SimplifyConditionalExpressionDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_string_interpolation_preferences, + IDEDiagnosticIds.SimplifyInterpolationId), + new DiagnosticSet(CSharpFeaturesResources.Make_private_field_readonly_when_possible, + IDEDiagnosticIds.MakeFieldReadonlyDiagnosticId), + new DiagnosticSet(FeaturesResources.Remove_unused_parameters, + IDEDiagnosticIds.UnusedParameterDiagnosticId), + new DiagnosticSet(FeaturesResources.Remove_unused_suppressions, + IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_blank_line_preferences_experimental, + IDEDiagnosticIds.MultipleBlankLinesDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_statement_after_block_preferences_experimental, + IDEDiagnosticIds.ConsecutiveStatementPlacementDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_var_preferences, + IDEDiagnosticIds.UseImplicitTypeDiagnosticId, + IDEDiagnosticIds.UseExplicitTypeDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_expression_block_body_preferences, + // csharp_style_expression_bodied_accessors + IDEDiagnosticIds.UseExpressionBodyForAccessorsDiagnosticId, + // csharp_style_expression_bodied_constructors + IDEDiagnosticIds.UseExpressionBodyForConstructorsDiagnosticId, + // csharp_style_expression_bodied_indexers + IDEDiagnosticIds.UseExpressionBodyForIndexersDiagnosticId, + // csharp_style_expression_bodied_lambdas + IDEDiagnosticIds.UseExpressionBodyForLambdaExpressionsDiagnosticId, + // csharp_style_expression_bodied_local_functions + IDEDiagnosticIds.UseExpressionBodyForLocalFunctionsDiagnosticId, + // csharp_style_expression_bodied_methods + IDEDiagnosticIds.UseExpressionBodyForMethodsDiagnosticId, + // csharp_style_expression_bodied_operators + IDEDiagnosticIds.UseExpressionBodyForOperatorsDiagnosticId, + IDEDiagnosticIds.UseExpressionBodyForConversionOperatorsDiagnosticId, + // csharp_style_expression_bodied_properties + IDEDiagnosticIds.UseExpressionBodyForPropertiesDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_pattern_matching_preferences, + // csharp_style_pattern_matching_over_as_with_null_check + IDEDiagnosticIds.InlineAsTypeCheckId, + // csharp_style_pattern_matching_over_is_with_cast_check + IDEDiagnosticIds.InlineIsTypeCheckId, + // csharp_style_prefer_extended_property_pattern + IDEDiagnosticIds.SimplifyPropertyPatternDiagnosticId, + // csharp_style_prefer_not_pattern + IDEDiagnosticIds.UseNotPatternDiagnosticId, + // csharp_style_prefer_pattern_matching + IDEDiagnosticIds.UsePatternCombinatorsDiagnosticId, + // csharp_style_prefer_switch_expression + IDEDiagnosticIds.ConvertSwitchStatementToExpressionDiagnosticId, + // csharp_style_prefer_null_check_over_type_check + IDEDiagnosticIds.UseNullCheckOverTypeCheckDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_conditional_delegate_call_preferences, + IDEDiagnosticIds.InvokeDelegateWithConditionalAccessId), + new DiagnosticSet(CSharpFeaturesResources.Apply_static_local_function_preferences, + IDEDiagnosticIds.MakeLocalFunctionStaticDiagnosticId), + new DiagnosticSet(FeaturesResources.Sort_accessibility_modifiers, + IDEDiagnosticIds.OrderModifiersDiagnosticId, + "CS0267"), + new DiagnosticSet(CSharpFeaturesResources.Apply_readonly_struct_preferences, + IDEDiagnosticIds.MakeStructReadOnlyDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Add_required_braces_for_single_line_control_statements, + IDEDiagnosticIds.AddBracesDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_using_statement_preferences, + IDEDiagnosticIds.UseSimpleUsingStatementDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_namespace_preferences, + IDEDiagnosticIds.UseFileScopedNamespaceDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_method_group_conversion_preferences, + IDEDiagnosticIds.RemoveUnnecessaryLambdaExpressionDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_default_T_preferences, + IDEDiagnosticIds.UseDefaultLiteralDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_deconstruct_preferences, + // csharp_style_deconstructed_variable_declaration + IDEDiagnosticIds.UseDeconstructionDiagnosticId, + // csharp_style_prefer_tuple_swap + IDEDiagnosticIds.UseTupleSwapDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_new_preferences, + IDEDiagnosticIds.UseImplicitObjectCreationDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_inline_out_variable_preferences, + IDEDiagnosticIds.InlineDeclarationDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_range_preferences, + // csharp_style_prefer_index_operator + IDEDiagnosticIds.UseIndexOperatorDiagnosticId, + // csharp_style_prefer_range_operator + IDEDiagnosticIds.UseRangeOperatorDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_local_over_anonymous_function_preferences, + IDEDiagnosticIds.UseLocalFunctionDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_throw_expression_preferences, + IDEDiagnosticIds.UseThrowExpressionDiagnosticId), + new DiagnosticSet(FeaturesResources.Apply_unused_value_preferences, + IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId, + IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_blank_line_after_colon_in_constructor_initializer_preferences_experimental, + IDEDiagnosticIds.ConstructorInitializerPlacementDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_blank_lines_between_consecutive_braces_preferences_experimental, + IDEDiagnosticIds.ConsecutiveBracePlacementDiagnosticId), + new DiagnosticSet(CSharpFeaturesResources.Apply_embedded_statements_on_same_line_preferences_experimental, + IDEDiagnosticIds.EmbeddedStatementPlacementDiagnosticId), + new DiagnosticSet(FeaturesResources.Remove_unnecessary_casts, + IDEDiagnosticIds.RemoveUnnecessaryCastDiagnosticId), + new DiagnosticSet(FeaturesResources.Remove_unused_variables, + CSharpRemoveUnusedVariableCodeFixProvider.CS0168, + CSharpRemoveUnusedVariableCodeFixProvider.CS0219), + new DiagnosticSet(CSharpAnalyzersResources.Remove_unnecessary_nullable_directive, + IDEDiagnosticIds.RemoveRedundantNullableDirectiveDiagnosticId, + IDEDiagnosticIds.RemoveUnnecessaryNullableDirectiveDiagnosticId) , - ]; + ]; - protected override string OrganizeImportsDescription - => CSharpFeaturesResources.Organize_Usings; + protected override string OrganizeImportsDescription + => CSharpFeaturesResources.Organize_Usings; - protected override ImmutableArray GetDiagnosticSets() - => s_diagnosticSets; - } + protected override ImmutableArray GetDiagnosticSets() + => s_diagnosticSets; } diff --git a/src/EditorFeatures/CSharp/CommentSelection/CSharpToggleBlockCommentCommandHandler.cs b/src/EditorFeatures/CSharp/CommentSelection/CSharpToggleBlockCommentCommandHandler.cs index 6cae4430dec4a..dc9511bf2b8d8 100644 --- a/src/EditorFeatures/CSharp/CommentSelection/CSharpToggleBlockCommentCommandHandler.cs +++ b/src/EditorFeatures/CSharp/CommentSelection/CSharpToggleBlockCommentCommandHandler.cs @@ -21,33 +21,32 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.CommentSelection +namespace Microsoft.CodeAnalysis.Editor.CSharp.CommentSelection; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(PredefinedCommandHandlerNames.ToggleBlockComment)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpToggleBlockCommentCommandHandler( + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + ITextStructureNavigatorSelectorService navigatorSelectorService, + EditorOptionsService editorOptionsService) : + ToggleBlockCommentCommandHandler(undoHistoryRegistry, editorOperationsFactoryService, navigatorSelectorService, editorOptionsService) { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(PredefinedCommandHandlerNames.ToggleBlockComment)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class CSharpToggleBlockCommentCommandHandler( - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - ITextStructureNavigatorSelectorService navigatorSelectorService, - EditorOptionsService editorOptionsService) : - ToggleBlockCommentCommandHandler(undoHistoryRegistry, editorOperationsFactoryService, navigatorSelectorService, editorOptionsService) - { - /// - /// Retrieves block comments near the selection in the document. - /// Uses the CSharp syntax tree to find the commented spans. - /// - protected override ImmutableArray GetBlockCommentsInDocument(Document document, ITextSnapshot snapshot, - TextSpan linesContainingSelections, CommentSelectionInfo commentInfo, CancellationToken cancellationToken) - { - var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); - // Only search for block comments intersecting the lines in the selections. - return root.DescendantTrivia(linesContainingSelections) - .Where(trivia => trivia.Kind() is SyntaxKind.MultiLineCommentTrivia or SyntaxKind.MultiLineDocumentationCommentTrivia) - .SelectAsArray(blockCommentTrivia => blockCommentTrivia.Span); - } + /// + /// Retrieves block comments near the selection in the document. + /// Uses the CSharp syntax tree to find the commented spans. + /// + protected override ImmutableArray GetBlockCommentsInDocument(Document document, ITextSnapshot snapshot, + TextSpan linesContainingSelections, CommentSelectionInfo commentInfo, CancellationToken cancellationToken) + { + var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); + // Only search for block comments intersecting the lines in the selections. + return root.DescendantTrivia(linesContainingSelections) + .Where(trivia => trivia.Kind() is SyntaxKind.MultiLineCommentTrivia or SyntaxKind.MultiLineDocumentationCommentTrivia) + .SelectAsArray(blockCommentTrivia => blockCommentTrivia.Span); } } diff --git a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs index e6ea5cad03fc8..b64b470029904 100644 --- a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs +++ b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs @@ -26,571 +26,570 @@ using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.CompleteStatement +namespace Microsoft.CodeAnalysis.Editor.CSharp.CompleteStatement; + +/// +/// When user types ; in a statement, semicolon is added and caret is placed after the semicolon +/// +[Export(typeof(ICommandHandler))] +[Export] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(nameof(CompleteStatementCommandHandler))] +[Order(After = PredefinedCompletionNames.CompletionCommandHandler)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CompleteStatementCommandHandler( + ITextUndoHistoryRegistry textUndoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + IGlobalOptionService globalOptions) : IChainedCommandHandler { - /// - /// When user types ; in a statement, semicolon is added and caret is placed after the semicolon - /// - [Export(typeof(ICommandHandler))] - [Export] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(nameof(CompleteStatementCommandHandler))] - [Order(After = PredefinedCompletionNames.CompletionCommandHandler)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class CompleteStatementCommandHandler( - ITextUndoHistoryRegistry textUndoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - IGlobalOptionService globalOptions) : IChainedCommandHandler + private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry = textUndoHistoryRegistry; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; + private readonly IGlobalOptionService _globalOptions = globalOptions; + + private enum SemicolonBehavior { - private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry = textUndoHistoryRegistry; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; - private readonly IGlobalOptionService _globalOptions = globalOptions; + /// + /// This command handler does not participate in handling the typing behavior of the current semicolon. + /// + None, - private enum SemicolonBehavior - { - /// - /// This command handler does not participate in handling the typing behavior of the current semicolon. - /// - None, + /// + /// This command handler moves the caret, but does not insert the necessary semicolon. + /// + Insert, + + /// + /// This command handler moves the caret, and overtypes an existing semicolon in the process. No additional + /// semicolon is needed. + /// + Overtype, + } - /// - /// This command handler moves the caret, but does not insert the necessary semicolon. - /// - Insert, + public CommandState GetCommandState(TypeCharCommandArgs args, Func nextCommandHandler) => nextCommandHandler(); - /// - /// This command handler moves the caret, and overtypes an existing semicolon in the process. No additional - /// semicolon is needed. - /// - Overtype, - } + public string DisplayName => CSharpEditorResources.Complete_statement_on_semicolon; - public CommandState GetCommandState(TypeCharCommandArgs args, Func nextCommandHandler) => nextCommandHandler(); + public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + var willMoveSemicolon = BeforeExecuteCommand(speculative: true, args, executionContext); + if (willMoveSemicolon == SemicolonBehavior.None) + { + // Pass this on without altering the undo stack + nextCommandHandler(); + return; + } - public string DisplayName => CSharpEditorResources.Complete_statement_on_semicolon; + using var transaction = CaretPreservingEditTransaction.TryCreate(CSharpEditorResources.Complete_statement_on_semicolon, args.TextView, _textUndoHistoryRegistry, _editorOperationsFactoryService); - public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + // Determine where semicolon should be placed and move caret to location + if (BeforeExecuteCommand(speculative: false, args, executionContext) != SemicolonBehavior.Overtype) { - var willMoveSemicolon = BeforeExecuteCommand(speculative: true, args, executionContext); - if (willMoveSemicolon == SemicolonBehavior.None) - { - // Pass this on without altering the undo stack - nextCommandHandler(); - return; - } + // Insert the semicolon using next command handler + nextCommandHandler(); + } - using var transaction = CaretPreservingEditTransaction.TryCreate(CSharpEditorResources.Complete_statement_on_semicolon, args.TextView, _textUndoHistoryRegistry, _editorOperationsFactoryService); + transaction?.Complete(); + } - // Determine where semicolon should be placed and move caret to location - if (BeforeExecuteCommand(speculative: false, args, executionContext) != SemicolonBehavior.Overtype) - { - // Insert the semicolon using next command handler - nextCommandHandler(); - } + private SemicolonBehavior BeforeExecuteCommand(bool speculative, TypeCharCommandArgs args, CommandExecutionContext executionContext) + { + if (args.TypedChar != ';' || !args.TextView.Selection.IsEmpty) + { + return SemicolonBehavior.None; + } - transaction?.Complete(); + var caretOpt = args.TextView.GetCaretPoint(args.SubjectBuffer); + if (!caretOpt.HasValue) + { + return SemicolonBehavior.None; } - private SemicolonBehavior BeforeExecuteCommand(bool speculative, TypeCharCommandArgs args, CommandExecutionContext executionContext) + if (!_globalOptions.GetOption(CompleteStatementOptionsStorage.AutomaticallyCompleteStatementOnSemicolon)) { - if (args.TypedChar != ';' || !args.TextView.Selection.IsEmpty) - { - return SemicolonBehavior.None; - } + return SemicolonBehavior.None; + } - var caretOpt = args.TextView.GetCaretPoint(args.SubjectBuffer); - if (!caretOpt.HasValue) - { - return SemicolonBehavior.None; - } + var caret = caretOpt.Value; + var document = caret.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + { + return SemicolonBehavior.None; + } - if (!_globalOptions.GetOption(CompleteStatementOptionsStorage.AutomaticallyCompleteStatementOnSemicolon)) - { - return SemicolonBehavior.None; - } + var cancellationToken = executionContext.OperationContext.UserCancellationToken; + var syntaxFacts = document.GetRequiredLanguageService(); + var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); - var caret = caretOpt.Value; - var document = caret.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return SemicolonBehavior.None; - } + if (!TryGetStartingNode(root, caret, out var currentNode, cancellationToken)) + { + return SemicolonBehavior.None; + } - var cancellationToken = executionContext.OperationContext.UserCancellationToken; - var syntaxFacts = document.GetRequiredLanguageService(); - var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); + return MoveCaretToSemicolonPosition(speculative, args, document, root, originalCaret: caret, caret, syntaxFacts, currentNode, + isInsideDelimiters: false, cancellationToken); + } - if (!TryGetStartingNode(root, caret, out var currentNode, cancellationToken)) - { - return SemicolonBehavior.None; - } + /// + /// Determines which node the caret is in. + /// Must be called on the UI thread. + /// + private static bool TryGetStartingNode( + SyntaxNode root, + SnapshotPoint caret, + [NotNullWhen(true)] out SyntaxNode? startingNode, + CancellationToken cancellationToken) + { + // on the UI thread + startingNode = null; + var caretPosition = caret.Position; - return MoveCaretToSemicolonPosition(speculative, args, document, root, originalCaret: caret, caret, syntaxFacts, currentNode, - isInsideDelimiters: false, cancellationToken); - } + var token = root.FindTokenOnLeftOfPosition(caretPosition); - /// - /// Determines which node the caret is in. - /// Must be called on the UI thread. - /// - private static bool TryGetStartingNode( - SyntaxNode root, - SnapshotPoint caret, - [NotNullWhen(true)] out SyntaxNode? startingNode, - CancellationToken cancellationToken) + if (token.SyntaxTree == null + || token.SyntaxTree.IsInNonUserCode(caretPosition, cancellationToken)) { - // on the UI thread - startingNode = null; - var caretPosition = caret.Position; + return false; + } - var token = root.FindTokenOnLeftOfPosition(caretPosition); + startingNode = token.GetRequiredParent(); - if (token.SyntaxTree == null - || token.SyntaxTree.IsInNonUserCode(caretPosition, cancellationToken)) - { - return false; - } - - startingNode = token.GetRequiredParent(); + // If the caret is before an opening delimiter or after a closing delimeter, + // start analysis with node outside of delimiters. + // + // Examples, + // `obj.ToString$()` where `token` references `(` but the caret isn't actually inside the argument list. + // `obj.ToString()$` or `obj.method()$ .method()` where `token` references `)` but the caret isn't inside the argument list. + // `defa$$ult(object)` where `token` references `default` but the caret isn't inside the parentheses. + if (HasDelimitersButCaretIsOutside(startingNode, caretPosition)) + { + startingNode = startingNode.GetRequiredParent(); + } - // If the caret is before an opening delimiter or after a closing delimeter, - // start analysis with node outside of delimiters. - // - // Examples, - // `obj.ToString$()` where `token` references `(` but the caret isn't actually inside the argument list. - // `obj.ToString()$` or `obj.method()$ .method()` where `token` references `)` but the caret isn't inside the argument list. - // `defa$$ult(object)` where `token` references `default` but the caret isn't inside the parentheses. - if (HasDelimitersButCaretIsOutside(startingNode, caretPosition)) - { - startingNode = startingNode.GetRequiredParent(); - } + return true; + } - return true; + private static SemicolonBehavior MoveCaretToSemicolonPosition( + bool speculative, + TypeCharCommandArgs args, + Document document, + SyntaxNode root, + SnapshotPoint originalCaret, + SnapshotPoint caret, + ISyntaxFactsService syntaxFacts, + SyntaxNode? currentNode, + bool isInsideDelimiters, + CancellationToken cancellationToken) + { + if (currentNode == null || + IsInAStringOrCharacter(currentNode, caret)) + { + // Don't complete statement. Return without moving the caret. + return SemicolonBehavior.None; } - private static SemicolonBehavior MoveCaretToSemicolonPosition( - bool speculative, - TypeCharCommandArgs args, - Document document, - SyntaxNode root, - SnapshotPoint originalCaret, - SnapshotPoint caret, - ISyntaxFactsService syntaxFacts, - SyntaxNode? currentNode, - bool isInsideDelimiters, - CancellationToken cancellationToken) - { - if (currentNode == null || - IsInAStringOrCharacter(currentNode, caret)) + if (currentNode.Kind() is + SyntaxKind.ArgumentList or + SyntaxKind.ArrayRankSpecifier or + SyntaxKind.BracketedArgumentList or + SyntaxKind.ParenthesizedExpression or + SyntaxKind.ParameterList or + SyntaxKind.DefaultExpression or + SyntaxKind.CheckedExpression or + SyntaxKind.UncheckedExpression or + SyntaxKind.TypeOfExpression or + SyntaxKind.TupleExpression or + SyntaxKind.ObjectInitializerExpression or + SyntaxKind.ArrayInitializerExpression or + SyntaxKind.CollectionInitializerExpression or + SyntaxKind.CollectionExpression or + SyntaxKind.SwitchExpression) + { + // make sure the closing delimiter exists + if (RequiredDelimiterIsMissing(currentNode)) { - // Don't complete statement. Return without moving the caret. return SemicolonBehavior.None; } - if (currentNode.Kind() is - SyntaxKind.ArgumentList or - SyntaxKind.ArrayRankSpecifier or - SyntaxKind.BracketedArgumentList or - SyntaxKind.ParenthesizedExpression or - SyntaxKind.ParameterList or - SyntaxKind.DefaultExpression or - SyntaxKind.CheckedExpression or - SyntaxKind.UncheckedExpression or - SyntaxKind.TypeOfExpression or - SyntaxKind.TupleExpression or - SyntaxKind.ObjectInitializerExpression or - SyntaxKind.ArrayInitializerExpression or - SyntaxKind.CollectionInitializerExpression or - SyntaxKind.CollectionExpression or - SyntaxKind.SwitchExpression) + // set caret to just outside the delimited span and analyze again + // if caret was already in that position, return to avoid infinite loop + var newCaretPosition = currentNode.Span.End; + if (newCaretPosition == caret.Position) { - // make sure the closing delimiter exists - if (RequiredDelimiterIsMissing(currentNode)) - { - return SemicolonBehavior.None; - } - - // set caret to just outside the delimited span and analyze again - // if caret was already in that position, return to avoid infinite loop - var newCaretPosition = currentNode.Span.End; - if (newCaretPosition == caret.Position) - { - return SemicolonBehavior.None; - } - - // We know the current node has delimiters due to the Kind() check above, so isInsideDelimiters is - // simply the inverse of being outside the delimiters. - isInsideDelimiters = !HasDelimitersButCaretIsOutside(currentNode, caret.Position); - - var newCaret = args.SubjectBuffer.CurrentSnapshot.GetPoint(newCaretPosition); - if (!TryGetStartingNode(root, newCaret, out currentNode, cancellationToken)) - return SemicolonBehavior.None; - - return MoveCaretToSemicolonPosition( - speculative, args, document, root, originalCaret, newCaret, syntaxFacts, currentNode, isInsideDelimiters, cancellationToken); + return SemicolonBehavior.None; } - else if (currentNode.IsKind(SyntaxKind.DoStatement)) - { - if (IsInConditionOfDoStatement(currentNode, caret)) - { - return MoveCaretToFinalPositionInStatement(speculative, currentNode, args, originalCaret, caret, isInsideDelimiters: true); - } + // We know the current node has delimiters due to the Kind() check above, so isInsideDelimiters is + // simply the inverse of being outside the delimiters. + isInsideDelimiters = !HasDelimitersButCaretIsOutside(currentNode, caret.Position); + + var newCaret = args.SubjectBuffer.CurrentSnapshot.GetPoint(newCaretPosition); + if (!TryGetStartingNode(root, newCaret, out currentNode, cancellationToken)) return SemicolonBehavior.None; - } - else if (syntaxFacts.IsStatement(currentNode) - || CanHaveSemicolon(currentNode)) - { - return MoveCaretToFinalPositionInStatement(speculative, currentNode, args, originalCaret, caret, isInsideDelimiters); - } - else + + return MoveCaretToSemicolonPosition( + speculative, args, document, root, originalCaret, newCaret, syntaxFacts, currentNode, isInsideDelimiters, cancellationToken); + } + else if (currentNode.IsKind(SyntaxKind.DoStatement)) + { + if (IsInConditionOfDoStatement(currentNode, caret)) { - // keep caret the same, but continue analyzing with the parent of the current node - currentNode = currentNode.Parent; - return MoveCaretToSemicolonPosition( - speculative, args, document, root, originalCaret, caret, syntaxFacts, currentNode, isInsideDelimiters, cancellationToken); + return MoveCaretToFinalPositionInStatement(speculative, currentNode, args, originalCaret, caret, isInsideDelimiters: true); } + + return SemicolonBehavior.None; + } + else if (syntaxFacts.IsStatement(currentNode) + || CanHaveSemicolon(currentNode)) + { + return MoveCaretToFinalPositionInStatement(speculative, currentNode, args, originalCaret, caret, isInsideDelimiters); + } + else + { + // keep caret the same, but continue analyzing with the parent of the current node + currentNode = currentNode.Parent; + return MoveCaretToSemicolonPosition( + speculative, args, document, root, originalCaret, caret, syntaxFacts, currentNode, isInsideDelimiters, cancellationToken); + } + } + + private static bool CanHaveSemicolon(SyntaxNode currentNode) + { + if (currentNode.Kind() is SyntaxKind.FieldDeclaration or SyntaxKind.DelegateDeclaration or SyntaxKind.ArrowExpressionClause) + { + return true; } - private static bool CanHaveSemicolon(SyntaxNode currentNode) + if (currentNode.IsKind(SyntaxKind.EqualsValueClause) && currentNode.IsParentKind(SyntaxKind.PropertyDeclaration)) { - if (currentNode.Kind() is SyntaxKind.FieldDeclaration or SyntaxKind.DelegateDeclaration or SyntaxKind.ArrowExpressionClause) - { - return true; - } + return true; + } + + if (currentNode is TypeDeclarationSyntax { OpenBraceToken.IsMissing: true }) + { + return true; + } - if (currentNode.IsKind(SyntaxKind.EqualsValueClause) && currentNode.IsParentKind(SyntaxKind.PropertyDeclaration)) + if (currentNode is MethodDeclarationSyntax method) + { + if (method.Modifiers.Any(SyntaxKind.AbstractKeyword) || method.Modifiers.Any(SyntaxKind.ExternKeyword) || + method.IsParentKind(SyntaxKind.InterfaceDeclaration)) { return true; } - if (currentNode is TypeDeclarationSyntax { OpenBraceToken.IsMissing: true }) + if (method.Modifiers.Any(SyntaxKind.PartialKeyword) && method.Body is null) { return true; } + } - if (currentNode is MethodDeclarationSyntax method) - { - if (method.Modifiers.Any(SyntaxKind.AbstractKeyword) || method.Modifiers.Any(SyntaxKind.ExternKeyword) || - method.IsParentKind(SyntaxKind.InterfaceDeclaration)) - { - return true; - } - - if (method.Modifiers.Any(SyntaxKind.PartialKeyword) && method.Body is null) - { - return true; - } - } + return false; + } + private static bool IsInConditionOfDoStatement(SyntaxNode currentNode, SnapshotPoint caret) + { + if (currentNode is not DoStatementSyntax doStatement) + { return false; } - private static bool IsInConditionOfDoStatement(SyntaxNode currentNode, SnapshotPoint caret) - { - if (currentNode is not DoStatementSyntax doStatement) - { - return false; - } + var condition = doStatement.Condition; + return (caret >= condition.Span.Start && caret <= condition.Span.End); + } - var condition = doStatement.Condition; - return (caret >= condition.Span.Start && caret <= condition.Span.End); + private static SemicolonBehavior MoveCaretToFinalPositionInStatement(bool speculative, SyntaxNode statementNode, TypeCharCommandArgs args, SnapshotPoint originalCaret, SnapshotPoint caret, bool isInsideDelimiters) + { + if (StatementClosingDelimiterIsMissing(statementNode)) + { + // Don't complete statement. Return without moving the caret. + return SemicolonBehavior.None; } - private static SemicolonBehavior MoveCaretToFinalPositionInStatement(bool speculative, SyntaxNode statementNode, TypeCharCommandArgs args, SnapshotPoint originalCaret, SnapshotPoint caret, bool isInsideDelimiters) + if (TryGetCaretPositionToMove(statementNode, caret, isInsideDelimiters, out var targetPosition) + && targetPosition != originalCaret) { - if (StatementClosingDelimiterIsMissing(statementNode)) - { - // Don't complete statement. Return without moving the caret. - return SemicolonBehavior.None; - } + var overtypedExisting = AdjustPositionForExistingSemicolon(statementNode, ref targetPosition); - if (TryGetCaretPositionToMove(statementNode, caret, isInsideDelimiters, out var targetPosition) - && targetPosition != originalCaret) + // If this operation is speculative, return an indication that moving the caret is required, but don't + // actually move it. + if (!speculative) { - var overtypedExisting = AdjustPositionForExistingSemicolon(statementNode, ref targetPosition); - - // If this operation is speculative, return an indication that moving the caret is required, but don't - // actually move it. - if (!speculative) + Logger.Log(FunctionId.CommandHandler_CompleteStatement, KeyValueLogMessage.Create(LogType.UserAction, m => { - Logger.Log(FunctionId.CommandHandler_CompleteStatement, KeyValueLogMessage.Create(LogType.UserAction, m => - { - m[nameof(isInsideDelimiters)] = isInsideDelimiters; - m[nameof(statementNode)] = statementNode.Kind(); - })); - - if (!args.TextView.TryMoveCaretToAndEnsureVisible(targetPosition)) - return SemicolonBehavior.None; - } + m[nameof(isInsideDelimiters)] = isInsideDelimiters; + m[nameof(statementNode)] = statementNode.Kind(); + })); - return overtypedExisting ? SemicolonBehavior.Overtype : SemicolonBehavior.Insert; + if (!args.TextView.TryMoveCaretToAndEnsureVisible(targetPosition)) + return SemicolonBehavior.None; } - return SemicolonBehavior.None; + return overtypedExisting ? SemicolonBehavior.Overtype : SemicolonBehavior.Insert; } - private static bool AdjustPositionForExistingSemicolon(SyntaxNode statementNode, ref SnapshotPoint targetPosition) - { - var existingSemicolon = statementNode.FindTokenOnRightOfPosition(targetPosition, includeSkipped: true); - if (existingSemicolon.IsKind(SyntaxKind.SemicolonToken) && !existingSemicolon.IsMissing) - { - targetPosition = new SnapshotPoint(targetPosition.Snapshot, existingSemicolon.Span.End); - return true; - } + return SemicolonBehavior.None; + } - return false; + private static bool AdjustPositionForExistingSemicolon(SyntaxNode statementNode, ref SnapshotPoint targetPosition) + { + var existingSemicolon = statementNode.FindTokenOnRightOfPosition(targetPosition, includeSkipped: true); + if (existingSemicolon.IsKind(SyntaxKind.SemicolonToken) && !existingSemicolon.IsMissing) + { + targetPosition = new SnapshotPoint(targetPosition.Snapshot, existingSemicolon.Span.End); + return true; } - private static bool TryGetCaretPositionToMove(SyntaxNode statementNode, SnapshotPoint caret, bool isInsideDelimiters, out SnapshotPoint targetPosition) - { - targetPosition = default; + return false; + } - switch (statementNode.Kind()) - { - case SyntaxKind.DoStatement: - // Move caret after the do statement's closing paren. - targetPosition = caret.Snapshot.GetPoint(((DoStatementSyntax)statementNode).CloseParenToken.Span.End); - return true; - case SyntaxKind.ForStatement: - // `For` statements can have semicolon after initializer/declaration or after condition. - // If caret is in initialer/declaration or condition, AND is inside other delimiters, complete statement - // Otherwise, return without moving the caret. - return isInsideDelimiters && TryGetForStatementCaret(caret, (ForStatementSyntax)statementNode, out targetPosition); - case SyntaxKind.ExpressionStatement: - case SyntaxKind.GotoCaseStatement: - case SyntaxKind.LocalDeclarationStatement: - case SyntaxKind.ReturnStatement: - case SyntaxKind.YieldReturnStatement: - case SyntaxKind.ThrowStatement: - case SyntaxKind.FieldDeclaration: - case SyntaxKind.DelegateDeclaration: - case SyntaxKind.ArrowExpressionClause: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.RecordDeclaration: - case SyntaxKind.EqualsValueClause: - case SyntaxKind.RecordStructDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.StructDeclaration: - case SyntaxKind.InterfaceDeclaration: - // These statement types end in a semicolon. - // if the original caret was inside any delimiters, `caret` will be after the outermost delimiter - targetPosition = caret; - return isInsideDelimiters; - default: - // For all other statement types, don't complete statement. Return without moving the caret. - return false; - } + private static bool TryGetCaretPositionToMove(SyntaxNode statementNode, SnapshotPoint caret, bool isInsideDelimiters, out SnapshotPoint targetPosition) + { + targetPosition = default; + + switch (statementNode.Kind()) + { + case SyntaxKind.DoStatement: + // Move caret after the do statement's closing paren. + targetPosition = caret.Snapshot.GetPoint(((DoStatementSyntax)statementNode).CloseParenToken.Span.End); + return true; + case SyntaxKind.ForStatement: + // `For` statements can have semicolon after initializer/declaration or after condition. + // If caret is in initialer/declaration or condition, AND is inside other delimiters, complete statement + // Otherwise, return without moving the caret. + return isInsideDelimiters && TryGetForStatementCaret(caret, (ForStatementSyntax)statementNode, out targetPosition); + case SyntaxKind.ExpressionStatement: + case SyntaxKind.GotoCaseStatement: + case SyntaxKind.LocalDeclarationStatement: + case SyntaxKind.ReturnStatement: + case SyntaxKind.YieldReturnStatement: + case SyntaxKind.ThrowStatement: + case SyntaxKind.FieldDeclaration: + case SyntaxKind.DelegateDeclaration: + case SyntaxKind.ArrowExpressionClause: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.EqualsValueClause: + case SyntaxKind.RecordStructDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.InterfaceDeclaration: + // These statement types end in a semicolon. + // if the original caret was inside any delimiters, `caret` will be after the outermost delimiter + targetPosition = caret; + return isInsideDelimiters; + default: + // For all other statement types, don't complete statement. Return without moving the caret. + return false; } + } - private static bool TryGetForStatementCaret(SnapshotPoint originalCaret, ForStatementSyntax forStatement, out SnapshotPoint forStatementCaret) + private static bool TryGetForStatementCaret(SnapshotPoint originalCaret, ForStatementSyntax forStatement, out SnapshotPoint forStatementCaret) + { + if (CaretIsInForStatementCondition(originalCaret, forStatement, out var condition)) { - if (CaretIsInForStatementCondition(originalCaret, forStatement, out var condition)) - { - forStatementCaret = GetCaretAtPosition(condition.Span.End); - } - else if (CaretIsInForStatementDeclaration(originalCaret, forStatement, out var declaration)) - { - forStatementCaret = GetCaretAtPosition(declaration.Span.End); - } - else if (CaretIsInForStatementInitializers(originalCaret, forStatement, out var relocatedPosition)) - { - forStatementCaret = GetCaretAtPosition(relocatedPosition); - } - else - { - // set caret to default, we will return false - forStatementCaret = default; - } + forStatementCaret = GetCaretAtPosition(condition.Span.End); + } + else if (CaretIsInForStatementDeclaration(originalCaret, forStatement, out var declaration)) + { + forStatementCaret = GetCaretAtPosition(declaration.Span.End); + } + else if (CaretIsInForStatementInitializers(originalCaret, forStatement, out var relocatedPosition)) + { + forStatementCaret = GetCaretAtPosition(relocatedPosition); + } + else + { + // set caret to default, we will return false + forStatementCaret = default; + } - return (forStatementCaret != default); + return (forStatementCaret != default); - // Locals - SnapshotPoint GetCaretAtPosition(int position) => originalCaret.Snapshot.GetPoint(position); - } + // Locals + SnapshotPoint GetCaretAtPosition(int position) => originalCaret.Snapshot.GetPoint(position); + } - private static bool CaretIsInForStatementCondition(int caretPosition, ForStatementSyntax forStatementSyntax, [NotNullWhen(true)] out ExpressionSyntax? condition) - { - condition = forStatementSyntax.Condition; - if (condition == null) - return false; + private static bool CaretIsInForStatementCondition(int caretPosition, ForStatementSyntax forStatementSyntax, [NotNullWhen(true)] out ExpressionSyntax? condition) + { + condition = forStatementSyntax.Condition; + if (condition == null) + return false; - // If condition is null and caret is in the condition section, as in `for ( ; $$; )`, - // we will have bailed earlier due to not being inside supported delimiters - return caretPosition > condition.SpanStart && caretPosition <= condition.Span.End; - } + // If condition is null and caret is in the condition section, as in `for ( ; $$; )`, + // we will have bailed earlier due to not being inside supported delimiters + return caretPosition > condition.SpanStart && caretPosition <= condition.Span.End; + } - private static bool CaretIsInForStatementDeclaration(int caretPosition, ForStatementSyntax forStatementSyntax, [NotNullWhen(true)] out VariableDeclarationSyntax? declaration) - { - declaration = forStatementSyntax.Declaration; - if (declaration == null) - return false; + private static bool CaretIsInForStatementDeclaration(int caretPosition, ForStatementSyntax forStatementSyntax, [NotNullWhen(true)] out VariableDeclarationSyntax? declaration) + { + declaration = forStatementSyntax.Declaration; + if (declaration == null) + return false; - return caretPosition > declaration.Span.Start && caretPosition <= declaration.Span.End; - } + return caretPosition > declaration.Span.Start && caretPosition <= declaration.Span.End; + } - private static bool CaretIsInForStatementInitializers(int caretPosition, ForStatementSyntax forStatementSyntax, out int relocatedPosition) + private static bool CaretIsInForStatementInitializers(int caretPosition, ForStatementSyntax forStatementSyntax, out int relocatedPosition) + { + if (forStatementSyntax.Initializers.Count != 0 + && caretPosition > forStatementSyntax.Initializers.Span.Start + && caretPosition <= forStatementSyntax.Initializers.Span.End) { - if (forStatementSyntax.Initializers.Count != 0 - && caretPosition > forStatementSyntax.Initializers.Span.Start - && caretPosition <= forStatementSyntax.Initializers.Span.End) + // Move the caret to the first missing separator, or to the end of the initializers list if all + // separators are present. + for (var separatorIndex = 0; separatorIndex < forStatementSyntax.Initializers.SeparatorCount; separatorIndex++) { - // Move the caret to the first missing separator, or to the end of the initializers list if all - // separators are present. - for (var separatorIndex = 0; separatorIndex < forStatementSyntax.Initializers.SeparatorCount; separatorIndex++) + var separator = forStatementSyntax.Initializers.GetSeparator(separatorIndex); + if (separator.IsMissing) { - var separator = forStatementSyntax.Initializers.GetSeparator(separatorIndex); - if (separator.IsMissing) - { - // We can't rely on the position of the missing separator, so move to the end of the last - // initializer preceding the missing separator. - relocatedPosition = forStatementSyntax.Initializers[separatorIndex].Span.End; - return true; - } + // We can't rely on the position of the missing separator, so move to the end of the last + // initializer preceding the missing separator. + relocatedPosition = forStatementSyntax.Initializers[separatorIndex].Span.End; + return true; } - - relocatedPosition = forStatementSyntax.Initializers.Span.End; - return true; } - relocatedPosition = default; - return false; + relocatedPosition = forStatementSyntax.Initializers.Span.End; + return true; } - private static bool IsInAStringOrCharacter(SyntaxNode currentNode, SnapshotPoint caret) + relocatedPosition = default; + return false; + } + + private static bool IsInAStringOrCharacter(SyntaxNode currentNode, SnapshotPoint caret) + { + // Check to see if caret is before or after string + if (currentNode.Kind() is not (SyntaxKind.InterpolatedStringExpression or SyntaxKind.StringLiteralExpression or SyntaxKind.Utf8StringLiteralExpression or SyntaxKind.CharacterLiteralExpression)) + return false; + + if (currentNode.IsKind(SyntaxKind.StringLiteralExpression, out LiteralExpressionSyntax? literalExpression) + && literalExpression.Token.Text.StartsWith("@")) { - // Check to see if caret is before or after string - if (currentNode.Kind() is not (SyntaxKind.InterpolatedStringExpression or SyntaxKind.StringLiteralExpression or SyntaxKind.Utf8StringLiteralExpression or SyntaxKind.CharacterLiteralExpression)) + // Verbatim strings start with @", so we only consider the caret to be inside the string if it's after the " + if (caret.Position <= currentNode.SpanStart + 1) return false; + } + else if (caret.Position <= currentNode.SpanStart) + { + return false; + } - if (currentNode.IsKind(SyntaxKind.StringLiteralExpression, out LiteralExpressionSyntax? literalExpression) - && literalExpression.Token.Text.StartsWith("@")) - { - // Verbatim strings start with @", so we only consider the caret to be inside the string if it's after the " - if (caret.Position <= currentNode.SpanStart + 1) - return false; - } - else if (caret.Position <= currentNode.SpanStart) - { - return false; - } + if (currentNode.IsKind(SyntaxKind.StringLiteralExpression, out literalExpression) + && (literalExpression.Token.Text.Length == 1 || literalExpression.Token.Text[^1] != '"')) + { + // This is an unterminated string literal, so we count the end of the span as included in the string + return caret.Position <= currentNode.Span.End; + } + else if (currentNode.IsKind(SyntaxKind.CharacterLiteralExpression, out literalExpression) + && (literalExpression.Token.Text.Length == 1 || literalExpression.Token.Text[^1] != '\'')) + { + // This is an unterminated character literal, so we count the end of the span as included in the character + return caret.Position <= currentNode.Span.End; + } + else if (currentNode.IsKind(SyntaxKind.Utf8StringLiteralExpression)) + { + // String literals are only considered utf-8 literals if they are terminated with a "u8 sequence. Only + // consider the caret inside the string if it precedes the position of the " in this sequence. + return caret.Position < currentNode.Span.End - 2; + } - if (currentNode.IsKind(SyntaxKind.StringLiteralExpression, out literalExpression) - && (literalExpression.Token.Text.Length == 1 || literalExpression.Token.Text[^1] != '"')) - { - // This is an unterminated string literal, so we count the end of the span as included in the string - return caret.Position <= currentNode.Span.End; - } - else if (currentNode.IsKind(SyntaxKind.CharacterLiteralExpression, out literalExpression) - && (literalExpression.Token.Text.Length == 1 || literalExpression.Token.Text[^1] != '\'')) - { - // This is an unterminated character literal, so we count the end of the span as included in the character - return caret.Position <= currentNode.Span.End; - } - else if (currentNode.IsKind(SyntaxKind.Utf8StringLiteralExpression)) - { - // String literals are only considered utf-8 literals if they are terminated with a "u8 sequence. Only - // consider the caret inside the string if it precedes the position of the " in this sequence. - return caret.Position < currentNode.Span.End - 2; - } + return caret.Position < currentNode.Span.End; + } - return caret.Position < currentNode.Span.End; + /// + /// Determines if a statement ends with a closing delimiter, and that closing delimiter exists. + /// + /// + /// Statements such as do { } while (expression); contain embedded enclosing delimiters immediately + /// preceding the semicolon. These delimiters are not part of the expression, but they behave like an argument + /// list for the purposes of identifying relevant places for statement completion: + /// + /// The closing delimiter is typically inserted by the Automatic Brace Completion feature. + /// It is not syntactically valid to place a semicolon directly within the delimiters. + /// + /// + /// + /// if is a statement that ends with a closing + /// delimiter, and that closing delimiter exists in the source code; otherwise, . + /// + private static bool StatementClosingDelimiterIsMissing(SyntaxNode currentNode) + { + switch (currentNode.Kind()) + { + case SyntaxKind.DoStatement: + var dostatement = (DoStatementSyntax)currentNode; + return dostatement.CloseParenToken.IsMissing; + case SyntaxKind.ForStatement: + var forStatement = (ForStatementSyntax)currentNode; + return forStatement.CloseParenToken.IsMissing; + default: + return false; } + } - /// - /// Determines if a statement ends with a closing delimiter, and that closing delimiter exists. - /// - /// - /// Statements such as do { } while (expression); contain embedded enclosing delimiters immediately - /// preceding the semicolon. These delimiters are not part of the expression, but they behave like an argument - /// list for the purposes of identifying relevant places for statement completion: - /// - /// The closing delimiter is typically inserted by the Automatic Brace Completion feature. - /// It is not syntactically valid to place a semicolon directly within the delimiters. - /// - /// - /// - /// if is a statement that ends with a closing - /// delimiter, and that closing delimiter exists in the source code; otherwise, . - /// - private static bool StatementClosingDelimiterIsMissing(SyntaxNode currentNode) - { - switch (currentNode.Kind()) - { - case SyntaxKind.DoStatement: - var dostatement = (DoStatementSyntax)currentNode; - return dostatement.CloseParenToken.IsMissing; - case SyntaxKind.ForStatement: - var forStatement = (ForStatementSyntax)currentNode; - return forStatement.CloseParenToken.IsMissing; - default: - return false; - } + /// + /// Determines if a syntax node includes all required closing delimiters. + /// + /// + /// Some syntax nodes, such as parenthesized expressions, require a matching closing delimiter to end the + /// syntax node. If this node is omitted from the source code, the parser will automatically insert a zero-width + /// "missing" closing delimiter token to produce a valid syntax tree. This method determines if required closing + /// delimiters are present in the original source. + /// + /// + /// + /// + /// if requires a closing delimiter and the closing delimiter is present in the source (i.e. not missing) + /// if does not require a closing delimiter + /// otherwise, . + /// + /// + private static bool RequiredDelimiterIsMissing(SyntaxNode currentNode) + { + if (currentNode.GetBrackets().closeBracket.IsMissing + || currentNode.GetParentheses().closeParen.IsMissing) + { + return true; } - /// - /// Determines if a syntax node includes all required closing delimiters. - /// - /// - /// Some syntax nodes, such as parenthesized expressions, require a matching closing delimiter to end the - /// syntax node. If this node is omitted from the source code, the parser will automatically insert a zero-width - /// "missing" closing delimiter token to produce a valid syntax tree. This method determines if required closing - /// delimiters are present in the original source. - /// - /// - /// - /// - /// if requires a closing delimiter and the closing delimiter is present in the source (i.e. not missing) - /// if does not require a closing delimiter - /// otherwise, . - /// - /// - private static bool RequiredDelimiterIsMissing(SyntaxNode currentNode) - { - if (currentNode.GetBrackets().closeBracket.IsMissing - || currentNode.GetParentheses().closeParen.IsMissing) + // If the current node has braces, we also need to make sure parent constructs are not missing braces + var braces = currentNode.GetBraces(); + if (braces.openBrace.IsKind(SyntaxKind.OpenBraceToken)) + { + if (braces.closeBrace.IsMissing) { return true; } - // If the current node has braces, we also need to make sure parent constructs are not missing braces - var braces = currentNode.GetBraces(); - if (braces.openBrace.IsKind(SyntaxKind.OpenBraceToken)) + for (var node = currentNode.Parent; node is not null; node = node.Parent) { - if (braces.closeBrace.IsMissing) - { + if (node.GetBraces().closeBrace.IsMissing) return true; - } - - for (var node = currentNode.Parent; node is not null; node = node.Parent) - { - if (node.GetBraces().closeBrace.IsMissing) - return true; - } } - - return false; } - private static bool HasDelimitersButCaretIsOutside(SyntaxNode currentNode, int caretPosition) + return false; + } + + private static bool HasDelimitersButCaretIsOutside(SyntaxNode currentNode, int caretPosition) + { + if (currentNode.GetParentheses() is ((not SyntaxKind.None) openParenthesis, (not SyntaxKind.None) closeParenthesis)) { - if (currentNode.GetParentheses() is ((not SyntaxKind.None) openParenthesis, (not SyntaxKind.None) closeParenthesis)) - { - return openParenthesis.SpanStart >= caretPosition || closeParenthesis.Span.End <= caretPosition; - } - else if (currentNode.GetBrackets() is ((not SyntaxKind.None) openBracket, (not SyntaxKind.None) closeBracket)) - { - return openBracket.SpanStart >= caretPosition || closeBracket.Span.End <= caretPosition; - } - else if (currentNode.GetBraces() is ((not SyntaxKind.None) openBrace, (not SyntaxKind.None) closeBrace)) - { - return openBrace.SpanStart >= caretPosition || closeBrace.Span.End <= caretPosition; - } - else - { - return false; - } + return openParenthesis.SpanStart >= caretPosition || closeParenthesis.Span.End <= caretPosition; + } + else if (currentNode.GetBrackets() is ((not SyntaxKind.None) openBracket, (not SyntaxKind.None) closeBracket)) + { + return openBracket.SpanStart >= caretPosition || closeBracket.Span.End <= caretPosition; + } + else if (currentNode.GetBraces() is ((not SyntaxKind.None) openBrace, (not SyntaxKind.None) closeBrace)) + { + return openBrace.SpanStart >= caretPosition || closeBrace.Span.End <= caretPosition; + } + else + { + return false; } } } diff --git a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementOptionsStorage.cs b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementOptionsStorage.cs index 1ddf87cf966b2..16ad236a1b169 100644 --- a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementOptionsStorage.cs +++ b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.CSharp.CompleteStatement +namespace Microsoft.CodeAnalysis.Editor.CSharp.CompleteStatement; + +internal static class CompleteStatementOptionsStorage { - internal static class CompleteStatementOptionsStorage - { - public static readonly Option2 AutomaticallyCompleteStatementOnSemicolon = new("csharp_complete_statement_on_semicolon", defaultValue: true); - } + public static readonly Option2 AutomaticallyCompleteStatementOnSemicolon = new("csharp_complete_statement_on_semicolon", defaultValue: true); } diff --git a/src/EditorFeatures/CSharp/ContentType/ContentTypeDefinitions.cs b/src/EditorFeatures/CSharp/ContentType/ContentTypeDefinitions.cs index ea636d1fd21d8..3b8475ae630c1 100644 --- a/src/EditorFeatures/CSharp/ContentType/ContentTypeDefinitions.cs +++ b/src/EditorFeatures/CSharp/ContentType/ContentTypeDefinitions.cs @@ -5,25 +5,24 @@ using System.ComponentModel.Composition; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.ContentType +namespace Microsoft.CodeAnalysis.Editor.CSharp.ContentType; + +internal static class ContentTypeDefinitions { - internal static class ContentTypeDefinitions - { - /// - /// Definition of the primary C# content type. - /// - [Export] - [Name(ContentTypeNames.CSharpContentType)] - [BaseDefinition(ContentTypeNames.RoslynContentType)] - // Adds the LSP base content type to ensure the LSP client activates on C# files. - // From Microsoft.VisualStudio.LanguageServer.Client.CodeRemoteContentDefinition.CodeRemoteBaseTypeName - // We cannot directly reference the LSP client package in EditorFeatures as it is a VS dependency. - [BaseDefinition("code-languageserver-base")] - public static readonly ContentTypeDefinition CSharpContentTypeDefinition = null!; + /// + /// Definition of the primary C# content type. + /// + [Export] + [Name(ContentTypeNames.CSharpContentType)] + [BaseDefinition(ContentTypeNames.RoslynContentType)] + // Adds the LSP base content type to ensure the LSP client activates on C# files. + // From Microsoft.VisualStudio.LanguageServer.Client.CodeRemoteContentDefinition.CodeRemoteBaseTypeName + // We cannot directly reference the LSP client package in EditorFeatures as it is a VS dependency. + [BaseDefinition("code-languageserver-base")] + public static readonly ContentTypeDefinition CSharpContentTypeDefinition = null!; - [Export] - [Name(ContentTypeNames.CSharpSignatureHelpContentType)] - [BaseDefinition("sighelp")] - public static readonly ContentTypeDefinition SignatureHelpContentTypeDefinition = null!; - } + [Export] + [Name(ContentTypeNames.CSharpSignatureHelpContentType)] + [BaseDefinition("sighelp")] + public static readonly ContentTypeDefinition SignatureHelpContentTypeDefinition = null!; } diff --git a/src/EditorFeatures/CSharp/ConvertNamespace/ConvertNamespaceCommandHandler.cs b/src/EditorFeatures/CSharp/ConvertNamespace/ConvertNamespaceCommandHandler.cs index f77a1a39b3ffd..4ad682630baff 100644 --- a/src/EditorFeatures/CSharp/ConvertNamespace/ConvertNamespaceCommandHandler.cs +++ b/src/EditorFeatures/CSharp/ConvertNamespace/ConvertNamespaceCommandHandler.cs @@ -24,125 +24,124 @@ using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.CompleteStatement +namespace Microsoft.CodeAnalysis.Editor.CSharp.CompleteStatement; + +/// +/// Converts a block-scoped namespace to a file-scoped one if the user types ; after its name. +/// +[Export(typeof(ICommandHandler))] +[Export] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(nameof(ConvertNamespaceCommandHandler))] +[Order(After = PredefinedCompletionNames.CompletionCommandHandler)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class ConvertNamespaceCommandHandler( + ITextUndoHistoryRegistry textUndoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService, + IGlobalOptionService globalOptions, + IIndentationManagerService indentationManager) : IChainedCommandHandler { /// - /// Converts a block-scoped namespace to a file-scoped one if the user types ; after its name. + /// Option setting 'use file scoped'. That way we can call into the helpers + /// and have the results come back positive for converting to file-scoped regardless of the current option + /// value. /// - [Export(typeof(ICommandHandler))] - [Export] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(nameof(ConvertNamespaceCommandHandler))] - [Order(After = PredefinedCompletionNames.CompletionCommandHandler)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class ConvertNamespaceCommandHandler( - ITextUndoHistoryRegistry textUndoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService, - IGlobalOptionService globalOptions, - IIndentationManagerService indentationManager) : IChainedCommandHandler + private static readonly CodeStyleOption2 s_fileScopedNamespacePreferenceOption = + new(NamespaceDeclarationPreference.FileScoped, NotificationOption2.Suggestion); + + private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry = textUndoHistoryRegistry; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly IIndentationManagerService _indentationManager = indentationManager; + private readonly IGlobalOptionService _globalOptions = globalOptions; + + public CommandState GetCommandState(TypeCharCommandArgs args, Func nextCommandHandler) + => nextCommandHandler(); + + public string DisplayName => CSharpAnalyzersResources.Convert_to_file_scoped_namespace; + + public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) { - /// - /// Option setting 'use file scoped'. That way we can call into the helpers - /// and have the results come back positive for converting to file-scoped regardless of the current option - /// value. - /// - private static readonly CodeStyleOption2 s_fileScopedNamespacePreferenceOption = - new(NamespaceDeclarationPreference.FileScoped, NotificationOption2.Suggestion); - - private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry = textUndoHistoryRegistry; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - private readonly IIndentationManagerService _indentationManager = indentationManager; - private readonly IGlobalOptionService _globalOptions = globalOptions; - - public CommandState GetCommandState(TypeCharCommandArgs args, Func nextCommandHandler) - => nextCommandHandler(); - - public string DisplayName => CSharpAnalyzersResources.Convert_to_file_scoped_namespace; - - public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) - { - // Attempt to convert the block-namespace to a file-scoped namespace if we're at the right location. - var (convertedText, semicolonSpan) = ConvertNamespace(args, executionContext); + // Attempt to convert the block-namespace to a file-scoped namespace if we're at the right location. + var (convertedText, semicolonSpan) = ConvertNamespace(args, executionContext); - // No matter if we succeeded or not, insert the semicolon. This way, when we convert, the user can still - // hit ctrl-z to get back to the code with just the semicolon inserted. - nextCommandHandler(); + // No matter if we succeeded or not, insert the semicolon. This way, when we convert, the user can still + // hit ctrl-z to get back to the code with just the semicolon inserted. + nextCommandHandler(); - // If we weren't on a block namespace (or couldn't convert it for some reason), then bail out after - // inserting the semicolon. - if (convertedText == null) - return; + // If we weren't on a block namespace (or couldn't convert it for some reason), then bail out after + // inserting the semicolon. + if (convertedText == null) + return; - // Otherwise, make a transaction for the edit and replace the buffer with the final text. - using var transaction = CaretPreservingEditTransaction.TryCreate( - this.DisplayName, args.TextView, _textUndoHistoryRegistry, _editorOperationsFactoryService); + // Otherwise, make a transaction for the edit and replace the buffer with the final text. + using var transaction = CaretPreservingEditTransaction.TryCreate( + this.DisplayName, args.TextView, _textUndoHistoryRegistry, _editorOperationsFactoryService); - var edit = args.SubjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null); - edit.Replace(new Span(0, args.SubjectBuffer.CurrentSnapshot.Length), convertedText.ToString()); + var edit = args.SubjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null); + edit.Replace(new Span(0, args.SubjectBuffer.CurrentSnapshot.Length), convertedText.ToString()); - edit.Apply(); + edit.Apply(); - // Place the caret right after the semicolon of the file-scoped namespace. - args.TextView.Caret.MoveTo(new SnapshotPoint(args.SubjectBuffer.CurrentSnapshot, semicolonSpan.End)); + // Place the caret right after the semicolon of the file-scoped namespace. + args.TextView.Caret.MoveTo(new SnapshotPoint(args.SubjectBuffer.CurrentSnapshot, semicolonSpan.End)); - transaction?.Complete(); - } + transaction?.Complete(); + } + + /// + /// Returns the updated file contents if semicolon is typed after a block-scoped namespace name that can be + /// converted. + /// + private (SourceText? convertedText, TextSpan semicolonSpan) ConvertNamespace( + TypeCharCommandArgs args, + CommandExecutionContext executionContext) + { + if (args.TypedChar != ';' || !args.TextView.Selection.IsEmpty) + return default; + + if (!_globalOptions.GetOption(CompleteStatementOptionsStorage.AutomaticallyCompleteStatementOnSemicolon)) + return default; + + var subjectBuffer = args.SubjectBuffer; + var caretOpt = args.TextView.GetCaretPoint(subjectBuffer); + if (!caretOpt.HasValue) + return default; + + var caret = caretOpt.Value.Position; + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return default; + + var cancellationToken = executionContext.OperationContext.UserCancellationToken; + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - /// - /// Returns the updated file contents if semicolon is typed after a block-scoped namespace name that can be - /// converted. - /// - private (SourceText? convertedText, TextSpan semicolonSpan) ConvertNamespace( - TypeCharCommandArgs args, - CommandExecutionContext executionContext) + // User has to be *after* an identifier token. + var token = parsedDocument.Root.FindToken(caret); + if (token.Kind() != SyntaxKind.IdentifierToken) + return default; + + if (caret < token.Span.End || + caret >= token.FullSpan.End) { - if (args.TypedChar != ';' || !args.TextView.Selection.IsEmpty) - return default; - - if (!_globalOptions.GetOption(CompleteStatementOptionsStorage.AutomaticallyCompleteStatementOnSemicolon)) - return default; - - var subjectBuffer = args.SubjectBuffer; - var caretOpt = args.TextView.GetCaretPoint(subjectBuffer); - if (!caretOpt.HasValue) - return default; - - var caret = caretOpt.Value.Position; - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return default; - - var cancellationToken = executionContext.OperationContext.UserCancellationToken; - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - - // User has to be *after* an identifier token. - var token = parsedDocument.Root.FindToken(caret); - if (token.Kind() != SyntaxKind.IdentifierToken) - return default; - - if (caret < token.Span.End || - caret >= token.FullSpan.End) - { - return default; - } - - var namespaceDecl = token.GetRequiredParent().GetAncestor(); - if (namespaceDecl == null) - return default; - - // That identifier token has to be the last part of a namespace name. - if (namespaceDecl.Name.GetLastToken() != token) - return default; - - // Pass in our special options, and C#10 so that if we can convert this to file-scoped, we will. - if (!ConvertNamespaceAnalysis.CanOfferUseFileScoped(s_fileScopedNamespacePreferenceOption, (CompilationUnitSyntax)parsedDocument.Root, namespaceDecl, forAnalyzer: true, LanguageVersion.CSharp10)) - return default; - - var formattingOptions = subjectBuffer.GetSyntaxFormattingOptions(_editorOptionsService, document.Project.Services, explicitFormat: false); - return ConvertNamespaceTransform.ConvertNamespaceDeclaration(parsedDocument, namespaceDecl, formattingOptions, cancellationToken); + return default; } + + var namespaceDecl = token.GetRequiredParent().GetAncestor(); + if (namespaceDecl == null) + return default; + + // That identifier token has to be the last part of a namespace name. + if (namespaceDecl.Name.GetLastToken() != token) + return default; + + // Pass in our special options, and C#10 so that if we can convert this to file-scoped, we will. + if (!ConvertNamespaceAnalysis.CanOfferUseFileScoped(s_fileScopedNamespacePreferenceOption, (CompilationUnitSyntax)parsedDocument.Root, namespaceDecl, forAnalyzer: true, LanguageVersion.CSharp10)) + return default; + + var formattingOptions = subjectBuffer.GetSyntaxFormattingOptions(_editorOptionsService, document.Project.Services, explicitFormat: false); + return ConvertNamespaceTransform.ConvertNamespaceDeclaration(parsedDocument, namespaceDecl, formattingOptions, cancellationToken); } } diff --git a/src/EditorFeatures/CSharp/DocumentationComments/DocumentationCommentCommandHandler.cs b/src/EditorFeatures/CSharp/DocumentationComments/DocumentationCommentCommandHandler.cs index 17a51026a96cc..9c56b5ff7ce90 100644 --- a/src/EditorFeatures/CSharp/DocumentationComments/DocumentationCommentCommandHandler.cs +++ b/src/EditorFeatures/CSharp/DocumentationComments/DocumentationCommentCommandHandler.cs @@ -15,22 +15,21 @@ using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.DocumentationComments +namespace Microsoft.CodeAnalysis.Editor.CSharp.DocumentationComments; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(PredefinedCommandHandlerNames.DocumentationComments)] +[Order(After = PredefinedCommandHandlerNames.Rename)] +[Order(After = PredefinedCompletionNames.CompletionCommandHandler)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class DocumentationCommentCommandHandler( + IUIThreadOperationExecutor uiThreadOperationExecutor, + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService) + : AbstractDocumentationCommentCommandHandler(uiThreadOperationExecutor, undoHistoryRegistry, editorOperationsFactoryService, editorOptionsService) { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(PredefinedCommandHandlerNames.DocumentationComments)] - [Order(After = PredefinedCommandHandlerNames.Rename)] - [Order(After = PredefinedCompletionNames.CompletionCommandHandler)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class DocumentationCommentCommandHandler( - IUIThreadOperationExecutor uiThreadOperationExecutor, - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService) - : AbstractDocumentationCommentCommandHandler(uiThreadOperationExecutor, undoHistoryRegistry, editorOperationsFactoryService, editorOptionsService) - { - protected override string ExteriorTriviaText => "///"; - } + protected override string ExteriorTriviaText => "///"; } diff --git a/src/EditorFeatures/CSharp/DocumentationComments/XmlTagCompletionCommandHandler.cs b/src/EditorFeatures/CSharp/DocumentationComments/XmlTagCompletionCommandHandler.cs index 4156c0022bac5..2cc89c2763e00 100644 --- a/src/EditorFeatures/CSharp/DocumentationComments/XmlTagCompletionCommandHandler.cs +++ b/src/EditorFeatures/CSharp/DocumentationComments/XmlTagCompletionCommandHandler.cs @@ -12,35 +12,34 @@ using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.DocumentationComments +namespace Microsoft.CodeAnalysis.Editor.CSharp.DocumentationComments; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(nameof(XmlTagCompletionCommandHandler))] +[Order(Before = PredefinedCompletionNames.CompletionCommandHandler)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class XmlTagCompletionCommandHandler(ITextUndoHistoryRegistry undoHistory) : AbstractXmlTagCompletionCommandHandler< + XmlNameSyntax, + XmlTextSyntax, + XmlElementSyntax, + XmlElementStartTagSyntax, + XmlElementEndTagSyntax, + DocumentationCommentTriviaSyntax>(undoHistory) { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(nameof(XmlTagCompletionCommandHandler))] - [Order(Before = PredefinedCompletionNames.CompletionCommandHandler)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class XmlTagCompletionCommandHandler(ITextUndoHistoryRegistry undoHistory) : AbstractXmlTagCompletionCommandHandler< - XmlNameSyntax, - XmlTextSyntax, - XmlElementSyntax, - XmlElementStartTagSyntax, - XmlElementEndTagSyntax, - DocumentationCommentTriviaSyntax>(undoHistory) - { - protected override XmlElementStartTagSyntax GetStartTag(XmlElementSyntax xmlElement) - => xmlElement.StartTag; + protected override XmlElementStartTagSyntax GetStartTag(XmlElementSyntax xmlElement) + => xmlElement.StartTag; - protected override XmlElementEndTagSyntax GetEndTag(XmlElementSyntax xmlElement) - => xmlElement.EndTag; + protected override XmlElementEndTagSyntax GetEndTag(XmlElementSyntax xmlElement) + => xmlElement.EndTag; - protected override XmlNameSyntax GetName(XmlElementEndTagSyntax endTag) - => endTag.Name; + protected override XmlNameSyntax GetName(XmlElementEndTagSyntax endTag) + => endTag.Name; - protected override XmlNameSyntax GetName(XmlElementStartTagSyntax startTag) - => startTag.Name; + protected override XmlNameSyntax GetName(XmlElementStartTagSyntax startTag) + => startTag.Name; - protected override SyntaxToken GetLocalName(XmlNameSyntax name) - => name.LocalName; - } + protected override SyntaxToken GetLocalName(XmlNameSyntax name) + => name.LocalName; } diff --git a/src/EditorFeatures/CSharp/EncapsulateField/EncapsulateFieldCommandHandler.cs b/src/EditorFeatures/CSharp/EncapsulateField/EncapsulateFieldCommandHandler.cs index 86a7e0be54630..48aa771a49a1e 100644 --- a/src/EditorFeatures/CSharp/EncapsulateField/EncapsulateFieldCommandHandler.cs +++ b/src/EditorFeatures/CSharp/EncapsulateField/EncapsulateFieldCommandHandler.cs @@ -13,19 +13,18 @@ using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.EncapsulateField +namespace Microsoft.CodeAnalysis.Editor.CSharp.EncapsulateField; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(PredefinedCommandHandlerNames.EncapsulateField)] +[Order(After = PredefinedCommandHandlerNames.DocumentationComments)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class EncapsulateFieldCommandHandler( + IThreadingContext threadingContext, + ITextBufferUndoManagerProvider undoManager, + IGlobalOptionService globalOptions, + IAsynchronousOperationListenerProvider listenerProvider) : AbstractEncapsulateFieldCommandHandler(threadingContext, undoManager, globalOptions, listenerProvider) { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(PredefinedCommandHandlerNames.EncapsulateField)] - [Order(After = PredefinedCommandHandlerNames.DocumentationComments)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class EncapsulateFieldCommandHandler( - IThreadingContext threadingContext, - ITextBufferUndoManagerProvider undoManager, - IGlobalOptionService globalOptions, - IAsynchronousOperationListenerProvider listenerProvider) : AbstractEncapsulateFieldCommandHandler(threadingContext, undoManager, globalOptions, listenerProvider) - { - } } diff --git a/src/EditorFeatures/CSharp/EndConstruct/CSharpEndConstructGenerationService.cs b/src/EditorFeatures/CSharp/EndConstruct/CSharpEndConstructGenerationService.cs index 32b394789c129..d44bf6e7c6a66 100644 --- a/src/EditorFeatures/CSharp/EndConstruct/CSharpEndConstructGenerationService.cs +++ b/src/EditorFeatures/CSharp/EndConstruct/CSharpEndConstructGenerationService.cs @@ -11,25 +11,24 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor.CSharp.EndConstructGeneration +namespace Microsoft.CodeAnalysis.Editor.CSharp.EndConstructGeneration; + +[ExportLanguageService(typeof(IEndConstructGenerationService), LanguageNames.CSharp), Shared] +[ExcludeFromCodeCoverage] +internal class CSharpEndConstructGenerationService : IEndConstructGenerationService { - [ExportLanguageService(typeof(IEndConstructGenerationService), LanguageNames.CSharp), Shared] - [ExcludeFromCodeCoverage] - internal class CSharpEndConstructGenerationService : IEndConstructGenerationService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpEndConstructGenerationService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpEndConstructGenerationService() - { - } + } - public bool TryDo( - ITextView textView, - ITextBuffer subjectBuffer, - char typedChar, - CancellationToken cancellationToken) - { - return false; - } + public bool TryDo( + ITextView textView, + ITextBuffer subjectBuffer, + char typedChar, + CancellationToken cancellationToken) + { + return false; } } diff --git a/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler.cs b/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler.cs index adab3f346faf4..a2b546fd7fcaf 100644 --- a/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler.cs +++ b/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler.cs @@ -13,48 +13,47 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup +namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup; + +/// +/// Ignores commands until '=' is pressed, at which point we determine if the '=' is part of a +/// "+=" that is used to attach an event handler to an event. Once we determine that it is a +/// "+=" attaching an event handler to an event, we show a UI that tells the user they can hit +/// tab to generate a handler method. +/// +/// Once we receive the '=' we watch all actions within the buffer. Anything (including use of +/// directional arrows) other than a typed space removes the UI or cancels any background +/// computation. +/// +/// The determination of whether the "+=" is being used to attach an event handler to an event +/// can be costly, so it operates on a background thread. After the '=' of a "+=" is typed, +/// only a tab will cause the UI thread to block while it determines whether we should +/// intercept the tab and generate an event handler or just let the tab through to other +/// handlers. +/// +/// Because we are explicitly asking the user to tab, so we should handle the tab command before +/// Automatic Completion. +/// +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(PredefinedCommandHandlerNames.EventHookup)] +[Order(Before = PredefinedCommandHandlerNames.AutomaticCompletion)] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal partial class EventHookupCommandHandler( + IThreadingContext threadingContext, + IInlineRenameService inlineRenameService, + EventHookupSessionManager eventHookupSessionManager, + IGlobalOptionService globalOptions, + IAsynchronousOperationListenerProvider listenerProvider) { - /// - /// Ignores commands until '=' is pressed, at which point we determine if the '=' is part of a - /// "+=" that is used to attach an event handler to an event. Once we determine that it is a - /// "+=" attaching an event handler to an event, we show a UI that tells the user they can hit - /// tab to generate a handler method. - /// - /// Once we receive the '=' we watch all actions within the buffer. Anything (including use of - /// directional arrows) other than a typed space removes the UI or cancels any background - /// computation. - /// - /// The determination of whether the "+=" is being used to attach an event handler to an event - /// can be costly, so it operates on a background thread. After the '=' of a "+=" is typed, - /// only a tab will cause the UI thread to block while it determines whether we should - /// intercept the tab and generate an event handler or just let the tab through to other - /// handlers. - /// - /// Because we are explicitly asking the user to tab, so we should handle the tab command before - /// Automatic Completion. - /// - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(PredefinedCommandHandlerNames.EventHookup)] - [Order(Before = PredefinedCommandHandlerNames.AutomaticCompletion)] - [method: ImportingConstructor] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal partial class EventHookupCommandHandler( - IThreadingContext threadingContext, - IInlineRenameService inlineRenameService, - EventHookupSessionManager eventHookupSessionManager, - IGlobalOptionService globalOptions, - IAsynchronousOperationListenerProvider listenerProvider) - { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IInlineRenameService _inlineRenameService = inlineRenameService; - private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.EventHookup); - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IInlineRenameService _inlineRenameService = inlineRenameService; + private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.EventHookup); + private readonly IGlobalOptionService _globalOptions = globalOptions; - internal readonly EventHookupSessionManager EventHookupSessionManager = eventHookupSessionManager; + internal readonly EventHookupSessionManager EventHookupSessionManager = eventHookupSessionManager; - // For testing purposes only! Will always be null except in certain tests. - internal Mutex TESTSessionHookupMutex; - } + // For testing purposes only! Will always be null except in certain tests. + internal Mutex TESTSessionHookupMutex; } diff --git a/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_SessionCancellingCommands.cs b/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_SessionCancellingCommands.cs index 1e2c981f3cb78..c83b4957eea8c 100644 --- a/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_SessionCancellingCommands.cs +++ b/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_SessionCancellingCommands.cs @@ -9,24 +9,23 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup +namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup; + +internal partial class EventHookupCommandHandler : + ICommandHandler { - internal partial class EventHookupCommandHandler : - ICommandHandler - { - public string DisplayName => CSharpEditorResources.Generate_Event_Subscription; + public string DisplayName => CSharpEditorResources.Generate_Event_Subscription; - public bool ExecuteCommand(EscapeKeyCommandArgs args, CommandExecutionContext context) - { - _threadingContext.ThrowIfNotOnUIThread(); - EventHookupSessionManager.CancelAndDismissExistingSessions(); - return false; - } + public bool ExecuteCommand(EscapeKeyCommandArgs args, CommandExecutionContext context) + { + _threadingContext.ThrowIfNotOnUIThread(); + EventHookupSessionManager.CancelAndDismissExistingSessions(); + return false; + } - public CommandState GetCommandState(EscapeKeyCommandArgs args) - { - _threadingContext.ThrowIfNotOnUIThread(); - return CommandState.Unspecified; - } + public CommandState GetCommandState(EscapeKeyCommandArgs args) + { + _threadingContext.ThrowIfNotOnUIThread(); + return CommandState.Unspecified; } } diff --git a/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_TabKeyCommand.cs b/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_TabKeyCommand.cs index b0799dba49f1d..c4c3702a81936 100644 --- a/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_TabKeyCommand.cs +++ b/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_TabKeyCommand.cs @@ -33,285 +33,284 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup +namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup; + +internal partial class EventHookupCommandHandler : IChainedCommandHandler { - internal partial class EventHookupCommandHandler : IChainedCommandHandler + public void ExecuteCommand(TabKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) { - public void ExecuteCommand(TabKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) + _threadingContext.ThrowIfNotOnUIThread(); + if (!_globalOptions.GetOption(EventHookupOptionsStorage.EventHookup)) { - _threadingContext.ThrowIfNotOnUIThread(); - if (!_globalOptions.GetOption(EventHookupOptionsStorage.EventHookup)) - { - nextHandler(); - return; - } + nextHandler(); + return; + } - if (EventHookupSessionManager.CurrentSession == null) - { - nextHandler(); - return; - } + if (EventHookupSessionManager.CurrentSession == null) + { + nextHandler(); + return; + } + + // Handling tab is currently uncancellable. + HandleTabWorker(args.TextView, args.SubjectBuffer, nextHandler, CancellationToken.None); + } - // Handling tab is currently uncancellable. - HandleTabWorker(args.TextView, args.SubjectBuffer, nextHandler, CancellationToken.None); + public CommandState GetCommandState(TabKeyCommandArgs args, Func nextHandler) + { + _threadingContext.ThrowIfNotOnUIThread(); + if (EventHookupSessionManager.CurrentSession != null) + { + return CommandState.Available; } + else + { + return nextHandler(); + } + } + + private void HandleTabWorker(ITextView textView, ITextBuffer subjectBuffer, Action nextHandler, CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnUIThread(); - public CommandState GetCommandState(TabKeyCommandArgs args, Func nextHandler) + // For test purposes only! + if (EventHookupSessionManager.CurrentSession.TESTSessionHookupMutex != null) { - _threadingContext.ThrowIfNotOnUIThread(); - if (EventHookupSessionManager.CurrentSession != null) + try { - return CommandState.Available; + EventHookupSessionManager.CurrentSession.TESTSessionHookupMutex.ReleaseMutex(); } - else + catch (ApplicationException) { - return nextHandler(); } } - private void HandleTabWorker(ITextView textView, ITextBuffer subjectBuffer, Action nextHandler, CancellationToken cancellationToken) + // Blocking wait (if necessary) to determine whether to consume the tab and + // generate the event handler. + EventHookupSessionManager.CurrentSession.GetEventNameTask.Wait(cancellationToken); + + string eventHandlerMethodName = null; + if (EventHookupSessionManager.CurrentSession.GetEventNameTask.Status == TaskStatus.RanToCompletion) { - _threadingContext.ThrowIfNotOnUIThread(); + eventHandlerMethodName = EventHookupSessionManager.CurrentSession.GetEventNameTask.WaitAndGetResult(cancellationToken); + } - // For test purposes only! - if (EventHookupSessionManager.CurrentSession.TESTSessionHookupMutex != null) - { - try - { - EventHookupSessionManager.CurrentSession.TESTSessionHookupMutex.ReleaseMutex(); - } - catch (ApplicationException) - { - } - } + if (eventHandlerMethodName == null || + EventHookupSessionManager.CurrentSession.TextView != textView) + { + nextHandler(); + EventHookupSessionManager.CancelAndDismissExistingSessions(); + return; + } - // Blocking wait (if necessary) to determine whether to consume the tab and - // generate the event handler. - EventHookupSessionManager.CurrentSession.GetEventNameTask.Wait(cancellationToken); + // This tab means we should generate the event handler method. Begin the code + // generation process. + GenerateAndAddEventHandler(textView, subjectBuffer, eventHandlerMethodName, nextHandler, cancellationToken); + } - string eventHandlerMethodName = null; - if (EventHookupSessionManager.CurrentSession.GetEventNameTask.Status == TaskStatus.RanToCompletion) - { - eventHandlerMethodName = EventHookupSessionManager.CurrentSession.GetEventNameTask.WaitAndGetResult(cancellationToken); - } + private void GenerateAndAddEventHandler(ITextView textView, ITextBuffer subjectBuffer, string eventHandlerMethodName, Action nextHandler, CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnUIThread(); + + using (Logger.LogBlock(FunctionId.EventHookup_Generate_Handler, cancellationToken)) + { + EventHookupSessionManager.CancelAndDismissExistingSessions(); - if (eventHandlerMethodName == null || - EventHookupSessionManager.CurrentSession.TextView != textView) + var workspace = textView.TextSnapshot.TextBuffer.GetWorkspace(); + if (workspace == null) { nextHandler(); EventHookupSessionManager.CancelAndDismissExistingSessions(); return; } - // This tab means we should generate the event handler method. Begin the code - // generation process. - GenerateAndAddEventHandler(textView, subjectBuffer, eventHandlerMethodName, nextHandler, cancellationToken); - } + var document = textView.TextSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + Contract.ThrowIfNull(document, "Event Hookup could not find the document for the IBufferView."); - private void GenerateAndAddEventHandler(ITextView textView, ITextBuffer subjectBuffer, string eventHandlerMethodName, Action nextHandler, CancellationToken cancellationToken) - { - _threadingContext.ThrowIfNotOnUIThread(); + var position = textView.GetCaretPoint(subjectBuffer).Value.Position; + var solutionWithEventHandler = CreateSolutionWithEventHandler( + document, + eventHandlerMethodName, + position, + out var plusEqualTokenEndPosition, + _globalOptions, + cancellationToken); - using (Logger.LogBlock(FunctionId.EventHookup_Generate_Handler, cancellationToken)) - { - EventHookupSessionManager.CancelAndDismissExistingSessions(); + Contract.ThrowIfNull(solutionWithEventHandler, "Event Hookup could not create solution with event handler."); - var workspace = textView.TextSnapshot.TextBuffer.GetWorkspace(); - if (workspace == null) - { - nextHandler(); - EventHookupSessionManager.CancelAndDismissExistingSessions(); - return; - } - - var document = textView.TextSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - Contract.ThrowIfNull(document, "Event Hookup could not find the document for the IBufferView."); + // The new solution is created, so start user observable changes - var position = textView.GetCaretPoint(subjectBuffer).Value.Position; - var solutionWithEventHandler = CreateSolutionWithEventHandler( - document, - eventHandlerMethodName, - position, - out var plusEqualTokenEndPosition, - _globalOptions, - cancellationToken); + Contract.ThrowIfFalse(workspace.TryApplyChanges(solutionWithEventHandler), "Event Hookup could not update the solution."); - Contract.ThrowIfNull(solutionWithEventHandler, "Event Hookup could not create solution with event handler."); + // The += token will not move during this process, so it is safe to use that + // position as a location from which to find the identifier we're renaming. + BeginInlineRename(textView, plusEqualTokenEndPosition, cancellationToken); + } + } - // The new solution is created, so start user observable changes + private Solution CreateSolutionWithEventHandler( + Document document, + string eventHandlerMethodName, + int position, + out int plusEqualTokenEndPosition, + IGlobalOptionService globalOptions, + CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnUIThread(); - Contract.ThrowIfFalse(workspace.TryApplyChanges(solutionWithEventHandler), "Event Hookup could not update the solution."); + // Mark the += token with an annotation so we can find it after formatting + var plusEqualsTokenAnnotation = new SyntaxAnnotation(); - // The += token will not move during this process, so it is safe to use that - // position as a location from which to find the identifier we're renaming. - BeginInlineRename(textView, plusEqualTokenEndPosition, cancellationToken); - } - } + var documentWithNameAndAnnotationsAdded = AddMethodNameAndAnnotationsToSolution(document, eventHandlerMethodName, position, plusEqualsTokenAnnotation, cancellationToken); + var semanticDocument = SemanticDocument.CreateAsync(documentWithNameAndAnnotationsAdded, cancellationToken).WaitAndGetResult(cancellationToken); + var options = (CSharpCodeGenerationOptions)document.GetCodeGenerationOptionsAsync(globalOptions, cancellationToken).AsTask().WaitAndGetResult(cancellationToken); + var updatedRoot = AddGeneratedHandlerMethodToSolution(semanticDocument, options, eventHandlerMethodName, plusEqualsTokenAnnotation, cancellationToken); - private Solution CreateSolutionWithEventHandler( - Document document, - string eventHandlerMethodName, - int position, - out int plusEqualTokenEndPosition, - IGlobalOptionService globalOptions, - CancellationToken cancellationToken) + if (updatedRoot == null) { - _threadingContext.ThrowIfNotOnUIThread(); + plusEqualTokenEndPosition = 0; + return null; + } - // Mark the += token with an annotation so we can find it after formatting - var plusEqualsTokenAnnotation = new SyntaxAnnotation(); + var cleanupOptions = documentWithNameAndAnnotationsAdded.GetCodeCleanupOptionsAsync(globalOptions, cancellationToken).AsTask().WaitAndGetResult(cancellationToken); + var simplifiedDocument = Simplifier.ReduceAsync(documentWithNameAndAnnotationsAdded.WithSyntaxRoot(updatedRoot), Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken).WaitAndGetResult(cancellationToken); + var formattedDocument = Formatter.FormatAsync(simplifiedDocument, Formatter.Annotation, cleanupOptions.FormattingOptions, cancellationToken).WaitAndGetResult(cancellationToken); - var documentWithNameAndAnnotationsAdded = AddMethodNameAndAnnotationsToSolution(document, eventHandlerMethodName, position, plusEqualsTokenAnnotation, cancellationToken); - var semanticDocument = SemanticDocument.CreateAsync(documentWithNameAndAnnotationsAdded, cancellationToken).WaitAndGetResult(cancellationToken); - var options = (CSharpCodeGenerationOptions)document.GetCodeGenerationOptionsAsync(globalOptions, cancellationToken).AsTask().WaitAndGetResult(cancellationToken); - var updatedRoot = AddGeneratedHandlerMethodToSolution(semanticDocument, options, eventHandlerMethodName, plusEqualsTokenAnnotation, cancellationToken); + var newRoot = formattedDocument.GetSyntaxRootSynchronously(cancellationToken); + plusEqualTokenEndPosition = newRoot.GetAnnotatedNodesAndTokens(plusEqualsTokenAnnotation) + .Single().Span.End; - if (updatedRoot == null) - { - plusEqualTokenEndPosition = 0; - return null; - } + return document.Project.Solution.WithDocumentText( + formattedDocument.Id, formattedDocument.GetTextSynchronously(cancellationToken)); + } - var cleanupOptions = documentWithNameAndAnnotationsAdded.GetCodeCleanupOptionsAsync(globalOptions, cancellationToken).AsTask().WaitAndGetResult(cancellationToken); - var simplifiedDocument = Simplifier.ReduceAsync(documentWithNameAndAnnotationsAdded.WithSyntaxRoot(updatedRoot), Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken).WaitAndGetResult(cancellationToken); - var formattedDocument = Formatter.FormatAsync(simplifiedDocument, Formatter.Annotation, cleanupOptions.FormattingOptions, cancellationToken).WaitAndGetResult(cancellationToken); + private static Document AddMethodNameAndAnnotationsToSolution( + Document document, + string eventHandlerMethodName, + int position, + SyntaxAnnotation plusEqualsTokenAnnotation, + CancellationToken cancellationToken) + { + // First find the event hookup to determine if we are in a static context. + var root = document.GetSyntaxRootSynchronously(cancellationToken); + var plusEqualsToken = root.FindTokenOnLeftOfPosition(position); + var eventHookupExpression = plusEqualsToken.GetAncestor(); + var typeDecl = eventHookupExpression.GetAncestor(); + + var textToInsert = eventHandlerMethodName + ";"; + if (!eventHookupExpression.IsInStaticContext() && typeDecl is not null) + { + // This will be simplified later if it's not needed. + textToInsert = "this." + textToInsert; + } - var newRoot = formattedDocument.GetSyntaxRootSynchronously(cancellationToken); - plusEqualTokenEndPosition = newRoot.GetAnnotatedNodesAndTokens(plusEqualsTokenAnnotation) - .Single().Span.End; + // Next, perform a textual insertion of the event handler method name. + var textChange = new TextChange(new TextSpan(position, 0), textToInsert); + var newText = document.GetTextSynchronously(cancellationToken).WithChanges(textChange); + var documentWithNameAdded = document.WithText(newText); - return document.Project.Solution.WithDocumentText( - formattedDocument.Id, formattedDocument.GetTextSynchronously(cancellationToken)); - } + // Now find the event hookup again to add the appropriate annotations. + root = documentWithNameAdded.GetSyntaxRootSynchronously(cancellationToken); + plusEqualsToken = root.FindTokenOnLeftOfPosition(position); + eventHookupExpression = plusEqualsToken.GetAncestor(); - private static Document AddMethodNameAndAnnotationsToSolution( - Document document, - string eventHandlerMethodName, - int position, - SyntaxAnnotation plusEqualsTokenAnnotation, - CancellationToken cancellationToken) - { - // First find the event hookup to determine if we are in a static context. - var root = document.GetSyntaxRootSynchronously(cancellationToken); - var plusEqualsToken = root.FindTokenOnLeftOfPosition(position); - var eventHookupExpression = plusEqualsToken.GetAncestor(); - var typeDecl = eventHookupExpression.GetAncestor(); - - var textToInsert = eventHandlerMethodName + ";"; - if (!eventHookupExpression.IsInStaticContext() && typeDecl is not null) - { - // This will be simplified later if it's not needed. - textToInsert = "this." + textToInsert; - } + var updatedEventHookupExpression = eventHookupExpression + .ReplaceToken(plusEqualsToken, plusEqualsToken.WithAdditionalAnnotations(plusEqualsTokenAnnotation)) + .WithRight(eventHookupExpression.Right.WithAdditionalAnnotations(Simplifier.Annotation)) + .WithAdditionalAnnotations(Formatter.Annotation); - // Next, perform a textual insertion of the event handler method name. - var textChange = new TextChange(new TextSpan(position, 0), textToInsert); - var newText = document.GetTextSynchronously(cancellationToken).WithChanges(textChange); - var documentWithNameAdded = document.WithText(newText); + var rootWithUpdatedEventHookupExpression = root.ReplaceNode(eventHookupExpression, updatedEventHookupExpression); + return documentWithNameAdded.WithSyntaxRoot(rootWithUpdatedEventHookupExpression); + } - // Now find the event hookup again to add the appropriate annotations. - root = documentWithNameAdded.GetSyntaxRootSynchronously(cancellationToken); - plusEqualsToken = root.FindTokenOnLeftOfPosition(position); - eventHookupExpression = plusEqualsToken.GetAncestor(); + private static SyntaxNode AddGeneratedHandlerMethodToSolution( + SemanticDocument document, + CSharpCodeGenerationOptions options, + string eventHandlerMethodName, + SyntaxAnnotation plusEqualsTokenAnnotation, + CancellationToken cancellationToken) + { + var root = document.Root; + var eventHookupExpression = root.GetAnnotatedNodesAndTokens(plusEqualsTokenAnnotation).Single().AsToken().GetAncestor(); - var updatedEventHookupExpression = eventHookupExpression - .ReplaceToken(plusEqualsToken, plusEqualsToken.WithAdditionalAnnotations(plusEqualsTokenAnnotation)) - .WithRight(eventHookupExpression.Right.WithAdditionalAnnotations(Simplifier.Annotation)) - .WithAdditionalAnnotations(Formatter.Annotation); + var typeDecl = eventHookupExpression.GetAncestor(); - var rootWithUpdatedEventHookupExpression = root.ReplaceNode(eventHookupExpression, updatedEventHookupExpression); - return documentWithNameAdded.WithSyntaxRoot(rootWithUpdatedEventHookupExpression); - } + var generatedMethodSymbol = GetMethodSymbol(document, eventHandlerMethodName, eventHookupExpression, cancellationToken); - private static SyntaxNode AddGeneratedHandlerMethodToSolution( - SemanticDocument document, - CSharpCodeGenerationOptions options, - string eventHandlerMethodName, - SyntaxAnnotation plusEqualsTokenAnnotation, - CancellationToken cancellationToken) + if (generatedMethodSymbol == null) { - var root = document.Root; - var eventHookupExpression = root.GetAnnotatedNodesAndTokens(plusEqualsTokenAnnotation).Single().AsToken().GetAncestor(); - - var typeDecl = eventHookupExpression.GetAncestor(); + return null; + } - var generatedMethodSymbol = GetMethodSymbol(document, eventHandlerMethodName, eventHookupExpression, cancellationToken); + var container = (SyntaxNode)typeDecl ?? eventHookupExpression.GetAncestor(); - if (generatedMethodSymbol == null) - { - return null; - } + var codeGenerator = document.Document.GetRequiredLanguageService(); + var codeGenOptions = codeGenerator.GetInfo(new CodeGenerationContext(afterThisLocation: eventHookupExpression.GetLocation()), options, root.SyntaxTree.Options); + var newContainer = codeGenerator.AddMethod(container, generatedMethodSymbol, codeGenOptions, cancellationToken); - var container = (SyntaxNode)typeDecl ?? eventHookupExpression.GetAncestor(); + return root.ReplaceNode(container, newContainer); + } - var codeGenerator = document.Document.GetRequiredLanguageService(); - var codeGenOptions = codeGenerator.GetInfo(new CodeGenerationContext(afterThisLocation: eventHookupExpression.GetLocation()), options, root.SyntaxTree.Options); - var newContainer = codeGenerator.AddMethod(container, generatedMethodSymbol, codeGenOptions, cancellationToken); + private static IMethodSymbol GetMethodSymbol( + SemanticDocument semanticDocument, + string eventHandlerMethodName, + AssignmentExpressionSyntax eventHookupExpression, + CancellationToken cancellationToken) + { + var semanticModel = semanticDocument.SemanticModel; + var symbolInfo = semanticModel.GetSymbolInfo(eventHookupExpression.Left, cancellationToken); - return root.ReplaceNode(container, newContainer); + var symbol = symbolInfo.Symbol; + if (symbol == null || symbol.Kind != SymbolKind.Event) + { + return null; } - private static IMethodSymbol GetMethodSymbol( - SemanticDocument semanticDocument, - string eventHandlerMethodName, - AssignmentExpressionSyntax eventHookupExpression, - CancellationToken cancellationToken) + var typeInference = semanticDocument.Document.GetLanguageService(); + var delegateType = typeInference.InferDelegateType(semanticModel, eventHookupExpression.Right, cancellationToken); + if (delegateType == null || delegateType.DelegateInvokeMethod == null) { - var semanticModel = semanticDocument.SemanticModel; - var symbolInfo = semanticModel.GetSymbolInfo(eventHookupExpression.Left, cancellationToken); - - var symbol = symbolInfo.Symbol; - if (symbol == null || symbol.Kind != SymbolKind.Event) - { - return null; - } + return null; + } - var typeInference = semanticDocument.Document.GetLanguageService(); - var delegateType = typeInference.InferDelegateType(semanticModel, eventHookupExpression.Right, cancellationToken); - if (delegateType == null || delegateType.DelegateInvokeMethod == null) - { - return null; - } + var syntaxFactory = semanticDocument.Document.GetLanguageService(); + var delegateInvokeMethod = delegateType.DelegateInvokeMethod.RemoveInaccessibleAttributesAndAttributesOfTypes(semanticDocument.SemanticModel.Compilation.Assembly); + + return CodeGenerationSymbolFactory.CreateMethodSymbol( + attributes: default, + accessibility: Accessibility.Private, + modifiers: new DeclarationModifiers(isStatic: eventHookupExpression.IsInStaticContext()), + returnType: delegateInvokeMethod.ReturnType, + refKind: delegateInvokeMethod.RefKind, + explicitInterfaceImplementations: default, + name: eventHandlerMethodName, + typeParameters: default, + parameters: delegateInvokeMethod.Parameters, + statements: [CodeGenerationHelpers.GenerateThrowStatement(syntaxFactory, semanticDocument, "System.NotImplementedException")]); + } - var syntaxFactory = semanticDocument.Document.GetLanguageService(); - var delegateInvokeMethod = delegateType.DelegateInvokeMethod.RemoveInaccessibleAttributesAndAttributesOfTypes(semanticDocument.SemanticModel.Compilation.Assembly); - - return CodeGenerationSymbolFactory.CreateMethodSymbol( - attributes: default, - accessibility: Accessibility.Private, - modifiers: new DeclarationModifiers(isStatic: eventHookupExpression.IsInStaticContext()), - returnType: delegateInvokeMethod.ReturnType, - refKind: delegateInvokeMethod.RefKind, - explicitInterfaceImplementations: default, - name: eventHandlerMethodName, - typeParameters: default, - parameters: delegateInvokeMethod.Parameters, - statements: [CodeGenerationHelpers.GenerateThrowStatement(syntaxFactory, semanticDocument, "System.NotImplementedException")]); - } + private void BeginInlineRename(ITextView textView, int plusEqualTokenEndPosition, CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnUIThread(); - private void BeginInlineRename(ITextView textView, int plusEqualTokenEndPosition, CancellationToken cancellationToken) + if (_inlineRenameService.ActiveSession == null) { - _threadingContext.ThrowIfNotOnUIThread(); - - if (_inlineRenameService.ActiveSession == null) + var document = textView.TextSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document != null) { - var document = textView.TextSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document != null) + // In the middle of a user action, cannot cancel. + var root = document.GetSyntaxRootSynchronously(cancellationToken); + var token = root.FindTokenOnRightOfPosition(plusEqualTokenEndPosition); + var editSpan = token.Span; + var memberAccessExpression = token.GetAncestor(); + if (memberAccessExpression != null) { - // In the middle of a user action, cannot cancel. - var root = document.GetSyntaxRootSynchronously(cancellationToken); - var token = root.FindTokenOnRightOfPosition(plusEqualTokenEndPosition); - var editSpan = token.Span; - var memberAccessExpression = token.GetAncestor(); - if (memberAccessExpression != null) - { - // the event hookup might look like `MyEvent += this.GeneratedHandlerName;` - editSpan = memberAccessExpression.Name.Span; - } - - _inlineRenameService.StartInlineSession(document, editSpan, cancellationToken); - textView.SetSelection(editSpan.ToSnapshotSpan(textView.TextSnapshot)); + // the event hookup might look like `MyEvent += this.GeneratedHandlerName;` + editSpan = memberAccessExpression.Name.Span; } + + _inlineRenameService.StartInlineSession(document, editSpan, cancellationToken); + textView.SetSelection(editSpan.ToSnapshotSpan(textView.TextSnapshot)); } } } diff --git a/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_TypeCharCommand.cs b/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_TypeCharCommand.cs index 66aab1cd3fdb1..a906b99debfba 100644 --- a/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_TypeCharCommand.cs +++ b/src/EditorFeatures/CSharp/EventHookup/EventHookupCommandHandler_TypeCharCommand.cs @@ -14,66 +14,65 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup +namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup; + +internal partial class EventHookupCommandHandler : IChainedCommandHandler { - internal partial class EventHookupCommandHandler : IChainedCommandHandler + public void ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context) { - public void ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context) + _threadingContext.ThrowIfNotOnUIThread(); + nextHandler(); + + if (!_globalOptions.GetOption(EventHookupOptionsStorage.EventHookup)) { - _threadingContext.ThrowIfNotOnUIThread(); - nextHandler(); + EventHookupSessionManager.CancelAndDismissExistingSessions(); + return; + } - if (!_globalOptions.GetOption(EventHookupOptionsStorage.EventHookup)) + // Event hookup is current uncancellable. + var cancellationToken = CancellationToken.None; + using (Logger.LogBlock(FunctionId.EventHookup_Type_Char, cancellationToken)) + { + if (args.TypedChar == '=') { + // They've typed an equals. Cancel existing sessions and potentially start a + // new session. + EventHookupSessionManager.CancelAndDismissExistingSessions(); - return; - } - // Event hookup is current uncancellable. - var cancellationToken = CancellationToken.None; - using (Logger.LogBlock(FunctionId.EventHookup_Type_Char, cancellationToken)) - { - if (args.TypedChar == '=') + if (IsTextualPlusEquals(args.TextView, args.SubjectBuffer)) { - // They've typed an equals. Cancel existing sessions and potentially start a - // new session. - - EventHookupSessionManager.CancelAndDismissExistingSessions(); - - if (IsTextualPlusEquals(args.TextView, args.SubjectBuffer)) - { - EventHookupSessionManager.BeginSession(this, args.TextView, args.SubjectBuffer, _asyncListener, TESTSessionHookupMutex); - } + EventHookupSessionManager.BeginSession(this, args.TextView, args.SubjectBuffer, _asyncListener, TESTSessionHookupMutex); } - else + } + else + { + // Spaces are the only non-'=' character that allow the session to continue + if (args.TypedChar != ' ') { - // Spaces are the only non-'=' character that allow the session to continue - if (args.TypedChar != ' ') - { - EventHookupSessionManager.CancelAndDismissExistingSessions(); - } + EventHookupSessionManager.CancelAndDismissExistingSessions(); } } } + } - private bool IsTextualPlusEquals(ITextView textView, ITextBuffer subjectBuffer) - { - _threadingContext.ThrowIfNotOnUIThread(); - - var caretPoint = textView.GetCaretPoint(subjectBuffer); - if (!caretPoint.HasValue) - { - return false; - } - - var position = caretPoint.Value.Position; - return position - 2 >= 0 && subjectBuffer.CurrentSnapshot.GetText(position - 2, 2) == "+="; - } + private bool IsTextualPlusEquals(ITextView textView, ITextBuffer subjectBuffer) + { + _threadingContext.ThrowIfNotOnUIThread(); - public CommandState GetCommandState(TypeCharCommandArgs args, Func nextHandler) + var caretPoint = textView.GetCaretPoint(subjectBuffer); + if (!caretPoint.HasValue) { - _threadingContext.ThrowIfNotOnUIThread(); - return nextHandler(); + return false; } + + var position = caretPoint.Value.Position; + return position - 2 >= 0 && subjectBuffer.CurrentSnapshot.GetText(position - 2, 2) == "+="; + } + + public CommandState GetCommandState(TypeCharCommandArgs args, Func nextHandler) + { + _threadingContext.ThrowIfNotOnUIThread(); + return nextHandler(); } } diff --git a/src/EditorFeatures/CSharp/EventHookup/EventHookupSessionManager.cs b/src/EditorFeatures/CSharp/EventHookup/EventHookupSessionManager.cs index d004495c328aa..9b9c8baad59f2 100644 --- a/src/EditorFeatures/CSharp/EventHookup/EventHookupSessionManager.cs +++ b/src/EditorFeatures/CSharp/EventHookup/EventHookupSessionManager.cs @@ -19,166 +19,165 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup +namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup; + +[Export] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed partial class EventHookupSessionManager( + IThreadingContext threadingContext, + IToolTipService toolTipService, + IGlobalOptionService globalOptions) { - [Export] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed partial class EventHookupSessionManager( - IThreadingContext threadingContext, - IToolTipService toolTipService, - IGlobalOptionService globalOptions) - { - public readonly IThreadingContext ThreadingContext = threadingContext; - private readonly IToolTipService _toolTipService = toolTipService; - private readonly IGlobalOptionService _globalOptions = globalOptions; - - private IToolTipPresenter _toolTipPresenter; + public readonly IThreadingContext ThreadingContext = threadingContext; + private readonly IToolTipService _toolTipService = toolTipService; + private readonly IGlobalOptionService _globalOptions = globalOptions; - internal EventHookupSession CurrentSession { get; set; } + private IToolTipPresenter _toolTipPresenter; - // For test purposes only! - internal ClassifiedTextElement[] TEST_MostRecentToolTipContent { get; set; } + internal EventHookupSession CurrentSession { get; set; } - internal void EventHookupFoundInSession(EventHookupSession analyzedSession) - { - ThreadingContext.ThrowIfNotOnUIThread(); + // For test purposes only! + internal ClassifiedTextElement[] TEST_MostRecentToolTipContent { get; set; } - var caretPoint = analyzedSession.TextView.GetCaretPoint(analyzedSession.SubjectBuffer); + internal void EventHookupFoundInSession(EventHookupSession analyzedSession) + { + ThreadingContext.ThrowIfNotOnUIThread(); - // only generate tooltip if it is not already shown (_toolTipPresenter == null) - // Ensure the analyzed session matches the current session and that the caret is still - // in the session's tracking span. - if (_toolTipPresenter == null && - CurrentSession == analyzedSession && - caretPoint.HasValue && - IsCaretWithinSpanOrAtEnd(analyzedSession.TrackingSpan, analyzedSession.TextView.TextSnapshot, caretPoint.Value)) - { - // Create a tooltip presenter that stays alive, even when the user types, without tracking the mouse. - _toolTipPresenter = _toolTipService.CreatePresenter(analyzedSession.TextView, - new ToolTipParameters(trackMouse: false, ignoreBufferChange: true)); - - // tooltips text is: Program_MyEvents; (Press TAB to insert) - // GetEventNameTask() gets back the event name, only needs to add a semicolon after it. - var textRuns = new[] - { - new ClassifiedTextRun(ClassificationTypeNames.MethodName, analyzedSession.GetEventNameTask.Result, ClassifiedTextRunStyle.UseClassificationFont), - new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ";", ClassifiedTextRunStyle.UseClassificationFont), - new ClassifiedTextRun(ClassificationTypeNames.Text, CSharpEditorResources.Press_TAB_to_insert), - }; - var content = new[] { new ClassifiedTextElement(textRuns) }; - - _toolTipPresenter.StartOrUpdate(analyzedSession.TrackingSpan, content); - - // For test purposes only! - TEST_MostRecentToolTipContent = content; - - // Watch all text buffer changes & caret moves while this event hookup session is active - analyzedSession.TextView.TextSnapshot.TextBuffer.Changed += TextBuffer_Changed; - CurrentSession.Dismissed += () => { analyzedSession.TextView.TextSnapshot.TextBuffer.Changed -= TextBuffer_Changed; }; - - analyzedSession.TextView.Caret.PositionChanged += Caret_PositionChanged; - CurrentSession.Dismissed += () => { analyzedSession.TextView.Caret.PositionChanged -= Caret_PositionChanged; }; - } - } + var caretPoint = analyzedSession.TextView.GetCaretPoint(analyzedSession.SubjectBuffer); - private static bool IsCaretWithinSpanOrAtEnd(ITrackingSpan trackingSpan, ITextSnapshot textSnapshot, SnapshotPoint caretPoint) + // only generate tooltip if it is not already shown (_toolTipPresenter == null) + // Ensure the analyzed session matches the current session and that the caret is still + // in the session's tracking span. + if (_toolTipPresenter == null && + CurrentSession == analyzedSession && + caretPoint.HasValue && + IsCaretWithinSpanOrAtEnd(analyzedSession.TrackingSpan, analyzedSession.TextView.TextSnapshot, caretPoint.Value)) { - var snapshotSpan = trackingSpan.GetSpan(textSnapshot); + // Create a tooltip presenter that stays alive, even when the user types, without tracking the mouse. + _toolTipPresenter = _toolTipService.CreatePresenter(analyzedSession.TextView, + new ToolTipParameters(trackMouse: false, ignoreBufferChange: true)); - // If the caret is within the span, then we want to show the tooltip - if (snapshotSpan.Contains(caretPoint)) + // tooltips text is: Program_MyEvents; (Press TAB to insert) + // GetEventNameTask() gets back the event name, only needs to add a semicolon after it. + var textRuns = new[] { - return true; - } + new ClassifiedTextRun(ClassificationTypeNames.MethodName, analyzedSession.GetEventNameTask.Result, ClassifiedTextRunStyle.UseClassificationFont), + new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ";", ClassifiedTextRunStyle.UseClassificationFont), + new ClassifiedTextRun(ClassificationTypeNames.Text, CSharpEditorResources.Press_TAB_to_insert), + }; + var content = new[] { new ClassifiedTextElement(textRuns) }; - // Otherwise if the span is empty, and at the end of the file, and the caret - // is also at the end of the file, then show the tooltip. - if (snapshotSpan.IsEmpty && - snapshotSpan.Start.Position == caretPoint.Position && - caretPoint.Position == textSnapshot.Length) - { - return true; - } + _toolTipPresenter.StartOrUpdate(analyzedSession.TrackingSpan, content); + + // For test purposes only! + TEST_MostRecentToolTipContent = content; + + // Watch all text buffer changes & caret moves while this event hookup session is active + analyzedSession.TextView.TextSnapshot.TextBuffer.Changed += TextBuffer_Changed; + CurrentSession.Dismissed += () => { analyzedSession.TextView.TextSnapshot.TextBuffer.Changed -= TextBuffer_Changed; }; - return false; + analyzedSession.TextView.Caret.PositionChanged += Caret_PositionChanged; + CurrentSession.Dismissed += () => { analyzedSession.TextView.Caret.PositionChanged -= Caret_PositionChanged; }; } + } + + private static bool IsCaretWithinSpanOrAtEnd(ITrackingSpan trackingSpan, ITextSnapshot textSnapshot, SnapshotPoint caretPoint) + { + var snapshotSpan = trackingSpan.GetSpan(textSnapshot); - internal void BeginSession( - EventHookupCommandHandler eventHookupCommandHandler, - ITextView textView, - ITextBuffer subjectBuffer, - IAsynchronousOperationListener asyncListener, - Mutex testSessionHookupMutex) + // If the caret is within the span, then we want to show the tooltip + if (snapshotSpan.Contains(caretPoint)) { - CurrentSession = new EventHookupSession(this, eventHookupCommandHandler, textView, subjectBuffer, asyncListener, _globalOptions, testSessionHookupMutex); + return true; } - internal void CancelAndDismissExistingSessions() + // Otherwise if the span is empty, and at the end of the file, and the caret + // is also at the end of the file, then show the tooltip. + if (snapshotSpan.IsEmpty && + snapshotSpan.Start.Position == caretPoint.Position && + caretPoint.Position == textSnapshot.Length) { - ThreadingContext.ThrowIfNotOnUIThread(); + return true; + } - if (CurrentSession != null) - { - CurrentSession.Cancel(); - CurrentSession = null; - } + return false; + } - if (_toolTipPresenter != null) - { - _toolTipPresenter.Dismiss(); - _toolTipPresenter = null; - } + internal void BeginSession( + EventHookupCommandHandler eventHookupCommandHandler, + ITextView textView, + ITextBuffer subjectBuffer, + IAsynchronousOperationListener asyncListener, + Mutex testSessionHookupMutex) + { + CurrentSession = new EventHookupSession(this, eventHookupCommandHandler, textView, subjectBuffer, asyncListener, _globalOptions, testSessionHookupMutex); + } - // For test purposes only! - TEST_MostRecentToolTipContent = null; - } + internal void CancelAndDismissExistingSessions() + { + ThreadingContext.ThrowIfNotOnUIThread(); - /// - /// If any text is deleted or any non-space text is entered, cancel the session. - /// - private void TextBuffer_Changed(object sender, TextContentChangedEventArgs e) + if (CurrentSession != null) { - ThreadingContext.ThrowIfNotOnUIThread(); - - foreach (var change in e.Changes) - { - if (change.OldText.Length > 0 || change.NewText.Any(c => c != ' ')) - { - CancelAndDismissExistingSessions(); - return; - } - } + CurrentSession.Cancel(); + CurrentSession = null; } - /// - /// If the caret moves outside the session's tracking span, cancel the session. - /// - private void Caret_PositionChanged(object sender, EventArgs e) + if (_toolTipPresenter != null) { - ThreadingContext.ThrowIfNotOnUIThread(); + _toolTipPresenter.Dismiss(); + _toolTipPresenter = null; + } + + // For test purposes only! + TEST_MostRecentToolTipContent = null; + } + + /// + /// If any text is deleted or any non-space text is entered, cancel the session. + /// + private void TextBuffer_Changed(object sender, TextContentChangedEventArgs e) + { + ThreadingContext.ThrowIfNotOnUIThread(); - if (CurrentSession == null) + foreach (var change in e.Changes) + { + if (change.OldText.Length > 0 || change.NewText.Any(c => c != ' ')) { CancelAndDismissExistingSessions(); return; } + } + } - var caretPoint = CurrentSession.TextView.GetCaretPoint(CurrentSession.SubjectBuffer); + /// + /// If the caret moves outside the session's tracking span, cancel the session. + /// + private void Caret_PositionChanged(object sender, EventArgs e) + { + ThreadingContext.ThrowIfNotOnUIThread(); - if (!caretPoint.HasValue) - { - CancelAndDismissExistingSessions(); - } + if (CurrentSession == null) + { + CancelAndDismissExistingSessions(); + return; + } - var snapshotSpan = CurrentSession.TrackingSpan.GetSpan(CurrentSession.TextView.TextSnapshot); - if (snapshotSpan.Snapshot != caretPoint.Value.Snapshot || !snapshotSpan.Contains(caretPoint.Value)) - { - CancelAndDismissExistingSessions(); - } + var caretPoint = CurrentSession.TextView.GetCaretPoint(CurrentSession.SubjectBuffer); + + if (!caretPoint.HasValue) + { + CancelAndDismissExistingSessions(); } - internal bool IsTrackingSession() - => CurrentSession != null; + var snapshotSpan = CurrentSession.TrackingSpan.GetSpan(CurrentSession.TextView.TextSnapshot); + if (snapshotSpan.Snapshot != caretPoint.Value.Snapshot || !snapshotSpan.Contains(caretPoint.Value)) + { + CancelAndDismissExistingSessions(); + } } + + internal bool IsTrackingSession() + => CurrentSession != null; } diff --git a/src/EditorFeatures/CSharp/EventHookup/EventHookupSessionManager_EventHookupSession.cs b/src/EditorFeatures/CSharp/EventHookup/EventHookupSessionManager_EventHookupSession.cs index 7f2501a7bed18..f5d25b51cccac 100644 --- a/src/EditorFeatures/CSharp/EventHookup/EventHookupSessionManager_EventHookupSession.cs +++ b/src/EditorFeatures/CSharp/EventHookup/EventHookupSessionManager_EventHookupSession.cs @@ -29,265 +29,264 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles.SymbolSpecification; -namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup +namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup; + +internal sealed partial class EventHookupSessionManager { - internal sealed partial class EventHookupSessionManager + /// + /// A session begins when an '=' is typed after a '+' and requires determining whether the + /// += is being used to add an event handler to an event. If it is, then we also determine + /// a candidate name for the event handler. + /// + internal class EventHookupSession { - /// - /// A session begins when an '=' is typed after a '+' and requires determining whether the - /// += is being used to add an event handler to an event. If it is, then we also determine - /// a candidate name for the event handler. - /// - internal class EventHookupSession - { - public readonly Task GetEventNameTask; - private readonly IThreadingContext _threadingContext; - private readonly CancellationTokenSource _cancellationTokenSource = new(); - private readonly ITrackingPoint _trackingPoint; - private readonly ITrackingSpan _trackingSpan; - private readonly ITextView _textView; - private readonly ITextBuffer _subjectBuffer; - private readonly IGlobalOptionService _globalOptions; + public readonly Task GetEventNameTask; + private readonly IThreadingContext _threadingContext; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly ITrackingPoint _trackingPoint; + private readonly ITrackingSpan _trackingSpan; + private readonly ITextView _textView; + private readonly ITextBuffer _subjectBuffer; + private readonly IGlobalOptionService _globalOptions; - public event Action Dismissed = () => { }; + public event Action Dismissed = () => { }; - // For testing purposes only! Should always be null except in tests. - internal Mutex TESTSessionHookupMutex = null; + // For testing purposes only! Should always be null except in tests. + internal Mutex TESTSessionHookupMutex = null; - public ITrackingPoint TrackingPoint - { - get - { - _threadingContext.ThrowIfNotOnUIThread(); - return _trackingPoint; - } - } - - public ITrackingSpan TrackingSpan + public ITrackingPoint TrackingPoint + { + get { - get - { - _threadingContext.ThrowIfNotOnUIThread(); - return _trackingSpan; - } + _threadingContext.ThrowIfNotOnUIThread(); + return _trackingPoint; } + } - public ITextView TextView + public ITrackingSpan TrackingSpan + { + get { - get - { - _threadingContext.ThrowIfNotOnUIThread(); - return _textView; - } + _threadingContext.ThrowIfNotOnUIThread(); + return _trackingSpan; } + } - public ITextBuffer SubjectBuffer + public ITextView TextView + { + get { - get - { - _threadingContext.ThrowIfNotOnUIThread(); - return _subjectBuffer; - } + _threadingContext.ThrowIfNotOnUIThread(); + return _textView; } + } - public void Cancel() + public ITextBuffer SubjectBuffer + { + get { _threadingContext.ThrowIfNotOnUIThread(); - _cancellationTokenSource.Cancel(); + return _subjectBuffer; } + } - public EventHookupSession( - EventHookupSessionManager eventHookupSessionManager, - EventHookupCommandHandler commandHandler, - ITextView textView, - ITextBuffer subjectBuffer, - IAsynchronousOperationListener asyncListener, - IGlobalOptionService globalOptions, - Mutex testSessionHookupMutex) + public void Cancel() + { + _threadingContext.ThrowIfNotOnUIThread(); + _cancellationTokenSource.Cancel(); + } + + public EventHookupSession( + EventHookupSessionManager eventHookupSessionManager, + EventHookupCommandHandler commandHandler, + ITextView textView, + ITextBuffer subjectBuffer, + IAsynchronousOperationListener asyncListener, + IGlobalOptionService globalOptions, + Mutex testSessionHookupMutex) + { + _threadingContext = eventHookupSessionManager.ThreadingContext; + var cancellationToken = _cancellationTokenSource.Token; + _textView = textView; + _subjectBuffer = subjectBuffer; + _globalOptions = globalOptions; + this.TESTSessionHookupMutex = testSessionHookupMutex; + + var document = textView.TextSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + var workspace = textView.TextSnapshot.TextBuffer.GetWorkspace(); + if (document != null && workspace != null && workspace.CanApplyChange(ApplyChangesKind.ChangeDocument)) { - _threadingContext = eventHookupSessionManager.ThreadingContext; - var cancellationToken = _cancellationTokenSource.Token; - _textView = textView; - _subjectBuffer = subjectBuffer; - _globalOptions = globalOptions; - this.TESTSessionHookupMutex = testSessionHookupMutex; - - var document = textView.TextSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - var workspace = textView.TextSnapshot.TextBuffer.GetWorkspace(); - if (document != null && workspace != null && workspace.CanApplyChange(ApplyChangesKind.ChangeDocument)) - { - var position = textView.GetCaretPoint(subjectBuffer).Value.Position; - _trackingPoint = textView.TextSnapshot.CreateTrackingPoint(position, PointTrackingMode.Negative); + var position = textView.GetCaretPoint(subjectBuffer).Value.Position; + _trackingPoint = textView.TextSnapshot.CreateTrackingPoint(position, PointTrackingMode.Negative); - // If the caret is at the end of the document we just create an empty span - var length = textView.TextSnapshot.Length > position + 1 ? 1 : 0; - _trackingSpan = textView.TextSnapshot.CreateTrackingSpan(new Span(position, length), SpanTrackingMode.EdgeInclusive); + // If the caret is at the end of the document we just create an empty span + var length = textView.TextSnapshot.Length > position + 1 ? 1 : 0; + _trackingSpan = textView.TextSnapshot.CreateTrackingSpan(new Span(position, length), SpanTrackingMode.EdgeInclusive); - var asyncToken = asyncListener.BeginAsyncOperation(GetType().Name + ".Start"); + var asyncToken = asyncListener.BeginAsyncOperation(GetType().Name + ".Start"); - this.GetEventNameTask = Task.Factory.SafeStartNewFromAsync( - () => DetermineIfEventHookupAndGetHandlerNameAsync(document, position, cancellationToken), - cancellationToken, - TaskScheduler.Default); + this.GetEventNameTask = Task.Factory.SafeStartNewFromAsync( + () => DetermineIfEventHookupAndGetHandlerNameAsync(document, position, cancellationToken), + cancellationToken, + TaskScheduler.Default); + + var continuedTask = this.GetEventNameTask.SafeContinueWithFromAsync( + async t => + { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken); - var continuedTask = this.GetEventNameTask.SafeContinueWithFromAsync( - async t => + if (t.Result != null) { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken); - - if (t.Result != null) - { - commandHandler.EventHookupSessionManager.EventHookupFoundInSession(this); - } - }, - cancellationToken, - TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - - continuedTask.CompletesAsyncOperation(asyncToken); - } - else - { - _trackingPoint = textView.TextSnapshot.CreateTrackingPoint(0, PointTrackingMode.Negative); - _trackingSpan = textView.TextSnapshot.CreateTrackingSpan(new Span(), SpanTrackingMode.EdgeInclusive); - this.GetEventNameTask = SpecializedTasks.Null(); - eventHookupSessionManager.CancelAndDismissExistingSessions(); - } + commandHandler.EventHookupSessionManager.EventHookupFoundInSession(this); + } + }, + cancellationToken, + TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + + continuedTask.CompletesAsyncOperation(asyncToken); } - - private async Task DetermineIfEventHookupAndGetHandlerNameAsync(Document document, int position, CancellationToken cancellationToken) + else { - _threadingContext.ThrowIfNotOnBackgroundThread(); - - // For test purposes only! - if (TESTSessionHookupMutex != null) - { - TESTSessionHookupMutex.WaitOne(); - TESTSessionHookupMutex.ReleaseMutex(); - } - - using (Logger.LogBlock(FunctionId.EventHookup_Determine_If_Event_Hookup, cancellationToken)) - { - var plusEqualsToken = await GetPlusEqualsTokenInsideAddAssignExpressionAsync(document, position, cancellationToken).ConfigureAwait(false); - if (plusEqualsToken == null) - { - return null; - } + _trackingPoint = textView.TextSnapshot.CreateTrackingPoint(0, PointTrackingMode.Negative); + _trackingSpan = textView.TextSnapshot.CreateTrackingSpan(new Span(), SpanTrackingMode.EdgeInclusive); + this.GetEventNameTask = SpecializedTasks.Null(); + eventHookupSessionManager.CancelAndDismissExistingSessions(); + } + } - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + private async Task DetermineIfEventHookupAndGetHandlerNameAsync(Document document, int position, CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnBackgroundThread(); - var eventSymbol = GetEventSymbol(semanticModel, plusEqualsToken.Value, cancellationToken); - if (eventSymbol == null) - { - return null; - } - - var namingRule = await document.GetApplicableNamingRuleAsync( - new SymbolKindOrTypeKind(MethodKind.Ordinary), - new DeclarationModifiers(isStatic: plusEqualsToken.Value.Parent.IsInStaticContext()), - Accessibility.Private, - _globalOptions.CreateProvider(), - cancellationToken).ConfigureAwait(false); - - return GetEventHandlerName( - eventSymbol, plusEqualsToken.Value, semanticModel, - document.GetLanguageService(), namingRule); - } + // For test purposes only! + if (TESTSessionHookupMutex != null) + { + TESTSessionHookupMutex.WaitOne(); + TESTSessionHookupMutex.ReleaseMutex(); } - private async Task GetPlusEqualsTokenInsideAddAssignExpressionAsync(Document document, int position, CancellationToken cancellationToken) + using (Logger.LogBlock(FunctionId.EventHookup_Determine_If_Event_Hookup, cancellationToken)) { - _threadingContext.ThrowIfNotOnBackgroundThread(); - var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); - - if (token.Kind() != SyntaxKind.PlusEqualsToken) + var plusEqualsToken = await GetPlusEqualsTokenInsideAddAssignExpressionAsync(document, position, cancellationToken).ConfigureAwait(false); + if (plusEqualsToken == null) { return null; } - if (!token.Parent.IsKind(SyntaxKind.AddAssignmentExpression)) + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var eventSymbol = GetEventSymbol(semanticModel, plusEqualsToken.Value, cancellationToken); + if (eventSymbol == null) { return null; } - return token; - } + var namingRule = await document.GetApplicableNamingRuleAsync( + new SymbolKindOrTypeKind(MethodKind.Ordinary), + new DeclarationModifiers(isStatic: plusEqualsToken.Value.Parent.IsInStaticContext()), + Accessibility.Private, + _globalOptions.CreateProvider(), + cancellationToken).ConfigureAwait(false); - private IEventSymbol GetEventSymbol(SemanticModel semanticModel, SyntaxToken plusEqualsToken, CancellationToken cancellationToken) - { - _threadingContext.ThrowIfNotOnBackgroundThread(); - if (plusEqualsToken.Parent is not AssignmentExpressionSyntax parentToken) - { - return null; - } + return GetEventHandlerName( + eventSymbol, plusEqualsToken.Value, semanticModel, + document.GetLanguageService(), namingRule); + } + } - var symbol = semanticModel.GetSymbolInfo(parentToken.Left, cancellationToken).Symbol; - if (symbol == null) - { - return null; - } + private async Task GetPlusEqualsTokenInsideAddAssignExpressionAsync(Document document, int position, CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnBackgroundThread(); + var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); - return symbol as IEventSymbol; + if (token.Kind() != SyntaxKind.PlusEqualsToken) + { + return null; } - private string GetEventHandlerName( - IEventSymbol eventSymbol, SyntaxToken plusEqualsToken, SemanticModel semanticModel, - ISyntaxFactsService syntaxFactsService, NamingRule namingRule) + if (!token.Parent.IsKind(SyntaxKind.AddAssignmentExpression)) { - _threadingContext.ThrowIfNotOnBackgroundThread(); - var objectPart = GetNameObjectPart(eventSymbol, plusEqualsToken, semanticModel, syntaxFactsService); - var basename = namingRule.NamingStyle.CreateName([string.Format("{0}_{1}", objectPart, eventSymbol.Name)]); + return null; + } - var reservedNames = semanticModel.LookupSymbols(plusEqualsToken.SpanStart).Select(m => m.Name); + return token; + } - return NameGenerator.EnsureUniqueness(basename, reservedNames); + private IEventSymbol GetEventSymbol(SemanticModel semanticModel, SyntaxToken plusEqualsToken, CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnBackgroundThread(); + if (plusEqualsToken.Parent is not AssignmentExpressionSyntax parentToken) + { + return null; } - /// - /// Take another look at the LHS of the += node -- we need to figure out a default name - /// for the event handler, and that's usually based on the object (which is usually a - /// field of 'this', but not always) to which the event belongs. So, if the event is - /// something like 'button1.Click' or 'this.listBox1.Select', we want the names - /// 'button1' and 'listBox1' respectively. If the field belongs to 'this', then we use - /// the name of this class, as we do if we can't make any sense out of the parse tree. - /// - private string GetNameObjectPart(IEventSymbol eventSymbol, SyntaxToken plusEqualsToken, SemanticModel semanticModel, ISyntaxFactsService syntaxFactsService) + var symbol = semanticModel.GetSymbolInfo(parentToken.Left, cancellationToken).Symbol; + if (symbol == null) { - _threadingContext.ThrowIfNotOnBackgroundThread(); - var parentToken = plusEqualsToken.Parent as AssignmentExpressionSyntax; + return null; + } - if (parentToken.Left is MemberAccessExpressionSyntax memberAccessExpression) - { - // This is expected -- it means the last thing is(probably) the event name. We - // already have that in eventSymbol. What we need is the LHS of that dot. + return symbol as IEventSymbol; + } - var lhs = memberAccessExpression.Expression.GetRightmostName(); + private string GetEventHandlerName( + IEventSymbol eventSymbol, SyntaxToken plusEqualsToken, SemanticModel semanticModel, + ISyntaxFactsService syntaxFactsService, NamingRule namingRule) + { + _threadingContext.ThrowIfNotOnBackgroundThread(); + var objectPart = GetNameObjectPart(eventSymbol, plusEqualsToken, semanticModel, syntaxFactsService); + var basename = namingRule.NamingStyle.CreateName([string.Format("{0}_{1}", objectPart, eventSymbol.Name)]); - if (lhs is GenericNameSyntax lhsGenericNameSyntax) - { - // For generic we must exclude type variables - return lhsGenericNameSyntax.Identifier.Text; - } + var reservedNames = semanticModel.LookupSymbols(plusEqualsToken.SpanStart).Select(m => m.Name); - if (lhs != null) - { - return lhs.ToString(); - } - } + return NameGenerator.EnsureUniqueness(basename, reservedNames); + } + + /// + /// Take another look at the LHS of the += node -- we need to figure out a default name + /// for the event handler, and that's usually based on the object (which is usually a + /// field of 'this', but not always) to which the event belongs. So, if the event is + /// something like 'button1.Click' or 'this.listBox1.Select', we want the names + /// 'button1' and 'listBox1' respectively. If the field belongs to 'this', then we use + /// the name of this class, as we do if we can't make any sense out of the parse tree. + /// + private string GetNameObjectPart(IEventSymbol eventSymbol, SyntaxToken plusEqualsToken, SemanticModel semanticModel, ISyntaxFactsService syntaxFactsService) + { + _threadingContext.ThrowIfNotOnBackgroundThread(); + var parentToken = plusEqualsToken.Parent as AssignmentExpressionSyntax; + + if (parentToken.Left is MemberAccessExpressionSyntax memberAccessExpression) + { + // This is expected -- it means the last thing is(probably) the event name. We + // already have that in eventSymbol. What we need is the LHS of that dot. - // If we didn't find an object name above, then the object name is the name of this class. - // Note: For generic, it's ok(it's even a good idea) to exclude type variables, - // because the name is only used as a prefix for the method name. + var lhs = memberAccessExpression.Expression.GetRightmostName(); - var typeDeclaration = syntaxFactsService.GetContainingTypeDeclaration( - semanticModel.SyntaxTree.GetRoot(), - plusEqualsToken.SpanStart) as BaseTypeDeclarationSyntax; + if (lhs is GenericNameSyntax lhsGenericNameSyntax) + { + // For generic we must exclude type variables + return lhsGenericNameSyntax.Identifier.Text; + } - return typeDeclaration != null - ? typeDeclaration.Identifier.Text - : eventSymbol.ContainingType.Name; + if (lhs != null) + { + return lhs.ToString(); + } } + + // If we didn't find an object name above, then the object name is the name of this class. + // Note: For generic, it's ok(it's even a good idea) to exclude type variables, + // because the name is only used as a prefix for the method name. + + var typeDeclaration = syntaxFactsService.GetContainingTypeDeclaration( + semanticModel.SyntaxTree.GetRoot(), + plusEqualsToken.SpanStart) as BaseTypeDeclarationSyntax; + + return typeDeclaration != null + ? typeDeclaration.Identifier.Text + : eventSymbol.ContainingType.Name; } } } diff --git a/src/EditorFeatures/CSharp/ExtractInterface/ExtractInterfaceCommandHandler.cs b/src/EditorFeatures/CSharp/ExtractInterface/ExtractInterfaceCommandHandler.cs index 8b7f5e051b0a4..5e43d72ea629c 100644 --- a/src/EditorFeatures/CSharp/ExtractInterface/ExtractInterfaceCommandHandler.cs +++ b/src/EditorFeatures/CSharp/ExtractInterface/ExtractInterfaceCommandHandler.cs @@ -12,14 +12,13 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractInterface +namespace Microsoft.CodeAnalysis.CSharp.ExtractInterface; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(PredefinedCommandHandlerNames.ExtractInterface)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class ExtractInterfaceCommandHandler(IThreadingContext threadingContext, IGlobalOptionService globalOptions) : AbstractExtractInterfaceCommandHandler(threadingContext, globalOptions) { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(PredefinedCommandHandlerNames.ExtractInterface)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class ExtractInterfaceCommandHandler(IThreadingContext threadingContext, IGlobalOptionService globalOptions) : AbstractExtractInterfaceCommandHandler(threadingContext, globalOptions) - { - } } diff --git a/src/EditorFeatures/CSharp/FixInterpolatedVerbatimString/FixInterpolatedVerbatimStringCommandHandler.cs b/src/EditorFeatures/CSharp/FixInterpolatedVerbatimString/FixInterpolatedVerbatimStringCommandHandler.cs index f4ef71f269715..92dd6e2d708f9 100644 --- a/src/EditorFeatures/CSharp/FixInterpolatedVerbatimString/FixInterpolatedVerbatimStringCommandHandler.cs +++ b/src/EditorFeatures/CSharp/FixInterpolatedVerbatimString/FixInterpolatedVerbatimStringCommandHandler.cs @@ -15,79 +15,78 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.FixInterpolatedVerbatimString +namespace Microsoft.CodeAnalysis.Editor.CSharp.FixInterpolatedVerbatimString; + +/// +/// Replaces @$" with $@", which is the preferred and until C# 8.0 the only supported form +/// of an interpolated verbatim string start token. In C# 8.0 we still auto-correct to this form for consistency. +/// +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(nameof(FixInterpolatedVerbatimStringCommandHandler))] +internal sealed class FixInterpolatedVerbatimStringCommandHandler : IChainedCommandHandler { - /// - /// Replaces @$" with $@", which is the preferred and until C# 8.0 the only supported form - /// of an interpolated verbatim string start token. In C# 8.0 we still auto-correct to this form for consistency. - /// - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(nameof(FixInterpolatedVerbatimStringCommandHandler))] - internal sealed class FixInterpolatedVerbatimStringCommandHandler : IChainedCommandHandler + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public FixInterpolatedVerbatimStringCommandHandler() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public FixInterpolatedVerbatimStringCommandHandler() - { - } + } - public string DisplayName => CSharpEditorResources.Fix_interpolated_verbatim_string; + public string DisplayName => CSharpEditorResources.Fix_interpolated_verbatim_string; - public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) - { - // We need to check for the token *after* the opening quote is typed, so defer to the editor first - nextCommandHandler(); + public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + // We need to check for the token *after* the opening quote is typed, so defer to the editor first + nextCommandHandler(); - var cancellationToken = executionContext.OperationContext.UserCancellationToken; - if (cancellationToken.IsCancellationRequested) - { - return; - } + var cancellationToken = executionContext.OperationContext.UserCancellationToken; + if (cancellationToken.IsCancellationRequested) + { + return; + } - try - { - ExecuteCommandWorker(args, cancellationToken); - } - catch (OperationCanceledException) - { - // According to Editor command handler API guidelines, it's best if we return early if cancellation - // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already - // calling nextCommandHandler(). - } + try + { + ExecuteCommandWorker(args, cancellationToken); } + catch (OperationCanceledException) + { + // According to Editor command handler API guidelines, it's best if we return early if cancellation + // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already + // calling nextCommandHandler(). + } + } - private static void ExecuteCommandWorker(TypeCharCommandArgs args, CancellationToken cancellationToken) + private static void ExecuteCommandWorker(TypeCharCommandArgs args, CancellationToken cancellationToken) + { + if (args.TypedChar == '"') { - if (args.TypedChar == '"') + var caret = args.TextView.GetCaretPoint(args.SubjectBuffer); + if (caret != null) { - var caret = args.TextView.GetCaretPoint(args.SubjectBuffer); - if (caret != null) - { - var position = caret.Value.Position; - var snapshot = caret.Value.Snapshot; + var position = caret.Value.Position; + var snapshot = caret.Value.Snapshot; - if (position >= 3 && - snapshot[position - 1] == '"' && - snapshot[position - 2] == '$' && - snapshot[position - 3] == '@') + if (position >= 3 && + snapshot[position - 1] == '"' && + snapshot[position - 2] == '$' && + snapshot[position - 3] == '@') + { + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document != null) { - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document != null) + var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); + var token = root.FindToken(position - 3); + if (token.IsKind(SyntaxKind.InterpolatedVerbatimStringStartToken)) { - var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); - var token = root.FindToken(position - 3); - if (token.IsKind(SyntaxKind.InterpolatedVerbatimStringStartToken)) - { - args.SubjectBuffer.Replace(new Span(position - 3, 2), "$@"); - } + args.SubjectBuffer.Replace(new Span(position - 3, 2), "$@"); } } } } } - - public CommandState GetCommandState(TypeCharCommandArgs args, Func nextCommandHandler) - => nextCommandHandler(); } + + public CommandState GetCommandState(TypeCharCommandArgs args, Func nextCommandHandler) + => nextCommandHandler(); } diff --git a/src/EditorFeatures/CSharp/Formatting/CSharpFormattingInteractionService.cs b/src/EditorFeatures/CSharp/Formatting/CSharpFormattingInteractionService.cs index b948af3efb3dd..4c0abffa1b6b8 100644 --- a/src/EditorFeatures/CSharp/Formatting/CSharpFormattingInteractionService.cs +++ b/src/EditorFeatures/CSharp/Formatting/CSharpFormattingInteractionService.cs @@ -20,104 +20,103 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Formatting +namespace Microsoft.CodeAnalysis.CSharp.Formatting; + +[ExportLanguageService(typeof(IFormattingInteractionService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal partial class CSharpFormattingInteractionService(EditorOptionsService editorOptionsService) : IFormattingInteractionService { - [ExportLanguageService(typeof(IFormattingInteractionService), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal partial class CSharpFormattingInteractionService(EditorOptionsService editorOptionsService) : IFormattingInteractionService - { - // All the characters that might potentially trigger formatting when typed - private static readonly char[] _supportedChars = ";{}#nte:)".ToCharArray(); + // All the characters that might potentially trigger formatting when typed + private static readonly char[] _supportedChars = ";{}#nte:)".ToCharArray(); - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - public bool SupportsFormatDocument => true; - public bool SupportsFormatOnPaste => true; - public bool SupportsFormatSelection => true; - public bool SupportsFormatOnReturn => false; + public bool SupportsFormatDocument => true; + public bool SupportsFormatOnPaste => true; + public bool SupportsFormatSelection => true; + public bool SupportsFormatOnReturn => false; - public bool SupportsFormattingOnTypedCharacter(Document document, char ch) + public bool SupportsFormattingOnTypedCharacter(Document document, char ch) + { + var isSmartIndent = _editorOptionsService.GlobalOptions.GetOption(IndentationOptionsStorage.SmartIndent, LanguageNames.CSharp) == FormattingOptions2.IndentStyle.Smart; + + // We consider the proper placement of a close curly or open curly when it is typed at + // the start of the line to be a smart-indentation operation. As such, even if "format + // on typing" is off, if "smart indent" is on, we'll still format this. (However, we + // won't touch anything else in the block this close curly belongs to.). + // + // See extended comment in GetFormattingChangesAsync for more details on this. + if (isSmartIndent && ch is '{' or '}') { - var isSmartIndent = _editorOptionsService.GlobalOptions.GetOption(IndentationOptionsStorage.SmartIndent, LanguageNames.CSharp) == FormattingOptions2.IndentStyle.Smart; - - // We consider the proper placement of a close curly or open curly when it is typed at - // the start of the line to be a smart-indentation operation. As such, even if "format - // on typing" is off, if "smart indent" is on, we'll still format this. (However, we - // won't touch anything else in the block this close curly belongs to.). - // - // See extended comment in GetFormattingChangesAsync for more details on this. - if (isSmartIndent && ch is '{' or '}') - { - return true; - } - - var options = _editorOptionsService.GlobalOptions.GetAutoFormattingOptions(LanguageNames.CSharp); - - // If format-on-typing is not on, then we don't support formatting on any other characters. - var autoFormattingOnTyping = options.FormatOnTyping; - if (!autoFormattingOnTyping) - { - return false; - } - - if (ch == '}' && !options.FormatOnCloseBrace) - { - return false; - } - - if (ch == ';' && !options.FormatOnSemicolon) - { - return false; - } - - // don't auto format after these keys if smart indenting is not on. - if (ch is '#' or 'n' && !isSmartIndent) - { - return false; - } - - return _supportedChars.Contains(ch); + return true; } - public Task> GetFormattingChangesAsync( - Document document, - ITextBuffer textBuffer, - TextSpan? textSpan, - CancellationToken cancellationToken) + var options = _editorOptionsService.GlobalOptions.GetAutoFormattingOptions(LanguageNames.CSharp); + + // If format-on-typing is not on, then we don't support formatting on any other characters. + var autoFormattingOnTyping = options.FormatOnTyping; + if (!autoFormattingOnTyping) { - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - var options = textBuffer.GetSyntaxFormattingOptions(_editorOptionsService, parsedDocument.LanguageServices, explicitFormat: true); + return false; + } - var span = textSpan ?? new TextSpan(0, parsedDocument.Root.FullSpan.Length); - var formattingSpan = CommonFormattingHelpers.GetFormattingSpan(parsedDocument.Root, span); + if (ch == '}' && !options.FormatOnCloseBrace) + { + return false; + } - return Task.FromResult(Formatter.GetFormattedTextChanges(parsedDocument.Root, SpecializedCollections.SingletonEnumerable(formattingSpan), document.Project.Solution.Services, options, cancellationToken).ToImmutableArray()); + if (ch == ';' && !options.FormatOnSemicolon) + { + return false; } - public Task> GetFormattingChangesOnPasteAsync(Document document, ITextBuffer textBuffer, TextSpan textSpan, CancellationToken cancellationToken) + // don't auto format after these keys if smart indenting is not on. + if (ch is '#' or 'n' && !isSmartIndent) { - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - var options = textBuffer.GetSyntaxFormattingOptions(_editorOptionsService, parsedDocument.LanguageServices, explicitFormat: true); - var service = parsedDocument.LanguageServices.GetRequiredService(); - return Task.FromResult(service.GetFormattingChangesOnPaste(parsedDocument, textSpan, options, cancellationToken)); + return false; } - public Task> GetFormattingChangesOnReturnAsync(Document document, int caretPosition, CancellationToken cancellationToken) - => SpecializedTasks.EmptyImmutableArray(); + return _supportedChars.Contains(ch); + } - public Task> GetFormattingChangesAsync(Document document, ITextBuffer textBuffer, char typedChar, int position, CancellationToken cancellationToken) - { - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - var service = parsedDocument.LanguageServices.GetRequiredService(); + public Task> GetFormattingChangesAsync( + Document document, + ITextBuffer textBuffer, + TextSpan? textSpan, + CancellationToken cancellationToken) + { + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + var options = textBuffer.GetSyntaxFormattingOptions(_editorOptionsService, parsedDocument.LanguageServices, explicitFormat: true); - if (service.ShouldFormatOnTypedCharacter(parsedDocument, typedChar, position, cancellationToken)) - { - var indentationOptions = textBuffer.GetIndentationOptions(_editorOptionsService, parsedDocument.LanguageServices, explicitFormat: false); - return Task.FromResult(service.GetFormattingChangesOnTypedCharacter(parsedDocument, position, indentationOptions, cancellationToken)); - } + var span = textSpan ?? new TextSpan(0, parsedDocument.Root.FullSpan.Length); + var formattingSpan = CommonFormattingHelpers.GetFormattingSpan(parsedDocument.Root, span); - return SpecializedTasks.EmptyImmutableArray(); + return Task.FromResult(Formatter.GetFormattedTextChanges(parsedDocument.Root, SpecializedCollections.SingletonEnumerable(formattingSpan), document.Project.Solution.Services, options, cancellationToken).ToImmutableArray()); + } + + public Task> GetFormattingChangesOnPasteAsync(Document document, ITextBuffer textBuffer, TextSpan textSpan, CancellationToken cancellationToken) + { + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + var options = textBuffer.GetSyntaxFormattingOptions(_editorOptionsService, parsedDocument.LanguageServices, explicitFormat: true); + var service = parsedDocument.LanguageServices.GetRequiredService(); + return Task.FromResult(service.GetFormattingChangesOnPaste(parsedDocument, textSpan, options, cancellationToken)); + } + + public Task> GetFormattingChangesOnReturnAsync(Document document, int caretPosition, CancellationToken cancellationToken) + => SpecializedTasks.EmptyImmutableArray(); + + public Task> GetFormattingChangesAsync(Document document, ITextBuffer textBuffer, char typedChar, int position, CancellationToken cancellationToken) + { + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + var service = parsedDocument.LanguageServices.GetRequiredService(); + + if (service.ShouldFormatOnTypedCharacter(parsedDocument, typedChar, position, cancellationToken)) + { + var indentationOptions = textBuffer.GetIndentationOptions(_editorOptionsService, parsedDocument.LanguageServices, explicitFormat: false); + return Task.FromResult(service.GetFormattingChangesOnTypedCharacter(parsedDocument, position, indentationOptions, cancellationToken)); } + + return SpecializedTasks.EmptyImmutableArray(); } } diff --git a/src/EditorFeatures/CSharp/GoToBase/CSharpGoToBaseService.cs b/src/EditorFeatures/CSharp/GoToBase/CSharpGoToBaseService.cs index 37866c571e462..b1c7ac3ff9bbe 100644 --- a/src/EditorFeatures/CSharp/GoToBase/CSharpGoToBaseService.cs +++ b/src/EditorFeatures/CSharp/GoToBase/CSharpGoToBaseService.cs @@ -12,33 +12,32 @@ using Microsoft.CodeAnalysis.GoToBase; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.GoToBase +namespace Microsoft.CodeAnalysis.CSharp.GoToBase; + +[ExportLanguageService(typeof(IGoToBaseService), LanguageNames.CSharp), Shared] +internal sealed class CSharpGoToBaseService : AbstractGoToBaseService { - [ExportLanguageService(typeof(IGoToBaseService), LanguageNames.CSharp), Shared] - internal sealed class CSharpGoToBaseService : AbstractGoToBaseService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGoToBaseService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGoToBaseService() - { - } + } - protected override async Task FindNextConstructorInChainAsync( - Solution solution, IMethodSymbol constructor, CancellationToken cancellationToken) - { - if (constructor.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken) is not ConstructorDeclarationSyntax constructorDeclaration) - return null; + protected override async Task FindNextConstructorInChainAsync( + Solution solution, IMethodSymbol constructor, CancellationToken cancellationToken) + { + if (constructor.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken) is not ConstructorDeclarationSyntax constructorDeclaration) + return null; - var document = solution.GetDocument(constructorDeclaration.SyntaxTree); - if (document is null) - return null; + var document = solution.GetDocument(constructorDeclaration.SyntaxTree); + if (document is null) + return null; - // this constructor must be calling an accessible no-arg constructor in the base type. - if (constructorDeclaration.Initializer is null) - return FindBaseNoArgConstructor(constructor); + // this constructor must be calling an accessible no-arg constructor in the base type. + if (constructorDeclaration.Initializer is null) + return FindBaseNoArgConstructor(constructor); - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - return semanticModel.GetSymbolInfo(constructorDeclaration.Initializer, cancellationToken).GetAnySymbol() as IMethodSymbol; - } + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + return semanticModel.GetSymbolInfo(constructorDeclaration.Initializer, cancellationToken).GetAnySymbol() as IMethodSymbol; } } diff --git a/src/EditorFeatures/CSharp/InlineRename/CSharpEditorInlineRenameService.cs b/src/EditorFeatures/CSharp/InlineRename/CSharpEditorInlineRenameService.cs index 2c1fe27133269..bbb8adf16055d 100644 --- a/src/EditorFeatures/CSharp/InlineRename/CSharpEditorInlineRenameService.cs +++ b/src/EditorFeatures/CSharp/InlineRename/CSharpEditorInlineRenameService.cs @@ -9,14 +9,13 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.CSharp.InlineRename +namespace Microsoft.CodeAnalysis.Editor.CSharp.InlineRename; + +[ExportLanguageService(typeof(IEditorInlineRenameService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpEditorInlineRenameService( + [ImportMany] IEnumerable refactorNotifyServices, + IGlobalOptionService globalOptions) : AbstractEditorInlineRenameService(refactorNotifyServices, globalOptions) { - [ExportLanguageService(typeof(IEditorInlineRenameService), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class CSharpEditorInlineRenameService( - [ImportMany] IEnumerable refactorNotifyServices, - IGlobalOptionService globalOptions) : AbstractEditorInlineRenameService(refactorNotifyServices, globalOptions) - { - } } diff --git a/src/EditorFeatures/CSharp/Interactive/CSharpInteractiveEvaluatorLanguageInfoProvider.cs b/src/EditorFeatures/CSharp/Interactive/CSharpInteractiveEvaluatorLanguageInfoProvider.cs index 94edfb224c348..d67bdb5742dd3 100644 --- a/src/EditorFeatures/CSharp/Interactive/CSharpInteractiveEvaluatorLanguageInfoProvider.cs +++ b/src/EditorFeatures/CSharp/Interactive/CSharpInteractiveEvaluatorLanguageInfoProvider.cs @@ -10,49 +10,48 @@ using Microsoft.CodeAnalysis.Interactive; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor.CSharp.Interactive +namespace Microsoft.CodeAnalysis.Editor.CSharp.Interactive; + +internal sealed class CSharpInteractiveEvaluatorLanguageInfoProvider : InteractiveEvaluatorLanguageInfoProvider { - internal sealed class CSharpInteractiveEvaluatorLanguageInfoProvider : InteractiveEvaluatorLanguageInfoProvider - { - public static readonly CSharpInteractiveEvaluatorLanguageInfoProvider Instance = new(); + public static readonly CSharpInteractiveEvaluatorLanguageInfoProvider Instance = new(); - private CSharpInteractiveEvaluatorLanguageInfoProvider() - { - } + private CSharpInteractiveEvaluatorLanguageInfoProvider() + { + } - private static readonly CSharpParseOptions s_parseOptions = - new(languageVersion: LanguageVersion.Latest, kind: SourceCodeKind.Script); + private static readonly CSharpParseOptions s_parseOptions = + new(languageVersion: LanguageVersion.Latest, kind: SourceCodeKind.Script); - public override string LanguageName - => LanguageNames.CSharp; + public override string LanguageName + => LanguageNames.CSharp; - public override ParseOptions ParseOptions - => s_parseOptions; + public override ParseOptions ParseOptions + => s_parseOptions; - public override CommandLineParser CommandLineParser - => CSharpCommandLineParser.Script; + public override CommandLineParser CommandLineParser + => CSharpCommandLineParser.Script; - public override CompilationOptions GetSubmissionCompilationOptions(string name, MetadataReferenceResolver metadataReferenceResolver, SourceReferenceResolver sourceReferenceResolver, ImmutableArray imports) - => CSharpScriptCompiler.WithTopLevelBinderFlags( - new CSharpCompilationOptions( - OutputKind.DynamicallyLinkedLibrary, - scriptClassName: name, - allowUnsafe: true, - xmlReferenceResolver: null, // no support for permission set and doc includes in interactive - usings: imports, - sourceReferenceResolver: sourceReferenceResolver, - metadataReferenceResolver: metadataReferenceResolver, - assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); + public override CompilationOptions GetSubmissionCompilationOptions(string name, MetadataReferenceResolver metadataReferenceResolver, SourceReferenceResolver sourceReferenceResolver, ImmutableArray imports) + => CSharpScriptCompiler.WithTopLevelBinderFlags( + new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + scriptClassName: name, + allowUnsafe: true, + xmlReferenceResolver: null, // no support for permission set and doc includes in interactive + usings: imports, + sourceReferenceResolver: sourceReferenceResolver, + metadataReferenceResolver: metadataReferenceResolver, + assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); - public override bool IsCompleteSubmission(string text) - => SyntaxFactory.IsCompleteSubmission(SyntaxFactory.ParseSyntaxTree(SourceText.From(text, encoding: null, SourceHashAlgorithms.Default), options: s_parseOptions)); + public override bool IsCompleteSubmission(string text) + => SyntaxFactory.IsCompleteSubmission(SyntaxFactory.ParseSyntaxTree(SourceText.From(text, encoding: null, SourceHashAlgorithms.Default), options: s_parseOptions)); - public override string InteractiveResponseFileName - => "CSharpInteractive.rsp"; + public override string InteractiveResponseFileName + => "CSharpInteractive.rsp"; - public override Type ReplServiceProviderType - => typeof(CSharpReplServiceProvider); + public override Type ReplServiceProviderType + => typeof(CSharpReplServiceProvider); - public override string Extension => ".csx"; - } + public override string Extension => ".csx"; } diff --git a/src/EditorFeatures/CSharp/Interactive/CSharpSendToInteractiveSubmissionProvider.cs b/src/EditorFeatures/CSharp/Interactive/CSharpSendToInteractiveSubmissionProvider.cs index a8e8baae1dfd9..178476a2ebfa4 100644 --- a/src/EditorFeatures/CSharp/Interactive/CSharpSendToInteractiveSubmissionProvider.cs +++ b/src/EditorFeatures/CSharp/Interactive/CSharpSendToInteractiveSubmissionProvider.cs @@ -13,118 +13,117 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor.CSharp.Interactive +namespace Microsoft.CodeAnalysis.Editor.CSharp.Interactive; + +[Export(typeof(ISendToInteractiveSubmissionProvider))] +internal sealed class CSharpSendToInteractiveSubmissionProvider + : AbstractSendToInteractiveSubmissionProvider { - [Export(typeof(ISendToInteractiveSubmissionProvider))] - internal sealed class CSharpSendToInteractiveSubmissionProvider - : AbstractSendToInteractiveSubmissionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpSendToInteractiveSubmissionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpSendToInteractiveSubmissionProvider() - { - } + } - protected override bool CanParseSubmission(string code) + protected override bool CanParseSubmission(string code) + { + var options = CSharpInteractiveEvaluatorLanguageInfoProvider.Instance.ParseOptions; + var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(code, encoding: null, SourceHashAlgorithms.Default), options); + return tree.HasCompilationUnitRoot && + !tree.GetDiagnostics().Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error); + } + + protected override IEnumerable GetExecutableSyntaxTreeNodeSelection(TextSpan selectionSpan, SyntaxNode root) + { + var expandedNode = GetSyntaxNodeForSubmission(selectionSpan, root); + return expandedNode != null + ? [expandedNode.Span] + : Array.Empty(); + } + + /// + /// Finds a that should be submitted to REPL. + /// + /// Selection that user has originally made. + /// Root of the syntax tree. + private static SyntaxNode? GetSyntaxNodeForSubmission(TextSpan selectionSpan, SyntaxNode root) + { + GetSelectedTokens(selectionSpan, root, out var startToken, out var endToken); + + // Ensure that the first token comes before the last token. + // Otherwise selection did not contain any tokens. + if (startToken != endToken && startToken.Span.End > endToken.SpanStart) + return null; + + if (startToken == endToken) { - var options = CSharpInteractiveEvaluatorLanguageInfoProvider.Instance.ParseOptions; - var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(code, encoding: null, SourceHashAlgorithms.Default), options); - return tree.HasCompilationUnitRoot && - !tree.GetDiagnostics().Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error); + return GetSyntaxNodeForSubmission(startToken.GetRequiredParent()); } - protected override IEnumerable GetExecutableSyntaxTreeNodeSelection(TextSpan selectionSpan, SyntaxNode root) + var startNode = GetSyntaxNodeForSubmission(startToken.GetRequiredParent()); + var endNode = GetSyntaxNodeForSubmission(endToken.GetRequiredParent()); + + // If there is no SyntaxNode worth sending to the REPL return null. + if (startNode == null || endNode == null) { - var expandedNode = GetSyntaxNodeForSubmission(selectionSpan, root); - return expandedNode != null - ? [expandedNode.Span] - : Array.Empty(); + return null; } - /// - /// Finds a that should be submitted to REPL. - /// - /// Selection that user has originally made. - /// Root of the syntax tree. - private static SyntaxNode? GetSyntaxNodeForSubmission(TextSpan selectionSpan, SyntaxNode root) + // If one of the nodes is an ancestor of another node return that node. + if (startNode.Span.Contains(endNode.Span)) { - GetSelectedTokens(selectionSpan, root, out var startToken, out var endToken); - - // Ensure that the first token comes before the last token. - // Otherwise selection did not contain any tokens. - if (startToken != endToken && startToken.Span.End > endToken.SpanStart) - return null; - - if (startToken == endToken) - { - return GetSyntaxNodeForSubmission(startToken.GetRequiredParent()); - } - - var startNode = GetSyntaxNodeForSubmission(startToken.GetRequiredParent()); - var endNode = GetSyntaxNodeForSubmission(endToken.GetRequiredParent()); - - // If there is no SyntaxNode worth sending to the REPL return null. - if (startNode == null || endNode == null) - { - return null; - } - - // If one of the nodes is an ancestor of another node return that node. - if (startNode.Span.Contains(endNode.Span)) - { - return startNode; - } - else if (endNode.Span.Contains(startNode.Span)) - { - return endNode; - } - - // Selection spans multiple statements. - // In this case find common parent and find a span of statements within that parent. - return GetSyntaxNodeForSubmission(startNode.GetCommonRoot(endNode)); + return startNode; } - - /// - /// Finds a that should be submitted to REPL. - /// - /// The currently selected node. - private static SyntaxNode? GetSyntaxNodeForSubmission(SyntaxNode node) + else if (endNode.Span.Contains(startNode.Span)) { - SyntaxNode? candidate = node.GetAncestorOrThis(); - if (candidate != null) - { - return candidate; - } - - candidate = node.GetAncestorsOrThis() - .Where(IsSubmissionNode).FirstOrDefault(); - if (candidate != null) - { - return candidate; - } - - return null; + return endNode; } - /// Returns true if node could be treated as a REPL submission. - private static bool IsSubmissionNode(SyntaxNode node) + // Selection spans multiple statements. + // In this case find common parent and find a span of statements within that parent. + return GetSyntaxNodeForSubmission(startNode.GetCommonRoot(endNode)); + } + + /// + /// Finds a that should be submitted to REPL. + /// + /// The currently selected node. + private static SyntaxNode? GetSyntaxNodeForSubmission(SyntaxNode node) + { + SyntaxNode? candidate = node.GetAncestorOrThis(); + if (candidate != null) { - var kind = node.Kind(); - return SyntaxFacts.IsTypeDeclaration(kind) - || SyntaxFacts.IsGlobalMemberDeclaration(kind) - || node.IsKind(SyntaxKind.UsingDirective); + return candidate; } - private static void GetSelectedTokens( - TextSpan selectionSpan, - SyntaxNode root, - out SyntaxToken startToken, - out SyntaxToken endToken) + candidate = node.GetAncestorsOrThis() + .Where(IsSubmissionNode).FirstOrDefault(); + if (candidate != null) { - endToken = root.FindTokenOnLeftOfPosition(selectionSpan.End); - startToken = selectionSpan.Length == 0 - ? endToken - : root.FindTokenOnRightOfPosition(selectionSpan.Start); + return candidate; } + + return null; + } + + /// Returns true if node could be treated as a REPL submission. + private static bool IsSubmissionNode(SyntaxNode node) + { + var kind = node.Kind(); + return SyntaxFacts.IsTypeDeclaration(kind) + || SyntaxFacts.IsGlobalMemberDeclaration(kind) + || node.IsKind(SyntaxKind.UsingDirective); + } + + private static void GetSelectedTokens( + TextSpan selectionSpan, + SyntaxNode root, + out SyntaxToken startToken, + out SyntaxToken endToken) + { + endToken = root.FindTokenOnLeftOfPosition(selectionSpan.End); + startToken = selectionSpan.Length == 0 + ? endToken + : root.FindTokenOnRightOfPosition(selectionSpan.Start); } } diff --git a/src/EditorFeatures/CSharp/LanguageServices/CSharpContentTypeLanguageService.cs b/src/EditorFeatures/CSharp/LanguageServices/CSharpContentTypeLanguageService.cs index d96b25668a156..ae83f7b858c61 100644 --- a/src/EditorFeatures/CSharp/LanguageServices/CSharpContentTypeLanguageService.cs +++ b/src/EditorFeatures/CSharp/LanguageServices/CSharpContentTypeLanguageService.cs @@ -7,16 +7,15 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.LanguageServices +namespace Microsoft.CodeAnalysis.Editor.CSharp.LanguageServices; + +[ExportContentTypeLanguageService(ContentTypeNames.CSharpContentType, LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpContentTypeLanguageService(IContentTypeRegistryService contentTypeRegistry) : IContentTypeLanguageService { - [ExportContentTypeLanguageService(ContentTypeNames.CSharpContentType, LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpContentTypeLanguageService(IContentTypeRegistryService contentTypeRegistry) : IContentTypeLanguageService - { - private readonly IContentTypeRegistryService _contentTypeRegistry = contentTypeRegistry; + private readonly IContentTypeRegistryService _contentTypeRegistry = contentTypeRegistry; - public IContentType GetDefaultContentType() - => _contentTypeRegistry.GetContentType(ContentTypeNames.CSharpContentType); - } + public IContentType GetDefaultContentType() + => _contentTypeRegistry.GetContentType(ContentTypeNames.CSharpContentType); } diff --git a/src/EditorFeatures/CSharp/NavigationBar/CSharpEditorNavigationBarItemService.cs b/src/EditorFeatures/CSharp/NavigationBar/CSharpEditorNavigationBarItemService.cs index 99424cce062e8..2eba567e638bf 100644 --- a/src/EditorFeatures/CSharp/NavigationBar/CSharpEditorNavigationBarItemService.cs +++ b/src/EditorFeatures/CSharp/NavigationBar/CSharpEditorNavigationBarItemService.cs @@ -13,17 +13,16 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor.CSharp.NavigationBar +namespace Microsoft.CodeAnalysis.Editor.CSharp.NavigationBar; + +[ExportLanguageService(typeof(INavigationBarItemService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpEditorNavigationBarItemService(IThreadingContext threadingContext) : AbstractEditorNavigationBarItemService(threadingContext) { - [ExportLanguageService(typeof(INavigationBarItemService), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpEditorNavigationBarItemService(IThreadingContext threadingContext) : AbstractEditorNavigationBarItemService(threadingContext) + protected override async Task TryNavigateToItemAsync(Document document, WrappedNavigationBarItem item, ITextView textView, ITextVersion textVersion, CancellationToken cancellationToken) { - protected override async Task TryNavigateToItemAsync(Document document, WrappedNavigationBarItem item, ITextView textView, ITextVersion textVersion, CancellationToken cancellationToken) - { - await NavigateToSymbolItemAsync(document, item, (RoslynNavigationBarItem.SymbolItem)item.UnderlyingItem, textVersion, cancellationToken).ConfigureAwait(false); - return true; - } + await NavigateToSymbolItemAsync(document, item, (RoslynNavigationBarItem.SymbolItem)item.UnderlyingItem, textVersion, cancellationToken).ConfigureAwait(false); + return true; } } diff --git a/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler.cs b/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler.cs index 09000656a62ab..1fa853da29253 100644 --- a/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler.cs +++ b/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler.cs @@ -12,27 +12,26 @@ using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.RawStringLiteral +namespace Microsoft.CodeAnalysis.Editor.CSharp.RawStringLiteral; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(nameof(RawStringLiteralCommandHandler))] +[Order(After = nameof(SplitStringLiteralCommandHandler))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal partial class RawStringLiteralCommandHandler( + ITextUndoHistoryRegistry undoHistoryRegistry, + IGlobalOptionService globalOptions, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService, + IIndentationManagerService indentationManager) { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(nameof(RawStringLiteralCommandHandler))] - [Order(After = nameof(SplitStringLiteralCommandHandler))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal partial class RawStringLiteralCommandHandler( - ITextUndoHistoryRegistry undoHistoryRegistry, - IGlobalOptionService globalOptions, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService, - IIndentationManagerService indentationManager) - { - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; - private readonly IGlobalOptionService _globalOptions = globalOptions; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - private readonly IIndentationManagerService _indentationManager = indentationManager; + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; + private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly IIndentationManagerService _indentationManager = indentationManager; - public string DisplayName => CSharpEditorResources.Split_raw_string; - } + public string DisplayName => CSharpEditorResources.Split_raw_string; } diff --git a/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler_Return.cs b/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler_Return.cs index fd4d38290fcb2..35ec41c436d82 100644 --- a/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler_Return.cs +++ b/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler_Return.cs @@ -18,108 +18,107 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.RawStringLiteral +namespace Microsoft.CodeAnalysis.Editor.CSharp.RawStringLiteral; + +internal partial class RawStringLiteralCommandHandler : ICommandHandler { - internal partial class RawStringLiteralCommandHandler : ICommandHandler + public CommandState GetCommandState(ReturnKeyCommandArgs args) + => CommandState.Unspecified; + + /// + /// Checks to see if the user is typing return in """$$""" and then properly indents the end + /// delimiter of the raw string literal. + /// + public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) { - public CommandState GetCommandState(ReturnKeyCommandArgs args) - => CommandState.Unspecified; - - /// - /// Checks to see if the user is typing return in """$$""" and then properly indents the end - /// delimiter of the raw string literal. - /// - public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) - { - var textView = args.TextView; - var subjectBuffer = args.SubjectBuffer; - var spans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); + var textView = args.TextView; + var subjectBuffer = args.SubjectBuffer; + var spans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); - if (spans.Count != 1) - return false; + if (spans.Count != 1) + return false; - var span = spans.First(); - if (span.Length != 0) - return false; + var span = spans.First(); + if (span.Length != 0) + return false; - var caret = textView.GetCaretPoint(subjectBuffer); - if (caret == null) - return false; + var caret = textView.GetCaretPoint(subjectBuffer); + if (caret == null) + return false; - var position = caret.Value.Position; - var currentSnapshot = subjectBuffer.CurrentSnapshot; - if (position >= currentSnapshot.Length) - return false; + var position = caret.Value.Position; + var currentSnapshot = subjectBuffer.CurrentSnapshot; + if (position >= currentSnapshot.Length) + return false; - if (currentSnapshot[position] != '"') - return false; + if (currentSnapshot[position] != '"') + return false; - var quotesBefore = 0; - var quotesAfter = 0; + var quotesBefore = 0; + var quotesAfter = 0; - for (int i = position, n = currentSnapshot.Length; i < n; i++) - { - if (currentSnapshot[i] != '"') - break; + for (int i = position, n = currentSnapshot.Length; i < n; i++) + { + if (currentSnapshot[i] != '"') + break; - quotesAfter++; - } + quotesAfter++; + } - for (var i = position - 1; i >= 0; i--) - { - if (currentSnapshot[i] != '"') - break; + for (var i = position - 1; i >= 0; i--) + { + if (currentSnapshot[i] != '"') + break; - quotesBefore++; - } + quotesBefore++; + } - if (quotesAfter != quotesBefore) - return false; + if (quotesAfter != quotesBefore) + return false; - if (quotesAfter < 3) - return false; + if (quotesAfter < 3) + return false; - return SplitRawString(textView, subjectBuffer, span.Start.Position, CancellationToken.None); - } + return SplitRawString(textView, subjectBuffer, span.Start.Position, CancellationToken.None); + } - private bool SplitRawString(ITextView textView, ITextBuffer subjectBuffer, int position, CancellationToken cancellationToken) - { - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return false; + private bool SplitRawString(ITextView textView, ITextBuffer subjectBuffer, int position, CancellationToken cancellationToken) + { + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return false; - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - var token = parsedDocument.Root.FindToken(position); - if (token.Kind() is not (SyntaxKind.SingleLineRawStringLiteralToken or - SyntaxKind.MultiLineRawStringLiteralToken or - SyntaxKind.InterpolatedSingleLineRawStringStartToken or - SyntaxKind.InterpolatedMultiLineRawStringStartToken)) - { - return false; - } + var token = parsedDocument.Root.FindToken(position); + if (token.Kind() is not (SyntaxKind.SingleLineRawStringLiteralToken or + SyntaxKind.MultiLineRawStringLiteralToken or + SyntaxKind.InterpolatedSingleLineRawStringStartToken or + SyntaxKind.InterpolatedMultiLineRawStringStartToken)) + { + return false; + } - var indentationOptions = subjectBuffer.GetIndentationOptions(_editorOptionsService, document.Project.Services, explicitFormat: false); - var indentation = token.GetPreferredIndentation(parsedDocument, indentationOptions, cancellationToken); + var indentationOptions = subjectBuffer.GetIndentationOptions(_editorOptionsService, document.Project.Services, explicitFormat: false); + var indentation = token.GetPreferredIndentation(parsedDocument, indentationOptions, cancellationToken); - var newLine = indentationOptions.FormattingOptions.NewLine; + var newLine = indentationOptions.FormattingOptions.NewLine; - using var transaction = CaretPreservingEditTransaction.TryCreate( - CSharpEditorResources.Split_string, textView, _undoHistoryRegistry, _editorOperationsFactoryService); + using var transaction = CaretPreservingEditTransaction.TryCreate( + CSharpEditorResources.Split_string, textView, _undoHistoryRegistry, _editorOperationsFactoryService); - var edit = subjectBuffer.CreateEdit(); + var edit = subjectBuffer.CreateEdit(); - // apply the change: - edit.Insert(position, newLine + newLine + indentation); - var snapshot = edit.Apply(); + // apply the change: + edit.Insert(position, newLine + newLine + indentation); + var snapshot = edit.Apply(); - // move caret: - var lineInNewSnapshot = snapshot.GetLineFromPosition(position); - var nextLine = snapshot.GetLineFromLineNumber(lineInNewSnapshot.LineNumber + 1); - textView.Caret.MoveTo(new VirtualSnapshotPoint(nextLine, indentation.Length)); + // move caret: + var lineInNewSnapshot = snapshot.GetLineFromPosition(position); + var nextLine = snapshot.GetLineFromLineNumber(lineInNewSnapshot.LineNumber + 1); + textView.Caret.MoveTo(new VirtualSnapshotPoint(nextLine, indentation.Length)); - transaction?.Complete(); - return true; - } + transaction?.Complete(); + return true; } } diff --git a/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler_TypeChar.cs b/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler_TypeChar.cs index 27b6a9e986b2f..f652c81fd8d2b 100644 --- a/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler_TypeChar.cs +++ b/src/EditorFeatures/CSharp/RawStringLiteral/RawStringLiteralCommandHandler_TypeChar.cs @@ -16,223 +16,222 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.RawStringLiteral +namespace Microsoft.CodeAnalysis.Editor.CSharp.RawStringLiteral; + +internal partial class RawStringLiteralCommandHandler : IChainedCommandHandler { - internal partial class RawStringLiteralCommandHandler : IChainedCommandHandler + public CommandState GetCommandState(TypeCharCommandArgs args, Func nextCommandHandler) + => nextCommandHandler(); + + public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext context) { - public CommandState GetCommandState(TypeCharCommandArgs args, Func nextCommandHandler) - => nextCommandHandler(); + if (!ExecuteCommandWorker(args, nextCommandHandler)) + nextCommandHandler(); + } - public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext context) - { - if (!ExecuteCommandWorker(args, nextCommandHandler)) - nextCommandHandler(); - } + private bool ExecuteCommandWorker(TypeCharCommandArgs args, Action nextCommandHandler) + { + if (args.TypedChar != '"') + return false; - private bool ExecuteCommandWorker(TypeCharCommandArgs args, Action nextCommandHandler) - { - if (args.TypedChar != '"') - return false; + var textView = args.TextView; + var subjectBuffer = args.SubjectBuffer; + var spans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); - var textView = args.TextView; - var subjectBuffer = args.SubjectBuffer; - var spans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); + if (spans.Count != 1) + return false; - if (spans.Count != 1) - return false; + var span = spans.First(); + if (span.Length != 0) + return false; - var span = spans.First(); - if (span.Length != 0) - return false; + var caret = textView.GetCaretPoint(subjectBuffer); + if (caret == null) + return false; - var caret = textView.GetCaretPoint(subjectBuffer); - if (caret == null) - return false; + var cancellationToken = CancellationToken.None; + var textChangeOpt = + TryGenerateInitialEmptyRawString(caret.Value, cancellationToken) ?? + TryGrowInitialEmptyRawString(caret.Value, cancellationToken) ?? + TryGrowRawStringDelimeters(caret.Value, cancellationToken); - var cancellationToken = CancellationToken.None; - var textChangeOpt = - TryGenerateInitialEmptyRawString(caret.Value, cancellationToken) ?? - TryGrowInitialEmptyRawString(caret.Value, cancellationToken) ?? - TryGrowRawStringDelimeters(caret.Value, cancellationToken); + if (textChangeOpt is not TextChange textChange) + return false; - if (textChangeOpt is not TextChange textChange) - return false; + // Looks good. First, let the quote get added by the normal type char handlers. Then make our text change. + // We do this in two steps so that undo can work properly. + nextCommandHandler(); - // Looks good. First, let the quote get added by the normal type char handlers. Then make our text change. - // We do this in two steps so that undo can work properly. - nextCommandHandler(); + using var transaction = CaretPreservingEditTransaction.TryCreate( + CSharpEditorResources.Grow_raw_string, textView, _undoHistoryRegistry, _editorOperationsFactoryService); - using var transaction = CaretPreservingEditTransaction.TryCreate( - CSharpEditorResources.Grow_raw_string, textView, _undoHistoryRegistry, _editorOperationsFactoryService); + var edit = subjectBuffer.CreateEdit(); + edit.Insert(textChange.Span.Start, textChange.NewText); + edit.Apply(); - var edit = subjectBuffer.CreateEdit(); - edit.Insert(textChange.Span.Start, textChange.NewText); - edit.Apply(); + // ensure the caret is placed after where the original quote got added. + textView.Caret.MoveTo(new SnapshotPoint(subjectBuffer.CurrentSnapshot, caret.Value.Position + 1)); - // ensure the caret is placed after where the original quote got added. - textView.Caret.MoveTo(new SnapshotPoint(subjectBuffer.CurrentSnapshot, caret.Value.Position + 1)); + transaction?.Complete(); + return true; + } - transaction?.Complete(); - return true; - } + /// + /// When typing " given a normal string like ""$$, then update the text to be """$$""". + /// Note that this puts the user in the position where TryGrowInitialEmptyRawString can now take effect. + /// + private static TextChange? TryGenerateInitialEmptyRawString( + SnapshotPoint caret, + CancellationToken cancellationToken) + { + var snapshot = caret.Snapshot; + var position = caret.Position; - /// - /// When typing " given a normal string like ""$$, then update the text to be """$$""". - /// Note that this puts the user in the position where TryGrowInitialEmptyRawString can now take effect. - /// - private static TextChange? TryGenerateInitialEmptyRawString( - SnapshotPoint caret, - CancellationToken cancellationToken) - { - var snapshot = caret.Snapshot; - var position = caret.Position; + // if we have ""$$" then typing `"` here should not be handled by this path but by TryGrowInitialEmptyRawString + if (position + 1 < snapshot.Length && snapshot[position + 1] == '"') + return null; - // if we have ""$$" then typing `"` here should not be handled by this path but by TryGrowInitialEmptyRawString - if (position + 1 < snapshot.Length && snapshot[position + 1] == '"') - return null; + var start = position; + while (start - 1 >= 0 && snapshot[start - 1] == '"') + start--; - var start = position; - while (start - 1 >= 0 && snapshot[start - 1] == '"') - start--; + // must have exactly `""` + if (position - start != 2) + return null; - // must have exactly `""` - if (position - start != 2) - return null; + while (start - 1 >= 0 && snapshot[start - 1] == '$') + start--; - while (start - 1 >= 0 && snapshot[start - 1] == '$') - start--; + // hitting `"` after `@""` shouldn't do anything + if (start - 1 >= 0 && snapshot[start - 1] == '@') + return null; - // hitting `"` after `@""` shouldn't do anything - if (start - 1 >= 0 && snapshot[start - 1] == '@') - return null; + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return null; - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return null; + var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); + var token = root.FindToken(start); + if (token.SpanStart != start) + return null; - var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); - var token = root.FindToken(start); - if (token.SpanStart != start) - return null; + if (token.Kind() is not (SyntaxKind.StringLiteralToken or SyntaxKind.InterpolatedStringStartToken or SyntaxKind.InterpolatedSingleLineRawStringStartToken)) + return null; - if (token.Kind() is not (SyntaxKind.StringLiteralToken or SyntaxKind.InterpolatedStringStartToken or SyntaxKind.InterpolatedSingleLineRawStringStartToken)) - return null; + return new TextChange(new TextSpan(position + 1, 0), "\"\"\""); + } - return new TextChange(new TextSpan(position + 1, 0), "\"\"\""); + /// + /// When typing " given a raw string like """$$""" (or a similar multiline form), then update the + /// text to be: """"$$"""". i.e. grow both the start and end delimiters to keep the string properly + /// balanced. This differs from TryGrowRawStringDelimeters in that the language will consider that initial + /// """""" text to be a single delimeter, while we want to treat it as two. + /// + private static TextChange? TryGrowInitialEmptyRawString( + SnapshotPoint caret, + CancellationToken cancellationToken) + { + var snapshot = caret.Snapshot; + var position = caret.Position; + + var start = position; + while (start - 1 >= 0 && snapshot[start - 1] == '"') + start--; + + var end = position; + while (end < snapshot.Length && snapshot[end] == '"') + end++; + + // Have to have an even number of quotes. + var quoteLength = end - start; + if (quoteLength % 2 == 1) + return null; + + // User position must be halfway through the quotes. + if (position != (start + quoteLength / 2)) + return null; + + // have to at least have `"""$$"""` + if (quoteLength < 6) + return null; + + while (start - 1 >= 0 && snapshot[start - 1] == '$') + start--; + + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return null; + + var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); + var token = root.FindToken(start); + if (token.SpanStart != start) + return null; + + if (token.Kind() is not (SyntaxKind.SingleLineRawStringLiteralToken or + SyntaxKind.MultiLineRawStringLiteralToken or + SyntaxKind.InterpolatedSingleLineRawStringStartToken or + SyntaxKind.InterpolatedMultiLineRawStringStartToken)) + { + return null; } - /// - /// When typing " given a raw string like """$$""" (or a similar multiline form), then update the - /// text to be: """"$$"""". i.e. grow both the start and end delimiters to keep the string properly - /// balanced. This differs from TryGrowRawStringDelimeters in that the language will consider that initial - /// """""" text to be a single delimeter, while we want to treat it as two. - /// - private static TextChange? TryGrowInitialEmptyRawString( - SnapshotPoint caret, - CancellationToken cancellationToken) - { - var snapshot = caret.Snapshot; - var position = caret.Position; + return new TextChange(new TextSpan(position + 1, 0), "\""); + } - var start = position; - while (start - 1 >= 0 && snapshot[start - 1] == '"') - start--; + /// + /// When typing " given a raw string like """$$ goo bar """ (or a similar multiline form), then + /// update the text to be: """" goo bar """". i.e. grow both the start and end delimiters to keep the + /// string properly balanced. + /// + private static TextChange? TryGrowRawStringDelimeters( + SnapshotPoint caret, + CancellationToken cancellationToken) + { + var snapshot = caret.Snapshot; + var position = caret.Position; - var end = position; - while (end < snapshot.Length && snapshot[end] == '"') - end++; + // if we have """$$" then typing `"` here should not grow the start/end quotes. we only want to grow them + // if the user is at the end of the start delimeter. + if (position < snapshot.Length && snapshot[position] == '"') + return null; - // Have to have an even number of quotes. - var quoteLength = end - start; - if (quoteLength % 2 == 1) - return null; + var start = position; + while (start - 1 >= 0 && snapshot[start - 1] == '"') + start--; - // User position must be halfway through the quotes. - if (position != (start + quoteLength / 2)) - return null; + // must have at least three quotes for this to be a raw string + var quoteCount = position - start; + if (quoteCount < 3) + return null; - // have to at least have `"""$$"""` - if (quoteLength < 6) - return null; + while (start - 1 >= 0 && snapshot[start - 1] == '$') + start--; - while (start - 1 >= 0 && snapshot[start - 1] == '$') - start--; + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return null; - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return null; + var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); + var token = root.FindToken(start); + if (token.SpanStart != start) + return null; - var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); - var token = root.FindToken(start); - if (token.SpanStart != start) - return null; + if (token.Span.Length < (2 * quoteCount)) + return null; - if (token.Kind() is not (SyntaxKind.SingleLineRawStringLiteralToken or - SyntaxKind.MultiLineRawStringLiteralToken or - SyntaxKind.InterpolatedSingleLineRawStringStartToken or - SyntaxKind.InterpolatedMultiLineRawStringStartToken)) - { + if (token.Kind() is SyntaxKind.InterpolatedSingleLineRawStringStartToken or SyntaxKind.InterpolatedMultiLineRawStringStartToken) + { + var interpolatedString = (InterpolatedStringExpressionSyntax)token.GetRequiredParent(); + var endToken = interpolatedString.StringEndToken; + if (!endToken.Text.EndsWith(new string('"', quoteCount))) return null; - } - - return new TextChange(new TextSpan(position + 1, 0), "\""); } - - /// - /// When typing " given a raw string like """$$ goo bar """ (or a similar multiline form), then - /// update the text to be: """" goo bar """". i.e. grow both the start and end delimiters to keep the - /// string properly balanced. - /// - private static TextChange? TryGrowRawStringDelimeters( - SnapshotPoint caret, - CancellationToken cancellationToken) + else if (token.Kind() is SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.MultiLineRawStringLiteralToken) { - var snapshot = caret.Snapshot; - var position = caret.Position; - - // if we have """$$" then typing `"` here should not grow the start/end quotes. we only want to grow them - // if the user is at the end of the start delimeter. - if (position < snapshot.Length && snapshot[position] == '"') + if (!token.Text.EndsWith(new string('"', quoteCount))) return null; - - var start = position; - while (start - 1 >= 0 && snapshot[start - 1] == '"') - start--; - - // must have at least three quotes for this to be a raw string - var quoteCount = position - start; - if (quoteCount < 3) - return null; - - while (start - 1 >= 0 && snapshot[start - 1] == '$') - start--; - - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return null; - - var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); - var token = root.FindToken(start); - if (token.SpanStart != start) - return null; - - if (token.Span.Length < (2 * quoteCount)) - return null; - - if (token.Kind() is SyntaxKind.InterpolatedSingleLineRawStringStartToken or SyntaxKind.InterpolatedMultiLineRawStringStartToken) - { - var interpolatedString = (InterpolatedStringExpressionSyntax)token.GetRequiredParent(); - var endToken = interpolatedString.StringEndToken; - if (!endToken.Text.EndsWith(new string('"', quoteCount))) - return null; - } - else if (token.Kind() is SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.MultiLineRawStringLiteralToken) - { - if (!token.Text.EndsWith(new string('"', quoteCount))) - return null; - } - - return new TextChange(new TextSpan(token.GetRequiredParent().Span.End, 0), "\""); } + + return new TextChange(new TextSpan(token.GetRequiredParent().Span.End, 0), "\""); } } diff --git a/src/EditorFeatures/CSharp/RenameTracking/CSharpRenameTrackingLanguageHeuristicsService.cs b/src/EditorFeatures/CSharp/RenameTracking/CSharpRenameTrackingLanguageHeuristicsService.cs index 2a68f4d712439..6fe8cb8c0188f 100644 --- a/src/EditorFeatures/CSharp/RenameTracking/CSharpRenameTrackingLanguageHeuristicsService.cs +++ b/src/EditorFeatures/CSharp/RenameTracking/CSharpRenameTrackingLanguageHeuristicsService.cs @@ -7,18 +7,17 @@ using Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.Editor.CSharp.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.CSharp.RenameTracking; + +[ExportLanguageService(typeof(IRenameTrackingLanguageHeuristicsService), LanguageNames.CSharp), Shared] +internal sealed class CSharpRenameTrackingLanguageHeuristicsService : IRenameTrackingLanguageHeuristicsService { - [ExportLanguageService(typeof(IRenameTrackingLanguageHeuristicsService), LanguageNames.CSharp), Shared] - internal sealed class CSharpRenameTrackingLanguageHeuristicsService : IRenameTrackingLanguageHeuristicsService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpRenameTrackingLanguageHeuristicsService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpRenameTrackingLanguageHeuristicsService() - { - } - - public bool IsIdentifierValidForRenameTracking(string name) - => name is not "var" and not "dynamic" and not "_"; } + + public bool IsIdentifierValidForRenameTracking(string name) + => name is not "var" and not "dynamic" and not "_"; } diff --git a/src/EditorFeatures/CSharp/SplitComment/CSharpSplitCommentService.cs b/src/EditorFeatures/CSharp/SplitComment/CSharpSplitCommentService.cs index 0a0bf5099e643..53b6a72611b31 100644 --- a/src/EditorFeatures/CSharp/SplitComment/CSharpSplitCommentService.cs +++ b/src/EditorFeatures/CSharp/SplitComment/CSharpSplitCommentService.cs @@ -7,20 +7,19 @@ using Microsoft.CodeAnalysis.Editor.Implementation.SplitComment; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.Editor.CSharp.SplitComment +namespace Microsoft.CodeAnalysis.Editor.CSharp.SplitComment; + +[ExportLanguageService(typeof(ISplitCommentService), LanguageNames.CSharp), Shared] +internal class CSharpSplitCommentService : ISplitCommentService { - [ExportLanguageService(typeof(ISplitCommentService), LanguageNames.CSharp), Shared] - internal class CSharpSplitCommentService : ISplitCommentService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpSplitCommentService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpSplitCommentService() - { - } + } - public string CommentStart => "//"; + public string CommentStart => "//"; - public bool IsAllowed(SyntaxNode root, SyntaxTrivia trivia) - => true; - } + public bool IsAllowed(SyntaxNode root, SyntaxTrivia trivia) + => true; } diff --git a/src/EditorFeatures/CSharp/SplitStringLiteral/SplitStringLiteralCommandHandler.cs b/src/EditorFeatures/CSharp/SplitStringLiteral/SplitStringLiteralCommandHandler.cs index 9fca0e014c5b5..aa0819766aa6e 100644 --- a/src/EditorFeatures/CSharp/SplitStringLiteral/SplitStringLiteralCommandHandler.cs +++ b/src/EditorFeatures/CSharp/SplitStringLiteral/SplitStringLiteralCommandHandler.cs @@ -20,119 +20,118 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.SplitStringLiteral +namespace Microsoft.CodeAnalysis.Editor.CSharp.SplitStringLiteral; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(nameof(SplitStringLiteralCommandHandler))] +[Order(After = PredefinedCompletionNames.CompletionCommandHandler)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal partial class SplitStringLiteralCommandHandler( + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService) : ICommandHandler { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(nameof(SplitStringLiteralCommandHandler))] - [Order(After = PredefinedCompletionNames.CompletionCommandHandler)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal partial class SplitStringLiteralCommandHandler( - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService) : ICommandHandler + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + + public string DisplayName => CSharpEditorResources.Split_string; + + public CommandState GetCommandState(ReturnKeyCommandArgs args) + => CommandState.Unspecified; + + public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) + => ExecuteCommandWorker(args, context.OperationContext.UserCancellationToken); + + public bool ExecuteCommandWorker(ReturnKeyCommandArgs args, CancellationToken cancellationToken) { - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + if (!_editorOptionsService.GlobalOptions.GetOption(SplitStringLiteralOptionsStorage.Enabled)) + { + return false; + } - public string DisplayName => CSharpEditorResources.Split_string; + var textView = args.TextView; + var subjectBuffer = args.SubjectBuffer; + var spans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); - public CommandState GetCommandState(ReturnKeyCommandArgs args) - => CommandState.Unspecified; + // Don't split strings if there is any actual selection. + // We must check all spans to account for multi-carets. + if (spans.IsEmpty() || !spans.All(s => s.IsEmpty)) + return false; - public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) - => ExecuteCommandWorker(args, context.OperationContext.UserCancellationToken); + var caret = textView.GetCaretPoint(subjectBuffer); + if (caret == null) + return false; - public bool ExecuteCommandWorker(ReturnKeyCommandArgs args, CancellationToken cancellationToken) + // First, we need to verify that we are only working with string literals. + // Otherwise, let the editor handle all carets. + foreach (var span in spans) { - if (!_editorOptionsService.GlobalOptions.GetOption(SplitStringLiteralOptionsStorage.Enabled)) - { + var spanStart = span.Start; + var line = subjectBuffer.CurrentSnapshot.GetLineFromPosition(span.Start); + if (!LineContainsQuote(line, span.Start)) return false; - } + } - var textView = args.TextView; - var subjectBuffer = args.SubjectBuffer; - var spans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return false; - // Don't split strings if there is any actual selection. - // We must check all spans to account for multi-carets. - if (spans.IsEmpty() || !spans.All(s => s.IsEmpty)) - return false; + var parsedDocument = ParsedDocument.CreateSynchronously(document, CancellationToken.None); + var indentationOptions = subjectBuffer.GetIndentationOptions(_editorOptionsService, parsedDocument.LanguageServices, explicitFormat: false); - var caret = textView.GetCaretPoint(subjectBuffer); - if (caret == null) - return false; + // We now go through the verified string literals and split each of them. + // The list of spans is traversed in reverse order so we do not have to + // deal with updating later caret positions to account for the added space + // from splitting at earlier caret positions. + foreach (var span in spans.Reverse()) + { + using var transaction = CaretPreservingEditTransaction.TryCreate( + CSharpEditorResources.Split_string, textView, _undoHistoryRegistry, _editorOperationsFactoryService); - // First, we need to verify that we are only working with string literals. - // Otherwise, let the editor handle all carets. - foreach (var span in spans) + var splitter = StringSplitter.TryCreate(parsedDocument, span.Start.Position, indentationOptions, cancellationToken); + if (splitter is null || + !splitter.TrySplit(out var newRoot, out var newPosition)) { - var spanStart = span.Start; - var line = subjectBuffer.CurrentSnapshot.GetLineFromPosition(span.Start); - if (!LineContainsQuote(line, span.Start)) - return false; - } - - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) return false; + } - var parsedDocument = ParsedDocument.CreateSynchronously(document, CancellationToken.None); - var indentationOptions = subjectBuffer.GetIndentationOptions(_editorOptionsService, parsedDocument.LanguageServices, explicitFormat: false); + // apply the change: + var newDocument = parsedDocument.WithChangedRoot(newRoot, cancellationToken); + var newSnapshot = subjectBuffer.ApplyChanges(newDocument.GetChanges(parsedDocument)); + parsedDocument = newDocument; - // We now go through the verified string literals and split each of them. - // The list of spans is traversed in reverse order so we do not have to - // deal with updating later caret positions to account for the added space - // from splitting at earlier caret positions. - foreach (var span in spans.Reverse()) + // The buffer edit may have adjusted to position of the current caret but we might need a different location. + // Only adjust caret if it is the only one (no multi-caret support: https://github.com/dotnet/roslyn/issues/64812). + if (spans.Count == 1) { - using var transaction = CaretPreservingEditTransaction.TryCreate( - CSharpEditorResources.Split_string, textView, _undoHistoryRegistry, _editorOperationsFactoryService); - - var splitter = StringSplitter.TryCreate(parsedDocument, span.Start.Position, indentationOptions, cancellationToken); - if (splitter is null || - !splitter.TrySplit(out var newRoot, out var newPosition)) - { - return false; - } - - // apply the change: - var newDocument = parsedDocument.WithChangedRoot(newRoot, cancellationToken); - var newSnapshot = subjectBuffer.ApplyChanges(newDocument.GetChanges(parsedDocument)); - parsedDocument = newDocument; - - // The buffer edit may have adjusted to position of the current caret but we might need a different location. - // Only adjust caret if it is the only one (no multi-caret support: https://github.com/dotnet/roslyn/issues/64812). - if (spans.Count == 1) - { - var newCaretPoint = textView.BufferGraph.MapUpToBuffer( - new SnapshotPoint(newSnapshot, newPosition), - PointTrackingMode.Negative, - PositionAffinity.Predecessor, - textView.TextBuffer); - - if (newCaretPoint != null) - textView.Caret.MoveTo(newCaretPoint.Value); - } - - transaction?.Complete(); + var newCaretPoint = textView.BufferGraph.MapUpToBuffer( + new SnapshotPoint(newSnapshot, newPosition), + PointTrackingMode.Negative, + PositionAffinity.Predecessor, + textView.TextBuffer); + + if (newCaretPoint != null) + textView.Caret.MoveTo(newCaretPoint.Value); } - return true; + transaction?.Complete(); + } + + return true; - static bool LineContainsQuote(ITextSnapshotLine line, int caretPosition) + static bool LineContainsQuote(ITextSnapshotLine line, int caretPosition) + { + var snapshot = line.Snapshot; + for (int i = line.Start; i < caretPosition; i++) { - var snapshot = line.Snapshot; - for (int i = line.Start; i < caretPosition; i++) - { - if (snapshot[i] == '"') - return true; - } - - return false; + if (snapshot[i] == '"') + return true; } + + return false; } } } diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/AbstractPasteProcessor.cs b/src/EditorFeatures/CSharp/StringCopyPaste/AbstractPasteProcessor.cs index 2f1db0deaa883..00a57052a54ab 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/AbstractPasteProcessor.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/AbstractPasteProcessor.cs @@ -15,161 +15,160 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste +namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste; + +using static StringCopyPasteHelpers; + +/// +/// Holds core before/after state related to the paste to allow subclasses to decide what text changes to make +/// without having to pass around tons of common values. +/// +internal abstract class AbstractPasteProcessor { - using static StringCopyPasteHelpers; + /// + /// The buffer's snapshot prior to the paste application. + /// + protected readonly ITextSnapshot SnapshotBeforePaste; + + /// + /// The buffer's snapshot right after the paste application. Guaranteed to be exactly one version ahead of . + /// + protected readonly ITextSnapshot SnapshotAfterPaste; + + /// + /// Roslyn SourceText corresponding to . + /// + protected readonly SourceText TextBeforePaste; + + /// + /// Roslyn SourceText corresponding to . + /// + protected readonly SourceText TextAfterPaste; + + /// + /// Roslyn document corresponding to . + /// + protected readonly Document DocumentBeforePaste; + + /// + /// Roslyn document corresponding to . + /// + protected readonly Document DocumentAfterPaste; + + /// + /// The or that the + /// changes were pasted into. All changes in the paste will be in the same 'content text span' in that string + /// expression. + /// + protected readonly ExpressionSyntax StringExpressionBeforePaste; + + /// + /// Information about the relevant pieces of (like where its + /// delimiters are). + /// + protected readonly StringInfo StringExpressionBeforePasteInfo; + + /// + /// All the spans of 's + /// mapped forward () to in an inclusive + /// manner. This can be used to determine what content exists post paste, and if that content requires the + /// literal to revised to be legal. For example, if the text content in a raw-literal contains a longer + /// sequence of quotes after pasting, then the delimiters of the raw literal may need to be increased + /// accordingly. + /// + protected readonly ImmutableArray TextContentsSpansAfterPaste; + + /// + /// User's desired new-line sequence if we need to add newlines to our text changes. + /// + protected readonly string NewLine; + + /// + /// Amount to indent content in a multi-line raw string literal. + /// + protected readonly string IndentationWhitespace; + + /// + /// The set of 's that produced from . + /// + protected INormalizedTextChangeCollection Changes => SnapshotBeforePaste.Version.Changes; + + protected AbstractPasteProcessor( + string newLine, + string indentationWhitespace, + ITextSnapshot snapshotBeforePaste, + ITextSnapshot snapshotAfterPaste, + Document documentBeforePaste, + Document documentAfterPaste, + ExpressionSyntax stringExpressionBeforePaste) + { + NewLine = newLine; + IndentationWhitespace = indentationWhitespace; + + SnapshotBeforePaste = snapshotBeforePaste; + SnapshotAfterPaste = snapshotAfterPaste; + + TextBeforePaste = SnapshotBeforePaste.AsText(); + TextAfterPaste = SnapshotAfterPaste.AsText(); + + DocumentBeforePaste = documentBeforePaste; + DocumentAfterPaste = documentAfterPaste; + + StringExpressionBeforePaste = stringExpressionBeforePaste; + StringExpressionBeforePasteInfo = StringInfo.GetStringInfo(TextBeforePaste, stringExpressionBeforePaste); + TextContentsSpansAfterPaste = StringExpressionBeforePasteInfo.ContentSpans.SelectAsArray(MapSpanForward); + + Contract.ThrowIfTrue(StringExpressionBeforePasteInfo.ContentSpans.IsEmpty); + } + + /// + /// Determine the edits that should be made to smartly handle pasting hte data that is on the clipboard._selectionBeforePaste + /// + public abstract ImmutableArray GetEdits(); + + /// + /// Takes a span in and maps it appropriately (in an manner) to . + /// + protected TextSpan MapSpanForward(TextSpan span) + => MapSpan(span, SnapshotBeforePaste, SnapshotAfterPaste); /// - /// Holds core before/after state related to the paste to allow subclasses to decide what text changes to make - /// without having to pass around tons of common values. + /// Given an initial raw string literal, and the changes made to it by the paste, determines how many quotes to + /// add to the start and end to keep things parsing properly. /// - internal abstract class AbstractPasteProcessor + protected string? GetQuotesToAddToRawString( + SourceText textAfterChange, ImmutableArray textContentSpansAfterChange) { - /// - /// The buffer's snapshot prior to the paste application. - /// - protected readonly ITextSnapshot SnapshotBeforePaste; - - /// - /// The buffer's snapshot right after the paste application. Guaranteed to be exactly one version ahead of . - /// - protected readonly ITextSnapshot SnapshotAfterPaste; - - /// - /// Roslyn SourceText corresponding to . - /// - protected readonly SourceText TextBeforePaste; - - /// - /// Roslyn SourceText corresponding to . - /// - protected readonly SourceText TextAfterPaste; - - /// - /// Roslyn document corresponding to . - /// - protected readonly Document DocumentBeforePaste; - - /// - /// Roslyn document corresponding to . - /// - protected readonly Document DocumentAfterPaste; - - /// - /// The or that the - /// changes were pasted into. All changes in the paste will be in the same 'content text span' in that string - /// expression. - /// - protected readonly ExpressionSyntax StringExpressionBeforePaste; - - /// - /// Information about the relevant pieces of (like where its - /// delimiters are). - /// - protected readonly StringInfo StringExpressionBeforePasteInfo; - - /// - /// All the spans of 's - /// mapped forward () to in an inclusive - /// manner. This can be used to determine what content exists post paste, and if that content requires the - /// literal to revised to be legal. For example, if the text content in a raw-literal contains a longer - /// sequence of quotes after pasting, then the delimiters of the raw literal may need to be increased - /// accordingly. - /// - protected readonly ImmutableArray TextContentsSpansAfterPaste; - - /// - /// User's desired new-line sequence if we need to add newlines to our text changes. - /// - protected readonly string NewLine; - - /// - /// Amount to indent content in a multi-line raw string literal. - /// - protected readonly string IndentationWhitespace; - - /// - /// The set of 's that produced from . - /// - protected INormalizedTextChangeCollection Changes => SnapshotBeforePaste.Version.Changes; - - protected AbstractPasteProcessor( - string newLine, - string indentationWhitespace, - ITextSnapshot snapshotBeforePaste, - ITextSnapshot snapshotAfterPaste, - Document documentBeforePaste, - Document documentAfterPaste, - ExpressionSyntax stringExpressionBeforePaste) - { - NewLine = newLine; - IndentationWhitespace = indentationWhitespace; - - SnapshotBeforePaste = snapshotBeforePaste; - SnapshotAfterPaste = snapshotAfterPaste; - - TextBeforePaste = SnapshotBeforePaste.AsText(); - TextAfterPaste = SnapshotAfterPaste.AsText(); - - DocumentBeforePaste = documentBeforePaste; - DocumentAfterPaste = documentAfterPaste; - - StringExpressionBeforePaste = stringExpressionBeforePaste; - StringExpressionBeforePasteInfo = StringInfo.GetStringInfo(TextBeforePaste, stringExpressionBeforePaste); - TextContentsSpansAfterPaste = StringExpressionBeforePasteInfo.ContentSpans.SelectAsArray(MapSpanForward); - - Contract.ThrowIfTrue(StringExpressionBeforePasteInfo.ContentSpans.IsEmpty); - } - - /// - /// Determine the edits that should be made to smartly handle pasting hte data that is on the clipboard._selectionBeforePaste - /// - public abstract ImmutableArray GetEdits(); - - /// - /// Takes a span in and maps it appropriately (in an manner) to . - /// - protected TextSpan MapSpanForward(TextSpan span) - => MapSpan(span, SnapshotBeforePaste, SnapshotAfterPaste); - - /// - /// Given an initial raw string literal, and the changes made to it by the paste, determines how many quotes to - /// add to the start and end to keep things parsing properly. - /// - protected string? GetQuotesToAddToRawString( - SourceText textAfterChange, ImmutableArray textContentSpansAfterChange) - { - Contract.ThrowIfFalse(IsAnyRawStringExpression(StringExpressionBeforePaste)); - - var longestQuoteSequence = textContentSpansAfterChange.Max(ts => GetLongestQuoteSequence(textAfterChange, ts)); - - var quotesToAddCount = (longestQuoteSequence - StringExpressionBeforePasteInfo.DelimiterQuoteCount) + 1; - return quotesToAddCount <= 0 ? null : new string('"', quotesToAddCount); - } - - /// - /// Given an initial raw string literal, and the changes made to it by the paste, determines how many dollar - /// signs to add to the start to keep things parsing properly. - /// - protected string? GetDollarSignsToAddToRawString( - SourceText textAfterChange, ImmutableArray textContentSpansAfterChange) - { - Contract.ThrowIfFalse(IsAnyRawStringExpression(StringExpressionBeforePaste)); - - // Only have to do this for interpolated strings. Other strings never have a $ in their starting delimiter. - if (StringExpressionBeforePaste is not InterpolatedStringExpressionSyntax) - return null; - - var longestBraceSequence = textContentSpansAfterChange.Max( - ts => Math.Max( - GetLongestOpenBraceSequence(textAfterChange, ts), - GetLongestCloseBraceSequence(textAfterChange, ts))); - - var dollarsToAddCount = (longestBraceSequence - StringExpressionBeforePasteInfo.DelimiterDollarCount) + 1; - return dollarsToAddCount <= 0 ? null : new string('$', dollarsToAddCount); - } + Contract.ThrowIfFalse(IsAnyRawStringExpression(StringExpressionBeforePaste)); + + var longestQuoteSequence = textContentSpansAfterChange.Max(ts => GetLongestQuoteSequence(textAfterChange, ts)); + + var quotesToAddCount = (longestQuoteSequence - StringExpressionBeforePasteInfo.DelimiterQuoteCount) + 1; + return quotesToAddCount <= 0 ? null : new string('"', quotesToAddCount); + } + + /// + /// Given an initial raw string literal, and the changes made to it by the paste, determines how many dollar + /// signs to add to the start to keep things parsing properly. + /// + protected string? GetDollarSignsToAddToRawString( + SourceText textAfterChange, ImmutableArray textContentSpansAfterChange) + { + Contract.ThrowIfFalse(IsAnyRawStringExpression(StringExpressionBeforePaste)); + + // Only have to do this for interpolated strings. Other strings never have a $ in their starting delimiter. + if (StringExpressionBeforePaste is not InterpolatedStringExpressionSyntax) + return null; + + var longestBraceSequence = textContentSpansAfterChange.Max( + ts => Math.Max( + GetLongestOpenBraceSequence(textAfterChange, ts), + GetLongestCloseBraceSequence(textAfterChange, ts))); + + var dollarsToAddCount = (longestBraceSequence - StringExpressionBeforePasteInfo.DelimiterDollarCount) + 1; + return dollarsToAddCount <= 0 ? null : new string('$', dollarsToAddCount); } } diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/KnownSourcePasteProcessor.cs b/src/EditorFeatures/CSharp/StringCopyPaste/KnownSourcePasteProcessor.cs index 8959b20ad31af..27f9e58606bf2 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/KnownSourcePasteProcessor.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/KnownSourcePasteProcessor.cs @@ -17,338 +17,337 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste +namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste; + +using static StringCopyPasteHelpers; + +/// +/// Implementation of used when we know the original string literal expression +/// we were copying text out of. Because we know the original literal expression, we can determine what the +/// characters being pasted meant in the original context and we can attempt to preserve that as closely as +/// possible. +/// +internal class KnownSourcePasteProcessor( + string newLine, + string indentationWhitespace, + ITextSnapshot snapshotBeforePaste, + ITextSnapshot snapshotAfterPaste, + Document documentBeforePaste, + Document documentAfterPaste, + ExpressionSyntax stringExpressionBeforePaste, + TextSpan selectionSpanBeforePaste, + StringCopyPasteData copyPasteData, + ITextBufferFactoryService2 textBufferFactoryService) : AbstractPasteProcessor(newLine, indentationWhitespace, snapshotBeforePaste, snapshotAfterPaste, documentBeforePaste, documentAfterPaste, stringExpressionBeforePaste) { - using static StringCopyPasteHelpers; + /// + /// The selection in the document prior to the paste happening. + /// + private readonly TextSpan _selectionSpanBeforePaste = selectionSpanBeforePaste; /// - /// Implementation of used when we know the original string literal expression - /// we were copying text out of. Because we know the original literal expression, we can determine what the - /// characters being pasted meant in the original context and we can attempt to preserve that as closely as - /// possible. + /// Information stored to the clipboard about the original cut/copy. /// - internal class KnownSourcePasteProcessor( - string newLine, - string indentationWhitespace, - ITextSnapshot snapshotBeforePaste, - ITextSnapshot snapshotAfterPaste, - Document documentBeforePaste, - Document documentAfterPaste, - ExpressionSyntax stringExpressionBeforePaste, - TextSpan selectionSpanBeforePaste, - StringCopyPasteData copyPasteData, - ITextBufferFactoryService2 textBufferFactoryService) : AbstractPasteProcessor(newLine, indentationWhitespace, snapshotBeforePaste, snapshotAfterPaste, documentBeforePaste, documentAfterPaste, stringExpressionBeforePaste) - { - /// - /// The selection in the document prior to the paste happening. - /// - private readonly TextSpan _selectionSpanBeforePaste = selectionSpanBeforePaste; + private readonly StringCopyPasteData _copyPasteData = copyPasteData; - /// - /// Information stored to the clipboard about the original cut/copy. - /// - private readonly StringCopyPasteData _copyPasteData = copyPasteData; + private readonly ITextBufferFactoryService2 _textBufferFactoryService = textBufferFactoryService; - private readonly ITextBufferFactoryService2 _textBufferFactoryService = textBufferFactoryService; + public override ImmutableArray GetEdits() + { + // For pastes into non-raw strings, we can just determine how the change should be escaped in-line at that + // same location the paste originally happened at. For raw-strings things get more complex as we have to + // deal with things like indentation and potentially adding newlines to make things legal. + + // Smart Pasting into raw string not supported yet. + return IsAnyRawStringExpression(StringExpressionBeforePaste) + ? GetEditsForRawString() + : GetEditsForNonRawString(); + } - public override ImmutableArray GetEdits() - { - // For pastes into non-raw strings, we can just determine how the change should be escaped in-line at that - // same location the paste originally happened at. For raw-strings things get more complex as we have to - // deal with things like indentation and potentially adding newlines to make things legal. - - // Smart Pasting into raw string not supported yet. - return IsAnyRawStringExpression(StringExpressionBeforePaste) - ? GetEditsForRawString() - : GetEditsForNonRawString(); - } + private string EscapeForNonRawStringLiteral(string value) + => EscapeForNonRawStringLiteral_DoNotCallDirectly( + IsVerbatimStringExpression(StringExpressionBeforePaste), + StringExpressionBeforePaste is InterpolatedStringExpressionSyntax, + // We do not want to try skipping escapes in the 'value'. We know exactly what 'value' means and don't + // want it touched. + trySkipExistingEscapes: false, + value); - private string EscapeForNonRawStringLiteral(string value) - => EscapeForNonRawStringLiteral_DoNotCallDirectly( - IsVerbatimStringExpression(StringExpressionBeforePaste), - StringExpressionBeforePaste is InterpolatedStringExpressionSyntax, - // We do not want to try skipping escapes in the 'value'. We know exactly what 'value' means and don't - // want it touched. - trySkipExistingEscapes: false, - value); + private ImmutableArray GetEditsForNonRawString() + { + using var _ = PooledStringBuilder.GetInstance(out var builder); - private ImmutableArray GetEditsForNonRawString() + var isLiteral = StringExpressionBeforePaste is LiteralExpressionSyntax; + foreach (var content in _copyPasteData.Contents) { - using var _ = PooledStringBuilder.GetInstance(out var builder); - - var isLiteral = StringExpressionBeforePaste is LiteralExpressionSyntax; - foreach (var content in _copyPasteData.Contents) + if (content.IsText) + { + builder.Append(EscapeForNonRawStringLiteral(content.TextValue)); + } + else if (content.IsInterpolation) { - if (content.IsText) + builder.Append('{'); + + if (isLiteral) { - builder.Append(EscapeForNonRawStringLiteral(content.TextValue)); + // we're copying an interpolation from an interpolated string to a string literal. For example, + // we're pasting `{x + y}` into the middle of `"goobar"`. One thing we could potentially do in + // the future is split the literal into `"goo" + $"{x + y}" + "bar"`, or just making the + // containing literal into an interpolation itself. However, for now, we do the simple thing + // and just treat the interpolation as raw text that should just be escaped as appropriate into + // the destination. + builder.Append(EscapeForNonRawStringLiteral(content.InterpolationExpression)); } - else if (content.IsInterpolation) + else { - builder.Append('{'); - - if (isLiteral) - { - // we're copying an interpolation from an interpolated string to a string literal. For example, - // we're pasting `{x + y}` into the middle of `"goobar"`. One thing we could potentially do in - // the future is split the literal into `"goo" + $"{x + y}" + "bar"`, or just making the - // containing literal into an interpolation itself. However, for now, we do the simple thing - // and just treat the interpolation as raw text that should just be escaped as appropriate into - // the destination. - builder.Append(EscapeForNonRawStringLiteral(content.InterpolationExpression)); - } - else - { - // we're moving an interpolation from one interpolation to another. This can just be copied - // wholesale *except* for the format literal portion (e.g. `{...:XXXX}` which may have to be - // updated for the destination type. - builder.Append(content.InterpolationExpression); - } - - builder.Append(content.InterpolationAlignmentClause); - if (content.InterpolationFormatClause != null) - { - builder.Append(':'); - builder.Append(EscapeForNonRawStringLiteral(content.InterpolationFormatClause)); - } - - builder.Append('}'); + // we're moving an interpolation from one interpolation to another. This can just be copied + // wholesale *except* for the format literal portion (e.g. `{...:XXXX}` which may have to be + // updated for the destination type. + builder.Append(content.InterpolationExpression); } - else + + builder.Append(content.InterpolationAlignmentClause); + if (content.InterpolationFormatClause != null) { - throw ExceptionUtilities.UnexpectedValue(content.Kind); + builder.Append(':'); + builder.Append(EscapeForNonRawStringLiteral(content.InterpolationFormatClause)); } - } - return [new TextChange(_selectionSpanBeforePaste, builder.ToString())]; + builder.Append('}'); + } + else + { + throw ExceptionUtilities.UnexpectedValue(content.Kind); + } } - private ImmutableArray GetEditsForRawString() - { - // To make a change to a raw string we have to go through several passes to determine what to do. - // - // First, just take the copied text and determine the most basic edit that would insert it into the - // destination string. Importantly, do not insert interpolations in this step (instead just replace them - // with a dummy character). - // - // Second, after this text is inserted, look at the content regions of the string after the paste and look - // at the sequences of `"` and `{` in them to see if we need to update the delimiters of the raw string. - // Note: this is why it is critical that any interpolations are not inserted. We don't want the content of - // the interpolation to affect the delimiters. e.g. a interpolation containing `""""` *inside* of it - // doesn't require updating the delimiters of the outer raw-string expression. - // - // Also, after the text is inserted, look to see if we need to convert a single-line raw expression to - // multi-line. - // - // At this point, we will now have the information necessary to actually insert the content and do things - // like give interpolations the proper number of braces for the final string we're making. - - var (quotesToAdd, dollarSignsToAdd, convertToMultiLine) = DetermineTopLevelChangesToMakeToRawString(); - - return DetermineTotalEditsToMakeToRawString(quotesToAdd, dollarSignsToAdd, convertToMultiLine); - } + return [new TextChange(_selectionSpanBeforePaste, builder.ToString())]; + } - private (string? quotesToAdd, string? dollarSignsToAdd, bool convertToMultiLine) DetermineTopLevelChangesToMakeToRawString() - { - PerformInitialBasicPasteInRawString(out var textAfterBasicPaste, out var contentSpansAfterBasicPaste); + private ImmutableArray GetEditsForRawString() + { + // To make a change to a raw string we have to go through several passes to determine what to do. + // + // First, just take the copied text and determine the most basic edit that would insert it into the + // destination string. Importantly, do not insert interpolations in this step (instead just replace them + // with a dummy character). + // + // Second, after this text is inserted, look at the content regions of the string after the paste and look + // at the sequences of `"` and `{` in them to see if we need to update the delimiters of the raw string. + // Note: this is why it is critical that any interpolations are not inserted. We don't want the content of + // the interpolation to affect the delimiters. e.g. a interpolation containing `""""` *inside* of it + // doesn't require updating the delimiters of the outer raw-string expression. + // + // Also, after the text is inserted, look to see if we need to convert a single-line raw expression to + // multi-line. + // + // At this point, we will now have the information necessary to actually insert the content and do things + // like give interpolations the proper number of braces for the final string we're making. + + var (quotesToAdd, dollarSignsToAdd, convertToMultiLine) = DetermineTopLevelChangesToMakeToRawString(); + + return DetermineTotalEditsToMakeToRawString(quotesToAdd, dollarSignsToAdd, convertToMultiLine); + } - var convertToMultiLine = !IsAnyMultiLineRawStringExpression(StringExpressionBeforePaste) && RawContentMustBeMultiLine(textAfterBasicPaste, contentSpansAfterBasicPaste); - return (GetQuotesToAddToRawString(textAfterBasicPaste, contentSpansAfterBasicPaste), - GetDollarSignsToAddToRawString(textAfterBasicPaste, contentSpansAfterBasicPaste), - convertToMultiLine); - } + private (string? quotesToAdd, string? dollarSignsToAdd, bool convertToMultiLine) DetermineTopLevelChangesToMakeToRawString() + { + PerformInitialBasicPasteInRawString(out var textAfterBasicPaste, out var contentSpansAfterBasicPaste); - private void PerformInitialBasicPasteInRawString( - out SourceText textAfterBasicPaste, out ImmutableArray contentSpansAfterBasicPaste) - { - var trivialContentEdit = GetContentEditForRawString(insertInterpolations: false, dollarSignCount: -1); + var convertToMultiLine = !IsAnyMultiLineRawStringExpression(StringExpressionBeforePaste) && RawContentMustBeMultiLine(textAfterBasicPaste, contentSpansAfterBasicPaste); + return (GetQuotesToAddToRawString(textAfterBasicPaste, contentSpansAfterBasicPaste), + GetDollarSignsToAddToRawString(textAfterBasicPaste, contentSpansAfterBasicPaste), + convertToMultiLine); + } - // We want to map spans forward (which requires tracking spans), but we don't want to modify the original - // text buffer. So clone the text buffer to a new one where we can then make the change without touching - // the original. - var clonedBuffer = _textBufferFactoryService.CreateTextBuffer( - new SnapshotSpan(SnapshotBeforePaste, 0, SnapshotBeforePaste.Length), SnapshotBeforePaste.ContentType); - var snapshotBeforeTrivialEdit = clonedBuffer.CurrentSnapshot; + private void PerformInitialBasicPasteInRawString( + out SourceText textAfterBasicPaste, out ImmutableArray contentSpansAfterBasicPaste) + { + var trivialContentEdit = GetContentEditForRawString(insertInterpolations: false, dollarSignCount: -1); - var edit = clonedBuffer.CreateEdit(); - edit.Replace(_selectionSpanBeforePaste.ToSpan(), trivialContentEdit.NewText); + // We want to map spans forward (which requires tracking spans), but we don't want to modify the original + // text buffer. So clone the text buffer to a new one where we can then make the change without touching + // the original. + var clonedBuffer = _textBufferFactoryService.CreateTextBuffer( + new SnapshotSpan(SnapshotBeforePaste, 0, SnapshotBeforePaste.Length), SnapshotBeforePaste.ContentType); + var snapshotBeforeTrivialEdit = clonedBuffer.CurrentSnapshot; - var snapshotAfterTrivialEdit = edit.Apply(); + var edit = clonedBuffer.CreateEdit(); + edit.Replace(_selectionSpanBeforePaste.ToSpan(), trivialContentEdit.NewText); - textAfterBasicPaste = snapshotAfterTrivialEdit.AsText(); - contentSpansAfterBasicPaste = StringExpressionBeforePasteInfo.ContentSpans.SelectAsArray( - ts => MapSpan(ts, snapshotBeforeTrivialEdit, snapshotAfterTrivialEdit)); - } + var snapshotAfterTrivialEdit = edit.Apply(); - private ImmutableArray DetermineTotalEditsToMakeToRawString( - string? quotesToAdd, string? dollarSignsToAdd, bool convertToMultiLine) - { - var finalDollarSignCount = StringExpressionBeforePasteInfo.DelimiterDollarCount + - (dollarSignsToAdd == null ? 0 : dollarSignsToAdd.Length); + textAfterBasicPaste = snapshotAfterTrivialEdit.AsText(); + contentSpansAfterBasicPaste = StringExpressionBeforePasteInfo.ContentSpans.SelectAsArray( + ts => MapSpan(ts, snapshotBeforeTrivialEdit, snapshotAfterTrivialEdit)); + } - using var _ = ArrayBuilder.GetInstance(out var edits); + private ImmutableArray DetermineTotalEditsToMakeToRawString( + string? quotesToAdd, string? dollarSignsToAdd, bool convertToMultiLine) + { + var finalDollarSignCount = StringExpressionBeforePasteInfo.DelimiterDollarCount + + (dollarSignsToAdd == null ? 0 : dollarSignsToAdd.Length); - // First, add any extra dollar signs needed. - if (dollarSignsToAdd != null) - edits.Add(new TextChange(new TextSpan(StringExpressionBeforePaste.Span.Start, 0), dollarSignsToAdd)); + using var _ = ArrayBuilder.GetInstance(out var edits); - // Then any quotes to the start delimiter. - if (quotesToAdd != null) - edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.ContentSpans.First().Start, 0), quotesToAdd)); + // First, add any extra dollar signs needed. + if (dollarSignsToAdd != null) + edits.Add(new TextChange(new TextSpan(StringExpressionBeforePaste.Span.Start, 0), dollarSignsToAdd)); - // A newline and the indentation to start with. Note: adding the indentation here means that existing - // content will start at the right location, as will any content we are pasting in. - if (convertToMultiLine) - edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.StartDelimiterSpan.End, 0), NewLine + IndentationWhitespace)); + // Then any quotes to the start delimiter. + if (quotesToAdd != null) + edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.ContentSpans.First().Start, 0), quotesToAdd)); - // If we need to add braces to existing interpolations, do so now for the interpolations after the selection. - if (dollarSignsToAdd != null) - UpdateExistingInterpolationBraces(edits, beforeSelection: true, dollarSignsToAdd.Length); + // A newline and the indentation to start with. Note: adding the indentation here means that existing + // content will start at the right location, as will any content we are pasting in. + if (convertToMultiLine) + edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.StartDelimiterSpan.End, 0), NewLine + IndentationWhitespace)); - // Now determine the actual content to add again, this time properly emitting it with - // indentation/interpolations correctly. - edits.Add(GetContentEditForRawString(insertInterpolations: true, finalDollarSignCount)); + // If we need to add braces to existing interpolations, do so now for the interpolations after the selection. + if (dollarSignsToAdd != null) + UpdateExistingInterpolationBraces(edits, beforeSelection: true, dollarSignsToAdd.Length); - // If we need to add braces to existing interpolations, do so now for the interpolations before the selection. - if (dollarSignsToAdd != null) - UpdateExistingInterpolationBraces(edits, beforeSelection: false, dollarSignsToAdd.Length); + // Now determine the actual content to add again, this time properly emitting it with + // indentation/interpolations correctly. + edits.Add(GetContentEditForRawString(insertInterpolations: true, finalDollarSignCount)); - // A final new-line and indentation before the end delimiter. - if (convertToMultiLine) - edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpan.Start, 0), NewLine + IndentationWhitespace)); + // If we need to add braces to existing interpolations, do so now for the interpolations before the selection. + if (dollarSignsToAdd != null) + UpdateExistingInterpolationBraces(edits, beforeSelection: false, dollarSignsToAdd.Length); - // Then any extra quotes to the end delimiter. - if (quotesToAdd != null) - edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpanWithoutSuffix.End, 0), quotesToAdd)); + // A final new-line and indentation before the end delimiter. + if (convertToMultiLine) + edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpan.Start, 0), NewLine + IndentationWhitespace)); - return edits.ToImmutable(); - } + // Then any extra quotes to the end delimiter. + if (quotesToAdd != null) + edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpanWithoutSuffix.End, 0), quotesToAdd)); - private void UpdateExistingInterpolationBraces( - ArrayBuilder edits, bool beforeSelection, int dollarSignsToAdd) - { - var interpolatedStringExpression = (InterpolatedStringExpressionSyntax)StringExpressionBeforePaste; + return edits.ToImmutable(); + } - foreach (var content in interpolatedStringExpression.Contents) + private void UpdateExistingInterpolationBraces( + ArrayBuilder edits, bool beforeSelection, int dollarSignsToAdd) + { + var interpolatedStringExpression = (InterpolatedStringExpressionSyntax)StringExpressionBeforePaste; + + foreach (var content in interpolatedStringExpression.Contents) + { + if (content is InterpolationSyntax interpolation) { - if (content is InterpolationSyntax interpolation) - { - if (beforeSelection && interpolation.Span.End > _selectionSpanBeforePaste.Start) - continue; + if (beforeSelection && interpolation.Span.End > _selectionSpanBeforePaste.Start) + continue; - if (!beforeSelection && interpolation.Span.Start < _selectionSpanBeforePaste.End) - continue; + if (!beforeSelection && interpolation.Span.Start < _selectionSpanBeforePaste.End) + continue; - edits.Add(new TextChange(new TextSpan(interpolation.OpenBraceToken.Span.End, 0), new string('{', dollarSignsToAdd))); - edits.Add(new TextChange(new TextSpan(interpolation.CloseBraceToken.Span.Start, 0), new string('}', dollarSignsToAdd))); - } + edits.Add(new TextChange(new TextSpan(interpolation.OpenBraceToken.Span.End, 0), new string('{', dollarSignsToAdd))); + edits.Add(new TextChange(new TextSpan(interpolation.CloseBraceToken.Span.Start, 0), new string('}', dollarSignsToAdd))); } } + } - private TextChange GetContentEditForRawString( - bool insertInterpolations, int dollarSignCount) - { - dollarSignCount = Math.Max(1, dollarSignCount); - using var _ = PooledStringBuilder.GetInstance(out var builder); + private TextChange GetContentEditForRawString( + bool insertInterpolations, int dollarSignCount) + { + dollarSignCount = Math.Max(1, dollarSignCount); + using var _ = PooledStringBuilder.GetInstance(out var builder); - var isLiteral = StringExpressionBeforePaste is LiteralExpressionSyntax; - var isMultiLine = IsAnyMultiLineRawStringExpression(StringExpressionBeforePaste); + var isLiteral = StringExpressionBeforePaste is LiteralExpressionSyntax; + var isMultiLine = IsAnyMultiLineRawStringExpression(StringExpressionBeforePaste); - for (var contentIndex = 0; contentIndex < _copyPasteData.Contents.Length; contentIndex++) + for (var contentIndex = 0; contentIndex < _copyPasteData.Contents.Length; contentIndex++) + { + // Special handling for the first thing being pasted if we are pasting into a multi-line expression. + if (isMultiLine && contentIndex == 0) { - // Special handling for the first thing being pasted if we are pasting into a multi-line expression. - if (isMultiLine && contentIndex == 0) + TextBeforePaste.GetLineAndOffset(_selectionSpanBeforePaste.Start, out var line, out var offset); + if (line == TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.SpanStart).LineNumber) { - TextBeforePaste.GetLineAndOffset(_selectionSpanBeforePaste.Start, out var line, out var offset); - if (line == TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.SpanStart).LineNumber) - { - // the user selection starts on the line containing the leading delimiter. e.g. - // - // var v = """ [| - // content|] - // """ - // - // In this case, ensure we add a new-line + indentation so that the copied - // text will actually start in the right location. - builder.Append(NewLine); - builder.Append(IndentationWhitespace); - } - else if (offset < IndentationWhitespace.Length) - { - // if the line they're pasting into doesn't have enough indentation whitespace, then - // add enough whitespace to make the text insertion point level. e.g.: - // - // var v = """ - // [| content|] - // """ - builder.Append(IndentationWhitespace[offset..]); - } + // the user selection starts on the line containing the leading delimiter. e.g. + // + // var v = """ [| + // content|] + // """ + // + // In this case, ensure we add a new-line + indentation so that the copied + // text will actually start in the right location. + builder.Append(NewLine); + builder.Append(IndentationWhitespace); } - - var content = _copyPasteData.Contents[contentIndex]; - SourceText? lastContentSourceText = null; - if (content.IsText) + else if (offset < IndentationWhitespace.Length) { - // Convert the string to a source-text instance so we can easily process it one line at a time. - var sourceText = SourceText.From(content.TextValue); - lastContentSourceText = sourceText; + // if the line they're pasting into doesn't have enough indentation whitespace, then + // add enough whitespace to make the text insertion point level. e.g.: + // + // var v = """ + // [| content|] + // """ + builder.Append(IndentationWhitespace[offset..]); + } + } - for (var i = 0; i < sourceText.Lines.Count; i++) - { - if (i != 0) - builder.Append(IndentationWhitespace); + var content = _copyPasteData.Contents[contentIndex]; + SourceText? lastContentSourceText = null; + if (content.IsText) + { + // Convert the string to a source-text instance so we can easily process it one line at a time. + var sourceText = SourceText.From(content.TextValue); + lastContentSourceText = sourceText; - builder.Append(sourceText.ToString(sourceText.Lines[i].SpanIncludingLineBreak)); - } - } - else if (content.IsInterpolation) + for (var i = 0; i < sourceText.Lines.Count; i++) { - if (!insertInterpolations) - { - // Just insert a basic string that represents the interpolation, but doesn't actually insert any - // potential " or { characters that might screw up later computations. - builder.Append('0'); - } - else - { - builder.Append(new string('{', dollarSignCount)); - builder.Append(content.InterpolationExpression); - builder.Append(content.InterpolationAlignmentClause); - - if (content.InterpolationFormatClause != null) - { - builder.Append(':'); - builder.Append(content.InterpolationFormatClause); - } + if (i != 0) + builder.Append(IndentationWhitespace); - builder.Append(new string('}', dollarSignCount)); - } + builder.Append(sourceText.ToString(sourceText.Lines[i].SpanIncludingLineBreak)); } - else + } + else if (content.IsInterpolation) + { + if (!insertInterpolations) { - throw ExceptionUtilities.UnexpectedValue(content.Kind); + // Just insert a basic string that represents the interpolation, but doesn't actually insert any + // potential " or { characters that might screw up later computations. + builder.Append('0'); } - - if (isMultiLine && contentIndex == _copyPasteData.Contents.Length - 1) + else { - // Similar to the check we do for the first-change, if the last change was pasted into the space - // before the last `"""` then we need potentially insert a newline, then enough indentation - // whitespace to keep delimiter in the right location. - - TextBeforePaste.GetLineAndOffset(_selectionSpanBeforePaste.End, out var line, out var offset); + builder.Append(new string('{', dollarSignCount)); + builder.Append(content.InterpolationExpression); + builder.Append(content.InterpolationAlignmentClause); - if (line == TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.Span.End).LineNumber) + if (content.InterpolationFormatClause != null) { - var hasNewLine = content.IsText && HasNewLine(lastContentSourceText!.Lines.Last()); - if (!hasNewLine) - builder.Append(NewLine); - - builder.Append(TextBeforePaste.ToString(new TextSpan(TextBeforePaste.Lines[line].Start, offset))); + builder.Append(':'); + builder.Append(content.InterpolationFormatClause); } + + builder.Append(new string('}', dollarSignCount)); } } + else + { + throw ExceptionUtilities.UnexpectedValue(content.Kind); + } + + if (isMultiLine && contentIndex == _copyPasteData.Contents.Length - 1) + { + // Similar to the check we do for the first-change, if the last change was pasted into the space + // before the last `"""` then we need potentially insert a newline, then enough indentation + // whitespace to keep delimiter in the right location. + + TextBeforePaste.GetLineAndOffset(_selectionSpanBeforePaste.End, out var line, out var offset); - return new TextChange(_selectionSpanBeforePaste, builder.ToString()); + if (line == TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.Span.End).LineNumber) + { + var hasNewLine = content.IsText && HasNewLine(lastContentSourceText!.Lines.Last()); + if (!hasNewLine) + builder.Append(NewLine); + + builder.Append(TextBeforePaste.ToString(new TextSpan(TextBeforePaste.Lines[line].Start, offset))); + } + } } + + return new TextChange(_selectionSpanBeforePaste, builder.ToString()); } } diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteCommandHandler.cs b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteCommandHandler.cs index 07e1ba1166313..34c277b0d2faf 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteCommandHandler.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteCommandHandler.cs @@ -29,330 +29,329 @@ using Roslyn.Utilities; using VSUtilities = Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste +namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste; + +using static StringCopyPasteHelpers; + +/// +/// Command handler that both tracks 'copy' commands within VS to see what text the user copied (and from where), +/// but also then handles pasting that text back in a sensible fashion (e.g. escaping/unescaping/wrapping/indenting) +/// inside a string-literal. Can also handle pasting code from unknown sources as well, though heuristics must be +/// applied in that case to make a best effort guess as to what the original text meant and how to preserve that +/// in the final context. +/// +/// +/// Because we are revising what the normal editor does, we follow the standard behavior of first allowing the +/// editor to process paste commands, and then adding our own changes as an edit after that. That way if the user +/// doesn't want the change we made, they can always undo to get the prior paste behavior. +/// +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[Name(PredefinedCommandHandlerNames.StringCopyPaste)] +[Order(After = PredefinedCommandHandlerNames.FormatDocument)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal partial class StringCopyPasteCommandHandler( + IThreadingContext threadingContext, + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + IGlobalOptionService globalOptions, + ITextBufferFactoryService2 textBufferFactoryService, + EditorOptionsService editorOptionsService, + IIndentationManagerService indentationManager) : + IChainedCommandHandler, + IChainedCommandHandler, + IChainedCommandHandler { - using static StringCopyPasteHelpers; + private const string CopyId = "RoslynStringCopyPasteId"; - /// - /// Command handler that both tracks 'copy' commands within VS to see what text the user copied (and from where), - /// but also then handles pasting that text back in a sensible fashion (e.g. escaping/unescaping/wrapping/indenting) - /// inside a string-literal. Can also handle pasting code from unknown sources as well, though heuristics must be - /// applied in that case to make a best effort guess as to what the original text meant and how to preserve that - /// in the final context. - /// - /// - /// Because we are revising what the normal editor does, we follow the standard behavior of first allowing the - /// editor to process paste commands, and then adding our own changes as an edit after that. That way if the user - /// doesn't want the change we made, they can always undo to get the prior paste behavior. - /// - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [Name(PredefinedCommandHandlerNames.StringCopyPaste)] - [Order(After = PredefinedCommandHandlerNames.FormatDocument)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal partial class StringCopyPasteCommandHandler( - IThreadingContext threadingContext, - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - IGlobalOptionService globalOptions, - ITextBufferFactoryService2 textBufferFactoryService, - EditorOptionsService editorOptionsService, - IIndentationManagerService indentationManager) : - IChainedCommandHandler, - IChainedCommandHandler, - IChainedCommandHandler + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly IIndentationManagerService _indentationManager = indentationManager; + private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly ITextBufferFactoryService2 _textBufferFactoryService = textBufferFactoryService; + + public string DisplayName => nameof(StringCopyPasteCommandHandler); + + public CommandState GetCommandState(PasteCommandArgs args, Func nextCommandHandler) + => nextCommandHandler(); + + public void ExecuteCommand(PasteCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) { - private const string CopyId = "RoslynStringCopyPasteId"; + Contract.ThrowIfFalse(_threadingContext.HasMainThread); - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - private readonly IIndentationManagerService _indentationManager = indentationManager; - private readonly IGlobalOptionService _globalOptions = globalOptions; - private readonly ITextBufferFactoryService2 _textBufferFactoryService = textBufferFactoryService; + var textView = args.TextView; + var subjectBuffer = args.SubjectBuffer; - public string DisplayName => nameof(StringCopyPasteCommandHandler); + var selectionsBeforePaste = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); + var snapshotBeforePaste = subjectBuffer.CurrentSnapshot; - public CommandState GetCommandState(PasteCommandArgs args, Func nextCommandHandler) - => nextCommandHandler(); + // Always let the real paste go through. That way we always have a version of the document that doesn't + // include our changes that we can undo back to. + nextCommandHandler(); - public void ExecuteCommand(PasteCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) - { - Contract.ThrowIfFalse(_threadingContext.HasMainThread); - - var textView = args.TextView; - var subjectBuffer = args.SubjectBuffer; - - var selectionsBeforePaste = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); - var snapshotBeforePaste = subjectBuffer.CurrentSnapshot; - - // Always let the real paste go through. That way we always have a version of the document that doesn't - // include our changes that we can undo back to. - nextCommandHandler(); - - // If we don't even see any changes from the paste, there's nothing we can do. - if (snapshotBeforePaste.Version.Changes is null) - return; - - // If the user has the option off, then don't bother doing anything once we've sent the paste through. - if (!_globalOptions.GetOption(StringCopyPasteOptionsStorage.AutomaticallyFixStringContentsOnPaste, LanguageNames.CSharp)) - return; - - // if we're not even sure where the user caret/selection is on this buffer, we can't proceed. - if (selectionsBeforePaste.Count == 0) - return; - - var snapshotAfterPaste = subjectBuffer.CurrentSnapshot; - - // If there were multiple changes that already happened, then don't make any changes. Some other component - // already did something advanced. - if (snapshotAfterPaste.Version != snapshotBeforePaste.Version.Next) - return; - - // Have to even be in a C# doc to be able to have special space processing here. - - var documentBeforePaste = snapshotBeforePaste.GetOpenDocumentInCurrentContextWithChanges(); - var documentAfterPaste = snapshotAfterPaste.GetOpenDocumentInCurrentContextWithChanges(); - if (documentBeforePaste == null || documentAfterPaste == null) - return; - - var cancellationToken = executionContext.OperationContext.UserCancellationToken; - var parsedDocumentBeforePaste = ParsedDocument.CreateSynchronously(documentBeforePaste, cancellationToken); - - // When pasting, only do anything special if the user selections were entirely inside a single string - // token/expression. Otherwise, we have a multi-selection across token kinds which will be extremely - // complex to try to reconcile. - var stringExpressionBeforePaste = TryGetCompatibleContainingStringExpression(parsedDocumentBeforePaste, selectionsBeforePaste); - if (stringExpressionBeforePaste == null) - return; - - // Also ensure that all the changes the editor actually applied were inside a single string - // token/expression. If the editor decided to make changes outside of the string, we definitely do not want - // to do anything here. - var stringExpressionBeforePasteFromChanges = TryGetCompatibleContainingStringExpression( - parsedDocumentBeforePaste, new NormalizedSnapshotSpanCollection(snapshotBeforePaste, snapshotBeforePaste.Version.Changes.Select(c => c.OldSpan))); - if (stringExpressionBeforePaste != stringExpressionBeforePasteFromChanges) - return; - - var textChanges = GetEdits(cancellationToken); - - // If we didn't get any viable changes back, don't do anything. - if (textChanges.IsDefaultOrEmpty) - return; - - var newTextAfterChanges = snapshotBeforePaste.AsText().WithChanges(textChanges); - - // If we end up making the same changes as what the paste did, then no need to proceed. - if (ContentsAreSame(snapshotBeforePaste, snapshotAfterPaste, stringExpressionBeforePaste, newTextAfterChanges)) - return; - - // Create two edits to make the change. The first restores the buffer to the original snapshot (effectively - // undoing the first set of changes). Then the second actually applies the change. - // - // Do this as direct edits, passing 'EditOptions.None' for the options, as we want to control the edits - // precisely and don't want any strange interpretation of where the caret should end up. Other options - // (like DefaultMinimalChange) will attempt to diff/merge edits oddly sometimes which can lead the caret - // ending up before/after some merged change, which will no longer match the behavior of precise pastes. - // - // Wrap this all as a transaction so that these two edits appear to be one single change. This also allows - // the user to do a single 'undo' that gets them back to the original paste made at the start of this - // method. - - using var transaction = new CaretPreservingEditTransaction( - CSharpEditorResources.Fixing_string_literal_after_paste, - textView, _undoHistoryRegistry, _editorOperationsFactoryService); - - { - var edit = subjectBuffer.CreateEdit(EditOptions.None, reiteratedVersionNumber: null, editTag: null); - foreach (var change in snapshotBeforePaste.Version.Changes) - edit.Replace(change.NewSpan, change.OldText); - edit.Apply(); - } - - { - var edit = subjectBuffer.CreateEdit(EditOptions.None, reiteratedVersionNumber: null, editTag: null); - foreach (var selection in selectionsBeforePaste) - edit.Replace(selection.Span, ""); - - foreach (var change in textChanges) - edit.Replace(change.Span.ToSpan(), change.NewText); - edit.Apply(); - } - - transaction.Complete(); + // If we don't even see any changes from the paste, there's nothing we can do. + if (snapshotBeforePaste.Version.Changes is null) return; - ImmutableArray GetEdits(CancellationToken cancellationToken) - { - var newLine = textView.Options.GetNewLineCharacter(); - var indentationWhitespace = DetermineIndentationWhitespace( - parsedDocumentBeforePaste, subjectBuffer, snapshotBeforePaste.AsText(), stringExpressionBeforePaste, cancellationToken); - - // See if this is a paste of the last copy that we heard about. - var edits = TryGetEditsFromKnownCopySource(newLine, indentationWhitespace); - if (!edits.IsDefaultOrEmpty) - return edits; - - var pasteWasSuccessful = PasteWasSuccessful( - snapshotBeforePaste, snapshotAfterPaste, documentAfterPaste, stringExpressionBeforePaste, cancellationToken); - - // If not, then just go through the fallback code path that applies more heuristics. - var unknownPasteProcessor = new UnknownSourcePasteProcessor( - newLine, indentationWhitespace, - snapshotBeforePaste, snapshotAfterPaste, - documentBeforePaste, documentAfterPaste, - stringExpressionBeforePaste, pasteWasSuccessful); - return unknownPasteProcessor.GetEdits(); - } - - ImmutableArray TryGetEditsFromKnownCopySource( - string newLine, string indentationWhitespace) - { - // For simplicity, we only support smart copy/paste when we are pasting into a single contiguous region. - if (selectionsBeforePaste.Count != 1) - return default; - - var copyPasteService = documentBeforePaste.Project.Solution.Services.GetRequiredService(); - var clipboardData = copyPasteService.TryGetClipboardData(KeyAndVersion); - var copyPasteData = StringCopyPasteData.FromJson(clipboardData); - - if (copyPasteData == null) - return default; - - var knownProcessor = new KnownSourcePasteProcessor( - newLine, indentationWhitespace, - snapshotBeforePaste, snapshotAfterPaste, - documentBeforePaste, documentAfterPaste, - stringExpressionBeforePaste, - selectionsBeforePaste[0].Span.ToTextSpan(), - copyPasteData, _textBufferFactoryService); - return knownProcessor.GetEdits(); - } - } + // If the user has the option off, then don't bother doing anything once we've sent the paste through. + if (!_globalOptions.GetOption(StringCopyPasteOptionsStorage.AutomaticallyFixStringContentsOnPaste, LanguageNames.CSharp)) + return; + + // if we're not even sure where the user caret/selection is on this buffer, we can't proceed. + if (selectionsBeforePaste.Count == 0) + return; + + var snapshotAfterPaste = subjectBuffer.CurrentSnapshot; + + // If there were multiple changes that already happened, then don't make any changes. Some other component + // already did something advanced. + if (snapshotAfterPaste.Version != snapshotBeforePaste.Version.Next) + return; + + // Have to even be in a C# doc to be able to have special space processing here. + + var documentBeforePaste = snapshotBeforePaste.GetOpenDocumentInCurrentContextWithChanges(); + var documentAfterPaste = snapshotAfterPaste.GetOpenDocumentInCurrentContextWithChanges(); + if (documentBeforePaste == null || documentAfterPaste == null) + return; + + var cancellationToken = executionContext.OperationContext.UserCancellationToken; + var parsedDocumentBeforePaste = ParsedDocument.CreateSynchronously(documentBeforePaste, cancellationToken); + + // When pasting, only do anything special if the user selections were entirely inside a single string + // token/expression. Otherwise, we have a multi-selection across token kinds which will be extremely + // complex to try to reconcile. + var stringExpressionBeforePaste = TryGetCompatibleContainingStringExpression(parsedDocumentBeforePaste, selectionsBeforePaste); + if (stringExpressionBeforePaste == null) + return; + + // Also ensure that all the changes the editor actually applied were inside a single string + // token/expression. If the editor decided to make changes outside of the string, we definitely do not want + // to do anything here. + var stringExpressionBeforePasteFromChanges = TryGetCompatibleContainingStringExpression( + parsedDocumentBeforePaste, new NormalizedSnapshotSpanCollection(snapshotBeforePaste, snapshotBeforePaste.Version.Changes.Select(c => c.OldSpan))); + if (stringExpressionBeforePaste != stringExpressionBeforePasteFromChanges) + return; + + var textChanges = GetEdits(cancellationToken); + + // If we didn't get any viable changes back, don't do anything. + if (textChanges.IsDefaultOrEmpty) + return; + + var newTextAfterChanges = snapshotBeforePaste.AsText().WithChanges(textChanges); + + // If we end up making the same changes as what the paste did, then no need to proceed. + if (ContentsAreSame(snapshotBeforePaste, snapshotAfterPaste, stringExpressionBeforePaste, newTextAfterChanges)) + return; + + // Create two edits to make the change. The first restores the buffer to the original snapshot (effectively + // undoing the first set of changes). Then the second actually applies the change. + // + // Do this as direct edits, passing 'EditOptions.None' for the options, as we want to control the edits + // precisely and don't want any strange interpretation of where the caret should end up. Other options + // (like DefaultMinimalChange) will attempt to diff/merge edits oddly sometimes which can lead the caret + // ending up before/after some merged change, which will no longer match the behavior of precise pastes. + // + // Wrap this all as a transaction so that these two edits appear to be one single change. This also allows + // the user to do a single 'undo' that gets them back to the original paste made at the start of this + // method. + + using var transaction = new CaretPreservingEditTransaction( + CSharpEditorResources.Fixing_string_literal_after_paste, + textView, _undoHistoryRegistry, _editorOperationsFactoryService); - private string DetermineIndentationWhitespace( - ParsedDocument documentBeforePaste, - ITextBuffer textBuffer, - SourceText textBeforePaste, - ExpressionSyntax stringExpressionBeforePaste, - CancellationToken cancellationToken) { - // Only raw strings care about indentation. Don't bother computing if we don't need it. - if (!IsAnyRawStringExpression(stringExpressionBeforePaste)) - return ""; - - if (IsAnyMultiLineRawStringExpression(stringExpressionBeforePaste)) - { - // already have a multi-line raw string. The indentation of it's end delimiter is the indentation all - // lines within it should have. - var lastLine = textBeforePaste.Lines.GetLineFromPosition(stringExpressionBeforePaste.Span.End); - var quotePosition = lastLine.GetFirstNonWhitespacePosition()!.Value; - return textBeforePaste.ToString(TextSpan.FromBounds(lastLine.Span.Start, quotePosition)); - } - - // Otherwise, we have a single-line raw string. Determine the default indentation desired here. - // We'll use that if we have to convert this single-line raw string to a multi-line one. - var indentationOptions = textBuffer.GetIndentationOptions(_editorOptionsService, documentBeforePaste.LanguageServices, explicitFormat: false); - return stringExpressionBeforePaste.GetFirstToken().GetPreferredIndentation(documentBeforePaste, indentationOptions, cancellationToken); + var edit = subjectBuffer.CreateEdit(EditOptions.None, reiteratedVersionNumber: null, editTag: null); + foreach (var change in snapshotBeforePaste.Version.Changes) + edit.Replace(change.NewSpan, change.OldText); + edit.Apply(); } - /// - /// Returns true if the paste resulted in legal code for the string literal. The string literal is - /// considered legal if it has the same span as the original string (adjusted as per the edit) and that - /// there are no errors in it. For this purposes of this check, errors in interpolation holes are not - /// considered. We only care about the textual content of the string. - /// - internal static bool PasteWasSuccessful( - ITextSnapshot snapshotBeforePaste, - ITextSnapshot snapshotAfterPaste, - Document documentAfterPaste, - ExpressionSyntax stringExpressionBeforePaste, - CancellationToken cancellationToken) { - var rootAfterPaste = documentAfterPaste.GetRequiredSyntaxRootSynchronously(cancellationToken); - var stringExpressionAfterPaste = FindContainingSupportedStringExpression(rootAfterPaste, stringExpressionBeforePaste.SpanStart); - if (stringExpressionAfterPaste == null) - return false; + var edit = subjectBuffer.CreateEdit(EditOptions.None, reiteratedVersionNumber: null, editTag: null); + foreach (var selection in selectionsBeforePaste) + edit.Replace(selection.Span, ""); - if (ContainsError(stringExpressionAfterPaste)) - return false; - - var spanAfterPaste = MapSpan(stringExpressionBeforePaste.Span, snapshotBeforePaste, snapshotAfterPaste); - return spanAfterPaste == stringExpressionAfterPaste.Span; + foreach (var change in textChanges) + edit.Replace(change.Span.ToSpan(), change.NewText); + edit.Apply(); } - /// - /// Given the snapshots before/after pasting, and the source-text our manual fixup edits produced, see if our - /// manual application actually produced the same results as the paste. If so, we don't need to actually do - /// anything. To optimize this check, we pass in the original string expression as that's all we have to check - /// (adjusting for where it now ends up) in both the 'after' documents. - /// - private static bool ContentsAreSame( - ITextSnapshot snapshotBeforePaste, - ITextSnapshot snapshotAfterPaste, - ExpressionSyntax stringExpressionBeforePaste, - SourceText newTextAfterChanges) + transaction.Complete(); + return; + + ImmutableArray GetEdits(CancellationToken cancellationToken) { - // We ended up with documents of different length after we escaped/manipulated the pasted text. So the - // contents are definitely not the same. - if (newTextAfterChanges.Length != snapshotAfterPaste.Length) - return false; + var newLine = textView.Options.GetNewLineCharacter(); + var indentationWhitespace = DetermineIndentationWhitespace( + parsedDocumentBeforePaste, subjectBuffer, snapshotBeforePaste.AsText(), stringExpressionBeforePaste, cancellationToken); + + // See if this is a paste of the last copy that we heard about. + var edits = TryGetEditsFromKnownCopySource(newLine, indentationWhitespace); + if (!edits.IsDefaultOrEmpty) + return edits; + + var pasteWasSuccessful = PasteWasSuccessful( + snapshotBeforePaste, snapshotAfterPaste, documentAfterPaste, stringExpressionBeforePaste, cancellationToken); + + // If not, then just go through the fallback code path that applies more heuristics. + var unknownPasteProcessor = new UnknownSourcePasteProcessor( + newLine, indentationWhitespace, + snapshotBeforePaste, snapshotAfterPaste, + documentBeforePaste, documentAfterPaste, + stringExpressionBeforePaste, pasteWasSuccessful); + return unknownPasteProcessor.GetEdits(); + } - var spanAfterPaste = MapSpan(stringExpressionBeforePaste.Span, snapshotBeforePaste, snapshotAfterPaste); + ImmutableArray TryGetEditsFromKnownCopySource( + string newLine, string indentationWhitespace) + { + // For simplicity, we only support smart copy/paste when we are pasting into a single contiguous region. + if (selectionsBeforePaste.Count != 1) + return default; + + var copyPasteService = documentBeforePaste.Project.Solution.Services.GetRequiredService(); + var clipboardData = copyPasteService.TryGetClipboardData(KeyAndVersion); + var copyPasteData = StringCopyPasteData.FromJson(clipboardData); + + if (copyPasteData == null) + return default; + + var knownProcessor = new KnownSourcePasteProcessor( + newLine, indentationWhitespace, + snapshotBeforePaste, snapshotAfterPaste, + documentBeforePaste, documentAfterPaste, + stringExpressionBeforePaste, + selectionsBeforePaste[0].Span.ToTextSpan(), + copyPasteData, _textBufferFactoryService); + return knownProcessor.GetEdits(); + } + } - var originalStringContentsAfterPaste = snapshotAfterPaste.AsText().GetSubText(spanAfterPaste); - var newStringContentsAfterEdit = newTextAfterChanges.GetSubText(spanAfterPaste); + private string DetermineIndentationWhitespace( + ParsedDocument documentBeforePaste, + ITextBuffer textBuffer, + SourceText textBeforePaste, + ExpressionSyntax stringExpressionBeforePaste, + CancellationToken cancellationToken) + { + // Only raw strings care about indentation. Don't bother computing if we don't need it. + if (!IsAnyRawStringExpression(stringExpressionBeforePaste)) + return ""; - return originalStringContentsAfterPaste.ContentEquals(newStringContentsAfterEdit); + if (IsAnyMultiLineRawStringExpression(stringExpressionBeforePaste)) + { + // already have a multi-line raw string. The indentation of it's end delimiter is the indentation all + // lines within it should have. + var lastLine = textBeforePaste.Lines.GetLineFromPosition(stringExpressionBeforePaste.Span.End); + var quotePosition = lastLine.GetFirstNonWhitespacePosition()!.Value; + return textBeforePaste.ToString(TextSpan.FromBounds(lastLine.Span.Start, quotePosition)); } - /// - /// Returns the or if the - /// selections were all contained within a single literal in a compatible fashion. This means all the - /// selections have to start/end in a content-span portion of the literal. For example, if we paste into an - /// interpolated string and have half of the selection outside an interpolation and half inside, we don't do - /// anything special as trying to correct in this scenario is too difficult. - /// - private static ExpressionSyntax? TryGetCompatibleContainingStringExpression( - ParsedDocument document, NormalizedSnapshotSpanCollection spans) - { - if (spans.Count == 0) - return null; + // Otherwise, we have a single-line raw string. Determine the default indentation desired here. + // We'll use that if we have to convert this single-line raw string to a multi-line one. + var indentationOptions = textBuffer.GetIndentationOptions(_editorOptionsService, documentBeforePaste.LanguageServices, explicitFormat: false); + return stringExpressionBeforePaste.GetFirstToken().GetPreferredIndentation(documentBeforePaste, indentationOptions, cancellationToken); + } + + /// + /// Returns true if the paste resulted in legal code for the string literal. The string literal is + /// considered legal if it has the same span as the original string (adjusted as per the edit) and that + /// there are no errors in it. For this purposes of this check, errors in interpolation holes are not + /// considered. We only care about the textual content of the string. + /// + internal static bool PasteWasSuccessful( + ITextSnapshot snapshotBeforePaste, + ITextSnapshot snapshotAfterPaste, + Document documentAfterPaste, + ExpressionSyntax stringExpressionBeforePaste, + CancellationToken cancellationToken) + { + var rootAfterPaste = documentAfterPaste.GetRequiredSyntaxRootSynchronously(cancellationToken); + var stringExpressionAfterPaste = FindContainingSupportedStringExpression(rootAfterPaste, stringExpressionBeforePaste.SpanStart); + if (stringExpressionAfterPaste == null) + return false; - var snapshot = spans[0].Snapshot; + if (ContainsError(stringExpressionAfterPaste)) + return false; - // First, try to see if all the selections are at least contained within a single string literal expression. - var stringExpression = FindCommonContainingStringExpression(document.Root, spans); - if (stringExpression == null) - return null; + var spanAfterPaste = MapSpan(stringExpressionBeforePaste.Span, snapshotBeforePaste, snapshotAfterPaste); + return spanAfterPaste == stringExpressionAfterPaste.Span; + } - // Now, given that string expression, find the inside 'text' spans of the expression. These are the parts - // of the literal between the quotes. It does not include the interpolation holes in an interpolated - // string. These spans may be empty (for an empty string, or empty text gap between interpolations). - var contentSpans = StringInfo.GetStringInfo(snapshot.AsText(), stringExpression).ContentSpans; - foreach (var snapshotSpan in spans) - { - var startIndex = contentSpans.BinarySearch(snapshotSpan.Span.Start, FindIndex); - var endIndex = contentSpans.BinarySearch(snapshotSpan.Span.End, FindIndex); + /// + /// Given the snapshots before/after pasting, and the source-text our manual fixup edits produced, see if our + /// manual application actually produced the same results as the paste. If so, we don't need to actually do + /// anything. To optimize this check, we pass in the original string expression as that's all we have to check + /// (adjusting for where it now ends up) in both the 'after' documents. + /// + private static bool ContentsAreSame( + ITextSnapshot snapshotBeforePaste, + ITextSnapshot snapshotAfterPaste, + ExpressionSyntax stringExpressionBeforePaste, + SourceText newTextAfterChanges) + { + // We ended up with documents of different length after we escaped/manipulated the pasted text. So the + // contents are definitely not the same. + if (newTextAfterChanges.Length != snapshotAfterPaste.Length) + return false; - if (startIndex < 0 || endIndex < 0) - return null; - } + var spanAfterPaste = MapSpan(stringExpressionBeforePaste.Span, snapshotBeforePaste, snapshotAfterPaste); - return stringExpression; + var originalStringContentsAfterPaste = snapshotAfterPaste.AsText().GetSubText(spanAfterPaste); + var newStringContentsAfterEdit = newTextAfterChanges.GetSubText(spanAfterPaste); - static int FindIndex(TextSpan span, int position) - { - if (span.IntersectsWith(position)) - return 0; + return originalStringContentsAfterPaste.ContentEquals(newStringContentsAfterEdit); + } + + /// + /// Returns the or if the + /// selections were all contained within a single literal in a compatible fashion. This means all the + /// selections have to start/end in a content-span portion of the literal. For example, if we paste into an + /// interpolated string and have half of the selection outside an interpolation and half inside, we don't do + /// anything special as trying to correct in this scenario is too difficult. + /// + private static ExpressionSyntax? TryGetCompatibleContainingStringExpression( + ParsedDocument document, NormalizedSnapshotSpanCollection spans) + { + if (spans.Count == 0) + return null; + + var snapshot = spans[0].Snapshot; + + // First, try to see if all the selections are at least contained within a single string literal expression. + var stringExpression = FindCommonContainingStringExpression(document.Root, spans); + if (stringExpression == null) + return null; + + // Now, given that string expression, find the inside 'text' spans of the expression. These are the parts + // of the literal between the quotes. It does not include the interpolation holes in an interpolated + // string. These spans may be empty (for an empty string, or empty text gap between interpolations). + var contentSpans = StringInfo.GetStringInfo(snapshot.AsText(), stringExpression).ContentSpans; + foreach (var snapshotSpan in spans) + { + var startIndex = contentSpans.BinarySearch(snapshotSpan.Span.Start, FindIndex); + var endIndex = contentSpans.BinarySearch(snapshotSpan.Span.End, FindIndex); + + if (startIndex < 0 || endIndex < 0) + return null; + } + + return stringExpression; + + static int FindIndex(TextSpan span, int position) + { + if (span.IntersectsWith(position)) + return 0; - if (span.End < position) - return -1; + if (span.End < position) + return -1; - return 1; - } + return 1; } } } diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteCommandHandler_CutCopy.cs b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteCommandHandler_CutCopy.cs index 04f63ea216d8d..6417d40261d7c 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteCommandHandler_CutCopy.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteCommandHandler_CutCopy.cs @@ -24,78 +24,77 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste +namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste; + +internal partial class StringCopyPasteCommandHandler : + IChainedCommandHandler, + IChainedCommandHandler { - internal partial class StringCopyPasteCommandHandler : - IChainedCommandHandler, - IChainedCommandHandler + public const string KeyAndVersion = nameof(StringCopyPasteCommandHandler) + "V1"; + + public CommandState GetCommandState(CutCommandArgs args, Func nextCommandHandler) + => nextCommandHandler(); + + public CommandState GetCommandState(CopyCommandArgs args, Func nextCommandHandler) + => nextCommandHandler(); + + public void ExecuteCommand(CutCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + => ExecuteCutOrCopyCommand(args.TextView, args.SubjectBuffer, nextCommandHandler, executionContext); + + public void ExecuteCommand(CopyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + => ExecuteCutOrCopyCommand(args.TextView, args.SubjectBuffer, nextCommandHandler, executionContext); + + private void ExecuteCutOrCopyCommand(ITextView textView, ITextBuffer subjectBuffer, Action nextCommandHandler, CommandExecutionContext executionContext) { - public const string KeyAndVersion = nameof(StringCopyPasteCommandHandler) + "V1"; + Contract.ThrowIfFalse(_threadingContext.HasMainThread); + var (dataToStore, copyPasteService) = CaptureCutCopyInformation(textView, subjectBuffer, executionContext.OperationContext.UserCancellationToken); - public CommandState GetCommandState(CutCommandArgs args, Func nextCommandHandler) - => nextCommandHandler(); + // Ensure that the copy always goes through all other handlers. + nextCommandHandler(); - public CommandState GetCommandState(CopyCommandArgs args, Func nextCommandHandler) - => nextCommandHandler(); + // Always try to store our data to the clipboard (if we have access to the clipboard service). Even if we + // didn't capture any useful data, we want to store that to blow away any prior stored data we have. + copyPasteService?.TrySetClipboardData(KeyAndVersion, dataToStore ?? ""); + } - public void ExecuteCommand(CutCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) - => ExecuteCutOrCopyCommand(args.TextView, args.SubjectBuffer, nextCommandHandler, executionContext); + private static (string? dataToStore, IStringCopyPasteService service) CaptureCutCopyInformation( + ITextView textView, ITextBuffer subjectBuffer, CancellationToken cancellationToken) + { + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return default; - public void ExecuteCommand(CopyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) - => ExecuteCutOrCopyCommand(args.TextView, args.SubjectBuffer, nextCommandHandler, executionContext); + var copyPasteService = document.Project.Solution.Services.GetService(); + if (copyPasteService == null) + return default; - private void ExecuteCutOrCopyCommand(ITextView textView, ITextBuffer subjectBuffer, Action nextCommandHandler, CommandExecutionContext executionContext) - { - Contract.ThrowIfFalse(_threadingContext.HasMainThread); - var (dataToStore, copyPasteService) = CaptureCutCopyInformation(textView, subjectBuffer, executionContext.OperationContext.UserCancellationToken); + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - // Ensure that the copy always goes through all other handlers. - nextCommandHandler(); + var spans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); - // Always try to store our data to the clipboard (if we have access to the clipboard service). Even if we - // didn't capture any useful data, we want to store that to blow away any prior stored data we have. - copyPasteService?.TrySetClipboardData(KeyAndVersion, dataToStore ?? ""); - } + // We only support smart copy/paste when a single selection is copied (and a single selection is pasted + // over). This vastly simplifies the logic we need, and it means we don't have to try to reimplement the + // editor logic for what it means when you are copying X selections and pasting over Y selections. + if (spans.Count != 1) + return default; - private static (string? dataToStore, IStringCopyPasteService service) CaptureCutCopyInformation( - ITextView textView, ITextBuffer subjectBuffer, CancellationToken cancellationToken) + var span = spans[0]; + var snapshot = span.Snapshot; + if (span.IsEmpty) { - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return default; - - var copyPasteService = document.Project.Solution.Services.GetService(); - if (copyPasteService == null) - return default; - - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - - var spans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); - - // We only support smart copy/paste when a single selection is copied (and a single selection is pasted - // over). This vastly simplifies the logic we need, and it means we don't have to try to reimplement the - // editor logic for what it means when you are copying X selections and pasting over Y selections. - if (spans.Count != 1) - return default; - - var span = spans[0]; - var snapshot = span.Snapshot; - if (span.IsEmpty) - { - // cut/copy on an empty span means "cut/copy the entire line". - var line = snapshot.GetLineFromPosition(span.Start); - span = line.ExtentIncludingLineBreak; - } - - var stringExpression = TryGetCompatibleContainingStringExpression( - parsedDocument, new NormalizedSnapshotSpanCollection(span)); - if (stringExpression is null) - return default; - - var virtualCharService = document.GetRequiredLanguageService(); - var stringData = StringCopyPasteData.TryCreate(virtualCharService, stringExpression, span.Span.ToTextSpan()); - - return (stringData?.ToJson(), copyPasteService); + // cut/copy on an empty span means "cut/copy the entire line". + var line = snapshot.GetLineFromPosition(span.Start); + span = line.ExtentIncludingLineBreak; } + + var stringExpression = TryGetCompatibleContainingStringExpression( + parsedDocument, new NormalizedSnapshotSpanCollection(span)); + if (stringExpression is null) + return default; + + var virtualCharService = document.GetRequiredLanguageService(); + var stringData = StringCopyPasteData.TryCreate(virtualCharService, stringExpression, span.Span.ToTextSpan()); + + return (stringData?.ToJson(), copyPasteService); } } diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteContent.cs b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteContent.cs index c280b2fcbee54..973515b21ca5c 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteContent.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteContent.cs @@ -6,60 +6,59 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste +namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste; + +/// +/// Content from a string literal or interpolated string that has been copied. +/// +[method: JsonConstructor] +internal readonly struct StringCopyPasteContent( + StringCopyPasteContentKind kind, + string? textValue, + string? interpolationExpression, + string? interpolationAlignmentClause, + string? interpolationFormatClause) { + public StringCopyPasteContentKind Kind { get; } = kind; + /// - /// Content from a string literal or interpolated string that has been copied. + /// The actual string value for . for . /// - [method: JsonConstructor] - internal readonly struct StringCopyPasteContent( - StringCopyPasteContentKind kind, - string? textValue, - string? interpolationExpression, - string? interpolationAlignmentClause, - string? interpolationFormatClause) - { - public StringCopyPasteContentKind Kind { get; } = kind; - - /// - /// The actual string value for . for . - /// - public string? TextValue { get; } = textValue; + public string? TextValue { get; } = textValue; - /// - /// The actual string value for for . for . - /// - public string? InterpolationExpression { get; } = interpolationExpression; + /// + /// The actual string value for for . for . + /// + public string? InterpolationExpression { get; } = interpolationExpression; - /// - /// The actual string value for for . for . - /// - public string? InterpolationAlignmentClause { get; } = interpolationAlignmentClause; + /// + /// The actual string value for for . for . + /// + public string? InterpolationAlignmentClause { get; } = interpolationAlignmentClause; - /// - /// The actual string value for for . for . - /// - public string? InterpolationFormatClause { get; } = interpolationFormatClause; + /// + /// The actual string value for for . for . + /// + public string? InterpolationFormatClause { get; } = interpolationFormatClause; - [JsonIgnore] - [MemberNotNullWhen(true, nameof(TextValue))] - public bool IsText => Kind == StringCopyPasteContentKind.Text; + [JsonIgnore] + [MemberNotNullWhen(true, nameof(TextValue))] + public bool IsText => Kind == StringCopyPasteContentKind.Text; - [JsonIgnore] - [MemberNotNullWhen(true, nameof(InterpolationExpression))] - public bool IsInterpolation => Kind == StringCopyPasteContentKind.Interpolation; + [JsonIgnore] + [MemberNotNullWhen(true, nameof(InterpolationExpression))] + public bool IsInterpolation => Kind == StringCopyPasteContentKind.Interpolation; - public static StringCopyPasteContent ForText(string text) - => new(StringCopyPasteContentKind.Text, text, null, null, null); + public static StringCopyPasteContent ForText(string text) + => new(StringCopyPasteContentKind.Text, text, null, null, null); - public static StringCopyPasteContent ForInterpolation(string expression, string? alignmentClause, string? formatClause) - => new(StringCopyPasteContentKind.Interpolation, null, expression, alignmentClause, formatClause); - } + public static StringCopyPasteContent ForInterpolation(string expression, string? alignmentClause, string? formatClause) + => new(StringCopyPasteContentKind.Interpolation, null, expression, alignmentClause, formatClause); } diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteContentKind.cs b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteContentKind.cs index ec5d3fc058f24..7f048875094ef 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteContentKind.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteContentKind.cs @@ -2,18 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste +namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste; + +internal enum StringCopyPasteContentKind { - internal enum StringCopyPasteContentKind - { - /// - /// When text content is copied. - /// - Text, + /// + /// When text content is copied. + /// + Text, - /// - /// When an interpolation is copied. - /// - Interpolation, - } + /// + /// When an interpolation is copied. + /// + Interpolation, } diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteData.cs b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteData.cs index f0d77f8940645..d2dc1d9b78710 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteData.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteData.cs @@ -18,176 +18,175 @@ using Roslyn.Utilities; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste +namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste; + +/// +/// Data about a string that a user has copied a subsection of. This will itself be placed on the clipboard so that +/// it can be retrieved later on if the user pastes. +/// +[method: JsonConstructor] +internal class StringCopyPasteData(ImmutableArray contents) { - /// - /// Data about a string that a user has copied a subsection of. This will itself be placed on the clipboard so that - /// it can be retrieved later on if the user pastes. - /// - [method: JsonConstructor] - internal class StringCopyPasteData(ImmutableArray contents) - { - public ImmutableArray Contents { get; } = contents; + public ImmutableArray Contents { get; } = contents; - public string? ToJson() + public string? ToJson() + { + try { - try - { - return JsonSerializer.Serialize(this, typeof(StringCopyPasteData)); - } - catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical)) - { - } + return JsonSerializer.Serialize(this, typeof(StringCopyPasteData)); + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical)) + { + } + return null; + } + + public static StringCopyPasteData? FromJson(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return null; - } - public static StringCopyPasteData? FromJson(string? json) + try { - if (string.IsNullOrWhiteSpace(json)) + var value = JsonSerializer.Deserialize(JsonDocument.Parse(json), typeof(StringCopyPasteData)); + if (value is null) return null; - try - { - var value = JsonSerializer.Deserialize(JsonDocument.Parse(json), typeof(StringCopyPasteData)); - if (value is null) - return null; - - return (StringCopyPasteData)value; - } - catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical)) - { - } - - return null; + return (StringCopyPasteData)value; + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical)) + { } - /// - /// Given a for a string literal or interpolated string, and the the user has selected in it, tries to determine the interpreted content within that - /// expression that has been copied. "interpreted" in this context means the actual value of the content that - /// was selected, with things like escape characters embedded as the actual characters they represent. - /// - public static StringCopyPasteData? TryCreate(IVirtualCharLanguageService virtualCharService, ExpressionSyntax stringExpression, TextSpan selectionSpan) - => stringExpression switch - { - LiteralExpressionSyntax literal => TryCreateForLiteral(virtualCharService, literal, selectionSpan), - InterpolatedStringExpressionSyntax interpolatedString => TryCreateForInterpolatedString(virtualCharService, interpolatedString, selectionSpan), - _ => throw ExceptionUtilities.UnexpectedValue(stringExpression.Kind()), - }; - - private static StringCopyPasteData? TryCreateForLiteral(IVirtualCharLanguageService virtualCharService, LiteralExpressionSyntax literal, TextSpan span) - => TryGetContentForSpan(virtualCharService, literal.Token, span, out var content) - ? new StringCopyPasteData([content]) - : null; - - /// - /// Given a string , and the the user has selected that - /// overlaps with it, tries to determine the interpreted content within that token that has been copied. - /// "interpreted" in this context means the actual value of the content that was selected, with things like - /// escape characters embedded as the actual characters they represent. - /// - private static bool TryGetNormalizedStringForSpan( - IVirtualCharLanguageService virtualCharService, - SyntaxToken token, - TextSpan selectionSpan, - [NotNullWhen(true)] out string? normalizedText) + return null; + } + + /// + /// Given a for a string literal or interpolated string, and the the user has selected in it, tries to determine the interpreted content within that + /// expression that has been copied. "interpreted" in this context means the actual value of the content that + /// was selected, with things like escape characters embedded as the actual characters they represent. + /// + public static StringCopyPasteData? TryCreate(IVirtualCharLanguageService virtualCharService, ExpressionSyntax stringExpression, TextSpan selectionSpan) + => stringExpression switch { - normalizedText = null; + LiteralExpressionSyntax literal => TryCreateForLiteral(virtualCharService, literal, selectionSpan), + InterpolatedStringExpressionSyntax interpolatedString => TryCreateForInterpolatedString(virtualCharService, interpolatedString, selectionSpan), + _ => throw ExceptionUtilities.UnexpectedValue(stringExpression.Kind()), + }; - // First, try to convert this token to a sequence of virtual chars. - var virtualChars = virtualCharService.TryConvertToVirtualChars(token); - if (virtualChars.IsDefaultOrEmpty) - return false; + private static StringCopyPasteData? TryCreateForLiteral(IVirtualCharLanguageService virtualCharService, LiteralExpressionSyntax literal, TextSpan span) + => TryGetContentForSpan(virtualCharService, literal.Token, span, out var content) + ? new StringCopyPasteData([content]) + : null; - // Then find the start/end of the token's characters that overlap with the selection span. - var firstOverlappingChar = virtualChars.FirstOrNull(vc => vc.Span.OverlapsWith(selectionSpan)); - var lastOverlappingChar = virtualChars.LastOrNull(vc => vc.Span.OverlapsWith(selectionSpan)); + /// + /// Given a string , and the the user has selected that + /// overlaps with it, tries to determine the interpreted content within that token that has been copied. + /// "interpreted" in this context means the actual value of the content that was selected, with things like + /// escape characters embedded as the actual characters they represent. + /// + private static bool TryGetNormalizedStringForSpan( + IVirtualCharLanguageService virtualCharService, + SyntaxToken token, + TextSpan selectionSpan, + [NotNullWhen(true)] out string? normalizedText) + { + normalizedText = null; - if (firstOverlappingChar is null || lastOverlappingChar is null) - return false; + // First, try to convert this token to a sequence of virtual chars. + var virtualChars = virtualCharService.TryConvertToVirtualChars(token); + if (virtualChars.IsDefaultOrEmpty) + return false; - // Don't allow partial selection of an escaped character. e.g. if they select 'n' in '\n' - if (selectionSpan.Start > firstOverlappingChar.Value.Span.Start) - return false; + // Then find the start/end of the token's characters that overlap with the selection span. + var firstOverlappingChar = virtualChars.FirstOrNull(vc => vc.Span.OverlapsWith(selectionSpan)); + var lastOverlappingChar = virtualChars.LastOrNull(vc => vc.Span.OverlapsWith(selectionSpan)); - if (selectionSpan.End < lastOverlappingChar.Value.Span.End) - return false; + if (firstOverlappingChar is null || lastOverlappingChar is null) + return false; - var firstCharIndexInclusive = virtualChars.IndexOf(firstOverlappingChar.Value); - var lastCharIndexInclusive = virtualChars.IndexOf(lastOverlappingChar.Value); + // Don't allow partial selection of an escaped character. e.g. if they select 'n' in '\n' + if (selectionSpan.Start > firstOverlappingChar.Value.Span.Start) + return false; - // Grab that subsequence of characters and get the final interpreted string for it. - var subsequence = virtualChars.GetSubSequence(TextSpan.FromBounds(firstCharIndexInclusive, lastCharIndexInclusive + 1)); - normalizedText = subsequence.CreateString(); - return true; - } + if (selectionSpan.End < lastOverlappingChar.Value.Span.End) + return false; + + var firstCharIndexInclusive = virtualChars.IndexOf(firstOverlappingChar.Value); + var lastCharIndexInclusive = virtualChars.IndexOf(lastOverlappingChar.Value); - private static bool TryGetContentForSpan( - IVirtualCharLanguageService virtualCharService, - SyntaxToken token, - TextSpan selectionSpan, - out StringCopyPasteContent content) + // Grab that subsequence of characters and get the final interpreted string for it. + var subsequence = virtualChars.GetSubSequence(TextSpan.FromBounds(firstCharIndexInclusive, lastCharIndexInclusive + 1)); + normalizedText = subsequence.CreateString(); + return true; + } + + private static bool TryGetContentForSpan( + IVirtualCharLanguageService virtualCharService, + SyntaxToken token, + TextSpan selectionSpan, + out StringCopyPasteContent content) + { + if (!TryGetNormalizedStringForSpan(virtualCharService, token, selectionSpan, out var text)) { - if (!TryGetNormalizedStringForSpan(virtualCharService, token, selectionSpan, out var text)) - { - content = default; - return false; - } - else - { - content = StringCopyPasteContent.ForText(text); - return true; - } + content = default; + return false; } - - private static StringCopyPasteData? TryCreateForInterpolatedString( - IVirtualCharLanguageService virtualCharService, - InterpolatedStringExpressionSyntax interpolatedString, - TextSpan selectionSpan) + else { - using var _ = ArrayBuilder.GetInstance(out var result); + content = StringCopyPasteContent.ForText(text); + return true; + } + } - foreach (var interpolatedContent in interpolatedString.Contents) + private static StringCopyPasteData? TryCreateForInterpolatedString( + IVirtualCharLanguageService virtualCharService, + InterpolatedStringExpressionSyntax interpolatedString, + TextSpan selectionSpan) + { + using var _ = ArrayBuilder.GetInstance(out var result); + + foreach (var interpolatedContent in interpolatedString.Contents) + { + // Only consider portions of the interpolated string that overlap the selection. + if (interpolatedContent.Span.OverlapsWith(selectionSpan)) { - // Only consider portions of the interpolated string that overlap the selection. - if (interpolatedContent.Span.OverlapsWith(selectionSpan)) + if (interpolatedContent is InterpolationSyntax interpolation) { - if (interpolatedContent is InterpolationSyntax interpolation) + // If the user copies a portion of an interpolation, just treat this as a non-smart copy paste + // for simplicity. + if (!selectionSpan.Contains(interpolation.Span)) + return null; + + // The format clause needs to be written differently depending on what sort of interpolated + // string we have (normal, verbatim, raw). So grab the token for it and determine it's actual + // interpreted value so we can paste it properly at the destination side. + var formatClause = (string?)null; + if (interpolation.FormatClause != null && + !TryGetNormalizedStringForSpan(virtualCharService, interpolation.FormatClause.FormatStringToken, selectionSpan, out formatClause)) { - // If the user copies a portion of an interpolation, just treat this as a non-smart copy paste - // for simplicity. - if (!selectionSpan.Contains(interpolation.Span)) - return null; - - // The format clause needs to be written differently depending on what sort of interpolated - // string we have (normal, verbatim, raw). So grab the token for it and determine it's actual - // interpreted value so we can paste it properly at the destination side. - var formatClause = (string?)null; - if (interpolation.FormatClause != null && - !TryGetNormalizedStringForSpan(virtualCharService, interpolation.FormatClause.FormatStringToken, selectionSpan, out formatClause)) - { - return null; - } - - // Can grab the expression and alignment-clause as is. That's just normal C# code, and will - // remain the same no matter what we past into. - result.Add(StringCopyPasteContent.ForInterpolation( - interpolation.Expression.ToFullString(), - interpolation.AlignmentClause?.ToFullString(), - formatClause)); + return null; } - else if (interpolatedContent is InterpolatedStringTextSyntax stringText) - { - if (!TryGetContentForSpan(virtualCharService, stringText.TextToken, selectionSpan, out var content)) - return null; - result.Add(content); - } + // Can grab the expression and alignment-clause as is. That's just normal C# code, and will + // remain the same no matter what we past into. + result.Add(StringCopyPasteContent.ForInterpolation( + interpolation.Expression.ToFullString(), + interpolation.AlignmentClause?.ToFullString(), + formatClause)); } - } + else if (interpolatedContent is InterpolatedStringTextSyntax stringText) + { + if (!TryGetContentForSpan(virtualCharService, stringText.TextToken, selectionSpan, out var content)) + return null; - return new StringCopyPasteData(result.ToImmutable()); + result.Add(content); + } + } } + + return new StringCopyPasteData(result.ToImmutable()); } } diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs index 51f1e078db963..1d90e23e29aab 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs @@ -19,555 +19,554 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste +namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste; + +internal static class StringCopyPasteHelpers { - internal static class StringCopyPasteHelpers + public static bool HasNewLine(TextLine line) + => line.Span.End != line.SpanIncludingLineBreak.End; + + /// + /// Gets the character at the requested position, or \0 if out of bounds. + /// + public static char SafeCharAt(SourceText text, int index) + => index >= 0 && index < text.Length ? text[index] : '\0'; + + /// + /// True if the string literal contains an error diagnostic that indicates a parsing problem with it. For + /// interpolated strings, this only includes the text sections, and not any interpolation holes in the literal. + /// + public static bool ContainsError(ExpressionSyntax stringExpression) { - public static bool HasNewLine(TextLine line) - => line.Span.End != line.SpanIncludingLineBreak.End; - - /// - /// Gets the character at the requested position, or \0 if out of bounds. - /// - public static char SafeCharAt(SourceText text, int index) - => index >= 0 && index < text.Length ? text[index] : '\0'; - - /// - /// True if the string literal contains an error diagnostic that indicates a parsing problem with it. For - /// interpolated strings, this only includes the text sections, and not any interpolation holes in the literal. - /// - public static bool ContainsError(ExpressionSyntax stringExpression) - { - if (stringExpression is LiteralExpressionSyntax) - return NodeOrTokenContainsError(stringExpression); + if (stringExpression is LiteralExpressionSyntax) + return NodeOrTokenContainsError(stringExpression); - if (stringExpression is InterpolatedStringExpressionSyntax interpolatedString) + if (stringExpression is InterpolatedStringExpressionSyntax interpolatedString) + { + using var _ = PooledHashSet.GetInstance(out var errors); + foreach (var diagnostic in interpolatedString.GetDiagnostics()) { - using var _ = PooledHashSet.GetInstance(out var errors); - foreach (var diagnostic in interpolatedString.GetDiagnostics()) - { - if (diagnostic.Severity == DiagnosticSeverity.Error) - errors.Add(diagnostic); - } + if (diagnostic.Severity == DiagnosticSeverity.Error) + errors.Add(diagnostic); + } - // we don't care about errors in holes. Only errors in the content portions of the string. - for (int i = 0, n = interpolatedString.Contents.Count; i < n && errors.Count > 0; i++) + // we don't care about errors in holes. Only errors in the content portions of the string. + for (int i = 0, n = interpolatedString.Contents.Count; i < n && errors.Count > 0; i++) + { + if (interpolatedString.Contents[i] is InterpolatedStringTextSyntax text) { - if (interpolatedString.Contents[i] is InterpolatedStringTextSyntax text) - { - foreach (var diagnostic in text.GetDiagnostics()) - errors.Remove(diagnostic); - } + foreach (var diagnostic in text.GetDiagnostics()) + errors.Remove(diagnostic); } - - return errors.Count > 0; } - throw ExceptionUtilities.UnexpectedValue(stringExpression); + return errors.Count > 0; } - public static bool NodeOrTokenContainsError(SyntaxNodeOrToken nodeOrToken) - { - foreach (var diagnostic in nodeOrToken.GetDiagnostics()) - { - if (diagnostic.Severity == DiagnosticSeverity.Error) - return true; - } + throw ExceptionUtilities.UnexpectedValue(stringExpression); + } - return false; + public static bool NodeOrTokenContainsError(SyntaxNodeOrToken nodeOrToken) + { + foreach (var diagnostic in nodeOrToken.GetDiagnostics()) + { + if (diagnostic.Severity == DiagnosticSeverity.Error) + return true; } - public static bool AllWhitespace(INormalizedTextChangeCollection changes) - { - foreach (var change in changes) - { - if (!AllWhitespace(change.NewText)) - return false; - } + return false; + } - return true; + public static bool AllWhitespace(INormalizedTextChangeCollection changes) + { + foreach (var change in changes) + { + if (!AllWhitespace(change.NewText)) + return false; } - private static bool AllWhitespace(string text) - { - foreach (var ch in text) - { - if (!SyntaxFacts.IsWhitespace(ch)) - return false; - } + return true; + } - return true; + private static bool AllWhitespace(string text) + { + foreach (var ch in text) + { + if (!SyntaxFacts.IsWhitespace(ch)) + return false; } - /// - /// Given a TextLine, returns the index (in the SourceText) of the first character of it that is not a - /// Whitespace character. The LineBreak parts of the line are not considered here. If the line is empty/blank - /// (again, not counting LineBreak characters) then -1 is returned. - /// - public static int GetFirstNonWhitespaceIndex(SourceText text, TextLine line) - { - for (int i = line.Start, n = line.End; i < n; i++) - { - if (!SyntaxFacts.IsWhitespace(text[i])) - return i; - } + return true; + } - return -1; + /// + /// Given a TextLine, returns the index (in the SourceText) of the first character of it that is not a + /// Whitespace character. The LineBreak parts of the line are not considered here. If the line is empty/blank + /// (again, not counting LineBreak characters) then -1 is returned. + /// + public static int GetFirstNonWhitespaceIndex(SourceText text, TextLine line) + { + for (int i = line.Start, n = line.End; i < n; i++) + { + if (!SyntaxFacts.IsWhitespace(text[i])) + return i; } - public static bool ContainsControlCharacter(INormalizedTextChangeCollection changes) - { - foreach (var change in changes) - { - if (ContainsControlCharacter(change.NewText)) - return true; - } + return -1; + } - return false; + public static bool ContainsControlCharacter(INormalizedTextChangeCollection changes) + { + foreach (var change in changes) + { + if (ContainsControlCharacter(change.NewText)) + return true; } - public static bool ContainsControlCharacter(string newText) - { - foreach (var c in newText) - { - if (char.IsControl(c)) - return true; - } + return false; + } - return false; + public static bool ContainsControlCharacter(string newText) + { + foreach (var c in newText) + { + if (char.IsControl(c)) + return true; } - /// - /// Removes all characters matching from the start of . - /// - public static (string whitespace, string contents) ExtractWhitespace(string value) - { - var start = 0; - while (start < value.Length && SyntaxFacts.IsWhitespace(value[start])) - start++; + return false; + } - return (value[..start], value[start..]); - } + /// + /// Removes all characters matching from the start of . + /// + public static (string whitespace, string contents) ExtractWhitespace(string value) + { + var start = 0; + while (start < value.Length && SyntaxFacts.IsWhitespace(value[start])) + start++; - public static bool IsVerbatimStringExpression(ExpressionSyntax stringExpression) - => stringExpression is LiteralExpressionSyntax literalExpression && literalExpression.Token.IsVerbatimStringLiteral() || - stringExpression is InterpolatedStringExpressionSyntax { StringStartToken.RawKind: (int)SyntaxKind.InterpolatedVerbatimStringStartToken }; + return (value[..start], value[start..]); + } - public static bool IsAnyRawStringExpression(ExpressionSyntax expression) - => expression is LiteralExpressionSyntax literal - ? IsRawStringLiteral(literal) - : IsRawStringLiteral((InterpolatedStringExpressionSyntax)expression); + public static bool IsVerbatimStringExpression(ExpressionSyntax stringExpression) + => stringExpression is LiteralExpressionSyntax literalExpression && literalExpression.Token.IsVerbatimStringLiteral() || + stringExpression is InterpolatedStringExpressionSyntax { StringStartToken.RawKind: (int)SyntaxKind.InterpolatedVerbatimStringStartToken }; - public static bool IsAnyMultiLineRawStringExpression(ExpressionSyntax expression) - => expression is LiteralExpressionSyntax { Token.RawKind: (int)SyntaxKind.MultiLineRawStringLiteralToken } or - InterpolatedStringExpressionSyntax { StringStartToken.RawKind: (int)SyntaxKind.InterpolatedMultiLineRawStringStartToken }; + public static bool IsAnyRawStringExpression(ExpressionSyntax expression) + => expression is LiteralExpressionSyntax literal + ? IsRawStringLiteral(literal) + : IsRawStringLiteral((InterpolatedStringExpressionSyntax)expression); - public static bool IsRawStringLiteral(InterpolatedStringExpressionSyntax interpolatedString) - => interpolatedString.StringStartToken.Kind() is SyntaxKind.InterpolatedSingleLineRawStringStartToken or SyntaxKind.InterpolatedMultiLineRawStringStartToken; + public static bool IsAnyMultiLineRawStringExpression(ExpressionSyntax expression) + => expression is LiteralExpressionSyntax { Token.RawKind: (int)SyntaxKind.MultiLineRawStringLiteralToken } or + InterpolatedStringExpressionSyntax { StringStartToken.RawKind: (int)SyntaxKind.InterpolatedMultiLineRawStringStartToken }; - public static bool IsRawStringLiteral(LiteralExpressionSyntax literal) - => literal.Token.Kind() is SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.MultiLineRawStringLiteralToken; + public static bool IsRawStringLiteral(InterpolatedStringExpressionSyntax interpolatedString) + => interpolatedString.StringStartToken.Kind() is SyntaxKind.InterpolatedSingleLineRawStringStartToken or SyntaxKind.InterpolatedMultiLineRawStringStartToken; - public static int SkipU8Suffix(SourceText text, int end) - { - if (SafeCharAt(text, end - 1) == '8') - end--; - if (SafeCharAt(text, end - 1) is 'u' or 'U') - end--; - return end; - } + public static bool IsRawStringLiteral(LiteralExpressionSyntax literal) + => literal.Token.Kind() is SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.MultiLineRawStringLiteralToken; - /// - /// Given a section of a document, finds the longest sequence of quote (") characters in it. Used to - /// determine if a raw string literal needs to grow its delimiters to ensure that the quote sequence will no - /// longer be a problem. - /// - public static int GetLongestQuoteSequence(SourceText text, TextSpan span) - => GetLongestCharacterSequence(text, span, '"'); - - public static int GetLongestOpenBraceSequence(SourceText text, TextSpan span) - => GetLongestCharacterSequence(text, span, '{'); - - public static int GetLongestCloseBraceSequence(SourceText text, TextSpan span) - => GetLongestCharacterSequence(text, span, '}'); - - /// - /// Given a section of a document, finds the longest sequence of of a given in it. - /// Used to determine if a raw string literal needs to grow its delimiters to ensure that the sequence - /// will no longer be a problem. - /// - private static int GetLongestCharacterSequence(SourceText text, TextSpan span, char character) + public static int SkipU8Suffix(SourceText text, int end) + { + if (SafeCharAt(text, end - 1) == '8') + end--; + if (SafeCharAt(text, end - 1) is 'u' or 'U') + end--; + return end; + } + + /// + /// Given a section of a document, finds the longest sequence of quote (") characters in it. Used to + /// determine if a raw string literal needs to grow its delimiters to ensure that the quote sequence will no + /// longer be a problem. + /// + public static int GetLongestQuoteSequence(SourceText text, TextSpan span) + => GetLongestCharacterSequence(text, span, '"'); + + public static int GetLongestOpenBraceSequence(SourceText text, TextSpan span) + => GetLongestCharacterSequence(text, span, '{'); + + public static int GetLongestCloseBraceSequence(SourceText text, TextSpan span) + => GetLongestCharacterSequence(text, span, '}'); + + /// + /// Given a section of a document, finds the longest sequence of of a given in it. + /// Used to determine if a raw string literal needs to grow its delimiters to ensure that the sequence + /// will no longer be a problem. + /// + private static int GetLongestCharacterSequence(SourceText text, TextSpan span, char character) + { + var longestCount = 0; + for (int currentIndex = span.Start, contentEnd = span.End; currentIndex < contentEnd;) { - var longestCount = 0; - for (int currentIndex = span.Start, contentEnd = span.End; currentIndex < contentEnd;) + if (text[currentIndex] == character) { - if (text[currentIndex] == character) - { - var endQuoteIndex = currentIndex; - while (endQuoteIndex < contentEnd && text[endQuoteIndex] == character) - endQuoteIndex++; + var endQuoteIndex = currentIndex; + while (endQuoteIndex < contentEnd && text[endQuoteIndex] == character) + endQuoteIndex++; - longestCount = Math.Max(longestCount, endQuoteIndex - currentIndex); - currentIndex = endQuoteIndex; - } - else - { - currentIndex++; - } + longestCount = Math.Max(longestCount, endQuoteIndex - currentIndex); + currentIndex = endQuoteIndex; + } + else + { + currentIndex++; } - - return longestCount; } - /// - /// Given a set of selections, finds the innermost string-literal/interpolation that they are all contained in. - /// If no such literal/interpolation exists, this returns null. - /// - public static ExpressionSyntax? FindCommonContainingStringExpression( - SyntaxNode root, NormalizedSnapshotSpanCollection selectionsBeforePaste) - { - ExpressionSyntax? expression = null; - foreach (var snapshotSpan in selectionsBeforePaste) - { - var container = FindContainingSupportedStringExpression(root, snapshotSpan.Start.Position); - if (container == null) - return null; + return longestCount; + } - expression ??= container; - if (expression != container) - return null; - } + /// + /// Given a set of selections, finds the innermost string-literal/interpolation that they are all contained in. + /// If no such literal/interpolation exists, this returns null. + /// + public static ExpressionSyntax? FindCommonContainingStringExpression( + SyntaxNode root, NormalizedSnapshotSpanCollection selectionsBeforePaste) + { + ExpressionSyntax? expression = null; + foreach (var snapshotSpan in selectionsBeforePaste) + { + var container = FindContainingSupportedStringExpression(root, snapshotSpan.Start.Position); + if (container == null) + return null; - return expression; + expression ??= container; + if (expression != container) + return null; } - public static ExpressionSyntax? FindContainingSupportedStringExpression(SyntaxNode root, int position) - { - var node = root.FindToken(position).Parent; - for (var current = node; current != null; current = current.Parent) - { - if (current is LiteralExpressionSyntax literalExpression) - return IsSupportedStringExpression(literalExpression) ? literalExpression : null; + return expression; + } - if (current is InterpolatedStringExpressionSyntax interpolatedString) - return IsSupportedStringExpression(interpolatedString) ? interpolatedString : null; - } + public static ExpressionSyntax? FindContainingSupportedStringExpression(SyntaxNode root, int position) + { + var node = root.FindToken(position).Parent; + for (var current = node; current != null; current = current.Parent) + { + if (current is LiteralExpressionSyntax literalExpression) + return IsSupportedStringExpression(literalExpression) ? literalExpression : null; - return null; + if (current is InterpolatedStringExpressionSyntax interpolatedString) + return IsSupportedStringExpression(interpolatedString) ? interpolatedString : null; } - public static bool IsSupportedStringExpression(ExpressionSyntax expression) - { - // When new string forms are added, support for them can be introduced here. However, by checking the exact - // types of strings supported, downstream code can know exactly what forms they should be looking for and - // that nothing else may flow down to them. + return null; + } - if (expression is LiteralExpressionSyntax - { - RawKind: (int)SyntaxKind.StringLiteralExpression, - Token.RawKind: (int)SyntaxKind.StringLiteralToken or - (int)SyntaxKind.SingleLineRawStringLiteralToken or - (int)SyntaxKind.MultiLineRawStringLiteralToken, - }) - { - return true; - } + public static bool IsSupportedStringExpression(ExpressionSyntax expression) + { + // When new string forms are added, support for them can be introduced here. However, by checking the exact + // types of strings supported, downstream code can know exactly what forms they should be looking for and + // that nothing else may flow down to them. - if (expression is InterpolatedStringExpressionSyntax - { - StringStartToken.RawKind: (int)SyntaxKind.InterpolatedStringStartToken or - (int)SyntaxKind.InterpolatedVerbatimStringStartToken or - (int)SyntaxKind.InterpolatedSingleLineRawStringStartToken or - (int)SyntaxKind.InterpolatedMultiLineRawStringStartToken, - }) + if (expression is LiteralExpressionSyntax { - return true; - } - - return false; + RawKind: (int)SyntaxKind.StringLiteralExpression, + Token.RawKind: (int)SyntaxKind.StringLiteralToken or + (int)SyntaxKind.SingleLineRawStringLiteralToken or + (int)SyntaxKind.MultiLineRawStringLiteralToken, + }) + { + return true; } - public static string EscapeForNonRawStringLiteral_DoNotCallDirectly(bool isVerbatim, bool isInterpolated, bool trySkipExistingEscapes, string value) + if (expression is InterpolatedStringExpressionSyntax + { + StringStartToken.RawKind: (int)SyntaxKind.InterpolatedStringStartToken or + (int)SyntaxKind.InterpolatedVerbatimStringStartToken or + (int)SyntaxKind.InterpolatedSingleLineRawStringStartToken or + (int)SyntaxKind.InterpolatedMultiLineRawStringStartToken, + }) { - if (isVerbatim) - return EscapeForNonRawVerbatimStringLiteral(isInterpolated, trySkipExistingEscapes, value); + return true; + } - // Standard strings have a much larger set of cases to consider. - using var _ = PooledStringBuilder.GetInstance(out var builder); + return false; + } - // taken from object-display - for (var i = 0; i < value.Length; i++) - { - var ch = value[i]; - var nextCh = i == value.Length - 1 ? 0 : value[i + 1]; + public static string EscapeForNonRawStringLiteral_DoNotCallDirectly(bool isVerbatim, bool isInterpolated, bool trySkipExistingEscapes, string value) + { + if (isVerbatim) + return EscapeForNonRawVerbatimStringLiteral(isInterpolated, trySkipExistingEscapes, value); - if (CharUnicodeInfo.GetUnicodeCategory(ch) == UnicodeCategory.Surrogate) + // Standard strings have a much larger set of cases to consider. + using var _ = PooledStringBuilder.GetInstance(out var builder); + + // taken from object-display + for (var i = 0; i < value.Length; i++) + { + var ch = value[i]; + var nextCh = i == value.Length - 1 ? 0 : value[i + 1]; + + if (CharUnicodeInfo.GetUnicodeCategory(ch) == UnicodeCategory.Surrogate) + { + var category = CharUnicodeInfo.GetUnicodeCategory(value, i); + if (category == UnicodeCategory.Surrogate) { - var category = CharUnicodeInfo.GetUnicodeCategory(value, i); - if (category == UnicodeCategory.Surrogate) - { - // an unpaired surrogate - builder.Append("\\u" + ((int)ch).ToString("x4")); - } - else if (NeedsEscaping(category)) - { - // a surrogate pair that needs to be escaped - var unicode = char.ConvertToUtf32(value, i); - builder.Append("\\U" + unicode.ToString("x8")); - i++; // skip the already-encoded second surrogate of the pair - } - else - { - // copy a printable surrogate pair directly - builder.Append(ch); - builder.Append(value[++i]); - } + // an unpaired surrogate + builder.Append("\\u" + ((int)ch).ToString("x4")); } - else if (TryReplaceChar(ch, out var replaceWith)) + else if (NeedsEscaping(category)) { - builder.Append(replaceWith); + // a surrogate pair that needs to be escaped + var unicode = char.ConvertToUtf32(value, i); + builder.Append("\\U" + unicode.ToString("x8")); + i++; // skip the already-encoded second surrogate of the pair } else { + // copy a printable surrogate pair directly builder.Append(ch); - - // if we see a special character then skip the following one if the following one already escapes it. - // Otherwise, if it's not already escaped, then escape it. - if (isInterpolated && ch is '{' or '}') - { - if (trySkipExistingEscapes && nextCh == ch) - i++; - else - builder.Append(ch); - } + builder.Append(value[++i]); } } - - return builder.ToString(); - - static bool TryReplaceChar(char c, [NotNullWhen(true)] out string? replaceWith) + else if (TryReplaceChar(ch, out var replaceWith)) { - replaceWith = null; - switch (c) + builder.Append(replaceWith); + } + else + { + builder.Append(ch); + + // if we see a special character then skip the following one if the following one already escapes it. + // Otherwise, if it's not already escaped, then escape it. + if (isInterpolated && ch is '{' or '}') { - case '\\': - replaceWith = "\\\\"; - break; - case '\0': - replaceWith = "\\0"; - break; - case '\a': - replaceWith = "\\a"; - break; - case '\b': - replaceWith = "\\b"; - break; - case '\f': - replaceWith = "\\f"; - break; - case '\n': - replaceWith = "\\n"; - break; - case '\r': - replaceWith = "\\r"; - break; - case '\t': - replaceWith = "\\t"; - break; - case '\v': - replaceWith = "\\v"; - break; - case '"': - replaceWith = "\\\""; - break; + if (trySkipExistingEscapes && nextCh == ch) + i++; + else + builder.Append(ch); } + } + } - if (replaceWith != null) - return true; + return builder.ToString(); - if (NeedsEscaping(CharUnicodeInfo.GetUnicodeCategory(c))) - { - replaceWith = "\\u" + ((int)c).ToString("x4"); - return true; - } + static bool TryReplaceChar(char c, [NotNullWhen(true)] out string? replaceWith) + { + replaceWith = null; + switch (c) + { + case '\\': + replaceWith = "\\\\"; + break; + case '\0': + replaceWith = "\\0"; + break; + case '\a': + replaceWith = "\\a"; + break; + case '\b': + replaceWith = "\\b"; + break; + case '\f': + replaceWith = "\\f"; + break; + case '\n': + replaceWith = "\\n"; + break; + case '\r': + replaceWith = "\\r"; + break; + case '\t': + replaceWith = "\\t"; + break; + case '\v': + replaceWith = "\\v"; + break; + case '"': + replaceWith = "\\\""; + break; + } - return false; + if (replaceWith != null) + return true; + + if (NeedsEscaping(CharUnicodeInfo.GetUnicodeCategory(c))) + { + replaceWith = "\\u" + ((int)c).ToString("x4"); + return true; } - static bool NeedsEscaping(UnicodeCategory category) + return false; + } + + static bool NeedsEscaping(UnicodeCategory category) + { + switch (category) { - switch (category) - { - case UnicodeCategory.Control: - case UnicodeCategory.OtherNotAssigned: - case UnicodeCategory.ParagraphSeparator: - case UnicodeCategory.LineSeparator: - case UnicodeCategory.Surrogate: - return true; - default: - return false; - } + case UnicodeCategory.Control: + case UnicodeCategory.OtherNotAssigned: + case UnicodeCategory.ParagraphSeparator: + case UnicodeCategory.LineSeparator: + case UnicodeCategory.Surrogate: + return true; + default: + return false; } } + } + + private static string EscapeForNonRawVerbatimStringLiteral(bool isInterpolated, bool trySkipExistingEscapes, string value) + { + using var _ = PooledStringBuilder.GetInstance(out var builder); + + // First, go through and see if we're escaping *anything* in the original. If so, then we'll escape + // everything. In other words, say we're pasting `[SuppressMessage("", "CA2013")]`. We technically don't + // need to escape the `""` (since that is legal in a verbatim string). However, we will be escaping the + // quotes in teh `"CA2013"` to become `""CA2013""`. Once we decide we're escaping some quotes, we should + // then realize that we *should* escape the `""` to `""""` to be consistent. - private static string EscapeForNonRawVerbatimStringLiteral(bool isInterpolated, bool trySkipExistingEscapes, string value) + // So if we determine that we will be escaping all code, then just recurse, this time setting + // trySkipExistingEscapes to false. That will prevent calling back into this check and it means whatever we + // run into we will escape. + if (trySkipExistingEscapes && WillEscapeAnyCharacters(isInterpolated, value)) + return EscapeForNonRawVerbatimStringLiteral(isInterpolated, trySkipExistingEscapes: false, value); + + for (var i = 0; i < value.Length; i++) { - using var _ = PooledStringBuilder.GetInstance(out var builder); + var ch = value[i]; + var nextCh = i == value.Length - 1 ? 0 : value[i + 1]; + + builder.Append(ch); + + // if we see a special character then skip the following one if the following one already escapes it. + // Otherwise, if it's not already escaped, then escape it. + if (ch == '"') + { + builder.Append(ch); + + if (trySkipExistingEscapes && nextCh == ch) + i++; + } + else if (isInterpolated && ch is '{' or '}') + { + builder.Append(ch); - // First, go through and see if we're escaping *anything* in the original. If so, then we'll escape - // everything. In other words, say we're pasting `[SuppressMessage("", "CA2013")]`. We technically don't - // need to escape the `""` (since that is legal in a verbatim string). However, we will be escaping the - // quotes in teh `"CA2013"` to become `""CA2013""`. Once we decide we're escaping some quotes, we should - // then realize that we *should* escape the `""` to `""""` to be consistent. + if (trySkipExistingEscapes && nextCh == ch) + i++; + } + } - // So if we determine that we will be escaping all code, then just recurse, this time setting - // trySkipExistingEscapes to false. That will prevent calling back into this check and it means whatever we - // run into we will escape. - if (trySkipExistingEscapes && WillEscapeAnyCharacters(isInterpolated, value)) - return EscapeForNonRawVerbatimStringLiteral(isInterpolated, trySkipExistingEscapes: false, value); + return builder.ToString(); + static bool WillEscapeAnyCharacters(bool isInterpolated, string value) + { for (var i = 0; i < value.Length; i++) { var ch = value[i]; var nextCh = i == value.Length - 1 ? 0 : value[i + 1]; - builder.Append(ch); - - // if we see a special character then skip the following one if the following one already escapes it. - // Otherwise, if it's not already escaped, then escape it. if (ch == '"') { - builder.Append(ch); + // we have an isolated quote. we will need to escape it (and thus should escape everything in + // the string. + if (nextCh != ch) + return true; - if (trySkipExistingEscapes && nextCh == ch) - i++; + // Quotes are paired. This is already escaped fine. Skip both quotes. + i++; } else if (isInterpolated && ch is '{' or '}') { - builder.Append(ch); - - if (trySkipExistingEscapes && nextCh == ch) - i++; - } - } - - return builder.ToString(); - - static bool WillEscapeAnyCharacters(bool isInterpolated, string value) - { - for (var i = 0; i < value.Length; i++) - { - var ch = value[i]; - var nextCh = i == value.Length - 1 ? 0 : value[i + 1]; - - if (ch == '"') - { - // we have an isolated quote. we will need to escape it (and thus should escape everything in - // the string. - if (nextCh != ch) - return true; - - // Quotes are paired. This is already escaped fine. Skip both quotes. - i++; - } - else if (isInterpolated && ch is '{' or '}') - { - // we have an isolated brace. we will need to escape it (and thus should escape everything in - // the string. - if (nextCh != ch) - return true; - - // Braces are paired. This is already escaped fine. Skip both braces. - i++; - } + // we have an isolated brace. we will need to escape it (and thus should escape everything in + // the string. + if (nextCh != ch) + return true; - // continue looking forward. + // Braces are paired. This is already escaped fine. Skip both braces. + i++; } - return false; + // continue looking forward. } + + return false; } + } - /// - /// Given a set of source text lines, determines what common whitespace prefix each line has. Note that this - /// does *not* include the first line as it's super common for someone to copy a set of lines while only - /// starting the selection at the start of the content on the first line. This also does not include empty - /// lines as they're also very common, but are clearly not a way of indicating indentation indent for the normal - /// lines. - /// - public static string? GetCommonIndentationPrefix(INormalizedTextChangeCollection textChanges) - { - string? commonIndentPrefix = null; - var first = true; + /// + /// Given a set of source text lines, determines what common whitespace prefix each line has. Note that this + /// does *not* include the first line as it's super common for someone to copy a set of lines while only + /// starting the selection at the start of the content on the first line. This also does not include empty + /// lines as they're also very common, but are clearly not a way of indicating indentation indent for the normal + /// lines. + /// + public static string? GetCommonIndentationPrefix(INormalizedTextChangeCollection textChanges) + { + string? commonIndentPrefix = null; + var first = true; - foreach (var change in textChanges) + foreach (var change in textChanges) + { + var text = SourceText.From(change.NewText); + foreach (var line in text.Lines) { - var text = SourceText.From(change.NewText); - foreach (var line in text.Lines) + if (first) { - if (first) - { - first = false; - continue; - } - - var nonWhitespaceIndex = GetFirstNonWhitespaceIndex(text, line); - if (nonWhitespaceIndex >= 0) - commonIndentPrefix = GetCommonIndentationPrefix(commonIndentPrefix, text, TextSpan.FromBounds(line.Start, nonWhitespaceIndex)); + first = false; + continue; } - } - return commonIndentPrefix; + var nonWhitespaceIndex = GetFirstNonWhitespaceIndex(text, line); + if (nonWhitespaceIndex >= 0) + commonIndentPrefix = GetCommonIndentationPrefix(commonIndentPrefix, text, TextSpan.FromBounds(line.Start, nonWhitespaceIndex)); + } } - private static string? GetCommonIndentationPrefix(string? commonIndentPrefix, SourceText text, TextSpan lineWhitespaceSpan) - { - // first line with indentation whitespace we're seeing. Just keep track of that. - if (commonIndentPrefix == null) - return text.ToString(lineWhitespaceSpan); - - // we have indentation whitespace from a previous line. Figure out the max commonality between it and the - // line we're currently looking at. - var commonPrefixLength = 0; - for (var n = Math.Min(commonIndentPrefix.Length, lineWhitespaceSpan.Length); commonPrefixLength < n; commonPrefixLength++) - { - if (commonIndentPrefix[commonPrefixLength] != text[lineWhitespaceSpan.Start + commonPrefixLength]) - break; - } + return commonIndentPrefix; + } - return commonIndentPrefix[..commonPrefixLength]; + private static string? GetCommonIndentationPrefix(string? commonIndentPrefix, SourceText text, TextSpan lineWhitespaceSpan) + { + // first line with indentation whitespace we're seeing. Just keep track of that. + if (commonIndentPrefix == null) + return text.ToString(lineWhitespaceSpan); + + // we have indentation whitespace from a previous line. Figure out the max commonality between it and the + // line we're currently looking at. + var commonPrefixLength = 0; + for (var n = Math.Min(commonIndentPrefix.Length, lineWhitespaceSpan.Length); commonPrefixLength < n; commonPrefixLength++) + { + if (commonIndentPrefix[commonPrefixLength] != text[lineWhitespaceSpan.Start + commonPrefixLength]) + break; } - public static TextSpan MapSpan(TextSpan span, ITextSnapshot from, ITextSnapshot to) - => from.CreateTrackingSpan(span.ToSpan(), SpanTrackingMode.EdgeInclusive).GetSpan(to).Span.ToTextSpan(); + return commonIndentPrefix[..commonPrefixLength]; + } - public static bool RawContentMustBeMultiLine(SourceText text, ImmutableArray spans) - { - Contract.ThrowIfTrue(spans.Length == 0); + public static TextSpan MapSpan(TextSpan span, ITextSnapshot from, ITextSnapshot to) + => from.CreateTrackingSpan(span.ToSpan(), SpanTrackingMode.EdgeInclusive).GetSpan(to).Span.ToTextSpan(); - // Empty raw string must be multiline. - if (spans is [{ IsEmpty: true }]) - return true; + public static bool RawContentMustBeMultiLine(SourceText text, ImmutableArray spans) + { + Contract.ThrowIfTrue(spans.Length == 0); - // Or if it starts/ends with a quote - if (spans.First().Length > 0 && text[spans.First().Start] == '"') - return true; + // Empty raw string must be multiline. + if (spans is [{ IsEmpty: true }]) + return true; - if (spans.Last().Length > 0 && text[spans.Last().End - 1] == '"') - return true; + // Or if it starts/ends with a quote + if (spans.First().Length > 0 && text[spans.First().Start] == '"') + return true; + + if (spans.Last().Length > 0 && text[spans.Last().End - 1] == '"') + return true; - // or contains a newline - foreach (var span in spans) + // or contains a newline + foreach (var span in spans) + { + for (var i = span.Start; i < span.End; i++) { - for (var i = span.Start; i < span.End; i++) - { - if (SyntaxFacts.IsNewLine(text[i])) - return true; - } + if (SyntaxFacts.IsNewLine(text[i])) + return true; } - - return false; } + + return false; } } diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/UnknownSourcePasteProcessor.cs b/src/EditorFeatures/CSharp/StringCopyPaste/UnknownSourcePasteProcessor.cs index 31bcebb78f8d8..2ad2bb0331962 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/UnknownSourcePasteProcessor.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/UnknownSourcePasteProcessor.cs @@ -17,321 +17,320 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste +namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste; + +using static StringCopyPasteHelpers; + +/// +/// Paste processor responsible for determining how text should be treated if it came from a source outside of the +/// editor we're in. In that case, we don't know what any particular piece of text means. For example, \t +/// might be a tab or it could be the literal two characters \ and t. +/// +internal sealed class UnknownSourcePasteProcessor( + string newLine, + string indentationWhitespace, + ITextSnapshot snapshotBeforePaste, + ITextSnapshot snapshotAfterPaste, + Document documentBeforePaste, + Document documentAfterPaste, + ExpressionSyntax stringExpressionBeforePaste, + bool pasteWasSuccessful) : AbstractPasteProcessor(newLine, indentationWhitespace, snapshotBeforePaste, snapshotAfterPaste, documentBeforePaste, documentAfterPaste, stringExpressionBeforePaste) { - using static StringCopyPasteHelpers; - /// - /// Paste processor responsible for determining how text should be treated if it came from a source outside of the - /// editor we're in. In that case, we don't know what any particular piece of text means. For example, \t - /// might be a tab or it could be the literal two characters \ and t. + /// Whether or not the string expression remained successfully parseable after the paste. . If it can still be successfully parsed subclasses + /// can adjust their view on which pieces of content need to be escaped or not. /// - internal sealed class UnknownSourcePasteProcessor( - string newLine, - string indentationWhitespace, - ITextSnapshot snapshotBeforePaste, - ITextSnapshot snapshotAfterPaste, - Document documentBeforePaste, - Document documentAfterPaste, - ExpressionSyntax stringExpressionBeforePaste, - bool pasteWasSuccessful) : AbstractPasteProcessor(newLine, indentationWhitespace, snapshotBeforePaste, snapshotAfterPaste, documentBeforePaste, documentAfterPaste, stringExpressionBeforePaste) + private readonly bool _pasteWasSuccessful = pasteWasSuccessful; + + public override ImmutableArray GetEdits() { - /// - /// Whether or not the string expression remained successfully parseable after the paste. . If it can still be successfully parsed subclasses - /// can adjust their view on which pieces of content need to be escaped or not. - /// - private readonly bool _pasteWasSuccessful = pasteWasSuccessful; - - public override ImmutableArray GetEdits() + // If we have a raw-string, then we always want to check for changes to make, even if the paste was + // technically legal. This is because we may want to touch up things like indentation to make the + // pasted text look good for raw strings. + // + // Check for certain things we always think we should escape. + if (!IsAnyRawStringExpression(StringExpressionBeforePaste) && !ShouldAlwaysEscapeTextForNonRawString()) { - // If we have a raw-string, then we always want to check for changes to make, even if the paste was - // technically legal. This is because we may want to touch up things like indentation to make the - // pasted text look good for raw strings. - // - // Check for certain things we always think we should escape. - if (!IsAnyRawStringExpression(StringExpressionBeforePaste) && !ShouldAlwaysEscapeTextForNonRawString()) - { - // If the pasting was successful, then no need to change anything. - if (_pasteWasSuccessful) - return default; - } + // If the pasting was successful, then no need to change anything. + if (_pasteWasSuccessful) + return default; + } - // Ok, the user pasted text that couldn't cleanly be added to this token without issue. Repaste the - // contents, but this time properly escaped/manipulated so that it follows the rule of the particular token - // kind. + // Ok, the user pasted text that couldn't cleanly be added to this token without issue. Repaste the + // contents, but this time properly escaped/manipulated so that it follows the rule of the particular token + // kind. - // For pastes into non-raw strings, we can just determine how the change should be escaped in-line at that - // same location the paste originally happened at. For raw-strings things get more complex as we have to - // deal with things like indentation and potentially adding newlines to make things legal. - return IsAnyRawStringExpression(StringExpressionBeforePaste) - ? GetEditsForRawString() - : GetEditsForNonRawString(); - } + // For pastes into non-raw strings, we can just determine how the change should be escaped in-line at that + // same location the paste originally happened at. For raw-strings things get more complex as we have to + // deal with things like indentation and potentially adding newlines to make things legal. + return IsAnyRawStringExpression(StringExpressionBeforePaste) + ? GetEditsForRawString() + : GetEditsForNonRawString(); + } + + private string EscapeForNonRawStringLiteral(string value) + => EscapeForNonRawStringLiteral_DoNotCallDirectly( + IsVerbatimStringExpression(StringExpressionBeforePaste), + StringExpressionBeforePaste is InterpolatedStringExpressionSyntax, + // We do not want to try skipping escapes in the 'value'. We don't know where it came from, and if it + // had some escapes in it, it's probably a good idea to remove to keep the final pasted text clean. + trySkipExistingEscapes: true, + value); - private string EscapeForNonRawStringLiteral(string value) - => EscapeForNonRawStringLiteral_DoNotCallDirectly( - IsVerbatimStringExpression(StringExpressionBeforePaste), - StringExpressionBeforePaste is InterpolatedStringExpressionSyntax, - // We do not want to try skipping escapes in the 'value'. We don't know where it came from, and if it - // had some escapes in it, it's probably a good idea to remove to keep the final pasted text clean. - trySkipExistingEscapes: true, - value); + private bool ShouldAlwaysEscapeTextForNonRawString() + { + // Pasting a control character into a normal string literal is normally not desired. So even if this + // is legal, we still escape the contents to make the pasted code clear. + return !IsVerbatimStringExpression(StringExpressionBeforePaste) && ContainsControlCharacter(Changes); + } - private bool ShouldAlwaysEscapeTextForNonRawString() + private ImmutableArray GetEditsForNonRawString() + { + using var textChanges = TemporaryArray.Empty; + + foreach (var change in Changes) { - // Pasting a control character into a normal string literal is normally not desired. So even if this - // is legal, we still escape the contents to make the pasted code clear. - return !IsVerbatimStringExpression(StringExpressionBeforePaste) && ContainsControlCharacter(Changes); + // We're pasting from an unknown source. If we see a viable escape in that source treat it as an escape + // instead of escaping it one more time upon paste. + textChanges.Add(new TextChange( + change.OldSpan.ToTextSpan(), + EscapeForNonRawStringLiteral(change.NewText))); } - private ImmutableArray GetEditsForNonRawString() - { - using var textChanges = TemporaryArray.Empty; + return textChanges.ToImmutableAndClear(); + } - foreach (var change in Changes) - { - // We're pasting from an unknown source. If we see a viable escape in that source treat it as an escape - // instead of escaping it one more time upon paste. - textChanges.Add(new TextChange( - change.OldSpan.ToTextSpan(), - EscapeForNonRawStringLiteral(change.NewText))); - } + private ImmutableArray GetEditsForRawString() + { + // Can't really figure anything out if the raw string is in error. + if (NodeOrTokenContainsError(StringExpressionBeforePaste)) + return default; - return textChanges.ToImmutableAndClear(); - } + // If all we're going to do is insert whitespace, then don't make any adjustments to the text. We don't want + // to end up inserting nothing and having the user very confused why their paste did nothing. + if (AllWhitespace(SnapshotBeforePaste.Version.Changes)) + return default; - private ImmutableArray GetEditsForRawString() - { - // Can't really figure anything out if the raw string is in error. - if (NodeOrTokenContainsError(StringExpressionBeforePaste)) - return default; + // if the content we're going to add itself contains quotes, then figure out how many start/end quotes the + // final string literal will need (which also gives us the number of quotes to add to the start/end). + // + // note: we don't have to do this if the paste was successful. Instead, we'll just process the contents, + // adjusting whitespace below. + var quotesToAdd = _pasteWasSuccessful ? null : GetQuotesToAddToRawString(); + var dollarSignsToAdd = _pasteWasSuccessful ? null : GetDollarSignsToAddToRawString(); - // If all we're going to do is insert whitespace, then don't make any adjustments to the text. We don't want - // to end up inserting nothing and having the user very confused why their paste did nothing. - if (AllWhitespace(SnapshotBeforePaste.Version.Changes)) - return default; + using var _ = ArrayBuilder.GetInstance(out var edits); - // if the content we're going to add itself contains quotes, then figure out how many start/end quotes the - // final string literal will need (which also gives us the number of quotes to add to the start/end). - // - // note: we don't have to do this if the paste was successful. Instead, we'll just process the contents, - // adjusting whitespace below. - var quotesToAdd = _pasteWasSuccessful ? null : GetQuotesToAddToRawString(); - var dollarSignsToAdd = _pasteWasSuccessful ? null : GetDollarSignsToAddToRawString(); + // First, add any extra dollar signs needed. + if (dollarSignsToAdd != null) + edits.Add(new TextChange(new TextSpan(StringExpressionBeforePaste.Span.Start, 0), dollarSignsToAdd)); - using var _ = ArrayBuilder.GetInstance(out var edits); + // Then any quotes to the start delimiter. + if (quotesToAdd != null) + edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.ContentSpans.First().Start, 0), quotesToAdd)); - // First, add any extra dollar signs needed. - if (dollarSignsToAdd != null) - edits.Add(new TextChange(new TextSpan(StringExpressionBeforePaste.Span.Start, 0), dollarSignsToAdd)); + // Then add the actual changes in the content. - // Then any quotes to the start delimiter. - if (quotesToAdd != null) - edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.ContentSpans.First().Start, 0), quotesToAdd)); + if (IsAnyMultiLineRawStringExpression(StringExpressionBeforePaste)) + AdjustWhitespaceAndAddTextChangesForMultiLineRawStringLiteral(edits); + else + AdjustWhitespaceAndAddTextChangesForSingleLineRawStringLiteral(edits); - // Then add the actual changes in the content. + // Then any extra quotes to the end delimiter. + if (quotesToAdd != null) + edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpanWithoutSuffix.End, 0), quotesToAdd)); - if (IsAnyMultiLineRawStringExpression(StringExpressionBeforePaste)) - AdjustWhitespaceAndAddTextChangesForMultiLineRawStringLiteral(edits); - else - AdjustWhitespaceAndAddTextChangesForSingleLineRawStringLiteral(edits); + return edits.ToImmutable(); + } - // Then any extra quotes to the end delimiter. - if (quotesToAdd != null) - edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpanWithoutSuffix.End, 0), quotesToAdd)); + /// + private string? GetQuotesToAddToRawString() + => GetQuotesToAddToRawString(TextAfterPaste, TextContentsSpansAfterPaste); - return edits.ToImmutable(); - } + /// + private string? GetDollarSignsToAddToRawString() + => GetDollarSignsToAddToRawString(TextAfterPaste, TextContentsSpansAfterPaste); - /// - private string? GetQuotesToAddToRawString() - => GetQuotesToAddToRawString(TextAfterPaste, TextContentsSpansAfterPaste); + // Pasting with single line case. - /// - private string? GetDollarSignsToAddToRawString() - => GetDollarSignsToAddToRawString(TextAfterPaste, TextContentsSpansAfterPaste); + private void AdjustWhitespaceAndAddTextChangesForSingleLineRawStringLiteral(ArrayBuilder edits) + { + // When pasting into a single-line raw literal we will keep it a single line if we can. If the content + // we're pasting starts/ends with a quote, or contains a newline, then we have to convert to a multiline. + // + // Pasting any other content into a single-line raw literal is always legal and needs no extra work on our + // part. - // Pasting with single line case. + var mustBeMultiLine = RawContentMustBeMultiLine(TextAfterPaste, TextContentsSpansAfterPaste); - private void AdjustWhitespaceAndAddTextChangesForSingleLineRawStringLiteral(ArrayBuilder edits) - { - // When pasting into a single-line raw literal we will keep it a single line if we can. If the content - // we're pasting starts/ends with a quote, or contains a newline, then we have to convert to a multiline. - // - // Pasting any other content into a single-line raw literal is always legal and needs no extra work on our - // part. + using var _ = PooledStringBuilder.GetInstance(out var buffer); - var mustBeMultiLine = RawContentMustBeMultiLine(TextAfterPaste, TextContentsSpansAfterPaste); + // A newline and the indentation to start with. + if (mustBeMultiLine) + edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.StartDelimiterSpan.End, 0), NewLine + IndentationWhitespace)); - using var _ = PooledStringBuilder.GetInstance(out var buffer); + SourceText? textOfCurrentChange = null; + var commonIndentationPrefix = GetCommonIndentationPrefix(Changes) ?? ""; - // A newline and the indentation to start with. - if (mustBeMultiLine) - edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.StartDelimiterSpan.End, 0), NewLine + IndentationWhitespace)); + foreach (var change in Changes) + { + // Create a text object around the change text we're making. This is a very simple way to get + // a nice view of the text lines in the change. + textOfCurrentChange = SourceText.From(change.NewText); - SourceText? textOfCurrentChange = null; - var commonIndentationPrefix = GetCommonIndentationPrefix(Changes) ?? ""; + buffer.Clear(); - foreach (var change in Changes) + for (var i = 0; i < textOfCurrentChange.Lines.Count; i++) { - // Create a text object around the change text we're making. This is a very simple way to get - // a nice view of the text lines in the change. - textOfCurrentChange = SourceText.From(change.NewText); + // The actual full line that was pasted in. + var currentChangeLine = textOfCurrentChange.Lines[i]; + var fullChangeLineText = textOfCurrentChange.ToString(currentChangeLine.SpanIncludingLineBreak); - buffer.Clear(); - - for (var i = 0; i < textOfCurrentChange.Lines.Count; i++) + if (i == 0) { - // The actual full line that was pasted in. - var currentChangeLine = textOfCurrentChange.Lines[i]; - var fullChangeLineText = textOfCurrentChange.ToString(currentChangeLine.SpanIncludingLineBreak); - - if (i == 0) - { - // on the first line, remove the common indentation if we can. Otherwise leave alone. - if (fullChangeLineText.StartsWith(commonIndentationPrefix, StringComparison.OrdinalIgnoreCase)) - buffer.Append(fullChangeLineText[commonIndentationPrefix.Length..]); - else - buffer.Append(fullChangeLineText); - } - else - { - // on all the rest of the lines, always remove the common indentation. + // on the first line, remove the common indentation if we can. Otherwise leave alone. + if (fullChangeLineText.StartsWith(commonIndentationPrefix, StringComparison.OrdinalIgnoreCase)) buffer.Append(fullChangeLineText[commonIndentationPrefix.Length..]); - } - - // if we ended with a newline, make sure the next line is indented enough. - if (HasNewLine(currentChangeLine)) - buffer.Append(IndentationWhitespace); + else + buffer.Append(fullChangeLineText); + } + else + { + // on all the rest of the lines, always remove the common indentation. + buffer.Append(fullChangeLineText[commonIndentationPrefix.Length..]); } - edits.Add(new TextChange(change.OldSpan.ToTextSpan(), buffer.ToString())); + // if we ended with a newline, make sure the next line is indented enough. + if (HasNewLine(currentChangeLine)) + buffer.Append(IndentationWhitespace); } - // if the last change ended at the closing delimiter *and* ended with a newline, then we don't need to add a - // final newline-space at the end because we will have already done that. - if (mustBeMultiLine && !LastPastedLineAddedNewLine()) - edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpan.Start, 0), NewLine + IndentationWhitespace)); + edits.Add(new TextChange(change.OldSpan.ToTextSpan(), buffer.ToString())); + } - return; + // if the last change ended at the closing delimiter *and* ended with a newline, then we don't need to add a + // final newline-space at the end because we will have already done that. + if (mustBeMultiLine && !LastPastedLineAddedNewLine()) + edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpan.Start, 0), NewLine + IndentationWhitespace)); - bool LastPastedLineAddedNewLine() - { - return textOfCurrentChange != null && - Changes.Last().OldEnd == StringExpressionBeforePasteInfo.ContentSpans.Last().End && - HasNewLine(textOfCurrentChange.Lines.Last()); - } + return; + + bool LastPastedLineAddedNewLine() + { + return textOfCurrentChange != null && + Changes.Last().OldEnd == StringExpressionBeforePasteInfo.ContentSpans.Last().End && + HasNewLine(textOfCurrentChange.Lines.Last()); } + } - // Pasting into multi line case. + // Pasting into multi line case. - private void AdjustWhitespaceAndAddTextChangesForMultiLineRawStringLiteral( - ArrayBuilder edits) - { - var endLine = TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.Span.End); + private void AdjustWhitespaceAndAddTextChangesForMultiLineRawStringLiteral( + ArrayBuilder edits) + { + var endLine = TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.Span.End); + + // The indentation whitespace every line of the final raw string needs. + var indentationWhitespace = endLine.GetLeadingWhitespace(); + + using var _ = PooledStringBuilder.GetInstance(out var buffer); - // The indentation whitespace every line of the final raw string needs. - var indentationWhitespace = endLine.GetLeadingWhitespace(); + var commonIndentationPrefix = GetCommonIndentationPrefix(Changes); - using var _ = PooledStringBuilder.GetInstance(out var buffer); + for (int changeIndex = 0, lastChangeIndex = Changes.Count; changeIndex < lastChangeIndex; changeIndex++) + { + var change = Changes[changeIndex]; - var commonIndentationPrefix = GetCommonIndentationPrefix(Changes); + // Create a text object around the change text we're making. This is a very simple way to get + // a nice view of the text lines in the change. + var textOfCurrentChange = SourceText.From(change.NewText); + buffer.Clear(); - for (int changeIndex = 0, lastChangeIndex = Changes.Count; changeIndex < lastChangeIndex; changeIndex++) + for (int lineIndex = 0, lastLineIndex = textOfCurrentChange.Lines.Count; lineIndex < lastLineIndex; lineIndex++) { - var change = Changes[changeIndex]; + var firstChange = changeIndex == 0 && lineIndex == 0; + var lastChange = (changeIndex == lastChangeIndex - 1) && (lineIndex == lastLineIndex - 1); - // Create a text object around the change text we're making. This is a very simple way to get - // a nice view of the text lines in the change. - var textOfCurrentChange = SourceText.From(change.NewText); - buffer.Clear(); + // The actual full line that was pasted in. + var currentChangeLine = textOfCurrentChange.Lines[lineIndex]; + var fullChangeLineText = textOfCurrentChange.ToString(currentChangeLine.SpanIncludingLineBreak); + // The contents of the line, with all leading whitespace removed. + var (lineLeadingWhitespace, lineWithoutLeadingWhitespace) = ExtractWhitespace(fullChangeLineText); - for (int lineIndex = 0, lastLineIndex = textOfCurrentChange.Lines.Count; lineIndex < lastLineIndex; lineIndex++) - { - var firstChange = changeIndex == 0 && lineIndex == 0; - var lastChange = (changeIndex == lastChangeIndex - 1) && (lineIndex == lastLineIndex - 1); + // This entire if-chain is only concerned with inserting the necessary whitespace a line should have. - // The actual full line that was pasted in. - var currentChangeLine = textOfCurrentChange.Lines[lineIndex]; - var fullChangeLineText = textOfCurrentChange.ToString(currentChangeLine.SpanIncludingLineBreak); - // The contents of the line, with all leading whitespace removed. - var (lineLeadingWhitespace, lineWithoutLeadingWhitespace) = ExtractWhitespace(fullChangeLineText); + if (firstChange) + { + // The first line is often special. It may be copied without any whitespace (e.g. the user + // starts their selection at the start of text on that line, not the start of the line itself). + // So we use some heuristics to try to decide what to do depending on how much whitespace we see + // on that first copied line. - // This entire if-chain is only concerned with inserting the necessary whitespace a line should have. + TextBeforePaste.GetLineAndOffset(change.OldSpan.Start, out var line, out var offset); - if (firstChange) - { - // The first line is often special. It may be copied without any whitespace (e.g. the user - // starts their selection at the start of text on that line, not the start of the line itself). - // So we use some heuristics to try to decide what to do depending on how much whitespace we see - // on that first copied line. - - TextBeforePaste.GetLineAndOffset(change.OldSpan.Start, out var line, out var offset); - - // First, ensure that the indentation whitespace of the *inserted* first line is sufficient. - if (line == TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.SpanStart).LineNumber) - { - // if the first chunk was pasted into the space after the first `"""` then we need to actually - // insert a newline, then the indentation whitespace, then the first line of the change. - buffer.Append(NewLine); - buffer.Append(indentationWhitespace); - } - else if (offset < indentationWhitespace.Length) - { - // On the first line, we were pasting into the indentation whitespace. Ensure we add enough - // whitespace so that the trimmed line starts at an acceptable position. - buffer.Append(indentationWhitespace[offset..]); - } - - // Now, we want to actually insert any whitespace the line itself should have *if* it's got more - // than the common indentation whitespace. - if (commonIndentationPrefix != null && lineLeadingWhitespace.StartsWith(commonIndentationPrefix, StringComparison.OrdinalIgnoreCase)) - buffer.Append(lineLeadingWhitespace[commonIndentationPrefix.Length..]); - } - else if (!lastChange && lineWithoutLeadingWhitespace.Length > 0 && SyntaxFacts.IsNewLine(lineWithoutLeadingWhitespace[0])) + // First, ensure that the indentation whitespace of the *inserted* first line is sufficient. + if (line == TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.SpanStart).LineNumber) { - // if it's just an empty line, don't bother adding any whitespace at all. This will just end up - // inserting the blank line here. We don't do this on the last line as we want to still insert - // the right amount of indentation so that the user's caret is placed properly in the text. We - // could technically not insert the whitespace and attempt to place the caret using a virtual - // position, but this adds a lot of complexity to this system, so we avoid that for now and go - // for the simpler approach.. + // if the first chunk was pasted into the space after the first `"""` then we need to actually + // insert a newline, then the indentation whitespace, then the first line of the change. + buffer.Append(NewLine); + buffer.Append(indentationWhitespace); } - else + else if (offset < indentationWhitespace.Length) { - // On any other line we're adding, ensure we have enough indentation whitespace to proceed. - // Add the necessary whitespace the literal needs, then add the line contents without - // the common whitespace included. - buffer.Append(indentationWhitespace); - if (commonIndentationPrefix != null) - buffer.Append(lineLeadingWhitespace[commonIndentationPrefix.Length..]); + // On the first line, we were pasting into the indentation whitespace. Ensure we add enough + // whitespace so that the trimmed line starts at an acceptable position. + buffer.Append(indentationWhitespace[offset..]); } - // After the necessary whitespace has been added, add the actual non-whitespace contents of the - // line. - buffer.Append(lineWithoutLeadingWhitespace); + // Now, we want to actually insert any whitespace the line itself should have *if* it's got more + // than the common indentation whitespace. + if (commonIndentationPrefix != null && lineLeadingWhitespace.StartsWith(commonIndentationPrefix, StringComparison.OrdinalIgnoreCase)) + buffer.Append(lineLeadingWhitespace[commonIndentationPrefix.Length..]); + } + else if (!lastChange && lineWithoutLeadingWhitespace.Length > 0 && SyntaxFacts.IsNewLine(lineWithoutLeadingWhitespace[0])) + { + // if it's just an empty line, don't bother adding any whitespace at all. This will just end up + // inserting the blank line here. We don't do this on the last line as we want to still insert + // the right amount of indentation so that the user's caret is placed properly in the text. We + // could technically not insert the whitespace and attempt to place the caret using a virtual + // position, but this adds a lot of complexity to this system, so we avoid that for now and go + // for the simpler approach.. + } + else + { + // On any other line we're adding, ensure we have enough indentation whitespace to proceed. + // Add the necessary whitespace the literal needs, then add the line contents without + // the common whitespace included. + buffer.Append(indentationWhitespace); + if (commonIndentationPrefix != null) + buffer.Append(lineLeadingWhitespace[commonIndentationPrefix.Length..]); + } - if (lastChange) - { - // Similar to the check we do for the first-change, if the last change was pasted into the space - // before the last `"""` then we need potentially insert a newline, then enough indentation - // whitespace to keep delimiter in the right location. + // After the necessary whitespace has been added, add the actual non-whitespace contents of the + // line. + buffer.Append(lineWithoutLeadingWhitespace); + + if (lastChange) + { + // Similar to the check we do for the first-change, if the last change was pasted into the space + // before the last `"""` then we need potentially insert a newline, then enough indentation + // whitespace to keep delimiter in the right location. - TextBeforePaste.GetLineAndOffset(change.OldSpan.End, out var line, out var offset); + TextBeforePaste.GetLineAndOffset(change.OldSpan.End, out var line, out var offset); - if (line == TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.Span.End).LineNumber) - { - if (!HasNewLine(currentChangeLine)) - buffer.Append(NewLine); + if (line == TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.Span.End).LineNumber) + { + if (!HasNewLine(currentChangeLine)) + buffer.Append(NewLine); - buffer.Append(TextBeforePaste.ToString(new TextSpan(TextBeforePaste.Lines[line].Start, offset))); - } + buffer.Append(TextBeforePaste.ToString(new TextSpan(TextBeforePaste.Lines[line].Start, offset))); } } - - edits.Add(new TextChange(change.OldSpan.ToTextSpan(), buffer.ToString())); } + + edits.Add(new TextChange(change.OldSpan.ToTextSpan(), buffer.ToString())); } } } diff --git a/src/EditorFeatures/CSharp/TextStructureNavigation/CSharpTextStructureNavigatorProvider.cs b/src/EditorFeatures/CSharp/TextStructureNavigation/CSharpTextStructureNavigatorProvider.cs index 9362bcc5a1ad7..0400e3443aab7 100644 --- a/src/EditorFeatures/CSharp/TextStructureNavigation/CSharpTextStructureNavigatorProvider.cs +++ b/src/EditorFeatures/CSharp/TextStructureNavigation/CSharpTextStructureNavigatorProvider.cs @@ -13,106 +13,105 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.TextStructureNavigation +namespace Microsoft.CodeAnalysis.Editor.CSharp.TextStructureNavigation; + +[Export(typeof(ITextStructureNavigatorProvider))] +[ContentType(ContentTypeNames.CSharpContentType)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpTextStructureNavigatorProvider( + ITextStructureNavigatorSelectorService selectorService, + IContentTypeRegistryService contentTypeService, + IUIThreadOperationExecutor uIThreadOperationExecutor) : AbstractTextStructureNavigatorProvider(selectorService, contentTypeService, uIThreadOperationExecutor) { - [Export(typeof(ITextStructureNavigatorProvider))] - [ContentType(ContentTypeNames.CSharpContentType)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpTextStructureNavigatorProvider( - ITextStructureNavigatorSelectorService selectorService, - IContentTypeRegistryService contentTypeService, - IUIThreadOperationExecutor uIThreadOperationExecutor) : AbstractTextStructureNavigatorProvider(selectorService, contentTypeService, uIThreadOperationExecutor) - { - protected override bool ShouldSelectEntireTriviaFromStart(SyntaxTrivia trivia) - => trivia.IsRegularOrDocComment(); + protected override bool ShouldSelectEntireTriviaFromStart(SyntaxTrivia trivia) + => trivia.IsRegularOrDocComment(); - protected override bool IsWithinNaturalLanguage(SyntaxToken token, int position) + protected override bool IsWithinNaturalLanguage(SyntaxToken token, int position) + { + switch (token.Kind()) { - switch (token.Kind()) - { - case SyntaxKind.StringLiteralToken: - case SyntaxKind.Utf8StringLiteralToken: - // This, in combination with the override of GetExtentOfWordFromToken() below, treats the closing - // quote as a separate token. This maintains behavior with VS2013. - return !IsAtClosingQuote(token, position); + case SyntaxKind.StringLiteralToken: + case SyntaxKind.Utf8StringLiteralToken: + // This, in combination with the override of GetExtentOfWordFromToken() below, treats the closing + // quote as a separate token. This maintains behavior with VS2013. + return !IsAtClosingQuote(token, position); - case SyntaxKind.SingleLineRawStringLiteralToken: - case SyntaxKind.MultiLineRawStringLiteralToken: - case SyntaxKind.Utf8SingleLineRawStringLiteralToken: - case SyntaxKind.Utf8MultiLineRawStringLiteralToken: - { - // Like with normal string literals, treat the closing quotes as as the end of the string so that - // navigation ends there and doesn't go past them. - var end = GetStartOfRawStringLiteralEndDelimiter(token); - return position < end; - } + case SyntaxKind.SingleLineRawStringLiteralToken: + case SyntaxKind.MultiLineRawStringLiteralToken: + case SyntaxKind.Utf8SingleLineRawStringLiteralToken: + case SyntaxKind.Utf8MultiLineRawStringLiteralToken: + { + // Like with normal string literals, treat the closing quotes as as the end of the string so that + // navigation ends there and doesn't go past them. + var end = GetStartOfRawStringLiteralEndDelimiter(token); + return position < end; + } - case SyntaxKind.CharacterLiteralToken: - // Before the ' is considered outside the character - return position != token.SpanStart; + case SyntaxKind.CharacterLiteralToken: + // Before the ' is considered outside the character + return position != token.SpanStart; - case SyntaxKind.InterpolatedStringTextToken: - case SyntaxKind.XmlTextLiteralToken: - return true; - } - - return false; + case SyntaxKind.InterpolatedStringTextToken: + case SyntaxKind.XmlTextLiteralToken: + return true; } - private static int GetStartOfRawStringLiteralEndDelimiter(SyntaxToken token) - { - var text = token.ToString(); - var start = 0; - var end = text.Length; + return false; + } + + private static int GetStartOfRawStringLiteralEndDelimiter(SyntaxToken token) + { + var text = token.ToString(); + var start = 0; + var end = text.Length; - if (token.Kind() is SyntaxKind.Utf8MultiLineRawStringLiteralToken or SyntaxKind.Utf8SingleLineRawStringLiteralToken) - { - // Skip past the u8 suffix - end -= "u8".Length; - } + if (token.Kind() is SyntaxKind.Utf8MultiLineRawStringLiteralToken or SyntaxKind.Utf8SingleLineRawStringLiteralToken) + { + // Skip past the u8 suffix + end -= "u8".Length; + } - while (start < end && text[start] == '"') - start++; + while (start < end && text[start] == '"') + start++; - while (end > start && text[end - 1] == '"') - end--; + while (end > start && text[end - 1] == '"') + end--; - return token.SpanStart + end; - } + return token.SpanStart + end; + } - private static bool IsAtClosingQuote(SyntaxToken token, int position) - => token.Kind() switch - { - SyntaxKind.StringLiteralToken => position == token.Span.End - 1 && token.Text[^1] == '"', - SyntaxKind.Utf8StringLiteralToken => position == token.Span.End - 3 && token.Text is [.., '"', 'u' or 'U', '8'], - _ => throw ExceptionUtilities.Unreachable() - }; + private static bool IsAtClosingQuote(SyntaxToken token, int position) + => token.Kind() switch + { + SyntaxKind.StringLiteralToken => position == token.Span.End - 1 && token.Text[^1] == '"', + SyntaxKind.Utf8StringLiteralToken => position == token.Span.End - 3 && token.Text is [.., '"', 'u' or 'U', '8'], + _ => throw ExceptionUtilities.Unreachable() + }; - protected override TextExtent GetExtentOfWordFromToken(SyntaxToken token, SnapshotPoint position) + protected override TextExtent GetExtentOfWordFromToken(SyntaxToken token, SnapshotPoint position) + { + if (token.Kind() is SyntaxKind.StringLiteralToken or SyntaxKind.Utf8StringLiteralToken && + IsAtClosingQuote(token, position.Position)) + { + // Special case to treat the closing quote of a string literal as a separate token. This allows the + // cursor to stop during word navigation (Ctrl+LeftArrow, etc.) immediately before AND after the + // closing quote, just like it did in VS2013 and like it currently does for interpolated strings. + var span = new Span(position.Position, token.Span.End - position.Position); + return new TextExtent(new SnapshotSpan(position.Snapshot, span), isSignificant: true); + } + else if (token.Kind() is + SyntaxKind.SingleLineRawStringLiteralToken or + SyntaxKind.MultiLineRawStringLiteralToken or + SyntaxKind.Utf8SingleLineRawStringLiteralToken or + SyntaxKind.Utf8MultiLineRawStringLiteralToken) + { + var delimiterStart = GetStartOfRawStringLiteralEndDelimiter(token); + return new TextExtent(new SnapshotSpan(position.Snapshot, Span.FromBounds(delimiterStart, token.Span.End)), isSignificant: true); + } + else { - if (token.Kind() is SyntaxKind.StringLiteralToken or SyntaxKind.Utf8StringLiteralToken && - IsAtClosingQuote(token, position.Position)) - { - // Special case to treat the closing quote of a string literal as a separate token. This allows the - // cursor to stop during word navigation (Ctrl+LeftArrow, etc.) immediately before AND after the - // closing quote, just like it did in VS2013 and like it currently does for interpolated strings. - var span = new Span(position.Position, token.Span.End - position.Position); - return new TextExtent(new SnapshotSpan(position.Snapshot, span), isSignificant: true); - } - else if (token.Kind() is - SyntaxKind.SingleLineRawStringLiteralToken or - SyntaxKind.MultiLineRawStringLiteralToken or - SyntaxKind.Utf8SingleLineRawStringLiteralToken or - SyntaxKind.Utf8MultiLineRawStringLiteralToken) - { - var delimiterStart = GetStartOfRawStringLiteralEndDelimiter(token); - return new TextExtent(new SnapshotSpan(position.Snapshot, Span.FromBounds(delimiterStart, token.Span.End)), isSignificant: true); - } - else - { - return base.GetExtentOfWordFromToken(token, position); - } + return base.GetExtentOfWordFromToken(token, position); } } } diff --git a/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticLiteralCompletionTests.cs b/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticLiteralCompletionTests.cs index bda3d46fe1874..99fe39fe9f6f9 100644 --- a/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticLiteralCompletionTests.cs +++ b/src/EditorFeatures/CSharpTest/AutomaticCompletion/AutomaticLiteralCompletionTests.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.CodeAnalysis.AutomaticCompletion; using Microsoft.CodeAnalysis.Editor.UnitTests.AutomaticCompletion; -using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; using Xunit; @@ -13,7 +11,7 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.AutomaticCompletion { [Trait(Traits.Feature, Traits.Features.AutomaticCompletion)] - public class AutomaticLiteralCompletionTests : AbstractAutomaticBraceCompletionTests + public sealed class AutomaticLiteralCompletionTests : AbstractAutomaticBraceCompletionTests { [WpfFact] public void Creation() @@ -75,8 +73,7 @@ void Method() } """; using var session = CreateSessionDoubleQuote(code); - Assert.NotNull(session); - CheckStart(session.Session, expectValidSession: false); + Assert.Null(session); } [WpfFact] @@ -92,8 +89,7 @@ void Method() } """; using var session = CreateSessionDoubleQuote(code); - Assert.NotNull(session); - CheckStart(session.Session); + Assert.Null(session); } [WpfFact] @@ -464,8 +460,7 @@ void Method() } """; using var session = CreateSessionDoubleQuote(code); - Assert.NotNull(session); - CheckStart(session.Session, expectValidSession: false); + Assert.Null(session); } [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/59178")] @@ -552,6 +547,103 @@ void Method() CheckStart(session.Session, expectValidSession: false); } + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/62571")] + public void String_InArgumentList1() + { + var code = """ + class C + { + void Method() + { + var s = Goo($[||]$$); + } + } + """; + using var session = CreateSessionDoubleQuote(code); + Assert.NotNull(session); + CheckStart(session.Session); + } + + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/62571")] + public void String_InArgumentList2() + { + var code = """ + class C + { + void Method() + { + var s = Goo(@[||]$$); + } + } + """; + using var session = CreateSessionDoubleQuote(code); + Assert.NotNull(session); + CheckStart(session.Session); + } + + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/62571")] + public void String_InArgumentList3() + { + var code = """ + class C + { + void Method() + { + var s = Goo(@$[||]$$); + } + } + """; + using var session = CreateSessionDoubleQuote(code); + Assert.NotNull(session); + CheckStart(session.Session); + } + + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/62571")] + public void String_InArgumentList4() + { + var code = """ + class C + { + void Method() + { + var s = Goo($@[||]$$); + } + } + """; + using var session = CreateSessionDoubleQuote(code); + Assert.NotNull(session); + CheckStart(session.Session); + } + + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/62571")] + public void String_InArgumentList5() + { + var code = """ + class C + { + void Method() + { + // Handled by normal verbatim string handler. + var s = Goo(@[||]$$); + } + } + """; + using var session = CreateSessionDoubleQuote(code); + Assert.NotNull(session); + CheckStart(session.Session); + } + + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/62571")] + public void String_GlobalStatement() + { + var code = """ + $[||]$$ + """; + using var session = CreateSessionDoubleQuote(code); + Assert.NotNull(session); + CheckStart(session.Session); + } + internal static Holder CreateSessionSingleQuote(string code) { return CreateSession( diff --git a/src/EditorFeatures/CSharpTest/CodeActions/ImplementInterface/ImplementExplicitlyTests.cs b/src/EditorFeatures/CSharpTest/CodeActions/ImplementInterface/ImplementExplicitlyTests.cs index adeaf3d5ae955..fe28f9d4a5301 100644 --- a/src/EditorFeatures/CSharpTest/CodeActions/ImplementInterface/ImplementExplicitlyTests.cs +++ b/src/EditorFeatures/CSharpTest/CodeActions/ImplementInterface/ImplementExplicitlyTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections.Immutable; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; @@ -13,827 +12,826 @@ using Roslyn.Test.Utilities; using Xunit; -namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.ImplementInterface +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.ImplementInterface; + +[Trait(Traits.Feature, Traits.Features.CodeActionsImplementInterface)] +public sealed class ImplementExplicitlyTests : AbstractCSharpCodeActionTest { - [Trait(Traits.Feature, Traits.Features.CodeActionsImplementInterface)] - public class ImplementExplicitlyTests : AbstractCSharpCodeActionTest - { - private const int SingleMember = 0; - private const int SameInterface = 1; - private const int AllInterfaces = 2; + private const int SingleMember = 0; + private const int SameInterface = 1; + private const int AllInterfaces = 2; - protected override CodeRefactoringProvider CreateCodeRefactoringProvider(EditorTestWorkspace workspace, TestParameters parameters) - => new CSharpImplementExplicitlyCodeRefactoringProvider(); + protected override CodeRefactoringProvider CreateCodeRefactoringProvider(EditorTestWorkspace workspace, TestParameters parameters) + => new CSharpImplementExplicitlyCodeRefactoringProvider(); - protected override ImmutableArray MassageActions(ImmutableArray actions) - => FlattenActions(actions); + protected override ImmutableArray MassageActions(ImmutableArray actions) + => FlattenActions(actions); - [Fact] - public async Task TestSingleMember() - { - await TestInRegularAndScriptAsync( - """ - interface IGoo { void Goo1(); void Goo2(); } - interface IBar { void Bar(); } + [Fact] + public async Task TestSingleMember() + { + await TestInRegularAndScriptAsync( + """ + interface IGoo { void Goo1(); void Goo2(); } + interface IBar { void Bar(); } - class C : IGoo, IBar - { - public void [||]Goo1() { } + class C : IGoo, IBar + { + public void [||]Goo1() { } - public void Goo2() { } + public void Goo2() { } - public void Bar() { } - } - """, - """ - interface IGoo { void Goo1(); void Goo2(); } - interface IBar { void Bar(); } + public void Bar() { } + } + """, + """ + interface IGoo { void Goo1(); void Goo2(); } + interface IBar { void Bar(); } - class C : IGoo, IBar - { - void IGoo.Goo1() { } + class C : IGoo, IBar + { + void IGoo.Goo1() { } - public void Goo2() { } + public void Goo2() { } - public void Bar() { } - } - """, index: SingleMember); - } - - [Fact] - public async Task TestSameInterface() - { - await TestInRegularAndScriptAsync( - """ - interface IGoo { void Goo1(); void Goo2(); } - interface IBar { void Bar(); } - - class C : IGoo, IBar - { - public void [||]Goo1() { } + public void Bar() { } + } + """, index: SingleMember); + } - public void Goo2() { } + [Fact] + public async Task TestSameInterface() + { + await TestInRegularAndScriptAsync( + """ + interface IGoo { void Goo1(); void Goo2(); } + interface IBar { void Bar(); } - public void Bar() { } - } - """, - """ - interface IGoo { void Goo1(); void Goo2(); } - interface IBar { void Bar(); } + class C : IGoo, IBar + { + public void [||]Goo1() { } - class C : IGoo, IBar - { - void IGoo.Goo1() { } + public void Goo2() { } - void IGoo.Goo2() { } + public void Bar() { } + } + """, + """ + interface IGoo { void Goo1(); void Goo2(); } + interface IBar { void Bar(); } - public void Bar() { } - } - """, index: SameInterface); - } - - [Fact] - public async Task TestAllInterfaces() - { - await TestInRegularAndScriptAsync( - """ - interface IGoo { void Goo1(); void Goo2(); } - interface IBar { void Bar(); } - - class C : IGoo, IBar - { - public void [||]Goo1() { } + class C : IGoo, IBar + { + void IGoo.Goo1() { } - public void Goo2() { } + void IGoo.Goo2() { } - public void Bar() { } - } - """, - """ - interface IGoo { void Goo1(); void Goo2(); } - interface IBar { void Bar(); } + public void Bar() { } + } + """, index: SameInterface); + } - class C : IGoo, IBar - { - void IGoo.Goo1() { } + [Fact] + public async Task TestAllInterfaces() + { + await TestInRegularAndScriptAsync( + """ + interface IGoo { void Goo1(); void Goo2(); } + interface IBar { void Bar(); } - void IGoo.Goo2() { } + class C : IGoo, IBar + { + public void [||]Goo1() { } - void IBar.Bar() { } - } - """, index: AllInterfaces); - } + public void Goo2() { } - [Fact] - public async Task TestProperty() - { - await TestInRegularAndScriptAsync( - """ - interface IGoo { int Goo1 { get; } } + public void Bar() { } + } + """, + """ + interface IGoo { void Goo1(); void Goo2(); } + interface IBar { void Bar(); } - class C : IGoo - { - public int [||]Goo1 { get { } } - } - """, - """ - interface IGoo { int Goo1 { get; } } + class C : IGoo, IBar + { + void IGoo.Goo1() { } - class C : IGoo - { - int IGoo.Goo1 { get { } } - } - """, index: SingleMember); - } + void IGoo.Goo2() { } - [Fact] - public async Task TestEvent() - { - await TestInRegularAndScriptAsync( - """ - interface IGoo { event Action E; } + void IBar.Bar() { } + } + """, index: AllInterfaces); + } - class C : IGoo - { - public event Action [||]E { add { } remove { } } - } - """, - """ - interface IGoo { event Action E; } + [Fact] + public async Task TestProperty() + { + await TestInRegularAndScriptAsync( + """ + interface IGoo { int Goo1 { get; } } + + class C : IGoo + { + public int [||]Goo1 { get { } } + } + """, + """ + interface IGoo { int Goo1 { get; } } + + class C : IGoo + { + int IGoo.Goo1 { get { } } + } + """, index: SingleMember); + } - class C : IGoo - { - event Action IGoo.E { add { } remove { } } - } - """, index: SingleMember); - } + [Fact] + public async Task TestEvent() + { + await TestInRegularAndScriptAsync( + """ + interface IGoo { event Action E; } + + class C : IGoo + { + public event Action [||]E { add { } remove { } } + } + """, + """ + interface IGoo { event Action E; } + + class C : IGoo + { + event Action IGoo.E { add { } remove { } } + } + """, index: SingleMember); + } - [Fact] - public async Task TestNotOnExplicitMember() - { - await TestMissingAsync( - """ - interface IGoo { void Goo1(); } + [Fact] + public async Task TestNotOnExplicitMember() + { + await TestMissingAsync( + """ + interface IGoo { void Goo1(); } + + class C : IGoo + { + void IGoo.[||]Goo1() { } + } + """); + } - class C : IGoo - { - void IGoo.[||]Goo1() { } - } - """); - } + [Fact] + public async Task TestNotOnUnboundImplicitImpl() + { + await TestMissingAsync( + """ + interface IGoo { void Goo1(); } + + class C + { + public void [||]Goo1() { } + } + """); + } - [Fact] - public async Task TestNotOnUnboundImplicitImpl() - { - await TestMissingAsync( - """ - interface IGoo { void Goo1(); } + [Fact] + public async Task TestUpdateReferences_InsideDeclarations_Explicit() + { + await TestInRegularAndScriptAsync( + """ + using System; + interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + + class C : IGoo + { + public int [||]Prop { get { return this.Prop; } set { this.Prop = value; } } + public int M(int i) { return this.M(i); } + public event Action Ev { add { this.Ev += value; } remove { this.Ev -= value; } } + } + """, + """ + using System; + interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + + class C : IGoo + { + int IGoo.Prop { get { return ((IGoo)this).Prop; } set { ((IGoo)this).Prop = value; } } + int IGoo.M(int i) { return ((IGoo)this).M(i); } + event Action IGoo.Ev { add { ((IGoo)this).Ev += value; } remove { ((IGoo)this).Ev -= value; } } + } + """, index: SameInterface); + } - class C - { - public void [||]Goo1() { } - } - """); - } - - [Fact] - public async Task TestUpdateReferences_InsideDeclarations_Explicit() - { - await TestInRegularAndScriptAsync( - """ - using System; - interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - - class C : IGoo - { - public int [||]Prop { get { return this.Prop; } set { this.Prop = value; } } - public int M(int i) { return this.M(i); } - public event Action Ev { add { this.Ev += value; } remove { this.Ev -= value; } } - } - """, - """ - using System; - interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + [Fact] + public async Task TestUpdateReferences_InsideDeclarations_Implicit() + { + await TestInRegularAndScriptAsync( + """ + using System; + interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + + class C : IGoo + { + public int [||]Prop { get { return Prop; } set { Prop = value; } } + public int M(int i) { return M(i); } + public event Action Ev { add { Ev += value; } remove { Ev -= value; } } + } + """, + """ + using System; + interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + + class C : IGoo + { + int IGoo.Prop { get { return ((IGoo)this).Prop; } set { ((IGoo)this).Prop = value; } } + int IGoo.M(int i) { return ((IGoo)this).M(i); } + event Action IGoo.Ev { add { ((IGoo)this).Ev += value; } remove { ((IGoo)this).Ev -= value; } } + } + """, index: SameInterface); + } - class C : IGoo - { - int IGoo.Prop { get { return ((IGoo)this).Prop; } set { ((IGoo)this).Prop = value; } } - int IGoo.M(int i) { return ((IGoo)this).M(i); } - event Action IGoo.Ev { add { ((IGoo)this).Ev += value; } remove { ((IGoo)this).Ev -= value; } } - } - """, index: SameInterface); - } - - [Fact] - public async Task TestUpdateReferences_InsideDeclarations_Implicit() - { - await TestInRegularAndScriptAsync( - """ - using System; - interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - - class C : IGoo - { - public int [||]Prop { get { return Prop; } set { Prop = value; } } - public int M(int i) { return M(i); } - public event Action Ev { add { Ev += value; } remove { Ev -= value; } } - } - """, - """ - using System; - interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + [Fact] + public async Task TestUpdateReferences_InternalImplicit() + { + await TestInRegularAndScriptAsync( + """ + using System; + interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - class C : IGoo - { - int IGoo.Prop { get { return ((IGoo)this).Prop; } set { ((IGoo)this).Prop = value; } } - int IGoo.M(int i) { return ((IGoo)this).M(i); } - event Action IGoo.Ev { add { ((IGoo)this).Ev += value; } remove { ((IGoo)this).Ev -= value; } } - } - """, index: SameInterface); - } - - [Fact] - public async Task TestUpdateReferences_InternalImplicit() - { - await TestInRegularAndScriptAsync( - """ - using System; - interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - - class C : IGoo - { - public int [||]Prop { get { } set { } } - public int M(int i) { } - public event Action Ev { add { } remove { } } + class C : IGoo + { + public int [||]Prop { get { } set { } } + public int M(int i) { } + public event Action Ev { add { } remove { } } - void InternalImplicit() - { - var v = Prop; - Prop = 1; - Prop++; - ++Prop; + void InternalImplicit() + { + var v = Prop; + Prop = 1; + Prop++; + ++Prop; - M(0); - M(M(0)); + M(0); + M(M(0)); - Ev += () => {}; + Ev += () => {}; - var v1 = nameof(Prop); - } + var v1 = nameof(Prop); } - """, - """ - using System; - interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + } + """, + """ + using System; + interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - class C : IGoo - { - int IGoo.Prop { get { } set { } } - int IGoo.M(int i) { } - event Action IGoo.Ev { add { } remove { } } + class C : IGoo + { + int IGoo.Prop { get { } set { } } + int IGoo.M(int i) { } + event Action IGoo.Ev { add { } remove { } } - void InternalImplicit() - { - var v = ((IGoo)this).Prop; - ((IGoo)this).Prop = 1; - ((IGoo)this).Prop++; - ++((IGoo)this).Prop; + void InternalImplicit() + { + var v = ((IGoo)this).Prop; + ((IGoo)this).Prop = 1; + ((IGoo)this).Prop++; + ++((IGoo)this).Prop; - ((IGoo)this).M(0); - ((IGoo)this).M(((IGoo)this).M(0)); + ((IGoo)this).M(0); + ((IGoo)this).M(((IGoo)this).M(0)); - ((IGoo)this).Ev += () => {}; + ((IGoo)this).Ev += () => {}; - var v1 = nameof(((IGoo)this).Prop); - } + var v1 = nameof(((IGoo)this).Prop); } - """, index: SameInterface); - } - - [Fact] - public async Task TestUpdateReferences_InternalExplicit() - { - await TestInRegularAndScriptAsync( - """ - using System; - interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - - class C : IGoo - { - public int [||]Prop { get { } set { } } - public int M(int i) { } - public event Action Ev { add { } remove { } } + } + """, index: SameInterface); + } - void InternalExplicit() - { - var v = this.Prop; - this.Prop = 1; - this.Prop++; - ++this.Prop; + [Fact] + public async Task TestUpdateReferences_InternalExplicit() + { + await TestInRegularAndScriptAsync( + """ + using System; + interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - this.M(0); - this.M(this.M(0)); + class C : IGoo + { + public int [||]Prop { get { } set { } } + public int M(int i) { } + public event Action Ev { add { } remove { } } - this.Ev += () => {}; + void InternalExplicit() + { + var v = this.Prop; + this.Prop = 1; + this.Prop++; + ++this.Prop; - var v1 = nameof(this.Prop); - } + this.M(0); + this.M(this.M(0)); + + this.Ev += () => {}; + + var v1 = nameof(this.Prop); } - """, - """ - using System; - interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + } + """, + """ + using System; + interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - class C : IGoo - { - int IGoo.Prop { get { } set { } } - int IGoo.M(int i) { } - event Action IGoo.Ev { add { } remove { } } + class C : IGoo + { + int IGoo.Prop { get { } set { } } + int IGoo.M(int i) { } + event Action IGoo.Ev { add { } remove { } } - void InternalExplicit() - { - var v = ((IGoo)this).Prop; - ((IGoo)this).Prop = 1; - ((IGoo)this).Prop++; - ++((IGoo)this).Prop; + void InternalExplicit() + { + var v = ((IGoo)this).Prop; + ((IGoo)this).Prop = 1; + ((IGoo)this).Prop++; + ++((IGoo)this).Prop; - ((IGoo)this).M(0); - ((IGoo)this).M(((IGoo)this).M(0)); + ((IGoo)this).M(0); + ((IGoo)this).M(((IGoo)this).M(0)); - ((IGoo)this).Ev += () => {}; + ((IGoo)this).Ev += () => {}; - var v1 = nameof(((IGoo)this).Prop); - } - } - """, index: SameInterface); - } - - [Fact] - public async Task TestUpdateReferences_External() - { - await TestInRegularAndScriptAsync( - """ - using System; - interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - - class C : IGoo - { - public int [||]Prop { get { } set { } } - public int M(int i) { } - public event Action Ev { add { } remove { } } + var v1 = nameof(((IGoo)this).Prop); } + } + """, index: SameInterface); + } + + [Fact] + public async Task TestUpdateReferences_External() + { + await TestInRegularAndScriptAsync( + """ + using System; + interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + + class C : IGoo + { + public int [||]Prop { get { } set { } } + public int M(int i) { } + public event Action Ev { add { } remove { } } + } - class T + class T + { + void External(C c) { - void External(C c) - { - var v = c.Prop; - c.Prop = 1; - c.Prop++; - ++c.Prop; + var v = c.Prop; + c.Prop = 1; + c.Prop++; + ++c.Prop; - c.M(0); - c.M(c.M(0)); + c.M(0); + c.M(c.M(0)); - c.Ev += () => {}; + c.Ev += () => {}; - new C - { - Prop = 1 - }; + new C + { + Prop = 1 + }; - var v1 = nameof(c.Prop); - } + var v1 = nameof(c.Prop); } - """, - """ - using System; - interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + } + """, + """ + using System; + interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - class C : IGoo - { - int IGoo.Prop { get { } set { } } - int IGoo.M(int i) { } - event Action IGoo.Ev { add { } remove { } } - } + class C : IGoo + { + int IGoo.Prop { get { } set { } } + int IGoo.M(int i) { } + event Action IGoo.Ev { add { } remove { } } + } - class T + class T + { + void External(C c) { - void External(C c) - { - var v = ((IGoo)c).Prop; - ((IGoo)c).Prop = 1; - ((IGoo)c).Prop++; - ++((IGoo)c).Prop; - - ((IGoo)c).M(0); - ((IGoo)c).M(((IGoo)c).M(0)); + var v = ((IGoo)c).Prop; + ((IGoo)c).Prop = 1; + ((IGoo)c).Prop++; + ++((IGoo)c).Prop; - ((IGoo)c).Ev += () => {}; + ((IGoo)c).M(0); + ((IGoo)c).M(((IGoo)c).M(0)); - new C - { - Prop = 1 - }; + ((IGoo)c).Ev += () => {}; - var v1 = nameof(((IGoo)c).Prop); - } - } - """, index: SameInterface); - } - - [Fact] - public async Task TestUpdateReferences_CrossLanguage() - { - await TestInRegularAndScriptAsync( - """ - - - - using System; - public interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - - public class C : IGoo - { - public int [||]Prop { get { } set { } } - public int M(int i) { } - public event Action Ev { add { } remove { } } - } - - - - A1 - - class T - sub External(c1 as C) - dim v = c1.Prop - c1.Prop = 1 - - c1.M(0) - c1.M(c1.M(0)) - - dim x = new C() with { - .Prop = 1 - } - - dim v1 = nameof(c1.Prop) - end sub - end class - - - - """, - """ - - - - using System; - public interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } - - public class C : IGoo - { - int IGoo.Prop { get { } set { } } - int IGoo.M(int i) { } - event Action IGoo.Ev { add { } remove { } } - } - - - - A1 - - class T - sub External(c1 as C) - dim v = DirectCast(c1, IGoo).Prop - DirectCast(c1, IGoo).Prop = 1 - - DirectCast(c1, IGoo).M(0) - DirectCast(c1, IGoo).M(DirectCast(c1, IGoo).M(0)) - - dim x = new C() with { - .Prop = 1 - } - - dim v1 = nameof(c1.Prop) - end sub - end class - - - - """, index: SameInterface); - } - - [Fact] - public async Task TestMemberWhichImplementsMultipleMembers() - { - await TestInRegularAndScriptAsync( - """ - interface IGoo { int M(int i); } - interface IBar { int M(int i); } - - class C : IGoo, IBar - { - public int [||]M(int i) + new C { - throw new System.Exception(); - } - } - """, - """ - interface IGoo { int M(int i); } - interface IBar { int M(int i); } + Prop = 1 + }; - class C : IGoo, IBar - { - int IGoo.M(int i) - { - throw new System.Exception(); - } - int IBar.M(int i) - { - throw new System.Exception(); - } + var v1 = nameof(((IGoo)c).Prop); } - """, index: SingleMember); - } - - [Fact] - public async Task TestMemberWhichImplementsMultipleMembers2() - { - await TestInRegularAndScriptAsync( - """ - interface IGoo { int M(int i); } - interface IBar { int M(int i); } - - class C : IGoo, IBar - { - public int [||]M(int i) - { - return this.M(1); - } - } - """, - """ - interface IGoo { int M(int i); } - interface IBar { int M(int i); } + } + """, index: SameInterface); + } - class C : IGoo, IBar - { - int IGoo.M(int i) - { - return ((IGoo)this).M(1); - } - int IBar.M(int i) - { - return ((IGoo)this).M(1); + [Fact] + public async Task TestUpdateReferences_CrossLanguage() + { + await TestInRegularAndScriptAsync( + """ + + + + using System; + public interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + + public class C : IGoo + { + public int [||]Prop { get { } set { } } + public int M(int i) { } + public event Action Ev { add { } remove { } } + } + + + + A1 + + class T + sub External(c1 as C) + dim v = c1.Prop + c1.Prop = 1 + + c1.M(0) + c1.M(c1.M(0)) + + dim x = new C() with { + .Prop = 1 } - } - """, index: SingleMember); - } - - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52020")] - public async Task TestWithContraints() - { - await TestInRegularAndScriptAsync( - """ - interface IRepro - { - void A(int value) where T : class; - } - class Repro : IRepro - { - public void [||]A(int value) where T : class - { + dim v1 = nameof(c1.Prop) + end sub + end class + + + + """, + """ + + + + using System; + public interface IGoo { int Prop { get; set; } int M(int i); event Action Ev; } + + public class C : IGoo + { + int IGoo.Prop { get { } set { } } + int IGoo.M(int i) { } + event Action IGoo.Ev { add { } remove { } } + } + + + + A1 + + class T + sub External(c1 as C) + dim v = DirectCast(c1, IGoo).Prop + DirectCast(c1, IGoo).Prop = 1 + + DirectCast(c1, IGoo).M(0) + DirectCast(c1, IGoo).M(DirectCast(c1, IGoo).M(0)) + + dim x = new C() with { + .Prop = 1 } - } - """, - """ - interface IRepro - { - void A(int value) where T : class; - } - class Repro : IRepro - { - void IRepro.A(int value) - { - } - } - """); - } - - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52020")] - public async Task TestWithDefaultParameterValues() - { - await TestInRegularAndScriptAsync( - """ - interface IRepro - { - void A(int value = 0); - } + dim v1 = nameof(c1.Prop) + end sub + end class + + + + """, index: SameInterface); + } - class Repro : IRepro - { - public void [||]A(int value = 0) - { - } - } - """, - """ - interface IRepro - { - void A(int value = 0); - } + [Fact] + public async Task TestMemberWhichImplementsMultipleMembers() + { + await TestInRegularAndScriptAsync( + """ + interface IGoo { int M(int i); } + interface IBar { int M(int i); } - class Repro : IRepro - { - void IRepro.A(int value) - { - } - } - """); - } - - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52020")] - public async Task TestWithMismatchedDefaultParameterValues() - { - await TestInRegularAndScriptAsync( - """ - interface IRepro + class C : IGoo, IBar + { + public int [||]M(int i) { - void A(int value = 0); + throw new System.Exception(); } + } + """, + """ + interface IGoo { int M(int i); } + interface IBar { int M(int i); } - class Repro : IRepro + class C : IGoo, IBar + { + int IGoo.M(int i) { - public void [||]A(int value = 1) - { - } + throw new System.Exception(); } - """, - """ - interface IRepro + int IBar.M(int i) { - void A(int value = 0); + throw new System.Exception(); } + } + """, index: SingleMember); + } - class Repro : IRepro - { - void IRepro.A(int value = 1) - { - } - } - """); - } - - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52020")] - public async Task TestWithMismatchedDefault1() - { - await TestInRegularAndScriptAsync( - """ - interface IRepro + [Fact] + public async Task TestMemberWhichImplementsMultipleMembers2() + { + await TestInRegularAndScriptAsync( + """ + interface IGoo { int M(int i); } + interface IBar { int M(int i); } + + class C : IGoo, IBar + { + public int [||]M(int i) { - void A(int value); + return this.M(1); } + } + """, + """ + interface IGoo { int M(int i); } + interface IBar { int M(int i); } - class Repro : IRepro + class C : IGoo, IBar + { + int IGoo.M(int i) { - public void [||]A(int value = 1) - { - } + return ((IGoo)this).M(1); } - """, - """ - interface IRepro + int IBar.M(int i) { - void A(int value); + return ((IGoo)this).M(1); } + } + """, index: SingleMember); + } - class Repro : IRepro - { - void IRepro.A(int value = 1) - { - } - } - """); - } - - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52020")] - public async Task TestWithMismatchedDefault2() - { - await TestInRegularAndScriptAsync( - """ - interface IRepro + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52020")] + public async Task TestWithContraints() + { + await TestInRegularAndScriptAsync( + """ + interface IRepro + { + void A(int value) where T : class; + } + + class Repro : IRepro + { + public void [||]A(int value) where T : class { - void A(int value = 0); } + } + """, + """ + interface IRepro + { + void A(int value) where T : class; + } - class Repro : IRepro + class Repro : IRepro + { + void IRepro.A(int value) { - public void [||]A(int value) - { - } } - """, - """ - interface IRepro + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52020")] + public async Task TestWithDefaultParameterValues() + { + await TestInRegularAndScriptAsync( + """ + interface IRepro + { + void A(int value = 0); + } + + class Repro : IRepro + { + public void [||]A(int value = 0) { - void A(int value = 0); } + } + """, + """ + interface IRepro + { + void A(int value = 0); + } - class Repro : IRepro + class Repro : IRepro + { + void IRepro.A(int value) { - void IRepro.A(int value) - { - } } - """); - } - - [Fact] - public async Task TestPreserveReadOnly() - { - await TestInRegularAndScriptAsync( - """ - interface IRepro + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52020")] + public async Task TestWithMismatchedDefaultParameterValues() + { + await TestInRegularAndScriptAsync( + """ + interface IRepro + { + void A(int value = 0); + } + + class Repro : IRepro + { + public void [||]A(int value = 1) { - void A(); } + } + """, + """ + interface IRepro + { + void A(int value = 0); + } - class Repro : IRepro + class Repro : IRepro + { + void IRepro.A(int value = 1) { - public readonly void [||]A() - { - } } - """, - """ - interface IRepro + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52020")] + public async Task TestWithMismatchedDefault1() + { + await TestInRegularAndScriptAsync( + """ + interface IRepro + { + void A(int value); + } + + class Repro : IRepro + { + public void [||]A(int value = 1) { - void A(); } + } + """, + """ + interface IRepro + { + void A(int value); + } - class Repro : IRepro + class Repro : IRepro + { + void IRepro.A(int value = 1) { - readonly void IRepro.A() - { - } } - """); - } - - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72024")] - public async Task TestFieldEvent() - { - await TestInRegularAndScriptAsync( - """ - using System; + } + """); + } - interface IGoo { event Action E; void M(); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52020")] + public async Task TestWithMismatchedDefault2() + { + await TestInRegularAndScriptAsync( + """ + interface IRepro + { + void A(int value = 0); + } - class C : IGoo + class Repro : IRepro + { + public void [||]A(int value) { - public event Action E; - public void [||]M() { } } - """, - """ - using System; - - interface IGoo { event Action E; void M(); } - - class C : IGoo + } + """, + """ + interface IRepro + { + void A(int value = 0); + } + + class Repro : IRepro + { + void IRepro.A(int value) { - public event Action E; - void IGoo.M() { } } - """, index: SameInterface); - } - - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72024")] - public async Task TestPropertyEvent() - { - await TestInRegularAndScriptAsync( - """ - using System; + } + """); + } - interface IGoo { event Action E; } + [Fact] + public async Task TestPreserveReadOnly() + { + await TestInRegularAndScriptAsync( + """ + interface IRepro + { + void A(); + } - class C : IGoo + class Repro : IRepro + { + public readonly void [||]A() { - public event Action [||]E { add { } remove { } }; } - """, - """ - using System; - - interface IGoo { event Action E; } - - class C : IGoo + } + """, + """ + interface IRepro + { + void A(); + } + + class Repro : IRepro + { + readonly void IRepro.A() { - event Action IGoo.E { add { } remove { } }; } - """, index: SingleMember); - } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72024")] + public async Task TestFieldEvent() + { + await TestInRegularAndScriptAsync( + """ + using System; + + interface IGoo { event Action E; void M(); } + + class C : IGoo + { + public event Action E; + public void [||]M() { } + } + """, + """ + using System; + + interface IGoo { event Action E; void M(); } + + class C : IGoo + { + public event Action E; + void IGoo.M() { } + } + """, index: SameInterface); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72024")] + public async Task TestPropertyEvent() + { + await TestInRegularAndScriptAsync( + """ + using System; + + interface IGoo { event Action E; } + + class C : IGoo + { + public event Action [||]E { add { } remove { } }; + } + """, + """ + using System; + + interface IGoo { event Action E; } + + class C : IGoo + { + event Action IGoo.E { add { } remove { } }; + } + """, index: SingleMember); } } diff --git a/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs b/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs index 29edb1e7107ee..bb48f4784f1fc 100644 --- a/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs +++ b/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs @@ -8831,5 +8831,26 @@ public async Task TestCollectionExpression_Start_Typeless() "var x = $$[1, 2]"; await TestAsync(source); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/71638")] + public async Task TestAnonymousType() + { + var markup = """ + _ = new + { + @string = "" + }.$$@string; + """; + var description = $"string 'a.@string {{ get; }}"; + + await VerifyWithMscorlib45Async(markup, new[] + { + MainDescription(description), + AnonymousTypes( +$@" +{FeaturesResources.Types_colon} + 'a {FeaturesResources.is_} new {{ string @string }}") + }); + } } } diff --git a/src/EditorFeatures/CSharpTest/Structure/ArgumentListSyntaxStructureTests.cs b/src/EditorFeatures/CSharpTest/Structure/ArgumentListSyntaxStructureTests.cs new file mode 100644 index 0000000000000..487e524c168ec --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Structure/ArgumentListSyntaxStructureTests.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Structure; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Structure; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Structure; + +[Trait(Traits.Feature, Traits.Features.Outlining)] +public class ArgumentListSyntaxStructureTests : AbstractCSharpSyntaxNodeStructureTests +{ + internal override AbstractSyntaxStructureProvider CreateProvider() => new ArgumentListStructureProvider(); + + [Fact] + public async Task TestInvocationExpressionSingleLine() + { + var code = """ + var x = M$$(); + """; + + await VerifyBlockSpansAsync(code); + } + + [Fact] + public async Task TestInvocationExpressionTwoArgumentsInTwoLines() + { + var code = """ + var x = M$$("Hello", + "World"); + """; + + await VerifyBlockSpansAsync(code); + } + + [Fact] + public async Task TestInvocationExpressionThreeLines() + { + var code = """ + var x = M$${|span:( + "", + "")|}; + """; + + await VerifyBlockSpansAsync(code, + Region("span", CSharpStructureHelpers.Ellipsis, autoCollapse: false)); + } + + [Fact] + public async Task TestTwoInvocationExpressionsThreeLines() + { + // The inner argument list should be collapsible, but the outer one shouldn't. + // While this test shows both as collapsible, they will be deduplicated by AbstractBlockStructureProvider + // This test only tests ArgumentListStructureProvider specifically, so it doesn't show the deduplication. + // Tests in BlockStructureServiceTests show the overall behavior accurately. + var testInner = """ + var x = M(M$${|span:( + "", + "")|}); + """; + + await VerifyBlockSpansAsync(testInner, + Region("span", CSharpStructureHelpers.Ellipsis, autoCollapse: false)); + + var testOuter = """ + var x = M$${|span:(M( + "", + ""))|}; + """; + + await VerifyBlockSpansAsync(testOuter, + Region("span", CSharpStructureHelpers.Ellipsis, autoCollapse: false)); + } + + [Fact] + public async Task TestObjectCreationSingleLine() + { + var code = """ + var x = new C$$(); + """; + + await VerifyBlockSpansAsync(code); + } + + [Fact] + public async Task TestObjectCreationThreeLines() + { + var code = """ + var x = new C$${|span:( + "", + "")|}; + """; + + await VerifyBlockSpansAsync(code, + Region("span", CSharpStructureHelpers.Ellipsis, autoCollapse: false)); + } + + [Fact] + public async Task TestImplicitObjectCreationSingleLine() + { + var code = """ + C x = new$$(); + """; + + await VerifyBlockSpansAsync(code); + } + + [Fact] + public async Task TestImplicitObjectCreationThreeLines() + { + var code = """ + C x = new$${|span:( + "", + "")|}; + """; + + await VerifyBlockSpansAsync(code, + Region("span", CSharpStructureHelpers.Ellipsis, autoCollapse: false)); + } +} diff --git a/src/EditorFeatures/CSharpTest/Structure/MetadataAsSource/InvalidIdentifierStructureTests.cs b/src/EditorFeatures/CSharpTest/Structure/MetadataAsSource/InvalidIdentifierStructureTests.cs index b13e56d2451c9..4c7aa49f62348 100644 --- a/src/EditorFeatures/CSharpTest/Structure/MetadataAsSource/InvalidIdentifierStructureTests.cs +++ b/src/EditorFeatures/CSharpTest/Structure/MetadataAsSource/InvalidIdentifierStructureTests.cs @@ -71,8 +71,7 @@ public async Task IdentifierThatLooksLikeCode() """; await VerifyBlockSpansAsync(code, - Region("textspan1", "hint1", CSharpStructureHelpers.Ellipsis, autoCollapse: false), - Region("textspan2", "hint2", CSharpStructureHelpers.Ellipsis, autoCollapse: false), - Region("textspan3", "/* now everything is commented (); ...", autoCollapse: true)); + Region("textspan3", "/* now everything is commented (); ...", autoCollapse: true), + Region("textspan1", "hint1", CSharpStructureHelpers.Ellipsis, autoCollapse: false)); } } diff --git a/src/EditorFeatures/Core.Wpf/Interactive/InteractiveDocumentNavigationServiceFactory.cs b/src/EditorFeatures/Core.Wpf/Interactive/InteractiveDocumentNavigationServiceFactory.cs index 0cc118e18409c..2800938b291de 100644 --- a/src/EditorFeatures/Core.Wpf/Interactive/InteractiveDocumentNavigationServiceFactory.cs +++ b/src/EditorFeatures/Core.Wpf/Interactive/InteractiveDocumentNavigationServiceFactory.cs @@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.Interactive { - [ExportWorkspaceServiceFactory(typeof(IDocumentNavigationService), WorkspaceKind.Interactive), Shared] + [ExportWorkspaceServiceFactory(typeof(IDocumentNavigationService), [WorkspaceKind.Interactive]), Shared] internal sealed class InteractiveDocumentNavigationServiceFactory : IWorkspaceServiceFactory { private readonly IDocumentNavigationService _singleton; diff --git a/src/EditorFeatures/Core.Wpf/Interactive/InteractiveGlobalUndoServiceFactory.cs b/src/EditorFeatures/Core.Wpf/Interactive/InteractiveGlobalUndoServiceFactory.cs index 2e9b0946cba22..f3cdb820b317c 100644 --- a/src/EditorFeatures/Core.Wpf/Interactive/InteractiveGlobalUndoServiceFactory.cs +++ b/src/EditorFeatures/Core.Wpf/Interactive/InteractiveGlobalUndoServiceFactory.cs @@ -14,7 +14,7 @@ namespace Microsoft.CodeAnalysis.Interactive { - [ExportWorkspaceServiceFactory(typeof(IGlobalUndoService), WorkspaceKind.Interactive), Shared] + [ExportWorkspaceServiceFactory(typeof(IGlobalUndoService), [WorkspaceKind.Interactive]), Shared] internal sealed class InteractiveGlobalUndoServiceFactory : IWorkspaceServiceFactory { private readonly GlobalUndoService _singleton; diff --git a/src/EditorFeatures/Core.Wpf/Interactive/InteractiveSupportsFeatureService.cs b/src/EditorFeatures/Core.Wpf/Interactive/InteractiveSupportsFeatureService.cs index 0957d40d78b5a..97e4601e660f0 100644 --- a/src/EditorFeatures/Core.Wpf/Interactive/InteractiveSupportsFeatureService.cs +++ b/src/EditorFeatures/Core.Wpf/Interactive/InteractiveSupportsFeatureService.cs @@ -14,7 +14,7 @@ namespace Microsoft.CodeAnalysis.Interactive { internal sealed class InteractiveSupportsFeatureService { - [ExportWorkspaceService(typeof(ITextBufferSupportsFeatureService), WorkspaceKind.Interactive), Shared] + [ExportWorkspaceService(typeof(ITextBufferSupportsFeatureService), [WorkspaceKind.Interactive]), Shared] internal class InteractiveTextBufferSupportsFeatureService : ITextBufferSupportsFeatureService { [ImportingConstructor] @@ -52,7 +52,7 @@ public bool SupportsNavigationToAnyPosition(ITextBuffer textBuffer) => true; } - [ExportWorkspaceService(typeof(IDocumentSupportsFeatureService), WorkspaceKind.Interactive), Shared] + [ExportWorkspaceService(typeof(IDocumentSupportsFeatureService), [WorkspaceKind.Interactive]), Shared] internal class InteractiveDocumentSupportsFeatureService : IDocumentSupportsFeatureService { [ImportingConstructor] diff --git a/src/EditorFeatures/Core.Wpf/Interactive/InteractiveTextUndoHistoryWorkspaceServiceFactory.cs b/src/EditorFeatures/Core.Wpf/Interactive/InteractiveTextUndoHistoryWorkspaceServiceFactory.cs index 514666010f028..a97c8a627d52c 100644 --- a/src/EditorFeatures/Core.Wpf/Interactive/InteractiveTextUndoHistoryWorkspaceServiceFactory.cs +++ b/src/EditorFeatures/Core.Wpf/Interactive/InteractiveTextUndoHistoryWorkspaceServiceFactory.cs @@ -14,7 +14,7 @@ namespace Microsoft.CodeAnalysis.Interactive { - [ExportWorkspaceServiceFactory(typeof(ITextUndoHistoryWorkspaceService), WorkspaceKind.Interactive), Shared] + [ExportWorkspaceServiceFactory(typeof(ITextUndoHistoryWorkspaceService), [WorkspaceKind.Interactive]), Shared] internal sealed class InteractiveTextUndoHistoryWorkspaceServiceFactory : IWorkspaceServiceFactory { private readonly TextUndoHistoryWorkspaceService _serviceSingleton; diff --git a/src/EditorFeatures/Core/AddImportOnPaste/AddImportOnPasteOptionsStorage.cs b/src/EditorFeatures/Core/AddImportOnPaste/AddImportOnPasteOptionsStorage.cs index 61beb2d36542b..15835761d66b4 100644 --- a/src/EditorFeatures/Core/AddImportOnPaste/AddImportOnPasteOptionsStorage.cs +++ b/src/EditorFeatures/Core/AddImportOnPaste/AddImportOnPasteOptionsStorage.cs @@ -4,16 +4,15 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.AddImportOnPaste +namespace Microsoft.CodeAnalysis.AddImportOnPaste; + +internal static class AddImportOnPasteOptionsStorage { - internal static class AddImportOnPasteOptionsStorage - { - /// - /// This option was previously "bool?" to accomodate different supported defaults - /// that were being provided via remote settings. The feature has stabalized and moved - /// to on by default, so the storage location was changed to - /// TextEditor.%LANGUAGE%.Specific.AddImportsOnPaste2 (note the 2 suffix). - /// - public static readonly PerLanguageOption2 AddImportsOnPaste = new("dotnet_add_imports_on_paste", defaultValue: true); - } + /// + /// This option was previously "bool?" to accomodate different supported defaults + /// that were being provided via remote settings. The feature has stabalized and moved + /// to on by default, so the storage location was changed to + /// TextEditor.%LANGUAGE%.Specific.AddImportsOnPaste2 (note the 2 suffix). + /// + public static readonly PerLanguageOption2 AddImportsOnPaste = new("dotnet_add_imports_on_paste", defaultValue: true); } diff --git a/src/EditorFeatures/Core/AddImports/AbstractAddImportsPasteCommandHandler.cs b/src/EditorFeatures/Core/AddImports/AbstractAddImportsPasteCommandHandler.cs index 8a31daebe5849..9a6b5f99f6e92 100644 --- a/src/EditorFeatures/Core/AddImports/AbstractAddImportsPasteCommandHandler.cs +++ b/src/EditorFeatures/Core/AddImports/AbstractAddImportsPasteCommandHandler.cs @@ -24,153 +24,152 @@ using Microsoft.VisualStudio.Threading; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract class AbstractAddImportsPasteCommandHandler( + IThreadingContext threadingContext, + IGlobalOptionService globalOptions, + IAsynchronousOperationListenerProvider listenerProvider) : IChainedCommandHandler { - internal abstract class AbstractAddImportsPasteCommandHandler( - IThreadingContext threadingContext, - IGlobalOptionService globalOptions, - IAsynchronousOperationListenerProvider listenerProvider) : IChainedCommandHandler - { - /// - /// The command handler display name - /// - public abstract string DisplayName { get; } + /// + /// The command handler display name + /// + public abstract string DisplayName { get; } - /// - /// The thread await dialog text shown to the user if the operation takes a long time - /// - protected abstract string DialogText { get; } + /// + /// The thread await dialog text shown to the user if the operation takes a long time + /// + protected abstract string DialogText { get; } - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IGlobalOptionService _globalOptions = globalOptions; - private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.AddImportsOnPaste); + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.AddImportsOnPaste); - public CommandState GetCommandState(PasteCommandArgs args, Func nextCommandHandler) - => nextCommandHandler(); + public CommandState GetCommandState(PasteCommandArgs args, Func nextCommandHandler) + => nextCommandHandler(); - public void ExecuteCommand(PasteCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + public void ExecuteCommand(PasteCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + var language = args.SubjectBuffer.GetLanguageName(); + + // If the feature is not explicitly enabled we can exit early + if (language is null || !_globalOptions.GetOption(AddImportOnPasteOptionsStorage.AddImportsOnPaste, language)) { - var language = args.SubjectBuffer.GetLanguageName(); - - // If the feature is not explicitly enabled we can exit early - if (language is null || !_globalOptions.GetOption(AddImportOnPasteOptionsStorage.AddImportsOnPaste, language)) - { - nextCommandHandler(); - return; - } - - // Capture the pre-paste caret position - var caretPosition = args.TextView.GetCaretPoint(args.SubjectBuffer); - if (!caretPosition.HasValue) - { - nextCommandHandler(); - return; - } - - // Create a tracking span from the pre-paste caret position that will grow as text is inserted. - var trackingSpan = caretPosition.Value.Snapshot.CreateTrackingSpan(caretPosition.Value.Position, 0, SpanTrackingMode.EdgeInclusive); - - // Perform the paste command before adding imports nextCommandHandler(); + return; + } + + // Capture the pre-paste caret position + var caretPosition = args.TextView.GetCaretPoint(args.SubjectBuffer); + if (!caretPosition.HasValue) + { + nextCommandHandler(); + return; + } + + // Create a tracking span from the pre-paste caret position that will grow as text is inserted. + var trackingSpan = caretPosition.Value.Snapshot.CreateTrackingSpan(caretPosition.Value.Position, 0, SpanTrackingMode.EdgeInclusive); + + // Perform the paste command before adding imports + nextCommandHandler(); + + if (executionContext.OperationContext.UserCancellationToken.IsCancellationRequested) + { + return; + } + + try + { + ExecuteCommandWorker(args, executionContext, trackingSpan); + } + catch (OperationCanceledException) + { + // According to Editor command handler API guidelines, it's best if we return early if cancellation + // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already + // calling nextCommandHandler(). + } + } - if (executionContext.OperationContext.UserCancellationToken.IsCancellationRequested) - { - return; - } - - try - { - ExecuteCommandWorker(args, executionContext, trackingSpan); - } - catch (OperationCanceledException) - { - // According to Editor command handler API guidelines, it's best if we return early if cancellation - // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already - // calling nextCommandHandler(). - } + private void ExecuteCommandWorker( + PasteCommandArgs args, + CommandExecutionContext executionContext, + ITrackingSpan trackingSpan) + { + if (!args.SubjectBuffer.CanApplyChangeDocumentToWorkspace()) + { + return; } - private void ExecuteCommandWorker( - PasteCommandArgs args, - CommandExecutionContext executionContext, - ITrackingSpan trackingSpan) + // Don't perform work if we're inside the interactive window + if (args.TextView.IsNotSurfaceBufferOfTextView(args.SubjectBuffer)) { - if (!args.SubjectBuffer.CanApplyChangeDocumentToWorkspace()) - { - return; - } - - // Don't perform work if we're inside the interactive window - if (args.TextView.IsNotSurfaceBufferOfTextView(args.SubjectBuffer)) - { - return; - } - - // Applying the post-paste snapshot to the tracking span gives us the span of pasted text. - var snapshotSpan = trackingSpan.GetSpan(args.SubjectBuffer.CurrentSnapshot); - - var sourceTextContainer = args.SubjectBuffer.AsTextContainer(); - if (!Workspace.TryGetWorkspace(sourceTextContainer, out var workspace)) - { - return; - } - - var document = sourceTextContainer.GetOpenDocumentInCurrentContext(); - if (document is null) - { - return; - } - - // We're showing our own UI, ensure the editor doesn't show anything itself. - executionContext.OperationContext.TakeOwnership(); - - var token = _listener.BeginAsyncOperation(nameof(ExecuteAsync)); - - ExecuteAsync(document, snapshotSpan, args.TextView) - .ReportNonFatalErrorAsync() - .CompletesAsyncOperation(token); + return; } - private async Task ExecuteAsync(Document document, SnapshotSpan snapshotSpan, ITextView textView) + // Applying the post-paste snapshot to the tracking span gives us the span of pasted text. + var snapshotSpan = trackingSpan.GetSpan(args.SubjectBuffer.CurrentSnapshot); + + var sourceTextContainer = args.SubjectBuffer.AsTextContainer(); + if (!Workspace.TryGetWorkspace(sourceTextContainer, out var workspace)) { - _threadingContext.ThrowIfNotOnUIThread(); + return; + } - var indicatorFactory = document.Project.Solution.Services.GetRequiredService(); - using var backgroundWorkContext = indicatorFactory.Create( - textView, - snapshotSpan, - DialogText, - cancelOnEdit: true, - cancelOnFocusLost: true); + var document = sourceTextContainer.GetOpenDocumentInCurrentContext(); + if (document is null) + { + return; + } - var cancellationToken = backgroundWorkContext.UserCancellationToken; + // We're showing our own UI, ensure the editor doesn't show anything itself. + executionContext.OperationContext.TakeOwnership(); - // We're going to log the same thing on success or failure since this blocks the UI thread. This measurement is - // intended to tell us how long we're blocking the user from typing with this action. - using var blockLogger = Logger.LogBlock(FunctionId.CommandHandler_Paste_ImportsOnPaste, KeyValueLogMessage.Create(LogType.UserAction), cancellationToken); + var token = _listener.BeginAsyncOperation(nameof(ExecuteAsync)); - await TaskScheduler.Default; + ExecuteAsync(document, snapshotSpan, args.TextView) + .ReportNonFatalErrorAsync() + .CompletesAsyncOperation(token); + } + + private async Task ExecuteAsync(Document document, SnapshotSpan snapshotSpan, ITextView textView) + { + _threadingContext.ThrowIfNotOnUIThread(); + + var indicatorFactory = document.Project.Solution.Services.GetRequiredService(); + using var backgroundWorkContext = indicatorFactory.Create( + textView, + snapshotSpan, + DialogText, + cancelOnEdit: true, + cancelOnFocusLost: true); - var addMissingImportsService = document.GetRequiredLanguageService(); + var cancellationToken = backgroundWorkContext.UserCancellationToken; - var cleanupOptions = await document.GetCodeCleanupOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false); + // We're going to log the same thing on success or failure since this blocks the UI thread. This measurement is + // intended to tell us how long we're blocking the user from typing with this action. + using var blockLogger = Logger.LogBlock(FunctionId.CommandHandler_Paste_ImportsOnPaste, KeyValueLogMessage.Create(LogType.UserAction), cancellationToken); - var options = new AddMissingImportsOptions( - CleanupOptions: cleanupOptions, - HideAdvancedMembers: _globalOptions.GetOption(CompletionOptionsStorage.HideAdvancedMembers, document.Project.Language)); + await TaskScheduler.Default; - var textSpan = snapshotSpan.Span.ToTextSpan(); - var updatedDocument = await addMissingImportsService.AddMissingImportsAsync( - document, textSpan, options, backgroundWorkContext.GetCodeAnalysisProgress(), cancellationToken).ConfigureAwait(false); + var addMissingImportsService = document.GetRequiredLanguageService(); - if (updatedDocument is null) - { - return; - } + var cleanupOptions = await document.GetCodeCleanupOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false); - // Required to switch back to the UI thread to call TryApplyChanges - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - document.Project.Solution.Workspace.TryApplyChanges(updatedDocument.Project.Solution); + var options = new AddMissingImportsOptions( + CleanupOptions: cleanupOptions, + HideAdvancedMembers: _globalOptions.GetOption(CompletionOptionsStorage.HideAdvancedMembers, document.Project.Language)); + + var textSpan = snapshotSpan.Span.ToTextSpan(); + var updatedDocument = await addMissingImportsService.AddMissingImportsAsync( + document, textSpan, options, backgroundWorkContext.GetCodeAnalysisProgress(), cancellationToken).ConfigureAwait(false); + + if (updatedDocument is null) + { + return; } + + // Required to switch back to the UI thread to call TryApplyChanges + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + document.Project.Solution.Workspace.TryApplyChanges(updatedDocument.Project.Solution); } } diff --git a/src/EditorFeatures/Core/AutomaticCompletion/AbstractAutomaticLineEnderCommandHandler.cs b/src/EditorFeatures/Core/AutomaticCompletion/AbstractAutomaticLineEnderCommandHandler.cs index 11c427dd46f4b..9515c1e4ed9c9 100644 --- a/src/EditorFeatures/Core/AutomaticCompletion/AbstractAutomaticLineEnderCommandHandler.cs +++ b/src/EditorFeatures/Core/AutomaticCompletion/AbstractAutomaticLineEnderCommandHandler.cs @@ -16,201 +16,200 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.AutomaticCompletion +namespace Microsoft.CodeAnalysis.AutomaticCompletion; + +internal abstract class AbstractAutomaticLineEnderCommandHandler : + IChainedCommandHandler { - internal abstract class AbstractAutomaticLineEnderCommandHandler : - IChainedCommandHandler + private readonly ITextUndoHistoryRegistry _undoRegistry; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; + + public readonly EditorOptionsService EditorOptionsService; + + public string DisplayName => EditorFeaturesResources.Automatic_Line_Ender; + + protected AbstractAutomaticLineEnderCommandHandler( + ITextUndoHistoryRegistry undoRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService) { - private readonly ITextUndoHistoryRegistry _undoRegistry; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; + _undoRegistry = undoRegistry; + _editorOperationsFactoryService = editorOperationsFactoryService; + EditorOptionsService = editorOptionsService; + } - public readonly EditorOptionsService EditorOptionsService; + /// + /// get ending string if there is one + /// + protected abstract string? GetEndingString(ParsedDocument document, int position); - public string DisplayName => EditorFeaturesResources.Automatic_Line_Ender; + /// + /// do next action + /// + protected abstract void NextAction(IEditorOperations editorOperation, Action nextAction); - protected AbstractAutomaticLineEnderCommandHandler( - ITextUndoHistoryRegistry undoRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService) - { - _undoRegistry = undoRegistry; - _editorOperationsFactoryService = editorOperationsFactoryService; - EditorOptionsService = editorOptionsService; - } + /// + /// format after inserting ending string + /// + protected abstract IList FormatBasedOnEndToken(ParsedDocument document, int position, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken); - /// - /// get ending string if there is one - /// - protected abstract string? GetEndingString(ParsedDocument document, int position); + /// + /// special cases where we do not want to do line completion but just fall back to line break and formatting. + /// + protected abstract bool TreatAsReturn(ParsedDocument document, int caretPosition, CancellationToken cancellationToken); - /// - /// do next action - /// - protected abstract void NextAction(IEditorOperations editorOperation, Action nextAction); + /// + /// Add or remove the braces for . + /// + protected abstract void ModifySelectedNode(AutomaticLineEnderCommandArgs args, ParsedDocument document, SyntaxNode selectedNode, bool addBrace, int caretPosition, CancellationToken cancellationToken); - /// - /// format after inserting ending string - /// - protected abstract IList FormatBasedOnEndToken(ParsedDocument document, int position, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken); + /// + /// Get the syntax node needs add/remove braces. + /// + protected abstract (SyntaxNode selectedNode, bool addBrace)? GetValidNodeToModifyBraces(ParsedDocument document, int caretPosition, CancellationToken cancellationToken); - /// - /// special cases where we do not want to do line completion but just fall back to line break and formatting. - /// - protected abstract bool TreatAsReturn(ParsedDocument document, int caretPosition, CancellationToken cancellationToken); + public CommandState GetCommandState(AutomaticLineEnderCommandArgs args, Func nextHandler) + => CommandState.Available; - /// - /// Add or remove the braces for . - /// - protected abstract void ModifySelectedNode(AutomaticLineEnderCommandArgs args, ParsedDocument document, SyntaxNode selectedNode, bool addBrace, int caretPosition, CancellationToken cancellationToken); + public void ExecuteCommand(AutomaticLineEnderCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + // get editor operation + var operations = _editorOperationsFactoryService.GetEditorOperations(args.TextView); + if (operations == null) + { + nextHandler(); + return; + } - /// - /// Get the syntax node needs add/remove braces. - /// - protected abstract (SyntaxNode selectedNode, bool addBrace)? GetValidNodeToModifyBraces(ParsedDocument document, int caretPosition, CancellationToken cancellationToken); + var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + { + NextAction(operations, nextHandler); + return; + } - public CommandState GetCommandState(AutomaticLineEnderCommandArgs args, Func nextHandler) - => CommandState.Available; + // feature off + if (!EditorOptionsService.GlobalOptions.GetOption(AutomaticLineEnderOptionsStorage.AutomaticLineEnder)) + { + NextAction(operations, nextHandler); + return; + } - public void ExecuteCommand(AutomaticLineEnderCommandArgs args, Action nextHandler, CommandExecutionContext context) + using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Automatically_completing)) { - // get editor operation - var operations = _editorOperationsFactoryService.GetEditorOperations(args.TextView); - if (operations == null) + var cancellationToken = context.OperationContext.UserCancellationToken; + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + + // caret is not on the subject buffer. nothing we can do + var caret = args.TextView.GetCaretPoint(args.SubjectBuffer); + if (!caret.HasValue) { - nextHandler(); + NextAction(operations, nextHandler); return; } - var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) + var caretPosition = caret.Value; + // special cases where we treat this command simply as Return. + if (TreatAsReturn(parsedDocument, caretPosition, cancellationToken)) { - NextAction(operations, nextHandler); + // leave it to the VS editor to handle this command. + // VS editor's default implementation of SmartBreakLine is simply BreakLine, which inserts + // a new line and positions the caret with smart indent. + nextHandler(); return; } - // feature off - if (!EditorOptionsService.GlobalOptions.GetOption(AutomaticLineEnderOptionsStorage.AutomaticLineEnder)) + var subjectLineWhereCaretIsOn = caretPosition.GetContainingLine(); + + // Two possible operations + // 1. Add/remove the brace for the selected syntax node (only for C#) + // 2. Append an ending string to the line. (For C#, it is semicolon ';', For VB, it is underline '_') + + // Check if the node could be used to add/remove brace. + var selectNodeAndOperationKind = GetValidNodeToModifyBraces(parsedDocument, caretPosition, cancellationToken); + if (selectNodeAndOperationKind != null) { + var (selectedNode, addBrace) = selectNodeAndOperationKind.Value; + using var transaction = args.TextView.CreateEditTransaction(EditorFeaturesResources.Automatic_Line_Ender, _undoRegistry, _editorOperationsFactoryService); + ModifySelectedNode(args, parsedDocument, selectedNode, addBrace, caretPosition, cancellationToken); NextAction(operations, nextHandler); + transaction.Complete(); return; } - using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Automatically_completing)) + // Check if we could find the ending position + var endingInsertionPosition = GetInsertionPositionForEndingString(parsedDocument, subjectLineWhereCaretIsOn); + if (endingInsertionPosition != null) { - var cancellationToken = context.OperationContext.UserCancellationToken; - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - - // caret is not on the subject buffer. nothing we can do - var caret = args.TextView.GetCaretPoint(args.SubjectBuffer); - if (!caret.HasValue) - { - NextAction(operations, nextHandler); - return; - } - - var caretPosition = caret.Value; - // special cases where we treat this command simply as Return. - if (TreatAsReturn(parsedDocument, caretPosition, cancellationToken)) - { - // leave it to the VS editor to handle this command. - // VS editor's default implementation of SmartBreakLine is simply BreakLine, which inserts - // a new line and positions the caret with smart indent. - nextHandler(); - return; - } - - var subjectLineWhereCaretIsOn = caretPosition.GetContainingLine(); - - // Two possible operations - // 1. Add/remove the brace for the selected syntax node (only for C#) - // 2. Append an ending string to the line. (For C#, it is semicolon ';', For VB, it is underline '_') - - // Check if the node could be used to add/remove brace. - var selectNodeAndOperationKind = GetValidNodeToModifyBraces(parsedDocument, caretPosition, cancellationToken); - if (selectNodeAndOperationKind != null) - { - var (selectedNode, addBrace) = selectNodeAndOperationKind.Value; - using var transaction = args.TextView.CreateEditTransaction(EditorFeaturesResources.Automatic_Line_Ender, _undoRegistry, _editorOperationsFactoryService); - ModifySelectedNode(args, parsedDocument, selectedNode, addBrace, caretPosition, cancellationToken); - NextAction(operations, nextHandler); - transaction.Complete(); - return; - } - - // Check if we could find the ending position - var endingInsertionPosition = GetInsertionPositionForEndingString(parsedDocument, subjectLineWhereCaretIsOn); - if (endingInsertionPosition != null) - { - using var transaction = args.TextView.CreateEditTransaction(EditorFeaturesResources.Automatic_Line_Ender, _undoRegistry, _editorOperationsFactoryService); - var formattingOptions = args.SubjectBuffer.GetSyntaxFormattingOptions(EditorOptionsService, parsedDocument.LanguageServices, explicitFormat: false); - InsertEnding(args.TextView, args.SubjectBuffer, parsedDocument, endingInsertionPosition.Value, caretPosition, formattingOptions, cancellationToken); - NextAction(operations, nextHandler); - transaction.Complete(); - return; - } - - // Neither of the two operations could be performed - using var editTransaction = args.TextView.CreateEditTransaction(EditorFeaturesResources.Automatic_Line_Ender, _undoRegistry, _editorOperationsFactoryService); + using var transaction = args.TextView.CreateEditTransaction(EditorFeaturesResources.Automatic_Line_Ender, _undoRegistry, _editorOperationsFactoryService); + var formattingOptions = args.SubjectBuffer.GetSyntaxFormattingOptions(EditorOptionsService, parsedDocument.LanguageServices, explicitFormat: false); + InsertEnding(args.TextView, args.SubjectBuffer, parsedDocument, endingInsertionPosition.Value, caretPosition, formattingOptions, cancellationToken); NextAction(operations, nextHandler); - editTransaction.Complete(); + transaction.Complete(); + return; } - } - /// - /// return insertion point for the ending string - /// - private static int? GetInsertionPositionForEndingString(ParsedDocument document, ITextSnapshotLine line) - { - // find last token on the line - var token = document.Root.FindTokenOnLeftOfPosition(line.End); - if (token.RawKind == 0) - return null; - - // bug # 16770 - // don't do anything if token is multiline token such as verbatim string - if (line.End < token.Span.End) - return null; - - // if there is only whitespace, token doesn't need to be on same line - var text = document.Text; - if (string.IsNullOrWhiteSpace(text.ToString(TextSpan.FromBounds(token.Span.End, line.End)))) - return line.End; - - // if token is on different line than caret but caret line is empty, we insert ending point at the end of the line - if (text.Lines.IndexOf(token.Span.End) != text.Lines.IndexOf(line.End)) - return string.IsNullOrWhiteSpace(line.GetText()) ? line.End : null; - - return token.Span.End; + // Neither of the two operations could be performed + using var editTransaction = args.TextView.CreateEditTransaction(EditorFeaturesResources.Automatic_Line_Ender, _undoRegistry, _editorOperationsFactoryService); + NextAction(operations, nextHandler); + editTransaction.Complete(); } + } - /// - /// insert ending string if there is one to insert - /// - private void InsertEnding( - ITextView textView, - ITextBuffer buffer, - ParsedDocument document, - int insertPosition, - SnapshotPoint caretPosition, - SyntaxFormattingOptions formattingOptions, - CancellationToken cancellationToken) - { - // 1. Move the caret to line end. - textView.TryMoveCaretToAndEnsureVisible(caretPosition.GetContainingLine().End); + /// + /// return insertion point for the ending string + /// + private static int? GetInsertionPositionForEndingString(ParsedDocument document, ITextSnapshotLine line) + { + // find last token on the line + var token = document.Root.FindTokenOnLeftOfPosition(line.End); + if (token.RawKind == 0) + return null; + + // bug # 16770 + // don't do anything if token is multiline token such as verbatim string + if (line.End < token.Span.End) + return null; + + // if there is only whitespace, token doesn't need to be on same line + var text = document.Text; + if (string.IsNullOrWhiteSpace(text.ToString(TextSpan.FromBounds(token.Span.End, line.End)))) + return line.End; + + // if token is on different line than caret but caret line is empty, we insert ending point at the end of the line + if (text.Lines.IndexOf(token.Span.End) != text.Lines.IndexOf(line.End)) + return string.IsNullOrWhiteSpace(line.GetText()) ? line.End : null; + + return token.Span.End; + } - // 2. Insert ending to the document. - var newDocument = document; - var endingString = GetEndingString(document, caretPosition); - if (endingString != null) - { - var insertChange = new TextChange(new TextSpan(insertPosition, 0), endingString); - buffer.ApplyChange(insertChange); - newDocument = document.WithChange(insertChange, cancellationToken); - } + /// + /// insert ending string if there is one to insert + /// + private void InsertEnding( + ITextView textView, + ITextBuffer buffer, + ParsedDocument document, + int insertPosition, + SnapshotPoint caretPosition, + SyntaxFormattingOptions formattingOptions, + CancellationToken cancellationToken) + { + // 1. Move the caret to line end. + textView.TryMoveCaretToAndEnsureVisible(caretPosition.GetContainingLine().End); - // 3. format the document and apply the changes to the workspace - var changes = FormatBasedOnEndToken(newDocument, insertPosition, formattingOptions, cancellationToken); - buffer.ApplyChanges(changes); + // 2. Insert ending to the document. + var newDocument = document; + var endingString = GetEndingString(document, caretPosition); + if (endingString != null) + { + var insertChange = new TextChange(new TextSpan(insertPosition, 0), endingString); + buffer.ApplyChange(insertChange); + newDocument = document.WithChange(insertChange, cancellationToken); } + + // 3. format the document and apply the changes to the workspace + var changes = FormatBasedOnEndToken(newDocument, insertPosition, formattingOptions, cancellationToken); + buffer.ApplyChanges(changes); } } diff --git a/src/EditorFeatures/Core/AutomaticCompletion/AbstractBraceCompletionServiceFactory.cs b/src/EditorFeatures/Core/AutomaticCompletion/AbstractBraceCompletionServiceFactory.cs index e3b9376318f60..809f5c590a93b 100644 --- a/src/EditorFeatures/Core/AutomaticCompletion/AbstractBraceCompletionServiceFactory.cs +++ b/src/EditorFeatures/Core/AutomaticCompletion/AbstractBraceCompletionServiceFactory.cs @@ -8,29 +8,28 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.BraceCompletion; -namespace Microsoft.CodeAnalysis.AutomaticCompletion +namespace Microsoft.CodeAnalysis.AutomaticCompletion; + +internal abstract class AbstractBraceCompletionServiceFactory : IBraceCompletionServiceFactory { - internal abstract class AbstractBraceCompletionServiceFactory : IBraceCompletionServiceFactory - { - private readonly ImmutableArray _braceCompletionServices; + private readonly ImmutableArray _braceCompletionServices; - protected AbstractBraceCompletionServiceFactory( - IEnumerable braceCompletionServices) - { - _braceCompletionServices = braceCompletionServices.ToImmutableArray(); - } + protected AbstractBraceCompletionServiceFactory( + IEnumerable braceCompletionServices) + { + _braceCompletionServices = braceCompletionServices.ToImmutableArray(); + } - public IBraceCompletionService? TryGetService(ParsedDocument document, int openingPosition, char openingBrace, CancellationToken cancellationToken) + public IBraceCompletionService? TryGetService(ParsedDocument document, int openingPosition, char openingBrace, CancellationToken cancellationToken) + { + foreach (var service in _braceCompletionServices) { - foreach (var service in _braceCompletionServices) + if (service.CanProvideBraceCompletion(openingBrace, openingPosition, document, cancellationToken)) { - if (service.CanProvideBraceCompletion(openingBrace, openingPosition, document, cancellationToken)) - { - return service; - } + return service; } - - return null; } + + return null; } } diff --git a/src/EditorFeatures/Core/AutomaticCompletion/AutomaticLineEnderOptionsStorage.cs b/src/EditorFeatures/Core/AutomaticCompletion/AutomaticLineEnderOptionsStorage.cs index 07d195560a047..b6831a47a02b1 100644 --- a/src/EditorFeatures/Core/AutomaticCompletion/AutomaticLineEnderOptionsStorage.cs +++ b/src/EditorFeatures/Core/AutomaticCompletion/AutomaticLineEnderOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.AutomaticCompletion +namespace Microsoft.CodeAnalysis.AutomaticCompletion; + +internal static class AutomaticLineEnderOptionsStorage { - internal static class AutomaticLineEnderOptionsStorage - { - public static readonly Option2 AutomaticLineEnder = new("dotnet_enable_automatic_line_ender", defaultValue: true); - } + public static readonly Option2 AutomaticLineEnder = new("dotnet_enable_automatic_line_ender", defaultValue: true); } diff --git a/src/EditorFeatures/Core/AutomaticCompletion/BraceCompletionSessionProvider.BraceCompletionSession.cs b/src/EditorFeatures/Core/AutomaticCompletion/BraceCompletionSessionProvider.BraceCompletionSession.cs index 7cd8982080d0e..c6739a0767443 100644 --- a/src/EditorFeatures/Core/AutomaticCompletion/BraceCompletionSessionProvider.BraceCompletionSession.cs +++ b/src/EditorFeatures/Core/AutomaticCompletion/BraceCompletionSessionProvider.BraceCompletionSession.cs @@ -23,428 +23,427 @@ using Microsoft.VisualStudio.Text.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AutomaticCompletion +namespace Microsoft.CodeAnalysis.AutomaticCompletion; + +internal partial class BraceCompletionSessionProvider { - internal partial class BraceCompletionSessionProvider + // ported and modified from "Platform\Text\Impl\BraceCompletion\BraceCompletionDefaultSession.cs" + // + // we want to provide better context based brace completion but IBraceCompletionContext is too simple for that. + // fortunately, editor provides another extension point where we have more control over brace completion but we do not + // want to re-implement logics base session provider already provides. so I ported editor's default session and + // modified it little bit so that we can use it as base class. + private class BraceCompletionSession : IBraceCompletionSession { - // ported and modified from "Platform\Text\Impl\BraceCompletion\BraceCompletionDefaultSession.cs" - // - // we want to provide better context based brace completion but IBraceCompletionContext is too simple for that. - // fortunately, editor provides another extension point where we have more control over brace completion but we do not - // want to re-implement logics base session provider already provides. so I ported editor's default session and - // modified it little bit so that we can use it as base class. - private class BraceCompletionSession : IBraceCompletionSession + public char OpeningBrace { get; } + public char ClosingBrace { get; } + public ITrackingPoint OpeningPoint { get; private set; } + public ITrackingPoint ClosingPoint { get; private set; } + public ITextBuffer SubjectBuffer { get; } + public ITextView TextView { get; } + + private readonly ITextUndoHistory _undoHistory; + private readonly IEditorOperations _editorOperations; + private readonly EditorOptionsService _editorOptionsService; + private readonly IBraceCompletionService _service; + private readonly IThreadingContext _threadingContext; + + public BraceCompletionSession( + ITextView textView, ITextBuffer subjectBuffer, + SnapshotPoint openingPoint, char openingBrace, char closingBrace, ITextUndoHistory undoHistory, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService, IBraceCompletionService service, IThreadingContext threadingContext) { - public char OpeningBrace { get; } - public char ClosingBrace { get; } - public ITrackingPoint OpeningPoint { get; private set; } - public ITrackingPoint ClosingPoint { get; private set; } - public ITextBuffer SubjectBuffer { get; } - public ITextView TextView { get; } - - private readonly ITextUndoHistory _undoHistory; - private readonly IEditorOperations _editorOperations; - private readonly EditorOptionsService _editorOptionsService; - private readonly IBraceCompletionService _service; - private readonly IThreadingContext _threadingContext; - - public BraceCompletionSession( - ITextView textView, ITextBuffer subjectBuffer, - SnapshotPoint openingPoint, char openingBrace, char closingBrace, ITextUndoHistory undoHistory, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService, IBraceCompletionService service, IThreadingContext threadingContext) - { - TextView = textView; - SubjectBuffer = subjectBuffer; - OpeningBrace = openingBrace; - ClosingBrace = closingBrace; - ClosingPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(openingPoint.Position, PointTrackingMode.Positive); - _undoHistory = undoHistory; - _editorOperations = editorOperationsFactoryService.GetEditorOperations(textView); - _editorOptionsService = editorOptionsService; - _service = service; - _threadingContext = threadingContext; - } + TextView = textView; + SubjectBuffer = subjectBuffer; + OpeningBrace = openingBrace; + ClosingBrace = closingBrace; + ClosingPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(openingPoint.Position, PointTrackingMode.Positive); + _undoHistory = undoHistory; + _editorOperations = editorOperationsFactoryService.GetEditorOperations(textView); + _editorOptionsService = editorOptionsService; + _service = service; + _threadingContext = threadingContext; + } - #region IBraceCompletionSession Methods + #region IBraceCompletionSession Methods - public void Start() + public void Start() + { + _threadingContext.ThrowIfNotOnUIThread(); + // Brace completion is not cancellable. + if (!this.TryStart(CancellationToken.None)) { - _threadingContext.ThrowIfNotOnUIThread(); - // Brace completion is not cancellable. - if (!this.TryStart(CancellationToken.None)) - { - EndSession(); - } + EndSession(); } + } - private bool TryStart(CancellationToken cancellationToken) - { - _threadingContext.ThrowIfNotOnUIThread(); - var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot); + private bool TryStart(CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnUIThread(); + var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot); - if (closingSnapshotPoint.Position < 1) - { - Debug.Fail("The closing point was not found at the expected position."); - return false; - } + if (closingSnapshotPoint.Position < 1) + { + Debug.Fail("The closing point was not found at the expected position."); + return false; + } - var openingSnapshotPoint = closingSnapshotPoint.Subtract(1); + var openingSnapshotPoint = closingSnapshotPoint.Subtract(1); - if (openingSnapshotPoint.GetChar() != OpeningBrace) - { - // there is a bug in editor brace completion engine on projection buffer that already fixed in vs_pro. until that is FIed to use - // I will make this not to assert - // Debug.Fail("The opening brace was not found at the expected position."); - return false; - } + if (openingSnapshotPoint.GetChar() != OpeningBrace) + { + // there is a bug in editor brace completion engine on projection buffer that already fixed in vs_pro. until that is FIed to use + // I will make this not to assert + // Debug.Fail("The opening brace was not found at the expected position."); + return false; + } - OpeningPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(openingSnapshotPoint, PointTrackingMode.Positive); + OpeningPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(openingSnapshotPoint, PointTrackingMode.Positive); - var document = SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return false; - } + var document = SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + { + return false; + } - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - var context = GetBraceCompletionContext(parsedDocument); + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + var context = GetBraceCompletionContext(parsedDocument); - // Note: completes synchronously unless Semantic Model is needed to determine the result: - if (!_service.HasBraceCompletionAsync(context, document, cancellationToken).WaitAndGetResult(cancellationToken)) - { - return false; - } + // Note: completes synchronously unless Semantic Model is needed to determine the result: + if (!_service.HasBraceCompletionAsync(context, document, cancellationToken).WaitAndGetResult(cancellationToken)) + { + return false; + } - var braceResult = _service.GetBraceCompletion(context); + var braceResult = _service.GetBraceCompletion(context); - using var caretPreservingTransaction = new CaretPreservingEditTransaction(EditorFeaturesResources.Brace_Completion, _undoHistory, _editorOperations); + using var caretPreservingTransaction = new CaretPreservingEditTransaction(EditorFeaturesResources.Brace_Completion, _undoHistory, _editorOperations); - // Apply the change to complete the brace. - ApplyBraceCompletionResult(braceResult); + // Apply the change to complete the brace. + ApplyBraceCompletionResult(braceResult); - // switch the closing point from positive to negative tracking so that the closing point stays against the closing brace - ClosingPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot), PointTrackingMode.Negative); + // switch the closing point from positive to negative tracking so that the closing point stays against the closing brace + ClosingPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot), PointTrackingMode.Negative); - if (TryGetBraceCompletionContext(out var contextAfterStart, cancellationToken)) + if (TryGetBraceCompletionContext(out var contextAfterStart, cancellationToken)) + { + var indentationOptions = SubjectBuffer.GetIndentationOptions(_editorOptionsService, contextAfterStart.Document.LanguageServices, explicitFormat: false); + var changesAfterStart = _service.GetTextChangesAfterCompletion(contextAfterStart, indentationOptions, cancellationToken); + if (changesAfterStart != null) { - var indentationOptions = SubjectBuffer.GetIndentationOptions(_editorOptionsService, contextAfterStart.Document.LanguageServices, explicitFormat: false); - var changesAfterStart = _service.GetTextChangesAfterCompletion(contextAfterStart, indentationOptions, cancellationToken); - if (changesAfterStart != null) - { - ApplyBraceCompletionResult(changesAfterStart.Value); - } + ApplyBraceCompletionResult(changesAfterStart.Value); } - - caretPreservingTransaction.Complete(); - return true; } - public void PreBackspace(out bool handledCommand) - { - _threadingContext.ThrowIfNotOnUIThread(); - handledCommand = false; + caretPreservingTransaction.Complete(); + return true; + } - var caretPos = this.GetCaretPosition(); - var snapshot = SubjectBuffer.CurrentSnapshot; + public void PreBackspace(out bool handledCommand) + { + _threadingContext.ThrowIfNotOnUIThread(); + handledCommand = false; - if (caretPos.HasValue && caretPos.Value.Position > 0 && (caretPos.Value.Position - 1) == OpeningPoint.GetPoint(snapshot).Position - && !HasForwardTyping) - { - using var undo = CreateUndoTransaction(); - using var edit = SubjectBuffer.CreateEdit(); + var caretPos = this.GetCaretPosition(); + var snapshot = SubjectBuffer.CurrentSnapshot; - var span = new SnapshotSpan(OpeningPoint.GetPoint(snapshot), ClosingPoint.GetPoint(snapshot)); + if (caretPos.HasValue && caretPos.Value.Position > 0 && (caretPos.Value.Position - 1) == OpeningPoint.GetPoint(snapshot).Position + && !HasForwardTyping) + { + using var undo = CreateUndoTransaction(); + using var edit = SubjectBuffer.CreateEdit(); - edit.Delete(span); + var span = new SnapshotSpan(OpeningPoint.GetPoint(snapshot), ClosingPoint.GetPoint(snapshot)); - if (edit.HasFailedChanges) - { - edit.Cancel(); - undo.Cancel(); - Debug.Fail("Unable to clear braces"); - } - else - { - // handle the command so the backspace does - // not go through since we've already cleared the braces - handledCommand = true; - edit.ApplyAndLogExceptions(); - undo.Complete(); - EndSession(); - } + edit.Delete(span); + + if (edit.HasFailedChanges) + { + edit.Cancel(); + undo.Cancel(); + Debug.Fail("Unable to clear braces"); + } + else + { + // handle the command so the backspace does + // not go through since we've already cleared the braces + handledCommand = true; + edit.ApplyAndLogExceptions(); + undo.Complete(); + EndSession(); } } + } - public void PostBackspace() - { - } + public void PostBackspace() + { + } - public void PreOverType(out bool handledCommand) + public void PreOverType(out bool handledCommand) + { + _threadingContext.ThrowIfNotOnUIThread(); + handledCommand = false; + if (ClosingPoint == null) { - _threadingContext.ThrowIfNotOnUIThread(); - handledCommand = false; - if (ClosingPoint == null) - { - return; - } + return; + } - // Brace completion is not cancellable. - var cancellationToken = CancellationToken.None; - var snapshot = this.SubjectBuffer.CurrentSnapshot; + // Brace completion is not cancellable. + var cancellationToken = CancellationToken.None; + var snapshot = this.SubjectBuffer.CurrentSnapshot; - var closingSnapshotPoint = ClosingPoint.GetPoint(snapshot); + var closingSnapshotPoint = ClosingPoint.GetPoint(snapshot); - if (HasForwardTyping) - { - return; - } + if (HasForwardTyping) + { + return; + } - if (!TryGetBraceCompletionContext(out var context, cancellationToken) || - !_service.AllowOverType(context, cancellationToken)) - { - return; - } + if (!TryGetBraceCompletionContext(out var context, cancellationToken) || + !_service.AllowOverType(context, cancellationToken)) + { + return; + } - var caretPos = this.GetCaretPosition(); + var caretPos = this.GetCaretPosition(); - Debug.Assert(caretPos.HasValue && caretPos.Value.Position < closingSnapshotPoint.Position); + Debug.Assert(caretPos.HasValue && caretPos.Value.Position < closingSnapshotPoint.Position); - // ensure that we are within the session before clearing - if (caretPos.HasValue && caretPos.Value.Position < closingSnapshotPoint.Position && closingSnapshotPoint.Position > 0) - { - using var undo = CreateUndoTransaction(); + // ensure that we are within the session before clearing + if (caretPos.HasValue && caretPos.Value.Position < closingSnapshotPoint.Position && closingSnapshotPoint.Position > 0) + { + using var undo = CreateUndoTransaction(); - _editorOperations.AddBeforeTextBufferChangePrimitive(); + _editorOperations.AddBeforeTextBufferChangePrimitive(); - var span = new SnapshotSpan(caretPos.Value, closingSnapshotPoint.Subtract(1)); + var span = new SnapshotSpan(caretPos.Value, closingSnapshotPoint.Subtract(1)); - using var edit = SubjectBuffer.CreateEdit(); + using var edit = SubjectBuffer.CreateEdit(); - edit.Delete(span); + edit.Delete(span); - if (edit.HasFailedChanges) - { - Debug.Fail("Unable to clear closing brace"); - edit.Cancel(); - undo.Cancel(); - } - else - { - handledCommand = true; + if (edit.HasFailedChanges) + { + Debug.Fail("Unable to clear closing brace"); + edit.Cancel(); + undo.Cancel(); + } + else + { + handledCommand = true; - edit.ApplyAndLogExceptions(); + edit.ApplyAndLogExceptions(); - MoveCaretToClosingPoint(); + MoveCaretToClosingPoint(); - _editorOperations.AddAfterTextBufferChangePrimitive(); + _editorOperations.AddAfterTextBufferChangePrimitive(); - undo.Complete(); - } + undo.Complete(); } } + } - public void PostOverType() - { - } + public void PostOverType() + { + } - public void PreTab(out bool handledCommand) - { - _threadingContext.ThrowIfNotOnUIThread(); - handledCommand = false; + public void PreTab(out bool handledCommand) + { + _threadingContext.ThrowIfNotOnUIThread(); + handledCommand = false; - if (!HasForwardTyping) - { - handledCommand = true; + if (!HasForwardTyping) + { + handledCommand = true; - using var undo = CreateUndoTransaction(); + using var undo = CreateUndoTransaction(); - _editorOperations.AddBeforeTextBufferChangePrimitive(); + _editorOperations.AddBeforeTextBufferChangePrimitive(); - MoveCaretToClosingPoint(); + MoveCaretToClosingPoint(); - _editorOperations.AddAfterTextBufferChangePrimitive(); + _editorOperations.AddAfterTextBufferChangePrimitive(); - undo.Complete(); - } + undo.Complete(); } + } - public void PreReturn(out bool handledCommand) - => handledCommand = false; + public void PreReturn(out bool handledCommand) + => handledCommand = false; - public void PostReturn() + public void PostReturn() + { + _threadingContext.ThrowIfNotOnUIThread(); + if (this.GetCaretPosition().HasValue) { - _threadingContext.ThrowIfNotOnUIThread(); - if (this.GetCaretPosition().HasValue) + var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot); + + if (closingSnapshotPoint.Position > 0 && HasNoForwardTyping(this.GetCaretPosition().Value, closingSnapshotPoint.Subtract(1))) { - var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot); + if (!TryGetBraceCompletionContext(out var context, CancellationToken.None)) + { + return; + } - if (closingSnapshotPoint.Position > 0 && HasNoForwardTyping(this.GetCaretPosition().Value, closingSnapshotPoint.Subtract(1))) + var indentationOptions = SubjectBuffer.GetIndentationOptions(_editorOptionsService, context.Document.LanguageServices, explicitFormat: false); + var changesAfterReturn = _service.GetTextChangeAfterReturn(context, indentationOptions, CancellationToken.None); + if (changesAfterReturn != null) { - if (!TryGetBraceCompletionContext(out var context, CancellationToken.None)) - { - return; - } - - var indentationOptions = SubjectBuffer.GetIndentationOptions(_editorOptionsService, context.Document.LanguageServices, explicitFormat: false); - var changesAfterReturn = _service.GetTextChangeAfterReturn(context, indentationOptions, CancellationToken.None); - if (changesAfterReturn != null) - { - using var caretPreservingTransaction = new CaretPreservingEditTransaction(EditorFeaturesResources.Brace_Completion, _undoHistory, _editorOperations); - ApplyBraceCompletionResult(changesAfterReturn.Value); - caretPreservingTransaction.Complete(); - } + using var caretPreservingTransaction = new CaretPreservingEditTransaction(EditorFeaturesResources.Brace_Completion, _undoHistory, _editorOperations); + ApplyBraceCompletionResult(changesAfterReturn.Value); + caretPreservingTransaction.Complete(); } } } + } - public void Finish() - { - } + public void Finish() + { + } - #endregion + #endregion - #region Unused IBraceCompletionSession Methods + #region Unused IBraceCompletionSession Methods - public void PostTab() { } + public void PostTab() { } - public void PreDelete(out bool handledCommand) - => handledCommand = false; + public void PreDelete(out bool handledCommand) + => handledCommand = false; - public void PostDelete() { } + public void PostDelete() { } - #endregion + #endregion - #region Private Helpers + #region Private Helpers - private void EndSession() - { - // set the points to null to get off the stack - // the stack will determine that the current point - // is not contained within the session if either are null - OpeningPoint = null; - ClosingPoint = null; - } + private void EndSession() + { + // set the points to null to get off the stack + // the stack will determine that the current point + // is not contained within the session if either are null + OpeningPoint = null; + ClosingPoint = null; + } - // check if there any typing between the caret the closing point - private bool HasForwardTyping + // check if there any typing between the caret the closing point + private bool HasForwardTyping + { + get { - get + _threadingContext.ThrowIfNotOnUIThread(); + var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot); + + if (closingSnapshotPoint.Position > 0) { - _threadingContext.ThrowIfNotOnUIThread(); - var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot); + var caretPos = this.GetCaretPosition(); - if (closingSnapshotPoint.Position > 0) + if (caretPos.HasValue && !HasNoForwardTyping(caretPos.Value, closingSnapshotPoint.Subtract(1))) { - var caretPos = this.GetCaretPosition(); - - if (caretPos.HasValue && !HasNoForwardTyping(caretPos.Value, closingSnapshotPoint.Subtract(1))) - { - return true; - } + return true; } - - return false; } + + return false; } + } - // verify that there is only whitespace between the two given points - private static bool HasNoForwardTyping(SnapshotPoint caretPoint, SnapshotPoint endPoint) - { - Debug.Assert(caretPoint.Snapshot == endPoint.Snapshot, "snapshots do not match"); + // verify that there is only whitespace between the two given points + private static bool HasNoForwardTyping(SnapshotPoint caretPoint, SnapshotPoint endPoint) + { + Debug.Assert(caretPoint.Snapshot == endPoint.Snapshot, "snapshots do not match"); - if (caretPoint.Snapshot == endPoint.Snapshot) + if (caretPoint.Snapshot == endPoint.Snapshot) + { + if (caretPoint == endPoint) { - if (caretPoint == endPoint) - { - return true; - } + return true; + } - if (caretPoint.Position < endPoint.Position) - { - var span = new SnapshotSpan(caretPoint, endPoint); + if (caretPoint.Position < endPoint.Position) + { + var span = new SnapshotSpan(caretPoint, endPoint); - return string.IsNullOrWhiteSpace(span.GetText()); - } + return string.IsNullOrWhiteSpace(span.GetText()); } - - return false; } - internal ITextUndoTransaction CreateUndoTransaction() - => _undoHistory.CreateTransaction(EditorFeaturesResources.Brace_Completion); + return false; + } - private void MoveCaretToClosingPoint() - { - _threadingContext.ThrowIfNotOnUIThread(); - var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot); + internal ITextUndoTransaction CreateUndoTransaction() + => _undoHistory.CreateTransaction(EditorFeaturesResources.Brace_Completion); - // find the position just after the closing brace in the view's text buffer - var afterBrace = TextView.BufferGraph.MapUpToBuffer(closingSnapshotPoint, - PointTrackingMode.Negative, PositionAffinity.Predecessor, TextView.TextBuffer); + private void MoveCaretToClosingPoint() + { + _threadingContext.ThrowIfNotOnUIThread(); + var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot); - Debug.Assert(afterBrace.HasValue, "Unable to move caret to closing point"); + // find the position just after the closing brace in the view's text buffer + var afterBrace = TextView.BufferGraph.MapUpToBuffer(closingSnapshotPoint, + PointTrackingMode.Negative, PositionAffinity.Predecessor, TextView.TextBuffer); - if (afterBrace.HasValue) - { - TextView.Caret.MoveTo(afterBrace.Value); - } - } + Debug.Assert(afterBrace.HasValue, "Unable to move caret to closing point"); - private bool TryGetBraceCompletionContext(out BraceCompletionContext context, CancellationToken cancellationToken) + if (afterBrace.HasValue) { - var document = SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - context = default; - return false; - } - - context = GetBraceCompletionContext(ParsedDocument.CreateSynchronously(document, cancellationToken)); - return true; + TextView.Caret.MoveTo(afterBrace.Value); } + } - private BraceCompletionContext GetBraceCompletionContext(ParsedDocument document) + private bool TryGetBraceCompletionContext(out BraceCompletionContext context, CancellationToken cancellationToken) + { + var document = SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) { - _threadingContext.ThrowIfNotOnUIThread(); - var snapshot = SubjectBuffer.CurrentSnapshot; + context = default; + return false; + } - var closingSnapshotPoint = ClosingPoint.GetPosition(snapshot); - var openingSnapshotPoint = OpeningPoint.GetPosition(snapshot); - // The user is actively typing so the caret position should not be null. - var caretPosition = this.GetCaretPosition().Value.Position; + context = GetBraceCompletionContext(ParsedDocument.CreateSynchronously(document, cancellationToken)); + return true; + } - return new BraceCompletionContext(document, openingSnapshotPoint, closingSnapshotPoint, caretPosition); - } + private BraceCompletionContext GetBraceCompletionContext(ParsedDocument document) + { + _threadingContext.ThrowIfNotOnUIThread(); + var snapshot = SubjectBuffer.CurrentSnapshot; - private void ApplyBraceCompletionResult(BraceCompletionResult result) - { - _threadingContext.ThrowIfNotOnUIThread(); - using var edit = SubjectBuffer.CreateEdit(); - foreach (var change in result.TextChanges) - { - edit.Replace(change.Span.ToSpan(), change.NewText); - } + var closingSnapshotPoint = ClosingPoint.GetPosition(snapshot); + var openingSnapshotPoint = OpeningPoint.GetPosition(snapshot); + // The user is actively typing so the caret position should not be null. + var caretPosition = this.GetCaretPosition().Value.Position; - edit.ApplyAndLogExceptions(); + return new BraceCompletionContext(document, openingSnapshotPoint, closingSnapshotPoint, caretPosition); + } - try - { - Contract.ThrowIfFalse(SubjectBuffer.CurrentSnapshot[OpeningPoint.GetPosition(SubjectBuffer.CurrentSnapshot)] == OpeningBrace, - "The opening point does not match the opening brace character"); - Contract.ThrowIfFalse(SubjectBuffer.CurrentSnapshot[ClosingPoint.GetPosition(SubjectBuffer.CurrentSnapshot) - 1] == ClosingBrace, - "The closing point does not match the closing brace character"); - } - catch (Exception e) when (FatalError.ReportAndCatch(e)) - { - return; - } + private void ApplyBraceCompletionResult(BraceCompletionResult result) + { + _threadingContext.ThrowIfNotOnUIThread(); + using var edit = SubjectBuffer.CreateEdit(); + foreach (var change in result.TextChanges) + { + edit.Replace(change.Span.ToSpan(), change.NewText); + } - var caretLine = SubjectBuffer.CurrentSnapshot.GetLineFromLineNumber(result.CaretLocation.Line); - TextView.TryMoveCaretToAndEnsureVisible(new VirtualSnapshotPoint(caretLine, result.CaretLocation.Character)); + edit.ApplyAndLogExceptions(); + + try + { + Contract.ThrowIfFalse(SubjectBuffer.CurrentSnapshot[OpeningPoint.GetPosition(SubjectBuffer.CurrentSnapshot)] == OpeningBrace, + "The opening point does not match the opening brace character"); + Contract.ThrowIfFalse(SubjectBuffer.CurrentSnapshot[ClosingPoint.GetPosition(SubjectBuffer.CurrentSnapshot) - 1] == ClosingBrace, + "The closing point does not match the closing brace character"); + } + catch (Exception e) when (FatalError.ReportAndCatch(e)) + { + return; } - #endregion + var caretLine = SubjectBuffer.CurrentSnapshot.GetLineFromLineNumber(result.CaretLocation.Line); + TextView.TryMoveCaretToAndEnsureVisible(new VirtualSnapshotPoint(caretLine, result.CaretLocation.Character)); } + + #endregion } } diff --git a/src/EditorFeatures/Core/AutomaticCompletion/BraceCompletionSessionProvider.cs b/src/EditorFeatures/Core/AutomaticCompletion/BraceCompletionSessionProvider.cs index a75c9807e6f52..c4312ab891879 100644 --- a/src/EditorFeatures/Core/AutomaticCompletion/BraceCompletionSessionProvider.cs +++ b/src/EditorFeatures/Core/AutomaticCompletion/BraceCompletionSessionProvider.cs @@ -22,58 +22,57 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.BraceCompletion.AbstractBraceCompletionService; -namespace Microsoft.CodeAnalysis.AutomaticCompletion +namespace Microsoft.CodeAnalysis.AutomaticCompletion; + +[Export(typeof(IBraceCompletionSessionProvider))] +[ContentType(ContentTypeNames.RoslynContentType)] +[BracePair(CurlyBrace.OpenCharacter, CurlyBrace.CloseCharacter)] +[BracePair(Bracket.OpenCharacter, Bracket.CloseCharacter)] +[BracePair(SingleQuote.OpenCharacter, SingleQuote.CloseCharacter)] +[BracePair(DoubleQuote.OpenCharacter, DoubleQuote.CloseCharacter)] +[BracePair(Parenthesis.OpenCharacter, Parenthesis.CloseCharacter)] +[BracePair(LessAndGreaterThan.OpenCharacter, LessAndGreaterThan.CloseCharacter)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal partial class BraceCompletionSessionProvider( + IThreadingContext threadingContext, + ITextBufferUndoManagerProvider undoManager, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService) : IBraceCompletionSessionProvider { - [Export(typeof(IBraceCompletionSessionProvider))] - [ContentType(ContentTypeNames.RoslynContentType)] - [BracePair(CurlyBrace.OpenCharacter, CurlyBrace.CloseCharacter)] - [BracePair(Bracket.OpenCharacter, Bracket.CloseCharacter)] - [BracePair(SingleQuote.OpenCharacter, SingleQuote.CloseCharacter)] - [BracePair(DoubleQuote.OpenCharacter, DoubleQuote.CloseCharacter)] - [BracePair(Parenthesis.OpenCharacter, Parenthesis.CloseCharacter)] - [BracePair(LessAndGreaterThan.OpenCharacter, LessAndGreaterThan.CloseCharacter)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal partial class BraceCompletionSessionProvider( - IThreadingContext threadingContext, - ITextBufferUndoManagerProvider undoManager, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService) : IBraceCompletionSessionProvider - { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly ITextBufferUndoManagerProvider _undoManager = undoManager; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly ITextBufferUndoManagerProvider _undoManager = undoManager; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - public bool TryCreateSession(ITextView textView, SnapshotPoint openingPoint, char openingBrace, char closingBrace, out IBraceCompletionSession session) + public bool TryCreateSession(ITextView textView, SnapshotPoint openingPoint, char openingBrace, char closingBrace, out IBraceCompletionSession session) + { + _threadingContext.ThrowIfNotOnUIThread(); + var textSnapshot = openingPoint.Snapshot; + var document = textSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document != null) { - _threadingContext.ThrowIfNotOnUIThread(); - var textSnapshot = openingPoint.Snapshot; - var document = textSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document != null) + var editorSessionFactory = document.GetLanguageService(); + if (editorSessionFactory != null) { - var editorSessionFactory = document.GetLanguageService(); - if (editorSessionFactory != null) - { - // Brace completion is (currently) not cancellable. - var cancellationToken = CancellationToken.None; + // Brace completion is (currently) not cancellable. + var cancellationToken = CancellationToken.None; - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - var editorSession = editorSessionFactory.TryGetService(parsedDocument, openingPoint, openingBrace, cancellationToken); - if (editorSession != null) - { - var undoHistory = _undoManager.GetTextBufferUndoManager(textView.TextBuffer).TextBufferUndoHistory; - session = new BraceCompletionSession( - textView, openingPoint.Snapshot.TextBuffer, openingPoint, openingBrace, closingBrace, - undoHistory, _editorOperationsFactoryService, _editorOptionsService, - editorSession, _threadingContext); - return true; - } + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + var editorSession = editorSessionFactory.TryGetService(parsedDocument, openingPoint, openingBrace, cancellationToken); + if (editorSession != null) + { + var undoHistory = _undoManager.GetTextBufferUndoManager(textView.TextBuffer).TextBufferUndoHistory; + session = new BraceCompletionSession( + textView, openingPoint.Snapshot.TextBuffer, openingPoint, openingBrace, closingBrace, + undoHistory, _editorOperationsFactoryService, _editorOptionsService, + editorSession, _threadingContext); + return true; } } - - session = null; - return false; } + + session = null; + return false; } } diff --git a/src/EditorFeatures/Core/AutomaticCompletion/Extensions.cs b/src/EditorFeatures/Core/AutomaticCompletion/Extensions.cs index ee36c6453301e..26ffb71470ca7 100644 --- a/src/EditorFeatures/Core/AutomaticCompletion/Extensions.cs +++ b/src/EditorFeatures/Core/AutomaticCompletion/Extensions.cs @@ -10,27 +10,26 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.AutomaticCompletion +namespace Microsoft.CodeAnalysis.AutomaticCompletion; + +internal static class Extensions { - internal static class Extensions + /// + /// create caret preserving edit transaction with automatic code change undo merging policy + /// + public static CaretPreservingEditTransaction CreateEditTransaction( + this ITextView view, string description, ITextUndoHistoryRegistry registry, IEditorOperationsFactoryService service) { - /// - /// create caret preserving edit transaction with automatic code change undo merging policy - /// - public static CaretPreservingEditTransaction CreateEditTransaction( - this ITextView view, string description, ITextUndoHistoryRegistry registry, IEditorOperationsFactoryService service) + return new CaretPreservingEditTransaction(description, view, registry, service) { - return new CaretPreservingEditTransaction(description, view, registry, service) - { - MergePolicy = AutomaticCodeChangeMergePolicy.Instance - }; - } + MergePolicy = AutomaticCodeChangeMergePolicy.Instance + }; + } - public static SnapshotPoint? GetCaretPosition(this IBraceCompletionSession session) - => GetCaretPoint(session, session.SubjectBuffer); + public static SnapshotPoint? GetCaretPosition(this IBraceCompletionSession session) + => GetCaretPoint(session, session.SubjectBuffer); - // get the caret position within the given buffer - private static SnapshotPoint? GetCaretPoint(this IBraceCompletionSession session, ITextBuffer buffer) - => session.TextView.Caret.Position.Point.GetPoint(buffer, PositionAffinity.Predecessor); - } + // get the caret position within the given buffer + private static SnapshotPoint? GetCaretPoint(this IBraceCompletionSession session, ITextBuffer buffer) + => session.TextView.Caret.Position.Point.GetPoint(buffer, PositionAffinity.Predecessor); } diff --git a/src/EditorFeatures/Core/AutomaticCompletion/IBraceCompletionServiceFactory.cs b/src/EditorFeatures/Core/AutomaticCompletion/IBraceCompletionServiceFactory.cs index 35ffe020ab447..88f0d89a7eaff 100644 --- a/src/EditorFeatures/Core/AutomaticCompletion/IBraceCompletionServiceFactory.cs +++ b/src/EditorFeatures/Core/AutomaticCompletion/IBraceCompletionServiceFactory.cs @@ -7,10 +7,9 @@ using Microsoft.CodeAnalysis.BraceCompletion; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.AutomaticCompletion +namespace Microsoft.CodeAnalysis.AutomaticCompletion; + +internal interface IBraceCompletionServiceFactory : ILanguageService { - internal interface IBraceCompletionServiceFactory : ILanguageService - { - IBraceCompletionService? TryGetService(ParsedDocument document, int openingPosition, char openingBrace, CancellationToken cancellationToken); - } + IBraceCompletionService? TryGetService(ParsedDocument document, int openingPosition, char openingBrace, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/BraceMatching/BraceHighlightTag.cs b/src/EditorFeatures/Core/BraceMatching/BraceHighlightTag.cs index a742c010677e8..b7c91e05ccf7d 100644 --- a/src/EditorFeatures/Core/BraceMatching/BraceHighlightTag.cs +++ b/src/EditorFeatures/Core/BraceMatching/BraceHighlightTag.cs @@ -6,19 +6,18 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +internal sealed class BraceHighlightTag : TextMarkerTag { - internal sealed class BraceHighlightTag : TextMarkerTag - { - public static readonly BraceHighlightTag StartTag = new(navigateToStart: true); - public static readonly BraceHighlightTag EndTag = new(navigateToStart: false); + public static readonly BraceHighlightTag StartTag = new(navigateToStart: true); + public static readonly BraceHighlightTag EndTag = new(navigateToStart: false); - public bool NavigateToStart { get; } + public bool NavigateToStart { get; } - private BraceHighlightTag(bool navigateToStart) - : base(ClassificationTypeDefinitions.BraceMatchingName) - { - this.NavigateToStart = navigateToStart; - } + private BraceHighlightTag(bool navigateToStart) + : base(ClassificationTypeDefinitions.BraceMatchingName) + { + this.NavigateToStart = navigateToStart; } } diff --git a/src/EditorFeatures/Core/BraceMatching/BraceHighlightingViewTaggerProvider.cs b/src/EditorFeatures/Core/BraceMatching/BraceHighlightingViewTaggerProvider.cs index 0f3d5d4ed1c33..f0cca6c059e64 100644 --- a/src/EditorFeatures/Core/BraceMatching/BraceHighlightingViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/BraceMatching/BraceHighlightingViewTaggerProvider.cs @@ -26,152 +26,151 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +[Export(typeof(IViewTaggerProvider))] +[ContentType(ContentTypeNames.RoslynContentType)] +[TagType(typeof(BraceHighlightTag))] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal sealed class BraceHighlightingViewTaggerProvider( + IThreadingContext threadingContext, + IBraceMatchingService braceMatcherService, + IGlobalOptionService globalOptions, + [Import(AllowDefault = true)] ITextBufferVisibilityTracker visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) : AsynchronousViewTaggerProvider(threadingContext, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.BraceHighlighting)) { - [Export(typeof(IViewTaggerProvider))] - [ContentType(ContentTypeNames.RoslynContentType)] - [TagType(typeof(BraceHighlightTag))] - [method: ImportingConstructor] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal sealed class BraceHighlightingViewTaggerProvider( - IThreadingContext threadingContext, - IBraceMatchingService braceMatcherService, - IGlobalOptionService globalOptions, - [Import(AllowDefault = true)] ITextBufferVisibilityTracker visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) : AsynchronousViewTaggerProvider(threadingContext, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.BraceHighlighting)) - { - private readonly IBraceMatchingService _braceMatcherService = braceMatcherService; + private readonly IBraceMatchingService _braceMatcherService = braceMatcherService; - protected sealed override ImmutableArray Options { get; } = [BraceMatchingOptionsStorage.BraceMatching]; + protected sealed override ImmutableArray Options { get; } = [BraceMatchingOptionsStorage.BraceMatching]; - protected override TaggerDelay EventChangeDelay => TaggerDelay.NearImmediate; + protected override TaggerDelay EventChangeDelay => TaggerDelay.NearImmediate; - protected override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) - { - return TaggerEventSources.Compose( - TaggerEventSources.OnTextChanged(subjectBuffer), - TaggerEventSources.OnCaretPositionChanged(textView, subjectBuffer), - TaggerEventSources.OnParseOptionChanged(subjectBuffer)); - } + protected override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) + { + return TaggerEventSources.Compose( + TaggerEventSources.OnTextChanged(subjectBuffer), + TaggerEventSources.OnCaretPositionChanged(textView, subjectBuffer), + TaggerEventSources.OnParseOptionChanged(subjectBuffer)); + } - protected override Task ProduceTagsAsync( - TaggerContext context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition, CancellationToken cancellationToken) + protected override Task ProduceTagsAsync( + TaggerContext context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition, CancellationToken cancellationToken) + { + var document = documentSnapshotSpan.Document; + if (!caretPosition.HasValue || document == null) { - var document = documentSnapshotSpan.Document; - if (!caretPosition.HasValue || document == null) - { - return Task.CompletedTask; - } + return Task.CompletedTask; + } - var options = GlobalOptions.GetBraceMatchingOptions(document.Project.Language); + var options = GlobalOptions.GetBraceMatchingOptions(document.Project.Language); - return ProduceTagsAsync( - context, document, documentSnapshotSpan.SnapshotSpan.Snapshot, caretPosition.Value, options, cancellationToken); - } + return ProduceTagsAsync( + context, document, documentSnapshotSpan.SnapshotSpan.Snapshot, caretPosition.Value, options, cancellationToken); + } - internal async Task ProduceTagsAsync( - TaggerContext context, Document document, ITextSnapshot snapshot, int position, BraceMatchingOptions options, CancellationToken cancellationToken) + internal async Task ProduceTagsAsync( + TaggerContext context, Document document, ITextSnapshot snapshot, int position, BraceMatchingOptions options, CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.Tagger_BraceHighlighting_TagProducer_ProduceTags, cancellationToken)) { - using (Logger.LogBlock(FunctionId.Tagger_BraceHighlighting_TagProducer_ProduceTags, cancellationToken)) + if (position >= 0 && position <= snapshot.Length) { - if (position >= 0 && position <= snapshot.Length) - { - var (bracesLeftOfPosition, bracesRightOfPosition) = await GetAllMatchingBracesAsync( - _braceMatcherService, document, position, options, cancellationToken).ConfigureAwait(false); - - AddBraces(context, snapshot, bracesLeftOfPosition); - AddBraces(context, snapshot, bracesRightOfPosition); - } + var (bracesLeftOfPosition, bracesRightOfPosition) = await GetAllMatchingBracesAsync( + _braceMatcherService, document, position, options, cancellationToken).ConfigureAwait(false); + + AddBraces(context, snapshot, bracesLeftOfPosition); + AddBraces(context, snapshot, bracesRightOfPosition); } } + } - /// - /// Given code like ()^() (where ^ is the caret position), returns the two pairs of - /// matching braces on the left and the right of the position. Note: a brace matching - /// pair is only returned if the position is on the left-side of hte start brace, or the - /// right side of end brace. So, for example, if you have (^()), then only the inner - /// braces are returned as the position is not on the right-side of the outer braces. - /// - /// This function also works for multi-character braces i.e. ([ ]) In this case, - /// the rule is that the position has to be on the left side of the start brace, or - /// inside the start brace (but not at the end). So, ^([ ]) will return this - /// as a brace match, as will (^[ ]). But ([^ ]) will not. - /// - /// The same goes for the braces on the the left of the caret. i.e.: ([ ])^ - /// will return the braces on the left, as will ([ ]^). But ([ ^]) will not. - /// - private static async Task<(BraceMatchingResult? leftOfPosition, BraceMatchingResult? rightOfPosition)> GetAllMatchingBracesAsync( - IBraceMatchingService service, - Document document, - int position, - BraceMatchingOptions options, - CancellationToken cancellationToken) + /// + /// Given code like ()^() (where ^ is the caret position), returns the two pairs of + /// matching braces on the left and the right of the position. Note: a brace matching + /// pair is only returned if the position is on the left-side of hte start brace, or the + /// right side of end brace. So, for example, if you have (^()), then only the inner + /// braces are returned as the position is not on the right-side of the outer braces. + /// + /// This function also works for multi-character braces i.e. ([ ]) In this case, + /// the rule is that the position has to be on the left side of the start brace, or + /// inside the start brace (but not at the end). So, ^([ ]) will return this + /// as a brace match, as will (^[ ]). But ([^ ]) will not. + /// + /// The same goes for the braces on the the left of the caret. i.e.: ([ ])^ + /// will return the braces on the left, as will ([ ]^). But ([ ^]) will not. + /// + private static async Task<(BraceMatchingResult? leftOfPosition, BraceMatchingResult? rightOfPosition)> GetAllMatchingBracesAsync( + IBraceMatchingService service, + Document document, + int position, + BraceMatchingOptions options, + CancellationToken cancellationToken) + { + // These are the matching spans when checking the token to the right of the position. + var rightOfPosition = await service.GetMatchingBracesAsync(document, position, options, cancellationToken).ConfigureAwait(false); + + // The braces to the right of the position should only be added if the position is + // actually within the span of the start brace. Note that this is what we want for + // single character braces as well as multi char braces. i.e. if the user has: + // + // ^{ } // then { and } are matching braces. + // {^ } // then { and } are not matching braces. + // + // ^<@ @> // then <@ and @> are matching braces. + // <^@ @> // then <@ and @> are matching braces. + // <@^ @> // then <@ and @> are not matching braces. + if (rightOfPosition.HasValue && + !rightOfPosition.Value.LeftSpan.Contains(position)) { - // These are the matching spans when checking the token to the right of the position. - var rightOfPosition = await service.GetMatchingBracesAsync(document, position, options, cancellationToken).ConfigureAwait(false); - - // The braces to the right of the position should only be added if the position is - // actually within the span of the start brace. Note that this is what we want for - // single character braces as well as multi char braces. i.e. if the user has: - // - // ^{ } // then { and } are matching braces. - // {^ } // then { and } are not matching braces. - // - // ^<@ @> // then <@ and @> are matching braces. - // <^@ @> // then <@ and @> are matching braces. - // <@^ @> // then <@ and @> are not matching braces. - if (rightOfPosition.HasValue && - !rightOfPosition.Value.LeftSpan.Contains(position)) - { - // Not a valid match. - rightOfPosition = null; - } - - if (position == 0) - { - // We're at the start of the document, can't find braces to the left of the position. - return (leftOfPosition: null, rightOfPosition); - } - - // See if we're touching the end of some construct. i.e.: - // - // { }^ - // <@ @>^ - // <@ @^> - // - // But not - // - // { ^} - // <@ ^@> - - var leftOfPosition = await service.GetMatchingBracesAsync(document, position - 1, options, cancellationToken).ConfigureAwait(false); - - if (leftOfPosition.HasValue && - position <= leftOfPosition.Value.RightSpan.End && - position > leftOfPosition.Value.RightSpan.Start) - { - // Found a valid pair on the left of us. - return (leftOfPosition, rightOfPosition); - } + // Not a valid match. + rightOfPosition = null; + } - // No valid pair of braces on the left of us. + if (position == 0) + { + // We're at the start of the document, can't find braces to the left of the position. return (leftOfPosition: null, rightOfPosition); } - private static void AddBraces( - TaggerContext context, - ITextSnapshot snapshot, - BraceMatchingResult? braces) + // See if we're touching the end of some construct. i.e.: + // + // { }^ + // <@ @>^ + // <@ @^> + // + // But not + // + // { ^} + // <@ ^@> + + var leftOfPosition = await service.GetMatchingBracesAsync(document, position - 1, options, cancellationToken).ConfigureAwait(false); + + if (leftOfPosition.HasValue && + position <= leftOfPosition.Value.RightSpan.End && + position > leftOfPosition.Value.RightSpan.Start) { - if (braces.HasValue) - { - context.AddTag(snapshot.GetTagSpan(braces.Value.LeftSpan.ToSpan(), BraceHighlightTag.StartTag)); - context.AddTag(snapshot.GetTagSpan(braces.Value.RightSpan.ToSpan(), BraceHighlightTag.EndTag)); - } + // Found a valid pair on the left of us. + return (leftOfPosition, rightOfPosition); } - // Safe to directly compare as BraceHighlightTag uses singleton instances. - protected override bool TagEquals(BraceHighlightTag tag1, BraceHighlightTag tag2) - => tag1 == tag2; + // No valid pair of braces on the left of us. + return (leftOfPosition: null, rightOfPosition); + } + + private static void AddBraces( + TaggerContext context, + ITextSnapshot snapshot, + BraceMatchingResult? braces) + { + if (braces.HasValue) + { + context.AddTag(snapshot.GetTagSpan(braces.Value.LeftSpan.ToSpan(), BraceHighlightTag.StartTag)); + context.AddTag(snapshot.GetTagSpan(braces.Value.RightSpan.ToSpan(), BraceHighlightTag.EndTag)); + } } + + // Safe to directly compare as BraceHighlightTag uses singleton instances. + protected override bool TagEquals(BraceHighlightTag tag1, BraceHighlightTag tag2) + => tag1 == tag2; } diff --git a/src/EditorFeatures/Core/BraceMatching/BraceMatchingOptionsStorage.cs b/src/EditorFeatures/Core/BraceMatching/BraceMatchingOptionsStorage.cs index 4acf0702fe187..33ee159318cd3 100644 --- a/src/EditorFeatures/Core/BraceMatching/BraceMatchingOptionsStorage.cs +++ b/src/EditorFeatures/Core/BraceMatching/BraceMatchingOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +internal static class BraceMatchingOptionsStorage { - internal static class BraceMatchingOptionsStorage - { - public static readonly Option2 BraceMatching = new("dotnet_enable_brace_matching", defaultValue: true); - } + public static readonly Option2 BraceMatching = new("dotnet_enable_brace_matching", defaultValue: true); } diff --git a/src/EditorFeatures/Core/BraceMatching/ClassificationTypeDefinitions.cs b/src/EditorFeatures/Core/BraceMatching/ClassificationTypeDefinitions.cs index a7223f2027c57..b859e0464b3e4 100644 --- a/src/EditorFeatures/Core/BraceMatching/ClassificationTypeDefinitions.cs +++ b/src/EditorFeatures/Core/BraceMatching/ClassificationTypeDefinitions.cs @@ -9,15 +9,14 @@ using Microsoft.VisualStudio.Text.Classification; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +internal sealed class ClassificationTypeDefinitions { - internal sealed class ClassificationTypeDefinitions - { - public const string BraceMatchingName = "brace matching"; + public const string BraceMatchingName = "brace matching"; - [Export] - [Name(BraceMatchingName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition BraceMatching; - } + [Export] + [Name(BraceMatchingName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition BraceMatching; } diff --git a/src/EditorFeatures/Core/ChangeSignature/AbstractChangeSignatureCommandHandler.cs b/src/EditorFeatures/Core/ChangeSignature/AbstractChangeSignatureCommandHandler.cs index 407a22c414494..88e43a9c07911 100644 --- a/src/EditorFeatures/Core/ChangeSignature/AbstractChangeSignatureCommandHandler.cs +++ b/src/EditorFeatures/Core/ChangeSignature/AbstractChangeSignatureCommandHandler.cs @@ -18,161 +18,160 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +internal abstract class AbstractChangeSignatureCommandHandler : ICommandHandler, + ICommandHandler { - internal abstract class AbstractChangeSignatureCommandHandler : ICommandHandler, - ICommandHandler - { - private readonly IThreadingContext _threadingContext; - private readonly IGlobalOptionService _globalOptions; + private readonly IThreadingContext _threadingContext; + private readonly IGlobalOptionService _globalOptions; - protected AbstractChangeSignatureCommandHandler(IThreadingContext threadingContext, IGlobalOptionService globalOptions) - { - _threadingContext = threadingContext; - _globalOptions = globalOptions; - } + protected AbstractChangeSignatureCommandHandler(IThreadingContext threadingContext, IGlobalOptionService globalOptions) + { + _threadingContext = threadingContext; + _globalOptions = globalOptions; + } - public string DisplayName => EditorFeaturesResources.Change_Signature; + public string DisplayName => EditorFeaturesResources.Change_Signature; - public CommandState GetCommandState(ReorderParametersCommandArgs args) - => GetCommandState(args.SubjectBuffer); + public CommandState GetCommandState(ReorderParametersCommandArgs args) + => GetCommandState(args.SubjectBuffer); - public CommandState GetCommandState(RemoveParametersCommandArgs args) - => GetCommandState(args.SubjectBuffer); + public CommandState GetCommandState(RemoveParametersCommandArgs args) + => GetCommandState(args.SubjectBuffer); - private static CommandState GetCommandState(ITextBuffer subjectBuffer) - => IsAvailable(subjectBuffer, out _) ? CommandState.Available : CommandState.Unspecified; + private static CommandState GetCommandState(ITextBuffer subjectBuffer) + => IsAvailable(subjectBuffer, out _) ? CommandState.Available : CommandState.Unspecified; - public bool ExecuteCommand(RemoveParametersCommandArgs args, CommandExecutionContext context) - => ExecuteCommand(args.TextView, args.SubjectBuffer, context); + public bool ExecuteCommand(RemoveParametersCommandArgs args, CommandExecutionContext context) + => ExecuteCommand(args.TextView, args.SubjectBuffer, context); - public bool ExecuteCommand(ReorderParametersCommandArgs args, CommandExecutionContext context) - => ExecuteCommand(args.TextView, args.SubjectBuffer, context); + public bool ExecuteCommand(ReorderParametersCommandArgs args, CommandExecutionContext context) + => ExecuteCommand(args.TextView, args.SubjectBuffer, context); - private static bool IsAvailable(ITextBuffer subjectBuffer, [NotNullWhen(true)] out Workspace? workspace) - => subjectBuffer.TryGetWorkspace(out workspace) && - workspace.CanApplyChange(ApplyChangesKind.ChangeDocument) && - subjectBuffer.SupportsRefactorings(); + private static bool IsAvailable(ITextBuffer subjectBuffer, [NotNullWhen(true)] out Workspace? workspace) + => subjectBuffer.TryGetWorkspace(out workspace) && + workspace.CanApplyChange(ApplyChangesKind.ChangeDocument) && + subjectBuffer.SupportsRefactorings(); - private bool ExecuteCommand(ITextView textView, ITextBuffer subjectBuffer, CommandExecutionContext context) + private bool ExecuteCommand(ITextView textView, ITextBuffer subjectBuffer, CommandExecutionContext context) + { + using (context.OperationContext.AddScope(allowCancellation: true, FeaturesResources.Change_signature)) { - using (context.OperationContext.AddScope(allowCancellation: true, FeaturesResources.Change_signature)) + if (!IsAvailable(subjectBuffer, out var workspace)) { - if (!IsAvailable(subjectBuffer, out var workspace)) - { - return false; - } + return false; + } - var caretPoint = textView.GetCaretPoint(subjectBuffer); - if (!caretPoint.HasValue) - { - return false; - } + var caretPoint = textView.GetCaretPoint(subjectBuffer); + if (!caretPoint.HasValue) + { + return false; + } - var document = subjectBuffer.CurrentSnapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChanges( - context.OperationContext, _threadingContext); - if (document == null) - { - return false; - } + var document = subjectBuffer.CurrentSnapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChanges( + context.OperationContext, _threadingContext); + if (document == null) + { + return false; + } - var changeSignatureService = document.GetRequiredLanguageService(); + var changeSignatureService = document.GetRequiredLanguageService(); - var cancellationToken = context.OperationContext.UserCancellationToken; + var cancellationToken = context.OperationContext.UserCancellationToken; - // TODO: Make asynchronous and avoid expensive semantic operations on UI thread: - // https://github.com/dotnet/roslyn/issues/62135 + // TODO: Make asynchronous and avoid expensive semantic operations on UI thread: + // https://github.com/dotnet/roslyn/issues/62135 - // Async operation to determine the change signature - var changeSignatureContext = changeSignatureService.GetChangeSignatureContextAsync( - document, - caretPoint.Value.Position, - restrictToDeclarations: false, - _globalOptions.CreateProvider(), - cancellationToken).WaitAndGetResult(context.OperationContext.UserCancellationToken); + // Async operation to determine the change signature + var changeSignatureContext = changeSignatureService.GetChangeSignatureContextAsync( + document, + caretPoint.Value.Position, + restrictToDeclarations: false, + _globalOptions.CreateProvider(), + cancellationToken).WaitAndGetResult(context.OperationContext.UserCancellationToken); - // UI thread bound operation to show the change signature dialog. - var changeSignatureOptions = AbstractChangeSignatureService.GetChangeSignatureOptions(changeSignatureContext); + // UI thread bound operation to show the change signature dialog. + var changeSignatureOptions = AbstractChangeSignatureService.GetChangeSignatureOptions(changeSignatureContext); - // Async operation to compute the new solution created from the specified options. - var result = changeSignatureService.ChangeSignatureWithContextAsync(changeSignatureContext, changeSignatureOptions, cancellationToken).WaitAndGetResult(cancellationToken); + // Async operation to compute the new solution created from the specified options. + var result = changeSignatureService.ChangeSignatureWithContextAsync(changeSignatureContext, changeSignatureOptions, cancellationToken).WaitAndGetResult(cancellationToken); - // UI thread bound operation to show preview changes dialog / show error message, then apply the solution changes (if applicable). - HandleResult(result, document.Project.Solution, workspace, context); + // UI thread bound operation to show preview changes dialog / show error message, then apply the solution changes (if applicable). + HandleResult(result, document.Project.Solution, workspace, context); - return true; - } + return true; } + } - private static void HandleResult(ChangeSignatureResult result, Solution oldSolution, Workspace workspace, CommandExecutionContext context) + private static void HandleResult(ChangeSignatureResult result, Solution oldSolution, Workspace workspace, CommandExecutionContext context) + { + var notificationService = workspace.Services.GetRequiredService(); + if (!result.Succeeded) { - var notificationService = workspace.Services.GetRequiredService(); - if (!result.Succeeded) + if (result.ChangeSignatureFailureKind != null) { - if (result.ChangeSignatureFailureKind != null) - { - ShowError(result.ChangeSignatureFailureKind.Value, context.OperationContext, notificationService); - } - - return; + ShowError(result.ChangeSignatureFailureKind.Value, context.OperationContext, notificationService); } - if (result.ConfirmationMessage != null && !notificationService.ConfirmMessageBox(result.ConfirmationMessage, severity: NotificationSeverity.Warning)) - { - return; - } + return; + } - var finalSolution = result.UpdatedSolution; + if (result.ConfirmationMessage != null && !notificationService.ConfirmMessageBox(result.ConfirmationMessage, severity: NotificationSeverity.Warning)) + { + return; + } - var previewService = workspace.Services.GetService(); - if (previewService != null && result.PreviewChanges) - { - // We are about to show a modal UI dialog so we should take over the command execution - // wait context. That means the command system won't attempt to show its own wait dialog - // and also will take it into consideration when measuring command handling duration. - context.OperationContext.TakeOwnership(); - finalSolution = previewService.PreviewChanges( - string.Format(EditorFeaturesResources.Preview_Changes_0, EditorFeaturesResources.Change_Signature), - "vs.csharp.refactoring.preview", - EditorFeaturesResources.Change_Signature_colon, - result.Name, - result.Glyph.GetValueOrDefault(), - result.UpdatedSolution, - oldSolution); - } + var finalSolution = result.UpdatedSolution; - if (finalSolution == null) - { - // User clicked cancel. - return; - } + var previewService = workspace.Services.GetService(); + if (previewService != null && result.PreviewChanges) + { + // We are about to show a modal UI dialog so we should take over the command execution + // wait context. That means the command system won't attempt to show its own wait dialog + // and also will take it into consideration when measuring command handling duration. + context.OperationContext.TakeOwnership(); + finalSolution = previewService.PreviewChanges( + string.Format(EditorFeaturesResources.Preview_Changes_0, EditorFeaturesResources.Change_Signature), + "vs.csharp.refactoring.preview", + EditorFeaturesResources.Change_Signature_colon, + result.Name, + result.Glyph.GetValueOrDefault(), + result.UpdatedSolution, + oldSolution); + } - using var workspaceUndoTransaction = workspace.OpenGlobalUndoTransaction(FeaturesResources.Change_signature); - if (workspace.TryApplyChanges(finalSolution)) - { - workspaceUndoTransaction.Commit(); - } + if (finalSolution == null) + { + // User clicked cancel. + return; + } - // TODO: handle failure + using var workspaceUndoTransaction = workspace.OpenGlobalUndoTransaction(FeaturesResources.Change_signature); + if (workspace.TryApplyChanges(finalSolution)) + { + workspaceUndoTransaction.Commit(); } - private static void ShowError(ChangeSignatureFailureKind reason, IUIThreadOperationContext operationContext, INotificationService notificationService) + // TODO: handle failure + } + + private static void ShowError(ChangeSignatureFailureKind reason, IUIThreadOperationContext operationContext, INotificationService notificationService) + { + switch (reason) { - switch (reason) - { - case ChangeSignatureFailureKind.DefinedInMetadata: - ShowMessage(FeaturesResources.The_member_is_defined_in_metadata, NotificationSeverity.Error, operationContext, notificationService); - break; - case ChangeSignatureFailureKind.IncorrectKind: - ShowMessage(FeaturesResources.You_can_only_change_the_signature_of_a_constructor_indexer_method_or_delegate, NotificationSeverity.Error, operationContext, notificationService); - break; - } + case ChangeSignatureFailureKind.DefinedInMetadata: + ShowMessage(FeaturesResources.The_member_is_defined_in_metadata, NotificationSeverity.Error, operationContext, notificationService); + break; + case ChangeSignatureFailureKind.IncorrectKind: + ShowMessage(FeaturesResources.You_can_only_change_the_signature_of_a_constructor_indexer_method_or_delegate, NotificationSeverity.Error, operationContext, notificationService); + break; + } - static void ShowMessage(string errorMessage, NotificationSeverity severity, IUIThreadOperationContext operationContext, INotificationService notificationService) - { - operationContext.TakeOwnership(); - notificationService.SendNotification(errorMessage, severity: severity); - } + static void ShowMessage(string errorMessage, NotificationSeverity severity, IUIThreadOperationContext operationContext, INotificationService notificationService) + { + operationContext.TakeOwnership(); + notificationService.SendNotification(errorMessage, severity: severity); } } } diff --git a/src/EditorFeatures/Core/Classification/ClassificationTags.cs b/src/EditorFeatures/Core/Classification/ClassificationTags.cs index a8cb40f49fd86..4fb4aadf7e4ab 100644 --- a/src/EditorFeatures/Core/Classification/ClassificationTags.cs +++ b/src/EditorFeatures/Core/Classification/ClassificationTags.cs @@ -6,12 +6,11 @@ using System; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal static class ClassificationTags { - internal static class ClassificationTags - { - [Obsolete("Use ToClassificationTypeName")] - public static string GetClassificationTypeName(string textTag) - => textTag.ToClassificationTypeName(); - } + [Obsolete("Use ToClassificationTypeName")] + public static string GetClassificationTypeName(string textTag) + => textTag.ToClassificationTypeName(); } diff --git a/src/EditorFeatures/Core/Classification/ClassificationTypeDefinitions.cs b/src/EditorFeatures/Core/Classification/ClassificationTypeDefinitions.cs index ab95c4d6bdbbc..f7829ff3920bf 100644 --- a/src/EditorFeatures/Core/Classification/ClassificationTypeDefinitions.cs +++ b/src/EditorFeatures/Core/Classification/ClassificationTypeDefinitions.cs @@ -9,434 +9,433 @@ using Microsoft.VisualStudio.Text.Classification; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal sealed class ClassificationTypeDefinitions { - internal sealed class ClassificationTypeDefinitions - { - #region Preprocessor Text - [Export] - [Name(ClassificationTypeNames.PreprocessorText)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal ClassificationTypeDefinition PreprocessorTextTypeDefinition { get; set; } - #endregion - #region Punctuation - [Export] - [Name(ClassificationTypeNames.Punctuation)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal ClassificationTypeDefinition PunctuationTypeDefinition; - #endregion - #region String - Verbatim - [Export] - [Name(ClassificationTypeNames.VerbatimStringLiteral)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition StringVerbatimTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.StringEscapeCharacter)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition StringEscapeCharacterTypeDefinition; - #endregion - #region Keyword - Control - // Keyword - Control sets its BaseDefinitions to be Keyword so that - // in the absence of specific styling they will appear as keywords. - [Export] - [Name(ClassificationTypeNames.ControlKeyword)] - [BaseDefinition(PredefinedClassificationTypeNames.Keyword)] - internal ClassificationTypeDefinition ControlKeywordTypeDefinition; - #endregion - - #region User Types - Classes - [Export] - [Name(ClassificationTypeNames.ClassName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition UserTypeClassesTypeDefinition; - #endregion - #region User Types - Records - [Export] - [Name(ClassificationTypeNames.RecordClassName)] - [BaseDefinition(ClassificationTypeNames.ClassName)] - internal readonly ClassificationTypeDefinition UserTypeRecordsTypeDefinition; - #endregion - #region User Types - Record Structs - [Export] - [Name(ClassificationTypeNames.RecordStructName)] - [BaseDefinition(ClassificationTypeNames.StructName)] - internal readonly ClassificationTypeDefinition UserTypeRecordStructsTypeDefinition; - #endregion - #region User Types - Delegates - [Export] - [Name(ClassificationTypeNames.DelegateName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition UserTypeDelegatesTypeDefinition; - #endregion - #region User Types - Enums - [Export] - [Name(ClassificationTypeNames.EnumName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition UserTypeEnumsTypeDefinition; - #endregion - #region User Types - Interfaces - [Export] - [Name(ClassificationTypeNames.InterfaceName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition UserTypeInterfacesTypeDefinition; - #endregion - #region User Types - Modules - [Export] - [Name(ClassificationTypeNames.ModuleName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition UserTypeModulesTypeDefinition; - #endregion - #region User Types - Structures - [Export] - [Name(ClassificationTypeNames.StructName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition UserTypeStructuresTypeDefinition; - #endregion - #region User Types - Type Parameters - [Export] - [Name(ClassificationTypeNames.TypeParameterName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition UserTypeTypeParametersTypeDefinition; - #endregion - - #region Test Code - [Export] - [Name(ClassificationTypeNames.TestCode)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition TestCodeTypeDefinition; - [Export] - [Name(ClassificationTypeNames.TestCodeMarkdown)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition TestCodeMarkdownTypeDefinition; - #endregion - - // User Members - * set their BaseDefinitions to be Identifier so that - // in the absence of specific styling they will appear as identifiers. - // Extension Methods are an exception and their base definition is Method - // since it is a more specific type of method. - #region User Members - Fields - [Export] - [Name(ClassificationTypeNames.FieldName)] - [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] - internal readonly ClassificationTypeDefinition UserMembersFieldsTypeDefinition; - #endregion - #region User Members - Enum Memberd - [Export] - [Name(ClassificationTypeNames.EnumMemberName)] - [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] - internal readonly ClassificationTypeDefinition UserMembersEnumMembersTypeDefinition; - #endregion - #region User Members - Constants - [Export] - [Name(ClassificationTypeNames.ConstantName)] - [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] - internal readonly ClassificationTypeDefinition UserMembersConstantsTypeDefinition; - #endregion - #region User Members - Locals - [Export] - [Name(ClassificationTypeNames.LocalName)] - [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] - internal readonly ClassificationTypeDefinition UserMembersLocalsTypeDefinition; - #endregion - #region User Members - Parameters - [Export] - [Name(ClassificationTypeNames.ParameterName)] - [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] - internal readonly ClassificationTypeDefinition UserMembersParametersTypeDefinition; - #endregion - #region User Members - Methods - [Export] - [Name(ClassificationTypeNames.MethodName)] - [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] - internal readonly ClassificationTypeDefinition UserMembersMethodsTypeDefinition; - #endregion - #region User Members - Extension Methods - [Export] - [Name(ClassificationTypeNames.ExtensionMethodName)] - [BaseDefinition(ClassificationTypeNames.MethodName)] - internal readonly ClassificationTypeDefinition UserMembersExtensionMethodsTypeDefinition; - #endregion - #region User Members - Properties - [Export] - [Name(ClassificationTypeNames.PropertyName)] - [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] - internal readonly ClassificationTypeDefinition UserMembersPropertiesTypeDefinition; - #endregion - #region User Members - Events - [Export] - [Name(ClassificationTypeNames.EventName)] - [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] - internal readonly ClassificationTypeDefinition UserMembersEventsTypeDefinition; - #endregion - #region User Members - Namespaces - [Export] - [Name(ClassificationTypeNames.NamespaceName)] - [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] - internal readonly ClassificationTypeDefinition UserMembersNamespacesTypeDefinition; - #endregion - #region User Members - Labels - [Export] - [Name(ClassificationTypeNames.LabelName)] - [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] - internal readonly ClassificationTypeDefinition UserMembersLabelsTypeDefinition; - #endregion - - #region XML Doc Comments - Attribute Name - [Export] - [Name(ClassificationTypeNames.XmlDocCommentAttributeName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlDocCommentAttributeNameTypeDefinition; - #endregion - #region XML Doc Comments - Attribute Quotes - [Export] - [Name(ClassificationTypeNames.XmlDocCommentAttributeQuotes)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlDocCommentAttributeQuotesTypeDefinition; - #endregion - #region XML Doc Comments - Attribute Value - [Export] - [Name(ClassificationTypeNames.XmlDocCommentAttributeValue)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlDocCommentAttributeValueTypeDefinition; - #endregion - #region XML Doc Comments - CData Section - [Export] - [Name(ClassificationTypeNames.XmlDocCommentCDataSection)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlDocCommentCDataSectionTypeDefinition; - #endregion - #region XML Doc Comments - Comment - [Export] - [Name(ClassificationTypeNames.XmlDocCommentComment)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlDocCommentCommentTypeDefinition; - #endregion - #region XML Doc Comments - Delimiter - [Export] - [Name(ClassificationTypeNames.XmlDocCommentDelimiter)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlDocCommentDelimiterTypeDefinition; - #endregion - #region XML Doc Comments - Entity Reference - [Export] - [Name(ClassificationTypeNames.XmlDocCommentEntityReference)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlDocCommentEntityReferenceTypeDefinition; - #endregion - #region XML Doc Comments - Name - [Export] - [Name(ClassificationTypeNames.XmlDocCommentName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlDocCommentNameTypeDefinition; - #endregion - #region XML Doc Comments - Processing Instruction - [Export] - [Name(ClassificationTypeNames.XmlDocCommentProcessingInstruction)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlDocCommentProcessingInstructionTypeDefinition; - #endregion - #region XML Doc Comments - Text - [Export] - [Name(ClassificationTypeNames.XmlDocCommentText)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlDocCommentTextTypeDefinition; - #endregion - - #region Regex - [Export] - [Name(ClassificationTypeNames.RegexComment)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition RegexCommentTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.RegexText)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition RegexTextTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.RegexCharacterClass)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition RegexCharacterClassTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.RegexQuantifier)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition RegexQuantifierTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.RegexAnchor)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition RegexAnchorTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.RegexAlternation)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition RegexAlternationTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.RegexOtherEscape)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition RegexOtherEscapeTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.RegexSelfEscapedCharacter)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition RegexSelfEscapedCharacterTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.RegexGrouping)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition RegexGroupingTypeDefinition; - - #endregion - - #region JSON - [Export] - [Name(ClassificationTypeNames.JsonComment)] - [BaseDefinition(PredefinedClassificationTypeNames.Comment)] - internal readonly ClassificationTypeDefinition JsonCommentTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.JsonNumber)] - [BaseDefinition(PredefinedClassificationTypeNames.Number)] - internal readonly ClassificationTypeDefinition JsonNumberTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.JsonString)] - [BaseDefinition(PredefinedClassificationTypeNames.String)] - internal readonly ClassificationTypeDefinition JsonStringTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.JsonKeyword)] - [BaseDefinition(PredefinedClassificationTypeNames.Keyword)] - internal readonly ClassificationTypeDefinition JsonKeywordTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.JsonText)] - [BaseDefinition(PredefinedClassificationTypeNames.Text)] - internal readonly ClassificationTypeDefinition JsonTextTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.JsonOperator)] - [BaseDefinition(PredefinedClassificationTypeNames.Operator)] - internal readonly ClassificationTypeDefinition JsonOperatorTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.JsonPunctuation)] - [BaseDefinition(PredefinedClassificationTypeNames.Punctuation)] - internal readonly ClassificationTypeDefinition JsonPunctuationTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.JsonArray)] - [BaseDefinition(PredefinedClassificationTypeNames.Punctuation)] - internal readonly ClassificationTypeDefinition JsonArrayTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.JsonObject)] - [BaseDefinition(PredefinedClassificationTypeNames.Punctuation)] - internal readonly ClassificationTypeDefinition JsonObjectTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.JsonPropertyName)] - [BaseDefinition(ClassificationTypeNames.MethodName)] - internal readonly ClassificationTypeDefinition JsonPropertyNameTypeDefinition; - - [Export] - [Name(ClassificationTypeNames.JsonConstructorName)] - [BaseDefinition(ClassificationTypeNames.StructName)] - internal readonly ClassificationTypeDefinition JsonConstructorNameTypeDefinition; - - #endregion - - #region VB XML Literals - Attribute Name - [Export] - [Name(ClassificationTypeNames.XmlLiteralAttributeName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlLiteralAttributeNameTypeDefinition; - #endregion - #region VB XML Literals - Attribute Quotes - [Export] - [Name(ClassificationTypeNames.XmlLiteralAttributeQuotes)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlLiteralAttributeQuotesTypeDefinition; - #endregion - #region VB XML Literals - Attribute Value - [Export] - [Name(ClassificationTypeNames.XmlLiteralAttributeValue)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlLiteralAttributeValueTypeDefinition; - #endregion - #region VB XML Literals - CData Section - [Export] - [Name(ClassificationTypeNames.XmlLiteralCDataSection)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlLiteralCDataSectionTypeDefinition; - #endregion - #region VB XML Literals - Comment - [Export] - [Name(ClassificationTypeNames.XmlLiteralComment)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlLiteralCommentTypeDefinition; - #endregion - #region VB XML Literals - Delimiter - [Export] - [Name(ClassificationTypeNames.XmlLiteralDelimiter)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlLiteralDelimiterTypeDefinition; - #endregion - #region VB XML Literals - Embedded Expression - [Export] - [Name(ClassificationTypeNames.XmlLiteralEmbeddedExpression)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlLiteralEmbeddedExpressionTypeDefinition; - #endregion - #region VB XML Literals - Entity Reference - [Export] - [Name(ClassificationTypeNames.XmlLiteralEntityReference)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlLiteralEntityReferenceTypeDefinition; - #endregion - #region VB XML Literals - Name - [Export] - [Name(ClassificationTypeNames.XmlLiteralName)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlLiteralNameTypeDefinition; - #endregion - #region VB XML Literals - Processing Instruction - [Export] - [Name(ClassificationTypeNames.XmlLiteralProcessingInstruction)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlLiteralProcessingInstructionTypeDefinition; - #endregion - #region VB XML Literals - Text - [Export] - [Name(ClassificationTypeNames.XmlLiteralText)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition XmlLiteralTextTypeDefinition; - #endregion - - #region Reassigned Variable - [Export] - [Name(ClassificationTypeNames.ReassignedVariable)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition ReassignedVariableTypeDefinition; - #endregion - - #region Static Symbol - [Export] - [Name(ClassificationTypeNames.StaticSymbol)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition StaticSymbolTypeDefinition; - #endregion - - #region Operator - Overloaded - // Operator - Overloaded sets its BaseDefinitions to be Operator so that - // in the absence of specific styling they will appear as operators. - [Export] - [Name(ClassificationTypeNames.OperatorOverloaded)] - [BaseDefinition(PredefinedClassificationTypeNames.Operator)] - internal readonly ClassificationTypeDefinition OperatorOverloadTypeDefinition; - #endregion - } + #region Preprocessor Text + [Export] + [Name(ClassificationTypeNames.PreprocessorText)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition PreprocessorTextTypeDefinition { get; set; } + #endregion + #region Punctuation + [Export] + [Name(ClassificationTypeNames.Punctuation)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition PunctuationTypeDefinition; + #endregion + #region String - Verbatim + [Export] + [Name(ClassificationTypeNames.VerbatimStringLiteral)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition StringVerbatimTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.StringEscapeCharacter)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition StringEscapeCharacterTypeDefinition; + #endregion + #region Keyword - Control + // Keyword - Control sets its BaseDefinitions to be Keyword so that + // in the absence of specific styling they will appear as keywords. + [Export] + [Name(ClassificationTypeNames.ControlKeyword)] + [BaseDefinition(PredefinedClassificationTypeNames.Keyword)] + internal ClassificationTypeDefinition ControlKeywordTypeDefinition; + #endregion + + #region User Types - Classes + [Export] + [Name(ClassificationTypeNames.ClassName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition UserTypeClassesTypeDefinition; + #endregion + #region User Types - Records + [Export] + [Name(ClassificationTypeNames.RecordClassName)] + [BaseDefinition(ClassificationTypeNames.ClassName)] + internal readonly ClassificationTypeDefinition UserTypeRecordsTypeDefinition; + #endregion + #region User Types - Record Structs + [Export] + [Name(ClassificationTypeNames.RecordStructName)] + [BaseDefinition(ClassificationTypeNames.StructName)] + internal readonly ClassificationTypeDefinition UserTypeRecordStructsTypeDefinition; + #endregion + #region User Types - Delegates + [Export] + [Name(ClassificationTypeNames.DelegateName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition UserTypeDelegatesTypeDefinition; + #endregion + #region User Types - Enums + [Export] + [Name(ClassificationTypeNames.EnumName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition UserTypeEnumsTypeDefinition; + #endregion + #region User Types - Interfaces + [Export] + [Name(ClassificationTypeNames.InterfaceName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition UserTypeInterfacesTypeDefinition; + #endregion + #region User Types - Modules + [Export] + [Name(ClassificationTypeNames.ModuleName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition UserTypeModulesTypeDefinition; + #endregion + #region User Types - Structures + [Export] + [Name(ClassificationTypeNames.StructName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition UserTypeStructuresTypeDefinition; + #endregion + #region User Types - Type Parameters + [Export] + [Name(ClassificationTypeNames.TypeParameterName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition UserTypeTypeParametersTypeDefinition; + #endregion + + #region Test Code + [Export] + [Name(ClassificationTypeNames.TestCode)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition TestCodeTypeDefinition; + [Export] + [Name(ClassificationTypeNames.TestCodeMarkdown)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition TestCodeMarkdownTypeDefinition; + #endregion + + // User Members - * set their BaseDefinitions to be Identifier so that + // in the absence of specific styling they will appear as identifiers. + // Extension Methods are an exception and their base definition is Method + // since it is a more specific type of method. + #region User Members - Fields + [Export] + [Name(ClassificationTypeNames.FieldName)] + [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] + internal readonly ClassificationTypeDefinition UserMembersFieldsTypeDefinition; + #endregion + #region User Members - Enum Memberd + [Export] + [Name(ClassificationTypeNames.EnumMemberName)] + [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] + internal readonly ClassificationTypeDefinition UserMembersEnumMembersTypeDefinition; + #endregion + #region User Members - Constants + [Export] + [Name(ClassificationTypeNames.ConstantName)] + [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] + internal readonly ClassificationTypeDefinition UserMembersConstantsTypeDefinition; + #endregion + #region User Members - Locals + [Export] + [Name(ClassificationTypeNames.LocalName)] + [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] + internal readonly ClassificationTypeDefinition UserMembersLocalsTypeDefinition; + #endregion + #region User Members - Parameters + [Export] + [Name(ClassificationTypeNames.ParameterName)] + [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] + internal readonly ClassificationTypeDefinition UserMembersParametersTypeDefinition; + #endregion + #region User Members - Methods + [Export] + [Name(ClassificationTypeNames.MethodName)] + [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] + internal readonly ClassificationTypeDefinition UserMembersMethodsTypeDefinition; + #endregion + #region User Members - Extension Methods + [Export] + [Name(ClassificationTypeNames.ExtensionMethodName)] + [BaseDefinition(ClassificationTypeNames.MethodName)] + internal readonly ClassificationTypeDefinition UserMembersExtensionMethodsTypeDefinition; + #endregion + #region User Members - Properties + [Export] + [Name(ClassificationTypeNames.PropertyName)] + [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] + internal readonly ClassificationTypeDefinition UserMembersPropertiesTypeDefinition; + #endregion + #region User Members - Events + [Export] + [Name(ClassificationTypeNames.EventName)] + [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] + internal readonly ClassificationTypeDefinition UserMembersEventsTypeDefinition; + #endregion + #region User Members - Namespaces + [Export] + [Name(ClassificationTypeNames.NamespaceName)] + [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] + internal readonly ClassificationTypeDefinition UserMembersNamespacesTypeDefinition; + #endregion + #region User Members - Labels + [Export] + [Name(ClassificationTypeNames.LabelName)] + [BaseDefinition(PredefinedClassificationTypeNames.Identifier)] + internal readonly ClassificationTypeDefinition UserMembersLabelsTypeDefinition; + #endregion + + #region XML Doc Comments - Attribute Name + [Export] + [Name(ClassificationTypeNames.XmlDocCommentAttributeName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlDocCommentAttributeNameTypeDefinition; + #endregion + #region XML Doc Comments - Attribute Quotes + [Export] + [Name(ClassificationTypeNames.XmlDocCommentAttributeQuotes)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlDocCommentAttributeQuotesTypeDefinition; + #endregion + #region XML Doc Comments - Attribute Value + [Export] + [Name(ClassificationTypeNames.XmlDocCommentAttributeValue)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlDocCommentAttributeValueTypeDefinition; + #endregion + #region XML Doc Comments - CData Section + [Export] + [Name(ClassificationTypeNames.XmlDocCommentCDataSection)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlDocCommentCDataSectionTypeDefinition; + #endregion + #region XML Doc Comments - Comment + [Export] + [Name(ClassificationTypeNames.XmlDocCommentComment)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlDocCommentCommentTypeDefinition; + #endregion + #region XML Doc Comments - Delimiter + [Export] + [Name(ClassificationTypeNames.XmlDocCommentDelimiter)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlDocCommentDelimiterTypeDefinition; + #endregion + #region XML Doc Comments - Entity Reference + [Export] + [Name(ClassificationTypeNames.XmlDocCommentEntityReference)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlDocCommentEntityReferenceTypeDefinition; + #endregion + #region XML Doc Comments - Name + [Export] + [Name(ClassificationTypeNames.XmlDocCommentName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlDocCommentNameTypeDefinition; + #endregion + #region XML Doc Comments - Processing Instruction + [Export] + [Name(ClassificationTypeNames.XmlDocCommentProcessingInstruction)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlDocCommentProcessingInstructionTypeDefinition; + #endregion + #region XML Doc Comments - Text + [Export] + [Name(ClassificationTypeNames.XmlDocCommentText)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlDocCommentTextTypeDefinition; + #endregion + + #region Regex + [Export] + [Name(ClassificationTypeNames.RegexComment)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition RegexCommentTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.RegexText)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition RegexTextTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.RegexCharacterClass)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition RegexCharacterClassTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.RegexQuantifier)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition RegexQuantifierTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.RegexAnchor)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition RegexAnchorTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.RegexAlternation)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition RegexAlternationTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.RegexOtherEscape)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition RegexOtherEscapeTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.RegexSelfEscapedCharacter)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition RegexSelfEscapedCharacterTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.RegexGrouping)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition RegexGroupingTypeDefinition; + + #endregion + + #region JSON + [Export] + [Name(ClassificationTypeNames.JsonComment)] + [BaseDefinition(PredefinedClassificationTypeNames.Comment)] + internal readonly ClassificationTypeDefinition JsonCommentTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.JsonNumber)] + [BaseDefinition(PredefinedClassificationTypeNames.Number)] + internal readonly ClassificationTypeDefinition JsonNumberTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.JsonString)] + [BaseDefinition(PredefinedClassificationTypeNames.String)] + internal readonly ClassificationTypeDefinition JsonStringTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.JsonKeyword)] + [BaseDefinition(PredefinedClassificationTypeNames.Keyword)] + internal readonly ClassificationTypeDefinition JsonKeywordTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.JsonText)] + [BaseDefinition(PredefinedClassificationTypeNames.Text)] + internal readonly ClassificationTypeDefinition JsonTextTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.JsonOperator)] + [BaseDefinition(PredefinedClassificationTypeNames.Operator)] + internal readonly ClassificationTypeDefinition JsonOperatorTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.JsonPunctuation)] + [BaseDefinition(PredefinedClassificationTypeNames.Punctuation)] + internal readonly ClassificationTypeDefinition JsonPunctuationTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.JsonArray)] + [BaseDefinition(PredefinedClassificationTypeNames.Punctuation)] + internal readonly ClassificationTypeDefinition JsonArrayTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.JsonObject)] + [BaseDefinition(PredefinedClassificationTypeNames.Punctuation)] + internal readonly ClassificationTypeDefinition JsonObjectTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.JsonPropertyName)] + [BaseDefinition(ClassificationTypeNames.MethodName)] + internal readonly ClassificationTypeDefinition JsonPropertyNameTypeDefinition; + + [Export] + [Name(ClassificationTypeNames.JsonConstructorName)] + [BaseDefinition(ClassificationTypeNames.StructName)] + internal readonly ClassificationTypeDefinition JsonConstructorNameTypeDefinition; + + #endregion + + #region VB XML Literals - Attribute Name + [Export] + [Name(ClassificationTypeNames.XmlLiteralAttributeName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlLiteralAttributeNameTypeDefinition; + #endregion + #region VB XML Literals - Attribute Quotes + [Export] + [Name(ClassificationTypeNames.XmlLiteralAttributeQuotes)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlLiteralAttributeQuotesTypeDefinition; + #endregion + #region VB XML Literals - Attribute Value + [Export] + [Name(ClassificationTypeNames.XmlLiteralAttributeValue)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlLiteralAttributeValueTypeDefinition; + #endregion + #region VB XML Literals - CData Section + [Export] + [Name(ClassificationTypeNames.XmlLiteralCDataSection)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlLiteralCDataSectionTypeDefinition; + #endregion + #region VB XML Literals - Comment + [Export] + [Name(ClassificationTypeNames.XmlLiteralComment)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlLiteralCommentTypeDefinition; + #endregion + #region VB XML Literals - Delimiter + [Export] + [Name(ClassificationTypeNames.XmlLiteralDelimiter)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlLiteralDelimiterTypeDefinition; + #endregion + #region VB XML Literals - Embedded Expression + [Export] + [Name(ClassificationTypeNames.XmlLiteralEmbeddedExpression)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlLiteralEmbeddedExpressionTypeDefinition; + #endregion + #region VB XML Literals - Entity Reference + [Export] + [Name(ClassificationTypeNames.XmlLiteralEntityReference)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlLiteralEntityReferenceTypeDefinition; + #endregion + #region VB XML Literals - Name + [Export] + [Name(ClassificationTypeNames.XmlLiteralName)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlLiteralNameTypeDefinition; + #endregion + #region VB XML Literals - Processing Instruction + [Export] + [Name(ClassificationTypeNames.XmlLiteralProcessingInstruction)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlLiteralProcessingInstructionTypeDefinition; + #endregion + #region VB XML Literals - Text + [Export] + [Name(ClassificationTypeNames.XmlLiteralText)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition XmlLiteralTextTypeDefinition; + #endregion + + #region Reassigned Variable + [Export] + [Name(ClassificationTypeNames.ReassignedVariable)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition ReassignedVariableTypeDefinition; + #endregion + + #region Static Symbol + [Export] + [Name(ClassificationTypeNames.StaticSymbol)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition StaticSymbolTypeDefinition; + #endregion + + #region Operator - Overloaded + // Operator - Overloaded sets its BaseDefinitions to be Operator so that + // in the absence of specific styling they will appear as operators. + [Export] + [Name(ClassificationTypeNames.OperatorOverloaded)] + [BaseDefinition(PredefinedClassificationTypeNames.Operator)] + internal readonly ClassificationTypeDefinition OperatorOverloadTypeDefinition; + #endregion } diff --git a/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.Tagger.cs b/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.Tagger.cs index acd7c69a31d39..88b5464cdb662 100644 --- a/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.Tagger.cs +++ b/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.Tagger.cs @@ -21,162 +21,156 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal partial class CopyPasteAndPrintingClassificationBufferTaggerProvider { - internal partial class CopyPasteAndPrintingClassificationBufferTaggerProvider + private sealed class Tagger : IAccurateTagger, IDisposable { - private sealed class Tagger : IAccurateTagger, IDisposable + private readonly CopyPasteAndPrintingClassificationBufferTaggerProvider _owner; + private readonly ITextBuffer _subjectBuffer; + private readonly ITaggerEventSource _eventSource; + private readonly IGlobalOptionService _globalOptions; + + // State for the tagger. Can be accessed from any thread. Access should be protected by _gate. + + private readonly object _gate = new(); + private TagSpanIntervalTree? _cachedTags; + private SnapshotSpan? _cachedTaggedSpan; + + public Tagger( + CopyPasteAndPrintingClassificationBufferTaggerProvider owner, + ITextBuffer subjectBuffer, + IAsynchronousOperationListener asyncListener, + IGlobalOptionService globalOptions) { - private readonly CopyPasteAndPrintingClassificationBufferTaggerProvider _owner; - private readonly ITextBuffer _subjectBuffer; - private readonly ITaggerEventSource _eventSource; - private readonly IGlobalOptionService _globalOptions; - - // State for the tagger. Can be accessed from any thread. Access should be protected by _gate. - - private readonly object _gate = new(); - private TagSpanIntervalTree? _cachedTags; - private SnapshotSpan? _cachedTaggedSpan; - - public Tagger( - CopyPasteAndPrintingClassificationBufferTaggerProvider owner, - ITextBuffer subjectBuffer, - IAsynchronousOperationListener asyncListener, - IGlobalOptionService globalOptions) - { - _owner = owner; - _subjectBuffer = subjectBuffer; - _globalOptions = globalOptions; - - // Note: because we use frozen-partial documents for semantic classification, we may end up with incomplete - // semantics (esp. during solution load). Because of this, we also register to hear when the full - // compilation is available so that reclassify and bring ourselves up to date. - _eventSource = new CompilationAvailableTaggerEventSource( - subjectBuffer, - asyncListener, - TaggerEventSources.OnWorkspaceChanged(subjectBuffer, asyncListener), - TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer)); - - _eventSource.Changed += OnEventSourceChanged; - _eventSource.Connect(); - } + _owner = owner; + _subjectBuffer = subjectBuffer; + _globalOptions = globalOptions; + + _eventSource = TaggerEventSources.Compose( + TaggerEventSources.OnWorkspaceChanged(subjectBuffer, asyncListener), + TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer)); + + _eventSource.Changed += OnEventSourceChanged; + _eventSource.Connect(); + } - // Explicitly a no-op. This classifier does not support change notifications. See comment in - // OnEventSourceChanged_OnForeground for more details. - public event EventHandler TagsChanged { add { } remove { } } + // Explicitly a no-op. This classifier does not support change notifications. See comment in + // OnEventSourceChanged_OnForeground for more details. + public event EventHandler TagsChanged { add { } remove { } } - public void Dispose() + public void Dispose() + { + _owner._threadingContext.ThrowIfNotOnUIThread(); + _eventSource.Changed -= OnEventSourceChanged; + _eventSource.Disconnect(); + } + + private void OnEventSourceChanged(object? sender, TaggerEventArgs _) + { + lock (_gate) { - _owner._threadingContext.ThrowIfNotOnUIThread(); - _eventSource.Changed -= OnEventSourceChanged; - _eventSource.Disconnect(); + _cachedTags = null; + _cachedTaggedSpan = null; } - private void OnEventSourceChanged(object? sender, TaggerEventArgs _) - { - lock (_gate) - { - _cachedTags = null; - _cachedTaggedSpan = null; - } + // Note: we explicitly do *not* call into TagsChanged here. This type exists only for the copy/paste + // scenario, and in the case the editor always calls into us for the span in question, ignoring + // TagsChanged, as per DPugh: + // + // For rich text copy, we always call the buffer classifier to get the classifications of the copied + // text. It ignores any tags changed events. + // + // It's important that we do not call TagsChanged here as the only thing we could do is notify that the + // entire doc is changed, and that incurs a heavy cost for the editor reacting to that notification. + } - // Note: we explicitly do *not* call into TagsChanged here. This type exists only for the copy/paste - // scenario, and in the case the editor always calls into us for the span in question, ignoring - // TagsChanged, as per DPugh: - // - // For rich text copy, we always call the buffer classifier to get the classifications of the copied - // text. It ignores any tags changed events. - // - // It's important that we do not call TagsChanged here as the only thing we could do is notify that the - // entire doc is changed, and that incurs a heavy cost for the editor reacting to that notification. - } + public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) + { + _owner._threadingContext.ThrowIfNotOnUIThread(); - public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) - { - _owner._threadingContext.ThrowIfNotOnUIThread(); + // we never return any tags for GetTags. This tagger is only for 'Accurate' scenarios. + return []; + } - // we never return any tags for GetTags. This tagger is only for 'Accurate' scenarios. + public IEnumerable> GetAllTags(NormalizedSnapshotSpanCollection spans, CancellationToken cancellationToken) + { + _owner._threadingContext.ThrowIfNotOnUIThread(); + if (spans.Count == 0) return []; - } - public IEnumerable> GetAllTags(NormalizedSnapshotSpanCollection spans, CancellationToken cancellationToken) - { - _owner._threadingContext.ThrowIfNotOnUIThread(); - if (spans.Count == 0) - return []; + var firstSpan = spans.First(); + var snapshot = firstSpan.Snapshot; + Debug.Assert(snapshot.TextBuffer == _subjectBuffer); - var firstSpan = spans.First(); - var snapshot = firstSpan.Snapshot; - Debug.Assert(snapshot.TextBuffer == _subjectBuffer); + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return []; - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return []; + var classificationService = document.GetLanguageService(); + if (classificationService == null) + return []; - var classificationService = document.GetLanguageService(); - if (classificationService == null) - return []; + // We want to classify from the start of the first requested span to the end of the + // last requested span. + var spanToTag = new SnapshotSpan(snapshot, Span.FromBounds(spans.First().Start, spans.Last().End)); - // We want to classify from the start of the first requested span to the end of the - // last requested span. - var spanToTag = new SnapshotSpan(snapshot, Span.FromBounds(spans.First().Start, spans.Last().End)); + GetCachedInfo(out var cachedTaggedSpan, out var cachedTags); - GetCachedInfo(out var cachedTaggedSpan, out var cachedTags); + // We don't need to actually classify if what we're being asked for is a subspan + // of the last classification we performed. + var canReuseCache = + cachedTaggedSpan?.Snapshot == snapshot && + cachedTaggedSpan.Value.Contains(spanToTag); - // We don't need to actually classify if what we're being asked for is a subspan - // of the last classification we performed. - var canReuseCache = - cachedTaggedSpan?.Snapshot == snapshot && - cachedTaggedSpan.Value.Contains(spanToTag); + if (!canReuseCache) + { + // Our cache is not there, or is out of date. We need to compute the up to date results. + var context = new TaggerContext(document, snapshot); + var options = _globalOptions.GetClassificationOptions(document.Project.Language); - if (!canReuseCache) + _owner._threadingContext.JoinableTaskFactory.Run(async () => { - // Our cache is not there, or is out of date. We need to compute the up to date results. - var context = new TaggerContext(document, snapshot); - var options = _globalOptions.GetClassificationOptions(document.Project.Language); - - _owner._threadingContext.JoinableTaskFactory.Run(async () => - { - var snapshotSpan = new DocumentSnapshotSpan(document, spanToTag); - - // When copying/pasting, ensure we have classifications fully computed for the requested spans - // for both semantic classifications and embedded lang classifications. - await ProduceTagsAsync(context, snapshotSpan, classificationService, options, ClassificationType.Semantic, cancellationToken).ConfigureAwait(false); - await ProduceTagsAsync(context, snapshotSpan, classificationService, options, ClassificationType.EmbeddedLanguage, cancellationToken).ConfigureAwait(false); - }); - - cachedTaggedSpan = spanToTag; - cachedTags = new TagSpanIntervalTree(snapshot.TextBuffer, SpanTrackingMode.EdgeExclusive, context.TagSpans); - - lock (_gate) - { - _cachedTaggedSpan = cachedTaggedSpan; - _cachedTags = cachedTags; - } - } + var snapshotSpan = new DocumentSnapshotSpan(document, spanToTag); - return SegmentedListPool.ComputeList( - static (args, tags) => args.cachedTags?.AddIntersectingTagSpans(args.spans, tags), - (cachedTags, spans), - _: (ITagSpan?)null); - } + // When copying/pasting, ensure we have classifications fully computed for the requested spans + // for both semantic classifications and embedded lang classifications. + await ProduceTagsAsync(context, snapshotSpan, classificationService, options, ClassificationType.Semantic, cancellationToken).ConfigureAwait(false); + await ProduceTagsAsync(context, snapshotSpan, classificationService, options, ClassificationType.EmbeddedLanguage, cancellationToken).ConfigureAwait(false); + }); - private Task ProduceTagsAsync( - TaggerContext context, DocumentSnapshotSpan snapshotSpan, - IClassificationService classificationService, ClassificationOptions options, ClassificationType type, CancellationToken cancellationToken) - { - return ClassificationUtilities.ProduceTagsAsync( - context, snapshotSpan, classificationService, _owner._typeMap, options, type, cancellationToken); - } + cachedTaggedSpan = spanToTag; + cachedTags = new TagSpanIntervalTree(snapshot.TextBuffer, SpanTrackingMode.EdgeExclusive, context.TagSpans); - private void GetCachedInfo(out SnapshotSpan? cachedTaggedSpan, out TagSpanIntervalTree? cachedTags) - { lock (_gate) { - cachedTaggedSpan = _cachedTaggedSpan; - cachedTags = _cachedTags; + _cachedTaggedSpan = cachedTaggedSpan; + _cachedTags = cachedTags; } } + + return SegmentedListPool.ComputeList( + static (args, tags) => args.cachedTags?.AddIntersectingTagSpans(args.spans, tags), + (cachedTags, spans), + _: (ITagSpan?)null); + } + + private Task ProduceTagsAsync( + TaggerContext context, DocumentSnapshotSpan snapshotSpan, + IClassificationService classificationService, ClassificationOptions options, ClassificationType type, CancellationToken cancellationToken) + { + return ClassificationUtilities.ProduceTagsAsync( + context, snapshotSpan, classificationService, _owner._typeMap, options, type, cancellationToken); + } + + private void GetCachedInfo(out SnapshotSpan? cachedTaggedSpan, out TagSpanIntervalTree? cachedTags) + { + lock (_gate) + { + cachedTaggedSpan = _cachedTaggedSpan; + cachedTags = _cachedTags; + } } } } diff --git a/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.cs b/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.cs index 9e23e82a0f21c..7679a7cb4ac9d 100644 --- a/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.cs @@ -13,48 +13,47 @@ using Microsoft.VisualStudio.Text.Tagging; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +/// +/// This is the tagger we use for buffer classification scenarios. It is only used for +/// IAccurateTagger scenarios. Namely: Copy/Paste and Printing. We use an 'Accurate' buffer +/// tagger since these features need to get classification tags for the entire file. +/// +/// i.e. if you're printing, you want semantic classification even for code that's not in view. +/// The same applies to copy/pasting. +/// +[Export(typeof(ITaggerProvider))] +[TagType(typeof(IClassificationTag))] +[ContentType(ContentTypeNames.CSharpContentType)] +[ContentType(ContentTypeNames.VisualBasicContentType)] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal partial class CopyPasteAndPrintingClassificationBufferTaggerProvider( + IThreadingContext threadingContext, + ClassificationTypeMap typeMap, + IAsynchronousOperationListenerProvider listenerProvider, + IGlobalOptionService globalOptions) : ITaggerProvider { - /// - /// This is the tagger we use for buffer classification scenarios. It is only used for - /// IAccurateTagger scenarios. Namely: Copy/Paste and Printing. We use an 'Accurate' buffer - /// tagger since these features need to get classification tags for the entire file. - /// - /// i.e. if you're printing, you want semantic classification even for code that's not in view. - /// The same applies to copy/pasting. - /// - [Export(typeof(ITaggerProvider))] - [TagType(typeof(IClassificationTag))] - [ContentType(ContentTypeNames.CSharpContentType)] - [ContentType(ContentTypeNames.VisualBasicContentType)] - [method: ImportingConstructor] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal partial class CopyPasteAndPrintingClassificationBufferTaggerProvider( - IThreadingContext threadingContext, - ClassificationTypeMap typeMap, - IAsynchronousOperationListenerProvider listenerProvider, - IGlobalOptionService globalOptions) : ITaggerProvider + private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.Classification); + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly ClassificationTypeMap _typeMap = typeMap; + private readonly IGlobalOptionService _globalOptions = globalOptions; + + public IAccurateTagger? CreateTagger(ITextBuffer buffer) where T : ITag { - private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.Classification); - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly ClassificationTypeMap _typeMap = typeMap; - private readonly IGlobalOptionService _globalOptions = globalOptions; + _threadingContext.ThrowIfNotOnUIThread(); - public IAccurateTagger? CreateTagger(ITextBuffer buffer) where T : ITag + // The LSP client will handle producing tags when running under the LSP editor. + // Our tagger implementation should return nothing to prevent conflicts. + if (buffer.IsInLspEditorContext()) { - _threadingContext.ThrowIfNotOnUIThread(); - - // The LSP client will handle producing tags when running under the LSP editor. - // Our tagger implementation should return nothing to prevent conflicts. - if (buffer.IsInLspEditorContext()) - { - return null; - } - - return new Tagger(this, buffer, _asyncListener, _globalOptions) as IAccurateTagger; + return null; } - ITagger? ITaggerProvider.CreateTagger(ITextBuffer buffer) - => CreateTagger(buffer); + return new Tagger(this, buffer, _asyncListener, _globalOptions) as IAccurateTagger; } + + ITagger? ITaggerProvider.CreateTagger(ITextBuffer buffer) + => CreateTagger(buffer); } diff --git a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs index 5a912b50f9897..88b1ff86d4d79 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs @@ -59,13 +59,7 @@ protected sealed override ITaggerEventSource CreateEventSource(ITextView textVie // Note: we don't listen for OnTextChanged. They'll get reported by the ViewSpan changing and also the // SemanticChange notification. - // - // Note: because we use frozen-partial documents for semantic classification, we may end up with incomplete - // semantics (esp. during solution load). Because of this, we also register to hear when the full - // compilation is available so that reclassify and bring ourselves up to date. - return new CompilationAvailableTaggerEventSource( - subjectBuffer, - AsyncListener, + return TaggerEventSources.Compose( TaggerEventSources.OnViewSpanChanged(ThreadingContext, textView), TaggerEventSources.OnWorkspaceChanged(subjectBuffer, AsyncListener), TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer), diff --git a/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs b/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs index ee4ae67f0d5ed..98aac8074ef6a 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs @@ -25,179 +25,190 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal static class ClassificationUtilities { - internal static class ClassificationUtilities + public static TagSpan Convert(IClassificationTypeMap typeMap, ITextSnapshot snapshot, ClassifiedSpan classifiedSpan) + { + return new TagSpan( + classifiedSpan.TextSpan.ToSnapshotSpan(snapshot), + new ClassificationTag(typeMap.GetClassificationType(classifiedSpan.ClassificationType))); + } + + public static async Task ProduceTagsAsync( + TaggerContext context, + DocumentSnapshotSpan spanToTag, + IClassificationService classificationService, + ClassificationTypeMap typeMap, + ClassificationOptions options, + ClassificationType type, + CancellationToken cancellationToken) { - public static TagSpan Convert(IClassificationTypeMap typeMap, ITextSnapshot snapshot, ClassifiedSpan classifiedSpan) + var document = spanToTag.Document; + if (document == null) + return; + + // Don't block getting classifications on building the full compilation. This may take a significant amount + // of time and can cause a very latency sensitive operation (copying) to block the user while we wait on this + // work to happen. + // + // It's also a better experience to get classifications to the user faster versus waiting a potentially + // large amount of time waiting for all the compilation information to be built. For example, we can + // classify types that we've parsed in other files, or partially loaded from metadata, even if we're still + // parsing/loading. For cross language projects, this also produces semantic classifications more quickly + // as we do not have to wait on skeletons to be built. + + document = document.WithFrozenPartialSemantics(cancellationToken); + options = options with { ForceFrozenPartialSemanticsForCrossProcessOperations = true }; + + var classified = await TryClassifyContainingMemberSpanAsync( + context, document, spanToTag.SnapshotSpan, classificationService, typeMap, options, type, cancellationToken).ConfigureAwait(false); + if (classified) { - return new TagSpan( - classifiedSpan.TextSpan.ToSnapshotSpan(snapshot), - new ClassificationTag(typeMap.GetClassificationType(classifiedSpan.ClassificationType))); + return; } - public static async Task ProduceTagsAsync( - TaggerContext context, - DocumentSnapshotSpan spanToTag, - IClassificationService classificationService, - ClassificationTypeMap typeMap, - ClassificationOptions options, - ClassificationType type, - CancellationToken cancellationToken) - { - var document = spanToTag.Document; - if (document == null) - return; - - // Don't block getting classifications on building the full compilation. This may take a significant amount - // of time and can cause a very latency sensitive operation (copying) to block the user while we wait on this - // work to happen. - // - // It's also a better experience to get classifications to the user faster versus waiting a potentially - // large amount of time waiting for all the compilation information to be built. For example, we can - // classify types that we've parsed in other files, or partially loaded from metadata, even if we're still - // parsing/loading. For cross language projects, this also produces semantic classifications more quickly - // as we do not have to wait on skeletons to be built. - - document = document.WithFrozenPartialSemantics(cancellationToken); - options = options with { ForceFrozenPartialSemanticsForCrossProcessOperations = true }; - - var classified = await TryClassifyContainingMemberSpanAsync( - context, document, spanToTag.SnapshotSpan, classificationService, typeMap, options, type, cancellationToken).ConfigureAwait(false); - if (classified) - { - return; - } + // We weren't able to use our specialized codepaths for semantic classifying. + // Fall back to classifying the full span that was asked for. + await ClassifySpansAsync( + context, document, spanToTag.SnapshotSpan, classificationService, typeMap, options, type, cancellationToken).ConfigureAwait(false); + } - // We weren't able to use our specialized codepaths for semantic classifying. - // Fall back to classifying the full span that was asked for. - await ClassifySpansAsync( - context, document, spanToTag.SnapshotSpan, classificationService, typeMap, options, type, cancellationToken).ConfigureAwait(false); + private static async Task TryClassifyContainingMemberSpanAsync( + TaggerContext context, + Document document, + SnapshotSpan snapshotSpan, + IClassificationService classificationService, + ClassificationTypeMap typeMap, + ClassificationOptions options, + ClassificationType type, + CancellationToken cancellationToken) + { + var range = context.TextChangeRange; + if (range == null) + { + // There was no text change range, we can't just reclassify a member body. + return false; } - private static async Task TryClassifyContainingMemberSpanAsync( - TaggerContext context, - Document document, - SnapshotSpan snapshotSpan, - IClassificationService classificationService, - ClassificationTypeMap typeMap, - ClassificationOptions options, - ClassificationType type, - CancellationToken cancellationToken) + // there was top level edit, check whether that edit updated top level element + if (!document.SupportsSyntaxTree) + return false; + + var lastSemanticVersion = (VersionStamp?)context.State; + if (lastSemanticVersion != null) { - var range = context.TextChangeRange; - if (range == null) + var currentSemanticVersion = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); + if (lastSemanticVersion.Value != currentSemanticVersion) { - // There was no text change range, we can't just reclassify a member body. + // A top level change was made. We can't perform this optimization. return false; } + } - // there was top level edit, check whether that edit updated top level element - if (!document.SupportsSyntaxTree) - return false; - - var lastSemanticVersion = (VersionStamp?)context.State; - if (lastSemanticVersion != null) - { - var currentSemanticVersion = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); - if (lastSemanticVersion.Value != currentSemanticVersion) - { - // A top level change was made. We can't perform this optimization. - return false; - } - } - - var service = document.GetRequiredLanguageService(); - - // perf optimization. Check whether all edits since the last update has happened within - // a member. If it did, it will find the member that contains the changes and only refresh - // that member. If possible, try to get a speculative binder to make things even cheaper. + var service = document.GetRequiredLanguageService(); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + // perf optimization. Check whether all edits since the last update has happened within + // a member. If it did, it will find the member that contains the changes and only refresh + // that member. If possible, try to get a speculative binder to make things even cheaper. - var changedSpan = new TextSpan(range.Value.Span.Start, range.Value.NewLength); - var member = service.GetContainingMemberDeclaration(root, changedSpan.Start); - if (member == null || !member.FullSpan.Contains(changedSpan)) - { - // The edit was not fully contained in a member. Reclassify everything. - return false; - } + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var subTextSpan = service.GetMemberBodySpanForSpeculativeBinding(member); - if (subTextSpan.IsEmpty) - { - // Wasn't a member we could reclassify independently. - return false; - } - - var subSpanToTag = new SnapshotSpan( - snapshotSpan.Snapshot, - subTextSpan.Contains(changedSpan) ? subTextSpan.ToSpan() : member.FullSpan.ToSpan()); + var changedSpan = new TextSpan(range.Value.Span.Start, range.Value.NewLength); + var member = service.GetContainingMemberDeclaration(root, changedSpan.Start); + if (member == null || !member.FullSpan.Contains(changedSpan)) + { + // The edit was not fully contained in a member. Reclassify everything. + return false; + } - // re-classify only the member we're inside. - await ClassifySpansAsync( - context, document, subSpanToTag, classificationService, typeMap, options, type, cancellationToken).ConfigureAwait(false); - return true; + var memberBodySpan = service.GetMemberBodySpanForSpeculativeBinding(member); + if (memberBodySpan.IsEmpty) + { + // Wasn't a member we could reclassify independently. + return false; } - private static async Task ClassifySpansAsync( - TaggerContext context, - Document document, - SnapshotSpan snapshotSpan, - IClassificationService classificationService, - ClassificationTypeMap typeMap, - ClassificationOptions options, - ClassificationType type, - CancellationToken cancellationToken) + // TODO(cyrusn): Unclear what this logic is for. It looks like it's just trying to narrow the span down + // slightly from the full member, just to its body. Unclear if this provides any substantive benefits. But + // keeping for now to preserve long standing logic. + var memberSpanToClassify = memberBodySpan.Contains(changedSpan) + ? memberBodySpan.ToSpan() + : member.FullSpan.ToSpan(); + + // Take the subspan we know we want to classify, and intersect that with the actual span being asked for. + // That way if we're only asking for a portion of a method, we still only classify that, and not the whole + // method. + var finalSpanToClassify = memberSpanToClassify.Intersection(snapshotSpan.Span); + if (finalSpanToClassify is null) + return false; + + var subSpanToTag = new SnapshotSpan(snapshotSpan.Snapshot, finalSpanToClassify.Value); + + // re-classify only the member we're inside. + await ClassifySpansAsync( + context, document, subSpanToTag, classificationService, typeMap, options, type, cancellationToken).ConfigureAwait(false); + return true; + } + + private static async Task ClassifySpansAsync( + TaggerContext context, + Document document, + SnapshotSpan snapshotSpan, + IClassificationService classificationService, + ClassificationTypeMap typeMap, + ClassificationOptions options, + ClassificationType type, + CancellationToken cancellationToken) + { + try { - try + using (Logger.LogBlock(FunctionId.Tagger_SemanticClassification_TagProducer_ProduceTags, cancellationToken)) { - using (Logger.LogBlock(FunctionId.Tagger_SemanticClassification_TagProducer_ProduceTags, cancellationToken)) - { - using var _ = Classifier.GetPooledList(out var classifiedSpans); + using var _ = Classifier.GetPooledList(out var classifiedSpans); - await AddClassificationsAsync( - classificationService, options, document, snapshotSpan, classifiedSpans, type, cancellationToken).ConfigureAwait(false); + await AddClassificationsAsync( + classificationService, options, document, snapshotSpan, classifiedSpans, type, cancellationToken).ConfigureAwait(false); - foreach (var span in classifiedSpans) - context.AddTag(Convert(typeMap, snapshotSpan.Snapshot, span)); + foreach (var span in classifiedSpans) + context.AddTag(Convert(typeMap, snapshotSpan.Snapshot, span)); - var version = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); + var version = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); - // Let the context know that this was the span we actually tried to tag. - context.SetSpansTagged([snapshotSpan]); - context.State = version; - } - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) - { - throw ExceptionUtilities.Unreachable(); + // Let the context know that this was the span we actually tried to tag. + context.SetSpansTagged([snapshotSpan]); + context.State = version; } } + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) + { + throw ExceptionUtilities.Unreachable(); + } + } - private static async Task AddClassificationsAsync( - IClassificationService classificationService, - ClassificationOptions options, - Document document, - SnapshotSpan snapshotSpan, - SegmentedList classifiedSpans, - ClassificationType type, - CancellationToken cancellationToken) + private static async Task AddClassificationsAsync( + IClassificationService classificationService, + ClassificationOptions options, + Document document, + SnapshotSpan snapshotSpan, + SegmentedList classifiedSpans, + ClassificationType type, + CancellationToken cancellationToken) + { + if (type == ClassificationType.Semantic) { - if (type == ClassificationType.Semantic) - { - await classificationService.AddSemanticClassificationsAsync( - document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); - } - else if (type == ClassificationType.EmbeddedLanguage) - { - await classificationService.AddEmbeddedLanguageClassificationsAsync( - document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); - } - else - { - throw ExceptionUtilities.UnexpectedValue(type); - } + await classificationService.AddSemanticClassificationsAsync( + document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); + } + else if (type == ClassificationType.EmbeddedLanguage) + { + await classificationService.AddEmbeddedLanguageClassificationsAsync( + document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); + } + else + { + throw ExceptionUtilities.UnexpectedValue(type); } } } diff --git a/src/EditorFeatures/Core/Classification/Semantic/EmbeddedLanguageClassificationViewTaggerProvider.cs b/src/EditorFeatures/Core/Classification/Semantic/EmbeddedLanguageClassificationViewTaggerProvider.cs index bf099b75c7867..93e126db770a9 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/EmbeddedLanguageClassificationViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/EmbeddedLanguageClassificationViewTaggerProvider.cs @@ -8,19 +8,18 @@ using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Workspaces; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +/// +/// This is the tagger we use for classifying the embedded language string literals currently visible in the editor +/// view. Intentionally not exported. It is consumed by the +/// instead. +/// +internal partial class EmbeddedLanguageClassificationViewTaggerProvider( + IThreadingContext threadingContext, + ClassificationTypeMap typeMap, + IGlobalOptionService globalOptions, + [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) : AbstractSemanticOrEmbeddedClassificationViewTaggerProvider(threadingContext, typeMap, globalOptions, visibilityTracker, listenerProvider, ClassificationType.EmbeddedLanguage) { - /// - /// This is the tagger we use for classifying the embedded language string literals currently visible in the editor - /// view. Intentionally not exported. It is consumed by the - /// instead. - /// - internal partial class EmbeddedLanguageClassificationViewTaggerProvider( - IThreadingContext threadingContext, - ClassificationTypeMap typeMap, - IGlobalOptionService globalOptions, - [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) : AbstractSemanticOrEmbeddedClassificationViewTaggerProvider(threadingContext, typeMap, globalOptions, visibilityTracker, listenerProvider, ClassificationType.EmbeddedLanguage) - { - } } diff --git a/src/EditorFeatures/Core/Classification/Semantic/SemanticClassificationViewTaggerProvider.cs b/src/EditorFeatures/Core/Classification/Semantic/SemanticClassificationViewTaggerProvider.cs index 11cb173c98d6b..1cdb5fd314633 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/SemanticClassificationViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/SemanticClassificationViewTaggerProvider.cs @@ -8,19 +8,18 @@ using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Workspaces; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +/// +/// This is the tagger we use for view classification scenarios. It is used for classifying code in the editor. We +/// use a view tagger so that we can only classify what's in view, and not the whole file. Intentionally not +/// exported. It is consumed by the instead. +/// +internal partial class SemanticClassificationViewTaggerProvider( + IThreadingContext threadingContext, + ClassificationTypeMap typeMap, + IGlobalOptionService globalOptions, + [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) : AbstractSemanticOrEmbeddedClassificationViewTaggerProvider(threadingContext, typeMap, globalOptions, visibilityTracker, listenerProvider, ClassificationType.Semantic) { - /// - /// This is the tagger we use for view classification scenarios. It is used for classifying code in the editor. We - /// use a view tagger so that we can only classify what's in view, and not the whole file. Intentionally not - /// exported. It is consumed by the instead. - /// - internal partial class SemanticClassificationViewTaggerProvider( - IThreadingContext threadingContext, - ClassificationTypeMap typeMap, - IGlobalOptionService globalOptions, - [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) : AbstractSemanticOrEmbeddedClassificationViewTaggerProvider(threadingContext, typeMap, globalOptions, visibilityTracker, listenerProvider, ClassificationType.Semantic) - { - } } diff --git a/src/EditorFeatures/Core/Classification/Semantic/SemanticColorizerOptionsStorage.cs b/src/EditorFeatures/Core/Classification/Semantic/SemanticColorizerOptionsStorage.cs index 40e1f43405d86..7aa0880e1f0f4 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/SemanticColorizerOptionsStorage.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/SemanticColorizerOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal static class SemanticColorizerOptionsStorage { - internal static class SemanticColorizerOptionsStorage - { - public static readonly Option2 SemanticColorizer = new("dotnet_enable_semantic_colorizer", defaultValue: true); - } + public static readonly Option2 SemanticColorizer = new("dotnet_enable_semantic_colorizer", defaultValue: true); } diff --git a/src/EditorFeatures/Core/Classification/Syntactic/SyntacticClassificationTaggerProvider.TagComputer.LastLineCache.cs b/src/EditorFeatures/Core/Classification/Syntactic/SyntacticClassificationTaggerProvider.TagComputer.LastLineCache.cs index 380b041bc3458..02c1ade1988b5 100644 --- a/src/EditorFeatures/Core/Classification/Syntactic/SyntacticClassificationTaggerProvider.TagComputer.LastLineCache.cs +++ b/src/EditorFeatures/Core/Classification/Syntactic/SyntacticClassificationTaggerProvider.TagComputer.LastLineCache.cs @@ -9,60 +9,59 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal partial class SyntacticClassificationTaggerProvider { - internal partial class SyntacticClassificationTaggerProvider + internal partial class TagComputer { - internal partial class TagComputer + /// + /// it is a helper class that encapsulates logic on holding onto last classification result + /// + private class LastLineCache(IThreadingContext threadingContext) { - /// - /// it is a helper class that encapsulates logic on holding onto last classification result - /// - private class LastLineCache(IThreadingContext threadingContext) - { - // this helper class is primarily to improve active typing perf. don't bother to cache - // something very big. - private const int MaxClassificationNumber = 32; + // this helper class is primarily to improve active typing perf. don't bother to cache + // something very big. + private const int MaxClassificationNumber = 32; - // mutating state - private SnapshotSpan _span; - private readonly SegmentedList _classifications = []; - private readonly IThreadingContext _threadingContext = threadingContext; + // mutating state + private SnapshotSpan _span; + private readonly SegmentedList _classifications = []; + private readonly IThreadingContext _threadingContext = threadingContext; - private void Clear() - { - _threadingContext.ThrowIfNotOnUIThread(); + private void Clear() + { + _threadingContext.ThrowIfNotOnUIThread(); - _span = default; - _classifications.Clear(); - } + _span = default; + _classifications.Clear(); + } - public bool TryUseCache(SnapshotSpan span, SegmentedList classifications) + public bool TryUseCache(SnapshotSpan span, SegmentedList classifications) + { + _threadingContext.ThrowIfNotOnUIThread(); + + // currently, it is using SnapshotSpan even though holding onto it could be + // expensive. reason being it should be very soon sync-ed to latest snapshot. + if (_span.Equals(span)) { - _threadingContext.ThrowIfNotOnUIThread(); + classifications.AddRange(_classifications); + return true; + } - // currently, it is using SnapshotSpan even though holding onto it could be - // expensive. reason being it should be very soon sync-ed to latest snapshot. - if (_span.Equals(span)) - { - classifications.AddRange(_classifications); - return true; - } + this.Clear(); + return false; + } - this.Clear(); - return false; - } + public void Update(SnapshotSpan span, SegmentedList classifications) + { + _threadingContext.ThrowIfNotOnUIThread(); + this.Clear(); - public void Update(SnapshotSpan span, SegmentedList classifications) + if (classifications.Count < MaxClassificationNumber) { - _threadingContext.ThrowIfNotOnUIThread(); - this.Clear(); - - if (classifications.Count < MaxClassificationNumber) - { - _span = span; - _classifications.AddRange(classifications); - } + _span = span; + _classifications.AddRange(classifications); } } } diff --git a/src/EditorFeatures/Core/Classification/Syntactic/SyntacticColorizerOptionsStorage.cs b/src/EditorFeatures/Core/Classification/Syntactic/SyntacticColorizerOptionsStorage.cs index b30c19bc3e7a8..0c9dff0b37788 100644 --- a/src/EditorFeatures/Core/Classification/Syntactic/SyntacticColorizerOptionsStorage.cs +++ b/src/EditorFeatures/Core/Classification/Syntactic/SyntacticColorizerOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal static class SyntacticColorizerOptionsStorage { - internal static class SyntacticColorizerOptionsStorage - { - public static readonly Option2 SyntacticColorizer = new("dotnet_enable_syntactic_colorizer", defaultValue: true); - } + public static readonly Option2 SyntacticColorizer = new("dotnet_enable_syntactic_colorizer", defaultValue: true); } diff --git a/src/EditorFeatures/Core/Classification/TotalClassificationTaggerProvider.cs b/src/EditorFeatures/Core/Classification/TotalClassificationTaggerProvider.cs index fe59deb7043c5..ed3aab5395493 100644 --- a/src/EditorFeatures/Core/Classification/TotalClassificationTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/TotalClassificationTaggerProvider.cs @@ -190,7 +190,12 @@ void AddEmbeddedClassifications() // Only need to ask for the spans that overlapped the string literals. using var _1 = SegmentedListPool.GetPooledList>(out var embeddedClassifications); - var stringLiteralSpans = new NormalizedSnapshotSpanCollection(stringLiterals.Select(s => s.Span)); + var stringLiteralSpansFull = new NormalizedSnapshotSpanCollection(stringLiterals.Select(s => s.Span)); + + // The spans of the string literal itself may be far off screen. Intersect the string literal spans + // with the view spans to get the actual spans we want to classify. + var stringLiteralSpans = NormalizedSnapshotSpanCollection.Intersection(stringLiteralSpansFull, spans); + embeddedTagger.AddTags(stringLiteralSpans, embeddedClassifications); // Nothing complex to do if we got no embedded classifications back. Just add in all the string diff --git a/src/EditorFeatures/Core/CodeActions/CodeActionEditHandlerService.cs b/src/EditorFeatures/Core/CodeActions/CodeActionEditHandlerService.cs index 2fa2073e77f25..695928b88052c 100644 --- a/src/EditorFeatures/Core/CodeActions/CodeActionEditHandlerService.cs +++ b/src/EditorFeatures/Core/CodeActions/CodeActionEditHandlerService.cs @@ -23,367 +23,366 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeActions +namespace Microsoft.CodeAnalysis.CodeActions; + +[Export(typeof(ICodeActionEditHandlerService))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CodeActionEditHandlerService( + IThreadingContext threadingContext, + IPreviewFactoryService previewService, + IInlineRenameService renameService, + ITextBufferAssociatedViewService associatedViewService) : ICodeActionEditHandlerService { - [Export(typeof(ICodeActionEditHandlerService))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CodeActionEditHandlerService( - IThreadingContext threadingContext, - IPreviewFactoryService previewService, - IInlineRenameService renameService, - ITextBufferAssociatedViewService associatedViewService) : ICodeActionEditHandlerService + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IPreviewFactoryService _previewService = previewService; + private readonly IInlineRenameService _renameService = renameService; + private readonly ITextBufferAssociatedViewService _associatedViewService = associatedViewService; + + public ITextBufferAssociatedViewService AssociatedViewService => _associatedViewService; + + public async Task GetPreviewsAsync( + Workspace workspace, ImmutableArray operations, CancellationToken cancellationToken) { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IPreviewFactoryService _previewService = previewService; - private readonly IInlineRenameService _renameService = renameService; - private readonly ITextBufferAssociatedViewService _associatedViewService = associatedViewService; + if (operations.IsDefaultOrEmpty) + return null; - public ITextBufferAssociatedViewService AssociatedViewService => _associatedViewService; + SolutionPreviewResult? currentResult = null; - public async Task GetPreviewsAsync( - Workspace workspace, ImmutableArray operations, CancellationToken cancellationToken) + foreach (var op in operations) { - if (operations.IsDefaultOrEmpty) - return null; + cancellationToken.ThrowIfCancellationRequested(); - SolutionPreviewResult? currentResult = null; - - foreach (var op in operations) + if (op is ApplyChangesOperation applyChanges) { - cancellationToken.ThrowIfCancellationRequested(); + var oldSolution = workspace.CurrentSolution; + var newSolution = await applyChanges.ChangedSolution.WithMergedLinkedFileChangesAsync( + oldSolution, cancellationToken: cancellationToken).ConfigureAwait(false); + var preview = _previewService.GetSolutionPreviews( + oldSolution, newSolution, cancellationToken); - if (op is ApplyChangesOperation applyChanges) + if (preview != null && !preview.IsEmpty) { - var oldSolution = workspace.CurrentSolution; - var newSolution = await applyChanges.ChangedSolution.WithMergedLinkedFileChangesAsync( - oldSolution, cancellationToken: cancellationToken).ConfigureAwait(false); - var preview = _previewService.GetSolutionPreviews( - oldSolution, newSolution, cancellationToken); - - if (preview != null && !preview.IsEmpty) - { - currentResult = SolutionPreviewResult.Merge(currentResult, preview); - continue; - } - } - - if (op is PreviewOperation previewOp) - { - currentResult = SolutionPreviewResult.Merge(currentResult, - new SolutionPreviewResult(_threadingContext, new SolutionPreviewItem( - projectId: null, documentId: null, - lazyPreview: c => previewOp.GetPreviewAsync(c)))); - continue; - } - - var title = op.Title; - - if (title != null) - { - currentResult = SolutionPreviewResult.Merge(currentResult, - new SolutionPreviewResult(_threadingContext, new SolutionPreviewItem( - projectId: null, documentId: null, text: title))); + currentResult = SolutionPreviewResult.Merge(currentResult, preview); continue; } } - return currentResult; - } - - public async Task ApplyAsync( - Workspace workspace, - Solution originalSolution, - Document? fromDocument, - ImmutableArray operations, - string title, - IProgress progressTracker, - CancellationToken cancellationToken) - { - // Much of the work we're going to do will be on the UI thread, so switch there preemptively. - // When we get to the expensive parts we can do in the BG then we'll switch over to relinquish - // the UI thread. - await this._threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - - if (operations.IsDefaultOrEmpty) + if (op is PreviewOperation previewOp) { - return _renameService.ActiveSession is null; + currentResult = SolutionPreviewResult.Merge(currentResult, + new SolutionPreviewResult(_threadingContext, new SolutionPreviewItem( + projectId: null, documentId: null, + lazyPreview: c => previewOp.GetPreviewAsync(c)))); + continue; } - if (_renameService.ActiveSession != null) + var title = op.Title; + + if (title != null) { - workspace.Services.GetService()?.SendNotification( - EditorFeaturesResources.Cannot_apply_operation_while_a_rename_session_is_active, - severity: NotificationSeverity.Error); - return false; + currentResult = SolutionPreviewResult.Merge(currentResult, + new SolutionPreviewResult(_threadingContext, new SolutionPreviewItem( + projectId: null, documentId: null, text: title))); + continue; } + } - var oldSolution = workspace.CurrentSolution; - - var applied = false; - - // Determine if we're making a simple text edit to a single file or not. - // If we're not, then we need to make a linked global undo to wrap the - // application of these operations. This way we should be able to undo - // them all with one user action. - // - // The reason we don't always create a global undo is that a global undo - // forces all files to save. And that's rather a heavyweight and - // unexpected experience for users (for the common case where a single - // file got edited). - var singleChangedDocument = TryGetSingleChangedText(oldSolution, operations); - if (singleChangedDocument != null) - { - var text = await singleChangedDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(true); + return currentResult; + } - using (workspace.Services.GetRequiredService().RegisterUndoTransaction(text, title)) - { - try - { - _threadingContext.ThrowIfNotOnUIThread(); + public async Task ApplyAsync( + Workspace workspace, + Solution originalSolution, + Document? fromDocument, + ImmutableArray operations, + string title, + IProgress progressTracker, + CancellationToken cancellationToken) + { + // Much of the work we're going to do will be on the UI thread, so switch there preemptively. + // When we get to the expensive parts we can do in the BG then we'll switch over to relinquish + // the UI thread. + await this._threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - applied = await operations.Single().TryApplyAsync( - workspace, originalSolution, progressTracker, cancellationToken).ConfigureAwait(true); - } - catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, cancellationToken)) - { - throw ExceptionUtilities.Unreachable(); - } - } - } - else - { - // More than just a single document changed. Make a global undo to run - // all the changes under. - using var transaction = workspace.OpenGlobalUndoTransaction(title); - - // link current file in the global undo transaction - // Do this before processing operations, since that can change - // documentIds. - if (fromDocument != null) - { - transaction.AddDocument(fromDocument.Id); - } + if (operations.IsDefaultOrEmpty) + { + return _renameService.ActiveSession is null; + } + + if (_renameService.ActiveSession != null) + { + workspace.Services.GetService()?.SendNotification( + EditorFeaturesResources.Cannot_apply_operation_while_a_rename_session_is_active, + severity: NotificationSeverity.Error); + return false; + } + var oldSolution = workspace.CurrentSolution; + + var applied = false; + + // Determine if we're making a simple text edit to a single file or not. + // If we're not, then we need to make a linked global undo to wrap the + // application of these operations. This way we should be able to undo + // them all with one user action. + // + // The reason we don't always create a global undo is that a global undo + // forces all files to save. And that's rather a heavyweight and + // unexpected experience for users (for the common case where a single + // file got edited). + var singleChangedDocument = TryGetSingleChangedText(oldSolution, operations); + if (singleChangedDocument != null) + { + var text = await singleChangedDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(true); + + using (workspace.Services.GetRequiredService().RegisterUndoTransaction(text, title)) + { try { - // Come back to the UI thread after processing the operations so we can commit the transaction - applied = await ProcessOperationsAsync( - workspace, originalSolution, operations, progressTracker, cancellationToken).ConfigureAwait(true); + _threadingContext.ThrowIfNotOnUIThread(); + + applied = await operations.Single().TryApplyAsync( + workspace, originalSolution, progressTracker, cancellationToken).ConfigureAwait(true); } catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, cancellationToken)) { throw ExceptionUtilities.Unreachable(); } - - transaction.Commit(); } - - var updatedSolution = operations.OfType().FirstOrDefault()?.ChangedSolution ?? oldSolution; - await TryNavigateToLocationOrStartRenameSessionAsync( - workspace, operations, oldSolution, updatedSolution, cancellationToken).ConfigureAwait(false); - return applied; } - - private static TextDocument? TryGetSingleChangedText( - Solution oldSolution, ImmutableArray operationsList) + else { - Debug.Assert(operationsList.Length > 0); - if (operationsList.Length > 1) - return null; - - if (operationsList.Single() is not ApplyChangesOperation applyOperation) - return null; - - var newSolution = applyOperation.ChangedSolution; - var changes = newSolution.GetChanges(oldSolution); - - if (changes.GetAddedProjects().Any() || - changes.GetRemovedProjects().Any()) + // More than just a single document changed. Make a global undo to run + // all the changes under. + using var transaction = workspace.OpenGlobalUndoTransaction(title); + + // link current file in the global undo transaction + // Do this before processing operations, since that can change + // documentIds. + if (fromDocument != null) { - return null; + transaction.AddDocument(fromDocument.Id); } - var projectChanges = changes.GetProjectChanges().ToImmutableArray(); - if (projectChanges.Length != 1) + try { - return null; + // Come back to the UI thread after processing the operations so we can commit the transaction + applied = await ProcessOperationsAsync( + workspace, originalSolution, operations, progressTracker, cancellationToken).ConfigureAwait(true); } - - var projectChange = projectChanges.Single(); - if (projectChange.GetAddedAdditionalDocuments().Any() || - projectChange.GetAddedAnalyzerReferences().Any() || - projectChange.GetAddedDocuments().Any() || - projectChange.GetAddedAnalyzerConfigDocuments().Any() || - projectChange.GetAddedMetadataReferences().Any() || - projectChange.GetAddedProjectReferences().Any() || - projectChange.GetRemovedAdditionalDocuments().Any() || - projectChange.GetRemovedAnalyzerReferences().Any() || - projectChange.GetRemovedDocuments().Any() || - projectChange.GetRemovedAnalyzerConfigDocuments().Any() || - projectChange.GetRemovedMetadataReferences().Any() || - projectChange.GetRemovedProjectReferences().Any()) + catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, cancellationToken)) { - return null; + throw ExceptionUtilities.Unreachable(); } - var changedAdditionalDocuments = projectChange.GetChangedAdditionalDocuments().ToImmutableArray(); - var changedDocuments = projectChange.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true).ToImmutableArray(); - var changedAnalyzerConfigDocuments = projectChange.GetChangedAnalyzerConfigDocuments().ToImmutableArray(); + transaction.Commit(); + } - if (changedAdditionalDocuments.Length + changedDocuments.Length + changedAnalyzerConfigDocuments.Length != 1) - { - return null; - } + var updatedSolution = operations.OfType().FirstOrDefault()?.ChangedSolution ?? oldSolution; + await TryNavigateToLocationOrStartRenameSessionAsync( + workspace, operations, oldSolution, updatedSolution, cancellationToken).ConfigureAwait(false); + return applied; + } - if (changedDocuments.Any(static (id, arg) => arg.newSolution.GetRequiredDocument(id).HasInfoChanged(arg.oldSolution.GetRequiredDocument(id)), (oldSolution, newSolution)) || - changedAdditionalDocuments.Any(static (id, arg) => arg.newSolution.GetRequiredAdditionalDocument(id).HasInfoChanged(arg.oldSolution.GetRequiredAdditionalDocument(id)), (oldSolution, newSolution)) || - changedAnalyzerConfigDocuments.Any(static (id, arg) => arg.newSolution.GetRequiredAnalyzerConfigDocument(id).HasInfoChanged(arg.oldSolution.GetRequiredAnalyzerConfigDocument(id)), (oldSolution, newSolution))) - { - return null; - } + private static TextDocument? TryGetSingleChangedText( + Solution oldSolution, ImmutableArray operationsList) + { + Debug.Assert(operationsList.Length > 0); + if (operationsList.Length > 1) + return null; - if (changedDocuments.Length == 1) - { - return oldSolution.GetDocument(changedDocuments[0]); - } - else if (changedAdditionalDocuments.Length == 1) - { - return oldSolution.GetAdditionalDocument(changedAdditionalDocuments[0]); - } - else - { - return oldSolution.GetAnalyzerConfigDocument(changedAnalyzerConfigDocuments[0]); - } + if (operationsList.Single() is not ApplyChangesOperation applyOperation) + return null; + + var newSolution = applyOperation.ChangedSolution; + var changes = newSolution.GetChanges(oldSolution); + + if (changes.GetAddedProjects().Any() || + changes.GetRemovedProjects().Any()) + { + return null; } - /// if all expected are applied successfully; - /// otherwise, . - private async Task ProcessOperationsAsync( - Workspace workspace, - Solution originalSolution, - ImmutableArray operations, - IProgress progressTracker, - CancellationToken cancellationToken) + var projectChanges = changes.GetProjectChanges().ToImmutableArray(); + if (projectChanges.Length != 1) { - await this._threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + return null; + } - var applied = true; - var seenApplyChanges = false; - foreach (var operation in operations) - { - if (operation is ApplyChangesOperation) - { - // there must be only one ApplyChangesOperation, we will ignore all other ones. - if (seenApplyChanges) - continue; + var projectChange = projectChanges.Single(); + if (projectChange.GetAddedAdditionalDocuments().Any() || + projectChange.GetAddedAnalyzerReferences().Any() || + projectChange.GetAddedDocuments().Any() || + projectChange.GetAddedAnalyzerConfigDocuments().Any() || + projectChange.GetAddedMetadataReferences().Any() || + projectChange.GetAddedProjectReferences().Any() || + projectChange.GetRemovedAdditionalDocuments().Any() || + projectChange.GetRemovedAnalyzerReferences().Any() || + projectChange.GetRemovedDocuments().Any() || + projectChange.GetRemovedAnalyzerConfigDocuments().Any() || + projectChange.GetRemovedMetadataReferences().Any() || + projectChange.GetRemovedProjectReferences().Any()) + { + return null; + } - seenApplyChanges = true; - } + var changedAdditionalDocuments = projectChange.GetChangedAdditionalDocuments().ToImmutableArray(); + var changedDocuments = projectChange.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true).ToImmutableArray(); + var changedAnalyzerConfigDocuments = projectChange.GetChangedAnalyzerConfigDocuments().ToImmutableArray(); + + if (changedAdditionalDocuments.Length + changedDocuments.Length + changedAnalyzerConfigDocuments.Length != 1) + { + return null; + } + + if (changedDocuments.Any(static (id, arg) => arg.newSolution.GetRequiredDocument(id).HasInfoChanged(arg.oldSolution.GetRequiredDocument(id)), (oldSolution, newSolution)) || + changedAdditionalDocuments.Any(static (id, arg) => arg.newSolution.GetRequiredAdditionalDocument(id).HasInfoChanged(arg.oldSolution.GetRequiredAdditionalDocument(id)), (oldSolution, newSolution)) || + changedAnalyzerConfigDocuments.Any(static (id, arg) => arg.newSolution.GetRequiredAnalyzerConfigDocument(id).HasInfoChanged(arg.oldSolution.GetRequiredAnalyzerConfigDocument(id)), (oldSolution, newSolution))) + { + return null; + } + + if (changedDocuments.Length == 1) + { + return oldSolution.GetDocument(changedDocuments[0]); + } + else if (changedAdditionalDocuments.Length == 1) + { + return oldSolution.GetAdditionalDocument(changedAdditionalDocuments[0]); + } + else + { + return oldSolution.GetAnalyzerConfigDocument(changedAnalyzerConfigDocuments[0]); + } + } + + /// if all expected are applied successfully; + /// otherwise, . + private async Task ProcessOperationsAsync( + Workspace workspace, + Solution originalSolution, + ImmutableArray operations, + IProgress progressTracker, + CancellationToken cancellationToken) + { + await this._threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + var applied = true; + var seenApplyChanges = false; + foreach (var operation in operations) + { + if (operation is ApplyChangesOperation) + { + // there must be only one ApplyChangesOperation, we will ignore all other ones. + if (seenApplyChanges) + continue; - _threadingContext.ThrowIfNotOnUIThread(); - applied &= await operation.TryApplyAsync(workspace, originalSolution, progressTracker, cancellationToken).ConfigureAwait(true); + seenApplyChanges = true; } - return applied; + _threadingContext.ThrowIfNotOnUIThread(); + applied &= await operation.TryApplyAsync(workspace, originalSolution, progressTracker, cancellationToken).ConfigureAwait(true); + } + + return applied; + } + + private async Task TryNavigateToLocationOrStartRenameSessionAsync( + Workspace workspace, + ImmutableArray operations, + Solution oldSolution, + Solution newSolution, + CancellationToken cancellationToken) + { + var navigationOperation = operations.OfType().FirstOrDefault(); + if (navigationOperation != null && workspace.CanOpenDocuments) + { + var navigationService = workspace.Services.GetRequiredService(); + await navigationService.TryNavigateToPositionAsync( + this._threadingContext, workspace, navigationOperation.DocumentId, navigationOperation.Position, cancellationToken).ConfigureAwait(false); + return; } - private async Task TryNavigateToLocationOrStartRenameSessionAsync( - Workspace workspace, - ImmutableArray operations, - Solution oldSolution, - Solution newSolution, - CancellationToken cancellationToken) + var renameOperation = operations.OfType().FirstOrDefault(); + if (renameOperation != null && workspace.CanOpenDocuments) { - var navigationOperation = operations.OfType().FirstOrDefault(); - if (navigationOperation != null && workspace.CanOpenDocuments) + var navigationService = workspace.Services.GetRequiredService(); + if (await navigationService.TryNavigateToPositionAsync( + this._threadingContext, workspace, renameOperation.DocumentId, renameOperation.Position, cancellationToken).ConfigureAwait(true)) { - var navigationService = workspace.Services.GetRequiredService(); - await navigationService.TryNavigateToPositionAsync( - this._threadingContext, workspace, navigationOperation.DocumentId, navigationOperation.Position, cancellationToken).ConfigureAwait(false); + var openDocument = workspace.CurrentSolution.GetRequiredDocument(renameOperation.DocumentId); + _renameService.StartInlineSession(openDocument, new TextSpan(renameOperation.Position, 0), cancellationToken); return; } + } + + var changedDocuments = newSolution.GetChangedDocuments(oldSolution); + foreach (var documentId in changedDocuments) + { + var document = newSolution.GetRequiredDocument(documentId); + if (!document.SupportsSyntaxTree) + continue; - var renameOperation = operations.OfType().FirstOrDefault(); - if (renameOperation != null && workspace.CanOpenDocuments) + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var navigationToken = root.GetAnnotatedTokens(NavigationAnnotation.Kind).FirstOrNull(); + if (navigationToken.HasValue) { var navigationService = workspace.Services.GetRequiredService(); - if (await navigationService.TryNavigateToPositionAsync( - this._threadingContext, workspace, renameOperation.DocumentId, renameOperation.Position, cancellationToken).ConfigureAwait(true)) - { - var openDocument = workspace.CurrentSolution.GetRequiredDocument(renameOperation.DocumentId); - _renameService.StartInlineSession(openDocument, new TextSpan(renameOperation.Position, 0), cancellationToken); - return; - } + await navigationService.TryNavigateToPositionAsync( + this._threadingContext, workspace, documentId, navigationToken.Value.SpanStart, cancellationToken).ConfigureAwait(false); + return; } - var changedDocuments = newSolution.GetChangedDocuments(oldSolution); - foreach (var documentId in changedDocuments) + var renameToken = root.GetAnnotatedTokens(RenameAnnotation.Kind).FirstOrNull(); + if (renameToken.HasValue) { - var document = newSolution.GetRequiredDocument(documentId); - if (!document.SupportsSyntaxTree) - continue; - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - var navigationToken = root.GetAnnotatedTokens(NavigationAnnotation.Kind).FirstOrNull(); - if (navigationToken.HasValue) - { - var navigationService = workspace.Services.GetRequiredService(); - await navigationService.TryNavigateToPositionAsync( - this._threadingContext, workspace, documentId, navigationToken.Value.SpanStart, cancellationToken).ConfigureAwait(false); - return; - } - - var renameToken = root.GetAnnotatedTokens(RenameAnnotation.Kind).FirstOrNull(); - if (renameToken.HasValue) + // It's possible that the workspace's current solution is not the same as + // newSolution. This can happen if the workspace host performs other edits + // during ApplyChanges, such as in the Venus scenario where indentation and + // formatting can happen. To work around this, we create a SyntaxPath to the + // rename token in the newSolution and resolve it to the current solution. + + var pathToRenameToken = new SyntaxPath(renameToken.Value); + var latestDocument = workspace.CurrentSolution.GetDocument(documentId); + if (latestDocument != null) { - // It's possible that the workspace's current solution is not the same as - // newSolution. This can happen if the workspace host performs other edits - // during ApplyChanges, such as in the Venus scenario where indentation and - // formatting can happen. To work around this, we create a SyntaxPath to the - // rename token in the newSolution and resolve it to the current solution. - - var pathToRenameToken = new SyntaxPath(renameToken.Value); - var latestDocument = workspace.CurrentSolution.GetDocument(documentId); - if (latestDocument != null) + var latestRoot = await latestDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (pathToRenameToken.TryResolve(latestRoot, out var resolvedRenameToken) && + resolvedRenameToken.IsToken) { - var latestRoot = await latestDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (pathToRenameToken.TryResolve(latestRoot, out var resolvedRenameToken) && - resolvedRenameToken.IsToken) - { - var editorWorkspace = workspace; - var navigationService = editorWorkspace.Services.GetRequiredService(); + var editorWorkspace = workspace; + var navigationService = editorWorkspace.Services.GetRequiredService(); - if (await navigationService.TryNavigateToSpanAsync( - this._threadingContext, editorWorkspace, documentId, resolvedRenameToken.Span, cancellationToken).ConfigureAwait(false)) - { - var openDocument = workspace.CurrentSolution.GetRequiredDocument(documentId); - var openRoot = await openDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (await navigationService.TryNavigateToSpanAsync( + this._threadingContext, editorWorkspace, documentId, resolvedRenameToken.Span, cancellationToken).ConfigureAwait(false)) + { + var openDocument = workspace.CurrentSolution.GetRequiredDocument(documentId); + var openRoot = await openDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // NOTE: We need to resolve the syntax path again in case VB line commit kicked in - // due to the navigation. + // NOTE: We need to resolve the syntax path again in case VB line commit kicked in + // due to the navigation. - // TODO(DustinCa): We still have a potential problem here with VB line commit, - // because it can insert tokens and all sorts of other business, which could - // wind up with us not being able to resolve the token. - if (pathToRenameToken.TryResolve(openRoot, out resolvedRenameToken) && - resolvedRenameToken.IsToken) + // TODO(DustinCa): We still have a potential problem here with VB line commit, + // because it can insert tokens and all sorts of other business, which could + // wind up with us not being able to resolve the token. + if (pathToRenameToken.TryResolve(openRoot, out resolvedRenameToken) && + resolvedRenameToken.IsToken) + { + var text = await openDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var snapshot = text.FindCorrespondingEditorTextSnapshot(); + if (snapshot != null) { - var text = await openDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var snapshot = text.FindCorrespondingEditorTextSnapshot(); - if (snapshot != null) - { - await this._threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - _renameService.StartInlineSession(openDocument, resolvedRenameToken.AsToken().Span, cancellationToken); - } + await this._threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + _renameService.StartInlineSession(openDocument, resolvedRenameToken.AsToken().Span, cancellationToken); } } } } - - return; } + + return; } } } diff --git a/src/EditorFeatures/Core/CodeActions/ICodeActionEditHandlerService.cs b/src/EditorFeatures/Core/CodeActions/ICodeActionEditHandlerService.cs index e6073b0bbfd0e..633998cb9b086 100644 --- a/src/EditorFeatures/Core/CodeActions/ICodeActionEditHandlerService.cs +++ b/src/EditorFeatures/Core/CodeActions/ICodeActionEditHandlerService.cs @@ -8,22 +8,21 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor; -namespace Microsoft.CodeAnalysis.CodeActions +namespace Microsoft.CodeAnalysis.CodeActions; + +internal interface ICodeActionEditHandlerService { - internal interface ICodeActionEditHandlerService - { - ITextBufferAssociatedViewService AssociatedViewService { get; } + ITextBufferAssociatedViewService AssociatedViewService { get; } - Task GetPreviewsAsync( - Workspace workspace, ImmutableArray operations, CancellationToken cancellationToken); + Task GetPreviewsAsync( + Workspace workspace, ImmutableArray operations, CancellationToken cancellationToken); - Task ApplyAsync( - Workspace workspace, - Solution originalSolution, - Document? fromDocument, - ImmutableArray operations, - string title, - IProgress progressTracker, - CancellationToken cancellationToken); - } + Task ApplyAsync( + Workspace workspace, + Solution originalSolution, + Document? fromDocument, + ImmutableArray operations, + string title, + IProgress progressTracker, + CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/CodeRefactorings/EditorLayerCodeActionHelpersService.cs b/src/EditorFeatures/Core/CodeRefactorings/EditorLayerCodeActionHelpersService.cs index 5c8c8cf5503d3..8377489ee15b9 100644 --- a/src/EditorFeatures/Core/CodeRefactorings/EditorLayerCodeActionHelpersService.cs +++ b/src/EditorFeatures/Core/CodeRefactorings/EditorLayerCodeActionHelpersService.cs @@ -10,28 +10,27 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +[ExportWorkspaceServiceFactory(typeof(ICodeRefactoringHelpersService), ServiceLayer.Editor), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class EditorLayerCodeActionHelpersService(IInlineRenameService renameService) : IWorkspaceServiceFactory { - [ExportWorkspaceServiceFactory(typeof(ICodeRefactoringHelpersService), ServiceLayer.Editor), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class EditorLayerCodeActionHelpersService(IInlineRenameService renameService) : IWorkspaceServiceFactory - { - private readonly IInlineRenameService _renameService = renameService; + private readonly IInlineRenameService _renameService = renameService; - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new CodeActionHelpersService(this); + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => new CodeActionHelpersService(this); - private class CodeActionHelpersService(EditorLayerCodeActionHelpersService service) : ICodeRefactoringHelpersService - { - private readonly EditorLayerCodeActionHelpersService _service = service; + private class CodeActionHelpersService(EditorLayerCodeActionHelpersService service) : ICodeRefactoringHelpersService + { + private readonly EditorLayerCodeActionHelpersService _service = service; - public bool ActiveInlineRenameSession + public bool ActiveInlineRenameSession + { + get { - get - { - return _service._renameService.ActiveSession != null; - } + return _service._renameService.ActiveSession != null; } } } diff --git a/src/EditorFeatures/Core/Commanding/LegacyCommandHandlerServiceFactory.cs b/src/EditorFeatures/Core/Commanding/LegacyCommandHandlerServiceFactory.cs index ffcde7835e854..c8344eed699a6 100644 --- a/src/EditorFeatures/Core/Commanding/LegacyCommandHandlerServiceFactory.cs +++ b/src/EditorFeatures/Core/Commanding/LegacyCommandHandlerServiceFactory.cs @@ -8,16 +8,15 @@ using System.ComponentModel.Composition; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.Editor.Implementation.Commanding +namespace Microsoft.CodeAnalysis.Editor.Implementation.Commanding; + +[Obsolete("This is a compatibility shim for TypeScript; please do not use it.")] +[Export(typeof(ICommandHandlerServiceFactory))] +internal sealed class LegacyCommandHandlerServiceFactory : ICommandHandlerServiceFactory { - [Obsolete("This is a compatibility shim for TypeScript; please do not use it.")] - [Export(typeof(ICommandHandlerServiceFactory))] - internal sealed class LegacyCommandHandlerServiceFactory : ICommandHandlerServiceFactory + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LegacyCommandHandlerServiceFactory() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public LegacyCommandHandlerServiceFactory() - { - } } } diff --git a/src/EditorFeatures/Core/CommentSelection/AbstractCommentSelectionBase.cs b/src/EditorFeatures/Core/CommentSelection/AbstractCommentSelectionBase.cs index b0d0434fa6521..785c8d8e8eb64 100644 --- a/src/EditorFeatures/Core/CommentSelection/AbstractCommentSelectionBase.cs +++ b/src/EditorFeatures/Core/CommentSelection/AbstractCommentSelectionBase.cs @@ -24,200 +24,199 @@ using Microsoft.VisualStudio.Text.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CommentSelection +namespace Microsoft.CodeAnalysis.CommentSelection; + +internal enum Operation +{ + /// + /// The operation is a comment action. + /// + Comment, + + /// + /// The operation is an uncomment action. + /// + Uncomment +} + +internal abstract class AbstractCommentSelectionBase { - internal enum Operation + protected const string LanguageNameString = "languagename"; + protected const string LengthString = "length"; + + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; + private readonly EditorOptionsService _editorOptionsService; + + internal AbstractCommentSelectionBase( + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService) { - /// - /// The operation is a comment action. - /// - Comment, - - /// - /// The operation is an uncomment action. - /// - Uncomment + Contract.ThrowIfNull(undoHistoryRegistry); + Contract.ThrowIfNull(editorOperationsFactoryService); + + _undoHistoryRegistry = undoHistoryRegistry; + _editorOperationsFactoryService = editorOperationsFactoryService; + _editorOptionsService = editorOptionsService; } - internal abstract class AbstractCommentSelectionBase - { - protected const string LanguageNameString = "languagename"; - protected const string LengthString = "length"; + public abstract string DisplayName { get; } - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; - private readonly EditorOptionsService _editorOptionsService; + protected abstract string GetTitle(TCommand command); - internal AbstractCommentSelectionBase( - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService) - { - Contract.ThrowIfNull(undoHistoryRegistry); - Contract.ThrowIfNull(editorOperationsFactoryService); + protected abstract string GetMessage(TCommand command); - _undoHistoryRegistry = undoHistoryRegistry; - _editorOperationsFactoryService = editorOperationsFactoryService; - _editorOptionsService = editorOptionsService; - } + // Internal as tests currently rely on this method. + internal abstract CommentSelectionResult CollectEdits( + Document document, ICommentSelectionService service, ITextBuffer textBuffer, NormalizedSnapshotSpanCollection selectedSpans, + TCommand command, CancellationToken cancellationToken); - public abstract string DisplayName { get; } + protected static CommandState GetCommandState(ITextBuffer buffer) + { + return buffer.CanApplyChangeDocumentToWorkspace() + ? CommandState.Available + : CommandState.Unspecified; + } - protected abstract string GetTitle(TCommand command); + protected static void InsertText(ArrayBuilder textChanges, int position, string text) + => textChanges.Add(new TextChange(new TextSpan(position, 0), text)); - protected abstract string GetMessage(TCommand command); + protected static void DeleteText(ArrayBuilder textChanges, TextSpan span) + => textChanges.Add(new TextChange(span, string.Empty)); - // Internal as tests currently rely on this method. - internal abstract CommentSelectionResult CollectEdits( - Document document, ICommentSelectionService service, ITextBuffer textBuffer, NormalizedSnapshotSpanCollection selectedSpans, - TCommand command, CancellationToken cancellationToken); + internal bool ExecuteCommand(ITextView textView, ITextBuffer subjectBuffer, TCommand command, CommandExecutionContext context) + { + var title = GetTitle(command); + var message = GetMessage(command); - protected static CommandState GetCommandState(ITextBuffer buffer) + using (context.OperationContext.AddScope(allowCancellation: true, message)) { - return buffer.CanApplyChangeDocumentToWorkspace() - ? CommandState.Available - : CommandState.Unspecified; - } - - protected static void InsertText(ArrayBuilder textChanges, int position, string text) - => textChanges.Add(new TextChange(new TextSpan(position, 0), text)); + var cancellationToken = context.OperationContext.UserCancellationToken; - protected static void DeleteText(ArrayBuilder textChanges, TextSpan span) - => textChanges.Add(new TextChange(span, string.Empty)); + var selectedSpans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); + if (selectedSpans.IsEmpty()) + { + return true; + } - internal bool ExecuteCommand(ITextView textView, ITextBuffer subjectBuffer, TCommand command, CommandExecutionContext context) - { - var title = GetTitle(command); - var message = GetMessage(command); + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + { + return true; + } - using (context.OperationContext.AddScope(allowCancellation: true, message)) + var service = document.GetLanguageService(); + if (service == null) { - var cancellationToken = context.OperationContext.UserCancellationToken; - - var selectedSpans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); - if (selectedSpans.IsEmpty()) - { - return true; - } - - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return true; - } - - var service = document.GetLanguageService(); - if (service == null) - { - return true; - } - - var edits = CollectEdits(document, service, subjectBuffer, selectedSpans, command, cancellationToken); - ApplyEdits(document, textView, subjectBuffer, title, edits, cancellationToken); + return true; } - return true; + var edits = CollectEdits(document, service, subjectBuffer, selectedSpans, command, cancellationToken); + ApplyEdits(document, textView, subjectBuffer, title, edits, cancellationToken); } - /// - /// Applies the requested edits and sets the selection. - /// This operation is not cancellable. - /// - private void ApplyEdits(Document document, ITextView textView, ITextBuffer subjectBuffer, string title, CommentSelectionResult edits, CancellationToken cancellationToken) - { - var originalSnapshot = subjectBuffer.CurrentSnapshot; + return true; + } - // Apply the text changes. - using (var transaction = new CaretPreservingEditTransaction(title, textView, _undoHistoryRegistry, _editorOperationsFactoryService)) - { - subjectBuffer.ApplyChanges(edits.TextChanges); - transaction.Complete(); - } + /// + /// Applies the requested edits and sets the selection. + /// This operation is not cancellable. + /// + private void ApplyEdits(Document document, ITextView textView, ITextBuffer subjectBuffer, string title, CommentSelectionResult edits, CancellationToken cancellationToken) + { + var originalSnapshot = subjectBuffer.CurrentSnapshot; - if (edits.TrackingSpans.Any()) - { - // Create tracking spans to track the text changes. - var trackingSpans = edits.TrackingSpans - .SelectAsArray(textSpan => (originalSpan: textSpan, trackingSpan: CreateTrackingSpan(edits.ResultOperation, originalSnapshot, textSpan.TrackingTextSpan))); + // Apply the text changes. + using (var transaction = new CaretPreservingEditTransaction(title, textView, _undoHistoryRegistry, _editorOperationsFactoryService)) + { + subjectBuffer.ApplyChanges(edits.TextChanges); + transaction.Complete(); + } - // Convert the tracking spans into snapshot spans for formatting and selection. - var trackingSnapshotSpans = trackingSpans.Select(s => CreateSnapshotSpan(subjectBuffer.CurrentSnapshot, s.trackingSpan, s.originalSpan)); + if (edits.TrackingSpans.Any()) + { + // Create tracking spans to track the text changes. + var trackingSpans = edits.TrackingSpans + .SelectAsArray(textSpan => (originalSpan: textSpan, trackingSpan: CreateTrackingSpan(edits.ResultOperation, originalSnapshot, textSpan.TrackingTextSpan))); - if (edits.ResultOperation == Operation.Uncomment && document.SupportsSyntaxTree) - { - // Format the document only during uncomment operations. Use second transaction so it can be undone. - using var transaction = new CaretPreservingEditTransaction(title, textView, _undoHistoryRegistry, _editorOperationsFactoryService); + // Convert the tracking spans into snapshot spans for formatting and selection. + var trackingSnapshotSpans = trackingSpans.Select(s => CreateSnapshotSpan(subjectBuffer.CurrentSnapshot, s.trackingSpan, s.originalSpan)); - var newText = subjectBuffer.CurrentSnapshot.AsText(); - var oldSyntaxTree = document.GetSyntaxTreeSynchronously(cancellationToken); - var newRoot = oldSyntaxTree.WithChangedText(newText).GetRoot(cancellationToken); + if (edits.ResultOperation == Operation.Uncomment && document.SupportsSyntaxTree) + { + // Format the document only during uncomment operations. Use second transaction so it can be undone. + using var transaction = new CaretPreservingEditTransaction(title, textView, _undoHistoryRegistry, _editorOperationsFactoryService); - var formattingOptions = subjectBuffer.GetSyntaxFormattingOptions(_editorOptionsService, document.Project.Services, explicitFormat: false); - var formattingSpans = trackingSnapshotSpans.Select(change => CommonFormattingHelpers.GetFormattingSpan(newRoot, change.Span.ToTextSpan())); - var formattedChanges = Formatter.GetFormattedTextChanges(newRoot, formattingSpans, document.Project.Solution.Services, formattingOptions, rules: null, cancellationToken); + var newText = subjectBuffer.CurrentSnapshot.AsText(); + var oldSyntaxTree = document.GetSyntaxTreeSynchronously(cancellationToken); + var newRoot = oldSyntaxTree.WithChangedText(newText).GetRoot(cancellationToken); - subjectBuffer.ApplyChanges(formattedChanges); + var formattingOptions = subjectBuffer.GetSyntaxFormattingOptions(_editorOptionsService, document.Project.Services, explicitFormat: false); + var formattingSpans = trackingSnapshotSpans.Select(change => CommonFormattingHelpers.GetFormattingSpan(newRoot, change.Span.ToTextSpan())); + var formattedChanges = Formatter.GetFormattedTextChanges(newRoot, formattingSpans, document.Project.Solution.Services, formattingOptions, rules: null, cancellationToken); - transaction.Complete(); - } + subjectBuffer.ApplyChanges(formattedChanges); - // Set the multi selection after edits have been applied. - textView.SetMultiSelection(trackingSnapshotSpans); + transaction.Complete(); } - } - /// - /// Creates a tracking span for the operation. - /// Internal for tests. - /// - internal static ITrackingSpan CreateTrackingSpan(Operation operation, ITextSnapshot snapshot, TextSpan textSpan) - { - // If a comment is being added, the tracking span must include changes at the edge. - var spanTrackingMode = operation == Operation.Comment - ? SpanTrackingMode.EdgeInclusive - : SpanTrackingMode.EdgeExclusive; - return snapshot.CreateTrackingSpan(Span.FromBounds(textSpan.Start, textSpan.End), spanTrackingMode); + // Set the multi selection after edits have been applied. + textView.SetMultiSelection(trackingSnapshotSpans); } + } - /// - /// Retrieves the snapshot span from a post edited tracking span. - /// Additionally applies any extra modifications to the tracking span post edit. - /// - private static SnapshotSpan CreateSnapshotSpan(ITextSnapshot snapshot, ITrackingSpan trackingSpan, CommentTrackingSpan originalSpan) + /// + /// Creates a tracking span for the operation. + /// Internal for tests. + /// + internal static ITrackingSpan CreateTrackingSpan(Operation operation, ITextSnapshot snapshot, TextSpan textSpan) + { + // If a comment is being added, the tracking span must include changes at the edge. + var spanTrackingMode = operation == Operation.Comment + ? SpanTrackingMode.EdgeInclusive + : SpanTrackingMode.EdgeExclusive; + return snapshot.CreateTrackingSpan(Span.FromBounds(textSpan.Start, textSpan.End), spanTrackingMode); + } + + /// + /// Retrieves the snapshot span from a post edited tracking span. + /// Additionally applies any extra modifications to the tracking span post edit. + /// + private static SnapshotSpan CreateSnapshotSpan(ITextSnapshot snapshot, ITrackingSpan trackingSpan, CommentTrackingSpan originalSpan) + { + var snapshotSpan = trackingSpan.GetSpan(snapshot); + if (originalSpan.HasPostApplyChanges()) { - var snapshotSpan = trackingSpan.GetSpan(snapshot); - if (originalSpan.HasPostApplyChanges()) + var updatedStart = snapshotSpan.Start.Position + originalSpan.AmountToAddToTrackingSpanStart; + var updatedEnd = snapshotSpan.End.Position + originalSpan.AmountToAddToTrackingSpanEnd; + if (updatedStart >= snapshotSpan.Start.Position && updatedEnd <= snapshotSpan.End.Position) { - var updatedStart = snapshotSpan.Start.Position + originalSpan.AmountToAddToTrackingSpanStart; - var updatedEnd = snapshotSpan.End.Position + originalSpan.AmountToAddToTrackingSpanEnd; - if (updatedStart >= snapshotSpan.Start.Position && updatedEnd <= snapshotSpan.End.Position) - { - snapshotSpan = new SnapshotSpan(snapshot, Span.FromBounds(updatedStart, updatedEnd)); - } + snapshotSpan = new SnapshotSpan(snapshot, Span.FromBounds(updatedStart, updatedEnd)); } - - return snapshotSpan; } - /// - /// Given a set of lines, find the minimum indent of all of the non-blank, non-whitespace lines. - /// - protected static int DetermineSmallestIndent( - SnapshotSpan span, ITextSnapshotLine firstLine, ITextSnapshotLine lastLine) - { - // TODO: This breaks if you have mixed tabs/spaces, and/or tabsize != indentsize. - var indentToCommentAt = int.MaxValue; - for (var lineNumber = firstLine.LineNumber; lineNumber <= lastLine.LineNumber; ++lineNumber) - { - var line = span.Snapshot.GetLineFromLineNumber(lineNumber); - var firstNonWhitespacePosition = line.GetFirstNonWhitespacePosition(); - var firstNonWhitespaceOnLine = firstNonWhitespacePosition.HasValue - ? firstNonWhitespacePosition.Value - line.Start - : int.MaxValue; - indentToCommentAt = Math.Min(indentToCommentAt, firstNonWhitespaceOnLine); - } + return snapshotSpan; + } - return indentToCommentAt; + /// + /// Given a set of lines, find the minimum indent of all of the non-blank, non-whitespace lines. + /// + protected static int DetermineSmallestIndent( + SnapshotSpan span, ITextSnapshotLine firstLine, ITextSnapshotLine lastLine) + { + // TODO: This breaks if you have mixed tabs/spaces, and/or tabsize != indentsize. + var indentToCommentAt = int.MaxValue; + for (var lineNumber = firstLine.LineNumber; lineNumber <= lastLine.LineNumber; ++lineNumber) + { + var line = span.Snapshot.GetLineFromLineNumber(lineNumber); + var firstNonWhitespacePosition = line.GetFirstNonWhitespacePosition(); + var firstNonWhitespaceOnLine = firstNonWhitespacePosition.HasValue + ? firstNonWhitespacePosition.Value - line.Start + : int.MaxValue; + indentToCommentAt = Math.Min(indentToCommentAt, firstNonWhitespaceOnLine); } + + return indentToCommentAt; } } diff --git a/src/EditorFeatures/Core/CommentSelection/AbstractToggleBlockCommentBase.cs b/src/EditorFeatures/Core/CommentSelection/AbstractToggleBlockCommentBase.cs index 864a873e7b54d..c8c5859d075d5 100644 --- a/src/EditorFeatures/Core/CommentSelection/AbstractToggleBlockCommentBase.cs +++ b/src/EditorFeatures/Core/CommentSelection/AbstractToggleBlockCommentBase.cs @@ -24,420 +24,419 @@ using Microsoft.VisualStudio.Text.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CommentSelection +namespace Microsoft.CodeAnalysis.CommentSelection; + +internal abstract class AbstractToggleBlockCommentBase : + // Value tuple to represent that there is no distinct command to be passed in. + AbstractCommentSelectionBase, + ICommandHandler { - internal abstract class AbstractToggleBlockCommentBase : - // Value tuple to represent that there is no distinct command to be passed in. - AbstractCommentSelectionBase, - ICommandHandler - { - private static readonly CommentSelectionResult s_emptyCommentSelectionResult = - new([], [], Operation.Uncomment); + private static readonly CommentSelectionResult s_emptyCommentSelectionResult = + new([], [], Operation.Uncomment); - private readonly ITextStructureNavigatorSelectorService _navigatorSelectorService; + private readonly ITextStructureNavigatorSelectorService _navigatorSelectorService; - internal AbstractToggleBlockCommentBase( - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - ITextStructureNavigatorSelectorService navigatorSelectorService, - EditorOptionsService editorOptionsService) - : base(undoHistoryRegistry, editorOperationsFactoryService, editorOptionsService) - { - _navigatorSelectorService = navigatorSelectorService; - } + internal AbstractToggleBlockCommentBase( + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + ITextStructureNavigatorSelectorService navigatorSelectorService, + EditorOptionsService editorOptionsService) + : base(undoHistoryRegistry, editorOperationsFactoryService, editorOptionsService) + { + _navigatorSelectorService = navigatorSelectorService; + } - /// - /// Retrieves data about the commented spans near the selection. - /// - /// the current document. - /// the current text snapshot. - /// - /// a span that contains text from the first character of the first line in the selection(s) - /// until the last character of the last line in the selection(s) - /// - /// the comment information for the document. - /// any commented spans relevant to the selection in the document. - protected abstract ImmutableArray GetBlockCommentsInDocument(Document document, ITextSnapshot snapshot, - TextSpan linesContainingSelections, CommentSelectionInfo commentInfo, CancellationToken cancellationToken); + /// + /// Retrieves data about the commented spans near the selection. + /// + /// the current document. + /// the current text snapshot. + /// + /// a span that contains text from the first character of the first line in the selection(s) + /// until the last character of the last line in the selection(s) + /// + /// the comment information for the document. + /// any commented spans relevant to the selection in the document. + protected abstract ImmutableArray GetBlockCommentsInDocument(Document document, ITextSnapshot snapshot, + TextSpan linesContainingSelections, CommentSelectionInfo commentInfo, CancellationToken cancellationToken); - public CommandState GetCommandState(ToggleBlockCommentCommandArgs args) - => GetCommandState(args.SubjectBuffer); + public CommandState GetCommandState(ToggleBlockCommentCommandArgs args) + => GetCommandState(args.SubjectBuffer); - public bool ExecuteCommand(ToggleBlockCommentCommandArgs args, CommandExecutionContext context) - => ExecuteCommand(args.TextView, args.SubjectBuffer, ValueTuple.Create(), context); + public bool ExecuteCommand(ToggleBlockCommentCommandArgs args, CommandExecutionContext context) + => ExecuteCommand(args.TextView, args.SubjectBuffer, ValueTuple.Create(), context); - public override string DisplayName => EditorFeaturesResources.Toggle_Block_Comment; + public override string DisplayName => EditorFeaturesResources.Toggle_Block_Comment; - protected override string GetTitle(ValueTuple command) => EditorFeaturesResources.Toggle_Block_Comment; + protected override string GetTitle(ValueTuple command) => EditorFeaturesResources.Toggle_Block_Comment; - protected override string GetMessage(ValueTuple command) => EditorFeaturesResources.Toggling_block_comment; + protected override string GetMessage(ValueTuple command) => EditorFeaturesResources.Toggling_block_comment; - internal override CommentSelectionResult CollectEdits(Document document, ICommentSelectionService service, - ITextBuffer subjectBuffer, NormalizedSnapshotSpanCollection selectedSpans, ValueTuple command, CancellationToken cancellationToken) + internal override CommentSelectionResult CollectEdits(Document document, ICommentSelectionService service, + ITextBuffer subjectBuffer, NormalizedSnapshotSpanCollection selectedSpans, ValueTuple command, CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.CommandHandler_ToggleBlockComment, KeyValueLogMessage.Create(LogType.UserAction, m => { - using (Logger.LogBlock(FunctionId.CommandHandler_ToggleBlockComment, KeyValueLogMessage.Create(LogType.UserAction, m => - { - m[LanguageNameString] = document.Project.Language; - m[LengthString] = subjectBuffer.CurrentSnapshot.Length; - }), cancellationToken)) - { - var navigator = _navigatorSelectorService.GetTextStructureNavigator(subjectBuffer); - - var commentInfo = service.GetInfo(); - if (commentInfo.SupportsBlockComment) - { - return ToggleBlockComments(document, commentInfo, navigator, selectedSpans, cancellationToken); - } + m[LanguageNameString] = document.Project.Language; + m[LengthString] = subjectBuffer.CurrentSnapshot.Length; + }), cancellationToken)) + { + var navigator = _navigatorSelectorService.GetTextStructureNavigator(subjectBuffer); - return s_emptyCommentSelectionResult; + var commentInfo = service.GetInfo(); + if (commentInfo.SupportsBlockComment) + { + return ToggleBlockComments(document, commentInfo, navigator, selectedSpans, cancellationToken); } - } - private CommentSelectionResult ToggleBlockComments(Document document, CommentSelectionInfo commentInfo, - ITextStructureNavigator navigator, NormalizedSnapshotSpanCollection selectedSpans, CancellationToken cancellationToken) - { - var firstLineAroundSelection = selectedSpans.First().Start.GetContainingLine().Start; - var lastLineAroundSelection = selectedSpans.Last().End.GetContainingLine().End; - var linesContainingSelection = TextSpan.FromBounds(firstLineAroundSelection, lastLineAroundSelection); - var blockCommentedSpans = GetBlockCommentsInDocument( - document, selectedSpans.First().Snapshot, linesContainingSelection, commentInfo, cancellationToken); + return s_emptyCommentSelectionResult; + } + } - var blockCommentSelections = selectedSpans.SelectAsArray(span => new BlockCommentSelectionHelper(blockCommentedSpans, span)); + private CommentSelectionResult ToggleBlockComments(Document document, CommentSelectionInfo commentInfo, + ITextStructureNavigator navigator, NormalizedSnapshotSpanCollection selectedSpans, CancellationToken cancellationToken) + { + var firstLineAroundSelection = selectedSpans.First().Start.GetContainingLine().Start; + var lastLineAroundSelection = selectedSpans.Last().End.GetContainingLine().End; + var linesContainingSelection = TextSpan.FromBounds(firstLineAroundSelection, lastLineAroundSelection); + var blockCommentedSpans = GetBlockCommentsInDocument( + document, selectedSpans.First().Snapshot, linesContainingSelection, commentInfo, cancellationToken); - var returnOperation = Operation.Uncomment; + var blockCommentSelections = selectedSpans.SelectAsArray(span => new BlockCommentSelectionHelper(blockCommentedSpans, span)); - var textChanges = ArrayBuilder.GetInstance(); - var trackingSpans = ArrayBuilder.GetInstance(); - // Try to uncomment until an already uncommented span is found. - foreach (var blockCommentSelection in blockCommentSelections) - { - // If any selection does not have comments to remove, then the operation should be comment. - if (!TryUncommentBlockComment(blockCommentedSpans, blockCommentSelection, textChanges, trackingSpans, commentInfo)) - { - returnOperation = Operation.Comment; - break; - } - } + var returnOperation = Operation.Uncomment; - if (returnOperation == Operation.Comment) + var textChanges = ArrayBuilder.GetInstance(); + var trackingSpans = ArrayBuilder.GetInstance(); + // Try to uncomment until an already uncommented span is found. + foreach (var blockCommentSelection in blockCommentSelections) + { + // If any selection does not have comments to remove, then the operation should be comment. + if (!TryUncommentBlockComment(blockCommentedSpans, blockCommentSelection, textChanges, trackingSpans, commentInfo)) { - textChanges.Clear(); - trackingSpans.Clear(); - foreach (var blockCommentSelection in blockCommentSelections) - { - BlockCommentSpan(blockCommentSelection, navigator, textChanges, trackingSpans, commentInfo); - } + returnOperation = Operation.Comment; + break; } - - return new CommentSelectionResult(textChanges.ToArrayAndFree(), trackingSpans.ToArrayAndFree(), returnOperation); } - private static bool TryUncommentBlockComment(ImmutableArray blockCommentedSpans, - BlockCommentSelectionHelper blockCommentSelection, ArrayBuilder textChanges, - ArrayBuilder trackingSpans, CommentSelectionInfo commentInfo) + if (returnOperation == Operation.Comment) { - // If the selection is just a caret, try and uncomment blocks on the same line with only whitespace on the line. - if (blockCommentSelection.SelectedSpan.IsEmpty - && blockCommentSelection.TryGetBlockCommentOnSameLine(blockCommentedSpans, out var blockCommentOnSameLine)) - { - DeleteBlockComment(blockCommentSelection, blockCommentOnSameLine, textChanges, commentInfo); - trackingSpans.Add(new CommentTrackingSpan(blockCommentOnSameLine)); - return true; - } - // If the selection is entirely commented, remove any block comments that intersect. - else if (blockCommentSelection.IsEntirelyCommented()) - { - var intersectingBlockComments = blockCommentSelection.IntersectingBlockComments; - foreach (var spanToRemove in intersectingBlockComments) - { - DeleteBlockComment(blockCommentSelection, spanToRemove, textChanges, commentInfo); - } - - var trackingSpan = TextSpan.FromBounds(intersectingBlockComments.First().Start, intersectingBlockComments.Last().End); - trackingSpans.Add(new CommentTrackingSpan(trackingSpan)); - return true; - } - else + textChanges.Clear(); + trackingSpans.Clear(); + foreach (var blockCommentSelection in blockCommentSelections) { - return false; + BlockCommentSpan(blockCommentSelection, navigator, textChanges, trackingSpans, commentInfo); } } - private static void BlockCommentSpan(BlockCommentSelectionHelper blockCommentSelection, ITextStructureNavigator navigator, - ArrayBuilder textChanges, ArrayBuilder trackingSpans, CommentSelectionInfo commentInfo) + return new CommentSelectionResult(textChanges.ToArrayAndFree(), trackingSpans.ToArrayAndFree(), returnOperation); + } + + private static bool TryUncommentBlockComment(ImmutableArray blockCommentedSpans, + BlockCommentSelectionHelper blockCommentSelection, ArrayBuilder textChanges, + ArrayBuilder trackingSpans, CommentSelectionInfo commentInfo) + { + // If the selection is just a caret, try and uncomment blocks on the same line with only whitespace on the line. + if (blockCommentSelection.SelectedSpan.IsEmpty + && blockCommentSelection.TryGetBlockCommentOnSameLine(blockCommentedSpans, out var blockCommentOnSameLine)) + { + DeleteBlockComment(blockCommentSelection, blockCommentOnSameLine, textChanges, commentInfo); + trackingSpans.Add(new CommentTrackingSpan(blockCommentOnSameLine)); + return true; + } + // If the selection is entirely commented, remove any block comments that intersect. + else if (blockCommentSelection.IsEntirelyCommented()) { - // Add sequential block comments if the selection contains any intersecting comments. - if (blockCommentSelection.HasIntersectingBlockComments()) + var intersectingBlockComments = blockCommentSelection.IntersectingBlockComments; + foreach (var spanToRemove in intersectingBlockComments) { - AddBlockCommentWithIntersectingSpans(blockCommentSelection, textChanges, trackingSpans, commentInfo); + DeleteBlockComment(blockCommentSelection, spanToRemove, textChanges, commentInfo); } - else - { - // Comment the selected span or caret location. - var spanToAdd = blockCommentSelection.SelectedSpan; - if (spanToAdd.IsEmpty) - { - var caretLocation = GetCaretLocationAfterToken(navigator, blockCommentSelection); - spanToAdd = TextSpan.FromBounds(caretLocation, caretLocation); - } - trackingSpans.Add(new CommentTrackingSpan(spanToAdd)); - AddBlockComment(commentInfo, spanToAdd, textChanges); - } + var trackingSpan = TextSpan.FromBounds(intersectingBlockComments.First().Start, intersectingBlockComments.Last().End); + trackingSpans.Add(new CommentTrackingSpan(trackingSpan)); + return true; } - - /// - /// Returns a caret location of itself or the location after the token the caret is inside of. - /// - private static int GetCaretLocationAfterToken(ITextStructureNavigator navigator, BlockCommentSelectionHelper blockCommentSelection) + else { - var snapshotSpan = blockCommentSelection.SnapshotSpan; - if (navigator == null) - { - return snapshotSpan.Start; - } + return false; + } + } - var extent = navigator.GetExtentOfWord(snapshotSpan.Start); - int locationAfterToken = extent.Span.End; - // Don't move to the end if it's already before the token. - if (snapshotSpan.Start == extent.Span.Start) - { - locationAfterToken = extent.Span.Start; - } - // If the 'word' is just whitespace, use the selected location. - if (blockCommentSelection.IsSpanWhitespace(TextSpan.FromBounds(extent.Span.Start, extent.Span.End))) + private static void BlockCommentSpan(BlockCommentSelectionHelper blockCommentSelection, ITextStructureNavigator navigator, + ArrayBuilder textChanges, ArrayBuilder trackingSpans, CommentSelectionInfo commentInfo) + { + // Add sequential block comments if the selection contains any intersecting comments. + if (blockCommentSelection.HasIntersectingBlockComments()) + { + AddBlockCommentWithIntersectingSpans(blockCommentSelection, textChanges, trackingSpans, commentInfo); + } + else + { + // Comment the selected span or caret location. + var spanToAdd = blockCommentSelection.SelectedSpan; + if (spanToAdd.IsEmpty) { - locationAfterToken = snapshotSpan.Start; + var caretLocation = GetCaretLocationAfterToken(navigator, blockCommentSelection); + spanToAdd = TextSpan.FromBounds(caretLocation, caretLocation); } - return locationAfterToken; + trackingSpans.Add(new CommentTrackingSpan(spanToAdd)); + AddBlockComment(commentInfo, spanToAdd, textChanges); } + } - /// - /// Adds a block comment when the selection already contains block comment(s). - /// The result will be sequential block comments with the entire selection being commented out. - /// - private static void AddBlockCommentWithIntersectingSpans(BlockCommentSelectionHelper blockCommentSelection, - ArrayBuilder textChanges, ArrayBuilder trackingSpans, CommentSelectionInfo commentInfo) + /// + /// Returns a caret location of itself or the location after the token the caret is inside of. + /// + private static int GetCaretLocationAfterToken(ITextStructureNavigator navigator, BlockCommentSelectionHelper blockCommentSelection) + { + var snapshotSpan = blockCommentSelection.SnapshotSpan; + if (navigator == null) { - var selectedSpan = blockCommentSelection.SelectedSpan; + return snapshotSpan.Start; + } - var amountToAddToStart = 0; - var amountToAddToEnd = 0; + var extent = navigator.GetExtentOfWord(snapshotSpan.Start); + int locationAfterToken = extent.Span.End; + // Don't move to the end if it's already before the token. + if (snapshotSpan.Start == extent.Span.Start) + { + locationAfterToken = extent.Span.Start; + } + // If the 'word' is just whitespace, use the selected location. + if (blockCommentSelection.IsSpanWhitespace(TextSpan.FromBounds(extent.Span.Start, extent.Span.End))) + { + locationAfterToken = snapshotSpan.Start; + } - // Add comments to all uncommented spans in the selection. - foreach (var uncommentedSpan in blockCommentSelection.UncommentedSpansInSelection) - { - AddBlockComment(commentInfo, uncommentedSpan, textChanges); - } + return locationAfterToken; + } - var startsWithCommentMarker = blockCommentSelection.StartsWithAnyBlockCommentMarker(commentInfo); - var endsWithCommentMarker = blockCommentSelection.EndsWithAnyBlockCommentMarker(commentInfo); - // If the start is commented (and not a comment marker), close the current comment and open a new one. - if (blockCommentSelection.IsLocationCommented(selectedSpan.Start) && !startsWithCommentMarker) - { - InsertText(textChanges, selectedSpan.Start, commentInfo.BlockCommentEndString); - InsertText(textChanges, selectedSpan.Start, commentInfo.BlockCommentStartString); - // Shrink the tracking so the previous comment start marker is not included in selection. - amountToAddToStart = commentInfo.BlockCommentEndString.Length; - } + /// + /// Adds a block comment when the selection already contains block comment(s). + /// The result will be sequential block comments with the entire selection being commented out. + /// + private static void AddBlockCommentWithIntersectingSpans(BlockCommentSelectionHelper blockCommentSelection, + ArrayBuilder textChanges, ArrayBuilder trackingSpans, CommentSelectionInfo commentInfo) + { + var selectedSpan = blockCommentSelection.SelectedSpan; - // If the end is commented (and not a comment marker), close the current comment and open a new one. - if (blockCommentSelection.IsLocationCommented(selectedSpan.End) && !endsWithCommentMarker) - { - InsertText(textChanges, selectedSpan.End, commentInfo.BlockCommentEndString); - InsertText(textChanges, selectedSpan.End, commentInfo.BlockCommentStartString); - // Shrink the tracking span so the next comment start marker is not included in selection. - amountToAddToEnd = -commentInfo.BlockCommentStartString.Length; - } + var amountToAddToStart = 0; + var amountToAddToEnd = 0; - trackingSpans.Add(new CommentTrackingSpan(selectedSpan, amountToAddToStart, amountToAddToEnd)); + // Add comments to all uncommented spans in the selection. + foreach (var uncommentedSpan in blockCommentSelection.UncommentedSpansInSelection) + { + AddBlockComment(commentInfo, uncommentedSpan, textChanges); } - private static void AddBlockComment(CommentSelectionInfo commentInfo, TextSpan span, ArrayBuilder textChanges) + var startsWithCommentMarker = blockCommentSelection.StartsWithAnyBlockCommentMarker(commentInfo); + var endsWithCommentMarker = blockCommentSelection.EndsWithAnyBlockCommentMarker(commentInfo); + // If the start is commented (and not a comment marker), close the current comment and open a new one. + if (blockCommentSelection.IsLocationCommented(selectedSpan.Start) && !startsWithCommentMarker) { - InsertText(textChanges, span.Start, commentInfo.BlockCommentStartString); - InsertText(textChanges, span.End, commentInfo.BlockCommentEndString); + InsertText(textChanges, selectedSpan.Start, commentInfo.BlockCommentEndString); + InsertText(textChanges, selectedSpan.Start, commentInfo.BlockCommentStartString); + // Shrink the tracking so the previous comment start marker is not included in selection. + amountToAddToStart = commentInfo.BlockCommentEndString.Length; } - private static void DeleteBlockComment(BlockCommentSelectionHelper blockCommentSelection, TextSpan spanToRemove, - ArrayBuilder textChanges, CommentSelectionInfo commentInfo) + // If the end is commented (and not a comment marker), close the current comment and open a new one. + if (blockCommentSelection.IsLocationCommented(selectedSpan.End) && !endsWithCommentMarker) { - DeleteText(textChanges, new TextSpan(spanToRemove.Start, commentInfo.BlockCommentStartString.Length)); - var endMarkerPosition = spanToRemove.End - commentInfo.BlockCommentEndString.Length; - // Sometimes the block comment will be missing a close marker. - if (Equals(blockCommentSelection.GetSubstringFromText(endMarkerPosition, commentInfo.BlockCommentEndString.Length), - commentInfo.BlockCommentEndString)) - { - DeleteText(textChanges, new TextSpan(endMarkerPosition, commentInfo.BlockCommentEndString.Length)); - } + InsertText(textChanges, selectedSpan.End, commentInfo.BlockCommentEndString); + InsertText(textChanges, selectedSpan.End, commentInfo.BlockCommentStartString); + // Shrink the tracking span so the next comment start marker is not included in selection. + amountToAddToEnd = -commentInfo.BlockCommentStartString.Length; } - private class BlockCommentSelectionHelper + trackingSpans.Add(new CommentTrackingSpan(selectedSpan, amountToAddToStart, amountToAddToEnd)); + } + + private static void AddBlockComment(CommentSelectionInfo commentInfo, TextSpan span, ArrayBuilder textChanges) + { + InsertText(textChanges, span.Start, commentInfo.BlockCommentStartString); + InsertText(textChanges, span.End, commentInfo.BlockCommentEndString); + } + + private static void DeleteBlockComment(BlockCommentSelectionHelper blockCommentSelection, TextSpan spanToRemove, + ArrayBuilder textChanges, CommentSelectionInfo commentInfo) + { + DeleteText(textChanges, new TextSpan(spanToRemove.Start, commentInfo.BlockCommentStartString.Length)); + var endMarkerPosition = spanToRemove.End - commentInfo.BlockCommentEndString.Length; + // Sometimes the block comment will be missing a close marker. + if (Equals(blockCommentSelection.GetSubstringFromText(endMarkerPosition, commentInfo.BlockCommentEndString.Length), + commentInfo.BlockCommentEndString)) { - /// - /// Trimmed text of the selection. - /// - private readonly string _trimmedText; + DeleteText(textChanges, new TextSpan(endMarkerPosition, commentInfo.BlockCommentEndString.Length)); + } + } - public SnapshotSpan SnapshotSpan { get; } + private class BlockCommentSelectionHelper + { + /// + /// Trimmed text of the selection. + /// + private readonly string _trimmedText; - public TextSpan SelectedSpan { get; } + public SnapshotSpan SnapshotSpan { get; } - public ImmutableArray IntersectingBlockComments { get; } + public TextSpan SelectedSpan { get; } - public ImmutableArray UncommentedSpansInSelection { get; } + public ImmutableArray IntersectingBlockComments { get; } - public BlockCommentSelectionHelper(ImmutableArray allBlockComments, SnapshotSpan selectedSnapshotSpan) - { - _trimmedText = selectedSnapshotSpan.GetText().Trim(); - SnapshotSpan = selectedSnapshotSpan; + public ImmutableArray UncommentedSpansInSelection { get; } - SelectedSpan = TextSpan.FromBounds(selectedSnapshotSpan.Start, selectedSnapshotSpan.End); - IntersectingBlockComments = GetIntersectingBlockComments(allBlockComments, SelectedSpan); - UncommentedSpansInSelection = GetUncommentedSpansInSelection(); - } + public BlockCommentSelectionHelper(ImmutableArray allBlockComments, SnapshotSpan selectedSnapshotSpan) + { + _trimmedText = selectedSnapshotSpan.GetText().Trim(); + SnapshotSpan = selectedSnapshotSpan; - /// - /// Determines if the given span is entirely whitespace. - /// - public bool IsSpanWhitespace(TextSpan span) + SelectedSpan = TextSpan.FromBounds(selectedSnapshotSpan.Start, selectedSnapshotSpan.End); + IntersectingBlockComments = GetIntersectingBlockComments(allBlockComments, SelectedSpan); + UncommentedSpansInSelection = GetUncommentedSpansInSelection(); + } + + /// + /// Determines if the given span is entirely whitespace. + /// + public bool IsSpanWhitespace(TextSpan span) + { + for (var i = span.Start; i < span.End; i++) { - for (var i = span.Start; i < span.End; i++) + if (!char.IsWhiteSpace(SnapshotSpan.Snapshot[i])) { - if (!char.IsWhiteSpace(SnapshotSpan.Snapshot[i])) - { - return false; - } + return false; } - - return true; } - /// - /// Determines if the location falls inside a commented span. - /// - public bool IsLocationCommented(int location) - => IntersectingBlockComments.Contains(span => span.Contains(location)); - - /// - /// Checks if the selection already starts with a comment marker. - /// This prevents us from adding an extra marker. - /// - public bool StartsWithAnyBlockCommentMarker(CommentSelectionInfo commentInfo) - { - return _trimmedText.StartsWith(commentInfo.BlockCommentStartString, StringComparison.Ordinal) - || _trimmedText.StartsWith(commentInfo.BlockCommentEndString, StringComparison.Ordinal); - } + return true; + } - /// - /// Checks if the selection already ends with a comment marker. - /// This prevents us from adding an extra marker. - /// - public bool EndsWithAnyBlockCommentMarker(CommentSelectionInfo commentInfo) - { - return _trimmedText.EndsWith(commentInfo.BlockCommentStartString, StringComparison.Ordinal) - || _trimmedText.EndsWith(commentInfo.BlockCommentEndString, StringComparison.Ordinal); - } + /// + /// Determines if the location falls inside a commented span. + /// + public bool IsLocationCommented(int location) + => IntersectingBlockComments.Contains(span => span.Contains(location)); - /// - /// Checks if the selected span contains any uncommented non whitespace characters. - /// - public bool IsEntirelyCommented() - => !UncommentedSpansInSelection.Any() && HasIntersectingBlockComments(); - - /// - /// Returns if the selection intersects with any block comments. - /// - public bool HasIntersectingBlockComments() - => IntersectingBlockComments.Any(); - - public string GetSubstringFromText(int position, int length) - => SnapshotSpan.Snapshot.GetText().Substring(position, length); - - /// - /// Tries to get a block comment on the same line. There are two cases: - /// 1. The caret is preceding a block comment on the same line, with only whitespace before the comment. - /// 2. The caret is following a block comment on the same line, with only whitespace after the comment. - /// - public bool TryGetBlockCommentOnSameLine(ImmutableArray allBlockComments, out TextSpan commentedSpanOnSameLine) + /// + /// Checks if the selection already starts with a comment marker. + /// This prevents us from adding an extra marker. + /// + public bool StartsWithAnyBlockCommentMarker(CommentSelectionInfo commentInfo) + { + return _trimmedText.StartsWith(commentInfo.BlockCommentStartString, StringComparison.Ordinal) + || _trimmedText.StartsWith(commentInfo.BlockCommentEndString, StringComparison.Ordinal); + } + + /// + /// Checks if the selection already ends with a comment marker. + /// This prevents us from adding an extra marker. + /// + public bool EndsWithAnyBlockCommentMarker(CommentSelectionInfo commentInfo) + { + return _trimmedText.EndsWith(commentInfo.BlockCommentStartString, StringComparison.Ordinal) + || _trimmedText.EndsWith(commentInfo.BlockCommentEndString, StringComparison.Ordinal); + } + + /// + /// Checks if the selected span contains any uncommented non whitespace characters. + /// + public bool IsEntirelyCommented() + => !UncommentedSpansInSelection.Any() && HasIntersectingBlockComments(); + + /// + /// Returns if the selection intersects with any block comments. + /// + public bool HasIntersectingBlockComments() + => IntersectingBlockComments.Any(); + + public string GetSubstringFromText(int position, int length) + => SnapshotSpan.Snapshot.GetText().Substring(position, length); + + /// + /// Tries to get a block comment on the same line. There are two cases: + /// 1. The caret is preceding a block comment on the same line, with only whitespace before the comment. + /// 2. The caret is following a block comment on the same line, with only whitespace after the comment. + /// + public bool TryGetBlockCommentOnSameLine(ImmutableArray allBlockComments, out TextSpan commentedSpanOnSameLine) + { + var snapshot = SnapshotSpan.Snapshot; + var selectedLine = snapshot.GetLineFromPosition(SelectedSpan.Start); + var lineStartToCaretIsWhitespace = IsSpanWhitespace(TextSpan.FromBounds(selectedLine.Start, SelectedSpan.Start)); + var caretToLineEndIsWhitespace = IsSpanWhitespace(TextSpan.FromBounds(SelectedSpan.Start, selectedLine.End)); + foreach (var blockComment in allBlockComments) { - var snapshot = SnapshotSpan.Snapshot; - var selectedLine = snapshot.GetLineFromPosition(SelectedSpan.Start); - var lineStartToCaretIsWhitespace = IsSpanWhitespace(TextSpan.FromBounds(selectedLine.Start, SelectedSpan.Start)); - var caretToLineEndIsWhitespace = IsSpanWhitespace(TextSpan.FromBounds(SelectedSpan.Start, selectedLine.End)); - foreach (var blockComment in allBlockComments) + if (lineStartToCaretIsWhitespace + && SelectedSpan.Start < blockComment.Start + && snapshot.AreOnSameLine(SelectedSpan.Start, blockComment.Start)) { - if (lineStartToCaretIsWhitespace - && SelectedSpan.Start < blockComment.Start - && snapshot.AreOnSameLine(SelectedSpan.Start, blockComment.Start)) + if (IsSpanWhitespace(TextSpan.FromBounds(SelectedSpan.Start, blockComment.Start))) { - if (IsSpanWhitespace(TextSpan.FromBounds(SelectedSpan.Start, blockComment.Start))) - { - commentedSpanOnSameLine = blockComment; - return true; - } + commentedSpanOnSameLine = blockComment; + return true; } - else if (caretToLineEndIsWhitespace - && SelectedSpan.Start > blockComment.End - && snapshot.AreOnSameLine(SelectedSpan.Start, blockComment.End)) + } + else if (caretToLineEndIsWhitespace + && SelectedSpan.Start > blockComment.End + && snapshot.AreOnSameLine(SelectedSpan.Start, blockComment.End)) + { + if (IsSpanWhitespace(TextSpan.FromBounds(blockComment.End, SelectedSpan.Start))) { - if (IsSpanWhitespace(TextSpan.FromBounds(blockComment.End, SelectedSpan.Start))) - { - commentedSpanOnSameLine = blockComment; - return true; - } + commentedSpanOnSameLine = blockComment; + return true; } } - - commentedSpanOnSameLine = new TextSpan(); - return false; } - /// - /// Gets a list of block comments that intersect the span. - /// Spans are intersecting if 1 location is the same between them (empty spans look at the start). - /// - private static ImmutableArray GetIntersectingBlockComments(ImmutableArray allBlockComments, TextSpan span) - => allBlockComments.WhereAsArray(blockCommentSpan => span.OverlapsWith(blockCommentSpan) || blockCommentSpan.Contains(span)); - - /// - /// Retrieves all non commented, non whitespace spans. - /// - private ImmutableArray GetUncommentedSpansInSelection() - { - var uncommentedSpans = new List(); + commentedSpanOnSameLine = new TextSpan(); + return false; + } - // Invert the commented spans to get the uncommented spans. - var spanStart = SelectedSpan.Start; - foreach (var commentedSpan in IntersectingBlockComments) - { - if (commentedSpan.Start > spanStart) - { - // Get span up until the comment and check to make sure it is not whitespace. - var possibleUncommentedSpan = TextSpan.FromBounds(spanStart, commentedSpan.Start); - if (!IsSpanWhitespace(possibleUncommentedSpan)) - { - uncommentedSpans.Add(possibleUncommentedSpan); - } - } + /// + /// Gets a list of block comments that intersect the span. + /// Spans are intersecting if 1 location is the same between them (empty spans look at the start). + /// + private static ImmutableArray GetIntersectingBlockComments(ImmutableArray allBlockComments, TextSpan span) + => allBlockComments.WhereAsArray(blockCommentSpan => span.OverlapsWith(blockCommentSpan) || blockCommentSpan.Contains(span)); - // The next possible uncommented span starts at the end of this commented span. - spanStart = commentedSpan.End; - } + /// + /// Retrieves all non commented, non whitespace spans. + /// + private ImmutableArray GetUncommentedSpansInSelection() + { + var uncommentedSpans = new List(); - // If part of the selection is left over, it's not commented. Add if not whitespace. - if (spanStart < SelectedSpan.End) + // Invert the commented spans to get the uncommented spans. + var spanStart = SelectedSpan.Start; + foreach (var commentedSpan in IntersectingBlockComments) + { + if (commentedSpan.Start > spanStart) { - var uncommentedSpan = TextSpan.FromBounds(spanStart, SelectedSpan.End); - if (!IsSpanWhitespace(uncommentedSpan)) + // Get span up until the comment and check to make sure it is not whitespace. + var possibleUncommentedSpan = TextSpan.FromBounds(spanStart, commentedSpan.Start); + if (!IsSpanWhitespace(possibleUncommentedSpan)) { - uncommentedSpans.Add(uncommentedSpan); + uncommentedSpans.Add(possibleUncommentedSpan); } } - return uncommentedSpans.ToImmutableArray(); + // The next possible uncommented span starts at the end of this commented span. + spanStart = commentedSpan.End; } + + // If part of the selection is left over, it's not commented. Add if not whitespace. + if (spanStart < SelectedSpan.End) + { + var uncommentedSpan = TextSpan.FromBounds(spanStart, SelectedSpan.End); + if (!IsSpanWhitespace(uncommentedSpan)) + { + uncommentedSpans.Add(uncommentedSpan); + } + } + + return uncommentedSpans.ToImmutableArray(); } } } diff --git a/src/EditorFeatures/Core/CommentSelection/CommentSelectionResult.cs b/src/EditorFeatures/Core/CommentSelection/CommentSelectionResult.cs index 0f67055cd2bf9..f08e300202a96 100644 --- a/src/EditorFeatures/Core/CommentSelection/CommentSelectionResult.cs +++ b/src/EditorFeatures/Core/CommentSelection/CommentSelectionResult.cs @@ -8,23 +8,22 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CommentSelection +namespace Microsoft.CodeAnalysis.CommentSelection; + +internal readonly struct CommentSelectionResult(IEnumerable textChanges, IEnumerable trackingSpans, Operation resultOperation) { - internal readonly struct CommentSelectionResult(IEnumerable textChanges, IEnumerable trackingSpans, Operation resultOperation) - { - /// - /// Text changes to make for this operation. - /// - public ImmutableArray TextChanges { get; } = textChanges.ToImmutableArray(); - /// - /// Tracking spans used to format and set the output selection after edits. - /// - public ImmutableArray TrackingSpans { get; } = trackingSpans.ToImmutableArray(); - /// - /// The type of text changes being made. - /// This is known beforehand in some cases (comment selection) - /// and determined after as a result in others (toggle comment). - /// - public Operation ResultOperation { get; } = resultOperation; - } + /// + /// Text changes to make for this operation. + /// + public ImmutableArray TextChanges { get; } = textChanges.ToImmutableArray(); + /// + /// Tracking spans used to format and set the output selection after edits. + /// + public ImmutableArray TrackingSpans { get; } = trackingSpans.ToImmutableArray(); + /// + /// The type of text changes being made. + /// This is known beforehand in some cases (comment selection) + /// and determined after as a result in others (toggle comment). + /// + public Operation ResultOperation { get; } = resultOperation; } diff --git a/src/EditorFeatures/Core/CommentSelection/CommentTrackingSpan.cs b/src/EditorFeatures/Core/CommentSelection/CommentTrackingSpan.cs index 980a959796347..e97807eab7194 100644 --- a/src/EditorFeatures/Core/CommentSelection/CommentTrackingSpan.cs +++ b/src/EditorFeatures/Core/CommentSelection/CommentTrackingSpan.cs @@ -6,38 +6,37 @@ using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CommentSelection +namespace Microsoft.CodeAnalysis.CommentSelection; + +/// +/// Wrapper around a TextSpan that holds extra data used to create a tracking span. +/// +internal readonly struct CommentTrackingSpan { - /// - /// Wrapper around a TextSpan that holds extra data used to create a tracking span. - /// - internal readonly struct CommentTrackingSpan + public TextSpan TrackingTextSpan { get; } + + // In some cases, the tracking span needs to be adjusted by a specific amount after the changes have been applied. + // These fields store the amount to adjust the span by after edits have been applied. + // e.g. The selection begins in a comment - + // /*Com[|ment*/ int i = 1;|] -> /*Com*//*ment*/ int i = 1;*/ + // There are two new comment markers added inside the original comment, only the second should be selected. + public int AmountToAddToTrackingSpanStart { get; } + public int AmountToAddToTrackingSpanEnd { get; } + + public CommentTrackingSpan(TextSpan trackingTextSpan) { - public TextSpan TrackingTextSpan { get; } - - // In some cases, the tracking span needs to be adjusted by a specific amount after the changes have been applied. - // These fields store the amount to adjust the span by after edits have been applied. - // e.g. The selection begins in a comment - - // /*Com[|ment*/ int i = 1;|] -> /*Com*//*ment*/ int i = 1;*/ - // There are two new comment markers added inside the original comment, only the second should be selected. - public int AmountToAddToTrackingSpanStart { get; } - public int AmountToAddToTrackingSpanEnd { get; } - - public CommentTrackingSpan(TextSpan trackingTextSpan) - { - TrackingTextSpan = trackingTextSpan; - AmountToAddToTrackingSpanStart = 0; - AmountToAddToTrackingSpanEnd = 0; - } - - public CommentTrackingSpan(TextSpan trackingTextSpan, int amountToAddToStart, int amountToAddToEnd) - { - TrackingTextSpan = trackingTextSpan; - AmountToAddToTrackingSpanStart = amountToAddToStart; - AmountToAddToTrackingSpanEnd = amountToAddToEnd; - } - - public bool HasPostApplyChanges() - => AmountToAddToTrackingSpanStart != 0 || AmountToAddToTrackingSpanEnd != 0; + TrackingTextSpan = trackingTextSpan; + AmountToAddToTrackingSpanStart = 0; + AmountToAddToTrackingSpanEnd = 0; } + + public CommentTrackingSpan(TextSpan trackingTextSpan, int amountToAddToStart, int amountToAddToEnd) + { + TrackingTextSpan = trackingTextSpan; + AmountToAddToTrackingSpanStart = amountToAddToStart; + AmountToAddToTrackingSpanEnd = amountToAddToEnd; + } + + public bool HasPostApplyChanges() + => AmountToAddToTrackingSpanStart != 0 || AmountToAddToTrackingSpanEnd != 0; } diff --git a/src/EditorFeatures/Core/CommentSelection/CommentUncommentSelectionCommandHandler.cs b/src/EditorFeatures/Core/CommentSelection/CommentUncommentSelectionCommandHandler.cs index c2004c1edb5b5..a0cb546a79dff 100644 --- a/src/EditorFeatures/Core/CommentSelection/CommentUncommentSelectionCommandHandler.cs +++ b/src/EditorFeatures/Core/CommentSelection/CommentUncommentSelectionCommandHandler.cs @@ -24,331 +24,330 @@ using Microsoft.VisualStudio.Text.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CommentSelection +namespace Microsoft.CodeAnalysis.CommentSelection; + +[Export(typeof(ICommandHandler))] +[VisualStudio.Utilities.ContentType(ContentTypeNames.RoslynContentType)] +[VisualStudio.Utilities.Name(PredefinedCommandHandlerNames.CommentSelection)] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal class CommentUncommentSelectionCommandHandler( + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService) : + AbstractCommentSelectionBase(undoHistoryRegistry, editorOperationsFactoryService, editorOptionsService), + ICommandHandler, + ICommandHandler { - [Export(typeof(ICommandHandler))] - [VisualStudio.Utilities.ContentType(ContentTypeNames.RoslynContentType)] - [VisualStudio.Utilities.Name(PredefinedCommandHandlerNames.CommentSelection)] - [method: ImportingConstructor] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal class CommentUncommentSelectionCommandHandler( - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService) : - AbstractCommentSelectionBase(undoHistoryRegistry, editorOperationsFactoryService, editorOptionsService), - ICommandHandler, - ICommandHandler + public CommandState GetCommandState(CommentSelectionCommandArgs args) + => GetCommandState(args.SubjectBuffer); + + /// + /// Comment the selected spans, and reset the selection. + /// + public bool ExecuteCommand(CommentSelectionCommandArgs args, CommandExecutionContext context) + => this.ExecuteCommand(args.TextView, args.SubjectBuffer, Operation.Comment, context); + + public CommandState GetCommandState(UncommentSelectionCommandArgs args) + => GetCommandState(args.SubjectBuffer); + + /// + /// Uncomment the selected spans, and reset the selection. + /// + public bool ExecuteCommand(UncommentSelectionCommandArgs args, CommandExecutionContext context) + => this.ExecuteCommand(args.TextView, args.SubjectBuffer, Operation.Uncomment, context); + + public override string DisplayName => EditorFeaturesResources.Comment_Uncomment_Selection; + + protected override string GetTitle(Operation operation) + => operation == Operation.Comment + ? EditorFeaturesResources.Comment_Selection + : EditorFeaturesResources.Uncomment_Selection; + + protected override string GetMessage(Operation operation) + => operation == Operation.Comment + ? EditorFeaturesResources.Commenting_currently_selected_text + : EditorFeaturesResources.Uncommenting_currently_selected_text; + + /// + /// Add the necessary edits to the given spans. Also collect tracking spans over each span. + /// + /// Internal so that it can be called by unit tests. + /// + internal override CommentSelectionResult CollectEdits( + Document document, ICommentSelectionService service, ITextBuffer subjectBuffer, NormalizedSnapshotSpanCollection selectedSpans, + Operation operation, CancellationToken cancellationToken) { - public CommandState GetCommandState(CommentSelectionCommandArgs args) - => GetCommandState(args.SubjectBuffer); - - /// - /// Comment the selected spans, and reset the selection. - /// - public bool ExecuteCommand(CommentSelectionCommandArgs args, CommandExecutionContext context) - => this.ExecuteCommand(args.TextView, args.SubjectBuffer, Operation.Comment, context); - - public CommandState GetCommandState(UncommentSelectionCommandArgs args) - => GetCommandState(args.SubjectBuffer); - - /// - /// Uncomment the selected spans, and reset the selection. - /// - public bool ExecuteCommand(UncommentSelectionCommandArgs args, CommandExecutionContext context) - => this.ExecuteCommand(args.TextView, args.SubjectBuffer, Operation.Uncomment, context); - - public override string DisplayName => EditorFeaturesResources.Comment_Uncomment_Selection; - - protected override string GetTitle(Operation operation) - => operation == Operation.Comment - ? EditorFeaturesResources.Comment_Selection - : EditorFeaturesResources.Uncomment_Selection; - - protected override string GetMessage(Operation operation) - => operation == Operation.Comment - ? EditorFeaturesResources.Commenting_currently_selected_text - : EditorFeaturesResources.Uncommenting_currently_selected_text; - - /// - /// Add the necessary edits to the given spans. Also collect tracking spans over each span. - /// - /// Internal so that it can be called by unit tests. - /// - internal override CommentSelectionResult CollectEdits( - Document document, ICommentSelectionService service, ITextBuffer subjectBuffer, NormalizedSnapshotSpanCollection selectedSpans, - Operation operation, CancellationToken cancellationToken) + var spanTrackingList = ArrayBuilder.GetInstance(); + var textChanges = ArrayBuilder.GetInstance(); + foreach (var span in selectedSpans) { - var spanTrackingList = ArrayBuilder.GetInstance(); - var textChanges = ArrayBuilder.GetInstance(); - foreach (var span in selectedSpans) + if (operation == Operation.Comment) { - if (operation == Operation.Comment) - { - CommentSpan(service, span, textChanges, spanTrackingList); - } - else - { - UncommentSpan(service, span, textChanges, spanTrackingList); - } + CommentSpan(service, span, textChanges, spanTrackingList); } + else + { + UncommentSpan(service, span, textChanges, spanTrackingList); + } + } + + return new CommentSelectionResult(textChanges.ToArrayAndFree(), spanTrackingList.ToArrayAndFree(), operation); + } - return new CommentSelectionResult(textChanges.ToArrayAndFree(), spanTrackingList.ToArrayAndFree(), operation); + /// + /// Add the necessary edits to comment out a single span. + /// + private static void CommentSpan( + ICommentSelectionService service, SnapshotSpan span, + ArrayBuilder textChanges, ArrayBuilder trackingSpans) + { + var (firstLine, lastLine) = DetermineFirstAndLastLine(span); + + if (span.IsEmpty && firstLine.IsEmptyOrWhitespace()) + { + // No selection, and on an empty line, don't do anything. + return; } - /// - /// Add the necessary edits to comment out a single span. - /// - private static void CommentSpan( - ICommentSelectionService service, SnapshotSpan span, - ArrayBuilder textChanges, ArrayBuilder trackingSpans) + if (!span.IsEmpty && string.IsNullOrWhiteSpace(span.GetText())) { - var (firstLine, lastLine) = DetermineFirstAndLastLine(span); + // Just whitespace selected, don't do anything. + return; + } - if (span.IsEmpty && firstLine.IsEmptyOrWhitespace()) - { - // No selection, and on an empty line, don't do anything. - return; - } + // Get the information from the language as to how they'd like to comment this region. + var commentInfo = service.GetInfo(); + if (!commentInfo.SupportsBlockComment && !commentInfo.SupportsSingleLineComment) + { + // Neither type of comment supported. + return; + } - if (!span.IsEmpty && string.IsNullOrWhiteSpace(span.GetText())) + if (commentInfo.SupportsBlockComment && !commentInfo.SupportsSingleLineComment) + { + // Only block comments supported here. If there is a span, just surround that + // span with a block comment. If tehre is no span then surround the entire line + // with a block comment. + if (span.IsEmpty) { - // Just whitespace selected, don't do anything. - return; - } + var firstNonWhitespaceOnLine = firstLine.GetFirstNonWhitespacePosition(); + var insertPosition = firstNonWhitespaceOnLine ?? firstLine.Start; - // Get the information from the language as to how they'd like to comment this region. - var commentInfo = service.GetInfo(); - if (!commentInfo.SupportsBlockComment && !commentInfo.SupportsSingleLineComment) - { - // Neither type of comment supported. - return; + span = new SnapshotSpan(span.Snapshot, Span.FromBounds(insertPosition, firstLine.End)); } - if (commentInfo.SupportsBlockComment && !commentInfo.SupportsSingleLineComment) + AddBlockComment(span, textChanges, trackingSpans, commentInfo); + } + else if (!commentInfo.SupportsBlockComment && commentInfo.SupportsSingleLineComment) + { + // Only single line comments supported here. + AddSingleLineComments(span, textChanges, trackingSpans, firstLine, lastLine, commentInfo); + } + else + { + // both comment forms supported. Do a block comment only if a portion of code is + // selected on a single line, otherwise comment out all the lines using single-line + // comments. + if (!span.IsEmpty && + !SpanIncludesAllTextOnIncludedLines(span) && + firstLine.LineNumber == lastLine.LineNumber) { - // Only block comments supported here. If there is a span, just surround that - // span with a block comment. If tehre is no span then surround the entire line - // with a block comment. - if (span.IsEmpty) - { - var firstNonWhitespaceOnLine = firstLine.GetFirstNonWhitespacePosition(); - var insertPosition = firstNonWhitespaceOnLine ?? firstLine.Start; - - span = new SnapshotSpan(span.Snapshot, Span.FromBounds(insertPosition, firstLine.End)); - } - AddBlockComment(span, textChanges, trackingSpans, commentInfo); } - else if (!commentInfo.SupportsBlockComment && commentInfo.SupportsSingleLineComment) - { - // Only single line comments supported here. - AddSingleLineComments(span, textChanges, trackingSpans, firstLine, lastLine, commentInfo); - } else { - // both comment forms supported. Do a block comment only if a portion of code is - // selected on a single line, otherwise comment out all the lines using single-line - // comments. - if (!span.IsEmpty && - !SpanIncludesAllTextOnIncludedLines(span) && - firstLine.LineNumber == lastLine.LineNumber) - { - AddBlockComment(span, textChanges, trackingSpans, commentInfo); - } - else - { - AddSingleLineComments(span, textChanges, trackingSpans, firstLine, lastLine, commentInfo); - } + AddSingleLineComments(span, textChanges, trackingSpans, firstLine, lastLine, commentInfo); } } + } + + private static void AddSingleLineComments(SnapshotSpan span, ArrayBuilder textChanges, ArrayBuilder trackingSpans, ITextSnapshotLine firstLine, ITextSnapshotLine lastLine, CommentSelectionInfo commentInfo) + { + // Select the entirety of the lines, so that another comment operation will add more + // comments, not insert block comments. + trackingSpans.Add(new CommentTrackingSpan(TextSpan.FromBounds(firstLine.Start.Position, lastLine.End.Position))); + var indentToCommentAt = DetermineSmallestIndent(span, firstLine, lastLine); + ApplySingleLineCommentToNonBlankLines(commentInfo, textChanges, firstLine, lastLine, indentToCommentAt); + } + + private static void AddBlockComment(SnapshotSpan span, ArrayBuilder textChanges, ArrayBuilder trackingSpans, CommentSelectionInfo commentInfo) + { + trackingSpans.Add(new CommentTrackingSpan(TextSpan.FromBounds(span.Start, span.End))); + InsertText(textChanges, span.Start, commentInfo.BlockCommentStartString); + InsertText(textChanges, span.End, commentInfo.BlockCommentEndString); + } - private static void AddSingleLineComments(SnapshotSpan span, ArrayBuilder textChanges, ArrayBuilder trackingSpans, ITextSnapshotLine firstLine, ITextSnapshotLine lastLine, CommentSelectionInfo commentInfo) + /// + /// Add the necessary edits to uncomment out a single span. + /// + private static void UncommentSpan( + ICommentSelectionService service, SnapshotSpan span, + ArrayBuilder textChanges, ArrayBuilder spansToSelect) + { + var info = service.GetInfo(); + + // If the selection is exactly a block comment, use it as priority over single line comments. + if (info.SupportsBlockComment && TryUncommentExactlyBlockComment(info, span, textChanges, spansToSelect)) { - // Select the entirety of the lines, so that another comment operation will add more - // comments, not insert block comments. - trackingSpans.Add(new CommentTrackingSpan(TextSpan.FromBounds(firstLine.Start.Position, lastLine.End.Position))); - var indentToCommentAt = DetermineSmallestIndent(span, firstLine, lastLine); - ApplySingleLineCommentToNonBlankLines(commentInfo, textChanges, firstLine, lastLine, indentToCommentAt); + return; } - private static void AddBlockComment(SnapshotSpan span, ArrayBuilder textChanges, ArrayBuilder trackingSpans, CommentSelectionInfo commentInfo) + if (info.SupportsSingleLineComment && + TryUncommentSingleLineComments(info, span, textChanges, spansToSelect)) { - trackingSpans.Add(new CommentTrackingSpan(TextSpan.FromBounds(span.Start, span.End))); - InsertText(textChanges, span.Start, commentInfo.BlockCommentStartString); - InsertText(textChanges, span.End, commentInfo.BlockCommentEndString); + return; } - /// - /// Add the necessary edits to uncomment out a single span. - /// - private static void UncommentSpan( - ICommentSelectionService service, SnapshotSpan span, - ArrayBuilder textChanges, ArrayBuilder spansToSelect) + // We didn't make any single line changes. If the language supports block comments, see + // if we're inside a containing block comment and uncomment that. + if (info.SupportsBlockComment) { - var info = service.GetInfo(); - - // If the selection is exactly a block comment, use it as priority over single line comments. - if (info.SupportsBlockComment && TryUncommentExactlyBlockComment(info, span, textChanges, spansToSelect)) - { - return; - } + UncommentContainingBlockComment(info, span, textChanges, spansToSelect); + } + } - if (info.SupportsSingleLineComment && - TryUncommentSingleLineComments(info, span, textChanges, spansToSelect)) - { - return; - } + /// + /// Check if the selected span matches an entire block comment. + /// If it does, uncomment it and return true. + /// + private static bool TryUncommentExactlyBlockComment(CommentSelectionInfo info, SnapshotSpan span, ArrayBuilder textChanges, + ArrayBuilder spansToSelect) + { + var spanText = span.GetText(); + var trimmedSpanText = spanText.Trim(); - // We didn't make any single line changes. If the language supports block comments, see - // if we're inside a containing block comment and uncomment that. - if (info.SupportsBlockComment) - { - UncommentContainingBlockComment(info, span, textChanges, spansToSelect); - } + // See if the selection includes just a block comment (plus whitespace) + if (trimmedSpanText.StartsWith(info.BlockCommentStartString, StringComparison.Ordinal) && trimmedSpanText.EndsWith(info.BlockCommentEndString, StringComparison.Ordinal)) + { + var positionOfStart = span.Start + spanText.IndexOf(info.BlockCommentStartString, StringComparison.Ordinal); + var positionOfEnd = span.Start + spanText.LastIndexOf(info.BlockCommentEndString, StringComparison.Ordinal); + UncommentPosition(info, textChanges, spansToSelect, positionOfStart, positionOfEnd); + return true; } - /// - /// Check if the selected span matches an entire block comment. - /// If it does, uncomment it and return true. - /// - private static bool TryUncommentExactlyBlockComment(CommentSelectionInfo info, SnapshotSpan span, ArrayBuilder textChanges, - ArrayBuilder spansToSelect) - { - var spanText = span.GetText(); - var trimmedSpanText = spanText.Trim(); + return false; + } - // See if the selection includes just a block comment (plus whitespace) - if (trimmedSpanText.StartsWith(info.BlockCommentStartString, StringComparison.Ordinal) && trimmedSpanText.EndsWith(info.BlockCommentEndString, StringComparison.Ordinal)) + private static void UncommentContainingBlockComment(CommentSelectionInfo info, SnapshotSpan span, ArrayBuilder textChanges, + ArrayBuilder spansToSelect) + { + // See if we are (textually) contained in a block comment. + // This could allow a selection that spans multiple block comments to uncomment the beginning of + // the first and end of the last. Oh well. + var positionOfEnd = -1; + var text = span.Snapshot.AsText(); + var positionOfStart = text.LastIndexOf(info.BlockCommentStartString, span.Start, caseSensitive: true); + + // If we found a start comment marker, make sure there isn't an end comment marker after it but before our span. + if (positionOfStart >= 0) + { + var lastEnd = text.LastIndexOf(info.BlockCommentEndString, span.Start, caseSensitive: true); + if (lastEnd < positionOfStart) { - var positionOfStart = span.Start + spanText.IndexOf(info.BlockCommentStartString, StringComparison.Ordinal); - var positionOfEnd = span.Start + spanText.LastIndexOf(info.BlockCommentEndString, StringComparison.Ordinal); - UncommentPosition(info, textChanges, spansToSelect, positionOfStart, positionOfEnd); - return true; + positionOfEnd = text.IndexOf(info.BlockCommentEndString, span.End, caseSensitive: true); } - - return false; - } - - private static void UncommentContainingBlockComment(CommentSelectionInfo info, SnapshotSpan span, ArrayBuilder textChanges, - ArrayBuilder spansToSelect) - { - // See if we are (textually) contained in a block comment. - // This could allow a selection that spans multiple block comments to uncomment the beginning of - // the first and end of the last. Oh well. - var positionOfEnd = -1; - var text = span.Snapshot.AsText(); - var positionOfStart = text.LastIndexOf(info.BlockCommentStartString, span.Start, caseSensitive: true); - - // If we found a start comment marker, make sure there isn't an end comment marker after it but before our span. - if (positionOfStart >= 0) + else if (lastEnd + info.BlockCommentEndString.Length > span.End) { - var lastEnd = text.LastIndexOf(info.BlockCommentEndString, span.Start, caseSensitive: true); - if (lastEnd < positionOfStart) - { - positionOfEnd = text.IndexOf(info.BlockCommentEndString, span.End, caseSensitive: true); - } - else if (lastEnd + info.BlockCommentEndString.Length > span.End) - { - // The end of the span is *inside* the end marker, so searching backwards found it. - positionOfEnd = lastEnd; - } + // The end of the span is *inside* the end marker, so searching backwards found it. + positionOfEnd = lastEnd; } - - UncommentPosition(info, textChanges, spansToSelect, positionOfStart, positionOfEnd); } - private static void UncommentPosition(CommentSelectionInfo info, ArrayBuilder textChanges, - ArrayBuilder spansToSelect, int positionOfStart, int positionOfEnd) - { - if (positionOfStart < 0 || positionOfEnd < 0) - { - return; - } + UncommentPosition(info, textChanges, spansToSelect, positionOfStart, positionOfEnd); + } - spansToSelect.Add(new CommentTrackingSpan(TextSpan.FromBounds(positionOfStart, positionOfEnd + info.BlockCommentEndString.Length))); - DeleteText(textChanges, new TextSpan(positionOfStart, info.BlockCommentStartString.Length)); - DeleteText(textChanges, new TextSpan(positionOfEnd, info.BlockCommentEndString.Length)); + private static void UncommentPosition(CommentSelectionInfo info, ArrayBuilder textChanges, + ArrayBuilder spansToSelect, int positionOfStart, int positionOfEnd) + { + if (positionOfStart < 0 || positionOfEnd < 0) + { + return; } - private static bool TryUncommentSingleLineComments(CommentSelectionInfo info, SnapshotSpan span, ArrayBuilder textChanges, - ArrayBuilder spansToSelect) - { - // First see if we're selecting any lines that have the single-line comment prefix. - // If so, then we'll just remove the single-line comment prefix from those lines. - var (firstLine, lastLine) = DetermineFirstAndLastLine(span); + spansToSelect.Add(new CommentTrackingSpan(TextSpan.FromBounds(positionOfStart, positionOfEnd + info.BlockCommentEndString.Length))); + DeleteText(textChanges, new TextSpan(positionOfStart, info.BlockCommentStartString.Length)); + DeleteText(textChanges, new TextSpan(positionOfEnd, info.BlockCommentEndString.Length)); + } - for (var lineNumber = firstLine.LineNumber; lineNumber <= lastLine.LineNumber; ++lineNumber) - { - var line = span.Snapshot.GetLineFromLineNumber(lineNumber); - var lineText = line.GetText(); - if (lineText.Trim().StartsWith(info.SingleLineCommentString, StringComparison.Ordinal)) - { - DeleteText(textChanges, new TextSpan(line.Start.Position + lineText.IndexOf(info.SingleLineCommentString, StringComparison.Ordinal), info.SingleLineCommentString.Length)); - } - } + private static bool TryUncommentSingleLineComments(CommentSelectionInfo info, SnapshotSpan span, ArrayBuilder textChanges, + ArrayBuilder spansToSelect) + { + // First see if we're selecting any lines that have the single-line comment prefix. + // If so, then we'll just remove the single-line comment prefix from those lines. + var (firstLine, lastLine) = DetermineFirstAndLastLine(span); - // If we made any changes, select the entirety of the lines we change, so that subsequent invocations will - // affect the same lines. - if (textChanges.Count == 0) + for (var lineNumber = firstLine.LineNumber; lineNumber <= lastLine.LineNumber; ++lineNumber) + { + var line = span.Snapshot.GetLineFromLineNumber(lineNumber); + var lineText = line.GetText(); + if (lineText.Trim().StartsWith(info.SingleLineCommentString, StringComparison.Ordinal)) { - return false; + DeleteText(textChanges, new TextSpan(line.Start.Position + lineText.IndexOf(info.SingleLineCommentString, StringComparison.Ordinal), info.SingleLineCommentString.Length)); } - - spansToSelect.Add(new CommentTrackingSpan(TextSpan.FromBounds(firstLine.Start.Position, lastLine.End.Position))); - return true; } - /// - /// Adds edits to comment out each non-blank line, at the given indent. - /// - private static void ApplySingleLineCommentToNonBlankLines( - CommentSelectionInfo info, ArrayBuilder textChanges, ITextSnapshotLine firstLine, ITextSnapshotLine lastLine, int indentToCommentAt) + // If we made any changes, select the entirety of the lines we change, so that subsequent invocations will + // affect the same lines. + if (textChanges.Count == 0) { - var snapshot = firstLine.Snapshot; - for (var lineNumber = firstLine.LineNumber; lineNumber <= lastLine.LineNumber; ++lineNumber) - { - var line = snapshot.GetLineFromLineNumber(lineNumber); - if (!line.IsEmptyOrWhitespace()) - { - InsertText(textChanges, line.Start + indentToCommentAt, info.SingleLineCommentString); - } - } + return false; } - /// - /// Given a span, find the first and last line that are part of the span. NOTE: If the - /// span ends in column zero, we back up to the previous line, to handle the case where - /// the user used shift + down to select a bunch of lines. They probably don't want the - /// last line commented in that case. - /// - private static (ITextSnapshotLine firstLine, ITextSnapshotLine lastLine) DetermineFirstAndLastLine(SnapshotSpan span) + spansToSelect.Add(new CommentTrackingSpan(TextSpan.FromBounds(firstLine.Start.Position, lastLine.End.Position))); + return true; + } + + /// + /// Adds edits to comment out each non-blank line, at the given indent. + /// + private static void ApplySingleLineCommentToNonBlankLines( + CommentSelectionInfo info, ArrayBuilder textChanges, ITextSnapshotLine firstLine, ITextSnapshotLine lastLine, int indentToCommentAt) + { + var snapshot = firstLine.Snapshot; + for (var lineNumber = firstLine.LineNumber; lineNumber <= lastLine.LineNumber; ++lineNumber) { - var firstLine = span.Snapshot.GetLineFromPosition(span.Start.Position); - var lastLine = span.Snapshot.GetLineFromPosition(span.End.Position); - if (lastLine.Start == span.End.Position && !span.IsEmpty) + var line = snapshot.GetLineFromLineNumber(lineNumber); + if (!line.IsEmptyOrWhitespace()) { - lastLine = lastLine.GetPreviousMatchingLine(_ => true); + InsertText(textChanges, line.Start + indentToCommentAt, info.SingleLineCommentString); } - - return (firstLine, lastLine); } + } - /// - /// Returns true if the span includes all of the non-whitespace text on the first and last line. - /// - private static bool SpanIncludesAllTextOnIncludedLines(SnapshotSpan span) + /// + /// Given a span, find the first and last line that are part of the span. NOTE: If the + /// span ends in column zero, we back up to the previous line, to handle the case where + /// the user used shift + down to select a bunch of lines. They probably don't want the + /// last line commented in that case. + /// + private static (ITextSnapshotLine firstLine, ITextSnapshotLine lastLine) DetermineFirstAndLastLine(SnapshotSpan span) + { + var firstLine = span.Snapshot.GetLineFromPosition(span.Start.Position); + var lastLine = span.Snapshot.GetLineFromPosition(span.End.Position); + if (lastLine.Start == span.End.Position && !span.IsEmpty) { - var (firstLine, lastLine) = DetermineFirstAndLastLine(span); + lastLine = lastLine.GetPreviousMatchingLine(_ => true); + } - var firstNonWhitespacePosition = firstLine.GetFirstNonWhitespacePosition(); - var lastNonWhitespacePosition = lastLine.GetLastNonWhitespacePosition(); + return (firstLine, lastLine); + } - var allOnFirst = !firstNonWhitespacePosition.HasValue || - span.Start.Position <= firstNonWhitespacePosition.Value; - var allOnLast = !lastNonWhitespacePosition.HasValue || - span.End.Position > lastNonWhitespacePosition.Value; + /// + /// Returns true if the span includes all of the non-whitespace text on the first and last line. + /// + private static bool SpanIncludesAllTextOnIncludedLines(SnapshotSpan span) + { + var (firstLine, lastLine) = DetermineFirstAndLastLine(span); - return allOnFirst && allOnLast; - } + var firstNonWhitespacePosition = firstLine.GetFirstNonWhitespacePosition(); + var lastNonWhitespacePosition = lastLine.GetLastNonWhitespacePosition(); + + var allOnFirst = !firstNonWhitespacePosition.HasValue || + span.Start.Position <= firstNonWhitespacePosition.Value; + var allOnLast = !lastNonWhitespacePosition.HasValue || + span.End.Position > lastNonWhitespacePosition.Value; + + return allOnFirst && allOnLast; } } diff --git a/src/EditorFeatures/Core/CommentSelection/ToggleBlockCommentCommandHandler.cs b/src/EditorFeatures/Core/CommentSelection/ToggleBlockCommentCommandHandler.cs index c03a7308bd2e7..a199d90c309cb 100644 --- a/src/EditorFeatures/Core/CommentSelection/ToggleBlockCommentCommandHandler.cs +++ b/src/EditorFeatures/Core/CommentSelection/ToggleBlockCommentCommandHandler.cs @@ -20,46 +20,45 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.CommentSelection +namespace Microsoft.CodeAnalysis.CommentSelection; + +[Export(typeof(ICommandHandler))] +[VisualStudio.Utilities.ContentType(ContentTypeNames.RoslynContentType)] +[VisualStudio.Utilities.Name(PredefinedCommandHandlerNames.ToggleBlockComment)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class ToggleBlockCommentCommandHandler( + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + ITextStructureNavigatorSelectorService navigatorSelectorService, + EditorOptionsService editorOptionsService) : AbstractToggleBlockCommentBase(undoHistoryRegistry, editorOperationsFactoryService, navigatorSelectorService, editorOptionsService) { - [Export(typeof(ICommandHandler))] - [VisualStudio.Utilities.ContentType(ContentTypeNames.RoslynContentType)] - [VisualStudio.Utilities.Name(PredefinedCommandHandlerNames.ToggleBlockComment)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class ToggleBlockCommentCommandHandler( - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - ITextStructureNavigatorSelectorService navigatorSelectorService, - EditorOptionsService editorOptionsService) : AbstractToggleBlockCommentBase(undoHistoryRegistry, editorOperationsFactoryService, navigatorSelectorService, editorOptionsService) + + /// + /// Gets block comments by parsing the text for comment markers. + /// + protected override ImmutableArray GetBlockCommentsInDocument(Document document, ITextSnapshot snapshot, + TextSpan linesContainingSelections, CommentSelectionInfo commentInfo, CancellationToken cancellationToken) { + var allText = snapshot.AsText(); + var commentedSpans = ArrayBuilder.GetInstance(); - /// - /// Gets block comments by parsing the text for comment markers. - /// - protected override ImmutableArray GetBlockCommentsInDocument(Document document, ITextSnapshot snapshot, - TextSpan linesContainingSelections, CommentSelectionInfo commentInfo, CancellationToken cancellationToken) + var openIdx = 0; + while ((openIdx = allText.IndexOf(commentInfo.BlockCommentStartString, openIdx, caseSensitive: true)) >= 0) { - var allText = snapshot.AsText(); - var commentedSpans = ArrayBuilder.GetInstance(); - - var openIdx = 0; - while ((openIdx = allText.IndexOf(commentInfo.BlockCommentStartString, openIdx, caseSensitive: true)) >= 0) + // Retrieve the first closing marker located after the open index. + var closeIdx = allText.IndexOf(commentInfo.BlockCommentEndString, openIdx + commentInfo.BlockCommentStartString.Length, caseSensitive: true); + // If an open marker is found without a close marker, it's an unclosed comment. + if (closeIdx < 0) { - // Retrieve the first closing marker located after the open index. - var closeIdx = allText.IndexOf(commentInfo.BlockCommentEndString, openIdx + commentInfo.BlockCommentStartString.Length, caseSensitive: true); - // If an open marker is found without a close marker, it's an unclosed comment. - if (closeIdx < 0) - { - closeIdx = allText.Length - commentInfo.BlockCommentEndString.Length; - } - - var blockCommentSpan = new TextSpan(openIdx, closeIdx + commentInfo.BlockCommentEndString.Length - openIdx); - commentedSpans.Add(blockCommentSpan); - openIdx = closeIdx; + closeIdx = allText.Length - commentInfo.BlockCommentEndString.Length; } - return commentedSpans.ToImmutableAndFree(); + var blockCommentSpan = new TextSpan(openIdx, closeIdx + commentInfo.BlockCommentEndString.Length - openIdx); + commentedSpans.Add(blockCommentSpan); + openIdx = closeIdx; } + + return commentedSpans.ToImmutableAndFree(); } } diff --git a/src/EditorFeatures/Core/CommentSelection/ToggleLineCommentCommandHandler.cs b/src/EditorFeatures/Core/CommentSelection/ToggleLineCommentCommandHandler.cs index 7f8352887e767..154a95fa74c81 100644 --- a/src/EditorFeatures/Core/CommentSelection/ToggleLineCommentCommandHandler.cs +++ b/src/EditorFeatures/Core/CommentSelection/ToggleLineCommentCommandHandler.cs @@ -27,163 +27,162 @@ using Microsoft.VisualStudio.Text.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CommentSelection +namespace Microsoft.CodeAnalysis.CommentSelection; + +[Export(typeof(ICommandHandler))] +[VisualStudio.Utilities.ContentType(ContentTypeNames.RoslynContentType)] +[VisualStudio.Utilities.Name(PredefinedCommandHandlerNames.ToggleLineComment)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class ToggleLineCommentCommandHandler( + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService) : + // Value tuple to represent that there is no distinct command to be passed in. + AbstractCommentSelectionBase(undoHistoryRegistry, editorOperationsFactoryService, editorOptionsService), + ICommandHandler { - [Export(typeof(ICommandHandler))] - [VisualStudio.Utilities.ContentType(ContentTypeNames.RoslynContentType)] - [VisualStudio.Utilities.Name(PredefinedCommandHandlerNames.ToggleLineComment)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class ToggleLineCommentCommandHandler( - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService) : - // Value tuple to represent that there is no distinct command to be passed in. - AbstractCommentSelectionBase(undoHistoryRegistry, editorOperationsFactoryService, editorOptionsService), - ICommandHandler - { - private static readonly CommentSelectionResult s_emptyCommentSelectionResult = - new([], [], Operation.Uncomment); + private static readonly CommentSelectionResult s_emptyCommentSelectionResult = + new([], [], Operation.Uncomment); - public CommandState GetCommandState(ToggleLineCommentCommandArgs args) - => GetCommandState(args.SubjectBuffer); + public CommandState GetCommandState(ToggleLineCommentCommandArgs args) + => GetCommandState(args.SubjectBuffer); - public bool ExecuteCommand(ToggleLineCommentCommandArgs args, CommandExecutionContext context) - => ExecuteCommand(args.TextView, args.SubjectBuffer, ValueTuple.Create(), context); + public bool ExecuteCommand(ToggleLineCommentCommandArgs args, CommandExecutionContext context) + => ExecuteCommand(args.TextView, args.SubjectBuffer, ValueTuple.Create(), context); - public override string DisplayName => EditorFeaturesResources.Toggle_Line_Comment; + public override string DisplayName => EditorFeaturesResources.Toggle_Line_Comment; - protected override string GetTitle(ValueTuple command) => EditorFeaturesResources.Toggle_Line_Comment; + protected override string GetTitle(ValueTuple command) => EditorFeaturesResources.Toggle_Line_Comment; - protected override string GetMessage(ValueTuple command) => EditorFeaturesResources.Toggling_line_comment; + protected override string GetMessage(ValueTuple command) => EditorFeaturesResources.Toggling_line_comment; - internal override CommentSelectionResult CollectEdits(Document document, ICommentSelectionService service, - ITextBuffer subjectBuffer, NormalizedSnapshotSpanCollection selectedSpans, ValueTuple command, CancellationToken cancellationToken) + internal override CommentSelectionResult CollectEdits(Document document, ICommentSelectionService service, + ITextBuffer subjectBuffer, NormalizedSnapshotSpanCollection selectedSpans, ValueTuple command, CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.CommandHandler_ToggleLineComment, KeyValueLogMessage.Create(LogType.UserAction, m => { - using (Logger.LogBlock(FunctionId.CommandHandler_ToggleLineComment, KeyValueLogMessage.Create(LogType.UserAction, m => - { - m[LanguageNameString] = document.Project.Language; - m[LengthString] = subjectBuffer.CurrentSnapshot.Length; - }), cancellationToken)) + m[LanguageNameString] = document.Project.Language; + m[LengthString] = subjectBuffer.CurrentSnapshot.Length; + }), cancellationToken)) + { + var commentInfo = service.GetInfo(); + if (commentInfo.SupportsSingleLineComment) { - var commentInfo = service.GetInfo(); - if (commentInfo.SupportsSingleLineComment) - { - return ToggleLineComment(commentInfo, selectedSpans); - } - - return s_emptyCommentSelectionResult; + return ToggleLineComment(commentInfo, selectedSpans); } - } - private static CommentSelectionResult ToggleLineComment(CommentSelectionInfo commentInfo, - NormalizedSnapshotSpanCollection selectedSpans) - { - var textChanges = ArrayBuilder.GetInstance(); - var trackingSpans = ArrayBuilder.GetInstance(); + return s_emptyCommentSelectionResult; + } + } - var linesInSelections = selectedSpans.ToDictionary( - span => span, - span => GetLinesFromSelectedSpan(span).ToImmutableArray()); + private static CommentSelectionResult ToggleLineComment(CommentSelectionInfo commentInfo, + NormalizedSnapshotSpanCollection selectedSpans) + { + var textChanges = ArrayBuilder.GetInstance(); + var trackingSpans = ArrayBuilder.GetInstance(); - var isMultiCaret = selectedSpans.Count > 1; + var linesInSelections = selectedSpans.ToDictionary( + span => span, + span => GetLinesFromSelectedSpan(span).ToImmutableArray()); - Operation operation; - // If any of the lines are uncommented, add comments. - if (linesInSelections.Values.Any(lines => SelectionHasUncommentedLines(lines, commentInfo))) - { - foreach (var selection in linesInSelections) - { - CommentLines(selection.Key, selection.Value, textChanges, trackingSpans, commentInfo); - } + var isMultiCaret = selectedSpans.Count > 1; - operation = Operation.Comment; - } - else + Operation operation; + // If any of the lines are uncommented, add comments. + if (linesInSelections.Values.Any(lines => SelectionHasUncommentedLines(lines, commentInfo))) + { + foreach (var selection in linesInSelections) { - foreach (var selection in linesInSelections) - { - UncommentLines(selection.Key, selection.Value, textChanges, trackingSpans, commentInfo); - } - - operation = Operation.Uncomment; + CommentLines(selection.Key, selection.Value, textChanges, trackingSpans, commentInfo); } - return new CommentSelectionResult(textChanges, trackingSpans, operation); + operation = Operation.Comment; } - - private static void UncommentLines( - SnapshotSpan selectedSpan, - ImmutableArray commentedLines, - ArrayBuilder textChanges, - ArrayBuilder trackingSpans, - CommentSelectionInfo commentInfo) + else { - foreach (var line in commentedLines) + foreach (var selection in linesInSelections) { - if (!line.IsEmptyOrWhitespace()) - { - var text = line.GetText(); - var commentIndex = text.IndexOf(commentInfo.SingleLineCommentString) + line.Start; - var spanToRemove = TextSpan.FromBounds(commentIndex, commentIndex + commentInfo.SingleLineCommentString.Length); - DeleteText(textChanges, spanToRemove); - } + UncommentLines(selection.Key, selection.Value, textChanges, trackingSpans, commentInfo); } - var commentTrackingSpan = new CommentTrackingSpan(selectedSpan.Span.ToTextSpan()); - trackingSpans.Add(commentTrackingSpan); + operation = Operation.Uncomment; } - private static void CommentLines( - SnapshotSpan selectedSpan, - ImmutableArray linesInSelection, - ArrayBuilder textChanges, - ArrayBuilder trackingSpans, - CommentSelectionInfo commentInfo) + return new CommentSelectionResult(textChanges, trackingSpans, operation); + } + + private static void UncommentLines( + SnapshotSpan selectedSpan, + ImmutableArray commentedLines, + ArrayBuilder textChanges, + ArrayBuilder trackingSpans, + CommentSelectionInfo commentInfo) + { + foreach (var line in commentedLines) { - var indentation = DetermineSmallestIndent(selectedSpan, linesInSelection.First(), linesInSelection.Last()); - foreach (var line in linesInSelection) + if (!line.IsEmptyOrWhitespace()) { - if (!line.IsEmptyOrWhitespace()) - { - InsertText(textChanges, line.Start + indentation, commentInfo.SingleLineCommentString); - } + var text = line.GetText(); + var commentIndex = text.IndexOf(commentInfo.SingleLineCommentString) + line.Start; + var spanToRemove = TextSpan.FromBounds(commentIndex, commentIndex + commentInfo.SingleLineCommentString.Length); + DeleteText(textChanges, spanToRemove); } - - var commentTrackingSpan = new CommentTrackingSpan(selectedSpan.Span.ToTextSpan()); - trackingSpans.Add(commentTrackingSpan); } - private static List GetLinesFromSelectedSpan(SnapshotSpan span) + var commentTrackingSpan = new CommentTrackingSpan(selectedSpan.Span.ToTextSpan()); + trackingSpans.Add(commentTrackingSpan); + } + + private static void CommentLines( + SnapshotSpan selectedSpan, + ImmutableArray linesInSelection, + ArrayBuilder textChanges, + ArrayBuilder trackingSpans, + CommentSelectionInfo commentInfo) + { + var indentation = DetermineSmallestIndent(selectedSpan, linesInSelection.First(), linesInSelection.Last()); + foreach (var line in linesInSelection) { - var lines = new List(); - var startLine = span.Snapshot.GetLineFromPosition(span.Start); - var endLine = span.Snapshot.GetLineFromPosition(span.End); - // Don't include the last line if the span is just the start of the line. - if (endLine.Start == span.End.Position && !span.IsEmpty) + if (!line.IsEmptyOrWhitespace()) { - endLine = endLine.GetPreviousMatchingLine(_ => true); + InsertText(textChanges, line.Start + indentation, commentInfo.SingleLineCommentString); } + } - if (startLine.LineNumber <= endLine.LineNumber) - { - for (var i = startLine.LineNumber; i <= endLine.LineNumber; i++) - { - lines.Add(span.Snapshot.GetLineFromLineNumber(i)); - } - } + var commentTrackingSpan = new CommentTrackingSpan(selectedSpan.Span.ToTextSpan()); + trackingSpans.Add(commentTrackingSpan); + } - return lines; + private static List GetLinesFromSelectedSpan(SnapshotSpan span) + { + var lines = new List(); + var startLine = span.Snapshot.GetLineFromPosition(span.Start); + var endLine = span.Snapshot.GetLineFromPosition(span.End); + // Don't include the last line if the span is just the start of the line. + if (endLine.Start == span.End.Position && !span.IsEmpty) + { + endLine = endLine.GetPreviousMatchingLine(_ => true); } - private static bool SelectionHasUncommentedLines(ImmutableArray linesInSelection, CommentSelectionInfo commentInfo) - => linesInSelection.Any(static (l, commentInfo) => !IsLineCommentedOrEmpty(l, commentInfo), commentInfo); - - private static bool IsLineCommentedOrEmpty(ITextSnapshotLine line, CommentSelectionInfo info) + if (startLine.LineNumber <= endLine.LineNumber) { - var lineText = line.GetText(); - // We don't add / remove anything for empty lines. - return lineText.Trim().StartsWith(info.SingleLineCommentString, StringComparison.Ordinal) || line.IsEmptyOrWhitespace(); + for (var i = startLine.LineNumber; i <= endLine.LineNumber; i++) + { + lines.Add(span.Snapshot.GetLineFromLineNumber(i)); + } } + + return lines; + } + + private static bool SelectionHasUncommentedLines(ImmutableArray linesInSelection, CommentSelectionInfo commentInfo) + => linesInSelection.Any(static (l, commentInfo) => !IsLineCommentedOrEmpty(l, commentInfo), commentInfo); + + private static bool IsLineCommentedOrEmpty(ITextSnapshotLine line, CommentSelectionInfo info) + { + var lineText = line.GetText(); + // We don't add / remove anything for empty lines. + return lineText.Trim().StartsWith(info.SingleLineCommentString, StringComparison.Ordinal) || line.IsEmptyOrWhitespace(); } } diff --git a/src/EditorFeatures/Core/ContentTypeLanguageMetadata.cs b/src/EditorFeatures/Core/ContentTypeLanguageMetadata.cs index 1cd6140077fdd..c3e1d90ff94cc 100644 --- a/src/EditorFeatures/Core/ContentTypeLanguageMetadata.cs +++ b/src/EditorFeatures/Core/ContentTypeLanguageMetadata.cs @@ -2,28 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System.Collections.Generic; using Microsoft.CodeAnalysis.Host.Mef; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor -{ - internal class ContentTypeLanguageMetadata : LanguageMetadata - { - public string DefaultContentType { get; } - - public ContentTypeLanguageMetadata(IDictionary data) - : base(data) - { - this.DefaultContentType = (string)data.GetValueOrDefault("DefaultContentType"); - } +namespace Microsoft.CodeAnalysis.Editor; - public ContentTypeLanguageMetadata(string defaultContentType, string language) - : base(language) - { - this.DefaultContentType = defaultContentType; - } - } +internal sealed class ContentTypeLanguageMetadata(IDictionary data) : ILanguageMetadata +{ + public string Language { get; } = (string)data[nameof(ExportLanguageServiceAttribute.Language)]; + public string? DefaultContentType { get; } = (string?)data.GetValueOrDefault(nameof(ExportContentTypeLanguageServiceAttribute.DefaultContentType)); } diff --git a/src/EditorFeatures/Core/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.RoslynErrorTag.cs b/src/EditorFeatures/Core/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.RoslynErrorTag.cs index b279571ce26bf..42a1bc01b9726 100644 --- a/src/EditorFeatures/Core/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.RoslynErrorTag.cs +++ b/src/EditorFeatures/Core/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.RoslynErrorTag.cs @@ -9,57 +9,56 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal partial class AbstractDiagnosticsAdornmentTaggerProvider { - internal partial class AbstractDiagnosticsAdornmentTaggerProvider + protected sealed class RoslynErrorTag(string errorType, Workspace workspace, DiagnosticData data) : ErrorTag(errorType, CreateToolTipContent(workspace, data)), IEquatable { - protected sealed class RoslynErrorTag(string errorType, Workspace workspace, DiagnosticData data) : ErrorTag(errorType, CreateToolTipContent(workspace, data)), IEquatable - { - private readonly DiagnosticData _data = data; + private readonly DiagnosticData _data = data; - private static object CreateToolTipContent(Workspace workspace, DiagnosticData diagnostic) + private static object CreateToolTipContent(Workspace workspace, DiagnosticData diagnostic) + { + Action? navigationAction = null; + string? tooltip = null; + if (workspace != null) { - Action? navigationAction = null; - string? tooltip = null; - if (workspace != null) + var helpLinkUri = diagnostic.GetValidHelpLinkUri(); + if (helpLinkUri != null) { - var helpLinkUri = diagnostic.GetValidHelpLinkUri(); - if (helpLinkUri != null) - { - navigationAction = new QuickInfoHyperLink(workspace, helpLinkUri).NavigationAction; - tooltip = diagnostic.HelpLink; - } + navigationAction = new QuickInfoHyperLink(workspace, helpLinkUri).NavigationAction; + tooltip = diagnostic.HelpLink; } - - var diagnosticIdTextRun = navigationAction is null - ? new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Id) - : new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Id, navigationAction, tooltip); - - return new ContainerElement( - ContainerElementStyle.Wrapped, - new ClassifiedTextElement( - diagnosticIdTextRun, - new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ":"), - new ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Message))); } - public override bool Equals(object? obj) - => Equals(obj as RoslynErrorTag); + var diagnosticIdTextRun = navigationAction is null + ? new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Id) + : new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Id, navigationAction, tooltip); - public bool Equals(RoslynErrorTag? other) - { - return other != null && - this.ErrorType == other.ErrorType && - this._data.HelpLink == other._data.HelpLink && - this._data.Id == other._data.Id && - this._data.Message == other._data.Message; - } + return new ContainerElement( + ContainerElementStyle.Wrapped, + new ClassifiedTextElement( + diagnosticIdTextRun, + new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ":"), + new ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Message))); + } + + public override bool Equals(object? obj) + => Equals(obj as RoslynErrorTag); - // Intentionally throwing, we have never supported this facility, and there is no contract around placing - // these tags in sets or maps. - public override int GetHashCode() - => throw new NotImplementedException(); + public bool Equals(RoslynErrorTag? other) + { + return other != null && + this.ErrorType == other.ErrorType && + this._data.HelpLink == other._data.HelpLink && + this._data.Id == other._data.Id && + this._data.Message == other._data.Message; } + + // Intentionally throwing, we have never supported this facility, and there is no contract around placing + // these tags in sets or maps. + public override int GetHashCode() + => throw new NotImplementedException(); } } diff --git a/src/EditorFeatures/Core/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.cs b/src/EditorFeatures/Core/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.cs index 3b70603321d0b..8b14f8ea2b33f 100644 --- a/src/EditorFeatures/Core/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.cs +++ b/src/EditorFeatures/Core/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.cs @@ -10,57 +10,56 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal abstract partial class AbstractDiagnosticsAdornmentTaggerProvider : + AbstractDiagnosticsTaggerProvider + where TTag : class, ITag { - internal abstract partial class AbstractDiagnosticsAdornmentTaggerProvider : - AbstractDiagnosticsTaggerProvider - where TTag : class, ITag + protected AbstractDiagnosticsAdornmentTaggerProvider( + IThreadingContext threadingContext, + IDiagnosticService diagnosticService, + IDiagnosticAnalyzerService analyzerService, + IGlobalOptionService globalOptions, + ITextBufferVisibilityTracker? visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) + : base(threadingContext, diagnosticService, analyzerService, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.ErrorSquiggles)) { - protected AbstractDiagnosticsAdornmentTaggerProvider( - IThreadingContext threadingContext, - IDiagnosticService diagnosticService, - IDiagnosticAnalyzerService analyzerService, - IGlobalOptionService globalOptions, - ITextBufferVisibilityTracker? visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) - : base(threadingContext, diagnosticService, analyzerService, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.ErrorSquiggles)) - { - } + } - protected abstract TTag? CreateTag(Workspace workspace, DiagnosticData diagnostic); + protected abstract TTag? CreateTag(Workspace workspace, DiagnosticData diagnostic); - protected sealed override bool IsEnabled => true; + protected sealed override bool IsEnabled => true; - protected sealed override ITagSpan? CreateTagSpan(Workspace workspace, SnapshotSpan span, DiagnosticData data) - { - var errorTag = CreateTag(workspace, data); - if (errorTag == null) - return null; + protected sealed override ITagSpan? CreateTagSpan(Workspace workspace, SnapshotSpan span, DiagnosticData data) + { + var errorTag = CreateTag(workspace, data); + if (errorTag == null) + return null; - // Ensure the diagnostic has at least length 1. Tags must have a non-empty length in order to actually show - // up in the editor. - var adjustedSpan = AdjustSnapshotSpan(span, minimumLength: 1); - if (adjustedSpan.Length == 0) - return null; + // Ensure the diagnostic has at least length 1. Tags must have a non-empty length in order to actually show + // up in the editor. + var adjustedSpan = AdjustSnapshotSpan(span, minimumLength: 1); + if (adjustedSpan.Length == 0) + return null; - return new TagSpan(adjustedSpan, errorTag); - } + return new TagSpan(adjustedSpan, errorTag); + } - protected virtual SnapshotSpan AdjustSnapshotSpan(SnapshotSpan span, int minimumLength) - => AdjustSnapshotSpan(span, minimumLength, maximumLength: int.MaxValue); + protected virtual SnapshotSpan AdjustSnapshotSpan(SnapshotSpan span, int minimumLength) + => AdjustSnapshotSpan(span, minimumLength, maximumLength: int.MaxValue); - protected static SnapshotSpan AdjustSnapshotSpan(SnapshotSpan span, int minimumLength, int maximumLength) - { - var snapshot = span.Snapshot; + protected static SnapshotSpan AdjustSnapshotSpan(SnapshotSpan span, int minimumLength, int maximumLength) + { + var snapshot = span.Snapshot; - // new length - var length = Math.Min(Math.Max(span.Length, minimumLength), maximumLength); + // new length + var length = Math.Min(Math.Max(span.Length, minimumLength), maximumLength); - // make sure start + length is smaller than snapshot.Length and start is >= 0 - var start = Math.Max(0, Math.Min(span.Start, snapshot.Length - length)); + // make sure start + length is smaller than snapshot.Length and start is >= 0 + var start = Math.Max(0, Math.Min(span.Start, snapshot.Length - length)); - // make sure length is smaller than snapshot.Length which can happen if start == 0 - return new SnapshotSpan(snapshot, start, Math.Min(start + length, snapshot.Length) - start); - } + // make sure length is smaller than snapshot.Length which can happen if start == 0 + return new SnapshotSpan(snapshot, start, Math.Min(start + length, snapshot.Length) - start); } } diff --git a/src/EditorFeatures/Core/Diagnostics/ClassificationTypeDefinitions.cs b/src/EditorFeatures/Core/Diagnostics/ClassificationTypeDefinitions.cs index 93a10fc8c1775..7778e3a4aa66a 100644 --- a/src/EditorFeatures/Core/Diagnostics/ClassificationTypeDefinitions.cs +++ b/src/EditorFeatures/Core/Diagnostics/ClassificationTypeDefinitions.cs @@ -9,15 +9,14 @@ using Microsoft.VisualStudio.Text.Classification; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal sealed class ClassificationTypeDefinitions { - internal sealed class ClassificationTypeDefinitions - { - public const string UnnecessaryCode = "unnecessary code"; + public const string UnnecessaryCode = "unnecessary code"; - [Export] - [Name(UnnecessaryCode)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal ClassificationTypeDefinition UnnecessaryCodeTypeDefinition { get; set; } - } + [Export] + [Name(UnnecessaryCode)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition UnnecessaryCodeTypeDefinition { get; set; } } diff --git a/src/EditorFeatures/Core/Diagnostics/DiagnosticsClassificationTaggerProvider.cs b/src/EditorFeatures/Core/Diagnostics/DiagnosticsClassificationTaggerProvider.cs index b86f615b5b723..deda9048452f8 100644 --- a/src/EditorFeatures/Core/Diagnostics/DiagnosticsClassificationTaggerProvider.cs +++ b/src/EditorFeatures/Core/Diagnostics/DiagnosticsClassificationTaggerProvider.cs @@ -25,81 +25,80 @@ using Microsoft.VisualStudio.Text.Tagging; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +[Export(typeof(ITaggerProvider))] +[ContentType(ContentTypeNames.RoslynContentType)] +[ContentType(ContentTypeNames.XamlContentType)] +[TagType(typeof(ClassificationTag))] +internal sealed partial class DiagnosticsClassificationTaggerProvider : AbstractDiagnosticsTaggerProvider { - [Export(typeof(ITaggerProvider))] - [ContentType(ContentTypeNames.RoslynContentType)] - [ContentType(ContentTypeNames.XamlContentType)] - [TagType(typeof(ClassificationTag))] - internal sealed partial class DiagnosticsClassificationTaggerProvider : AbstractDiagnosticsTaggerProvider - { - private readonly ClassificationTypeMap _typeMap; - private readonly ClassificationTag _classificationTag; - private readonly EditorOptionsService _editorOptionsService; + private readonly ClassificationTypeMap _typeMap; + private readonly ClassificationTag _classificationTag; + private readonly EditorOptionsService _editorOptionsService; - protected sealed override ImmutableArray Options { get; } = [DiagnosticsOptionsStorage.Classification]; + protected sealed override ImmutableArray Options { get; } = [DiagnosticsOptionsStorage.Classification]; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public DiagnosticsClassificationTaggerProvider( - IThreadingContext threadingContext, - IDiagnosticService diagnosticService, - IDiagnosticAnalyzerService analyzerService, - ClassificationTypeMap typeMap, - EditorOptionsService editorOptionsService, - [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) - : base(threadingContext, diagnosticService, analyzerService, editorOptionsService.GlobalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.Classification)) - { - _typeMap = typeMap; - _classificationTag = new ClassificationTag(_typeMap.GetClassificationType(ClassificationTypeDefinitions.UnnecessaryCode)); - _editorOptionsService = editorOptionsService; - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DiagnosticsClassificationTaggerProvider( + IThreadingContext threadingContext, + IDiagnosticService diagnosticService, + IDiagnosticAnalyzerService analyzerService, + ClassificationTypeMap typeMap, + EditorOptionsService editorOptionsService, + [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) + : base(threadingContext, diagnosticService, analyzerService, editorOptionsService.GlobalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.Classification)) + { + _typeMap = typeMap; + _classificationTag = new ClassificationTag(_typeMap.GetClassificationType(ClassificationTypeDefinitions.UnnecessaryCode)); + _editorOptionsService = editorOptionsService; + } + + // If we are under high contrast mode, the editor ignores classification tags that fade things out, + // because that reduces contrast. Since the editor will ignore them, there's no reason to produce them. + protected sealed override bool IsEnabled + => !_editorOptionsService.Factory.GlobalOptions.GetOptionValue(DefaultTextViewHostOptions.IsInContrastModeId); - // If we are under high contrast mode, the editor ignores classification tags that fade things out, - // because that reduces contrast. Since the editor will ignore them, there's no reason to produce them. - protected sealed override bool IsEnabled - => !_editorOptionsService.Factory.GlobalOptions.GetOptionValue(DefaultTextViewHostOptions.IsInContrastModeId); + protected sealed override bool SupportsDiagnosticMode(DiagnosticMode mode) + { + // We only support solution crawler push diagnostics. When lsp pull diagnostics are on, diagnostic fading + // is handled by the lsp client. + return mode == DiagnosticMode.SolutionCrawlerPush; + } - protected sealed override bool SupportsDiagnosticMode(DiagnosticMode mode) + protected sealed override bool IncludeDiagnostic(DiagnosticData data) + { + if (!data.CustomTags.Contains(WellKnownDiagnosticTags.Unnecessary)) { - // We only support solution crawler push diagnostics. When lsp pull diagnostics are on, diagnostic fading - // is handled by the lsp client. - return mode == DiagnosticMode.SolutionCrawlerPush; + return false; } - protected sealed override bool IncludeDiagnostic(DiagnosticData data) + // Do not fade if user has disabled the fading option corresponding to this diagnostic. + if (IDEDiagnosticIdToOptionMappingHelper.TryGetMappedFadingOption(data.Id, out var fadingOption)) { - if (!data.CustomTags.Contains(WellKnownDiagnosticTags.Unnecessary)) - { - return false; - } - - // Do not fade if user has disabled the fading option corresponding to this diagnostic. - if (IDEDiagnosticIdToOptionMappingHelper.TryGetMappedFadingOption(data.Id, out var fadingOption)) - { - return data.Language != null - && _editorOptionsService.GlobalOptions.GetOption(fadingOption, data.Language); - } - - return true; + return data.Language != null + && _editorOptionsService.GlobalOptions.GetOption(fadingOption, data.Language); } - protected sealed override ITagSpan CreateTagSpan(Workspace workspace, SnapshotSpan span, DiagnosticData data) - => new TagSpan(span, _classificationTag); + return true; + } + + protected sealed override ITagSpan CreateTagSpan(Workspace workspace, SnapshotSpan span, DiagnosticData data) + => new TagSpan(span, _classificationTag); - protected sealed override ImmutableArray GetLocationsToTag(DiagnosticData diagnosticData) + protected sealed override ImmutableArray GetLocationsToTag(DiagnosticData diagnosticData) + { + if (diagnosticData.TryGetUnnecessaryDataLocations(out var locationsToTag)) { - if (diagnosticData.TryGetUnnecessaryDataLocations(out var locationsToTag)) - { - return locationsToTag.Value; - } - - // Default to the base implementation for the diagnostic data - return base.GetLocationsToTag(diagnosticData); + return locationsToTag.Value; } - protected sealed override bool TagEquals(ClassificationTag tag1, ClassificationTag tag2) - => tag1.ClassificationType.Classification == tag2.ClassificationType.Classification; + // Default to the base implementation for the diagnostic data + return base.GetLocationsToTag(diagnosticData); } + + protected sealed override bool TagEquals(ClassificationTag tag1, ClassificationTag tag2) + => tag1.ClassificationType.Classification == tag2.ClassificationType.Classification; } diff --git a/src/EditorFeatures/Core/Diagnostics/DiagnosticsOptionsStorage.cs b/src/EditorFeatures/Core/Diagnostics/DiagnosticsOptionsStorage.cs index 2b712fcd47133..1069c1b0238ea 100644 --- a/src/EditorFeatures/Core/Diagnostics/DiagnosticsOptionsStorage.cs +++ b/src/EditorFeatures/Core/Diagnostics/DiagnosticsOptionsStorage.cs @@ -4,12 +4,11 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal static class DiagnosticsOptionsStorage { - internal static class DiagnosticsOptionsStorage - { - public static readonly Option2 Classification = new("dotnet_enable_classification", defaultValue: true); + public static readonly Option2 Classification = new("dotnet_enable_classification", defaultValue: true); - public static readonly Option2 Squiggles = new("dotnet_enable_squiggles", defaultValue: true); - } + public static readonly Option2 Squiggles = new("dotnet_enable_squiggles", defaultValue: true); } diff --git a/src/EditorFeatures/Core/Diagnostics/DiagnosticsSquiggleTaggerProvider.cs b/src/EditorFeatures/Core/Diagnostics/DiagnosticsSquiggleTaggerProvider.cs index 06c0699c88767..c38644fc78537 100644 --- a/src/EditorFeatures/Core/Diagnostics/DiagnosticsSquiggleTaggerProvider.cs +++ b/src/EditorFeatures/Core/Diagnostics/DiagnosticsSquiggleTaggerProvider.cs @@ -20,112 +20,111 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +[Export(typeof(ITaggerProvider))] +[ContentType(ContentTypeNames.RoslynContentType)] +[ContentType(ContentTypeNames.XamlContentType)] +[TagType(typeof(IErrorTag))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed partial class DiagnosticsSquiggleTaggerProvider( + IThreadingContext threadingContext, + IDiagnosticService diagnosticService, + IDiagnosticAnalyzerService analyzerService, + IGlobalOptionService globalOptions, + [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) : AbstractDiagnosticsAdornmentTaggerProvider(threadingContext, diagnosticService, analyzerService, globalOptions, visibilityTracker, listenerProvider) { - [Export(typeof(ITaggerProvider))] - [ContentType(ContentTypeNames.RoslynContentType)] - [ContentType(ContentTypeNames.XamlContentType)] - [TagType(typeof(IErrorTag))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed partial class DiagnosticsSquiggleTaggerProvider( - IThreadingContext threadingContext, - IDiagnosticService diagnosticService, - IDiagnosticAnalyzerService analyzerService, - IGlobalOptionService globalOptions, - [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) : AbstractDiagnosticsAdornmentTaggerProvider(threadingContext, diagnosticService, analyzerService, globalOptions, visibilityTracker, listenerProvider) - { - protected override ImmutableArray Options { get; } = [DiagnosticsOptionsStorage.Squiggles]; + protected override ImmutableArray Options { get; } = [DiagnosticsOptionsStorage.Squiggles]; - protected sealed override bool SupportsDiagnosticMode(DiagnosticMode mode) - { - // We only support solution crawler push diagnostics. When lsp pull diagnostics are on, squiggles - // are handled by the lsp client. - return mode == DiagnosticMode.SolutionCrawlerPush; - } + protected sealed override bool SupportsDiagnosticMode(DiagnosticMode mode) + { + // We only support solution crawler push diagnostics. When lsp pull diagnostics are on, squiggles + // are handled by the lsp client. + return mode == DiagnosticMode.SolutionCrawlerPush; + } - protected sealed override bool IncludeDiagnostic(DiagnosticData diagnostic) - { - var isUnnecessary = diagnostic.Severity == DiagnosticSeverity.Hidden && diagnostic.CustomTags.Contains(WellKnownDiagnosticTags.Unnecessary); + protected sealed override bool IncludeDiagnostic(DiagnosticData diagnostic) + { + var isUnnecessary = diagnostic.Severity == DiagnosticSeverity.Hidden && diagnostic.CustomTags.Contains(WellKnownDiagnosticTags.Unnecessary); - return - (diagnostic.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error || isUnnecessary) && - !string.IsNullOrWhiteSpace(diagnostic.Message); - } + return + (diagnostic.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error || isUnnecessary) && + !string.IsNullOrWhiteSpace(diagnostic.Message); + } - protected sealed override IErrorTag? CreateTag(Workspace workspace, DiagnosticData diagnostic) + protected sealed override IErrorTag? CreateTag(Workspace workspace, DiagnosticData diagnostic) + { + Debug.Assert(!string.IsNullOrWhiteSpace(diagnostic.Message)); + var errorType = GetErrorTypeFromDiagnostic(diagnostic); + if (errorType == null) { - Debug.Assert(!string.IsNullOrWhiteSpace(diagnostic.Message)); - var errorType = GetErrorTypeFromDiagnostic(diagnostic); - if (errorType == null) - { - // unknown diagnostic kind. - // we don't provide tagging for unknown diagnostic kind. - // - // it should be provided by the one who introduced the new diagnostic kind. - return null; - } - - return new RoslynErrorTag(errorType, workspace, diagnostic); + // unknown diagnostic kind. + // we don't provide tagging for unknown diagnostic kind. + // + // it should be provided by the one who introduced the new diagnostic kind. + return null; } - private static string? GetErrorTypeFromDiagnostic(DiagnosticData diagnostic) - { - if (diagnostic.IsSuppressed) - { - // Don't squiggle suppressed diagnostics. - return null; - } - - return GetErrorTypeFromDiagnosticTags(diagnostic) ?? - GetErrorTypeFromDiagnosticSeverity(diagnostic); - } + return new RoslynErrorTag(errorType, workspace, diagnostic); + } - private static string? GetErrorTypeFromDiagnosticTags(DiagnosticData diagnostic) + private static string? GetErrorTypeFromDiagnostic(DiagnosticData diagnostic) + { + if (diagnostic.IsSuppressed) { - if (diagnostic.Severity == DiagnosticSeverity.Error && - diagnostic.CustomTags.Contains(WellKnownDiagnosticTags.EditAndContinue)) - { - return EditAndContinueErrorTypeDefinition.Name; - } - + // Don't squiggle suppressed diagnostics. return null; } - private static string? GetErrorTypeFromDiagnosticSeverity(DiagnosticData diagnostic) - { - switch (diagnostic.Severity) - { - case DiagnosticSeverity.Error: - return PredefinedErrorTypeNames.SyntaxError; - case DiagnosticSeverity.Warning: - return PredefinedErrorTypeNames.Warning; - case DiagnosticSeverity.Info: - return null; - case DiagnosticSeverity.Hidden: - if (diagnostic.CustomTags.Contains(WellKnownDiagnosticTags.Unnecessary)) - { - // This ensures that we have an 'invisible' squiggle (which will in turn - // display Quick Info on mouse hover) for the hidden diagnostics that we - // report for 'Remove unnecessary usings' and 'Simplify Type Name'. The - // presence of Quick Info pane for such squiggles allows platform - // to display Light Bulb for the corresponding fixes (per their current - // design platform can only display light bulb if Quick Info pane is present). - return PredefinedErrorTypeNames.Suggestion; - } + return GetErrorTypeFromDiagnosticTags(diagnostic) ?? + GetErrorTypeFromDiagnosticSeverity(diagnostic); + } - return null; - default: - return PredefinedErrorTypeNames.OtherError; - } + private static string? GetErrorTypeFromDiagnosticTags(DiagnosticData diagnostic) + { + if (diagnostic.Severity == DiagnosticSeverity.Error && + diagnostic.CustomTags.Contains(WellKnownDiagnosticTags.EditAndContinue)) + { + return EditAndContinueErrorTypeDefinition.Name; } - protected sealed override bool TagEquals(IErrorTag tag1, IErrorTag tag2) + return null; + } + + private static string? GetErrorTypeFromDiagnosticSeverity(DiagnosticData diagnostic) + { + switch (diagnostic.Severity) { - Contract.ThrowIfFalse(tag1 is RoslynErrorTag); - Contract.ThrowIfFalse(tag2 is RoslynErrorTag); - return tag1.Equals(tag2); + case DiagnosticSeverity.Error: + return PredefinedErrorTypeNames.SyntaxError; + case DiagnosticSeverity.Warning: + return PredefinedErrorTypeNames.Warning; + case DiagnosticSeverity.Info: + return null; + case DiagnosticSeverity.Hidden: + if (diagnostic.CustomTags.Contains(WellKnownDiagnosticTags.Unnecessary)) + { + // This ensures that we have an 'invisible' squiggle (which will in turn + // display Quick Info on mouse hover) for the hidden diagnostics that we + // report for 'Remove unnecessary usings' and 'Simplify Type Name'. The + // presence of Quick Info pane for such squiggles allows platform + // to display Light Bulb for the corresponding fixes (per their current + // design platform can only display light bulb if Quick Info pane is present). + return PredefinedErrorTypeNames.Suggestion; + } + + return null; + default: + return PredefinedErrorTypeNames.OtherError; } } + + protected sealed override bool TagEquals(IErrorTag tag1, IErrorTag tag2) + { + Contract.ThrowIfFalse(tag1 is RoslynErrorTag); + Contract.ThrowIfFalse(tag2 is RoslynErrorTag); + return tag1.Equals(tag2); + } } diff --git a/src/EditorFeatures/Core/Diagnostics/DiagnosticsSuggestionTaggerProvider.cs b/src/EditorFeatures/Core/Diagnostics/DiagnosticsSuggestionTaggerProvider.cs index 78c87b5475482..93d37a3411eae 100644 --- a/src/EditorFeatures/Core/Diagnostics/DiagnosticsSuggestionTaggerProvider.cs +++ b/src/EditorFeatures/Core/Diagnostics/DiagnosticsSuggestionTaggerProvider.cs @@ -19,49 +19,48 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +[Export(typeof(ITaggerProvider))] +[ContentType(ContentTypeNames.RoslynContentType)] +[ContentType(ContentTypeNames.XamlContentType)] +[TagType(typeof(IErrorTag))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed partial class DiagnosticsSuggestionTaggerProvider( + IThreadingContext threadingContext, + IDiagnosticService diagnosticService, + IDiagnosticAnalyzerService analyzerService, + IGlobalOptionService globalOptions, + [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) : + AbstractDiagnosticsAdornmentTaggerProvider(threadingContext, diagnosticService, analyzerService, globalOptions, visibilityTracker, listenerProvider) { - [Export(typeof(ITaggerProvider))] - [ContentType(ContentTypeNames.RoslynContentType)] - [ContentType(ContentTypeNames.XamlContentType)] - [TagType(typeof(IErrorTag))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed partial class DiagnosticsSuggestionTaggerProvider( - IThreadingContext threadingContext, - IDiagnosticService diagnosticService, - IDiagnosticAnalyzerService analyzerService, - IGlobalOptionService globalOptions, - [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) : - AbstractDiagnosticsAdornmentTaggerProvider(threadingContext, diagnosticService, analyzerService, globalOptions, visibilityTracker, listenerProvider) - { - protected sealed override ImmutableArray Options { get; } = [DiagnosticsOptionsStorage.Squiggles]; + protected sealed override ImmutableArray Options { get; } = [DiagnosticsOptionsStorage.Squiggles]; - protected sealed override bool IncludeDiagnostic(DiagnosticData diagnostic) - => diagnostic.Severity == DiagnosticSeverity.Info; + protected sealed override bool IncludeDiagnostic(DiagnosticData diagnostic) + => diagnostic.Severity == DiagnosticSeverity.Info; - protected sealed override bool SupportsDiagnosticMode(DiagnosticMode mode) - { - // We only support solution crawler push diagnostics. When lsp pull diagnostics are on, ellipses - // suggestions are handled by the lsp client. - return mode == DiagnosticMode.SolutionCrawlerPush; - } + protected sealed override bool SupportsDiagnosticMode(DiagnosticMode mode) + { + // We only support solution crawler push diagnostics. When lsp pull diagnostics are on, ellipses + // suggestions are handled by the lsp client. + return mode == DiagnosticMode.SolutionCrawlerPush; + } - protected sealed override IErrorTag CreateTag(Workspace workspace, DiagnosticData diagnostic) - => new RoslynErrorTag(PredefinedErrorTypeNames.HintedSuggestion, workspace, diagnostic); + protected sealed override IErrorTag CreateTag(Workspace workspace, DiagnosticData diagnostic) + => new RoslynErrorTag(PredefinedErrorTypeNames.HintedSuggestion, workspace, diagnostic); - protected sealed override SnapshotSpan AdjustSnapshotSpan(SnapshotSpan snapshotSpan, int minimumLength) - { - // We always want suggestion tags to be two characters long. - return AdjustSnapshotSpan(snapshotSpan, minimumLength: 2, maximumLength: 2); - } + protected sealed override SnapshotSpan AdjustSnapshotSpan(SnapshotSpan snapshotSpan, int minimumLength) + { + // We always want suggestion tags to be two characters long. + return AdjustSnapshotSpan(snapshotSpan, minimumLength: 2, maximumLength: 2); + } - protected sealed override bool TagEquals(IErrorTag tag1, IErrorTag tag2) - { - Contract.ThrowIfFalse(tag1 is RoslynErrorTag); - Contract.ThrowIfFalse(tag2 is RoslynErrorTag); - return tag1.Equals(tag2); - } + protected sealed override bool TagEquals(IErrorTag tag1, IErrorTag tag2) + { + Contract.ThrowIfFalse(tag1 is RoslynErrorTag); + Contract.ThrowIfFalse(tag2 is RoslynErrorTag); + return tag1.Equals(tag2); } } diff --git a/src/EditorFeatures/Core/DocumentSnapshotSpan.cs b/src/EditorFeatures/Core/DocumentSnapshotSpan.cs index 09fae16ee5bd5..5ef8b16f461ad 100644 --- a/src/EditorFeatures/Core/DocumentSnapshotSpan.cs +++ b/src/EditorFeatures/Core/DocumentSnapshotSpan.cs @@ -4,25 +4,24 @@ using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +/// +/// Represents an editor and the +/// the span was produced from. +/// +/// +/// Creates a new . +/// +internal readonly struct DocumentSnapshotSpan(Document? document, SnapshotSpan snapshotSpan) { /// - /// Represents an editor and the - /// the span was produced from. + /// The the span was produced from. /// - /// - /// Creates a new . - /// - internal readonly struct DocumentSnapshotSpan(Document? document, SnapshotSpan snapshotSpan) - { - /// - /// The the span was produced from. - /// - public Document? Document { get; } = document; + public Document? Document { get; } = document; - /// - /// The editor . - /// - public SnapshotSpan SnapshotSpan { get; } = snapshotSpan; - } + /// + /// The editor . + /// + public SnapshotSpan SnapshotSpan { get; } = snapshotSpan; } diff --git a/src/EditorFeatures/Core/DocumentationComments/AbstractDocumentationCommentCommandHandler.cs b/src/EditorFeatures/Core/DocumentationComments/AbstractDocumentationCommentCommandHandler.cs index 2aa4bacfe494d..3c6747dade9b7 100644 --- a/src/EditorFeatures/Core/DocumentationComments/AbstractDocumentationCommentCommandHandler.cs +++ b/src/EditorFeatures/Core/DocumentationComments/AbstractDocumentationCommentCommandHandler.cs @@ -18,355 +18,354 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.DocumentationComments +namespace Microsoft.CodeAnalysis.DocumentationComments; + +internal abstract class AbstractDocumentationCommentCommandHandler : + IChainedCommandHandler, + ICommandHandler, + ICommandHandler, + IChainedCommandHandler, + IChainedCommandHandler { - internal abstract class AbstractDocumentationCommentCommandHandler : - IChainedCommandHandler, - ICommandHandler, - ICommandHandler, - IChainedCommandHandler, - IChainedCommandHandler + private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; + private readonly EditorOptionsService _editorOptionsService; + + protected AbstractDocumentationCommentCommandHandler( + IUIThreadOperationExecutor uiThreadOperationExecutor, + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService) { - private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; - private readonly EditorOptionsService _editorOptionsService; - - protected AbstractDocumentationCommentCommandHandler( - IUIThreadOperationExecutor uiThreadOperationExecutor, - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService) - { - Contract.ThrowIfNull(uiThreadOperationExecutor); - Contract.ThrowIfNull(undoHistoryRegistry); - Contract.ThrowIfNull(editorOperationsFactoryService); - - _uiThreadOperationExecutor = uiThreadOperationExecutor; - _undoHistoryRegistry = undoHistoryRegistry; - _editorOperationsFactoryService = editorOperationsFactoryService; - _editorOptionsService = editorOptionsService; - } + Contract.ThrowIfNull(uiThreadOperationExecutor); + Contract.ThrowIfNull(undoHistoryRegistry); + Contract.ThrowIfNull(editorOperationsFactoryService); + + _uiThreadOperationExecutor = uiThreadOperationExecutor; + _undoHistoryRegistry = undoHistoryRegistry; + _editorOperationsFactoryService = editorOperationsFactoryService; + _editorOptionsService = editorOptionsService; + } - protected abstract string ExteriorTriviaText { get; } + protected abstract string ExteriorTriviaText { get; } - private char TriggerCharacter - { - get { return ExteriorTriviaText[^1]; } - } + private char TriggerCharacter + { + get { return ExteriorTriviaText[^1]; } + } - public string DisplayName => EditorFeaturesResources.Documentation_Comment; + public string DisplayName => EditorFeaturesResources.Documentation_Comment; - private static DocumentationCommentSnippet? InsertOnCharacterTyped(IDocumentationCommentSnippetService service, SyntaxTree syntaxTree, SourceText text, int position, DocumentationCommentOptions options, CancellationToken cancellationToken) - => service.GetDocumentationCommentSnippetOnCharacterTyped(syntaxTree, text, position, options, cancellationToken); + private static DocumentationCommentSnippet? InsertOnCharacterTyped(IDocumentationCommentSnippetService service, SyntaxTree syntaxTree, SourceText text, int position, DocumentationCommentOptions options, CancellationToken cancellationToken) + => service.GetDocumentationCommentSnippetOnCharacterTyped(syntaxTree, text, position, options, cancellationToken); - private static DocumentationCommentSnippet? InsertOnEnterTyped(IDocumentationCommentSnippetService service, SyntaxTree syntaxTree, SourceText text, int position, DocumentationCommentOptions options, CancellationToken cancellationToken) - => service.GetDocumentationCommentSnippetOnEnterTyped(syntaxTree, text, position, options, cancellationToken); + private static DocumentationCommentSnippet? InsertOnEnterTyped(IDocumentationCommentSnippetService service, SyntaxTree syntaxTree, SourceText text, int position, DocumentationCommentOptions options, CancellationToken cancellationToken) + => service.GetDocumentationCommentSnippetOnEnterTyped(syntaxTree, text, position, options, cancellationToken); - private static DocumentationCommentSnippet? InsertOnCommandInvoke(IDocumentationCommentSnippetService service, SyntaxTree syntaxTree, SourceText text, int position, DocumentationCommentOptions options, CancellationToken cancellationToken) - => service.GetDocumentationCommentSnippetOnCommandInvoke(syntaxTree, text, position, options, cancellationToken); + private static DocumentationCommentSnippet? InsertOnCommandInvoke(IDocumentationCommentSnippetService service, SyntaxTree syntaxTree, SourceText text, int position, DocumentationCommentOptions options, CancellationToken cancellationToken) + => service.GetDocumentationCommentSnippetOnCommandInvoke(syntaxTree, text, position, options, cancellationToken); - private static void ApplySnippet(DocumentationCommentSnippet snippet, ITextBuffer subjectBuffer, ITextView textView) + private static void ApplySnippet(DocumentationCommentSnippet snippet, ITextBuffer subjectBuffer, ITextView textView) + { + var replaceSpan = snippet.SpanToReplace.ToSpan(); + subjectBuffer.Replace(replaceSpan, snippet.SnippetText); + textView.TryMoveCaretToAndEnsureVisible(subjectBuffer.CurrentSnapshot.GetPoint(replaceSpan.Start + snippet.CaretOffset)); + } + + private bool CompleteComment( + ITextBuffer subjectBuffer, + ITextView textView, + Func getSnippetAction, + CancellationToken cancellationToken) + { + var caretPosition = textView.GetCaretPoint(subjectBuffer) ?? -1; + if (caretPosition < 0) { - var replaceSpan = snippet.SpanToReplace.ToSpan(); - subjectBuffer.Replace(replaceSpan, snippet.SnippetText); - textView.TryMoveCaretToAndEnsureVisible(subjectBuffer.CurrentSnapshot.GetPoint(replaceSpan.Start + snippet.CaretOffset)); + return false; } - private bool CompleteComment( - ITextBuffer subjectBuffer, - ITextView textView, - Func getSnippetAction, - CancellationToken cancellationToken) + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) { - var caretPosition = textView.GetCaretPoint(subjectBuffer) ?? -1; - if (caretPosition < 0) - { - return false; - } - - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return false; - } - - var service = document.GetRequiredLanguageService(); - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - var options = subjectBuffer.GetDocumentationCommentOptions(_editorOptionsService, document.Project.Services); - - // Apply snippet in reverse order so that the first applied snippet doesn't affect span of next snippets. - var snapshots = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer).OrderByDescending(s => s.Span.Start); - var returnValue = false; - foreach (var snapshot in snapshots) - { - var snippet = getSnippetAction(service, parsedDocument.SyntaxTree, parsedDocument.Text, snapshot.Span.Start, options, cancellationToken); - if (snippet != null) - { - ApplySnippet(snippet, subjectBuffer, textView); - returnValue = true; - } - } - - return returnValue; + return false; } - public CommandState GetCommandState(TypeCharCommandArgs args, Func nextHandler) - => nextHandler(); + var service = document.GetRequiredLanguageService(); + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + var options = subjectBuffer.GetDocumentationCommentOptions(_editorOptionsService, document.Project.Services); - public void ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context) + // Apply snippet in reverse order so that the first applied snippet doesn't affect span of next snippets. + var snapshots = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer).OrderByDescending(s => s.Span.Start); + var returnValue = false; + foreach (var snapshot in snapshots) { - // Ensure the character is actually typed in the editor - nextHandler(); - - if (args.TypedChar != TriggerCharacter) + var snippet = getSnippetAction(service, parsedDocument.SyntaxTree, parsedDocument.Text, snapshot.Span.Start, options, cancellationToken); + if (snippet != null) { - return; + ApplySnippet(snippet, subjectBuffer, textView); + returnValue = true; } + } - // Don't execute in cloud environment, as we let LSP handle that - if (args.SubjectBuffer.IsInLspEditorContext()) - { - return; - } + return returnValue; + } - CompleteComment(args.SubjectBuffer, args.TextView, InsertOnCharacterTyped, CancellationToken.None); - } + public CommandState GetCommandState(TypeCharCommandArgs args, Func nextHandler) + => nextHandler(); - public CommandState GetCommandState(ReturnKeyCommandArgs args) - => CommandState.Unspecified; + public void ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + // Ensure the character is actually typed in the editor + nextHandler(); - public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) + if (args.TypedChar != TriggerCharacter) { - // Don't execute in cloud environment, as we let LSP handle that - if (args.SubjectBuffer.IsInLspEditorContext()) - { - return false; - } + return; + } - // Check to see if the current line starts with exterior trivia. If so, we'll take over. - // If not, let the nextHandler run. + // Don't execute in cloud environment, as we let LSP handle that + if (args.SubjectBuffer.IsInLspEditorContext()) + { + return; + } - var originalPosition = -1; + CompleteComment(args.SubjectBuffer, args.TextView, InsertOnCharacterTyped, CancellationToken.None); + } - // The original position should be a position that is consistent with the syntax tree, even - // after Enter is pressed. Thus, we use the start of the first selection if there is one. - // Otherwise, getting the tokens to the right or the left might return unexpected results. + public CommandState GetCommandState(ReturnKeyCommandArgs args) + => CommandState.Unspecified; - if (args.TextView.Selection.SelectedSpans.Count > 0) - { - var selectedSpan = args.TextView.Selection - .GetSnapshotSpansOnBuffer(args.SubjectBuffer) - .FirstOrNull(); + public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) + { + // Don't execute in cloud environment, as we let LSP handle that + if (args.SubjectBuffer.IsInLspEditorContext()) + { + return false; + } - originalPosition = selectedSpan != null - ? selectedSpan.Value.Start - : args.TextView.GetCaretPoint(args.SubjectBuffer) ?? -1; - } + // Check to see if the current line starts with exterior trivia. If so, we'll take over. + // If not, let the nextHandler run. - if (originalPosition < 0) - { - return false; - } + var originalPosition = -1; - if (!CurrentLineStartsWithExteriorTrivia(args.SubjectBuffer, originalPosition, context.OperationContext.UserCancellationToken)) - { - return false; - } + // The original position should be a position that is consistent with the syntax tree, even + // after Enter is pressed. Thus, we use the start of the first selection if there is one. + // Otherwise, getting the tokens to the right or the left might return unexpected results. - // According to JasonMal, the text undo history is associated with the surface buffer - // in projection buffer scenarios, so the following line's usage of the surface buffer - // is correct. - using (var transaction = _undoHistoryRegistry.GetHistory(args.TextView.TextBuffer).CreateTransaction(EditorFeaturesResources.Insert_new_line)) - { - var editorOperations = _editorOperationsFactoryService.GetEditorOperations(args.TextView); - editorOperations.InsertNewLine(); + if (args.TextView.Selection.SelectedSpans.Count > 0) + { + var selectedSpan = args.TextView.Selection + .GetSnapshotSpansOnBuffer(args.SubjectBuffer) + .FirstOrNull(); - CompleteComment(args.SubjectBuffer, args.TextView, InsertOnEnterTyped, CancellationToken.None); + originalPosition = selectedSpan != null + ? selectedSpan.Value.Start + : args.TextView.GetCaretPoint(args.SubjectBuffer) ?? -1; + } - // Since we're wrapping the ENTER key undo transaction, we always complete - // the transaction -- even if we didn't generate anything. - transaction.Complete(); - } + if (originalPosition < 0) + { + return false; + } - return true; + if (!CurrentLineStartsWithExteriorTrivia(args.SubjectBuffer, originalPosition, context.OperationContext.UserCancellationToken)) + { + return false; } - public CommandState GetCommandState(InsertCommentCommandArgs args) + // According to JasonMal, the text undo history is associated with the surface buffer + // in projection buffer scenarios, so the following line's usage of the surface buffer + // is correct. + using (var transaction = _undoHistoryRegistry.GetHistory(args.TextView.TextBuffer).CreateTransaction(EditorFeaturesResources.Insert_new_line)) { - var caretPosition = args.TextView.GetCaretPoint(args.SubjectBuffer) ?? -1; - if (caretPosition < 0) - { - return CommandState.Unavailable; - } + var editorOperations = _editorOperationsFactoryService.GetEditorOperations(args.TextView); + editorOperations.InsertNewLine(); - var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return CommandState.Unavailable; - } + CompleteComment(args.SubjectBuffer, args.TextView, InsertOnEnterTyped, CancellationToken.None); - var service = document.GetRequiredLanguageService(); + // Since we're wrapping the ENTER key undo transaction, we always complete + // the transaction -- even if we didn't generate anything. + transaction.Complete(); + } - var isValidTargetMember = false; - _uiThreadOperationExecutor.Execute("IntelliSense", defaultDescription: "", allowCancellation: true, showProgress: false, action: c => - { - var syntaxTree = document.GetRequiredSyntaxTreeSynchronously(c.UserCancellationToken); - var text = syntaxTree.GetText(c.UserCancellationToken); - isValidTargetMember = service.IsValidTargetMember(syntaxTree, text, caretPosition, c.UserCancellationToken); - }); - - return isValidTargetMember - ? CommandState.Available - : CommandState.Unavailable; + return true; + } + + public CommandState GetCommandState(InsertCommentCommandArgs args) + { + var caretPosition = args.TextView.GetCaretPoint(args.SubjectBuffer) ?? -1; + if (caretPosition < 0) + { + return CommandState.Unavailable; } - public bool ExecuteCommand(InsertCommentCommandArgs args, CommandExecutionContext context) + var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) { - using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Inserting_documentation_comment)) - { - return CompleteComment(args.SubjectBuffer, args.TextView, InsertOnCommandInvoke, context.OperationContext.UserCancellationToken); - } + return CommandState.Unavailable; } - public CommandState GetCommandState(OpenLineAboveCommandArgs args, Func nextHandler) - => nextHandler(); + var service = document.GetRequiredLanguageService(); - public void ExecuteCommand(OpenLineAboveCommandArgs args, Action nextHandler, CommandExecutionContext context) + var isValidTargetMember = false; + _uiThreadOperationExecutor.Execute("IntelliSense", defaultDescription: "", allowCancellation: true, showProgress: false, action: c => { - // Check to see if the current line starts with exterior trivia. If so, we'll take over. - // If not, let the nextHandler run. - - var subjectBuffer = args.SubjectBuffer; - var caretPosition = args.TextView.GetCaretPoint(subjectBuffer) ?? -1; - if (caretPosition < 0) - { - nextHandler(); - return; - } + var syntaxTree = document.GetRequiredSyntaxTreeSynchronously(c.UserCancellationToken); + var text = syntaxTree.GetText(c.UserCancellationToken); + isValidTargetMember = service.IsValidTargetMember(syntaxTree, text, caretPosition, c.UserCancellationToken); + }); + + return isValidTargetMember + ? CommandState.Available + : CommandState.Unavailable; + } - if (!CurrentLineStartsWithExteriorTrivia(subjectBuffer, caretPosition, context.OperationContext.UserCancellationToken)) - { - nextHandler(); - return; - } + public bool ExecuteCommand(InsertCommentCommandArgs args, CommandExecutionContext context) + { + using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Inserting_documentation_comment)) + { + return CompleteComment(args.SubjectBuffer, args.TextView, InsertOnCommandInvoke, context.OperationContext.UserCancellationToken); + } + } - // Allow nextHandler() to run and then insert exterior trivia if necessary. - nextHandler(); + public CommandState GetCommandState(OpenLineAboveCommandArgs args, Func nextHandler) + => nextHandler(); - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return; - } + public void ExecuteCommand(OpenLineAboveCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + // Check to see if the current line starts with exterior trivia. If so, we'll take over. + // If not, let the nextHandler run. - var service = document.GetRequiredLanguageService(); + var subjectBuffer = args.SubjectBuffer; + var caretPosition = args.TextView.GetCaretPoint(subjectBuffer) ?? -1; + if (caretPosition < 0) + { + nextHandler(); + return; + } - InsertExteriorTriviaIfNeeded(service, args.TextView, subjectBuffer, context.OperationContext.UserCancellationToken); + if (!CurrentLineStartsWithExteriorTrivia(subjectBuffer, caretPosition, context.OperationContext.UserCancellationToken)) + { + nextHandler(); + return; } - public CommandState GetCommandState(OpenLineBelowCommandArgs args, Func nextHandler) - => nextHandler(); + // Allow nextHandler() to run and then insert exterior trivia if necessary. + nextHandler(); - public void ExecuteCommand(OpenLineBelowCommandArgs args, Action nextHandler, CommandExecutionContext context) + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) { - // Check to see if the current line starts with exterior trivia. If so, we'll take over. - // If not, let the nextHandler run. + return; + } - var subjectBuffer = args.SubjectBuffer; - var caretPosition = args.TextView.GetCaretPoint(subjectBuffer) ?? -1; - if (caretPosition < 0) - { - nextHandler(); - return; - } + var service = document.GetRequiredLanguageService(); - if (!CurrentLineStartsWithExteriorTrivia(subjectBuffer, caretPosition, context.OperationContext.UserCancellationToken)) - { - nextHandler(); - return; - } + InsertExteriorTriviaIfNeeded(service, args.TextView, subjectBuffer, context.OperationContext.UserCancellationToken); + } - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return; - } + public CommandState GetCommandState(OpenLineBelowCommandArgs args, Func nextHandler) + => nextHandler(); - var service = document.GetRequiredLanguageService(); + public void ExecuteCommand(OpenLineBelowCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + // Check to see if the current line starts with exterior trivia. If so, we'll take over. + // If not, let the nextHandler run. - // Allow nextHandler() to run and the insert exterior trivia if necessary. + var subjectBuffer = args.SubjectBuffer; + var caretPosition = args.TextView.GetCaretPoint(subjectBuffer) ?? -1; + if (caretPosition < 0) + { nextHandler(); + return; + } - InsertExteriorTriviaIfNeeded(service, args.TextView, subjectBuffer, context.OperationContext.UserCancellationToken); + if (!CurrentLineStartsWithExteriorTrivia(subjectBuffer, caretPosition, context.OperationContext.UserCancellationToken)) + { + nextHandler(); + return; } - private void InsertExteriorTriviaIfNeeded(IDocumentationCommentSnippetService service, ITextView textView, ITextBuffer subjectBuffer, CancellationToken cancellationToken) + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) { - var caretPosition = textView.GetCaretPoint(subjectBuffer) ?? -1; - if (caretPosition < 0) - { - return; - } + return; + } - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return; - } + var service = document.GetRequiredLanguageService(); - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + // Allow nextHandler() to run and the insert exterior trivia if necessary. + nextHandler(); - // We only insert exterior trivia if the current line does not start with exterior trivia - // and the previous line does. + InsertExteriorTriviaIfNeeded(service, args.TextView, subjectBuffer, context.OperationContext.UserCancellationToken); + } - var currentLine = parsedDocument.Text.Lines.GetLineFromPosition(caretPosition); - if (currentLine.LineNumber <= 0) - { - return; - } + private void InsertExteriorTriviaIfNeeded(IDocumentationCommentSnippetService service, ITextView textView, ITextBuffer subjectBuffer, CancellationToken cancellationToken) + { + var caretPosition = textView.GetCaretPoint(subjectBuffer) ?? -1; + if (caretPosition < 0) + { + return; + } - var previousLine = parsedDocument.Text.Lines[currentLine.LineNumber - 1]; + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + { + return; + } - if (LineStartsWithExteriorTrivia(currentLine) || !LineStartsWithExteriorTrivia(previousLine)) - { - return; - } + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - var options = subjectBuffer.GetDocumentationCommentOptions(_editorOptionsService, document.Project.Services); + // We only insert exterior trivia if the current line does not start with exterior trivia + // and the previous line does. - var snippet = service.GetDocumentationCommentSnippetFromPreviousLine(options, currentLine, previousLine); - if (snippet != null) - { - ApplySnippet(snippet, subjectBuffer, textView); - } + var currentLine = parsedDocument.Text.Lines.GetLineFromPosition(caretPosition); + if (currentLine.LineNumber <= 0) + { + return; } - private bool CurrentLineStartsWithExteriorTrivia(ITextBuffer subjectBuffer, int position, CancellationToken cancellationToken) + var previousLine = parsedDocument.Text.Lines[currentLine.LineNumber - 1]; + + if (LineStartsWithExteriorTrivia(currentLine) || !LineStartsWithExteriorTrivia(previousLine)) { - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return false; - } + return; + } - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - var currentLine = parsedDocument.Text.Lines.GetLineFromPosition(position); + var options = subjectBuffer.GetDocumentationCommentOptions(_editorOptionsService, document.Project.Services); - return LineStartsWithExteriorTrivia(currentLine); + var snippet = service.GetDocumentationCommentSnippetFromPreviousLine(options, currentLine, previousLine); + if (snippet != null) + { + ApplySnippet(snippet, subjectBuffer, textView); } + } - private bool LineStartsWithExteriorTrivia(TextLine line) + private bool CurrentLineStartsWithExteriorTrivia(ITextBuffer subjectBuffer, int position, CancellationToken cancellationToken) + { + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) { - var lineText = line.ToString(); + return false; + } - var lineOffset = lineText.GetFirstNonWhitespaceOffset() ?? -1; - if (lineOffset < 0) - { - return false; - } + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + var currentLine = parsedDocument.Text.Lines.GetLineFromPosition(position); + + return LineStartsWithExteriorTrivia(currentLine); + } - return string.CompareOrdinal(lineText, lineOffset, ExteriorTriviaText, 0, ExteriorTriviaText.Length) == 0; + private bool LineStartsWithExteriorTrivia(TextLine line) + { + var lineText = line.ToString(); + + var lineOffset = lineText.GetFirstNonWhitespaceOffset() ?? -1; + if (lineOffset < 0) + { + return false; } + + return string.CompareOrdinal(lineText, lineOffset, ExteriorTriviaText, 0, ExteriorTriviaText.Length) == 0; } } diff --git a/src/EditorFeatures/Core/DocumentationComments/AbstractXmlTagCompletionCommandHandler.cs b/src/EditorFeatures/Core/DocumentationComments/AbstractXmlTagCompletionCommandHandler.cs index 16725917127cc..970bc73271640 100644 --- a/src/EditorFeatures/Core/DocumentationComments/AbstractXmlTagCompletionCommandHandler.cs +++ b/src/EditorFeatures/Core/DocumentationComments/AbstractXmlTagCompletionCommandHandler.cs @@ -15,230 +15,229 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.DocumentationComments +namespace Microsoft.CodeAnalysis.DocumentationComments; + +internal abstract class AbstractXmlTagCompletionCommandHandler< + TXmlNameSyntax, + TXmlTextSyntax, + TXmlElementSyntax, + TXmlElementStartTagSyntax, + TXmlElementEndTagSyntax, + TDocumentationCommentTriviaSyntax> + (ITextUndoHistoryRegistry undoHistory) : IChainedCommandHandler + where TXmlNameSyntax : SyntaxNode + where TXmlTextSyntax : SyntaxNode + where TXmlElementSyntax : SyntaxNode + where TXmlElementStartTagSyntax : SyntaxNode + where TXmlElementEndTagSyntax : SyntaxNode + where TDocumentationCommentTriviaSyntax : SyntaxNode { - internal abstract class AbstractXmlTagCompletionCommandHandler< - TXmlNameSyntax, - TXmlTextSyntax, - TXmlElementSyntax, - TXmlElementStartTagSyntax, - TXmlElementEndTagSyntax, - TDocumentationCommentTriviaSyntax> - (ITextUndoHistoryRegistry undoHistory) : IChainedCommandHandler - where TXmlNameSyntax : SyntaxNode - where TXmlTextSyntax : SyntaxNode - where TXmlElementSyntax : SyntaxNode - where TXmlElementStartTagSyntax : SyntaxNode - where TXmlElementEndTagSyntax : SyntaxNode - where TDocumentationCommentTriviaSyntax : SyntaxNode - { - private readonly ITextUndoHistoryRegistry _undoHistory = undoHistory; + private readonly ITextUndoHistoryRegistry _undoHistory = undoHistory; - public string DisplayName => EditorFeaturesResources.XML_End_Tag_Completion; + public string DisplayName => EditorFeaturesResources.XML_End_Tag_Completion; - protected abstract TXmlElementStartTagSyntax GetStartTag(TXmlElementSyntax xmlElement); - protected abstract TXmlElementEndTagSyntax GetEndTag(TXmlElementSyntax xmlElement); - protected abstract TXmlNameSyntax GetName(TXmlElementStartTagSyntax startTag); - protected abstract TXmlNameSyntax GetName(TXmlElementEndTagSyntax endTag); - protected abstract SyntaxToken GetLocalName(TXmlNameSyntax name); + protected abstract TXmlElementStartTagSyntax GetStartTag(TXmlElementSyntax xmlElement); + protected abstract TXmlElementEndTagSyntax GetEndTag(TXmlElementSyntax xmlElement); + protected abstract TXmlNameSyntax GetName(TXmlElementStartTagSyntax startTag); + protected abstract TXmlNameSyntax GetName(TXmlElementEndTagSyntax endTag); + protected abstract SyntaxToken GetLocalName(TXmlNameSyntax name); - public CommandState GetCommandState(TypeCharCommandArgs args, Func nextHandler) - => nextHandler(); + public CommandState GetCommandState(TypeCharCommandArgs args, Func nextHandler) + => nextHandler(); - public void ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context) - { - // Ensure completion and any other buffer edits happen first. - nextHandler(); + public void ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + // Ensure completion and any other buffer edits happen first. + nextHandler(); - var cancellationToken = context.OperationContext.UserCancellationToken; - if (cancellationToken.IsCancellationRequested) - return; + var cancellationToken = context.OperationContext.UserCancellationToken; + if (cancellationToken.IsCancellationRequested) + return; - try - { - ExecuteCommandWorker(args, context); - } - catch (OperationCanceledException) - { - // According to Editor command handler API guidelines, it's best if we return early if cancellation - // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already - // calling nextHandler(). - } + try + { + ExecuteCommandWorker(args, context); } + catch (OperationCanceledException) + { + // According to Editor command handler API guidelines, it's best if we return early if cancellation + // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already + // calling nextHandler(). + } + } - private void ExecuteCommandWorker(TypeCharCommandArgs args, CommandExecutionContext context) + private void ExecuteCommandWorker(TypeCharCommandArgs args, CommandExecutionContext context) + { + if (args.TypedChar is not '>' and not '/') + return; + + using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Completing_Tag)) { - if (args.TypedChar is not '>' and not '/') + var buffer = args.SubjectBuffer; + + var document = buffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) return; - using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Completing_Tag)) - { - var buffer = args.SubjectBuffer; + // We actually want the caret position after any operations + var position = args.TextView.GetCaretPoint(args.SubjectBuffer); - var document = buffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return; + // No caret position? No edit! + if (!position.HasValue) + return; - // We actually want the caret position after any operations - var position = args.TextView.GetCaretPoint(args.SubjectBuffer); + TryCompleteTag(args.TextView, args.SubjectBuffer, document, position.Value, context.OperationContext.UserCancellationToken); + } + } - // No caret position? No edit! - if (!position.HasValue) - return; + protected void InsertTextAndMoveCaret(ITextView textView, ITextBuffer subjectBuffer, SnapshotPoint position, string insertionText, int? finalCaretPosition) + { + using var transaction = _undoHistory.GetHistory(textView.TextBuffer).CreateTransaction("XmlTagCompletion"); - TryCompleteTag(args.TextView, args.SubjectBuffer, document, position.Value, context.OperationContext.UserCancellationToken); - } - } + subjectBuffer.Insert(position, insertionText); - protected void InsertTextAndMoveCaret(ITextView textView, ITextBuffer subjectBuffer, SnapshotPoint position, string insertionText, int? finalCaretPosition) + if (finalCaretPosition.HasValue) { - using var transaction = _undoHistory.GetHistory(textView.TextBuffer).CreateTransaction("XmlTagCompletion"); + var point = subjectBuffer.CurrentSnapshot.GetPoint(finalCaretPosition.Value); + textView.TryMoveCaretToAndEnsureVisible(point); + } - subjectBuffer.Insert(position, insertionText); + transaction.Complete(); + } - if (finalCaretPosition.HasValue) - { - var point = subjectBuffer.CurrentSnapshot.GetPoint(finalCaretPosition.Value); - textView.TryMoveCaretToAndEnsureVisible(point); - } + private SyntaxToken GetLocalName(TXmlElementStartTagSyntax startTag) + => GetLocalName(GetName(startTag)); - transaction.Complete(); - } + private SyntaxToken GetLocalName(TXmlElementEndTagSyntax startTag) + => GetLocalName(GetName(startTag)); - private SyntaxToken GetLocalName(TXmlElementStartTagSyntax startTag) - => GetLocalName(GetName(startTag)); + private void TryCompleteTag(ITextView textView, ITextBuffer subjectBuffer, Document document, SnapshotPoint position, CancellationToken cancellationToken) + { + var tree = document.GetRequiredSyntaxTreeSynchronously(cancellationToken); + var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDocumentationComments: true); + var syntaxFacts = document.GetRequiredLanguageService(); + var syntaxKinds = syntaxFacts.SyntaxKinds; - private SyntaxToken GetLocalName(TXmlElementEndTagSyntax startTag) - => GetLocalName(GetName(startTag)); + var parentTrivia = token.GetAncestor(); + if (parentTrivia is null) + return; - private void TryCompleteTag(ITextView textView, ITextBuffer subjectBuffer, Document document, SnapshotPoint position, CancellationToken cancellationToken) + if (token.RawKind == syntaxKinds.GreaterThanToken) { - var tree = document.GetRequiredSyntaxTreeSynchronously(cancellationToken); - var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDocumentationComments: true); - var syntaxFacts = document.GetRequiredLanguageService(); - var syntaxKinds = syntaxFacts.SyntaxKinds; - - var parentTrivia = token.GetAncestor(); - if (parentTrivia is null) + if (token.Parent is not TXmlElementStartTagSyntax parentStartTag || + parentStartTag.Parent is not TXmlElementSyntax parentElement) + { return; + } - if (token.RawKind == syntaxKinds.GreaterThanToken) + // Slightly special case: + // If we already have a matching end tag and we're parented by + // an xml element with the same start tag and a missing/non-matching end tag, + // do completion anyway. Generally, if this is the case, we have to walk + // up the parent elements until we find an unmatched start tag. + + if (GetLocalName(parentStartTag).ValueText.Length > 0 && HasMatchingEndTag(parentElement)) { - if (token.Parent is not TXmlElementStartTagSyntax parentStartTag || - parentStartTag.Parent is not TXmlElementSyntax parentElement) + if (HasUnmatchedIdenticalParent(parentElement)) { + InsertTextAndMoveCaret(textView, subjectBuffer, position, "", position); return; } + } - // Slightly special case: - // If we already have a matching end tag and we're parented by - // an xml element with the same start tag and a missing/non-matching end tag, - // do completion anyway. Generally, if this is the case, we have to walk - // up the parent elements until we find an unmatched start tag. - - if (GetLocalName(parentStartTag).ValueText.Length > 0 && HasMatchingEndTag(parentElement)) - { - if (HasUnmatchedIdenticalParent(parentElement)) - { - InsertTextAndMoveCaret(textView, subjectBuffer, position, "", position); - return; - } - } + CheckNameAndInsertText(textView, subjectBuffer, position, parentElement, position.Position, ""); + } + else if (token.RawKind == syntaxKinds.LessThanSlashToken) + { + // /// + // /// + // We need to check for non-trivia XML text tokens after $$ that match the expected end tag text. - CheckNameAndInsertText(textView, subjectBuffer, position, parentElement, position.Position, ""); - } - else if (token.RawKind == syntaxKinds.LessThanSlashToken) + if (token.Parent is TXmlElementEndTagSyntax { Parent: TXmlElementSyntax parentElement }) { - // /// - // /// - // We need to check for non-trivia XML text tokens after $$ that match the expected end tag text. - - if (token.Parent is TXmlElementEndTagSyntax { Parent: TXmlElementSyntax parentElement }) + var startTag = GetStartTag(parentElement); + if (startTag != null && + !HasFollowingEndTagTrivia(startTag, token)) { - var startTag = GetStartTag(parentElement); - if (startTag != null && - !HasFollowingEndTagTrivia(startTag, token)) - { - CheckNameAndInsertText(textView, subjectBuffer, position, parentElement, null, "{0}>"); - } + CheckNameAndInsertText(textView, subjectBuffer, position, parentElement, null, "{0}>"); } } } + } - private bool HasFollowingEndTagTrivia( - TXmlElementStartTagSyntax startTag, - SyntaxToken lessThanSlashToken) - { - var tagName = GetLocalName(startTag).ValueText; - var expectedEndTagText = ""; - - var token = lessThanSlashToken.GetNextToken(includeDocumentationComments: true); - while (token.Parent is TXmlTextSyntax) - { - if (token.ValueText == expectedEndTagText) - return true; - - token = token.GetNextToken(includeDocumentationComments: true); - } + private bool HasFollowingEndTagTrivia( + TXmlElementStartTagSyntax startTag, + SyntaxToken lessThanSlashToken) + { + var tagName = GetLocalName(startTag).ValueText; + var expectedEndTagText = ""; - if (token.Parent is TXmlElementEndTagSyntax endTag && - GetLocalName(endTag).ValueText == tagName) - { + var token = lessThanSlashToken.GetNextToken(includeDocumentationComments: true); + while (token.Parent is TXmlTextSyntax) + { + if (token.ValueText == expectedEndTagText) return true; - } - return false; + token = token.GetNextToken(includeDocumentationComments: true); } - private bool HasUnmatchedIdenticalParent(TXmlElementSyntax parentElement) + if (token.Parent is TXmlElementEndTagSyntax endTag && + GetLocalName(endTag).ValueText == tagName) { - if (parentElement.Parent is TXmlElementSyntax grandParentElement) + return true; + } + + return false; + } + + private bool HasUnmatchedIdenticalParent(TXmlElementSyntax parentElement) + { + if (parentElement.Parent is TXmlElementSyntax grandParentElement) + { + var parentStartTag = GetStartTag(parentElement); + if (GetLocalName(GetStartTag(grandParentElement)).ValueText == GetLocalName(parentStartTag).ValueText) { - var parentStartTag = GetStartTag(parentElement); - if (GetLocalName(GetStartTag(grandParentElement)).ValueText == GetLocalName(parentStartTag).ValueText) + if (HasMatchingEndTag(grandParentElement)) { - if (HasMatchingEndTag(grandParentElement)) - { - return HasUnmatchedIdenticalParent(grandParentElement); - } - - return true; + return HasUnmatchedIdenticalParent(grandParentElement); } - } - return false; + return true; + } } - private bool HasMatchingEndTag(TXmlElementSyntax parentElement) - { - var startTag = GetStartTag(parentElement); - var endTag = GetEndTag(parentElement); - return endTag != null && - !endTag.IsMissing && - GetLocalName(endTag).ValueText == GetLocalName(startTag).ValueText; - } + return false; + } - private void CheckNameAndInsertText( - ITextView textView, - ITextBuffer subjectBuffer, - SnapshotPoint position, - TXmlElementSyntax parentElement, - int? finalCaretPosition, - string formatString) - { - var startTag = GetStartTag(parentElement); - var endTag = GetEndTag(parentElement); - if (startTag is null || endTag is null) - return; + private bool HasMatchingEndTag(TXmlElementSyntax parentElement) + { + var startTag = GetStartTag(parentElement); + var endTag = GetEndTag(parentElement); + return endTag != null && + !endTag.IsMissing && + GetLocalName(endTag).ValueText == GetLocalName(startTag).ValueText; + } - var elementName = GetLocalName(startTag).ValueText; + private void CheckNameAndInsertText( + ITextView textView, + ITextBuffer subjectBuffer, + SnapshotPoint position, + TXmlElementSyntax parentElement, + int? finalCaretPosition, + string formatString) + { + var startTag = GetStartTag(parentElement); + var endTag = GetEndTag(parentElement); + if (startTag is null || endTag is null) + return; - if (elementName.Length > 0 && - GetLocalName(endTag).ValueText != elementName) - { - InsertTextAndMoveCaret(textView, subjectBuffer, position, string.Format(formatString, elementName), finalCaretPosition); - } + var elementName = GetLocalName(startTag).ValueText; + + if (elementName.Length > 0 && + GetLocalName(endTag).ValueText != elementName) + { + InsertTextAndMoveCaret(textView, subjectBuffer, position, string.Format(formatString, elementName), finalCaretPosition); } } } diff --git a/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTaggerProvider.EventSource.cs b/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTaggerProvider.EventSource.cs index f6d935db8068e..50a967969f2f8 100644 --- a/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTaggerProvider.EventSource.cs +++ b/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTaggerProvider.EventSource.cs @@ -5,30 +5,29 @@ using Microsoft.CodeAnalysis.Editor.Shared.Tagging; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal partial class ActiveStatementTaggerProvider { - internal partial class ActiveStatementTaggerProvider + private sealed class EventSource(ITextBuffer subjectBuffer) : AbstractWorkspaceTrackingTaggerEventSource(subjectBuffer) { - private sealed class EventSource(ITextBuffer subjectBuffer) : AbstractWorkspaceTrackingTaggerEventSource(subjectBuffer) + protected override void ConnectToWorkspace(Workspace workspace) { - protected override void ConnectToWorkspace(Workspace workspace) + var trackingService = workspace.Services.GetService(); + if (trackingService != null) { - var trackingService = workspace.Services.GetService(); - if (trackingService != null) - { - trackingService.TrackingChanged += RaiseChanged; - RaiseChanged(); - } + trackingService.TrackingChanged += RaiseChanged; + RaiseChanged(); } + } - protected override void DisconnectFromWorkspace(Workspace workspace) + protected override void DisconnectFromWorkspace(Workspace workspace) + { + var trackingService = workspace.Services.GetService(); + if (trackingService != null) { - var trackingService = workspace.Services.GetService(); - if (trackingService != null) - { - trackingService.TrackingChanged -= RaiseChanged; - RaiseChanged(); - } + trackingService.TrackingChanged -= RaiseChanged; + RaiseChanged(); } } } diff --git a/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTaggerProvider.cs b/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTaggerProvider.cs index f4af3a986de56..c5da2a5b1f682 100644 --- a/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTaggerProvider.cs +++ b/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTaggerProvider.cs @@ -25,83 +25,82 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Tagger for active statements. Active statements are only tracked for langauges that support EnC (C#, VB). +/// +[Export(typeof(ITaggerProvider))] +[TagType(typeof(ActiveStatementTag))] +[ContentType(ContentTypeNames.CSharpContentType)] +[ContentType(ContentTypeNames.VisualBasicContentType)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal partial class ActiveStatementTaggerProvider( + IThreadingContext threadingContext, + IGlobalOptionService globalOptions, + [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) : AsynchronousTaggerProvider(threadingContext, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.Classification)) { - /// - /// Tagger for active statements. Active statements are only tracked for langauges that support EnC (C#, VB). - /// - [Export(typeof(ITaggerProvider))] - [TagType(typeof(ActiveStatementTag))] - [ContentType(ContentTypeNames.CSharpContentType)] - [ContentType(ContentTypeNames.VisualBasicContentType)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal partial class ActiveStatementTaggerProvider( - IThreadingContext threadingContext, - IGlobalOptionService globalOptions, - [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) : AsynchronousTaggerProvider(threadingContext, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.Classification)) + // We want to track text changes so that we can try to only reclassify a method body if + // all edits were contained within one. + protected override TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.TrackTextChanges; + + protected override TaggerDelay EventChangeDelay => TaggerDelay.NearImmediate; + + protected override ITaggerEventSource CreateEventSource(ITextView? textView, ITextBuffer subjectBuffer) { - // We want to track text changes so that we can try to only reclassify a method body if - // all edits were contained within one. - protected override TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.TrackTextChanges; + this.ThreadingContext.ThrowIfNotOnUIThread(); - protected override TaggerDelay EventChangeDelay => TaggerDelay.NearImmediate; + return TaggerEventSources.Compose( + new EventSource(subjectBuffer), + TaggerEventSources.OnTextChanged(subjectBuffer), + TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer)); + } - protected override ITaggerEventSource CreateEventSource(ITextView? textView, ITextBuffer subjectBuffer) - { - this.ThreadingContext.ThrowIfNotOnUIThread(); + protected override async Task ProduceTagsAsync( + TaggerContext context, CancellationToken cancellationToken) + { + Debug.Assert(context.SpansToTag.IsSingle()); - return TaggerEventSources.Compose( - new EventSource(subjectBuffer), - TaggerEventSources.OnTextChanged(subjectBuffer), - TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer)); + var spanToTag = context.SpansToTag.Single(); + + var document = spanToTag.Document; + if (document == null) + { + return; } - protected override async Task ProduceTagsAsync( - TaggerContext context, CancellationToken cancellationToken) + var activeStatementTrackingService = document.Project.Solution.Services.GetService(); + if (activeStatementTrackingService == null) { - Debug.Assert(context.SpansToTag.IsSingle()); + return; + } - var spanToTag = context.SpansToTag.Single(); + var snapshot = spanToTag.SnapshotSpan.Snapshot; - var document = spanToTag.Document; - if (document == null) - { - return; - } - - var activeStatementTrackingService = document.Project.Solution.Services.GetService(); - if (activeStatementTrackingService == null) + var activeStatementSpans = await activeStatementTrackingService.GetAdjustedTrackingSpansAsync(document, snapshot, cancellationToken).ConfigureAwait(false); + foreach (var activeStatementSpan in activeStatementSpans) + { + if (activeStatementSpan.IsLeaf) { - return; + continue; } - var snapshot = spanToTag.SnapshotSpan.Snapshot; - - var activeStatementSpans = await activeStatementTrackingService.GetAdjustedTrackingSpansAsync(document, snapshot, cancellationToken).ConfigureAwait(false); - foreach (var activeStatementSpan in activeStatementSpans) + var snapshotSpan = activeStatementSpan.Span.GetSpan(snapshot); + if (snapshotSpan.OverlapsWith(spanToTag.SnapshotSpan)) { - if (activeStatementSpan.IsLeaf) - { - continue; - } - - var snapshotSpan = activeStatementSpan.Span.GetSpan(snapshot); - if (snapshotSpan.OverlapsWith(spanToTag.SnapshotSpan)) - { - context.AddTag(new TagSpan(snapshotSpan, ActiveStatementTag.Instance)); - } + context.AddTag(new TagSpan(snapshotSpan, ActiveStatementTag.Instance)); } - - // Let the context know that this was the span we actually tried to tag. - context.SetSpansTagged([spanToTag.SnapshotSpan]); } - protected override bool TagEquals(ITextMarkerTag tag1, ITextMarkerTag tag2) - { - Contract.ThrowIfFalse(tag1 == tag2, "ActiveStatementTag is a supposed to be a singleton"); - return true; - } + // Let the context know that this was the span we actually tried to tag. + context.SetSpansTagged([spanToTag.SnapshotSpan]); + } + + protected override bool TagEquals(ITextMarkerTag tag1, ITextMarkerTag tag2) + { + Contract.ThrowIfFalse(tag1 == tag2, "ActiveStatementTag is a supposed to be a singleton"); + return true; } } diff --git a/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTrackingService.cs b/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTrackingService.cs index d5bc84697b78a..3ba759484c71e 100644 --- a/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTrackingService.cs +++ b/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTrackingService.cs @@ -24,356 +24,355 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Tracks active statements for the debugger during an edit session. +/// +/// +/// An active statement is a source statement that occurs in a stack trace of a thread. +/// Active statements are visualized via a gray marker in the text editor. +/// +internal sealed class ActiveStatementTrackingService(Workspace workspace, IAsynchronousOperationListener listener) : IActiveStatementTrackingService { + [ExportWorkspaceServiceFactory(typeof(IActiveStatementTrackingService), ServiceLayer.Editor), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + internal sealed class Factory(IAsynchronousOperationListenerProvider listenerProvider) : IWorkspaceServiceFactory + { + private readonly IAsynchronousOperationListenerProvider _listenerProvider = listenerProvider; + + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => new ActiveStatementTrackingService(workspaceServices.Workspace, _listenerProvider.GetListener(FeatureAttribute.EditAndContinue)); + } + + private readonly IAsynchronousOperationListener _listener = listener; + + private TrackingSession? _session; + private readonly Workspace _workspace = workspace; + /// - /// Tracks active statements for the debugger during an edit session. + /// Raised whenever span tracking starts or ends. /// - /// - /// An active statement is a source statement that occurs in a stack trace of a thread. - /// Active statements are visualized via a gray marker in the text editor. - /// - internal sealed class ActiveStatementTrackingService(Workspace workspace, IAsynchronousOperationListener listener) : IActiveStatementTrackingService + public event Action? TrackingChanged; + + public void StartTracking(Solution solution, IActiveStatementSpanProvider spanProvider) { - [ExportWorkspaceServiceFactory(typeof(IActiveStatementTrackingService), ServiceLayer.Editor), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class Factory(IAsynchronousOperationListenerProvider listenerProvider) : IWorkspaceServiceFactory + var newSession = new TrackingSession(_workspace, spanProvider); + if (Interlocked.CompareExchange(ref _session, newSession, null) != null) { - private readonly IAsynchronousOperationListenerProvider _listenerProvider = listenerProvider; - - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new ActiveStatementTrackingService(workspaceServices.Workspace, _listenerProvider.GetListener(FeatureAttribute.EditAndContinue)); + newSession.EndTracking(); + Contract.Fail("Can only track active statements for a single edit session."); } - private readonly IAsynchronousOperationListener _listener = listener; + var token = _listener.BeginAsyncOperation(nameof(ActiveStatementTrackingService)); + _ = newSession.TrackActiveSpansAsync(solution).CompletesAsyncOperation(token); - private TrackingSession? _session; - private readonly Workspace _workspace = workspace; + TrackingChanged?.Invoke(); + } - /// - /// Raised whenever span tracking starts or ends. - /// - public event Action? TrackingChanged; + public void EndTracking() + { + var session = Interlocked.Exchange(ref _session, null); + Contract.ThrowIfNull(session, "Active statement tracking not started."); + session.EndTracking(); - public void StartTracking(Solution solution, IActiveStatementSpanProvider spanProvider) - { - var newSession = new TrackingSession(_workspace, spanProvider); - if (Interlocked.CompareExchange(ref _session, newSession, null) != null) - { - newSession.EndTracking(); - Contract.Fail("Can only track active statements for a single edit session."); - } + TrackingChanged?.Invoke(); + } - var token = _listener.BeginAsyncOperation(nameof(ActiveStatementTrackingService)); - _ = newSession.TrackActiveSpansAsync(solution).CompletesAsyncOperation(token); + public ValueTask> GetSpansAsync(Solution solution, DocumentId? documentId, string filePath, CancellationToken cancellationToken) + => _session?.GetSpansAsync(solution, documentId, filePath, cancellationToken) ?? new([]); - TrackingChanged?.Invoke(); - } + public ValueTask> GetAdjustedTrackingSpansAsync(TextDocument document, ITextSnapshot snapshot, CancellationToken cancellationToken) + => _session?.GetAdjustedTrackingSpansAsync(document, snapshot, cancellationToken) ?? new([]); - public void EndTracking() - { - var session = Interlocked.Exchange(ref _session, null); - Contract.ThrowIfNull(session, "Active statement tracking not started."); - session.EndTracking(); + // internal for testing + internal sealed class TrackingSession + { + private readonly Workspace _workspace; + private readonly CancellationTokenSource _cancellationSource = new(); + private readonly IActiveStatementSpanProvider _spanProvider; + private readonly ICompileTimeSolutionProvider _compileTimeSolutionProvider; - TrackingChanged?.Invoke(); - } + #region lock(_trackingSpans) - public ValueTask> GetSpansAsync(Solution solution, DocumentId? documentId, string filePath, CancellationToken cancellationToken) - => _session?.GetSpansAsync(solution, documentId, filePath, cancellationToken) ?? new([]); + /// + /// Spans that are tracking active statements contained in the document of given file path. + /// For each document the array contains spans for all active statements present in the file + /// (even if they have been deleted, in which case the spans are empty). + /// + private readonly Dictionary> _trackingSpans = []; - public ValueTask> GetAdjustedTrackingSpansAsync(TextDocument document, ITextSnapshot snapshot, CancellationToken cancellationToken) - => _session?.GetAdjustedTrackingSpansAsync(document, snapshot, cancellationToken) ?? new([]); + #endregion - // internal for testing - internal sealed class TrackingSession + public TrackingSession(Workspace workspace, IActiveStatementSpanProvider spanProvider) { - private readonly Workspace _workspace; - private readonly CancellationTokenSource _cancellationSource = new(); - private readonly IActiveStatementSpanProvider _spanProvider; - private readonly ICompileTimeSolutionProvider _compileTimeSolutionProvider; + _workspace = workspace; + _spanProvider = spanProvider; + _compileTimeSolutionProvider = workspace.Services.GetRequiredService(); - #region lock(_trackingSpans) + _workspace.DocumentOpened += DocumentOpened; + _workspace.DocumentClosed += DocumentClosed; + } - /// - /// Spans that are tracking active statements contained in the document of given file path. - /// For each document the array contains spans for all active statements present in the file - /// (even if they have been deleted, in which case the spans are empty). - /// - private readonly Dictionary> _trackingSpans = []; + internal Dictionary> Test_GetTrackingSpans() + => _trackingSpans; - #endregion + public void EndTracking() + { + _cancellationSource.Cancel(); + _cancellationSource.Dispose(); - public TrackingSession(Workspace workspace, IActiveStatementSpanProvider spanProvider) - { - _workspace = workspace; - _spanProvider = spanProvider; - _compileTimeSolutionProvider = workspace.Services.GetRequiredService(); + _workspace.DocumentOpened -= DocumentOpened; + _workspace.DocumentClosed -= DocumentClosed; - _workspace.DocumentOpened += DocumentOpened; - _workspace.DocumentClosed += DocumentClosed; + lock (_trackingSpans) + { + _trackingSpans.Clear(); } + } - internal Dictionary> Test_GetTrackingSpans() - => _trackingSpans; - - public void EndTracking() + private void DocumentClosed(object? sender, DocumentEventArgs e) + { + if (e.Document.FilePath != null) { - _cancellationSource.Cancel(); - _cancellationSource.Dispose(); - - _workspace.DocumentOpened -= DocumentOpened; - _workspace.DocumentClosed -= DocumentClosed; - lock (_trackingSpans) { - _trackingSpans.Clear(); + _trackingSpans.Remove(e.Document.FilePath); } } + } + + private void DocumentOpened(object? sender, DocumentEventArgs e) + => _ = TrackActiveSpansAsync(e.Document); - private void DocumentClosed(object? sender, DocumentEventArgs e) + private async Task TrackActiveSpansAsync(Document designTimeDocument) + { + try { - if (e.Document.FilePath != null) + var cancellationToken = _cancellationSource.Token; + + if (!designTimeDocument.DocumentState.SupportsEditAndContinue()) { - lock (_trackingSpans) - { - _trackingSpans.Remove(e.Document.FilePath); - } + return; } - } - private void DocumentOpened(object? sender, DocumentEventArgs e) - => _ = TrackActiveSpansAsync(e.Document); + var compileTimeSolution = _compileTimeSolutionProvider.GetCompileTimeSolution(designTimeDocument.Project.Solution); + var compileTimeDocument = await compileTimeSolution.GetDocumentAsync(designTimeDocument.Id, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - private async Task TrackActiveSpansAsync(Document designTimeDocument) - { - try + if (compileTimeDocument == null || !TryGetSnapshot(compileTimeDocument, out var snapshot)) { - var cancellationToken = _cancellationSource.Token; - - if (!designTimeDocument.DocumentState.SupportsEditAndContinue()) - { - return; - } + return; + } - var compileTimeSolution = _compileTimeSolutionProvider.GetCompileTimeSolution(designTimeDocument.Project.Solution); - var compileTimeDocument = await compileTimeSolution.GetDocumentAsync(designTimeDocument.Id, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + _ = await GetAdjustedTrackingSpansAsync(compileTimeDocument, snapshot, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // nop + } + catch (Exception e) when (FatalError.ReportAndCatch(e)) + { + // nop + } + } - if (compileTimeDocument == null || !TryGetSnapshot(compileTimeDocument, out var snapshot)) - { - return; - } + internal async Task TrackActiveSpansAsync(Solution solution) + { + try + { + var cancellationToken = _cancellationSource.Token; - _ = await GetAdjustedTrackingSpansAsync(compileTimeDocument, snapshot, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) + var openDocumentIds = _workspace.GetOpenDocumentIds().ToImmutableArray(); + if (openDocumentIds.Length == 0) { - // nop + return; } - catch (Exception e) when (FatalError.ReportAndCatch(e)) + + var baseActiveStatementSpans = await _spanProvider.GetBaseActiveStatementSpansAsync(solution, openDocumentIds, cancellationToken).ConfigureAwait(false); + if (baseActiveStatementSpans.IsDefault) { - // nop + // Edit session not in progress. + return; } - } - internal async Task TrackActiveSpansAsync(Solution solution) - { - try - { - var cancellationToken = _cancellationSource.Token; + Debug.Assert(openDocumentIds.Length == baseActiveStatementSpans.Length); + using var _ = ArrayBuilder.GetInstance(out var documents); - var openDocumentIds = _workspace.GetOpenDocumentIds().ToImmutableArray(); - if (openDocumentIds.Length == 0) - { - return; - } + foreach (var id in openDocumentIds) + { + // active statements may be in any document kind (#line may in theory map to analyzer config as well, no need to exclude it): + documents.Add(await solution.GetTextDocumentAsync(id, cancellationToken).ConfigureAwait(false)); + } - var baseActiveStatementSpans = await _spanProvider.GetBaseActiveStatementSpansAsync(solution, openDocumentIds, cancellationToken).ConfigureAwait(false); - if (baseActiveStatementSpans.IsDefault) + lock (_trackingSpans) + { + for (var i = 0; i < baseActiveStatementSpans.Length; i++) { - // Edit session not in progress. - return; - } - - Debug.Assert(openDocumentIds.Length == baseActiveStatementSpans.Length); - using var _ = ArrayBuilder.GetInstance(out var documents); + var document = documents[i]; + if (document?.FilePath == null) + { + // Document has been deleted, doesn't have a path or is an open design-time document (which does not exist in the compile-time solution) + continue; + } - foreach (var id in openDocumentIds) - { - // active statements may be in any document kind (#line may in theory map to analyzer config as well, no need to exclude it): - documents.Add(await solution.GetTextDocumentAsync(id, cancellationToken).ConfigureAwait(false)); - } + if (!TryGetSnapshot(document, out var snapshot)) + { + // Document is not open in an editor or a corresponding snapshot doesn't exist anymore. + continue; + } - lock (_trackingSpans) - { - for (var i = 0; i < baseActiveStatementSpans.Length; i++) + if (!_trackingSpans.ContainsKey(document.FilePath)) { - var document = documents[i]; - if (document?.FilePath == null) - { - // Document has been deleted, doesn't have a path or is an open design-time document (which does not exist in the compile-time solution) - continue; - } - - if (!TryGetSnapshot(document, out var snapshot)) - { - // Document is not open in an editor or a corresponding snapshot doesn't exist anymore. - continue; - } - - if (!_trackingSpans.ContainsKey(document.FilePath)) - { - // Create tracking spans if they have not been created for this open document yet - // (avoids race condition with DocumentOpen event handler). - _trackingSpans.Add(document.FilePath, CreateTrackingSpans(snapshot, baseActiveStatementSpans[i])); - } + // Create tracking spans if they have not been created for this open document yet + // (avoids race condition with DocumentOpen event handler). + _trackingSpans.Add(document.FilePath, CreateTrackingSpans(snapshot, baseActiveStatementSpans[i])); } } } - catch (OperationCanceledException) - { - // nop - } - catch (Exception e) when (FatalError.ReportAndCatch(e)) - { - // nop - } } + catch (OperationCanceledException) + { + // nop + } + catch (Exception e) when (FatalError.ReportAndCatch(e)) + { + // nop + } + } - private static ImmutableArray CreateTrackingSpans(ITextSnapshot snapshot, ImmutableArray activeStatementSpans) - => activeStatementSpans.SelectAsArray((span, snapshot) => ActiveStatementTrackingSpan.Create(snapshot, span), snapshot); + private static ImmutableArray CreateTrackingSpans(ITextSnapshot snapshot, ImmutableArray activeStatementSpans) + => activeStatementSpans.SelectAsArray((span, snapshot) => ActiveStatementTrackingSpan.Create(snapshot, span), snapshot); - private static ImmutableArray UpdateTrackingSpans( - ITextSnapshot snapshot, - ImmutableArray oldSpans, - ImmutableArray newSpans) - { - Debug.Assert(oldSpans.Length == newSpans.Length); + private static ImmutableArray UpdateTrackingSpans( + ITextSnapshot snapshot, + ImmutableArray oldSpans, + ImmutableArray newSpans) + { + Debug.Assert(oldSpans.Length == newSpans.Length); - ArrayBuilder? lazyBuilder = null; + ArrayBuilder? lazyBuilder = null; - for (var i = 0; i < oldSpans.Length; i++) - { - var oldSpan = oldSpans[i]; - var newSpan = newSpans[i]; + for (var i = 0; i < oldSpans.Length; i++) + { + var oldSpan = oldSpans[i]; + var newSpan = newSpans[i]; - Contract.ThrowIfFalse(oldSpan.Flags == newSpan.Flags); - Contract.ThrowIfFalse(oldSpan.Ordinal == newSpan.Ordinal); + Contract.ThrowIfFalse(oldSpan.Flags == newSpan.Flags); + Contract.ThrowIfFalse(oldSpan.Ordinal == newSpan.Ordinal); - var newTextSpan = snapshot.GetTextSpan(newSpan.LineSpan).ToSpan(); - if (oldSpan.Span.GetSpan(snapshot).Span != newTextSpan) + var newTextSpan = snapshot.GetTextSpan(newSpan.LineSpan).ToSpan(); + if (oldSpan.Span.GetSpan(snapshot).Span != newTextSpan) + { + if (lazyBuilder == null) { - if (lazyBuilder == null) - { - lazyBuilder = ArrayBuilder.GetInstance(oldSpans.Length); - lazyBuilder.AddRange(oldSpans); - } - - lazyBuilder[i] = new ActiveStatementTrackingSpan( - snapshot.CreateTrackingSpan(newTextSpan, SpanTrackingMode.EdgeExclusive), - newSpan.Ordinal, - newSpan.Flags, - newSpan.UnmappedDocumentId); + lazyBuilder = ArrayBuilder.GetInstance(oldSpans.Length); + lazyBuilder.AddRange(oldSpans); } - } - return lazyBuilder?.ToImmutableAndFree() ?? oldSpans; + lazyBuilder[i] = new ActiveStatementTrackingSpan( + snapshot.CreateTrackingSpan(newTextSpan, SpanTrackingMode.EdgeExclusive), + newSpan.Ordinal, + newSpan.Flags, + newSpan.UnmappedDocumentId); + } } - private static bool TryGetSnapshot(TextDocument document, [NotNullWhen(true)] out ITextSnapshot? snapshot) - { - if (!document.TryGetText(out var source)) - { - snapshot = null; - return false; - } + return lazyBuilder?.ToImmutableAndFree() ?? oldSpans; + } - snapshot = source.FindCorrespondingEditorTextSnapshot(); - return snapshot != null; + private static bool TryGetSnapshot(TextDocument document, [NotNullWhen(true)] out ITextSnapshot? snapshot) + { + if (!document.TryGetText(out var source)) + { + snapshot = null; + return false; } - /// - /// Returns location of the tracking spans in the specified snapshot (#line target document). - /// - /// Empty array if tracking spans are not available for the document. - public async ValueTask> GetSpansAsync(Solution solution, DocumentId? documentId, string filePath, CancellationToken cancellationToken) - { - documentId ??= solution.GetDocumentIdsWithFilePath(filePath).FirstOrDefault(); + snapshot = source.FindCorrespondingEditorTextSnapshot(); + return snapshot != null; + } - var document = await solution.GetTextDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); - if (document == null) - { - return []; - } + /// + /// Returns location of the tracking spans in the specified snapshot (#line target document). + /// + /// Empty array if tracking spans are not available for the document. + public async ValueTask> GetSpansAsync(Solution solution, DocumentId? documentId, string filePath, CancellationToken cancellationToken) + { + documentId ??= solution.GetDocumentIdsWithFilePath(filePath).FirstOrDefault(); - var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var document = await solution.GetTextDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); + if (document == null) + { + return []; + } - lock (_trackingSpans) + var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + + lock (_trackingSpans) + { + if (_trackingSpans.TryGetValue(filePath, out var documentSpans) && !documentSpans.IsDefaultOrEmpty) { - if (_trackingSpans.TryGetValue(filePath, out var documentSpans) && !documentSpans.IsDefaultOrEmpty) + var snapshot = sourceText.FindCorrespondingEditorTextSnapshot(); + if (snapshot != null && snapshot.TextBuffer == documentSpans.First().Span.TextBuffer) { - var snapshot = sourceText.FindCorrespondingEditorTextSnapshot(); - if (snapshot != null && snapshot.TextBuffer == documentSpans.First().Span.TextBuffer) - { - return documentSpans.SelectAsArray(s => new ActiveStatementSpan(s.Ordinal, s.Span.GetSpan(snapshot).ToLinePositionSpan(), s.Flags, s.UnmappedDocumentId)); - } + return documentSpans.SelectAsArray(s => new ActiveStatementSpan(s.Ordinal, s.Span.GetSpan(snapshot).ToLinePositionSpan(), s.Flags, s.UnmappedDocumentId)); } } - - return []; } - /// - /// Updates tracking spans with the latest positions of all active statements in the specified document snapshot (#line target document) and returns them. - /// - internal async ValueTask> GetAdjustedTrackingSpansAsync(TextDocument document, ITextSnapshot snapshot, CancellationToken cancellationToken) + return []; + } + + /// + /// Updates tracking spans with the latest positions of all active statements in the specified document snapshot (#line target document) and returns them. + /// + internal async ValueTask> GetAdjustedTrackingSpansAsync(TextDocument document, ITextSnapshot snapshot, CancellationToken cancellationToken) + { + try { - try + if (document.FilePath == null) { - if (document.FilePath == null) - { - return []; - } - - Debug.Assert(TryGetSnapshot(document, out var s) && s == snapshot); + return []; + } - var solution = document.Project.Solution; + Debug.Assert(TryGetSnapshot(document, out var s) && s == snapshot); - var activeStatementSpans = await _spanProvider.GetAdjustedActiveStatementSpansAsync( - document, - (documentId, filePath, cancellationToken) => GetSpansAsync(solution, documentId, filePath, cancellationToken), - cancellationToken).ConfigureAwait(false); + var solution = document.Project.Solution; - Contract.ThrowIfTrue(activeStatementSpans.IsDefault); + var activeStatementSpans = await _spanProvider.GetAdjustedActiveStatementSpansAsync( + document, + (documentId, filePath, cancellationToken) => GetSpansAsync(solution, documentId, filePath, cancellationToken), + cancellationToken).ConfigureAwait(false); - lock (_trackingSpans) - { - var hasExistingSpans = _trackingSpans.TryGetValue(document.FilePath, out var oldSpans); + Contract.ThrowIfTrue(activeStatementSpans.IsDefault); - if (activeStatementSpans.IsEmpty) - { - // Unable to determine the latest positions of active statements for the document snapshot (the document is out-of-sync). - // Return the current tracking spans. - return oldSpans.NullToEmpty(); - } + lock (_trackingSpans) + { + var hasExistingSpans = _trackingSpans.TryGetValue(document.FilePath, out var oldSpans); - return _trackingSpans[document.FilePath] = hasExistingSpans - ? UpdateTrackingSpans(snapshot, oldSpans, activeStatementSpans) - : CreateTrackingSpans(snapshot, activeStatementSpans); + if (activeStatementSpans.IsEmpty) + { + // Unable to determine the latest positions of active statements for the document snapshot (the document is out-of-sync). + // Return the current tracking spans. + return oldSpans.NullToEmpty(); } - } - catch (OperationCanceledException) - { - // nop - } - catch (Exception e) when (FatalError.ReportAndCatch(e)) - { - // nop - } - return []; + return _trackingSpans[document.FilePath] = hasExistingSpans + ? UpdateTrackingSpans(snapshot, oldSpans, activeStatementSpans) + : CreateTrackingSpans(snapshot, activeStatementSpans); + } } + catch (OperationCanceledException) + { + // nop + } + catch (Exception e) when (FatalError.ReportAndCatch(e)) + { + // nop + } + + return []; } } } diff --git a/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTrackingSpan.cs b/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTrackingSpan.cs index c7dfb5261bf40..4c950bf180af3 100644 --- a/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTrackingSpan.cs +++ b/src/EditorFeatures/Core/EditAndContinue/ActiveStatementTrackingSpan.cs @@ -6,21 +6,20 @@ using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal readonly struct ActiveStatementTrackingSpan(ITrackingSpan trackingSpan, int ordinal, ActiveStatementFlags flags, DocumentId? unmappedDocumentId) { - internal readonly struct ActiveStatementTrackingSpan(ITrackingSpan trackingSpan, int ordinal, ActiveStatementFlags flags, DocumentId? unmappedDocumentId) - { - public readonly ITrackingSpan Span = trackingSpan; - public readonly int Ordinal = ordinal; - public readonly ActiveStatementFlags Flags = flags; - public readonly DocumentId? UnmappedDocumentId = unmappedDocumentId; + public readonly ITrackingSpan Span = trackingSpan; + public readonly int Ordinal = ordinal; + public readonly ActiveStatementFlags Flags = flags; + public readonly DocumentId? UnmappedDocumentId = unmappedDocumentId; - /// - /// True if at least one of the threads whom this active statement belongs to is in a leaf frame. - /// - public bool IsLeaf => (Flags & ActiveStatementFlags.LeafFrame) != 0; + /// + /// True if at least one of the threads whom this active statement belongs to is in a leaf frame. + /// + public bool IsLeaf => (Flags & ActiveStatementFlags.LeafFrame) != 0; - public static ActiveStatementTrackingSpan Create(ITextSnapshot snapshot, ActiveStatementSpan span) - => new(snapshot.CreateTrackingSpan(snapshot.GetTextSpan(span.LineSpan).ToSpan(), SpanTrackingMode.EdgeExclusive), span.Ordinal, span.Flags, span.UnmappedDocumentId); - } + public static ActiveStatementTrackingSpan Create(ITextSnapshot snapshot, ActiveStatementSpan span) + => new(snapshot.CreateTrackingSpan(snapshot.GetTextSpan(span.LineSpan).ToSpan(), SpanTrackingMode.EdgeExclusive), span.Ordinal, span.Flags, span.UnmappedDocumentId); } diff --git a/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs b/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs index 39e1249e33b32..76aa982ff92df 100644 --- a/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs +++ b/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs @@ -7,67 +7,66 @@ using Microsoft.VisualStudio.Debugger.Contracts.HotReload; using InternalContracts = Microsoft.CodeAnalysis.Contracts.EditAndContinue; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal static class ContractWrappers { - internal static class ContractWrappers - { - public static InternalContracts.ManagedActiveStatementDebugInfo ToContract(this ManagedActiveStatementDebugInfo info) - => new(ToContract(info.ActiveInstruction), info.DocumentName, ToContract(info.SourceSpan), (InternalContracts.ActiveStatementFlags)info.Flags); + public static InternalContracts.ManagedActiveStatementDebugInfo ToContract(this ManagedActiveStatementDebugInfo info) + => new(ToContract(info.ActiveInstruction), info.DocumentName, ToContract(info.SourceSpan), (InternalContracts.ActiveStatementFlags)info.Flags); - public static InternalContracts.ManagedInstructionId ToContract(this ManagedInstructionId id) - => new(ToContract(id.Method), id.ILOffset); + public static InternalContracts.ManagedInstructionId ToContract(this ManagedInstructionId id) + => new(ToContract(id.Method), id.ILOffset); - public static InternalContracts.ManagedMethodId ToContract(this ManagedMethodId id) - => new(id.Module, id.Token, id.Version); + public static InternalContracts.ManagedMethodId ToContract(this ManagedMethodId id) + => new(id.Module, id.Token, id.Version); - public static InternalContracts.SourceSpan ToContract(this SourceSpan id) - => new(id.StartLine, id.StartColumn, id.EndLine, id.EndColumn); + public static InternalContracts.SourceSpan ToContract(this SourceSpan id) + => new(id.StartLine, id.StartColumn, id.EndLine, id.EndColumn); - public static InternalContracts.ManagedHotReloadAvailability ToContract(this ManagedHotReloadAvailability value) - => new((InternalContracts.ManagedHotReloadAvailabilityStatus)value.Status, value.LocalizedMessage); + public static InternalContracts.ManagedHotReloadAvailability ToContract(this ManagedHotReloadAvailability value) + => new((InternalContracts.ManagedHotReloadAvailabilityStatus)value.Status, value.LocalizedMessage); - public static ManagedHotReloadUpdate FromContract(this InternalContracts.ManagedHotReloadUpdate update) - => new( - module: update.Module, - moduleName: update.ModuleName, - ilDelta: update.ILDelta, - metadataDelta: update.MetadataDelta, - pdbDelta: update.PdbDelta, - updatedTypes: update.UpdatedTypes, - requiredCapabilities: update.RequiredCapabilities, - updatedMethods: update.UpdatedMethods, - sequencePoints: update.SequencePoints.SelectAsArray(FromContract), - activeStatements: update.ActiveStatements.SelectAsArray(FromContract), - exceptionRegions: update.ExceptionRegions.SelectAsArray(FromContract)); + public static ManagedHotReloadUpdate FromContract(this InternalContracts.ManagedHotReloadUpdate update) + => new( + module: update.Module, + moduleName: update.ModuleName, + ilDelta: update.ILDelta, + metadataDelta: update.MetadataDelta, + pdbDelta: update.PdbDelta, + updatedTypes: update.UpdatedTypes, + requiredCapabilities: update.RequiredCapabilities, + updatedMethods: update.UpdatedMethods, + sequencePoints: update.SequencePoints.SelectAsArray(FromContract), + activeStatements: update.ActiveStatements.SelectAsArray(FromContract), + exceptionRegions: update.ExceptionRegions.SelectAsArray(FromContract)); - public static ManagedHotReloadUpdates FromContract(this InternalContracts.ManagedHotReloadUpdates updates) - => new(updates.Updates.FromContract(), updates.Diagnostics.FromContract()); + public static ManagedHotReloadUpdates FromContract(this InternalContracts.ManagedHotReloadUpdates updates) + => new(updates.Updates.FromContract(), updates.Diagnostics.FromContract()); - public static ImmutableArray FromContract(this ImmutableArray diagnostics) - => diagnostics.SelectAsArray(FromContract); + public static ImmutableArray FromContract(this ImmutableArray diagnostics) + => diagnostics.SelectAsArray(FromContract); - public static SequencePointUpdates FromContract(this InternalContracts.SequencePointUpdates updates) - => new(updates.FileName, updates.LineUpdates.SelectAsArray(FromContract)); + public static SequencePointUpdates FromContract(this InternalContracts.SequencePointUpdates updates) + => new(updates.FileName, updates.LineUpdates.SelectAsArray(FromContract)); - public static SourceLineUpdate FromContract(this InternalContracts.SourceLineUpdate update) - => new(update.OldLine, update.NewLine); + public static SourceLineUpdate FromContract(this InternalContracts.SourceLineUpdate update) + => new(update.OldLine, update.NewLine); - public static ManagedActiveStatementUpdate FromContract(this InternalContracts.ManagedActiveStatementUpdate update) - => new(FromContract(update.Method), update.ILOffset, FromContract(update.NewSpan)); + public static ManagedActiveStatementUpdate FromContract(this InternalContracts.ManagedActiveStatementUpdate update) + => new(FromContract(update.Method), update.ILOffset, FromContract(update.NewSpan)); - public static ManagedModuleMethodId FromContract(this InternalContracts.ManagedModuleMethodId update) - => new(update.Token, update.Version); + public static ManagedModuleMethodId FromContract(this InternalContracts.ManagedModuleMethodId update) + => new(update.Token, update.Version); - public static SourceSpan FromContract(this InternalContracts.SourceSpan id) - => new(id.StartLine, id.StartColumn, id.EndLine, id.EndColumn); + public static SourceSpan FromContract(this InternalContracts.SourceSpan id) + => new(id.StartLine, id.StartColumn, id.EndLine, id.EndColumn); - public static ManagedExceptionRegionUpdate FromContract(this InternalContracts.ManagedExceptionRegionUpdate update) - => new(FromContract(update.Method), update.Delta, FromContract(update.NewSpan)); + public static ManagedExceptionRegionUpdate FromContract(this InternalContracts.ManagedExceptionRegionUpdate update) + => new(FromContract(update.Method), update.Delta, FromContract(update.NewSpan)); - public static ImmutableArray FromContract(this ImmutableArray diagnostics) - => diagnostics.SelectAsArray(FromContract); + public static ImmutableArray FromContract(this ImmutableArray diagnostics) + => diagnostics.SelectAsArray(FromContract); - public static ManagedHotReloadDiagnostic FromContract(this InternalContracts.ManagedHotReloadDiagnostic diagnostic) - => new(diagnostic.Id, diagnostic.Message, (ManagedHotReloadDiagnosticSeverity)diagnostic.Severity, diagnostic.FilePath, FromContract(diagnostic.Span)); - } + public static ManagedHotReloadDiagnostic FromContract(this InternalContracts.ManagedHotReloadDiagnostic diagnostic) + => new(diagnostic.Id, diagnostic.Message, (ManagedHotReloadDiagnosticSeverity)diagnostic.Severity, diagnostic.FilePath, FromContract(diagnostic.Span)); } diff --git a/src/EditorFeatures/Core/EditAndContinue/Contracts/ManagedHotReloadServiceBridge.cs b/src/EditorFeatures/Core/EditAndContinue/Contracts/ManagedHotReloadServiceBridge.cs index 8909aeefde139..5e956b35a8fe7 100644 --- a/src/EditorFeatures/Core/EditAndContinue/Contracts/ManagedHotReloadServiceBridge.cs +++ b/src/EditorFeatures/Core/EditAndContinue/Contracts/ManagedHotReloadServiceBridge.cs @@ -9,20 +9,19 @@ using Microsoft.VisualStudio.Debugger.Contracts.HotReload; using InternalContracts = Microsoft.CodeAnalysis.Contracts.EditAndContinue; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal sealed class ManagedHotReloadServiceBridge(IManagedHotReloadService service) : InternalContracts.IManagedHotReloadService { - internal sealed class ManagedHotReloadServiceBridge(IManagedHotReloadService service) : InternalContracts.IManagedHotReloadService - { - public async ValueTask> GetActiveStatementsAsync(CancellationToken cancellation) - => (await service.GetActiveStatementsAsync(cancellation).ConfigureAwait(false)).SelectAsArray(a => a.ToContract()); + public async ValueTask> GetActiveStatementsAsync(CancellationToken cancellation) + => (await service.GetActiveStatementsAsync(cancellation).ConfigureAwait(false)).SelectAsArray(a => a.ToContract()); - public async ValueTask GetAvailabilityAsync(Guid module, CancellationToken cancellation) - => (await service.GetAvailabilityAsync(module, cancellation).ConfigureAwait(false)).ToContract(); + public async ValueTask GetAvailabilityAsync(Guid module, CancellationToken cancellation) + => (await service.GetAvailabilityAsync(module, cancellation).ConfigureAwait(false)).ToContract(); - public ValueTask> GetCapabilitiesAsync(CancellationToken cancellation) - => service.GetCapabilitiesAsync(cancellation); + public ValueTask> GetCapabilitiesAsync(CancellationToken cancellation) + => service.GetCapabilitiesAsync(cancellation); - public ValueTask PrepareModuleForUpdateAsync(Guid module, CancellationToken cancellation) - => service.PrepareModuleForUpdateAsync(module, cancellation); - } + public ValueTask PrepareModuleForUpdateAsync(Guid module, CancellationToken cancellation) + => service.PrepareModuleForUpdateAsync(module, cancellation); } diff --git a/src/EditorFeatures/Core/EditAndContinue/DebuggerContractVersionCheck.cs b/src/EditorFeatures/Core/EditAndContinue/DebuggerContractVersionCheck.cs index 7a2af33e64f6c..78b8b9e65ebf9 100644 --- a/src/EditorFeatures/Core/EditAndContinue/DebuggerContractVersionCheck.cs +++ b/src/EditorFeatures/Core/EditAndContinue/DebuggerContractVersionCheck.cs @@ -6,28 +6,27 @@ using System.Runtime.CompilerServices; using Microsoft.VisualStudio.Debugger.Contracts.HotReload; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Allow us to run integration tests on older VS than build that has the required version of Microsoft.VisualStudio.Debugger.Contracts. +/// +internal static class DebuggerContractVersionCheck { - /// - /// Allow us to run integration tests on older VS than build that has the required version of Microsoft.VisualStudio.Debugger.Contracts. - /// - internal static class DebuggerContractVersionCheck + public static bool IsRequiredDebuggerContractVersionAvailable() { - public static bool IsRequiredDebuggerContractVersionAvailable() + try { - try - { - _ = LoadContracts(); - return true; - } - catch - { - return false; - } + _ = LoadContracts(); + return true; + } + catch + { + return false; } - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - private static Type LoadContracts() - => typeof(ManagedActiveStatementUpdate); } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static Type LoadContracts() + => typeof(ManagedActiveStatementUpdate); } diff --git a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueDiagnosticAnalyzer.cs b/src/EditorFeatures/Core/EditAndContinue/EditAndContinueDiagnosticAnalyzer.cs index 7e578b455877e..1508acb0ad807 100644 --- a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueDiagnosticAnalyzer.cs +++ b/src/EditorFeatures/Core/EditAndContinue/EditAndContinueDiagnosticAnalyzer.cs @@ -17,75 +17,74 @@ using Microsoft.VisualStudio.Debugger.Contracts; using Microsoft.CodeAnalysis.Simplification; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +internal sealed class EditAndContinueDiagnosticAnalyzer : DocumentDiagnosticAnalyzer, IBuiltInAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - internal sealed class EditAndContinueDiagnosticAnalyzer : DocumentDiagnosticAnalyzer, IBuiltInAnalyzer - { - private static readonly ImmutableArray s_supportedDiagnostics = EditAndContinueDiagnosticDescriptors.GetDescriptors(); + private static readonly ImmutableArray s_supportedDiagnostics = EditAndContinueDiagnosticDescriptors.GetDescriptors(); - // Return known descriptors. This will not include module diagnostics reported on behalf of the debugger. - public override ImmutableArray SupportedDiagnostics - => s_supportedDiagnostics; + // Return known descriptors. This will not include module diagnostics reported on behalf of the debugger. + public override ImmutableArray SupportedDiagnostics + => s_supportedDiagnostics; - public DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + public DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; - public bool IsHighPriority => false; + public bool IsHighPriority => false; - public bool OpenFileOnly(SimplifierOptions? options) - => false; + public bool OpenFileOnly(SimplifierOptions? options) + => false; - // No syntax diagnostics produced by the EnC engine. - public override Task> AnalyzeSyntaxAsync(Document document, CancellationToken cancellationToken) - => SpecializedTasks.EmptyImmutableArray(); + // No syntax diagnostics produced by the EnC engine. + public override Task> AnalyzeSyntaxAsync(Document document, CancellationToken cancellationToken) + => SpecializedTasks.EmptyImmutableArray(); - public override Task> AnalyzeSemanticsAsync(Document document, CancellationToken cancellationToken) + public override Task> AnalyzeSemanticsAsync(Document document, CancellationToken cancellationToken) + { + if (!DebuggerContractVersionCheck.IsRequiredDebuggerContractVersionAvailable()) { - if (!DebuggerContractVersionCheck.IsRequiredDebuggerContractVersionAvailable()) - { - return SpecializedTasks.EmptyImmutableArray(); - } - - return AnalyzeSemanticsImplAsync(document, cancellationToken); + return SpecializedTasks.EmptyImmutableArray(); } - [MethodImpl(MethodImplOptions.NoInlining)] - private static async Task> AnalyzeSemanticsImplAsync(Document designTimeDocument, CancellationToken cancellationToken) - { - var workspace = designTimeDocument.Project.Solution.Workspace; + return AnalyzeSemanticsImplAsync(document, cancellationToken); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task> AnalyzeSemanticsImplAsync(Document designTimeDocument, CancellationToken cancellationToken) + { + var workspace = designTimeDocument.Project.Solution.Workspace; - if (workspace.Services.HostServices is not IMefHostExportProvider mefServices) - { - return []; - } + if (workspace.Services.HostServices is not IMefHostExportProvider mefServices) + { + return []; + } - // avoid creating and synchronizing compile-time solution if the Hot Reload/EnC session is not active - if (mefServices.GetExports().SingleOrDefault()?.Value.IsSessionActive != true) - { - return []; - } + // avoid creating and synchronizing compile-time solution if the Hot Reload/EnC session is not active + if (mefServices.GetExports().SingleOrDefault()?.Value.IsSessionActive != true) + { + return []; + } - var designTimeSolution = designTimeDocument.Project.Solution; - var compileTimeSolution = workspace.Services.GetRequiredService().GetCompileTimeSolution(designTimeSolution); + var designTimeSolution = designTimeDocument.Project.Solution; + var compileTimeSolution = workspace.Services.GetRequiredService().GetCompileTimeSolution(designTimeSolution); - var compileTimeDocument = await CompileTimeSolutionProvider.TryGetCompileTimeDocumentAsync(designTimeDocument, compileTimeSolution, cancellationToken).ConfigureAwait(false); - if (compileTimeDocument == null) - { - return []; - } + var compileTimeDocument = await CompileTimeSolutionProvider.TryGetCompileTimeDocumentAsync(designTimeDocument, compileTimeSolution, cancellationToken).ConfigureAwait(false); + if (compileTimeDocument == null) + { + return []; + } - // EnC services should never be called on a design-time solution. + // EnC services should never be called on a design-time solution. - var proxy = new RemoteEditAndContinueServiceProxy(workspace); + var proxy = new RemoteEditAndContinueServiceProxy(workspace); - var activeStatementSpanProvider = new ActiveStatementSpanProvider(async (documentId, filePath, cancellationToken) => - { - var trackingService = workspace.Services.GetRequiredService(); - return await trackingService.GetSpansAsync(compileTimeSolution, documentId, filePath, cancellationToken).ConfigureAwait(false); - }); + var activeStatementSpanProvider = new ActiveStatementSpanProvider(async (documentId, filePath, cancellationToken) => + { + var trackingService = workspace.Services.GetRequiredService(); + return await trackingService.GetSpansAsync(compileTimeSolution, documentId, filePath, cancellationToken).ConfigureAwait(false); + }); - return await proxy.GetDocumentDiagnosticsAsync(compileTimeDocument, designTimeDocument, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); - } + return await proxy.GetDocumentDiagnosticsAsync(compileTimeDocument, designTimeDocument, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); } } diff --git a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs b/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs index 41dc3ee8684ef..10a8aca886b7b 100644 --- a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs +++ b/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs @@ -16,346 +16,345 @@ using Microsoft.VisualStudio.Debugger.Contracts.HotReload; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[Shared] +[Export(typeof(IManagedHotReloadLanguageService))] +[Export(typeof(IEditAndContinueSolutionProvider))] +[Export(typeof(EditAndContinueLanguageService))] +[ExportMetadata("UIContext", EditAndContinueUIContext.EncCapableProjectExistsInWorkspaceUIContextString)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class EditAndContinueLanguageService( + IServiceBrokerProvider serviceBrokerProvider, + Lazy workspaceProvider, + Lazy debuggerService, + IDiagnosticAnalyzerService diagnosticService, + EditAndContinueDiagnosticUpdateSource diagnosticUpdateSource, + PdbMatchingSourceTextProvider sourceTextProvider, + IDiagnosticsRefresher diagnosticRefresher, + IAsynchronousOperationListenerProvider listenerProvider) : IManagedHotReloadLanguageService, IEditAndContinueSolutionProvider { - [Shared] - [Export(typeof(IManagedHotReloadLanguageService))] - [Export(typeof(IEditAndContinueSolutionProvider))] - [Export(typeof(EditAndContinueLanguageService))] - [ExportMetadata("UIContext", EditAndContinueUIContext.EncCapableProjectExistsInWorkspaceUIContextString)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class EditAndContinueLanguageService( - IServiceBrokerProvider serviceBrokerProvider, - Lazy workspaceProvider, - Lazy debuggerService, - IDiagnosticAnalyzerService diagnosticService, - EditAndContinueDiagnosticUpdateSource diagnosticUpdateSource, - PdbMatchingSourceTextProvider sourceTextProvider, - IDiagnosticsRefresher diagnosticRefresher, - IAsynchronousOperationListenerProvider listenerProvider) : IManagedHotReloadLanguageService, IEditAndContinueSolutionProvider + private sealed class NoSessionException : InvalidOperationException { - private sealed class NoSessionException : InvalidOperationException + public NoSessionException() + : base("Internal error: no session.") { - public NoSessionException() - : base("Internal error: no session.") - { - // unique enough HResult to distinguish from other exceptions - HResult = unchecked((int)0x801315087); - } + // unique enough HResult to distinguish from other exceptions + HResult = unchecked((int)0x801315087); } + } - private readonly PdbMatchingSourceTextProvider _sourceTextProvider = sourceTextProvider; - private readonly IDiagnosticsRefresher _diagnosticRefresher = diagnosticRefresher; - private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.EditAndContinue); - private readonly Lazy _debuggerService = debuggerService; - private readonly IDiagnosticAnalyzerService _diagnosticService = diagnosticService; - private readonly EditAndContinueDiagnosticUpdateSource _diagnosticUpdateSource = diagnosticUpdateSource; - private readonly HotReloadLoggerProxy _logger = new(serviceBrokerProvider.ServiceBroker); + private readonly PdbMatchingSourceTextProvider _sourceTextProvider = sourceTextProvider; + private readonly IDiagnosticsRefresher _diagnosticRefresher = diagnosticRefresher; + private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.EditAndContinue); + private readonly Lazy _debuggerService = debuggerService; + private readonly IDiagnosticAnalyzerService _diagnosticService = diagnosticService; + private readonly EditAndContinueDiagnosticUpdateSource _diagnosticUpdateSource = diagnosticUpdateSource; + private readonly HotReloadLoggerProxy _logger = new(serviceBrokerProvider.ServiceBroker); - public readonly Lazy WorkspaceProvider = workspaceProvider; + public readonly Lazy WorkspaceProvider = workspaceProvider; - public bool IsSessionActive { get; private set; } + public bool IsSessionActive { get; private set; } - private bool _disabled; - private RemoteDebuggingSessionProxy? _debuggingSession; + private bool _disabled; + private RemoteDebuggingSessionProxy? _debuggingSession; - private Solution? _pendingUpdatedDesignTimeSolution; - private Solution? _committedDesignTimeSolution; + private Solution? _pendingUpdatedDesignTimeSolution; + private Solution? _committedDesignTimeSolution; - public event Action? SolutionCommitted; + public event Action? SolutionCommitted; - public void SetFileLoggingDirectory(string? logDirectory) + public void SetFileLoggingDirectory(string? logDirectory) + { + _ = Task.Run(async () => { - _ = Task.Run(async () => + try { - try - { - var proxy = new RemoteEditAndContinueServiceProxy(WorkspaceProvider.Value.Workspace); - await proxy.SetFileLoggingDirectoryAsync(logDirectory, CancellationToken.None).ConfigureAwait(false); - } - catch - { - // ignore - } - }); - } - - private Solution GetCurrentCompileTimeSolution(Solution? currentDesignTimeSolution = null) - { - var workspace = WorkspaceProvider.Value.Workspace; - return workspace.Services.GetRequiredService().GetCompileTimeSolution(currentDesignTimeSolution ?? workspace.CurrentSolution); - } + var proxy = new RemoteEditAndContinueServiceProxy(WorkspaceProvider.Value.Workspace); + await proxy.SetFileLoggingDirectoryAsync(logDirectory, CancellationToken.None).ConfigureAwait(false); + } + catch + { + // ignore + } + }); + } - private RemoteDebuggingSessionProxy GetDebuggingSession() - => _debuggingSession ?? throw new NoSessionException(); + private Solution GetCurrentCompileTimeSolution(Solution? currentDesignTimeSolution = null) + { + var workspace = WorkspaceProvider.Value.Workspace; + return workspace.Services.GetRequiredService().GetCompileTimeSolution(currentDesignTimeSolution ?? workspace.CurrentSolution); + } - private IActiveStatementTrackingService GetActiveStatementTrackingService() - => WorkspaceProvider.Value.Workspace.Services.GetRequiredService(); + private RemoteDebuggingSessionProxy GetDebuggingSession() + => _debuggingSession ?? throw new NoSessionException(); - internal void Disable(Exception e) - { - _disabled = true; + private IActiveStatementTrackingService GetActiveStatementTrackingService() + => WorkspaceProvider.Value.Workspace.Services.GetRequiredService(); - var token = _asyncListener.BeginAsyncOperation(nameof(EditAndContinueLanguageService) + ".LogToOutput"); + internal void Disable(Exception e) + { + _disabled = true; - _ = _logger.LogAsync(new HotReloadLogMessage(HotReloadVerbosity.Diagnostic, e.ToString(), errorLevel: HotReloadDiagnosticErrorLevel.Error), CancellationToken.None).AsTask() - .ReportNonFatalErrorAsync().CompletesAsyncOperation(token); - } + var token = _asyncListener.BeginAsyncOperation(nameof(EditAndContinueLanguageService) + ".LogToOutput"); - /// - /// Called by the debugger when a debugging session starts and managed debugging is being used. - /// - public async ValueTask StartSessionAsync(CancellationToken cancellationToken) - { - IsSessionActive = true; + _ = _logger.LogAsync(new HotReloadLogMessage(HotReloadVerbosity.Diagnostic, e.ToString(), errorLevel: HotReloadDiagnosticErrorLevel.Error), CancellationToken.None).AsTask() + .ReportNonFatalErrorAsync().CompletesAsyncOperation(token); + } - if (_disabled) - { - return; - } + /// + /// Called by the debugger when a debugging session starts and managed debugging is being used. + /// + public async ValueTask StartSessionAsync(CancellationToken cancellationToken) + { + IsSessionActive = true; - try - { - // Activate listener before capturing the current solution snapshot, - // so that we don't miss any pertinent workspace update events. - _sourceTextProvider.Activate(); - - var workspace = WorkspaceProvider.Value.Workspace; - var currentSolution = workspace.CurrentSolution; - _committedDesignTimeSolution = currentSolution; - var solution = GetCurrentCompileTimeSolution(currentSolution); - - _sourceTextProvider.SetBaseline(currentSolution); - - var proxy = new RemoteEditAndContinueServiceProxy(workspace); - - _debuggingSession = await proxy.StartDebuggingSessionAsync( - solution, - new ManagedHotReloadServiceBridge(_debuggerService.Value), - _sourceTextProvider, - captureMatchingDocuments: [], - captureAllMatchingDocuments: false, - reportDiagnostics: true, - cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - // the service failed, error has been reported - disable further operations - Disable(e); - } + if (_disabled) + { + return; } - public async ValueTask EnterBreakStateAsync(CancellationToken cancellationToken) + try { - if (_disabled) - { - return; - } + // Activate listener before capturing the current solution snapshot, + // so that we don't miss any pertinent workspace update events. + _sourceTextProvider.Activate(); - var solution = GetCurrentCompileTimeSolution(); - var session = GetDebuggingSession(); + var workspace = WorkspaceProvider.Value.Workspace; + var currentSolution = workspace.CurrentSolution; + _committedDesignTimeSolution = currentSolution; + var solution = GetCurrentCompileTimeSolution(currentSolution); + + _sourceTextProvider.SetBaseline(currentSolution); + + var proxy = new RemoteEditAndContinueServiceProxy(workspace); + + _debuggingSession = await proxy.StartDebuggingSessionAsync( + solution, + new ManagedHotReloadServiceBridge(_debuggerService.Value), + _sourceTextProvider, + captureMatchingDocuments: [], + captureAllMatchingDocuments: false, + reportDiagnostics: true, + cancellationToken).ConfigureAwait(false); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + // the service failed, error has been reported - disable further operations + Disable(e); + } + } - try - { - await session.BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: true, cancellationToken).ConfigureAwait(false); + public async ValueTask EnterBreakStateAsync(CancellationToken cancellationToken) + { + if (_disabled) + { + return; + } - _diagnosticRefresher.RequestWorkspaceRefresh(); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - Disable(e); - return; - } + var solution = GetCurrentCompileTimeSolution(); + var session = GetDebuggingSession(); - // Start tracking after we entered break state so that break-state session is active. - // This is potentially costly operation as source generators might get invoked in OOP - // to determine the spans of all active statements. - // We start the operation but do not wait for it to complete. - // The tracking session is cancelled when we exit the break state. + try + { + await session.BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: true, cancellationToken).ConfigureAwait(false); - GetActiveStatementTrackingService().StartTracking(solution, session); + _diagnosticRefresher.RequestWorkspaceRefresh(); } - - public async ValueTask ExitBreakStateAsync(CancellationToken cancellationToken) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - if (_disabled) - { - return; - } + Disable(e); + return; + } - var session = GetDebuggingSession(); + // Start tracking after we entered break state so that break-state session is active. + // This is potentially costly operation as source generators might get invoked in OOP + // to determine the spans of all active statements. + // We start the operation but do not wait for it to complete. + // The tracking session is cancelled when we exit the break state. - try - { - await session.BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: false, cancellationToken).ConfigureAwait(false); + GetActiveStatementTrackingService().StartTracking(solution, session); + } - _diagnosticRefresher.RequestWorkspaceRefresh(); - GetActiveStatementTrackingService().EndTracking(); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - Disable(e); - return; - } + public async ValueTask ExitBreakStateAsync(CancellationToken cancellationToken) + { + if (_disabled) + { + return; } - public async ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellationToken) + var session = GetDebuggingSession(); + + try { - if (_disabled) - { - return; - } + await session.BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: false, cancellationToken).ConfigureAwait(false); - try - { - await GetDebuggingSession().BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: null, cancellationToken).ConfigureAwait(false); + _diagnosticRefresher.RequestWorkspaceRefresh(); + GetActiveStatementTrackingService().EndTracking(); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + Disable(e); + return; + } + } - _diagnosticRefresher.RequestWorkspaceRefresh(); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - Disable(e); - } + public async ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellationToken) + { + if (_disabled) + { + return; } - public async ValueTask CommitUpdatesAsync(CancellationToken cancellationToken) + try { - if (_disabled) - { - return; - } + await GetDebuggingSession().BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: null, cancellationToken).ConfigureAwait(false); - var committedDesignTimeSolution = Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null); - Contract.ThrowIfNull(committedDesignTimeSolution); + _diagnosticRefresher.RequestWorkspaceRefresh(); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + Disable(e); + } + } - try - { - SolutionCommitted?.Invoke(committedDesignTimeSolution); - } - catch (Exception e) when (FatalError.ReportAndCatch(e)) - { - } + public async ValueTask CommitUpdatesAsync(CancellationToken cancellationToken) + { + if (_disabled) + { + return; + } - _committedDesignTimeSolution = committedDesignTimeSolution; + var committedDesignTimeSolution = Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null); + Contract.ThrowIfNull(committedDesignTimeSolution); - try - { - await GetDebuggingSession().CommitSolutionUpdateAsync(_diagnosticService, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - } + try + { + SolutionCommitted?.Invoke(committedDesignTimeSolution); } - - public async ValueTask DiscardUpdatesAsync(CancellationToken cancellationToken) + catch (Exception e) when (FatalError.ReportAndCatch(e)) { - if (_disabled) - { - return; - } + } - Contract.ThrowIfNull(Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null)); + _committedDesignTimeSolution = committedDesignTimeSolution; - try - { - await GetDebuggingSession().DiscardSolutionUpdateAsync(cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - } + try + { + await GetDebuggingSession().CommitSolutionUpdateAsync(_diagnosticService, cancellationToken).ConfigureAwait(false); } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + } + } - public async ValueTask EndSessionAsync(CancellationToken cancellationToken) + public async ValueTask DiscardUpdatesAsync(CancellationToken cancellationToken) + { + if (_disabled) { - IsSessionActive = false; + return; + } - if (!_disabled) - { - try - { - var solution = GetCurrentCompileTimeSolution(); - await GetDebuggingSession().EndDebuggingSessionAsync(solution, _diagnosticUpdateSource, _diagnosticService, cancellationToken).ConfigureAwait(false); - - _diagnosticRefresher.RequestWorkspaceRefresh(); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - Disable(e); - } - } + Contract.ThrowIfNull(Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null)); - _sourceTextProvider.Deactivate(); - _debuggingSession = null; - _committedDesignTimeSolution = null; - _pendingUpdatedDesignTimeSolution = null; + try + { + await GetDebuggingSession().DiscardSolutionUpdateAsync(cancellationToken).ConfigureAwait(false); } - - private ActiveStatementSpanProvider GetActiveStatementSpanProvider(Solution solution) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - var service = GetActiveStatementTrackingService(); - return new((documentId, filePath, cancellationToken) => service.GetSpansAsync(solution, documentId, filePath, cancellationToken)); } + } - /// - /// Returns true if any changes have been made to the source since the last changes had been applied. - /// For performance reasons it only implements a heuristic and may return both false positives and false negatives. - /// If the result is a false negative the debugger will not apply the changes unless the user explicitly triggers apply change command. - /// The background diagnostic analysis will still report rude edits for these ignored changes. It may also happen that these rude edits - /// will disappear once the debuggee is resumed - if they are caused by presence of active statements around the change. - /// If the result is a false positive the debugger attempts to apply the changes, which will result in a delay but will correctly end up - /// with no actual deltas to be applied. - /// - /// If is specified checks for changes only in a document of the given path. - /// This is not supported (returns false) for source-generated documents. - /// - public async ValueTask HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken) + public async ValueTask EndSessionAsync(CancellationToken cancellationToken) + { + IsSessionActive = false; + + if (!_disabled) { try { - var debuggingSession = _debuggingSession; - if (debuggingSession == null) - { - return false; - } - - Contract.ThrowIfNull(_committedDesignTimeSolution); - var oldSolution = _committedDesignTimeSolution; - var newSolution = WorkspaceProvider.Value.Workspace.CurrentSolution; - - return (sourceFilePath != null) - ? await EditSession.HasChangesAsync(oldSolution, newSolution, sourceFilePath, cancellationToken).ConfigureAwait(false) - : await EditSession.HasChangesAsync(oldSolution, newSolution, cancellationToken).ConfigureAwait(false); + var solution = GetCurrentCompileTimeSolution(); + await GetDebuggingSession().EndDebuggingSessionAsync(solution, _diagnosticUpdateSource, _diagnosticService, cancellationToken).ConfigureAwait(false); + + _diagnosticRefresher.RequestWorkspaceRefresh(); } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - return true; + Disable(e); } } - public async ValueTask GetUpdatesAsync(CancellationToken cancellationToken) + _sourceTextProvider.Deactivate(); + _debuggingSession = null; + _committedDesignTimeSolution = null; + _pendingUpdatedDesignTimeSolution = null; + } + + private ActiveStatementSpanProvider GetActiveStatementSpanProvider(Solution solution) + { + var service = GetActiveStatementTrackingService(); + return new((documentId, filePath, cancellationToken) => service.GetSpansAsync(solution, documentId, filePath, cancellationToken)); + } + + /// + /// Returns true if any changes have been made to the source since the last changes had been applied. + /// For performance reasons it only implements a heuristic and may return both false positives and false negatives. + /// If the result is a false negative the debugger will not apply the changes unless the user explicitly triggers apply change command. + /// The background diagnostic analysis will still report rude edits for these ignored changes. It may also happen that these rude edits + /// will disappear once the debuggee is resumed - if they are caused by presence of active statements around the change. + /// If the result is a false positive the debugger attempts to apply the changes, which will result in a delay but will correctly end up + /// with no actual deltas to be applied. + /// + /// If is specified checks for changes only in a document of the given path. + /// This is not supported (returns false) for source-generated documents. + /// + public async ValueTask HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken) + { + try { - if (_disabled) + var debuggingSession = _debuggingSession; + if (debuggingSession == null) { - return new ManagedHotReloadUpdates([], []); + return false; } - var workspace = WorkspaceProvider.Value.Workspace; - var designTimeSolution = workspace.CurrentSolution; - var solution = GetCurrentCompileTimeSolution(designTimeSolution); - var activeStatementSpanProvider = GetActiveStatementSpanProvider(solution); - var (moduleUpdates, diagnosticData, rudeEdits, syntaxError) = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, _diagnosticService, _diagnosticUpdateSource, cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(_committedDesignTimeSolution); + var oldSolution = _committedDesignTimeSolution; + var newSolution = WorkspaceProvider.Value.Workspace.CurrentSolution; - // Only store the solution if we have any changes to apply, otherwise CommitUpdatesAsync/DiscardUpdatesAsync won't be called. - if (moduleUpdates.Status == ModuleUpdateStatus.Ready) - { - _pendingUpdatedDesignTimeSolution = designTimeSolution; - } + return (sourceFilePath != null) + ? await EditSession.HasChangesAsync(oldSolution, newSolution, sourceFilePath, cancellationToken).ConfigureAwait(false) + : await EditSession.HasChangesAsync(oldSolution, newSolution, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + return true; + } + } - _diagnosticRefresher.RequestWorkspaceRefresh(); + public async ValueTask GetUpdatesAsync(CancellationToken cancellationToken) + { + if (_disabled) + { + return new ManagedHotReloadUpdates([], []); + } - var diagnostics = await EmitSolutionUpdateResults.GetHotReloadDiagnosticsAsync(solution, diagnosticData, rudeEdits, syntaxError, moduleUpdates.Status, cancellationToken).ConfigureAwait(false); - return new ManagedHotReloadUpdates(moduleUpdates.Updates.FromContract(), diagnostics.FromContract()); + var workspace = WorkspaceProvider.Value.Workspace; + var designTimeSolution = workspace.CurrentSolution; + var solution = GetCurrentCompileTimeSolution(designTimeSolution); + var activeStatementSpanProvider = GetActiveStatementSpanProvider(solution); + var (moduleUpdates, diagnosticData, rudeEdits, syntaxError) = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, _diagnosticService, _diagnosticUpdateSource, cancellationToken).ConfigureAwait(false); + + // Only store the solution if we have any changes to apply, otherwise CommitUpdatesAsync/DiscardUpdatesAsync won't be called. + if (moduleUpdates.Status == ModuleUpdateStatus.Ready) + { + _pendingUpdatedDesignTimeSolution = designTimeSolution; } + + _diagnosticRefresher.RequestWorkspaceRefresh(); + + var diagnostics = await EmitSolutionUpdateResults.GetHotReloadDiagnosticsAsync(solution, diagnosticData, rudeEdits, syntaxError, moduleUpdates.Status, cancellationToken).ConfigureAwait(false); + return new ManagedHotReloadUpdates(moduleUpdates.Updates.FromContract(), diagnostics.FromContract()); } } diff --git a/src/EditorFeatures/Core/EditAndContinue/PdbMatchingSourceTextProvider.cs b/src/EditorFeatures/Core/EditAndContinue/PdbMatchingSourceTextProvider.cs index a1c1282d09b12..0fc00c9f1f77d 100644 --- a/src/EditorFeatures/Core/EditAndContinue/PdbMatchingSourceTextProvider.cs +++ b/src/EditorFeatures/Core/EditAndContinue/PdbMatchingSourceTextProvider.cs @@ -14,159 +14,158 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Notifies EnC service of host workspace events. +/// +[ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared] +[Export(typeof(PdbMatchingSourceTextProvider))] +internal sealed class PdbMatchingSourceTextProvider : IEventListener, IEventListenerStoppable, IPdbMatchingSourceTextProvider { - /// - /// Notifies EnC service of host workspace events. - /// - [ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared] - [Export(typeof(PdbMatchingSourceTextProvider))] - internal sealed class PdbMatchingSourceTextProvider : IEventListener, IEventListenerStoppable, IPdbMatchingSourceTextProvider + private readonly object _guard = new(); + + private bool _isActive; + private int _baselineSolutionVersion; + private readonly Dictionary _documentsWithChangedLoaderByPath = []; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public PdbMatchingSourceTextProvider() { - private readonly object _guard = new(); + } - private bool _isActive; - private int _baselineSolutionVersion; - private readonly Dictionary _documentsWithChangedLoaderByPath = []; + public void StartListening(Workspace workspace, object serviceOpt) + { + workspace.WorkspaceChanged += WorkspaceChanged; + } + + public void StopListening(Workspace workspace) + { + workspace.WorkspaceChanged -= WorkspaceChanged; + } - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public PdbMatchingSourceTextProvider() + private void WorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) + { + if (!_isActive) { + // Not capturing document states because debugging session isn't active. + return; } - public void StartListening(Workspace workspace, object serviceOpt) + if (e.DocumentId == null) { - workspace.WorkspaceChanged += WorkspaceChanged; + return; } - public void StopListening(Workspace workspace) + var oldDocument = e.OldSolution.GetDocument(e.DocumentId); + if (oldDocument == null) { - workspace.WorkspaceChanged -= WorkspaceChanged; + // document added + return; } - private void WorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) + var newDocument = e.NewSolution.GetDocument(e.DocumentId); + if (newDocument == null) { - if (!_isActive) - { - // Not capturing document states because debugging session isn't active. - return; - } - - if (e.DocumentId == null) - { - return; - } - - var oldDocument = e.OldSolution.GetDocument(e.DocumentId); - if (oldDocument == null) - { - // document added - return; - } + // document removed + return; + } - var newDocument = e.NewSolution.GetDocument(e.DocumentId); - if (newDocument == null) - { - // document removed - return; - } + if (!oldDocument.State.SupportsEditAndContinue()) + { + return; + } - if (!oldDocument.State.SupportsEditAndContinue()) - { - return; - } + Contract.ThrowIfNull(oldDocument.FilePath); - Contract.ThrowIfNull(oldDocument.FilePath); + // When a document is open its loader transitions from file-based loader to text buffer based. + // The file checksum is no longer available from the latter, so capture it at this moment. + if (oldDocument.State.TextAndVersionSource.CanReloadText && !newDocument.State.TextAndVersionSource.CanReloadText) + { + var oldSolutionVersion = oldDocument.Project.Solution.WorkspaceVersion; - // When a document is open its loader transitions from file-based loader to text buffer based. - // The file checksum is no longer available from the latter, so capture it at this moment. - if (oldDocument.State.TextAndVersionSource.CanReloadText && !newDocument.State.TextAndVersionSource.CanReloadText) + lock (_guard) { - var oldSolutionVersion = oldDocument.Project.Solution.WorkspaceVersion; - - lock (_guard) + // ignore updates to a document that we have already seen this session: + if (_isActive && oldSolutionVersion >= _baselineSolutionVersion && !_documentsWithChangedLoaderByPath.ContainsKey(oldDocument.FilePath)) { - // ignore updates to a document that we have already seen this session: - if (_isActive && oldSolutionVersion >= _baselineSolutionVersion && !_documentsWithChangedLoaderByPath.ContainsKey(oldDocument.FilePath)) - { - _documentsWithChangedLoaderByPath.Add(oldDocument.FilePath, (oldDocument.DocumentState, oldSolutionVersion)); - } + _documentsWithChangedLoaderByPath.Add(oldDocument.FilePath, (oldDocument.DocumentState, oldSolutionVersion)); } } } + } - /// - /// Establish a baseline snapshot. The listener will ignore all document snapshots that are older. - /// - public void SetBaseline(Solution solution) + /// + /// Establish a baseline snapshot. The listener will ignore all document snapshots that are older. + /// + public void SetBaseline(Solution solution) + { + lock (_guard) { - lock (_guard) - { - _baselineSolutionVersion = solution.WorkspaceVersion; - } + _baselineSolutionVersion = solution.WorkspaceVersion; } + } - public void Activate() + public void Activate() + { + lock (_guard) { - lock (_guard) - { - _isActive = true; - } + _isActive = true; } + } - public void Deactivate() + public void Deactivate() + { + lock (_guard) { - lock (_guard) - { - _isActive = false; - _documentsWithChangedLoaderByPath.Clear(); - } + _isActive = false; + _documentsWithChangedLoaderByPath.Clear(); } + } - public async ValueTask TryGetMatchingSourceTextAsync(string filePath, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + public async ValueTask TryGetMatchingSourceTextAsync(string filePath, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + { + DocumentState? state; + lock (_guard) { - DocumentState? state; - lock (_guard) - { - if (!_documentsWithChangedLoaderByPath.TryGetValue(filePath, out var stateAndVersion)) - { - return null; - } - - state = stateAndVersion.state; - } - - if (state.LoadTextOptions.ChecksumAlgorithm != checksumAlgorithm) + if (!_documentsWithChangedLoaderByPath.TryGetValue(filePath, out var stateAndVersion)) { return null; } - var text = await state.GetTextAsync(cancellationToken).ConfigureAwait(false); - if (!text.GetChecksum().SequenceEqual(requiredChecksum)) - { - return null; - } - - return text.ToString(); + state = stateAndVersion.state; } - internal TestAccessor GetTestAccessor() - => new(this); + if (state.LoadTextOptions.ChecksumAlgorithm != checksumAlgorithm) + { + return null; + } - internal readonly struct TestAccessor + var text = await state.GetTextAsync(cancellationToken).ConfigureAwait(false); + if (!text.GetChecksum().SequenceEqual(requiredChecksum)) { - private readonly PdbMatchingSourceTextProvider _instance; + return null; + } - internal TestAccessor(PdbMatchingSourceTextProvider instance) - => _instance = instance; + return text.ToString(); + } + + internal TestAccessor GetTestAccessor() + => new(this); + + internal readonly struct TestAccessor + { + private readonly PdbMatchingSourceTextProvider _instance; - public ImmutableDictionary GetDocumentsWithChangedLoaderByPath() + internal TestAccessor(PdbMatchingSourceTextProvider instance) + => _instance = instance; + + public ImmutableDictionary GetDocumentsWithChangedLoaderByPath() + { + lock (_instance._guard) { - lock (_instance._guard) - { - return _instance._documentsWithChangedLoaderByPath.ToImmutableDictionary(); - } + return _instance._documentsWithChangedLoaderByPath.ToImmutableDictionary(); } } } diff --git a/src/EditorFeatures/Core/Editor/ContentTypeDefinitions.cs b/src/EditorFeatures/Core/Editor/ContentTypeDefinitions.cs index f64ea7ec9bd95..40f08af4d9ab7 100644 --- a/src/EditorFeatures/Core/Editor/ContentTypeDefinitions.cs +++ b/src/EditorFeatures/Core/Editor/ContentTypeDefinitions.cs @@ -7,16 +7,15 @@ using System.ComponentModel.Composition; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.ContentTypes +namespace Microsoft.CodeAnalysis.Editor.Implementation.ContentTypes; + +internal static class ContentTypeDefinitions { - internal static class ContentTypeDefinitions - { - /// - /// Definition of a content type that is a base definition for all content types supported by Roslyn. - /// - [Export] - [Name(ContentTypeNames.RoslynContentType)] - [BaseDefinition("code")] - public static readonly ContentTypeDefinition RoslynContentTypeDefinition; - } + /// + /// Definition of a content type that is a base definition for all content types supported by Roslyn. + /// + [Export] + [Name(ContentTypeNames.RoslynContentType)] + [BaseDefinition("code")] + public static readonly ContentTypeDefinition RoslynContentTypeDefinition; } diff --git a/src/EditorFeatures/Core/Editor/GoToAdjacentMemberCommandHandler.cs b/src/EditorFeatures/Core/Editor/GoToAdjacentMemberCommandHandler.cs index 164decf143516..760afed63291f 100644 --- a/src/EditorFeatures/Core/Editor/GoToAdjacentMemberCommandHandler.cs +++ b/src/EditorFeatures/Core/Editor/GoToAdjacentMemberCommandHandler.cs @@ -22,124 +22,123 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.RoslynContentType)] +[Name(PredefinedCommandHandlerNames.GoToAdjacentMember)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class GoToAdjacentMemberCommandHandler(IOutliningManagerService outliningManagerService) : + ICommandHandler, + ICommandHandler { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.RoslynContentType)] - [Name(PredefinedCommandHandlerNames.GoToAdjacentMember)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class GoToAdjacentMemberCommandHandler(IOutliningManagerService outliningManagerService) : - ICommandHandler, - ICommandHandler + private readonly IOutliningManagerService _outliningManagerService = outliningManagerService; + + public string DisplayName => EditorFeaturesResources.Go_To_Adjacent_Member; + + public CommandState GetCommandState(GoToNextMemberCommandArgs args) + => GetCommandStateImpl(args); + + public bool ExecuteCommand(GoToNextMemberCommandArgs args, CommandExecutionContext context) + => ExecuteCommandImpl(args, gotoNextMember: true, context); + + public CommandState GetCommandState(GoToPreviousMemberCommandArgs args) + => GetCommandStateImpl(args); + + public bool ExecuteCommand(GoToPreviousMemberCommandArgs args, CommandExecutionContext context) + => ExecuteCommandImpl(args, gotoNextMember: false, context); + + private static CommandState GetCommandStateImpl(EditorCommandArgs args) { - private readonly IOutliningManagerService _outliningManagerService = outliningManagerService; + var subjectBuffer = args.SubjectBuffer; + var caretPoint = args.TextView.GetCaretPoint(subjectBuffer); + if (!caretPoint.HasValue || !subjectBuffer.SupportsNavigationToAnyPosition()) + { + return CommandState.Unspecified; + } - public string DisplayName => EditorFeaturesResources.Go_To_Adjacent_Member; + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document?.SupportsSyntaxTree != true) + { + return CommandState.Unspecified; + } - public CommandState GetCommandState(GoToNextMemberCommandArgs args) - => GetCommandStateImpl(args); + return CommandState.Available; + } - public bool ExecuteCommand(GoToNextMemberCommandArgs args, CommandExecutionContext context) - => ExecuteCommandImpl(args, gotoNextMember: true, context); + private bool ExecuteCommandImpl(EditorCommandArgs args, bool gotoNextMember, CommandExecutionContext context) + { + var subjectBuffer = args.SubjectBuffer; + var caretPoint = args.TextView.GetCaretPoint(subjectBuffer); + if (!caretPoint.HasValue || !subjectBuffer.SupportsNavigationToAnyPosition()) + { + return false; + } - public CommandState GetCommandState(GoToPreviousMemberCommandArgs args) - => GetCommandStateImpl(args); + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + var syntaxFactsService = document?.GetLanguageService(); + if (syntaxFactsService == null) + { + return false; + } - public bool ExecuteCommand(GoToPreviousMemberCommandArgs args, CommandExecutionContext context) - => ExecuteCommandImpl(args, gotoNextMember: false, context); + int? targetPosition = null; + using (context.OperationContext.AddScope(allowCancellation: true, description: EditorFeaturesResources.Navigating)) + { + var root = document.GetSyntaxRootSynchronously(context.OperationContext.UserCancellationToken); + targetPosition = GetTargetPosition(syntaxFactsService, root, caretPoint.Value.Position, gotoNextMember); + } - private static CommandState GetCommandStateImpl(EditorCommandArgs args) + if (targetPosition != null) { - var subjectBuffer = args.SubjectBuffer; - var caretPoint = args.TextView.GetCaretPoint(subjectBuffer); - if (!caretPoint.HasValue || !subjectBuffer.SupportsNavigationToAnyPosition()) - { - return CommandState.Unspecified; - } - - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document?.SupportsSyntaxTree != true) - { - return CommandState.Unspecified; - } - - return CommandState.Available; + args.TextView.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(subjectBuffer.CurrentSnapshot, targetPosition.Value), _outliningManagerService); } - private bool ExecuteCommandImpl(EditorCommandArgs args, bool gotoNextMember, CommandExecutionContext context) + return true; + } + + /// + /// Internal for testing purposes. + /// + internal static int? GetTargetPosition(ISyntaxFactsService service, SyntaxNode root, int caretPosition, bool next) + { + var members = service.GetMethodLevelMembers(root); + if (members.Count == 0) { - var subjectBuffer = args.SubjectBuffer; - var caretPoint = args.TextView.GetCaretPoint(subjectBuffer); - if (!caretPoint.HasValue || !subjectBuffer.SupportsNavigationToAnyPosition()) - { - return false; - } - - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - var syntaxFactsService = document?.GetLanguageService(); - if (syntaxFactsService == null) - { - return false; - } - - int? targetPosition = null; - using (context.OperationContext.AddScope(allowCancellation: true, description: EditorFeaturesResources.Navigating)) - { - var root = document.GetSyntaxRootSynchronously(context.OperationContext.UserCancellationToken); - targetPosition = GetTargetPosition(syntaxFactsService, root, caretPoint.Value.Position, gotoNextMember); - } - - if (targetPosition != null) - { - args.TextView.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(subjectBuffer.CurrentSnapshot, targetPosition.Value), _outliningManagerService); - } - - return true; + return null; } - /// - /// Internal for testing purposes. - /// - internal static int? GetTargetPosition(ISyntaxFactsService service, SyntaxNode root, int caretPosition, bool next) + var starts = members.Select(m => MemberStart(m)).ToArray(); + var index = Array.BinarySearch(starts, caretPosition); + if (index >= 0) + { + // We're actually contained in a member, go to the next or previous. + index = next ? index + 1 : index - 1; + } + else { - var members = service.GetMethodLevelMembers(root); - if (members.Count == 0) - { - return null; - } - - var starts = members.Select(m => MemberStart(m)).ToArray(); - var index = Array.BinarySearch(starts, caretPosition); - if (index >= 0) - { - // We're actually contained in a member, go to the next or previous. - index = next ? index + 1 : index - 1; - } - else - { - // We're in between to members, ~index gives us the member we're before, so we'll just - // advance to the start of it - index = next ? ~index : ~index - 1; - } - - // Wrap if necessary - if (index >= members.Count) - { - index = 0; - } - else if (index < 0) - { - index = members.Count - 1; - } - - return MemberStart(members[index]); + // We're in between to members, ~index gives us the member we're before, so we'll just + // advance to the start of it + index = next ? ~index : ~index - 1; } - private static int MemberStart(SyntaxNode node) + // Wrap if necessary + if (index >= members.Count) + { + index = 0; + } + else if (index < 0) { - // TODO: Better position within the node (e.g. attributes?) - return node.SpanStart; + index = members.Count - 1; } + + return MemberStart(members[index]); + } + + private static int MemberStart(SyntaxNode node) + { + // TODO: Better position within the node (e.g. attributes?) + return node.SpanStart; } } diff --git a/src/EditorFeatures/Core/Editor/ITextBufferAssociatedViewService.cs b/src/EditorFeatures/Core/Editor/ITextBufferAssociatedViewService.cs index b478492cd1b30..c9c32606f1e80 100644 --- a/src/EditorFeatures/Core/Editor/ITextBufferAssociatedViewService.cs +++ b/src/EditorFeatures/Core/Editor/ITextBufferAssociatedViewService.cs @@ -10,19 +10,18 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface ITextBufferAssociatedViewService { - internal interface ITextBufferAssociatedViewService - { - IEnumerable GetAssociatedTextViews(ITextBuffer textBuffer); + IEnumerable GetAssociatedTextViews(ITextBuffer textBuffer); - event EventHandler SubjectBuffersConnected; - event EventHandler SubjectBuffersDisconnected; - } + event EventHandler SubjectBuffersConnected; + event EventHandler SubjectBuffersDisconnected; +} - internal class SubjectBuffersConnectedEventArgs(ITextView textView, ReadOnlyCollection subjectBuffers) - { - public ReadOnlyCollection SubjectBuffers { get; } = subjectBuffers; - public ITextView TextView { get; } = textView; - } +internal class SubjectBuffersConnectedEventArgs(ITextView textView, ReadOnlyCollection subjectBuffers) +{ + public ReadOnlyCollection SubjectBuffers { get; } = subjectBuffers; + public ITextView TextView { get; } = textView; } diff --git a/src/EditorFeatures/Core/Editor/ITextUndoHistoryWorkspaceService.cs b/src/EditorFeatures/Core/Editor/ITextUndoHistoryWorkspaceService.cs index 990714bfea107..6dbfc7d6bb813 100644 --- a/src/EditorFeatures/Core/Editor/ITextUndoHistoryWorkspaceService.cs +++ b/src/EditorFeatures/Core/Editor/ITextUndoHistoryWorkspaceService.cs @@ -8,10 +8,9 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface ITextUndoHistoryWorkspaceService : IWorkspaceService { - internal interface ITextUndoHistoryWorkspaceService : IWorkspaceService - { - bool TryGetTextUndoHistory(Workspace editorWorkspace, ITextBuffer textBuffer, out ITextUndoHistory undoHistory); - } + bool TryGetTextUndoHistory(Workspace editorWorkspace, ITextBuffer textBuffer, out ITextUndoHistory undoHistory); } diff --git a/src/EditorFeatures/Core/Editor/TextBufferAssociatedViewService.cs b/src/EditorFeatures/Core/Editor/TextBufferAssociatedViewService.cs index bec8714f572a6..5ef3941846cd9 100644 --- a/src/EditorFeatures/Core/Editor/TextBufferAssociatedViewService.cs +++ b/src/EditorFeatures/Core/Editor/TextBufferAssociatedViewService.cs @@ -16,146 +16,145 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +[Export(typeof(ITextViewConnectionListener))] +[ContentType(ContentTypeNames.RoslynContentType)] +[ContentType(ContentTypeNames.XamlContentType)] +[TextViewRole(PredefinedTextViewRoles.Interactive)] +[Export(typeof(ITextBufferAssociatedViewService))] +internal class TextBufferAssociatedViewService : ITextViewConnectionListener, ITextBufferAssociatedViewService { - [Export(typeof(ITextViewConnectionListener))] - [ContentType(ContentTypeNames.RoslynContentType)] - [ContentType(ContentTypeNames.XamlContentType)] - [TextViewRole(PredefinedTextViewRoles.Interactive)] - [Export(typeof(ITextBufferAssociatedViewService))] - internal class TextBufferAssociatedViewService : ITextViewConnectionListener, ITextBufferAssociatedViewService - { #if DEBUG - private static readonly HashSet s_registeredViews = []; + private static readonly HashSet s_registeredViews = []; #endif - private static readonly object s_gate = new(); - private static readonly ConditionalWeakTable> s_map = new(); + private static readonly object s_gate = new(); + private static readonly ConditionalWeakTable> s_map = new(); - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public TextBufferAssociatedViewService() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public TextBufferAssociatedViewService() + { + } - public event EventHandler SubjectBuffersConnected; - public event EventHandler SubjectBuffersDisconnected; + public event EventHandler SubjectBuffersConnected; + public event EventHandler SubjectBuffersDisconnected; - void ITextViewConnectionListener.SubjectBuffersConnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection subjectBuffers) + void ITextViewConnectionListener.SubjectBuffersConnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection subjectBuffers) + { + lock (s_gate) { - lock (s_gate) + // only add roslyn type to tracking map + foreach (var buffer in subjectBuffers.Where(b => IsSupportedContentType(b.ContentType))) { - // only add roslyn type to tracking map - foreach (var buffer in subjectBuffers.Where(b => IsSupportedContentType(b.ContentType))) + if (!s_map.TryGetValue(buffer, out var set)) { - if (!s_map.TryGetValue(buffer, out var set)) - { - set = []; - s_map.Add(buffer, set); - } - - set.Add(textView); - DebugRegisterView_NoLock(textView); + set = []; + s_map.Add(buffer, set); } - } - this.SubjectBuffersConnected?.Invoke(this, new SubjectBuffersConnectedEventArgs(textView, subjectBuffers.ToReadOnlyCollection())); + set.Add(textView); + DebugRegisterView_NoLock(textView); + } } - void ITextViewConnectionListener.SubjectBuffersDisconnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection subjectBuffers) + this.SubjectBuffersConnected?.Invoke(this, new SubjectBuffersConnectedEventArgs(textView, subjectBuffers.ToReadOnlyCollection())); + } + + void ITextViewConnectionListener.SubjectBuffersDisconnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection subjectBuffers) + { + lock (s_gate) { - lock (s_gate) + // we need to check all buffers reported since we will be called after actual changes have happened. + // for example, if content type of a buffer changed, we will be called after it is changed, rather than before it. + foreach (var buffer in subjectBuffers) { - // we need to check all buffers reported since we will be called after actual changes have happened. - // for example, if content type of a buffer changed, we will be called after it is changed, rather than before it. - foreach (var buffer in subjectBuffers) + if (s_map.TryGetValue(buffer, out var set)) { - if (s_map.TryGetValue(buffer, out var set)) + set.Remove(textView); + if (set.Count == 0) { - set.Remove(textView); - if (set.Count == 0) - { - s_map.Remove(buffer); - } + s_map.Remove(buffer); } } } - - this.SubjectBuffersDisconnected?.Invoke(this, new SubjectBuffersConnectedEventArgs(textView, subjectBuffers.ToReadOnlyCollection())); } - private static bool IsSupportedContentType(IContentType contentType) - { - // This list should match the list of exported content types above - return contentType.IsOfType(ContentTypeNames.RoslynContentType) || - contentType.IsOfType(ContentTypeNames.XamlContentType); - } + this.SubjectBuffersDisconnected?.Invoke(this, new SubjectBuffersConnectedEventArgs(textView, subjectBuffers.ToReadOnlyCollection())); + } + + private static bool IsSupportedContentType(IContentType contentType) + { + // This list should match the list of exported content types above + return contentType.IsOfType(ContentTypeNames.RoslynContentType) || + contentType.IsOfType(ContentTypeNames.XamlContentType); + } - private static IList GetTextViews(ITextBuffer textBuffer) + private static IList GetTextViews(ITextBuffer textBuffer) + { + lock (s_gate) { - lock (s_gate) + if (!s_map.TryGetValue(textBuffer, out var set)) { - if (!s_map.TryGetValue(textBuffer, out var set)) - { - return SpecializedCollections.EmptyList(); - } - - return set.ToList(); + return SpecializedCollections.EmptyList(); } + + return set.ToList(); } + } - public IEnumerable GetAssociatedTextViews(ITextBuffer textBuffer) - => GetTextViews(textBuffer); + public IEnumerable GetAssociatedTextViews(ITextBuffer textBuffer) + => GetTextViews(textBuffer); - private static bool HasFocus(ITextView textView) - => textView.HasAggregateFocus; + private static bool HasFocus(ITextView textView) + => textView.HasAggregateFocus; - public static bool AnyAssociatedViewHasFocus(ITextBuffer textBuffer) + public static bool AnyAssociatedViewHasFocus(ITextBuffer textBuffer) + { + if (textBuffer == null) { - if (textBuffer == null) - { - return false; - } - - var views = GetTextViews(textBuffer); - if (views.Count == 0) - { - // We haven't seen the view yet. Assume it is visible. - return true; - } - - return views.Any(HasFocus); + return false; } - [Conditional("DEBUG")] - private static void DebugRegisterView_NoLock(ITextView textView) + var views = GetTextViews(textBuffer); + if (views.Count == 0) { -#if DEBUG - if (s_registeredViews.Add(textView)) - { - textView.Closed += OnTextViewClose; - } -#endif + // We haven't seen the view yet. Assume it is visible. + return true; } + return views.Any(HasFocus); + } + + [Conditional("DEBUG")] + private static void DebugRegisterView_NoLock(ITextView textView) + { #if DEBUG - private static void OnTextViewClose(object sender, EventArgs e) + if (s_registeredViews.Add(textView)) { - var view = sender as ITextView; + textView.Closed += OnTextViewClose; + } +#endif + } - lock (s_gate) +#if DEBUG + private static void OnTextViewClose(object sender, EventArgs e) + { + var view = sender as ITextView; + + lock (s_gate) + { + foreach (var buffer in view.BufferGraph.GetTextBuffers(b => IsSupportedContentType(b.ContentType))) { - foreach (var buffer in view.BufferGraph.GetTextBuffers(b => IsSupportedContentType(b.ContentType))) + if (s_map.TryGetValue(buffer, out var set)) { - if (s_map.TryGetValue(buffer, out var set)) - { - Contract.ThrowIfTrue(set.Contains(view)); - } + Contract.ThrowIfTrue(set.Contains(view)); } - - s_registeredViews.Remove(view); } + + s_registeredViews.Remove(view); } -#endif } +#endif } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/ISettingsAggregator.cs b/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/ISettingsAggregator.cs index 74520e4ac98dc..ddc0ab75d128d 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/ISettingsAggregator.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/ISettingsAggregator.cs @@ -5,10 +5,9 @@ using Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings; + +internal interface ISettingsAggregator : IWorkspaceService { - internal interface ISettingsAggregator : IWorkspaceService - { - ISettingsProvider? GetSettingsProvider(string fileName); - } + ISettingsProvider? GetSettingsProvider(string fileName); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregator.cs b/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregator.cs index 9b6004b25a9d9..ead1708f6bec9 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregator.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregator.cs @@ -9,99 +9,98 @@ using Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data; using Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings; + +internal partial class SettingsAggregator : ISettingsAggregator { - internal partial class SettingsAggregator : ISettingsAggregator + private readonly Workspace _workspace; + private readonly ISettingsProviderFactory _analyzerProvider; + private ISettingsProviderFactory _whitespaceProvider; + private ISettingsProviderFactory _namingStyleProvider; + private ISettingsProviderFactory _codeStyleProvider; + + public SettingsAggregator(Workspace workspace) { - private readonly Workspace _workspace; - private readonly ISettingsProviderFactory _analyzerProvider; - private ISettingsProviderFactory _whitespaceProvider; - private ISettingsProviderFactory _namingStyleProvider; - private ISettingsProviderFactory _codeStyleProvider; + _workspace = workspace; + _workspace.WorkspaceChanged += UpdateProviders; + _whitespaceProvider = GetOptionsProviderFactory(_workspace); + _codeStyleProvider = GetOptionsProviderFactory(_workspace); + _namingStyleProvider = GetOptionsProviderFactory(_workspace); + _analyzerProvider = GetOptionsProviderFactory(_workspace); + } - public SettingsAggregator(Workspace workspace) + private void UpdateProviders(object? sender, WorkspaceChangeEventArgs e) + { + switch (e.Kind) { - _workspace = workspace; - _workspace.WorkspaceChanged += UpdateProviders; - _whitespaceProvider = GetOptionsProviderFactory(_workspace); - _codeStyleProvider = GetOptionsProviderFactory(_workspace); - _namingStyleProvider = GetOptionsProviderFactory(_workspace); - _analyzerProvider = GetOptionsProviderFactory(_workspace); + case WorkspaceChangeKind.SolutionChanged: + case WorkspaceChangeKind.SolutionAdded: + case WorkspaceChangeKind.SolutionRemoved: + case WorkspaceChangeKind.SolutionCleared: + case WorkspaceChangeKind.SolutionReloaded: + case WorkspaceChangeKind.ProjectAdded: + case WorkspaceChangeKind.ProjectRemoved: + case WorkspaceChangeKind.ProjectChanged: + _whitespaceProvider = GetOptionsProviderFactory(_workspace); + _codeStyleProvider = GetOptionsProviderFactory(_workspace); + _namingStyleProvider = GetOptionsProviderFactory(_workspace); + break; + default: + break; } + } - private void UpdateProviders(object? sender, WorkspaceChangeEventArgs e) + public ISettingsProvider? GetSettingsProvider(string fileName) + { + if (typeof(TData) == typeof(AnalyzerSetting)) { - switch (e.Kind) - { - case WorkspaceChangeKind.SolutionChanged: - case WorkspaceChangeKind.SolutionAdded: - case WorkspaceChangeKind.SolutionRemoved: - case WorkspaceChangeKind.SolutionCleared: - case WorkspaceChangeKind.SolutionReloaded: - case WorkspaceChangeKind.ProjectAdded: - case WorkspaceChangeKind.ProjectRemoved: - case WorkspaceChangeKind.ProjectChanged: - _whitespaceProvider = GetOptionsProviderFactory(_workspace); - _codeStyleProvider = GetOptionsProviderFactory(_workspace); - _namingStyleProvider = GetOptionsProviderFactory(_workspace); - break; - default: - break; - } + return (ISettingsProvider)_analyzerProvider.GetForFile(fileName); } - public ISettingsProvider? GetSettingsProvider(string fileName) + if (typeof(TData) == typeof(Setting)) { - if (typeof(TData) == typeof(AnalyzerSetting)) - { - return (ISettingsProvider)_analyzerProvider.GetForFile(fileName); - } + return (ISettingsProvider)_whitespaceProvider.GetForFile(fileName); + } - if (typeof(TData) == typeof(Setting)) - { - return (ISettingsProvider)_whitespaceProvider.GetForFile(fileName); - } + if (typeof(TData) == typeof(NamingStyleSetting)) + { + return (ISettingsProvider)_namingStyleProvider.GetForFile(fileName); + } - if (typeof(TData) == typeof(NamingStyleSetting)) - { - return (ISettingsProvider)_namingStyleProvider.GetForFile(fileName); - } + if (typeof(TData) == typeof(CodeStyleSetting)) + { + return (ISettingsProvider)_codeStyleProvider.GetForFile(fileName); + } - if (typeof(TData) == typeof(CodeStyleSetting)) - { - return (ISettingsProvider)_codeStyleProvider.GetForFile(fileName); - } + return null; + } - return null; + private static ISettingsProviderFactory GetOptionsProviderFactory(Workspace workspace) + { + var providers = new List>(); + var commonProvider = workspace.Services.GetRequiredService>(); + providers.Add(commonProvider); + var solution = workspace.CurrentSolution; + var supportsCSharp = solution.Projects.Any(p => p.Language.Equals(LanguageNames.CSharp, StringComparison.OrdinalIgnoreCase)); + var supportsVisualBasic = solution.Projects.Any(p => p.Language.Equals(LanguageNames.VisualBasic, StringComparison.OrdinalIgnoreCase)); + if (supportsCSharp) + { + TryAddProviderForLanguage(LanguageNames.CSharp, workspace, providers); } - private static ISettingsProviderFactory GetOptionsProviderFactory(Workspace workspace) + if (supportsVisualBasic) { - var providers = new List>(); - var commonProvider = workspace.Services.GetRequiredService>(); - providers.Add(commonProvider); - var solution = workspace.CurrentSolution; - var supportsCSharp = solution.Projects.Any(p => p.Language.Equals(LanguageNames.CSharp, StringComparison.OrdinalIgnoreCase)); - var supportsVisualBasic = solution.Projects.Any(p => p.Language.Equals(LanguageNames.VisualBasic, StringComparison.OrdinalIgnoreCase)); - if (supportsCSharp) - { - TryAddProviderForLanguage(LanguageNames.CSharp, workspace, providers); - } - - if (supportsVisualBasic) - { - TryAddProviderForLanguage(LanguageNames.VisualBasic, workspace, providers); - } + TryAddProviderForLanguage(LanguageNames.VisualBasic, workspace, providers); + } - return new CombinedOptionsProviderFactory(providers.ToImmutableArray()); + return new CombinedOptionsProviderFactory(providers.ToImmutableArray()); - static void TryAddProviderForLanguage(string language, Workspace workspace, List> providers) + static void TryAddProviderForLanguage(string language, Workspace workspace, List> providers) + { + var provider = workspace.Services.GetLanguageServices(language).GetService>(); + if (provider is not null) { - var provider = workspace.Services.GetLanguageServices(language).GetService>(); - if (provider is not null) - { - providers.Add(provider); - } + providers.Add(provider); } } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregatorFactory.cs b/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregatorFactory.cs index 87a8598b69e32..c0baf26042b58 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregatorFactory.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregatorFactory.cs @@ -7,19 +7,17 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings -{ +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings; - [ExportWorkspaceServiceFactory(typeof(ISettingsAggregator), ServiceLayer.Default), Shared] - internal class SettingsAggregatorFactory : IWorkspaceServiceFactory +[ExportWorkspaceServiceFactory(typeof(ISettingsAggregator), ServiceLayer.Default), Shared] +internal class SettingsAggregatorFactory : IWorkspaceServiceFactory +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public SettingsAggregatorFactory() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public SettingsAggregatorFactory() - { - } - - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new SettingsAggregator(workspaceServices.Workspace); } + + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => new SettingsAggregator(workspaceServices.Workspace); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Data/AnalyzerSetting.cs b/src/EditorFeatures/Core/EditorConfigSettings/Data/AnalyzerSetting.cs index 0874cbecbdfb8..1d8f711aece27 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Data/AnalyzerSetting.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Data/AnalyzerSetting.cs @@ -9,51 +9,50 @@ using Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; using Microsoft.CodeAnalysis.EditorConfig; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data; + +internal class AnalyzerSetting { - internal class AnalyzerSetting + private readonly DiagnosticDescriptor _descriptor; + private readonly AnalyzerSettingsUpdater _settingsUpdater; + + public AnalyzerSetting(DiagnosticDescriptor descriptor, + ReportDiagnostic effectiveSeverity, + AnalyzerSettingsUpdater settingsUpdater, + Language language, + SettingLocation location) { - private readonly DiagnosticDescriptor _descriptor; - private readonly AnalyzerSettingsUpdater _settingsUpdater; - - public AnalyzerSetting(DiagnosticDescriptor descriptor, - ReportDiagnostic effectiveSeverity, - AnalyzerSettingsUpdater settingsUpdater, - Language language, - SettingLocation location) + _descriptor = descriptor; + _settingsUpdater = settingsUpdater; + if (effectiveSeverity == ReportDiagnostic.Default) { - _descriptor = descriptor; - _settingsUpdater = settingsUpdater; - if (effectiveSeverity == ReportDiagnostic.Default) - { - effectiveSeverity = descriptor.DefaultSeverity.ToReportDiagnostic(); - } - - var enabled = effectiveSeverity != ReportDiagnostic.Suppress; - IsEnabled = enabled; - Severity = effectiveSeverity; - Language = language; - IsNotConfigurable = descriptor.CustomTags.Any(t => t == WellKnownDiagnosticTags.NotConfigurable); - Location = location; + effectiveSeverity = descriptor.DefaultSeverity.ToReportDiagnostic(); } - public string Id => _descriptor.Id; - public string Title => _descriptor.Title.ToString(CultureInfo.CurrentUICulture); - public string Description => _descriptor.Description.ToString(CultureInfo.CurrentUICulture); - public string Category => _descriptor.Category; - public ReportDiagnostic Severity { get; private set; } - public bool IsEnabled { get; private set; } - public Language Language { get; } - public bool IsNotConfigurable { get; set; } - public SettingLocation Location { get; } - - internal void ChangeSeverity(ReportDiagnostic severity) - { - if (severity == Severity) - return; + var enabled = effectiveSeverity != ReportDiagnostic.Suppress; + IsEnabled = enabled; + Severity = effectiveSeverity; + Language = language; + IsNotConfigurable = descriptor.CustomTags.Any(t => t == WellKnownDiagnosticTags.NotConfigurable); + Location = location; + } - Severity = severity; - _settingsUpdater.QueueUpdate(this, severity); - } + public string Id => _descriptor.Id; + public string Title => _descriptor.Title.ToString(CultureInfo.CurrentUICulture); + public string Description => _descriptor.Description.ToString(CultureInfo.CurrentUICulture); + public string Category => _descriptor.Category; + public ReportDiagnostic Severity { get; private set; } + public bool IsEnabled { get; private set; } + public Language Language { get; } + public bool IsNotConfigurable { get; set; } + public SettingLocation Location { get; } + + internal void ChangeSeverity(ReportDiagnostic severity) + { + if (severity == Severity) + return; + + Severity = severity; + _settingsUpdater.QueueUpdate(this, severity); } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Data/CodeStyleSetting.cs b/src/EditorFeatures/Core/EditorConfigSettings/Data/CodeStyleSetting.cs index 8b9b8511a2139..e04f0211966f3 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Data/CodeStyleSetting.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Data/CodeStyleSetting.cs @@ -7,115 +7,114 @@ using Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data; + +internal abstract class CodeStyleSetting(OptionKey2 optionKey, string description, OptionUpdater updater, SettingLocation location) : Setting(optionKey, description, updater, location) { - internal abstract class CodeStyleSetting(OptionKey2 optionKey, string description, OptionUpdater updater, SettingLocation location) : Setting(optionKey, description, updater, location) - { - private static readonly bool[] s_boolValues = [true, false]; + private static readonly bool[] s_boolValues = [true, false]; - public abstract ICodeStyleOption GetCodeStyle(); + public abstract ICodeStyleOption GetCodeStyle(); - public ReportDiagnostic GetSeverity() - { - var severity = GetCodeStyle().Notification.Severity; - if (severity is ReportDiagnostic.Default or ReportDiagnostic.Suppress) - severity = ReportDiagnostic.Hidden; - return severity; - } + public ReportDiagnostic GetSeverity() + { + var severity = GetCodeStyle().Notification.Severity; + if (severity is ReportDiagnostic.Default or ReportDiagnostic.Suppress) + severity = ReportDiagnostic.Hidden; + return severity; + } - public sealed override object? GetValue() - => GetCodeStyle(); + public sealed override object? GetValue() + => GetCodeStyle(); - public abstract string[] GetValueDescriptions(); - public abstract string GetCurrentValueDescription(); + public abstract string[] GetValueDescriptions(); + public abstract string GetCurrentValueDescription(); - protected abstract object GetPossibleValue(int valueIndex); + protected abstract object GetPossibleValue(int valueIndex); - public void ChangeSeverity(ReportDiagnostic severity) - { - var notification = severity switch - { - ReportDiagnostic.Hidden => NotificationOption2.Silent, - ReportDiagnostic.Info => NotificationOption2.Suggestion, - ReportDiagnostic.Warn => NotificationOption2.Warning, - ReportDiagnostic.Error => NotificationOption2.Error, - _ => NotificationOption2.None, - }; - - SetValue(GetCodeStyle().WithNotification(notification)); - } - - public void ChangeValue(int valueIndex) - { - SetValue(GetCodeStyle().WithValue(GetPossibleValue(valueIndex))); - } - - internal static CodeStyleSetting Create( - Option2> option, - string description, - TieredAnalyzerConfigOptions options, - OptionUpdater updater, - string? trueValueDescription = null, - string? falseValueDescription = null) - { - var optionKey = new OptionKey2(option); - options.GetInitialLocationAndValue>(option, out var initialLocation, out var initialValue); - - var valueDescriptions = new[] - { - trueValueDescription ?? EditorFeaturesResources.Yes, - falseValueDescription ?? EditorFeaturesResources.No - }; - - return new CodeStyleSetting(optionKey, description, updater, initialLocation, initialValue, s_boolValues, valueDescriptions); - } - - internal static CodeStyleSetting Create( - PerLanguageOption2> option, - string description, - TieredAnalyzerConfigOptions options, - OptionUpdater updater, - string? trueValueDescription = null, - string? falseValueDescription = null) + public void ChangeSeverity(ReportDiagnostic severity) + { + var notification = severity switch { - var optionKey = new OptionKey2(option, options.Language); - options.GetInitialLocationAndValue>(option, out var initialLocation, out var initialValue); - - var valueDescriptions = new[] - { - trueValueDescription ?? EditorFeaturesResources.Yes, - falseValueDescription ?? EditorFeaturesResources.No - }; - - return new CodeStyleSetting(optionKey, description, updater, initialLocation, initialValue, s_boolValues, valueDescriptions); - } - - internal static CodeStyleSetting Create( - Option2> option, - string description, - TieredAnalyzerConfigOptions options, - OptionUpdater updater, - T[] enumValues, - string[] valueDescriptions) - where T : Enum + ReportDiagnostic.Hidden => NotificationOption2.Silent, + ReportDiagnostic.Info => NotificationOption2.Suggestion, + ReportDiagnostic.Warn => NotificationOption2.Warning, + ReportDiagnostic.Error => NotificationOption2.Error, + _ => NotificationOption2.None, + }; + + SetValue(GetCodeStyle().WithNotification(notification)); + } + + public void ChangeValue(int valueIndex) + { + SetValue(GetCodeStyle().WithValue(GetPossibleValue(valueIndex))); + } + + internal static CodeStyleSetting Create( + Option2> option, + string description, + TieredAnalyzerConfigOptions options, + OptionUpdater updater, + string? trueValueDescription = null, + string? falseValueDescription = null) + { + var optionKey = new OptionKey2(option); + options.GetInitialLocationAndValue>(option, out var initialLocation, out var initialValue); + + var valueDescriptions = new[] { - var optionKey = new OptionKey2(option); - options.GetInitialLocationAndValue>(option, out var initialLocation, out var initialValue); - return new CodeStyleSetting(optionKey, description, updater, initialLocation, initialValue, enumValues, valueDescriptions); - } - - internal static CodeStyleSetting Create( - PerLanguageOption2> option, - string description, - TieredAnalyzerConfigOptions options, - OptionUpdater updater, - T[] enumValues, - string[] valueDescriptions) - where T : Enum + trueValueDescription ?? EditorFeaturesResources.Yes, + falseValueDescription ?? EditorFeaturesResources.No + }; + + return new CodeStyleSetting(optionKey, description, updater, initialLocation, initialValue, s_boolValues, valueDescriptions); + } + + internal static CodeStyleSetting Create( + PerLanguageOption2> option, + string description, + TieredAnalyzerConfigOptions options, + OptionUpdater updater, + string? trueValueDescription = null, + string? falseValueDescription = null) + { + var optionKey = new OptionKey2(option, options.Language); + options.GetInitialLocationAndValue>(option, out var initialLocation, out var initialValue); + + var valueDescriptions = new[] { - var optionKey = new OptionKey2(option, options.Language); - options.GetInitialLocationAndValue>(option, out var initialLocation, out var initialValue); - return new CodeStyleSetting(optionKey, description, updater, initialLocation, initialValue, enumValues, valueDescriptions); - } + trueValueDescription ?? EditorFeaturesResources.Yes, + falseValueDescription ?? EditorFeaturesResources.No + }; + + return new CodeStyleSetting(optionKey, description, updater, initialLocation, initialValue, s_boolValues, valueDescriptions); + } + + internal static CodeStyleSetting Create( + Option2> option, + string description, + TieredAnalyzerConfigOptions options, + OptionUpdater updater, + T[] enumValues, + string[] valueDescriptions) + where T : Enum + { + var optionKey = new OptionKey2(option); + options.GetInitialLocationAndValue>(option, out var initialLocation, out var initialValue); + return new CodeStyleSetting(optionKey, description, updater, initialLocation, initialValue, enumValues, valueDescriptions); + } + + internal static CodeStyleSetting Create( + PerLanguageOption2> option, + string description, + TieredAnalyzerConfigOptions options, + OptionUpdater updater, + T[] enumValues, + string[] valueDescriptions) + where T : Enum + { + var optionKey = new OptionKey2(option, options.Language); + options.GetInitialLocationAndValue>(option, out var initialLocation, out var initialValue); + return new CodeStyleSetting(optionKey, description, updater, initialLocation, initialValue, enumValues, valueDescriptions); } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Data/Conversions.cs b/src/EditorFeatures/Core/EditorConfigSettings/Data/Conversions.cs index 268edb23a5ca2..03f2d4f393b87 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Data/Conversions.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Data/Conversions.cs @@ -4,11 +4,10 @@ using System; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data; + +internal readonly struct Conversions(Func to, Func from) { - internal readonly struct Conversions(Func to, Func from) - { - public readonly Func To = to; - public readonly Func From = from; - } + public readonly Func To = to; + public readonly Func From = from; } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Data/NamingStyleSetting.cs b/src/EditorFeatures/Core/EditorConfigSettings/Data/NamingStyleSetting.cs index 7423cd05b856c..15b1edbd97a14 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Data/NamingStyleSetting.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Data/NamingStyleSetting.cs @@ -9,86 +9,85 @@ using Microsoft.CodeAnalysis.EditorConfig.Parsing.NamingStyles; using Microsoft.CodeAnalysis.NamingStyles; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data; + +internal class NamingStyleSetting { - internal class NamingStyleSetting - { - private NamingStyle[] _allStyles; - private readonly NamingStyleSettingsUpdater? _settingsUpdater; + private NamingStyle[] _allStyles; + private readonly NamingStyleSettingsUpdater? _settingsUpdater; - public NamingStyleSetting( - NamingRule namingRule, - NamingStyle[] allStyles, - NamingStyleSettingsUpdater settingsUpdater, - string? fileName = null) - { - Style = namingRule.NamingStyle; - _allStyles = allStyles; - Type = namingRule.SymbolSpecification; - Severity = namingRule.EnforcementLevel; - _settingsUpdater = settingsUpdater; - Location = new SettingLocation(fileName is null ? LocationKind.VisualStudio : LocationKind.EditorConfig, fileName); - } + public NamingStyleSetting( + NamingRule namingRule, + NamingStyle[] allStyles, + NamingStyleSettingsUpdater settingsUpdater, + string? fileName = null) + { + Style = namingRule.NamingStyle; + _allStyles = allStyles; + Type = namingRule.SymbolSpecification; + Severity = namingRule.EnforcementLevel; + _settingsUpdater = settingsUpdater; + Location = new SettingLocation(fileName is null ? LocationKind.VisualStudio : LocationKind.EditorConfig, fileName); + } - private NamingStyleSetting() - { - _allStyles = []; - } + private NamingStyleSetting() + { + _allStyles = []; + } - public event EventHandler? SettingChanged; + public event EventHandler? SettingChanged; - internal static NamingStyleSetting FromParseResult(NamingStyleOption namingStyleOption) + internal static NamingStyleSetting FromParseResult(NamingStyleOption namingStyleOption) + { + return new NamingStyleSetting { - return new NamingStyleSetting - { - Style = namingStyleOption.NamingScheme.AsNamingStyle(), - Type = namingStyleOption.ApplicableSymbolInfo.AsSymbolSpecification(), - Severity = namingStyleOption.Severity, - Location = new SettingLocation(LocationKind.EditorConfig, namingStyleOption.Section.FilePath) - }; - } + Style = namingStyleOption.NamingScheme.AsNamingStyle(), + Type = namingStyleOption.ApplicableSymbolInfo.AsSymbolSpecification(), + Severity = namingStyleOption.Severity, + Location = new SettingLocation(LocationKind.EditorConfig, namingStyleOption.Section.FilePath) + }; + } - internal NamingStyle Style { get; set; } - internal SymbolSpecification? Type { get; set; } + internal NamingStyle Style { get; set; } + internal SymbolSpecification? Type { get; set; } - public string StyleName => Style.Name; - public string[] AllStyles => _allStyles.Select(style => style.Name).ToArray(); - public string TypeName => Type?.Name ?? string.Empty; - public ReportDiagnostic Severity { get; private set; } - public SettingLocation? Location { get; protected set; } + public string StyleName => Style.Name; + public string[] AllStyles => _allStyles.Select(style => style.Name).ToArray(); + public string TypeName => Type?.Name ?? string.Empty; + public ReportDiagnostic Severity { get; private set; } + public SettingLocation? Location { get; protected set; } - private void OnSettingChanged((object, object?) setting) + private void OnSettingChanged((object, object?) setting) + { + if (setting is (ReportDiagnostic severity, _)) { - if (setting is (ReportDiagnostic severity, _)) - { - Severity = severity; - SettingChanged?.Invoke(this, EventArgs.Empty); - } + Severity = severity; + SettingChanged?.Invoke(this, EventArgs.Empty); + } - if (setting is (NamingStyle style, NamingStyle[] allStyles)) - { - Style = style; - _allStyles = allStyles; - SettingChanged?.Invoke(this, EventArgs.Empty); - } + if (setting is (NamingStyle style, NamingStyle[] allStyles)) + { + Style = style; + _allStyles = allStyles; + SettingChanged?.Invoke(this, EventArgs.Empty); } + } - internal void ChangeSeverity(ReportDiagnostic severity) + internal void ChangeSeverity(ReportDiagnostic severity) + { + if (Location is not null) { - if (Location is not null) - { - Location = Location with { LocationKind = LocationKind.EditorConfig }; - _settingsUpdater?.QueueUpdate((OnSettingChanged, this), severity); - } + Location = Location with { LocationKind = LocationKind.EditorConfig }; + _settingsUpdater?.QueueUpdate((OnSettingChanged, this), severity); } + } - internal void ChangeStyle(int selectedIndex) + internal void ChangeStyle(int selectedIndex) + { + if (selectedIndex > -1 && selectedIndex < _allStyles.Length && Location is not null) { - if (selectedIndex > -1 && selectedIndex < _allStyles.Length && Location is not null) - { - Location = Location with { LocationKind = LocationKind.EditorConfig }; - _settingsUpdater?.QueueUpdate((OnSettingChanged, this), _allStyles[selectedIndex]); - } + Location = Location with { LocationKind = LocationKind.EditorConfig }; + _settingsUpdater?.QueueUpdate((OnSettingChanged, this), _allStyles[selectedIndex]); } } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Data/Setting.cs b/src/EditorFeatures/Core/EditorConfigSettings/Data/Setting.cs index fe0dd62a71476..c30c2dd2e6ea0 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Data/Setting.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Data/Setting.cs @@ -8,76 +8,75 @@ using Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data; + +internal abstract class Setting { - internal abstract class Setting - { - public OptionKey2 Key { get; } - public OptionUpdater Updater { get; } - public string Description { get; } + public OptionKey2 Key { get; } + public OptionUpdater Updater { get; } + public string Description { get; } - public SettingLocation Location { get; private set; } + public SettingLocation Location { get; private set; } - protected Setting(OptionKey2 optionKey, string description, OptionUpdater updater, SettingLocation location) - { - Key = optionKey; - Description = description; - Updater = updater; - Location = location; - } + protected Setting(OptionKey2 optionKey, string description, OptionUpdater updater, SettingLocation location) + { + Key = optionKey; + Description = description; + Updater = updater; + Location = location; + } - public abstract Type Type { get; } - protected abstract object UpdateValue(object settingValue); - public abstract object? GetValue(); + public abstract Type Type { get; } + protected abstract object UpdateValue(object settingValue); + public abstract object? GetValue(); - public void SetValue(object value) - { - Location = Location with { LocationKind = LocationKind.EditorConfig }; - Updater.QueueUpdate(Key.Option, UpdateValue(value)); - } + public void SetValue(object value) + { + Location = Location with { LocationKind = LocationKind.EditorConfig }; + Updater.QueueUpdate(Key.Option, UpdateValue(value)); + } - public string Category => Key.Option.Definition.Group.Description; - public bool IsDefinedInEditorConfig => Location.LocationKind != LocationKind.VisualStudio; + public string Category => Key.Option.Definition.Group.Description; + public bool IsDefinedInEditorConfig => Location.LocationKind != LocationKind.VisualStudio; - public static Setting Create( - Option2 option, - string description, - TieredAnalyzerConfigOptions options, - OptionUpdater updater) - where TValue : notnull - { - var optionKey = new OptionKey2(option); - options.GetInitialLocationAndValue(option, out var initialLocation, out var initialValue); - return new Setting(optionKey, description, updater, initialLocation, initialValue); - } + public static Setting Create( + Option2 option, + string description, + TieredAnalyzerConfigOptions options, + OptionUpdater updater) + where TValue : notnull + { + var optionKey = new OptionKey2(option); + options.GetInitialLocationAndValue(option, out var initialLocation, out var initialValue); + return new Setting(optionKey, description, updater, initialLocation, initialValue); + } - public static Setting Create( - PerLanguageOption2 option, - string description, - TieredAnalyzerConfigOptions options, - OptionUpdater updater) - where TValue : notnull - { - // TODO: Support for other languages https://github.com/dotnet/roslyn/issues/65859 - var optionKey = new OptionKey2(option, LanguageNames.CSharp); - options.GetInitialLocationAndValue(option, out var initialLocation, out var initialValue); - return new Setting(optionKey, description, updater, initialLocation, initialValue); - } + public static Setting Create( + PerLanguageOption2 option, + string description, + TieredAnalyzerConfigOptions options, + OptionUpdater updater) + where TValue : notnull + { + // TODO: Support for other languages https://github.com/dotnet/roslyn/issues/65859 + var optionKey = new OptionKey2(option, LanguageNames.CSharp); + options.GetInitialLocationAndValue(option, out var initialLocation, out var initialValue); + return new Setting(optionKey, description, updater, initialLocation, initialValue); + } - public static EnumFlagsSetting CreateEnumFlags( - Option2 option, - int flag, - string description, - StrongBox valueStorage, - Conversions conversions, - TieredAnalyzerConfigOptions options, - OptionUpdater updater) - where TValue : struct, Enum - { - var optionKey = new OptionKey2(option); - options.GetInitialLocationAndValue(option, out var initialLocation, out var initialValue); - valueStorage.Value = initialValue; - return new EnumFlagsSetting(optionKey, description, updater, initialLocation, flag, valueStorage, conversions); - } + public static EnumFlagsSetting CreateEnumFlags( + Option2 option, + int flag, + string description, + StrongBox valueStorage, + Conversions conversions, + TieredAnalyzerConfigOptions options, + OptionUpdater updater) + where TValue : struct, Enum + { + var optionKey = new OptionKey2(option); + options.GetInitialLocationAndValue(option, out var initialLocation, out var initialValue); + valueStorage.Value = initialValue; + return new EnumFlagsSetting(optionKey, description, updater, initialLocation, flag, valueStorage, conversions); } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsProvider.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsProvider.cs index b623b75f70a4e..516f2bddf6c02 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsProvider.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsProvider.cs @@ -14,75 +14,74 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using RoslynEnumerableExtensions = Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Extensions.EnumerableExtensions; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.Analyzer +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.Analyzer; + +internal sealed class AnalyzerSettingsProvider : SettingsProviderBase { - internal sealed class AnalyzerSettingsProvider : SettingsProviderBase - { - private readonly IDiagnosticAnalyzerService _analyzerService; + private readonly IDiagnosticAnalyzerService _analyzerService; - public AnalyzerSettingsProvider(string fileName, AnalyzerSettingsUpdater settingsUpdater, Workspace workspace, IDiagnosticAnalyzerService analyzerService) - : base(fileName, settingsUpdater, workspace, analyzerService.GlobalOptions) - { - _analyzerService = analyzerService; - Update(); - } + public AnalyzerSettingsProvider(string fileName, AnalyzerSettingsUpdater settingsUpdater, Workspace workspace, IDiagnosticAnalyzerService analyzerService) + : base(fileName, settingsUpdater, workspace, analyzerService.GlobalOptions) + { + _analyzerService = analyzerService; + Update(); + } - protected override void UpdateOptions(TieredAnalyzerConfigOptions options, ImmutableArray projectsInScope) + protected override void UpdateOptions(TieredAnalyzerConfigOptions options, ImmutableArray projectsInScope) + { + var analyzerReferences = RoslynEnumerableExtensions.DistinctBy(projectsInScope.SelectMany(p => p.AnalyzerReferences), a => a.Id).ToImmutableArray(); + foreach (var analyzerReference in analyzerReferences) { - var analyzerReferences = RoslynEnumerableExtensions.DistinctBy(projectsInScope.SelectMany(p => p.AnalyzerReferences), a => a.Id).ToImmutableArray(); - foreach (var analyzerReference in analyzerReferences) - { - var configSettings = GetSettings(analyzerReference, options.EditorConfigOptions); - AddRange(configSettings); - } + var configSettings = GetSettings(analyzerReference, options.EditorConfigOptions); + AddRange(configSettings); } + } - private IEnumerable GetSettings(AnalyzerReference analyzerReference, AnalyzerConfigOptions editorConfigOptions) - { - IEnumerable csharpAnalyzers = analyzerReference.GetAnalyzers(LanguageNames.CSharp); - IEnumerable visualBasicAnalyzers = analyzerReference.GetAnalyzers(LanguageNames.VisualBasic); - var dotnetAnalyzers = csharpAnalyzers.Intersect(visualBasicAnalyzers, DiagnosticAnalyzerComparer.Instance); - csharpAnalyzers = csharpAnalyzers.Except(dotnetAnalyzers, DiagnosticAnalyzerComparer.Instance); - visualBasicAnalyzers = visualBasicAnalyzers.Except(dotnetAnalyzers, DiagnosticAnalyzerComparer.Instance); - - var csharpSettings = ToAnalyzerSetting(csharpAnalyzers, Language.CSharp); - var csharpAndVisualBasicSettings = csharpSettings.Concat(ToAnalyzerSetting(visualBasicAnalyzers, Language.VisualBasic)); - return csharpAndVisualBasicSettings.Concat(ToAnalyzerSetting(dotnetAnalyzers, Language.CSharp | Language.VisualBasic)); + private IEnumerable GetSettings(AnalyzerReference analyzerReference, AnalyzerConfigOptions editorConfigOptions) + { + IEnumerable csharpAnalyzers = analyzerReference.GetAnalyzers(LanguageNames.CSharp); + IEnumerable visualBasicAnalyzers = analyzerReference.GetAnalyzers(LanguageNames.VisualBasic); + var dotnetAnalyzers = csharpAnalyzers.Intersect(visualBasicAnalyzers, DiagnosticAnalyzerComparer.Instance); + csharpAnalyzers = csharpAnalyzers.Except(dotnetAnalyzers, DiagnosticAnalyzerComparer.Instance); + visualBasicAnalyzers = visualBasicAnalyzers.Except(dotnetAnalyzers, DiagnosticAnalyzerComparer.Instance); - IEnumerable ToAnalyzerSetting(IEnumerable analyzers, - Language language) - { - return analyzers - .SelectMany(a => _analyzerService.AnalyzerInfoCache.GetDiagnosticDescriptors(a)) - .GroupBy(d => d.Id) - .OrderBy(g => g.Key, StringComparer.CurrentCulture) - .Select(g => - { - var selectedDiagnostic = g.First(); - var isEditorconfig = selectedDiagnostic.IsDefinedInEditorConfig(editorConfigOptions); - var settingLocation = new SettingLocation(isEditorconfig ? LocationKind.EditorConfig : LocationKind.VisualStudio, FileName); - var severity = selectedDiagnostic.GetEffectiveSeverity(editorConfigOptions); - return new AnalyzerSetting(selectedDiagnostic, severity, SettingsUpdater, language, settingLocation); - }); - } - } + var csharpSettings = ToAnalyzerSetting(csharpAnalyzers, Language.CSharp); + var csharpAndVisualBasicSettings = csharpSettings.Concat(ToAnalyzerSetting(visualBasicAnalyzers, Language.VisualBasic)); + return csharpAndVisualBasicSettings.Concat(ToAnalyzerSetting(dotnetAnalyzers, Language.CSharp | Language.VisualBasic)); - private class DiagnosticAnalyzerComparer : IEqualityComparer + IEnumerable ToAnalyzerSetting(IEnumerable analyzers, + Language language) { - public static readonly DiagnosticAnalyzerComparer Instance = new(); + return analyzers + .SelectMany(a => _analyzerService.AnalyzerInfoCache.GetDiagnosticDescriptors(a)) + .GroupBy(d => d.Id) + .OrderBy(g => g.Key, StringComparer.CurrentCulture) + .Select(g => + { + var selectedDiagnostic = g.First(); + var isEditorconfig = selectedDiagnostic.IsDefinedInEditorConfig(editorConfigOptions); + var settingLocation = new SettingLocation(isEditorconfig ? LocationKind.EditorConfig : LocationKind.VisualStudio, FileName); + var severity = selectedDiagnostic.GetEffectiveSeverity(editorConfigOptions); + return new AnalyzerSetting(selectedDiagnostic, severity, SettingsUpdater, language, settingLocation); + }); + } + } - public bool Equals(DiagnosticAnalyzer? x, DiagnosticAnalyzer? y) - { - if (x is null && y is null) - return true; + private class DiagnosticAnalyzerComparer : IEqualityComparer + { + public static readonly DiagnosticAnalyzerComparer Instance = new(); - if (x is null || y is null) - return false; + public bool Equals(DiagnosticAnalyzer? x, DiagnosticAnalyzer? y) + { + if (x is null && y is null) + return true; - return x.GetAnalyzerIdAndVersion().GetHashCode() == y.GetAnalyzerIdAndVersion().GetHashCode(); - } + if (x is null || y is null) + return false; - public int GetHashCode(DiagnosticAnalyzer obj) => obj.GetAnalyzerIdAndVersion().GetHashCode(); + return x.GetAnalyzerIdAndVersion().GetHashCode() == y.GetAnalyzerIdAndVersion().GetHashCode(); } + + public int GetHashCode(DiagnosticAnalyzer obj) => obj.GetAnalyzerIdAndVersion().GetHashCode(); } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsProviderFactory.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsProviderFactory.cs index bf1ec30ad695d..6d9c634d7a6a2 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsProviderFactory.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsProviderFactory.cs @@ -6,17 +6,16 @@ using Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data; using Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.Analyzer +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.Analyzer; + +internal class AnalyzerSettingsProviderFactory(Workspace workspace, IDiagnosticAnalyzerService analyzerService) : IWorkspaceSettingsProviderFactory { - internal class AnalyzerSettingsProviderFactory(Workspace workspace, IDiagnosticAnalyzerService analyzerService) : IWorkspaceSettingsProviderFactory - { - private readonly Workspace _workspace = workspace; - private readonly IDiagnosticAnalyzerService _analyzerService = analyzerService; + private readonly Workspace _workspace = workspace; + private readonly IDiagnosticAnalyzerService _analyzerService = analyzerService; - public ISettingsProvider GetForFile(string filePath) - { - var updater = new AnalyzerSettingsUpdater(_workspace, filePath); - return new AnalyzerSettingsProvider(filePath, updater, _workspace, _analyzerService); - } + public ISettingsProvider GetForFile(string filePath) + { + var updater = new AnalyzerSettingsUpdater(_workspace, filePath); + return new AnalyzerSettingsProvider(filePath, updater, _workspace, _analyzerService); } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsWorkspaceServiceFactory.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsWorkspaceServiceFactory.cs index 5e640667f94c3..b098b95760e16 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsWorkspaceServiceFactory.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/Analyzer/AnalyzerSettingsWorkspaceServiceFactory.cs @@ -9,16 +9,15 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.Analyzer +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.Analyzer; + +[ExportWorkspaceServiceFactory(typeof(IWorkspaceSettingsProviderFactory)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class AnalyzerSettingsWorkspaceServiceFactory(IDiagnosticAnalyzerService analyzerService) : IWorkspaceServiceFactory { - [ExportWorkspaceServiceFactory(typeof(IWorkspaceSettingsProviderFactory)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class AnalyzerSettingsWorkspaceServiceFactory(IDiagnosticAnalyzerService analyzerService) : IWorkspaceServiceFactory - { - private readonly IDiagnosticAnalyzerService _analyzerService = analyzerService; + private readonly IDiagnosticAnalyzerService _analyzerService = analyzerService; - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new AnalyzerSettingsProviderFactory(workspaceServices.Workspace, _analyzerService); - } + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => new AnalyzerSettingsProviderFactory(workspaceServices.Workspace, _analyzerService); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/CombinedOptionsProviderFactory.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/CombinedOptionsProviderFactory.cs index 3965dc5429160..1860c6e935398 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/CombinedOptionsProviderFactory.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/CombinedOptionsProviderFactory.cs @@ -5,21 +5,20 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Shared.Collections; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider; + +internal class CombinedOptionsProviderFactory(ImmutableArray> factories) : ISettingsProviderFactory { - internal class CombinedOptionsProviderFactory(ImmutableArray> factories) : ISettingsProviderFactory - { - private readonly ImmutableArray> _factories = factories; + private readonly ImmutableArray> _factories = factories; - public ISettingsProvider GetForFile(string filePath) + public ISettingsProvider GetForFile(string filePath) + { + var providers = TemporaryArray>.Empty; + foreach (var factory in _factories) { - var providers = TemporaryArray>.Empty; - foreach (var factory in _factories) - { - providers.Add(factory.GetForFile(filePath)); - } - - return new CombinedProvider(providers.ToImmutableAndClear()); + providers.Add(factory.GetForFile(filePath)); } + + return new CombinedProvider(providers.ToImmutableAndClear()); } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/CombinedProvider.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/CombinedProvider.cs index 4598452dbc5f8..8e9bd93ea38f4 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/CombinedProvider.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/CombinedProvider.cs @@ -6,39 +6,38 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider; + +internal class CombinedProvider(ImmutableArray> providers) : ISettingsProvider { - internal class CombinedProvider(ImmutableArray> providers) : ISettingsProvider - { - private readonly ImmutableArray> _providers = providers; + private readonly ImmutableArray> _providers = providers; - public async Task GetChangedEditorConfigAsync(SourceText sourceText) + public async Task GetChangedEditorConfigAsync(SourceText sourceText) + { + foreach (var provider in _providers) { - foreach (var provider in _providers) - { - sourceText = await provider.GetChangedEditorConfigAsync(sourceText).ConfigureAwait(false); - } - - return sourceText; + sourceText = await provider.GetChangedEditorConfigAsync(sourceText).ConfigureAwait(false); } - public ImmutableArray GetCurrentDataSnapshot() - { - var snapShot = ImmutableArray.Empty; - foreach (var provider in _providers) - { - snapShot = snapShot.Concat(provider.GetCurrentDataSnapshot()); - } + return sourceText; + } - return snapShot; + public ImmutableArray GetCurrentDataSnapshot() + { + var snapShot = ImmutableArray.Empty; + foreach (var provider in _providers) + { + snapShot = snapShot.Concat(provider.GetCurrentDataSnapshot()); } - public void RegisterViewModel(ISettingsEditorViewModel model) + return snapShot; + } + + public void RegisterViewModel(ISettingsEditorViewModel model) + { + foreach (var provider in _providers) { - foreach (var provider in _providers) - { - provider.RegisterViewModel(model); - } + provider.RegisterViewModel(model); } } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ILanguageSettingsProviderFactory.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ILanguageSettingsProviderFactory.cs index 800df15d4f056..dd77cc3258cf4 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ILanguageSettingsProviderFactory.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ILanguageSettingsProviderFactory.cs @@ -4,9 +4,8 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider; + +internal interface ILanguageSettingsProviderFactory : ISettingsProviderFactory, ILanguageService { - internal interface ILanguageSettingsProviderFactory : ISettingsProviderFactory, ILanguageService - { - } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ISettingsProvider.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ISettingsProvider.cs index 7eaece9b9f9ed..188a9a38059e1 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ISettingsProvider.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ISettingsProvider.cs @@ -7,12 +7,11 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider; + +internal interface ISettingsProvider { - internal interface ISettingsProvider - { - void RegisterViewModel(ISettingsEditorViewModel model); - ImmutableArray GetCurrentDataSnapshot(); - Task GetChangedEditorConfigAsync(SourceText sourceText); - } + void RegisterViewModel(ISettingsEditorViewModel model); + ImmutableArray GetCurrentDataSnapshot(); + Task GetChangedEditorConfigAsync(SourceText sourceText); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ISettingsProviderFactory.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ISettingsProviderFactory.cs index 4c87bfb021ae3..b4e84241fd9b6 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ISettingsProviderFactory.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/ISettingsProviderFactory.cs @@ -2,10 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider; + +internal interface ISettingsProviderFactory { - internal interface ISettingsProviderFactory - { - ISettingsProvider GetForFile(string filePath); - } + ISettingsProvider GetForFile(string filePath); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/IWorkspaceSettingsProviderFactory.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/IWorkspaceSettingsProviderFactory.cs index a241256ac0d26..0e30e3cf27273 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/IWorkspaceSettingsProviderFactory.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/IWorkspaceSettingsProviderFactory.cs @@ -4,9 +4,8 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider; + +internal interface IWorkspaceSettingsProviderFactory : ISettingsProviderFactory, IWorkspaceService { - internal interface IWorkspaceSettingsProviderFactory : ISettingsProviderFactory, IWorkspaceService - { - } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsProvider.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsProvider.cs index 83e497e9b2c77..db18cbc2712b2 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsProvider.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsProvider.cs @@ -14,26 +14,25 @@ using Microsoft.CodeAnalysis.Options; using RoslynEnumerableExtensions = Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Extensions.EnumerableExtensions; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.NamingStyles +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.NamingStyles; + +internal class NamingStyleSettingsProvider : SettingsProviderBase, NamingStyleSetting), object> { - internal class NamingStyleSettingsProvider : SettingsProviderBase, NamingStyleSetting), object> + public NamingStyleSettingsProvider(string fileName, NamingStyleSettingsUpdater settingsUpdater, Workspace workspace, IGlobalOptionService globalOptions) + : base(fileName, settingsUpdater, workspace, globalOptions) { - public NamingStyleSettingsProvider(string fileName, NamingStyleSettingsUpdater settingsUpdater, Workspace workspace, IGlobalOptionService globalOptions) - : base(fileName, settingsUpdater, workspace, globalOptions) - { - Update(); - } + Update(); + } - protected override void UpdateOptions(TieredAnalyzerConfigOptions options, ImmutableArray projectsInScope) - { - options.GetInitialLocationAndValue(NamingStyleOptions.NamingPreferences, out var location, out var namingPreferences); + protected override void UpdateOptions(TieredAnalyzerConfigOptions options, ImmutableArray projectsInScope) + { + options.GetInitialLocationAndValue(NamingStyleOptions.NamingPreferences, out var location, out var namingPreferences); - var fileName = (location.LocationKind != LocationKind.VisualStudio) ? options.EditorConfigFileName : null; - var namingRules = namingPreferences.NamingRules.Select(r => r.GetRule(namingPreferences)); - var allStyles = RoslynEnumerableExtensions.DistinctBy(namingPreferences.NamingStyles, s => s.Name).ToArray(); - var namingStyles = namingRules.Select(namingRule => new NamingStyleSetting(namingRule, allStyles, SettingsUpdater, fileName)); + var fileName = (location.LocationKind != LocationKind.VisualStudio) ? options.EditorConfigFileName : null; + var namingRules = namingPreferences.NamingRules.Select(r => r.GetRule(namingPreferences)); + var allStyles = RoslynEnumerableExtensions.DistinctBy(namingPreferences.NamingStyles, s => s.Name).ToArray(); + var namingStyles = namingRules.Select(namingRule => new NamingStyleSetting(namingRule, allStyles, SettingsUpdater, fileName)); - AddRange(namingStyles); - } + AddRange(namingStyles); } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsProviderFactory.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsProviderFactory.cs index eae7c42ed1691..aedece002ba5d 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsProviderFactory.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsProviderFactory.cs @@ -6,17 +6,16 @@ using Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.NamingStyles +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.NamingStyles; + +internal sealed class NamingStyleSettingsProviderFactory(Workspace workspace, IGlobalOptionService globalOptions) : IWorkspaceSettingsProviderFactory { - internal sealed class NamingStyleSettingsProviderFactory(Workspace workspace, IGlobalOptionService globalOptions) : IWorkspaceSettingsProviderFactory - { - private readonly Workspace _workspace = workspace; - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly Workspace _workspace = workspace; + private readonly IGlobalOptionService _globalOptions = globalOptions; - public ISettingsProvider GetForFile(string filePath) - { - var updater = new NamingStyleSettingsUpdater(_workspace, _globalOptions, filePath); - return new NamingStyleSettingsProvider(filePath, updater, _workspace, _globalOptions); - } + public ISettingsProvider GetForFile(string filePath) + { + var updater = new NamingStyleSettingsUpdater(_workspace, _globalOptions, filePath); + return new NamingStyleSettingsProvider(filePath, updater, _workspace, _globalOptions); } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsWorkspaceServiceFactory.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsWorkspaceServiceFactory.cs index c8bee4782c48a..9e0549f2d1fa0 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsWorkspaceServiceFactory.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/NamingStyles/NamingStyleSettingsWorkspaceServiceFactory.cs @@ -9,16 +9,15 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.NamingStyles +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider.NamingStyles; + +[ExportWorkspaceServiceFactory(typeof(IWorkspaceSettingsProviderFactory)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class NamingStyleSettingsWorkspaceServiceFactory(IGlobalOptionService globalOptions) : IWorkspaceServiceFactory { - [ExportWorkspaceServiceFactory(typeof(IWorkspaceSettingsProviderFactory)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class NamingStyleSettingsWorkspaceServiceFactory(IGlobalOptionService globalOptions) : IWorkspaceServiceFactory - { - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IGlobalOptionService _globalOptions = globalOptions; - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new NamingStyleSettingsProviderFactory(workspaceServices.Workspace, _globalOptions); - } + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => new NamingStyleSettingsProviderFactory(workspaceServices.Workspace, _globalOptions); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs index 63c0fcd65312b..fb310e0fc78f7 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs @@ -20,162 +20,161 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider; + +internal abstract class SettingsProviderBase : ISettingsProvider + where TOptionsUpdater : ISettingUpdater { - internal abstract class SettingsProviderBase : ISettingsProvider - where TOptionsUpdater : ISettingUpdater - { - private readonly List _snapshot = []; - private static readonly object s_gate = new(); - private ISettingsEditorViewModel? _viewModel; - protected readonly string FileName; - protected readonly TOptionsUpdater SettingsUpdater; - protected readonly Workspace Workspace; - public readonly IGlobalOptionService GlobalOptions; + private readonly List _snapshot = []; + private static readonly object s_gate = new(); + private ISettingsEditorViewModel? _viewModel; + protected readonly string FileName; + protected readonly TOptionsUpdater SettingsUpdater; + protected readonly Workspace Workspace; + public readonly IGlobalOptionService GlobalOptions; - protected abstract void UpdateOptions(TieredAnalyzerConfigOptions options, ImmutableArray projectsInScope); + protected abstract void UpdateOptions(TieredAnalyzerConfigOptions options, ImmutableArray projectsInScope); + + protected SettingsProviderBase(string fileName, TOptionsUpdater settingsUpdater, Workspace workspace, IGlobalOptionService globalOptions) + { + FileName = fileName; + SettingsUpdater = settingsUpdater; + Workspace = workspace; + GlobalOptions = globalOptions; + } - protected SettingsProviderBase(string fileName, TOptionsUpdater settingsUpdater, Workspace workspace, IGlobalOptionService globalOptions) + protected void Update() + { + var givenFolder = new DirectoryInfo(FileName).Parent; + if (givenFolder is null) { - FileName = fileName; - SettingsUpdater = settingsUpdater; - Workspace = workspace; - GlobalOptions = globalOptions; + return; } - protected void Update() + var solution = Workspace.CurrentSolution; + var projects = solution.GetProjectsUnderEditorConfigFile(FileName); + var project = projects.FirstOrDefault(); + if (project is null) { - var givenFolder = new DirectoryInfo(FileName).Parent; - if (givenFolder is null) - { - return; - } + // no .NET projects in the solution + return; + } - var solution = Workspace.CurrentSolution; - var projects = solution.GetProjectsUnderEditorConfigFile(FileName); - var project = projects.FirstOrDefault(); - if (project is null) - { - // no .NET projects in the solution - return; - } + var configFileDirectoryOptions = project.State.GetAnalyzerOptionsForPath(givenFolder.FullName, CancellationToken.None); + var projectDirectoryOptions = project.GetAnalyzerConfigOptions(); - var configFileDirectoryOptions = project.State.GetAnalyzerOptionsForPath(givenFolder.FullName, CancellationToken.None); - var projectDirectoryOptions = project.GetAnalyzerConfigOptions(); + // TODO: Support for multiple languages https://github.com/dotnet/roslyn/issues/65859 + var options = new TieredAnalyzerConfigOptions( + new CombinedAnalyzerConfigOptions(configFileDirectoryOptions, projectDirectoryOptions), + GlobalOptions, + language: LanguageNames.CSharp, + editorConfigFileName: FileName); - // TODO: Support for multiple languages https://github.com/dotnet/roslyn/issues/65859 - var options = new TieredAnalyzerConfigOptions( - new CombinedAnalyzerConfigOptions(configFileDirectoryOptions, projectDirectoryOptions), - GlobalOptions, - language: LanguageNames.CSharp, - editorConfigFileName: FileName); + UpdateOptions(options, projects); + } - UpdateOptions(options, projects); + public async Task GetChangedEditorConfigAsync(SourceText sourceText) + { + if (!await SettingsUpdater.HasAnyChangesAsync().ConfigureAwait(false)) + { + return sourceText; } - public async Task GetChangedEditorConfigAsync(SourceText sourceText) - { - if (!await SettingsUpdater.HasAnyChangesAsync().ConfigureAwait(false)) - { - return sourceText; - } + var text = await SettingsUpdater.GetChangedEditorConfigAsync(sourceText, default).ConfigureAwait(false); + return text is not null ? text : sourceText; + } - var text = await SettingsUpdater.GetChangedEditorConfigAsync(sourceText, default).ConfigureAwait(false); - return text is not null ? text : sourceText; + public ImmutableArray GetCurrentDataSnapshot() + { + lock (s_gate) + { + return _snapshot.ToImmutableArray(); } + } - public ImmutableArray GetCurrentDataSnapshot() + protected void AddRange(IEnumerable items) + { + lock (s_gate) { - lock (s_gate) - { - return _snapshot.ToImmutableArray(); - } + _snapshot.AddRange(items); } - protected void AddRange(IEnumerable items) + _viewModel?.NotifyOfUpdate(); + } + + public void RegisterViewModel(ISettingsEditorViewModel viewModel) + => _viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel)); + + private sealed class CombinedAnalyzerConfigOptions(AnalyzerConfigData fileDirectoryConfigData, AnalyzerConfigData? projectDirectoryConfigData) : StructuredAnalyzerConfigOptions + { + private readonly AnalyzerConfigData _fileDirectoryConfigData = fileDirectoryConfigData; + private readonly AnalyzerConfigData? _projectDirectoryConfigData = projectDirectoryConfigData; + + public override NamingStylePreferences GetNamingStylePreferences() { - lock (s_gate) + var preferences = _fileDirectoryConfigData.ConfigOptions.GetNamingStylePreferences(); + if (preferences.IsEmpty && _projectDirectoryConfigData.HasValue) { - _snapshot.AddRange(items); + preferences = _projectDirectoryConfigData.Value.ConfigOptions.GetNamingStylePreferences(); } - _viewModel?.NotifyOfUpdate(); + return preferences; } - public void RegisterViewModel(ISettingsEditorViewModel viewModel) - => _viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel)); - - private sealed class CombinedAnalyzerConfigOptions(AnalyzerConfigData fileDirectoryConfigData, AnalyzerConfigData? projectDirectoryConfigData) : StructuredAnalyzerConfigOptions + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) { - private readonly AnalyzerConfigData _fileDirectoryConfigData = fileDirectoryConfigData; - private readonly AnalyzerConfigData? _projectDirectoryConfigData = projectDirectoryConfigData; - - public override NamingStylePreferences GetNamingStylePreferences() + if (_fileDirectoryConfigData.ConfigOptions.TryGetValue(key, out value)) { - var preferences = _fileDirectoryConfigData.ConfigOptions.GetNamingStylePreferences(); - if (preferences.IsEmpty && _projectDirectoryConfigData.HasValue) - { - preferences = _projectDirectoryConfigData.Value.ConfigOptions.GetNamingStylePreferences(); - } + return true; + } - return preferences; + if (!_projectDirectoryConfigData.HasValue) + { + value = null; + return false; } - public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + if (_projectDirectoryConfigData.Value.AnalyzerOptions.TryGetValue(key, out value)) { - if (_fileDirectoryConfigData.ConfigOptions.TryGetValue(key, out value)) - { - return true; - } + return true; + } - if (!_projectDirectoryConfigData.HasValue) - { - value = null; - return false; - } + var diagnosticKey = "dotnet_diagnostic.(?.*).severity"; + var match = Regex.Match(key, diagnosticKey); + if (match.Success && match.Groups["key"].Value is string isolatedKey && + _projectDirectoryConfigData.Value.TreeOptions.TryGetValue(isolatedKey, out var severity)) + { + value = severity.ToEditorConfigString(); + return true; + } - if (_projectDirectoryConfigData.Value.AnalyzerOptions.TryGetValue(key, out value)) - { - return true; - } + value = null; + return false; + } - var diagnosticKey = "dotnet_diagnostic.(?.*).severity"; - var match = Regex.Match(key, diagnosticKey); - if (match.Success && match.Groups["key"].Value is string isolatedKey && - _projectDirectoryConfigData.Value.TreeOptions.TryGetValue(isolatedKey, out var severity)) - { - value = severity.ToEditorConfigString(); - return true; - } + public override IEnumerable Keys + { + get + { + foreach (var key in _fileDirectoryConfigData.ConfigOptions.Keys) + yield return key; - value = null; - return false; - } + if (!_projectDirectoryConfigData.HasValue) + yield break; - public override IEnumerable Keys - { - get + foreach (var key in _projectDirectoryConfigData.Value.AnalyzerOptions.Keys) { - foreach (var key in _fileDirectoryConfigData.ConfigOptions.Keys) + if (!_fileDirectoryConfigData.ConfigOptions.TryGetValue(key, out _)) yield return key; + } - if (!_projectDirectoryConfigData.HasValue) - yield break; - - foreach (var key in _projectDirectoryConfigData.Value.AnalyzerOptions.Keys) - { - if (!_fileDirectoryConfigData.ConfigOptions.TryGetValue(key, out _)) - yield return key; - } - - foreach (var (key, severity) in _projectDirectoryConfigData.Value.TreeOptions) + foreach (var (key, severity) in _projectDirectoryConfigData.Value.TreeOptions) + { + var diagnosticKey = "dotnet_diagnostic." + key + ".severity"; + if (!_fileDirectoryConfigData.ConfigOptions.TryGetValue(diagnosticKey, out _) && + !_projectDirectoryConfigData.Value.AnalyzerOptions.TryGetKey(diagnosticKey, out _)) { - var diagnosticKey = "dotnet_diagnostic." + key + ".severity"; - if (!_fileDirectoryConfigData.ConfigOptions.TryGetValue(diagnosticKey, out _) && - !_projectDirectoryConfigData.Value.AnalyzerOptions.TryGetKey(diagnosticKey, out _)) - { - yield return diagnosticKey; - } + yield return diagnosticKey; } } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Extensions/EnumerableExtensions.cs b/src/EditorFeatures/Core/EditorConfigSettings/Extensions/EnumerableExtensions.cs index 6ef0a5228cda0..a78914cbae4e0 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Extensions/EnumerableExtensions.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Extensions/EnumerableExtensions.cs @@ -5,19 +5,18 @@ using System; using System.Collections.Generic; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Extensions +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Extensions; + +internal static class EnumerableExtensions { - internal static class EnumerableExtensions + public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) { - public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) + var seenKeys = new HashSet(); + foreach (var element in source) { - var seenKeys = new HashSet(); - foreach (var element in source) + if (seenKeys.Add(keySelector(element))) { - if (seenKeys.Add(keySelector(element))) - { - yield return element; - } + yield return element; } } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Extensions/SolutionExtensions.cs b/src/EditorFeatures/Core/EditorConfigSettings/Extensions/SolutionExtensions.cs index 91723f3e22081..62d037769967d 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Extensions/SolutionExtensions.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Extensions/SolutionExtensions.cs @@ -7,79 +7,78 @@ using System.IO; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Extensions +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Extensions; + +internal static class SolutionExtensions { - internal static class SolutionExtensions + public static ImmutableArray GetProjectsUnderEditorConfigFile(this Solution solution, string pathToEditorConfigFile) { - public static ImmutableArray GetProjectsUnderEditorConfigFile(this Solution solution, string pathToEditorConfigFile) + var directoryPathToCheck = Path.GetDirectoryName(pathToEditorConfigFile); + if (directoryPathToCheck is null) + { + // we have been given an invalid file path + return []; + } + + var directoryInfoToCheck = new DirectoryInfo(directoryPathToCheck); + var builder = ArrayBuilder.GetInstance(); + foreach (var project in solution.Projects) { - var directoryPathToCheck = Path.GetDirectoryName(pathToEditorConfigFile); - if (directoryPathToCheck is null) + if (!TryGetFolderContainingProject(project, out var projectDirectory)) { - // we have been given an invalid file path - return []; + // Certain ASP.NET scenarios will create artificial projects that do not exist on disk + continue; } - var directoryInfoToCheck = new DirectoryInfo(directoryPathToCheck); - var builder = ArrayBuilder.GetInstance(); - foreach (var project in solution.Projects) + if (ContainsPath(directoryInfoToCheck, projectDirectory)) { - if (!TryGetFolderContainingProject(project, out var projectDirectory)) - { - // Certain ASP.NET scenarios will create artificial projects that do not exist on disk - continue; - } - - if (ContainsPath(directoryInfoToCheck, projectDirectory)) - { - builder.Add(project); - } + builder.Add(project); } + } - return builder.ToImmutableAndFree(); + return builder.ToImmutableAndFree(); - static bool TryGetFolderContainingProject(Project project, [NotNullWhen(true)] out DirectoryInfo? directoryInfo) + static bool TryGetFolderContainingProject(Project project, [NotNullWhen(true)] out DirectoryInfo? directoryInfo) + { + directoryInfo = null; + if (project.FilePath is null) { - directoryInfo = null; - if (project.FilePath is null) - { - return false; - } + return false; + } - var fileDirectoryInfo = new DirectoryInfo(project.FilePath); - if (fileDirectoryInfo.Parent is null) - { - return false; - } + var fileDirectoryInfo = new DirectoryInfo(project.FilePath); + if (fileDirectoryInfo.Parent is null) + { + return false; + } - directoryInfo = fileDirectoryInfo.Parent; + directoryInfo = fileDirectoryInfo.Parent; + return true; + } + + static bool ContainsPath(DirectoryInfo directoryContainingEditorConfig, DirectoryInfo projectDirectory) + { + if (directoryContainingEditorConfig.FullName == projectDirectory.FullName) + { return true; } - static bool ContainsPath(DirectoryInfo directoryContainingEditorConfig, DirectoryInfo projectDirectory) + // walk up each folder for the project and see if it matches + // example match: + // C:\source\roslyn\.editorconfig + // C:\source\roslyn\src\project.csproj + + while (projectDirectory.Parent is not null) { - if (directoryContainingEditorConfig.FullName == projectDirectory.FullName) + if (projectDirectory.Parent.FullName == directoryContainingEditorConfig.FullName) { return true; } - // walk up each folder for the project and see if it matches - // example match: - // C:\source\roslyn\.editorconfig - // C:\source\roslyn\src\project.csproj - - while (projectDirectory.Parent is not null) - { - if (projectDirectory.Parent.FullName == directoryContainingEditorConfig.FullName) - { - return true; - } - - projectDirectory = projectDirectory.Parent; - } - - return false; + projectDirectory = projectDirectory.Parent; } + + return false; } } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/ISettingsEditorViewModel.cs b/src/EditorFeatures/Core/EditorConfigSettings/ISettingsEditorViewModel.cs index 5cabb84670fd5..100c2e87a86f9 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/ISettingsEditorViewModel.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/ISettingsEditorViewModel.cs @@ -6,11 +6,10 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface ISettingsEditorViewModel { - internal interface ISettingsEditorViewModel - { - void NotifyOfUpdate(); - Task UpdateEditorConfigAsync(SourceText sourceText); - } + void NotifyOfUpdate(); + Task UpdateEditorConfigAsync(SourceText sourceText); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/LocationKind.cs b/src/EditorFeatures/Core/EditorConfigSettings/LocationKind.cs index 541d0a7204cae..e92aa422c9b83 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/LocationKind.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/LocationKind.cs @@ -4,13 +4,12 @@ using System; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings; + +[Flags] +internal enum LocationKind { - [Flags] - internal enum LocationKind - { - EditorConfig, - GlobalConfig, - VisualStudio, - } + EditorConfig, + GlobalConfig, + VisualStudio, } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/SettingLocation.cs b/src/EditorFeatures/Core/EditorConfigSettings/SettingLocation.cs index b39313bfe6db1..cd7a1482d3325 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/SettingLocation.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/SettingLocation.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings -{ - internal record SettingLocation(LocationKind LocationKind, string? Path) { } -} +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings; + +internal record SettingLocation(LocationKind LocationKind, string? Path) { } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Updater/AnalyzerSettingsUpdater.cs b/src/EditorFeatures/Core/EditorConfigSettings/Updater/AnalyzerSettingsUpdater.cs index a4a1c7f0b9391..5eb74061db2d4 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Updater/AnalyzerSettingsUpdater.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Updater/AnalyzerSettingsUpdater.cs @@ -8,13 +8,12 @@ using Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Data; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; + +internal class AnalyzerSettingsUpdater(Workspace workspace, string editorconfigPath) : SettingsUpdaterBase(workspace, editorconfigPath) { - internal class AnalyzerSettingsUpdater(Workspace workspace, string editorconfigPath) : SettingsUpdaterBase(workspace, editorconfigPath) - { - protected override SourceText? GetNewText(SourceText sourceText, - IReadOnlyList<(AnalyzerSetting option, ReportDiagnostic value)> settingsToUpdate, - CancellationToken token) - => SettingsUpdateHelper.TryUpdateAnalyzerConfigDocument(sourceText, EditorconfigPath, settingsToUpdate); - } + protected override SourceText? GetNewText(SourceText sourceText, + IReadOnlyList<(AnalyzerSetting option, ReportDiagnostic value)> settingsToUpdate, + CancellationToken token) + => SettingsUpdateHelper.TryUpdateAnalyzerConfigDocument(sourceText, EditorconfigPath, settingsToUpdate); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Updater/ISettingUpdater.cs b/src/EditorFeatures/Core/EditorConfigSettings/Updater/ISettingUpdater.cs index 34365e8a3234c..6f830a9e95847 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Updater/ISettingUpdater.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Updater/ISettingUpdater.cs @@ -6,12 +6,11 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; + +internal interface ISettingUpdater { - internal interface ISettingUpdater - { - void QueueUpdate(TSetting setting, TValue value); - Task GetChangedEditorConfigAsync(SourceText sourceText, CancellationToken token); - Task HasAnyChangesAsync(); - } + void QueueUpdate(TSetting setting, TValue value); + Task GetChangedEditorConfigAsync(SourceText sourceText, CancellationToken token); + Task HasAnyChangesAsync(); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/EditorConfigNamingStylesExtensions.cs b/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/EditorConfigNamingStylesExtensions.cs index a7d932eeca367..5b22a9993a9e3 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/EditorConfigNamingStylesExtensions.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/EditorConfigNamingStylesExtensions.cs @@ -11,74 +11,73 @@ using Microsoft.CodeAnalysis.EditorConfig.Parsing.NamingStyles; using Microsoft.CodeAnalysis.NamingStyles; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; + +internal static class EditorConfigNamingStylesExtensions { - internal static class EditorConfigNamingStylesExtensions + public static bool TryGetParseResultForRule( + this EditorConfigNamingStyles editorConfigNamingStyles, + NamingStyleSetting namingStyleSetting, + [NotNullWhen(true)] out NamingStyleOption? namingStyleOption) { - public static bool TryGetParseResultForRule( - this EditorConfigNamingStyles editorConfigNamingStyles, - NamingStyleSetting namingStyleSetting, - [NotNullWhen(true)] out NamingStyleOption? namingStyleOption) + namingStyleOption = null; + foreach (var (option, optionAsNamingStyle) in editorConfigNamingStyles.Rules.AsNamingStyleSettings()) { - namingStyleOption = null; - foreach (var (option, optionAsNamingStyle) in editorConfigNamingStyles.Rules.AsNamingStyleSettings()) + if (AreSameRule(optionAsNamingStyle, namingStyleSetting)) { - if (AreSameRule(optionAsNamingStyle, namingStyleSetting)) - { - namingStyleOption = option; - return true; - } + namingStyleOption = option; + return true; } + } - return false; + return false; - static bool AreSameRule(NamingStyleSetting left, NamingStyleSetting right) - => left.Severity == right.Severity && - AreSameSymbolSpec(left.Type, right.Type) && - AreSameNamingStyle(left.Style, right.Style); + static bool AreSameRule(NamingStyleSetting left, NamingStyleSetting right) + => left.Severity == right.Severity && + AreSameSymbolSpec(left.Type, right.Type) && + AreSameNamingStyle(left.Style, right.Style); - static bool AreSameSymbolSpec(SymbolSpecification? left, SymbolSpecification? right) + static bool AreSameSymbolSpec(SymbolSpecification? left, SymbolSpecification? right) + { + if (left is null && right is null) { - if (left is null && right is null) - { - return true; - } - - if (left is null || right is null) - { - return false; - } + return true; + } - return left.ApplicableSymbolKindList.SequenceEqual(right!.ApplicableSymbolKindList) && - left.ApplicableAccessibilityList.SequenceEqual(right.ApplicableAccessibilityList) && - left.RequiredModifierList.SequenceEqual(right.RequiredModifierList); + if (left is null || right is null) + { + return false; } - static bool AreSameNamingStyle(NamingStyle left, NamingStyle right) - => left.Prefix == right.Prefix && - left.Suffix == right.Suffix && - left.WordSeparator == right.WordSeparator && - left.CapitalizationScheme == right.CapitalizationScheme; + return left.ApplicableSymbolKindList.SequenceEqual(right!.ApplicableSymbolKindList) && + left.ApplicableAccessibilityList.SequenceEqual(right.ApplicableAccessibilityList) && + left.RequiredModifierList.SequenceEqual(right.RequiredModifierList); } - public static ImmutableArray<(NamingStyleOption namingStyleOption, NamingStyleSetting namingStyleSetting)> AsNamingStyleSettings(this ImmutableArray namingStyleOptions) - => namingStyleOptions.SelectAsArray(rule => (rule, NamingStyleSetting.FromParseResult(rule))); + static bool AreSameNamingStyle(NamingStyle left, NamingStyle right) + => left.Prefix == right.Prefix && + left.Suffix == right.Suffix && + left.WordSeparator == right.WordSeparator && + left.CapitalizationScheme == right.CapitalizationScheme; + } + + public static ImmutableArray<(NamingStyleOption namingStyleOption, NamingStyleSetting namingStyleSetting)> AsNamingStyleSettings(this ImmutableArray namingStyleOptions) + => namingStyleOptions.SelectAsArray(rule => (rule, NamingStyleSetting.FromParseResult(rule))); - public static NamingStyle AsNamingStyle(this NamingScheme namingScheme) - => new( - Guid.NewGuid(), - namingScheme.OptionName, - namingScheme.Prefix, - namingScheme.Suffix, - namingScheme.WordSeparator, - namingScheme.Capitalization); + public static NamingStyle AsNamingStyle(this NamingScheme namingScheme) + => new( + Guid.NewGuid(), + namingScheme.OptionName, + namingScheme.Prefix, + namingScheme.Suffix, + namingScheme.WordSeparator, + namingScheme.Capitalization); - public static SymbolSpecification AsSymbolSpecification(this ApplicableSymbolInfo applicableSymbolInfo) - => new( - Guid.NewGuid(), - applicableSymbolInfo.OptionName, - applicableSymbolInfo.SymbolKinds, - applicableSymbolInfo.Accessibilities, - applicableSymbolInfo.Modifiers); - } + public static SymbolSpecification AsSymbolSpecification(this ApplicableSymbolInfo applicableSymbolInfo) + => new( + Guid.NewGuid(), + applicableSymbolInfo.OptionName, + applicableSymbolInfo.SymbolKinds, + applicableSymbolInfo.Accessibilities, + applicableSymbolInfo.Modifiers); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/NamingStyleSettingsUpdater.cs b/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/NamingStyleSettingsUpdater.cs index 05c9c18cfe5ff..eb5b1a9ce1e02 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/NamingStyleSettingsUpdater.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/NamingStyleSettingsUpdater.cs @@ -16,91 +16,90 @@ using static Microsoft.CodeAnalysis.EditorConfig.Parsing.NamingStyles.EditorConfigNamingStylesParser; using RoslynEnumerableExtensions = Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Extensions.EnumerableExtensions; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; + +internal partial class NamingStyleSettingsUpdater(Workspace workspace, IGlobalOptionService globalOptions, string editorconfigPath) : SettingsUpdaterBase<(Action<(object, object?)> onSettingChange, NamingStyleSetting option), object>(workspace, editorconfigPath) { - internal partial class NamingStyleSettingsUpdater(Workspace workspace, IGlobalOptionService globalOptions, string editorconfigPath) : SettingsUpdaterBase<(Action<(object, object?)> onSettingChange, NamingStyleSetting option), object>(workspace, editorconfigPath) + public readonly IGlobalOptionService GlobalOptions = globalOptions; + + protected override SourceText? GetNewText( + SourceText analyzerConfigDocument, + IReadOnlyList<((Action<(object, object?)> onSettingChange, NamingStyleSetting option) option, object value)> settingsToUpdate, + CancellationToken token) { - public readonly IGlobalOptionService GlobalOptions = globalOptions; + var result = Parse(analyzerConfigDocument, EditorconfigPath); + if (!result.Rules.Any() && settingsToUpdate.Any()) + { + // handle no naming style rules in the editorconfig file. + // The implementation does not allow naming style rules to layer meaning all rules are either + // defined in Visual Studios settings or in an editorconfig file. + analyzerConfigDocument = analyzerConfigDocument.WithNamingStyles(GlobalOptions); + result = Parse(analyzerConfigDocument, EditorconfigPath); + } - protected override SourceText? GetNewText( - SourceText analyzerConfigDocument, - IReadOnlyList<((Action<(object, object?)> onSettingChange, NamingStyleSetting option) option, object value)> settingsToUpdate, - CancellationToken token) + foreach (var ((onSettingChange, option), value) in settingsToUpdate) { - var result = Parse(analyzerConfigDocument, EditorconfigPath); - if (!result.Rules.Any() && settingsToUpdate.Any()) + if (result.TryGetParseResultForRule(option, out var parseResult)) { - // handle no naming style rules in the editorconfig file. - // The implementation does not allow naming style rules to layer meaning all rules are either - // defined in Visual Studios settings or in an editorconfig file. - analyzerConfigDocument = analyzerConfigDocument.WithNamingStyles(GlobalOptions); - result = Parse(analyzerConfigDocument, EditorconfigPath); - } + var endOfSection = new TextSpan(parseResult.Section.Span.End, 0); + if (value is ReportDiagnostic enforcement) + { + var newLine = $"dotnet_naming_rule.{parseResult.RuleName.Value}.severity = {enforcement.ToEditorConfigString()}"; + analyzerConfigDocument = UpdateDocument(analyzerConfigDocument, newLine, parseResult.Severity.Span, endOfSection); + result = Parse(analyzerConfigDocument, EditorconfigPath); + onSettingChange((enforcement, null)); + } - foreach (var ((onSettingChange, option), value) in settingsToUpdate) - { - if (result.TryGetParseResultForRule(option, out var parseResult)) + if (value is NamingStyle prevStyle) { - var endOfSection = new TextSpan(parseResult.Section.Span.End, 0); - if (value is ReportDiagnostic enforcement) + var allCurrentStyles = result.Rules.Select(x => x.NamingScheme).Distinct().Select(x => (x, style: x.AsNamingStyle())); + var styleParseResult = TryGetStyleParseResult(prevStyle, allCurrentStyles); + var allDistinctStyles = RoslynEnumerableExtensions.DistinctBy(allCurrentStyles.Select(x => x.style), x => x.Name).ToArray(); + if (styleParseResult is (NamingScheme namingScheme, NamingStyle style)) { - var newLine = $"dotnet_naming_rule.{parseResult.RuleName.Value}.severity = {enforcement.ToEditorConfigString()}"; - analyzerConfigDocument = UpdateDocument(analyzerConfigDocument, newLine, parseResult.Severity.Span, endOfSection); + var newLine = $"dotnet_naming_rule.{parseResult.RuleName.Value}.style = {namingScheme.OptionName.Value}"; + analyzerConfigDocument = UpdateDocument(analyzerConfigDocument, newLine, parseResult.NamingScheme.OptionName.Span, endOfSection); result = Parse(analyzerConfigDocument, EditorconfigPath); - onSettingChange((enforcement, null)); + onSettingChange((style, allDistinctStyles)); } - if (value is NamingStyle prevStyle) - { - var allCurrentStyles = result.Rules.Select(x => x.NamingScheme).Distinct().Select(x => (x, style: x.AsNamingStyle())); - var styleParseResult = TryGetStyleParseResult(prevStyle, allCurrentStyles); - var allDistinctStyles = RoslynEnumerableExtensions.DistinctBy(allCurrentStyles.Select(x => x.style), x => x.Name).ToArray(); - if (styleParseResult is (NamingScheme namingScheme, NamingStyle style)) - { - var newLine = $"dotnet_naming_rule.{parseResult.RuleName.Value}.style = {namingScheme.OptionName.Value}"; - analyzerConfigDocument = UpdateDocument(analyzerConfigDocument, newLine, parseResult.NamingScheme.OptionName.Span, endOfSection); - result = Parse(analyzerConfigDocument, EditorconfigPath); - onSettingChange((style, allDistinctStyles)); - } - - continue; - } + continue; } } + } - return analyzerConfigDocument; + return analyzerConfigDocument; - static (NamingScheme? scheme, NamingStyle style) TryGetStyleParseResult( - NamingStyle prevStyle, - IEnumerable<(NamingScheme scheme, NamingStyle style)> allCurrentStyles) + static (NamingScheme? scheme, NamingStyle style) TryGetStyleParseResult( + NamingStyle prevStyle, + IEnumerable<(NamingScheme scheme, NamingStyle style)> allCurrentStyles) + { + foreach (var (scheme, currentStyle) in allCurrentStyles) { - foreach (var (scheme, currentStyle) in allCurrentStyles) + if (prevStyle.Prefix == currentStyle.Prefix && + prevStyle.Suffix == currentStyle.Suffix && + prevStyle.WordSeparator == currentStyle.WordSeparator && + prevStyle.CapitalizationScheme == currentStyle.CapitalizationScheme) { - if (prevStyle.Prefix == currentStyle.Prefix && - prevStyle.Suffix == currentStyle.Suffix && - prevStyle.WordSeparator == currentStyle.WordSeparator && - prevStyle.CapitalizationScheme == currentStyle.CapitalizationScheme) - { - return (scheme, currentStyle); - } + return (scheme, currentStyle); } - - return (null, default); } - static SourceText UpdateDocument(SourceText sourceText, string newLine, TextSpan? potentialSpan, TextSpan backupSpan) - { - if (potentialSpan is null) - { - // there is no place to update in the current document instead - // we are appending to the end of a section so we need to add a newline - newLine = "\r\n" + newLine; - } + return (null, default); + } - var span = potentialSpan ?? backupSpan; - var textChange = new TextChange(span, newLine); - return sourceText.WithChanges(textChange); + static SourceText UpdateDocument(SourceText sourceText, string newLine, TextSpan? potentialSpan, TextSpan backupSpan) + { + if (potentialSpan is null) + { + // there is no place to update in the current document instead + // we are appending to the end of a section so we need to add a newline + newLine = "\r\n" + newLine; } + + var span = potentialSpan ?? backupSpan; + var textChange = new TextChange(span, newLine); + return sourceText.WithChanges(textChange); } } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/SourceTextExtensions.cs b/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/SourceTextExtensions.cs index d5c546bcd20fc..62e3cdf590e8f 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/SourceTextExtensions.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Updater/NamingStyles/SourceTextExtensions.cs @@ -16,162 +16,161 @@ using Roslyn.Utilities; using NamingStylesParser = Microsoft.CodeAnalysis.EditorConfig.Parsing.NamingStyles.EditorConfigNamingStylesParser; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; + +internal static class SourceTextExtensions { - internal static class SourceTextExtensions + public static SourceText WithNamingStyles(this SourceText sourceText, IGlobalOptionService globalOptions) { - public static SourceText WithNamingStyles(this SourceText sourceText, IGlobalOptionService globalOptions) - { - var (common, csharp, visualBasic) = GetPreferencesForAllLanguages(globalOptions); + var (common, csharp, visualBasic) = GetPreferencesForAllLanguages(globalOptions); - sourceText = WithNamingStyles(sourceText, csharp, Language.CSharp); - sourceText = WithNamingStyles(sourceText, visualBasic, Language.VisualBasic); - return WithNamingStyles(sourceText, common, Language.CSharp | Language.VisualBasic); - } + sourceText = WithNamingStyles(sourceText, csharp, Language.CSharp); + sourceText = WithNamingStyles(sourceText, visualBasic, Language.VisualBasic); + return WithNamingStyles(sourceText, common, Language.CSharp | Language.VisualBasic); + } - private static SourceText WithNamingStyles(SourceText sourceText, IEnumerable rules, Language language) + private static SourceText WithNamingStyles(SourceText sourceText, IEnumerable rules, Language language) + { + if (rules.Any()) { - if (rules.Any()) + var parseResult = NamingStylesParser.Parse(sourceText, null); // file path unnecessary here + var newNamingStyleSection = new StringBuilder(); + if (parseResult.TryGetSectionForLanguage(language, out var existingSection)) { - var parseResult = NamingStylesParser.Parse(sourceText, null); // file path unnecessary here - var newNamingStyleSection = new StringBuilder(); - if (parseResult.TryGetSectionForLanguage(language, out var existingSection)) - { - var span = new TextSpan(existingSection.Span.End, 0); - EditorConfigFileGenerator.AppendNamingStylePreferencesToEditorConfig(rules, newNamingStyleSection, GetLanguageString(language)); - return WithChanges(sourceText, span, newNamingStyleSection.ToString()); - } - else - { - var span = new TextSpan(sourceText.Length, 0); - newNamingStyleSection.Append("\r\n"); - newNamingStyleSection.Append(Section.GetHeaderTextForLanguage(language)); - EditorConfigFileGenerator.AppendNamingStylePreferencesToEditorConfig(rules, newNamingStyleSection, GetLanguageString(language)); - return WithChanges(sourceText, span, newNamingStyleSection.ToString()); - } + var span = new TextSpan(existingSection.Span.End, 0); + EditorConfigFileGenerator.AppendNamingStylePreferencesToEditorConfig(rules, newNamingStyleSection, GetLanguageString(language)); + return WithChanges(sourceText, span, newNamingStyleSection.ToString()); + } + else + { + var span = new TextSpan(sourceText.Length, 0); + newNamingStyleSection.Append("\r\n"); + newNamingStyleSection.Append(Section.GetHeaderTextForLanguage(language)); + EditorConfigFileGenerator.AppendNamingStylePreferencesToEditorConfig(rules, newNamingStyleSection, GetLanguageString(language)); + return WithChanges(sourceText, span, newNamingStyleSection.ToString()); } + } + + return sourceText; - return sourceText; + static SourceText WithChanges(SourceText sourceText, TextSpan span, string newText) + { + var textChange = new TextChange(span, newText); + return sourceText.WithChanges(textChange); + } - static SourceText WithChanges(SourceText sourceText, TextSpan span, string newText) + static string? GetLanguageString(Language language) + { + if (language.HasFlag(Language.CSharp) && language.HasFlag(Language.VisualBasic)) { - var textChange = new TextChange(span, newText); - return sourceText.WithChanges(textChange); + return null; } - - static string? GetLanguageString(Language language) + else if (language.HasFlag(Language.CSharp)) { - if (language.HasFlag(Language.CSharp) && language.HasFlag(Language.VisualBasic)) - { - return null; - } - else if (language.HasFlag(Language.CSharp)) - { - return LanguageNames.CSharp; - } - else if (language.HasFlag(Language.VisualBasic)) - { - return LanguageNames.VisualBasic; - } - - throw new InvalidOperationException("Invalid language specified"); + return LanguageNames.CSharp; + } + else if (language.HasFlag(Language.VisualBasic)) + { + return LanguageNames.VisualBasic; } + + throw new InvalidOperationException("Invalid language specified"); } + } - private static (IEnumerable Common, IEnumerable CSharp, IEnumerable VisualBasic) GetPreferencesForAllLanguages(IGlobalOptionService globalOptions) - { - var csharpNamingStylePreferences = globalOptions.GetOption(NamingStyleOptions.NamingPreferences, LanguageNames.CSharp); - var vbNamingStylePreferences = globalOptions.GetOption(NamingStyleOptions.NamingPreferences, LanguageNames.VisualBasic); + private static (IEnumerable Common, IEnumerable CSharp, IEnumerable VisualBasic) GetPreferencesForAllLanguages(IGlobalOptionService globalOptions) + { + var csharpNamingStylePreferences = globalOptions.GetOption(NamingStyleOptions.NamingPreferences, LanguageNames.CSharp); + var vbNamingStylePreferences = globalOptions.GetOption(NamingStyleOptions.NamingPreferences, LanguageNames.VisualBasic); - var commonOptions = GetCommonOptions(csharpNamingStylePreferences, vbNamingStylePreferences); - var csharpOnlyOptions = GetOptionsUniqueOptions(csharpNamingStylePreferences, commonOptions); - var vbOnlyOptions = GetOptionsUniqueOptions(vbNamingStylePreferences, commonOptions); - return (commonOptions, csharpOnlyOptions, vbOnlyOptions); + var commonOptions = GetCommonOptions(csharpNamingStylePreferences, vbNamingStylePreferences); + var csharpOnlyOptions = GetOptionsUniqueOptions(csharpNamingStylePreferences, commonOptions); + var vbOnlyOptions = GetOptionsUniqueOptions(vbNamingStylePreferences, commonOptions); + return (commonOptions, csharpOnlyOptions, vbOnlyOptions); - static IEnumerable GetCommonOptions(NamingStylePreferences csharp, NamingStylePreferences visualBasic) - => csharp.Rules.NamingRules.Intersect(visualBasic.Rules.NamingRules, NamingRuleComparerIgnoreGUIDs.Instance); + static IEnumerable GetCommonOptions(NamingStylePreferences csharp, NamingStylePreferences visualBasic) + => csharp.Rules.NamingRules.Intersect(visualBasic.Rules.NamingRules, NamingRuleComparerIgnoreGUIDs.Instance); - static IEnumerable GetOptionsUniqueOptions(NamingStylePreferences csharp, IEnumerable common) - => csharp.Rules.NamingRules.Except(common, NamingRuleComparerIgnoreGUIDs.Instance); + static IEnumerable GetOptionsUniqueOptions(NamingStylePreferences csharp, IEnumerable common) + => csharp.Rules.NamingRules.Except(common, NamingRuleComparerIgnoreGUIDs.Instance); + } + + private class NamingRuleComparerIgnoreGUIDs : IEqualityComparer + { + private static readonly Lazy s_lazyInstance = new(() => new NamingRuleComparerIgnoreGUIDs()); + + public static NamingRuleComparerIgnoreGUIDs Instance => s_lazyInstance.Value; + + public bool Equals(NamingRule left, NamingRule right) + { + return left.EnforcementLevel == right.EnforcementLevel && + NamingStyleComparerIgnoreGUIDs.Instance.Equals(left.NamingStyle, right.NamingStyle) && + SymbolSpecificationComparerIgnoreGUIDs.Instance.Equals(left.SymbolSpecification, right.SymbolSpecification); + } + + public int GetHashCode(NamingRule rule) + { + var enforcementLevelHashCode = (int)rule.EnforcementLevel; + var namingStyleHashCode = NamingStyleComparerIgnoreGUIDs.Instance.GetHashCode(rule.NamingStyle); + var symbolSpecificationHashCode = SymbolSpecificationComparerIgnoreGUIDs.Instance.GetHashCode(rule.SymbolSpecification); + return Hash.Combine(enforcementLevelHashCode, Hash.Combine(namingStyleHashCode, symbolSpecificationHashCode)); } - private class NamingRuleComparerIgnoreGUIDs : IEqualityComparer + private class NamingStyleComparerIgnoreGUIDs : IEqualityComparer { - private static readonly Lazy s_lazyInstance = new(() => new NamingRuleComparerIgnoreGUIDs()); + private static readonly Lazy s_lazyInstance = new(() => new NamingStyleComparerIgnoreGUIDs()); - public static NamingRuleComparerIgnoreGUIDs Instance => s_lazyInstance.Value; + public static NamingStyleComparerIgnoreGUIDs Instance => s_lazyInstance.Value; - public bool Equals(NamingRule left, NamingRule right) + public bool Equals(NamingStyle left, NamingStyle right) { - return left.EnforcementLevel == right.EnforcementLevel && - NamingStyleComparerIgnoreGUIDs.Instance.Equals(left.NamingStyle, right.NamingStyle) && - SymbolSpecificationComparerIgnoreGUIDs.Instance.Equals(left.SymbolSpecification, right.SymbolSpecification); + return StringComparer.OrdinalIgnoreCase.Equals(left.Name, right.Name) && + StringComparer.Ordinal.Equals(left.Prefix, right.Prefix) && + StringComparer.Ordinal.Equals(left.Suffix, right.Suffix) && + StringComparer.Ordinal.Equals(left.WordSeparator, right.WordSeparator) && + left.CapitalizationScheme == right.CapitalizationScheme; } - public int GetHashCode(NamingRule rule) + public int GetHashCode(NamingStyle style) { - var enforcementLevelHashCode = (int)rule.EnforcementLevel; - var namingStyleHashCode = NamingStyleComparerIgnoreGUIDs.Instance.GetHashCode(rule.NamingStyle); - var symbolSpecificationHashCode = SymbolSpecificationComparerIgnoreGUIDs.Instance.GetHashCode(rule.SymbolSpecification); - return Hash.Combine(enforcementLevelHashCode, Hash.Combine(namingStyleHashCode, symbolSpecificationHashCode)); + return Hash.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(style.Name), + Hash.Combine(StringComparer.Ordinal.GetHashCode(style.Prefix), + Hash.Combine(StringComparer.Ordinal.GetHashCode(style.Suffix), + Hash.Combine(StringComparer.Ordinal.GetHashCode(style.WordSeparator), + (int)style.CapitalizationScheme)))); } + } - private class NamingStyleComparerIgnoreGUIDs : IEqualityComparer - { - private static readonly Lazy s_lazyInstance = new(() => new NamingStyleComparerIgnoreGUIDs()); + private class SymbolSpecificationComparerIgnoreGUIDs : IEqualityComparer + { + private static readonly Lazy s_lazyInstance = new(() => new SymbolSpecificationComparerIgnoreGUIDs()); - public static NamingStyleComparerIgnoreGUIDs Instance => s_lazyInstance.Value; + public static SymbolSpecificationComparerIgnoreGUIDs Instance => s_lazyInstance.Value; - public bool Equals(NamingStyle left, NamingStyle right) + public bool Equals(SymbolSpecification? left, SymbolSpecification? right) + { + if (left is null && right is null) { - return StringComparer.OrdinalIgnoreCase.Equals(left.Name, right.Name) && - StringComparer.Ordinal.Equals(left.Prefix, right.Prefix) && - StringComparer.Ordinal.Equals(left.Suffix, right.Suffix) && - StringComparer.Ordinal.Equals(left.WordSeparator, right.WordSeparator) && - left.CapitalizationScheme == right.CapitalizationScheme; + return true; } - public int GetHashCode(NamingStyle style) + if (left is null || right is null) { - return Hash.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(style.Name), - Hash.Combine(StringComparer.Ordinal.GetHashCode(style.Prefix), - Hash.Combine(StringComparer.Ordinal.GetHashCode(style.Suffix), - Hash.Combine(StringComparer.Ordinal.GetHashCode(style.WordSeparator), - (int)style.CapitalizationScheme)))); + return false; } + + return StringComparer.OrdinalIgnoreCase.Equals(left!.Name, right!.Name) && + left.RequiredModifierList.SequenceEqual(right.RequiredModifierList) && + left.ApplicableAccessibilityList.SequenceEqual(right.ApplicableAccessibilityList) && + left.ApplicableSymbolKindList.SequenceEqual(right.ApplicableSymbolKindList); } - private class SymbolSpecificationComparerIgnoreGUIDs : IEqualityComparer + public int GetHashCode(SymbolSpecification symbolSpecification) { - private static readonly Lazy s_lazyInstance = new(() => new SymbolSpecificationComparerIgnoreGUIDs()); - - public static SymbolSpecificationComparerIgnoreGUIDs Instance => s_lazyInstance.Value; - - public bool Equals(SymbolSpecification? left, SymbolSpecification? right) - { - if (left is null && right is null) - { - return true; - } - - if (left is null || right is null) - { - return false; - } - - return StringComparer.OrdinalIgnoreCase.Equals(left!.Name, right!.Name) && - left.RequiredModifierList.SequenceEqual(right.RequiredModifierList) && - left.ApplicableAccessibilityList.SequenceEqual(right.ApplicableAccessibilityList) && - left.ApplicableSymbolKindList.SequenceEqual(right.ApplicableSymbolKindList); - } - - public int GetHashCode(SymbolSpecification symbolSpecification) - { - return Hash.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(symbolSpecification.Name), - Hash.Combine(Hash.CombineValues(symbolSpecification.RequiredModifierList), - Hash.Combine(Hash.CombineValues(symbolSpecification.ApplicableAccessibilityList), - Hash.CombineValues(symbolSpecification.ApplicableSymbolKindList)))); - } + return Hash.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(symbolSpecification.Name), + Hash.Combine(Hash.CombineValues(symbolSpecification.RequiredModifierList), + Hash.Combine(Hash.CombineValues(symbolSpecification.ApplicableAccessibilityList), + Hash.CombineValues(symbolSpecification.ApplicableSymbolKindList)))); } } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Updater/OptionUpdater.cs b/src/EditorFeatures/Core/EditorConfigSettings/Updater/OptionUpdater.cs index b86029bf6c3ed..e28b5f86fcac6 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Updater/OptionUpdater.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Updater/OptionUpdater.cs @@ -8,13 +8,12 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; + +internal class OptionUpdater(Workspace workspace, string editorconfigPath) : SettingsUpdaterBase(workspace, editorconfigPath) { - internal class OptionUpdater(Workspace workspace, string editorconfigPath) : SettingsUpdaterBase(workspace, editorconfigPath) - { - protected override SourceText? GetNewText(SourceText sourceText, - IReadOnlyList<(IOption2 option, object value)> settingsToUpdate, - CancellationToken token) - => SettingsUpdateHelper.TryUpdateAnalyzerConfigDocument(sourceText, EditorconfigPath, settingsToUpdate); - } + protected override SourceText? GetNewText(SourceText sourceText, + IReadOnlyList<(IOption2 option, object value)> settingsToUpdate, + CancellationToken token) + => SettingsUpdateHelper.TryUpdateAnalyzerConfigDocument(sourceText, EditorconfigPath, settingsToUpdate); } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Updater/SettingsUpdateHelper.cs b/src/EditorFeatures/Core/EditorConfigSettings/Updater/SettingsUpdateHelper.cs index 21f5e6f83f857..ce02fda9a9073 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Updater/SettingsUpdateHelper.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Updater/SettingsUpdateHelper.cs @@ -14,340 +14,339 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; + +internal static partial class SettingsUpdateHelper { - internal static partial class SettingsUpdateHelper + private const string DiagnosticOptionPrefix = "dotnet_diagnostic."; + private const string SeveritySuffix = ".severity"; + + public static SourceText? TryUpdateAnalyzerConfigDocument(SourceText originalText, + string filePath, + IReadOnlyList<(AnalyzerSetting option, ReportDiagnostic value)> settingsToUpdate) { - private const string DiagnosticOptionPrefix = "dotnet_diagnostic."; - private const string SeveritySuffix = ".severity"; + if (originalText is null) + return null; + if (settingsToUpdate is null) + return null; + if (filePath is null) + return null; - public static SourceText? TryUpdateAnalyzerConfigDocument(SourceText originalText, - string filePath, - IReadOnlyList<(AnalyzerSetting option, ReportDiagnostic value)> settingsToUpdate) + return TryUpdateAnalyzerConfigDocument(originalText, filePath, settingsToUpdate.Select(x => GetOptionValueAndLanguage(x.option, x.value))); + + static (string option, string value, Language language) GetOptionValueAndLanguage(AnalyzerSetting diagnostic, ReportDiagnostic severity) { - if (originalText is null) - return null; - if (settingsToUpdate is null) - return null; - if (filePath is null) - return null; + var optionName = $"{DiagnosticOptionPrefix}{diagnostic.Id}{SeveritySuffix}"; + var optionValue = severity.ToEditorConfigString(); + var language = diagnostic.Language; + return (optionName, optionValue, language); + } + } - return TryUpdateAnalyzerConfigDocument(originalText, filePath, settingsToUpdate.Select(x => GetOptionValueAndLanguage(x.option, x.value))); + public static SourceText? TryUpdateAnalyzerConfigDocument( + SourceText originalText, + string filePath, + IReadOnlyList<(IOption2 option, object value)> settingsToUpdate) + { + if (originalText is null) + return null; + if (settingsToUpdate is null) + return null; + if (filePath is null) + return null; - static (string option, string value, Language language) GetOptionValueAndLanguage(AnalyzerSetting diagnostic, ReportDiagnostic severity) - { - var optionName = $"{DiagnosticOptionPrefix}{diagnostic.Id}{SeveritySuffix}"; - var optionValue = severity.ToEditorConfigString(); - var language = diagnostic.Language; - return (optionName, optionValue, language); - } - } + return TryUpdateAnalyzerConfigDocument(originalText, filePath, settingsToUpdate.Select(x => GetOptionValueAndLanguage(x.option, x.value))); - public static SourceText? TryUpdateAnalyzerConfigDocument( - SourceText originalText, - string filePath, - IReadOnlyList<(IOption2 option, object value)> settingsToUpdate) + static (string option, string value, Language language) GetOptionValueAndLanguage(IOption2 option, object value) { - if (originalText is null) - return null; - if (settingsToUpdate is null) - return null; - if (filePath is null) - return null; - - return TryUpdateAnalyzerConfigDocument(originalText, filePath, settingsToUpdate.Select(x => GetOptionValueAndLanguage(x.option, x.value))); + var optionName = option.Definition.ConfigName; + var optionValue = option.Definition.Serializer.Serialize(value); - static (string option, string value, Language language) GetOptionValueAndLanguage(IOption2 option, object value) + if (value is ICodeStyleOption codeStyleOption && !optionValue.Contains(':')) { - var optionName = option.Definition.ConfigName; - var optionValue = option.Definition.Serializer.Serialize(value); - - if (value is ICodeStyleOption codeStyleOption && !optionValue.Contains(':')) + var severity = codeStyleOption.Notification.Severity switch { - var severity = codeStyleOption.Notification.Severity switch - { - ReportDiagnostic.Hidden => "silent", - ReportDiagnostic.Info => "suggestion", - ReportDiagnostic.Warn => "warning", - ReportDiagnostic.Error => "error", - _ => string.Empty - }; - optionValue = $"{optionValue}:{severity}"; - } + ReportDiagnostic.Hidden => "silent", + ReportDiagnostic.Info => "suggestion", + ReportDiagnostic.Warn => "warning", + ReportDiagnostic.Error => "error", + _ => string.Empty + }; + optionValue = $"{optionValue}:{severity}"; + } - Language language; - if (option is ISingleValuedOption singleValuedOption) - { - language = singleValuedOption.LanguageName switch - { - LanguageNames.CSharp => Language.CSharp, - LanguageNames.VisualBasic => Language.VisualBasic, - null => Language.CSharp | Language.VisualBasic, - _ => throw ExceptionUtilities.UnexpectedValue(singleValuedOption.LanguageName), - }; - } - else if (option.IsPerLanguage) - { - language = Language.CSharp | Language.VisualBasic; - } - else + Language language; + if (option is ISingleValuedOption singleValuedOption) + { + language = singleValuedOption.LanguageName switch { - throw ExceptionUtilities.UnexpectedValue(option); - } - - return (optionName, optionValue, language); + LanguageNames.CSharp => Language.CSharp, + LanguageNames.VisualBasic => Language.VisualBasic, + null => Language.CSharp | Language.VisualBasic, + _ => throw ExceptionUtilities.UnexpectedValue(singleValuedOption.LanguageName), + }; + } + else if (option.IsPerLanguage) + { + language = Language.CSharp | Language.VisualBasic; + } + else + { + throw ExceptionUtilities.UnexpectedValue(option); } + + return (optionName, optionValue, language); } + } - public static SourceText? TryUpdateAnalyzerConfigDocument(SourceText originalText, - string filePath, - IEnumerable<(string option, string value, Language language)> settingsToUpdate) + public static SourceText? TryUpdateAnalyzerConfigDocument(SourceText originalText, + string filePath, + IEnumerable<(string option, string value, Language language)> settingsToUpdate) + { + var updatedText = originalText; + TextLine? lastValidHeaderSpanEnd; + TextLine? lastValidSpecificHeaderSpanEnd; + foreach (var (option, value, language) in settingsToUpdate) { - var updatedText = originalText; - TextLine? lastValidHeaderSpanEnd; - TextLine? lastValidSpecificHeaderSpanEnd; - foreach (var (option, value, language) in settingsToUpdate) + SourceText? newText; + (newText, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd) = UpdateIfExistsInFile(updatedText, filePath, option, value, language); + if (newText != null) { - SourceText? newText; - (newText, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd) = UpdateIfExistsInFile(updatedText, filePath, option, value, language); - if (newText != null) - { - updatedText = newText; - continue; - } - - (newText, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd) = AddMissingRule(updatedText, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd, option, value, language); - if (newText != null) - { - updatedText = newText; - } + updatedText = newText; + continue; } - return updatedText.Equals(originalText) ? null : updatedText; + (newText, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd) = AddMissingRule(updatedText, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd, option, value, language); + if (newText != null) + { + updatedText = newText; + } } - /// - /// Regular expression for .editorconfig header. - /// For example: "[*.cs] # Optional comment" - /// "[*.{vb,cs}]" - /// "[*] ; Optional comment" - /// "[ConsoleApp/Program.cs]" - /// - private static readonly Regex s_headerPattern = new(@"\[(\*|[^ #;\[\]]+\.({[^ #;{}\.\[\]]+}|[^ #;{}\.\[\]]+))\]\s*([#;].*)?"); - - /// - /// Regular expression for .editorconfig code style option entry. - /// For example: - /// 1. "dotnet_style_object_initializer = true # Optional comment" - /// 2. "dotnet_style_object_initializer = true:suggestion ; Optional comment" - /// 3. "dotnet_diagnostic.CA2000.severity = suggestion # Optional comment" - /// 4. "dotnet_analyzer_diagnostic.category-Security.severity = suggestion # Optional comment" - /// 5. "dotnet_analyzer_diagnostic.severity = suggestion # Optional comment" - /// Regex groups: - /// 1. Option key - /// 2. Option value - /// 3. Optional severity suffix in option value, i.e. ':severity' suffix - /// 4. Optional comment suffix - /// - private static readonly Regex s_optionEntryPattern = new($@"(.*)=([\w, ]*)(:[\w]+)?([ ]*[;#].*)?"); - - private static (SourceText? newText, TextLine? lastValidHeaderSpanEnd, TextLine? lastValidSpecificHeaderSpanEnd) UpdateIfExistsInFile(SourceText editorConfigText, - string filePath, - string optionName, - string optionValue, - Language language) - { - var editorConfigDirectory = PathUtilities.GetDirectoryName(filePath); - Assumes.NotNull(editorConfigDirectory); - var relativePath = PathUtilities.GetRelativePath(editorConfigDirectory.ToLowerInvariant(), filePath); + return updatedText.Equals(originalText) ? null : updatedText; + } + + /// + /// Regular expression for .editorconfig header. + /// For example: "[*.cs] # Optional comment" + /// "[*.{vb,cs}]" + /// "[*] ; Optional comment" + /// "[ConsoleApp/Program.cs]" + /// + private static readonly Regex s_headerPattern = new(@"\[(\*|[^ #;\[\]]+\.({[^ #;{}\.\[\]]+}|[^ #;{}\.\[\]]+))\]\s*([#;].*)?"); + + /// + /// Regular expression for .editorconfig code style option entry. + /// For example: + /// 1. "dotnet_style_object_initializer = true # Optional comment" + /// 2. "dotnet_style_object_initializer = true:suggestion ; Optional comment" + /// 3. "dotnet_diagnostic.CA2000.severity = suggestion # Optional comment" + /// 4. "dotnet_analyzer_diagnostic.category-Security.severity = suggestion # Optional comment" + /// 5. "dotnet_analyzer_diagnostic.severity = suggestion # Optional comment" + /// Regex groups: + /// 1. Option key + /// 2. Option value + /// 3. Optional severity suffix in option value, i.e. ':severity' suffix + /// 4. Optional comment suffix + /// + private static readonly Regex s_optionEntryPattern = new($@"(.*)=([\w, ]*)(:[\w]+)?([ ]*[;#].*)?"); + + private static (SourceText? newText, TextLine? lastValidHeaderSpanEnd, TextLine? lastValidSpecificHeaderSpanEnd) UpdateIfExistsInFile(SourceText editorConfigText, + string filePath, + string optionName, + string optionValue, + Language language) + { + var editorConfigDirectory = PathUtilities.GetDirectoryName(filePath); + Assumes.NotNull(editorConfigDirectory); + var relativePath = PathUtilities.GetRelativePath(editorConfigDirectory.ToLowerInvariant(), filePath); - TextLine? mostRecentHeader = null; - TextLine? lastValidHeader = null; - TextLine? lastValidHeaderSpanEnd = null; + TextLine? mostRecentHeader = null; + TextLine? lastValidHeader = null; + TextLine? lastValidHeaderSpanEnd = null; - TextLine? lastValidSpecificHeader = null; - TextLine? lastValidSpecificHeaderSpanEnd = null; + TextLine? lastValidSpecificHeader = null; + TextLine? lastValidSpecificHeaderSpanEnd = null; - var textChange = new TextChange(); - foreach (var curLine in editorConfigText.Lines) + var textChange = new TextChange(); + foreach (var curLine in editorConfigText.Lines) + { + var curLineText = curLine.ToString(); + if (s_optionEntryPattern.IsMatch(curLineText)) { - var curLineText = curLine.ToString(); - if (s_optionEntryPattern.IsMatch(curLineText)) - { - var groups = s_optionEntryPattern.Match(curLineText).Groups; - var (untrimmedKey, key, value, severity, comment) = GetGroups(groups); + var groups = s_optionEntryPattern.Match(curLineText).Groups; + var (untrimmedKey, key, value, severity, comment) = GetGroups(groups); - // Verify the most recent header is a valid header - if (IsValidHeader(mostRecentHeader, lastValidHeader) && - string.Equals(key, optionName, StringComparison.OrdinalIgnoreCase)) - { - // We found the rule in the file -- replace it with updated option value. - textChange = new TextChange(curLine.Span, $"{untrimmedKey}= {optionValue}{comment}"); - } + // Verify the most recent header is a valid header + if (IsValidHeader(mostRecentHeader, lastValidHeader) && + string.Equals(key, optionName, StringComparison.OrdinalIgnoreCase)) + { + // We found the rule in the file -- replace it with updated option value. + textChange = new TextChange(curLine.Span, $"{untrimmedKey}= {optionValue}{comment}"); } - else if (s_headerPattern.IsMatch(curLineText.Trim())) + } + else if (s_headerPattern.IsMatch(curLineText.Trim())) + { + mostRecentHeader = curLine; + if (ShouldSetAsLastValidHeader(curLineText, out var mostRecentHeaderText)) { - mostRecentHeader = curLine; - if (ShouldSetAsLastValidHeader(curLineText, out var mostRecentHeaderText)) - { - lastValidHeader = mostRecentHeader; - } - else - { - var (fileName, splicedFileExtensions) = ParseHeaderParts(mostRecentHeaderText); - if ((relativePath.IsEmpty() || new Regex(fileName).IsMatch(relativePath)) && - HeaderMatchesLanguageRequirements(language, splicedFileExtensions)) - { - lastValidHeader = mostRecentHeader; - } - } + lastValidHeader = mostRecentHeader; } - - // We want to keep track of how far this (valid) section spans. - if (IsValidHeader(mostRecentHeader, lastValidHeader) && IsNotEmptyOrComment(curLineText)) + else { - lastValidHeaderSpanEnd = curLine; - if (lastValidSpecificHeader != null && mostRecentHeader.Equals(lastValidSpecificHeader)) + var (fileName, splicedFileExtensions) = ParseHeaderParts(mostRecentHeaderText); + if ((relativePath.IsEmpty() || new Regex(fileName).IsMatch(relativePath)) && + HeaderMatchesLanguageRequirements(language, splicedFileExtensions)) { - lastValidSpecificHeaderSpanEnd = curLine; + lastValidHeader = mostRecentHeader; } } } - // We return only the last text change in case of duplicate entries for the same rule. - if (textChange != default) + // We want to keep track of how far this (valid) section spans. + if (IsValidHeader(mostRecentHeader, lastValidHeader) && IsNotEmptyOrComment(curLineText)) { - return (editorConfigText.WithChanges(textChange), lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + lastValidHeaderSpanEnd = curLine; + if (lastValidSpecificHeader != null && mostRecentHeader.Equals(lastValidSpecificHeader)) + { + lastValidSpecificHeaderSpanEnd = curLine; + } } + } - // Rule not found. - return (null, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + // We return only the last text change in case of duplicate entries for the same rule. + if (textChange != default) + { + return (editorConfigText.WithChanges(textChange), lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + } - static (string untrimmedKey, string key, string value, string severitySuffixInValue, string commentValue) GetGroups(GroupCollection groups) - { - var untrimmedKey = groups[1].Value.ToString(); - var key = untrimmedKey.Trim(); - var value = groups[2].Value.ToString(); - var severitySuffixInValue = groups[3].Value.ToString(); - var commentValue = groups[4].Value.ToString(); - return (untrimmedKey, key, value, severitySuffixInValue, commentValue); - } + // Rule not found. + return (null, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); - static bool IsValidHeader(TextLine? mostRecentHeader, TextLine? lastValidHeader) - { - return mostRecentHeader is not null && - lastValidHeader is not null && - mostRecentHeader.Equals(lastValidHeader); - } + static (string untrimmedKey, string key, string value, string severitySuffixInValue, string commentValue) GetGroups(GroupCollection groups) + { + var untrimmedKey = groups[1].Value.ToString(); + var key = untrimmedKey.Trim(); + var value = groups[2].Value.ToString(); + var severitySuffixInValue = groups[3].Value.ToString(); + var commentValue = groups[4].Value.ToString(); + return (untrimmedKey, key, value, severitySuffixInValue, commentValue); + } - static bool ShouldSetAsLastValidHeader(string curLineText, out string mostRecentHeaderText) - { - var groups = s_headerPattern.Match(curLineText.Trim()).Groups; - mostRecentHeaderText = groups[1].Value.ToString().ToLowerInvariant(); - return mostRecentHeaderText.Equals("*", StringComparison.Ordinal); - } + static bool IsValidHeader(TextLine? mostRecentHeader, TextLine? lastValidHeader) + { + return mostRecentHeader is not null && + lastValidHeader is not null && + mostRecentHeader.Equals(lastValidHeader); + } - static (string fileName, string[] splicedFileExtensions) ParseHeaderParts(string mostRecentHeaderText) - { - // We splice on the last occurrence of '.' to account for filenames containing periods. - var nameExtensionSplitIndex = mostRecentHeaderText.LastIndexOf('.'); - var fileName = mostRecentHeaderText[..nameExtensionSplitIndex]; - var splicedFileExtensions = mostRecentHeaderText[(nameExtensionSplitIndex + 1)..].Split(',', ' ', '{', '}'); + static bool ShouldSetAsLastValidHeader(string curLineText, out string mostRecentHeaderText) + { + var groups = s_headerPattern.Match(curLineText.Trim()).Groups; + mostRecentHeaderText = groups[1].Value.ToString().ToLowerInvariant(); + return mostRecentHeaderText.Equals("*", StringComparison.Ordinal); + } - // Replacing characters in the header with the regex equivalent. - fileName = fileName.Replace(".", @"\."); - fileName = fileName.Replace("*", ".*"); - fileName = fileName.Replace("/", @"\/"); + static (string fileName, string[] splicedFileExtensions) ParseHeaderParts(string mostRecentHeaderText) + { + // We splice on the last occurrence of '.' to account for filenames containing periods. + var nameExtensionSplitIndex = mostRecentHeaderText.LastIndexOf('.'); + var fileName = mostRecentHeaderText[..nameExtensionSplitIndex]; + var splicedFileExtensions = mostRecentHeaderText[(nameExtensionSplitIndex + 1)..].Split(',', ' ', '{', '}'); - return (fileName, splicedFileExtensions); - } + // Replacing characters in the header with the regex equivalent. + fileName = fileName.Replace(".", @"\."); + fileName = fileName.Replace("*", ".*"); + fileName = fileName.Replace("/", @"\/"); - static bool IsNotEmptyOrComment(string currentLineText) - { - return !string.IsNullOrWhiteSpace(currentLineText) && !currentLineText.Trim().StartsWith("#", StringComparison.OrdinalIgnoreCase); - } + return (fileName, splicedFileExtensions); + } - static bool HeaderMatchesLanguageRequirements(Language language, string[] splicedFileExtensions) - { - return IsCSharpOnly(language, splicedFileExtensions) || IsVisualBasicOnly(language, splicedFileExtensions) || IsBothVisualBasicAndCSharp(language, splicedFileExtensions); - } + static bool IsNotEmptyOrComment(string currentLineText) + { + return !string.IsNullOrWhiteSpace(currentLineText) && !currentLineText.Trim().StartsWith("#", StringComparison.OrdinalIgnoreCase); + } - static bool IsCSharpOnly(Language language, string[] splicedFileExtensions) - { - return language.HasFlag(Language.CSharp) && !language.HasFlag(Language.VisualBasic) && splicedFileExtensions.Contains("cs") && splicedFileExtensions.Length == 1; - } + static bool HeaderMatchesLanguageRequirements(Language language, string[] splicedFileExtensions) + { + return IsCSharpOnly(language, splicedFileExtensions) || IsVisualBasicOnly(language, splicedFileExtensions) || IsBothVisualBasicAndCSharp(language, splicedFileExtensions); + } - static bool IsVisualBasicOnly(Language language, string[] splicedFileExtensions) - { - return language.HasFlag(Language.VisualBasic) && !language.HasFlag(Language.CSharp) && splicedFileExtensions.Contains("vb") && splicedFileExtensions.Length == 1; - } + static bool IsCSharpOnly(Language language, string[] splicedFileExtensions) + { + return language.HasFlag(Language.CSharp) && !language.HasFlag(Language.VisualBasic) && splicedFileExtensions.Contains("cs") && splicedFileExtensions.Length == 1; + } - static bool IsBothVisualBasicAndCSharp(Language language, string[] splicedFileExtensions) - { - return language.HasFlag(Language.VisualBasic) && language.HasFlag(Language.CSharp) && splicedFileExtensions.Contains("vb") && splicedFileExtensions.Contains("cs"); - } + static bool IsVisualBasicOnly(Language language, string[] splicedFileExtensions) + { + return language.HasFlag(Language.VisualBasic) && !language.HasFlag(Language.CSharp) && splicedFileExtensions.Contains("vb") && splicedFileExtensions.Length == 1; } - private static (SourceText? newText, TextLine? lastValidHeaderSpanEnd, TextLine? lastValidSpecificHeaderSpanEnd) AddMissingRule(SourceText editorConfigText, - TextLine? lastValidHeaderSpanEnd, - TextLine? lastValidSpecificHeaderSpanEnd, - string optionName, - string optionValue, - Language language) + static bool IsBothVisualBasicAndCSharp(Language language, string[] splicedFileExtensions) { - var newEntry = $"{optionName} = {optionValue}"; - if (lastValidSpecificHeaderSpanEnd.HasValue) - { - if (lastValidSpecificHeaderSpanEnd.Value.ToString().Trim().Length != 0) - { - newEntry = "\r\n" + newEntry; // TODO(jmarolf): do we need to read in the users newline settings? - } + return language.HasFlag(Language.VisualBasic) && language.HasFlag(Language.CSharp) && splicedFileExtensions.Contains("vb") && splicedFileExtensions.Contains("cs"); + } + } - return (editorConfigText.WithChanges(new TextChange(new TextSpan(lastValidSpecificHeaderSpanEnd.Value.Span.End, 0), newEntry)), lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); - } - else if (lastValidHeaderSpanEnd.HasValue) + private static (SourceText? newText, TextLine? lastValidHeaderSpanEnd, TextLine? lastValidSpecificHeaderSpanEnd) AddMissingRule(SourceText editorConfigText, + TextLine? lastValidHeaderSpanEnd, + TextLine? lastValidSpecificHeaderSpanEnd, + string optionName, + string optionValue, + Language language) + { + var newEntry = $"{optionName} = {optionValue}"; + if (lastValidSpecificHeaderSpanEnd.HasValue) + { + if (lastValidSpecificHeaderSpanEnd.Value.ToString().Trim().Length != 0) { - if (lastValidHeaderSpanEnd.Value.ToString().Trim().Length != 0) - { - newEntry = "\r\n" + newEntry; // TODO(jmarolf): do we need to read in the users newline settings? - } - - return (editorConfigText.WithChanges(new TextChange(new TextSpan(lastValidHeaderSpanEnd.Value.Span.End, 0), newEntry)), lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + newEntry = "\r\n" + newEntry; // TODO(jmarolf): do we need to read in the users newline settings? } - // We need to generate a new header such as '[*.cs]' or '[*.vb]': - // - For compiler diagnostic entries and code style entries which have per-language option = false, generate only [*.cs] or [*.vb]. - // - For the remainder, generate [*.{cs,vb}] - // Insert a newline if not already present - var lines = editorConfigText.Lines; - var lastLine = lines.Count > 0 ? lines[^1] : default; - var prefix = string.Empty; - if (lastLine.ToString().Trim().Length != 0) + return (editorConfigText.WithChanges(new TextChange(new TextSpan(lastValidSpecificHeaderSpanEnd.Value.Span.End, 0), newEntry)), lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + } + else if (lastValidHeaderSpanEnd.HasValue) + { + if (lastValidHeaderSpanEnd.Value.ToString().Trim().Length != 0) { - prefix = "\r\n"; + newEntry = "\r\n" + newEntry; // TODO(jmarolf): do we need to read in the users newline settings? } - // Insert newline if file is not empty - if (lines.Count > 1 && lastLine.ToString().Trim().Length == 0) - { - prefix += "\r\n"; - } + return (editorConfigText.WithChanges(new TextChange(new TextSpan(lastValidHeaderSpanEnd.Value.Span.End, 0), newEntry)), lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + } - if (language.HasFlag(Language.CSharp) && language.HasFlag(Language.VisualBasic)) - { - prefix += "[*.{cs,vb}]\r\n"; - } - else if (language.HasFlag(Language.CSharp)) - { - prefix += "[*.cs]\r\n"; - } - else if (language.HasFlag(Language.VisualBasic)) - { - prefix += "[*.vb]\r\n"; - } + // We need to generate a new header such as '[*.cs]' or '[*.vb]': + // - For compiler diagnostic entries and code style entries which have per-language option = false, generate only [*.cs] or [*.vb]. + // - For the remainder, generate [*.{cs,vb}] + // Insert a newline if not already present + var lines = editorConfigText.Lines; + var lastLine = lines.Count > 0 ? lines[^1] : default; + var prefix = string.Empty; + if (lastLine.ToString().Trim().Length != 0) + { + prefix = "\r\n"; + } - var result = editorConfigText.WithChanges(new TextChange(new TextSpan(editorConfigText.Length, 0), prefix + newEntry)); - return (result, lastValidHeaderSpanEnd, result.Lines[^2]); + // Insert newline if file is not empty + if (lines.Count > 1 && lastLine.ToString().Trim().Length == 0) + { + prefix += "\r\n"; } + + if (language.HasFlag(Language.CSharp) && language.HasFlag(Language.VisualBasic)) + { + prefix += "[*.{cs,vb}]\r\n"; + } + else if (language.HasFlag(Language.CSharp)) + { + prefix += "[*.cs]\r\n"; + } + else if (language.HasFlag(Language.VisualBasic)) + { + prefix += "[*.vb]\r\n"; + } + + var result = editorConfigText.WithChanges(new TextChange(new TextSpan(editorConfigText.Length, 0), prefix + newEntry)); + return (result, lastValidHeaderSpanEnd, result.Lines[^2]); } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Updater/SettingsUpdaterBase.cs b/src/EditorFeatures/Core/EditorConfigSettings/Updater/SettingsUpdaterBase.cs index ae11ecf0dc9f7..f2b2eabeb7971 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Updater/SettingsUpdaterBase.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Updater/SettingsUpdaterBase.cs @@ -11,104 +11,103 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater +namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Updater; + +internal abstract class SettingsUpdaterBase : ISettingUpdater { - internal abstract class SettingsUpdaterBase : ISettingUpdater - { - private readonly List<(TOption option, TValue value)> _queue = []; - private readonly SemaphoreSlim _guard = new(1); - private readonly IAsynchronousOperationListener _listener; - protected readonly Workspace Workspace; - protected readonly string EditorconfigPath; + private readonly List<(TOption option, TValue value)> _queue = []; + private readonly SemaphoreSlim _guard = new(1); + private readonly IAsynchronousOperationListener _listener; + protected readonly Workspace Workspace; + protected readonly string EditorconfigPath; - protected abstract SourceText? GetNewText(SourceText analyzerConfigDocument, IReadOnlyList<(TOption option, TValue value)> settingsToUpdate, CancellationToken token); + protected abstract SourceText? GetNewText(SourceText analyzerConfigDocument, IReadOnlyList<(TOption option, TValue value)> settingsToUpdate, CancellationToken token); - protected SettingsUpdaterBase(Workspace workspace, string editorconfigPath) - { - Workspace = workspace; - _listener = workspace.Services.GetRequiredService().GetListener(); - EditorconfigPath = editorconfigPath; - } + protected SettingsUpdaterBase(Workspace workspace, string editorconfigPath) + { + Workspace = workspace; + _listener = workspace.Services.GetRequiredService().GetListener(); + EditorconfigPath = editorconfigPath; + } - public void QueueUpdate(TOption setting, TValue value) - { - var token = _listener.BeginAsyncOperation(nameof(QueueUpdate)); - _ = QueueUpdateAsync().CompletesAsyncOperation(token); + public void QueueUpdate(TOption setting, TValue value) + { + var token = _listener.BeginAsyncOperation(nameof(QueueUpdate)); + _ = QueueUpdateAsync().CompletesAsyncOperation(token); - return; + return; - // local function - async Task QueueUpdateAsync() + // local function + async Task QueueUpdateAsync() + { + using (await _guard.DisposableWaitAsync().ConfigureAwait(false)) { - using (await _guard.DisposableWaitAsync().ConfigureAwait(false)) - { - _queue.Add((setting, value)); - } + _queue.Add((setting, value)); } } + } + + public async Task GetChangedEditorConfigAsync(AnalyzerConfigDocument? analyzerConfigDocument, CancellationToken token) + { + if (analyzerConfigDocument is null) + return null; - public async Task GetChangedEditorConfigAsync(AnalyzerConfigDocument? analyzerConfigDocument, CancellationToken token) + var originalText = await analyzerConfigDocument.GetValueTextAsync(token).ConfigureAwait(false); + using (await _guard.DisposableWaitAsync(token).ConfigureAwait(false)) { - if (analyzerConfigDocument is null) + var newText = GetNewText(originalText, _queue, token); + if (newText is null || newText.Equals(originalText)) + { + _queue.Clear(); return null; - - var originalText = await analyzerConfigDocument.GetValueTextAsync(token).ConfigureAwait(false); - using (await _guard.DisposableWaitAsync(token).ConfigureAwait(false)) + } + else { - var newText = GetNewText(originalText, _queue, token); - if (newText is null || newText.Equals(originalText)) - { - _queue.Clear(); - return null; - } - else - { - _queue.Clear(); - return newText; - } + _queue.Clear(); + return newText; } } + } - public async Task?> GetChangedEditorConfigAsync(CancellationToken token) + public async Task?> GetChangedEditorConfigAsync(CancellationToken token) + { + var solution = Workspace.CurrentSolution; + var analyzerConfigDocument = solution.Projects + .SelectMany(p => p.AnalyzerConfigDocuments) + .FirstOrDefault(d => d.FilePath == EditorconfigPath); + var newText = await GetChangedEditorConfigAsync(analyzerConfigDocument, token).ConfigureAwait(false); + if (newText is null) { - var solution = Workspace.CurrentSolution; - var analyzerConfigDocument = solution.Projects - .SelectMany(p => p.AnalyzerConfigDocuments) - .FirstOrDefault(d => d.FilePath == EditorconfigPath); - var newText = await GetChangedEditorConfigAsync(analyzerConfigDocument, token).ConfigureAwait(false); - if (newText is null) - { - return null; - } - - var originalText = await analyzerConfigDocument!.GetValueTextAsync(token).ConfigureAwait(false); - return newText.GetTextChanges(originalText); + return null; } - public async Task GetChangedEditorConfigAsync(SourceText originalText, CancellationToken token) + var originalText = await analyzerConfigDocument!.GetValueTextAsync(token).ConfigureAwait(false); + return newText.GetTextChanges(originalText); + } + + public async Task GetChangedEditorConfigAsync(SourceText originalText, CancellationToken token) + { + using (await _guard.DisposableWaitAsync(token).ConfigureAwait(false)) { - using (await _guard.DisposableWaitAsync(token).ConfigureAwait(false)) + var newText = GetNewText(originalText, _queue, token); + if (newText is null || newText.Equals(originalText)) { - var newText = GetNewText(originalText, _queue, token); - if (newText is null || newText.Equals(originalText)) - { - _queue.Clear(); - return null; - } - else - { - _queue.Clear(); - return newText; - } + _queue.Clear(); + return null; + } + else + { + _queue.Clear(); + return newText; } } + } - public async Task HasAnyChangesAsync() + public async Task HasAnyChangesAsync() + { + using (await _guard.DisposableWaitAsync().ConfigureAwait(false)) { - using (await _guard.DisposableWaitAsync().ConfigureAwait(false)) - { - return _queue.Any(); - } + return _queue.Any(); } } } diff --git a/src/EditorFeatures/Core/EncapsulateField/AbstractEncapsulateFieldCommandHandler.cs b/src/EditorFeatures/Core/EncapsulateField/AbstractEncapsulateFieldCommandHandler.cs index 15e3ace124d74..05764c88f5d27 100644 --- a/src/EditorFeatures/Core/EncapsulateField/AbstractEncapsulateFieldCommandHandler.cs +++ b/src/EditorFeatures/Core/EncapsulateField/AbstractEncapsulateFieldCommandHandler.cs @@ -21,124 +21,123 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EncapsulateField +namespace Microsoft.CodeAnalysis.EncapsulateField; + +internal abstract class AbstractEncapsulateFieldCommandHandler( + IThreadingContext threadingContext, + ITextBufferUndoManagerProvider undoManager, + IGlobalOptionService globalOptions, + IAsynchronousOperationListenerProvider listenerProvider) : ICommandHandler { - internal abstract class AbstractEncapsulateFieldCommandHandler( - IThreadingContext threadingContext, - ITextBufferUndoManagerProvider undoManager, - IGlobalOptionService globalOptions, - IAsynchronousOperationListenerProvider listenerProvider) : ICommandHandler + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly ITextBufferUndoManagerProvider _undoManager = undoManager; + private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.EncapsulateField); + + public string DisplayName => EditorFeaturesResources.Encapsulate_Field; + + public CommandState GetCommandState(EncapsulateFieldCommandArgs args) + => args.SubjectBuffer.SupportsRefactorings() ? CommandState.Available : CommandState.Unspecified; + + public bool ExecuteCommand(EncapsulateFieldCommandArgs args, CommandExecutionContext context) { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly ITextBufferUndoManagerProvider _undoManager = undoManager; - private readonly IGlobalOptionService _globalOptions = globalOptions; - private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.EncapsulateField); + var textBuffer = args.SubjectBuffer; + if (!textBuffer.SupportsRefactorings()) + return false; + + var spans = args.TextView.Selection.GetSnapshotSpansOnBuffer(textBuffer); + if (spans.Count != 1) + return false; + + var document = args.SubjectBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); + if (document == null) + return false; + + // Fire and forget + var token = _listener.BeginAsyncOperation(FeatureAttribute.EncapsulateField); + _ = ExecuteAsync(args, document, spans.Single()).CompletesAsyncOperation(token); + return true; + } - public string DisplayName => EditorFeaturesResources.Encapsulate_Field; + private async Task ExecuteAsync( + EncapsulateFieldCommandArgs args, + Document initialDocument, + SnapshotSpan span) + { + _threadingContext.ThrowIfNotOnUIThread(); - public CommandState GetCommandState(EncapsulateFieldCommandArgs args) - => args.SubjectBuffer.SupportsRefactorings() ? CommandState.Available : CommandState.Unspecified; + var subjectBuffer = args.SubjectBuffer; + var workspace = initialDocument.Project.Solution.Workspace; - public bool ExecuteCommand(EncapsulateFieldCommandArgs args, CommandExecutionContext context) - { - var textBuffer = args.SubjectBuffer; - if (!textBuffer.SupportsRefactorings()) - return false; - - var spans = args.TextView.Selection.GetSnapshotSpansOnBuffer(textBuffer); - if (spans.Count != 1) - return false; - - var document = args.SubjectBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); - if (document == null) - return false; - - // Fire and forget - var token = _listener.BeginAsyncOperation(FeatureAttribute.EncapsulateField); - _ = ExecuteAsync(args, document, spans.Single()).CompletesAsyncOperation(token); - return true; - } + var indicatorFactory = workspace.Services.GetRequiredService(); + using var context = indicatorFactory.Create( + args.TextView, span, EditorFeaturesResources.Computing_Encapsulate_Field_information, + cancelOnEdit: true, cancelOnFocusLost: true); - private async Task ExecuteAsync( - EncapsulateFieldCommandArgs args, - Document initialDocument, - SnapshotSpan span) - { - _threadingContext.ThrowIfNotOnUIThread(); + var cancellationToken = context.UserCancellationToken; + var document = await subjectBuffer.CurrentSnapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context).ConfigureAwait(false); + Contract.ThrowIfNull(document); - var subjectBuffer = args.SubjectBuffer; - var workspace = initialDocument.Project.Solution.Workspace; + var service = document.GetRequiredLanguageService(); - var indicatorFactory = workspace.Services.GetRequiredService(); - using var context = indicatorFactory.Create( - args.TextView, span, EditorFeaturesResources.Computing_Encapsulate_Field_information, - cancelOnEdit: true, cancelOnFocusLost: true); + var result = await service.EncapsulateFieldsInSpanAsync( + document, span.Span.ToTextSpan(), _globalOptions.CreateProvider(), useDefaultBehavior: true, cancellationToken).ConfigureAwait(false); - var cancellationToken = context.UserCancellationToken; - var document = await subjectBuffer.CurrentSnapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context).ConfigureAwait(false); - Contract.ThrowIfNull(document); + if (result == null) + { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var service = document.GetRequiredLanguageService(); + // We are about to show a modal UI dialog so we should take over the command execution + // wait context. That means the command system won't attempt to show its own wait dialog + // and also will take it into consideration when measuring command handling duration. + context.TakeOwnership(); - var result = await service.EncapsulateFieldsInSpanAsync( - document, span.Span.ToTextSpan(), _globalOptions.CreateProvider(), useDefaultBehavior: true, cancellationToken).ConfigureAwait(false); + var notificationService = workspace.Services.GetRequiredService(); + notificationService.SendNotification(EditorFeaturesResources.Please_select_the_definition_of_the_field_to_encapsulate, severity: NotificationSeverity.Error); + return; + } - if (result == null) - { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + await ApplyChangeAsync(subjectBuffer, document, result, cancellationToken).ConfigureAwait(false); + } - // We are about to show a modal UI dialog so we should take over the command execution - // wait context. That means the command system won't attempt to show its own wait dialog - // and also will take it into consideration when measuring command handling duration. - context.TakeOwnership(); + private async Task ApplyChangeAsync( + ITextBuffer subjectBuffer, + Document document, + EncapsulateFieldResult result, CancellationToken cancellationToken) + { + var finalSolution = await result.GetSolutionAsync(cancellationToken).ConfigureAwait(false); - var notificationService = workspace.Services.GetRequiredService(); - notificationService.SendNotification(EditorFeaturesResources.Please_select_the_definition_of_the_field_to_encapsulate, severity: NotificationSeverity.Error); - return; - } + var solution = document.Project.Solution; + var workspace = solution.Workspace; + var previewService = workspace.Services.GetService(); + if (previewService != null) + { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + finalSolution = previewService.PreviewChanges( + string.Format(EditorFeaturesResources.Preview_Changes_0, EditorFeaturesResources.Encapsulate_Field), + "vs.csharp.refactoring.preview", + EditorFeaturesResources.Encapsulate_Field_colon, + result.Name, + result.Glyph, + finalSolution, + solution); + } - await ApplyChangeAsync(subjectBuffer, document, result, cancellationToken).ConfigureAwait(false); + if (finalSolution == null) + { + // User clicked cancel. + return; } - private async Task ApplyChangeAsync( - ITextBuffer subjectBuffer, - Document document, - EncapsulateFieldResult result, CancellationToken cancellationToken) + using var undoTransaction = _undoManager.GetTextBufferUndoManager(subjectBuffer).TextBufferUndoHistory.CreateTransaction(EditorFeaturesResources.Encapsulate_Field); + + if (workspace.TryApplyChanges(finalSolution)) + { + undoTransaction.Complete(); + } + else { - var finalSolution = await result.GetSolutionAsync(cancellationToken).ConfigureAwait(false); - - var solution = document.Project.Solution; - var workspace = solution.Workspace; - var previewService = workspace.Services.GetService(); - if (previewService != null) - { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - finalSolution = previewService.PreviewChanges( - string.Format(EditorFeaturesResources.Preview_Changes_0, EditorFeaturesResources.Encapsulate_Field), - "vs.csharp.refactoring.preview", - EditorFeaturesResources.Encapsulate_Field_colon, - result.Name, - result.Glyph, - finalSolution, - solution); - } - - if (finalSolution == null) - { - // User clicked cancel. - return; - } - - using var undoTransaction = _undoManager.GetTextBufferUndoManager(subjectBuffer).TextBufferUndoHistory.CreateTransaction(EditorFeaturesResources.Encapsulate_Field); - - if (workspace.TryApplyChanges(finalSolution)) - { - undoTransaction.Complete(); - } - else - { - undoTransaction.Cancel(); - } + undoTransaction.Cancel(); } } } diff --git a/src/EditorFeatures/Core/EndConstructGeneration/IEndConstructGenerationService.cs b/src/EditorFeatures/Core/EndConstructGeneration/IEndConstructGenerationService.cs index 0ccdf323e9a6d..99e2571f7ace6 100644 --- a/src/EditorFeatures/Core/EndConstructGeneration/IEndConstructGenerationService.cs +++ b/src/EditorFeatures/Core/EndConstructGeneration/IEndConstructGenerationService.cs @@ -9,10 +9,9 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor.Implementation.EndConstructGeneration +namespace Microsoft.CodeAnalysis.Editor.Implementation.EndConstructGeneration; + +internal interface IEndConstructGenerationService : ILanguageService { - internal interface IEndConstructGenerationService : ILanguageService - { - bool TryDo(ITextView textView, ITextBuffer subjectBuffer, char typedChar, CancellationToken cancellationToken); - } + bool TryDo(ITextView textView, ITextBuffer subjectBuffer, char typedChar, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/ErrorReporting/EditorErrorReportingService.cs b/src/EditorFeatures/Core/ErrorReporting/EditorErrorReportingService.cs index 1c3ce53a6e287..1731e5d90a5e3 100644 --- a/src/EditorFeatures/Core/ErrorReporting/EditorErrorReportingService.cs +++ b/src/EditorFeatures/Core/ErrorReporting/EditorErrorReportingService.cs @@ -8,28 +8,27 @@ using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Telemetry; -namespace Microsoft.CodeAnalysis.ErrorReporting +namespace Microsoft.CodeAnalysis.ErrorReporting; + +[ExportWorkspaceService(typeof(IErrorReportingService), ServiceLayer.Editor), Shared] +internal sealed class EditorErrorReportingService : IErrorReportingService { - [ExportWorkspaceService(typeof(IErrorReportingService), ServiceLayer.Editor), Shared] - internal sealed class EditorErrorReportingService : IErrorReportingService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public EditorErrorReportingService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EditorErrorReportingService() - { - } + } - public string HostDisplayName => "host"; + public string HostDisplayName => "host"; - public void ShowDetailedErrorInfo(Exception exception) - => Logger.Log(FunctionId.Extension_Exception, exception.StackTrace); + public void ShowDetailedErrorInfo(Exception exception) + => Logger.Log(FunctionId.Extension_Exception, exception.StackTrace); - public void ShowGlobalErrorInfo(string message, TelemetryFeatureName featureName, Exception? exception, params InfoBarUI[] items) - => Logger.Log(FunctionId.Extension_Exception, message); + public void ShowGlobalErrorInfo(string message, TelemetryFeatureName featureName, Exception? exception, params InfoBarUI[] items) + => Logger.Log(FunctionId.Extension_Exception, message); - public void ShowFeatureNotAvailableErrorInfo(string message, TelemetryFeatureName featureName, Exception? exception) - { - // telemetry has already been reported - } + public void ShowFeatureNotAvailableErrorInfo(string message, TelemetryFeatureName featureName, Exception? exception) + { + // telemetry has already been reported } } diff --git a/src/EditorFeatures/Core/EventHookup/EventHookupOptionsStorage.cs b/src/EditorFeatures/Core/EventHookup/EventHookupOptionsStorage.cs index 9a3d45b144495..d0dcfe06bd174 100644 --- a/src/EditorFeatures/Core/EventHookup/EventHookupOptionsStorage.cs +++ b/src/EditorFeatures/Core/EventHookup/EventHookupOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.EventHookup +namespace Microsoft.CodeAnalysis.EventHookup; + +internal static class EventHookupOptionsStorage { - internal static class EventHookupOptionsStorage - { - public static readonly Option2 EventHookup = new("dotnet_enable_event_hook_up", defaultValue: true); - } + public static readonly Option2 EventHookup = new("dotnet_enable_event_hook_up", defaultValue: true); } diff --git a/src/EditorFeatures/Core/ExportContentTypeLanguageServiceAttribute.cs b/src/EditorFeatures/Core/ExportContentTypeLanguageServiceAttribute.cs index f8248333a52fc..4a85f045d9281 100644 --- a/src/EditorFeatures/Core/ExportContentTypeLanguageServiceAttribute.cs +++ b/src/EditorFeatures/Core/ExportContentTypeLanguageServiceAttribute.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Composition; using Microsoft.CodeAnalysis.Host.Mef; @@ -15,8 +13,9 @@ namespace Microsoft.CodeAnalysis.Editor /// [MetadataAttribute] [AttributeUsage(AttributeTargets.Class)] - internal class ExportContentTypeLanguageServiceAttribute(string defaultContentType, string language, string layer = ServiceLayer.Default) : ExportLanguageServiceAttribute(typeof(IContentTypeLanguageService), language, layer) + internal class ExportContentTypeLanguageServiceAttribute(string defaultContentType, string language, string layer = ServiceLayer.Default) + : ExportLanguageServiceAttribute(typeof(IContentTypeLanguageService), language, layer) { - public string DefaultContentType { get; set; } = defaultContentType ?? throw new ArgumentNullException(nameof(defaultContentType)); + public string DefaultContentType { get; set; } = defaultContentType; } } diff --git a/src/EditorFeatures/Core/Extensibility/Commands/ExportInteractiveCommandAttribute.cs b/src/EditorFeatures/Core/Extensibility/Commands/ExportInteractiveCommandAttribute.cs index e7e594962c0ae..1d49a1309b221 100644 --- a/src/EditorFeatures/Core/Extensibility/Commands/ExportInteractiveCommandAttribute.cs +++ b/src/EditorFeatures/Core/Extensibility/Commands/ExportInteractiveCommandAttribute.cs @@ -8,12 +8,11 @@ using System.Collections.Generic; using System.ComponentModel.Composition; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +[MetadataAttribute] +[AttributeUsage(AttributeTargets.Class)] +internal class ExportInteractiveAttribute(Type t, params string[] contentTypes) : ExportAttribute(t) { - [MetadataAttribute] - [AttributeUsage(AttributeTargets.Class)] - internal class ExportInteractiveAttribute(Type t, params string[] contentTypes) : ExportAttribute(t) - { - public IEnumerable ContentTypes { get; } = contentTypes; - } + public IEnumerable ContentTypes { get; } = contentTypes; } diff --git a/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs b/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs index de1e119d5563a..978fac362050f 100644 --- a/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs +++ b/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs @@ -6,192 +6,191 @@ using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +/// +/// Command handler names +/// +internal static class PredefinedCommandHandlerNames { /// - /// Command handler names - /// - internal static class PredefinedCommandHandlerNames - { - /// - /// Command handler name for Automatic Pair Completion - /// - /// - public const string AutomaticCompletion = "Automatic Pair Completion Command Handler"; - - /// - /// Command handler name for Automatic Line Ender - /// - /// - public const string AutomaticLineEnder = "Automatic Line Ender Command Handler"; - - /// - /// Command handler name for Change Signature. - /// - public const string ChangeSignature = "Change Signature"; - - /// - /// Command handler name for Class View. - /// - public const string ClassView = "Class View"; - - /// - /// Command handler name for Comment Selection. - /// - /// - public const string CommentSelection = "Comment Selection Command Handler"; - - /// - /// Command handler name for Commit. - /// - /// - public const string Commit = "Commit Command Handler"; - - /// - /// Command handler name for Documentation Comments. - /// - public const string DocumentationComments = "Documentation Comments Command Handler"; - - /// - /// Command handler name for Encapsulate Field. - /// - public const string EncapsulateField = nameof(EncapsulateField); - - /// - /// Command handler name for End Construct. - /// - public const string EndConstruct = "End Construct Command Handler"; - - /// - /// Command handler name for Event Hookup. - /// - public const string EventHookup = "Event Hookup Command Handler"; - - /// - /// Command handler name for Extract Interface - /// - public const string ExtractInterface = "Extract Interface Command Handler"; - - /// - /// Command handler name for Extract Method - /// - public const string ExtractMethod = "Extract Method Command Handler"; - - /// - /// Command handler name for Find References. - /// - public const string FindReferences = "Find References Command Handler"; - - /// - /// Command handler name for Format Document. - /// - public const string FormatDocument = "Format Document Command Handler"; - - /// - /// Command handler name for Go to Base. - /// - public const string GoToBase = "Go To Base Command Handler"; - - /// - /// Command handler name for Go to Definition. - /// - public const string GoToDefinition = "Go To Definition Command Handler"; - - /// - /// Command handler name for Go to Implementation. - /// - public const string GoToImplementation = "Go To Implementation Command Handler"; - - /// - /// Command handler name for Go to Adjacent Member. - /// - public const string GoToAdjacentMember = "Go To Adjacent Member Command Handler"; - - /// - /// Command handler name for Indent. - /// - public const string Indent = "Indent Command Handler"; - - /// - /// Command handler name for Navigate to Highlighted Reference. - /// - public const string NavigateToHighlightedReference = "Navigate to Highlighted Reference Command Handler"; - - /// - /// Command handler name for Organize Document. - /// - public const string OrganizeDocument = "Organize Document Command Handler"; - - /// - /// Command handler name for Quick Info. - /// - public const string QuickInfo = "Quick Info Command Handler"; - - /// - /// Command handler name for Rename. - /// - public const string Rename = "Rename Command Handler"; - - /// - /// Command handler name for Rename Tracking cancellation. - /// - public const string RenameTrackingCancellation = "Rename Tracking Cancellation Command Handler"; - - /// - /// Command handler name for a Signature Help command handler executing before . - /// - public const string SignatureHelpBeforeCompletion = "Signature Help Before Completion Command Handler"; - - /// - /// Command handler name for a Signature Help command handler executing after . - /// - public const string SignatureHelpAfterCompletion = "Signature Help After Completion Command Handler"; - - /// - /// Command handler name for String Copy Paste. - /// - public const string StringCopyPaste = "String Copy Paste"; - - /// - /// Command handler name for Toggle Block Comments. - /// - /// - public const string ToggleBlockComment = "Toggle Block Comment Command Handler"; - - /// - /// Command handler name for Toggle Line Comments. - /// - /// - public const string ToggleLineComment = "Toggle Line Comment Command Handler"; - - /// - /// Command handler name for Paste Content in Interactive Format. - /// - public const string InteractivePaste = "Interactive Paste Command Handler"; - - /// - /// Command handler name for Paste in Paste Tracking. - /// - public const string PasteTrackingPaste = "Paste Tracking Paste Command Handler"; - - /// - /// Command handler name for Paste in Add Imports. - /// - public const string AddImportsPaste = "Add Imports Paste Command Handler"; - - /// - /// Command handler name for Edit and Continue file save handler. - /// - public const string EditAndContinueFileSave = "Edit and Continue Save File Handler"; - - /// - /// Command handler name for showing the Value Tracking tool window. - /// - public const string ShowValueTracking = "Show Value Tracking"; - - /// - /// Command handler name for showing the Callstack Explorer tool window. - /// - public const string ShowCallstackExplorer = "Show Callstack Explorer"; - } + /// Command handler name for Automatic Pair Completion + /// + /// + public const string AutomaticCompletion = "Automatic Pair Completion Command Handler"; + + /// + /// Command handler name for Automatic Line Ender + /// + /// + public const string AutomaticLineEnder = "Automatic Line Ender Command Handler"; + + /// + /// Command handler name for Change Signature. + /// + public const string ChangeSignature = "Change Signature"; + + /// + /// Command handler name for Class View. + /// + public const string ClassView = "Class View"; + + /// + /// Command handler name for Comment Selection. + /// + /// + public const string CommentSelection = "Comment Selection Command Handler"; + + /// + /// Command handler name for Commit. + /// + /// + public const string Commit = "Commit Command Handler"; + + /// + /// Command handler name for Documentation Comments. + /// + public const string DocumentationComments = "Documentation Comments Command Handler"; + + /// + /// Command handler name for Encapsulate Field. + /// + public const string EncapsulateField = nameof(EncapsulateField); + + /// + /// Command handler name for End Construct. + /// + public const string EndConstruct = "End Construct Command Handler"; + + /// + /// Command handler name for Event Hookup. + /// + public const string EventHookup = "Event Hookup Command Handler"; + + /// + /// Command handler name for Extract Interface + /// + public const string ExtractInterface = "Extract Interface Command Handler"; + + /// + /// Command handler name for Extract Method + /// + public const string ExtractMethod = "Extract Method Command Handler"; + + /// + /// Command handler name for Find References. + /// + public const string FindReferences = "Find References Command Handler"; + + /// + /// Command handler name for Format Document. + /// + public const string FormatDocument = "Format Document Command Handler"; + + /// + /// Command handler name for Go to Base. + /// + public const string GoToBase = "Go To Base Command Handler"; + + /// + /// Command handler name for Go to Definition. + /// + public const string GoToDefinition = "Go To Definition Command Handler"; + + /// + /// Command handler name for Go to Implementation. + /// + public const string GoToImplementation = "Go To Implementation Command Handler"; + + /// + /// Command handler name for Go to Adjacent Member. + /// + public const string GoToAdjacentMember = "Go To Adjacent Member Command Handler"; + + /// + /// Command handler name for Indent. + /// + public const string Indent = "Indent Command Handler"; + + /// + /// Command handler name for Navigate to Highlighted Reference. + /// + public const string NavigateToHighlightedReference = "Navigate to Highlighted Reference Command Handler"; + + /// + /// Command handler name for Organize Document. + /// + public const string OrganizeDocument = "Organize Document Command Handler"; + + /// + /// Command handler name for Quick Info. + /// + public const string QuickInfo = "Quick Info Command Handler"; + + /// + /// Command handler name for Rename. + /// + public const string Rename = "Rename Command Handler"; + + /// + /// Command handler name for Rename Tracking cancellation. + /// + public const string RenameTrackingCancellation = "Rename Tracking Cancellation Command Handler"; + + /// + /// Command handler name for a Signature Help command handler executing before . + /// + public const string SignatureHelpBeforeCompletion = "Signature Help Before Completion Command Handler"; + + /// + /// Command handler name for a Signature Help command handler executing after . + /// + public const string SignatureHelpAfterCompletion = "Signature Help After Completion Command Handler"; + + /// + /// Command handler name for String Copy Paste. + /// + public const string StringCopyPaste = "String Copy Paste"; + + /// + /// Command handler name for Toggle Block Comments. + /// + /// + public const string ToggleBlockComment = "Toggle Block Comment Command Handler"; + + /// + /// Command handler name for Toggle Line Comments. + /// + /// + public const string ToggleLineComment = "Toggle Line Comment Command Handler"; + + /// + /// Command handler name for Paste Content in Interactive Format. + /// + public const string InteractivePaste = "Interactive Paste Command Handler"; + + /// + /// Command handler name for Paste in Paste Tracking. + /// + public const string PasteTrackingPaste = "Paste Tracking Paste Command Handler"; + + /// + /// Command handler name for Paste in Add Imports. + /// + public const string AddImportsPaste = "Add Imports Paste Command Handler"; + + /// + /// Command handler name for Edit and Continue file save handler. + /// + public const string EditAndContinueFileSave = "Edit and Continue Save File Handler"; + + /// + /// Command handler name for showing the Value Tracking tool window. + /// + public const string ShowValueTracking = "Show Value Tracking"; + + /// + /// Command handler name for showing the Callstack Explorer tool window. + /// + public const string ShowCallstackExplorer = "Show Callstack Explorer"; } diff --git a/src/EditorFeatures/Core/Extensibility/Completion/ExportCompletionProviderAttribute.cs b/src/EditorFeatures/Core/Extensibility/Completion/ExportCompletionProviderAttribute.cs index 80f97decdba4b..48be90a3f2da5 100644 --- a/src/EditorFeatures/Core/Extensibility/Completion/ExportCompletionProviderAttribute.cs +++ b/src/EditorFeatures/Core/Extensibility/Completion/ExportCompletionProviderAttribute.cs @@ -8,13 +8,12 @@ using System.ComponentModel.Composition; using Microsoft.CodeAnalysis.Completion; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +[MetadataAttribute] +[AttributeUsage(AttributeTargets.Class)] +internal class ExportCompletionProviderMef1Attribute(string name, string language) : ExportAttribute(typeof(CompletionProvider)) { - [MetadataAttribute] - [AttributeUsage(AttributeTargets.Class)] - internal class ExportCompletionProviderMef1Attribute(string name, string language) : ExportAttribute(typeof(CompletionProvider)) - { - public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); - public string Language { get; } = language ?? throw new ArgumentNullException(nameof(language)); - } + public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); + public string Language { get; } = language ?? throw new ArgumentNullException(nameof(language)); } diff --git a/src/EditorFeatures/Core/Extensibility/Completion/ICustomCommitCompletionProvider.cs b/src/EditorFeatures/Core/Extensibility/Completion/ICustomCommitCompletionProvider.cs index 9711a7b613d43..c4146fca3c140 100644 --- a/src/EditorFeatures/Core/Extensibility/Completion/ICustomCommitCompletionProvider.cs +++ b/src/EditorFeatures/Core/Extensibility/Completion/ICustomCommitCompletionProvider.cs @@ -6,14 +6,13 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +/// +/// Interface to implement for a completion provider that wants to provide customized commit +/// behavior. +/// +internal interface ICustomCommitCompletionProvider { - /// - /// Interface to implement for a completion provider that wants to provide customized commit - /// behavior. - /// - internal interface ICustomCommitCompletionProvider - { - void Commit(CompletionItem completionItem, Document document, ITextView textView, ITextBuffer subjectBuffer, ITextSnapshot triggerSnapshot, char? commitChar); - } + void Commit(CompletionItem completionItem, Document document, ITextView textView, ITextBuffer subjectBuffer, ITextSnapshot triggerSnapshot, char? commitChar); } diff --git a/src/EditorFeatures/Core/Extensibility/Completion/PredefinedCompletionProviderNames.cs b/src/EditorFeatures/Core/Extensibility/Completion/PredefinedCompletionProviderNames.cs index 5e3db22f22bf5..cdd08a294d4f4 100644 --- a/src/EditorFeatures/Core/Extensibility/Completion/PredefinedCompletionProviderNames.cs +++ b/src/EditorFeatures/Core/Extensibility/Completion/PredefinedCompletionProviderNames.cs @@ -4,13 +4,12 @@ #nullable disable -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal static class PredefinedCompletionProviderNames { - internal static class PredefinedCompletionProviderNames - { - /// - /// Completion provider for language keywords. - /// - public const string Keyword = "Keyword Completion Provider"; - } + /// + /// Completion provider for language keywords. + /// + public const string Keyword = "Keyword Completion Provider"; } diff --git a/src/EditorFeatures/Core/Extensibility/Composition/ContentTypeMetadata.cs b/src/EditorFeatures/Core/Extensibility/Composition/ContentTypeMetadata.cs index 3a9bcabd5f601..b89523d72290f 100644 --- a/src/EditorFeatures/Core/Extensibility/Composition/ContentTypeMetadata.cs +++ b/src/EditorFeatures/Core/Extensibility/Composition/ContentTypeMetadata.cs @@ -7,10 +7,9 @@ using System.Collections.Generic; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal class ContentTypeMetadata(IDictionary data) : IContentTypeMetadata { - internal class ContentTypeMetadata(IDictionary data) : IContentTypeMetadata - { - public IEnumerable ContentTypes { get; } = (IEnumerable)data.GetValueOrDefault("ContentTypes"); - } + public IEnumerable ContentTypes { get; } = (IEnumerable)data.GetValueOrDefault("ContentTypes"); } diff --git a/src/EditorFeatures/Core/Extensibility/Composition/IContentTypeMetadata.cs b/src/EditorFeatures/Core/Extensibility/Composition/IContentTypeMetadata.cs index bab644b4946fa..fa25e7c01c22d 100644 --- a/src/EditorFeatures/Core/Extensibility/Composition/IContentTypeMetadata.cs +++ b/src/EditorFeatures/Core/Extensibility/Composition/IContentTypeMetadata.cs @@ -6,10 +6,9 @@ using System.Collections.Generic; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface IContentTypeMetadata { - internal interface IContentTypeMetadata - { - IEnumerable ContentTypes { get; } - } + IEnumerable ContentTypes { get; } } diff --git a/src/EditorFeatures/Core/Extensibility/Composition/OrderableContentTypeMetadata.cs b/src/EditorFeatures/Core/Extensibility/Composition/OrderableContentTypeMetadata.cs index 63c47aebbe711..c9b72718dbda3 100644 --- a/src/EditorFeatures/Core/Extensibility/Composition/OrderableContentTypeMetadata.cs +++ b/src/EditorFeatures/Core/Extensibility/Composition/OrderableContentTypeMetadata.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System.Collections.Generic; using Roslyn.Utilities; @@ -11,6 +9,6 @@ namespace Microsoft.CodeAnalysis.Editor { internal class OrderableContentTypeMetadata(IDictionary data) : OrderableMetadata(data), IContentTypeMetadata { - public IEnumerable ContentTypes { get; } = (IEnumerable)data.GetValueOrDefault("ContentTypes"); + public IEnumerable ContentTypes { get; } = (IEnumerable?)data.GetValueOrDefault("ContentTypes") ?? []; } } diff --git a/src/EditorFeatures/Core/Extensibility/Composition/OrderableLanguageAndRoleMetadata.cs b/src/EditorFeatures/Core/Extensibility/Composition/OrderableLanguageAndRoleMetadata.cs deleted file mode 100644 index 5ce044c4b5b23..0000000000000 --- a/src/EditorFeatures/Core/Extensibility/Composition/OrderableLanguageAndRoleMetadata.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#nullable disable - -using System.Collections.Generic; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Host.Mef; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Editor -{ - internal class OrderableLanguageAndRoleMetadata(IDictionary data) : OrderableLanguageMetadata(data) - { - public IEnumerable Roles { get; } = (IEnumerable)data.GetValueOrDefault("TextViewRoles"); - } -} diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/AbstractEditorNavigationBarItemService.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/AbstractEditorNavigationBarItemService.cs index ab34ba9e49605..45620952b4f9b 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/AbstractEditorNavigationBarItemService.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/AbstractEditorNavigationBarItemService.cs @@ -14,88 +14,87 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.NavigationBar.RoslynNavigationBarItem; -namespace Microsoft.CodeAnalysis.Editor.Extensibility.NavigationBar +namespace Microsoft.CodeAnalysis.Editor.Extensibility.NavigationBar; + +internal abstract class AbstractEditorNavigationBarItemService : INavigationBarItemService { - internal abstract class AbstractEditorNavigationBarItemService : INavigationBarItemService + protected readonly IThreadingContext ThreadingContext; + + protected AbstractEditorNavigationBarItemService(IThreadingContext threadingContext) { - protected readonly IThreadingContext ThreadingContext; + ThreadingContext = threadingContext; + } - protected AbstractEditorNavigationBarItemService(IThreadingContext threadingContext) - { - ThreadingContext = threadingContext; - } + protected abstract Task TryNavigateToItemAsync(Document document, WrappedNavigationBarItem item, ITextView textView, ITextVersion textVersion, CancellationToken cancellationToken); - protected abstract Task TryNavigateToItemAsync(Document document, WrappedNavigationBarItem item, ITextView textView, ITextVersion textVersion, CancellationToken cancellationToken); + public async Task> GetItemsAsync( + Document document, + bool workspaceSupportsDocumentChanges, + bool forceFrozenPartialSemanticsForCrossProcessOperations, + ITextVersion textVersion, + CancellationToken cancellationToken) + { + var service = document.GetRequiredLanguageService(); + var items = await service.GetItemsAsync(document, workspaceSupportsDocumentChanges, forceFrozenPartialSemanticsForCrossProcessOperations, cancellationToken).ConfigureAwait(false); + return items.SelectAsArray(v => (NavigationBarItem)new WrappedNavigationBarItem(textVersion, v)); + } - public async Task> GetItemsAsync( - Document document, - bool workspaceSupportsDocumentChanges, - bool forceFrozenPartialSemanticsForCrossProcessOperations, - ITextVersion textVersion, - CancellationToken cancellationToken) - { - var service = document.GetRequiredLanguageService(); - var items = await service.GetItemsAsync(document, workspaceSupportsDocumentChanges, forceFrozenPartialSemanticsForCrossProcessOperations, cancellationToken).ConfigureAwait(false); - return items.SelectAsArray(v => (NavigationBarItem)new WrappedNavigationBarItem(textVersion, v)); - } + public Task TryNavigateToItemAsync(Document document, NavigationBarItem item, ITextView textView, ITextVersion textVersion, CancellationToken cancellationToken) + => TryNavigateToItemAsync(document, (WrappedNavigationBarItem)item, textView, textVersion, cancellationToken); - public Task TryNavigateToItemAsync(Document document, NavigationBarItem item, ITextView textView, ITextVersion textVersion, CancellationToken cancellationToken) - => TryNavigateToItemAsync(document, (WrappedNavigationBarItem)item, textView, textVersion, cancellationToken); + protected async Task NavigateToSymbolItemAsync( + Document document, NavigationBarItem item, SymbolItem symbolItem, ITextVersion textVersion, CancellationToken cancellationToken) + { + var workspace = document.Project.Solution.Workspace; - protected async Task NavigateToSymbolItemAsync( - Document document, NavigationBarItem item, SymbolItem symbolItem, ITextVersion textVersion, CancellationToken cancellationToken) - { - var workspace = document.Project.Solution.Workspace; + var (documentId, position, virtualSpace) = await GetNavigationLocationAsync( + document, item, symbolItem, textVersion, cancellationToken).ConfigureAwait(false); - var (documentId, position, virtualSpace) = await GetNavigationLocationAsync( - document, item, symbolItem, textVersion, cancellationToken).ConfigureAwait(false); + await NavigateToPositionAsync(workspace, documentId, position, virtualSpace, cancellationToken).ConfigureAwait(false); + } - await NavigateToPositionAsync(workspace, documentId, position, virtualSpace, cancellationToken).ConfigureAwait(false); - } + protected async Task NavigateToPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, CancellationToken cancellationToken) + { + var navigationService = workspace.Services.GetRequiredService(); - protected async Task NavigateToPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, CancellationToken cancellationToken) + if (!await navigationService.TryNavigateToPositionAsync( + ThreadingContext, workspace, documentId, position, virtualSpace, NavigationOptions.Default, cancellationToken).ConfigureAwait(false)) { - var navigationService = workspace.Services.GetRequiredService(); - - if (!await navigationService.TryNavigateToPositionAsync( - ThreadingContext, workspace, documentId, position, virtualSpace, NavigationOptions.Default, cancellationToken).ConfigureAwait(false)) - { - // Ensure we're back on the UI thread before showing a failure message. - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var notificationService = workspace.Services.GetRequiredService(); - notificationService.SendNotification(EditorFeaturesResources.The_definition_of_the_object_is_hidden, severity: NotificationSeverity.Error); - } + // Ensure we're back on the UI thread before showing a failure message. + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var notificationService = workspace.Services.GetRequiredService(); + notificationService.SendNotification(EditorFeaturesResources.The_definition_of_the_object_is_hidden, severity: NotificationSeverity.Error); } + } - internal virtual Task<(DocumentId documentId, int position, int virtualSpace)> GetNavigationLocationAsync( - Document document, - NavigationBarItem item, - SymbolItem symbolItem, - ITextVersion textVersion, - CancellationToken cancellationToken) + internal virtual Task<(DocumentId documentId, int position, int virtualSpace)> GetNavigationLocationAsync( + Document document, + NavigationBarItem item, + SymbolItem symbolItem, + ITextVersion textVersion, + CancellationToken cancellationToken) + { + if (symbolItem.Location.InDocumentInfo != null) { - if (symbolItem.Location.InDocumentInfo != null) - { - // If the item points to a location in this document, then just determine the where that span currently - // is (in case recent edits have moved it) and navigate there. - var navigationSpan = item.GetCurrentItemSpan(textVersion, symbolItem.Location.InDocumentInfo.Value.navigationSpan); - return Task.FromResult((document.Id, navigationSpan.Start, 0)); - } - else - { - // Otherwise, the item pointed to a location in another document. Just return the position we - // computed and stored for it. - Contract.ThrowIfNull(symbolItem.Location.OtherDocumentInfo); - var (documentId, span) = symbolItem.Location.OtherDocumentInfo.Value; - return Task.FromResult((documentId, span.Start, 0)); - } + // If the item points to a location in this document, then just determine the where that span currently + // is (in case recent edits have moved it) and navigate there. + var navigationSpan = item.GetCurrentItemSpan(textVersion, symbolItem.Location.InDocumentInfo.Value.navigationSpan); + return Task.FromResult((document.Id, navigationSpan.Start, 0)); } - - public bool ShowItemGrayedIfNear(NavigationBarItem item) + else { - // We only show items in gray when near that actually exist (i.e. are not meant for codegen). - // This will be all C# items, and only VB non-codegen items. - return ((WrappedNavigationBarItem)item).UnderlyingItem is SymbolItem; + // Otherwise, the item pointed to a location in another document. Just return the position we + // computed and stored for it. + Contract.ThrowIfNull(symbolItem.Location.OtherDocumentInfo); + var (documentId, span) = symbolItem.Location.OtherDocumentInfo.Value; + return Task.FromResult((documentId, span.Start, 0)); } } + + public bool ShowItemGrayedIfNear(NavigationBarItem item) + { + // We only show items in gray when near that actually exist (i.e. are not meant for codegen). + // This will be all C# items, and only VB non-codegen items. + return ((WrappedNavigationBarItem)item).UnderlyingItem is SymbolItem; + } } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarControllerFactoryService.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarControllerFactoryService.cs index 0ff6f947d5f33..f88d4818bf1ac 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarControllerFactoryService.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarControllerFactoryService.cs @@ -7,10 +7,9 @@ using System; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface INavigationBarControllerFactoryService { - internal interface INavigationBarControllerFactoryService - { - IDisposable CreateController(INavigationBarPresenter presenter, ITextBuffer textBuffer); - } + IDisposable CreateController(INavigationBarPresenter presenter, ITextBuffer textBuffer); } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarItemService.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarItemService.cs index bdcf0ed3c19b9..650a61dea5e00 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarItemService.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarItemService.cs @@ -9,22 +9,21 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface INavigationBarItemService : ILanguageService { - internal interface INavigationBarItemService : ILanguageService - { - Task> GetItemsAsync( - Document document, - bool workspaceSupportsDocumentChanges, - bool forceFrozenPartialSemanticsForCrossProcessOperations, - ITextVersion textVersion, - CancellationToken cancellationToken); - bool ShowItemGrayedIfNear(NavigationBarItem item); + Task> GetItemsAsync( + Document document, + bool workspaceSupportsDocumentChanges, + bool forceFrozenPartialSemanticsForCrossProcessOperations, + ITextVersion textVersion, + CancellationToken cancellationToken); + bool ShowItemGrayedIfNear(NavigationBarItem item); - /// - /// Returns if navigation (or generation) happened. otherwise. - /// - Task TryNavigateToItemAsync( - Document document, NavigationBarItem item, ITextView view, ITextVersion textVersion, CancellationToken cancellationToken); - } + /// + /// Returns if navigation (or generation) happened. otherwise. + /// + Task TryNavigateToItemAsync( + Document document, NavigationBarItem item, ITextView view, ITextVersion textVersion, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarPresenter.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarPresenter.cs index f1ec22e2cc1d6..9f417e4a9aadf 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarPresenter.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarPresenter.cs @@ -6,26 +6,25 @@ using System.Collections.Immutable; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface INavigationBarPresenter { - internal interface INavigationBarPresenter - { - void Disconnect(); + void Disconnect(); - void PresentItems( - ImmutableArray projects, - NavigationBarProjectItem? selectedProject, - ImmutableArray typesWithMembers, - NavigationBarItem? selectedType, - NavigationBarItem? selectedMember); + void PresentItems( + ImmutableArray projects, + NavigationBarProjectItem? selectedProject, + ImmutableArray typesWithMembers, + NavigationBarItem? selectedType, + NavigationBarItem? selectedMember); - ITextView TryGetCurrentView(); + ITextView TryGetCurrentView(); - /// - /// An event raised when the caret is moved or the active view in the document changed. In either case, the symbol under the caret has changed, so we should refresh. - /// - event EventHandler CaretMovedOrActiveViewChanged; + /// + /// An event raised when the caret is moved or the active view in the document changed. In either case, the symbol under the caret has changed, so we should refresh. + /// + event EventHandler CaretMovedOrActiveViewChanged; - event EventHandler ItemSelected; - } + event EventHandler ItemSelected; } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarAutomationStrings.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarAutomationStrings.cs index 1b6811482cda3..7aa89660ee9a0 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarAutomationStrings.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarAutomationStrings.cs @@ -4,15 +4,14 @@ #nullable disable -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal static class NavigationBarAutomationStrings { - internal static class NavigationBarAutomationStrings - { - public const string ProjectDropdownName = "Projects"; - public const string ProjectDropdownId = "ProjectsList"; - public const string TypeDropdownName = "Objects"; - public const string TypeDropdownId = "ScopesList"; - public const string MemberDropdownName = "Members"; - public const string MemberDropdownId = "FunctionsList"; - } + public const string ProjectDropdownName = "Projects"; + public const string ProjectDropdownId = "ProjectsList"; + public const string TypeDropdownName = "Objects"; + public const string TypeDropdownId = "ScopesList"; + public const string MemberDropdownName = "Members"; + public const string MemberDropdownId = "FunctionsList"; } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarDropdownKind.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarDropdownKind.cs index 7af52ee4570dd..6f8956239d402 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarDropdownKind.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarDropdownKind.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal enum NavigationBarDropdownKind { - internal enum NavigationBarDropdownKind - { - Project = 0, - Type = 1, - Member = 2 - } + Project = 0, + Type = 1, + Member = 2 } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarItem.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarItem.cs index 77254b6ea4f32..163b544cdc304 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarItem.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarItem.cs @@ -11,59 +11,58 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal abstract class NavigationBarItem( + ITextVersion? textVersion, + string text, + Glyph glyph, + ImmutableArray spans, + ImmutableArray childItems = default, + int indent = 0, + bool bolded = false, + bool grayed = false) : IEquatable { - internal abstract class NavigationBarItem( - ITextVersion? textVersion, - string text, - Glyph glyph, - ImmutableArray spans, - ImmutableArray childItems = default, - int indent = 0, - bool bolded = false, - bool grayed = false) : IEquatable - { - public string Text { get; } = text; - public Glyph Glyph { get; } = glyph; - public bool Bolded { get; } = bolded; - public bool Grayed { get; } = grayed; - public int Indent { get; } = indent; - public ImmutableArray ChildItems { get; } = childItems.NullToEmpty(); + public string Text { get; } = text; + public Glyph Glyph { get; } = glyph; + public bool Bolded { get; } = bolded; + public bool Grayed { get; } = grayed; + public int Indent { get; } = indent; + public ImmutableArray ChildItems { get; } = childItems.NullToEmpty(); - /// - /// The spans in the owning document corresponding to this nav bar item. If the user's caret enters one of - /// these spans, we'll select that item in the nav bar (except if they're in an item's span that is nested - /// within this). - /// - /// This can be empty for items whose location is in another document. - public ImmutableArray Spans { get; } = spans; + /// + /// The spans in the owning document corresponding to this nav bar item. If the user's caret enters one of + /// these spans, we'll select that item in the nav bar (except if they're in an item's span that is nested + /// within this). + /// + /// This can be empty for items whose location is in another document. + public ImmutableArray Spans { get; } = spans; - internal ITextVersion? TextVersion { get; } = textVersion; + internal ITextVersion? TextVersion { get; } = textVersion; - public abstract override bool Equals(object? obj); - public abstract override int GetHashCode(); + public abstract override bool Equals(object? obj); + public abstract override int GetHashCode(); - public bool Equals(NavigationBarItem? other) - { - return other != null && - Text == other.Text && - Glyph == other.Glyph && - Bolded == other.Bolded && - Grayed == other.Grayed && - Indent == other.Indent && - ChildItems.SequenceEqual(other.ChildItems) && - Spans.SequenceEqual(other.Spans); - } + public bool Equals(NavigationBarItem? other) + { + return other != null && + Text == other.Text && + Glyph == other.Glyph && + Bolded == other.Bolded && + Grayed == other.Grayed && + Indent == other.Indent && + ChildItems.SequenceEqual(other.ChildItems) && + Spans.SequenceEqual(other.Spans); } +} - internal static class NavigationBarItemExtensions +internal static class NavigationBarItemExtensions +{ + public static TextSpan GetCurrentItemSpan(this NavigationBarItem item, ITextVersion toVersion, TextSpan span) { - public static TextSpan GetCurrentItemSpan(this NavigationBarItem item, ITextVersion toVersion, TextSpan span) - { - Contract.ThrowIfNull(item.TextVersion, "This should only be called for locations the caller knows to be in the open file"); - return item.TextVersion.CreateTrackingSpan(span.ToSpan(), SpanTrackingMode.EdgeExclusive) - .GetSpan(toVersion) - .ToTextSpan(); - } + Contract.ThrowIfNull(item.TextVersion, "This should only be called for locations the caller knows to be in the open file"); + return item.TextVersion.CreateTrackingSpan(span.ToSpan(), SpanTrackingMode.EdgeExclusive) + .GetSpan(toVersion) + .ToTextSpan(); } } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarItemSelectedEventArgs.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarItemSelectedEventArgs.cs index f721e5c26b194..d0d049f32c4f6 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarItemSelectedEventArgs.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarItemSelectedEventArgs.cs @@ -4,10 +4,9 @@ using System; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal sealed class NavigationBarItemSelectedEventArgs(NavigationBarItem item) : EventArgs { - internal sealed class NavigationBarItemSelectedEventArgs(NavigationBarItem item) : EventArgs - { - public NavigationBarItem Item { get; } = item; - } + public NavigationBarItem Item { get; } = item; } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarProjectItem.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarProjectItem.cs index 0242b6ff0ddb5..9d741f28f74e0 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarProjectItem.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarProjectItem.cs @@ -6,41 +6,40 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal sealed class NavigationBarProjectItem( + string text, + Glyph glyph, + Workspace workspace, + DocumentId documentId, + string language) : NavigationBarItem(textVersion: null, text, glyph, + spans: ImmutableArray.Empty, + childItems: ImmutableArray.Empty, + indent: 0, bolded: false, grayed: false), IEquatable { - internal sealed class NavigationBarProjectItem( - string text, - Glyph glyph, - Workspace workspace, - DocumentId documentId, - string language) : NavigationBarItem(textVersion: null, text, glyph, - spans: ImmutableArray.Empty, - childItems: ImmutableArray.Empty, - indent: 0, bolded: false, grayed: false), IEquatable - { - public Workspace Workspace { get; } = workspace; - public DocumentId DocumentId { get; } = documentId; - public string Language { get; } = language; + public Workspace Workspace { get; } = workspace; + public DocumentId DocumentId { get; } = documentId; + public string Language { get; } = language; - internal void SwitchToContext() + internal void SwitchToContext() + { + if (this.Workspace.CanChangeActiveContextDocument) { - if (this.Workspace.CanChangeActiveContextDocument) - { - // TODO: Can we pass something better? - this.Workspace.SetDocumentContext(DocumentId); - } + // TODO: Can we pass something better? + this.Workspace.SetDocumentContext(DocumentId); } + } - public override bool Equals(object? obj) - => Equals(obj as NavigationBarProjectItem); + public override bool Equals(object? obj) + => Equals(obj as NavigationBarProjectItem); - public bool Equals(NavigationBarProjectItem? item) - => base.Equals(item) && - Workspace == item.Workspace && - DocumentId == item.DocumentId && - Language == item.Language; + public bool Equals(NavigationBarProjectItem? item) + => base.Equals(item) && + Workspace == item.Workspace && + DocumentId == item.DocumentId && + Language == item.Language; - public override int GetHashCode() - => throw new NotImplementedException(); - } + public override int GetHashCode() + => throw new NotImplementedException(); } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarSelectedItems.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarSelectedItems.cs index 3c2703d0ce264..0146b46a71dc0 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarSelectedItems.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/NavigationBarSelectedItems.cs @@ -4,37 +4,36 @@ using System; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal class NavigationBarSelectedTypeAndMember( + NavigationBarItem? typeItem, + bool showTypeItemGrayed, + NavigationBarItem? memberItem, + bool showMemberItemGrayed) : IEquatable { - internal class NavigationBarSelectedTypeAndMember( - NavigationBarItem? typeItem, - bool showTypeItemGrayed, - NavigationBarItem? memberItem, - bool showMemberItemGrayed) : IEquatable + public static readonly NavigationBarSelectedTypeAndMember Empty = new(typeItem: null, memberItem: null); + + public NavigationBarItem? TypeItem { get; } = typeItem; + public bool ShowTypeItemGrayed { get; } = showTypeItemGrayed; + public NavigationBarItem? MemberItem { get; } = memberItem; + public bool ShowMemberItemGrayed { get; } = showMemberItemGrayed; + + public NavigationBarSelectedTypeAndMember(NavigationBarItem? typeItem, NavigationBarItem? memberItem) + : this(typeItem, showTypeItemGrayed: false, memberItem, showMemberItemGrayed: false) { - public static readonly NavigationBarSelectedTypeAndMember Empty = new(typeItem: null, memberItem: null); - - public NavigationBarItem? TypeItem { get; } = typeItem; - public bool ShowTypeItemGrayed { get; } = showTypeItemGrayed; - public NavigationBarItem? MemberItem { get; } = memberItem; - public bool ShowMemberItemGrayed { get; } = showMemberItemGrayed; - - public NavigationBarSelectedTypeAndMember(NavigationBarItem? typeItem, NavigationBarItem? memberItem) - : this(typeItem, showTypeItemGrayed: false, memberItem, showMemberItemGrayed: false) - { - } - - public override bool Equals(object? obj) - => Equals(obj as NavigationBarSelectedTypeAndMember); - - public bool Equals(NavigationBarSelectedTypeAndMember? other) - => other != null && - this.ShowTypeItemGrayed == other.ShowTypeItemGrayed && - this.ShowMemberItemGrayed == other.ShowMemberItemGrayed && - Equals(this.TypeItem, other.TypeItem) && - Equals(this.MemberItem, other.MemberItem); - - public override int GetHashCode() - => throw new NotImplementedException(); } + + public override bool Equals(object? obj) + => Equals(obj as NavigationBarSelectedTypeAndMember); + + public bool Equals(NavigationBarSelectedTypeAndMember? other) + => other != null && + this.ShowTypeItemGrayed == other.ShowTypeItemGrayed && + this.ShowMemberItemGrayed == other.ShowMemberItemGrayed && + Equals(this.TypeItem, other.TypeItem) && + Equals(this.MemberItem, other.MemberItem); + + public override int GetHashCode() + => throw new NotImplementedException(); } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/SimpleNavigationBarItem.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/SimpleNavigationBarItem.cs index 2b81a30f7fcc4..fdebb1c894056 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/SimpleNavigationBarItem.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/SimpleNavigationBarItem.cs @@ -7,17 +7,16 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal sealed class SimpleNavigationBarItem(ITextVersion textVersion, string text, Glyph glyph, ImmutableArray spans, ImmutableArray childItems, int indent, bool bolded, bool grayed) : NavigationBarItem(textVersion, text, glyph, spans, childItems, indent, bolded, grayed), IEquatable { - internal sealed class SimpleNavigationBarItem(ITextVersion textVersion, string text, Glyph glyph, ImmutableArray spans, ImmutableArray childItems, int indent, bool bolded, bool grayed) : NavigationBarItem(textVersion, text, glyph, spans, childItems, indent, bolded, grayed), IEquatable - { - public override bool Equals(object? obj) - => Equals(obj as SimpleNavigationBarItem); + public override bool Equals(object? obj) + => Equals(obj as SimpleNavigationBarItem); - public bool Equals(SimpleNavigationBarItem? other) - => base.Equals(other); + public bool Equals(SimpleNavigationBarItem? other) + => base.Equals(other); - public override int GetHashCode() - => throw new NotImplementedException(); - } + public override int GetHashCode() + => throw new NotImplementedException(); } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/WrappedNavigationBarItem.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/WrappedNavigationBarItem.cs index 38254efe062ef..872596c7274ca 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/WrappedNavigationBarItem.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/WrappedNavigationBarItem.cs @@ -10,67 +10,66 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +/// +/// Implementation of the editor layer that wraps a feature layer +/// +// We suppress this as this type *does* override ComputeAdditionalHashCodeParts +internal sealed class WrappedNavigationBarItem : NavigationBarItem, IEquatable { - /// - /// Implementation of the editor layer that wraps a feature layer - /// - // We suppress this as this type *does* override ComputeAdditionalHashCodeParts - internal sealed class WrappedNavigationBarItem : NavigationBarItem, IEquatable + public readonly RoslynNavigationBarItem UnderlyingItem; + + internal WrappedNavigationBarItem(ITextVersion textVersion, RoslynNavigationBarItem underlyingItem) + : base( + textVersion, + underlyingItem.Text, + underlyingItem.Glyph, + GetSpans(underlyingItem), + underlyingItem.ChildItems.SelectAsArray(v => (NavigationBarItem)new WrappedNavigationBarItem(textVersion, v)), + underlyingItem.Indent, + underlyingItem.Bolded, + underlyingItem.Grayed) { - public readonly RoslynNavigationBarItem UnderlyingItem; + UnderlyingItem = underlyingItem; + } - internal WrappedNavigationBarItem(ITextVersion textVersion, RoslynNavigationBarItem underlyingItem) - : base( - textVersion, - underlyingItem.Text, - underlyingItem.Glyph, - GetSpans(underlyingItem), - underlyingItem.ChildItems.SelectAsArray(v => (NavigationBarItem)new WrappedNavigationBarItem(textVersion, v)), - underlyingItem.Indent, - underlyingItem.Bolded, - underlyingItem.Grayed) - { - UnderlyingItem = underlyingItem; - } + private static ImmutableArray GetSpans(RoslynNavigationBarItem underlyingItem) + { + using var _ = ArrayBuilder.GetInstance(out var spans); + AddSpans(underlyingItem, spans); + spans.SortAndRemoveDuplicates(Comparer.Default); + return spans.ToImmutable(); - private static ImmutableArray GetSpans(RoslynNavigationBarItem underlyingItem) + static void AddSpans(RoslynNavigationBarItem underlyingItem, ArrayBuilder spans) { - using var _ = ArrayBuilder.GetInstance(out var spans); - AddSpans(underlyingItem, spans); - spans.SortAndRemoveDuplicates(Comparer.Default); - return spans.ToImmutable(); - - static void AddSpans(RoslynNavigationBarItem underlyingItem, ArrayBuilder spans) + // For a regular symbol we want to select it if the user puts their caret in any of the spans of it in this file. + if (underlyingItem is RoslynNavigationBarItem.SymbolItem { Location.InDocumentInfo.spans: var symbolSpans }) + { + spans.AddRange(symbolSpans); + } + else if (underlyingItem is RoslynNavigationBarItem.ActionlessItem) { - // For a regular symbol we want to select it if the user puts their caret in any of the spans of it in this file. - if (underlyingItem is RoslynNavigationBarItem.SymbolItem { Location.InDocumentInfo.spans: var symbolSpans }) - { - spans.AddRange(symbolSpans); - } - else if (underlyingItem is RoslynNavigationBarItem.ActionlessItem) - { - // An actionless item represents something that exists just to show a child-list, but should otherwise - // not navigate or cause anything to be generated. However, we still want to automatically select it - // whenever the user puts their caret in any of the spans of its child items in this file. - // - // For example, in VB any withevents members will be put in the type-list, and the events those members - // are hooked up to will then be in the member-list. In this case, we want moving into the span of that - // member to select the withevent member in the type-list. - foreach (var child in underlyingItem.ChildItems) - AddSpans(child, spans); - } + // An actionless item represents something that exists just to show a child-list, but should otherwise + // not navigate or cause anything to be generated. However, we still want to automatically select it + // whenever the user puts their caret in any of the spans of its child items in this file. + // + // For example, in VB any withevents members will be put in the type-list, and the events those members + // are hooked up to will then be in the member-list. In this case, we want moving into the span of that + // member to select the withevent member in the type-list. + foreach (var child in underlyingItem.ChildItems) + AddSpans(child, spans); } } + } - public override bool Equals(object? obj) - => Equals(obj as WrappedNavigationBarItem); + public override bool Equals(object? obj) + => Equals(obj as WrappedNavigationBarItem); - public bool Equals(WrappedNavigationBarItem? other) - => base.Equals(other) && - UnderlyingItem.Equals(other.UnderlyingItem); + public bool Equals(WrappedNavigationBarItem? other) + => base.Equals(other) && + UnderlyingItem.Equals(other.UnderlyingItem); - public override int GetHashCode() - => throw new NotImplementedException(); - } + public override int GetHashCode() + => throw new NotImplementedException(); } diff --git a/src/EditorFeatures/Core/Extensibility/SignatureHelp/ISignatureHelpPresenterSession.cs b/src/EditorFeatures/Core/Extensibility/SignatureHelp/ISignatureHelpPresenterSession.cs index b97cccc33290d..b5fcb1a79acd2 100644 --- a/src/EditorFeatures/Core/Extensibility/SignatureHelp/ISignatureHelpPresenterSession.cs +++ b/src/EditorFeatures/Core/Extensibility/SignatureHelp/ISignatureHelpPresenterSession.cs @@ -9,16 +9,15 @@ using Microsoft.CodeAnalysis.SignatureHelp; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface ISignatureHelpPresenterSession : IIntelliSensePresenterSession { - internal interface ISignatureHelpPresenterSession : IIntelliSensePresenterSession - { - void PresentItems(ITrackingSpan triggerSpan, IList items, SignatureHelpItem selectedItem, int? selectedParameter); - void SelectPreviousItem(); - void SelectNextItem(); + void PresentItems(ITrackingSpan triggerSpan, IList items, SignatureHelpItem selectedItem, int? selectedParameter); + void SelectPreviousItem(); + void SelectNextItem(); - event EventHandler ItemSelected; + event EventHandler ItemSelected; - bool EditorSessionIsActive { get; } - } + bool EditorSessionIsActive { get; } } diff --git a/src/EditorFeatures/Core/Extensibility/SignatureHelp/PredefinedSignatureHelpPresenterNames.cs b/src/EditorFeatures/Core/Extensibility/SignatureHelp/PredefinedSignatureHelpPresenterNames.cs index d5161b57b4850..169faddbb2498 100644 --- a/src/EditorFeatures/Core/Extensibility/SignatureHelp/PredefinedSignatureHelpPresenterNames.cs +++ b/src/EditorFeatures/Core/Extensibility/SignatureHelp/PredefinedSignatureHelpPresenterNames.cs @@ -4,10 +4,9 @@ #nullable disable -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal static class PredefinedSignatureHelpPresenterNames { - internal static class PredefinedSignatureHelpPresenterNames - { - public const string RoslynSignatureHelpPresenter = "Roslyn Signature Help Presenter"; - } + public const string RoslynSignatureHelpPresenter = "Roslyn Signature Help Presenter"; } diff --git a/src/EditorFeatures/Core/Extensibility/SignatureHelp/SignatureHelpItemEventArgs.cs b/src/EditorFeatures/Core/Extensibility/SignatureHelp/SignatureHelpItemEventArgs.cs index 32e74a26d2867..a57372b3b5a38 100644 --- a/src/EditorFeatures/Core/Extensibility/SignatureHelp/SignatureHelpItemEventArgs.cs +++ b/src/EditorFeatures/Core/Extensibility/SignatureHelp/SignatureHelpItemEventArgs.cs @@ -7,10 +7,9 @@ using System; using Microsoft.CodeAnalysis.SignatureHelp; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal class SignatureHelpItemEventArgs(SignatureHelpItem signatureHelpItem) : EventArgs { - internal class SignatureHelpItemEventArgs(SignatureHelpItem signatureHelpItem) : EventArgs - { - public SignatureHelpItem SignatureHelpItem { get; } = signatureHelpItem; - } + public SignatureHelpItem SignatureHelpItem { get; } = signatureHelpItem; } diff --git a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntelliCodeArgumentDefaultsSource.cs b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntelliCodeArgumentDefaultsSource.cs index 7ac6a9ac08fe5..8d8993dfc115c 100644 --- a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntelliCodeArgumentDefaultsSource.cs +++ b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntelliCodeArgumentDefaultsSource.cs @@ -7,52 +7,51 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.ExternalAccess.IntelliCode.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.IntelliCode.Api; + +/// +/// Provides a list of possible default arguments for method calls. +/// +/// +/// This is a MEF component and should be exported with and attributes +/// and optional and attributes. +/// An instance of is selected +/// first by matching ContentType with content type of the , and then by order. +/// Only one is used in a given view. +/// +/// Only one will used for any given . The sources are +/// ordered by the Order attribute. The first source (if any) that satisfies the ContentType and TextViewRoles +/// attributes will be the source used to provide defaults. +/// +/// +/// +/// [Export(typeof(IIntelliCodeArgumentDefaultsSource))] +/// [Name(nameof(IntelliCodeArgumentDefaultsSource))] +/// [ContentType("text")] +/// [TextViewRoles(PredefinedTextViewRoles.Editable)] +/// [Order(Before = "OtherCompletionDefaultsSource")] +/// public class IntelliCodeArgumentDefaultsSource : IIntelliCodeArgumentDefaultsSource +/// +/// +/// +internal interface IIntelliCodeArgumentDefaultsSource { /// - /// Provides a list of possible default arguments for method calls. + /// Gets a list of possible default arguments for a method signature. /// + /// View for which the defaults are desired. + /// A list of possible default arguments for a method signature. /// - /// This is a MEF component and should be exported with and attributes - /// and optional and attributes. - /// An instance of is selected - /// first by matching ContentType with content type of the , and then by order. - /// Only one is used in a given view. - /// - /// Only one will used for any given . The sources are - /// ordered by the Order attribute. The first source (if any) that satisfies the ContentType and TextViewRoles - /// attributes will be the source used to provide defaults. - /// - /// + /// The returned value will always be in the form of a "complete" set of arguments, including the leading and trailing parenthesis. + /// For example: /// - /// [Export(typeof(IIntelliCodeArgumentDefaultsSource))] - /// [Name(nameof(IntelliCodeArgumentDefaultsSource))] - /// [ContentType("text")] - /// [TextViewRoles(PredefinedTextViewRoles.Editable)] - /// [Order(Before = "OtherCompletionDefaultsSource")] - /// public class IntelliCodeArgumentDefaultsSource : IIntelliCodeArgumentDefaultsSource + /// () + /// (args[0]) + /// (args.Length) + /// (value: args.Length) /// - /// + /// + /// Some of the proposals may be syntactically/semantically invalid (and can be ignored by the caller). /// - internal interface IIntelliCodeArgumentDefaultsSource - { - /// - /// Gets a list of possible default arguments for a method signature. - /// - /// View for which the defaults are desired. - /// A list of possible default arguments for a method signature. - /// - /// The returned value will always be in the form of a "complete" set of arguments, including the leading and trailing parenthesis. - /// For example: - /// - /// () - /// (args[0]) - /// (args.Length) - /// (value: args.Length) - /// - /// - /// Some of the proposals may be syntactically/semantically invalid (and can be ignored by the caller). - /// - Task> GetArgumentDefaultsAsync(ITextView view); - } + Task> GetArgumentDefaultsAsync(ITextView view); } diff --git a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntentSourceProvider.cs b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntentSourceProvider.cs index 2fa83a2559c75..2389d70300c63 100644 --- a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntentSourceProvider.cs +++ b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/Api/IIntentSourceProvider.cs @@ -10,72 +10,71 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.IntelliCode.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.IntelliCode.Api; + +internal interface IIntentSourceProvider { - internal interface IIntentSourceProvider - { - /// - /// For an input intent, computes the edits required to apply that intent and returns them. - /// - /// the intents with the context in which the intent was found. - /// the edits that should be applied to the current snapshot. - Task> ComputeIntentsAsync(IntentRequestContext context, CancellationToken cancellationToken = default); - } + /// + /// For an input intent, computes the edits required to apply that intent and returns them. + /// + /// the intents with the context in which the intent was found. + /// the edits that should be applied to the current snapshot. + Task> ComputeIntentsAsync(IntentRequestContext context, CancellationToken cancellationToken = default); +} +/// +/// Defines the data needed to compute the code action edits from an intent. +/// +internal readonly struct IntentRequestContext(string intentName, SnapshotSpan currentSnapshotSpan, ImmutableArray textEditsToPrior, TextSpan priorSelection, string? intentData) +{ /// - /// Defines the data needed to compute the code action edits from an intent. + /// The intent name. contains all intents roslyn knows how to handle. /// - internal readonly struct IntentRequestContext(string intentName, SnapshotSpan currentSnapshotSpan, ImmutableArray textEditsToPrior, TextSpan priorSelection, string? intentData) - { - /// - /// The intent name. contains all intents roslyn knows how to handle. - /// - public string IntentName { get; } = intentName ?? throw new ArgumentNullException(nameof(intentName)); + public string IntentName { get; } = intentName ?? throw new ArgumentNullException(nameof(intentName)); - /// - /// JSON formatted data specific to the intent that must be deserialized into the appropriate object. - /// - public string? IntentData { get; } = intentData; + /// + /// JSON formatted data specific to the intent that must be deserialized into the appropriate object. + /// + public string? IntentData { get; } = intentData; - /// - /// The text snapshot and selection when - /// was called to compute the text edits and against which the resulting text edits will be calculated. - /// - public SnapshotSpan CurrentSnapshotSpan { get; } = currentSnapshotSpan; + /// + /// The text snapshot and selection when + /// was called to compute the text edits and against which the resulting text edits will be calculated. + /// + public SnapshotSpan CurrentSnapshotSpan { get; } = currentSnapshotSpan; - /// - /// The text edits that should be applied to the to calculate - /// a prior text snapshot before the intent happened. The snapshot is used to calculate the actions. - /// - public ImmutableArray PriorTextEdits { get; } = textEditsToPrior; + /// + /// The text edits that should be applied to the to calculate + /// a prior text snapshot before the intent happened. The snapshot is used to calculate the actions. + /// + public ImmutableArray PriorTextEdits { get; } = textEditsToPrior; - /// - /// The caret position / selection in the snapshot calculated by applying - /// to the - /// - public TextSpan PriorSelection { get; } = priorSelection; - } + /// + /// The caret position / selection in the snapshot calculated by applying + /// to the + /// + public TextSpan PriorSelection { get; } = priorSelection; +} +/// +/// Defines the text changes needed to apply an intent. +/// +internal readonly struct IntentSource(string title, string actionName, ImmutableDictionary> documentChanges) +{ /// - /// Defines the text changes needed to apply an intent. + /// The title associated with this intent result. /// - internal readonly struct IntentSource(string title, string actionName, ImmutableDictionary> documentChanges) - { - /// - /// The title associated with this intent result. - /// - public readonly string Title { get; } = title ?? throw new ArgumentNullException(nameof(title)); + public readonly string Title { get; } = title ?? throw new ArgumentNullException(nameof(title)); - /// - /// The text changes that should be applied to each document. - /// - public readonly ImmutableDictionary> DocumentChanges = documentChanges; + /// + /// The text changes that should be applied to each document. + /// + public readonly ImmutableDictionary> DocumentChanges = documentChanges; - /// - /// Contains metadata that can be used to identify the kind of sub-action these edits - /// apply to for the requested intent. Used for telemetry purposes only. - /// For example, the code action type name like FieldDelegatingCodeAction. - /// - public readonly string ActionName { get; } = actionName ?? throw new ArgumentNullException(nameof(actionName)); - } + /// + /// Contains metadata that can be used to identify the kind of sub-action these edits + /// apply to for the requested intent. Used for telemetry purposes only. + /// For example, the code action type name like FieldDelegatingCodeAction. + /// + public readonly string ActionName { get; } = actionName ?? throw new ArgumentNullException(nameof(actionName)); } diff --git a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/IntentProcessor.cs b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/IntentProcessor.cs index 7a6914802dfbc..295958e5aca64 100644 --- a/src/EditorFeatures/Core/ExternalAccess/IntelliCode/IntentProcessor.cs +++ b/src/EditorFeatures/Core/ExternalAccess/IntelliCode/IntentProcessor.cs @@ -22,117 +22,116 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ExternalAccess.IntelliCode +namespace Microsoft.CodeAnalysis.ExternalAccess.IntelliCode; + +[Export(typeof(IIntentSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class IntentSourceProvider( + [ImportMany] IEnumerable> lazyIntentProviders, + IGlobalOptionService globalOptions) : IIntentSourceProvider { - [Export(typeof(IIntentSourceProvider)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class IntentSourceProvider( - [ImportMany] IEnumerable> lazyIntentProviders, - IGlobalOptionService globalOptions) : IIntentSourceProvider + private readonly ImmutableDictionary<(string LanguageName, string IntentName), Lazy> _lazyIntentProviders = CreateProviderMap(lazyIntentProviders); + private readonly IGlobalOptionService _globalOptions = globalOptions; + + private static ImmutableDictionary<(string LanguageName, string IntentName), Lazy> CreateProviderMap( + IEnumerable> lazyIntentProviders) { - private readonly ImmutableDictionary<(string LanguageName, string IntentName), Lazy> _lazyIntentProviders = CreateProviderMap(lazyIntentProviders); - private readonly IGlobalOptionService _globalOptions = globalOptions; + return lazyIntentProviders.ToImmutableDictionary( + provider => (provider.Metadata.LanguageName, provider.Metadata.IntentName), + provider => provider); + } - private static ImmutableDictionary<(string LanguageName, string IntentName), Lazy> CreateProviderMap( - IEnumerable> lazyIntentProviders) + public async Task> ComputeIntentsAsync(IntentRequestContext intentRequestContext, CancellationToken cancellationToken) + { + var currentDocument = intentRequestContext.CurrentSnapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (currentDocument == null) { - return lazyIntentProviders.ToImmutableDictionary( - provider => (provider.Metadata.LanguageName, provider.Metadata.IntentName), - provider => provider); + throw new ArgumentException("could not retrieve document for request snapshot"); } - public async Task> ComputeIntentsAsync(IntentRequestContext intentRequestContext, CancellationToken cancellationToken) + var languageName = currentDocument.Project.Language; + if (!_lazyIntentProviders.TryGetValue((LanguageName: languageName, IntentName: intentRequestContext.IntentName), out var provider)) { - var currentDocument = intentRequestContext.CurrentSnapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (currentDocument == null) + Logger.Log(FunctionId.Intellicode_UnknownIntent, KeyValueLogMessage.Create(LogType.UserAction, m => { - throw new ArgumentException("could not retrieve document for request snapshot"); - } + m["intent"] = intentRequestContext.IntentName; + m["language"] = languageName; + })); - var languageName = currentDocument.Project.Language; - if (!_lazyIntentProviders.TryGetValue((LanguageName: languageName, IntentName: intentRequestContext.IntentName), out var provider)) - { - Logger.Log(FunctionId.Intellicode_UnknownIntent, KeyValueLogMessage.Create(LogType.UserAction, m => - { - m["intent"] = intentRequestContext.IntentName; - m["language"] = languageName; - })); + return []; + } - return []; - } + var currentText = await currentDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var originalDocument = currentDocument.WithText(currentText.WithChanges(intentRequestContext.PriorTextEdits)); - var currentText = await currentDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var originalDocument = currentDocument.WithText(currentText.WithChanges(intentRequestContext.PriorTextEdits)); + var selectionTextSpan = intentRequestContext.PriorSelection; - var selectionTextSpan = intentRequestContext.PriorSelection; + var results = await provider.Value.ComputeIntentAsync( + originalDocument, + selectionTextSpan, + currentDocument, + new IntentDataProvider( + intentRequestContext.IntentData, + _globalOptions.CreateProvider()), + cancellationToken).ConfigureAwait(false); - var results = await provider.Value.ComputeIntentAsync( - originalDocument, - selectionTextSpan, - currentDocument, - new IntentDataProvider( - intentRequestContext.IntentData, - _globalOptions.CreateProvider()), - cancellationToken).ConfigureAwait(false); + if (results.IsDefaultOrEmpty) + { + return []; + } - if (results.IsDefaultOrEmpty) - { - return []; - } + using var _ = ArrayBuilder.GetInstance(out var convertedResults); + foreach (var result in results) + { + var convertedIntent = await ConvertToIntelliCodeResultAsync(result, originalDocument, currentDocument, cancellationToken).ConfigureAwait(false); + convertedResults.AddIfNotNull(convertedIntent); + } - using var _ = ArrayBuilder.GetInstance(out var convertedResults); - foreach (var result in results) - { - var convertedIntent = await ConvertToIntelliCodeResultAsync(result, originalDocument, currentDocument, cancellationToken).ConfigureAwait(false); - convertedResults.AddIfNotNull(convertedIntent); - } + return convertedResults.ToImmutable(); + } - return convertedResults.ToImmutable(); - } + private static async Task ConvertToIntelliCodeResultAsync( + IntentProcessorResult processorResult, + Document originalDocument, + Document currentDocument, + CancellationToken cancellationToken) + { + var newSolution = processorResult.Solution; + // Merge linked file changes so all linked files have the same text changes. + newSolution = await newSolution.WithMergedLinkedFileChangesAsync(originalDocument.Project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); - private static async Task ConvertToIntelliCodeResultAsync( - IntentProcessorResult processorResult, - Document originalDocument, - Document currentDocument, - CancellationToken cancellationToken) + using var _ = PooledDictionary>.GetInstance(out var results); + foreach (var changedDocumentId in processorResult.ChangedDocuments) { - var newSolution = processorResult.Solution; - // Merge linked file changes so all linked files have the same text changes. - newSolution = await newSolution.WithMergedLinkedFileChangesAsync(originalDocument.Project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); - - using var _ = PooledDictionary>.GetInstance(out var results); - foreach (var changedDocumentId in processorResult.ChangedDocuments) + // Calculate the text changes by comparing the solution with intent applied to the current solution (not to be confused with the original solution, the one prior to intent detection). + var docChanges = await GetTextChangesForDocumentAsync(newSolution, currentDocument.Project.Solution, changedDocumentId, cancellationToken).ConfigureAwait(false); + if (docChanges != null) { - // Calculate the text changes by comparing the solution with intent applied to the current solution (not to be confused with the original solution, the one prior to intent detection). - var docChanges = await GetTextChangesForDocumentAsync(newSolution, currentDocument.Project.Solution, changedDocumentId, cancellationToken).ConfigureAwait(false); - if (docChanges != null) - { - results[changedDocumentId] = docChanges.Value; - } + results[changedDocumentId] = docChanges.Value; } - - return new IntentSource(processorResult.Title, processorResult.ActionName, results.ToImmutableDictionary()); } - private static async Task?> GetTextChangesForDocumentAsync( - Solution changedSolution, - Solution currentSolution, - DocumentId changedDocumentId, - CancellationToken cancellationToken) - { - var changedDocument = changedSolution.GetRequiredDocument(changedDocumentId); - var currentDocument = currentSolution.GetRequiredDocument(changedDocumentId); + return new IntentSource(processorResult.Title, processorResult.ActionName, results.ToImmutableDictionary()); + } - var textDiffService = changedSolution.Services.GetRequiredService(); - // Compute changes against the current version of the document. - var textDiffs = await textDiffService.GetTextChangesAsync(currentDocument, changedDocument, cancellationToken).ConfigureAwait(false); - if (textDiffs.IsEmpty) - { - return null; - } + private static async Task?> GetTextChangesForDocumentAsync( + Solution changedSolution, + Solution currentSolution, + DocumentId changedDocumentId, + CancellationToken cancellationToken) + { + var changedDocument = changedSolution.GetRequiredDocument(changedDocumentId); + var currentDocument = currentSolution.GetRequiredDocument(changedDocumentId); - return textDiffs; + var textDiffService = changedSolution.Services.GetRequiredService(); + // Compute changes against the current version of the document. + var textDiffs = await textDiffService.GetTextChangesAsync(currentDocument, changedDocument, cancellationToken).ConfigureAwait(false); + if (textDiffs.IsEmpty) + { + return null; } + + return textDiffs; } } diff --git a/src/EditorFeatures/Core/ExternalAccess/UnitTesting/Api/UnitTestingGlobalOptions.cs b/src/EditorFeatures/Core/ExternalAccess/UnitTesting/Api/UnitTestingGlobalOptions.cs index 3f8f15c7ad3bc..88602125ec598 100644 --- a/src/EditorFeatures/Core/ExternalAccess/UnitTesting/Api/UnitTestingGlobalOptions.cs +++ b/src/EditorFeatures/Core/ExternalAccess/UnitTesting/Api/UnitTestingGlobalOptions.cs @@ -8,16 +8,15 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Remote; -namespace Microsoft.CodeAnalysis.ExternalAccess.UnitTesting.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.UnitTesting.Api; + +[Export(typeof(UnitTestingGlobalOptions)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class UnitTestingGlobalOptions(IGlobalOptionService globalOptions) { - [Export(typeof(UnitTestingGlobalOptions)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class UnitTestingGlobalOptions(IGlobalOptionService globalOptions) - { - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IGlobalOptionService _globalOptions = globalOptions; - public bool IsServiceHubProcessCoreClr - => _globalOptions.GetOption(RemoteHostOptionsStorage.OOPCoreClr); - } + public bool IsServiceHubProcessCoreClr + => _globalOptions.GetOption(RemoteHostOptionsStorage.OOPCoreClr); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/ITypeScriptGoToDefinitionServiceFactoryImplementation.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/ITypeScriptGoToDefinitionServiceFactoryImplementation.cs index 03d7813de6eb7..c9414a0c97b50 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/ITypeScriptGoToDefinitionServiceFactoryImplementation.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/ITypeScriptGoToDefinitionServiceFactoryImplementation.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal interface ITypeScriptGoToDefinitionServiceFactoryImplementation { - internal interface ITypeScriptGoToDefinitionServiceFactoryImplementation - { - ILanguageService CreateLanguageService(HostLanguageServices languageServices); - } + ILanguageService CreateLanguageService(HostLanguageServices languageServices); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptBlockStructureServiceImplementation.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptBlockStructureServiceImplementation.cs index b4477d3f0ce6e..f5d63ad1a5ef5 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptBlockStructureServiceImplementation.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptBlockStructureServiceImplementation.cs @@ -5,10 +5,9 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal interface IVSTypeScriptBlockStructureServiceImplementation { - internal interface IVSTypeScriptBlockStructureServiceImplementation - { - Task GetBlockStructureAsync(Document document, CancellationToken cancellationToken); - } + Task GetBlockStructureAsync(Document document, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptBreakpointResolutionServiceImplementation.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptBreakpointResolutionServiceImplementation.cs index ee9f4eff397ef..6461990e306a1 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptBreakpointResolutionServiceImplementation.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptBreakpointResolutionServiceImplementation.cs @@ -7,12 +7,11 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal interface IVSTypeScriptBreakpointResolutionServiceImplementation { - internal interface IVSTypeScriptBreakpointResolutionServiceImplementation - { - Task ResolveBreakpointAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken = default); + Task ResolveBreakpointAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken = default); - Task> ResolveBreakpointsAsync(Solution solution, string name, CancellationToken cancellationToken = default); - } + Task> ResolveBreakpointsAsync(Solution solution, string name, CancellationToken cancellationToken = default); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptEditorInlineRenameService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptEditorInlineRenameService.cs index 1c3e311b1dfb5..9d8e0cfa172ff 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptEditorInlineRenameService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptEditorInlineRenameService.cs @@ -8,13 +8,12 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +/// +/// Language service that allows a language to participate in the editor's inline rename feature. +/// +internal abstract class VSTypeScriptEditorInlineRenameServiceImplementation { - /// - /// Language service that allows a language to participate in the editor's inline rename feature. - /// - internal abstract class VSTypeScriptEditorInlineRenameServiceImplementation - { - public abstract Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken); - } + public abstract Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFindUsagesContext.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFindUsagesContext.cs index f972e62f5dd53..ec6c2fda1ffa9 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFindUsagesContext.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFindUsagesContext.cs @@ -10,147 +10,149 @@ using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Navigation; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal interface IVSTypeScriptFindUsagesContext { - internal interface IVSTypeScriptFindUsagesContext - { - /// - /// Used for clients that are finding usages to push information about how far along they - /// are in their search. - /// - IVSTypeScriptStreamingProgressTracker ProgressTracker { get; } - - /// - /// Report a message to be displayed to the user. - /// - ValueTask ReportMessageAsync(string message, CancellationToken cancellationToken); - - /// - /// Set the title of the window that results are displayed in. - /// - ValueTask SetSearchTitleAsync(string title, CancellationToken cancellationToken); - - ValueTask OnDefinitionFoundAsync(VSTypeScriptDefinitionItem definition, CancellationToken cancellationToken); - ValueTask OnReferenceFoundAsync(VSTypeScriptSourceReferenceItem reference, CancellationToken cancellationToken); - - ValueTask OnCompletedAsync(CancellationToken cancellationToken); - } + /// + /// Used for clients that are finding usages to push information about how far along they + /// are in their search. + /// + IVSTypeScriptStreamingProgressTracker ProgressTracker { get; } + + /// + /// Report a message to be displayed to the user. + /// + ValueTask ReportMessageAsync(string message, CancellationToken cancellationToken); + + /// + /// Set the title of the window that results are displayed in. + /// + ValueTask SetSearchTitleAsync(string title, CancellationToken cancellationToken); + + ValueTask OnDefinitionFoundAsync(VSTypeScriptDefinitionItem definition, CancellationToken cancellationToken); + ValueTask OnReferenceFoundAsync(VSTypeScriptSourceReferenceItem reference, CancellationToken cancellationToken); + + ValueTask OnCompletedAsync(CancellationToken cancellationToken); +} - internal interface IVSTypeScriptStreamingProgressTracker - { - ValueTask AddItemsAsync(int count, CancellationToken cancellationToken); - ValueTask ItemCompletedAsync(CancellationToken cancellationToken); - } +internal interface IVSTypeScriptStreamingProgressTracker +{ + ValueTask AddItemsAsync(int count, CancellationToken cancellationToken); + ValueTask ItemCompletedAsync(CancellationToken cancellationToken); +} - internal abstract class VSTypeScriptDefinitionItemNavigator - { - public abstract Task CanNavigateToAsync(Workspace workspace, CancellationToken cancellationToken); - public abstract Task TryNavigateToAsync(Workspace workspace, bool showInPreviewTab, bool activateTab, CancellationToken cancellationToken); - } +internal abstract class VSTypeScriptDefinitionItemNavigator +{ + public abstract Task CanNavigateToAsync(Workspace workspace, CancellationToken cancellationToken); + public abstract Task TryNavigateToAsync(Workspace workspace, bool showInPreviewTab, bool activateTab, CancellationToken cancellationToken); +} - internal sealed class VSTypeScriptDefinitionItem +internal sealed class VSTypeScriptDefinitionItem +{ + private sealed class ExternalDefinitionItem(VSTypeScriptDefinitionItemNavigator navigator, ImmutableArray tags, ImmutableArray displayParts) + : DefinitionItem( + tags, + displayParts, + ImmutableArray.Empty, + sourceSpans: default, + metadataLocations: [], + classifiedSpans: default, + properties: null, + displayableProperties: ImmutableDictionary.Empty, + displayIfNoReferences: true) { - private sealed class ExternalDefinitionItem(VSTypeScriptDefinitionItemNavigator navigator, ImmutableArray tags, ImmutableArray displayParts) : DefinitionItem(tags, - displayParts, - ImmutableArray.Empty, - originationParts: default, - sourceSpans: default, - classifiedSpans: default, - properties: null, - displayIfNoReferences: true) - { - private readonly VSTypeScriptDefinitionItemNavigator _navigator = navigator; + private readonly VSTypeScriptDefinitionItemNavigator _navigator = navigator; - internal override bool IsExternal => true; + internal override bool IsExternal => true; - public override async Task GetNavigableLocationAsync(Workspace workspace, CancellationToken cancellationToken) - { - if (!await _navigator.CanNavigateToAsync(workspace, cancellationToken).ConfigureAwait(false)) - return null; + public override async Task GetNavigableLocationAsync(Workspace workspace, CancellationToken cancellationToken) + { + if (!await _navigator.CanNavigateToAsync(workspace, cancellationToken).ConfigureAwait(false)) + return null; - return new NavigableLocation((options, cancellationToken) => - _navigator.TryNavigateToAsync(workspace, options.PreferProvisionalTab, options.ActivateTab, cancellationToken)); - } + return new NavigableLocation((options, cancellationToken) => + _navigator.TryNavigateToAsync(workspace, options.PreferProvisionalTab, options.ActivateTab, cancellationToken)); } + } - internal readonly DefinitionItem UnderlyingObject; + internal readonly DefinitionItem UnderlyingObject; - internal VSTypeScriptDefinitionItem(DefinitionItem underlyingObject) - => UnderlyingObject = underlyingObject; + internal VSTypeScriptDefinitionItem(DefinitionItem underlyingObject) + => UnderlyingObject = underlyingObject; - public static VSTypeScriptDefinitionItem Create( - ImmutableArray tags, - ImmutableArray displayParts, - ImmutableArray sourceSpans, - ImmutableArray nameDisplayParts = default, - bool displayIfNoReferences = true) - { - return new(DefinitionItem.Create( - tags, displayParts, sourceSpans.SelectAsArray(span => span.ToDocumentSpan()), sourceSpans.SelectAsArray(_ => (ClassifiedSpansAndHighlightSpan?)null), - nameDisplayParts, properties: null, displayableProperties: ImmutableDictionary.Empty, displayIfNoReferences: displayIfNoReferences)); - } + public static VSTypeScriptDefinitionItem Create( + ImmutableArray tags, + ImmutableArray displayParts, + ImmutableArray sourceSpans, + ImmutableArray nameDisplayParts = default, + bool displayIfNoReferences = true) + { + return new(DefinitionItem.Create( + tags, displayParts, sourceSpans.SelectAsArray(static span => span.ToDocumentSpan()), classifiedSpans: [], + metadataLocations: [], nameDisplayParts, properties: null, displayableProperties: ImmutableDictionary.Empty, displayIfNoReferences: displayIfNoReferences)); + } - public static VSTypeScriptDefinitionItem CreateExternal( - VSTypeScriptDefinitionItemNavigator navigator, - ImmutableArray tags, - ImmutableArray displayParts) - => new(new ExternalDefinitionItem(navigator, tags, displayParts)); + public static VSTypeScriptDefinitionItem CreateExternal( + VSTypeScriptDefinitionItemNavigator navigator, + ImmutableArray tags, + ImmutableArray displayParts) + => new(new ExternalDefinitionItem(navigator, tags, displayParts)); - [Obsolete] - public static VSTypeScriptDefinitionItem Create(VSTypeScriptDefinitionItemBase item) - => new(item); + [Obsolete] + public static VSTypeScriptDefinitionItem Create(VSTypeScriptDefinitionItemBase item) + => new(item); - public ImmutableArray Tags => UnderlyingObject.Tags; - public ImmutableArray DisplayParts => UnderlyingObject.DisplayParts; + public ImmutableArray Tags => UnderlyingObject.Tags; + public ImmutableArray DisplayParts => UnderlyingObject.DisplayParts; - public ImmutableArray GetSourceSpans() - => UnderlyingObject.SourceSpans.SelectAsArray(span => new VSTypeScriptDocumentSpan(span)); + public ImmutableArray GetSourceSpans() + => UnderlyingObject.SourceSpans.SelectAsArray(span => new VSTypeScriptDocumentSpan(span)); - public async Task CanNavigateToAsync(Workspace workspace, CancellationToken cancellationToken) - => await UnderlyingObject.GetNavigableLocationAsync(workspace, cancellationToken).ConfigureAwait(false) != null; + public async Task CanNavigateToAsync(Workspace workspace, CancellationToken cancellationToken) + => await UnderlyingObject.GetNavigableLocationAsync(workspace, cancellationToken).ConfigureAwait(false) != null; - public async Task TryNavigateToAsync(Workspace workspace, bool showInPreviewTab, bool activateTab, CancellationToken cancellationToken) - { - var location = await UnderlyingObject.GetNavigableLocationAsync(workspace, cancellationToken).ConfigureAwait(false); - return location != null && - await location.NavigateToAsync(new NavigationOptions(showInPreviewTab, activateTab), cancellationToken).ConfigureAwait(false); - } + public async Task TryNavigateToAsync(Workspace workspace, bool showInPreviewTab, bool activateTab, CancellationToken cancellationToken) + { + var location = await UnderlyingObject.GetNavigableLocationAsync(workspace, cancellationToken).ConfigureAwait(false); + return location != null && + await location.NavigateToAsync(new NavigationOptions(showInPreviewTab, activateTab), cancellationToken).ConfigureAwait(false); } +} - internal sealed class VSTypeScriptSourceReferenceItem( - VSTypeScriptDefinitionItem definition, - VSTypeScriptDocumentSpan sourceSpan, - VSTypeScriptSymbolUsageInfo symbolUsageInfo) - { - internal readonly SourceReferenceItem UnderlyingObject = new SourceReferenceItem( - definition.UnderlyingObject, sourceSpan.ToDocumentSpan(), classifiedSpans: null, symbolUsageInfo.UnderlyingObject); +internal sealed class VSTypeScriptSourceReferenceItem( + VSTypeScriptDefinitionItem definition, + VSTypeScriptDocumentSpan sourceSpan, + VSTypeScriptSymbolUsageInfo symbolUsageInfo) +{ + internal readonly SourceReferenceItem UnderlyingObject = new SourceReferenceItem( + definition.UnderlyingObject, sourceSpan.ToDocumentSpan(), classifiedSpans: null, symbolUsageInfo.UnderlyingObject); - public VSTypeScriptDocumentSpan GetSourceSpan() - => new(UnderlyingObject.SourceSpan); - } + public VSTypeScriptDocumentSpan GetSourceSpan() + => new(UnderlyingObject.SourceSpan); +} - internal readonly struct VSTypeScriptSymbolUsageInfo - { - internal readonly SymbolUsageInfo UnderlyingObject; +internal readonly struct VSTypeScriptSymbolUsageInfo +{ + internal readonly SymbolUsageInfo UnderlyingObject; - private VSTypeScriptSymbolUsageInfo(SymbolUsageInfo underlyingObject) - => UnderlyingObject = underlyingObject; + private VSTypeScriptSymbolUsageInfo(SymbolUsageInfo underlyingObject) + => UnderlyingObject = underlyingObject; - public static VSTypeScriptSymbolUsageInfo Create(VSTypeScriptValueUsageInfo valueUsageInfo) - => new(SymbolUsageInfo.Create((ValueUsageInfo)valueUsageInfo)); - } + public static VSTypeScriptSymbolUsageInfo Create(VSTypeScriptValueUsageInfo valueUsageInfo) + => new(SymbolUsageInfo.Create((ValueUsageInfo)valueUsageInfo)); +} - [Flags] - internal enum VSTypeScriptValueUsageInfo - { - None = ValueUsageInfo.None, - Read = ValueUsageInfo.Read, - Write = ValueUsageInfo.Write, - Reference = ValueUsageInfo.Reference, - Name = ValueUsageInfo.Name, - ReadWrite = ValueUsageInfo.ReadWrite, - ReadableReference = ValueUsageInfo.ReadableReference, - WritableReference = ValueUsageInfo.WritableReference, - ReadableWritableReference = ValueUsageInfo.ReadableWritableReference - } +[Flags] +internal enum VSTypeScriptValueUsageInfo +{ + None = ValueUsageInfo.None, + Read = ValueUsageInfo.Read, + Write = ValueUsageInfo.Write, + Reference = ValueUsageInfo.Reference, + Name = ValueUsageInfo.Name, + ReadWrite = ValueUsageInfo.ReadWrite, + ReadableReference = ValueUsageInfo.ReadableReference, + WritableReference = ValueUsageInfo.WritableReference, + ReadableWritableReference = ValueUsageInfo.ReadableWritableReference } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFindUsagesService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFindUsagesService.cs index d18d021097cbe..88e3b8737d507 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFindUsagesService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFindUsagesService.cs @@ -6,20 +6,19 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal interface IVSTypeScriptFindUsagesService : ILanguageService { - internal interface IVSTypeScriptFindUsagesService : ILanguageService - { - /// - /// Finds the references for the symbol at the specific position in the document, - /// pushing the results into the context instance. - /// - Task FindReferencesAsync(Document document, int position, IVSTypeScriptFindUsagesContext context, CancellationToken cancellationToken); + /// + /// Finds the references for the symbol at the specific position in the document, + /// pushing the results into the context instance. + /// + Task FindReferencesAsync(Document document, int position, IVSTypeScriptFindUsagesContext context, CancellationToken cancellationToken); - /// - /// Finds the implementations for the symbol at the specific position in the document, - /// pushing the results into the context instance. - /// - Task FindImplementationsAsync(Document document, int position, IVSTypeScriptFindUsagesContext context, CancellationToken cancellationToken); - } + /// + /// Finds the implementations for the symbol at the specific position in the document, + /// pushing the results into the context instance. + /// + Task FindImplementationsAsync(Document document, int position, IVSTypeScriptFindUsagesContext context, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFormattingInteractionService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFormattingInteractionService.cs index ed037254993fa..1fc37d5e61247 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFormattingInteractionService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptFormattingInteractionService.cs @@ -10,44 +10,43 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal interface IVSTypeScriptFormattingInteractionService { - internal interface IVSTypeScriptFormattingInteractionService - { - bool SupportsFormatDocument { get; } - bool SupportsFormatSelection { get; } - bool SupportsFormatOnPaste { get; } - bool SupportsFormatOnReturn { get; } - - /// - /// True if this service would like to format the document based on the user typing the - /// provided character. - /// - bool SupportsFormattingOnTypedCharacter(Document document, char ch); + bool SupportsFormatDocument { get; } + bool SupportsFormatSelection { get; } + bool SupportsFormatOnPaste { get; } + bool SupportsFormatOnReturn { get; } + + /// + /// True if this service would like to format the document based on the user typing the + /// provided character. + /// + bool SupportsFormattingOnTypedCharacter(Document document, char ch); #pragma warning disable RS0030 // Do not used banned APIs (backwards compat) - /// - /// Returns the text changes necessary to format the document. If "textSpan" is provided, - /// only the text changes necessary to format that span are needed. - /// - Task> GetFormattingChangesAsync(Document document, TextSpan? textSpan, DocumentOptionSet? documentOptions, CancellationToken cancellationToken); - - /// - /// Returns the text changes necessary to format the document on paste operation. - /// - Task> GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, DocumentOptionSet? documentOptions, CancellationToken cancellationToken); - - /// - /// Returns the text changes necessary to format the document after the user enters a - /// character. The position provided is the position of the caret in the document after - /// the character been inserted into the document. - /// - Task> GetFormattingChangesAsync(Document document, char typedChar, int position, DocumentOptionSet? documentOptions, CancellationToken cancellationToken); - - /// - /// Returns the text changes necessary to format the document after the user enters a Return - /// The position provided is the position of the caret in the document after Return. - Task> GetFormattingChangesOnReturnAsync(Document document, int position, DocumentOptionSet? documentOptions, CancellationToken cancellationToken); + /// + /// Returns the text changes necessary to format the document. If "textSpan" is provided, + /// only the text changes necessary to format that span are needed. + /// + Task> GetFormattingChangesAsync(Document document, TextSpan? textSpan, DocumentOptionSet? documentOptions, CancellationToken cancellationToken); + + /// + /// Returns the text changes necessary to format the document on paste operation. + /// + Task> GetFormattingChangesOnPasteAsync(Document document, TextSpan textSpan, DocumentOptionSet? documentOptions, CancellationToken cancellationToken); + + /// + /// Returns the text changes necessary to format the document after the user enters a + /// character. The position provided is the position of the caret in the document after + /// the character been inserted into the document. + /// + Task> GetFormattingChangesAsync(Document document, char typedChar, int position, DocumentOptionSet? documentOptions, CancellationToken cancellationToken); + + /// + /// Returns the text changes necessary to format the document after the user enters a Return + /// The position provided is the position of the caret in the document after Return. + Task> GetFormattingChangesOnReturnAsync(Document document, int position, DocumentOptionSet? documentOptions, CancellationToken cancellationToken); #pragma warning restore - } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptInlineRenameLocationSet.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptInlineRenameLocationSet.cs index 864646d3598c8..bcbf5bb854d50 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptInlineRenameLocationSet.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptInlineRenameLocationSet.cs @@ -13,29 +13,28 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Rename; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal abstract class VSTypeScriptInlineRenameLocationSet : IInlineRenameLocationSet { - internal abstract class VSTypeScriptInlineRenameLocationSet : IInlineRenameLocationSet - { - /// - /// The set of locations that need to be updated with the replacement text that the user - /// has entered in the inline rename session. These are the locations are all relative - /// to the solution when the inline rename session began. - /// - public abstract IList Locations { get; } + /// + /// The set of locations that need to be updated with the replacement text that the user + /// has entered in the inline rename session. These are the locations are all relative + /// to the solution when the inline rename session began. + /// + public abstract IList Locations { get; } - IList IInlineRenameLocationSet.Locations - => Locations?.Select(x => new InlineRenameLocation(x.Document, x.TextSpan)).ToList(); + IList IInlineRenameLocationSet.Locations + => Locations?.Select(x => new InlineRenameLocation(x.Document, x.TextSpan)).ToList(); - /// - /// Returns the set of replacements and their possible resolutions if the user enters the - /// provided replacement text and options. Replacements are keyed by their document id - /// and TextSpan in the original solution, and specify their new span and possible conflict - /// resolution. - /// - public abstract Task GetReplacementsAsync(string replacementText, CancellationToken cancellationToken); + /// + /// Returns the set of replacements and their possible resolutions if the user enters the + /// provided replacement text and options. Replacements are keyed by their document id + /// and TextSpan in the original solution, and specify their new span and possible conflict + /// resolution. + /// + public abstract Task GetReplacementsAsync(string replacementText, CancellationToken cancellationToken); - async Task IInlineRenameLocationSet.GetReplacementsAsync(string replacementText, SymbolRenameOptions options, CancellationToken cancellationToken) - => await GetReplacementsAsync(replacementText, cancellationToken).ConfigureAwait(false); - } + async Task IInlineRenameLocationSet.GetReplacementsAsync(string replacementText, SymbolRenameOptions options, CancellationToken cancellationToken) + => await GetReplacementsAsync(replacementText, cancellationToken).ConfigureAwait(false); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptInlineRenameReplacementInfo.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptInlineRenameReplacementInfo.cs index 28b3367c75a73..73dafc8aa5173 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptInlineRenameReplacementInfo.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptInlineRenameReplacementInfo.cs @@ -9,31 +9,30 @@ using System.Linq; using Microsoft.CodeAnalysis.Editor; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal abstract class VSTypeScriptInlineRenameReplacementInfo : IInlineRenameReplacementInfo { - internal abstract class VSTypeScriptInlineRenameReplacementInfo : IInlineRenameReplacementInfo - { - /// - /// The solution obtained after resolving all conflicts. - /// - public abstract Solution NewSolution { get; } - - /// - /// Whether or not the replacement text entered by the user is valid. - /// - public abstract bool ReplacementTextValid { get; } - - /// - /// The documents that need to be updated. - /// - public abstract IEnumerable DocumentIds { get; } - - /// - /// Returns all the replacements that need to be performed for the specified document. - /// - public abstract IEnumerable GetReplacements(DocumentId documentId); - - IEnumerable IInlineRenameReplacementInfo.GetReplacements(DocumentId documentId) - => GetReplacements(documentId).Select(r => r.UnderlyingObject); - } + /// + /// The solution obtained after resolving all conflicts. + /// + public abstract Solution NewSolution { get; } + + /// + /// Whether or not the replacement text entered by the user is valid. + /// + public abstract bool ReplacementTextValid { get; } + + /// + /// The documents that need to be updated. + /// + public abstract IEnumerable DocumentIds { get; } + + /// + /// Returns all the replacements that need to be performed for the specified document. + /// + public abstract IEnumerable GetReplacements(DocumentId documentId); + + IEnumerable IInlineRenameReplacementInfo.GetReplacements(DocumentId documentId) + => GetReplacements(documentId).Select(r => r.UnderlyingObject); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptLanguageDebugInfoServiceImplementation.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptLanguageDebugInfoServiceImplementation.cs index cf9a5efbe3864..9ad5ccf90be31 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptLanguageDebugInfoServiceImplementation.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptLanguageDebugInfoServiceImplementation.cs @@ -5,18 +5,17 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal interface IVSTypeScriptLanguageDebugInfoServiceImplementation { - internal interface IVSTypeScriptLanguageDebugInfoServiceImplementation - { - Task GetLocationInfoAsync(Document document, int position, CancellationToken cancellationToken); + Task GetLocationInfoAsync(Document document, int position, CancellationToken cancellationToken); - /// - /// Find an appropriate span to pass the debugger given a point in a snapshot. Optionally - /// pass back a string to pass to the debugger instead if no good span can be found. For - /// example, if the user hovers on "var" then we actually want to pass the fully qualified - /// name of the type that 'var' binds to, to the debugger. - /// - Task GetDataTipInfoAsync(Document document, int position, CancellationToken cancellationToken); - } + /// + /// Find an appropriate span to pass the debugger given a point in a snapshot. Optionally + /// pass back a string to pass to the debugger instead if no good span can be found. For + /// example, if the user hovers on "var" then we actually want to pass the fully qualified + /// name of the type that 'var' binds to, to the debugger. + /// + Task GetDataTipInfoAsync(Document document, int position, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptNavigationBarItemService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptNavigationBarItemService.cs index 718dda05781d5..8c1e22e838b10 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptNavigationBarItemService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptNavigationBarItemService.cs @@ -6,10 +6,9 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal interface IVSTypeScriptNavigationBarItemService { - internal interface IVSTypeScriptNavigationBarItemService - { - Task> GetItemsAsync(Document document, CancellationToken cancellationToken); - } + Task> GetItemsAsync(Document document, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptSignatureHelpClassifierProvider.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptSignatureHelpClassifierProvider.cs index 4bbb79862fcd0..f891572589a12 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptSignatureHelpClassifierProvider.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptSignatureHelpClassifierProvider.cs @@ -6,10 +6,9 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Classification; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal interface IVSTypeScriptSignatureHelpClassifierProvider { - internal interface IVSTypeScriptSignatureHelpClassifierProvider - { - IClassifier Create(ITextBuffer textBuffer, ClassificationTypeMap typeMap); - } + IClassifier Create(ITextBuffer textBuffer, ClassificationTypeMap typeMap); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptStreamingFindUsagesPresenterAccessor.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptStreamingFindUsagesPresenterAccessor.cs index ab3b09da58289..2b1fad1d0a3e3 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptStreamingFindUsagesPresenterAccessor.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/IVSTypeScriptStreamingFindUsagesPresenterAccessor.cs @@ -4,13 +4,12 @@ using System.Threading; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal interface IVSTypeScriptStreamingFindUsagesPresenterAccessor { - internal interface IVSTypeScriptStreamingFindUsagesPresenterAccessor - { - (IVSTypeScriptFindUsagesContext context, CancellationToken cancellationToken) StartSearch( - string title, bool supportsReferences); + (IVSTypeScriptFindUsagesContext context, CancellationToken cancellationToken) StartSearch( + string title, bool supportsReferences); - void ClearAll(); - } + void ClearAll(); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptAsynchronousTaggerProvider.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptAsynchronousTaggerProvider.cs index f9ceeda2cada9..6b497196a91f4 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptAsynchronousTaggerProvider.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptAsynchronousTaggerProvider.cs @@ -9,27 +9,26 @@ using Microsoft.CodeAnalysis.Workspaces; using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal abstract class VSTypeScriptAsynchronousTaggerProvider : AsynchronousViewTaggerProvider + where TTag : ITag { - internal abstract class VSTypeScriptAsynchronousTaggerProvider : AsynchronousViewTaggerProvider - where TTag : ITag + [Obsolete("Use constructor that takes ITextBufferVisibilityTracker. Use `[Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker`")] + protected VSTypeScriptAsynchronousTaggerProvider( + IThreadingContext threadingContext, + IAsynchronousOperationListenerProvider asyncListenerProvider, + VSTypeScriptGlobalOptions globalOptions) + : base(threadingContext, globalOptions.Service, visibilityTracker: null, asyncListenerProvider.GetListener(FeatureAttribute.Classification)) { - [Obsolete("Use constructor that takes ITextBufferVisibilityTracker. Use `[Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker`")] - protected VSTypeScriptAsynchronousTaggerProvider( - IThreadingContext threadingContext, - IAsynchronousOperationListenerProvider asyncListenerProvider, - VSTypeScriptGlobalOptions globalOptions) - : base(threadingContext, globalOptions.Service, visibilityTracker: null, asyncListenerProvider.GetListener(FeatureAttribute.Classification)) - { - } + } - protected VSTypeScriptAsynchronousTaggerProvider( - IThreadingContext threadingContext, - VSTypeScriptGlobalOptions globalOptions, - ITextBufferVisibilityTracker? visibilityTracker, - IAsynchronousOperationListenerProvider asyncListenerProvider) - : base(threadingContext, globalOptions.Service, visibilityTracker, asyncListenerProvider.GetListener(FeatureAttribute.Classification)) - { - } + protected VSTypeScriptAsynchronousTaggerProvider( + IThreadingContext threadingContext, + VSTypeScriptGlobalOptions globalOptions, + ITextBufferVisibilityTracker? visibilityTracker, + IAsynchronousOperationListenerProvider asyncListenerProvider) + : base(threadingContext, globalOptions.Service, visibilityTracker, asyncListenerProvider.GetListener(FeatureAttribute.Classification)) + { } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBlockSpan.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBlockSpan.cs index b0a4ed7915ea8..4d99c5e1e7a3e 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBlockSpan.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBlockSpan.cs @@ -4,19 +4,18 @@ using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal readonly struct VSTypeScriptBlockSpan( + string? type, bool isCollapsible, TextSpan textSpan, TextSpan hintSpan, string bannerText = VSTypeScriptBlockSpan.Ellipses, bool autoCollapse = false, bool isDefaultCollapsed = false) { - internal readonly struct VSTypeScriptBlockSpan( - string? type, bool isCollapsible, TextSpan textSpan, TextSpan hintSpan, string bannerText = VSTypeScriptBlockSpan.Ellipses, bool autoCollapse = false, bool isDefaultCollapsed = false) - { - private const string Ellipses = "..."; + private const string Ellipses = "..."; - public bool IsCollapsible { get; } = isCollapsible; - public TextSpan TextSpan { get; } = textSpan; - public TextSpan HintSpan { get; } = hintSpan; - public string BannerText { get; } = bannerText; - public bool AutoCollapse { get; } = autoCollapse; - public bool IsDefaultCollapsed { get; } = isDefaultCollapsed; - public string? Type { get; } = type; - } + public bool IsCollapsible { get; } = isCollapsible; + public TextSpan TextSpan { get; } = textSpan; + public TextSpan HintSpan { get; } = hintSpan; + public string BannerText { get; } = bannerText; + public bool AutoCollapse { get; } = autoCollapse; + public bool IsDefaultCollapsed { get; } = isDefaultCollapsed; + public string? Type { get; } = type; } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBlockStructure.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBlockStructure.cs index 841ed1f74abef..eece0f9ea2eea 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBlockStructure.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBlockStructure.cs @@ -4,10 +4,9 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal readonly struct VSTypeScriptBlockStructure(ImmutableArray spans) { - internal readonly struct VSTypeScriptBlockStructure(ImmutableArray spans) - { - public ImmutableArray Spans { get; } = spans; - } + public ImmutableArray Spans { get; } = spans; } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBreakpointResolutionResultWrapper.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBreakpointResolutionResultWrapper.cs index a936c3b920b32..565be68562bfe 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBreakpointResolutionResultWrapper.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptBreakpointResolutionResultWrapper.cs @@ -5,24 +5,23 @@ using Microsoft.CodeAnalysis.Debugging; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal readonly struct VSTypeScriptBreakpointResolutionResultWrapper { - internal readonly struct VSTypeScriptBreakpointResolutionResultWrapper - { - internal readonly BreakpointResolutionResult UnderlyingObject; + internal readonly BreakpointResolutionResult UnderlyingObject; - private VSTypeScriptBreakpointResolutionResultWrapper(BreakpointResolutionResult result) - => UnderlyingObject = result; + private VSTypeScriptBreakpointResolutionResultWrapper(BreakpointResolutionResult result) + => UnderlyingObject = result; - public Document Document => UnderlyingObject.Document; - public TextSpan TextSpan => UnderlyingObject.TextSpan; - public string? LocationNameOpt => UnderlyingObject.LocationNameOpt; - public bool IsLineBreakpoint => UnderlyingObject.IsLineBreakpoint; + public Document Document => UnderlyingObject.Document; + public TextSpan TextSpan => UnderlyingObject.TextSpan; + public string? LocationNameOpt => UnderlyingObject.LocationNameOpt; + public bool IsLineBreakpoint => UnderlyingObject.IsLineBreakpoint; - public static VSTypeScriptBreakpointResolutionResultWrapper CreateSpanResult(Document document, TextSpan textSpan, string? locationNameOpt = null) - => new(BreakpointResolutionResult.CreateSpanResult(document, textSpan, locationNameOpt)); + public static VSTypeScriptBreakpointResolutionResultWrapper CreateSpanResult(Document document, TextSpan textSpan, string? locationNameOpt = null) + => new(BreakpointResolutionResult.CreateSpanResult(document, textSpan, locationNameOpt)); - public static VSTypeScriptBreakpointResolutionResultWrapper CreateLineResult(Document document, string? locationNameOpt = null) - => new(BreakpointResolutionResult.CreateLineResult(document, locationNameOpt)); - } + public static VSTypeScriptBreakpointResolutionResultWrapper CreateLineResult(Document document, string? locationNameOpt = null) + => new(BreakpointResolutionResult.CreateLineResult(document, locationNameOpt)); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDebugDataTipInfoWrapper.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDebugDataTipInfoWrapper.cs index 3b3eff7163e6c..f1b4c228b9337 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDebugDataTipInfoWrapper.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDebugDataTipInfoWrapper.cs @@ -5,14 +5,13 @@ using Microsoft.CodeAnalysis.Debugging; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal readonly struct VSTypeScriptDebugDataTipInfoWrapper(TextSpan span, string text) { - internal readonly struct VSTypeScriptDebugDataTipInfoWrapper(TextSpan span, string text) - { - internal readonly DebugDataTipInfo UnderlyingObject = new DebugDataTipInfo(span, text); + internal readonly DebugDataTipInfo UnderlyingObject = new DebugDataTipInfo(span, text); - public readonly TextSpan Span => UnderlyingObject.Span; - public readonly string Text => UnderlyingObject.Text; - public bool IsDefault => UnderlyingObject.IsDefault; - } + public readonly TextSpan Span => UnderlyingObject.Span; + public readonly string Text => UnderlyingObject.Text; + public bool IsDefault => UnderlyingObject.IsDefault; } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDebugLocationInfoWrapper.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDebugLocationInfoWrapper.cs index f11aa9f0b4dd0..6d7a280f8f162 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDebugLocationInfoWrapper.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDebugLocationInfoWrapper.cs @@ -4,14 +4,13 @@ using Microsoft.CodeAnalysis.Debugging; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal readonly struct VSTypeScriptDebugLocationInfoWrapper(string name, int lineOffset) { - internal readonly struct VSTypeScriptDebugLocationInfoWrapper(string name, int lineOffset) - { - internal readonly DebugLocationInfo UnderlyingObject = new DebugLocationInfo(name, lineOffset); + internal readonly DebugLocationInfo UnderlyingObject = new DebugLocationInfo(name, lineOffset); - public readonly string Name => UnderlyingObject.Name; - public readonly int LineOffset => UnderlyingObject.LineOffset; - internal bool IsDefault => UnderlyingObject.IsDefault; - } + public readonly string Name => UnderlyingObject.Name; + public readonly int LineOffset => UnderlyingObject.LineOffset; + internal bool IsDefault => UnderlyingObject.IsDefault; } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDocumentSpan.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDocumentSpan.cs index 92d1d50471109..bb66de14574dc 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDocumentSpan.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptDocumentSpan.cs @@ -6,19 +6,18 @@ using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api -{ - internal readonly struct VSTypeScriptDocumentSpan(Document document, TextSpan sourceSpan) - { - public Document Document { get; } = document; - public TextSpan SourceSpan { get; } = sourceSpan; +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; - internal VSTypeScriptDocumentSpan(DocumentSpan span) - : this(span.Document, span.SourceSpan) - { - } +internal readonly struct VSTypeScriptDocumentSpan(Document document, TextSpan sourceSpan) +{ + public Document Document { get; } = document; + public TextSpan SourceSpan { get; } = sourceSpan; - internal DocumentSpan ToDocumentSpan() - => new(Document, SourceSpan); + internal VSTypeScriptDocumentSpan(DocumentSpan span) + : this(span.Document, span.SourceSpan) + { } + + internal DocumentSpan ToDocumentSpan() + => new(Document, SourceSpan); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptExtensions.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptExtensions.cs index abde290a59291..2dcaca631cf4a 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptExtensions.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptExtensions.cs @@ -9,32 +9,31 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal static class VSTypeScriptExtensions { - internal static class VSTypeScriptExtensions - { - public static void ApplyTextChanges(this Workspace workspace, DocumentId id, IEnumerable textChanges, CancellationToken cancellationToken) - => Editor.Shared.Extensions.IWorkspaceExtensions.ApplyTextChanges(workspace, id, textChanges, cancellationToken); + public static void ApplyTextChanges(this Workspace workspace, DocumentId id, IEnumerable textChanges, CancellationToken cancellationToken) + => Editor.Shared.Extensions.IWorkspaceExtensions.ApplyTextChanges(workspace, id, textChanges, cancellationToken); - public static SnapshotPoint? GetCaretPoint(this ITextView textView, ITextBuffer subjectBuffer) - => Editor.Shared.Extensions.ITextViewExtensions.GetCaretPoint(textView, subjectBuffer); + public static SnapshotPoint? GetCaretPoint(this ITextView textView, ITextBuffer subjectBuffer) + => Editor.Shared.Extensions.ITextViewExtensions.GetCaretPoint(textView, subjectBuffer); - public static bool TryMoveCaretToAndEnsureVisible(this ITextView textView, SnapshotPoint point) - => Editor.Shared.Extensions.ITextViewExtensions.TryMoveCaretToAndEnsureVisible(textView, point); + public static bool TryMoveCaretToAndEnsureVisible(this ITextView textView, SnapshotPoint point) + => Editor.Shared.Extensions.ITextViewExtensions.TryMoveCaretToAndEnsureVisible(textView, point); - public static SnapshotPoint? GetCaretPoint(this ITextView textView, Predicate match) - => Editor.Shared.Extensions.ITextViewExtensions.GetCaretPoint(textView, match); + public static SnapshotPoint? GetCaretPoint(this ITextView textView, Predicate match) + => Editor.Shared.Extensions.ITextViewExtensions.GetCaretPoint(textView, match); - public static bool TryMoveCaretToAndEnsureVisible(this ITextView textView, VirtualSnapshotPoint point) - => Editor.Shared.Extensions.ITextViewExtensions.TryMoveCaretToAndEnsureVisible(textView, point); + public static bool TryMoveCaretToAndEnsureVisible(this ITextView textView, VirtualSnapshotPoint point) + => Editor.Shared.Extensions.ITextViewExtensions.TryMoveCaretToAndEnsureVisible(textView, point); - public static SnapshotSpan? TryGetSpan(this ITextSnapshot snapshot, int startLine, int startIndex, int endLine, int endIndex) - => Text.Shared.Extensions.ITextSnapshotExtensions.TryGetSpan(snapshot, startLine, startIndex, endLine, endIndex); + public static SnapshotSpan? TryGetSpan(this ITextSnapshot snapshot, int startLine, int startIndex, int endLine, int endIndex) + => Text.Shared.Extensions.ITextSnapshotExtensions.TryGetSpan(snapshot, startLine, startIndex, endLine, endIndex); - public static void GetLineAndCharacter(this ITextSnapshot snapshot, int position, out int lineNumber, out int characterIndex) - => Text.Shared.Extensions.ITextSnapshotExtensions.GetLineAndCharacter(snapshot, position, out lineNumber, out characterIndex); + public static void GetLineAndCharacter(this ITextSnapshot snapshot, int position, out int lineNumber, out int characterIndex) + => Text.Shared.Extensions.ITextSnapshotExtensions.GetLineAndCharacter(snapshot, position, out lineNumber, out characterIndex); - public static int GetColumnFromLineOffset(this string line, int endPosition, int tabSize) - => Shared.Extensions.StringExtensions.GetColumnFromLineOffset(line, endPosition, tabSize); - } + public static int GetColumnFromLineOffset(this string line, int endPosition, int tabSize) + => Shared.Extensions.StringExtensions.GetColumnFromLineOffset(line, endPosition, tabSize); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptGlobalOptions.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptGlobalOptions.cs index 0724191605c3b..e80b36e4cea71 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptGlobalOptions.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptGlobalOptions.cs @@ -9,36 +9,35 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.SolutionCrawler; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +[Export(typeof(VSTypeScriptGlobalOptions)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VSTypeScriptGlobalOptions(IGlobalOptionService globalOptions) { - [Export(typeof(VSTypeScriptGlobalOptions)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class VSTypeScriptGlobalOptions(IGlobalOptionService globalOptions) - { - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IGlobalOptionService _globalOptions = globalOptions; - public bool BlockForCompletionItems - { - get => _globalOptions.GetOption(CompletionViewOptionsStorage.BlockForCompletionItems, InternalLanguageNames.TypeScript); - set => _globalOptions.SetGlobalOption(CompletionViewOptionsStorage.BlockForCompletionItems, InternalLanguageNames.TypeScript, value); - } + public bool BlockForCompletionItems + { + get => _globalOptions.GetOption(CompletionViewOptionsStorage.BlockForCompletionItems, InternalLanguageNames.TypeScript); + set => _globalOptions.SetGlobalOption(CompletionViewOptionsStorage.BlockForCompletionItems, InternalLanguageNames.TypeScript, value); + } - public void SetBackgroundAnalysisScope(bool openFilesOnly) - { - _globalOptions.SetGlobalOption(SolutionCrawlerOptionsStorage.BackgroundAnalysisScopeOption, InternalLanguageNames.TypeScript, - openFilesOnly ? BackgroundAnalysisScope.OpenFiles : BackgroundAnalysisScope.FullSolution); + public void SetBackgroundAnalysisScope(bool openFilesOnly) + { + _globalOptions.SetGlobalOption(SolutionCrawlerOptionsStorage.BackgroundAnalysisScopeOption, InternalLanguageNames.TypeScript, + openFilesOnly ? BackgroundAnalysisScope.OpenFiles : BackgroundAnalysisScope.FullSolution); - _globalOptions.SetGlobalOption(SolutionCrawlerOptionsStorage.RemoveDocumentDiagnosticsOnDocumentClose, InternalLanguageNames.TypeScript, - openFilesOnly); - } + _globalOptions.SetGlobalOption(SolutionCrawlerOptionsStorage.RemoveDocumentDiagnosticsOnDocumentClose, InternalLanguageNames.TypeScript, + openFilesOnly); + } #pragma warning disable IDE0060 // Remove unused parameter - [Obsolete("Do not pass workspace")] - public void SetBackgroundAnalysisScope(Workspace workspace, bool openFilesOnly) - => SetBackgroundAnalysisScope(openFilesOnly); + [Obsolete("Do not pass workspace")] + public void SetBackgroundAnalysisScope(Workspace workspace, bool openFilesOnly) + => SetBackgroundAnalysisScope(openFilesOnly); #pragma warning restore - internal IGlobalOptionService Service => _globalOptions; - } + internal IGlobalOptionService Service => _globalOptions; } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptGoToSymbolContext.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptGoToSymbolContext.cs index 5a1784f8c331e..5cc14efca8319 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptGoToSymbolContext.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptGoToSymbolContext.cs @@ -6,29 +6,28 @@ using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal sealed class VSTypeScriptGoToSymbolContext { - internal sealed class VSTypeScriptGoToSymbolContext - { - internal DefinitionItem? DefinitionItem; + internal DefinitionItem? DefinitionItem; - internal VSTypeScriptGoToSymbolContext(Document document, int position, CancellationToken cancellationToken) - { - Document = document; - Position = position; - CancellationToken = cancellationToken; - } + internal VSTypeScriptGoToSymbolContext(Document document, int position, CancellationToken cancellationToken) + { + Document = document; + Position = position; + CancellationToken = cancellationToken; + } - public Document Document { get; } - public int Position { get; } - public CancellationToken CancellationToken { get; } + public Document Document { get; } + public int Position { get; } + public CancellationToken CancellationToken { get; } - public TextSpan Span { get; set; } + public TextSpan Span { get; set; } - public void AddItem(string key, VSTypeScriptDefinitionItem item) - { - _ = key; - this.DefinitionItem = item.UnderlyingObject; - } + public void AddItem(string key, VSTypeScriptDefinitionItem item) + { + _ = key; + this.DefinitionItem = item.UnderlyingObject; } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameInfo.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameInfo.cs index 4aaa1b227d345..fad8b2be82672 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameInfo.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameInfo.cs @@ -14,51 +14,50 @@ using Microsoft.CodeAnalysis.Rename; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api -{ - internal abstract class VSTypeScriptInlineRenameInfo : IInlineRenameInfo - { - public abstract bool CanRename { get; } - public abstract string DisplayName { get; } - public abstract string FullDisplayName { get; } - public abstract VSTypeScriptGlyph Glyph { get; } - public abstract bool HasOverloads { get; } - public abstract bool ForceRenameOverloads { get; } - public abstract string LocalizedErrorMessage { get; } - public abstract TextSpan TriggerSpan { get; } - public abstract ImmutableArray DefinitionLocations { get; } - public abstract Task FindRenameLocationsAsync(bool renameInStrings, bool renameInComments, CancellationToken cancellationToken); - public abstract TextSpan? GetConflictEditSpan(VSTypeScriptInlineRenameLocationWrapper location, string replacementText, CancellationToken cancellationToken); - public abstract string GetFinalSymbolName(string replacementText); - public abstract TextSpan GetReferenceEditSpan(VSTypeScriptInlineRenameLocationWrapper location, CancellationToken cancellationToken); - - bool IInlineRenameInfo.MustRenameOverloads - => ForceRenameOverloads; - - Glyph IInlineRenameInfo.Glyph - => VSTypeScriptGlyphHelpers.ConvertTo(Glyph); - - ImmutableArray IInlineRenameInfo.DefinitionLocations - => DefinitionLocations.SelectAsArray(l => new DocumentSpan(l.Document, l.SourceSpan)); - - async Task IInlineRenameInfo.FindRenameLocationsAsync(SymbolRenameOptions options, CancellationToken cancellationToken) - => await FindRenameLocationsAsync(options.RenameInStrings, options.RenameInComments, cancellationToken).ConfigureAwait(false); +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; - TextSpan? IInlineRenameInfo.GetConflictEditSpan(InlineRenameLocation location, string triggerText, string replacementText, CancellationToken cancellationToken) - => GetConflictEditSpan(new VSTypeScriptInlineRenameLocationWrapper( - new InlineRenameLocation(location.Document, location.TextSpan)), replacementText, cancellationToken); - - TextSpan IInlineRenameInfo.GetReferenceEditSpan(InlineRenameLocation location, string triggerText, CancellationToken cancellationToken) - => GetReferenceEditSpan(new VSTypeScriptInlineRenameLocationWrapper( - new InlineRenameLocation(location.Document, location.TextSpan)), cancellationToken); - - bool IInlineRenameInfo.TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) - => true; - - bool IInlineRenameInfo.TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) - => true; - - public InlineRenameFileRenameInfo GetFileRenameInfo() - => InlineRenameFileRenameInfo.NotAllowed; - } +internal abstract class VSTypeScriptInlineRenameInfo : IInlineRenameInfo +{ + public abstract bool CanRename { get; } + public abstract string DisplayName { get; } + public abstract string FullDisplayName { get; } + public abstract VSTypeScriptGlyph Glyph { get; } + public abstract bool HasOverloads { get; } + public abstract bool ForceRenameOverloads { get; } + public abstract string LocalizedErrorMessage { get; } + public abstract TextSpan TriggerSpan { get; } + public abstract ImmutableArray DefinitionLocations { get; } + public abstract Task FindRenameLocationsAsync(bool renameInStrings, bool renameInComments, CancellationToken cancellationToken); + public abstract TextSpan? GetConflictEditSpan(VSTypeScriptInlineRenameLocationWrapper location, string replacementText, CancellationToken cancellationToken); + public abstract string GetFinalSymbolName(string replacementText); + public abstract TextSpan GetReferenceEditSpan(VSTypeScriptInlineRenameLocationWrapper location, CancellationToken cancellationToken); + + bool IInlineRenameInfo.MustRenameOverloads + => ForceRenameOverloads; + + Glyph IInlineRenameInfo.Glyph + => VSTypeScriptGlyphHelpers.ConvertTo(Glyph); + + ImmutableArray IInlineRenameInfo.DefinitionLocations + => DefinitionLocations.SelectAsArray(l => new DocumentSpan(l.Document, l.SourceSpan)); + + async Task IInlineRenameInfo.FindRenameLocationsAsync(SymbolRenameOptions options, CancellationToken cancellationToken) + => await FindRenameLocationsAsync(options.RenameInStrings, options.RenameInComments, cancellationToken).ConfigureAwait(false); + + TextSpan? IInlineRenameInfo.GetConflictEditSpan(InlineRenameLocation location, string triggerText, string replacementText, CancellationToken cancellationToken) + => GetConflictEditSpan(new VSTypeScriptInlineRenameLocationWrapper( + new InlineRenameLocation(location.Document, location.TextSpan)), replacementText, cancellationToken); + + TextSpan IInlineRenameInfo.GetReferenceEditSpan(InlineRenameLocation location, string triggerText, CancellationToken cancellationToken) + => GetReferenceEditSpan(new VSTypeScriptInlineRenameLocationWrapper( + new InlineRenameLocation(location.Document, location.TextSpan)), cancellationToken); + + bool IInlineRenameInfo.TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) + => true; + + bool IInlineRenameInfo.TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) + => true; + + public InlineRenameFileRenameInfo GetFileRenameInfo() + => InlineRenameFileRenameInfo.NotAllowed; } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameLocationWrapper.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameLocationWrapper.cs index c554f002f03c3..73be95096df28 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameLocationWrapper.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameLocationWrapper.cs @@ -7,13 +7,12 @@ using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal readonly struct VSTypeScriptInlineRenameLocationWrapper(InlineRenameLocation underlyingObject) { - internal readonly struct VSTypeScriptInlineRenameLocationWrapper(InlineRenameLocation underlyingObject) - { - private readonly InlineRenameLocation _underlyingObject = underlyingObject; + private readonly InlineRenameLocation _underlyingObject = underlyingObject; - public Document Document => _underlyingObject.Document; - public TextSpan TextSpan => _underlyingObject.TextSpan; - } + public Document Document => _underlyingObject.Document; + public TextSpan TextSpan => _underlyingObject.TextSpan; } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameReplacementKind.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameReplacementKind.cs index 93a825cd832b0..d19b1cb08dab4 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameReplacementKind.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameReplacementKind.cs @@ -2,14 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal enum VSTypeScriptInlineRenameReplacementKind { - internal enum VSTypeScriptInlineRenameReplacementKind - { - NoConflict, - ResolvedReferenceConflict, - ResolvedNonReferenceConflict, - UnresolvedConflict, - Complexified, - } + NoConflict, + ResolvedReferenceConflict, + ResolvedNonReferenceConflict, + UnresolvedConflict, + Complexified, } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameReplacementWrapper.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameReplacementWrapper.cs index 8b85fbd2c5429..345fb9de25f20 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameReplacementWrapper.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptInlineRenameReplacementWrapper.cs @@ -7,14 +7,13 @@ using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal readonly struct VSTypeScriptInlineRenameReplacementWrapper(InlineRenameReplacement underlyingObject) { - internal readonly struct VSTypeScriptInlineRenameReplacementWrapper(InlineRenameReplacement underlyingObject) - { - internal readonly InlineRenameReplacement UnderlyingObject = underlyingObject; + internal readonly InlineRenameReplacement UnderlyingObject = underlyingObject; - public VSTypeScriptInlineRenameReplacementKind Kind => VSTypeScriptInlineRenameReplacementKindHelpers.ConvertFrom(UnderlyingObject.Kind); - public TextSpan OriginalSpan => UnderlyingObject.OriginalSpan; - public TextSpan NewSpan => UnderlyingObject.NewSpan; - } + public VSTypeScriptInlineRenameReplacementKind Kind => VSTypeScriptInlineRenameReplacementKindHelpers.ConvertFrom(UnderlyingObject.Kind); + public TextSpan OriginalSpan => UnderlyingObject.OriginalSpan; + public TextSpan NewSpan => UnderlyingObject.NewSpan; } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptPredefinedCommandHandlerNames.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptPredefinedCommandHandlerNames.cs index fddb7694c2e21..f2cb0c2585ed2 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptPredefinedCommandHandlerNames.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptPredefinedCommandHandlerNames.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Editor; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal static class VSTypeScriptPredefinedCommandHandlerNames { - internal static class VSTypeScriptPredefinedCommandHandlerNames - { - public const string SignatureHelpAfterCompletion = PredefinedCommandHandlerNames.SignatureHelpAfterCompletion; - } + public const string SignatureHelpAfterCompletion = PredefinedCommandHandlerNames.SignatureHelpAfterCompletion; } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptRenameOperationProvider.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptRenameOperationProvider.cs index b355abf9e8cf2..aa3693c6bcf8c 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptRenameOperationProvider.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptRenameOperationProvider.cs @@ -4,11 +4,10 @@ using Microsoft.CodeAnalysis.CodeActions; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal static class VSTypeScriptRenameOperationFactory { - internal static class VSTypeScriptRenameOperationFactory - { - public static CodeActionOperation CreateRenameOperation(DocumentId documentId, int position) - => new StartInlineRenameSessionOperation(documentId, position); - } + public static CodeActionOperation CreateRenameOperation(DocumentId documentId, int position) + => new StartInlineRenameSessionOperation(documentId, position); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptWellKnownSymbolTypes.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptWellKnownSymbolTypes.cs index 008f0586d1e6c..d8eecfb1c298a 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptWellKnownSymbolTypes.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypeScriptWellKnownSymbolTypes.cs @@ -2,13 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal static class VSTypeScriptWellKnownSymbolTypes { - internal static class VSTypeScriptWellKnownSymbolTypes - { - /// - /// Exists for binary compat. Not actually used for any purpose. - /// - public const string Definition = nameof(Definition); - } + /// + /// Exists for binary compat. Not actually used for any purpose. + /// + public const string Definition = nameof(Definition); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypescriptNavigationBarItem.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypescriptNavigationBarItem.cs index f174508242752..541076c3fd20a 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypescriptNavigationBarItem.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/VSTypescriptNavigationBarItem.cs @@ -5,23 +5,22 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; + +internal class VSTypescriptNavigationBarItem( + string text, + VSTypeScriptGlyph glyph, + ImmutableArray spans, + ImmutableArray childItems = default, + int indent = 0, + bool bolded = false, + bool grayed = false) { - internal class VSTypescriptNavigationBarItem( - string text, - VSTypeScriptGlyph glyph, - ImmutableArray spans, - ImmutableArray childItems = default, - int indent = 0, - bool bolded = false, - bool grayed = false) - { - public string Text { get; } = text; - public VSTypeScriptGlyph Glyph { get; } = glyph; - public bool Bolded { get; } = bolded; - public bool Grayed { get; } = grayed; - public int Indent { get; } = indent; - public ImmutableArray ChildItems { get; } = childItems.NullToEmpty(); - public ImmutableArray Spans { get; } = spans.NullToEmpty(); - } + public string Text { get; } = text; + public VSTypeScriptGlyph Glyph { get; } = glyph; + public bool Bolded { get; } = bolded; + public bool Grayed { get; } = grayed; + public int Indent { get; } = indent; + public ImmutableArray ChildItems { get; } = childItems.NullToEmpty(); + public ImmutableArray Spans { get; } = spans.NullToEmpty(); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptBlockStructureService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptBlockStructureService.cs index f64f7545ddae0..47d8f9766c3ef 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptBlockStructureService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptBlockStructureService.cs @@ -10,23 +10,22 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +[ExportLanguageService(typeof(BlockStructureService), InternalLanguageNames.TypeScript), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VSTypeScriptBlockStructureService(IVSTypeScriptBlockStructureServiceImplementation impl) : BlockStructureService { - [ExportLanguageService(typeof(BlockStructureService), InternalLanguageNames.TypeScript), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class VSTypeScriptBlockStructureService(IVSTypeScriptBlockStructureServiceImplementation impl) : BlockStructureService - { - private readonly IVSTypeScriptBlockStructureServiceImplementation _impl = impl; + private readonly IVSTypeScriptBlockStructureServiceImplementation _impl = impl; - public override string Language => InternalLanguageNames.TypeScript; + public override string Language => InternalLanguageNames.TypeScript; - public override async Task GetBlockStructureAsync(Document document, BlockStructureOptions options, CancellationToken cancellationToken) - { - var blockStructure = await _impl.GetBlockStructureAsync(document, cancellationToken).ConfigureAwait(false); + public override async Task GetBlockStructureAsync(Document document, BlockStructureOptions options, CancellationToken cancellationToken) + { + var blockStructure = await _impl.GetBlockStructureAsync(document, cancellationToken).ConfigureAwait(false); - return new BlockStructure(blockStructure.Spans.SelectAsArray( - x => new BlockSpan(x.Type!, x.IsCollapsible, x.TextSpan, x.HintSpan, subHeadings: default, x.BannerText, x.AutoCollapse, x.IsDefaultCollapsed))); - } + return new BlockStructure(blockStructure.Spans.SelectAsArray( + x => new BlockSpan(x.Type!, x.IsCollapsible, x.TextSpan, x.HintSpan, subHeadings: default, x.BannerText, x.AutoCollapse, x.IsDefaultCollapsed))); } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptBreakpointResolutionService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptBreakpointResolutionService.cs index ad63aa7e89673..096e20b09d860 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptBreakpointResolutionService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptBreakpointResolutionService.cs @@ -13,21 +13,20 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +[Shared] +[ExportLanguageService(typeof(IBreakpointResolutionService), InternalLanguageNames.TypeScript)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VSTypeScriptBreakpointResolutionService(IVSTypeScriptBreakpointResolutionServiceImplementation implementation) : IBreakpointResolutionService { - [Shared] - [ExportLanguageService(typeof(IBreakpointResolutionService), InternalLanguageNames.TypeScript)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class VSTypeScriptBreakpointResolutionService(IVSTypeScriptBreakpointResolutionServiceImplementation implementation) : IBreakpointResolutionService - { - private readonly IVSTypeScriptBreakpointResolutionServiceImplementation _implementation = implementation; + private readonly IVSTypeScriptBreakpointResolutionServiceImplementation _implementation = implementation; - public async Task ResolveBreakpointAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken = default) - => (await _implementation.ResolveBreakpointAsync(document, textSpan, cancellationToken).ConfigureAwait(false)).UnderlyingObject; + public async Task ResolveBreakpointAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken = default) + => (await _implementation.ResolveBreakpointAsync(document, textSpan, cancellationToken).ConfigureAwait(false)).UnderlyingObject; - public async Task> ResolveBreakpointsAsync(Solution solution, string name, CancellationToken cancellationToken = default) - => (await _implementation.ResolveBreakpointsAsync(solution, name, cancellationToken).ConfigureAwait(false)).Select(r => r.UnderlyingObject); + public async Task> ResolveBreakpointsAsync(Solution solution, string name, CancellationToken cancellationToken = default) + => (await _implementation.ResolveBreakpointsAsync(solution, name, cancellationToken).ConfigureAwait(false)).Select(r => r.UnderlyingObject); - } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptDiagnosticService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptDiagnosticService.cs index 5c983262256fb..35853b1ca7593 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptDiagnosticService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptDiagnosticService.cs @@ -12,65 +12,64 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +[Export(typeof(IVSTypeScriptDiagnosticService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VSTypeScriptDiagnosticService(IDiagnosticService service, IGlobalOptionService globalOptions) : IVSTypeScriptDiagnosticService { - [Export(typeof(IVSTypeScriptDiagnosticService)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class VSTypeScriptDiagnosticService(IDiagnosticService service, IGlobalOptionService globalOptions) : IVSTypeScriptDiagnosticService + private readonly IDiagnosticService _service = service; + private readonly IGlobalOptionService _globalOptions = globalOptions; + + public async Task> GetPushDiagnosticsAsync(Workspace workspace, ProjectId projectId, DocumentId documentId, object id, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) { - private readonly IDiagnosticService _service = service; - private readonly IGlobalOptionService _globalOptions = globalOptions; + // this is the TS entrypoint to get push diagnostics. Only return diagnostics if we're actually in push-mode. + var diagnosticMode = _globalOptions.GetDiagnosticMode(); + if (diagnosticMode != DiagnosticMode.SolutionCrawlerPush) + return []; - public async Task> GetPushDiagnosticsAsync(Workspace workspace, ProjectId projectId, DocumentId documentId, object id, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) - { - // this is the TS entrypoint to get push diagnostics. Only return diagnostics if we're actually in push-mode. - var diagnosticMode = _globalOptions.GetDiagnosticMode(); - if (diagnosticMode != DiagnosticMode.SolutionCrawlerPush) - return []; + var result = await _service.GetDiagnosticsAsync(workspace, projectId, documentId, id, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + return result.SelectAsArray(data => new VSTypeScriptDiagnosticData(data)); + } - var result = await _service.GetDiagnosticsAsync(workspace, projectId, documentId, id, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - return result.SelectAsArray(data => new VSTypeScriptDiagnosticData(data)); - } + [Obsolete] + public IDisposable RegisterDiagnosticsUpdatedEventHandler(Action action) + => new EventHandlerWrapper(_service, action); - [Obsolete] - public IDisposable RegisterDiagnosticsUpdatedEventHandler(Action action) - => new EventHandlerWrapper(_service, action); + public IDisposable RegisterDiagnosticsUpdatedEventHandler(Action> action) + => new EventHandlerWrapper(_service, action); - public IDisposable RegisterDiagnosticsUpdatedEventHandler(Action> action) - => new EventHandlerWrapper(_service, action); + private sealed class EventHandlerWrapper : IDisposable + { + private readonly IDiagnosticService _service; + private readonly EventHandler> _handler; - private sealed class EventHandlerWrapper : IDisposable + [Obsolete] + internal EventHandlerWrapper(IDiagnosticService service, Action action) { - private readonly IDiagnosticService _service; - private readonly EventHandler> _handler; - - [Obsolete] - internal EventHandlerWrapper(IDiagnosticService service, Action action) + _service = service; + _handler = (sender, argsCollection) => { - _service = service; - _handler = (sender, argsCollection) => - { - foreach (var args in argsCollection) - action(new VSTypeScriptDiagnosticsUpdatedArgsWrapper(args)); - }; - _service.DiagnosticsUpdated += _handler; - } + foreach (var args in argsCollection) + action(new VSTypeScriptDiagnosticsUpdatedArgsWrapper(args)); + }; + _service.DiagnosticsUpdated += _handler; + } - internal EventHandlerWrapper(IDiagnosticService service, Action> action) + internal EventHandlerWrapper(IDiagnosticService service, Action> action) + { + _service = service; + _handler = (sender, argsCollection) => { - _service = service; - _handler = (sender, argsCollection) => - { - action(ImmutableArray.CreateRange(argsCollection, static args => new VSTypeScriptDiagnosticsUpdatedArgsWrapper(args))); - }; - _service.DiagnosticsUpdated += _handler; - } + action(ImmutableArray.CreateRange(argsCollection, static args => new VSTypeScriptDiagnosticsUpdatedArgsWrapper(args))); + }; + _service.DiagnosticsUpdated += _handler; + } - public void Dispose() - { - _service.DiagnosticsUpdated -= _handler; - } + public void Dispose() + { + _service.DiagnosticsUpdated -= _handler; } } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptEditorInlineRenameService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptEditorInlineRenameService.cs index c4cadb2bdb0ad..417c3d27b7b75 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptEditorInlineRenameService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptEditorInlineRenameService.cs @@ -11,25 +11,24 @@ using Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +[Shared] +[ExportLanguageService(typeof(IEditorInlineRenameService), InternalLanguageNames.TypeScript)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VSTypeScriptEditorInlineRenameService( + [Import(AllowDefault = true)] Lazy? service) : IEditorInlineRenameService { - [Shared] - [ExportLanguageService(typeof(IEditorInlineRenameService), InternalLanguageNames.TypeScript)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class VSTypeScriptEditorInlineRenameService( - [Import(AllowDefault = true)] Lazy? service) : IEditorInlineRenameService - { - private readonly Lazy? _service = service; + private readonly Lazy? _service = service; - public async Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken) + public async Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken) + { + if (_service != null) { - if (_service != null) - { - return await _service.Value.GetRenameInfoAsync(document, position, cancellationToken).ConfigureAwait(false); - } - - return AbstractEditorInlineRenameService.DefaultFailureInfo; + return await _service.Value.GetRenameInfoAsync(document, position, cancellationToken).ConfigureAwait(false); } + + return AbstractEditorInlineRenameService.DefaultFailureInfo; } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFindUsagesContext.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFindUsagesContext.cs index 588feb1d250d6..fb303bfacba06 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFindUsagesContext.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFindUsagesContext.cs @@ -7,28 +7,27 @@ using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +internal sealed class VSTypeScriptFindUsagesContext(FindUsagesContext underlyingObject) : IVSTypeScriptFindUsagesContext { - internal sealed class VSTypeScriptFindUsagesContext(FindUsagesContext underlyingObject) : IVSTypeScriptFindUsagesContext - { - internal readonly FindUsagesContext UnderlyingObject = underlyingObject; + internal readonly FindUsagesContext UnderlyingObject = underlyingObject; - public IVSTypeScriptStreamingProgressTracker ProgressTracker - => new VSTypeScriptStreamingProgressTracker(UnderlyingObject.ProgressTracker); + public IVSTypeScriptStreamingProgressTracker ProgressTracker + => new VSTypeScriptStreamingProgressTracker(UnderlyingObject.ProgressTracker); - public ValueTask ReportMessageAsync(string message, CancellationToken cancellationToken) - => UnderlyingObject.ReportMessageAsync(message, cancellationToken); + public ValueTask ReportMessageAsync(string message, CancellationToken cancellationToken) + => UnderlyingObject.ReportNoResultsAsync(message, cancellationToken); - public ValueTask SetSearchTitleAsync(string title, CancellationToken cancellationToken) - => UnderlyingObject.SetSearchTitleAsync(title, cancellationToken); + public ValueTask SetSearchTitleAsync(string title, CancellationToken cancellationToken) + => UnderlyingObject.SetSearchTitleAsync(title, cancellationToken); - public ValueTask OnDefinitionFoundAsync(VSTypeScriptDefinitionItem definition, CancellationToken cancellationToken) - => UnderlyingObject.OnDefinitionFoundAsync(definition.UnderlyingObject, cancellationToken); + public ValueTask OnDefinitionFoundAsync(VSTypeScriptDefinitionItem definition, CancellationToken cancellationToken) + => UnderlyingObject.OnDefinitionFoundAsync(definition.UnderlyingObject, cancellationToken); - public ValueTask OnReferenceFoundAsync(VSTypeScriptSourceReferenceItem reference, CancellationToken cancellationToken) - => UnderlyingObject.OnReferenceFoundAsync(reference.UnderlyingObject, cancellationToken); + public ValueTask OnReferenceFoundAsync(VSTypeScriptSourceReferenceItem reference, CancellationToken cancellationToken) + => UnderlyingObject.OnReferenceFoundAsync(reference.UnderlyingObject, cancellationToken); - public ValueTask OnCompletedAsync(CancellationToken cancellationToken) - => UnderlyingObject.OnCompletedAsync(cancellationToken); - } + public ValueTask OnCompletedAsync(CancellationToken cancellationToken) + => UnderlyingObject.OnCompletedAsync(cancellationToken); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFindUsagesService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFindUsagesService.cs index 0fd1424d79d89..aa1ebbf355ccb 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFindUsagesService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFindUsagesService.cs @@ -14,53 +14,52 @@ using Roslyn.Utilities; using Microsoft.CodeAnalysis.Classification; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +[ExportLanguageService(typeof(IFindUsagesService), InternalLanguageNames.TypeScript), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VSTypeScriptFindUsagesService(IVSTypeScriptFindUsagesService underlyingService) : IFindUsagesService { - [ExportLanguageService(typeof(IFindUsagesService), InternalLanguageNames.TypeScript), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class VSTypeScriptFindUsagesService(IVSTypeScriptFindUsagesService underlyingService) : IFindUsagesService - { - private readonly IVSTypeScriptFindUsagesService _underlyingService = underlyingService; + private readonly IVSTypeScriptFindUsagesService _underlyingService = underlyingService; - public Task FindReferencesAsync(IFindUsagesContext context, Document document, int position, OptionsProvider classificationOptions, CancellationToken cancellationToken) - => _underlyingService.FindReferencesAsync(document, position, new Context(context), cancellationToken); + public Task FindReferencesAsync(IFindUsagesContext context, Document document, int position, OptionsProvider classificationOptions, CancellationToken cancellationToken) + => _underlyingService.FindReferencesAsync(document, position, new Context(context), cancellationToken); - public Task FindImplementationsAsync(IFindUsagesContext context, Document document, int position, OptionsProvider classificationOptions, CancellationToken cancellationToken) - => _underlyingService.FindImplementationsAsync(document, position, new Context(context), cancellationToken); + public Task FindImplementationsAsync(IFindUsagesContext context, Document document, int position, OptionsProvider classificationOptions, CancellationToken cancellationToken) + => _underlyingService.FindImplementationsAsync(document, position, new Context(context), cancellationToken); - private sealed class Context(IFindUsagesContext context) : IVSTypeScriptFindUsagesContext - { - private readonly IFindUsagesContext _context = context; + private sealed class Context(IFindUsagesContext context) : IVSTypeScriptFindUsagesContext + { + private readonly IFindUsagesContext _context = context; - public IVSTypeScriptStreamingProgressTracker ProgressTracker - => new ProgressTracker(_context.ProgressTracker); + public IVSTypeScriptStreamingProgressTracker ProgressTracker + => new ProgressTracker(_context.ProgressTracker); - public ValueTask ReportMessageAsync(string message, CancellationToken cancellationToken) - => _context.ReportMessageAsync(message, cancellationToken); + public ValueTask ReportMessageAsync(string message, CancellationToken cancellationToken) + => _context.ReportNoResultsAsync(message, cancellationToken); - public ValueTask SetSearchTitleAsync(string title, CancellationToken cancellationToken) - => _context.SetSearchTitleAsync(title, cancellationToken); + public ValueTask SetSearchTitleAsync(string title, CancellationToken cancellationToken) + => _context.SetSearchTitleAsync(title, cancellationToken); - public ValueTask OnDefinitionFoundAsync(VSTypeScriptDefinitionItem definition, CancellationToken cancellationToken) - => _context.OnDefinitionFoundAsync(definition.UnderlyingObject, cancellationToken); + public ValueTask OnDefinitionFoundAsync(VSTypeScriptDefinitionItem definition, CancellationToken cancellationToken) + => _context.OnDefinitionFoundAsync(definition.UnderlyingObject, cancellationToken); - public ValueTask OnReferenceFoundAsync(VSTypeScriptSourceReferenceItem reference, CancellationToken cancellationToken) - => _context.OnReferenceFoundAsync(reference.UnderlyingObject, cancellationToken); + public ValueTask OnReferenceFoundAsync(VSTypeScriptSourceReferenceItem reference, CancellationToken cancellationToken) + => _context.OnReferenceFoundAsync(reference.UnderlyingObject, cancellationToken); - public ValueTask OnCompletedAsync(CancellationToken cancellationToken) - => ValueTaskFactory.CompletedTask; - } + public ValueTask OnCompletedAsync(CancellationToken cancellationToken) + => ValueTaskFactory.CompletedTask; + } - private sealed class ProgressTracker(IStreamingProgressTracker progressTracker) : IVSTypeScriptStreamingProgressTracker - { - private readonly IStreamingProgressTracker _progressTracker = progressTracker; + private sealed class ProgressTracker(IStreamingProgressTracker progressTracker) : IVSTypeScriptStreamingProgressTracker + { + private readonly IStreamingProgressTracker _progressTracker = progressTracker; - public ValueTask AddItemsAsync(int count, CancellationToken cancellationToken) - => _progressTracker.AddItemsAsync(count, cancellationToken); + public ValueTask AddItemsAsync(int count, CancellationToken cancellationToken) + => _progressTracker.AddItemsAsync(count, cancellationToken); - public ValueTask ItemCompletedAsync(CancellationToken cancellationToken) - => _progressTracker.ItemCompletedAsync(cancellationToken); - } + public ValueTask ItemCompletedAsync(CancellationToken cancellationToken) + => _progressTracker.ItemCompletedAsync(cancellationToken); } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFormattingInteractionService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFormattingInteractionService.cs index ab6855f07a5d5..646a439c625f8 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFormattingInteractionService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptFormattingInteractionService.cs @@ -16,33 +16,32 @@ using Microsoft.CodeAnalysis.Indentation; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +[ExportLanguageService(typeof(IFormattingInteractionService), InternalLanguageNames.TypeScript), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VSTypeScriptFormattingInteractionService(IVSTypeScriptFormattingInteractionService implementation) : IFormattingInteractionService { - [ExportLanguageService(typeof(IFormattingInteractionService), InternalLanguageNames.TypeScript), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class VSTypeScriptFormattingInteractionService(IVSTypeScriptFormattingInteractionService implementation) : IFormattingInteractionService - { - private readonly IVSTypeScriptFormattingInteractionService _implementation = implementation; + private readonly IVSTypeScriptFormattingInteractionService _implementation = implementation; - public bool SupportsFormatDocument => _implementation.SupportsFormatDocument; - public bool SupportsFormatSelection => _implementation.SupportsFormatSelection; - public bool SupportsFormatOnPaste => _implementation.SupportsFormatOnPaste; - public bool SupportsFormatOnReturn => _implementation.SupportsFormatOnReturn; + public bool SupportsFormatDocument => _implementation.SupportsFormatDocument; + public bool SupportsFormatSelection => _implementation.SupportsFormatSelection; + public bool SupportsFormatOnPaste => _implementation.SupportsFormatOnPaste; + public bool SupportsFormatOnReturn => _implementation.SupportsFormatOnReturn; - public bool SupportsFormattingOnTypedCharacter(Document document, char ch) - => _implementation.SupportsFormattingOnTypedCharacter(document, ch); + public bool SupportsFormattingOnTypedCharacter(Document document, char ch) + => _implementation.SupportsFormattingOnTypedCharacter(document, ch); - public Task> GetFormattingChangesAsync(Document document, ITextBuffer textBuffer, TextSpan? textSpan, CancellationToken cancellationToken) - => _implementation.GetFormattingChangesAsync(document, textSpan, documentOptions: null, cancellationToken); + public Task> GetFormattingChangesAsync(Document document, ITextBuffer textBuffer, TextSpan? textSpan, CancellationToken cancellationToken) + => _implementation.GetFormattingChangesAsync(document, textSpan, documentOptions: null, cancellationToken); - public Task> GetFormattingChangesOnPasteAsync(Document document, ITextBuffer textBuffer, TextSpan textSpan, CancellationToken cancellationToken) - => _implementation.GetFormattingChangesOnPasteAsync(document, textSpan, documentOptions: null, cancellationToken); + public Task> GetFormattingChangesOnPasteAsync(Document document, ITextBuffer textBuffer, TextSpan textSpan, CancellationToken cancellationToken) + => _implementation.GetFormattingChangesOnPasteAsync(document, textSpan, documentOptions: null, cancellationToken); - public Task> GetFormattingChangesAsync(Document document, ITextBuffer textBuffer, char typedChar, int position, CancellationToken cancellationToken) - => _implementation.GetFormattingChangesAsync(document, typedChar, position, documentOptions: null, cancellationToken); + public Task> GetFormattingChangesAsync(Document document, ITextBuffer textBuffer, char typedChar, int position, CancellationToken cancellationToken) + => _implementation.GetFormattingChangesAsync(document, typedChar, position, documentOptions: null, cancellationToken); - public Task> GetFormattingChangesOnReturnAsync(Document document, int position, CancellationToken cancellationToken) - => _implementation.GetFormattingChangesOnReturnAsync(document, position, documentOptions: null, cancellationToken); - } + public Task> GetFormattingChangesOnReturnAsync(Document document, int position, CancellationToken cancellationToken) + => _implementation.GetFormattingChangesOnReturnAsync(document, position, documentOptions: null, cancellationToken); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInProcLanguageClient.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInProcLanguageClient.cs index 044228a1ccc40..c6ec2d94c64d7 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInProcLanguageClient.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInProcLanguageClient.cs @@ -20,74 +20,73 @@ using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript -{ - /// - /// Language client to handle TS LSP requests. - /// Allows us to move features to LSP without being blocked by TS as well - /// as ensures that TS LSP features use correct solution snapshots. - /// - [ContentType(ContentTypeNames.TypeScriptContentTypeName)] - [ContentType(ContentTypeNames.JavaScriptContentTypeName)] - [Export(typeof(ILanguageClient))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, true)] - internal class VSTypeScriptInProcLanguageClient( - [Import(AllowDefault = true)] IVSTypeScriptCapabilitiesProvider? typeScriptCapabilitiesProvider, - VSTypeScriptLspServiceProvider lspServiceProvider, - IGlobalOptionService globalOptions, - ILspServiceLoggerFactory lspLoggerFactory, - IThreadingContext threadingContext, - ExportProvider exportProvider) : AbstractInProcLanguageClient(lspServiceProvider, globalOptions, lspLoggerFactory, threadingContext, exportProvider) - { - private readonly IVSTypeScriptCapabilitiesProvider? _typeScriptCapabilitiesProvider = typeScriptCapabilitiesProvider; +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; - protected override ImmutableArray SupportedLanguages => [InternalLanguageNames.TypeScript]; +/// +/// Language client to handle TS LSP requests. +/// Allows us to move features to LSP without being blocked by TS as well +/// as ensures that TS LSP features use correct solution snapshots. +/// +[ContentType(ContentTypeNames.TypeScriptContentTypeName)] +[ContentType(ContentTypeNames.JavaScriptContentTypeName)] +[Export(typeof(ILanguageClient))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, true)] +internal class VSTypeScriptInProcLanguageClient( + [Import(AllowDefault = true)] IVSTypeScriptCapabilitiesProvider? typeScriptCapabilitiesProvider, + VSTypeScriptLspServiceProvider lspServiceProvider, + IGlobalOptionService globalOptions, + ILspServiceLoggerFactory lspLoggerFactory, + IThreadingContext threadingContext, + ExportProvider exportProvider) : AbstractInProcLanguageClient(lspServiceProvider, globalOptions, lspLoggerFactory, threadingContext, exportProvider) +{ + private readonly IVSTypeScriptCapabilitiesProvider? _typeScriptCapabilitiesProvider = typeScriptCapabilitiesProvider; - public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) - { - var serverCapabilities = GetTypeScriptServerCapabilities(clientCapabilities); + protected override ImmutableArray SupportedLanguages => [InternalLanguageNames.TypeScript]; - serverCapabilities.TextDocumentSync = new TextDocumentSyncOptions - { - Change = TextDocumentSyncKind.Incremental, - OpenClose = true, - }; + public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) + { + var serverCapabilities = GetTypeScriptServerCapabilities(clientCapabilities); - serverCapabilities.ProjectContextProvider = true; + serverCapabilities.TextDocumentSync = new TextDocumentSyncOptions + { + Change = TextDocumentSyncKind.Incremental, + OpenClose = true, + }; - var isPullDiagnostics = GlobalOptions.IsLspPullDiagnostics(); - if (isPullDiagnostics) - { - serverCapabilities.SupportsDiagnosticRequests = true; - } + serverCapabilities.ProjectContextProvider = true; - return serverCapabilities; + var isPullDiagnostics = GlobalOptions.IsLspPullDiagnostics(); + if (isPullDiagnostics) + { + serverCapabilities.SupportsDiagnosticRequests = true; } - /// - /// When pull diagnostics is enabled, ensure that initialization failures are displayed to the user as - /// they will get no diagnostics. When not enabled we don't show the failure box (failure will still be recorded in the task status center) - /// as the failure is not catastrophic. - /// - public override bool ShowNotificationOnInitializeFailed => GlobalOptions.IsLspPullDiagnostics(); + return serverCapabilities; + } + + /// + /// When pull diagnostics is enabled, ensure that initialization failures are displayed to the user as + /// they will get no diagnostics. When not enabled we don't show the failure box (failure will still be recorded in the task status center) + /// as the failure is not catastrophic. + /// + public override bool ShowNotificationOnInitializeFailed => GlobalOptions.IsLspPullDiagnostics(); - public override WellKnownLspServerKinds ServerKind => WellKnownLspServerKinds.RoslynTypeScriptLspServer; + public override WellKnownLspServerKinds ServerKind => WellKnownLspServerKinds.RoslynTypeScriptLspServer; - private VSInternalServerCapabilities GetTypeScriptServerCapabilities(ClientCapabilities clientCapabilities) + private VSInternalServerCapabilities GetTypeScriptServerCapabilities(ClientCapabilities clientCapabilities) + { + if (_typeScriptCapabilitiesProvider != null) + { + var serializedClientCapabilities = JsonConvert.SerializeObject(clientCapabilities); + var serializedServerCapabilities = _typeScriptCapabilitiesProvider.GetServerCapabilities(serializedClientCapabilities); + var typeScriptServerCapabilities = JsonConvert.DeserializeObject(serializedServerCapabilities); + Contract.ThrowIfNull(typeScriptServerCapabilities); + return typeScriptServerCapabilities; + } + else { - if (_typeScriptCapabilitiesProvider != null) - { - var serializedClientCapabilities = JsonConvert.SerializeObject(clientCapabilities); - var serializedServerCapabilities = _typeScriptCapabilitiesProvider.GetServerCapabilities(serializedClientCapabilities); - var typeScriptServerCapabilities = JsonConvert.DeserializeObject(serializedServerCapabilities); - Contract.ThrowIfNull(typeScriptServerCapabilities); - return typeScriptServerCapabilities; - } - else - { - return new VSInternalServerCapabilities(); - } + return new VSInternalServerCapabilities(); } } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInlineRenameReplacementKindHelpers.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInlineRenameReplacementKindHelpers.cs index 391abbf909f83..41931120f1da6 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInlineRenameReplacementKindHelpers.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInlineRenameReplacementKindHelpers.cs @@ -8,34 +8,33 @@ using Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +internal static class VSTypeScriptInlineRenameReplacementKindHelpers { - internal static class VSTypeScriptInlineRenameReplacementKindHelpers + public static VSTypeScriptInlineRenameReplacementKind ConvertFrom(InlineRenameReplacementKind kind) { - public static VSTypeScriptInlineRenameReplacementKind ConvertFrom(InlineRenameReplacementKind kind) + return kind switch { - return kind switch - { - InlineRenameReplacementKind.NoConflict => VSTypeScriptInlineRenameReplacementKind.NoConflict, - InlineRenameReplacementKind.ResolvedReferenceConflict => VSTypeScriptInlineRenameReplacementKind.ResolvedReferenceConflict, - InlineRenameReplacementKind.ResolvedNonReferenceConflict => VSTypeScriptInlineRenameReplacementKind.ResolvedNonReferenceConflict, - InlineRenameReplacementKind.UnresolvedConflict => VSTypeScriptInlineRenameReplacementKind.UnresolvedConflict, - InlineRenameReplacementKind.Complexified => VSTypeScriptInlineRenameReplacementKind.Complexified, - _ => throw ExceptionUtilities.UnexpectedValue(kind), - }; - } + InlineRenameReplacementKind.NoConflict => VSTypeScriptInlineRenameReplacementKind.NoConflict, + InlineRenameReplacementKind.ResolvedReferenceConflict => VSTypeScriptInlineRenameReplacementKind.ResolvedReferenceConflict, + InlineRenameReplacementKind.ResolvedNonReferenceConflict => VSTypeScriptInlineRenameReplacementKind.ResolvedNonReferenceConflict, + InlineRenameReplacementKind.UnresolvedConflict => VSTypeScriptInlineRenameReplacementKind.UnresolvedConflict, + InlineRenameReplacementKind.Complexified => VSTypeScriptInlineRenameReplacementKind.Complexified, + _ => throw ExceptionUtilities.UnexpectedValue(kind), + }; + } - public static InlineRenameReplacementKind ConvertTo(VSTypeScriptInlineRenameReplacementKind kind) + public static InlineRenameReplacementKind ConvertTo(VSTypeScriptInlineRenameReplacementKind kind) + { + return kind switch { - return kind switch - { - VSTypeScriptInlineRenameReplacementKind.NoConflict => InlineRenameReplacementKind.NoConflict, - VSTypeScriptInlineRenameReplacementKind.ResolvedReferenceConflict => InlineRenameReplacementKind.ResolvedReferenceConflict, - VSTypeScriptInlineRenameReplacementKind.ResolvedNonReferenceConflict => InlineRenameReplacementKind.ResolvedNonReferenceConflict, - VSTypeScriptInlineRenameReplacementKind.UnresolvedConflict => InlineRenameReplacementKind.UnresolvedConflict, - VSTypeScriptInlineRenameReplacementKind.Complexified => InlineRenameReplacementKind.Complexified, - _ => throw ExceptionUtilities.UnexpectedValue(kind), - }; - } + VSTypeScriptInlineRenameReplacementKind.NoConflict => InlineRenameReplacementKind.NoConflict, + VSTypeScriptInlineRenameReplacementKind.ResolvedReferenceConflict => InlineRenameReplacementKind.ResolvedReferenceConflict, + VSTypeScriptInlineRenameReplacementKind.ResolvedNonReferenceConflict => InlineRenameReplacementKind.ResolvedNonReferenceConflict, + VSTypeScriptInlineRenameReplacementKind.UnresolvedConflict => InlineRenameReplacementKind.UnresolvedConflict, + VSTypeScriptInlineRenameReplacementKind.Complexified => InlineRenameReplacementKind.Complexified, + _ => throw ExceptionUtilities.UnexpectedValue(kind), + }; } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptLanguageDebugInfoService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptLanguageDebugInfoService.cs index 4fc79ca132f13..51c59c3d372f0 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptLanguageDebugInfoService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptLanguageDebugInfoService.cs @@ -10,20 +10,19 @@ using Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +[Shared] +[ExportLanguageService(typeof(ILanguageDebugInfoService), InternalLanguageNames.TypeScript)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VSTypeScriptLanguageDebugInfoService(IVSTypeScriptLanguageDebugInfoServiceImplementation implementation) : ILanguageDebugInfoService { - [Shared] - [ExportLanguageService(typeof(ILanguageDebugInfoService), InternalLanguageNames.TypeScript)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class VSTypeScriptLanguageDebugInfoService(IVSTypeScriptLanguageDebugInfoServiceImplementation implementation) : ILanguageDebugInfoService - { - private readonly IVSTypeScriptLanguageDebugInfoServiceImplementation _implementation = implementation; + private readonly IVSTypeScriptLanguageDebugInfoServiceImplementation _implementation = implementation; - public async Task GetDataTipInfoAsync(Document document, int position, CancellationToken cancellationToken) - => (await _implementation.GetDataTipInfoAsync(document, position, cancellationToken).ConfigureAwait(false)).UnderlyingObject; + public async Task GetDataTipInfoAsync(Document document, int position, CancellationToken cancellationToken) + => (await _implementation.GetDataTipInfoAsync(document, position, cancellationToken).ConfigureAwait(false)).UnderlyingObject; - public async Task GetLocationInfoAsync(Document document, int position, CancellationToken cancellationToken) - => (await _implementation.GetLocationInfoAsync(document, position, cancellationToken).ConfigureAwait(false)).UnderlyingObject; - } + public async Task GetLocationInfoAsync(Document document, int position, CancellationToken cancellationToken) + => (await _implementation.GetLocationInfoAsync(document, position, cancellationToken).ConfigureAwait(false)).UnderlyingObject; } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptNavigationBarItemService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptNavigationBarItemService.cs index 35c956229fbf9..567a49f8f0f0e 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptNavigationBarItemService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptNavigationBarItemService.cs @@ -17,68 +17,67 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +[ExportLanguageService(typeof(INavigationBarItemService), InternalLanguageNames.TypeScript), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class VSTypeScriptNavigationBarItemService( + IThreadingContext threadingContext, + IVSTypeScriptNavigationBarItemService service) : INavigationBarItemService { - [ExportLanguageService(typeof(INavigationBarItemService), InternalLanguageNames.TypeScript), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class VSTypeScriptNavigationBarItemService( - IThreadingContext threadingContext, - IVSTypeScriptNavigationBarItemService service) : INavigationBarItemService - { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IVSTypeScriptNavigationBarItemService _service = service; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IVSTypeScriptNavigationBarItemService _service = service; - public Task> GetItemsAsync( - Document document, ITextVersion textVersion, CancellationToken cancellationToken) - { - return ((INavigationBarItemService)this).GetItemsAsync( - document, workspaceSupportsDocumentChanges: true, forceFrozenPartialSemanticsForCrossProcessOperations: false, textVersion, cancellationToken); - } + public Task> GetItemsAsync( + Document document, ITextVersion textVersion, CancellationToken cancellationToken) + { + return ((INavigationBarItemService)this).GetItemsAsync( + document, workspaceSupportsDocumentChanges: true, forceFrozenPartialSemanticsForCrossProcessOperations: false, textVersion, cancellationToken); + } - async Task> INavigationBarItemService.GetItemsAsync( - Document document, - bool workspaceSupportsDocumentChanges, - bool forceFrozenPartialSemanticsForCrossProcessOperations, - ITextVersion textVersion, - CancellationToken cancellationToken) - { - var items = await _service.GetItemsAsync(document, cancellationToken).ConfigureAwait(false); - return ConvertItems(items, textVersion); - } + async Task> INavigationBarItemService.GetItemsAsync( + Document document, + bool workspaceSupportsDocumentChanges, + bool forceFrozenPartialSemanticsForCrossProcessOperations, + ITextVersion textVersion, + CancellationToken cancellationToken) + { + var items = await _service.GetItemsAsync(document, cancellationToken).ConfigureAwait(false); + return ConvertItems(items, textVersion); + } - private static ImmutableArray ConvertItems(ImmutableArray items, ITextVersion textVersion) - => items.SelectAsArray(x => !x.Spans.IsEmpty, x => ConvertToNavigationBarItem(x, textVersion)); + private static ImmutableArray ConvertItems(ImmutableArray items, ITextVersion textVersion) + => items.SelectAsArray(x => !x.Spans.IsEmpty, x => ConvertToNavigationBarItem(x, textVersion)); - public async Task TryNavigateToItemAsync( - Document document, NavigationBarItem item, ITextView view, ITextVersion textVersion, CancellationToken cancellationToken) - { - // Spans.First() is safe here as we filtered out any items with no spans above in ConvertItems. - var navigationSpan = item.GetCurrentItemSpan(textVersion, item.Spans.First()); + public async Task TryNavigateToItemAsync( + Document document, NavigationBarItem item, ITextView view, ITextVersion textVersion, CancellationToken cancellationToken) + { + // Spans.First() is safe here as we filtered out any items with no spans above in ConvertItems. + var navigationSpan = item.GetCurrentItemSpan(textVersion, item.Spans.First()); - var workspace = document.Project.Solution.Workspace; - var navigationService = workspace.Services.GetRequiredService(); - return await navigationService.TryNavigateToPositionAsync( - _threadingContext, workspace, document.Id, navigationSpan.Start, virtualSpace: 0, NavigationOptions.Default, cancellationToken).ConfigureAwait(false); - } + var workspace = document.Project.Solution.Workspace; + var navigationService = workspace.Services.GetRequiredService(); + return await navigationService.TryNavigateToPositionAsync( + _threadingContext, workspace, document.Id, navigationSpan.Start, virtualSpace: 0, NavigationOptions.Default, cancellationToken).ConfigureAwait(false); + } - public bool ShowItemGrayedIfNear(NavigationBarItem item) - { - return true; - } + public bool ShowItemGrayedIfNear(NavigationBarItem item) + { + return true; + } - private static NavigationBarItem ConvertToNavigationBarItem(VSTypescriptNavigationBarItem item, ITextVersion textVersion) - { - Contract.ThrowIfTrue(item.Spans.IsEmpty); - return new SimpleNavigationBarItem( - textVersion, - item.Text, - VSTypeScriptGlyphHelpers.ConvertTo(item.Glyph), - item.Spans, - ConvertItems(item.ChildItems, textVersion), - item.Indent, - item.Bolded, - item.Grayed); - } + private static NavigationBarItem ConvertToNavigationBarItem(VSTypescriptNavigationBarItem item, ITextVersion textVersion) + { + Contract.ThrowIfTrue(item.Spans.IsEmpty); + return new SimpleNavigationBarItem( + textVersion, + item.Text, + VSTypeScriptGlyphHelpers.ConvertTo(item.Glyph), + item.Spans, + ConvertItems(item.ChildItems, textVersion), + item.Indent, + item.Bolded, + item.Grayed); } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptStreamingFindUsagesPresenterAccessor.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptStreamingFindUsagesPresenterAccessor.cs index 025ef9eda2740..6b7793bbecec2 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptStreamingFindUsagesPresenterAccessor.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptStreamingFindUsagesPresenterAccessor.cs @@ -9,23 +9,22 @@ using Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript -{ - [Export(typeof(IVSTypeScriptStreamingFindUsagesPresenterAccessor)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class VSTypeScriptStreamingFindUsagesPresenterAccessor(IStreamingFindUsagesPresenter underlyingObject) : IVSTypeScriptStreamingFindUsagesPresenterAccessor - { - private readonly IStreamingFindUsagesPresenter _underlyingObject = underlyingObject; +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; - public (IVSTypeScriptFindUsagesContext context, CancellationToken cancellationToken) StartSearch( - string title, bool supportsReferences) - { - var (context, cancellationToken) = _underlyingObject.StartSearch(title, supportsReferences); - return (new VSTypeScriptFindUsagesContext(context), cancellationToken); - } +[Export(typeof(IVSTypeScriptStreamingFindUsagesPresenterAccessor)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VSTypeScriptStreamingFindUsagesPresenterAccessor(IStreamingFindUsagesPresenter underlyingObject) : IVSTypeScriptStreamingFindUsagesPresenterAccessor +{ + private readonly IStreamingFindUsagesPresenter _underlyingObject = underlyingObject; - public void ClearAll() - => _underlyingObject.ClearAll(); + public (IVSTypeScriptFindUsagesContext context, CancellationToken cancellationToken) StartSearch( + string title, bool supportsReferences) + { + var (context, cancellationToken) = _underlyingObject.StartSearch(title, new StreamingFindUsagesPresenterOptions() { SupportsReferences = supportsReferences }); + return (new VSTypeScriptFindUsagesContext(context), cancellationToken); } + + public void ClearAll() + => _underlyingObject.ClearAll(); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptStreamingProgressTracker.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptStreamingProgressTracker.cs index 5f65fde4efec0..f21456f5fafc8 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptStreamingProgressTracker.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptStreamingProgressTracker.cs @@ -7,16 +7,15 @@ using Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +internal sealed class VSTypeScriptStreamingProgressTracker(IStreamingProgressTracker progressTracker) : IVSTypeScriptStreamingProgressTracker { - internal sealed class VSTypeScriptStreamingProgressTracker(IStreamingProgressTracker progressTracker) : IVSTypeScriptStreamingProgressTracker - { - private readonly IStreamingProgressTracker _progressTracker = progressTracker; + private readonly IStreamingProgressTracker _progressTracker = progressTracker; - public ValueTask AddItemsAsync(int count, CancellationToken cancellationToken) - => _progressTracker.AddItemsAsync(count, cancellationToken); + public ValueTask AddItemsAsync(int count, CancellationToken cancellationToken) + => _progressTracker.AddItemsAsync(count, cancellationToken); - public ValueTask ItemCompletedAsync(CancellationToken cancellationToken) - => _progressTracker.ItemCompletedAsync(cancellationToken); - } + public ValueTask ItemCompletedAsync(CancellationToken cancellationToken) + => _progressTracker.ItemCompletedAsync(cancellationToken); } diff --git a/src/EditorFeatures/Core/ExtractInterface/AbstractExtractInterfaceCommandHandler.cs b/src/EditorFeatures/Core/ExtractInterface/AbstractExtractInterfaceCommandHandler.cs index ef340576250de..e5d0374bd87b5 100644 --- a/src/EditorFeatures/Core/ExtractInterface/AbstractExtractInterfaceCommandHandler.cs +++ b/src/EditorFeatures/Core/ExtractInterface/AbstractExtractInterfaceCommandHandler.cs @@ -17,90 +17,89 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.ExtractInterface +namespace Microsoft.CodeAnalysis.ExtractInterface; + +internal abstract class AbstractExtractInterfaceCommandHandler : ICommandHandler { - internal abstract class AbstractExtractInterfaceCommandHandler : ICommandHandler - { - private readonly IThreadingContext _threadingContext; - private readonly IGlobalOptionService _globalOptions; + private readonly IThreadingContext _threadingContext; + private readonly IGlobalOptionService _globalOptions; - protected AbstractExtractInterfaceCommandHandler(IThreadingContext threadingContext, IGlobalOptionService globalOptions) - { - _threadingContext = threadingContext; - _globalOptions = globalOptions; - } + protected AbstractExtractInterfaceCommandHandler(IThreadingContext threadingContext, IGlobalOptionService globalOptions) + { + _threadingContext = threadingContext; + _globalOptions = globalOptions; + } - public string DisplayName => EditorFeaturesResources.Extract_Interface; + public string DisplayName => EditorFeaturesResources.Extract_Interface; - public CommandState GetCommandState(ExtractInterfaceCommandArgs args) - => IsAvailable(args.SubjectBuffer, out _) ? CommandState.Available : CommandState.Unspecified; + public CommandState GetCommandState(ExtractInterfaceCommandArgs args) + => IsAvailable(args.SubjectBuffer, out _) ? CommandState.Available : CommandState.Unspecified; - public bool ExecuteCommand(ExtractInterfaceCommandArgs args, CommandExecutionContext context) + public bool ExecuteCommand(ExtractInterfaceCommandArgs args, CommandExecutionContext context) + { + using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Extract_Interface)) { - using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Extract_Interface)) + var subjectBuffer = args.SubjectBuffer; + if (!IsAvailable(subjectBuffer, out var workspace)) { - var subjectBuffer = args.SubjectBuffer; - if (!IsAvailable(subjectBuffer, out var workspace)) - { - return false; - } + return false; + } + + var caretPoint = args.TextView.GetCaretPoint(subjectBuffer); + if (!caretPoint.HasValue) + { + return false; + } + + var document = subjectBuffer.CurrentSnapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChanges( + context.OperationContext, _threadingContext); + if (document == null) + { + return false; + } + + // We are about to show a modal UI dialog so we should take over the command execution + // wait context. That means the command system won't attempt to show its own wait dialog + // and also will take it into consideration when measuring command handling duration. + context.OperationContext.TakeOwnership(); - var caretPoint = args.TextView.GetCaretPoint(subjectBuffer); - if (!caretPoint.HasValue) + var extractInterfaceService = document.GetLanguageService(); + _threadingContext.JoinableTaskFactory.Run(async () => + { + // ConfigureAwait(true) here so we are back on the UI thread + // before calling TryApplyChanges below. Make sure if other + // async code is added between the two calls to handle thread + // affinity accordingly + var result = await extractInterfaceService.ExtractInterfaceAsync( + document, + caretPoint.Value.Position, + _globalOptions.CreateProvider(), + (errorMessage, severity) => workspace.Services.GetService().SendNotification(errorMessage, severity: severity), + CancellationToken.None).ConfigureAwait(true); + + if (result == null || !result.Succeeded) { - return false; + return; } - var document = subjectBuffer.CurrentSnapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChanges( - context.OperationContext, _threadingContext); - if (document == null) + if (!document.Project.Solution.Workspace.TryApplyChanges(result.UpdatedSolution)) { - return false; + // TODO: handle failure + return; } - // We are about to show a modal UI dialog so we should take over the command execution - // wait context. That means the command system won't attempt to show its own wait dialog - // and also will take it into consideration when measuring command handling duration. - context.OperationContext.TakeOwnership(); + var navigationService = workspace.Services.GetService(); + await navigationService.TryNavigateToPositionAsync( + _threadingContext, workspace, result.NavigationDocumentId, position: 0, CancellationToken.None).ConfigureAwait(false); + }); - var extractInterfaceService = document.GetLanguageService(); - _threadingContext.JoinableTaskFactory.Run(async () => - { - // ConfigureAwait(true) here so we are back on the UI thread - // before calling TryApplyChanges below. Make sure if other - // async code is added between the two calls to handle thread - // affinity accordingly - var result = await extractInterfaceService.ExtractInterfaceAsync( - document, - caretPoint.Value.Position, - _globalOptions.CreateProvider(), - (errorMessage, severity) => workspace.Services.GetService().SendNotification(errorMessage, severity: severity), - CancellationToken.None).ConfigureAwait(true); - - if (result == null || !result.Succeeded) - { - return; - } - - if (!document.Project.Solution.Workspace.TryApplyChanges(result.UpdatedSolution)) - { - // TODO: handle failure - return; - } - - var navigationService = workspace.Services.GetService(); - await navigationService.TryNavigateToPositionAsync( - _threadingContext, workspace, result.NavigationDocumentId, position: 0, CancellationToken.None).ConfigureAwait(false); - }); - - return true; - } + return true; } - - private static bool IsAvailable(ITextBuffer subjectBuffer, out Workspace workspace) - => subjectBuffer.TryGetWorkspace(out workspace) && - workspace.CanApplyChange(ApplyChangesKind.AddDocument) && - workspace.CanApplyChange(ApplyChangesKind.ChangeDocument) && - subjectBuffer.SupportsRefactorings(); } + + private static bool IsAvailable(ITextBuffer subjectBuffer, out Workspace workspace) + => subjectBuffer.TryGetWorkspace(out workspace) && + workspace.CanApplyChange(ApplyChangesKind.AddDocument) && + workspace.CanApplyChange(ApplyChangesKind.ChangeDocument) && + subjectBuffer.SupportsRefactorings(); } diff --git a/src/EditorFeatures/Core/ExtractMethod/ExtractMethodPresentationOptionsStorage.cs b/src/EditorFeatures/Core/ExtractMethod/ExtractMethodPresentationOptionsStorage.cs index 145fe88a88b3e..4dd8b0c921ab7 100644 --- a/src/EditorFeatures/Core/ExtractMethod/ExtractMethodPresentationOptionsStorage.cs +++ b/src/EditorFeatures/Core/ExtractMethod/ExtractMethodPresentationOptionsStorage.cs @@ -4,12 +4,11 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.ExtractMethod +namespace Microsoft.CodeAnalysis.ExtractMethod; + +internal static class ExtractMethodPresentationOptionsStorage { - internal static class ExtractMethodPresentationOptionsStorage - { - // Deprecated. Never exposed in the UI to users in any way. Kept around to still support automation - // getting/setting our storage values from the user's profile. - public static readonly PerLanguageOption2 AllowBestEffort = new("dotnet_allow_best_effort_when_extracting_method", defaultValue: true); - } + // Deprecated. Never exposed in the UI to users in any way. Kept around to still support automation + // getting/setting our storage values from the user's profile. + public static readonly PerLanguageOption2 AllowBestEffort = new("dotnet_allow_best_effort_when_extracting_method", defaultValue: true); } diff --git a/src/EditorFeatures/Core/FindReferences/FindReferencesCommandHandler.cs b/src/EditorFeatures/Core/FindReferences/FindReferencesCommandHandler.cs index 5448eec7b2adf..439de5926d51c 100644 --- a/src/EditorFeatures/Core/FindReferences/FindReferencesCommandHandler.cs +++ b/src/EditorFeatures/Core/FindReferences/FindReferencesCommandHandler.cs @@ -25,134 +25,136 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.FindReferences +namespace Microsoft.CodeAnalysis.FindReferences; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.RoslynContentType)] +[Name(PredefinedCommandHandlerNames.FindReferences)] +internal class FindReferencesCommandHandler : ICommandHandler { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.RoslynContentType)] - [Name(PredefinedCommandHandlerNames.FindReferences)] - internal class FindReferencesCommandHandler : ICommandHandler + private readonly IStreamingFindUsagesPresenter _streamingPresenter; + private readonly IGlobalOptionService _globalOptions; + private readonly IAsynchronousOperationListener _asyncListener; + + public string DisplayName => EditorFeaturesResources.Find_References; + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public FindReferencesCommandHandler( + IStreamingFindUsagesPresenter streamingPresenter, + IGlobalOptionService globalOptions, + IAsynchronousOperationListenerProvider listenerProvider) { - private readonly IStreamingFindUsagesPresenter _streamingPresenter; - private readonly IGlobalOptionService _globalOptions; - private readonly IAsynchronousOperationListener _asyncListener; - - public string DisplayName => EditorFeaturesResources.Find_References; - - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public FindReferencesCommandHandler( - IStreamingFindUsagesPresenter streamingPresenter, - IGlobalOptionService globalOptions, - IAsynchronousOperationListenerProvider listenerProvider) - { - Contract.ThrowIfNull(listenerProvider); + Contract.ThrowIfNull(listenerProvider); - _streamingPresenter = streamingPresenter; - _globalOptions = globalOptions; - _asyncListener = listenerProvider.GetListener(FeatureAttribute.FindReferences); - } + _streamingPresenter = streamingPresenter; + _globalOptions = globalOptions; + _asyncListener = listenerProvider.GetListener(FeatureAttribute.FindReferences); + } - public CommandState GetCommandState(FindReferencesCommandArgs args) - { - var (_, service) = GetDocumentAndService(args.SubjectBuffer.CurrentSnapshot); - return service != null - ? CommandState.Available - : CommandState.Unspecified; - } + public CommandState GetCommandState(FindReferencesCommandArgs args) + { + var (_, service) = GetDocumentAndService(args.SubjectBuffer.CurrentSnapshot); + return service != null + ? CommandState.Available + : CommandState.Unspecified; + } - public bool ExecuteCommand(FindReferencesCommandArgs args, CommandExecutionContext context) + public bool ExecuteCommand(FindReferencesCommandArgs args, CommandExecutionContext context) + { + var subjectBuffer = args.SubjectBuffer; + + // Get the selection that user has in our buffer (this also works if there + // is no selection and the caret is just at a single position). If we + // can't get the selection, or there are multiple spans for it (i.e. a + // box selection), then don't do anything. + var snapshotSpans = args.TextView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); + if (snapshotSpans.Count == 1) { - var subjectBuffer = args.SubjectBuffer; - - // Get the selection that user has in our buffer (this also works if there - // is no selection and the caret is just at a single position). If we - // can't get the selection, or there are multiple spans for it (i.e. a - // box selection), then don't do anything. - var snapshotSpans = args.TextView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); - if (snapshotSpans.Count == 1) + var selectedSpan = snapshotSpans[0]; + var (document, service) = GetDocumentAndService(subjectBuffer.CurrentSnapshot); + if (document != null) { - var selectedSpan = snapshotSpans[0]; - var (document, service) = GetDocumentAndService(subjectBuffer.CurrentSnapshot); - if (document != null) + // Do a find-refs at the *start* of the selection. That way if the + // user has selected a symbol that has another symbol touching it + // on the right (i.e. Goo++ ), then we'll do the find-refs on the + // symbol selected, not the symbol following. + if (TryExecuteCommand(selectedSpan.Start, document, service)) { - // Do a find-refs at the *start* of the selection. That way if the - // user has selected a symbol that has another symbol touching it - // on the right (i.e. Goo++ ), then we'll do the find-refs on the - // symbol selected, not the symbol following. - if (TryExecuteCommand(selectedSpan.Start, document, service)) - { - return true; - } + return true; } } - - return false; } - private static (Document, IFindUsagesService) GetDocumentAndService(ITextSnapshot snapshot) - { - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - return (document, document?.GetLanguageService()); - } + return false; + } - private bool TryExecuteCommand(int caretPosition, Document document, IFindUsagesService findUsagesService) - { - // See if we're running on a host that can provide streaming results. - // We'll both need a FAR service that can stream results to us, and - // a presenter that can accept streamed results. - if (findUsagesService != null && _streamingPresenter != null) - { - // kick this work off in a fire and forget fashion. Importantly, this means we do - // not pass in any ambient cancellation information as the execution of this command - // will complete and will have no bearing on the computation of the references we compute. - _ = StreamingFindReferencesAsync(document, caretPosition, findUsagesService, _streamingPresenter); - return true; - } + private static (Document, IFindUsagesService) GetDocumentAndService(ITextSnapshot snapshot) + { + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + return (document, document?.GetLanguageService()); + } - return false; + private bool TryExecuteCommand(int caretPosition, Document document, IFindUsagesService findUsagesService) + { + // See if we're running on a host that can provide streaming results. + // We'll both need a FAR service that can stream results to us, and + // a presenter that can accept streamed results. + if (findUsagesService != null && _streamingPresenter != null) + { + // kick this work off in a fire and forget fashion. Importantly, this means we do + // not pass in any ambient cancellation information as the execution of this command + // will complete and will have no bearing on the computation of the references we compute. + _ = StreamingFindReferencesAsync(document, caretPosition, findUsagesService, _streamingPresenter); + return true; } - private async Task StreamingFindReferencesAsync( - Document document, - int caretPosition, - IFindUsagesService findUsagesService, - IStreamingFindUsagesPresenter presenter) + return false; + } + + private async Task StreamingFindReferencesAsync( + Document document, + int caretPosition, + IFindUsagesService findUsagesService, + IStreamingFindUsagesPresenter presenter) + { + try { - try + using var token = _asyncListener.BeginAsyncOperation(nameof(StreamingFindReferencesAsync)); + var classificationOptions = _globalOptions.GetClassificationOptionsProvider(); + + // Let the presented know we're starting a search. It will give us back the context object that the FAR + // service will push results into. This operation is not externally cancellable. Instead, the find refs + // window will cancel it if another request is made to use it. + var (context, cancellationToken) = presenter.StartSearch( + EditorFeaturesResources.Find_References, + new StreamingFindUsagesPresenterOptions() + { + SupportsReferences = true, + IncludeContainingTypeAndMemberColumns = document.Project.SupportsCompilation, + IncludeKindColumn = document.Project.Language != LanguageNames.FSharp + }); + + using (Logger.LogBlock( + FunctionId.CommandHandler_FindAllReference, + KeyValueLogMessage.Create(LogType.UserAction, m => m["type"] = "streaming"), + cancellationToken)) { - using var token = _asyncListener.BeginAsyncOperation(nameof(StreamingFindReferencesAsync)); - var classificationOptions = _globalOptions.GetClassificationOptionsProvider(); - - // Let the presented know we're starting a search. It will give us back the context object that the FAR - // service will push results into. This operation is not externally cancellable. Instead, the find refs - // window will cancel it if another request is made to use it. - var (context, cancellationToken) = presenter.StartSearchWithCustomColumns( - EditorFeaturesResources.Find_References, - supportsReferences: true, - includeContainingTypeAndMemberColumns: document.Project.SupportsCompilation, - includeKindColumn: document.Project.Language != LanguageNames.FSharp); - - using (Logger.LogBlock( - FunctionId.CommandHandler_FindAllReference, - KeyValueLogMessage.Create(LogType.UserAction, m => m["type"] = "streaming"), - cancellationToken)) + try { - try - { - await findUsagesService.FindReferencesAsync(context, document, caretPosition, classificationOptions, cancellationToken).ConfigureAwait(false); - } - finally - { - await context.OnCompletedAsync(cancellationToken).ConfigureAwait(false); - } + await findUsagesService.FindReferencesAsync(context, document, caretPosition, classificationOptions, cancellationToken).ConfigureAwait(false); + } + finally + { + await context.OnCompletedAsync(cancellationToken).ConfigureAwait(false); } } - catch (OperationCanceledException) - { - } - catch (Exception e) when (FatalError.ReportAndCatch(e)) - { - } + } + catch (OperationCanceledException) + { + } + catch (Exception e) when (FatalError.ReportAndCatch(e)) + { } } } diff --git a/src/EditorFeatures/Core/FindUsages/BufferedFindUsagesContext.cs b/src/EditorFeatures/Core/FindUsages/BufferedFindUsagesContext.cs index f0523e4c971b7..16f60a8fca065 100644 --- a/src/EditorFeatures/Core/FindUsages/BufferedFindUsagesContext.cs +++ b/src/EditorFeatures/Core/FindUsages/BufferedFindUsagesContext.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Notification; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; @@ -101,10 +102,10 @@ public async Task AttachToStreamingPresenterAsync(IFindUsagesContext presenterCo await presenterContext.SetSearchTitleAsync(_state.SearchTitle, cancellationToken).ConfigureAwait(false); if (_state.Message != null) - await presenterContext.ReportMessageAsync(_state.Message, cancellationToken).ConfigureAwait(false); + await presenterContext.ReportNoResultsAsync(_state.Message, cancellationToken).ConfigureAwait(false); if (_state.InformationalMessage != null) - await presenterContext.ReportInformationalMessageAsync(_state.InformationalMessage, cancellationToken).ConfigureAwait(false); + await presenterContext.ReportMessageAsync(_state.InformationalMessage, NotificationSeverity.Information, cancellationToken).ConfigureAwait(false); foreach (var definition in _state.Definitions) await presenterContext.OnDefinitionFoundAsync(definition, cancellationToken).ConfigureAwait(false); @@ -148,12 +149,12 @@ async ValueTask IStreamingProgressTracker.ItemsCompletedAsync(int count, Cancell #region IFindUsagesContext - async ValueTask IFindUsagesContext.ReportMessageAsync(string message, CancellationToken cancellationToken) + async ValueTask IFindUsagesContext.ReportNoResultsAsync(string message, CancellationToken cancellationToken) { using var _ = await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false); if (IsSwapped) { - await _streamingPresenterContext.ReportMessageAsync(message, cancellationToken).ConfigureAwait(false); + await _streamingPresenterContext.ReportNoResultsAsync(message, cancellationToken).ConfigureAwait(false); } else { @@ -161,12 +162,12 @@ async ValueTask IFindUsagesContext.ReportMessageAsync(string message, Cancellati } } - async ValueTask IFindUsagesContext.ReportInformationalMessageAsync(string message, CancellationToken cancellationToken) + async ValueTask IFindUsagesContext.ReportMessageAsync(string message, NotificationSeverity severity, CancellationToken cancellationToken) { using var _ = await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false); if (IsSwapped) { - await _streamingPresenterContext.ReportInformationalMessageAsync(message, cancellationToken).ConfigureAwait(false); + await _streamingPresenterContext.ReportMessageAsync(message, severity, cancellationToken).ConfigureAwait(false); } else { diff --git a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.FormatDocument.cs b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.FormatDocument.cs index 471ceee106ea5..5997c71ab7afb 100644 --- a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.FormatDocument.cs +++ b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.FormatDocument.cs @@ -8,38 +8,37 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Formatting +namespace Microsoft.CodeAnalysis.Formatting; + +internal partial class FormatCommandHandler { - internal partial class FormatCommandHandler + public CommandState GetCommandState(FormatDocumentCommandArgs args) + => GetCommandState(args.SubjectBuffer); + + public bool ExecuteCommand(FormatDocumentCommandArgs args, CommandExecutionContext context) { - public CommandState GetCommandState(FormatDocumentCommandArgs args) - => GetCommandState(args.SubjectBuffer); + if (!CanExecuteCommand(args.SubjectBuffer)) + { + return false; + } - public bool ExecuteCommand(FormatDocumentCommandArgs args, CommandExecutionContext context) + var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) { - if (!CanExecuteCommand(args.SubjectBuffer)) - { - return false; - } - - var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return false; - } - - var formattingService = document.GetLanguageService(); - if (formattingService == null || !formattingService.SupportsFormatDocument) - { - return false; - } - - using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Formatting_document)) - { - Format(args.TextView, args.SubjectBuffer, document, selectionOpt: null, context.OperationContext.UserCancellationToken); - } - - return true; + return false; } + + var formattingService = document.GetLanguageService(); + if (formattingService == null || !formattingService.SupportsFormatDocument) + { + return false; + } + + using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Formatting_document)) + { + Format(args.TextView, args.SubjectBuffer, document, selectionOpt: null, context.OperationContext.UserCancellationToken); + } + + return true; } } diff --git a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.FormatSelection.cs b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.FormatSelection.cs index 68497fef6c581..44a40a53464a4 100644 --- a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.FormatSelection.cs +++ b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.FormatSelection.cs @@ -11,60 +11,59 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Formatting +namespace Microsoft.CodeAnalysis.Formatting; + +internal partial class FormatCommandHandler { - internal partial class FormatCommandHandler + public CommandState GetCommandState(FormatSelectionCommandArgs args) + => GetCommandState(args.SubjectBuffer); + + public bool ExecuteCommand(FormatSelectionCommandArgs args, CommandExecutionContext context) + => TryExecuteCommand(args, context); + + private bool TryExecuteCommand(FormatSelectionCommandArgs args, CommandExecutionContext context) { - public CommandState GetCommandState(FormatSelectionCommandArgs args) - => GetCommandState(args.SubjectBuffer); + if (!args.SubjectBuffer.CanApplyChangeDocumentToWorkspace()) + { + return false; + } - public bool ExecuteCommand(FormatSelectionCommandArgs args, CommandExecutionContext context) - => TryExecuteCommand(args, context); + var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + { + return false; + } - private bool TryExecuteCommand(FormatSelectionCommandArgs args, CommandExecutionContext context) + var formattingService = document.GetLanguageService(); + if (formattingService == null || !formattingService.SupportsFormatSelection) { - if (!args.SubjectBuffer.CanApplyChangeDocumentToWorkspace()) - { - return false; - } + return false; + } - var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return false; - } + using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Formatting_currently_selected_text)) + { + var buffer = args.SubjectBuffer; - var formattingService = document.GetLanguageService(); - if (formattingService == null || !formattingService.SupportsFormatSelection) + // we only support single selection for now + var selection = args.TextView.Selection.GetSnapshotSpansOnBuffer(buffer); + if (selection.Count != 1) { return false; } - using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Formatting_currently_selected_text)) - { - var buffer = args.SubjectBuffer; - - // we only support single selection for now - var selection = args.TextView.Selection.GetSnapshotSpansOnBuffer(buffer); - if (selection.Count != 1) - { - return false; - } + var formattingSpan = selection[0].Span.ToTextSpan(); - var formattingSpan = selection[0].Span.ToTextSpan(); + Format(args.TextView, buffer, document, formattingSpan, context.OperationContext.UserCancellationToken); - Format(args.TextView, buffer, document, formattingSpan, context.OperationContext.UserCancellationToken); + // make behavior same as dev12. + // make sure we set selection back and set caret position at the end of selection + // we can delete this code once razor side fixes a bug where it depends on this behavior (dev12) on formatting. + var currentSelection = selection[0].TranslateTo(args.SubjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeExclusive); + args.TextView.SetSelection(currentSelection); + args.TextView.TryMoveCaretToAndEnsureVisible(currentSelection.End, ensureSpanVisibleOptions: EnsureSpanVisibleOptions.MinimumScroll); - // make behavior same as dev12. - // make sure we set selection back and set caret position at the end of selection - // we can delete this code once razor side fixes a bug where it depends on this behavior (dev12) on formatting. - var currentSelection = selection[0].TranslateTo(args.SubjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeExclusive); - args.TextView.SetSelection(currentSelection); - args.TextView.TryMoveCaretToAndEnsureVisible(currentSelection.End, ensureSpanVisibleOptions: EnsureSpanVisibleOptions.MinimumScroll); - - // We have handled this command - return true; - } + // We have handled this command + return true; } } } diff --git a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.Paste.cs b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.Paste.cs index f5a0788d9d27c..38c16c1011436 100644 --- a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.Paste.cs +++ b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.Paste.cs @@ -16,76 +16,75 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Formatting +namespace Microsoft.CodeAnalysis.Formatting; + +internal partial class FormatCommandHandler { - internal partial class FormatCommandHandler + public CommandState GetCommandState(PasteCommandArgs args, Func nextHandler) + => nextHandler(); + + public void ExecuteCommand(PasteCommandArgs args, Action nextHandler, CommandExecutionContext context) { - public CommandState GetCommandState(PasteCommandArgs args, Func nextHandler) - => nextHandler(); + using var _ = context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Formatting_pasted_text); + var caretPosition = args.TextView.GetCaretPoint(args.SubjectBuffer); + + nextHandler(); - public void ExecuteCommand(PasteCommandArgs args, Action nextHandler, CommandExecutionContext context) + var cancellationToken = context.OperationContext.UserCancellationToken; + if (cancellationToken.IsCancellationRequested) { - using var _ = context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Formatting_pasted_text); - var caretPosition = args.TextView.GetCaretPoint(args.SubjectBuffer); - - nextHandler(); - - var cancellationToken = context.OperationContext.UserCancellationToken; - if (cancellationToken.IsCancellationRequested) - { - return; - } - - try - { - ExecuteCommandWorker(args, caretPosition, cancellationToken); - } - catch (OperationCanceledException) - { - // According to Editor command handler API guidelines, it's best if we return early if cancellation - // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already - // calling nextHandler(). - } + return; } - private void ExecuteCommandWorker(PasteCommandArgs args, SnapshotPoint? caretPosition, CancellationToken cancellationToken) + try + { + ExecuteCommandWorker(args, caretPosition, cancellationToken); + } + catch (OperationCanceledException) + { + // According to Editor command handler API guidelines, it's best if we return early if cancellation + // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already + // calling nextHandler(). + } + } + + private void ExecuteCommandWorker(PasteCommandArgs args, SnapshotPoint? caretPosition, CancellationToken cancellationToken) + { + if (!caretPosition.HasValue) + return; + + var subjectBuffer = args.SubjectBuffer; + if (!subjectBuffer.TryGetWorkspace(out var workspace) || + !workspace.CanApplyChange(ApplyChangesKind.ChangeDocument)) { - if (!caretPosition.HasValue) - return; - - var subjectBuffer = args.SubjectBuffer; - if (!subjectBuffer.TryGetWorkspace(out var workspace) || - !workspace.CanApplyChange(ApplyChangesKind.ChangeDocument)) - { - return; - } - - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return; - - if (!_globalOptions.GetOption(FormattingOptionsStorage.FormatOnPaste, document.Project.Language)) - return; - - var solution = document.Project.Solution; - var services = solution.Services; - var formattingRuleService = services.GetService(); - if (formattingRuleService != null && formattingRuleService.ShouldNotFormatOrCommitOnPaste(document.Id)) - return; - - var formattingService = document.GetLanguageService(); - if (formattingService == null || !formattingService.SupportsFormatOnPaste) - return; - - var trackingSpan = caretPosition.Value.Snapshot.CreateTrackingSpan(caretPosition.Value.Position, 0, SpanTrackingMode.EdgeInclusive); - var span = trackingSpan.GetSpan(subjectBuffer.CurrentSnapshot).Span.ToTextSpan(); - - // Note: C# always completes synchronously, TypeScript is async - var changes = formattingService.GetFormattingChangesOnPasteAsync(document, subjectBuffer, span, cancellationToken).WaitAndGetResult(cancellationToken); - if (changes.IsEmpty) - return; - - subjectBuffer.ApplyChanges(changes); + return; } + + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return; + + if (!_globalOptions.GetOption(FormattingOptionsStorage.FormatOnPaste, document.Project.Language)) + return; + + var solution = document.Project.Solution; + var services = solution.Services; + var formattingRuleService = services.GetService(); + if (formattingRuleService != null && formattingRuleService.ShouldNotFormatOrCommitOnPaste(document.Id)) + return; + + var formattingService = document.GetLanguageService(); + if (formattingService == null || !formattingService.SupportsFormatOnPaste) + return; + + var trackingSpan = caretPosition.Value.Snapshot.CreateTrackingSpan(caretPosition.Value.Position, 0, SpanTrackingMode.EdgeInclusive); + var span = trackingSpan.GetSpan(subjectBuffer.CurrentSnapshot).Span.ToTextSpan(); + + // Note: C# always completes synchronously, TypeScript is async + var changes = formattingService.GetFormattingChangesOnPasteAsync(document, subjectBuffer, span, cancellationToken).WaitAndGetResult(cancellationToken); + if (changes.IsEmpty) + return; + + subjectBuffer.ApplyChanges(changes); } } diff --git a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.ReturnKey.cs b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.ReturnKey.cs index 6339ef130206f..1640bdd2bea5b 100644 --- a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.ReturnKey.cs +++ b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.ReturnKey.cs @@ -6,14 +6,13 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Formatting +namespace Microsoft.CodeAnalysis.Formatting; + +internal partial class FormatCommandHandler { - internal partial class FormatCommandHandler - { - public CommandState GetCommandState(ReturnKeyCommandArgs args, Func nextHandler) - => nextHandler(); + public CommandState GetCommandState(ReturnKeyCommandArgs args, Func nextHandler) + => nextHandler(); - public void ExecuteCommand(ReturnKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) - => ExecuteReturnOrTypeCommand(args, nextHandler, context.OperationContext.UserCancellationToken); - } + public void ExecuteCommand(ReturnKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) + => ExecuteReturnOrTypeCommand(args, nextHandler, context.OperationContext.UserCancellationToken); } diff --git a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.TypeChar.cs b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.TypeChar.cs index 18a2623a34b3d..9f6c93fb1a9a5 100644 --- a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.TypeChar.cs +++ b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.TypeChar.cs @@ -6,14 +6,13 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Formatting +namespace Microsoft.CodeAnalysis.Formatting; + +internal partial class FormatCommandHandler { - internal partial class FormatCommandHandler - { - public CommandState GetCommandState(TypeCharCommandArgs args, Func nextHandler) - => nextHandler(); + public CommandState GetCommandState(TypeCharCommandArgs args, Func nextHandler) + => nextHandler(); - public void ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context) - => ExecuteReturnOrTypeCommand(args, nextHandler, context.OperationContext.UserCancellationToken); - } + public void ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context) + => ExecuteReturnOrTypeCommand(args, nextHandler, context.OperationContext.UserCancellationToken); } diff --git a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.cs b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.cs index 29751cceddf91..2bfb4a68365fb 100644 --- a/src/EditorFeatures/Core/Formatting/FormatCommandHandler.cs +++ b/src/EditorFeatures/Core/Formatting/FormatCommandHandler.cs @@ -27,181 +27,180 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Formatting +namespace Microsoft.CodeAnalysis.Formatting; + +[Export] +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.RoslynContentType)] +[Name(PredefinedCommandHandlerNames.FormatDocument)] +[Order(After = PredefinedCommandHandlerNames.Rename)] +[Order(Before = PredefinedCommandHandlerNames.StringCopyPaste)] +[Order(Before = PredefinedCompletionNames.CompletionCommandHandler)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal partial class FormatCommandHandler( + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + IGlobalOptionService globalOptions) : + ICommandHandler, + ICommandHandler, + IChainedCommandHandler, + IChainedCommandHandler, + IChainedCommandHandler { - [Export] - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.RoslynContentType)] - [Name(PredefinedCommandHandlerNames.FormatDocument)] - [Order(After = PredefinedCommandHandlerNames.Rename)] - [Order(Before = PredefinedCommandHandlerNames.StringCopyPaste)] - [Order(Before = PredefinedCompletionNames.CompletionCommandHandler)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal partial class FormatCommandHandler( - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - IGlobalOptionService globalOptions) : - ICommandHandler, - ICommandHandler, - IChainedCommandHandler, - IChainedCommandHandler, - IChainedCommandHandler - { - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; + private readonly IGlobalOptionService _globalOptions = globalOptions; - public string DisplayName => EditorFeaturesResources.Automatic_Formatting; + public string DisplayName => EditorFeaturesResources.Automatic_Formatting; - private void Format(ITextView textView, ITextBuffer textBuffer, Document document, TextSpan? selectionOpt, CancellationToken cancellationToken) - { - var formattingService = document.GetRequiredLanguageService(); + private void Format(ITextView textView, ITextBuffer textBuffer, Document document, TextSpan? selectionOpt, CancellationToken cancellationToken) + { + var formattingService = document.GetRequiredLanguageService(); - using (Logger.LogBlock(FunctionId.CommandHandler_FormatCommand, KeyValueLogMessage.Create(LogType.UserAction, m => m["Span"] = selectionOpt?.Length ?? -1), cancellationToken)) - using (var transaction = CreateEditTransaction(textView, EditorFeaturesResources.Formatting)) + using (Logger.LogBlock(FunctionId.CommandHandler_FormatCommand, KeyValueLogMessage.Create(LogType.UserAction, m => m["Span"] = selectionOpt?.Length ?? -1), cancellationToken)) + using (var transaction = CreateEditTransaction(textView, EditorFeaturesResources.Formatting)) + { + // Note: C# always completes synchronously, TypeScript is async + var changes = formattingService.GetFormattingChangesAsync(document, textBuffer, selectionOpt, cancellationToken).WaitAndGetResult(cancellationToken); + if (changes.IsEmpty) { - // Note: C# always completes synchronously, TypeScript is async - var changes = formattingService.GetFormattingChangesAsync(document, textBuffer, selectionOpt, cancellationToken).WaitAndGetResult(cancellationToken); - if (changes.IsEmpty) - { - return; - } + return; + } - if (selectionOpt.HasValue) - { - var ruleFactory = document.Project.Solution.Services.GetRequiredService(); - changes = ruleFactory.FilterFormattedChanges(document.Id, selectionOpt.Value, changes).ToImmutableArray(); - } + if (selectionOpt.HasValue) + { + var ruleFactory = document.Project.Solution.Services.GetRequiredService(); + changes = ruleFactory.FilterFormattedChanges(document.Id, selectionOpt.Value, changes).ToImmutableArray(); + } - if (!changes.IsEmpty) + if (!changes.IsEmpty) + { + using (Logger.LogBlock(FunctionId.Formatting_ApplyResultToBuffer, cancellationToken)) { - using (Logger.LogBlock(FunctionId.Formatting_ApplyResultToBuffer, cancellationToken)) - { - textBuffer.ApplyChanges(changes); - } + textBuffer.ApplyChanges(changes); } - - transaction.Complete(); } + + transaction.Complete(); } + } - private static bool CanExecuteCommand(ITextBuffer buffer) - => buffer.CanApplyChangeDocumentToWorkspace(); + private static bool CanExecuteCommand(ITextBuffer buffer) + => buffer.CanApplyChangeDocumentToWorkspace(); - private static CommandState GetCommandState(ITextBuffer buffer) - => CanExecuteCommand(buffer) ? CommandState.Available : CommandState.Unspecified; + private static CommandState GetCommandState(ITextBuffer buffer) + => CanExecuteCommand(buffer) ? CommandState.Available : CommandState.Unspecified; - public void ExecuteReturnOrTypeCommand(EditorCommandArgs args, Action nextHandler, CancellationToken cancellationToken) + public void ExecuteReturnOrTypeCommand(EditorCommandArgs args, Action nextHandler, CancellationToken cancellationToken) + { + // run next handler first so that editor has chance to put the return into the buffer first. + nextHandler(); + if (cancellationToken.IsCancellationRequested) { - // run next handler first so that editor has chance to put the return into the buffer first. - nextHandler(); - if (cancellationToken.IsCancellationRequested) - { - return; - } - - try - { - ExecuteReturnOrTypeCommandWorker(args, cancellationToken); - } - catch (OperationCanceledException) - { - // According to Editor command handler API guidelines, it's best if we return early if cancellation - // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already - // calling nextHandler(). - } + return; } - private void ExecuteReturnOrTypeCommandWorker(EditorCommandArgs args, CancellationToken cancellationToken) + try { - var textView = args.TextView; - var subjectBuffer = args.SubjectBuffer; - if (!CanExecuteCommand(subjectBuffer)) - { - return; - } - - var caretPosition = textView.GetCaretPoint(args.SubjectBuffer); - if (!caretPosition.HasValue) - { - return; - } + ExecuteReturnOrTypeCommandWorker(args, cancellationToken); + } + catch (OperationCanceledException) + { + // According to Editor command handler API guidelines, it's best if we return early if cancellation + // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already + // calling nextHandler(). + } + } - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return; - } + private void ExecuteReturnOrTypeCommandWorker(EditorCommandArgs args, CancellationToken cancellationToken) + { + var textView = args.TextView; + var subjectBuffer = args.SubjectBuffer; + if (!CanExecuteCommand(subjectBuffer)) + { + return; + } - var service = document.GetLanguageService(); - if (service == null) - { - return; - } + var caretPosition = textView.GetCaretPoint(args.SubjectBuffer); + if (!caretPosition.HasValue) + { + return; + } - IList? textChanges; + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + { + return; + } - // save current caret position - if (args is ReturnKeyCommandArgs) - { - if (!service.SupportsFormatOnReturn) - { - return; - } + var service = document.GetLanguageService(); + if (service == null) + { + return; + } - // Note: C# always completes synchronously, TypeScript is async - textChanges = service.GetFormattingChangesOnReturnAsync(document, caretPosition.Value, cancellationToken).WaitAndGetResult(cancellationToken); - } - else if (args is TypeCharCommandArgs typeCharArgs) - { - if (!service.SupportsFormattingOnTypedCharacter(document, typeCharArgs.TypedChar)) - { - return; - } + IList? textChanges; - // Note: C# always completes synchronously, TypeScript is async - textChanges = service.GetFormattingChangesAsync( - document, typeCharArgs.SubjectBuffer, typeCharArgs.TypedChar, caretPosition.Value, cancellationToken).WaitAndGetResult(cancellationToken); - } - else + // save current caret position + if (args is ReturnKeyCommandArgs) + { + if (!service.SupportsFormatOnReturn) { - throw ExceptionUtilities.UnexpectedValue(args); + return; } - if (textChanges == null || textChanges.Count == 0) + // Note: C# always completes synchronously, TypeScript is async + textChanges = service.GetFormattingChangesOnReturnAsync(document, caretPosition.Value, cancellationToken).WaitAndGetResult(cancellationToken); + } + else if (args is TypeCharCommandArgs typeCharArgs) + { + if (!service.SupportsFormattingOnTypedCharacter(document, typeCharArgs.TypedChar)) { return; } - using (var transaction = CreateEditTransaction(textView, EditorFeaturesResources.Automatic_Formatting)) - { - transaction.MergePolicy = AutomaticCodeChangeMergePolicy.Instance; - subjectBuffer.ApplyChanges(textChanges); - transaction.Complete(); - } + // Note: C# always completes synchronously, TypeScript is async + textChanges = service.GetFormattingChangesAsync( + document, typeCharArgs.SubjectBuffer, typeCharArgs.TypedChar, caretPosition.Value, cancellationToken).WaitAndGetResult(cancellationToken); + } + else + { + throw ExceptionUtilities.UnexpectedValue(args); + } - // get new caret position after formatting - var newCaretPositionMarker = args.TextView.GetCaretPoint(args.SubjectBuffer); - if (!newCaretPositionMarker.HasValue) - { - return; - } + if (textChanges == null || textChanges.Count == 0) + { + return; + } - var snapshotAfterFormatting = subjectBuffer.CurrentSnapshot; + using (var transaction = CreateEditTransaction(textView, EditorFeaturesResources.Automatic_Formatting)) + { + transaction.MergePolicy = AutomaticCodeChangeMergePolicy.Instance; + subjectBuffer.ApplyChanges(textChanges); + transaction.Complete(); + } - var oldCaretPosition = caretPosition.Value.TranslateTo(snapshotAfterFormatting, PointTrackingMode.Negative); - var newCaretPosition = newCaretPositionMarker.Value.TranslateTo(snapshotAfterFormatting, PointTrackingMode.Negative); - if (oldCaretPosition.Position == newCaretPosition.Position) - { - return; - } + // get new caret position after formatting + var newCaretPositionMarker = args.TextView.GetCaretPoint(args.SubjectBuffer); + if (!newCaretPositionMarker.HasValue) + { + return; + } + + var snapshotAfterFormatting = subjectBuffer.CurrentSnapshot; - // caret has moved to wrong position, move it back to correct position - args.TextView.TryMoveCaretToAndEnsureVisible(oldCaretPosition); + var oldCaretPosition = caretPosition.Value.TranslateTo(snapshotAfterFormatting, PointTrackingMode.Negative); + var newCaretPosition = newCaretPositionMarker.Value.TranslateTo(snapshotAfterFormatting, PointTrackingMode.Negative); + if (oldCaretPosition.Position == newCaretPosition.Position) + { + return; } - private CaretPreservingEditTransaction CreateEditTransaction(ITextView view, string description) - => new(description, view, _undoHistoryRegistry, _editorOperationsFactoryService); + // caret has moved to wrong position, move it back to correct position + args.TextView.TryMoveCaretToAndEnsureVisible(oldCaretPosition); } + + private CaretPreservingEditTransaction CreateEditTransaction(ITextView view, string description) + => new(description, view, _undoHistoryRegistry, _editorOperationsFactoryService); } diff --git a/src/EditorFeatures/Core/Formatting/FormattingOptionsStorage.cs b/src/EditorFeatures/Core/Formatting/FormattingOptionsStorage.cs index f186c34faa6d7..341a4c6ded253 100644 --- a/src/EditorFeatures/Core/Formatting/FormattingOptionsStorage.cs +++ b/src/EditorFeatures/Core/Formatting/FormattingOptionsStorage.cs @@ -4,13 +4,12 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Formatting +namespace Microsoft.CodeAnalysis.Formatting; + +internal sealed class FormattingOptionsStorage { - internal sealed class FormattingOptionsStorage - { - public static readonly PerLanguageOption2 FormatOnPaste = - new("dotnet_format_on_paste", defaultValue: true); + public static readonly PerLanguageOption2 FormatOnPaste = + new("dotnet_format_on_paste", defaultValue: true); - public static readonly Option2 FormatOnSave = new("dotnet_format_on_save", defaultValue: true); - } + public static readonly Option2 FormatOnSave = new("dotnet_format_on_save", defaultValue: true); } diff --git a/src/EditorFeatures/Core/Formatting/IFormattingInteractionService.cs b/src/EditorFeatures/Core/Formatting/IFormattingInteractionService.cs index 52eb780536435..725a154e2b227 100644 --- a/src/EditorFeatures/Core/Formatting/IFormattingInteractionService.cs +++ b/src/EditorFeatures/Core/Formatting/IFormattingInteractionService.cs @@ -10,42 +10,41 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Formatting +namespace Microsoft.CodeAnalysis.Formatting; + +internal interface IFormattingInteractionService : ILanguageService { - internal interface IFormattingInteractionService : ILanguageService - { - bool SupportsFormatDocument { get; } - bool SupportsFormatSelection { get; } - bool SupportsFormatOnPaste { get; } - bool SupportsFormatOnReturn { get; } - - /// - /// True if this service would like to format the document based on the user typing the - /// provided character. - /// - bool SupportsFormattingOnTypedCharacter(Document document, char ch); - - /// - /// Returns the text changes necessary to format the document. If is provided, - /// only the text changes necessary to format that span are needed. - /// - Task> GetFormattingChangesAsync(Document document, ITextBuffer textBuffer, TextSpan? textSpan, CancellationToken cancellationToken); - - /// - /// Returns the text changes necessary to format the document on paste operation. - /// - Task> GetFormattingChangesOnPasteAsync(Document document, ITextBuffer textBuffer, TextSpan textSpan, CancellationToken cancellationToken); - - /// - /// Returns the text changes necessary to format the document after the user enters a - /// character. The position provided is the position of the caret in the document after - /// the character been inserted into the document. - /// - Task> GetFormattingChangesAsync(Document document, ITextBuffer textBuffer, char typedChar, int position, CancellationToken cancellationToken); - - /// - /// Returns the text changes necessary to format the document after the user enters a Return - /// The position provided is the position of the caret in the document after Return. - Task> GetFormattingChangesOnReturnAsync(Document document, int position, CancellationToken cancellationToken); - } + bool SupportsFormatDocument { get; } + bool SupportsFormatSelection { get; } + bool SupportsFormatOnPaste { get; } + bool SupportsFormatOnReturn { get; } + + /// + /// True if this service would like to format the document based on the user typing the + /// provided character. + /// + bool SupportsFormattingOnTypedCharacter(Document document, char ch); + + /// + /// Returns the text changes necessary to format the document. If is provided, + /// only the text changes necessary to format that span are needed. + /// + Task> GetFormattingChangesAsync(Document document, ITextBuffer textBuffer, TextSpan? textSpan, CancellationToken cancellationToken); + + /// + /// Returns the text changes necessary to format the document on paste operation. + /// + Task> GetFormattingChangesOnPasteAsync(Document document, ITextBuffer textBuffer, TextSpan textSpan, CancellationToken cancellationToken); + + /// + /// Returns the text changes necessary to format the document after the user enters a + /// character. The position provided is the position of the caret in the document after + /// the character been inserted into the document. + /// + Task> GetFormattingChangesAsync(Document document, ITextBuffer textBuffer, char typedChar, int position, CancellationToken cancellationToken); + + /// + /// Returns the text changes necessary to format the document after the user enters a Return + /// The position provided is the position of the caret in the document after Return. + Task> GetFormattingChangesOnReturnAsync(Document document, int position, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/GoToBase/GoToBaseCommandHandler.cs b/src/EditorFeatures/Core/GoToBase/GoToBaseCommandHandler.cs index fdd4ad9c8b923..d8d091263f367 100644 --- a/src/EditorFeatures/Core/GoToBase/GoToBaseCommandHandler.cs +++ b/src/EditorFeatures/Core/GoToBase/GoToBaseCommandHandler.cs @@ -20,31 +20,30 @@ using Microsoft.VisualStudio.Utilities; using VSCommanding = Microsoft.VisualStudio.Commanding; -namespace Microsoft.CodeAnalysis.GoToBase +namespace Microsoft.CodeAnalysis.GoToBase; + +[Export(typeof(VSCommanding.ICommandHandler))] +[ContentType(ContentTypeNames.RoslynContentType)] +[Name(PredefinedCommandHandlerNames.GoToBase)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class GoToBaseCommandHandler( + IThreadingContext threadingContext, + IStreamingFindUsagesPresenter streamingPresenter, + IUIThreadOperationExecutor uiThreadOperationExecutor, + IAsynchronousOperationListenerProvider listenerProvider, + IGlobalOptionService globalOptions) : AbstractGoToCommandHandler(threadingContext, + streamingPresenter, + uiThreadOperationExecutor, + listenerProvider.GetListener(FeatureAttribute.GoToBase), + globalOptions) { - [Export(typeof(VSCommanding.ICommandHandler))] - [ContentType(ContentTypeNames.RoslynContentType)] - [Name(PredefinedCommandHandlerNames.GoToBase)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class GoToBaseCommandHandler( - IThreadingContext threadingContext, - IStreamingFindUsagesPresenter streamingPresenter, - IUIThreadOperationExecutor uiThreadOperationExecutor, - IAsynchronousOperationListenerProvider listenerProvider, - IGlobalOptionService globalOptions) : AbstractGoToCommandHandler(threadingContext, - streamingPresenter, - uiThreadOperationExecutor, - listenerProvider.GetListener(FeatureAttribute.GoToBase), - globalOptions) - { - public override string DisplayName => EditorFeaturesResources.Go_To_Base; + public override string DisplayName => EditorFeaturesResources.Go_To_Base; - protected override string ScopeDescription => EditorFeaturesResources.Locating_bases; - protected override FunctionId FunctionId => FunctionId.CommandHandler_GoToBase; + protected override string ScopeDescription => EditorFeaturesResources.Locating_bases; + protected override FunctionId FunctionId => FunctionId.CommandHandler_GoToBase; - protected override Task FindActionAsync(IFindUsagesContext context, Document document, int caretPosition, CancellationToken cancellationToken) - => document.GetRequiredLanguageService() - .FindBasesAsync(context, document, caretPosition, ClassificationOptionsProvider, cancellationToken); - } + protected override Task FindActionAsync(IFindUsagesContext context, Document document, int caretPosition, CancellationToken cancellationToken) + => document.GetRequiredLanguageService() + .FindBasesAsync(context, document, caretPosition, ClassificationOptionsProvider, cancellationToken); } diff --git a/src/EditorFeatures/Core/GoToDefinition/AbstractGoToCommandHandler`2.cs b/src/EditorFeatures/Core/GoToDefinition/AbstractGoToCommandHandler`2.cs index 5ecb0de0b743c..b1f8f6233b662 100644 --- a/src/EditorFeatures/Core/GoToDefinition/AbstractGoToCommandHandler`2.cs +++ b/src/EditorFeatures/Core/GoToDefinition/AbstractGoToCommandHandler`2.cs @@ -16,6 +16,7 @@ using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.Notification; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; @@ -221,7 +222,7 @@ private async Task PresentResultsInStreamingPresenterAsync( { var cancellationToken = cancellationTokenSource.Token; await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var (presenterContext, presenterCancellationToken) = _streamingPresenter.StartSearch(DisplayName, supportsReferences: false); + var (presenterContext, presenterCancellationToken) = _streamingPresenter.StartSearch(DisplayName, StreamingFindUsagesPresenterOptions.Default); try { @@ -262,8 +263,8 @@ private async Task FindResultsAsync( var isFullyLoaded = await service.IsFullyLoadedAsync(cancellationToken).ConfigureAwait(false); if (!isFullyLoaded) { - await findContext.ReportInformationalMessageAsync( - EditorFeaturesResources.The_results_may_be_incomplete_due_to_the_solution_still_loading_projects, cancellationToken).ConfigureAwait(false); + await findContext.ReportMessageAsync( + EditorFeaturesResources.The_results_may_be_incomplete_due_to_the_solution_still_loading_projects, NotificationSeverity.Information, cancellationToken).ConfigureAwait(false); } // We were able to find the doc prior to loading the workspace (or else we would not have the service). diff --git a/src/EditorFeatures/Core/GoToDefinition/GoToDefinitionCommandHandler.cs b/src/EditorFeatures/Core/GoToDefinition/GoToDefinitionCommandHandler.cs index 47c8f73decf3c..66160ae4e2fb9 100644 --- a/src/EditorFeatures/Core/GoToDefinition/GoToDefinitionCommandHandler.cs +++ b/src/EditorFeatures/Core/GoToDefinition/GoToDefinitionCommandHandler.cs @@ -23,125 +23,124 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.GoToDefinition +namespace Microsoft.CodeAnalysis.GoToDefinition; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.RoslynContentType)] +[Name(PredefinedCommandHandlerNames.GoToDefinition)] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal class GoToDefinitionCommandHandler( + IGlobalOptionService globalOptionService, + IThreadingContext threadingContext, + IUIThreadOperationExecutor executor, + IAsynchronousOperationListenerProvider listenerProvider) : + ICommandHandler { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.RoslynContentType)] - [Name(PredefinedCommandHandlerNames.GoToDefinition)] - [method: ImportingConstructor] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal class GoToDefinitionCommandHandler( - IGlobalOptionService globalOptionService, - IThreadingContext threadingContext, - IUIThreadOperationExecutor executor, - IAsynchronousOperationListenerProvider listenerProvider) : - ICommandHandler + private readonly IGlobalOptionService _globalOptionService = globalOptionService; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IUIThreadOperationExecutor _executor = executor; + private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.GoToDefinition); + + public string DisplayName => EditorFeaturesResources.Go_to_Definition; + + private static (Document?, IDefinitionLocationService?) GetDocumentAndService(ITextSnapshot snapshot) { - private readonly IGlobalOptionService _globalOptionService = globalOptionService; - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IUIThreadOperationExecutor _executor = executor; - private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.GoToDefinition); + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + return (document, document?.GetLanguageService()); + } - public string DisplayName => EditorFeaturesResources.Go_to_Definition; + public CommandState GetCommandState(GoToDefinitionCommandArgs args) + { + var (_, service) = GetDocumentAndService(args.SubjectBuffer.CurrentSnapshot); + return service != null + ? CommandState.Available + : CommandState.Unspecified; + } - private static (Document?, IDefinitionLocationService?) GetDocumentAndService(ITextSnapshot snapshot) - { - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - return (document, document?.GetLanguageService()); - } + public bool ExecuteCommand(GoToDefinitionCommandArgs args, CommandExecutionContext context) + { + var subjectBuffer = args.SubjectBuffer; + var (document, service) = GetDocumentAndService(subjectBuffer.CurrentSnapshot); + + if (service == null) + return false; + + Contract.ThrowIfNull(document); + + // In Live Share, typescript exports a gotodefinition service that returns no results and prevents the LSP + // client from handling the request. So prevent the local service from handling goto def commands in the + // remote workspace. This can be removed once typescript implements LSP support for goto def. + if (subjectBuffer.IsInLspEditorContext()) + return false; + + // If the file is empty, there's nothing to be on that we can goto-def on. This also ensures that we can + // create an appropriate non-empty tracking span later on. + var currentSnapshot = subjectBuffer.CurrentSnapshot; + if (currentSnapshot.Length == 0) + return false; + + // If there's a selection, use the starting point of the selection as the invocation point. Otherwise, just + // pick wherever the caret is exactly at. + var caretPos = + args.TextView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer).FirstOrNull()?.Start ?? + args.TextView.GetCaretPoint(subjectBuffer); + + if (!caretPos.HasValue) + return false; + + // We're showing our own UI, ensure the editor doesn't show anything itself. + context.OperationContext.TakeOwnership(); + var token = _listener.BeginAsyncOperation(nameof(ExecuteCommand)); + ExecuteAsynchronouslyAsync(args, document, service, caretPos.Value) + .ReportNonFatalErrorAsync() + .CompletesAsyncOperation(token); + + return true; + } - public CommandState GetCommandState(GoToDefinitionCommandArgs args) - { - var (_, service) = GetDocumentAndService(args.SubjectBuffer.CurrentSnapshot); - return service != null - ? CommandState.Available - : CommandState.Unspecified; - } + private async Task ExecuteAsynchronouslyAsync( + GoToDefinitionCommandArgs args, Document document, IDefinitionLocationService service, SnapshotPoint position) + { + bool succeeded; + + var indicatorFactory = document.Project.Solution.Services.GetRequiredService(); + + // TODO: prior logic was to get a tracking span of length 1 here. Preserving that, though it's unclear if + // that is necessary for the BWI to work properly. + Contract.ThrowIfTrue(position.Snapshot.Length == 0); + var applicableToSpan = position < position.Snapshot.Length + ? new SnapshotSpan(position, position + 1) + : new SnapshotSpan(position - 1, position); - public bool ExecuteCommand(GoToDefinitionCommandArgs args, CommandExecutionContext context) + using (var backgroundIndicator = indicatorFactory.Create( + args.TextView, applicableToSpan, + EditorFeaturesResources.Navigating_to_definition)) { - var subjectBuffer = args.SubjectBuffer; - var (document, service) = GetDocumentAndService(subjectBuffer.CurrentSnapshot); - - if (service == null) - return false; - - Contract.ThrowIfNull(document); - - // In Live Share, typescript exports a gotodefinition service that returns no results and prevents the LSP - // client from handling the request. So prevent the local service from handling goto def commands in the - // remote workspace. This can be removed once typescript implements LSP support for goto def. - if (subjectBuffer.IsInLspEditorContext()) - return false; - - // If the file is empty, there's nothing to be on that we can goto-def on. This also ensures that we can - // create an appropriate non-empty tracking span later on. - var currentSnapshot = subjectBuffer.CurrentSnapshot; - if (currentSnapshot.Length == 0) - return false; - - // If there's a selection, use the starting point of the selection as the invocation point. Otherwise, just - // pick wherever the caret is exactly at. - var caretPos = - args.TextView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer).FirstOrNull()?.Start ?? - args.TextView.GetCaretPoint(subjectBuffer); - - if (!caretPos.HasValue) - return false; - - // We're showing our own UI, ensure the editor doesn't show anything itself. - context.OperationContext.TakeOwnership(); - var token = _listener.BeginAsyncOperation(nameof(ExecuteCommand)); - ExecuteAsynchronouslyAsync(args, document, service, caretPos.Value) - .ReportNonFatalErrorAsync() - .CompletesAsyncOperation(token); - - return true; + var cancellationToken = backgroundIndicator.UserCancellationToken; + + // determine the location first. + var definitionLocation = await service.GetDefinitionLocationAsync( + document, position, cancellationToken).ConfigureAwait(false); + + // make sure that if our background indicator got canceled, that we do not still perform the navigation. + if (backgroundIndicator.UserCancellationToken.IsCancellationRequested) + return; + + // we're about to navigate. so disable cancellation on focus-lost in our indicator so we don't end up + // causing ourselves to self-cancel. + backgroundIndicator.CancelOnFocusLost = false; + succeeded = definitionLocation != null && await definitionLocation.Location.TryNavigateToAsync( + _threadingContext, new NavigationOptions(PreferProvisionalTab: true, ActivateTab: true), cancellationToken).ConfigureAwait(false); } - private async Task ExecuteAsynchronouslyAsync( - GoToDefinitionCommandArgs args, Document document, IDefinitionLocationService service, SnapshotPoint position) + if (!succeeded) { - bool succeeded; - - var indicatorFactory = document.Project.Solution.Services.GetRequiredService(); - - // TODO: prior logic was to get a tracking span of length 1 here. Preserving that, though it's unclear if - // that is necessary for the BWI to work properly. - Contract.ThrowIfTrue(position.Snapshot.Length == 0); - var applicableToSpan = position < position.Snapshot.Length - ? new SnapshotSpan(position, position + 1) - : new SnapshotSpan(position - 1, position); - - using (var backgroundIndicator = indicatorFactory.Create( - args.TextView, applicableToSpan, - EditorFeaturesResources.Navigating_to_definition)) - { - var cancellationToken = backgroundIndicator.UserCancellationToken; - - // determine the location first. - var definitionLocation = await service.GetDefinitionLocationAsync( - document, position, cancellationToken).ConfigureAwait(false); - - // make sure that if our background indicator got canceled, that we do not still perform the navigation. - if (backgroundIndicator.UserCancellationToken.IsCancellationRequested) - return; - - // we're about to navigate. so disable cancellation on focus-lost in our indicator so we don't end up - // causing ourselves to self-cancel. - backgroundIndicator.CancelOnFocusLost = false; - succeeded = definitionLocation != null && await definitionLocation.Location.TryNavigateToAsync( - _threadingContext, new NavigationOptions(PreferProvisionalTab: true, ActivateTab: true), cancellationToken).ConfigureAwait(false); - } - - if (!succeeded) - { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None); - - var notificationService = document.Project.Solution.Services.GetRequiredService(); - notificationService.SendNotification( - FeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret, EditorFeaturesResources.Go_to_Definition, NotificationSeverity.Information); - } + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None); + + var notificationService = document.Project.Solution.Services.GetRequiredService(); + notificationService.SendNotification( + FeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret, EditorFeaturesResources.Go_to_Definition, NotificationSeverity.Information); } } } diff --git a/src/EditorFeatures/Core/GoToImplementation/GoToImplementationCommandHandler.cs b/src/EditorFeatures/Core/GoToImplementation/GoToImplementationCommandHandler.cs index 495625cf499c0..442fd4d8aca9d 100644 --- a/src/EditorFeatures/Core/GoToImplementation/GoToImplementationCommandHandler.cs +++ b/src/EditorFeatures/Core/GoToImplementation/GoToImplementationCommandHandler.cs @@ -20,31 +20,30 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.GoToImplementation +namespace Microsoft.CodeAnalysis.GoToImplementation; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.RoslynContentType)] +[Name(PredefinedCommandHandlerNames.GoToImplementation)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class GoToImplementationCommandHandler( + IThreadingContext threadingContext, + IStreamingFindUsagesPresenter streamingPresenter, + IUIThreadOperationExecutor uiThreadOperationExecutor, + IAsynchronousOperationListenerProvider listenerProvider, + IGlobalOptionService globalOptions) : AbstractGoToCommandHandler(threadingContext, + streamingPresenter, + uiThreadOperationExecutor, + listenerProvider.GetListener(FeatureAttribute.GoToImplementation), + globalOptions) { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.RoslynContentType)] - [Name(PredefinedCommandHandlerNames.GoToImplementation)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class GoToImplementationCommandHandler( - IThreadingContext threadingContext, - IStreamingFindUsagesPresenter streamingPresenter, - IUIThreadOperationExecutor uiThreadOperationExecutor, - IAsynchronousOperationListenerProvider listenerProvider, - IGlobalOptionService globalOptions) : AbstractGoToCommandHandler(threadingContext, - streamingPresenter, - uiThreadOperationExecutor, - listenerProvider.GetListener(FeatureAttribute.GoToImplementation), - globalOptions) - { - public override string DisplayName => EditorFeaturesResources.Go_To_Implementation; + public override string DisplayName => EditorFeaturesResources.Go_To_Implementation; - protected override string ScopeDescription => EditorFeaturesResources.Locating_implementations; - protected override FunctionId FunctionId => FunctionId.CommandHandler_GoToImplementation; + protected override string ScopeDescription => EditorFeaturesResources.Locating_implementations; + protected override FunctionId FunctionId => FunctionId.CommandHandler_GoToImplementation; - protected override Task FindActionAsync(IFindUsagesContext context, Document document, int caretPosition, CancellationToken cancellationToken) - => document.GetRequiredLanguageService() - .FindImplementationsAsync(context, document, caretPosition, ClassificationOptionsProvider, cancellationToken); - } + protected override Task FindActionAsync(IFindUsagesContext context, Document document, int caretPosition, CancellationToken cancellationToken) + => document.GetRequiredLanguageService() + .FindImplementationsAsync(context, document, caretPosition, ClassificationOptionsProvider, cancellationToken); } diff --git a/src/EditorFeatures/Core/Host/IPreviewDialogService.cs b/src/EditorFeatures/Core/Host/IPreviewDialogService.cs index b91c6c5a20bbc..66e05ac12eae9 100644 --- a/src/EditorFeatures/Core/Host/IPreviewDialogService.cs +++ b/src/EditorFeatures/Core/Host/IPreviewDialogService.cs @@ -6,35 +6,34 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Editor.Host +namespace Microsoft.CodeAnalysis.Editor.Host; + +/// +/// Displays the Preview Changes Dialog comparing two solutions. +/// +internal interface IPreviewDialogService : IWorkspaceService { /// - /// Displays the Preview Changes Dialog comparing two solutions. + /// Presents the user a preview of the changes, based on a textual diff + /// between and . /// - internal interface IPreviewDialogService : IWorkspaceService - { - /// - /// Presents the user a preview of the changes, based on a textual diff - /// between and . - /// - /// The title of the preview changes dialog. - /// The keyword used by F1 help in the dialog. - /// Text to display above the treeview in the dialog. - /// The name of the root item in the treeview in the dialog. - /// The of the root item in the treeview. - /// The changes to preview. - /// The baseline solution. - /// Whether or not preview dialog should display item checkboxes. - /// Returns with the changes selected in the dialog - /// applied. Returns null if cancelled. - Solution PreviewChanges( - string title, - string helpString, - string description, - string topLevelName, - Glyph topLevelGlyph, - Solution newSolution, - Solution oldSolution, - bool showCheckBoxes = true); - } + /// The title of the preview changes dialog. + /// The keyword used by F1 help in the dialog. + /// Text to display above the treeview in the dialog. + /// The name of the root item in the treeview in the dialog. + /// The of the root item in the treeview. + /// The changes to preview. + /// The baseline solution. + /// Whether or not preview dialog should display item checkboxes. + /// Returns with the changes selected in the dialog + /// applied. Returns null if cancelled. + Solution PreviewChanges( + string title, + string helpString, + string description, + string topLevelName, + Glyph topLevelGlyph, + Solution newSolution, + Solution oldSolution, + bool showCheckBoxes = true); } diff --git a/src/EditorFeatures/Core/Host/IPreviewPaneService.cs b/src/EditorFeatures/Core/Host/IPreviewPaneService.cs index 8947a3947f7c0..33ec7c74d8bb1 100644 --- a/src/EditorFeatures/Core/Host/IPreviewPaneService.cs +++ b/src/EditorFeatures/Core/Host/IPreviewPaneService.cs @@ -8,10 +8,9 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Editor.Host +namespace Microsoft.CodeAnalysis.Editor.Host; + +internal interface IPreviewPaneService : IWorkspaceService { - internal interface IPreviewPaneService : IWorkspaceService - { - object GetPreviewPane(DiagnosticData diagnostic, IReadOnlyList previewContent); - } + object GetPreviewPane(DiagnosticData diagnostic, IReadOnlyList previewContent); } diff --git a/src/EditorFeatures/Core/Host/IStreamingFindReferencesPresenter.cs b/src/EditorFeatures/Core/Host/IStreamingFindReferencesPresenter.cs index e7b959c4a15da..90a2dc7dace26 100644 --- a/src/EditorFeatures/Core/Host/IStreamingFindReferencesPresenter.cs +++ b/src/EditorFeatures/Core/Host/IStreamingFindReferencesPresenter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; @@ -10,130 +11,139 @@ using Microsoft.CodeAnalysis.Navigation; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.Editor.Host +namespace Microsoft.CodeAnalysis.Editor.Host; + +/// +/// API for hosts to provide if they can present FindUsages results in a streaming manner. +/// i.e. if they support showing results as they are found instead of after all of the results +/// are found. +/// +internal interface IStreamingFindUsagesPresenter { /// - /// API for hosts to provide if they can present FindUsages results in a streaming manner. - /// i.e. if they support showing results as they are found instead of after all of the results - /// are found. + /// Tells the presenter that a search is starting. The returned + /// is used to push information about the search into. i.e. when a reference is found + /// should be called. When the + /// search completes should be called. + /// etc. etc. /// - internal interface IStreamingFindUsagesPresenter - { - /// - /// Tells the presenter that a search is starting. The returned - /// is used to push information about the search into. i.e. when a reference is found - /// should be called. When the - /// search completes should be called. - /// etc. etc. - /// - /// A title to display to the user in the presentation of the results. - /// Whether or not showing references is supported. - /// If true, then the presenter can group by definition, showing references underneath. - /// It can also show messages about no references being found at the end of the search. - /// If false, the presenter will not group by definitions, and will show the definition - /// items in isolation. - /// A cancellation token that will be triggered if the presenter thinks the search - /// should stop. This can normally happen if the presenter view is closed, or recycled to - /// start a new search in it. Callers should only use this if they intend to report results - /// asynchronously and thus relinquish their own control over cancellation from their own - /// surrounding context. If the caller intends to populate the presenter synchronously, - /// then this cancellation token can be ignored. - (FindUsagesContext context, CancellationToken cancellationToken) StartSearch(string title, bool supportsReferences); + /// A title to display to the user in the presentation of the results. + /// Options + /// A cancellation token that will be triggered if the presenter thinks the search + /// should stop. This can normally happen if the presenter view is closed, or recycled to + /// start a new search in it. Callers should only use this if they intend to report results + /// asynchronously and thus relinquish their own control over cancellation from their own + /// surrounding context. If the caller intends to populate the presenter synchronously, + /// then this cancellation token can be ignored. + (FindUsagesContext context, CancellationToken cancellationToken) StartSearch(string title, StreamingFindUsagesPresenterOptions options); - /// - /// Call this method to display the Containing Type, Containing Member, or Kind columns - /// - (FindUsagesContext context, CancellationToken cancellationToken) StartSearchWithCustomColumns(string title, bool supportsReferences, bool includeContainingTypeAndMemberColumns, bool includeKindColumn); + /// + /// Clears all the items from the presenter. + /// + void ClearAll(); +} + +/// +/// options. +/// +/// +/// Whether or not showing references is supported. +/// If true, then the presenter can group by definition, showing references underneath. +/// It can also show messages about no references being found at the end of the search. +/// If false, the presenter will not group by definitions, and will show the definition +/// items in isolation. +/// +/// +internal readonly record struct StreamingFindUsagesPresenterOptions( + bool SupportsReferences = false, + bool IncludeContainingTypeAndMemberColumns = false, + bool IncludeKindColumn = false) +{ + public static readonly StreamingFindUsagesPresenterOptions Default = new(); +} - /// - /// Clears all the items from the presenter. - /// - void ClearAll(); +internal static class IStreamingFindUsagesPresenterExtensions +{ + public static async Task TryPresentLocationOrNavigateIfOneAsync( + this IStreamingFindUsagesPresenter presenter, + IThreadingContext threadingContext, + Workspace workspace, + string title, + ImmutableArray items, + CancellationToken cancellationToken) + { + var location = await presenter.GetStreamingLocationAsync( + threadingContext, workspace, title, items, cancellationToken).ConfigureAwait(false); + return await location.TryNavigateToAsync( + threadingContext, new NavigationOptions(PreferProvisionalTab: true, ActivateTab: true), cancellationToken).ConfigureAwait(false); } - internal static class IStreamingFindUsagesPresenterExtensions + /// + /// Returns a navigable location that will take the user to the location there's only destination, or which will + /// present all the locations if there are many. + /// + public static async Task GetStreamingLocationAsync( + this IStreamingFindUsagesPresenter presenter, + IThreadingContext threadingContext, + Workspace workspace, + string title, + ImmutableArray items, + CancellationToken cancellationToken) { - public static async Task TryPresentLocationOrNavigateIfOneAsync( - this IStreamingFindUsagesPresenter presenter, - IThreadingContext threadingContext, - Workspace workspace, - string title, - ImmutableArray items, - CancellationToken cancellationToken) - { - var location = await presenter.GetStreamingLocationAsync( - threadingContext, workspace, title, items, cancellationToken).ConfigureAwait(false); - return await location.TryNavigateToAsync( - threadingContext, new NavigationOptions(PreferProvisionalTab: true, ActivateTab: true), cancellationToken).ConfigureAwait(false); - } + if (items.IsDefaultOrEmpty) + return null; - /// - /// Returns a navigable location that will take the user to the location there's only destination, or which will - /// present all the locations if there are many. - /// - public static async Task GetStreamingLocationAsync( - this IStreamingFindUsagesPresenter presenter, - IThreadingContext threadingContext, - Workspace workspace, - string title, - ImmutableArray items, - CancellationToken cancellationToken) + using var _ = ArrayBuilder<(DefinitionItem item, INavigableLocation location)>.GetInstance(out var builder); + foreach (var item in items) { - if (items.IsDefaultOrEmpty) - return null; - - using var _ = ArrayBuilder<(DefinitionItem item, INavigableLocation location)>.GetInstance(out var builder); - foreach (var item in items) + // Ignore any definitions that we can't navigate to. + var navigableItem = await item.GetNavigableLocationAsync(workspace, cancellationToken).ConfigureAwait(false); + if (navigableItem != null) { - // Ignore any definitions that we can't navigate to. - var navigableItem = await item.GetNavigableLocationAsync(workspace, cancellationToken).ConfigureAwait(false); - if (navigableItem != null) - { - // If there's a third party external item we can navigate to. Defer to that item and finish. - if (item.IsExternal) - return navigableItem; + // If there's a third party external item we can navigate to. Defer to that item and finish. + if (item.IsExternal) + return navigableItem; - builder.Add((item, navigableItem)); - } + builder.Add((item, navigableItem)); } + } - if (builder.Count == 0) - return null; + if (builder.Count == 0) + return null; - if (builder is [{ item.SourceSpans.Length: <= 1, location: var location }]) - { - // There was only one location to navigate to. Just directly go to that location. If we're directly - // going to a location we need to activate the preview so that focus follows to the new cursor position. - return location; - } + if (builder is [{ item.SourceSpans.Length: <= 1, location: var location }]) + { + // There was only one location to navigate to. Just directly go to that location. If we're directly + // going to a location we need to activate the preview so that focus follows to the new cursor position. + return location; + } - if (presenter == null) - return null; + if (presenter == null) + return null; - var navigableItems = builder.SelectAsArray(t => t.item); - return new NavigableLocation(async (options, cancellationToken) => - { - // Can only navigate or present items on UI thread. - await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var navigableItems = builder.SelectAsArray(t => t.item); + return new NavigableLocation(async (options, cancellationToken) => + { + // Can only navigate or present items on UI thread. + await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - // We have multiple definitions, or we have definitions with multiple locations. Present this to the - // user so they can decide where they want to go to. - // - // We ignore the cancellation token returned by StartSearch as we're in a context where - // we've computed all the results and we're synchronously populating the UI with it. - var (context, _) = presenter.StartSearch(title, supportsReferences: false); - try - { - foreach (var item in navigableItems) - await context.OnDefinitionFoundAsync(item, cancellationToken).ConfigureAwait(false); - } - finally - { - await context.OnCompletedAsync(cancellationToken).ConfigureAwait(false); - } + // We have multiple definitions, or we have definitions with multiple locations. Present this to the + // user so they can decide where they want to go to. + // + // We ignore the cancellation token returned by StartSearch as we're in a context where + // we've computed all the results and we're synchronously populating the UI with it. + var (context, _) = presenter.StartSearch(title, StreamingFindUsagesPresenterOptions.Default); + try + { + foreach (var item in navigableItems) + await context.OnDefinitionFoundAsync(item, cancellationToken).ConfigureAwait(false); + } + finally + { + await context.OnCompletedAsync(cancellationToken).ConfigureAwait(false); + } - return true; - }); - } + return true; + }); } } diff --git a/src/EditorFeatures/Core/ICommandHandlerServiceFactory.cs b/src/EditorFeatures/Core/ICommandHandlerServiceFactory.cs index 02057074a42e2..6913532ac170f 100644 --- a/src/EditorFeatures/Core/ICommandHandlerServiceFactory.cs +++ b/src/EditorFeatures/Core/ICommandHandlerServiceFactory.cs @@ -6,12 +6,11 @@ using System; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +// This is defined only to allow TypeScript to still import it and pass it to the VenusCommandHandler constructor. +// The commit that is is introducing this type can be reverted once TypeScript has moved off of the use. +[Obsolete("This is a compatibility shim for TypeScript; please do not use it.")] +internal interface ICommandHandlerServiceFactory { - // This is defined only to allow TypeScript to still import it and pass it to the VenusCommandHandler constructor. - // The commit that is is introducing this type can be reverted once TypeScript has moved off of the use. - [Obsolete("This is a compatibility shim for TypeScript; please do not use it.")] - internal interface ICommandHandlerServiceFactory - { - } } diff --git a/src/EditorFeatures/Core/IContentTypeLanguageService.cs b/src/EditorFeatures/Core/IContentTypeLanguageService.cs index 00cfe7389f08a..c2b38ba6c8574 100644 --- a/src/EditorFeatures/Core/IContentTypeLanguageService.cs +++ b/src/EditorFeatures/Core/IContentTypeLanguageService.cs @@ -7,13 +7,12 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +/// +/// Service to provide the default content type for a language. +/// +internal interface IContentTypeLanguageService : ILanguageService { - /// - /// Service to provide the default content type for a language. - /// - internal interface IContentTypeLanguageService : ILanguageService - { - IContentType GetDefaultContentType(); - } + IContentType GetDefaultContentType(); } diff --git a/src/EditorFeatures/Core/IDebuggerTextView.cs b/src/EditorFeatures/Core/IDebuggerTextView.cs index 07f191feb38f8..3895cbbf7b412 100644 --- a/src/EditorFeatures/Core/IDebuggerTextView.cs +++ b/src/EditorFeatures/Core/IDebuggerTextView.cs @@ -5,13 +5,12 @@ using System; using Microsoft.VisualStudio.Language.Intellisense; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface IDebuggerTextView { - internal interface IDebuggerTextView - { - bool IsImmediateWindow { get; } + bool IsImmediateWindow { get; } - uint StartBufferUpdate(); - void EndBufferUpdate(uint cookie); - } + uint StartBufferUpdate(); + void EndBufferUpdate(uint cookie); } diff --git a/src/EditorFeatures/Core/IInlineRenameService.cs b/src/EditorFeatures/Core/IInlineRenameService.cs index c144c4db9ef5b..e2853b8443c82 100644 --- a/src/EditorFeatures/Core/IInlineRenameService.cs +++ b/src/EditorFeatures/Core/IInlineRenameService.cs @@ -6,26 +6,25 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Rename; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +/// +/// Provides services for starting an interactive rename session. +/// +internal interface IInlineRenameService { /// - /// Provides services for starting an interactive rename session. + /// Starts an interactive rename session. If an existing inline session was active, it will + /// commit the previous session, possibly causing changes to the text buffer. /// - internal interface IInlineRenameService - { - /// - /// Starts an interactive rename session. If an existing inline session was active, it will - /// commit the previous session, possibly causing changes to the text buffer. - /// - /// The Document containing the triggerSpan. - /// The triggerSpan itself. - /// An optional cancellation token. - /// The rename session. - InlineRenameSessionInfo StartInlineSession(Document document, TextSpan triggerSpan, CancellationToken cancellationToken); + /// The Document containing the triggerSpan. + /// The triggerSpan itself. + /// An optional cancellation token. + /// The rename session. + InlineRenameSessionInfo StartInlineSession(Document document, TextSpan triggerSpan, CancellationToken cancellationToken); - /// - /// Returns the currently active inline session, or null if none is active. - /// - IInlineRenameSession? ActiveSession { get; } - } + /// + /// Returns the currently active inline session, or null if none is active. + /// + IInlineRenameSession? ActiveSession { get; } } diff --git a/src/EditorFeatures/Core/IInlineRenameSession.cs b/src/EditorFeatures/Core/IInlineRenameSession.cs index 670f3d09fc1ba..b99fbc478dfae 100644 --- a/src/EditorFeatures/Core/IInlineRenameSession.cs +++ b/src/EditorFeatures/Core/IInlineRenameSession.cs @@ -8,49 +8,48 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal sealed class InlineRenameSessionInfo { - internal sealed class InlineRenameSessionInfo + /// + /// Whether or not the entity at the selected location can be renamed. + /// + public bool CanRename { get; } + + /// + /// Provides the reason that can be displayed to the user if the entity at the selected + /// location cannot be renamed. + /// + public string LocalizedErrorMessage { get; } + + /// + /// The session created if it was possible to rename the entity. + /// + public IInlineRenameSession Session { get; } + + internal InlineRenameSessionInfo(string localizedErrorMessage) { - /// - /// Whether or not the entity at the selected location can be renamed. - /// - public bool CanRename { get; } - - /// - /// Provides the reason that can be displayed to the user if the entity at the selected - /// location cannot be renamed. - /// - public string LocalizedErrorMessage { get; } - - /// - /// The session created if it was possible to rename the entity. - /// - public IInlineRenameSession Session { get; } - - internal InlineRenameSessionInfo(string localizedErrorMessage) - { - this.CanRename = false; - this.LocalizedErrorMessage = localizedErrorMessage; - } - - internal InlineRenameSessionInfo(IInlineRenameSession session) - { - this.CanRename = true; - this.Session = session; - } + this.CanRename = false; + this.LocalizedErrorMessage = localizedErrorMessage; } - internal interface IInlineRenameSession + internal InlineRenameSessionInfo(IInlineRenameSession session) { - /// - /// Cancels the rename session, and undoes any edits that had been performed by the session. - /// - void Cancel(); - - /// - /// Dismisses the rename session, completing the rename operation across all files. - /// - Task CommitAsync(bool previewChanges, CancellationToken cancellationToken); + this.CanRename = true; + this.Session = session; } } + +internal interface IInlineRenameSession +{ + /// + /// Cancels the rename session, and undoes any edits that had been performed by the session. + /// + void Cancel(); + + /// + /// Dismisses the rename session, completing the rename operation across all files. + /// + Task CommitAsync(bool previewChanges, CancellationToken cancellationToken); +} diff --git a/src/EditorFeatures/Core/IIntellisensePresenterSession.cs b/src/EditorFeatures/Core/IIntellisensePresenterSession.cs index 477ea141228d2..23470bb1f5756 100644 --- a/src/EditorFeatures/Core/IIntellisensePresenterSession.cs +++ b/src/EditorFeatures/Core/IIntellisensePresenterSession.cs @@ -8,16 +8,15 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface IIntelliSensePresenterSession { - internal interface IIntelliSensePresenterSession - { - void Dismiss(); - event EventHandler Dismissed; - } + void Dismiss(); + event EventHandler Dismissed; +} - internal interface IIntelliSensePresenter where TPresenter : IIntelliSensePresenterSession - { - TPresenter CreateSession(ITextView textView, ITextBuffer subjectBuffer, TEditorSessionOpt sessionOpt); - } +internal interface IIntelliSensePresenter where TPresenter : IIntelliSensePresenterSession +{ + TPresenter CreateSession(ITextView textView, ITextBuffer subjectBuffer, TEditorSessionOpt sessionOpt); } diff --git a/src/EditorFeatures/Core/IOptionPageService.cs b/src/EditorFeatures/Core/IOptionPageService.cs index ff89affaab81c..9e0483add9093 100644 --- a/src/EditorFeatures/Core/IOptionPageService.cs +++ b/src/EditorFeatures/Core/IOptionPageService.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface IOptionPageService : ILanguageService { - internal interface IOptionPageService : ILanguageService - { - void ShowFormattingOptionPage(); - } + void ShowFormattingOptionPage(); } diff --git a/src/EditorFeatures/Core/IRefactorNotifyService.cs b/src/EditorFeatures/Core/IRefactorNotifyService.cs index 841083a864d00..4c420434982ad 100644 --- a/src/EditorFeatures/Core/IRefactorNotifyService.cs +++ b/src/EditorFeatures/Core/IRefactorNotifyService.cs @@ -6,32 +6,31 @@ using System.Collections.Generic; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +/// +/// Allows editors to listen to refactoring events and take appropriate action. For example, +/// when VS knows about a symbol rename, it asks the Xaml language service to update xaml files +/// +internal interface IRefactorNotifyService { /// - /// Allows editors to listen to refactoring events and take appropriate action. For example, - /// when VS knows about a symbol rename, it asks the Xaml language service to update xaml files + /// Notifies any interested parties that a rename action is about to happen. + /// Implementers can request the rename action be cancelled, in which case they should + /// return false or throw an exception, depending on the throwOnFailure argument. Callers + /// should honor cancellation requests by not applying the rename and not calling + /// . /// - internal interface IRefactorNotifyService - { - /// - /// Notifies any interested parties that a rename action is about to happen. - /// Implementers can request the rename action be cancelled, in which case they should - /// return false or throw an exception, depending on the throwOnFailure argument. Callers - /// should honor cancellation requests by not applying the rename and not calling - /// . - /// - /// True if the rename should proceed. - bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, ISymbol symbol, string newName, bool throwOnFailure); + /// True if the rename should proceed. + bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, ISymbol symbol, string newName, bool throwOnFailure); - /// - /// Notifies any interested parties that a symbol rename has been applied to the - /// workspace. This should only be called if was - /// called and returned true before the symbol rename was applied to the workspace. - /// In the event of a failure to rename, implementers should return false or throw an - /// exception, depending on the throwOnFailure argument. - /// - /// True if the rename was successful. - bool TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, ISymbol symbol, string newName, bool throwOnFailure); - } + /// + /// Notifies any interested parties that a symbol rename has been applied to the + /// workspace. This should only be called if was + /// called and returned true before the symbol rename was applied to the workspace. + /// In the event of a failure to rename, implementers should return false or throw an + /// exception, depending on the throwOnFailure argument. + /// + /// True if the rename was successful. + bool TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, ISymbol symbol, string newName, bool throwOnFailure); } diff --git a/src/EditorFeatures/Core/InheritanceMargin/InheritanceMarginOptionsStorage.cs b/src/EditorFeatures/Core/InheritanceMargin/InheritanceMarginOptionsStorage.cs index dff4aac0b0d28..1400ad4c38bd3 100644 --- a/src/EditorFeatures/Core/InheritanceMargin/InheritanceMarginOptionsStorage.cs +++ b/src/EditorFeatures/Core/InheritanceMargin/InheritanceMarginOptionsStorage.cs @@ -4,14 +4,13 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.InheritanceMargin +namespace Microsoft.CodeAnalysis.InheritanceMargin; + +internal static class InheritanceMarginOptionsStorage { - internal static class InheritanceMarginOptionsStorage - { - public static readonly PerLanguageOption2 ShowInheritanceMargin = new("dotnet_show_inheritance_margin", defaultValue: true); + public static readonly PerLanguageOption2 ShowInheritanceMargin = new("dotnet_show_inheritance_margin", defaultValue: true); - public static readonly Option2 InheritanceMarginCombinedWithIndicatorMargin = new("dotnet_combine_inheritance_and_indicator_margins", defaultValue: false); + public static readonly Option2 InheritanceMarginCombinedWithIndicatorMargin = new("dotnet_combine_inheritance_and_indicator_margins", defaultValue: false); - public static readonly PerLanguageOption2 InheritanceMarginIncludeGlobalImports = new("dotnet_show_global_imports_in_inheritance_margin", defaultValue: true); - } + public static readonly PerLanguageOption2 InheritanceMarginIncludeGlobalImports = new("dotnet_show_global_imports_in_inheritance_margin", defaultValue: true); } diff --git a/src/EditorFeatures/Core/InlineDiagnostics/InlineDiagnosticsLocations.cs b/src/EditorFeatures/Core/InlineDiagnostics/InlineDiagnosticsLocations.cs index 4570fef93d352..16ce03eedc81b 100644 --- a/src/EditorFeatures/Core/InlineDiagnostics/InlineDiagnosticsLocations.cs +++ b/src/EditorFeatures/Core/InlineDiagnostics/InlineDiagnosticsLocations.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Editor.InlineDiagnostics +namespace Microsoft.CodeAnalysis.Editor.InlineDiagnostics; + +internal enum InlineDiagnosticsLocations { - internal enum InlineDiagnosticsLocations - { - PlacedAtEndOfCode, - PlacedAtEndOfEditor, - } + PlacedAtEndOfCode, + PlacedAtEndOfEditor, } diff --git a/src/EditorFeatures/Core/InlineDiagnostics/InlineDiagnosticsOptionsStorage.cs b/src/EditorFeatures/Core/InlineDiagnostics/InlineDiagnosticsOptionsStorage.cs index 8919d7e82a792..46d12ad897a69 100644 --- a/src/EditorFeatures/Core/InlineDiagnostics/InlineDiagnosticsOptionsStorage.cs +++ b/src/EditorFeatures/Core/InlineDiagnostics/InlineDiagnosticsOptionsStorage.cs @@ -4,16 +4,15 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.InlineDiagnostics +namespace Microsoft.CodeAnalysis.Editor.InlineDiagnostics; + +internal sealed class InlineDiagnosticsOptionsStorage { - internal sealed class InlineDiagnosticsOptionsStorage - { - public static readonly PerLanguageOption2 EnableInlineDiagnostics = - new("dotnet_enable_inline_diagnostics", - defaultValue: false); + public static readonly PerLanguageOption2 EnableInlineDiagnostics = + new("dotnet_enable_inline_diagnostics", + defaultValue: false); - public static readonly PerLanguageOption2 Location = - new("dotnet_inline_diagnostics_location", - defaultValue: InlineDiagnosticsLocations.PlacedAtEndOfCode, serializer: EditorConfigValueSerializer.CreateSerializerForEnum()); - } + public static readonly PerLanguageOption2 Location = + new("dotnet_inline_diagnostics_location", + defaultValue: InlineDiagnosticsLocations.PlacedAtEndOfCode, serializer: EditorConfigValueSerializer.CreateSerializerForEnum()); } diff --git a/src/EditorFeatures/Core/InlineHints/IInlineHintKeyProcessor.cs b/src/EditorFeatures/Core/InlineHints/IInlineHintKeyProcessor.cs index edbbaac1f6ca2..fe7b1d194ac69 100644 --- a/src/EditorFeatures/Core/InlineHints/IInlineHintKeyProcessor.cs +++ b/src/EditorFeatures/Core/InlineHints/IInlineHintKeyProcessor.cs @@ -4,19 +4,18 @@ using System; -namespace Microsoft.CodeAnalysis.Editor.InlineHints +namespace Microsoft.CodeAnalysis.Editor.InlineHints; + +internal interface IInlineHintKeyProcessor { - internal interface IInlineHintKeyProcessor - { - /// - /// The current state of the keyprocessor. i.e. whether or not the key binding is currently being held down or - /// not. Can be read on any thread. - /// - bool State { get; } + /// + /// The current state of the keyprocessor. i.e. whether or not the key binding is currently being held down or + /// not. Can be read on any thread. + /// + bool State { get; } - /// - /// Called when the state of the keyprocessor changes. Only fired on UI thread. - /// - event Action StateChanged; - } + /// + /// Called when the state of the keyprocessor changes. Only fired on UI thread. + /// + event Action StateChanged; } diff --git a/src/EditorFeatures/Core/InlineHints/InlineHintDataTag.cs b/src/EditorFeatures/Core/InlineHints/InlineHintDataTag.cs index cf86ae6650770..c3232634139c8 100644 --- a/src/EditorFeatures/Core/InlineHints/InlineHintDataTag.cs +++ b/src/EditorFeatures/Core/InlineHints/InlineHintDataTag.cs @@ -10,57 +10,56 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.InlineHints +namespace Microsoft.CodeAnalysis.Editor.InlineHints; + +/// +/// The simple tag that only holds information regarding the associated parameter name +/// for the argument +/// +internal sealed class InlineHintDataTag(InlineHintsDataTaggerProvider provider, ITextSnapshot snapshot, InlineHint hint) : ITag, IEquatable { + private readonly InlineHintsDataTaggerProvider _provider = provider; + /// - /// The simple tag that only holds information regarding the associated parameter name - /// for the argument + /// The snapshot this tag was created against. /// - internal sealed class InlineHintDataTag(InlineHintsDataTaggerProvider provider, ITextSnapshot snapshot, InlineHint hint) : ITag, IEquatable - { - private readonly InlineHintsDataTaggerProvider _provider = provider; + private readonly ITextSnapshot _snapshot = snapshot; - /// - /// The snapshot this tag was created against. - /// - private readonly ITextSnapshot _snapshot = snapshot; + public readonly InlineHint Hint = hint; - public readonly InlineHint Hint = hint; + // Intentionally throwing, we have never supported this facility, and there is no contract around placing + // these tags in sets or maps. + public override int GetHashCode() + => throw new NotImplementedException(); - // Intentionally throwing, we have never supported this facility, and there is no contract around placing - // these tags in sets or maps. - public override int GetHashCode() - => throw new NotImplementedException(); + public override bool Equals(object? obj) + => obj is InlineHintDataTag tag && Equals(tag); - public override bool Equals(object? obj) - => obj is InlineHintDataTag tag && Equals(tag); - - public bool Equals(InlineHintDataTag? other) - { - if (other is null) - return false; - - // they have to match if they're going to change text. - if (this.Hint.ReplacementTextChange is null != other.Hint.ReplacementTextChange is null) - return false; + public bool Equals(InlineHintDataTag? other) + { + if (other is null) + return false; - // the text change text has to match. - if (this.Hint.ReplacementTextChange?.NewText != other.Hint.ReplacementTextChange?.NewText) - return false; + // they have to match if they're going to change text. + if (this.Hint.ReplacementTextChange is null != other.Hint.ReplacementTextChange is null) + return false; - // Ensure both hints are talking about the same snapshot. - if (!_provider.SpanEquals(_snapshot, this.Hint.Span, other._snapshot, other.Hint.Span)) - return false; + // the text change text has to match. + if (this.Hint.ReplacementTextChange?.NewText != other.Hint.ReplacementTextChange?.NewText) + return false; - if (this.Hint.ReplacementTextChange != null && - other.Hint.ReplacementTextChange != null && - !_provider.SpanEquals(_snapshot, this.Hint.ReplacementTextChange.Value.Span, other._snapshot, other.Hint.ReplacementTextChange.Value.Span)) - { - return false; - } + // Ensure both hints are talking about the same snapshot. + if (!_provider.SpanEquals(_snapshot, this.Hint.Span, other._snapshot, other.Hint.Span)) + return false; - // ensure all the display parts are the same. - return this.Hint.DisplayParts.SequenceEqual(other.Hint.DisplayParts); + if (this.Hint.ReplacementTextChange != null && + other.Hint.ReplacementTextChange != null && + !_provider.SpanEquals(_snapshot, this.Hint.ReplacementTextChange.Value.Span, other._snapshot, other.Hint.ReplacementTextChange.Value.Span)) + { + return false; } + + // ensure all the display parts are the same. + return this.Hint.DisplayParts.SequenceEqual(other.Hint.DisplayParts); } } diff --git a/src/EditorFeatures/Core/InlineHints/InlineHintKeyProcessorEventSource.cs b/src/EditorFeatures/Core/InlineHints/InlineHintKeyProcessorEventSource.cs index f3c532beac472..d14a599f3c6b3 100644 --- a/src/EditorFeatures/Core/InlineHints/InlineHintKeyProcessorEventSource.cs +++ b/src/EditorFeatures/Core/InlineHints/InlineHintKeyProcessorEventSource.cs @@ -4,25 +4,24 @@ using Microsoft.CodeAnalysis.Editor.Shared.Tagging; -namespace Microsoft.CodeAnalysis.Editor.InlineHints +namespace Microsoft.CodeAnalysis.Editor.InlineHints; + +internal partial class InlineHintsDataTaggerProvider { - internal partial class InlineHintsDataTaggerProvider + private sealed class InlineHintKeyProcessorEventSource(IInlineHintKeyProcessor? inlineHintKeyProcessor) : AbstractTaggerEventSource { - private sealed class InlineHintKeyProcessorEventSource(IInlineHintKeyProcessor? inlineHintKeyProcessor) : AbstractTaggerEventSource - { - private readonly IInlineHintKeyProcessor? _inlineHintKeyProcessor = inlineHintKeyProcessor; + private readonly IInlineHintKeyProcessor? _inlineHintKeyProcessor = inlineHintKeyProcessor; - public override void Connect() - { - if (_inlineHintKeyProcessor != null) - _inlineHintKeyProcessor.StateChanged += this.RaiseChanged; - } + public override void Connect() + { + if (_inlineHintKeyProcessor != null) + _inlineHintKeyProcessor.StateChanged += this.RaiseChanged; + } - public override void Disconnect() - { - if (_inlineHintKeyProcessor != null) - _inlineHintKeyProcessor.StateChanged -= this.RaiseChanged; - } + public override void Disconnect() + { + if (_inlineHintKeyProcessor != null) + _inlineHintKeyProcessor.StateChanged -= this.RaiseChanged; } } } diff --git a/src/EditorFeatures/Core/InlineHints/InlineHintsDataTaggerProvider.cs b/src/EditorFeatures/Core/InlineHints/InlineHintsDataTaggerProvider.cs index 49ea250a76db9..699a25166b41c 100644 --- a/src/EditorFeatures/Core/InlineHints/InlineHintsDataTaggerProvider.cs +++ b/src/EditorFeatures/Core/InlineHints/InlineHintsDataTaggerProvider.cs @@ -24,114 +24,113 @@ using Roslyn.Utilities; using VSUtilities = Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.InlineHints +namespace Microsoft.CodeAnalysis.Editor.InlineHints; + +/// +/// The TaggerProvider that calls upon the service in order to locate the spans and names +/// +[Export(typeof(IViewTaggerProvider))] +[VSUtilities.ContentType(ContentTypeNames.RoslynContentType)] +[TagType(typeof(InlineHintDataTag))] +[VSUtilities.Name(nameof(InlineHintsDataTaggerProvider))] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +[method: ImportingConstructor] +internal partial class InlineHintsDataTaggerProvider( + IThreadingContext threadingContext, + IGlobalOptionService globalOptions, + [Import(AllowDefault = true)] IInlineHintKeyProcessor inlineHintKeyProcessor, + [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) : AsynchronousViewTaggerProvider(threadingContext, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.InlineHints)) { + private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.InlineHints); + private readonly IInlineHintKeyProcessor _inlineHintKeyProcessor = inlineHintKeyProcessor; + + protected override SpanTrackingMode SpanTrackingMode => SpanTrackingMode.EdgeInclusive; + /// - /// The TaggerProvider that calls upon the service in order to locate the spans and names + /// We want to make sure that if the user edits the space that the tag exists in that it goes away and they + /// don't see stale tags sticking around in random locations until the next update. A good example of when this + /// is desirable is 'cut line'. If the tags aren't removed, then the line will be gone but the tags will remain + /// at whatever points the tracking spans moved them to. /// - [Export(typeof(IViewTaggerProvider))] - [VSUtilities.ContentType(ContentTypeNames.RoslynContentType)] - [TagType(typeof(InlineHintDataTag))] - [VSUtilities.Name(nameof(InlineHintsDataTaggerProvider))] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - [method: ImportingConstructor] - internal partial class InlineHintsDataTaggerProvider( - IThreadingContext threadingContext, - IGlobalOptionService globalOptions, - [Import(AllowDefault = true)] IInlineHintKeyProcessor inlineHintKeyProcessor, - [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) : AsynchronousViewTaggerProvider(threadingContext, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.InlineHints)) - { - private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.InlineHints); - private readonly IInlineHintKeyProcessor _inlineHintKeyProcessor = inlineHintKeyProcessor; + protected override TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.RemoveTagsThatIntersectEdits; - protected override SpanTrackingMode SpanTrackingMode => SpanTrackingMode.EdgeInclusive; + protected override TaggerDelay EventChangeDelay => TaggerDelay.Short; - /// - /// We want to make sure that if the user edits the space that the tag exists in that it goes away and they - /// don't see stale tags sticking around in random locations until the next update. A good example of when this - /// is desirable is 'cut line'. If the tags aren't removed, then the line will be gone but the tags will remain - /// at whatever points the tracking spans moved them to. - /// - protected override TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.RemoveTagsThatIntersectEdits; + protected override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) + { + return TaggerEventSources.Compose( + TaggerEventSources.OnViewSpanChanged(this.ThreadingContext, textView), + TaggerEventSources.OnWorkspaceChanged(subjectBuffer, _listener), + new InlineHintKeyProcessorEventSource(_inlineHintKeyProcessor), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.EnabledForParameters), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForLiteralParameters), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForIndexerParameters), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForObjectCreationParameters), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForOtherParameters), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.SuppressForParametersThatMatchMethodIntent), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.SuppressForParametersThatDifferOnlyBySuffix), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.SuppressForParametersThatMatchArgumentName), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.EnabledForTypes), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForImplicitVariableTypes), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForLambdaParameterTypes), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForImplicitObjectCreation)); + } - protected override TaggerDelay EventChangeDelay => TaggerDelay.Short; + protected override IEnumerable GetSpansToTag(ITextView? textView, ITextBuffer subjectBuffer) + { + this.ThreadingContext.ThrowIfNotOnUIThread(); + Contract.ThrowIfNull(textView); - protected override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) + // Find the visible span some 100 lines +/- what's actually in view. This way + // if the user scrolls up/down, we'll already have the results. + var visibleSpanOpt = textView.GetVisibleLinesSpan(subjectBuffer, extraLines: 100); + if (visibleSpanOpt == null) { - return TaggerEventSources.Compose( - TaggerEventSources.OnViewSpanChanged(this.ThreadingContext, textView), - TaggerEventSources.OnWorkspaceChanged(subjectBuffer, _listener), - new InlineHintKeyProcessorEventSource(_inlineHintKeyProcessor), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.EnabledForParameters), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForLiteralParameters), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForIndexerParameters), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForObjectCreationParameters), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForOtherParameters), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.SuppressForParametersThatMatchMethodIntent), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.SuppressForParametersThatDifferOnlyBySuffix), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.SuppressForParametersThatMatchArgumentName), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.EnabledForTypes), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForImplicitVariableTypes), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForLambdaParameterTypes), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, InlineHintsOptionsStorage.ForImplicitObjectCreation)); + // Couldn't find anything visible, just fall back to tagging all hint locations + return base.GetSpansToTag(textView, subjectBuffer); } - protected override IEnumerable GetSpansToTag(ITextView? textView, ITextBuffer subjectBuffer) - { - this.ThreadingContext.ThrowIfNotOnUIThread(); - Contract.ThrowIfNull(textView); - - // Find the visible span some 100 lines +/- what's actually in view. This way - // if the user scrolls up/down, we'll already have the results. - var visibleSpanOpt = textView.GetVisibleLinesSpan(subjectBuffer, extraLines: 100); - if (visibleSpanOpt == null) - { - // Couldn't find anything visible, just fall back to tagging all hint locations - return base.GetSpansToTag(textView, subjectBuffer); - } - - return SpecializedCollections.SingletonEnumerable(visibleSpanOpt.Value); - } + return SpecializedCollections.SingletonEnumerable(visibleSpanOpt.Value); + } - protected override async Task ProduceTagsAsync( - TaggerContext context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition, CancellationToken cancellationToken) + protected override async Task ProduceTagsAsync( + TaggerContext context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition, CancellationToken cancellationToken) + { + var document = documentSnapshotSpan.Document; + if (document == null) + return; + + // The LSP client will handle producing tags when running under the LSP editor. + // Our tagger implementation should return nothing to prevent conflicts. + var workspaceContextService = document.Project.Solution.Services.GetRequiredService(); + if (workspaceContextService.IsInLspEditorContext()) + return; + + var service = document.GetLanguageService(); + if (service == null) + return; + + var options = GlobalOptions.GetInlineHintsOptions(document.Project.Language); + + var snapshotSpan = documentSnapshotSpan.SnapshotSpan; + var hints = await service.GetInlineHintsAsync( + document, snapshotSpan.Span.ToTextSpan(), options, + displayAllOverride: _inlineHintKeyProcessor?.State is true, + cancellationToken).ConfigureAwait(false); + + foreach (var hint in hints) { - var document = documentSnapshotSpan.Document; - if (document == null) - return; - - // The LSP client will handle producing tags when running under the LSP editor. - // Our tagger implementation should return nothing to prevent conflicts. - var workspaceContextService = document.Project.Solution.Services.GetRequiredService(); - if (workspaceContextService.IsInLspEditorContext()) - return; - - var service = document.GetLanguageService(); - if (service == null) - return; - - var options = GlobalOptions.GetInlineHintsOptions(document.Project.Language); - - var snapshotSpan = documentSnapshotSpan.SnapshotSpan; - var hints = await service.GetInlineHintsAsync( - document, snapshotSpan.Span.ToTextSpan(), options, - displayAllOverride: _inlineHintKeyProcessor?.State is true, - cancellationToken).ConfigureAwait(false); - - foreach (var hint in hints) - { - // If we don't have any text to actually show the user, then don't make a tag. - if (hint.DisplayParts.Sum(p => p.ToString().Length) == 0) - continue; - - context.AddTag(new TagSpan( - hint.Span.ToSnapshotSpan(snapshotSpan.Snapshot), - new InlineHintDataTag(this, snapshotSpan.Snapshot, hint))); - } - } + // If we don't have any text to actually show the user, then don't make a tag. + if (hint.DisplayParts.Sum(p => p.ToString().Length) == 0) + continue; - protected override bool TagEquals(InlineHintDataTag tag1, InlineHintDataTag tag2) - => tag1.Equals(tag2); + context.AddTag(new TagSpan( + hint.Span.ToSnapshotSpan(snapshotSpan.Snapshot), + new InlineHintDataTag(this, snapshotSpan.Snapshot, hint))); + } } + + protected override bool TagEquals(InlineHintDataTag tag1, InlineHintDataTag tag2) + => tag1.Equals(tag2); } diff --git a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.FailureInlineRenameInfo.cs b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.FailureInlineRenameInfo.cs index 61ad5ad8d4db6..7827777589572 100644 --- a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.FailureInlineRenameInfo.cs +++ b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.FailureInlineRenameInfo.cs @@ -11,45 +11,44 @@ using Microsoft.CodeAnalysis.Rename; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractEditorInlineRenameService { - internal abstract partial class AbstractEditorInlineRenameService - { - internal static readonly IInlineRenameInfo DefaultFailureInfo = new FailureInlineRenameInfo(FeaturesResources.You_cannot_rename_this_element); + internal static readonly IInlineRenameInfo DefaultFailureInfo = new FailureInlineRenameInfo(FeaturesResources.You_cannot_rename_this_element); - private sealed class FailureInlineRenameInfo(string localizedErrorMessage) : IInlineRenameInfo - { - public bool CanRename => false; + private sealed class FailureInlineRenameInfo(string localizedErrorMessage) : IInlineRenameInfo + { + public bool CanRename => false; - public bool HasOverloads => false; + public bool HasOverloads => false; - public bool MustRenameOverloads => false; + public bool MustRenameOverloads => false; - public string LocalizedErrorMessage { get; } = localizedErrorMessage; + public string LocalizedErrorMessage { get; } = localizedErrorMessage; - public TextSpan TriggerSpan => default; + public TextSpan TriggerSpan => default; - public string DisplayName => null; + public string DisplayName => null; - public string FullDisplayName => null; + public string FullDisplayName => null; - public Glyph Glyph => Glyph.None; + public Glyph Glyph => Glyph.None; - public ImmutableArray DefinitionLocations => default; + public ImmutableArray DefinitionLocations => default; - public string GetFinalSymbolName(string replacementText) => null; + public string GetFinalSymbolName(string replacementText) => null; - public TextSpan GetReferenceEditSpan(InlineRenameLocation location, string triggerText, CancellationToken cancellationToken) => default; + public TextSpan GetReferenceEditSpan(InlineRenameLocation location, string triggerText, CancellationToken cancellationToken) => default; - public TextSpan? GetConflictEditSpan(InlineRenameLocation location, string triggerText, string replacementText, CancellationToken cancellationToken) => null; + public TextSpan? GetConflictEditSpan(InlineRenameLocation location, string triggerText, string replacementText, CancellationToken cancellationToken) => null; - public Task FindRenameLocationsAsync(SymbolRenameOptions options, CancellationToken cancellationToken) => Task.FromResult(null); + public Task FindRenameLocationsAsync(SymbolRenameOptions options, CancellationToken cancellationToken) => Task.FromResult(null); - public bool TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) => false; + public bool TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) => false; - public bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) => false; + public bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) => false; - public InlineRenameFileRenameInfo GetFileRenameInfo() => InlineRenameFileRenameInfo.NotAllowed; - } + public InlineRenameFileRenameInfo GetFileRenameInfo() => InlineRenameFileRenameInfo.NotAllowed; } } diff --git a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.InlineRenameLocationSet.cs b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.InlineRenameLocationSet.cs index 9a52069e3b835..5fafe15fe12db 100644 --- a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.InlineRenameLocationSet.cs +++ b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.InlineRenameLocationSet.cs @@ -11,47 +11,46 @@ using Microsoft.CodeAnalysis.Rename; using Microsoft.CodeAnalysis.Rename.ConflictEngine; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractEditorInlineRenameService { - internal abstract partial class AbstractEditorInlineRenameService + private class InlineRenameLocationSet : IInlineRenameLocationSet { - private class InlineRenameLocationSet : IInlineRenameLocationSet + private readonly LightweightRenameLocations _renameLocationSet; + private readonly CodeCleanupOptionsProvider _fallbackOptions; + private readonly SymbolInlineRenameInfo _renameInfo; + + public IList Locations { get; } + + public InlineRenameLocationSet( + SymbolInlineRenameInfo renameInfo, + LightweightRenameLocations renameLocationSet, + CodeCleanupOptionsProvider fallbackOptions) + { + _renameInfo = renameInfo; + _renameLocationSet = renameLocationSet; + _fallbackOptions = fallbackOptions; + this.Locations = renameLocationSet.Locations.Where(RenameLocation.ShouldRename) + .Select(ConvertLocation) + .ToImmutableArray(); + } + + private InlineRenameLocation ConvertLocation(RenameLocation location) { - private readonly LightweightRenameLocations _renameLocationSet; - private readonly CodeCleanupOptionsProvider _fallbackOptions; - private readonly SymbolInlineRenameInfo _renameInfo; - - public IList Locations { get; } - - public InlineRenameLocationSet( - SymbolInlineRenameInfo renameInfo, - LightweightRenameLocations renameLocationSet, - CodeCleanupOptionsProvider fallbackOptions) - { - _renameInfo = renameInfo; - _renameLocationSet = renameLocationSet; - _fallbackOptions = fallbackOptions; - this.Locations = renameLocationSet.Locations.Where(RenameLocation.ShouldRename) - .Select(ConvertLocation) - .ToImmutableArray(); - } - - private InlineRenameLocation ConvertLocation(RenameLocation location) - { - return new InlineRenameLocation( - _renameLocationSet.Solution.GetDocument(location.DocumentId), location.Location.SourceSpan); - } - - public async Task GetReplacementsAsync( - string replacementText, - SymbolRenameOptions options, - CancellationToken cancellationToken) - { - var conflicts = await _renameLocationSet.ResolveConflictsAsync( - _renameInfo.RenameSymbol, _renameInfo.GetFinalSymbolName(replacementText), nonConflictSymbolKeys: default, _fallbackOptions, cancellationToken).ConfigureAwait(false); - - return new InlineRenameReplacementInfo(conflicts); - } + return new InlineRenameLocation( + _renameLocationSet.Solution.GetDocument(location.DocumentId), location.Location.SourceSpan); + } + + public async Task GetReplacementsAsync( + string replacementText, + SymbolRenameOptions options, + CancellationToken cancellationToken) + { + var conflicts = await _renameLocationSet.ResolveConflictsAsync( + _renameInfo.RenameSymbol, _renameInfo.GetFinalSymbolName(replacementText), nonConflictSymbolKeys: default, _fallbackOptions, cancellationToken).ConfigureAwait(false); + + return new InlineRenameReplacementInfo(conflicts); } } } diff --git a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.InlineRenameReplacementInfo.cs b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.InlineRenameReplacementInfo.cs index 1fad3164f0159..ef1823b6dc65d 100644 --- a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.InlineRenameReplacementInfo.cs +++ b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.InlineRenameReplacementInfo.cs @@ -7,59 +7,58 @@ using Microsoft.CodeAnalysis.Rename; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractEditorInlineRenameService : IEditorInlineRenameService { - internal abstract partial class AbstractEditorInlineRenameService : IEditorInlineRenameService + private class InlineRenameReplacementInfo : IInlineRenameReplacementInfo { - private class InlineRenameReplacementInfo : IInlineRenameReplacementInfo + private readonly ConflictResolution _conflicts; + + public InlineRenameReplacementInfo(ConflictResolution conflicts) { - private readonly ConflictResolution _conflicts; + Contract.ThrowIfFalse(conflicts.IsSuccessful); + _conflicts = conflicts; + } - public InlineRenameReplacementInfo(ConflictResolution conflicts) - { - Contract.ThrowIfFalse(conflicts.IsSuccessful); - _conflicts = conflicts; - } + public IEnumerable DocumentIds => _conflicts.DocumentIds; - public IEnumerable DocumentIds => _conflicts.DocumentIds; + public Solution NewSolution => _conflicts.NewSolution!; - public Solution NewSolution => _conflicts.NewSolution!; + public bool ReplacementTextValid => _conflicts.ReplacementTextValid; - public bool ReplacementTextValid => _conflicts.ReplacementTextValid; + public IEnumerable GetReplacements(DocumentId documentId) + { + var nonComplexifiedSpans = GetNonComplexifiedReplacements(documentId); + var complexifiedSpans = GetComplexifiedReplacements(documentId); - public IEnumerable GetReplacements(DocumentId documentId) - { - var nonComplexifiedSpans = GetNonComplexifiedReplacements(documentId); - var complexifiedSpans = GetComplexifiedReplacements(documentId); + return nonComplexifiedSpans.Concat(complexifiedSpans); + } - return nonComplexifiedSpans.Concat(complexifiedSpans); - } + private IEnumerable GetNonComplexifiedReplacements(DocumentId documentId) + { + var modifiedSpans = _conflicts.GetModifiedSpanMap(documentId); + var locationsForDocument = _conflicts.GetRelatedLocationsForDocument(documentId); - private IEnumerable GetNonComplexifiedReplacements(DocumentId documentId) + // The RenamedSpansTracker doesn't currently track unresolved conflicts for + // unmodified locations. If the document wasn't modified, we can just use the + // original span as the new span, but otherwise we need to filter out + // locations that aren't in modifiedSpans. + if (modifiedSpans.Any()) { - var modifiedSpans = _conflicts.GetModifiedSpanMap(documentId); - var locationsForDocument = _conflicts.GetRelatedLocationsForDocument(documentId); - - // The RenamedSpansTracker doesn't currently track unresolved conflicts for - // unmodified locations. If the document wasn't modified, we can just use the - // original span as the new span, but otherwise we need to filter out - // locations that aren't in modifiedSpans. - if (modifiedSpans.Any()) - { - return locationsForDocument.Where(loc => modifiedSpans.ContainsKey(loc.ConflictCheckSpan)) - .Select(loc => new InlineRenameReplacement(loc, modifiedSpans[loc.ConflictCheckSpan])); - } - else - { - return locationsForDocument.Select(loc => new InlineRenameReplacement(loc, loc.ConflictCheckSpan)); - } + return locationsForDocument.Where(loc => modifiedSpans.ContainsKey(loc.ConflictCheckSpan)) + .Select(loc => new InlineRenameReplacement(loc, modifiedSpans[loc.ConflictCheckSpan])); } - - private IEnumerable GetComplexifiedReplacements(DocumentId documentId) + else { - return _conflicts.GetComplexifiedSpans(documentId) - .Select(s => new InlineRenameReplacement(InlineRenameReplacementKind.Complexified, s.oldSpan, s.newSpan)); + return locationsForDocument.Select(loc => new InlineRenameReplacement(loc, loc.ConflictCheckSpan)); } } + + private IEnumerable GetComplexifiedReplacements(DocumentId documentId) + { + return _conflicts.GetComplexifiedSpans(documentId) + .Select(s => new InlineRenameReplacement(InlineRenameReplacementKind.Complexified, s.oldSpan, s.newSpan)); + } } } diff --git a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.SymbolRenameInfo.cs b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.SymbolRenameInfo.cs index f1aa077a7b607..6bac167b390b8 100644 --- a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.SymbolRenameInfo.cs +++ b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.SymbolRenameInfo.cs @@ -19,167 +19,166 @@ using Microsoft.CodeAnalysis.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractEditorInlineRenameService { - internal abstract partial class AbstractEditorInlineRenameService + /// + /// Represents information about the ability to rename a particular location. + /// + private partial class SymbolInlineRenameInfo : IInlineRenameInfo { + private const string AttributeSuffix = "Attribute"; + + private readonly SymbolicRenameInfo _info; + + private Document Document => _info.Document!; + private readonly CodeCleanupOptionsProvider _fallbackOptions; + private readonly IEnumerable _refactorNotifyServices; + /// - /// Represents information about the ability to rename a particular location. + /// Whether or not we shortened the trigger span (say because we were renaming an attribute, + /// and we didn't select the 'Attribute' portion of the name). /// - private partial class SymbolInlineRenameInfo : IInlineRenameInfo + private bool IsRenamingAttributePrefix => _info.IsRenamingAttributePrefix; + + public bool CanRename { get; } + public string? LocalizedErrorMessage => null; + public TextSpan TriggerSpan { get; } + public bool HasOverloads { get; } + public bool MustRenameOverloads => _info.ForceRenameOverloads; + + /// + /// The locations of the potential rename candidates for the symbol. + /// + public ImmutableArray DefinitionLocations => _info.DocumentSpans; + + public ISymbol RenameSymbol => _info.Symbol!; + + public SymbolInlineRenameInfo( + IEnumerable refactorNotifyServices, + SymbolicRenameInfo info, + CodeCleanupOptionsProvider fallbackOptions, + CancellationToken cancellationToken) { - private const string AttributeSuffix = "Attribute"; - - private readonly SymbolicRenameInfo _info; - - private Document Document => _info.Document!; - private readonly CodeCleanupOptionsProvider _fallbackOptions; - private readonly IEnumerable _refactorNotifyServices; - - /// - /// Whether or not we shortened the trigger span (say because we were renaming an attribute, - /// and we didn't select the 'Attribute' portion of the name). - /// - private bool IsRenamingAttributePrefix => _info.IsRenamingAttributePrefix; - - public bool CanRename { get; } - public string? LocalizedErrorMessage => null; - public TextSpan TriggerSpan { get; } - public bool HasOverloads { get; } - public bool MustRenameOverloads => _info.ForceRenameOverloads; - - /// - /// The locations of the potential rename candidates for the symbol. - /// - public ImmutableArray DefinitionLocations => _info.DocumentSpans; - - public ISymbol RenameSymbol => _info.Symbol!; - - public SymbolInlineRenameInfo( - IEnumerable refactorNotifyServices, - SymbolicRenameInfo info, - CodeCleanupOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - Contract.ThrowIfTrue(info.IsError); - this.CanRename = true; + Contract.ThrowIfTrue(info.IsError); + this.CanRename = true; + + _info = info; + _refactorNotifyServices = refactorNotifyServices; + _fallbackOptions = fallbackOptions; - _info = info; - _refactorNotifyServices = refactorNotifyServices; - _fallbackOptions = fallbackOptions; + this.HasOverloads = RenameUtilities.GetOverloadedSymbols(this.RenameSymbol).Any(); - this.HasOverloads = RenameUtilities.GetOverloadedSymbols(this.RenameSymbol).Any(); + this.TriggerSpan = GetReferenceEditSpan(new InlineRenameLocation(this.Document, info.TriggerToken.Span), info.TriggerText, cancellationToken); + } - this.TriggerSpan = GetReferenceEditSpan(new InlineRenameLocation(this.Document, info.TriggerToken.Span), info.TriggerText, cancellationToken); + /// + /// Given a span of text, we need to return the subspan that is editable and + /// contains the current replacementText. + /// + /// These cases are currently handled: + /// - Escaped identifiers [goo] => goo + /// - Type suffixes in VB goo$ => goo + /// - Qualified names from complexification A.goo => goo + /// - Optional Attribute suffixes XAttribute => X + /// Careful here: XAttribute => XAttribute if renamesymbol is XAttributeAttribute + /// - Compiler-generated EventHandler suffix XEventHandler => X + /// - Compiler-generated get_ and set_ prefixes get_X => X + /// + public TextSpan GetReferenceEditSpan(InlineRenameLocation location, string triggerText, CancellationToken cancellationToken) + { + var searchName = this.RenameSymbol.Name; + if (this.IsRenamingAttributePrefix) + { + // We're only renaming the attribute prefix part. We want to adjust the span of + // the reference we've found to only update the prefix portion. + searchName = _info.GetWithoutAttributeSuffix(this.RenameSymbol.Name); } - /// - /// Given a span of text, we need to return the subspan that is editable and - /// contains the current replacementText. - /// - /// These cases are currently handled: - /// - Escaped identifiers [goo] => goo - /// - Type suffixes in VB goo$ => goo - /// - Qualified names from complexification A.goo => goo - /// - Optional Attribute suffixes XAttribute => X - /// Careful here: XAttribute => XAttribute if renamesymbol is XAttributeAttribute - /// - Compiler-generated EventHandler suffix XEventHandler => X - /// - Compiler-generated get_ and set_ prefixes get_X => X - /// - public TextSpan GetReferenceEditSpan(InlineRenameLocation location, string triggerText, CancellationToken cancellationToken) + var index = triggerText.LastIndexOf(searchName, StringComparison.Ordinal); + if (index < 0) { - var searchName = this.RenameSymbol.Name; - if (this.IsRenamingAttributePrefix) - { - // We're only renaming the attribute prefix part. We want to adjust the span of - // the reference we've found to only update the prefix portion. - searchName = _info.GetWithoutAttributeSuffix(this.RenameSymbol.Name); - } + // Couldn't even find the search text at this reference location. This might happen + // if the user used things like unicode escapes. IN that case, we'll have to rename + // the entire identifier. + return location.TextSpan; + } - var index = triggerText.LastIndexOf(searchName, StringComparison.Ordinal); - if (index < 0) - { - // Couldn't even find the search text at this reference location. This might happen - // if the user used things like unicode escapes. IN that case, we'll have to rename - // the entire identifier. - return location.TextSpan; - } + return new TextSpan(location.TextSpan.Start + index, searchName.Length); + } - return new TextSpan(location.TextSpan.Start + index, searchName.Length); - } + public TextSpan? GetConflictEditSpan(InlineRenameLocation location, string triggerText, string replacementText, CancellationToken cancellationToken) + { + var position = triggerText.LastIndexOf(replacementText, StringComparison.Ordinal); - public TextSpan? GetConflictEditSpan(InlineRenameLocation location, string triggerText, string replacementText, CancellationToken cancellationToken) + if (this.IsRenamingAttributePrefix) { - var position = triggerText.LastIndexOf(replacementText, StringComparison.Ordinal); + // We're only renaming the attribute prefix part. We want to adjust the span of + // the reference we've found to only update the prefix portion. + var index = triggerText.LastIndexOf(replacementText + AttributeSuffix, StringComparison.Ordinal); + position = index >= 0 ? index : position; + } - if (this.IsRenamingAttributePrefix) - { - // We're only renaming the attribute prefix part. We want to adjust the span of - // the reference we've found to only update the prefix portion. - var index = triggerText.LastIndexOf(replacementText + AttributeSuffix, StringComparison.Ordinal); - position = index >= 0 ? index : position; - } + if (position < 0) + { + return null; + } - if (position < 0) - { - return null; - } + return new TextSpan(location.TextSpan.Start + position, replacementText.Length); + } - return new TextSpan(location.TextSpan.Start + position, replacementText.Length); - } + public string DisplayName => RenameSymbol.Name; + public string FullDisplayName => RenameSymbol.ToDisplayString(); + public Glyph Glyph => RenameSymbol.GetGlyph(); - public string DisplayName => RenameSymbol.Name; - public string FullDisplayName => RenameSymbol.ToDisplayString(); - public Glyph Glyph => RenameSymbol.GetGlyph(); + public string GetFinalSymbolName(string replacementText) + => _info.GetFinalSymbolName(replacementText); - public string GetFinalSymbolName(string replacementText) - => _info.GetFinalSymbolName(replacementText); + public async Task FindRenameLocationsAsync(SymbolRenameOptions options, CancellationToken cancellationToken) + { + var solution = this.Document.Project.Solution; + var locations = await Renamer.FindRenameLocationsAsync( + solution, this.RenameSymbol, options, cancellationToken).ConfigureAwait(false); - public async Task FindRenameLocationsAsync(SymbolRenameOptions options, CancellationToken cancellationToken) - { - var solution = this.Document.Project.Solution; - var locations = await Renamer.FindRenameLocationsAsync( - solution, this.RenameSymbol, options, cancellationToken).ConfigureAwait(false); + return new InlineRenameLocationSet(this, locations, _fallbackOptions); + } - return new InlineRenameLocationSet(this, locations, _fallbackOptions); - } + public bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) + { + return _refactorNotifyServices.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocumentIDs, RenameSymbol, + this.GetFinalSymbolName(replacementText), throwOnFailure: false); + } - public bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) - { - return _refactorNotifyServices.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocumentIDs, RenameSymbol, - this.GetFinalSymbolName(replacementText), throwOnFailure: false); - } + public bool TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) + { + return _refactorNotifyServices.TryOnAfterGlobalSymbolRenamed(workspace, changedDocumentIDs, RenameSymbol, + this.GetFinalSymbolName(replacementText), throwOnFailure: false); + } - public bool TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText) + public InlineRenameFileRenameInfo GetFileRenameInfo() + { + if (RenameSymbol.Kind == SymbolKind.NamedType && + this.Document.Project.Solution.CanApplyChange(ApplyChangesKind.ChangeDocumentInfo)) { - return _refactorNotifyServices.TryOnAfterGlobalSymbolRenamed(workspace, changedDocumentIDs, RenameSymbol, - this.GetFinalSymbolName(replacementText), throwOnFailure: false); - } + if (RenameSymbol.Locations.Length > 1) + { + return InlineRenameFileRenameInfo.TypeWithMultipleLocations; + } - public InlineRenameFileRenameInfo GetFileRenameInfo() - { - if (RenameSymbol.Kind == SymbolKind.NamedType && - this.Document.Project.Solution.CanApplyChange(ApplyChangesKind.ChangeDocumentInfo)) + // Get the document that the symbol is defined in to compare + // the name with the symbol name. If they match allow + // rename file rename as part of the symbol rename + var symbolSourceDocument = this.Document.Project.Solution.GetDocument(RenameSymbol.Locations.Single().SourceTree); + if (symbolSourceDocument != null && WorkspacePathUtilities.TypeNameMatchesDocumentName(symbolSourceDocument, RenameSymbol.Name)) { - if (RenameSymbol.Locations.Length > 1) - { - return InlineRenameFileRenameInfo.TypeWithMultipleLocations; - } - - // Get the document that the symbol is defined in to compare - // the name with the symbol name. If they match allow - // rename file rename as part of the symbol rename - var symbolSourceDocument = this.Document.Project.Solution.GetDocument(RenameSymbol.Locations.Single().SourceTree); - if (symbolSourceDocument != null && WorkspacePathUtilities.TypeNameMatchesDocumentName(symbolSourceDocument, RenameSymbol.Name)) - { - return InlineRenameFileRenameInfo.Allowed; - } - - return InlineRenameFileRenameInfo.TypeDoesNotMatchFileName; + return InlineRenameFileRenameInfo.Allowed; } - return InlineRenameFileRenameInfo.NotAllowed; + return InlineRenameFileRenameInfo.TypeDoesNotMatchFileName; } + + return InlineRenameFileRenameInfo.NotAllowed; } } } diff --git a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.cs b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.cs index 5ca1b749278c5..5534d677149a0 100644 --- a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.cs +++ b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.cs @@ -8,27 +8,26 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Rename; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractEditorInlineRenameService : IEditorInlineRenameService { - internal abstract partial class AbstractEditorInlineRenameService : IEditorInlineRenameService - { - private readonly IEnumerable _refactorNotifyServices; - private readonly IGlobalOptionService _globalOptions; + private readonly IEnumerable _refactorNotifyServices; + private readonly IGlobalOptionService _globalOptions; - protected AbstractEditorInlineRenameService(IEnumerable refactorNotifyServices, IGlobalOptionService globalOptions) - { - _refactorNotifyServices = refactorNotifyServices; - _globalOptions = globalOptions; - } + protected AbstractEditorInlineRenameService(IEnumerable refactorNotifyServices, IGlobalOptionService globalOptions) + { + _refactorNotifyServices = refactorNotifyServices; + _globalOptions = globalOptions; + } - public async Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken) - { - var symbolicInfo = await SymbolicRenameInfo.GetRenameInfoAsync(document, position, cancellationToken).ConfigureAwait(false); - if (symbolicInfo.LocalizedErrorMessage != null) - return new FailureInlineRenameInfo(symbolicInfo.LocalizedErrorMessage); + public async Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken) + { + var symbolicInfo = await SymbolicRenameInfo.GetRenameInfoAsync(document, position, cancellationToken).ConfigureAwait(false); + if (symbolicInfo.LocalizedErrorMessage != null) + return new FailureInlineRenameInfo(symbolicInfo.LocalizedErrorMessage); - return new SymbolInlineRenameInfo( - _refactorNotifyServices, symbolicInfo, _globalOptions.CreateProvider(), cancellationToken); - } + return new SymbolInlineRenameInfo( + _refactorNotifyServices, symbolicInfo, _globalOptions.CreateProvider(), cancellationToken); } } diff --git a/src/EditorFeatures/Core/InlineRename/AbstractInlineRenameUndoManager.cs b/src/EditorFeatures/Core/InlineRename/AbstractInlineRenameUndoManager.cs index 0e567da95a8fa..3e15d293f2716 100644 --- a/src/EditorFeatures/Core/InlineRename/AbstractInlineRenameUndoManager.cs +++ b/src/EditorFeatures/Core/InlineRename/AbstractInlineRenameUndoManager.cs @@ -15,190 +15,189 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +/// +/// This class contains the logic common to VS and ETA when implementing IInlineRenameUndoManager +/// +internal abstract class AbstractInlineRenameUndoManager { - /// - /// This class contains the logic common to VS and ETA when implementing IInlineRenameUndoManager - /// - internal abstract class AbstractInlineRenameUndoManager + protected class ActiveSpanState { - protected class ActiveSpanState - { - public string ReplacementText; - public int SelectionAnchorPoint; - public int SelectionActivePoint; - } + public string ReplacementText; + public int SelectionAnchorPoint; + public int SelectionActivePoint; + } - protected readonly InlineRenameService InlineRenameService; - private readonly IGlobalOptionService _globalOptionService; - protected readonly Dictionary UndoManagers = []; - protected readonly Stack UndoStack = new Stack(); - protected readonly Stack RedoStack = new Stack(); - protected ActiveSpanState initialState; - protected ActiveSpanState currentState; - protected bool updatePending = false; + protected readonly InlineRenameService InlineRenameService; + private readonly IGlobalOptionService _globalOptionService; + protected readonly Dictionary UndoManagers = []; + protected readonly Stack UndoStack = new Stack(); + protected readonly Stack RedoStack = new Stack(); + protected ActiveSpanState initialState; + protected ActiveSpanState currentState; + protected bool updatePending = false; - private InlineRenameSession _trackedSession; + private InlineRenameSession _trackedSession; - public AbstractInlineRenameUndoManager(InlineRenameService inlineRenameService, IGlobalOptionService globalOptionService) - { - this.InlineRenameService = inlineRenameService; - _globalOptionService = globalOptionService; + public AbstractInlineRenameUndoManager(InlineRenameService inlineRenameService, IGlobalOptionService globalOptionService) + { + this.InlineRenameService = inlineRenameService; + _globalOptionService = globalOptionService; - InlineRenameService.ActiveSessionChanged += InlineRenameService_ActiveSessionChanged; - } + InlineRenameService.ActiveSessionChanged += InlineRenameService_ActiveSessionChanged; + } - private void InlineRenameService_ActiveSessionChanged(object sender, InlineRenameService.ActiveSessionChangedEventArgs e) + private void InlineRenameService_ActiveSessionChanged(object sender, InlineRenameService.ActiveSessionChangedEventArgs e) + { + if (_trackedSession is not null) { - if (_trackedSession is not null) - { - _trackedSession.ReplacementTextChanged -= InlineRenameSession_ReplacementTextChanged; - } - - if (!_globalOptionService.GetOption(InlineRenameUIOptionsStorage.UseInlineAdornment)) - { - // If the user is typing directly into the editor as the only way to change - // the replacement text then we don't need to respond to text changes. The - // listener on the textview that calls UpdateCurrentState will handle - // this correctly. This option cannot change when we are currently in a session, so - // only hook up as needed - _trackedSession = null; - return; - } - - _trackedSession = InlineRenameService.ActiveSession; - - if (_trackedSession is not null) - { - _trackedSession.ReplacementTextChanged += InlineRenameSession_ReplacementTextChanged; - } + _trackedSession.ReplacementTextChanged -= InlineRenameSession_ReplacementTextChanged; } - private void InlineRenameSession_ReplacementTextChanged(object sender, System.EventArgs e) + if (!_globalOptionService.GetOption(InlineRenameUIOptionsStorage.UseInlineAdornment)) { - if (currentState.ReplacementText != _trackedSession.ReplacementText) - { - // No need to update anchor points here, just make sure the state in the undo stack - // ends up with the correct replacement text. This can happen if the text buffer isn't - // edited directly - this.currentState = new ActiveSpanState() - { - ReplacementText = _trackedSession.ReplacementText, - SelectionAnchorPoint = currentState.SelectionAnchorPoint, - SelectionActivePoint = currentState.SelectionActivePoint - }; - } + // If the user is typing directly into the editor as the only way to change + // the replacement text then we don't need to respond to text changes. The + // listener on the textview that calls UpdateCurrentState will handle + // this correctly. This option cannot change when we are currently in a session, so + // only hook up as needed + _trackedSession = null; + return; } - public void Disconnect() + _trackedSession = InlineRenameService.ActiveSession; + + if (_trackedSession is not null) { - this.UndoManagers.Clear(); - this.UndoStack.Clear(); - this.RedoStack.Clear(); - this.initialState = null; - this.currentState = null; + _trackedSession.ReplacementTextChanged += InlineRenameSession_ReplacementTextChanged; } + } - private void UpdateCurrentState(string replacementText, ITextSelection selection, SnapshotSpan activeSpan) + private void InlineRenameSession_ReplacementTextChanged(object sender, System.EventArgs e) + { + if (currentState.ReplacementText != _trackedSession.ReplacementText) { - var snapshot = activeSpan.Snapshot; - var selectionSpan = selection.GetSnapshotSpansOnBuffer(snapshot.TextBuffer).Single(); - - var start = selectionSpan.Start.TranslateTo(snapshot, PointTrackingMode.Positive).Position - activeSpan.Start.Position; - var end = selectionSpan.End.TranslateTo(snapshot, PointTrackingMode.Positive).Position - activeSpan.Start.Position; - + // No need to update anchor points here, just make sure the state in the undo stack + // ends up with the correct replacement text. This can happen if the text buffer isn't + // edited directly this.currentState = new ActiveSpanState() { - ReplacementText = replacementText, - SelectionAnchorPoint = selection.IsReversed ? end : start, - SelectionActivePoint = selection.IsReversed ? start : end + ReplacementText = _trackedSession.ReplacementText, + SelectionAnchorPoint = currentState.SelectionAnchorPoint, + SelectionActivePoint = currentState.SelectionActivePoint }; } + } - public void CreateInitialState(string replacementText, ITextSelection selection, SnapshotSpan startingSpan) - { - UpdateCurrentState(replacementText, selection, startingSpan); - this.initialState = this.currentState; - } + public void Disconnect() + { + this.UndoManagers.Clear(); + this.UndoStack.Clear(); + this.RedoStack.Clear(); + this.initialState = null; + this.currentState = null; + } + + private void UpdateCurrentState(string replacementText, ITextSelection selection, SnapshotSpan activeSpan) + { + var snapshot = activeSpan.Snapshot; + var selectionSpan = selection.GetSnapshotSpansOnBuffer(snapshot.TextBuffer).Single(); + + var start = selectionSpan.Start.TranslateTo(snapshot, PointTrackingMode.Positive).Position - activeSpan.Start.Position; + var end = selectionSpan.End.TranslateTo(snapshot, PointTrackingMode.Positive).Position - activeSpan.Start.Position; - public void OnTextChanged(ITextSelection selection, SnapshotSpan singleTrackingSpanTouched) + this.currentState = new ActiveSpanState() { - this.RedoStack.Clear(); - if (!this.UndoStack.Any()) - { - this.UndoStack.Push(this.initialState); - } + ReplacementText = replacementText, + SelectionAnchorPoint = selection.IsReversed ? end : start, + SelectionActivePoint = selection.IsReversed ? start : end + }; + } - // For now, we will only ever be one Undo away from the beginning of the rename session. We can - // implement Undo merging in the future. - var replacementText = singleTrackingSpanTouched.GetText(); - UpdateCurrentState(replacementText, selection, singleTrackingSpanTouched); + public void CreateInitialState(string replacementText, ITextSelection selection, SnapshotSpan startingSpan) + { + UpdateCurrentState(replacementText, selection, startingSpan); + this.initialState = this.currentState; + } - this.InlineRenameService.ActiveSession.ApplyReplacementText(replacementText, propagateEditImmediately: false); + public void OnTextChanged(ITextSelection selection, SnapshotSpan singleTrackingSpanTouched) + { + this.RedoStack.Clear(); + if (!this.UndoStack.Any()) + { + this.UndoStack.Push(this.initialState); } - public void UpdateSelection(ITextView textView, ITextBuffer subjectBuffer, ITrackingSpan activeRenameSpan) + // For now, we will only ever be one Undo away from the beginning of the rename session. We can + // implement Undo merging in the future. + var replacementText = singleTrackingSpanTouched.GetText(); + UpdateCurrentState(replacementText, selection, singleTrackingSpanTouched); + + this.InlineRenameService.ActiveSession.ApplyReplacementText(replacementText, propagateEditImmediately: false); + } + + public void UpdateSelection(ITextView textView, ITextBuffer subjectBuffer, ITrackingSpan activeRenameSpan) + { + var snapshot = subjectBuffer.CurrentSnapshot; + var anchor = new VirtualSnapshotPoint(snapshot, this.currentState.SelectionAnchorPoint + activeRenameSpan.GetStartPoint(snapshot)); + var active = new VirtualSnapshotPoint(snapshot, this.currentState.SelectionActivePoint + activeRenameSpan.GetStartPoint(snapshot)); + textView.SetSelection(anchor, active); + } + + public void Undo(ITextBuffer _) + { + if (this.UndoStack.Count > 0) { - var snapshot = subjectBuffer.CurrentSnapshot; - var anchor = new VirtualSnapshotPoint(snapshot, this.currentState.SelectionAnchorPoint + activeRenameSpan.GetStartPoint(snapshot)); - var active = new VirtualSnapshotPoint(snapshot, this.currentState.SelectionActivePoint + activeRenameSpan.GetStartPoint(snapshot)); - textView.SetSelection(anchor, active); + this.RedoStack.Push(this.currentState); + this.currentState = this.UndoStack.Pop(); + this.InlineRenameService.ActiveSession.ApplyReplacementText(this.currentState.ReplacementText, propagateEditImmediately: true); } - - public void Undo(ITextBuffer _) + else { - if (this.UndoStack.Count > 0) - { - this.RedoStack.Push(this.currentState); - this.currentState = this.UndoStack.Pop(); - this.InlineRenameService.ActiveSession.ApplyReplacementText(this.currentState.ReplacementText, propagateEditImmediately: true); - } - else - { - this.InlineRenameService.ActiveSession.Cancel(); - } + this.InlineRenameService.ActiveSession.Cancel(); } + } - public void Redo(ITextBuffer _) + public void Redo(ITextBuffer _) + { + if (this.RedoStack.Count > 0) { - if (this.RedoStack.Count > 0) - { - this.UndoStack.Push(this.currentState); - this.currentState = this.RedoStack.Pop(); - this.InlineRenameService.ActiveSession.ApplyReplacementText(this.currentState.ReplacementText, propagateEditImmediately: true); - } + this.UndoStack.Push(this.currentState); + this.currentState = this.RedoStack.Pop(); + this.InlineRenameService.ActiveSession.ApplyReplacementText(this.currentState.ReplacementText, propagateEditImmediately: true); } + } - protected abstract void UndoTemporaryEdits(ITextBuffer subjectBuffer, bool disconnect, bool undoConflictResolution); + protected abstract void UndoTemporaryEdits(ITextBuffer subjectBuffer, bool disconnect, bool undoConflictResolution); - protected void ApplyReplacementText(ITextBuffer subjectBuffer, ITextUndoHistory undoHistory, object propagateSpansEditTag, IEnumerable spans, string replacementText) - { - // roll back to the initial state for the buffer after conflict resolution - this.UndoTemporaryEdits(subjectBuffer, disconnect: false, undoConflictResolution: replacementText == string.Empty); - - using var transaction = undoHistory.CreateTransaction(GetUndoTransactionDescription(replacementText)); - using var edit = subjectBuffer.CreateEdit(EditOptions.None, null, propagateSpansEditTag); + protected void ApplyReplacementText(ITextBuffer subjectBuffer, ITextUndoHistory undoHistory, object propagateSpansEditTag, IEnumerable spans, string replacementText) + { + // roll back to the initial state for the buffer after conflict resolution + this.UndoTemporaryEdits(subjectBuffer, disconnect: false, undoConflictResolution: replacementText == string.Empty); - foreach (var span in spans) - { - if (span.GetText(subjectBuffer.CurrentSnapshot) != replacementText) - { - edit.Replace(span.GetSpan(subjectBuffer.CurrentSnapshot), replacementText); - } - } + using var transaction = undoHistory.CreateTransaction(GetUndoTransactionDescription(replacementText)); + using var edit = subjectBuffer.CreateEdit(EditOptions.None, null, propagateSpansEditTag); - edit.ApplyAndLogExceptions(); - if (!edit.HasEffectiveChanges && !this.UndoStack.Any()) - { - transaction.Cancel(); - } - else + foreach (var span in spans) + { + if (span.GetText(subjectBuffer.CurrentSnapshot) != replacementText) { - transaction.Complete(); + edit.Replace(span.GetSpan(subjectBuffer.CurrentSnapshot), replacementText); } } - protected static string GetUndoTransactionDescription(string replacementText) - => replacementText == string.Empty ? "Delete Text" : replacementText; + edit.ApplyAndLogExceptions(); + if (!edit.HasEffectiveChanges && !this.UndoStack.Any()) + { + transaction.Cancel(); + } + else + { + transaction.Complete(); + } } + + protected static string GetUndoTransactionDescription(string replacementText) + => replacementText == string.Empty ? "Delete Text" : replacementText; } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler.cs index d8f1718d7fe18..72c71a55a3e51 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler.cs @@ -13,106 +13,105 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler { - internal abstract partial class AbstractRenameCommandHandler + private readonly IThreadingContext _threadingContext; + private readonly InlineRenameService _renameService; + private readonly IAsynchronousOperationListener _listener; + + protected AbstractRenameCommandHandler( + IThreadingContext threadingContext, + InlineRenameService renameService, + IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider) { - private readonly IThreadingContext _threadingContext; - private readonly InlineRenameService _renameService; - private readonly IAsynchronousOperationListener _listener; - - protected AbstractRenameCommandHandler( - IThreadingContext threadingContext, - InlineRenameService renameService, - IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider) - { - _threadingContext = threadingContext; - _renameService = renameService; - _listener = asynchronousOperationListenerProvider.GetListener(FeatureAttribute.Rename); - } + _threadingContext = threadingContext; + _renameService = renameService; + _listener = asynchronousOperationListenerProvider.GetListener(FeatureAttribute.Rename); + } - public string DisplayName => EditorFeaturesResources.Rename; + public string DisplayName => EditorFeaturesResources.Rename; - protected abstract bool AdornmentShouldReceiveKeyboardNavigation(ITextView textView); + protected abstract bool AdornmentShouldReceiveKeyboardNavigation(ITextView textView); - protected abstract void SetFocusToTextView(ITextView textView); + protected abstract void SetFocusToTextView(ITextView textView); - protected abstract void SetFocusToAdornment(ITextView textView); + protected abstract void SetFocusToAdornment(ITextView textView); - protected abstract void SetAdornmentFocusToPreviousElement(ITextView textView); + protected abstract void SetAdornmentFocusToPreviousElement(ITextView textView); - protected abstract void SetAdornmentFocusToNextElement(ITextView textView); + protected abstract void SetAdornmentFocusToNextElement(ITextView textView); - private CommandState GetCommandState(Func nextHandler) + private CommandState GetCommandState(Func nextHandler) + { + if (_renameService.ActiveSession != null) { - if (_renameService.ActiveSession != null) - { - return CommandState.Available; - } - - return nextHandler(); + return CommandState.Available; } - private CommandState GetCommandState() - => _renameService.ActiveSession != null ? CommandState.Available : CommandState.Unspecified; + return nextHandler(); + } + + private CommandState GetCommandState() + => _renameService.ActiveSession != null ? CommandState.Available : CommandState.Unspecified; - private void HandlePossibleTypingCommand(TArgs args, Action nextHandler, Action actionIfInsideActiveSpan) - where TArgs : EditorCommandArgs + private void HandlePossibleTypingCommand(TArgs args, Action nextHandler, Action actionIfInsideActiveSpan) + where TArgs : EditorCommandArgs + { + if (_renameService.ActiveSession == null) { - if (_renameService.ActiveSession == null) - { - nextHandler(); - return; - } - - var selectedSpans = args.TextView.Selection.GetSnapshotSpansOnBuffer(args.SubjectBuffer); - - if (selectedSpans.Count > 1) - { - // If we have multiple spans active, then that means we have something like box - // selection going on. In this case, we'll just forward along. - nextHandler(); - return; - } - - var singleSpan = selectedSpans.Single(); - if (_renameService.ActiveSession.TryGetContainingEditableSpan(singleSpan.Start, out var containingSpan) && - containingSpan.Contains(singleSpan)) - { - actionIfInsideActiveSpan(_renameService.ActiveSession, containingSpan); - } - else if (_renameService.ActiveSession.IsInOpenTextBuffer(singleSpan.Start)) - { - // It's in a read-only area that is open, so let's commit the rename - // and then let the character go through - - CommitIfActiveAndCallNextHandler(args, nextHandler); - } - else - { - nextHandler(); - return; - } + nextHandler(); + return; } - private void CommitIfActive(EditorCommandArgs args) - { - if (_renameService.ActiveSession != null) - { - var selection = args.TextView.Selection.VirtualSelectedSpans.First(); + var selectedSpans = args.TextView.Selection.GetSnapshotSpansOnBuffer(args.SubjectBuffer); - _renameService.ActiveSession.Commit(); + if (selectedSpans.Count > 1) + { + // If we have multiple spans active, then that means we have something like box + // selection going on. In this case, we'll just forward along. + nextHandler(); + return; + } - var translatedSelection = selection.TranslateTo(args.TextView.TextBuffer.CurrentSnapshot); - args.TextView.Selection.Select(translatedSelection.Start, translatedSelection.End); - args.TextView.Caret.MoveTo(translatedSelection.End); - } + var singleSpan = selectedSpans.Single(); + if (_renameService.ActiveSession.TryGetContainingEditableSpan(singleSpan.Start, out var containingSpan) && + containingSpan.Contains(singleSpan)) + { + actionIfInsideActiveSpan(_renameService.ActiveSession, containingSpan); } + else if (_renameService.ActiveSession.IsInOpenTextBuffer(singleSpan.Start)) + { + // It's in a read-only area that is open, so let's commit the rename + // and then let the character go through - private void CommitIfActiveAndCallNextHandler(EditorCommandArgs args, Action nextHandler) + CommitIfActiveAndCallNextHandler(args, nextHandler); + } + else { - CommitIfActive(args); nextHandler(); + return; } } + + private void CommitIfActive(EditorCommandArgs args) + { + if (_renameService.ActiveSession != null) + { + var selection = args.TextView.Selection.VirtualSelectedSpans.First(); + + _renameService.ActiveSession.Commit(); + + var translatedSelection = selection.TranslateTo(args.TextView.TextBuffer.CurrentSnapshot); + args.TextView.Selection.Select(translatedSelection.Start, translatedSelection.End); + args.TextView.Caret.MoveTo(translatedSelection.End); + } + } + + private void CommitIfActiveAndCallNextHandler(EditorCommandArgs args, Action nextHandler) + { + CommitIfActive(args); + nextHandler(); + } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_BackspaceDeleteHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_BackspaceDeleteHandler.cs index 65061b194a36a..21c6c04c1dea9 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_BackspaceDeleteHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_BackspaceDeleteHandler.cs @@ -9,38 +9,37 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : IChainedCommandHandler, IChainedCommandHandler { - internal abstract partial class AbstractRenameCommandHandler : IChainedCommandHandler, IChainedCommandHandler - { - public CommandState GetCommandState(BackspaceKeyCommandArgs args, Func nextHandler) - => GetCommandState(nextHandler); + public CommandState GetCommandState(BackspaceKeyCommandArgs args, Func nextHandler) + => GetCommandState(nextHandler); - public CommandState GetCommandState(DeleteKeyCommandArgs args, Func nextHandler) - => GetCommandState(nextHandler); + public CommandState GetCommandState(DeleteKeyCommandArgs args, Func nextHandler) + => GetCommandState(nextHandler); - public void ExecuteCommand(BackspaceKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) - { - HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => + public void ExecuteCommand(BackspaceKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => + { + var caretPoint = args.TextView.GetCaretPoint(args.SubjectBuffer); + if (!args.TextView.Selection.IsEmpty || caretPoint.Value != span.Start) { - var caretPoint = args.TextView.GetCaretPoint(args.SubjectBuffer); - if (!args.TextView.Selection.IsEmpty || caretPoint.Value != span.Start) - { - nextHandler(); - } - }); - } + nextHandler(); + } + }); + } - public void ExecuteCommand(DeleteKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) - { - HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => + public void ExecuteCommand(DeleteKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => + { + var caretPoint = args.TextView.GetCaretPoint(args.SubjectBuffer); + if (!args.TextView.Selection.IsEmpty || caretPoint.Value != span.End) { - var caretPoint = args.TextView.GetCaretPoint(args.SubjectBuffer); - if (!args.TextView.Selection.IsEmpty || caretPoint.Value != span.End) - { - nextHandler(); - } - }); - } + nextHandler(); + } + }); } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_CutPasteHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_CutPasteHandler.cs index 3fd9053dc5adb..3448d2eda764c 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_CutPasteHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_CutPasteHandler.cs @@ -6,31 +6,30 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : + IChainedCommandHandler, IChainedCommandHandler { - internal abstract partial class AbstractRenameCommandHandler : - IChainedCommandHandler, IChainedCommandHandler - { - public CommandState GetCommandState(CutCommandArgs args, Func nextHandler) - => nextHandler(); + public CommandState GetCommandState(CutCommandArgs args, Func nextHandler) + => nextHandler(); - public void ExecuteCommand(CutCommandArgs args, Action nextHandler, CommandExecutionContext context) + public void ExecuteCommand(CutCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => { - HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => - { - nextHandler(); - }); - } + nextHandler(); + }); + } - public CommandState GetCommandState(PasteCommandArgs args, Func nextHandler) - => nextHandler(); + public CommandState GetCommandState(PasteCommandArgs args, Func nextHandler) + => nextHandler(); - public void ExecuteCommand(PasteCommandArgs args, Action nextHandler, CommandExecutionContext context) + public void ExecuteCommand(PasteCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => { - HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => - { - nextHandler(); - }); - } + nextHandler(); + }); } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_EscapeHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_EscapeHandler.cs index b906f20217a1f..9d80929da596a 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_EscapeHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_EscapeHandler.cs @@ -5,23 +5,22 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : ICommandHandler { - internal abstract partial class AbstractRenameCommandHandler : ICommandHandler - { - public CommandState GetCommandState(EscapeKeyCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(EscapeKeyCommandArgs args) + => GetCommandState(); - public bool ExecuteCommand(EscapeKeyCommandArgs args, CommandExecutionContext context) + public bool ExecuteCommand(EscapeKeyCommandArgs args, CommandExecutionContext context) + { + if (_renameService.ActiveSession != null) { - if (_renameService.ActiveSession != null) - { - _renameService.ActiveSession.Cancel(); - SetFocusToTextView(args.TextView); - return true; - } - - return false; + _renameService.ActiveSession.Cancel(); + SetFocusToTextView(args.TextView); + return true; } + + return false; } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_LineStartEndHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_LineStartEndHandler.cs index c19bd9e5b609e..6a78bc4b23ae0 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_LineStartEndHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_LineStartEndHandler.cs @@ -8,83 +8,82 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : + ICommandHandler, ICommandHandler, + ICommandHandler, ICommandHandler { - internal abstract partial class AbstractRenameCommandHandler : - ICommandHandler, ICommandHandler, - ICommandHandler, ICommandHandler - { - public CommandState GetCommandState(LineStartCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(LineStartCommandArgs args) + => GetCommandState(); - public CommandState GetCommandState(LineEndCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(LineEndCommandArgs args) + => GetCommandState(); - public CommandState GetCommandState(LineStartExtendCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(LineStartExtendCommandArgs args) + => GetCommandState(); - public CommandState GetCommandState(LineEndExtendCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(LineEndExtendCommandArgs args) + => GetCommandState(); - public bool ExecuteCommand(LineStartCommandArgs args, CommandExecutionContext context) - => HandleLineStartOrLineEndCommand(args.SubjectBuffer, args.TextView, lineStart: true, extendSelection: false); + public bool ExecuteCommand(LineStartCommandArgs args, CommandExecutionContext context) + => HandleLineStartOrLineEndCommand(args.SubjectBuffer, args.TextView, lineStart: true, extendSelection: false); - public bool ExecuteCommand(LineEndCommandArgs args, CommandExecutionContext context) - => HandleLineStartOrLineEndCommand(args.SubjectBuffer, args.TextView, lineStart: false, extendSelection: false); + public bool ExecuteCommand(LineEndCommandArgs args, CommandExecutionContext context) + => HandleLineStartOrLineEndCommand(args.SubjectBuffer, args.TextView, lineStart: false, extendSelection: false); - public bool ExecuteCommand(LineStartExtendCommandArgs args, CommandExecutionContext context) - => HandleLineStartOrLineEndCommand(args.SubjectBuffer, args.TextView, lineStart: true, extendSelection: true); + public bool ExecuteCommand(LineStartExtendCommandArgs args, CommandExecutionContext context) + => HandleLineStartOrLineEndCommand(args.SubjectBuffer, args.TextView, lineStart: true, extendSelection: true); - public bool ExecuteCommand(LineEndExtendCommandArgs args, CommandExecutionContext context) - => HandleLineStartOrLineEndCommand(args.SubjectBuffer, args.TextView, lineStart: false, extendSelection: true); + public bool ExecuteCommand(LineEndExtendCommandArgs args, CommandExecutionContext context) + => HandleLineStartOrLineEndCommand(args.SubjectBuffer, args.TextView, lineStart: false, extendSelection: true); - private bool HandleLineStartOrLineEndCommand(ITextBuffer subjectBuffer, ITextView view, bool lineStart, bool extendSelection) + private bool HandleLineStartOrLineEndCommand(ITextBuffer subjectBuffer, ITextView view, bool lineStart, bool extendSelection) + { + if (_renameService.ActiveSession == null) { - if (_renameService.ActiveSession == null) - { - return false; - } + return false; + } - var caretPoint = view.GetCaretPoint(subjectBuffer); - if (caretPoint.HasValue) + var caretPoint = view.GetCaretPoint(subjectBuffer); + if (caretPoint.HasValue) + { + if (_renameService.ActiveSession.TryGetContainingEditableSpan(caretPoint.Value, out var span)) { - if (_renameService.ActiveSession.TryGetContainingEditableSpan(caretPoint.Value, out var span)) + var newPoint = lineStart ? span.Start : span.End; + if (newPoint == caretPoint.Value && (view.Selection.IsEmpty || extendSelection)) { - var newPoint = lineStart ? span.Start : span.End; - if (newPoint == caretPoint.Value && (view.Selection.IsEmpty || extendSelection)) - { - // We're already at a boundary, let the editor handle the command - return false; - } - - // The PointTrackingMode should not matter because we are not tracking between - // versions, and the PositionAffinity is set towards the identifier. - var newPointInView = view.BufferGraph.MapUpToBuffer( - newPoint, - PointTrackingMode.Negative, - lineStart ? PositionAffinity.Successor : PositionAffinity.Predecessor, - view.TextBuffer); - - if (!newPointInView.HasValue) - { - return false; - } - - if (extendSelection) - { - view.Selection.Select(view.Selection.AnchorPoint, new VirtualSnapshotPoint(newPointInView.Value)); - } - else - { - view.Selection.Clear(); - } - - view.Caret.MoveTo(newPointInView.Value); - return true; + // We're already at a boundary, let the editor handle the command + return false; } - } - return false; + // The PointTrackingMode should not matter because we are not tracking between + // versions, and the PositionAffinity is set towards the identifier. + var newPointInView = view.BufferGraph.MapUpToBuffer( + newPoint, + PointTrackingMode.Negative, + lineStart ? PositionAffinity.Successor : PositionAffinity.Predecessor, + view.TextBuffer); + + if (!newPointInView.HasValue) + { + return false; + } + + if (extendSelection) + { + view.Selection.Select(view.Selection.AnchorPoint, new VirtualSnapshotPoint(newPointInView.Value)); + } + else + { + view.Selection.Clear(); + } + + view.Caret.MoveTo(newPointInView.Value); + return true; + } } + + return false; } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_MoveSelectedLinesHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_MoveSelectedLinesHandler.cs index 22d597c9876d4..ffee45ee36f18 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_MoveSelectedLinesHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_MoveSelectedLinesHandler.cs @@ -5,27 +5,26 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : + ICommandHandler, ICommandHandler { - internal abstract partial class AbstractRenameCommandHandler : - ICommandHandler, ICommandHandler - { - public CommandState GetCommandState(MoveSelectedLinesUpCommandArgs args) - => CommandState.Unspecified; + public CommandState GetCommandState(MoveSelectedLinesUpCommandArgs args) + => CommandState.Unspecified; - public bool ExecuteCommand(MoveSelectedLinesUpCommandArgs args, CommandExecutionContext context) - { - CommitIfActive(args); - return false; - } + public bool ExecuteCommand(MoveSelectedLinesUpCommandArgs args, CommandExecutionContext context) + { + CommitIfActive(args); + return false; + } - public CommandState GetCommandState(MoveSelectedLinesDownCommandArgs args) - => CommandState.Unspecified; + public CommandState GetCommandState(MoveSelectedLinesDownCommandArgs args) + => CommandState.Unspecified; - public bool ExecuteCommand(MoveSelectedLinesDownCommandArgs args, CommandExecutionContext context) - { - CommitIfActive(args); - return false; - } + public bool ExecuteCommand(MoveSelectedLinesDownCommandArgs args, CommandExecutionContext context) + { + CommitIfActive(args); + return false; } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_OpenLineAboveHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_OpenLineAboveHandler.cs index 98fcc54a41cf4..6670627524316 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_OpenLineAboveHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_OpenLineAboveHandler.cs @@ -6,20 +6,19 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : IChainedCommandHandler { - internal abstract partial class AbstractRenameCommandHandler : IChainedCommandHandler - { - public CommandState GetCommandState(OpenLineAboveCommandArgs args, Func nextHandler) - => GetCommandState(nextHandler); + public CommandState GetCommandState(OpenLineAboveCommandArgs args, Func nextHandler) + => GetCommandState(nextHandler); - public void ExecuteCommand(OpenLineAboveCommandArgs args, Action nextHandler, CommandExecutionContext context) + public void ExecuteCommand(OpenLineAboveCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => { - HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => - { - activeSession.Commit(); - nextHandler(); - }); - } + activeSession.Commit(); + nextHandler(); + }); } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_OpenLineBelowHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_OpenLineBelowHandler.cs index 0322a43b6ff5a..e8649fbf5577c 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_OpenLineBelowHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_OpenLineBelowHandler.cs @@ -6,20 +6,19 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : IChainedCommandHandler { - internal abstract partial class AbstractRenameCommandHandler : IChainedCommandHandler - { - public CommandState GetCommandState(OpenLineBelowCommandArgs args, Func nextHandler) - => GetCommandState(nextHandler); + public CommandState GetCommandState(OpenLineBelowCommandArgs args, Func nextHandler) + => GetCommandState(nextHandler); - public void ExecuteCommand(OpenLineBelowCommandArgs args, Action nextHandler, CommandExecutionContext context) + public void ExecuteCommand(OpenLineBelowCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => { - HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => - { - activeSession.Commit(); - nextHandler(); - }); - } + activeSession.Commit(); + nextHandler(); + }); } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RefactoringWithCommandHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RefactoringWithCommandHandler.cs index 7377b9a37782d..8c403e0d14712 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RefactoringWithCommandHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RefactoringWithCommandHandler.cs @@ -5,48 +5,47 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : + ICommandHandler, + ICommandHandler, + ICommandHandler, + ICommandHandler { - internal abstract partial class AbstractRenameCommandHandler : - ICommandHandler, - ICommandHandler, - ICommandHandler, - ICommandHandler + public CommandState GetCommandState(ReorderParametersCommandArgs args) + => CommandState.Unspecified; + + public bool ExecuteCommand(ReorderParametersCommandArgs args, CommandExecutionContext context) + { + CommitIfActive(args); + return false; + } + + public CommandState GetCommandState(RemoveParametersCommandArgs args) + => CommandState.Unspecified; + + public bool ExecuteCommand(RemoveParametersCommandArgs args, CommandExecutionContext context) + { + CommitIfActive(args); + return false; + } + + public CommandState GetCommandState(ExtractInterfaceCommandArgs args) + => CommandState.Unspecified; + + public bool ExecuteCommand(ExtractInterfaceCommandArgs args, CommandExecutionContext context) + { + CommitIfActive(args); + return false; + } + + public CommandState GetCommandState(EncapsulateFieldCommandArgs args) + => CommandState.Unspecified; + + public bool ExecuteCommand(EncapsulateFieldCommandArgs args, CommandExecutionContext context) { - public CommandState GetCommandState(ReorderParametersCommandArgs args) - => CommandState.Unspecified; - - public bool ExecuteCommand(ReorderParametersCommandArgs args, CommandExecutionContext context) - { - CommitIfActive(args); - return false; - } - - public CommandState GetCommandState(RemoveParametersCommandArgs args) - => CommandState.Unspecified; - - public bool ExecuteCommand(RemoveParametersCommandArgs args, CommandExecutionContext context) - { - CommitIfActive(args); - return false; - } - - public CommandState GetCommandState(ExtractInterfaceCommandArgs args) - => CommandState.Unspecified; - - public bool ExecuteCommand(ExtractInterfaceCommandArgs args, CommandExecutionContext context) - { - CommitIfActive(args); - return false; - } - - public CommandState GetCommandState(EncapsulateFieldCommandArgs args) - => CommandState.Unspecified; - - public bool ExecuteCommand(EncapsulateFieldCommandArgs args, CommandExecutionContext context) - { - CommitIfActive(args); - return false; - } + CommitIfActive(args); + return false; } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RenameHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RenameHandler.cs index 0f42f61c2f503..3613ed200f053 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RenameHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RenameHandler.cs @@ -18,121 +18,120 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : ICommandHandler { - internal abstract partial class AbstractRenameCommandHandler : ICommandHandler + public CommandState GetCommandState(RenameCommandArgs args) { - public CommandState GetCommandState(RenameCommandArgs args) + var caretPoint = args.TextView.GetCaretPoint(args.SubjectBuffer); + if (!caretPoint.HasValue) { - var caretPoint = args.TextView.GetCaretPoint(args.SubjectBuffer); - if (!caretPoint.HasValue) - { - return CommandState.Unspecified; - } - - if (!CanRename(args)) - { - return CommandState.Unspecified; - } - - return CommandState.Available; + return CommandState.Unspecified; } - public bool ExecuteCommand(RenameCommandArgs args, CommandExecutionContext context) + if (!CanRename(args)) { - if (!CanRename(args)) - { - return false; - } - - var token = _listener.BeginAsyncOperation(nameof(ExecuteCommand)); - _ = ExecuteCommandAsync(args).CompletesAsyncOperation(token); - return true; + return CommandState.Unspecified; } - private async Task ExecuteCommandAsync(RenameCommandArgs args) - { - _threadingContext.ThrowIfNotOnUIThread(); + return CommandState.Available; + } - if (!args.SubjectBuffer.TryGetWorkspace(out var workspace)) - { - return; - } + public bool ExecuteCommand(RenameCommandArgs args, CommandExecutionContext context) + { + if (!CanRename(args)) + { + return false; + } - var caretPoint = args.TextView.GetCaretPoint(args.SubjectBuffer); - if (!caretPoint.HasValue) - { - await ShowErrorDialogAsync(workspace, FeaturesResources.You_must_rename_an_identifier).ConfigureAwait(false); - return; - } + var token = _listener.BeginAsyncOperation(nameof(ExecuteCommand)); + _ = ExecuteCommandAsync(args).CompletesAsyncOperation(token); + return true; + } - var backgroundWorkIndicatorFactory = workspace.Services.GetRequiredService(); - using var context = backgroundWorkIndicatorFactory.Create( - args.TextView, - args.TextView.GetTextElementSpan(caretPoint.Value), - EditorFeaturesResources.Finding_token_to_rename); + private async Task ExecuteCommandAsync(RenameCommandArgs args) + { + _threadingContext.ThrowIfNotOnUIThread(); - // If there is already an active session, commit it first - if (_renameService.ActiveSession != null) - { - // Is the caret within any of the rename fields in this buffer? - // If so, focus the dashboard - if (_renameService.ActiveSession.TryGetContainingEditableSpan(caretPoint.Value, out _)) - { - SetFocusToAdornment(args.TextView); - return; - } - else - { - // Otherwise, commit the existing session and start a new one. - _renameService.ActiveSession.Commit(); - } - } + if (!args.SubjectBuffer.TryGetWorkspace(out var workspace)) + { + return; + } - var cancellationToken = context.UserCancellationToken; + var caretPoint = args.TextView.GetCaretPoint(args.SubjectBuffer); + if (!caretPoint.HasValue) + { + await ShowErrorDialogAsync(workspace, FeaturesResources.You_must_rename_an_identifier).ConfigureAwait(false); + return; + } - var document = await args - .SubjectBuffer - .CurrentSnapshot - .GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context) - .ConfigureAwait(false); + var backgroundWorkIndicatorFactory = workspace.Services.GetRequiredService(); + using var context = backgroundWorkIndicatorFactory.Create( + args.TextView, + args.TextView.GetTextElementSpan(caretPoint.Value), + EditorFeaturesResources.Finding_token_to_rename); - if (document == null) + // If there is already an active session, commit it first + if (_renameService.ActiveSession != null) + { + // Is the caret within any of the rename fields in this buffer? + // If so, focus the dashboard + if (_renameService.ActiveSession.TryGetContainingEditableSpan(caretPoint.Value, out _)) { - await ShowErrorDialogAsync(workspace, FeaturesResources.You_must_rename_an_identifier).ConfigureAwait(false); + SetFocusToAdornment(args.TextView); return; } - - var selectedSpans = args.TextView.Selection.GetSnapshotSpansOnBuffer(args.SubjectBuffer); - - // Now make sure the entire selection is contained within that token. - // There can be zero selectedSpans in projection scenarios. - if (selectedSpans.Count != 1) + else { - await ShowErrorDialogAsync(workspace, FeaturesResources.You_must_rename_an_identifier).ConfigureAwait(false); - return; + // Otherwise, commit the existing session and start a new one. + _renameService.ActiveSession.Commit(); } + } - var sessionInfo = await _renameService.StartInlineSessionAsync(document, selectedSpans.Single().Span.ToTextSpan(), cancellationToken).ConfigureAwait(false); - if (!sessionInfo.CanRename) - { - await ShowErrorDialogAsync(workspace, sessionInfo.LocalizedErrorMessage).ConfigureAwait(false); - return; - } + var cancellationToken = context.UserCancellationToken; + + var document = await args + .SubjectBuffer + .CurrentSnapshot + .GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context) + .ConfigureAwait(false); + + if (document == null) + { + await ShowErrorDialogAsync(workspace, FeaturesResources.You_must_rename_an_identifier).ConfigureAwait(false); + return; } - private static bool CanRename(RenameCommandArgs args) + var selectedSpans = args.TextView.Selection.GetSnapshotSpansOnBuffer(args.SubjectBuffer); + + // Now make sure the entire selection is contained within that token. + // There can be zero selectedSpans in projection scenarios. + if (selectedSpans.Count != 1) { - return args.SubjectBuffer.TryGetWorkspace(out var workspace) && - workspace.CanApplyChange(ApplyChangesKind.ChangeDocument) && - args.SubjectBuffer.SupportsRename() && !args.SubjectBuffer.IsInLspEditorContext(); + await ShowErrorDialogAsync(workspace, FeaturesResources.You_must_rename_an_identifier).ConfigureAwait(false); + return; } - private async Task ShowErrorDialogAsync(Workspace workspace, string message) + var sessionInfo = await _renameService.StartInlineSessionAsync(document, selectedSpans.Single().Span.ToTextSpan(), cancellationToken).ConfigureAwait(false); + if (!sessionInfo.CanRename) { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(); - var notificationService = workspace.Services.GetService(); - notificationService.SendNotification(message, title: EditorFeaturesResources.Rename, severity: NotificationSeverity.Error); + await ShowErrorDialogAsync(workspace, sessionInfo.LocalizedErrorMessage).ConfigureAwait(false); + return; } } + + private static bool CanRename(RenameCommandArgs args) + { + return args.SubjectBuffer.TryGetWorkspace(out var workspace) && + workspace.CanApplyChange(ApplyChangesKind.ChangeDocument) && + args.SubjectBuffer.SupportsRename() && !args.SubjectBuffer.IsInLspEditorContext(); + } + + private async Task ShowErrorDialogAsync(Workspace workspace, string message) + { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(); + var notificationService = workspace.Services.GetService(); + notificationService.SendNotification(message, title: EditorFeaturesResources.Rename, severity: NotificationSeverity.Error); + } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_ReturnHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_ReturnHandler.cs index 3f0efc43b8f7f..0206190d50a4e 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_ReturnHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_ReturnHandler.cs @@ -7,32 +7,31 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : ICommandHandler { - internal abstract partial class AbstractRenameCommandHandler : ICommandHandler - { - public CommandState GetCommandState(ReturnKeyCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(ReturnKeyCommandArgs args) + => GetCommandState(); - public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) + public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) + { + if (_renameService.ActiveSession != null) { - if (_renameService.ActiveSession != null) - { - // Prevent Editor's typing responsiveness auto canceling the rename operation. - // InlineRenameSession will call IUIThreadOperationExecutor to sets up our own IUIThreadOperationContext - context.OperationContext.TakeOwnership(); + // Prevent Editor's typing responsiveness auto canceling the rename operation. + // InlineRenameSession will call IUIThreadOperationExecutor to sets up our own IUIThreadOperationContext + context.OperationContext.TakeOwnership(); - Commit(_renameService.ActiveSession, args.TextView); - return true; - } - - return false; + Commit(_renameService.ActiveSession, args.TextView); + return true; } - protected virtual void Commit(InlineRenameSession activeSession, ITextView textView) - { - activeSession.Commit(); - SetFocusToTextView(textView); - } + return false; + } + + protected virtual void Commit(InlineRenameSession activeSession, ITextView textView) + { + activeSession.Commit(); + SetFocusToTextView(textView); } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_SaveHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_SaveHandler.cs index e0f0236dfaf88..4a9bb0c50b1e1 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_SaveHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_SaveHandler.cs @@ -5,22 +5,21 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : ICommandHandler { - internal abstract partial class AbstractRenameCommandHandler : ICommandHandler - { - public CommandState GetCommandState(SaveCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(SaveCommandArgs args) + => GetCommandState(); - public bool ExecuteCommand(SaveCommandArgs args, CommandExecutionContext context) + public bool ExecuteCommand(SaveCommandArgs args, CommandExecutionContext context) + { + if (_renameService.ActiveSession != null) { - if (_renameService.ActiveSession != null) - { - _renameService.ActiveSession.Commit(); - SetFocusToTextView(args.TextView); - } - - return false; + _renameService.ActiveSession.Commit(); + SetFocusToTextView(args.TextView); } + + return false; } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_SelectAllHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_SelectAllHandler.cs index 5a575c2e0ca21..037ebed9e5189 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_SelectAllHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_SelectAllHandler.cs @@ -8,39 +8,38 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : + ICommandHandler { - internal abstract partial class AbstractRenameCommandHandler : - ICommandHandler - { - public CommandState GetCommandState(SelectAllCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(SelectAllCommandArgs args) + => GetCommandState(); - public bool ExecuteCommand(SelectAllCommandArgs args, CommandExecutionContext context) - => ExecuteSelectAll(args.SubjectBuffer, args.TextView); + public bool ExecuteCommand(SelectAllCommandArgs args, CommandExecutionContext context) + => ExecuteSelectAll(args.SubjectBuffer, args.TextView); - private bool ExecuteSelectAll(ITextBuffer subjectBuffer, ITextView view) + private bool ExecuteSelectAll(ITextBuffer subjectBuffer, ITextView view) + { + if (_renameService.ActiveSession == null) { - if (_renameService.ActiveSession == null) - { - return false; - } + return false; + } - var caretPoint = view.GetCaretPoint(subjectBuffer); - if (caretPoint.HasValue) + var caretPoint = view.GetCaretPoint(subjectBuffer); + if (caretPoint.HasValue) + { + if (_renameService.ActiveSession.TryGetContainingEditableSpan(caretPoint.Value, out var span)) { - if (_renameService.ActiveSession.TryGetContainingEditableSpan(caretPoint.Value, out var span)) + if (view.Selection.Start.Position != span.Start.Position || + view.Selection.End.Position != span.End.Position) { - if (view.Selection.Start.Position != span.Start.Position || - view.Selection.End.Position != span.End.Position) - { - view.SetSelection(span); - return true; - } + view.SetSelection(span); + return true; } } - - return false; } + + return false; } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_TabHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_TabHandler.cs index 82b8e618a0d01..b7cff5b4ed1c7 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_TabHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_TabHandler.cs @@ -8,59 +8,58 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : + IChainedCommandHandler, + IChainedCommandHandler { - internal abstract partial class AbstractRenameCommandHandler : - IChainedCommandHandler, - IChainedCommandHandler + public CommandState GetCommandState(TabKeyCommandArgs args, Func nextHandler) + => GetCommandState(nextHandler); + + public void ExecuteCommand(TabKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) { - public CommandState GetCommandState(TabKeyCommandArgs args, Func nextHandler) - => GetCommandState(nextHandler); + // If the Dashboard is focused, just navigate through its UI. + if (AdornmentShouldReceiveKeyboardNavigation(args.TextView)) + { + SetAdornmentFocusToNextElement(args.TextView); + return; + } - public void ExecuteCommand(TabKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) + HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => { - // If the Dashboard is focused, just navigate through its UI. - if (AdornmentShouldReceiveKeyboardNavigation(args.TextView)) - { - SetAdornmentFocusToNextElement(args.TextView); - return; - } + var spans = new NormalizedSnapshotSpanCollection( + activeSession.GetBufferManager(args.SubjectBuffer) + .GetEditableSpansForSnapshot(args.SubjectBuffer.CurrentSnapshot)); - HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => + for (var i = 0; i < spans.Count; i++) { - var spans = new NormalizedSnapshotSpanCollection( - activeSession.GetBufferManager(args.SubjectBuffer) - .GetEditableSpansForSnapshot(args.SubjectBuffer.CurrentSnapshot)); - - for (var i = 0; i < spans.Count; i++) + if (span == spans[i]) { - if (span == spans[i]) - { - var selectNext = i < spans.Count - 1 ? i + 1 : 0; - var newSelection = spans[selectNext]; - args.TextView.TryMoveCaretToAndEnsureVisible(newSelection.Start); - args.TextView.SetSelection(newSelection); - break; - } + var selectNext = i < spans.Count - 1 ? i + 1 : 0; + var newSelection = spans[selectNext]; + args.TextView.TryMoveCaretToAndEnsureVisible(newSelection.Start); + args.TextView.SetSelection(newSelection); + break; } - }); - } + } + }); + } - public CommandState GetCommandState(BackTabKeyCommandArgs args, Func nextHandler) - => GetCommandState(nextHandler); + public CommandState GetCommandState(BackTabKeyCommandArgs args, Func nextHandler) + => GetCommandState(nextHandler); - public void ExecuteCommand(BackTabKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) + public void ExecuteCommand(BackTabKeyCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + // If the Dashboard is focused, just navigate through its UI. + if (AdornmentShouldReceiveKeyboardNavigation(args.TextView)) { - // If the Dashboard is focused, just navigate through its UI. - if (AdornmentShouldReceiveKeyboardNavigation(args.TextView)) - { - SetAdornmentFocusToPreviousElement(args.TextView); - return; - } - else - { - nextHandler(); - } + SetAdornmentFocusToPreviousElement(args.TextView); + return; + } + else + { + nextHandler(); } } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_TypeCharHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_TypeCharHandler.cs index 12eddc3e75e0c..6b1da07bed52b 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_TypeCharHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_TypeCharHandler.cs @@ -9,37 +9,36 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : + IChainedCommandHandler { - internal abstract partial class AbstractRenameCommandHandler : - IChainedCommandHandler - { - public CommandState GetCommandState(TypeCharCommandArgs args, Func nextHandler) - => GetCommandState(nextHandler); + public CommandState GetCommandState(TypeCharCommandArgs args, Func nextHandler) + => GetCommandState(nextHandler); - public void ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context) + public void ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context) + { + HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => { - HandlePossibleTypingCommand(args, nextHandler, (activeSession, span) => + var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) { - var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - nextHandler(); - return; - } + nextHandler(); + return; + } - var syntaxFactsService = document.GetLanguageService(); + var syntaxFactsService = document.GetLanguageService(); - // We are inside the region we can edit, so let's forward only if it's a valid - // character - if (syntaxFactsService == null || - syntaxFactsService.IsIdentifierStartCharacter(args.TypedChar) || - syntaxFactsService.IsIdentifierPartCharacter(args.TypedChar) || - syntaxFactsService.IsStartOfUnicodeEscapeSequence(args.TypedChar)) - { - nextHandler(); - } - }); - } + // We are inside the region we can edit, so let's forward only if it's a valid + // character + if (syntaxFactsService == null || + syntaxFactsService.IsIdentifierStartCharacter(args.TypedChar) || + syntaxFactsService.IsIdentifierPartCharacter(args.TypedChar) || + syntaxFactsService.IsStartOfUnicodeEscapeSequence(args.TypedChar)) + { + nextHandler(); + } + }); } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_UndoRedoHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_UndoRedoHandler.cs index 7d07e0b0a5562..3dd653f02acba 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_UndoRedoHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_UndoRedoHandler.cs @@ -5,45 +5,44 @@ using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : + ICommandHandler, ICommandHandler { - internal abstract partial class AbstractRenameCommandHandler : - ICommandHandler, ICommandHandler - { - public CommandState GetCommandState(UndoCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(UndoCommandArgs args) + => GetCommandState(); - public CommandState GetCommandState(RedoCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(RedoCommandArgs args) + => GetCommandState(); - public bool ExecuteCommand(UndoCommandArgs args, CommandExecutionContext context) + public bool ExecuteCommand(UndoCommandArgs args, CommandExecutionContext context) + { + if (_renameService.ActiveSession != null) { - if (_renameService.ActiveSession != null) + for (var i = 0; i < args.Count && _renameService.ActiveSession != null; i++) { - for (var i = 0; i < args.Count && _renameService.ActiveSession != null; i++) - { - _renameService.ActiveSession.UndoManager.Undo(args.SubjectBuffer); - } - - return true; + _renameService.ActiveSession.UndoManager.Undo(args.SubjectBuffer); } - return false; + return true; } - public bool ExecuteCommand(RedoCommandArgs args, CommandExecutionContext context) + return false; + } + + public bool ExecuteCommand(RedoCommandArgs args, CommandExecutionContext context) + { + if (_renameService.ActiveSession != null) { - if (_renameService.ActiveSession != null) + for (var i = 0; i < args.Count && _renameService.ActiveSession != null; i++) { - for (var i = 0; i < args.Count && _renameService.ActiveSession != null; i++) - { - _renameService.ActiveSession.UndoManager.Redo(args.SubjectBuffer); - } - - return true; + _renameService.ActiveSession.UndoManager.Redo(args.SubjectBuffer); } - return false; + return true; } + + return false; } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_WordDeleteHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_WordDeleteHandler.cs index e05844bc8c9d3..af47ca63fc252 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_WordDeleteHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_WordDeleteHandler.cs @@ -9,63 +9,62 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract partial class AbstractRenameCommandHandler : + ICommandHandler, + ICommandHandler { - internal abstract partial class AbstractRenameCommandHandler : - ICommandHandler, - ICommandHandler - { - public CommandState GetCommandState(WordDeleteToStartCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(WordDeleteToStartCommandArgs args) + => GetCommandState(); - public CommandState GetCommandState(WordDeleteToEndCommandArgs args) - => GetCommandState(); + public CommandState GetCommandState(WordDeleteToEndCommandArgs args) + => GetCommandState(); - public bool ExecuteCommand(WordDeleteToStartCommandArgs args, CommandExecutionContext context) - => HandleWordDeleteCommand(args.SubjectBuffer, args.TextView, deleteToStart: true); + public bool ExecuteCommand(WordDeleteToStartCommandArgs args, CommandExecutionContext context) + => HandleWordDeleteCommand(args.SubjectBuffer, args.TextView, deleteToStart: true); - public bool ExecuteCommand(WordDeleteToEndCommandArgs args, CommandExecutionContext context) - => HandleWordDeleteCommand(args.SubjectBuffer, args.TextView, deleteToStart: false); + public bool ExecuteCommand(WordDeleteToEndCommandArgs args, CommandExecutionContext context) + => HandleWordDeleteCommand(args.SubjectBuffer, args.TextView, deleteToStart: false); - private bool HandleWordDeleteCommand(ITextBuffer subjectBuffer, ITextView view, bool deleteToStart) + private bool HandleWordDeleteCommand(ITextBuffer subjectBuffer, ITextView view, bool deleteToStart) + { + if (_renameService.ActiveSession == null) { - if (_renameService.ActiveSession == null) - { - return false; - } + return false; + } - var caretPoint = view.GetCaretPoint(subjectBuffer); - if (caretPoint.HasValue) + var caretPoint = view.GetCaretPoint(subjectBuffer); + if (caretPoint.HasValue) + { + if (_renameService.ActiveSession.TryGetContainingEditableSpan(caretPoint.Value, out var span)) { - if (_renameService.ActiveSession.TryGetContainingEditableSpan(caretPoint.Value, out var span)) + int start = caretPoint.Value; + int end = caretPoint.Value; + if (!view.Selection.IsEmpty) { - int start = caretPoint.Value; - int end = caretPoint.Value; - if (!view.Selection.IsEmpty) + var selectedSpans = view.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); + if (selectedSpans.Count == 1 && span.Contains(selectedSpans.Single().Span)) { - var selectedSpans = view.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); - if (selectedSpans.Count == 1 && span.Contains(selectedSpans.Single().Span)) - { - // We might want to delete past the caret's active position if there's a selection - start = selectedSpans.Single().Start; - end = selectedSpans.Single().End; - } - else - { - // we're outside of an editable span, so let this command go to the next handler - return false; - } + // We might want to delete past the caret's active position if there's a selection + start = selectedSpans.Single().Start; + end = selectedSpans.Single().End; } + else + { + // we're outside of an editable span, so let this command go to the next handler + return false; + } + } - subjectBuffer.Delete(deleteToStart - ? Span.FromBounds(span.Start, end) - : Span.FromBounds(start, span.End)); + subjectBuffer.Delete(deleteToStart + ? Span.FromBounds(span.Start, end) + : Span.FromBounds(start, span.End)); - return true; - } + return true; } - - return false; } + + return false; } } diff --git a/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameConflictTag.cs b/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameConflictTag.cs index a08549dac76b2..76bd2c88b16f9 100644 --- a/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameConflictTag.cs +++ b/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameConflictTag.cs @@ -4,18 +4,17 @@ using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename.HighlightTags +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename.HighlightTags; + +internal class RenameConflictTag : TextMarkerTag { - internal class RenameConflictTag : TextMarkerTag - { - // Only used for theming, does not need localized - internal const string TagId = "RoslynRenameConflictTag"; + // Only used for theming, does not need localized + internal const string TagId = "RoslynRenameConflictTag"; - public static readonly RenameConflictTag Instance = new RenameConflictTag(); + public static readonly RenameConflictTag Instance = new RenameConflictTag(); - private RenameConflictTag() - : base(TagId) - { - } + private RenameConflictTag() + : base(TagId) + { } } diff --git a/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameFieldBackgroundAndBorderTag.cs b/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameFieldBackgroundAndBorderTag.cs index 1072fd8c0f1d8..a4d8ed470a5ad 100644 --- a/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameFieldBackgroundAndBorderTag.cs +++ b/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameFieldBackgroundAndBorderTag.cs @@ -4,25 +4,24 @@ using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename.HighlightTags -{ - internal class RenameFieldBackgroundAndBorderTag : TextMarkerTag - { - // Only used for theming, does not need localized - internal const string TagId = "RoslynRenameFieldBackgroundAndBorderTag"; +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename.HighlightTags; - public static readonly RenameFieldBackgroundAndBorderTag Instance = new RenameFieldBackgroundAndBorderTag(); +internal class RenameFieldBackgroundAndBorderTag : TextMarkerTag +{ + // Only used for theming, does not need localized + internal const string TagId = "RoslynRenameFieldBackgroundAndBorderTag"; - private RenameFieldBackgroundAndBorderTag() - : base(TagId) - { - } - } + public static readonly RenameFieldBackgroundAndBorderTag Instance = new RenameFieldBackgroundAndBorderTag(); - // Only used to keep the closed repository building. This will be removed once the - // closed repository is updated to use RenameFieldBackgroundAndBorderTag. - internal class ValidTag + private RenameFieldBackgroundAndBorderTag() + : base(TagId) { - internal const string TagId = ""; } } + +// Only used to keep the closed repository building. This will be removed once the +// closed repository is updated to use RenameFieldBackgroundAndBorderTag. +internal class ValidTag +{ + internal const string TagId = ""; +} diff --git a/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameFixupTag.cs b/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameFixupTag.cs index 2a8619cb96346..8468ae8337d2a 100644 --- a/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameFixupTag.cs +++ b/src/EditorFeatures/Core/InlineRename/HighlightTags/RenameFixupTag.cs @@ -4,18 +4,17 @@ using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename.HighlightTags +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename.HighlightTags; + +internal class RenameFixupTag : TextMarkerTag { - internal class RenameFixupTag : TextMarkerTag - { - // Only used for theming, does not need localized - internal const string TagId = "RoslynRenameFixupTag"; + // Only used for theming, does not need localized + internal const string TagId = "RoslynRenameFixupTag"; - public static readonly RenameFixupTag Instance = new RenameFixupTag(); + public static readonly RenameFixupTag Instance = new RenameFixupTag(); - private RenameFixupTag() - : base(TagId) - { - } + private RenameFixupTag() + : base(TagId) + { } } diff --git a/src/EditorFeatures/Core/InlineRename/IEditorInlineRenameService.cs b/src/EditorFeatures/Core/InlineRename/IEditorInlineRenameService.cs index 903ac0a655cf1..d7f218d719e5c 100644 --- a/src/EditorFeatures/Core/InlineRename/IEditorInlineRenameService.cs +++ b/src/EditorFeatures/Core/InlineRename/IEditorInlineRenameService.cs @@ -16,244 +16,243 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor -{ - internal readonly struct InlineRenameLocation - { - public Document Document { get; } - public TextSpan TextSpan { get; } +namespace Microsoft.CodeAnalysis.Editor; - public InlineRenameLocation(Document document, TextSpan textSpan) : this() - { - this.Document = document; - this.TextSpan = textSpan; - } - } +internal readonly struct InlineRenameLocation +{ + public Document Document { get; } + public TextSpan TextSpan { get; } - internal enum InlineRenameReplacementKind + public InlineRenameLocation(Document document, TextSpan textSpan) : this() { - NoConflict, - ResolvedReferenceConflict, - ResolvedNonReferenceConflict, - UnresolvedConflict, - Complexified, + this.Document = document; + this.TextSpan = textSpan; } +} - internal enum InlineRenameFileRenameInfo - { - /// - /// This operation is not allowed - /// on the symbol being renamed - /// - NotAllowed, - - /// - /// The type being renamed has multiple definition - /// locations which is not supported. - /// - TypeWithMultipleLocations, - - /// - /// The type being renamed doesn't match the file - /// name prior to renaming - /// - TypeDoesNotMatchFileName, - - /// - /// File rename is allowed - /// - Allowed - } +internal enum InlineRenameReplacementKind +{ + NoConflict, + ResolvedReferenceConflict, + ResolvedNonReferenceConflict, + UnresolvedConflict, + Complexified, +} - internal readonly struct InlineRenameReplacement - { - public InlineRenameReplacementKind Kind { get; } - public TextSpan OriginalSpan { get; } - public TextSpan NewSpan { get; } +internal enum InlineRenameFileRenameInfo +{ + /// + /// This operation is not allowed + /// on the symbol being renamed + /// + NotAllowed, - public InlineRenameReplacement(InlineRenameReplacementKind kind, TextSpan originalSpan, TextSpan newSpan) : this() - { - this.Kind = kind; - this.OriginalSpan = originalSpan; - this.NewSpan = newSpan; - } + /// + /// The type being renamed has multiple definition + /// locations which is not supported. + /// + TypeWithMultipleLocations, - internal InlineRenameReplacement(RelatedLocation location, TextSpan newSpan) - : this(GetReplacementKind(location), location.ConflictCheckSpan, newSpan) - { - } + /// + /// The type being renamed doesn't match the file + /// name prior to renaming + /// + TypeDoesNotMatchFileName, - private static InlineRenameReplacementKind GetReplacementKind(RelatedLocation location) - { - switch (location.Type) - { - case RelatedLocationType.NoConflict: - return InlineRenameReplacementKind.NoConflict; - case RelatedLocationType.ResolvedReferenceConflict: - return InlineRenameReplacementKind.ResolvedReferenceConflict; - case RelatedLocationType.ResolvedNonReferenceConflict: - return InlineRenameReplacementKind.ResolvedNonReferenceConflict; - case RelatedLocationType.UnresolvableConflict: - case RelatedLocationType.UnresolvedConflict: - return InlineRenameReplacementKind.UnresolvedConflict; - default: - case RelatedLocationType.PossiblyResolvableConflict: - throw ExceptionUtilities.UnexpectedValue(location.Type); - } - } + /// + /// File rename is allowed + /// + Allowed +} + +internal readonly struct InlineRenameReplacement +{ + public InlineRenameReplacementKind Kind { get; } + public TextSpan OriginalSpan { get; } + public TextSpan NewSpan { get; } + + public InlineRenameReplacement(InlineRenameReplacementKind kind, TextSpan originalSpan, TextSpan newSpan) : this() + { + this.Kind = kind; + this.OriginalSpan = originalSpan; + this.NewSpan = newSpan; } - internal interface IInlineRenameReplacementInfo + internal InlineRenameReplacement(RelatedLocation location, TextSpan newSpan) + : this(GetReplacementKind(location), location.ConflictCheckSpan, newSpan) { - /// - /// The solution obtained after resolving all conflicts. - /// - Solution NewSolution { get; } - - /// - /// Whether or not the replacement text entered by the user is valid. - /// - bool ReplacementTextValid { get; } - - /// - /// The documents that need to be updated. - /// - IEnumerable DocumentIds { get; } - - /// - /// Returns all the replacements that need to be performed for the specified document. - /// - IEnumerable GetReplacements(DocumentId documentId); } - internal static class InlineRenameReplacementInfoExtensions + private static InlineRenameReplacementKind GetReplacementKind(RelatedLocation location) { - public static IEnumerable GetAllReplacementKinds(this IInlineRenameReplacementInfo info) + switch (location.Type) { - var replacements = info.DocumentIds.SelectMany(info.GetReplacements); - return replacements.Select(r => r.Kind); + case RelatedLocationType.NoConflict: + return InlineRenameReplacementKind.NoConflict; + case RelatedLocationType.ResolvedReferenceConflict: + return InlineRenameReplacementKind.ResolvedReferenceConflict; + case RelatedLocationType.ResolvedNonReferenceConflict: + return InlineRenameReplacementKind.ResolvedNonReferenceConflict; + case RelatedLocationType.UnresolvableConflict: + case RelatedLocationType.UnresolvedConflict: + return InlineRenameReplacementKind.UnresolvedConflict; + default: + case RelatedLocationType.PossiblyResolvableConflict: + throw ExceptionUtilities.UnexpectedValue(location.Type); } } +} - internal interface IInlineRenameLocationSet - { - /// - /// The set of locations that need to be updated with the replacement text that the user - /// has entered in the inline rename session. These are the locations are all relative - /// to the solution when the inline rename session began. - /// - IList Locations { get; } - - /// - /// Returns the set of replacements and their possible resolutions if the user enters the - /// provided replacement text and options. Replacements are keyed by their document id - /// and TextSpan in the original solution, and specify their new span and possible conflict - /// resolution. - /// - Task GetReplacementsAsync(string replacementText, SymbolRenameOptions options, CancellationToken cancellationToken); - } +internal interface IInlineRenameReplacementInfo +{ + /// + /// The solution obtained after resolving all conflicts. + /// + Solution NewSolution { get; } - internal interface IInlineRenameInfo - { - /// - /// Whether or not the entity at the selected location can be renamed. - /// - bool CanRename { get; } - - /// - /// Provides the reason that can be displayed to the user if the entity at the selected - /// location cannot be renamed. - /// - string LocalizedErrorMessage { get; } - - /// - /// The span of the entity that is being renamed. - /// - TextSpan TriggerSpan { get; } - - /// - /// Whether or not this entity has overloads that can also be renamed if the user wants. - /// - bool HasOverloads { get; } - - /// - /// True if overloads must be renamed (the user is not given a choice). Used if rename is invoked from within a nameof expression. - /// - bool MustRenameOverloads { get; } - - /// - /// The short name of the symbol being renamed, for use in displaying information to the user. - /// - string DisplayName { get; } - - /// - /// The full name of the symbol being renamed, for use in displaying information to the user. - /// - string FullDisplayName { get; } - - /// - /// The glyph for the symbol being renamed, for use in displaying information to the user. - /// - Glyph Glyph { get; } - - /// - /// The locations of the potential rename candidates for the symbol. - /// - ImmutableArray DefinitionLocations { get; } - - /// - /// Gets the final name of the symbol if the user has typed the provided replacement text - /// in the editor. Normally, the final name will be same as the replacement text. However, - /// that may not always be the same. For example, when renaming an attribute the replacement - /// text may be "NewName" while the final symbol name might be "NewNameAttribute". - /// - string GetFinalSymbolName(string replacementText); - - /// - /// Returns the actual span that should be edited in the buffer for a given rename reference - /// location. - /// - TextSpan GetReferenceEditSpan(InlineRenameLocation location, string triggerText, CancellationToken cancellationToken); - - /// - /// Returns the actual span that should be edited in the buffer for a given rename conflict - /// location. - /// - TextSpan? GetConflictEditSpan(InlineRenameLocation location, string triggerText, string replacementText, CancellationToken cancellationToken); - - /// - /// Determine the set of locations to rename given the provided options. May be called - /// multiple times. For example, this can be called one time for the initial set of - /// locations to rename, as well as any time the rename options are changed by the user. - /// - Task FindRenameLocationsAsync(SymbolRenameOptions options, CancellationToken cancellationToken); - - /// - /// Called before the rename is applied to the specified documents in the workspace. Return - /// if rename should proceed, or if it should be canceled. - /// - bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText); - - /// - /// Called after the rename is applied to the specified documents in the workspace. Return - /// if this operation succeeded, or if it failed. - /// - bool TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText); - - /// - /// Returns information about the file rename capabilities of - /// an inline rename - /// - InlineRenameFileRenameInfo GetFileRenameInfo(); - } + /// + /// Whether or not the replacement text entered by the user is valid. + /// + bool ReplacementTextValid { get; } -#nullable enable + /// + /// The documents that need to be updated. + /// + IEnumerable DocumentIds { get; } /// - /// Language service that allows a language to participate in the editor's inline rename feature. + /// Returns all the replacements that need to be performed for the specified document. /// - internal interface IEditorInlineRenameService : ILanguageService + IEnumerable GetReplacements(DocumentId documentId); +} + +internal static class InlineRenameReplacementInfoExtensions +{ + public static IEnumerable GetAllReplacementKinds(this IInlineRenameReplacementInfo info) { - Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken); + var replacements = info.DocumentIds.SelectMany(info.GetReplacements); + return replacements.Select(r => r.Kind); } } + +internal interface IInlineRenameLocationSet +{ + /// + /// The set of locations that need to be updated with the replacement text that the user + /// has entered in the inline rename session. These are the locations are all relative + /// to the solution when the inline rename session began. + /// + IList Locations { get; } + + /// + /// Returns the set of replacements and their possible resolutions if the user enters the + /// provided replacement text and options. Replacements are keyed by their document id + /// and TextSpan in the original solution, and specify their new span and possible conflict + /// resolution. + /// + Task GetReplacementsAsync(string replacementText, SymbolRenameOptions options, CancellationToken cancellationToken); +} + +internal interface IInlineRenameInfo +{ + /// + /// Whether or not the entity at the selected location can be renamed. + /// + bool CanRename { get; } + + /// + /// Provides the reason that can be displayed to the user if the entity at the selected + /// location cannot be renamed. + /// + string LocalizedErrorMessage { get; } + + /// + /// The span of the entity that is being renamed. + /// + TextSpan TriggerSpan { get; } + + /// + /// Whether or not this entity has overloads that can also be renamed if the user wants. + /// + bool HasOverloads { get; } + + /// + /// True if overloads must be renamed (the user is not given a choice). Used if rename is invoked from within a nameof expression. + /// + bool MustRenameOverloads { get; } + + /// + /// The short name of the symbol being renamed, for use in displaying information to the user. + /// + string DisplayName { get; } + + /// + /// The full name of the symbol being renamed, for use in displaying information to the user. + /// + string FullDisplayName { get; } + + /// + /// The glyph for the symbol being renamed, for use in displaying information to the user. + /// + Glyph Glyph { get; } + + /// + /// The locations of the potential rename candidates for the symbol. + /// + ImmutableArray DefinitionLocations { get; } + + /// + /// Gets the final name of the symbol if the user has typed the provided replacement text + /// in the editor. Normally, the final name will be same as the replacement text. However, + /// that may not always be the same. For example, when renaming an attribute the replacement + /// text may be "NewName" while the final symbol name might be "NewNameAttribute". + /// + string GetFinalSymbolName(string replacementText); + + /// + /// Returns the actual span that should be edited in the buffer for a given rename reference + /// location. + /// + TextSpan GetReferenceEditSpan(InlineRenameLocation location, string triggerText, CancellationToken cancellationToken); + + /// + /// Returns the actual span that should be edited in the buffer for a given rename conflict + /// location. + /// + TextSpan? GetConflictEditSpan(InlineRenameLocation location, string triggerText, string replacementText, CancellationToken cancellationToken); + + /// + /// Determine the set of locations to rename given the provided options. May be called + /// multiple times. For example, this can be called one time for the initial set of + /// locations to rename, as well as any time the rename options are changed by the user. + /// + Task FindRenameLocationsAsync(SymbolRenameOptions options, CancellationToken cancellationToken); + + /// + /// Called before the rename is applied to the specified documents in the workspace. Return + /// if rename should proceed, or if it should be canceled. + /// + bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText); + + /// + /// Called after the rename is applied to the specified documents in the workspace. Return + /// if this operation succeeded, or if it failed. + /// + bool TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable changedDocumentIDs, string replacementText); + + /// + /// Returns information about the file rename capabilities of + /// an inline rename + /// + InlineRenameFileRenameInfo GetFileRenameInfo(); +} + +#nullable enable + +/// +/// Language service that allows a language to participate in the editor's inline rename feature. +/// +internal interface IEditorInlineRenameService : ILanguageService +{ + Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken); +} diff --git a/src/EditorFeatures/Core/InlineRename/IInlineRenameUndoManager.cs b/src/EditorFeatures/Core/InlineRename/IInlineRenameUndoManager.cs index 1472d17799459..f609e0d1f2945 100644 --- a/src/EditorFeatures/Core/InlineRename/IInlineRenameUndoManager.cs +++ b/src/EditorFeatures/Core/InlineRename/IInlineRenameUndoManager.cs @@ -8,29 +8,28 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +/// +/// This interface contains the methods required to manipulate the undo stack +/// in each buffer during an inline rename session. VS and ETA have differing +/// implementations +/// +internal interface IInlineRenameUndoManager : IWorkspaceService { - /// - /// This interface contains the methods required to manipulate the undo stack - /// in each buffer during an inline rename session. VS and ETA have differing - /// implementations - /// - internal interface IInlineRenameUndoManager : IWorkspaceService - { - void CreateInitialState(string replacementText, ITextSelection selection, SnapshotSpan startingSpan); + void CreateInitialState(string replacementText, ITextSelection selection, SnapshotSpan startingSpan); - void CreateStartRenameUndoTransaction(Workspace workspace, ITextBuffer subjectBuffer, IInlineRenameSession inlineRenameSession); - void CreateConflictResolutionUndoTransaction(ITextBuffer subjectBuffer, Action applyEdit); + void CreateStartRenameUndoTransaction(Workspace workspace, ITextBuffer subjectBuffer, IInlineRenameSession inlineRenameSession); + void CreateConflictResolutionUndoTransaction(ITextBuffer subjectBuffer, Action applyEdit); - void UndoTemporaryEdits(ITextBuffer subjectBuffer, bool disconnect); - void OnTextChanged(ITextSelection selection, SnapshotSpan singleTrackingSpanTouched); + void UndoTemporaryEdits(ITextBuffer subjectBuffer, bool disconnect); + void OnTextChanged(ITextSelection selection, SnapshotSpan singleTrackingSpanTouched); - void UpdateSelection(ITextView textView, ITextBuffer subjectBuffer, ITrackingSpan trackingSpan); - void ApplyCurrentState(ITextBuffer subjectBuffer, object propagateSpansEditTag, IEnumerable spans); + void UpdateSelection(ITextView textView, ITextBuffer subjectBuffer, ITrackingSpan trackingSpan); + void ApplyCurrentState(ITextBuffer subjectBuffer, object propagateSpansEditTag, IEnumerable spans); - void Undo(ITextBuffer subjectBuffer); - void Redo(ITextBuffer subjectBuffer); + void Undo(ITextBuffer subjectBuffer); + void Redo(ITextBuffer subjectBuffer); - void Disconnect(); - } + void Disconnect(); } diff --git a/src/EditorFeatures/Core/InlineRename/InlineRenameService.cs b/src/EditorFeatures/Core/InlineRename/InlineRenameService.cs index 0cc12d099d5fc..7697a502b5e02 100644 --- a/src/EditorFeatures/Core/InlineRename/InlineRenameService.cs +++ b/src/EditorFeatures/Core/InlineRename/InlineRenameService.cs @@ -22,179 +22,178 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +[Export(typeof(IInlineRenameService))] +[Export(typeof(InlineRenameService))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class InlineRenameService( + IThreadingContext threadingContext, + IUIThreadOperationExecutor uiThreadOperationExecutor, + ITextBufferAssociatedViewService textBufferAssociatedViewService, + ITextBufferFactoryService textBufferFactoryService, + ITextBufferCloneService textBufferCloneService, + IFeatureServiceFactory featureServiceFactory, + IGlobalOptionService globalOptions, + [ImportMany] IEnumerable refactorNotifyServices, + IAsynchronousOperationListenerProvider listenerProvider) : IInlineRenameService { - [Export(typeof(IInlineRenameService))] - [Export(typeof(InlineRenameService))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class InlineRenameService( - IThreadingContext threadingContext, - IUIThreadOperationExecutor uiThreadOperationExecutor, - ITextBufferAssociatedViewService textBufferAssociatedViewService, - ITextBufferFactoryService textBufferFactoryService, - ITextBufferCloneService textBufferCloneService, - IFeatureServiceFactory featureServiceFactory, - IGlobalOptionService globalOptions, - [ImportMany] IEnumerable refactorNotifyServices, - IAsynchronousOperationListenerProvider listenerProvider) : IInlineRenameService + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor = uiThreadOperationExecutor; + private readonly ITextBufferAssociatedViewService _textBufferAssociatedViewService = textBufferAssociatedViewService; + private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.Rename); + private readonly IEnumerable _refactorNotifyServices = refactorNotifyServices; + private readonly ITextBufferFactoryService _textBufferFactoryService = textBufferFactoryService; + private readonly ITextBufferCloneService _textBufferCloneService = textBufferCloneService; + private readonly IFeatureServiceFactory _featureServiceFactory = featureServiceFactory; + + internal readonly IGlobalOptionService GlobalOptions = globalOptions; + + private InlineRenameSession? _activeRenameSession; + + public InlineRenameSessionInfo StartInlineSession( + Document document, + TextSpan textSpan, + CancellationToken cancellationToken) { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor = uiThreadOperationExecutor; - private readonly ITextBufferAssociatedViewService _textBufferAssociatedViewService = textBufferAssociatedViewService; - private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.Rename); - private readonly IEnumerable _refactorNotifyServices = refactorNotifyServices; - private readonly ITextBufferFactoryService _textBufferFactoryService = textBufferFactoryService; - private readonly ITextBufferCloneService _textBufferCloneService = textBufferCloneService; - private readonly IFeatureServiceFactory _featureServiceFactory = featureServiceFactory; - - internal readonly IGlobalOptionService GlobalOptions = globalOptions; - - private InlineRenameSession? _activeRenameSession; - - public InlineRenameSessionInfo StartInlineSession( - Document document, - TextSpan textSpan, - CancellationToken cancellationToken) - { - return _threadingContext.JoinableTaskFactory.Run(() => StartInlineSessionAsync(document, textSpan, cancellationToken)); - } + return _threadingContext.JoinableTaskFactory.Run(() => StartInlineSessionAsync(document, textSpan, cancellationToken)); + } - public async Task StartInlineSessionAsync( - Document document, - TextSpan textSpan, - CancellationToken cancellationToken) + public async Task StartInlineSessionAsync( + Document document, + TextSpan textSpan, + CancellationToken cancellationToken) + { + if (_activeRenameSession != null) { - if (_activeRenameSession != null) - { - throw new InvalidOperationException(EditorFeaturesResources.An_active_inline_rename_session_is_still_active_Complete_it_before_starting_a_new_one); - } + throw new InvalidOperationException(EditorFeaturesResources.An_active_inline_rename_session_is_still_active_Complete_it_before_starting_a_new_one); + } - var editorRenameService = document.GetRequiredLanguageService(); - var renameInfo = await editorRenameService.GetRenameInfoAsync(document, textSpan.Start, cancellationToken).ConfigureAwait(false); + var editorRenameService = document.GetRequiredLanguageService(); + var renameInfo = await editorRenameService.GetRenameInfoAsync(document, textSpan.Start, cancellationToken).ConfigureAwait(false); - var readOnlyOrCannotNavigateToSpanSessionInfo = await IsReadOnlyOrCannotNavigateToSpanAsync( - _threadingContext, renameInfo, document, cancellationToken).ConfigureAwait(false); - if (readOnlyOrCannotNavigateToSpanSessionInfo != null) - { - return readOnlyOrCannotNavigateToSpanSessionInfo; - } + var readOnlyOrCannotNavigateToSpanSessionInfo = await IsReadOnlyOrCannotNavigateToSpanAsync( + _threadingContext, renameInfo, document, cancellationToken).ConfigureAwait(false); + if (readOnlyOrCannotNavigateToSpanSessionInfo != null) + { + return readOnlyOrCannotNavigateToSpanSessionInfo; + } - if (!renameInfo.CanRename) - { - return new InlineRenameSessionInfo(renameInfo.LocalizedErrorMessage); - } + if (!renameInfo.CanRename) + { + return new InlineRenameSessionInfo(renameInfo.LocalizedErrorMessage); + } - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var snapshot = text.FindCorrespondingEditorTextSnapshot(); - Contract.ThrowIfNull(snapshot, "The document used for starting the inline rename session should still be open and associated with a snapshot."); - - var fileRenameInfo = renameInfo.GetFileRenameInfo(); - var canRenameFile = fileRenameInfo == InlineRenameFileRenameInfo.Allowed; - - var options = new SymbolRenameOptions( - RenameOverloads: renameInfo.MustRenameOverloads || GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameOverloads), - RenameInStrings: GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameInStrings), - RenameInComments: GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameInComments), - RenameFile: canRenameFile && GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameFile)); - - var previewChanges = GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.PreviewChanges); - - // The session currently has UI thread affinity. - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - ActiveSession = new InlineRenameSession( - _threadingContext, - this, - document.Project.Solution.Workspace, - renameInfo.TriggerSpan.ToSnapshotSpan(snapshot), - renameInfo, - options, - previewChanges, - _uiThreadOperationExecutor, - _textBufferAssociatedViewService, - _textBufferFactoryService, - _textBufferCloneService, - _featureServiceFactory, - _refactorNotifyServices, - _asyncListener); - - return new InlineRenameSessionInfo(ActiveSession); - - static async Task IsReadOnlyOrCannotNavigateToSpanAsync( - IThreadingContext threadingContext, IInlineRenameInfo renameInfo, Document document, CancellationToken cancellationToken) + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var snapshot = text.FindCorrespondingEditorTextSnapshot(); + Contract.ThrowIfNull(snapshot, "The document used for starting the inline rename session should still be open and associated with a snapshot."); + + var fileRenameInfo = renameInfo.GetFileRenameInfo(); + var canRenameFile = fileRenameInfo == InlineRenameFileRenameInfo.Allowed; + + var options = new SymbolRenameOptions( + RenameOverloads: renameInfo.MustRenameOverloads || GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameOverloads), + RenameInStrings: GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameInStrings), + RenameInComments: GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameInComments), + RenameFile: canRenameFile && GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameFile)); + + var previewChanges = GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.PreviewChanges); + + // The session currently has UI thread affinity. + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + ActiveSession = new InlineRenameSession( + _threadingContext, + this, + document.Project.Solution.Workspace, + renameInfo.TriggerSpan.ToSnapshotSpan(snapshot), + renameInfo, + options, + previewChanges, + _uiThreadOperationExecutor, + _textBufferAssociatedViewService, + _textBufferFactoryService, + _textBufferCloneService, + _featureServiceFactory, + _refactorNotifyServices, + _asyncListener); + + return new InlineRenameSessionInfo(ActiveSession); + + static async Task IsReadOnlyOrCannotNavigateToSpanAsync( + IThreadingContext threadingContext, IInlineRenameInfo renameInfo, Document document, CancellationToken cancellationToken) + { + if (renameInfo is IInlineRenameInfo inlineRenameInfo && inlineRenameInfo.DefinitionLocations != default) { - if (renameInfo is IInlineRenameInfo inlineRenameInfo && inlineRenameInfo.DefinitionLocations != default) + var workspace = document.Project.Solution.Workspace; + var navigationService = workspace.Services.GetRequiredService(); + using var _ = PooledObjects.ArrayBuilder<(ITextBuffer, SnapshotSpan)>.GetInstance(out var buffersAndSpans); + foreach (var documentSpan in inlineRenameInfo.DefinitionLocations) { - var workspace = document.Project.Solution.Workspace; - var navigationService = workspace.Services.GetRequiredService(); - using var _ = PooledObjects.ArrayBuilder<(ITextBuffer, SnapshotSpan)>.GetInstance(out var buffersAndSpans); - foreach (var documentSpan in inlineRenameInfo.DefinitionLocations) + var sourceText = await documentSpan.Document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var textSnapshot = sourceText.FindCorrespondingEditorTextSnapshot(); + + if (textSnapshot != null) { - var sourceText = await documentSpan.Document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var textSnapshot = sourceText.FindCorrespondingEditorTextSnapshot(); - - if (textSnapshot != null) - { - var buffer = textSnapshot.TextBuffer; - var originalSpan = documentSpan.SourceSpan.ToSnapshotSpan(textSnapshot).TranslateTo(buffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive); - buffersAndSpans.Add((buffer, originalSpan)); - } - - var canNavigate = await navigationService.CanNavigateToSpanAsync( - workspace, document.Id, documentSpan.SourceSpan, cancellationToken).ConfigureAwait(false); - if (!canNavigate) - { - return new InlineRenameSessionInfo(EditorFeaturesResources.You_cannot_rename_this_element_because_it_is_in_a_location_that_cannot_be_navigated_to); - } + var buffer = textSnapshot.TextBuffer; + var originalSpan = documentSpan.SourceSpan.ToSnapshotSpan(textSnapshot).TranslateTo(buffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive); + buffersAndSpans.Add((buffer, originalSpan)); } - await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - foreach (var (buffer, originalSpan) in buffersAndSpans) + var canNavigate = await navigationService.CanNavigateToSpanAsync( + workspace, document.Id, documentSpan.SourceSpan, cancellationToken).ConfigureAwait(false); + if (!canNavigate) { - if (buffer.IsReadOnly(originalSpan)) - { - return new InlineRenameSessionInfo(EditorFeaturesResources.You_cannot_rename_this_element_because_it_is_contained_in_a_read_only_file); - } + return new InlineRenameSessionInfo(EditorFeaturesResources.You_cannot_rename_this_element_because_it_is_in_a_location_that_cannot_be_navigated_to); } } - return null; + await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + foreach (var (buffer, originalSpan) in buffersAndSpans) + { + if (buffer.IsReadOnly(originalSpan)) + { + return new InlineRenameSessionInfo(EditorFeaturesResources.You_cannot_rename_this_element_because_it_is_contained_in_a_read_only_file); + } + } } + + return null; } + } - IInlineRenameSession? IInlineRenameService.ActiveSession => _activeRenameSession; + IInlineRenameSession? IInlineRenameService.ActiveSession => _activeRenameSession; - internal InlineRenameSession? ActiveSession + internal InlineRenameSession? ActiveSession + { + get { - get - { - _threadingContext.ThrowIfNotOnUIThread(); + _threadingContext.ThrowIfNotOnUIThread(); - return _activeRenameSession; - } + return _activeRenameSession; + } - set - { - _threadingContext.ThrowIfNotOnUIThread(); + set + { + _threadingContext.ThrowIfNotOnUIThread(); - // This is also checked in InlineRenameSession (which should be the only thing that ever sets this). - // However, this just adds an extra level of safety to make sure nothing bad is about to happen. - Contract.ThrowIfTrue(_activeRenameSession != null && value != null, "Cannot assign an active rename session when one is already in progress."); + // This is also checked in InlineRenameSession (which should be the only thing that ever sets this). + // However, this just adds an extra level of safety to make sure nothing bad is about to happen. + Contract.ThrowIfTrue(_activeRenameSession != null && value != null, "Cannot assign an active rename session when one is already in progress."); - var previousSession = _activeRenameSession; - _activeRenameSession = value; - ActiveSessionChanged?.Invoke(this, new ActiveSessionChangedEventArgs(previousSession!)); - } + var previousSession = _activeRenameSession; + _activeRenameSession = value; + ActiveSessionChanged?.Invoke(this, new ActiveSessionChangedEventArgs(previousSession!)); } + } - /// - /// Raised when the ActiveSession property has changed. - /// - internal event EventHandler? ActiveSessionChanged; + /// + /// Raised when the ActiveSession property has changed. + /// + internal event EventHandler? ActiveSessionChanged; - internal class ActiveSessionChangedEventArgs(InlineRenameSession previousSession) : EventArgs - { - public InlineRenameSession PreviousSession { get; } = previousSession; - } + internal class ActiveSessionChangedEventArgs(InlineRenameSession previousSession) : EventArgs + { + public InlineRenameSession PreviousSession { get; } = previousSession; } } diff --git a/src/EditorFeatures/Core/InlineRename/InlineRenameSession.OpenTextBufferManager.cs b/src/EditorFeatures/Core/InlineRename/InlineRenameSession.OpenTextBufferManager.cs index 8452ddae5396d..f75305bfba53d 100644 --- a/src/EditorFeatures/Core/InlineRename/InlineRenameSession.OpenTextBufferManager.cs +++ b/src/EditorFeatures/Core/InlineRename/InlineRenameSession.OpenTextBufferManager.cs @@ -22,357 +22,393 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal partial class InlineRenameSession { - internal partial class InlineRenameSession + /// + /// Manages state for open text buffers. + /// + internal class OpenTextBufferManager { + private static readonly object s_propagateSpansEditTag = new(); + private static readonly object s_calculateMergedSpansEditTag = new(); + + private readonly DynamicReadOnlyRegionQuery _isBufferReadOnly; + private readonly InlineRenameSession _session; + private readonly ITextBuffer _subjectBuffer; + private readonly IEnumerable _baseDocuments; + private readonly ITextBufferFactoryService _textBufferFactoryService; + private readonly ITextBufferCloneService _textBufferCloneService; + /// - /// Manages state for open text buffers. + /// The list of active tracking spans that are updated with the session's replacement text. + /// These are also the only spans the user can edit during an inline rename session. /// - internal class OpenTextBufferManager + private readonly Dictionary _referenceSpanToLinkedRenameSpanMap = []; + + private readonly List _conflictResolutionRenameTrackingSpans = []; + private readonly IList _readOnlyRegions = []; + + private readonly IList _textViews = []; + + private TextSpan? _activeSpan; + + public OpenTextBufferManager( + InlineRenameSession session, + Workspace workspace, + ITextBufferFactoryService textBufferFactoryService, + ITextBufferCloneService textBufferCloneService, + ITextBuffer subjectBuffer) { - private static readonly object s_propagateSpansEditTag = new(); - private static readonly object s_calculateMergedSpansEditTag = new(); - - private readonly DynamicReadOnlyRegionQuery _isBufferReadOnly; - private readonly InlineRenameSession _session; - private readonly ITextBuffer _subjectBuffer; - private readonly IEnumerable _baseDocuments; - private readonly ITextBufferFactoryService _textBufferFactoryService; - private readonly ITextBufferCloneService _textBufferCloneService; - - /// - /// The list of active tracking spans that are updated with the session's replacement text. - /// These are also the only spans the user can edit during an inline rename session. - /// - private readonly Dictionary _referenceSpanToLinkedRenameSpanMap = []; - - private readonly List _conflictResolutionRenameTrackingSpans = []; - private readonly IList _readOnlyRegions = []; - - private readonly IList _textViews = []; - - private TextSpan? _activeSpan; - - public OpenTextBufferManager( - InlineRenameSession session, - Workspace workspace, - ITextBufferFactoryService textBufferFactoryService, - ITextBufferCloneService textBufferCloneService, - ITextBuffer subjectBuffer) + _session = session; + _subjectBuffer = subjectBuffer; + _baseDocuments = subjectBuffer.GetRelatedDocuments(); + _textBufferFactoryService = textBufferFactoryService; + _textBufferCloneService = textBufferCloneService; + _subjectBuffer.ChangedLowPriority += OnTextBufferChanged; + + foreach (var view in session._textBufferAssociatedViewService.GetAssociatedTextViews(_subjectBuffer)) { - _session = session; - _subjectBuffer = subjectBuffer; - _baseDocuments = subjectBuffer.GetRelatedDocuments(); - _textBufferFactoryService = textBufferFactoryService; - _textBufferCloneService = textBufferCloneService; - _subjectBuffer.ChangedLowPriority += OnTextBufferChanged; - - foreach (var view in session._textBufferAssociatedViewService.GetAssociatedTextViews(_subjectBuffer)) - { - ConnectToView(view); - } + ConnectToView(view); + } - session.UndoManager.CreateStartRenameUndoTransaction(workspace, subjectBuffer, session); + session.UndoManager.CreateStartRenameUndoTransaction(workspace, subjectBuffer, session); - _isBufferReadOnly = new DynamicReadOnlyRegionQuery(isEdit => !_session._isApplyingEdit); - UpdateReadOnlyRegions(); - } + _isBufferReadOnly = new DynamicReadOnlyRegionQuery(isEdit => !_session._isApplyingEdit); + UpdateReadOnlyRegions(); + } - public ITextView ActiveTextView + public ITextView ActiveTextView + { + get { - get + foreach (var view in _textViews) { - foreach (var view in _textViews) + if (view.HasAggregateFocus) { - if (view.HasAggregateFocus) - { - return view; - } + return view; } - - return _textViews.FirstOrDefault(); } + + return _textViews.FirstOrDefault(); } + } - private void UpdateReadOnlyRegions(bool removeOnly = false) + private void UpdateReadOnlyRegions(bool removeOnly = false) + { + _session._threadingContext.ThrowIfNotOnUIThread(); + if (!removeOnly && _session.ReplacementText == string.Empty) { - _session._threadingContext.ThrowIfNotOnUIThread(); - if (!removeOnly && _session.ReplacementText == string.Empty) - { - return; - } + return; + } - using var readOnlyEdit = _subjectBuffer.CreateReadOnlyRegionEdit(); + using var readOnlyEdit = _subjectBuffer.CreateReadOnlyRegionEdit(); - foreach (var oldReadOnlyRegion in _readOnlyRegions) + foreach (var oldReadOnlyRegion in _readOnlyRegions) + { + readOnlyEdit.RemoveReadOnlyRegion(oldReadOnlyRegion); + } + + _readOnlyRegions.Clear(); + + if (!removeOnly) + { + // We will compute the new read only regions to be all spans that are not currently in an editable span + var editableSpans = GetEditableSpansForSnapshot(_subjectBuffer.CurrentSnapshot); + var entireBufferSpan = _subjectBuffer.CurrentSnapshot.GetSnapshotSpanCollection(); + var newReadOnlySpans = NormalizedSnapshotSpanCollection.Difference(entireBufferSpan, new NormalizedSnapshotSpanCollection(editableSpans)); + + foreach (var newReadOnlySpan in newReadOnlySpans) { - readOnlyEdit.RemoveReadOnlyRegion(oldReadOnlyRegion); + _readOnlyRegions.Add(readOnlyEdit.CreateDynamicReadOnlyRegion(newReadOnlySpan, SpanTrackingMode.EdgeExclusive, EdgeInsertionMode.Allow, _isBufferReadOnly)); } - _readOnlyRegions.Clear(); - - if (!removeOnly) + // The spans we added allow typing at the start and end. We'll add extra + // zero-width read-only regions at the start and end of the file to fix this, + // but only if we don't have an identifier at the start or end that _would_ let + // them type there. + if (editableSpans.All(s => s.Start > 0)) { - // We will compute the new read only regions to be all spans that are not currently in an editable span - var editableSpans = GetEditableSpansForSnapshot(_subjectBuffer.CurrentSnapshot); - var entireBufferSpan = _subjectBuffer.CurrentSnapshot.GetSnapshotSpanCollection(); - var newReadOnlySpans = NormalizedSnapshotSpanCollection.Difference(entireBufferSpan, new NormalizedSnapshotSpanCollection(editableSpans)); + _readOnlyRegions.Add(readOnlyEdit.CreateDynamicReadOnlyRegion(new Span(0, 0), SpanTrackingMode.EdgeExclusive, EdgeInsertionMode.Deny, _isBufferReadOnly)); + } - foreach (var newReadOnlySpan in newReadOnlySpans) - { - _readOnlyRegions.Add(readOnlyEdit.CreateDynamicReadOnlyRegion(newReadOnlySpan, SpanTrackingMode.EdgeExclusive, EdgeInsertionMode.Allow, _isBufferReadOnly)); - } + if (editableSpans.All(s => s.End < _subjectBuffer.CurrentSnapshot.Length)) + { + _readOnlyRegions.Add(readOnlyEdit.CreateDynamicReadOnlyRegion(new Span(_subjectBuffer.CurrentSnapshot.Length, 0), SpanTrackingMode.EdgeExclusive, EdgeInsertionMode.Deny, _isBufferReadOnly)); + } + } - // The spans we added allow typing at the start and end. We'll add extra - // zero-width read-only regions at the start and end of the file to fix this, - // but only if we don't have an identifier at the start or end that _would_ let - // them type there. - if (editableSpans.All(s => s.Start > 0)) - { - _readOnlyRegions.Add(readOnlyEdit.CreateDynamicReadOnlyRegion(new Span(0, 0), SpanTrackingMode.EdgeExclusive, EdgeInsertionMode.Deny, _isBufferReadOnly)); - } + readOnlyEdit.Apply(); + } - if (editableSpans.All(s => s.End < _subjectBuffer.CurrentSnapshot.Length)) - { - _readOnlyRegions.Add(readOnlyEdit.CreateDynamicReadOnlyRegion(new Span(_subjectBuffer.CurrentSnapshot.Length, 0), SpanTrackingMode.EdgeExclusive, EdgeInsertionMode.Deny, _isBufferReadOnly)); - } - } + private void OnTextViewClosed(object sender, EventArgs e) + { + var view = sender as ITextView; + view.Closed -= OnTextViewClosed; + _textViews.Remove(view); + _session.Cancel(); + } - readOnlyEdit.Apply(); - } + internal void ConnectToView(ITextView textView) + { + textView.Closed += OnTextViewClosed; + _textViews.Add(textView); + } - private void OnTextViewClosed(object sender, EventArgs e) - { - var view = sender as ITextView; - view.Closed -= OnTextViewClosed; - _textViews.Remove(view); - _session.Cancel(); - } + public event Action SpansChanged; - internal void ConnectToView(ITextView textView) - { - textView.Closed += OnTextViewClosed; - _textViews.Add(textView); - } + private void RaiseSpansChanged() + => this.SpansChanged?.Invoke(); - public event Action SpansChanged; + internal IEnumerable GetRenameTrackingSpans() + => _referenceSpanToLinkedRenameSpanMap.Values.Where(r => r.Type != RenameSpanKind.None).Concat(_conflictResolutionRenameTrackingSpans); - private void RaiseSpansChanged() - => this.SpansChanged?.Invoke(); + internal IEnumerable GetEditableSpansForSnapshot(ITextSnapshot snapshot) + => _referenceSpanToLinkedRenameSpanMap.Values.Where(r => r.Type != RenameSpanKind.None).Select(r => r.TrackingSpan.GetSpan(snapshot)); - internal IEnumerable GetRenameTrackingSpans() - => _referenceSpanToLinkedRenameSpanMap.Values.Where(r => r.Type != RenameSpanKind.None).Concat(_conflictResolutionRenameTrackingSpans); + internal void SetReferenceSpans(IEnumerable spans) + { + _session._threadingContext.ThrowIfNotOnUIThread(); - internal IEnumerable GetEditableSpansForSnapshot(ITextSnapshot snapshot) - => _referenceSpanToLinkedRenameSpanMap.Values.Where(r => r.Type != RenameSpanKind.None).Select(r => r.TrackingSpan.GetSpan(snapshot)); + if (spans.SetEquals(_referenceSpanToLinkedRenameSpanMap.Keys)) + { + return; + } - internal void SetReferenceSpans(IEnumerable spans) + using (new SelectionTracking(this)) { - _session._threadingContext.ThrowIfNotOnUIThread(); + // Revert any previous edits in case we're removing spans. Undo conflict resolution as well to avoid + // handling the various edge cases where a tracking span might not map to the right span in the current snapshot + _session.UndoManager.UndoTemporaryEdits(_subjectBuffer, disconnect: false); - if (spans.SetEquals(_referenceSpanToLinkedRenameSpanMap.Keys)) + _referenceSpanToLinkedRenameSpanMap.Clear(); + foreach (var span in spans) { - return; + var document = _baseDocuments.First(); + var renameableSpan = _session._renameInfo.GetReferenceEditSpan( + new InlineRenameLocation(document, span), GetTriggerText(document, span), CancellationToken.None); + var trackingSpan = new RenameTrackingSpan( + _subjectBuffer.CurrentSnapshot.CreateTrackingSpan(renameableSpan.ToSpan(), SpanTrackingMode.EdgeInclusive, TrackingFidelityMode.Forward), + RenameSpanKind.Reference); + + _referenceSpanToLinkedRenameSpanMap[span] = trackingSpan; } - using (new SelectionTracking(this)) - { - // Revert any previous edits in case we're removing spans. Undo conflict resolution as well to avoid - // handling the various edge cases where a tracking span might not map to the right span in the current snapshot - _session.UndoManager.UndoTemporaryEdits(_subjectBuffer, disconnect: false); + _activeSpan = _activeSpan.HasValue && spans.Contains(_activeSpan.Value) + ? _activeSpan + : spans.Where(s => + // in tests `ActiveTextview` can be null so don't depend on it + ActiveTextView == null || + ActiveTextView.GetSpanInView(_subjectBuffer.CurrentSnapshot.GetSpan(s.ToSpan())).Count != 0) // spans were successfully projected + .FirstOrNull(); // filter to spans that have a projection - _referenceSpanToLinkedRenameSpanMap.Clear(); - foreach (var span in spans) - { - var document = _baseDocuments.First(); - var renameableSpan = _session._renameInfo.GetReferenceEditSpan( - new InlineRenameLocation(document, span), GetTriggerText(document, span), CancellationToken.None); - var trackingSpan = new RenameTrackingSpan( - _subjectBuffer.CurrentSnapshot.CreateTrackingSpan(renameableSpan.ToSpan(), SpanTrackingMode.EdgeInclusive, TrackingFidelityMode.Forward), - RenameSpanKind.Reference); - - _referenceSpanToLinkedRenameSpanMap[span] = trackingSpan; - } + UpdateReadOnlyRegions(); + this.ApplyReplacementText(updateSelection: false); + } - _activeSpan = _activeSpan.HasValue && spans.Contains(_activeSpan.Value) - ? _activeSpan - : spans.Where(s => - // in tests `ActiveTextview` can be null so don't depend on it - ActiveTextView == null || - ActiveTextView.GetSpanInView(_subjectBuffer.CurrentSnapshot.GetSpan(s.ToSpan())).Count != 0) // spans were successfully projected - .FirstOrNull(); // filter to spans that have a projection + RaiseSpansChanged(); + } - UpdateReadOnlyRegions(); - this.ApplyReplacementText(updateSelection: false); - } + private static string GetTriggerText(Document document, TextSpan span) + { + var sourceText = document.GetTextSynchronously(CancellationToken.None); + return sourceText.ToString(span); + } - RaiseSpansChanged(); - } + private void OnTextBufferChanged(object sender, TextContentChangedEventArgs args) + { + _session._threadingContext.ThrowIfNotOnUIThread(); - private static string GetTriggerText(Document document, TextSpan span) + // This might be an event fired due to our own edit + if (args.EditTag == s_propagateSpansEditTag || _session._isApplyingEdit) { - var sourceText = document.GetTextSynchronously(CancellationToken.None); - return sourceText.ToString(span); + return; } - private void OnTextBufferChanged(object sender, TextContentChangedEventArgs args) + using (Logger.LogBlock(FunctionId.Rename_OnTextBufferChanged, CancellationToken.None)) { - _session._threadingContext.ThrowIfNotOnUIThread(); + var trackingSpansAfterEdit = new NormalizedSpanCollection(GetEditableSpansForSnapshot(args.After).Select(ss => (Span)ss)); + var spansTouchedInEdit = new NormalizedSpanCollection(args.Changes.Select(c => c.NewSpan)); - // This might be an event fired due to our own edit - if (args.EditTag == s_propagateSpansEditTag || _session._isApplyingEdit) + var intersectionSpans = NormalizedSpanCollection.Intersection(trackingSpansAfterEdit, spansTouchedInEdit); + if (intersectionSpans.Count == 0) { + // In Razor we sometimes get formatting changes during inline rename that + // do not intersect with any of our spans. Ideally this shouldn't happen at + // all, but if it does happen we can just ignore it. return; } - using (Logger.LogBlock(FunctionId.Rename_OnTextBufferChanged, CancellationToken.None)) - { - var trackingSpansAfterEdit = new NormalizedSpanCollection(GetEditableSpansForSnapshot(args.After).Select(ss => (Span)ss)); - var spansTouchedInEdit = new NormalizedSpanCollection(args.Changes.Select(c => c.NewSpan)); + // Cases with invalid identifiers may cause there to be multiple intersection + // spans, but they should still all map to a single tracked rename span (e.g. + // renaming "two" to "one two three" may be interpreted as two distinct + // additions of "one" and "three"). + var boundingIntersectionSpan = Span.FromBounds(intersectionSpans.First().Start, intersectionSpans.Last().End); + var trackingSpansTouched = GetEditableSpansForSnapshot(args.After).Where(ss => ss.IntersectsWith(boundingIntersectionSpan)); + Debug.Assert(trackingSpansTouched.Count() == 1); + + var singleTrackingSpanTouched = trackingSpansTouched.Single(); + _activeSpan = _referenceSpanToLinkedRenameSpanMap.Where(kvp => kvp.Value.TrackingSpan.GetSpan(args.After).Contains(boundingIntersectionSpan)).Single().Key; + _session.UndoManager.OnTextChanged(this.ActiveTextView.Selection, singleTrackingSpanTouched); + } + } - var intersectionSpans = NormalizedSpanCollection.Intersection(trackingSpansAfterEdit, spansTouchedInEdit); - if (intersectionSpans.Count == 0) - { - // In Razor we sometimes get formatting changes during inline rename that - // do not intersect with any of our spans. Ideally this shouldn't happen at - // all, but if it does happen we can just ignore it. - return; - } + /// + /// This is a work around for a bug in Razor where the projection spans can get out-of-sync with the + /// identifiers. When that bug is fixed this helper can be deleted. + /// + private bool AreAllReferenceSpansMappable() + { + // in tests `ActiveTextview` could be null so don't depend on it + return ActiveTextView == null || + _referenceSpanToLinkedRenameSpanMap.Values + .Select(renameTrackingSpan => renameTrackingSpan.TrackingSpan.GetSpan(_subjectBuffer.CurrentSnapshot)) + .All(s => + s.End <= _subjectBuffer.CurrentSnapshot.Length && // span is valid for the snapshot + ActiveTextView.GetSpanInView(_subjectBuffer.CurrentSnapshot.GetSpan(s)).Count != 0); // spans were successfully projected + } - // Cases with invalid identifiers may cause there to be multiple intersection - // spans, but they should still all map to a single tracked rename span (e.g. - // renaming "two" to "one two three" may be interpreted as two distinct - // additions of "one" and "three"). - var boundingIntersectionSpan = Span.FromBounds(intersectionSpans.First().Start, intersectionSpans.Last().End); - var trackingSpansTouched = GetEditableSpansForSnapshot(args.After).Where(ss => ss.IntersectsWith(boundingIntersectionSpan)); - Debug.Assert(trackingSpansTouched.Count() == 1); - - var singleTrackingSpanTouched = trackingSpansTouched.Single(); - _activeSpan = _referenceSpanToLinkedRenameSpanMap.Where(kvp => kvp.Value.TrackingSpan.GetSpan(args.After).Contains(boundingIntersectionSpan)).Single().Key; - _session.UndoManager.OnTextChanged(this.ActiveTextView.Selection, singleTrackingSpanTouched); - } - } + internal void ApplyReplacementText(bool updateSelection = true) + { + _session._threadingContext.ThrowIfNotOnUIThread(); - /// - /// This is a work around for a bug in Razor where the projection spans can get out-of-sync with the - /// identifiers. When that bug is fixed this helper can be deleted. - /// - private bool AreAllReferenceSpansMappable() + if (!AreAllReferenceSpansMappable()) { - // in tests `ActiveTextview` could be null so don't depend on it - return ActiveTextView == null || - _referenceSpanToLinkedRenameSpanMap.Values - .Select(renameTrackingSpan => renameTrackingSpan.TrackingSpan.GetSpan(_subjectBuffer.CurrentSnapshot)) - .All(s => - s.End <= _subjectBuffer.CurrentSnapshot.Length && // span is valid for the snapshot - ActiveTextView.GetSpanInView(_subjectBuffer.CurrentSnapshot.GetSpan(s)).Count != 0); // spans were successfully projected + // don't dynamically update the reference spans for documents with unmappable projections + return; } - internal void ApplyReplacementText(bool updateSelection = true) - { - _session._threadingContext.ThrowIfNotOnUIThread(); + _session.UndoManager.ApplyCurrentState( + _subjectBuffer, + s_propagateSpansEditTag, + _referenceSpanToLinkedRenameSpanMap.Values.Where(r => r.Type != RenameSpanKind.None).Select(r => r.TrackingSpan)); - if (!AreAllReferenceSpansMappable()) - { - // don't dynamically update the reference spans for documents with unmappable projections - return; - } + if (updateSelection && _activeSpan.HasValue && this.ActiveTextView != null) + { + var snapshot = _subjectBuffer.CurrentSnapshot; + _session.UndoManager.UpdateSelection(this.ActiveTextView, _subjectBuffer, _referenceSpanToLinkedRenameSpanMap[_activeSpan.Value].TrackingSpan); + } + } - _session.UndoManager.ApplyCurrentState( - _subjectBuffer, - s_propagateSpansEditTag, - _referenceSpanToLinkedRenameSpanMap.Values.Where(r => r.Type != RenameSpanKind.None).Select(r => r.TrackingSpan)); + internal void DisconnectAndRollbackEdits(bool documentIsClosed) + { + _session._threadingContext.ThrowIfNotOnUIThread(); - if (updateSelection && _activeSpan.HasValue && this.ActiveTextView != null) - { - var snapshot = _subjectBuffer.CurrentSnapshot; - _session.UndoManager.UpdateSelection(this.ActiveTextView, _subjectBuffer, _referenceSpanToLinkedRenameSpanMap[_activeSpan.Value].TrackingSpan); - } - } + // Detach from the buffer; it is important that this is done before we start + // undoing transactions, since the undo actions will cause buffer changes. + _subjectBuffer.ChangedLowPriority -= OnTextBufferChanged; - internal void DisconnectAndRollbackEdits(bool documentIsClosed) + foreach (var view in _textViews) { - _session._threadingContext.ThrowIfNotOnUIThread(); + view.Closed -= OnTextViewClosed; + } - // Detach from the buffer; it is important that this is done before we start - // undoing transactions, since the undo actions will cause buffer changes. - _subjectBuffer.ChangedLowPriority -= OnTextBufferChanged; + // Remove any old read only regions we had + UpdateReadOnlyRegions(removeOnly: true); - foreach (var view in _textViews) - { - view.Closed -= OnTextViewClosed; - } + if (!documentIsClosed) + { + _session.UndoManager.UndoTemporaryEdits(_subjectBuffer, disconnect: true); + } + } - // Remove any old read only regions we had - UpdateReadOnlyRegions(removeOnly: true); + internal void ApplyConflictResolutionEdits(IInlineRenameReplacementInfo conflictResolution, LinkedFileMergeSessionResult mergeResult, IEnumerable documents, CancellationToken cancellationToken) + { + _session._threadingContext.ThrowIfNotOnUIThread(); - if (!documentIsClosed) - { - _session.UndoManager.UndoTemporaryEdits(_subjectBuffer, disconnect: true); - } + if (!AreAllReferenceSpansMappable()) + { + // don't dynamically update the reference spans for documents with unmappable projections + return; } - internal void ApplyConflictResolutionEdits(IInlineRenameReplacementInfo conflictResolution, LinkedFileMergeSessionResult mergeResult, IEnumerable documents, CancellationToken cancellationToken) + using (new SelectionTracking(this)) { - _session._threadingContext.ThrowIfNotOnUIThread(); + // 1. Undo any previous edits and update the buffer to resulting document after conflict resolution + _session.UndoManager.UndoTemporaryEdits(_subjectBuffer, disconnect: false); - if (!AreAllReferenceSpansMappable()) - { - // don't dynamically update the reference spans for documents with unmappable projections - return; - } + var newDocument = mergeResult.MergedSolution.GetDocument(documents.First().Id); + var originalDocument = _baseDocuments.Single(d => d.Id == newDocument.Id); + + var changes = GetTextChangesFromTextDifferencingServiceAsync(originalDocument, newDocument, cancellationToken).WaitAndGetResult(cancellationToken); + + // TODO: why does the following line stop responding when uncommented? + // newDocument.GetTextChangesAsync(this.baseDocuments.Single(d => d.Id == newDocument.Id), cancellationToken).WaitAndGetResult(cancellationToken).Reverse(); - using (new SelectionTracking(this)) + _session.UndoManager.CreateConflictResolutionUndoTransaction(_subjectBuffer, () => { - // 1. Undo any previous edits and update the buffer to resulting document after conflict resolution - _session.UndoManager.UndoTemporaryEdits(_subjectBuffer, disconnect: false); + using var edit = _subjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, null, s_propagateSpansEditTag); - var newDocument = mergeResult.MergedSolution.GetDocument(documents.First().Id); - var originalDocument = _baseDocuments.Single(d => d.Id == newDocument.Id); + foreach (var change in changes) + { + edit.Replace(change.Span.Start, change.Span.Length, change.NewText); + } - var changes = GetTextChangesFromTextDifferencingServiceAsync(originalDocument, newDocument, cancellationToken).WaitAndGetResult(cancellationToken); + edit.ApplyAndLogExceptions(); + }); - // TODO: why does the following line stop responding when uncommented? - // newDocument.GetTextChangesAsync(this.baseDocuments.Single(d => d.Id == newDocument.Id), cancellationToken).WaitAndGetResult(cancellationToken).Reverse(); + // 2. We want to update referenceSpanToLinkedRenameSpanMap where spans were affected by conflict resolution. + // We also need to add the remaining document edits to conflictResolutionRenameTrackingSpans + // so they get classified/tagged correctly in the editor. + _conflictResolutionRenameTrackingSpans.Clear(); - _session.UndoManager.CreateConflictResolutionUndoTransaction(_subjectBuffer, () => - { - using var edit = _subjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, null, s_propagateSpansEditTag); + var documentReplacements = documents + .Select(document => (document, conflictResolution.GetReplacements(document.Id).Where(r => GetRenameSpanKind(r.Kind) != RenameSpanKind.None).ToImmutableArray())) + .ToImmutableArray(); - foreach (var change in changes) + var firstDocumentReplacements = documentReplacements.FirstOrDefault(d => !d.Item2.IsEmpty); + var bufferContainsLinkedDocuments = documentReplacements.Length > 1 && firstDocumentReplacements.document != null; + var linkedDocumentsMightConflict = bufferContainsLinkedDocuments; + if (linkedDocumentsMightConflict) + { + // When changes are made and linked documents are involved, some of the linked documents may + // have changes that differ from others. When these changes conflict (both differ and overlap), + // the inline rename UI reveals the conflicts. However, the merge process for finding these + // conflicts is slow, so we want to avoid it when possible. This code block attempts to set + // linkedDocumentsMightConflict back to false, eliminating the need to merge the changes as part + // of the conflict detection process. Currently we only special case one scenario: ignoring + // documents that have no changes at all, we check if all linked documents have exactly the same + // set of changes. + + // 1. Check if all documents have the same replacement spans (or no replacements) + var spansMatch = true; + foreach (var (document, replacements) in documentReplacements) + { + if (document == firstDocumentReplacements.document || replacements.IsEmpty) { - edit.Replace(change.Span.Start, change.Span.Length, change.NewText); + continue; } - edit.ApplyAndLogExceptions(); - }); + if (replacements.Length != firstDocumentReplacements.Item2.Length) + { + spansMatch = false; + break; + } - // 2. We want to update referenceSpanToLinkedRenameSpanMap where spans were affected by conflict resolution. - // We also need to add the remaining document edits to conflictResolutionRenameTrackingSpans - // so they get classified/tagged correctly in the editor. - _conflictResolutionRenameTrackingSpans.Clear(); + for (var i = 0; i < replacements.Length; i++) + { + if (!replacements[i].Equals(firstDocumentReplacements.Item2[i])) + { + spansMatch = false; + break; + } + } - var documentReplacements = documents - .Select(document => (document, conflictResolution.GetReplacements(document.Id).Where(r => GetRenameSpanKind(r.Kind) != RenameSpanKind.None).ToImmutableArray())) - .ToImmutableArray(); + if (!spansMatch) + { + break; + } + } - var firstDocumentReplacements = documentReplacements.FirstOrDefault(d => !d.Item2.IsEmpty); - var bufferContainsLinkedDocuments = documentReplacements.Length > 1 && firstDocumentReplacements.document != null; - var linkedDocumentsMightConflict = bufferContainsLinkedDocuments; - if (linkedDocumentsMightConflict) + // 2. If spans match, check content + if (spansMatch) { - // When changes are made and linked documents are involved, some of the linked documents may - // have changes that differ from others. When these changes conflict (both differ and overlap), - // the inline rename UI reveals the conflicts. However, the merge process for finding these - // conflicts is slow, so we want to avoid it when possible. This code block attempts to set - // linkedDocumentsMightConflict back to false, eliminating the need to merge the changes as part - // of the conflict detection process. Currently we only special case one scenario: ignoring - // documents that have no changes at all, we check if all linked documents have exactly the same - // set of changes. - - // 1. Check if all documents have the same replacement spans (or no replacements) - var spansMatch = true; + linkedDocumentsMightConflict = false; + + // Only need to check the new span's content + var firstDocumentNewText = conflictResolution.NewSolution.GetDocument(firstDocumentReplacements.document.Id).GetTextSynchronously(cancellationToken); + var firstDocumentNewSpanText = firstDocumentReplacements.Item2.SelectAsArray(replacement => firstDocumentNewText.ToString(replacement.NewSpan)); foreach (var (document, replacements) in documentReplacements) { if (document == firstDocumentReplacements.document || replacements.IsEmpty) @@ -380,368 +416,331 @@ internal void ApplyConflictResolutionEdits(IInlineRenameReplacementInfo conflict continue; } - if (replacements.Length != firstDocumentReplacements.Item2.Length) - { - spansMatch = false; - break; - } - + var documentNewText = conflictResolution.NewSolution.GetDocument(document.Id).GetTextSynchronously(cancellationToken); for (var i = 0; i < replacements.Length; i++) { - if (!replacements[i].Equals(firstDocumentReplacements.Item2[i])) + if (documentNewText.ToString(replacements[i].NewSpan) != firstDocumentNewSpanText[i]) { - spansMatch = false; + // Have to use the slower merge process + linkedDocumentsMightConflict = true; break; } } - if (!spansMatch) + if (linkedDocumentsMightConflict) { break; } } - - // 2. If spans match, check content - if (spansMatch) - { - linkedDocumentsMightConflict = false; - - // Only need to check the new span's content - var firstDocumentNewText = conflictResolution.NewSolution.GetDocument(firstDocumentReplacements.document.Id).GetTextSynchronously(cancellationToken); - var firstDocumentNewSpanText = firstDocumentReplacements.Item2.SelectAsArray(replacement => firstDocumentNewText.ToString(replacement.NewSpan)); - foreach (var (document, replacements) in documentReplacements) - { - if (document == firstDocumentReplacements.document || replacements.IsEmpty) - { - continue; - } - - var documentNewText = conflictResolution.NewSolution.GetDocument(document.Id).GetTextSynchronously(cancellationToken); - for (var i = 0; i < replacements.Length; i++) - { - if (documentNewText.ToString(replacements[i].NewSpan) != firstDocumentNewSpanText[i]) - { - // Have to use the slower merge process - linkedDocumentsMightConflict = true; - break; - } - } - - if (linkedDocumentsMightConflict) - { - break; - } - } - } } + } - foreach (var document in documents) + foreach (var document in documents) + { + var relevantReplacements = conflictResolution.GetReplacements(document.Id).Where(r => GetRenameSpanKind(r.Kind) != RenameSpanKind.None); + if (!relevantReplacements.Any()) { - var relevantReplacements = conflictResolution.GetReplacements(document.Id).Where(r => GetRenameSpanKind(r.Kind) != RenameSpanKind.None); - if (!relevantReplacements.Any()) - { - continue; - } + continue; + } - var mergedReplacements = linkedDocumentsMightConflict - ? GetMergedReplacementInfos( - relevantReplacements, - conflictResolution.NewSolution.GetDocument(document.Id), - mergeResult.MergedSolution.GetDocument(document.Id), - cancellationToken) - : relevantReplacements; + var mergedReplacements = linkedDocumentsMightConflict + ? GetMergedReplacementInfos( + relevantReplacements, + conflictResolution.NewSolution.GetDocument(document.Id), + mergeResult.MergedSolution.GetDocument(document.Id), + cancellationToken) + : relevantReplacements; - // Show merge conflicts comments as unresolvable conflicts, and do not - // show any other rename-related spans that overlap a merge conflict comment. - mergeResult.MergeConflictCommentSpans.TryGetValue(document.Id, out var mergeConflictComments); - mergeConflictComments ??= SpecializedCollections.EmptyEnumerable(); + // Show merge conflicts comments as unresolvable conflicts, and do not + // show any other rename-related spans that overlap a merge conflict comment. + mergeResult.MergeConflictCommentSpans.TryGetValue(document.Id, out var mergeConflictComments); + mergeConflictComments ??= SpecializedCollections.EmptyEnumerable(); - foreach (var conflict in mergeConflictComments) - { - // TODO: Add these to the unresolvable conflict counts in the dashboard + foreach (var conflict in mergeConflictComments) + { + // TODO: Add these to the unresolvable conflict counts in the dashboard - _conflictResolutionRenameTrackingSpans.Add(new RenameTrackingSpan( - _subjectBuffer.CurrentSnapshot.CreateTrackingSpan(conflict.ToSpan(), SpanTrackingMode.EdgeInclusive, TrackingFidelityMode.Forward), - RenameSpanKind.UnresolvedConflict)); - } + _conflictResolutionRenameTrackingSpans.Add(new RenameTrackingSpan( + _subjectBuffer.CurrentSnapshot.CreateTrackingSpan(conflict.ToSpan(), SpanTrackingMode.EdgeInclusive, TrackingFidelityMode.Forward), + RenameSpanKind.UnresolvedConflict)); + } - foreach (var replacement in mergedReplacements) + foreach (var replacement in mergedReplacements) + { + var kind = GetRenameSpanKind(replacement.Kind); + + if (_referenceSpanToLinkedRenameSpanMap.ContainsKey(replacement.OriginalSpan) && kind != RenameSpanKind.Complexified) { - var kind = GetRenameSpanKind(replacement.Kind); + var linkedRenameSpan = _session._renameInfo.GetConflictEditSpan( + new InlineRenameLocation(newDocument, replacement.NewSpan), GetTriggerText(newDocument, replacement.NewSpan), + GetWithoutAttributeSuffix(_session.ReplacementText, + document.GetLanguageService().IsCaseSensitive), cancellationToken); - if (_referenceSpanToLinkedRenameSpanMap.ContainsKey(replacement.OriginalSpan) && kind != RenameSpanKind.Complexified) + if (linkedRenameSpan.HasValue) { - var linkedRenameSpan = _session._renameInfo.GetConflictEditSpan( - new InlineRenameLocation(newDocument, replacement.NewSpan), GetTriggerText(newDocument, replacement.NewSpan), - GetWithoutAttributeSuffix(_session.ReplacementText, - document.GetLanguageService().IsCaseSensitive), cancellationToken); - - if (linkedRenameSpan.HasValue) - { - if (!mergeConflictComments.Any(s => replacement.NewSpan.IntersectsWith(s))) - { - _referenceSpanToLinkedRenameSpanMap[replacement.OriginalSpan] = new RenameTrackingSpan( - _subjectBuffer.CurrentSnapshot.CreateTrackingSpan( - linkedRenameSpan.Value.ToSpan(), - SpanTrackingMode.EdgeInclusive, - TrackingFidelityMode.Forward), - kind); - } - } - else + if (!mergeConflictComments.Any(s => replacement.NewSpan.IntersectsWith(s))) { - // We might not have a renameable span if an alias conflict completely changed the text _referenceSpanToLinkedRenameSpanMap[replacement.OriginalSpan] = new RenameTrackingSpan( - _referenceSpanToLinkedRenameSpanMap[replacement.OriginalSpan].TrackingSpan, - RenameSpanKind.None); - - if (_activeSpan.HasValue && _activeSpan.Value.IntersectsWith(replacement.OriginalSpan)) - { - _activeSpan = null; - } + _subjectBuffer.CurrentSnapshot.CreateTrackingSpan( + linkedRenameSpan.Value.ToSpan(), + SpanTrackingMode.EdgeInclusive, + TrackingFidelityMode.Forward), + kind); } } else { - if (!mergeConflictComments.Any(s => replacement.NewSpan.IntersectsWith(s))) + // We might not have a renameable span if an alias conflict completely changed the text + _referenceSpanToLinkedRenameSpanMap[replacement.OriginalSpan] = new RenameTrackingSpan( + _referenceSpanToLinkedRenameSpanMap[replacement.OriginalSpan].TrackingSpan, + RenameSpanKind.None); + + if (_activeSpan.HasValue && _activeSpan.Value.IntersectsWith(replacement.OriginalSpan)) { - _conflictResolutionRenameTrackingSpans.Add(new RenameTrackingSpan( - _subjectBuffer.CurrentSnapshot.CreateTrackingSpan(replacement.NewSpan.ToSpan(), SpanTrackingMode.EdgeInclusive, TrackingFidelityMode.Forward), - kind)); + _activeSpan = null; } } } - - if (!linkedDocumentsMightConflict) + else { - break; + if (!mergeConflictComments.Any(s => replacement.NewSpan.IntersectsWith(s))) + { + _conflictResolutionRenameTrackingSpans.Add(new RenameTrackingSpan( + _subjectBuffer.CurrentSnapshot.CreateTrackingSpan(replacement.NewSpan.ToSpan(), SpanTrackingMode.EdgeInclusive, TrackingFidelityMode.Forward), + kind)); + } } } - UpdateReadOnlyRegions(); - - // 3. Reset the undo state and notify the taggers. - this.ApplyReplacementText(updateSelection: false); - RaiseSpansChanged(); + if (!linkedDocumentsMightConflict) + { + break; + } } + + UpdateReadOnlyRegions(); + + // 3. Reset the undo state and notify the taggers. + this.ApplyReplacementText(updateSelection: false); + RaiseSpansChanged(); } + } - private static string GetWithoutAttributeSuffix(string text, bool isCaseSensitive) + private static string GetWithoutAttributeSuffix(string text, bool isCaseSensitive) + { + if (!text.TryGetWithoutAttributeSuffix(isCaseSensitive, out var replaceText)) { - if (!text.TryGetWithoutAttributeSuffix(isCaseSensitive, out var replaceText)) - { - replaceText = text; - } - - return replaceText; + replaceText = text; } - private static async Task> GetTextChangesFromTextDifferencingServiceAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken = default) + return replaceText; + } + + private static async Task> GetTextChangesFromTextDifferencingServiceAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken = default) + { + try { - try + using (Logger.LogBlock(FunctionId.Workspace_Document_GetTextChanges, newDocument.Name, cancellationToken)) { - using (Logger.LogBlock(FunctionId.Workspace_Document_GetTextChanges, newDocument.Name, cancellationToken)) + if (oldDocument == newDocument) { - if (oldDocument == newDocument) - { - // no changes - return SpecializedCollections.EmptyEnumerable(); - } + // no changes + return SpecializedCollections.EmptyEnumerable(); + } - if (newDocument.Id != oldDocument.Id) - { - throw new ArgumentException(WorkspacesResources.The_specified_document_is_not_a_version_of_this_document); - } + if (newDocument.Id != oldDocument.Id) + { + throw new ArgumentException(WorkspacesResources.The_specified_document_is_not_a_version_of_this_document); + } - var oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (oldText == newText) - { - return SpecializedCollections.EmptyEnumerable(); - } + if (oldText == newText) + { + return SpecializedCollections.EmptyEnumerable(); + } - var textChanges = newText.GetTextChanges(oldText).ToList(); + var textChanges = newText.GetTextChanges(oldText).ToList(); - // if changes are significant (not the whole document being replaced) then use these changes - if (textChanges.Count != 1 || textChanges[0].Span != new TextSpan(0, oldText.Length)) - { - return textChanges; - } - - var textDiffService = oldDocument.Project.Solution.Services.GetService(); - return await textDiffService.GetTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false); + // if changes are significant (not the whole document being replaced) then use these changes + if (textChanges.Count != 1 || textChanges[0].Span != new TextSpan(0, oldText.Length)) + { + return textChanges; } - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) - { - throw ExceptionUtilities.Unreachable(); + + var textDiffService = oldDocument.Project.Solution.Services.GetService(); + return await textDiffService.GetTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false); } } - - private IEnumerable GetMergedReplacementInfos( - IEnumerable relevantReplacements, - Document preMergeDocument, - Document postMergeDocument, - CancellationToken cancellationToken) + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) { - _session._threadingContext.ThrowIfNotOnUIThread(); + throw ExceptionUtilities.Unreachable(); + } + } - var textDiffService = preMergeDocument.Project.Solution.Services.GetService(); - var contentType = preMergeDocument.Project.Services.GetService().GetDefaultContentType(); + private IEnumerable GetMergedReplacementInfos( + IEnumerable relevantReplacements, + Document preMergeDocument, + Document postMergeDocument, + CancellationToken cancellationToken) + { + _session._threadingContext.ThrowIfNotOnUIThread(); - // TODO: Track all spans at once + var textDiffService = preMergeDocument.Project.Solution.Services.GetService(); + var contentType = preMergeDocument.Project.Services.GetService().GetDefaultContentType(); - SnapshotSpan? snapshotSpanToClone = null; - string preMergeDocumentTextString = null; + // TODO: Track all spans at once - var preMergeDocumentText = preMergeDocument.GetTextSynchronously(cancellationToken); - var snapshot = preMergeDocumentText.FindCorrespondingEditorTextSnapshot(); - if (snapshot != null && _textBufferCloneService != null) - { - snapshotSpanToClone = snapshot.GetFullSpan(); - } + SnapshotSpan? snapshotSpanToClone = null; + string preMergeDocumentTextString = null; - if (snapshotSpanToClone == null) - { - preMergeDocumentTextString = preMergeDocument.GetTextSynchronously(cancellationToken).ToString(); - } + var preMergeDocumentText = preMergeDocument.GetTextSynchronously(cancellationToken); + var snapshot = preMergeDocumentText.FindCorrespondingEditorTextSnapshot(); + if (snapshot != null && _textBufferCloneService != null) + { + snapshotSpanToClone = snapshot.GetFullSpan(); + } - foreach (var replacement in relevantReplacements) - { - var buffer = snapshotSpanToClone.HasValue ? _textBufferCloneService.CloneWithUnknownContentType(snapshotSpanToClone.Value) : _textBufferFactoryService.CreateTextBuffer(preMergeDocumentTextString, contentType); - var trackingSpan = buffer.CurrentSnapshot.CreateTrackingSpan(replacement.NewSpan.ToSpan(), SpanTrackingMode.EdgeExclusive, TrackingFidelityMode.Forward); + if (snapshotSpanToClone == null) + { + preMergeDocumentTextString = preMergeDocument.GetTextSynchronously(cancellationToken).ToString(); + } - using (var edit = _subjectBuffer.CreateEdit(EditOptions.None, null, s_calculateMergedSpansEditTag)) - { - foreach (var change in textDiffService.GetTextChangesAsync(preMergeDocument, postMergeDocument, cancellationToken).WaitAndGetResult(cancellationToken)) - { - buffer.Replace(change.Span.ToSpan(), change.NewText); - } + foreach (var replacement in relevantReplacements) + { + var buffer = snapshotSpanToClone.HasValue ? _textBufferCloneService.CloneWithUnknownContentType(snapshotSpanToClone.Value) : _textBufferFactoryService.CreateTextBuffer(preMergeDocumentTextString, contentType); + var trackingSpan = buffer.CurrentSnapshot.CreateTrackingSpan(replacement.NewSpan.ToSpan(), SpanTrackingMode.EdgeExclusive, TrackingFidelityMode.Forward); - edit.ApplyAndLogExceptions(); + using (var edit = _subjectBuffer.CreateEdit(EditOptions.None, null, s_calculateMergedSpansEditTag)) + { + foreach (var change in textDiffService.GetTextChangesAsync(preMergeDocument, postMergeDocument, cancellationToken).WaitAndGetResult(cancellationToken)) + { + buffer.Replace(change.Span.ToSpan(), change.NewText); } - yield return new InlineRenameReplacement(replacement.Kind, replacement.OriginalSpan, trackingSpan.GetSpan(buffer.CurrentSnapshot).Span.ToTextSpan()); + edit.ApplyAndLogExceptions(); } + + yield return new InlineRenameReplacement(replacement.Kind, replacement.OriginalSpan, trackingSpan.GetSpan(buffer.CurrentSnapshot).Span.ToTextSpan()); } + } - private static RenameSpanKind GetRenameSpanKind(InlineRenameReplacementKind kind) + private static RenameSpanKind GetRenameSpanKind(InlineRenameReplacementKind kind) + { + switch (kind) { - switch (kind) - { - case InlineRenameReplacementKind.NoConflict: - case InlineRenameReplacementKind.ResolvedReferenceConflict: - return RenameSpanKind.Reference; + case InlineRenameReplacementKind.NoConflict: + case InlineRenameReplacementKind.ResolvedReferenceConflict: + return RenameSpanKind.Reference; - case InlineRenameReplacementKind.ResolvedNonReferenceConflict: - return RenameSpanKind.None; + case InlineRenameReplacementKind.ResolvedNonReferenceConflict: + return RenameSpanKind.None; - case InlineRenameReplacementKind.UnresolvedConflict: - return RenameSpanKind.UnresolvedConflict; + case InlineRenameReplacementKind.UnresolvedConflict: + return RenameSpanKind.UnresolvedConflict; - case InlineRenameReplacementKind.Complexified: - return RenameSpanKind.Complexified; + case InlineRenameReplacementKind.Complexified: + return RenameSpanKind.Complexified; - default: - throw ExceptionUtilities.UnexpectedValue(kind); - } + default: + throw ExceptionUtilities.UnexpectedValue(kind); } + } - private readonly struct SelectionTracking : IDisposable - { - private readonly int? _anchor; - private readonly int? _active; - private readonly TextSpan _anchorSpan; - private readonly TextSpan _activeSpan; - private readonly OpenTextBufferManager _openTextBufferManager; + private readonly struct SelectionTracking : IDisposable + { + private readonly int? _anchor; + private readonly int? _active; + private readonly TextSpan _anchorSpan; + private readonly TextSpan _activeSpan; + private readonly OpenTextBufferManager _openTextBufferManager; - public SelectionTracking(OpenTextBufferManager openTextBufferManager) + public SelectionTracking(OpenTextBufferManager openTextBufferManager) + { + _openTextBufferManager = openTextBufferManager; + _anchor = null; + _anchorSpan = default; + _active = null; + _activeSpan = default; + + var textView = openTextBufferManager.ActiveTextView; + if (textView == null) { - _openTextBufferManager = openTextBufferManager; - _anchor = null; - _anchorSpan = default; - _active = null; - _activeSpan = default; - - var textView = openTextBufferManager.ActiveTextView; - if (textView == null) - { - return; - } + return; + } - var selection = textView.Selection; - var snapshot = openTextBufferManager._subjectBuffer.CurrentSnapshot; + var selection = textView.Selection; + var snapshot = openTextBufferManager._subjectBuffer.CurrentSnapshot; - var containingSpans = openTextBufferManager._referenceSpanToLinkedRenameSpanMap.Select(kvp => + var containingSpans = openTextBufferManager._referenceSpanToLinkedRenameSpanMap.Select(kvp => + { + // GetSpanInView() can return an empty collection if the tracking span isn't mapped to anything + // in the current view, specifically a `@model SomeModelClass` directive in a Razor file. + var ss = textView.GetSpanInView(kvp.Value.TrackingSpan.GetSpan(snapshot)).FirstOrDefault(); + if (ss != default && (ss.IntersectsWith(selection.ActivePoint.Position) || ss.IntersectsWith(selection.AnchorPoint.Position))) { - // GetSpanInView() can return an empty collection if the tracking span isn't mapped to anything - // in the current view, specifically a `@model SomeModelClass` directive in a Razor file. - var ss = textView.GetSpanInView(kvp.Value.TrackingSpan.GetSpan(snapshot)).FirstOrDefault(); - if (ss != default && (ss.IntersectsWith(selection.ActivePoint.Position) || ss.IntersectsWith(selection.AnchorPoint.Position))) - { - return Tuple.Create(kvp.Key, ss); - } - else - { - return null; - } - }).WhereNotNull(); - - foreach (var tuple in containingSpans) + return Tuple.Create(kvp.Key, ss); + } + else { - if (tuple.Item2.IntersectsWith(selection.AnchorPoint.Position)) - { - _anchor = tuple.Item2.End - selection.AnchorPoint.Position; - _anchorSpan = tuple.Item1; - } - - if (tuple.Item2.IntersectsWith(selection.ActivePoint.Position)) - { - _active = tuple.Item2.End - selection.ActivePoint.Position; - _activeSpan = tuple.Item1; - } + return null; } - } + }).WhereNotNull(); - public void Dispose() + foreach (var tuple in containingSpans) { - var textView = _openTextBufferManager.ActiveTextView; - if (textView == null) + if (tuple.Item2.IntersectsWith(selection.AnchorPoint.Position)) { - return; + _anchor = tuple.Item2.End - selection.AnchorPoint.Position; + _anchorSpan = tuple.Item1; } - if (_anchor.HasValue || _active.HasValue) + if (tuple.Item2.IntersectsWith(selection.ActivePoint.Position)) { - var selection = textView.Selection; - var snapshot = _openTextBufferManager._subjectBuffer.CurrentSnapshot; - - var anchorSpan = _anchorSpan; - var anchorPoint = new VirtualSnapshotPoint(textView.TextSnapshot, - _anchor.HasValue && _openTextBufferManager._referenceSpanToLinkedRenameSpanMap.Keys.Any(s => s.OverlapsWith(anchorSpan)) - ? GetNewEndpoint(_anchorSpan) - _anchor.Value - : selection.AnchorPoint.Position); - - var activeSpan = _activeSpan; - var activePoint = new VirtualSnapshotPoint(textView.TextSnapshot, - _active.HasValue && _openTextBufferManager._referenceSpanToLinkedRenameSpanMap.Keys.Any(s => s.OverlapsWith(activeSpan)) - ? GetNewEndpoint(_activeSpan) - _active.Value - : selection.ActivePoint.Position); - - textView.SetSelection(anchorPoint, activePoint); + _active = tuple.Item2.End - selection.ActivePoint.Position; + _activeSpan = tuple.Item1; } } + } + + public void Dispose() + { + var textView = _openTextBufferManager.ActiveTextView; + if (textView == null) + { + return; + } - private SnapshotPoint GetNewEndpoint(TextSpan span) + if (_anchor.HasValue || _active.HasValue) { + var selection = textView.Selection; var snapshot = _openTextBufferManager._subjectBuffer.CurrentSnapshot; - var endPoint = _openTextBufferManager._referenceSpanToLinkedRenameSpanMap.TryGetValue(span, out var renameTrackingSpan) - ? renameTrackingSpan.TrackingSpan.GetEndPoint(snapshot) - : _openTextBufferManager._referenceSpanToLinkedRenameSpanMap.First(kvp => kvp.Key.OverlapsWith(span)).Value.TrackingSpan.GetEndPoint(snapshot); - return _openTextBufferManager.ActiveTextView.BufferGraph.MapUpToBuffer(endPoint, PointTrackingMode.Positive, PositionAffinity.Successor, _openTextBufferManager.ActiveTextView.TextBuffer).Value; + + var anchorSpan = _anchorSpan; + var anchorPoint = new VirtualSnapshotPoint(textView.TextSnapshot, + _anchor.HasValue && _openTextBufferManager._referenceSpanToLinkedRenameSpanMap.Keys.Any(s => s.OverlapsWith(anchorSpan)) + ? GetNewEndpoint(_anchorSpan) - _anchor.Value + : selection.AnchorPoint.Position); + + var activeSpan = _activeSpan; + var activePoint = new VirtualSnapshotPoint(textView.TextSnapshot, + _active.HasValue && _openTextBufferManager._referenceSpanToLinkedRenameSpanMap.Keys.Any(s => s.OverlapsWith(activeSpan)) + ? GetNewEndpoint(_activeSpan) - _active.Value + : selection.ActivePoint.Position); + + textView.SetSelection(anchorPoint, activePoint); } } + + private SnapshotPoint GetNewEndpoint(TextSpan span) + { + var snapshot = _openTextBufferManager._subjectBuffer.CurrentSnapshot; + var endPoint = _openTextBufferManager._referenceSpanToLinkedRenameSpanMap.TryGetValue(span, out var renameTrackingSpan) + ? renameTrackingSpan.TrackingSpan.GetEndPoint(snapshot) + : _openTextBufferManager._referenceSpanToLinkedRenameSpanMap.First(kvp => kvp.Key.OverlapsWith(span)).Value.TrackingSpan.GetEndPoint(snapshot); + return _openTextBufferManager.ActiveTextView.BufferGraph.MapUpToBuffer(endPoint, PointTrackingMode.Positive, PositionAffinity.Successor, _openTextBufferManager.ActiveTextView.TextBuffer).Value; + } } } } diff --git a/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs b/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs index ec414c2094be0..bb8d8217667c3 100644 --- a/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs +++ b/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs @@ -33,945 +33,944 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal partial class InlineRenameSession : IInlineRenameSession, IFeatureController { - internal partial class InlineRenameSession : IInlineRenameSession, IFeatureController + private readonly Workspace _workspace; + private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; + + private readonly ITextBufferAssociatedViewService _textBufferAssociatedViewService; + private readonly ITextBufferFactoryService _textBufferFactoryService; + private readonly ITextBufferCloneService _textBufferCloneService; + + private readonly IFeatureService _featureService; + private readonly IFeatureDisableToken _completionDisabledToken; + private readonly IEnumerable _refactorNotifyServices; + private readonly IAsynchronousOperationListener _asyncListener; + private readonly Solution _baseSolution; + private readonly Document _triggerDocument; + private readonly ITextView _triggerView; + private readonly IDisposable _inlineRenameSessionDurationLogBlock; + private readonly IThreadingContext _threadingContext; + public readonly InlineRenameService RenameService; + + private bool _dismissed; + private bool _isApplyingEdit; + private string _replacementText; + private SymbolRenameOptions _options; + private bool _previewChanges; + private readonly Dictionary _openTextBuffers = []; + + /// + /// The original for the identifier that rename was triggered on + /// + public SnapshotSpan TriggerSpan { get; } + + /// + /// If non-null, the current text of the replacement. Linked spans added will automatically be updated with this + /// text. + /// + public string ReplacementText { - private readonly Workspace _workspace; - private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; - - private readonly ITextBufferAssociatedViewService _textBufferAssociatedViewService; - private readonly ITextBufferFactoryService _textBufferFactoryService; - private readonly ITextBufferCloneService _textBufferCloneService; - - private readonly IFeatureService _featureService; - private readonly IFeatureDisableToken _completionDisabledToken; - private readonly IEnumerable _refactorNotifyServices; - private readonly IAsynchronousOperationListener _asyncListener; - private readonly Solution _baseSolution; - private readonly Document _triggerDocument; - private readonly ITextView _triggerView; - private readonly IDisposable _inlineRenameSessionDurationLogBlock; - private readonly IThreadingContext _threadingContext; - public readonly InlineRenameService RenameService; - - private bool _dismissed; - private bool _isApplyingEdit; - private string _replacementText; - private SymbolRenameOptions _options; - private bool _previewChanges; - private readonly Dictionary _openTextBuffers = []; - - /// - /// The original for the identifier that rename was triggered on - /// - public SnapshotSpan TriggerSpan { get; } - - /// - /// If non-null, the current text of the replacement. Linked spans added will automatically be updated with this - /// text. - /// - public string ReplacementText - { - get - { - return _replacementText; - } - private set - { - _replacementText = value; - ReplacementTextChanged?.Invoke(this, EventArgs.Empty); - } + get + { + return _replacementText; } + private set + { + _replacementText = value; + ReplacementTextChanged?.Invoke(this, EventArgs.Empty); + } + } - /// - /// Information about whether a file rename should be allowed as part - /// of the rename operation, as determined by the language - /// - public InlineRenameFileRenameInfo FileRenameInfo { get; } - - /// - /// Keep-alive session held alive with the OOP server. This allows us to pin the initial solution snapshot over on - /// the oop side, which is valuable for preventing it from constantly being dropped/synced on every conflict - /// resolution step. - /// - private readonly RemoteKeepAliveSession _keepAliveSession; - - /// - /// The task which computes the main rename locations against the original workspace - /// snapshot. - /// - private JoinableTask _allRenameLocationsTask; - - /// - /// The cancellation token for most work being done by the inline rename session. This - /// includes the tasks. - /// - private readonly CancellationTokenSource _cancellationTokenSource = new(); - - /// - /// This task is a continuation of the that is the result of computing - /// the resolutions of the rename spans for the current replacementText. - /// - private JoinableTask _conflictResolutionTask; - - /// - /// The cancellation source for . - /// - private CancellationTokenSource _conflictResolutionTaskCancellationSource = new CancellationTokenSource(); - - private readonly IInlineRenameInfo _renameInfo; - - /// - /// The initial text being renamed. - /// - private readonly string _initialRenameText; - - public InlineRenameSession( - IThreadingContext threadingContext, - InlineRenameService renameService, - Workspace workspace, - SnapshotSpan triggerSpan, - IInlineRenameInfo renameInfo, - SymbolRenameOptions options, - bool previewChanges, - IUIThreadOperationExecutor uiThreadOperationExecutor, - ITextBufferAssociatedViewService textBufferAssociatedViewService, - ITextBufferFactoryService textBufferFactoryService, - ITextBufferCloneService textBufferCloneService, - IFeatureServiceFactory featureServiceFactory, - IEnumerable refactorNotifyServices, - IAsynchronousOperationListener asyncListener) - { - // This should always be touching a symbol since we verified that upon invocation - _threadingContext = threadingContext; - _renameInfo = renameInfo; - - TriggerSpan = triggerSpan; - _triggerDocument = triggerSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (_triggerDocument == null) - { - throw new InvalidOperationException(EditorFeaturesResources.The_triggerSpan_is_not_included_in_the_given_workspace); - } + /// + /// Information about whether a file rename should be allowed as part + /// of the rename operation, as determined by the language + /// + public InlineRenameFileRenameInfo FileRenameInfo { get; } + + /// + /// Keep-alive session held alive with the OOP server. This allows us to pin the initial solution snapshot over on + /// the oop side, which is valuable for preventing it from constantly being dropped/synced on every conflict + /// resolution step. + /// + private readonly RemoteKeepAliveSession _keepAliveSession; + + /// + /// The task which computes the main rename locations against the original workspace + /// snapshot. + /// + private JoinableTask _allRenameLocationsTask; + + /// + /// The cancellation token for most work being done by the inline rename session. This + /// includes the tasks. + /// + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + /// + /// This task is a continuation of the that is the result of computing + /// the resolutions of the rename spans for the current replacementText. + /// + private JoinableTask _conflictResolutionTask; + + /// + /// The cancellation source for . + /// + private CancellationTokenSource _conflictResolutionTaskCancellationSource = new CancellationTokenSource(); + + private readonly IInlineRenameInfo _renameInfo; + + /// + /// The initial text being renamed. + /// + private readonly string _initialRenameText; + + public InlineRenameSession( + IThreadingContext threadingContext, + InlineRenameService renameService, + Workspace workspace, + SnapshotSpan triggerSpan, + IInlineRenameInfo renameInfo, + SymbolRenameOptions options, + bool previewChanges, + IUIThreadOperationExecutor uiThreadOperationExecutor, + ITextBufferAssociatedViewService textBufferAssociatedViewService, + ITextBufferFactoryService textBufferFactoryService, + ITextBufferCloneService textBufferCloneService, + IFeatureServiceFactory featureServiceFactory, + IEnumerable refactorNotifyServices, + IAsynchronousOperationListener asyncListener) + { + // This should always be touching a symbol since we verified that upon invocation + _threadingContext = threadingContext; + _renameInfo = renameInfo; - _inlineRenameSessionDurationLogBlock = Logger.LogBlock(FunctionId.Rename_InlineSession, CancellationToken.None); + TriggerSpan = triggerSpan; + _triggerDocument = triggerSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (_triggerDocument == null) + { + throw new InvalidOperationException(EditorFeaturesResources.The_triggerSpan_is_not_included_in_the_given_workspace); + } - _workspace = workspace; - _workspace.WorkspaceChanged += OnWorkspaceChanged; + _inlineRenameSessionDurationLogBlock = Logger.LogBlock(FunctionId.Rename_InlineSession, CancellationToken.None); - _textBufferFactoryService = textBufferFactoryService; - _textBufferCloneService = textBufferCloneService; - _textBufferAssociatedViewService = textBufferAssociatedViewService; - _textBufferAssociatedViewService.SubjectBuffersConnected += OnSubjectBuffersConnected; + _workspace = workspace; + _workspace.WorkspaceChanged += OnWorkspaceChanged; - // Disable completion when an inline rename session starts - _featureService = featureServiceFactory.GlobalFeatureService; - _completionDisabledToken = _featureService.Disable(PredefinedEditorFeatureNames.Completion, this); - RenameService = renameService; - _uiThreadOperationExecutor = uiThreadOperationExecutor; - _refactorNotifyServices = refactorNotifyServices; - _asyncListener = asyncListener; - _triggerView = textBufferAssociatedViewService.GetAssociatedTextViews(triggerSpan.Snapshot.TextBuffer).FirstOrDefault(v => v.HasAggregateFocus) ?? - textBufferAssociatedViewService.GetAssociatedTextViews(triggerSpan.Snapshot.TextBuffer).First(); + _textBufferFactoryService = textBufferFactoryService; + _textBufferCloneService = textBufferCloneService; + _textBufferAssociatedViewService = textBufferAssociatedViewService; + _textBufferAssociatedViewService.SubjectBuffersConnected += OnSubjectBuffersConnected; - _options = options; - _previewChanges = previewChanges; + // Disable completion when an inline rename session starts + _featureService = featureServiceFactory.GlobalFeatureService; + _completionDisabledToken = _featureService.Disable(PredefinedEditorFeatureNames.Completion, this); + RenameService = renameService; + _uiThreadOperationExecutor = uiThreadOperationExecutor; + _refactorNotifyServices = refactorNotifyServices; + _asyncListener = asyncListener; + _triggerView = textBufferAssociatedViewService.GetAssociatedTextViews(triggerSpan.Snapshot.TextBuffer).FirstOrDefault(v => v.HasAggregateFocus) ?? + textBufferAssociatedViewService.GetAssociatedTextViews(triggerSpan.Snapshot.TextBuffer).First(); - _initialRenameText = triggerSpan.GetText(); - this.ReplacementText = _initialRenameText; + _options = options; + _previewChanges = previewChanges; - _baseSolution = _triggerDocument.Project.Solution; - this.UndoManager = workspace.Services.GetService(); + _initialRenameText = triggerSpan.GetText(); + this.ReplacementText = _initialRenameText; - FileRenameInfo = _renameInfo.GetFileRenameInfo(); + _baseSolution = _triggerDocument.Project.Solution; + this.UndoManager = workspace.Services.GetService(); - // Open a session to oop, syncing our solution to it and pinning it there. The connection will close once - // _cancellationTokenSource is canceled (which we always do when the session is finally ended). - _keepAliveSession = RemoteKeepAliveSession.Create(_baseSolution, asyncListener); - InitializeOpenBuffers(triggerSpan); - } + FileRenameInfo = _renameInfo.GetFileRenameInfo(); + + // Open a session to oop, syncing our solution to it and pinning it there. The connection will close once + // _cancellationTokenSource is canceled (which we always do when the session is finally ended). + _keepAliveSession = RemoteKeepAliveSession.Create(_baseSolution, asyncListener); + InitializeOpenBuffers(triggerSpan); + } - public string OriginalSymbolName => _renameInfo.DisplayName; + public string OriginalSymbolName => _renameInfo.DisplayName; - // Used to aid the investigation of https://github.com/dotnet/roslyn/issues/7364 - private class NullTextBufferException(Document document, SourceText text) : Exception("Cannot retrieve textbuffer from document.") - { + // Used to aid the investigation of https://github.com/dotnet/roslyn/issues/7364 + private class NullTextBufferException(Document document, SourceText text) : Exception("Cannot retrieve textbuffer from document.") + { #pragma warning disable IDE0052 // Remove unread private members - private readonly Document _document = document; - private readonly SourceText _text = text; - } + private readonly Document _document = document; + private readonly SourceText _text = text; + } - private void InitializeOpenBuffers(SnapshotSpan triggerSpan) + private void InitializeOpenBuffers(SnapshotSpan triggerSpan) + { + using (Logger.LogBlock(FunctionId.Rename_CreateOpenTextBufferManagerForAllOpenDocs, CancellationToken.None)) { - using (Logger.LogBlock(FunctionId.Rename_CreateOpenTextBufferManagerForAllOpenDocs, CancellationToken.None)) + var openBuffers = new HashSet(); + foreach (var d in _workspace.GetOpenDocumentIds()) { - var openBuffers = new HashSet(); - foreach (var d in _workspace.GetOpenDocumentIds()) + var document = _baseSolution.GetDocument(d); + if (document == null) { - var document = _baseSolution.GetDocument(d); - if (document == null) - { - continue; - } - - Contract.ThrowIfFalse(document.TryGetText(out var text)); - Contract.ThrowIfNull(text); - - var textSnapshot = text.FindCorrespondingEditorTextSnapshot(); - if (textSnapshot == null) - { - FatalError.ReportAndCatch(new NullTextBufferException(document, text)); - continue; - } - - Contract.ThrowIfNull(textSnapshot.TextBuffer); - - openBuffers.Add(textSnapshot.TextBuffer); + continue; } - foreach (var buffer in openBuffers) + Contract.ThrowIfFalse(document.TryGetText(out var text)); + Contract.ThrowIfNull(text); + + var textSnapshot = text.FindCorrespondingEditorTextSnapshot(); + if (textSnapshot == null) { - TryPopulateOpenTextBufferManagerForBuffer(buffer); + FatalError.ReportAndCatch(new NullTextBufferException(document, text)); + continue; } - } - var startingSpan = triggerSpan.Span; + Contract.ThrowIfNull(textSnapshot.TextBuffer); - // Select this span if we didn't already have something selected - var selections = _triggerView.Selection.GetSnapshotSpansOnBuffer(triggerSpan.Snapshot.TextBuffer); - if (!selections.Any() || - selections.First().IsEmpty || - !startingSpan.Contains(selections.First())) - { - _triggerView.SetSelection(new SnapshotSpan(triggerSpan.Snapshot, startingSpan)); + openBuffers.Add(textSnapshot.TextBuffer); } - this.UndoManager.CreateInitialState(this.ReplacementText, _triggerView.Selection, new SnapshotSpan(triggerSpan.Snapshot, startingSpan)); - _openTextBuffers[triggerSpan.Snapshot.TextBuffer].SetReferenceSpans(SpecializedCollections.SingletonEnumerable(startingSpan.ToTextSpan())); + foreach (var buffer in openBuffers) + { + TryPopulateOpenTextBufferManagerForBuffer(buffer); + } + } - UpdateReferenceLocationsTask(); + var startingSpan = triggerSpan.Span; - RenameTrackingDismisser.DismissRenameTracking(_workspace, _workspace.GetOpenDocumentIds()); + // Select this span if we didn't already have something selected + var selections = _triggerView.Selection.GetSnapshotSpansOnBuffer(triggerSpan.Snapshot.TextBuffer); + if (!selections.Any() || + selections.First().IsEmpty || + !startingSpan.Contains(selections.First())) + { + _triggerView.SetSelection(new SnapshotSpan(triggerSpan.Snapshot, startingSpan)); } - private bool TryPopulateOpenTextBufferManagerForBuffer(ITextBuffer buffer) - { - _threadingContext.ThrowIfNotOnUIThread(); - VerifyNotDismissed(); + this.UndoManager.CreateInitialState(this.ReplacementText, _triggerView.Selection, new SnapshotSpan(triggerSpan.Snapshot, startingSpan)); + _openTextBuffers[triggerSpan.Snapshot.TextBuffer].SetReferenceSpans(SpecializedCollections.SingletonEnumerable(startingSpan.ToTextSpan())); - if (_workspace.Kind == WorkspaceKind.Interactive) - { - Debug.Assert(buffer.GetRelatedDocuments().Count() == 1); - Debug.Assert(buffer.IsReadOnly(0) == buffer.IsReadOnly(VisualStudio.Text.Span.FromBounds(0, buffer.CurrentSnapshot.Length))); // All or nothing. - if (buffer.IsReadOnly(0)) - { - return false; - } - } + UpdateReferenceLocationsTask(); + + RenameTrackingDismisser.DismissRenameTracking(_workspace, _workspace.GetOpenDocumentIds()); + } + + private bool TryPopulateOpenTextBufferManagerForBuffer(ITextBuffer buffer) + { + _threadingContext.ThrowIfNotOnUIThread(); + VerifyNotDismissed(); - if (!_openTextBuffers.ContainsKey(buffer) && buffer.SupportsRename()) + if (_workspace.Kind == WorkspaceKind.Interactive) + { + Debug.Assert(buffer.GetRelatedDocuments().Count() == 1); + Debug.Assert(buffer.IsReadOnly(0) == buffer.IsReadOnly(VisualStudio.Text.Span.FromBounds(0, buffer.CurrentSnapshot.Length))); // All or nothing. + if (buffer.IsReadOnly(0)) { - _openTextBuffers[buffer] = new OpenTextBufferManager(this, _workspace, _textBufferFactoryService, _textBufferCloneService, buffer); - return true; + return false; } + } - return _openTextBuffers.ContainsKey(buffer); + if (!_openTextBuffers.ContainsKey(buffer) && buffer.SupportsRename()) + { + _openTextBuffers[buffer] = new OpenTextBufferManager(this, _workspace, _textBufferFactoryService, _textBufferCloneService, buffer); + return true; } - private void OnSubjectBuffersConnected(object sender, SubjectBuffersConnectedEventArgs e) + return _openTextBuffers.ContainsKey(buffer); + } + + private void OnSubjectBuffersConnected(object sender, SubjectBuffersConnectedEventArgs e) + { + _threadingContext.ThrowIfNotOnUIThread(); + foreach (var buffer in e.SubjectBuffers) { - _threadingContext.ThrowIfNotOnUIThread(); - foreach (var buffer in e.SubjectBuffers) + if (buffer.GetWorkspace() == _workspace) { - if (buffer.GetWorkspace() == _workspace) + if (TryPopulateOpenTextBufferManagerForBuffer(buffer)) { - if (TryPopulateOpenTextBufferManagerForBuffer(buffer)) - { - _openTextBuffers[buffer].ConnectToView(e.TextView); - } + _openTextBuffers[buffer].ConnectToView(e.TextView); } } } + } - private void UpdateReferenceLocationsTask() - { - _threadingContext.ThrowIfNotOnUIThread(); + private void UpdateReferenceLocationsTask() + { + _threadingContext.ThrowIfNotOnUIThread(); - var asyncToken = _asyncListener.BeginAsyncOperation("UpdateReferencesTask"); + var asyncToken = _asyncListener.BeginAsyncOperation("UpdateReferencesTask"); - var currentOptions = _options; - var currentRenameLocationsTask = _allRenameLocationsTask; - var cancellationToken = _cancellationTokenSource.Token; + var currentOptions = _options; + var currentRenameLocationsTask = _allRenameLocationsTask; + var cancellationToken = _cancellationTokenSource.Token; - _allRenameLocationsTask = _threadingContext.JoinableTaskFactory.RunAsync(async () => - { - // Join prior work before proceeding, since it performs a required state update. - // https://github.com/dotnet/roslyn/pull/34254#discussion_r267024593 - if (currentRenameLocationsTask != null) - await _allRenameLocationsTask.JoinAsync(cancellationToken).ConfigureAwait(false); + _allRenameLocationsTask = _threadingContext.JoinableTaskFactory.RunAsync(async () => + { + // Join prior work before proceeding, since it performs a required state update. + // https://github.com/dotnet/roslyn/pull/34254#discussion_r267024593 + if (currentRenameLocationsTask != null) + await _allRenameLocationsTask.JoinAsync(cancellationToken).ConfigureAwait(false); - await TaskScheduler.Default; - var inlineRenameLocations = await _renameInfo.FindRenameLocationsAsync(currentOptions, cancellationToken).ConfigureAwait(false); + await TaskScheduler.Default; + var inlineRenameLocations = await _renameInfo.FindRenameLocationsAsync(currentOptions, cancellationToken).ConfigureAwait(false); - // It's unfortunate that _allRenameLocationsTask has a UI thread dependency (prevents continuations - // from running prior to the completion of the UI operation), but the implementation does not currently - // follow the originally-intended design. - // https://github.com/dotnet/roslyn/issues/40890 - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken); + // It's unfortunate that _allRenameLocationsTask has a UI thread dependency (prevents continuations + // from running prior to the completion of the UI operation), but the implementation does not currently + // follow the originally-intended design. + // https://github.com/dotnet/roslyn/issues/40890 + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken); - RaiseSessionSpansUpdated(inlineRenameLocations.Locations.ToImmutableArray()); + RaiseSessionSpansUpdated(inlineRenameLocations.Locations.ToImmutableArray()); - return inlineRenameLocations; - }); + return inlineRenameLocations; + }); - _allRenameLocationsTask.Task.CompletesAsyncOperation(asyncToken); + _allRenameLocationsTask.Task.CompletesAsyncOperation(asyncToken); - UpdateConflictResolutionTask(); - QueueApplyReplacements(); - } + UpdateConflictResolutionTask(); + QueueApplyReplacements(); + } - public Workspace Workspace => _workspace; - public SymbolRenameOptions Options => _options; - public bool PreviewChanges => _previewChanges; - public bool HasRenameOverloads => _renameInfo.HasOverloads; - public bool MustRenameOverloads => _renameInfo.MustRenameOverloads; + public Workspace Workspace => _workspace; + public SymbolRenameOptions Options => _options; + public bool PreviewChanges => _previewChanges; + public bool HasRenameOverloads => _renameInfo.HasOverloads; + public bool MustRenameOverloads => _renameInfo.MustRenameOverloads; - public IInlineRenameUndoManager UndoManager { get; } + public IInlineRenameUndoManager UndoManager { get; } - public event EventHandler> ReferenceLocationsChanged; - public event EventHandler ReplacementsComputed; - public event EventHandler ReplacementTextChanged; + public event EventHandler> ReferenceLocationsChanged; + public event EventHandler ReplacementsComputed; + public event EventHandler ReplacementTextChanged; - internal OpenTextBufferManager GetBufferManager(ITextBuffer buffer) - => _openTextBuffers[buffer]; + internal OpenTextBufferManager GetBufferManager(ITextBuffer buffer) + => _openTextBuffers[buffer]; - internal bool TryGetBufferManager(ITextBuffer buffer, out OpenTextBufferManager bufferManager) - => _openTextBuffers.TryGetValue(buffer, out bufferManager); + internal bool TryGetBufferManager(ITextBuffer buffer, out OpenTextBufferManager bufferManager) + => _openTextBuffers.TryGetValue(buffer, out bufferManager); - public void RefreshRenameSessionWithOptionsChanged(SymbolRenameOptions newOptions) + public void RefreshRenameSessionWithOptionsChanged(SymbolRenameOptions newOptions) + { + if (_options == newOptions) { - if (_options == newOptions) - { - return; - } + return; + } - _threadingContext.ThrowIfNotOnUIThread(); - VerifyNotDismissed(); + _threadingContext.ThrowIfNotOnUIThread(); + VerifyNotDismissed(); - _options = newOptions; - UpdateReferenceLocationsTask(); - } + _options = newOptions; + UpdateReferenceLocationsTask(); + } - public void SetPreviewChanges(bool value) - { - _threadingContext.ThrowIfNotOnUIThread(); - VerifyNotDismissed(); + public void SetPreviewChanges(bool value) + { + _threadingContext.ThrowIfNotOnUIThread(); + VerifyNotDismissed(); - _previewChanges = value; - } + _previewChanges = value; + } - private void VerifyNotDismissed() + private void VerifyNotDismissed() + { + if (_dismissed) { - if (_dismissed) - { - throw new InvalidOperationException(EditorFeaturesResources.This_session_has_already_been_dismissed); - } + throw new InvalidOperationException(EditorFeaturesResources.This_session_has_already_been_dismissed); } + } - private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs args) + private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs args) + { + if (args.Kind != WorkspaceChangeKind.DocumentChanged) { - if (args.Kind != WorkspaceChangeKind.DocumentChanged) + Logger.Log(FunctionId.Rename_InlineSession_Cancel_NonDocumentChangedWorkspaceChange, KeyValueLogMessage.Create(m => { - Logger.Log(FunctionId.Rename_InlineSession_Cancel_NonDocumentChangedWorkspaceChange, KeyValueLogMessage.Create(m => - { - m["Kind"] = Enum.GetName(typeof(WorkspaceChangeKind), args.Kind); - })); + m["Kind"] = Enum.GetName(typeof(WorkspaceChangeKind), args.Kind); + })); - Cancel(); - } + Cancel(); } + } - private void RaiseSessionSpansUpdated(ImmutableArray locations) + private void RaiseSessionSpansUpdated(ImmutableArray locations) + { + _threadingContext.ThrowIfNotOnUIThread(); + SetReferenceLocations(locations); + + // It's OK to call SetReferenceLocations with all documents, including unchangeable ones, + // because they can't be opened, so the _openTextBuffers loop won't matter. In fact, the entire + // inline rename is oblivious to unchangeable documents, we just need to filter out references + // in them to avoid displaying them in the UI. + // https://github.com/dotnet/roslyn/issues/41242 + if (_workspace.IgnoreUnchangeableDocumentsWhenApplyingChanges) { - _threadingContext.ThrowIfNotOnUIThread(); - SetReferenceLocations(locations); - - // It's OK to call SetReferenceLocations with all documents, including unchangeable ones, - // because they can't be opened, so the _openTextBuffers loop won't matter. In fact, the entire - // inline rename is oblivious to unchangeable documents, we just need to filter out references - // in them to avoid displaying them in the UI. - // https://github.com/dotnet/roslyn/issues/41242 - if (_workspace.IgnoreUnchangeableDocumentsWhenApplyingChanges) + locations = locations.WhereAsArray(l => l.Document.CanApplyChange()); + } + + ReferenceLocationsChanged?.Invoke(this, locations); + } + + private void SetReferenceLocations(ImmutableArray locations) + { + _threadingContext.ThrowIfNotOnUIThread(); + + var locationsByDocument = locations.ToLookup(l => l.Document.Id); + + _isApplyingEdit = true; + foreach (var textBuffer in _openTextBuffers.Keys) + { + var documents = textBuffer.AsTextContainer().GetRelatedDocuments(); + + if (!documents.Any(static (d, locationsByDocument) => locationsByDocument.Contains(d.Id), locationsByDocument)) { - locations = locations.WhereAsArray(l => l.Document.CanApplyChange()); + _openTextBuffers[textBuffer].SetReferenceSpans(SpecializedCollections.EmptyEnumerable()); + } + else + { + var spans = documents.SelectMany(d => locationsByDocument[d.Id]).Select(l => l.TextSpan).Distinct(); + _openTextBuffers[textBuffer].SetReferenceSpans(spans); } - - ReferenceLocationsChanged?.Invoke(this, locations); } - private void SetReferenceLocations(ImmutableArray locations) + _isApplyingEdit = false; + } + + /// + /// Updates the replacement text for the rename session and propagates it to all live buffers. + /// + internal void ApplyReplacementText(string replacementText, bool propagateEditImmediately, bool updateSelection = true) + { + _threadingContext.ThrowIfNotOnUIThread(); + VerifyNotDismissed(); + this.ReplacementText = _renameInfo.GetFinalSymbolName(replacementText); + + var asyncToken = _asyncListener.BeginAsyncOperation(nameof(ApplyReplacementText)); + + Action propagateEditAction = delegate { _threadingContext.ThrowIfNotOnUIThread(); - var locationsByDocument = locations.ToLookup(l => l.Document.Id); + if (_dismissed) + { + asyncToken.Dispose(); + return; + } _isApplyingEdit = true; - foreach (var textBuffer in _openTextBuffers.Keys) + using (Logger.LogBlock(FunctionId.Rename_ApplyReplacementText, replacementText, _cancellationTokenSource.Token)) { - var documents = textBuffer.AsTextContainer().GetRelatedDocuments(); - - if (!documents.Any(static (d, locationsByDocument) => locationsByDocument.Contains(d.Id), locationsByDocument)) - { - _openTextBuffers[textBuffer].SetReferenceSpans(SpecializedCollections.EmptyEnumerable()); - } - else + foreach (var openBuffer in _openTextBuffers.Values) { - var spans = documents.SelectMany(d => locationsByDocument[d.Id]).Select(l => l.TextSpan).Distinct(); - _openTextBuffers[textBuffer].SetReferenceSpans(spans); + openBuffer.ApplyReplacementText(updateSelection); } } _isApplyingEdit = false; - } - /// - /// Updates the replacement text for the rename session and propagates it to all live buffers. - /// - internal void ApplyReplacementText(string replacementText, bool propagateEditImmediately, bool updateSelection = true) + // We already kicked off UpdateConflictResolutionTask below (outside the delegate). + // Now that we are certain the replacement text has been propagated to all of the + // open buffers, it is safe to actually apply the replacements it has calculated. + // See https://devdiv.visualstudio.com/DevDiv/_workitems?_a=edit&id=227513 + QueueApplyReplacements(); + + asyncToken.Dispose(); + }; + + // Start the conflict resolution task but do not apply the results immediately. The + // buffer changes performed in propagateEditAction can cause source control modal + // dialogs to show. Those dialogs pump, and yield the UI thread to whatever work is + // waiting to be done there, including our ApplyReplacements work. If ApplyReplacements + // starts running on the UI thread while propagateEditAction is still updating buffers + // on the UI thread, we crash because we try to enumerate the undo stack while an undo + // transaction is still in process. Therefore, we defer QueueApplyReplacements until + // after the buffers have been edited, and any modal dialogs have been completed. + // In addition to avoiding the crash, this also ensures that the resolved conflict text + // is applied after the simple text change is propagated. + // See https://devdiv.visualstudio.com/DevDiv/_workitems?_a=edit&id=227513 + UpdateConflictResolutionTask(); + + if (propagateEditImmediately) { - _threadingContext.ThrowIfNotOnUIThread(); - VerifyNotDismissed(); - this.ReplacementText = _renameInfo.GetFinalSymbolName(replacementText); + propagateEditAction(); + } + else + { + // When responding to a text edit, we delay propagating the edit until the first transaction completes. + _threadingContext.JoinableTaskFactory.RunAsync(async () => + { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true); + propagateEditAction(); + }); + } + } - var asyncToken = _asyncListener.BeginAsyncOperation(nameof(ApplyReplacementText)); + private void UpdateConflictResolutionTask() + { + _threadingContext.ThrowIfNotOnUIThread(); - Action propagateEditAction = delegate - { - _threadingContext.ThrowIfNotOnUIThread(); + _conflictResolutionTaskCancellationSource.Cancel(); + _conflictResolutionTaskCancellationSource = new CancellationTokenSource(); - if (_dismissed) - { - asyncToken.Dispose(); - return; - } + // If the replacement text is empty, we do not update the results of the conflict + // resolution task. We instead wait for a non-empty identifier. + if (this.ReplacementText == string.Empty) + { + return; + } - _isApplyingEdit = true; - using (Logger.LogBlock(FunctionId.Rename_ApplyReplacementText, replacementText, _cancellationTokenSource.Token)) - { - foreach (var openBuffer in _openTextBuffers.Values) - { - openBuffer.ApplyReplacementText(updateSelection); - } - } + var replacementText = this.ReplacementText; + var options = _options; + var cancellationToken = _conflictResolutionTaskCancellationSource.Token; - _isApplyingEdit = false; + var asyncToken = _asyncListener.BeginAsyncOperation(nameof(UpdateConflictResolutionTask)); - // We already kicked off UpdateConflictResolutionTask below (outside the delegate). - // Now that we are certain the replacement text has been propagated to all of the - // open buffers, it is safe to actually apply the replacements it has calculated. - // See https://devdiv.visualstudio.com/DevDiv/_workitems?_a=edit&id=227513 - QueueApplyReplacements(); + _conflictResolutionTask = _threadingContext.JoinableTaskFactory.RunAsync(async () => + { + // Join prior work before proceeding, since it performs a required state update. + // https://github.com/dotnet/roslyn/pull/34254#discussion_r267024593 + // + // If cancellation of the conflict resolution task is requested before the rename locations task + // completes, we do not need to wait for rename before cancelling. The next conflict resolution task + // will wait on the latest rename location task if/when necessary. + var result = await _allRenameLocationsTask.JoinAsync(cancellationToken).ConfigureAwait(false); + await TaskScheduler.Default; - asyncToken.Dispose(); - }; - - // Start the conflict resolution task but do not apply the results immediately. The - // buffer changes performed in propagateEditAction can cause source control modal - // dialogs to show. Those dialogs pump, and yield the UI thread to whatever work is - // waiting to be done there, including our ApplyReplacements work. If ApplyReplacements - // starts running on the UI thread while propagateEditAction is still updating buffers - // on the UI thread, we crash because we try to enumerate the undo stack while an undo - // transaction is still in process. Therefore, we defer QueueApplyReplacements until - // after the buffers have been edited, and any modal dialogs have been completed. - // In addition to avoiding the crash, this also ensures that the resolved conflict text - // is applied after the simple text change is propagated. - // See https://devdiv.visualstudio.com/DevDiv/_workitems?_a=edit&id=227513 - UpdateConflictResolutionTask(); + return await result.GetReplacementsAsync(replacementText, options, cancellationToken).ConfigureAwait(false); + }); - if (propagateEditImmediately) - { - propagateEditAction(); - } - else - { - // When responding to a text edit, we delay propagating the edit until the first transaction completes. - _threadingContext.JoinableTaskFactory.RunAsync(async () => - { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true); - propagateEditAction(); - }); - } - } + _conflictResolutionTask.Task.CompletesAsyncOperation(asyncToken); + } - private void UpdateConflictResolutionTask() + [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "False positive in methods using JTF: https://github.com/dotnet/roslyn-analyzers/issues/4283")] + private void QueueApplyReplacements() + { + // If the replacement text is empty, we do not update the results of the conflict + // resolution task. We instead wait for a non-empty identifier. + if (this.ReplacementText == string.Empty) { - _threadingContext.ThrowIfNotOnUIThread(); - - _conflictResolutionTaskCancellationSource.Cancel(); - _conflictResolutionTaskCancellationSource = new CancellationTokenSource(); + return; + } - // If the replacement text is empty, we do not update the results of the conflict - // resolution task. We instead wait for a non-empty identifier. - if (this.ReplacementText == string.Empty) + var cancellationToken = _conflictResolutionTaskCancellationSource.Token; + var asyncToken = _asyncListener.BeginAsyncOperation(nameof(QueueApplyReplacements)); + var replacementOperation = _threadingContext.JoinableTaskFactory.RunAsync(async () => + { + var replacementInfo = await _conflictResolutionTask.JoinAsync(CancellationToken.None).ConfigureAwait(false); + if (replacementInfo == null || cancellationToken.IsCancellationRequested) { return; } - var replacementText = this.ReplacementText; - var options = _options; - var cancellationToken = _conflictResolutionTaskCancellationSource.Token; + // Switch to a background thread for expensive work + await TaskScheduler.Default; + var computedMergeResult = await ComputeMergeResultAsync(replacementInfo, cancellationToken); + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken); + ApplyReplacements(computedMergeResult.replacementInfo, computedMergeResult.mergeResult, cancellationToken); + }); + replacementOperation.Task.CompletesAsyncOperation(asyncToken); + } - var asyncToken = _asyncListener.BeginAsyncOperation(nameof(UpdateConflictResolutionTask)); + private async Task<(IInlineRenameReplacementInfo replacementInfo, LinkedFileMergeSessionResult mergeResult)> ComputeMergeResultAsync(IInlineRenameReplacementInfo replacementInfo, CancellationToken cancellationToken) + { + var diffMergingSession = new LinkedFileDiffMergingSession(_baseSolution, replacementInfo.NewSolution, replacementInfo.NewSolution.GetChanges(_baseSolution)); + var mergeResult = await diffMergingSession.MergeDiffsAsync(mergeConflictHandler: null, cancellationToken: cancellationToken).ConfigureAwait(false); + return (replacementInfo, mergeResult); + } - _conflictResolutionTask = _threadingContext.JoinableTaskFactory.RunAsync(async () => - { - // Join prior work before proceeding, since it performs a required state update. - // https://github.com/dotnet/roslyn/pull/34254#discussion_r267024593 - // - // If cancellation of the conflict resolution task is requested before the rename locations task - // completes, we do not need to wait for rename before cancelling. The next conflict resolution task - // will wait on the latest rename location task if/when necessary. - var result = await _allRenameLocationsTask.JoinAsync(cancellationToken).ConfigureAwait(false); - await TaskScheduler.Default; - - return await result.GetReplacementsAsync(replacementText, options, cancellationToken).ConfigureAwait(false); - }); + private void ApplyReplacements(IInlineRenameReplacementInfo replacementInfo, LinkedFileMergeSessionResult mergeResult, CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnUIThread(); + cancellationToken.ThrowIfCancellationRequested(); - _conflictResolutionTask.Task.CompletesAsyncOperation(asyncToken); - } + RaiseReplacementsComputed(replacementInfo); - [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "False positive in methods using JTF: https://github.com/dotnet/roslyn-analyzers/issues/4283")] - private void QueueApplyReplacements() + _isApplyingEdit = true; + foreach (var textBuffer in _openTextBuffers.Keys) { - // If the replacement text is empty, we do not update the results of the conflict - // resolution task. We instead wait for a non-empty identifier. - if (this.ReplacementText == string.Empty) + var documents = textBuffer.CurrentSnapshot.GetRelatedDocumentsWithChanges(); + if (documents.Any()) { - return; + var textBufferManager = _openTextBuffers[textBuffer]; + textBufferManager.ApplyConflictResolutionEdits(replacementInfo, mergeResult, documents, cancellationToken); } + } - var cancellationToken = _conflictResolutionTaskCancellationSource.Token; - var asyncToken = _asyncListener.BeginAsyncOperation(nameof(QueueApplyReplacements)); - var replacementOperation = _threadingContext.JoinableTaskFactory.RunAsync(async () => - { - var replacementInfo = await _conflictResolutionTask.JoinAsync(CancellationToken.None).ConfigureAwait(false); - if (replacementInfo == null || cancellationToken.IsCancellationRequested) - { - return; - } + _isApplyingEdit = false; + } - // Switch to a background thread for expensive work - await TaskScheduler.Default; - var computedMergeResult = await ComputeMergeResultAsync(replacementInfo, cancellationToken); - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken); - ApplyReplacements(computedMergeResult.replacementInfo, computedMergeResult.mergeResult, cancellationToken); - }); - replacementOperation.Task.CompletesAsyncOperation(asyncToken); - } + private void RaiseReplacementsComputed(IInlineRenameReplacementInfo resolution) + { + _threadingContext.ThrowIfNotOnUIThread(); + ReplacementsComputed?.Invoke(this, resolution); + } - private async Task<(IInlineRenameReplacementInfo replacementInfo, LinkedFileMergeSessionResult mergeResult)> ComputeMergeResultAsync(IInlineRenameReplacementInfo replacementInfo, CancellationToken cancellationToken) + private void LogRenameSession(RenameLogMessage.UserActionOutcome outcome, bool previewChanges) + { + if (_conflictResolutionTask == null) { - var diffMergingSession = new LinkedFileDiffMergingSession(_baseSolution, replacementInfo.NewSolution, replacementInfo.NewSolution.GetChanges(_baseSolution)); - var mergeResult = await diffMergingSession.MergeDiffsAsync(mergeConflictHandler: null, cancellationToken: cancellationToken).ConfigureAwait(false); - return (replacementInfo, mergeResult); + return; } - private void ApplyReplacements(IInlineRenameReplacementInfo replacementInfo, LinkedFileMergeSessionResult mergeResult, CancellationToken cancellationToken) + var conflictResolutionFinishedComputing = _conflictResolutionTask.Task.Status == TaskStatus.RanToCompletion; + + if (conflictResolutionFinishedComputing) { - _threadingContext.ThrowIfNotOnUIThread(); - cancellationToken.ThrowIfCancellationRequested(); + var result = _conflictResolutionTask.Task.Result; + var replacementKinds = result.GetAllReplacementKinds().ToList(); + + Logger.Log(FunctionId.Rename_InlineSession_Session, RenameLogMessage.Create( + _options, + outcome, + conflictResolutionFinishedComputing, + previewChanges, + replacementKinds)); + } + else + { + Debug.Assert(outcome.HasFlag(RenameLogMessage.UserActionOutcome.Canceled)); + Logger.Log(FunctionId.Rename_InlineSession_Session, RenameLogMessage.Create( + _options, + outcome, + conflictResolutionFinishedComputing, + previewChanges, + SpecializedCollections.EmptyList())); + } + } - RaiseReplacementsComputed(replacementInfo); + public void Cancel() + { + _threadingContext.ThrowIfNotOnUIThread(); - _isApplyingEdit = true; - foreach (var textBuffer in _openTextBuffers.Keys) - { - var documents = textBuffer.CurrentSnapshot.GetRelatedDocumentsWithChanges(); - if (documents.Any()) - { - var textBufferManager = _openTextBuffers[textBuffer]; - textBufferManager.ApplyConflictResolutionEdits(replacementInfo, mergeResult, documents, cancellationToken); - } - } + // This wait is safe. We are not passing the async callback to DismissUIAndRollbackEditsAndEndRenameSessionAsync. + // So everything in that method will happen synchronously. + DismissUIAndRollbackEditsAndEndRenameSessionAsync( + RenameLogMessage.UserActionOutcome.Canceled, previewChanges: false).Wait(); + } - _isApplyingEdit = false; - } + private async Task DismissUIAndRollbackEditsAndEndRenameSessionAsync( + RenameLogMessage.UserActionOutcome outcome, + bool previewChanges, + Func finalCommitAction = null) + { + // Note: this entire sequence of steps is not cancellable. We must perform it all to get back to a correct + // state for all the editors the user is interacting with. + var cancellationToken = CancellationToken.None; + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - private void RaiseReplacementsComputed(IInlineRenameReplacementInfo resolution) + if (_dismissed) { - _threadingContext.ThrowIfNotOnUIThread(); - ReplacementsComputed?.Invoke(this, resolution); + return; } - private void LogRenameSession(RenameLogMessage.UserActionOutcome outcome, bool previewChanges) - { - if (_conflictResolutionTask == null) - { - return; - } + _dismissed = true; - var conflictResolutionFinishedComputing = _conflictResolutionTask.Task.Status == TaskStatus.RanToCompletion; + // Remove all our adornments and restore all buffer texts to their initial state. + DismissUIAndRollbackEdits(); - if (conflictResolutionFinishedComputing) - { - var result = _conflictResolutionTask.Task.Result; - var replacementKinds = result.GetAllReplacementKinds().ToList(); - - Logger.Log(FunctionId.Rename_InlineSession_Session, RenameLogMessage.Create( - _options, - outcome, - conflictResolutionFinishedComputing, - previewChanges, - replacementKinds)); - } - else - { - Debug.Assert(outcome.HasFlag(RenameLogMessage.UserActionOutcome.Canceled)); - Logger.Log(FunctionId.Rename_InlineSession_Session, RenameLogMessage.Create( - _options, - outcome, - conflictResolutionFinishedComputing, - previewChanges, - SpecializedCollections.EmptyList())); - } - } + // We're about to perform the final commit action. No need to do any of our BG work to find-refs or compute conflicts. + _cancellationTokenSource.Cancel(); + _conflictResolutionTaskCancellationSource.Cancel(); - public void Cancel() - { - _threadingContext.ThrowIfNotOnUIThread(); + // Close the keep alive session we have open with OOP, allowing it to release the solution it is holding onto. + _keepAliveSession.Dispose(); - // This wait is safe. We are not passing the async callback to DismissUIAndRollbackEditsAndEndRenameSessionAsync. - // So everything in that method will happen synchronously. - DismissUIAndRollbackEditsAndEndRenameSessionAsync( - RenameLogMessage.UserActionOutcome.Canceled, previewChanges: false).Wait(); + // Perform the actual commit step if we've been asked to. + if (finalCommitAction != null) + { + // ConfigureAwait(true) so we come back to the UI thread to finish work. + await finalCommitAction().ConfigureAwait(true); } - private async Task DismissUIAndRollbackEditsAndEndRenameSessionAsync( - RenameLogMessage.UserActionOutcome outcome, - bool previewChanges, - Func finalCommitAction = null) - { - // Note: this entire sequence of steps is not cancellable. We must perform it all to get back to a correct - // state for all the editors the user is interacting with. - var cancellationToken = CancellationToken.None; - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + // Log the result so we know how well rename is going in practice. + LogRenameSession(outcome, previewChanges); - if (_dismissed) - { - return; - } + // Remove all our rename trackers from the text buffer properties. + RenameTrackingDismisser.DismissRenameTracking(_workspace, _workspace.GetOpenDocumentIds()); - _dismissed = true; + // Log how long the full rename took. + _inlineRenameSessionDurationLogBlock.Dispose(); - // Remove all our adornments and restore all buffer texts to their initial state. - DismissUIAndRollbackEdits(); + return; - // We're about to perform the final commit action. No need to do any of our BG work to find-refs or compute conflicts. - _cancellationTokenSource.Cancel(); - _conflictResolutionTaskCancellationSource.Cancel(); + void DismissUIAndRollbackEdits() + { + _workspace.WorkspaceChanged -= OnWorkspaceChanged; + _textBufferAssociatedViewService.SubjectBuffersConnected -= OnSubjectBuffersConnected; - // Close the keep alive session we have open with OOP, allowing it to release the solution it is holding onto. - _keepAliveSession.Dispose(); + // Reenable completion now that the inline rename session is done + _completionDisabledToken.Dispose(); - // Perform the actual commit step if we've been asked to. - if (finalCommitAction != null) + foreach (var textBuffer in _openTextBuffers.Keys) { - // ConfigureAwait(true) so we come back to the UI thread to finish work. - await finalCommitAction().ConfigureAwait(true); + var document = textBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + var isClosed = document == null; + + var openBuffer = _openTextBuffers[textBuffer]; + openBuffer.DisconnectAndRollbackEdits(isClosed); } - // Log the result so we know how well rename is going in practice. - LogRenameSession(outcome, previewChanges); + this.UndoManager.Disconnect(); - // Remove all our rename trackers from the text buffer properties. - RenameTrackingDismisser.DismissRenameTracking(_workspace, _workspace.GetOpenDocumentIds()); + if (_triggerView != null && !_triggerView.IsClosed) + { + _triggerView.Selection.Clear(); + } - // Log how long the full rename took. - _inlineRenameSessionDurationLogBlock.Dispose(); + RenameService.ActiveSession = null; + } + } - return; + public void Commit(bool previewChanges = false) + => CommitWorker(previewChanges); - void DismissUIAndRollbackEdits() - { - _workspace.WorkspaceChanged -= OnWorkspaceChanged; - _textBufferAssociatedViewService.SubjectBuffersConnected -= OnSubjectBuffersConnected; + /// if the rename operation was committed, otherwise + private bool CommitWorker(bool previewChanges) + { + // We're going to synchronously block the UI thread here. So we can't use the background work indicator (as + // it needs the UI thread to update itself. This will force us to go through the Threaded-Wait-Dialog path + // which at least will allow the user to cancel the rename if they want. + // + // In the future we should remove this entrypoint and have all callers use CommitAsync instead. + return _threadingContext.JoinableTaskFactory.Run(() => CommitWorkerAsync(previewChanges, canUseBackgroundWorkIndicator: false, CancellationToken.None)); + } - // Reenable completion now that the inline rename session is done - _completionDisabledToken.Dispose(); + public Task CommitAsync(bool previewChanges, CancellationToken cancellationToken) + => CommitWorkerAsync(previewChanges, canUseBackgroundWorkIndicator: true, cancellationToken); - foreach (var textBuffer in _openTextBuffers.Keys) - { - var document = textBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - var isClosed = document == null; + /// if the rename operation was commited, otherwise + private async Task CommitWorkerAsync(bool previewChanges, bool canUseBackgroundWorkIndicator, CancellationToken cancellationToken) + { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + VerifyNotDismissed(); + + // If the identifier was deleted (or didn't change at all) then cancel the operation. + // Note: an alternative approach would be for the work we're doing (like detecting + // conflicts) to quickly bail in the case of no change. However, that involves deeper + // changes to the system and is less easy to validate that nothing happens. + // + // The only potential downside here would be if there was a language that wanted to + // still 'rename' even if the identifier went away (or was unchanged). But that isn't + // a case we're aware of, so it's fine to be opinionated here that we can quickly bail + // in these cases. + if (this.ReplacementText == string.Empty || + this.ReplacementText == _initialRenameText) + { + Cancel(); + return false; + } - var openBuffer = _openTextBuffers[textBuffer]; - openBuffer.DisconnectAndRollbackEdits(isClosed); - } + previewChanges = previewChanges || _previewChanges; - this.UndoManager.Disconnect(); + try + { + if (canUseBackgroundWorkIndicator && this.RenameService.GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameAsynchronously)) + { + // We do not cancel on edit because as part of the rename system we have asynchronous work still + // occurring that itself may be asynchronously editing the buffer (for example, updating reference + // locations with the final renamed text). Ideally though, once we start comitting, we would cancel + // any of that work and then only have the work of rolling back to the original state of the world + // and applying the desired edits ourselves. + var factory = _workspace.Services.GetRequiredService(); + using var context = factory.Create( + _triggerView, TriggerSpan, EditorFeaturesResources.Computing_Rename_information, + cancelOnEdit: false, cancelOnFocusLost: false); - if (_triggerView != null && !_triggerView.IsClosed) - { - _triggerView.Selection.Clear(); - } + await CommitCoreAsync(context, previewChanges).ConfigureAwait(true); + } + else + { + using var context = _uiThreadOperationExecutor.BeginExecute( + title: EditorFeaturesResources.Rename, + defaultDescription: EditorFeaturesResources.Computing_Rename_information, + allowCancellation: true, + showProgress: false); - RenameService.ActiveSession = null; + // .ConfigureAwait(true); so we can return to the UI thread to dispose the operation context. It + // has a non-JTF threading dependency on the main thread. So it can deadlock if you call it on a BG + // thread when in a blocking JTF call. + await CommitCoreAsync(context, previewChanges).ConfigureAwait(true); } } - - public void Commit(bool previewChanges = false) - => CommitWorker(previewChanges); - - /// if the rename operation was committed, otherwise - private bool CommitWorker(bool previewChanges) + catch (OperationCanceledException) { - // We're going to synchronously block the UI thread here. So we can't use the background work indicator (as - // it needs the UI thread to update itself. This will force us to go through the Threaded-Wait-Dialog path - // which at least will allow the user to cancel the rename if they want. - // - // In the future we should remove this entrypoint and have all callers use CommitAsync instead. - return _threadingContext.JoinableTaskFactory.Run(() => CommitWorkerAsync(previewChanges, canUseBackgroundWorkIndicator: false, CancellationToken.None)); + await DismissUIAndRollbackEditsAndEndRenameSessionAsync( + RenameLogMessage.UserActionOutcome.Canceled | RenameLogMessage.UserActionOutcome.Committed, previewChanges).ConfigureAwait(false); + return false; } - public Task CommitAsync(bool previewChanges, CancellationToken cancellationToken) - => CommitWorkerAsync(previewChanges, canUseBackgroundWorkIndicator: true, cancellationToken); + return true; + } - /// if the rename operation was commited, otherwise - private async Task CommitWorkerAsync(bool previewChanges, bool canUseBackgroundWorkIndicator, CancellationToken cancellationToken) + private async Task CommitCoreAsync(IUIThreadOperationContext operationContext, bool previewChanges) + { + var cancellationToken = operationContext.UserCancellationToken; + var eventName = previewChanges ? FunctionId.Rename_CommitCoreWithPreview : FunctionId.Rename_CommitCore; + using (Logger.LogBlock(eventName, KeyValueLogMessage.Create(LogType.UserAction), cancellationToken)) { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - VerifyNotDismissed(); + var info = await _conflictResolutionTask.JoinAsync(cancellationToken).ConfigureAwait(true); + var newSolution = info.NewSolution; - // If the identifier was deleted (or didn't change at all) then cancel the operation. - // Note: an alternative approach would be for the work we're doing (like detecting - // conflicts) to quickly bail in the case of no change. However, that involves deeper - // changes to the system and is less easy to validate that nothing happens. - // - // The only potential downside here would be if there was a language that wanted to - // still 'rename' even if the identifier went away (or was unchanged). But that isn't - // a case we're aware of, so it's fine to be opinionated here that we can quickly bail - // in these cases. - if (this.ReplacementText == string.Empty || - this.ReplacementText == _initialRenameText) + if (previewChanges) { - Cancel(); - return false; - } + var previewService = _workspace.Services.GetService(); - previewChanges = previewChanges || _previewChanges; + // The preview service needs to be called from the UI thread, since it's doing COM calls underneath. + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + newSolution = previewService.PreviewChanges( + string.Format(EditorFeaturesResources.Preview_Changes_0, EditorFeaturesResources.Rename), + "vs.csharp.refactoring.rename", + string.Format(EditorFeaturesResources.Rename_0_to_1_colon, this.OriginalSymbolName, this.ReplacementText), + _renameInfo.FullDisplayName, + _renameInfo.Glyph, + newSolution, + _triggerDocument.Project.Solution); - try - { - if (canUseBackgroundWorkIndicator && this.RenameService.GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameAsynchronously)) - { - // We do not cancel on edit because as part of the rename system we have asynchronous work still - // occurring that itself may be asynchronously editing the buffer (for example, updating reference - // locations with the final renamed text). Ideally though, once we start comitting, we would cancel - // any of that work and then only have the work of rolling back to the original state of the world - // and applying the desired edits ourselves. - var factory = _workspace.Services.GetRequiredService(); - using var context = factory.Create( - _triggerView, TriggerSpan, EditorFeaturesResources.Computing_Rename_information, - cancelOnEdit: false, cancelOnFocusLost: false); - - await CommitCoreAsync(context, previewChanges).ConfigureAwait(true); - } - else + if (newSolution == null) { - using var context = _uiThreadOperationExecutor.BeginExecute( - title: EditorFeaturesResources.Rename, - defaultDescription: EditorFeaturesResources.Computing_Rename_information, - allowCancellation: true, - showProgress: false); - - // .ConfigureAwait(true); so we can return to the UI thread to dispose the operation context. It - // has a non-JTF threading dependency on the main thread. So it can deadlock if you call it on a BG - // thread when in a blocking JTF call. - await CommitCoreAsync(context, previewChanges).ConfigureAwait(true); + // User clicked cancel. + return; } } - catch (OperationCanceledException) - { - await DismissUIAndRollbackEditsAndEndRenameSessionAsync( - RenameLogMessage.UserActionOutcome.Canceled | RenameLogMessage.UserActionOutcome.Committed, previewChanges).ConfigureAwait(false); - return false; - } - - return true; - } - private async Task CommitCoreAsync(IUIThreadOperationContext operationContext, bool previewChanges) - { - var cancellationToken = operationContext.UserCancellationToken; - var eventName = previewChanges ? FunctionId.Rename_CommitCoreWithPreview : FunctionId.Rename_CommitCore; - using (Logger.LogBlock(eventName, KeyValueLogMessage.Create(LogType.UserAction), cancellationToken)) - { - var info = await _conflictResolutionTask.JoinAsync(cancellationToken).ConfigureAwait(true); - var newSolution = info.NewSolution; + // The user hasn't canceled by now, so we're done waiting for them. Off to rename! + using var _ = operationContext.AddScope(allowCancellation: false, EditorFeaturesResources.Updating_files); - if (previewChanges) + await DismissUIAndRollbackEditsAndEndRenameSessionAsync( + RenameLogMessage.UserActionOutcome.Committed, previewChanges, + async () => { - var previewService = _workspace.Services.GetService(); - - // The preview service needs to be called from the UI thread, since it's doing COM calls underneath. - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - newSolution = previewService.PreviewChanges( - string.Format(EditorFeaturesResources.Preview_Changes_0, EditorFeaturesResources.Rename), - "vs.csharp.refactoring.rename", - string.Format(EditorFeaturesResources.Rename_0_to_1_colon, this.OriginalSymbolName, this.ReplacementText), - _renameInfo.FullDisplayName, - _renameInfo.Glyph, - newSolution, - _triggerDocument.Project.Solution); - - if (newSolution == null) - { - // User clicked cancel. - return; - } - } - - // The user hasn't canceled by now, so we're done waiting for them. Off to rename! - using var _ = operationContext.AddScope(allowCancellation: false, EditorFeaturesResources.Updating_files); + var error = await TryApplyRenameAsync(newSolution, cancellationToken).ConfigureAwait(false); - await DismissUIAndRollbackEditsAndEndRenameSessionAsync( - RenameLogMessage.UserActionOutcome.Committed, previewChanges, - async () => + if (error is not null) { - var error = await TryApplyRenameAsync(newSolution, cancellationToken).ConfigureAwait(false); - - if (error is not null) - { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var notificationService = _workspace.Services.GetService(); - notificationService.SendNotification( - error.Value.message, EditorFeaturesResources.Rename_Symbol, error.Value.severity); - } - }).ConfigureAwait(false); - } + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var notificationService = _workspace.Services.GetService(); + notificationService.SendNotification( + error.Value.message, EditorFeaturesResources.Rename_Symbol, error.Value.severity); + } + }).ConfigureAwait(false); } + } - /// - /// Returns non-null error message if renaming fails. - /// - private async Task<(NotificationSeverity severity, string message)?> TryApplyRenameAsync( - Solution newSolution, CancellationToken cancellationToken) - { - var changes = _baseSolution.GetChanges(newSolution); - var changedDocumentIDs = changes.GetProjectChanges().SelectMany(c => c.GetChangedDocuments()).ToList(); + /// + /// Returns non-null error message if renaming fails. + /// + private async Task<(NotificationSeverity severity, string message)?> TryApplyRenameAsync( + Solution newSolution, CancellationToken cancellationToken) + { + var changes = _baseSolution.GetChanges(newSolution); + var changedDocumentIDs = changes.GetProjectChanges().SelectMany(c => c.GetChangedDocuments()).ToList(); - // Go to the background thread for initial calculation of the final solution - await TaskScheduler.Default; - var finalSolution = CalculateFinalSolutionSynchronously(newSolution, newSolution.Workspace, changedDocumentIDs, cancellationToken); + // Go to the background thread for initial calculation of the final solution + await TaskScheduler.Default; + var finalSolution = CalculateFinalSolutionSynchronously(newSolution, newSolution.Workspace, changedDocumentIDs, cancellationToken); + + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + using var undoTransaction = _workspace.OpenGlobalUndoTransaction(EditorFeaturesResources.Inline_Rename); - using var undoTransaction = _workspace.OpenGlobalUndoTransaction(EditorFeaturesResources.Inline_Rename); + if (!_renameInfo.TryOnBeforeGlobalSymbolRenamed(_workspace, changedDocumentIDs, this.ReplacementText)) + return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid); - if (!_renameInfo.TryOnBeforeGlobalSymbolRenamed(_workspace, changedDocumentIDs, this.ReplacementText)) - return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid); + if (!_workspace.TryApplyChanges(finalSolution)) + { + // If the workspace changed in TryOnBeforeGlobalSymbolRenamed retry, this prevents rename from failing for cases + // where text changes to other files or workspace state change doesn't impact the text changes being applied. + Logger.Log(FunctionId.Rename_TryApplyRename_WorkspaceChanged, message: null, LogLevel.Information); + finalSolution = CalculateFinalSolutionSynchronously(newSolution, _workspace, changedDocumentIDs, cancellationToken); if (!_workspace.TryApplyChanges(finalSolution)) - { - // If the workspace changed in TryOnBeforeGlobalSymbolRenamed retry, this prevents rename from failing for cases - // where text changes to other files or workspace state change doesn't impact the text changes being applied. - Logger.Log(FunctionId.Rename_TryApplyRename_WorkspaceChanged, message: null, LogLevel.Information); - finalSolution = CalculateFinalSolutionSynchronously(newSolution, _workspace, changedDocumentIDs, cancellationToken); + return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace); + } - if (!_workspace.TryApplyChanges(finalSolution)) - return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace); - } + try + { + // Since rename can apply file changes as well, and those file + // changes can generate new document ids, include added documents + // as well as changed documents. This also ensures that any document + // that was removed is not included + var finalChanges = _workspace.CurrentSolution.GetChanges(_baseSolution); - try - { - // Since rename can apply file changes as well, and those file - // changes can generate new document ids, include added documents - // as well as changed documents. This also ensures that any document - // that was removed is not included - var finalChanges = _workspace.CurrentSolution.GetChanges(_baseSolution); + var finalChangedIds = finalChanges + .GetProjectChanges() + .SelectMany(c => c.GetChangedDocuments().Concat(c.GetAddedDocuments())) + .ToList(); - var finalChangedIds = finalChanges - .GetProjectChanges() - .SelectMany(c => c.GetChangedDocuments().Concat(c.GetAddedDocuments())) - .ToList(); + if (!_renameInfo.TryOnAfterGlobalSymbolRenamed(_workspace, finalChangedIds, this.ReplacementText)) + return (NotificationSeverity.Information, EditorFeaturesResources.Rename_operation_was_not_properly_completed_Some_file_might_not_have_been_updated); - if (!_renameInfo.TryOnAfterGlobalSymbolRenamed(_workspace, finalChangedIds, this.ReplacementText)) - return (NotificationSeverity.Information, EditorFeaturesResources.Rename_operation_was_not_properly_completed_Some_file_might_not_have_been_updated); + return null; + } + finally + { + // If we successfully updated the workspace then make sure the undo transaction is committed and is + // always able to undo anything any other external listener did. + undoTransaction.Commit(); + } - return null; - } - finally + static Solution CalculateFinalSolutionSynchronously(Solution newSolution, Workspace workspace, List changedDocumentIDs, CancellationToken cancellationToken) + { + var finalSolution = workspace.CurrentSolution; + foreach (var id in changedDocumentIDs) { - // If we successfully updated the workspace then make sure the undo transaction is committed and is - // always able to undo anything any other external listener did. - undoTransaction.Commit(); - } + // If the document supports syntax tree, then create the new solution from the + // updated syntax root. This should ensure that annotations are preserved, and + // prevents the solution from having to reparse documents when we already have + // the trees for them. If we don't support syntax, then just use the text of + // the document. + var newDocument = newSolution.GetDocument(id); - static Solution CalculateFinalSolutionSynchronously(Solution newSolution, Workspace workspace, List changedDocumentIDs, CancellationToken cancellationToken) - { - var finalSolution = workspace.CurrentSolution; - foreach (var id in changedDocumentIDs) + if (newDocument.SupportsSyntaxTree) { - // If the document supports syntax tree, then create the new solution from the - // updated syntax root. This should ensure that annotations are preserved, and - // prevents the solution from having to reparse documents when we already have - // the trees for them. If we don't support syntax, then just use the text of - // the document. - var newDocument = newSolution.GetDocument(id); - - if (newDocument.SupportsSyntaxTree) - { - var root = newDocument.GetRequiredSyntaxRootSynchronously(cancellationToken); - finalSolution = finalSolution.WithDocumentSyntaxRoot(id, root); - } - else - { - var newText = newDocument.GetTextSynchronously(cancellationToken); - finalSolution = finalSolution.WithDocumentText(id, newText); - } - - // Make sure to include any document rename as well - finalSolution = finalSolution.WithDocumentName(id, newDocument.Name); + var root = newDocument.GetRequiredSyntaxRootSynchronously(cancellationToken); + finalSolution = finalSolution.WithDocumentSyntaxRoot(id, root); + } + else + { + var newText = newDocument.GetTextSynchronously(cancellationToken); + finalSolution = finalSolution.WithDocumentText(id, newText); } - return finalSolution; + // Make sure to include any document rename as well + finalSolution = finalSolution.WithDocumentName(id, newDocument.Name); } + + return finalSolution; } + } - internal bool TryGetContainingEditableSpan(SnapshotPoint point, out SnapshotSpan editableSpan) + internal bool TryGetContainingEditableSpan(SnapshotPoint point, out SnapshotSpan editableSpan) + { + editableSpan = default; + if (!_openTextBuffers.TryGetValue(point.Snapshot.TextBuffer, out var bufferManager)) { - editableSpan = default; - if (!_openTextBuffers.TryGetValue(point.Snapshot.TextBuffer, out var bufferManager)) - { - return false; - } + return false; + } - foreach (var span in bufferManager.GetEditableSpansForSnapshot(point.Snapshot)) + foreach (var span in bufferManager.GetEditableSpansForSnapshot(point.Snapshot)) + { + if (span.Contains(point) || span.End == point) { - if (span.Contains(point) || span.End == point) - { - editableSpan = span; - return true; - } + editableSpan = span; + return true; } - - return false; } - internal bool IsInOpenTextBuffer(SnapshotPoint point) - => _openTextBuffers.ContainsKey(point.Snapshot.TextBuffer); + return false; + } - internal TestAccessor GetTestAccessor() - => new TestAccessor(this); + internal bool IsInOpenTextBuffer(SnapshotPoint point) + => _openTextBuffers.ContainsKey(point.Snapshot.TextBuffer); - public readonly struct TestAccessor(InlineRenameSession inlineRenameSession) - { - private readonly InlineRenameSession _inlineRenameSession = inlineRenameSession; + internal TestAccessor GetTestAccessor() + => new TestAccessor(this); - public bool CommitWorker(bool previewChanges) - => _inlineRenameSession.CommitWorker(previewChanges); - } + public readonly struct TestAccessor(InlineRenameSession inlineRenameSession) + { + private readonly InlineRenameSession _inlineRenameSession = inlineRenameSession; + + public bool CommitWorker(bool previewChanges) + => _inlineRenameSession.CommitWorker(previewChanges); } } diff --git a/src/EditorFeatures/Core/InlineRename/InlineRenameSessionOptionsStorage.cs b/src/EditorFeatures/Core/InlineRename/InlineRenameSessionOptionsStorage.cs index 0f8b0efe714cf..7655b058f080c 100644 --- a/src/EditorFeatures/Core/InlineRename/InlineRenameSessionOptionsStorage.cs +++ b/src/EditorFeatures/Core/InlineRename/InlineRenameSessionOptionsStorage.cs @@ -4,15 +4,14 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.InlineRename +namespace Microsoft.CodeAnalysis.InlineRename; + +internal static class InlineRenameSessionOptionsStorage { - internal static class InlineRenameSessionOptionsStorage - { - public static readonly Option2 RenameOverloads = new("dotnet_rename_overloads", defaultValue: false); - public static readonly Option2 RenameInStrings = new("dotnet_rename_in_strings", defaultValue: false); - public static readonly Option2 RenameInComments = new("dotnet_rename_in_comments", defaultValue: false); - public static readonly Option2 RenameFile = new("dotnet_rename_file", defaultValue: true); - public static readonly Option2 PreviewChanges = new("dotnet_preview_inline_rename_changes", defaultValue: false); - public static readonly Option2 RenameAsynchronously = new("dotnet_rename_asynchronously", defaultValue: true); - } + public static readonly Option2 RenameOverloads = new("dotnet_rename_overloads", defaultValue: false); + public static readonly Option2 RenameInStrings = new("dotnet_rename_in_strings", defaultValue: false); + public static readonly Option2 RenameInComments = new("dotnet_rename_in_comments", defaultValue: false); + public static readonly Option2 RenameFile = new("dotnet_rename_file", defaultValue: true); + public static readonly Option2 PreviewChanges = new("dotnet_preview_inline_rename_changes", defaultValue: false); + public static readonly Option2 RenameAsynchronously = new("dotnet_rename_asynchronously", defaultValue: true); } diff --git a/src/EditorFeatures/Core/InlineRename/InlineRenameUIOptionsStorage.cs b/src/EditorFeatures/Core/InlineRename/InlineRenameUIOptionsStorage.cs index 6be37719ac353..14629a946f40b 100644 --- a/src/EditorFeatures/Core/InlineRename/InlineRenameUIOptionsStorage.cs +++ b/src/EditorFeatures/Core/InlineRename/InlineRenameUIOptionsStorage.cs @@ -4,11 +4,10 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.InlineRename +namespace Microsoft.CodeAnalysis.Editor.InlineRename; + +internal sealed class InlineRenameUIOptionsStorage { - internal sealed class InlineRenameUIOptionsStorage - { - public static readonly Option2 UseInlineAdornment = new("dotnet_rename_use_inline_adornment", defaultValue: true); - public static readonly Option2 CollapseUI = new("dotnet_collapse_inline_rename_ui", defaultValue: false); - } + public static readonly Option2 UseInlineAdornment = new("dotnet_rename_use_inline_adornment", defaultValue: true); + public static readonly Option2 CollapseUI = new("dotnet_collapse_inline_rename_ui", defaultValue: false); } diff --git a/src/EditorFeatures/Core/InlineRename/RenameLogMessage.cs b/src/EditorFeatures/Core/InlineRename/RenameLogMessage.cs index ef0c8972f5a82..1db109986767a 100644 --- a/src/EditorFeatures/Core/InlineRename/RenameLogMessage.cs +++ b/src/EditorFeatures/Core/InlineRename/RenameLogMessage.cs @@ -9,56 +9,55 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Rename; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal static class RenameLogMessage { - internal static class RenameLogMessage + private const string RenameInComments = nameof(RenameInComments); + private const string RenameInStrings = nameof(RenameInStrings); + private const string RenameOverloads = nameof(RenameOverloads); + private const string RenameFile = nameof(RenameFile); + + private const string Committed = nameof(Committed); + private const string Canceled = nameof(Canceled); + + private const string ConflictResolutionFinishedComputing = nameof(ConflictResolutionFinishedComputing); + private const string PreviewChanges = nameof(PreviewChanges); + + private const string RenamedIdentifiersWithoutConflicts = nameof(RenamedIdentifiersWithoutConflicts); + private const string ResolvableReferenceConflicts = nameof(ResolvableReferenceConflicts); + private const string ResolvableNonReferenceConflicts = nameof(ResolvableNonReferenceConflicts); + private const string UnresolvableConflicts = nameof(UnresolvableConflicts); + + public static KeyValueLogMessage Create( + SymbolRenameOptions options, UserActionOutcome outcome, + bool conflictResolutionFinishedComputing, bool previewChanges, + IList replacementKinds) { - private const string RenameInComments = nameof(RenameInComments); - private const string RenameInStrings = nameof(RenameInStrings); - private const string RenameOverloads = nameof(RenameOverloads); - private const string RenameFile = nameof(RenameFile); - - private const string Committed = nameof(Committed); - private const string Canceled = nameof(Canceled); - - private const string ConflictResolutionFinishedComputing = nameof(ConflictResolutionFinishedComputing); - private const string PreviewChanges = nameof(PreviewChanges); - - private const string RenamedIdentifiersWithoutConflicts = nameof(RenamedIdentifiersWithoutConflicts); - private const string ResolvableReferenceConflicts = nameof(ResolvableReferenceConflicts); - private const string ResolvableNonReferenceConflicts = nameof(ResolvableNonReferenceConflicts); - private const string UnresolvableConflicts = nameof(UnresolvableConflicts); - - public static KeyValueLogMessage Create( - SymbolRenameOptions options, UserActionOutcome outcome, - bool conflictResolutionFinishedComputing, bool previewChanges, - IList replacementKinds) + return KeyValueLogMessage.Create(LogType.UserAction, m => { - return KeyValueLogMessage.Create(LogType.UserAction, m => - { - m[RenameInComments] = options.RenameInComments; - m[RenameInStrings] = options.RenameInStrings; - m[RenameOverloads] = options.RenameOverloads; - m[RenameFile] = options.RenameFile; - - m[Committed] = (outcome & UserActionOutcome.Committed) == UserActionOutcome.Committed; - m[Canceled] = (outcome & UserActionOutcome.Canceled) == UserActionOutcome.Canceled; - - m[ConflictResolutionFinishedComputing] = conflictResolutionFinishedComputing; - m[PreviewChanges] = previewChanges; - - m[RenamedIdentifiersWithoutConflicts] = replacementKinds.Count(r => r == InlineRenameReplacementKind.NoConflict); - m[ResolvableReferenceConflicts] = replacementKinds.Count(r => r == InlineRenameReplacementKind.ResolvedReferenceConflict); - m[ResolvableNonReferenceConflicts] = replacementKinds.Count(r => r == InlineRenameReplacementKind.ResolvedNonReferenceConflict); - m[UnresolvableConflicts] = replacementKinds.Count(r => r == InlineRenameReplacementKind.UnresolvedConflict); - }); - } + m[RenameInComments] = options.RenameInComments; + m[RenameInStrings] = options.RenameInStrings; + m[RenameOverloads] = options.RenameOverloads; + m[RenameFile] = options.RenameFile; + + m[Committed] = (outcome & UserActionOutcome.Committed) == UserActionOutcome.Committed; + m[Canceled] = (outcome & UserActionOutcome.Canceled) == UserActionOutcome.Canceled; + + m[ConflictResolutionFinishedComputing] = conflictResolutionFinishedComputing; + m[PreviewChanges] = previewChanges; + + m[RenamedIdentifiersWithoutConflicts] = replacementKinds.Count(r => r == InlineRenameReplacementKind.NoConflict); + m[ResolvableReferenceConflicts] = replacementKinds.Count(r => r == InlineRenameReplacementKind.ResolvedReferenceConflict); + m[ResolvableNonReferenceConflicts] = replacementKinds.Count(r => r == InlineRenameReplacementKind.ResolvedNonReferenceConflict); + m[UnresolvableConflicts] = replacementKinds.Count(r => r == InlineRenameReplacementKind.UnresolvedConflict); + }); + } - [Flags] - public enum UserActionOutcome - { - Committed = 0x1, - Canceled = 0x2, - } + [Flags] + public enum UserActionOutcome + { + Committed = 0x1, + Canceled = 0x2, } } diff --git a/src/EditorFeatures/Core/InlineRename/RenameTrackingSpan.cs b/src/EditorFeatures/Core/InlineRename/RenameTrackingSpan.cs index a3f04f5d1fda0..658c43d77f1ea 100644 --- a/src/EditorFeatures/Core/InlineRename/RenameTrackingSpan.cs +++ b/src/EditorFeatures/Core/InlineRename/RenameTrackingSpan.cs @@ -4,19 +4,18 @@ using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal readonly struct RenameTrackingSpan(ITrackingSpan trackingSpan, RenameSpanKind type) { - internal readonly struct RenameTrackingSpan(ITrackingSpan trackingSpan, RenameSpanKind type) - { - public readonly ITrackingSpan TrackingSpan = trackingSpan; - public readonly RenameSpanKind Type = type; - } + public readonly ITrackingSpan TrackingSpan = trackingSpan; + public readonly RenameSpanKind Type = type; +} - internal enum RenameSpanKind - { - None, - Reference, - UnresolvedConflict, - Complexified - } +internal enum RenameSpanKind +{ + None, + Reference, + UnresolvedConflict, + Complexified } diff --git a/src/EditorFeatures/Core/InlineRename/Taggers/AbstractRenameTagger.cs b/src/EditorFeatures/Core/InlineRename/Taggers/AbstractRenameTagger.cs index 252d1db8cfc47..29a85fb2a8f65 100644 --- a/src/EditorFeatures/Core/InlineRename/Taggers/AbstractRenameTagger.cs +++ b/src/EditorFeatures/Core/InlineRename/Taggers/AbstractRenameTagger.cs @@ -12,109 +12,108 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal abstract class AbstractRenameTagger : ITagger, IDisposable where T : ITag { - internal abstract class AbstractRenameTagger : ITagger, IDisposable where T : ITag - { - private readonly ITextBuffer _buffer; - private readonly InlineRenameService _renameService; + private readonly ITextBuffer _buffer; + private readonly InlineRenameService _renameService; - private InlineRenameSession.OpenTextBufferManager _bufferManager; - private IEnumerable _currentSpans; + private InlineRenameSession.OpenTextBufferManager _bufferManager; + private IEnumerable _currentSpans; - protected AbstractRenameTagger(ITextBuffer buffer, InlineRenameService renameService) - { - _buffer = buffer; - _renameService = renameService; + protected AbstractRenameTagger(ITextBuffer buffer, InlineRenameService renameService) + { + _buffer = buffer; + _renameService = renameService; - _renameService.ActiveSessionChanged += OnActiveSessionChanged; + _renameService.ActiveSessionChanged += OnActiveSessionChanged; - if (_renameService.ActiveSession != null) - { - AttachToSession(_renameService.ActiveSession); - } + if (_renameService.ActiveSession != null) + { + AttachToSession(_renameService.ActiveSession); } + } - private void OnActiveSessionChanged(object sender, InlineRenameService.ActiveSessionChangedEventArgs e) + private void OnActiveSessionChanged(object sender, InlineRenameService.ActiveSessionChangedEventArgs e) + { + if (e.PreviousSession != null) { - if (e.PreviousSession != null) - { - DetachFromSession(); - } - - if (_renameService.ActiveSession != null) - { - AttachToSession(_renameService.ActiveSession); - } + DetachFromSession(); } - private void AttachToSession(InlineRenameSession session) + if (_renameService.ActiveSession != null) { - if (session.TryGetBufferManager(_buffer, out _bufferManager)) - { - _bufferManager.SpansChanged += OnSpansChanged; - OnSpansChanged(); - } + AttachToSession(_renameService.ActiveSession); } + } - private void DetachFromSession() + private void AttachToSession(InlineRenameSession session) + { + if (session.TryGetBufferManager(_buffer, out _bufferManager)) { - if (_bufferManager != null) - { - RaiseTagsChangedForEntireBuffer(); - - _bufferManager.SpansChanged -= OnSpansChanged; - _bufferManager = null; - _currentSpans = null; - } + _bufferManager.SpansChanged += OnSpansChanged; + OnSpansChanged(); } + } - private void OnSpansChanged() + private void DetachFromSession() + { + if (_bufferManager != null) { - _currentSpans = _bufferManager.GetRenameTrackingSpans(); RaiseTagsChangedForEntireBuffer(); + + _bufferManager.SpansChanged -= OnSpansChanged; + _bufferManager = null; + _currentSpans = null; } + } - private void RaiseTagsChangedForEntireBuffer() - => TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(_buffer.CurrentSnapshot.GetFullSpan())); + private void OnSpansChanged() + { + _currentSpans = _bufferManager.GetRenameTrackingSpans(); + RaiseTagsChangedForEntireBuffer(); + } - public void Dispose() - { - _renameService.ActiveSessionChanged -= OnActiveSessionChanged; + private void RaiseTagsChangedForEntireBuffer() + => TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(_buffer.CurrentSnapshot.GetFullSpan())); - if (_renameService.ActiveSession != null) - { - DetachFromSession(); - } + public void Dispose() + { + _renameService.ActiveSessionChanged -= OnActiveSessionChanged; + + if (_renameService.ActiveSession != null) + { + DetachFromSession(); } + } - public event EventHandler TagsChanged; + public event EventHandler TagsChanged; - public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) + public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) + { + if (_renameService.ActiveSession == null) { - if (_renameService.ActiveSession == null) - { - yield break; - } + yield break; + } - var renameSpans = _currentSpans; - if (renameSpans != null) + var renameSpans = _currentSpans; + if (renameSpans != null) + { + var snapshot = spans.First().Snapshot; + foreach (var renameSpan in renameSpans) { - var snapshot = spans.First().Snapshot; - foreach (var renameSpan in renameSpans) + var span = renameSpan.TrackingSpan.GetSpan(snapshot); + if (spans.OverlapsWith(span)) { - var span = renameSpan.TrackingSpan.GetSpan(snapshot); - if (spans.OverlapsWith(span)) + if (TryCreateTagSpan(span, renameSpan.Type, out var tagSpan)) { - if (TryCreateTagSpan(span, renameSpan.Type, out var tagSpan)) - { - yield return tagSpan; - } + yield return tagSpan; } } } } - - protected abstract bool TryCreateTagSpan(SnapshotSpan span, RenameSpanKind type, out TagSpan tagSpan); } + + protected abstract bool TryCreateTagSpan(SnapshotSpan span, RenameSpanKind type, out TagSpan tagSpan); } diff --git a/src/EditorFeatures/Core/InlineRename/Taggers/ClassificationTypeDefinitions.cs b/src/EditorFeatures/Core/InlineRename/Taggers/ClassificationTypeDefinitions.cs index 3a10b8fe0eaa2..bfc9bafa00efb 100644 --- a/src/EditorFeatures/Core/InlineRename/Taggers/ClassificationTypeDefinitions.cs +++ b/src/EditorFeatures/Core/InlineRename/Taggers/ClassificationTypeDefinitions.cs @@ -7,16 +7,15 @@ using Microsoft.VisualStudio.Text.Classification; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal sealed class ClassificationTypeDefinitions { - internal sealed class ClassificationTypeDefinitions - { - // Only used for theming, does not need localized - public const string InlineRenameField = "Inline Rename Field Text"; + // Only used for theming, does not need localized + public const string InlineRenameField = "Inline Rename Field Text"; - [Export] - [Name(InlineRenameField)] - [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] - internal readonly ClassificationTypeDefinition? InlineRenameFieldTypeDefinition; - } + [Export] + [Name(InlineRenameField)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition? InlineRenameFieldTypeDefinition; } diff --git a/src/EditorFeatures/Core/InlineRename/Taggers/RenameClassificationTagger.cs b/src/EditorFeatures/Core/InlineRename/Taggers/RenameClassificationTagger.cs index a3815e9dab597..7a6705fbeee7b 100644 --- a/src/EditorFeatures/Core/InlineRename/Taggers/RenameClassificationTagger.cs +++ b/src/EditorFeatures/Core/InlineRename/Taggers/RenameClassificationTagger.cs @@ -8,22 +8,21 @@ using Microsoft.VisualStudio.Text.Classification; using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal class RenameClassificationTagger(ITextBuffer buffer, InlineRenameService renameService, IClassificationType classificationType) : AbstractRenameTagger(buffer, renameService) { - internal class RenameClassificationTagger(ITextBuffer buffer, InlineRenameService renameService, IClassificationType classificationType) : AbstractRenameTagger(buffer, renameService) - { - private readonly IClassificationType _classificationType = classificationType; + private readonly IClassificationType _classificationType = classificationType; - protected override bool TryCreateTagSpan(SnapshotSpan span, RenameSpanKind type, out TagSpan tagSpan) + protected override bool TryCreateTagSpan(SnapshotSpan span, RenameSpanKind type, out TagSpan tagSpan) + { + if (type == RenameSpanKind.Reference) { - if (type == RenameSpanKind.Reference) - { - tagSpan = new TagSpan(span, new ClassificationTag(_classificationType)); - return true; - } - - tagSpan = null; - return false; + tagSpan = new TagSpan(span, new ClassificationTag(_classificationType)); + return true; } + + tagSpan = null; + return false; } } diff --git a/src/EditorFeatures/Core/InlineRename/Taggers/RenameClassificationTaggerProvider.cs b/src/EditorFeatures/Core/InlineRename/Taggers/RenameClassificationTaggerProvider.cs index c90d7a75a2a0e..c0e573602e9ad 100644 --- a/src/EditorFeatures/Core/InlineRename/Taggers/RenameClassificationTaggerProvider.cs +++ b/src/EditorFeatures/Core/InlineRename/Taggers/RenameClassificationTaggerProvider.cs @@ -12,21 +12,20 @@ using Microsoft.VisualStudio.Text.Tagging; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +[Export(typeof(ITaggerProvider))] +[ContentType(ContentTypeNames.RoslynContentType)] +[TagType(typeof(IClassificationTag))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class RenameClassificationTaggerProvider( + InlineRenameService renameService, + IClassificationTypeRegistryService classificationTypeRegistryService) : ITaggerProvider { - [Export(typeof(ITaggerProvider))] - [ContentType(ContentTypeNames.RoslynContentType)] - [TagType(typeof(IClassificationTag))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class RenameClassificationTaggerProvider( - InlineRenameService renameService, - IClassificationTypeRegistryService classificationTypeRegistryService) : ITaggerProvider - { - private readonly InlineRenameService _renameService = renameService; - private readonly IClassificationType _classificationType = classificationTypeRegistryService.GetClassificationType(ClassificationTypeDefinitions.InlineRenameField); + private readonly InlineRenameService _renameService = renameService; + private readonly IClassificationType _classificationType = classificationTypeRegistryService.GetClassificationType(ClassificationTypeDefinitions.InlineRenameField); - public ITagger CreateTagger(ITextBuffer buffer) where T : ITag - => new RenameClassificationTagger(buffer, _renameService, _classificationType) as ITagger; - } + public ITagger CreateTagger(ITextBuffer buffer) where T : ITag + => new RenameClassificationTagger(buffer, _renameService, _classificationType) as ITagger; } diff --git a/src/EditorFeatures/Core/InlineRename/Taggers/RenameTagger.cs b/src/EditorFeatures/Core/InlineRename/Taggers/RenameTagger.cs index b271e378f8ad9..e7c4f32d22cde 100644 --- a/src/EditorFeatures/Core/InlineRename/Taggers/RenameTagger.cs +++ b/src/EditorFeatures/Core/InlineRename/Taggers/RenameTagger.cs @@ -7,30 +7,29 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal sealed partial class RenameTagger(ITextBuffer buffer, InlineRenameService renameService) : AbstractRenameTagger(buffer, renameService) { - internal sealed partial class RenameTagger(ITextBuffer buffer, InlineRenameService renameService) : AbstractRenameTagger(buffer, renameService) + protected override bool TryCreateTagSpan(SnapshotSpan span, RenameSpanKind type, out TagSpan tagSpan) { - protected override bool TryCreateTagSpan(SnapshotSpan span, RenameSpanKind type, out TagSpan tagSpan) + ITextMarkerTag tagKind; + switch (type) { - ITextMarkerTag tagKind; - switch (type) - { - case RenameSpanKind.Reference: - tagKind = RenameFieldBackgroundAndBorderTag.Instance; - break; - case RenameSpanKind.UnresolvedConflict: - tagKind = RenameConflictTag.Instance; - break; - case RenameSpanKind.Complexified: - tagKind = RenameFixupTag.Instance; - break; - default: - throw ExceptionUtilities.UnexpectedValue(type); - } - - tagSpan = new TagSpan(span, tagKind); - return true; + case RenameSpanKind.Reference: + tagKind = RenameFieldBackgroundAndBorderTag.Instance; + break; + case RenameSpanKind.UnresolvedConflict: + tagKind = RenameConflictTag.Instance; + break; + case RenameSpanKind.Complexified: + tagKind = RenameFixupTag.Instance; + break; + default: + throw ExceptionUtilities.UnexpectedValue(type); } + + tagSpan = new TagSpan(span, tagKind); + return true; } } diff --git a/src/EditorFeatures/Core/InlineRename/Taggers/RenameTaggerProvider.cs b/src/EditorFeatures/Core/InlineRename/Taggers/RenameTaggerProvider.cs index a0b902ab42fce..86fd88c8563b9 100644 --- a/src/EditorFeatures/Core/InlineRename/Taggers/RenameTaggerProvider.cs +++ b/src/EditorFeatures/Core/InlineRename/Taggers/RenameTaggerProvider.cs @@ -11,19 +11,18 @@ using Microsoft.VisualStudio.Text.Tagging; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +[Export(typeof(ITaggerProvider))] +[ContentType(ContentTypeNames.RoslynContentType)] +[ContentType(ContentTypeNames.XamlContentType)] +[TagType(typeof(ITextMarkerTag))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class RenameTaggerProvider(InlineRenameService renameService) : ITaggerProvider { - [Export(typeof(ITaggerProvider))] - [ContentType(ContentTypeNames.RoslynContentType)] - [ContentType(ContentTypeNames.XamlContentType)] - [TagType(typeof(ITextMarkerTag))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class RenameTaggerProvider(InlineRenameService renameService) : ITaggerProvider - { - private readonly InlineRenameService _renameService = renameService; + private readonly InlineRenameService _renameService = renameService; - public ITagger CreateTagger(ITextBuffer buffer) where T : ITag - => new RenameTagger(buffer, _renameService) as ITagger; - } + public ITagger CreateTagger(ITextBuffer buffer) where T : ITag + => new RenameTagger(buffer, _renameService) as ITagger; } diff --git a/src/EditorFeatures/Core/InlineRename/TrackingSpanIntrospector.cs b/src/EditorFeatures/Core/InlineRename/TrackingSpanIntrospector.cs index 5fe3e607f45ad..20ce8c70346ed 100644 --- a/src/EditorFeatures/Core/InlineRename/TrackingSpanIntrospector.cs +++ b/src/EditorFeatures/Core/InlineRename/TrackingSpanIntrospector.cs @@ -7,16 +7,15 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +internal sealed class TrackingSpanIntrospector(ITextSnapshot snapshot) : IIntervalIntrospector { - internal sealed class TrackingSpanIntrospector(ITextSnapshot snapshot) : IIntervalIntrospector - { - private readonly ITextSnapshot _snapshot = snapshot; + private readonly ITextSnapshot _snapshot = snapshot; - public int GetStart(ITrackingSpan value) - => value.GetStartPoint(_snapshot); + public int GetStart(ITrackingSpan value) + => value.GetStartPoint(_snapshot); - public int GetLength(ITrackingSpan value) - => value.GetSpan(_snapshot).Length; - } + public int GetLength(ITrackingSpan value) + => value.GetSpan(_snapshot).Length; } diff --git a/src/EditorFeatures/Core/InlineRename/UndoManagerServiceFactory.cs b/src/EditorFeatures/Core/InlineRename/UndoManagerServiceFactory.cs index c9e18e6c976c5..7bdb7f84c4d01 100644 --- a/src/EditorFeatures/Core/InlineRename/UndoManagerServiceFactory.cs +++ b/src/EditorFeatures/Core/InlineRename/UndoManagerServiceFactory.cs @@ -16,114 +16,113 @@ using Microsoft.VisualStudio.Text.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename +namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; + +[ExportWorkspaceServiceFactory(typeof(IInlineRenameUndoManager), ServiceLayer.Default), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class UndoManagerServiceFactory(InlineRenameService inlineRenameService, IGlobalOptionService globalOptionService) : IWorkspaceServiceFactory { - [ExportWorkspaceServiceFactory(typeof(IInlineRenameUndoManager), ServiceLayer.Default), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class UndoManagerServiceFactory(InlineRenameService inlineRenameService, IGlobalOptionService globalOptionService) : IWorkspaceServiceFactory - { - private readonly InlineRenameService _inlineRenameService = inlineRenameService; - private readonly IGlobalOptionService _globalOptionService = globalOptionService; + private readonly InlineRenameService _inlineRenameService = inlineRenameService; + private readonly IGlobalOptionService _globalOptionService = globalOptionService; - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new InlineRenameUndoManager(_inlineRenameService, _globalOptionService); + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => new InlineRenameUndoManager(_inlineRenameService, _globalOptionService); - internal class InlineRenameUndoManager(InlineRenameService inlineRenameService, IGlobalOptionService globalOptionService) : AbstractInlineRenameUndoManager(inlineRenameService, globalOptionService), IInlineRenameUndoManager + internal class InlineRenameUndoManager(InlineRenameService inlineRenameService, IGlobalOptionService globalOptionService) : AbstractInlineRenameUndoManager(inlineRenameService, globalOptionService), IInlineRenameUndoManager + { + internal class BufferUndoState { - internal class BufferUndoState - { - public ITextUndoHistory TextUndoHistory { get; set; } - public ITextUndoTransaction StartRenameSessionUndoTransaction { get; set; } - public ITextUndoTransaction ConflictResolutionUndoTransaction { get; set; } - } + public ITextUndoHistory TextUndoHistory { get; set; } + public ITextUndoTransaction StartRenameSessionUndoTransaction { get; set; } + public ITextUndoTransaction ConflictResolutionUndoTransaction { get; set; } + } - public void CreateStartRenameUndoTransaction(Workspace workspace, ITextBuffer subjectBuffer, IInlineRenameSession inlineRenameSession) - { - var textUndoHistoryService = workspace.Services.GetService(); - Contract.ThrowIfFalse(textUndoHistoryService.TryGetTextUndoHistory(workspace, subjectBuffer, out var undoHistory)); - UndoManagers[subjectBuffer] = new BufferUndoState() { TextUndoHistory = undoHistory }; - CreateStartRenameUndoTransaction(subjectBuffer); - } + public void CreateStartRenameUndoTransaction(Workspace workspace, ITextBuffer subjectBuffer, IInlineRenameSession inlineRenameSession) + { + var textUndoHistoryService = workspace.Services.GetService(); + Contract.ThrowIfFalse(textUndoHistoryService.TryGetTextUndoHistory(workspace, subjectBuffer, out var undoHistory)); + UndoManagers[subjectBuffer] = new BufferUndoState() { TextUndoHistory = undoHistory }; + CreateStartRenameUndoTransaction(subjectBuffer); + } - public void CreateStartRenameUndoTransaction(ITextBuffer subjectBuffer) - { - var undoHistory = this.UndoManagers[subjectBuffer].TextUndoHistory; + public void CreateStartRenameUndoTransaction(ITextBuffer subjectBuffer) + { + var undoHistory = this.UndoManagers[subjectBuffer].TextUndoHistory; - // Create an undo transaction to mark the starting point of the rename session in this buffer - using var undoTransaction = undoHistory.CreateTransaction(EditorFeaturesResources.Start_Rename); + // Create an undo transaction to mark the starting point of the rename session in this buffer + using var undoTransaction = undoHistory.CreateTransaction(EditorFeaturesResources.Start_Rename); - undoTransaction.Complete(); - this.UndoManagers[subjectBuffer].StartRenameSessionUndoTransaction = undoTransaction; - this.UndoManagers[subjectBuffer].ConflictResolutionUndoTransaction = null; - } + undoTransaction.Complete(); + this.UndoManagers[subjectBuffer].StartRenameSessionUndoTransaction = undoTransaction; + this.UndoManagers[subjectBuffer].ConflictResolutionUndoTransaction = null; + } - public void CreateConflictResolutionUndoTransaction(ITextBuffer subjectBuffer, Action applyEdit) + public void CreateConflictResolutionUndoTransaction(ITextBuffer subjectBuffer, Action applyEdit) + { + var undoHistory = this.UndoManagers[subjectBuffer].TextUndoHistory; + while (true) { - var undoHistory = this.UndoManagers[subjectBuffer].TextUndoHistory; - while (true) + if (undoHistory.UndoStack.First() == this.UndoManagers[subjectBuffer].StartRenameSessionUndoTransaction) { - if (undoHistory.UndoStack.First() == this.UndoManagers[subjectBuffer].StartRenameSessionUndoTransaction) - { - undoHistory.Undo(1); - break; - } - undoHistory.Undo(1); + break; } - using var undoTransaction = undoHistory.CreateTransaction(EditorFeaturesResources.Start_Rename); + undoHistory.Undo(1); + } + + using var undoTransaction = undoHistory.CreateTransaction(EditorFeaturesResources.Start_Rename); + + applyEdit(); + undoTransaction.Complete(); + UndoManagers[subjectBuffer].ConflictResolutionUndoTransaction = undoTransaction; + } + + public void UndoTemporaryEdits(ITextBuffer subjectBuffer, bool disconnect) + => UndoTemporaryEdits(subjectBuffer, disconnect, true); - applyEdit(); - undoTransaction.Complete(); - UndoManagers[subjectBuffer].ConflictResolutionUndoTransaction = undoTransaction; + protected override void UndoTemporaryEdits(ITextBuffer subjectBuffer, bool disconnect, bool undoConflictResolution) + { + if (!this.UndoManagers.TryGetValue(subjectBuffer, out var bufferUndoState)) + { + return; } - public void UndoTemporaryEdits(ITextBuffer subjectBuffer, bool disconnect) - => UndoTemporaryEdits(subjectBuffer, disconnect, true); + var undoHistory = bufferUndoState.TextUndoHistory; + var targetTransaction = this.UndoManagers[subjectBuffer].ConflictResolutionUndoTransaction ?? this.UndoManagers[subjectBuffer].StartRenameSessionUndoTransaction; + while (undoHistory.UndoStack.First() != targetTransaction) + { + undoHistory.Undo(1); + } - protected override void UndoTemporaryEdits(ITextBuffer subjectBuffer, bool disconnect, bool undoConflictResolution) + if (undoConflictResolution) { - if (!this.UndoManagers.TryGetValue(subjectBuffer, out var bufferUndoState)) + undoHistory.Undo(1); + if (disconnect) { return; } - var undoHistory = bufferUndoState.TextUndoHistory; - var targetTransaction = this.UndoManagers[subjectBuffer].ConflictResolutionUndoTransaction ?? this.UndoManagers[subjectBuffer].StartRenameSessionUndoTransaction; - while (undoHistory.UndoStack.First() != targetTransaction) - { - undoHistory.Undo(1); - } + CreateStartRenameUndoTransaction(subjectBuffer); + } + } - if (undoConflictResolution) - { - undoHistory.Undo(1); - if (disconnect) - { - return; - } + public void ApplyCurrentState(ITextBuffer subjectBuffer, object propagateSpansEditTag, IEnumerable spans) + { + ApplyReplacementText(subjectBuffer, this.UndoManagers[subjectBuffer].TextUndoHistory, propagateSpansEditTag, spans, this.currentState.ReplacementText); - CreateStartRenameUndoTransaction(subjectBuffer); - } + // Here we create the descriptions for the redo list dropdown. + var undoHistory = this.UndoManagers[subjectBuffer].TextUndoHistory; + foreach (var state in this.RedoStack.Reverse()) + { + using var transaction = undoHistory.CreateTransaction(GetUndoTransactionDescription(state.ReplacementText)); + transaction.Complete(); } - public void ApplyCurrentState(ITextBuffer subjectBuffer, object propagateSpansEditTag, IEnumerable spans) + if (this.RedoStack.Any()) { - ApplyReplacementText(subjectBuffer, this.UndoManagers[subjectBuffer].TextUndoHistory, propagateSpansEditTag, spans, this.currentState.ReplacementText); - - // Here we create the descriptions for the redo list dropdown. - var undoHistory = this.UndoManagers[subjectBuffer].TextUndoHistory; - foreach (var state in this.RedoStack.Reverse()) - { - using var transaction = undoHistory.CreateTransaction(GetUndoTransactionDescription(state.ReplacementText)); - transaction.Complete(); - } - - if (this.RedoStack.Any()) - { - undoHistory.Undo(this.RedoStack.Count); - } + undoHistory.Undo(this.RedoStack.Count); } } } diff --git a/src/EditorFeatures/Core/IntelliSense/AbstractController.cs b/src/EditorFeatures/Core/IntelliSense/AbstractController.cs index 59ebd56ec2481..f240f64e55d4a 100644 --- a/src/EditorFeatures/Core/IntelliSense/AbstractController.cs +++ b/src/EditorFeatures/Core/IntelliSense/AbstractController.cs @@ -13,150 +13,149 @@ using Roslyn.Utilities; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense; + +internal abstract class AbstractController : IController + where TSession : class, ISession + where TPresenterSession : IIntelliSensePresenterSession { - internal abstract class AbstractController : IController - where TSession : class, ISession - where TPresenterSession : IIntelliSensePresenterSession + protected readonly IThreadingContext ThreadingContext; + protected readonly IGlobalOptionService GlobalOptions; + protected readonly ITextView TextView; + protected readonly ITextBuffer SubjectBuffer; + protected readonly IIntelliSensePresenter Presenter; + protected readonly IDocumentProvider DocumentProvider; + + private readonly IAsynchronousOperationListener _asyncListener; + private readonly string _asyncOperationId; + + // Null when we absolutely know we don't have any sort of item computation going on. Non + // null the moment we think we start computing state. Null again once we decide we can + // stop. + protected TSession sessionOpt; + + protected bool IsSessionActive => sessionOpt != null; + + protected AbstractController( + IGlobalOptionService globalOptions, + IThreadingContext threadingContext, + ITextView textView, + ITextBuffer subjectBuffer, + IIntelliSensePresenter presenter, + IAsynchronousOperationListener asyncListener, + IDocumentProvider documentProvider, + string asyncOperationId) { - protected readonly IThreadingContext ThreadingContext; - protected readonly IGlobalOptionService GlobalOptions; - protected readonly ITextView TextView; - protected readonly ITextBuffer SubjectBuffer; - protected readonly IIntelliSensePresenter Presenter; - protected readonly IDocumentProvider DocumentProvider; - - private readonly IAsynchronousOperationListener _asyncListener; - private readonly string _asyncOperationId; - - // Null when we absolutely know we don't have any sort of item computation going on. Non - // null the moment we think we start computing state. Null again once we decide we can - // stop. - protected TSession sessionOpt; - - protected bool IsSessionActive => sessionOpt != null; - - protected AbstractController( - IGlobalOptionService globalOptions, - IThreadingContext threadingContext, - ITextView textView, - ITextBuffer subjectBuffer, - IIntelliSensePresenter presenter, - IAsynchronousOperationListener asyncListener, - IDocumentProvider documentProvider, - string asyncOperationId) - { - this.GlobalOptions = globalOptions; - ThreadingContext = threadingContext; - this.TextView = textView; - this.SubjectBuffer = subjectBuffer; - this.Presenter = presenter; - _asyncListener = asyncListener; - this.DocumentProvider = documentProvider; - _asyncOperationId = asyncOperationId; - - this.TextView.Closed += OnTextViewClosed; - - // Caret position changed only fires if the caret is explicitly moved. It doesn't fire - // when the caret is moved because of text change events. - this.TextView.Caret.PositionChanged += this.OnCaretPositionChanged; - this.TextView.TextBuffer.PostChanged += this.OnTextViewBufferPostChanged; - } + this.GlobalOptions = globalOptions; + ThreadingContext = threadingContext; + this.TextView = textView; + this.SubjectBuffer = subjectBuffer; + this.Presenter = presenter; + _asyncListener = asyncListener; + this.DocumentProvider = documentProvider; + _asyncOperationId = asyncOperationId; + + this.TextView.Closed += OnTextViewClosed; + + // Caret position changed only fires if the caret is explicitly moved. It doesn't fire + // when the caret is moved because of text change events. + this.TextView.Caret.PositionChanged += this.OnCaretPositionChanged; + this.TextView.TextBuffer.PostChanged += this.OnTextViewBufferPostChanged; + } - internal abstract void OnModelUpdated(TModel result, bool updateController); - internal abstract void OnTextViewBufferPostChanged(object sender, EventArgs e); - internal abstract void OnCaretPositionChanged(object sender, EventArgs e); + internal abstract void OnModelUpdated(TModel result, bool updateController); + internal abstract void OnTextViewBufferPostChanged(object sender, EventArgs e); + internal abstract void OnCaretPositionChanged(object sender, EventArgs e); - private void OnTextViewClosed(object sender, EventArgs e) - { - this.ThreadingContext.ThrowIfNotOnUIThread(); - DismissSessionIfActive(); + private void OnTextViewClosed(object sender, EventArgs e) + { + this.ThreadingContext.ThrowIfNotOnUIThread(); + DismissSessionIfActive(); - this.TextView.Closed -= OnTextViewClosed; - this.TextView.Caret.PositionChanged -= this.OnCaretPositionChanged; - this.TextView.TextBuffer.PostChanged -= this.OnTextViewBufferPostChanged; - } + this.TextView.Closed -= OnTextViewClosed; + this.TextView.Caret.PositionChanged -= this.OnCaretPositionChanged; + this.TextView.TextBuffer.PostChanged -= this.OnTextViewBufferPostChanged; + } - public TModel WaitForController() - { - this.ThreadingContext.ThrowIfNotOnUIThread(); - VerifySessionIsActive(); - return sessionOpt.WaitForController(); - } + public TModel WaitForController() + { + this.ThreadingContext.ThrowIfNotOnUIThread(); + VerifySessionIsActive(); + return sessionOpt.WaitForController(); + } - void IController.OnModelUpdated(TModel result, bool updateController) - { - // This is only called from the model computation if it was not cancelled. And if it was - // not cancelled then we must have a pointer to it (as well as the presenter session). - this.ThreadingContext.ThrowIfNotOnUIThread(); - VerifySessionIsActive(); + void IController.OnModelUpdated(TModel result, bool updateController) + { + // This is only called from the model computation if it was not cancelled. And if it was + // not cancelled then we must have a pointer to it (as well as the presenter session). + this.ThreadingContext.ThrowIfNotOnUIThread(); + VerifySessionIsActive(); - this.OnModelUpdated(result, updateController); - } + this.OnModelUpdated(result, updateController); + } - IAsyncToken IController.BeginAsyncOperation(string name, object tag, string filePath, int lineNumber) - { - this.ThreadingContext.ThrowIfNotOnUIThread(); - VerifySessionIsActive(); - name = String.IsNullOrEmpty(name) - ? _asyncOperationId - : $"{_asyncOperationId} - {name}"; - return _asyncListener.BeginAsyncOperation(name, tag, filePath: filePath, lineNumber: lineNumber); - } + IAsyncToken IController.BeginAsyncOperation(string name, object tag, string filePath, int lineNumber) + { + this.ThreadingContext.ThrowIfNotOnUIThread(); + VerifySessionIsActive(); + name = String.IsNullOrEmpty(name) + ? _asyncOperationId + : $"{_asyncOperationId} - {name}"; + return _asyncListener.BeginAsyncOperation(name, tag, filePath: filePath, lineNumber: lineNumber); + } - protected void VerifySessionIsActive() - { - this.ThreadingContext.ThrowIfNotOnUIThread(); - Contract.ThrowIfFalse(IsSessionActive); - } + protected void VerifySessionIsActive() + { + this.ThreadingContext.ThrowIfNotOnUIThread(); + Contract.ThrowIfFalse(IsSessionActive); + } - protected void VerifySessionIsInactive() - { - this.ThreadingContext.ThrowIfNotOnUIThread(); - Contract.ThrowIfTrue(IsSessionActive); - } + protected void VerifySessionIsInactive() + { + this.ThreadingContext.ThrowIfNotOnUIThread(); + Contract.ThrowIfTrue(IsSessionActive); + } - protected void DismissSessionIfActive() + protected void DismissSessionIfActive() + { + this.ThreadingContext.ThrowIfNotOnUIThread(); + if (IsSessionActive) { - this.ThreadingContext.ThrowIfNotOnUIThread(); - if (IsSessionActive) - { - this.StopModelComputation(); - } + this.StopModelComputation(); } + } - public void StopModelComputation() - { - this.ThreadingContext.ThrowIfNotOnUIThread(); - VerifySessionIsActive(); - - // Make a local copy so that we won't do anything that causes us to recurse and try to - // dismiss this again. - var localSession = sessionOpt; - sessionOpt = null; - localSession.Stop(); - } + public void StopModelComputation() + { + this.ThreadingContext.ThrowIfNotOnUIThread(); + VerifySessionIsActive(); + + // Make a local copy so that we won't do anything that causes us to recurse and try to + // dismiss this again. + var localSession = sessionOpt; + sessionOpt = null; + localSession.Stop(); + } - public bool TryHandleEscapeKey() - { - this.ThreadingContext.ThrowIfNotOnUIThread(); + public bool TryHandleEscapeKey() + { + this.ThreadingContext.ThrowIfNotOnUIThread(); - // Escape simply dismissed a session if it's up. Otherwise let the next thing in the - // chain handle us. - if (!IsSessionActive) - { - return false; - } + // Escape simply dismissed a session if it's up. Otherwise let the next thing in the + // chain handle us. + if (!IsSessionActive) + { + return false; + } - // If we haven't even computed a model yet, then also send this command to anyone - // listening. It's unlikely that the command was intended for us (as we wouldn't - // have even shown ui yet. - var handledCommand = sessionOpt.InitialUnfilteredModel != null; + // If we haven't even computed a model yet, then also send this command to anyone + // listening. It's unlikely that the command was intended for us (as we wouldn't + // have even shown ui yet. + var handledCommand = sessionOpt.InitialUnfilteredModel != null; - // In the presence of an escape, we always stop what we're doing. - this.StopModelComputation(); + // In the presence of an escape, we always stop what we're doing. + this.StopModelComputation(); - return handledCommand; - } + return handledCommand; } } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/AsyncCompletionLogger.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/AsyncCompletionLogger.cs index 0dc5b5cd7d05e..542773fb982e4 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/AsyncCompletionLogger.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/AsyncCompletionLogger.cs @@ -5,95 +5,94 @@ using System; using Microsoft.CodeAnalysis.Internal.Log; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +internal static class AsyncCompletionLogger { - internal static class AsyncCompletionLogger + private static readonly CountLogAggregator s_countLogAggregator = new(); + private static readonly StatisticLogAggregator s_statisticLogAggregator = new(); + private static readonly HistogramLogAggregator s_histogramLogAggregator = new(25, 500); + + private enum ActionInfo { - private static readonly CountLogAggregator s_countLogAggregator = new(); - private static readonly StatisticLogAggregator s_statisticLogAggregator = new(); - private static readonly HistogramLogAggregator s_histogramLogAggregator = new(25, 500); + // # of sessions where import completion is enabled by default + SessionWithTypeImportCompletionEnabled, - private enum ActionInfo - { - // # of sessions where import completion is enabled by default - SessionWithTypeImportCompletionEnabled, + // # of session among SessionWithImportCompletionDelayed where import completion items + // are later included in list update. Note this doesn't include using of expander. + SessionWithDelayedImportCompletionIncludedInUpdate, - // # of session among SessionWithImportCompletionDelayed where import completion items - // are later included in list update. Note this doesn't include using of expander. - SessionWithDelayedImportCompletionIncludedInUpdate, + ExpanderUsageCount, - ExpanderUsageCount, + SourceInitializationTicks, + SourceGetContextCompletedTicks, + SourceGetContextCanceledTicks, - SourceInitializationTicks, - SourceGetContextCompletedTicks, - SourceGetContextCanceledTicks, + ItemManagerSortTicks, + ItemManagerUpdateCompletedTicks, + ItemManagerUpdateCanceledTicks, + } - ItemManagerSortTicks, - ItemManagerUpdateCompletedTicks, - ItemManagerUpdateCanceledTicks, - } + internal static void LogImportCompletionGetContext() + => s_countLogAggregator.IncreaseCount(ActionInfo.SessionWithTypeImportCompletionEnabled); - internal static void LogImportCompletionGetContext() - => s_countLogAggregator.IncreaseCount(ActionInfo.SessionWithTypeImportCompletionEnabled); + internal static void LogSessionWithDelayedImportCompletionIncludedInUpdate() + => s_countLogAggregator.IncreaseCount(ActionInfo.SessionWithDelayedImportCompletionIncludedInUpdate); - internal static void LogSessionWithDelayedImportCompletionIncludedInUpdate() - => s_countLogAggregator.IncreaseCount(ActionInfo.SessionWithDelayedImportCompletionIncludedInUpdate); + internal static void LogExpanderUsage() + => s_countLogAggregator.IncreaseCount(ActionInfo.ExpanderUsageCount); - internal static void LogExpanderUsage() - => s_countLogAggregator.IncreaseCount(ActionInfo.ExpanderUsageCount); + internal static void LogSourceInitializationTicksDataPoint(TimeSpan elapsed) + { + s_statisticLogAggregator.AddDataPoint(ActionInfo.SourceInitializationTicks, elapsed); + s_histogramLogAggregator.LogTime(ActionInfo.SourceInitializationTicks, elapsed); + } - internal static void LogSourceInitializationTicksDataPoint(TimeSpan elapsed) - { - s_statisticLogAggregator.AddDataPoint(ActionInfo.SourceInitializationTicks, elapsed); - s_histogramLogAggregator.LogTime(ActionInfo.SourceInitializationTicks, elapsed); - } + internal static void LogSourceGetContextTicksDataPoint(TimeSpan elapsed, bool isCanceled) + { + var key = isCanceled + ? ActionInfo.SourceGetContextCanceledTicks + : ActionInfo.SourceGetContextCompletedTicks; - internal static void LogSourceGetContextTicksDataPoint(TimeSpan elapsed, bool isCanceled) - { - var key = isCanceled - ? ActionInfo.SourceGetContextCanceledTicks - : ActionInfo.SourceGetContextCompletedTicks; + s_statisticLogAggregator.AddDataPoint(key, elapsed); + s_histogramLogAggregator.LogTime(key, elapsed); + } - s_statisticLogAggregator.AddDataPoint(key, elapsed); - s_histogramLogAggregator.LogTime(key, elapsed); - } + internal static void LogItemManagerSortTicksDataPoint(TimeSpan elapsed) + { + s_statisticLogAggregator.AddDataPoint(ActionInfo.ItemManagerSortTicks, elapsed); + s_histogramLogAggregator.LogTime(ActionInfo.ItemManagerSortTicks, elapsed); + } - internal static void LogItemManagerSortTicksDataPoint(TimeSpan elapsed) - { - s_statisticLogAggregator.AddDataPoint(ActionInfo.ItemManagerSortTicks, elapsed); - s_histogramLogAggregator.LogTime(ActionInfo.ItemManagerSortTicks, elapsed); - } + internal static void LogItemManagerUpdateDataPoint(TimeSpan elapsed, bool isCanceled) + { + var key = isCanceled + ? ActionInfo.ItemManagerUpdateCanceledTicks + : ActionInfo.ItemManagerUpdateCompletedTicks; + + s_statisticLogAggregator.AddDataPoint(key, elapsed); + s_histogramLogAggregator.LogTime(key, elapsed); + } - internal static void LogItemManagerUpdateDataPoint(TimeSpan elapsed, bool isCanceled) + internal static void ReportTelemetry() + { + Logger.Log(FunctionId.Intellisense_AsyncCompletion_Data, KeyValueLogMessage.Create(m => { - var key = isCanceled - ? ActionInfo.ItemManagerUpdateCanceledTicks - : ActionInfo.ItemManagerUpdateCompletedTicks; + foreach (var kv in s_statisticLogAggregator) + { + var statistics = kv.Value.GetStatisticResult(); + statistics.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); + } - s_statisticLogAggregator.AddDataPoint(key, elapsed); - s_histogramLogAggregator.LogTime(key, elapsed); - } + foreach (var kv in s_countLogAggregator) + { + m[kv.Key.ToString()] = kv.Value.GetCount(); + } - internal static void ReportTelemetry() - { - Logger.Log(FunctionId.Intellisense_AsyncCompletion_Data, KeyValueLogMessage.Create(m => + foreach (var kv in s_histogramLogAggregator) { - foreach (var kv in s_statisticLogAggregator) - { - var statistics = kv.Value.GetStatisticResult(); - statistics.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); - } - - foreach (var kv in s_countLogAggregator) - { - m[kv.Key.ToString()] = kv.Value.GetCount(); - } - - foreach (var kv in s_histogramLogAggregator) - { - kv.Value.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); - } - })); - } + kv.Value.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); + } + })); } } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs index f4272cbf7f0e1..7d4b4c67a47bc 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs @@ -28,386 +28,385 @@ using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem; using VSCompletionItem = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data.CompletionItem; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +internal sealed class CommitManager : IAsyncCompletionCommitManager { - internal sealed class CommitManager : IAsyncCompletionCommitManager - { - private static readonly AsyncCompletionData.CommitResult CommitResultUnhandled = - new(isHandled: false, AsyncCompletionData.CommitBehavior.None); + private static readonly AsyncCompletionData.CommitResult CommitResultUnhandled = + new(isHandled: false, AsyncCompletionData.CommitBehavior.None); - private readonly RecentItemsManager _recentItemsManager; - private readonly ITextView _textView; - private readonly IGlobalOptionService _globalOptions; - private readonly IThreadingContext _threadingContext; - private readonly ILanguageServerSnippetExpander? _languageServerSnippetExpander; + private readonly RecentItemsManager _recentItemsManager; + private readonly ITextView _textView; + private readonly IGlobalOptionService _globalOptions; + private readonly IThreadingContext _threadingContext; + private readonly ILanguageServerSnippetExpander? _languageServerSnippetExpander; - public IEnumerable PotentialCommitCharacters + public IEnumerable PotentialCommitCharacters + { + get { - get + if (_textView.Properties.TryGetProperty(CompletionSource.PotentialCommitCharacters, out ImmutableArray potentialCommitCharacters)) { - if (_textView.Properties.TryGetProperty(CompletionSource.PotentialCommitCharacters, out ImmutableArray potentialCommitCharacters)) - { - return potentialCommitCharacters; - } - else - { - // If we were not initialized with a CompletionService or are called for a wrong textView, we should not make a commit. - return []; - } + return potentialCommitCharacters; + } + else + { + // If we were not initialized with a CompletionService or are called for a wrong textView, we should not make a commit. + return []; } } + } + + internal CommitManager( + ITextView textView, + RecentItemsManager recentItemsManager, + IGlobalOptionService globalOptions, + IThreadingContext threadingContext, + ILanguageServerSnippetExpander? languageServerSnippetExpander) + { + _globalOptions = globalOptions; + _threadingContext = threadingContext; + _recentItemsManager = recentItemsManager; + _textView = textView; + _languageServerSnippetExpander = languageServerSnippetExpander; + } - internal CommitManager( - ITextView textView, - RecentItemsManager recentItemsManager, - IGlobalOptionService globalOptions, - IThreadingContext threadingContext, - ILanguageServerSnippetExpander? languageServerSnippetExpander) + /// + /// The method performs a preliminarily filtering of commit availability. + /// In case of a doubt, it should respond with true. + /// We will be able to cancel later in + /// + /// based on item, e.g. based on . + /// + public bool ShouldCommitCompletion( + IAsyncCompletionSession session, + SnapshotPoint location, + char typedChar, + CancellationToken cancellationToken) + { + // this is called only when the typedChar is in the list returned by PotentialCommitCharacters. + // It's possible typedChar is intended to be a filter char for some items (either currently considered for commit of not) + // we let this case to be handled in `TryCommit` instead, where we will have all the information needed to decide. + return true; + } + + public AsyncCompletionData.CommitResult TryCommit( + IAsyncCompletionSession session, + ITextBuffer subjectBuffer, + VSCompletionItem item, + char typedChar, + CancellationToken cancellationToken) + { + // We can make changes to buffers. We would like to be sure nobody can change them at the same time. + _threadingContext.ThrowIfNotOnUIThread(); + + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) { - _globalOptions = globalOptions; - _threadingContext = threadingContext; - _recentItemsManager = recentItemsManager; - _textView = textView; - _languageServerSnippetExpander = languageServerSnippetExpander; + return CommitResultUnhandled; } - /// - /// The method performs a preliminarily filtering of commit availability. - /// In case of a doubt, it should respond with true. - /// We will be able to cancel later in - /// - /// based on item, e.g. based on . - /// - public bool ShouldCommitCompletion( - IAsyncCompletionSession session, - SnapshotPoint location, - char typedChar, - CancellationToken cancellationToken) + var completionService = document.GetLanguageService(); + if (completionService == null) { - // this is called only when the typedChar is in the list returned by PotentialCommitCharacters. - // It's possible typedChar is intended to be a filter char for some items (either currently considered for commit of not) - // we let this case to be handled in `TryCommit` instead, where we will have all the information needed to decide. - return true; + return CommitResultUnhandled; } - public AsyncCompletionData.CommitResult TryCommit( - IAsyncCompletionSession session, - ITextBuffer subjectBuffer, - VSCompletionItem item, - char typedChar, - CancellationToken cancellationToken) + if (!CompletionItemData.TryGetData(item, out var itemData)) { - // We can make changes to buffers. We would like to be sure nobody can change them at the same time. - _threadingContext.ThrowIfNotOnUIThread(); + // Roslyn should not be called if the item committing was not provided by Roslyn. + return CommitResultUnhandled; + } - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return CommitResultUnhandled; - } + var roslynItem = itemData.RoslynItem; + var filterText = session.ApplicableToSpan.GetText(session.ApplicableToSpan.TextBuffer.CurrentSnapshot) + typedChar; - var completionService = document.GetLanguageService(); - if (completionService == null) - { - return CommitResultUnhandled; - } + if (Helpers.IsFilterCharacter(roslynItem, typedChar, filterText)) + { + // Returning Cancel means we keep the current session and consider the character for further filtering. + return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.CancelCommit); + } - if (!CompletionItemData.TryGetData(item, out var itemData)) + // typedChar could be a filter character for another item. If we find such an item that the current filter + // text matches its start, then we should cancel commit and give ItemManager a chance to handle it. + // This is done here instead of in `ShouldCommitCompletion` because `ShouldCommitCompletion` might be called before + // CompletionSource add the `excludedCommitCharactersMap` to the session property bag. + if (session.Properties.TryGetProperty(CompletionSource.ExcludedCommitCharactersMap, out MultiDictionary excludedCommitCharactersMap)) + { + foreach (var potentialItemForSelection in excludedCommitCharactersMap[typedChar]) { - // Roslyn should not be called if the item committing was not provided by Roslyn. - return CommitResultUnhandled; + if (potentialItemForSelection != roslynItem && Helpers.TextTypedSoFarMatchesItem(potentialItemForSelection, filterText)) + return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.CancelCommit); } + } - var roslynItem = itemData.RoslynItem; - var filterText = session.ApplicableToSpan.GetText(session.ApplicableToSpan.TextBuffer.CurrentSnapshot) + typedChar; - - if (Helpers.IsFilterCharacter(roslynItem, typedChar, filterText)) - { - // Returning Cancel means we keep the current session and consider the character for further filtering. - return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.CancelCommit); - } + var options = _globalOptions.GetCompletionOptions(document.Project.Language); + var serviceRules = completionService.GetRules(options); - // typedChar could be a filter character for another item. If we find such an item that the current filter - // text matches its start, then we should cancel commit and give ItemManager a chance to handle it. - // This is done here instead of in `ShouldCommitCompletion` because `ShouldCommitCompletion` might be called before - // CompletionSource add the `excludedCommitCharactersMap` to the session property bag. - if (session.Properties.TryGetProperty(CompletionSource.ExcludedCommitCharactersMap, out MultiDictionary excludedCommitCharactersMap)) - { - foreach (var potentialItemForSelection in excludedCommitCharactersMap[typedChar]) - { - if (potentialItemForSelection != roslynItem && Helpers.TextTypedSoFarMatchesItem(potentialItemForSelection, filterText)) - return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.CancelCommit); - } - } + // We can be called before for ShouldCommitCompletion. However, that call does not provide rules applied for the completion item. + // Now we check for the commit character in the context of Rules that could change the list of commit characters. - var options = _globalOptions.GetCompletionOptions(document.Project.Language); - var serviceRules = completionService.GetRules(options); + if (!Helpers.IsStandardCommitCharacter(typedChar) && !IsCommitCharacter(serviceRules, roslynItem, typedChar)) + { + // Returning None means we complete the current session with a void commit. + // The Editor then will try to trigger a new completion session for the character. + return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); + } - // We can be called before for ShouldCommitCompletion. However, that call does not provide rules applied for the completion item. - // Now we check for the commit character in the context of Rules that could change the list of commit characters. + if (!itemData.TriggerLocation.HasValue) + { + // Need the trigger snapshot to calculate the span when the commit changes to be applied. + // They should always be available from items provided by Roslyn CompletionSource. + // Just to be defensive, if it's not found here, Roslyn should not make a commit. + return CommitResultUnhandled; + } - if (!Helpers.IsStandardCommitCharacter(typedChar) && !IsCommitCharacter(serviceRules, roslynItem, typedChar)) - { - // Returning None means we complete the current session with a void commit. - // The Editor then will try to trigger a new completion session for the character. - return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); - } + var triggerDocument = itemData.TriggerLocation.Value.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (triggerDocument == null) + { + return CommitResultUnhandled; + } - if (!itemData.TriggerLocation.HasValue) - { - // Need the trigger snapshot to calculate the span when the commit changes to be applied. - // They should always be available from items provided by Roslyn CompletionSource. - // Just to be defensive, if it's not found here, Roslyn should not make a commit. - return CommitResultUnhandled; - } + var sessionData = CompletionSessionData.GetOrCreateSessionData(session); + if (!sessionData.CompletionListSpan.HasValue) + { + return CommitResultUnhandled; + } - var triggerDocument = itemData.TriggerLocation.Value.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (triggerDocument == null) - { - return CommitResultUnhandled; - } + // Commit with completion service assumes that null is provided is case of invoke. VS provides '\0' in the case. + var commitChar = typedChar == '\0' ? null : (char?)typedChar; + return Commit( + session, triggerDocument, completionService, subjectBuffer, + roslynItem, sessionData.CompletionListSpan.Value, commitChar, itemData.TriggerLocation.Value.Snapshot, serviceRules, + filterText, cancellationToken); + } - var sessionData = CompletionSessionData.GetOrCreateSessionData(session); - if (!sessionData.CompletionListSpan.HasValue) - { - return CommitResultUnhandled; - } + private AsyncCompletionData.CommitResult Commit( + IAsyncCompletionSession session, + Document document, + CompletionService completionService, + ITextBuffer subjectBuffer, + RoslynCompletionItem roslynItem, + TextSpan completionListSpan, + char? commitCharacter, + ITextSnapshot triggerSnapshot, + CompletionRules rules, + string filterText, + CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnUIThread(); - // Commit with completion service assumes that null is provided is case of invoke. VS provides '\0' in the case. - var commitChar = typedChar == '\0' ? null : (char?)typedChar; - return Commit( - session, triggerDocument, completionService, subjectBuffer, - roslynItem, sessionData.CompletionListSpan.Value, commitChar, itemData.TriggerLocation.Value.Snapshot, serviceRules, - filterText, cancellationToken); + bool includesCommitCharacter; + if (!subjectBuffer.CheckEditAccess()) + { + // We are on the wrong thread. + FatalError.ReportAndCatch(new InvalidOperationException("Subject buffer did not provide Edit Access"), ErrorSeverity.Critical); + return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); } - private AsyncCompletionData.CommitResult Commit( - IAsyncCompletionSession session, - Document document, - CompletionService completionService, - ITextBuffer subjectBuffer, - RoslynCompletionItem roslynItem, - TextSpan completionListSpan, - char? commitCharacter, - ITextSnapshot triggerSnapshot, - CompletionRules rules, - string filterText, - CancellationToken cancellationToken) + if (subjectBuffer.EditInProgress) { - _threadingContext.ThrowIfNotOnUIThread(); + FatalError.ReportAndCatch(new InvalidOperationException("Subject buffer is editing by someone else."), ErrorSeverity.Critical); + return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); + } - bool includesCommitCharacter; - if (!subjectBuffer.CheckEditAccess()) - { - // We are on the wrong thread. - FatalError.ReportAndCatch(new InvalidOperationException("Subject buffer did not provide Edit Access"), ErrorSeverity.Critical); - return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); - } + // This might be an item promoted by us, make sure we restore it to the original state first. + roslynItem = Helpers.DemoteItem(roslynItem); + CompletionChange change; - if (subjectBuffer.EditInProgress) - { - FatalError.ReportAndCatch(new InvalidOperationException("Subject buffer is editing by someone else."), ErrorSeverity.Critical); - return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); - } + // We met an issue when external code threw an OperationCanceledException and the cancellationToken is not canceled. + // Catching this scenario for further investigations. + // See https://github.com/dotnet/roslyn/issues/38455. + try + { + // Cached items have a span computed at the point they were created. This span may no + // longer be valid when used again. In that case, override the span with the latest span + // for the completion list itself. + if (roslynItem.Flags.IsCached()) + roslynItem.Span = completionListSpan; - // This might be an item promoted by us, make sure we restore it to the original state first. - roslynItem = Helpers.DemoteItem(roslynItem); - CompletionChange change; + // Adding import is not allowed in debugger view + if (_textView is IDebuggerTextView) + roslynItem = ImportCompletionItem.MarkItemToAlwaysFullyQualify(roslynItem); - // We met an issue when external code threw an OperationCanceledException and the cancellationToken is not canceled. - // Catching this scenario for further investigations. - // See https://github.com/dotnet/roslyn/issues/38455. - try - { - // Cached items have a span computed at the point they were created. This span may no - // longer be valid when used again. In that case, override the span with the latest span - // for the completion list itself. - if (roslynItem.Flags.IsCached()) - roslynItem.Span = completionListSpan; + change = completionService.GetChangeAsync(document, roslynItem, commitCharacter, cancellationToken).WaitAndGetResult(cancellationToken); + } + catch (OperationCanceledException e) when (e.CancellationToken != cancellationToken && FatalError.ReportAndCatch(e)) + { + return CommitResultUnhandled; + } - // Adding import is not allowed in debugger view - if (_textView is IDebuggerTextView) - roslynItem = ImportCompletionItem.MarkItemToAlwaysFullyQualify(roslynItem); + cancellationToken.ThrowIfCancellationRequested(); - change = completionService.GetChangeAsync(document, roslynItem, commitCharacter, cancellationToken).WaitAndGetResult(cancellationToken); - } - catch (OperationCanceledException e) when (e.CancellationToken != cancellationToken && FatalError.ReportAndCatch(e)) - { - return CommitResultUnhandled; - } + var view = session.TextView; - cancellationToken.ThrowIfCancellationRequested(); + var provider = completionService.GetProvider(roslynItem, document.Project); + if (provider is ICustomCommitCompletionProvider customCommitProvider) + { + customCommitProvider.Commit(roslynItem, document, view, subjectBuffer, triggerSnapshot, commitCharacter); + return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); + } - var view = session.TextView; + var textChange = change.TextChange; + var triggerSnapshotSpan = new SnapshotSpan(triggerSnapshot, textChange.Span.ToSpan()); + var mappedSpan = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive); - var provider = completionService.GetProvider(roslynItem, document.Project); - if (provider is ICustomCommitCompletionProvider customCommitProvider) - { - customCommitProvider.Commit(roslynItem, document, view, subjectBuffer, triggerSnapshot, commitCharacter); - return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); - } + // Specifically for snippets, we check to see if the associated completion item is a snippet, + // and if so, we call upon the LanguageServerSnippetExpander's TryExpand to insert the snippet. + if (SnippetCompletionItem.IsSnippet(roslynItem)) + { + Contract.ThrowIfNull(_languageServerSnippetExpander); - var textChange = change.TextChange; - var triggerSnapshotSpan = new SnapshotSpan(triggerSnapshot, textChange.Span.ToSpan()); - var mappedSpan = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive); + var lspSnippetText = change.Properties[SnippetCompletionItem.LSPSnippetKey]; - // Specifically for snippets, we check to see if the associated completion item is a snippet, - // and if so, we call upon the LanguageServerSnippetExpander's TryExpand to insert the snippet. - if (SnippetCompletionItem.IsSnippet(roslynItem)) + Contract.ThrowIfNull(lspSnippetText); + if (!_languageServerSnippetExpander.TryExpand(lspSnippetText, mappedSpan, _textView)) { - Contract.ThrowIfNull(_languageServerSnippetExpander); - - var lspSnippetText = change.Properties[SnippetCompletionItem.LSPSnippetKey]; - - Contract.ThrowIfNull(lspSnippetText); - if (!_languageServerSnippetExpander.TryExpand(lspSnippetText, mappedSpan, _textView)) - { - FatalError.ReportAndCatch(new InvalidOperationException("The invoked LSP snippet expander came back as false."), ErrorSeverity.Critical); - } - - return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); + FatalError.ReportAndCatch(new InvalidOperationException("The invoked LSP snippet expander came back as false."), ErrorSeverity.Critical); } - ITextSnapshot updatedCurrentSnapshot; - using (var edit = subjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null)) - { - edit.Replace(mappedSpan.Span, change.TextChange.NewText); + return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); + } - // edit.Apply() may trigger changes made by extensions. - // updatedCurrentSnapshot will contain changes made by Roslyn but not by other extensions. - updatedCurrentSnapshot = edit.Apply(); - } + ITextSnapshot updatedCurrentSnapshot; + using (var edit = subjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null)) + { + edit.Replace(mappedSpan.Span, change.TextChange.NewText); - if (change.NewPosition.HasValue) + // edit.Apply() may trigger changes made by extensions. + // updatedCurrentSnapshot will contain changes made by Roslyn but not by other extensions. + updatedCurrentSnapshot = edit.Apply(); + } + + if (change.NewPosition.HasValue) + { + // Roslyn knows how to position the caret in the snapshot we just created. + // If there were more edits made by extensions, TryMoveCaretToAndEnsureVisible maps the snapshot point to the most recent one. + view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(updatedCurrentSnapshot, change.NewPosition.Value)); + } + else + { + // Or, If we're doing a minimal change, then the edit that we make to the + // buffer may not make the total text change that places the caret where we + // would expect it to go based on the requested change. In this case, + // determine where the item should go and set the care manually. + + // Note: we only want to move the caret if the caret would have been moved + // by the edit. i.e. if the caret was actually in the mapped span that + // we're replacing. + var caretPositionInBuffer = view.GetCaretPoint(subjectBuffer); + if (caretPositionInBuffer.HasValue && mappedSpan.IntersectsWith(caretPositionInBuffer.Value)) { - // Roslyn knows how to position the caret in the snapshot we just created. - // If there were more edits made by extensions, TryMoveCaretToAndEnsureVisible maps the snapshot point to the most recent one. - view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(updatedCurrentSnapshot, change.NewPosition.Value)); + view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(subjectBuffer.CurrentSnapshot, mappedSpan.Start.Position + textChange.NewText?.Length ?? 0)); } else { - // Or, If we're doing a minimal change, then the edit that we make to the - // buffer may not make the total text change that places the caret where we - // would expect it to go based on the requested change. In this case, - // determine where the item should go and set the care manually. - - // Note: we only want to move the caret if the caret would have been moved - // by the edit. i.e. if the caret was actually in the mapped span that - // we're replacing. - var caretPositionInBuffer = view.GetCaretPoint(subjectBuffer); - if (caretPositionInBuffer.HasValue && mappedSpan.IntersectsWith(caretPositionInBuffer.Value)) - { - view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(subjectBuffer.CurrentSnapshot, mappedSpan.Start.Position + textChange.NewText?.Length ?? 0)); - } - else - { - view.Caret.EnsureVisible(); - } + view.Caret.EnsureVisible(); } + } - includesCommitCharacter = change.IncludesCommitCharacter; + includesCommitCharacter = change.IncludesCommitCharacter; - if (roslynItem.Rules.FormatOnCommit) - { - // The edit updates the snapshot however other extensions may make changes there. - // Therefore, it is required to use subjectBuffer.CurrentSnapshot for further calculations rather than the updated current snapshot defined above. - var currentDocument = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - var formattingService = currentDocument?.GetRequiredLanguageService(); + if (roslynItem.Rules.FormatOnCommit) + { + // The edit updates the snapshot however other extensions may make changes there. + // Therefore, it is required to use subjectBuffer.CurrentSnapshot for further calculations rather than the updated current snapshot defined above. + var currentDocument = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + var formattingService = currentDocument?.GetRequiredLanguageService(); - if (currentDocument != null && formattingService != null) - { - var spanToFormat = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive); + if (currentDocument != null && formattingService != null) + { + var spanToFormat = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive); - // Note: C# always completes synchronously, TypeScript is async - var changes = formattingService.GetFormattingChangesAsync(currentDocument, subjectBuffer, spanToFormat.Span.ToTextSpan(), cancellationToken).WaitAndGetResult(cancellationToken); - subjectBuffer.ApplyChanges(changes); - } + // Note: C# always completes synchronously, TypeScript is async + var changes = formattingService.GetFormattingChangesAsync(currentDocument, subjectBuffer, spanToFormat.Span.ToTextSpan(), cancellationToken).WaitAndGetResult(cancellationToken); + subjectBuffer.ApplyChanges(changes); } + } - _recentItemsManager.MakeMostRecentItem(roslynItem); + _recentItemsManager.MakeMostRecentItem(roslynItem); - if (provider is INotifyCommittingItemCompletionProvider notifyProvider) - { - _ = _threadingContext.JoinableTaskFactory.RunAsync(async () => - { - // Make sure the notification isn't sent on UI thread. - await TaskScheduler.Default; - _ = notifyProvider.NotifyCommittingItemAsync(document, roslynItem, commitCharacter, cancellationToken).ReportNonFatalErrorAsync(); - }); - } - - if (includesCommitCharacter) + if (provider is INotifyCommittingItemCompletionProvider notifyProvider) + { + _ = _threadingContext.JoinableTaskFactory.RunAsync(async () => { - return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.SuppressFurtherTypeCharCommandHandlers); - } + // Make sure the notification isn't sent on UI thread. + await TaskScheduler.Default; + _ = notifyProvider.NotifyCommittingItemAsync(document, roslynItem, commitCharacter, cancellationToken).ReportNonFatalErrorAsync(); + }); + } - if (commitCharacter == '\n' && SendEnterThroughToEditor(rules, roslynItem, filterText)) - { - return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers); - } + if (includesCommitCharacter) + { + return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.SuppressFurtherTypeCharCommandHandlers); + } - return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); + if (commitCharacter == '\n' && SendEnterThroughToEditor(rules, roslynItem, filterText)) + { + return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers); } - internal static bool IsCommitCharacter(CompletionRules completionRules, CompletionItem item, char ch) + return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); + } + + internal static bool IsCommitCharacter(CompletionRules completionRules, CompletionItem item, char ch) + { + // First see if the item has any specific commit rules it wants followed. + foreach (var rule in item.Rules.CommitCharacterRules) { - // First see if the item has any specific commit rules it wants followed. - foreach (var rule in item.Rules.CommitCharacterRules) + switch (rule.Kind) { - switch (rule.Kind) - { - case CharacterSetModificationKind.Add: - if (rule.Characters.Contains(ch)) - { - return true; - } + case CharacterSetModificationKind.Add: + if (rule.Characters.Contains(ch)) + { + return true; + } - continue; + continue; - case CharacterSetModificationKind.Remove: - if (rule.Characters.Contains(ch)) - { - return false; - } + case CharacterSetModificationKind.Remove: + if (rule.Characters.Contains(ch)) + { + return false; + } - continue; + continue; - case CharacterSetModificationKind.Replace: - return rule.Characters.Contains(ch); - } + case CharacterSetModificationKind.Replace: + return rule.Characters.Contains(ch); } - - // Fall back to the default rules for this language's completion service. - return completionRules.DefaultCommitCharacters.IndexOf(ch) >= 0; } - internal static bool SendEnterThroughToEditor(CompletionRules rules, RoslynCompletionItem item, string textTypedSoFar) + // Fall back to the default rules for this language's completion service. + return completionRules.DefaultCommitCharacters.IndexOf(ch) >= 0; + } + + internal static bool SendEnterThroughToEditor(CompletionRules rules, RoslynCompletionItem item, string textTypedSoFar) + { + var rule = item.Rules.EnterKeyRule; + if (rule == EnterKeyRule.Default) { - var rule = item.Rules.EnterKeyRule; - if (rule == EnterKeyRule.Default) - { - rule = rules.DefaultEnterKeyRule; - } + rule = rules.DefaultEnterKeyRule; + } - switch (rule) - { - default: - case EnterKeyRule.Default: - case EnterKeyRule.Never: - return false; - case EnterKeyRule.Always: - return true; - case EnterKeyRule.AfterFullyTypedWord: - // textTypedSoFar is concatenated from individual chars typed. - // '\n' is the enter char. - // That is why, there is no need to check for '\r\n'. - if (textTypedSoFar.LastOrDefault() == '\n') - { - textTypedSoFar = textTypedSoFar[..^1]; - } + switch (rule) + { + default: + case EnterKeyRule.Default: + case EnterKeyRule.Never: + return false; + case EnterKeyRule.Always: + return true; + case EnterKeyRule.AfterFullyTypedWord: + // textTypedSoFar is concatenated from individual chars typed. + // '\n' is the enter char. + // That is why, there is no need to check for '\r\n'. + if (textTypedSoFar.LastOrDefault() == '\n') + { + textTypedSoFar = textTypedSoFar[..^1]; + } - return item.GetEntireDisplayText() == textTypedSoFar; - } + return item.GetEntireDisplayText() == textTypedSoFar; } } } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs index 6db9621264300..77dfaa8ec4df6 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs @@ -12,32 +12,31 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +[Export(typeof(IAsyncCompletionCommitManagerProvider))] +[Name("Roslyn Completion Commit Manager")] +[ContentType(ContentTypeNames.RoslynContentType)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CommitManagerProvider( + IThreadingContext threadingContext, + RecentItemsManager recentItemsManager, + IGlobalOptionService globalOptions, + [Import(AllowDefault = true)] ILanguageServerSnippetExpander? languageServerSnippetExpander) : IAsyncCompletionCommitManagerProvider { - [Export(typeof(IAsyncCompletionCommitManagerProvider))] - [Name("Roslyn Completion Commit Manager")] - [ContentType(ContentTypeNames.RoslynContentType)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CommitManagerProvider( - IThreadingContext threadingContext, - RecentItemsManager recentItemsManager, - IGlobalOptionService globalOptions, - [Import(AllowDefault = true)] ILanguageServerSnippetExpander? languageServerSnippetExpander) : IAsyncCompletionCommitManagerProvider - { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly RecentItemsManager _recentItemsManager = recentItemsManager; - private readonly IGlobalOptionService _globalOptions = globalOptions; - private readonly ILanguageServerSnippetExpander? _languageServerSnippetExpander = languageServerSnippetExpander; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly RecentItemsManager _recentItemsManager = recentItemsManager; + private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly ILanguageServerSnippetExpander? _languageServerSnippetExpander = languageServerSnippetExpander; - IAsyncCompletionCommitManager? IAsyncCompletionCommitManagerProvider.GetOrCreate(ITextView textView) + IAsyncCompletionCommitManager? IAsyncCompletionCommitManagerProvider.GetOrCreate(ITextView textView) + { + if (textView.IsInLspEditorContext()) { - if (textView.IsInLspEditorContext()) - { - return null; - } - - return new CommitManager(textView, _recentItemsManager, _globalOptions, _threadingContext, _languageServerSnippetExpander); + return null; } + + return new CommitManager(textView, _recentItemsManager, _globalOptions, _threadingContext, _languageServerSnippetExpander); } } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionItemData.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionItemData.cs index 18d8df6891afb..c7f26629e03c8 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionItemData.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionItemData.cs @@ -6,35 +6,34 @@ using Microsoft.VisualStudio.Text; using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +internal sealed record class CompletionItemData(RoslynCompletionItem RoslynItem, SnapshotPoint? TriggerLocation) { - internal sealed record class CompletionItemData(RoslynCompletionItem RoslynItem, SnapshotPoint? TriggerLocation) - { - private const string RoslynCompletionItemData = nameof(RoslynCompletionItemData); + private const string RoslynCompletionItemData = nameof(RoslynCompletionItemData); - public static bool TryGetData(CompletionItem vsCompletionItem, out CompletionItemData data) - => vsCompletionItem.Properties.TryGetProperty(RoslynCompletionItemData, out data); + public static bool TryGetData(CompletionItem vsCompletionItem, out CompletionItemData data) + => vsCompletionItem.Properties.TryGetProperty(RoslynCompletionItemData, out data); - public static RoslynCompletionItem GetOrAddDummyRoslynItem(CompletionItem vsItem) - { - if (TryGetData(vsItem, out var data)) - return data.RoslynItem; + public static RoslynCompletionItem GetOrAddDummyRoslynItem(CompletionItem vsItem) + { + if (TryGetData(vsItem, out var data)) + return data.RoslynItem; - // TriggerLocation is null for items provided by non-roslyn completion source - var roslynItem = CreateDummyRoslynItem(vsItem); - AddData(vsItem, roslynItem, triggerLocation: null); + // TriggerLocation is null for items provided by non-roslyn completion source + var roslynItem = CreateDummyRoslynItem(vsItem); + AddData(vsItem, roslynItem, triggerLocation: null); - return roslynItem; - } + return roslynItem; + } - public static void AddData(CompletionItem vsCompletionItem, RoslynCompletionItem roslynItem, SnapshotPoint? triggerLocation) - => vsCompletionItem.Properties[RoslynCompletionItemData] = new CompletionItemData(roslynItem, triggerLocation); + public static void AddData(CompletionItem vsCompletionItem, RoslynCompletionItem roslynItem, SnapshotPoint? triggerLocation) + => vsCompletionItem.Properties[RoslynCompletionItemData] = new CompletionItemData(roslynItem, triggerLocation); - private static RoslynCompletionItem CreateDummyRoslynItem(CompletionItem vsItem) - => RoslynCompletionItem.Create( - displayText: vsItem.DisplayText, - filterText: vsItem.FilterText, - sortText: vsItem.SortText, - displayTextSuffix: vsItem.Suffix); - } + private static RoslynCompletionItem CreateDummyRoslynItem(CompletionItem vsItem) + => RoslynCompletionItem.Create( + displayText: vsItem.DisplayText, + filterText: vsItem.FilterText, + sortText: vsItem.SortText, + displayTextSuffix: vsItem.Suffix); } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSessionData.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSessionData.cs index 543df9384b43b..fc95d7c26fad1 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSessionData.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSessionData.cs @@ -10,36 +10,35 @@ using Microsoft.VisualStudio.Text.Editor; using RoslynCompletionList = Microsoft.CodeAnalysis.Completion.CompletionList; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion -{ - /// - /// Contains data need to be tracked over an entire IAsyncCompletionSession completion - /// session for various operations. - /// - internal sealed class CompletionSessionData - { - private const string RoslynCompletionSessionData = nameof(RoslynCompletionSessionData); - public bool TargetTypeFilterSelected { get; set; } - public bool HasSuggestionItemOptions { get; set; } +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; - public SnapshotPoint? ExpandedItemTriggerLocation { get; set; } - public TextSpan? CompletionListSpan { get; set; } - public CompletionList? CombinedSortedList { get; set; } - public Task<(CompletionContext, RoslynCompletionList)>? ExpandedItemsTask { get; set; } - public bool IsExclusive { get; set; } - public bool NonBlockingCompletionEnabled { get; } +/// +/// Contains data need to be tracked over an entire IAsyncCompletionSession completion +/// session for various operations. +/// +internal sealed class CompletionSessionData +{ + private const string RoslynCompletionSessionData = nameof(RoslynCompletionSessionData); + public bool TargetTypeFilterSelected { get; set; } + public bool HasSuggestionItemOptions { get; set; } - private CompletionSessionData(IAsyncCompletionSession session) - { - // Editor has to separate options control the behavior of block waiting computation of completion items. - // When set to true, `NonBlockingCompletionOptionId` takes precedence over `ResponsiveCompletionOptionId` - // and is equivalent to `ResponsiveCompletionOptionId` to true and `ResponsiveCompletionThresholdOptionId` to 0. - var nonBlockingCompletionEnabled = session.TextView.Options.GetOptionValue(DefaultOptions.NonBlockingCompletionOptionId); - var responsiveCompletionEnabled = session.TextView.Options.GetOptionValue(DefaultOptions.ResponsiveCompletionOptionId); - NonBlockingCompletionEnabled = nonBlockingCompletionEnabled || responsiveCompletionEnabled; - } + public SnapshotPoint? ExpandedItemTriggerLocation { get; set; } + public TextSpan? CompletionListSpan { get; set; } + public CompletionList? CombinedSortedList { get; set; } + public Task<(CompletionContext, RoslynCompletionList)>? ExpandedItemsTask { get; set; } + public bool IsExclusive { get; set; } + public bool NonBlockingCompletionEnabled { get; } - public static CompletionSessionData GetOrCreateSessionData(IAsyncCompletionSession session) - => session.Properties.GetOrCreateSingletonProperty(RoslynCompletionSessionData, () => new CompletionSessionData(session)); + private CompletionSessionData(IAsyncCompletionSession session) + { + // Editor has to separate options control the behavior of block waiting computation of completion items. + // When set to true, `NonBlockingCompletionOptionId` takes precedence over `ResponsiveCompletionOptionId` + // and is equivalent to `ResponsiveCompletionOptionId` to true and `ResponsiveCompletionThresholdOptionId` to 0. + var nonBlockingCompletionEnabled = session.TextView.Options.GetOptionValue(DefaultOptions.NonBlockingCompletionOptionId); + var responsiveCompletionEnabled = session.TextView.Options.GetOptionValue(DefaultOptions.ResponsiveCompletionOptionId); + NonBlockingCompletionEnabled = nonBlockingCompletionEnabled || responsiveCompletionEnabled; } + + public static CompletionSessionData GetOrCreateSessionData(IAsyncCompletionSession session) + => session.Properties.GetOrCreateSingletonProperty(RoslynCompletionSessionData, () => new CompletionSessionData(session)); } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSource.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSource.cs index 1fe78f9034ebe..2f738faae61df 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSource.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSource.cs @@ -35,594 +35,593 @@ using VSCompletionItem = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data.CompletionItem; using VSUtilities = Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +internal sealed class CompletionSource : IAsyncExpandingCompletionSource { - internal sealed class CompletionSource : IAsyncExpandingCompletionSource + internal const string PotentialCommitCharacters = nameof(PotentialCommitCharacters); + internal const string NonBlockingCompletion = nameof(NonBlockingCompletion); + + // Don't change this property! Editor code currently has a dependency on it. + internal const string ExcludedCommitCharacters = nameof(ExcludedCommitCharacters); + internal const string ExcludedCommitCharactersMap = nameof(ExcludedCommitCharactersMap); + + private static readonly ImmutableArray s_warningImageAttributeImagesArray = + [new ImageElement(Glyph.CompletionWarning.GetImageId(), EditorFeaturesResources.Warning_image_element)]; + + private static readonly EditorOptionKey s_nonBlockingCompletionEditorOption = new(NonBlockingCompletion); + + // Use CWT to cache data needed to create VSCompletionItem, so the table would be cleared when Roslyn completion item cache is cleared. + private static readonly ConditionalWeakTable> s_roslynItemToVsItemData = new(); + + // Cancellation series we use to stop background task for expanded items when exclusive items are returned by core providers. + private readonly CancellationSeries _expandedItemsTaskCancellationSeries = new(); + + private readonly ITextView _textView; + private readonly bool _isDebuggerTextView; + private readonly ImmutableHashSet _roles; + private readonly Lazy _streamingPresenter; + private readonly IThreadingContext _threadingContext; + private readonly VSUtilities.IUIThreadOperationExecutor _operationExecutor; + private readonly IAsynchronousOperationListener _asyncListener; + private readonly EditorOptionsService _editorOptionsService; + private bool _snippetCompletionTriggeredIndirectly; + + internal CompletionSource( + ITextView textView, + Lazy streamingPresenter, + IThreadingContext threadingContext, + VSUtilities.IUIThreadOperationExecutor operationExecutor, + IAsynchronousOperationListener asyncListener, + EditorOptionsService editorOptionsService) { - internal const string PotentialCommitCharacters = nameof(PotentialCommitCharacters); - internal const string NonBlockingCompletion = nameof(NonBlockingCompletion); - - // Don't change this property! Editor code currently has a dependency on it. - internal const string ExcludedCommitCharacters = nameof(ExcludedCommitCharacters); - internal const string ExcludedCommitCharactersMap = nameof(ExcludedCommitCharactersMap); - - private static readonly ImmutableArray s_warningImageAttributeImagesArray = - [new ImageElement(Glyph.CompletionWarning.GetImageId(), EditorFeaturesResources.Warning_image_element)]; - - private static readonly EditorOptionKey s_nonBlockingCompletionEditorOption = new(NonBlockingCompletion); - - // Use CWT to cache data needed to create VSCompletionItem, so the table would be cleared when Roslyn completion item cache is cleared. - private static readonly ConditionalWeakTable> s_roslynItemToVsItemData = new(); - - // Cancellation series we use to stop background task for expanded items when exclusive items are returned by core providers. - private readonly CancellationSeries _expandedItemsTaskCancellationSeries = new(); - - private readonly ITextView _textView; - private readonly bool _isDebuggerTextView; - private readonly ImmutableHashSet _roles; - private readonly Lazy _streamingPresenter; - private readonly IThreadingContext _threadingContext; - private readonly VSUtilities.IUIThreadOperationExecutor _operationExecutor; - private readonly IAsynchronousOperationListener _asyncListener; - private readonly EditorOptionsService _editorOptionsService; - private bool _snippetCompletionTriggeredIndirectly; - - internal CompletionSource( - ITextView textView, - Lazy streamingPresenter, - IThreadingContext threadingContext, - VSUtilities.IUIThreadOperationExecutor operationExecutor, - IAsynchronousOperationListener asyncListener, - EditorOptionsService editorOptionsService) - { - _textView = textView; - _streamingPresenter = streamingPresenter; - _threadingContext = threadingContext; - _operationExecutor = operationExecutor; - _asyncListener = asyncListener; - _editorOptionsService = editorOptionsService; - _isDebuggerTextView = textView is IDebuggerTextView; - _roles = textView.Roles.ToImmutableHashSet(); - } + _textView = textView; + _streamingPresenter = streamingPresenter; + _threadingContext = threadingContext; + _operationExecutor = operationExecutor; + _asyncListener = asyncListener; + _editorOptionsService = editorOptionsService; + _isDebuggerTextView = textView is IDebuggerTextView; + _roles = textView.Roles.ToImmutableHashSet(); + } - public AsyncCompletionData.CompletionStartData InitializeCompletion( - AsyncCompletionData.CompletionTrigger trigger, - SnapshotPoint triggerLocation, - CancellationToken cancellationToken) + public AsyncCompletionData.CompletionStartData InitializeCompletion( + AsyncCompletionData.CompletionTrigger trigger, + SnapshotPoint triggerLocation, + CancellationToken cancellationToken) + { + var stopwatch = SharedStopwatch.StartNew(); + try { - var stopwatch = SharedStopwatch.StartNew(); - try - { - // We take sourceText from document to get a snapshot span. - // We would like to be sure that nobody changes buffers at the same time. - _threadingContext.ThrowIfNotOnUIThread(); + // We take sourceText from document to get a snapshot span. + // We would like to be sure that nobody changes buffers at the same time. + _threadingContext.ThrowIfNotOnUIThread(); - if (_textView.Selection.Mode == TextSelectionMode.Box) - { - // No completion with multiple selection - return AsyncCompletionData.CompletionStartData.DoesNotParticipateInCompletion; - } + if (_textView.Selection.Mode == TextSelectionMode.Box) + { + // No completion with multiple selection + return AsyncCompletionData.CompletionStartData.DoesNotParticipateInCompletion; + } - var document = triggerLocation.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return AsyncCompletionData.CompletionStartData.DoesNotParticipateInCompletion; - } + var document = triggerLocation.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + { + return AsyncCompletionData.CompletionStartData.DoesNotParticipateInCompletion; + } - var service = document.GetLanguageService(); - if (service == null) - { - return AsyncCompletionData.CompletionStartData.DoesNotParticipateInCompletion; - } + var service = document.GetLanguageService(); + if (service == null) + { + return AsyncCompletionData.CompletionStartData.DoesNotParticipateInCompletion; + } - var options = _editorOptionsService.GlobalOptions.GetCompletionOptions(document.Project.Language); + var options = _editorOptionsService.GlobalOptions.GetCompletionOptions(document.Project.Language); - // The Editor supports the option per textView. - // There could be mixed desired behavior per textView and even per same completion session. - // The right fix would be to send this information as a result of the method. - // Then, the Editor would choose the right behavior for mixed cases. - var blockForCompletionItem = _editorOptionsService.GlobalOptions.GetOption(CompletionViewOptionsStorage.BlockForCompletionItems, service.Language); - _textView.Options.GlobalOptions.SetOptionValue(s_nonBlockingCompletionEditorOption, !blockForCompletionItem); + // The Editor supports the option per textView. + // There could be mixed desired behavior per textView and even per same completion session. + // The right fix would be to send this information as a result of the method. + // Then, the Editor would choose the right behavior for mixed cases. + var blockForCompletionItem = _editorOptionsService.GlobalOptions.GetOption(CompletionViewOptionsStorage.BlockForCompletionItems, service.Language); + _textView.Options.GlobalOptions.SetOptionValue(s_nonBlockingCompletionEditorOption, !blockForCompletionItem); - // In case of calls with multiple completion services for the same view (e.g. TypeScript and C#), those completion services must not be called simultaneously for the same session. - // Therefore, in each completion session we use a list of commit character for a specific completion service and a specific content type. - _textView.Properties[PotentialCommitCharacters] = service.GetRules(options).DefaultCommitCharacters; + // In case of calls with multiple completion services for the same view (e.g. TypeScript and C#), those completion services must not be called simultaneously for the same session. + // Therefore, in each completion session we use a list of commit character for a specific completion service and a specific content type. + _textView.Properties[PotentialCommitCharacters] = service.GetRules(options).DefaultCommitCharacters; - // Reset a flag which means a snippet triggered by ? + Tab. - // Set it later if met the condition. - _snippetCompletionTriggeredIndirectly = false; + // Reset a flag which means a snippet triggered by ? + Tab. + // Set it later if met the condition. + _snippetCompletionTriggeredIndirectly = false; - var sourceText = document.GetTextSynchronously(cancellationToken); + var sourceText = document.GetTextSynchronously(cancellationToken); - return ShouldTriggerCompletion(trigger, triggerLocation, sourceText, document, service, options) - ? new AsyncCompletionData.CompletionStartData( - participation: AsyncCompletionData.CompletionParticipation.ProvidesItems, - applicableToSpan: new SnapshotSpan( - triggerLocation.Snapshot, - service.GetDefaultCompletionListSpan(sourceText, triggerLocation.Position).ToSpan())) - : AsyncCompletionData.CompletionStartData.DoesNotParticipateInCompletion; - } - finally - { - AsyncCompletionLogger.LogSourceInitializationTicksDataPoint(stopwatch.Elapsed); - } + return ShouldTriggerCompletion(trigger, triggerLocation, sourceText, document, service, options) + ? new AsyncCompletionData.CompletionStartData( + participation: AsyncCompletionData.CompletionParticipation.ProvidesItems, + applicableToSpan: new SnapshotSpan( + triggerLocation.Snapshot, + service.GetDefaultCompletionListSpan(sourceText, triggerLocation.Position).ToSpan())) + : AsyncCompletionData.CompletionStartData.DoesNotParticipateInCompletion; } + finally + { + AsyncCompletionLogger.LogSourceInitializationTicksDataPoint(stopwatch.Elapsed); + } + } - private bool ShouldTriggerCompletion( - AsyncCompletionData.CompletionTrigger trigger, - SnapshotPoint triggerLocation, - SourceText sourceText, - Document document, - CompletionService completionService, - CompletionOptions options) + private bool ShouldTriggerCompletion( + AsyncCompletionData.CompletionTrigger trigger, + SnapshotPoint triggerLocation, + SourceText sourceText, + Document document, + CompletionService completionService, + CompletionOptions options) + { + //The user may be trying to invoke snippets through question-tab. + // We may provide a completion after that. + // Otherwise, tab should not be a completion trigger. + if (trigger.Reason == AsyncCompletionData.CompletionTriggerReason.Insertion && trigger.Character == '\t') { - //The user may be trying to invoke snippets through question-tab. - // We may provide a completion after that. - // Otherwise, tab should not be a completion trigger. - if (trigger.Reason == AsyncCompletionData.CompletionTriggerReason.Insertion && trigger.Character == '\t') - { - return TryInvokeSnippetCompletion(triggerLocation.Snapshot.TextBuffer, triggerLocation.Position, sourceText, document.Project.Services, completionService.GetRules(options)); - } + return TryInvokeSnippetCompletion(triggerLocation.Snapshot.TextBuffer, triggerLocation.Position, sourceText, document.Project.Services, completionService.GetRules(options)); + } - var roslynTrigger = Helpers.GetRoslynTrigger(trigger, triggerLocation); + var roslynTrigger = Helpers.GetRoslynTrigger(trigger, triggerLocation); - // The completion service decides that user may want a completion. - return completionService.ShouldTriggerCompletion( - document.Project, document.Project.Services, sourceText, triggerLocation.Position, roslynTrigger, options, document.Project.Solution.Options, _roles); - } + // The completion service decides that user may want a completion. + return completionService.ShouldTriggerCompletion( + document.Project, document.Project.Services, sourceText, triggerLocation.Position, roslynTrigger, options, document.Project.Solution.Options, _roles); + } - private bool TryInvokeSnippetCompletion( - ITextBuffer buffer, int caretPoint, SourceText text, LanguageServices services, CompletionRules rules) + private bool TryInvokeSnippetCompletion( + ITextBuffer buffer, int caretPoint, SourceText text, LanguageServices services, CompletionRules rules) + { + // Do not invoke snippet if the corresponding rule is not set in options. + if (rules.SnippetsRule != SnippetsRule.IncludeAfterTypingIdentifierQuestionTab) { - // Do not invoke snippet if the corresponding rule is not set in options. - if (rules.SnippetsRule != SnippetsRule.IncludeAfterTypingIdentifierQuestionTab) - { - return false; - } + return false; + } - var syntaxFacts = services.GetService(); - // Snippets are included if the user types: - // If at least one condition for snippets do not hold, bail out. - if (syntaxFacts == null || - caretPoint < 3 || - text[caretPoint - 2] != '?' || - !QuestionMarkIsPrecededByIdentifierAndWhitespace(text, caretPoint - 2, syntaxFacts)) - { - return false; - } + var syntaxFacts = services.GetService(); + // Snippets are included if the user types: + // If at least one condition for snippets do not hold, bail out. + if (syntaxFacts == null || + caretPoint < 3 || + text[caretPoint - 2] != '?' || + !QuestionMarkIsPrecededByIdentifierAndWhitespace(text, caretPoint - 2, syntaxFacts)) + { + return false; + } - // Because is actually a command to bring up snippets, - // we delete the last that was typed. - buffer.ApplyChange(new TextChange(TextSpan.FromBounds(caretPoint - 2, caretPoint), string.Empty)); + // Because is actually a command to bring up snippets, + // we delete the last that was typed. + buffer.ApplyChange(new TextChange(TextSpan.FromBounds(caretPoint - 2, caretPoint), string.Empty)); - _snippetCompletionTriggeredIndirectly = true; - return true; - } + _snippetCompletionTriggeredIndirectly = true; + return true; + } - public async Task GetCompletionContextAsync( - IAsyncCompletionSession session, - AsyncCompletionData.CompletionTrigger trigger, - SnapshotPoint triggerLocation, - SnapshotSpan applicableToSpan, - CancellationToken cancellationToken) + public async Task GetCompletionContextAsync( + IAsyncCompletionSession session, + AsyncCompletionData.CompletionTrigger trigger, + SnapshotPoint triggerLocation, + SnapshotSpan applicableToSpan, + CancellationToken cancellationToken) + { + var totalStopWatch = SharedStopwatch.StartNew(); + try { - var totalStopWatch = SharedStopwatch.StartNew(); - try + if (session is null) + throw new ArgumentNullException(nameof(session)); + + var document = triggerLocation.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return VSCompletionContext.Empty; + + // The computation of completion items is divided into two tasks: + // + // 1. "Core" items (i.e. non-expanded) which should be included in the list regardless of the selection of expander. + // Right now this includes all items except those from unimported namespaces. + // + // 2. Expanded items which only show in the completion list when expander is selected, or by default if the corresponding + // features are enabled. Right now only items from unimported namespaces are associated with expander. + // + // #1 is the essence of completion so we'd always wait until its task is completed and return the results. However, because we have + // a really tight perf budget in completion, and computing those items in #2 could be expensive especially in a large solution + // (e.g. requires syntax/symbol indices and/or runs in OOP,) we decide to kick off the computation in parallel when completion is + // triggered, but only include its results if: + // + // (a) it's completed by the time task #1 is completed and + // (b) including them won't interfere with users' ability to browse the list (e.g. when the list is too long since filter text is short) + // + // Otherwise we don't wait on it and return items from #1 immediately. Task #2 will still be running in the background + // (until session is dismissed/committed) and we'd check back to see if it's completed whenever we have a chance to update the completion list, + // i.e. when user typed another character, a filter was selected, etc. If so, those items will be added as part of the refresh. + // + // The reason of adopting this approach is we want to minimize typing delays. There are two ways user might perceive a delay in typing. + // First, they could see a delay between typing a character and completion list being displayed if they want to examine the items available. + // Second, they might be typing continuously w/o paying attention to completion list, and simply expect the completion to do the "right thing" + // when a commit char is typed (e.g. commit "cancellationToken" when typing 'can$TAB$'). However, the commit could be delayed if completion is + // still waiting on the computation of all available items, which manifests as UI delays and in worst case timeouts in commit which results in + // unexpected behavior (e.g. typing 'can$TAB$' results in a 250ms UI freeze and still ends up with "can" instead of "cancellationToken".) + // + // This approach would ensure the computation of #2 will not be the cause of such delays, with the obvious trade off of potentially not providing + // expanded items until later (or never) in a completion session even if the feature is enabled. Note that in most cases we'd expect task #2 to finish + // in time and complete result would be available when it's most likely needed (see `ShouldHideExpandedItems` helper in ItemManager for details.) + // However, even in the case only partial result is returned at the start, we still believe this is acceptable given how critical perf is in typing scenario. + // Additionally, expanded items are usually considered complementary. The need for them only rise occasionally (it's rare when users need to add imports,) + // and when they are needed, our hypothesis is because of their more intrusive nature (adding an import to the document) users would more likely to + // contemplate such action thus typing slower before commit and/or spending more time examining the list, which give us some opportunities + // to still provide those items later before they are truly required. + + var showCompletionItemFilters = _editorOptionsService.GlobalOptions.GetOption(CompletionViewOptionsStorage.ShowCompletionItemFilters, document.Project.Language); + var options = _editorOptionsService.GlobalOptions.GetCompletionOptions(document.Project.Language) with { - if (session is null) - throw new ArgumentNullException(nameof(session)); - - var document = triggerLocation.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return VSCompletionContext.Empty; - - // The computation of completion items is divided into two tasks: - // - // 1. "Core" items (i.e. non-expanded) which should be included in the list regardless of the selection of expander. - // Right now this includes all items except those from unimported namespaces. - // - // 2. Expanded items which only show in the completion list when expander is selected, or by default if the corresponding - // features are enabled. Right now only items from unimported namespaces are associated with expander. - // - // #1 is the essence of completion so we'd always wait until its task is completed and return the results. However, because we have - // a really tight perf budget in completion, and computing those items in #2 could be expensive especially in a large solution - // (e.g. requires syntax/symbol indices and/or runs in OOP,) we decide to kick off the computation in parallel when completion is - // triggered, but only include its results if: - // - // (a) it's completed by the time task #1 is completed and - // (b) including them won't interfere with users' ability to browse the list (e.g. when the list is too long since filter text is short) - // - // Otherwise we don't wait on it and return items from #1 immediately. Task #2 will still be running in the background - // (until session is dismissed/committed) and we'd check back to see if it's completed whenever we have a chance to update the completion list, - // i.e. when user typed another character, a filter was selected, etc. If so, those items will be added as part of the refresh. - // - // The reason of adopting this approach is we want to minimize typing delays. There are two ways user might perceive a delay in typing. - // First, they could see a delay between typing a character and completion list being displayed if they want to examine the items available. - // Second, they might be typing continuously w/o paying attention to completion list, and simply expect the completion to do the "right thing" - // when a commit char is typed (e.g. commit "cancellationToken" when typing 'can$TAB$'). However, the commit could be delayed if completion is - // still waiting on the computation of all available items, which manifests as UI delays and in worst case timeouts in commit which results in - // unexpected behavior (e.g. typing 'can$TAB$' results in a 250ms UI freeze and still ends up with "can" instead of "cancellationToken".) - // - // This approach would ensure the computation of #2 will not be the cause of such delays, with the obvious trade off of potentially not providing - // expanded items until later (or never) in a completion session even if the feature is enabled. Note that in most cases we'd expect task #2 to finish - // in time and complete result would be available when it's most likely needed (see `ShouldHideExpandedItems` helper in ItemManager for details.) - // However, even in the case only partial result is returned at the start, we still believe this is acceptable given how critical perf is in typing scenario. - // Additionally, expanded items are usually considered complementary. The need for them only rise occasionally (it's rare when users need to add imports,) - // and when they are needed, our hypothesis is because of their more intrusive nature (adding an import to the document) users would more likely to - // contemplate such action thus typing slower before commit and/or spending more time examining the list, which give us some opportunities - // to still provide those items later before they are truly required. - - var showCompletionItemFilters = _editorOptionsService.GlobalOptions.GetOption(CompletionViewOptionsStorage.ShowCompletionItemFilters, document.Project.Language); - var options = _editorOptionsService.GlobalOptions.GetCompletionOptions(document.Project.Language) with - { - PerformSort = false, - UpdateImportCompletionCacheInBackground = true, - TargetTypedCompletionFilter = showCompletionItemFilters // Compute targeted types if filter is enabled - }; + PerformSort = false, + UpdateImportCompletionCacheInBackground = true, + TargetTypedCompletionFilter = showCompletionItemFilters // Compute targeted types if filter is enabled + }; - var sessionData = CompletionSessionData.GetOrCreateSessionData(session); + var sessionData = CompletionSessionData.GetOrCreateSessionData(session); - if (!options.ShouldShowItemsFromUnimportedNamespaces) - { - // No need to trigger expanded providers at all if the feature is disabled, just trigger core providers and return; - var (context, list) = await GetCompletionContextWorkerAsync(session, document, trigger, triggerLocation, + if (!options.ShouldShowItemsFromUnimportedNamespaces) + { + // No need to trigger expanded providers at all if the feature is disabled, just trigger core providers and return; + var (context, list) = await GetCompletionContextWorkerAsync(session, document, trigger, triggerLocation, + options with { ExpandedCompletionBehavior = ExpandedCompletionMode.NonExpandedItemsOnly }, cancellationToken).ConfigureAwait(false); + + UpdateSessionData(session, sessionData, list, triggerLocation); + return context; + } + else + { + // Kicking off the task for expanded items, so it runs in parallel with regular providers. + // Otherwise, the computation of unimported items won't start until we return those regular items to editor, + // which combined with our behavior of not showing expanded items until ready (and only adding them during + // completion list refresh) means increased chance that users won't see those items for the first few characters typed. + // This does mean we might do unnecessary work if any regular provider is `exclusive`, but such cases are relatively infrequent + // and we'd like to have expanded items available when they are needed. As these results come back potentially after + // presentation (and sorting) of the non-expanded results, we need these results to come back already sorted. + var expandedItemsTaskCancellationToken = _expandedItemsTaskCancellationSeries.CreateNext(cancellationToken); + var expandedItemsTask = Task.Run(() => GetCompletionContextWorkerAsync(session, document, trigger, triggerLocation, + options with { ExpandedCompletionBehavior = ExpandedCompletionMode.ExpandedItemsOnly, PerformSort = true }, + expandedItemsTaskCancellationToken), + expandedItemsTaskCancellationToken); + + // Now trigger and wait for core providers to return; + var (nonExpandedContext, nonExpandedCompletionList) = await GetCompletionContextWorkerAsync(session, document, trigger, triggerLocation, options with { ExpandedCompletionBehavior = ExpandedCompletionMode.NonExpandedItemsOnly }, cancellationToken).ConfigureAwait(false); - UpdateSessionData(session, sessionData, list, triggerLocation); - return context; + UpdateSessionData(session, sessionData, nonExpandedCompletionList, triggerLocation); + + if (sessionData.IsExclusive) + { + // If the core items are exclusive, we won't ever include expanded items. + // This would cancel expandedItemsTask. + _ = _expandedItemsTaskCancellationSeries.CreateNext(CancellationToken.None); } else { - // Kicking off the task for expanded items, so it runs in parallel with regular providers. - // Otherwise, the computation of unimported items won't start until we return those regular items to editor, - // which combined with our behavior of not showing expanded items until ready (and only adding them during - // completion list refresh) means increased chance that users won't see those items for the first few characters typed. - // This does mean we might do unnecessary work if any regular provider is `exclusive`, but such cases are relatively infrequent - // and we'd like to have expanded items available when they are needed. As these results come back potentially after - // presentation (and sorting) of the non-expanded results, we need these results to come back already sorted. - var expandedItemsTaskCancellationToken = _expandedItemsTaskCancellationSeries.CreateNext(cancellationToken); - var expandedItemsTask = Task.Run(() => GetCompletionContextWorkerAsync(session, document, trigger, triggerLocation, - options with { ExpandedCompletionBehavior = ExpandedCompletionMode.ExpandedItemsOnly, PerformSort = true }, - expandedItemsTaskCancellationToken), - expandedItemsTaskCancellationToken); - - // Now trigger and wait for core providers to return; - var (nonExpandedContext, nonExpandedCompletionList) = await GetCompletionContextWorkerAsync(session, document, trigger, triggerLocation, - options with { ExpandedCompletionBehavior = ExpandedCompletionMode.NonExpandedItemsOnly }, cancellationToken).ConfigureAwait(false); - - UpdateSessionData(session, sessionData, nonExpandedCompletionList, triggerLocation); - - if (sessionData.IsExclusive) - { - // If the core items are exclusive, we won't ever include expanded items. - // This would cancel expandedItemsTask. - _ = _expandedItemsTaskCancellationSeries.CreateNext(CancellationToken.None); - } - else - { - sessionData.ExpandedItemsTask = expandedItemsTask; - } - - AsyncCompletionLogger.LogImportCompletionGetContext(); - return nonExpandedContext; + sessionData.ExpandedItemsTask = expandedItemsTask; } - } - finally - { - AsyncCompletionLogger.LogSourceGetContextTicksDataPoint(totalStopWatch.Elapsed, isCanceled: cancellationToken.IsCancellationRequested); + + AsyncCompletionLogger.LogImportCompletionGetContext(); + return nonExpandedContext; } } + finally + { + AsyncCompletionLogger.LogSourceGetContextTicksDataPoint(totalStopWatch.Elapsed, isCanceled: cancellationToken.IsCancellationRequested); + } + } + + public async Task GetExpandedCompletionContextAsync( + IAsyncCompletionSession session, + AsyncCompletionData.CompletionExpander expander, + AsyncCompletionData.CompletionTrigger initialTrigger, + SnapshotSpan applicableToSpan, + CancellationToken cancellationToken) + { + var sessionData = CompletionSessionData.GetOrCreateSessionData(session); - public async Task GetExpandedCompletionContextAsync( - IAsyncCompletionSession session, - AsyncCompletionData.CompletionExpander expander, - AsyncCompletionData.CompletionTrigger initialTrigger, - SnapshotSpan applicableToSpan, - CancellationToken cancellationToken) + // We only want to provide expanded items for Roslyn's expander + if (!sessionData.IsExclusive && expander == FilterSet.Expander && sessionData.ExpandedItemTriggerLocation.HasValue) { - var sessionData = CompletionSessionData.GetOrCreateSessionData(session); + var initialTriggerLocation = sessionData.ExpandedItemTriggerLocation.Value; + AsyncCompletionLogger.LogExpanderUsage(); - // We only want to provide expanded items for Roslyn's expander - if (!sessionData.IsExclusive && expander == FilterSet.Expander && sessionData.ExpandedItemTriggerLocation.HasValue) + // It's possible we didn't provide expanded items at the beginning of completion session because it was slow even if the feature is enabled. + // ExpandedItemsTask would be available in this case, so we just need to return its result. + if (sessionData.ExpandedItemsTask is not null) { - var initialTriggerLocation = sessionData.ExpandedItemTriggerLocation.Value; - AsyncCompletionLogger.LogExpanderUsage(); - - // It's possible we didn't provide expanded items at the beginning of completion session because it was slow even if the feature is enabled. - // ExpandedItemsTask would be available in this case, so we just need to return its result. - if (sessionData.ExpandedItemsTask is not null) - { - // Make sure the task is removed when returning expanded items, - // so duplicated items won't be added in subsequent list updates. - var task = sessionData.ExpandedItemsTask; - sessionData.ExpandedItemsTask = null; - - var (expandedContext, expandedCompletionList) = await task.ConfigureAwait(false); - UpdateSessionData(session, sessionData, expandedCompletionList, initialTriggerLocation); - return expandedContext; - } + // Make sure the task is removed when returning expanded items, + // so duplicated items won't be added in subsequent list updates. + var task = sessionData.ExpandedItemsTask; + sessionData.ExpandedItemsTask = null; + + var (expandedContext, expandedCompletionList) = await task.ConfigureAwait(false); + UpdateSessionData(session, sessionData, expandedCompletionList, initialTriggerLocation); + return expandedContext; + } - if (sessionData.CombinedSortedList is null) + if (sessionData.CombinedSortedList is null) + { + // We only reach here when expanded items are disabled, but user requested them explicitly via expander. + // In this case, enable expanded items and trigger the completion only for them. + var document = initialTriggerLocation.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document != null) { - // We only reach here when expanded items are disabled, but user requested them explicitly via expander. - // In this case, enable expanded items and trigger the completion only for them. - var document = initialTriggerLocation.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document != null) + // User selected expander explicitly, which means we need to collect and return + // items from unimported namespace (and only those items) regardless of whether it's enabled. + var options = _editorOptionsService.GlobalOptions.GetCompletionOptions(document.Project.Language) with { - // User selected expander explicitly, which means we need to collect and return - // items from unimported namespace (and only those items) regardless of whether it's enabled. - var options = _editorOptionsService.GlobalOptions.GetCompletionOptions(document.Project.Language) with - { - ShowItemsFromUnimportedNamespaces = true, - ExpandedCompletionBehavior = ExpandedCompletionMode.ExpandedItemsOnly - }; - - var (context, completionList) = await GetCompletionContextWorkerAsync(session, document, initialTrigger, initialTriggerLocation, options, cancellationToken).ConfigureAwait(false); - UpdateSessionData(session, sessionData, completionList, initialTriggerLocation); - - return context; - } + ShowItemsFromUnimportedNamespaces = true, + ExpandedCompletionBehavior = ExpandedCompletionMode.ExpandedItemsOnly + }; + + var (context, completionList) = await GetCompletionContextWorkerAsync(session, document, initialTrigger, initialTriggerLocation, options, cancellationToken).ConfigureAwait(false); + UpdateSessionData(session, sessionData, completionList, initialTriggerLocation); + + return context; } } - - return VSCompletionContext.Empty; } - private async Task<(VSCompletionContext, CompletionList)> GetCompletionContextWorkerAsync( - IAsyncCompletionSession session, - Document document, - AsyncCompletionData.CompletionTrigger trigger, - SnapshotPoint triggerLocation, - CompletionOptions options, - CancellationToken cancellationToken) + return VSCompletionContext.Empty; + } + + private async Task<(VSCompletionContext, CompletionList)> GetCompletionContextWorkerAsync( + IAsyncCompletionSession session, + Document document, + AsyncCompletionData.CompletionTrigger trigger, + SnapshotPoint triggerLocation, + CompletionOptions options, + CancellationToken cancellationToken) + { + if (_isDebuggerTextView) { - if (_isDebuggerTextView) + options = options with { - options = options with - { - FilterOutOfScopeLocals = false, - ShowXmlDocCommentCompletion = false, - // Adding import is not allowed in debugger view - CanAddImportStatement = false, - }; - } + FilterOutOfScopeLocals = false, + ShowXmlDocCommentCompletion = false, + // Adding import is not allowed in debugger view + CanAddImportStatement = false, + }; + } - var completionService = document.GetRequiredLanguageService(); - var roslynTrigger = _snippetCompletionTriggeredIndirectly - ? new CompletionTrigger(CompletionTriggerKind.Snippets) - : Helpers.GetRoslynTrigger(trigger, triggerLocation); + var completionService = document.GetRequiredLanguageService(); + var roslynTrigger = _snippetCompletionTriggeredIndirectly + ? new CompletionTrigger(CompletionTriggerKind.Snippets) + : Helpers.GetRoslynTrigger(trigger, triggerLocation); - var completionList = await completionService.GetCompletionsAsync( - document, triggerLocation, options, document.Project.Solution.Options, roslynTrigger, _roles, cancellationToken).ConfigureAwait(false); + var completionList = await completionService.GetCompletionsAsync( + document, triggerLocation, options, document.Project.Solution.Options, roslynTrigger, _roles, cancellationToken).ConfigureAwait(false); - var filterSet = new FilterSet(document.Project.Language is LanguageNames.CSharp or LanguageNames.VisualBasic); - var completionItemList = session.CreateCompletionList( - completionList.ItemsList.Select(i => Convert(document, i, filterSet, triggerLocation, cancellationToken))); + var filterSet = new FilterSet(document.Project.Language is LanguageNames.CSharp or LanguageNames.VisualBasic); + var completionItemList = session.CreateCompletionList( + completionList.ItemsList.Select(i => Convert(document, i, filterSet, triggerLocation, cancellationToken))); - var filters = filterSet.GetFilterStatesInSet(); + var filters = filterSet.GetFilterStatesInSet(); - if (completionList.SuggestionModeItem is null) - return (new(completionItemList, suggestionItemOptions: null, selectionHint: AsyncCompletionData.InitialSelectionHint.RegularSelection, filters, isIncomplete: false, null), completionList); + if (completionList.SuggestionModeItem is null) + return (new(completionItemList, suggestionItemOptions: null, selectionHint: AsyncCompletionData.InitialSelectionHint.RegularSelection, filters, isIncomplete: false, null), completionList); - var suggestionItemOptions = new AsyncCompletionData.SuggestionItemOptions( - completionList.SuggestionModeItem.DisplayText, - completionList.SuggestionModeItem.TryGetProperty(CommonCompletionItem.DescriptionProperty, out var description) ? description : string.Empty); + var suggestionItemOptions = new AsyncCompletionData.SuggestionItemOptions( + completionList.SuggestionModeItem.DisplayText, + completionList.SuggestionModeItem.TryGetProperty(CommonCompletionItem.DescriptionProperty, out var description) ? description : string.Empty); - return (new(completionItemList, suggestionItemOptions, selectionHint: AsyncCompletionData.InitialSelectionHint.SoftSelection, filters, isIncomplete: false, null), completionList); - } + return (new(completionItemList, suggestionItemOptions, selectionHint: AsyncCompletionData.InitialSelectionHint.SoftSelection, filters, isIncomplete: false, null), completionList); + } - private static void UpdateSessionData(IAsyncCompletionSession session, CompletionSessionData sessionData, CompletionList completionList, SnapshotPoint triggerLocation) - { - sessionData.IsExclusive |= completionList.IsExclusive; + private static void UpdateSessionData(IAsyncCompletionSession session, CompletionSessionData sessionData, CompletionList completionList, SnapshotPoint triggerLocation) + { + sessionData.IsExclusive |= completionList.IsExclusive; - // Store around the span this completion list applies to. We'll use this later - // to pass this value in when we're committing a completion list item. - // It's OK to overwrite this value when expanded items are requested. - sessionData.CompletionListSpan = completionList.Span; + // Store around the span this completion list applies to. We'll use this later + // to pass this value in when we're committing a completion list item. + // It's OK to overwrite this value when expanded items are requested. + sessionData.CompletionListSpan = completionList.Span; - // This is a code supporting original completion scenarios: - // Controller.Session_ComputeModel: if completionList.SuggestionModeItem != null, then suggestionMode = true - // If there are suggestionItemOptions, then later HandleNormalFiltering should set selection to SoftSelection. - sessionData.HasSuggestionItemOptions |= completionList.SuggestionModeItem != null; + // This is a code supporting original completion scenarios: + // Controller.Session_ComputeModel: if completionList.SuggestionModeItem != null, then suggestionMode = true + // If there are suggestionItemOptions, then later HandleNormalFiltering should set selection to SoftSelection. + sessionData.HasSuggestionItemOptions |= completionList.SuggestionModeItem != null; - var excludedCommitCharactersFromList = GetExcludedCommitCharacters(completionList.ItemsList); - if (session.Properties.TryGetProperty(ExcludedCommitCharactersMap, out MultiDictionary excludedCommitCharactersMap)) + var excludedCommitCharactersFromList = GetExcludedCommitCharacters(completionList.ItemsList); + if (session.Properties.TryGetProperty(ExcludedCommitCharactersMap, out MultiDictionary excludedCommitCharactersMap)) + { + foreach (var kvp in excludedCommitCharactersFromList) { - foreach (var kvp in excludedCommitCharactersFromList) + foreach (var item in kvp.Value) { - foreach (var item in kvp.Value) - { - excludedCommitCharactersMap.Add(kvp.Key, item); - } + excludedCommitCharactersMap.Add(kvp.Key, item); } } - else - { - excludedCommitCharactersMap = excludedCommitCharactersFromList; - } + } + else + { + excludedCommitCharactersMap = excludedCommitCharactersFromList; + } - session.Properties[ExcludedCommitCharactersMap] = excludedCommitCharactersMap; - session.Properties[ExcludedCommitCharacters] = excludedCommitCharactersMap.Keys.ToImmutableArray(); + session.Properties[ExcludedCommitCharactersMap] = excludedCommitCharactersMap; + session.Properties[ExcludedCommitCharacters] = excludedCommitCharactersMap.Keys.ToImmutableArray(); - // We need to remember the trigger location for when a completion service claims expanded items are available - // since the initial trigger we are able to get from IAsyncCompletionSession might not be the same (e.g. in projection scenarios) - // so when they are requested via expander later, we can retrieve it. - // Technically we should save the trigger location for each individual service that made such claim, but in reality only Roslyn's - // completion service uses expander, so we can get away with not making such distinction. - if (!sessionData.ExpandedItemTriggerLocation.HasValue) - { - sessionData.ExpandedItemTriggerLocation = triggerLocation; - } + // We need to remember the trigger location for when a completion service claims expanded items are available + // since the initial trigger we are able to get from IAsyncCompletionSession might not be the same (e.g. in projection scenarios) + // so when they are requested via expander later, we can retrieve it. + // Technically we should save the trigger location for each individual service that made such claim, but in reality only Roslyn's + // completion service uses expander, so we can get away with not making such distinction. + if (!sessionData.ExpandedItemTriggerLocation.HasValue) + { + sessionData.ExpandedItemTriggerLocation = triggerLocation; } + } - public async Task GetDescriptionAsync(IAsyncCompletionSession session, VSCompletionItem item, CancellationToken cancellationToken) - { - if (session is null) - throw new ArgumentNullException(nameof(session)); - if (item is null) - throw new ArgumentNullException(nameof(item)); + public async Task GetDescriptionAsync(IAsyncCompletionSession session, VSCompletionItem item, CancellationToken cancellationToken) + { + if (session is null) + throw new ArgumentNullException(nameof(session)); + if (item is null) + throw new ArgumentNullException(nameof(item)); - if (!CompletionItemData.TryGetData(item, out var itemData) || !itemData.TriggerLocation.HasValue) - return null; + if (!CompletionItemData.TryGetData(item, out var itemData) || !itemData.TriggerLocation.HasValue) + return null; - var snapshot = itemData.TriggerLocation.Value.Snapshot; - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return null; + var snapshot = itemData.TriggerLocation.Value.Snapshot; + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return null; - var service = document.GetLanguageService(); - if (service == null) - return null; + var service = document.GetLanguageService(); + if (service == null) + return null; - var completionOptions = _editorOptionsService.GlobalOptions.GetCompletionOptions(document.Project.Language); - var displayOptions = _editorOptionsService.GlobalOptions.GetSymbolDescriptionOptions(document.Project.Language); - var description = await service.GetDescriptionAsync(document, itemData.RoslynItem, completionOptions, displayOptions, cancellationToken).ConfigureAwait(false); - if (description == null) - return null; + var completionOptions = _editorOptionsService.GlobalOptions.GetCompletionOptions(document.Project.Language); + var displayOptions = _editorOptionsService.GlobalOptions.GetSymbolDescriptionOptions(document.Project.Language); + var description = await service.GetDescriptionAsync(document, itemData.RoslynItem, completionOptions, displayOptions, cancellationToken).ConfigureAwait(false); + if (description == null) + return null; - var lineFormattingOptions = snapshot.TextBuffer.GetLineFormattingOptions(_editorOptionsService, explicitFormat: false); - var context = new IntellisenseQuickInfoBuilderContext( - document, displayOptions.ClassificationOptions, lineFormattingOptions, _threadingContext, _operationExecutor, _asyncListener, _streamingPresenter); + var lineFormattingOptions = snapshot.TextBuffer.GetLineFormattingOptions(_editorOptionsService, explicitFormat: false); + var context = new IntellisenseQuickInfoBuilderContext( + document, displayOptions.ClassificationOptions, lineFormattingOptions, _threadingContext, _operationExecutor, _asyncListener, _streamingPresenter); - var elements = IntelliSense.Helpers.BuildInteractiveTextElements(description.TaggedParts, context).ToArray(); - if (elements.Length == 0) - return new ClassifiedTextElement(); + var elements = IntelliSense.Helpers.BuildInteractiveTextElements(description.TaggedParts, context).ToArray(); + if (elements.Length == 0) + return new ClassifiedTextElement(); - if (elements.Length == 1) - return elements[0]; + if (elements.Length == 1) + return elements[0]; - return new ContainerElement(ContainerElementStyle.Stacked | ContainerElementStyle.VerticalPadding, elements); - } + return new ContainerElement(ContainerElementStyle.Stacked | ContainerElementStyle.VerticalPadding, elements); + } + + /// + /// We'd like to cache VS Completion item directly to avoid allocation completely. However it holds references + /// to transient objects, which would cause memory leak (among other potential issues) if cached. + /// So as a compromise, we cache data that can be calculated from Roslyn completion item to avoid repeated + /// calculation cost for cached Roslyn completion items. + /// FilterSetData is the bit vector value from the FilterSet of this item. + /// + private readonly record struct VSCompletionItemData( + string DisplayText, + ImageElement Icon, + ImmutableArray Filters, + int FilterSetData, + ImmutableArray AttributeIcons, + string InsertionText); + + private VSCompletionItem Convert( + Document document, + RoslynCompletionItem roslynItem, + FilterSet filterSet, + SnapshotPoint initialTriggerLocation, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - /// - /// We'd like to cache VS Completion item directly to avoid allocation completely. However it holds references - /// to transient objects, which would cause memory leak (among other potential issues) if cached. - /// So as a compromise, we cache data that can be calculated from Roslyn completion item to avoid repeated - /// calculation cost for cached Roslyn completion items. - /// FilterSetData is the bit vector value from the FilterSet of this item. - /// - private readonly record struct VSCompletionItemData( - string DisplayText, - ImageElement Icon, - ImmutableArray Filters, - int FilterSetData, - ImmutableArray AttributeIcons, - string InsertionText); - - private VSCompletionItem Convert( - Document document, - RoslynCompletionItem roslynItem, - FilterSet filterSet, - SnapshotPoint initialTriggerLocation, - CancellationToken cancellationToken) + VSCompletionItemData itemData; + if (roslynItem.Flags.IsCached() && s_roslynItemToVsItemData.TryGetValue(roslynItem, out var boxedItemData)) + { + itemData = boxedItemData.Value; + filterSet.CombineData(itemData.FilterSetData); + } + else { - cancellationToken.ThrowIfCancellationRequested(); + var imageId = roslynItem.Tags.GetFirstGlyph().GetImageId(); + var (filters, filterSetData) = filterSet.GetFiltersAndAddToSet(roslynItem); - VSCompletionItemData itemData; - if (roslynItem.Flags.IsCached() && s_roslynItemToVsItemData.TryGetValue(roslynItem, out var boxedItemData)) + // roslynItem generated by providers can contain an insertionText in a property bag. + // We will not use it but other providers may need it. + // We actually will calculate the insertion text once again when called TryCommit. + if (!SymbolCompletionItem.TryGetInsertionText(roslynItem, out var insertionText)) { - itemData = boxedItemData.Value; - filterSet.CombineData(itemData.FilterSetData); + insertionText = roslynItem.DisplayText; } - else - { - var imageId = roslynItem.Tags.GetFirstGlyph().GetImageId(); - var (filters, filterSetData) = filterSet.GetFiltersAndAddToSet(roslynItem); - - // roslynItem generated by providers can contain an insertionText in a property bag. - // We will not use it but other providers may need it. - // We actually will calculate the insertion text once again when called TryCommit. - if (!SymbolCompletionItem.TryGetInsertionText(roslynItem, out var insertionText)) - { - insertionText = roslynItem.DisplayText; - } - var supportedPlatforms = SymbolCompletionItem.GetSupportedPlatforms(roslynItem, document.Project.Solution); - var attributeImages = supportedPlatforms != null ? s_warningImageAttributeImagesArray : []; + var supportedPlatforms = SymbolCompletionItem.GetSupportedPlatforms(roslynItem, document.Project.Solution); + var attributeImages = supportedPlatforms != null ? s_warningImageAttributeImagesArray : []; - itemData = new VSCompletionItemData( - DisplayText: roslynItem.GetEntireDisplayText(), - Icon: new ImageElement(new ImageId(imageId.Guid, imageId.Id), roslynItem.DisplayText), - Filters: filters, - FilterSetData: filterSetData, - AttributeIcons: attributeImages, - InsertionText: insertionText); + itemData = new VSCompletionItemData( + DisplayText: roslynItem.GetEntireDisplayText(), + Icon: new ImageElement(new ImageId(imageId.Guid, imageId.Id), roslynItem.DisplayText), + Filters: filters, + FilterSetData: filterSetData, + AttributeIcons: attributeImages, + InsertionText: insertionText); - // It doesn't make sense to cache VS item data for those Roslyn items created from scratch for each session, - // since CWT uses object identity for comparison. - if (roslynItem.Flags.IsCached()) - { - s_roslynItemToVsItemData.Add(roslynItem, new StrongBox(itemData)); - } + // It doesn't make sense to cache VS item data for those Roslyn items created from scratch for each session, + // since CWT uses object identity for comparison. + if (roslynItem.Flags.IsCached()) + { + s_roslynItemToVsItemData.Add(roslynItem, new StrongBox(itemData)); } - - var item = new VSCompletionItem( - displayText: itemData.DisplayText, - source: this, - icon: itemData.Icon, - filters: itemData.Filters, - suffix: roslynItem.InlineDescription, // InlineDescription will be right-aligned in the selection popup - insertText: itemData.InsertionText, - sortText: roslynItem.SortText, - filterText: roslynItem.FilterText, - automationText: roslynItem.AutomationText ?? roslynItem.DisplayText, - attributeIcons: itemData.AttributeIcons); - - CompletionItemData.AddData(item, roslynItem, initialTriggerLocation); - return item; } - /// - /// Build a map from added filter characters to corresponding items. - /// CommitManager needs this information to decide whether it should commit selected item. - /// - private static MultiDictionary GetExcludedCommitCharacters(IReadOnlyList roslynItems) + var item = new VSCompletionItem( + displayText: itemData.DisplayText, + source: this, + icon: itemData.Icon, + filters: itemData.Filters, + suffix: roslynItem.InlineDescription, // InlineDescription will be right-aligned in the selection popup + insertText: itemData.InsertionText, + sortText: roslynItem.SortText, + filterText: roslynItem.FilterText, + automationText: roslynItem.AutomationText ?? roslynItem.DisplayText, + attributeIcons: itemData.AttributeIcons); + + CompletionItemData.AddData(item, roslynItem, initialTriggerLocation); + return item; + } + + /// + /// Build a map from added filter characters to corresponding items. + /// CommitManager needs this information to decide whether it should commit selected item. + /// + private static MultiDictionary GetExcludedCommitCharacters(IReadOnlyList roslynItems) + { + var map = new MultiDictionary(); + foreach (var roslynItem in roslynItems) { - var map = new MultiDictionary(); - foreach (var roslynItem in roslynItems) + foreach (var rule in roslynItem.Rules.FilterCharacterRules) { - foreach (var rule in roslynItem.Rules.FilterCharacterRules) + if (rule.Kind == CharacterSetModificationKind.Add) { - if (rule.Kind == CharacterSetModificationKind.Add) + foreach (var c in rule.Characters) { - foreach (var c in rule.Characters) - { - map.Add(c, roslynItem); - } + map.Add(c, roslynItem); } } } - - return map; } - internal static bool QuestionMarkIsPrecededByIdentifierAndWhitespace( - SourceText text, int questionPosition, ISyntaxFactsService syntaxFacts) - { - var startOfLine = text.Lines.GetLineFromPosition(questionPosition).Start; + return map; + } - // First, skip all the whitespace. - var current = startOfLine; - while (current < questionPosition && char.IsWhiteSpace(text[current])) - { - current++; - } + internal static bool QuestionMarkIsPrecededByIdentifierAndWhitespace( + SourceText text, int questionPosition, ISyntaxFactsService syntaxFacts) + { + var startOfLine = text.Lines.GetLineFromPosition(questionPosition).Start; - if (current < questionPosition && syntaxFacts.IsIdentifierStartCharacter(text[current])) - { - current++; - } - else - { - return false; - } + // First, skip all the whitespace. + var current = startOfLine; + while (current < questionPosition && char.IsWhiteSpace(text[current])) + { + current++; + } - while (current < questionPosition && syntaxFacts.IsIdentifierPartCharacter(text[current])) - { - current++; - } + if (current < questionPosition && syntaxFacts.IsIdentifierStartCharacter(text[current])) + { + current++; + } + else + { + return false; + } - return current == questionPosition; + while (current < questionPosition && syntaxFacts.IsIdentifierPartCharacter(text[current])) + { + current++; } + + return current == questionPosition; } } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSourceProvider.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSourceProvider.cs index 83cac992c401f..b24cf666e27ed 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSourceProvider.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSourceProvider.cs @@ -14,32 +14,31 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +[Export(typeof(IAsyncCompletionSourceProvider))] +[Name("Roslyn Completion Source Provider")] +[ContentType(ContentTypeNames.RoslynContentType)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CompletionSourceProvider( + IThreadingContext threadingContext, + IUIThreadOperationExecutor operationExecutor, + IAsynchronousOperationListenerProvider listenerProvider, + Lazy streamingPresenter, + EditorOptionsService editorOptionsService) : IAsyncCompletionSourceProvider { - [Export(typeof(IAsyncCompletionSourceProvider))] - [Name("Roslyn Completion Source Provider")] - [ContentType(ContentTypeNames.RoslynContentType)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CompletionSourceProvider( - IThreadingContext threadingContext, - IUIThreadOperationExecutor operationExecutor, - IAsynchronousOperationListenerProvider listenerProvider, - Lazy streamingPresenter, - EditorOptionsService editorOptionsService) : IAsyncCompletionSourceProvider - { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IUIThreadOperationExecutor _operationExecutor = operationExecutor; - private readonly Lazy _streamingPresenter = streamingPresenter; - private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.CompletionSet); - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IUIThreadOperationExecutor _operationExecutor = operationExecutor; + private readonly Lazy _streamingPresenter = streamingPresenter; + private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.CompletionSet); + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - public IAsyncCompletionSource? GetOrCreate(ITextView textView) - { - if (textView.IsInLspEditorContext()) - return null; + public IAsyncCompletionSource? GetOrCreate(ITextView textView) + { + if (textView.IsInLspEditorContext()) + return null; - return new CompletionSource(textView, _streamingPresenter, _threadingContext, _operationExecutor, _listener, _editorOptionsService); - } + return new CompletionSource(textView, _streamingPresenter, _threadingContext, _operationExecutor, _listener, _editorOptionsService); } } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/FilterSet.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/FilterSet.cs index 669b2d9b53a33..ebc2953cab5e5 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/FilterSet.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/FilterSet.cs @@ -16,220 +16,219 @@ using Microsoft.VisualStudio.Text.Adornments; using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +/// +/// Provides an efficient way to compute a set of completion filters associated with a collection of completion items. +/// Presence of expander and filter in the set have different meanings. Set contains a filter means the filter is +/// available but unselected, whereas it means available and selected for an expander. Note that even though VS supports +/// having multiple expanders, we only support one. +/// +internal sealed class FilterSet(bool supportExpander) { - /// - /// Provides an efficient way to compute a set of completion filters associated with a collection of completion items. - /// Presence of expander and filter in the set have different meanings. Set contains a filter means the filter is - /// available but unselected, whereas it means available and selected for an expander. Note that even though VS supports - /// having multiple expanders, we only support one. - /// - internal sealed class FilterSet(bool supportExpander) + // Cache all the VS completion filters which essentially make them singletons. + // Need to map item tags such as Class, Interface, Local, Enum to filter buttons. + // There can be tags mapping to the same button: + // Local -> Locals and Parameters, Parameter -> Locals and Parameters. + private static readonly ImmutableDictionary s_filterMap; + + // Distinct list of all filters. + // Need to iterate over a distinct list of filters + // to create a filter list covering a completion session. + private static readonly ImmutableArray s_filters; + + private BitVector32 _vector = new BitVector32(); + private static readonly int s_expanderMask; + private readonly bool _supportExpander = supportExpander; + + public static readonly CompletionFilter NamespaceFilter; + public static readonly CompletionFilter ClassFilter; + public static readonly CompletionFilter ModuleFilter; + public static readonly CompletionFilter StructureFilter; + public static readonly CompletionFilter InterfaceFilter; + public static readonly CompletionFilter EnumFilter; + public static readonly CompletionFilter EnumMemberFilter; + public static readonly CompletionFilter DelegateFilter; + public static readonly CompletionFilter ConstantFilter; + public static readonly CompletionFilter FieldFilter; + public static readonly CompletionFilter EventFilter; + public static readonly CompletionFilter PropertyFilter; + public static readonly CompletionFilter MethodFilter; + public static readonly CompletionFilter ExtensionMethodFilter; + public static readonly CompletionFilter OperatorFilter; + public static readonly CompletionFilter LocalAndParameterFilter; + public static readonly CompletionFilter KeywordFilter; + public static readonly CompletionFilter SnippetFilter; + public static readonly CompletionFilter TargetTypedFilter; + + public static readonly CompletionExpander Expander; + + static FilterSet() { - // Cache all the VS completion filters which essentially make them singletons. - // Need to map item tags such as Class, Interface, Local, Enum to filter buttons. - // There can be tags mapping to the same button: - // Local -> Locals and Parameters, Parameter -> Locals and Parameters. - private static readonly ImmutableDictionary s_filterMap; - - // Distinct list of all filters. - // Need to iterate over a distinct list of filters - // to create a filter list covering a completion session. - private static readonly ImmutableArray s_filters; - - private BitVector32 _vector = new BitVector32(); - private static readonly int s_expanderMask; - private readonly bool _supportExpander = supportExpander; - - public static readonly CompletionFilter NamespaceFilter; - public static readonly CompletionFilter ClassFilter; - public static readonly CompletionFilter ModuleFilter; - public static readonly CompletionFilter StructureFilter; - public static readonly CompletionFilter InterfaceFilter; - public static readonly CompletionFilter EnumFilter; - public static readonly CompletionFilter EnumMemberFilter; - public static readonly CompletionFilter DelegateFilter; - public static readonly CompletionFilter ConstantFilter; - public static readonly CompletionFilter FieldFilter; - public static readonly CompletionFilter EventFilter; - public static readonly CompletionFilter PropertyFilter; - public static readonly CompletionFilter MethodFilter; - public static readonly CompletionFilter ExtensionMethodFilter; - public static readonly CompletionFilter OperatorFilter; - public static readonly CompletionFilter LocalAndParameterFilter; - public static readonly CompletionFilter KeywordFilter; - public static readonly CompletionFilter SnippetFilter; - public static readonly CompletionFilter TargetTypedFilter; - - public static readonly CompletionExpander Expander; - - static FilterSet() + var mapBuilder = ImmutableDictionary.CreateBuilder(); + var arrayBuilder = ImmutableArray.CreateBuilder(); + + var previousMask = 0; + + // Filters will show up in the VS completion list in the same order. 'a' is used as access key for expander button. + NamespaceFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Namespaces, 'n', WellKnownTags.Namespace); + ClassFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Classes, 'c', WellKnownTags.Class); + ModuleFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Modules, 'u', WellKnownTags.Module); + StructureFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Structures, 's', WellKnownTags.Structure); + InterfaceFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Interfaces, 'i', WellKnownTags.Interface); + EnumFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Enums, 'e', WellKnownTags.Enum); + EnumMemberFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Enum_members, 'b', WellKnownTags.EnumMember); + DelegateFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Delegates, 'd', WellKnownTags.Delegate); + ConstantFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Constants, 'o', WellKnownTags.Constant); + FieldFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Fields, 'f', WellKnownTags.Field); + EventFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Events, 'v', WellKnownTags.Event); + PropertyFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Properties, 'p', WellKnownTags.Property); + MethodFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Methods, 'm', WellKnownTags.Method); + ExtensionMethodFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Extension_methods, 'x', WellKnownTags.ExtensionMethod); + OperatorFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Operators, 'r', WellKnownTags.Operator); + LocalAndParameterFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Locals_and_parameters, 'l', WellKnownTags.Local, WellKnownTags.Parameter); + KeywordFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Keywords, 'k', WellKnownTags.Keyword); + SnippetFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Snippets, 't', WellKnownTags.Snippet); + TargetTypedFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Target_type_matches, 'j', WellKnownTags.TargetTypeMatch); + + s_filterMap = mapBuilder.ToImmutable(); + s_filters = arrayBuilder.ToImmutable(); + + s_expanderMask = BitVector32.CreateMask(previousMask); + + var addImageId = Shared.Extensions.GlyphExtensions.GetImageCatalogImageId(KnownImageIds.ExpandScope); + Expander = new CompletionExpander( + EditorFeaturesResources.Expander_display_text, + accessKey: "a", + new ImageElement(addImageId, EditorFeaturesResources.Expander_image_element)); + + CompletionFilter CreateCompletionFilterAndAddToBuilder(string displayText, char accessKey, params string[] tags) { - var mapBuilder = ImmutableDictionary.CreateBuilder(); - var arrayBuilder = ImmutableArray.CreateBuilder(); - - var previousMask = 0; - - // Filters will show up in the VS completion list in the same order. 'a' is used as access key for expander button. - NamespaceFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Namespaces, 'n', WellKnownTags.Namespace); - ClassFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Classes, 'c', WellKnownTags.Class); - ModuleFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Modules, 'u', WellKnownTags.Module); - StructureFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Structures, 's', WellKnownTags.Structure); - InterfaceFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Interfaces, 'i', WellKnownTags.Interface); - EnumFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Enums, 'e', WellKnownTags.Enum); - EnumMemberFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Enum_members, 'b', WellKnownTags.EnumMember); - DelegateFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Delegates, 'd', WellKnownTags.Delegate); - ConstantFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Constants, 'o', WellKnownTags.Constant); - FieldFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Fields, 'f', WellKnownTags.Field); - EventFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Events, 'v', WellKnownTags.Event); - PropertyFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Properties, 'p', WellKnownTags.Property); - MethodFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Methods, 'm', WellKnownTags.Method); - ExtensionMethodFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Extension_methods, 'x', WellKnownTags.ExtensionMethod); - OperatorFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Operators, 'r', WellKnownTags.Operator); - LocalAndParameterFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Locals_and_parameters, 'l', WellKnownTags.Local, WellKnownTags.Parameter); - KeywordFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Keywords, 'k', WellKnownTags.Keyword); - SnippetFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Snippets, 't', WellKnownTags.Snippet); - TargetTypedFilter = CreateCompletionFilterAndAddToBuilder(FeaturesResources.Target_type_matches, 'j', WellKnownTags.TargetTypeMatch); - - s_filterMap = mapBuilder.ToImmutable(); - s_filters = arrayBuilder.ToImmutable(); - - s_expanderMask = BitVector32.CreateMask(previousMask); - - var addImageId = Shared.Extensions.GlyphExtensions.GetImageCatalogImageId(KnownImageIds.ExpandScope); - Expander = new CompletionExpander( - EditorFeaturesResources.Expander_display_text, - accessKey: "a", - new ImageElement(addImageId, EditorFeaturesResources.Expander_image_element)); - - CompletionFilter CreateCompletionFilterAndAddToBuilder(string displayText, char accessKey, params string[] tags) - { - var filter = CreateCompletionFilter(displayText, tags, accessKey); - previousMask = BitVector32.CreateMask(previousMask); + var filter = CreateCompletionFilter(displayText, tags, accessKey); + previousMask = BitVector32.CreateMask(previousMask); - var filterWithMask = new FilterWithMask(filter, previousMask); - arrayBuilder.Add(filterWithMask); + var filterWithMask = new FilterWithMask(filter, previousMask); + arrayBuilder.Add(filterWithMask); - foreach (var tag in tags) - { - mapBuilder.Add(tag, filterWithMask); - } - - return filter; + foreach (var tag in tags) + { + mapBuilder.Add(tag, filterWithMask); } + + return filter; } + } + + private static CompletionFilter CreateCompletionFilter( + string displayText, string[] tags, char accessKey) + { + var imageId = tags.ToImmutableArray().GetFirstGlyph().GetImageId(); + return new CompletionFilter( + displayText, + accessKey.ToString(), + new ImageElement(new ImageId(imageId.Guid, imageId.Id), EditorFeaturesResources.Filter_image_element)); + } + + public (ImmutableArray filters, int data) GetFiltersAndAddToSet(RoslynCompletionItem item) + { + var listBuilder = new ArrayBuilder(); + var vectorForSingleItem = new BitVector32(); - private static CompletionFilter CreateCompletionFilter( - string displayText, string[] tags, char accessKey) + if (item.Flags.IsExpanded()) { - var imageId = tags.ToImmutableArray().GetFirstGlyph().GetImageId(); - return new CompletionFilter( - displayText, - accessKey.ToString(), - new ImageElement(new ImageId(imageId.Guid, imageId.Id), EditorFeaturesResources.Filter_image_element)); + Debug.Assert(_supportExpander); + listBuilder.Add(Expander); + vectorForSingleItem[s_expanderMask] = _vector[s_expanderMask] = true; } - public (ImmutableArray filters, int data) GetFiltersAndAddToSet(RoslynCompletionItem item) + foreach (var tag in item.Tags) { - var listBuilder = new ArrayBuilder(); - var vectorForSingleItem = new BitVector32(); - - if (item.Flags.IsExpanded()) + if (s_filterMap.TryGetValue(tag, out var filterWithMask)) { - Debug.Assert(_supportExpander); - listBuilder.Add(Expander); - vectorForSingleItem[s_expanderMask] = _vector[s_expanderMask] = true; + listBuilder.Add(filterWithMask.Filter); + vectorForSingleItem[filterWithMask.Mask] = _vector[filterWithMask.Mask] = true; } + } - foreach (var tag in item.Tags) - { - if (s_filterMap.TryGetValue(tag, out var filterWithMask)) - { - listBuilder.Add(filterWithMask.Filter); - vectorForSingleItem[filterWithMask.Mask] = _vector[filterWithMask.Mask] = true; - } - } + return (listBuilder.ToImmutableAndFree(), vectorForSingleItem.Data); + } - return (listBuilder.ToImmutableAndFree(), vectorForSingleItem.Data); - } + // test only + internal static List GetFilters(RoslynCompletionItem item) + { + var result = new List(); - // test only - internal static List GetFilters(RoslynCompletionItem item) + foreach (var tag in item.Tags) { - var result = new List(); - - foreach (var tag in item.Tags) + if (s_filterMap.TryGetValue(tag, out var filterWithMask)) { - if (s_filterMap.TryGetValue(tag, out var filterWithMask)) - { - result.Add(filterWithMask.Filter); - } + result.Add(filterWithMask.Filter); } - - return result; } - public void CombineData(int filterSetData) - { - _vector[filterSetData] = true; - Debug.Assert(!_vector[s_expanderMask] || (_supportExpander && _vector[s_expanderMask])); - } + return result; + } - public ImmutableArray GetFilterStatesInSet() - { - using var _ = ArrayBuilder.GetInstance(out var builder); + public void CombineData(int filterSetData) + { + _vector[filterSetData] = true; + Debug.Assert(!_vector[s_expanderMask] || (_supportExpander && _vector[s_expanderMask])); + } + + public ImmutableArray GetFilterStatesInSet() + { + using var _ = ArrayBuilder.GetInstance(out var builder); - // We always show expander if supported but its selection state depends on whether it is in the set. - if (_supportExpander) - builder.Add(new CompletionFilterWithState(Expander, isAvailable: true, isSelected: _vector[s_expanderMask])); + // We always show expander if supported but its selection state depends on whether it is in the set. + if (_supportExpander) + builder.Add(new CompletionFilterWithState(Expander, isAvailable: true, isSelected: _vector[s_expanderMask])); - foreach (var filterWithMask in s_filters) + foreach (var filterWithMask in s_filters) + { + if (_vector[filterWithMask.Mask]) { - if (_vector[filterWithMask.Mask]) - { - builder.Add(new CompletionFilterWithState(filterWithMask.Filter, isAvailable: true, isSelected: false)); - } + builder.Add(new CompletionFilterWithState(filterWithMask.Filter, isAvailable: true, isSelected: false)); } - - return builder.ToImmutable(); } - /// - /// Combine two filter lists while preserving the order as defined in . - /// - public static ImmutableArray CombineFilterStates(ImmutableArray filters1, ImmutableArray filters2) - { - using var _1 = PooledDictionary.GetInstance(out var filterStateMap); - AddFilterState(filters1); - AddFilterState(filters2); + return builder.ToImmutable(); + } - using var _2 = ArrayBuilder.GetInstance(out var builder); - if (filterStateMap.TryGetValue(Expander, out var isSelected)) - { - builder.Add(new CompletionFilterWithState(Expander, isAvailable: true, isSelected: isSelected)); - } + /// + /// Combine two filter lists while preserving the order as defined in . + /// + public static ImmutableArray CombineFilterStates(ImmutableArray filters1, ImmutableArray filters2) + { + using var _1 = PooledDictionary.GetInstance(out var filterStateMap); + AddFilterState(filters1); + AddFilterState(filters2); + + using var _2 = ArrayBuilder.GetInstance(out var builder); + if (filterStateMap.TryGetValue(Expander, out var isSelected)) + { + builder.Add(new CompletionFilterWithState(Expander, isAvailable: true, isSelected: isSelected)); + } - // Make sure filters are kept in the relative order of their declaration above. - foreach (var filterWithMask in s_filters) + // Make sure filters are kept in the relative order of their declaration above. + foreach (var filterWithMask in s_filters) + { + if (filterStateMap.TryGetValue(filterWithMask.Filter, out isSelected)) { - if (filterStateMap.TryGetValue(filterWithMask.Filter, out isSelected)) - { - builder.Add(new CompletionFilterWithState(filterWithMask.Filter, isAvailable: true, isSelected: isSelected)); - } + builder.Add(new CompletionFilterWithState(filterWithMask.Filter, isAvailable: true, isSelected: isSelected)); } + } - return builder.ToImmutable(); + return builder.ToImmutable(); - void AddFilterState(ImmutableArray filterStates) + void AddFilterState(ImmutableArray filterStates) + { + foreach (var state in filterStates) { - foreach (var state in filterStates) - { - filterStateMap.TryGetValue(state.Filter, out var isSelected); - filterStateMap[state.Filter] = state.IsSelected || isSelected; - } + filterStateMap.TryGetValue(state.Filter, out var isSelected); + filterStateMap[state.Filter] = state.IsSelected || isSelected; } } - - private readonly record struct FilterWithMask(CompletionFilter Filter, int Mask); } + + private readonly record struct FilterWithMask(CompletionFilter Filter, int Mask); } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/Helpers.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/Helpers.cs index 418ca0db6d86a..de4320d2ca0ab 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/Helpers.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/Helpers.cs @@ -14,166 +14,165 @@ using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem; using RoslynTrigger = Microsoft.CodeAnalysis.Completion.CompletionTrigger; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +internal static class Helpers { - internal static class Helpers + private const string PromotedItemOriginalIndexPropertyName = nameof(PromotedItemOriginalIndexPropertyName); + + /// + /// Add star to display text and store the index of the passed-in item in the original sorted list in + /// so we can retrieve it when needed. + /// + public static RoslynCompletionItem PromoteItem(RoslynCompletionItem item, int index) { - private const string PromotedItemOriginalIndexPropertyName = nameof(PromotedItemOriginalIndexPropertyName); + return item.WithDisplayText(Completion.Utilities.UnicodeStarAndSpace + item.DisplayText) + .AddProperty(PromotedItemOriginalIndexPropertyName, index.ToString()); + } - /// - /// Add star to display text and store the index of the passed-in item in the original sorted list in - /// so we can retrieve it when needed. - /// - public static RoslynCompletionItem PromoteItem(RoslynCompletionItem item, int index) - { - return item.WithDisplayText(Completion.Utilities.UnicodeStarAndSpace + item.DisplayText) - .AddProperty(PromotedItemOriginalIndexPropertyName, index.ToString()); - } + public static RoslynCompletionItem DemoteItem(RoslynCompletionItem item) + { + if (!TryGetOriginalIndexOfPromotedItem(item, out _)) + return item; + + Debug.Assert(item.DisplayText.StartsWith(Completion.Utilities.UnicodeStarAndSpace)); + var newProperties = item.GetProperties().WhereAsArray((kvp, propName) => kvp.Key != propName, PromotedItemOriginalIndexPropertyName); + return item + .WithDisplayText(item.DisplayText[Completion.Utilities.UnicodeStarAndSpace.Length..]) + .WithProperties(newProperties); + } - public static RoslynCompletionItem DemoteItem(RoslynCompletionItem item) + public static bool TryGetOriginalIndexOfPromotedItem(RoslynCompletionItem item, out int originalIndex) + { + if (item.TryGetProperty(PromotedItemOriginalIndexPropertyName, out var indexString)) { - if (!TryGetOriginalIndexOfPromotedItem(item, out _)) - return item; - - Debug.Assert(item.DisplayText.StartsWith(Completion.Utilities.UnicodeStarAndSpace)); - var newProperties = item.GetProperties().WhereAsArray((kvp, propName) => kvp.Key != propName, PromotedItemOriginalIndexPropertyName); - return item - .WithDisplayText(item.DisplayText[Completion.Utilities.UnicodeStarAndSpace.Length..]) - .WithProperties(newProperties); + originalIndex = int.Parse(indexString); + return true; } - public static bool TryGetOriginalIndexOfPromotedItem(RoslynCompletionItem item, out int originalIndex) - { - if (item.TryGetProperty(PromotedItemOriginalIndexPropertyName, out var indexString)) - { - originalIndex = int.Parse(indexString); - return true; - } - - originalIndex = -1; - return false; - } + originalIndex = -1; + return false; + } - /// - /// Attempts to convert VS Completion trigger into Roslyn completion trigger - /// - /// VS completion trigger - /// Character. - /// VS provides Backspace and Delete characters inside the trigger while Roslyn needs the char deleted by the trigger. - /// Therefore, we provide this character separately and use it for Delete and Backspace cases only. - /// We retrieve this character from triggerLocation. - /// - /// Roslyn completion trigger - public static RoslynTrigger GetRoslynTrigger(EditorAsyncCompletionData.CompletionTrigger trigger, SnapshotPoint triggerLocation) + /// + /// Attempts to convert VS Completion trigger into Roslyn completion trigger + /// + /// VS completion trigger + /// Character. + /// VS provides Backspace and Delete characters inside the trigger while Roslyn needs the char deleted by the trigger. + /// Therefore, we provide this character separately and use it for Delete and Backspace cases only. + /// We retrieve this character from triggerLocation. + /// + /// Roslyn completion trigger + public static RoslynTrigger GetRoslynTrigger(EditorAsyncCompletionData.CompletionTrigger trigger, SnapshotPoint triggerLocation) + { + var completionTriggerKind = GetRoslynTriggerKind(trigger.Reason); + switch (completionTriggerKind) { - var completionTriggerKind = GetRoslynTriggerKind(trigger.Reason); - switch (completionTriggerKind) - { - case CompletionTriggerKind.Deletion: - var snapshotBeforeEdit = trigger.ViewSnapshotBeforeTrigger; - char characterRemoved; - if (triggerLocation.Position >= 0 && triggerLocation.Position < snapshotBeforeEdit.Length) - { - // If multiple characters were removed (selection), this finds the first character from the left. - characterRemoved = snapshotBeforeEdit[triggerLocation.Position]; - } - else - { - characterRemoved = (char)0; - } + case CompletionTriggerKind.Deletion: + var snapshotBeforeEdit = trigger.ViewSnapshotBeforeTrigger; + char characterRemoved; + if (triggerLocation.Position >= 0 && triggerLocation.Position < snapshotBeforeEdit.Length) + { + // If multiple characters were removed (selection), this finds the first character from the left. + characterRemoved = snapshotBeforeEdit[triggerLocation.Position]; + } + else + { + characterRemoved = (char)0; + } - return RoslynTrigger.CreateDeletionTrigger(characterRemoved); + return RoslynTrigger.CreateDeletionTrigger(characterRemoved); - case CompletionTriggerKind.Insertion: - return RoslynTrigger.CreateInsertionTrigger(trigger.Character); + case CompletionTriggerKind.Insertion: + return RoslynTrigger.CreateInsertionTrigger(trigger.Character); - default: - return new RoslynTrigger(completionTriggerKind); - } + default: + return new RoslynTrigger(completionTriggerKind); } + } - public static CompletionTriggerKind GetRoslynTriggerKind(EditorAsyncCompletionData.CompletionTriggerReason triggerReason) + public static CompletionTriggerKind GetRoslynTriggerKind(EditorAsyncCompletionData.CompletionTriggerReason triggerReason) + { + return triggerReason switch { - return triggerReason switch - { - EditorAsyncCompletionData.CompletionTriggerReason.InvokeAndCommitIfUnique => CompletionTriggerKind.InvokeAndCommitIfUnique, - EditorAsyncCompletionData.CompletionTriggerReason.Insertion => CompletionTriggerKind.Insertion, - EditorAsyncCompletionData.CompletionTriggerReason.Deletion or EditorAsyncCompletionData.CompletionTriggerReason.Backspace => CompletionTriggerKind.Deletion, - EditorAsyncCompletionData.CompletionTriggerReason.SnippetsMode => CompletionTriggerKind.Snippets, - _ => CompletionTriggerKind.Invoke, - }; - } + EditorAsyncCompletionData.CompletionTriggerReason.InvokeAndCommitIfUnique => CompletionTriggerKind.InvokeAndCommitIfUnique, + EditorAsyncCompletionData.CompletionTriggerReason.Insertion => CompletionTriggerKind.Insertion, + EditorAsyncCompletionData.CompletionTriggerReason.Deletion or EditorAsyncCompletionData.CompletionTriggerReason.Backspace => CompletionTriggerKind.Deletion, + EditorAsyncCompletionData.CompletionTriggerReason.SnippetsMode => CompletionTriggerKind.Snippets, + _ => CompletionTriggerKind.Invoke, + }; + } - public static CompletionFilterReason GetFilterReason(EditorAsyncCompletionData.CompletionTriggerReason triggerReason) + public static CompletionFilterReason GetFilterReason(EditorAsyncCompletionData.CompletionTriggerReason triggerReason) + { + return triggerReason switch { - return triggerReason switch - { - EditorAsyncCompletionData.CompletionTriggerReason.Insertion => CompletionFilterReason.Insertion, - EditorAsyncCompletionData.CompletionTriggerReason.Deletion or EditorAsyncCompletionData.CompletionTriggerReason.Backspace => CompletionFilterReason.Deletion, - _ => CompletionFilterReason.Other, - }; - } + EditorAsyncCompletionData.CompletionTriggerReason.Insertion => CompletionFilterReason.Insertion, + EditorAsyncCompletionData.CompletionTriggerReason.Deletion or EditorAsyncCompletionData.CompletionTriggerReason.Backspace => CompletionFilterReason.Deletion, + _ => CompletionFilterReason.Other, + }; + } - public static bool IsFilterCharacter(RoslynCompletionItem item, char ch, string textTypedSoFar) + public static bool IsFilterCharacter(RoslynCompletionItem item, char ch, string textTypedSoFar) + { + // Exclude standard commit character upfront because TextTypedSoFarMatchesItem can miss them on non-Windows platforms. + if (IsStandardCommitCharacter(ch)) { - // Exclude standard commit character upfront because TextTypedSoFarMatchesItem can miss them on non-Windows platforms. - if (IsStandardCommitCharacter(ch)) - { - return false; - } + return false; + } - // First see if the item has any specific filter rules it wants followed. - foreach (var rule in item.Rules.FilterCharacterRules) + // First see if the item has any specific filter rules it wants followed. + foreach (var rule in item.Rules.FilterCharacterRules) + { + switch (rule.Kind) { - switch (rule.Kind) - { - case CharacterSetModificationKind.Add: - if (rule.Characters.Contains(ch)) - { - return true; - } - - continue; + case CharacterSetModificationKind.Add: + if (rule.Characters.Contains(ch)) + { + return true; + } - case CharacterSetModificationKind.Remove: - if (rule.Characters.Contains(ch)) - { - return false; - } + continue; - continue; + case CharacterSetModificationKind.Remove: + if (rule.Characters.Contains(ch)) + { + return false; + } - case CharacterSetModificationKind.Replace: - return rule.Characters.Contains(ch); - } - } + continue; - // general rule: if the filtering text exactly matches the start of the item then it must be a filter character - if (TextTypedSoFarMatchesItem(item, textTypedSoFar)) - { - return true; + case CharacterSetModificationKind.Replace: + return rule.Characters.Contains(ch); } - - return false; } - public static bool TextTypedSoFarMatchesItem(RoslynCompletionItem item, string textTypedSoFar) + // general rule: if the filtering text exactly matches the start of the item then it must be a filter character + if (TextTypedSoFarMatchesItem(item, textTypedSoFar)) { - if (textTypedSoFar.Length > 0) - { - using var _ = PooledDelegates.GetPooledFunction(static (filterText, pattern) => filterText.StartsWith(pattern, StringComparison.CurrentCultureIgnoreCase), textTypedSoFar, out Func isPrefixMatch); + return true; + } - // Note that StartsWith ignores \0 at the end of textTypedSoFar on VS Mac and Mono. - return item.DisplayText.StartsWith(textTypedSoFar, StringComparison.CurrentCultureIgnoreCase) || - item.HasDifferentFilterText && item.FilterText.StartsWith(textTypedSoFar, StringComparison.CurrentCultureIgnoreCase) || - item.HasAdditionalFilterTexts && item.AdditionalFilterTexts.Any(isPrefixMatch); - } + return false; + } - return false; + public static bool TextTypedSoFarMatchesItem(RoslynCompletionItem item, string textTypedSoFar) + { + if (textTypedSoFar.Length > 0) + { + using var _ = PooledDelegates.GetPooledFunction(static (filterText, pattern) => filterText.StartsWith(pattern, StringComparison.CurrentCultureIgnoreCase), textTypedSoFar, out Func isPrefixMatch); + + // Note that StartsWith ignores \0 at the end of textTypedSoFar on VS Mac and Mono. + return item.DisplayText.StartsWith(textTypedSoFar, StringComparison.CurrentCultureIgnoreCase) || + item.HasDifferentFilterText && item.FilterText.StartsWith(textTypedSoFar, StringComparison.CurrentCultureIgnoreCase) || + item.HasAdditionalFilterTexts && item.AdditionalFilterTexts.Any(isPrefixMatch); } - // Tab, Enter and Null (call invoke commit) are always commit characters. - public static bool IsStandardCommitCharacter(char c) - => c is '\t' or '\n' or '\0'; + return false; } + + // Tab, Enter and Null (call invoke commit) are always commit characters. + public static bool IsStandardCommitCharacter(char c) + => c is '\t' or '\n' or '\0'; } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ILanguageServerSnippetExpander.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ILanguageServerSnippetExpander.cs index bdfe02aa3816b..00ed0f11ceeb3 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ILanguageServerSnippetExpander.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ILanguageServerSnippetExpander.cs @@ -5,10 +5,9 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +internal interface ILanguageServerSnippetExpander { - internal interface ILanguageServerSnippetExpander - { - bool TryExpand(string lspSnippetText, SnapshotSpan snapshotSpan, ITextView textView); - } + bool TryExpand(string lspSnippetText, SnapshotSpan snapshotSpan, ITextView textView); } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManager.CompletionListUpdater.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManager.CompletionListUpdater.cs index 096444c2067ca..23542684ff4b8 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManager.CompletionListUpdater.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManager.CompletionListUpdater.cs @@ -25,990 +25,989 @@ using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem; using VSCompletionItem = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data.CompletionItem; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +internal partial class ItemManager { - internal partial class ItemManager + /// + /// Handles the filtering, sorting and selection of the completion items based on user inputs + /// (e.g. typed characters, selected filters, etc.) + /// + private sealed class CompletionListUpdater { - /// - /// Handles the filtering, sorting and selection of the completion items based on user inputs - /// (e.g. typed characters, selected filters, etc.) - /// - private sealed class CompletionListUpdater + // Index used for selecting suggestion item when in suggestion mode. + private const int SuggestionItemIndex = -1; + + private readonly CompletionSessionData _sessionData; + private readonly AsyncCompletionSessionDataSnapshot _snapshotData; + private readonly RecentItemsManager _recentItemsManager; + + private readonly ITrackingSpan _applicableToSpan; + private readonly bool _hasSuggestedItemOptions; + private readonly string _filterText; + private readonly Document? _document; + private readonly CompletionService? _completionService; + private readonly CompletionRules _completionRules; + private readonly CompletionHelper _completionHelper; + private readonly bool _highlightMatchingPortions; + private readonly bool _showCompletionItemFilters; + + // Used for building MatchResult list in parallel + private readonly object _gate = new(); + + private readonly Action, string, IList> _filterMethod; + + private bool ShouldSelectSuggestionItemWhenNoItemMatchesFilterText + => _snapshotData.DisplaySuggestionItem && _filterText.Length > 0; + + private CompletionTriggerReason InitialTriggerReason => _snapshotData.InitialTrigger.Reason; + private CompletionTriggerReason UpdateTriggerReason => _snapshotData.Trigger.Reason; + + // We might need to handle large amount of items with import completion enabled, so use a dedicated pool to minimize/avoid array allocations + // (especially in LOH). In practice, the size of pool should be 1 because we don't expect UpdateCompletionListAsync to be called concurrently, + // which essentially makes the pooled list a singleton, but we still use ObjectPool for concurrency handling just to be robust. + private static readonly ObjectPool> s_listOfMatchResultPool = new(factory: () => []); + + public CompletionListUpdater( + ITrackingSpan applicableToSpan, + CompletionSessionData sessionData, + AsyncCompletionSessionDataSnapshot snapshotData, + RecentItemsManager recentItemsManager, + IGlobalOptionService globalOptions) { - // Index used for selecting suggestion item when in suggestion mode. - private const int SuggestionItemIndex = -1; - - private readonly CompletionSessionData _sessionData; - private readonly AsyncCompletionSessionDataSnapshot _snapshotData; - private readonly RecentItemsManager _recentItemsManager; - - private readonly ITrackingSpan _applicableToSpan; - private readonly bool _hasSuggestedItemOptions; - private readonly string _filterText; - private readonly Document? _document; - private readonly CompletionService? _completionService; - private readonly CompletionRules _completionRules; - private readonly CompletionHelper _completionHelper; - private readonly bool _highlightMatchingPortions; - private readonly bool _showCompletionItemFilters; - - // Used for building MatchResult list in parallel - private readonly object _gate = new(); - - private readonly Action, string, IList> _filterMethod; - - private bool ShouldSelectSuggestionItemWhenNoItemMatchesFilterText - => _snapshotData.DisplaySuggestionItem && _filterText.Length > 0; - - private CompletionTriggerReason InitialTriggerReason => _snapshotData.InitialTrigger.Reason; - private CompletionTriggerReason UpdateTriggerReason => _snapshotData.Trigger.Reason; - - // We might need to handle large amount of items with import completion enabled, so use a dedicated pool to minimize/avoid array allocations - // (especially in LOH). In practice, the size of pool should be 1 because we don't expect UpdateCompletionListAsync to be called concurrently, - // which essentially makes the pooled list a singleton, but we still use ObjectPool for concurrency handling just to be robust. - private static readonly ObjectPool> s_listOfMatchResultPool = new(factory: () => []); - - public CompletionListUpdater( - ITrackingSpan applicableToSpan, - CompletionSessionData sessionData, - AsyncCompletionSessionDataSnapshot snapshotData, - RecentItemsManager recentItemsManager, - IGlobalOptionService globalOptions) - { - _sessionData = sessionData; - _snapshotData = snapshotData; - _recentItemsManager = recentItemsManager; - - _applicableToSpan = applicableToSpan; - _filterText = applicableToSpan.GetText(_snapshotData.Snapshot); - - _hasSuggestedItemOptions = _sessionData.HasSuggestionItemOptions || _snapshotData.DisplaySuggestionItem; - - // We prefer using the original snapshot, which should always be available from items provided by Roslyn's CompletionSource. - // Only use data.Snapshot in the theoretically possible but rare case when all items we are handling are from some non-Roslyn CompletionSource. - var snapshotForDocument = TryGetInitialTriggerLocation(_snapshotData, out var initialTriggerLocation) - ? initialTriggerLocation.Snapshot - : _snapshotData.Snapshot; - - _document = snapshotForDocument?.TextBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); - if (_document != null) - { - _completionService = _document.GetLanguageService(); - _completionRules = _completionService?.GetRules(globalOptions.GetCompletionOptions(_document.Project.Language)) ?? CompletionRules.Default; + _sessionData = sessionData; + _snapshotData = snapshotData; + _recentItemsManager = recentItemsManager; - // Let us make the completion Helper used for non-Roslyn items case-sensitive. - // We can change this if get requests from partner teams. - _completionHelper = CompletionHelper.GetHelper(_document); - _filterMethod = _completionService == null - ? ((matchResults, text, filteredItemsBuilder) => CompletionService.FilterItems(_completionHelper, matchResults, text, filteredItemsBuilder)) - : ((matchResults, text, filteredItemsBuilder) => _completionService.FilterItems(_document, matchResults, text, filteredItemsBuilder)); + _applicableToSpan = applicableToSpan; + _filterText = applicableToSpan.GetText(_snapshotData.Snapshot); - // Nothing to highlight if user hasn't typed anything yet. - _highlightMatchingPortions = _filterText.Length > 0 - && globalOptions.GetOption(CompletionViewOptionsStorage.HighlightMatchingPortionsOfCompletionListItems, _document.Project.Language); + _hasSuggestedItemOptions = _sessionData.HasSuggestionItemOptions || _snapshotData.DisplaySuggestionItem; - _showCompletionItemFilters = globalOptions.GetOption(CompletionViewOptionsStorage.ShowCompletionItemFilters, _document.Project.Language); - } - else - { - _completionService = null; - _completionRules = CompletionRules.Default; + // We prefer using the original snapshot, which should always be available from items provided by Roslyn's CompletionSource. + // Only use data.Snapshot in the theoretically possible but rare case when all items we are handling are from some non-Roslyn CompletionSource. + var snapshotForDocument = TryGetInitialTriggerLocation(_snapshotData, out var initialTriggerLocation) + ? initialTriggerLocation.Snapshot + : _snapshotData.Snapshot; - // Let us make the completion Helper used for non-Roslyn items case-sensitive. - // We can change this if get requests from partner teams. - _completionHelper = new CompletionHelper(isCaseSensitive: true); - _filterMethod = (matchResults, text, filteredMatchResultsBuilder) => CompletionService.FilterItems(_completionHelper, matchResults, text, filteredMatchResultsBuilder); + _document = snapshotForDocument?.TextBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); + if (_document != null) + { + _completionService = _document.GetLanguageService(); + _completionRules = _completionService?.GetRules(globalOptions.GetCompletionOptions(_document.Project.Language)) ?? CompletionRules.Default; - _highlightMatchingPortions = false; - _showCompletionItemFilters = true; - } - } + // Let us make the completion Helper used for non-Roslyn items case-sensitive. + // We can change this if get requests from partner teams. + _completionHelper = CompletionHelper.GetHelper(_document); + _filterMethod = _completionService == null + ? ((matchResults, text, filteredItemsBuilder) => CompletionService.FilterItems(_completionHelper, matchResults, text, filteredItemsBuilder)) + : ((matchResults, text, filteredItemsBuilder) => _completionService.FilterItems(_document, matchResults, text, filteredItemsBuilder)); + + // Nothing to highlight if user hasn't typed anything yet. + _highlightMatchingPortions = _filterText.Length > 0 + && globalOptions.GetOption(CompletionViewOptionsStorage.HighlightMatchingPortionsOfCompletionListItems, _document.Project.Language); - public async Task UpdateCompletionListAsync(IAsyncCompletionSession session, CancellationToken cancellationToken) + _showCompletionItemFilters = globalOptions.GetOption(CompletionViewOptionsStorage.ShowCompletionItemFilters, _document.Project.Language); + } + else { - if (ShouldDismissCompletionListImmediately()) - return null; + _completionService = null; + _completionRules = CompletionRules.Default; - // Use a dedicated pool to minimize potentially repeated large allocations, - // since the completion list could be long with import completion enabled. - var itemsToBeIncluded = s_listOfMatchResultPool.Allocate(); - var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var threadLocalPatternMatchHelper = new ThreadLocal(() => new PatternMatchHelper(_filterText), trackAllValues: true); + // Let us make the completion Helper used for non-Roslyn items case-sensitive. + // We can change this if get requests from partner teams. + _completionHelper = new CompletionHelper(isCaseSensitive: true); + _filterMethod = (matchResults, text, filteredMatchResultsBuilder) => CompletionService.FilterItems(_completionHelper, matchResults, text, filteredMatchResultsBuilder); - try - { - // Determine the list of items to be included in the completion list. - // This is computed based on the filter text as well as the current - // selection of filters and expander. - await AddCompletionItemsAsync(itemsToBeIncluded, threadLocalPatternMatchHelper, cancellationToken).ConfigureAwait(false); - - // Decide if we want to dismiss an empty completion list based on CompletionRules and filter usage. - if (itemsToBeIncluded.Count == 0) - return HandleAllItemsFilteredOut(); - - // Sort items based on pattern matching result - itemsToBeIncluded.Sort(MatchResult.SortingComparer); - - var highlightAndFilterTask = Task.Run( - () => GetHighlightedListAndUpdatedFilters(session, itemsToBeIncluded, threadLocalPatternMatchHelper, cancellationTokenSource.Token), - cancellationTokenSource.Token); - - // Decide the item to be selected for this completion session. - // The selection is mostly based on how well the item matches with the filter text, but we also need to - // take into consideration for things like CompletionTrigger, MatchPriority, MRU, etc. - var initialSelection = InitialTriggerReason == CompletionTriggerReason.Backspace || InitialTriggerReason == CompletionTriggerReason.Deletion - ? HandleDeletionTrigger(itemsToBeIncluded, cancellationToken) - : HandleNormalFiltering(itemsToBeIncluded, cancellationToken); - - if (!initialSelection.HasValue) - return null; - - // Editor might provide a list of items to us as a suggestion to what to select for this session - // (via IAsyncCompletionDefaultsSource), where the "default" means the "default selection". - // The main scenario for this is to keep the selected item in completion list in sync with the - // suggestion of "Whole-Line Completion" feature, where the default is usually set to the first token - // of the WLC suggestion. - var finalSelection = UpdateSelectionBasedOnSuggestedDefaults(itemsToBeIncluded, initialSelection.Value, cancellationToken); - var (highlightedList, updatedFilters) = await highlightAndFilterTask.ConfigureAwait(false); - - return new FilteredCompletionModel( - items: highlightedList, - finalSelection.SelectedItemIndex, - filters: updatedFilters, - finalSelection.SelectionHint, - centerSelection: true, - finalSelection.UniqueItem); - } - finally - { - cancellationTokenSource.Cancel(); - cancellationTokenSource.Dispose(); + _highlightMatchingPortions = false; + _showCompletionItemFilters = true; + } + } - // Don't call ClearAndFree, which resets the capacity to a default value. - itemsToBeIncluded.Clear(); - s_listOfMatchResultPool.Free(itemsToBeIncluded); + public async Task UpdateCompletionListAsync(IAsyncCompletionSession session, CancellationToken cancellationToken) + { + if (ShouldDismissCompletionListImmediately()) + return null; - // Dispose PatternMatchers - foreach (var helper in threadLocalPatternMatchHelper.Values) - helper.Dispose(); + // Use a dedicated pool to minimize potentially repeated large allocations, + // since the completion list could be long with import completion enabled. + var itemsToBeIncluded = s_listOfMatchResultPool.Allocate(); + var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var threadLocalPatternMatchHelper = new ThreadLocal(() => new PatternMatchHelper(_filterText), trackAllValues: true); - threadLocalPatternMatchHelper.Dispose(); - } + try + { + // Determine the list of items to be included in the completion list. + // This is computed based on the filter text as well as the current + // selection of filters and expander. + await AddCompletionItemsAsync(itemsToBeIncluded, threadLocalPatternMatchHelper, cancellationToken).ConfigureAwait(false); + + // Decide if we want to dismiss an empty completion list based on CompletionRules and filter usage. + if (itemsToBeIncluded.Count == 0) + return HandleAllItemsFilteredOut(); + + // Sort items based on pattern matching result + itemsToBeIncluded.Sort(MatchResult.SortingComparer); + + var highlightAndFilterTask = Task.Run( + () => GetHighlightedListAndUpdatedFilters(session, itemsToBeIncluded, threadLocalPatternMatchHelper, cancellationTokenSource.Token), + cancellationTokenSource.Token); + + // Decide the item to be selected for this completion session. + // The selection is mostly based on how well the item matches with the filter text, but we also need to + // take into consideration for things like CompletionTrigger, MatchPriority, MRU, etc. + var initialSelection = InitialTriggerReason == CompletionTriggerReason.Backspace || InitialTriggerReason == CompletionTriggerReason.Deletion + ? HandleDeletionTrigger(itemsToBeIncluded, cancellationToken) + : HandleNormalFiltering(itemsToBeIncluded, cancellationToken); + + if (!initialSelection.HasValue) + return null; - (CompletionList, ImmutableArray) GetHighlightedListAndUpdatedFilters( - IAsyncCompletionSession session, IReadOnlyList itemsToBeIncluded, ThreadLocal patternMatcherHelper, CancellationToken cancellationToken) - { - var highLightedList = GetHighlightedList(session, patternMatcherHelper.Value!, itemsToBeIncluded, cancellationToken); - var updatedFilters = GetUpdatedFilters(itemsToBeIncluded, cancellationToken); - return (highLightedList, updatedFilters); - } + // Editor might provide a list of items to us as a suggestion to what to select for this session + // (via IAsyncCompletionDefaultsSource), where the "default" means the "default selection". + // The main scenario for this is to keep the selected item in completion list in sync with the + // suggestion of "Whole-Line Completion" feature, where the default is usually set to the first token + // of the WLC suggestion. + var finalSelection = UpdateSelectionBasedOnSuggestedDefaults(itemsToBeIncluded, initialSelection.Value, cancellationToken); + var (highlightedList, updatedFilters) = await highlightAndFilterTask.ConfigureAwait(false); + + return new FilteredCompletionModel( + items: highlightedList, + finalSelection.SelectedItemIndex, + filters: updatedFilters, + finalSelection.SelectionHint, + centerSelection: true, + finalSelection.UniqueItem); } + finally + { + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); - private bool ShouldDismissCompletionListImmediately() - { - // Check if the user is typing a number. If so, only proceed if it's a number - // directly after a . That's because it is actually reasonable for completion - // to be brought up after a and for the user to want to filter completion - // items based on a number that exists in the name of the item. However, when - // we are not after a dot (i.e. we're being brought up after is typed) - // then we don't want to filter things. Consider the user writing: - // - // dim i = - // - // We'll bring up the completion list here (as VB has completion on ). - // If the user then types '3', we don't want to match against Int32. - if (_filterText.Length > 0 && char.IsNumber(_filterText[0]) && !IsAfterDot(_snapshotData.Snapshot, _applicableToSpan)) - { - // Dismiss the session. - return true; - } + // Don't call ClearAndFree, which resets the capacity to a default value. + itemsToBeIncluded.Clear(); + s_listOfMatchResultPool.Free(itemsToBeIncluded); - // DismissIfLastCharacterDeleted should be applied only when started with Insertion, and then Deleted all characters typed. - // This conforms with the original VS 2010 behavior. - if (InitialTriggerReason == CompletionTriggerReason.Insertion && - UpdateTriggerReason == CompletionTriggerReason.Backspace && - _completionRules.DismissIfLastCharacterDeleted && - _filterText.Length == 0) - { - // Dismiss the session - return true; - } + // Dispose PatternMatchers + foreach (var helper in threadLocalPatternMatchHelper.Values) + helper.Dispose(); - return false; + threadLocalPatternMatchHelper.Dispose(); + } + + (CompletionList, ImmutableArray) GetHighlightedListAndUpdatedFilters( + IAsyncCompletionSession session, IReadOnlyList itemsToBeIncluded, ThreadLocal patternMatcherHelper, CancellationToken cancellationToken) + { + var highLightedList = GetHighlightedList(session, patternMatcherHelper.Value!, itemsToBeIncluded, cancellationToken); + var updatedFilters = GetUpdatedFilters(itemsToBeIncluded, cancellationToken); + return (highLightedList, updatedFilters); } + } - private async Task AddCompletionItemsAsync(List list, ThreadLocal threadLocalPatternMatchHelper, CancellationToken cancellationToken) + private bool ShouldDismissCompletionListImmediately() + { + // Check if the user is typing a number. If so, only proceed if it's a number + // directly after a . That's because it is actually reasonable for completion + // to be brought up after a and for the user to want to filter completion + // items based on a number that exists in the name of the item. However, when + // we are not after a dot (i.e. we're being brought up after is typed) + // then we don't want to filter things. Consider the user writing: + // + // dim i = + // + // We'll bring up the completion list here (as VB has completion on ). + // If the user then types '3', we don't want to match against Int32. + if (_filterText.Length > 0 && char.IsNumber(_filterText[0]) && !IsAfterDot(_snapshotData.Snapshot, _applicableToSpan)) { - // Convert initial and update trigger reasons to corresponding Roslyn type so - // we can interact with Roslyn's completion system - var roslynInitialTriggerKind = Helpers.GetRoslynTriggerKind(InitialTriggerReason); - var roslynFilterReason = Helpers.GetFilterReason(UpdateTriggerReason); + // Dismiss the session. + return true; + } + + // DismissIfLastCharacterDeleted should be applied only when started with Insertion, and then Deleted all characters typed. + // This conforms with the original VS 2010 behavior. + if (InitialTriggerReason == CompletionTriggerReason.Insertion && + UpdateTriggerReason == CompletionTriggerReason.Backspace && + _completionRules.DismissIfLastCharacterDeleted && + _filterText.Length == 0) + { + // Dismiss the session + return true; + } - // FilterStateHelper is used to decide whether a given item should be included in the list based on the state of filter/expander buttons. - var filterHelper = new FilterStateHelper(_snapshotData.SelectedFilters); + return false; + } + + private async Task AddCompletionItemsAsync(List list, ThreadLocal threadLocalPatternMatchHelper, CancellationToken cancellationToken) + { + // Convert initial and update trigger reasons to corresponding Roslyn type so + // we can interact with Roslyn's completion system + var roslynInitialTriggerKind = Helpers.GetRoslynTriggerKind(InitialTriggerReason); + var roslynFilterReason = Helpers.GetFilterReason(UpdateTriggerReason); + + // FilterStateHelper is used to decide whether a given item should be included in the list based on the state of filter/expander buttons. + var filterHelper = new FilterStateHelper(_snapshotData.SelectedFilters); + + var includedPreferredItems = new ConcurrentSet(); + var includedDefaults = new ConcurrentDictionary(); - var includedPreferredItems = new ConcurrentSet(); - var includedDefaults = new ConcurrentDictionary(); + // Make sure we are on threadpool thread before running PLinq query to avoid sync waiting on the special high-pri thread of async-completion. + await TaskScheduler.Default; - // Make sure we are on threadpool thread before running PLinq query to avoid sync waiting on the special high-pri thread of async-completion. - await TaskScheduler.Default; + Enumerable.Range(0, _snapshotData.InitialSortedItemList.Count) + .AsParallel() + .WithCancellation(cancellationToken) + .ForAll(CreateMatchResultAndProcessMatchingDefaults); + + PromoteDefaultItemsToPreferredState(); + + void CreateMatchResultAndProcessMatchingDefaults(int index) + { + cancellationToken.ThrowIfCancellationRequested(); + var item = _snapshotData.InitialSortedItemList[index]; - Enumerable.Range(0, _snapshotData.InitialSortedItemList.Count) - .AsParallel() - .WithCancellation(cancellationToken) - .ForAll(CreateMatchResultAndProcessMatchingDefaults); + // All items passed in should contain a CompletionItemData object in the property bag, + // which is guaranteed in `ItemManager.SortCompletionListAsync`. + if (!CompletionItemData.TryGetData(item, out var itemData)) + throw ExceptionUtilities.Unreachable(); - PromoteDefaultItemsToPreferredState(); + if (filterHelper.ShouldBeFilteredOut(item)) + return; - void CreateMatchResultAndProcessMatchingDefaults(int index) + // currentIndex is used to track the index of the VS CompletionItem in the initial sorted list to maintain a map from Roslyn item to VS item. + // It's also used to sort the items by pattern matching results while preserving the original alphabetical order for items with + // same pattern match score since `List.Sort` isn't stable. + if (threadLocalPatternMatchHelper.Value!.TryCreateMatchResult(itemData.RoslynItem, roslynInitialTriggerKind, roslynFilterReason, + _recentItemsManager.GetRecentItemIndex(itemData.RoslynItem), _highlightMatchingPortions, index, out var matchResult)) { - cancellationToken.ThrowIfCancellationRequested(); - var item = _snapshotData.InitialSortedItemList[index]; - - // All items passed in should contain a CompletionItemData object in the property bag, - // which is guaranteed in `ItemManager.SortCompletionListAsync`. - if (!CompletionItemData.TryGetData(item, out var itemData)) - throw ExceptionUtilities.Unreachable(); - - if (filterHelper.ShouldBeFilteredOut(item)) - return; - - // currentIndex is used to track the index of the VS CompletionItem in the initial sorted list to maintain a map from Roslyn item to VS item. - // It's also used to sort the items by pattern matching results while preserving the original alphabetical order for items with - // same pattern match score since `List.Sort` isn't stable. - if (threadLocalPatternMatchHelper.Value!.TryCreateMatchResult(itemData.RoslynItem, roslynInitialTriggerKind, roslynFilterReason, - _recentItemsManager.GetRecentItemIndex(itemData.RoslynItem), _highlightMatchingPortions, index, out var matchResult)) - { - lock (_gate) - list.Add(matchResult); + lock (_gate) + list.Add(matchResult); - if (!_snapshotData.Defaults.IsEmpty) + if (!_snapshotData.Defaults.IsEmpty) + { + // Collect all the preferred items from Pythia and regular items matched defaults provided by WLC. + // We will use this info next to "promote" regular default items to "preferred" state on behalf of + // WLC to provide a coherent IntelliCode completion experience. + if (matchResult.CompletionItem.IsPreferredItem()) { - // Collect all the preferred items from Pythia and regular items matched defaults provided by WLC. - // We will use this info next to "promote" regular default items to "preferred" state on behalf of - // WLC to provide a coherent IntelliCode completion experience. - if (matchResult.CompletionItem.IsPreferredItem()) - { - includedPreferredItems.Add(matchResult.CompletionItem.FilterText); - } - else + includedPreferredItems.Add(matchResult.CompletionItem.FilterText); + } + else + { + if (_snapshotData.Defaults.IndexOf(matchResult.CompletionItem.FilterText) >= 0) { - if (_snapshotData.Defaults.IndexOf(matchResult.CompletionItem.FilterText) >= 0) - { - includedDefaults.TryAdd(matchResult.CompletionItem.FilterText, matchResult); - } + includedDefaults.TryAdd(matchResult.CompletionItem.FilterText, matchResult); } } } } + } - // Go through items matched with defaults. If it doesn't have - // a corresponding preferred items, we will add one that mimic - // the "starred" item from Pythia. - void PromoteDefaultItemsToPreferredState() + // Go through items matched with defaults. If it doesn't have + // a corresponding preferred items, we will add one that mimic + // the "starred" item from Pythia. + void PromoteDefaultItemsToPreferredState() + { + foreach (var includedDefault in includedDefaults.Values) { - foreach (var includedDefault in includedDefaults.Values) - { - var completionItem = includedDefault.CompletionItem; + var completionItem = includedDefault.CompletionItem; - // There a preferred item matches the same default, so no need to promote this. - if (includedPreferredItems.Contains(completionItem.FilterText)) - continue; + // There a preferred item matches the same default, so no need to promote this. + if (includedPreferredItems.Contains(completionItem.FilterText)) + continue; - var defaultIndex = _snapshotData.Defaults.IndexOf(completionItem.FilterText); - var fabricatedIndex = DefaultIndexToFabricatedOriginalSortedIndex(defaultIndex); + var defaultIndex = _snapshotData.Defaults.IndexOf(completionItem.FilterText); + var fabricatedIndex = DefaultIndexToFabricatedOriginalSortedIndex(defaultIndex); - var promotedDefaultItemMatchResult = new MatchResult( - Helpers.PromoteItem(completionItem, includedDefault.IndexInOriginalSortedOrder), - includedDefault.ShouldBeConsideredMatchingFilterText, - includedDefault.PatternMatch, - fabricatedIndex, - matchedAdditionalFilterText: includedDefault.MatchedAdditionalFilterText, - includedDefault.RecentItemIndex); + var promotedDefaultItemMatchResult = new MatchResult( + Helpers.PromoteItem(completionItem, includedDefault.IndexInOriginalSortedOrder), + includedDefault.ShouldBeConsideredMatchingFilterText, + includedDefault.PatternMatch, + fabricatedIndex, + matchedAdditionalFilterText: includedDefault.MatchedAdditionalFilterText, + includedDefault.RecentItemIndex); - list.Add(promotedDefaultItemMatchResult); - } + list.Add(promotedDefaultItemMatchResult); } - - // This ensures promoted items are sorted in the same relative order as in defaults list, as well as before all - // items from initial sorted list (by giving them negative value as index). - // e.g. if Defaults.Length = 3, we set index of Defaults[0] to -3, Defaults[1] to -2, etc. where the last one is -1. - // All other items have original index >= 0. - int DefaultIndexToFabricatedOriginalSortedIndex(int i) - => i - _snapshotData.Defaults.Length; } - private ItemSelection? HandleNormalFiltering(IReadOnlyList matchResults, CancellationToken cancellationToken) + // This ensures promoted items are sorted in the same relative order as in defaults list, as well as before all + // items from initial sorted list (by giving them negative value as index). + // e.g. if Defaults.Length = 3, we set index of Defaults[0] to -3, Defaults[1] to -2, etc. where the last one is -1. + // All other items have original index >= 0. + int DefaultIndexToFabricatedOriginalSortedIndex(int i) + => i - _snapshotData.Defaults.Length; + } + + private ItemSelection? HandleNormalFiltering(IReadOnlyList matchResults, CancellationToken cancellationToken) + { + Debug.Assert(matchResults.Count > 0); + var filteredMatchResultsBuilder = s_listOfMatchResultPool.Allocate(); + + try { - Debug.Assert(matchResults.Count > 0); - var filteredMatchResultsBuilder = s_listOfMatchResultPool.Allocate(); + // Not deletion. Defer to the language to decide which item it thinks best + // matches the text typed so far. + _filterMethod(matchResults, _filterText, filteredMatchResultsBuilder); + + // Ask the language to determine which of the *matched* items it wants to select. + int selectedItemIndex; + VSCompletionItem? uniqueItem = null; + MatchResult bestOrFirstMatchResult; + if (filteredMatchResultsBuilder.Count == 0) + { + // When we are in suggestion mode and there's nothing in the list matches what user has typed in any ways, + // we should select the SuggestionItem instead. + if (ShouldSelectSuggestionItemWhenNoItemMatchesFilterText) + return new ItemSelection(SelectedItemIndex: SuggestionItemIndex, SelectionHint: UpdateSelectionHint.SoftSelected, UniqueItem: null); - try - { - // Not deletion. Defer to the language to decide which item it thinks best - // matches the text typed so far. - _filterMethod(matchResults, _filterText, filteredMatchResultsBuilder); - - // Ask the language to determine which of the *matched* items it wants to select. - int selectedItemIndex; - VSCompletionItem? uniqueItem = null; - MatchResult bestOrFirstMatchResult; - if (filteredMatchResultsBuilder.Count == 0) - { - // When we are in suggestion mode and there's nothing in the list matches what user has typed in any ways, - // we should select the SuggestionItem instead. - if (ShouldSelectSuggestionItemWhenNoItemMatchesFilterText) - return new ItemSelection(SelectedItemIndex: SuggestionItemIndex, SelectionHint: UpdateSelectionHint.SoftSelected, UniqueItem: null); + // We do not have matches: pick the one with longest common prefix. + // If we can't find such an item, just return the first item from the list. + selectedItemIndex = 0; + bestOrFirstMatchResult = matchResults[0]; - // We do not have matches: pick the one with longest common prefix. - // If we can't find such an item, just return the first item from the list. - selectedItemIndex = 0; - bestOrFirstMatchResult = matchResults[0]; + var longestCommonPrefixLength = bestOrFirstMatchResult.FilterTextUsed.GetCaseInsensitivePrefixLength(_filterText); - var longestCommonPrefixLength = bestOrFirstMatchResult.FilterTextUsed.GetCaseInsensitivePrefixLength(_filterText); + for (var i = 1; i < matchResults.Count; ++i) + { + var matchResult = matchResults[i]; + var commonPrefixLength = matchResult.FilterTextUsed.GetCaseInsensitivePrefixLength(_filterText); - for (var i = 1; i < matchResults.Count; ++i) + if (commonPrefixLength > longestCommonPrefixLength) { - var matchResult = matchResults[i]; - var commonPrefixLength = matchResult.FilterTextUsed.GetCaseInsensitivePrefixLength(_filterText); - - if (commonPrefixLength > longestCommonPrefixLength) - { - selectedItemIndex = i; - bestOrFirstMatchResult = matchResult; - longestCommonPrefixLength = commonPrefixLength; - } + selectedItemIndex = i; + bestOrFirstMatchResult = matchResult; + longestCommonPrefixLength = commonPrefixLength; } } - else + } + else + { + // Of the items the service returned, pick the one most recently committed + var bestResult = GetBestCompletionItemSelectionFromFilteredResults(filteredMatchResultsBuilder); + + // Determine if we should consider this item 'unique' or not. A unique item + // will be automatically committed if the user hits the 'invoke completion' + // without bringing up the completion list. An item is unique if it was the + // only item to match the text typed so far, and there was at least some text + // typed. i.e. if we have "Console.$$" we don't want to commit something + // like "WriteLine" since no filter text has actually been provided. However, + // if "Console.WriteL$$" is typed, then we do want "WriteLine" to be committed. + for (selectedItemIndex = 0; selectedItemIndex < matchResults.Count; ++selectedItemIndex) { - // Of the items the service returned, pick the one most recently committed - var bestResult = GetBestCompletionItemSelectionFromFilteredResults(filteredMatchResultsBuilder); - - // Determine if we should consider this item 'unique' or not. A unique item - // will be automatically committed if the user hits the 'invoke completion' - // without bringing up the completion list. An item is unique if it was the - // only item to match the text typed so far, and there was at least some text - // typed. i.e. if we have "Console.$$" we don't want to commit something - // like "WriteLine" since no filter text has actually been provided. However, - // if "Console.WriteL$$" is typed, then we do want "WriteLine" to be committed. - for (selectedItemIndex = 0; selectedItemIndex < matchResults.Count; ++selectedItemIndex) - { - if (Equals(matchResults[selectedItemIndex].CompletionItem, bestResult.CompletionItem)) - break; - } + if (Equals(matchResults[selectedItemIndex].CompletionItem, bestResult.CompletionItem)) + break; + } - Debug.Assert(selectedItemIndex < matchResults.Count); + Debug.Assert(selectedItemIndex < matchResults.Count); - bestOrFirstMatchResult = matchResults[selectedItemIndex]; + bestOrFirstMatchResult = matchResults[selectedItemIndex]; - if (_filterText.Length > 0) - { - // PreferredItems from IntelliCode are duplicate of normal items, so we ignore them - // when deciding if we have an unique item. - if (matchResults.Count(matchResult => matchResult.ShouldBeConsideredMatchingFilterText && !matchResult.CompletionItem.IsPreferredItem()) == 1) - uniqueItem = GetCorrespondingVsCompletionItem(matchResults[selectedItemIndex], cancellationToken); - } - } - - var typedChar = _snapshotData.Trigger.Character; - - // Check that it is a filter symbol. We can be called for a non-filter symbol. - // If inserting a non-filter character (neither IsPotentialFilterCharacter, nor Helpers.IsFilterCharacter), - // we should dismiss completion except cases where this is the first symbol typed for the completion session - // (string.IsNullOrEmpty(filterText) or string.Equals(filterText, typeChar.ToString(), StringComparison.OrdinalIgnoreCase)). - // In the latter case, we should keep the completion because it was confirmed just before in InitializeCompletion. - if (UpdateTriggerReason == CompletionTriggerReason.Insertion && - !string.IsNullOrEmpty(_filterText) && - !string.Equals(_filterText, typedChar.ToString(), StringComparison.OrdinalIgnoreCase) && - !IsPotentialFilterCharacter(typedChar) && - !Helpers.IsFilterCharacter(bestOrFirstMatchResult.CompletionItem, typedChar, _filterText)) + if (_filterText.Length > 0) { - return null; + // PreferredItems from IntelliCode are duplicate of normal items, so we ignore them + // when deciding if we have an unique item. + if (matchResults.Count(matchResult => matchResult.ShouldBeConsideredMatchingFilterText && !matchResult.CompletionItem.IsPreferredItem()) == 1) + uniqueItem = GetCorrespondingVsCompletionItem(matchResults[selectedItemIndex], cancellationToken); } + } - var isHardSelection = IsHardSelection(bestOrFirstMatchResult.CompletionItem, bestOrFirstMatchResult.ShouldBeConsideredMatchingFilterText); - var updateSelectionHint = isHardSelection ? UpdateSelectionHint.Selected : UpdateSelectionHint.SoftSelected; + var typedChar = _snapshotData.Trigger.Character; - return new(selectedItemIndex, updateSelectionHint, uniqueItem); - } - finally + // Check that it is a filter symbol. We can be called for a non-filter symbol. + // If inserting a non-filter character (neither IsPotentialFilterCharacter, nor Helpers.IsFilterCharacter), + // we should dismiss completion except cases where this is the first symbol typed for the completion session + // (string.IsNullOrEmpty(filterText) or string.Equals(filterText, typeChar.ToString(), StringComparison.OrdinalIgnoreCase)). + // In the latter case, we should keep the completion because it was confirmed just before in InitializeCompletion. + if (UpdateTriggerReason == CompletionTriggerReason.Insertion && + !string.IsNullOrEmpty(_filterText) && + !string.Equals(_filterText, typedChar.ToString(), StringComparison.OrdinalIgnoreCase) && + !IsPotentialFilterCharacter(typedChar) && + !Helpers.IsFilterCharacter(bestOrFirstMatchResult.CompletionItem, typedChar, _filterText)) { - // Don't call ClearAndFree, which resets the capacity to a default value. - filteredMatchResultsBuilder.Clear(); - s_listOfMatchResultPool.Free(filteredMatchResultsBuilder); + return null; } - } - private VSCompletionItem GetCorrespondingVsCompletionItem(MatchResult matchResult, CancellationToken cancellationToken) + var isHardSelection = IsHardSelection(bestOrFirstMatchResult.CompletionItem, bestOrFirstMatchResult.ShouldBeConsideredMatchingFilterText); + var updateSelectionHint = isHardSelection ? UpdateSelectionHint.Selected : UpdateSelectionHint.SoftSelected; + + return new(selectedItemIndex, updateSelectionHint, uniqueItem); + } + finally { - cancellationToken.ThrowIfCancellationRequested(); + // Don't call ClearAndFree, which resets the capacity to a default value. + filteredMatchResultsBuilder.Clear(); + s_listOfMatchResultPool.Free(filteredMatchResultsBuilder); + } + } - if (matchResult.IndexInOriginalSortedOrder >= 0) - return _snapshotData.InitialSortedItemList[matchResult.IndexInOriginalSortedOrder]; + private VSCompletionItem GetCorrespondingVsCompletionItem(MatchResult matchResult, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - // This is a "promoted" item added by us, so there's no corresponding VS item from initial sorted list. - // We need to craft one based on the data of the original item. - var item = matchResult.CompletionItem; - if (Helpers.TryGetOriginalIndexOfPromotedItem(item, out var unpromotedIndex)) - { - var unpromotedVsItem = _snapshotData.InitialSortedItemList[unpromotedIndex]; - var promotedVsItem = new VSCompletionItem( - displayText: item.GetEntireDisplayText(), - unpromotedVsItem.Source, - unpromotedVsItem.Icon, - unpromotedVsItem.Filters, - unpromotedVsItem.Suffix, - unpromotedVsItem.InsertText, - unpromotedVsItem.SortText, - unpromotedVsItem.FilterText, - unpromotedVsItem.AutomationText, - unpromotedVsItem.AttributeIcons); - - // This is guaranteed to return true here - CompletionItemData.TryGetData(unpromotedVsItem, out var data); - CompletionItemData.AddData(promotedVsItem, item, data.TriggerLocation); - - return promotedVsItem; - } + if (matchResult.IndexInOriginalSortedOrder >= 0) + return _snapshotData.InitialSortedItemList[matchResult.IndexInOriginalSortedOrder]; - throw ExceptionUtilities.Unreachable(); + // This is a "promoted" item added by us, so there's no corresponding VS item from initial sorted list. + // We need to craft one based on the data of the original item. + var item = matchResult.CompletionItem; + if (Helpers.TryGetOriginalIndexOfPromotedItem(item, out var unpromotedIndex)) + { + var unpromotedVsItem = _snapshotData.InitialSortedItemList[unpromotedIndex]; + var promotedVsItem = new VSCompletionItem( + displayText: item.GetEntireDisplayText(), + unpromotedVsItem.Source, + unpromotedVsItem.Icon, + unpromotedVsItem.Filters, + unpromotedVsItem.Suffix, + unpromotedVsItem.InsertText, + unpromotedVsItem.SortText, + unpromotedVsItem.FilterText, + unpromotedVsItem.AutomationText, + unpromotedVsItem.AttributeIcons); + + // This is guaranteed to return true here + CompletionItemData.TryGetData(unpromotedVsItem, out var data); + CompletionItemData.AddData(promotedVsItem, item, data.TriggerLocation); + + return promotedVsItem; } - private ItemSelection? HandleDeletionTrigger(IReadOnlyList items, CancellationToken cancellationToken) - { - // Go through the entire item list to find the best match(es). - // If we had matching items, then pick the best of the matching items and - // choose that one to be hard selected. If we had no actual matching items - // (which can happen if the user deletes down to a single character and we - // include everything), then we just soft select the first item. - var indexToSelect = 0; - var hardSelect = false; - MatchResult? bestMatchResult = null; - var moreThanOneMatch = false; + throw ExceptionUtilities.Unreachable(); + } - for (var i = 0; i < items.Count; ++i) - { - var currentMatchResult = items[i]; + private ItemSelection? HandleDeletionTrigger(IReadOnlyList items, CancellationToken cancellationToken) + { + // Go through the entire item list to find the best match(es). + // If we had matching items, then pick the best of the matching items and + // choose that one to be hard selected. If we had no actual matching items + // (which can happen if the user deletes down to a single character and we + // include everything), then we just soft select the first item. + var indexToSelect = 0; + var hardSelect = false; + MatchResult? bestMatchResult = null; + var moreThanOneMatch = false; + + for (var i = 0; i < items.Count; ++i) + { + var currentMatchResult = items[i]; - if (!currentMatchResult.ShouldBeConsideredMatchingFilterText) - continue; + if (!currentMatchResult.ShouldBeConsideredMatchingFilterText) + continue; - if (bestMatchResult == null) + if (bestMatchResult == null) + { + // We had no best result yet, so this is now our best result. + bestMatchResult = currentMatchResult; + indexToSelect = i; + } + else + { + var match = CompareForDeletion(currentMatchResult, bestMatchResult.Value, _filterText); + if (match > 0) { - // We had no best result yet, so this is now our best result. + moreThanOneMatch = false; bestMatchResult = currentMatchResult; indexToSelect = i; } - else + else if (match == 0) { - var match = CompareForDeletion(currentMatchResult, bestMatchResult.Value, _filterText); - if (match > 0) - { - moreThanOneMatch = false; - bestMatchResult = currentMatchResult; - indexToSelect = i; - } - else if (match == 0) - { - moreThanOneMatch = true; - } + moreThanOneMatch = true; } } + } - if (bestMatchResult is null) - { - // The user has typed something, but nothing in the actual list matched what - // they were typing. In this case, we want to dismiss completion entirely. - // The thought process is as follows: we aggressively brought up completion - // to help them when they typed delete (in case they wanted to pick another - // item). However, they're typing something that doesn't seem to match at all - // The completion list is just distracting at this point. - if (UpdateTriggerReason == CompletionTriggerReason.Insertion) - return null; - - // If we are in suggestion mode and nothing matches filter text, we should soft select SuggestionItem. - if (ShouldSelectSuggestionItemWhenNoItemMatchesFilterText) - indexToSelect = SuggestionItemIndex; - } - else - { - // Only hard select this result if it's a prefix match - // We need to do this so that - // * deleting and retyping a dot in a member access does not change the - // text that originally appeared before the dot - // * deleting through a word from the end keeps that word selected - // This also preserves the behavior the VB had through Dev12. - hardSelect = !_hasSuggestedItemOptions && bestMatchResult.Value.FilterTextUsed.StartsWith(_filterText, StringComparison.CurrentCultureIgnoreCase); - } + if (bestMatchResult is null) + { + // The user has typed something, but nothing in the actual list matched what + // they were typing. In this case, we want to dismiss completion entirely. + // The thought process is as follows: we aggressively brought up completion + // to help them when they typed delete (in case they wanted to pick another + // item). However, they're typing something that doesn't seem to match at all + // The completion list is just distracting at this point. + if (UpdateTriggerReason == CompletionTriggerReason.Insertion) + return null; + + // If we are in suggestion mode and nothing matches filter text, we should soft select SuggestionItem. + if (ShouldSelectSuggestionItemWhenNoItemMatchesFilterText) + indexToSelect = SuggestionItemIndex; + } + else + { + // Only hard select this result if it's a prefix match + // We need to do this so that + // * deleting and retyping a dot in a member access does not change the + // text that originally appeared before the dot + // * deleting through a word from the end keeps that word selected + // This also preserves the behavior the VB had through Dev12. + hardSelect = !_hasSuggestedItemOptions && bestMatchResult.Value.FilterTextUsed.StartsWith(_filterText, StringComparison.CurrentCultureIgnoreCase); + } - // The best match we have selected is unique if `moreThanOneMatch` is false. - return new(SelectedItemIndex: indexToSelect, - SelectionHint: hardSelect ? UpdateSelectionHint.Selected : UpdateSelectionHint.SoftSelected, - UniqueItem: moreThanOneMatch || !bestMatchResult.HasValue ? null : GetCorrespondingVsCompletionItem(bestMatchResult.Value, cancellationToken)); + // The best match we have selected is unique if `moreThanOneMatch` is false. + return new(SelectedItemIndex: indexToSelect, + SelectionHint: hardSelect ? UpdateSelectionHint.Selected : UpdateSelectionHint.SoftSelected, + UniqueItem: moreThanOneMatch || !bestMatchResult.HasValue ? null : GetCorrespondingVsCompletionItem(bestMatchResult.Value, cancellationToken)); - static int CompareForDeletion(MatchResult x, MatchResult y, string pattern) - { - // Prefer the item that matches a longer prefix of the filter text. - var comparison = x.FilterTextUsed.GetCaseInsensitivePrefixLength(pattern).CompareTo(y.FilterTextUsed.GetCaseInsensitivePrefixLength(pattern)); - if (comparison != 0) - return comparison; - - // If there are "Abc" vs "abc", we should prefer the case typed by user. - comparison = x.FilterTextUsed.GetCaseSensitivePrefixLength(pattern).CompareTo(y.FilterTextUsed.GetCaseSensitivePrefixLength(pattern)); - if (comparison != 0) - return comparison; - - var xItem = x.CompletionItem; - var yItem = y.CompletionItem; - - // If the lengths are the same, prefer the one with the higher match priority. - // But only if it's an item that would have been hard selected. We don't want - // to aggressively select an item that was only going to be softly offered. - comparison = GetPriority(xItem).CompareTo(GetPriority(yItem)); - if (comparison != 0) - return comparison; - - // Prefer Intellicode items. - return xItem.IsPreferredItem().CompareTo(yItem.IsPreferredItem()); - - static int GetPriority(RoslynCompletionItem item) - => item.Rules.SelectionBehavior == CompletionItemSelectionBehavior.HardSelection ? item.Rules.MatchPriority : MatchPriority.Default; - } + static int CompareForDeletion(MatchResult x, MatchResult y, string pattern) + { + // Prefer the item that matches a longer prefix of the filter text. + var comparison = x.FilterTextUsed.GetCaseInsensitivePrefixLength(pattern).CompareTo(y.FilterTextUsed.GetCaseInsensitivePrefixLength(pattern)); + if (comparison != 0) + return comparison; + + // If there are "Abc" vs "abc", we should prefer the case typed by user. + comparison = x.FilterTextUsed.GetCaseSensitivePrefixLength(pattern).CompareTo(y.FilterTextUsed.GetCaseSensitivePrefixLength(pattern)); + if (comparison != 0) + return comparison; + + var xItem = x.CompletionItem; + var yItem = y.CompletionItem; + + // If the lengths are the same, prefer the one with the higher match priority. + // But only if it's an item that would have been hard selected. We don't want + // to aggressively select an item that was only going to be softly offered. + comparison = GetPriority(xItem).CompareTo(GetPriority(yItem)); + if (comparison != 0) + return comparison; + + // Prefer Intellicode items. + return xItem.IsPreferredItem().CompareTo(yItem.IsPreferredItem()); + + static int GetPriority(RoslynCompletionItem item) + => item.Rules.SelectionBehavior == CompletionItemSelectionBehavior.HardSelection ? item.Rules.MatchPriority : MatchPriority.Default; } + } - private CompletionList GetHighlightedList( - IAsyncCompletionSession session, - PatternMatchHelper patternMatchers, - IReadOnlyList matchResults, - CancellationToken cancellationToken) + private CompletionList GetHighlightedList( + IAsyncCompletionSession session, + PatternMatchHelper patternMatchers, + IReadOnlyList matchResults, + CancellationToken cancellationToken) + { + return session.CreateCompletionList(matchResults.Select(matchResult => { - return session.CreateCompletionList(matchResults.Select(matchResult => - { - var vsItem = GetCorrespondingVsCompletionItem(matchResult, cancellationToken); - var highlightedSpans = _highlightMatchingPortions - ? GetHighlightedSpans(matchResult, patternMatchers) - : []; + var vsItem = GetCorrespondingVsCompletionItem(matchResult, cancellationToken); + var highlightedSpans = _highlightMatchingPortions + ? GetHighlightedSpans(matchResult, patternMatchers) + : []; - return new CompletionItemWithHighlight(vsItem, highlightedSpans); - })); + return new CompletionItemWithHighlight(vsItem, highlightedSpans); + })); - static ImmutableArray GetHighlightedSpans(MatchResult matchResult, PatternMatchHelper patternMatchers) + static ImmutableArray GetHighlightedSpans(MatchResult matchResult, PatternMatchHelper patternMatchers) + { + if (matchResult.CompletionItem.HasDifferentFilterText || matchResult.CompletionItem.HasAdditionalFilterTexts) { - if (matchResult.CompletionItem.HasDifferentFilterText || matchResult.CompletionItem.HasAdditionalFilterTexts) - { - // The PatternMatch in MatchResult is calculated based on Roslyn item's FilterText, which can be used to calculate - // highlighted span for VSCompletion item's DisplayText w/o doing the matching again. - // However, if the Roslyn item's FilterText is different from its DisplayText, we need to do the match against the - // display text of the VS item directly to get the highlighted spans. This is done in a best effort fashion and there - // is no guarantee a proper match would be found for highlighting. - return patternMatchers.GetHighlightedSpans(matchResult.CompletionItem.GetEntireDisplayText(), CultureInfo.CurrentCulture) - .SelectAsArray(s => s.ToSpan()); - } - - var patternMatch = matchResult.PatternMatch; - if (patternMatch.HasValue) - { - // Since VS item's display text is created as Prefix + DisplayText + Suffix, - // we can calculate the highlighted span by adding an offset that is the length of the Prefix. - return patternMatch.Value.MatchedSpans.SelectAsArray(GetOffsetSpan, matchResult.CompletionItem); - } + // The PatternMatch in MatchResult is calculated based on Roslyn item's FilterText, which can be used to calculate + // highlighted span for VSCompletion item's DisplayText w/o doing the matching again. + // However, if the Roslyn item's FilterText is different from its DisplayText, we need to do the match against the + // display text of the VS item directly to get the highlighted spans. This is done in a best effort fashion and there + // is no guarantee a proper match would be found for highlighting. + return patternMatchers.GetHighlightedSpans(matchResult.CompletionItem.GetEntireDisplayText(), CultureInfo.CurrentCulture) + .SelectAsArray(s => s.ToSpan()); + } - // If there's no match for Roslyn item's filter text which is identical to its display text, - // then we can safely assume there'd be no matching to VS item's display text. - return []; + var patternMatch = matchResult.PatternMatch; + if (patternMatch.HasValue) + { + // Since VS item's display text is created as Prefix + DisplayText + Suffix, + // we can calculate the highlighted span by adding an offset that is the length of the Prefix. + return patternMatch.Value.MatchedSpans.SelectAsArray(GetOffsetSpan, matchResult.CompletionItem); } - // PERF: static local function to avoid lambda allocation on hot path - static Span GetOffsetSpan(TextSpan span, RoslynCompletionItem item) - => span.MoveTo(item.DisplayTextPrefix?.Length ?? 0).ToSpan(); + // If there's no match for Roslyn item's filter text which is identical to its display text, + // then we can safely assume there'd be no matching to VS item's display text. + return []; } - private FilteredCompletionModel? HandleAllItemsFilteredOut() + // PERF: static local function to avoid lambda allocation on hot path + static Span GetOffsetSpan(TextSpan span, RoslynCompletionItem item) + => span.MoveTo(item.DisplayTextPrefix?.Length ?? 0).ToSpan(); + } + + private FilteredCompletionModel? HandleAllItemsFilteredOut() + { + if (UpdateTriggerReason == CompletionTriggerReason.Insertion) { - if (UpdateTriggerReason == CompletionTriggerReason.Insertion) + // If the user was just typing, and the list went to empty *and* this is a + // language that wants to dismiss on empty, then just return a null model + // to stop the completion session. + if (_completionRules.DismissIfEmpty) { - // If the user was just typing, and the list went to empty *and* this is a - // language that wants to dismiss on empty, then just return a null model - // to stop the completion session. - if (_completionRules.DismissIfEmpty) - { - return null; - } + return null; } + } - // If we are in suggestion mode then we should select the SuggestionItem instead. - var selectedItemIndex = ShouldSelectSuggestionItemWhenNoItemMatchesFilterText ? SuggestionItemIndex : 0; + // If we are in suggestion mode then we should select the SuggestionItem instead. + var selectedItemIndex = ShouldSelectSuggestionItemWhenNoItemMatchesFilterText ? SuggestionItemIndex : 0; - // If the user has turned on some filtering states, and we filtered down to - // nothing, then we do want the UI to show that to them. That way the user - // can turn off filters they don't want and get the right set of items. + // If the user has turned on some filtering states, and we filtered down to + // nothing, then we do want the UI to show that to them. That way the user + // can turn off filters they don't want and get the right set of items. - // If we are going to filter everything out, then just preserve the existing - // model (and all the previously filtered items), but switch over to soft - // selection. - return new FilteredCompletionModel( - items: ImmutableArray.Empty, selectedItemIndex, - filters: _snapshotData.SelectedFilters, selectionHint: UpdateSelectionHint.SoftSelected, centerSelection: true, uniqueItem: null); - } + // If we are going to filter everything out, then just preserve the existing + // model (and all the previously filtered items), but switch over to soft + // selection. + return new FilteredCompletionModel( + items: ImmutableArray.Empty, selectedItemIndex, + filters: _snapshotData.SelectedFilters, selectionHint: UpdateSelectionHint.SoftSelected, centerSelection: true, uniqueItem: null); + } + + private ImmutableArray GetUpdatedFilters(IReadOnlyList matchResults, CancellationToken cancellationToken) + { + if (!_showCompletionItemFilters) + return []; - private ImmutableArray GetUpdatedFilters(IReadOnlyList matchResults, CancellationToken cancellationToken) + // See which filters might be enabled based on the typed code + using var _ = PooledHashSet.GetInstance(out var filters); + foreach (var item in matchResults) { - if (!_showCompletionItemFilters) - return []; + cancellationToken.ThrowIfCancellationRequested(); + filters.AddRange(GetCorrespondingVsCompletionItem(item, cancellationToken).Filters); + } - // See which filters might be enabled based on the typed code - using var _ = PooledHashSet.GetInstance(out var filters); - foreach (var item in matchResults) - { - cancellationToken.ThrowIfCancellationRequested(); - filters.AddRange(GetCorrespondingVsCompletionItem(item, cancellationToken).Filters); - } + // When no items are available for a given filter, it becomes unavailable. + // Expanders always appear available as long as it's presented. + return _snapshotData.SelectedFilters.SelectAsArray(n => n.WithAvailability(n.Filter is CompletionExpander || filters.Contains(n.Filter))); + } - // When no items are available for a given filter, it becomes unavailable. - // Expanders always appear available as long as it's presented. - return _snapshotData.SelectedFilters.SelectAsArray(n => n.WithAvailability(n.Filter is CompletionExpander || filters.Contains(n.Filter))); - } + /// + /// Given multiple possible chosen completion items, pick the one using the following preferences (in order): + /// 1. Most recently used item is our top preference + /// 2. IntelliCode item over non-IntelliCode item + /// 3. Higher MatchPriority + /// 4. Match to FilterText over AdditionalFilterTexts + /// + private static MatchResult GetBestCompletionItemSelectionFromFilteredResults(IReadOnlyList filteredMatchResults) + { + Debug.Assert(filteredMatchResults.Count > 0); - /// - /// Given multiple possible chosen completion items, pick the one using the following preferences (in order): - /// 1. Most recently used item is our top preference - /// 2. IntelliCode item over non-IntelliCode item - /// 3. Higher MatchPriority - /// 4. Match to FilterText over AdditionalFilterTexts - /// - private static MatchResult GetBestCompletionItemSelectionFromFilteredResults(IReadOnlyList filteredMatchResults) - { - Debug.Assert(filteredMatchResults.Count > 0); + var bestResult = filteredMatchResults[0]; + var bestResultMruIndex = bestResult.RecentItemIndex; - var bestResult = filteredMatchResults[0]; - var bestResultMruIndex = bestResult.RecentItemIndex; + for (int i = 1, n = filteredMatchResults.Count; i < n; i++) + { + var currentResult = filteredMatchResults[i]; + var currentResultMruIndex = currentResult.RecentItemIndex; - for (int i = 1, n = filteredMatchResults.Count; i < n; i++) + // Most recently used item is our top preference. + if (currentResultMruIndex != bestResultMruIndex) { - var currentResult = filteredMatchResults[i]; - var currentResultMruIndex = currentResult.RecentItemIndex; - - // Most recently used item is our top preference. - if (currentResultMruIndex != bestResultMruIndex) + if (currentResultMruIndex > bestResultMruIndex) { - if (currentResultMruIndex > bestResultMruIndex) - { - bestResult = currentResult; - bestResultMruIndex = currentResultMruIndex; - } - - continue; + bestResult = currentResult; + bestResultMruIndex = currentResultMruIndex; } - // 2nd preference is IntelliCode item - var currentIsPreferred = currentResult.CompletionItem.IsPreferredItem(); - var bestIsPreferred = bestResult.CompletionItem.IsPreferredItem(); + continue; + } - if (currentIsPreferred != bestIsPreferred) - { - if (currentIsPreferred && !bestIsPreferred) - { - bestResult = currentResult; - } + // 2nd preference is IntelliCode item + var currentIsPreferred = currentResult.CompletionItem.IsPreferredItem(); + var bestIsPreferred = bestResult.CompletionItem.IsPreferredItem(); - continue; + if (currentIsPreferred != bestIsPreferred) + { + if (currentIsPreferred && !bestIsPreferred) + { + bestResult = currentResult; } - // 3rd preference is higher MatchPriority - var currentMatchPriority = currentResult.CompletionItem.Rules.MatchPriority; - var bestMatchPriority = bestResult.CompletionItem.Rules.MatchPriority; - - if (currentMatchPriority != bestMatchPriority) - { - if (currentMatchPriority > bestMatchPriority) - { - bestResult = currentResult; - } + continue; + } - continue; - } + // 3rd preference is higher MatchPriority + var currentMatchPriority = currentResult.CompletionItem.Rules.MatchPriority; + var bestMatchPriority = bestResult.CompletionItem.Rules.MatchPriority; - // final preference is match to FilterText over AdditionalFilterTexts - if (bestResult.MatchedWithAdditionalFilterTexts && !currentResult.MatchedWithAdditionalFilterTexts) + if (currentMatchPriority != bestMatchPriority) + { + if (currentMatchPriority > bestMatchPriority) { bestResult = currentResult; } + + continue; } - return bestResult; + // final preference is match to FilterText over AdditionalFilterTexts + if (bestResult.MatchedWithAdditionalFilterTexts && !currentResult.MatchedWithAdditionalFilterTexts) + { + bestResult = currentResult; + } } - private static bool TryGetInitialTriggerLocation(AsyncCompletionSessionDataSnapshot data, out SnapshotPoint initialTriggerLocation) + return bestResult; + } + + private static bool TryGetInitialTriggerLocation(AsyncCompletionSessionDataSnapshot data, out SnapshotPoint initialTriggerLocation) + { + foreach (var item in data.InitialSortedItemList) { - foreach (var item in data.InitialSortedItemList) + if (CompletionItemData.TryGetData(item, out var itemData) && itemData.TriggerLocation.HasValue) { - if (CompletionItemData.TryGetData(item, out var itemData) && itemData.TriggerLocation.HasValue) - { - initialTriggerLocation = itemData.TriggerLocation.Value; - return true; - } + initialTriggerLocation = itemData.TriggerLocation.Value; + return true; } + } + + initialTriggerLocation = default; + return false; + } - initialTriggerLocation = default; + private bool IsHardSelection( + RoslynCompletionItem item, + bool matchedFilterText) + { + if (_hasSuggestedItemOptions) + { + return false; + } + + // We don't have a builder and we have a best match. Normally this will be hard + // selected, except for a few cases. Specifically, if no filter text has been + // provided, and this is not a preselect match then we will soft select it. This + // happens when the completion list comes up implicitly and there is something in + // the MRU list. In this case we do want to select it, but not with a hard + // selection. Otherwise you can end up with the following problem: + // + // dim i as integer = + // + // Completion will comes up after = with 'integer' selected (Because of MRU). We do + // not want 'space' to commit this. + + // If all that has been typed is punctuation, then don't hard select anything. + // It's possible the user is just typing language punctuation and selecting + // anything in the list will interfere. We only allow this if the filter text + // exactly matches something in the list already. + if (_filterText.Length > 0 && IsAllPunctuation(_filterText) && _filterText != item.DisplayText) + { return false; } - private bool IsHardSelection( - RoslynCompletionItem item, - bool matchedFilterText) + // If the user hasn't actually typed anything, then don't hard select any item. + // The only exception to this is if the completion provider has requested the + // item be preselected. + if (_filterText.Length == 0) { - if (_hasSuggestedItemOptions) + // Item didn't want to be hard selected with no filter text. + // So definitely soft select it. + if (item.Rules.SelectionBehavior != CompletionItemSelectionBehavior.HardSelection) { return false; } - // We don't have a builder and we have a best match. Normally this will be hard - // selected, except for a few cases. Specifically, if no filter text has been - // provided, and this is not a preselect match then we will soft select it. This - // happens when the completion list comes up implicitly and there is something in - // the MRU list. In this case we do want to select it, but not with a hard - // selection. Otherwise you can end up with the following problem: - // - // dim i as integer = - // - // Completion will comes up after = with 'integer' selected (Because of MRU). We do - // not want 'space' to commit this. - - // If all that has been typed is punctuation, then don't hard select anything. - // It's possible the user is just typing language punctuation and selecting - // anything in the list will interfere. We only allow this if the filter text - // exactly matches something in the list already. - if (_filterText.Length > 0 && IsAllPunctuation(_filterText) && _filterText != item.DisplayText) + // Item did not ask to be preselected. So definitely soft select it. + if (item.Rules.MatchPriority == MatchPriority.Default) { return false; } + } - // If the user hasn't actually typed anything, then don't hard select any item. - // The only exception to this is if the completion provider has requested the - // item be preselected. - if (_filterText.Length == 0) - { - // Item didn't want to be hard selected with no filter text. - // So definitely soft select it. - if (item.Rules.SelectionBehavior != CompletionItemSelectionBehavior.HardSelection) - { - return false; - } + // The user typed something, or the item asked to be preselected. In + // either case, don't soft select this. + Debug.Assert(_filterText.Length > 0 || item.Rules.MatchPriority != MatchPriority.Default); - // Item did not ask to be preselected. So definitely soft select it. - if (item.Rules.MatchPriority == MatchPriority.Default) - { - return false; - } - } + // If the user moved the caret left after they started typing, the 'best' match may not match at all + // against the full text span that this item would be replacing. + if (!matchedFilterText) + { + return false; + } - // The user typed something, or the item asked to be preselected. In - // either case, don't soft select this. - Debug.Assert(_filterText.Length > 0 || item.Rules.MatchPriority != MatchPriority.Default); + // There was either filter text, or this was a preselect match. In either case, we + // can hard select this. + return true; + } - // If the user moved the caret left after they started typing, the 'best' match may not match at all - // against the full text span that this item would be replacing. - if (!matchedFilterText) + private static bool IsAllPunctuation(string filterText) + { + foreach (var ch in filterText) + { + if (!char.IsPunctuation(ch)) { return false; } - - // There was either filter text, or this was a preselect match. In either case, we - // can hard select this. - return true; } - private static bool IsAllPunctuation(string filterText) - { - foreach (var ch in filterText) - { - if (!char.IsPunctuation(ch)) - { - return false; - } - } - - return true; - } + return true; + } - /// - /// A potential filter character is something that can filter a completion lists and is - /// *guaranteed* to not be a commit character. - /// - private static bool IsPotentialFilterCharacter(char c) - { - // TODO(cyrusn): Actually use the right Unicode categories here. - return char.IsLetter(c) - || char.IsNumber(c) - || c == '_'; - } + /// + /// A potential filter character is something that can filter a completion lists and is + /// *guaranteed* to not be a commit character. + /// + private static bool IsPotentialFilterCharacter(char c) + { + // TODO(cyrusn): Actually use the right Unicode categories here. + return char.IsLetter(c) + || char.IsNumber(c) + || c == '_'; + } - private ItemSelection UpdateSelectionBasedOnSuggestedDefaults(IReadOnlyList items, ItemSelection itemSelection, CancellationToken cancellationToken) + private ItemSelection UpdateSelectionBasedOnSuggestedDefaults(IReadOnlyList items, ItemSelection itemSelection, CancellationToken cancellationToken) + { + // Editor doesn't provide us a list of "default" items, or we select SuggestionItem (because we are in suggestion mode and have no match in the list) + if (_snapshotData.Defaults.IsDefaultOrEmpty || itemSelection.SelectedItemIndex == SuggestionItemIndex) + return itemSelection; + + // "Preselect" is only used when we have high confidence with the selection, so don't override it; + // unless it's a real Pythia item (i.e. not fake one from us,) in which case the default item wins. + var selectedItem = items[itemSelection.SelectedItemIndex].CompletionItem; + if (selectedItem.Rules.MatchPriority >= MatchPriority.Preselect) { - // Editor doesn't provide us a list of "default" items, or we select SuggestionItem (because we are in suggestion mode and have no match in the list) - if (_snapshotData.Defaults.IsDefaultOrEmpty || itemSelection.SelectedItemIndex == SuggestionItemIndex) + // This is a high priority item provided by Roslyn, either a regular or preferred one promoted by us + // (because it matches one of the defaults even though it might not be the best default) so we want to stick with this selection. + if (!selectedItem.IsPreferredItem() || Helpers.TryGetOriginalIndexOfPromotedItem(selectedItem, out _)) return itemSelection; + } - // "Preselect" is only used when we have high confidence with the selection, so don't override it; - // unless it's a real Pythia item (i.e. not fake one from us,) in which case the default item wins. - var selectedItem = items[itemSelection.SelectedItemIndex].CompletionItem; - if (selectedItem.Rules.MatchPriority >= MatchPriority.Preselect) - { - // This is a high priority item provided by Roslyn, either a regular or preferred one promoted by us - // (because it matches one of the defaults even though it might not be the best default) so we want to stick with this selection. - if (!selectedItem.IsPreferredItem() || Helpers.TryGetOriginalIndexOfPromotedItem(selectedItem, out _)) - return itemSelection; - } + return GetDefaultsMatch(items, itemSelection, cancellationToken); + } - return GetDefaultsMatch(items, itemSelection, cancellationToken); + /// + /// Compare the pattern matching result of the current selection with the pattern matching result of the suggested defaults (both w.r.t. the filter text.) + /// If the suggested default is no worse than current selected item (in a case-sensitive manner,) use the suggested default. Otherwise use the original selection. + /// For example, if user typed "C", roslyn might select "CancellationToken", but with suggested default "Console" we will end up selecting "Console" instead. + /// + private ItemSelection GetDefaultsMatch(IReadOnlyList matches, ItemSelection initialSelection, CancellationToken cancellationToken) + { + // Because the items are already sorted based on pattern-matching score, try to limit the range for the items we search + // by stopping as soon as we encountered the top item from original default list, or at the first match that is inferior than current selection. + int inferiorItemIndex; + if (_filterText.Length == 0) + { + // Without filterText, all items are equally good match (w.r.t to the empty filterText), so we have to consider all of them. + inferiorItemIndex = matches.Count; } - - /// - /// Compare the pattern matching result of the current selection with the pattern matching result of the suggested defaults (both w.r.t. the filter text.) - /// If the suggested default is no worse than current selected item (in a case-sensitive manner,) use the suggested default. Otherwise use the original selection. - /// For example, if user typed "C", roslyn might select "CancellationToken", but with suggested default "Console" we will end up selecting "Console" instead. - /// - private ItemSelection GetDefaultsMatch(IReadOnlyList matches, ItemSelection initialSelection, CancellationToken cancellationToken) + else { - // Because the items are already sorted based on pattern-matching score, try to limit the range for the items we search - // by stopping as soon as we encountered the top item from original default list, or at the first match that is inferior than current selection. - int inferiorItemIndex; - if (_filterText.Length == 0) - { - // Without filterText, all items are equally good match (w.r.t to the empty filterText), so we have to consider all of them. - inferiorItemIndex = matches.Count; - } - else + var selectedItemMatch = matches[initialSelection.SelectedItemIndex].PatternMatch; + + // It's possible that an item doesn't match filter text but still ended up being selected, this is because we just always keep all the + // items in the list in some cases. For example, user brought up completion with ctrl-j or through deletion. + // Don't bother changing the selection in such cases (since there's no match to the filter text in the list) + if (!selectedItemMatch.HasValue) + return initialSelection; + + // Because the items are sorted based on pattern-matching score, the selectedIndex is in the middle of a range of + // -- as far as the pattern matcher is concerned -- equivalent items (items with identical PatternMatch.Kind and IsCaseSensitive). + // Find the last items in the range and use that to limit the items searched for from the defaults list. + inferiorItemIndex = initialSelection.SelectedItemIndex; + while (++inferiorItemIndex < matches.Count) { - var selectedItemMatch = matches[initialSelection.SelectedItemIndex].PatternMatch; - - // It's possible that an item doesn't match filter text but still ended up being selected, this is because we just always keep all the - // items in the list in some cases. For example, user brought up completion with ctrl-j or through deletion. - // Don't bother changing the selection in such cases (since there's no match to the filter text in the list) - if (!selectedItemMatch.HasValue) - return initialSelection; - - // Because the items are sorted based on pattern-matching score, the selectedIndex is in the middle of a range of - // -- as far as the pattern matcher is concerned -- equivalent items (items with identical PatternMatch.Kind and IsCaseSensitive). - // Find the last items in the range and use that to limit the items searched for from the defaults list. - inferiorItemIndex = initialSelection.SelectedItemIndex; - while (++inferiorItemIndex < matches.Count) + var itemMatch = matches[inferiorItemIndex].PatternMatch; + if (!itemMatch.HasValue + || itemMatch.Value.Kind != selectedItemMatch.Value.Kind + || itemMatch.Value.IsCaseSensitive != selectedItemMatch.Value.IsCaseSensitive) { - var itemMatch = matches[inferiorItemIndex].PatternMatch; - if (!itemMatch.HasValue - || itemMatch.Value.Kind != selectedItemMatch.Value.Kind - || itemMatch.Value.IsCaseSensitive != selectedItemMatch.Value.IsCaseSensitive) - { - break; - } + break; } } + } - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - // The range includes all items that are as good of a match as what we initially selected (and in descending order of matching score) - // so we just need to search for a preferred item with best default match. - var bestPromotedItemSoFar = -1; - var bestDefaultIndex = -1; - for (var i = 0; i < inferiorItemIndex; ++i) + // The range includes all items that are as good of a match as what we initially selected (and in descending order of matching score) + // so we just need to search for a preferred item with best default match. + var bestPromotedItemSoFar = -1; + var bestDefaultIndex = -1; + for (var i = 0; i < inferiorItemIndex; ++i) + { + var item = matches[i].CompletionItem; + if (item.IsPreferredItem()) { - var item = matches[i].CompletionItem; - if (item.IsPreferredItem()) - { - var defaultIndex = _snapshotData.Defaults.IndexOf(matches[i].CompletionItem.FilterText); - - // This is not a starred item that matches default - if (defaultIndex < 0) - continue; + var defaultIndex = _snapshotData.Defaults.IndexOf(matches[i].CompletionItem.FilterText); - if (bestPromotedItemSoFar < 0 || defaultIndex <= bestDefaultIndex) - { - bestPromotedItemSoFar = i; - bestDefaultIndex = defaultIndex; - } + // This is not a starred item that matches default + if (defaultIndex < 0) + continue; - // We have found the top default item, no need to proceed. - if (bestDefaultIndex == 0) - break; + if (bestPromotedItemSoFar < 0 || defaultIndex <= bestDefaultIndex) + { + bestPromotedItemSoFar = i; + bestDefaultIndex = defaultIndex; } + + // We have found the top default item, no need to proceed. + if (bestDefaultIndex == 0) + break; } + } - // Don't change the original selection since there's no match to the defaults provided. - if (bestPromotedItemSoFar < 0) - return initialSelection; + // Don't change the original selection since there's no match to the defaults provided. + if (bestPromotedItemSoFar < 0) + return initialSelection; - // If user hasn't typed anything, we'd like to soft select the default item. - // This way, the selection won't get in the way of typing. - var selectionHint = _filterText.Length == 0 ? UpdateSelectionHint.SoftSelected : initialSelection.SelectionHint; - return initialSelection with { SelectedItemIndex = bestPromotedItemSoFar, SelectionHint = selectionHint }; - } + // If user hasn't typed anything, we'd like to soft select the default item. + // This way, the selection won't get in the way of typing. + var selectionHint = _filterText.Length == 0 ? UpdateSelectionHint.SoftSelected : initialSelection.SelectionHint; + return initialSelection with { SelectedItemIndex = bestPromotedItemSoFar, SelectionHint = selectionHint }; + } + + private sealed class FilterStateHelper + { + private readonly ImmutableArray _nonExpanderFilterStates; + private readonly ImmutableArray _selectedNonExpanderFilters; + private readonly ImmutableArray _unselectedExpanders; + private readonly bool _needToFilter; + private readonly bool _needToFilterExpanded; - private sealed class FilterStateHelper + public FilterStateHelper(ImmutableArray filtersWithState) { - private readonly ImmutableArray _nonExpanderFilterStates; - private readonly ImmutableArray _selectedNonExpanderFilters; - private readonly ImmutableArray _unselectedExpanders; - private readonly bool _needToFilter; - private readonly bool _needToFilterExpanded; + // The filter state list contains two kinds of "filters": regular filter and expander. + // The difference between them is they have different semantics. + // - When all filters or no filter is selected, everything should be included. + // But when a strict subset of filters is selected, only items corresponding to the selected filters should be included. + // - When expander is selected, all expanded items should be included, otherwise, expanded items should be excluded. + // expander state has no affect on non-expanded items. + // For example, right now we only have one expander for items from unimported namespaces, selecting/unselecting expander would + // include/exclude those items from completion list, but in-scope items would be shown regardless. + // + // Therefore, we need to filter if + // 1. a non-empty strict subset of filters are selected + // 2. a non-empty set of expanders are unselected + _nonExpanderFilterStates = filtersWithState.WhereAsArray(f => f.Filter is not CompletionExpander); + + _selectedNonExpanderFilters = _nonExpanderFilterStates.SelectAsArray(f => f.IsSelected, f => f.Filter); + _needToFilter = _selectedNonExpanderFilters.Length > 0 && _selectedNonExpanderFilters.Length < _nonExpanderFilterStates.Length; + + _unselectedExpanders = filtersWithState.SelectAsArray(f => !f.IsSelected && f.Filter is CompletionExpander, f => f.Filter); + _needToFilterExpanded = _unselectedExpanders.Length > 0; + } - public FilterStateHelper(ImmutableArray filtersWithState) - { - // The filter state list contains two kinds of "filters": regular filter and expander. - // The difference between them is they have different semantics. - // - When all filters or no filter is selected, everything should be included. - // But when a strict subset of filters is selected, only items corresponding to the selected filters should be included. - // - When expander is selected, all expanded items should be included, otherwise, expanded items should be excluded. - // expander state has no affect on non-expanded items. - // For example, right now we only have one expander for items from unimported namespaces, selecting/unselecting expander would - // include/exclude those items from completion list, but in-scope items would be shown regardless. - // - // Therefore, we need to filter if - // 1. a non-empty strict subset of filters are selected - // 2. a non-empty set of expanders are unselected - _nonExpanderFilterStates = filtersWithState.WhereAsArray(f => f.Filter is not CompletionExpander); - - _selectedNonExpanderFilters = _nonExpanderFilterStates.SelectAsArray(f => f.IsSelected, f => f.Filter); - _needToFilter = _selectedNonExpanderFilters.Length > 0 && _selectedNonExpanderFilters.Length < _nonExpanderFilterStates.Length; - - _unselectedExpanders = filtersWithState.SelectAsArray(f => !f.IsSelected && f.Filter is CompletionExpander, f => f.Filter); - _needToFilterExpanded = _unselectedExpanders.Length > 0; - } + public bool ShouldBeFilteredOut(VSCompletionItem item) + => ShouldBeFilteredOutOfCompletionList(item) || ShouldBeFilteredOutOfExpandedCompletionList(item); - public bool ShouldBeFilteredOut(VSCompletionItem item) - => ShouldBeFilteredOutOfCompletionList(item) || ShouldBeFilteredOutOfExpandedCompletionList(item); + private bool ShouldBeFilteredOutOfCompletionList(VSCompletionItem item) + => _needToFilter && !item.Filters.Any(static (filter, self) => self._selectedNonExpanderFilters.Contains(filter), this); - private bool ShouldBeFilteredOutOfCompletionList(VSCompletionItem item) - => _needToFilter && !item.Filters.Any(static (filter, self) => self._selectedNonExpanderFilters.Contains(filter), this); + private bool ShouldBeFilteredOutOfExpandedCompletionList(VSCompletionItem item) + { + if (!_needToFilterExpanded) + return false; - private bool ShouldBeFilteredOutOfExpandedCompletionList(VSCompletionItem item) + var associatedWithUnselectedExpander = false; + foreach (var itemFilter in item.Filters) { - if (!_needToFilterExpanded) - return false; - - var associatedWithUnselectedExpander = false; - foreach (var itemFilter in item.Filters) + if (itemFilter is CompletionExpander) { - if (itemFilter is CompletionExpander) + if (!_unselectedExpanders.Contains(itemFilter)) { - if (!_unselectedExpanders.Contains(itemFilter)) - { - // If any of the associated expander is selected, the item should be included in the expanded list. - return false; - } - - associatedWithUnselectedExpander = true; + // If any of the associated expander is selected, the item should be included in the expanded list. + return false; } - } - // at this point, the item either: - // 1. has no expander filter, therefore should be included - // 2. or, all associated expanders are unselected, therefore should be excluded - return associatedWithUnselectedExpander; + associatedWithUnselectedExpander = true; + } } - } - private readonly record struct ItemSelection(int SelectedItemIndex, UpdateSelectionHint SelectionHint, VSCompletionItem? UniqueItem); + // at this point, the item either: + // 1. has no expander filter, therefore should be included + // 2. or, all associated expanders are unselected, therefore should be excluded + return associatedWithUnselectedExpander; + } } + + private readonly record struct ItemSelection(int SelectedItemIndex, UpdateSelectionHint SelectionHint, VSCompletionItem? UniqueItem); } } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManager.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManager.cs index 354879117c9ea..1f461898746ca 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManager.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManager.cs @@ -18,180 +18,179 @@ using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem; using VSCompletionItem = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data.CompletionItem; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +internal sealed partial class ItemManager : IAsyncCompletionItemManager2 { - internal sealed partial class ItemManager : IAsyncCompletionItemManager2 + private static readonly ObjectPool> s_sortListPool = new(factory: () => [], size: 5); + + /// + /// The threshold for us to consider exclude (potentially large amount of) expanded items from completion list. + /// Showing a large amount of expanded items to user would introduce noise and render the list too long to browse. + /// Not processing those expanded items also has perf benefit (e.g. matching and highlighting could be expensive.) + /// We set it to 2 because it's common to use filter of length 2 for camel case match, e.g. `AB` for `ArrayBuilder`. + /// + public const int FilterTextLengthToExcludeExpandedItemsExclusive = 2; + + private readonly RecentItemsManager _recentItemsManager; + private readonly EditorOptionsService _editorOptionsService; + + internal ItemManager(RecentItemsManager recentItemsManager, EditorOptionsService editorOptionsService) + { + _recentItemsManager = recentItemsManager; + _editorOptionsService = editorOptionsService; + } + + public Task> SortCompletionListAsync( + IAsyncCompletionSession session, + AsyncCompletionSessionInitialDataSnapshot data, + CancellationToken cancellationToken) { - private static readonly ObjectPool> s_sortListPool = new(factory: () => [], size: 5); + // Platform prefers IAsyncCompletionItemManager2.SortCompletionItemListAsync when available + throw new NotImplementedException(); + } - /// - /// The threshold for us to consider exclude (potentially large amount of) expanded items from completion list. - /// Showing a large amount of expanded items to user would introduce noise and render the list too long to browse. - /// Not processing those expanded items also has perf benefit (e.g. matching and highlighting could be expensive.) - /// We set it to 2 because it's common to use filter of length 2 for camel case match, e.g. `AB` for `ArrayBuilder`. - /// - public const int FilterTextLengthToExcludeExpandedItemsExclusive = 2; + public Task> SortCompletionItemListAsync( + IAsyncCompletionSession session, + AsyncCompletionSessionInitialDataSnapshot data, + CancellationToken cancellationToken) + { + var stopwatch = SharedStopwatch.StartNew(); - private readonly RecentItemsManager _recentItemsManager; - private readonly EditorOptionsService _editorOptionsService; + var list = s_sortListPool.Allocate(); + CompletionList itemList; - internal ItemManager(RecentItemsManager recentItemsManager, EditorOptionsService editorOptionsService) + try { - _recentItemsManager = recentItemsManager; - _editorOptionsService = editorOptionsService; - } + SortCompletionItems(list, data, cancellationToken); - public Task> SortCompletionListAsync( - IAsyncCompletionSession session, - AsyncCompletionSessionInitialDataSnapshot data, - CancellationToken cancellationToken) - { - // Platform prefers IAsyncCompletionItemManager2.SortCompletionItemListAsync when available - throw new NotImplementedException(); + itemList = session.CreateCompletionList(list); } - - public Task> SortCompletionItemListAsync( - IAsyncCompletionSession session, - AsyncCompletionSessionInitialDataSnapshot data, - CancellationToken cancellationToken) + finally { - var stopwatch = SharedStopwatch.StartNew(); - - var list = s_sortListPool.Allocate(); - CompletionList itemList; + list.Clear(); + s_sortListPool.Free(list); + } - try - { - SortCompletionItems(list, data, cancellationToken); + AsyncCompletionLogger.LogItemManagerSortTicksDataPoint(stopwatch.Elapsed); + return Task.FromResult(itemList); + } - itemList = session.CreateCompletionList(list); - } - finally - { - list.Clear(); - s_sortListPool.Free(list); - } + private static void SortCompletionItems(List list, AsyncCompletionSessionInitialDataSnapshot data, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - AsyncCompletionLogger.LogItemManagerSortTicksDataPoint(stopwatch.Elapsed); - return Task.FromResult(itemList); + foreach (var item in data.InitialItemList) + { + CompletionItemData.GetOrAddDummyRoslynItem(item); + list.Add(item); } - private static void SortCompletionItems(List list, AsyncCompletionSessionInitialDataSnapshot data, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + list.Sort(VSItemComparer.Instance); + } - foreach (var item in data.InitialItemList) + public async Task UpdateCompletionListAsync( + IAsyncCompletionSession session, + AsyncCompletionSessionDataSnapshot data, + CancellationToken cancellationToken) + { + var stopwatch = SharedStopwatch.StartNew(); + try + { + var sessionData = CompletionSessionData.GetOrCreateSessionData(session); + + // As explained in more details in the comments for `CompletionSource.GetCompletionContextAsync`, expanded items might + // not be provided upon initial trigger of completion to reduce typing delays, even if they are supposed to be included by default. + // While we do not expect to run in to this scenario very often, we'd still want to minimize the impact on user experience of this feature + // as best as we could when it does occur. So the solution we came up with is this: if we decided to not include expanded items (because the + // computation is running too long,) we will let it run in the background as long as the completion session is still active. Then whenever + // any user input that would cause the completion list to refresh, we will check the state of this background task and add expanded items as part + // of the update if they are available. + // There is a `CompletionContext.IsIncomplete` flag, which is only supported in LSP mode at the moment. Therefore we opt to handle the checking + // and combining the items in Roslyn until the `IsIncomplete` flag is fully supported in classic mode. + + if (sessionData.CombinedSortedList is not null) { - CompletionItemData.GetOrAddDummyRoslynItem(item); - list.Add(item); + // Always use the previously saved combined list if available. + data = new AsyncCompletionSessionDataSnapshot(sessionData.CombinedSortedList, data.Snapshot, data.Trigger, data.InitialTrigger, data.SelectedFilters, + data.IsSoftSelected, data.DisplaySuggestionItem, data.Defaults); } + else if (ShouldShowExpandedItems() && sessionData.ExpandedItemsTask is not null) + { + var task = sessionData.ExpandedItemsTask; - list.Sort(VSItemComparer.Instance); - } + // we don't want to delay showing completion list on waiting for + // expanded items, unless responsive typing is disabled by user. + if (!sessionData.NonBlockingCompletionEnabled) + await task.ConfigureAwait(false); - public async Task UpdateCompletionListAsync( - IAsyncCompletionSession session, - AsyncCompletionSessionDataSnapshot data, - CancellationToken cancellationToken) - { - var stopwatch = SharedStopwatch.StartNew(); - try - { - var sessionData = CompletionSessionData.GetOrCreateSessionData(session); - - // As explained in more details in the comments for `CompletionSource.GetCompletionContextAsync`, expanded items might - // not be provided upon initial trigger of completion to reduce typing delays, even if they are supposed to be included by default. - // While we do not expect to run in to this scenario very often, we'd still want to minimize the impact on user experience of this feature - // as best as we could when it does occur. So the solution we came up with is this: if we decided to not include expanded items (because the - // computation is running too long,) we will let it run in the background as long as the completion session is still active. Then whenever - // any user input that would cause the completion list to refresh, we will check the state of this background task and add expanded items as part - // of the update if they are available. - // There is a `CompletionContext.IsIncomplete` flag, which is only supported in LSP mode at the moment. Therefore we opt to handle the checking - // and combining the items in Roslyn until the `IsIncomplete` flag is fully supported in classic mode. - - if (sessionData.CombinedSortedList is not null) - { - // Always use the previously saved combined list if available. - data = new AsyncCompletionSessionDataSnapshot(sessionData.CombinedSortedList, data.Snapshot, data.Trigger, data.InitialTrigger, data.SelectedFilters, - data.IsSoftSelected, data.DisplaySuggestionItem, data.Defaults); - } - else if (ShouldShowExpandedItems() && sessionData.ExpandedItemsTask is not null) + if (task.Status == TaskStatus.RanToCompletion) { - var task = sessionData.ExpandedItemsTask; + // Make sure the task is removed when Adding expanded items, + // so duplicated items won't be added in subsequent list updates. + sessionData.ExpandedItemsTask = null; - // we don't want to delay showing completion list on waiting for - // expanded items, unless responsive typing is disabled by user. - if (!sessionData.NonBlockingCompletionEnabled) - await task.ConfigureAwait(false); - - if (task.Status == TaskStatus.RanToCompletion) + var (expandedContext, _) = await task.ConfigureAwait(false); + if (expandedContext.ItemList.Count > 0) { - // Make sure the task is removed when Adding expanded items, - // so duplicated items won't be added in subsequent list updates. - sessionData.ExpandedItemsTask = null; - - var (expandedContext, _) = await task.ConfigureAwait(false); - if (expandedContext.ItemList.Count > 0) - { - // Here we rely on the implementation detail of `CompletionItem.CompareTo`, which always put expand items after regular ones. - var combinedItemList = session.CreateCompletionList(data.InitialSortedItemList.Concat(expandedContext.ItemList)); - - // Add expanded items into a combined list, and save it to be used for future updates during the same session. - sessionData.CombinedSortedList = combinedItemList; - var combinedFilterStates = FilterSet.CombineFilterStates(expandedContext.Filters, data.SelectedFilters); + // Here we rely on the implementation detail of `CompletionItem.CompareTo`, which always put expand items after regular ones. + var combinedItemList = session.CreateCompletionList(data.InitialSortedItemList.Concat(expandedContext.ItemList)); - data = new AsyncCompletionSessionDataSnapshot(combinedItemList, data.Snapshot, data.Trigger, data.InitialTrigger, combinedFilterStates, - data.IsSoftSelected, data.DisplaySuggestionItem, data.Defaults); - } + // Add expanded items into a combined list, and save it to be used for future updates during the same session. + sessionData.CombinedSortedList = combinedItemList; + var combinedFilterStates = FilterSet.CombineFilterStates(expandedContext.Filters, data.SelectedFilters); - AsyncCompletionLogger.LogSessionWithDelayedImportCompletionIncludedInUpdate(); + data = new AsyncCompletionSessionDataSnapshot(combinedItemList, data.Snapshot, data.Trigger, data.InitialTrigger, combinedFilterStates, + data.IsSoftSelected, data.DisplaySuggestionItem, data.Defaults); } - } - var updater = new CompletionListUpdater(session.ApplicableToSpan, sessionData, data, _recentItemsManager, _editorOptionsService.GlobalOptions); - return await updater.UpdateCompletionListAsync(session, cancellationToken).ConfigureAwait(false); - } - finally - { - AsyncCompletionLogger.LogItemManagerUpdateDataPoint(stopwatch.Elapsed, isCanceled: cancellationToken.IsCancellationRequested); + AsyncCompletionLogger.LogSessionWithDelayedImportCompletionIncludedInUpdate(); + } } - // Show expanded items if any of these conditions is true: - // 1. filter text length >= 2 (it's common to use filter of length 2 for camel case match, e.g. `AB` for `ArrayBuilder`) - // 2. the completion is triggered in the context of listing members (it usually has much fewer items and more often used for browsing purpose) - // 3. defaults is not empty, since they might suggest expanded items (Defaults are the mechanism whole-line-completion uses to communicate with - // completion and make our selection consistent with their suggestion) - bool ShouldShowExpandedItems() - => session.ApplicableToSpan.GetText(data.Snapshot).Length >= FilterTextLengthToExcludeExpandedItemsExclusive - || IsAfterDot(data.Snapshot, session.ApplicableToSpan) - || !data.Defaults.IsEmpty; + var updater = new CompletionListUpdater(session.ApplicableToSpan, sessionData, data, _recentItemsManager, _editorOptionsService.GlobalOptions); + return await updater.UpdateCompletionListAsync(session, cancellationToken).ConfigureAwait(false); } - - private static bool IsAfterDot(ITextSnapshot snapshot, ITrackingSpan applicableToSpan) + finally { - var position = applicableToSpan.GetStartPoint(snapshot).Position; - return position > 0 && snapshot[position - 1] == '.'; + AsyncCompletionLogger.LogItemManagerUpdateDataPoint(stopwatch.Elapsed, isCanceled: cancellationToken.IsCancellationRequested); } - private sealed class VSItemComparer : IComparer - { - public static VSItemComparer Instance { get; } = new(); + // Show expanded items if any of these conditions is true: + // 1. filter text length >= 2 (it's common to use filter of length 2 for camel case match, e.g. `AB` for `ArrayBuilder`) + // 2. the completion is triggered in the context of listing members (it usually has much fewer items and more often used for browsing purpose) + // 3. defaults is not empty, since they might suggest expanded items (Defaults are the mechanism whole-line-completion uses to communicate with + // completion and make our selection consistent with their suggestion) + bool ShouldShowExpandedItems() + => session.ApplicableToSpan.GetText(data.Snapshot).Length >= FilterTextLengthToExcludeExpandedItemsExclusive + || IsAfterDot(data.Snapshot, session.ApplicableToSpan) + || !data.Defaults.IsEmpty; + } - private VSItemComparer() - { - } + private static bool IsAfterDot(ITextSnapshot snapshot, ITrackingSpan applicableToSpan) + { + var position = applicableToSpan.GetStartPoint(snapshot).Position; + return position > 0 && snapshot[position - 1] == '.'; + } - public int Compare(VSCompletionItem? x, VSCompletionItem? y) - { - if (x is null && y is null) - return 0; + private sealed class VSItemComparer : IComparer + { + public static VSItemComparer Instance { get; } = new(); - var xRoslyn = x is not null ? CompletionItemData.GetOrAddDummyRoslynItem(x) : null; - var yRoslyn = y is not null ? CompletionItemData.GetOrAddDummyRoslynItem(y) : null; + private VSItemComparer() + { + } - // Sort by default comparer of Roslyn CompletionItem - return Comparer.Default.Compare(xRoslyn, yRoslyn); - } + public int Compare(VSCompletionItem? x, VSCompletionItem? y) + { + if (x is null && y is null) + return 0; + + var xRoslyn = x is not null ? CompletionItemData.GetOrAddDummyRoslynItem(x) : null; + var yRoslyn = y is not null ? CompletionItemData.GetOrAddDummyRoslynItem(y) : null; + + // Sort by default comparer of Roslyn CompletionItem + return Comparer.Default.Compare(xRoslyn, yRoslyn); } } } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManagerProvider.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManagerProvider.cs index 8864d935aec8b..40cf401d424b8 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManagerProvider.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/ItemManagerProvider.cs @@ -11,27 +11,26 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +[Export(typeof(IAsyncCompletionItemManagerProvider))] +[Name("Roslyn Completion Service Provider")] +[ContentType(ContentTypeNames.RoslynContentType)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class ItemManagerProvider(RecentItemsManager recentItemsManager, EditorOptionsService editorOptionsService) : IAsyncCompletionItemManagerProvider { - [Export(typeof(IAsyncCompletionItemManagerProvider))] - [Name("Roslyn Completion Service Provider")] - [ContentType(ContentTypeNames.RoslynContentType)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class ItemManagerProvider(RecentItemsManager recentItemsManager, EditorOptionsService editorOptionsService) : IAsyncCompletionItemManagerProvider - { - private readonly ItemManager _instance = new ItemManager(recentItemsManager, editorOptionsService); + private readonly ItemManager _instance = new ItemManager(recentItemsManager, editorOptionsService); - public IAsyncCompletionItemManager? GetOrCreate(ITextView textView) + public IAsyncCompletionItemManager? GetOrCreate(ITextView textView) + { + if (textView.IsInLspEditorContext()) { - if (textView.IsInLspEditorContext()) - { - // If we're in an LSP editing context, we want to avoid returning a completion item manager. - // Otherwise, we'll interfere with the LSP client manager and disrupt filtering. - return null; - } - - return _instance; + // If we're in an LSP editing context, we want to avoid returning a completion item manager. + // Otherwise, we'll interfere with the LSP client manager and disrupt filtering. + return null; } + + return _instance; } } diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/RecentItemsManager.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/RecentItemsManager.cs index d92463382c888..e456a27fa7992 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/RecentItemsManager.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/RecentItemsManager.cs @@ -8,46 +8,45 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; + +[Export] +internal class RecentItemsManager { - [Export] - internal class RecentItemsManager - { - private const int MaxMRUSize = 10; + private const int MaxMRUSize = 10; - /// - /// Guard for - /// - private readonly object _mruUpdateLock = new(); + /// + /// Guard for + /// + private readonly object _mruUpdateLock = new(); - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public RecentItemsManager() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RecentItemsManager() + { + } - private ImmutableArray RecentItems { get; set; } = []; + private ImmutableArray RecentItems { get; set; } = []; - public void MakeMostRecentItem(CompletionItem item) + public void MakeMostRecentItem(CompletionItem item) + { + lock (_mruUpdateLock) { - lock (_mruUpdateLock) - { - var items = RecentItems; - items = items.Remove(item.FilterText); + var items = RecentItems; + items = items.Remove(item.FilterText); - if (items.Length == MaxMRUSize) - { - // Remove the least recent item. - items = items.RemoveAt(0); - } - - RecentItems = items.Add(item.FilterText); + if (items.Length == MaxMRUSize) + { + // Remove the least recent item. + items = items.RemoveAt(0); } - } - public int GetRecentItemIndex(CompletionItem item) - { - return RecentItems.IndexOf(item.FilterText); + RecentItems = items.Add(item.FilterText); } } + + public int GetRecentItemIndex(CompletionItem item) + { + return RecentItems.IndexOf(item.FilterText); + } } diff --git a/src/EditorFeatures/Core/IntelliSense/Helpers.cs b/src/EditorFeatures/Core/IntelliSense/Helpers.cs index f2636aae1d5dd..d864f4747537e 100644 --- a/src/EditorFeatures/Core/IntelliSense/Helpers.cs +++ b/src/EditorFeatures/Core/IntelliSense/Helpers.cs @@ -20,259 +20,258 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense; + +internal static class Helpers { - internal static class Helpers + internal static IReadOnlyCollection BuildInteractiveTextElements( + ImmutableArray taggedTexts, + IntellisenseQuickInfoBuilderContext? context) { - internal static IReadOnlyCollection BuildInteractiveTextElements( - ImmutableArray taggedTexts, - IntellisenseQuickInfoBuilderContext? context) - { - var index = 0; - return BuildInteractiveTextElements(taggedTexts, ref index, context); - } + var index = 0; + return BuildInteractiveTextElements(taggedTexts, ref index, context); + } - private static IReadOnlyCollection BuildInteractiveTextElements( - ImmutableArray taggedTexts, - ref int index, - IntellisenseQuickInfoBuilderContext? context) - { - // This method produces a sequence of zero or more paragraphs - var paragraphs = new List(); + private static IReadOnlyCollection BuildInteractiveTextElements( + ImmutableArray taggedTexts, + ref int index, + IntellisenseQuickInfoBuilderContext? context) + { + // This method produces a sequence of zero or more paragraphs + var paragraphs = new List(); - // Each paragraph is constructed from one or more lines - var currentParagraph = new List(); + // Each paragraph is constructed from one or more lines + var currentParagraph = new List(); - // Each line is constructed from one or more inline elements - var currentRuns = new List(); + // Each line is constructed from one or more inline elements + var currentRuns = new List(); - while (index < taggedTexts.Length) + while (index < taggedTexts.Length) + { + var part = taggedTexts[index]; + + // These tags can be ignored - they are for markdown formatting only. + if (part.Tag is TextTags.CodeBlockStart or TextTags.CodeBlockEnd) { - var part = taggedTexts[index]; + index++; + continue; + } - // These tags can be ignored - they are for markdown formatting only. - if (part.Tag is TextTags.CodeBlockStart or TextTags.CodeBlockEnd) + if (part.Tag == TextTags.ContainerStart) + { + if (currentRuns.Count > 0) { - index++; - continue; + // This line break means the end of a line within a paragraph. + currentParagraph.Add(new ClassifiedTextElement(currentRuns)); + currentRuns.Clear(); } - if (part.Tag == TextTags.ContainerStart) + index++; + var nestedElements = BuildInteractiveTextElements(taggedTexts, ref index, context); + if (nestedElements.Count <= 1) { - if (currentRuns.Count > 0) - { - // This line break means the end of a line within a paragraph. - currentParagraph.Add(new ClassifiedTextElement(currentRuns)); - currentRuns.Clear(); - } - - index++; - var nestedElements = BuildInteractiveTextElements(taggedTexts, ref index, context); - if (nestedElements.Count <= 1) - { - currentParagraph.Add(new ContainerElement( - ContainerElementStyle.Wrapped, - new ClassifiedTextElement(new ClassifiedTextRun(ClassificationTypeNames.Text, part.Text)), - new ContainerElement(ContainerElementStyle.Stacked, nestedElements))); - } - else - { - currentParagraph.Add(new ContainerElement( - ContainerElementStyle.Wrapped, - new ClassifiedTextElement(new ClassifiedTextRun(ClassificationTypeNames.Text, part.Text)), - new ContainerElement( - ContainerElementStyle.Stacked, - nestedElements.First(), - new ContainerElement( - ContainerElementStyle.Stacked | ContainerElementStyle.VerticalPadding, - nestedElements.Skip(1))))); - } - - index++; - continue; + currentParagraph.Add(new ContainerElement( + ContainerElementStyle.Wrapped, + new ClassifiedTextElement(new ClassifiedTextRun(ClassificationTypeNames.Text, part.Text)), + new ContainerElement(ContainerElementStyle.Stacked, nestedElements))); } - else if (part.Tag == TextTags.ContainerEnd) + else { - // Return the current result and let the caller continue - break; + currentParagraph.Add(new ContainerElement( + ContainerElementStyle.Wrapped, + new ClassifiedTextElement(new ClassifiedTextRun(ClassificationTypeNames.Text, part.Text)), + new ContainerElement( + ContainerElementStyle.Stacked, + nestedElements.First(), + new ContainerElement( + ContainerElementStyle.Stacked | ContainerElementStyle.VerticalPadding, + nestedElements.Skip(1))))); } - if (part.Tag is TextTags.ContainerStart - or TextTags.ContainerEnd) + index++; + continue; + } + else if (part.Tag == TextTags.ContainerEnd) + { + // Return the current result and let the caller continue + break; + } + + if (part.Tag is TextTags.ContainerStart + or TextTags.ContainerEnd) + { + index++; + continue; + } + + if (part.Tag == TextTags.LineBreak) + { + if (currentRuns.Count > 0) { - index++; - continue; + // This line break means the end of a line within a paragraph. + currentParagraph.Add(new ClassifiedTextElement(currentRuns)); + currentRuns.Clear(); } - - if (part.Tag == TextTags.LineBreak) + else { - if (currentRuns.Count > 0) + // This line break means the end of a paragraph. Empty paragraphs are ignored, but could appear + // in the input to this method: + // + // * Empty elements + // * Explicit line breaks at the start of a comment + // * Multiple line breaks between paragraphs + if (currentParagraph.Count > 0) { - // This line break means the end of a line within a paragraph. - currentParagraph.Add(new ClassifiedTextElement(currentRuns)); - currentRuns.Clear(); + // The current paragraph is not empty, so add it to the result collection + paragraphs.Add(CreateParagraphFromLines(currentParagraph)); + currentParagraph.Clear(); } else { - // This line break means the end of a paragraph. Empty paragraphs are ignored, but could appear - // in the input to this method: - // - // * Empty elements - // * Explicit line breaks at the start of a comment - // * Multiple line breaks between paragraphs - if (currentParagraph.Count > 0) - { - // The current paragraph is not empty, so add it to the result collection - paragraphs.Add(CreateParagraphFromLines(currentParagraph)); - currentParagraph.Clear(); - } - else - { - // The current paragraph is empty, so we simply ignore it. - } + // The current paragraph is empty, so we simply ignore it. } } - else + } + else + { + // This is tagged text getting added to the current line we are building. + var style = GetClassifiedTextRunStyle(part.Style); + if (part.NavigationTarget is not null && + context?.ThreadingContext is { } threadingContext && + context?.OperationExecutor is { } operationExecutor && + context?.AsynchronousOperationListener is { } asyncListener && + context?.StreamingPresenter is { } streamingPresenter) { - // This is tagged text getting added to the current line we are building. - var style = GetClassifiedTextRunStyle(part.Style); - if (part.NavigationTarget is not null && - context?.ThreadingContext is { } threadingContext && - context?.OperationExecutor is { } operationExecutor && - context?.AsynchronousOperationListener is { } asyncListener && - context?.StreamingPresenter is { } streamingPresenter) + var document = context.Document; + if (Uri.TryCreate(part.NavigationTarget, UriKind.Absolute, out var absoluteUri)) { - var document = context.Document; - if (Uri.TryCreate(part.NavigationTarget, UriKind.Absolute, out var absoluteUri)) - { - var target = new QuickInfoHyperLink(document.Project.Solution.Workspace, absoluteUri); - var tooltip = part.NavigationHint; - currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, target.NavigationAction, tooltip, style)); - } - else - { - // ⚠ PERF: avoid capturing Solution (including indirectly through Project or Document - // instances) as part of the navigationAction delegate. - var target = part.NavigationTarget; - var tooltip = part.NavigationHint; - var documentId = document.Id; - var workspace = document.Project.Solution.Workspace; - currentRuns.Add(new ClassifiedTextRun( - part.Tag.ToClassificationTypeName(), part.Text, - () => _ = NavigateToQuickInfoTargetAsync(target, workspace, documentId, threadingContext, operationExecutor, asyncListener, streamingPresenter.Value), - tooltip, style)); - } + var target = new QuickInfoHyperLink(document.Project.Solution.Workspace, absoluteUri); + var tooltip = part.NavigationHint; + currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, target.NavigationAction, tooltip, style)); } else { - currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, style)); + // ⚠ PERF: avoid capturing Solution (including indirectly through Project or Document + // instances) as part of the navigationAction delegate. + var target = part.NavigationTarget; + var tooltip = part.NavigationHint; + var documentId = document.Id; + var workspace = document.Project.Solution.Workspace; + currentRuns.Add(new ClassifiedTextRun( + part.Tag.ToClassificationTypeName(), part.Text, + () => _ = NavigateToQuickInfoTargetAsync(target, workspace, documentId, threadingContext, operationExecutor, asyncListener, streamingPresenter.Value), + tooltip, style)); } } - - index++; + else + { + currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, style)); + } } - if (currentRuns.Count > 0) - { - // Add the final line to the final paragraph. - currentParagraph.Add(new ClassifiedTextElement(currentRuns)); - } + index++; + } - if (currentParagraph.Count > 0) - { - // Add the final paragraph to the result. - paragraphs.Add(CreateParagraphFromLines(currentParagraph)); - } + if (currentRuns.Count > 0) + { + // Add the final line to the final paragraph. + currentParagraph.Add(new ClassifiedTextElement(currentRuns)); + } - return paragraphs; + if (currentParagraph.Count > 0) + { + // Add the final paragraph to the result. + paragraphs.Add(CreateParagraphFromLines(currentParagraph)); } - private static async Task NavigateToQuickInfoTargetAsync( - string navigationTarget, - Workspace workspace, - DocumentId documentId, - IThreadingContext threadingContext, - IUIThreadOperationExecutor operationExecutor, - IAsynchronousOperationListener asyncListener, - IStreamingFindUsagesPresenter streamingPresenter) + return paragraphs; + } + + private static async Task NavigateToQuickInfoTargetAsync( + string navigationTarget, + Workspace workspace, + DocumentId documentId, + IThreadingContext threadingContext, + IUIThreadOperationExecutor operationExecutor, + IAsynchronousOperationListener asyncListener, + IStreamingFindUsagesPresenter streamingPresenter) + { + try { + using var token = asyncListener.BeginAsyncOperation(nameof(NavigateToQuickInfoTargetAsync)); + using var context = operationExecutor.BeginExecute(EditorFeaturesResources.IntelliSense, EditorFeaturesResources.Navigating, allowCancellation: true, showProgress: false); + + var cancellationToken = context.UserCancellationToken; + var solution = workspace.CurrentSolution; + SymbolKeyResolution resolvedSymbolKey; try { - using var token = asyncListener.BeginAsyncOperation(nameof(NavigateToQuickInfoTargetAsync)); - using var context = operationExecutor.BeginExecute(EditorFeaturesResources.IntelliSense, EditorFeaturesResources.Navigating, allowCancellation: true, showProgress: false); - - var cancellationToken = context.UserCancellationToken; - var solution = workspace.CurrentSolution; - SymbolKeyResolution resolvedSymbolKey; - try - { - var project = solution.GetRequiredProject(documentId.ProjectId); - var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - resolvedSymbolKey = SymbolKey.ResolveString(navigationTarget, compilation, cancellationToken: cancellationToken); - } - catch - { - // Ignore symbol resolution failures. It likely is just a badly formed URI. - return; - } - - if (resolvedSymbolKey.GetAnySymbol() is { } symbol) - { - var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync( - symbol, solution, threadingContext, streamingPresenter, cancellationToken).ConfigureAwait(false); - await location.TryNavigateToAsync(threadingContext, new NavigationOptions(PreferProvisionalTab: true, ActivateTab: true), cancellationToken).ConfigureAwait(false); - } + var project = solution.GetRequiredProject(documentId.ProjectId); + var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + resolvedSymbolKey = SymbolKey.ResolveString(navigationTarget, compilation, cancellationToken: cancellationToken); } - catch (OperationCanceledException) + catch { + // Ignore symbol resolution failures. It likely is just a badly formed URI. + return; } - catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical)) + + if (resolvedSymbolKey.GetAnySymbol() is { } symbol) { + var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync( + symbol, solution, threadingContext, streamingPresenter, cancellationToken).ConfigureAwait(false); + await location.TryNavigateToAsync(threadingContext, new NavigationOptions(PreferProvisionalTab: true, ActivateTab: true), cancellationToken).ConfigureAwait(false); } } - - private static ClassifiedTextRunStyle GetClassifiedTextRunStyle(TaggedTextStyle style) + catch (OperationCanceledException) { - var result = ClassifiedTextRunStyle.Plain; - if ((style & TaggedTextStyle.Emphasis) == TaggedTextStyle.Emphasis) - { - result |= ClassifiedTextRunStyle.Italic; - } - - if ((style & TaggedTextStyle.Strong) == TaggedTextStyle.Strong) - { - result |= ClassifiedTextRunStyle.Bold; - } + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical)) + { + } + } - if ((style & TaggedTextStyle.Underline) == TaggedTextStyle.Underline) - { - result |= ClassifiedTextRunStyle.Underline; - } + private static ClassifiedTextRunStyle GetClassifiedTextRunStyle(TaggedTextStyle style) + { + var result = ClassifiedTextRunStyle.Plain; + if ((style & TaggedTextStyle.Emphasis) == TaggedTextStyle.Emphasis) + { + result |= ClassifiedTextRunStyle.Italic; + } - if ((style & TaggedTextStyle.Code) == TaggedTextStyle.Code) - { - result |= ClassifiedTextRunStyle.UseClassificationFont; - } + if ((style & TaggedTextStyle.Strong) == TaggedTextStyle.Strong) + { + result |= ClassifiedTextRunStyle.Bold; + } - return result; + if ((style & TaggedTextStyle.Underline) == TaggedTextStyle.Underline) + { + result |= ClassifiedTextRunStyle.Underline; } - internal static object CreateParagraphFromLines(IReadOnlyList lines) + if ((style & TaggedTextStyle.Code) == TaggedTextStyle.Code) { - Contract.ThrowIfFalse(lines.Count > 0); + result |= ClassifiedTextRunStyle.UseClassificationFont; + } - if (lines.Count == 1) - { - // The paragraph contains only one line, so it doesn't need to be added to a container. Avoiding the - // wrapping container here also avoids a wrapping element in the WPF elements used for rendering, - // improving efficiency. - return lines[0]; - } - else - { - // The lines of a multi-line paragraph are stacked to produce the full paragraph. - return new ContainerElement(ContainerElementStyle.Stacked, lines); - } + return result; + } + + internal static object CreateParagraphFromLines(IReadOnlyList lines) + { + Contract.ThrowIfFalse(lines.Count > 0); + + if (lines.Count == 1) + { + // The paragraph contains only one line, so it doesn't need to be added to a container. Avoiding the + // wrapping container here also avoids a wrapping element in the WPF elements used for rendering, + // improving efficiency. + return lines[0]; + } + else + { + // The lines of a multi-line paragraph are stacked to produce the full paragraph. + return new ContainerElement(ContainerElementStyle.Stacked, lines); } } } diff --git a/src/EditorFeatures/Core/IntelliSense/IController.cs b/src/EditorFeatures/Core/IntelliSense/IController.cs index 0d076bd219c95..9794c94b5cbc5 100644 --- a/src/EditorFeatures/Core/IntelliSense/IController.cs +++ b/src/EditorFeatures/Core/IntelliSense/IController.cs @@ -7,12 +7,11 @@ using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis.Shared.TestHooks; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense; + +internal interface IController { - internal interface IController - { - void OnModelUpdated(TModel result, bool updateController); - IAsyncToken BeginAsyncOperation(string name = "", object tag = null, [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0); - void StopModelComputation(); - } + void OnModelUpdated(TModel result, bool updateController); + IAsyncToken BeginAsyncOperation(string name = "", object tag = null, [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0); + void StopModelComputation(); } diff --git a/src/EditorFeatures/Core/IntelliSense/IDocumentProvider.cs b/src/EditorFeatures/Core/IntelliSense/IDocumentProvider.cs index ec8ffc40e7ce9..7df3f9ac750c9 100644 --- a/src/EditorFeatures/Core/IntelliSense/IDocumentProvider.cs +++ b/src/EditorFeatures/Core/IntelliSense/IDocumentProvider.cs @@ -10,21 +10,20 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense; + +internal interface IDocumentProvider { - internal interface IDocumentProvider - { - Document GetDocument(ITextSnapshot snapshot, CancellationToken cancellationToken); - } + Document GetDocument(ITextSnapshot snapshot, CancellationToken cancellationToken); +} - internal class DocumentProvider(IThreadingContext threadingContext) : IDocumentProvider - { - private readonly IThreadingContext _threadingContext = threadingContext; +internal class DocumentProvider(IThreadingContext threadingContext) : IDocumentProvider +{ + private readonly IThreadingContext _threadingContext = threadingContext; - public Document GetDocument(ITextSnapshot snapshot, CancellationToken cancellationToken) - { - _threadingContext.ThrowIfNotOnBackgroundThread(); - return snapshot.AsText().GetDocumentWithFrozenPartialSemantics(cancellationToken); - } + public Document GetDocument(ITextSnapshot snapshot, CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnBackgroundThread(); + return snapshot.AsText().GetDocumentWithFrozenPartialSemantics(cancellationToken); } } diff --git a/src/EditorFeatures/Core/IntelliSense/ISession.cs b/src/EditorFeatures/Core/IntelliSense/ISession.cs index 0390dcdaf941e..d68a14cdc820e 100644 --- a/src/EditorFeatures/Core/IntelliSense/ISession.cs +++ b/src/EditorFeatures/Core/IntelliSense/ISession.cs @@ -4,14 +4,13 @@ #nullable disable -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense; + +internal interface ISession { - internal interface ISession - { - TModel InitialUnfilteredModel { get; } + TModel InitialUnfilteredModel { get; } - void Stop(); + void Stop(); - TModel WaitForController(); - } + TModel WaitForController(); } diff --git a/src/EditorFeatures/Core/IntelliSense/ImportCompletionCacheService/EditorExtensionMethodImportCompletionCacheServiceFactory.cs b/src/EditorFeatures/Core/IntelliSense/ImportCompletionCacheService/EditorExtensionMethodImportCompletionCacheServiceFactory.cs index 89b8ae49b3a44..f16343682813e 100644 --- a/src/EditorFeatures/Core/IntelliSense/ImportCompletionCacheService/EditorExtensionMethodImportCompletionCacheServiceFactory.cs +++ b/src/EditorFeatures/Core/IntelliSense/ImportCompletionCacheService/EditorExtensionMethodImportCompletionCacheServiceFactory.cs @@ -9,15 +9,14 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.TestHooks; -namespace Microsoft.CodeAnalysis.IntelliSense +namespace Microsoft.CodeAnalysis.IntelliSense; + +// This is the implementation at Editor layer to provide a CancellationToken +// for the workqueue used for background cache refresh. +[ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Editor), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class EditorExtensionMethodImportCompletionCacheServiceFactory(IAsynchronousOperationListenerProvider listenerProvider, IThreadingContext threadingContext) + : AbstractImportCompletionCacheServiceFactory(listenerProvider, ExtensionMethodImportCompletionHelper.BatchUpdateCacheAsync, threadingContext.DisposalToken) { - // This is the implementation at Editor layer to provide a CancellationToken - // for the workqueue used for background cache refresh. - [ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Editor), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class EditorExtensionMethodImportCompletionCacheServiceFactory(IAsynchronousOperationListenerProvider listenerProvider, IThreadingContext threadingContext) - : AbstractImportCompletionCacheServiceFactory(listenerProvider, ExtensionMethodImportCompletionHelper.BatchUpdateCacheAsync, threadingContext.DisposalToken) - { - } } diff --git a/src/EditorFeatures/Core/IntelliSense/ImportCompletionCacheService/EditorTypeImportCompletionCacheServiceFactory.cs b/src/EditorFeatures/Core/IntelliSense/ImportCompletionCacheService/EditorTypeImportCompletionCacheServiceFactory.cs index 8037c66864e91..b604c2927f27e 100644 --- a/src/EditorFeatures/Core/IntelliSense/ImportCompletionCacheService/EditorTypeImportCompletionCacheServiceFactory.cs +++ b/src/EditorFeatures/Core/IntelliSense/ImportCompletionCacheService/EditorTypeImportCompletionCacheServiceFactory.cs @@ -9,15 +9,14 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.TestHooks; -namespace Microsoft.CodeAnalysis.IntelliSense +namespace Microsoft.CodeAnalysis.IntelliSense; + +// This is the implementation at Editor layer to provide a CancellationToken +// for the workqueue used for background cache refresh. +[ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Editor), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class EditorTypeImportCompletionCacheServiceFactory(IAsynchronousOperationListenerProvider listenerProvider, IThreadingContext threadingContext) + : AbstractImportCompletionCacheServiceFactory(listenerProvider, AbstractTypeImportCompletionService.BatchUpdateCacheAsync, threadingContext.DisposalToken) { - // This is the implementation at Editor layer to provide a CancellationToken - // for the workqueue used for background cache refresh. - [ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Editor), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class EditorTypeImportCompletionCacheServiceFactory(IAsynchronousOperationListenerProvider listenerProvider, IThreadingContext threadingContext) - : AbstractImportCompletionCacheServiceFactory(listenerProvider, AbstractTypeImportCompletionService.BatchUpdateCacheAsync, threadingContext.DisposalToken) - { - } } diff --git a/src/EditorFeatures/Core/IntelliSense/ModelComputation.cs b/src/EditorFeatures/Core/IntelliSense/ModelComputation.cs index 7a8e5c4056cf4..e75fcf801da6f 100644 --- a/src/EditorFeatures/Core/IntelliSense/ModelComputation.cs +++ b/src/EditorFeatures/Core/IntelliSense/ModelComputation.cs @@ -14,177 +14,176 @@ using Microsoft.CodeAnalysis.Shared.TestHooks; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense; + +internal class ModelComputation where TModel : class { - internal class ModelComputation where TModel : class - { - #region Fields that can be accessed from either thread + #region Fields that can be accessed from either thread - public readonly IThreadingContext ThreadingContext; - private readonly CancellationToken _stopCancellationToken; + public readonly IThreadingContext ThreadingContext; + private readonly CancellationToken _stopCancellationToken; - /// - /// Set when the first compute task completes - /// - private TModel _initialUnfilteredModel = null; + /// + /// Set when the first compute task completes + /// + private TModel _initialUnfilteredModel = null; - #endregion + #endregion - #region Fields that can only be accessed from the foreground thread + #region Fields that can only be accessed from the foreground thread - private readonly IController _controller; - private readonly TaskScheduler __taskScheduler; + private readonly IController _controller; + private readonly TaskScheduler __taskScheduler; - private TaskScheduler _taskScheduler + private TaskScheduler _taskScheduler + { + get { - get - { - ThreadingContext.ThrowIfNotOnUIThread(); - return __taskScheduler; - } + ThreadingContext.ThrowIfNotOnUIThread(); + return __taskScheduler; } + } - private readonly CancellationTokenSource _stopTokenSource; - - // There may be multiple compute tasks chained together. When a compute task finishes it - // may end up with a null model (i.e. it found no items). At that point *if* it is the - // *last* compute task, then we will want to stop everything. However, if it is not the - // last compute task, then we just want to ignore that result and allow the actual - // latest compute task to proceed. - private Task _lastTask; - private Task _notifyControllerTask; + private readonly CancellationTokenSource _stopTokenSource; - #endregion + // There may be multiple compute tasks chained together. When a compute task finishes it + // may end up with a null model (i.e. it found no items). At that point *if* it is the + // *last* compute task, then we will want to stop everything. However, if it is not the + // last compute task, then we just want to ignore that result and allow the actual + // latest compute task to proceed. + private Task _lastTask; + private Task _notifyControllerTask; - public ModelComputation( - IThreadingContext threadingContext, - IController controller, - TaskScheduler computationTaskScheduler) - { - ThreadingContext = threadingContext; - _controller = controller; - __taskScheduler = computationTaskScheduler; + #endregion - _stopTokenSource = new CancellationTokenSource(); - _stopCancellationToken = _stopTokenSource.Token; + public ModelComputation( + IThreadingContext threadingContext, + IController controller, + TaskScheduler computationTaskScheduler) + { + ThreadingContext = threadingContext; + _controller = controller; + __taskScheduler = computationTaskScheduler; - // Dummy up a new task so we don't need to check for null. - _notifyControllerTask = _lastTask = SpecializedTasks.Null(); - } + _stopTokenSource = new CancellationTokenSource(); + _stopCancellationToken = _stopTokenSource.Token; - public TModel InitialUnfilteredModel - { - get - { - ThreadingContext.ThrowIfNotOnUIThread(); - return _initialUnfilteredModel; - } - } + // Dummy up a new task so we don't need to check for null. + _notifyControllerTask = _lastTask = SpecializedTasks.Null(); + } - public Task ModelTask + public TModel InitialUnfilteredModel + { + get { - get - { - ThreadingContext.ThrowIfNotOnUIThread(); - - // We should never be called if we were stopped. - Contract.ThrowIfTrue(_stopCancellationToken.IsCancellationRequested); - return _lastTask; - } + ThreadingContext.ThrowIfNotOnUIThread(); + return _initialUnfilteredModel; } + } - public TModel WaitForController() + public Task ModelTask + { + get { ThreadingContext.ThrowIfNotOnUIThread(); - var model = ModelTask.WaitAndGetResult(CancellationToken.None); - if (!_notifyControllerTask.IsCompleted) - { - OnModelUpdated(model, updateController: true); - - // Reset lastTask so controller.OnModelUpdated is only called once - _lastTask = Task.FromResult(model); - } - - return model; + // We should never be called if we were stopped. + Contract.ThrowIfTrue(_stopCancellationToken.IsCancellationRequested); + return _lastTask; } + } - public virtual void Stop() - { - ThreadingContext.ThrowIfNotOnUIThread(); + public TModel WaitForController() + { + ThreadingContext.ThrowIfNotOnUIThread(); - // cancel all outstanding tasks. - _stopTokenSource.Cancel(); + var model = ModelTask.WaitAndGetResult(CancellationToken.None); + if (!_notifyControllerTask.IsCompleted) + { + OnModelUpdated(model, updateController: true); - // reset task so that it doesn't hold onto things like WpfTextView - _notifyControllerTask = _lastTask = SpecializedTasks.Null(); + // Reset lastTask so controller.OnModelUpdated is only called once + _lastTask = Task.FromResult(model); } - public void ChainTaskAndNotifyControllerWhenFinished( - Func transformModel, - bool updateController = true) - { - ChainTaskAndNotifyControllerWhenFinished((m, c) => Task.FromResult(transformModel(m)), updateController); - } + return model; + } - public void ChainTaskAndNotifyControllerWhenFinished( - Func> transformModelAsync, - bool updateController = true) - { - ThreadingContext.ThrowIfNotOnUIThread(); + public virtual void Stop() + { + ThreadingContext.ThrowIfNotOnUIThread(); - Contract.ThrowIfTrue(_stopCancellationToken.IsCancellationRequested, "should not chain tasks after we've been cancelled"); + // cancel all outstanding tasks. + _stopTokenSource.Cancel(); - // Mark that an async operation has begun. This way tests know to wait until the - // async operation is done before verifying results. We will consider this - // background task complete when its result has finally been displayed on the UI. - var asyncToken = _controller.BeginAsyncOperation(); + // reset task so that it doesn't hold onto things like WpfTextView + _notifyControllerTask = _lastTask = SpecializedTasks.Null(); + } - // Create the task that will actually run the transformation step. - var nextTask = _lastTask.SafeContinueWithFromAsync( - t => transformModelAsync(t.Result, _stopCancellationToken), - _stopCancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, _taskScheduler); + public void ChainTaskAndNotifyControllerWhenFinished( + Func transformModel, + bool updateController = true) + { + ChainTaskAndNotifyControllerWhenFinished((m, c) => Task.FromResult(transformModel(m)), updateController); + } - // The next task is now the last task in the chain. - _lastTask = nextTask; + public void ChainTaskAndNotifyControllerWhenFinished( + Func> transformModelAsync, + bool updateController = true) + { + ThreadingContext.ThrowIfNotOnUIThread(); + + Contract.ThrowIfTrue(_stopCancellationToken.IsCancellationRequested, "should not chain tasks after we've been cancelled"); + + // Mark that an async operation has begun. This way tests know to wait until the + // async operation is done before verifying results. We will consider this + // background task complete when its result has finally been displayed on the UI. + var asyncToken = _controller.BeginAsyncOperation(); + + // Create the task that will actually run the transformation step. + var nextTask = _lastTask.SafeContinueWithFromAsync( + t => transformModelAsync(t.Result, _stopCancellationToken), + _stopCancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, _taskScheduler); + + // The next task is now the last task in the chain. + _lastTask = nextTask; + + // When this task is complete *and* the last notification to the controller is + // complete, then issue the next notification to the controller. When we try to + // issue the notification, see if we're still at the end of the chain. If we're not, + // then we don't need to notify as a later task will do so. + _notifyControllerTask = Task.Factory.ContinueWhenAll( + [_notifyControllerTask, nextTask], + async tasks => + { + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _stopCancellationToken); - // When this task is complete *and* the last notification to the controller is - // complete, then issue the next notification to the controller. When we try to - // issue the notification, see if we're still at the end of the chain. If we're not, - // then we don't need to notify as a later task will do so. - _notifyControllerTask = Task.Factory.ContinueWhenAll( - [_notifyControllerTask, nextTask], - async tasks => + if (tasks.All(t => t.Status == TaskStatus.RanToCompletion)) { - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _stopCancellationToken); - - if (tasks.All(t => t.Status == TaskStatus.RanToCompletion)) - { - _stopCancellationToken.ThrowIfCancellationRequested(); - - // Check if we're still the last task. If so then we should update the - // controller. Otherwise there's a pending task that should run. We - // don't need to update the controller (and the presenters) until our - // chain is finished. - updateController &= nextTask == _lastTask; - OnModelUpdated(nextTask.Result, updateController); - } - }, - _stopCancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default).Unwrap(); - - // When we've notified the controller of our result, we consider the async operation - // to be completed. - _notifyControllerTask.CompletesAsyncOperation(asyncToken); - } + _stopCancellationToken.ThrowIfCancellationRequested(); + + // Check if we're still the last task. If so then we should update the + // controller. Otherwise there's a pending task that should run. We + // don't need to update the controller (and the presenters) until our + // chain is finished. + updateController &= nextTask == _lastTask; + OnModelUpdated(nextTask.Result, updateController); + } + }, + _stopCancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default).Unwrap(); + + // When we've notified the controller of our result, we consider the async operation + // to be completed. + _notifyControllerTask.CompletesAsyncOperation(asyncToken); + } - private void OnModelUpdated(TModel result, bool updateController) - { - this.ThreadingContext.ThrowIfNotOnUIThread(); + private void OnModelUpdated(TModel result, bool updateController) + { + this.ThreadingContext.ThrowIfNotOnUIThread(); - // Store the first result so that anyone who cares knows we've computed something - _initialUnfilteredModel ??= result; + // Store the first result so that anyone who cares knows we've computed something + _initialUnfilteredModel ??= result; - _controller.OnModelUpdated(result, updateController); - } + _controller.OnModelUpdated(result, updateController); } } diff --git a/src/EditorFeatures/Core/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs b/src/EditorFeatures/Core/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs index 504cecfd894cf..3515897e3310b 100644 --- a/src/EditorFeatures/Core/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs +++ b/src/EditorFeatures/Core/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs @@ -22,154 +22,153 @@ using CodeAnalysisQuickInfoItem = Microsoft.CodeAnalysis.QuickInfo.QuickInfoItem; using IntellisenseQuickInfoItem = Microsoft.VisualStudio.Language.Intellisense.QuickInfoItem; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo; + +internal static class IntellisenseQuickInfoBuilder { - internal static class IntellisenseQuickInfoBuilder + private static async Task BuildInteractiveContentAsync( + CodeAnalysisQuickInfoItem quickInfoItem, + IntellisenseQuickInfoBuilderContext? context, + CancellationToken cancellationToken) { - private static async Task BuildInteractiveContentAsync( - CodeAnalysisQuickInfoItem quickInfoItem, - IntellisenseQuickInfoBuilderContext? context, - CancellationToken cancellationToken) + // Build the first line of QuickInfo item, the images and the Description section should be on the first line with Wrapped style + var glyphs = quickInfoItem.Tags.GetGlyphs(); + var symbolGlyph = glyphs.FirstOrDefault(g => g != Glyph.CompletionWarning); + var warningGlyph = glyphs.FirstOrDefault(g => g == Glyph.CompletionWarning); + var firstLineElements = new List(); + if (symbolGlyph != Glyph.None) { - // Build the first line of QuickInfo item, the images and the Description section should be on the first line with Wrapped style - var glyphs = quickInfoItem.Tags.GetGlyphs(); - var symbolGlyph = glyphs.FirstOrDefault(g => g != Glyph.CompletionWarning); - var warningGlyph = glyphs.FirstOrDefault(g => g == Glyph.CompletionWarning); - var firstLineElements = new List(); - if (symbolGlyph != Glyph.None) - { - firstLineElements.Add(new ImageElement(symbolGlyph.GetImageId())); - } + firstLineElements.Add(new ImageElement(symbolGlyph.GetImageId())); + } - if (warningGlyph != Glyph.None) - { - firstLineElements.Add(new ImageElement(warningGlyph.GetImageId())); - } + if (warningGlyph != Glyph.None) + { + firstLineElements.Add(new ImageElement(warningGlyph.GetImageId())); + } - var elements = new List(); - var descSection = quickInfoItem.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description); - if (descSection != null) + var elements = new List(); + var descSection = quickInfoItem.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description); + if (descSection != null) + { + var isFirstElement = true; + foreach (var element in Helpers.BuildInteractiveTextElements(descSection.TaggedParts, context)) { - var isFirstElement = true; - foreach (var element in Helpers.BuildInteractiveTextElements(descSection.TaggedParts, context)) + if (isFirstElement) { - if (isFirstElement) - { - isFirstElement = false; - firstLineElements.Add(element); - } - else - { - // If the description section contains multiple paragraphs, the second and additional paragraphs - // are not wrapped in firstLineElements (they are normal paragraphs). - elements.Add(element); - } + isFirstElement = false; + firstLineElements.Add(element); + } + else + { + // If the description section contains multiple paragraphs, the second and additional paragraphs + // are not wrapped in firstLineElements (they are normal paragraphs). + elements.Add(element); } } + } - elements.Insert(0, new ContainerElement(ContainerElementStyle.Wrapped, firstLineElements)); + elements.Insert(0, new ContainerElement(ContainerElementStyle.Wrapped, firstLineElements)); - var documentationCommentSection = quickInfoItem.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.DocumentationComments); - if (documentationCommentSection != null) + var documentationCommentSection = quickInfoItem.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.DocumentationComments); + if (documentationCommentSection != null) + { + var isFirstElement = true; + foreach (var element in Helpers.BuildInteractiveTextElements(documentationCommentSection.TaggedParts, context)) { - var isFirstElement = true; - foreach (var element in Helpers.BuildInteractiveTextElements(documentationCommentSection.TaggedParts, context)) + if (isFirstElement) { - if (isFirstElement) - { - isFirstElement = false; - - // Stack the first paragraph of the documentation comments with the last line of the description - // to avoid vertical padding between the two. - var lastElement = elements[elements.Count - 1]; - elements[elements.Count - 1] = new ContainerElement( - ContainerElementStyle.Stacked, - lastElement, - element); - } - else - { - elements.Add(element); - } + isFirstElement = false; + + // Stack the first paragraph of the documentation comments with the last line of the description + // to avoid vertical padding between the two. + var lastElement = elements[elements.Count - 1]; + elements[elements.Count - 1] = new ContainerElement( + ContainerElementStyle.Stacked, + lastElement, + element); + } + else + { + elements.Add(element); } } + } - // Add the remaining sections as Stacked style - elements.AddRange( - quickInfoItem.Sections.Where(s => s.Kind is not QuickInfoSectionKinds.Description and not QuickInfoSectionKinds.DocumentationComments) - .SelectMany(s => Helpers.BuildInteractiveTextElements(s.TaggedParts, context))); + // Add the remaining sections as Stacked style + elements.AddRange( + quickInfoItem.Sections.Where(s => s.Kind is not QuickInfoSectionKinds.Description and not QuickInfoSectionKinds.DocumentationComments) + .SelectMany(s => Helpers.BuildInteractiveTextElements(s.TaggedParts, context))); - // build text for RelatedSpan - if (quickInfoItem.RelatedSpans.Any() && context != null) + // build text for RelatedSpan + if (quickInfoItem.RelatedSpans.Any() && context != null) + { + var document = context.Document; + var textRuns = new List(); + var spanSeparatorNeededBefore = false; + foreach (var span in quickInfoItem.RelatedSpans) { - var document = context.Document; - var textRuns = new List(); - var spanSeparatorNeededBefore = false; - foreach (var span in quickInfoItem.RelatedSpans) - { - // We don't present additive-spans (like static/reassigned-variable) any differently, so strip them - // out of the classifications we get back. - var classifiedSpans = await ClassifierHelper.GetClassifiedSpansAsync( - document, span, context.ClassificationOptions, includeAdditiveSpans: false, cancellationToken).ConfigureAwait(false); + // We don't present additive-spans (like static/reassigned-variable) any differently, so strip them + // out of the classifications we get back. + var classifiedSpans = await ClassifierHelper.GetClassifiedSpansAsync( + document, span, context.ClassificationOptions, includeAdditiveSpans: false, cancellationToken).ConfigureAwait(false); - var tabSize = context.LineFormattingOptions.TabSize; + var tabSize = context.LineFormattingOptions.TabSize; - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var spans = IndentationHelper.GetSpansWithAlignedIndentation(text, classifiedSpans, tabSize); - var textRunsOfSpan = spans.Select(s => new ClassifiedTextRun(s.ClassificationType, text.GetSubText(s.TextSpan).ToString(), ClassifiedTextRunStyle.UseClassificationFont)).ToList(); - if (textRunsOfSpan.Count > 0) + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var spans = IndentationHelper.GetSpansWithAlignedIndentation(text, classifiedSpans, tabSize); + var textRunsOfSpan = spans.Select(s => new ClassifiedTextRun(s.ClassificationType, text.GetSubText(s.TextSpan).ToString(), ClassifiedTextRunStyle.UseClassificationFont)).ToList(); + if (textRunsOfSpan.Count > 0) + { + if (spanSeparatorNeededBefore) { - if (spanSeparatorNeededBefore) - { - textRuns.Add(new ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, "\r\n", ClassifiedTextRunStyle.UseClassificationFont)); - } - - textRuns.AddRange(textRunsOfSpan); - spanSeparatorNeededBefore = true; + textRuns.Add(new ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, "\r\n", ClassifiedTextRunStyle.UseClassificationFont)); } - } - if (textRuns.Any()) - { - elements.Add(new ClassifiedTextElement(textRuns)); + textRuns.AddRange(textRunsOfSpan); + spanSeparatorNeededBefore = true; } } - return new ContainerElement( - ContainerElementStyle.Stacked | ContainerElementStyle.VerticalPadding, - elements); + if (textRuns.Any()) + { + elements.Add(new ClassifiedTextElement(textRuns)); + } } - internal static async Task BuildItemAsync( - ITrackingSpan trackingSpan, - CodeAnalysisQuickInfoItem quickInfoItem, - Document document, - ClassificationOptions classificationOptions, - LineFormattingOptions lineFormattingOptions, - IThreadingContext threadingContext, - IUIThreadOperationExecutor operationExecutor, - IAsynchronousOperationListener asyncListener, - Lazy streamingPresenter, - CancellationToken cancellationToken) - { - var context = new IntellisenseQuickInfoBuilderContext(document, classificationOptions, lineFormattingOptions, threadingContext, operationExecutor, asyncListener, streamingPresenter); - var content = await BuildInteractiveContentAsync(quickInfoItem, context, cancellationToken).ConfigureAwait(false); + return new ContainerElement( + ContainerElementStyle.Stacked | ContainerElementStyle.VerticalPadding, + elements); + } - return new IntellisenseQuickInfoItem(trackingSpan, content); - } + internal static async Task BuildItemAsync( + ITrackingSpan trackingSpan, + CodeAnalysisQuickInfoItem quickInfoItem, + Document document, + ClassificationOptions classificationOptions, + LineFormattingOptions lineFormattingOptions, + IThreadingContext threadingContext, + IUIThreadOperationExecutor operationExecutor, + IAsynchronousOperationListener asyncListener, + Lazy streamingPresenter, + CancellationToken cancellationToken) + { + var context = new IntellisenseQuickInfoBuilderContext(document, classificationOptions, lineFormattingOptions, threadingContext, operationExecutor, asyncListener, streamingPresenter); + var content = await BuildInteractiveContentAsync(quickInfoItem, context, cancellationToken).ConfigureAwait(false); - /// - /// Builds the classified hover content without navigation actions and requiring - /// an instance of - /// TODO - This can be removed once LSP supports colorization in markupcontent - /// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/918138 - /// - internal static Task BuildContentWithoutNavigationActionsAsync( - CodeAnalysisQuickInfoItem quickInfoItem, - IntellisenseQuickInfoBuilderContext? context, - CancellationToken cancellationToken) - { - return BuildInteractiveContentAsync(quickInfoItem, context, cancellationToken); - } + return new IntellisenseQuickInfoItem(trackingSpan, content); + } + + /// + /// Builds the classified hover content without navigation actions and requiring + /// an instance of + /// TODO - This can be removed once LSP supports colorization in markupcontent + /// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/918138 + /// + internal static Task BuildContentWithoutNavigationActionsAsync( + CodeAnalysisQuickInfoItem quickInfoItem, + IntellisenseQuickInfoBuilderContext? context, + CancellationToken cancellationToken) + { + return BuildInteractiveContentAsync(quickInfoItem, context, cancellationToken); } } diff --git a/src/EditorFeatures/Core/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs b/src/EditorFeatures/Core/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs index 79d85a295d626..977a9868c444a 100644 --- a/src/EditorFeatures/Core/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs +++ b/src/EditorFeatures/Core/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs @@ -10,26 +10,25 @@ using Microsoft.VisualStudio.Utilities; using Microsoft.CodeAnalysis.Formatting; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo; + +/// +/// Context to build content for quick info item for intellisense. +/// +internal sealed class IntellisenseQuickInfoBuilderContext( + Document document, + ClassificationOptions classificationOptions, + LineFormattingOptions lineFormattingOptions, + IThreadingContext? threadingContext, + IUIThreadOperationExecutor? operationExecutor, + IAsynchronousOperationListener? asynchronousOperationListener, + Lazy? streamingPresenter) { - /// - /// Context to build content for quick info item for intellisense. - /// - internal sealed class IntellisenseQuickInfoBuilderContext( - Document document, - ClassificationOptions classificationOptions, - LineFormattingOptions lineFormattingOptions, - IThreadingContext? threadingContext, - IUIThreadOperationExecutor? operationExecutor, - IAsynchronousOperationListener? asynchronousOperationListener, - Lazy? streamingPresenter) - { - public Document Document { get; } = document; - public ClassificationOptions ClassificationOptions { get; } = classificationOptions; - public LineFormattingOptions LineFormattingOptions { get; } = lineFormattingOptions; - public IThreadingContext? ThreadingContext { get; } = threadingContext; - public IUIThreadOperationExecutor? OperationExecutor { get; } = operationExecutor; - public IAsynchronousOperationListener? AsynchronousOperationListener { get; } = asynchronousOperationListener; - public Lazy? StreamingPresenter { get; } = streamingPresenter; - } + public Document Document { get; } = document; + public ClassificationOptions ClassificationOptions { get; } = classificationOptions; + public LineFormattingOptions LineFormattingOptions { get; } = lineFormattingOptions; + public IThreadingContext? ThreadingContext { get; } = threadingContext; + public IUIThreadOperationExecutor? OperationExecutor { get; } = operationExecutor; + public IAsynchronousOperationListener? AsynchronousOperationListener { get; } = asynchronousOperationListener; + public Lazy? StreamingPresenter { get; } = streamingPresenter; } diff --git a/src/EditorFeatures/Core/IntelliSense/QuickInfo/Model.cs b/src/EditorFeatures/Core/IntelliSense/QuickInfo/Model.cs index 2b77a5d5a31ae..bfc1378a802b6 100644 --- a/src/EditorFeatures/Core/IntelliSense/QuickInfo/Model.cs +++ b/src/EditorFeatures/Core/IntelliSense/QuickInfo/Model.cs @@ -10,30 +10,29 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo; + +internal class Model { - internal class Model - { - public ITextVersion TextVersion { get; } - public QuickInfoItem Item { get; } - public bool TrackMouse { get; } + public ITextVersion TextVersion { get; } + public QuickInfoItem Item { get; } + public bool TrackMouse { get; } - public Model( - ITextVersion textVersion, - QuickInfoItem item, - bool trackMouse) - { - Contract.ThrowIfNull(item); + public Model( + ITextVersion textVersion, + QuickInfoItem item, + bool trackMouse) + { + Contract.ThrowIfNull(item); - this.TextVersion = textVersion; - this.Item = item; - this.TrackMouse = trackMouse; - } + this.TextVersion = textVersion; + this.Item = item; + this.TrackMouse = trackMouse; + } - internal SnapshotSpan GetCurrentSpanInSnapshot(TextSpan originalSpan, ITextSnapshot textSnapshot) - { - var trackingSpan = this.TextVersion.CreateTrackingSpan(originalSpan.ToSpan(), SpanTrackingMode.EdgeInclusive); - return trackingSpan.GetSpan(textSnapshot); - } + internal SnapshotSpan GetCurrentSpanInSnapshot(TextSpan originalSpan, ITextSnapshot textSnapshot) + { + var trackingSpan = this.TextVersion.CreateTrackingSpan(originalSpan.ToSpan(), SpanTrackingMode.EdgeInclusive); + return trackingSpan.GetSpan(textSnapshot); } } diff --git a/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoHyperLink.cs b/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoHyperLink.cs index 6cc130105d6cf..9730658f3f2f4 100644 --- a/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoHyperLink.cs +++ b/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoHyperLink.cs @@ -6,52 +6,51 @@ using System.Collections.Generic; using System.Threading; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo; + +internal sealed class QuickInfoHyperLink : IEquatable { - internal sealed class QuickInfoHyperLink : IEquatable - { - private readonly Workspace _workspace; + private readonly Workspace _workspace; - public QuickInfoHyperLink(Workspace workspace, Uri uri) - { - _workspace = workspace; - Uri = uri; + public QuickInfoHyperLink(Workspace workspace, Uri uri) + { + _workspace = workspace; + Uri = uri; - NavigationAction = OpenLink; - } + NavigationAction = OpenLink; + } - public Action NavigationAction { get; } + public Action NavigationAction { get; } - public Uri Uri { get; } + public Uri Uri { get; } - public override bool Equals(object? obj) - { - return Equals(obj as QuickInfoHyperLink); - } + public override bool Equals(object? obj) + { + return Equals(obj as QuickInfoHyperLink); + } - public bool Equals(QuickInfoHyperLink? other) - { - return EqualityComparer.Default.Equals(Uri, other?.Uri); - } + public bool Equals(QuickInfoHyperLink? other) + { + return EqualityComparer.Default.Equals(Uri, other?.Uri); + } - public override int GetHashCode() - { - return Uri.GetHashCode(); - } + public override int GetHashCode() + { + return Uri.GetHashCode(); + } - private void OpenLink() - { - var navigateToLinkService = _workspace.Services.GetRequiredService(); - _ = navigateToLinkService.TryNavigateToLinkAsync(Uri, CancellationToken.None); - } + private void OpenLink() + { + var navigateToLinkService = _workspace.Services.GetRequiredService(); + _ = navigateToLinkService.TryNavigateToLinkAsync(Uri, CancellationToken.None); + } - internal readonly struct TestAccessor + internal readonly struct TestAccessor + { + public static Action CreateNavigationAction(Uri uri) { - public static Action CreateNavigationAction(Uri uri) - { - // The workspace is not validated by tests - return new QuickInfoHyperLink(null!, uri).NavigationAction; - } + // The workspace is not validated by tests + return new QuickInfoHyperLink(null!, uri).NavigationAction; } } } diff --git a/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoSourceProvider.QuickInfoSource.cs b/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoSourceProvider.QuickInfoSource.cs index 847392ac99cf5..eebb50a490b4c 100644 --- a/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoSourceProvider.QuickInfoSource.cs +++ b/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoSourceProvider.QuickInfoSource.cs @@ -27,81 +27,80 @@ using IntellisenseQuickInfoItem = Microsoft.VisualStudio.Language.Intellisense.QuickInfoItem; using Microsoft.CodeAnalysis.Editor.InlineRename; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo; + +internal partial class QuickInfoSourceProvider { - internal partial class QuickInfoSourceProvider + private sealed class QuickInfoSource( + ITextBuffer subjectBuffer, + IThreadingContext threadingContext, + IUIThreadOperationExecutor operationExecutor, + IAsynchronousOperationListener asyncListener, + Lazy streamingPresenter, + EditorOptionsService editorOptionsService, + IInlineRenameService inlineRenameService) : IAsyncQuickInfoSource { - private sealed class QuickInfoSource( - ITextBuffer subjectBuffer, - IThreadingContext threadingContext, - IUIThreadOperationExecutor operationExecutor, - IAsynchronousOperationListener asyncListener, - Lazy streamingPresenter, - EditorOptionsService editorOptionsService, - IInlineRenameService inlineRenameService) : IAsyncQuickInfoSource - { - private readonly ITextBuffer _subjectBuffer = subjectBuffer; - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IUIThreadOperationExecutor _operationExecutor = operationExecutor; - private readonly IAsynchronousOperationListener _asyncListener = asyncListener; - private readonly Lazy _streamingPresenter = streamingPresenter; - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - private readonly IInlineRenameService _inlineRenameService = inlineRenameService; + private readonly ITextBuffer _subjectBuffer = subjectBuffer; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IUIThreadOperationExecutor _operationExecutor = operationExecutor; + private readonly IAsynchronousOperationListener _asyncListener = asyncListener; + private readonly Lazy _streamingPresenter = streamingPresenter; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly IInlineRenameService _inlineRenameService = inlineRenameService; - public async Task GetQuickInfoItemAsync(IAsyncQuickInfoSession session, CancellationToken cancellationToken) - { - // Until https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1611398 is resolved we can't disable - // quickinfo in InlineRename. Instead, we return no quickinfo information while the adornment - // is being shown. This can be removed after IFeaturesService supports disabling quickinfo - if (_editorOptionsService.GlobalOptions.GetOption(InlineRenameUIOptionsStorage.UseInlineAdornment) && _inlineRenameService.ActiveSession is not null) - return null; + public async Task GetQuickInfoItemAsync(IAsyncQuickInfoSession session, CancellationToken cancellationToken) + { + // Until https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1611398 is resolved we can't disable + // quickinfo in InlineRename. Instead, we return no quickinfo information while the adornment + // is being shown. This can be removed after IFeaturesService supports disabling quickinfo + if (_editorOptionsService.GlobalOptions.GetOption(InlineRenameUIOptionsStorage.UseInlineAdornment) && _inlineRenameService.ActiveSession is not null) + return null; - var triggerPoint = session.GetTriggerPoint(_subjectBuffer.CurrentSnapshot); - if (!triggerPoint.HasValue) - return null; + var triggerPoint = session.GetTriggerPoint(_subjectBuffer.CurrentSnapshot); + if (!triggerPoint.HasValue) + return null; - var snapshot = triggerPoint.Value.Snapshot; - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return null; + var snapshot = triggerPoint.Value.Snapshot; + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return null; - var service = QuickInfoService.GetService(document); - if (service == null) - return null; + var service = QuickInfoService.GetService(document); + if (service == null) + return null; - try + try + { + using (Logger.LogBlock(FunctionId.Get_QuickInfo_Async, cancellationToken)) { - using (Logger.LogBlock(FunctionId.Get_QuickInfo_Async, cancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - var options = _editorOptionsService.GlobalOptions.GetSymbolDescriptionOptions(document.Project.Language); - var item = await service.GetQuickInfoAsync(document, triggerPoint.Value, options, cancellationToken).ConfigureAwait(false); - if (item != null) - { - var textVersion = snapshot.Version; - var trackingSpan = textVersion.CreateTrackingSpan(item.Span.ToSpan(), SpanTrackingMode.EdgeInclusive); - var classificationOptions = _editorOptionsService.GlobalOptions.GetClassificationOptions(document.Project.Language); - var lineFormattingOptions = snapshot.TextBuffer.GetLineFormattingOptions(_editorOptionsService, explicitFormat: false); - - return await IntellisenseQuickInfoBuilder.BuildItemAsync( - trackingSpan, item, document, classificationOptions, lineFormattingOptions, - _threadingContext, _operationExecutor, - _asyncListener, _streamingPresenter, cancellationToken).ConfigureAwait(false); - } + var options = _editorOptionsService.GlobalOptions.GetSymbolDescriptionOptions(document.Project.Language); + var item = await service.GetQuickInfoAsync(document, triggerPoint.Value, options, cancellationToken).ConfigureAwait(false); + if (item != null) + { + var textVersion = snapshot.Version; + var trackingSpan = textVersion.CreateTrackingSpan(item.Span.ToSpan(), SpanTrackingMode.EdgeInclusive); + var classificationOptions = _editorOptionsService.GlobalOptions.GetClassificationOptions(document.Project.Language); + var lineFormattingOptions = snapshot.TextBuffer.GetLineFormattingOptions(_editorOptionsService, explicitFormat: false); - return null; + return await IntellisenseQuickInfoBuilder.BuildItemAsync( + trackingSpan, item, document, classificationOptions, lineFormattingOptions, + _threadingContext, _operationExecutor, + _asyncListener, _streamingPresenter, cancellationToken).ConfigureAwait(false); } - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical)) - { - throw ExceptionUtilities.Unreachable(); + + return null; } } - - public void Dispose() + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical)) { + throw ExceptionUtilities.Unreachable(); } } + + public void Dispose() + { + } } } diff --git a/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoSourceProvider.cs b/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoSourceProvider.cs index 334d5daa2f330..050b7782cd586 100644 --- a/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoSourceProvider.cs +++ b/src/EditorFeatures/Core/IntelliSense/QuickInfo/QuickInfoSourceProvider.cs @@ -17,35 +17,34 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo; + +[ContentType(ContentTypeNames.RoslynContentType)] +[Export(typeof(IAsyncQuickInfoSourceProvider))] +[Name("RoslynQuickInfoProvider")] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal partial class QuickInfoSourceProvider( + IThreadingContext threadingContext, + IUIThreadOperationExecutor operationExecutor, + IAsynchronousOperationListenerProvider listenerProvider, + Lazy streamingPresenter, + EditorOptionsService editorOptionsService, + IInlineRenameService inlineRenameService) : IAsyncQuickInfoSourceProvider { - [ContentType(ContentTypeNames.RoslynContentType)] - [Export(typeof(IAsyncQuickInfoSourceProvider))] - [Name("RoslynQuickInfoProvider")] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal partial class QuickInfoSourceProvider( - IThreadingContext threadingContext, - IUIThreadOperationExecutor operationExecutor, - IAsynchronousOperationListenerProvider listenerProvider, - Lazy streamingPresenter, - EditorOptionsService editorOptionsService, - IInlineRenameService inlineRenameService) : IAsyncQuickInfoSourceProvider - { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IUIThreadOperationExecutor _operationExecutor = operationExecutor; - private readonly Lazy _streamingPresenter = streamingPresenter; - private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.QuickInfo); - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - private readonly IInlineRenameService _inlineRenameService = inlineRenameService; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IUIThreadOperationExecutor _operationExecutor = operationExecutor; + private readonly Lazy _streamingPresenter = streamingPresenter; + private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.QuickInfo); + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly IInlineRenameService _inlineRenameService = inlineRenameService; - public IAsyncQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer) - { - if (textBuffer.IsInLspEditorContext()) - return null; + public IAsyncQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer) + { + if (textBuffer.IsInLspEditorContext()) + return null; - return new QuickInfoSource( - textBuffer, _threadingContext, _operationExecutor, _listener, _streamingPresenter, _editorOptionsService, _inlineRenameService); - } + return new QuickInfoSource( + textBuffer, _threadingContext, _operationExecutor, _listener, _streamingPresenter, _editorOptionsService, _inlineRenameService); } } diff --git a/src/EditorFeatures/Core/IntelliSense/Session.cs b/src/EditorFeatures/Core/IntelliSense/Session.cs index b9cb036383ad1..7b8336e76473c 100644 --- a/src/EditorFeatures/Core/IntelliSense/Session.cs +++ b/src/EditorFeatures/Core/IntelliSense/Session.cs @@ -9,54 +9,53 @@ using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense; + +internal class Session : ISession + where TPresenterSession : IIntelliSensePresenterSession + where TController : IController + where TModel : class { - internal class Session : ISession - where TPresenterSession : IIntelliSensePresenterSession - where TController : IController - where TModel : class + public TController Controller { get; } + public ModelComputation Computation { get; } + + // The presenter session for the computation we've got going. It's lifetime is tied 1:1 with + // the computation. When the computation starts we make a presenter (note: this does not + // mean that the user will ever see any UI), and when the computation is stopped, we will + // end the presentation session. + public TPresenterSession PresenterSession { get; } + + public Session(TController controller, ModelComputation computation, TPresenterSession presenterSession) + { + this.Controller = controller; + this.Computation = computation; + this.PresenterSession = presenterSession; + + // If the UI layer dismisses the presenter, then we want to know about it so we can stop + // doing whatever it is we're doing. + this.PresenterSession.Dismissed += OnPresenterSessionDismissed; + } + + public TModel InitialUnfilteredModel { get { return this.Computation.InitialUnfilteredModel; } } + + private void OnPresenterSessionDismissed(object sender, EventArgs e) + { + Computation.ThreadingContext.ThrowIfNotOnUIThread(); + Contract.ThrowIfFalse(ReferenceEquals(this.PresenterSession, sender)); + Controller.StopModelComputation(); + } + + public virtual void Stop() + { + Computation.ThreadingContext.ThrowIfNotOnUIThread(); + this.Computation.Stop(); + this.PresenterSession.Dismissed -= OnPresenterSessionDismissed; + this.PresenterSession.Dismiss(); + } + + public TModel WaitForController() { - public TController Controller { get; } - public ModelComputation Computation { get; } - - // The presenter session for the computation we've got going. It's lifetime is tied 1:1 with - // the computation. When the computation starts we make a presenter (note: this does not - // mean that the user will ever see any UI), and when the computation is stopped, we will - // end the presentation session. - public TPresenterSession PresenterSession { get; } - - public Session(TController controller, ModelComputation computation, TPresenterSession presenterSession) - { - this.Controller = controller; - this.Computation = computation; - this.PresenterSession = presenterSession; - - // If the UI layer dismisses the presenter, then we want to know about it so we can stop - // doing whatever it is we're doing. - this.PresenterSession.Dismissed += OnPresenterSessionDismissed; - } - - public TModel InitialUnfilteredModel { get { return this.Computation.InitialUnfilteredModel; } } - - private void OnPresenterSessionDismissed(object sender, EventArgs e) - { - Computation.ThreadingContext.ThrowIfNotOnUIThread(); - Contract.ThrowIfFalse(ReferenceEquals(this.PresenterSession, sender)); - Controller.StopModelComputation(); - } - - public virtual void Stop() - { - Computation.ThreadingContext.ThrowIfNotOnUIThread(); - this.Computation.Stop(); - this.PresenterSession.Dismissed -= OnPresenterSessionDismissed; - this.PresenterSession.Dismiss(); - } - - public TModel WaitForController() - { - Computation.ThreadingContext.ThrowIfNotOnUIThread(); - return Computation.WaitForController(); - } + Computation.ThreadingContext.ThrowIfNotOnUIThread(); + return Computation.WaitForController(); } } diff --git a/src/EditorFeatures/Core/IntelliSense/ViewTextSpan.cs b/src/EditorFeatures/Core/IntelliSense/ViewTextSpan.cs index acff0de3d3dbf..684e6fc271ffe 100644 --- a/src/EditorFeatures/Core/IntelliSense/ViewTextSpan.cs +++ b/src/EditorFeatures/Core/IntelliSense/ViewTextSpan.cs @@ -13,114 +13,113 @@ using Microsoft.VisualStudio.Text.Projection; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense; + +/// +/// Helper class to use type-safety to enforce we're using TextSpans from the +/// TextView's buffer. Intellisense primarily uses spans from the SubjectBuffer +/// which need to be mapped to ViewTextSpans before comparing to view positions +/// such as the current caret location. +/// +internal readonly struct ViewTextSpan(TextSpan textSpan) { - /// - /// Helper class to use type-safety to enforce we're using TextSpans from the - /// TextView's buffer. Intellisense primarily uses spans from the SubjectBuffer - /// which need to be mapped to ViewTextSpans before comparing to view positions - /// such as the current caret location. - /// - internal readonly struct ViewTextSpan(TextSpan textSpan) - { - public readonly TextSpan TextSpan = textSpan; - } + public readonly TextSpan TextSpan = textSpan; +} - internal readonly struct DisconnectedBufferGraph(ITextBuffer subjectBuffer, ITextBuffer viewBuffer) - { - // The subject buffer's snapshot at the point of the initial model's creation - public readonly ITextSnapshot SubjectBufferSnapshot = subjectBuffer.CurrentSnapshot; +internal readonly struct DisconnectedBufferGraph(ITextBuffer subjectBuffer, ITextBuffer viewBuffer) +{ + // The subject buffer's snapshot at the point of the initial model's creation + public readonly ITextSnapshot SubjectBufferSnapshot = subjectBuffer.CurrentSnapshot; - // The TextView's snapshot at the point of the initial model's creation - public readonly ITextSnapshot ViewSnapshot = viewBuffer.CurrentSnapshot; + // The TextView's snapshot at the point of the initial model's creation + public readonly ITextSnapshot ViewSnapshot = viewBuffer.CurrentSnapshot; - // The relation of the subject buffer to the TextView's top buffer. This information - // is used with the subjectBufferSnapshot and viewSnapshot to map spans even when - // the buffers might be temporarily disconnected during Razor/Venus remappings. - public readonly BufferMapDirection SubjectBufferToTextViewDirection = IBufferGraphExtensions.ClassifyBufferMapDirection( - subjectBuffer, - viewBuffer); + // The relation of the subject buffer to the TextView's top buffer. This information + // is used with the subjectBufferSnapshot and viewSnapshot to map spans even when + // the buffers might be temporarily disconnected during Razor/Venus remappings. + public readonly BufferMapDirection SubjectBufferToTextViewDirection = IBufferGraphExtensions.ClassifyBufferMapDirection( + subjectBuffer, + viewBuffer); - // Normally, we could just use a BufferGraph to do the mapping, but our subjectBuffer may be - // disconnected from the view when we are asked to do this mapping. - public ViewTextSpan GetSubjectBufferTextSpanInViewBuffer(TextSpan textSpan) + // Normally, we could just use a BufferGraph to do the mapping, but our subjectBuffer may be + // disconnected from the view when we are asked to do this mapping. + public ViewTextSpan GetSubjectBufferTextSpanInViewBuffer(TextSpan textSpan) + { + switch (SubjectBufferToTextViewDirection) { - switch (SubjectBufferToTextViewDirection) - { - // The view and subject buffer are the same - case BufferMapDirection.Identity: - return new ViewTextSpan(textSpan); + // The view and subject buffer are the same + case BufferMapDirection.Identity: + return new ViewTextSpan(textSpan); - // The subject buffer contains the view buffer. This happens in debugger intellisense. - case BufferMapDirection.Down: - { - var projection = SubjectBufferSnapshot as IProjectionSnapshot; - var span = MapDownToSnapshot(textSpan.ToSpan(), projection, ViewSnapshot); - return new ViewTextSpan(span.ToTextSpan()); - } + // The subject buffer contains the view buffer. This happens in debugger intellisense. + case BufferMapDirection.Down: + { + var projection = SubjectBufferSnapshot as IProjectionSnapshot; + var span = MapDownToSnapshot(textSpan.ToSpan(), projection, ViewSnapshot); + return new ViewTextSpan(span.ToTextSpan()); + } - // The view buffer contains the subject buffer. This is the typical Razor setup. - case BufferMapDirection.Up: - { - var projection = ViewSnapshot as IProjectionSnapshot; - var span = MapUpToSnapshot(textSpan.ToSpan(), SubjectBufferSnapshot, projection); - return new ViewTextSpan(span.ToTextSpan()); - } + // The view buffer contains the subject buffer. This is the typical Razor setup. + case BufferMapDirection.Up: + { + var projection = ViewSnapshot as IProjectionSnapshot; + var span = MapUpToSnapshot(textSpan.ToSpan(), SubjectBufferSnapshot, projection); + return new ViewTextSpan(span.ToTextSpan()); + } - default: - throw ExceptionUtilities.Unreachable(); - } + default: + throw ExceptionUtilities.Unreachable(); } + } - private static Span MapUpToSnapshot(Span span, ITextSnapshot start, IProjectionSnapshot target) - { - var spans = MapUpToSnapshotRecursive(new SnapshotSpan(start, span), target); - return spans.First(); - } + private static Span MapUpToSnapshot(Span span, ITextSnapshot start, IProjectionSnapshot target) + { + var spans = MapUpToSnapshotRecursive(new SnapshotSpan(start, span), target); + return spans.First(); + } - // Do a depth first search through the projection graph to find the first mapping - private static IEnumerable MapUpToSnapshotRecursive(SnapshotSpan start, IProjectionSnapshot target) + // Do a depth first search through the projection graph to find the first mapping + private static IEnumerable MapUpToSnapshotRecursive(SnapshotSpan start, IProjectionSnapshot target) + { + foreach (var source in target.SourceSnapshots) { - foreach (var source in target.SourceSnapshots) + if (source == start.Snapshot) { - if (source == start.Snapshot) + foreach (var result in target.MapFromSourceSnapshot(start)) { - foreach (var result in target.MapFromSourceSnapshot(start)) - { - yield return result; - } + yield return result; } - else if (source is IProjectionSnapshot sourceProjection) + } + else if (source is IProjectionSnapshot sourceProjection) + { + foreach (var span in MapUpToSnapshotRecursive(start, sourceProjection)) { - foreach (var span in MapUpToSnapshotRecursive(start, sourceProjection)) + foreach (var result in target.MapFromSourceSnapshot(new SnapshotSpan(source, span))) { - foreach (var result in target.MapFromSourceSnapshot(new SnapshotSpan(source, span))) - { - yield return result; - } + yield return result; } } } - - yield break; } - private static Span MapDownToSnapshot(Span span, IProjectionSnapshot start, ITextSnapshot target) + yield break; + } + + private static Span MapDownToSnapshot(Span span, IProjectionSnapshot start, ITextSnapshot target) + { + var sourceSpans = new Queue(start.MapToSourceSnapshots(span)); + while (true) { - var sourceSpans = new Queue(start.MapToSourceSnapshots(span)); - while (true) + var sourceSpan = sourceSpans.Dequeue(); + if (sourceSpan.Snapshot == target) { - var sourceSpan = sourceSpans.Dequeue(); - if (sourceSpan.Snapshot == target) - { - return sourceSpan.Span; - } - else if (sourceSpan.Snapshot is IProjectionSnapshot) + return sourceSpan.Span; + } + else if (sourceSpan.Snapshot is IProjectionSnapshot) + { + foreach (var s in (sourceSpan.Snapshot as IProjectionSnapshot).MapToSourceSnapshots(sourceSpan.Span)) { - foreach (var s in (sourceSpan.Snapshot as IProjectionSnapshot).MapToSourceSnapshots(sourceSpan.Span)) - { - sourceSpans.Enqueue(s); - } + sourceSpans.Enqueue(s); } } } diff --git a/src/EditorFeatures/Core/Interactive/CommandHandlers/ExecuteInInteractiveCommandHandler.cs b/src/EditorFeatures/Core/Interactive/CommandHandlers/ExecuteInInteractiveCommandHandler.cs index 6c05caf74714e..13a6672c12284 100644 --- a/src/EditorFeatures/Core/Interactive/CommandHandlers/ExecuteInInteractiveCommandHandler.cs +++ b/src/EditorFeatures/Core/Interactive/CommandHandlers/ExecuteInInteractiveCommandHandler.cs @@ -16,42 +16,41 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Interactive +namespace Microsoft.CodeAnalysis.Interactive; + +/// +/// Implements a execute in interactive command handler. +/// This class is separated from the +/// in order to ensure that the interactive command can be exposed without the necessity +/// to load any of the interactive dll files just to get the command's status. +/// +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.RoslynContentType)] +[Name("Interactive Command Handler")] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class ExecuteInInteractiveCommandHandler( + [ImportMany] IEnumerable> executeInInteractiveHandlers) + : ICommandHandler { - /// - /// Implements a execute in interactive command handler. - /// This class is separated from the - /// in order to ensure that the interactive command can be exposed without the necessity - /// to load any of the interactive dll files just to get the command's status. - /// - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.RoslynContentType)] - [Name("Interactive Command Handler")] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class ExecuteInInteractiveCommandHandler( - [ImportMany] IEnumerable> executeInInteractiveHandlers) - : ICommandHandler + private readonly IEnumerable> _executeInInteractiveHandlers = executeInInteractiveHandlers; + + public string DisplayName => EditorFeaturesResources.Execute_In_Interactive; + + private Lazy GetCommandHandler(ITextBuffer textBuffer) + { + return _executeInInteractiveHandlers + .Where(handler => handler.Metadata.ContentTypes.Any(textBuffer.ContentType.IsOfType)) + .SingleOrDefault(); + } + + bool ICommandHandler.ExecuteCommand(ExecuteInInteractiveCommandArgs args, CommandExecutionContext context) + => GetCommandHandler(args.SubjectBuffer)?.Value.ExecuteCommand(args, context) ?? false; + + CommandState ICommandHandler.GetCommandState(ExecuteInInteractiveCommandArgs args) { - private readonly IEnumerable> _executeInInteractiveHandlers = executeInInteractiveHandlers; - - public string DisplayName => EditorFeaturesResources.Execute_In_Interactive; - - private Lazy GetCommandHandler(ITextBuffer textBuffer) - { - return _executeInInteractiveHandlers - .Where(handler => handler.Metadata.ContentTypes.Any(textBuffer.ContentType.IsOfType)) - .SingleOrDefault(); - } - - bool ICommandHandler.ExecuteCommand(ExecuteInInteractiveCommandArgs args, CommandExecutionContext context) - => GetCommandHandler(args.SubjectBuffer)?.Value.ExecuteCommand(args, context) ?? false; - - CommandState ICommandHandler.GetCommandState(ExecuteInInteractiveCommandArgs args) - { - return GetCommandHandler(args.SubjectBuffer) == null - ? CommandState.Unavailable - : CommandState.Available; - } + return GetCommandHandler(args.SubjectBuffer) == null + ? CommandState.Unavailable + : CommandState.Available; } } diff --git a/src/EditorFeatures/Core/Interactive/Completion/InteractiveCommandCompletionService.cs b/src/EditorFeatures/Core/Interactive/Completion/InteractiveCommandCompletionService.cs index 68de884216488..231009fa8d779 100644 --- a/src/EditorFeatures/Core/Interactive/Completion/InteractiveCommandCompletionService.cs +++ b/src/EditorFeatures/Core/Interactive/Completion/InteractiveCommandCompletionService.cs @@ -9,30 +9,29 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.TestHooks; -namespace Microsoft.CodeAnalysis.Interactive +namespace Microsoft.CodeAnalysis.Interactive; + +internal sealed class InteractiveCommandCompletionService : CompletionService { - internal sealed class InteractiveCommandCompletionService : CompletionService + [ExportLanguageServiceFactory(typeof(CompletionService), InteractiveLanguageNames.InteractiveCommand), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + internal sealed class Factory(IAsynchronousOperationListenerProvider listenerProvider) : ILanguageServiceFactory { - [ExportLanguageServiceFactory(typeof(CompletionService), InteractiveLanguageNames.InteractiveCommand), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class Factory(IAsynchronousOperationListenerProvider listenerProvider) : ILanguageServiceFactory - { - private readonly IAsynchronousOperationListenerProvider _listenerProvider = listenerProvider; + private readonly IAsynchronousOperationListenerProvider _listenerProvider = listenerProvider; - public ILanguageService CreateLanguageService(HostLanguageServices languageServices) - => new InteractiveCommandCompletionService(languageServices.LanguageServices.SolutionServices, _listenerProvider); - } + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + => new InteractiveCommandCompletionService(languageServices.LanguageServices.SolutionServices, _listenerProvider); + } - private InteractiveCommandCompletionService(SolutionServices services, IAsynchronousOperationListenerProvider listenerProvider) - : base(services, listenerProvider) - { - } + private InteractiveCommandCompletionService(SolutionServices services, IAsynchronousOperationListenerProvider listenerProvider) + : base(services, listenerProvider) + { + } - public override string Language - => InteractiveLanguageNames.InteractiveCommand; + public override string Language + => InteractiveLanguageNames.InteractiveCommand; - internal override CompletionRules GetRules(CompletionOptions options) - => CompletionRules.Default; - } + internal override CompletionRules GetRules(CompletionOptions options) + => CompletionRules.Default; } diff --git a/src/EditorFeatures/Core/Interactive/ISendToInteractiveSubmissionProvider.cs b/src/EditorFeatures/Core/Interactive/ISendToInteractiveSubmissionProvider.cs index a96574c0c119d..8a9dfa1cd9d42 100644 --- a/src/EditorFeatures/Core/Interactive/ISendToInteractiveSubmissionProvider.cs +++ b/src/EditorFeatures/Core/Interactive/ISendToInteractiveSubmissionProvider.cs @@ -8,10 +8,9 @@ using Microsoft.VisualStudio.Text.Editor.Commanding; using System.Threading; -namespace Microsoft.CodeAnalysis.Interactive +namespace Microsoft.CodeAnalysis.Interactive; + +internal interface ISendToInteractiveSubmissionProvider { - internal interface ISendToInteractiveSubmissionProvider - { - string GetSelectedText(IEditorOptions editorOptions, EditorCommandArgs args, CancellationToken cancellationToken); - } + string GetSelectedText(IEditorOptions editorOptions, EditorCommandArgs args, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/Interactive/InteractiveEvaluatorLanguageInfoProvider.cs b/src/EditorFeatures/Core/Interactive/InteractiveEvaluatorLanguageInfoProvider.cs index 09c5b29cbf5ca..86c2c5b93fb24 100644 --- a/src/EditorFeatures/Core/Interactive/InteractiveEvaluatorLanguageInfoProvider.cs +++ b/src/EditorFeatures/Core/Interactive/InteractiveEvaluatorLanguageInfoProvider.cs @@ -5,17 +5,16 @@ using System; using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.Interactive +namespace Microsoft.CodeAnalysis.Interactive; + +internal abstract class InteractiveEvaluatorLanguageInfoProvider { - internal abstract class InteractiveEvaluatorLanguageInfoProvider - { - public abstract string LanguageName { get; } - public abstract CompilationOptions GetSubmissionCompilationOptions(string name, MetadataReferenceResolver metadataReferenceResolver, SourceReferenceResolver sourceReferenceResolver, ImmutableArray imports); - public abstract ParseOptions ParseOptions { get; } - public abstract CommandLineParser CommandLineParser { get; } - public abstract bool IsCompleteSubmission(string text); - public abstract string InteractiveResponseFileName { get; } - public abstract Type ReplServiceProviderType { get; } - public abstract string Extension { get; } - } + public abstract string LanguageName { get; } + public abstract CompilationOptions GetSubmissionCompilationOptions(string name, MetadataReferenceResolver metadataReferenceResolver, SourceReferenceResolver sourceReferenceResolver, ImmutableArray imports); + public abstract ParseOptions ParseOptions { get; } + public abstract CommandLineParser CommandLineParser { get; } + public abstract bool IsCompleteSubmission(string text); + public abstract string InteractiveResponseFileName { get; } + public abstract Type ReplServiceProviderType { get; } + public abstract string Extension { get; } } diff --git a/src/EditorFeatures/Core/Interactive/InteractiveSession.cs b/src/EditorFeatures/Core/Interactive/InteractiveSession.cs index db2e026ce43eb..0ee05928f38fb 100644 --- a/src/EditorFeatures/Core/Interactive/InteractiveSession.cs +++ b/src/EditorFeatures/Core/Interactive/InteractiveSession.cs @@ -26,371 +26,370 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Interactive -{ - using InteractiveHost::Microsoft.CodeAnalysis.Interactive; - using RelativePathResolver = Scripting::Microsoft.CodeAnalysis.RelativePathResolver; +namespace Microsoft.CodeAnalysis.Interactive; + +using InteractiveHost::Microsoft.CodeAnalysis.Interactive; +using RelativePathResolver = Scripting::Microsoft.CodeAnalysis.RelativePathResolver; - internal sealed class InteractiveSession : IDisposable +internal sealed class InteractiveSession : IDisposable +{ + public InteractiveHost Host { get; } + + private readonly IThreadingContext _threadingContext; + private readonly InteractiveEvaluatorLanguageInfoProvider _languageInfo; + private readonly InteractiveWorkspace _workspace; + private readonly ITextDocumentFactoryService _textDocumentFactoryService; + private readonly EditorOptionsService _editorOptionsService; + + private readonly CancellationTokenSource _shutdownCancellationSource; + + /// + /// The top level directory where all the interactive host extensions are installed (both Core and Desktop). + /// + private readonly string _hostDirectory; + + #region State only accessible by queued tasks + + // Use to serialize InteractiveHost process initialization and code execution. + // The process may restart any time and we need to react to that by clearing + // the current solution and setting up the first submission project. + // At the same time a code submission might be in progress. + // If we left these operations run in parallel we might get into a state + // inconsistent with the state of the host. + private readonly TaskQueue _taskQueue; + + private ProjectId? _lastSuccessfulSubmissionProjectId; + private ProjectId? _currentSubmissionProjectId; + public int SubmissionCount { get; private set; } + + private RemoteInitializationResult? _initializationResult; + private InteractiveHostPlatformInfo _platformInfo; + private ImmutableArray _referenceSearchPaths; + private ImmutableArray _sourceSearchPaths; + private string _workingDirectory; + private InteractiveHostOptions? _hostOptions; + + /// + /// Buffers that need to be associated with a submission project once the process initialization completes. + /// + private readonly List<(ITextBuffer buffer, string name)> _pendingBuffers = []; + + #endregion + + public InteractiveSession( + InteractiveWorkspace workspace, + IThreadingContext threadingContext, + IAsynchronousOperationListener listener, + ITextDocumentFactoryService documentFactory, + EditorOptionsService editorOptionsService, + InteractiveEvaluatorLanguageInfoProvider languageInfo, + string initialWorkingDirectory) { - public InteractiveHost Host { get; } - - private readonly IThreadingContext _threadingContext; - private readonly InteractiveEvaluatorLanguageInfoProvider _languageInfo; - private readonly InteractiveWorkspace _workspace; - private readonly ITextDocumentFactoryService _textDocumentFactoryService; - private readonly EditorOptionsService _editorOptionsService; - - private readonly CancellationTokenSource _shutdownCancellationSource; - - /// - /// The top level directory where all the interactive host extensions are installed (both Core and Desktop). - /// - private readonly string _hostDirectory; - - #region State only accessible by queued tasks - - // Use to serialize InteractiveHost process initialization and code execution. - // The process may restart any time and we need to react to that by clearing - // the current solution and setting up the first submission project. - // At the same time a code submission might be in progress. - // If we left these operations run in parallel we might get into a state - // inconsistent with the state of the host. - private readonly TaskQueue _taskQueue; - - private ProjectId? _lastSuccessfulSubmissionProjectId; - private ProjectId? _currentSubmissionProjectId; - public int SubmissionCount { get; private set; } - - private RemoteInitializationResult? _initializationResult; - private InteractiveHostPlatformInfo _platformInfo; - private ImmutableArray _referenceSearchPaths; - private ImmutableArray _sourceSearchPaths; - private string _workingDirectory; - private InteractiveHostOptions? _hostOptions; - - /// - /// Buffers that need to be associated with a submission project once the process initialization completes. - /// - private readonly List<(ITextBuffer buffer, string name)> _pendingBuffers = []; - - #endregion - - public InteractiveSession( - InteractiveWorkspace workspace, - IThreadingContext threadingContext, - IAsynchronousOperationListener listener, - ITextDocumentFactoryService documentFactory, - EditorOptionsService editorOptionsService, - InteractiveEvaluatorLanguageInfoProvider languageInfo, - string initialWorkingDirectory) - { - _workspace = workspace; - _threadingContext = threadingContext; - _languageInfo = languageInfo; - _textDocumentFactoryService = documentFactory; - _editorOptionsService = editorOptionsService; + _workspace = workspace; + _threadingContext = threadingContext; + _languageInfo = languageInfo; + _textDocumentFactoryService = documentFactory; + _editorOptionsService = editorOptionsService; - _taskQueue = new TaskQueue(listener, TaskScheduler.Default); - _shutdownCancellationSource = new CancellationTokenSource(); + _taskQueue = new TaskQueue(listener, TaskScheduler.Default); + _shutdownCancellationSource = new CancellationTokenSource(); - // The following settings will apply when the REPL starts without .rsp file. - // They are discarded once the REPL is reset. - _referenceSearchPaths = []; - _sourceSearchPaths = []; - _workingDirectory = initialWorkingDirectory; + // The following settings will apply when the REPL starts without .rsp file. + // They are discarded once the REPL is reset. + _referenceSearchPaths = []; + _sourceSearchPaths = []; + _workingDirectory = initialWorkingDirectory; - _hostDirectory = Path.Combine(Path.GetDirectoryName(typeof(InteractiveSession).Assembly.Location)!, "InteractiveHost"); + _hostDirectory = Path.Combine(Path.GetDirectoryName(typeof(InteractiveSession).Assembly.Location)!, "InteractiveHost"); - Host = new InteractiveHost(languageInfo.ReplServiceProviderType, initialWorkingDirectory); - Host.ProcessInitialized += ProcessInitialized; - } + Host = new InteractiveHost(languageInfo.ReplServiceProviderType, initialWorkingDirectory); + Host.ProcessInitialized += ProcessInitialized; + } - public void Dispose() - { - _shutdownCancellationSource.Cancel(); - _shutdownCancellationSource.Dispose(); + public void Dispose() + { + _shutdownCancellationSource.Cancel(); + _shutdownCancellationSource.Dispose(); - Host.Dispose(); - } + Host.Dispose(); + } + + /// + /// Invoked by when a new process initialization completes. + /// + private void ProcessInitialized(InteractiveHostPlatformInfo platformInfo, InteractiveHostOptions options, RemoteExecutionResult result) + { + Contract.ThrowIfFalse(result.InitializationResult != null); - /// - /// Invoked by when a new process initialization completes. - /// - private void ProcessInitialized(InteractiveHostPlatformInfo platformInfo, InteractiveHostOptions options, RemoteExecutionResult result) + _ = _taskQueue.ScheduleTask(nameof(ProcessInitialized), () => { - Contract.ThrowIfFalse(result.InitializationResult != null); + _workspace.ResetSolution(); + + _currentSubmissionProjectId = null; + _lastSuccessfulSubmissionProjectId = null; + + // update host state: + _platformInfo = platformInfo; + _initializationResult = result.InitializationResult; + _hostOptions = options; + UpdatePathsNoLock(result); - _ = _taskQueue.ScheduleTask(nameof(ProcessInitialized), () => + // Create submission projects for buffers that were added by the Interactive Window + // before the process initialization completed. + foreach (var (buffer, languageName) in _pendingBuffers) { - _workspace.ResetSolution(); + AddSubmissionProjectNoLock(buffer, languageName); + } - _currentSubmissionProjectId = null; - _lastSuccessfulSubmissionProjectId = null; + _pendingBuffers.Clear(); + }, _shutdownCancellationSource.Token); + } - // update host state: - _platformInfo = platformInfo; - _initializationResult = result.InitializationResult; - _hostOptions = options; - UpdatePathsNoLock(result); + /// + /// Invoked on UI thread when a new language buffer is created and before it is added to the projection. + /// + internal void AddSubmissionProject(ITextBuffer submissionBuffer) + { + _taskQueue.ScheduleTask( + nameof(AddSubmissionProject), + () => AddSubmissionProjectNoLock(submissionBuffer, _languageInfo.LanguageName), + _shutdownCancellationSource.Token); + } - // Create submission projects for buffers that were added by the Interactive Window - // before the process initialization completed. - foreach (var (buffer, languageName) in _pendingBuffers) - { - AddSubmissionProjectNoLock(buffer, languageName); - } + private void AddSubmissionProjectNoLock(ITextBuffer submissionBuffer, string languageName) + { + var initResult = _initializationResult; - _pendingBuffers.Clear(); - }, _shutdownCancellationSource.Token); - } + var imports = ImmutableArray.Empty; + var references = ImmutableArray.Empty; - /// - /// Invoked on UI thread when a new language buffer is created and before it is added to the projection. - /// - internal void AddSubmissionProject(ITextBuffer submissionBuffer) - { - _taskQueue.ScheduleTask( - nameof(AddSubmissionProject), - () => AddSubmissionProjectNoLock(submissionBuffer, _languageInfo.LanguageName), - _shutdownCancellationSource.Token); - } + var initializationScriptImports = ImmutableArray.Empty; + var initializationScriptReferences = ImmutableArray.Empty; - private void AddSubmissionProjectNoLock(ITextBuffer submissionBuffer, string languageName) + ProjectId? initializationScriptProjectId = null; + string? initializationScriptPath = null; + + if (_currentSubmissionProjectId == null) { - var initResult = _initializationResult; + Debug.Assert(_lastSuccessfulSubmissionProjectId == null); - var imports = ImmutableArray.Empty; - var references = ImmutableArray.Empty; + // The Interactive Window may have added the first language buffer before + // the host initialization has completed. Do not create a submission project + // for the buffer in such case. It will be created when the initialization completes. + if (initResult == null) + { + _pendingBuffers.Add((submissionBuffer, languageName)); + return; + } - var initializationScriptImports = ImmutableArray.Empty; - var initializationScriptReferences = ImmutableArray.Empty; + imports = initResult.Imports.ToImmutableArrayOrEmpty(); - ProjectId? initializationScriptProjectId = null; - string? initializationScriptPath = null; + var metadataService = _workspace.Services.GetRequiredService(); + references = initResult.MetadataReferencePaths.ToImmutableArrayOrEmpty().SelectAsArray( + (path, metadataService) => (MetadataReference)metadataService.GetReference(path, MetadataReferenceProperties.Assembly), + metadataService); - if (_currentSubmissionProjectId == null) + // if a script was specified in .rps file insert a project with a document that represents it: + initializationScriptPath = initResult!.ScriptPath; + if (initializationScriptPath != null) { - Debug.Assert(_lastSuccessfulSubmissionProjectId == null); - - // The Interactive Window may have added the first language buffer before - // the host initialization has completed. Do not create a submission project - // for the buffer in such case. It will be created when the initialization completes. - if (initResult == null) - { - _pendingBuffers.Add((submissionBuffer, languageName)); - return; - } - - imports = initResult.Imports.ToImmutableArrayOrEmpty(); - - var metadataService = _workspace.Services.GetRequiredService(); - references = initResult.MetadataReferencePaths.ToImmutableArrayOrEmpty().SelectAsArray( - (path, metadataService) => (MetadataReference)metadataService.GetReference(path, MetadataReferenceProperties.Assembly), - metadataService); - - // if a script was specified in .rps file insert a project with a document that represents it: - initializationScriptPath = initResult!.ScriptPath; - if (initializationScriptPath != null) - { - initializationScriptProjectId = ProjectId.CreateNewId(CreateNewSubmissionName()); - - _lastSuccessfulSubmissionProjectId = initializationScriptProjectId; - - // imports and references will be inherited: - initializationScriptImports = imports; - initializationScriptReferences = references; - imports = []; - references = []; - } + initializationScriptProjectId = ProjectId.CreateNewId(CreateNewSubmissionName()); + + _lastSuccessfulSubmissionProjectId = initializationScriptProjectId; + + // imports and references will be inherited: + initializationScriptImports = imports; + initializationScriptReferences = references; + imports = []; + references = []; } + } - var newSubmissionProjectName = CreateNewSubmissionName(); - var newSubmissionText = submissionBuffer.CurrentSnapshot.AsText(); - _currentSubmissionProjectId = ProjectId.CreateNewId(newSubmissionProjectName); - var newSubmissionDocumentId = DocumentId.CreateNewId(_currentSubmissionProjectId, newSubmissionProjectName); + var newSubmissionProjectName = CreateNewSubmissionName(); + var newSubmissionText = submissionBuffer.CurrentSnapshot.AsText(); + _currentSubmissionProjectId = ProjectId.CreateNewId(newSubmissionProjectName); + var newSubmissionDocumentId = DocumentId.CreateNewId(_currentSubmissionProjectId, newSubmissionProjectName); - // If the _initializationResult is not null we must also have the host options. - RoslynDebug.AssertNotNull(_hostOptions); - // Retrieve the directory that the host path exe is located in. - var hostPathDirectory = Path.GetDirectoryName(_hostOptions.HostPath); - RoslynDebug.AssertNotNull(hostPathDirectory); + // If the _initializationResult is not null we must also have the host options. + RoslynDebug.AssertNotNull(_hostOptions); + // Retrieve the directory that the host path exe is located in. + var hostPathDirectory = Path.GetDirectoryName(_hostOptions.HostPath); + RoslynDebug.AssertNotNull(hostPathDirectory); - // Create a file path for the submission located in the interactive host directory. - var newSubmissionFilePath = Path.Combine(hostPathDirectory, $"Submission{SubmissionCount}{_languageInfo.Extension}"); + // Create a file path for the submission located in the interactive host directory. + var newSubmissionFilePath = Path.Combine(hostPathDirectory, $"Submission{SubmissionCount}{_languageInfo.Extension}"); - // Associate the path with both the editor document and our roslyn document so LSP can make requests on it. - _textDocumentFactoryService.TryGetTextDocument(submissionBuffer, out var textDocument); - textDocument.Rename(newSubmissionFilePath); + // Associate the path with both the editor document and our roslyn document so LSP can make requests on it. + _textDocumentFactoryService.TryGetTextDocument(submissionBuffer, out var textDocument); + textDocument.Rename(newSubmissionFilePath); - // Chain projects to the the last submission that successfully executed. - _workspace.SetCurrentSolution(solution => + // Chain projects to the the last submission that successfully executed. + _workspace.SetCurrentSolution(solution => + { + if (initializationScriptProjectId != null) { - if (initializationScriptProjectId != null) - { - RoslynDebug.AssertNotNull(initializationScriptPath); - - var initProject = CreateSubmissionProjectNoLock(solution, initializationScriptProjectId, previousSubmissionProjectId: null, languageName, initializationScriptImports, initializationScriptReferences); - solution = initProject.Solution.AddDocument( - DocumentId.CreateNewId(initializationScriptProjectId, debugName: initializationScriptPath), - Path.GetFileName(initializationScriptPath), - new WorkspaceFileTextLoader(solution.Services, initializationScriptPath, defaultEncoding: null)); - } - - var newSubmissionProject = CreateSubmissionProjectNoLock(solution, _currentSubmissionProjectId, _lastSuccessfulSubmissionProjectId, languageName, imports, references); - solution = newSubmissionProject.Solution.AddDocument( - newSubmissionDocumentId, - newSubmissionProjectName, - newSubmissionText, - filePath: newSubmissionFilePath); - - return solution; - }, WorkspaceChangeKind.SolutionChanged); - - // opening document will start workspace listening to changes in this text container - _workspace.OpenDocument(newSubmissionDocumentId, submissionBuffer.AsTextContainer()); - } + RoslynDebug.AssertNotNull(initializationScriptPath); + + var initProject = CreateSubmissionProjectNoLock(solution, initializationScriptProjectId, previousSubmissionProjectId: null, languageName, initializationScriptImports, initializationScriptReferences); + solution = initProject.Solution.AddDocument( + DocumentId.CreateNewId(initializationScriptProjectId, debugName: initializationScriptPath), + Path.GetFileName(initializationScriptPath), + new WorkspaceFileTextLoader(solution.Services, initializationScriptPath, defaultEncoding: null)); + } + + var newSubmissionProject = CreateSubmissionProjectNoLock(solution, _currentSubmissionProjectId, _lastSuccessfulSubmissionProjectId, languageName, imports, references); + solution = newSubmissionProject.Solution.AddDocument( + newSubmissionDocumentId, + newSubmissionProjectName, + newSubmissionText, + filePath: newSubmissionFilePath); + + return solution; + }, WorkspaceChangeKind.SolutionChanged); + + // opening document will start workspace listening to changes in this text container + _workspace.OpenDocument(newSubmissionDocumentId, submissionBuffer.AsTextContainer()); + } + + private string CreateNewSubmissionName() + => "Submission#" + SubmissionCount++; - private string CreateNewSubmissionName() - => "Submission#" + SubmissionCount++; + private Project CreateSubmissionProjectNoLock(Solution solution, ProjectId newSubmissionProjectId, ProjectId? previousSubmissionProjectId, string languageName, ImmutableArray imports, ImmutableArray references) + { + var name = newSubmissionProjectId.DebugName; + RoslynDebug.AssertNotNull(name); - private Project CreateSubmissionProjectNoLock(Solution solution, ProjectId newSubmissionProjectId, ProjectId? previousSubmissionProjectId, string languageName, ImmutableArray imports, ImmutableArray references) + CompilationOptions compilationOptions; + if (previousSubmissionProjectId != null) { - var name = newSubmissionProjectId.DebugName; - RoslynDebug.AssertNotNull(name); + compilationOptions = solution.GetRequiredProject(previousSubmissionProjectId).CompilationOptions!; - CompilationOptions compilationOptions; - if (previousSubmissionProjectId != null) - { - compilationOptions = solution.GetRequiredProject(previousSubmissionProjectId).CompilationOptions!; - - var metadataResolver = (RuntimeMetadataReferenceResolver)compilationOptions.MetadataReferenceResolver!; - if (metadataResolver.PathResolver.BaseDirectory != _workingDirectory || - !metadataResolver.PathResolver.SearchPaths.SequenceEqual(_referenceSearchPaths)) - { - compilationOptions = compilationOptions.WithMetadataReferenceResolver(metadataResolver.WithRelativePathResolver(new RelativePathResolver(_referenceSearchPaths, _workingDirectory))); - } - - var sourceResolver = (SourceFileResolver)compilationOptions.SourceReferenceResolver!; - if (sourceResolver.BaseDirectory != _workingDirectory || - !sourceResolver.SearchPaths.SequenceEqual(_sourceSearchPaths)) - { - compilationOptions = compilationOptions.WithSourceReferenceResolver(CreateSourceReferenceResolver(sourceResolver.SearchPaths, _workingDirectory)); - } - } - else + var metadataResolver = (RuntimeMetadataReferenceResolver)compilationOptions.MetadataReferenceResolver!; + if (metadataResolver.PathResolver.BaseDirectory != _workingDirectory || + !metadataResolver.PathResolver.SearchPaths.SequenceEqual(_referenceSearchPaths)) { - var metadataService = _workspace.Services.GetRequiredService(); - compilationOptions = _languageInfo.GetSubmissionCompilationOptions( - name, - CreateMetadataReferenceResolver(metadataService, _platformInfo, _referenceSearchPaths, _workingDirectory), - CreateSourceReferenceResolver(_sourceSearchPaths, _workingDirectory), - imports); + compilationOptions = compilationOptions.WithMetadataReferenceResolver(metadataResolver.WithRelativePathResolver(new RelativePathResolver(_referenceSearchPaths, _workingDirectory))); } - solution = solution.AddProject( - ProjectInfo.Create( - new ProjectInfo.ProjectAttributes( - id: newSubmissionProjectId, - version: VersionStamp.Create(), - name: name, - assemblyName: name, - language: languageName, - compilationOutputFilePaths: default, - checksumAlgorithm: SourceHashAlgorithms.Default, - isSubmission: true), - compilationOptions: compilationOptions, - parseOptions: _languageInfo.ParseOptions, - documents: null, - projectReferences: null, - metadataReferences: references, - hostObjectType: typeof(InteractiveScriptGlobals))); - - if (previousSubmissionProjectId != null) + var sourceResolver = (SourceFileResolver)compilationOptions.SourceReferenceResolver!; + if (sourceResolver.BaseDirectory != _workingDirectory || + !sourceResolver.SearchPaths.SequenceEqual(_sourceSearchPaths)) { - solution = solution.AddProjectReference(newSubmissionProjectId, new ProjectReference(previousSubmissionProjectId)); + compilationOptions = compilationOptions.WithSourceReferenceResolver(CreateSourceReferenceResolver(sourceResolver.SearchPaths, _workingDirectory)); } + } + else + { + var metadataService = _workspace.Services.GetRequiredService(); + compilationOptions = _languageInfo.GetSubmissionCompilationOptions( + name, + CreateMetadataReferenceResolver(metadataService, _platformInfo, _referenceSearchPaths, _workingDirectory), + CreateSourceReferenceResolver(_sourceSearchPaths, _workingDirectory), + imports); + } - return solution.GetRequiredProject(newSubmissionProjectId); + solution = solution.AddProject( + ProjectInfo.Create( + new ProjectInfo.ProjectAttributes( + id: newSubmissionProjectId, + version: VersionStamp.Create(), + name: name, + assemblyName: name, + language: languageName, + compilationOutputFilePaths: default, + checksumAlgorithm: SourceHashAlgorithms.Default, + isSubmission: true), + compilationOptions: compilationOptions, + parseOptions: _languageInfo.ParseOptions, + documents: null, + projectReferences: null, + metadataReferences: references, + hostObjectType: typeof(InteractiveScriptGlobals))); + + if (previousSubmissionProjectId != null) + { + solution = solution.AddProjectReference(newSubmissionProjectId, new ProjectReference(previousSubmissionProjectId)); } - /// - /// Called once a code snippet is submitted. - /// Followed by creation of a new language buffer and call to . - /// - internal Task ExecuteCodeAsync(string text) + return solution.GetRequiredProject(newSubmissionProjectId); + } + + /// + /// Called once a code snippet is submitted. + /// Followed by creation of a new language buffer and call to . + /// + internal Task ExecuteCodeAsync(string text) + { + return _taskQueue.ScheduleTask(nameof(ExecuteCodeAsync), async () => { - return _taskQueue.ScheduleTask(nameof(ExecuteCodeAsync), async () => + var result = await Host.ExecuteAsync(text).ConfigureAwait(false); + if (result.Success) { - var result = await Host.ExecuteAsync(text).ConfigureAwait(false); - if (result.Success) - { - _lastSuccessfulSubmissionProjectId = _currentSubmissionProjectId; + _lastSuccessfulSubmissionProjectId = _currentSubmissionProjectId; - // update local search paths - remote paths has already been updated - UpdatePathsNoLock(result); - } + // update local search paths - remote paths has already been updated + UpdatePathsNoLock(result); + } - return result.Success; - }, _shutdownCancellationSource.Token); - } + return result.Success; + }, _shutdownCancellationSource.Token); + } - internal async Task ResetAsync(InteractiveHostOptions options) + internal async Task ResetAsync(InteractiveHostOptions options) + { + try { - try - { - // Do not queue reset operation - invoke it directly. - // Code execution might be in progress when the user requests reset (via a reset button, or process terminating on its own). - // We need the execution to be interrupted and the process restarted, not wait for it to complete. + // Do not queue reset operation - invoke it directly. + // Code execution might be in progress when the user requests reset (via a reset button, or process terminating on its own). + // We need the execution to be interrupted and the process restarted, not wait for it to complete. - var result = await Host.ResetAsync(options).ConfigureAwait(false); + var result = await Host.ResetAsync(options).ConfigureAwait(false); - // Note: Not calling UpdatePathsNoLock here. The paths will be updated by ProcessInitialized - // which is executed once the new host process finishes its initialization. + // Note: Not calling UpdatePathsNoLock here. The paths will be updated by ProcessInitialized + // which is executed once the new host process finishes its initialization. - return result.Success; - } - catch (Exception e) when (FatalError.ReportAndPropagate(e)) - { - throw ExceptionUtilities.Unreachable(); - } + return result.Success; } - - public InteractiveHostOptions GetHostOptions(bool initialize, InteractiveHostPlatform? platform) - => InteractiveHostOptions.CreateFromDirectory( - _hostDirectory, - initialize ? _languageInfo.InteractiveResponseFileName : null, - CultureInfo.CurrentCulture, - CultureInfo.CurrentUICulture, - platform ?? Host.OptionsOpt?.Platform ?? InteractiveHost.DefaultPlatform); - - private static RuntimeMetadataReferenceResolver CreateMetadataReferenceResolver(IMetadataService metadataService, InteractiveHostPlatformInfo platformInfo, ImmutableArray searchPaths, string baseDirectory) + catch (Exception e) when (FatalError.ReportAndPropagate(e)) { - return new RuntimeMetadataReferenceResolver( - searchPaths, - baseDirectory, - gacFileResolver: platformInfo.HasGlobalAssemblyCache ? new GacFileResolver(preferredCulture: CultureInfo.CurrentCulture) : null, - platformAssemblyPaths: platformInfo.PlatformAssemblyPaths, - fileReferenceProvider: (path, properties) => metadataService.GetReference(path, properties)); + throw ExceptionUtilities.Unreachable(); } + } - private static SourceReferenceResolver CreateSourceReferenceResolver(ImmutableArray searchPaths, string baseDirectory) - => new SourceFileResolver(searchPaths, baseDirectory); + public InteractiveHostOptions GetHostOptions(bool initialize, InteractiveHostPlatform? platform) + => InteractiveHostOptions.CreateFromDirectory( + _hostDirectory, + initialize ? _languageInfo.InteractiveResponseFileName : null, + CultureInfo.CurrentCulture, + CultureInfo.CurrentUICulture, + platform ?? Host.OptionsOpt?.Platform ?? InteractiveHost.DefaultPlatform); - public Task SetPathsAsync(ImmutableArray referenceSearchPaths, ImmutableArray sourceSearchPaths, string workingDirectory) - { - return _taskQueue.ScheduleTask(nameof(ExecuteCodeAsync), async () => - { - var result = await Host.SetPathsAsync(referenceSearchPaths, sourceSearchPaths, workingDirectory).ConfigureAwait(false); - UpdatePathsNoLock(result); - }, _shutdownCancellationSource.Token); - } + private static RuntimeMetadataReferenceResolver CreateMetadataReferenceResolver(IMetadataService metadataService, InteractiveHostPlatformInfo platformInfo, ImmutableArray searchPaths, string baseDirectory) + { + return new RuntimeMetadataReferenceResolver( + searchPaths, + baseDirectory, + gacFileResolver: platformInfo.HasGlobalAssemblyCache ? new GacFileResolver(preferredCulture: CultureInfo.CurrentCulture) : null, + platformAssemblyPaths: platformInfo.PlatformAssemblyPaths, + fileReferenceProvider: (path, properties) => metadataService.GetReference(path, properties)); + } - private void UpdatePathsNoLock(RemoteExecutionResult result) + private static SourceReferenceResolver CreateSourceReferenceResolver(ImmutableArray searchPaths, string baseDirectory) + => new SourceFileResolver(searchPaths, baseDirectory); + + public Task SetPathsAsync(ImmutableArray referenceSearchPaths, ImmutableArray sourceSearchPaths, string workingDirectory) + { + return _taskQueue.ScheduleTask(nameof(ExecuteCodeAsync), async () => { - _workingDirectory = result.WorkingDirectory; - _referenceSearchPaths = result.ReferencePaths; - _sourceSearchPaths = result.SourcePaths; - } + var result = await Host.SetPathsAsync(referenceSearchPaths, sourceSearchPaths, workingDirectory).ConfigureAwait(false); + UpdatePathsNoLock(result); + }, _shutdownCancellationSource.Token); + } + + private void UpdatePathsNoLock(RemoteExecutionResult result) + { + _workingDirectory = result.WorkingDirectory; + _referenceSearchPaths = result.ReferencePaths; + _sourceSearchPaths = result.SourcePaths; } } diff --git a/src/EditorFeatures/Core/Interactive/InteractiveWorkspace.SolutionAnalyzerSetter.cs b/src/EditorFeatures/Core/Interactive/InteractiveWorkspace.SolutionAnalyzerSetter.cs deleted file mode 100644 index f56fa6269f515..0000000000000 --- a/src/EditorFeatures/Core/Interactive/InteractiveWorkspace.SolutionAnalyzerSetter.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Immutable; -using System.Composition; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; - -namespace Microsoft.CodeAnalysis.Interactive -{ - internal partial class InteractiveWorkspace - { - internal sealed class SolutionAnalyzerSetter(InteractiveWorkspace workspace) : ISolutionAnalyzerSetterWorkspaceService - { - [ExportWorkspaceServiceFactory(typeof(ISolutionAnalyzerSetterWorkspaceService), WorkspaceKind.Interactive), Shared] - internal sealed class Factory : IWorkspaceServiceFactory - { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public Factory() - { - } - - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new SolutionAnalyzerSetter((InteractiveWorkspace)workspaceServices.Workspace); - } - - private readonly InteractiveWorkspace _workspace = workspace; - - public void SetAnalyzerReferences(ImmutableArray references) - => _workspace.SetCurrentSolution(s => s.WithAnalyzerReferences(references), WorkspaceChangeKind.SolutionChanged); - } - } -} diff --git a/src/EditorFeatures/Core/Interactive/InteractiveWorkspace.cs b/src/EditorFeatures/Core/Interactive/InteractiveWorkspace.cs index 2348187d602f1..7f07603f4a583 100644 --- a/src/EditorFeatures/Core/Interactive/InteractiveWorkspace.cs +++ b/src/EditorFeatures/Core/Interactive/InteractiveWorkspace.cs @@ -9,79 +9,78 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Interactive +namespace Microsoft.CodeAnalysis.Interactive; + +internal partial class InteractiveWorkspace : Workspace { - internal partial class InteractiveWorkspace : Workspace - { - private SourceTextContainer? _openTextContainer; - private DocumentId? _openDocumentId; + private SourceTextContainer? _openTextContainer; + private DocumentId? _openDocumentId; - internal InteractiveWorkspace(HostServices hostServices, IGlobalOptionService globalOptions) - : base(hostServices, WorkspaceKind.Interactive) + internal InteractiveWorkspace(HostServices hostServices, IGlobalOptionService globalOptions) + : base(hostServices, WorkspaceKind.Interactive) + { + // register work coordinator for this workspace + if (globalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler)) { - // register work coordinator for this workspace - if (globalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler)) - { - Services.GetRequiredService().Register(this); - } + Services.GetRequiredService().Register(this); } + } - protected override void Dispose(bool finalize) - { - // workspace is going away. unregister this workspace from work coordinator - Services.GetRequiredService().Unregister(this, blockingShutdown: true); + protected override void Dispose(bool finalize) + { + // workspace is going away. unregister this workspace from work coordinator + Services.GetRequiredService().Unregister(this, blockingShutdown: true); - base.Dispose(finalize); - } + base.Dispose(finalize); + } - public override bool CanOpenDocuments - => true; + public override bool CanOpenDocuments + => true; - public override bool CanApplyChange(ApplyChangesKind feature) - => feature == ApplyChangesKind.ChangeDocument; + public override bool CanApplyChange(ApplyChangesKind feature) + => feature == ApplyChangesKind.ChangeDocument; + + public void OpenDocument(DocumentId documentId, SourceTextContainer textContainer) + { + _openTextContainer = textContainer; + _openDocumentId = documentId; + OnDocumentOpened(documentId, textContainer); + } - public void OpenDocument(DocumentId documentId, SourceTextContainer textContainer) + protected override void ApplyDocumentTextChanged(DocumentId document, SourceText newText) + { + if (_openDocumentId != document) { - _openTextContainer = textContainer; - _openDocumentId = documentId; - OnDocumentOpened(documentId, textContainer); + return; } - protected override void ApplyDocumentTextChanged(DocumentId document, SourceText newText) - { - if (_openDocumentId != document) - { - return; - } + Contract.ThrowIfNull(_openTextContainer); - Contract.ThrowIfNull(_openTextContainer); + ITextSnapshot appliedText; + using (var edit = _openTextContainer.GetTextBuffer().CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null)) + { + var oldText = _openTextContainer.CurrentText; + var changes = newText.GetTextChanges(oldText); - ITextSnapshot appliedText; - using (var edit = _openTextContainer.GetTextBuffer().CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null)) + foreach (var change in changes) { - var oldText = _openTextContainer.CurrentText; - var changes = newText.GetTextChanges(oldText); - - foreach (var change in changes) - { - edit.Replace(change.Span.Start, change.Span.Length, change.NewText); - } - - appliedText = edit.Apply(); + edit.Replace(change.Span.Start, change.Span.Length, change.NewText); } - OnDocumentTextChanged(document, appliedText.AsText(), PreservationMode.PreserveIdentity); + appliedText = edit.Apply(); } - /// - /// Closes all open documents and empties the solution but keeps all solution-level analyzers. - /// - public void ResetSolution() - { - ClearOpenDocuments(); + OnDocumentTextChanged(document, appliedText.AsText(), PreservationMode.PreserveIdentity); + } - var emptySolution = CreateSolution(SolutionId.CreateNewId("InteractiveSolution")); - SetCurrentSolution(solution => emptySolution.WithAnalyzerReferences(solution.AnalyzerReferences), WorkspaceChangeKind.SolutionCleared); - } + /// + /// Closes all open documents and empties the solution but keeps all solution-level analyzers. + /// + public void ResetSolution() + { + ClearOpenDocuments(); + + var emptySolution = CreateSolution(SolutionId.CreateNewId("InteractiveSolution")); + SetCurrentSolution(solution => emptySolution.WithAnalyzerReferences(solution.AnalyzerReferences), WorkspaceChangeKind.SolutionCleared); } } diff --git a/src/EditorFeatures/Core/Interactive/SendToInteractiveSubmissionProvider.cs b/src/EditorFeatures/Core/Interactive/SendToInteractiveSubmissionProvider.cs index 7daccf0b885f1..86b31448ad65c 100644 --- a/src/EditorFeatures/Core/Interactive/SendToInteractiveSubmissionProvider.cs +++ b/src/EditorFeatures/Core/Interactive/SendToInteractiveSubmissionProvider.cs @@ -16,67 +16,66 @@ using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; using Microsoft.VisualStudio.Text.Editor.Commanding; -namespace Microsoft.CodeAnalysis.Interactive -{ - /// - /// Implementers of this interface are responsible for retrieving source code that - /// should be sent to the REPL given the user's selection. - /// - /// If the user does not make a selection then a line should be selected. - /// If the user selects code that fails to be parsed then the selection gets expanded - /// to a syntax node. - /// - internal abstract class AbstractSendToInteractiveSubmissionProvider : ISendToInteractiveSubmissionProvider - { - /// Expands the selection span of an invalid selection to a span that should be sent to REPL. - protected abstract IEnumerable GetExecutableSyntaxTreeNodeSelection(TextSpan selectedSpan, SyntaxNode node); +namespace Microsoft.CodeAnalysis.Interactive; - /// Returns whether the submission can be parsed in interactive. - protected abstract bool CanParseSubmission(string code); +/// +/// Implementers of this interface are responsible for retrieving source code that +/// should be sent to the REPL given the user's selection. +/// +/// If the user does not make a selection then a line should be selected. +/// If the user selects code that fails to be parsed then the selection gets expanded +/// to a syntax node. +/// +internal abstract class AbstractSendToInteractiveSubmissionProvider : ISendToInteractiveSubmissionProvider +{ + /// Expands the selection span of an invalid selection to a span that should be sent to REPL. + protected abstract IEnumerable GetExecutableSyntaxTreeNodeSelection(TextSpan selectedSpan, SyntaxNode node); - string ISendToInteractiveSubmissionProvider.GetSelectedText(IEditorOptions editorOptions, EditorCommandArgs args, CancellationToken cancellationToken) - { - var selectedSpans = args.TextView.Selection.IsEmpty - ? GetExpandedLine(editorOptions, args, cancellationToken) - : args.TextView.Selection.GetSnapshotSpansOnBuffer(args.SubjectBuffer).Where(ss => ss.Length > 0); + /// Returns whether the submission can be parsed in interactive. + protected abstract bool CanParseSubmission(string code); - return GetSubmissionFromSelectedSpans(editorOptions, selectedSpans); - } + string ISendToInteractiveSubmissionProvider.GetSelectedText(IEditorOptions editorOptions, EditorCommandArgs args, CancellationToken cancellationToken) + { + var selectedSpans = args.TextView.Selection.IsEmpty + ? GetExpandedLine(editorOptions, args, cancellationToken) + : args.TextView.Selection.GetSnapshotSpansOnBuffer(args.SubjectBuffer).Where(ss => ss.Length > 0); - /// Returns the span for the selected line. Extends it if it is a part of a multi line statement or declaration. - private IEnumerable GetExpandedLine(IEditorOptions editorOptions, EditorCommandArgs args, CancellationToken cancellationToken) - { - var selectedSpans = GetSelectedLine(args.TextView); - var candidateSubmission = GetSubmissionFromSelectedSpans(editorOptions, selectedSpans); - return CanParseSubmission(candidateSubmission) - ? selectedSpans - : ExpandSelection(selectedSpans, args, cancellationToken); - } + return GetSubmissionFromSelectedSpans(editorOptions, selectedSpans); + } - /// Returns the span for the currently selected line. - private static IEnumerable GetSelectedLine(ITextView textView) - { - var snapshotLine = textView.Caret.Position.VirtualBufferPosition.Position.GetContainingLine(); - var span = new SnapshotSpan(snapshotLine.Start, snapshotLine.LengthIncludingLineBreak); - return new NormalizedSnapshotSpanCollection(span); - } + /// Returns the span for the selected line. Extends it if it is a part of a multi line statement or declaration. + private IEnumerable GetExpandedLine(IEditorOptions editorOptions, EditorCommandArgs args, CancellationToken cancellationToken) + { + var selectedSpans = GetSelectedLine(args.TextView); + var candidateSubmission = GetSubmissionFromSelectedSpans(editorOptions, selectedSpans); + return CanParseSubmission(candidateSubmission) + ? selectedSpans + : ExpandSelection(selectedSpans, args, cancellationToken); + } - private IEnumerable ExpandSelection(IEnumerable selectedSpans, EditorCommandArgs args, CancellationToken cancellationToken) - { - var selectedSpansStart = selectedSpans.Min(span => span.Start); - var selectedSpansEnd = selectedSpans.Max(span => span.End); - var snapshot = args.TextView.TextSnapshot; + /// Returns the span for the currently selected line. + private static IEnumerable GetSelectedLine(ITextView textView) + { + var snapshotLine = textView.Caret.Position.VirtualBufferPosition.Position.GetContainingLine(); + var span = new SnapshotSpan(snapshotLine.Start, snapshotLine.LengthIncludingLineBreak); + return new NormalizedSnapshotSpanCollection(span); + } - var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - var root = document.GetSyntaxRootSynchronously(cancellationToken); + private IEnumerable ExpandSelection(IEnumerable selectedSpans, EditorCommandArgs args, CancellationToken cancellationToken) + { + var selectedSpansStart = selectedSpans.Min(span => span.Start); + var selectedSpansEnd = selectedSpans.Max(span => span.End); + var snapshot = args.TextView.TextSnapshot; - var newSpans = GetExecutableSyntaxTreeNodeSelection(TextSpan.FromBounds(selectedSpansStart, selectedSpansEnd), root). - Select(span => new SnapshotSpan(snapshot, span.Start, span.Length)); + var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + var root = document.GetSyntaxRootSynchronously(cancellationToken); - return newSpans.Any() ? newSpans : selectedSpans; - } + var newSpans = GetExecutableSyntaxTreeNodeSelection(TextSpan.FromBounds(selectedSpansStart, selectedSpansEnd), root). + Select(span => new SnapshotSpan(snapshot, span.Start, span.Length)); - private static string GetSubmissionFromSelectedSpans(IEditorOptions editorOptions, IEnumerable selectedSpans) - => string.Join(editorOptions.GetNewLineCharacter(), selectedSpans.Select(ss => ss.GetText())); + return newSpans.Any() ? newSpans : selectedSpans; } + + private static string GetSubmissionFromSelectedSpans(IEditorOptions editorOptions, IEnumerable selectedSpans) + => string.Join(editorOptions.GetNewLineCharacter(), selectedSpans.Select(ss => ss.GetText())); } diff --git a/src/EditorFeatures/Core/KeywordHighlighting/HighlighterViewTaggerProvider.cs b/src/EditorFeatures/Core/KeywordHighlighting/HighlighterViewTaggerProvider.cs index 7156d26e50c0d..eb69aee72fe85 100644 --- a/src/EditorFeatures/Core/KeywordHighlighting/HighlighterViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/KeywordHighlighting/HighlighterViewTaggerProvider.cs @@ -27,101 +27,100 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.Highlighting +namespace Microsoft.CodeAnalysis.Editor.Implementation.Highlighting; + +[Export(typeof(IViewTaggerProvider))] +[TagType(typeof(KeywordHighlightTag))] +[ContentType(ContentTypeNames.CSharpContentType)] +[ContentType(ContentTypeNames.VisualBasicContentType)] +[TextViewRole(PredefinedTextViewRoles.Interactive)] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal sealed class HighlighterViewTaggerProvider( + IThreadingContext threadingContext, + IHighlightingService highlightingService, + IGlobalOptionService globalOptions, + [Import(AllowDefault = true)] ITextBufferVisibilityTracker visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) : AsynchronousViewTaggerProvider(threadingContext, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.KeywordHighlighting)) { - [Export(typeof(IViewTaggerProvider))] - [TagType(typeof(KeywordHighlightTag))] - [ContentType(ContentTypeNames.CSharpContentType)] - [ContentType(ContentTypeNames.VisualBasicContentType)] - [TextViewRole(PredefinedTextViewRoles.Interactive)] - [method: ImportingConstructor] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal sealed class HighlighterViewTaggerProvider( - IThreadingContext threadingContext, - IHighlightingService highlightingService, - IGlobalOptionService globalOptions, - [Import(AllowDefault = true)] ITextBufferVisibilityTracker visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) : AsynchronousViewTaggerProvider(threadingContext, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.KeywordHighlighting)) - { - private readonly IHighlightingService _highlightingService = highlightingService; - private static readonly PooledObjects.ObjectPool> s_listPool = new(() => []); + private readonly IHighlightingService _highlightingService = highlightingService; + private static readonly PooledObjects.ObjectPool> s_listPool = new(() => []); + + // Whenever an edit happens, clear all highlights. When moving the caret, preserve + // highlights if the caret stays within an existing tag. + protected override TaggerCaretChangeBehavior CaretChangeBehavior => TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag; + protected override TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.RemoveAllTags; - // Whenever an edit happens, clear all highlights. When moving the caret, preserve - // highlights if the caret stays within an existing tag. - protected override TaggerCaretChangeBehavior CaretChangeBehavior => TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag; - protected override TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.RemoveAllTags; + protected override ImmutableArray Options { get; } = [KeywordHighlightingOptionsStorage.KeywordHighlighting]; - protected override ImmutableArray Options { get; } = [KeywordHighlightingOptionsStorage.KeywordHighlighting]; + protected override TaggerDelay EventChangeDelay => TaggerDelay.NearImmediate; - protected override TaggerDelay EventChangeDelay => TaggerDelay.NearImmediate; + protected override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) + { + return TaggerEventSources.Compose( + TaggerEventSources.OnTextChanged(subjectBuffer), + TaggerEventSources.OnCaretPositionChanged(textView, subjectBuffer), + TaggerEventSources.OnParseOptionChanged(subjectBuffer)); + } - protected override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) + protected override async Task ProduceTagsAsync( + TaggerContext context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition, CancellationToken cancellationToken) + { + var document = documentSnapshotSpan.Document; + + // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/763988 + // It turns out a document might be associated with a project of wrong language, e.g. C# document in a Xaml project. + // Even though we couldn't repro the crash above, a fix is made in one of possibly multiple code paths that could cause + // us to end up in this situation. + // Regardless of the effective of the fix, we want to enhance the guard against such scenario here until an audit in + // workspace is completed to eliminate the root cause. + if (document?.SupportsSyntaxTree != true) { - return TaggerEventSources.Compose( - TaggerEventSources.OnTextChanged(subjectBuffer), - TaggerEventSources.OnCaretPositionChanged(textView, subjectBuffer), - TaggerEventSources.OnParseOptionChanged(subjectBuffer)); + return; } - protected override async Task ProduceTagsAsync( - TaggerContext context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition, CancellationToken cancellationToken) + if (!GlobalOptions.GetOption(KeywordHighlightingOptionsStorage.KeywordHighlighting, document.Project.Language)) { - var document = documentSnapshotSpan.Document; - - // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/763988 - // It turns out a document might be associated with a project of wrong language, e.g. C# document in a Xaml project. - // Even though we couldn't repro the crash above, a fix is made in one of possibly multiple code paths that could cause - // us to end up in this situation. - // Regardless of the effective of the fix, we want to enhance the guard against such scenario here until an audit in - // workspace is completed to eliminate the root cause. - if (document?.SupportsSyntaxTree != true) - { - return; - } - - if (!GlobalOptions.GetOption(KeywordHighlightingOptionsStorage.KeywordHighlighting, document.Project.Language)) - { - return; - } + return; + } - if (!caretPosition.HasValue) - { - return; - } + if (!caretPosition.HasValue) + { + return; + } - var snapshotSpan = documentSnapshotSpan.SnapshotSpan; - var position = caretPosition.Value; - var snapshot = snapshotSpan.Snapshot; + var snapshotSpan = documentSnapshotSpan.SnapshotSpan; + var position = caretPosition.Value; + var snapshot = snapshotSpan.Snapshot; - // See if the user is just moving their caret around in an existing tag. If so, we don't - // want to actually go recompute things. Note: this only works for containment. If the - // user moves their caret to the end of a highlighted reference, we do want to recompute - // as they may now be at the start of some other reference that should be highlighted instead. - var onExistingTags = context.HasExistingContainingTags(new SnapshotPoint(snapshot, position)); - if (onExistingTags) - { - context.SetSpansTagged([]); - return; - } + // See if the user is just moving their caret around in an existing tag. If so, we don't + // want to actually go recompute things. Note: this only works for containment. If the + // user moves their caret to the end of a highlighted reference, we do want to recompute + // as they may now be at the start of some other reference that should be highlighted instead. + var onExistingTags = context.HasExistingContainingTags(new SnapshotPoint(snapshot, position)); + if (onExistingTags) + { + context.SetSpansTagged([]); + return; + } - using (Logger.LogBlock(FunctionId.Tagger_Highlighter_TagProducer_ProduceTags, cancellationToken)) - using (s_listPool.GetPooledObject(out var highlights)) - { - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + using (Logger.LogBlock(FunctionId.Tagger_Highlighter_TagProducer_ProduceTags, cancellationToken)) + using (s_listPool.GetPooledObject(out var highlights)) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - _highlightingService.AddHighlights(root, position, highlights, cancellationToken); + _highlightingService.AddHighlights(root, position, highlights, cancellationToken); - foreach (var span in highlights) - { - context.AddTag(new TagSpan(span.ToSnapshotSpan(snapshot), KeywordHighlightTag.Instance)); - } + foreach (var span in highlights) + { + context.AddTag(new TagSpan(span.ToSnapshotSpan(snapshot), KeywordHighlightTag.Instance)); } } + } - protected override bool TagEquals(KeywordHighlightTag tag1, KeywordHighlightTag tag2) - { - Contract.ThrowIfFalse(tag1 == tag2, "KeywordHighlightTag is supposed to be a singleton"); - return true; - } + protected override bool TagEquals(KeywordHighlightTag tag1, KeywordHighlightTag tag2) + { + Contract.ThrowIfFalse(tag1 == tag2, "KeywordHighlightTag is supposed to be a singleton"); + return true; } } diff --git a/src/EditorFeatures/Core/KeywordHighlighting/KeywordHighlightTag.cs b/src/EditorFeatures/Core/KeywordHighlighting/KeywordHighlightTag.cs index bdedd575b57ca..fa1900e27a55d 100644 --- a/src/EditorFeatures/Core/KeywordHighlighting/KeywordHighlightTag.cs +++ b/src/EditorFeatures/Core/KeywordHighlighting/KeywordHighlightTag.cs @@ -6,17 +6,16 @@ using Microsoft.CodeAnalysis.Editor.Shared.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Implementation.Highlighting +namespace Microsoft.CodeAnalysis.Editor.Implementation.Highlighting; + +internal sealed class KeywordHighlightTag : NavigableHighlightTag { - internal sealed class KeywordHighlightTag : NavigableHighlightTag - { - internal const string TagId = "MarkerFormatDefinition/HighlightedReference"; + internal const string TagId = "MarkerFormatDefinition/HighlightedReference"; - public static readonly KeywordHighlightTag Instance = new(); + public static readonly KeywordHighlightTag Instance = new(); - private KeywordHighlightTag() - : base(TagId) - { - } + private KeywordHighlightTag() + : base(TagId) + { } } diff --git a/src/EditorFeatures/Core/KeywordHighlighting/KeywordHighlightingOptionsStorage.cs b/src/EditorFeatures/Core/KeywordHighlighting/KeywordHighlightingOptionsStorage.cs index d44c4a278acea..6ec24952720a9 100644 --- a/src/EditorFeatures/Core/KeywordHighlighting/KeywordHighlightingOptionsStorage.cs +++ b/src/EditorFeatures/Core/KeywordHighlighting/KeywordHighlightingOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.KeywordHighlighting +namespace Microsoft.CodeAnalysis.KeywordHighlighting; + +internal static class KeywordHighlightingOptionsStorage { - internal static class KeywordHighlightingOptionsStorage - { - public static readonly PerLanguageOption2 KeywordHighlighting = new("dotnet_highlight_keywords", defaultValue: true); - } + public static readonly PerLanguageOption2 KeywordHighlighting = new("dotnet_highlight_keywords", defaultValue: true); } diff --git a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs index a93418fe75d85..1bbb94b3add03 100644 --- a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs +++ b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs @@ -22,256 +22,255 @@ using Roslyn.LanguageServer.Protocol; using StreamJsonRpc; -namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient +namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient; + +internal abstract partial class AbstractInProcLanguageClient( + AbstractLspServiceProvider lspServiceProvider, + IGlobalOptionService globalOptions, + ILspServiceLoggerFactory lspLoggerFactory, + IThreadingContext threadingContext, + ExportProvider exportProvider, + AbstractLanguageClientMiddleLayer? middleLayer = null) : ILanguageClient, ILanguageServerFactory, ICapabilitiesProvider, ILanguageClientCustomMessage2 { - internal abstract partial class AbstractInProcLanguageClient( - AbstractLspServiceProvider lspServiceProvider, - IGlobalOptionService globalOptions, - ILspServiceLoggerFactory lspLoggerFactory, - IThreadingContext threadingContext, - ExportProvider exportProvider, - AbstractLanguageClientMiddleLayer? middleLayer = null) : ILanguageClient, ILanguageServerFactory, ICapabilitiesProvider, ILanguageClientCustomMessage2 + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly ILanguageClientMiddleLayer? _middleLayer = middleLayer; + private readonly ILspServiceLoggerFactory _lspLoggerFactory = lspLoggerFactory; + private readonly ExportProvider _exportProvider = exportProvider; + + protected readonly AbstractLspServiceProvider LspServiceProvider = lspServiceProvider; + + protected readonly IGlobalOptionService GlobalOptions = globalOptions; + + /// + /// Created when is called. + /// + private AbstractLanguageServer? _languageServer; + + /// + /// Gets the name of the language client (displayed to the user). + /// + public string Name => ServerKind.ToUserVisibleString(); + + /// + /// Gets the optional middle layer object that can intercept outgoing requests and responses. + /// + /// + /// Currently utilized by Razor to intercept Roslyn's workspace/semanticTokens/refresh requests. + /// + public object? MiddleLayer => _middleLayer; + + /// + /// Unused, implementing . + /// Gets the optional target object for receiving custom messages not covered by the language server protocol. + /// + public virtual object? CustomMessageTarget => null; + + /// + /// An enum representing this server instance. + /// + public abstract WellKnownLspServerKinds ServerKind { get; } + + /// + /// The set of languages that this LSP server supports and can return results for. + /// + protected abstract ImmutableArray SupportedLanguages { get; } + + /// + /// Unused, implementing + /// No additional settings are provided for this server, so we do not need any configuration section names. + /// + public IEnumerable? ConfigurationSections { get; } + + /// + /// Gets the initialization options object the client wants to send when 'initialize' message is sent. + /// See https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#initialize + /// We do not provide any additional initialization options. + /// + public object? InitializationOptions { get; } + + /// + /// Gets a value indicating whether a notification bubble show be shown when the language server fails to initialize. + /// + public abstract bool ShowNotificationOnInitializeFailed { get; } + + /// + /// Unused, implementing + /// Files that we care about are already provided and watched by the workspace. + /// + public IEnumerable? FilesToWatch { get; } + + public event AsyncEventHandler? StartAsync; + + /// + /// Unused, implementing + /// + public event AsyncEventHandler? StopAsync { add { } remove { } } + + public async Task ActivateAsync(CancellationToken cancellationToken) { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly ILanguageClientMiddleLayer? _middleLayer = middleLayer; - private readonly ILspServiceLoggerFactory _lspLoggerFactory = lspLoggerFactory; - private readonly ExportProvider _exportProvider = exportProvider; - - protected readonly AbstractLspServiceProvider LspServiceProvider = lspServiceProvider; - - protected readonly IGlobalOptionService GlobalOptions = globalOptions; - - /// - /// Created when is called. - /// - private AbstractLanguageServer? _languageServer; - - /// - /// Gets the name of the language client (displayed to the user). - /// - public string Name => ServerKind.ToUserVisibleString(); - - /// - /// Gets the optional middle layer object that can intercept outgoing requests and responses. - /// - /// - /// Currently utilized by Razor to intercept Roslyn's workspace/semanticTokens/refresh requests. - /// - public object? MiddleLayer => _middleLayer; - - /// - /// Unused, implementing . - /// Gets the optional target object for receiving custom messages not covered by the language server protocol. - /// - public virtual object? CustomMessageTarget => null; - - /// - /// An enum representing this server instance. - /// - public abstract WellKnownLspServerKinds ServerKind { get; } - - /// - /// The set of languages that this LSP server supports and can return results for. - /// - protected abstract ImmutableArray SupportedLanguages { get; } - - /// - /// Unused, implementing - /// No additional settings are provided for this server, so we do not need any configuration section names. - /// - public IEnumerable? ConfigurationSections { get; } - - /// - /// Gets the initialization options object the client wants to send when 'initialize' message is sent. - /// See https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#initialize - /// We do not provide any additional initialization options. - /// - public object? InitializationOptions { get; } - - /// - /// Gets a value indicating whether a notification bubble show be shown when the language server fails to initialize. - /// - public abstract bool ShowNotificationOnInitializeFailed { get; } - - /// - /// Unused, implementing - /// Files that we care about are already provided and watched by the workspace. - /// - public IEnumerable? FilesToWatch { get; } - - public event AsyncEventHandler? StartAsync; - - /// - /// Unused, implementing - /// - public event AsyncEventHandler? StopAsync { add { } remove { } } - - public async Task ActivateAsync(CancellationToken cancellationToken) + // HACK HACK HACK: prevent potential crashes/state corruption during load. Fixes + // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1261421 + // + // When we create an LSP server, we compute our server capabilities; this may depend on + // reading things like workspace options which will force us to initialize our option persisters. + // Unfortunately some of our option persisters currently assert they are first created on the UI + // thread. If the first time they're created is because of LSP initialization, we might end up loading + // them on a background thread which will throw exceptions and then prevent them from being created + // again later. + // + // The correct fix for this is to fix the threading violations in the option persister code; + // asserting a MEF component is constructed on the foreground thread is never allowed, but alas it's + // done there. Fixing that isn't difficult but comes with some risk I don't want to take for 16.9; + // instead we'll just compute our capabilities here on the UI thread to ensure everything is loaded. + // We _could_ consider doing a SwitchToMainThreadAsync in InProcLanguageServer.InitializeAsync + // (where the problematic call to GetCapabilites is), but that call is invoked across the StreamJsonRpc + // link where it's unclear if VS Threading rules apply. By doing this here, we are dong it in a + // VS API that is following VS Threading rules, and it also ensures that the preereqs are loaded + // prior to any RPC calls being made. + // + // https://github.com/dotnet/roslyn/issues/29602 will track removing this hack + // since that's the primary offending persister that needs to be addressed. + + // To help mitigate some of the issues with this hack we first allow implementors to do some work + // so they can do MEF part loading before the UI thread switch. This doesn't help with the options + // persisters, but at least doesn't make it worse. + Activate_OffUIThread(); + + // Now switch and do the problematic GetCapabilities call + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + _ = GetCapabilities(new VSInternalClientCapabilities { SupportsVisualStudioExtensions = true }); + + if (_languageServer is not null) { - // HACK HACK HACK: prevent potential crashes/state corruption during load. Fixes - // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1261421 - // - // When we create an LSP server, we compute our server capabilities; this may depend on - // reading things like workspace options which will force us to initialize our option persisters. - // Unfortunately some of our option persisters currently assert they are first created on the UI - // thread. If the first time they're created is because of LSP initialization, we might end up loading - // them on a background thread which will throw exceptions and then prevent them from being created - // again later. - // - // The correct fix for this is to fix the threading violations in the option persister code; - // asserting a MEF component is constructed on the foreground thread is never allowed, but alas it's - // done there. Fixing that isn't difficult but comes with some risk I don't want to take for 16.9; - // instead we'll just compute our capabilities here on the UI thread to ensure everything is loaded. - // We _could_ consider doing a SwitchToMainThreadAsync in InProcLanguageServer.InitializeAsync - // (where the problematic call to GetCapabilites is), but that call is invoked across the StreamJsonRpc - // link where it's unclear if VS Threading rules apply. By doing this here, we are dong it in a - // VS API that is following VS Threading rules, and it also ensures that the preereqs are loaded - // prior to any RPC calls being made. - // - // https://github.com/dotnet/roslyn/issues/29602 will track removing this hack - // since that's the primary offending persister that needs to be addressed. - - // To help mitigate some of the issues with this hack we first allow implementors to do some work - // so they can do MEF part loading before the UI thread switch. This doesn't help with the options - // persisters, but at least doesn't make it worse. - Activate_OffUIThread(); - - // Now switch and do the problematic GetCapabilities call - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - _ = GetCapabilities(new VSInternalClientCapabilities { SupportsVisualStudioExtensions = true }); - - if (_languageServer is not null) - { - await _languageServer.WaitForExitAsync().WithCancellation(cancellationToken).ConfigureAwait(false); - } - - var (clientStream, serverStream) = FullDuplexStream.CreatePair(); - - _languageServer = await CreateAsync( - this, - serverStream, - serverStream, - ServerKind, - _lspLoggerFactory, - cancellationToken).ConfigureAwait(false); - - return new Connection(clientStream, clientStream); + await _languageServer.WaitForExitAsync().WithCancellation(cancellationToken).ConfigureAwait(false); } - protected virtual void Activate_OffUIThread() - { - } + var (clientStream, serverStream) = FullDuplexStream.CreatePair(); + + _languageServer = await CreateAsync( + this, + serverStream, + serverStream, + ServerKind, + _lspLoggerFactory, + cancellationToken).ConfigureAwait(false); + + return new Connection(clientStream, clientStream); + } + + protected virtual void Activate_OffUIThread() + { + } - /// - /// Signals that the extension has been loaded. The server can be started immediately, or wait for user action to start. - /// To start the server, invoke the event; - /// - public virtual async Task OnLoadedAsync() + /// + /// Signals that the extension has been loaded. The server can be started immediately, or wait for user action to start. + /// To start the server, invoke the event; + /// + public virtual async Task OnLoadedAsync() + { + try { - try - { - await StartAsync.InvokeAsync(this, EventArgs.Empty).ConfigureAwait(false); - } - catch (AggregateException e) - { - // The VS LSP client allows an unexpected OperationCanceledException to propagate out of the StartAsync - // callback. Avoid allowing it to propagate further. - e.Handle(ex => ex is OperationCanceledException); - } + await StartAsync.InvokeAsync(this, EventArgs.Empty).ConfigureAwait(false); } - - /// - /// Signals the extension that the language server has been successfully initialized. - /// - /// A which completes when actions that need to be performed when the server is ready are done. - public Task OnServerInitializedAsync() + catch (AggregateException e) { - // We don't have any tasks that need to be triggered after the server has successfully initialized. - return Task.CompletedTask; + // The VS LSP client allows an unexpected OperationCanceledException to propagate out of the StartAsync + // callback. Avoid allowing it to propagate further. + e.Handle(ex => ex is OperationCanceledException); } + } - internal async Task> CreateAsync( - AbstractInProcLanguageClient languageClient, - Stream inputStream, - Stream outputStream, - WellKnownLspServerKinds serverKind, - ILspServiceLoggerFactory lspLoggerFactory, - CancellationToken cancellationToken) - { - var jsonMessageFormatter = new JsonMessageFormatter(); - VSInternalExtensionUtilities.AddVSInternalExtensionConverters(jsonMessageFormatter.JsonSerializer); + /// + /// Signals the extension that the language server has been successfully initialized. + /// + /// A which completes when actions that need to be performed when the server is ready are done. + public Task OnServerInitializedAsync() + { + // We don't have any tasks that need to be triggered after the server has successfully initialized. + return Task.CompletedTask; + } - var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(outputStream, inputStream, jsonMessageFormatter)) - { - ExceptionStrategy = ExceptionProcessing.ISerializable, - }; + internal async Task> CreateAsync( + AbstractInProcLanguageClient languageClient, + Stream inputStream, + Stream outputStream, + WellKnownLspServerKinds serverKind, + ILspServiceLoggerFactory lspLoggerFactory, + CancellationToken cancellationToken) + { + var jsonMessageFormatter = new JsonMessageFormatter(); + VSInternalExtensionUtilities.AddVSInternalExtensionConverters(jsonMessageFormatter.JsonSerializer); - var serverTypeName = languageClient.GetType().Name; + var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(outputStream, inputStream, jsonMessageFormatter)) + { + ExceptionStrategy = ExceptionProcessing.ISerializable, + }; - var logger = await lspLoggerFactory.CreateLoggerAsync(serverTypeName, jsonRpc, cancellationToken).ConfigureAwait(false); + var serverTypeName = languageClient.GetType().Name; - var hostServices = VisualStudioMefHostServices.Create(_exportProvider); - var server = Create( - jsonRpc, - languageClient, - serverKind, - logger, - hostServices); + var logger = await lspLoggerFactory.CreateLoggerAsync(serverTypeName, jsonRpc, cancellationToken).ConfigureAwait(false); - jsonRpc.StartListening(); - return server; - } + var hostServices = VisualStudioMefHostServices.Create(_exportProvider); + var server = Create( + jsonRpc, + languageClient, + serverKind, + logger, + hostServices); - public virtual AbstractLanguageServer Create( - JsonRpc jsonRpc, - ICapabilitiesProvider capabilitiesProvider, - WellKnownLspServerKinds serverKind, - AbstractLspLogger logger, - HostServices hostServices) - { - var server = new RoslynLanguageServer( - LspServiceProvider, - jsonRpc, - capabilitiesProvider, - logger, - hostServices, - SupportedLanguages, - serverKind); - - return server; - } + jsonRpc.StartListening(); + return server; + } - public abstract ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities); + public virtual AbstractLanguageServer Create( + JsonRpc jsonRpc, + ICapabilitiesProvider capabilitiesProvider, + WellKnownLspServerKinds serverKind, + AbstractLspLogger logger, + HostServices hostServices) + { + var server = new RoslynLanguageServer( + LspServiceProvider, + jsonRpc, + capabilitiesProvider, + logger, + hostServices, + SupportedLanguages, + serverKind); + + return server; + } - public Task OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState) - { - var initializationFailureContext = new InitializationFailureContext(); - initializationFailureContext.FailureMessage = string.Format(EditorFeaturesResources.Language_client_initialization_failed, - Name, initializationState.StatusMessage, initializationState.InitializationException?.ToString()); - return Task.FromResult(initializationFailureContext); - } + public abstract ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities); - /// - /// Unused, implementing . - /// This method is called after the language server has been activated, but connection has not been established. - /// - public Task AttachForCustomMessageAsync(JsonRpc rpc) => Task.CompletedTask; + public Task OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState) + { + var initializationFailureContext = new InitializationFailureContext(); + initializationFailureContext.FailureMessage = string.Format(EditorFeaturesResources.Language_client_initialization_failed, + Name, initializationState.StatusMessage, initializationState.InitializationException?.ToString()); + return Task.FromResult(initializationFailureContext); + } - internal TestAccessor GetTestAccessor() - { - return new TestAccessor(this); - } + /// + /// Unused, implementing . + /// This method is called after the language server has been activated, but connection has not been established. + /// + public Task AttachForCustomMessageAsync(JsonRpc rpc) => Task.CompletedTask; - internal readonly struct TestAccessor - { - private readonly AbstractInProcLanguageClient _instance; + internal TestAccessor GetTestAccessor() + { + return new TestAccessor(this); + } - internal TestAccessor(AbstractInProcLanguageClient instance) - { - _instance = instance; - } + internal readonly struct TestAccessor + { + private readonly AbstractInProcLanguageClient _instance; - public AbstractLanguageServer? LanguageServer => _instance._languageServer; + internal TestAccessor(AbstractInProcLanguageClient instance) + { + _instance = instance; } + + public AbstractLanguageServer? LanguageServer => _instance._languageServer; } } diff --git a/src/EditorFeatures/Core/LanguageServer/AbstractLanguageClientMiddleLayer.cs b/src/EditorFeatures/Core/LanguageServer/AbstractLanguageClientMiddleLayer.cs index 9e8d2fcb1537b..e7c26c5b245b0 100644 --- a/src/EditorFeatures/Core/LanguageServer/AbstractLanguageClientMiddleLayer.cs +++ b/src/EditorFeatures/Core/LanguageServer/AbstractLanguageClientMiddleLayer.cs @@ -7,14 +7,13 @@ using Microsoft.VisualStudio.LanguageServer.Client; using Newtonsoft.Json.Linq; -namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient +namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient; + +internal abstract class AbstractLanguageClientMiddleLayer : ILanguageClientMiddleLayer { - internal abstract class AbstractLanguageClientMiddleLayer : ILanguageClientMiddleLayer - { - public abstract bool CanHandle(string methodName); + public abstract bool CanHandle(string methodName); - public abstract Task HandleNotificationAsync(string methodName, JToken methodParam, Func sendNotification); + public abstract Task HandleNotificationAsync(string methodName, JToken methodParam, Func sendNotification); - public abstract Task HandleRequestAsync(string methodName, JToken methodParam, Func> sendRequest); - } + public abstract Task HandleRequestAsync(string methodName, JToken methodParam, Func> sendRequest); } \ No newline at end of file diff --git a/src/EditorFeatures/Core/LanguageServer/AlwaysActivateInProcLanguageClient.cs b/src/EditorFeatures/Core/LanguageServer/AlwaysActivateInProcLanguageClient.cs index d083c40829563..71e76d1267d78 100644 --- a/src/EditorFeatures/Core/LanguageServer/AlwaysActivateInProcLanguageClient.cs +++ b/src/EditorFeatures/Core/LanguageServer/AlwaysActivateInProcLanguageClient.cs @@ -19,126 +19,125 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.LanguageServer.Protocol; -namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient +namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient; + +/// +/// Language client responsible for handling C# / VB / F# LSP requests in any scenario (both local and codespaces). +/// This powers "LSP only" features (e.g. cntrl+Q code search) that do not use traditional editor APIs. +/// It is always activated whenever roslyn is activated. +/// +[ContentType(ContentTypeNames.CSharpContentType)] +[ContentType(ContentTypeNames.VisualBasicContentType)] +[ContentType(ContentTypeNames.FSharpContentType)] +[Export(typeof(ILanguageClient))] +[Export(typeof(AlwaysActivateInProcLanguageClient))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, true)] +internal class AlwaysActivateInProcLanguageClient( + CSharpVisualBasicLspServiceProvider lspServiceProvider, + IGlobalOptionService globalOptions, + ExperimentalCapabilitiesProvider defaultCapabilitiesProvider, + ILspServiceLoggerFactory lspLoggerFactory, + IThreadingContext threadingContext, + ExportProvider exportProvider, + [ImportMany] IEnumerable> buildOnlyDiagnostics) : AbstractInProcLanguageClient(lspServiceProvider, globalOptions, lspLoggerFactory, threadingContext, exportProvider) { - /// - /// Language client responsible for handling C# / VB / F# LSP requests in any scenario (both local and codespaces). - /// This powers "LSP only" features (e.g. cntrl+Q code search) that do not use traditional editor APIs. - /// It is always activated whenever roslyn is activated. - /// - [ContentType(ContentTypeNames.CSharpContentType)] - [ContentType(ContentTypeNames.VisualBasicContentType)] - [ContentType(ContentTypeNames.FSharpContentType)] - [Export(typeof(ILanguageClient))] - [Export(typeof(AlwaysActivateInProcLanguageClient))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, true)] - internal class AlwaysActivateInProcLanguageClient( - CSharpVisualBasicLspServiceProvider lspServiceProvider, - IGlobalOptionService globalOptions, - ExperimentalCapabilitiesProvider defaultCapabilitiesProvider, - ILspServiceLoggerFactory lspLoggerFactory, - IThreadingContext threadingContext, - ExportProvider exportProvider, - [ImportMany] IEnumerable> buildOnlyDiagnostics) : AbstractInProcLanguageClient(lspServiceProvider, globalOptions, lspLoggerFactory, threadingContext, exportProvider) - { - private readonly ExperimentalCapabilitiesProvider _experimentalCapabilitiesProvider = defaultCapabilitiesProvider; - private readonly IEnumerable> _buildOnlyDiagnostics = buildOnlyDiagnostics; + private readonly ExperimentalCapabilitiesProvider _experimentalCapabilitiesProvider = defaultCapabilitiesProvider; + private readonly IEnumerable> _buildOnlyDiagnostics = buildOnlyDiagnostics; - protected override ImmutableArray SupportedLanguages => ProtocolConstants.RoslynLspLanguages; + protected override ImmutableArray SupportedLanguages => ProtocolConstants.RoslynLspLanguages; - public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) - { - // If the LSP editor feature flag is enabled advertise support for LSP features here so they are available locally and remote. - var isLspEditorEnabled = GlobalOptions.GetOption(LspOptionsStorage.LspEditorFeatureFlag); + public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) + { + // If the LSP editor feature flag is enabled advertise support for LSP features here so they are available locally and remote. + var isLspEditorEnabled = GlobalOptions.GetOption(LspOptionsStorage.LspEditorFeatureFlag); - var serverCapabilities = isLspEditorEnabled - ? (VSInternalServerCapabilities)_experimentalCapabilitiesProvider.GetCapabilities(clientCapabilities) - : new VSInternalServerCapabilities() + var serverCapabilities = isLspEditorEnabled + ? (VSInternalServerCapabilities)_experimentalCapabilitiesProvider.GetCapabilities(clientCapabilities) + : new VSInternalServerCapabilities() + { + // Even if the flag is off, we want to include text sync capabilities. + TextDocumentSync = new TextDocumentSyncOptions { - // Even if the flag is off, we want to include text sync capabilities. - TextDocumentSync = new TextDocumentSyncOptions - { - Change = TextDocumentSyncKind.Incremental, - OpenClose = true, - }, - }; + Change = TextDocumentSyncKind.Incremental, + OpenClose = true, + }, + }; - serverCapabilities.ProjectContextProvider = true; - serverCapabilities.BreakableRangeProvider = true; + serverCapabilities.ProjectContextProvider = true; + serverCapabilities.BreakableRangeProvider = true; - var isPullDiagnostics = GlobalOptions.IsLspPullDiagnostics(); - if (isPullDiagnostics) + var isPullDiagnostics = GlobalOptions.IsLspPullDiagnostics(); + if (isPullDiagnostics) + { + serverCapabilities.SupportsDiagnosticRequests = true; + serverCapabilities.DiagnosticProvider ??= new(); + serverCapabilities.DiagnosticProvider = serverCapabilities.DiagnosticProvider with { - serverCapabilities.SupportsDiagnosticRequests = true; - serverCapabilities.DiagnosticProvider ??= new(); - serverCapabilities.DiagnosticProvider = serverCapabilities.DiagnosticProvider with - { - SupportsMultipleContextsDiagnostics = true, - DiagnosticKinds = - [ - // Support a specialized requests dedicated to task-list items. This way the client can ask just - // for these, independently of other diagnostics. They can also throttle themselves to not ask if - // the task list would not be visible. - new(PullDiagnosticCategories.Task), - // Dedicated request for workspace-diagnostics only. We will only respond to these if FSA is on. - new(PullDiagnosticCategories.WorkspaceDocumentsAndProject), - // Fine-grained diagnostics requests. Importantly, this separates out syntactic vs semantic - // requests, allowing the former to quickly reach the user without blocking on the latter. In a - // similar vein, compiler diagnostics are explicitly distinct from analyzer-diagnostics, allowing - // the former to appear as soon as possible as they are much more critical for the user and should - // not be delayed by a slow analyzer. - new(PullDiagnosticCategories.DocumentCompilerSyntax), - new(PullDiagnosticCategories.DocumentCompilerSemantic), - new(PullDiagnosticCategories.DocumentAnalyzerSyntax), - new(PullDiagnosticCategories.DocumentAnalyzerSemantic), - ], - BuildOnlyDiagnosticIds = _buildOnlyDiagnostics - .SelectMany(lazy => lazy.Metadata.BuildOnlyDiagnostics) - .Distinct() - .ToArray(), - }; - } + SupportsMultipleContextsDiagnostics = true, + DiagnosticKinds = + [ + // Support a specialized requests dedicated to task-list items. This way the client can ask just + // for these, independently of other diagnostics. They can also throttle themselves to not ask if + // the task list would not be visible. + new(PullDiagnosticCategories.Task), + // Dedicated request for workspace-diagnostics only. We will only respond to these if FSA is on. + new(PullDiagnosticCategories.WorkspaceDocumentsAndProject), + // Fine-grained diagnostics requests. Importantly, this separates out syntactic vs semantic + // requests, allowing the former to quickly reach the user without blocking on the latter. In a + // similar vein, compiler diagnostics are explicitly distinct from analyzer-diagnostics, allowing + // the former to appear as soon as possible as they are much more critical for the user and should + // not be delayed by a slow analyzer. + new(PullDiagnosticCategories.DocumentCompilerSyntax), + new(PullDiagnosticCategories.DocumentCompilerSemantic), + new(PullDiagnosticCategories.DocumentAnalyzerSyntax), + new(PullDiagnosticCategories.DocumentAnalyzerSemantic), + ], + BuildOnlyDiagnosticIds = _buildOnlyDiagnostics + .SelectMany(lazy => lazy.Metadata.BuildOnlyDiagnostics) + .Distinct() + .ToArray(), + }; + } - // This capability is always enabled as we provide cntrl+Q VS search only via LSP in ever scenario. - serverCapabilities.WorkspaceSymbolProvider = true; - // This capability prevents NavigateTo (cntrl+,) from using LSP symbol search when the server also supports WorkspaceSymbolProvider. - // Since WorkspaceSymbolProvider=true always to allow cntrl+Q VS search to function, we set DisableGoToWorkspaceSymbols=true - // when not running the experimental LSP editor. This ensures NavigateTo uses the existing editor APIs. - // However, when the experimental LSP editor is enabled we want LSP to power NavigateTo, so we set DisableGoToWorkspaceSymbols=false. - serverCapabilities.DisableGoToWorkspaceSymbols = !isLspEditorEnabled; + // This capability is always enabled as we provide cntrl+Q VS search only via LSP in ever scenario. + serverCapabilities.WorkspaceSymbolProvider = true; + // This capability prevents NavigateTo (cntrl+,) from using LSP symbol search when the server also supports WorkspaceSymbolProvider. + // Since WorkspaceSymbolProvider=true always to allow cntrl+Q VS search to function, we set DisableGoToWorkspaceSymbols=true + // when not running the experimental LSP editor. This ensures NavigateTo uses the existing editor APIs. + // However, when the experimental LSP editor is enabled we want LSP to power NavigateTo, so we set DisableGoToWorkspaceSymbols=false. + serverCapabilities.DisableGoToWorkspaceSymbols = !isLspEditorEnabled; - var isLspSemanticTokensEnabled = GlobalOptions.GetOption(LspOptionsStorage.LspSemanticTokensFeatureFlag); - if (isLspSemanticTokensEnabled) + var isLspSemanticTokensEnabled = GlobalOptions.GetOption(LspOptionsStorage.LspSemanticTokensFeatureFlag); + if (isLspSemanticTokensEnabled) + { + // Using only range handling has shown to be more performant than using a combination of full/edits/range handling, + // especially for larger files. With range handling, we only need to compute tokens for whatever is in view, while + // with full/edits handling we need to compute tokens for the entire file and then potentially run a diff between + // the old and new tokens. + serverCapabilities.SemanticTokensOptions = new SemanticTokensOptions { - // Using only range handling has shown to be more performant than using a combination of full/edits/range handling, - // especially for larger files. With range handling, we only need to compute tokens for whatever is in view, while - // with full/edits handling we need to compute tokens for the entire file and then potentially run a diff between - // the old and new tokens. - serverCapabilities.SemanticTokensOptions = new SemanticTokensOptions + Full = false, + Range = true, + Legend = new SemanticTokensLegend { - Full = false, - Range = true, - Legend = new SemanticTokensLegend - { - TokenTypes = SemanticTokensSchema.GetSchema(clientCapabilities.HasVisualStudioLspCapability()).AllTokenTypes.ToArray(), - TokenModifiers = SemanticTokensSchema.TokenModifiers - } - }; - } - - serverCapabilities.SpellCheckingProvider = true; - - return serverCapabilities; + TokenTypes = SemanticTokensSchema.GetSchema(clientCapabilities.HasVisualStudioLspCapability()).AllTokenTypes.ToArray(), + TokenModifiers = SemanticTokensSchema.TokenModifiers + } + }; } - /// - /// When pull diagnostics is enabled, ensure that initialization failures are displayed to the user as - /// they will get no diagnostics. When not enabled we don't show the failure box (failure will still be recorded in the task status center) - /// as the failure is not catastrophic. - /// - public override bool ShowNotificationOnInitializeFailed => GlobalOptions.IsLspPullDiagnostics(); + serverCapabilities.SpellCheckingProvider = true; - public override WellKnownLspServerKinds ServerKind => WellKnownLspServerKinds.AlwaysActiveVSLspServer; + return serverCapabilities; } + + /// + /// When pull diagnostics is enabled, ensure that initialization failures are displayed to the user as + /// they will get no diagnostics. When not enabled we don't show the failure box (failure will still be recorded in the task status center) + /// as the failure is not catastrophic. + /// + public override bool ShowNotificationOnInitializeFailed => GlobalOptions.IsLspPullDiagnostics(); + + public override WellKnownLspServerKinds ServerKind => WellKnownLspServerKinds.AlwaysActiveVSLspServer; } diff --git a/src/EditorFeatures/Core/LanguageServer/AlwaysActiveLanguageClientEventListener.cs b/src/EditorFeatures/Core/LanguageServer/AlwaysActiveLanguageClientEventListener.cs index 3dd8442dcefda..a7eda00428dec 100644 --- a/src/EditorFeatures/Core/LanguageServer/AlwaysActiveLanguageClientEventListener.cs +++ b/src/EditorFeatures/Core/LanguageServer/AlwaysActiveLanguageClientEventListener.cs @@ -17,69 +17,68 @@ using Microsoft.VisualStudio.LanguageServer.Client; using Microsoft.VisualStudio.Threading; -namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient +namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient; + +// unfortunately, we can't implement this on LanguageServerClient since this uses MEF v2 and +// ILanguageClient requires MEF v1 and two can't be mixed exported in 1 class. +[Export] +[ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class AlwaysActiveLanguageClientEventListener( + AlwaysActivateInProcLanguageClient languageClient, + Lazy languageClientBroker, + IAsynchronousOperationListenerProvider listenerProvider) : IEventListener { - // unfortunately, we can't implement this on LanguageServerClient since this uses MEF v2 and - // ILanguageClient requires MEF v1 and two can't be mixed exported in 1 class. - [Export] - [ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class AlwaysActiveLanguageClientEventListener( - AlwaysActivateInProcLanguageClient languageClient, - Lazy languageClientBroker, - IAsynchronousOperationListenerProvider listenerProvider) : IEventListener - { - private readonly AlwaysActivateInProcLanguageClient _languageClient = languageClient; - private readonly Lazy _languageClientBroker = languageClientBroker; + private readonly AlwaysActivateInProcLanguageClient _languageClient = languageClient; + private readonly Lazy _languageClientBroker = languageClientBroker; - private readonly IAsynchronousOperationListener _asynchronousOperationListener = listenerProvider.GetListener(FeatureAttribute.LanguageServer); + private readonly IAsynchronousOperationListener _asynchronousOperationListener = listenerProvider.GetListener(FeatureAttribute.LanguageServer); - /// - /// LSP clients do not necessarily know which language servers (and when) to activate as they are language - /// agnostic. We know we can provide as soon as the - /// workspace is started, so tell the to start loading it. - /// - public void StartListening(Workspace workspace, object serviceOpt) - { - // Trigger a fire and forget request to the VS LSP client to load our ILanguageClient. - _ = LoadAsync(); - } + /// + /// LSP clients do not necessarily know which language servers (and when) to activate as they are language + /// agnostic. We know we can provide as soon as the + /// workspace is started, so tell the to start loading it. + /// + public void StartListening(Workspace workspace, object serviceOpt) + { + // Trigger a fire and forget request to the VS LSP client to load our ILanguageClient. + _ = LoadAsync(); + } - private async Task LoadAsync() + private async Task LoadAsync() + { + try { - try - { - using var token = _asynchronousOperationListener.BeginAsyncOperation(nameof(LoadAsync)); + using var token = _asynchronousOperationListener.BeginAsyncOperation(nameof(LoadAsync)); - // Explicitly switch to the bg so that if this causes any expensive work (like mef loads) it - // doesn't block the UI thread. Note, we always yield because sometimes our caller starts - // on the threadpool thread but is indirectly blocked on by the UI thread. - await TaskScheduler.Default.SwitchTo(alwaysYield: true); + // Explicitly switch to the bg so that if this causes any expensive work (like mef loads) it + // doesn't block the UI thread. Note, we always yield because sometimes our caller starts + // on the threadpool thread but is indirectly blocked on by the UI thread. + await TaskScheduler.Default.SwitchTo(alwaysYield: true); - await _languageClientBroker.Value.LoadAsync(new LanguageClientMetadata( - [ - ContentTypeNames.CSharpContentType, - ContentTypeNames.VisualBasicContentType, - ContentTypeNames.FSharpContentType - ]), _languageClient).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatch(e)) - { - } + await _languageClientBroker.Value.LoadAsync(new LanguageClientMetadata( + [ + ContentTypeNames.CSharpContentType, + ContentTypeNames.VisualBasicContentType, + ContentTypeNames.FSharpContentType + ]), _languageClient).ConfigureAwait(false); } - - /// - /// The - /// requires that we pass the along with the language client instance. - /// The implementation of is not public, so have to re-implement. - /// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1043922 tracking to remove this. - /// - private class LanguageClientMetadata(string[] contentTypes, string clientName = null) : ILanguageClientMetadata + catch (Exception e) when (FatalError.ReportAndCatch(e)) { - public string ClientName { get; } = clientName; - - public IEnumerable ContentTypes { get; } = contentTypes; } } + + /// + /// The + /// requires that we pass the along with the language client instance. + /// The implementation of is not public, so have to re-implement. + /// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1043922 tracking to remove this. + /// + private class LanguageClientMetadata(string[] contentTypes, string clientName = null) : ILanguageClientMetadata + { + public string ClientName { get; } = clientName; + + public IEnumerable ContentTypes { get; } = contentTypes; + } } diff --git a/src/EditorFeatures/Core/LanguageServer/EditorHoverCreationService.cs b/src/EditorFeatures/Core/LanguageServer/EditorHoverCreationService.cs index 70029355e3faf..6cd6b3c9f2149 100644 --- a/src/EditorFeatures/Core/LanguageServer/EditorHoverCreationService.cs +++ b/src/EditorFeatures/Core/LanguageServer/EditorHoverCreationService.cs @@ -16,51 +16,50 @@ using Microsoft.CodeAnalysis.QuickInfo; using Roslyn.LanguageServer.Protocol; -namespace Microsoft.CodeAnalysis.LanguageServer +namespace Microsoft.CodeAnalysis.LanguageServer; + +[ExportWorkspaceService(typeof(ILspHoverResultCreationService), ServiceLayer.Editor), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class EditorLspHoverResultCreationService(IGlobalOptionService globalOptions) : ILspHoverResultCreationService { - [ExportWorkspaceService(typeof(ILspHoverResultCreationService), ServiceLayer.Editor), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class EditorLspHoverResultCreationService(IGlobalOptionService globalOptions) : ILspHoverResultCreationService - { - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IGlobalOptionService _globalOptions = globalOptions; - public async Task CreateHoverAsync( - Document document, QuickInfoItem info, ClientCapabilities clientCapabilities, CancellationToken cancellationToken) - { - var supportsVSExtensions = clientCapabilities.HasVisualStudioLspCapability(); + public async Task CreateHoverAsync( + Document document, QuickInfoItem info, ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + { + var supportsVSExtensions = clientCapabilities.HasVisualStudioLspCapability(); - if (!supportsVSExtensions) - return await DefaultLspHoverResultCreationService.CreateDefaultHoverAsync(document, info, clientCapabilities, cancellationToken).ConfigureAwait(false); + if (!supportsVSExtensions) + return await DefaultLspHoverResultCreationService.CreateDefaultHoverAsync(document, info, clientCapabilities, cancellationToken).ConfigureAwait(false); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var language = document.Project.Language; + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var language = document.Project.Language; - var classificationOptions = _globalOptions.GetClassificationOptions(language); + var classificationOptions = _globalOptions.GetClassificationOptions(language); - // We can pass null for all these parameter values as they're only needed for quick-info content navigation - // and we explicitly calling BuildContentWithoutNavigationActionsAsync. - var context = document is null - ? null - : new IntellisenseQuickInfoBuilderContext( - document, - classificationOptions, - await document.GetLineFormattingOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false), - threadingContext: null, - operationExecutor: null, - asynchronousOperationListener: null, - streamingPresenter: null); + // We can pass null for all these parameter values as they're only needed for quick-info content navigation + // and we explicitly calling BuildContentWithoutNavigationActionsAsync. + var context = document is null + ? null + : new IntellisenseQuickInfoBuilderContext( + document, + classificationOptions, + await document.GetLineFormattingOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false), + threadingContext: null, + operationExecutor: null, + asynchronousOperationListener: null, + streamingPresenter: null); - var element = await IntellisenseQuickInfoBuilder.BuildContentWithoutNavigationActionsAsync(info, context, cancellationToken).ConfigureAwait(false); - return new VSInternalHover - { - Range = ProtocolConversions.TextSpanToRange(info.Span, text), - Contents = new SumType[], MarkupContent>(string.Empty), - // Build the classified text without navigation actions - they are not serializable. - // TODO - Switch to markup content once it supports classifications. - // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/918138 - RawContent = element.ToLSPElement(), - }; - } + var element = await IntellisenseQuickInfoBuilder.BuildContentWithoutNavigationActionsAsync(info, context, cancellationToken).ConfigureAwait(false); + return new VSInternalHover + { + Range = ProtocolConversions.TextSpanToRange(info.Span, text), + Contents = new SumType[], MarkupContent>(string.Empty), + // Build the classified text without navigation actions - they are not serializable. + // TODO - Switch to markup content once it supports classifications. + // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/918138 + RawContent = element.ToLSPElement(), + }; } } diff --git a/src/EditorFeatures/Core/LanguageServer/EditorLspCompletionResultCreationService.cs b/src/EditorFeatures/Core/LanguageServer/EditorLspCompletionResultCreationService.cs index e79d0c7744cdb..c70197ea91e15 100644 --- a/src/EditorFeatures/Core/LanguageServer/EditorLspCompletionResultCreationService.cs +++ b/src/EditorFeatures/Core/LanguageServer/EditorLspCompletionResultCreationService.cs @@ -18,105 +18,104 @@ using Roslyn.Utilities; using LSP = Roslyn.LanguageServer.Protocol; -namespace Microsoft.CodeAnalysis.LanguageServer +namespace Microsoft.CodeAnalysis.LanguageServer; + +[ExportWorkspaceService(typeof(ILspCompletionResultCreationService), ServiceLayer.Editor), Shared] +internal sealed class EditorLspCompletionResultCreationService : AbstractLspCompletionResultCreationService { - [ExportWorkspaceService(typeof(ILspCompletionResultCreationService), ServiceLayer.Editor), Shared] - internal sealed class EditorLspCompletionResultCreationService : AbstractLspCompletionResultCreationService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public EditorLspCompletionResultCreationService() + { + } + + protected override async Task CreateItemAndPopulateTextEditAsync( + Document document, + SourceText documentText, + bool snippetsSupported, + bool itemDefaultsSupported, + TextSpan defaultSpan, + string typedText, + CompletionItem item, + CompletionService completionService, + CancellationToken cancellationToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EditorLspCompletionResultCreationService() + var lspItem = new LSP.VSInternalCompletionItem { - } + Label = item.GetEntireDisplayText(), + Icon = new ImageElement(item.Tags.GetFirstGlyph().GetImageId().ToLSPImageId()), + }; - protected override async Task CreateItemAndPopulateTextEditAsync( - Document document, - SourceText documentText, - bool snippetsSupported, - bool itemDefaultsSupported, - TextSpan defaultSpan, - string typedText, - CompletionItem item, - CompletionService completionService, - CancellationToken cancellationToken) + // Complex text edits (e.g. override and partial method completions) are always populated in the + // resolve handler, so we leave both TextEdit and InsertText unpopulated in these cases. + if (item.IsComplexTextEdit) { - var lspItem = new LSP.VSInternalCompletionItem - { - Label = item.GetEntireDisplayText(), - Icon = new ImageElement(item.Tags.GetFirstGlyph().GetImageId().ToLSPImageId()), - }; + lspItem.VsResolveTextEditOnCommit = true; - // Complex text edits (e.g. override and partial method completions) are always populated in the - // resolve handler, so we leave both TextEdit and InsertText unpopulated in these cases. - if (item.IsComplexTextEdit) - { - lspItem.VsResolveTextEditOnCommit = true; + // Razor C# is currently the only language client that supports LSP.InsertTextFormat.Snippet. + // We can enable it for regular C# once LSP is used for local completion. + if (snippetsSupported) + lspItem.InsertTextFormat = LSP.InsertTextFormat.Snippet; + } + else + { + await GetChangeAndPopulateSimpleTextEditAsync( + document, + documentText, + itemDefaultsSupported, + defaultSpan, + item, + lspItem, + completionService, + cancellationToken).ConfigureAwait(false); + } + + return lspItem; + } - // Razor C# is currently the only language client that supports LSP.InsertTextFormat.Snippet. - // We can enable it for regular C# once LSP is used for local completion. - if (snippetsSupported) - lspItem.InsertTextFormat = LSP.InsertTextFormat.Snippet; + public override async Task ResolveAsync( + LSP.CompletionItem lspItem, + CompletionItem roslynItem, + LSP.TextDocumentIdentifier textDocumentIdentifier, + Document document, + CompletionCapabilityHelper capabilityHelper, + CompletionService completionService, + CompletionOptions completionOptions, + SymbolDescriptionOptions symbolDescriptionOptions, + CancellationToken cancellationToken) + { + var description = await completionService.GetDescriptionAsync(document, roslynItem, completionOptions, symbolDescriptionOptions, cancellationToken).ConfigureAwait(false)!; + if (description != null) + { + if (capabilityHelper.SupportVSInternalClientCapabilities) + { + var vsCompletionItem = (LSP.VSInternalCompletionItem)lspItem; + vsCompletionItem.Description = new ClassifiedTextElement(description.TaggedParts + .Select(tp => new ClassifiedTextRun(tp.Tag.ToClassificationTypeName(), tp.Text))); } else { - await GetChangeAndPopulateSimpleTextEditAsync( - document, - documentText, - itemDefaultsSupported, - defaultSpan, - item, - lspItem, - completionService, - cancellationToken).ConfigureAwait(false); + lspItem.Documentation = ProtocolConversions.GetDocumentationMarkupContent(description.TaggedParts, document, capabilityHelper.SupportsMarkdownDocumentation); } - - return lspItem; } - public override async Task ResolveAsync( - LSP.CompletionItem lspItem, - CompletionItem roslynItem, - LSP.TextDocumentIdentifier textDocumentIdentifier, - Document document, - CompletionCapabilityHelper capabilityHelper, - CompletionService completionService, - CompletionOptions completionOptions, - SymbolDescriptionOptions symbolDescriptionOptions, - CancellationToken cancellationToken) + // We compute the TextEdit resolves for complex text edits (e.g. override and partial + // method completions) here. Lazily resolving TextEdits is technically a violation of + // the LSP spec, but is currently supported by the VS client anyway. Once the VS client + // adheres to the spec, this logic will need to change and VS will need to provide + // official support for TextEdit resolution in some form. + if (roslynItem.IsComplexTextEdit) { - var description = await completionService.GetDescriptionAsync(document, roslynItem, completionOptions, symbolDescriptionOptions, cancellationToken).ConfigureAwait(false)!; - if (description != null) - { - if (capabilityHelper.SupportVSInternalClientCapabilities) - { - var vsCompletionItem = (LSP.VSInternalCompletionItem)lspItem; - vsCompletionItem.Description = new ClassifiedTextElement(description.TaggedParts - .Select(tp => new ClassifiedTextRun(tp.Tag.ToClassificationTypeName(), tp.Text))); - } - else - { - lspItem.Documentation = ProtocolConversions.GetDocumentationMarkupContent(description.TaggedParts, document, capabilityHelper.SupportsMarkdownDocumentation); - } - } + Contract.ThrowIfTrue(lspItem.InsertText != null); + Contract.ThrowIfTrue(lspItem.TextEdit != null); - // We compute the TextEdit resolves for complex text edits (e.g. override and partial - // method completions) here. Lazily resolving TextEdits is technically a violation of - // the LSP spec, but is currently supported by the VS client anyway. Once the VS client - // adheres to the spec, this logic will need to change and VS will need to provide - // official support for TextEdit resolution in some form. - if (roslynItem.IsComplexTextEdit) - { - Contract.ThrowIfTrue(lspItem.InsertText != null); - Contract.ThrowIfTrue(lspItem.TextEdit != null); - - var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - var (edit, _, _) = await GenerateComplexTextEditAsync( - document, completionService, roslynItem, capabilityHelper.SupportSnippets, insertNewPositionPlaceholder: true, cancellationToken).ConfigureAwait(false); + var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var (edit, _, _) = await GenerateComplexTextEditAsync( + document, completionService, roslynItem, capabilityHelper.SupportSnippets, insertNewPositionPlaceholder: true, cancellationToken).ConfigureAwait(false); - lspItem.TextEdit = edit; - } - - return lspItem; + lspItem.TextEdit = edit; } + + return lspItem; } } diff --git a/src/EditorFeatures/Core/LanguageServer/EditorLspReferencesResultCreationService.cs b/src/EditorFeatures/Core/LanguageServer/EditorLspReferencesResultCreationService.cs index a261b2ef69cf7..b60d5f08024af 100644 --- a/src/EditorFeatures/Core/LanguageServer/EditorLspReferencesResultCreationService.cs +++ b/src/EditorFeatures/Core/LanguageServer/EditorLspReferencesResultCreationService.cs @@ -14,60 +14,59 @@ using Roslyn.Text.Adornments; using LSP = Roslyn.LanguageServer.Protocol; -namespace Microsoft.CodeAnalysis.LanguageServer +namespace Microsoft.CodeAnalysis.LanguageServer; + +[ExportWorkspaceService(typeof(ILspReferencesResultCreationService), ServiceLayer.Editor), Shared] +internal sealed class EditorLspReferencesResultCreationService : ILspReferencesResultCreationService { - [ExportWorkspaceService(typeof(ILspReferencesResultCreationService), ServiceLayer.Editor), Shared] - internal sealed class EditorLspReferencesResultCreationService : ILspReferencesResultCreationService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public EditorLspReferencesResultCreationService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EditorLspReferencesResultCreationService() - { - } + } - public SumType? CreateReference( - int definitionId, - int id, - ClassifiedTextElement text, - DocumentSpan? documentSpan, - ImmutableDictionary properties, - ClassifiedTextElement? definitionText, - Glyph definitionGlyph, - SymbolUsageInfo? symbolUsageInfo, - Roslyn.LanguageServer.Protocol.Location? location) + public SumType? CreateReference( + int definitionId, + int id, + ClassifiedTextElement text, + DocumentSpan? documentSpan, + ImmutableDictionary properties, + ClassifiedTextElement? definitionText, + Glyph definitionGlyph, + SymbolUsageInfo? symbolUsageInfo, + Roslyn.LanguageServer.Protocol.Location? location) + { + // TO-DO: The Origin property should be added once Rich-Nav is completed. + // https://github.com/dotnet/roslyn/issues/42847 + var imageId = definitionGlyph.GetImageId(); + var result = new VSInternalReferenceItem { - // TO-DO: The Origin property should be added once Rich-Nav is completed. - // https://github.com/dotnet/roslyn/issues/42847 - var imageId = definitionGlyph.GetImageId(); - var result = new VSInternalReferenceItem - { - DefinitionId = definitionId, - DefinitionText = definitionText, // Only definitions should have a non-null DefinitionText - DefinitionIcon = new ImageElement(imageId.ToLSPImageId()), - DisplayPath = location?.Uri.LocalPath, - Id = id, - Kind = symbolUsageInfo.HasValue ? ProtocolConversions.SymbolUsageInfoToReferenceKinds(symbolUsageInfo.Value) : [], - ResolutionStatus = VSInternalResolutionStatusKind.ConfirmedAsReference, - Text = text, - }; + DefinitionId = definitionId, + DefinitionText = definitionText, // Only definitions should have a non-null DefinitionText + DefinitionIcon = new ImageElement(imageId.ToLSPImageId()), + DisplayPath = location?.Uri.LocalPath, + Id = id, + Kind = symbolUsageInfo.HasValue ? ProtocolConversions.SymbolUsageInfoToReferenceKinds(symbolUsageInfo.Value) : [], + ResolutionStatus = VSInternalResolutionStatusKind.ConfirmedAsReference, + Text = text, + }; - // There are certain items that may not have locations, such as namespace definitions. - if (location != null) - result.Location = location; + // There are certain items that may not have locations, such as namespace definitions. + if (location != null) + result.Location = location; - if (documentSpan != null) - { - result.DocumentName = documentSpan.Value.Document.Name; - result.ProjectName = documentSpan.Value.Document.Project.Name; - } + if (documentSpan != null) + { + result.DocumentName = documentSpan.Value.Document.Name; + result.ProjectName = documentSpan.Value.Document.Project.Name; + } - if (properties.TryGetValue(AbstractReferenceFinder.ContainingMemberInfoPropertyName, out var referenceContainingMember)) - result.ContainingMember = referenceContainingMember; + if (properties.TryGetValue(AbstractReferenceFinder.ContainingMemberInfoPropertyName, out var referenceContainingMember)) + result.ContainingMember = referenceContainingMember; - if (properties.TryGetValue(AbstractReferenceFinder.ContainingTypeInfoPropertyName, out var referenceContainingType)) - result.ContainingType = referenceContainingType; + if (properties.TryGetValue(AbstractReferenceFinder.ContainingTypeInfoPropertyName, out var referenceContainingType)) + result.ContainingType = referenceContainingType; - return result; - } + return result; } } diff --git a/src/EditorFeatures/Core/LanguageServer/EditorLspSymbolInformationCreationService.cs b/src/EditorFeatures/Core/LanguageServer/EditorLspSymbolInformationCreationService.cs index 2359a42ccc472..63396a1ff96c5 100644 --- a/src/EditorFeatures/Core/LanguageServer/EditorLspSymbolInformationCreationService.cs +++ b/src/EditorFeatures/Core/LanguageServer/EditorLspSymbolInformationCreationService.cs @@ -10,28 +10,27 @@ using Roslyn.LanguageServer.Protocol; using LSP = Roslyn.LanguageServer.Protocol; -namespace Microsoft.CodeAnalysis.LanguageServer +namespace Microsoft.CodeAnalysis.LanguageServer; + +[ExportWorkspaceService(typeof(ILspSymbolInformationCreationService), ServiceLayer.Editor), Shared] +internal sealed class EditorLspSymbolInformationCreationService : ILspSymbolInformationCreationService { - [ExportWorkspaceService(typeof(ILspSymbolInformationCreationService), ServiceLayer.Editor), Shared] - internal sealed class EditorLspSymbolInformationCreationService : ILspSymbolInformationCreationService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public EditorLspSymbolInformationCreationService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EditorLspSymbolInformationCreationService() - { - } + } - public SymbolInformation Create(string name, string? containerName, LSP.SymbolKind kind, LSP.Location location, Glyph glyph) + public SymbolInformation Create(string name, string? containerName, LSP.SymbolKind kind, LSP.Location location, Glyph glyph) + { + var imageId = glyph.GetImageId(); + return new VSSymbolInformation { - var imageId = glyph.GetImageId(); - return new VSSymbolInformation - { - Name = name, - ContainerName = containerName, - Kind = kind, - Location = location, - Icon = new VSImageId { Guid = imageId.Guid, Id = imageId.Id }, - }; - } + Name = name, + ContainerName = containerName, + Kind = kind, + Location = location, + Icon = new VSImageId { Guid = imageId.Guid, Id = imageId.Id }, + }; } } diff --git a/src/EditorFeatures/Core/LanguageServer/LiveShareInProcLanguageClient.cs b/src/EditorFeatures/Core/LanguageServer/LiveShareInProcLanguageClient.cs index 646d1c44a31f5..8be92b5240da3 100644 --- a/src/EditorFeatures/Core/LanguageServer/LiveShareInProcLanguageClient.cs +++ b/src/EditorFeatures/Core/LanguageServer/LiveShareInProcLanguageClient.cs @@ -15,74 +15,73 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.LanguageServer.Protocol; -namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient +namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient; + +// The C# and VB ILanguageClient should not activate on the host. When LiveShare mirrors the C# ILC to the guest, +// they will not copy the DisableUserExperience attribute, so guests will still use the C# ILC. +[DisableUserExperience(true)] +[ContentType(ContentTypeNames.CSharpContentType)] +[ContentType(ContentTypeNames.VisualBasicContentType)] +[Export(typeof(ILanguageClient))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, true)] +internal class LiveShareInProcLanguageClient( + CSharpVisualBasicLspServiceProvider lspServiceProvider, + IGlobalOptionService globalOptions, + ExperimentalCapabilitiesProvider experimentalCapabilitiesProvider, + ILspServiceLoggerFactory lspLoggerFactory, + IThreadingContext threadingContext, + ExportProvider exportProvider) : AbstractInProcLanguageClient(lspServiceProvider, globalOptions, lspLoggerFactory, threadingContext, exportProvider) { - // The C# and VB ILanguageClient should not activate on the host. When LiveShare mirrors the C# ILC to the guest, - // they will not copy the DisableUserExperience attribute, so guests will still use the C# ILC. - [DisableUserExperience(true)] - [ContentType(ContentTypeNames.CSharpContentType)] - [ContentType(ContentTypeNames.VisualBasicContentType)] - [Export(typeof(ILanguageClient))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, true)] - internal class LiveShareInProcLanguageClient( - CSharpVisualBasicLspServiceProvider lspServiceProvider, - IGlobalOptionService globalOptions, - ExperimentalCapabilitiesProvider experimentalCapabilitiesProvider, - ILspServiceLoggerFactory lspLoggerFactory, - IThreadingContext threadingContext, - ExportProvider exportProvider) : AbstractInProcLanguageClient(lspServiceProvider, globalOptions, lspLoggerFactory, threadingContext, exportProvider) - { - private readonly ExperimentalCapabilitiesProvider _experimentalCapabilitiesProvider = experimentalCapabilitiesProvider; + private readonly ExperimentalCapabilitiesProvider _experimentalCapabilitiesProvider = experimentalCapabilitiesProvider; - protected override ImmutableArray SupportedLanguages => ProtocolConstants.RoslynLspLanguages; + protected override ImmutableArray SupportedLanguages => ProtocolConstants.RoslynLspLanguages; - public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) - { - var isLspEditorEnabled = GlobalOptions.GetOption(LspOptionsStorage.LspEditorFeatureFlag); + public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) + { + var isLspEditorEnabled = GlobalOptions.GetOption(LspOptionsStorage.LspEditorFeatureFlag); - // If the preview feature flag to turn on the LSP editor in local scenarios is on, advertise no capabilities for this Live Share - // LSP server as LSP requests will be serviced by the AlwaysActiveInProcLanguageClient in both local and remote scenarios. - if (isLspEditorEnabled) + // If the preview feature flag to turn on the LSP editor in local scenarios is on, advertise no capabilities for this Live Share + // LSP server as LSP requests will be serviced by the AlwaysActiveInProcLanguageClient in both local and remote scenarios. + if (isLspEditorEnabled) + { + return new VSServerCapabilities { - return new VSServerCapabilities + TextDocumentSync = new TextDocumentSyncOptions { - TextDocumentSync = new TextDocumentSyncOptions - { - OpenClose = false, - Change = TextDocumentSyncKind.None, - } - }; - } - - var defaultCapabilities = _experimentalCapabilitiesProvider.GetCapabilities(clientCapabilities); - - // If the LSP semantic tokens feature flag is enabled, advertise no semantic tokens capabilities for this Live Share - // LSP server as LSP semantic tokens requests will be serviced by the AlwaysActiveInProcLanguageClient in both local and - // remote scenarios. - var isLspSemanticTokenEnabled = GlobalOptions.GetOption(LspOptionsStorage.LspSemanticTokensFeatureFlag); - if (isLspSemanticTokenEnabled) - { - defaultCapabilities.SemanticTokensOptions = null; - } + OpenClose = false, + Change = TextDocumentSyncKind.None, + } + }; + } - // When the lsp pull diagnostics feature flag is enabled we do not advertise pull diagnostics capabilities from here - // as the AlwaysActivateInProcLanguageClient will provide pull diagnostics both locally and remote. - var isPullDiagnosticsEnabled = GlobalOptions.IsLspPullDiagnostics(); - if (!isPullDiagnosticsEnabled) - { - // Pull diagnostics isn't enabled, let the live share server provide pull diagnostics. - ((VSInternalServerCapabilities)defaultCapabilities).SupportsDiagnosticRequests = true; - } + var defaultCapabilities = _experimentalCapabilitiesProvider.GetCapabilities(clientCapabilities); - return defaultCapabilities; + // If the LSP semantic tokens feature flag is enabled, advertise no semantic tokens capabilities for this Live Share + // LSP server as LSP semantic tokens requests will be serviced by the AlwaysActiveInProcLanguageClient in both local and + // remote scenarios. + var isLspSemanticTokenEnabled = GlobalOptions.GetOption(LspOptionsStorage.LspSemanticTokensFeatureFlag); + if (isLspSemanticTokenEnabled) + { + defaultCapabilities.SemanticTokensOptions = null; } - /// - /// Failures are catastrophic as liveshare guests will not have language features without this server. - /// - public override bool ShowNotificationOnInitializeFailed => true; + // When the lsp pull diagnostics feature flag is enabled we do not advertise pull diagnostics capabilities from here + // as the AlwaysActivateInProcLanguageClient will provide pull diagnostics both locally and remote. + var isPullDiagnosticsEnabled = GlobalOptions.IsLspPullDiagnostics(); + if (!isPullDiagnosticsEnabled) + { + // Pull diagnostics isn't enabled, let the live share server provide pull diagnostics. + ((VSInternalServerCapabilities)defaultCapabilities).SupportsDiagnosticRequests = true; + } - public override WellKnownLspServerKinds ServerKind => WellKnownLspServerKinds.LiveShareLspServer; + return defaultCapabilities; } + + /// + /// Failures are catastrophic as liveshare guests will not have language features without this server. + /// + public override bool ShowNotificationOnInitializeFailed => true; + + public override WellKnownLspServerKinds ServerKind => WellKnownLspServerKinds.LiveShareLspServer; } diff --git a/src/EditorFeatures/Core/LanguageServer/RazorInProcLanguageClient.cs b/src/EditorFeatures/Core/LanguageServer/RazorInProcLanguageClient.cs index d89534b313e24..daffb59e72ff8 100644 --- a/src/EditorFeatures/Core/LanguageServer/RazorInProcLanguageClient.cs +++ b/src/EditorFeatures/Core/LanguageServer/RazorInProcLanguageClient.cs @@ -19,86 +19,85 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.LanguageServer.Protocol; -namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient -{ - /// - /// Defines the LSP server for Razor C#. This is separate so that we can - /// activate this outside of a liveshare session and publish diagnostics - /// only for razor cs files. - /// TODO - This can be removed once C# is using LSP for diagnostics. - /// https://github.com/dotnet/roslyn/issues/42630 - /// - /// - /// This specifies RunOnHost because in LiveShare we don't want this to activate on the guest instance - /// because LiveShare drops the ClientName when it mirrors guest clients, so this client ends up being - /// activated solely by its content type, which means it receives requests for normal .cs and .vb files - /// even for non-razor projects, which then of course fails because it gets text sync info for documents - /// it doesn't know about. - /// - [ContentType(ContentTypeNames.CSharpContentType)] - [ClientName(ClientName)] - [RunOnContext(RunningContext.RunOnHost)] - [Export(typeof(ILanguageClient))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class RazorInProcLanguageClient( - CSharpVisualBasicLspServiceProvider lspServiceProvider, - IGlobalOptionService globalOptions, - ExperimentalCapabilitiesProvider experimentalCapabilitiesProvider, - IThreadingContext threadingContext, - ILspServiceLoggerFactory lspLoggerFactory, - ExportProvider exportProvider, - [Import(AllowDefault = true)] AbstractLanguageClientMiddleLayer middleLayer) : AbstractInProcLanguageClient(lspServiceProvider, globalOptions, lspLoggerFactory, threadingContext, exportProvider, middleLayer) - { - public const string ClientName = ProtocolConstants.RazorCSharp; +namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient; - private readonly ExperimentalCapabilitiesProvider _experimentalCapabilitiesProvider = experimentalCapabilitiesProvider; +/// +/// Defines the LSP server for Razor C#. This is separate so that we can +/// activate this outside of a liveshare session and publish diagnostics +/// only for razor cs files. +/// TODO - This can be removed once C# is using LSP for diagnostics. +/// https://github.com/dotnet/roslyn/issues/42630 +/// +/// +/// This specifies RunOnHost because in LiveShare we don't want this to activate on the guest instance +/// because LiveShare drops the ClientName when it mirrors guest clients, so this client ends up being +/// activated solely by its content type, which means it receives requests for normal .cs and .vb files +/// even for non-razor projects, which then of course fails because it gets text sync info for documents +/// it doesn't know about. +/// +[ContentType(ContentTypeNames.CSharpContentType)] +[ClientName(ClientName)] +[RunOnContext(RunningContext.RunOnHost)] +[Export(typeof(ILanguageClient))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class RazorInProcLanguageClient( + CSharpVisualBasicLspServiceProvider lspServiceProvider, + IGlobalOptionService globalOptions, + ExperimentalCapabilitiesProvider experimentalCapabilitiesProvider, + IThreadingContext threadingContext, + ILspServiceLoggerFactory lspLoggerFactory, + ExportProvider exportProvider, + [Import(AllowDefault = true)] AbstractLanguageClientMiddleLayer middleLayer) : AbstractInProcLanguageClient(lspServiceProvider, globalOptions, lspLoggerFactory, threadingContext, exportProvider, middleLayer) +{ + public const string ClientName = ProtocolConstants.RazorCSharp; - protected override ImmutableArray SupportedLanguages => ProtocolConstants.RoslynLspLanguages; + private readonly ExperimentalCapabilitiesProvider _experimentalCapabilitiesProvider = experimentalCapabilitiesProvider; - protected override void Activate_OffUIThread() - { - // Ensure we let the default capabilities provider initialize off the UI thread to avoid - // unnecessary MEF part loading during the GetCapabilities call, which is done on the UI thread - _experimentalCapabilitiesProvider.Initialize(); - } + protected override ImmutableArray SupportedLanguages => ProtocolConstants.RoslynLspLanguages; - public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) - { - var capabilities = _experimentalCapabilitiesProvider.GetCapabilities(clientCapabilities); + protected override void Activate_OffUIThread() + { + // Ensure we let the default capabilities provider initialize off the UI thread to avoid + // unnecessary MEF part loading during the GetCapabilities call, which is done on the UI thread + _experimentalCapabilitiesProvider.Initialize(); + } - // Razor doesn't use workspace symbols, so disable to prevent duplicate results (with LiveshareLanguageClient) in liveshare. - capabilities.WorkspaceSymbolProvider = false; + public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) + { + var capabilities = _experimentalCapabilitiesProvider.GetCapabilities(clientCapabilities); - if (capabilities is VSInternalServerCapabilities vsServerCapabilities) - { - vsServerCapabilities.SupportsDiagnosticRequests = true; - vsServerCapabilities.SpellCheckingProvider = true; - vsServerCapabilities.Experimental ??= new Dictionary(); - vsServerCapabilities.MapCodeProvider = true; - var experimental = (Dictionary)vsServerCapabilities.Experimental; - experimental[SimplifyMethodHandler.SimplifyMethodMethodName] = true; - experimental[FormatNewFileHandler.FormatNewFileMethodName] = true; - experimental[SemanticTokensRangesHandler.SemanticRangesMethodName] = true; + // Razor doesn't use workspace symbols, so disable to prevent duplicate results (with LiveshareLanguageClient) in liveshare. + capabilities.WorkspaceSymbolProvider = false; - var regexExpression = string.Join("|", InlineCompletionsHandler.BuiltInSnippets); - var regex = new Regex(regexExpression, RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); - vsServerCapabilities.InlineCompletionOptions = new VSInternalInlineCompletionOptions - { - Pattern = regex - }; + if (capabilities is VSInternalServerCapabilities vsServerCapabilities) + { + vsServerCapabilities.SupportsDiagnosticRequests = true; + vsServerCapabilities.SpellCheckingProvider = true; + vsServerCapabilities.Experimental ??= new Dictionary(); + vsServerCapabilities.MapCodeProvider = true; + var experimental = (Dictionary)vsServerCapabilities.Experimental; + experimental[SimplifyMethodHandler.SimplifyMethodMethodName] = true; + experimental[FormatNewFileHandler.FormatNewFileMethodName] = true; + experimental[SemanticTokensRangesHandler.SemanticRangesMethodName] = true; - return vsServerCapabilities; - } + var regexExpression = string.Join("|", InlineCompletionsHandler.BuiltInSnippets); + var regex = new Regex(regexExpression, RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); + vsServerCapabilities.InlineCompletionOptions = new VSInternalInlineCompletionOptions + { + Pattern = regex + }; - return capabilities; + return vsServerCapabilities; } - /// - /// If the razor server is activated then any failures are catastrophic as no razor c# features will work. - /// - public override bool ShowNotificationOnInitializeFailed => true; - - public override WellKnownLspServerKinds ServerKind => WellKnownLspServerKinds.RazorLspServer; + return capabilities; } + + /// + /// If the razor server is activated then any failures are catastrophic as no razor c# features will work. + /// + public override bool ShowNotificationOnInitializeFailed => true; + + public override WellKnownLspServerKinds ServerKind => WellKnownLspServerKinds.RazorLspServer; } diff --git a/src/EditorFeatures/Core/LineSeparators/LineSeparatorsOptionsStorage.cs b/src/EditorFeatures/Core/LineSeparators/LineSeparatorsOptionsStorage.cs index 31b1bad51c7e6..f742481d22787 100644 --- a/src/EditorFeatures/Core/LineSeparators/LineSeparatorsOptionsStorage.cs +++ b/src/EditorFeatures/Core/LineSeparators/LineSeparatorsOptionsStorage.cs @@ -4,11 +4,10 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.LineSeparators +namespace Microsoft.CodeAnalysis.LineSeparators; + +internal static class LineSeparatorsOptionsStorage { - internal static class LineSeparatorsOptionsStorage - { - public static readonly PerLanguageOption2 LineSeparator = new("dotnet_display_line_separators", defaultValue: false); + public static readonly PerLanguageOption2 LineSeparator = new("dotnet_display_line_separators", defaultValue: false); - } } diff --git a/src/EditorFeatures/Core/Logging/FunctionIdOptions.cs b/src/EditorFeatures/Core/Logging/FunctionIdOptions.cs index 31005c0e123df..844e3959ed706 100644 --- a/src/EditorFeatures/Core/Logging/FunctionIdOptions.cs +++ b/src/EditorFeatures/Core/Logging/FunctionIdOptions.cs @@ -9,45 +9,44 @@ using Roslyn.Utilities; using System.Collections.Generic; -namespace Microsoft.CodeAnalysis.Internal.Log +namespace Microsoft.CodeAnalysis.Internal.Log; + +internal static class FunctionIdOptions { - internal static class FunctionIdOptions + private static readonly ConcurrentDictionary> s_options = []; + + private static readonly Func> s_optionCreator = CreateOption; + + private static Option2 CreateOption(FunctionId id) + { + var name = id.ToString(); + + // This local storage location can be set via vsregedit. Which is available on any VS Command Prompt. + // + // To enable logging: + // + // vsregedit set local [hive name] HKCU Roslyn\Internal\Performance\FunctionId [function name] dword 1 + // + // To disable logging + // + // vsregedit delete local [hive name] HKCU Roslyn\Internal\Performance\FunctionId [function name] + // + // If you want to set it for the default hive, use "" as the hive name (i.e. an empty argument) + return new("FunctionIdOptions_" + name, defaultValue: false); + } + + private static IEnumerable GetFunctionIds() + => Enum.GetValues(typeof(FunctionId)).Cast(); + + public static IEnumerable GetOptions() + => GetFunctionIds().Select(GetOption); + + public static Option2 GetOption(FunctionId id) + => s_options.GetOrAdd(id, s_optionCreator); + + public static Func CreateFunctionIsEnabledPredicate(IGlobalOptionService globalOptions) { - private static readonly ConcurrentDictionary> s_options = []; - - private static readonly Func> s_optionCreator = CreateOption; - - private static Option2 CreateOption(FunctionId id) - { - var name = id.ToString(); - - // This local storage location can be set via vsregedit. Which is available on any VS Command Prompt. - // - // To enable logging: - // - // vsregedit set local [hive name] HKCU Roslyn\Internal\Performance\FunctionId [function name] dword 1 - // - // To disable logging - // - // vsregedit delete local [hive name] HKCU Roslyn\Internal\Performance\FunctionId [function name] - // - // If you want to set it for the default hive, use "" as the hive name (i.e. an empty argument) - return new("FunctionIdOptions_" + name, defaultValue: false); - } - - private static IEnumerable GetFunctionIds() - => Enum.GetValues(typeof(FunctionId)).Cast(); - - public static IEnumerable GetOptions() - => GetFunctionIds().Select(GetOption); - - public static Option2 GetOption(FunctionId id) - => s_options.GetOrAdd(id, s_optionCreator); - - public static Func CreateFunctionIsEnabledPredicate(IGlobalOptionService globalOptions) - { - var functionIdOptions = GetFunctionIds().ToDictionary(id => id, id => globalOptions.GetOption(GetOption(id))); - return functionId => functionIdOptions[functionId]; - } + var functionIdOptions = GetFunctionIds().ToDictionary(id => id, id => globalOptions.GetOption(GetOption(id))); + return functionId => functionIdOptions[functionId]; } } diff --git a/src/EditorFeatures/Core/ModernCommands/GoToImplementationCommandArgs.cs b/src/EditorFeatures/Core/ModernCommands/GoToImplementationCommandArgs.cs index 111ebf3b65ebd..cccb9c6789a30 100644 --- a/src/EditorFeatures/Core/ModernCommands/GoToImplementationCommandArgs.cs +++ b/src/EditorFeatures/Core/ModernCommands/GoToImplementationCommandArgs.cs @@ -9,13 +9,12 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding; -namespace Microsoft.CodeAnalysis.Editor.Commanding.Commands +namespace Microsoft.CodeAnalysis.Editor.Commanding.Commands; + +/// +/// Arguments for Go To Implementation. +/// +[ExcludeFromCodeCoverage] +internal sealed class GoToImplementationCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : EditorCommandArgs(textView, subjectBuffer) { - /// - /// Arguments for Go To Implementation. - /// - [ExcludeFromCodeCoverage] - internal sealed class GoToImplementationCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : EditorCommandArgs(textView, subjectBuffer) - { - } } diff --git a/src/EditorFeatures/Core/ModernCommands/OrganizeDocumentCommandArgs.cs b/src/EditorFeatures/Core/ModernCommands/OrganizeDocumentCommandArgs.cs index e90d9c5780621..0e2ccf238ef3f 100644 --- a/src/EditorFeatures/Core/ModernCommands/OrganizeDocumentCommandArgs.cs +++ b/src/EditorFeatures/Core/ModernCommands/OrganizeDocumentCommandArgs.cs @@ -9,13 +9,12 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding; -namespace Microsoft.CodeAnalysis.Editor.Commanding.Commands +namespace Microsoft.CodeAnalysis.Editor.Commanding.Commands; + +/// +/// Arguments for the Organize Document command being invoked. +/// +[ExcludeFromCodeCoverage] +internal class OrganizeDocumentCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : EditorCommandArgs(textView, subjectBuffer) { - /// - /// Arguments for the Organize Document command being invoked. - /// - [ExcludeFromCodeCoverage] - internal class OrganizeDocumentCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : EditorCommandArgs(textView, subjectBuffer) - { - } } diff --git a/src/EditorFeatures/Core/ModernCommands/SortAndRemoveUnnecessaryImportsCommandArgs.cs b/src/EditorFeatures/Core/ModernCommands/SortAndRemoveUnnecessaryImportsCommandArgs.cs index fa92cacdc872f..f3faa1694223f 100644 --- a/src/EditorFeatures/Core/ModernCommands/SortAndRemoveUnnecessaryImportsCommandArgs.cs +++ b/src/EditorFeatures/Core/ModernCommands/SortAndRemoveUnnecessaryImportsCommandArgs.cs @@ -9,13 +9,12 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding; -namespace Microsoft.CodeAnalysis.Editor.Commanding.Commands +namespace Microsoft.CodeAnalysis.Editor.Commanding.Commands; + +/// +/// Arguments for the Sort and Remove Unused Usings command being invoked. +/// +[ExcludeFromCodeCoverage] +internal class SortAndRemoveUnnecessaryImportsCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : EditorCommandArgs(textView, subjectBuffer) { - /// - /// Arguments for the Sort and Remove Unused Usings command being invoked. - /// - [ExcludeFromCodeCoverage] - internal class SortAndRemoveUnnecessaryImportsCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : EditorCommandArgs(textView, subjectBuffer) - { - } } diff --git a/src/EditorFeatures/Core/ModernCommands/SortImportsCommandArgs.cs b/src/EditorFeatures/Core/ModernCommands/SortImportsCommandArgs.cs index 7ff0deb29d500..c7ea673010532 100644 --- a/src/EditorFeatures/Core/ModernCommands/SortImportsCommandArgs.cs +++ b/src/EditorFeatures/Core/ModernCommands/SortImportsCommandArgs.cs @@ -9,13 +9,12 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.Commanding; -namespace Microsoft.CodeAnalysis.Editor.Commanding.Commands +namespace Microsoft.CodeAnalysis.Editor.Commanding.Commands; + +/// +/// Arguments for the Sort Imports command being invoked. +/// +[ExcludeFromCodeCoverage] +internal class SortImportsCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : EditorCommandArgs(textView, subjectBuffer) { - /// - /// Arguments for the Sort Imports command being invoked. - /// - [ExcludeFromCodeCoverage] - internal class SortImportsCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : EditorCommandArgs(textView, subjectBuffer) - { - } } diff --git a/src/EditorFeatures/Core/NavigateTo/DefaultNavigateToLinkService.cs b/src/EditorFeatures/Core/NavigateTo/DefaultNavigateToLinkService.cs index eac59d03a0ce8..266e45bfdafa3 100644 --- a/src/EditorFeatures/Core/NavigateTo/DefaultNavigateToLinkService.cs +++ b/src/EditorFeatures/Core/NavigateTo/DefaultNavigateToLinkService.cs @@ -11,19 +11,18 @@ using Microsoft.CodeAnalysis.Host.Mef; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation +namespace Microsoft.CodeAnalysis.Editor.Implementation; + +[ExportWorkspaceService(typeof(INavigateToLinkService), layer: ServiceLayer.Default)] +[Shared] +internal sealed class DefaultNavigateToLinkService : INavigateToLinkService { - [ExportWorkspaceService(typeof(INavigateToLinkService), layer: ServiceLayer.Default)] - [Shared] - internal sealed class DefaultNavigateToLinkService : INavigateToLinkService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DefaultNavigateToLinkService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public DefaultNavigateToLinkService() - { - } - - public Task TryNavigateToLinkAsync(Uri uri, CancellationToken cancellationToken) - => SpecializedTasks.False; } + + public Task TryNavigateToLinkAsync(Uri uri, CancellationToken cancellationToken) + => SpecializedTasks.False; } diff --git a/src/EditorFeatures/Core/NavigateTo/INavigateToLinkService.cs b/src/EditorFeatures/Core/NavigateTo/INavigateToLinkService.cs index 37e588824f367..1b6d388cdbbf3 100644 --- a/src/EditorFeatures/Core/NavigateTo/INavigateToLinkService.cs +++ b/src/EditorFeatures/Core/NavigateTo/INavigateToLinkService.cs @@ -9,10 +9,9 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface INavigateToLinkService : IWorkspaceService { - internal interface INavigateToLinkService : IWorkspaceService - { - Task TryNavigateToLinkAsync(Uri uri, CancellationToken cancellationToken); - } + Task TryNavigateToLinkAsync(Uri uri, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/NavigateTo/NavigateToHelpers.cs b/src/EditorFeatures/Core/NavigateTo/NavigateToHelpers.cs index 8147413c8415d..6d89303695430 100644 --- a/src/EditorFeatures/Core/NavigateTo/NavigateToHelpers.cs +++ b/src/EditorFeatures/Core/NavigateTo/NavigateToHelpers.cs @@ -10,50 +10,49 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.NavigateTo +namespace Microsoft.CodeAnalysis.Editor.NavigateTo; + +internal static class NavigateToHelpers { - internal static class NavigateToHelpers + public static void NavigateTo( + INavigateToSearchResult searchResult, + IThreadingContext threadingContext, + IUIThreadOperationExecutor threadOperationExecutor, + IAsynchronousOperationListener asyncListener) { - public static void NavigateTo( - INavigateToSearchResult searchResult, - IThreadingContext threadingContext, - IUIThreadOperationExecutor threadOperationExecutor, - IAsynchronousOperationListener asyncListener) - { - var token = asyncListener.BeginAsyncOperation(nameof(NavigateTo)); - NavigateToAsync(searchResult, threadingContext, threadOperationExecutor) - .ReportNonFatalErrorAsync() - .CompletesAsyncOperation(token); - } + var token = asyncListener.BeginAsyncOperation(nameof(NavigateTo)); + NavigateToAsync(searchResult, threadingContext, threadOperationExecutor) + .ReportNonFatalErrorAsync() + .CompletesAsyncOperation(token); + } - private static async Task NavigateToAsync( - INavigateToSearchResult searchResult, - IThreadingContext threadingContext, - IUIThreadOperationExecutor threadOperationExecutor) - { - var document = searchResult.NavigableItem.Document; - if (document == null) - return; + private static async Task NavigateToAsync( + INavigateToSearchResult searchResult, + IThreadingContext threadingContext, + IUIThreadOperationExecutor threadOperationExecutor) + { + var document = searchResult.NavigableItem.Document; + if (document == null) + return; - var workspace = document.Workspace; - var navigationService = workspace.Services.GetRequiredService(); + var workspace = document.Workspace; + var navigationService = workspace.Services.GetRequiredService(); - // Document tabs opened by NavigateTo are carefully created as preview or regular tabs - // by them; trying to specifically open them in a particular kind of tab here has no - // effect. - // - // In the case of a stale item, don't require that the span be in bounds of the document - // as it exists right now. - using var context = threadOperationExecutor.BeginExecute( - EditorFeaturesResources.Navigating_to_definition, EditorFeaturesResources.Navigating_to_definition, allowCancellation: true, showProgress: false); - await navigationService.TryNavigateToSpanAsync( - threadingContext, - workspace, - document.Id, - searchResult.NavigableItem.SourceSpan, - NavigationOptions.Default, - allowInvalidSpan: searchResult.NavigableItem.IsStale, - context.UserCancellationToken).ConfigureAwait(false); - } + // Document tabs opened by NavigateTo are carefully created as preview or regular tabs + // by them; trying to specifically open them in a particular kind of tab here has no + // effect. + // + // In the case of a stale item, don't require that the span be in bounds of the document + // as it exists right now. + using var context = threadOperationExecutor.BeginExecute( + EditorFeaturesResources.Navigating_to_definition, EditorFeaturesResources.Navigating_to_definition, allowCancellation: true, showProgress: false); + await navigationService.TryNavigateToSpanAsync( + threadingContext, + workspace, + document.Id, + searchResult.NavigableItem.SourceSpan, + NavigationOptions.Default, + allowInvalidSpan: searchResult.NavigableItem.IsStale, + context.UserCancellationToken).ConfigureAwait(false); } } diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index b13c873071e41..6f5eb9015334d 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -13,157 +13,156 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Navigation +namespace Microsoft.CodeAnalysis.Navigation; + +internal abstract class AbstractDefinitionLocationService : IDefinitionLocationService { - internal abstract class AbstractDefinitionLocationService : IDefinitionLocationService - { - private readonly IThreadingContext _threadingContext; - private readonly IStreamingFindUsagesPresenter _streamingPresenter; + private readonly IThreadingContext _threadingContext; + private readonly IStreamingFindUsagesPresenter _streamingPresenter; - protected AbstractDefinitionLocationService( - IThreadingContext threadingContext, - IStreamingFindUsagesPresenter streamingPresenter) - { - _threadingContext = threadingContext; - _streamingPresenter = streamingPresenter; - } + protected AbstractDefinitionLocationService( + IThreadingContext threadingContext, + IStreamingFindUsagesPresenter streamingPresenter) + { + _threadingContext = threadingContext; + _streamingPresenter = streamingPresenter; + } - private static Task GetNavigableLocationAsync( - Document document, int position, CancellationToken cancellationToken) - { - var solution = document.Project.Solution; - var workspace = solution.Workspace; - var service = workspace.Services.GetRequiredService(); + private static Task GetNavigableLocationAsync( + Document document, int position, CancellationToken cancellationToken) + { + var solution = document.Project.Solution; + var workspace = solution.Workspace; + var service = workspace.Services.GetRequiredService(); - return service.GetLocationForPositionAsync( - workspace, document.Id, position, virtualSpace: 0, cancellationToken); - } + return service.GetLocationForPositionAsync( + workspace, document.Id, position, virtualSpace: 0, cancellationToken); + } - public async Task GetDefinitionLocationAsync( - Document document, int position, CancellationToken cancellationToken) + public async Task GetDefinitionLocationAsync( + Document document, int position, CancellationToken cancellationToken) + { + var symbolService = document.GetRequiredLanguageService(); + var (controlFlowTarget, controlFlowSpan) = await symbolService.GetTargetIfControlFlowAsync( + document, position, cancellationToken).ConfigureAwait(false); + if (controlFlowTarget != null) { - var symbolService = document.GetRequiredLanguageService(); - var (controlFlowTarget, controlFlowSpan) = await symbolService.GetTargetIfControlFlowAsync( - document, position, cancellationToken).ConfigureAwait(false); - if (controlFlowTarget != null) - { - var location = await GetNavigableLocationAsync( - document, controlFlowTarget.Value, cancellationToken).ConfigureAwait(false); - return location is null ? null : new DefinitionLocation(location, new DocumentSpan(document, controlFlowSpan)); - } - else - { - // Try to compute the referenced symbol and attempt to go to definition for the symbol. - var (symbol, project, span) = await symbolService.GetSymbolProjectAndBoundSpanAsync( - document, position, cancellationToken).ConfigureAwait(false); - if (symbol is null) - return null; - - // if the symbol only has a single source location, and we're already on it, - // try to see if there's a better symbol we could navigate to. - var remappedLocation = await GetAlternativeLocationIfAlreadyOnDefinitionAsync( - project, position, symbol, originalDocument: document, cancellationToken).ConfigureAwait(false); - if (remappedLocation != null) - return new DefinitionLocation(remappedLocation, new DocumentSpan(document, span)); - - var isThirdPartyNavigationAllowed = await IsThirdPartyNavigationAllowedAsync( - symbol, position, document, cancellationToken).ConfigureAwait(false); - - var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync( - symbol, - project.Solution, - _threadingContext, - _streamingPresenter, - thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed, - cancellationToken: cancellationToken).ConfigureAwait(false); - if (location is null) - return null; - - return new DefinitionLocation(location, new DocumentSpan(document, span)); - } + var location = await GetNavigableLocationAsync( + document, controlFlowTarget.Value, cancellationToken).ConfigureAwait(false); + return location is null ? null : new DefinitionLocation(location, new DocumentSpan(document, controlFlowSpan)); } - - /// - /// Attempts to find a better definition for the symbol, if the user is already on the definition of it. - /// - /// The project context to use for finding symbols - /// The document the user is navigating from. This may not be part of the project supplied. - private async Task GetAlternativeLocationIfAlreadyOnDefinitionAsync( - Project project, int position, ISymbol symbol, Document originalDocument, CancellationToken cancellationToken) + else { - var solution = project.Solution; - - var sourceLocations = symbol.Locations.WhereAsArray(loc => loc.IsInSource); - if (sourceLocations.Length != 1) - return null; - - var definitionLocation = sourceLocations[0]; - if (!definitionLocation.SourceSpan.IntersectsWith(position)) + // Try to compute the referenced symbol and attempt to go to definition for the symbol. + var (symbol, project, span) = await symbolService.GetSymbolProjectAndBoundSpanAsync( + document, position, cancellationToken).ConfigureAwait(false); + if (symbol is null) return null; - var definitionTree = definitionLocation.SourceTree; - var definitionDocument = solution.GetDocument(definitionTree); - if (definitionDocument != originalDocument) + // if the symbol only has a single source location, and we're already on it, + // try to see if there's a better symbol we could navigate to. + var remappedLocation = await GetAlternativeLocationIfAlreadyOnDefinitionAsync( + project, position, symbol, originalDocument: document, cancellationToken).ConfigureAwait(false); + if (remappedLocation != null) + return new DefinitionLocation(remappedLocation, new DocumentSpan(document, span)); + + var isThirdPartyNavigationAllowed = await IsThirdPartyNavigationAllowedAsync( + symbol, position, document, cancellationToken).ConfigureAwait(false); + + var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync( + symbol, + project.Solution, + _threadingContext, + _streamingPresenter, + thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed, + cancellationToken: cancellationToken).ConfigureAwait(false); + if (location is null) return null; - // Ok, we were already on the definition. Look for better symbols we could show results - // for instead. For now, just see if we're on an interface member impl. If so, we can - // instead navigate to the actual interface member. - // - // In the future we can expand this with other mappings if appropriate. - var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations(); - if (interfaceImpls.Length == 0) - return null; + return new DefinitionLocation(location, new DocumentSpan(document, span)); + } + } - var title = string.Format(EditorFeaturesResources._0_implemented_members, - FindUsagesHelpers.GetDisplayName(symbol)); + /// + /// Attempts to find a better definition for the symbol, if the user is already on the definition of it. + /// + /// The project context to use for finding symbols + /// The document the user is navigating from. This may not be part of the project supplied. + private async Task GetAlternativeLocationIfAlreadyOnDefinitionAsync( + Project project, int position, ISymbol symbol, Document originalDocument, CancellationToken cancellationToken) + { + var solution = project.Solution; + + var sourceLocations = symbol.Locations.WhereAsArray(loc => loc.IsInSource); + if (sourceLocations.Length != 1) + return null; + + var definitionLocation = sourceLocations[0]; + if (!definitionLocation.SourceSpan.IntersectsWith(position)) + return null; + + var definitionTree = definitionLocation.SourceTree; + var definitionDocument = solution.GetDocument(definitionTree); + if (definitionDocument != originalDocument) + return null; + + // Ok, we were already on the definition. Look for better symbols we could show results + // for instead. For now, just see if we're on an interface member impl. If so, we can + // instead navigate to the actual interface member. + // + // In the future we can expand this with other mappings if appropriate. + var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations(); + if (interfaceImpls.Length == 0) + return null; + + var title = string.Format(EditorFeaturesResources._0_implemented_members, + FindUsagesHelpers.GetDisplayName(symbol)); + + using var _ = ArrayBuilder.GetInstance(out var builder); + foreach (var impl in interfaceImpls) + { + builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync( + impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false)); + } - using var _ = ArrayBuilder.GetInstance(out var builder); - foreach (var impl in interfaceImpls) - { - builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync( - impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false)); - } + var definitions = builder.ToImmutable(); - var definitions = builder.ToImmutable(); + return await _streamingPresenter.GetStreamingLocationAsync( + _threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false); + } - return await _streamingPresenter.GetStreamingLocationAsync( - _threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false); - } + private static async Task IsThirdPartyNavigationAllowedAsync( + ISymbol symbolToNavigateTo, int caretPosition, Document document, CancellationToken cancellationToken) + { + var syntaxRoot = document.GetRequiredSyntaxRootSynchronously(cancellationToken); + var syntaxFactsService = document.GetRequiredLanguageService(); + var containingTypeDeclaration = syntaxFactsService.GetContainingTypeDeclaration(syntaxRoot, caretPosition); - private static async Task IsThirdPartyNavigationAllowedAsync( - ISymbol symbolToNavigateTo, int caretPosition, Document document, CancellationToken cancellationToken) + if (containingTypeDeclaration != null) { - var syntaxRoot = document.GetRequiredSyntaxRootSynchronously(cancellationToken); - var syntaxFactsService = document.GetRequiredLanguageService(); - var containingTypeDeclaration = syntaxFactsService.GetContainingTypeDeclaration(syntaxRoot, caretPosition); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + Debug.Assert(semanticModel != null); - if (containingTypeDeclaration != null) - { - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - Debug.Assert(semanticModel != null); + // Allow third parties to navigate to all symbols except types/constructors + // if we are navigating from the corresponding type. - // Allow third parties to navigate to all symbols except types/constructors - // if we are navigating from the corresponding type. + if (semanticModel.GetDeclaredSymbol(containingTypeDeclaration, cancellationToken) is ITypeSymbol containingTypeSymbol && + (symbolToNavigateTo is ITypeSymbol || symbolToNavigateTo.IsConstructor())) + { + var candidateTypeSymbol = symbolToNavigateTo is ITypeSymbol + ? symbolToNavigateTo + : symbolToNavigateTo.ContainingType; - if (semanticModel.GetDeclaredSymbol(containingTypeDeclaration, cancellationToken) is ITypeSymbol containingTypeSymbol && - (symbolToNavigateTo is ITypeSymbol || symbolToNavigateTo.IsConstructor())) + if (Equals(containingTypeSymbol, candidateTypeSymbol)) { - var candidateTypeSymbol = symbolToNavigateTo is ITypeSymbol - ? symbolToNavigateTo - : symbolToNavigateTo.ContainingType; - - if (Equals(containingTypeSymbol, candidateTypeSymbol)) - { - // We are navigating from the same type, so don't allow third parties to perform the navigation. - // This ensures that if we navigate to a class from within that class, we'll stay in the same file - // rather than navigate to, say, XAML. - return false; - } + // We are navigating from the same type, so don't allow third parties to perform the navigation. + // This ensures that if we navigate to a class from within that class, we'll stay in the same file + // rather than navigate to, say, XAML. + return false; } } - - return true; } + + return true; } } diff --git a/src/EditorFeatures/Core/Navigation/IDocumentNavigationServiceExtensions.cs b/src/EditorFeatures/Core/Navigation/IDocumentNavigationServiceExtensions.cs index f4bfbfec622dd..4921763e60213 100644 --- a/src/EditorFeatures/Core/Navigation/IDocumentNavigationServiceExtensions.cs +++ b/src/EditorFeatures/Core/Navigation/IDocumentNavigationServiceExtensions.cs @@ -18,7 +18,7 @@ public static async Task TryNavigateToAsync( if (location == null) return false; - // This switch is currently unnecessary. Howevver, it helps support a future where location.NavigateTo becomes + // This switch is currently unnecessary. However, it helps support a future where location.NavigateTo becomes // async and must be on the UI thread. await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); return await location.NavigateToAsync(options, cancellationToken).ConfigureAwait(false); diff --git a/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs b/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs index c252dae794230..abce7a9ad1c88 100644 --- a/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs +++ b/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs @@ -21,288 +21,285 @@ using Roslyn.Utilities; using IUIThreadOperationExecutor = Microsoft.VisualStudio.Utilities.IUIThreadOperationExecutor; -namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigationBar +namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigationBar; + +/// +/// The controller for navigation bars. +/// +/// +/// The threading model for this class is simple: all non-static members are affinitized to the +/// UI thread. +/// +internal partial class NavigationBarController : IDisposable { + private readonly IThreadingContext _threadingContext; + private readonly INavigationBarPresenter _presenter; + private readonly ITextBuffer _subjectBuffer; + private readonly ITextBufferVisibilityTracker? _visibilityTracker; + private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; + private readonly IAsynchronousOperationListener _asyncListener; + + private bool _disconnected = false; + + /// + /// The last full information we have presented. If we end up wanting to present the same thing again, we can + /// just skip doing that as the UI will already know about this. + /// + private (ImmutableArray projectItems, NavigationBarProjectItem? selectedProjectItem, NavigationBarModel? model, NavigationBarSelectedTypeAndMember selectedInfo) _lastPresentedInfo; + + /// + /// Source of events that should cause us to update the nav bar model with new information. + /// + private readonly ITaggerEventSource _eventSource; + + /// + /// Callback to us when the visibility of our changes. + /// + private readonly Action _onVisibilityChanged; + + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + /// + /// Queue to batch up work to do to compute the current model. Used so we can batch up a lot of events and only + /// compute the model once for every batch. The bool type parameter isn't used, but is provided as this + /// type is generic. + /// + private readonly AsyncBatchingWorkQueue _computeModelQueue; + /// - /// The controller for navigation bars. + /// Queue to batch up work to do to determine the selected item. Used so we can batch up a lot of events and + /// only compute the selected item once for every batch. /// - /// - /// The threading model for this class is simple: all non-static members are affinitized to the - /// UI thread. - /// - internal partial class NavigationBarController : IDisposable + private readonly AsyncBatchingWorkQueue _selectItemQueue; + + /// + /// Whether or not the navbar is paused. We pause updates when documents become non-visible. See . + /// + private bool _paused = false; + + public NavigationBarController( + IThreadingContext threadingContext, + INavigationBarPresenter presenter, + ITextBuffer subjectBuffer, + ITextBufferVisibilityTracker? visibilityTracker, + IUIThreadOperationExecutor uiThreadOperationExecutor, + IAsynchronousOperationListener asyncListener) { - private readonly IThreadingContext _threadingContext; - private readonly INavigationBarPresenter _presenter; - private readonly ITextBuffer _subjectBuffer; - private readonly ITextBufferVisibilityTracker? _visibilityTracker; - private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; - private readonly IAsynchronousOperationListener _asyncListener; - - private bool _disconnected = false; - - /// - /// The last full information we have presented. If we end up wanting to present the same thing again, we can - /// just skip doing that as the UI will already know about this. - /// - private (ImmutableArray projectItems, NavigationBarProjectItem? selectedProjectItem, NavigationBarModel? model, NavigationBarSelectedTypeAndMember selectedInfo) _lastPresentedInfo; - - /// - /// Source of events that should cause us to update the nav bar model with new information. - /// - private readonly ITaggerEventSource _eventSource; - - /// - /// Callback to us when the visibility of our changes. - /// - private readonly Action _onVisibilityChanged; - - private readonly CancellationTokenSource _cancellationTokenSource = new(); - - /// - /// Queue to batch up work to do to compute the current model. Used so we can batch up a lot of events and only - /// compute the model once for every batch. The bool type parameter isn't used, but is provided as this - /// type is generic. - /// - private readonly AsyncBatchingWorkQueue _computeModelQueue; - - /// - /// Queue to batch up work to do to determine the selected item. Used so we can batch up a lot of events and - /// only compute the selected item once for every batch. - /// - private readonly AsyncBatchingWorkQueue _selectItemQueue; - - /// - /// Whether or not the navbar is paused. We pause updates when documents become non-visible. See . - /// - private bool _paused = false; - - public NavigationBarController( - IThreadingContext threadingContext, - INavigationBarPresenter presenter, - ITextBuffer subjectBuffer, - ITextBufferVisibilityTracker? visibilityTracker, - IUIThreadOperationExecutor uiThreadOperationExecutor, - IAsynchronousOperationListener asyncListener) + _threadingContext = threadingContext; + _presenter = presenter; + _subjectBuffer = subjectBuffer; + _visibilityTracker = visibilityTracker; + _uiThreadOperationExecutor = uiThreadOperationExecutor; + _asyncListener = asyncListener; + + _computeModelQueue = new AsyncBatchingWorkQueue( + DelayTimeSpan.Short, + ComputeModelAndSelectItemAsync, + EqualityComparer.Default, + asyncListener, + _cancellationTokenSource.Token); + + _selectItemQueue = new AsyncBatchingWorkQueue( + DelayTimeSpan.NearImmediate, + SelectItemAsync, + asyncListener, + _cancellationTokenSource.Token); + + presenter.CaretMovedOrActiveViewChanged += OnCaretMovedOrActiveViewChanged; + + presenter.ItemSelected += OnItemSelected; + + // Use 'compilation available' as that may produce different results from the initial 'frozen partial' + // snapshot we use. + _eventSource = TaggerEventSources.Compose( + // Any time an edit happens, recompute as the nav bar items may have changed. + TaggerEventSources.OnTextChanged(subjectBuffer), + // Switching what is the active context may change the nav bar contents. + TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer), + // Many workspace changes may need us to change the items (like options changing, or project renaming). + TaggerEventSources.OnWorkspaceChanged(subjectBuffer, asyncListener), + // Once we hook this buffer up to the workspace, then we can start computing the nav bar items. + TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer)); + _eventSource.Changed += OnEventSourceChanged; + + _onVisibilityChanged = () => { - _threadingContext = threadingContext; - _presenter = presenter; - _subjectBuffer = subjectBuffer; - _visibilityTracker = visibilityTracker; - _uiThreadOperationExecutor = uiThreadOperationExecutor; - _asyncListener = asyncListener; - - _computeModelQueue = new AsyncBatchingWorkQueue( - DelayTimeSpan.Short, - ComputeModelAndSelectItemAsync, - EqualityComparer.Default, - asyncListener, - _cancellationTokenSource.Token); - - _selectItemQueue = new AsyncBatchingWorkQueue( - DelayTimeSpan.NearImmediate, - SelectItemAsync, - asyncListener, - _cancellationTokenSource.Token); - - presenter.CaretMovedOrActiveViewChanged += OnCaretMovedOrActiveViewChanged; - - presenter.ItemSelected += OnItemSelected; - - // Use 'compilation available' as that may produce different results from the initial 'frozen partial' - // snapshot we use. - _eventSource = new CompilationAvailableTaggerEventSource( - subjectBuffer, - asyncListener, - // Any time an edit happens, recompute as the nav bar items may have changed. - TaggerEventSources.OnTextChanged(subjectBuffer), - // Switching what is the active context may change the nav bar contents. - TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer), - // Many workspace changes may need us to change the items (like options changing, or project renaming). - TaggerEventSources.OnWorkspaceChanged(subjectBuffer, asyncListener), - // Once we hook this buffer up to the workspace, then we can start computing the nav bar items. - TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer)); - _eventSource.Changed += OnEventSourceChanged; - - _onVisibilityChanged = () => - { - threadingContext.ThrowIfNotOnUIThread(); + threadingContext.ThrowIfNotOnUIThread(); - // any time visibility changes, resume tagging on all taggers. Any non-visible taggers will pause - // themselves immediately afterwards. - Resume(); - }; + // any time visibility changes, resume tagging on all taggers. Any non-visible taggers will pause + // themselves immediately afterwards. + Resume(); + }; - // Register to hear about visibility changes so we can pause/resume this tagger. - _visibilityTracker?.RegisterForVisibilityChanges(subjectBuffer, _onVisibilityChanged); + // Register to hear about visibility changes so we can pause/resume this tagger. + _visibilityTracker?.RegisterForVisibilityChanges(subjectBuffer, _onVisibilityChanged); - _eventSource.Connect(); + _eventSource.Connect(); - // Kick off initial work to populate the navbars - StartModelUpdateAndSelectedItemUpdateTasks(); - } + // Kick off initial work to populate the navbars + StartModelUpdateAndSelectedItemUpdateTasks(); + } - void IDisposable.Dispose() - { - _threadingContext.ThrowIfNotOnUIThread(); + void IDisposable.Dispose() + { + _threadingContext.ThrowIfNotOnUIThread(); - _visibilityTracker?.UnregisterForVisibilityChanges(_subjectBuffer, _onVisibilityChanged); + _visibilityTracker?.UnregisterForVisibilityChanges(_subjectBuffer, _onVisibilityChanged); - _presenter.CaretMovedOrActiveViewChanged -= OnCaretMovedOrActiveViewChanged; + _presenter.CaretMovedOrActiveViewChanged -= OnCaretMovedOrActiveViewChanged; - _presenter.ItemSelected -= OnItemSelected; + _presenter.ItemSelected -= OnItemSelected; - _presenter.Disconnect(); + _presenter.Disconnect(); - _eventSource.Changed -= OnEventSourceChanged; - _eventSource.Disconnect(); + _eventSource.Changed -= OnEventSourceChanged; + _eventSource.Disconnect(); - _disconnected = true; + _disconnected = true; - // Cancel off any remaining background work - _cancellationTokenSource.Cancel(); - } + // Cancel off any remaining background work + _cancellationTokenSource.Cancel(); + } - private void Pause() - { - _threadingContext.ThrowIfNotOnUIThread(); - _paused = true; - _eventSource.Pause(); - } + private void Pause() + { + _threadingContext.ThrowIfNotOnUIThread(); + _paused = true; + _eventSource.Pause(); + } - private void Resume() + private void Resume() + { + _threadingContext.ThrowIfNotOnUIThread(); + // if we're not actually paused, no need to do anything. + if (_paused) { - _threadingContext.ThrowIfNotOnUIThread(); - // if we're not actually paused, no need to do anything. - if (_paused) - { - // Set us back to running, and kick off work to compute tags now that we're visible again. - _paused = false; - _eventSource.Resume(); - StartModelUpdateAndSelectedItemUpdateTasks(); - } + // Set us back to running, and kick off work to compute tags now that we're visible again. + _paused = false; + _eventSource.Resume(); + StartModelUpdateAndSelectedItemUpdateTasks(); } + } + + public TestAccessor GetTestAccessor() => new TestAccessor(this); + + private void OnEventSourceChanged(object? sender, TaggerEventArgs e) + { + StartModelUpdateAndSelectedItemUpdateTasks(); + } - public TestAccessor GetTestAccessor() => new TestAccessor(this); + private void StartModelUpdateAndSelectedItemUpdateTasks() + { + // If we disconnected already, just disregard + if (_disconnected) + return; + + // 'true' value is unused. this just signals to the queue that we have work to do. + _computeModelQueue.AddWork(true); + } + + private void OnCaretMovedOrActiveViewChanged(object? sender, EventArgs e) + { + _threadingContext.ThrowIfNotOnUIThread(); + StartSelectedItemUpdateTask(); + } - private void OnEventSourceChanged(object? sender, TaggerEventArgs e) + private void GetProjectItems(out ImmutableArray projectItems, out NavigationBarProjectItem? selectedProjectItem) + { + var documents = _subjectBuffer.CurrentSnapshot.GetRelatedDocumentsWithChanges(); + if (!documents.Any()) { - StartModelUpdateAndSelectedItemUpdateTasks(); + projectItems = []; + selectedProjectItem = null; + return; } - private void StartModelUpdateAndSelectedItemUpdateTasks() - { - // If we disconnected already, just disregard - if (_disconnected) - return; + projectItems = documents.Select(d => + new NavigationBarProjectItem( + d.Project.Name, + d.Project.GetGlyph(), + workspace: d.Project.Solution.Workspace, + documentId: d.Id, + language: d.Project.Language)).OrderBy(projectItem => projectItem.Text).ToImmutableArray(); + + var document = _subjectBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); + selectedProjectItem = document != null + ? projectItems.FirstOrDefault(p => p.Text == document.Project.Name) ?? projectItems.First() + : projectItems.First(); + } - // 'true' value is unused. this just signals to the queue that we have work to do. - _computeModelQueue.AddWork(true); - } + private void OnItemSelected(object? sender, NavigationBarItemSelectedEventArgs e) + { + _threadingContext.ThrowIfNotOnUIThread(); + var token = _asyncListener.BeginAsyncOperation(nameof(OnItemSelected)); + var task = OnItemSelectedAsync(e.Item); + _ = task.CompletesAsyncOperation(token); + } - private void OnCaretMovedOrActiveViewChanged(object? sender, EventArgs e) + private async Task OnItemSelectedAsync(NavigationBarItem item) + { + _threadingContext.ThrowIfNotOnUIThread(); + using var waitContext = _uiThreadOperationExecutor.BeginExecute( + EditorFeaturesResources.Navigation_Bars, + EditorFeaturesResources.Refreshing_navigation_bars, + allowCancellation: true, + showProgress: false); + + try { - _threadingContext.ThrowIfNotOnUIThread(); - StartSelectedItemUpdateTask(); + await ProcessItemSelectionAsync(item, waitContext.UserCancellationToken).ConfigureAwait(false); } - - private void GetProjectItems(out ImmutableArray projectItems, out NavigationBarProjectItem? selectedProjectItem) + catch (OperationCanceledException) { - var documents = _subjectBuffer.CurrentSnapshot.GetRelatedDocumentsWithChanges(); - if (!documents.Any()) - { - projectItems = []; - selectedProjectItem = null; - return; - } - - projectItems = documents.Select(d => - new NavigationBarProjectItem( - d.Project.Name, - d.Project.GetGlyph(), - workspace: d.Project.Solution.Workspace, - documentId: d.Id, - language: d.Project.Language)).OrderBy(projectItem => projectItem.Text).ToImmutableArray(); - - var document = _subjectBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); - selectedProjectItem = document != null - ? projectItems.FirstOrDefault(p => p.Text == document.Project.Name) ?? projectItems.First() - : projectItems.First(); } - - private void OnItemSelected(object? sender, NavigationBarItemSelectedEventArgs e) + catch (Exception e) when (FatalError.ReportAndCatch(e, ErrorSeverity.Critical)) { - _threadingContext.ThrowIfNotOnUIThread(); - var token = _asyncListener.BeginAsyncOperation(nameof(OnItemSelected)); - var task = OnItemSelectedAsync(e.Item); - _ = task.CompletesAsyncOperation(token); } + } - private async Task OnItemSelectedAsync(NavigationBarItem item) + private async Task ProcessItemSelectionAsync(NavigationBarItem item, CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnUIThread(); + + if (item is NavigationBarProjectItem projectItem) { - _threadingContext.ThrowIfNotOnUIThread(); - using var waitContext = _uiThreadOperationExecutor.BeginExecute( - EditorFeaturesResources.Navigation_Bars, - EditorFeaturesResources.Refreshing_navigation_bars, - allowCancellation: true, - showProgress: false); - - try - { - await ProcessItemSelectionAsync(item, waitContext.UserCancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - } - catch (Exception e) when (FatalError.ReportAndCatch(e, ErrorSeverity.Critical)) - { - } + projectItem.SwitchToContext(); } - - private async Task ProcessItemSelectionAsync(NavigationBarItem item, CancellationToken cancellationToken) + else { - _threadingContext.ThrowIfNotOnUIThread(); - - if (item is NavigationBarProjectItem projectItem) + // When navigating, just use the partial semantics workspace. Navigation doesn't need the fully bound + // compilations to be created, and it can save us a lot of costly time building skeleton assemblies. + var textSnapshot = _subjectBuffer.CurrentSnapshot; + var document = textSnapshot.AsText().GetDocumentWithFrozenPartialSemantics(cancellationToken); + if (document != null) { - projectItem.SwitchToContext(); - } - else - { - // When navigating, just use the partial semantics workspace. Navigation doesn't need the fully bound - // compilations to be created, and it can save us a lot of costly time building skeleton assemblies. - var textSnapshot = _subjectBuffer.CurrentSnapshot; - var document = textSnapshot.AsText().GetDocumentWithFrozenPartialSemantics(cancellationToken); - if (document != null) + var navBarService = document.GetRequiredLanguageService(); + var view = _presenter.TryGetCurrentView(); + + // ConfigureAwait(true) as we have to come back to UI thread in order to kick of the refresh task + // below. Note that we only want to refresh if selecting the item had an effect (either navigating + // or generating). If nothing happened to don't want to refresh. This is important as some items + // exist in the type list that are only there to show a set a particular set of items in the member + // list. So selecting such an item should only update the member list, and we do not want a refresh + // to wipe that out. + if (!await navBarService.TryNavigateToItemAsync( + document, item, view, textSnapshot.Version, cancellationToken).ConfigureAwait(true)) { - var navBarService = document.GetRequiredLanguageService(); - var view = _presenter.TryGetCurrentView(); - - // ConfigureAwait(true) as we have to come back to UI thread in order to kick of the refresh task - // below. Note that we only want to refresh if selecting the item had an effect (either navigating - // or generating). If nothing happened to don't want to refresh. This is important as some items - // exist in the type list that are only there to show a set a particular set of items in the member - // list. So selecting such an item should only update the member list, and we do not want a refresh - // to wipe that out. - if (!await navBarService.TryNavigateToItemAsync( - document, item, view, textSnapshot.Version, cancellationToken).ConfigureAwait(true)) - { - return; - } + return; } } - - // Now that the edit has been done, refresh to make sure everything is up-to-date. - StartModelUpdateAndSelectedItemUpdateTasks(); } - public readonly struct TestAccessor(NavigationBarController navigationBarController) - { - private readonly NavigationBarController _navigationBarController = navigationBarController; + // Now that the edit has been done, refresh to make sure everything is up-to-date. + StartModelUpdateAndSelectedItemUpdateTasks(); + } - public Task GetModelAsync() - => _navigationBarController._computeModelQueue.WaitUntilCurrentBatchCompletesAsync(); - } + public readonly struct TestAccessor(NavigationBarController navigationBarController) + { + private readonly NavigationBarController _navigationBarController = navigationBarController; + + public Task GetModelAsync() + => _navigationBarController._computeModelQueue.WaitUntilCurrentBatchCompletesAsync(); } } diff --git a/src/EditorFeatures/Core/NavigationBar/NavigationBarControllerFactoryService.cs b/src/EditorFeatures/Core/NavigationBar/NavigationBarControllerFactoryService.cs index c0ad49cc3db00..967b6d35490d5 100644 --- a/src/EditorFeatures/Core/NavigationBar/NavigationBarControllerFactoryService.cs +++ b/src/EditorFeatures/Core/NavigationBar/NavigationBarControllerFactoryService.cs @@ -11,31 +11,30 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigationBar +namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigationBar; + +[Export(typeof(INavigationBarControllerFactoryService))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class NavigationBarControllerFactoryService( + IThreadingContext threadingContext, + [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, + IUIThreadOperationExecutor uIThreadOperationExecutor, + IAsynchronousOperationListenerProvider listenerProvider) : INavigationBarControllerFactoryService { - [Export(typeof(INavigationBarControllerFactoryService))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class NavigationBarControllerFactoryService( - IThreadingContext threadingContext, - [Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, - IUIThreadOperationExecutor uIThreadOperationExecutor, - IAsynchronousOperationListenerProvider listenerProvider) : INavigationBarControllerFactoryService - { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly ITextBufferVisibilityTracker? _visibilityTracker = visibilityTracker; - private readonly IUIThreadOperationExecutor _uIThreadOperationExecutor = uIThreadOperationExecutor; - private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.NavigationBar); + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly ITextBufferVisibilityTracker? _visibilityTracker = visibilityTracker; + private readonly IUIThreadOperationExecutor _uIThreadOperationExecutor = uIThreadOperationExecutor; + private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.NavigationBar); - public IDisposable CreateController(INavigationBarPresenter presenter, ITextBuffer textBuffer) - { - return new NavigationBarController( - _threadingContext, - presenter, - textBuffer, - _visibilityTracker, - _uIThreadOperationExecutor, - _asyncListener); - } + public IDisposable CreateController(INavigationBarPresenter presenter, ITextBuffer textBuffer) + { + return new NavigationBarController( + _threadingContext, + presenter, + textBuffer, + _visibilityTracker, + _uIThreadOperationExecutor, + _asyncListener); } } diff --git a/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs b/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs index 12cb203585d27..c68bd25868ead 100644 --- a/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs +++ b/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs @@ -15,227 +15,226 @@ using Microsoft.CodeAnalysis.Workspaces; using Microsoft.VisualStudio.Threading; -namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigationBar +namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigationBar; + +internal partial class NavigationBarController { - internal partial class NavigationBarController + /// + /// Starts a new task to compute the model based on the current text. + /// + private async ValueTask ComputeModelAndSelectItemAsync(ImmutableSegmentedList unused, CancellationToken cancellationToken) { - /// - /// Starts a new task to compute the model based on the current text. - /// - private async ValueTask ComputeModelAndSelectItemAsync(ImmutableSegmentedList unused, CancellationToken cancellationToken) - { - // Jump back to the UI thread to determine what snapshot the user is processing. - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); + // Jump back to the UI thread to determine what snapshot the user is processing. + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); - // Cancellation exceptions are ignored in AsyncBatchingWorkQueue, so return without throwing if cancellation - // occurred while switching to the main thread. - if (cancellationToken.IsCancellationRequested) - return null; + // Cancellation exceptions are ignored in AsyncBatchingWorkQueue, so return without throwing if cancellation + // occurred while switching to the main thread. + if (cancellationToken.IsCancellationRequested) + return null; - var textSnapshot = _subjectBuffer.CurrentSnapshot; + var textSnapshot = _subjectBuffer.CurrentSnapshot; - // Ensure we switch to the threadpool before calling GetDocumentWithFrozenPartialSemantics. It ensures - // that any IO that performs is not potentially on the UI thread. - await TaskScheduler.Default; + // Ensure we switch to the threadpool before calling GetDocumentWithFrozenPartialSemantics. It ensures + // that any IO that performs is not potentially on the UI thread. + await TaskScheduler.Default; - var model = await ComputeModelAsync().ConfigureAwait(false); + var model = await ComputeModelAsync().ConfigureAwait(false); - // Now, enqueue work to select the right item in this new model. - if (model != null) - StartSelectedItemUpdateTask(); + // Now, enqueue work to select the right item in this new model. + if (model != null) + StartSelectedItemUpdateTask(); + + return model; + + async Task ComputeModelAsync() + { + // When computing items just get the partial semantics workspace. This will ensure we can get data for this + // file, and hopefully have enough loaded to get data for other files in the case of partial types. In the + // event the other files aren't available, then partial-type information won't be correct. That's ok though + // as this is just something that happens during solution load and will pass once that is over. By using + // partial semantics, we can ensure we don't spend an inordinate amount of time computing and using full + // compilation data (like skeleton assemblies). + var forceFrozenPartialSemanticsForCrossProcessOperations = true; + + var workspace = textSnapshot.TextBuffer.GetWorkspace(); + if (workspace is null) + return null; - return model; + var document = textSnapshot.AsText().GetDocumentWithFrozenPartialSemantics(cancellationToken); + if (document == null) + return null; + + var itemService = document.GetLanguageService(); + if (itemService == null) + return null; + + // If these are navbars for a file that isn't even visible, then avoid doing any unnecessary computation + // work until far in the future (or if visibility changes). This ensures our non-visible docs do settle + // once enough time has passed, while greatly reducing their impact on the system. + // + // Use NoThrow as this is a high source of cancellation exceptions. This avoids the exception and instead + // bails gracefully by checking below. + await _visibilityTracker.DelayWhileNonVisibleAsync( + _threadingContext, _asyncListener, _subjectBuffer, DelayTimeSpan.NonFocus, cancellationToken).NoThrowAwaitable(false); + + if (cancellationToken.IsCancellationRequested) + return null; - async Task ComputeModelAsync() + using (Logger.LogBlock(FunctionId.NavigationBar_ComputeModelAsync, cancellationToken)) { - // When computing items just get the partial semantics workspace. This will ensure we can get data for this - // file, and hopefully have enough loaded to get data for other files in the case of partial types. In the - // event the other files aren't available, then partial-type information won't be correct. That's ok though - // as this is just something that happens during solution load and will pass once that is over. By using - // partial semantics, we can ensure we don't spend an inordinate amount of time computing and using full - // compilation data (like skeleton assemblies). - var forceFrozenPartialSemanticsForCrossProcessOperations = true; - - var workspace = textSnapshot.TextBuffer.GetWorkspace(); - if (workspace is null) - return null; - - var document = textSnapshot.AsText().GetDocumentWithFrozenPartialSemantics(cancellationToken); - if (document == null) - return null; - - var itemService = document.GetLanguageService(); - if (itemService == null) - return null; - - // If these are navbars for a file that isn't even visible, then avoid doing any unnecessary computation - // work until far in the future (or if visibility changes). This ensures our non-visible docs do settle - // once enough time has passed, while greatly reducing their impact on the system. - // - // Use NoThrow as this is a high source of cancellation exceptions. This avoids the exception and instead - // bails gracefully by checking below. - await _visibilityTracker.DelayWhileNonVisibleAsync( - _threadingContext, _asyncListener, _subjectBuffer, DelayTimeSpan.NonFocus, cancellationToken).NoThrowAwaitable(false); - - if (cancellationToken.IsCancellationRequested) - return null; - - using (Logger.LogBlock(FunctionId.NavigationBar_ComputeModelAsync, cancellationToken)) - { - var items = await itemService.GetItemsAsync( - document, - workspace.CanApplyChange(ApplyChangesKind.ChangeDocument), - forceFrozenPartialSemanticsForCrossProcessOperations, - textSnapshot.Version, - cancellationToken).ConfigureAwait(false); - return new NavigationBarModel(itemService, items); - } + var items = await itemService.GetItemsAsync( + document, + workspace.CanApplyChange(ApplyChangesKind.ChangeDocument), + forceFrozenPartialSemanticsForCrossProcessOperations, + textSnapshot.Version, + cancellationToken).ConfigureAwait(false); + return new NavigationBarModel(itemService, items); } } + } - /// - /// Starts a new task to compute what item should be selected. - /// - private void StartSelectedItemUpdateTask() - { - // 'true' value is unused. this just signals to the queue that we have work to do. - _selectItemQueue.AddWork(); - } + /// + /// Starts a new task to compute what item should be selected. + /// + private void StartSelectedItemUpdateTask() + { + // 'true' value is unused. this just signals to the queue that we have work to do. + _selectItemQueue.AddWork(); + } - private async ValueTask SelectItemAsync(CancellationToken cancellationToken) - { - // Switch to the UI so we can determine where the user is and determine the state the last time we updated - // the UI. - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); + private async ValueTask SelectItemAsync(CancellationToken cancellationToken) + { + // Switch to the UI so we can determine where the user is and determine the state the last time we updated + // the UI. + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); - // Cancellation exceptions are ignored in AsyncBatchingWorkQueue, so return without throwing if cancellation - // occurred while switching to the main thread. - if (cancellationToken.IsCancellationRequested) - return; + // Cancellation exceptions are ignored in AsyncBatchingWorkQueue, so return without throwing if cancellation + // occurred while switching to the main thread. + if (cancellationToken.IsCancellationRequested) + return; - await SelectItemWorkerAsync(cancellationToken).ConfigureAwait(true); + await SelectItemWorkerAsync(cancellationToken).ConfigureAwait(true); - // Once we've computed and selected the latest navbar items, pause ourselves if we're no longer visible. - // That way we don't consume any machine resources that the user won't even notice. - if (_visibilityTracker?.IsVisible(_subjectBuffer) is false) - Pause(); - } + // Once we've computed and selected the latest navbar items, pause ourselves if we're no longer visible. + // That way we don't consume any machine resources that the user won't even notice. + if (_visibilityTracker?.IsVisible(_subjectBuffer) is false) + Pause(); + } - private async ValueTask SelectItemWorkerAsync(CancellationToken cancellationToken) - { - _threadingContext.ThrowIfNotOnUIThread(); + private async ValueTask SelectItemWorkerAsync(CancellationToken cancellationToken) + { + _threadingContext.ThrowIfNotOnUIThread(); - var currentView = _presenter.TryGetCurrentView(); - var caretPosition = currentView?.GetCaretPoint(_subjectBuffer); - if (!caretPosition.HasValue) - return; + var currentView = _presenter.TryGetCurrentView(); + var caretPosition = currentView?.GetCaretPoint(_subjectBuffer); + if (!caretPosition.HasValue) + return; - var position = caretPosition.Value.Position; - var lastPresentedInfo = _lastPresentedInfo; + var position = caretPosition.Value.Position; + var lastPresentedInfo = _lastPresentedInfo; - // Jump back to the BG to do any expensive work walking the entire model - await TaskScheduler.Default; + // Jump back to the BG to do any expensive work walking the entire model + await TaskScheduler.Default; - // Ensure the latest model is computed. - var model = await _computeModelQueue.WaitUntilCurrentBatchCompletesAsync().ConfigureAwait(true); + // Ensure the latest model is computed. + var model = await _computeModelQueue.WaitUntilCurrentBatchCompletesAsync().ConfigureAwait(true); - var currentSelectedItem = ComputeSelectedTypeAndMember(model, position, cancellationToken); + var currentSelectedItem = ComputeSelectedTypeAndMember(model, position, cancellationToken); - GetProjectItems(out var projectItems, out var selectedProjectItem); - if (Equals(model, lastPresentedInfo.model) && - Equals(currentSelectedItem, lastPresentedInfo.selectedInfo) && - Equals(selectedProjectItem, lastPresentedInfo.selectedProjectItem) && - projectItems.SequenceEqual(lastPresentedInfo.projectItems)) - { - // Nothing changed, so we can skip presenting these items. - return; - } + GetProjectItems(out var projectItems, out var selectedProjectItem); + if (Equals(model, lastPresentedInfo.model) && + Equals(currentSelectedItem, lastPresentedInfo.selectedInfo) && + Equals(selectedProjectItem, lastPresentedInfo.selectedProjectItem) && + projectItems.SequenceEqual(lastPresentedInfo.projectItems)) + { + // Nothing changed, so we can skip presenting these items. + return; + } - // Finally, switch back to the UI to update our state and UI. - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + // Finally, switch back to the UI to update our state and UI. + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - _presenter.PresentItems( - projectItems, - selectedProjectItem, - model?.Types ?? [], - currentSelectedItem.TypeItem, - currentSelectedItem.MemberItem); + _presenter.PresentItems( + projectItems, + selectedProjectItem, + model?.Types ?? [], + currentSelectedItem.TypeItem, + currentSelectedItem.MemberItem); - _lastPresentedInfo = (projectItems, selectedProjectItem, model, currentSelectedItem); - } + _lastPresentedInfo = (projectItems, selectedProjectItem, model, currentSelectedItem); + } - internal static NavigationBarSelectedTypeAndMember ComputeSelectedTypeAndMember( - NavigationBarModel? model, int caretPosition, CancellationToken cancellationToken) + internal static NavigationBarSelectedTypeAndMember ComputeSelectedTypeAndMember( + NavigationBarModel? model, int caretPosition, CancellationToken cancellationToken) + { + if (model != null) { - if (model != null) + var (item, gray) = GetMatchingItem(model.Types, caretPosition, model.ItemService, cancellationToken); + if (item != null) { - var (item, gray) = GetMatchingItem(model.Types, caretPosition, model.ItemService, cancellationToken); - if (item != null) - { - var rightItem = GetMatchingItem(item.ChildItems, caretPosition, model.ItemService, cancellationToken); - return new NavigationBarSelectedTypeAndMember(item, gray, rightItem.item, rightItem.gray); - } + var rightItem = GetMatchingItem(item.ChildItems, caretPosition, model.ItemService, cancellationToken); + return new NavigationBarSelectedTypeAndMember(item, gray, rightItem.item, rightItem.gray); } - - return NavigationBarSelectedTypeAndMember.Empty; } - /// - /// Finds the item that point is in, or if it's not in any items, gets the first item that's - /// positioned after the cursor. - /// - /// A tuple of the matching item, and if it should be shown grayed. - private static (NavigationBarItem? item, bool gray) GetMatchingItem( - ImmutableArray items, int point, INavigationBarItemService itemsService, CancellationToken cancellationToken) - { - NavigationBarItem? exactItem = null; - var exactItemStart = 0; - NavigationBarItem? nextItem = null; - var nextItemStart = int.MaxValue; + return NavigationBarSelectedTypeAndMember.Empty; + } + + /// + /// Finds the item that point is in, or if it's not in any items, gets the first item that's + /// positioned after the cursor. + /// + /// A tuple of the matching item, and if it should be shown grayed. + private static (NavigationBarItem? item, bool gray) GetMatchingItem( + ImmutableArray items, int point, INavigationBarItemService itemsService, CancellationToken cancellationToken) + { + NavigationBarItem? exactItem = null; + var exactItemStart = 0; + NavigationBarItem? nextItem = null; + var nextItemStart = int.MaxValue; - foreach (var item in items) + foreach (var item in items) + { + foreach (var span in item.Spans) { - foreach (var span in item.Spans) + cancellationToken.ThrowIfCancellationRequested(); + + if (span.Contains(point) || span.End == point) { - cancellationToken.ThrowIfCancellationRequested(); + // This is the item we should show normally. We'll continue looking at other + // items as there might be a nested type that we're actually in. If there + // are multiple items containing the point, choose whichever containing span + // starts later because that will be the most nested item. - if (span.Contains(point) || span.End == point) - { - // This is the item we should show normally. We'll continue looking at other - // items as there might be a nested type that we're actually in. If there - // are multiple items containing the point, choose whichever containing span - // starts later because that will be the most nested item. - - if (exactItem == null || span.Start >= exactItemStart) - { - exactItem = item; - exactItemStart = span.Start; - } - } - else if (span.Start > point && span.Start <= nextItemStart) + if (exactItem == null || span.Start >= exactItemStart) { - nextItem = item; - nextItemStart = span.Start; + exactItem = item; + exactItemStart = span.Start; } } + else if (span.Start > point && span.Start <= nextItemStart) + { + nextItem = item; + nextItemStart = span.Start; + } } + } - if (exactItem != null) + if (exactItem != null) + { + return (exactItem, gray: false); + } + else + { + // The second parameter is if we should show it grayed. We'll be nice and say false + // unless we actually have an item + var itemToGray = nextItem ?? items.LastOrDefault(); + if (itemToGray != null && !itemsService.ShowItemGrayedIfNear(itemToGray)) { - return (exactItem, gray: false); + itemToGray = null; } - else - { - // The second parameter is if we should show it grayed. We'll be nice and say false - // unless we actually have an item - var itemToGray = nextItem ?? items.LastOrDefault(); - if (itemToGray != null && !itemsService.ShowItemGrayedIfNear(itemToGray)) - { - itemToGray = null; - } - return (itemToGray, gray: itemToGray != null); - } + return (itemToGray, gray: itemToGray != null); } } } diff --git a/src/EditorFeatures/Core/NavigationBar/NavigationBarModel.cs b/src/EditorFeatures/Core/NavigationBar/NavigationBarModel.cs index 22e7aa60cd4d0..2bbb852185765 100644 --- a/src/EditorFeatures/Core/NavigationBar/NavigationBarModel.cs +++ b/src/EditorFeatures/Core/NavigationBar/NavigationBarModel.cs @@ -6,20 +6,19 @@ using System.Collections.Immutable; using System.Linq; -namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigationBar +namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigationBar; + +internal sealed class NavigationBarModel(INavigationBarItemService itemService, ImmutableArray types) : IEquatable { - internal sealed class NavigationBarModel(INavigationBarItemService itemService, ImmutableArray types) : IEquatable - { - public INavigationBarItemService ItemService { get; } = itemService; - public ImmutableArray Types { get; } = types; + public INavigationBarItemService ItemService { get; } = itemService; + public ImmutableArray Types { get; } = types; - public override bool Equals(object? obj) - => Equals(obj as NavigationBarModel); + public override bool Equals(object? obj) + => Equals(obj as NavigationBarModel); - public bool Equals(NavigationBarModel? other) - => other != null && Types.SequenceEqual(other.Types); + public bool Equals(NavigationBarModel? other) + => other != null && Types.SequenceEqual(other.Types); - public override int GetHashCode() - => throw new NotImplementedException(); - } + public override int GetHashCode() + => throw new NotImplementedException(); } diff --git a/src/EditorFeatures/Core/Options/CompletionViewOptionsStorage.cs b/src/EditorFeatures/Core/Options/CompletionViewOptionsStorage.cs index 7e7924fdc4853..9233a3776693d 100644 --- a/src/EditorFeatures/Core/Options/CompletionViewOptionsStorage.cs +++ b/src/EditorFeatures/Core/Options/CompletionViewOptionsStorage.cs @@ -4,21 +4,20 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal sealed class CompletionViewOptionsStorage { - internal sealed class CompletionViewOptionsStorage - { - public static readonly PerLanguageOption2 HighlightMatchingPortionsOfCompletionListItems = - new("dotnet_highlight_matching_portions_of_completion_list_items", defaultValue: true); + public static readonly PerLanguageOption2 HighlightMatchingPortionsOfCompletionListItems = + new("dotnet_highlight_matching_portions_of_completion_list_items", defaultValue: true); - public static readonly PerLanguageOption2 ShowCompletionItemFilters = - new("dotnet_show_completion_item_filters", defaultValue: true); + public static readonly PerLanguageOption2 ShowCompletionItemFilters = + new("dotnet_show_completion_item_filters", defaultValue: true); - // Use tri-value so the default state can be used to turn on the feature with experimentation service. - public static readonly PerLanguageOption2 EnableArgumentCompletionSnippets = - new("dotnet_enable_argument_completion_snippets", defaultValue: null); + // Use tri-value so the default state can be used to turn on the feature with experimentation service. + public static readonly PerLanguageOption2 EnableArgumentCompletionSnippets = + new("dotnet_enable_argument_completion_snippets", defaultValue: null); - public static readonly PerLanguageOption2 BlockForCompletionItems = - new("block_for_completion_items", defaultValue: true); - } + public static readonly PerLanguageOption2 BlockForCompletionItems = + new("block_for_completion_items", defaultValue: true); } diff --git a/src/EditorFeatures/Core/Options/EditorAnalyzerConfigOptions.cs b/src/EditorFeatures/Core/Options/EditorAnalyzerConfigOptions.cs index ed915e40fac02..dc302c205ac29 100644 --- a/src/EditorFeatures/Core/Options/EditorAnalyzerConfigOptions.cs +++ b/src/EditorFeatures/Core/Options/EditorAnalyzerConfigOptions.cs @@ -12,60 +12,59 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal sealed class EditorAnalyzerConfigOptions : AnalyzerConfigOptions { - internal sealed class EditorAnalyzerConfigOptions : AnalyzerConfigOptions + private readonly IDictionary _configOptions; + + internal EditorAnalyzerConfigOptions(IEditorOptions editorOptions) { - private readonly IDictionary _configOptions; + _configOptions = (editorOptions.GetOptionValue(DefaultOptions.RawCodingConventionsSnapshotOptionName) as IDictionary) ?? + SpecializedCollections.EmptyDictionary(); + } - internal EditorAnalyzerConfigOptions(IEditorOptions editorOptions) - { - _configOptions = (editorOptions.GetOptionValue(DefaultOptions.RawCodingConventionsSnapshotOptionName) as IDictionary) ?? - SpecializedCollections.EmptyDictionary(); - } + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + { + // TODO: the editor currently seems to store both lower-cased keys and original casing, the comparer is case-sensitive + // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1556206 - public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + if (_configOptions.TryGetValue(key.ToLowerInvariant(), out var objectValue)) { - // TODO: the editor currently seems to store both lower-cased keys and original casing, the comparer is case-sensitive - // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1556206 - - if (_configOptions.TryGetValue(key.ToLowerInvariant(), out var objectValue)) + // TODO: Although the editor exposes values typed to object they are actually strings. + value = objectValue switch { - // TODO: Although the editor exposes values typed to object they are actually strings. - value = objectValue switch - { - null => null, - string s => s, - object o => o.ToString() - }; - - return value != null; - } + null => null, + string s => s, + object o => o.ToString() + }; - value = null; - return false; + return value != null; } - public override IEnumerable Keys - => _configOptions.Keys.Where(IsLowercase); + value = null; + return false; + } + + public override IEnumerable Keys + => _configOptions.Keys.Where(IsLowercase); - private static bool IsLowercase(string str) + private static bool IsLowercase(string str) + { + foreach (var c in str) { - foreach (var c in str) + if (!char.IsLower(c)) { - if (!char.IsLower(c)) - { - return false; - } + return false; } - - return true; } - } - internal static partial class EditorOptionsExtensions - { - public static StructuredAnalyzerConfigOptions ToAnalyzerConfigOptions(this IEditorOptions editorOptions) - => StructuredAnalyzerConfigOptions.Create(new EditorAnalyzerConfigOptions(editorOptions)); + return true; } } + +internal static partial class EditorOptionsExtensions +{ + public static StructuredAnalyzerConfigOptions ToAnalyzerConfigOptions(this IEditorOptions editorOptions) + => StructuredAnalyzerConfigOptions.Create(new EditorAnalyzerConfigOptions(editorOptions)); +} diff --git a/src/EditorFeatures/Core/Options/LegacyGlobalOptionsWorkspaceService.cs b/src/EditorFeatures/Core/Options/LegacyGlobalOptionsWorkspaceService.cs index ad6cb16a84f5b..1a02b95cf0b79 100644 --- a/src/EditorFeatures/Core/Options/LegacyGlobalOptionsWorkspaceService.cs +++ b/src/EditorFeatures/Core/Options/LegacyGlobalOptionsWorkspaceService.cs @@ -9,61 +9,60 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.InlineHints; -namespace Microsoft.CodeAnalysis.Options +namespace Microsoft.CodeAnalysis.Options; + +/// +/// Enables legacy APIs to access global options from workspace. +/// +[ExportWorkspaceService(typeof(ILegacyGlobalOptionsWorkspaceService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class LegacyGlobalOptionsWorkspaceService(IGlobalOptionService globalOptions) : ILegacyGlobalOptionsWorkspaceService { - /// - /// Enables legacy APIs to access global options from workspace. - /// - [ExportWorkspaceService(typeof(ILegacyGlobalOptionsWorkspaceService)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class LegacyGlobalOptionsWorkspaceService(IGlobalOptionService globalOptions) : ILegacyGlobalOptionsWorkspaceService - { - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IGlobalOptionService _globalOptions = globalOptions; - private static readonly Option2 s_generateOverridesOption = new( - "dotnet_generate_overrides_for_all_members", defaultValue: true); + private static readonly Option2 s_generateOverridesOption = new( + "dotnet_generate_overrides_for_all_members", defaultValue: true); - private static readonly PerLanguageOption2 s_generateOperators = new( - "dotnet_generate_equality_operators", - defaultValue: false); + private static readonly PerLanguageOption2 s_generateOperators = new( + "dotnet_generate_equality_operators", + defaultValue: false); - private static readonly PerLanguageOption2 s_implementIEquatable = new( - "dotnet_generate_iequatable_implementation", - defaultValue: false); + private static readonly PerLanguageOption2 s_implementIEquatable = new( + "dotnet_generate_iequatable_implementation", + defaultValue: false); - internal static readonly PerLanguageOption2 s_addNullChecks = new( - "dotnet_generate_constructor_parameter_null_checks", - defaultValue: false); + internal static readonly PerLanguageOption2 s_addNullChecks = new( + "dotnet_generate_constructor_parameter_null_checks", + defaultValue: false); - public bool GenerateOverrides - { - get => _globalOptions.GetOption(s_generateOverridesOption); - set => _globalOptions.SetGlobalOption(s_generateOverridesOption, value); - } + public bool GenerateOverrides + { + get => _globalOptions.GetOption(s_generateOverridesOption); + set => _globalOptions.SetGlobalOption(s_generateOverridesOption, value); + } - public bool RazorUseTabs - => _globalOptions.GetOption(RazorLineFormattingOptionsStorage.UseTabs); + public bool RazorUseTabs + => _globalOptions.GetOption(RazorLineFormattingOptionsStorage.UseTabs); - public int RazorTabSize - => _globalOptions.GetOption(RazorLineFormattingOptionsStorage.TabSize); + public int RazorTabSize + => _globalOptions.GetOption(RazorLineFormattingOptionsStorage.TabSize); - public bool GetGenerateEqualsAndGetHashCodeFromMembersGenerateOperators(string language) - => _globalOptions.GetOption(s_implementIEquatable, language); + public bool GetGenerateEqualsAndGetHashCodeFromMembersGenerateOperators(string language) + => _globalOptions.GetOption(s_implementIEquatable, language); - public void SetGenerateEqualsAndGetHashCodeFromMembersGenerateOperators(string language, bool value) - => _globalOptions.SetGlobalOption(s_generateOperators, language, value); + public void SetGenerateEqualsAndGetHashCodeFromMembersGenerateOperators(string language, bool value) + => _globalOptions.SetGlobalOption(s_generateOperators, language, value); - public bool GetGenerateEqualsAndGetHashCodeFromMembersImplementIEquatable(string language) - => _globalOptions.GetOption(s_implementIEquatable, language); + public bool GetGenerateEqualsAndGetHashCodeFromMembersImplementIEquatable(string language) + => _globalOptions.GetOption(s_implementIEquatable, language); - public void SetGenerateEqualsAndGetHashCodeFromMembersImplementIEquatable(string language, bool value) - => _globalOptions.SetGlobalOption(s_implementIEquatable, language, value); + public void SetGenerateEqualsAndGetHashCodeFromMembersImplementIEquatable(string language, bool value) + => _globalOptions.SetGlobalOption(s_implementIEquatable, language, value); - public bool GetGenerateConstructorFromMembersOptionsAddNullChecks(string language) - => _globalOptions.GetOption(s_addNullChecks, language); + public bool GetGenerateConstructorFromMembersOptionsAddNullChecks(string language) + => _globalOptions.GetOption(s_addNullChecks, language); - public void SetGenerateConstructorFromMembersOptionsAddNullChecks(string language, bool value) - => _globalOptions.SetGlobalOption(s_addNullChecks, language, value); - } + public void SetGenerateConstructorFromMembersOptionsAddNullChecks(string language, bool value) + => _globalOptions.SetGlobalOption(s_addNullChecks, language, value); } diff --git a/src/EditorFeatures/Core/Organizing/OrganizeDocumentCommandHandler.cs b/src/EditorFeatures/Core/Organizing/OrganizeDocumentCommandHandler.cs index cecf378bd0a8b..6e14ab4d415b6 100644 --- a/src/EditorFeatures/Core/Organizing/OrganizeDocumentCommandHandler.cs +++ b/src/EditorFeatures/Core/Organizing/OrganizeDocumentCommandHandler.cs @@ -26,176 +26,175 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.Organizing +namespace Microsoft.CodeAnalysis.Editor.Implementation.Organizing; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[ContentType(ContentTypeNames.VisualBasicContentType)] +[ContentType(ContentTypeNames.XamlContentType)] +[Name(PredefinedCommandHandlerNames.OrganizeDocument)] +[method: ImportingConstructor] +[SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal class OrganizeDocumentCommandHandler( + IThreadingContext threadingContext, + IGlobalOptionService globalOptions, + IAsynchronousOperationListenerProvider listenerProvider) : + ICommandHandler, + ICommandHandler, + ICommandHandler { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.CSharpContentType)] - [ContentType(ContentTypeNames.VisualBasicContentType)] - [ContentType(ContentTypeNames.XamlContentType)] - [Name(PredefinedCommandHandlerNames.OrganizeDocument)] - [method: ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal class OrganizeDocumentCommandHandler( - IThreadingContext threadingContext, - IGlobalOptionService globalOptions, - IAsynchronousOperationListenerProvider listenerProvider) : - ICommandHandler, - ICommandHandler, - ICommandHandler - { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IGlobalOptionService _globalOptions = globalOptions; - private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.OrganizeDocument); + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.OrganizeDocument); - public string DisplayName => EditorFeaturesResources.Organize_Document; + public string DisplayName => EditorFeaturesResources.Organize_Document; - public CommandState GetCommandState(OrganizeDocumentCommandArgs args) - => GetCommandState(args, _ => EditorFeaturesResources.Organize_Document, needsSemantics: true); + public CommandState GetCommandState(OrganizeDocumentCommandArgs args) + => GetCommandState(args, _ => EditorFeaturesResources.Organize_Document, needsSemantics: true); - public CommandState GetCommandState(SortImportsCommandArgs args) - => GetCommandState(args, o => o.SortImportsDisplayStringWithAccelerator, needsSemantics: false); + public CommandState GetCommandState(SortImportsCommandArgs args) + => GetCommandState(args, o => o.SortImportsDisplayStringWithAccelerator, needsSemantics: false); - public CommandState GetCommandState(SortAndRemoveUnnecessaryImportsCommandArgs args) - => GetCommandState(args, o => o.SortAndRemoveUnusedImportsDisplayStringWithAccelerator, needsSemantics: true); + public CommandState GetCommandState(SortAndRemoveUnnecessaryImportsCommandArgs args) + => GetCommandState(args, o => o.SortAndRemoveUnusedImportsDisplayStringWithAccelerator, needsSemantics: true); - private static CommandState GetCommandState(EditorCommandArgs args, Func descriptionString, bool needsSemantics) + private static CommandState GetCommandState(EditorCommandArgs args, Func descriptionString, bool needsSemantics) + { + if (IsCommandSupported(args, needsSemantics, out var workspace)) { - if (IsCommandSupported(args, needsSemantics, out var workspace)) - { - var organizeImportsService = workspace.Services.SolutionServices.GetProjectServices(args.SubjectBuffer)!.GetRequiredService(); - return new CommandState(isAvailable: true, displayText: descriptionString(organizeImportsService)); - } - else - { - return CommandState.Unspecified; - } + var organizeImportsService = workspace.Services.SolutionServices.GetProjectServices(args.SubjectBuffer)!.GetRequiredService(); + return new CommandState(isAvailable: true, displayText: descriptionString(organizeImportsService)); } + else + { + return CommandState.Unspecified; + } + } - private static bool IsCommandSupported(EditorCommandArgs args, bool needsSemantics, [NotNullWhen(true)] out Workspace? workspace) + private static bool IsCommandSupported(EditorCommandArgs args, bool needsSemantics, [NotNullWhen(true)] out Workspace? workspace) + { + workspace = null; + if (args.SubjectBuffer.TryGetWorkspace(out var retrievedWorkspace)) { - workspace = null; - if (args.SubjectBuffer.TryGetWorkspace(out var retrievedWorkspace)) + workspace = retrievedWorkspace; + if (!workspace.CanApplyChange(ApplyChangesKind.ChangeDocument)) + { + return false; + } + + if (workspace.Kind == WorkspaceKind.MiscellaneousFiles) { - workspace = retrievedWorkspace; - if (!workspace.CanApplyChange(ApplyChangesKind.ChangeDocument)) - { - return false; - } - - if (workspace.Kind == WorkspaceKind.MiscellaneousFiles) - { - return !needsSemantics; - } - - return args.SubjectBuffer.SupportsRefactorings(); + return !needsSemantics; } - return false; + return args.SubjectBuffer.SupportsRefactorings(); } - private bool ExecuteCommand( - EditorCommandArgs commandArgs, - CommandExecutionContext context, - Func> getCurrentDocumentAsync, - Func> getChangedDocumentAsync) - { - // We're showing our own UI, ensure the editor doesn't show anything itself. - context.OperationContext.TakeOwnership(); + return false; + } - var token = _listener.BeginAsyncOperation(nameof(ExecuteCommand)); - _ = ExecuteAsync(commandArgs, getCurrentDocumentAsync, getChangedDocumentAsync) - .ReportNonFatalErrorAsync() - .CompletesAsyncOperation(token); + private bool ExecuteCommand( + EditorCommandArgs commandArgs, + CommandExecutionContext context, + Func> getCurrentDocumentAsync, + Func> getChangedDocumentAsync) + { + // We're showing our own UI, ensure the editor doesn't show anything itself. + context.OperationContext.TakeOwnership(); - return true; - } + var token = _listener.BeginAsyncOperation(nameof(ExecuteCommand)); + _ = ExecuteAsync(commandArgs, getCurrentDocumentAsync, getChangedDocumentAsync) + .ReportNonFatalErrorAsync() + .CompletesAsyncOperation(token); - private async Task ExecuteAsync( - EditorCommandArgs commandArgs, - Func> getCurrentDocumentAsync, - Func> getChangedDocumentAsync) - { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(); + return true; + } - var subjectBuffer = commandArgs.SubjectBuffer; - var textView = commandArgs.TextView; + private async Task ExecuteAsync( + EditorCommandArgs commandArgs, + Func> getCurrentDocumentAsync, + Func> getChangedDocumentAsync) + { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(); - var caretPoint = textView.GetCaretPoint(subjectBuffer); - if (caretPoint is null) - return; + var subjectBuffer = commandArgs.SubjectBuffer; + var textView = commandArgs.TextView; - if (!subjectBuffer.TryGetWorkspace(out var workspace)) - return; + var caretPoint = textView.GetCaretPoint(subjectBuffer); + if (caretPoint is null) + return; - var snapshotSpan = textView.GetTextElementSpan(caretPoint.Value); + if (!subjectBuffer.TryGetWorkspace(out var workspace)) + return; - var indicatorFactory = workspace.Services.GetRequiredService(); - using var backgroundWorkContext = indicatorFactory.Create( - commandArgs.TextView, - snapshotSpan, - EditorFeaturesResources.Organizing_document, - cancelOnEdit: true, - cancelOnFocusLost: false); + var snapshotSpan = textView.GetTextElementSpan(caretPoint.Value); - var cancellationToken = backgroundWorkContext.UserCancellationToken; + var indicatorFactory = workspace.Services.GetRequiredService(); + using var backgroundWorkContext = indicatorFactory.Create( + commandArgs.TextView, + snapshotSpan, + EditorFeaturesResources.Organizing_document, + cancelOnEdit: true, + cancelOnFocusLost: false); - await TaskScheduler.Default; + var cancellationToken = backgroundWorkContext.UserCancellationToken; - var currentDocument = await getCurrentDocumentAsync(snapshotSpan.Snapshot, backgroundWorkContext).ConfigureAwait(false); - if (currentDocument is null) - return; + await TaskScheduler.Default; - var newDocument = await getChangedDocumentAsync(currentDocument, cancellationToken).ConfigureAwait(false); - if (currentDocument == newDocument) - return; + var currentDocument = await getCurrentDocumentAsync(snapshotSpan.Snapshot, backgroundWorkContext).ConfigureAwait(false); + if (currentDocument is null) + return; - var changes = await newDocument.GetTextChangesAsync(currentDocument, cancellationToken).ConfigureAwait(false); + var newDocument = await getChangedDocumentAsync(currentDocument, cancellationToken).ConfigureAwait(false); + if (currentDocument == newDocument) + return; - // Required to switch back to the UI thread to call TryApplyChanges - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var changes = await newDocument.GetTextChangesAsync(currentDocument, cancellationToken).ConfigureAwait(false); - // We're about to make an edit ourselves. so disable the cancellation that happens on editing. - backgroundWorkContext.CancelOnEdit = false; + // Required to switch back to the UI thread to call TryApplyChanges + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - commandArgs.SubjectBuffer.ApplyChanges(changes); - } + // We're about to make an edit ourselves. so disable the cancellation that happens on editing. + backgroundWorkContext.CancelOnEdit = false; - public bool ExecuteCommand(OrganizeDocumentCommandArgs args, CommandExecutionContext context) - => ExecuteCommand( - args, context, - // Need full semantics for this operation. - (snapshot, context) => snapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context), - (document, cancellationToken) => OrganizingService.OrganizeAsync(document, cancellationToken: cancellationToken)); - - public bool ExecuteCommand(SortImportsCommandArgs args, CommandExecutionContext context) - => ExecuteCommand( - args, context, - // Only need syntax for this operation. - (snapshot, context) => Task.FromResult(snapshot.GetOpenDocumentInCurrentContextWithChanges()), - async (document, cancellationToken) => - { - var organizeImportsService = document.GetRequiredLanguageService(); - var options = await document.GetOrganizeImportsOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false); - return await organizeImportsService.OrganizeImportsAsync(document, options, cancellationToken).ConfigureAwait(false); - }); - - public bool ExecuteCommand(SortAndRemoveUnnecessaryImportsCommandArgs args, CommandExecutionContext context) - => ExecuteCommand( - args, context, - // Need full semantics for this operation. - (snapshot, context) => snapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context), - async (document, cancellationToken) => - { - var formattingOptions = document.SupportsSyntaxTree - ? await document.GetSyntaxFormattingOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false) - : null; - - var removeImportsService = document.GetRequiredLanguageService(); - var organizeImportsService = document.GetRequiredLanguageService(); - - var newDocument = await removeImportsService.RemoveUnnecessaryImportsAsync(document, formattingOptions, cancellationToken).ConfigureAwait(false); - var options = await document.GetOrganizeImportsOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false); - return await organizeImportsService.OrganizeImportsAsync(newDocument, options, cancellationToken).ConfigureAwait(false); - }); + commandArgs.SubjectBuffer.ApplyChanges(changes); } + + public bool ExecuteCommand(OrganizeDocumentCommandArgs args, CommandExecutionContext context) + => ExecuteCommand( + args, context, + // Need full semantics for this operation. + (snapshot, context) => snapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context), + (document, cancellationToken) => OrganizingService.OrganizeAsync(document, cancellationToken: cancellationToken)); + + public bool ExecuteCommand(SortImportsCommandArgs args, CommandExecutionContext context) + => ExecuteCommand( + args, context, + // Only need syntax for this operation. + (snapshot, context) => Task.FromResult(snapshot.GetOpenDocumentInCurrentContextWithChanges()), + async (document, cancellationToken) => + { + var organizeImportsService = document.GetRequiredLanguageService(); + var options = await document.GetOrganizeImportsOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false); + return await organizeImportsService.OrganizeImportsAsync(document, options, cancellationToken).ConfigureAwait(false); + }); + + public bool ExecuteCommand(SortAndRemoveUnnecessaryImportsCommandArgs args, CommandExecutionContext context) + => ExecuteCommand( + args, context, + // Need full semantics for this operation. + (snapshot, context) => snapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context), + async (document, cancellationToken) => + { + var formattingOptions = document.SupportsSyntaxTree + ? await document.GetSyntaxFormattingOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false) + : null; + + var removeImportsService = document.GetRequiredLanguageService(); + var organizeImportsService = document.GetRequiredLanguageService(); + + var newDocument = await removeImportsService.RemoveUnnecessaryImportsAsync(document, formattingOptions, cancellationToken).ConfigureAwait(false); + var options = await document.GetOrganizeImportsOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false); + return await organizeImportsService.OrganizeImportsAsync(newDocument, options, cancellationToken).ConfigureAwait(false); + }); } diff --git a/src/EditorFeatures/Core/PasteTracking/PasteTrackingPasteCommandHandler.cs b/src/EditorFeatures/Core/PasteTracking/PasteTrackingPasteCommandHandler.cs index fd2f8edbc272c..b9b0ff214cc1e 100644 --- a/src/EditorFeatures/Core/PasteTracking/PasteTrackingPasteCommandHandler.cs +++ b/src/EditorFeatures/Core/PasteTracking/PasteTrackingPasteCommandHandler.cs @@ -13,53 +13,52 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.PasteTracking +namespace Microsoft.CodeAnalysis.PasteTracking; + +[Export] +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.RoslynContentType)] +[Name(PredefinedCommandHandlerNames.PasteTrackingPaste)] +// By registering to run prior to FormatDocument and deferring until it has completed we +// will be able to register the pasted text span after any formatting changes have been +// applied. This is important because the PasteTrackingService will dismiss the registered +// textspan when the textbuffer is changed. +[Order(Before = PredefinedCommandHandlerNames.FormatDocument)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class PasteTrackingPasteCommandHandler(PasteTrackingService pasteTrackingService) : IChainedCommandHandler { - [Export] - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.RoslynContentType)] - [Name(PredefinedCommandHandlerNames.PasteTrackingPaste)] - // By registering to run prior to FormatDocument and deferring until it has completed we - // will be able to register the pasted text span after any formatting changes have been - // applied. This is important because the PasteTrackingService will dismiss the registered - // textspan when the textbuffer is changed. - [Order(Before = PredefinedCommandHandlerNames.FormatDocument)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class PasteTrackingPasteCommandHandler(PasteTrackingService pasteTrackingService) : IChainedCommandHandler - { - public string DisplayName => EditorFeaturesResources.Paste_Tracking; + public string DisplayName => EditorFeaturesResources.Paste_Tracking; - private readonly PasteTrackingService _pasteTrackingService = pasteTrackingService; + private readonly PasteTrackingService _pasteTrackingService = pasteTrackingService; - public CommandState GetCommandState(PasteCommandArgs args, Func nextCommandHandler) - => nextCommandHandler(); + public CommandState GetCommandState(PasteCommandArgs args, Func nextCommandHandler) + => nextCommandHandler(); - public void ExecuteCommand(PasteCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + public void ExecuteCommand(PasteCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + // Capture the pre-paste caret position + var caretPosition = args.TextView.GetCaretPoint(args.SubjectBuffer); + if (!caretPosition.HasValue) { - // Capture the pre-paste caret position - var caretPosition = args.TextView.GetCaretPoint(args.SubjectBuffer); - if (!caretPosition.HasValue) - { - return; - } + return; + } - // Allow the pasted text to be inserted and formatted. - nextCommandHandler(); + // Allow the pasted text to be inserted and formatted. + nextCommandHandler(); - if (!args.SubjectBuffer.CanApplyChangeDocumentToWorkspace()) - { - return; - } + if (!args.SubjectBuffer.CanApplyChangeDocumentToWorkspace()) + { + return; + } - // Create a tracking span from the pre-paste caret position that will grow as text is inserted. - var trackingSpan = caretPosition.Value.Snapshot.CreateTrackingSpan(caretPosition.Value.Position, 0, SpanTrackingMode.EdgeInclusive); + // Create a tracking span from the pre-paste caret position that will grow as text is inserted. + var trackingSpan = caretPosition.Value.Snapshot.CreateTrackingSpan(caretPosition.Value.Position, 0, SpanTrackingMode.EdgeInclusive); - // Applying the post-paste snapshot to the tracking span gives us the span of pasted text. - var snapshotSpan = trackingSpan.GetSpan(args.SubjectBuffer.CurrentSnapshot); - var textSpan = TextSpan.FromBounds(snapshotSpan.Start, snapshotSpan.End); + // Applying the post-paste snapshot to the tracking span gives us the span of pasted text. + var snapshotSpan = trackingSpan.GetSpan(args.SubjectBuffer.CurrentSnapshot); + var textSpan = TextSpan.FromBounds(snapshotSpan.Start, snapshotSpan.End); - _pasteTrackingService.RegisterPastedTextSpan(args.SubjectBuffer, textSpan); - } + _pasteTrackingService.RegisterPastedTextSpan(args.SubjectBuffer, textSpan); } } diff --git a/src/EditorFeatures/Core/PasteTracking/PasteTrackingService.cs b/src/EditorFeatures/Core/PasteTracking/PasteTrackingService.cs index 710e1e8d4fe3f..f3e593dc039e8 100644 --- a/src/EditorFeatures/Core/PasteTracking/PasteTrackingService.cs +++ b/src/EditorFeatures/Core/PasteTracking/PasteTrackingService.cs @@ -13,50 +13,49 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.PasteTracking +namespace Microsoft.CodeAnalysis.PasteTracking; + +[Export(typeof(IPasteTrackingService)), Shared] +[Export(typeof(PasteTrackingService))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class PasteTrackingService(IThreadingContext threadingContext) : IPasteTrackingService { - [Export(typeof(IPasteTrackingService)), Shared] - [Export(typeof(PasteTrackingService))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class PasteTrackingService(IThreadingContext threadingContext) : IPasteTrackingService - { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly object _pastedTextSpanKey = new(); + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly object _pastedTextSpanKey = new(); - public bool TryGetPastedTextSpan(SourceTextContainer sourceTextContainer, out TextSpan textSpan) + public bool TryGetPastedTextSpan(SourceTextContainer sourceTextContainer, out TextSpan textSpan) + { + var textBuffer = sourceTextContainer.TryGetTextBuffer(); + if (textBuffer is null) { - var textBuffer = sourceTextContainer.TryGetTextBuffer(); - if (textBuffer is null) - { - textSpan = default; - return false; - } - - // `PropertiesCollection` is thread-safe - return textBuffer.Properties.TryGetProperty(_pastedTextSpanKey, out textSpan); + textSpan = default; + return false; } - internal void RegisterPastedTextSpan(ITextBuffer textBuffer, TextSpan textSpan) + // `PropertiesCollection` is thread-safe + return textBuffer.Properties.TryGetProperty(_pastedTextSpanKey, out textSpan); + } + + internal void RegisterPastedTextSpan(ITextBuffer textBuffer, TextSpan textSpan) + { + _threadingContext.ThrowIfNotOnUIThread(); + + // Use the TextBuffer properties to store the pasted text span. + // The `PropertiesCollection` is thread-safe and will be cleared + // when all TextViews that share this buffer are closed. + // Any change to the TextBuffer will remove the pasted text span. + // This includes consecutive paste operations which will fire the + // Changed event prior to the handler registering a new text span. + textBuffer.Properties[_pastedTextSpanKey] = textSpan; + textBuffer.Changed += RemovePastedTextSpan; + + return; + + void RemovePastedTextSpan(object sender, TextContentChangedEventArgs e) { - _threadingContext.ThrowIfNotOnUIThread(); - - // Use the TextBuffer properties to store the pasted text span. - // The `PropertiesCollection` is thread-safe and will be cleared - // when all TextViews that share this buffer are closed. - // Any change to the TextBuffer will remove the pasted text span. - // This includes consecutive paste operations which will fire the - // Changed event prior to the handler registering a new text span. - textBuffer.Properties[_pastedTextSpanKey] = textSpan; - textBuffer.Changed += RemovePastedTextSpan; - - return; - - void RemovePastedTextSpan(object sender, TextContentChangedEventArgs e) - { - textBuffer.Changed -= RemovePastedTextSpan; - textBuffer.Properties.RemoveProperty(_pastedTextSpanKey); - } + textBuffer.Changed -= RemovePastedTextSpan; + textBuffer.Properties.RemoveProperty(_pastedTextSpanKey); } } } diff --git a/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs b/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs index e0b4b55c71216..4735ad7f20aac 100644 --- a/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs +++ b/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs @@ -29,794 +29,793 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.Preview +namespace Microsoft.CodeAnalysis.Editor.Implementation.Preview; + +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal abstract class AbstractPreviewFactoryService( + IThreadingContext threadingContext, + ITextBufferFactoryService textBufferFactoryService, + IContentTypeRegistryService contentTypeRegistryService, + IProjectionBufferFactoryService projectionBufferFactoryService, + EditorOptionsService editorOptionsService, + ITextDifferencingSelectorService differenceSelectorService, + IDifferenceBufferFactoryService differenceBufferService, + ITextDocumentFactoryService textDocumentFactoryService, + ITextViewRoleSet previewRoleSet) : IPreviewFactoryService + where TDifferenceViewer : IDifferenceViewer { - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal abstract class AbstractPreviewFactoryService( - IThreadingContext threadingContext, - ITextBufferFactoryService textBufferFactoryService, - IContentTypeRegistryService contentTypeRegistryService, - IProjectionBufferFactoryService projectionBufferFactoryService, - EditorOptionsService editorOptionsService, - ITextDifferencingSelectorService differenceSelectorService, - IDifferenceBufferFactoryService differenceBufferService, - ITextDocumentFactoryService textDocumentFactoryService, - ITextViewRoleSet previewRoleSet) : IPreviewFactoryService - where TDifferenceViewer : IDifferenceViewer + private const double DefaultZoomLevel = 0.75; + private readonly ITextViewRoleSet _previewRoleSet = previewRoleSet; + private readonly ITextBufferFactoryService _textBufferFactoryService = textBufferFactoryService; + private readonly IContentTypeRegistryService _contentTypeRegistryService = contentTypeRegistryService; + private readonly IProjectionBufferFactoryService _projectionBufferFactoryService = projectionBufferFactoryService; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly ITextDifferencingSelectorService _differenceSelectorService = differenceSelectorService; + private readonly IDifferenceBufferFactoryService _differenceBufferService = differenceBufferService; + private readonly ITextDocumentFactoryService _textDocumentFactoryService = textDocumentFactoryService; + + protected readonly IThreadingContext ThreadingContext = threadingContext; + + public SolutionPreviewResult? GetSolutionPreviews(Solution oldSolution, Solution? newSolution, CancellationToken cancellationToken) + => GetSolutionPreviews(oldSolution, newSolution, DefaultZoomLevel, cancellationToken); + + public SolutionPreviewResult? GetSolutionPreviews(Solution oldSolution, Solution? newSolution, double zoomLevel, CancellationToken cancellationToken) { - private const double DefaultZoomLevel = 0.75; - private readonly ITextViewRoleSet _previewRoleSet = previewRoleSet; - private readonly ITextBufferFactoryService _textBufferFactoryService = textBufferFactoryService; - private readonly IContentTypeRegistryService _contentTypeRegistryService = contentTypeRegistryService; - private readonly IProjectionBufferFactoryService _projectionBufferFactoryService = projectionBufferFactoryService; - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - private readonly ITextDifferencingSelectorService _differenceSelectorService = differenceSelectorService; - private readonly IDifferenceBufferFactoryService _differenceBufferService = differenceBufferService; - private readonly ITextDocumentFactoryService _textDocumentFactoryService = textDocumentFactoryService; - - protected readonly IThreadingContext ThreadingContext = threadingContext; - - public SolutionPreviewResult? GetSolutionPreviews(Solution oldSolution, Solution? newSolution, CancellationToken cancellationToken) - => GetSolutionPreviews(oldSolution, newSolution, DefaultZoomLevel, cancellationToken); - - public SolutionPreviewResult? GetSolutionPreviews(Solution oldSolution, Solution? newSolution, double zoomLevel, CancellationToken cancellationToken) + cancellationToken.ThrowIfCancellationRequested(); + + // Note: The order in which previews are added to the below list is significant. + // Preview for a changed document is preferred over preview for changed references and so on. + var previewItems = new List(); + SolutionChangeSummary? changeSummary = null; + if (newSolution != null) { - cancellationToken.ThrowIfCancellationRequested(); + var solutionChanges = newSolution.GetChanges(oldSolution); + var ignoreUnchangeableDocuments = oldSolution.Workspace.IgnoreUnchangeableDocumentsWhenApplyingChanges; - // Note: The order in which previews are added to the below list is significant. - // Preview for a changed document is preferred over preview for changed references and so on. - var previewItems = new List(); - SolutionChangeSummary? changeSummary = null; - if (newSolution != null) + foreach (var projectChanges in solutionChanges.GetProjectChanges()) { - var solutionChanges = newSolution.GetChanges(oldSolution); - var ignoreUnchangeableDocuments = oldSolution.Workspace.IgnoreUnchangeableDocumentsWhenApplyingChanges; + cancellationToken.ThrowIfCancellationRequested(); + + var projectId = projectChanges.ProjectId; + var oldProject = projectChanges.OldProject; + var newProject = projectChanges.NewProject; - foreach (var projectChanges in solutionChanges.GetProjectChanges()) + // Exclude changes to unchangeable documents if they will be ignored when applied to workspace. + foreach (var documentId in projectChanges.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true, ignoreUnchangeableDocuments)) { cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => + await CreateChangedDocumentPreviewViewAsync(oldSolution.GetRequiredDocument(documentId), newSolution.GetRequiredDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); + } - var projectId = projectChanges.ProjectId; - var oldProject = projectChanges.OldProject; - var newProject = projectChanges.NewProject; - - // Exclude changes to unchangeable documents if they will be ignored when applied to workspace. - foreach (var documentId in projectChanges.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true, ignoreUnchangeableDocuments)) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => - await CreateChangedDocumentPreviewViewAsync(oldSolution.GetRequiredDocument(documentId), newSolution.GetRequiredDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); - } - - foreach (var documentId in projectChanges.GetAddedDocuments()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => - await CreateAddedDocumentPreviewViewAsync(newSolution.GetRequiredDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); - } - - foreach (var documentId in projectChanges.GetRemovedDocuments()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(oldProject.Id, documentId, async c => - await CreateRemovedDocumentPreviewViewAsync(oldSolution.GetRequiredDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); - } - - foreach (var documentId in projectChanges.GetChangedAdditionalDocuments()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => - await CreateChangedAdditionalDocumentPreviewViewAsync(oldSolution.GetRequiredAdditionalDocument(documentId), newSolution.GetRequiredAdditionalDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); - } - - foreach (var documentId in projectChanges.GetAddedAdditionalDocuments()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => - await CreateAddedAdditionalDocumentPreviewViewAsync(newSolution.GetRequiredAdditionalDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); - } - - foreach (var documentId in projectChanges.GetRemovedAdditionalDocuments()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(oldProject.Id, documentId, async c => - await CreateRemovedAdditionalDocumentPreviewViewAsync(oldSolution.GetRequiredAdditionalDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); - } - - foreach (var documentId in projectChanges.GetChangedAnalyzerConfigDocuments()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => - await CreateChangedAnalyzerConfigDocumentPreviewViewAsync(oldSolution.GetRequiredAnalyzerConfigDocument(documentId), newSolution.GetRequiredAnalyzerConfigDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); - } - - foreach (var documentId in projectChanges.GetAddedAnalyzerConfigDocuments()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => - await CreateAddedAnalyzerConfigDocumentPreviewViewAsync(newSolution.GetRequiredAnalyzerConfigDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); - } - - foreach (var documentId in projectChanges.GetRemovedAnalyzerConfigDocuments()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(oldProject.Id, documentId, async c => - await CreateRemovedAnalyzerConfigDocumentPreviewViewAsync(oldSolution.GetRequiredAnalyzerConfigDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); - } - - foreach (var metadataReference in projectChanges.GetAddedMetadataReferences()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, - string.Format(EditorFeaturesResources.Adding_reference_0_to_1, metadataReference.Display, oldProject.Name))); - } - - foreach (var metadataReference in projectChanges.GetRemovedMetadataReferences()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, - string.Format(EditorFeaturesResources.Removing_reference_0_from_1, metadataReference.Display, oldProject.Name))); - } - - foreach (var projectReference in projectChanges.GetAddedProjectReferences()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, - string.Format(EditorFeaturesResources.Adding_reference_0_to_1, newSolution.GetRequiredProject(projectReference.ProjectId).Name, oldProject.Name))); - } - - foreach (var projectReference in projectChanges.GetRemovedProjectReferences()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, - string.Format(EditorFeaturesResources.Removing_reference_0_from_1, oldSolution.GetRequiredProject(projectReference.ProjectId).Name, oldProject.Name))); - } - - foreach (var analyzer in projectChanges.GetAddedAnalyzerReferences()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, - string.Format(EditorFeaturesResources.Adding_analyzer_reference_0_to_1, analyzer.Display, oldProject.Name))); - } - - foreach (var analyzer in projectChanges.GetRemovedAnalyzerReferences()) - { - cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, - string.Format(EditorFeaturesResources.Removing_analyzer_reference_0_from_1, analyzer.Display, oldProject.Name))); - } + foreach (var documentId in projectChanges.GetAddedDocuments()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => + await CreateAddedDocumentPreviewViewAsync(newSolution.GetRequiredDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); } - foreach (var project in solutionChanges.GetAddedProjects()) + foreach (var documentId in projectChanges.GetRemovedDocuments()) { cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(project.Id, null, - string.Format(EditorFeaturesResources.Adding_project_0, project.Name))); + previewItems.Add(new SolutionPreviewItem(oldProject.Id, documentId, async c => + await CreateRemovedDocumentPreviewViewAsync(oldSolution.GetRequiredDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); } - foreach (var project in solutionChanges.GetRemovedProjects()) + foreach (var documentId in projectChanges.GetChangedAdditionalDocuments()) { cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(project.Id, null, - string.Format(EditorFeaturesResources.Removing_project_0, project.Name))); + previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => + await CreateChangedAdditionalDocumentPreviewViewAsync(oldSolution.GetRequiredAdditionalDocument(documentId), newSolution.GetRequiredAdditionalDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); } - foreach (var projectChanges in solutionChanges.GetProjectChanges().Where(ProjectReferencesChanged)) + foreach (var documentId in projectChanges.GetAddedAdditionalDocuments()) { cancellationToken.ThrowIfCancellationRequested(); - previewItems.Add(new SolutionPreviewItem(projectChanges.OldProject.Id, null, - string.Format(EditorFeaturesResources.Changing_project_references_for_0, projectChanges.OldProject.Name))); + previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => + await CreateAddedAdditionalDocumentPreviewViewAsync(newSolution.GetRequiredAdditionalDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); } - changeSummary = new SolutionChangeSummary(oldSolution, newSolution, solutionChanges); - } + foreach (var documentId in projectChanges.GetRemovedAdditionalDocuments()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(oldProject.Id, documentId, async c => + await CreateRemovedAdditionalDocumentPreviewViewAsync(oldSolution.GetRequiredAdditionalDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); + } - return new SolutionPreviewResult(ThreadingContext, previewItems, changeSummary); - } + foreach (var documentId in projectChanges.GetChangedAnalyzerConfigDocuments()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => + await CreateChangedAnalyzerConfigDocumentPreviewViewAsync(oldSolution.GetRequiredAnalyzerConfigDocument(documentId), newSolution.GetRequiredAnalyzerConfigDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); + } - private bool ProjectReferencesChanged(ProjectChanges projectChanges) - { - var oldProjectReferences = projectChanges.OldProject.ProjectReferences.ToDictionary(r => r.ProjectId); - var newProjectReferences = projectChanges.NewProject.ProjectReferences.ToDictionary(r => r.ProjectId); + foreach (var documentId in projectChanges.GetAddedAnalyzerConfigDocuments()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c => + await CreateAddedAnalyzerConfigDocumentPreviewViewAsync(newSolution.GetRequiredAnalyzerConfigDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); + } - // These are the set of project reference that remained in the project. We don't care - // about project references that were added or removed. Those will already be reported. - var preservedProjectIds = oldProjectReferences.Keys.Intersect(newProjectReferences.Keys); + foreach (var documentId in projectChanges.GetRemovedAnalyzerConfigDocuments()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(oldProject.Id, documentId, async c => + await CreateRemovedAnalyzerConfigDocumentPreviewViewAsync(oldSolution.GetRequiredAnalyzerConfigDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline())); + } - foreach (var projectId in preservedProjectIds) - { - var oldProjectReference = oldProjectReferences[projectId]; - var newProjectReference = newProjectReferences[projectId]; + foreach (var metadataReference in projectChanges.GetAddedMetadataReferences()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, + string.Format(EditorFeaturesResources.Adding_reference_0_to_1, metadataReference.Display, oldProject.Name))); + } + + foreach (var metadataReference in projectChanges.GetRemovedMetadataReferences()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, + string.Format(EditorFeaturesResources.Removing_reference_0_from_1, metadataReference.Display, oldProject.Name))); + } + + foreach (var projectReference in projectChanges.GetAddedProjectReferences()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, + string.Format(EditorFeaturesResources.Adding_reference_0_to_1, newSolution.GetRequiredProject(projectReference.ProjectId).Name, oldProject.Name))); + } - if (!oldProjectReference.Equals(newProjectReference)) + foreach (var projectReference in projectChanges.GetRemovedProjectReferences()) { - return true; + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, + string.Format(EditorFeaturesResources.Removing_reference_0_from_1, oldSolution.GetRequiredProject(projectReference.ProjectId).Name, oldProject.Name))); + } + + foreach (var analyzer in projectChanges.GetAddedAnalyzerReferences()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, + string.Format(EditorFeaturesResources.Adding_analyzer_reference_0_to_1, analyzer.Display, oldProject.Name))); + } + + foreach (var analyzer in projectChanges.GetRemovedAnalyzerReferences()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(oldProject.Id, null, + string.Format(EditorFeaturesResources.Removing_analyzer_reference_0_from_1, analyzer.Display, oldProject.Name))); } } - return false; + foreach (var project in solutionChanges.GetAddedProjects()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(project.Id, null, + string.Format(EditorFeaturesResources.Adding_project_0, project.Name))); + } + + foreach (var project in solutionChanges.GetRemovedProjects()) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(project.Id, null, + string.Format(EditorFeaturesResources.Removing_project_0, project.Name))); + } + + foreach (var projectChanges in solutionChanges.GetProjectChanges().Where(ProjectReferencesChanged)) + { + cancellationToken.ThrowIfCancellationRequested(); + previewItems.Add(new SolutionPreviewItem(projectChanges.OldProject.Id, null, + string.Format(EditorFeaturesResources.Changing_project_references_for_0, projectChanges.OldProject.Name))); + } + + changeSummary = new SolutionChangeSummary(oldSolution, newSolution, solutionChanges); } - public Task> CreateAddedDocumentPreviewViewAsync(Document document, CancellationToken cancellationToken) - => CreateAddedDocumentPreviewViewAsync(document, DefaultZoomLevel, cancellationToken); + return new SolutionPreviewResult(ThreadingContext, previewItems, changeSummary); + } + + private bool ProjectReferencesChanged(ProjectChanges projectChanges) + { + var oldProjectReferences = projectChanges.OldProject.ProjectReferences.ToDictionary(r => r.ProjectId); + var newProjectReferences = projectChanges.NewProject.ProjectReferences.ToDictionary(r => r.ProjectId); - private async ValueTask> CreateAddedDocumentPreviewViewCoreAsync(ITextBuffer newBuffer, ReferenceCountedDisposable workspace, TextDocument document, double zoomLevel, CancellationToken cancellationToken) + // These are the set of project reference that remained in the project. We don't care + // about project references that were added or removed. Those will already be reported. + var preservedProjectIds = oldProjectReferences.Keys.Intersect(newProjectReferences.Keys); + + foreach (var projectId in preservedProjectIds) { - // IProjectionBufferFactoryService is a Visual Studio API which is not documented as free-threaded - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var oldProjectReference = oldProjectReferences[projectId]; + var newProjectReference = newProjectReferences[projectId]; + + if (!oldProjectReference.Equals(newProjectReference)) + { + return true; + } + } + + return false; + } + + public Task> CreateAddedDocumentPreviewViewAsync(Document document, CancellationToken cancellationToken) + => CreateAddedDocumentPreviewViewAsync(document, DefaultZoomLevel, cancellationToken); + + private async ValueTask> CreateAddedDocumentPreviewViewCoreAsync(ITextBuffer newBuffer, ReferenceCountedDisposable workspace, TextDocument document, double zoomLevel, CancellationToken cancellationToken) + { + // IProjectionBufferFactoryService is a Visual Studio API which is not documented as free-threaded + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var firstLine = string.Format(EditorFeaturesResources.Adding_0_to_1_with_content_colon, - document.Name, document.Project.Name); + var firstLine = string.Format(EditorFeaturesResources.Adding_0_to_1_with_content_colon, + document.Name, document.Project.Name); - var originalBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer( - sourceSpans: [firstLine, "\r\n"], registryService: _contentTypeRegistryService); + var originalBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer( + sourceSpans: [firstLine, "\r\n"], registryService: _contentTypeRegistryService); - var span = new SnapshotSpan(newBuffer.CurrentSnapshot, Span.FromBounds(0, newBuffer.CurrentSnapshot.Length)) - .CreateTrackingSpan(SpanTrackingMode.EdgeExclusive); - var changedBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer( - sourceSpans: [firstLine, "\r\n", span], registryService: _contentTypeRegistryService); + var span = new SnapshotSpan(newBuffer.CurrentSnapshot, Span.FromBounds(0, newBuffer.CurrentSnapshot.Length)) + .CreateTrackingSpan(SpanTrackingMode.EdgeExclusive); + var changedBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer( + sourceSpans: [firstLine, "\r\n", span], registryService: _contentTypeRegistryService); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - return await CreateNewDifferenceViewerAsync(null, workspace, originalBuffer, changedBuffer, zoomLevel, cancellationToken); + return await CreateNewDifferenceViewerAsync(null, workspace, originalBuffer, changedBuffer, zoomLevel, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - } + } - private async Task> CreateAddedTextDocumentPreviewViewAsync( - TDocument document, - double zoomLevel, - Func> createBufferAsync, - CancellationToken cancellationToken) - where TDocument : TextDocument - { - // createBufferAsync must be called from the main thread - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + private async Task> CreateAddedTextDocumentPreviewViewAsync( + TDocument document, + double zoomLevel, + Func> createBufferAsync, + CancellationToken cancellationToken) + where TDocument : TextDocument + { + // createBufferAsync must be called from the main thread + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - var newBuffer = await createBufferAsync(document, cancellationToken); + var newBuffer = await createBufferAsync(document, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - // Create PreviewWorkspace around the buffer to be displayed in the diff preview - // so that all IDE services (colorizer, squiggles etc.) light up in this buffer. - using var rightWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(document.Project.Solution)); - rightWorkspace.Target.OpenDocument(document.Id, newBuffer.AsTextContainer()); + // Create PreviewWorkspace around the buffer to be displayed in the diff preview + // so that all IDE services (colorizer, squiggles etc.) light up in this buffer. + using var rightWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(document.Project.Solution)); + rightWorkspace.Target.OpenDocument(document.Id, newBuffer.AsTextContainer()); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - return await CreateAddedDocumentPreviewViewCoreAsync(newBuffer, rightWorkspace, document, zoomLevel, cancellationToken); + return await CreateAddedDocumentPreviewViewCoreAsync(newBuffer, rightWorkspace, document, zoomLevel, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - } + } - public Task> CreateAddedDocumentPreviewViewAsync(Document document, double zoomLevel, CancellationToken cancellationToken) - { - return CreateAddedTextDocumentPreviewViewAsync( - document, zoomLevel, - createBufferAsync: (textDocument, cancellationToken) => CreateNewBufferAsync(textDocument, cancellationToken), - cancellationToken); - } + public Task> CreateAddedDocumentPreviewViewAsync(Document document, double zoomLevel, CancellationToken cancellationToken) + { + return CreateAddedTextDocumentPreviewViewAsync( + document, zoomLevel, + createBufferAsync: (textDocument, cancellationToken) => CreateNewBufferAsync(textDocument, cancellationToken), + cancellationToken); + } - public Task> CreateAddedAdditionalDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken) - { - return CreateAddedTextDocumentPreviewViewAsync( - document, zoomLevel, - createBufferAsync: CreateNewPlainTextBufferAsync, - cancellationToken); - } + public Task> CreateAddedAdditionalDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken) + { + return CreateAddedTextDocumentPreviewViewAsync( + document, zoomLevel, + createBufferAsync: CreateNewPlainTextBufferAsync, + cancellationToken); + } - public Task> CreateAddedAnalyzerConfigDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken) - { - return CreateAddedTextDocumentPreviewViewAsync( - document, zoomLevel, - createBufferAsync: CreateNewPlainTextBufferAsync, - cancellationToken); - } + public Task> CreateAddedAnalyzerConfigDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken) + { + return CreateAddedTextDocumentPreviewViewAsync( + document, zoomLevel, + createBufferAsync: CreateNewPlainTextBufferAsync, + cancellationToken); + } - public Task> CreateRemovedDocumentPreviewViewAsync(Document document, CancellationToken cancellationToken) - => CreateRemovedDocumentPreviewViewAsync(document, DefaultZoomLevel, cancellationToken); + public Task> CreateRemovedDocumentPreviewViewAsync(Document document, CancellationToken cancellationToken) + => CreateRemovedDocumentPreviewViewAsync(document, DefaultZoomLevel, cancellationToken); - private async ValueTask> CreateRemovedDocumentPreviewViewCoreAsync(ITextBuffer oldBuffer, ReferenceCountedDisposable workspace, TextDocument document, double zoomLevel, CancellationToken cancellationToken) - { - // IProjectionBufferFactoryService is a Visual Studio API which is not documented as free-threaded - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + private async ValueTask> CreateRemovedDocumentPreviewViewCoreAsync(ITextBuffer oldBuffer, ReferenceCountedDisposable workspace, TextDocument document, double zoomLevel, CancellationToken cancellationToken) + { + // IProjectionBufferFactoryService is a Visual Studio API which is not documented as free-threaded + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var firstLine = string.Format(EditorFeaturesResources.Removing_0_from_1_with_content_colon, - document.Name, document.Project.Name); + var firstLine = string.Format(EditorFeaturesResources.Removing_0_from_1_with_content_colon, + document.Name, document.Project.Name); - var span = new SnapshotSpan(oldBuffer.CurrentSnapshot, Span.FromBounds(0, oldBuffer.CurrentSnapshot.Length)) - .CreateTrackingSpan(SpanTrackingMode.EdgeExclusive); - var originalBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer( - sourceSpans: [firstLine, "\r\n", span], registryService: _contentTypeRegistryService); + var span = new SnapshotSpan(oldBuffer.CurrentSnapshot, Span.FromBounds(0, oldBuffer.CurrentSnapshot.Length)) + .CreateTrackingSpan(SpanTrackingMode.EdgeExclusive); + var originalBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer( + sourceSpans: [firstLine, "\r\n", span], registryService: _contentTypeRegistryService); - var changedBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer( - sourceSpans: [firstLine, "\r\n"], registryService: _contentTypeRegistryService); + var changedBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer( + sourceSpans: [firstLine, "\r\n"], registryService: _contentTypeRegistryService); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - return await CreateNewDifferenceViewerAsync(workspace, null, originalBuffer, changedBuffer, zoomLevel, cancellationToken); + return await CreateNewDifferenceViewerAsync(workspace, null, originalBuffer, changedBuffer, zoomLevel, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - } + } - private async Task> CreateRemovedTextDocumentPreviewViewAsync( - TDocument document, - double zoomLevel, - Func> createBufferAsync, - CancellationToken cancellationToken) - where TDocument : TextDocument - { - // createBufferAsync must be called from the main thread - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - - // Note: We don't use the original buffer that is associated with oldDocument - // (and possibly open in the editor) for oldBuffer below. This is because oldBuffer - // will be used inside a projection buffer inside our inline diff preview below - // and platform's implementation currently has a bug where projection buffers - // are being leaked. This leak means that if we use the original buffer that is - // currently visible in the editor here, the projection buffer span calculation - // would be triggered every time user changes some code in this buffer (even though - // the diff view would long have been dismissed by the time user edits the code) - // resulting in crashes. Instead we create a new buffer from the same content. - // TODO: We could use ITextBufferCloneService instead here to clone the original buffer. + private async Task> CreateRemovedTextDocumentPreviewViewAsync( + TDocument document, + double zoomLevel, + Func> createBufferAsync, + CancellationToken cancellationToken) + where TDocument : TextDocument + { + // createBufferAsync must be called from the main thread + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + // Note: We don't use the original buffer that is associated with oldDocument + // (and possibly open in the editor) for oldBuffer below. This is because oldBuffer + // will be used inside a projection buffer inside our inline diff preview below + // and platform's implementation currently has a bug where projection buffers + // are being leaked. This leak means that if we use the original buffer that is + // currently visible in the editor here, the projection buffer span calculation + // would be triggered every time user changes some code in this buffer (even though + // the diff view would long have been dismissed by the time user edits the code) + // resulting in crashes. Instead we create a new buffer from the same content. + // TODO: We could use ITextBufferCloneService instead here to clone the original buffer. #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - var oldBuffer = await createBufferAsync(document, cancellationToken); + var oldBuffer = await createBufferAsync(document, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - // Create PreviewWorkspace around the buffer to be displayed in the diff preview - // so that all IDE services (colorizer, squiggles etc.) light up in this buffer. - using var leftWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(document.Project.Solution)); - leftWorkspace.Target.OpenDocument(document.Id, oldBuffer.AsTextContainer()); + // Create PreviewWorkspace around the buffer to be displayed in the diff preview + // so that all IDE services (colorizer, squiggles etc.) light up in this buffer. + using var leftWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(document.Project.Solution)); + leftWorkspace.Target.OpenDocument(document.Id, oldBuffer.AsTextContainer()); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - return await CreateRemovedDocumentPreviewViewCoreAsync(oldBuffer, leftWorkspace, document, zoomLevel, cancellationToken); + return await CreateRemovedDocumentPreviewViewCoreAsync(oldBuffer, leftWorkspace, document, zoomLevel, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - } + } - public Task> CreateRemovedDocumentPreviewViewAsync(Document document, double zoomLevel, CancellationToken cancellationToken) - { - return CreateRemovedTextDocumentPreviewViewAsync( - document, zoomLevel, - createBufferAsync: (textDocument, cancellationToken) => CreateNewBufferAsync(textDocument, cancellationToken), - cancellationToken); - } + public Task> CreateRemovedDocumentPreviewViewAsync(Document document, double zoomLevel, CancellationToken cancellationToken) + { + return CreateRemovedTextDocumentPreviewViewAsync( + document, zoomLevel, + createBufferAsync: (textDocument, cancellationToken) => CreateNewBufferAsync(textDocument, cancellationToken), + cancellationToken); + } - public Task> CreateRemovedAdditionalDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken) - { - return CreateRemovedTextDocumentPreviewViewAsync( - document, zoomLevel, - createBufferAsync: CreateNewPlainTextBufferAsync, - cancellationToken); - } + public Task> CreateRemovedAdditionalDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken) + { + return CreateRemovedTextDocumentPreviewViewAsync( + document, zoomLevel, + createBufferAsync: CreateNewPlainTextBufferAsync, + cancellationToken); + } - public Task> CreateRemovedAnalyzerConfigDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken) - { - return CreateRemovedTextDocumentPreviewViewAsync( - document, zoomLevel, - createBufferAsync: CreateNewPlainTextBufferAsync, - cancellationToken); - } + public Task> CreateRemovedAnalyzerConfigDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken) + { + return CreateRemovedTextDocumentPreviewViewAsync( + document, zoomLevel, + createBufferAsync: CreateNewPlainTextBufferAsync, + cancellationToken); + } - public Task?> CreateChangedDocumentPreviewViewAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken) - => CreateChangedDocumentPreviewViewAsync(oldDocument, newDocument, DefaultZoomLevel, cancellationToken); + public Task?> CreateChangedDocumentPreviewViewAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken) + => CreateChangedDocumentPreviewViewAsync(oldDocument, newDocument, DefaultZoomLevel, cancellationToken); - public async Task?> CreateChangedDocumentPreviewViewAsync(Document oldDocument, Document newDocument, double zoomLevel, CancellationToken cancellationToken) - { - // CreateNewBufferAsync must be called from the main thread - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - - // Note: We don't use the original buffer that is associated with oldDocument - // (and currently open in the editor) for oldBuffer below. This is because oldBuffer - // will be used inside a projection buffer inside our inline diff preview below - // and platform's implementation currently has a bug where projection buffers - // are being leaked. This leak means that if we use the original buffer that is - // currently visible in the editor here, the projection buffer span calculation - // would be triggered every time user changes some code in this buffer (even though - // the diff view would long have been dismissed by the time user edits the code) - // resulting in crashes. Instead we create a new buffer from the same content. - // TODO: We could use ITextBufferCloneService instead here to clone the original buffer. + public async Task?> CreateChangedDocumentPreviewViewAsync(Document oldDocument, Document newDocument, double zoomLevel, CancellationToken cancellationToken) + { + // CreateNewBufferAsync must be called from the main thread + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + // Note: We don't use the original buffer that is associated with oldDocument + // (and currently open in the editor) for oldBuffer below. This is because oldBuffer + // will be used inside a projection buffer inside our inline diff preview below + // and platform's implementation currently has a bug where projection buffers + // are being leaked. This leak means that if we use the original buffer that is + // currently visible in the editor here, the projection buffer span calculation + // would be triggered every time user changes some code in this buffer (even though + // the diff view would long have been dismissed by the time user edits the code) + // resulting in crashes. Instead we create a new buffer from the same content. + // TODO: We could use ITextBufferCloneService instead here to clone the original buffer. #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - var oldBuffer = await CreateNewBufferAsync(oldDocument, cancellationToken); - var newBuffer = await CreateNewBufferAsync(newDocument, cancellationToken); + var oldBuffer = await CreateNewBufferAsync(oldDocument, cancellationToken); + var newBuffer = await CreateNewBufferAsync(newDocument, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - // Convert the diffs to be line based. - // Compute the diffs between the old text and the new. - var diffResult = ComputeEditDifferences(oldDocument, newDocument, cancellationToken); + // Convert the diffs to be line based. + // Compute the diffs between the old text and the new. + var diffResult = ComputeEditDifferences(oldDocument, newDocument, cancellationToken); - // Need to show the spans in the right that are different. - // We also need to show the spans that are in conflict. - var originalSpans = GetOriginalSpans(diffResult, cancellationToken); - var changedSpans = GetChangedSpans(diffResult, cancellationToken); - string? description = null; - NormalizedSpanCollection allSpans; + // Need to show the spans in the right that are different. + // We also need to show the spans that are in conflict. + var originalSpans = GetOriginalSpans(diffResult, cancellationToken); + var changedSpans = GetChangedSpans(diffResult, cancellationToken); + string? description = null; + NormalizedSpanCollection allSpans; - if (newDocument.SupportsSyntaxTree) - { + if (newDocument.SupportsSyntaxTree) + { #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - var newRoot = await newDocument.GetRequiredSyntaxRootAsync(cancellationToken); + var newRoot = await newDocument.GetRequiredSyntaxRootAsync(cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - var conflictNodes = newRoot.GetAnnotatedNodesAndTokens(ConflictAnnotation.Kind); - var conflictSpans = conflictNodes.Select(n => n.Span.ToSpan()).ToList(); - var conflictDescriptions = conflictNodes.SelectMany(n => n.GetAnnotations(ConflictAnnotation.Kind)) - .Select(a => $"❌ {ConflictAnnotation.GetDescription(a)}") - .Distinct(); - - var warningNodes = newRoot.GetAnnotatedNodesAndTokens(WarningAnnotation.Kind); - var warningSpans = warningNodes.Select(n => n.Span.ToSpan()).ToList(); - var warningDescriptions = warningNodes.SelectMany(n => n.GetAnnotations(WarningAnnotation.Kind)) - .Select(a => $"⚠ {WarningAnnotation.GetDescription(a)}") - .Distinct(); - - var suppressDiagnosticsNodes = newRoot.GetAnnotatedNodesAndTokens(SuppressDiagnosticsAnnotation.Kind); - var suppressDiagnosticsSpans = suppressDiagnosticsNodes.Select(n => n.Span.ToSpan()).ToList(); - AttachAnnotationsToBuffer(newBuffer, conflictSpans, warningSpans, suppressDiagnosticsSpans); - - description = conflictSpans.Count == 0 && warningSpans.Count == 0 - ? null - : string.Join(Environment.NewLine, conflictDescriptions.Concat(warningDescriptions)); - allSpans = new NormalizedSpanCollection(conflictSpans.Concat(warningSpans).Concat(changedSpans)); - } - else - { - allSpans = new NormalizedSpanCollection(changedSpans); - } + var conflictNodes = newRoot.GetAnnotatedNodesAndTokens(ConflictAnnotation.Kind); + var conflictSpans = conflictNodes.Select(n => n.Span.ToSpan()).ToList(); + var conflictDescriptions = conflictNodes.SelectMany(n => n.GetAnnotations(ConflictAnnotation.Kind)) + .Select(a => $"❌ {ConflictAnnotation.GetDescription(a)}") + .Distinct(); + + var warningNodes = newRoot.GetAnnotatedNodesAndTokens(WarningAnnotation.Kind); + var warningSpans = warningNodes.Select(n => n.Span.ToSpan()).ToList(); + var warningDescriptions = warningNodes.SelectMany(n => n.GetAnnotations(WarningAnnotation.Kind)) + .Select(a => $"⚠ {WarningAnnotation.GetDescription(a)}") + .Distinct(); + + var suppressDiagnosticsNodes = newRoot.GetAnnotatedNodesAndTokens(SuppressDiagnosticsAnnotation.Kind); + var suppressDiagnosticsSpans = suppressDiagnosticsNodes.Select(n => n.Span.ToSpan()).ToList(); + AttachAnnotationsToBuffer(newBuffer, conflictSpans, warningSpans, suppressDiagnosticsSpans); + + description = conflictSpans.Count == 0 && warningSpans.Count == 0 + ? null + : string.Join(Environment.NewLine, conflictDescriptions.Concat(warningDescriptions)); + allSpans = new NormalizedSpanCollection(conflictSpans.Concat(warningSpans).Concat(changedSpans)); + } + else + { + allSpans = new NormalizedSpanCollection(changedSpans); + } - var originalLineSpans = CreateLineSpans(oldBuffer.CurrentSnapshot, originalSpans, cancellationToken); - var changedLineSpans = CreateLineSpans(newBuffer.CurrentSnapshot, allSpans, cancellationToken); - if (!originalLineSpans.Any()) - { - // This means that we have no differences (likely because of conflicts). - // In such cases, use the same spans for the left (old) buffer as the right (new) buffer. - originalLineSpans = changedLineSpans; - } + var originalLineSpans = CreateLineSpans(oldBuffer.CurrentSnapshot, originalSpans, cancellationToken); + var changedLineSpans = CreateLineSpans(newBuffer.CurrentSnapshot, allSpans, cancellationToken); + if (!originalLineSpans.Any()) + { + // This means that we have no differences (likely because of conflicts). + // In such cases, use the same spans for the left (old) buffer as the right (new) buffer. + originalLineSpans = changedLineSpans; + } - // Create PreviewWorkspaces around the buffers to be displayed on the left and right - // so that all IDE services (colorizer, squiggles etc.) light up in these buffers. - // - // Performance: Replace related documents to oldBuffer and newBuffer in these workspaces with the - // relating SourceText. This prevents cascading forks as taggers call to - // GetOpenTextDocumentInCurrentContextWithChanges would eventually wind up - // calling Solution.WithDocumentText using the related ids. - var leftSolution = oldDocument.Project.Solution; - var allLeftIds = leftSolution.GetRelatedDocumentIds(oldDocument.Id); - leftSolution = leftSolution.WithDocumentText(allLeftIds, oldBuffer.AsTextContainer().CurrentText, PreservationMode.PreserveIdentity); + // Create PreviewWorkspaces around the buffers to be displayed on the left and right + // so that all IDE services (colorizer, squiggles etc.) light up in these buffers. + // + // Performance: Replace related documents to oldBuffer and newBuffer in these workspaces with the + // relating SourceText. This prevents cascading forks as taggers call to + // GetOpenTextDocumentInCurrentContextWithChanges would eventually wind up + // calling Solution.WithDocumentText using the related ids. + var leftSolution = oldDocument.Project.Solution; + var allLeftIds = leftSolution.GetRelatedDocumentIds(oldDocument.Id); + leftSolution = leftSolution.WithDocumentText(allLeftIds, oldBuffer.AsTextContainer().CurrentText, PreservationMode.PreserveIdentity); - using var leftWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(leftSolution)); - leftWorkspace.Target.OpenDocument(oldDocument.Id, oldBuffer.AsTextContainer()); + using var leftWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(leftSolution)); + leftWorkspace.Target.OpenDocument(oldDocument.Id, oldBuffer.AsTextContainer()); - var rightSolution = newDocument.Project.Solution; - var allRightIds = rightSolution.GetRelatedDocumentIds(newDocument.Id); - rightSolution = rightSolution.WithDocumentText(allRightIds, newBuffer.AsTextContainer().CurrentText, PreservationMode.PreserveIdentity); + var rightSolution = newDocument.Project.Solution; + var allRightIds = rightSolution.GetRelatedDocumentIds(newDocument.Id); + rightSolution = rightSolution.WithDocumentText(allRightIds, newBuffer.AsTextContainer().CurrentText, PreservationMode.PreserveIdentity); - using var rightWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(rightSolution)); - rightWorkspace.Target.OpenDocument(newDocument.Id, newBuffer.AsTextContainer()); + using var rightWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(rightSolution)); + rightWorkspace.Target.OpenDocument(newDocument.Id, newBuffer.AsTextContainer()); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - return await CreateChangedDocumentViewAsync( - oldBuffer, newBuffer, description, originalLineSpans, changedLineSpans, - leftWorkspace, rightWorkspace, zoomLevel, cancellationToken); + return await CreateChangedDocumentViewAsync( + oldBuffer, newBuffer, description, originalLineSpans, changedLineSpans, + leftWorkspace, rightWorkspace, zoomLevel, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - } + } - // NOTE: We are only sharing this code between additional documents and analyzer config documents, - // which are essentially plain text documents. Regular source documents need special handling - // and hence have a different implementation. - private async Task?> CreateChangedAdditionalOrAnalyzerConfigDocumentPreviewViewAsync( - TextDocument oldDocument, - TextDocument newDocument, - double zoomLevel, - CancellationToken cancellationToken) - { - Debug.Assert(oldDocument.Kind is TextDocumentKind.AdditionalDocument or TextDocumentKind.AnalyzerConfigDocument); - - // openTextDocument must be called from the main thread - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - - // Note: We don't use the original buffer that is associated with oldDocument - // (and currently open in the editor) for oldBuffer below. This is because oldBuffer - // will be used inside a projection buffer inside our inline diff preview below - // and platform's implementation currently has a bug where projection buffers - // are being leaked. This leak means that if we use the original buffer that is - // currently visible in the editor here, the projection buffer span calculation - // would be triggered every time user changes some code in this buffer (even though - // the diff view would long have been dismissed by the time user edits the code) - // resulting in crashes. Instead we create a new buffer from the same content. - // TODO: We could use ITextBufferCloneService instead here to clone the original buffer. + // NOTE: We are only sharing this code between additional documents and analyzer config documents, + // which are essentially plain text documents. Regular source documents need special handling + // and hence have a different implementation. + private async Task?> CreateChangedAdditionalOrAnalyzerConfigDocumentPreviewViewAsync( + TextDocument oldDocument, + TextDocument newDocument, + double zoomLevel, + CancellationToken cancellationToken) + { + Debug.Assert(oldDocument.Kind is TextDocumentKind.AdditionalDocument or TextDocumentKind.AnalyzerConfigDocument); + + // openTextDocument must be called from the main thread + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + // Note: We don't use the original buffer that is associated with oldDocument + // (and currently open in the editor) for oldBuffer below. This is because oldBuffer + // will be used inside a projection buffer inside our inline diff preview below + // and platform's implementation currently has a bug where projection buffers + // are being leaked. This leak means that if we use the original buffer that is + // currently visible in the editor here, the projection buffer span calculation + // would be triggered every time user changes some code in this buffer (even though + // the diff view would long have been dismissed by the time user edits the code) + // resulting in crashes. Instead we create a new buffer from the same content. + // TODO: We could use ITextBufferCloneService instead here to clone the original buffer. #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - var oldBuffer = await CreateNewPlainTextBufferAsync(oldDocument, cancellationToken); - var newBuffer = await CreateNewPlainTextBufferAsync(newDocument, cancellationToken); + var oldBuffer = await CreateNewPlainTextBufferAsync(oldDocument, cancellationToken); + var newBuffer = await CreateNewPlainTextBufferAsync(newDocument, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - // Convert the diffs to be line based. - // Compute the diffs between the old text and the new. - var diffResult = ComputeEditDifferences(oldDocument, newDocument, cancellationToken); + // Convert the diffs to be line based. + // Compute the diffs between the old text and the new. + var diffResult = ComputeEditDifferences(oldDocument, newDocument, cancellationToken); - // Need to show the spans in the right that are different. - var originalSpans = GetOriginalSpans(diffResult, cancellationToken); - var changedSpans = GetChangedSpans(diffResult, cancellationToken); + // Need to show the spans in the right that are different. + var originalSpans = GetOriginalSpans(diffResult, cancellationToken); + var changedSpans = GetChangedSpans(diffResult, cancellationToken); - var originalLineSpans = CreateLineSpans(oldBuffer.CurrentSnapshot, originalSpans, cancellationToken); - var changedLineSpans = CreateLineSpans(newBuffer.CurrentSnapshot, changedSpans, cancellationToken); + var originalLineSpans = CreateLineSpans(oldBuffer.CurrentSnapshot, originalSpans, cancellationToken); + var changedLineSpans = CreateLineSpans(newBuffer.CurrentSnapshot, changedSpans, cancellationToken); - // TODO: Why aren't we attaching conflict / warning annotations here like we do for regular documents above? + // TODO: Why aren't we attaching conflict / warning annotations here like we do for regular documents above? - // Create PreviewWorkspaces around the buffers to be displayed on the left and right - // so that all IDE services (colorizer, squiggles etc.) light up in these buffers. - using var leftWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(oldDocument.Project.Solution)); - leftWorkspace.Target.OpenDocument(oldDocument.Id, oldBuffer.AsTextContainer()); + // Create PreviewWorkspaces around the buffers to be displayed on the left and right + // so that all IDE services (colorizer, squiggles etc.) light up in these buffers. + using var leftWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(oldDocument.Project.Solution)); + leftWorkspace.Target.OpenDocument(oldDocument.Id, oldBuffer.AsTextContainer()); - using var rightWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(newDocument.Project.Solution)); - rightWorkspace.Target.OpenDocument(newDocument.Id, newBuffer.AsTextContainer()); + using var rightWorkspace = new ReferenceCountedDisposable(new PreviewWorkspace(newDocument.Project.Solution)); + rightWorkspace.Target.OpenDocument(newDocument.Id, newBuffer.AsTextContainer()); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - return await CreateChangedDocumentViewAsync( - oldBuffer, newBuffer, description: null, originalLineSpans, changedLineSpans, - leftWorkspace, rightWorkspace, zoomLevel, cancellationToken); + return await CreateChangedDocumentViewAsync( + oldBuffer, newBuffer, description: null, originalLineSpans, changedLineSpans, + leftWorkspace, rightWorkspace, zoomLevel, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - } + } - public Task?> CreateChangedAdditionalDocumentPreviewViewAsync(TextDocument oldDocument, TextDocument newDocument, double zoomLevel, CancellationToken cancellationToken) - { - return CreateChangedAdditionalOrAnalyzerConfigDocumentPreviewViewAsync( - oldDocument, newDocument, zoomLevel, cancellationToken); - } + public Task?> CreateChangedAdditionalDocumentPreviewViewAsync(TextDocument oldDocument, TextDocument newDocument, double zoomLevel, CancellationToken cancellationToken) + { + return CreateChangedAdditionalOrAnalyzerConfigDocumentPreviewViewAsync( + oldDocument, newDocument, zoomLevel, cancellationToken); + } - public Task?> CreateChangedAnalyzerConfigDocumentPreviewViewAsync(TextDocument oldDocument, TextDocument newDocument, double zoomLevel, CancellationToken cancellationToken) - { - return CreateChangedAdditionalOrAnalyzerConfigDocumentPreviewViewAsync( - oldDocument, newDocument, zoomLevel, cancellationToken); - } + public Task?> CreateChangedAnalyzerConfigDocumentPreviewViewAsync(TextDocument oldDocument, TextDocument newDocument, double zoomLevel, CancellationToken cancellationToken) + { + return CreateChangedAdditionalOrAnalyzerConfigDocumentPreviewViewAsync( + oldDocument, newDocument, zoomLevel, cancellationToken); + } - private async ValueTask?> CreateChangedDocumentViewAsync(ITextBuffer oldBuffer, ITextBuffer newBuffer, string? description, - List originalSpans, List changedSpans, ReferenceCountedDisposable leftWorkspace, ReferenceCountedDisposable rightWorkspace, - double zoomLevel, CancellationToken cancellationToken) + private async ValueTask?> CreateChangedDocumentViewAsync(ITextBuffer oldBuffer, ITextBuffer newBuffer, string? description, + List originalSpans, List changedSpans, ReferenceCountedDisposable leftWorkspace, ReferenceCountedDisposable rightWorkspace, + double zoomLevel, CancellationToken cancellationToken) + { + if (!(originalSpans.Any() && changedSpans.Any())) { - if (!(originalSpans.Any() && changedSpans.Any())) - { - // Both line spans must be non-empty. Otherwise, below projection buffer factory API call will throw. - // So if either is empty (signaling that there are no changes to preview in the document), then we bail out. - // This can happen in cases where the user has already applied the fix and light bulb has already been dismissed, - // but platform hasn't cancelled the preview operation yet. Since the light bulb has already been dismissed at - // this point, the preview that we return will never be displayed to the user. So returning null here is harmless. - - // TODO: understand how this can even happen. The diff input is stable -- we shouldn't be depending on some sort of - // state that could change underneath us. If we know the file changed, how would we discover here it didn't? - return null; - } + // Both line spans must be non-empty. Otherwise, below projection buffer factory API call will throw. + // So if either is empty (signaling that there are no changes to preview in the document), then we bail out. + // This can happen in cases where the user has already applied the fix and light bulb has already been dismissed, + // but platform hasn't cancelled the preview operation yet. Since the light bulb has already been dismissed at + // this point, the preview that we return will never be displayed to the user. So returning null here is harmless. + + // TODO: understand how this can even happen. The diff input is stable -- we shouldn't be depending on some sort of + // state that could change underneath us. If we know the file changed, how would we discover here it didn't? + return null; + } - // IProjectionBufferFactoryService is a Visual Studio API which is not documented as free-threaded - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - - var originalBuffer = _projectionBufferFactoryService.CreateProjectionBufferWithoutIndentation( - _contentTypeRegistryService, - _editorOptionsService.Factory.GlobalOptions, - oldBuffer.CurrentSnapshot, - "...", - description, - originalSpans.ToArray()); - - var changedBuffer = _projectionBufferFactoryService.CreateProjectionBufferWithoutIndentation( - _contentTypeRegistryService, - _editorOptionsService.Factory.GlobalOptions, - newBuffer.CurrentSnapshot, - "...", - description, - changedSpans.ToArray()); + // IProjectionBufferFactoryService is a Visual Studio API which is not documented as free-threaded + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + var originalBuffer = _projectionBufferFactoryService.CreateProjectionBufferWithoutIndentation( + _contentTypeRegistryService, + _editorOptionsService.Factory.GlobalOptions, + oldBuffer.CurrentSnapshot, + "...", + description, + originalSpans.ToArray()); + + var changedBuffer = _projectionBufferFactoryService.CreateProjectionBufferWithoutIndentation( + _contentTypeRegistryService, + _editorOptionsService.Factory.GlobalOptions, + newBuffer.CurrentSnapshot, + "...", + description, + changedSpans.ToArray()); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - return await CreateNewDifferenceViewerAsync(leftWorkspace, rightWorkspace, originalBuffer, changedBuffer, zoomLevel, cancellationToken); + return await CreateNewDifferenceViewerAsync(leftWorkspace, rightWorkspace, originalBuffer, changedBuffer, zoomLevel, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - } + } - private static void AttachAnnotationsToBuffer( - ITextBuffer newBuffer, IEnumerable conflictSpans, IEnumerable warningSpans, IEnumerable suppressDiagnosticsSpans) - { - // Attach the spans to the buffer. - newBuffer.Properties.AddProperty(PredefinedPreviewTaggerKeys.ConflictSpansKey, new NormalizedSnapshotSpanCollection(newBuffer.CurrentSnapshot, conflictSpans)); - newBuffer.Properties.AddProperty(PredefinedPreviewTaggerKeys.WarningSpansKey, new NormalizedSnapshotSpanCollection(newBuffer.CurrentSnapshot, warningSpans)); - newBuffer.Properties.AddProperty(PredefinedPreviewTaggerKeys.SuppressDiagnosticsSpansKey, new NormalizedSnapshotSpanCollection(newBuffer.CurrentSnapshot, suppressDiagnosticsSpans)); - } + private static void AttachAnnotationsToBuffer( + ITextBuffer newBuffer, IEnumerable conflictSpans, IEnumerable warningSpans, IEnumerable suppressDiagnosticsSpans) + { + // Attach the spans to the buffer. + newBuffer.Properties.AddProperty(PredefinedPreviewTaggerKeys.ConflictSpansKey, new NormalizedSnapshotSpanCollection(newBuffer.CurrentSnapshot, conflictSpans)); + newBuffer.Properties.AddProperty(PredefinedPreviewTaggerKeys.WarningSpansKey, new NormalizedSnapshotSpanCollection(newBuffer.CurrentSnapshot, warningSpans)); + newBuffer.Properties.AddProperty(PredefinedPreviewTaggerKeys.SuppressDiagnosticsSpansKey, new NormalizedSnapshotSpanCollection(newBuffer.CurrentSnapshot, suppressDiagnosticsSpans)); + } - private async ValueTask CreateNewBufferAsync(Document document, CancellationToken cancellationToken) - { - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + private async ValueTask CreateNewBufferAsync(Document document, CancellationToken cancellationToken) + { + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var contentTypeService = document.GetRequiredLanguageService(); - var contentType = contentTypeService.GetDefaultContentType(); + var contentTypeService = document.GetRequiredLanguageService(); + var contentType = contentTypeService.GetDefaultContentType(); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - return await CreateTextBufferCoreAsync(document, contentType, cancellationToken); + return await CreateTextBufferCoreAsync(document, contentType, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - } + } - private async ValueTask CreateNewPlainTextBufferAsync(TextDocument document, CancellationToken cancellationToken) - { - // ITextBufferFactoryService is a Visual Studio API which is not documented as free-threaded - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + private async ValueTask CreateNewPlainTextBufferAsync(TextDocument document, CancellationToken cancellationToken) + { + // ITextBufferFactoryService is a Visual Studio API which is not documented as free-threaded + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var contentType = _textBufferFactoryService.TextContentType; + var contentType = _textBufferFactoryService.TextContentType; #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - return await CreateTextBufferCoreAsync(document, contentType, cancellationToken); + return await CreateTextBufferCoreAsync(document, contentType, cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - } + } - private async ValueTask CreateTextBufferCoreAsync(TextDocument document, IContentType? contentType, CancellationToken cancellationToken) - { - ThreadingContext.ThrowIfNotOnUIThread(); + private async ValueTask CreateTextBufferCoreAsync(TextDocument document, IContentType? contentType, CancellationToken cancellationToken) + { + ThreadingContext.ThrowIfNotOnUIThread(); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) - var text = await document.State.GetTextAsync(cancellationToken); + var text = await document.State.GetTextAsync(cancellationToken); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - var buffer = _textBufferFactoryService.CreateTextBuffer(text.ToString(), contentType); + var buffer = _textBufferFactoryService.CreateTextBuffer(text.ToString(), contentType); - // Associate buffer with a text document with random file path to satisfy extensibility points expecting absolute file path. - _textDocumentFactoryService.CreateTextDocument(buffer, Path.GetTempFileName()); + // Associate buffer with a text document with random file path to satisfy extensibility points expecting absolute file path. + _textDocumentFactoryService.CreateTextDocument(buffer, Path.GetTempFileName()); - return buffer; - } - - protected abstract IDifferenceViewerPreview CreateDifferenceViewerPreview(TDifferenceViewer viewer); - protected abstract Task CreateDifferenceViewAsync(IDifferenceBuffer diffBuffer, ITextViewRoleSet previewRoleSet, DifferenceViewMode mode, double zoomLevel, CancellationToken cancellationToken); + return buffer; + } - private async ValueTask> CreateNewDifferenceViewerAsync( - ReferenceCountedDisposable? leftWorkspace, ReferenceCountedDisposable? rightWorkspace, - IProjectionBuffer originalBuffer, IProjectionBuffer changedBuffer, - double zoomLevel, CancellationToken cancellationToken) - { - // IWpfDifferenceViewerFactoryService is a Visual Studio API which is not documented as free-threaded - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + protected abstract IDifferenceViewerPreview CreateDifferenceViewerPreview(TDifferenceViewer viewer); + protected abstract Task CreateDifferenceViewAsync(IDifferenceBuffer diffBuffer, ITextViewRoleSet previewRoleSet, DifferenceViewMode mode, double zoomLevel, CancellationToken cancellationToken); - // leftWorkspace can be null if the change is adding a document. - // rightWorkspace can be null if the change is removing a document. - // However both leftWorkspace and rightWorkspace can't be null at the same time. - Contract.ThrowIfTrue((leftWorkspace == null) && (rightWorkspace == null)); + private async ValueTask> CreateNewDifferenceViewerAsync( + ReferenceCountedDisposable? leftWorkspace, ReferenceCountedDisposable? rightWorkspace, + IProjectionBuffer originalBuffer, IProjectionBuffer changedBuffer, + double zoomLevel, CancellationToken cancellationToken) + { + // IWpfDifferenceViewerFactoryService is a Visual Studio API which is not documented as free-threaded + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var diffBuffer = _differenceBufferService.CreateDifferenceBuffer( - originalBuffer, changedBuffer, - new StringDifferenceOptions(), disableEditing: true); + // leftWorkspace can be null if the change is adding a document. + // rightWorkspace can be null if the change is removing a document. + // However both leftWorkspace and rightWorkspace can't be null at the same time. + Contract.ThrowIfTrue((leftWorkspace == null) && (rightWorkspace == null)); - var mode = leftWorkspace == null ? DifferenceViewMode.RightViewOnly : - rightWorkspace == null ? DifferenceViewMode.LeftViewOnly : - DifferenceViewMode.Inline; + var diffBuffer = _differenceBufferService.CreateDifferenceBuffer( + originalBuffer, changedBuffer, + new StringDifferenceOptions(), disableEditing: true); - var diffViewer = await CreateDifferenceViewAsync(diffBuffer, _previewRoleSet, mode, zoomLevel, cancellationToken).ConfigureAwait(true); + var mode = leftWorkspace == null ? DifferenceViewMode.RightViewOnly : + rightWorkspace == null ? DifferenceViewMode.LeftViewOnly : + DifferenceViewMode.Inline; - // Claim ownership of the workspace references - leftWorkspace = leftWorkspace?.TryAddReference(); - rightWorkspace = rightWorkspace?.TryAddReference(); + var diffViewer = await CreateDifferenceViewAsync(diffBuffer, _previewRoleSet, mode, zoomLevel, cancellationToken).ConfigureAwait(true); - diffViewer.Closed += (s, e) => - { - // Workaround Editor bug. The editor has an issue where they sometimes crash when - // trying to apply changes to projection buffer. So, when the user actually invokes - // a SuggestedAction we may then edit a text buffer, which the editor will then - // try to propagate through the projections we have here over that buffer. To ensure - // that that doesn't happen, we clear out the projections first so that this crash - // won't happen. - originalBuffer.DeleteSpans(0, originalBuffer.CurrentSnapshot.SpanCount); - changedBuffer.DeleteSpans(0, changedBuffer.CurrentSnapshot.SpanCount); - - leftWorkspace?.Dispose(); - leftWorkspace = null; - - rightWorkspace?.Dispose(); - rightWorkspace = null; - }; - - if (_editorOptionsService.GlobalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler)) - { - leftWorkspace?.Target.EnableSolutionCrawler(); - rightWorkspace?.Target.EnableSolutionCrawler(); - } + // Claim ownership of the workspace references + leftWorkspace = leftWorkspace?.TryAddReference(); + rightWorkspace = rightWorkspace?.TryAddReference(); - return CreateDifferenceViewerPreview(diffViewer); + diffViewer.Closed += (s, e) => + { + // Workaround Editor bug. The editor has an issue where they sometimes crash when + // trying to apply changes to projection buffer. So, when the user actually invokes + // a SuggestedAction we may then edit a text buffer, which the editor will then + // try to propagate through the projections we have here over that buffer. To ensure + // that that doesn't happen, we clear out the projections first so that this crash + // won't happen. + originalBuffer.DeleteSpans(0, originalBuffer.CurrentSnapshot.SpanCount); + changedBuffer.DeleteSpans(0, changedBuffer.CurrentSnapshot.SpanCount); + + leftWorkspace?.Dispose(); + leftWorkspace = null; + + rightWorkspace?.Dispose(); + rightWorkspace = null; + }; + + if (_editorOptionsService.GlobalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler)) + { + leftWorkspace?.Target.EnableSolutionCrawler(); + rightWorkspace?.Target.EnableSolutionCrawler(); } - private static List CreateLineSpans(ITextSnapshot textSnapshot, NormalizedSpanCollection allSpans, CancellationToken cancellationToken) + return CreateDifferenceViewerPreview(diffViewer); + } + + private static List CreateLineSpans(ITextSnapshot textSnapshot, NormalizedSpanCollection allSpans, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var result = new List(); + + foreach (var span in allSpans) { cancellationToken.ThrowIfCancellationRequested(); - var result = new List(); + var lineSpan = GetLineSpan(textSnapshot, span); + MergeLineSpans(result, lineSpan); + } - foreach (var span in allSpans) - { - cancellationToken.ThrowIfCancellationRequested(); + return result; + } - var lineSpan = GetLineSpan(textSnapshot, span); - MergeLineSpans(result, lineSpan); - } + // Find the lines that surround the span of the difference. Try to expand the span to + // include both the previous and next lines so that we can show more context to the + // user. + private static LineSpan GetLineSpan( + ITextSnapshot snapshot, + Span span) + { + var startLine = snapshot.GetLineNumberFromPosition(span.Start); + var endLine = snapshot.GetLineNumberFromPosition(span.End); - return result; + if (startLine > 0) + { + startLine--; } - // Find the lines that surround the span of the difference. Try to expand the span to - // include both the previous and next lines so that we can show more context to the - // user. - private static LineSpan GetLineSpan( - ITextSnapshot snapshot, - Span span) + if (endLine < snapshot.LineCount) { - var startLine = snapshot.GetLineNumberFromPosition(span.Start); - var endLine = snapshot.GetLineNumberFromPosition(span.End); + endLine++; + } - if (startLine > 0) - { - startLine--; - } + return LineSpan.FromBounds(startLine, endLine); + } - if (endLine < snapshot.LineCount) + // Adds a line span to the spans we've been collecting. If the line span overlaps or + // abuts a previous span then the two are merged. + private static void MergeLineSpans(List lineSpans, LineSpan nextLineSpan) + { + if (lineSpans.Count > 0) + { + var lastLineSpan = lineSpans.Last(); + + // We merge them if there's no more than one line between the two. Otherwise + // we'd show "..." between two spans where we could just show the actual code. + if (nextLineSpan.Start >= lastLineSpan.Start && nextLineSpan.Start <= (lastLineSpan.End + 1)) { - endLine++; + nextLineSpan = LineSpan.FromBounds(lastLineSpan.Start, nextLineSpan.End); + lineSpans.RemoveAt(lineSpans.Count - 1); } - - return LineSpan.FromBounds(startLine, endLine); } - // Adds a line span to the spans we've been collecting. If the line span overlaps or - // abuts a previous span then the two are merged. - private static void MergeLineSpans(List lineSpans, LineSpan nextLineSpan) - { - if (lineSpans.Count > 0) - { - var lastLineSpan = lineSpans.Last(); - - // We merge them if there's no more than one line between the two. Otherwise - // we'd show "..." between two spans where we could just show the actual code. - if (nextLineSpan.Start >= lastLineSpan.Start && nextLineSpan.Start <= (lastLineSpan.End + 1)) - { - nextLineSpan = LineSpan.FromBounds(lastLineSpan.Start, nextLineSpan.End); - lineSpans.RemoveAt(lineSpans.Count - 1); - } - } + lineSpans.Add(nextLineSpan); + } - lineSpans.Add(nextLineSpan); - } + private IHierarchicalDifferenceCollection ComputeEditDifferences(TextDocument oldDocument, TextDocument newDocument, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - private IHierarchicalDifferenceCollection ComputeEditDifferences(TextDocument oldDocument, TextDocument newDocument, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + // Get the text that's actually in the editor. + var oldText = oldDocument.GetTextSynchronously(cancellationToken); + var newText = newDocument.GetTextSynchronously(cancellationToken); - // Get the text that's actually in the editor. - var oldText = oldDocument.GetTextSynchronously(cancellationToken); - var newText = newDocument.GetTextSynchronously(cancellationToken); + // Defer to the editor to figure out what changes the client made. + var diffService = _differenceSelectorService.GetTextDifferencingService( + oldDocument.Project.Services.GetRequiredService().GetDefaultContentType()); - // Defer to the editor to figure out what changes the client made. - var diffService = _differenceSelectorService.GetTextDifferencingService( - oldDocument.Project.Services.GetRequiredService().GetDefaultContentType()); + diffService ??= _differenceSelectorService.DefaultTextDifferencingService; + return diffService.DiffStrings(oldText.ToString(), newText.ToString(), new StringDifferenceOptions() + { + DifferenceType = StringDifferenceTypes.Word | StringDifferenceTypes.Line, + }); + } - diffService ??= _differenceSelectorService.DefaultTextDifferencingService; - return diffService.DiffStrings(oldText.ToString(), newText.ToString(), new StringDifferenceOptions() - { - DifferenceType = StringDifferenceTypes.Word | StringDifferenceTypes.Line, - }); - } + private static NormalizedSpanCollection GetOriginalSpans(IHierarchicalDifferenceCollection diffResult, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var lineSpans = new List(); - private static NormalizedSpanCollection GetOriginalSpans(IHierarchicalDifferenceCollection diffResult, CancellationToken cancellationToken) + foreach (var difference in diffResult) { cancellationToken.ThrowIfCancellationRequested(); - var lineSpans = new List(); + var mappedSpan = diffResult.LeftDecomposition.GetSpanInOriginal(difference.Left); + lineSpans.Add(mappedSpan); + } - foreach (var difference in diffResult) - { - cancellationToken.ThrowIfCancellationRequested(); - var mappedSpan = diffResult.LeftDecomposition.GetSpanInOriginal(difference.Left); - lineSpans.Add(mappedSpan); - } + return new NormalizedSpanCollection(lineSpans); + } - return new NormalizedSpanCollection(lineSpans); - } + private static NormalizedSpanCollection GetChangedSpans(IHierarchicalDifferenceCollection diffResult, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var lineSpans = new List(); - private static NormalizedSpanCollection GetChangedSpans(IHierarchicalDifferenceCollection diffResult, CancellationToken cancellationToken) + foreach (var difference in diffResult) { cancellationToken.ThrowIfCancellationRequested(); - var lineSpans = new List(); - - foreach (var difference in diffResult) - { - cancellationToken.ThrowIfCancellationRequested(); - var mappedSpan = diffResult.RightDecomposition.GetSpanInOriginal(difference.Right); - lineSpans.Add(mappedSpan); - } - - return new NormalizedSpanCollection(lineSpans); + var mappedSpan = diffResult.RightDecomposition.GetSpanInOriginal(difference.Right); + lineSpans.Add(mappedSpan); } + + return new NormalizedSpanCollection(lineSpans); } } diff --git a/src/EditorFeatures/Core/Preview/IDifferenceViewerPreview.cs b/src/EditorFeatures/Core/Preview/IDifferenceViewerPreview.cs index 50f6d2b9222ed..082f6e540b9b4 100644 --- a/src/EditorFeatures/Core/Preview/IDifferenceViewerPreview.cs +++ b/src/EditorFeatures/Core/Preview/IDifferenceViewerPreview.cs @@ -5,11 +5,10 @@ using System; using Microsoft.VisualStudio.Text.Differencing; -namespace Microsoft.CodeAnalysis.Editor.Implementation.Preview +namespace Microsoft.CodeAnalysis.Editor.Implementation.Preview; + +internal interface IDifferenceViewerPreview : IDisposable + where TDifferenceViewer : IDifferenceViewer { - internal interface IDifferenceViewerPreview : IDisposable - where TDifferenceViewer : IDifferenceViewer - { - public TDifferenceViewer Viewer { get; } - } + public TDifferenceViewer Viewer { get; } } diff --git a/src/EditorFeatures/Core/Preview/IPreviewFactoryService.cs b/src/EditorFeatures/Core/Preview/IPreviewFactoryService.cs index f751a68f987f9..e3e92afd02aac 100644 --- a/src/EditorFeatures/Core/Preview/IPreviewFactoryService.cs +++ b/src/EditorFeatures/Core/Preview/IPreviewFactoryService.cs @@ -4,11 +4,10 @@ using System.Threading; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal interface IPreviewFactoryService { - internal interface IPreviewFactoryService - { - SolutionPreviewResult? GetSolutionPreviews(Solution oldSolution, Solution newSolution, CancellationToken cancellationToken); - SolutionPreviewResult? GetSolutionPreviews(Solution oldSolution, Solution newSolution, double zoomLevel, CancellationToken cancellationToken); - } + SolutionPreviewResult? GetSolutionPreviews(Solution oldSolution, Solution newSolution, CancellationToken cancellationToken); + SolutionPreviewResult? GetSolutionPreviews(Solution oldSolution, Solution newSolution, double zoomLevel, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/Preview/SolutionChangeSummary.cs b/src/EditorFeatures/Core/Preview/SolutionChangeSummary.cs index 449fc2c3b04fb..9f00a6eba9d34 100644 --- a/src/EditorFeatures/Core/Preview/SolutionChangeSummary.cs +++ b/src/EditorFeatures/Core/Preview/SolutionChangeSummary.cs @@ -4,50 +4,49 @@ using System.Linq; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal class SolutionChangeSummary { - internal class SolutionChangeSummary - { - public readonly Solution OldSolution; - public readonly Solution NewSolution; + public readonly Solution OldSolution; + public readonly Solution NewSolution; - public readonly int TotalFilesAffected; - public readonly int TotalProjectsAffected; + public readonly int TotalFilesAffected; + public readonly int TotalProjectsAffected; - public SolutionChangeSummary(Solution oldSolution, Solution newSolution, SolutionChanges changes) - { - OldSolution = oldSolution; - NewSolution = newSolution; + public SolutionChangeSummary(Solution oldSolution, Solution newSolution, SolutionChanges changes) + { + OldSolution = oldSolution; + NewSolution = newSolution; - foreach (var p in changes.GetProjectChanges()) + foreach (var p in changes.GetProjectChanges()) + { + TotalProjectsAffected += 1; + + TotalFilesAffected += p.GetAddedDocuments().Count() + + p.GetChangedDocuments().Count() + + p.GetRemovedDocuments().Count() + + p.GetAddedAdditionalDocuments().Count() + + p.GetChangedAdditionalDocuments().Count() + + p.GetRemovedAdditionalDocuments().Count() + + p.GetAddedAnalyzerConfigDocuments().Count() + + p.GetChangedAnalyzerConfigDocuments().Count() + + p.GetRemovedAnalyzerConfigDocuments().Count(); + + if (p.GetAddedDocuments().Any() || p.GetRemovedDocuments().Any() || + p.GetAddedAdditionalDocuments().Any() || p.GetRemovedAdditionalDocuments().Any() || + p.GetAddedAnalyzerConfigDocuments().Any() || p.GetRemovedAnalyzerConfigDocuments().Any() || + p.GetAddedMetadataReferences().Any() || p.GetRemovedMetadataReferences().Any() || + p.GetAddedProjectReferences().Any() || p.GetRemovedProjectReferences().Any() || + p.GetAddedAnalyzerReferences().Any() || p.GetRemovedAnalyzerReferences().Any()) { - TotalProjectsAffected += 1; - - TotalFilesAffected += p.GetAddedDocuments().Count() + - p.GetChangedDocuments().Count() + - p.GetRemovedDocuments().Count() + - p.GetAddedAdditionalDocuments().Count() + - p.GetChangedAdditionalDocuments().Count() + - p.GetRemovedAdditionalDocuments().Count() + - p.GetAddedAnalyzerConfigDocuments().Count() + - p.GetChangedAnalyzerConfigDocuments().Count() + - p.GetRemovedAnalyzerConfigDocuments().Count(); - - if (p.GetAddedDocuments().Any() || p.GetRemovedDocuments().Any() || - p.GetAddedAdditionalDocuments().Any() || p.GetRemovedAdditionalDocuments().Any() || - p.GetAddedAnalyzerConfigDocuments().Any() || p.GetRemovedAnalyzerConfigDocuments().Any() || - p.GetAddedMetadataReferences().Any() || p.GetRemovedMetadataReferences().Any() || - p.GetAddedProjectReferences().Any() || p.GetRemovedProjectReferences().Any() || - p.GetAddedAnalyzerReferences().Any() || p.GetRemovedAnalyzerReferences().Any()) - { - TotalFilesAffected += 1; // The project file itself was affected too. - } + TotalFilesAffected += 1; // The project file itself was affected too. } + } - var totalProjectsAddedOrRemoved = changes.GetAddedProjects().Count() + changes.GetRemovedProjects().Count(); + var totalProjectsAddedOrRemoved = changes.GetAddedProjects().Count() + changes.GetRemovedProjects().Count(); - TotalFilesAffected += totalProjectsAddedOrRemoved; - TotalProjectsAffected += totalProjectsAddedOrRemoved; - } + TotalFilesAffected += totalProjectsAddedOrRemoved; + TotalProjectsAffected += totalProjectsAddedOrRemoved; } } diff --git a/src/EditorFeatures/Core/Preview/SolutionPreviewItem.cs b/src/EditorFeatures/Core/Preview/SolutionPreviewItem.cs index aa6600b88d42c..9c7c9e19342ab 100644 --- a/src/EditorFeatures/Core/Preview/SolutionPreviewItem.cs +++ b/src/EditorFeatures/Core/Preview/SolutionPreviewItem.cs @@ -7,28 +7,27 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +/// +/// Construct an instance of +/// +/// for the that contains the content being visualized in the supplied +/// for the being visualized in the supplied +/// Lazily instantiated preview content. +/// Use lazy instantiation to ensure that any that may be present inside a given preview are only instantiated at the point +/// when the VS lightbulb requests that preview. Otherwise, we could end up instantiating a bunch of s most of which will never get +/// passed to the VS lightbulb. Such zombie s will never get closed and we will end up leaking memory. +internal class SolutionPreviewItem(ProjectId? projectId, DocumentId? documentId, Func> lazyPreview) { - /// - /// Construct an instance of - /// - /// for the that contains the content being visualized in the supplied - /// for the being visualized in the supplied - /// Lazily instantiated preview content. - /// Use lazy instantiation to ensure that any that may be present inside a given preview are only instantiated at the point - /// when the VS lightbulb requests that preview. Otherwise, we could end up instantiating a bunch of s most of which will never get - /// passed to the VS lightbulb. Such zombie s will never get closed and we will end up leaking memory. - internal class SolutionPreviewItem(ProjectId? projectId, DocumentId? documentId, Func> lazyPreview) - { - public readonly ProjectId? ProjectId = projectId; - public readonly DocumentId? DocumentId = documentId; - public readonly Func> LazyPreview = lazyPreview; - public readonly string? Text; + public readonly ProjectId? ProjectId = projectId; + public readonly DocumentId? DocumentId = documentId; + public readonly Func> LazyPreview = lazyPreview; + public readonly string? Text; - public SolutionPreviewItem(ProjectId? projectId, DocumentId? documentId, string text) - : this(projectId, documentId, c => Task.FromResult(text)) - { - Text = text; - } + public SolutionPreviewItem(ProjectId? projectId, DocumentId? documentId, string text) + : this(projectId, documentId, c => Task.FromResult(text)) + { + Text = text; } } diff --git a/src/EditorFeatures/Core/Preview/SolutionPreviewResult.cs b/src/EditorFeatures/Core/Preview/SolutionPreviewResult.cs index 7de0de1b9aed0..ebb88186ebc74 100644 --- a/src/EditorFeatures/Core/Preview/SolutionPreviewResult.cs +++ b/src/EditorFeatures/Core/Preview/SolutionPreviewResult.cs @@ -11,98 +11,97 @@ using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal class SolutionPreviewResult( + IThreadingContext threadingContext, + IList? previews, + SolutionChangeSummary? changeSummary = null) { - internal class SolutionPreviewResult( - IThreadingContext threadingContext, - IList? previews, - SolutionChangeSummary? changeSummary = null) + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IList _previews = previews ?? SpecializedCollections.EmptyList(); + public readonly SolutionChangeSummary? ChangeSummary = changeSummary; + + public SolutionPreviewResult(IThreadingContext threadingContext, SolutionPreviewItem preview, SolutionChangeSummary? changeSummary = null) + : this(threadingContext, [preview], changeSummary) { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IList _previews = previews ?? SpecializedCollections.EmptyList(); - public readonly SolutionChangeSummary? ChangeSummary = changeSummary; + } - public SolutionPreviewResult(IThreadingContext threadingContext, SolutionPreviewItem preview, SolutionChangeSummary? changeSummary = null) - : this(threadingContext, [preview], changeSummary) - { - } + public bool IsEmpty => _previews.Count == 0; - public bool IsEmpty => _previews.Count == 0; + public async Task?> GetPreviewsAsync(DocumentId? preferredDocumentId = null, ProjectId? preferredProjectId = null, CancellationToken cancellationToken = default) + { + _threadingContext.ThrowIfNotOnUIThread(); + cancellationToken.ThrowIfCancellationRequested(); - public async Task?> GetPreviewsAsync(DocumentId? preferredDocumentId = null, ProjectId? preferredProjectId = null, CancellationToken cancellationToken = default) + var orderedPreviews = _previews.OrderBy((i1, i2) => { - _threadingContext.ThrowIfNotOnUIThread(); - cancellationToken.ThrowIfCancellationRequested(); - - var orderedPreviews = _previews.OrderBy((i1, i2) => - { - return i1.DocumentId == preferredDocumentId && i2.DocumentId != preferredDocumentId ? -1 : - i1.DocumentId != preferredDocumentId && i2.DocumentId == preferredDocumentId ? 1 : - _previews.IndexOf(i1) - _previews.IndexOf(i2); - }).ThenBy((i1, i2) => - { - return i1.ProjectId == preferredProjectId && i2.ProjectId != preferredProjectId ? -1 : - i1.ProjectId != preferredProjectId && i2.ProjectId == preferredProjectId ? 1 : - _previews.IndexOf(i1) - _previews.IndexOf(i2); - }).ThenBy((i1, i2) => - { - return i1.Text == null && i2.Text != null ? -1 : - i1.Text != null && i2.Text == null ? 1 : - _previews.IndexOf(i1) - _previews.IndexOf(i2); - }); + return i1.DocumentId == preferredDocumentId && i2.DocumentId != preferredDocumentId ? -1 : + i1.DocumentId != preferredDocumentId && i2.DocumentId == preferredDocumentId ? 1 : + _previews.IndexOf(i1) - _previews.IndexOf(i2); + }).ThenBy((i1, i2) => + { + return i1.ProjectId == preferredProjectId && i2.ProjectId != preferredProjectId ? -1 : + i1.ProjectId != preferredProjectId && i2.ProjectId == preferredProjectId ? 1 : + _previews.IndexOf(i1) - _previews.IndexOf(i2); + }).ThenBy((i1, i2) => + { + return i1.Text == null && i2.Text != null ? -1 : + i1.Text != null && i2.Text == null ? 1 : + _previews.IndexOf(i1) - _previews.IndexOf(i2); + }); - var result = new List(); - var gotRichPreview = false; + var result = new List(); + var gotRichPreview = false; - try + try + { + foreach (var previewItem in _previews) { - foreach (var previewItem in _previews) + cancellationToken.ThrowIfCancellationRequested(); + if (previewItem.Text != null) { - cancellationToken.ThrowIfCancellationRequested(); - if (previewItem.Text != null) - { - result.Add(previewItem.Text); - } - else if (!gotRichPreview) + result.Add(previewItem.Text); + } + else if (!gotRichPreview) + { + var preview = await previewItem.LazyPreview(cancellationToken).ConfigureAwait(true); + if (preview != null) { - var preview = await previewItem.LazyPreview(cancellationToken).ConfigureAwait(true); - if (preview != null) - { - result.Add(preview); - gotRichPreview = true; - } + result.Add(preview); + gotRichPreview = true; } } - - return result.Count == 0 ? null : result; - } - catch (OperationCanceledException) - { - // make sure we dispose all disposable preview objects before - // we let control to exit this method - result.OfType().Do(d => d.Dispose()); - throw; } - } - /// Merge two different previews into one final preview result. The final preview will - /// have a concatenation of all the inidivual previews contained within each result. - internal static SolutionPreviewResult? Merge(SolutionPreviewResult? result1, SolutionPreviewResult? result2) + return result.Count == 0 ? null : result; + } + catch (OperationCanceledException) { - if (result1 == null) - { - return result2; - } + // make sure we dispose all disposable preview objects before + // we let control to exit this method + result.OfType().Do(d => d.Dispose()); + throw; + } + } - if (result2 == null) - { - return result1; - } + /// Merge two different previews into one final preview result. The final preview will + /// have a concatenation of all the inidivual previews contained within each result. + internal static SolutionPreviewResult? Merge(SolutionPreviewResult? result1, SolutionPreviewResult? result2) + { + if (result1 == null) + { + return result2; + } - return new SolutionPreviewResult( - result1._threadingContext, - result1._previews.Concat(result2._previews).ToList(), - result1.ChangeSummary ?? result2.ChangeSummary); + if (result2 == null) + { + return result1; } + + return new SolutionPreviewResult( + result1._threadingContext, + result1._previews.Concat(result2._previews).ToList(), + result1.ChangeSummary ?? result2.ChangeSummary); } } diff --git a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingOptionsStorage.cs b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingOptionsStorage.cs index aeb8deed8a92b..c40eafc03a749 100644 --- a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingOptionsStorage.cs +++ b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.ReferenceHighlighting +namespace Microsoft.CodeAnalysis.ReferenceHighlighting; + +internal static class ReferenceHighlightingOptionsStorage { - internal static class ReferenceHighlightingOptionsStorage - { - public static readonly PerLanguageOption2 ReferenceHighlighting = new("dotnet_highlight_references", defaultValue: true); - } + public static readonly PerLanguageOption2 ReferenceHighlighting = new("dotnet_highlight_references", defaultValue: true); } diff --git a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs index edcd157d786a9..f3fafcae3ed66 100644 --- a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs @@ -31,199 +31,198 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.ReferenceHighlighting +namespace Microsoft.CodeAnalysis.Editor.ReferenceHighlighting; + +[Export(typeof(IViewTaggerProvider))] +[ContentType(ContentTypeNames.RoslynContentType)] +[ContentType(ContentTypeNames.XamlContentType)] +[TagType(typeof(NavigableHighlightTag))] +[TextViewRole(PredefinedTextViewRoles.Interactive)] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal sealed partial class ReferenceHighlightingViewTaggerProvider( + IThreadingContext threadingContext, + IGlobalOptionService globalOptions, + [Import(AllowDefault = true)] ITextBufferVisibilityTracker visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) : AsynchronousViewTaggerProvider(threadingContext, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.ReferenceHighlighting)) { - [Export(typeof(IViewTaggerProvider))] - [ContentType(ContentTypeNames.RoslynContentType)] - [ContentType(ContentTypeNames.XamlContentType)] - [TagType(typeof(NavigableHighlightTag))] - [TextViewRole(PredefinedTextViewRoles.Interactive)] - [method: ImportingConstructor] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal sealed partial class ReferenceHighlightingViewTaggerProvider( - IThreadingContext threadingContext, - IGlobalOptionService globalOptions, - [Import(AllowDefault = true)] ITextBufferVisibilityTracker visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) : AsynchronousViewTaggerProvider(threadingContext, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.ReferenceHighlighting)) - { - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IGlobalOptionService _globalOptions = globalOptions; + + // Whenever an edit happens, clear all highlights. When moving the caret, preserve + // highlights if the caret stays within an existing tag. + protected override TaggerCaretChangeBehavior CaretChangeBehavior => TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag; + protected override TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.RemoveAllTags; - // Whenever an edit happens, clear all highlights. When moving the caret, preserve - // highlights if the caret stays within an existing tag. - protected override TaggerCaretChangeBehavior CaretChangeBehavior => TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag; - protected override TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.RemoveAllTags; + protected override ImmutableArray Options { get; } = [ReferenceHighlightingOptionsStorage.ReferenceHighlighting]; - protected override ImmutableArray Options { get; } = [ReferenceHighlightingOptionsStorage.ReferenceHighlighting]; + protected override TaggerDelay EventChangeDelay => TaggerDelay.Medium; - protected override TaggerDelay EventChangeDelay => TaggerDelay.Medium; + protected override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) + { + // Note: we don't listen for OnTextChanged. Text changes to this buffer will get + // reported by OnSemanticChanged. + return TaggerEventSources.Compose( + TaggerEventSources.OnCaretPositionChanged(textView, textView.TextBuffer), + TaggerEventSources.OnWorkspaceChanged(subjectBuffer, AsyncListener), + TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer)); + } - protected override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) + protected override SnapshotPoint? GetCaretPoint(ITextView textViewOpt, ITextBuffer subjectBuffer) + { + // With no selection we just use the caret position as expected + if (textViewOpt.Selection.IsEmpty) { - // Note: we don't listen for OnTextChanged. Text changes to this buffer will get - // reported by OnSemanticChanged. - return TaggerEventSources.Compose( - TaggerEventSources.OnCaretPositionChanged(textView, textView.TextBuffer), - TaggerEventSources.OnWorkspaceChanged(subjectBuffer, AsyncListener), - TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer)); + return textViewOpt.Caret.Position.Point.GetPoint(b => IsSupportedContentType(b.ContentType), PositionAffinity.Successor); } - protected override SnapshotPoint? GetCaretPoint(ITextView textViewOpt, ITextBuffer subjectBuffer) - { - // With no selection we just use the caret position as expected - if (textViewOpt.Selection.IsEmpty) - { - return textViewOpt.Caret.Position.Point.GetPoint(b => IsSupportedContentType(b.ContentType), PositionAffinity.Successor); - } + // If there is a selection then it makes more sense for highlighting to apply to the token at the start + // of the selection rather than where the caret is, otherwise you can be in a situation like [|count$$|]++ + // and it will try to highlight the operator. + return textViewOpt.BufferGraph.MapDownToFirstMatch(textViewOpt.Selection.Start.Position, PointTrackingMode.Positive, b => IsSupportedContentType(b.ContentType), PositionAffinity.Successor); + } - // If there is a selection then it makes more sense for highlighting to apply to the token at the start - // of the selection rather than where the caret is, otherwise you can be in a situation like [|count$$|]++ - // and it will try to highlight the operator. - return textViewOpt.BufferGraph.MapDownToFirstMatch(textViewOpt.Selection.Start.Position, PointTrackingMode.Positive, b => IsSupportedContentType(b.ContentType), PositionAffinity.Successor); - } + protected override IEnumerable GetSpansToTag(ITextView textViewOpt, ITextBuffer subjectBuffer) + { + // Note: this may return no snapshot spans. We have to be resilient to that + // when processing the TaggerContext<>.SpansToTag below. + return textViewOpt.BufferGraph.GetTextBuffers(b => IsSupportedContentType(b.ContentType)) + .Select(b => b.CurrentSnapshot.GetFullSpan()) + .ToList(); + } - protected override IEnumerable GetSpansToTag(ITextView textViewOpt, ITextBuffer subjectBuffer) + protected override Task ProduceTagsAsync( + TaggerContext context, CancellationToken cancellationToken) + { + // NOTE(cyrusn): Normally we'd limit ourselves to producing tags in the span we were + // asked about. However, we want to produce all tags here so that the user can actually + // navigate between all of them using the appropriate tag navigation commands. If we + // don't generate all the tags then the user will cycle through an incorrect subset. + if (context.CaretPosition == null) { - // Note: this may return no snapshot spans. We have to be resilient to that - // when processing the TaggerContext<>.SpansToTag below. - return textViewOpt.BufferGraph.GetTextBuffers(b => IsSupportedContentType(b.ContentType)) - .Select(b => b.CurrentSnapshot.GetFullSpan()) - .ToList(); + return Task.CompletedTask; } - protected override Task ProduceTagsAsync( - TaggerContext context, CancellationToken cancellationToken) - { - // NOTE(cyrusn): Normally we'd limit ourselves to producing tags in the span we were - // asked about. However, we want to produce all tags here so that the user can actually - // navigate between all of them using the appropriate tag navigation commands. If we - // don't generate all the tags then the user will cycle through an incorrect subset. - if (context.CaretPosition == null) - { - return Task.CompletedTask; - } + var caretPosition = context.CaretPosition.Value; - var caretPosition = context.CaretPosition.Value; + // GetSpansToTag may have produced no actual spans to tag. Be resilient to that. + var document = context.SpansToTag.FirstOrDefault(vt => vt.SnapshotSpan.Snapshot == caretPosition.Snapshot).Document; + if (document == null) + { + return Task.CompletedTask; + } - // GetSpansToTag may have produced no actual spans to tag. Be resilient to that. - var document = context.SpansToTag.FirstOrDefault(vt => vt.SnapshotSpan.Snapshot == caretPosition.Snapshot).Document; - if (document == null) - { - return Task.CompletedTask; - } + // Don't produce tags if the feature is not enabled. + if (!_globalOptions.GetOption(ReferenceHighlightingOptionsStorage.ReferenceHighlighting, document.Project.Language)) + { + return Task.CompletedTask; + } - // Don't produce tags if the feature is not enabled. - if (!_globalOptions.GetOption(ReferenceHighlightingOptionsStorage.ReferenceHighlighting, document.Project.Language)) - { - return Task.CompletedTask; - } + // See if the user is just moving their caret around in an existing tag. If so, we don't + // want to actually go recompute things. Note: this only works for containment. If the + // user moves their caret to the end of a highlighted reference, we do want to recompute + // as they may now be at the start of some other reference that should be highlighted instead. + var onExistingTags = context.HasExistingContainingTags(caretPosition); + if (onExistingTags) + { + context.SetSpansTagged([]); + return Task.CompletedTask; + } - // See if the user is just moving their caret around in an existing tag. If so, we don't - // want to actually go recompute things. Note: this only works for containment. If the - // user moves their caret to the end of a highlighted reference, we do want to recompute - // as they may now be at the start of some other reference that should be highlighted instead. - var onExistingTags = context.HasExistingContainingTags(caretPosition); - if (onExistingTags) - { - context.SetSpansTagged([]); - return Task.CompletedTask; - } + // Otherwise, we need to go produce all tags. + var options = _globalOptions.GetHighlightingOptions(document.Project.Language); + return ProduceTagsAsync(context, caretPosition, document, options, cancellationToken); + } - // Otherwise, we need to go produce all tags. - var options = _globalOptions.GetHighlightingOptions(document.Project.Language); - return ProduceTagsAsync(context, caretPosition, document, options, cancellationToken); - } + private static async Task ProduceTagsAsync( + TaggerContext context, + SnapshotPoint position, + Document document, + HighlightingOptions options, + CancellationToken cancellationToken) + { + var solution = document.Project.Solution; - private static async Task ProduceTagsAsync( - TaggerContext context, - SnapshotPoint position, - Document document, - HighlightingOptions options, - CancellationToken cancellationToken) + using (Logger.LogBlock(FunctionId.Tagger_ReferenceHighlighting_TagProducer_ProduceTags, cancellationToken)) { - var solution = document.Project.Solution; - - using (Logger.LogBlock(FunctionId.Tagger_ReferenceHighlighting_TagProducer_ProduceTags, cancellationToken)) + if (document != null) { - if (document != null) + var service = document.GetLanguageService(); + if (service != null) { - var service = document.GetLanguageService(); - if (service != null) + // We only want to search inside documents that correspond to the snapshots + // we're looking at + var documentsToSearch = ImmutableHashSet.CreateRange(context.SpansToTag.Select(vt => vt.Document).WhereNotNull()); + var documentHighlightsList = await service.GetDocumentHighlightsAsync( + document, position, documentsToSearch, options, cancellationToken).ConfigureAwait(false); + if (documentHighlightsList != null) { - // We only want to search inside documents that correspond to the snapshots - // we're looking at - var documentsToSearch = ImmutableHashSet.CreateRange(context.SpansToTag.Select(vt => vt.Document).WhereNotNull()); - var documentHighlightsList = await service.GetDocumentHighlightsAsync( - document, position, documentsToSearch, options, cancellationToken).ConfigureAwait(false); - if (documentHighlightsList != null) + foreach (var documentHighlights in documentHighlightsList) { - foreach (var documentHighlights in documentHighlightsList) - { - AddTagSpans(context, documentHighlights, cancellationToken); - } + AddTagSpans(context, documentHighlights, cancellationToken); } } } } } + } - private static void AddTagSpans( - TaggerContext context, - DocumentHighlights documentHighlights, - CancellationToken cancellationToken) - { - var document = documentHighlights.Document; - - var textSnapshot = context.SpansToTag.FirstOrDefault(s => s.Document == document).SnapshotSpan.Snapshot; - if (textSnapshot == null) - { - // There is no longer an editor snapshot for this document, so we can't care about the - // results. - return; - } + private static void AddTagSpans( + TaggerContext context, + DocumentHighlights documentHighlights, + CancellationToken cancellationToken) + { + var document = documentHighlights.Document; - try - { - foreach (var span in documentHighlights.HighlightSpans) - { - var tag = GetTag(span); - context.AddTag(new TagSpan( - textSnapshot.GetSpan(Span.FromBounds(span.TextSpan.Start, span.TextSpan.End)), tag)); - } - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken, ErrorSeverity.General)) - { - // report NFW and continue. - // also, rather than return partial results, return nothing - context.ClearTags(); - } + var textSnapshot = context.SpansToTag.FirstOrDefault(s => s.Document == document).SnapshotSpan.Snapshot; + if (textSnapshot == null) + { + // There is no longer an editor snapshot for this document, so we can't care about the + // results. + return; } - private static NavigableHighlightTag GetTag(HighlightSpan span) + try { - switch (span.Kind) + foreach (var span in documentHighlights.HighlightSpans) { - case HighlightSpanKind.WrittenReference: - return WrittenReferenceHighlightTag.Instance; - - case HighlightSpanKind.Definition: - return DefinitionHighlightTag.Instance; - - case HighlightSpanKind.Reference: - case HighlightSpanKind.None: - default: - return ReferenceHighlightTag.Instance; + var tag = GetTag(span); + context.AddTag(new TagSpan( + textSnapshot.GetSpan(Span.FromBounds(span.TextSpan.Start, span.TextSpan.End)), tag)); } } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken, ErrorSeverity.General)) + { + // report NFW and continue. + // also, rather than return partial results, return nothing + context.ClearTags(); + } + } - private static bool IsSupportedContentType(IContentType contentType) + private static NavigableHighlightTag GetTag(HighlightSpan span) + { + switch (span.Kind) { - // This list should match the list of exported content types above - return contentType.IsOfType(ContentTypeNames.RoslynContentType) || - contentType.IsOfType(ContentTypeNames.XamlContentType); + case HighlightSpanKind.WrittenReference: + return WrittenReferenceHighlightTag.Instance; + + case HighlightSpanKind.Definition: + return DefinitionHighlightTag.Instance; + + case HighlightSpanKind.Reference: + case HighlightSpanKind.None: + default: + return ReferenceHighlightTag.Instance; } + } - // Safe to directly reference compare as all the NavigableHighlightTag subclasses are singletons. - protected override bool TagEquals(NavigableHighlightTag tag1, NavigableHighlightTag tag2) - => tag1 == tag2; + private static bool IsSupportedContentType(IContentType contentType) + { + // This list should match the list of exported content types above + return contentType.IsOfType(ContentTypeNames.RoslynContentType) || + contentType.IsOfType(ContentTypeNames.XamlContentType); } + + // Safe to directly reference compare as all the NavigableHighlightTag subclasses are singletons. + protected override bool TagEquals(NavigableHighlightTag tag1, NavigableHighlightTag tag2) + => tag1 == tag2; } diff --git a/src/EditorFeatures/Core/ReferenceHighlighting/Tags/DefinitionHighlightTag.cs b/src/EditorFeatures/Core/ReferenceHighlighting/Tags/DefinitionHighlightTag.cs index f2f556482fcd0..1d268abf43acf 100644 --- a/src/EditorFeatures/Core/ReferenceHighlighting/Tags/DefinitionHighlightTag.cs +++ b/src/EditorFeatures/Core/ReferenceHighlighting/Tags/DefinitionHighlightTag.cs @@ -5,17 +5,16 @@ using Microsoft.CodeAnalysis.Editor.Shared.Tagging; using Microsoft.CodeAnalysis.ReferenceHighlighting; -namespace Microsoft.CodeAnalysis.Editor.ReferenceHighlighting +namespace Microsoft.CodeAnalysis.Editor.ReferenceHighlighting; + +internal sealed class DefinitionHighlightTag : NavigableHighlightTag { - internal sealed class DefinitionHighlightTag : NavigableHighlightTag - { - public const string TagId = ReferenceHighlightingConstants.DefinitionTagId; + public const string TagId = ReferenceHighlightingConstants.DefinitionTagId; - public static readonly DefinitionHighlightTag Instance = new(); + public static readonly DefinitionHighlightTag Instance = new(); - private DefinitionHighlightTag() - : base(TagId) - { - } + private DefinitionHighlightTag() + : base(TagId) + { } } diff --git a/src/EditorFeatures/Core/ReferenceHighlighting/Tags/ReferenceHighlightTag.cs b/src/EditorFeatures/Core/ReferenceHighlighting/Tags/ReferenceHighlightTag.cs index a140c08880416..becde28e4d4dd 100644 --- a/src/EditorFeatures/Core/ReferenceHighlighting/Tags/ReferenceHighlightTag.cs +++ b/src/EditorFeatures/Core/ReferenceHighlighting/Tags/ReferenceHighlightTag.cs @@ -5,17 +5,16 @@ using Microsoft.CodeAnalysis.Editor.Shared.Tagging; using Microsoft.CodeAnalysis.ReferenceHighlighting; -namespace Microsoft.CodeAnalysis.Editor.ReferenceHighlighting +namespace Microsoft.CodeAnalysis.Editor.ReferenceHighlighting; + +internal sealed class ReferenceHighlightTag : NavigableHighlightTag { - internal sealed class ReferenceHighlightTag : NavigableHighlightTag - { - public const string TagId = ReferenceHighlightingConstants.ReferenceTagId; + public const string TagId = ReferenceHighlightingConstants.ReferenceTagId; - public static readonly ReferenceHighlightTag Instance = new(); + public static readonly ReferenceHighlightTag Instance = new(); - private ReferenceHighlightTag() - : base(TagId) - { - } + private ReferenceHighlightTag() + : base(TagId) + { } } diff --git a/src/EditorFeatures/Core/ReferenceHighlighting/Tags/WrittenReferenceHighlightTag.cs b/src/EditorFeatures/Core/ReferenceHighlighting/Tags/WrittenReferenceHighlightTag.cs index bbdeb78c07c49..6c7f078549ff1 100644 --- a/src/EditorFeatures/Core/ReferenceHighlighting/Tags/WrittenReferenceHighlightTag.cs +++ b/src/EditorFeatures/Core/ReferenceHighlighting/Tags/WrittenReferenceHighlightTag.cs @@ -5,17 +5,16 @@ using Microsoft.CodeAnalysis.Editor.Shared.Tagging; using Microsoft.CodeAnalysis.ReferenceHighlighting; -namespace Microsoft.CodeAnalysis.Editor.ReferenceHighlighting +namespace Microsoft.CodeAnalysis.Editor.ReferenceHighlighting; + +internal sealed class WrittenReferenceHighlightTag : NavigableHighlightTag { - internal sealed class WrittenReferenceHighlightTag : NavigableHighlightTag - { - public const string TagId = ReferenceHighlightingConstants.WrittenReferenceTagId; + public const string TagId = ReferenceHighlightingConstants.WrittenReferenceTagId; - public static readonly WrittenReferenceHighlightTag Instance = new(); + public static readonly WrittenReferenceHighlightTag Instance = new(); - private WrittenReferenceHighlightTag() - : base(TagId) - { - } + private WrittenReferenceHighlightTag() + : base(TagId) + { } } diff --git a/src/EditorFeatures/Core/Remote/RemoteHostOptionsStorage.cs b/src/EditorFeatures/Core/Remote/RemoteHostOptionsStorage.cs index f0533143232df..b791ccb4817f2 100644 --- a/src/EditorFeatures/Core/Remote/RemoteHostOptionsStorage.cs +++ b/src/EditorFeatures/Core/Remote/RemoteHostOptionsStorage.cs @@ -4,16 +4,15 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Remote +namespace Microsoft.CodeAnalysis.Remote; + +internal sealed class RemoteHostOptionsStorage { - internal sealed class RemoteHostOptionsStorage - { - // use 64bit OOP - public static readonly Option2 OOP64Bit = new("dotnet_code_analysis_in_separate_process", defaultValue: true); + // use 64bit OOP + public static readonly Option2 OOP64Bit = new("dotnet_code_analysis_in_separate_process", defaultValue: true); - // use coreclr host for OOP - public static readonly Option2 OOPCoreClr = new("dotnet_enable_core_clr_in_code_analysis_process", defaultValue: true); + // use coreclr host for OOP + public static readonly Option2 OOPCoreClr = new("dotnet_enable_core_clr_in_code_analysis_process", defaultValue: true); - public static readonly Option2 OOPServerGCFeatureFlag = new("dotnet_enable_server_garbage_collection_in_code_analysis_process", defaultValue: false); - } + public static readonly Option2 OOPServerGCFeatureFlag = new("dotnet_enable_server_garbage_collection_in_code_analysis_process", defaultValue: false); } diff --git a/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs b/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs index 8d91d12ed9374..12dd252c7bec2 100644 --- a/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs +++ b/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs @@ -12,217 +12,216 @@ using Microsoft.CodeAnalysis.Shared.TestHooks; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Remote +namespace Microsoft.CodeAnalysis.Remote; + +/// +/// This class runs against the in-process workspace, and when it sees changes proactively pushes them to +/// the out-of-process workspace through the . +/// +internal sealed class SolutionChecksumUpdater { + private readonly Workspace _workspace; + /// - /// This class runs against the in-process workspace, and when it sees changes proactively pushes them to - /// the out-of-process workspace through the . + /// We're not at a layer where we are guaranteed to have an IGlobalOperationNotificationService. So allow for + /// it being null. /// - internal sealed class SolutionChecksumUpdater - { - private readonly Workspace _workspace; - - /// - /// We're not at a layer where we are guaranteed to have an IGlobalOperationNotificationService. So allow for - /// it being null. - /// - private readonly IGlobalOperationNotificationService? _globalOperationService; - - /// - /// Queue to push out text changes in a batched fashion when we hear about them. Because these should be short - /// operations (only syncing text changes) we don't cancel this when we enter the paused state. We simply don't - /// start queuing more requests into this until we become unpaused. - /// - private readonly AsyncBatchingWorkQueue<(Document? oldDocument, Document? newDocument)> _textChangeQueue; - - /// - /// Queue for kicking off the work to synchronize the primary workspace's solution. - /// - private readonly AsyncBatchingWorkQueue _synchronizeWorkspaceQueue; - - private readonly object _gate = new(); - private bool _isPaused; - - public SolutionChecksumUpdater( - Workspace workspace, - IAsynchronousOperationListenerProvider listenerProvider, - CancellationToken shutdownToken) - { - var listener = listenerProvider.GetListener(FeatureAttribute.SolutionChecksumUpdater); + private readonly IGlobalOperationNotificationService? _globalOperationService; + + /// + /// Queue to push out text changes in a batched fashion when we hear about them. Because these should be short + /// operations (only syncing text changes) we don't cancel this when we enter the paused state. We simply don't + /// start queuing more requests into this until we become unpaused. + /// + private readonly AsyncBatchingWorkQueue<(Document? oldDocument, Document? newDocument)> _textChangeQueue; + + /// + /// Queue for kicking off the work to synchronize the primary workspace's solution. + /// + private readonly AsyncBatchingWorkQueue _synchronizeWorkspaceQueue; - _globalOperationService = workspace.Services.SolutionServices.ExportProvider.GetExports().FirstOrDefault()?.Value; + private readonly object _gate = new(); + private bool _isPaused; - _workspace = workspace; + public SolutionChecksumUpdater( + Workspace workspace, + IAsynchronousOperationListenerProvider listenerProvider, + CancellationToken shutdownToken) + { + var listener = listenerProvider.GetListener(FeatureAttribute.SolutionChecksumUpdater); - _textChangeQueue = new AsyncBatchingWorkQueue<(Document? oldDocument, Document? newDocument)>( - DelayTimeSpan.NearImmediate, - SynchronizeTextChangesAsync, - listener, - shutdownToken); + _globalOperationService = workspace.Services.SolutionServices.ExportProvider.GetExports().FirstOrDefault()?.Value; - // Use an equality comparer here as we will commonly get lots of change notifications that will all be - // associated with the same cancellation token controlling that batch of work. No need to enqueue the same - // token a huge number of times when we only need the single value of it when doing the work. - _synchronizeWorkspaceQueue = new AsyncBatchingWorkQueue( - DelayTimeSpan.NearImmediate, - SynchronizePrimaryWorkspaceAsync, - listener, - shutdownToken); + _workspace = workspace; - // start listening workspace change event - _workspace.WorkspaceChanged += OnWorkspaceChanged; + _textChangeQueue = new AsyncBatchingWorkQueue<(Document? oldDocument, Document? newDocument)>( + DelayTimeSpan.NearImmediate, + SynchronizeTextChangesAsync, + listener, + shutdownToken); - if (_globalOperationService != null) - { - _globalOperationService.Started += OnGlobalOperationStarted; - _globalOperationService.Stopped += OnGlobalOperationStopped; - } + // Use an equality comparer here as we will commonly get lots of change notifications that will all be + // associated with the same cancellation token controlling that batch of work. No need to enqueue the same + // token a huge number of times when we only need the single value of it when doing the work. + _synchronizeWorkspaceQueue = new AsyncBatchingWorkQueue( + DelayTimeSpan.NearImmediate, + SynchronizePrimaryWorkspaceAsync, + listener, + shutdownToken); - // Enqueue the work to sync the initial solution. - ResumeWork(); - } + // start listening workspace change event + _workspace.WorkspaceChanged += OnWorkspaceChanged; - public void Shutdown() + if (_globalOperationService != null) { - // Try to stop any work that is in progress. - PauseWork(); + _globalOperationService.Started += OnGlobalOperationStarted; + _globalOperationService.Stopped += OnGlobalOperationStopped; + } - _workspace.WorkspaceChanged -= OnWorkspaceChanged; + // Enqueue the work to sync the initial solution. + ResumeWork(); + } - if (_globalOperationService != null) - { - _globalOperationService.Started -= OnGlobalOperationStarted; - _globalOperationService.Stopped -= OnGlobalOperationStopped; - } + public void Shutdown() + { + // Try to stop any work that is in progress. + PauseWork(); + + _workspace.WorkspaceChanged -= OnWorkspaceChanged; + + if (_globalOperationService != null) + { + _globalOperationService.Started -= OnGlobalOperationStarted; + _globalOperationService.Stopped -= OnGlobalOperationStopped; } + } - private void OnGlobalOperationStarted(object? sender, EventArgs e) - => PauseWork(); + private void OnGlobalOperationStarted(object? sender, EventArgs e) + => PauseWork(); - private void OnGlobalOperationStopped(object? sender, EventArgs e) - => ResumeWork(); + private void OnGlobalOperationStopped(object? sender, EventArgs e) + => ResumeWork(); - private void PauseWork() + private void PauseWork() + { + // An expensive global operation started (like a build). Pause ourselves and cancel any outstanding work in + // progress to synchronize the solution. + lock (_gate) { - // An expensive global operation started (like a build). Pause ourselves and cancel any outstanding work in - // progress to synchronize the solution. - lock (_gate) - { - _synchronizeWorkspaceQueue.CancelExistingWork(); - _isPaused = true; - } + _synchronizeWorkspaceQueue.CancelExistingWork(); + _isPaused = true; } + } - private void ResumeWork() + private void ResumeWork() + { + lock (_gate) { - lock (_gate) - { - _isPaused = false; - _synchronizeWorkspaceQueue.AddWork(); - } + _isPaused = false; + _synchronizeWorkspaceQueue.AddWork(); } + } - private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) + private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) + { + // Check if we're currently paused. If so ignore this notification. We don't want to any work in response + // to whatever the workspace is doing. + lock (_gate) { - // Check if we're currently paused. If so ignore this notification. We don't want to any work in response - // to whatever the workspace is doing. - lock (_gate) - { - if (_isPaused) - return; - } + if (_isPaused) + return; + } - if (e.Kind == WorkspaceChangeKind.DocumentChanged) - { - _textChangeQueue.AddWork((e.OldSolution.GetDocument(e.DocumentId), e.NewSolution.GetDocument(e.DocumentId))); - } + if (e.Kind == WorkspaceChangeKind.DocumentChanged) + { + _textChangeQueue.AddWork((e.OldSolution.GetDocument(e.DocumentId), e.NewSolution.GetDocument(e.DocumentId))); + } - _synchronizeWorkspaceQueue.AddWork(); + _synchronizeWorkspaceQueue.AddWork(); + } + + private async ValueTask SynchronizePrimaryWorkspaceAsync(CancellationToken cancellationToken) + { + var solution = _workspace.CurrentSolution; + var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); + if (client == null) + return; + + using (Logger.LogBlock(FunctionId.SolutionChecksumUpdater_SynchronizePrimaryWorkspace, cancellationToken)) + { + var workspaceVersion = solution.WorkspaceVersion; + await client.TryInvokeAsync( + solution, + (service, solution, cancellationToken) => service.SynchronizePrimaryWorkspaceAsync(solution, workspaceVersion, cancellationToken), + cancellationToken).ConfigureAwait(false); } + } - private async ValueTask SynchronizePrimaryWorkspaceAsync(CancellationToken cancellationToken) + private async ValueTask SynchronizeTextChangesAsync( + ImmutableSegmentedList<(Document? oldDocument, Document? newDocument)> values, + CancellationToken cancellationToken) + { + foreach (var (oldDocument, newDocument) in values) { - var solution = _workspace.CurrentSolution; - var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); - if (client == null) - return; + if (oldDocument is null || newDocument is null) + continue; - using (Logger.LogBlock(FunctionId.SolutionChecksumUpdater_SynchronizePrimaryWorkspace, cancellationToken)) - { - var workspaceVersion = solution.WorkspaceVersion; - await client.TryInvokeAsync( - solution, - (service, solution, cancellationToken) => service.SynchronizePrimaryWorkspaceAsync(solution, workspaceVersion, cancellationToken), - cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); + await SynchronizeTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false); } - private async ValueTask SynchronizeTextChangesAsync( - ImmutableSegmentedList<(Document? oldDocument, Document? newDocument)> values, - CancellationToken cancellationToken) + return; + + async ValueTask SynchronizeTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken) { - foreach (var (oldDocument, newDocument) in values) + // this pushes text changes to the remote side if it can. + // this is purely perf optimization. whether this pushing text change + // worked or not doesn't affect feature's functionality. + // + // this basically see whether it can cheaply find out text changes + // between 2 snapshots, if it can, it will send out that text changes to + // remote side. + // + // the remote side, once got the text change, will again see whether + // it can use that text change information without any high cost and + // create new snapshot from it. + // + // otherwise, it will do the normal behavior of getting full text from + // VS side. this optimization saves times we need to do full text + // synchronization for typing scenario. + + if ((oldDocument.TryGetText(out var oldText) == false) || + (newDocument.TryGetText(out var newText) == false)) { - if (oldDocument is null || newDocument is null) - continue; - - cancellationToken.ThrowIfCancellationRequested(); - await SynchronizeTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false); + // we only support case where text already exist + return; } - return; + // get text changes + var textChanges = newText.GetTextChanges(oldText); + if (textChanges.Count == 0) + { + // no changes + return; + } - async ValueTask SynchronizeTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken) + // whole document case + if (textChanges.Count == 1 && textChanges[0].Span.Length == oldText.Length) { - // this pushes text changes to the remote side if it can. - // this is purely perf optimization. whether this pushing text change - // worked or not doesn't affect feature's functionality. - // - // this basically see whether it can cheaply find out text changes - // between 2 snapshots, if it can, it will send out that text changes to - // remote side. - // - // the remote side, once got the text change, will again see whether - // it can use that text change information without any high cost and - // create new snapshot from it. - // - // otherwise, it will do the normal behavior of getting full text from - // VS side. this optimization saves times we need to do full text - // synchronization for typing scenario. - - if ((oldDocument.TryGetText(out var oldText) == false) || - (newDocument.TryGetText(out var newText) == false)) - { - // we only support case where text already exist - return; - } - - // get text changes - var textChanges = newText.GetTextChanges(oldText); - if (textChanges.Count == 0) - { - // no changes - return; - } - - // whole document case - if (textChanges.Count == 1 && textChanges[0].Span.Length == oldText.Length) - { - // no benefit here. pulling from remote host is more efficient - return; - } - - // only cancelled when remote host gets shutdown - var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); - if (client == null) - return; - - var state = await oldDocument.State.GetStateChecksumsAsync(cancellationToken).ConfigureAwait(false); - - await client.TryInvokeAsync( - (service, cancellationToken) => service.SynchronizeTextAsync(oldDocument.Id, state.Text, textChanges, cancellationToken), - cancellationToken).ConfigureAwait(false); + // no benefit here. pulling from remote host is more efficient + return; } + + // only cancelled when remote host gets shutdown + var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); + if (client == null) + return; + + var state = await oldDocument.State.GetStateChecksumsAsync(cancellationToken).ConfigureAwait(false); + + await client.TryInvokeAsync( + (service, cancellationToken) => service.SynchronizeTextAsync(oldDocument.Id, state.Text, textChanges, cancellationToken), + cancellationToken).ConfigureAwait(false); } } } diff --git a/src/EditorFeatures/Core/RenameTracking/IRenameTrackingLanguageHeuristicsService.cs b/src/EditorFeatures/Core/RenameTracking/IRenameTrackingLanguageHeuristicsService.cs index 7a8f3252ce984..74ff0b2bebccb 100644 --- a/src/EditorFeatures/Core/RenameTracking/IRenameTrackingLanguageHeuristicsService.cs +++ b/src/EditorFeatures/Core/RenameTracking/IRenameTrackingLanguageHeuristicsService.cs @@ -6,10 +6,9 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +internal interface IRenameTrackingLanguageHeuristicsService : ILanguageService { - internal interface IRenameTrackingLanguageHeuristicsService : ILanguageService - { - bool IsIdentifierValidForRenameTracking(string name); - } + bool IsIdentifierValidForRenameTracking(string name); } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingCancellationCommandHandler.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingCancellationCommandHandler.cs index ff6d95ee605f3..50d4ea654dfc0 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingCancellationCommandHandler.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingCancellationCommandHandler.cs @@ -13,37 +13,36 @@ using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.RoslynContentType)] +[ContentType(ContentTypeNames.XamlContentType)] +[Name(PredefinedCommandHandlerNames.RenameTrackingCancellation)] +[Order(After = PredefinedCommandHandlerNames.SignatureHelpBeforeCompletion)] +[Order(After = PredefinedCommandHandlerNames.SignatureHelpAfterCompletion)] +[Order(After = PredefinedCommandHandlerNames.AutomaticCompletion)] +[Order(After = PredefinedCompletionNames.CompletionCommandHandler)] +[Order(After = PredefinedCommandHandlerNames.QuickInfo)] +[Order(After = PredefinedCommandHandlerNames.EventHookup)] +internal class RenameTrackingCancellationCommandHandler : ICommandHandler { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.RoslynContentType)] - [ContentType(ContentTypeNames.XamlContentType)] - [Name(PredefinedCommandHandlerNames.RenameTrackingCancellation)] - [Order(After = PredefinedCommandHandlerNames.SignatureHelpBeforeCompletion)] - [Order(After = PredefinedCommandHandlerNames.SignatureHelpAfterCompletion)] - [Order(After = PredefinedCommandHandlerNames.AutomaticCompletion)] - [Order(After = PredefinedCompletionNames.CompletionCommandHandler)] - [Order(After = PredefinedCommandHandlerNames.QuickInfo)] - [Order(After = PredefinedCommandHandlerNames.EventHookup)] - internal class RenameTrackingCancellationCommandHandler : ICommandHandler + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public RenameTrackingCancellationCommandHandler() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public RenameTrackingCancellationCommandHandler() - { - } - - public string DisplayName => EditorFeaturesResources.Rename_Tracking_Cancellation; + } - public bool ExecuteCommand(EscapeKeyCommandArgs args, CommandExecutionContext context) - { - var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + public string DisplayName => EditorFeaturesResources.Rename_Tracking_Cancellation; - return document != null && - RenameTrackingDismisser.DismissVisibleRenameTracking(document.Project.Solution.Workspace, document.Id); - } + public bool ExecuteCommand(EscapeKeyCommandArgs args, CommandExecutionContext context) + { + var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - public CommandState GetCommandState(EscapeKeyCommandArgs args) - => CommandState.Unspecified; + return document != null && + RenameTrackingDismisser.DismissVisibleRenameTracking(document.Project.Solution.Workspace, document.Id); } + + public CommandState GetCommandState(EscapeKeyCommandArgs args) + => CommandState.Unspecified; } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingCodeRefactoringProvider.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingCodeRefactoringProvider.cs index 8e58131463e52..c7b084bf7667e 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingCodeRefactoringProvider.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingCodeRefactoringProvider.cs @@ -10,47 +10,46 @@ using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, + Name = PredefinedCodeRefactoringProviderNames.RenameTracking), Shared] +internal class RenameTrackingCodeRefactoringProvider : CodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, - Name = PredefinedCodeRefactoringProviderNames.RenameTracking), Shared] - internal class RenameTrackingCodeRefactoringProvider : CodeRefactoringProvider + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; + private readonly IEnumerable _refactorNotifyServices; + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public RenameTrackingCodeRefactoringProvider( + ITextUndoHistoryRegistry undoHistoryRegistry, + [ImportMany] IEnumerable refactorNotifyServices) { - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; - private readonly IEnumerable _refactorNotifyServices; - - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public RenameTrackingCodeRefactoringProvider( - ITextUndoHistoryRegistry undoHistoryRegistry, - [ImportMany] IEnumerable refactorNotifyServices) - { - _undoHistoryRegistry = undoHistoryRegistry; - _refactorNotifyServices = refactorNotifyServices; - - // Backdoor that allows this provider to use the high-priority bucket. - this.CustomTags = this.CustomTags.Add(CodeAction.CanBeHighPriorityTag); - } - - public override Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, span, cancellationToken) = context; - - var (action, renameSpan) = RenameTrackingTaggerProvider.TryGetCodeAction( - document, span, _refactorNotifyServices, _undoHistoryRegistry, cancellationToken); - - if (action != null) - context.RegisterRefactoring(action, renameSpan); - - return Task.CompletedTask; - } - - /// - /// This is a high priority refactoring that we want to run first so that the user can quickly - /// change the name of something and pop up the lightbulb without having to wait for the rest to - /// compute. - /// - protected override CodeActionRequestPriority ComputeRequestPriority() - => CodeActionRequestPriority.High; + _undoHistoryRegistry = undoHistoryRegistry; + _refactorNotifyServices = refactorNotifyServices; + + // Backdoor that allows this provider to use the high-priority bucket. + this.CustomTags = this.CustomTags.Add(CodeAction.CanBeHighPriorityTag); } + + public override Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, span, cancellationToken) = context; + + var (action, renameSpan) = RenameTrackingTaggerProvider.TryGetCodeAction( + document, span, _refactorNotifyServices, _undoHistoryRegistry, cancellationToken); + + if (action != null) + context.RegisterRefactoring(action, renameSpan); + + return Task.CompletedTask; + } + + /// + /// This is a high priority refactoring that we want to run first so that the user can quickly + /// change the name of something and pop up the lightbulb without having to wait for the rest to + /// compute. + /// + protected override CodeActionRequestPriority ComputeRequestPriority() + => CodeActionRequestPriority.High; } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingOptionsStorage.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingOptionsStorage.cs index 409c7b3e087ae..ccd994c32a96e 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingOptionsStorage.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingOptionsStorage.cs @@ -4,12 +4,11 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +internal static class RenameTrackingOptionsStorage { - internal static class RenameTrackingOptionsStorage - { - public static readonly Option2 RenameTracking = new("dotnet_enable_rename_tracking", defaultValue: true); + public static readonly Option2 RenameTracking = new("dotnet_enable_rename_tracking", defaultValue: true); - public static readonly PerLanguageOption2 RenameTrackingPreview = new("dotnet_show_preview_for_rename_tracking", defaultValue: true); - } + public static readonly PerLanguageOption2 RenameTrackingPreview = new("dotnet_show_preview_for_rename_tracking", defaultValue: true); } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTag.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTag.cs index b7775b2221998..c2e3f57b04d60 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTag.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTag.cs @@ -6,17 +6,16 @@ using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +internal class RenameTrackingTag : TextMarkerTag { - internal class RenameTrackingTag : TextMarkerTag - { - internal const string TagId = "RenameTrackingTag"; + internal const string TagId = "RenameTrackingTag"; - public static readonly RenameTrackingTag Instance = new(); + public static readonly RenameTrackingTag Instance = new(); - private RenameTrackingTag() - : base(TagId) - { - } + private RenameTrackingTag() + : base(TagId) + { } } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCodeAction.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCodeAction.cs index 53294d8999402..2d1aecd212cff 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCodeAction.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCodeAction.cs @@ -17,120 +17,119 @@ using Microsoft.VisualStudio.Text.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +internal sealed partial class RenameTrackingTaggerProvider { - internal sealed partial class RenameTrackingTaggerProvider + private sealed class RenameTrackingCodeAction : CodeAction { - private sealed class RenameTrackingCodeAction : CodeAction + private readonly string _title; + private readonly IThreadingContext _threadingContext; + private readonly Document _document; + private readonly IEnumerable _refactorNotifyServices; + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; + private readonly IGlobalOptionService _globalOptions; + private RenameTrackingCommitter _renameTrackingCommitter; + + public RenameTrackingCodeAction( + IThreadingContext threadingContext, + Document document, + string title, + IEnumerable refactorNotifyServices, + ITextUndoHistoryRegistry undoHistoryRegistry, + IGlobalOptionService globalOptions) { - private readonly string _title; - private readonly IThreadingContext _threadingContext; - private readonly Document _document; - private readonly IEnumerable _refactorNotifyServices; - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; - private readonly IGlobalOptionService _globalOptions; - private RenameTrackingCommitter _renameTrackingCommitter; - - public RenameTrackingCodeAction( - IThreadingContext threadingContext, - Document document, - string title, - IEnumerable refactorNotifyServices, - ITextUndoHistoryRegistry undoHistoryRegistry, - IGlobalOptionService globalOptions) - { - _threadingContext = threadingContext; - _document = document; - _title = title; - _refactorNotifyServices = refactorNotifyServices; - _undoHistoryRegistry = undoHistoryRegistry; - _globalOptions = globalOptions; - - // Backdoor that allows this provider to use the high-priority bucket. - this.CustomTags = this.CustomTags.Add(CodeAction.CanBeHighPriorityTag); - } + _threadingContext = threadingContext; + _document = document; + _title = title; + _refactorNotifyServices = refactorNotifyServices; + _undoHistoryRegistry = undoHistoryRegistry; + _globalOptions = globalOptions; + + // Backdoor that allows this provider to use the high-priority bucket. + this.CustomTags = this.CustomTags.Add(CodeAction.CanBeHighPriorityTag); + } - public override string Title => _title; + public override string Title => _title; - protected sealed override CodeActionPriority ComputePriority() - => CodeActionPriority.High; + protected sealed override CodeActionPriority ComputePriority() + => CodeActionPriority.High; - protected override Task> ComputeOperationsAsync( - IProgress progress, CancellationToken cancellationToken) + protected override Task> ComputeOperationsAsync( + IProgress progress, CancellationToken cancellationToken) + { + // Invoked directly without previewing. + if (_renameTrackingCommitter == null) { - // Invoked directly without previewing. - if (_renameTrackingCommitter == null) + if (!TryInitializeRenameTrackingCommitter(cancellationToken)) { - if (!TryInitializeRenameTrackingCommitter(cancellationToken)) - { - return SpecializedTasks.EmptyImmutableArray(); - } + return SpecializedTasks.EmptyImmutableArray(); } - - var committerOperation = new RenameTrackingCommitterOperation(_renameTrackingCommitter, _threadingContext); - return Task.FromResult(ImmutableArray.Create(committerOperation)); } - protected override async Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) + var committerOperation = new RenameTrackingCommitterOperation(_renameTrackingCommitter, _threadingContext); + return Task.FromResult(ImmutableArray.Create(committerOperation)); + } + + protected override async Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) + { + if (!_globalOptions.GetOption(RenameTrackingOptionsStorage.RenameTrackingPreview, _document.Project.Language) || + !TryInitializeRenameTrackingCommitter(cancellationToken)) { - if (!_globalOptions.GetOption(RenameTrackingOptionsStorage.RenameTrackingPreview, _document.Project.Language) || - !TryInitializeRenameTrackingCommitter(cancellationToken)) - { - return await SpecializedTasks.EmptyEnumerable().ConfigureAwait(false); - } + return await SpecializedTasks.EmptyEnumerable().ConfigureAwait(false); + } - var solutionSet = await _renameTrackingCommitter.RenameSymbolAsync(cancellationToken).ConfigureAwait(false); + var solutionSet = await _renameTrackingCommitter.RenameSymbolAsync(cancellationToken).ConfigureAwait(false); - return SpecializedCollections.SingletonEnumerable( - (CodeActionOperation)new ApplyChangesOperation(solutionSet.RenamedSolution)); - } + return SpecializedCollections.SingletonEnumerable( + (CodeActionOperation)new ApplyChangesOperation(solutionSet.RenamedSolution)); + } - private bool TryInitializeRenameTrackingCommitter(CancellationToken cancellationToken) + private bool TryInitializeRenameTrackingCommitter(CancellationToken cancellationToken) + { + if (_document.TryGetText(out var text)) { - if (_document.TryGetText(out var text)) + var textBuffer = text.Container.GetTextBuffer(); + if (textBuffer.Properties.TryGetProperty(typeof(StateMachine), out StateMachine stateMachine)) { - var textBuffer = text.Container.GetTextBuffer(); - if (textBuffer.Properties.TryGetProperty(typeof(StateMachine), out StateMachine stateMachine)) + if (!stateMachine.CanInvokeRename(out _, cancellationToken: cancellationToken)) { - if (!stateMachine.CanInvokeRename(out _, cancellationToken: cancellationToken)) - { - // The rename tracking could be dismissed while a codefix is still cached - // in the lightbulb. If this happens, do not perform the rename requested - // and instead let the user know their fix will not be applied. - _document.Project.Solution.Services.GetService() - ?.SendNotification(EditorFeaturesResources.The_rename_tracking_session_was_cancelled_and_is_no_longer_available, severity: NotificationSeverity.Error); - return false; - } - - var snapshotSpan = stateMachine.TrackingSession.TrackingSpan.GetSpan(stateMachine.Buffer.CurrentSnapshot); - var newName = snapshotSpan.GetText(); - var displayText = string.Format(EditorFeaturesResources.Rename_0_to_1, stateMachine.TrackingSession.OriginalName, newName); - _renameTrackingCommitter = new RenameTrackingCommitter(stateMachine, snapshotSpan, _refactorNotifyServices, _undoHistoryRegistry, displayText); - return true; + // The rename tracking could be dismissed while a codefix is still cached + // in the lightbulb. If this happens, do not perform the rename requested + // and instead let the user know their fix will not be applied. + _document.Project.Solution.Services.GetService() + ?.SendNotification(EditorFeaturesResources.The_rename_tracking_session_was_cancelled_and_is_no_longer_available, severity: NotificationSeverity.Error); + return false; } - } - return false; + var snapshotSpan = stateMachine.TrackingSession.TrackingSpan.GetSpan(stateMachine.Buffer.CurrentSnapshot); + var newName = snapshotSpan.GetText(); + var displayText = string.Format(EditorFeaturesResources.Rename_0_to_1, stateMachine.TrackingSession.OriginalName, newName); + _renameTrackingCommitter = new RenameTrackingCommitter(stateMachine, snapshotSpan, _refactorNotifyServices, _undoHistoryRegistry, displayText); + return true; + } } - private sealed class RenameTrackingCommitterOperation(RenameTrackingCommitter committer, IThreadingContext threadingContext) : CodeActionOperation - { - private readonly RenameTrackingCommitter _committer = committer; - private readonly IThreadingContext _threadingContext = threadingContext; + return false; + } - internal override async Task TryApplyAsync( - Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) - { - var error = await _committer.TryCommitAsync(cancellationToken).ConfigureAwait(false); - if (error == null) - return true; - - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var notificationService = workspace.Services.GetService(); - notificationService.SendNotification( - error.Value.message, EditorFeaturesResources.Rename_Symbol, error.Value.severity); - return false; - } + private sealed class RenameTrackingCommitterOperation(RenameTrackingCommitter committer, IThreadingContext threadingContext) : CodeActionOperation + { + private readonly RenameTrackingCommitter _committer = committer; + private readonly IThreadingContext _threadingContext = threadingContext; + + internal override async Task TryApplyAsync( + Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) + { + var error = await _committer.TryCommitAsync(cancellationToken).ConfigureAwait(false); + if (error == null) + return true; + + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var notificationService = workspace.Services.GetService(); + notificationService.SendNotification( + error.Value.message, EditorFeaturesResources.Rename_Symbol, error.Value.severity); + return false; } } } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCommitter.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCommitter.cs index 6c0543806f00c..260f69db544da 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCommitter.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCommitter.cs @@ -21,280 +21,279 @@ using Microsoft.VisualStudio.Text.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +internal sealed partial class RenameTrackingTaggerProvider { - internal sealed partial class RenameTrackingTaggerProvider + private class RenameTrackingCommitter { - private class RenameTrackingCommitter + private readonly StateMachine _stateMachine; + private readonly SnapshotSpan _snapshotSpan; + private readonly IEnumerable _refactorNotifyServices; + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; + private readonly string _displayText; + private readonly AsyncLazy _renameSymbolResultGetter; + + public RenameTrackingCommitter( + StateMachine stateMachine, + SnapshotSpan snapshotSpan, + IEnumerable refactorNotifyServices, + ITextUndoHistoryRegistry undoHistoryRegistry, + string displayText) + { + _stateMachine = stateMachine; + _snapshotSpan = snapshotSpan; + _refactorNotifyServices = refactorNotifyServices; + _undoHistoryRegistry = undoHistoryRegistry; + _displayText = displayText; + _renameSymbolResultGetter = AsyncLazy.Create(c => RenameSymbolWorkerAsync(c)); + } + + /// + /// Returns non-null error message if renaming fails. + /// + public async Task<(NotificationSeverity severity, string message)?> TryCommitAsync(CancellationToken cancellationToken) { - private readonly StateMachine _stateMachine; - private readonly SnapshotSpan _snapshotSpan; - private readonly IEnumerable _refactorNotifyServices; - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; - private readonly string _displayText; - private readonly AsyncLazy _renameSymbolResultGetter; - - public RenameTrackingCommitter( - StateMachine stateMachine, - SnapshotSpan snapshotSpan, - IEnumerable refactorNotifyServices, - ITextUndoHistoryRegistry undoHistoryRegistry, - string displayText) + _stateMachine.ThreadingContext.ThrowIfNotOnUIThread(); + + try { - _stateMachine = stateMachine; - _snapshotSpan = snapshotSpan; - _refactorNotifyServices = refactorNotifyServices; - _undoHistoryRegistry = undoHistoryRegistry; - _displayText = displayText; - _renameSymbolResultGetter = AsyncLazy.Create(c => RenameSymbolWorkerAsync(c)); + return await TryApplyChangesToWorkspaceAsync(cancellationToken).ConfigureAwait(false); } - - /// - /// Returns non-null error message if renaming fails. - /// - public async Task<(NotificationSeverity severity, string message)?> TryCommitAsync(CancellationToken cancellationToken) + finally { - _stateMachine.ThreadingContext.ThrowIfNotOnUIThread(); - - try - { - return await TryApplyChangesToWorkspaceAsync(cancellationToken).ConfigureAwait(false); - } - finally - { - // Clear the state machine so that future updates to the same token work, and any text changes - // caused by this update are not interpreted as potential renames. Intentionally pass - // CancellationToken.None. We must clear this state out. - await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None); - _stateMachine.ClearTrackingSession(); - } + // Clear the state machine so that future updates to the same token work, and any text changes + // caused by this update are not interpreted as potential renames. Intentionally pass + // CancellationToken.None. We must clear this state out. + await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None); + _stateMachine.ClearTrackingSession(); } + } - public async Task RenameSymbolAsync(CancellationToken cancellationToken) - => await _renameSymbolResultGetter.GetValueAsync(cancellationToken).ConfigureAwait(false); + public async Task RenameSymbolAsync(CancellationToken cancellationToken) + => await _renameSymbolResultGetter.GetValueAsync(cancellationToken).ConfigureAwait(false); - private async Task RenameSymbolWorkerAsync(CancellationToken cancellationToken) - { - var document = _snapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - var newName = _snapshotSpan.GetText(); + private async Task RenameSymbolWorkerAsync(CancellationToken cancellationToken) + { + var document = _snapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + var newName = _snapshotSpan.GetText(); - Contract.ThrowIfNull(document, "Invoked rename tracking smart tag but cannot find the document for the snapshot span."); + Contract.ThrowIfNull(document, "Invoked rename tracking smart tag but cannot find the document for the snapshot span."); - // Get copy of solution with the original name in the place of the renamed name - var solutionWithOriginalName = await CreateSolutionWithOriginalNameAsync( - document, cancellationToken).ConfigureAwait(false); + // Get copy of solution with the original name in the place of the renamed name + var solutionWithOriginalName = await CreateSolutionWithOriginalNameAsync( + document, cancellationToken).ConfigureAwait(false); - var symbol = await TryGetSymbolAsync(solutionWithOriginalName, document.Id, cancellationToken).ConfigureAwait(false); - Contract.ThrowIfNull(symbol, "Invoked rename tracking smart tag but cannot find the symbol."); + var symbol = await TryGetSymbolAsync(solutionWithOriginalName, document.Id, cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(symbol, "Invoked rename tracking smart tag but cannot find the symbol."); - var options = new SymbolRenameOptions(RenameOverloads: _stateMachine.TrackingSession.ForceRenameOverloads); - var renamedSolution = await Renamer.RenameSymbolAsync(solutionWithOriginalName, symbol, options, newName, cancellationToken).ConfigureAwait(false); - return new RenameTrackingSolutionSet(symbol, solutionWithOriginalName, renamedSolution); - } + var options = new SymbolRenameOptions(RenameOverloads: _stateMachine.TrackingSession.ForceRenameOverloads); + var renamedSolution = await Renamer.RenameSymbolAsync(solutionWithOriginalName, symbol, options, newName, cancellationToken).ConfigureAwait(false); + return new RenameTrackingSolutionSet(symbol, solutionWithOriginalName, renamedSolution); + } - /// - /// Returns non-null error message if renaming fails. - /// - private async Task<(NotificationSeverity, string)?> TryApplyChangesToWorkspaceAsync(CancellationToken cancellationToken) + /// + /// Returns non-null error message if renaming fails. + /// + private async Task<(NotificationSeverity, string)?> TryApplyChangesToWorkspaceAsync(CancellationToken cancellationToken) + { + // Now that the necessary work has been done to create the intermediate and final + // solutions during PreparePreview, check one more time for cancellation before making all of the + // workspace changes. + cancellationToken.ThrowIfCancellationRequested(); + + // Undo must backtrack to the state with the original identifier before the state + // with the user-edited identifier. For example, + // + // 1. Original: void M() { M(); } + // 2. User types: void Method() { M(); } + // 3. Invoke rename: void Method() { Method(); } + // + // The undo process should be as follows + // 1. Back to original name everywhere: void M() { M(); } // No tracking session + // 2. Back to state 2 above: void Method() { M(); } // Resume tracking session + // 3. Finally, start undoing typing: void M() { M(); } + // + // As far as the user can see, undo state 1 never actually existed so we must insert + // a state here to facilitate the undo. Do the work to obtain the intermediate and + // final solution without updating the workspace, and then finally disallow + // cancellation and update the workspace twice. + + var renameTrackingSolutionSet = await RenameSymbolAsync(cancellationToken).ConfigureAwait(false); + + var document = _snapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + var newName = _snapshotSpan.GetText(); + + var workspace = document.Project.Solution.Workspace; + + // Since the state machine is only watching buffer changes, it will interpret the + // text changes caused by undo and redo actions as potential renames, so carefully + // update the state machine after undo/redo actions. + + var changedDocuments = renameTrackingSolutionSet.RenamedSolution.GetChangedDocuments(renameTrackingSolutionSet.OriginalSolution); + try { - // Now that the necessary work has been done to create the intermediate and final - // solutions during PreparePreview, check one more time for cancellation before making all of the - // workspace changes. - cancellationToken.ThrowIfCancellationRequested(); - - // Undo must backtrack to the state with the original identifier before the state - // with the user-edited identifier. For example, - // - // 1. Original: void M() { M(); } - // 2. User types: void Method() { M(); } - // 3. Invoke rename: void Method() { Method(); } - // - // The undo process should be as follows - // 1. Back to original name everywhere: void M() { M(); } // No tracking session - // 2. Back to state 2 above: void Method() { M(); } // Resume tracking session - // 3. Finally, start undoing typing: void M() { M(); } - // - // As far as the user can see, undo state 1 never actually existed so we must insert - // a state here to facilitate the undo. Do the work to obtain the intermediate and - // final solution without updating the workspace, and then finally disallow - // cancellation and update the workspace twice. - - var renameTrackingSolutionSet = await RenameSymbolAsync(cancellationToken).ConfigureAwait(false); - - var document = _snapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - var newName = _snapshotSpan.GetText(); - - var workspace = document.Project.Solution.Workspace; - - // Since the state machine is only watching buffer changes, it will interpret the - // text changes caused by undo and redo actions as potential renames, so carefully - // update the state machine after undo/redo actions. - - var changedDocuments = renameTrackingSolutionSet.RenamedSolution.GetChangedDocuments(renameTrackingSolutionSet.OriginalSolution); - try - { - // When this action is undone (the user has undone twice), restore the state - // machine to so that they can continue their original rename tracking session. - - await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var trackingSessionId = _stateMachine.StoreCurrentTrackingSessionAndGenerateId(); - var result = TryUpdateWorkspaceForResetOfTypedIdentifier(workspace, renameTrackingSolutionSet.OriginalSolution, trackingSessionId); - if (result is not null) - return result; - - // Now that the solution is back in its original state, notify third parties about - // the coming rename operation. - if (!_refactorNotifyServices.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocuments, renameTrackingSolutionSet.Symbol, newName, throwOnFailure: false)) - return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid); - - // move all changes to final solution based on the workspace's current solution, since the current solution - // got updated when we reset it above. - var finalSolution = workspace.CurrentSolution; - foreach (var docId in changedDocuments) - { - // because changes have already been made to the workspace (UpdateWorkspaceForResetOfTypedIdentifier() above), - // these calls can't be cancelled and must be allowed to complete. - var root = await renameTrackingSolutionSet.RenamedSolution.GetDocument(docId).GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - finalSolution = finalSolution.WithDocumentSyntaxRoot(docId, root); - } - - // Undo/redo on this action must always clear the state machine - await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - return TryUpdateWorkspaceForGlobalIdentifierRename( - workspace, - finalSolution, - _displayText, - changedDocuments, - renameTrackingSolutionSet.Symbol, - newName, - trackingSessionId); - } - finally + // When this action is undone (the user has undone twice), restore the state + // machine to so that they can continue their original rename tracking session. + + await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var trackingSessionId = _stateMachine.StoreCurrentTrackingSessionAndGenerateId(); + var result = TryUpdateWorkspaceForResetOfTypedIdentifier(workspace, renameTrackingSolutionSet.OriginalSolution, trackingSessionId); + if (result is not null) + return result; + + // Now that the solution is back in its original state, notify third parties about + // the coming rename operation. + if (!_refactorNotifyServices.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocuments, renameTrackingSolutionSet.Symbol, newName, throwOnFailure: false)) + return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid); + + // move all changes to final solution based on the workspace's current solution, since the current solution + // got updated when we reset it above. + var finalSolution = workspace.CurrentSolution; + foreach (var docId in changedDocuments) { - // Explicit CancellationToken.None here. We must clean up our state no matter what. - await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None); - RenameTrackingDismisser.DismissRenameTracking(workspace, changedDocuments); + // because changes have already been made to the workspace (UpdateWorkspaceForResetOfTypedIdentifier() above), + // these calls can't be cancelled and must be allowed to complete. + var root = await renameTrackingSolutionSet.RenamedSolution.GetDocument(docId).GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + finalSolution = finalSolution.WithDocumentSyntaxRoot(docId, root); } - } - private async Task CreateSolutionWithOriginalNameAsync( - Document document, CancellationToken cancellationToken) + // Undo/redo on this action must always clear the state machine + await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + return TryUpdateWorkspaceForGlobalIdentifierRename( + workspace, + finalSolution, + _displayText, + changedDocuments, + renameTrackingSolutionSet.Symbol, + newName, + trackingSessionId); + } + finally { - var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var fullText = syntaxTree.GetText(cancellationToken); - var textChange = new TextChange(new TextSpan(_snapshotSpan.Start, _snapshotSpan.Length), _stateMachine.TrackingSession.OriginalName); + // Explicit CancellationToken.None here. We must clean up our state no matter what. + await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None); + RenameTrackingDismisser.DismissRenameTracking(workspace, changedDocuments); + } + } - var newFullText = fullText.WithChanges(textChange); + private async Task CreateSolutionWithOriginalNameAsync( + Document document, CancellationToken cancellationToken) + { + var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var fullText = syntaxTree.GetText(cancellationToken); + var textChange = new TextChange(new TextSpan(_snapshotSpan.Start, _snapshotSpan.Length), _stateMachine.TrackingSession.OriginalName); + + var newFullText = fullText.WithChanges(textChange); #if DEBUG - var syntaxTreeWithOriginalName = syntaxTree.WithChangedText(newFullText); - var documentWithOriginalName = document.WithSyntaxRoot(syntaxTreeWithOriginalName.GetRoot(cancellationToken)); + var syntaxTreeWithOriginalName = syntaxTree.WithChangedText(newFullText); + var documentWithOriginalName = document.WithSyntaxRoot(syntaxTreeWithOriginalName.GetRoot(cancellationToken)); - Debug.Assert(newFullText.ToString() == documentWithOriginalName.GetTextSynchronously(cancellationToken).ToString()); + Debug.Assert(newFullText.ToString() == documentWithOriginalName.GetTextSynchronously(cancellationToken).ToString()); #endif - // Apply the original name to all linked documents to construct a consistent solution - var solution = document.Project.Solution; - foreach (var documentId in document.GetLinkedDocumentIds().Add(document.Id)) - { - solution = solution.WithDocumentText(documentId, newFullText); - } - - return solution; + // Apply the original name to all linked documents to construct a consistent solution + var solution = document.Project.Solution; + foreach (var documentId in document.GetLinkedDocumentIds().Add(document.Id)) + { + solution = solution.WithDocumentText(documentId, newFullText); } - private async Task TryGetSymbolAsync(Solution solutionWithOriginalName, DocumentId documentId, CancellationToken cancellationToken) - { - var documentWithOriginalName = solutionWithOriginalName.GetDocument(documentId); - var syntaxTreeWithOriginalName = await documentWithOriginalName.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + return solution; + } - var syntaxFacts = documentWithOriginalName.GetLanguageService(); - var semanticFacts = documentWithOriginalName.GetLanguageService(); - var semanticModel = await documentWithOriginalName.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + private async Task TryGetSymbolAsync(Solution solutionWithOriginalName, DocumentId documentId, CancellationToken cancellationToken) + { + var documentWithOriginalName = solutionWithOriginalName.GetDocument(documentId); + var syntaxTreeWithOriginalName = await documentWithOriginalName.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var token = await syntaxTreeWithOriginalName.GetTouchingWordAsync(_snapshotSpan.Start, syntaxFacts, cancellationToken).ConfigureAwait(false); - var tokenRenameInfo = RenameUtilities.GetTokenRenameInfo(semanticFacts, semanticModel, token, cancellationToken); + var syntaxFacts = documentWithOriginalName.GetLanguageService(); + var semanticFacts = documentWithOriginalName.GetLanguageService(); + var semanticModel = await documentWithOriginalName.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - return tokenRenameInfo.HasSymbols ? tokenRenameInfo.Symbols.First() : null; - } + var token = await syntaxTreeWithOriginalName.GetTouchingWordAsync(_snapshotSpan.Start, syntaxFacts, cancellationToken).ConfigureAwait(false); + var tokenRenameInfo = RenameUtilities.GetTokenRenameInfo(semanticFacts, semanticModel, token, cancellationToken); - /// - /// Returns non-null error message if renaming fails. - /// - private (NotificationSeverity, string)? TryUpdateWorkspaceForResetOfTypedIdentifier(Workspace workspace, Solution newSolution, int trackingSessionId) - { - _stateMachine.ThreadingContext.ThrowIfNotOnUIThread(); + return tokenRenameInfo.HasSymbols ? tokenRenameInfo.Symbols.First() : null; + } - // Update document in an ITextUndoTransaction with custom behaviors on undo/redo to - // deal with the state machine. + /// + /// Returns non-null error message if renaming fails. + /// + private (NotificationSeverity, string)? TryUpdateWorkspaceForResetOfTypedIdentifier(Workspace workspace, Solution newSolution, int trackingSessionId) + { + _stateMachine.ThreadingContext.ThrowIfNotOnUIThread(); - var undoHistory = _undoHistoryRegistry.RegisterHistory(_stateMachine.Buffer); - using var localUndoTransaction = undoHistory.CreateTransaction(EditorFeaturesResources.Text_Buffer_Change); + // Update document in an ITextUndoTransaction with custom behaviors on undo/redo to + // deal with the state machine. - var undoPrimitiveBefore = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: true); - localUndoTransaction.AddUndo(undoPrimitiveBefore); + var undoHistory = _undoHistoryRegistry.RegisterHistory(_stateMachine.Buffer); + using var localUndoTransaction = undoHistory.CreateTransaction(EditorFeaturesResources.Text_Buffer_Change); - if (!workspace.TryApplyChanges(newSolution)) - return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace); + var undoPrimitiveBefore = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: true); + localUndoTransaction.AddUndo(undoPrimitiveBefore); - // If we successfully updated the workspace then make sure the undo transaction is committed and is - // always able to undo anything any other external listener did. + if (!workspace.TryApplyChanges(newSolution)) + return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace); - // Never resume tracking session on redo - var undoPrimitiveAfter = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false); - localUndoTransaction.AddUndo(undoPrimitiveAfter); + // If we successfully updated the workspace then make sure the undo transaction is committed and is + // always able to undo anything any other external listener did. - localUndoTransaction.Complete(); + // Never resume tracking session on redo + var undoPrimitiveAfter = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false); + localUndoTransaction.AddUndo(undoPrimitiveAfter); - return null; - } + localUndoTransaction.Complete(); - /// - /// Returns non-null error message if renaming fails. - /// - private (NotificationSeverity, string)? TryUpdateWorkspaceForGlobalIdentifierRename( - Workspace workspace, - Solution newSolution, - string undoName, - IEnumerable changedDocuments, - ISymbol symbol, - string newName, - int trackingSessionId) - { - _stateMachine.ThreadingContext.ThrowIfNotOnUIThread(); + return null; + } - // Perform rename in a workspace undo action so that undo will revert all - // references. It should also be performed in an ITextUndoTransaction to handle + /// + /// Returns non-null error message if renaming fails. + /// + private (NotificationSeverity, string)? TryUpdateWorkspaceForGlobalIdentifierRename( + Workspace workspace, + Solution newSolution, + string undoName, + IEnumerable changedDocuments, + ISymbol symbol, + string newName, + int trackingSessionId) + { + _stateMachine.ThreadingContext.ThrowIfNotOnUIThread(); - var undoHistory = _undoHistoryRegistry.RegisterHistory(_stateMachine.Buffer); + // Perform rename in a workspace undo action so that undo will revert all + // references. It should also be performed in an ITextUndoTransaction to handle - using var workspaceUndoTransaction = workspace.OpenGlobalUndoTransaction(undoName); - using var localUndoTransaction = undoHistory.CreateTransaction(undoName); + var undoHistory = _undoHistoryRegistry.RegisterHistory(_stateMachine.Buffer); - var undoPrimitiveBefore = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false); - localUndoTransaction.AddUndo(undoPrimitiveBefore); + using var workspaceUndoTransaction = workspace.OpenGlobalUndoTransaction(undoName); + using var localUndoTransaction = undoHistory.CreateTransaction(undoName); - if (!workspace.TryApplyChanges(newSolution)) - return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace); + var undoPrimitiveBefore = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false); + localUndoTransaction.AddUndo(undoPrimitiveBefore); - try - { - if (!_refactorNotifyServices.TryOnAfterGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure: false)) - return (NotificationSeverity.Information, EditorFeaturesResources.Rename_operation_was_not_properly_completed_Some_file_might_not_have_been_updated); + if (!workspace.TryApplyChanges(newSolution)) + return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace); - return null; - } - finally - { - // If we successfully updated the workspace then make sure the undo transaction is committed and is - // always able to undo anything any other external listener did. + try + { + if (!_refactorNotifyServices.TryOnAfterGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure: false)) + return (NotificationSeverity.Information, EditorFeaturesResources.Rename_operation_was_not_properly_completed_Some_file_might_not_have_been_updated); - // Never resume tracking session on redo - var undoPrimitiveAfter = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false); - localUndoTransaction.AddUndo(undoPrimitiveAfter); + return null; + } + finally + { + // If we successfully updated the workspace then make sure the undo transaction is committed and is + // always able to undo anything any other external listener did. - localUndoTransaction.Complete(); - workspaceUndoTransaction.Commit(); - } + // Never resume tracking session on redo + var undoPrimitiveAfter = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false); + localUndoTransaction.AddUndo(undoPrimitiveAfter); + + localUndoTransaction.Complete(); + workspaceUndoTransaction.Commit(); } } } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingSolutionSet.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingSolutionSet.cs index 6e88174128d69..02d248980c121 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingSolutionSet.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingSolutionSet.cs @@ -4,21 +4,20 @@ #nullable disable -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +internal sealed partial class RenameTrackingTaggerProvider { - internal sealed partial class RenameTrackingTaggerProvider + /// + /// Tracks the solution before and after rename. + /// + private class RenameTrackingSolutionSet( + ISymbol symbolToRename, + Solution originalSolution, + Solution renamedSolution) { - /// - /// Tracks the solution before and after rename. - /// - private class RenameTrackingSolutionSet( - ISymbol symbolToRename, - Solution originalSolution, - Solution renamedSolution) - { - public ISymbol Symbol { get; } = symbolToRename; - public Solution OriginalSolution { get; } = originalSolution; - public Solution RenamedSolution { get; } = renamedSolution; - } + public ISymbol Symbol { get; } = symbolToRename; + public Solution OriginalSolution { get; } = originalSolution; + public Solution RenamedSolution { get; } = renamedSolution; } } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.StateMachine.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.StateMachine.cs index 69000d1009a29..3691c669f2017 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.StateMachine.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.StateMachine.cs @@ -24,375 +24,374 @@ using Microsoft.VisualStudio.Text.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +internal sealed partial class RenameTrackingTaggerProvider { - internal sealed partial class RenameTrackingTaggerProvider + /// + /// Keeps track of the rename tracking state for a given text buffer by tracking its + /// changes over time. + /// + private sealed class StateMachine { - /// - /// Keeps track of the rename tracking state for a given text buffer by tracking its - /// changes over time. - /// - private sealed class StateMachine + public readonly IThreadingContext ThreadingContext; + + private readonly IInlineRenameService _inlineRenameService; + private readonly IAsynchronousOperationListener _asyncListener; + private readonly ITextBuffer _buffer; + private readonly IDiagnosticAnalyzerService _diagnosticAnalyzerService; + + // Store committed sessions so they can be restored on undo/redo. The undo transactions + // may live beyond the lifetime of the buffer tracked by this StateMachine, so storing + // them here allows them to be correctly cleaned up when the buffer goes away. + private readonly IList _committedSessions = []; + + private int _refCount; + + public readonly IGlobalOptionService GlobalOptions; + public TrackingSession TrackingSession { get; private set; } + public ITextBuffer Buffer => _buffer; + + public event Action TrackingSessionUpdated = delegate { }; + public event Action TrackingSessionCleared = delegate { }; + + public StateMachine( + IThreadingContext threadingContext, + ITextBuffer buffer, + IInlineRenameService inlineRenameService, + IDiagnosticAnalyzerService diagnosticAnalyzerService, + IGlobalOptionService globalOptions, + IAsynchronousOperationListener asyncListener) { - public readonly IThreadingContext ThreadingContext; - - private readonly IInlineRenameService _inlineRenameService; - private readonly IAsynchronousOperationListener _asyncListener; - private readonly ITextBuffer _buffer; - private readonly IDiagnosticAnalyzerService _diagnosticAnalyzerService; - - // Store committed sessions so they can be restored on undo/redo. The undo transactions - // may live beyond the lifetime of the buffer tracked by this StateMachine, so storing - // them here allows them to be correctly cleaned up when the buffer goes away. - private readonly IList _committedSessions = []; - - private int _refCount; - - public readonly IGlobalOptionService GlobalOptions; - public TrackingSession TrackingSession { get; private set; } - public ITextBuffer Buffer => _buffer; - - public event Action TrackingSessionUpdated = delegate { }; - public event Action TrackingSessionCleared = delegate { }; - - public StateMachine( - IThreadingContext threadingContext, - ITextBuffer buffer, - IInlineRenameService inlineRenameService, - IDiagnosticAnalyzerService diagnosticAnalyzerService, - IGlobalOptionService globalOptions, - IAsynchronousOperationListener asyncListener) + ThreadingContext = threadingContext; + _buffer = buffer; + _buffer.Changed += Buffer_Changed; + _inlineRenameService = inlineRenameService; + _asyncListener = asyncListener; + _diagnosticAnalyzerService = diagnosticAnalyzerService; + GlobalOptions = globalOptions; + } + + private void Buffer_Changed(object sender, TextContentChangedEventArgs e) + { + ThreadingContext.ThrowIfNotOnUIThread(); + + if (!GlobalOptions.GetOption(RenameTrackingOptionsStorage.RenameTracking)) { - ThreadingContext = threadingContext; - _buffer = buffer; - _buffer.Changed += Buffer_Changed; - _inlineRenameService = inlineRenameService; - _asyncListener = asyncListener; - _diagnosticAnalyzerService = diagnosticAnalyzerService; - GlobalOptions = globalOptions; + // When disabled, ignore all text buffer changes and do not trigger retagging + return; } - private void Buffer_Changed(object sender, TextContentChangedEventArgs e) + using (Logger.LogBlock(FunctionId.Rename_Tracking_BufferChanged, CancellationToken.None)) { - ThreadingContext.ThrowIfNotOnUIThread(); - - if (!GlobalOptions.GetOption(RenameTrackingOptionsStorage.RenameTracking)) + // When the buffer changes, several things might be happening: + // 1. If a non-identifier character has been added or deleted, we stop tracking + // completely. + // 2. Otherwise, if the changes are completely contained an existing session, then + // continue that session. + // 3. Otherwise, we're starting a new tracking session. Find and track the span of + // the relevant word in the foreground, and use a task to figure out whether the + // original word was a renameable identifier or not. + + if (e.Changes.Count != 1 || ShouldClearTrackingSession(e.Changes.Single())) { - // When disabled, ignore all text buffer changes and do not trigger retagging + ClearTrackingSession(); return; } - using (Logger.LogBlock(FunctionId.Rename_Tracking_BufferChanged, CancellationToken.None)) - { - // When the buffer changes, several things might be happening: - // 1. If a non-identifier character has been added or deleted, we stop tracking - // completely. - // 2. Otherwise, if the changes are completely contained an existing session, then - // continue that session. - // 3. Otherwise, we're starting a new tracking session. Find and track the span of - // the relevant word in the foreground, and use a task to figure out whether the - // original word was a renameable identifier or not. - - if (e.Changes.Count != 1 || ShouldClearTrackingSession(e.Changes.Single())) - { - ClearTrackingSession(); - return; - } - - // The change is trackable. Figure out whether we should continue an existing - // session - - var change = e.Changes.Single(); - - if (this.TrackingSession == null) - { - StartTrackingSession(e); - return; - } + // The change is trackable. Figure out whether we should continue an existing + // session - // There's an existing session. Continue that session if the current change is - // contained inside the tracking span. + var change = e.Changes.Single(); - var trackingSpanInNewSnapshot = this.TrackingSession.TrackingSpan.GetSpan(e.After); - if (trackingSpanInNewSnapshot.Contains(change.NewSpan)) - { - // Continuing an existing tracking session. If there may have been a tag - // showing, then update the tags. - UpdateTrackingSessionIfRenamable(); - } - else - { - StartTrackingSession(e); - } - } - } - - public void UpdateTrackingSessionIfRenamable() - { - ThreadingContext.ThrowIfNotOnUIThread(); - if (this.TrackingSession.IsDefinitelyRenamableIdentifier()) + if (this.TrackingSession == null) { - this.TrackingSession.CheckNewIdentifier(this, _buffer.CurrentSnapshot); - TrackingSessionUpdated(); + StartTrackingSession(e); + return; } - } - private bool ShouldClearTrackingSession(ITextChange change) - { - ThreadingContext.ThrowIfNotOnUIThread(); - if (!TryGetSyntaxFactsService(out var syntaxFactsService)) + // There's an existing session. Continue that session if the current change is + // contained inside the tracking span. + + var trackingSpanInNewSnapshot = this.TrackingSession.TrackingSpan.GetSpan(e.After); + if (trackingSpanInNewSnapshot.Contains(change.NewSpan)) { - return true; + // Continuing an existing tracking session. If there may have been a tag + // showing, then update the tags. + UpdateTrackingSessionIfRenamable(); } - - // The editor will replace virtual space with spaces and/or tabs when typing on a - // previously blank line. Trim these characters from the start of change.NewText. If - // the resulting change is empty (the user just typed a ), clear the session. - var changedText = change.OldText + change.NewText.TrimStart(' ', '\t'); - if (changedText.IsEmpty()) + else { - return true; + StartTrackingSession(e); } - - return changedText.Any(c => !IsTrackableCharacter(syntaxFactsService, c)); } + } - private void StartTrackingSession(TextContentChangedEventArgs eventArgs) + public void UpdateTrackingSessionIfRenamable() + { + ThreadingContext.ThrowIfNotOnUIThread(); + if (this.TrackingSession.IsDefinitelyRenamableIdentifier()) { - ThreadingContext.ThrowIfNotOnUIThread(); - ClearTrackingSession(); + this.TrackingSession.CheckNewIdentifier(this, _buffer.CurrentSnapshot); + TrackingSessionUpdated(); + } + } - if (_inlineRenameService.ActiveSession != null) - { - return; - } + private bool ShouldClearTrackingSession(ITextChange change) + { + ThreadingContext.ThrowIfNotOnUIThread(); + if (!TryGetSyntaxFactsService(out var syntaxFactsService)) + { + return true; + } - // Synchronously find the tracking span in the old document. + // The editor will replace virtual space with spaces and/or tabs when typing on a + // previously blank line. Trim these characters from the start of change.NewText. If + // the resulting change is empty (the user just typed a ), clear the session. + var changedText = change.OldText + change.NewText.TrimStart(' ', '\t'); + if (changedText.IsEmpty()) + { + return true; + } - var change = eventArgs.Changes.Single(); - var beforeText = eventArgs.Before.AsText(); - if (!TryGetSyntaxFactsService(out var syntaxFactsService)) - { - return; - } + return changedText.Any(c => !IsTrackableCharacter(syntaxFactsService, c)); + } - var leftSidePosition = change.OldPosition; - var rightSidePosition = change.OldPosition + change.OldText.Length; + private void StartTrackingSession(TextContentChangedEventArgs eventArgs) + { + ThreadingContext.ThrowIfNotOnUIThread(); + ClearTrackingSession(); - while (leftSidePosition > 0 && IsTrackableCharacter(syntaxFactsService, beforeText[leftSidePosition - 1])) - { - leftSidePosition--; - } + if (_inlineRenameService.ActiveSession != null) + { + return; + } - while (rightSidePosition < beforeText.Length && IsTrackableCharacter(syntaxFactsService, beforeText[rightSidePosition])) - { - rightSidePosition++; - } + // Synchronously find the tracking span in the old document. - var originalSpan = new Span(leftSidePosition, rightSidePosition - leftSidePosition); - this.TrackingSession = new TrackingSession(this, new SnapshotSpan(eventArgs.Before, originalSpan), _asyncListener); + var change = eventArgs.Changes.Single(); + var beforeText = eventArgs.Before.AsText(); + if (!TryGetSyntaxFactsService(out var syntaxFactsService)) + { + return; } - private static bool IsTrackableCharacter(ISyntaxFactsService syntaxFactsService, char c) + var leftSidePosition = change.OldPosition; + var rightSidePosition = change.OldPosition + change.OldText.Length; + + while (leftSidePosition > 0 && IsTrackableCharacter(syntaxFactsService, beforeText[leftSidePosition - 1])) { - // Allow identifier part characters at the beginning of strings (even if they are - // not identifier start characters). If an intermediate name is not valid, the smart - // tag will not be shown due to later checks. Also allow escape chars anywhere as - // they might be in the middle of a complex edit. - return syntaxFactsService.IsIdentifierPartCharacter(c) || syntaxFactsService.IsIdentifierEscapeCharacter(c); + leftSidePosition--; } - public bool ClearTrackingSession() + while (rightSidePosition < beforeText.Length && IsTrackableCharacter(syntaxFactsService, beforeText[rightSidePosition])) { - ThreadingContext.ThrowIfNotOnUIThread(); - - if (this.TrackingSession != null) - { - // Disallow the existing TrackingSession from triggering IdentifierFound. - var previousTrackingSession = this.TrackingSession; - this.TrackingSession = null; - - previousTrackingSession.Cancel(); + rightSidePosition++; + } - // If there may have been a tag showing, then actually clear the tags. - if (previousTrackingSession.IsDefinitelyRenamableIdentifier()) - { - TrackingSessionCleared(previousTrackingSession.TrackingSpan); - } + var originalSpan = new Span(leftSidePosition, rightSidePosition - leftSidePosition); + this.TrackingSession = new TrackingSession(this, new SnapshotSpan(eventArgs.Before, originalSpan), _asyncListener); + } - return true; - } + private static bool IsTrackableCharacter(ISyntaxFactsService syntaxFactsService, char c) + { + // Allow identifier part characters at the beginning of strings (even if they are + // not identifier start characters). If an intermediate name is not valid, the smart + // tag will not be shown due to later checks. Also allow escape chars anywhere as + // they might be in the middle of a complex edit. + return syntaxFactsService.IsIdentifierPartCharacter(c) || syntaxFactsService.IsIdentifierEscapeCharacter(c); + } - return false; - } + public bool ClearTrackingSession() + { + ThreadingContext.ThrowIfNotOnUIThread(); - public bool ClearVisibleTrackingSession() + if (this.TrackingSession != null) { - ThreadingContext.ThrowIfNotOnUIThread(); - - if (this.TrackingSession != null && this.TrackingSession.IsDefinitelyRenamableIdentifier()) - { - var document = _buffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document != null) - { - // When rename tracking is dismissed via escape, we no longer wish to - // provide a diagnostic/codefix, but nothing has changed in the workspace - // to trigger the diagnostic system to reanalyze, so we trigger it - // manually. - - _diagnosticAnalyzerService?.Reanalyze( - document.Project.Solution.Workspace, - projectIds: null, - documentIds: SpecializedCollections.SingletonEnumerable(document.Id), - highPriority: true); - } + // Disallow the existing TrackingSession from triggering IdentifierFound. + var previousTrackingSession = this.TrackingSession; + this.TrackingSession = null; - // Disallow the existing TrackingSession from triggering IdentifierFound. - var previousTrackingSession = this.TrackingSession; - this.TrackingSession = null; + previousTrackingSession.Cancel(); - previousTrackingSession.Cancel(); + // If there may have been a tag showing, then actually clear the tags. + if (previousTrackingSession.IsDefinitelyRenamableIdentifier()) + { TrackingSessionCleared(previousTrackingSession.TrackingSpan); - return true; } - return false; + return true; } - internal int StoreCurrentTrackingSessionAndGenerateId() - { - ThreadingContext.ThrowIfNotOnUIThread(); + return false; + } - var existingIndex = _committedSessions.IndexOf(TrackingSession); - if (existingIndex >= 0) + public bool ClearVisibleTrackingSession() + { + ThreadingContext.ThrowIfNotOnUIThread(); + + if (this.TrackingSession != null && this.TrackingSession.IsDefinitelyRenamableIdentifier()) + { + var document = _buffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document != null) { - return existingIndex; + // When rename tracking is dismissed via escape, we no longer wish to + // provide a diagnostic/codefix, but nothing has changed in the workspace + // to trigger the diagnostic system to reanalyze, so we trigger it + // manually. + + _diagnosticAnalyzerService?.Reanalyze( + document.Project.Solution.Workspace, + projectIds: null, + documentIds: SpecializedCollections.SingletonEnumerable(document.Id), + highPriority: true); } - var index = _committedSessions.Count; - _committedSessions.Insert(index, TrackingSession); - return index; + // Disallow the existing TrackingSession from triggering IdentifierFound. + var previousTrackingSession = this.TrackingSession; + this.TrackingSession = null; + + previousTrackingSession.Cancel(); + TrackingSessionCleared(previousTrackingSession.TrackingSpan); + return true; } - public bool CanInvokeRename( - [NotNullWhen(true)] out TrackingSession trackingSession, - bool isSmartTagCheck = false, bool waitForResult = false, CancellationToken cancellationToken = default) - { - // This needs to be able to run on a background thread for the diagnostic. + return false; + } - trackingSession = this.TrackingSession; - if (trackingSession == null) - return false; + internal int StoreCurrentTrackingSessionAndGenerateId() + { + ThreadingContext.ThrowIfNotOnUIThread(); - return TryGetSyntaxFactsService(out var syntaxFactsService) && TryGetLanguageHeuristicsService(out var languageHeuristicsService) && - trackingSession.CanInvokeRename(syntaxFactsService, languageHeuristicsService, isSmartTagCheck, waitForResult, cancellationToken); + var existingIndex = _committedSessions.IndexOf(TrackingSession); + if (existingIndex >= 0) + { + return existingIndex; } - internal (CodeAction action, TextSpan renameSpan) TryGetCodeAction( - Document document, SourceText text, TextSpan userSpan, - IEnumerable refactorNotifyServices, - ITextUndoHistoryRegistry undoHistoryRegistry, - CancellationToken cancellationToken) + var index = _committedSessions.Count; + _committedSessions.Insert(index, TrackingSession); + return index; + } + + public bool CanInvokeRename( + [NotNullWhen(true)] out TrackingSession trackingSession, + bool isSmartTagCheck = false, bool waitForResult = false, CancellationToken cancellationToken = default) + { + // This needs to be able to run on a background thread for the diagnostic. + + trackingSession = this.TrackingSession; + if (trackingSession == null) + return false; + + return TryGetSyntaxFactsService(out var syntaxFactsService) && TryGetLanguageHeuristicsService(out var languageHeuristicsService) && + trackingSession.CanInvokeRename(syntaxFactsService, languageHeuristicsService, isSmartTagCheck, waitForResult, cancellationToken); + } + + internal (CodeAction action, TextSpan renameSpan) TryGetCodeAction( + Document document, SourceText text, TextSpan userSpan, + IEnumerable refactorNotifyServices, + ITextUndoHistoryRegistry undoHistoryRegistry, + CancellationToken cancellationToken) + { + try { - try + // This can be called on a background thread. We are being asked whether a + // lightbulb should be shown for the given document, but we only know about the + // current state of the buffer. Compare the text to see if we should bail early. + // Even if the text is the same, the buffer may change on the UI thread during this + // method. If it does, we may give an incorrect response, but the diagnostics + // engine will know that the document changed and not display the lightbulb anyway. + + if (Buffer.AsTextContainer().CurrentText == text && + CanInvokeRename(out var trackingSession, waitForResult: true, cancellationToken: cancellationToken)) { - // This can be called on a background thread. We are being asked whether a - // lightbulb should be shown for the given document, but we only know about the - // current state of the buffer. Compare the text to see if we should bail early. - // Even if the text is the same, the buffer may change on the UI thread during this - // method. If it does, we may give an incorrect response, but the diagnostics - // engine will know that the document changed and not display the lightbulb anyway. - - if (Buffer.AsTextContainer().CurrentText == text && - CanInvokeRename(out var trackingSession, waitForResult: true, cancellationToken: cancellationToken)) + var snapshotSpan = trackingSession.TrackingSpan.GetSpan(Buffer.CurrentSnapshot); + + // user needs to be on the same line as the diagnostic location. + if (text.AreOnSameLine(userSpan.Start, snapshotSpan.Start)) { - var snapshotSpan = trackingSession.TrackingSpan.GetSpan(Buffer.CurrentSnapshot); - - // user needs to be on the same line as the diagnostic location. - if (text.AreOnSameLine(userSpan.Start, snapshotSpan.Start)) - { - var title = string.Format( - EditorFeaturesResources.Rename_0_to_1, - trackingSession.OriginalName, - snapshotSpan.GetText()); - - return (new RenameTrackingCodeAction(ThreadingContext, document, title, refactorNotifyServices, undoHistoryRegistry, GlobalOptions), - snapshotSpan.Span.ToTextSpan()); - } - } + var title = string.Format( + EditorFeaturesResources.Rename_0_to_1, + trackingSession.OriginalName, + snapshotSpan.GetText()); - return default; - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) - { - throw ExceptionUtilities.Unreachable(); + return (new RenameTrackingCodeAction(ThreadingContext, document, title, refactorNotifyServices, undoHistoryRegistry, GlobalOptions), + snapshotSpan.Span.ToTextSpan()); + } } - } - - public void RestoreTrackingSession(int trackingSessionId) - { - ThreadingContext.ThrowIfNotOnUIThread(); - ClearTrackingSession(); - this.TrackingSession = _committedSessions[trackingSessionId]; - TrackingSessionUpdated(); + return default; } - - public void OnTrackingSessionUpdated(TrackingSession trackingSession) + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) { - ThreadingContext.ThrowIfNotOnUIThread(); - - if (this.TrackingSession == trackingSession) - { - TrackingSessionUpdated(); - } + throw ExceptionUtilities.Unreachable(); } + } - private bool TryGetSyntaxFactsService(out ISyntaxFactsService syntaxFactsService) - { - // Can be called on a background thread + public void RestoreTrackingSession(int trackingSessionId) + { + ThreadingContext.ThrowIfNotOnUIThread(); + ClearTrackingSession(); - syntaxFactsService = null; - var document = _buffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document != null) - { - syntaxFactsService = document.GetLanguageService(); - } + this.TrackingSession = _committedSessions[trackingSessionId]; + TrackingSessionUpdated(); + } - return syntaxFactsService != null; - } + public void OnTrackingSessionUpdated(TrackingSession trackingSession) + { + ThreadingContext.ThrowIfNotOnUIThread(); - private bool TryGetLanguageHeuristicsService(out IRenameTrackingLanguageHeuristicsService languageHeuristicsService) + if (this.TrackingSession == trackingSession) { - // Can be called on a background thread + TrackingSessionUpdated(); + } + } - languageHeuristicsService = null; - var document = _buffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document != null) - { - languageHeuristicsService = document.GetLanguageService(); - } + private bool TryGetSyntaxFactsService(out ISyntaxFactsService syntaxFactsService) + { + // Can be called on a background thread - return languageHeuristicsService != null; + syntaxFactsService = null; + var document = _buffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document != null) + { + syntaxFactsService = document.GetLanguageService(); } - public void Connect() + return syntaxFactsService != null; + } + + private bool TryGetLanguageHeuristicsService(out IRenameTrackingLanguageHeuristicsService languageHeuristicsService) + { + // Can be called on a background thread + + languageHeuristicsService = null; + var document = _buffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document != null) { - ThreadingContext.ThrowIfNotOnUIThread(); - _refCount++; + languageHeuristicsService = document.GetLanguageService(); } - public void Disconnect() - { - ThreadingContext.ThrowIfNotOnUIThread(); - _refCount--; - Contract.ThrowIfFalse(_refCount >= 0); + return languageHeuristicsService != null; + } - if (_refCount == 0) - { - this.Buffer.Properties.RemoveProperty(typeof(StateMachine)); - this.Buffer.Changed -= Buffer_Changed; - } + public void Connect() + { + ThreadingContext.ThrowIfNotOnUIThread(); + _refCount++; + } + + public void Disconnect() + { + ThreadingContext.ThrowIfNotOnUIThread(); + _refCount--; + Contract.ThrowIfFalse(_refCount >= 0); + + if (_refCount == 0) + { + this.Buffer.Properties.RemoveProperty(typeof(StateMachine)); + this.Buffer.Changed -= Buffer_Changed; } } } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.Tagger.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.Tagger.cs index 67b8e2d5e73c2..3d25bc9efd715 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.Tagger.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.Tagger.cs @@ -10,69 +10,68 @@ using Microsoft.VisualStudio.Text.Adornments; using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +internal sealed partial class RenameTrackingTaggerProvider { - internal sealed partial class RenameTrackingTaggerProvider + private class Tagger : ITagger, ITagger, IDisposable { - private class Tagger : ITagger, ITagger, IDisposable - { - private readonly StateMachine _stateMachine; + private readonly StateMachine _stateMachine; - public event EventHandler TagsChanged = delegate { }; + public event EventHandler TagsChanged = delegate { }; - public Tagger(StateMachine stateMachine) - { - _stateMachine = stateMachine; - _stateMachine.Connect(); - _stateMachine.TrackingSessionUpdated += StateMachine_TrackingSessionUpdated; - _stateMachine.TrackingSessionCleared += StateMachine_TrackingSessionCleared; - } + public Tagger(StateMachine stateMachine) + { + _stateMachine = stateMachine; + _stateMachine.Connect(); + _stateMachine.TrackingSessionUpdated += StateMachine_TrackingSessionUpdated; + _stateMachine.TrackingSessionCleared += StateMachine_TrackingSessionCleared; + } - private void StateMachine_TrackingSessionCleared(ITrackingSpan trackingSpanToClear) - => TagsChanged(this, new SnapshotSpanEventArgs(trackingSpanToClear.GetSpan(_stateMachine.Buffer.CurrentSnapshot))); + private void StateMachine_TrackingSessionCleared(ITrackingSpan trackingSpanToClear) + => TagsChanged(this, new SnapshotSpanEventArgs(trackingSpanToClear.GetSpan(_stateMachine.Buffer.CurrentSnapshot))); - private void StateMachine_TrackingSessionUpdated() + private void StateMachine_TrackingSessionUpdated() + { + if (_stateMachine.TrackingSession != null) { - if (_stateMachine.TrackingSession != null) - { - TagsChanged(this, new SnapshotSpanEventArgs(_stateMachine.TrackingSession.TrackingSpan.GetSpan(_stateMachine.Buffer.CurrentSnapshot))); - } + TagsChanged(this, new SnapshotSpanEventArgs(_stateMachine.TrackingSession.TrackingSpan.GetSpan(_stateMachine.Buffer.CurrentSnapshot))); } + } - public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) - => GetTags(spans, RenameTrackingTag.Instance); + public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) + => GetTags(spans, RenameTrackingTag.Instance); - IEnumerable> ITagger.GetTags(NormalizedSnapshotSpanCollection spans) - => GetTags(spans, new ErrorTag(PredefinedErrorTypeNames.Suggestion)); + IEnumerable> ITagger.GetTags(NormalizedSnapshotSpanCollection spans) + => GetTags(spans, new ErrorTag(PredefinedErrorTypeNames.Suggestion)); - private IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans, T tag) where T : ITag + private IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans, T tag) where T : ITag + { + if (!_stateMachine.GlobalOptions.GetOption(RenameTrackingOptionsStorage.RenameTracking)) { - if (!_stateMachine.GlobalOptions.GetOption(RenameTrackingOptionsStorage.RenameTracking)) - { - // Changes aren't being triggered by the buffer, but there may still be taggers - // out there which we should prevent from doing work. - yield break; - } + // Changes aren't being triggered by the buffer, but there may still be taggers + // out there which we should prevent from doing work. + yield break; + } - if (_stateMachine.CanInvokeRename(out var trackingSession, isSmartTagCheck: true)) + if (_stateMachine.CanInvokeRename(out var trackingSession, isSmartTagCheck: true)) + { + foreach (var span in spans) { - foreach (var span in spans) + var snapshotSpan = trackingSession.TrackingSpan.GetSpan(span.Snapshot); + if (span.IntersectsWith(snapshotSpan)) { - var snapshotSpan = trackingSession.TrackingSpan.GetSpan(span.Snapshot); - if (span.IntersectsWith(snapshotSpan)) - { - yield return new TagSpan(snapshotSpan, tag); - } + yield return new TagSpan(snapshotSpan, tag); } } } + } - public void Dispose() - { - _stateMachine.TrackingSessionUpdated -= StateMachine_TrackingSessionUpdated; - _stateMachine.TrackingSessionCleared -= StateMachine_TrackingSessionCleared; - _stateMachine.Disconnect(); - } + public void Dispose() + { + _stateMachine.TrackingSessionUpdated -= StateMachine_TrackingSessionUpdated; + _stateMachine.TrackingSessionCleared -= StateMachine_TrackingSessionCleared; + _stateMachine.Disconnect(); } } } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.TrackingSession.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.TrackingSession.cs index cf0e836737fb5..8808d2f170695 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.TrackingSession.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.TrackingSession.cs @@ -20,273 +20,272 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +internal sealed partial class RenameTrackingTaggerProvider { - internal sealed partial class RenameTrackingTaggerProvider + internal enum TriggerIdentifierKind { - internal enum TriggerIdentifierKind - { - NotRenamable, - RenamableDeclaration, - RenamableReference, - } + NotRenamable, + RenamableDeclaration, + RenamableReference, + } - /// - /// Determines whether the original token was a renameable identifier on a background thread - /// - private class TrackingSession - { - private static readonly Task s_notRenamableTask = Task.FromResult(TriggerIdentifierKind.NotRenamable); - private readonly Task _isRenamableIdentifierTask; - private readonly CancellationTokenSource _cancellationTokenSource = new(); - private readonly CancellationToken _cancellationToken; - private readonly IThreadingContext _threadingContext; - private readonly IAsynchronousOperationListener _asyncListener; + /// + /// Determines whether the original token was a renameable identifier on a background thread + /// + private class TrackingSession + { + private static readonly Task s_notRenamableTask = Task.FromResult(TriggerIdentifierKind.NotRenamable); + private readonly Task _isRenamableIdentifierTask; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly CancellationToken _cancellationToken; + private readonly IThreadingContext _threadingContext; + private readonly IAsynchronousOperationListener _asyncListener; - private Task _newIdentifierBindsTask = SpecializedTasks.False; + private Task _newIdentifierBindsTask = SpecializedTasks.False; - private readonly string _originalName; - public string OriginalName => _originalName; + private readonly string _originalName; + public string OriginalName => _originalName; - private readonly ITrackingSpan _trackingSpan; - public ITrackingSpan TrackingSpan => _trackingSpan; + private readonly ITrackingSpan _trackingSpan; + public ITrackingSpan TrackingSpan => _trackingSpan; - private bool _forceRenameOverloads; - public bool ForceRenameOverloads => _forceRenameOverloads; + private bool _forceRenameOverloads; + public bool ForceRenameOverloads => _forceRenameOverloads; - public TrackingSession( - StateMachine stateMachine, - SnapshotSpan snapshotSpan, - IAsynchronousOperationListener asyncListener) + public TrackingSession( + StateMachine stateMachine, + SnapshotSpan snapshotSpan, + IAsynchronousOperationListener asyncListener) + { + _threadingContext = stateMachine.ThreadingContext; + _asyncListener = asyncListener; + _trackingSpan = snapshotSpan.Snapshot.CreateTrackingSpan(snapshotSpan.Span, SpanTrackingMode.EdgeInclusive); + _cancellationToken = _cancellationTokenSource.Token; + + if (snapshotSpan.Length > 0) { - _threadingContext = stateMachine.ThreadingContext; - _asyncListener = asyncListener; - _trackingSpan = snapshotSpan.Snapshot.CreateTrackingSpan(snapshotSpan.Span, SpanTrackingMode.EdgeInclusive); - _cancellationToken = _cancellationTokenSource.Token; + // If the snapshotSpan is nonempty, then the session began with a change that + // was touching a word. Asynchronously determine whether that word was a + // renameable identifier. If it is, alert the state machine so it can trigger + // tagging. + + _originalName = snapshotSpan.GetText(); + _isRenamableIdentifierTask = Task.Factory.SafeStartNewFromAsync( + () => DetermineIfRenamableIdentifierAsync(snapshotSpan, initialCheck: true), + _cancellationToken, + TaskScheduler.Default); - if (snapshotSpan.Length > 0) - { - // If the snapshotSpan is nonempty, then the session began with a change that - // was touching a word. Asynchronously determine whether that word was a - // renameable identifier. If it is, alert the state machine so it can trigger - // tagging. + var asyncToken = _asyncListener.BeginAsyncOperation(GetType().Name + ".UpdateTrackingSessionAfterIsRenamableIdentifierTask"); - _originalName = snapshotSpan.GetText(); - _isRenamableIdentifierTask = Task.Factory.SafeStartNewFromAsync( - () => DetermineIfRenamableIdentifierAsync(snapshotSpan, initialCheck: true), - _cancellationToken, - TaskScheduler.Default); + _isRenamableIdentifierTask.SafeContinueWithFromAsync( + async t => + { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _cancellationToken).NoThrowAwaitable(); - var asyncToken = _asyncListener.BeginAsyncOperation(GetType().Name + ".UpdateTrackingSessionAfterIsRenamableIdentifierTask"); + // Avoid throwing an exception in this common case + if (_cancellationToken.IsCancellationRequested) + return; - _isRenamableIdentifierTask.SafeContinueWithFromAsync( - async t => - { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _cancellationToken).NoThrowAwaitable(); + stateMachine.UpdateTrackingSessionIfRenamable(); + }, + _cancellationToken, + TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default).CompletesAsyncOperation(asyncToken); - // Avoid throwing an exception in this common case - if (_cancellationToken.IsCancellationRequested) - return; + QueueUpdateToStateMachine(stateMachine, _isRenamableIdentifierTask); + } + else + { + // If the snapshotSpan is empty, that means text was added in a location that is + // not touching an existing word, which happens a fair amount when writing new + // code. In this case we already know that the user is not renaming an + // identifier. - stateMachine.UpdateTrackingSessionIfRenamable(); - }, - _cancellationToken, - TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default).CompletesAsyncOperation(asyncToken); + _isRenamableIdentifierTask = s_notRenamableTask; + } + } - QueueUpdateToStateMachine(stateMachine, _isRenamableIdentifierTask); - } - else - { - // If the snapshotSpan is empty, that means text was added in a location that is - // not touching an existing word, which happens a fair amount when writing new - // code. In this case we already know that the user is not renaming an - // identifier. + private void QueueUpdateToStateMachine(StateMachine stateMachine, Task task) + { + var asyncToken = _asyncListener.BeginAsyncOperation($"{GetType().Name}.{nameof(QueueUpdateToStateMachine)}"); - _isRenamableIdentifierTask = s_notRenamableTask; - } - } + task.SafeContinueWithFromAsync(async t => + { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _cancellationToken).NoThrowAwaitable(); - private void QueueUpdateToStateMachine(StateMachine stateMachine, Task task) - { - var asyncToken = _asyncListener.BeginAsyncOperation($"{GetType().Name}.{nameof(QueueUpdateToStateMachine)}"); + // Avoid throwing an exception in this common case + if (_cancellationToken.IsCancellationRequested) + return; - task.SafeContinueWithFromAsync(async t => + if (_isRenamableIdentifierTask.Result != TriggerIdentifierKind.NotRenamable) { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _cancellationToken).NoThrowAwaitable(); - - // Avoid throwing an exception in this common case - if (_cancellationToken.IsCancellationRequested) - return; - - if (_isRenamableIdentifierTask.Result != TriggerIdentifierKind.NotRenamable) - { - stateMachine.OnTrackingSessionUpdated(this); - } - }, - _cancellationToken, - TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default).CompletesAsyncOperation(asyncToken); - } + stateMachine.OnTrackingSessionUpdated(this); + } + }, + _cancellationToken, + TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default).CompletesAsyncOperation(asyncToken); + } - internal void CheckNewIdentifier(StateMachine stateMachine, ITextSnapshot snapshot) - { - _threadingContext.ThrowIfNotOnUIThread(); - - _newIdentifierBindsTask = _isRenamableIdentifierTask.SafeContinueWithFromAsync( - async t => t.Result != TriggerIdentifierKind.NotRenamable && - TriggerIdentifierKind.RenamableReference == - await DetermineIfRenamableIdentifierAsync( - TrackingSpan.GetSpan(snapshot), - initialCheck: false).ConfigureAwait(false), - _cancellationToken, - TaskContinuationOptions.OnlyOnRanToCompletion, - TaskScheduler.Default); + internal void CheckNewIdentifier(StateMachine stateMachine, ITextSnapshot snapshot) + { + _threadingContext.ThrowIfNotOnUIThread(); + + _newIdentifierBindsTask = _isRenamableIdentifierTask.SafeContinueWithFromAsync( + async t => t.Result != TriggerIdentifierKind.NotRenamable && + TriggerIdentifierKind.RenamableReference == + await DetermineIfRenamableIdentifierAsync( + TrackingSpan.GetSpan(snapshot), + initialCheck: false).ConfigureAwait(false), + _cancellationToken, + TaskContinuationOptions.OnlyOnRanToCompletion, + TaskScheduler.Default); + + QueueUpdateToStateMachine(stateMachine, _newIdentifierBindsTask); + } - QueueUpdateToStateMachine(stateMachine, _newIdentifierBindsTask); - } + internal bool IsDefinitelyRenamableIdentifier() + { + // This needs to be able to run on a background thread for the CodeFix + return IsRenamableIdentifier(_isRenamableIdentifierTask, waitForResult: false, cancellationToken: CancellationToken.None); + } - internal bool IsDefinitelyRenamableIdentifier() - { - // This needs to be able to run on a background thread for the CodeFix - return IsRenamableIdentifier(_isRenamableIdentifierTask, waitForResult: false, cancellationToken: CancellationToken.None); - } + public void Cancel() + { + _threadingContext.ThrowIfNotOnUIThread(); + _cancellationTokenSource.Cancel(); + } - public void Cancel() + private async Task DetermineIfRenamableIdentifierAsync(SnapshotSpan snapshotSpan, bool initialCheck) + { + _threadingContext.ThrowIfNotOnBackgroundThread(); + var document = snapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document != null) { - _threadingContext.ThrowIfNotOnUIThread(); - _cancellationTokenSource.Cancel(); - } + var syntaxFactsService = document.GetLanguageService(); + var syntaxTree = await document.GetSyntaxTreeAsync(_cancellationToken).ConfigureAwait(false); + var token = await syntaxTree.GetTouchingWordAsync(snapshotSpan.Start.Position, syntaxFactsService, _cancellationToken).ConfigureAwait(false); + + // The OriginalName is determined with a simple textual check, so for a + // statement such as "Dim [x = 1" the textual check will return a name of "[x". + // The token found for "[x" is an identifier token, but only due to error + // recovery (the "[x" is actually in the trailing trivia). If the OriginalName + // found through the textual check has a different length than the span of the + // touching word, then we cannot perform a rename. + if (initialCheck && token.Span.Length != this.OriginalName.Length) + { + return TriggerIdentifierKind.NotRenamable; + } - private async Task DetermineIfRenamableIdentifierAsync(SnapshotSpan snapshotSpan, bool initialCheck) - { - _threadingContext.ThrowIfNotOnBackgroundThread(); - var document = snapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document != null) + var languageHeuristicsService = document.GetLanguageService(); + if (syntaxFactsService.IsIdentifier(token) && languageHeuristicsService.IsIdentifierValidForRenameTracking(token.Text)) { - var syntaxFactsService = document.GetLanguageService(); - var syntaxTree = await document.GetSyntaxTreeAsync(_cancellationToken).ConfigureAwait(false); - var token = await syntaxTree.GetTouchingWordAsync(snapshotSpan.Start.Position, syntaxFactsService, _cancellationToken).ConfigureAwait(false); - - // The OriginalName is determined with a simple textual check, so for a - // statement such as "Dim [x = 1" the textual check will return a name of "[x". - // The token found for "[x" is an identifier token, but only due to error - // recovery (the "[x" is actually in the trailing trivia). If the OriginalName - // found through the textual check has a different length than the span of the - // touching word, then we cannot perform a rename. - if (initialCheck && token.Span.Length != this.OriginalName.Length) + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(token.Parent, _cancellationToken).ConfigureAwait(false); + var semanticFacts = document.GetLanguageService(); + + var renameSymbolInfo = RenameUtilities.GetTokenRenameInfo(semanticFacts, semanticModel, token, _cancellationToken); + if (!renameSymbolInfo.HasSymbols) { return TriggerIdentifierKind.NotRenamable; } - var languageHeuristicsService = document.GetLanguageService(); - if (syntaxFactsService.IsIdentifier(token) && languageHeuristicsService.IsIdentifierValidForRenameTracking(token.Text)) + if (renameSymbolInfo.IsMemberGroup) { - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(token.Parent, _cancellationToken).ConfigureAwait(false); - var semanticFacts = document.GetLanguageService(); + // This is a reference from a nameof expression. Allow the rename but set the RenameOverloads option + _forceRenameOverloads = true; - var renameSymbolInfo = RenameUtilities.GetTokenRenameInfo(semanticFacts, semanticModel, token, _cancellationToken); - if (!renameSymbolInfo.HasSymbols) + return await DetermineIfRenamableSymbolsAsync(renameSymbolInfo.Symbols, document).ConfigureAwait(false); + } + else + { + // We do not yet support renaming (inline rename or rename tracking) on + // named tuple elements. + if (renameSymbolInfo.Symbols.Single().ContainingType?.IsTupleType() == true) { return TriggerIdentifierKind.NotRenamable; } - if (renameSymbolInfo.IsMemberGroup) - { - // This is a reference from a nameof expression. Allow the rename but set the RenameOverloads option - _forceRenameOverloads = true; - - return await DetermineIfRenamableSymbolsAsync(renameSymbolInfo.Symbols, document).ConfigureAwait(false); - } - else - { - // We do not yet support renaming (inline rename or rename tracking) on - // named tuple elements. - if (renameSymbolInfo.Symbols.Single().ContainingType?.IsTupleType() == true) - { - return TriggerIdentifierKind.NotRenamable; - } - - return await DetermineIfRenamableSymbolAsync(renameSymbolInfo.Symbols.Single(), document, token).ConfigureAwait(false); - } + return await DetermineIfRenamableSymbolAsync(renameSymbolInfo.Symbols.Single(), document, token).ConfigureAwait(false); } } - - return TriggerIdentifierKind.NotRenamable; } - private async Task DetermineIfRenamableSymbolsAsync(IEnumerable symbols, Document document) - { - foreach (var symbol in symbols) - { - // Get the source symbol if possible - var sourceSymbol = await SymbolFinder.FindSourceDefinitionAsync(symbol, document.Project.Solution, _cancellationToken).ConfigureAwait(false) ?? symbol; - - if (!sourceSymbol.IsFromSource()) - { - return TriggerIdentifierKind.NotRenamable; - } - } - - return TriggerIdentifierKind.RenamableReference; - } + return TriggerIdentifierKind.NotRenamable; + } - private async Task DetermineIfRenamableSymbolAsync(ISymbol symbol, Document document, SyntaxToken token) + private async Task DetermineIfRenamableSymbolsAsync(IEnumerable symbols, Document document) + { + foreach (var symbol in symbols) { // Get the source symbol if possible var sourceSymbol = await SymbolFinder.FindSourceDefinitionAsync(symbol, document.Project.Solution, _cancellationToken).ConfigureAwait(false) ?? symbol; - if (sourceSymbol.Kind == SymbolKind.Field && - ((IFieldSymbol)sourceSymbol).ContainingType.IsTupleType && - sourceSymbol.IsImplicitlyDeclared) - { - // should not rename Item1, Item2... - // when user did not declare them in source. - return TriggerIdentifierKind.NotRenamable; - } - if (!sourceSymbol.IsFromSource()) { return TriggerIdentifierKind.NotRenamable; } + } - return sourceSymbol.Locations.Any(static (loc, token) => loc == token.GetLocation(), token) - ? TriggerIdentifierKind.RenamableDeclaration - : TriggerIdentifierKind.RenamableReference; + return TriggerIdentifierKind.RenamableReference; + } + + private async Task DetermineIfRenamableSymbolAsync(ISymbol symbol, Document document, SyntaxToken token) + { + // Get the source symbol if possible + var sourceSymbol = await SymbolFinder.FindSourceDefinitionAsync(symbol, document.Project.Solution, _cancellationToken).ConfigureAwait(false) ?? symbol; + + if (sourceSymbol.Kind == SymbolKind.Field && + ((IFieldSymbol)sourceSymbol).ContainingType.IsTupleType && + sourceSymbol.IsImplicitlyDeclared) + { + // should not rename Item1, Item2... + // when user did not declare them in source. + return TriggerIdentifierKind.NotRenamable; } - internal bool CanInvokeRename( - ISyntaxFactsService syntaxFactsService, - IRenameTrackingLanguageHeuristicsService languageHeuristicsService, - bool isSmartTagCheck, - bool waitForResult, - CancellationToken cancellationToken) + if (!sourceSymbol.IsFromSource()) { - if (IsRenamableIdentifier(_isRenamableIdentifierTask, waitForResult, cancellationToken)) - { - var isRenamingDeclaration = _isRenamableIdentifierTask.Result == TriggerIdentifierKind.RenamableDeclaration; - var newName = TrackingSpan.GetText(TrackingSpan.TextBuffer.CurrentSnapshot); - var comparison = isRenamingDeclaration || syntaxFactsService.IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + return TriggerIdentifierKind.NotRenamable; + } + + return sourceSymbol.Locations.Any(static (loc, token) => loc == token.GetLocation(), token) + ? TriggerIdentifierKind.RenamableDeclaration + : TriggerIdentifierKind.RenamableReference; + } + + internal bool CanInvokeRename( + ISyntaxFactsService syntaxFactsService, + IRenameTrackingLanguageHeuristicsService languageHeuristicsService, + bool isSmartTagCheck, + bool waitForResult, + CancellationToken cancellationToken) + { + if (IsRenamableIdentifier(_isRenamableIdentifierTask, waitForResult, cancellationToken)) + { + var isRenamingDeclaration = _isRenamableIdentifierTask.Result == TriggerIdentifierKind.RenamableDeclaration; + var newName = TrackingSpan.GetText(TrackingSpan.TextBuffer.CurrentSnapshot); + var comparison = isRenamingDeclaration || syntaxFactsService.IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - if (!string.Equals(OriginalName, newName, comparison) && - syntaxFactsService.IsValidIdentifier(newName) && - languageHeuristicsService.IsIdentifierValidForRenameTracking(newName)) + if (!string.Equals(OriginalName, newName, comparison) && + syntaxFactsService.IsValidIdentifier(newName) && + languageHeuristicsService.IsIdentifierValidForRenameTracking(newName)) + { + // At this point, we want to allow renaming if the user invoked Ctrl+. explicitly, but we + // want to avoid showing a smart tag if we're renaming a reference that binds to an existing + // symbol. + if (!isSmartTagCheck || isRenamingDeclaration || !NewIdentifierDefinitelyBindsToReference()) { - // At this point, we want to allow renaming if the user invoked Ctrl+. explicitly, but we - // want to avoid showing a smart tag if we're renaming a reference that binds to an existing - // symbol. - if (!isSmartTagCheck || isRenamingDeclaration || !NewIdentifierDefinitelyBindsToReference()) - { - return true; - } + return true; } } - - return false; } - private bool NewIdentifierDefinitelyBindsToReference() - => _newIdentifierBindsTask.Status == TaskStatus.RanToCompletion && _newIdentifierBindsTask.Result; + return false; } + + private bool NewIdentifierDefinitelyBindsToReference() + => _newIdentifierBindsTask.Status == TaskStatus.RanToCompletion && _newIdentifierBindsTask.Result; } } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.UndoPrimitive.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.UndoPrimitive.cs index 4a802e3555cb5..29de67751f109 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.UndoPrimitive.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.UndoPrimitive.cs @@ -8,69 +8,68 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +internal sealed partial class RenameTrackingTaggerProvider { - internal sealed partial class RenameTrackingTaggerProvider + /// + /// Clears or restores the state machine on relevant undo/redo actions. + /// + /// These may stay alive on the global undo stack well beyond the lifetime of the + /// on which they were created, so we must avoid strong + /// references to anything that may hold that alive. + /// + private class UndoPrimitive(ITextBuffer textBuffer, int trackingSessionId, bool shouldRestoreStateOnUndo) : ITextUndoPrimitive { - /// - /// Clears or restores the state machine on relevant undo/redo actions. - /// - /// These may stay alive on the global undo stack well beyond the lifetime of the - /// on which they were created, so we must avoid strong - /// references to anything that may hold that alive. - /// - private class UndoPrimitive(ITextBuffer textBuffer, int trackingSessionId, bool shouldRestoreStateOnUndo) : ITextUndoPrimitive - { - private readonly WeakReference _weakTextBuffer = new WeakReference(textBuffer); - private readonly int _trackingSessionId = trackingSessionId; - private readonly bool _shouldRestoreStateOnUndo = shouldRestoreStateOnUndo; + private readonly WeakReference _weakTextBuffer = new WeakReference(textBuffer); + private readonly int _trackingSessionId = trackingSessionId; + private readonly bool _shouldRestoreStateOnUndo = shouldRestoreStateOnUndo; - private ITextUndoTransaction _parent; - public ITextUndoTransaction Parent - { - get { return _parent; } - set { _parent = value; } - } + private ITextUndoTransaction _parent; + public ITextUndoTransaction Parent + { + get { return _parent; } + set { _parent = value; } + } - public bool CanRedo => true; + public bool CanRedo => true; - public bool CanUndo => true; + public bool CanUndo => true; - public void Do() + public void Do() + { + if (TryGetStateMachine(out var stateMachine)) { - if (TryGetStateMachine(out var stateMachine)) - { - stateMachine.ClearTrackingSession(); - } + stateMachine.ClearTrackingSession(); } + } - public void Undo() + public void Undo() + { + if (TryGetStateMachine(out var stateMachine)) { - if (TryGetStateMachine(out var stateMachine)) + if (_shouldRestoreStateOnUndo) { - if (_shouldRestoreStateOnUndo) - { - stateMachine.RestoreTrackingSession(_trackingSessionId); - } - else - { - stateMachine.ClearTrackingSession(); - } + stateMachine.RestoreTrackingSession(_trackingSessionId); + } + else + { + stateMachine.ClearTrackingSession(); } } + } - private bool TryGetStateMachine(out StateMachine stateMachine) - { - stateMachine = null; - return _weakTextBuffer.TryGetTarget(out var textBuffer) && - textBuffer.Properties.TryGetProperty(typeof(StateMachine), out stateMachine); - } + private bool TryGetStateMachine(out StateMachine stateMachine) + { + stateMachine = null; + return _weakTextBuffer.TryGetTarget(out var textBuffer) && + textBuffer.Properties.TryGetProperty(typeof(StateMachine), out stateMachine); + } - public bool CanMerge(ITextUndoPrimitive older) - => false; + public bool CanMerge(ITextUndoPrimitive older) + => false; - public ITextUndoPrimitive Merge(ITextUndoPrimitive older) - => throw new NotImplementedException(); - } + public ITextUndoPrimitive Merge(ITextUndoPrimitive older) + => throw new NotImplementedException(); } } diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.cs index 1d77391a089e3..a6afcd6ae2beb 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.cs @@ -25,144 +25,143 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking +namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; + +/// +/// Also known as "rename smart tag," this watches text changes in open buffers, determines +/// whether they can be interpreted as an identifier rename, and if so displays a smart tag +/// that can perform a rename on that symbol. Each text buffer is tracked independently. +/// +[Export(typeof(ITaggerProvider))] +[TagType(typeof(RenameTrackingTag))] +[TagType(typeof(IErrorTag))] +[ContentType(ContentTypeNames.RoslynContentType)] +[ContentType(ContentTypeNames.XamlContentType)] +[TextViewRole(PredefinedTextViewRoles.Editable)] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal sealed partial class RenameTrackingTaggerProvider( + IThreadingContext threadingContext, + IInlineRenameService inlineRenameService, + IDiagnosticAnalyzerService diagnosticAnalyzerService, + IGlobalOptionService globalOptions, + IAsynchronousOperationListenerProvider listenerProvider) : ITaggerProvider { - /// - /// Also known as "rename smart tag," this watches text changes in open buffers, determines - /// whether they can be interpreted as an identifier rename, and if so displays a smart tag - /// that can perform a rename on that symbol. Each text buffer is tracked independently. - /// - [Export(typeof(ITaggerProvider))] - [TagType(typeof(RenameTrackingTag))] - [TagType(typeof(IErrorTag))] - [ContentType(ContentTypeNames.RoslynContentType)] - [ContentType(ContentTypeNames.XamlContentType)] - [TextViewRole(PredefinedTextViewRoles.Editable)] - [method: ImportingConstructor] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal sealed partial class RenameTrackingTaggerProvider( - IThreadingContext threadingContext, - IInlineRenameService inlineRenameService, - IDiagnosticAnalyzerService diagnosticAnalyzerService, - IGlobalOptionService globalOptions, - IAsynchronousOperationListenerProvider listenerProvider) : ITaggerProvider - { - private readonly IThreadingContext _threadingContext = threadingContext; - private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.RenameTracking); - private readonly IInlineRenameService _inlineRenameService = inlineRenameService; - private readonly IDiagnosticAnalyzerService _diagnosticAnalyzerService = diagnosticAnalyzerService; - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.RenameTracking); + private readonly IInlineRenameService _inlineRenameService = inlineRenameService; + private readonly IDiagnosticAnalyzerService _diagnosticAnalyzerService = diagnosticAnalyzerService; + private readonly IGlobalOptionService _globalOptions = globalOptions; - public ITagger CreateTagger(ITextBuffer buffer) where T : ITag - { - var stateMachine = buffer.Properties.GetOrCreateSingletonProperty(() => new StateMachine(_threadingContext, buffer, _inlineRenameService, _diagnosticAnalyzerService, _globalOptions, _asyncListener)); - return new Tagger(stateMachine) as ITagger; - } + public ITagger CreateTagger(ITextBuffer buffer) where T : ITag + { + var stateMachine = buffer.Properties.GetOrCreateSingletonProperty(() => new StateMachine(_threadingContext, buffer, _inlineRenameService, _diagnosticAnalyzerService, _globalOptions, _asyncListener)); + return new Tagger(stateMachine) as ITagger; + } - internal static void ResetRenameTrackingState(Workspace workspace, DocumentId documentId) - => ResetRenameTrackingStateWorker(workspace, documentId, visible: false); + internal static void ResetRenameTrackingState(Workspace workspace, DocumentId documentId) + => ResetRenameTrackingStateWorker(workspace, documentId, visible: false); - internal static bool ResetVisibleRenameTrackingState(Workspace workspace, DocumentId documentId) - => ResetRenameTrackingStateWorker(workspace, documentId, visible: true); + internal static bool ResetVisibleRenameTrackingState(Workspace workspace, DocumentId documentId) + => ResetRenameTrackingStateWorker(workspace, documentId, visible: true); - internal static bool ResetRenameTrackingStateWorker(Workspace workspace, DocumentId documentId, bool visible) + internal static bool ResetRenameTrackingStateWorker(Workspace workspace, DocumentId documentId, bool visible) + { + if (workspace.IsDocumentOpen(documentId)) { - if (workspace.IsDocumentOpen(documentId)) + var document = workspace.CurrentSolution.GetDocument(documentId); + ITextBuffer textBuffer; + if (document != null && + document.TryGetText(out var text)) { - var document = workspace.CurrentSolution.GetDocument(documentId); - ITextBuffer textBuffer; - if (document != null && - document.TryGetText(out var text)) + textBuffer = text.Container.TryGetTextBuffer(); + if (textBuffer == null) { - textBuffer = text.Container.TryGetTextBuffer(); - if (textBuffer == null) + var ex = new InvalidOperationException(string.Format( + "document with name {0} is open but textBuffer is null. Textcontainer is of type {1}. SourceText is: {2}", + document.Name, + text.Container.GetType().FullName, + text.ToString())); + FatalError.ReportAndCatch(ex); + return false; + } + + if (textBuffer.Properties.TryGetProperty(typeof(StateMachine), out StateMachine stateMachine)) + { + if (visible) { - var ex = new InvalidOperationException(string.Format( - "document with name {0} is open but textBuffer is null. Textcontainer is of type {1}. SourceText is: {2}", - document.Name, - text.Container.GetType().FullName, - text.ToString())); - FatalError.ReportAndCatch(ex); - return false; + return stateMachine.ClearVisibleTrackingSession(); } - - if (textBuffer.Properties.TryGetProperty(typeof(StateMachine), out StateMachine stateMachine)) + else { - if (visible) - { - return stateMachine.ClearVisibleTrackingSession(); - } - else - { - return stateMachine.ClearTrackingSession(); - } + return stateMachine.ClearTrackingSession(); } } } - - return false; } - public static (CodeAction action, TextSpan renameSpan) TryGetCodeAction( - Document document, TextSpan textSpan, - IEnumerable refactorNotifyServices, - ITextUndoHistoryRegistry undoHistoryRegistry, - CancellationToken cancellationToken) + return false; + } + + public static (CodeAction action, TextSpan renameSpan) TryGetCodeAction( + Document document, TextSpan textSpan, + IEnumerable refactorNotifyServices, + ITextUndoHistoryRegistry undoHistoryRegistry, + CancellationToken cancellationToken) + { + try { - try + if (document != null && document.TryGetText(out var text)) { - if (document != null && document.TryGetText(out var text)) + var textBuffer = text.Container.TryGetTextBuffer(); + if (textBuffer != null && + textBuffer.Properties.TryGetProperty(typeof(StateMachine), out StateMachine stateMachine) && + stateMachine.CanInvokeRename(out _, cancellationToken: cancellationToken)) { - var textBuffer = text.Container.TryGetTextBuffer(); - if (textBuffer != null && - textBuffer.Properties.TryGetProperty(typeof(StateMachine), out StateMachine stateMachine) && - stateMachine.CanInvokeRename(out _, cancellationToken: cancellationToken)) - { - return stateMachine.TryGetCodeAction( - document, text, textSpan, refactorNotifyServices, undoHistoryRegistry, cancellationToken); - } + return stateMachine.TryGetCodeAction( + document, text, textSpan, refactorNotifyServices, undoHistoryRegistry, cancellationToken); } - - return default; - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.General)) - { - throw ExceptionUtilities.Unreachable(); } + + return default; + } + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.General)) + { + throw ExceptionUtilities.Unreachable(); } + } - internal static bool IsRenamableIdentifier(Task isRenamableIdentifierTask, bool waitForResult, CancellationToken cancellationToken) + internal static bool IsRenamableIdentifier(Task isRenamableIdentifierTask, bool waitForResult, CancellationToken cancellationToken) + { + if (isRenamableIdentifierTask.Status == TaskStatus.RanToCompletion && isRenamableIdentifierTask.Result != TriggerIdentifierKind.NotRenamable) { - if (isRenamableIdentifierTask.Status == TaskStatus.RanToCompletion && isRenamableIdentifierTask.Result != TriggerIdentifierKind.NotRenamable) - { - return true; - } - else if (isRenamableIdentifierTask.Status == TaskStatus.Canceled) - { - return false; - } - else if (waitForResult) - { - return WaitForIsRenamableIdentifier(isRenamableIdentifierTask, cancellationToken); - } - else - { - return false; - } + return true; + } + else if (isRenamableIdentifierTask.Status == TaskStatus.Canceled) + { + return false; + } + else if (waitForResult) + { + return WaitForIsRenamableIdentifier(isRenamableIdentifierTask, cancellationToken); } + else + { + return false; + } + } - internal static bool WaitForIsRenamableIdentifier(Task isRenamableIdentifierTask, CancellationToken cancellationToken) + internal static bool WaitForIsRenamableIdentifier(Task isRenamableIdentifierTask, CancellationToken cancellationToken) + { + try { - try - { - return isRenamableIdentifierTask.WaitAndGetResult_CanCallOnBackground(cancellationToken) != TriggerIdentifierKind.NotRenamable; - } - catch (OperationCanceledException e) when (e.CancellationToken != cancellationToken || cancellationToken == CancellationToken.None) - { - // We passed in a different cancellationToken, so if there's a race and - // isRenamableIdentifierTask was cancelled, we'll get a OperationCanceledException - return false; - } + return isRenamableIdentifierTask.WaitAndGetResult_CanCallOnBackground(cancellationToken) != TriggerIdentifierKind.NotRenamable; + } + catch (OperationCanceledException e) when (e.CancellationToken != cancellationToken || cancellationToken == CancellationToken.None) + { + // We passed in a different cancellationToken, so if there's a race and + // isRenamableIdentifierTask was cancelled, we'll get a OperationCanceledException + return false; } } } diff --git a/src/EditorFeatures/Core/Shared/DefaultTextBufferSupportsFeatureService.cs b/src/EditorFeatures/Core/Shared/DefaultTextBufferSupportsFeatureService.cs index e7eb8c8e7237e..a4f730ed5fa00 100644 --- a/src/EditorFeatures/Core/Shared/DefaultTextBufferSupportsFeatureService.cs +++ b/src/EditorFeatures/Core/Shared/DefaultTextBufferSupportsFeatureService.cs @@ -7,27 +7,26 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared +namespace Microsoft.CodeAnalysis.Editor.Shared; + +[ExportWorkspaceService(typeof(ITextBufferSupportsFeatureService), ServiceLayer.Editor), Shared] +internal sealed class DefaultTextBufferSupportsFeatureService : ITextBufferSupportsFeatureService { - [ExportWorkspaceService(typeof(ITextBufferSupportsFeatureService), ServiceLayer.Editor), Shared] - internal sealed class DefaultTextBufferSupportsFeatureService : ITextBufferSupportsFeatureService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DefaultTextBufferSupportsFeatureService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public DefaultTextBufferSupportsFeatureService() - { - } + } - public bool SupportsCodeFixes(ITextBuffer textBuffer) - => true; + public bool SupportsCodeFixes(ITextBuffer textBuffer) + => true; - public bool SupportsRefactorings(ITextBuffer textBuffer) - => true; + public bool SupportsRefactorings(ITextBuffer textBuffer) + => true; - public bool SupportsRename(ITextBuffer textBuffer) - => true; + public bool SupportsRename(ITextBuffer textBuffer) + => true; - public bool SupportsNavigationToAnyPosition(ITextBuffer textBuffer) - => true; - } + public bool SupportsNavigationToAnyPosition(ITextBuffer textBuffer) + => true; } diff --git a/src/EditorFeatures/Core/Shared/Extensions/ClassificationExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/ClassificationExtensions.cs index 002c4fcf558be..313e3b6845c36 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/ClassificationExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/ClassificationExtensions.cs @@ -8,29 +8,28 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Classification; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static partial class ClassificationExtensions { - internal static partial class ClassificationExtensions + public static IList ToClassificationSpans( + this IEnumerable parts, + ITextSnapshot textSnapshot, + ClassificationTypeMap typeMap) { - public static IList ToClassificationSpans( - this IEnumerable parts, - ITextSnapshot textSnapshot, - ClassificationTypeMap typeMap) - { - var result = new List(); - - var index = 0; - foreach (var part in parts) - { - var text = part.ToString(); - result.Add(new ClassificationSpan( - new SnapshotSpan(textSnapshot, new Span(index, text.Length)), - typeMap.GetClassificationType(part.Tag.ToClassificationTypeName()))); + var result = new List(); - index += text.Length; - } + var index = 0; + foreach (var part in parts) + { + var text = part.ToString(); + result.Add(new ClassificationSpan( + new SnapshotSpan(textSnapshot, new Span(index, text.Length)), + typeMap.GetClassificationType(part.Tag.ToClassificationTypeName()))); - return result; + index += text.Length; } + + return result; } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/GlyphExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/GlyphExtensions.cs index cbac0acd01236..4813f9d7ac8f9 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/GlyphExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/GlyphExtensions.cs @@ -7,221 +7,220 @@ using Microsoft.VisualStudio.Imaging; using Microsoft.VisualStudio.Text.Adornments; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class GlyphExtensions { - internal static class GlyphExtensions - { - // hardcode ImageCatalogGuid locally rather than calling KnownImageIds.ImageCatalogGuid - // So it doesnot have dependency for Microsoft.VisualStudio.ImageCatalog.dll - // https://github.com/dotnet/roslyn/issues/26642 - private static readonly Guid ImageCatalogGuid = Guid.Parse("ae27a6b0-e345-4288-96df-5eaf394ee369"); + // hardcode ImageCatalogGuid locally rather than calling KnownImageIds.ImageCatalogGuid + // So it doesnot have dependency for Microsoft.VisualStudio.ImageCatalog.dll + // https://github.com/dotnet/roslyn/issues/26642 + private static readonly Guid ImageCatalogGuid = Guid.Parse("ae27a6b0-e345-4288-96df-5eaf394ee369"); - public static ImageId GetImageCatalogImageId(int imageId) - => new(ImageCatalogGuid, imageId); + public static ImageId GetImageCatalogImageId(int imageId) + => new(ImageCatalogGuid, imageId); - public static ImageId GetImageId(this Glyph glyph) + public static ImageId GetImageId(this Glyph glyph) + { + // VS for mac cannot refer to ImageMoniker + // so we need to expose ImageId instead of ImageMoniker here + // and expose ImageMoniker in the EditorFeatures.wpf.dll + // The use of constants here is okay because the compiler inlines their values, so no runtime reference is needed. + // There are tests in src\EditorFeatures\Test\AssemblyReferenceTests.cs to ensure we don't regress that. + switch (glyph) { - // VS for mac cannot refer to ImageMoniker - // so we need to expose ImageId instead of ImageMoniker here - // and expose ImageMoniker in the EditorFeatures.wpf.dll - // The use of constants here is okay because the compiler inlines their values, so no runtime reference is needed. - // There are tests in src\EditorFeatures\Test\AssemblyReferenceTests.cs to ensure we don't regress that. - switch (glyph) - { - case Glyph.None: - return default; - - case Glyph.Assembly: - return new ImageId(ImageCatalogGuid, KnownImageIds.Assembly); - - case Glyph.BasicFile: - return new ImageId(ImageCatalogGuid, KnownImageIds.VBFileNode); - case Glyph.BasicProject: - return new ImageId(ImageCatalogGuid, KnownImageIds.VBProjectNode); - - case Glyph.ClassPublic: - return new ImageId(ImageCatalogGuid, KnownImageIds.ClassPublic); - case Glyph.ClassProtected: - return new ImageId(ImageCatalogGuid, KnownImageIds.ClassProtected); - case Glyph.ClassPrivate: - return new ImageId(ImageCatalogGuid, KnownImageIds.ClassPrivate); - case Glyph.ClassInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.ClassInternal); - - case Glyph.CSharpFile: - return new ImageId(ImageCatalogGuid, KnownImageIds.CSFileNode); - case Glyph.CSharpProject: - return new ImageId(ImageCatalogGuid, KnownImageIds.CSProjectNode); - - case Glyph.ConstantPublic: - return new ImageId(ImageCatalogGuid, KnownImageIds.ConstantPublic); - case Glyph.ConstantProtected: - return new ImageId(ImageCatalogGuid, KnownImageIds.ConstantProtected); - case Glyph.ConstantPrivate: - return new ImageId(ImageCatalogGuid, KnownImageIds.ConstantPrivate); - case Glyph.ConstantInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.ConstantInternal); - - case Glyph.DelegatePublic: - return new ImageId(ImageCatalogGuid, KnownImageIds.DelegatePublic); - case Glyph.DelegateProtected: - return new ImageId(ImageCatalogGuid, KnownImageIds.DelegateProtected); - case Glyph.DelegatePrivate: - return new ImageId(ImageCatalogGuid, KnownImageIds.DelegatePrivate); - case Glyph.DelegateInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.DelegateInternal); - - case Glyph.EnumPublic: - return new ImageId(ImageCatalogGuid, KnownImageIds.EnumerationPublic); - case Glyph.EnumProtected: - return new ImageId(ImageCatalogGuid, KnownImageIds.EnumerationProtected); - case Glyph.EnumPrivate: - return new ImageId(ImageCatalogGuid, KnownImageIds.EnumerationPrivate); - case Glyph.EnumInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.EnumerationInternal); - - case Glyph.EnumMemberPublic: - case Glyph.EnumMemberProtected: - case Glyph.EnumMemberPrivate: - case Glyph.EnumMemberInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.EnumerationItemPublic); - - case Glyph.Error: - return new ImageId(ImageCatalogGuid, KnownImageIds.StatusError); - - case Glyph.EventPublic: - return new ImageId(ImageCatalogGuid, KnownImageIds.EventPublic); - case Glyph.EventProtected: - return new ImageId(ImageCatalogGuid, KnownImageIds.EventProtected); - case Glyph.EventPrivate: - return new ImageId(ImageCatalogGuid, KnownImageIds.EventPrivate); - case Glyph.EventInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.EventInternal); - - // Extension methods have the same glyph regardless of accessibility. - case Glyph.ExtensionMethodPublic: - case Glyph.ExtensionMethodProtected: - case Glyph.ExtensionMethodPrivate: - case Glyph.ExtensionMethodInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.ExtensionMethod); - - case Glyph.FieldPublic: - return new ImageId(ImageCatalogGuid, KnownImageIds.FieldPublic); - case Glyph.FieldProtected: - return new ImageId(ImageCatalogGuid, KnownImageIds.FieldProtected); - case Glyph.FieldPrivate: - return new ImageId(ImageCatalogGuid, KnownImageIds.FieldPrivate); - case Glyph.FieldInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.FieldInternal); - - case Glyph.InterfacePublic: - return new ImageId(ImageCatalogGuid, KnownImageIds.InterfacePublic); - case Glyph.InterfaceProtected: - return new ImageId(ImageCatalogGuid, KnownImageIds.InterfaceProtected); - case Glyph.InterfacePrivate: - return new ImageId(ImageCatalogGuid, KnownImageIds.InterfacePrivate); - case Glyph.InterfaceInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.InterfaceInternal); - - // TODO: Figure out the right thing to return here. - case Glyph.Intrinsic: - return new ImageId(ImageCatalogGuid, KnownImageIds.Type); - - case Glyph.Keyword: - return new ImageId(ImageCatalogGuid, KnownImageIds.IntellisenseKeyword); - - case Glyph.Label: - return new ImageId(ImageCatalogGuid, KnownImageIds.Label); - - case Glyph.Parameter: - case Glyph.Local: - return new ImageId(ImageCatalogGuid, KnownImageIds.LocalVariable); - - case Glyph.Namespace: - return new ImageId(ImageCatalogGuid, KnownImageIds.Namespace); - - case Glyph.MethodPublic: - return new ImageId(ImageCatalogGuid, KnownImageIds.MethodPublic); - case Glyph.MethodProtected: - return new ImageId(ImageCatalogGuid, KnownImageIds.MethodProtected); - case Glyph.MethodPrivate: - return new ImageId(ImageCatalogGuid, KnownImageIds.MethodPrivate); - case Glyph.MethodInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.MethodInternal); - - case Glyph.ModulePublic: - return new ImageId(ImageCatalogGuid, KnownImageIds.ModulePublic); - case Glyph.ModuleProtected: - return new ImageId(ImageCatalogGuid, KnownImageIds.ModuleProtected); - case Glyph.ModulePrivate: - return new ImageId(ImageCatalogGuid, KnownImageIds.ModulePrivate); - case Glyph.ModuleInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.ModuleInternal); - - case Glyph.OpenFolder: - return new ImageId(ImageCatalogGuid, KnownImageIds.OpenFolder); - - case Glyph.Operator: - return new ImageId(ImageCatalogGuid, KnownImageIds.Operator); - - case Glyph.PropertyPublic: - return new ImageId(ImageCatalogGuid, KnownImageIds.PropertyPublic); - case Glyph.PropertyProtected: - return new ImageId(ImageCatalogGuid, KnownImageIds.PropertyProtected); - case Glyph.PropertyPrivate: - return new ImageId(ImageCatalogGuid, KnownImageIds.PropertyPrivate); - case Glyph.PropertyInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.PropertyInternal); - - case Glyph.RangeVariable: - return new ImageId(ImageCatalogGuid, KnownImageIds.FieldPublic); - - case Glyph.Reference: - return new ImageId(ImageCatalogGuid, KnownImageIds.Reference); - - //// this is not a copy-paste mistake, we were using these before in the previous GetImageMoniker() - //case Glyph.StructurePublic: - // return KnownMonikers.ValueTypePublic; - //case Glyph.StructureProtected: - // return KnownMonikers.ValueTypeProtected; - //case Glyph.StructurePrivate: - // return KnownMonikers.ValueTypePrivate; - //case Glyph.StructureInternal: - // return KnownMonikers.ValueTypeInternal; - - case Glyph.StructurePublic: - return new ImageId(ImageCatalogGuid, KnownImageIds.ValueTypePublic); - case Glyph.StructureProtected: - return new ImageId(ImageCatalogGuid, KnownImageIds.ValueTypeProtected); - case Glyph.StructurePrivate: - return new ImageId(ImageCatalogGuid, KnownImageIds.ValueTypePrivate); - case Glyph.StructureInternal: - return new ImageId(ImageCatalogGuid, KnownImageIds.ValueTypeInternal); - - case Glyph.TypeParameter: - return new ImageId(ImageCatalogGuid, KnownImageIds.Type); - - case Glyph.Snippet: - return new ImageId(ImageCatalogGuid, KnownImageIds.Snippet); - - case Glyph.CompletionWarning: - return new ImageId(ImageCatalogGuid, KnownImageIds.IntellisenseWarning); - - case Glyph.StatusInformation: - return new ImageId(ImageCatalogGuid, KnownImageIds.StatusInformation); - - case Glyph.NuGet: - return new ImageId(ImageCatalogGuid, KnownImageIds.NuGet); - - case Glyph.TargetTypeMatch: - return new ImageId(ImageCatalogGuid, KnownImageIds.MatchType); - - default: - throw new ArgumentException(nameof(glyph)); - } + case Glyph.None: + return default; + + case Glyph.Assembly: + return new ImageId(ImageCatalogGuid, KnownImageIds.Assembly); + + case Glyph.BasicFile: + return new ImageId(ImageCatalogGuid, KnownImageIds.VBFileNode); + case Glyph.BasicProject: + return new ImageId(ImageCatalogGuid, KnownImageIds.VBProjectNode); + + case Glyph.ClassPublic: + return new ImageId(ImageCatalogGuid, KnownImageIds.ClassPublic); + case Glyph.ClassProtected: + return new ImageId(ImageCatalogGuid, KnownImageIds.ClassProtected); + case Glyph.ClassPrivate: + return new ImageId(ImageCatalogGuid, KnownImageIds.ClassPrivate); + case Glyph.ClassInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.ClassInternal); + + case Glyph.CSharpFile: + return new ImageId(ImageCatalogGuid, KnownImageIds.CSFileNode); + case Glyph.CSharpProject: + return new ImageId(ImageCatalogGuid, KnownImageIds.CSProjectNode); + + case Glyph.ConstantPublic: + return new ImageId(ImageCatalogGuid, KnownImageIds.ConstantPublic); + case Glyph.ConstantProtected: + return new ImageId(ImageCatalogGuid, KnownImageIds.ConstantProtected); + case Glyph.ConstantPrivate: + return new ImageId(ImageCatalogGuid, KnownImageIds.ConstantPrivate); + case Glyph.ConstantInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.ConstantInternal); + + case Glyph.DelegatePublic: + return new ImageId(ImageCatalogGuid, KnownImageIds.DelegatePublic); + case Glyph.DelegateProtected: + return new ImageId(ImageCatalogGuid, KnownImageIds.DelegateProtected); + case Glyph.DelegatePrivate: + return new ImageId(ImageCatalogGuid, KnownImageIds.DelegatePrivate); + case Glyph.DelegateInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.DelegateInternal); + + case Glyph.EnumPublic: + return new ImageId(ImageCatalogGuid, KnownImageIds.EnumerationPublic); + case Glyph.EnumProtected: + return new ImageId(ImageCatalogGuid, KnownImageIds.EnumerationProtected); + case Glyph.EnumPrivate: + return new ImageId(ImageCatalogGuid, KnownImageIds.EnumerationPrivate); + case Glyph.EnumInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.EnumerationInternal); + + case Glyph.EnumMemberPublic: + case Glyph.EnumMemberProtected: + case Glyph.EnumMemberPrivate: + case Glyph.EnumMemberInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.EnumerationItemPublic); + + case Glyph.Error: + return new ImageId(ImageCatalogGuid, KnownImageIds.StatusError); + + case Glyph.EventPublic: + return new ImageId(ImageCatalogGuid, KnownImageIds.EventPublic); + case Glyph.EventProtected: + return new ImageId(ImageCatalogGuid, KnownImageIds.EventProtected); + case Glyph.EventPrivate: + return new ImageId(ImageCatalogGuid, KnownImageIds.EventPrivate); + case Glyph.EventInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.EventInternal); + + // Extension methods have the same glyph regardless of accessibility. + case Glyph.ExtensionMethodPublic: + case Glyph.ExtensionMethodProtected: + case Glyph.ExtensionMethodPrivate: + case Glyph.ExtensionMethodInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.ExtensionMethod); + + case Glyph.FieldPublic: + return new ImageId(ImageCatalogGuid, KnownImageIds.FieldPublic); + case Glyph.FieldProtected: + return new ImageId(ImageCatalogGuid, KnownImageIds.FieldProtected); + case Glyph.FieldPrivate: + return new ImageId(ImageCatalogGuid, KnownImageIds.FieldPrivate); + case Glyph.FieldInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.FieldInternal); + + case Glyph.InterfacePublic: + return new ImageId(ImageCatalogGuid, KnownImageIds.InterfacePublic); + case Glyph.InterfaceProtected: + return new ImageId(ImageCatalogGuid, KnownImageIds.InterfaceProtected); + case Glyph.InterfacePrivate: + return new ImageId(ImageCatalogGuid, KnownImageIds.InterfacePrivate); + case Glyph.InterfaceInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.InterfaceInternal); + + // TODO: Figure out the right thing to return here. + case Glyph.Intrinsic: + return new ImageId(ImageCatalogGuid, KnownImageIds.Type); + + case Glyph.Keyword: + return new ImageId(ImageCatalogGuid, KnownImageIds.IntellisenseKeyword); + + case Glyph.Label: + return new ImageId(ImageCatalogGuid, KnownImageIds.Label); + + case Glyph.Parameter: + case Glyph.Local: + return new ImageId(ImageCatalogGuid, KnownImageIds.LocalVariable); + + case Glyph.Namespace: + return new ImageId(ImageCatalogGuid, KnownImageIds.Namespace); + + case Glyph.MethodPublic: + return new ImageId(ImageCatalogGuid, KnownImageIds.MethodPublic); + case Glyph.MethodProtected: + return new ImageId(ImageCatalogGuid, KnownImageIds.MethodProtected); + case Glyph.MethodPrivate: + return new ImageId(ImageCatalogGuid, KnownImageIds.MethodPrivate); + case Glyph.MethodInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.MethodInternal); + + case Glyph.ModulePublic: + return new ImageId(ImageCatalogGuid, KnownImageIds.ModulePublic); + case Glyph.ModuleProtected: + return new ImageId(ImageCatalogGuid, KnownImageIds.ModuleProtected); + case Glyph.ModulePrivate: + return new ImageId(ImageCatalogGuid, KnownImageIds.ModulePrivate); + case Glyph.ModuleInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.ModuleInternal); + + case Glyph.OpenFolder: + return new ImageId(ImageCatalogGuid, KnownImageIds.OpenFolder); + + case Glyph.Operator: + return new ImageId(ImageCatalogGuid, KnownImageIds.Operator); + + case Glyph.PropertyPublic: + return new ImageId(ImageCatalogGuid, KnownImageIds.PropertyPublic); + case Glyph.PropertyProtected: + return new ImageId(ImageCatalogGuid, KnownImageIds.PropertyProtected); + case Glyph.PropertyPrivate: + return new ImageId(ImageCatalogGuid, KnownImageIds.PropertyPrivate); + case Glyph.PropertyInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.PropertyInternal); + + case Glyph.RangeVariable: + return new ImageId(ImageCatalogGuid, KnownImageIds.FieldPublic); + + case Glyph.Reference: + return new ImageId(ImageCatalogGuid, KnownImageIds.Reference); + + //// this is not a copy-paste mistake, we were using these before in the previous GetImageMoniker() + //case Glyph.StructurePublic: + // return KnownMonikers.ValueTypePublic; + //case Glyph.StructureProtected: + // return KnownMonikers.ValueTypeProtected; + //case Glyph.StructurePrivate: + // return KnownMonikers.ValueTypePrivate; + //case Glyph.StructureInternal: + // return KnownMonikers.ValueTypeInternal; + + case Glyph.StructurePublic: + return new ImageId(ImageCatalogGuid, KnownImageIds.ValueTypePublic); + case Glyph.StructureProtected: + return new ImageId(ImageCatalogGuid, KnownImageIds.ValueTypeProtected); + case Glyph.StructurePrivate: + return new ImageId(ImageCatalogGuid, KnownImageIds.ValueTypePrivate); + case Glyph.StructureInternal: + return new ImageId(ImageCatalogGuid, KnownImageIds.ValueTypeInternal); + + case Glyph.TypeParameter: + return new ImageId(ImageCatalogGuid, KnownImageIds.Type); + + case Glyph.Snippet: + return new ImageId(ImageCatalogGuid, KnownImageIds.Snippet); + + case Glyph.CompletionWarning: + return new ImageId(ImageCatalogGuid, KnownImageIds.IntellisenseWarning); + + case Glyph.StatusInformation: + return new ImageId(ImageCatalogGuid, KnownImageIds.StatusInformation); + + case Glyph.NuGet: + return new ImageId(ImageCatalogGuid, KnownImageIds.NuGet); + + case Glyph.TargetTypeMatch: + return new ImageId(ImageCatalogGuid, KnownImageIds.MatchType); + + default: + throw new ArgumentException(nameof(glyph)); } - - public static ImageElement GetImageElement(this Glyph glyph) - => new ImageElement(glyph.GetImageId()); } + + public static ImageElement GetImageElement(this Glyph glyph) + => new ImageElement(glyph.GetImageId()); } diff --git a/src/EditorFeatures/Core/Shared/Extensions/HostWorkspaceServicesExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/HostWorkspaceServicesExtensions.cs index 95edc883c681b..6b7e5a994289f 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/HostWorkspaceServicesExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/HostWorkspaceServicesExtensions.cs @@ -78,7 +78,7 @@ private static Dictionary CreateContentTypeMap(SolutionServices .Where(lz => !string.IsNullOrEmpty(lz.Metadata.DefaultContentType)) .Select(lz => (lz.Metadata.Language, lz.Metadata.DefaultContentType)) .Distinct() - .ToDictionary(lz => lz.Language, lz => lz.DefaultContentType); + .ToDictionary(lz => lz.Language, lz => lz.DefaultContentType)!; } // We can't do anything special, so fall back to the expensive path @@ -102,26 +102,6 @@ internal static IList SelectMatchingExtensionValues( Select(lazy => lazy.Value).ToList(); } - internal static IList SelectMatchingExtensionValues( - this SolutionServices workspaceServices, - IEnumerable> items, - IContentType contentType, - ITextViewRoleSet roleSet) - { - if (items == null) - { - return SpecializedCollections.EmptyList(); - } - - return items.Where(lazy => - { - var metadata = lazy.Metadata; - return LanguageMatches(metadata.Language, contentType, workspaceServices) && - RolesMatch(metadata.Roles, roleSet); - }). - Select(lazy => lazy.Value).ToList(); - } - private static bool LanguageMatches( string language, IContentType contentType, @@ -130,12 +110,5 @@ private static bool LanguageMatches( var defaultContentType = GetDefaultContentTypeName(workspaceServices, language); return (defaultContentType != null) ? contentType.IsOfType(defaultContentType) : false; } - - private static bool RolesMatch( - IEnumerable roles, - ITextViewRoleSet roleSet) - { - return (roles == null) || (roleSet == null) || roleSet.ContainsAll(roles); - } } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/IBufferGraphExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/IBufferGraphExtensions.cs index cbb78623a4a98..f99d5be3083c6 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/IBufferGraphExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/IBufferGraphExtensions.cs @@ -7,113 +7,112 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Projection; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal enum BufferMapDirection +{ + Identity, + Down, + Up, + Unrelated +} + +internal static class IBufferGraphExtensions { - internal enum BufferMapDirection + public static SnapshotSpan? MapUpOrDownToFirstMatch(this IBufferGraph bufferGraph, SnapshotSpan span, Predicate match) { - Identity, - Down, - Up, - Unrelated + var spans = bufferGraph.MapDownToFirstMatch(span, SpanTrackingMode.EdgeExclusive, match); + if (!spans.Any()) + { + spans = bufferGraph.MapUpToFirstMatch(span, SpanTrackingMode.EdgeExclusive, match); + } + + return spans.Select(s => (SnapshotSpan?)s).FirstOrDefault(); } - internal static class IBufferGraphExtensions + public static SnapshotSpan? MapUpOrDownToBuffer(this IBufferGraph bufferGraph, SnapshotSpan span, ITextBuffer targetBuffer) { - public static SnapshotSpan? MapUpOrDownToFirstMatch(this IBufferGraph bufferGraph, SnapshotSpan span, Predicate match) + var direction = ClassifyBufferMapDirection(span.Snapshot.TextBuffer, targetBuffer); + switch (direction) { - var spans = bufferGraph.MapDownToFirstMatch(span, SpanTrackingMode.EdgeExclusive, match); - if (!spans.Any()) - { - spans = bufferGraph.MapUpToFirstMatch(span, SpanTrackingMode.EdgeExclusive, match); - } - - return spans.Select(s => (SnapshotSpan?)s).FirstOrDefault(); + case BufferMapDirection.Identity: + return span; + + case BufferMapDirection.Down: + { + var spans = bufferGraph.MapDownToBuffer(span, SpanTrackingMode.EdgeExclusive, targetBuffer); + return spans.Select(s => (SnapshotSpan?)s).FirstOrDefault(); + } + + case BufferMapDirection.Up: + { + var spans = bufferGraph.MapUpToBuffer(span, SpanTrackingMode.EdgeExclusive, targetBuffer); + return spans.Select(s => (SnapshotSpan?)s).FirstOrDefault(); + } + + default: + return null; } + } - public static SnapshotSpan? MapUpOrDownToBuffer(this IBufferGraph bufferGraph, SnapshotSpan span, ITextBuffer targetBuffer) + public static SnapshotPoint? MapUpOrDownToBuffer(this IBufferGraph bufferGraph, SnapshotPoint point, ITextBuffer targetBuffer) + { + var direction = ClassifyBufferMapDirection(point.Snapshot.TextBuffer, targetBuffer); + switch (direction) { - var direction = ClassifyBufferMapDirection(span.Snapshot.TextBuffer, targetBuffer); - switch (direction) - { - case BufferMapDirection.Identity: - return span; + case BufferMapDirection.Identity: + return point; - case BufferMapDirection.Down: + case BufferMapDirection.Down: + { + // TODO (https://github.com/dotnet/roslyn/issues/5281): Remove try-catch. + try { - var spans = bufferGraph.MapDownToBuffer(span, SpanTrackingMode.EdgeExclusive, targetBuffer); - return spans.Select(s => (SnapshotSpan?)s).FirstOrDefault(); + return bufferGraph.MapDownToInsertionPoint(point, PointTrackingMode.Positive, s => s == targetBuffer.CurrentSnapshot); } - - case BufferMapDirection.Up: + catch (ArgumentOutOfRangeException) when (bufferGraph.TopBuffer.ContentType.TypeName == "Interactive Content") { - var spans = bufferGraph.MapUpToBuffer(span, SpanTrackingMode.EdgeExclusive, targetBuffer); - return spans.Select(s => (SnapshotSpan?)s).FirstOrDefault(); + // Suppress this to work around DevDiv #144964. + // Note: Other callers might be affected, but this is the narrowest workaround for the observed problems. + // A fix is already being reviewed, so a broader change is not required. + return null; } + } + + case BufferMapDirection.Up: + { + return bufferGraph.MapUpToBuffer(point, PointTrackingMode.Positive, PositionAffinity.Predecessor, targetBuffer); + } - default: - return null; - } + default: + return null; } + } - public static SnapshotPoint? MapUpOrDownToBuffer(this IBufferGraph bufferGraph, SnapshotPoint point, ITextBuffer targetBuffer) + public static BufferMapDirection ClassifyBufferMapDirection(ITextBuffer startBuffer, ITextBuffer destinationBuffer) + { + if (startBuffer == destinationBuffer) { - var direction = ClassifyBufferMapDirection(point.Snapshot.TextBuffer, targetBuffer); - switch (direction) - { - case BufferMapDirection.Identity: - return point; - - case BufferMapDirection.Down: - { - // TODO (https://github.com/dotnet/roslyn/issues/5281): Remove try-catch. - try - { - return bufferGraph.MapDownToInsertionPoint(point, PointTrackingMode.Positive, s => s == targetBuffer.CurrentSnapshot); - } - catch (ArgumentOutOfRangeException) when (bufferGraph.TopBuffer.ContentType.TypeName == "Interactive Content") - { - // Suppress this to work around DevDiv #144964. - // Note: Other callers might be affected, but this is the narrowest workaround for the observed problems. - // A fix is already being reviewed, so a broader change is not required. - return null; - } - } - - case BufferMapDirection.Up: - { - return bufferGraph.MapUpToBuffer(point, PointTrackingMode.Positive, PositionAffinity.Predecessor, targetBuffer); - } - - default: - return null; - } + return BufferMapDirection.Identity; } - public static BufferMapDirection ClassifyBufferMapDirection(ITextBuffer startBuffer, ITextBuffer destinationBuffer) + // Are we trying to map down or up? + if (startBuffer is IProjectionBufferBase startProjBuffer && IsSourceBuffer(startProjBuffer, destinationBuffer)) { - if (startBuffer == destinationBuffer) - { - return BufferMapDirection.Identity; - } - - // Are we trying to map down or up? - if (startBuffer is IProjectionBufferBase startProjBuffer && IsSourceBuffer(startProjBuffer, destinationBuffer)) - { - return BufferMapDirection.Down; - } - - if (destinationBuffer is IProjectionBufferBase destProjBuffer && IsSourceBuffer(destProjBuffer, startBuffer)) - { - return BufferMapDirection.Up; - } - - return BufferMapDirection.Unrelated; + return BufferMapDirection.Down; } - private static bool IsSourceBuffer(IProjectionBufferBase top, ITextBuffer bottom) + if (destinationBuffer is IProjectionBufferBase destProjBuffer && IsSourceBuffer(destProjBuffer, startBuffer)) { - return top.SourceBuffers.Contains(bottom) || - top.SourceBuffers.OfType().Any(b => IsSourceBuffer(b, bottom)); + return BufferMapDirection.Up; } + + return BufferMapDirection.Unrelated; + } + + private static bool IsSourceBuffer(IProjectionBufferBase top, ITextBuffer bottom) + { + return top.SourceBuffers.Contains(bottom) || + top.SourceBuffers.OfType().Any(b => IsSourceBuffer(b, bottom)); } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/IContentTypeExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/IContentTypeExtensions.cs index 1fba89667944e..a9616a8d363bf 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/IContentTypeExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/IContentTypeExtensions.cs @@ -6,20 +6,19 @@ using System.Linq; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class IContentTypeExtensions { - internal static class IContentTypeExtensions - { - /// - /// Test whether an extension matches a content type. - /// - /// Content type (typically of a text buffer) against which to - /// match an extension. - /// Content types from extension metadata. - public static bool MatchesAny(this IContentType dataContentType, IEnumerable extensionContentTypes) - => extensionContentTypes.Any(v => dataContentType.IsOfType(v)); + /// + /// Test whether an extension matches a content type. + /// + /// Content type (typically of a text buffer) against which to + /// match an extension. + /// Content types from extension metadata. + public static bool MatchesAny(this IContentType dataContentType, IEnumerable extensionContentTypes) + => extensionContentTypes.Any(v => dataContentType.IsOfType(v)); - public static bool MatchesAny(this IContentType dataContentType, params string[] extensionContentTypes) - => dataContentType.MatchesAny((IEnumerable)extensionContentTypes); - } + public static bool MatchesAny(this IContentType dataContentType, params string[] extensionContentTypes) + => dataContentType.MatchesAny((IEnumerable)extensionContentTypes); } diff --git a/src/EditorFeatures/Core/Shared/Extensions/IProjectionBufferFactoryServiceExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/IProjectionBufferFactoryServiceExtensions.cs index df7ad689b4319..beac7c810cc13 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/IProjectionBufferFactoryServiceExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/IProjectionBufferFactoryServiceExtensions.cs @@ -15,330 +15,329 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class IProjectionBufferFactoryServiceExtensions { - internal static class IProjectionBufferFactoryServiceExtensions + public const string RoslynPreviewContentType = nameof(RoslynPreviewContentType); + + /// + /// Hack to get view taggers working on our preview surfaces. We need to define + /// both projection and text in order for this to work. Talk to JasonMal for he is the only + /// one who understands this. + /// + [Export] + [Name(RoslynPreviewContentType)] + [BaseDefinition("text")] + [BaseDefinition("projection")] + public static readonly ContentTypeDefinition? RoslynPreviewContentTypeDefinition; + + public static IProjectionBuffer CreateProjectionBufferWithoutIndentation( + this IProjectionBufferFactoryService factoryService, + IEditorOptions editorOptions, + IContentType? contentType = null, + params SnapshotSpan[] exposedSpans) + { + return factoryService.CreateProjectionBufferWithoutIndentation( + editorOptions, + contentType, + (IEnumerable)exposedSpans); + } + + public static IProjectionBuffer CreateProjectionBufferWithoutIndentation( + this IProjectionBufferFactoryService factoryService, + IEditorOptions editorOptions, + IContentType? contentType, + IEnumerable exposedSpans) { - public const string RoslynPreviewContentType = nameof(RoslynPreviewContentType); - - /// - /// Hack to get view taggers working on our preview surfaces. We need to define - /// both projection and text in order for this to work. Talk to JasonMal for he is the only - /// one who understands this. - /// - [Export] - [Name(RoslynPreviewContentType)] - [BaseDefinition("text")] - [BaseDefinition("projection")] - public static readonly ContentTypeDefinition? RoslynPreviewContentTypeDefinition; - - public static IProjectionBuffer CreateProjectionBufferWithoutIndentation( - this IProjectionBufferFactoryService factoryService, - IEditorOptions editorOptions, - IContentType? contentType = null, - params SnapshotSpan[] exposedSpans) + var spans = new NormalizedSnapshotSpanCollection(exposedSpans); + + if (spans.Count > 0) { - return factoryService.CreateProjectionBufferWithoutIndentation( - editorOptions, - contentType, - (IEnumerable)exposedSpans); + // BUG(6335): We have to make sure that the spans refer to the current snapshot of + // the buffer. + var buffer = spans.First().Snapshot.TextBuffer; + var currentSnapshot = buffer.CurrentSnapshot; + spans = new NormalizedSnapshotSpanCollection( + spans.Select(s => s.TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive))); } - public static IProjectionBuffer CreateProjectionBufferWithoutIndentation( - this IProjectionBufferFactoryService factoryService, - IEditorOptions editorOptions, - IContentType? contentType, - IEnumerable exposedSpans) - { - var spans = new NormalizedSnapshotSpanCollection(exposedSpans); + contentType ??= factoryService.ProjectionContentType; + var projectionBuffer = factoryService.CreateProjectionBuffer( + projectionEditResolver: null, + sourceSpans: [], + options: ProjectionBufferOptions.None, + contentType: contentType); - if (spans.Count > 0) - { - // BUG(6335): We have to make sure that the spans refer to the current snapshot of - // the buffer. - var buffer = spans.First().Snapshot.TextBuffer; - var currentSnapshot = buffer.CurrentSnapshot; - spans = new NormalizedSnapshotSpanCollection( - spans.Select(s => s.TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive))); - } + if (spans.Count > 0) + { + var finalSpans = new List(); - contentType ??= factoryService.ProjectionContentType; - var projectionBuffer = factoryService.CreateProjectionBuffer( - projectionEditResolver: null, - sourceSpans: [], - options: ProjectionBufferOptions.None, - contentType: contentType); + // We need to figure out the shorted indentation level of the exposed lines. We'll + // then remove that indentation from all lines. + var indentationColumn = DetermineIndentationColumn(editorOptions, spans); - if (spans.Count > 0) + foreach (var span in spans) { - var finalSpans = new List(); - - // We need to figure out the shorted indentation level of the exposed lines. We'll - // then remove that indentation from all lines. - var indentationColumn = DetermineIndentationColumn(editorOptions, spans); + var snapshot = span.Snapshot; + var startLineNumber = snapshot.GetLineNumberFromPosition(span.Start); + var endLineNumber = snapshot.GetLineNumberFromPosition(span.End); - foreach (var span in spans) + for (var lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - var snapshot = span.Snapshot; - var startLineNumber = snapshot.GetLineNumberFromPosition(span.Start); - var endLineNumber = snapshot.GetLineNumberFromPosition(span.End); + // Compute the span clamped to this line + var line = snapshot.GetLineFromLineNumber(lineNumber); + var finalSpanStart = Math.Max(line.Start, span.Start); + var finalSpanEnd = Math.Min(line.EndIncludingLineBreak, span.End); - for (var lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) + // We'll only offset if our span doesn't already start at the start of the line. See the similar exclusion in + // DetermineIndentationColumn that this matches. + if (line.Start == finalSpanStart) { - // Compute the span clamped to this line - var line = snapshot.GetLineFromLineNumber(lineNumber); - var finalSpanStart = Math.Max(line.Start, span.Start); - var finalSpanEnd = Math.Min(line.EndIncludingLineBreak, span.End); - - // We'll only offset if our span doesn't already start at the start of the line. See the similar exclusion in - // DetermineIndentationColumn that this matches. - if (line.Start == finalSpanStart) - { - finalSpanStart += line.GetLineOffsetFromColumn(indentationColumn, editorOptions); + finalSpanStart += line.GetLineOffsetFromColumn(indentationColumn, editorOptions); - // Paranoia: what if the indentation reversed our ordering? - if (finalSpanStart > finalSpanEnd) - { - finalSpanStart = finalSpanEnd; - } + // Paranoia: what if the indentation reversed our ordering? + if (finalSpanStart > finalSpanEnd) + { + finalSpanStart = finalSpanEnd; } - - // We don't expect edits to happen while this projection buffer is active. We'll choose EdgeExclusive so - // if they do we don't end up in any cases where there is overlapping source spans. - finalSpans.Add(snapshot.CreateTrackingSpan(Span.FromBounds(finalSpanStart, finalSpanEnd), SpanTrackingMode.EdgeExclusive)); } - } - projectionBuffer.InsertSpans(0, finalSpans); + // We don't expect edits to happen while this projection buffer is active. We'll choose EdgeExclusive so + // if they do we don't end up in any cases where there is overlapping source spans. + finalSpans.Add(snapshot.CreateTrackingSpan(Span.FromBounds(finalSpanStart, finalSpanEnd), SpanTrackingMode.EdgeExclusive)); + } } - return projectionBuffer; + projectionBuffer.InsertSpans(0, finalSpans); } - private static int DetermineIndentationColumn( - IEditorOptions editorOptions, - IEnumerable spans) + return projectionBuffer; + } + + private static int DetermineIndentationColumn( + IEditorOptions editorOptions, + IEnumerable spans) + { + int? indentationColumn = null; + foreach (var span in spans) { - int? indentationColumn = null; - foreach (var span in spans) + var snapshot = span.Snapshot; + var startLineNumber = snapshot.GetLineNumberFromPosition(span.Start); + var endLineNumber = snapshot.GetLineNumberFromPosition(span.End); + + // If the span starts after the first non-whitespace of the first line, we'll + // exclude that line to avoid throwing off the calculation. Otherwise, the + // incorrect indentation will be returned for lambda cases like so: + // + // void M() + // { + // Func f = () => + // { + // return 1; + // }; + // } + // + // Without throwing out the first line in the example above, the indentation column + // used will be 4, rather than 8. + var startLineFirstNonWhitespace = snapshot.GetLineFromLineNumber(startLineNumber).GetFirstNonWhitespacePosition(); + if (startLineFirstNonWhitespace.HasValue && startLineFirstNonWhitespace.Value < span.Start) { - var snapshot = span.Snapshot; - var startLineNumber = snapshot.GetLineNumberFromPosition(span.Start); - var endLineNumber = snapshot.GetLineNumberFromPosition(span.End); + startLineNumber++; + } - // If the span starts after the first non-whitespace of the first line, we'll - // exclude that line to avoid throwing off the calculation. Otherwise, the - // incorrect indentation will be returned for lambda cases like so: - // - // void M() - // { - // Func f = () => - // { - // return 1; - // }; - // } - // - // Without throwing out the first line in the example above, the indentation column - // used will be 4, rather than 8. - var startLineFirstNonWhitespace = snapshot.GetLineFromLineNumber(startLineNumber).GetFirstNonWhitespacePosition(); - if (startLineFirstNonWhitespace.HasValue && startLineFirstNonWhitespace.Value < span.Start) + for (var lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) + { + var line = snapshot.GetLineFromLineNumber(lineNumber); + if (string.IsNullOrWhiteSpace(line.GetText())) { - startLineNumber++; + continue; } - for (var lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) - { - var line = snapshot.GetLineFromLineNumber(lineNumber); - if (string.IsNullOrWhiteSpace(line.GetText())) - { - continue; - } - - indentationColumn = indentationColumn.HasValue - ? Math.Min(indentationColumn.Value, line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(editorOptions)) - : line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(editorOptions); - } + indentationColumn = indentationColumn.HasValue + ? Math.Min(indentationColumn.Value, line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(editorOptions)) + : line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(editorOptions); } - - return indentationColumn ?? 0; } - public static IProjectionBuffer CreateProjectionBuffer( - this IProjectionBufferFactoryService factoryService, - IContentTypeRegistryService registryService, - IEditorOptions editorOptions, - ITextSnapshot snapshot, - string separator, - params LineSpan[] exposedLineSpans) - { - return CreateProjectionBuffer( - factoryService, - registryService, - editorOptions, - snapshot, - separator, - suffixOpt: null, - trim: false, - exposedLineSpans: exposedLineSpans); - } + return indentationColumn ?? 0; + } - public static IProjectionBuffer CreateProjectionBufferWithoutIndentation( - this IProjectionBufferFactoryService factoryService, - IContentTypeRegistryService registryService, - IEditorOptions editorOptions, - ITextSnapshot snapshot, - string separator, - params LineSpan[] exposedLineSpans) - { - return factoryService.CreateProjectionBufferWithoutIndentation( - registryService, - editorOptions, - snapshot, - separator, - suffixOpt: null, - exposedLineSpans: exposedLineSpans); - } + public static IProjectionBuffer CreateProjectionBuffer( + this IProjectionBufferFactoryService factoryService, + IContentTypeRegistryService registryService, + IEditorOptions editorOptions, + ITextSnapshot snapshot, + string separator, + params LineSpan[] exposedLineSpans) + { + return CreateProjectionBuffer( + factoryService, + registryService, + editorOptions, + snapshot, + separator, + suffixOpt: null, + trim: false, + exposedLineSpans: exposedLineSpans); + } - public static IProjectionBuffer CreateProjectionBufferWithoutIndentation( - this IProjectionBufferFactoryService factoryService, - IContentTypeRegistryService registryService, - IEditorOptions editorOptions, - ITextSnapshot snapshot, - string separator, - object? suffixOpt, - params LineSpan[] exposedLineSpans) - { - return CreateProjectionBuffer( - factoryService, - registryService, - editorOptions, - snapshot, - separator, - suffixOpt, - trim: true, - exposedLineSpans: exposedLineSpans); - } + public static IProjectionBuffer CreateProjectionBufferWithoutIndentation( + this IProjectionBufferFactoryService factoryService, + IContentTypeRegistryService registryService, + IEditorOptions editorOptions, + ITextSnapshot snapshot, + string separator, + params LineSpan[] exposedLineSpans) + { + return factoryService.CreateProjectionBufferWithoutIndentation( + registryService, + editorOptions, + snapshot, + separator, + suffixOpt: null, + exposedLineSpans: exposedLineSpans); + } - public static IProjectionBuffer CreatePreviewProjectionBuffer( - this IProjectionBufferFactoryService factoryService, - IList sourceSpans, - IContentTypeRegistryService registryService) - { - return factoryService.CreateProjectionBuffer( - projectionEditResolver: null, - sourceSpans: sourceSpans, - options: ProjectionBufferOptions.None, - contentType: registryService.GetContentType(RoslynPreviewContentType)); - } + public static IProjectionBuffer CreateProjectionBufferWithoutIndentation( + this IProjectionBufferFactoryService factoryService, + IContentTypeRegistryService registryService, + IEditorOptions editorOptions, + ITextSnapshot snapshot, + string separator, + object? suffixOpt, + params LineSpan[] exposedLineSpans) + { + return CreateProjectionBuffer( + factoryService, + registryService, + editorOptions, + snapshot, + separator, + suffixOpt, + trim: true, + exposedLineSpans: exposedLineSpans); + } + + public static IProjectionBuffer CreatePreviewProjectionBuffer( + this IProjectionBufferFactoryService factoryService, + IList sourceSpans, + IContentTypeRegistryService registryService) + { + return factoryService.CreateProjectionBuffer( + projectionEditResolver: null, + sourceSpans: sourceSpans, + options: ProjectionBufferOptions.None, + contentType: registryService.GetContentType(RoslynPreviewContentType)); + } - private static IProjectionBuffer CreateProjectionBuffer( - IProjectionBufferFactoryService factoryService, - IContentTypeRegistryService registryService, - IEditorOptions editorOptions, - ITextSnapshot snapshot, - string separator, - object? suffixOpt, - bool trim, - params LineSpan[] exposedLineSpans) + private static IProjectionBuffer CreateProjectionBuffer( + IProjectionBufferFactoryService factoryService, + IContentTypeRegistryService registryService, + IEditorOptions editorOptions, + ITextSnapshot snapshot, + string separator, + object? suffixOpt, + bool trim, + params LineSpan[] exposedLineSpans) + { + var spans = new List(); + if (exposedLineSpans.Length > 0) { - var spans = new List(); - if (exposedLineSpans.Length > 0) + if (exposedLineSpans[0].Start > 0 && !string.IsNullOrEmpty(separator)) { - if (exposedLineSpans[0].Start > 0 && !string.IsNullOrEmpty(separator)) - { - spans.Add(separator); - spans.Add(editorOptions.GetNewLineCharacter()); - } + spans.Add(separator); + spans.Add(editorOptions.GetNewLineCharacter()); + } - var snapshotSpanRanges = CreateSnapshotSpanRanges(snapshot, exposedLineSpans); - var indentColumn = trim - ? DetermineIndentationColumn(editorOptions, snapshotSpanRanges.Flatten()) - : 0; + var snapshotSpanRanges = CreateSnapshotSpanRanges(snapshot, exposedLineSpans); + var indentColumn = trim + ? DetermineIndentationColumn(editorOptions, snapshotSpanRanges.Flatten()) + : 0; - foreach (var snapshotSpanRange in snapshotSpanRanges) + foreach (var snapshotSpanRange in snapshotSpanRanges) + { + foreach (var snapshotSpan in snapshotSpanRange) { - foreach (var snapshotSpan in snapshotSpanRange) - { - var line = snapshotSpan.Snapshot.GetLineFromPosition(snapshotSpan.Start); - var indentPosition = line.GetLineOffsetFromColumn(indentColumn, editorOptions) + line.Start; - var mappedSpan = new SnapshotSpan(snapshotSpan.Snapshot, - Span.FromBounds(indentPosition, snapshotSpan.End)); + var line = snapshotSpan.Snapshot.GetLineFromPosition(snapshotSpan.Start); + var indentPosition = line.GetLineOffsetFromColumn(indentColumn, editorOptions) + line.Start; + var mappedSpan = new SnapshotSpan(snapshotSpan.Snapshot, + Span.FromBounds(indentPosition, snapshotSpan.End)); - var trackingSpan = mappedSpan.CreateTrackingSpan(SpanTrackingMode.EdgeExclusive); + var trackingSpan = mappedSpan.CreateTrackingSpan(SpanTrackingMode.EdgeExclusive); - spans.Add(trackingSpan); + spans.Add(trackingSpan); - // Add a newline between every line. - if (snapshotSpan != snapshotSpanRange.Last()) - { - spans.Add(editorOptions.GetNewLineCharacter()); - } - } - - // Add a separator between every set of lines. - if (snapshotSpanRange != snapshotSpanRanges.Last()) + // Add a newline between every line. + if (snapshotSpan != snapshotSpanRange.Last()) { spans.Add(editorOptions.GetNewLineCharacter()); - spans.Add(separator); - spans.Add(editorOptions.GetNewLineCharacter()); } } - if (snapshot.GetLineNumberFromPosition(snapshotSpanRanges.Last().Last().End) < snapshot.LineCount - 1) + // Add a separator between every set of lines. + if (snapshotSpanRange != snapshotSpanRanges.Last()) { spans.Add(editorOptions.GetNewLineCharacter()); spans.Add(separator); + spans.Add(editorOptions.GetNewLineCharacter()); } } - if (suffixOpt != null) + if (snapshot.GetLineNumberFromPosition(snapshotSpanRanges.Last().Last().End) < snapshot.LineCount - 1) { - if (spans.Count >= 0) - { - if (!separator.Equals(spans.Last())) - { - spans.Add(editorOptions.GetNewLineCharacter()); - spans.Add(separator); - } - - spans.Add(editorOptions.GetNewLineCharacter()); - } - - spans.Add(suffixOpt); + spans.Add(editorOptions.GetNewLineCharacter()); + spans.Add(separator); } - - return factoryService.CreateProjectionBuffer( - projectionEditResolver: null, - sourceSpans: spans, - options: ProjectionBufferOptions.None, - contentType: registryService.GetContentType(RoslynPreviewContentType)); } - private static IList> CreateSnapshotSpanRanges(ITextSnapshot snapshot, LineSpan[] exposedLineSpans) + if (suffixOpt != null) { - var result = new List>(); - foreach (var lineSpan in exposedLineSpans) + if (spans.Count >= 0) { - var snapshotSpans = CreateSnapshotSpans(snapshot, lineSpan); - if (snapshotSpans.Count > 0) + if (!separator.Equals(spans.Last())) { - result.Add(snapshotSpans); + spans.Add(editorOptions.GetNewLineCharacter()); + spans.Add(separator); } + + spans.Add(editorOptions.GetNewLineCharacter()); } - return result; + spans.Add(suffixOpt); } - private static IList CreateSnapshotSpans(ITextSnapshot snapshot, LineSpan lineSpan) + return factoryService.CreateProjectionBuffer( + projectionEditResolver: null, + sourceSpans: spans, + options: ProjectionBufferOptions.None, + contentType: registryService.GetContentType(RoslynPreviewContentType)); + } + + private static IList> CreateSnapshotSpanRanges(ITextSnapshot snapshot, LineSpan[] exposedLineSpans) + { + var result = new List>(); + foreach (var lineSpan in exposedLineSpans) { - var result = new List(); - for (var i = lineSpan.Start; i < lineSpan.End; i++) + var snapshotSpans = CreateSnapshotSpans(snapshot, lineSpan); + if (snapshotSpans.Count > 0) { - var line = snapshot.GetLineFromLineNumber(i); - result.Add(line.Extent); + result.Add(snapshotSpans); } + } - return result; + return result; + } + + private static IList CreateSnapshotSpans(ITextSnapshot snapshot, LineSpan lineSpan) + { + var result = new List(); + for (var i = lineSpan.Start; i < lineSpan.End; i++) + { + var line = snapshot.GetLineFromLineNumber(i); + result.Add(line.Extent); } + + return result; } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/IRefactorNotifyServiceExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/IRefactorNotifyServiceExtensions.cs index de47365091597..b97417e6dd6aa 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/IRefactorNotifyServiceExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/IRefactorNotifyServiceExtensions.cs @@ -4,46 +4,45 @@ using System.Collections.Generic; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class IRefactorNotifyServiceExtensions { - internal static class IRefactorNotifyServiceExtensions + public static bool TryOnBeforeGlobalSymbolRenamed( + this IEnumerable refactorNotifyServices, + Workspace workspace, + IEnumerable changedDocuments, + ISymbol symbol, + string newName, + bool throwOnFailure) { - public static bool TryOnBeforeGlobalSymbolRenamed( - this IEnumerable refactorNotifyServices, - Workspace workspace, - IEnumerable changedDocuments, - ISymbol symbol, - string newName, - bool throwOnFailure) + foreach (var refactorNotifyService in refactorNotifyServices) { - foreach (var refactorNotifyService in refactorNotifyServices) + if (!refactorNotifyService.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure)) { - if (!refactorNotifyService.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure)) - { - return false; - } + return false; } - - return true; } - public static bool TryOnAfterGlobalSymbolRenamed( - this IEnumerable refactorNotifyServices, - Workspace workspace, - IEnumerable changedDocuments, - ISymbol symbol, - string newName, - bool throwOnFailure) + return true; + } + + public static bool TryOnAfterGlobalSymbolRenamed( + this IEnumerable refactorNotifyServices, + Workspace workspace, + IEnumerable changedDocuments, + ISymbol symbol, + string newName, + bool throwOnFailure) + { + foreach (var refactorNotifyService in refactorNotifyServices) { - foreach (var refactorNotifyService in refactorNotifyServices) + if (!refactorNotifyService.TryOnAfterGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure)) { - if (!refactorNotifyService.TryOnAfterGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure)) - { - return false; - } + return false; } - - return true; } + + return true; } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/ITextBufferEditExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/ITextBufferEditExtensions.cs index 4b89d10366aec..b2d38dafb2390 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/ITextBufferEditExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/ITextBufferEditExtensions.cs @@ -5,33 +5,32 @@ using System; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class ITextBufferEditExtensions { - internal static class ITextBufferEditExtensions - { #pragma warning disable IDE0052 // Remove unread private members - Used for debugging. - private static Exception? s_lastException = null; + private static Exception? s_lastException = null; #pragma warning restore IDE0052 // Remove unread private members - /// - /// Logs exceptions thrown during as we look for issues. - /// - /// - /// - public static ITextSnapshot ApplyAndLogExceptions(this ITextBufferEdit edit) + /// + /// Logs exceptions thrown during as we look for issues. + /// + /// + /// + public static ITextSnapshot ApplyAndLogExceptions(this ITextBufferEdit edit) + { + try + { + return edit.Apply(); + } + catch (Exception e) when (ErrorReporting.FatalError.ReportAndCatch(e, ErrorReporting.ErrorSeverity.Critical)) { - try - { - return edit.Apply(); - } - catch (Exception e) when (ErrorReporting.FatalError.ReportAndCatch(e, ErrorReporting.ErrorSeverity.Critical)) - { - s_lastException = e; + s_lastException = e; - // Since we don't know what is causing this yet, I don't feel safe that catching - // will not cause some further downstream failure. So we'll continue to propagate. - throw; - } + // Since we don't know what is causing this yet, I don't feel safe that catching + // will not cause some further downstream failure. So we'll continue to propagate. + throw; } } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/ITextBufferExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/ITextBufferExtensions.cs index 4d6820845a389..e20cc9a3c7764 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/ITextBufferExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/ITextBufferExtensions.cs @@ -9,85 +9,84 @@ using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static partial class ITextBufferExtensions { - internal static partial class ITextBufferExtensions + internal static bool IsInLspEditorContext(this ITextBuffer buffer) { - internal static bool IsInLspEditorContext(this ITextBuffer buffer) + if (buffer.TryGetWorkspace(out var workspace)) { - if (buffer.TryGetWorkspace(out var workspace)) - { - var workspaceContextService = workspace.Services.GetRequiredService(); - return workspaceContextService.IsInLspEditorContext(); - } - - return false; + var workspaceContextService = workspace.Services.GetRequiredService(); + return workspaceContextService.IsInLspEditorContext(); } - internal static bool TryGetWorkspace(this ITextBuffer buffer, [NotNullWhen(true)] out Workspace? workspace) - => Workspace.TryGetWorkspace(buffer.AsTextContainer(), out workspace); - - /// - /// Checks if a buffer supports refactorings. - /// - internal static bool SupportsRefactorings(this ITextBuffer buffer) - => TryGetSupportsFeatureService(buffer, out var service) && service.SupportsRefactorings(buffer); - - /// - /// Checks if a buffer supports rename. - /// - internal static bool SupportsRename(this ITextBuffer buffer) - => TryGetSupportsFeatureService(buffer, out var service) && service.SupportsRename(buffer); - - /// - /// Checks if a buffer supports code fixes. - /// - internal static bool SupportsCodeFixes(this ITextBuffer buffer) - => TryGetSupportsFeatureService(buffer, out var service) && service.SupportsCodeFixes(buffer); - - /// - /// Checks if a buffer supports navigation. - /// - internal static bool SupportsNavigationToAnyPosition(this ITextBuffer buffer) - => TryGetSupportsFeatureService(buffer, out var service) && service.SupportsNavigationToAnyPosition(buffer); - - private static bool TryGetSupportsFeatureService(ITextBuffer buffer, [NotNullWhen(true)] out ITextBufferSupportsFeatureService? service) - { - service = null; - if (buffer.TryGetWorkspace(out var workspace)) - { - service = workspace.Services.GetService(); - } + return false; + } + + internal static bool TryGetWorkspace(this ITextBuffer buffer, [NotNullWhen(true)] out Workspace? workspace) + => Workspace.TryGetWorkspace(buffer.AsTextContainer(), out workspace); + + /// + /// Checks if a buffer supports refactorings. + /// + internal static bool SupportsRefactorings(this ITextBuffer buffer) + => TryGetSupportsFeatureService(buffer, out var service) && service.SupportsRefactorings(buffer); - return service != null; + /// + /// Checks if a buffer supports rename. + /// + internal static bool SupportsRename(this ITextBuffer buffer) + => TryGetSupportsFeatureService(buffer, out var service) && service.SupportsRename(buffer); + + /// + /// Checks if a buffer supports code fixes. + /// + internal static bool SupportsCodeFixes(this ITextBuffer buffer) + => TryGetSupportsFeatureService(buffer, out var service) && service.SupportsCodeFixes(buffer); + + /// + /// Checks if a buffer supports navigation. + /// + internal static bool SupportsNavigationToAnyPosition(this ITextBuffer buffer) + => TryGetSupportsFeatureService(buffer, out var service) && service.SupportsNavigationToAnyPosition(buffer); + + private static bool TryGetSupportsFeatureService(ITextBuffer buffer, [NotNullWhen(true)] out ITextBufferSupportsFeatureService? service) + { + service = null; + if (buffer.TryGetWorkspace(out var workspace)) + { + service = workspace.Services.GetService(); } - public static ITextSnapshot ApplyChange(this ITextBuffer buffer, TextChange change) + return service != null; + } + + public static ITextSnapshot ApplyChange(this ITextBuffer buffer, TextChange change) + { + if (buffer.Properties.TryGetProperty(typeof(IContainedDocument), out var containedDocument)) { - if (buffer.Properties.TryGetProperty(typeof(IContainedDocument), out var containedDocument)) - { - return containedDocument.ApplyChanges([change]); - } + return containedDocument.ApplyChanges([change]); + } - using var edit = buffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null); - edit.Replace(change.Span.ToSpan(), change.NewText); - return edit.Apply(); + using var edit = buffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null); + edit.Replace(change.Span.ToSpan(), change.NewText); + return edit.Apply(); + } + + public static ITextSnapshot ApplyChanges(this ITextBuffer buffer, IEnumerable changes) + { + if (buffer.Properties.TryGetProperty(typeof(IContainedDocument), out var containedDocument)) + { + return containedDocument.ApplyChanges(changes); } - public static ITextSnapshot ApplyChanges(this ITextBuffer buffer, IEnumerable changes) + using var edit = buffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null); + foreach (var change in changes) { - if (buffer.Properties.TryGetProperty(typeof(IContainedDocument), out var containedDocument)) - { - return containedDocument.ApplyChanges(changes); - } - - using var edit = buffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null); - foreach (var change in changes) - { - edit.Replace(change.Span.ToSpan(), change.NewText); - } - - return edit.Apply(); + edit.Replace(change.Span.ToSpan(), change.NewText); } + + return edit.Apply(); } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/ITextSelectionExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/ITextSelectionExtensions.cs index 6aa81013aeb35..9e73dc6aaea40 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/ITextSelectionExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/ITextSelectionExtensions.cs @@ -7,22 +7,21 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class ITextSelectionExtensions { - internal static class ITextSelectionExtensions + public static NormalizedSnapshotSpanCollection GetSnapshotSpansOnBuffer(this ITextSelection selection, ITextBuffer subjectBuffer) { - public static NormalizedSnapshotSpanCollection GetSnapshotSpansOnBuffer(this ITextSelection selection, ITextBuffer subjectBuffer) - { - Contract.ThrowIfNull(selection); - Contract.ThrowIfNull(subjectBuffer); - - var list = new List(); - foreach (var snapshotSpan in selection.SelectedSpans) - { - list.AddRange(selection.TextView.BufferGraph.MapDownToBuffer(snapshotSpan, SpanTrackingMode.EdgeExclusive, subjectBuffer)); - } + Contract.ThrowIfNull(selection); + Contract.ThrowIfNull(subjectBuffer); - return new NormalizedSnapshotSpanCollection(list); + var list = new List(); + foreach (var snapshotSpan in selection.SelectedSpans) + { + list.AddRange(selection.TextView.BufferGraph.MapDownToBuffer(snapshotSpan, SpanTrackingMode.EdgeExclusive, subjectBuffer)); } + + return new NormalizedSnapshotSpanCollection(list); } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/ITextSnapshotExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/ITextSnapshotExtensions.cs index c5b3b651a18ee..1bad744ea46fc 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/ITextSnapshotExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/ITextSnapshotExtensions.cs @@ -19,87 +19,86 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static partial class ITextSnapshotExtensions { - internal static partial class ITextSnapshotExtensions + /// + /// format given snapshot and apply text changes to buffer + /// + public static void FormatAndApplyToBuffer( + this ITextBuffer textBuffer, + TextSpan span, + EditorOptionsService editorOptionsService, + CancellationToken cancellationToken) { - /// - /// format given snapshot and apply text changes to buffer - /// - public static void FormatAndApplyToBuffer( - this ITextBuffer textBuffer, - TextSpan span, - EditorOptionsService editorOptionsService, - CancellationToken cancellationToken) + var document = textBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) { - var document = textBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - { - return; - } + return; + } - var documentSyntax = ParsedDocument.CreateSynchronously(document, cancellationToken); - var rules = FormattingRuleUtilities.GetFormattingRules(documentSyntax, span, additionalRules: null); + var documentSyntax = ParsedDocument.CreateSynchronously(document, cancellationToken); + var rules = FormattingRuleUtilities.GetFormattingRules(documentSyntax, span, additionalRules: null); - var formatter = document.GetRequiredLanguageService(); + var formatter = document.GetRequiredLanguageService(); - var options = textBuffer.GetSyntaxFormattingOptions(editorOptionsService, document.Project.Services, explicitFormat: false); - var result = formatter.GetFormattingResult(documentSyntax.Root, SpecializedCollections.SingletonEnumerable(span), options, rules, cancellationToken); - var changes = result.GetTextChanges(cancellationToken); + var options = textBuffer.GetSyntaxFormattingOptions(editorOptionsService, document.Project.Services, explicitFormat: false); + var result = formatter.GetFormattingResult(documentSyntax.Root, SpecializedCollections.SingletonEnumerable(span), options, rules, cancellationToken); + var changes = result.GetTextChanges(cancellationToken); - using (Logger.LogBlock(FunctionId.Formatting_ApplyResultToBuffer, cancellationToken)) - { - textBuffer.ApplyChanges(changes); - } + using (Logger.LogBlock(FunctionId.Formatting_ApplyResultToBuffer, cancellationToken)) + { + textBuffer.ApplyChanges(changes); } + } - /// - /// Get from - /// once returns - /// - /// for synchronous code path, make sure to use synchronous version - /// . - /// otherwise, one can get into a deadlock - /// - public static async Task GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync( - this ITextSnapshot snapshot, IUIThreadOperationContext operationContext) + /// + /// Get from + /// once returns + /// + /// for synchronous code path, make sure to use synchronous version + /// . + /// otherwise, one can get into a deadlock + /// + public static async Task GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync( + this ITextSnapshot snapshot, IUIThreadOperationContext operationContext) + { + // just get a document from whatever we have + var document = snapshot.TextBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); + if (document == null) { - // just get a document from whatever we have - var document = snapshot.TextBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); - if (document == null) - { - // we don't know about this buffer yet - return null; - } + // we don't know about this buffer yet + return null; + } - // partial mode is always cancellable - using (operationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Waiting_for_background_work_to_finish)) + // partial mode is always cancellable + using (operationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Waiting_for_background_work_to_finish)) + { + var service = document.Project.Solution.Services.GetService(); + if (service != null) { - var service = document.Project.Solution.Services.GetService(); - if (service != null) - { - // TODO: decide for prototype, we don't do anything complex and just ask workspace whether it is fully loaded - // later we might need to go and change all these with more specific info such as document/project/solution - await service.WaitUntilFullyLoadedAsync(operationContext.UserCancellationToken).ConfigureAwait(false); - } - - // get proper document - return snapshot.GetOpenDocumentInCurrentContextWithChanges(); + // TODO: decide for prototype, we don't do anything complex and just ask workspace whether it is fully loaded + // later we might need to go and change all these with more specific info such as document/project/solution + await service.WaitUntilFullyLoadedAsync(operationContext.UserCancellationToken).ConfigureAwait(false); } + + // get proper document + return snapshot.GetOpenDocumentInCurrentContextWithChanges(); } + } - /// - /// Get from - /// once returns - /// - public static Document? GetFullyLoadedOpenDocumentInCurrentContextWithChanges( - this ITextSnapshot snapshot, IUIThreadOperationContext operationContext, IThreadingContext threadingContext) - { - // make sure this is only called from UI thread - threadingContext.ThrowIfNotOnUIThread(); + /// + /// Get from + /// once returns + /// + public static Document? GetFullyLoadedOpenDocumentInCurrentContextWithChanges( + this ITextSnapshot snapshot, IUIThreadOperationContext operationContext, IThreadingContext threadingContext) + { + // make sure this is only called from UI thread + threadingContext.ThrowIfNotOnUIThread(); - return threadingContext.JoinableTaskFactory.Run(() => - snapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(operationContext)); - } + return threadingContext.JoinableTaskFactory.Run(() => + snapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(operationContext)); } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.AutoClosingViewProperty.cs b/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.AutoClosingViewProperty.cs index 6c5a726b97e15..282aa1add2ab0 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.AutoClosingViewProperty.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.AutoClosingViewProperty.cs @@ -8,87 +8,86 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static partial class ITextViewExtensions { - internal static partial class ITextViewExtensions + private class AutoClosingViewProperty where TTextView : ITextView { - private class AutoClosingViewProperty where TTextView : ITextView + private readonly TTextView _textView; + private readonly Dictionary _map = []; + + public static bool GetOrCreateValue( + TTextView textView, + object key, + Func valueCreator, + out TProperty value) { - private readonly TTextView _textView; - private readonly Dictionary _map = []; - - public static bool GetOrCreateValue( - TTextView textView, - object key, - Func valueCreator, - out TProperty value) + Contract.ThrowIfTrue(textView.IsClosed); + + var properties = textView.Properties.GetOrCreateSingletonProperty(() => new AutoClosingViewProperty(textView)); + if (!properties.TryGetValue(key, out var priorValue)) { - Contract.ThrowIfTrue(textView.IsClosed); - - var properties = textView.Properties.GetOrCreateSingletonProperty(() => new AutoClosingViewProperty(textView)); - if (!properties.TryGetValue(key, out var priorValue)) - { - // Need to create it. - value = valueCreator(textView); - properties.Add(key, value); - return true; - } - - // Already there. - value = priorValue; - return false; + // Need to create it. + value = valueCreator(textView); + properties.Add(key, value); + return true; } - public static bool TryGetValue( - TTextView textView, - object key, - [MaybeNullWhen(false)] out TProperty value) - { - Contract.ThrowIfTrue(textView.IsClosed); + // Already there. + value = priorValue; + return false; + } - var properties = textView.Properties.GetOrCreateSingletonProperty(() => new AutoClosingViewProperty(textView)); - return properties.TryGetValue(key, out value); - } + public static bool TryGetValue( + TTextView textView, + object key, + [MaybeNullWhen(false)] out TProperty value) + { + Contract.ThrowIfTrue(textView.IsClosed); - public static void AddValue( - TTextView textView, - object key, - TProperty value) - { - Contract.ThrowIfTrue(textView.IsClosed); + var properties = textView.Properties.GetOrCreateSingletonProperty(() => new AutoClosingViewProperty(textView)); + return properties.TryGetValue(key, out value); + } - var properties = textView.Properties.GetOrCreateSingletonProperty(() => new AutoClosingViewProperty(textView)); - properties.Add(key, value); - } + public static void AddValue( + TTextView textView, + object key, + TProperty value) + { + Contract.ThrowIfTrue(textView.IsClosed); - public static void RemoveValue(TTextView textView, object key) - { - if (textView.Properties.TryGetProperty(typeof(AutoClosingViewProperty), out AutoClosingViewProperty properties)) - { - properties.Remove(key); - } - } + var properties = textView.Properties.GetOrCreateSingletonProperty(() => new AutoClosingViewProperty(textView)); + properties.Add(key, value); + } - private AutoClosingViewProperty(TTextView textView) + public static void RemoveValue(TTextView textView, object key) + { + if (textView.Properties.TryGetProperty(typeof(AutoClosingViewProperty), out AutoClosingViewProperty properties)) { - _textView = textView; - _textView.Closed += OnTextViewClosed; + properties.Remove(key); } + } - private void OnTextViewClosed(object? sender, EventArgs e) - { - _textView.Closed -= OnTextViewClosed; - _textView.Properties.RemoveProperty(typeof(AutoClosingViewProperty)); - } + private AutoClosingViewProperty(TTextView textView) + { + _textView = textView; + _textView.Closed += OnTextViewClosed; + } + + private void OnTextViewClosed(object? sender, EventArgs e) + { + _textView.Closed -= OnTextViewClosed; + _textView.Properties.RemoveProperty(typeof(AutoClosingViewProperty)); + } - public bool TryGetValue(object key, [MaybeNullWhen(false)] out TProperty value) - => _map.TryGetValue(key, out value); + public bool TryGetValue(object key, [MaybeNullWhen(false)] out TProperty value) + => _map.TryGetValue(key, out value); - public void Add(object key, TProperty value) - => _map[key] = value; + public void Add(object key, TProperty value) + => _map[key] = value; - public void Remove(object key) - => _map.Remove(key); - } + public void Remove(object key) + => _map.Remove(key); } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.PerSubjectBufferProperty.cs b/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.PerSubjectBufferProperty.cs index 0173be1b957ca..dd07d002eb298 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.PerSubjectBufferProperty.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.PerSubjectBufferProperty.cs @@ -12,152 +12,151 @@ using Microsoft.VisualStudio.Text.Projection; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static partial class ITextViewExtensions { - internal static partial class ITextViewExtensions + private class PerSubjectBufferProperty where TTextView : ITextView { - private class PerSubjectBufferProperty where TTextView : ITextView + private readonly TTextView _textView; + private readonly Dictionary> _subjectBufferMap = []; + + // Some other VS components (e.g. Razor) will temporarily disconnect out ITextBuffer from the ITextView. When listening to + // BufferGraph.GraphBuffersChanged, we should allow buffers we previously knew about to be re-attached. + private readonly ConditionalWeakTable> _buffersRemovedFromTextViewBufferGraph = new(); + + public static bool GetOrCreateValue( + TTextView textView, + ITextBuffer subjectBuffer, + object key, + Func valueCreator, + out TProperty value) { - private readonly TTextView _textView; - private readonly Dictionary> _subjectBufferMap = []; - - // Some other VS components (e.g. Razor) will temporarily disconnect out ITextBuffer from the ITextView. When listening to - // BufferGraph.GraphBuffersChanged, we should allow buffers we previously knew about to be re-attached. - private readonly ConditionalWeakTable> _buffersRemovedFromTextViewBufferGraph = new(); - - public static bool GetOrCreateValue( - TTextView textView, - ITextBuffer subjectBuffer, - object key, - Func valueCreator, - out TProperty value) - { - Contract.ThrowIfTrue(textView.IsClosed); + Contract.ThrowIfTrue(textView.IsClosed); - var properties = textView.Properties.GetOrCreateSingletonProperty(() => new PerSubjectBufferProperty(textView)); - if (!properties.TryGetValue(subjectBuffer, key, out var priorValue)) - { - // Need to create it. - value = valueCreator(textView, subjectBuffer); - properties.Add(subjectBuffer, key, value); - return true; - } - - // Already there. - value = priorValue; - return false; + var properties = textView.Properties.GetOrCreateSingletonProperty(() => new PerSubjectBufferProperty(textView)); + if (!properties.TryGetValue(subjectBuffer, key, out var priorValue)) + { + // Need to create it. + value = valueCreator(textView, subjectBuffer); + properties.Add(subjectBuffer, key, value); + return true; } - public static bool TryGetValue( - TTextView textView, - ITextBuffer subjectBuffer, - object key, - [MaybeNullWhen(false)] out TProperty value) - { - Contract.ThrowIfTrue(textView.IsClosed); + // Already there. + value = priorValue; + return false; + } - var properties = textView.Properties.GetOrCreateSingletonProperty(() => new PerSubjectBufferProperty(textView)); - return properties.TryGetValue(subjectBuffer, key, out value); - } + public static bool TryGetValue( + TTextView textView, + ITextBuffer subjectBuffer, + object key, + [MaybeNullWhen(false)] out TProperty value) + { + Contract.ThrowIfTrue(textView.IsClosed); - public static void AddValue( - TTextView textView, - ITextBuffer subjectBuffer, - object key, - TProperty value) - { - Contract.ThrowIfTrue(textView.IsClosed); + var properties = textView.Properties.GetOrCreateSingletonProperty(() => new PerSubjectBufferProperty(textView)); + return properties.TryGetValue(subjectBuffer, key, out value); + } - var properties = textView.Properties.GetOrCreateSingletonProperty(() => new PerSubjectBufferProperty(textView)); - properties.Add(subjectBuffer, key, value); - } + public static void AddValue( + TTextView textView, + ITextBuffer subjectBuffer, + object key, + TProperty value) + { + Contract.ThrowIfTrue(textView.IsClosed); - public static void RemoveValue(TTextView textView, ITextBuffer subjectBuffer, object key) + var properties = textView.Properties.GetOrCreateSingletonProperty(() => new PerSubjectBufferProperty(textView)); + properties.Add(subjectBuffer, key, value); + } + + public static void RemoveValue(TTextView textView, ITextBuffer subjectBuffer, object key) + { + if (textView.Properties.TryGetProperty(typeof(PerSubjectBufferProperty), out PerSubjectBufferProperty properties)) { - if (textView.Properties.TryGetProperty(typeof(PerSubjectBufferProperty), out PerSubjectBufferProperty properties)) - { - properties.Remove(subjectBuffer, key); - } + properties.Remove(subjectBuffer, key); } + } - private PerSubjectBufferProperty(TTextView textView) - { - _textView = textView; + private PerSubjectBufferProperty(TTextView textView) + { + _textView = textView; - _textView.Closed += OnTextViewClosed; - _textView.BufferGraph.GraphBuffersChanged += OnTextViewBufferGraphChanged; - } + _textView.Closed += OnTextViewClosed; + _textView.BufferGraph.GraphBuffersChanged += OnTextViewBufferGraphChanged; + } - private void OnTextViewClosed(object? sender, EventArgs e) - { - _textView.Closed -= OnTextViewClosed; - _textView.BufferGraph.GraphBuffersChanged -= OnTextViewBufferGraphChanged; + private void OnTextViewClosed(object? sender, EventArgs e) + { + _textView.Closed -= OnTextViewClosed; + _textView.BufferGraph.GraphBuffersChanged -= OnTextViewBufferGraphChanged; - _subjectBufferMap.Clear(); - _textView.Properties.RemoveProperty(typeof(PerSubjectBufferProperty)); - } + _subjectBufferMap.Clear(); + _textView.Properties.RemoveProperty(typeof(PerSubjectBufferProperty)); + } - private void OnTextViewBufferGraphChanged(object? sender, GraphBuffersChangedEventArgs e) + private void OnTextViewBufferGraphChanged(object? sender, GraphBuffersChangedEventArgs e) + { + foreach (var buffer in e.RemovedBuffers) { - foreach (var buffer in e.RemovedBuffers) + if (_subjectBufferMap.TryGetValue(buffer, out var value)) { - if (_subjectBufferMap.TryGetValue(buffer, out var value)) - { - _subjectBufferMap.Remove(buffer); - _buffersRemovedFromTextViewBufferGraph.Add(buffer, value); - } - } - - foreach (var buffer in e.AddedBuffers) - { - if (_buffersRemovedFromTextViewBufferGraph.TryGetValue(buffer, out var value)) - { - _subjectBufferMap[buffer] = value; - _buffersRemovedFromTextViewBufferGraph.Remove(buffer); - } + _subjectBufferMap.Remove(buffer); + _buffersRemovedFromTextViewBufferGraph.Add(buffer, value); } } - public bool TryGetValue(ITextBuffer subjectBuffer, object key, [MaybeNullWhen(false)] out TProperty value) + foreach (var buffer in e.AddedBuffers) { - if (_subjectBufferMap.TryGetValue(subjectBuffer, out var bufferMap)) - { - return bufferMap.TryGetValue(key, out value); - } - - if (_buffersRemovedFromTextViewBufferGraph.TryGetValue(subjectBuffer, out bufferMap)) + if (_buffersRemovedFromTextViewBufferGraph.TryGetValue(buffer, out var value)) { - return bufferMap.TryGetValue(key, out value); + _subjectBufferMap[buffer] = value; + _buffersRemovedFromTextViewBufferGraph.Remove(buffer); } + } + } - value = default; - return false; + public bool TryGetValue(ITextBuffer subjectBuffer, object key, [MaybeNullWhen(false)] out TProperty value) + { + if (_subjectBufferMap.TryGetValue(subjectBuffer, out var bufferMap)) + { + return bufferMap.TryGetValue(key, out value); } - public void Add(ITextBuffer subjectBuffer, object key, TProperty value) + if (_buffersRemovedFromTextViewBufferGraph.TryGetValue(subjectBuffer, out bufferMap)) { - var bufferMap = _subjectBufferMap.GetOrAdd(subjectBuffer, _ => []); - bufferMap[key] = value; + return bufferMap.TryGetValue(key, out value); } - public void Remove(ITextBuffer subjectBuffer, object key) + value = default; + return false; + } + + public void Add(ITextBuffer subjectBuffer, object key, TProperty value) + { + var bufferMap = _subjectBufferMap.GetOrAdd(subjectBuffer, _ => []); + bufferMap[key] = value; + } + + public void Remove(ITextBuffer subjectBuffer, object key) + { + if (_subjectBufferMap.TryGetValue(subjectBuffer, out var bufferMap)) { - if (_subjectBufferMap.TryGetValue(subjectBuffer, out var bufferMap)) + bufferMap.Remove(key); + if (!bufferMap.Any()) { - bufferMap.Remove(key); - if (!bufferMap.Any()) - { - _subjectBufferMap.Remove(subjectBuffer); - } + _subjectBufferMap.Remove(subjectBuffer); } + } - if (_buffersRemovedFromTextViewBufferGraph.TryGetValue(subjectBuffer, out bufferMap)) + if (_buffersRemovedFromTextViewBufferGraph.TryGetValue(subjectBuffer, out bufferMap)) + { + bufferMap.Remove(key); + if (!bufferMap.Any()) { - bufferMap.Remove(key); - if (!bufferMap.Any()) - { - _buffersRemovedFromTextViewBufferGraph.Remove(subjectBuffer); - } + _buffersRemovedFromTextViewBufferGraph.Remove(subjectBuffer); } } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.cs index 5ff5954c1721a..35fb0108f6dcb 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.cs @@ -13,392 +13,391 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static partial class ITextViewExtensions { - internal static partial class ITextViewExtensions + /// + /// Collects the content types in the view's buffer graph. + /// + public static ISet GetContentTypes(this ITextView textView) { - /// - /// Collects the content types in the view's buffer graph. - /// - public static ISet GetContentTypes(this ITextView textView) - { - return new HashSet( - textView.BufferGraph.GetTextBuffers(_ => true).Select(b => b.ContentType)); - } + return new HashSet( + textView.BufferGraph.GetTextBuffers(_ => true).Select(b => b.ContentType)); + } - public static bool IsReadOnlyOnSurfaceBuffer(this ITextView textView, SnapshotSpan span) + public static bool IsReadOnlyOnSurfaceBuffer(this ITextView textView, SnapshotSpan span) + { + var spansInView = textView.BufferGraph.MapUpToBuffer(span, SpanTrackingMode.EdgeInclusive, textView.TextBuffer); + return spansInView.Any(spanInView => textView.TextBuffer.IsReadOnly(spanInView.Span)); + } + + public static SnapshotPoint? GetCaretPoint(this ITextView textView, ITextBuffer subjectBuffer) + { + var caret = textView.Caret.Position; + return textView.BufferGraph.MapUpOrDownToBuffer(caret.BufferPosition, subjectBuffer); + } + + public static SnapshotPoint? GetCaretPoint(this ITextView textView, Predicate match) + { + var caret = textView.Caret.Position; + var span = textView.BufferGraph.MapUpOrDownToFirstMatch(new SnapshotSpan(caret.BufferPosition, 0), match); + if (span.HasValue) { - var spansInView = textView.BufferGraph.MapUpToBuffer(span, SpanTrackingMode.EdgeInclusive, textView.TextBuffer); - return spansInView.Any(spanInView => textView.TextBuffer.IsReadOnly(spanInView.Span)); + return span.Value.Start; } - - public static SnapshotPoint? GetCaretPoint(this ITextView textView, ITextBuffer subjectBuffer) + else { - var caret = textView.Caret.Position; - return textView.BufferGraph.MapUpOrDownToBuffer(caret.BufferPosition, subjectBuffer); + return null; } + } - public static SnapshotPoint? GetCaretPoint(this ITextView textView, Predicate match) + public static VirtualSnapshotPoint? GetVirtualCaretPoint(this ITextView textView, ITextBuffer subjectBuffer) + { + if (subjectBuffer == textView.TextBuffer) { - var caret = textView.Caret.Position; - var span = textView.BufferGraph.MapUpOrDownToFirstMatch(new SnapshotSpan(caret.BufferPosition, 0), match); - if (span.HasValue) - { - return span.Value.Start; - } - else - { - return null; - } + return textView.Caret.Position.VirtualBufferPosition; } - public static VirtualSnapshotPoint? GetVirtualCaretPoint(this ITextView textView, ITextBuffer subjectBuffer) - { - if (subjectBuffer == textView.TextBuffer) - { - return textView.Caret.Position.VirtualBufferPosition; - } + var mappedPoint = textView.BufferGraph.MapDownToBuffer( + textView.Caret.Position.VirtualBufferPosition.Position, + PointTrackingMode.Negative, + subjectBuffer, + PositionAffinity.Predecessor); - var mappedPoint = textView.BufferGraph.MapDownToBuffer( - textView.Caret.Position.VirtualBufferPosition.Position, - PointTrackingMode.Negative, - subjectBuffer, - PositionAffinity.Predecessor); + return mappedPoint.HasValue + ? new VirtualSnapshotPoint(mappedPoint.Value) + : default; + } - return mappedPoint.HasValue - ? new VirtualSnapshotPoint(mappedPoint.Value) - : default; - } + public static ITextBuffer? GetBufferContainingCaret(this ITextView textView, string contentType = ContentTypeNames.RoslynContentType) + { + var point = GetCaretPoint(textView, s => s.ContentType.IsOfType(contentType)); + return point.HasValue ? point.Value.Snapshot.TextBuffer : null; + } - public static ITextBuffer? GetBufferContainingCaret(this ITextView textView, string contentType = ContentTypeNames.RoslynContentType) - { - var point = GetCaretPoint(textView, s => s.ContentType.IsOfType(contentType)); - return point.HasValue ? point.Value.Snapshot.TextBuffer : null; - } + public static SnapshotPoint? GetPositionInView(this ITextView textView, SnapshotPoint point) + => textView.BufferGraph.MapUpToSnapshot(point, PointTrackingMode.Positive, PositionAffinity.Successor, textView.TextSnapshot); + + public static NormalizedSnapshotSpanCollection GetSpanInView(this ITextView textView, SnapshotSpan span) + => textView.BufferGraph.MapUpToSnapshot(span, SpanTrackingMode.EdgeInclusive, textView.TextSnapshot); + + public static void SetSelection( + this ITextView textView, VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint) + { + var isReversed = activePoint < anchorPoint; + var start = isReversed ? activePoint : anchorPoint; + var end = isReversed ? anchorPoint : activePoint; + SetSelection(textView, new SnapshotSpan(start.Position, end.Position), isReversed); + } + + public static void SetSelection( + this ITextView textView, SnapshotSpan span, bool isReversed = false) + { + var spanInView = textView.GetSpanInView(span).Single(); + textView.Selection.Select(spanInView, isReversed); + textView.Caret.MoveTo(isReversed ? spanInView.Start : spanInView.End); + } - public static SnapshotPoint? GetPositionInView(this ITextView textView, SnapshotPoint point) - => textView.BufferGraph.MapUpToSnapshot(point, PointTrackingMode.Positive, PositionAffinity.Successor, textView.TextSnapshot); + /// + /// Sets a multi selection with the last span as the primary selection. + /// Also maps up to the correct span in view before attempting to set the selection. + /// + public static void SetMultiSelection(this ITextView textView, IEnumerable spans) + { + var spansInView = spans.Select(s => new Selection(textView.GetSpanInView(s).Single())); + textView.GetMultiSelectionBroker().SetSelectionRange(spansInView, spansInView.Last()); + } - public static NormalizedSnapshotSpanCollection GetSpanInView(this ITextView textView, SnapshotSpan span) - => textView.BufferGraph.MapUpToSnapshot(span, SpanTrackingMode.EdgeInclusive, textView.TextSnapshot); + public static bool TryMoveCaretToAndEnsureVisible(this ITextView textView, SnapshotPoint point, IOutliningManagerService? outliningManagerService = null, EnsureSpanVisibleOptions ensureSpanVisibleOptions = EnsureSpanVisibleOptions.None) + => textView.TryMoveCaretToAndEnsureVisible(new VirtualSnapshotPoint(point), outliningManagerService, ensureSpanVisibleOptions); - public static void SetSelection( - this ITextView textView, VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint) + public static bool TryMoveCaretToAndEnsureVisible(this ITextView textView, VirtualSnapshotPoint point, IOutliningManagerService? outliningManagerService = null, EnsureSpanVisibleOptions ensureSpanVisibleOptions = EnsureSpanVisibleOptions.None) + { + if (textView.IsClosed) { - var isReversed = activePoint < anchorPoint; - var start = isReversed ? activePoint : anchorPoint; - var end = isReversed ? anchorPoint : activePoint; - SetSelection(textView, new SnapshotSpan(start.Position, end.Position), isReversed); + return false; } - public static void SetSelection( - this ITextView textView, SnapshotSpan span, bool isReversed = false) + var pointInView = textView.GetPositionInView(point.Position); + + if (!pointInView.HasValue) { - var spanInView = textView.GetSpanInView(span).Single(); - textView.Selection.Select(spanInView, isReversed); - textView.Caret.MoveTo(isReversed ? spanInView.Start : spanInView.End); + return false; } - /// - /// Sets a multi selection with the last span as the primary selection. - /// Also maps up to the correct span in view before attempting to set the selection. - /// - public static void SetMultiSelection(this ITextView textView, IEnumerable spans) + // If we were given an outlining service, we need to expand any outlines first, or else + // the Caret.MoveTo won't land in the correct location if our target is inside a + // collapsed outline. + if (outliningManagerService != null) { - var spansInView = spans.Select(s => new Selection(textView.GetSpanInView(s).Single())); - textView.GetMultiSelectionBroker().SetSelectionRange(spansInView, spansInView.Last()); + var outliningManager = outliningManagerService.GetOutliningManager(textView); + + outliningManager?.ExpandAll(new SnapshotSpan(pointInView.Value, length: 0), match: _ => true); } - public static bool TryMoveCaretToAndEnsureVisible(this ITextView textView, SnapshotPoint point, IOutliningManagerService? outliningManagerService = null, EnsureSpanVisibleOptions ensureSpanVisibleOptions = EnsureSpanVisibleOptions.None) - => textView.TryMoveCaretToAndEnsureVisible(new VirtualSnapshotPoint(point), outliningManagerService, ensureSpanVisibleOptions); + var newPosition = textView.Caret.MoveTo(new VirtualSnapshotPoint(pointInView.Value, point.VirtualSpaces)); - public static bool TryMoveCaretToAndEnsureVisible(this ITextView textView, VirtualSnapshotPoint point, IOutliningManagerService? outliningManagerService = null, EnsureSpanVisibleOptions ensureSpanVisibleOptions = EnsureSpanVisibleOptions.None) - { - if (textView.IsClosed) - { - return false; - } + // We use the caret's position in the view's current snapshot here in case something + // changed text in response to a caret move (e.g. line commit) + var spanInView = new SnapshotSpan(newPosition.BufferPosition, 0); + textView.ViewScroller.EnsureSpanVisible(spanInView, ensureSpanVisibleOptions); - var pointInView = textView.GetPositionInView(point.Position); + return true; + } - if (!pointInView.HasValue) - { - return false; - } + /// + /// Gets or creates a view property that would go away when view gets closed + /// + public static TProperty GetOrCreateAutoClosingProperty( + this TTextView textView, + Func valueCreator) where TTextView : ITextView + { + return textView.GetOrCreateAutoClosingProperty(typeof(TProperty), valueCreator); + } - // If we were given an outlining service, we need to expand any outlines first, or else - // the Caret.MoveTo won't land in the correct location if our target is inside a - // collapsed outline. - if (outliningManagerService != null) - { - var outliningManager = outliningManagerService.GetOutliningManager(textView); + /// + /// Gets or creates a view property that would go away when view gets closed + /// + public static TProperty GetOrCreateAutoClosingProperty( + this TTextView textView, + object key, + Func valueCreator) where TTextView : ITextView + { + GetOrCreateAutoClosingProperty(textView, key, valueCreator, out var value); + return value; + } - outliningManager?.ExpandAll(new SnapshotSpan(pointInView.Value, length: 0), match: _ => true); - } + /// + /// Gets or creates a view property that would go away when view gets closed + /// + public static bool GetOrCreateAutoClosingProperty( + this TTextView textView, + object key, + Func valueCreator, + out TProperty value) where TTextView : ITextView + { + return AutoClosingViewProperty.GetOrCreateValue(textView, key, valueCreator, out value); + } - var newPosition = textView.Caret.MoveTo(new VirtualSnapshotPoint(pointInView.Value, point.VirtualSpaces)); + /// + /// Gets or creates a per subject buffer property. + /// + public static TProperty GetOrCreatePerSubjectBufferProperty( + this TTextView textView, + ITextBuffer subjectBuffer, + object key, + Func valueCreator) where TTextView : class, ITextView + { + GetOrCreatePerSubjectBufferProperty(textView, subjectBuffer, key, valueCreator, out var value); - // We use the caret's position in the view's current snapshot here in case something - // changed text in response to a caret move (e.g. line commit) - var spanInView = new SnapshotSpan(newPosition.BufferPosition, 0); - textView.ViewScroller.EnsureSpanVisible(spanInView, ensureSpanVisibleOptions); + return value; + } - return true; - } + /// + /// Gets or creates a per subject buffer property, returning true if it needed to create it. + /// + public static bool GetOrCreatePerSubjectBufferProperty( + this TTextView textView, + ITextBuffer subjectBuffer, + object key, + Func valueCreator, + out TProperty value) where TTextView : class, ITextView + { + Contract.ThrowIfNull(textView); + Contract.ThrowIfNull(subjectBuffer); + Contract.ThrowIfNull(valueCreator); - /// - /// Gets or creates a view property that would go away when view gets closed - /// - public static TProperty GetOrCreateAutoClosingProperty( - this TTextView textView, - Func valueCreator) where TTextView : ITextView - { - return textView.GetOrCreateAutoClosingProperty(typeof(TProperty), valueCreator); - } + return PerSubjectBufferProperty.GetOrCreateValue(textView, subjectBuffer, key, valueCreator, out value); + } - /// - /// Gets or creates a view property that would go away when view gets closed - /// - public static TProperty GetOrCreateAutoClosingProperty( - this TTextView textView, - object key, - Func valueCreator) where TTextView : ITextView - { - GetOrCreateAutoClosingProperty(textView, key, valueCreator, out var value); - return value; - } + public static bool TryGetPerSubjectBufferProperty( + this TTextView textView, + ITextBuffer subjectBuffer, + object key, + [MaybeNullWhen(false)] out TProperty value) where TTextView : class, ITextView + { + Contract.ThrowIfNull(textView); + Contract.ThrowIfNull(subjectBuffer); - /// - /// Gets or creates a view property that would go away when view gets closed - /// - public static bool GetOrCreateAutoClosingProperty( - this TTextView textView, - object key, - Func valueCreator, - out TProperty value) where TTextView : ITextView - { - return AutoClosingViewProperty.GetOrCreateValue(textView, key, valueCreator, out value); - } + return PerSubjectBufferProperty.TryGetValue(textView, subjectBuffer, key, out value); + } - /// - /// Gets or creates a per subject buffer property. - /// - public static TProperty GetOrCreatePerSubjectBufferProperty( - this TTextView textView, - ITextBuffer subjectBuffer, - object key, - Func valueCreator) where TTextView : class, ITextView - { - GetOrCreatePerSubjectBufferProperty(textView, subjectBuffer, key, valueCreator, out var value); + public static void AddPerSubjectBufferProperty( + this TTextView textView, + ITextBuffer subjectBuffer, + object key, + TProperty value) where TTextView : class, ITextView + { + Contract.ThrowIfNull(textView); + Contract.ThrowIfNull(subjectBuffer); - return value; - } + PerSubjectBufferProperty.AddValue(textView, subjectBuffer, key, value); + } - /// - /// Gets or creates a per subject buffer property, returning true if it needed to create it. - /// - public static bool GetOrCreatePerSubjectBufferProperty( - this TTextView textView, - ITextBuffer subjectBuffer, - object key, - Func valueCreator, - out TProperty value) where TTextView : class, ITextView - { - Contract.ThrowIfNull(textView); - Contract.ThrowIfNull(subjectBuffer); - Contract.ThrowIfNull(valueCreator); + public static void RemovePerSubjectBufferProperty( + this TTextView textView, + ITextBuffer subjectBuffer, + object key) where TTextView : class, ITextView + { + Contract.ThrowIfNull(textView); + Contract.ThrowIfNull(subjectBuffer); - return PerSubjectBufferProperty.GetOrCreateValue(textView, subjectBuffer, key, valueCreator, out value); - } + PerSubjectBufferProperty.RemoveValue(textView, subjectBuffer, key); + } - public static bool TryGetPerSubjectBufferProperty( - this TTextView textView, - ITextBuffer subjectBuffer, - object key, - [MaybeNullWhen(false)] out TProperty value) where TTextView : class, ITextView + public static bool TypeCharWasHandledStrangely( + this ITextView textView, + ITextBuffer subjectBuffer, + char ch) + { + var finalCaretPositionOpt = textView.GetCaretPoint(subjectBuffer); + if (finalCaretPositionOpt == null) { - Contract.ThrowIfNull(textView); - Contract.ThrowIfNull(subjectBuffer); - - return PerSubjectBufferProperty.TryGetValue(textView, subjectBuffer, key, out value); + // Caret moved outside of our buffer. Don't want to handle this typed character. + return true; } - public static void AddPerSubjectBufferProperty( - this TTextView textView, - ITextBuffer subjectBuffer, - object key, - TProperty value) where TTextView : class, ITextView + var previousPosition = finalCaretPositionOpt.Value.Position - 1; + var inRange = previousPosition >= 0 && previousPosition < subjectBuffer.CurrentSnapshot.Length; + if (!inRange) { - Contract.ThrowIfNull(textView); - Contract.ThrowIfNull(subjectBuffer); - - PerSubjectBufferProperty.AddValue(textView, subjectBuffer, key, value); + // The character before the caret isn't even in the buffer we care about. Don't + // handle this. + return true; } - public static void RemovePerSubjectBufferProperty( - this TTextView textView, - ITextBuffer subjectBuffer, - object key) where TTextView : class, ITextView + if (subjectBuffer.CurrentSnapshot[previousPosition] != ch) { - Contract.ThrowIfNull(textView); - Contract.ThrowIfNull(subjectBuffer); - - PerSubjectBufferProperty.RemoveValue(textView, subjectBuffer, key); + // The character that was typed is not in the buffer at the typed location. Don't + // handle this character. + return true; } - public static bool TypeCharWasHandledStrangely( - this ITextView textView, - ITextBuffer subjectBuffer, - char ch) - { - var finalCaretPositionOpt = textView.GetCaretPoint(subjectBuffer); - if (finalCaretPositionOpt == null) - { - // Caret moved outside of our buffer. Don't want to handle this typed character. - return true; - } - - var previousPosition = finalCaretPositionOpt.Value.Position - 1; - var inRange = previousPosition >= 0 && previousPosition < subjectBuffer.CurrentSnapshot.Length; - if (!inRange) - { - // The character before the caret isn't even in the buffer we care about. Don't - // handle this. - return true; - } - - if (subjectBuffer.CurrentSnapshot[previousPosition] != ch) - { - // The character that was typed is not in the buffer at the typed location. Don't - // handle this character. - return true; - } + return false; + } - return false; - } + public static int? GetDesiredIndentation(this ITextView textView, ISmartIndentationService smartIndentService, ITextSnapshotLine line) + { + var pointInView = textView.BufferGraph.MapUpToSnapshot( + line.Start, PointTrackingMode.Positive, PositionAffinity.Successor, textView.TextSnapshot); - public static int? GetDesiredIndentation(this ITextView textView, ISmartIndentationService smartIndentService, ITextSnapshotLine line) + if (!pointInView.HasValue) { - var pointInView = textView.BufferGraph.MapUpToSnapshot( - line.Start, PointTrackingMode.Positive, PositionAffinity.Successor, textView.TextSnapshot); + return null; + } - if (!pointInView.HasValue) - { - return null; - } + var lineInView = textView.TextSnapshot.GetLineFromPosition(pointInView.Value.Position); + return smartIndentService.GetDesiredIndentation(textView, lineInView); + } - var lineInView = textView.TextSnapshot.GetLineFromPosition(pointInView.Value.Position); - return smartIndentService.GetDesiredIndentation(textView, lineInView); + public static bool TryGetSurfaceBufferSpan( + this ITextView textView, + VirtualSnapshotSpan virtualSnapshotSpan, + out VirtualSnapshotSpan surfaceBufferSpan) + { + // If we are already on the surface buffer, then there's no reason to attempt mappings + // as we'll lose virtualness + if (virtualSnapshotSpan.Snapshot.TextBuffer == textView.TextBuffer) + { + surfaceBufferSpan = virtualSnapshotSpan; + return true; } - public static bool TryGetSurfaceBufferSpan( - this ITextView textView, - VirtualSnapshotSpan virtualSnapshotSpan, - out VirtualSnapshotSpan surfaceBufferSpan) - { - // If we are already on the surface buffer, then there's no reason to attempt mappings - // as we'll lose virtualness - if (virtualSnapshotSpan.Snapshot.TextBuffer == textView.TextBuffer) - { - surfaceBufferSpan = virtualSnapshotSpan; - return true; - } + // We have to map. We'll lose virtualness in this process because + // mapping virtual points through projections is poorly defined. + var targetSpan = textView.BufferGraph.MapUpToSnapshot( + virtualSnapshotSpan.SnapshotSpan, + SpanTrackingMode.EdgeExclusive, + textView.TextSnapshot).FirstOrNull(); - // We have to map. We'll lose virtualness in this process because - // mapping virtual points through projections is poorly defined. - var targetSpan = textView.BufferGraph.MapUpToSnapshot( - virtualSnapshotSpan.SnapshotSpan, - SpanTrackingMode.EdgeExclusive, - textView.TextSnapshot).FirstOrNull(); + if (targetSpan.HasValue) + { + surfaceBufferSpan = new VirtualSnapshotSpan(targetSpan.Value); + return true; + } - if (targetSpan.HasValue) - { - surfaceBufferSpan = new VirtualSnapshotSpan(targetSpan.Value); - return true; - } + surfaceBufferSpan = default; + return false; + } - surfaceBufferSpan = default; - return false; + /// + /// Returns the span of the lines in subjectBuffer that is currently visible in the provided + /// view. "extraLines" can be provided to get a span that encompasses some number of lines + /// before and after the actual visible lines. + /// + public static SnapshotSpan? GetVisibleLinesSpan(this ITextView textView, ITextBuffer subjectBuffer, int extraLines = 0) + { + // No point in continuing if the text view has been closed. + if (textView.IsClosed) + { + return null; } - /// - /// Returns the span of the lines in subjectBuffer that is currently visible in the provided - /// view. "extraLines" can be provided to get a span that encompasses some number of lines - /// before and after the actual visible lines. - /// - public static SnapshotSpan? GetVisibleLinesSpan(this ITextView textView, ITextBuffer subjectBuffer, int extraLines = 0) + // If we're being called while the textview is actually in the middle of a layout, then + // we can't proceed. Much of the text view state is unsafe to access (and will throw). + if (textView.InLayout) { - // No point in continuing if the text view has been closed. - if (textView.IsClosed) - { - return null; - } - - // If we're being called while the textview is actually in the middle of a layout, then - // we can't proceed. Much of the text view state is unsafe to access (and will throw). - if (textView.InLayout) - { - return null; - } + return null; + } - // During text view initialization the TextViewLines may be null. In that case we can't - // get an appropriate visisble span. - if (textView.TextViewLines == null) - { - return null; - } + // During text view initialization the TextViewLines may be null. In that case we can't + // get an appropriate visisble span. + if (textView.TextViewLines == null) + { + return null; + } - // Determine the range of text that is visible in the view. Then map this down to the - // bufffer passed in. From that, determine the start/end line for the buffer that is in - // view. - var visibleSpan = textView.TextViewLines.FormattedSpan; - var visibleSpansInBuffer = textView.BufferGraph.MapDownToBuffer(visibleSpan, SpanTrackingMode.EdgeInclusive, subjectBuffer); - if (visibleSpansInBuffer.Count == 0) - { - return null; - } + // Determine the range of text that is visible in the view. Then map this down to the + // bufffer passed in. From that, determine the start/end line for the buffer that is in + // view. + var visibleSpan = textView.TextViewLines.FormattedSpan; + var visibleSpansInBuffer = textView.BufferGraph.MapDownToBuffer(visibleSpan, SpanTrackingMode.EdgeInclusive, subjectBuffer); + if (visibleSpansInBuffer.Count == 0) + { + return null; + } - var visibleStart = visibleSpansInBuffer.First().Start; - var visibleEnd = visibleSpansInBuffer.Last().End; + var visibleStart = visibleSpansInBuffer.First().Start; + var visibleEnd = visibleSpansInBuffer.Last().End; - var snapshot = subjectBuffer.CurrentSnapshot; - var startLine = visibleStart.GetContainingLineNumber(); - var endLine = visibleEnd.GetContainingLineNumber(); + var snapshot = subjectBuffer.CurrentSnapshot; + var startLine = visibleStart.GetContainingLineNumber(); + var endLine = visibleEnd.GetContainingLineNumber(); - startLine = Math.Max(startLine - extraLines, 0); - endLine = Math.Min(endLine + extraLines, snapshot.LineCount - 1); + startLine = Math.Max(startLine - extraLines, 0); + endLine = Math.Min(endLine + extraLines, snapshot.LineCount - 1); - var start = snapshot.GetLineFromLineNumber(startLine).Start; - var end = snapshot.GetLineFromLineNumber(endLine).EndIncludingLineBreak; + var start = snapshot.GetLineFromLineNumber(startLine).Start; + var end = snapshot.GetLineFromLineNumber(endLine).EndIncludingLineBreak; - var span = new SnapshotSpan(snapshot, Span.FromBounds(start, end)); + var span = new SnapshotSpan(snapshot, Span.FromBounds(start, end)); - return span; - } + return span; + } - /// - /// Determines if the textbuffer passed in matches the buffer for the textview. - /// - public static bool IsNotSurfaceBufferOfTextView(this ITextView textView, ITextBuffer textBuffer) - => textBuffer != textView.TextBuffer; + /// + /// Determines if the textbuffer passed in matches the buffer for the textview. + /// + public static bool IsNotSurfaceBufferOfTextView(this ITextView textView, ITextBuffer textBuffer) + => textBuffer != textView.TextBuffer; - internal static bool IsInLspEditorContext(this ITextView textView) + internal static bool IsInLspEditorContext(this ITextView textView) + { + // If any of the buffers in the projection graph are in the LSP editor context, then we consider this to be in an LSP context. + // We cannot be in a partial context where some buffers are LSP and some are not. + var anyBufferInLspContext = false; + _ = textView.BufferGraph.GetTextBuffers(textBuffer => { - // If any of the buffers in the projection graph are in the LSP editor context, then we consider this to be in an LSP context. - // We cannot be in a partial context where some buffers are LSP and some are not. - var anyBufferInLspContext = false; - _ = textView.BufferGraph.GetTextBuffers(textBuffer => + // Just set a flag if we found one to avoid creating a collection of all the buffers + if (textBuffer.IsInLspEditorContext()) { - // Just set a flag if we found one to avoid creating a collection of all the buffers - if (textBuffer.IsInLspEditorContext()) - { - anyBufferInLspContext = true; - } + anyBufferInLspContext = true; + } - return false; - }); + return false; + }); - return anyBufferInLspContext; - } + return anyBufferInLspContext; } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/IThreadingContextExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/IThreadingContextExtensions.cs index 4496213ef62d3..ece4af98e0f52 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/IThreadingContextExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/IThreadingContextExtensions.cs @@ -5,14 +5,13 @@ using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class IThreadingContextExtensions { - internal static class IThreadingContextExtensions - { - public static void ThrowIfNotOnUIThread(this IThreadingContext threadingContext) - => Contract.ThrowIfFalse(threadingContext.JoinableTaskContext.IsOnMainThread); + public static void ThrowIfNotOnUIThread(this IThreadingContext threadingContext) + => Contract.ThrowIfFalse(threadingContext.JoinableTaskContext.IsOnMainThread); - public static void ThrowIfNotOnBackgroundThread(this IThreadingContext threadingContext) - => Contract.ThrowIfTrue(threadingContext.JoinableTaskContext.IsOnMainThread); - } + public static void ThrowIfNotOnBackgroundThread(this IThreadingContext threadingContext) + => Contract.ThrowIfTrue(threadingContext.JoinableTaskContext.IsOnMainThread); } diff --git a/src/EditorFeatures/Core/Shared/Extensions/ITrackingSpanExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/ITrackingSpanExtensions.cs index 418f3fe54b06f..97d12b0307d60 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/ITrackingSpanExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/ITrackingSpanExtensions.cs @@ -4,11 +4,10 @@ using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class ITrackingSpanExtensions { - internal static class ITrackingSpanExtensions - { - public static ITrackingPoint GetStartTrackingPoint(this ITrackingSpan span, PointTrackingMode mode) - => span.GetStartPoint(span.TextBuffer.CurrentSnapshot).CreateTrackingPoint(mode); - } + public static ITrackingPoint GetStartTrackingPoint(this ITrackingSpan span, PointTrackingMode mode) + => span.GetStartPoint(span.TextBuffer.CurrentSnapshot).CreateTrackingPoint(mode); } diff --git a/src/EditorFeatures/Core/Shared/Extensions/MefExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/MefExtensions.cs index 7031bc4425ebc..3847d66bb4136 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/MefExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/MefExtensions.cs @@ -7,60 +7,59 @@ using System.Linq; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +/// +/// Helper class to perform ContentType best-match against a set of extensions. This could +/// become a public service. +/// +internal static class MefExtensions { /// - /// Helper class to perform ContentType best-match against a set of extensions. This could - /// become a public service. + /// Given a list of extensions that provide content types, filter the list and return that + /// subset which matches the given content type /// - internal static class MefExtensions + public static IList> SelectMatchingExtensions( + this IEnumerable> extensions, + params IContentType[] contentTypes) + where TMetadata : IContentTypeMetadata { - /// - /// Given a list of extensions that provide content types, filter the list and return that - /// subset which matches the given content type - /// - public static IList> SelectMatchingExtensions( - this IEnumerable> extensions, - params IContentType[] contentTypes) - where TMetadata : IContentTypeMetadata - { - return extensions.SelectMatchingExtensions((IEnumerable)contentTypes); - } + return extensions.SelectMatchingExtensions((IEnumerable)contentTypes); + } - /// - /// Given a list of extensions that provide content types, filter the list and return that - /// subset which matches any of the given content types. - /// - public static IList> SelectMatchingExtensions( - this IEnumerable> extensions, - IEnumerable contentTypes) - where TMetadata : IContentTypeMetadata - { - return extensions.Where(h => contentTypes.Any(d => d.MatchesAny(h.Metadata.ContentTypes))).ToList(); - } + /// + /// Given a list of extensions that provide content types, filter the list and return that + /// subset which matches any of the given content types. + /// + public static IList> SelectMatchingExtensions( + this IEnumerable> extensions, + IEnumerable contentTypes) + where TMetadata : IContentTypeMetadata + { + return extensions.Where(h => contentTypes.Any(d => d.MatchesAny(h.Metadata.ContentTypes))).ToList(); + } - public static IList SelectMatchingExtensionValues( - this IEnumerable> extensions, - params IContentType[] contentTypes) - where TMetadata : IContentTypeMetadata - { - return extensions.SelectMatchingExtensions(contentTypes).Select(p => p.Value).ToList(); - } + public static IList SelectMatchingExtensionValues( + this IEnumerable> extensions, + params IContentType[] contentTypes) + where TMetadata : IContentTypeMetadata + { + return extensions.SelectMatchingExtensions(contentTypes).Select(p => p.Value).ToList(); + } - public static Lazy SelectMatchingExtension( - this IEnumerable> extensions, - params IContentType[] contentTypes) - where TMetadata : IContentTypeMetadata - { - return extensions.SelectMatchingExtensions(contentTypes).Single(); - } + public static Lazy SelectMatchingExtension( + this IEnumerable> extensions, + params IContentType[] contentTypes) + where TMetadata : IContentTypeMetadata + { + return extensions.SelectMatchingExtensions(contentTypes).Single(); + } - public static TExtension SelectMatchingExtensionValue( - this IEnumerable> extensions, - params IContentType[] contentTypes) - where TMetadata : IContentTypeMetadata - { - return extensions.SelectMatchingExtension(contentTypes).Value; - } + public static TExtension SelectMatchingExtensionValue( + this IEnumerable> extensions, + params IContentType[] contentTypes) + where TMetadata : IContentTypeMetadata + { + return extensions.SelectMatchingExtension(contentTypes).Value; } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/SmartIndentExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/SmartIndentExtensions.cs index 721f358d0e23a..df9c306f5495f 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/SmartIndentExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/SmartIndentExtensions.cs @@ -6,22 +6,21 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class IndentationResultExtensions { - internal static class IndentationResultExtensions + public static int GetIndentation(this Indentation.IndentationResult result, ITextView textView, ITextSnapshotLine lineToBeIndented) { - public static int GetIndentation(this Indentation.IndentationResult result, ITextView textView, ITextSnapshotLine lineToBeIndented) + var position = new SnapshotPoint(lineToBeIndented.Snapshot, result.BasePosition); + var pointInSurfaceSnapshot = textView.BufferGraph.MapUpToSnapshot(position, PointTrackingMode.Positive, PositionAffinity.Successor, textView.TextSnapshot); + if (!pointInSurfaceSnapshot.HasValue) { - var position = new SnapshotPoint(lineToBeIndented.Snapshot, result.BasePosition); - var pointInSurfaceSnapshot = textView.BufferGraph.MapUpToSnapshot(position, PointTrackingMode.Positive, PositionAffinity.Successor, textView.TextSnapshot); - if (!pointInSurfaceSnapshot.HasValue) - { - return position.GetContainingLine().GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(textView.Options); - } - - var lineInSurfaceSnapshot = pointInSurfaceSnapshot.Value.Snapshot.GetLineFromPosition(pointInSurfaceSnapshot.Value.Position); - var offsetInLine = pointInSurfaceSnapshot.Value.Position - lineInSurfaceSnapshot.Start.Position; - return lineInSurfaceSnapshot.GetColumnFromLineOffset(offsetInLine, textView.Options) + result.Offset; + return position.GetContainingLine().GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(textView.Options); } + + var lineInSurfaceSnapshot = pointInSurfaceSnapshot.Value.Snapshot.GetLineFromPosition(pointInSurfaceSnapshot.Value.Position); + var offsetInLine = pointInSurfaceSnapshot.Value.Position - lineInSurfaceSnapshot.Start.Position; + return lineInSurfaceSnapshot.GetColumnFromLineOffset(offsetInLine, textView.Options) + result.Offset; } } diff --git a/src/EditorFeatures/Core/Shared/Extensions/SnapshotPointExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/SnapshotPointExtensions.cs index e6a95c92b0510..5874e05c9289c 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/SnapshotPointExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/SnapshotPointExtensions.cs @@ -5,17 +5,16 @@ using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class SnapshotPointExtensions { - internal static class SnapshotPointExtensions - { - public static void GetLineAndCharacter(this SnapshotPoint point, out int lineNumber, out int characterIndex) - => point.Snapshot.GetLineAndCharacter(point.Position, out lineNumber, out characterIndex); + public static void GetLineAndCharacter(this SnapshotPoint point, out int lineNumber, out int characterIndex) + => point.Snapshot.GetLineAndCharacter(point.Position, out lineNumber, out characterIndex); - public static int GetContainingLineNumber(this SnapshotPoint point) - => point.GetContainingLineNumber(); + public static int GetContainingLineNumber(this SnapshotPoint point) + => point.GetContainingLineNumber(); - public static ITrackingPoint CreateTrackingPoint(this SnapshotPoint point, PointTrackingMode trackingMode) - => point.Snapshot.CreateTrackingPoint(point, trackingMode); - } + public static ITrackingPoint CreateTrackingPoint(this SnapshotPoint point, PointTrackingMode trackingMode) + => point.Snapshot.CreateTrackingPoint(point, trackingMode); } diff --git a/src/EditorFeatures/Core/Shared/Extensions/SnapshotSpanExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/SnapshotSpanExtensions.cs index 5b73da170b83e..4d6552c6762be 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/SnapshotSpanExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/SnapshotSpanExtensions.cs @@ -6,34 +6,33 @@ using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class SnapshotSpanExtensions { - internal static class SnapshotSpanExtensions - { - public static ITrackingSpan CreateTrackingSpan(this SnapshotSpan snapshotSpan, SpanTrackingMode trackingMode) - => snapshotSpan.Snapshot.CreateTrackingSpan(snapshotSpan.Span, trackingMode); + public static ITrackingSpan CreateTrackingSpan(this SnapshotSpan snapshotSpan, SpanTrackingMode trackingMode) + => snapshotSpan.Snapshot.CreateTrackingSpan(snapshotSpan.Span, trackingMode); - public static void GetLinesAndCharacters( - this SnapshotSpan snapshotSpan, - out int startLineNumber, - out int startCharacterIndex, - out int endLineNumber, - out int endCharacterIndex) - { - snapshotSpan.Snapshot.GetLineAndCharacter(snapshotSpan.Span.Start, out startLineNumber, out startCharacterIndex); - snapshotSpan.Snapshot.GetLineAndCharacter(snapshotSpan.Span.End, out endLineNumber, out endCharacterIndex); - } + public static void GetLinesAndCharacters( + this SnapshotSpan snapshotSpan, + out int startLineNumber, + out int startCharacterIndex, + out int endLineNumber, + out int endCharacterIndex) + { + snapshotSpan.Snapshot.GetLineAndCharacter(snapshotSpan.Span.Start, out startLineNumber, out startCharacterIndex); + snapshotSpan.Snapshot.GetLineAndCharacter(snapshotSpan.Span.End, out endLineNumber, out endCharacterIndex); + } - public static LinePositionSpan ToLinePositionSpan(this SnapshotSpan snapshotSpan) - { - snapshotSpan.GetLinesAndCharacters(out var startLine, out var startChar, out var endLine, out var endChar); - return new LinePositionSpan(new LinePosition(startLine, startChar), new LinePosition(endLine, endChar)); - } + public static LinePositionSpan ToLinePositionSpan(this SnapshotSpan snapshotSpan) + { + snapshotSpan.GetLinesAndCharacters(out var startLine, out var startChar, out var endLine, out var endChar); + return new LinePositionSpan(new LinePosition(startLine, startChar), new LinePosition(endLine, endChar)); + } - public static bool IntersectsWith(this SnapshotSpan snapshotSpan, TextSpan textSpan) - => snapshotSpan.IntersectsWith(textSpan.ToSpan()); + public static bool IntersectsWith(this SnapshotSpan snapshotSpan, TextSpan textSpan) + => snapshotSpan.IntersectsWith(textSpan.ToSpan()); - public static bool IntersectsWith(this SnapshotSpan snapshotSpan, int position) - => snapshotSpan.Span.IntersectsWith(position); - } + public static bool IntersectsWith(this SnapshotSpan snapshotSpan, int position) + => snapshotSpan.Span.IntersectsWith(position); } diff --git a/src/EditorFeatures/Core/Shared/Extensions/SpanExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/SpanExtensions.cs index c78ee9e28119a..d76293ba1933d 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/SpanExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/SpanExtensions.cs @@ -5,22 +5,21 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +/// +/// Extension methods for the editor Span struct +/// +internal static class SpanExtensions { /// - /// Extension methods for the editor Span struct + /// Convert the editor Span instance to the corresponding TextSpan instance /// - internal static class SpanExtensions - { - /// - /// Convert the editor Span instance to the corresponding TextSpan instance - /// - /// - /// - public static TextSpan ToTextSpan(this Span span) - => new(span.Start, span.Length); + /// + /// + public static TextSpan ToTextSpan(this Span span) + => new(span.Start, span.Length); - public static bool IntersectsWith(this Span span, int position) - => position >= span.Start && position <= span.End; - } + public static bool IntersectsWith(this Span span, int position) + => position >= span.Start && position <= span.End; } diff --git a/src/EditorFeatures/Core/Shared/Extensions/TextChangeExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/TextChangeExtensions.cs index d7b508dfc489d..2e4a1a3014eaf 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/TextChangeExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/TextChangeExtensions.cs @@ -5,11 +5,10 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static class TextChangeExtensions { - internal static class TextChangeExtensions - { - public static TextChangeRange ToTextChangeRange(this ITextChange textChange) - => new(textChange.OldSpan.ToTextSpan(), textChange.NewLength); - } + public static TextChangeRange ToTextChangeRange(this ITextChange textChange) + => new(textChange.OldSpan.ToTextSpan(), textChange.NewLength); } diff --git a/src/EditorFeatures/Core/Shared/Extensions/WorkspaceExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/WorkspaceExtensions.cs index 48416005e0eaf..8077271979078 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/WorkspaceExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/WorkspaceExtensions.cs @@ -8,40 +8,39 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions +namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions; + +internal static partial class IWorkspaceExtensions { - internal static partial class IWorkspaceExtensions + /// + /// Update the workspace so that the document with the Id of + /// has the text of newDocument. If the document is open, then this method will determine a + /// minimal set of changes to apply to the document. + /// + internal static void ApplyDocumentChanges(this Workspace workspace, Document newDocument, CancellationToken cancellationToken) { - /// - /// Update the workspace so that the document with the Id of - /// has the text of newDocument. If the document is open, then this method will determine a - /// minimal set of changes to apply to the document. - /// - internal static void ApplyDocumentChanges(this Workspace workspace, Document newDocument, CancellationToken cancellationToken) - { - var oldSolution = workspace.CurrentSolution; - var oldDocument = oldSolution.GetRequiredDocument(newDocument.Id); - var changes = newDocument.GetTextChangesAsync(oldDocument, cancellationToken).WaitAndGetResult(cancellationToken); - var newSolution = oldSolution.UpdateDocument(newDocument.Id, changes, cancellationToken); - workspace.TryApplyChanges(newSolution); - } + var oldSolution = workspace.CurrentSolution; + var oldDocument = oldSolution.GetRequiredDocument(newDocument.Id); + var changes = newDocument.GetTextChangesAsync(oldDocument, cancellationToken).WaitAndGetResult(cancellationToken); + var newSolution = oldSolution.UpdateDocument(newDocument.Id, changes, cancellationToken); + workspace.TryApplyChanges(newSolution); + } - /// - /// Update the solution so that the document with the Id has the text changes - /// - internal static void ApplyTextChanges(this Workspace workspace, DocumentId id, IEnumerable textChanges, CancellationToken cancellationToken) - { - var oldSolution = workspace.CurrentSolution; - var newSolution = oldSolution.UpdateDocument(id, textChanges, cancellationToken); - workspace.TryApplyChanges(newSolution); - } + /// + /// Update the solution so that the document with the Id has the text changes + /// + internal static void ApplyTextChanges(this Workspace workspace, DocumentId id, IEnumerable textChanges, CancellationToken cancellationToken) + { + var oldSolution = workspace.CurrentSolution; + var newSolution = oldSolution.UpdateDocument(id, textChanges, cancellationToken); + workspace.TryApplyChanges(newSolution); + } - private static Solution UpdateDocument(this Solution solution, DocumentId id, IEnumerable textChanges, CancellationToken cancellationToken) - { - var oldDocument = solution.GetRequiredDocument(id); - var oldText = oldDocument.GetTextSynchronously(cancellationToken); - var newText = oldText.WithChanges(textChanges); - return solution.WithDocumentText(id, newText, PreservationMode.PreserveIdentity); - } + private static Solution UpdateDocument(this Solution solution, DocumentId id, IEnumerable textChanges, CancellationToken cancellationToken) + { + var oldDocument = solution.GetRequiredDocument(id); + var oldText = oldDocument.GetTextSynchronously(cancellationToken); + var newText = oldText.WithChanges(textChanges); + return solution.WithDocumentText(id, newText, PreservationMode.PreserveIdentity); } } diff --git a/src/EditorFeatures/Core/Shared/ITextBufferSupportsFeatureService.cs b/src/EditorFeatures/Core/Shared/ITextBufferSupportsFeatureService.cs index 51bb71bc419f1..f25e2da590f2d 100644 --- a/src/EditorFeatures/Core/Shared/ITextBufferSupportsFeatureService.cs +++ b/src/EditorFeatures/Core/Shared/ITextBufferSupportsFeatureService.cs @@ -6,13 +6,12 @@ using Microsoft.CodeAnalysis.Shared; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared +namespace Microsoft.CodeAnalysis.Editor.Shared; + +internal interface ITextBufferSupportsFeatureService : IWorkspaceService { - internal interface ITextBufferSupportsFeatureService : IWorkspaceService - { - bool SupportsCodeFixes(ITextBuffer textBuffer); - bool SupportsRefactorings(ITextBuffer textBuffer); - bool SupportsRename(ITextBuffer textBuffer); - bool SupportsNavigationToAnyPosition(ITextBuffer textBuffer); - } + bool SupportsCodeFixes(ITextBuffer textBuffer); + bool SupportsRefactorings(ITextBuffer textBuffer); + bool SupportsRename(ITextBuffer textBuffer); + bool SupportsNavigationToAnyPosition(ITextBuffer textBuffer); } diff --git a/src/EditorFeatures/Core/Shared/Options/ComponentOnOffOptions.cs b/src/EditorFeatures/Core/Shared/Options/ComponentOnOffOptions.cs index b41276010b223..21ff88e886ff0 100644 --- a/src/EditorFeatures/Core/Shared/Options/ComponentOnOffOptions.cs +++ b/src/EditorFeatures/Core/Shared/Options/ComponentOnOffOptions.cs @@ -4,15 +4,14 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.Shared.Options +namespace Microsoft.CodeAnalysis.Editor.Shared.Options; + +/// +/// options to indicate whether a certain component in Roslyn is enabled or not +/// +internal sealed class EditorComponentOnOffOptions { - /// - /// options to indicate whether a certain component in Roslyn is enabled or not - /// - internal sealed class EditorComponentOnOffOptions - { - public static readonly Option2 Adornment = new("dotnet_enable_editor_adornment", defaultValue: true); - public static readonly Option2 Tagger = new("dotnet_enable_editor_tagger", defaultValue: true); - public static readonly Option2 CodeRefactorings = new("dotnet_enable_code_refactorings", defaultValue: true); - } + public static readonly Option2 Adornment = new("dotnet_enable_editor_adornment", defaultValue: true); + public static readonly Option2 Tagger = new("dotnet_enable_editor_tagger", defaultValue: true); + public static readonly Option2 CodeRefactorings = new("dotnet_enable_code_refactorings", defaultValue: true); } diff --git a/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs b/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs index e4036ce952d1a..09f7c428c8300 100644 --- a/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs +++ b/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs @@ -4,34 +4,33 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.Shared.Options +namespace Microsoft.CodeAnalysis.Editor.Shared.Options; + +internal sealed class FeatureOnOffOptions { - internal sealed class FeatureOnOffOptions - { - /// - /// This option is not currently used by Roslyn, but we might want to implement it in the - /// future. Keeping the option while it's unimplemented allows all upgrade paths to - /// maintain any customized value for this setting, even through versions that have not - /// implemented this feature yet. - /// - public static readonly PerLanguageOption2 RenameTracking = new("FeatureOnOffOptions_RenameTracking", defaultValue: true); + /// + /// This option is not currently used by Roslyn, but we might want to implement it in the + /// future. Keeping the option while it's unimplemented allows all upgrade paths to + /// maintain any customized value for this setting, even through versions that have not + /// implemented this feature yet. + /// + public static readonly PerLanguageOption2 RenameTracking = new("FeatureOnOffOptions_RenameTracking", defaultValue: true); - /// - /// This option is not currently used by Roslyn, but we might want to implement it in the - /// future. Keeping the option while it's unimplemented allows all upgrade paths to - /// maintain any customized value for this setting, even through versions that have not - /// implemented this feature yet. - /// - public static readonly PerLanguageOption2 RefactoringVerification = new("FeatureOnOffOptions_RefactoringVerification", defaultValue: false); + /// + /// This option is not currently used by Roslyn, but we might want to implement it in the + /// future. Keeping the option while it's unimplemented allows all upgrade paths to + /// maintain any customized value for this setting, even through versions that have not + /// implemented this feature yet. + /// + public static readonly PerLanguageOption2 RefactoringVerification = new("FeatureOnOffOptions_RefactoringVerification", defaultValue: false); - public static readonly Option2 OfferRemoveUnusedReferences = new("dotnet_offer_remove_unused_references", defaultValue: true); + public static readonly Option2 OfferRemoveUnusedReferences = new("dotnet_offer_remove_unused_references", defaultValue: true); - public static readonly Option2 OfferRemoveUnusedReferencesFeatureFlag = new("dotnet_offer_remove_unused_references_feature_flag", defaultValue: false); + public static readonly Option2 OfferRemoveUnusedReferencesFeatureFlag = new("dotnet_offer_remove_unused_references_feature_flag", defaultValue: false); - /// - /// Not used by Roslyn but exposed in C# and VB option UI. Used by TestWindow and Project System. - /// TODO: remove https://github.com/dotnet/roslyn/issues/57253 - /// - public static readonly Option2 SkipAnalyzersForImplicitlyTriggeredBuilds = new("dotnet_skip_analyzers_for_implicitly_triggered_builds", defaultValue: true); - } + /// + /// Not used by Roslyn but exposed in C# and VB option UI. Used by TestWindow and Project System. + /// TODO: remove https://github.com/dotnet/roslyn/issues/57253 + /// + public static readonly Option2 SkipAnalyzersForImplicitlyTriggeredBuilds = new("dotnet_skip_analyzers_for_implicitly_triggered_builds", defaultValue: true); } diff --git a/src/EditorFeatures/Core/Shared/Preview/PredefinedPreviewTaggerKeys.cs b/src/EditorFeatures/Core/Shared/Preview/PredefinedPreviewTaggerKeys.cs index 372468c5a4c5b..f30be07ec2278 100644 --- a/src/EditorFeatures/Core/Shared/Preview/PredefinedPreviewTaggerKeys.cs +++ b/src/EditorFeatures/Core/Shared/Preview/PredefinedPreviewTaggerKeys.cs @@ -2,16 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Editor.Shared.Preview +namespace Microsoft.CodeAnalysis.Editor.Shared.Preview; + +internal static class PredefinedPreviewTaggerKeys { - internal static class PredefinedPreviewTaggerKeys - { - public static readonly object DefinitionHighlightingSpansKey = new(); - public static readonly object ReferenceHighlightingSpansKey = new(); - public static readonly object WrittenReferenceHighlightingSpansKey = new(); - public static readonly object ConflictSpansKey = new(); - public static readonly object WarningSpansKey = new(); - public static readonly object SuppressDiagnosticsSpansKey = new(); - public static readonly object StaticClassificationSpansKey = new(); - } + public static readonly object DefinitionHighlightingSpansKey = new(); + public static readonly object ReferenceHighlightingSpansKey = new(); + public static readonly object WrittenReferenceHighlightingSpansKey = new(); + public static readonly object ConflictSpansKey = new(); + public static readonly object WarningSpansKey = new(); + public static readonly object SuppressDiagnosticsSpansKey = new(); + public static readonly object StaticClassificationSpansKey = new(); } diff --git a/src/EditorFeatures/Core/Shared/Preview/PreviewWorkspace.cs b/src/EditorFeatures/Core/Shared/Preview/PreviewWorkspace.cs index b1a92e6a3931a..69799485a6fb0 100644 --- a/src/EditorFeatures/Core/Shared/Preview/PreviewWorkspace.cs +++ b/src/EditorFeatures/Core/Shared/Preview/PreviewWorkspace.cs @@ -10,105 +10,104 @@ using Microsoft.CodeAnalysis.SolutionCrawler; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Preview +namespace Microsoft.CodeAnalysis.Editor.Shared.Preview; + +internal class PreviewWorkspace : Workspace { - internal class PreviewWorkspace : Workspace + public PreviewWorkspace() + : base(MefHostServices.DefaultHost, WorkspaceKind.Preview) { - public PreviewWorkspace() - : base(MefHostServices.DefaultHost, WorkspaceKind.Preview) - { - } + } - public PreviewWorkspace(HostServices hostServices) - : base(hostServices, WorkspaceKind.Preview) - { - } + public PreviewWorkspace(HostServices hostServices) + : base(hostServices, WorkspaceKind.Preview) + { + } - public PreviewWorkspace(Solution solution) - : base(solution.Workspace.Services.HostServices, WorkspaceKind.Preview) - { - var (oldSolution, newSolution) = this.SetCurrentSolutionEx(solution); + public PreviewWorkspace(Solution solution) + : base(solution.Workspace.Services.HostServices, WorkspaceKind.Preview) + { + var (oldSolution, newSolution) = this.SetCurrentSolutionEx(solution); - this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.SolutionChanged, oldSolution, newSolution); - } + this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.SolutionChanged, oldSolution, newSolution); + } + + public void EnableSolutionCrawler() + { + Services.GetRequiredService().Register(this); + } + + public override bool CanApplyChange(ApplyChangesKind feature) + { + // one can manipulate preview workspace solution as mush as they want. + return true; + } + + // This method signature is the base method signature which should be used for a client of a workspace to + // tell the host to open it; in our case we want to open documents directly by passing the known buffer we created + // for it. + [Obsolete("Do not call the base OpenDocument method; instead call the overload that takes a container.", error: true)] + public new void OpenDocument(DocumentId documentId, bool activate = true) + { + } - public void EnableSolutionCrawler() + public void OpenDocument(DocumentId documentId, SourceTextContainer textContainer) + { + var document = this.CurrentSolution.GetTextDocument(documentId); + + // This could be null if we're previewing a source generated document; we can't wire those up yet + // TODO: implement this + if (document == null) { - Services.GetRequiredService().Register(this); + return; } - public override bool CanApplyChange(ApplyChangesKind feature) + if (document is AnalyzerConfigDocument) { - // one can manipulate preview workspace solution as mush as they want. - return true; + this.OnAnalyzerConfigDocumentOpened(documentId, textContainer); } - - // This method signature is the base method signature which should be used for a client of a workspace to - // tell the host to open it; in our case we want to open documents directly by passing the known buffer we created - // for it. - [Obsolete("Do not call the base OpenDocument method; instead call the overload that takes a container.", error: true)] - public new void OpenDocument(DocumentId documentId, bool activate = true) + else if (document is Document) { + this.OnDocumentOpened(documentId, textContainer); } - - public void OpenDocument(DocumentId documentId, SourceTextContainer textContainer) + else { - var document = this.CurrentSolution.GetTextDocument(documentId); - - // This could be null if we're previewing a source generated document; we can't wire those up yet - // TODO: implement this - if (document == null) - { - return; - } - - if (document is AnalyzerConfigDocument) - { - this.OnAnalyzerConfigDocumentOpened(documentId, textContainer); - } - else if (document is Document) - { - this.OnDocumentOpened(documentId, textContainer); - } - else - { - this.OnAdditionalDocumentOpened(documentId, textContainer); - } + this.OnAdditionalDocumentOpened(documentId, textContainer); } + } - public override void CloseDocument(DocumentId documentId) - { - var document = this.CurrentSolution.GetRequiredDocument(documentId); - var text = document.GetTextSynchronously(CancellationToken.None); - var version = document.GetTextVersionSynchronously(CancellationToken.None); + public override void CloseDocument(DocumentId documentId) + { + var document = this.CurrentSolution.GetRequiredDocument(documentId); + var text = document.GetTextSynchronously(CancellationToken.None); + var version = document.GetTextVersionSynchronously(CancellationToken.None); - this.OnDocumentClosed(documentId, TextLoader.From(TextAndVersion.Create(text, version))); - } + this.OnDocumentClosed(documentId, TextLoader.From(TextAndVersion.Create(text, version))); + } - public override void CloseAdditionalDocument(DocumentId documentId) - { - var document = this.CurrentSolution.GetRequiredAdditionalDocument(documentId); - var text = document.GetTextSynchronously(CancellationToken.None); - var version = document.GetTextVersionSynchronously(CancellationToken.None); + public override void CloseAdditionalDocument(DocumentId documentId) + { + var document = this.CurrentSolution.GetRequiredAdditionalDocument(documentId); + var text = document.GetTextSynchronously(CancellationToken.None); + var version = document.GetTextVersionSynchronously(CancellationToken.None); - this.OnAdditionalDocumentClosed(documentId, TextLoader.From(TextAndVersion.Create(text, version))); - } + this.OnAdditionalDocumentClosed(documentId, TextLoader.From(TextAndVersion.Create(text, version))); + } - public override void CloseAnalyzerConfigDocument(DocumentId documentId) - { - var document = this.CurrentSolution.GetRequiredAnalyzerConfigDocument(documentId); - var text = document.GetTextSynchronously(CancellationToken.None); - var version = document.GetTextVersionSynchronously(CancellationToken.None); + public override void CloseAnalyzerConfigDocument(DocumentId documentId) + { + var document = this.CurrentSolution.GetRequiredAnalyzerConfigDocument(documentId); + var text = document.GetTextSynchronously(CancellationToken.None); + var version = document.GetTextVersionSynchronously(CancellationToken.None); - this.OnAnalyzerConfigDocumentClosed(documentId, TextLoader.From(TextAndVersion.Create(text, version))); - } + this.OnAnalyzerConfigDocumentClosed(documentId, TextLoader.From(TextAndVersion.Create(text, version))); + } - protected override void Dispose(bool finalize) - { - base.Dispose(finalize); + protected override void Dispose(bool finalize) + { + base.Dispose(finalize); - Services.GetRequiredService().Unregister(this); - ClearSolution(); - } + Services.GetRequiredService().Unregister(this); + ClearSolution(); } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/AbstractTaggerEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/AbstractTaggerEventSource.cs index 81a26a8105ce9..99c923b9601df 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/AbstractTaggerEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/AbstractTaggerEventSource.cs @@ -6,35 +6,34 @@ using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Editor.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal abstract class AbstractTaggerEventSource : ITaggerEventSource { - internal abstract class AbstractTaggerEventSource : ITaggerEventSource - { - private bool _paused; + private bool _paused; - protected AbstractTaggerEventSource() - { - } + protected AbstractTaggerEventSource() + { + } - public abstract void Connect(); - public abstract void Disconnect(); + public abstract void Connect(); + public abstract void Disconnect(); - public event EventHandler? Changed; + public event EventHandler? Changed; - protected void RaiseChanged() - { - if (!_paused) - this.Changed?.Invoke(this, TaggerEventArgs.Empty); - } + protected void RaiseChanged() + { + if (!_paused) + this.Changed?.Invoke(this, TaggerEventArgs.Empty); + } - public void Pause() - { - _paused = true; - } + public void Pause() + { + _paused = true; + } - public void Resume() - { - _paused = false; - } + public void Resume() + { + _paused = false; } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/AbstractWorkspaceTrackingTaggerEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/AbstractWorkspaceTrackingTaggerEventSource.cs index 996ae71f83ef0..816b831a05f79 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/AbstractWorkspaceTrackingTaggerEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/AbstractWorkspaceTrackingTaggerEventSource.cs @@ -6,63 +6,62 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +/// +/// An abstract implementation of a tagger event source that takes a buffer and tracks +/// the workspace that it's attached to. +/// +internal abstract class AbstractWorkspaceTrackingTaggerEventSource : AbstractTaggerEventSource { - /// - /// An abstract implementation of a tagger event source that takes a buffer and tracks - /// the workspace that it's attached to. - /// - internal abstract class AbstractWorkspaceTrackingTaggerEventSource : AbstractTaggerEventSource - { - private readonly WorkspaceRegistration _workspaceRegistration; + private readonly WorkspaceRegistration _workspaceRegistration; - protected ITextBuffer SubjectBuffer { get; } - protected Workspace? CurrentWorkspace { get; private set; } + protected ITextBuffer SubjectBuffer { get; } + protected Workspace? CurrentWorkspace { get; private set; } - protected AbstractWorkspaceTrackingTaggerEventSource(ITextBuffer subjectBuffer) - { - this.SubjectBuffer = subjectBuffer; - _workspaceRegistration = Workspace.GetWorkspaceRegistration(subjectBuffer.AsTextContainer()); - } + protected AbstractWorkspaceTrackingTaggerEventSource(ITextBuffer subjectBuffer) + { + this.SubjectBuffer = subjectBuffer; + _workspaceRegistration = Workspace.GetWorkspaceRegistration(subjectBuffer.AsTextContainer()); + } - protected abstract void ConnectToWorkspace(Workspace workspace); - protected abstract void DisconnectFromWorkspace(Workspace workspace); + protected abstract void ConnectToWorkspace(Workspace workspace); + protected abstract void DisconnectFromWorkspace(Workspace workspace); - public override void Connect() - { - this.CurrentWorkspace = _workspaceRegistration.Workspace; - _workspaceRegistration.WorkspaceChanged += OnWorkspaceRegistrationChanged; + public override void Connect() + { + this.CurrentWorkspace = _workspaceRegistration.Workspace; + _workspaceRegistration.WorkspaceChanged += OnWorkspaceRegistrationChanged; - if (this.CurrentWorkspace != null) - { - ConnectToWorkspace(this.CurrentWorkspace); - } + if (this.CurrentWorkspace != null) + { + ConnectToWorkspace(this.CurrentWorkspace); } + } - private void OnWorkspaceRegistrationChanged(object? sender, EventArgs e) + private void OnWorkspaceRegistrationChanged(object? sender, EventArgs e) + { + if (this.CurrentWorkspace != null) { - if (this.CurrentWorkspace != null) - { - DisconnectFromWorkspace(this.CurrentWorkspace); - } + DisconnectFromWorkspace(this.CurrentWorkspace); + } - this.CurrentWorkspace = _workspaceRegistration.Workspace; + this.CurrentWorkspace = _workspaceRegistration.Workspace; - if (this.CurrentWorkspace != null) - { - ConnectToWorkspace(this.CurrentWorkspace); - } + if (this.CurrentWorkspace != null) + { + ConnectToWorkspace(this.CurrentWorkspace); } + } - public override void Disconnect() + public override void Disconnect() + { + if (this.CurrentWorkspace != null) { - if (this.CurrentWorkspace != null) - { - DisconnectFromWorkspace(this.CurrentWorkspace); - this.CurrentWorkspace = null; - } - - _workspaceRegistration.WorkspaceChanged -= OnWorkspaceRegistrationChanged; + DisconnectFromWorkspace(this.CurrentWorkspace); + this.CurrentWorkspace = null; } + + _workspaceRegistration.WorkspaceChanged -= OnWorkspaceRegistrationChanged; } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerConstants.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerConstants.cs index d3d34c30286f6..2e38ff44c26ba 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerConstants.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerConstants.cs @@ -6,30 +6,29 @@ using Microsoft.CodeAnalysis.Editor.Tagging; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal static class TaggerConstants { - internal static class TaggerConstants + internal static TimeSpan ComputeTimeDelay(this TaggerDelay behavior, ITextBuffer textBufferOpt) { - internal static TimeSpan ComputeTimeDelay(this TaggerDelay behavior, ITextBuffer textBufferOpt) + if (TextBufferAssociatedViewService.AnyAssociatedViewHasFocus(textBufferOpt)) { - if (TextBufferAssociatedViewService.AnyAssociatedViewHasFocus(textBufferOpt)) - { - // TODO : should we remove TaggerBehavior enum all together and put NearImmediateDelay - // const in Interaction? - return ComputeTimeDelay(behavior); - } - - return DelayTimeSpan.NonFocus; + // TODO : should we remove TaggerBehavior enum all together and put NearImmediateDelay + // const in Interaction? + return ComputeTimeDelay(behavior); } - internal static TimeSpan ComputeTimeDelay(this TaggerDelay behavior) - => behavior switch - { - TaggerDelay.NearImmediate => DelayTimeSpan.NearImmediate, - TaggerDelay.Short => DelayTimeSpan.Short, - TaggerDelay.Medium => DelayTimeSpan.Medium, - TaggerDelay.OnIdle => DelayTimeSpan.Idle, - _ => DelayTimeSpan.NonFocus, - }; + return DelayTimeSpan.NonFocus; } + + internal static TimeSpan ComputeTimeDelay(this TaggerDelay behavior) + => behavior switch + { + TaggerDelay.NearImmediate => DelayTimeSpan.NearImmediate, + TaggerDelay.Short => DelayTimeSpan.Short, + TaggerDelay.Medium => DelayTimeSpan.Medium, + TaggerDelay.OnIdle => DelayTimeSpan.Idle, + _ => DelayTimeSpan.NonFocus, + }; } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.CaretPositionChangedEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.CaretPositionChangedEventSource.cs index f97921bbf63a0..52e1fb9ccf7ef 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.CaretPositionChangedEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.CaretPositionChangedEventSource.cs @@ -6,30 +6,29 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private class CaretPositionChangedEventSource : AbstractTaggerEventSource { - private class CaretPositionChangedEventSource : AbstractTaggerEventSource - { - private readonly ITextView _textView; + private readonly ITextView _textView; - public CaretPositionChangedEventSource(ITextView textView, ITextBuffer subjectBuffer) - { - Contract.ThrowIfNull(textView); - Contract.ThrowIfNull(subjectBuffer); + public CaretPositionChangedEventSource(ITextView textView, ITextBuffer subjectBuffer) + { + Contract.ThrowIfNull(textView); + Contract.ThrowIfNull(subjectBuffer); - _textView = textView; - } + _textView = textView; + } - public override void Connect() - => _textView.Caret.PositionChanged += OnCaretPositionChanged; + public override void Connect() + => _textView.Caret.PositionChanged += OnCaretPositionChanged; - public override void Disconnect() - => _textView.Caret.PositionChanged -= OnCaretPositionChanged; + public override void Disconnect() + => _textView.Caret.PositionChanged -= OnCaretPositionChanged; - private void OnCaretPositionChanged(object? sender, CaretPositionChangedEventArgs e) - => this.RaiseChanged(); - } + private void OnCaretPositionChanged(object? sender, CaretPositionChangedEventArgs e) + => this.RaiseChanged(); } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.CompositionEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.CompositionEventSource.cs index 0cf5d1546be24..e86949da00d7e 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.CompositionEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.CompositionEventSource.cs @@ -6,38 +6,37 @@ using Microsoft.CodeAnalysis.Editor.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private class CompositionEventSource : ITaggerEventSource { - private class CompositionEventSource : ITaggerEventSource - { - private readonly ITaggerEventSource[] _providers; + private readonly ITaggerEventSource[] _providers; - public CompositionEventSource(ITaggerEventSource[] providers) - { - Contract.ThrowIfNull(providers); + public CompositionEventSource(ITaggerEventSource[] providers) + { + Contract.ThrowIfNull(providers); - _providers = providers; - } + _providers = providers; + } - public void Connect() - => _providers.Do(p => p.Connect()); + public void Connect() + => _providers.Do(p => p.Connect()); - public void Disconnect() - => _providers.Do(p => p.Disconnect()); + public void Disconnect() + => _providers.Do(p => p.Disconnect()); - public void Pause() - => _providers.Do(p => p.Pause()); + public void Pause() + => _providers.Do(p => p.Pause()); - public void Resume() - => _providers.Do(p => p.Resume()); + public void Resume() + => _providers.Do(p => p.Resume()); - public event EventHandler Changed - { - add => _providers.Do(p => p.Changed += value); - remove => _providers.Do(p => p.Changed -= value); - } + public event EventHandler Changed + { + add => _providers.Do(p => p.Changed += value); + remove => _providers.Do(p => p.Changed -= value); } } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.DiagnosticsChangedEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.DiagnosticsChangedEventSource.cs index 41e1d525af722..56658d13a19a8 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.DiagnosticsChangedEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.DiagnosticsChangedEventSource.cs @@ -7,47 +7,46 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private class DiagnosticsChangedEventSource(ITextBuffer subjectBuffer, IDiagnosticService service) : AbstractTaggerEventSource { - private class DiagnosticsChangedEventSource(ITextBuffer subjectBuffer, IDiagnosticService service) : AbstractTaggerEventSource - { - private readonly ITextBuffer _subjectBuffer = subjectBuffer; - private readonly IDiagnosticService _service = service; + private readonly ITextBuffer _subjectBuffer = subjectBuffer; + private readonly IDiagnosticService _service = service; - private void OnDiagnosticsUpdated(object? sender, ImmutableArray e) + private void OnDiagnosticsUpdated(object? sender, ImmutableArray e) + { + var textContainer = _subjectBuffer.AsTextContainer(); + var anyChanged = false; + Workspace? lastWorkspace = null; + DocumentId? documentId = null; + foreach (var args in e) { - var textContainer = _subjectBuffer.AsTextContainer(); - var anyChanged = false; - Workspace? lastWorkspace = null; - DocumentId? documentId = null; - foreach (var args in e) + if (args.Workspace != lastWorkspace) { - if (args.Workspace != lastWorkspace) - { - lastWorkspace = args.Workspace; - documentId = args.Workspace.GetDocumentIdInCurrentContext(textContainer); - } - - if (args.DocumentId == documentId) - { - anyChanged = true; - break; - } + lastWorkspace = args.Workspace; + documentId = args.Workspace.GetDocumentIdInCurrentContext(textContainer); } - if (anyChanged) + if (args.DocumentId == documentId) { - this.RaiseChanged(); + anyChanged = true; + break; } } - public override void Connect() - => _service.DiagnosticsUpdated += OnDiagnosticsUpdated; - - public override void Disconnect() - => _service.DiagnosticsUpdated -= OnDiagnosticsUpdated; + if (anyChanged) + { + this.RaiseChanged(); + } } + + public override void Connect() + => _service.DiagnosticsUpdated += OnDiagnosticsUpdated; + + public override void Disconnect() + => _service.DiagnosticsUpdated -= OnDiagnosticsUpdated; } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.DocumentActiveContextChangedEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.DocumentActiveContextChangedEventSource.cs index 482946dd4d794..691eabd36dfd0 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.DocumentActiveContextChangedEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.DocumentActiveContextChangedEventSource.cs @@ -6,26 +6,25 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private class DocumentActiveContextChangedEventSource(ITextBuffer subjectBuffer) : AbstractWorkspaceTrackingTaggerEventSource(subjectBuffer) { - private class DocumentActiveContextChangedEventSource(ITextBuffer subjectBuffer) : AbstractWorkspaceTrackingTaggerEventSource(subjectBuffer) - { - protected override void ConnectToWorkspace(Workspace workspace) - => workspace.DocumentActiveContextChanged += OnDocumentActiveContextChanged; + protected override void ConnectToWorkspace(Workspace workspace) + => workspace.DocumentActiveContextChanged += OnDocumentActiveContextChanged; - protected override void DisconnectFromWorkspace(Workspace workspace) - => workspace.DocumentActiveContextChanged -= OnDocumentActiveContextChanged; + protected override void DisconnectFromWorkspace(Workspace workspace) + => workspace.DocumentActiveContextChanged -= OnDocumentActiveContextChanged; - private void OnDocumentActiveContextChanged(object? sender, DocumentActiveContextChangedEventArgs e) - { - var document = SubjectBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); + private void OnDocumentActiveContextChanged(object? sender, DocumentActiveContextChangedEventArgs e) + { + var document = SubjectBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); - if (document != null && document.Id == e.NewActiveContextDocumentId) - { - this.RaiseChanged(); - } + if (document != null && document.Id == e.NewActiveContextDocumentId) + { + this.RaiseChanged(); } } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.OptionChangedEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.OptionChangedEventSource.cs index 88f1b470d3d55..67b0cc0c7ffc6 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.OptionChangedEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.OptionChangedEventSource.cs @@ -5,31 +5,30 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private sealed class GlobalOptionChangedEventSource(IGlobalOptionService globalOptions, IOption2 globalOption) : AbstractTaggerEventSource { - private sealed class GlobalOptionChangedEventSource(IGlobalOptionService globalOptions, IOption2 globalOption) : AbstractTaggerEventSource - { - private readonly IOption2 _globalOption = globalOption; - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IOption2 _globalOption = globalOption; + private readonly IGlobalOptionService _globalOptions = globalOptions; - public override void Connect() - { - _globalOptions.AddOptionChangedHandler(this, OnGlobalOptionChanged); - } + public override void Connect() + { + _globalOptions.AddOptionChangedHandler(this, OnGlobalOptionChanged); + } - public override void Disconnect() - { - _globalOptions.RemoveOptionChangedHandler(this, OnGlobalOptionChanged); - } + public override void Disconnect() + { + _globalOptions.RemoveOptionChangedHandler(this, OnGlobalOptionChanged); + } - private void OnGlobalOptionChanged(object? sender, OptionChangedEventArgs e) + private void OnGlobalOptionChanged(object? sender, OptionChangedEventArgs e) + { + if (e.Option == _globalOption) { - if (e.Option == _globalOption) - { - RaiseChanged(); - } + RaiseChanged(); } } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ParseOptionChangedEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ParseOptionChangedEventSource.cs index bc5f858850f1e..ea4ab7aa9209c 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ParseOptionChangedEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ParseOptionChangedEventSource.cs @@ -9,38 +9,37 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private class ParseOptionChangedEventSource(ITextBuffer subjectBuffer) : AbstractWorkspaceTrackingTaggerEventSource(subjectBuffer) { - private class ParseOptionChangedEventSource(ITextBuffer subjectBuffer) : AbstractWorkspaceTrackingTaggerEventSource(subjectBuffer) - { - protected override void ConnectToWorkspace(Workspace workspace) - => workspace.WorkspaceChanged += OnWorkspaceChanged; + protected override void ConnectToWorkspace(Workspace workspace) + => workspace.WorkspaceChanged += OnWorkspaceChanged; - protected override void DisconnectFromWorkspace(Workspace workspace) - => workspace.WorkspaceChanged -= OnWorkspaceChanged; + protected override void DisconnectFromWorkspace(Workspace workspace) + => workspace.WorkspaceChanged -= OnWorkspaceChanged; - private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) + private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) + { + if (e.Kind == WorkspaceChangeKind.ProjectChanged) { - if (e.Kind == WorkspaceChangeKind.ProjectChanged) - { - RoslynDebug.AssertNotNull(e.ProjectId); - var oldProject = e.OldSolution.GetRequiredProject(e.ProjectId); - var newProject = e.NewSolution.GetRequiredProject(e.ProjectId); + RoslynDebug.AssertNotNull(e.ProjectId); + var oldProject = e.OldSolution.GetRequiredProject(e.ProjectId); + var newProject = e.NewSolution.GetRequiredProject(e.ProjectId); - if (!object.Equals(oldProject.ParseOptions, newProject.ParseOptions)) + if (!object.Equals(oldProject.ParseOptions, newProject.ParseOptions)) + { + var workspace = e.NewSolution.Workspace; + var documentId = workspace.GetDocumentIdInCurrentContext(SubjectBuffer.AsTextContainer()); + if (documentId != null) { - var workspace = e.NewSolution.Workspace; - var documentId = workspace.GetDocumentIdInCurrentContext(SubjectBuffer.AsTextContainer()); - if (documentId != null) - { - var relatedDocumentIds = e.NewSolution.GetRelatedDocumentIds(documentId); + var relatedDocumentIds = e.NewSolution.GetRelatedDocumentIds(documentId); - if (relatedDocumentIds.Any(static (d, e) => d.ProjectId == e.ProjectId, e)) - { - RaiseChanged(); - } + if (relatedDocumentIds.Any(static (d, e) => d.ProjectId == e.ProjectId, e)) + { + RaiseChanged(); } } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ReadOnlyRegionsChangedEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ReadOnlyRegionsChangedEventSource.cs index bbbd576c42a92..dc3ab3d637534 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ReadOnlyRegionsChangedEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ReadOnlyRegionsChangedEventSource.cs @@ -6,28 +6,27 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private class ReadOnlyRegionsChangedEventSource : AbstractTaggerEventSource { - private class ReadOnlyRegionsChangedEventSource : AbstractTaggerEventSource - { - private readonly ITextBuffer _subjectBuffer; + private readonly ITextBuffer _subjectBuffer; - public ReadOnlyRegionsChangedEventSource(ITextBuffer subjectBuffer) - { - Contract.ThrowIfNull(subjectBuffer); - _subjectBuffer = subjectBuffer; - } + public ReadOnlyRegionsChangedEventSource(ITextBuffer subjectBuffer) + { + Contract.ThrowIfNull(subjectBuffer); + _subjectBuffer = subjectBuffer; + } - public override void Connect() - => _subjectBuffer.ReadOnlyRegionsChanged += OnReadOnlyRegionsChanged; + public override void Connect() + => _subjectBuffer.ReadOnlyRegionsChanged += OnReadOnlyRegionsChanged; - public override void Disconnect() - => _subjectBuffer.ReadOnlyRegionsChanged -= OnReadOnlyRegionsChanged; + public override void Disconnect() + => _subjectBuffer.ReadOnlyRegionsChanged -= OnReadOnlyRegionsChanged; - private void OnReadOnlyRegionsChanged(object? sender, SnapshotSpanEventArgs e) - => this.RaiseChanged(); - } + private void OnReadOnlyRegionsChanged(object? sender, SnapshotSpanEventArgs e) + => this.RaiseChanged(); } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.SelectionChangedEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.SelectionChangedEventSource.cs index fc9e73694193b..260cbd543c5a5 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.SelectionChangedEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.SelectionChangedEventSource.cs @@ -5,22 +5,21 @@ using System; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private class SelectionChangedEventSource(ITextView textView) : AbstractTaggerEventSource { - private class SelectionChangedEventSource(ITextView textView) : AbstractTaggerEventSource - { - private readonly ITextView _textView = textView; + private readonly ITextView _textView = textView; - public override void Connect() - => _textView.Selection.SelectionChanged += OnSelectionChanged; + public override void Connect() + => _textView.Selection.SelectionChanged += OnSelectionChanged; - public override void Disconnect() - => _textView.Selection.SelectionChanged -= OnSelectionChanged; + public override void Disconnect() + => _textView.Selection.SelectionChanged -= OnSelectionChanged; - private void OnSelectionChanged(object? sender, EventArgs args) - => RaiseChanged(); - } + private void OnSelectionChanged(object? sender, EventArgs args) + => RaiseChanged(); } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.TextChangedEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.TextChangedEventSource.cs index dc423555936a0..0168783750532 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.TextChangedEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.TextChangedEventSource.cs @@ -6,33 +6,32 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private class TextChangedEventSource : AbstractTaggerEventSource { - private class TextChangedEventSource : AbstractTaggerEventSource - { - private readonly ITextBuffer _subjectBuffer; + private readonly ITextBuffer _subjectBuffer; - public TextChangedEventSource(ITextBuffer subjectBuffer) - { - Contract.ThrowIfNull(subjectBuffer); - _subjectBuffer = subjectBuffer; - } + public TextChangedEventSource(ITextBuffer subjectBuffer) + { + Contract.ThrowIfNull(subjectBuffer); + _subjectBuffer = subjectBuffer; + } - public override void Connect() - => _subjectBuffer.Changed += OnTextBufferChanged; + public override void Connect() + => _subjectBuffer.Changed += OnTextBufferChanged; - public override void Disconnect() - => _subjectBuffer.Changed -= OnTextBufferChanged; + public override void Disconnect() + => _subjectBuffer.Changed -= OnTextBufferChanged; - private void OnTextBufferChanged(object? sender, TextContentChangedEventArgs e) - { - if (e.Changes.Count == 0) - return; + private void OnTextBufferChanged(object? sender, TextContentChangedEventArgs e) + { + if (e.Changes.Count == 0) + return; - this.RaiseChanged(); - } + this.RaiseChanged(); } } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ViewSpanChangedEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ViewSpanChangedEventSource.cs index 3d4761f8a679d..c058b3575eb4a 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ViewSpanChangedEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.ViewSpanChangedEventSource.cs @@ -10,57 +10,56 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private class ViewSpanChangedEventSource : AbstractTaggerEventSource { - private class ViewSpanChangedEventSource : AbstractTaggerEventSource - { - private readonly IThreadingContext _threadingContext; - private readonly ITextView _textView; + private readonly IThreadingContext _threadingContext; + private readonly ITextView _textView; - private Span? _span; + private Span? _span; - public ViewSpanChangedEventSource(IThreadingContext threadingContext, ITextView textView) - { - Debug.Assert(textView != null); - _threadingContext = threadingContext; - _textView = textView; - } + public ViewSpanChangedEventSource(IThreadingContext threadingContext, ITextView textView) + { + Debug.Assert(textView != null); + _threadingContext = threadingContext; + _textView = textView; + } - public override void Connect() - { - _threadingContext.ThrowIfNotOnUIThread(); - _textView.LayoutChanged += OnLayoutChanged; - } + public override void Connect() + { + _threadingContext.ThrowIfNotOnUIThread(); + _textView.LayoutChanged += OnLayoutChanged; + } - public override void Disconnect() - { - _threadingContext.ThrowIfNotOnUIThread(); - _textView.LayoutChanged -= OnLayoutChanged; - } + public override void Disconnect() + { + _threadingContext.ThrowIfNotOnUIThread(); + _textView.LayoutChanged -= OnLayoutChanged; + } - private void OnLayoutChanged(object? sender, TextViewLayoutChangedEventArgs e) - { - _threadingContext.ThrowIfNotOnUIThread(); - // The formatted span refers to the span of the textview's buffer that is visible. - // If it changes, then we want to reclassify. Note: the span might not change if - // text were overwritten. However, in the case of text-edits, we'll hear about - // through other means as we have an EventSource for that purpose. This event - // source is for knowing if the user moves the view around. This handles direct - // moves using the caret/scrollbar, as well as moves that happen because someone - // jumped directly to a location using goto-def. It also handles view changes - // caused by the user collapsing an outlining region. + private void OnLayoutChanged(object? sender, TextViewLayoutChangedEventArgs e) + { + _threadingContext.ThrowIfNotOnUIThread(); + // The formatted span refers to the span of the textview's buffer that is visible. + // If it changes, then we want to reclassify. Note: the span might not change if + // text were overwritten. However, in the case of text-edits, we'll hear about + // through other means as we have an EventSource for that purpose. This event + // source is for knowing if the user moves the view around. This handles direct + // moves using the caret/scrollbar, as well as moves that happen because someone + // jumped directly to a location using goto-def. It also handles view changes + // caused by the user collapsing an outlining region. - var lastSpan = _span; - _span = _textView.TextViewLines.FormattedSpan.Span; + var lastSpan = _span; + _span = _textView.TextViewLines.FormattedSpan.Span; - if (_span != lastSpan) - { - // The span changed. This could have happened for a few different reasons. - // If none of the view's text snapshots changed, then it was because of scrolling. - RaiseChanged(); - } + if (_span != lastSpan) + { + // The span changed. This could have happened for a few different reasons. + // If none of the view's text snapshots changed, then it was because of scrolling. + RaiseChanged(); } } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.WorkspaceChangedEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.WorkspaceChangedEventSource.cs index a070bffead4ee..2c627b9a63a9e 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.WorkspaceChangedEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.WorkspaceChangedEventSource.cs @@ -7,46 +7,45 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private class WorkspaceChangedEventSource : AbstractWorkspaceTrackingTaggerEventSource { - private class WorkspaceChangedEventSource : AbstractWorkspaceTrackingTaggerEventSource - { - private readonly AsyncBatchingWorkQueue _asyncDelay; - - public WorkspaceChangedEventSource( - ITextBuffer subjectBuffer, - IAsynchronousOperationListener asyncListener) - : base(subjectBuffer) - { - // That will ensure that even if we get a flurry of workspace events that we - // only process a tag change once. - _asyncDelay = new AsyncBatchingWorkQueue( - DelayTimeSpan.Short, - processBatchAsync: cancellationToken => - { - RaiseChanged(); - return ValueTaskFactory.CompletedTask; - }, - asyncListener, - CancellationToken.None); - } + private readonly AsyncBatchingWorkQueue _asyncDelay; - protected override void ConnectToWorkspace(Workspace workspace) - { - workspace.WorkspaceChanged += OnWorkspaceChanged; - this.RaiseChanged(); - } + public WorkspaceChangedEventSource( + ITextBuffer subjectBuffer, + IAsynchronousOperationListener asyncListener) + : base(subjectBuffer) + { + // That will ensure that even if we get a flurry of workspace events that we + // only process a tag change once. + _asyncDelay = new AsyncBatchingWorkQueue( + DelayTimeSpan.Short, + processBatchAsync: cancellationToken => + { + RaiseChanged(); + return ValueTaskFactory.CompletedTask; + }, + asyncListener, + CancellationToken.None); + } - protected override void DisconnectFromWorkspace(Workspace workspace) - { - workspace.WorkspaceChanged -= OnWorkspaceChanged; - this.RaiseChanged(); - } + protected override void ConnectToWorkspace(Workspace workspace) + { + workspace.WorkspaceChanged += OnWorkspaceChanged; + this.RaiseChanged(); + } - private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs eventArgs) - => _asyncDelay.AddWork(); + protected override void DisconnectFromWorkspace(Workspace workspace) + { + workspace.WorkspaceChanged -= OnWorkspaceChanged; + this.RaiseChanged(); } + + private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs eventArgs) + => _asyncDelay.AddWork(); } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.WorkspaceRegistrationChangedEventSource.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.WorkspaceRegistrationChangedEventSource.cs index 3d73c68609aa2..d6f888532daa3 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.WorkspaceRegistrationChangedEventSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.WorkspaceRegistrationChangedEventSource.cs @@ -4,17 +4,16 @@ using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TaggerEventSources { - internal partial class TaggerEventSources + private class WorkspaceRegistrationChangedEventSource(ITextBuffer subjectBuffer) : AbstractWorkspaceTrackingTaggerEventSource(subjectBuffer) { - private class WorkspaceRegistrationChangedEventSource(ITextBuffer subjectBuffer) : AbstractWorkspaceTrackingTaggerEventSource(subjectBuffer) - { - protected override void ConnectToWorkspace(Workspace workspace) - => this.RaiseChanged(); + protected override void ConnectToWorkspace(Workspace workspace) + => this.RaiseChanged(); - protected override void DisconnectFromWorkspace(Workspace workspace) - => this.RaiseChanged(); - } + protected override void DisconnectFromWorkspace(Workspace workspace) + => this.RaiseChanged(); } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.cs b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.cs index 62e1766046caa..d36975a3afa9d 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/EventSources/TaggerEventSources.cs @@ -13,53 +13,52 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal static partial class TaggerEventSources { - internal static partial class TaggerEventSources + public static ITaggerEventSource Compose( + params ITaggerEventSource[] eventSources) { - public static ITaggerEventSource Compose( - params ITaggerEventSource[] eventSources) - { - return new CompositionEventSource(eventSources); - } + return new CompositionEventSource(eventSources); + } - public static ITaggerEventSource Compose(IEnumerable eventSources) - => new CompositionEventSource(eventSources.ToArray()); + public static ITaggerEventSource Compose(IEnumerable eventSources) + => new CompositionEventSource(eventSources.ToArray()); - public static ITaggerEventSource OnCaretPositionChanged(ITextView textView, ITextBuffer subjectBuffer) - => new CaretPositionChangedEventSource(textView, subjectBuffer); + public static ITaggerEventSource OnCaretPositionChanged(ITextView textView, ITextBuffer subjectBuffer) + => new CaretPositionChangedEventSource(textView, subjectBuffer); - public static ITaggerEventSource OnTextChanged(ITextBuffer subjectBuffer) - => new TextChangedEventSource(subjectBuffer); + public static ITaggerEventSource OnTextChanged(ITextBuffer subjectBuffer) + => new TextChangedEventSource(subjectBuffer); - /// - /// Reports an event any time the workspace changes. - /// - public static ITaggerEventSource OnWorkspaceChanged(ITextBuffer subjectBuffer, IAsynchronousOperationListener listener) - => new WorkspaceChangedEventSource(subjectBuffer, listener); + /// + /// Reports an event any time the workspace changes. + /// + public static ITaggerEventSource OnWorkspaceChanged(ITextBuffer subjectBuffer, IAsynchronousOperationListener listener) + => new WorkspaceChangedEventSource(subjectBuffer, listener); - public static ITaggerEventSource OnDocumentActiveContextChanged(ITextBuffer subjectBuffer) - => new DocumentActiveContextChangedEventSource(subjectBuffer); + public static ITaggerEventSource OnDocumentActiveContextChanged(ITextBuffer subjectBuffer) + => new DocumentActiveContextChangedEventSource(subjectBuffer); - public static ITaggerEventSource OnSelectionChanged(ITextView textView) - => new SelectionChangedEventSource(textView); + public static ITaggerEventSource OnSelectionChanged(ITextView textView) + => new SelectionChangedEventSource(textView); - public static ITaggerEventSource OnReadOnlyRegionsChanged(ITextBuffer subjectBuffer) - => new ReadOnlyRegionsChangedEventSource(subjectBuffer); + public static ITaggerEventSource OnReadOnlyRegionsChanged(ITextBuffer subjectBuffer) + => new ReadOnlyRegionsChangedEventSource(subjectBuffer); - public static ITaggerEventSource OnGlobalOptionChanged(IGlobalOptionService globalOptions, IOption2 globalOption) - => new GlobalOptionChangedEventSource(globalOptions, globalOption); + public static ITaggerEventSource OnGlobalOptionChanged(IGlobalOptionService globalOptions, IOption2 globalOption) + => new GlobalOptionChangedEventSource(globalOptions, globalOption); - public static ITaggerEventSource OnDiagnosticsChanged(ITextBuffer subjectBuffer, IDiagnosticService service) - => new DiagnosticsChangedEventSource(subjectBuffer, service); + public static ITaggerEventSource OnDiagnosticsChanged(ITextBuffer subjectBuffer, IDiagnosticService service) + => new DiagnosticsChangedEventSource(subjectBuffer, service); - public static ITaggerEventSource OnParseOptionChanged(ITextBuffer subjectBuffer) - => new ParseOptionChangedEventSource(subjectBuffer); + public static ITaggerEventSource OnParseOptionChanged(ITextBuffer subjectBuffer) + => new ParseOptionChangedEventSource(subjectBuffer); - public static ITaggerEventSource OnWorkspaceRegistrationChanged(ITextBuffer subjectBuffer) - => new WorkspaceRegistrationChangedEventSource(subjectBuffer); + public static ITaggerEventSource OnWorkspaceRegistrationChanged(ITextBuffer subjectBuffer) + => new WorkspaceRegistrationChangedEventSource(subjectBuffer); - public static ITaggerEventSource OnViewSpanChanged(IThreadingContext threadingContext, ITextView textView) - => new ViewSpanChangedEventSource(threadingContext, textView); - } + public static ITaggerEventSource OnViewSpanChanged(IThreadingContext threadingContext, ITextView textView) + => new ViewSpanChangedEventSource(threadingContext, textView); } diff --git a/src/EditorFeatures/Core/Shared/Tagging/Tags/ConflictTag.cs b/src/EditorFeatures/Core/Shared/Tagging/Tags/ConflictTag.cs index f8218c5014bca..cec9feaee9244 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/Tags/ConflictTag.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/Tags/ConflictTag.cs @@ -4,17 +4,16 @@ using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal class ConflictTag : TextMarkerTag { - internal class ConflictTag : TextMarkerTag - { - public const string TagId = "RoslynConflictTag"; + public const string TagId = "RoslynConflictTag"; - public static readonly ConflictTag Instance = new(); + public static readonly ConflictTag Instance = new(); - private ConflictTag() - : base(TagId) - { - } + private ConflictTag() + : base(TagId) + { } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/Tags/NavigableHighlightTag.cs b/src/EditorFeatures/Core/Shared/Tagging/Tags/NavigableHighlightTag.cs index c449f99558239..ab804f375c399 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/Tags/NavigableHighlightTag.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/Tags/NavigableHighlightTag.cs @@ -4,18 +4,17 @@ using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +/// +/// The base type of any text marker tags that can be navigated with Ctrl+Shift+Up and Ctrl+Shift+Down. +/// +/// +/// Unless you are writing code relating to reference or keyword highlighting, you should not be using +/// this type. +internal abstract class NavigableHighlightTag : TextMarkerTag { - /// - /// The base type of any text marker tags that can be navigated with Ctrl+Shift+Up and Ctrl+Shift+Down. - /// - /// - /// Unless you are writing code relating to reference or keyword highlighting, you should not be using - /// this type. - internal abstract class NavigableHighlightTag : TextMarkerTag + protected NavigableHighlightTag(string type) : base(type) { - protected NavigableHighlightTag(string type) : base(type) - { - } } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/Tags/PreviewWarningTag.cs b/src/EditorFeatures/Core/Shared/Tagging/Tags/PreviewWarningTag.cs index cd1e128bab1a4..122f4831adc47 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/Tags/PreviewWarningTag.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/Tags/PreviewWarningTag.cs @@ -4,17 +4,16 @@ using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal class PreviewWarningTag : TextMarkerTag { - internal class PreviewWarningTag : TextMarkerTag - { - public const string TagId = "RoslynPreviewWarningTag"; + public const string TagId = "RoslynPreviewWarningTag"; - public static readonly PreviewWarningTag Instance = new(); + public static readonly PreviewWarningTag Instance = new(); - private PreviewWarningTag() - : base(TagId) - { - } + private PreviewWarningTag() + : base(TagId) + { } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.IntervalIntrospector.cs b/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.IntervalIntrospector.cs index 610bb75d938aa..a23b0b3109db7 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.IntervalIntrospector.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.IntervalIntrospector.cs @@ -5,19 +5,18 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TagSpanIntervalTree { - internal partial class TagSpanIntervalTree + private readonly struct IntervalIntrospector(ITextSnapshot snapshot) : IIntervalIntrospector { - private readonly struct IntervalIntrospector(ITextSnapshot snapshot) : IIntervalIntrospector - { - public readonly ITextSnapshot Snapshot = snapshot; + public readonly ITextSnapshot Snapshot = snapshot; - public int GetStart(TagNode value) - => value.GetStart(this.Snapshot); + public int GetStart(TagNode value) + => value.GetStart(this.Snapshot); - public int GetLength(TagNode value) - => value.GetLength(this.Snapshot); - } + public int GetLength(TagNode value) + => value.GetLength(this.Snapshot); } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.TagNode.cs b/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.TagNode.cs index 4a180049449ef..31ad719a6512c 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.TagNode.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.TagNode.cs @@ -6,46 +6,45 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +internal partial class TagSpanIntervalTree { - internal partial class TagSpanIntervalTree + private class TagNode(ITagSpan ts, SpanTrackingMode trackingMode) { - private class TagNode(ITagSpan ts, SpanTrackingMode trackingMode) - { - public readonly TTag Tag = ts.Tag; - public readonly ITrackingSpan Span = ts.Span.CreateTrackingSpan(trackingMode); - private SnapshotSpan _snapshotSpan = ts.Span; + public readonly TTag Tag = ts.Tag; + public readonly ITrackingSpan Span = ts.Span.CreateTrackingSpan(trackingMode); + private SnapshotSpan _snapshotSpan = ts.Span; - private SnapshotSpan GetSnapshotSpan(ITextSnapshot textSnapshot) + private SnapshotSpan GetSnapshotSpan(ITextSnapshot textSnapshot) + { + var localSpan = _snapshotSpan; + if (localSpan.Snapshot == textSnapshot) { - var localSpan = _snapshotSpan; - if (localSpan.Snapshot == textSnapshot) - { - return localSpan; - } - else if (localSpan.Snapshot != null) - { - _snapshotSpan = default; - } - - return default; + return localSpan; } - - internal int GetStart(ITextSnapshot textSnapshot) + else if (localSpan.Snapshot != null) { - var localSpan = this.GetSnapshotSpan(textSnapshot); - return localSpan.Snapshot == textSnapshot - ? localSpan.Start - : this.Span.GetStartPoint(textSnapshot); + _snapshotSpan = default; } - internal int GetLength(ITextSnapshot textSnapshot) - { - var localSpan = this.GetSnapshotSpan(textSnapshot); - return localSpan.Snapshot == textSnapshot - ? localSpan.Length - : this.Span.GetSpan(textSnapshot).Length; - } + return default; + } + + internal int GetStart(ITextSnapshot textSnapshot) + { + var localSpan = this.GetSnapshotSpan(textSnapshot); + return localSpan.Snapshot == textSnapshot + ? localSpan.Start + : this.Span.GetStartPoint(textSnapshot); + } + + internal int GetLength(ITextSnapshot textSnapshot) + { + var localSpan = this.GetSnapshotSpan(textSnapshot); + return localSpan.Snapshot == textSnapshot + ? localSpan.Length + : this.Span.GetSpan(textSnapshot).Length; } } } diff --git a/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs b/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs index e3ea78ecce785..8ec9a5537670b 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs @@ -13,189 +13,188 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging +namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; + +/// +/// A tag span interval tree represents an ordered tree data structure to store tag spans in. It +/// allows you to efficiently find all tag spans that intersect a provided span. Tag spans are +/// tracked. That way you can query for intersecting/overlapping spans in a different snapshot +/// than the one for the tag spans that were added. +/// +internal partial class TagSpanIntervalTree where TTag : ITag { - /// - /// A tag span interval tree represents an ordered tree data structure to store tag spans in. It - /// allows you to efficiently find all tag spans that intersect a provided span. Tag spans are - /// tracked. That way you can query for intersecting/overlapping spans in a different snapshot - /// than the one for the tag spans that were added. - /// - internal partial class TagSpanIntervalTree where TTag : ITag + private readonly IntervalTree _tree; + private readonly ITextBuffer _textBuffer; + private readonly SpanTrackingMode _spanTrackingMode; + + public TagSpanIntervalTree(ITextBuffer textBuffer, + SpanTrackingMode trackingMode, + IEnumerable>? values = null) { - private readonly IntervalTree _tree; - private readonly ITextBuffer _textBuffer; - private readonly SpanTrackingMode _spanTrackingMode; + _textBuffer = textBuffer; + _spanTrackingMode = trackingMode; - public TagSpanIntervalTree(ITextBuffer textBuffer, - SpanTrackingMode trackingMode, - IEnumerable>? values = null) - { - _textBuffer = textBuffer; - _spanTrackingMode = trackingMode; + var nodeValues = values?.Select(ts => new TagNode(ts, trackingMode)); - var nodeValues = values?.Select(ts => new TagNode(ts, trackingMode)); + _tree = IntervalTree.Create(new IntervalIntrospector(textBuffer.CurrentSnapshot), nodeValues); + } - _tree = IntervalTree.Create(new IntervalIntrospector(textBuffer.CurrentSnapshot), nodeValues); - } + public ITextBuffer Buffer => _textBuffer; - public ITextBuffer Buffer => _textBuffer; + public SpanTrackingMode SpanTrackingMode => _spanTrackingMode; - public SpanTrackingMode SpanTrackingMode => _spanTrackingMode; + public bool HasSpanThatContains(SnapshotPoint point) + { + var snapshot = point.Snapshot; + Debug.Assert(snapshot.TextBuffer == _textBuffer); - public bool HasSpanThatContains(SnapshotPoint point) - { - var snapshot = point.Snapshot; - Debug.Assert(snapshot.TextBuffer == _textBuffer); + return _tree.HasIntervalThatContains(point.Position, length: 0, new IntervalIntrospector(snapshot)); + } - return _tree.HasIntervalThatContains(point.Position, length: 0, new IntervalIntrospector(snapshot)); - } + public IList> GetIntersectingSpans(SnapshotSpan snapshotSpan) + => SegmentedListPool.ComputeList( + static (args, tags) => args.@this.AppendIntersectingSpansInSortedOrder(args.snapshotSpan, tags), + (@this: this, snapshotSpan), + _: (ITagSpan?)null); - public IList> GetIntersectingSpans(SnapshotSpan snapshotSpan) - => SegmentedListPool.ComputeList( - static (args, tags) => args.@this.AppendIntersectingSpansInSortedOrder(args.snapshotSpan, tags), - (@this: this, snapshotSpan), - _: (ITagSpan?)null); - - /// - /// Gets all the spans that intersect with in sorted order and adds them to - /// . Note the sorted chunk of items are appended to . This - /// means that may not be sorted if there were already items in them. - /// - private void AppendIntersectingSpansInSortedOrder(SnapshotSpan snapshotSpan, SegmentedList> result) - { - var snapshot = snapshotSpan.Snapshot; - Debug.Assert(snapshot.TextBuffer == _textBuffer); + /// + /// Gets all the spans that intersect with in sorted order and adds them to + /// . Note the sorted chunk of items are appended to . This + /// means that may not be sorted if there were already items in them. + /// + private void AppendIntersectingSpansInSortedOrder(SnapshotSpan snapshotSpan, SegmentedList> result) + { + var snapshot = snapshotSpan.Snapshot; + Debug.Assert(snapshot.TextBuffer == _textBuffer); - using var intersectingIntervals = TemporaryArray.Empty; - _tree.FillWithIntervalsThatIntersectWith( - snapshotSpan.Start, snapshotSpan.Length, ref intersectingIntervals.AsRef(), new IntervalIntrospector(snapshot)); + using var intersectingIntervals = TemporaryArray.Empty; + _tree.FillWithIntervalsThatIntersectWith( + snapshotSpan.Start, snapshotSpan.Length, ref intersectingIntervals.AsRef(), new IntervalIntrospector(snapshot)); - foreach (var tagNode in intersectingIntervals) - result.Add(new TagSpan(tagNode.Span.GetSpan(snapshot), tagNode.Tag)); - } + foreach (var tagNode in intersectingIntervals) + result.Add(new TagSpan(tagNode.Span.GetSpan(snapshot), tagNode.Tag)); + } + + public IEnumerable> GetSpans(ITextSnapshot snapshot) + => _tree.Select(tn => new TagSpan(tn.Span.GetSpan(snapshot), tn.Tag)); - public IEnumerable> GetSpans(ITextSnapshot snapshot) - => _tree.Select(tn => new TagSpan(tn.Span.GetSpan(snapshot), tn.Tag)); + public bool IsEmpty() + => _tree.IsEmpty(); - public bool IsEmpty() - => _tree.IsEmpty(); + public void AddIntersectingTagSpans(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) + { + AddIntersectingTagSpansWorker(requestedSpans, tags); + DebugVerifyTags(requestedSpans, tags); + } - public void AddIntersectingTagSpans(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) + [Conditional("DEBUG")] + private static void DebugVerifyTags(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) + { + if (tags == null) { - AddIntersectingTagSpansWorker(requestedSpans, tags); - DebugVerifyTags(requestedSpans, tags); + return; } - [Conditional("DEBUG")] - private static void DebugVerifyTags(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) + foreach (var tag in tags) { - if (tags == null) - { - return; - } + var span = tag.Span; - foreach (var tag in tags) + if (!requestedSpans.Any(s => s.IntersectsWith(span))) { - var span = tag.Span; - - if (!requestedSpans.Any(s => s.IntersectsWith(span))) - { - Contract.Fail(tag + " doesn't intersects with any requested span"); - } + Contract.Fail(tag + " doesn't intersects with any requested span"); } } + } - private void AddIntersectingTagSpansWorker( - NormalizedSnapshotSpanCollection requestedSpans, - SegmentedList> tags) - { - const int MaxNumberOfRequestedSpans = 100; - - // Special case the case where there is only one requested span. In that case, we don't - // need to allocate any intermediate collections - if (requestedSpans.Count == 1) - AppendIntersectingSpansInSortedOrder(requestedSpans[0], tags); - else if (requestedSpans.Count < MaxNumberOfRequestedSpans) - AddTagsForSmallNumberOfSpans(requestedSpans, tags); - else - AddTagsForLargeNumberOfSpans(requestedSpans, tags); - } - - private void AddTagsForSmallNumberOfSpans( - NormalizedSnapshotSpanCollection requestedSpans, - SegmentedList> tags) - { - foreach (var span in requestedSpans) - AppendIntersectingSpansInSortedOrder(span, tags); - } + private void AddIntersectingTagSpansWorker( + NormalizedSnapshotSpanCollection requestedSpans, + SegmentedList> tags) + { + const int MaxNumberOfRequestedSpans = 100; + + // Special case the case where there is only one requested span. In that case, we don't + // need to allocate any intermediate collections + if (requestedSpans.Count == 1) + AppendIntersectingSpansInSortedOrder(requestedSpans[0], tags); + else if (requestedSpans.Count < MaxNumberOfRequestedSpans) + AddTagsForSmallNumberOfSpans(requestedSpans, tags); + else + AddTagsForLargeNumberOfSpans(requestedSpans, tags); + } - private void AddTagsForLargeNumberOfSpans(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) - { - // we are asked with bunch of spans. rather than asking same question again and again, ask once with big span - // which will return superset of what we want. and then filter them out in O(m+n) cost. - // m == number of requested spans, n = number of returned spans - var mergedSpan = new SnapshotSpan(requestedSpans[0].Start, requestedSpans[^1].End); + private void AddTagsForSmallNumberOfSpans( + NormalizedSnapshotSpanCollection requestedSpans, + SegmentedList> tags) + { + foreach (var span in requestedSpans) + AppendIntersectingSpansInSortedOrder(span, tags); + } - using var _1 = SegmentedListPool.GetPooledList>(out var tempList); + private void AddTagsForLargeNumberOfSpans(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) + { + // we are asked with bunch of spans. rather than asking same question again and again, ask once with big span + // which will return superset of what we want. and then filter them out in O(m+n) cost. + // m == number of requested spans, n = number of returned spans + var mergedSpan = new SnapshotSpan(requestedSpans[0].Start, requestedSpans[^1].End); - AppendIntersectingSpansInSortedOrder(mergedSpan, tempList); - if (tempList.Count == 0) - return; + using var _1 = SegmentedListPool.GetPooledList>(out var tempList); - // Note: both 'requstedSpans' and 'tempList' are in sorted order. + AppendIntersectingSpansInSortedOrder(mergedSpan, tempList); + if (tempList.Count == 0) + return; - using var enumerator = tempList.GetEnumerator(); + // Note: both 'requstedSpans' and 'tempList' are in sorted order. - if (!enumerator.MoveNext()) - return; + using var enumerator = tempList.GetEnumerator(); - using var _2 = PooledHashSet>.GetInstance(out var hashSet); + if (!enumerator.MoveNext()) + return; - var requestIndex = 0; - while (true) - { - var currentTag = enumerator.Current; + using var _2 = PooledHashSet>.GetInstance(out var hashSet); - var currentRequestSpan = requestedSpans[requestIndex]; - var currentTagSpan = currentTag.Span; + var requestIndex = 0; + while (true) + { + var currentTag = enumerator.Current; - // The current tag is *before* the current span we're trying to intersect with. Move to the next tag to - // see if it intersects with the current span. - if (currentTagSpan.End < currentRequestSpan.Start) - { - // If there are no more tags, then we're done. - if (!enumerator.MoveNext()) - return; + var currentRequestSpan = requestedSpans[requestIndex]; + var currentTagSpan = currentTag.Span; - continue; - } + // The current tag is *before* the current span we're trying to intersect with. Move to the next tag to + // see if it intersects with the current span. + if (currentTagSpan.End < currentRequestSpan.Start) + { + // If there are no more tags, then we're done. + if (!enumerator.MoveNext()) + return; - // The current tag is *after* teh current span we're trying to intersect with. Move to the next span to - // see if it intersects with the current tag. - if (currentTagSpan.Start > currentRequestSpan.End) - { - requestIndex++; + continue; + } - // If there are no more spans to intersect with, then we're done. - if (requestIndex >= requestedSpans.Count) - return; + // The current tag is *after* teh current span we're trying to intersect with. Move to the next span to + // see if it intersects with the current tag. + if (currentTagSpan.Start > currentRequestSpan.End) + { + requestIndex++; - continue; - } + // If there are no more spans to intersect with, then we're done. + if (requestIndex >= requestedSpans.Count) + return; - // This tag intersects the current span we're trying to intersect with. Ensure we only see and add a - // particular tag once. + continue; + } - if (currentTagSpan.Length > 0 && - hashSet.Add(currentTag)) - { - tags.Add(currentTag); - } + // This tag intersects the current span we're trying to intersect with. Ensure we only see and add a + // particular tag once. - if (!enumerator.MoveNext()) - break; + if (currentTagSpan.Length > 0 && + hashSet.Add(currentTag)) + { + tags.Add(currentTag); } + + if (!enumerator.MoveNext()) + break; } } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/AutomaticCodeChangeMergePolicy.cs b/src/EditorFeatures/Core/Shared/Utilities/AutomaticCodeChangeMergePolicy.cs index ab088277dd08c..78d92aa7b6b6f 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/AutomaticCodeChangeMergePolicy.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/AutomaticCodeChangeMergePolicy.cs @@ -4,35 +4,34 @@ using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +/// +/// a merge policy that should be used for any automatic code changes that could happen in sequences so that +/// all those steps are shown to users as one undo transaction rather than multiple ones +/// +internal class AutomaticCodeChangeMergePolicy : IMergeTextUndoTransactionPolicy { - /// - /// a merge policy that should be used for any automatic code changes that could happen in sequences so that - /// all those steps are shown to users as one undo transaction rather than multiple ones - /// - internal class AutomaticCodeChangeMergePolicy : IMergeTextUndoTransactionPolicy - { - public static readonly AutomaticCodeChangeMergePolicy Instance = new(); + public static readonly AutomaticCodeChangeMergePolicy Instance = new(); - public bool CanMerge(ITextUndoTransaction newerTransaction, ITextUndoTransaction olderTransaction) - { - // We want to merge with any other transaction of our policy type - return true; - } + public bool CanMerge(ITextUndoTransaction newerTransaction, ITextUndoTransaction olderTransaction) + { + // We want to merge with any other transaction of our policy type + return true; + } - public void PerformTransactionMerge(ITextUndoTransaction existingTransaction, ITextUndoTransaction newTransaction) + public void PerformTransactionMerge(ITextUndoTransaction existingTransaction, ITextUndoTransaction newTransaction) + { + // Add all of our commit primitives into the existing transaction + foreach (var primitive in newTransaction.UndoPrimitives) { - // Add all of our commit primitives into the existing transaction - foreach (var primitive in newTransaction.UndoPrimitives) - { - existingTransaction.UndoPrimitives.Add(primitive); - } + existingTransaction.UndoPrimitives.Add(primitive); } + } - public bool TestCompatiblePolicy(IMergeTextUndoTransactionPolicy other) - { - // We are compatible with any other merging policy - return other is AutomaticCodeChangeMergePolicy; - } + public bool TestCompatiblePolicy(IMergeTextUndoTransactionPolicy other) + { + // We are compatible with any other merging policy + return other is AutomaticCodeChangeMergePolicy; } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/CaretPreservingEditTransaction.cs b/src/EditorFeatures/Core/Shared/Utilities/CaretPreservingEditTransaction.cs index 9719cfc3fc891..1d8506f4d1da2 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/CaretPreservingEditTransaction.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/CaretPreservingEditTransaction.cs @@ -6,109 +6,108 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +internal class CaretPreservingEditTransaction : IDisposable { - internal class CaretPreservingEditTransaction : IDisposable + private readonly IEditorOperations _editorOperations; + private readonly ITextUndoHistory? _undoHistory; + private ITextUndoTransaction? _transaction; + private bool _active; + + public CaretPreservingEditTransaction( + string description, + ITextView textView, + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService) + : this(description, undoHistoryRegistry.GetHistory(textView.TextBuffer), editorOperationsFactoryService.GetEditorOperations(textView)) { - private readonly IEditorOperations _editorOperations; - private readonly ITextUndoHistory? _undoHistory; - private ITextUndoTransaction? _transaction; - private bool _active; - - public CaretPreservingEditTransaction( - string description, - ITextView textView, - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService) - : this(description, undoHistoryRegistry.GetHistory(textView.TextBuffer), editorOperationsFactoryService.GetEditorOperations(textView)) - { - } + } - public CaretPreservingEditTransaction(string description, ITextUndoHistory? undoHistory, IEditorOperations editorOperations) - { - _editorOperations = editorOperations; - _undoHistory = undoHistory; - _active = true; + public CaretPreservingEditTransaction(string description, ITextUndoHistory? undoHistory, IEditorOperations editorOperations) + { + _editorOperations = editorOperations; + _undoHistory = undoHistory; + _active = true; - if (_undoHistory != null) - { - _transaction = new HACK_TextUndoTransactionThatRollsBackProperly(_undoHistory.CreateTransaction(description)); - _editorOperations.AddBeforeTextBufferChangePrimitive(); - } + if (_undoHistory != null) + { + _transaction = new HACK_TextUndoTransactionThatRollsBackProperly(_undoHistory.CreateTransaction(description)); + _editorOperations.AddBeforeTextBufferChangePrimitive(); } + } - public static CaretPreservingEditTransaction? TryCreate(string description, - ITextView textView, - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService) + public static CaretPreservingEditTransaction? TryCreate(string description, + ITextView textView, + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService) + { + if (undoHistoryRegistry.TryGetHistory(textView.TextBuffer, out _)) { - if (undoHistoryRegistry.TryGetHistory(textView.TextBuffer, out _)) - { - return new CaretPreservingEditTransaction(description, textView, undoHistoryRegistry, editorOperationsFactoryService); - } - - return null; + return new CaretPreservingEditTransaction(description, textView, undoHistoryRegistry, editorOperationsFactoryService); } - public void Complete() + return null; + } + + public void Complete() + { + if (!_active) { - if (!_active) - { - throw new InvalidOperationException(EditorFeaturesResources.The_transaction_is_already_complete); - } + throw new InvalidOperationException(EditorFeaturesResources.The_transaction_is_already_complete); + } - _editorOperations.AddAfterTextBufferChangePrimitive(); - _transaction?.Complete(); + _editorOperations.AddAfterTextBufferChangePrimitive(); + _transaction?.Complete(); - EndTransaction(); - } + EndTransaction(); + } - public void Cancel() + public void Cancel() + { + if (!_active) { - if (!_active) - { - throw new InvalidOperationException(EditorFeaturesResources.The_transaction_is_already_complete); - } + throw new InvalidOperationException(EditorFeaturesResources.The_transaction_is_already_complete); + } - _transaction?.Cancel(); + _transaction?.Cancel(); - EndTransaction(); - } + EndTransaction(); + } - public void Dispose() + public void Dispose() + { + if (_transaction != null) { - if (_transaction != null) - { - // If the transaction is still pending, we'll cancel it - Cancel(); - } + // If the transaction is still pending, we'll cancel it + Cancel(); } + } - public IMergeTextUndoTransactionPolicy? MergePolicy + public IMergeTextUndoTransactionPolicy? MergePolicy + { + get { - get - { - return _transaction?.MergePolicy; - } - - set - { - if (_transaction != null) - { - _transaction.MergePolicy = value; - } - } + return _transaction?.MergePolicy; } - private void EndTransaction() + set { if (_transaction != null) { - _transaction.Dispose(); - _transaction = null; + _transaction.MergePolicy = value; } + } + } - _active = false; + private void EndTransaction() + { + if (_transaction != null) + { + _transaction.Dispose(); + _transaction = null; } + + _active = false; } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/CommonFormattingHelpers.cs b/src/EditorFeatures/Core/Shared/Utilities/CommonFormattingHelpers.cs index 3bf66e66f17b1..0bc2408bc2f43 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/CommonFormattingHelpers.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/CommonFormattingHelpers.cs @@ -8,51 +8,50 @@ using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +internal static class CommonFormattingHelpers { - internal static class CommonFormattingHelpers + public static TextSpan GetFormattingSpan(SyntaxNode root, TextSpan span) + => CodeAnalysis.Shared.Utilities.CommonFormattingHelpers.GetFormattingSpan(root, span); + + public static TextSpan GetFormattingSpan(ITextSnapshot snapshot, SnapshotSpan selectedSpan) { - public static TextSpan GetFormattingSpan(SyntaxNode root, TextSpan span) - => CodeAnalysis.Shared.Utilities.CommonFormattingHelpers.GetFormattingSpan(root, span); + var currentLine = snapshot.GetLineFromPosition(selectedSpan.Start); + var endPosition = selectedSpan.IsEmpty ? currentLine.End : selectedSpan.End; + var previousLine = GetNonEmptyPreviousLine(snapshot, currentLine); + + // first line on screen + if (currentLine == previousLine) + { + return TextSpan.FromBounds(currentLine.Start, endPosition); + } + + var lastNonNoisyCharPosition = previousLine.GetLastNonWhitespacePosition().GetValueOrDefault(); + return TextSpan.FromBounds(lastNonNoisyCharPosition, endPosition); + } - public static TextSpan GetFormattingSpan(ITextSnapshot snapshot, SnapshotSpan selectedSpan) + public static ITextSnapshotLine GetNonEmptyPreviousLine(ITextSnapshot snapshot, ITextSnapshotLine currentLine) + { + do { - var currentLine = snapshot.GetLineFromPosition(selectedSpan.Start); - var endPosition = selectedSpan.IsEmpty ? currentLine.End : selectedSpan.End; - var previousLine = GetNonEmptyPreviousLine(snapshot, currentLine); + var previousLine = snapshot.GetLineFromLineNumber(Math.Max(currentLine.LineNumber - 1, 0)); - // first line on screen - if (currentLine == previousLine) + // first line in the file + if (previousLine.LineNumber == currentLine.LineNumber) { - return TextSpan.FromBounds(currentLine.Start, endPosition); + return currentLine; } - var lastNonNoisyCharPosition = previousLine.GetLastNonWhitespacePosition().GetValueOrDefault(); - return TextSpan.FromBounds(lastNonNoisyCharPosition, endPosition); - } - - public static ITextSnapshotLine GetNonEmptyPreviousLine(ITextSnapshot snapshot, ITextSnapshotLine currentLine) - { - do + if (previousLine.IsEmptyOrWhitespace()) { - var previousLine = snapshot.GetLineFromLineNumber(Math.Max(currentLine.LineNumber - 1, 0)); - - // first line in the file - if (previousLine.LineNumber == currentLine.LineNumber) - { - return currentLine; - } - - if (previousLine.IsEmptyOrWhitespace()) - { - // keep goes up until it find non empty previous line - currentLine = previousLine; - continue; - } - - return previousLine; + // keep goes up until it find non empty previous line + currentLine = previousLine; + continue; } - while (true); + + return previousLine; } + while (true); } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/ForegroundThreadAffinitizedObject.cs b/src/EditorFeatures/Core/Shared/Utilities/ForegroundThreadAffinitizedObject.cs index 2a0594e89ca6e..fd97c32227d96 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/ForegroundThreadAffinitizedObject.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/ForegroundThreadAffinitizedObject.cs @@ -8,108 +8,107 @@ using System.Threading.Tasks; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +/// +/// Base class that allows some helpers for detecting whether we're on the main WPF foreground thread, or +/// a background thread. It also allows scheduling work to the foreground thread at below input priority. +/// +internal class ForegroundThreadAffinitizedObject { - /// - /// Base class that allows some helpers for detecting whether we're on the main WPF foreground thread, or - /// a background thread. It also allows scheduling work to the foreground thread at below input priority. - /// - internal class ForegroundThreadAffinitizedObject - { - private readonly IThreadingContext _threadingContext; + private readonly IThreadingContext _threadingContext; - internal IThreadingContext ThreadingContext => _threadingContext; + internal IThreadingContext ThreadingContext => _threadingContext; - public ForegroundThreadAffinitizedObject(IThreadingContext threadingContext, bool assertIsForeground = false) - { - _threadingContext = threadingContext ?? throw new ArgumentNullException(nameof(threadingContext)); + public ForegroundThreadAffinitizedObject(IThreadingContext threadingContext, bool assertIsForeground = false) + { + _threadingContext = threadingContext ?? throw new ArgumentNullException(nameof(threadingContext)); - // ForegroundThreadAffinitizedObject might not necessarily be created on a foreground thread. - // AssertIsForeground here only if the object must be created on a foreground thread. - if (assertIsForeground) - { - // Assert we have some kind of foreground thread - Contract.ThrowIfFalse(threadingContext.HasMainThread); + // ForegroundThreadAffinitizedObject might not necessarily be created on a foreground thread. + // AssertIsForeground here only if the object must be created on a foreground thread. + if (assertIsForeground) + { + // Assert we have some kind of foreground thread + Contract.ThrowIfFalse(threadingContext.HasMainThread); - AssertIsForeground(); - } + AssertIsForeground(); } + } - public bool IsForeground() - => _threadingContext.JoinableTaskContext.IsOnMainThread; + public bool IsForeground() + => _threadingContext.JoinableTaskContext.IsOnMainThread; - public void AssertIsForeground() - { - var whenCreatedThread = _threadingContext.JoinableTaskContext.MainThread; - var currentThread = Thread.CurrentThread; - - // In debug, provide a lot more information so that we can track down unit test flakiness. - // This is too expensive to do in retail as it creates way too many allocations. - Debug.Assert(currentThread == whenCreatedThread, - "When created thread id : " + whenCreatedThread?.ManagedThreadId + "\r\n" + - "When created thread name: " + whenCreatedThread?.Name + "\r\n" + - "Current thread id : " + currentThread?.ManagedThreadId + "\r\n" + - "Current thread name : " + currentThread?.Name); - - // But, in retail, do the check as well, so that we can catch problems that happen in the wild. - Contract.ThrowIfFalse(_threadingContext.JoinableTaskContext.IsOnMainThread); - } + public void AssertIsForeground() + { + var whenCreatedThread = _threadingContext.JoinableTaskContext.MainThread; + var currentThread = Thread.CurrentThread; + + // In debug, provide a lot more information so that we can track down unit test flakiness. + // This is too expensive to do in retail as it creates way too many allocations. + Debug.Assert(currentThread == whenCreatedThread, + "When created thread id : " + whenCreatedThread?.ManagedThreadId + "\r\n" + + "When created thread name: " + whenCreatedThread?.Name + "\r\n" + + "Current thread id : " + currentThread?.ManagedThreadId + "\r\n" + + "Current thread name : " + currentThread?.Name); + + // But, in retail, do the check as well, so that we can catch problems that happen in the wild. + Contract.ThrowIfFalse(_threadingContext.JoinableTaskContext.IsOnMainThread); + } + + public void AssertIsBackground() + => Contract.ThrowIfTrue(IsForeground()); - public void AssertIsBackground() - => Contract.ThrowIfTrue(IsForeground()); + /// + /// A helpful marker method that can be used by deriving classes to indicate that a + /// method can be called from any thread and is not foreground or background affinitized. + /// This is useful so that every method in deriving class can have some sort of marker + /// on each method stating the threading constraints (FG-only/BG-only/Any-thread). + /// + public static void ThisCanBeCalledOnAnyThread() + { + // Does nothing. + } - /// - /// A helpful marker method that can be used by deriving classes to indicate that a - /// method can be called from any thread and is not foreground or background affinitized. - /// This is useful so that every method in deriving class can have some sort of marker - /// on each method stating the threading constraints (FG-only/BG-only/Any-thread). - /// - public static void ThisCanBeCalledOnAnyThread() + public Task InvokeBelowInputPriorityAsync(Action action, CancellationToken cancellationToken = default) + { + if (IsForeground() && !IsInputPending()) { - // Does nothing. - } + // Optimize to inline the action if we're already on the foreground thread + // and there's no pending user input. + action(); - public Task InvokeBelowInputPriorityAsync(Action action, CancellationToken cancellationToken = default) + return Task.CompletedTask; + } + else { - if (IsForeground() && !IsInputPending()) - { - // Optimize to inline the action if we're already on the foreground thread - // and there's no pending user input. - action(); - - return Task.CompletedTask; - } - else - { - return Task.Factory.SafeStartNewFromAsync( - async () => - { - await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - - action(); - }, - cancellationToken, - TaskScheduler.Default); - } + return Task.Factory.SafeStartNewFromAsync( + async () => + { + await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + action(); + }, + cancellationToken, + TaskScheduler.Default); } + } - /// - /// Returns true if any keyboard or mouse button input is pending on the message queue. - /// - protected static bool IsInputPending() + /// + /// Returns true if any keyboard or mouse button input is pending on the message queue. + /// + protected static bool IsInputPending() + { + // The code below invokes into user32.dll, which is not available in non-Windows. + if (PlatformInformation.IsUnix) { - // The code below invokes into user32.dll, which is not available in non-Windows. - if (PlatformInformation.IsUnix) - { - return false; - } - - // The return value of GetQueueStatus is HIWORD:LOWORD. - // A non-zero value in HIWORD indicates some input message in the queue. - var result = NativeMethods.GetQueueStatus(NativeMethods.QS_INPUT); - - const uint InputMask = NativeMethods.QS_INPUT | (NativeMethods.QS_INPUT << 16); - return (result & InputMask) != 0; + return false; } + + // The return value of GetQueueStatus is HIWORD:LOWORD. + // A non-zero value in HIWORD indicates some input message in the queue. + var result = NativeMethods.GetQueueStatus(NativeMethods.QS_INPUT); + + const uint InputMask = NativeMethods.QS_INPUT | (NativeMethods.QS_INPUT << 16); + return (result & InputMask) != 0; } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/HACK_TextUndoTransactionThatRollsBackProperly.cs b/src/EditorFeatures/Core/Shared/Utilities/HACK_TextUndoTransactionThatRollsBackProperly.cs index fff5f483cb9e9..30a518502a742 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/HACK_TextUndoTransactionThatRollsBackProperly.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/HACK_TextUndoTransactionThatRollsBackProperly.cs @@ -7,132 +7,131 @@ using System.Linq; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +/// +/// An implementation of that wraps another +/// . Some undo implementations (notably the VS implementation) +/// violate the specified contract for Cancel(), which states that cancelling an active transaction +/// should undo the primitives that we already added. This works around that problem; calling Cancel() +/// on this forwards the cancellation to the inner transaction, and if it failed to roll back we +/// do it ourselves. +/// +internal sealed class HACK_TextUndoTransactionThatRollsBackProperly(ITextUndoTransaction innerTransaction) : ITextUndoTransaction { - /// - /// An implementation of that wraps another - /// . Some undo implementations (notably the VS implementation) - /// violate the specified contract for Cancel(), which states that cancelling an active transaction - /// should undo the primitives that we already added. This works around that problem; calling Cancel() - /// on this forwards the cancellation to the inner transaction, and if it failed to roll back we - /// do it ourselves. - /// - internal sealed class HACK_TextUndoTransactionThatRollsBackProperly(ITextUndoTransaction innerTransaction) : ITextUndoTransaction - { - private readonly ITextUndoTransaction _innerTransaction = innerTransaction; - private readonly RollbackDetectingUndoPrimitive _undoPrimitive = new(); + private readonly ITextUndoTransaction _innerTransaction = innerTransaction; + private readonly RollbackDetectingUndoPrimitive _undoPrimitive = new(); - private bool _transactionOpen = true; + private bool _transactionOpen = true; - public bool CanRedo => _innerTransaction.CanRedo; + public bool CanRedo => _innerTransaction.CanRedo; - public bool CanUndo => _innerTransaction.CanUndo; + public bool CanUndo => _innerTransaction.CanUndo; - public string Description + public string Description + { + get { - get - { - return _innerTransaction.Description; - } + return _innerTransaction.Description; + } - set - { - _innerTransaction.Description = value; - } + set + { + _innerTransaction.Description = value; } + } - public ITextUndoHistory History => _innerTransaction.History; + public ITextUndoHistory History => _innerTransaction.History; - public IMergeTextUndoTransactionPolicy MergePolicy + public IMergeTextUndoTransactionPolicy MergePolicy + { + get { - get - { - return _innerTransaction.MergePolicy; - } + return _innerTransaction.MergePolicy; + } - set - { - _innerTransaction.MergePolicy = value; - } + set + { + _innerTransaction.MergePolicy = value; } + } - public ITextUndoTransaction Parent => _innerTransaction.Parent; + public ITextUndoTransaction Parent => _innerTransaction.Parent; - public UndoTransactionState State => _innerTransaction.State; + public UndoTransactionState State => _innerTransaction.State; - public IList UndoPrimitives => _innerTransaction.UndoPrimitives; + public IList UndoPrimitives => _innerTransaction.UndoPrimitives; - public void AddUndo(ITextUndoPrimitive undo) - => _innerTransaction.AddUndo(undo); + public void AddUndo(ITextUndoPrimitive undo) + => _innerTransaction.AddUndo(undo); - public void Cancel() - { - var transactionWasOpen = _transactionOpen; - _transactionOpen = false; + public void Cancel() + { + var transactionWasOpen = _transactionOpen; + _transactionOpen = false; - // First, add an undo primitive so we can detect whether or not undo gets called - if (transactionWasOpen) - { - _innerTransaction.AddUndo(_undoPrimitive); - } + // First, add an undo primitive so we can detect whether or not undo gets called + if (transactionWasOpen) + { + _innerTransaction.AddUndo(_undoPrimitive); + } - _innerTransaction.Cancel(); + _innerTransaction.Cancel(); - if (transactionWasOpen && !_undoPrimitive.UndoCalled) + if (transactionWasOpen && !_undoPrimitive.UndoCalled) + { + // Undo each of the primitives in reverse order to clean up. This is slimy. + foreach (var primitive in _innerTransaction.UndoPrimitives.Reverse()) { - // Undo each of the primitives in reverse order to clean up. This is slimy. - foreach (var primitive in _innerTransaction.UndoPrimitives.Reverse()) - { - primitive.Undo(); - } + primitive.Undo(); } } + } - public void Complete() - { - _transactionOpen = false; - _innerTransaction.Complete(); - } + public void Complete() + { + _transactionOpen = false; + _innerTransaction.Complete(); + } - public void Dispose() + public void Dispose() + { + if (_transactionOpen) { - if (_transactionOpen) - { - // Call our cancel method first to ensure we handle it properly - Cancel(); - } - - _innerTransaction.Dispose(); + // Call our cancel method first to ensure we handle it properly + Cancel(); } - public void Do() - => _innerTransaction.Do(); - - public void Undo() - => _innerTransaction.Undo(); + _innerTransaction.Dispose(); + } - private class RollbackDetectingUndoPrimitive : ITextUndoPrimitive - { - internal bool UndoCalled = false; + public void Do() + => _innerTransaction.Do(); - public bool CanRedo => true; + public void Undo() + => _innerTransaction.Undo(); - public bool CanUndo => true; + private class RollbackDetectingUndoPrimitive : ITextUndoPrimitive + { + internal bool UndoCalled = false; - public ITextUndoTransaction? Parent { get; set; } + public bool CanRedo => true; - public bool CanMerge(ITextUndoPrimitive older) - => false; + public bool CanUndo => true; - public void Do() - { - } + public ITextUndoTransaction? Parent { get; set; } - public ITextUndoPrimitive Merge(ITextUndoPrimitive older) - => throw new NotSupportedException(); + public bool CanMerge(ITextUndoPrimitive older) + => false; - public void Undo() - => UndoCalled = true; + public void Do() + { } + + public ITextUndoPrimitive Merge(ITextUndoPrimitive older) + => throw new NotSupportedException(); + + public void Undo() + => UndoCalled = true; } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/IClassificationTypeMap.cs b/src/EditorFeatures/Core/Shared/Utilities/IClassificationTypeMap.cs index 8be883a6aa36b..fdc38442f822d 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/IClassificationTypeMap.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/IClassificationTypeMap.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities -{ - using Microsoft.VisualStudio.Text.Classification; +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +using Microsoft.VisualStudio.Text.Classification; - internal interface IClassificationTypeMap - { - IClassificationType GetClassificationType(string name); - } +internal interface IClassificationTypeMap +{ + IClassificationType GetClassificationType(string name); } diff --git a/src/EditorFeatures/Core/Shared/Utilities/IThreadingContext.cs b/src/EditorFeatures/Core/Shared/Utilities/IThreadingContext.cs index 4fea485d6c3d9..e697ecd8e6bfc 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/IThreadingContext.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/IThreadingContext.cs @@ -7,68 +7,67 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +/// +/// Provides a which Roslyn code can use for transitioning +/// to the main thread and/or waiting for asynchronous operations when required. +/// +internal interface IThreadingContext { /// - /// Provides a which Roslyn code can use for transitioning - /// to the main thread and/or waiting for asynchronous operations when required. + /// Gets a value indicating whether the threading context is configured with a main thread that can be used for + /// scheduling operations. /// - internal interface IThreadingContext + /// + /// Existence of a main thread is a requirement for correct runtime behavior (in production) of all types + /// that depend on , so code is generally not expected to check this property. + /// However, in some lightweight testing scenarios the main thread will not be used, and the test code avoids + /// setting up the main thread. This property improves the ability to detect incorrectly configured tests (where + /// a main thread is expected but not provided) and produce a meaningful error for developers. + /// + bool HasMainThread { - /// - /// Gets a value indicating whether the threading context is configured with a main thread that can be used for - /// scheduling operations. - /// - /// - /// Existence of a main thread is a requirement for correct runtime behavior (in production) of all types - /// that depend on , so code is generally not expected to check this property. - /// However, in some lightweight testing scenarios the main thread will not be used, and the test code avoids - /// setting up the main thread. This property improves the ability to detect incorrectly configured tests (where - /// a main thread is expected but not provided) and produce a meaningful error for developers. - /// - bool HasMainThread - { - get; - } - - /// - /// Gets the for use in Roslyn code. - /// - JoinableTaskContext JoinableTaskContext - { - get; - } + get; + } - /// - /// Gets the for use in Roslyn code. - /// - JoinableTaskFactory JoinableTaskFactory - { - get; - } + /// + /// Gets the for use in Roslyn code. + /// + JoinableTaskContext JoinableTaskContext + { + get; + } - /// - /// Gets a indicating that disposal has been requested for the threading - /// context. - /// - CancellationToken DisposalToken - { - get; - } + /// + /// Gets the for use in Roslyn code. + /// + JoinableTaskFactory JoinableTaskFactory + { + get; + } - /// - /// Runs an asynchronous operation. If the operation is not complete prior to the time the threading context is - /// disposed, the operation will block the shutdown. - /// - /// - /// The callback function is invoked synchronously prior to this method returning. - /// The entire asynchronous operation performed by will block shutdown of the threading - /// context. - /// - /// If the threading context is already disposed at the time this is called, the operation is cancelled - /// without calling . - /// - /// The callback function that performs an asynchronous operation. - JoinableTask RunWithShutdownBlockAsync(Func func); + /// + /// Gets a indicating that disposal has been requested for the threading + /// context. + /// + CancellationToken DisposalToken + { + get; } + + /// + /// Runs an asynchronous operation. If the operation is not complete prior to the time the threading context is + /// disposed, the operation will block the shutdown. + /// + /// + /// The callback function is invoked synchronously prior to this method returning. + /// The entire asynchronous operation performed by will block shutdown of the threading + /// context. + /// + /// If the threading context is already disposed at the time this is called, the operation is cancelled + /// without calling . + /// + /// The callback function that performs an asynchronous operation. + JoinableTask RunWithShutdownBlockAsync(Func func); } diff --git a/src/EditorFeatures/Core/Shared/Utilities/IWorkspaceContextService.cs b/src/EditorFeatures/Core/Shared/Utilities/IWorkspaceContextService.cs index 956ba965e5a23..cb26b8ef15f19 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/IWorkspaceContextService.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/IWorkspaceContextService.cs @@ -9,37 +9,36 @@ using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +internal interface IWorkspaceContextService : IWorkspaceService +{ + /// + /// Determines if LSP is being used as the editor. + /// Used to disable non-LSP editor feature integration. + /// + bool IsInLspEditorContext(); + + /// + /// Determines if the VS instance is being as a cloud environment client. + /// + bool IsCloudEnvironmentClient(); +} + +[ExportWorkspaceService(typeof(IWorkspaceContextService), ServiceLayer.Default), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class DefaultWorkspaceContextService(IGlobalOptionService globalOptionsService) : IWorkspaceContextService { - internal interface IWorkspaceContextService : IWorkspaceService - { - /// - /// Determines if LSP is being used as the editor. - /// Used to disable non-LSP editor feature integration. - /// - bool IsInLspEditorContext(); - - /// - /// Determines if the VS instance is being as a cloud environment client. - /// - bool IsCloudEnvironmentClient(); - } - - [ExportWorkspaceService(typeof(IWorkspaceContextService), ServiceLayer.Default), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class DefaultWorkspaceContextService(IGlobalOptionService globalOptionsService) : IWorkspaceContextService - { - /// - /// Roslyn LSP feature flag name, as defined in the PackageRegistraion.pkgdef - /// by everything following '$RootKey$\FeatureFlags\' and '\' replaced by '.' - /// - public const string LspEditorFeatureFlagName = "Roslyn.LSP.Editor"; - - private readonly IGlobalOptionService _globalOptionsService = globalOptionsService; - - public bool IsInLspEditorContext() => _globalOptionsService.GetOption(LspOptionsStorage.LspEditorFeatureFlag); - - public bool IsCloudEnvironmentClient() => false; - } + /// + /// Roslyn LSP feature flag name, as defined in the PackageRegistraion.pkgdef + /// by everything following '$RootKey$\FeatureFlags\' and '\' replaced by '.' + /// + public const string LspEditorFeatureFlagName = "Roslyn.LSP.Editor"; + + private readonly IGlobalOptionService _globalOptionsService = globalOptionsService; + + public bool IsInLspEditorContext() => _globalOptionsService.GetOption(LspOptionsStorage.LspEditorFeatureFlag); + + public bool IsCloudEnvironmentClient() => false; } diff --git a/src/EditorFeatures/Core/Shared/Utilities/LinkedEditsTracker.cs b/src/EditorFeatures/Core/Shared/Utilities/LinkedEditsTracker.cs index 0cd0fe9a93d38..c2b07f068d59d 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/LinkedEditsTracker.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/LinkedEditsTracker.cs @@ -9,110 +9,109 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +internal class LinkedEditsTracker { - internal class LinkedEditsTracker - { - private static readonly object s_propagateSpansEditTag = new(); + private static readonly object s_propagateSpansEditTag = new(); - private readonly ITextBuffer _subjectBuffer; + private readonly ITextBuffer _subjectBuffer; - /// - /// The list of active tracking spans. - /// - private readonly List _trackingSpans = []; + /// + /// The list of active tracking spans. + /// + private readonly List _trackingSpans = []; - public LinkedEditsTracker(ITextBuffer subjectBuffer) - { - Contract.ThrowIfNull(subjectBuffer); + public LinkedEditsTracker(ITextBuffer subjectBuffer) + { + Contract.ThrowIfNull(subjectBuffer); - _subjectBuffer = subjectBuffer; - } + _subjectBuffer = subjectBuffer; + } - public IList GetActiveSpansForSnapshot(ITextSnapshot snapshot) - => _trackingSpans.Select(ts => ts.GetSpan(snapshot)).ToList(); + public IList GetActiveSpansForSnapshot(ITextSnapshot snapshot) + => _trackingSpans.Select(ts => ts.GetSpan(snapshot)).ToList(); - public void AddSpans(IEnumerable spans) - { - var currentActiveSpans = GetActiveSpansForSnapshot(_subjectBuffer.CurrentSnapshot).ToSet(); + public void AddSpans(IEnumerable spans) + { + var currentActiveSpans = GetActiveSpansForSnapshot(_subjectBuffer.CurrentSnapshot).ToSet(); - foreach (var newTrackingSpan in spans) + foreach (var newTrackingSpan in spans) + { + if (!currentActiveSpans.Contains(newTrackingSpan.GetSpan(_subjectBuffer.CurrentSnapshot))) { - if (!currentActiveSpans.Contains(newTrackingSpan.GetSpan(_subjectBuffer.CurrentSnapshot))) - { - _trackingSpans.Add(newTrackingSpan); - } + _trackingSpans.Add(newTrackingSpan); } } + } - public void AddSpans(NormalizedSnapshotSpanCollection snapshotSpanCollection) - { - // TODO: custom tracking spans! - var newTrackingSpans = snapshotSpanCollection.Select(ss => ss.Snapshot.CreateTrackingSpan(ss, SpanTrackingMode.EdgeInclusive, TrackingFidelityMode.Forward)); - AddSpans(newTrackingSpans); - } - - public static bool MyOwnChanges(TextContentChangedEventArgs args) - => args.EditTag == s_propagateSpansEditTag; - - public bool TryGetTextChanged(TextContentChangedEventArgs args, [NotNullWhen(true)] out string? replacementText) - { - // make sure I am not called with my own changes - Contract.ThrowIfTrue(MyOwnChanges(args)); + public void AddSpans(NormalizedSnapshotSpanCollection snapshotSpanCollection) + { + // TODO: custom tracking spans! + var newTrackingSpans = snapshotSpanCollection.Select(ss => ss.Snapshot.CreateTrackingSpan(ss, SpanTrackingMode.EdgeInclusive, TrackingFidelityMode.Forward)); + AddSpans(newTrackingSpans); + } - // initialize out parameter - replacementText = null; + public static bool MyOwnChanges(TextContentChangedEventArgs args) + => args.EditTag == s_propagateSpansEditTag; - var trackingSpansAfterEdit = GetActiveSpansForSnapshot(args.After).Select(ss => (Span)ss).ToList(); - var normalizedTrackingSpansAfterEdit = new NormalizedSpanCollection(trackingSpansAfterEdit); + public bool TryGetTextChanged(TextContentChangedEventArgs args, [NotNullWhen(true)] out string? replacementText) + { + // make sure I am not called with my own changes + Contract.ThrowIfTrue(MyOwnChanges(args)); - if (trackingSpansAfterEdit.Count != normalizedTrackingSpansAfterEdit.Count) - { - // Because of this edit, some spans merged together. We'll abort - return false; - } + // initialize out parameter + replacementText = null; - // We want to find the single tracking span that encompasses all the edits made. If there - // is no such tracking span (or there are multiple), then we consider the change invalid - // and we don't return any changed text. If there is only one, then we find the text in - // the new document. - // - // Note there may be multiple intersecting spans in the case where user typing causes - // multiple edits to happen. For example, if the user has "Sub" and replaces it with "fu" - // Then there will be multiple edits due to the text change and then the case correction. - // However, both edits will be encompassed in one tracking span. - var spansTouchedInEdit = new NormalizedSpanCollection(args.Changes.Select(c => c.NewSpan)); - var intersection = NormalizedSpanCollection.Intersection(normalizedTrackingSpansAfterEdit, spansTouchedInEdit); - - var query = from trackingSpan in _trackingSpans - let mappedSpan = trackingSpan.GetSpan(args.After) - where intersection.All(intersectionSpan => mappedSpan.IntersectsWith(intersectionSpan)) - select trackingSpan; - - var trackingSpansThatIntersect = query.ToList(); - if (trackingSpansThatIntersect.Count != 1) - { - return false; - } + var trackingSpansAfterEdit = GetActiveSpansForSnapshot(args.After).Select(ss => (Span)ss).ToList(); + var normalizedTrackingSpansAfterEdit = new NormalizedSpanCollection(trackingSpansAfterEdit); - var singleIntersectingTrackingSpan = trackingSpansThatIntersect.Single(); - replacementText = singleIntersectingTrackingSpan.GetText(args.After); - return true; + if (trackingSpansAfterEdit.Count != normalizedTrackingSpansAfterEdit.Count) + { + // Because of this edit, some spans merged together. We'll abort + return false; } - public void ApplyReplacementText(string replacementText) + // We want to find the single tracking span that encompasses all the edits made. If there + // is no such tracking span (or there are multiple), then we consider the change invalid + // and we don't return any changed text. If there is only one, then we find the text in + // the new document. + // + // Note there may be multiple intersecting spans in the case where user typing causes + // multiple edits to happen. For example, if the user has "Sub" and replaces it with "fu" + // Then there will be multiple edits due to the text change and then the case correction. + // However, both edits will be encompassed in one tracking span. + var spansTouchedInEdit = new NormalizedSpanCollection(args.Changes.Select(c => c.NewSpan)); + var intersection = NormalizedSpanCollection.Intersection(normalizedTrackingSpansAfterEdit, spansTouchedInEdit); + + var query = from trackingSpan in _trackingSpans + let mappedSpan = trackingSpan.GetSpan(args.After) + where intersection.All(intersectionSpan => mappedSpan.IntersectsWith(intersectionSpan)) + select trackingSpan; + + var trackingSpansThatIntersect = query.ToList(); + if (trackingSpansThatIntersect.Count != 1) { - using var edit = _subjectBuffer.CreateEdit(new EditOptions(), null, s_propagateSpansEditTag); + return false; + } - foreach (var span in _trackingSpans) + var singleIntersectingTrackingSpan = trackingSpansThatIntersect.Single(); + replacementText = singleIntersectingTrackingSpan.GetText(args.After); + return true; + } + + public void ApplyReplacementText(string replacementText) + { + using var edit = _subjectBuffer.CreateEdit(new EditOptions(), null, s_propagateSpansEditTag); + + foreach (var span in _trackingSpans) + { + if (span.GetText(_subjectBuffer.CurrentSnapshot) != replacementText) { - if (span.GetText(_subjectBuffer.CurrentSnapshot) != replacementText) - { - edit.Replace(span.GetSpan(_subjectBuffer.CurrentSnapshot), replacementText); - } + edit.Replace(span.GetSpan(_subjectBuffer.CurrentSnapshot), replacementText); } - - edit.ApplyAndLogExceptions(); } + + edit.ApplyAndLogExceptions(); } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/NativeMethods.cs b/src/EditorFeatures/Core/Shared/Utilities/NativeMethods.cs index 57ccede43120e..f8e796a84b511 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/NativeMethods.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/NativeMethods.cs @@ -4,29 +4,28 @@ using System.Runtime.InteropServices; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +internal static class NativeMethods { - internal static class NativeMethods - { - internal const uint MWMO_INPUTAVAILABLE = 0x0004; + internal const uint MWMO_INPUTAVAILABLE = 0x0004; - internal const uint - QS_KEY = 0x0001, - QS_MOUSEMOVE = 0x0002, - QS_MOUSEBUTTON = 0x0004, - QS_POSTMESSAGE = 0x0008, - QS_TIMER = 0x0010, - QS_PAINT = 0x0020, - QS_SENDMESSAGE = 0x0040, - QS_HOTKEY = 0x0080, - QS_ALLPOSTMESSAGE = 0x0100, - QS_MOUSE = QS_MOUSEMOVE | QS_MOUSEBUTTON, - QS_INPUT = QS_MOUSE | QS_KEY, - QS_ALLEVENTS = QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY, - QS_ALLINPUT = QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY | QS_SENDMESSAGE, - QS_EVENT = 0x2000; + internal const uint + QS_KEY = 0x0001, + QS_MOUSEMOVE = 0x0002, + QS_MOUSEBUTTON = 0x0004, + QS_POSTMESSAGE = 0x0008, + QS_TIMER = 0x0010, + QS_PAINT = 0x0020, + QS_SENDMESSAGE = 0x0040, + QS_HOTKEY = 0x0080, + QS_ALLPOSTMESSAGE = 0x0100, + QS_MOUSE = QS_MOUSEMOVE | QS_MOUSEBUTTON, + QS_INPUT = QS_MOUSE | QS_KEY, + QS_ALLEVENTS = QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY, + QS_ALLINPUT = QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY | QS_SENDMESSAGE, + QS_EVENT = 0x2000; - [DllImport("user32.dll")] - internal static extern uint GetQueueStatus(uint flags); - } + [DllImport("user32.dll")] + internal static extern uint GetQueueStatus(uint flags); } diff --git a/src/EditorFeatures/Core/Shared/Utilities/RenameTrackingDismisser.cs b/src/EditorFeatures/Core/Shared/Utilities/RenameTrackingDismisser.cs index 0fcc8e048ceb2..59ce3290f7293 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/RenameTrackingDismisser.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/RenameTrackingDismisser.cs @@ -5,22 +5,21 @@ using System.Collections.Generic; using Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +internal static class RenameTrackingDismisser { - internal static class RenameTrackingDismisser - { - internal static void DismissRenameTracking(Workspace workspace, DocumentId documentId) - => RenameTrackingTaggerProvider.ResetRenameTrackingState(workspace, documentId); + internal static void DismissRenameTracking(Workspace workspace, DocumentId documentId) + => RenameTrackingTaggerProvider.ResetRenameTrackingState(workspace, documentId); - internal static void DismissRenameTracking(Workspace workspace, IEnumerable documentIds) + internal static void DismissRenameTracking(Workspace workspace, IEnumerable documentIds) + { + foreach (var docId in documentIds) { - foreach (var docId in documentIds) - { - DismissRenameTracking(workspace, docId); - } + DismissRenameTracking(workspace, docId); } - - internal static bool DismissVisibleRenameTracking(Workspace workspace, DocumentId documentId) - => RenameTrackingTaggerProvider.ResetVisibleRenameTrackingState(workspace, documentId); } + + internal static bool DismissVisibleRenameTracking(Workspace workspace, DocumentId documentId) + => RenameTrackingTaggerProvider.ResetVisibleRenameTrackingState(workspace, documentId); } diff --git a/src/EditorFeatures/Core/Shared/Utilities/ResettableDelay.cs b/src/EditorFeatures/Core/Shared/Utilities/ResettableDelay.cs index 0aafb145bcaeb..b7cf6c32c4d03 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/ResettableDelay.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/ResettableDelay.cs @@ -8,74 +8,73 @@ using Microsoft.CodeAnalysis.Shared.TestHooks; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +internal class ResettableDelay { - internal class ResettableDelay - { - public static readonly ResettableDelay CompletedDelay = new(); + public static readonly ResettableDelay CompletedDelay = new(); - private readonly int _delayInMilliseconds; - private readonly TaskCompletionSource _taskCompletionSource = new(); + private readonly int _delayInMilliseconds; + private readonly TaskCompletionSource _taskCompletionSource = new(); - private int _lastSetTime; + private int _lastSetTime; - /// - /// Create a ResettableDelay that will complete a task after a certain duration. The delay - /// can be reset at any point before it elapses in which case completion is postponed. The - /// delay can be reset multiple times. - /// - /// The time to delay before completing the task - public ResettableDelay(int delayInMilliseconds, IExpeditableDelaySource expeditableDelaySource, CancellationToken cancellationToken = default) - { - Contract.ThrowIfFalse(delayInMilliseconds >= 50, "Perf, only use delays >= 50ms"); - _delayInMilliseconds = delayInMilliseconds; + /// + /// Create a ResettableDelay that will complete a task after a certain duration. The delay + /// can be reset at any point before it elapses in which case completion is postponed. The + /// delay can be reset multiple times. + /// + /// The time to delay before completing the task + public ResettableDelay(int delayInMilliseconds, IExpeditableDelaySource expeditableDelaySource, CancellationToken cancellationToken = default) + { + Contract.ThrowIfFalse(delayInMilliseconds >= 50, "Perf, only use delays >= 50ms"); + _delayInMilliseconds = delayInMilliseconds; - Reset(); + Reset(); - _ = StartTimerAsync(expeditableDelaySource, cancellationToken); - } + _ = StartTimerAsync(expeditableDelaySource, cancellationToken); + } - private ResettableDelay() - { - // create resettableDelay with completed state - _delayInMilliseconds = 0; - _taskCompletionSource = new TaskCompletionSource(); - _taskCompletionSource.SetResult(null); + private ResettableDelay() + { + // create resettableDelay with completed state + _delayInMilliseconds = 0; + _taskCompletionSource = new TaskCompletionSource(); + _taskCompletionSource.SetResult(null); - Reset(); - } + Reset(); + } - public Task Task => _taskCompletionSource.Task; + public Task Task => _taskCompletionSource.Task; - public void Reset() - { - // Note: Environment.TickCount - this.lastSetTime is safe in the presence of overflow, but most - // other operations are not. - _lastSetTime = Environment.TickCount; - } + public void Reset() + { + // Note: Environment.TickCount - this.lastSetTime is safe in the presence of overflow, but most + // other operations are not. + _lastSetTime = Environment.TickCount; + } - private async Task StartTimerAsync(IExpeditableDelaySource expeditableDelaySource, CancellationToken cancellationToken) + private async Task StartTimerAsync(IExpeditableDelaySource expeditableDelaySource, CancellationToken cancellationToken) + { + try { - try + do { - do + // Keep delaying until at least delayInMilliseconds has elapsed since lastSetTime + if (!await expeditableDelaySource.Delay(TimeSpan.FromMilliseconds(_delayInMilliseconds), cancellationToken).ConfigureAwait(false)) { - // Keep delaying until at least delayInMilliseconds has elapsed since lastSetTime - if (!await expeditableDelaySource.Delay(TimeSpan.FromMilliseconds(_delayInMilliseconds), cancellationToken).ConfigureAwait(false)) - { - // The operation is being expedited. - break; - } + // The operation is being expedited. + break; } - while (Environment.TickCount - _lastSetTime < _delayInMilliseconds); - - _taskCompletionSource.SetResult(null); - } - catch (OperationCanceledException) - { - // Calling the "Try" variant because that's the only one that accepts the token to associate with the task - _taskCompletionSource.TrySetCanceled(cancellationToken); } + while (Environment.TickCount - _lastSetTime < _delayInMilliseconds); + + _taskCompletionSource.SetResult(null); + } + catch (OperationCanceledException) + { + // Calling the "Try" variant because that's the only one that accepts the token to associate with the task + _taskCompletionSource.TrySetCanceled(cancellationToken); } } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/ThreadingContext.cs b/src/EditorFeatures/Core/Shared/Utilities/ThreadingContext.cs index d24bf9003f360..919374021dffa 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/ThreadingContext.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/ThreadingContext.cs @@ -9,87 +9,86 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.VisualStudio.Threading; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +/// +/// Implements , which provides an implementation of +/// to Roslyn code. +/// +/// +/// The is constructed from the +/// provided by the MEF container, if available. If no +/// is available, a new instance is constructed using the +/// synchronization context of the current thread as the main thread. +/// +[Export(typeof(IThreadingContext))] +[Shared] +internal sealed class ThreadingContext : IThreadingContext, IDisposable { - /// - /// Implements , which provides an implementation of - /// to Roslyn code. - /// - /// - /// The is constructed from the - /// provided by the MEF container, if available. If no - /// is available, a new instance is constructed using the - /// synchronization context of the current thread as the main thread. - /// - [Export(typeof(IThreadingContext))] - [Shared] - internal sealed class ThreadingContext : IThreadingContext, IDisposable + private readonly CancellationTokenSource _disposalTokenSource = new(); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ThreadingContext(JoinableTaskContext joinableTaskContext) { - private readonly CancellationTokenSource _disposalTokenSource = new(); + HasMainThread = joinableTaskContext.MainThread.IsAlive; + JoinableTaskContext = joinableTaskContext; + JoinableTaskFactory = joinableTaskContext.Factory; + ShutdownBlockingTasks = new JoinableTaskCollection(JoinableTaskContext); + ShutdownBlockingTaskFactory = JoinableTaskContext.CreateFactory(ShutdownBlockingTasks); + } - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ThreadingContext(JoinableTaskContext joinableTaskContext) - { - HasMainThread = joinableTaskContext.MainThread.IsAlive; - JoinableTaskContext = joinableTaskContext; - JoinableTaskFactory = joinableTaskContext.Factory; - ShutdownBlockingTasks = new JoinableTaskCollection(JoinableTaskContext); - ShutdownBlockingTaskFactory = JoinableTaskContext.CreateFactory(ShutdownBlockingTasks); - } + /// + public bool HasMainThread + { + get; + } - /// - public bool HasMainThread - { - get; - } + /// + public JoinableTaskContext JoinableTaskContext + { + get; + } - /// - public JoinableTaskContext JoinableTaskContext - { - get; - } + /// + public JoinableTaskFactory JoinableTaskFactory + { + get; + } - /// - public JoinableTaskFactory JoinableTaskFactory - { - get; - } + public JoinableTaskCollection ShutdownBlockingTasks { get; } - public JoinableTaskCollection ShutdownBlockingTasks { get; } + private JoinableTaskFactory ShutdownBlockingTaskFactory { get; } - private JoinableTaskFactory ShutdownBlockingTaskFactory { get; } + public CancellationToken DisposalToken => _disposalTokenSource.Token; - public CancellationToken DisposalToken => _disposalTokenSource.Token; + public JoinableTask RunWithShutdownBlockAsync(Func func) + { + return ShutdownBlockingTaskFactory.RunAsync(() => + { + DisposalToken.ThrowIfCancellationRequested(); + return func(DisposalToken); + }); + } - public JoinableTask RunWithShutdownBlockAsync(Func func) + public void Dispose() + { + // https://github.com/Microsoft/vs-threading/blob/main/doc/cookbook_vs.md#how-to-write-a-fire-and-forget-method-responsibly + _disposalTokenSource.Cancel(); + + try { - return ShutdownBlockingTaskFactory.RunAsync(() => - { - DisposalToken.ThrowIfCancellationRequested(); - return func(DisposalToken); - }); + // Block Dispose until all async work has completed. + JoinableTaskContext.Factory.Run(ShutdownBlockingTasks.JoinTillEmptyAsync); } - - public void Dispose() + catch (OperationCanceledException) { - // https://github.com/Microsoft/vs-threading/blob/main/doc/cookbook_vs.md#how-to-write-a-fire-and-forget-method-responsibly - _disposalTokenSource.Cancel(); - - try - { - // Block Dispose until all async work has completed. - JoinableTaskContext.Factory.Run(ShutdownBlockingTasks.JoinTillEmptyAsync); - } - catch (OperationCanceledException) - { - // this exception is expected because we signaled the cancellation token - } - catch (AggregateException ex) - { - // ignore AggregateException containing only OperationCanceledException - ex.Handle(inner => inner is OperationCanceledException); - } + // this exception is expected because we signaled the cancellation token + } + catch (AggregateException ex) + { + // ignore AggregateException containing only OperationCanceledException + ex.Handle(inner => inner is OperationCanceledException); } } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/ThreadingContextTaskSchedulerProvider.cs b/src/EditorFeatures/Core/Shared/Utilities/ThreadingContextTaskSchedulerProvider.cs index a03e581faeb2d..fe6b2dcf2ae46 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/ThreadingContextTaskSchedulerProvider.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/ThreadingContextTaskSchedulerProvider.cs @@ -11,44 +11,43 @@ using Microsoft.VisualStudio.Threading; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +[ExportWorkspaceService(typeof(ITaskSchedulerProvider), ServiceLayer.Editor), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class ThreadingContextTaskSchedulerProvider(IThreadingContext threadingContext) : ITaskSchedulerProvider { - [ExportWorkspaceService(typeof(ITaskSchedulerProvider), ServiceLayer.Editor), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class ThreadingContextTaskSchedulerProvider(IThreadingContext threadingContext) : ITaskSchedulerProvider - { - public TaskScheduler CurrentContextScheduler { get; } = threadingContext.HasMainThread - ? new JoinableTaskFactoryTaskScheduler(threadingContext.JoinableTaskFactory) - : TaskScheduler.Default; + public TaskScheduler CurrentContextScheduler { get; } = threadingContext.HasMainThread + ? new JoinableTaskFactoryTaskScheduler(threadingContext.JoinableTaskFactory) + : TaskScheduler.Default; - private sealed class JoinableTaskFactoryTaskScheduler(JoinableTaskFactory joinableTaskFactory) : TaskScheduler - { - private readonly JoinableTaskFactory _joinableTaskFactory = joinableTaskFactory; + private sealed class JoinableTaskFactoryTaskScheduler(JoinableTaskFactory joinableTaskFactory) : TaskScheduler + { + private readonly JoinableTaskFactory _joinableTaskFactory = joinableTaskFactory; - public override int MaximumConcurrencyLevel => 1; + public override int MaximumConcurrencyLevel => 1; - protected override IEnumerable GetScheduledTasks() - => SpecializedCollections.EmptyEnumerable(); + protected override IEnumerable GetScheduledTasks() + => SpecializedCollections.EmptyEnumerable(); - protected override void QueueTask(Task task) + protected override void QueueTask(Task task) + { + _joinableTaskFactory.RunAsync(async () => { - _joinableTaskFactory.RunAsync(async () => - { - await _joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true); - TryExecuteTask(task); - }); - } + await _joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true); + TryExecuteTask(task); + }); + } - protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + if (_joinableTaskFactory.Context.IsOnMainThread) { - if (_joinableTaskFactory.Context.IsOnMainThread) - { - return TryExecuteTask(task); - } - - return false; + return TryExecuteTask(task); } + + return false; } } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/VirtualTreePoint.cs b/src/EditorFeatures/Core/Shared/Utilities/VirtualTreePoint.cs index 05c1e43e9b9c4..3a14bf4ea750b 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/VirtualTreePoint.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/VirtualTreePoint.cs @@ -7,75 +7,74 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +internal readonly record struct VirtualTreePoint : IComparable { - internal readonly record struct VirtualTreePoint : IComparable - { - private readonly SyntaxTree _tree; - private readonly SourceText _text; - private readonly int _position; - private readonly int _virtualSpaces; + private readonly SyntaxTree _tree; + private readonly SourceText _text; + private readonly int _position; + private readonly int _virtualSpaces; - public VirtualTreePoint(SyntaxTree tree, SourceText text, int position, int virtualSpaces = 0) - { - Contract.ThrowIfNull(tree); - Contract.ThrowIfFalse(position >= 0 && position <= tree.Length); - Contract.ThrowIfFalse(virtualSpaces >= 0); - - _tree = tree; - _text = text; - _position = position; - _virtualSpaces = virtualSpaces; - } + public VirtualTreePoint(SyntaxTree tree, SourceText text, int position, int virtualSpaces = 0) + { + Contract.ThrowIfNull(tree); + Contract.ThrowIfFalse(position >= 0 && position <= tree.Length); + Contract.ThrowIfFalse(virtualSpaces >= 0); + + _tree = tree; + _text = text; + _position = position; + _virtualSpaces = virtualSpaces; + } - public static bool operator <(VirtualTreePoint left, VirtualTreePoint right) - => left.CompareTo(right) < 0; + public static bool operator <(VirtualTreePoint left, VirtualTreePoint right) + => left.CompareTo(right) < 0; - public static bool operator <=(VirtualTreePoint left, VirtualTreePoint right) - => left.CompareTo(right) <= 0; + public static bool operator <=(VirtualTreePoint left, VirtualTreePoint right) + => left.CompareTo(right) <= 0; - public static bool operator >(VirtualTreePoint left, VirtualTreePoint right) - => left.CompareTo(right) > 0; + public static bool operator >(VirtualTreePoint left, VirtualTreePoint right) + => left.CompareTo(right) > 0; - public static bool operator >=(VirtualTreePoint left, VirtualTreePoint right) - => left.CompareTo(right) >= 0; + public static bool operator >=(VirtualTreePoint left, VirtualTreePoint right) + => left.CompareTo(right) >= 0; - public bool IsInVirtualSpace - { - get { return _virtualSpaces != 0; } - } + public bool IsInVirtualSpace + { + get { return _virtualSpaces != 0; } + } - public int Position => _position; + public int Position => _position; - public int VirtualSpaces => _virtualSpaces; + public int VirtualSpaces => _virtualSpaces; - public SourceText Text => _text; + public SourceText Text => _text; - public SyntaxTree Tree => _tree; + public SyntaxTree Tree => _tree; - public int CompareTo(VirtualTreePoint other) + public int CompareTo(VirtualTreePoint other) + { + if (Text != other.Text) { - if (Text != other.Text) - { - throw new InvalidOperationException(EditorFeaturesResources.Can_t_compare_positions_from_different_text_snapshots); - } - - return ComparerWithState.CompareTo(this, other, s_comparers); + throw new InvalidOperationException(EditorFeaturesResources.Can_t_compare_positions_from_different_text_snapshots); } - private static readonly ImmutableArray> s_comparers = - [p => p.Position, prop => prop.VirtualSpaces]; + return ComparerWithState.CompareTo(this, other, s_comparers); + } - public bool Equals(VirtualTreePoint other) - => CompareTo(other) == 0; + private static readonly ImmutableArray> s_comparers = + [p => p.Position, prop => prop.VirtualSpaces]; - public override int GetHashCode() - => Text.GetHashCode() ^ Position.GetHashCode() ^ VirtualSpaces.GetHashCode(); + public bool Equals(VirtualTreePoint other) + => CompareTo(other) == 0; - public override string ToString() - => $"VirtualTreePoint {{ Tree: '{Tree}', Text: '{Text}', Position: '{Position}', VirtualSpaces '{VirtualSpaces}' }}"; + public override int GetHashCode() + => Text.GetHashCode() ^ Position.GetHashCode() ^ VirtualSpaces.GetHashCode(); - public TextLine GetContainingLine() - => Text.Lines.GetLineFromPosition(Position); - } + public override string ToString() + => $"VirtualTreePoint {{ Tree: '{Tree}', Text: '{Text}', Position: '{Position}', VirtualSpaces '{VirtualSpaces}' }}"; + + public TextLine GetContainingLine() + => Text.Lines.GetLineFromPosition(Position); } diff --git a/src/EditorFeatures/Core/Shared/Utilities/WorkspaceThreadingService.cs b/src/EditorFeatures/Core/Shared/Utilities/WorkspaceThreadingService.cs index 9cc6ba506797a..2f7e1974dddd3 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/WorkspaceThreadingService.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/WorkspaceThreadingService.cs @@ -8,21 +8,20 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +[Export(typeof(IWorkspaceThreadingService))] +[Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class WorkspaceThreadingService(IThreadingContext threadingContext) : IWorkspaceThreadingService { - [Export(typeof(IWorkspaceThreadingService))] - [Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class WorkspaceThreadingService(IThreadingContext threadingContext) : IWorkspaceThreadingService - { - private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IThreadingContext _threadingContext = threadingContext; - public bool IsOnMainThread => _threadingContext.JoinableTaskContext.IsOnMainThread; + public bool IsOnMainThread => _threadingContext.JoinableTaskContext.IsOnMainThread; - public TResult Run(Func> asyncMethod) - { - return _threadingContext.JoinableTaskFactory.Run(asyncMethod); - } + public TResult Run(Func> asyncMethod) + { + return _threadingContext.JoinableTaskFactory.Run(asyncMethod); } } diff --git a/src/EditorFeatures/Core/Shared/Utilities/WorkspaceThreadingServiceProvider.cs b/src/EditorFeatures/Core/Shared/Utilities/WorkspaceThreadingServiceProvider.cs index f49b7c776cfda..2edd89bc7f0c9 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/WorkspaceThreadingServiceProvider.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/WorkspaceThreadingServiceProvider.cs @@ -7,14 +7,13 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +[ExportWorkspaceService(typeof(IWorkspaceThreadingServiceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class WorkspaceThreadingServiceProvider( + IWorkspaceThreadingService service) : IWorkspaceThreadingServiceProvider { - [ExportWorkspaceService(typeof(IWorkspaceThreadingServiceProvider)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class WorkspaceThreadingServiceProvider( - IWorkspaceThreadingService service) : IWorkspaceThreadingServiceProvider - { - public IWorkspaceThreadingService Service { get; } = service; - } + public IWorkspaceThreadingService Service { get; } = service; } diff --git a/src/EditorFeatures/Core/SmartIndent/SmartIndent.cs b/src/EditorFeatures/Core/SmartIndent/SmartIndent.cs index 48188e52c6c8a..335d0519d9661 100644 --- a/src/EditorFeatures/Core/SmartIndent/SmartIndent.cs +++ b/src/EditorFeatures/Core/SmartIndent/SmartIndent.cs @@ -13,40 +13,39 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -namespace Microsoft.CodeAnalysis.Editor.Implementation.SmartIndent +namespace Microsoft.CodeAnalysis.Editor.Implementation.SmartIndent; + +internal partial class SmartIndent(ITextView textView, EditorOptionsService editorOptionsService) : ISmartIndent { - internal partial class SmartIndent(ITextView textView, EditorOptionsService editorOptionsService) : ISmartIndent - { - private readonly ITextView _textView = textView; - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly ITextView _textView = textView; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - public int? GetDesiredIndentation(ITextSnapshotLine line) - => GetDesiredIndentation(line, CancellationToken.None); + public int? GetDesiredIndentation(ITextSnapshotLine line) + => GetDesiredIndentation(line, CancellationToken.None); - public void Dispose() - { - } + public void Dispose() + { + } + + private int? GetDesiredIndentation(ITextSnapshotLine line, CancellationToken cancellationToken) + { + if (line == null) + throw new ArgumentNullException(nameof(line)); - private int? GetDesiredIndentation(ITextSnapshotLine line, CancellationToken cancellationToken) + using (Logger.LogBlock(FunctionId.SmartIndentation_Start, cancellationToken)) { - if (line == null) - throw new ArgumentNullException(nameof(line)); - - using (Logger.LogBlock(FunctionId.SmartIndentation_Start, cancellationToken)) - { - var document = line.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return null; - - var newService = document.GetLanguageService(); - if (newService == null) - return null; - - var indentationOptions = line.Snapshot.TextBuffer.GetIndentationOptions(_editorOptionsService, document.Project.Services, explicitFormat: false); - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - var result = newService.GetIndentation(parsedDocument, line.LineNumber, indentationOptions, cancellationToken); - return result.GetIndentation(_textView, line); - } + var document = line.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return null; + + var newService = document.GetLanguageService(); + if (newService == null) + return null; + + var indentationOptions = line.Snapshot.TextBuffer.GetIndentationOptions(_editorOptionsService, document.Project.Services, explicitFormat: false); + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + var result = newService.GetIndentation(parsedDocument, line.LineNumber, indentationOptions, cancellationToken); + return result.GetIndentation(_textView, line); } } } diff --git a/src/EditorFeatures/Core/SmartIndent/SmartIndentProvider.cs b/src/EditorFeatures/Core/SmartIndent/SmartIndentProvider.cs index 006234c2015be..318810ffc6771 100644 --- a/src/EditorFeatures/Core/SmartIndent/SmartIndentProvider.cs +++ b/src/EditorFeatures/Core/SmartIndent/SmartIndentProvider.cs @@ -9,30 +9,29 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.SmartIndent +namespace Microsoft.CodeAnalysis.Editor.Implementation.SmartIndent; + +[Export(typeof(ISmartIndentProvider))] +[ContentType(ContentTypeNames.CSharpContentType)] +[ContentType(ContentTypeNames.VisualBasicContentType)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class SmartIndentProvider(EditorOptionsService editorOptionsService) : ISmartIndentProvider { - [Export(typeof(ISmartIndentProvider))] - [ContentType(ContentTypeNames.CSharpContentType)] - [ContentType(ContentTypeNames.VisualBasicContentType)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class SmartIndentProvider(EditorOptionsService editorOptionsService) : ISmartIndentProvider - { - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - public ISmartIndent? CreateSmartIndent(ITextView textView) + public ISmartIndent? CreateSmartIndent(ITextView textView) + { + if (textView == null) { - if (textView == null) - { - throw new ArgumentNullException(nameof(textView)); - } - - if (!_editorOptionsService.GlobalOptions.GetOption(SmartIndenterOptionsStorage.SmartIndenter)) - { - return null; - } + throw new ArgumentNullException(nameof(textView)); + } - return new SmartIndent(textView, _editorOptionsService); + if (!_editorOptionsService.GlobalOptions.GetOption(SmartIndenterOptionsStorage.SmartIndenter)) + { + return null; } + + return new SmartIndent(textView, _editorOptionsService); } } diff --git a/src/EditorFeatures/Core/SmartIndent/SmartIndenterOptionsStorage.cs b/src/EditorFeatures/Core/SmartIndent/SmartIndenterOptionsStorage.cs index e2118af9b3ec9..2b456c7f2c98d 100644 --- a/src/EditorFeatures/Core/SmartIndent/SmartIndenterOptionsStorage.cs +++ b/src/EditorFeatures/Core/SmartIndent/SmartIndenterOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.Implementation.SmartIndent +namespace Microsoft.CodeAnalysis.Editor.Implementation.SmartIndent; + +internal static class SmartIndenterOptionsStorage { - internal static class SmartIndenterOptionsStorage - { - public static readonly Option2 SmartIndenter = new("dotnet_enable_smart_indenter", defaultValue: true); - } + public static readonly Option2 SmartIndenter = new("dotnet_enable_smart_indenter", defaultValue: true); } diff --git a/src/EditorFeatures/Core/Snippets/SnippetsOptionsStorage.cs b/src/EditorFeatures/Core/Snippets/SnippetsOptionsStorage.cs index 0b0cffab4904b..4a040f7833c00 100644 --- a/src/EditorFeatures/Core/Snippets/SnippetsOptionsStorage.cs +++ b/src/EditorFeatures/Core/Snippets/SnippetsOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Snippets +namespace Microsoft.CodeAnalysis.Snippets; + +internal static class SnippetsOptionsStorage { - internal static class SnippetsOptionsStorage - { - public static readonly Option2 Snippets = new("dotnet_enable_snippets", defaultValue: true); - } + public static readonly Option2 Snippets = new("dotnet_enable_snippets", defaultValue: true); } diff --git a/src/EditorFeatures/Core/SolutionCrawler/HostSolutionCrawlerWorkspaceEventListener.cs b/src/EditorFeatures/Core/SolutionCrawler/HostSolutionCrawlerWorkspaceEventListener.cs index 1698d65c1cecf..e5a1e2a0ef317 100644 --- a/src/EditorFeatures/Core/SolutionCrawler/HostSolutionCrawlerWorkspaceEventListener.cs +++ b/src/EditorFeatures/Core/SolutionCrawler/HostSolutionCrawlerWorkspaceEventListener.cs @@ -8,29 +8,28 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.SolutionCrawler +namespace Microsoft.CodeAnalysis.SolutionCrawler; + +[ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class HostSolutionCrawlerWorkspaceEventListener(IGlobalOptionService globalOptions) : IEventListener, IEventListenerStoppable { - [ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class HostSolutionCrawlerWorkspaceEventListener(IGlobalOptionService globalOptions) : IEventListener, IEventListenerStoppable - { - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IGlobalOptionService _globalOptions = globalOptions; - public void StartListening(Workspace workspace, object? serviceOpt) + public void StartListening(Workspace workspace, object? serviceOpt) + { + if (_globalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler)) { - if (_globalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler)) - { - workspace.Services.GetRequiredService().Register(workspace); - } + workspace.Services.GetRequiredService().Register(workspace); } + } - public void StopListening(Workspace workspace) - { - // we do this so that we can stop solution crawler faster and fire some telemetry. - // this is to reduce a case where we keep going even when VS is shutting down since we don't know about that - var registration = workspace.Services.GetRequiredService(); - registration.Unregister(workspace, blockingShutdown: true); - } + public void StopListening(Workspace workspace) + { + // we do this so that we can stop solution crawler faster and fire some telemetry. + // this is to reduce a case where we keep going even when VS is shutting down since we don't know about that + var registration = workspace.Services.GetRequiredService(); + registration.Unregister(workspace, blockingShutdown: true); } } diff --git a/src/EditorFeatures/Core/SolutionEvents/HostLegacySolutionEventsWorkspaceEventListener.cs b/src/EditorFeatures/Core/SolutionEvents/HostLegacySolutionEventsWorkspaceEventListener.cs index 66fb52cd1200d..f0209b82cca09 100644 --- a/src/EditorFeatures/Core/SolutionEvents/HostLegacySolutionEventsWorkspaceEventListener.cs +++ b/src/EditorFeatures/Core/SolutionEvents/HostLegacySolutionEventsWorkspaceEventListener.cs @@ -17,102 +17,101 @@ using Microsoft.CodeAnalysis.SolutionCrawler; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.LegacySolutionEvents +namespace Microsoft.CodeAnalysis.LegacySolutionEvents; + +[ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared] +internal sealed partial class HostLegacySolutionEventsWorkspaceEventListener : IEventListener { - [ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared] - internal sealed partial class HostLegacySolutionEventsWorkspaceEventListener : IEventListener + private readonly IGlobalOptionService _globalOptions; + private readonly IThreadingContext _threadingContext; + private readonly AsyncBatchingWorkQueue _eventQueue; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public HostLegacySolutionEventsWorkspaceEventListener( + IGlobalOptionService globalOptions, + IThreadingContext threadingContext, + IAsynchronousOperationListenerProvider listenerProvider) { - private readonly IGlobalOptionService _globalOptions; - private readonly IThreadingContext _threadingContext; - private readonly AsyncBatchingWorkQueue _eventQueue; + _globalOptions = globalOptions; + _threadingContext = threadingContext; + _eventQueue = new AsyncBatchingWorkQueue( + DelayTimeSpan.Short, + ProcessWorkspaceChangeEventsAsync, + listenerProvider.GetListener(FeatureAttribute.SolutionCrawlerUnitTesting), + _threadingContext.DisposalToken); + } - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public HostLegacySolutionEventsWorkspaceEventListener( - IGlobalOptionService globalOptions, - IThreadingContext threadingContext, - IAsynchronousOperationListenerProvider listenerProvider) + public void StartListening(Workspace workspace, object? serviceOpt) + { + if (_globalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler)) { - _globalOptions = globalOptions; - _threadingContext = threadingContext; - _eventQueue = new AsyncBatchingWorkQueue( - DelayTimeSpan.Short, - ProcessWorkspaceChangeEventsAsync, - listenerProvider.GetListener(FeatureAttribute.SolutionCrawlerUnitTesting), - _threadingContext.DisposalToken); + workspace.WorkspaceChanged += OnWorkspaceChanged; + _threadingContext.DisposalToken.Register(() => + { + workspace.WorkspaceChanged -= OnWorkspaceChanged; + }); } + } - public void StartListening(Workspace workspace, object? serviceOpt) + private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) + { + // Legacy workspace events exist solely to let unit testing continue to work using their own fork of solution + // crawler. As such, they only need events for the project types they care about. Specifically, that is only + // for VB and C#. This is relevant as well as we don't sync any other project types to OOP. So sending + // notifications about other projects that don't even exist on the other side isn't helpful. + + var projectId = e.ProjectId ?? e.DocumentId?.ProjectId; + if (projectId != null) { - if (_globalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler)) - { - workspace.WorkspaceChanged += OnWorkspaceChanged; - _threadingContext.DisposalToken.Register(() => - { - workspace.WorkspaceChanged -= OnWorkspaceChanged; - }); - } + var project = e.OldSolution.GetProject(projectId) ?? e.NewSolution.GetProject(projectId); + if (project != null && !RemoteSupportedLanguages.IsSupported(project.Language)) + return; } - private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) - { - // Legacy workspace events exist solely to let unit testing continue to work using their own fork of solution - // crawler. As such, they only need events for the project types they care about. Specifically, that is only - // for VB and C#. This is relevant as well as we don't sync any other project types to OOP. So sending - // notifications about other projects that don't even exist on the other side isn't helpful. + _eventQueue.AddWork(e); + } - var projectId = e.ProjectId ?? e.DocumentId?.ProjectId; - if (projectId != null) - { - var project = e.OldSolution.GetProject(projectId) ?? e.NewSolution.GetProject(projectId); - if (project != null && !RemoteSupportedLanguages.IsSupported(project.Language)) - return; - } + private async ValueTask ProcessWorkspaceChangeEventsAsync(ImmutableSegmentedList events, CancellationToken cancellationToken) + { + if (events.IsEmpty) + return; - _eventQueue.AddWork(e); - } + var workspace = events[0].OldSolution.Workspace; + Contract.ThrowIfTrue(events.Any(e => e.OldSolution.Workspace != workspace || e.NewSolution.Workspace != workspace)); - private async ValueTask ProcessWorkspaceChangeEventsAsync(ImmutableSegmentedList events, CancellationToken cancellationToken) + var client = await RemoteHostClient.TryGetClientAsync(workspace, cancellationToken).ConfigureAwait(false); + + if (client is null) { - if (events.IsEmpty) + var aggregationService = workspace.Services.GetRequiredService(); + var shouldReport = aggregationService.ShouldReportChanges(workspace.Services.SolutionServices); + if (!shouldReport) return; - var workspace = events[0].OldSolution.Workspace; - Contract.ThrowIfTrue(events.Any(e => e.OldSolution.Workspace != workspace || e.NewSolution.Workspace != workspace)); - - var client = await RemoteHostClient.TryGetClientAsync(workspace, cancellationToken).ConfigureAwait(false); - - if (client is null) - { - var aggregationService = workspace.Services.GetRequiredService(); - var shouldReport = aggregationService.ShouldReportChanges(workspace.Services.SolutionServices); - if (!shouldReport) - return; + foreach (var args in events) + await aggregationService.OnWorkspaceChangedAsync(args, cancellationToken).ConfigureAwait(false); + } + else + { + // Notifying OOP of workspace events can be expensive (there may be a lot of them, and they involve + // syncing over entire solution snapshots). As such, do not bother to do this if the remote side says + // that it's not interested in the events. This will happen, for example, when the unittesting + // Test-Explorer window has not been shown yet, and so the unit testing system will not have registered + // an incremental analyzer with us. + var shouldReport = await client.TryInvokeAsync( + (service, cancellationToken) => service.ShouldReportChangesAsync(cancellationToken), + cancellationToken).ConfigureAwait(false); + if (!shouldReport.HasValue || !shouldReport.Value) + return; - foreach (var args in events) - await aggregationService.OnWorkspaceChangedAsync(args, cancellationToken).ConfigureAwait(false); - } - else + foreach (var args in events) { - // Notifying OOP of workspace events can be expensive (there may be a lot of them, and they involve - // syncing over entire solution snapshots). As such, do not bother to do this if the remote side says - // that it's not interested in the events. This will happen, for example, when the unittesting - // Test-Explorer window has not been shown yet, and so the unit testing system will not have registered - // an incremental analyzer with us. - var shouldReport = await client.TryInvokeAsync( - (service, cancellationToken) => service.ShouldReportChangesAsync(cancellationToken), + await client.TryInvokeAsync( + args.OldSolution, args.NewSolution, + (service, oldSolutionChecksum, newSolutionChecksum, cancellationToken) => + service.OnWorkspaceChangedAsync(oldSolutionChecksum, newSolutionChecksum, args.Kind, args.ProjectId, args.DocumentId, cancellationToken), cancellationToken).ConfigureAwait(false); - if (!shouldReport.HasValue || !shouldReport.Value) - return; - - foreach (var args in events) - { - await client.TryInvokeAsync( - args.OldSolution, args.NewSolution, - (service, oldSolutionChecksum, newSolutionChecksum, cancellationToken) => - service.OnWorkspaceChangedAsync(oldSolutionChecksum, newSolutionChecksum, args.Kind, args.ProjectId, args.DocumentId, cancellationToken), - cancellationToken).ConfigureAwait(false); - } } } } diff --git a/src/EditorFeatures/Core/SpellCheck/RoslynSpellCheckFixerProvider.cs b/src/EditorFeatures/Core/SpellCheck/RoslynSpellCheckFixerProvider.cs index d7adafe1e093b..11c5eac3fa336 100644 --- a/src/EditorFeatures/Core/SpellCheck/RoslynSpellCheckFixerProvider.cs +++ b/src/EditorFeatures/Core/SpellCheck/RoslynSpellCheckFixerProvider.cs @@ -18,110 +18,109 @@ using Microsoft.VisualStudio.Text.SpellChecker; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.SpellCheck +namespace Microsoft.CodeAnalysis.SpellCheck; + +[Obsolete] +[Export(typeof(ISpellCheckFixerProvider))] +[ContentType(ContentTypeNames.RoslynContentType)] +[Name(nameof(RoslynSpellCheckFixerProvider))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RoslynSpellCheckFixerProvider( + IThreadingContext threadingContext) : ISpellCheckFixerProvider { - [Obsolete] - [Export(typeof(ISpellCheckFixerProvider))] - [ContentType(ContentTypeNames.RoslynContentType)] - [Name(nameof(RoslynSpellCheckFixerProvider))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class RoslynSpellCheckFixerProvider( - IThreadingContext threadingContext) : ISpellCheckFixerProvider + private readonly IThreadingContext _threadingContext = threadingContext; + + public Task RenameWordAsync( + SnapshotSpan span, + string replacement, + IUIThreadOperationContext operationContext) { - private readonly IThreadingContext _threadingContext = threadingContext; + var cancellationToken = operationContext.UserCancellationToken; + return RenameWordAsync(span, replacement, cancellationToken); + } - public Task RenameWordAsync( - SnapshotSpan span, - string replacement, - IUIThreadOperationContext operationContext) - { - var cancellationToken = operationContext.UserCancellationToken; - return RenameWordAsync(span, replacement, cancellationToken); - } + private async Task<(FunctionId functionId, string? message)?> RenameWordAsync( + SnapshotSpan span, + string replacement, + CancellationToken cancellationToken) + { + var result = await TryRenameAsync(span, replacement, cancellationToken).ConfigureAwait(false); - private async Task<(FunctionId functionId, string? message)?> RenameWordAsync( - SnapshotSpan span, - string replacement, - CancellationToken cancellationToken) + // If we succeeded at renaming then nothing more to do. + if (result != null) { - var result = await TryRenameAsync(span, replacement, cancellationToken).ConfigureAwait(false); + // Record why we failed so we can determine what issues may be arising in the wild. + var (functionId, message) = result.Value; + Logger.Log(functionId, message); - // If we succeeded at renaming then nothing more to do. - if (result != null) - { - // Record why we failed so we can determine what issues may be arising in the wild. - var (functionId, message) = result.Value; - Logger.Log(functionId, message); - - // Then just apply the text change directly. - await ApplySimpleChangeAsync(span, replacement, cancellationToken).ConfigureAwait(false); - } - - return result; + // Then just apply the text change directly. + await ApplySimpleChangeAsync(span, replacement, cancellationToken).ConfigureAwait(false); } - private async Task ApplySimpleChangeAsync(SnapshotSpan span, string replacement, CancellationToken cancellationToken) - { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + return result; + } + + private async Task ApplySimpleChangeAsync(SnapshotSpan span, string replacement, CancellationToken cancellationToken) + { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var buffer = span.Snapshot.TextBuffer; - var edit = buffer.CreateEdit(); + var buffer = span.Snapshot.TextBuffer; + var edit = buffer.CreateEdit(); - edit.Replace(span.TranslateTo(buffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive), replacement); - edit.Apply(); - } + edit.Replace(span.TranslateTo(buffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive), replacement); + edit.Apply(); + } - private async Task<(FunctionId functionId, string? message)?> TryRenameAsync(SnapshotSpan span, string replacement, CancellationToken cancellationToken) - { - // See if we can map this to a roslyn document. - var snapshot = span.Snapshot; - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document is null) - return (FunctionId.SpellCheckFixer_CouldNotFindDocument, null); - - // If so, see if the language supports smart rename capabilities. - var renameService = document.GetLanguageService(); - if (renameService is null) - return (FunctionId.SpellCheckFixer_LanguageDoesNotSupportRename, null); - - // Attempt to figure out what the language would rename here given the position of the misspelled word in - // the full token. - var info = await renameService.GetRenameInfoAsync(document, span.Span.Start, cancellationToken).ConfigureAwait(false); - if (!info.CanRename) - return (FunctionId.SpellCheckFixer_LanguageCouldNotGetRenameInfo, null); - - // The subspan we're being asked to rename better fall entirely within the span of the token we're renaming. - var fullTokenSpan = info.TriggerSpan; - var subSpanBeingRenamed = span.Span.ToTextSpan(); - if (!fullTokenSpan.Contains(subSpanBeingRenamed)) - return (FunctionId.SpellCheckFixer_RenameSpanNotWithinTokenSpan, null); - - // Now attempt to call into the language to actually perform the rename. - var options = new SymbolRenameOptions(); - var renameLocations = await info.FindRenameLocationsAsync(options, cancellationToken).ConfigureAwait(false); - var replacements = await renameLocations.GetReplacementsAsync(replacement, options, cancellationToken).ConfigureAwait(false); - if (!replacements.ReplacementTextValid) - return (FunctionId.SpellCheckFixer_ReplacementTextInvalid, $"Renaming: '{span.GetText()}' to '{replacement}'"); - - // Finally, apply the rename to the solution. - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var workspace = document.Project.Solution.Workspace; - if (!workspace.TryApplyChanges(replacements.NewSolution)) - return (FunctionId.SpellCheckFixer_TryApplyChangesFailure, null); - - return null; - } + private async Task<(FunctionId functionId, string? message)?> TryRenameAsync(SnapshotSpan span, string replacement, CancellationToken cancellationToken) + { + // See if we can map this to a roslyn document. + var snapshot = span.Snapshot; + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document is null) + return (FunctionId.SpellCheckFixer_CouldNotFindDocument, null); + + // If so, see if the language supports smart rename capabilities. + var renameService = document.GetLanguageService(); + if (renameService is null) + return (FunctionId.SpellCheckFixer_LanguageDoesNotSupportRename, null); + + // Attempt to figure out what the language would rename here given the position of the misspelled word in + // the full token. + var info = await renameService.GetRenameInfoAsync(document, span.Span.Start, cancellationToken).ConfigureAwait(false); + if (!info.CanRename) + return (FunctionId.SpellCheckFixer_LanguageCouldNotGetRenameInfo, null); + + // The subspan we're being asked to rename better fall entirely within the span of the token we're renaming. + var fullTokenSpan = info.TriggerSpan; + var subSpanBeingRenamed = span.Span.ToTextSpan(); + if (!fullTokenSpan.Contains(subSpanBeingRenamed)) + return (FunctionId.SpellCheckFixer_RenameSpanNotWithinTokenSpan, null); + + // Now attempt to call into the language to actually perform the rename. + var options = new SymbolRenameOptions(); + var renameLocations = await info.FindRenameLocationsAsync(options, cancellationToken).ConfigureAwait(false); + var replacements = await renameLocations.GetReplacementsAsync(replacement, options, cancellationToken).ConfigureAwait(false); + if (!replacements.ReplacementTextValid) + return (FunctionId.SpellCheckFixer_ReplacementTextInvalid, $"Renaming: '{span.GetText()}' to '{replacement}'"); + + // Finally, apply the rename to the solution. + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var workspace = document.Project.Solution.Workspace; + if (!workspace.TryApplyChanges(replacements.NewSolution)) + return (FunctionId.SpellCheckFixer_TryApplyChangesFailure, null); + + return null; + } - public TestAccessor GetTestAccessor() - => new(this); + public TestAccessor GetTestAccessor() + => new(this); - public readonly struct TestAccessor(RoslynSpellCheckFixerProvider provider) - { - private readonly RoslynSpellCheckFixerProvider _provider = provider; + public readonly struct TestAccessor(RoslynSpellCheckFixerProvider provider) + { + private readonly RoslynSpellCheckFixerProvider _provider = provider; - public Task<(FunctionId functionId, string? message)?> TryRenameAsync(SnapshotSpan span, string replacement, CancellationToken cancellationToken) - => _provider.RenameWordAsync(span, replacement, cancellationToken); - } + public Task<(FunctionId functionId, string? message)?> TryRenameAsync(SnapshotSpan span, string replacement, CancellationToken cancellationToken) + => _provider.RenameWordAsync(span, replacement, cancellationToken); } } diff --git a/src/EditorFeatures/Core/SplitComment/ISplitCommentService.cs b/src/EditorFeatures/Core/SplitComment/ISplitCommentService.cs index e3cf7a094f2aa..f06a65102c62b 100644 --- a/src/EditorFeatures/Core/SplitComment/ISplitCommentService.cs +++ b/src/EditorFeatures/Core/SplitComment/ISplitCommentService.cs @@ -4,12 +4,11 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Editor.Implementation.SplitComment +namespace Microsoft.CodeAnalysis.Editor.Implementation.SplitComment; + +internal interface ISplitCommentService : ILanguageService { - internal interface ISplitCommentService : ILanguageService - { - string CommentStart { get; } + string CommentStart { get; } - bool IsAllowed(SyntaxNode root, SyntaxTrivia trivia); - } + bool IsAllowed(SyntaxNode root, SyntaxTrivia trivia); } diff --git a/src/EditorFeatures/Core/SplitComment/SplitCommentCommandHandler.cs b/src/EditorFeatures/Core/SplitComment/SplitCommentCommandHandler.cs index 5b681d71203e5..c09f5bc3bff08 100644 --- a/src/EditorFeatures/Core/SplitComment/SplitCommentCommandHandler.cs +++ b/src/EditorFeatures/Core/SplitComment/SplitCommentCommandHandler.cs @@ -24,229 +24,228 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.SplitComment +namespace Microsoft.CodeAnalysis.Editor.Implementation.SplitComment; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.RoslynContentType)] +[Name(nameof(SplitCommentCommandHandler))] +[Order(After = PredefinedCompletionNames.CompletionCommandHandler)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class SplitCommentCommandHandler( + ITextUndoHistoryRegistry undoHistoryRegistry, + IEditorOperationsFactoryService editorOperationsFactoryService, + EditorOptionsService editorOptionsService, + IIndentationManagerService indentationManager, + IGlobalOptionService globalOptions) : ICommandHandler { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.RoslynContentType)] - [Name(nameof(SplitCommentCommandHandler))] - [Order(After = PredefinedCompletionNames.CompletionCommandHandler)] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class SplitCommentCommandHandler( - ITextUndoHistoryRegistry undoHistoryRegistry, - IEditorOperationsFactoryService editorOperationsFactoryService, - EditorOptionsService editorOptionsService, - IIndentationManagerService indentationManager, - IGlobalOptionService globalOptions) : ICommandHandler - { - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; - private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; - private readonly EditorOptionsService _editorOptionsService = editorOptionsService; - private readonly IIndentationManagerService _indentationManager = indentationManager; - private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService; + private readonly EditorOptionsService _editorOptionsService = editorOptionsService; + private readonly IIndentationManagerService _indentationManager = indentationManager; + private readonly IGlobalOptionService _globalOptions = globalOptions; - public string DisplayName => EditorFeaturesResources.Split_comment; + public string DisplayName => EditorFeaturesResources.Split_comment; - public CommandState GetCommandState(ReturnKeyCommandArgs args) - => CommandState.Unspecified; + public CommandState GetCommandState(ReturnKeyCommandArgs args) + => CommandState.Unspecified; - public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) - { - var textView = args.TextView; - var subjectBuffer = args.SubjectBuffer; - var spans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); + public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context) + { + var textView = args.TextView; + var subjectBuffer = args.SubjectBuffer; + var spans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer); - // Don't do anything special if there is multi-selection. It's not clear what sort of semantics that should have. - if (spans.Count != 1) - return false; + // Don't do anything special if there is multi-selection. It's not clear what sort of semantics that should have. + if (spans.Count != 1) + return false; - var snapshot = subjectBuffer.CurrentSnapshot; - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return false; + var snapshot = subjectBuffer.CurrentSnapshot; + var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + return false; - if (!_globalOptions.GetOption(SplitCommentOptionsStorage.Enabled, document.Project.Language)) - return false; + if (!_globalOptions.GetOption(SplitCommentOptionsStorage.Enabled, document.Project.Language)) + return false; - var splitCommentService = document.GetLanguageService(); - if (splitCommentService == null) - return false; + var splitCommentService = document.GetLanguageService(); + if (splitCommentService == null) + return false; - // If there is a selection, ensure that it's all on one-line. It's not clear what sort of semantics we - // would want if this spanned multiple lines. - var selectionSpan = spans[0].Span; - var position = selectionSpan.Start; - var line = subjectBuffer.CurrentSnapshot.GetLineFromPosition(position); - var endLine = subjectBuffer.CurrentSnapshot.GetLineFromPosition(selectionSpan.End); - if (line.LineNumber != endLine.LineNumber) - return false; + // If there is a selection, ensure that it's all on one-line. It's not clear what sort of semantics we + // would want if this spanned multiple lines. + var selectionSpan = spans[0].Span; + var position = selectionSpan.Start; + var line = subjectBuffer.CurrentSnapshot.GetLineFromPosition(position); + var endLine = subjectBuffer.CurrentSnapshot.GetLineFromPosition(selectionSpan.End); + if (line.LineNumber != endLine.LineNumber) + return false; + + // Quick check. If the line doesn't contain a comment in it before the caret, + // then no point in doing any more expensive synchronous work. + if (!LineProbablyContainsComment(splitCommentService, new SnapshotPoint(snapshot, position))) + return false; - // Quick check. If the line doesn't contain a comment in it before the caret, - // then no point in doing any more expensive synchronous work. - if (!LineProbablyContainsComment(splitCommentService, new SnapshotPoint(snapshot, position))) + using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Split_comment)) + { + var cancellationToken = context.OperationContext.UserCancellationToken; + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); + var result = SplitComment(parsedDocument, textView, subjectBuffer, new SnapshotSpan(snapshot, selectionSpan)); + if (result == null) return false; - using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Split_comment)) - { - var cancellationToken = context.OperationContext.UserCancellationToken; - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); - var result = SplitComment(parsedDocument, textView, subjectBuffer, new SnapshotSpan(snapshot, selectionSpan)); - if (result == null) - return false; + using var transaction = CaretPreservingEditTransaction.TryCreate( + EditorFeaturesResources.Split_comment, textView, _undoHistoryRegistry, _editorOperationsFactoryService); - using var transaction = CaretPreservingEditTransaction.TryCreate( - EditorFeaturesResources.Split_comment, textView, _undoHistoryRegistry, _editorOperationsFactoryService); + subjectBuffer.Replace(result.Value.replacementSpan, result.Value.replacementText); - subjectBuffer.Replace(result.Value.replacementSpan, result.Value.replacementText); + transaction?.Complete(); + return true; + } + } + + private static bool LineProbablyContainsComment(ISplitCommentService service, SnapshotPoint position) + { + var commentStart = service.CommentStart; + var line = position.GetContainingLine(); - transaction?.Complete(); + for (var p = line.Start.Position; p < position; p++) + { + if (MatchesCommentStart(commentStart, line, p)) return true; - } } - private static bool LineProbablyContainsComment(ISplitCommentService service, SnapshotPoint position) - { - var commentStart = service.CommentStart; - var line = position.GetContainingLine(); + return false; + } - for (var p = line.Start.Position; p < position; p++) - { - if (MatchesCommentStart(commentStart, line, p)) - return true; - } + private static bool MatchesCommentStart(string commentStart, SnapshotPoint point) + => MatchesCommentStart(commentStart, point.GetContainingLine(), point.Position); + private static bool MatchesCommentStart(string commentStart, ITextSnapshotLine line, int position) + { + if (position + commentStart.Length > line.End) return false; - } - - private static bool MatchesCommentStart(string commentStart, SnapshotPoint point) - => MatchesCommentStart(commentStart, point.GetContainingLine(), point.Position); - private static bool MatchesCommentStart(string commentStart, ITextSnapshotLine line, int position) + var snapshot = line.Snapshot; + for (var c = 0; c < commentStart.Length; c++) { - if (position + commentStart.Length > line.End) + if (snapshot[position + c] != commentStart[c]) return false; - - var snapshot = line.Snapshot; - for (var c = 0; c < commentStart.Length; c++) - { - if (snapshot[position + c] != commentStart[c]) - return false; - } - - return true; } - private (Span replacementSpan, string replacementText)? SplitComment( - ParsedDocument document, - ITextView textView, - ITextBuffer textBuffer, - SnapshotSpan selectionSpan) - { - var syntaxKinds = document.LanguageServices.GetRequiredService(); - var trivia = document.Root.FindTrivia(selectionSpan.Start); - if (syntaxKinds.SingleLineCommentTrivia != trivia.RawKind) - return null; - - var splitCommentService = document.LanguageServices.GetRequiredService(); - - // if the user hits enter at `/$$/` we don't want to consider this a comment continuation. - if (selectionSpan.Start < (trivia.SpanStart + splitCommentService.CommentStart.Length)) - return null; - - if (!splitCommentService.IsAllowed(document.Root, trivia)) - return null; - - // If the user hits enter at: // goo $$ // bar - // - // we don't want to consider this a comment continuation. They likely were doing some text manipulations - // that put two comments on the same line, and really just want this to act like a normal enter. - if (IsFollowedByComment(selectionSpan.End, splitCommentService)) - return null; - - var textSnapshot = selectionSpan.Snapshot; - var triviaLine = textSnapshot.GetLineFromPosition(trivia.SpanStart); - - var options = textBuffer.GetLineFormattingOptions(_editorOptionsService, explicitFormat: false); - var replacementSpan = GetReplacementSpan(triviaLine, selectionSpan); - var replacementText = GetReplacementText(textView, options, triviaLine, trivia, selectionSpan.Start); - return (replacementSpan, replacementText); - } + return true; + } - private static bool IsFollowedByComment(SnapshotPoint point, ISplitCommentService splitCommentService) - { - var line = point.GetContainingLine(); + private (Span replacementSpan, string replacementText)? SplitComment( + ParsedDocument document, + ITextView textView, + ITextBuffer textBuffer, + SnapshotSpan selectionSpan) + { + var syntaxKinds = document.LanguageServices.GetRequiredService(); + var trivia = document.Root.FindTrivia(selectionSpan.Start); + if (syntaxKinds.SingleLineCommentTrivia != trivia.RawKind) + return null; + + var splitCommentService = document.LanguageServices.GetRequiredService(); + + // if the user hits enter at `/$$/` we don't want to consider this a comment continuation. + if (selectionSpan.Start < (trivia.SpanStart + splitCommentService.CommentStart.Length)) + return null; + + if (!splitCommentService.IsAllowed(document.Root, trivia)) + return null; + + // If the user hits enter at: // goo $$ // bar + // + // we don't want to consider this a comment continuation. They likely were doing some text manipulations + // that put two comments on the same line, and really just want this to act like a normal enter. + if (IsFollowedByComment(selectionSpan.End, splitCommentService)) + return null; + + var textSnapshot = selectionSpan.Snapshot; + var triviaLine = textSnapshot.GetLineFromPosition(trivia.SpanStart); + + var options = textBuffer.GetLineFormattingOptions(_editorOptionsService, explicitFormat: false); + var replacementSpan = GetReplacementSpan(triviaLine, selectionSpan); + var replacementText = GetReplacementText(textView, options, triviaLine, trivia, selectionSpan.Start); + return (replacementSpan, replacementText); + } - // skip past following whitespace. - while (point < line.End && char.IsWhiteSpace(point.GetChar())) - point += 1; + private static bool IsFollowedByComment(SnapshotPoint point, ISplitCommentService splitCommentService) + { + var line = point.GetContainingLine(); - return MatchesCommentStart(splitCommentService.CommentStart, point); - } + // skip past following whitespace. + while (point < line.End && char.IsWhiteSpace(point.GetChar())) + point += 1; - private static string GetReplacementText( - ITextView textView, LineFormattingOptions options, ITextSnapshotLine triviaLine, SyntaxTrivia trivia, int position) - { - // We're inside a comment. Instead of inserting just a newline here, insert - // 1. a newline - // 2. spaces up to the indentation of the current comment - // 3. the comment prefix (extended out for repeated chars). + return MatchesCommentStart(splitCommentService.CommentStart, point); + } - // Then, depending on if the current comment starts with whitespace or not, we will insert those same spaces - // to match. + private static string GetReplacementText( + ITextView textView, LineFormattingOptions options, ITextSnapshotLine triviaLine, SyntaxTrivia trivia, int position) + { + // We're inside a comment. Instead of inserting just a newline here, insert + // 1. a newline + // 2. spaces up to the indentation of the current comment + // 3. the comment prefix (extended out for repeated chars). - var commentStartColumn = triviaLine.GetColumnFromLineOffset(trivia.SpanStart - triviaLine.Start, textView.Options); + // Then, depending on if the current comment starts with whitespace or not, we will insert those same spaces + // to match. - var prefix = GetCommentPrefix(triviaLine.Snapshot, trivia, position); - var replacementText = options.NewLine + - commentStartColumn.CreateIndentationString(options.UseTabs, options.TabSize) + - prefix + - GetWhitespaceAfterCommentPrefix(trivia, triviaLine, prefix, position); + var commentStartColumn = triviaLine.GetColumnFromLineOffset(trivia.SpanStart - triviaLine.Start, textView.Options); - return replacementText; - } + var prefix = GetCommentPrefix(triviaLine.Snapshot, trivia, position); + var replacementText = options.NewLine + + commentStartColumn.CreateIndentationString(options.UseTabs, options.TabSize) + + prefix + + GetWhitespaceAfterCommentPrefix(trivia, triviaLine, prefix, position); - private static string GetCommentPrefix(ITextSnapshot snapshot, SyntaxTrivia trivia, int position) - { - // Consume as many of the comment start character as we can. That way if someone has something like - // `//// $$Goo` then hitting enter will respect that the next line should start with `////`. + return replacementText; + } - var triviaPrefixStart = trivia.SpanStart; - var triviaPrefixEnd = triviaPrefixStart; + private static string GetCommentPrefix(ITextSnapshot snapshot, SyntaxTrivia trivia, int position) + { + // Consume as many of the comment start character as we can. That way if someone has something like + // `//// $$Goo` then hitting enter will respect that the next line should start with `////`. - var triviaStartChar = snapshot[trivia.SpanStart]; - while (snapshot[triviaPrefixEnd] == triviaStartChar && triviaPrefixEnd < position) - triviaPrefixEnd++; + var triviaPrefixStart = trivia.SpanStart; + var triviaPrefixEnd = triviaPrefixStart; - return snapshot.GetText(Span.FromBounds(triviaPrefixStart, triviaPrefixEnd)); - } + var triviaStartChar = snapshot[trivia.SpanStart]; + while (snapshot[triviaPrefixEnd] == triviaStartChar && triviaPrefixEnd < position) + triviaPrefixEnd++; - private static string GetWhitespaceAfterCommentPrefix(SyntaxTrivia trivia, ITextSnapshotLine triviaLine, string commentPrefix, int position) - { - var startIndex = trivia.SpanStart + commentPrefix.Length; - var endIndex = startIndex; + return snapshot.GetText(Span.FromBounds(triviaPrefixStart, triviaPrefixEnd)); + } - while (endIndex < position && char.IsWhiteSpace(triviaLine.Snapshot[endIndex])) - endIndex++; + private static string GetWhitespaceAfterCommentPrefix(SyntaxTrivia trivia, ITextSnapshotLine triviaLine, string commentPrefix, int position) + { + var startIndex = trivia.SpanStart + commentPrefix.Length; + var endIndex = startIndex; - return triviaLine.Snapshot.GetText(Span.FromBounds(startIndex, endIndex)); - } + while (endIndex < position && char.IsWhiteSpace(triviaLine.Snapshot[endIndex])) + endIndex++; - private static Span GetReplacementSpan(ITextSnapshotLine triviaLine, Span selectionSpan) - { - var textSnapshot = triviaLine.Snapshot; + return triviaLine.Snapshot.GetText(Span.FromBounds(startIndex, endIndex)); + } - // When hitting enter in a comment consume the whitespace around the caret. That way the previous line - // doesn't have trailing whitespace, and the text following the caret is placed at the right location. - var replacementStart = selectionSpan.Start; - var replacementEnd = selectionSpan.End; - while (replacementStart > triviaLine.Start && textSnapshot[replacementStart - 1] == ' ') - replacementStart--; + private static Span GetReplacementSpan(ITextSnapshotLine triviaLine, Span selectionSpan) + { + var textSnapshot = triviaLine.Snapshot; - while (replacementEnd < triviaLine.End && textSnapshot[replacementEnd] == ' ') - replacementEnd++; + // When hitting enter in a comment consume the whitespace around the caret. That way the previous line + // doesn't have trailing whitespace, and the text following the caret is placed at the right location. + var replacementStart = selectionSpan.Start; + var replacementEnd = selectionSpan.End; + while (replacementStart > triviaLine.Start && textSnapshot[replacementStart - 1] == ' ') + replacementStart--; - var replacementSpan = Span.FromBounds(replacementStart, replacementEnd); - return replacementSpan; - } + while (replacementEnd < triviaLine.End && textSnapshot[replacementEnd] == ' ') + replacementEnd++; + + var replacementSpan = Span.FromBounds(replacementStart, replacementEnd); + return replacementSpan; } } diff --git a/src/EditorFeatures/Core/SplitComment/SplitCommentOptionsStorage.cs b/src/EditorFeatures/Core/SplitComment/SplitCommentOptionsStorage.cs index c8120ac528191..b26c393f1f34b 100644 --- a/src/EditorFeatures/Core/SplitComment/SplitCommentOptionsStorage.cs +++ b/src/EditorFeatures/Core/SplitComment/SplitCommentOptionsStorage.cs @@ -4,11 +4,10 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Editor.Implementation.SplitComment +namespace Microsoft.CodeAnalysis.Editor.Implementation.SplitComment; + +internal sealed class SplitCommentOptionsStorage { - internal sealed class SplitCommentOptionsStorage - { - public static PerLanguageOption2 Enabled = - new PerLanguageOption2("dotnet_split_comments", defaultValue: true); - } + public static PerLanguageOption2 Enabled = + new PerLanguageOption2("dotnet_split_comments", defaultValue: true); } diff --git a/src/EditorFeatures/Core/StringCopyPaste/IStringCopyPasteService.cs b/src/EditorFeatures/Core/StringCopyPaste/IStringCopyPasteService.cs index b9655fc00f97a..cf0ccd2d910f1 100644 --- a/src/EditorFeatures/Core/StringCopyPaste/IStringCopyPasteService.cs +++ b/src/EditorFeatures/Core/StringCopyPaste/IStringCopyPasteService.cs @@ -7,32 +7,31 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.Editor.StringCopyPaste +namespace Microsoft.CodeAnalysis.Editor.StringCopyPaste; + +internal interface IStringCopyPasteService : IWorkspaceService { - internal interface IStringCopyPasteService : IWorkspaceService - { - bool TrySetClipboardData(string key, string data); - string? TryGetClipboardData(string key); - } + bool TrySetClipboardData(string key, string data); + string? TryGetClipboardData(string key); +} - [ExportWorkspaceService(typeof(IStringCopyPasteService)), Shared] - internal class DefaultStringCopyPasteService : IStringCopyPasteService +[ExportWorkspaceService(typeof(IStringCopyPasteService)), Shared] +internal class DefaultStringCopyPasteService : IStringCopyPasteService +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DefaultStringCopyPasteService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public DefaultStringCopyPasteService() - { - } + } - // Note: we very intentionally do not try to store/retrieve any data in this default implementation. at this - // layer we have no information about the clipboard, so it would be dangerous to presume that that information - // had been validly associated with latest clipboard operation and had not been affected by things outside our - // awareness. + // Note: we very intentionally do not try to store/retrieve any data in this default implementation. at this + // layer we have no information about the clipboard, so it would be dangerous to presume that that information + // had been validly associated with latest clipboard operation and had not been affected by things outside our + // awareness. - public bool TrySetClipboardData(string key, string data) - => false; + public bool TrySetClipboardData(string key, string data) + => false; - public string? TryGetClipboardData(string key) - => null; - } + public string? TryGetClipboardData(string key) + => null; } diff --git a/src/EditorFeatures/Core/StringCopyPaste/StringCopyPasteOptionsStorage.cs b/src/EditorFeatures/Core/StringCopyPaste/StringCopyPasteOptionsStorage.cs index baac3b89e74ea..c764e7c5a730a 100644 --- a/src/EditorFeatures/Core/StringCopyPaste/StringCopyPasteOptionsStorage.cs +++ b/src/EditorFeatures/Core/StringCopyPaste/StringCopyPasteOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.StringCopyPaste +namespace Microsoft.CodeAnalysis.StringCopyPaste; + +internal static class StringCopyPasteOptionsStorage { - internal static class StringCopyPasteOptionsStorage - { - public static readonly PerLanguageOption2 AutomaticallyFixStringContentsOnPaste = new("dotnet_fix_string_contents_on_paste", defaultValue: true); - } + public static readonly PerLanguageOption2 AutomaticallyFixStringContentsOnPaste = new("dotnet_fix_string_contents_on_paste", defaultValue: true); } diff --git a/src/EditorFeatures/Core/StringIndentation/StringIndentationOptionsStorage.cs b/src/EditorFeatures/Core/StringIndentation/StringIndentationOptionsStorage.cs index f5b2db2259aa6..9b23b3f64daec 100644 --- a/src/EditorFeatures/Core/StringIndentation/StringIndentationOptionsStorage.cs +++ b/src/EditorFeatures/Core/StringIndentation/StringIndentationOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.StringIndentation +namespace Microsoft.CodeAnalysis.StringIndentation; + +internal static class StringIndentationOptionsStorage { - internal static class StringIndentationOptionsStorage - { - public static readonly PerLanguageOption2 StringIdentation = new("dotnet_indent_strings", defaultValue: true); - } + public static readonly PerLanguageOption2 StringIdentation = new("dotnet_indent_strings", defaultValue: true); } diff --git a/src/EditorFeatures/Core/Structure/AbstractStructureTaggerProvider.cs b/src/EditorFeatures/Core/Structure/AbstractStructureTaggerProvider.cs index 1ad6a1c00b05d..1fcca98b5e563 100644 --- a/src/EditorFeatures/Core/Structure/AbstractStructureTaggerProvider.cs +++ b/src/EditorFeatures/Core/Structure/AbstractStructureTaggerProvider.cs @@ -28,342 +28,342 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.Structure -{ - /// - /// Shared implementation of the outliner tagger provider. - /// - /// Note: the outliner tagger is a normal buffer tagger provider and not a view tagger provider. - /// This is important for two reasons. The first is that if it were view-based then we would lose - /// the state of the collapsed/open regions when they scrolled in and out of view. Also, if the - /// editor doesn't know about all the regions in the file, then it wouldn't be able to - /// persist them to the SUO file to persist this data across sessions. - /// +namespace Microsoft.CodeAnalysis.Editor.Implementation.Structure; + +/// +/// Shared implementation of the outliner tagger provider. +/// +/// Note: the outliner tagger is a normal buffer tagger provider and not a view tagger provider. +/// This is important for two reasons. The first is that if it were view-based then we would lose +/// the state of the collapsed/open regions when they scrolled in and out of view. Also, if the +/// editor doesn't know about all the regions in the file, then it wouldn't be able to +/// persist them to the SUO file to persist this data across sessions. +/// #pragma warning disable CS0618 // Type or member is obsolete - internal abstract partial class AbstractStructureTaggerProvider : AsynchronousTaggerProvider +internal abstract partial class AbstractStructureTaggerProvider : AsynchronousTaggerProvider +{ + private const string RegionDirective = "#region"; + private const string UsingDirective = "using"; + private const string ExternDeclaration = "extern"; + private const string ImportsStatement = "Imports"; + + protected readonly EditorOptionsService EditorOptionsService; + protected readonly IProjectionBufferFactoryService ProjectionBufferFactoryService; + + protected AbstractStructureTaggerProvider( + IThreadingContext threadingContext, + EditorOptionsService editorOptionsService, + IProjectionBufferFactoryService projectionBufferFactoryService, + ITextBufferVisibilityTracker? visibilityTracker, + IAsynchronousOperationListenerProvider listenerProvider) + : base(threadingContext, editorOptionsService.GlobalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.Outlining)) + { + EditorOptionsService = editorOptionsService; + ProjectionBufferFactoryService = projectionBufferFactoryService; + } + + protected override TaggerDelay EventChangeDelay => TaggerDelay.OnIdle; + + protected override bool ComputeInitialTagsSynchronously(ITextBuffer subjectBuffer) { - private const string RegionDirective = "#region"; - private const string UsingDirective = "using"; - private const string ExternDeclaration = "extern"; - private const string ImportsStatement = "Imports"; - - protected readonly EditorOptionsService EditorOptionsService; - protected readonly IProjectionBufferFactoryService ProjectionBufferFactoryService; - - protected AbstractStructureTaggerProvider( - IThreadingContext threadingContext, - EditorOptionsService editorOptionsService, - IProjectionBufferFactoryService projectionBufferFactoryService, - ITextBufferVisibilityTracker? visibilityTracker, - IAsynchronousOperationListenerProvider listenerProvider) - : base(threadingContext, editorOptionsService.GlobalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.Outlining)) + // If we can't find this doc, or outlining is not enabled for it, no need to computed anything synchronously. + + var openDocument = subjectBuffer.AsTextContainer().GetRelatedDocuments().FirstOrDefault(); + if (openDocument == null) + return false; + + // If the main Outlining option is turned off, we can just skip computing tags synchronously + // so when the document first opens, there won't be any tags yet. When the tags do come in + // the IsDefaultCollapsed property, which controls the initial collapsing, won't have any effect + // because the document will already be open. + if (!GlobalOptions.GetOption(OutliningOptionsStorage.Outlining, openDocument.Project.Language)) + return false; + + var options = BlockStructureOptionsStorage.GetBlockStructureOptions(GlobalOptions, openDocument.Project); + + // If we're a metadata-as-source doc, we need to compute the initial set of tags synchronously + // so that we can collapse all the .IsImplementation tags to keep the UI clean and condensed. + if (openDocument.Project.Solution.Workspace is MetadataAsSourceWorkspace masWorkspace && + masWorkspace.FileService.ShouldCollapseOnOpen(openDocument.FilePath, options)) { - EditorOptionsService = editorOptionsService; - ProjectionBufferFactoryService = projectionBufferFactoryService; + return true; } - protected override TaggerDelay EventChangeDelay => TaggerDelay.OnIdle; + // If the user wants to collapse imports or #regions then we need to compute + // synchronously, but only if there are imports or #regions in the file. To + // save some work, we'll look for both in a single pass. + var collapseRegions = GlobalOptions.GetOption(BlockStructureOptionsStorage.CollapseRegionsWhenFirstOpened, openDocument.Project.Language); + var collapseImports = GlobalOptions.GetOption(BlockStructureOptionsStorage.CollapseImportsWhenFirstOpened, openDocument.Project.Language); - protected override bool ComputeInitialTagsSynchronously(ITextBuffer subjectBuffer) + if (!collapseRegions && !collapseImports) { - // If we can't find this doc, or outlining is not enabled for it, no need to computed anything synchronously. - - var openDocument = subjectBuffer.AsTextContainer().GetRelatedDocuments().FirstOrDefault(); - if (openDocument == null) - return false; + return false; + } - // If the main Outlining option is turned off, we can just skip computing tags synchronously - // so when the document first opens, there won't be any tags yet. When the tags do come in - // the IsDefaultCollapsed property, which controls the initial collapsing, won't have any effect - // because the document will already be open. - if (!GlobalOptions.GetOption(OutliningOptionsStorage.Outlining, openDocument.Project.Language)) - return false; + if (ContainsRegionOrImport(subjectBuffer.CurrentSnapshot, collapseRegions, collapseImports, openDocument.Project.Language)) + { + return true; + } - var options = BlockStructureOptionsStorage.GetBlockStructureOptions(GlobalOptions, openDocument.Project); + return false; + } - // If we're a metadata-as-source doc, we need to compute the initial set of tags synchronously - // so that we can collapse all the .IsImplementation tags to keep the UI clean and condensed. - if (openDocument.Project.Solution.Workspace is MetadataAsSourceWorkspace masWorkspace && - masWorkspace.FileService.ShouldCollapseOnOpen(openDocument.FilePath, options)) + // Internal for testing + internal static bool ContainsRegionOrImport(ITextSnapshot textSnapshot, bool collapseRegions, bool collapseImports, string language) + { + foreach (var line in textSnapshot.Lines) + { + if (collapseRegions && StartsWithRegionTag(line)) { return true; } - - // If the user wants to collapse imports or #regions then we need to compute - // synchronously, but only if there are imports or #regions in the file. To - // save some work, we'll look for both in a single pass. - var collapseRegions = GlobalOptions.GetOption(BlockStructureOptionsStorage.CollapseRegionsWhenFirstOpened, openDocument.Project.Language); - var collapseImports = GlobalOptions.GetOption(BlockStructureOptionsStorage.CollapseImportsWhenFirstOpened, openDocument.Project.Language); - - if (!collapseRegions && !collapseImports) - { - return false; - } - - if (ContainsRegionOrImport(subjectBuffer.CurrentSnapshot, collapseRegions, collapseImports, openDocument.Project.Language)) + else if (collapseImports && IsImport(line, language)) { return true; } - - return false; } - // Internal for testing - internal static bool ContainsRegionOrImport(ITextSnapshot textSnapshot, bool collapseRegions, bool collapseImports, string language) + return false; + + static bool StartsWithRegionTag(ITextSnapshotLine line) { - foreach (var line in textSnapshot.Lines) - { - if (collapseRegions && StartsWithRegionTag(line)) - { - return true; - } - else if (collapseImports && IsImport(line, language)) - { - return true; - } - } + var start = line.GetFirstNonWhitespacePosition(); + return start != null && line.StartsWith(start.Value, RegionDirective, ignoreCase: true); + } - return false; + static bool IsImport(ITextSnapshotLine line, string language) + { + var start = line.GetFirstNonWhitespacePosition(); + if (start is null) + return false; - static bool StartsWithRegionTag(ITextSnapshotLine line) + // For VB we only need to find "Imports" at the start of a line + if (language == LanguageNames.VisualBasic) { - var start = line.GetFirstNonWhitespacePosition(); - return start != null && line.StartsWith(start.Value, RegionDirective, ignoreCase: true); + return line.StartsWith(start.Value, ImportsStatement, ignoreCase: true); } - static bool IsImport(ITextSnapshotLine line, string language) - { - var start = line.GetFirstNonWhitespacePosition(); - if (start is null) - return false; - - // For VB we only need to find "Imports" at the start of a line - if (language == LanguageNames.VisualBasic) - { - return line.StartsWith(start.Value, ImportsStatement, ignoreCase: true); - } - - // For the purposes of collapsing, extern aliases are grouped with usings - if (line.StartsWith(start.Value, ExternDeclaration, ignoreCase: false)) - return true; + // For the purposes of collapsing, extern aliases are grouped with usings + if (line.StartsWith(start.Value, ExternDeclaration, ignoreCase: false)) + return true; - return line.StartsWith(start.Value, UsingDirective, ignoreCase: false); - } + return line.StartsWith(start.Value, UsingDirective, ignoreCase: false); } + } - protected sealed override ITaggerEventSource CreateEventSource(ITextView? textView, ITextBuffer subjectBuffer) - { - // We listen to the following events: - // 1) Text changes. These can obviously affect outlining, so we need to recompute when - // we hear about them. - // 2) Parse option changes. These can affect outlining when, for example, we change from - // DEBUG to RELEASE (affecting the inactive/active regions). - // 3) When we hear about a workspace being registered. Outlining may run before a - // we even know about a workspace. This can happen, for example, in the TypeScript - // case. With TypeScript a file is opened, but the workspace is not generated until - // some time later when they have examined the file system. As such, initially, - // the file will not have outline spans. When the workspace is created, we want to - // then produce the right outlining spans. - return TaggerEventSources.Compose( - TaggerEventSources.OnTextChanged(subjectBuffer), - TaggerEventSources.OnParseOptionChanged(subjectBuffer), - TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowBlockStructureGuidesForCodeLevelConstructs), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowBlockStructureGuidesForDeclarationLevelConstructs), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowBlockStructureGuidesForCommentsAndPreprocessorRegions), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowOutliningForCodeLevelConstructs), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowOutliningForDeclarationLevelConstructs), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowOutliningForCommentsAndPreprocessorRegions), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.CollapseRegionsWhenCollapsingToDefinitions), - TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.CollapseLocalFunctionsWhenCollapsingToDefinitions)); - } + protected sealed override ITaggerEventSource CreateEventSource(ITextView? textView, ITextBuffer subjectBuffer) + { + // We listen to the following events: + // 1) Text changes. These can obviously affect outlining, so we need to recompute when + // we hear about them. + // 2) Parse option changes. These can affect outlining when, for example, we change from + // DEBUG to RELEASE (affecting the inactive/active regions). + // 3) When we hear about a workspace being registered. Outlining may run before a + // we even know about a workspace. This can happen, for example, in the TypeScript + // case. With TypeScript a file is opened, but the workspace is not generated until + // some time later when they have examined the file system. As such, initially, + // the file will not have outline spans. When the workspace is created, we want to + // then produce the right outlining spans. + return TaggerEventSources.Compose( + TaggerEventSources.OnTextChanged(subjectBuffer), + TaggerEventSources.OnParseOptionChanged(subjectBuffer), + TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowBlockStructureGuidesForCodeLevelConstructs), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowBlockStructureGuidesForDeclarationLevelConstructs), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowBlockStructureGuidesForCommentsAndPreprocessorRegions), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowOutliningForCodeLevelConstructs), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowOutliningForDeclarationLevelConstructs), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.ShowOutliningForCommentsAndPreprocessorRegions), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.CollapseRegionsWhenCollapsingToDefinitions), + TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, BlockStructureOptionsStorage.CollapseLocalFunctionsWhenCollapsingToDefinitions)); + } - protected sealed override async Task ProduceTagsAsync( - TaggerContext context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition, CancellationToken cancellationToken) + protected sealed override async Task ProduceTagsAsync( + TaggerContext context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition, CancellationToken cancellationToken) + { + try { - try - { - var document = documentSnapshotSpan.Document; - if (document == null) - return; + var document = documentSnapshotSpan.Document; + if (document == null) + return; - // Let LSP handle producing tags in the cloud scenario - if (documentSnapshotSpan.SnapshotSpan.Snapshot.TextBuffer.IsInLspEditorContext()) - return; + // Let LSP handle producing tags in the cloud scenario + if (documentSnapshotSpan.SnapshotSpan.Snapshot.TextBuffer.IsInLspEditorContext()) + return; - var outliningService = BlockStructureService.GetService(document); - if (outliningService == null) - return; + var outliningService = BlockStructureService.GetService(document); + if (outliningService == null) + return; - var options = GlobalOptions.GetBlockStructureOptions(document.Project); - var blockStructure = await outliningService.GetBlockStructureAsync( - documentSnapshotSpan.Document, options, cancellationToken).ConfigureAwait(false); + var options = GlobalOptions.GetBlockStructureOptions(document.Project); + var blockStructure = await outliningService.GetBlockStructureAsync( + documentSnapshotSpan.Document, options, cancellationToken).ConfigureAwait(false); - ProcessSpans( - context, documentSnapshotSpan.SnapshotSpan, outliningService, - blockStructure.Spans); - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) - { - throw ExceptionUtilities.Unreachable(); - } + ProcessSpans( + context, documentSnapshotSpan.SnapshotSpan, outliningService, + blockStructure.Spans); } - - private void ProcessSpans( - TaggerContext context, - SnapshotSpan snapshotSpan, - BlockStructureService outliningService, - ImmutableArray spans) + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) { - var snapshot = snapshotSpan.Snapshot; - spans = GetMultiLineRegions(outliningService, spans, snapshot); - - foreach (var span in spans) - { - var tag = new StructureTag(this, span, snapshot); - context.AddTag(new TagSpan(span.TextSpan.ToSnapshotSpan(snapshot), tag)); - } + throw ExceptionUtilities.Unreachable(); } + } - protected override bool TagEquals(IContainerStructureTag tag1, IContainerStructureTag tag2) + private void ProcessSpans( + TaggerContext context, + SnapshotSpan snapshotSpan, + BlockStructureService outliningService, + ImmutableArray spans) + { + var snapshot = snapshotSpan.Snapshot; + spans = GetMultiLineRegions(outliningService, spans, snapshot); + + foreach (var span in spans) { - Contract.ThrowIfFalse(tag1 is StructureTag); - Contract.ThrowIfFalse(tag2 is StructureTag); - return tag1.Equals(tag2); + var tag = new StructureTag(this, span, snapshot); + context.AddTag(new TagSpan(span.TextSpan.ToSnapshotSpan(snapshot), tag)); } + } - internal abstract object? GetCollapsedHintForm(StructureTag structureTag); + protected override bool TagEquals(IContainerStructureTag tag1, IContainerStructureTag tag2) + { + Contract.ThrowIfFalse(tag1 is StructureTag); + Contract.ThrowIfFalse(tag2 is StructureTag); + return tag1.Equals(tag2); + } - private static bool s_exceptionReported = false; + internal abstract object? GetCollapsedHintForm(StructureTag structureTag); - private static ImmutableArray GetMultiLineRegions( - BlockStructureService service, - ImmutableArray regions, ITextSnapshot snapshot) + private static bool s_exceptionReported = false; + + private static ImmutableArray GetMultiLineRegions( + BlockStructureService service, + ImmutableArray regions, ITextSnapshot snapshot) + { + // Remove any spans that aren't multiline. + var multiLineRegions = ArrayBuilder.GetInstance(); + foreach (var region in regions) { - // Remove any spans that aren't multiline. - var multiLineRegions = ArrayBuilder.GetInstance(); - foreach (var region in regions) + if (region.TextSpan.Length > 0) { - if (region.TextSpan.Length > 0) + // Check if any clients produced an invalid OutliningSpan. If so, filter them + // out and report a non-fatal watson so we can attempt to determine the source + // of the issue. + var snapshotSpan = snapshot.GetFullSpan().Span; + var regionSpan = region.TextSpan.ToSpan(); + if (!snapshotSpan.Contains(regionSpan)) { - // Check if any clients produced an invalid OutliningSpan. If so, filter them - // out and report a non-fatal watson so we can attempt to determine the source - // of the issue. - var snapshotSpan = snapshot.GetFullSpan().Span; - var regionSpan = region.TextSpan.ToSpan(); - if (!snapshotSpan.Contains(regionSpan)) + if (!s_exceptionReported) { - if (!s_exceptionReported) + s_exceptionReported = true; + try + { + throw new InvalidOutliningRegionException(service, snapshot, snapshotSpan, regionSpan); + } + catch (InvalidOutliningRegionException e) when (FatalError.ReportAndCatch(e)) { - s_exceptionReported = true; - try - { - throw new InvalidOutliningRegionException(service, snapshot, snapshotSpan, regionSpan); - } - catch (InvalidOutliningRegionException e) when (FatalError.ReportAndCatch(e)) - { - } } - - continue; } - var startLine = snapshot.GetLineNumberFromPosition(region.TextSpan.Start); - var endLine = snapshot.GetLineNumberFromPosition(region.TextSpan.End); - if (startLine != endLine) - { - multiLineRegions.Add(region); - } + continue; } - } - return multiLineRegions.ToImmutableAndFree(); + var startLine = snapshot.GetLineNumberFromPosition(region.TextSpan.Start); + var endLine = snapshot.GetLineNumberFromPosition(region.TextSpan.End); + if (startLine != endLine) + { + multiLineRegions.Add(region); + } + } } - #region Creating Preview Buffers + return multiLineRegions.ToImmutableAndFree(); + } - private const int MaxPreviewText = 1000; + #region Creating Preview Buffers - /// - /// Given a , creates an ITextBuffer with the content to display - /// in the tooltip. - /// - protected ITextBuffer CreateElisionBufferForTagTooltip(StructureTag tag) - { - // Remove any starting whitespace. - var span = TrimLeadingWhitespace(new SnapshotSpan(tag.Snapshot, tag.CollapsedHintFormSpan)); + private const int MaxPreviewText = 1000; - // Trim the length if it's too long. - var shortSpan = span; - if (span.Length > MaxPreviewText) - { - shortSpan = ComputeShortSpan(span); - } + /// + /// Given a , creates an ITextBuffer with the content to display + /// in the tooltip. + /// + protected ITextBuffer CreateElisionBufferForTagTooltip(StructureTag tag) + { + // Remove any starting whitespace. + var span = TrimLeadingWhitespace(new SnapshotSpan(tag.Snapshot, tag.CollapsedHintFormSpan)); - // Create an elision buffer for that span, also trimming the - // leading whitespace. - var elisionBuffer = CreateElisionBufferWithoutIndentation(shortSpan); - var finalBuffer = elisionBuffer; + // Trim the length if it's too long. + var shortSpan = span; + if (span.Length > MaxPreviewText) + { + shortSpan = ComputeShortSpan(span); + } - // If we trimmed the length, then make a projection buffer that - // has the above elision buffer and follows it with "..." - if (span.Length != shortSpan.Length) - { - finalBuffer = CreateTrimmedProjectionBuffer(elisionBuffer); - } + // Create an elision buffer for that span, also trimming the + // leading whitespace. + var elisionBuffer = CreateElisionBufferWithoutIndentation(shortSpan); + var finalBuffer = elisionBuffer; - return finalBuffer; + // If we trimmed the length, then make a projection buffer that + // has the above elision buffer and follows it with "..." + if (span.Length != shortSpan.Length) + { + finalBuffer = CreateTrimmedProjectionBuffer(elisionBuffer); } - private ITextBuffer CreateTrimmedProjectionBuffer(ITextBuffer elisionBuffer) - { - // The elision buffer is too long. We've already trimmed it, but now we want to add - // a "..." to it. We do that by creating a projection of both the elision buffer and - // a new text buffer wrapping the ellipsis. - var elisionSpan = elisionBuffer.CurrentSnapshot.GetFullSpan(); + return finalBuffer; + } - var sourceSpans = new List() - { - elisionSpan.Snapshot.CreateTrackingSpan(elisionSpan, SpanTrackingMode.EdgeExclusive), - "..." - }; + private ITextBuffer CreateTrimmedProjectionBuffer(ITextBuffer elisionBuffer) + { + // The elision buffer is too long. We've already trimmed it, but now we want to add + // a "..." to it. We do that by creating a projection of both the elision buffer and + // a new text buffer wrapping the ellipsis. + var elisionSpan = elisionBuffer.CurrentSnapshot.GetFullSpan(); - var projectionBuffer = ProjectionBufferFactoryService.CreateProjectionBuffer( - projectionEditResolver: null, - sourceSpans: sourceSpans, - options: ProjectionBufferOptions.None); + var sourceSpans = new List() + { + elisionSpan.Snapshot.CreateTrackingSpan(elisionSpan, SpanTrackingMode.EdgeExclusive), + "..." + }; - return projectionBuffer; - } + var projectionBuffer = ProjectionBufferFactoryService.CreateProjectionBuffer( + projectionEditResolver: null, + sourceSpans: sourceSpans, + options: ProjectionBufferOptions.None); - private static SnapshotSpan ComputeShortSpan(SnapshotSpan span) - { - var endIndex = span.Start + MaxPreviewText; - var line = span.Snapshot.GetLineFromPosition(endIndex); + return projectionBuffer; + } - return new SnapshotSpan(span.Snapshot, Span.FromBounds(span.Start, line.EndIncludingLineBreak)); - } + private static SnapshotSpan ComputeShortSpan(SnapshotSpan span) + { + var endIndex = span.Start + MaxPreviewText; + var line = span.Snapshot.GetLineFromPosition(endIndex); - internal static SnapshotSpan TrimLeadingWhitespace(SnapshotSpan span) - { - int start = span.Start; + return new SnapshotSpan(span.Snapshot, Span.FromBounds(span.Start, line.EndIncludingLineBreak)); + } - while (start < span.End && char.IsWhiteSpace(span.Snapshot[start])) - start++; + internal static SnapshotSpan TrimLeadingWhitespace(SnapshotSpan span) + { + int start = span.Start; - return new SnapshotSpan(span.Snapshot, Span.FromBounds(start, span.End)); - } + while (start < span.End && char.IsWhiteSpace(span.Snapshot[start])) + start++; - private ITextBuffer CreateElisionBufferWithoutIndentation( - SnapshotSpan shortHintSpan) - { - return ProjectionBufferFactoryService.CreateProjectionBufferWithoutIndentation( - EditorOptionsService.Factory.GlobalOptions, - contentType: null, - exposedSpans: shortHintSpan); - } + return new SnapshotSpan(span.Snapshot, Span.FromBounds(start, span.End)); + } - #endregion + private ITextBuffer CreateElisionBufferWithoutIndentation( + SnapshotSpan shortHintSpan) + { + return ProjectionBufferFactoryService.CreateProjectionBufferWithoutIndentation( + EditorOptionsService.Factory.GlobalOptions, + contentType: null, + exposedSpans: shortHintSpan); } -#pragma warning restore CS0618 // Type or member is obsolete + + #endregion } +#pragma warning restore CS0618 // Type or member is obsolete + diff --git a/src/EditorFeatures/Core/Structure/InvalidOutliningRegionException.cs b/src/EditorFeatures/Core/Structure/InvalidOutliningRegionException.cs index 5e71d7530cb32..fa2ea3e8262d8 100644 --- a/src/EditorFeatures/Core/Structure/InvalidOutliningRegionException.cs +++ b/src/EditorFeatures/Core/Structure/InvalidOutliningRegionException.cs @@ -6,17 +6,16 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Implementation.Structure +namespace Microsoft.CodeAnalysis.Editor.Implementation.Structure; + +internal class InvalidOutliningRegionException(BlockStructureService service, ITextSnapshot snapshot, Span snapshotSpan, Span regionSpan) : Exception(GetExceptionMessage(service, snapshotSpan, regionSpan)) { - internal class InvalidOutliningRegionException(BlockStructureService service, ITextSnapshot snapshot, Span snapshotSpan, Span regionSpan) : Exception(GetExceptionMessage(service, snapshotSpan, regionSpan)) - { #pragma warning disable IDE0052 // Remove unread private members - private readonly BlockStructureService _service = service; - private readonly ITextSnapshot _snapshot = snapshot; - private readonly Span _snapshotSpan = snapshotSpan; - private readonly Span _regionSpan = regionSpan; + private readonly BlockStructureService _service = service; + private readonly ITextSnapshot _snapshot = snapshot; + private readonly Span _snapshotSpan = snapshotSpan; + private readonly Span _regionSpan = regionSpan; - private static string GetExceptionMessage(BlockStructureService service, Span snapshotSpan, Span regionSpan) - => $"OutliningService({service.GetType()}) produced an invalid region. ITextSnapshot span is {snapshotSpan}. OutliningSpan is {regionSpan}."; - } + private static string GetExceptionMessage(BlockStructureService service, Span snapshotSpan, Span regionSpan) + => $"OutliningService({service.GetType()}) produced an invalid region. ITextSnapshot span is {snapshotSpan}. OutliningSpan is {regionSpan}."; } diff --git a/src/EditorFeatures/Core/Structure/OutliningCommandHandler.cs b/src/EditorFeatures/Core/Structure/OutliningCommandHandler.cs index 9b91480e73f29..2d7ca3987e5c7 100644 --- a/src/EditorFeatures/Core/Structure/OutliningCommandHandler.cs +++ b/src/EditorFeatures/Core/Structure/OutliningCommandHandler.cs @@ -10,35 +10,34 @@ using Microsoft.VisualStudio.Text.Outlining; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.Structure +namespace Microsoft.CodeAnalysis.Editor.Implementation.Structure; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.RoslynContentType)] +[Name("Outlining Command Handler")] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class OutliningCommandHandler(IOutliningManagerService outliningManagerService) : ICommandHandler { - [Export(typeof(ICommandHandler))] - [ContentType(ContentTypeNames.RoslynContentType)] - [Name("Outlining Command Handler")] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class OutliningCommandHandler(IOutliningManagerService outliningManagerService) : ICommandHandler - { - private readonly IOutliningManagerService _outliningManagerService = outliningManagerService; + private readonly IOutliningManagerService _outliningManagerService = outliningManagerService; - public string DisplayName => EditorFeaturesResources.Outlining; + public string DisplayName => EditorFeaturesResources.Outlining; - public bool ExecuteCommand(StartAutomaticOutliningCommandArgs args, CommandExecutionContext context) - { - // The editor actually handles this command, we just have to make sure it is enabled. - return false; - } + public bool ExecuteCommand(StartAutomaticOutliningCommandArgs args, CommandExecutionContext context) + { + // The editor actually handles this command, we just have to make sure it is enabled. + return false; + } - public CommandState GetCommandState(StartAutomaticOutliningCommandArgs args) + public CommandState GetCommandState(StartAutomaticOutliningCommandArgs args) + { + var outliningManager = _outliningManagerService.GetOutliningManager(args.TextView); + var enabled = false; + if (outliningManager != null) { - var outliningManager = _outliningManagerService.GetOutliningManager(args.TextView); - var enabled = false; - if (outliningManager != null) - { - enabled = outliningManager.Enabled; - } - - return new CommandState(isAvailable: !enabled); + enabled = outliningManager.Enabled; } + + return new CommandState(isAvailable: !enabled); } } diff --git a/src/EditorFeatures/Core/Structure/OutliningOptionsStorage.cs b/src/EditorFeatures/Core/Structure/OutliningOptionsStorage.cs index 815f4c8e7170a..5f487307d785d 100644 --- a/src/EditorFeatures/Core/Structure/OutliningOptionsStorage.cs +++ b/src/EditorFeatures/Core/Structure/OutliningOptionsStorage.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Structure +namespace Microsoft.CodeAnalysis.Structure; + +internal static class OutliningOptionsStorage { - internal static class OutliningOptionsStorage - { - public static readonly PerLanguageOption2 Outlining = new("dotnet_enter_outlining_mode_on_file_open", defaultValue: true); - } + public static readonly PerLanguageOption2 Outlining = new("dotnet_enter_outlining_mode_on_file_open", defaultValue: true); } diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs index 227025a793b44..79e233429623f 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs @@ -20,377 +20,376 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +internal partial class AbstractAsynchronousTaggerProvider { - internal partial class AbstractAsynchronousTaggerProvider + /// + /// The is the core part of our asynchronous + /// tagging infrastructure. It is the coordinator between s, + /// s, and s. + /// + /// The is the type that actually owns the + /// list of cached tags. When an says tags need to be recomputed, + /// the tag source starts the computation and calls to build + /// the new list of tags. When that's done, the tags are stored in . The + /// tagger, when asked for tags from the editor, then returns the tags that are stored in + /// + /// + /// There is a one-to-many relationship between s + /// and s. Special cases, like reference highlighting (which processes multiple + /// subject buffers at once) have their own providers and tag source derivations. + /// + private sealed partial class TagSource { /// - /// The is the core part of our asynchronous - /// tagging infrastructure. It is the coordinator between s, - /// s, and s. - /// - /// The is the type that actually owns the - /// list of cached tags. When an says tags need to be recomputed, - /// the tag source starts the computation and calls to build - /// the new list of tags. When that's done, the tags are stored in . The - /// tagger, when asked for tags from the editor, then returns the tags that are stored in - /// + /// If we get more than this many differences, then we just issue it as a single change + /// notification. The number has been completely made up without any data to support it. /// - /// There is a one-to-many relationship between s - /// and s. Special cases, like reference highlighting (which processes multiple - /// subject buffers at once) have their own providers and tag source derivations. + /// Internal for testing purposes. /// - private sealed partial class TagSource - { - /// - /// If we get more than this many differences, then we just issue it as a single change - /// notification. The number has been completely made up without any data to support it. - /// - /// Internal for testing purposes. - /// - private const int CoalesceDifferenceCount = 10; - - #region Fields that can be accessed from either thread - - private readonly AbstractAsynchronousTaggerProvider _dataSource; - - /// - /// async operation notifier - /// - private readonly IAsynchronousOperationListener _asyncListener; - - /// - /// Information about what workspace the buffer we're tagging is associated with. - /// - private readonly WorkspaceRegistration _workspaceRegistration; - - /// - /// Work queue that collects high priority requests to call TagsChanged with. - /// - private readonly AsyncBatchingWorkQueue _highPriTagsChangedQueue; - - /// - /// Work queue that collects normal priority requests to call TagsChanged with. - /// - private readonly AsyncBatchingWorkQueue _normalPriTagsChangedQueue; - - /// - /// Boolean specifies if this is the initial set of tags being computed or not. This queue is used to batch - /// up event change notifications and only dispatch one recomputation every - /// to actually produce the latest set of tags. - /// - private readonly AsyncBatchingWorkQueue _eventChangeQueue; - - #endregion - - #region Fields that can only be accessed from the foreground thread - - /// - /// Cancellation token governing all our async work. Canceled/disposed when we are 'd. - /// - private readonly CancellationTokenSource _disposalTokenSource = new(); - - private readonly ITextView? _textView; - private readonly ITextBuffer _subjectBuffer; - - /// - /// Used to keep track of if this is visible or not (e.g. is in some that has some part visible or not. This is used so we can tagging when not visible to avoid wasting machine resources. Note: we do not - /// examine for this as that is only available for "view taggers" (taggers which - /// only tag portions of the view) whereas we want this for all taggers (including just buffer taggers which - /// tag the entire document). - /// - private readonly ITextBufferVisibilityTracker? _visibilityTracker; - - /// - /// Callback to us when the visibility of our changes. - /// - private readonly Action _onVisibilityChanged; - - /// - /// Our tagger event source that lets us know when we should call into the tag producer for - /// new tags. - /// - private readonly ITaggerEventSource _eventSource; - - #region Mutable state. Can only be accessed from the foreground thread - - /// - /// accumulated text changes since last tag calculation - /// - private TextChangeRange? _accumulatedTextChanges_doNotAccessDirectly; - private ImmutableDictionary> _cachedTagTrees_doNotAccessDirectly = ImmutableDictionary.Create>(); - private object? _state_doNotAccessDirecty; - - /// - /// Keep track of if we are processing the first request. If our provider returns - /// for , - /// then we'll want to synchronously block then and only then for tags. - /// - private bool _firstTagsRequest = true; - - /// - /// Whether or not tag generation is paused. We pause producing tags when documents become non-visible. - /// See . - /// - private bool _paused = false; - - #endregion - - #endregion - - public TagSource( - ITextView? textView, - ITextBuffer subjectBuffer, - ITextBufferVisibilityTracker? visibilityTracker, - AbstractAsynchronousTaggerProvider dataSource, - IAsynchronousOperationListener asyncListener) - { - dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - if (dataSource.SpanTrackingMode == SpanTrackingMode.Custom) - throw new ArgumentException("SpanTrackingMode.Custom not allowed.", "spanTrackingMode"); - - _textView = textView; - _subjectBuffer = subjectBuffer; - _visibilityTracker = visibilityTracker; - _dataSource = dataSource; - _asyncListener = asyncListener; - - _workspaceRegistration = Workspace.GetWorkspaceRegistration(subjectBuffer.AsTextContainer()); - - // Collapse all booleans added to just a max of two ('true' or 'false') representing if we're being - // asked for initial tags or not - // - // PERF: Use AsyncBatchingWorkQueue instead of AsyncBatchingWorkQueue because - // the latter has an async state machine that rethrows a very common cancellation exception. - _eventChangeQueue = new AsyncBatchingWorkQueue( - dataSource.EventChangeDelay.ComputeTimeDelay(), - ProcessEventChangeAsync, - EqualityComparer.Default, - asyncListener, - _disposalTokenSource.Token); + private const int CoalesceDifferenceCount = 10; - _highPriTagsChangedQueue = new AsyncBatchingWorkQueue( - TaggerDelay.NearImmediate.ComputeTimeDelay(), - ProcessTagsChangedAsync, - equalityComparer: null, - asyncListener, - _disposalTokenSource.Token); + #region Fields that can be accessed from either thread - if (_dataSource.AddedTagNotificationDelay == TaggerDelay.NearImmediate) - { - // if the tagger wants "added tags" to be reported "NearImmediate"ly, then just reuse - // the "high pri" queue as that already reports things at that cadence. - _normalPriTagsChangedQueue = _highPriTagsChangedQueue; - } - else - { - _normalPriTagsChangedQueue = new AsyncBatchingWorkQueue( - _dataSource.AddedTagNotificationDelay.ComputeTimeDelay(), - ProcessTagsChangedAsync, - equalityComparer: null, - asyncListener, - _disposalTokenSource.Token); - } + private readonly AbstractAsynchronousTaggerProvider _dataSource; - DebugRecordInitialStackTrace(); + /// + /// async operation notifier + /// + private readonly IAsynchronousOperationListener _asyncListener; - // Create the tagger-specific events that will cause the tagger to refresh. - _eventSource = CreateEventSource(); + /// + /// Information about what workspace the buffer we're tagging is associated with. + /// + private readonly WorkspaceRegistration _workspaceRegistration; - // any time visibility changes, resume tagging on all taggers. Any non-visible taggers will pause - // themselves immediately afterwards. - _onVisibilityChanged = () => ResumeIfVisible(); + /// + /// Work queue that collects high priority requests to call TagsChanged with. + /// + private readonly AsyncBatchingWorkQueue _highPriTagsChangedQueue; - // Now hook up this tagger to all interesting events. - Connect(); + /// + /// Work queue that collects normal priority requests to call TagsChanged with. + /// + private readonly AsyncBatchingWorkQueue _normalPriTagsChangedQueue; - // Now that we're all hooked up to the events we care about, start computing the initial set of tags at - // high priority. We want to get the UI to a complete state as soon as possible. - EnqueueWork(highPriority: true); + /// + /// Boolean specifies if this is the initial set of tags being computed or not. This queue is used to batch + /// up event change notifications and only dispatch one recomputation every + /// to actually produce the latest set of tags. + /// + private readonly AsyncBatchingWorkQueue _eventChangeQueue; - return; + #endregion - // Represented as a local function just so we can keep this in sync with Dispose.Disconnect below. - void Connect() - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + #region Fields that can only be accessed from the foreground thread - // Register to hear about visibility changes so we can pause/resume this tagger. - _visibilityTracker?.RegisterForVisibilityChanges(subjectBuffer, _onVisibilityChanged); + /// + /// Cancellation token governing all our async work. Canceled/disposed when we are 'd. + /// + private readonly CancellationTokenSource _disposalTokenSource = new(); - _eventSource.Changed += OnEventSourceChanged; + private readonly ITextView? _textView; + private readonly ITextBuffer _subjectBuffer; - if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.TrackTextChanges)) - _subjectBuffer.Changed += OnSubjectBufferChanged; + /// + /// Used to keep track of if this is visible or not (e.g. is in some that has some part visible or not. This is used so we can tagging when not visible to avoid wasting machine resources. Note: we do not + /// examine for this as that is only available for "view taggers" (taggers which + /// only tag portions of the view) whereas we want this for all taggers (including just buffer taggers which + /// tag the entire document). + /// + private readonly ITextBufferVisibilityTracker? _visibilityTracker; - if (_dataSource.CaretChangeBehavior.HasFlag(TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag)) - { - if (_textView == null) - { - throw new ArgumentException( - nameof(_dataSource.CaretChangeBehavior) + " can only be specified for an " + nameof(IViewTaggerProvider)); - } + /// + /// Callback to us when the visibility of our changes. + /// + private readonly Action _onVisibilityChanged; - _textView.Caret.PositionChanged += OnCaretPositionChanged; - } + /// + /// Our tagger event source that lets us know when we should call into the tag producer for + /// new tags. + /// + private readonly ITaggerEventSource _eventSource; - // Tell the interaction object to start issuing events. - _eventSource.Connect(); - } - } + #region Mutable state. Can only be accessed from the foreground thread - private void Dispose() - { - _disposalTokenSource.Cancel(); - _disposalTokenSource.Dispose(); + /// + /// accumulated text changes since last tag calculation + /// + private TextChangeRange? _accumulatedTextChanges_doNotAccessDirectly; + private ImmutableDictionary> _cachedTagTrees_doNotAccessDirectly = ImmutableDictionary.Create>(); + private object? _state_doNotAccessDirecty; - _dataSource.RemoveTagSource(_textView, _subjectBuffer); - GC.SuppressFinalize(this); + /// + /// Keep track of if we are processing the first request. If our provider returns + /// for , + /// then we'll want to synchronously block then and only then for tags. + /// + private bool _firstTagsRequest = true; - Disconnect(); + /// + /// Whether or not tag generation is paused. We pause producing tags when documents become non-visible. + /// See . + /// + private bool _paused = false; - return; + #endregion - // Keep in sync with TagSource.Connect above (just performing the disconnect operations in the reverse order - void Disconnect() - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + #endregion - // Tell the interaction object to stop issuing events. - _eventSource.Disconnect(); + public TagSource( + ITextView? textView, + ITextBuffer subjectBuffer, + ITextBufferVisibilityTracker? visibilityTracker, + AbstractAsynchronousTaggerProvider dataSource, + IAsynchronousOperationListener asyncListener) + { + dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + if (dataSource.SpanTrackingMode == SpanTrackingMode.Custom) + throw new ArgumentException("SpanTrackingMode.Custom not allowed.", "spanTrackingMode"); + + _textView = textView; + _subjectBuffer = subjectBuffer; + _visibilityTracker = visibilityTracker; + _dataSource = dataSource; + _asyncListener = asyncListener; + + _workspaceRegistration = Workspace.GetWorkspaceRegistration(subjectBuffer.AsTextContainer()); + + // Collapse all booleans added to just a max of two ('true' or 'false') representing if we're being + // asked for initial tags or not + // + // PERF: Use AsyncBatchingWorkQueue instead of AsyncBatchingWorkQueue because + // the latter has an async state machine that rethrows a very common cancellation exception. + _eventChangeQueue = new AsyncBatchingWorkQueue( + dataSource.EventChangeDelay.ComputeTimeDelay(), + ProcessEventChangeAsync, + EqualityComparer.Default, + asyncListener, + _disposalTokenSource.Token); + + _highPriTagsChangedQueue = new AsyncBatchingWorkQueue( + TaggerDelay.NearImmediate.ComputeTimeDelay(), + ProcessTagsChangedAsync, + equalityComparer: null, + asyncListener, + _disposalTokenSource.Token); + + if (_dataSource.AddedTagNotificationDelay == TaggerDelay.NearImmediate) + { + // if the tagger wants "added tags" to be reported "NearImmediate"ly, then just reuse + // the "high pri" queue as that already reports things at that cadence. + _normalPriTagsChangedQueue = _highPriTagsChangedQueue; + } + else + { + _normalPriTagsChangedQueue = new AsyncBatchingWorkQueue( + _dataSource.AddedTagNotificationDelay.ComputeTimeDelay(), + ProcessTagsChangedAsync, + equalityComparer: null, + asyncListener, + _disposalTokenSource.Token); + } - if (_dataSource.CaretChangeBehavior.HasFlag(TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag)) - { - Contract.ThrowIfNull(_textView); - _textView.Caret.PositionChanged -= OnCaretPositionChanged; - } + DebugRecordInitialStackTrace(); - if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.TrackTextChanges)) - _subjectBuffer.Changed -= OnSubjectBufferChanged; + // Create the tagger-specific events that will cause the tagger to refresh. + _eventSource = CreateEventSource(); - _eventSource.Changed -= OnEventSourceChanged; + // any time visibility changes, resume tagging on all taggers. Any non-visible taggers will pause + // themselves immediately afterwards. + _onVisibilityChanged = () => ResumeIfVisible(); - _visibilityTracker?.UnregisterForVisibilityChanges(_subjectBuffer, _onVisibilityChanged); - } - } + // Now hook up this tagger to all interesting events. + Connect(); + + // Now that we're all hooked up to the events we care about, start computing the initial set of tags at + // high priority. We want to get the UI to a complete state as soon as possible. + EnqueueWork(highPriority: true); - private bool IsVisible() - => _visibilityTracker == null || _visibilityTracker.IsVisible(_subjectBuffer); + return; - private void PauseIfNotVisible() + // Represented as a local function just so we can keep this in sync with Dispose.Disconnect below. + void Connect() { _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - if (!IsVisible()) + // Register to hear about visibility changes so we can pause/resume this tagger. + _visibilityTracker?.RegisterForVisibilityChanges(subjectBuffer, _onVisibilityChanged); + + _eventSource.Changed += OnEventSourceChanged; + + if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.TrackTextChanges)) + _subjectBuffer.Changed += OnSubjectBufferChanged; + + if (_dataSource.CaretChangeBehavior.HasFlag(TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag)) { - _paused = true; - _eventSource.Pause(); + if (_textView == null) + { + throw new ArgumentException( + nameof(_dataSource.CaretChangeBehavior) + " can only be specified for an " + nameof(IViewTaggerProvider)); + } + + _textView.Caret.PositionChanged += OnCaretPositionChanged; } + + // Tell the interaction object to start issuing events. + _eventSource.Connect(); } + } + + private void Dispose() + { + _disposalTokenSource.Cancel(); + _disposalTokenSource.Dispose(); + + _dataSource.RemoveTagSource(_textView, _subjectBuffer); + GC.SuppressFinalize(this); - private void ResumeIfVisible() + Disconnect(); + + return; + + // Keep in sync with TagSource.Connect above (just performing the disconnect operations in the reverse order + void Disconnect() { _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - // if we're not actually paused, no need to do anything. - if (!_paused) - return; + // Tell the interaction object to stop issuing events. + _eventSource.Disconnect(); - // If we're not visible, no need to resume. - if (!IsVisible()) - return; + if (_dataSource.CaretChangeBehavior.HasFlag(TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag)) + { + Contract.ThrowIfNull(_textView); + _textView.Caret.PositionChanged -= OnCaretPositionChanged; + } - // Set us back to running, and kick off work to compute tags now that we're visible again. - _paused = false; - _eventSource.Resume(); + if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.TrackTextChanges)) + _subjectBuffer.Changed -= OnSubjectBufferChanged; - // We just transitioned to being visible, compute our tags at high priority so the view is updated as - // quickly as possible. - EnqueueWork(highPriority: true); + _eventSource.Changed -= OnEventSourceChanged; + + _visibilityTracker?.UnregisterForVisibilityChanges(_subjectBuffer, _onVisibilityChanged); } + } + + private bool IsVisible() + => _visibilityTracker == null || _visibilityTracker.IsVisible(_subjectBuffer); + + private void PauseIfNotVisible() + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - private ITaggerEventSource CreateEventSource() + if (!IsVisible()) { - Contract.ThrowIfTrue(_dataSource.Options.Any(o => o is not Option2 and not PerLanguageOption2), "All options must be Option2 or PerLanguageOption2"); + _paused = true; + _eventSource.Pause(); + } + } - var eventSource = _dataSource.CreateEventSource(_textView, _subjectBuffer); + private void ResumeIfVisible() + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - // If there are any options specified for this tagger, then also hook up event - // notifications for when those options change. - var optionChangedEventSources = _dataSource.Options.Concat(_dataSource.FeatureOptions) - .Select(globalOption => TaggerEventSources.OnGlobalOptionChanged(_dataSource.GlobalOptions, globalOption)) - .ToList(); + // if we're not actually paused, no need to do anything. + if (!_paused) + return; - if (optionChangedEventSources.Count == 0) - { - // No options specified for this tagger. So just keep the event source as is. - return eventSource; - } + // If we're not visible, no need to resume. + if (!IsVisible()) + return; - optionChangedEventSources.Add(eventSource); - return TaggerEventSources.Compose(optionChangedEventSources); - } + // Set us back to running, and kick off work to compute tags now that we're visible again. + _paused = false; + _eventSource.Resume(); + + // We just transitioned to being visible, compute our tags at high priority so the view is updated as + // quickly as possible. + EnqueueWork(highPriority: true); + } + + private ITaggerEventSource CreateEventSource() + { + Contract.ThrowIfTrue(_dataSource.Options.Any(o => o is not Option2 and not PerLanguageOption2), "All options must be Option2 or PerLanguageOption2"); + + var eventSource = _dataSource.CreateEventSource(_textView, _subjectBuffer); - private TextChangeRange? AccumulatedTextChanges + // If there are any options specified for this tagger, then also hook up event + // notifications for when those options change. + var optionChangedEventSources = _dataSource.Options.Concat(_dataSource.FeatureOptions) + .Select(globalOption => TaggerEventSources.OnGlobalOptionChanged(_dataSource.GlobalOptions, globalOption)) + .ToList(); + + if (optionChangedEventSources.Count == 0) { - get - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - return _accumulatedTextChanges_doNotAccessDirectly; - } + // No options specified for this tagger. So just keep the event source as is. + return eventSource; + } - set - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - _accumulatedTextChanges_doNotAccessDirectly = value; - } + optionChangedEventSources.Add(eventSource); + return TaggerEventSources.Compose(optionChangedEventSources); + } + + private TextChangeRange? AccumulatedTextChanges + { + get + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + return _accumulatedTextChanges_doNotAccessDirectly; } - private ImmutableDictionary> CachedTagTrees + set { - get - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - return _cachedTagTrees_doNotAccessDirectly; - } + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + _accumulatedTextChanges_doNotAccessDirectly = value; + } + } - set - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - _cachedTagTrees_doNotAccessDirectly = value; - } + private ImmutableDictionary> CachedTagTrees + { + get + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + return _cachedTagTrees_doNotAccessDirectly; } - private object? State + set { - get - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - return _state_doNotAccessDirecty; - } + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + _cachedTagTrees_doNotAccessDirectly = value; + } + } - set - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - _state_doNotAccessDirecty = value; - } + private object? State + { + get + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + return _state_doNotAccessDirecty; } - private void RaiseTagsChanged(ITextBuffer buffer, DiffResult difference) + set { _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - if (difference.Count == 0) - { - // nothing changed. - return; - } + _state_doNotAccessDirecty = value; + } + } - OnTagsChangedForBuffer(SpecializedCollections.SingletonCollection( - new KeyValuePair(buffer, difference)), - highPriority: false); + private void RaiseTagsChanged(ITextBuffer buffer, DiffResult difference) + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + if (difference.Count == 0) + { + // nothing changed. + return; } + + OnTagsChangedForBuffer(SpecializedCollections.SingletonCollection( + new KeyValuePair(buffer, difference)), + highPriority: false); } } } diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_IEqualityComparer.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_IEqualityComparer.cs index 491e9a64163e6..a278bd5d3dfc0 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_IEqualityComparer.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_IEqualityComparer.cs @@ -6,23 +6,22 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +internal abstract partial class AbstractAsynchronousTaggerProvider { - internal abstract partial class AbstractAsynchronousTaggerProvider + private partial class TagSource : IEqualityComparer> { - private partial class TagSource : IEqualityComparer> - { - public bool Equals(ITagSpan? x, ITagSpan? y) - => x != null && y != null && x.Span == y.Span && _dataSource.TagEquals(x.Tag, y.Tag); + public bool Equals(ITagSpan? x, ITagSpan? y) + => x != null && y != null && x.Span == y.Span && _dataSource.TagEquals(x.Tag, y.Tag); - /// - /// For the purposes of hashing, just hash spans. This will prevent most collisions. And the rare - /// collision of two tag spans with the same span will be handled by checking if their tags are the same - /// through . This prevents us from having to - /// define a suitable hashing strategy for all our tags. - /// - public int GetHashCode(ITagSpan obj) - => obj.Span.Span.GetHashCode(); - } + /// + /// For the purposes of hashing, just hash spans. This will prevent most collisions. And the rare + /// collision of two tag spans with the same span will be handled by checking if their tags are the same + /// through . This prevents us from having to + /// define a suitable hashing strategy for all our tags. + /// + public int GetHashCode(ITagSpan obj) + => obj.Span.Span.GetHashCode(); } } diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index cc69f52cfd0ed..0c74f79bdae22 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -23,570 +23,569 @@ using Microsoft.VisualStudio.Threading; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +internal partial class AbstractAsynchronousTaggerProvider { - internal partial class AbstractAsynchronousTaggerProvider + private partial class TagSource { - private partial class TagSource + private void OnCaretPositionChanged(object? _, CaretPositionChangedEventArgs e) { - private void OnCaretPositionChanged(object? _, CaretPositionChangedEventArgs e) - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - Debug.Assert(_dataSource.CaretChangeBehavior.HasFlag(TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag)); + Debug.Assert(_dataSource.CaretChangeBehavior.HasFlag(TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag)); - var caret = _dataSource.GetCaretPoint(_textView, _subjectBuffer); - if (caret.HasValue) + var caret = _dataSource.GetCaretPoint(_textView, _subjectBuffer); + if (caret.HasValue) + { + // If it changed position and we're still in a tag, there's nothing more to do + var currentTags = TryGetTagIntervalTreeForBuffer(caret.Value.Snapshot.TextBuffer); + if (currentTags != null && currentTags.GetIntersectingSpans(new SnapshotSpan(caret.Value, 0)).Count > 0) { - // If it changed position and we're still in a tag, there's nothing more to do - var currentTags = TryGetTagIntervalTreeForBuffer(caret.Value.Snapshot.TextBuffer); - if (currentTags != null && currentTags.GetIntersectingSpans(new SnapshotSpan(caret.Value, 0)).Count > 0) - { - // Caret is inside a tag. No need to do anything. - return; - } + // Caret is inside a tag. No need to do anything. + return; } - - RemoveAllTags(); } - private void RemoveAllTags() - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + RemoveAllTags(); + } - var oldTagTrees = this.CachedTagTrees; - this.CachedTagTrees = ImmutableDictionary>.Empty; + private void RemoveAllTags() + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - var snapshot = _subjectBuffer.CurrentSnapshot; - var oldTagTree = GetTagTree(snapshot, oldTagTrees); + var oldTagTrees = this.CachedTagTrees; + this.CachedTagTrees = ImmutableDictionary>.Empty; - // everything from old tree is removed. - RaiseTagsChanged(snapshot.TextBuffer, new DiffResult(added: null, removed: new(oldTagTree.GetSpans(snapshot).Select(s => s.Span)))); - } + var snapshot = _subjectBuffer.CurrentSnapshot; + var oldTagTree = GetTagTree(snapshot, oldTagTrees); - private void OnSubjectBufferChanged(object? _, TextContentChangedEventArgs e) - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - UpdateTagsForTextChange(e); - AccumulateTextChanges(e); - } + // everything from old tree is removed. + RaiseTagsChanged(snapshot.TextBuffer, new DiffResult(added: null, removed: new(oldTagTree.GetSpans(snapshot).Select(s => s.Span)))); + } + + private void OnSubjectBufferChanged(object? _, TextContentChangedEventArgs e) + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + UpdateTagsForTextChange(e); + AccumulateTextChanges(e); + } - private void AccumulateTextChanges(TextContentChangedEventArgs contentChanged) + private void AccumulateTextChanges(TextContentChangedEventArgs contentChanged) + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + var contentChanges = contentChanged.Changes; + var count = contentChanges.Count; + + switch (count) { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - var contentChanges = contentChanged.Changes; - var count = contentChanges.Count; + case 0: + return; - switch (count) - { - case 0: - return; - - case 1: - // PERF: Optimize for the simple case of typing on a line. - { - var c = contentChanges[0]; - var textChangeRange = new TextChangeRange(new TextSpan(c.OldSpan.Start, c.OldSpan.Length), c.NewLength); - this.AccumulatedTextChanges = this.AccumulatedTextChanges == null - ? textChangeRange - : this.AccumulatedTextChanges.Accumulate(SpecializedCollections.SingletonEnumerable(textChangeRange)); - } + case 1: + // PERF: Optimize for the simple case of typing on a line. + { + var c = contentChanges[0]; + var textChangeRange = new TextChangeRange(new TextSpan(c.OldSpan.Start, c.OldSpan.Length), c.NewLength); + this.AccumulatedTextChanges = this.AccumulatedTextChanges == null + ? textChangeRange + : this.AccumulatedTextChanges.Accumulate(SpecializedCollections.SingletonEnumerable(textChangeRange)); + } + + break; + + default: + { + using var _ = ArrayBuilder.GetInstance(count, out var textChangeRanges); + foreach (var c in contentChanges) + textChangeRanges.Add(new TextChangeRange(new TextSpan(c.OldSpan.Start, c.OldSpan.Length), c.NewLength)); + this.AccumulatedTextChanges = this.AccumulatedTextChanges.Accumulate(textChangeRanges); break; + } + } + } - default: - { - using var _ = ArrayBuilder.GetInstance(count, out var textChangeRanges); - foreach (var c in contentChanges) - textChangeRanges.Add(new TextChangeRange(new TextSpan(c.OldSpan.Start, c.OldSpan.Length), c.NewLength)); + private void UpdateTagsForTextChange(TextContentChangedEventArgs e) + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - this.AccumulatedTextChanges = this.AccumulatedTextChanges.Accumulate(textChangeRanges); - break; - } - } + if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveAllTags)) + { + this.RemoveAllTags(); + return; } - private void UpdateTagsForTextChange(TextContentChangedEventArgs e) + // Don't bother going forward if we're not going adjust any tags based on edits. + if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveTagsThatIntersectEdits)) { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + RemoveTagsThatIntersectEdit(e); + return; + } + } - if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveAllTags)) - { - this.RemoveAllTags(); - return; - } + private void RemoveTagsThatIntersectEdit(TextContentChangedEventArgs e) + { + if (e.Changes.Count == 0) + return; - // Don't bother going forward if we're not going adjust any tags based on edits. - if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveTagsThatIntersectEdits)) - { - RemoveTagsThatIntersectEdit(e); - return; - } - } + var buffer = e.After.TextBuffer; + if (!this.CachedTagTrees.TryGetValue(buffer, out var treeForBuffer)) + return; - private void RemoveTagsThatIntersectEdit(TextContentChangedEventArgs e) - { - if (e.Changes.Count == 0) - return; + var snapshot = e.After; - var buffer = e.After.TextBuffer; - if (!this.CachedTagTrees.TryGetValue(buffer, out var treeForBuffer)) - return; + var tagsToRemove = e.Changes.SelectMany(c => treeForBuffer.GetIntersectingSpans(new SnapshotSpan(snapshot, c.NewSpan))); + if (!tagsToRemove.Any()) + return; - var snapshot = e.After; + var allTags = treeForBuffer.GetSpans(e.After).ToList(); + var newTagTree = new TagSpanIntervalTree( + buffer, + treeForBuffer.SpanTrackingMode, + allTags.Except(tagsToRemove, comparer: this)); - var tagsToRemove = e.Changes.SelectMany(c => treeForBuffer.GetIntersectingSpans(new SnapshotSpan(snapshot, c.NewSpan))); - if (!tagsToRemove.Any()) - return; + this.CachedTagTrees = this.CachedTagTrees.SetItem(snapshot.TextBuffer, newTagTree); - var allTags = treeForBuffer.GetSpans(e.After).ToList(); - var newTagTree = new TagSpanIntervalTree( - buffer, - treeForBuffer.SpanTrackingMode, - allTags.Except(tagsToRemove, comparer: this)); + // Not sure why we are diffing when we already have tagsToRemove. is it due to _tagSpanComparer might return + // different result than GetIntersectingSpans? + // + // treeForBuffer basically points to oldTagTrees. case where oldTagTrees not exist is already taken cared by + // CachedTagTrees.TryGetValue. + var difference = ComputeDifference(snapshot, newTagTree, treeForBuffer); - this.CachedTagTrees = this.CachedTagTrees.SetItem(snapshot.TextBuffer, newTagTree); + RaiseTagsChanged(snapshot.TextBuffer, difference); + } - // Not sure why we are diffing when we already have tagsToRemove. is it due to _tagSpanComparer might return - // different result than GetIntersectingSpans? - // - // treeForBuffer basically points to oldTagTrees. case where oldTagTrees not exist is already taken cared by - // CachedTagTrees.TryGetValue. - var difference = ComputeDifference(snapshot, newTagTree, treeForBuffer); + private TagSpanIntervalTree GetTagTree(ITextSnapshot snapshot, ImmutableDictionary> tagTrees) + { + return tagTrees.TryGetValue(snapshot.TextBuffer, out var tagTree) + ? tagTree + : new TagSpanIntervalTree(snapshot.TextBuffer, _dataSource.SpanTrackingMode); + } - RaiseTagsChanged(snapshot.TextBuffer, difference); - } + private void OnEventSourceChanged(object? _1, TaggerEventArgs _2) + => EnqueueWork(highPriority: false); - private TagSpanIntervalTree GetTagTree(ITextSnapshot snapshot, ImmutableDictionary> tagTrees) - { - return tagTrees.TryGetValue(snapshot.TextBuffer, out var tagTree) - ? tagTree - : new TagSpanIntervalTree(snapshot.TextBuffer, _dataSource.SpanTrackingMode); - } + private void EnqueueWork(bool highPriority) + => _eventChangeQueue.AddWork(highPriority, _dataSource.CancelOnNewWork); - private void OnEventSourceChanged(object? _1, TaggerEventArgs _2) - => EnqueueWork(highPriority: false); + private ValueTask ProcessEventChangeAsync(ImmutableSegmentedList changes, CancellationToken cancellationToken) + { + // If any of the requests was high priority, then compute at that speed. + var highPriority = changes.Contains(true); + return new ValueTask(RecomputeTagsAsync(highPriority, cancellationToken)); + } - private void EnqueueWork(bool highPriority) - => _eventChangeQueue.AddWork(highPriority, _dataSource.CancelOnNewWork); + /// + /// Passed a boolean to say if we're computing the + /// initial set of tags or not. If we're computing the initial set of tags, we lower + /// all our delays so that we can get results to the screen as quickly as possible. + /// This gives a good experience when a document is opened as the document appears complete almost + /// immediately. Once open though, our normal delays come into play so as to not cause a flashy experience. + /// + /// + /// In the event of a cancellation request, this method may either return at the next availability + /// or throw a cancellation exception. + /// + /// + /// If this tagging request should be processed as quickly as possible with no extra delays added for it. + /// + private async Task RecomputeTagsAsync(bool highPriority, CancellationToken cancellationToken) + { + await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); + if (cancellationToken.IsCancellationRequested) + return default; - private ValueTask ProcessEventChangeAsync(ImmutableSegmentedList changes, CancellationToken cancellationToken) + // if we're tagging documents that are not visible, then introduce a long delay so that we avoid + // consuming machine resources on work the user isn't likely to see. ConfigureAwait(true) so that if + // we're on the UI thread that we stay on it. + // + // Don't do this for explicit high priority requests as the caller wants the UI updated as quickly as + // possible. + if (!highPriority) { - // If any of the requests was high priority, then compute at that speed. - var highPriority = changes.Contains(true); - return new ValueTask(RecomputeTagsAsync(highPriority, cancellationToken)); + // Use NoThrow as this is a high source of cancellation exceptions. This avoids the exception and instead + // bails gracefully by checking below. + await _visibilityTracker.DelayWhileNonVisibleAsync( + _dataSource.ThreadingContext, _dataSource.AsyncListener, _subjectBuffer, DelayTimeSpan.NonFocus, cancellationToken).NoThrowAwaitable(captureContext: true); + + if (cancellationToken.IsCancellationRequested) + return default; } - /// - /// Passed a boolean to say if we're computing the - /// initial set of tags or not. If we're computing the initial set of tags, we lower - /// all our delays so that we can get results to the screen as quickly as possible. - /// This gives a good experience when a document is opened as the document appears complete almost - /// immediately. Once open though, our normal delays come into play so as to not cause a flashy experience. - /// - /// - /// In the event of a cancellation request, this method may either return at the next availability - /// or throw a cancellation exception. - /// - /// - /// If this tagging request should be processed as quickly as possible with no extra delays added for it. - /// - private async Task RecomputeTagsAsync(bool highPriority, CancellationToken cancellationToken) + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + if (cancellationToken.IsCancellationRequested) + return default; + + using (Logger.LogBlock(FunctionId.Tagger_TagSource_RecomputeTags, cancellationToken)) { - await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); + // Make a copy of all the data we need while we're on the foreground. Then switch to a threadpool + // thread to do the computation. Finally, once new tags have been computed, then we update our state + // again on the foreground. + var spansToTag = GetSpansAndDocumentsToTag(); + var caretPosition = _dataSource.GetCaretPoint(_textView, _subjectBuffer); + var oldTagTrees = this.CachedTagTrees; + var oldState = this.State; + + var textChangeRange = this.AccumulatedTextChanges; + var subjectBufferVersion = _subjectBuffer.CurrentSnapshot.Version.VersionNumber; + + await TaskScheduler.Default; + if (cancellationToken.IsCancellationRequested) return default; - // if we're tagging documents that are not visible, then introduce a long delay so that we avoid - // consuming machine resources on work the user isn't likely to see. ConfigureAwait(true) so that if - // we're on the UI thread that we stay on it. - // - // Don't do this for explicit high priority requests as the caller wants the UI updated as quickly as - // possible. - if (!highPriority) - { - // Use NoThrow as this is a high source of cancellation exceptions. This avoids the exception and instead - // bails gracefully by checking below. - await _visibilityTracker.DelayWhileNonVisibleAsync( - _dataSource.ThreadingContext, _dataSource.AsyncListener, _subjectBuffer, DelayTimeSpan.NonFocus, cancellationToken).NoThrowAwaitable(captureContext: true); - - if (cancellationToken.IsCancellationRequested) - return default; - } + // Create a context to store pass the information along and collect the results. + var context = new TaggerContext( + oldState, spansToTag, caretPosition, textChangeRange, oldTagTrees); + await ProduceTagsAsync(context, cancellationToken).ConfigureAwait(false); - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); if (cancellationToken.IsCancellationRequested) return default; - using (Logger.LogBlock(FunctionId.Tagger_TagSource_RecomputeTags, cancellationToken)) - { - // Make a copy of all the data we need while we're on the foreground. Then switch to a threadpool - // thread to do the computation. Finally, once new tags have been computed, then we update our state - // again on the foreground. - var spansToTag = GetSpansAndDocumentsToTag(); - var caretPosition = _dataSource.GetCaretPoint(_textView, _subjectBuffer); - var oldTagTrees = this.CachedTagTrees; - var oldState = this.State; - - var textChangeRange = this.AccumulatedTextChanges; - var subjectBufferVersion = _subjectBuffer.CurrentSnapshot.Version.VersionNumber; - - await TaskScheduler.Default; - - if (cancellationToken.IsCancellationRequested) - return default; - - // Create a context to store pass the information along and collect the results. - var context = new TaggerContext( - oldState, spansToTag, caretPosition, textChangeRange, oldTagTrees); - await ProduceTagsAsync(context, cancellationToken).ConfigureAwait(false); - - if (cancellationToken.IsCancellationRequested) - return default; - - // Process the result to determine what changed. - var newTagTrees = ComputeNewTagTrees(oldTagTrees, context); - var bufferToChanges = ProcessNewTagTrees(spansToTag, oldTagTrees, newTagTrees, cancellationToken); - - // Then switch back to the UI thread to update our state and kick off the work to notify the editor. - await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); - if (cancellationToken.IsCancellationRequested) - return default; - - // Once we assign our state, we're uncancellable. We must report the changed information - // to the editor. The only case where it's ok not to is if the tagger itself is disposed. - cancellationToken = CancellationToken.None; - - this.CachedTagTrees = newTagTrees; - this.State = context.State; - if (this._subjectBuffer.CurrentSnapshot.Version.VersionNumber == subjectBufferVersion) - { - // Only clear the accumulated text changes if the subject buffer didn't change during the - // tagging operation. Otherwise, it is impossible to know which changes occurred prior to the - // request to tag, and which ones occurred during the tagging itself. Since - // AccumulatedTextChanges is a conservative representation of the work that needs to be done, in - // the event this value is not cleared the only potential impact will be slightly more work - // being done during the next classification pass. - this.AccumulatedTextChanges = null; - } + // Process the result to determine what changed. + var newTagTrees = ComputeNewTagTrees(oldTagTrees, context); + var bufferToChanges = ProcessNewTagTrees(spansToTag, oldTagTrees, newTagTrees, cancellationToken); - OnTagsChangedForBuffer(bufferToChanges, highPriority); + // Then switch back to the UI thread to update our state and kick off the work to notify the editor. + await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); + if (cancellationToken.IsCancellationRequested) + return default; - // Once we've computed tags, pause ourselves if we're no longer visible. That way we don't consume any - // machine resources that the user won't even notice. - PauseIfNotVisible(); + // Once we assign our state, we're uncancellable. We must report the changed information + // to the editor. The only case where it's ok not to is if the tagger itself is disposed. + cancellationToken = CancellationToken.None; + + this.CachedTagTrees = newTagTrees; + this.State = context.State; + if (this._subjectBuffer.CurrentSnapshot.Version.VersionNumber == subjectBufferVersion) + { + // Only clear the accumulated text changes if the subject buffer didn't change during the + // tagging operation. Otherwise, it is impossible to know which changes occurred prior to the + // request to tag, and which ones occurred during the tagging itself. Since + // AccumulatedTextChanges is a conservative representation of the work that needs to be done, in + // the event this value is not cleared the only potential impact will be slightly more work + // being done during the next classification pass. + this.AccumulatedTextChanges = null; } - return default; + OnTagsChangedForBuffer(bufferToChanges, highPriority); + + // Once we've computed tags, pause ourselves if we're no longer visible. That way we don't consume any + // machine resources that the user won't even notice. + PauseIfNotVisible(); } - private ImmutableArray GetSpansAndDocumentsToTag() - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + return default; + } + + private ImmutableArray GetSpansAndDocumentsToTag() + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - // TODO: Update to tag spans from all related documents. + // TODO: Update to tag spans from all related documents. - using var _ = PooledDictionary.GetInstance(out var snapshotToDocumentMap); - var spansToTag = _dataSource.GetSpansToTag(_textView, _subjectBuffer); + using var _ = PooledDictionary.GetInstance(out var snapshotToDocumentMap); + var spansToTag = _dataSource.GetSpansToTag(_textView, _subjectBuffer); - var spansAndDocumentsToTag = spansToTag.SelectAsArray(span => + var spansAndDocumentsToTag = spansToTag.SelectAsArray(span => + { + if (!snapshotToDocumentMap.TryGetValue(span.Snapshot, out var document)) { - if (!snapshotToDocumentMap.TryGetValue(span.Snapshot, out var document)) - { - CheckSnapshot(span.Snapshot); + CheckSnapshot(span.Snapshot); - document = span.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - snapshotToDocumentMap[span.Snapshot] = document; - } + document = span.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + snapshotToDocumentMap[span.Snapshot] = document; + } - // document can be null if the buffer the given span is part of is not part of our workspace. - return new DocumentSnapshotSpan(document, span); - }); + // document can be null if the buffer the given span is part of is not part of our workspace. + return new DocumentSnapshotSpan(document, span); + }); - return spansAndDocumentsToTag; + return spansAndDocumentsToTag; + } + + [Conditional("DEBUG")] + private static void CheckSnapshot(ITextSnapshot snapshot) + { + var container = snapshot.TextBuffer.AsTextContainer(); + if (Workspace.TryGetWorkspace(container, out _)) + { + // if the buffer is part of our workspace, it must be the latest. + Debug.Assert(snapshot.Version.Next == null, "should be on latest snapshot"); } + } - [Conditional("DEBUG")] - private static void CheckSnapshot(ITextSnapshot snapshot) + private ImmutableDictionary> ComputeNewTagTrees( + ImmutableDictionary> oldTagTrees, + TaggerContext context) + { + // Ignore any tag spans reported for any buffers we weren't interested in. + + var spansToTag = context.SpansToTag; + var buffersToTag = spansToTag.Select(dss => dss.SnapshotSpan.Snapshot.TextBuffer).ToSet(); + var newTagsByBuffer = + context.TagSpans.Where(ts => buffersToTag.Contains(ts.Span.Snapshot.TextBuffer)) + .ToLookup(t => t.Span.Snapshot.TextBuffer); + var spansTagged = context._spansTagged; + + var spansToInvalidateByBuffer = spansTagged.ToLookup( + keySelector: span => span.Snapshot.TextBuffer, + elementSelector: span => span); + + // Walk through each relevant buffer and decide what the interval tree should be + // for that buffer. In general this will work by keeping around old tags that + // weren't in the range that was re-tagged, and merging them with the new tags + // produced for the range that was re-tagged. + var newTagTrees = ImmutableDictionary>.Empty; + foreach (var buffer in buffersToTag) { - var container = snapshot.TextBuffer.AsTextContainer(); - if (Workspace.TryGetWorkspace(container, out _)) - { - // if the buffer is part of our workspace, it must be the latest. - Debug.Assert(snapshot.Version.Next == null, "should be on latest snapshot"); - } + var newTagTree = ComputeNewTagTree(oldTagTrees, buffer, newTagsByBuffer[buffer], spansToInvalidateByBuffer[buffer]); + if (newTagTree != null) + newTagTrees = newTagTrees.Add(buffer, newTagTree); } - private ImmutableDictionary> ComputeNewTagTrees( - ImmutableDictionary> oldTagTrees, - TaggerContext context) + return newTagTrees; + } + + private TagSpanIntervalTree? ComputeNewTagTree( + ImmutableDictionary> oldTagTrees, + ITextBuffer textBuffer, + IEnumerable> newTags, + IEnumerable spansToInvalidate) + { + var noNewTags = newTags.IsEmpty(); + var noSpansToInvalidate = spansToInvalidate.IsEmpty(); + oldTagTrees.TryGetValue(textBuffer, out var oldTagTree); + + if (oldTagTree == null) { - // Ignore any tag spans reported for any buffers we weren't interested in. - - var spansToTag = context.SpansToTag; - var buffersToTag = spansToTag.Select(dss => dss.SnapshotSpan.Snapshot.TextBuffer).ToSet(); - var newTagsByBuffer = - context.TagSpans.Where(ts => buffersToTag.Contains(ts.Span.Snapshot.TextBuffer)) - .ToLookup(t => t.Span.Snapshot.TextBuffer); - var spansTagged = context._spansTagged; - - var spansToInvalidateByBuffer = spansTagged.ToLookup( - keySelector: span => span.Snapshot.TextBuffer, - elementSelector: span => span); - - // Walk through each relevant buffer and decide what the interval tree should be - // for that buffer. In general this will work by keeping around old tags that - // weren't in the range that was re-tagged, and merging them with the new tags - // produced for the range that was re-tagged. - var newTagTrees = ImmutableDictionary>.Empty; - foreach (var buffer in buffersToTag) + if (noNewTags) { - var newTagTree = ComputeNewTagTree(oldTagTrees, buffer, newTagsByBuffer[buffer], spansToInvalidateByBuffer[buffer]); - if (newTagTree != null) - newTagTrees = newTagTrees.Add(buffer, newTagTree); + // We have no new tags, and no old tags either. No need to store anything + // for this buffer. + return null; } - return newTagTrees; + // If we don't have any old tags then we just need to return the new tags. + return new TagSpanIntervalTree(textBuffer, _dataSource.SpanTrackingMode, newTags); } - private TagSpanIntervalTree? ComputeNewTagTree( - ImmutableDictionary> oldTagTrees, - ITextBuffer textBuffer, - IEnumerable> newTags, - IEnumerable spansToInvalidate) + // If we don't have any new tags, and there was nothing to invalidate, then we can + // keep whatever old tags we have without doing any additional work. + if (noNewTags && noSpansToInvalidate) { - var noNewTags = newTags.IsEmpty(); - var noSpansToInvalidate = spansToInvalidate.IsEmpty(); - oldTagTrees.TryGetValue(textBuffer, out var oldTagTree); - - if (oldTagTree == null) - { - if (noNewTags) - { - // We have no new tags, and no old tags either. No need to store anything - // for this buffer. - return null; - } - - // If we don't have any old tags then we just need to return the new tags. - return new TagSpanIntervalTree(textBuffer, _dataSource.SpanTrackingMode, newTags); - } + return oldTagTree; + } - // If we don't have any new tags, and there was nothing to invalidate, then we can - // keep whatever old tags we have without doing any additional work. - if (noNewTags && noSpansToInvalidate) - { - return oldTagTree; - } + // We either have some new tags, or we have some tags to invalidate. + // First, determine which of the old tags we want to keep around. + var snapshot = noNewTags ? spansToInvalidate.First().Snapshot : newTags.First().Span.Snapshot; + var oldTagsToKeep = noSpansToInvalidate + ? oldTagTree.GetSpans(snapshot) + : GetNonIntersectingTagSpans(spansToInvalidate, oldTagTree); - // We either have some new tags, or we have some tags to invalidate. - // First, determine which of the old tags we want to keep around. - var snapshot = noNewTags ? spansToInvalidate.First().Snapshot : newTags.First().Span.Snapshot; - var oldTagsToKeep = noSpansToInvalidate - ? oldTagTree.GetSpans(snapshot) - : GetNonIntersectingTagSpans(spansToInvalidate, oldTagTree); + // Then union those with the new tags to produce the final tag tree. + var finalTags = oldTagsToKeep.Concat(newTags); + return new TagSpanIntervalTree(textBuffer, _dataSource.SpanTrackingMode, finalTags); + } - // Then union those with the new tags to produce the final tag tree. - var finalTags = oldTagsToKeep.Concat(newTags); - return new TagSpanIntervalTree(textBuffer, _dataSource.SpanTrackingMode, finalTags); - } + private IEnumerable> GetNonIntersectingTagSpans(IEnumerable spansToInvalidate, TagSpanIntervalTree oldTagTree) + { + var firstSpanToInvalidate = spansToInvalidate.First(); + var snapshot = firstSpanToInvalidate.Snapshot; - private IEnumerable> GetNonIntersectingTagSpans(IEnumerable spansToInvalidate, TagSpanIntervalTree oldTagTree) - { - var firstSpanToInvalidate = spansToInvalidate.First(); - var snapshot = firstSpanToInvalidate.Snapshot; + // Performance: No need to fully realize spansToInvalidate or do any of the calculations below if the + // full snapshot is being invalidated. + if (firstSpanToInvalidate.Length == snapshot.Length) + return []; - // Performance: No need to fully realize spansToInvalidate or do any of the calculations below if the - // full snapshot is being invalidated. - if (firstSpanToInvalidate.Length == snapshot.Length) - return []; + return oldTagTree.GetSpans(snapshot).Except( + spansToInvalidate.SelectMany(oldTagTree.GetIntersectingSpans), + comparer: this); + } - return oldTagTree.GetSpans(snapshot).Except( - spansToInvalidate.SelectMany(oldTagTree.GetIntersectingSpans), - comparer: this); - } + private bool ShouldSkipTagProduction() + { + if (_dataSource.Options.OfType>().Any(option => !_dataSource.GlobalOptions.GetOption(option))) + return true; - private bool ShouldSkipTagProduction() - { - if (_dataSource.Options.OfType>().Any(option => !_dataSource.GlobalOptions.GetOption(option))) - return true; + var languageName = _subjectBuffer.GetLanguageName(); + return _dataSource.Options.OfType>().Any(option => languageName == null || !_dataSource.GlobalOptions.GetOption(option, languageName)); + } - var languageName = _subjectBuffer.GetLanguageName(); - return _dataSource.Options.OfType>().Any(option => languageName == null || !_dataSource.GlobalOptions.GetOption(option, languageName)); - } + private Task ProduceTagsAsync(TaggerContext context, CancellationToken cancellationToken) + { + // If the feature is disabled, then just produce no tags. + return ShouldSkipTagProduction() + ? Task.CompletedTask + : _dataSource.ProduceTagsAsync(context, cancellationToken); + } - private Task ProduceTagsAsync(TaggerContext context, CancellationToken cancellationToken) + private Dictionary ProcessNewTagTrees( + ImmutableArray spansToTag, + ImmutableDictionary> oldTagTrees, + ImmutableDictionary> newTagTrees, + CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.Tagger_TagSource_ProcessNewTags, cancellationToken)) { - // If the feature is disabled, then just produce no tags. - return ShouldSkipTagProduction() - ? Task.CompletedTask - : _dataSource.ProduceTagsAsync(context, cancellationToken); - } + var bufferToChanges = new Dictionary(); - private Dictionary ProcessNewTagTrees( - ImmutableArray spansToTag, - ImmutableDictionary> oldTagTrees, - ImmutableDictionary> newTagTrees, - CancellationToken cancellationToken) - { - using (Logger.LogBlock(FunctionId.Tagger_TagSource_ProcessNewTags, cancellationToken)) + foreach (var (latestBuffer, latestSpans) in newTagTrees) { - var bufferToChanges = new Dictionary(); + var snapshot = spansToTag.First(s => s.SnapshotSpan.Snapshot.TextBuffer == latestBuffer).SnapshotSpan.Snapshot; - foreach (var (latestBuffer, latestSpans) in newTagTrees) + if (oldTagTrees.TryGetValue(latestBuffer, out var previousSpans)) { - var snapshot = spansToTag.First(s => s.SnapshotSpan.Snapshot.TextBuffer == latestBuffer).SnapshotSpan.Snapshot; - - if (oldTagTrees.TryGetValue(latestBuffer, out var previousSpans)) - { - var difference = ComputeDifference(snapshot, latestSpans, previousSpans); - bufferToChanges[latestBuffer] = difference; - } - else - { - // It's a new buffer, so report all spans are changed - bufferToChanges[latestBuffer] = new DiffResult(added: new(latestSpans.GetSpans(snapshot).Select(t => t.Span)), removed: null); - } + var difference = ComputeDifference(snapshot, latestSpans, previousSpans); + bufferToChanges[latestBuffer] = difference; } - - foreach (var (oldBuffer, previousSpans) in oldTagTrees) + else { - if (!newTagTrees.ContainsKey(oldBuffer)) - { - // This buffer disappeared, so let's notify that the old tags are gone - bufferToChanges[oldBuffer] = new DiffResult(added: null, removed: new(previousSpans.GetSpans(oldBuffer.CurrentSnapshot).Select(t => t.Span))); - } + // It's a new buffer, so report all spans are changed + bufferToChanges[latestBuffer] = new DiffResult(added: new(latestSpans.GetSpans(snapshot).Select(t => t.Span)), removed: null); } + } - return bufferToChanges; + foreach (var (oldBuffer, previousSpans) in oldTagTrees) + { + if (!newTagTrees.ContainsKey(oldBuffer)) + { + // This buffer disappeared, so let's notify that the old tags are gone + bufferToChanges[oldBuffer] = new DiffResult(added: null, removed: new(previousSpans.GetSpans(oldBuffer.CurrentSnapshot).Select(t => t.Span))); + } } + + return bufferToChanges; } + } - /// - /// Return all the spans that appear in only one of or . - /// - private DiffResult ComputeDifference( - ITextSnapshot snapshot, - TagSpanIntervalTree latestTree, - TagSpanIntervalTree previousTree) - { - var latestSpans = latestTree.GetSpans(snapshot); - var previousSpans = previousTree.GetSpans(snapshot); + /// + /// Return all the spans that appear in only one of or . + /// + private DiffResult ComputeDifference( + ITextSnapshot snapshot, + TagSpanIntervalTree latestTree, + TagSpanIntervalTree previousTree) + { + var latestSpans = latestTree.GetSpans(snapshot); + var previousSpans = previousTree.GetSpans(snapshot); - using var _1 = ArrayBuilder.GetInstance(out var added); - using var _2 = ArrayBuilder.GetInstance(out var removed); - using var latestEnumerator = latestSpans.GetEnumerator(); - using var previousEnumerator = previousSpans.GetEnumerator(); + using var _1 = ArrayBuilder.GetInstance(out var added); + using var _2 = ArrayBuilder.GetInstance(out var removed); + using var latestEnumerator = latestSpans.GetEnumerator(); + using var previousEnumerator = previousSpans.GetEnumerator(); - var latest = NextOrNull(latestEnumerator); - var previous = NextOrNull(previousEnumerator); + var latest = NextOrNull(latestEnumerator); + var previous = NextOrNull(previousEnumerator); - while (latest != null && previous != null) - { - var latestSpan = latest.Span; - var previousSpan = previous.Span; + while (latest != null && previous != null) + { + var latestSpan = latest.Span; + var previousSpan = previous.Span; - if (latestSpan.Start < previousSpan.Start) + if (latestSpan.Start < previousSpan.Start) + { + added.Add(latestSpan); + latest = NextOrNull(latestEnumerator); + } + else if (previousSpan.Start < latestSpan.Start) + { + removed.Add(previousSpan); + previous = NextOrNull(previousEnumerator); + } + else + { + // If the starts are the same, but the ends are different, report the larger + // region to be conservative. + if (previousSpan.End > latestSpan.End) { - added.Add(latestSpan); + removed.Add(previousSpan); latest = NextOrNull(latestEnumerator); } - else if (previousSpan.Start < latestSpan.Start) + else if (latestSpan.End > previousSpan.End) { - removed.Add(previousSpan); + added.Add(latestSpan); previous = NextOrNull(previousEnumerator); } else { - // If the starts are the same, but the ends are different, report the larger - // region to be conservative. - if (previousSpan.End > latestSpan.End) - { - removed.Add(previousSpan); - latest = NextOrNull(latestEnumerator); - } - else if (latestSpan.End > previousSpan.End) - { + if (!_dataSource.TagEquals(latest.Tag, previous.Tag)) added.Add(latestSpan); - previous = NextOrNull(previousEnumerator); - } - else - { - if (!_dataSource.TagEquals(latest.Tag, previous.Tag)) - added.Add(latestSpan); - - latest = NextOrNull(latestEnumerator); - previous = NextOrNull(previousEnumerator); - } - } - } - while (latest != null) - { - added.Add(latest.Span); - latest = NextOrNull(latestEnumerator); - } - - while (previous != null) - { - removed.Add(previous.Span); - previous = NextOrNull(previousEnumerator); + latest = NextOrNull(latestEnumerator); + previous = NextOrNull(previousEnumerator); + } } - - return new DiffResult(new(added), new(removed)); - - static ITagSpan? NextOrNull(IEnumerator> enumerator) - => enumerator.MoveNext() ? enumerator.Current : null; } - /// - /// Returns the TagSpanIntervalTree containing the tags for the given buffer. If no tags - /// exist for the buffer at all, null is returned. - /// - private TagSpanIntervalTree? TryGetTagIntervalTreeForBuffer(ITextBuffer buffer) + while (latest != null) { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + added.Add(latest.Span); + latest = NextOrNull(latestEnumerator); + } - // If we've been disposed, no need to proceed. - if (_disposalTokenSource.Token.IsCancellationRequested) - return null; + while (previous != null) + { + removed.Add(previous.Span); + previous = NextOrNull(previousEnumerator); + } - // If this is the first time we're being asked for tags, and we're a tagger that requires the initial - // tags be available synchronously on this call, and the computation of tags hasn't completed yet, then - // force the tags to be computed now on this thread. The singular use case for this is Outlining which - // needs those tags synchronously computed for things like Metadata-as-Source collapsing. - if (_firstTagsRequest && - _dataSource.ComputeInitialTagsSynchronously(buffer) && - !this.CachedTagTrees.TryGetValue(buffer, out _)) - { - // Compute this as a high priority work item to have the lease amount of blocking as possible. - _dataSource.ThreadingContext.JoinableTaskFactory.Run(() => - this.RecomputeTagsAsync(highPriority: true, _disposalTokenSource.Token)); - } + return new DiffResult(new(added), new(removed)); - _firstTagsRequest = false; + static ITagSpan? NextOrNull(IEnumerator> enumerator) + => enumerator.MoveNext() ? enumerator.Current : null; + } - // We're on the UI thread, so it's safe to access these variables. - this.CachedTagTrees.TryGetValue(buffer, out var tags); - return tags; + /// + /// Returns the TagSpanIntervalTree containing the tags for the given buffer. If no tags + /// exist for the buffer at all, null is returned. + /// + private TagSpanIntervalTree? TryGetTagIntervalTreeForBuffer(ITextBuffer buffer) + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + + // If we've been disposed, no need to proceed. + if (_disposalTokenSource.Token.IsCancellationRequested) + return null; + + // If this is the first time we're being asked for tags, and we're a tagger that requires the initial + // tags be available synchronously on this call, and the computation of tags hasn't completed yet, then + // force the tags to be computed now on this thread. The singular use case for this is Outlining which + // needs those tags synchronously computed for things like Metadata-as-Source collapsing. + if (_firstTagsRequest && + _dataSource.ComputeInitialTagsSynchronously(buffer) && + !this.CachedTagTrees.TryGetValue(buffer, out _)) + { + // Compute this as a high priority work item to have the lease amount of blocking as possible. + _dataSource.ThreadingContext.JoinableTaskFactory.Run(() => + this.RecomputeTagsAsync(highPriority: true, _disposalTokenSource.Token)); } - public void AddTags(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) - { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); + _firstTagsRequest = false; - // Some client is asking for tags. Possible that we're becoming visible. Preemptively start tagging - // again so we don't have to wait for the visibility notification to come in. - ResumeIfVisible(); + // We're on the UI thread, so it's safe to access these variables. + this.CachedTagTrees.TryGetValue(buffer, out var tags); + return tags; + } - if (requestedSpans.Count == 0) - return; + public void AddTags(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) + { + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - var buffer = requestedSpans.First().Snapshot.TextBuffer; - var tagIntervalTree = this.TryGetTagIntervalTreeForBuffer(buffer); + // Some client is asking for tags. Possible that we're becoming visible. Preemptively start tagging + // again so we don't have to wait for the visibility notification to come in. + ResumeIfVisible(); - tagIntervalTree?.AddIntersectingTagSpans(requestedSpans, tags); - } + if (requestedSpans.Count == 0) + return; + + var buffer = requestedSpans.First().Snapshot.TextBuffer; + var tagIntervalTree = this.TryGetTagIntervalTreeForBuffer(buffer); + + tagIntervalTree?.AddIntersectingTagSpans(requestedSpans, tags); } } } diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ReferenceCounting.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ReferenceCounting.cs index bd7974d1f375e..9a3503d556999 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ReferenceCounting.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ReferenceCounting.cs @@ -7,94 +7,93 @@ using System.Threading; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +internal partial class AbstractAsynchronousTaggerProvider { - internal partial class AbstractAsynchronousTaggerProvider + private partial class TagSource { - private partial class TagSource - { - /// How many taggers are currently using us. - private int _taggers = 0; + /// How many taggers are currently using us. + private int _taggers = 0; - ~TagSource() + ~TagSource() + { + if (!Environment.HasShutdownStarted) { - if (!Environment.HasShutdownStarted) - { #if DEBUG - Contract.Fail($@"Should have been disposed! + Contract.Fail($@"Should have been disposed! DataSource-StackTrace: {_dataSource.StackTrace} StackTrace: {_stackTrace}"); #else - Contract.Fail($@"Should have been disposed! Try running in Debug to get the allocation callstack"); + Contract.Fail($@"Should have been disposed! Try running in Debug to get the allocation callstack"); #endif - } } + } - internal void OnTaggerAdded(Tagger _) - { - // this should be only called from UI thread. - // in unit test, must be called from same thread as OnTaggerDisposed - Contract.ThrowIfFalse(_taggers >= 0); + internal void OnTaggerAdded(Tagger _) + { + // this should be only called from UI thread. + // in unit test, must be called from same thread as OnTaggerDisposed + Contract.ThrowIfFalse(_taggers >= 0); - _taggers++; + _taggers++; - DebugRecordCurrentThread(); - } + DebugRecordCurrentThread(); + } - internal void OnTaggerDisposed(Tagger _) - { - // this should be only called from UI thread. - // in unit test, must be called from same thread as OnTaggerAdded - Contract.ThrowIfFalse(_taggers > 0); + internal void OnTaggerDisposed(Tagger _) + { + // this should be only called from UI thread. + // in unit test, must be called from same thread as OnTaggerAdded + Contract.ThrowIfFalse(_taggers > 0); - _taggers--; + _taggers--; - if (_taggers == 0) - { - this.Dispose(); + if (_taggers == 0) + { + this.Dispose(); - DebugVerifyThread(); - } + DebugVerifyThread(); } + } - internal void TestOnly_Dispose() - => Dispose(); + internal void TestOnly_Dispose() + => Dispose(); #if DEBUG - private Thread? _thread; - private string? _stackTrace; + private Thread? _thread; + private string? _stackTrace; - private void DebugRecordInitialStackTrace() - => _stackTrace = new StackTrace().ToString(); + private void DebugRecordInitialStackTrace() + => _stackTrace = new StackTrace().ToString(); - private void DebugRecordCurrentThread() + private void DebugRecordCurrentThread() + { + if (_taggers != 1) { - if (_taggers != 1) - { - return; - } - - _thread = Thread.CurrentThread; + return; } - private void DebugVerifyThread() - => Contract.ThrowIfFalse(Thread.CurrentThread == _thread); + _thread = Thread.CurrentThread; + } + + private void DebugVerifyThread() + => Contract.ThrowIfFalse(Thread.CurrentThread == _thread); #else - private static void DebugRecordInitialStackTrace() - { - } + private static void DebugRecordInitialStackTrace() + { + } - private static void DebugRecordCurrentThread() - { - } + private static void DebugRecordCurrentThread() + { + } - private static void DebugVerifyThread() - { - } -#endif + private static void DebugVerifyThread() + { } +#endif } } diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_TagsChanged.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_TagsChanged.cs index 539ee6498771a..b218487bf372c 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_TagsChanged.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_TagsChanged.cs @@ -14,61 +14,60 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +internal partial class AbstractAsynchronousTaggerProvider { - internal partial class AbstractAsynchronousTaggerProvider + private partial class TagSource { - private partial class TagSource + public event EventHandler? TagsChanged; + + private void OnTagsChangedForBuffer( + ICollection> changes, bool highPriority) { - public event EventHandler? TagsChanged; + _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - private void OnTagsChangedForBuffer( - ICollection> changes, bool highPriority) + foreach (var change in changes) { - _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); - - foreach (var change in changes) - { - if (change.Key != _subjectBuffer) - continue; + if (change.Key != _subjectBuffer) + continue; - // Removed tags are always treated as high pri, so we can clean their stale - // data out from the ui immediately. - _highPriTagsChangedQueue.AddWork(change.Value.Removed); + // Removed tags are always treated as high pri, so we can clean their stale + // data out from the ui immediately. + _highPriTagsChangedQueue.AddWork(change.Value.Removed); - // Added tags are run at the requested priority. - var addedTagsQueue = highPriority ? _highPriTagsChangedQueue : _normalPriTagsChangedQueue; - addedTagsQueue.AddWork(change.Value.Added); - } + // Added tags are run at the requested priority. + var addedTagsQueue = highPriority ? _highPriTagsChangedQueue : _normalPriTagsChangedQueue; + addedTagsQueue.AddWork(change.Value.Added); } + } - private ValueTask ProcessTagsChangedAsync( - ImmutableSegmentedList snapshotSpans, CancellationToken cancellationToken) - { - var tagsChanged = this.TagsChanged; - if (tagsChanged == null) - return ValueTaskFactory.CompletedTask; - - foreach (var collection in snapshotSpans) - { - if (collection.Count == 0) - continue; + private ValueTask ProcessTagsChangedAsync( + ImmutableSegmentedList snapshotSpans, CancellationToken cancellationToken) + { + var tagsChanged = this.TagsChanged; + if (tagsChanged == null) + return ValueTaskFactory.CompletedTask; - var snapshot = collection.First().Snapshot; + foreach (var collection in snapshotSpans) + { + if (collection.Count == 0) + continue; - // Coalesce the spans if there are a lot of them. - var coalesced = collection.Count > CoalesceDifferenceCount - ? new NormalizedSnapshotSpanCollection(snapshot.GetSpanFromBounds(collection.First().Start, collection.Last().End)) - : collection; + var snapshot = collection.First().Snapshot; - _dataSource.BeforeTagsChanged(snapshot); + // Coalesce the spans if there are a lot of them. + var coalesced = collection.Count > CoalesceDifferenceCount + ? new NormalizedSnapshotSpanCollection(snapshot.GetSpanFromBounds(collection.First().Start, collection.Last().End)) + : collection; - foreach (var span in coalesced) - tagsChanged(this, new SnapshotSpanEventArgs(span)); - } + _dataSource.BeforeTagsChanged(snapshot); - return ValueTaskFactory.CompletedTask; + foreach (var span in coalesced) + tagsChanged(this, new SnapshotSpanEventArgs(span)); } + + return ValueTaskFactory.CompletedTask; } } } diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs index c13b574681ba7..68a64bd1f494d 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs @@ -25,249 +25,248 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +/// +/// Base type of all asynchronous tagger providers ( and ). +/// +internal abstract partial class AbstractAsynchronousTaggerProvider where TTag : ITag { + private readonly object _uniqueKey = new(); + + protected readonly IAsynchronousOperationListener AsyncListener; + protected readonly IThreadingContext ThreadingContext; + protected readonly IGlobalOptionService GlobalOptions; + private readonly ITextBufferVisibilityTracker? _visibilityTracker; + + /// + /// The behavior the tagger engine will have when text changes happen to the subject buffer + /// it is attached to. Most taggers can simply use . + /// However, advanced taggers that want to perform specialized behavior depending on what has + /// actually changed in the file can specify . + /// + /// If this is specified the tagger engine will track text changes and pass them along as + /// when calling + /// . + /// + protected virtual TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.None; + + /// + /// The behavior the tagger will have when changes happen to the caret. + /// + protected virtual TaggerCaretChangeBehavior CaretChangeBehavior => TaggerCaretChangeBehavior.None; + + /// + /// The behavior of tags that are created by the async tagger. This will matter for tags + /// created for a previous version of a document that are mapped forward by the async + /// tagging architecture. This value cannot be . + /// + protected virtual SpanTrackingMode SpanTrackingMode => SpanTrackingMode.EdgeExclusive; + + /// + /// Global options controlling if the tagger should tag or not. These correspond to user facing options to + /// completely disable a feature or not. + /// + /// An empty enumerable can be returned to indicate that this tagger should run unconditionally. + /// + /// All values must either be an or a . + protected virtual ImmutableArray Options => []; + + /// + /// Options controlling the feature that should be used to determine if the feature should recompute tags. + /// These generally correspond to user facing options to change how a feature behaves if it is running. + /// + protected virtual ImmutableArray FeatureOptions => []; + + protected virtual bool ComputeInitialTagsSynchronously(ITextBuffer subjectBuffer) => false; + + /// + /// How long the tagger should wait after hearing about an event before recomputing tags. + /// + protected abstract TaggerDelay EventChangeDelay { get; } + + /// + /// This controls what delay tagger will use to let editor know about newly inserted tags + /// + protected virtual TaggerDelay AddedTagNotificationDelay => TaggerDelay.NearImmediate; + /// - /// Base type of all asynchronous tagger providers ( and ). + /// Whether or not events from the should cancel in-flight tag-computation. /// - internal abstract partial class AbstractAsynchronousTaggerProvider where TTag : ITag + protected virtual bool CancelOnNewWork { get; } + + protected virtual void BeforeTagsChanged(ITextSnapshot snapshot) { - private readonly object _uniqueKey = new(); - - protected readonly IAsynchronousOperationListener AsyncListener; - protected readonly IThreadingContext ThreadingContext; - protected readonly IGlobalOptionService GlobalOptions; - private readonly ITextBufferVisibilityTracker? _visibilityTracker; - - /// - /// The behavior the tagger engine will have when text changes happen to the subject buffer - /// it is attached to. Most taggers can simply use . - /// However, advanced taggers that want to perform specialized behavior depending on what has - /// actually changed in the file can specify . - /// - /// If this is specified the tagger engine will track text changes and pass them along as - /// when calling - /// . - /// - protected virtual TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.None; - - /// - /// The behavior the tagger will have when changes happen to the caret. - /// - protected virtual TaggerCaretChangeBehavior CaretChangeBehavior => TaggerCaretChangeBehavior.None; - - /// - /// The behavior of tags that are created by the async tagger. This will matter for tags - /// created for a previous version of a document that are mapped forward by the async - /// tagging architecture. This value cannot be . - /// - protected virtual SpanTrackingMode SpanTrackingMode => SpanTrackingMode.EdgeExclusive; - - /// - /// Global options controlling if the tagger should tag or not. These correspond to user facing options to - /// completely disable a feature or not. - /// - /// An empty enumerable can be returned to indicate that this tagger should run unconditionally. - /// - /// All values must either be an or a . - protected virtual ImmutableArray Options => []; - - /// - /// Options controlling the feature that should be used to determine if the feature should recompute tags. - /// These generally correspond to user facing options to change how a feature behaves if it is running. - /// - protected virtual ImmutableArray FeatureOptions => []; - - protected virtual bool ComputeInitialTagsSynchronously(ITextBuffer subjectBuffer) => false; - - /// - /// How long the tagger should wait after hearing about an event before recomputing tags. - /// - protected abstract TaggerDelay EventChangeDelay { get; } - - /// - /// This controls what delay tagger will use to let editor know about newly inserted tags - /// - protected virtual TaggerDelay AddedTagNotificationDelay => TaggerDelay.NearImmediate; - - /// - /// Whether or not events from the should cancel in-flight tag-computation. - /// - protected virtual bool CancelOnNewWork { get; } - - protected virtual void BeforeTagsChanged(ITextSnapshot snapshot) - { - } + } + + /// + /// Comparer used to check if two tags are the same. Used so that when new tags are produced, they can be + /// appropriately 'diffed' to determine what changes to actually report in . + /// + /// Subclasses should always override this. It is only virtual for binary compat. + /// + /// + protected virtual bool TagEquals(TTag tag1, TTag tag2) + => EqualityComparer.Default.Equals(tag1, tag2); - /// - /// Comparer used to check if two tags are the same. Used so that when new tags are produced, they can be - /// appropriately 'diffed' to determine what changes to actually report in . - /// - /// Subclasses should always override this. It is only virtual for binary compat. - /// - /// - protected virtual bool TagEquals(TTag tag1, TTag tag2) - => EqualityComparer.Default.Equals(tag1, tag2); - - // Prevent accidental usage of object.Equals instead of TagEquals when comparing tags. - [Obsolete("Did you mean to call TagEquals(TTag tag1, TTag tag2) instead", error: true)] - public static new bool Equals(object objA, object objB) - => throw ExceptionUtilities.Unreachable(); + // Prevent accidental usage of object.Equals instead of TagEquals when comparing tags. + [Obsolete("Did you mean to call TagEquals(TTag tag1, TTag tag2) instead", error: true)] + public static new bool Equals(object objA, object objB) + => throw ExceptionUtilities.Unreachable(); #if DEBUG - public readonly string StackTrace; + public readonly string StackTrace; #endif - protected AbstractAsynchronousTaggerProvider( - IThreadingContext threadingContext, - IGlobalOptionService globalOptions, - ITextBufferVisibilityTracker? visibilityTracker, - IAsynchronousOperationListener asyncListener) - { - ThreadingContext = threadingContext; - GlobalOptions = globalOptions; - AsyncListener = asyncListener; - _visibilityTracker = visibilityTracker; + protected AbstractAsynchronousTaggerProvider( + IThreadingContext threadingContext, + IGlobalOptionService globalOptions, + ITextBufferVisibilityTracker? visibilityTracker, + IAsynchronousOperationListener asyncListener) + { + ThreadingContext = threadingContext; + GlobalOptions = globalOptions; + AsyncListener = asyncListener; + _visibilityTracker = visibilityTracker; #if DEBUG - StackTrace = new StackTrace().ToString(); + StackTrace = new StackTrace().ToString(); #endif - } + } - protected EfficientTagger? CreateEfficientTagger(ITextView? textView, ITextBuffer subjectBuffer) - { - if (!GlobalOptions.GetOption(EditorComponentOnOffOptions.Tagger)) - return null; + protected EfficientTagger? CreateEfficientTagger(ITextView? textView, ITextBuffer subjectBuffer) + { + if (!GlobalOptions.GetOption(EditorComponentOnOffOptions.Tagger)) + return null; - var tagSource = GetOrCreateTagSource(textView, subjectBuffer); - var tagger = new Tagger(tagSource); + var tagSource = GetOrCreateTagSource(textView, subjectBuffer); + var tagger = new Tagger(tagSource); - return tagger; - } + return tagger; + } - private TagSource GetOrCreateTagSource(ITextView? textView, ITextBuffer subjectBuffer) + private TagSource GetOrCreateTagSource(ITextView? textView, ITextBuffer subjectBuffer) + { + if (!this.TryRetrieveTagSource(textView, subjectBuffer, out var tagSource)) { - if (!this.TryRetrieveTagSource(textView, subjectBuffer, out var tagSource)) - { - tagSource = new TagSource(textView, subjectBuffer, _visibilityTracker, this, AsyncListener); - this.StoreTagSource(textView, subjectBuffer, tagSource); - } - - return tagSource; + tagSource = new TagSource(textView, subjectBuffer, _visibilityTracker, this, AsyncListener); + this.StoreTagSource(textView, subjectBuffer, tagSource); } - private bool TryRetrieveTagSource(ITextView? textView, ITextBuffer subjectBuffer, [NotNullWhen(true)] out TagSource? tagSource) - { - return textView != null - ? textView.TryGetPerSubjectBufferProperty(subjectBuffer, _uniqueKey, out tagSource) - : subjectBuffer.Properties.TryGetProperty(_uniqueKey, out tagSource); - } + return tagSource; + } - private void RemoveTagSource(ITextView? textView, ITextBuffer subjectBuffer) + private bool TryRetrieveTagSource(ITextView? textView, ITextBuffer subjectBuffer, [NotNullWhen(true)] out TagSource? tagSource) + { + return textView != null + ? textView.TryGetPerSubjectBufferProperty(subjectBuffer, _uniqueKey, out tagSource) + : subjectBuffer.Properties.TryGetProperty(_uniqueKey, out tagSource); + } + + private void RemoveTagSource(ITextView? textView, ITextBuffer subjectBuffer) + { + if (textView != null) { - if (textView != null) - { - textView.RemovePerSubjectBufferProperty(subjectBuffer, _uniqueKey); - } - else - { - subjectBuffer.Properties.RemoveProperty(_uniqueKey); - } + textView.RemovePerSubjectBufferProperty(subjectBuffer, _uniqueKey); } - - private void StoreTagSource(ITextView? textView, ITextBuffer subjectBuffer, TagSource tagSource) + else { - if (textView != null) - { - textView.AddPerSubjectBufferProperty(subjectBuffer, _uniqueKey, tagSource); - } - else - { - subjectBuffer.Properties.AddProperty(_uniqueKey, tagSource); - } + subjectBuffer.Properties.RemoveProperty(_uniqueKey); } + } - /// - /// Called by the infrastructure to - /// determine the caret position. This value will be passed in as the value to - /// in the call to - /// . - /// - protected virtual SnapshotPoint? GetCaretPoint(ITextView? textView, ITextBuffer subjectBuffer) - => textView?.GetCaretPoint(subjectBuffer); - - /// - /// Called by the infrastructure to determine - /// the set of spans that it should asynchronously tag. This will be called in response to - /// notifications from the that something has changed, and - /// will only be called from the UI thread. The tagger infrastructure will then determine - /// the s associated with these s - /// and will asynchronously call into at some point in - /// the future to produce tags for these spans. - /// - protected virtual IEnumerable GetSpansToTag(ITextView? textView, ITextBuffer subjectBuffer) + private void StoreTagSource(ITextView? textView, ITextBuffer subjectBuffer, TagSource tagSource) + { + if (textView != null) { - // For a standard tagger, the spans to tag is the span of the entire snapshot. - return SpecializedCollections.SingletonEnumerable(subjectBuffer.CurrentSnapshot.GetFullSpan()); + textView.AddPerSubjectBufferProperty(subjectBuffer, _uniqueKey, tagSource); } - - /// - /// Creates the that notifies the - /// that it should recompute tags for the text buffer after an appropriate . - /// - protected abstract ITaggerEventSource CreateEventSource(ITextView? textView, ITextBuffer subjectBuffer); - - /// - /// Produce tags for the given context. - /// - protected virtual async Task ProduceTagsAsync( - TaggerContext context, CancellationToken cancellationToken) + else { - foreach (var spanToTag in context.SpansToTag) - { - cancellationToken.ThrowIfCancellationRequested(); - await ProduceTagsAsync( - context, spanToTag, - GetCaretPosition(context.CaretPosition, spanToTag.SnapshotSpan), - cancellationToken).ConfigureAwait(false); - } + subjectBuffer.Properties.AddProperty(_uniqueKey, tagSource); } + } + + /// + /// Called by the infrastructure to + /// determine the caret position. This value will be passed in as the value to + /// in the call to + /// . + /// + protected virtual SnapshotPoint? GetCaretPoint(ITextView? textView, ITextBuffer subjectBuffer) + => textView?.GetCaretPoint(subjectBuffer); - private static int? GetCaretPosition(SnapshotPoint? caretPosition, SnapshotSpan snapshotSpan) + /// + /// Called by the infrastructure to determine + /// the set of spans that it should asynchronously tag. This will be called in response to + /// notifications from the that something has changed, and + /// will only be called from the UI thread. The tagger infrastructure will then determine + /// the s associated with these s + /// and will asynchronously call into at some point in + /// the future to produce tags for these spans. + /// + protected virtual IEnumerable GetSpansToTag(ITextView? textView, ITextBuffer subjectBuffer) + { + // For a standard tagger, the spans to tag is the span of the entire snapshot. + return SpecializedCollections.SingletonEnumerable(subjectBuffer.CurrentSnapshot.GetFullSpan()); + } + + /// + /// Creates the that notifies the + /// that it should recompute tags for the text buffer after an appropriate . + /// + protected abstract ITaggerEventSource CreateEventSource(ITextView? textView, ITextBuffer subjectBuffer); + + /// + /// Produce tags for the given context. + /// + protected virtual async Task ProduceTagsAsync( + TaggerContext context, CancellationToken cancellationToken) + { + foreach (var spanToTag in context.SpansToTag) { - return caretPosition.HasValue && caretPosition.Value.Snapshot == snapshotSpan.Snapshot - ? caretPosition.Value.Position : null; + cancellationToken.ThrowIfCancellationRequested(); + await ProduceTagsAsync( + context, spanToTag, + GetCaretPosition(context.CaretPosition, spanToTag.SnapshotSpan), + cancellationToken).ConfigureAwait(false); } + } - protected virtual Task ProduceTagsAsync(TaggerContext context, DocumentSnapshotSpan spanToTag, int? caretPosition, CancellationToken cancellationToken) - => Task.CompletedTask; + private static int? GetCaretPosition(SnapshotPoint? caretPosition, SnapshotSpan snapshotSpan) + { + return caretPosition.HasValue && caretPosition.Value.Snapshot == snapshotSpan.Snapshot + ? caretPosition.Value.Position : null; + } - public bool SpanEquals(ITextSnapshot snapshot1, TextSpan? span1, ITextSnapshot snapshot2, TextSpan? span2) - => SpanEquals(snapshot1, span1?.ToSpan(), snapshot2, span2?.ToSpan()); + protected virtual Task ProduceTagsAsync(TaggerContext context, DocumentSnapshotSpan spanToTag, int? caretPosition, CancellationToken cancellationToken) + => Task.CompletedTask; - public bool SpanEquals(ITextSnapshot snapshot1, Span? span1, ITextSnapshot snapshot2, Span? span2) - => SpanEquals(span1 is null ? null : new SnapshotSpan(snapshot1, span1.Value), span2 is null ? null : new SnapshotSpan(snapshot2, span2.Value)); + public bool SpanEquals(ITextSnapshot snapshot1, TextSpan? span1, ITextSnapshot snapshot2, TextSpan? span2) + => SpanEquals(snapshot1, span1?.ToSpan(), snapshot2, span2?.ToSpan()); - public bool SpanEquals(SnapshotSpan? span1, SnapshotSpan? span2) - => TaggerUtilities.SpanEquals(span1, span2, this.SpanTrackingMode); + public bool SpanEquals(ITextSnapshot snapshot1, Span? span1, ITextSnapshot snapshot2, Span? span2) + => SpanEquals(span1 is null ? null : new SnapshotSpan(snapshot1, span1.Value), span2 is null ? null : new SnapshotSpan(snapshot2, span2.Value)); - internal TestAccessor GetTestAccessor() - => new(this); + public bool SpanEquals(SnapshotSpan? span1, SnapshotSpan? span2) + => TaggerUtilities.SpanEquals(span1, span2, this.SpanTrackingMode); - private readonly struct DiffResult(NormalizedSnapshotSpanCollection? added, NormalizedSnapshotSpanCollection? removed) - { - public readonly NormalizedSnapshotSpanCollection Added = added ?? NormalizedSnapshotSpanCollection.Empty; - public readonly NormalizedSnapshotSpanCollection Removed = removed ?? NormalizedSnapshotSpanCollection.Empty; + internal TestAccessor GetTestAccessor() + => new(this); - public int Count => Added.Count + Removed.Count; - } + private readonly struct DiffResult(NormalizedSnapshotSpanCollection? added, NormalizedSnapshotSpanCollection? removed) + { + public readonly NormalizedSnapshotSpanCollection Added = added ?? NormalizedSnapshotSpanCollection.Empty; + public readonly NormalizedSnapshotSpanCollection Removed = removed ?? NormalizedSnapshotSpanCollection.Empty; - internal readonly struct TestAccessor(AbstractAsynchronousTaggerProvider provider) - { - private readonly AbstractAsynchronousTaggerProvider _provider = provider; + public int Count => Added.Count + Removed.Count; + } - internal Task ProduceTagsAsync(TaggerContext context) - => _provider.ProduceTagsAsync(context, CancellationToken.None); - } + internal readonly struct TestAccessor(AbstractAsynchronousTaggerProvider provider) + { + private readonly AbstractAsynchronousTaggerProvider _provider = provider; + + internal Task ProduceTagsAsync(TaggerContext context) + => _provider.ProduceTagsAsync(context, CancellationToken.None); } } diff --git a/src/EditorFeatures/Core/Tagging/CompilationAvailableTaggerEventSource.cs b/src/EditorFeatures/Core/Tagging/CompilationAvailableTaggerEventSource.cs deleted file mode 100644 index e25594cb44d9f..0000000000000 --- a/src/EditorFeatures/Core/Tagging/CompilationAvailableTaggerEventSource.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Editor.Shared.Tagging; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Remote; -using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.CodeAnalysis.Shared.Utilities; -using Microsoft.CodeAnalysis.Tagging; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Threading; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Editor.Tagging -{ - /// - /// Tagger event that fires once the compilation is available in the remote OOP process for a particular project. - /// Used to trigger things such as: - /// - /// reclassification pass as classification may show either cached classifications (from a previous session), - /// or incomplete classifications due to frozen-partial compilations being used. - /// recomputation of navigation bar items due to frozen-partial compilations being used. - /// recomputation of inheritance margin items due to frozen-partial compilations being used. - /// - /// - internal sealed class CompilationAvailableTaggerEventSource : ITaggerEventSource - { - private readonly ITextBuffer _subjectBuffer; - - /// - /// Other event sources we're composing over. If they fire, we should reclassify. However, after they fire, we - /// should also refire an event once we get the next full compilation ready. - /// - private readonly ITaggerEventSource _underlyingSource; - - private readonly CompilationAvailableEventSource _eventSource; - - private readonly Action _onCompilationAvailable; - - public CompilationAvailableTaggerEventSource( - ITextBuffer subjectBuffer, - IAsynchronousOperationListener asyncListener, - params ITaggerEventSource[] eventSources) - { - _subjectBuffer = subjectBuffer; - _eventSource = new CompilationAvailableEventSource(asyncListener); - _underlyingSource = TaggerEventSources.Compose(eventSources); - _onCompilationAvailable = () => this.Changed?.Invoke(this, TaggerEventArgs.Empty); - } - - public event EventHandler? Changed; - - public void Connect() - { - // When we are connected to, connect to all our underlying sources and have them notify us when they've changed. - _underlyingSource.Connect(); - _underlyingSource.Changed += OnUnderlyingSourceChanged; - } - - public void Disconnect() - { - _underlyingSource.Changed -= OnUnderlyingSourceChanged; - _underlyingSource.Disconnect(); - _eventSource.Dispose(); - } - - public void Pause() - => _underlyingSource.Pause(); - - public void Resume() - => _underlyingSource.Resume(); - - private void OnUnderlyingSourceChanged(object? sender, TaggerEventArgs args) - { - // First, notify anyone listening to us that something definitely changed. - this.Changed?.Invoke(this, args); - - var document = _subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document == null) - return; - - _eventSource.EnsureCompilationAvailability(document.Project, _onCompilationAvailable); - } - } -} diff --git a/src/EditorFeatures/Core/Tagging/ITaggerEventSource.cs b/src/EditorFeatures/Core/Tagging/ITaggerEventSource.cs index 671ff7a86d645..bba97133761dc 100644 --- a/src/EditorFeatures/Core/Tagging/ITaggerEventSource.cs +++ b/src/EditorFeatures/Core/Tagging/ITaggerEventSource.cs @@ -6,45 +6,44 @@ using System; -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +/// +/// The events that the listens to, to know when +/// to request more tags. For example, an may listen to text +/// buffer changes, and can tell the that it needs +/// to recompute tags. +/// +internal interface ITaggerEventSource { /// - /// The events that the listens to, to know when - /// to request more tags. For example, an may listen to text - /// buffer changes, and can tell the that it needs - /// to recompute tags. + /// Let event source know that it should start sending out events. Implementation can use + /// that as a point to attach to events and perform other initialization. This will only be + /// called once. + /// + void Connect(); + + /// + /// Let event source know that it is no longer needed. Implementations can use this as a + /// point to detach from events and perform other cleanup. This will only be called once. + /// + void Disconnect(); + + /// + /// Pauses this event source and prevents it from firing the event. Can be called many + /// times (but subsequence calls have no impact if already paused). Must be called on the UI thread. + /// + void Pause(); + + /// + /// Resumes this event source and allows firing the event. Can be called many times (but + /// subsequence calls have no impact if already resumed). Must be called on the UI thread. + /// + void Resume(); + + /// + /// An event has happened on the thing the tagger is attached to. The tagger should + /// recompute tags. /// - internal interface ITaggerEventSource - { - /// - /// Let event source know that it should start sending out events. Implementation can use - /// that as a point to attach to events and perform other initialization. This will only be - /// called once. - /// - void Connect(); - - /// - /// Let event source know that it is no longer needed. Implementations can use this as a - /// point to detach from events and perform other cleanup. This will only be called once. - /// - void Disconnect(); - - /// - /// Pauses this event source and prevents it from firing the event. Can be called many - /// times (but subsequence calls have no impact if already paused). Must be called on the UI thread. - /// - void Pause(); - - /// - /// Resumes this event source and allows firing the event. Can be called many times (but - /// subsequence calls have no impact if already resumed). Must be called on the UI thread. - /// - void Resume(); - - /// - /// An event has happened on the thing the tagger is attached to. The tagger should - /// recompute tags. - /// - event EventHandler Changed; - } + event EventHandler Changed; } diff --git a/src/EditorFeatures/Core/Tagging/TaggerCaretChangeBehavior.cs b/src/EditorFeatures/Core/Tagging/TaggerCaretChangeBehavior.cs index d548fc8063196..b419e476adae7 100644 --- a/src/EditorFeatures/Core/Tagging/TaggerCaretChangeBehavior.cs +++ b/src/EditorFeatures/Core/Tagging/TaggerCaretChangeBehavior.cs @@ -6,22 +6,21 @@ using System; -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +/// +/// Flags that affect how the tagger infrastructure responds to caret changes. +/// +[Flags] +internal enum TaggerCaretChangeBehavior { /// - /// Flags that affect how the tagger infrastructure responds to caret changes. + /// No special caret change behavior. /// - [Flags] - internal enum TaggerCaretChangeBehavior - { - /// - /// No special caret change behavior. - /// - None = 0, + None = 0, - /// - /// If the caret moves outside of a tag, immediately remove all existing tags. - /// - RemoveAllTagsOnCaretMoveOutsideOfTag = 1 << 0, - } + /// + /// If the caret moves outside of a tag, immediately remove all existing tags. + /// + RemoveAllTagsOnCaretMoveOutsideOfTag = 1 << 0, } diff --git a/src/EditorFeatures/Core/Tagging/TaggerContext.cs b/src/EditorFeatures/Core/Tagging/TaggerContext.cs index a0b35a9fde1cc..de20723c6349e 100644 --- a/src/EditorFeatures/Core/Tagging/TaggerContext.cs +++ b/src/EditorFeatures/Core/Tagging/TaggerContext.cs @@ -17,79 +17,78 @@ using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +internal class TaggerContext where TTag : ITag { - internal class TaggerContext where TTag : ITag - { - private readonly ImmutableDictionary> _existingTags; + private readonly ImmutableDictionary> _existingTags; - internal ImmutableArray _spansTagged; - public readonly SegmentedList> TagSpans = []; + internal ImmutableArray _spansTagged; + public readonly SegmentedList> TagSpans = []; - public ImmutableArray SpansToTag { get; } - public SnapshotPoint? CaretPosition { get; } + public ImmutableArray SpansToTag { get; } + public SnapshotPoint? CaretPosition { get; } - /// - /// The text that has changed between the last successful tagging and this new request to - /// produce tags. In order to be passed this value, - /// must be specified in . - /// - public TextChangeRange? TextChangeRange { get; } + /// + /// The text that has changed between the last successful tagging and this new request to + /// produce tags. In order to be passed this value, + /// must be specified in . + /// + public TextChangeRange? TextChangeRange { get; } - /// - /// The state of the tagger. Taggers can use this to keep track of information across calls - /// to . Note: state will - /// only be preserved if the tagger infrastructure fully updates itself with the tags that - /// were produced. i.e. if that tagging pass is canceled, then the state set here will not - /// be preserved and the previous preserved state will be used the next time ProduceTagsAsync - /// is called. - /// - public object State { get; set; } + /// + /// The state of the tagger. Taggers can use this to keep track of information across calls + /// to . Note: state will + /// only be preserved if the tagger infrastructure fully updates itself with the tags that + /// were produced. i.e. if that tagging pass is canceled, then the state set here will not + /// be preserved and the previous preserved state will be used the next time ProduceTagsAsync + /// is called. + /// + public object State { get; set; } - // For testing only. - internal TaggerContext( - Document document, ITextSnapshot snapshot, - SnapshotPoint? caretPosition = null, - TextChangeRange? textChangeRange = null) - : this(state: null, [new DocumentSnapshotSpan(document, snapshot.GetFullSpan())], - caretPosition, textChangeRange, existingTags: null) - { - } + // For testing only. + internal TaggerContext( + Document document, ITextSnapshot snapshot, + SnapshotPoint? caretPosition = null, + TextChangeRange? textChangeRange = null) + : this(state: null, [new DocumentSnapshotSpan(document, snapshot.GetFullSpan())], + caretPosition, textChangeRange, existingTags: null) + { + } - internal TaggerContext( - object state, - ImmutableArray spansToTag, - SnapshotPoint? caretPosition, - TextChangeRange? textChangeRange, - ImmutableDictionary> existingTags) - { - this.State = state; - this.SpansToTag = spansToTag; - this.CaretPosition = caretPosition; - this.TextChangeRange = textChangeRange; + internal TaggerContext( + object state, + ImmutableArray spansToTag, + SnapshotPoint? caretPosition, + TextChangeRange? textChangeRange, + ImmutableDictionary> existingTags) + { + this.State = state; + this.SpansToTag = spansToTag; + this.CaretPosition = caretPosition; + this.TextChangeRange = textChangeRange; - _spansTagged = spansToTag.SelectAsArray(ds => ds.SnapshotSpan); - _existingTags = existingTags; - } + _spansTagged = spansToTag.SelectAsArray(ds => ds.SnapshotSpan); + _existingTags = existingTags; + } - public void AddTag(ITagSpan tag) - => TagSpans.Add(tag); + public void AddTag(ITagSpan tag) + => TagSpans.Add(tag); - public void ClearTags() - => TagSpans.Clear(); + public void ClearTags() + => TagSpans.Clear(); - /// - /// Used to allow taggers to indicate what spans were actually tagged. This is useful when the tagger decides - /// to tag a different span than the entire file. If a sub-span of a document is tagged then the tagger - /// infrastructure will keep previously computed tags from before and after the sub-span and merge them with the - /// newly produced tags. - /// - public void SetSpansTagged(ImmutableArray spansTagged) - => _spansTagged = spansTagged; + /// + /// Used to allow taggers to indicate what spans were actually tagged. This is useful when the tagger decides + /// to tag a different span than the entire file. If a sub-span of a document is tagged then the tagger + /// infrastructure will keep previously computed tags from before and after the sub-span and merge them with the + /// newly produced tags. + /// + public void SetSpansTagged(ImmutableArray spansTagged) + => _spansTagged = spansTagged; - public bool HasExistingContainingTags(SnapshotPoint point) - => _existingTags != null && - _existingTags.TryGetValue(point.Snapshot.TextBuffer, out var tree) && - tree.HasSpanThatContains(point); - } + public bool HasExistingContainingTags(SnapshotPoint point) + => _existingTags != null && + _existingTags.TryGetValue(point.Snapshot.TextBuffer, out var tree) && + tree.HasSpanThatContains(point); } diff --git a/src/EditorFeatures/Core/Tagging/TaggerDelay.cs b/src/EditorFeatures/Core/Tagging/TaggerDelay.cs index 0f1cc3fe90ad7..e4aa0203a759b 100644 --- a/src/EditorFeatures/Core/Tagging/TaggerDelay.cs +++ b/src/EditorFeatures/Core/Tagging/TaggerDelay.cs @@ -4,43 +4,42 @@ #nullable disable -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +/// +/// How quickly the should update tags after +/// receiving an notification. +/// +internal enum TaggerDelay { /// - /// How quickly the should update tags after - /// receiving an notification. + /// Indicates that the tagger should retag after a short, but imperceptible delay. This is + /// for features that want to appear instantaneous to the user, but which can wait a short + /// while until a batch of changes has occurred before processing. Specifically, if a user + /// expects the tag immediately after typing a character or moving the caret, then this + /// delay should be used. /// - internal enum TaggerDelay - { - /// - /// Indicates that the tagger should retag after a short, but imperceptible delay. This is - /// for features that want to appear instantaneous to the user, but which can wait a short - /// while until a batch of changes has occurred before processing. Specifically, if a user - /// expects the tag immediately after typing a character or moving the caret, then this - /// delay should be used. - /// - NearImmediate, + NearImmediate, - /// - /// Not as fast as NearImmediate. A user typing quickly or navigating quickly should not - /// trigger this. However, any sort of pause will cause it to trigger - /// - Short, + /// + /// Not as fast as NearImmediate. A user typing quickly or navigating quickly should not + /// trigger this. However, any sort of pause will cause it to trigger + /// + Short, - /// - /// Not as fast as 'Short'. The user's pause should be more significant until the tag - /// appears. - /// - Medium, + /// + /// Not as fast as 'Short'. The user's pause should be more significant until the tag + /// appears. + /// + Medium, - /// - /// Indicates that the tagger should run when the user appears to be idle. - /// - OnIdle, + /// + /// Indicates that the tagger should run when the user appears to be idle. + /// + OnIdle, - /// - /// Indicates that the tagger is not view, and should be on a very delayed update cadence. - /// - NonFocus, - } + /// + /// Indicates that the tagger is not view, and should be on a very delayed update cadence. + /// + NonFocus, } diff --git a/src/EditorFeatures/Core/Tagging/TaggerEventArgs.cs b/src/EditorFeatures/Core/Tagging/TaggerEventArgs.cs index 55b580c93cd6f..bbf2191d64ad0 100644 --- a/src/EditorFeatures/Core/Tagging/TaggerEventArgs.cs +++ b/src/EditorFeatures/Core/Tagging/TaggerEventArgs.cs @@ -4,18 +4,17 @@ using System; -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +/// +/// Information provided to the when +/// fires. +/// +internal class TaggerEventArgs : EventArgs { - /// - /// Information provided to the when - /// fires. - /// - internal class TaggerEventArgs : EventArgs - { - public static new readonly TaggerEventArgs Empty = new(); + public static new readonly TaggerEventArgs Empty = new(); - private TaggerEventArgs() - { - } + private TaggerEventArgs() + { } } diff --git a/src/EditorFeatures/Core/Tagging/TaggerTextChangeBehavior.cs b/src/EditorFeatures/Core/Tagging/TaggerTextChangeBehavior.cs index b38d462778dc0..b91bfe6d6058f 100644 --- a/src/EditorFeatures/Core/Tagging/TaggerTextChangeBehavior.cs +++ b/src/EditorFeatures/Core/Tagging/TaggerTextChangeBehavior.cs @@ -5,42 +5,41 @@ using System; using System.Threading; -namespace Microsoft.CodeAnalysis.Editor.Tagging +namespace Microsoft.CodeAnalysis.Editor.Tagging; + +/// +/// Flags that affect how the tagger infrastructure responds to text changes. +/// +[Flags] +internal enum TaggerTextChangeBehavior { /// - /// Flags that affect how the tagger infrastructure responds to text changes. + /// The async tagger infrastructure will not track any text changes and will not do + /// anything special in the presence of them. /// - [Flags] - internal enum TaggerTextChangeBehavior - { - /// - /// The async tagger infrastructure will not track any text changes and will not do - /// anything special in the presence of them. - /// - None = 0, + None = 0, - /// - /// The async tagger infrastructure will track text changes to the subject buffer it is - /// attached to. The text changes will be provided to the - /// that is passed to . - /// - TrackTextChanges = 1 << 0, + /// + /// The async tagger infrastructure will track text changes to the subject buffer it is + /// attached to. The text changes will be provided to the + /// that is passed to . + /// + TrackTextChanges = 1 << 0, - /// - /// The async tagger infrastructure will track text changes to the subject buffer it is - /// attached to. The text changes will be provided to the - /// that is passed to . - /// - /// On any edit, tags that intersect the text change range will immediately removed. - /// - RemoveTagsThatIntersectEdits = TrackTextChanges | (1 << 1), + /// + /// The async tagger infrastructure will track text changes to the subject buffer it is + /// attached to. The text changes will be provided to the + /// that is passed to . + /// + /// On any edit, tags that intersect the text change range will immediately removed. + /// + RemoveTagsThatIntersectEdits = TrackTextChanges | (1 << 1), - /// - /// The async tagger infrastructure will track text changes to the subject buffer it is - /// attached to. - /// - /// On any edit all tags will we be removed. - /// - RemoveAllTags = TrackTextChanges | (1 << 2), - } + /// + /// The async tagger infrastructure will track text changes to the subject buffer it is + /// attached to. + /// + /// On any edit all tags will we be removed. + /// + RemoveAllTags = TrackTextChanges | (1 << 2), } diff --git a/src/EditorFeatures/Core/Tagging/TaggerUtilities.cs b/src/EditorFeatures/Core/Tagging/TaggerUtilities.cs index 32e96f8d392cf..34083cfa241b1 100644 --- a/src/EditorFeatures/Core/Tagging/TaggerUtilities.cs +++ b/src/EditorFeatures/Core/Tagging/TaggerUtilities.cs @@ -4,21 +4,20 @@ using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Tagging +namespace Microsoft.CodeAnalysis.Tagging; + +internal static class TaggerUtilities { - internal static class TaggerUtilities + public static bool SpanEquals(SnapshotSpan? span1, SnapshotSpan? span2, SpanTrackingMode spanTrackingMode) { - public static bool SpanEquals(SnapshotSpan? span1, SnapshotSpan? span2, SpanTrackingMode spanTrackingMode) - { - if (span1 is null && span2 is null) - return true; + if (span1 is null && span2 is null) + return true; - if (span1 is null || span2 is null) - return false; + if (span1 is null || span2 is null) + return false; - // map one span to the snapshot of the other and see if they match. - span1 = span1.Value.TranslateTo(span2.Value.Snapshot, spanTrackingMode); - return span1.Value.Span == span2.Value.Span; - } + // map one span to the snapshot of the other and see if they match. + span1 = span1.Value.TranslateTo(span2.Value.Snapshot, spanTrackingMode); + return span1.Value.Span == span2.Value.Span; } } diff --git a/src/EditorFeatures/Core/Tags/ExportImageIdServiceAttribute.cs b/src/EditorFeatures/Core/Tags/ExportImageIdServiceAttribute.cs index 391dde502f6fe..93deecc81cd5f 100644 --- a/src/EditorFeatures/Core/Tags/ExportImageIdServiceAttribute.cs +++ b/src/EditorFeatures/Core/Tags/ExportImageIdServiceAttribute.cs @@ -7,24 +7,23 @@ using System; using System.ComponentModel.Composition; -namespace Microsoft.CodeAnalysis.Editor.Tags +namespace Microsoft.CodeAnalysis.Editor.Tags; + +/// +/// Use this attribute to declare an implementation +/// so that it can be discovered by the host. +/// +[MetadataAttribute] +[AttributeUsage(AttributeTargets.Class)] +internal sealed class ExportImageIdServiceAttribute : ExportAttribute { /// - /// Use this attribute to declare an implementation - /// so that it can be discovered by the host. + /// The name of the . /// - [MetadataAttribute] - [AttributeUsage(AttributeTargets.Class)] - internal sealed class ExportImageIdServiceAttribute : ExportAttribute - { - /// - /// The name of the . - /// - public string Name { get; set; } + public string Name { get; set; } - public ExportImageIdServiceAttribute() - : base(typeof(IImageIdService)) - { - } + public ExportImageIdServiceAttribute() + : base(typeof(IImageIdService)) + { } } diff --git a/src/EditorFeatures/Core/Tags/IImageIdService.cs b/src/EditorFeatures/Core/Tags/IImageIdService.cs index 191ff3f4eda49..a584807d5a355 100644 --- a/src/EditorFeatures/Core/Tags/IImageIdService.cs +++ b/src/EditorFeatures/Core/Tags/IImageIdService.cs @@ -7,13 +7,12 @@ using System.Collections.Immutable; using Microsoft.VisualStudio.Core.Imaging; -namespace Microsoft.CodeAnalysis.Editor.Tags +namespace Microsoft.CodeAnalysis.Editor.Tags; + +/// +/// Extensibility point for hosts to display s for items with Tags. +/// +internal interface IImageIdService { - /// - /// Extensibility point for hosts to display s for items with Tags. - /// - internal interface IImageIdService - { - bool TryGetImageId(ImmutableArray tags, out ImageId imageId); - } + bool TryGetImageId(ImmutableArray tags, out ImageId imageId); } diff --git a/src/EditorFeatures/Core/TaskList/ITaskListProvider.cs b/src/EditorFeatures/Core/TaskList/ITaskListProvider.cs index 9a81f30391648..b512b9ab90ca3 100644 --- a/src/EditorFeatures/Core/TaskList/ITaskListProvider.cs +++ b/src/EditorFeatures/Core/TaskList/ITaskListProvider.cs @@ -7,21 +7,20 @@ using System.Threading; using Microsoft.CodeAnalysis.TaskList; -namespace Microsoft.CodeAnalysis.Editor.TaskList +namespace Microsoft.CodeAnalysis.Editor.TaskList; + +/// +/// Returns Roslyn todo list from the workspace. +/// +internal interface ITaskListProvider { /// - /// Returns Roslyn todo list from the workspace. + /// An event that is raised when the todo list has changed. + /// + /// When an event handler is newly added, this event will fire for the currently available todo items and then + /// afterward for any changes since. /// - internal interface ITaskListProvider - { - /// - /// An event that is raised when the todo list has changed. - /// - /// When an event handler is newly added, this event will fire for the currently available todo items and then - /// afterward for any changes since. - /// - event EventHandler TaskListUpdated; + event EventHandler TaskListUpdated; - ImmutableArray GetTaskListItems(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken); - } + ImmutableArray GetTaskListItems(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/TaskList/TaskListUpdatedArgs.cs b/src/EditorFeatures/Core/TaskList/TaskListUpdatedArgs.cs index a9bc2c5f6dda7..bd462abc338f7 100644 --- a/src/EditorFeatures/Core/TaskList/TaskListUpdatedArgs.cs +++ b/src/EditorFeatures/Core/TaskList/TaskListUpdatedArgs.cs @@ -6,24 +6,23 @@ using Microsoft.CodeAnalysis.Common; using Microsoft.CodeAnalysis.TaskList; -namespace Microsoft.CodeAnalysis.Editor.TaskList +namespace Microsoft.CodeAnalysis.Editor.TaskList; + +internal sealed class TaskListUpdatedArgs( + object id, Solution solution, DocumentId documentId, ImmutableArray items) : UpdatedEventArgs(id, solution.Workspace, documentId.ProjectId, documentId) { - internal sealed class TaskListUpdatedArgs( - object id, Solution solution, DocumentId documentId, ImmutableArray items) : UpdatedEventArgs(id, solution.Workspace, documentId.ProjectId, documentId) - { - /// - /// Solution this task items are associated with - /// - public Solution Solution { get; } = solution; + /// + /// Solution this task items are associated with + /// + public Solution Solution { get; } = solution; - /// - /// The task items associated with the ID. - /// - public ImmutableArray TaskListItems { get; } = items; + /// + /// The task items associated with the ID. + /// + public ImmutableArray TaskListItems { get; } = items; - /// - /// this update is associated with. - /// - public new DocumentId DocumentId => base.DocumentId!; - } + /// + /// this update is associated with. + /// + public new DocumentId DocumentId => base.DocumentId!; } diff --git a/src/EditorFeatures/Core/TextDiffing/EditorTextDifferencingService.cs b/src/EditorFeatures/Core/TextDiffing/EditorTextDifferencingService.cs index 9a6a6e4561338..b1338dda397b7 100644 --- a/src/EditorFeatures/Core/TextDiffing/EditorTextDifferencingService.cs +++ b/src/EditorFeatures/Core/TextDiffing/EditorTextDifferencingService.cs @@ -17,65 +17,64 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Differencing; -namespace Microsoft.CodeAnalysis.Editor.Implementation.TextDiffing +namespace Microsoft.CodeAnalysis.Editor.Implementation.TextDiffing; + +[ExportWorkspaceService(typeof(IDocumentTextDifferencingService), ServiceLayer.Host), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class EditorTextDifferencingService(ITextDifferencingSelectorService differenceSelectorService) : IDocumentTextDifferencingService { - [ExportWorkspaceService(typeof(IDocumentTextDifferencingService), ServiceLayer.Host), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class EditorTextDifferencingService(ITextDifferencingSelectorService differenceSelectorService) : IDocumentTextDifferencingService + private readonly ITextDifferencingSelectorService _differenceSelectorService = differenceSelectorService; + + public Task> GetTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken) + => GetTextChangesAsync(oldDocument, newDocument, TextDifferenceTypes.Word, cancellationToken); + + public async Task> GetTextChangesAsync(Document oldDocument, Document newDocument, TextDifferenceTypes preferredDifferenceType, CancellationToken cancellationToken) { - private readonly ITextDifferencingSelectorService _differenceSelectorService = differenceSelectorService; + var oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - public Task> GetTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken) - => GetTextChangesAsync(oldDocument, newDocument, TextDifferenceTypes.Word, cancellationToken); + var diffService = _differenceSelectorService.GetTextDifferencingService(oldDocument.Project.Services.GetService().GetDefaultContentType()) + ?? _differenceSelectorService.DefaultTextDifferencingService; - public async Task> GetTextChangesAsync(Document oldDocument, Document newDocument, TextDifferenceTypes preferredDifferenceType, CancellationToken cancellationToken) - { - var oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var differenceOptions = GetDifferenceOptions(preferredDifferenceType); + + var oldTextSnapshot = oldText.FindCorrespondingEditorTextSnapshot(); + var newTextSnapshot = newText.FindCorrespondingEditorTextSnapshot(); + var useSnapshots = oldTextSnapshot != null && newTextSnapshot != null; - var diffService = _differenceSelectorService.GetTextDifferencingService(oldDocument.Project.Services.GetService().GetDefaultContentType()) - ?? _differenceSelectorService.DefaultTextDifferencingService; + var diffResult = useSnapshots + ? diffService.DiffSnapshotSpans(oldTextSnapshot.GetFullSpan(), newTextSnapshot.GetFullSpan(), differenceOptions) + : diffService.DiffStrings(oldText.ToString(), newText.ToString(), differenceOptions); - var differenceOptions = GetDifferenceOptions(preferredDifferenceType); + return diffResult.Differences.Select(d => + new TextChange( + diffResult.LeftDecomposition.GetSpanInOriginal(d.Left).ToTextSpan(), + newText.GetSubText(diffResult.RightDecomposition.GetSpanInOriginal(d.Right).ToTextSpan()).ToString())).ToImmutableArray(); + } - var oldTextSnapshot = oldText.FindCorrespondingEditorTextSnapshot(); - var newTextSnapshot = newText.FindCorrespondingEditorTextSnapshot(); - var useSnapshots = oldTextSnapshot != null && newTextSnapshot != null; + private static StringDifferenceOptions GetDifferenceOptions(TextDifferenceTypes differenceTypes) + { + StringDifferenceTypes stringDifferenceTypes = default; - var diffResult = useSnapshots - ? diffService.DiffSnapshotSpans(oldTextSnapshot.GetFullSpan(), newTextSnapshot.GetFullSpan(), differenceOptions) - : diffService.DiffStrings(oldText.ToString(), newText.ToString(), differenceOptions); + if (differenceTypes.HasFlag(TextDifferenceTypes.Line)) + { + stringDifferenceTypes |= StringDifferenceTypes.Line; + } - return diffResult.Differences.Select(d => - new TextChange( - diffResult.LeftDecomposition.GetSpanInOriginal(d.Left).ToTextSpan(), - newText.GetSubText(diffResult.RightDecomposition.GetSpanInOriginal(d.Right).ToTextSpan()).ToString())).ToImmutableArray(); + if (differenceTypes.HasFlag(TextDifferenceTypes.Word)) + { + stringDifferenceTypes |= StringDifferenceTypes.Word; } - private static StringDifferenceOptions GetDifferenceOptions(TextDifferenceTypes differenceTypes) + if (differenceTypes.HasFlag(TextDifferenceTypes.Character)) { - StringDifferenceTypes stringDifferenceTypes = default; - - if (differenceTypes.HasFlag(TextDifferenceTypes.Line)) - { - stringDifferenceTypes |= StringDifferenceTypes.Line; - } - - if (differenceTypes.HasFlag(TextDifferenceTypes.Word)) - { - stringDifferenceTypes |= StringDifferenceTypes.Word; - } - - if (differenceTypes.HasFlag(TextDifferenceTypes.Character)) - { - stringDifferenceTypes |= StringDifferenceTypes.Character; - } - - return new StringDifferenceOptions() - { - DifferenceType = stringDifferenceTypes - }; + stringDifferenceTypes |= StringDifferenceTypes.Character; } + + return new StringDifferenceOptions() + { + DifferenceType = stringDifferenceTypes + }; } } diff --git a/src/EditorFeatures/Core/TextStructureNavigation/AbstractTextStructureNavigatorProvider.TextStructureNavigator.cs b/src/EditorFeatures/Core/TextStructureNavigation/AbstractTextStructureNavigatorProvider.TextStructureNavigator.cs index 66a4a3de22bd7..e17d767ce7001 100644 --- a/src/EditorFeatures/Core/TextStructureNavigation/AbstractTextStructureNavigatorProvider.TextStructureNavigator.cs +++ b/src/EditorFeatures/Core/TextStructureNavigation/AbstractTextStructureNavigatorProvider.TextStructureNavigator.cs @@ -14,357 +14,356 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.TextStructureNavigation +namespace Microsoft.CodeAnalysis.Editor.Implementation.TextStructureNavigation; + +internal partial class AbstractTextStructureNavigatorProvider { - internal partial class AbstractTextStructureNavigatorProvider + private class TextStructureNavigator : ITextStructureNavigator { - private class TextStructureNavigator : ITextStructureNavigator + private readonly ITextBuffer _subjectBuffer; + private readonly ITextStructureNavigator _naturalLanguageNavigator; + private readonly AbstractTextStructureNavigatorProvider _provider; + private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; + + internal TextStructureNavigator( + ITextBuffer subjectBuffer, + ITextStructureNavigator naturalLanguageNavigator, + AbstractTextStructureNavigatorProvider provider, + IUIThreadOperationExecutor uIThreadOperationExecutor) { - private readonly ITextBuffer _subjectBuffer; - private readonly ITextStructureNavigator _naturalLanguageNavigator; - private readonly AbstractTextStructureNavigatorProvider _provider; - private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; - - internal TextStructureNavigator( - ITextBuffer subjectBuffer, - ITextStructureNavigator naturalLanguageNavigator, - AbstractTextStructureNavigatorProvider provider, - IUIThreadOperationExecutor uIThreadOperationExecutor) - { - Contract.ThrowIfNull(subjectBuffer); - Contract.ThrowIfNull(naturalLanguageNavigator); - Contract.ThrowIfNull(provider); - - _subjectBuffer = subjectBuffer; - _naturalLanguageNavigator = naturalLanguageNavigator; - _provider = provider; - _uiThreadOperationExecutor = uIThreadOperationExecutor; - } + Contract.ThrowIfNull(subjectBuffer); + Contract.ThrowIfNull(naturalLanguageNavigator); + Contract.ThrowIfNull(provider); + + _subjectBuffer = subjectBuffer; + _naturalLanguageNavigator = naturalLanguageNavigator; + _provider = provider; + _uiThreadOperationExecutor = uIThreadOperationExecutor; + } - public IContentType ContentType => _subjectBuffer.ContentType; + public IContentType ContentType => _subjectBuffer.ContentType; - public TextExtent GetExtentOfWord(SnapshotPoint currentPosition) + public TextExtent GetExtentOfWord(SnapshotPoint currentPosition) + { + using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetExtentOfWord, CancellationToken.None)) { - using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetExtentOfWord, CancellationToken.None)) + var result = default(TextExtent); + _uiThreadOperationExecutor.Execute( + title: EditorFeaturesResources.Text_Navigation, + defaultDescription: EditorFeaturesResources.Finding_word_extent, + allowCancellation: true, + showProgress: false, + action: context => { - var result = default(TextExtent); - _uiThreadOperationExecutor.Execute( - title: EditorFeaturesResources.Text_Navigation, - defaultDescription: EditorFeaturesResources.Finding_word_extent, - allowCancellation: true, - showProgress: false, - action: context => - { - result = GetExtentOfWordWorker(currentPosition, context.UserCancellationToken); - }); + result = GetExtentOfWordWorker(currentPosition, context.UserCancellationToken); + }); - return result; - } + return result; } + } - private TextExtent GetExtentOfWordWorker(SnapshotPoint position, CancellationToken cancellationToken) + private TextExtent GetExtentOfWordWorker(SnapshotPoint position, CancellationToken cancellationToken) + { + var textLength = position.Snapshot.Length; + if (textLength == 0) { - var textLength = position.Snapshot.Length; - if (textLength == 0) - { - return _naturalLanguageNavigator.GetExtentOfWord(position); - } + return _naturalLanguageNavigator.GetExtentOfWord(position); + } - // If at the end of the file, go back one character so stuff works - if (position == textLength && position > 0) - { - position -= 1; - } + // If at the end of the file, go back one character so stuff works + if (position == textLength && position > 0) + { + position -= 1; + } - // If we're at the EOL position, return the line break's extent - var line = position.Snapshot.GetLineFromPosition(position); - if (position >= line.End && position < line.EndIncludingLineBreak) - { - return new TextExtent(new SnapshotSpan(line.End, line.EndIncludingLineBreak - line.End), isSignificant: false); - } + // If we're at the EOL position, return the line break's extent + var line = position.Snapshot.GetLineFromPosition(position); + if (position >= line.End && position < line.EndIncludingLineBreak) + { + return new TextExtent(new SnapshotSpan(line.End, line.EndIncludingLineBreak - line.End), isSignificant: false); + } - var document = GetDocument(position); - if (document != null) - { - var root = document.GetSyntaxRootSynchronously(cancellationToken); - var trivia = root.FindTrivia(position, findInsideTrivia: true); + var document = GetDocument(position); + if (document != null) + { + var root = document.GetSyntaxRootSynchronously(cancellationToken); + var trivia = root.FindTrivia(position, findInsideTrivia: true); - if (trivia != default) + if (trivia != default) + { + if (trivia.Span.Start == position && _provider.ShouldSelectEntireTriviaFromStart(trivia)) { - if (trivia.Span.Start == position && _provider.ShouldSelectEntireTriviaFromStart(trivia)) - { - // We want to select the entire comment - return new TextExtent(trivia.Span.ToSnapshotSpan(position.Snapshot), isSignificant: true); - } + // We want to select the entire comment + return new TextExtent(trivia.Span.ToSnapshotSpan(position.Snapshot), isSignificant: true); } + } - var token = root.FindToken(position, findInsideTrivia: true); - - // If end of file, go back a token - if (token.Span.Length == 0 && token.Span.Start == textLength) - { - token = token.GetPreviousToken(); - } + var token = root.FindToken(position, findInsideTrivia: true); - if (token.Span.Length > 0 && token.Span.Contains(position) && !_provider.IsWithinNaturalLanguage(token, position)) - { - // Cursor position is in our domain - handle it. - return _provider.GetExtentOfWordFromToken(token, position); - } + // If end of file, go back a token + if (token.Span.Length == 0 && token.Span.Start == textLength) + { + token = token.GetPreviousToken(); } - // Fall back to natural language navigator do its thing. - return _naturalLanguageNavigator.GetExtentOfWord(position); + if (token.Span.Length > 0 && token.Span.Contains(position) && !_provider.IsWithinNaturalLanguage(token, position)) + { + // Cursor position is in our domain - handle it. + return _provider.GetExtentOfWordFromToken(token, position); + } } - public SnapshotSpan GetSpanOfEnclosing(SnapshotSpan activeSpan) + // Fall back to natural language navigator do its thing. + return _naturalLanguageNavigator.GetExtentOfWord(position); + } + + public SnapshotSpan GetSpanOfEnclosing(SnapshotSpan activeSpan) + { + using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfEnclosing, CancellationToken.None)) { - using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfEnclosing, CancellationToken.None)) + var span = default(SnapshotSpan); + var result = _uiThreadOperationExecutor.Execute( + title: EditorFeaturesResources.Text_Navigation, + defaultDescription: EditorFeaturesResources.Finding_enclosing_span, + allowCancellation: true, + showProgress: false, + action: context => { - var span = default(SnapshotSpan); - var result = _uiThreadOperationExecutor.Execute( - title: EditorFeaturesResources.Text_Navigation, - defaultDescription: EditorFeaturesResources.Finding_enclosing_span, - allowCancellation: true, - showProgress: false, - action: context => - { - span = GetSpanOfEnclosingWorker(activeSpan, context.UserCancellationToken); - }); + span = GetSpanOfEnclosingWorker(activeSpan, context.UserCancellationToken); + }); - return result == UIThreadOperationStatus.Completed ? span : activeSpan; - } + return result == UIThreadOperationStatus.Completed ? span : activeSpan; } + } - private static SnapshotSpan GetSpanOfEnclosingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) + private static SnapshotSpan GetSpanOfEnclosingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) + { + // Find node that covers the entire span. + var node = FindLeafNode(activeSpan, cancellationToken); + if (node != null && activeSpan.Length == node.Value.Span.Length) { - // Find node that covers the entire span. - var node = FindLeafNode(activeSpan, cancellationToken); - if (node != null && activeSpan.Length == node.Value.Span.Length) - { - // Go one level up so the span widens. - node = GetEnclosingNode(node.Value); - } - - return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot); + // Go one level up so the span widens. + node = GetEnclosingNode(node.Value); } - public SnapshotSpan GetSpanOfFirstChild(SnapshotSpan activeSpan) + return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot); + } + + public SnapshotSpan GetSpanOfFirstChild(SnapshotSpan activeSpan) + { + using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfFirstChild, CancellationToken.None)) { - using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfFirstChild, CancellationToken.None)) + var span = default(SnapshotSpan); + var result = _uiThreadOperationExecutor.Execute( + title: EditorFeaturesResources.Text_Navigation, + defaultDescription: EditorFeaturesResources.Finding_enclosing_span, + allowCancellation: true, + showProgress: false, + action: context => { - var span = default(SnapshotSpan); - var result = _uiThreadOperationExecutor.Execute( - title: EditorFeaturesResources.Text_Navigation, - defaultDescription: EditorFeaturesResources.Finding_enclosing_span, - allowCancellation: true, - showProgress: false, - action: context => - { - span = GetSpanOfFirstChildWorker(activeSpan, context.UserCancellationToken); - }); + span = GetSpanOfFirstChildWorker(activeSpan, context.UserCancellationToken); + }); - return result == UIThreadOperationStatus.Completed ? span : activeSpan; - } + return result == UIThreadOperationStatus.Completed ? span : activeSpan; } + } - private static SnapshotSpan GetSpanOfFirstChildWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) + private static SnapshotSpan GetSpanOfFirstChildWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) + { + // Find node that covers the entire span. + var node = FindLeafNode(activeSpan, cancellationToken); + if (node != null) { - // Find node that covers the entire span. - var node = FindLeafNode(activeSpan, cancellationToken); - if (node != null) + // Take first child if possible, otherwise default to node itself. + var firstChild = node.Value.ChildNodesAndTokens().FirstOrNull(); + if (firstChild.HasValue) { - // Take first child if possible, otherwise default to node itself. - var firstChild = node.Value.ChildNodesAndTokens().FirstOrNull(); - if (firstChild.HasValue) - { - node = firstChild.Value; - } + node = firstChild.Value; } - - return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot); } - public SnapshotSpan GetSpanOfNextSibling(SnapshotSpan activeSpan) + return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot); + } + + public SnapshotSpan GetSpanOfNextSibling(SnapshotSpan activeSpan) + { + using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfNextSibling, CancellationToken.None)) { - using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfNextSibling, CancellationToken.None)) + var span = default(SnapshotSpan); + var result = _uiThreadOperationExecutor.Execute( + title: EditorFeaturesResources.Text_Navigation, + defaultDescription: EditorFeaturesResources.Finding_span_of_next_sibling, + allowCancellation: true, + showProgress: false, + action: context => { - var span = default(SnapshotSpan); - var result = _uiThreadOperationExecutor.Execute( - title: EditorFeaturesResources.Text_Navigation, - defaultDescription: EditorFeaturesResources.Finding_span_of_next_sibling, - allowCancellation: true, - showProgress: false, - action: context => - { - span = GetSpanOfNextSiblingWorker(activeSpan, context.UserCancellationToken); - }); + span = GetSpanOfNextSiblingWorker(activeSpan, context.UserCancellationToken); + }); - return result == UIThreadOperationStatus.Completed ? span : activeSpan; - } + return result == UIThreadOperationStatus.Completed ? span : activeSpan; } + } - private static SnapshotSpan GetSpanOfNextSiblingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) + private static SnapshotSpan GetSpanOfNextSiblingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) + { + // Find node that covers the entire span. + var node = FindLeafNode(activeSpan, cancellationToken); + if (node != null) { - // Find node that covers the entire span. - var node = FindLeafNode(activeSpan, cancellationToken); - if (node != null) + // Get ancestor with a wider span. + var parent = GetEnclosingNode(node.Value); + if (parent != null) { - // Get ancestor with a wider span. - var parent = GetEnclosingNode(node.Value); - if (parent != null) + // Find node immediately after the current in the children collection. + var nodeOrToken = parent.Value + .ChildNodesAndTokens() + .SkipWhile(child => child != node) + .Skip(1) + .FirstOrNull(); + + if (nodeOrToken.HasValue) + { + node = nodeOrToken.Value; + } + else { - // Find node immediately after the current in the children collection. - var nodeOrToken = parent.Value - .ChildNodesAndTokens() - .SkipWhile(child => child != node) - .Skip(1) - .FirstOrNull(); - - if (nodeOrToken.HasValue) - { - node = nodeOrToken.Value; - } - else - { - // If this is the last node, move to the parent so that the user can continue - // navigation at the higher level. - node = parent.Value; - } + // If this is the last node, move to the parent so that the user can continue + // navigation at the higher level. + node = parent.Value; } } - - return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot); } - public SnapshotSpan GetSpanOfPreviousSibling(SnapshotSpan activeSpan) + return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot); + } + + public SnapshotSpan GetSpanOfPreviousSibling(SnapshotSpan activeSpan) + { + using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfPreviousSibling, CancellationToken.None)) { - using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfPreviousSibling, CancellationToken.None)) + var span = default(SnapshotSpan); + var result = _uiThreadOperationExecutor.Execute( + title: EditorFeaturesResources.Text_Navigation, + defaultDescription: EditorFeaturesResources.Finding_span_of_previous_sibling, + allowCancellation: true, + showProgress: false, + action: context => { - var span = default(SnapshotSpan); - var result = _uiThreadOperationExecutor.Execute( - title: EditorFeaturesResources.Text_Navigation, - defaultDescription: EditorFeaturesResources.Finding_span_of_previous_sibling, - allowCancellation: true, - showProgress: false, - action: context => - { - span = GetSpanOfPreviousSiblingWorker(activeSpan, context.UserCancellationToken); - }); + span = GetSpanOfPreviousSiblingWorker(activeSpan, context.UserCancellationToken); + }); - return result == UIThreadOperationStatus.Completed ? span : activeSpan; - } + return result == UIThreadOperationStatus.Completed ? span : activeSpan; } + } - private static SnapshotSpan GetSpanOfPreviousSiblingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) + private static SnapshotSpan GetSpanOfPreviousSiblingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken) + { + // Find node that covers the entire span. + var node = FindLeafNode(activeSpan, cancellationToken); + if (node != null) { - // Find node that covers the entire span. - var node = FindLeafNode(activeSpan, cancellationToken); - if (node != null) + // Get ancestor with a wider span. + var parent = GetEnclosingNode(node.Value); + if (parent != null) { - // Get ancestor with a wider span. - var parent = GetEnclosingNode(node.Value); - if (parent != null) + // Find node immediately before the current in the children collection. + var nodeOrToken = parent.Value + .ChildNodesAndTokens() + .Reverse() + .SkipWhile(child => child != node) + .Skip(1) + .FirstOrNull(); + + if (nodeOrToken.HasValue) { - // Find node immediately before the current in the children collection. - var nodeOrToken = parent.Value - .ChildNodesAndTokens() - .Reverse() - .SkipWhile(child => child != node) - .Skip(1) - .FirstOrNull(); - - if (nodeOrToken.HasValue) - { - node = nodeOrToken.Value; - } - else - { - // If this is the first node, move to the parent so that the user can continue - // navigation at the higher level. - node = parent.Value; - } + node = nodeOrToken.Value; + } + else + { + // If this is the first node, move to the parent so that the user can continue + // navigation at the higher level. + node = parent.Value; } } - - return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot); } - private static Document GetDocument(SnapshotPoint point) + return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot); + } + + private static Document GetDocument(SnapshotPoint point) + { + var textLength = point.Snapshot.Length; + if (textLength == 0) { - var textLength = point.Snapshot.Length; - if (textLength == 0) - { - return null; - } + return null; + } + + return point.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + } - return point.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + /// + /// Finds deepest node that covers given . + /// + private static SyntaxNodeOrToken? FindLeafNode(SnapshotSpan span, CancellationToken cancellationToken) + { + if (!TryFindLeafToken(span.Start, out var token, cancellationToken)) + { + return null; } - /// - /// Finds deepest node that covers given . - /// - private static SyntaxNodeOrToken? FindLeafNode(SnapshotSpan span, CancellationToken cancellationToken) + SyntaxNodeOrToken? node = token; + while (node != null && (span.End.Position > node.Value.Span.End)) { - if (!TryFindLeafToken(span.Start, out var token, cancellationToken)) - { - return null; - } + node = GetEnclosingNode(node.Value); + } - SyntaxNodeOrToken? node = token; - while (node != null && (span.End.Position > node.Value.Span.End)) - { - node = GetEnclosingNode(node.Value); - } + return node; + } - return node; + /// + /// Given position in a text buffer returns the leaf syntax node it belongs to. + /// + private static bool TryFindLeafToken(SnapshotPoint point, out SyntaxToken token, CancellationToken cancellationToken) + { + var syntaxTree = GetDocument(point).GetSyntaxTreeSynchronously(cancellationToken); + if (syntaxTree != null) + { + token = syntaxTree.GetRoot(cancellationToken).FindToken(point, true); + return true; } - /// - /// Given position in a text buffer returns the leaf syntax node it belongs to. - /// - private static bool TryFindLeafToken(SnapshotPoint point, out SyntaxToken token, CancellationToken cancellationToken) - { - var syntaxTree = GetDocument(point).GetSyntaxTreeSynchronously(cancellationToken); - if (syntaxTree != null) - { - token = syntaxTree.GetRoot(cancellationToken).FindToken(point, true); - return true; - } + token = default; + return false; + } - token = default; - return false; + /// + /// Returns first ancestor of the node which has a span wider than node's span. + /// If none exist, returns the last available ancestor. + /// + private static SyntaxNodeOrToken SkipSameSpanParents(SyntaxNodeOrToken node) + { + while (node.Parent != null && node.Parent.Span == node.Span) + { + node = node.Parent; } - /// - /// Returns first ancestor of the node which has a span wider than node's span. - /// If none exist, returns the last available ancestor. - /// - private static SyntaxNodeOrToken SkipSameSpanParents(SyntaxNodeOrToken node) - { - while (node.Parent != null && node.Parent.Span == node.Span) - { - node = node.Parent; - } + return node; + } - return node; + /// + /// Finds node enclosing current from navigation point of view (that is, some immediate ancestors + /// may be skipped during this process). + /// + private static SyntaxNodeOrToken? GetEnclosingNode(SyntaxNodeOrToken node) + { + var parent = SkipSameSpanParents(node).Parent; + if (parent != null) + { + return parent; } - - /// - /// Finds node enclosing current from navigation point of view (that is, some immediate ancestors - /// may be skipped during this process). - /// - private static SyntaxNodeOrToken? GetEnclosingNode(SyntaxNodeOrToken node) + else { - var parent = SkipSameSpanParents(node).Parent; - if (parent != null) - { - return parent; - } - else - { - return null; - } + return null; } } } diff --git a/src/EditorFeatures/Core/TextStructureNavigation/AbstractTextStructureNavigatorProvider.cs b/src/EditorFeatures/Core/TextStructureNavigation/AbstractTextStructureNavigatorProvider.cs index 64df1b0f87cf0..d8a18e85d28f3 100644 --- a/src/EditorFeatures/Core/TextStructureNavigation/AbstractTextStructureNavigatorProvider.cs +++ b/src/EditorFeatures/Core/TextStructureNavigation/AbstractTextStructureNavigatorProvider.cs @@ -10,44 +10,43 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.TextStructureNavigation +namespace Microsoft.CodeAnalysis.Editor.Implementation.TextStructureNavigation; + +internal abstract partial class AbstractTextStructureNavigatorProvider : ITextStructureNavigatorProvider { - internal abstract partial class AbstractTextStructureNavigatorProvider : ITextStructureNavigatorProvider + private readonly ITextStructureNavigatorSelectorService _selectorService; + private readonly IContentTypeRegistryService _contentTypeService; + private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; + + protected AbstractTextStructureNavigatorProvider( + ITextStructureNavigatorSelectorService selectorService, + IContentTypeRegistryService contentTypeService, + IUIThreadOperationExecutor uIThreadOperationExecutor) + { + Contract.ThrowIfNull(selectorService); + Contract.ThrowIfNull(contentTypeService); + + _selectorService = selectorService; + _contentTypeService = contentTypeService; + _uiThreadOperationExecutor = uIThreadOperationExecutor; + } + + protected abstract bool ShouldSelectEntireTriviaFromStart(SyntaxTrivia trivia); + protected abstract bool IsWithinNaturalLanguage(SyntaxToken token, int position); + + protected virtual TextExtent GetExtentOfWordFromToken(SyntaxToken token, SnapshotPoint position) + => new(token.Span.ToSnapshotSpan(position.Snapshot), isSignificant: true); + + public ITextStructureNavigator CreateTextStructureNavigator(ITextBuffer subjectBuffer) { - private readonly ITextStructureNavigatorSelectorService _selectorService; - private readonly IContentTypeRegistryService _contentTypeService; - private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; - - protected AbstractTextStructureNavigatorProvider( - ITextStructureNavigatorSelectorService selectorService, - IContentTypeRegistryService contentTypeService, - IUIThreadOperationExecutor uIThreadOperationExecutor) - { - Contract.ThrowIfNull(selectorService); - Contract.ThrowIfNull(contentTypeService); - - _selectorService = selectorService; - _contentTypeService = contentTypeService; - _uiThreadOperationExecutor = uIThreadOperationExecutor; - } - - protected abstract bool ShouldSelectEntireTriviaFromStart(SyntaxTrivia trivia); - protected abstract bool IsWithinNaturalLanguage(SyntaxToken token, int position); - - protected virtual TextExtent GetExtentOfWordFromToken(SyntaxToken token, SnapshotPoint position) - => new(token.Span.ToSnapshotSpan(position.Snapshot), isSignificant: true); - - public ITextStructureNavigator CreateTextStructureNavigator(ITextBuffer subjectBuffer) - { - var naturalLanguageNavigator = _selectorService.CreateTextStructureNavigator( - subjectBuffer, - _contentTypeService.GetContentType("any")); - - return new TextStructureNavigator( - subjectBuffer, - naturalLanguageNavigator, - this, - _uiThreadOperationExecutor); - } + var naturalLanguageNavigator = _selectorService.CreateTextStructureNavigator( + subjectBuffer, + _contentTypeService.GetContentType("any")); + + return new TextStructureNavigator( + subjectBuffer, + naturalLanguageNavigator, + this, + _uiThreadOperationExecutor); } } diff --git a/src/EditorFeatures/Core/TextViewRoles.cs b/src/EditorFeatures/Core/TextViewRoles.cs index 921dc12225387..8838f360c21ae 100644 --- a/src/EditorFeatures/Core/TextViewRoles.cs +++ b/src/EditorFeatures/Core/TextViewRoles.cs @@ -4,10 +4,9 @@ #nullable disable -namespace Microsoft.CodeAnalysis.Editor +namespace Microsoft.CodeAnalysis.Editor; + +internal static class TextViewRoles { - internal static class TextViewRoles - { - public const string PreviewRole = "WorkspacePreviewRole"; - } + public const string PreviewRole = "WorkspacePreviewRole"; } diff --git a/src/EditorFeatures/Core/Undo/DefaultSourceTextUndoService.cs b/src/EditorFeatures/Core/Undo/DefaultSourceTextUndoService.cs index 08ee35000c9df..52f8d9bd2bbcd 100644 --- a/src/EditorFeatures/Core/Undo/DefaultSourceTextUndoService.cs +++ b/src/EditorFeatures/Core/Undo/DefaultSourceTextUndoService.cs @@ -10,24 +10,23 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Undo +namespace Microsoft.CodeAnalysis.Editor.Undo; + +[ExportWorkspaceService(typeof(ISourceTextUndoService), ServiceLayer.Default), Shared] +internal sealed class DefaultSourceTextUndoService : ISourceTextUndoService { - [ExportWorkspaceService(typeof(ISourceTextUndoService), ServiceLayer.Default), Shared] - internal sealed class DefaultSourceTextUndoService : ISourceTextUndoService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DefaultSourceTextUndoService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public DefaultSourceTextUndoService() - { - } + } - public ISourceTextUndoTransaction RegisterUndoTransaction(SourceText sourceText, string description) - => null; + public ISourceTextUndoTransaction RegisterUndoTransaction(SourceText sourceText, string description) + => null; - public bool BeginUndoTransaction(ITextSnapshot snapshot) - => false; + public bool BeginUndoTransaction(ITextSnapshot snapshot) + => false; - public bool EndUndoTransaction(ISourceTextUndoTransaction transaction) - => false; - } + public bool EndUndoTransaction(ISourceTextUndoTransaction transaction) + => false; } diff --git a/src/EditorFeatures/Core/Undo/EditorSourceTextUndoService.cs b/src/EditorFeatures/Core/Undo/EditorSourceTextUndoService.cs index abb357357053e..d1d716ca6b7ab 100644 --- a/src/EditorFeatures/Core/Undo/EditorSourceTextUndoService.cs +++ b/src/EditorFeatures/Core/Undo/EditorSourceTextUndoService.cs @@ -13,80 +13,79 @@ using Microsoft.VisualStudio.Text; using System; -namespace Microsoft.CodeAnalysis.Editor.Undo +namespace Microsoft.CodeAnalysis.Editor.Undo; + +[ExportWorkspaceService(typeof(ISourceTextUndoService), ServiceLayer.Editor), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class EditorSourceTextUndoService(ITextUndoHistoryRegistry undoHistoryRegistry) : ISourceTextUndoService { - [ExportWorkspaceService(typeof(ISourceTextUndoService), ServiceLayer.Editor), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class EditorSourceTextUndoService(ITextUndoHistoryRegistry undoHistoryRegistry) : ISourceTextUndoService - { - private readonly Dictionary _transactions = []; + private readonly Dictionary _transactions = []; - private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry = undoHistoryRegistry; - public ISourceTextUndoTransaction RegisterUndoTransaction(SourceText sourceText, string description) + public ISourceTextUndoTransaction RegisterUndoTransaction(SourceText sourceText, string description) + { + if (sourceText != null && !string.IsNullOrWhiteSpace(description)) { - if (sourceText != null && !string.IsNullOrWhiteSpace(description)) - { - var transaction = new SourceTextUndoTransaction(this, sourceText, description); - _transactions.Add(sourceText, transaction); - return transaction; - } - - return null; + var transaction = new SourceTextUndoTransaction(this, sourceText, description); + _transactions.Add(sourceText, transaction); + return transaction; } - public bool BeginUndoTransaction(ITextSnapshot snapshot) + return null; + } + + public bool BeginUndoTransaction(ITextSnapshot snapshot) + { + var sourceText = snapshot?.AsText(); + if (sourceText != null) { - var sourceText = snapshot?.AsText(); - if (sourceText != null) + _transactions.TryGetValue(sourceText, out var transaction); + if (transaction != null) { - _transactions.TryGetValue(sourceText, out var transaction); - if (transaction != null) - { - return transaction.Begin(_undoHistoryRegistry?.GetHistory(snapshot.TextBuffer)); - } + return transaction.Begin(_undoHistoryRegistry?.GetHistory(snapshot.TextBuffer)); } + } - return false; + return false; + } + + public bool EndUndoTransaction(ISourceTextUndoTransaction transaction) + { + if (transaction != null && _transactions.ContainsKey(transaction.SourceText)) + { + _transactions.Remove(transaction.SourceText); + return true; } - public bool EndUndoTransaction(ISourceTextUndoTransaction transaction) + return false; + } + + private sealed class SourceTextUndoTransaction(ISourceTextUndoService service, SourceText sourceText, string description) : ISourceTextUndoTransaction + { + private readonly ISourceTextUndoService _service = service; + public SourceText SourceText { get; } = sourceText; + public string Description { get; } = description; + + private ITextUndoTransaction _transaction; + + internal bool Begin(ITextUndoHistory undoHistory) { - if (transaction != null && _transactions.ContainsKey(transaction.SourceText)) + if (undoHistory != null) { - _transactions.Remove(transaction.SourceText); + _transaction = new HACK_TextUndoTransactionThatRollsBackProperly(undoHistory.CreateTransaction(Description)); return true; } return false; } - private sealed class SourceTextUndoTransaction(ISourceTextUndoService service, SourceText sourceText, string description) : ISourceTextUndoTransaction + public void Dispose() { - private readonly ISourceTextUndoService _service = service; - public SourceText SourceText { get; } = sourceText; - public string Description { get; } = description; - - private ITextUndoTransaction _transaction; + _transaction?.Complete(); - internal bool Begin(ITextUndoHistory undoHistory) - { - if (undoHistory != null) - { - _transaction = new HACK_TextUndoTransactionThatRollsBackProperly(undoHistory.CreateTransaction(Description)); - return true; - } - - return false; - } - - public void Dispose() - { - _transaction?.Complete(); - - _service.EndUndoTransaction(this); - } + _service.EndUndoTransaction(this); } } } diff --git a/src/EditorFeatures/Core/Undo/Extensions.cs b/src/EditorFeatures/Core/Undo/Extensions.cs index 3311b2e2f8e0d..15c8195bd0be8 100644 --- a/src/EditorFeatures/Core/Undo/Extensions.cs +++ b/src/EditorFeatures/Core/Undo/Extensions.cs @@ -6,34 +6,33 @@ using System; -namespace Microsoft.CodeAnalysis.Editor.Undo +namespace Microsoft.CodeAnalysis.Editor.Undo; + +internal static class Extensions { - internal static class Extensions + /// + /// Create a global undo transaction for the given workspace. if the host doesn't support undo transaction, + /// useFallback flag can be used to indicate whether it should fallback to base implementation or not. + /// + public static IWorkspaceGlobalUndoTransaction OpenGlobalUndoTransaction(this Workspace workspace, string description, bool useFallback = true) { - /// - /// Create a global undo transaction for the given workspace. if the host doesn't support undo transaction, - /// useFallback flag can be used to indicate whether it should fallback to base implementation or not. - /// - public static IWorkspaceGlobalUndoTransaction OpenGlobalUndoTransaction(this Workspace workspace, string description, bool useFallback = true) - { - var undoService = workspace.Services.GetService(); + var undoService = workspace.Services.GetService(); - try + try + { + // try using global undo service from host + return undoService.OpenGlobalUndoTransaction(workspace, description); + } + catch (ArgumentException) + { + // it looks like it is not supported. + // check whether we should use fallback mechanism or not + if (useFallback) { - // try using global undo service from host - return undoService.OpenGlobalUndoTransaction(workspace, description); + return NoOpGlobalUndoServiceFactory.Transaction; } - catch (ArgumentException) - { - // it looks like it is not supported. - // check whether we should use fallback mechanism or not - if (useFallback) - { - return NoOpGlobalUndoServiceFactory.Transaction; - } - throw; - } + throw; } } } diff --git a/src/EditorFeatures/Core/Undo/IGlobalUndoService.cs b/src/EditorFeatures/Core/Undo/IGlobalUndoService.cs index df86aa874188b..d9ac0af07f873 100644 --- a/src/EditorFeatures/Core/Undo/IGlobalUndoService.cs +++ b/src/EditorFeatures/Core/Undo/IGlobalUndoService.cs @@ -6,26 +6,25 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Editor.Undo +namespace Microsoft.CodeAnalysis.Editor.Undo; + +/// +/// This provides a way to do global undo. but semantic of the global undo is defined by the workspace host. +/// +internal interface IGlobalUndoService : IWorkspaceService { /// - /// This provides a way to do global undo. but semantic of the global undo is defined by the workspace host. + /// Queries whether a global transaction is currently active. /// - internal interface IGlobalUndoService : IWorkspaceService - { - /// - /// Queries whether a global transaction is currently active. - /// - bool IsGlobalTransactionOpen(Workspace workspace); + bool IsGlobalTransactionOpen(Workspace workspace); - /// - /// query method that can answer whether global undo is supported by the workspace - /// - bool CanUndo(Workspace workspace); + /// + /// query method that can answer whether global undo is supported by the workspace + /// + bool CanUndo(Workspace workspace); - /// - /// open global undo transaction for the workspace - /// - IWorkspaceGlobalUndoTransaction OpenGlobalUndoTransaction(Workspace workspace, string description); - } + /// + /// open global undo transaction for the workspace + /// + IWorkspaceGlobalUndoTransaction OpenGlobalUndoTransaction(Workspace workspace, string description); } diff --git a/src/EditorFeatures/Core/Undo/ISourceTextUndoService.cs b/src/EditorFeatures/Core/Undo/ISourceTextUndoService.cs index 494c985277466..9a399e8fdfb6d 100644 --- a/src/EditorFeatures/Core/Undo/ISourceTextUndoService.cs +++ b/src/EditorFeatures/Core/Undo/ISourceTextUndoService.cs @@ -8,36 +8,35 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; -namespace Microsoft.CodeAnalysis.Editor.Undo +namespace Microsoft.CodeAnalysis.Editor.Undo; + +/// +/// A service that allows consumers to register undo transactions for a supplied +/// with a supplied description. The description is the +/// display string by which the IDE's undo stack UI will subsequently refer to the transaction. +/// +internal interface ISourceTextUndoService : IWorkspaceService { /// - /// A service that allows consumers to register undo transactions for a supplied - /// with a supplied description. The description is the - /// display string by which the IDE's undo stack UI will subsequently refer to the transaction. + /// Registers undo transaction for the supplied . /// - internal interface ISourceTextUndoService : IWorkspaceService - { - /// - /// Registers undo transaction for the supplied . - /// - /// The for which undo transaction is being registered. - /// The display string by which the IDE's undo stack UI will subsequently refer to the transaction. - ISourceTextUndoTransaction RegisterUndoTransaction(SourceText sourceText, string description); + /// The for which undo transaction is being registered. + /// The display string by which the IDE's undo stack UI will subsequently refer to the transaction. + ISourceTextUndoTransaction RegisterUndoTransaction(SourceText sourceText, string description); - /// - /// Starts previously registered undo transaction for the supplied (if any). - /// - /// The for the for undo transaction being started. - /// - /// This method will handle the translation from to - /// and update the IDE's undo stack UI with the transaction's previously registered description string. - /// - bool BeginUndoTransaction(ITextSnapshot snapshot); + /// + /// Starts previously registered undo transaction for the supplied (if any). + /// + /// The for the for undo transaction being started. + /// + /// This method will handle the translation from to + /// and update the IDE's undo stack UI with the transaction's previously registered description string. + /// + bool BeginUndoTransaction(ITextSnapshot snapshot); - /// - /// Completes and deletes the supplied undo transaction. - /// - /// The undo transaction that is being ended. - bool EndUndoTransaction(ISourceTextUndoTransaction transaction); - } + /// + /// Completes and deletes the supplied undo transaction. + /// + /// The undo transaction that is being ended. + bool EndUndoTransaction(ISourceTextUndoTransaction transaction); } diff --git a/src/EditorFeatures/Core/Undo/ISourceTextUndoTransaction.cs b/src/EditorFeatures/Core/Undo/ISourceTextUndoTransaction.cs index 9a589f89e52d6..2a853e5b4d3c5 100644 --- a/src/EditorFeatures/Core/Undo/ISourceTextUndoTransaction.cs +++ b/src/EditorFeatures/Core/Undo/ISourceTextUndoTransaction.cs @@ -7,22 +7,21 @@ using System; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Editor.Undo +namespace Microsoft.CodeAnalysis.Editor.Undo; + +/// +/// Represents undo transaction for a +/// with a display string by which the IDE's undo stack UI refers to the transaction. +/// +internal interface ISourceTextUndoTransaction : IDisposable { /// - /// Represents undo transaction for a - /// with a display string by which the IDE's undo stack UI refers to the transaction. + /// The for this undo transaction. /// - internal interface ISourceTextUndoTransaction : IDisposable - { - /// - /// The for this undo transaction. - /// - SourceText SourceText { get; } + SourceText SourceText { get; } - /// - /// The display string by which the IDE's undo stack UI refers to the transaction. - /// - string Description { get; } - } + /// + /// The display string by which the IDE's undo stack UI refers to the transaction. + /// + string Description { get; } } diff --git a/src/EditorFeatures/Core/Undo/IWorkspaceGlobalUndoTransaction.cs b/src/EditorFeatures/Core/Undo/IWorkspaceGlobalUndoTransaction.cs index 25d7f09139593..930ebc1283781 100644 --- a/src/EditorFeatures/Core/Undo/IWorkspaceGlobalUndoTransaction.cs +++ b/src/EditorFeatures/Core/Undo/IWorkspaceGlobalUndoTransaction.cs @@ -6,21 +6,20 @@ using System; -namespace Microsoft.CodeAnalysis.Editor.Undo +namespace Microsoft.CodeAnalysis.Editor.Undo; + +/// +/// This represents workspace global undo transaction +/// +internal interface IWorkspaceGlobalUndoTransaction : IDisposable { /// - /// This represents workspace global undo transaction + /// explicitly add a document to the global undo transaction /// - internal interface IWorkspaceGlobalUndoTransaction : IDisposable - { - /// - /// explicitly add a document to the global undo transaction - /// - void AddDocument(DocumentId id); + void AddDocument(DocumentId id); - /// - /// finish the undo transaction - /// - void Commit(); - } + /// + /// finish the undo transaction + /// + void Commit(); } diff --git a/src/EditorFeatures/Core/Undo/NoOpGlobalUndoServiceFactory.cs b/src/EditorFeatures/Core/Undo/NoOpGlobalUndoServiceFactory.cs index bfe272830d049..42007f1d6886c 100644 --- a/src/EditorFeatures/Core/Undo/NoOpGlobalUndoServiceFactory.cs +++ b/src/EditorFeatures/Core/Undo/NoOpGlobalUndoServiceFactory.cs @@ -9,61 +9,60 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.Editor.Undo +namespace Microsoft.CodeAnalysis.Editor.Undo; + +/// +/// This factory will create a service that provides workspace global undo service. +/// +[ExportWorkspaceServiceFactory(typeof(IGlobalUndoService), ServiceLayer.Default), Shared] +internal class NoOpGlobalUndoServiceFactory : IWorkspaceServiceFactory { - /// - /// This factory will create a service that provides workspace global undo service. - /// - [ExportWorkspaceServiceFactory(typeof(IGlobalUndoService), ServiceLayer.Default), Shared] - internal class NoOpGlobalUndoServiceFactory : IWorkspaceServiceFactory + public static readonly IWorkspaceGlobalUndoTransaction Transaction = new NoOpUndoTransaction(); + + private readonly NoOpGlobalUndoService _singleton = new(); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public NoOpGlobalUndoServiceFactory() { - public static readonly IWorkspaceGlobalUndoTransaction Transaction = new NoOpUndoTransaction(); + } - private readonly NoOpGlobalUndoService _singleton = new(); + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => _singleton; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public NoOpGlobalUndoServiceFactory() + private class NoOpGlobalUndoService : IGlobalUndoService + { + public bool IsGlobalTransactionOpen(Workspace workspace) { + // TODO: this is technically wrong -- Transaction shouldn't be a singleton. + return false; } - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => _singleton; - - private class NoOpGlobalUndoService : IGlobalUndoService + public bool CanUndo(Workspace workspace) { - public bool IsGlobalTransactionOpen(Workspace workspace) - { - // TODO: this is technically wrong -- Transaction shouldn't be a singleton. - return false; - } + // by default, undo is not supported + return false; + } - public bool CanUndo(Workspace workspace) - { - // by default, undo is not supported - return false; - } + public IWorkspaceGlobalUndoTransaction OpenGlobalUndoTransaction(Workspace workspace, string description) + => Transaction; + } - public IWorkspaceGlobalUndoTransaction OpenGlobalUndoTransaction(Workspace workspace, string description) - => Transaction; + /// + /// null object that doesn't do anything + /// + private class NoOpUndoTransaction : IWorkspaceGlobalUndoTransaction + { + public void Commit() + { } - /// - /// null object that doesn't do anything - /// - private class NoOpUndoTransaction : IWorkspaceGlobalUndoTransaction + public void Dispose() { - public void Commit() - { - } - - public void Dispose() - { - } + } - public void AddDocument(DocumentId id) - { - } + public void AddDocument(DocumentId id) + { } } } diff --git a/src/EditorFeatures/Core/Workspaces/AbstractTextBufferVisibilityTracker.cs b/src/EditorFeatures/Core/Workspaces/AbstractTextBufferVisibilityTracker.cs index 3da911b7c53a9..31e0d68b529db 100644 --- a/src/EditorFeatures/Core/Workspaces/AbstractTextBufferVisibilityTracker.cs +++ b/src/EditorFeatures/Core/Workspaces/AbstractTextBufferVisibilityTracker.cs @@ -14,209 +14,208 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Workspaces +namespace Microsoft.CodeAnalysis.Workspaces; + +internal abstract class AbstractTextBufferVisibilityTracker< + TTextView, + TVisibilityChangedCallback> : ITextBufferVisibilityTracker + where TTextView : ITextView + where TVisibilityChangedCallback : System.Delegate { - internal abstract class AbstractTextBufferVisibilityTracker< - TTextView, - TVisibilityChangedCallback> : ITextBufferVisibilityTracker - where TTextView : ITextView - where TVisibilityChangedCallback : System.Delegate + private readonly ITextBufferAssociatedViewService _associatedViewService; + private readonly IThreadingContext _threadingContext; + + private readonly Dictionary _subjectBufferToCallbacks = []; + + protected AbstractTextBufferVisibilityTracker( + ITextBufferAssociatedViewService associatedViewService, + IThreadingContext threadingContext) { - private readonly ITextBufferAssociatedViewService _associatedViewService; - private readonly IThreadingContext _threadingContext; + _associatedViewService = associatedViewService; + _threadingContext = threadingContext; - private readonly Dictionary _subjectBufferToCallbacks = []; + associatedViewService.SubjectBuffersConnected += AssociatedViewService_SubjectBuffersConnected; + associatedViewService.SubjectBuffersDisconnected += AssociatedViewService_SubjectBuffersDisconnected; + } - protected AbstractTextBufferVisibilityTracker( - ITextBufferAssociatedViewService associatedViewService, - IThreadingContext threadingContext) - { - _associatedViewService = associatedViewService; - _threadingContext = threadingContext; + protected abstract bool IsVisible(TTextView view); + protected abstract TVisibilityChangedCallback GetVisiblityChangeCallback(VisibleTrackerData visibleTrackerData); + protected abstract void AddVisibilityChangedCallback(TTextView view, TVisibilityChangedCallback visibilityChangedCallback); + protected abstract void RemoveVisibilityChangedCallback(TTextView view, TVisibilityChangedCallback visibilityChangedCallback); - associatedViewService.SubjectBuffersConnected += AssociatedViewService_SubjectBuffersConnected; - associatedViewService.SubjectBuffersDisconnected += AssociatedViewService_SubjectBuffersDisconnected; - } + private void AssociatedViewService_SubjectBuffersConnected(object? sender, SubjectBuffersConnectedEventArgs e) + { + _threadingContext.ThrowIfNotOnUIThread(); + UpdateAllAssociatedViews(e.SubjectBuffers); + } - protected abstract bool IsVisible(TTextView view); - protected abstract TVisibilityChangedCallback GetVisiblityChangeCallback(VisibleTrackerData visibleTrackerData); - protected abstract void AddVisibilityChangedCallback(TTextView view, TVisibilityChangedCallback visibilityChangedCallback); - protected abstract void RemoveVisibilityChangedCallback(TTextView view, TVisibilityChangedCallback visibilityChangedCallback); + private void AssociatedViewService_SubjectBuffersDisconnected(object? sender, SubjectBuffersConnectedEventArgs e) + { + _threadingContext.ThrowIfNotOnUIThread(); + UpdateAllAssociatedViews(e.SubjectBuffers); + } - private void AssociatedViewService_SubjectBuffersConnected(object? sender, SubjectBuffersConnectedEventArgs e) + private void UpdateAllAssociatedViews(ReadOnlyCollection subjectBuffers) + { + // Whenever views get attached/detached from buffers, make sure we're hooked up to the appropriate events + // for them. + foreach (var buffer in subjectBuffers) { - _threadingContext.ThrowIfNotOnUIThread(); - UpdateAllAssociatedViews(e.SubjectBuffers); + if (_subjectBufferToCallbacks.TryGetValue(buffer, out var data)) + data.UpdateAssociatedViews(); } + } - private void AssociatedViewService_SubjectBuffersDisconnected(object? sender, SubjectBuffersConnectedEventArgs e) - { - _threadingContext.ThrowIfNotOnUIThread(); - UpdateAllAssociatedViews(e.SubjectBuffers); - } + public bool IsVisible(ITextBuffer subjectBuffer) + { + _threadingContext.ThrowIfNotOnUIThread(); - private void UpdateAllAssociatedViews(ReadOnlyCollection subjectBuffers) - { - // Whenever views get attached/detached from buffers, make sure we're hooked up to the appropriate events - // for them. - foreach (var buffer in subjectBuffers) - { - if (_subjectBufferToCallbacks.TryGetValue(buffer, out var data)) - data.UpdateAssociatedViews(); - } - } + var views = _associatedViewService.GetAssociatedTextViews(subjectBuffer).ToImmutableArrayOrEmpty(); + + // If we don't have any views at all, then assume the buffer is visible. + if (views.Length == 0) + return true; - public bool IsVisible(ITextBuffer subjectBuffer) + // if any of the views were *not* the right kind of text views, assume the buffer is visible. We don't know + // how to determine the visibility of this buffer. While unlikely to happen, this is possible with VS's + // extensibility model, which allows for a plugin to host an ITextBuffer in their own impl of an ITextView. + // For those cases, just assume these buffers are visible. + if (views.Any(static v => v is not TTextView)) + return true; + + return views.OfType().Any(v => IsVisible(v)); + } + + public void RegisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback) + { + _threadingContext.ThrowIfNotOnUIThread(); + if (!_subjectBufferToCallbacks.TryGetValue(subjectBuffer, out var data)) { - _threadingContext.ThrowIfNotOnUIThread(); + data = new VisibleTrackerData(this, subjectBuffer); + _subjectBufferToCallbacks.Add(subjectBuffer, data); + } - var views = _associatedViewService.GetAssociatedTextViews(subjectBuffer).ToImmutableArrayOrEmpty(); + data.AddCallback(callback); + } - // If we don't have any views at all, then assume the buffer is visible. - if (views.Length == 0) - return true; + public void UnregisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback) + { + _threadingContext.ThrowIfNotOnUIThread(); - // if any of the views were *not* the right kind of text views, assume the buffer is visible. We don't know - // how to determine the visibility of this buffer. While unlikely to happen, this is possible with VS's - // extensibility model, which allows for a plugin to host an ITextBuffer in their own impl of an ITextView. - // For those cases, just assume these buffers are visible. - if (views.Any(static v => v is not TTextView)) - return true; + // Both of these methods must succeed. Otherwise we're somehow unregistering something we don't know about. + Contract.ThrowIfFalse(_subjectBufferToCallbacks.TryGetValue(subjectBuffer, out var data)); + Contract.ThrowIfFalse(data.Callbacks.Contains(callback)); + data.RemoveCallback(callback); - return views.OfType().Any(v => IsVisible(v)); + // If we have nothing that wants to listen to information about this buffer anymore, then disconnect it + // from all events and remove our map. + if (data.Callbacks.Count == 0) + { + data.Dispose(); + _subjectBufferToCallbacks.Remove(subjectBuffer); } + } - public void RegisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback) - { - _threadingContext.ThrowIfNotOnUIThread(); - if (!_subjectBufferToCallbacks.TryGetValue(subjectBuffer, out var data)) - { - data = new VisibleTrackerData(this, subjectBuffer); - _subjectBufferToCallbacks.Add(subjectBuffer, data); - } + public TestAccessor GetTestAccessor() + => new(this); - data.AddCallback(callback); - } + public readonly struct TestAccessor(AbstractTextBufferVisibilityTracker visibilityTracker) + { + private readonly AbstractTextBufferVisibilityTracker _visibilityTracker = visibilityTracker; - public void UnregisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback) + public void TriggerCallbacks(ITextBuffer subjectBuffer) { - _threadingContext.ThrowIfNotOnUIThread(); + var data = _visibilityTracker._subjectBufferToCallbacks[subjectBuffer]; + data.TriggerCallbacks(); + } + } - // Both of these methods must succeed. Otherwise we're somehow unregistering something we don't know about. - Contract.ThrowIfFalse(_subjectBufferToCallbacks.TryGetValue(subjectBuffer, out var data)); - Contract.ThrowIfFalse(data.Callbacks.Contains(callback)); - data.RemoveCallback(callback); + protected sealed class VisibleTrackerData : IDisposable + { + public readonly HashSet TextViews = []; - // If we have nothing that wants to listen to information about this buffer anymore, then disconnect it - // from all events and remove our map. - if (data.Callbacks.Count == 0) - { - data.Dispose(); - _subjectBufferToCallbacks.Remove(subjectBuffer); - } - } + private readonly AbstractTextBufferVisibilityTracker _tracker; + private readonly ITextBuffer _subjectBuffer; + private readonly TVisibilityChangedCallback _visibilityChangedCallback; - public TestAccessor GetTestAccessor() - => new(this); + /// + /// The callbacks that want to be notified when our change visibility. Stored as an + /// so we can enumerate it safely without it changing underneath us. + /// + public ImmutableHashSet Callbacks { get; private set; } = []; - public readonly struct TestAccessor(AbstractTextBufferVisibilityTracker visibilityTracker) + public VisibleTrackerData( + AbstractTextBufferVisibilityTracker tracker, + ITextBuffer subjectBuffer) { - private readonly AbstractTextBufferVisibilityTracker _visibilityTracker = visibilityTracker; + _tracker = tracker; + _subjectBuffer = subjectBuffer; + _visibilityChangedCallback = tracker.GetVisiblityChangeCallback(this); - public void TriggerCallbacks(ITextBuffer subjectBuffer) - { - var data = _visibilityTracker._subjectBufferToCallbacks[subjectBuffer]; - data.TriggerCallbacks(); - } + UpdateAssociatedViews(); } - protected sealed class VisibleTrackerData : IDisposable + public void Dispose() { - public readonly HashSet TextViews = []; + _tracker._threadingContext.ThrowIfNotOnUIThread(); - private readonly AbstractTextBufferVisibilityTracker _tracker; - private readonly ITextBuffer _subjectBuffer; - private readonly TVisibilityChangedCallback _visibilityChangedCallback; + // Shouldn't be disposing of this if we still have clients that want to hear about visibility changes. + Contract.ThrowIfTrue(Callbacks.Count > 0); - /// - /// The callbacks that want to be notified when our change visibility. Stored as an - /// so we can enumerate it safely without it changing underneath us. - /// - public ImmutableHashSet Callbacks { get; private set; } = []; - - public VisibleTrackerData( - AbstractTextBufferVisibilityTracker tracker, - ITextBuffer subjectBuffer) - { - _tracker = tracker; - _subjectBuffer = subjectBuffer; - _visibilityChangedCallback = tracker.GetVisiblityChangeCallback(this); + // Clear out all our textviews. This will disconnect us from any events we have registered with them. + UpdateTextViews([]); - UpdateAssociatedViews(); - } + Contract.ThrowIfTrue(TextViews.Count > 0); + } - public void Dispose() - { - _tracker._threadingContext.ThrowIfNotOnUIThread(); + public void AddCallback(Action callback) + { + _tracker._threadingContext.ThrowIfNotOnUIThread(); + this.Callbacks = this.Callbacks.Add(callback); + } - // Shouldn't be disposing of this if we still have clients that want to hear about visibility changes. - Contract.ThrowIfTrue(Callbacks.Count > 0); + public void RemoveCallback(Action callback) + { + _tracker._threadingContext.ThrowIfNotOnUIThread(); + this.Callbacks = this.Callbacks.Remove(callback); + } - // Clear out all our textviews. This will disconnect us from any events we have registered with them. - UpdateTextViews([]); + public void UpdateAssociatedViews() + { + _tracker._threadingContext.ThrowIfNotOnUIThread(); - Contract.ThrowIfTrue(TextViews.Count > 0); - } + // Update us to whatever the currently associated text views are for this buffer. + UpdateTextViews(_tracker._associatedViewService.GetAssociatedTextViews(_subjectBuffer)); + } - public void AddCallback(Action callback) - { - _tracker._threadingContext.ThrowIfNotOnUIThread(); - this.Callbacks = this.Callbacks.Add(callback); - } + private void UpdateTextViews(IEnumerable associatedTextViews) + { + var removedViews = TextViews.Except(associatedTextViews); + var addedViews = associatedTextViews.Except(TextViews); - public void RemoveCallback(Action callback) + // Disconnect from hearing about visibility changes for any views we're no longer associated with. + foreach (var removedView in removedViews) { - _tracker._threadingContext.ThrowIfNotOnUIThread(); - this.Callbacks = this.Callbacks.Remove(callback); + if (removedView is TTextView genericView) + _tracker.RemoveVisibilityChangedCallback(genericView, _visibilityChangedCallback); } - public void UpdateAssociatedViews() + // Connect to hearing about visbility changes for any views we are associated with. + foreach (var addedView in addedViews) { - _tracker._threadingContext.ThrowIfNotOnUIThread(); - - // Update us to whatever the currently associated text views are for this buffer. - UpdateTextViews(_tracker._associatedViewService.GetAssociatedTextViews(_subjectBuffer)); + if (addedView is TTextView genericView) + _tracker.AddVisibilityChangedCallback(genericView, _visibilityChangedCallback); } - private void UpdateTextViews(IEnumerable associatedTextViews) - { - var removedViews = TextViews.Except(associatedTextViews); - var addedViews = associatedTextViews.Except(TextViews); - - // Disconnect from hearing about visibility changes for any views we're no longer associated with. - foreach (var removedView in removedViews) - { - if (removedView is TTextView genericView) - _tracker.RemoveVisibilityChangedCallback(genericView, _visibilityChangedCallback); - } - - // Connect to hearing about visbility changes for any views we are associated with. - foreach (var addedView in addedViews) - { - if (addedView is TTextView genericView) - _tracker.AddVisibilityChangedCallback(genericView, _visibilityChangedCallback); - } - - TextViews.Clear(); - TextViews.AddRange(associatedTextViews); - } + TextViews.Clear(); + TextViews.AddRange(associatedTextViews); + } - public void TriggerCallbacks() - { - _tracker._threadingContext.ThrowIfNotOnUIThread(); - foreach (var callback in Callbacks) - callback(); - } + public void TriggerCallbacks() + { + _tracker._threadingContext.ThrowIfNotOnUIThread(); + foreach (var callback in Callbacks) + callback(); } } } diff --git a/src/EditorFeatures/Core/Workspaces/EditorTextFactoryService.cs b/src/EditorFeatures/Core/Workspaces/EditorTextFactoryService.cs index c6220530b140c..5f675b452e9d2 100644 --- a/src/EditorFeatures/Core/Workspaces/EditorTextFactoryService.cs +++ b/src/EditorFeatures/Core/Workspaces/EditorTextFactoryService.cs @@ -14,76 +14,75 @@ using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.Workspaces +namespace Microsoft.CodeAnalysis.Editor.Implementation.Workspaces; + +[ExportWorkspaceService(typeof(ITextFactoryService), ServiceLayer.Editor), Shared] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal sealed class EditorTextFactoryService( + ITextBufferCloneService textBufferCloneService, + ITextBufferFactoryService textBufferFactoryService, + IContentTypeRegistryService contentTypeRegistryService) : ITextFactoryService { - [ExportWorkspaceService(typeof(ITextFactoryService), ServiceLayer.Editor), Shared] - [method: ImportingConstructor] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal sealed class EditorTextFactoryService( - ITextBufferCloneService textBufferCloneService, - ITextBufferFactoryService textBufferFactoryService, - IContentTypeRegistryService contentTypeRegistryService) : ITextFactoryService + private readonly ITextBufferCloneService _textBufferCloneService = textBufferCloneService; + private readonly ITextBufferFactoryService _textBufferFactory = textBufferFactoryService; + private readonly IContentType _unknownContentType = contentTypeRegistryService.UnknownContentType; + private static readonly Encoding s_throwingUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + + public SourceText CreateText(Stream stream, Encoding? defaultEncoding, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) { - private readonly ITextBufferCloneService _textBufferCloneService = textBufferCloneService; - private readonly ITextBufferFactoryService _textBufferFactory = textBufferFactoryService; - private readonly IContentType _unknownContentType = contentTypeRegistryService.UnknownContentType; - private static readonly Encoding s_throwingUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + // this API is for a case where user wants us to figure out encoding from the given stream. + // if defaultEncoding is given, we will use it if we couldn't figure out encoding used in the stream ourselves. + RoslynDebug.Assert(stream != null); + RoslynDebug.Assert(stream.CanSeek); + RoslynDebug.Assert(stream.CanRead); - public SourceText CreateText(Stream stream, Encoding? defaultEncoding, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + if (defaultEncoding == null) { - // this API is for a case where user wants us to figure out encoding from the given stream. - // if defaultEncoding is given, we will use it if we couldn't figure out encoding used in the stream ourselves. - RoslynDebug.Assert(stream != null); - RoslynDebug.Assert(stream.CanSeek); - RoslynDebug.Assert(stream.CanRead); - - if (defaultEncoding == null) - { - // Try UTF-8 - try - { - return CreateTextInternal(stream, s_throwingUtf8Encoding, checksumAlgorithm, cancellationToken); - } - catch (DecoderFallbackException) - { - // Try Encoding.Default - defaultEncoding = Encoding.Default; - } - } - + // Try UTF-8 try { - return CreateTextInternal(stream, defaultEncoding, checksumAlgorithm, cancellationToken); + return CreateTextInternal(stream, s_throwingUtf8Encoding, checksumAlgorithm, cancellationToken); } catch (DecoderFallbackException) { - // TODO: the callers do not expect null (https://github.com/dotnet/roslyn/issues/43040) - return null!; + // Try Encoding.Default + defaultEncoding = Encoding.Default; } } - public SourceText CreateText(TextReader reader, Encoding? encoding, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + try { - // this API is for a case where user just wants to create a source text with explicit encoding. - var buffer = CreateTextBuffer(reader); - - // use the given encoding as it is. - return buffer.CurrentSnapshot.AsRoslynText(_textBufferCloneService, encoding, checksumAlgorithm); + return CreateTextInternal(stream, defaultEncoding, checksumAlgorithm, cancellationToken); + } + catch (DecoderFallbackException) + { + // TODO: the callers do not expect null (https://github.com/dotnet/roslyn/issues/43040) + return null!; } + } - private ITextBuffer CreateTextBuffer(TextReader reader) - => _textBufferFactory.CreateTextBuffer(reader, _unknownContentType); + public SourceText CreateText(TextReader reader, Encoding? encoding, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + { + // this API is for a case where user just wants to create a source text with explicit encoding. + var buffer = CreateTextBuffer(reader); - private SourceText CreateTextInternal(Stream stream, Encoding encoding, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - stream.Seek(0, SeekOrigin.Begin); + // use the given encoding as it is. + return buffer.CurrentSnapshot.AsRoslynText(_textBufferCloneService, encoding, checksumAlgorithm); + } - using var reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true); + private ITextBuffer CreateTextBuffer(TextReader reader) + => _textBufferFactory.CreateTextBuffer(reader, _unknownContentType); - var buffer = CreateTextBuffer(reader); - return buffer.CurrentSnapshot.AsRoslynText(_textBufferCloneService, reader.CurrentEncoding ?? Encoding.UTF8, checksumAlgorithm); - } + private SourceText CreateTextInternal(Stream stream, Encoding encoding, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + stream.Seek(0, SeekOrigin.Begin); + + using var reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true); + + var buffer = CreateTextBuffer(reader); + return buffer.CurrentSnapshot.AsRoslynText(_textBufferCloneService, reader.CurrentEncoding ?? Encoding.UTF8, checksumAlgorithm); } } diff --git a/src/EditorFeatures/Core/Workspaces/ITextBufferVisibilityTracker.cs b/src/EditorFeatures/Core/Workspaces/ITextBufferVisibilityTracker.cs index fdc24c4d27df8..390bed532d090 100644 --- a/src/EditorFeatures/Core/Workspaces/ITextBufferVisibilityTracker.cs +++ b/src/EditorFeatures/Core/Workspaces/ITextBufferVisibilityTracker.cs @@ -11,99 +11,98 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Threading; -namespace Microsoft.CodeAnalysis.Workspaces +namespace Microsoft.CodeAnalysis.Workspaces; + +/// +/// All methods must be called on UI thread. +/// +internal interface ITextBufferVisibilityTracker { /// - /// All methods must be called on UI thread. + /// Whether or not this text buffer is in an actively visible . /// - internal interface ITextBufferVisibilityTracker - { - /// - /// Whether or not this text buffer is in an actively visible . - /// - bool IsVisible(ITextBuffer subjectBuffer); + bool IsVisible(ITextBuffer subjectBuffer); - /// - /// Registers to hear about visibility changes for this particular buffer. Note: registration will not trigger - /// a call to . If clients need that information, they should check the state of the themselves. - /// - void RegisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback); + /// + /// Registers to hear about visibility changes for this particular buffer. Note: registration will not trigger + /// a call to . If clients need that information, they should check the state of the themselves. + /// + void RegisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback); - /// - /// Unregister equivalent of . - /// - void UnregisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback); - } + /// + /// Unregister equivalent of . + /// + void UnregisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback); +} - internal static class ITextBufferVisibilityTrackerExtensions +internal static class ITextBufferVisibilityTrackerExtensions +{ + /// + /// Waits the specified amount of time while the specified is not visible. If + /// any document visibility changes happen, the delay will cancel. + /// + public static Task DelayWhileNonVisibleAsync( + this ITextBufferVisibilityTracker? service, + IThreadingContext threadingContext, + IAsynchronousOperationListener listener, + ITextBuffer subjectBuffer, + TimeSpan timeSpan, + CancellationToken cancellationToken) { - /// - /// Waits the specified amount of time while the specified is not visible. If - /// any document visibility changes happen, the delay will cancel. - /// - public static Task DelayWhileNonVisibleAsync( - this ITextBufferVisibilityTracker? service, - IThreadingContext threadingContext, - IAsynchronousOperationListener listener, - ITextBuffer subjectBuffer, - TimeSpan timeSpan, - CancellationToken cancellationToken) - { - // Only add a delay if we have access to a service that will tell us when the buffer become visible or not. - if (service is null) - return Task.CompletedTask; + // Only add a delay if we have access to a service that will tell us when the buffer become visible or not. + if (service is null) + return Task.CompletedTask; - // Because cancellation is both expensive, and a super common thing to occur while we're delaying the caller - // until visibility, we special case the implementation here and transition to the canceled state - // explicitly, rather than throwing a cancellation exception. + // Because cancellation is both expensive, and a super common thing to occur while we're delaying the caller + // until visibility, we special case the implementation here and transition to the canceled state + // explicitly, rather than throwing a cancellation exception. - var delayTask = DelayWhileNonVisibleWorkerAsync(); + var delayTask = DelayWhileNonVisibleWorkerAsync(); - // it's very reasonable for the delay-task to complete synchronously (we've already been canceled, or the - // buffer is already visible. So fast path that out. - if (delayTask.IsCompleted) - return delayTask; + // it's very reasonable for the delay-task to complete synchronously (we've already been canceled, or the + // buffer is already visible. So fast path that out. + if (delayTask.IsCompleted) + return delayTask; - var taskOfTask = delayTask.ContinueWith( - // Convert a successfully completed task when we were canceled to a canceled task. Otherwise, return - // the faulted or non-canceled task as is. - task => task.Status == TaskStatus.RanToCompletion && cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : task, - CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - return taskOfTask.Unwrap(); + var taskOfTask = delayTask.ContinueWith( + // Convert a successfully completed task when we were canceled to a canceled task. Otherwise, return + // the faulted or non-canceled task as is. + task => task.Status == TaskStatus.RanToCompletion && cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : task, + CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + return taskOfTask.Unwrap(); - // Normal delay logic, except that this does not throw in the event of cancellation, but instead returns - // gracefully. The above task continuation logic then ensures we return a canceled task without needing - // exceptions. - async Task DelayWhileNonVisibleWorkerAsync() - { - if (cancellationToken.IsCancellationRequested) - return; + // Normal delay logic, except that this does not throw in the event of cancellation, but instead returns + // gracefully. The above task continuation logic then ensures we return a canceled task without needing + // exceptions. + async Task DelayWhileNonVisibleWorkerAsync() + { + if (cancellationToken.IsCancellationRequested) + return; - await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); - if (cancellationToken.IsCancellationRequested) - return; + await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); + if (cancellationToken.IsCancellationRequested) + return; - if (service.IsVisible(subjectBuffer)) - return; + if (service.IsVisible(subjectBuffer)) + return; - // ensure we listen for visibility changes before checking. That way we don't have a race where we check - // something see it is not visible, but then do not hear about its visibility change because we've hooked up - // our event after that happens. - var visibilityChangedTaskSource = new TaskCompletionSource(); - var callback = void () => visibilityChangedTaskSource.TrySetResult(true); - service.RegisterForVisibilityChanges(subjectBuffer, callback); + // ensure we listen for visibility changes before checking. That way we don't have a race where we check + // something see it is not visible, but then do not hear about its visibility change because we've hooked up + // our event after that happens. + var visibilityChangedTaskSource = new TaskCompletionSource(); + var callback = void () => visibilityChangedTaskSource.TrySetResult(true); + service.RegisterForVisibilityChanges(subjectBuffer, callback); - try - { - // Listen to when the active document changed so that we startup work on a document once it becomes visible. - var delayTask = listener.Delay(timeSpan, cancellationToken); - await Task.WhenAny(delayTask, visibilityChangedTaskSource.Task).NoThrowAwaitable(captureContext: true); - } - finally - { - service.UnregisterForVisibilityChanges(subjectBuffer, callback); - } + try + { + // Listen to when the active document changed so that we startup work on a document once it becomes visible. + var delayTask = listener.Delay(timeSpan, cancellationToken); + await Task.WhenAny(delayTask, visibilityChangedTaskSource.Task).NoThrowAwaitable(captureContext: true); + } + finally + { + service.UnregisterForVisibilityChanges(subjectBuffer, callback); } } } diff --git a/src/EditorFeatures/Core/Workspaces/TextUndoHistoryWorkspaceServiceFactoryService.cs b/src/EditorFeatures/Core/Workspaces/TextUndoHistoryWorkspaceServiceFactoryService.cs index af30a8e6feabd..9a2487541b1d1 100644 --- a/src/EditorFeatures/Core/Workspaces/TextUndoHistoryWorkspaceServiceFactoryService.cs +++ b/src/EditorFeatures/Core/Workspaces/TextUndoHistoryWorkspaceServiceFactoryService.cs @@ -11,24 +11,23 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Operations; -namespace Microsoft.CodeAnalysis.Editor.Implementation.Workspaces +namespace Microsoft.CodeAnalysis.Editor.Implementation.Workspaces; + +[ExportWorkspaceServiceFactory(typeof(ITextUndoHistoryWorkspaceService), ServiceLayer.Default), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class TextUndoHistoryWorkspaceServiceFactoryService(ITextUndoHistoryRegistry textUndoHistoryRegistry) : IWorkspaceServiceFactory { - [ExportWorkspaceServiceFactory(typeof(ITextUndoHistoryWorkspaceService), ServiceLayer.Default), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class TextUndoHistoryWorkspaceServiceFactoryService(ITextUndoHistoryRegistry textUndoHistoryRegistry) : IWorkspaceServiceFactory - { - private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry = textUndoHistoryRegistry; + private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry = textUndoHistoryRegistry; - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new TextUndoHistoryWorkspaceService(_textUndoHistoryRegistry); + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => new TextUndoHistoryWorkspaceService(_textUndoHistoryRegistry); - private class TextUndoHistoryWorkspaceService(ITextUndoHistoryRegistry textUndoHistoryRegistry) : ITextUndoHistoryWorkspaceService - { - private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry = textUndoHistoryRegistry; + private class TextUndoHistoryWorkspaceService(ITextUndoHistoryRegistry textUndoHistoryRegistry) : ITextUndoHistoryWorkspaceService + { + private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry = textUndoHistoryRegistry; - public bool TryGetTextUndoHistory(Workspace editorWorkspace, ITextBuffer textBuffer, out ITextUndoHistory undoHistory) - => _textUndoHistoryRegistry.TryGetHistory(textBuffer, out undoHistory); - } + public bool TryGetTextUndoHistory(Workspace editorWorkspace, ITextBuffer textBuffer, out ITextUndoHistory undoHistory) + => _textUndoHistoryRegistry.TryGetHistory(textBuffer, out undoHistory); } } diff --git a/src/EditorFeatures/Test/Diagnostics/DiagnosticsClassificationTaggerProviderTests.cs b/src/EditorFeatures/Test/Diagnostics/DiagnosticsClassificationTaggerProviderTests.cs index df107ad3429a0..91a462a89dce7 100644 --- a/src/EditorFeatures/Test/Diagnostics/DiagnosticsClassificationTaggerProviderTests.cs +++ b/src/EditorFeatures/Test/Diagnostics/DiagnosticsClassificationTaggerProviderTests.cs @@ -101,6 +101,7 @@ public override void Initialize(AnalysisContext context) c.ReportDiagnostic(DiagnosticHelper.Create( _rule, primaryLocation, NotificationOption2.Error, + c.Options, additionalLocations: null, properties: null)); } @@ -114,6 +115,7 @@ public override void Initialize(AnalysisContext context) c.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( _rule, primaryLocation, NotificationOption2.Error, + c.Options, additionalLocations, additionalUnnecessaryLocations)); } diff --git a/src/EditorFeatures/Test/FindReferences/FindReferencesCommandHandlerTests.cs b/src/EditorFeatures/Test/FindReferences/FindReferencesCommandHandlerTests.cs index 31ded0281f2d6..80caece380ed4 100644 --- a/src/EditorFeatures/Test/FindReferences/FindReferencesCommandHandlerTests.cs +++ b/src/EditorFeatures/Test/FindReferences/FindReferencesCommandHandlerTests.cs @@ -4,14 +4,12 @@ #nullable disable -using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.Host; using Microsoft.CodeAnalysis.Editor.UnitTests.Utilities; -using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; using Microsoft.CodeAnalysis.FindReferences; using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Shared.TestHooks; @@ -49,14 +47,11 @@ private class MockStreamingFindUsagesPresenter : IStreamingFindUsagesPresenter public MockStreamingFindUsagesPresenter(FindUsagesContext context) => _context = context; - public (FindUsagesContext, CancellationToken) StartSearch(string title, bool supportsReferences) - => (_context, CancellationToken.None); - public void ClearAll() { } - public (FindUsagesContext, CancellationToken) StartSearchWithCustomColumns(string title, bool supportsReferences, bool includeContainingTypeAndMemberColumns, bool includeKindColumn) + public (FindUsagesContext, CancellationToken) StartSearch(string title, StreamingFindUsagesPresenterOptions options) => (_context, CancellationToken.None); } diff --git a/src/EditorFeatures/Test/SolutionCrawler/WorkCoordinatorTests.cs b/src/EditorFeatures/Test/SolutionCrawler/WorkCoordinatorTests.cs index cb530d727f1b1..596791608202f 100644 --- a/src/EditorFeatures/Test/SolutionCrawler/WorkCoordinatorTests.cs +++ b/src/EditorFeatures/Test/SolutionCrawler/WorkCoordinatorTests.cs @@ -12,14 +12,12 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.Test; -using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Notification; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.SolutionCrawler; using Microsoft.CodeAnalysis.Test.Utilities; -using Microsoft.CodeAnalysis.Test.Utilities.Notification; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Composition; using Roslyn.Test.Utilities; @@ -69,7 +67,14 @@ public async Task DynamicallyAddAnalyzer() var worker = new Analyzer(workspace.GlobalOptions); var provider = new AnalyzerProvider(worker); - service.AddAnalyzerProvider(provider, Metadata.Crawler); + + service.AddAnalyzerProvider(provider, metadata: new( + new Dictionary + { + { "WorkspaceKinds", new[] { SolutionCrawlerWorkspaceKind } }, + { "HighPriorityForActiveFile", false }, + { "Name", "TestAnalyzer" } + })); // wait for everything to settle await WaitAsync(service, workspace); @@ -659,7 +664,7 @@ internal async Task Document_Reanalyze(BackgroundAnalysisScope analysisScope, bo await WaitWaiterAsync(workspace.ExportProvider); var lazyWorker = Assert.Single(workspace.ExportProvider.GetExports()); - Assert.Equal(Metadata.Crawler, lazyWorker.Metadata); + VerifyMetadata(lazyWorker.Metadata); var worker = Assert.IsType(Assert.IsAssignableFrom(lazyWorker.Value).Analyzer); Assert.False(worker.WaitForCancellation); Assert.False(worker.BlockedRun); @@ -813,7 +818,7 @@ internal async Task Document_Cancellation(BackgroundAnalysisScope analysisScope, await WaitWaiterAsync(workspace.ExportProvider); var lazyWorker = Assert.Single(workspace.ExportProvider.GetExports()); - Assert.Equal(Metadata.Crawler, lazyWorker.Metadata); + VerifyMetadata(lazyWorker.Metadata); var analyzer = Assert.IsType(Assert.IsAssignableFrom(lazyWorker.Value).Analyzer); Assert.True(analyzer.WaitForCancellation); Assert.False(analyzer.BlockedRun); @@ -871,7 +876,7 @@ internal async Task Document_Cancellation_MultipleTimes(BackgroundAnalysisScope var expectedDocumentSemanticEvents = 5; var lazyWorker = Assert.Single(workspace.ExportProvider.GetExports()); - Assert.Equal(Metadata.Crawler, lazyWorker.Metadata); + VerifyMetadata(lazyWorker.Metadata); var analyzer = Assert.IsType(Assert.IsAssignableFrom(lazyWorker.Value).Analyzer); Assert.True(analyzer.WaitForCancellation); Assert.False(analyzer.BlockedRun); @@ -921,7 +926,7 @@ public async Task Document_InvocationReasons() var id = workspace.CurrentSolution.Projects.First().DocumentIds[0]; var lazyWorker = Assert.Single(workspace.ExportProvider.GetExports()); - Assert.Equal(Metadata.Crawler, lazyWorker.Metadata); + VerifyMetadata(lazyWorker.Metadata); var analyzer = Assert.IsType(Assert.IsAssignableFrom(lazyWorker.Value).Analyzer); Assert.False(analyzer.WaitForCancellation); Assert.True(analyzer.BlockedRun); @@ -1449,7 +1454,7 @@ public async Task FileFromSameProjectTogetherTest() // add analyzer var lazyWorker = Assert.Single(workspace.ExportProvider.GetExports()); - Assert.Equal(Metadata.Crawler, lazyWorker.Metadata); + VerifyMetadata(lazyWorker.Metadata); var worker = Assert.IsType(Assert.IsAssignableFrom(lazyWorker.Value).Analyzer); // enable solution crawler @@ -1534,7 +1539,7 @@ private static async Task InsertText(string code, string text, bool expectDocume var textBuffer = testDocument.GetTextBuffer(); var lazyWorker = Assert.Single(workspace.ExportProvider.GetExports()); - Assert.Equal(Metadata.Crawler, lazyWorker.Metadata); + VerifyMetadata(lazyWorker.Metadata); var analyzer = Assert.IsType(Assert.IsAssignableFrom(lazyWorker.Value).Analyzer); Assert.False(analyzer.WaitForCancellation); Assert.False(analyzer.BlockedRun); @@ -1568,7 +1573,8 @@ private static Task ExecuteOperationAsync(EditorTestWorkspace workspac private static async Task ExecuteOperationAsync(EditorTestWorkspace workspace, Func operation) { var lazyWorker = Assert.Single(workspace.ExportProvider.GetExports()); - Assert.Equal(Metadata.Crawler, lazyWorker.Metadata); + VerifyMetadata(lazyWorker.Metadata); + var worker = Assert.IsType(Assert.IsAssignableFrom(lazyWorker.Value).Analyzer); Assert.False(worker.WaitForCancellation); Assert.False(worker.BlockedRun); @@ -1789,9 +1795,11 @@ public AnalyzerProvider2() } } - internal static class Metadata + internal static void VerifyMetadata(IncrementalAnalyzerProviderMetadata metadata) { - public static readonly IncrementalAnalyzerProviderMetadata Crawler = new IncrementalAnalyzerProviderMetadata(new Dictionary { { "WorkspaceKinds", new[] { SolutionCrawlerWorkspaceKind } }, { "HighPriorityForActiveFile", false }, { "Name", "TestAnalyzer" } }); + AssertEx.AreEqual([SolutionCrawlerWorkspaceKind], metadata.WorkspaceKinds); + Assert.False(metadata.HighPriorityForActiveFile); + AssertEx.Equal("TestAnalyzer", metadata.Name); } private class Analyzer : IIncrementalAnalyzer diff --git a/src/EditorFeatures/Test/StackTraceExplorer/StackTraceExplorerTests.cs b/src/EditorFeatures/Test/StackTraceExplorer/StackTraceExplorerTests.cs index 5988d9b7defe0..5018a09bfacf7 100644 --- a/src/EditorFeatures/Test/StackTraceExplorer/StackTraceExplorerTests.cs +++ b/src/EditorFeatures/Test/StackTraceExplorer/StackTraceExplorerTests.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.UnitTests.Utilities; -using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.StackTraceExplorer; @@ -58,7 +57,6 @@ private static async Task TestSymbolFoundAsync(string inputLine, string code) Assert.Equal(expectedDefinition.IsExternal, definition.IsExternal); AssertEx.SetEqual(expectedDefinition.NameDisplayParts, definition.NameDisplayParts); - AssertEx.SetEqual(expectedDefinition.OriginationParts, definition.OriginationParts); AssertEx.SetEqual(expectedDefinition.Properties, definition.Properties); AssertEx.SetEqual(expectedDefinition.SourceSpans, definition.SourceSpans); AssertEx.SetEqual(expectedDefinition.Tags, definition.Tags); diff --git a/src/EditorFeatures/Test/Structure/BlockStructureServiceTests.cs b/src/EditorFeatures/Test/Structure/BlockStructureServiceTests.cs index 54581a7fae6b8..0e1f01ebd8146 100644 --- a/src/EditorFeatures/Test/Structure/BlockStructureServiceTests.cs +++ b/src/EditorFeatures/Test/Structure/BlockStructureServiceTests.cs @@ -92,6 +92,24 @@ static void Goo() Assert.Equal(4, spans.Length); } + [Fact] + public async Task TestTwoInvocationExpressionsThreeLines() + { + // The inner argument list should be collapsible, but the outer one shouldn't. + var code = """ + var x = MyMethod1(MyMethod2( + "", + ""); + """; + + using var workspace = TestWorkspace.CreateCSharp(code); + var spans = await GetSpansFromWorkspaceAsync(workspace); + + Assert.Equal(1, spans.Length); + + Assert.Equal(27, spans[0].TextSpan.Start); + } + private static async Task> GetSpansFromWorkspaceAsync( TestWorkspace workspace) { diff --git a/src/EditorFeatures/Test2/FindReferences/FindReferencesCommandHandlerTests.vb b/src/EditorFeatures/Test2/FindReferences/FindReferencesCommandHandlerTests.vb index 806e02550d625..0280c60690d8c 100644 --- a/src/EditorFeatures/Test2/FindReferences/FindReferencesCommandHandlerTests.vb +++ b/src/EditorFeatures/Test2/FindReferences/FindReferencesCommandHandlerTests.vb @@ -4,7 +4,6 @@ Imports System.Threading Imports Microsoft.CodeAnalysis.Editor.Host -Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces Imports Microsoft.CodeAnalysis.FindReferences Imports Microsoft.CodeAnalysis.FindUsages Imports Microsoft.CodeAnalysis.Shared.TestHooks @@ -74,11 +73,7 @@ class C Public Sub ClearAll() Implements IStreamingFindUsagesPresenter.ClearAll End Sub - Public Function StartSearch(title As String, supportsReferences As Boolean) As (FindUsagesContext, CancellationToken) Implements IStreamingFindUsagesPresenter.StartSearch - Return (_context, CancellationToken.None) - End Function - - Public Function StartSearchWithCustomColumns(title As String, supportsReferences As Boolean, includeContainingTypeAndMemberColumns As Boolean, includeKindColumn As Boolean) As (FindUsagesContext, CancellationToken) Implements IStreamingFindUsagesPresenter.StartSearchWithCustomColumns + Public Function StartSearch(title As String, options As StreamingFindUsagesPresenterOptions) As (FindUsagesContext, CancellationToken) Implements IStreamingFindUsagesPresenter.StartSearch Return (_context, CancellationToken.None) End Function End Class diff --git a/src/EditorFeatures/Test2/GoToHelpers/GoToHelpers.vb b/src/EditorFeatures/Test2/GoToHelpers/GoToHelpers.vb index b1589c5363aec..9e35a2e110b5f 100644 --- a/src/EditorFeatures/Test2/GoToHelpers/GoToHelpers.vb +++ b/src/EditorFeatures/Test2/GoToHelpers/GoToHelpers.vb @@ -54,21 +54,21 @@ Friend Class GoToHelpers $"Expected: ({expected}) but got: ({actual})") Next - Dim actualDefintionsWithoutSpans = context.GetDefinitions(). + Dim actualDefinitionsWithoutSpans = context.GetDefinitions(). Where(Function(d) d.SourceSpans.IsDefaultOrEmpty). Select(Function(di) Return String.Format("{0}:{1}", - String.Join("", di.OriginationParts.Select(Function(t) t.Text)), + String.Join("", di.MetadataLocations.Single().Name), String.Join("", di.NameDisplayParts.Select(Function(t) t.Text))) End Function).ToList() - actualDefintionsWithoutSpans.Sort() + actualDefinitionsWithoutSpans.Sort() If metadataDefinitions Is Nothing Then metadataDefinitions = {} End If - AssertEx.Equal(metadataDefinitions, actualDefintionsWithoutSpans) + AssertEx.Equal(metadataDefinitions, actualDefinitionsWithoutSpans) End If End Using End Function diff --git a/src/EditorFeatures/Test2/Rename/RenameCommandHandlerTests.vb b/src/EditorFeatures/Test2/Rename/RenameCommandHandlerTests.vb index 9ba400e420fda..b960777b00914 100644 --- a/src/EditorFeatures/Test2/Rename/RenameCommandHandlerTests.vb +++ b/src/EditorFeatures/Test2/Rename/RenameCommandHandlerTests.vb @@ -24,7 +24,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Rename - Public Sub RenameCommandInvokesInlineRename(host As RenameTestHost) + Public Async Function RenameCommandInvokesInlineRename(host As RenameTestHost) As Task Using workspace = CreateWorkspaceWithWaiter( @@ -40,11 +40,12 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Rename view.Caret.MoveTo(New SnapshotPoint(view.TextBuffer.CurrentSnapshot, workspace.Documents.Single(Function(d) d.CursorPosition.HasValue).CursorPosition.Value)) CreateCommandHandler(workspace).ExecuteCommand(New RenameCommandArgs(view, view.TextBuffer), Sub() Throw New Exception("The operation should have been handled."), Utilities.TestCommandExecutionContext.Create()) + Await WaitForRename(workspace) Dim expectedTriggerToken = workspace.CurrentSolution.Projects.Single().Documents.Single().GetSyntaxRootAsync().Result.FindToken(view.Caret.Position.BufferPosition) Assert.Equal(expectedTriggerToken.Span.ToSnapshotSpan(view.TextSnapshot), view.Selection.SelectedSpans.Single()) End Using - End Sub + End Function diff --git a/src/EditorFeatures/TestUtilities2/Utilities/GoToHelpers/MockNavigableItemsPresenter.vb b/src/EditorFeatures/TestUtilities2/Utilities/GoToHelpers/MockNavigableItemsPresenter.vb index 3d703492872f5..3af706b692abb 100644 --- a/src/EditorFeatures/TestUtilities2/Utilities/GoToHelpers/MockNavigableItemsPresenter.vb +++ b/src/EditorFeatures/TestUtilities2/Utilities/GoToHelpers/MockNavigableItemsPresenter.vb @@ -22,13 +22,9 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Utilities.GoToHelpers Throw New NotImplementedException() End Sub - Public Function StartSearch(title As String, alwaysShowDeclarations As Boolean) As (FindUsagesContext, CancellationToken) Implements IStreamingFindUsagesPresenter.StartSearch + Public Function StartSearch(title As String, options As StreamingFindUsagesPresenterOptions) As (FindUsagesContext, CancellationToken) Implements IStreamingFindUsagesPresenter.StartSearch _action() Return (Context, CancellationToken.None) End Function - - Public Function StartSearchWithCustomColumns(title As String, supportsReferences As Boolean, includeContainingTypeAndMemberColumns As Boolean, includeKindColumn As Boolean) As (FindUsagesContext, CancellationToken) Implements IStreamingFindUsagesPresenter.StartSearchWithCustomColumns - Return (Context, CancellationToken.None) - End Function End Class End Namespace diff --git a/src/EditorFeatures/VisualBasicTest/Structure/MetadataAsSource/InvalidIdentifierStructureTests.vb b/src/EditorFeatures/VisualBasicTest/Structure/MetadataAsSource/InvalidIdentifierStructureTests.vb index 2b54b949d75a5..caca3dd788c5e 100644 --- a/src/EditorFeatures/VisualBasicTest/Structure/MetadataAsSource/InvalidIdentifierStructureTests.vb +++ b/src/EditorFeatures/VisualBasicTest/Structure/MetadataAsSource/InvalidIdentifierStructureTests.vb @@ -64,8 +64,8 @@ End Class End Class " Await VerifyBlockSpansAsync(code, - Region("textspan1", "hint1", "Class C " & Ellipsis, autoCollapse:=False), - Region("textspan2", "hint2", "Public Sub " & Ellipsis, autoCollapse:=True)) + Region("textspan2", "hint2", "Public Sub " & Ellipsis, autoCollapse:=True), + Region("textspan1", "hint1", "Class C " & Ellipsis, autoCollapse:=False)) End Function End Class diff --git a/src/EditorFeatures/VisualBasicTest/Structure/OverallStructureTests.vb b/src/EditorFeatures/VisualBasicTest/Structure/OverallStructureTests.vb index d6431f706ebfb..3c730432a25a7 100644 --- a/src/EditorFeatures/VisualBasicTest/Structure/OverallStructureTests.vb +++ b/src/EditorFeatures/VisualBasicTest/Structure/OverallStructureTests.vb @@ -34,8 +34,8 @@ End Class|} " Await VerifyBlockSpansAsync(code, - Region("span1", "Class C ...", autoCollapse:=False), - Region("span2", "Something", autoCollapse:=False, isDefaultCollapsed:=True)) + Region("span2", "Something", autoCollapse:=False, isDefaultCollapsed:=True), + Region("span1", "Class C ...", autoCollapse:=False)) End Function End Class End Namespace diff --git a/src/Features/CSharp/Portable/AddDebuggerDisplay/CSharpAddDebuggerDisplayCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/AddDebuggerDisplay/CSharpAddDebuggerDisplayCodeRefactoringProvider.cs index e7a01ce10b8f1..d40331add1911 100644 --- a/src/Features/CSharp/Portable/AddDebuggerDisplay/CSharpAddDebuggerDisplayCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/AddDebuggerDisplay/CSharpAddDebuggerDisplayCodeRefactoringProvider.cs @@ -11,23 +11,22 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.AddDebuggerDisplay +namespace Microsoft.CodeAnalysis.CSharp.AddDebuggerDisplay; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.AddDebuggerDisplay), Shared] +internal sealed class CSharpAddDebuggerDisplayCodeRefactoringProvider + : AbstractAddDebuggerDisplayCodeRefactoringProvider< + TypeDeclarationSyntax, + MethodDeclarationSyntax> { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.AddDebuggerDisplay), Shared] - internal sealed class CSharpAddDebuggerDisplayCodeRefactoringProvider - : AbstractAddDebuggerDisplayCodeRefactoringProvider< - TypeDeclarationSyntax, - MethodDeclarationSyntax> + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpAddDebuggerDisplayCodeRefactoringProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpAddDebuggerDisplayCodeRefactoringProvider() - { - } + } - protected override bool CanNameofAccessNonPublicMembersFromAttributeArgument => true; + protected override bool CanNameofAccessNonPublicMembersFromAttributeArgument => true; - protected override bool SupportsConstantInterpolatedStrings(Document document) - => document.Project.ParseOptions!.LanguageVersion().HasConstantInterpolatedStrings(); - } + protected override bool SupportsConstantInterpolatedStrings(Document document) + => document.Project.ParseOptions!.LanguageVersion().HasConstantInterpolatedStrings(); } diff --git a/src/Features/CSharp/Portable/AddFileBanner/CSharpAddFileBannerCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/AddFileBanner/CSharpAddFileBannerCodeRefactoringProvider.cs index b77f0640b0d46..7a747adfcea0d 100644 --- a/src/Features/CSharp/Portable/AddFileBanner/CSharpAddFileBannerCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/AddFileBanner/CSharpAddFileBannerCodeRefactoringProvider.cs @@ -7,33 +7,32 @@ using Microsoft.CodeAnalysis.AddFileBanner; using Microsoft.CodeAnalysis.CodeRefactorings; -namespace Microsoft.CodeAnalysis.CSharp.AddFileBanner +namespace Microsoft.CodeAnalysis.CSharp.AddFileBanner; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, + Name = PredefinedCodeRefactoringProviderNames.AddFileBanner), Shared] +internal class CSharpAddFileBannerCodeRefactoringProvider : AbstractAddFileBannerCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, - Name = PredefinedCodeRefactoringProviderNames.AddFileBanner), Shared] - internal class CSharpAddFileBannerCodeRefactoringProvider : AbstractAddFileBannerCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddFileBannerCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddFileBannerCodeRefactoringProvider() - { - } + } - protected override bool IsCommentStartCharacter(char ch) - => ch == '/'; + protected override bool IsCommentStartCharacter(char ch) + => ch == '/'; - protected override SyntaxTrivia CreateTrivia(SyntaxTrivia trivia, string text) + protected override SyntaxTrivia CreateTrivia(SyntaxTrivia trivia, string text) + { + switch (trivia.Kind()) { - switch (trivia.Kind()) - { - case SyntaxKind.SingleLineCommentTrivia: - case SyntaxKind.MultiLineCommentTrivia: - case SyntaxKind.SingleLineDocumentationCommentTrivia: - case SyntaxKind.MultiLineDocumentationCommentTrivia: - return SyntaxFactory.Comment(text); - } - - return trivia; + case SyntaxKind.SingleLineCommentTrivia: + case SyntaxKind.MultiLineCommentTrivia: + case SyntaxKind.SingleLineDocumentationCommentTrivia: + case SyntaxKind.MultiLineDocumentationCommentTrivia: + return SyntaxFactory.Comment(text); } + + return trivia; } } diff --git a/src/Features/CSharp/Portable/AddFileBanner/CSharpAddFileBannerNewDocumentFormattingProvider.cs b/src/Features/CSharp/Portable/AddFileBanner/CSharpAddFileBannerNewDocumentFormattingProvider.cs index 3019df71bde1b..c375b13091fdc 100644 --- a/src/Features/CSharp/Portable/AddFileBanner/CSharpAddFileBannerNewDocumentFormattingProvider.cs +++ b/src/Features/CSharp/Portable/AddFileBanner/CSharpAddFileBannerNewDocumentFormattingProvider.cs @@ -12,19 +12,18 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.AddFileBanner +namespace Microsoft.CodeAnalysis.CSharp.AddFileBanner; + +[ExportNewDocumentFormattingProvider(LanguageNames.CSharp), Shared] +internal class CSharpAddFileBannerNewDocumentFormattingProvider : AbstractAddFileBannerNewDocumentFormattingProvider { - [ExportNewDocumentFormattingProvider(LanguageNames.CSharp), Shared] - internal class CSharpAddFileBannerNewDocumentFormattingProvider : AbstractAddFileBannerNewDocumentFormattingProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpAddFileBannerNewDocumentFormattingProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpAddFileBannerNewDocumentFormattingProvider() - { - } - - protected override SyntaxGenerator SyntaxGenerator => CSharpSyntaxGenerator.Instance; - protected override SyntaxGeneratorInternal SyntaxGeneratorInternal => CSharpSyntaxGeneratorInternal.Instance; - protected override AbstractFileHeaderHelper FileHeaderHelper => CSharpFileHeaderHelper.Instance; } + + protected override SyntaxGenerator SyntaxGenerator => CSharpSyntaxGenerator.Instance; + protected override SyntaxGeneratorInternal SyntaxGeneratorInternal => CSharpSyntaxGeneratorInternal.Instance; + protected override AbstractFileHeaderHelper FileHeaderHelper => CSharpFileHeaderHelper.Instance; } diff --git a/src/Features/CSharp/Portable/AddImport/CSharpAddImportCodeFixProvider.cs b/src/Features/CSharp/Portable/AddImport/CSharpAddImportCodeFixProvider.cs index 255f5c4da9f21..06bb66db75487 100644 --- a/src/Features/CSharp/Portable/AddImport/CSharpAddImportCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/AddImport/CSharpAddImportCodeFixProvider.cs @@ -13,182 +13,181 @@ using Microsoft.CodeAnalysis.Packaging; using Microsoft.CodeAnalysis.SymbolSearch; -namespace Microsoft.CodeAnalysis.CSharp.AddImport +namespace Microsoft.CodeAnalysis.CSharp.AddImport; + +internal static class AddImportDiagnosticIds { - internal static class AddImportDiagnosticIds + /// + /// name does not exist in context + /// + public const string CS0103 = nameof(CS0103); + + /// + /// type or namespace could not be found + /// + public const string CS0246 = nameof(CS0246); + + /// + /// wrong number of type args + /// + public const string CS0305 = nameof(CS0305); + + /// + /// type does not contain a definition of method or extension method + /// + public const string CS1061 = nameof(CS1061); + + /// + /// cannot find implementation of query pattern + /// + public const string CS1935 = nameof(CS1935); + + /// + /// The non-generic type 'A' cannot be used with type arguments + /// + public const string CS0308 = nameof(CS0308); + + /// + /// 'A' is inaccessible due to its protection level + /// + public const string CS0122 = nameof(CS0122); + + /// + /// The using alias 'A' cannot be used with type arguments + /// + public const string CS0307 = nameof(CS0307); + + /// + /// 'A' is not an attribute class + /// + public const string CS0616 = nameof(CS0616); + + /// + /// No overload for method 'X' takes 'N' arguments + /// + public const string CS1501 = nameof(CS1501); + + /// + /// cannot convert from 'int' to 'string' + /// + public const string CS1503 = nameof(CS1503); + + /// + /// XML comment on 'construct' has syntactically incorrect cref attribute 'name' + /// + public const string CS1574 = nameof(CS1574); + + /// + /// Invalid type for parameter 'parameter number' in XML comment cref attribute + /// + public const string CS1580 = nameof(CS1580); + + /// + /// Invalid return type in XML comment cref attribute + /// + public const string CS1581 = nameof(CS1581); + + /// + /// XML comment has syntactically incorrect cref attribute + /// + public const string CS1584 = nameof(CS1584); + + /// + /// Type 'X' does not contain a valid extension method accepting 'Y' + /// + public const string CS1929 = nameof(CS1929); + + /// + /// Property cannot be used like a method + /// + public const string CS1955 = nameof(CS1955); + + /// + /// Cannot convert method group 'X' to non-delegate type 'Y'. Did you intend to invoke the method? + /// + public const string CS0428 = nameof(CS0428); + + /// + /// There is no argument given that corresponds to the required parameter 'X' of 'Y' + /// + public const string CS7036 = nameof(CS7036); + + /// + /// o Deconstruct instance or extension method was found for type 'X', with N out parameters + /// + public const string CS8129 = nameof(CS8129); + + /// + /// Internal symbol inaccessible because public key is wrong + /// + public const string CS0281 = nameof(CS0281); + + /// + /// 'X' does not contain a definition for 'Y' and no extension method 'Y' accepting a first argument of type 'X' could be found (are you missing a using directive for 'System'?) + /// Specialized for WinRT + /// + public const string CS4036 = nameof(CS4036); + + /// + /// foreach statement cannot operate on variables of type 'X' because 'X' does not contain a public instance or extension definition for 'GetEnumerator' + /// + public const string CS1579 = nameof(CS1579); + + /// + /// foreach statement cannot operate on variables of type 'X' because 'X' does not contain a public instance or extension definition for 'GetEnumerator'. Did you mean 'await foreach' rather than 'foreach'? + /// + public const string CS8414 = nameof(CS8414); + + /// + /// Asynchronous foreach statement cannot operate on variables of type 'X' because 'X' does not contain a suitable public instance or extension definition for 'GetAsyncEnumerator' + /// + public const string CS8411 = nameof(CS8411); + + /// + /// Asynchronous foreach statement cannot operate on variables of type 'X' because 'X' does not contain a suitable public instance or extension definition for 'GetAsyncEnumerator'. Did you mean 'foreach' rather than 'await foreach'? + /// + public const string CS8415 = nameof(CS8415); + + public static ImmutableArray FixableTypeIds = + [CS0103, CS0246, CS0305, CS0308, CS0122, CS0307, CS0616, CS1580, CS1581, CS8129, IDEDiagnosticIds.UnboundIdentifierId]; + + public static ImmutableArray FixableDiagnosticIds = + FixableTypeIds.Concat(ImmutableArray.Create( + CS1061, + CS1935, + CS1501, + CS1503, + CS1574, + CS1584, + CS1929, + CS1955, + CS0428, + CS7036, + CS0281, + CS4036, + CS1579, + CS8414, + CS8411, + CS8415)); +} + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddImport), Shared] +internal class CSharpAddImportCodeFixProvider : AbstractAddImportCodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => AddImportDiagnosticIds.FixableDiagnosticIds; + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddImportCodeFixProvider() { - /// - /// name does not exist in context - /// - public const string CS0103 = nameof(CS0103); - - /// - /// type or namespace could not be found - /// - public const string CS0246 = nameof(CS0246); - - /// - /// wrong number of type args - /// - public const string CS0305 = nameof(CS0305); - - /// - /// type does not contain a definition of method or extension method - /// - public const string CS1061 = nameof(CS1061); - - /// - /// cannot find implementation of query pattern - /// - public const string CS1935 = nameof(CS1935); - - /// - /// The non-generic type 'A' cannot be used with type arguments - /// - public const string CS0308 = nameof(CS0308); - - /// - /// 'A' is inaccessible due to its protection level - /// - public const string CS0122 = nameof(CS0122); - - /// - /// The using alias 'A' cannot be used with type arguments - /// - public const string CS0307 = nameof(CS0307); - - /// - /// 'A' is not an attribute class - /// - public const string CS0616 = nameof(CS0616); - - /// - /// No overload for method 'X' takes 'N' arguments - /// - public const string CS1501 = nameof(CS1501); - - /// - /// cannot convert from 'int' to 'string' - /// - public const string CS1503 = nameof(CS1503); - - /// - /// XML comment on 'construct' has syntactically incorrect cref attribute 'name' - /// - public const string CS1574 = nameof(CS1574); - - /// - /// Invalid type for parameter 'parameter number' in XML comment cref attribute - /// - public const string CS1580 = nameof(CS1580); - - /// - /// Invalid return type in XML comment cref attribute - /// - public const string CS1581 = nameof(CS1581); - - /// - /// XML comment has syntactically incorrect cref attribute - /// - public const string CS1584 = nameof(CS1584); - - /// - /// Type 'X' does not contain a valid extension method accepting 'Y' - /// - public const string CS1929 = nameof(CS1929); - - /// - /// Property cannot be used like a method - /// - public const string CS1955 = nameof(CS1955); - - /// - /// Cannot convert method group 'X' to non-delegate type 'Y'. Did you intend to invoke the method? - /// - public const string CS0428 = nameof(CS0428); - - /// - /// There is no argument given that corresponds to the required parameter 'X' of 'Y' - /// - public const string CS7036 = nameof(CS7036); - - /// - /// o Deconstruct instance or extension method was found for type 'X', with N out parameters - /// - public const string CS8129 = nameof(CS8129); - - /// - /// Internal symbol inaccessible because public key is wrong - /// - public const string CS0281 = nameof(CS0281); - - /// - /// 'X' does not contain a definition for 'Y' and no extension method 'Y' accepting a first argument of type 'X' could be found (are you missing a using directive for 'System'?) - /// Specialized for WinRT - /// - public const string CS4036 = nameof(CS4036); - - /// - /// foreach statement cannot operate on variables of type 'X' because 'X' does not contain a public instance or extension definition for 'GetEnumerator' - /// - public const string CS1579 = nameof(CS1579); - - /// - /// foreach statement cannot operate on variables of type 'X' because 'X' does not contain a public instance or extension definition for 'GetEnumerator'. Did you mean 'await foreach' rather than 'foreach'? - /// - public const string CS8414 = nameof(CS8414); - - /// - /// Asynchronous foreach statement cannot operate on variables of type 'X' because 'X' does not contain a suitable public instance or extension definition for 'GetAsyncEnumerator' - /// - public const string CS8411 = nameof(CS8411); - - /// - /// Asynchronous foreach statement cannot operate on variables of type 'X' because 'X' does not contain a suitable public instance or extension definition for 'GetAsyncEnumerator'. Did you mean 'foreach' rather than 'await foreach'? - /// - public const string CS8415 = nameof(CS8415); - - public static ImmutableArray FixableTypeIds = - [CS0103, CS0246, CS0305, CS0308, CS0122, CS0307, CS0616, CS1580, CS1581, CS8129, IDEDiagnosticIds.UnboundIdentifierId]; - - public static ImmutableArray FixableDiagnosticIds = - FixableTypeIds.Concat(ImmutableArray.Create( - CS1061, - CS1935, - CS1501, - CS1503, - CS1574, - CS1584, - CS1929, - CS1955, - CS0428, - CS7036, - CS0281, - CS4036, - CS1579, - CS8414, - CS8411, - CS8415)); } - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddImport), Shared] - internal class CSharpAddImportCodeFixProvider : AbstractAddImportCodeFixProvider + /// For testing purposes only (so that tests can pass in mock values) + [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] + internal CSharpAddImportCodeFixProvider( + IPackageInstallerService installerService, + ISymbolSearchService symbolSearchService) + : base(installerService, symbolSearchService) { - public override ImmutableArray FixableDiagnosticIds => AddImportDiagnosticIds.FixableDiagnosticIds; - - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddImportCodeFixProvider() - { - } - - /// For testing purposes only (so that tests can pass in mock values) - [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] - internal CSharpAddImportCodeFixProvider( - IPackageInstallerService installerService, - ISymbolSearchService symbolSearchService) - : base(installerService, symbolSearchService) - { - } } } diff --git a/src/Features/CSharp/Portable/AddImport/CSharpAddImportFeatureService.cs b/src/Features/CSharp/Portable/AddImport/CSharpAddImportFeatureService.cs index 075ad0817bef2..3e6f09860dc1e 100644 --- a/src/Features/CSharp/Portable/AddImport/CSharpAddImportFeatureService.cs +++ b/src/Features/CSharp/Portable/AddImport/CSharpAddImportFeatureService.cs @@ -29,612 +29,611 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.CSharp.AddImport.AddImportDiagnosticIds; -namespace Microsoft.CodeAnalysis.CSharp.AddImport +namespace Microsoft.CodeAnalysis.CSharp.AddImport; + +[ExportLanguageService(typeof(IAddImportFeatureService), LanguageNames.CSharp), Shared] +internal class CSharpAddImportFeatureService : AbstractAddImportFeatureService { - [ExportLanguageService(typeof(IAddImportFeatureService), LanguageNames.CSharp), Shared] - internal class CSharpAddImportFeatureService : AbstractAddImportFeatureService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpAddImportFeatureService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpAddImportFeatureService() - { - } - - protected override bool CanAddImport(SyntaxNode node, bool allowInHiddenRegions, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - return node.CanAddUsingDirectives(allowInHiddenRegions, cancellationToken); - } + } - protected override bool CanAddImportForMethod( - string diagnosticId, ISyntaxFacts syntaxFacts, SyntaxNode node, out SimpleNameSyntax nameNode) - { - nameNode = null; + protected override bool CanAddImport(SyntaxNode node, bool allowInHiddenRegions, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return node.CanAddUsingDirectives(allowInHiddenRegions, cancellationToken); + } - switch (diagnosticId) - { - case CS7036: - case CS0308: - case CS0428: - case CS1061: - if (node is ConditionalAccessExpressionSyntax conditionalAccess) - { - node = conditionalAccess.WhenNotNull; - } - else if (node is MemberBindingExpressionSyntax memberBinding1) - { - node = memberBinding1.Name; - } - else if (node.Parent.IsKind(SyntaxKind.CollectionInitializerExpression)) - { - return true; - } + protected override bool CanAddImportForMethod( + string diagnosticId, ISyntaxFacts syntaxFacts, SyntaxNode node, out SimpleNameSyntax nameNode) + { + nameNode = null; - break; - case CS0122: - case CS1501: - if (node is SimpleNameSyntax) - { - break; - } - else if (node is MemberBindingExpressionSyntax memberBindingExpr) - { - node = memberBindingExpr.Name; - } + switch (diagnosticId) + { + case CS7036: + case CS0308: + case CS0428: + case CS1061: + if (node is ConditionalAccessExpressionSyntax conditionalAccess) + { + node = conditionalAccess.WhenNotNull; + } + else if (node is MemberBindingExpressionSyntax memberBinding1) + { + node = memberBinding1.Name; + } + else if (node.Parent.IsKind(SyntaxKind.CollectionInitializerExpression)) + { + return true; + } + break; + case CS0122: + case CS1501: + if (node is SimpleNameSyntax) + { break; - case CS1929: - var memberAccessName = (node.Parent as MemberAccessExpressionSyntax)?.Name; - var conditionalAccessName = (((node.Parent as ConditionalAccessExpressionSyntax)?.WhenNotNull as InvocationExpressionSyntax)?.Expression as MemberBindingExpressionSyntax)?.Name; - if (memberAccessName == null && conditionalAccessName == null) - { - return false; - } - - node = memberAccessName ?? conditionalAccessName; - break; - - case CS1503: - //// look up its corresponding method name - var parent = node.GetAncestor(); - if (parent == null) - { - return false; - } + } + else if (node is MemberBindingExpressionSyntax memberBindingExpr) + { + node = memberBindingExpr.Name; + } - if (parent.Expression is MemberAccessExpressionSyntax method) - { - node = method.Name; - } + break; + case CS1929: + var memberAccessName = (node.Parent as MemberAccessExpressionSyntax)?.Name; + var conditionalAccessName = (((node.Parent as ConditionalAccessExpressionSyntax)?.WhenNotNull as InvocationExpressionSyntax)?.Expression as MemberBindingExpressionSyntax)?.Name; + if (memberAccessName == null && conditionalAccessName == null) + { + return false; + } - break; - case CS1955: - break; + node = memberAccessName ?? conditionalAccessName; + break; - default: + case CS1503: + //// look up its corresponding method name + var parent = node.GetAncestor(); + if (parent == null) + { return false; - } + } - nameNode = node as SimpleNameSyntax; - if (nameNode?.Parent?.Kind() is not SyntaxKind.SimpleMemberAccessExpression and not SyntaxKind.MemberBindingExpression) - return false; + if (parent.Expression is MemberAccessExpressionSyntax method) + { + node = method.Name; + } - var memberAccess = nameNode.Parent as MemberAccessExpressionSyntax; - var memberBinding = nameNode.Parent as MemberBindingExpressionSyntax; - if (memberAccess?.Parent?.Kind() is SyntaxKind.SimpleMemberAccessExpression or SyntaxKind.ElementAccessExpression || - memberBinding?.Parent?.Kind() is SyntaxKind.SimpleMemberAccessExpression or SyntaxKind.ElementAccessExpression) - { - return false; - } + break; + case CS1955: + break; - if (!syntaxFacts.IsNameOfSimpleMemberAccessExpression(node) && - !syntaxFacts.IsNameOfMemberBindingExpression(node)) - { + default: return false; - } - - return true; } - protected override bool CanAddImportForDeconstruct(string diagnosticId, SyntaxNode node) - => diagnosticId == CS8129; - - protected override bool CanAddImportForGetAwaiter(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node) - => (diagnosticId == CS1061 || // Regular cases - diagnosticId == CS4036 || // WinRT async interfaces - diagnosticId == CS1929) && // An extension `GetAwaiter()` is in scope, but for another type - AncestorOrSelfIsAwaitExpression(syntaxFactsService, node); - - protected override bool CanAddImportForGetEnumerator(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node) - => diagnosticId is CS1579 or CS8414; - - protected override bool CanAddImportForGetAsyncEnumerator(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node) - => diagnosticId is CS8411 or CS8415; + nameNode = node as SimpleNameSyntax; + if (nameNode?.Parent?.Kind() is not SyntaxKind.SimpleMemberAccessExpression and not SyntaxKind.MemberBindingExpression) + return false; - protected override bool CanAddImportForNamespace(string diagnosticId, SyntaxNode node, out SimpleNameSyntax nameNode) + var memberAccess = nameNode.Parent as MemberAccessExpressionSyntax; + var memberBinding = nameNode.Parent as MemberBindingExpressionSyntax; + if (memberAccess?.Parent?.Kind() is SyntaxKind.SimpleMemberAccessExpression or SyntaxKind.ElementAccessExpression || + memberBinding?.Parent?.Kind() is SyntaxKind.SimpleMemberAccessExpression or SyntaxKind.ElementAccessExpression) { - nameNode = null; return false; } - protected override bool CanAddImportForQuery(string diagnosticId, SyntaxNode node) - => (diagnosticId == CS1935 || // Regular cases - diagnosticId == CS1929) && // An extension method is in scope, but for another type - node.AncestorsAndSelf().Any(n => n is QueryExpressionSyntax && !(n.Parent is QueryContinuationSyntax)); - - protected override bool CanAddImportForType(string diagnosticId, SyntaxNode node, out SimpleNameSyntax nameNode) + if (!syntaxFacts.IsNameOfSimpleMemberAccessExpression(node) && + !syntaxFacts.IsNameOfMemberBindingExpression(node)) { - nameNode = null; - switch (diagnosticId) - { - case CS0103: - case IDEDiagnosticIds.UnboundIdentifierId: - case CS0246: - case CS0305: - case CS0308: - case CS0122: - case CS0307: - case CS0616: - case CS1580: - case CS1581: - case CS1955: - case CS0281: - break; + return false; + } - case CS1574: - case CS1584: - if (node is QualifiedCrefSyntax cref) - { - node = cref.Container; - } + return true; + } - break; + protected override bool CanAddImportForDeconstruct(string diagnosticId, SyntaxNode node) + => diagnosticId == CS8129; - default: - return false; - } + protected override bool CanAddImportForGetAwaiter(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node) + => (diagnosticId == CS1061 || // Regular cases + diagnosticId == CS4036 || // WinRT async interfaces + diagnosticId == CS1929) && // An extension `GetAwaiter()` is in scope, but for another type + AncestorOrSelfIsAwaitExpression(syntaxFactsService, node); - return TryFindStandaloneType(node, out nameNode); - } + protected override bool CanAddImportForGetEnumerator(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node) + => diagnosticId is CS1579 or CS8414; - private static bool TryFindStandaloneType(SyntaxNode node, out SimpleNameSyntax nameNode) - { - if (node is QualifiedNameSyntax qn) - { - node = GetLeftMostSimpleName(qn); - } + protected override bool CanAddImportForGetAsyncEnumerator(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node) + => diagnosticId is CS8411 or CS8415; - nameNode = node as SimpleNameSyntax; - return nameNode.LooksLikeStandaloneTypeName(); - } + protected override bool CanAddImportForNamespace(string diagnosticId, SyntaxNode node, out SimpleNameSyntax nameNode) + { + nameNode = null; + return false; + } + + protected override bool CanAddImportForQuery(string diagnosticId, SyntaxNode node) + => (diagnosticId == CS1935 || // Regular cases + diagnosticId == CS1929) && // An extension method is in scope, but for another type + node.AncestorsAndSelf().Any(n => n is QueryExpressionSyntax && !(n.Parent is QueryContinuationSyntax)); - private static SimpleNameSyntax GetLeftMostSimpleName(QualifiedNameSyntax qn) + protected override bool CanAddImportForType(string diagnosticId, SyntaxNode node, out SimpleNameSyntax nameNode) + { + nameNode = null; + switch (diagnosticId) { - while (qn != null) - { - var left = qn.Left; - if (left is SimpleNameSyntax simpleName) + case CS0103: + case IDEDiagnosticIds.UnboundIdentifierId: + case CS0246: + case CS0305: + case CS0308: + case CS0122: + case CS0307: + case CS0616: + case CS1580: + case CS1581: + case CS1955: + case CS0281: + break; + + case CS1574: + case CS1584: + if (node is QualifiedCrefSyntax cref) { - return simpleName; + node = cref.Container; } - qn = left as QualifiedNameSyntax; - } + break; - return null; + default: + return false; } - protected override ISet GetImportNamespacesInScope( - SemanticModel semanticModel, - SyntaxNode node, - CancellationToken cancellationToken) - { - return semanticModel.GetUsingNamespacesInScope(node); - } + return TryFindStandaloneType(node, out nameNode); + } - protected override ITypeSymbol GetDeconstructInfo( - SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + private static bool TryFindStandaloneType(SyntaxNode node, out SimpleNameSyntax nameNode) + { + if (node is QualifiedNameSyntax qn) { - return semanticModel.GetTypeInfo(node, cancellationToken).Type; + node = GetLeftMostSimpleName(qn); } - protected override ITypeSymbol GetQueryClauseInfo( - SemanticModel semanticModel, - SyntaxNode node, - CancellationToken cancellationToken) - { - var query = node.AncestorsAndSelf().OfType().First(); + nameNode = node as SimpleNameSyntax; + return nameNode.LooksLikeStandaloneTypeName(); + } - if (InfoBoundSuccessfully(semanticModel.GetQueryClauseInfo(query.FromClause, cancellationToken))) + private static SimpleNameSyntax GetLeftMostSimpleName(QualifiedNameSyntax qn) + { + while (qn != null) + { + var left = qn.Left; + if (left is SimpleNameSyntax simpleName) { - return null; + return simpleName; } - foreach (var clause in query.Body.Clauses) - { - if (InfoBoundSuccessfully(semanticModel.GetQueryClauseInfo(clause, cancellationToken))) - { - return null; - } - } + qn = left as QualifiedNameSyntax; + } - if (InfoBoundSuccessfully(semanticModel.GetSymbolInfo(query.Body.SelectOrGroup, cancellationToken))) - { - return null; - } + return null; + } - var fromClause = query.FromClause; - return semanticModel.GetTypeInfo(fromClause.Expression, cancellationToken).Type; - } + protected override ISet GetImportNamespacesInScope( + SemanticModel semanticModel, + SyntaxNode node, + CancellationToken cancellationToken) + { + return semanticModel.GetUsingNamespacesInScope(node); + } - private static bool InfoBoundSuccessfully(SymbolInfo symbolInfo) - => InfoBoundSuccessfully(symbolInfo.Symbol); + protected override ITypeSymbol GetDeconstructInfo( + SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + { + return semanticModel.GetTypeInfo(node, cancellationToken).Type; + } - private static bool InfoBoundSuccessfully(QueryClauseInfo semanticInfo) - => InfoBoundSuccessfully(semanticInfo.OperationInfo); + protected override ITypeSymbol GetQueryClauseInfo( + SemanticModel semanticModel, + SyntaxNode node, + CancellationToken cancellationToken) + { + var query = node.AncestorsAndSelf().OfType().First(); - private static bool InfoBoundSuccessfully(ISymbol operation) + if (InfoBoundSuccessfully(semanticModel.GetQueryClauseInfo(query.FromClause, cancellationToken))) { - operation = operation.GetOriginalUnreducedDefinition(); - return operation != null; + return null; } - protected override string GetDescription(IReadOnlyList nameParts) - => $"using {string.Join(".", nameParts)};"; + foreach (var clause in query.Body.Clauses) + { + if (InfoBoundSuccessfully(semanticModel.GetQueryClauseInfo(clause, cancellationToken))) + { + return null; + } + } - protected override (string description, bool hasExistingImport) GetDescription( - Document document, - AddImportPlacementOptions options, - INamespaceOrTypeSymbol namespaceOrTypeSymbol, - SemanticModel semanticModel, - SyntaxNode contextNode, - CancellationToken cancellationToken) + if (InfoBoundSuccessfully(semanticModel.GetSymbolInfo(query.Body.SelectOrGroup, cancellationToken))) { - var root = GetCompilationUnitSyntaxNode(contextNode, cancellationToken); + return null; + } - // See if this is a reference to a type from a reference that has a specific alias - // associated with it. If that extern alias hasn't already been brought into scope - // then add that one. - var (externAlias, hasExistingExtern) = GetExternAliasDirective( - namespaceOrTypeSymbol, semanticModel, contextNode); + var fromClause = query.FromClause; + return semanticModel.GetTypeInfo(fromClause.Expression, cancellationToken).Type; + } - var (usingDirective, hasExistingUsing) = GetUsingDirective( - document, options, namespaceOrTypeSymbol, semanticModel, root, contextNode); + private static bool InfoBoundSuccessfully(SymbolInfo symbolInfo) + => InfoBoundSuccessfully(symbolInfo.Symbol); - var externAliasString = externAlias != null ? $"extern alias {externAlias.Identifier.ValueText};" : null; - var usingDirectiveString = usingDirective != null ? GetUsingDirectiveString(namespaceOrTypeSymbol) : null; + private static bool InfoBoundSuccessfully(QueryClauseInfo semanticInfo) + => InfoBoundSuccessfully(semanticInfo.OperationInfo); - if (externAlias == null && usingDirective == null) - { - return (null, false); - } + private static bool InfoBoundSuccessfully(ISymbol operation) + { + operation = operation.GetOriginalUnreducedDefinition(); + return operation != null; + } - if (externAlias != null && !hasExistingExtern) - { - return (externAliasString, false); - } + protected override string GetDescription(IReadOnlyList nameParts) + => $"using {string.Join(".", nameParts)};"; - if (usingDirective != null && !hasExistingUsing) - { - return (usingDirectiveString, false); - } + protected override (string description, bool hasExistingImport) GetDescription( + Document document, + AddImportPlacementOptions options, + INamespaceOrTypeSymbol namespaceOrTypeSymbol, + SemanticModel semanticModel, + SyntaxNode contextNode, + CancellationToken cancellationToken) + { + var root = GetCompilationUnitSyntaxNode(contextNode, cancellationToken); - return externAlias != null - ? (externAliasString, hasExistingExtern) - : (usingDirectiveString, hasExistingUsing); - } + // See if this is a reference to a type from a reference that has a specific alias + // associated with it. If that extern alias hasn't already been brought into scope + // then add that one. + var (externAlias, hasExistingExtern) = GetExternAliasDirective( + namespaceOrTypeSymbol, semanticModel, contextNode); + + var (usingDirective, hasExistingUsing) = GetUsingDirective( + document, options, namespaceOrTypeSymbol, semanticModel, root, contextNode); - private static string GetUsingDirectiveString(INamespaceOrTypeSymbol namespaceOrTypeSymbol) + var externAliasString = externAlias != null ? $"extern alias {externAlias.Identifier.ValueText};" : null; + var usingDirectiveString = usingDirective != null ? GetUsingDirectiveString(namespaceOrTypeSymbol) : null; + + if (externAlias == null && usingDirective == null) { - var displayString = namespaceOrTypeSymbol.ToDisplayString(); - return namespaceOrTypeSymbol.IsKind(SymbolKind.Namespace) - ? $"using {displayString};" - : $"using static {displayString};"; + return (null, false); } - protected override async Task AddImportAsync( - SyntaxNode contextNode, - INamespaceOrTypeSymbol namespaceOrTypeSymbol, - Document document, - AddImportPlacementOptions options, - CancellationToken cancellationToken) + if (externAlias != null && !hasExistingExtern) { - var root = GetCompilationUnitSyntaxNode(contextNode, cancellationToken); - var newRoot = await AddImportWorkerAsync(document, root, contextNode, namespaceOrTypeSymbol, options, cancellationToken).ConfigureAwait(false); - return document.WithSyntaxRoot(newRoot); + return (externAliasString, false); } - private static async Task AddImportWorkerAsync( - Document document, CompilationUnitSyntax root, SyntaxNode contextNode, INamespaceOrTypeSymbol namespaceOrTypeSymbol, - AddImportPlacementOptions options, CancellationToken cancellationToken) + if (usingDirective != null && !hasExistingUsing) { - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + return (usingDirectiveString, false); + } + + return externAlias != null + ? (externAliasString, hasExistingExtern) + : (usingDirectiveString, hasExistingUsing); + } + + private static string GetUsingDirectiveString(INamespaceOrTypeSymbol namespaceOrTypeSymbol) + { + var displayString = namespaceOrTypeSymbol.ToDisplayString(); + return namespaceOrTypeSymbol.IsKind(SymbolKind.Namespace) + ? $"using {displayString};" + : $"using static {displayString};"; + } - var (externAliasDirective, hasExistingExtern) = GetExternAliasDirective( - namespaceOrTypeSymbol, semanticModel, contextNode); + protected override async Task AddImportAsync( + SyntaxNode contextNode, + INamespaceOrTypeSymbol namespaceOrTypeSymbol, + Document document, + AddImportPlacementOptions options, + CancellationToken cancellationToken) + { + var root = GetCompilationUnitSyntaxNode(contextNode, cancellationToken); + var newRoot = await AddImportWorkerAsync(document, root, contextNode, namespaceOrTypeSymbol, options, cancellationToken).ConfigureAwait(false); + return document.WithSyntaxRoot(newRoot); + } - var (usingDirective, hasExistingUsing) = GetUsingDirective( - document, options, namespaceOrTypeSymbol, semanticModel, root, contextNode); + private static async Task AddImportWorkerAsync( + Document document, CompilationUnitSyntax root, SyntaxNode contextNode, INamespaceOrTypeSymbol namespaceOrTypeSymbol, + AddImportPlacementOptions options, CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var newImports); + var (externAliasDirective, hasExistingExtern) = GetExternAliasDirective( + namespaceOrTypeSymbol, semanticModel, contextNode); - if (!hasExistingExtern && externAliasDirective != null) - { - newImports.Add(externAliasDirective); - } + var (usingDirective, hasExistingUsing) = GetUsingDirective( + document, options, namespaceOrTypeSymbol, semanticModel, root, contextNode); - if (!hasExistingUsing && usingDirective != null) - { - newImports.Add(usingDirective); - } + using var _ = ArrayBuilder.GetInstance(out var newImports); - if (newImports.Count == 0) - { - return root; - } + if (!hasExistingExtern && externAliasDirective != null) + { + newImports.Add(externAliasDirective); + } - var addImportService = document.GetLanguageService(); - var generator = SyntaxGenerator.GetGenerator(document); - var newRoot = addImportService.AddImports( - semanticModel.Compilation, root, contextNode, newImports, generator, options, cancellationToken); - return (CompilationUnitSyntax)newRoot; + if (!hasExistingUsing && usingDirective != null) + { + newImports.Add(usingDirective); } - protected override async Task AddImportAsync( - SyntaxNode contextNode, IReadOnlyList namespaceParts, - Document document, AddImportPlacementOptions options, CancellationToken cancellationToken) + if (newImports.Count == 0) { - var root = GetCompilationUnitSyntaxNode(contextNode, cancellationToken); + return root; + } - var usingDirective = SyntaxFactory.UsingDirective( - CreateNameSyntax(namespaceParts, namespaceParts.Count - 1)); + var addImportService = document.GetLanguageService(); + var generator = SyntaxGenerator.GetGenerator(document); + var newRoot = addImportService.AddImports( + semanticModel.Compilation, root, contextNode, newImports, generator, options, cancellationToken); + return (CompilationUnitSyntax)newRoot; + } - var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var service = document.GetLanguageService(); - var generator = SyntaxGenerator.GetGenerator(document); - var newRoot = service.AddImport( - compilation, root, contextNode, usingDirective, generator, options, cancellationToken); + protected override async Task AddImportAsync( + SyntaxNode contextNode, IReadOnlyList namespaceParts, + Document document, AddImportPlacementOptions options, CancellationToken cancellationToken) + { + var root = GetCompilationUnitSyntaxNode(contextNode, cancellationToken); - return document.WithSyntaxRoot(newRoot); - } + var usingDirective = SyntaxFactory.UsingDirective( + CreateNameSyntax(namespaceParts, namespaceParts.Count - 1)); - private static NameSyntax CreateNameSyntax(IReadOnlyList namespaceParts, int index) - { - var part = namespaceParts[index]; - if (SyntaxFacts.GetKeywordKind(part) != SyntaxKind.None) - { - part = "@" + part; - } + var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + var service = document.GetLanguageService(); + var generator = SyntaxGenerator.GetGenerator(document); + var newRoot = service.AddImport( + compilation, root, contextNode, usingDirective, generator, options, cancellationToken); - var namePiece = SyntaxFactory.IdentifierName(part); - return index == 0 - ? namePiece - : SyntaxFactory.QualifiedName(CreateNameSyntax(namespaceParts, index - 1), namePiece); - } + return document.WithSyntaxRoot(newRoot); + } - private static (ExternAliasDirectiveSyntax, bool hasExistingImport) GetExternAliasDirective( - INamespaceOrTypeSymbol namespaceSymbol, - SemanticModel semanticModel, - SyntaxNode contextNode) + private static NameSyntax CreateNameSyntax(IReadOnlyList namespaceParts, int index) + { + var part = namespaceParts[index]; + if (SyntaxFacts.GetKeywordKind(part) != SyntaxKind.None) { - var (val, hasExistingExtern) = GetExternAliasString(namespaceSymbol, semanticModel, contextNode); - if (val == null) - { - return (null, false); - } - - return (SyntaxFactory.ExternAliasDirective(SyntaxFactory.Identifier(val)) - .WithAdditionalAnnotations(Formatter.Annotation), - hasExistingExtern); + part = "@" + part; } - private static (UsingDirectiveSyntax, bool hasExistingImport) GetUsingDirective( - Document document, - AddImportPlacementOptions options, - INamespaceOrTypeSymbol namespaceOrTypeSymbol, - SemanticModel semanticModel, - CompilationUnitSyntax root, - SyntaxNode contextNode) - { - var addImportService = document.GetLanguageService(); - var generator = SyntaxGenerator.GetGenerator(document); + var namePiece = SyntaxFactory.IdentifierName(part); + return index == 0 + ? namePiece + : SyntaxFactory.QualifiedName(CreateNameSyntax(namespaceParts, index - 1), namePiece); + } - var nameSyntax = namespaceOrTypeSymbol.GenerateNameSyntax(); + private static (ExternAliasDirectiveSyntax, bool hasExistingImport) GetExternAliasDirective( + INamespaceOrTypeSymbol namespaceSymbol, + SemanticModel semanticModel, + SyntaxNode contextNode) + { + var (val, hasExistingExtern) = GetExternAliasString(namespaceSymbol, semanticModel, contextNode); + if (val == null) + { + return (null, false); + } - // We need to create our using in two passes. This is because we need a using - // directive so we can figure out where to put it. Then, once we figure out - // where to put it, we might need to change it a bit (e.g. removing 'global' - // from it if necessary). So we first create a dummy using directive just to - // determine which container we're going in. Then we'll use the container to - // help create the final using. - var dummyUsing = SyntaxFactory.UsingDirective(nameSyntax); + return (SyntaxFactory.ExternAliasDirective(SyntaxFactory.Identifier(val)) + .WithAdditionalAnnotations(Formatter.Annotation), + hasExistingExtern); + } - var container = addImportService.GetImportContainer(root, contextNode, dummyUsing, options); - var namespaceToAddTo = container as BaseNamespaceDeclarationSyntax; + private static (UsingDirectiveSyntax, bool hasExistingImport) GetUsingDirective( + Document document, + AddImportPlacementOptions options, + INamespaceOrTypeSymbol namespaceOrTypeSymbol, + SemanticModel semanticModel, + CompilationUnitSyntax root, + SyntaxNode contextNode) + { + var addImportService = document.GetLanguageService(); + var generator = SyntaxGenerator.GetGenerator(document); - // Replace the alias that GenerateTypeSyntax added if we want this to be looked - // up off of an extern alias. - var (externAliasDirective, _) = GetExternAliasDirective( - namespaceOrTypeSymbol, semanticModel, contextNode); + var nameSyntax = namespaceOrTypeSymbol.GenerateNameSyntax(); - var externAlias = externAliasDirective?.Identifier.ValueText; - if (externAlias != null) - { - nameSyntax = AddOrReplaceAlias(nameSyntax, SyntaxFactory.IdentifierName(externAlias)); - } - else - { - // The name we generated will have the global:: alias on it. We only need - // that if the name of our symbol is actually ambiguous in this context. - // If so, keep global:: on it, otherwise remove it. - // - // Note: doing this has a couple of benefits. First, it's easy for us to see - // if we have an existing using for this with the same syntax. Second, - // it's easy to sort usings properly. If "global::" was attached to the - // using directive, then it would make both of those operations more difficult - // to achieve. - nameSyntax = RemoveGlobalAliasIfUnnecessary(semanticModel, nameSyntax, namespaceToAddTo); - } + // We need to create our using in two passes. This is because we need a using + // directive so we can figure out where to put it. Then, once we figure out + // where to put it, we might need to change it a bit (e.g. removing 'global' + // from it if necessary). So we first create a dummy using directive just to + // determine which container we're going in. Then we'll use the container to + // help create the final using. + var dummyUsing = SyntaxFactory.UsingDirective(nameSyntax); - var usingDirective = SyntaxFactory.UsingDirective(nameSyntax) - .WithAdditionalAnnotations(Formatter.Annotation); + var container = addImportService.GetImportContainer(root, contextNode, dummyUsing, options); + var namespaceToAddTo = container as BaseNamespaceDeclarationSyntax; - usingDirective = namespaceOrTypeSymbol.IsKind(SymbolKind.Namespace) - ? usingDirective - : usingDirective.WithStaticKeyword(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); + // Replace the alias that GenerateTypeSyntax added if we want this to be looked + // up off of an extern alias. + var (externAliasDirective, _) = GetExternAliasDirective( + namespaceOrTypeSymbol, semanticModel, contextNode); - return (usingDirective, addImportService.HasExistingImport(semanticModel.Compilation, root, contextNode, usingDirective, generator)); + var externAlias = externAliasDirective?.Identifier.ValueText; + if (externAlias != null) + { + nameSyntax = AddOrReplaceAlias(nameSyntax, SyntaxFactory.IdentifierName(externAlias)); + } + else + { + // The name we generated will have the global:: alias on it. We only need + // that if the name of our symbol is actually ambiguous in this context. + // If so, keep global:: on it, otherwise remove it. + // + // Note: doing this has a couple of benefits. First, it's easy for us to see + // if we have an existing using for this with the same syntax. Second, + // it's easy to sort usings properly. If "global::" was attached to the + // using directive, then it would make both of those operations more difficult + // to achieve. + nameSyntax = RemoveGlobalAliasIfUnnecessary(semanticModel, nameSyntax, namespaceToAddTo); } - private static NameSyntax RemoveGlobalAliasIfUnnecessary( - SemanticModel semanticModel, - NameSyntax nameSyntax, - BaseNamespaceDeclarationSyntax namespaceToAddTo) + var usingDirective = SyntaxFactory.UsingDirective(nameSyntax) + .WithAdditionalAnnotations(Formatter.Annotation); + + usingDirective = namespaceOrTypeSymbol.IsKind(SymbolKind.Namespace) + ? usingDirective + : usingDirective.WithStaticKeyword(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); + + return (usingDirective, addImportService.HasExistingImport(semanticModel.Compilation, root, contextNode, usingDirective, generator)); + } + + private static NameSyntax RemoveGlobalAliasIfUnnecessary( + SemanticModel semanticModel, + NameSyntax nameSyntax, + BaseNamespaceDeclarationSyntax namespaceToAddTo) + { + var aliasQualifiedName = nameSyntax.DescendantNodesAndSelf() + .OfType() + .FirstOrDefault(); + if (aliasQualifiedName != null) { - var aliasQualifiedName = nameSyntax.DescendantNodesAndSelf() - .OfType() - .FirstOrDefault(); - if (aliasQualifiedName != null) + var rightOfAliasName = aliasQualifiedName.Name.Identifier.ValueText; + if (!ConflictsWithExistingMember(semanticModel, namespaceToAddTo, rightOfAliasName)) { - var rightOfAliasName = aliasQualifiedName.Name.Identifier.ValueText; - if (!ConflictsWithExistingMember(semanticModel, namespaceToAddTo, rightOfAliasName)) - { - // Strip off the alias. - return nameSyntax.ReplaceNode(aliasQualifiedName, aliasQualifiedName.Name); - } + // Strip off the alias. + return nameSyntax.ReplaceNode(aliasQualifiedName, aliasQualifiedName.Name); } - - return nameSyntax; } - private static bool ConflictsWithExistingMember( - SemanticModel semanticModel, - BaseNamespaceDeclarationSyntax namespaceToAddTo, - string rightOfAliasName) + return nameSyntax; + } + + private static bool ConflictsWithExistingMember( + SemanticModel semanticModel, + BaseNamespaceDeclarationSyntax namespaceToAddTo, + string rightOfAliasName) + { + if (namespaceToAddTo != null) { - if (namespaceToAddTo != null) - { - var containingNamespaceSymbol = semanticModel.GetDeclaredSymbol(namespaceToAddTo); + var containingNamespaceSymbol = semanticModel.GetDeclaredSymbol(namespaceToAddTo); - while (containingNamespaceSymbol != null && !containingNamespaceSymbol.IsGlobalNamespace) + while (containingNamespaceSymbol != null && !containingNamespaceSymbol.IsGlobalNamespace) + { + if (containingNamespaceSymbol.GetMembers(rightOfAliasName).Any()) { - if (containingNamespaceSymbol.GetMembers(rightOfAliasName).Any()) - { - // A containing namespace had this name in it. We need to stay globally qualified. - return true; - } - - containingNamespaceSymbol = containingNamespaceSymbol.ContainingNamespace; + // A containing namespace had this name in it. We need to stay globally qualified. + return true; } + + containingNamespaceSymbol = containingNamespaceSymbol.ContainingNamespace; } + } - // Didn't conflict with anything. We should remove the global:: alias. - return false; + // Didn't conflict with anything. We should remove the global:: alias. + return false; + } + + private static NameSyntax AddOrReplaceAlias( + NameSyntax nameSyntax, IdentifierNameSyntax alias) + { + if (nameSyntax is SimpleNameSyntax simpleName) + { + return SyntaxFactory.AliasQualifiedName(alias, simpleName); } - private static NameSyntax AddOrReplaceAlias( - NameSyntax nameSyntax, IdentifierNameSyntax alias) + if (nameSyntax is QualifiedNameSyntax qualifiedName) { - if (nameSyntax is SimpleNameSyntax simpleName) - { - return SyntaxFactory.AliasQualifiedName(alias, simpleName); - } + return qualifiedName.WithLeft(AddOrReplaceAlias(qualifiedName.Left, alias)); + } - if (nameSyntax is QualifiedNameSyntax qualifiedName) - { - return qualifiedName.WithLeft(AddOrReplaceAlias(qualifiedName.Left, alias)); - } + var aliasName = nameSyntax as AliasQualifiedNameSyntax; + return aliasName.WithAlias(alias); + } - var aliasName = nameSyntax as AliasQualifiedNameSyntax; - return aliasName.WithAlias(alias); + private static (string, bool hasExistingImport) GetExternAliasString( + INamespaceOrTypeSymbol namespaceSymbol, + SemanticModel semanticModel, + SyntaxNode contextNode) + { + string externAliasString = null; + var metadataReference = semanticModel.Compilation.GetMetadataReference(namespaceSymbol.ContainingAssembly); + if (metadataReference == null) + { + return (null, false); } - private static (string, bool hasExistingImport) GetExternAliasString( - INamespaceOrTypeSymbol namespaceSymbol, - SemanticModel semanticModel, - SyntaxNode contextNode) + var aliases = metadataReference.Properties.Aliases; + if (aliases.IsEmpty) { - string externAliasString = null; - var metadataReference = semanticModel.Compilation.GetMetadataReference(namespaceSymbol.ContainingAssembly); - if (metadataReference == null) - { - return (null, false); - } - - var aliases = metadataReference.Properties.Aliases; - if (aliases.IsEmpty) - { - return (null, false); - } - - aliases = metadataReference.Properties.Aliases.Where(a => a != MetadataReferenceProperties.GlobalAlias).ToImmutableArray(); - if (!aliases.Any()) - { - return (null, false); - } + return (null, false); + } - // Just default to using the first alias we see for this symbol. - externAliasString = aliases.First(); - return (externAliasString, HasExistingExternAlias(externAliasString, contextNode)); + aliases = metadataReference.Properties.Aliases.Where(a => a != MetadataReferenceProperties.GlobalAlias).ToImmutableArray(); + if (!aliases.Any()) + { + return (null, false); } - private static bool HasExistingExternAlias(string alias, SyntaxNode contextNode) + // Just default to using the first alias we see for this symbol. + externAliasString = aliases.First(); + return (externAliasString, HasExistingExternAlias(externAliasString, contextNode)); + } + + private static bool HasExistingExternAlias(string alias, SyntaxNode contextNode) + { + foreach (var externAlias in contextNode.GetEnclosingExternAliasDirectives()) { - foreach (var externAlias in contextNode.GetEnclosingExternAliasDirectives()) + // We already have this extern alias in scope. No need to add it. + if (externAlias.Identifier.ValueText == alias) { - // We already have this extern alias in scope. No need to add it. - if (externAlias.Identifier.ValueText == alias) - { - return true; - } + return true; } - - return false; } - private static CompilationUnitSyntax GetCompilationUnitSyntaxNode( - SyntaxNode contextNode, CancellationToken cancellationToken) - { - return (CompilationUnitSyntax)contextNode.SyntaxTree.GetRoot(cancellationToken); - } + return false; + } - protected override bool IsViableExtensionMethod(IMethodSymbol method, SyntaxNode expression, SemanticModel semanticModel, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) + private static CompilationUnitSyntax GetCompilationUnitSyntaxNode( + SyntaxNode contextNode, CancellationToken cancellationToken) + { + return (CompilationUnitSyntax)contextNode.SyntaxTree.GetRoot(cancellationToken); + } + + protected override bool IsViableExtensionMethod(IMethodSymbol method, SyntaxNode expression, SemanticModel semanticModel, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) + { + var leftExpression = syntaxFacts.IsMemberAccessExpression(expression) + ? syntaxFacts.GetExpressionOfMemberAccessExpression(expression) + : syntaxFacts.GetTargetOfMemberBinding(expression); + if (leftExpression == null) { - var leftExpression = syntaxFacts.IsMemberAccessExpression(expression) - ? syntaxFacts.GetExpressionOfMemberAccessExpression(expression) - : syntaxFacts.GetTargetOfMemberBinding(expression); - if (leftExpression == null) + if (expression.IsKind(SyntaxKind.CollectionInitializerExpression)) { - if (expression.IsKind(SyntaxKind.CollectionInitializerExpression)) - { - leftExpression = expression.GetAncestor(); - } - else - { - return false; - } + leftExpression = expression.GetAncestor(); + } + else + { + return false; } + } - var semanticInfo = semanticModel.GetTypeInfo(leftExpression, cancellationToken); - var leftExpressionType = semanticInfo.Type; + var semanticInfo = semanticModel.GetTypeInfo(leftExpression, cancellationToken); + var leftExpressionType = semanticInfo.Type; - return IsViableExtensionMethod(method, leftExpressionType); - } + return IsViableExtensionMethod(method, leftExpressionType); + } - protected override bool IsAddMethodContext(SyntaxNode node, SemanticModel semanticModel) + protected override bool IsAddMethodContext(SyntaxNode node, SemanticModel semanticModel) + { + if (node.Parent.IsKind(SyntaxKind.CollectionInitializerExpression)) { - if (node.Parent.IsKind(SyntaxKind.CollectionInitializerExpression)) + var objectCreationExpressionSyntax = node.GetAncestor(); + if (objectCreationExpressionSyntax == null) { - var objectCreationExpressionSyntax = node.GetAncestor(); - if (objectCreationExpressionSyntax == null) - { - return false; - } - - return true; + return false; } - return false; + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/AddImport/CSharpAddMissingImportsFeatureService.cs b/src/Features/CSharp/Portable/AddImport/CSharpAddMissingImportsFeatureService.cs index 2e86ccf03779b..2bcd747918e87 100644 --- a/src/Features/CSharp/Portable/AddImport/CSharpAddMissingImportsFeatureService.cs +++ b/src/Features/CSharp/Portable/AddImport/CSharpAddMissingImportsFeatureService.cs @@ -14,20 +14,19 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.AddMissingImports +namespace Microsoft.CodeAnalysis.CSharp.AddMissingImports; + +[ExportLanguageService(typeof(IAddMissingImportsFeatureService), LanguageNames.CSharp), Shared] +internal class CSharpAddMissingImportsFeatureService : AbstractAddMissingImportsFeatureService { - [ExportLanguageService(typeof(IAddMissingImportsFeatureService), LanguageNames.CSharp), Shared] - internal class CSharpAddMissingImportsFeatureService : AbstractAddMissingImportsFeatureService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpAddMissingImportsFeatureService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpAddMissingImportsFeatureService() - { - } + } - protected sealed override ImmutableArray FixableDiagnosticIds => AddImportDiagnosticIds.FixableDiagnosticIds; + protected sealed override ImmutableArray FixableDiagnosticIds => AddImportDiagnosticIds.FixableDiagnosticIds; - protected override ImmutableArray GetFormatRules(SourceText text) - => [new CleanUpNewLinesFormatter(text), new IndentBlockFormattingRule()]; - } + protected override ImmutableArray GetFormatRules(SourceText text) + => [new CleanUpNewLinesFormatter(text), new IndentBlockFormattingRule()]; } diff --git a/src/Features/CSharp/Portable/AddMissingReference/CSharpAddMissingReferenceCodeFixProvider.cs b/src/Features/CSharp/Portable/AddMissingReference/CSharpAddMissingReferenceCodeFixProvider.cs index 4436a58ddee68..abe2730970a06 100644 --- a/src/Features/CSharp/Portable/AddMissingReference/CSharpAddMissingReferenceCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/AddMissingReference/CSharpAddMissingReferenceCodeFixProvider.cs @@ -12,30 +12,29 @@ using Microsoft.CodeAnalysis.Packaging; using Microsoft.CodeAnalysis.SymbolSearch; -namespace Microsoft.CodeAnalysis.CSharp.AddMissingReference +namespace Microsoft.CodeAnalysis.CSharp.AddMissingReference; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddMissingReference), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.SimplifyNames)] +internal class CSharpAddMissingReferenceCodeFixProvider : AbstractAddMissingReferenceCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddMissingReference), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.SimplifyNames)] - internal class CSharpAddMissingReferenceCodeFixProvider : AbstractAddMissingReferenceCodeFixProvider - { - private const string CS0012 = nameof(CS0012); // The type 'A' is defined in an assembly that is not referenced. You must add a reference to assembly 'ProjectA, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'. + private const string CS0012 = nameof(CS0012); // The type 'A' is defined in an assembly that is not referenced. You must add a reference to assembly 'ProjectA, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'. - public sealed override ImmutableArray FixableDiagnosticIds { get; } - = [CS0012]; + public sealed override ImmutableArray FixableDiagnosticIds { get; } + = [CS0012]; - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddMissingReferenceCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddMissingReferenceCodeFixProvider() + { + } - /// For testing purposes only (so that tests can pass in mock values) - [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] - internal CSharpAddMissingReferenceCodeFixProvider( - IPackageInstallerService installerService, - ISymbolSearchService symbolSearchService) - : base(installerService, symbolSearchService) - { - } + /// For testing purposes only (so that tests can pass in mock values) + [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] + internal CSharpAddMissingReferenceCodeFixProvider( + IPackageInstallerService installerService, + ISymbolSearchService symbolSearchService) + : base(installerService, symbolSearchService) + { } } diff --git a/src/Features/CSharp/Portable/AddPackage/CSharpAddSpecificPackageCodeFixProvider.cs b/src/Features/CSharp/Portable/AddPackage/CSharpAddSpecificPackageCodeFixProvider.cs index 4459f7ce551c3..b303bac2c753f 100644 --- a/src/Features/CSharp/Portable/AddPackage/CSharpAddSpecificPackageCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/AddPackage/CSharpAddSpecificPackageCodeFixProvider.cs @@ -11,30 +11,29 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.AddPackage +namespace Microsoft.CodeAnalysis.CSharp.AddPackage; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddPackage), Shared] +internal class CSharpAddSpecificPackageCodeFixProvider : AbstractAddSpecificPackageCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddPackage), Shared] - internal class CSharpAddSpecificPackageCodeFixProvider : AbstractAddSpecificPackageCodeFixProvider - { - private const string CS8179 = nameof(CS8179); // Predefined type 'System.ValueTuple`2' is not defined or imported + private const string CS8179 = nameof(CS8179); // Predefined type 'System.ValueTuple`2' is not defined or imported - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpAddSpecificPackageCodeFixProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpAddSpecificPackageCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds - => [CS8179]; + public override ImmutableArray FixableDiagnosticIds + => [CS8179]; - protected override string GetAssemblyName(string id) + protected override string GetAssemblyName(string id) + { + switch (id) { - switch (id) - { - case CS8179: return "System.ValueTuple"; - } - - return null; + case CS8179: return "System.ValueTuple"; } + + return null; } } diff --git a/src/Features/CSharp/Portable/BraceCompletion/AbstractCSharpBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/AbstractCSharpBraceCompletionService.cs index 3e0cc7c900725..ad956ae70fa17 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/AbstractCSharpBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/AbstractCSharpBraceCompletionService.cs @@ -2,14 +2,24 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Threading; using Microsoft.CodeAnalysis.BraceCompletion; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.LanguageService; using Microsoft.CodeAnalysis.LanguageService; +using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion +namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion; + +internal abstract class AbstractCSharpBraceCompletionService : AbstractBraceCompletionService { - internal abstract class AbstractCSharpBraceCompletionService : AbstractBraceCompletionService + protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; + + protected static bool IsLegalExpressionLocation(SyntaxTree tree, int position, CancellationToken cancellationToken) { - protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; + var leftToken = tree.FindTokenOnLeftOfPosition(position, cancellationToken); + return tree.IsExpressionContext(position, leftToken, attributes: true, cancellationToken) + || tree.IsStatementContext(position, leftToken, cancellationToken) + || tree.IsGlobalStatementContext(position, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/BraceCompletion/AbstractCurlyBraceOrBracketCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/AbstractCurlyBraceOrBracketCompletionService.cs index f5d0a53f6d984..cadacf5d61fcd 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/AbstractCurlyBraceOrBracketCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/AbstractCurlyBraceOrBracketCompletionService.cs @@ -16,261 +16,260 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion +namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion; + +internal abstract class AbstractCurlyBraceOrBracketCompletionService : AbstractCSharpBraceCompletionService { - internal abstract class AbstractCurlyBraceOrBracketCompletionService : AbstractCSharpBraceCompletionService + /// + /// Annotation used to find the closing brace location after formatting changes are applied. + /// The closing brace location is then used as the caret location. + /// + private static readonly SyntaxAnnotation s_closingBraceFormatAnnotation = new(nameof(s_closingBraceFormatAnnotation)); + private static readonly SyntaxAnnotation s_closingBraceNewlineAnnotation = new(nameof(s_closingBraceNewlineAnnotation)); + + protected abstract ImmutableArray GetBraceFormattingIndentationRulesAfterReturn(IndentationOptions options); + + protected abstract int AdjustFormattingEndPoint(ParsedDocument document, int startPoint, int endPoint); + + public sealed override BraceCompletionResult? GetTextChangesAfterCompletion(BraceCompletionContext context, IndentationOptions options, CancellationToken cancellationToken) { - /// - /// Annotation used to find the closing brace location after formatting changes are applied. - /// The closing brace location is then used as the caret location. - /// - private static readonly SyntaxAnnotation s_closingBraceFormatAnnotation = new(nameof(s_closingBraceFormatAnnotation)); - private static readonly SyntaxAnnotation s_closingBraceNewlineAnnotation = new(nameof(s_closingBraceNewlineAnnotation)); + // After the closing brace is completed we need to format the span from the opening point to the closing point. + // E.g. when the user triggers completion for an if statement ($$ is the caret location) we insert braces to get + // if (true){$$} + // We then need to format this to + // if (true) { $$} - protected abstract ImmutableArray GetBraceFormattingIndentationRulesAfterReturn(IndentationOptions options); + if (!options.AutoFormattingOptions.FormatOnCloseBrace) + { + return null; + } - protected abstract int AdjustFormattingEndPoint(ParsedDocument document, int startPoint, int endPoint); + var (_, formattingChanges, finalCurlyBraceEnd) = FormatTrackingSpan( + context.Document, + context.OpeningPoint, + context.ClosingPoint, + // We're not trying to format the indented block here, so no need to pass in additional rules. + braceFormattingIndentationRules: [], + options, + cancellationToken); - public sealed override BraceCompletionResult? GetTextChangesAfterCompletion(BraceCompletionContext context, IndentationOptions options, CancellationToken cancellationToken) + if (formattingChanges.IsEmpty) { - // After the closing brace is completed we need to format the span from the opening point to the closing point. - // E.g. when the user triggers completion for an if statement ($$ is the caret location) we insert braces to get - // if (true){$$} - // We then need to format this to - // if (true) { $$} + return null; + } - if (!options.AutoFormattingOptions.FormatOnCloseBrace) - { - return null; - } + // The caret location should be at the start of the closing brace character. + var formattedText = context.Document.Text.WithChanges(formattingChanges); + var caretLocation = formattedText.Lines.GetLinePosition(finalCurlyBraceEnd - 1); - var (_, formattingChanges, finalCurlyBraceEnd) = FormatTrackingSpan( - context.Document, - context.OpeningPoint, - context.ClosingPoint, - // We're not trying to format the indented block here, so no need to pass in additional rules. - braceFormattingIndentationRules: [], - options, - cancellationToken); + return new BraceCompletionResult(formattingChanges, caretLocation); + } - if (formattingChanges.IsEmpty) + private static bool ContainsOnlyWhitespace(SourceText text, int openingPosition, int closingBraceEndPoint) + { + // Set the start point to the character after the opening brace. + var start = openingPosition + 1; + // Set the end point to the closing brace start character position. + var end = closingBraceEndPoint - 1; + + for (var i = start; i < end; i++) + { + if (!char.IsWhiteSpace(text[i])) { - return null; + return false; } - - // The caret location should be at the start of the closing brace character. - var formattedText = context.Document.Text.WithChanges(formattingChanges); - var caretLocation = formattedText.Lines.GetLinePosition(finalCurlyBraceEnd - 1); - - return new BraceCompletionResult(formattingChanges, caretLocation); } - private static bool ContainsOnlyWhitespace(SourceText text, int openingPosition, int closingBraceEndPoint) + return true; + } + + public sealed override BraceCompletionResult? GetTextChangeAfterReturn( + BraceCompletionContext context, + IndentationOptions options, + CancellationToken cancellationToken) + { + var document = context.Document; + var closingPoint = context.ClosingPoint; + var openingPoint = context.OpeningPoint; + var originalDocumentText = document.Text; + + // check whether shape of the braces are what we support + // shape must be either "{|}" or "{ }". | is where caret is. otherwise, we don't do any special behavior + if (!ContainsOnlyWhitespace(originalDocumentText, openingPoint, closingPoint)) { - // Set the start point to the character after the opening brace. - var start = openingPosition + 1; - // Set the end point to the closing brace start character position. - var end = closingBraceEndPoint - 1; + return null; + } - for (var i = start; i < end; i++) - { - if (!char.IsWhiteSpace(text[i])) - { - return false; - } - } + var openingPointLine = originalDocumentText.Lines.GetLineFromPosition(openingPoint).LineNumber; + var closingPointLine = originalDocumentText.Lines.GetLineFromPosition(closingPoint).LineNumber; - return true; + // If there are already multiple empty lines between the braces, don't do anything. + // We need to allow a single empty line between the braces to account for razor scenarios where they insert a line. + if (closingPointLine - openingPointLine > 2) + { + return null; } - public sealed override BraceCompletionResult? GetTextChangeAfterReturn( - BraceCompletionContext context, - IndentationOptions options, - CancellationToken cancellationToken) + // If there is not already an empty line inserted between the braces, insert one. + TextChange? newLineEdit = null; + if (closingPointLine - openingPointLine == 1) { - var document = context.Document; - var closingPoint = context.ClosingPoint; - var openingPoint = context.OpeningPoint; - var originalDocumentText = document.Text; - - // check whether shape of the braces are what we support - // shape must be either "{|}" or "{ }". | is where caret is. otherwise, we don't do any special behavior - if (!ContainsOnlyWhitespace(originalDocumentText, openingPoint, closingPoint)) - { - return null; - } + // Handling syntax tree directly to avoid parsing in potentially UI blocking code-path + var closingToken = FindClosingBraceToken(document.Root, closingPoint); + var annotatedNewline = SyntaxFactory.EndOfLine(options.FormattingOptions.NewLine).WithAdditionalAnnotations(s_closingBraceNewlineAnnotation); + var newClosingToken = closingToken.WithPrependedLeadingTrivia(SpecializedCollections.SingletonEnumerable(annotatedNewline)); + + var rootToFormat = document.Root.ReplaceToken(closingToken, newClosingToken); + annotatedNewline = rootToFormat.GetAnnotatedTrivia(s_closingBraceNewlineAnnotation).Single(); + document = document.WithChangedRoot(rootToFormat, cancellationToken); + + // Calculate text change for adding a newline and adjust closing point location. + closingPoint = annotatedNewline.Token.Span.End; + newLineEdit = new TextChange(new TextSpan(annotatedNewline.SpanStart, 0), annotatedNewline.ToString()); + } - var openingPointLine = originalDocumentText.Lines.GetLineFromPosition(openingPoint).LineNumber; - var closingPointLine = originalDocumentText.Lines.GetLineFromPosition(closingPoint).LineNumber; + // Format the text that contains the newly inserted line. + var (formattedRoot, formattingChanges, newClosingPoint) = FormatTrackingSpan( + document, + openingPoint, + closingPoint, + braceFormattingIndentationRules: GetBraceFormattingIndentationRulesAfterReturn(options), + options, + cancellationToken); - // If there are already multiple empty lines between the braces, don't do anything. - // We need to allow a single empty line between the braces to account for razor scenarios where they insert a line. - if (closingPointLine - openingPointLine > 2) - { - return null; - } + var newDocument = document.WithChangedRoot(formattedRoot, cancellationToken); - // If there is not already an empty line inserted between the braces, insert one. - TextChange? newLineEdit = null; - if (closingPointLine - openingPointLine == 1) - { - // Handling syntax tree directly to avoid parsing in potentially UI blocking code-path - var closingToken = FindClosingBraceToken(document.Root, closingPoint); - var annotatedNewline = SyntaxFactory.EndOfLine(options.FormattingOptions.NewLine).WithAdditionalAnnotations(s_closingBraceNewlineAnnotation); - var newClosingToken = closingToken.WithPrependedLeadingTrivia(SpecializedCollections.SingletonEnumerable(annotatedNewline)); - - var rootToFormat = document.Root.ReplaceToken(closingToken, newClosingToken); - annotatedNewline = rootToFormat.GetAnnotatedTrivia(s_closingBraceNewlineAnnotation).Single(); - document = document.WithChangedRoot(rootToFormat, cancellationToken); - - // Calculate text change for adding a newline and adjust closing point location. - closingPoint = annotatedNewline.Token.Span.End; - newLineEdit = new TextChange(new TextSpan(annotatedNewline.SpanStart, 0), annotatedNewline.ToString()); - } + // Get the empty line between the curly braces. + var desiredCaretLine = GetLineBetweenCurlys(newClosingPoint, newDocument.Text); + Debug.Assert(desiredCaretLine.GetFirstNonWhitespacePosition() == null, "the line between the formatted braces is not empty"); - // Format the text that contains the newly inserted line. - var (formattedRoot, formattingChanges, newClosingPoint) = FormatTrackingSpan( - document, - openingPoint, - closingPoint, - braceFormattingIndentationRules: GetBraceFormattingIndentationRulesAfterReturn(options), - options, - cancellationToken); + // Set the caret position to the properly indented column in the desired line. + var caretPosition = GetIndentedLinePosition(newDocument, newDocument.Text, desiredCaretLine.LineNumber, options, cancellationToken); - var newDocument = document.WithChangedRoot(formattedRoot, cancellationToken); + return new BraceCompletionResult(GetMergedChanges(newLineEdit, formattingChanges, newDocument.Text), caretPosition); - // Get the empty line between the curly braces. - var desiredCaretLine = GetLineBetweenCurlys(newClosingPoint, newDocument.Text); - Debug.Assert(desiredCaretLine.GetFirstNonWhitespacePosition() == null, "the line between the formatted braces is not empty"); + static TextLine GetLineBetweenCurlys(int closingPosition, SourceText text) + { + var closingBraceLineNumber = text.Lines.GetLineFromPosition(closingPosition - 1).LineNumber; + return text.Lines[closingBraceLineNumber - 1]; + } - // Set the caret position to the properly indented column in the desired line. - var caretPosition = GetIndentedLinePosition(newDocument, newDocument.Text, desiredCaretLine.LineNumber, options, cancellationToken); + static LinePosition GetIndentedLinePosition(ParsedDocument document, SourceText sourceText, int lineNumber, IndentationOptions options, CancellationToken cancellationToken) + { + var indentationService = document.LanguageServices.GetRequiredService(); + var indentation = indentationService.GetIndentation(document, lineNumber, options, cancellationToken); + + var baseLinePosition = sourceText.Lines.GetLinePosition(indentation.BasePosition); + var offsetOfBasePosition = baseLinePosition.Character; + var totalOffset = offsetOfBasePosition + indentation.Offset; + var indentedLinePosition = new LinePosition(lineNumber, totalOffset); + return indentedLinePosition; + } - return new BraceCompletionResult(GetMergedChanges(newLineEdit, formattingChanges, newDocument.Text), caretPosition); + static ImmutableArray GetMergedChanges(TextChange? newLineEdit, ImmutableArray formattingChanges, SourceText formattedText) + { + // The new line edit is calculated against the original text, d0, to get text d1. + // The formatting edits are calculated against d1 to get text d2. + // Merge the formatting and new line edits into a set of whitespace only text edits that all apply to d0. + if (!newLineEdit.HasValue) + return formattingChanges; + + // Depending on options, we might not get any formatting change. + // In this case, the newline edit is the only change. + if (formattingChanges.IsEmpty) + return [newLineEdit.Value]; - static TextLine GetLineBetweenCurlys(int closingPosition, SourceText text) - { - var closingBraceLineNumber = text.Lines.GetLineFromPosition(closingPosition - 1).LineNumber; - return text.Lines[closingBraceLineNumber - 1]; - } + var newRanges = TextChangeRangeExtensions.Merge( + [newLineEdit.Value.ToTextChangeRange()], + formattingChanges.SelectAsArray(f => f.ToTextChangeRange())); - static LinePosition GetIndentedLinePosition(ParsedDocument document, SourceText sourceText, int lineNumber, IndentationOptions options, CancellationToken cancellationToken) + using var _ = ArrayBuilder.GetInstance(out var mergedChanges); + var amountToShift = 0; + foreach (var newRange in newRanges) { - var indentationService = document.LanguageServices.GetRequiredService(); - var indentation = indentationService.GetIndentation(document, lineNumber, options, cancellationToken); - - var baseLinePosition = sourceText.Lines.GetLinePosition(indentation.BasePosition); - var offsetOfBasePosition = baseLinePosition.Character; - var totalOffset = offsetOfBasePosition + indentation.Offset; - var indentedLinePosition = new LinePosition(lineNumber, totalOffset); - return indentedLinePosition; + var newTextChangeSpan = newRange.Span; + // Get the text to put in the text change by looking at the span in the formatted text. + // As the new range start is relative to the original text, we need to adjust it assuming the previous changes were applied + // to get the correct start location in the formatted text. + // E.g. with changes + // 1. Insert "hello" at 2 + // 2. Insert "goodbye" at 3 + // "goodbye" is after "hello" at location 3 + 5 (length of "hello") in the new text. + var newTextChangeText = formattedText.GetSubText(new TextSpan(newRange.Span.Start + amountToShift, newRange.NewLength)).ToString(); + amountToShift += (newRange.NewLength - newRange.Span.Length); + mergedChanges.Add(new TextChange(newTextChangeSpan, newTextChangeText)); } - static ImmutableArray GetMergedChanges(TextChange? newLineEdit, ImmutableArray formattingChanges, SourceText formattedText) - { - // The new line edit is calculated against the original text, d0, to get text d1. - // The formatting edits are calculated against d1 to get text d2. - // Merge the formatting and new line edits into a set of whitespace only text edits that all apply to d0. - if (!newLineEdit.HasValue) - return formattingChanges; - - // Depending on options, we might not get any formatting change. - // In this case, the newline edit is the only change. - if (formattingChanges.IsEmpty) - return [newLineEdit.Value]; - - var newRanges = TextChangeRangeExtensions.Merge( - [newLineEdit.Value.ToTextChangeRange()], - formattingChanges.SelectAsArray(f => f.ToTextChangeRange())); - - using var _ = ArrayBuilder.GetInstance(out var mergedChanges); - var amountToShift = 0; - foreach (var newRange in newRanges) - { - var newTextChangeSpan = newRange.Span; - // Get the text to put in the text change by looking at the span in the formatted text. - // As the new range start is relative to the original text, we need to adjust it assuming the previous changes were applied - // to get the correct start location in the formatted text. - // E.g. with changes - // 1. Insert "hello" at 2 - // 2. Insert "goodbye" at 3 - // "goodbye" is after "hello" at location 3 + 5 (length of "hello") in the new text. - var newTextChangeText = formattedText.GetSubText(new TextSpan(newRange.Span.Start + amountToShift, newRange.NewLength)).ToString(); - amountToShift += (newRange.NewLength - newRange.Span.Length); - mergedChanges.Add(new TextChange(newTextChangeSpan, newTextChangeText)); - } - - return mergedChanges.ToImmutable(); - } + return mergedChanges.ToImmutable(); } + } - /// - /// Formats the span between the opening and closing points, options permitting. - /// Returns the text changes that should be applied to the input document to - /// get the formatted text and the end of the close curly brace in the formatted text. - /// - private (SyntaxNode formattedRoot, ImmutableArray textChanges, int finalBraceEnd) FormatTrackingSpan( - ParsedDocument document, - int openingPoint, - int closingPoint, - ImmutableArray braceFormattingIndentationRules, - IndentationOptions options, - CancellationToken cancellationToken) - { - var startPoint = openingPoint; - var endPoint = AdjustFormattingEndPoint(document, startPoint, closingPoint); + /// + /// Formats the span between the opening and closing points, options permitting. + /// Returns the text changes that should be applied to the input document to + /// get the formatted text and the end of the close curly brace in the formatted text. + /// + private (SyntaxNode formattedRoot, ImmutableArray textChanges, int finalBraceEnd) FormatTrackingSpan( + ParsedDocument document, + int openingPoint, + int closingPoint, + ImmutableArray braceFormattingIndentationRules, + IndentationOptions options, + CancellationToken cancellationToken) + { + var startPoint = openingPoint; + var endPoint = AdjustFormattingEndPoint(document, startPoint, closingPoint); - if (options.IndentStyle == FormattingOptions2.IndentStyle.Smart) + if (options.IndentStyle == FormattingOptions2.IndentStyle.Smart) + { + // Set the formatting start point to be the beginning of the first word to the left + // of the opening brace location. + // skip whitespace + while (startPoint >= 0 && char.IsWhiteSpace(document.Text[startPoint])) { - // Set the formatting start point to be the beginning of the first word to the left - // of the opening brace location. - // skip whitespace - while (startPoint >= 0 && char.IsWhiteSpace(document.Text[startPoint])) - { - startPoint--; - } - - // skip tokens in the first word to the left. startPoint--; - while (startPoint >= 0 && !char.IsWhiteSpace(document.Text[startPoint])) - { - startPoint--; - } } - var spanToFormat = TextSpan.FromBounds(Math.Max(startPoint, 0), endPoint); - var rules = FormattingRuleUtilities.GetFormattingRules(document, spanToFormat, braceFormattingIndentationRules); - - // Annotate the original closing brace so we can find it after formatting. - var annotatedRoot = GetSyntaxRootWithAnnotatedClosingBrace(document.Root, closingPoint); - - var result = Formatter.GetFormattingResult( - annotatedRoot, SpecializedCollections.SingletonEnumerable(spanToFormat), document.SolutionServices, options.FormattingOptions, rules, cancellationToken); - - if (result == null) + // skip tokens in the first word to the left. + startPoint--; + while (startPoint >= 0 && !char.IsWhiteSpace(document.Text[startPoint])) { - return (document.Root, ImmutableArray.Empty, closingPoint); + startPoint--; } + } - var newRoot = result.GetFormattedRoot(cancellationToken); - var newClosingPoint = newRoot.GetAnnotatedTokens(s_closingBraceFormatAnnotation).Single().SpanStart + 1; + var spanToFormat = TextSpan.FromBounds(Math.Max(startPoint, 0), endPoint); + var rules = FormattingRuleUtilities.GetFormattingRules(document, spanToFormat, braceFormattingIndentationRules); - var textChanges = result.GetTextChanges(cancellationToken).ToImmutableArray(); - return (newRoot, textChanges, newClosingPoint); + // Annotate the original closing brace so we can find it after formatting. + var annotatedRoot = GetSyntaxRootWithAnnotatedClosingBrace(document.Root, closingPoint); - SyntaxNode GetSyntaxRootWithAnnotatedClosingBrace(SyntaxNode originalRoot, int closingBraceEndPoint) - { - var closeBraceToken = FindClosingBraceToken(originalRoot, closingBraceEndPoint); - var newCloseBraceToken = closeBraceToken.WithAdditionalAnnotations(s_closingBraceFormatAnnotation); - return originalRoot.ReplaceToken(closeBraceToken, newCloseBraceToken); - } + var result = Formatter.GetFormattingResult( + annotatedRoot, SpecializedCollections.SingletonEnumerable(spanToFormat), document.SolutionServices, options.FormattingOptions, rules, cancellationToken); + + if (result == null) + { + return (document.Root, ImmutableArray.Empty, closingPoint); } - private SyntaxToken FindClosingBraceToken(SyntaxNode root, int closingBraceEndPoint) + var newRoot = result.GetFormattedRoot(cancellationToken); + var newClosingPoint = newRoot.GetAnnotatedTokens(s_closingBraceFormatAnnotation).Single().SpanStart + 1; + + var textChanges = result.GetTextChanges(cancellationToken).ToImmutableArray(); + return (newRoot, textChanges, newClosingPoint); + + SyntaxNode GetSyntaxRootWithAnnotatedClosingBrace(SyntaxNode originalRoot, int closingBraceEndPoint) { - var closeBraceToken = root.FindToken(closingBraceEndPoint - 1); - Debug.Assert(IsValidClosingBraceToken(closeBraceToken)); - return closeBraceToken; + var closeBraceToken = FindClosingBraceToken(originalRoot, closingBraceEndPoint); + var newCloseBraceToken = closeBraceToken.WithAdditionalAnnotations(s_closingBraceFormatAnnotation); + return originalRoot.ReplaceToken(closeBraceToken, newCloseBraceToken); } } + + private SyntaxToken FindClosingBraceToken(SyntaxNode root, int closingBraceEndPoint) + { + var closeBraceToken = root.FindToken(closingBraceEndPoint - 1); + Debug.Assert(IsValidClosingBraceToken(closeBraceToken)); + return closeBraceToken; + } } diff --git a/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs index 5c024505994a8..35e12b8a3042d 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs @@ -18,64 +18,63 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion +namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion; + +[Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] +internal class BracketBraceCompletionService : AbstractCurlyBraceOrBracketCompletionService { - [Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] - internal class BracketBraceCompletionService : AbstractCurlyBraceOrBracketCompletionService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public BracketBraceCompletionService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public BracketBraceCompletionService() - { - } + } - protected override char OpeningBrace => Bracket.OpenCharacter; + protected override char OpeningBrace => Bracket.OpenCharacter; - protected override char ClosingBrace => Bracket.CloseCharacter; + protected override char ClosingBrace => Bracket.CloseCharacter; - public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) - => AllowOverTypeInUserCodeWithValidClosingToken(context, cancellationToken); + public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) + => AllowOverTypeInUserCodeWithValidClosingToken(context, cancellationToken); - protected override bool IsValidOpeningBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.OpenBracketToken); + protected override bool IsValidOpeningBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.OpenBracketToken); - protected override bool IsValidClosingBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.CloseBracketToken); + protected override bool IsValidClosingBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.CloseBracketToken); - protected override int AdjustFormattingEndPoint(ParsedDocument document, int startPoint, int endPoint) - => endPoint; + protected override int AdjustFormattingEndPoint(ParsedDocument document, int startPoint, int endPoint) + => endPoint; - protected override ImmutableArray GetBraceFormattingIndentationRulesAfterReturn(IndentationOptions options) - { - return [BracketCompletionFormattingRule.Instance]; - } + protected override ImmutableArray GetBraceFormattingIndentationRulesAfterReturn(IndentationOptions options) + { + return [BracketCompletionFormattingRule.Instance]; + } - private sealed class BracketCompletionFormattingRule : BaseFormattingRule - { - public static readonly AbstractFormattingRule Instance = new BracketCompletionFormattingRule(); + private sealed class BracketCompletionFormattingRule : BaseFormattingRule + { + public static readonly AbstractFormattingRule Instance = new BracketCompletionFormattingRule(); - public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + { + if (currentToken.IsKind(SyntaxKind.OpenBracketToken) && currentToken.Parent.IsKind(SyntaxKind.ListPattern)) { - if (currentToken.IsKind(SyntaxKind.OpenBracketToken) && currentToken.Parent.IsKind(SyntaxKind.ListPattern)) - { - // For list patterns we format brackets as though they are a block, so when formatting after Return - // we add a newline - return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines); - } - - return base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation); + // For list patterns we format brackets as though they are a block, so when formatting after Return + // we add a newline + return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines); } - public override void AddAlignTokensOperations(List list, SyntaxNode node, in NextAlignTokensOperationAction nextOperation) - { - base.AddAlignTokensOperations(list, node, in nextOperation); + return base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation); + } + + public override void AddAlignTokensOperations(List list, SyntaxNode node, in NextAlignTokensOperationAction nextOperation) + { + base.AddAlignTokensOperations(list, node, in nextOperation); - var bracketPair = node.GetBracketPair(); - if (bracketPair.IsValidBracketOrBracePair() && node is ListPatternSyntax or CollectionExpressionSyntax) - { - // For list patterns we format brackets as though they are a block, so ensure the close bracket - // is aligned with the open bracket - AddAlignIndentationOfTokensToBaseTokenOperation(list, node, bracketPair.openBracket, - SpecializedCollections.SingletonEnumerable(bracketPair.closeBracket), AlignTokensOption.AlignIndentationOfTokensToFirstTokenOfBaseTokenLine); - } + var bracketPair = node.GetBracketPair(); + if (bracketPair.IsValidBracketOrBracePair() && node is ListPatternSyntax or CollectionExpressionSyntax) + { + // For list patterns we format brackets as though they are a block, so ensure the close bracket + // is aligned with the open bracket + AddAlignIndentationOfTokensToBaseTokenOperation(list, node, bracketPair.openBracket, + SpecializedCollections.SingletonEnumerable(bracketPair.closeBracket), AlignTokensOption.AlignIndentationOfTokensToFirstTokenOfBaseTokenLine); } } } diff --git a/src/Features/CSharp/Portable/BraceCompletion/CharLiteralBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/CharLiteralBraceCompletionService.cs index f5ba3719023ce..c159b79f6818d 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/CharLiteralBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/CharLiteralBraceCompletionService.cs @@ -12,27 +12,25 @@ using Microsoft.CodeAnalysis.LanguageService; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion -{ +namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion; - [Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] - internal class CharLiteralBraceCompletionService : AbstractCSharpBraceCompletionService +[Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] +internal class CharLiteralBraceCompletionService : AbstractCSharpBraceCompletionService +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CharLiteralBraceCompletionService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CharLiteralBraceCompletionService() - { - } + } - protected override char OpeningBrace => SingleQuote.OpenCharacter; + protected override char OpeningBrace => SingleQuote.OpenCharacter; - protected override char ClosingBrace => SingleQuote.CloseCharacter; + protected override char ClosingBrace => SingleQuote.CloseCharacter; - public override bool AllowOverType(BraceCompletionContext braceCompletionContext, CancellationToken cancellationToken) - => AllowOverTypeWithValidClosingToken(braceCompletionContext); + public override bool AllowOverType(BraceCompletionContext braceCompletionContext, CancellationToken cancellationToken) + => AllowOverTypeWithValidClosingToken(braceCompletionContext); - protected override bool IsValidOpeningBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.CharacterLiteralToken); + protected override bool IsValidOpeningBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.CharacterLiteralToken); - protected override bool IsValidClosingBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.CharacterLiteralToken); - } + protected override bool IsValidClosingBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.CharacterLiteralToken); } diff --git a/src/Features/CSharp/Portable/BraceCompletion/CurlyBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/CurlyBraceCompletionService.cs index 8ffb991cd6da3..6551e0b13d9bd 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/CurlyBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/CurlyBraceCompletionService.cs @@ -23,260 +23,259 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion +namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion; + +[Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] +internal class CurlyBraceCompletionService : AbstractCurlyBraceOrBracketCompletionService { - [Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] - internal class CurlyBraceCompletionService : AbstractCurlyBraceOrBracketCompletionService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CurlyBraceCompletionService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CurlyBraceCompletionService() + } + + protected override char OpeningBrace => CurlyBrace.OpenCharacter; + + protected override char ClosingBrace => CurlyBrace.CloseCharacter; + + public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) + => AllowOverTypeInUserCodeWithValidClosingToken(context, cancellationToken); + + public override bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken) + { + // Only potentially valid for curly brace completion if not in an interpolation brace completion context. + if (OpeningBrace == brace && InterpolationBraceCompletionService.IsPositionInInterpolationContext(document, openingPosition)) { + return false; } - protected override char OpeningBrace => CurlyBrace.OpenCharacter; + return base.CanProvideBraceCompletion(brace, openingPosition, document, cancellationToken); + } - protected override char ClosingBrace => CurlyBrace.CloseCharacter; + protected override bool IsValidOpeningBraceToken(SyntaxToken token) + => token.IsKind(SyntaxKind.OpenBraceToken) && !token.Parent.IsKind(SyntaxKind.Interpolation); - public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) - => AllowOverTypeInUserCodeWithValidClosingToken(context, cancellationToken); + protected override bool IsValidClosingBraceToken(SyntaxToken token) + => token.IsKind(SyntaxKind.CloseBraceToken); - public override bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken) + protected override int AdjustFormattingEndPoint(ParsedDocument document, int startPoint, int endPoint) + { + // Only format outside of the completed braces if they're on the same line for array/collection/object initializer expressions. + // Example: `var x = new int[]{}`: + // Correct: `var x = new int[] {}` + // Incorrect: `var x = new int[] { }` + // This is a heuristic to prevent brace completion from breaking user expectation/muscle memory in common scenarios. + // see bug Devdiv:823958 + if (document.Text.Lines.GetLineFromPosition(startPoint) == document.Text.Lines.GetLineFromPosition(endPoint)) { - // Only potentially valid for curly brace completion if not in an interpolation brace completion context. - if (OpeningBrace == brace && InterpolationBraceCompletionService.IsPositionInInterpolationContext(document, openingPosition)) + var startToken = document.Root.FindToken(startPoint, findInsideTrivia: true); + if (IsValidOpeningBraceToken(startToken) && + (startToken.Parent?.IsInitializerForArrayOrCollectionCreationExpression() == true || + startToken.Parent is AnonymousObjectCreationExpressionSyntax)) { - return false; + // Since the braces are next to each other the span to format is everything up to the opening brace start. + endPoint = startToken.SpanStart; } - - return base.CanProvideBraceCompletion(brace, openingPosition, document, cancellationToken); } - protected override bool IsValidOpeningBraceToken(SyntaxToken token) - => token.IsKind(SyntaxKind.OpenBraceToken) && !token.Parent.IsKind(SyntaxKind.Interpolation); + return endPoint; + } + + protected override ImmutableArray GetBraceFormattingIndentationRulesAfterReturn(IndentationOptions options) + { + var indentStyle = options.IndentStyle; + return [BraceCompletionFormattingRule.ForIndentStyle(indentStyle)]; + } - protected override bool IsValidClosingBraceToken(SyntaxToken token) - => token.IsKind(SyntaxKind.CloseBraceToken); + private sealed class BraceCompletionFormattingRule : BaseFormattingRule + { + private static readonly Predicate s_predicate = o => o.Option.IsOn(SuppressOption.NoWrapping); - protected override int AdjustFormattingEndPoint(ParsedDocument document, int startPoint, int endPoint) - { - // Only format outside of the completed braces if they're on the same line for array/collection/object initializer expressions. - // Example: `var x = new int[]{}`: - // Correct: `var x = new int[] {}` - // Incorrect: `var x = new int[] { }` - // This is a heuristic to prevent brace completion from breaking user expectation/muscle memory in common scenarios. - // see bug Devdiv:823958 - if (document.Text.Lines.GetLineFromPosition(startPoint) == document.Text.Lines.GetLineFromPosition(endPoint)) - { - var startToken = document.Root.FindToken(startPoint, findInsideTrivia: true); - if (IsValidOpeningBraceToken(startToken) && - (startToken.Parent?.IsInitializerForArrayOrCollectionCreationExpression() == true || - startToken.Parent is AnonymousObjectCreationExpressionSyntax)) - { - // Since the braces are next to each other the span to format is everything up to the opening brace start. - endPoint = startToken.SpanStart; - } - } + private static readonly ImmutableArray s_instances = + [ + new BraceCompletionFormattingRule(FormattingOptions2.IndentStyle.None), + new BraceCompletionFormattingRule(FormattingOptions2.IndentStyle.Block), + new BraceCompletionFormattingRule(FormattingOptions2.IndentStyle.Smart), + ]; - return endPoint; + private readonly FormattingOptions2.IndentStyle _indentStyle; + private readonly CSharpSyntaxFormattingOptions _options; + + public BraceCompletionFormattingRule(FormattingOptions2.IndentStyle indentStyle) + : this(indentStyle, CSharpSyntaxFormattingOptions.Default) + { } - protected override ImmutableArray GetBraceFormattingIndentationRulesAfterReturn(IndentationOptions options) + private BraceCompletionFormattingRule(FormattingOptions2.IndentStyle indentStyle, CSharpSyntaxFormattingOptions options) { - var indentStyle = options.IndentStyle; - return [BraceCompletionFormattingRule.ForIndentStyle(indentStyle)]; + _indentStyle = indentStyle; + _options = options; } - private sealed class BraceCompletionFormattingRule : BaseFormattingRule + public static AbstractFormattingRule ForIndentStyle(FormattingOptions2.IndentStyle indentStyle) { - private static readonly Predicate s_predicate = o => o.Option.IsOn(SuppressOption.NoWrapping); + Debug.Assert(s_instances[(int)indentStyle]._indentStyle == indentStyle); + return s_instances[(int)indentStyle]; + } - private static readonly ImmutableArray s_instances = - [ - new BraceCompletionFormattingRule(FormattingOptions2.IndentStyle.None), - new BraceCompletionFormattingRule(FormattingOptions2.IndentStyle.Block), - new BraceCompletionFormattingRule(FormattingOptions2.IndentStyle.Smart), - ]; + public override AbstractFormattingRule WithOptions(SyntaxFormattingOptions options) + { + var newOptions = options as CSharpSyntaxFormattingOptions ?? CSharpSyntaxFormattingOptions.Default; + if (_options.NewLines == newOptions.NewLines) + { + return this; + } - private readonly FormattingOptions2.IndentStyle _indentStyle; - private readonly CSharpSyntaxFormattingOptions _options; + return new BraceCompletionFormattingRule(_indentStyle, newOptions); + } - public BraceCompletionFormattingRule(FormattingOptions2.IndentStyle indentStyle) - : this(indentStyle, CSharpSyntaxFormattingOptions.Default) + private static bool? NeedsNewLine(in SyntaxToken currentToken, CSharpSyntaxFormattingOptions options) + { + if (!currentToken.IsKind(SyntaxKind.OpenBraceToken)) { + return null; } - private BraceCompletionFormattingRule(FormattingOptions2.IndentStyle indentStyle, CSharpSyntaxFormattingOptions options) + // If we're inside any of the following expressions check if the option for + // braces on new lines in object / array initializers is set before we attempt + // to move the open brace location to a new line. + // new MyObject { + // new List { + // int[] arr = { + // = new[] { + // = new int[] { + if (currentToken.Parent is (kind: + SyntaxKind.ObjectInitializerExpression or + SyntaxKind.CollectionInitializerExpression or + SyntaxKind.ArrayInitializerExpression or + SyntaxKind.ImplicitArrayCreationExpression or + SyntaxKind.WithInitializerExpression or + SyntaxKind.PropertyPatternClause)) { - _indentStyle = indentStyle; - _options = options; + return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInObjectCollectionArrayInitializers); } - public static AbstractFormattingRule ForIndentStyle(FormattingOptions2.IndentStyle indentStyle) + var currentTokenParentParent = currentToken.Parent?.Parent; + + // * { - in the property accessor context + if (currentTokenParentParent is AccessorDeclarationSyntax) { - Debug.Assert(s_instances[(int)indentStyle]._indentStyle == indentStyle); - return s_instances[(int)indentStyle]; + return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInAccessors); } - public override AbstractFormattingRule WithOptions(SyntaxFormattingOptions options) + // * { - in the anonymous Method context + if (currentTokenParentParent.IsKind(SyntaxKind.AnonymousMethodExpression)) { - var newOptions = options as CSharpSyntaxFormattingOptions ?? CSharpSyntaxFormattingOptions.Default; - if (_options.NewLines == newOptions.NewLines) - { - return this; - } - - return new BraceCompletionFormattingRule(_indentStyle, newOptions); + return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInAnonymousMethods); } - private static bool? NeedsNewLine(in SyntaxToken currentToken, CSharpSyntaxFormattingOptions options) + // new { - Anonymous object creation + if (currentToken.Parent.IsKind(SyntaxKind.AnonymousObjectCreationExpression)) { - if (!currentToken.IsKind(SyntaxKind.OpenBraceToken)) - { - return null; - } - - // If we're inside any of the following expressions check if the option for - // braces on new lines in object / array initializers is set before we attempt - // to move the open brace location to a new line. - // new MyObject { - // new List { - // int[] arr = { - // = new[] { - // = new int[] { - if (currentToken.Parent is (kind: - SyntaxKind.ObjectInitializerExpression or - SyntaxKind.CollectionInitializerExpression or - SyntaxKind.ArrayInitializerExpression or - SyntaxKind.ImplicitArrayCreationExpression or - SyntaxKind.WithInitializerExpression or - SyntaxKind.PropertyPatternClause)) - { - return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInObjectCollectionArrayInitializers); - } - - var currentTokenParentParent = currentToken.Parent?.Parent; - - // * { - in the property accessor context - if (currentTokenParentParent is AccessorDeclarationSyntax) - { - return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInAccessors); - } - - // * { - in the anonymous Method context - if (currentTokenParentParent.IsKind(SyntaxKind.AnonymousMethodExpression)) - { - return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInAnonymousMethods); - } + return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInAnonymousTypes); + } - // new { - Anonymous object creation - if (currentToken.Parent.IsKind(SyntaxKind.AnonymousObjectCreationExpression)) - { - return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInAnonymousTypes); - } + // * { - in the control statement context + if (IsControlBlock(currentToken.Parent)) + { + return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInControlBlocks); + } - // * { - in the control statement context - if (IsControlBlock(currentToken.Parent)) - { - return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInControlBlocks); - } + // * { - in the simple Lambda context + if (currentTokenParentParent is (kind: SyntaxKind.SimpleLambdaExpression or SyntaxKind.ParenthesizedLambdaExpression)) + { + return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInLambdaExpressionBody); + } - // * { - in the simple Lambda context - if (currentTokenParentParent is (kind: SyntaxKind.SimpleLambdaExpression or SyntaxKind.ParenthesizedLambdaExpression)) - { - return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInLambdaExpressionBody); - } + // * { - in the member declaration context + if (currentTokenParentParent is MemberDeclarationSyntax) + { + return currentTokenParentParent is BasePropertyDeclarationSyntax + ? options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInProperties) + : options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInMethods); + } - // * { - in the member declaration context - if (currentTokenParentParent is MemberDeclarationSyntax) - { - return currentTokenParentParent is BasePropertyDeclarationSyntax - ? options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInProperties) - : options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInMethods); - } + // * { - in the type declaration context + if (currentToken.Parent is BaseTypeDeclarationSyntax or NamespaceDeclarationSyntax) + { + return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInTypes); + } - // * { - in the type declaration context - if (currentToken.Parent is BaseTypeDeclarationSyntax or NamespaceDeclarationSyntax) - { - return options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInTypes); - } + return null; + } - return null; + private static bool IsControlBlock(SyntaxNode? node) + { + if (node.IsKind(SyntaxKind.SwitchStatement)) + { + return true; } - private static bool IsControlBlock(SyntaxNode? node) + var parentKind = node?.Parent?.Kind(); + + switch (parentKind.GetValueOrDefault()) { - if (node.IsKind(SyntaxKind.SwitchStatement)) - { + case SyntaxKind.IfStatement: + case SyntaxKind.ElseClause: + case SyntaxKind.WhileStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + case SyntaxKind.UsingStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.TryStatement: + case SyntaxKind.CatchClause: + case SyntaxKind.FinallyClause: + case SyntaxKind.LockStatement: + case SyntaxKind.CheckedStatement: + case SyntaxKind.UncheckedStatement: + case SyntaxKind.SwitchSection: + case SyntaxKind.FixedStatement: + case SyntaxKind.UnsafeStatement: return true; - } - - var parentKind = node?.Parent?.Kind(); - - switch (parentKind.GetValueOrDefault()) - { - case SyntaxKind.IfStatement: - case SyntaxKind.ElseClause: - case SyntaxKind.WhileStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - case SyntaxKind.UsingStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.TryStatement: - case SyntaxKind.CatchClause: - case SyntaxKind.FinallyClause: - case SyntaxKind.LockStatement: - case SyntaxKind.CheckedStatement: - case SyntaxKind.UncheckedStatement: - case SyntaxKind.SwitchSection: - case SyntaxKind.FixedStatement: - case SyntaxKind.UnsafeStatement: - return true; - default: - return false; - } + default: + return false; } + } - public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + { + var needsNewLine = NeedsNewLine(currentToken, _options); + return needsNewLine switch { - var needsNewLine = NeedsNewLine(currentToken, _options); - return needsNewLine switch - { - true => CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines), - false => null, - _ => base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation), - }; - } + true => CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines), + false => null, + _ => base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation), + }; + } - public override void AddAlignTokensOperations(List list, SyntaxNode node, in NextAlignTokensOperationAction nextOperation) + public override void AddAlignTokensOperations(List list, SyntaxNode node, in NextAlignTokensOperationAction nextOperation) + { + base.AddAlignTokensOperations(list, node, in nextOperation); + if (_indentStyle == FormattingOptions2.IndentStyle.Block) { - base.AddAlignTokensOperations(list, node, in nextOperation); - if (_indentStyle == FormattingOptions2.IndentStyle.Block) + var bracePair = node.GetBracePair(); + if (bracePair.IsValidBracketOrBracePair()) { - var bracePair = node.GetBracePair(); - if (bracePair.IsValidBracketOrBracePair()) - { - // If the user has set block style indentation and we're in a valid brace pair - // then make sure we align the close brace to the open brace. - AddAlignIndentationOfTokensToBaseTokenOperation(list, node, bracePair.openBrace, - SpecializedCollections.SingletonEnumerable(bracePair.closeBrace), AlignTokensOption.AlignIndentationOfTokensToFirstTokenOfBaseTokenLine); - } + // If the user has set block style indentation and we're in a valid brace pair + // then make sure we align the close brace to the open brace. + AddAlignIndentationOfTokensToBaseTokenOperation(list, node, bracePair.openBrace, + SpecializedCollections.SingletonEnumerable(bracePair.closeBrace), AlignTokensOption.AlignIndentationOfTokensToFirstTokenOfBaseTokenLine); } } + } - public override void AddSuppressOperations(List list, SyntaxNode node, in NextSuppressOperationAction nextOperation) - { - base.AddSuppressOperations(list, node, in nextOperation); + public override void AddSuppressOperations(List list, SyntaxNode node, in NextSuppressOperationAction nextOperation) + { + base.AddSuppressOperations(list, node, in nextOperation); - // not sure exactly what is happening here, but removing the bellow causesthe indentation to be wrong. + // not sure exactly what is happening here, but removing the bellow causesthe indentation to be wrong. - // remove suppression rules for array and collection initializer - if (node.IsInitializerForArrayOrCollectionCreationExpression()) - { - // remove any suppression operation - list.RemoveAll(s_predicate); - } + // remove suppression rules for array and collection initializer + if (node.IsInitializerForArrayOrCollectionCreationExpression()) + { + // remove any suppression operation + list.RemoveAll(s_predicate); } } } diff --git a/src/Features/CSharp/Portable/BraceCompletion/InterpolatedStringBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/InterpolatedStringBraceCompletionService.cs index 2abcd2ad8412f..0ec8311ba7cef 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/InterpolatedStringBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/InterpolatedStringBraceCompletionService.cs @@ -5,87 +5,75 @@ using System; using System.Composition; using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.BraceCompletion; -using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion +namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion; + +/// +/// Brace completion service for double quotes marking an interpolated string. Note that the is used for other double quote completions. +/// +[Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class InterpolatedStringBraceCompletionService() : AbstractCSharpBraceCompletionService { + protected override char OpeningBrace => DoubleQuote.OpenCharacter; + protected override char ClosingBrace => DoubleQuote.CloseCharacter; + + public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) + => AllowOverTypeWithValidClosingToken(context); + /// - /// Brace completion service for double quotes marking an interpolated string. - /// Note that the is used for - /// other double quote completions. + /// Only return this service as valid when we're starting an interpolated string. Otherwise double quotes should be + /// completed using the /// - [Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] - internal class InterpolatedStringBraceCompletionService : AbstractCSharpBraceCompletionService - { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public InterpolatedStringBraceCompletionService() - { - } + public override bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken) + => OpeningBrace == brace && IsPositionInInterpolatedStringContext(document, openingPosition, cancellationToken); - protected override char OpeningBrace => DoubleQuote.OpenCharacter; - protected override char ClosingBrace => DoubleQuote.CloseCharacter; + protected override bool IsValidOpeningBraceToken(SyntaxToken leftToken) + => leftToken.Kind() is SyntaxKind.InterpolatedStringStartToken or SyntaxKind.InterpolatedVerbatimStringStartToken; - public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) - => AllowOverTypeWithValidClosingToken(context); + protected override bool IsValidClosingBraceToken(SyntaxToken rightToken) + => rightToken.IsKind(SyntaxKind.InterpolatedStringEndToken); - /// - /// Only return this service as valid when we're starting an interpolated string. - /// Otherwise double quotes should be completed using the - /// - public override bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken) - => OpeningBrace == brace && IsPositionInInterpolatedStringContext(document, openingPosition, cancellationToken); + protected override bool IsValidOpenBraceTokenAtPosition(SourceText text, SyntaxToken token, int position) + => IsValidOpeningBraceToken(token) && token.Span.End - 1 == position; - protected override bool IsValidOpeningBraceToken(SyntaxToken leftToken) - => leftToken.Kind() is SyntaxKind.InterpolatedStringStartToken or SyntaxKind.InterpolatedVerbatimStringStartToken; - - protected override bool IsValidClosingBraceToken(SyntaxToken rightToken) - => rightToken.IsKind(SyntaxKind.InterpolatedStringEndToken); + /// + /// Returns true when the input position could be starting an interpolated string if opening quotes were typed. + /// + public static bool IsPositionInInterpolatedStringContext(ParsedDocument document, int position, CancellationToken cancellationToken) + { + var text = document.Text; - protected override bool IsValidOpenBraceTokenAtPosition(SourceText text, SyntaxToken token, int position) - => IsValidOpeningBraceToken(token) && token.Span.End - 1 == position; + var start = position - 1; + if (start < 0) + return false; - /// - /// Returns true when the input position could be starting an interpolated string if opening quotes were typed. - /// - public static bool IsPositionInInterpolatedStringContext(ParsedDocument document, int position, CancellationToken cancellationToken) + // Check if the user is typing an interpolated or interpolated verbatim string. + // If the preceding character(s) are not '$' or '$@' then we can't be starting an interpolated string. + if (text[start] == '@') { - var text = document.Text; - - var start = position - 1; - if (start < 0) + // must have $@ for an interpolated string. otherwise this is some other @ construct. + if (start == 0 || text[start - 1] != '$') return false; - // Check if the user is typing an interpolated or interpolated verbatim string. - // If the preceding character(s) are not '$' or '$@' then we can't be starting an interpolated string. - if (text[start] == '@') - { - if (--start < 0) - return false; - } - - if (text[start] != '$') - return false; - - // Verify that we are actually in an location allowed for an interpolated string. - var token = document.Root.FindToken(start); - if (token.Kind() is not SyntaxKind.InterpolatedStringStartToken and - not SyntaxKind.InterpolatedVerbatimStringStartToken and - not SyntaxKind.StringLiteralToken and - not SyntaxKind.IdentifierToken) - { - return false; - } - - var previousToken = token.GetPreviousToken(); - - return document.SyntaxTree.IsExpressionContext(token.SpanStart, previousToken, attributes: true, cancellationToken) - || document.SyntaxTree.IsStatementContext(token.SpanStart, previousToken, cancellationToken); + start--; } + else if (text[start] == '$') + { + // could be @$ + if (start > 0 && text[start - 1] == '@') + start--; + } + else + { + return false; + } + + return IsLegalExpressionLocation(document.SyntaxTree, start, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/BraceCompletion/InterpolationBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/InterpolationBraceCompletionService.cs index 0490bc8a2d4ad..59306e82962d1 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/InterpolationBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/InterpolationBraceCompletionService.cs @@ -12,64 +12,63 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion +namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion; + +/// +/// Brace completion service used for completing curly braces inside interpolated strings. +/// In other curly brace completion scenarios the should be used. +/// +[Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] +internal class InterpolationBraceCompletionService : AbstractCSharpBraceCompletionService { - /// - /// Brace completion service used for completing curly braces inside interpolated strings. - /// In other curly brace completion scenarios the should be used. - /// - [Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] - internal class InterpolationBraceCompletionService : AbstractCSharpBraceCompletionService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public InterpolationBraceCompletionService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public InterpolationBraceCompletionService() - { - } + } - protected override char OpeningBrace => CurlyBrace.OpenCharacter; - protected override char ClosingBrace => CurlyBrace.CloseCharacter; + protected override char OpeningBrace => CurlyBrace.OpenCharacter; + protected override char ClosingBrace => CurlyBrace.CloseCharacter; - public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) - => AllowOverTypeWithValidClosingToken(context); + public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) + => AllowOverTypeWithValidClosingToken(context); - /// - /// Only return this service as valid when we're typing curly braces inside an interpolated string. - /// Otherwise curly braces should be completed using the - /// - public override bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken) - => OpeningBrace == brace && IsPositionInInterpolationContext(document, openingPosition); + /// + /// Only return this service as valid when we're typing curly braces inside an interpolated string. + /// Otherwise curly braces should be completed using the + /// + public override bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken) + => OpeningBrace == brace && IsPositionInInterpolationContext(document, openingPosition); - protected override bool IsValidOpenBraceTokenAtPosition(SourceText text, SyntaxToken token, int position) - => IsValidOpeningBraceToken(token) && token.SpanStart == position; + protected override bool IsValidOpenBraceTokenAtPosition(SourceText text, SyntaxToken token, int position) + => IsValidOpeningBraceToken(token) && token.SpanStart == position; - protected override bool IsValidOpeningBraceToken(SyntaxToken token) - => token.IsKind(SyntaxKind.OpenBraceToken) && token.Parent.IsKind(SyntaxKind.Interpolation); + protected override bool IsValidOpeningBraceToken(SyntaxToken token) + => token.IsKind(SyntaxKind.OpenBraceToken) && token.Parent.IsKind(SyntaxKind.Interpolation); - protected override bool IsValidClosingBraceToken(SyntaxToken token) - => token.IsKind(SyntaxKind.CloseBraceToken); + protected override bool IsValidClosingBraceToken(SyntaxToken token) + => token.IsKind(SyntaxKind.CloseBraceToken); - /// - /// Returns true when the input position could be starting an interpolation expression if a curly brace was typed. - /// - public static bool IsPositionInInterpolationContext(ParsedDocument document, int position) + /// + /// Returns true when the input position could be starting an interpolation expression if a curly brace was typed. + /// + public static bool IsPositionInInterpolationContext(ParsedDocument document, int position) + { + // First, check to see if the character to the left of the position is an open curly. + // If it is, we shouldn't complete because the user may be trying to escape a curly. + // E.g. they are trying to type $"{{" + if (CouldEscapePreviousOpenBrace('{', position, document.Text)) { - // First, check to see if the character to the left of the position is an open curly. - // If it is, we shouldn't complete because the user may be trying to escape a curly. - // E.g. they are trying to type $"{{" - if (CouldEscapePreviousOpenBrace('{', position, document.Text)) - { - return false; - } - - var token = document.Root.FindTokenOnLeftOfPosition(position); - if (!token.Span.IntersectsWith(position)) - { - return false; - } + return false; + } - // We can be starting an interpolation expression if we're inside an interpolated string. - return token.Parent.IsKind(SyntaxKind.InterpolatedStringExpression) || token.Parent.IsParentKind(SyntaxKind.InterpolatedStringExpression); + var token = document.Root.FindTokenOnLeftOfPosition(position); + if (!token.Span.IntersectsWith(position)) + { + return false; } + + // We can be starting an interpolation expression if we're inside an interpolated string. + return token.Parent.IsKind(SyntaxKind.InterpolatedStringExpression) || token.Parent.IsParentKind(SyntaxKind.InterpolatedStringExpression); } } diff --git a/src/Features/CSharp/Portable/BraceCompletion/LessAndGreaterThanBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/LessAndGreaterThanBraceCompletionService.cs index 4fbf8cb446456..e05488ea1640a 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/LessAndGreaterThanBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/LessAndGreaterThanBraceCompletionService.cs @@ -14,64 +14,63 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion +namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion; + +[Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] +internal class LessAndGreaterThanBraceCompletionService : AbstractCSharpBraceCompletionService { - [Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] - internal class LessAndGreaterThanBraceCompletionService : AbstractCSharpBraceCompletionService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LessAndGreaterThanBraceCompletionService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public LessAndGreaterThanBraceCompletionService() - { - } + } - protected override bool NeedsSemantics => true; + protected override bool NeedsSemantics => true; - protected override char OpeningBrace => LessAndGreaterThan.OpenCharacter; - protected override char ClosingBrace => LessAndGreaterThan.CloseCharacter; + protected override char OpeningBrace => LessAndGreaterThan.OpenCharacter; + protected override char ClosingBrace => LessAndGreaterThan.CloseCharacter; - public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) - => AllowOverTypeInUserCodeWithValidClosingToken(context, cancellationToken); + public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) + => AllowOverTypeInUserCodeWithValidClosingToken(context, cancellationToken); - protected override bool IsValidOpeningBraceToken(SyntaxToken token) - => token.IsKind(SyntaxKind.LessThanToken); + protected override bool IsValidOpeningBraceToken(SyntaxToken token) + => token.IsKind(SyntaxKind.LessThanToken); - protected override bool IsValidClosingBraceToken(SyntaxToken token) - => token.IsKind(SyntaxKind.GreaterThanToken); + protected override bool IsValidClosingBraceToken(SyntaxToken token) + => token.IsKind(SyntaxKind.GreaterThanToken); - protected override Task IsValidOpenBraceTokenAtPositionAsync(Document document, SyntaxToken token, int position, CancellationToken cancellationToken) + protected override Task IsValidOpenBraceTokenAtPositionAsync(Document document, SyntaxToken token, int position, CancellationToken cancellationToken) + { + // check what parser thinks about the newly typed "<" and only proceed if parser thinks it is "<" of + // type argument or parameter list + if (token.CheckParent(n => n.LessThanToken == token) || + token.CheckParent(n => n.LessThanToken == token) || + token.CheckParent(n => n.LessThanToken == token)) { - // check what parser thinks about the newly typed "<" and only proceed if parser thinks it is "<" of - // type argument or parameter list - if (token.CheckParent(n => n.LessThanToken == token) || - token.CheckParent(n => n.LessThanToken == token) || - token.CheckParent(n => n.LessThanToken == token)) - { - return Task.FromResult(true); - } + return Task.FromResult(true); + } - // type argument can be easily ambiguous with normal < operations - if (token.Parent is not BinaryExpressionSyntax(SyntaxKind.LessThanExpression) node || node.OperatorToken != token) - return Task.FromResult(false); + // type argument can be easily ambiguous with normal < operations + if (token.Parent is not BinaryExpressionSyntax(SyntaxKind.LessThanExpression) node || node.OperatorToken != token) + return Task.FromResult(false); - // type_argument_list only shows up in the following grammar construct: - // - // generic_name - // : identifier_token type_argument_list - // - // So if the prior token is not an identifier, this could not be a type-argument-list. - var previousToken = token.GetPreviousToken(); - if (previousToken.Parent is not IdentifierNameSyntax identifier) - return Task.FromResult(false); + // type_argument_list only shows up in the following grammar construct: + // + // generic_name + // : identifier_token type_argument_list + // + // So if the prior token is not an identifier, this could not be a type-argument-list. + var previousToken = token.GetPreviousToken(); + if (previousToken.Parent is not IdentifierNameSyntax identifier) + return Task.FromResult(false); - return IsSemanticTypeArgumentAsync(document, node.SpanStart, identifier, cancellationToken); + return IsSemanticTypeArgumentAsync(document, node.SpanStart, identifier, cancellationToken); - static async Task IsSemanticTypeArgumentAsync(Document document, int position, IdentifierNameSyntax identifier, CancellationToken cancellationToken) - { - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); - var info = semanticModel.GetSymbolInfo(identifier, cancellationToken); - return info.CandidateSymbols.Any(static s => s.GetArity() > 0); - } + static async Task IsSemanticTypeArgumentAsync(Document document, int position, IdentifierNameSyntax identifier, CancellationToken cancellationToken) + { + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); + var info = semanticModel.GetSymbolInfo(identifier, cancellationToken); + return info.CandidateSymbols.Any(static s => s.GetArity() > 0); } } } diff --git a/src/Features/CSharp/Portable/BraceCompletion/ParenthesisBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/ParenthesisBraceCompletionService.cs index 21e2ea31cf96a..969bb1a0614cf 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/ParenthesisBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/ParenthesisBraceCompletionService.cs @@ -13,50 +13,49 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion +namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion; + +[Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] +internal class ParenthesisBraceCompletionService : AbstractCSharpBraceCompletionService { - [Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] - internal class ParenthesisBraceCompletionService : AbstractCSharpBraceCompletionService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ParenthesisBraceCompletionService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ParenthesisBraceCompletionService() - { - } + } + + protected override char OpeningBrace => Parenthesis.OpenCharacter; + protected override char ClosingBrace => Parenthesis.CloseCharacter; - protected override char OpeningBrace => Parenthesis.OpenCharacter; - protected override char ClosingBrace => Parenthesis.CloseCharacter; + public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) + => AllowOverTypeInUserCodeWithValidClosingToken(context, cancellationToken); - public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) - => AllowOverTypeInUserCodeWithValidClosingToken(context, cancellationToken); + protected override bool IsValidOpeningBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.OpenParenToken); - protected override bool IsValidOpeningBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.OpenParenToken); + protected override bool IsValidClosingBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.CloseParenToken); - protected override bool IsValidClosingBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.CloseParenToken); + protected override bool IsValidOpenBraceTokenAtPosition(SourceText text, SyntaxToken token, int position) + { + if (ParentIsSkippedTokensTriviaOrNull(this.SyntaxFacts, token) + || !IsValidOpeningBraceToken(token) + || token.SpanStart != position + || token.Parent == null) + { + return false; + } - protected override bool IsValidOpenBraceTokenAtPosition(SourceText text, SyntaxToken token, int position) + // now check whether parser think whether there is already counterpart closing parenthesis + var (openParen, closeParen) = token.Parent.GetParentheses(); + + // We can complete the brace if the closing brace is missing or the incorrect kind. + if (closeParen.Kind() != SyntaxKind.CloseParenToken || closeParen.Span.Length == 0) { - if (ParentIsSkippedTokensTriviaOrNull(this.SyntaxFacts, token) - || !IsValidOpeningBraceToken(token) - || token.SpanStart != position - || token.Parent == null) - { - return false; - } - - // now check whether parser think whether there is already counterpart closing parenthesis - var (openParen, closeParen) = token.Parent.GetParentheses(); - - // We can complete the brace if the closing brace is missing or the incorrect kind. - if (closeParen.Kind() != SyntaxKind.CloseParenToken || closeParen.Span.Length == 0) - { - return true; - } - - // If the completed pair is on the same line, then the closing parenthesis must belong to a different - // brace completion session higher up on the stack. If that's the case then we can - // complete the opening brace here, so return this as valid for completion. - return text.Lines.GetLineFromPosition(openParen.SpanStart).LineNumber == text.Lines.GetLineFromPosition(closeParen.Span.End).LineNumber; + return true; } + + // If the completed pair is on the same line, then the closing parenthesis must belong to a different + // brace completion session higher up on the stack. If that's the case then we can + // complete the opening brace here, so return this as valid for completion. + return text.Lines.GetLineFromPosition(openParen.SpanStart).LineNumber == text.Lines.GetLineFromPosition(closeParen.Span.End).LineNumber; } } diff --git a/src/Features/CSharp/Portable/BraceCompletion/StringLiteralBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/StringLiteralBraceCompletionService.cs index 11f5290aa5522..b98330cae0794 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/StringLiteralBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/StringLiteralBraceCompletionService.cs @@ -5,98 +5,104 @@ using System; using System.Composition; using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.BraceCompletion; using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion +namespace Microsoft.CodeAnalysis.CSharp.BraceCompletion; + +[Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class StringLiteralBraceCompletionService() : AbstractCSharpBraceCompletionService { - [Export(LanguageNames.CSharp, typeof(IBraceCompletionService)), Shared] - internal class StringLiteralBraceCompletionService : AbstractCSharpBraceCompletionService + protected override char OpeningBrace => DoubleQuote.OpenCharacter; + protected override char ClosingBrace => DoubleQuote.CloseCharacter; + + public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) + => AllowOverTypeWithValidClosingToken(context); + + public override bool CanProvideBraceCompletion(char brace, int position, ParsedDocument document, CancellationToken cancellationToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public StringLiteralBraceCompletionService() - { - } + var text = document.Text; - protected override char OpeningBrace => DoubleQuote.OpenCharacter; - protected override char ClosingBrace => DoubleQuote.CloseCharacter; + // Only potentially valid for string literal completion if not in an interpolated string brace completion context. + if (OpeningBrace == brace && InterpolatedStringBraceCompletionService.IsPositionInInterpolatedStringContext(document, position, cancellationToken)) + return false; - public override bool AllowOverType(BraceCompletionContext context, CancellationToken cancellationToken) - => AllowOverTypeWithValidClosingToken(context); + if (!base.CanProvideBraceCompletion(brace, position, document, cancellationToken)) + return false; - public override bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken) - { - // Only potentially valid for string literal completion if not in an interpolated string brace completion context. - if (OpeningBrace == brace && InterpolatedStringBraceCompletionService.IsPositionInInterpolatedStringContext(document, openingPosition, cancellationToken)) - { - return false; - } + // Find the start of the string literal token (including if it is a verbatim string). + var start = position; + if (start > 0 && text[start - 1] == '@') + start--; - return base.CanProvideBraceCompletion(brace, openingPosition, document, cancellationToken); - } + // Has to be the start of an expression, or within a directive (string literals are legal there). + return IsLegalExpressionLocation(document.SyntaxTree, start, cancellationToken) + || document.SyntaxTree.IsPreProcessorDirectiveContext(start, cancellationToken); + } - protected override bool IsValidOpeningBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.StringLiteralToken); + protected override bool IsValidOpeningBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.StringLiteralToken); - protected override bool IsValidClosingBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.StringLiteralToken); + protected override bool IsValidClosingBraceToken(SyntaxToken token) => token.IsKind(SyntaxKind.StringLiteralToken); - protected override bool IsValidOpenBraceTokenAtPosition(SourceText text, SyntaxToken token, int position) - { - if (ParentIsSkippedTokensTriviaOrNull(this.SyntaxFacts, token) || !IsValidOpeningBraceToken(token)) - return false; - - // If the single token that the user typed is a string literal that is more than just - // the one double quote character they typed, and the line doesn't have errors, then - // it means it is completing an existing token, from the start. For example given: - // - // var s = "te$$st"; - // - // When the user types `" + "` to split the string into two literals, the first - // quote won't be completed (because its in a string literal), and with this check - // the second quote won't either. - // - // We don't do this optimization for verbatim strings because they are multi-line so - // the flow on effects from us getting it wrong are much greater, and it can really change - // the tree. - if (token.IsKind(SyntaxKind.StringLiteralToken) && - !token.IsVerbatimStringLiteral() && - token.Span.Length > 1 && - !RestOfLineContainsDiagnostics(token)) - { - return false; - } - - if (token.SpanStart == position) - { - return true; - } + protected override bool IsValidOpenBraceTokenAtPosition(SourceText text, SyntaxToken token, int position) + { + if (ParentIsSkippedTokensTriviaOrNull(this.SyntaxFacts, token) || !IsValidOpeningBraceToken(token)) + return false; - // The character at the position is a double quote but the token's span start we found at the position - // doesn't match the position. Check if we're in a verbatim string token @" where the token span start - // is the @ character and the " is one past the token start. - return token.SpanStart + 1 == position && token.IsVerbatimStringLiteral(); + // If the single token that the user typed is a string literal that is more than just + // the one double quote character they typed, and the line doesn't have errors, then + // it means it is completing an existing token, from the start. For example given: + // + // var s = "te$$st"; + // + // When the user types `" + "` to split the string into two literals, the first + // quote won't be completed (because its in a string literal), and with this check + // the second quote won't either. + // + // We don't do this optimization for verbatim strings because they are multi-line so + // the flow on effects from us getting it wrong are much greater, and it can really change + // the tree. + if (token.IsKind(SyntaxKind.StringLiteralToken) && + !token.IsVerbatimStringLiteral() && + token.Span.Length > 1 && + !RestOfLineContainsDiagnostics(token)) + { + return false; } - private static bool RestOfLineContainsDiagnostics(SyntaxToken token) - { - while (!token.IsKind(SyntaxKind.None) && !token.TrailingTrivia.Contains(t => t.IsEndOfLine())) - { - if (token.ContainsDiagnostics) - return true; + var isStartOfString = token.SpanStart == position; - token = token.GetNextToken(); - } + // If the character at the position is a double quote but the token's span start we found at the position + // doesn't match the position. Check if we're in a verbatim string token @" where the token span start is the @ + // character and the " is one past the token start. + var isStartOfVerbatimString = token.SpanStart + 1 == position && token.IsVerbatimStringLiteral(); + if (!isStartOfString && !isStartOfVerbatimString) + return false; + + return true; + } + + private static bool RestOfLineContainsDiagnostics(SyntaxToken token) + { + while (!token.IsKind(SyntaxKind.None) && !token.TrailingTrivia.Contains(t => t.IsEndOfLine())) + { if (token.ContainsDiagnostics) return true; - return false; + token = token.GetNextToken(); } + + if (token.ContainsDiagnostics) + return true; + + return false; } } diff --git a/src/Features/CSharp/Portable/BraceMatching/AbstractCSharpBraceMatcher.cs b/src/Features/CSharp/Portable/BraceMatching/AbstractCSharpBraceMatcher.cs index 1e45c7aa47af1..835955b00318b 100644 --- a/src/Features/CSharp/Portable/BraceMatching/AbstractCSharpBraceMatcher.cs +++ b/src/Features/CSharp/Portable/BraceMatching/AbstractCSharpBraceMatcher.cs @@ -5,14 +5,13 @@ using Microsoft.CodeAnalysis.BraceMatching; using Microsoft.CodeAnalysis.CSharp; -namespace Microsoft.CodeAnalysis.CSharp.BraceMatching +namespace Microsoft.CodeAnalysis.CSharp.BraceMatching; + +internal abstract class AbstractCSharpBraceMatcher : AbstractBraceMatcher { - internal abstract class AbstractCSharpBraceMatcher : AbstractBraceMatcher + protected AbstractCSharpBraceMatcher(SyntaxKind openBrace, SyntaxKind closeBrace) + : base(new BraceCharacterAndKind(SyntaxFacts.GetText(openBrace)[0], (int)openBrace), + new BraceCharacterAndKind(SyntaxFacts.GetText(closeBrace)[0], (int)closeBrace)) { - protected AbstractCSharpBraceMatcher(SyntaxKind openBrace, SyntaxKind closeBrace) - : base(new BraceCharacterAndKind(SyntaxFacts.GetText(openBrace)[0], (int)openBrace), - new BraceCharacterAndKind(SyntaxFacts.GetText(closeBrace)[0], (int)closeBrace)) - { - } } } diff --git a/src/Features/CSharp/Portable/BraceMatching/BlockCommentBraceMatcher.cs b/src/Features/CSharp/Portable/BraceMatching/BlockCommentBraceMatcher.cs index f2d686f318973..fc03a7203d271 100644 --- a/src/Features/CSharp/Portable/BraceMatching/BlockCommentBraceMatcher.cs +++ b/src/Features/CSharp/Portable/BraceMatching/BlockCommentBraceMatcher.cs @@ -11,37 +11,36 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.BraceMatching +namespace Microsoft.CodeAnalysis.CSharp.BraceMatching; + +[ExportBraceMatcher(LanguageNames.CSharp), Shared] +internal class BlockCommentBraceMatcher : IBraceMatcher { - [ExportBraceMatcher(LanguageNames.CSharp), Shared] - internal class BlockCommentBraceMatcher : IBraceMatcher + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public BlockCommentBraceMatcher() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public BlockCommentBraceMatcher() + } + + public async Task FindBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var trivia = root.FindTrivia(position); + + if (trivia.Kind() is SyntaxKind.MultiLineCommentTrivia && + trivia.ToString() is ['/', '*', .., '*', '/']) { + return new BraceMatchingResult(new TextSpan(trivia.SpanStart, "/*".Length), TextSpan.FromBounds(trivia.Span.End - "*/".Length, trivia.Span.End)); } - - public async Task FindBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken) + else if (trivia.Kind() is SyntaxKind.MultiLineDocumentationCommentTrivia) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var trivia = root.FindTrivia(position); - - if (trivia.Kind() is SyntaxKind.MultiLineCommentTrivia && - trivia.ToString() is ['/', '*', .., '*', '/']) - { - return new BraceMatchingResult(new TextSpan(trivia.SpanStart, "/*".Length), TextSpan.FromBounds(trivia.Span.End - "*/".Length, trivia.Span.End)); - } - else if (trivia.Kind() is SyntaxKind.MultiLineDocumentationCommentTrivia) - { - var startBrace = new TextSpan(trivia.FullSpan.Start, "/**".Length); - var endBrace = TextSpan.FromBounds(trivia.FullSpan.End - "*/".Length, trivia.FullSpan.End); - if (text.ToString(startBrace) == "/**" && text.ToString(endBrace) == "*/") - return new BraceMatchingResult(startBrace, endBrace); - } - - return null; + var startBrace = new TextSpan(trivia.FullSpan.Start, "/**".Length); + var endBrace = TextSpan.FromBounds(trivia.FullSpan.End - "*/".Length, trivia.FullSpan.End); + if (text.ToString(startBrace) == "/**" && text.ToString(endBrace) == "*/") + return new BraceMatchingResult(startBrace, endBrace); } + + return null; } } diff --git a/src/Features/CSharp/Portable/BraceMatching/CSharpDirectiveTriviaBraceMatcher.cs b/src/Features/CSharp/Portable/BraceMatching/CSharpDirectiveTriviaBraceMatcher.cs index 97d048ed57e4c..e2b128f19ce8f 100644 --- a/src/Features/CSharp/Portable/BraceMatching/CSharpDirectiveTriviaBraceMatcher.cs +++ b/src/Features/CSharp/Portable/BraceMatching/CSharpDirectiveTriviaBraceMatcher.cs @@ -14,27 +14,26 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.BraceMatching +namespace Microsoft.CodeAnalysis.CSharp.BraceMatching; + +[ExportBraceMatcher(LanguageNames.CSharp), Shared] +internal class CSharpDirectiveTriviaBraceMatcher : AbstractDirectiveTriviaBraceMatcher { - [ExportBraceMatcher(LanguageNames.CSharp), Shared] - internal class CSharpDirectiveTriviaBraceMatcher : AbstractDirectiveTriviaBraceMatcher + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpDirectiveTriviaBraceMatcher() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpDirectiveTriviaBraceMatcher() - { - } + } - protected override ImmutableArray GetMatchingConditionalDirectives(DirectiveTriviaSyntax directive, CancellationToken cancellationToken) - => directive.GetMatchingConditionalDirectives(cancellationToken); + protected override ImmutableArray GetMatchingConditionalDirectives(DirectiveTriviaSyntax directive, CancellationToken cancellationToken) + => directive.GetMatchingConditionalDirectives(cancellationToken); - protected override DirectiveTriviaSyntax? GetMatchingDirective(DirectiveTriviaSyntax directive, CancellationToken cancellationToken) - => directive.GetMatchingDirective(cancellationToken); + protected override DirectiveTriviaSyntax? GetMatchingDirective(DirectiveTriviaSyntax directive, CancellationToken cancellationToken) + => directive.GetMatchingDirective(cancellationToken); - internal override TextSpan GetSpanForTagging(DirectiveTriviaSyntax directive) - => TextSpan.FromBounds(directive.HashToken.SpanStart, directive.DirectiveNameToken.Span.End); - } + internal override TextSpan GetSpanForTagging(DirectiveTriviaSyntax directive) + => TextSpan.FromBounds(directive.HashToken.SpanStart, directive.DirectiveNameToken.Span.End); } diff --git a/src/Features/CSharp/Portable/BraceMatching/CSharpEmbeddedLanguageBraceMatcher.cs b/src/Features/CSharp/Portable/BraceMatching/CSharpEmbeddedLanguageBraceMatcher.cs index 3f5f2f840ab53..376f95b9c5a69 100644 --- a/src/Features/CSharp/Portable/BraceMatching/CSharpEmbeddedLanguageBraceMatcher.cs +++ b/src/Features/CSharp/Portable/BraceMatching/CSharpEmbeddedLanguageBraceMatcher.cs @@ -11,13 +11,12 @@ using Microsoft.CodeAnalysis.EmbeddedLanguages; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.BraceMatching +namespace Microsoft.CodeAnalysis.CSharp.BraceMatching; + +[ExportBraceMatcher(LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpEmbeddedLanguageBraceMatcher( + [ImportMany] IEnumerable> services) : AbstractEmbeddedLanguageBraceMatcher(LanguageNames.CSharp, CSharpEmbeddedLanguagesProvider.Info, CSharpSyntaxKinds.Instance, services) { - [ExportBraceMatcher(LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpEmbeddedLanguageBraceMatcher( - [ImportMany] IEnumerable> services) : AbstractEmbeddedLanguageBraceMatcher(LanguageNames.CSharp, CSharpEmbeddedLanguagesProvider.Info, CSharpSyntaxKinds.Instance, services) - { - } } diff --git a/src/Features/CSharp/Portable/BraceMatching/LessThanGreaterThanBraceMatcher.cs b/src/Features/CSharp/Portable/BraceMatching/LessThanGreaterThanBraceMatcher.cs index 2299841fc6724..64d8e0623244f 100644 --- a/src/Features/CSharp/Portable/BraceMatching/LessThanGreaterThanBraceMatcher.cs +++ b/src/Features/CSharp/Portable/BraceMatching/LessThanGreaterThanBraceMatcher.cs @@ -8,16 +8,15 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.BraceMatching +namespace Microsoft.CodeAnalysis.CSharp.BraceMatching; + +[ExportBraceMatcher(LanguageNames.CSharp), Shared] +internal class LessThanGreaterThanBraceMatcher : AbstractCSharpBraceMatcher { - [ExportBraceMatcher(LanguageNames.CSharp), Shared] - internal class LessThanGreaterThanBraceMatcher : AbstractCSharpBraceMatcher + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LessThanGreaterThanBraceMatcher() + : base(SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public LessThanGreaterThanBraceMatcher() - : base(SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken) - { - } } } diff --git a/src/Features/CSharp/Portable/BraceMatching/OpenCloseBraceBraceMatcher.cs b/src/Features/CSharp/Portable/BraceMatching/OpenCloseBraceBraceMatcher.cs index f81acc69b1ff8..fa61b537ef287 100644 --- a/src/Features/CSharp/Portable/BraceMatching/OpenCloseBraceBraceMatcher.cs +++ b/src/Features/CSharp/Portable/BraceMatching/OpenCloseBraceBraceMatcher.cs @@ -8,16 +8,15 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.BraceMatching +namespace Microsoft.CodeAnalysis.CSharp.BraceMatching; + +[ExportBraceMatcher(LanguageNames.CSharp), Shared] +internal class OpenCloseBraceBraceMatcher : AbstractCSharpBraceMatcher { - [ExportBraceMatcher(LanguageNames.CSharp), Shared] - internal class OpenCloseBraceBraceMatcher : AbstractCSharpBraceMatcher + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public OpenCloseBraceBraceMatcher() + : base(SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public OpenCloseBraceBraceMatcher() - : base(SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken) - { - } } } diff --git a/src/Features/CSharp/Portable/BraceMatching/OpenCloseBracketBraceMatcher.cs b/src/Features/CSharp/Portable/BraceMatching/OpenCloseBracketBraceMatcher.cs index b6fded68b2311..cfa69178f30a1 100644 --- a/src/Features/CSharp/Portable/BraceMatching/OpenCloseBracketBraceMatcher.cs +++ b/src/Features/CSharp/Portable/BraceMatching/OpenCloseBracketBraceMatcher.cs @@ -8,16 +8,15 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.BraceMatching +namespace Microsoft.CodeAnalysis.CSharp.BraceMatching; + +[ExportBraceMatcher(LanguageNames.CSharp), Shared] +internal class OpenCloseBracketBraceMatcher : AbstractCSharpBraceMatcher { - [ExportBraceMatcher(LanguageNames.CSharp), Shared] - internal class OpenCloseBracketBraceMatcher : AbstractCSharpBraceMatcher + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public OpenCloseBracketBraceMatcher() + : base(SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public OpenCloseBracketBraceMatcher() - : base(SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken) - { - } } } diff --git a/src/Features/CSharp/Portable/BraceMatching/OpenCloseParenBraceMatcher.cs b/src/Features/CSharp/Portable/BraceMatching/OpenCloseParenBraceMatcher.cs index 0227dd5f118cc..11c76733869a8 100644 --- a/src/Features/CSharp/Portable/BraceMatching/OpenCloseParenBraceMatcher.cs +++ b/src/Features/CSharp/Portable/BraceMatching/OpenCloseParenBraceMatcher.cs @@ -8,16 +8,15 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.BraceMatching +namespace Microsoft.CodeAnalysis.CSharp.BraceMatching; + +[ExportBraceMatcher(LanguageNames.CSharp), Shared] +internal class OpenCloseParenBraceMatcher : AbstractCSharpBraceMatcher { - [ExportBraceMatcher(LanguageNames.CSharp), Shared] - internal class OpenCloseParenBraceMatcher : AbstractCSharpBraceMatcher + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public OpenCloseParenBraceMatcher() + : base(SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public OpenCloseParenBraceMatcher() - : base(SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken) - { - } } } diff --git a/src/Features/CSharp/Portable/BraceMatching/StringLiteralBraceMatcher.cs b/src/Features/CSharp/Portable/BraceMatching/StringLiteralBraceMatcher.cs index 79f09c5ae6e7b..cbc3038480b97 100644 --- a/src/Features/CSharp/Portable/BraceMatching/StringLiteralBraceMatcher.cs +++ b/src/Features/CSharp/Portable/BraceMatching/StringLiteralBraceMatcher.cs @@ -14,65 +14,64 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.BraceMatching +namespace Microsoft.CodeAnalysis.CSharp.BraceMatching; + +[ExportBraceMatcher(LanguageNames.CSharp), Shared] +internal class StringLiteralBraceMatcher : IBraceMatcher { - [ExportBraceMatcher(LanguageNames.CSharp), Shared] - internal class StringLiteralBraceMatcher : IBraceMatcher + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public StringLiteralBraceMatcher() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public StringLiteralBraceMatcher() - { - } + } - public async Task FindBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(position); + public async Task FindBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(position); - if (!token.ContainsDiagnostics) + if (!token.ContainsDiagnostics) + { + if (token.IsKind(SyntaxKind.StringLiteralToken)) { - if (token.IsKind(SyntaxKind.StringLiteralToken)) - { - return GetSimpleStringBraceMatchingResult(token, endTokenLength: 1); - } - else if (token.IsKind(SyntaxKind.Utf8StringLiteralToken)) - { - return GetSimpleStringBraceMatchingResult(token, endTokenLength: 3); - } - else if (token.Kind() is SyntaxKind.InterpolatedStringStartToken or SyntaxKind.InterpolatedVerbatimStringStartToken) + return GetSimpleStringBraceMatchingResult(token, endTokenLength: 1); + } + else if (token.IsKind(SyntaxKind.Utf8StringLiteralToken)) + { + return GetSimpleStringBraceMatchingResult(token, endTokenLength: 3); + } + else if (token.Kind() is SyntaxKind.InterpolatedStringStartToken or SyntaxKind.InterpolatedVerbatimStringStartToken) + { + if (token.Parent is InterpolatedStringExpressionSyntax interpolatedString) { - if (token.Parent is InterpolatedStringExpressionSyntax interpolatedString) - { - return new BraceMatchingResult(token.Span, interpolatedString.StringEndToken.Span); - } + return new BraceMatchingResult(token.Span, interpolatedString.StringEndToken.Span); } - else if (token.IsKind(SyntaxKind.InterpolatedStringEndToken)) + } + else if (token.IsKind(SyntaxKind.InterpolatedStringEndToken)) + { + if (token.Parent is InterpolatedStringExpressionSyntax interpolatedString) { - if (token.Parent is InterpolatedStringExpressionSyntax interpolatedString) - { - return new BraceMatchingResult(interpolatedString.StringStartToken.Span, token.Span); - } + return new BraceMatchingResult(interpolatedString.StringStartToken.Span, token.Span); } } - - return null; } - private static BraceMatchingResult GetSimpleStringBraceMatchingResult(SyntaxToken token, int endTokenLength) + return null; + } + + private static BraceMatchingResult GetSimpleStringBraceMatchingResult(SyntaxToken token, int endTokenLength) + { + if (token.IsVerbatimStringLiteral()) { - if (token.IsVerbatimStringLiteral()) - { - return new BraceMatchingResult( - new TextSpan(token.SpanStart, 2), - new TextSpan(token.Span.End - endTokenLength, endTokenLength)); - } - else - { - return new BraceMatchingResult( - new TextSpan(token.SpanStart, 1), - new TextSpan(token.Span.End - endTokenLength, endTokenLength)); - } + return new BraceMatchingResult( + new TextSpan(token.SpanStart, 2), + new TextSpan(token.Span.End - endTokenLength, endTokenLength)); + } + else + { + return new BraceMatchingResult( + new TextSpan(token.SpanStart, 1), + new TextSpan(token.Span.End - endTokenLength, endTokenLength)); } } } diff --git a/src/Features/CSharp/Portable/BracePairs/CSharpBracePairsService.cs b/src/Features/CSharp/Portable/BracePairs/CSharpBracePairsService.cs index 159e4a4dcd031..6af1a80374add 100644 --- a/src/Features/CSharp/Portable/BracePairs/CSharpBracePairsService.cs +++ b/src/Features/CSharp/Portable/BracePairs/CSharpBracePairsService.cs @@ -8,16 +8,15 @@ using Microsoft.CodeAnalysis.CSharp.LanguageService; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.BracePairs +namespace Microsoft.CodeAnalysis.CSharp.BracePairs; + +[ExportLanguageService(typeof(IBracePairsService), LanguageNames.CSharp), Shared] +internal sealed class CSharpBracePairsService : AbstractBracePairsService { - [ExportLanguageService(typeof(IBracePairsService), LanguageNames.CSharp), Shared] - internal sealed class CSharpBracePairsService : AbstractBracePairsService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpBracePairsService() + : base(CSharpSyntaxKinds.Instance) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpBracePairsService() - : base(CSharpSyntaxKinds.Instance) - { - } } } diff --git a/src/Features/CSharp/Portable/ChangeSignature/CSharpChangeSignatureService.cs b/src/Features/CSharp/Portable/ChangeSignature/CSharpChangeSignatureService.cs index f2f0e5ba69770..9aa089ce5b27f 100644 --- a/src/Features/CSharp/Portable/ChangeSignature/CSharpChangeSignatureService.cs +++ b/src/Features/CSharp/Portable/ChangeSignature/CSharpChangeSignatureService.cs @@ -28,64 +28,32 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -namespace Microsoft.CodeAnalysis.CSharp.ChangeSignature -{ - [ExportLanguageService(typeof(AbstractChangeSignatureService), LanguageNames.CSharp), Shared] - internal sealed class CSharpChangeSignatureService : AbstractChangeSignatureService - { - protected override SyntaxGenerator Generator => CSharpSyntaxGenerator.Instance; - protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; - - private static readonly ImmutableArray _declarationKinds = - [ - SyntaxKind.MethodDeclaration, - SyntaxKind.ConstructorDeclaration, - SyntaxKind.IndexerDeclaration, - SyntaxKind.DelegateDeclaration, - SyntaxKind.SimpleLambdaExpression, - SyntaxKind.ParenthesizedLambdaExpression, - SyntaxKind.LocalFunctionStatement, - SyntaxKind.RecordStructDeclaration, - SyntaxKind.RecordDeclaration, - SyntaxKind.StructDeclaration, - SyntaxKind.ClassDeclaration, - ]; - - private static readonly ImmutableArray _declarationAndInvocableKinds = - _declarationKinds.Concat( - [ - SyntaxKind.InvocationExpression, - SyntaxKind.ElementAccessExpression, - SyntaxKind.ThisConstructorInitializer, - SyntaxKind.BaseConstructorInitializer, - SyntaxKind.ObjectCreationExpression, - SyntaxKind.ImplicitObjectCreationExpression, - SyntaxKind.Attribute, - SyntaxKind.NameMemberCref, - ]); - - private static readonly ImmutableArray _updatableAncestorKinds = - [ - SyntaxKind.ConstructorDeclaration, - SyntaxKind.IndexerDeclaration, - SyntaxKind.InvocationExpression, - SyntaxKind.ElementAccessExpression, - SyntaxKind.ThisConstructorInitializer, - SyntaxKind.BaseConstructorInitializer, - SyntaxKind.ObjectCreationExpression, - SyntaxKind.Attribute, - SyntaxKind.DelegateDeclaration, - SyntaxKind.SimpleLambdaExpression, - SyntaxKind.ParenthesizedLambdaExpression, - SyntaxKind.NameMemberCref, - ]; +namespace Microsoft.CodeAnalysis.CSharp.ChangeSignature; - private static readonly ImmutableArray _updatableNodeKinds = +[ExportLanguageService(typeof(AbstractChangeSignatureService), LanguageNames.CSharp), Shared] +internal sealed class CSharpChangeSignatureService : AbstractChangeSignatureService +{ + protected override SyntaxGenerator Generator => CSharpSyntaxGenerator.Instance; + protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; + + private static readonly ImmutableArray _declarationKinds = + [ + SyntaxKind.MethodDeclaration, + SyntaxKind.ConstructorDeclaration, + SyntaxKind.IndexerDeclaration, + SyntaxKind.DelegateDeclaration, + SyntaxKind.SimpleLambdaExpression, + SyntaxKind.ParenthesizedLambdaExpression, + SyntaxKind.LocalFunctionStatement, + SyntaxKind.RecordStructDeclaration, + SyntaxKind.RecordDeclaration, + SyntaxKind.StructDeclaration, + SyntaxKind.ClassDeclaration, + ]; + + private static readonly ImmutableArray _declarationAndInvocableKinds = + _declarationKinds.Concat( [ - SyntaxKind.MethodDeclaration, - SyntaxKind.LocalFunctionStatement, - SyntaxKind.ConstructorDeclaration, - SyntaxKind.IndexerDeclaration, SyntaxKind.InvocationExpression, SyntaxKind.ElementAccessExpression, SyntaxKind.ThisConstructorInitializer, @@ -93,844 +61,875 @@ internal sealed class CSharpChangeSignatureService : AbstractChangeSignatureServ SyntaxKind.ObjectCreationExpression, SyntaxKind.ImplicitObjectCreationExpression, SyntaxKind.Attribute, - SyntaxKind.DelegateDeclaration, SyntaxKind.NameMemberCref, - SyntaxKind.AnonymousMethodExpression, - SyntaxKind.ParenthesizedLambdaExpression, - SyntaxKind.SimpleLambdaExpression, - SyntaxKind.RecordStructDeclaration, - SyntaxKind.RecordDeclaration, - SyntaxKind.StructDeclaration, - SyntaxKind.ClassDeclaration, - ]; + ]); + + private static readonly ImmutableArray _updatableAncestorKinds = + [ + SyntaxKind.ConstructorDeclaration, + SyntaxKind.IndexerDeclaration, + SyntaxKind.InvocationExpression, + SyntaxKind.ElementAccessExpression, + SyntaxKind.ThisConstructorInitializer, + SyntaxKind.BaseConstructorInitializer, + SyntaxKind.ObjectCreationExpression, + SyntaxKind.Attribute, + SyntaxKind.DelegateDeclaration, + SyntaxKind.SimpleLambdaExpression, + SyntaxKind.ParenthesizedLambdaExpression, + SyntaxKind.NameMemberCref, + ]; + + private static readonly ImmutableArray _updatableNodeKinds = + [ + SyntaxKind.MethodDeclaration, + SyntaxKind.LocalFunctionStatement, + SyntaxKind.ConstructorDeclaration, + SyntaxKind.IndexerDeclaration, + SyntaxKind.InvocationExpression, + SyntaxKind.ElementAccessExpression, + SyntaxKind.ThisConstructorInitializer, + SyntaxKind.BaseConstructorInitializer, + SyntaxKind.ObjectCreationExpression, + SyntaxKind.ImplicitObjectCreationExpression, + SyntaxKind.Attribute, + SyntaxKind.DelegateDeclaration, + SyntaxKind.NameMemberCref, + SyntaxKind.AnonymousMethodExpression, + SyntaxKind.ParenthesizedLambdaExpression, + SyntaxKind.SimpleLambdaExpression, + SyntaxKind.RecordStructDeclaration, + SyntaxKind.RecordDeclaration, + SyntaxKind.StructDeclaration, + SyntaxKind.ClassDeclaration, + ]; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpChangeSignatureService() + { + } + + public override async Task<(ISymbol? symbol, int selectedIndex)> GetInvocationSymbolAsync( + Document document, int position, bool restrictToDeclarations, CancellationToken cancellationToken) + { + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var token = root.FindToken(position != tree.Length ? position : Math.Max(0, position - 1)); - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpChangeSignatureService() + // Allow the user to invoke Change-Sig if they've written: Goo(a, b, c);$$ + if (token.Kind() == SyntaxKind.SemicolonToken && token.Parent is StatementSyntax) { + token = token.GetPreviousToken(); + position = token.Span.End; } - public override async Task<(ISymbol? symbol, int selectedIndex)> GetInvocationSymbolAsync( - Document document, int position, bool restrictToDeclarations, CancellationToken cancellationToken) + if (token.Parent == null) { - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + return default; + } - var token = root.FindToken(position != tree.Length ? position : Math.Max(0, position - 1)); + var matchingNode = GetMatchingNode(token.Parent, restrictToDeclarations); + if (matchingNode == null) + { + return default; + } - // Allow the user to invoke Change-Sig if they've written: Goo(a, b, c);$$ - if (token.Kind() == SyntaxKind.SemicolonToken && token.Parent is StatementSyntax) - { - token = token.GetPreviousToken(); - position = token.Span.End; - } + // Don't show change-signature in the random whitespace/trivia for code. + if (!matchingNode.Span.IntersectsWith(position)) + { + return default; + } - if (token.Parent == null) - { - return default; - } + // If we're actually on the declaration of some symbol, ensure that we're + // in a good location for that symbol (i.e. not in the attributes/constraints). + if (!InSymbolHeader(matchingNode, position)) + { + return default; + } - var matchingNode = GetMatchingNode(token.Parent, restrictToDeclarations); - if (matchingNode == null) - { - return default; - } + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var symbol = semanticModel.GetDeclaredSymbol(matchingNode, cancellationToken); + if (symbol != null) + { + var selectedIndex = TryGetSelectedIndexFromDeclaration(position, matchingNode); + return (symbol, selectedIndex); + } - // Don't show change-signature in the random whitespace/trivia for code. - if (!matchingNode.Span.IntersectsWith(position)) + if (matchingNode is ObjectCreationExpressionSyntax objectCreation && + token.Parent.AncestorsAndSelf().Any(a => a == objectCreation.Type)) + { + var typeSymbol = semanticModel.GetSymbolInfo(objectCreation.Type, cancellationToken).Symbol; + if (typeSymbol != null && typeSymbol.IsKind(SymbolKind.NamedType) && ((ITypeSymbol)typeSymbol).TypeKind == TypeKind.Delegate) { - return default; + return (typeSymbol, 0); } + } - // If we're actually on the declaration of some symbol, ensure that we're - // in a good location for that symbol (i.e. not in the attributes/constraints). - if (!InSymbolHeader(matchingNode, position)) - { - return default; - } + var symbolInfo = semanticModel.GetSymbolInfo(matchingNode, cancellationToken); + var parameterIndex = 0; + + // If we're being called on an invocation and not a definition we need to find the selected argument index based on the original definition. + var argumentList = matchingNode is ObjectCreationExpressionSyntax objCreation ? objCreation.ArgumentList + : matchingNode.GetAncestorOrThis()?.ArgumentList; + var argument = argumentList?.Arguments.FirstOrDefault(a => a.Span.Contains(position)); + if (argument != null) + { + parameterIndex = GetParameterIndexFromInvocationArgument(argument, document, semanticModel, cancellationToken); + } - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var symbol = semanticModel.GetDeclaredSymbol(matchingNode, cancellationToken); - if (symbol != null) + return (symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault(), parameterIndex); + } + + private static int TryGetSelectedIndexFromDeclaration(int position, SyntaxNode matchingNode) + { + var parameters = matchingNode.ChildNodes().OfType().SingleOrDefault(); + return parameters != null ? GetParameterIndex(parameters.Parameters, position) : 0; + } + + private static SyntaxNode? GetMatchingNode(SyntaxNode node, bool restrictToDeclarations) + { + var matchKinds = restrictToDeclarations + ? _declarationKinds + : _declarationAndInvocableKinds; + + for (var current = node; current != null; current = current.Parent) + { + if (restrictToDeclarations && + current.Kind() == SyntaxKind.Block || current.Kind() == SyntaxKind.ArrowExpressionClause) { - var selectedIndex = TryGetSelectedIndexFromDeclaration(position, matchingNode); - return (symbol, selectedIndex); + return null; } - if (matchingNode is ObjectCreationExpressionSyntax objectCreation && - token.Parent.AncestorsAndSelf().Any(a => a == objectCreation.Type)) + if (matchKinds.Contains(current.Kind())) { - var typeSymbol = semanticModel.GetSymbolInfo(objectCreation.Type, cancellationToken).Symbol; - if (typeSymbol != null && typeSymbol.IsKind(SymbolKind.NamedType) && ((ITypeSymbol)typeSymbol).TypeKind == TypeKind.Delegate) - { - return (typeSymbol, 0); - } + return current; } + } - var symbolInfo = semanticModel.GetSymbolInfo(matchingNode, cancellationToken); - var parameterIndex = 0; + return null; + } - // If we're being called on an invocation and not a definition we need to find the selected argument index based on the original definition. - var argumentList = matchingNode is ObjectCreationExpressionSyntax objCreation ? objCreation.ArgumentList - : matchingNode.GetAncestorOrThis()?.ArgumentList; - var argument = argumentList?.Arguments.FirstOrDefault(a => a.Span.Contains(position)); - if (argument != null) - { - parameterIndex = GetParameterIndexFromInvocationArgument(argument, document, semanticModel, cancellationToken); - } + private static bool InSymbolHeader(SyntaxNode matchingNode, int position) + { + // Caret has to be after the attributes if the symbol has any. + var lastAttributes = matchingNode.ChildNodes().LastOrDefault(n => n is AttributeListSyntax); + var start = lastAttributes?.GetLastToken().GetNextToken().SpanStart ?? + matchingNode.SpanStart; - return (symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault(), parameterIndex); + if (position < start) + { + return false; } - private static int TryGetSelectedIndexFromDeclaration(int position, SyntaxNode matchingNode) + // If the symbol has a parameter list, then the caret shouldn't be past the end of it. + var parameterList = matchingNode.ChildNodes().LastOrDefault(n => n is ParameterListSyntax); + if (parameterList != null) { - var parameters = matchingNode.ChildNodes().OfType().SingleOrDefault(); - return parameters != null ? GetParameterIndex(parameters.Parameters, position) : 0; + return position <= parameterList.FullSpan.End; } - private static SyntaxNode? GetMatchingNode(SyntaxNode node, bool restrictToDeclarations) + // Case we haven't handled yet. Just assume we're in the header. + return true; + } + + public override SyntaxNode? FindNodeToUpdate(Document document, SyntaxNode node) + { + if (_updatableNodeKinds.Contains(node.Kind())) { - var matchKinds = restrictToDeclarations - ? _declarationKinds - : _declarationAndInvocableKinds; + return node; + } - for (var current = node; current != null; current = current.Parent) - { - if (restrictToDeclarations && - current.Kind() == SyntaxKind.Block || current.Kind() == SyntaxKind.ArrowExpressionClause) - { - return null; - } - - if (matchKinds.Contains(current.Kind())) - { - return current; - } - } + // TODO: file bug about this: var invocation = csnode.Ancestors().FirstOrDefault(a => a.Kind == SyntaxKind.InvocationExpression); + var matchingNode = node.AncestorsAndSelf().FirstOrDefault(n => _updatableAncestorKinds.Contains(n.Kind())); + if (matchingNode == null) + { + return null; + } + var nodeContainingOriginal = GetNodeContainingTargetNode(matchingNode); + if (nodeContainingOriginal == null) + { return null; } - private static bool InSymbolHeader(SyntaxNode matchingNode, int position) + return node.AncestorsAndSelf().Any(n => n == nodeContainingOriginal) ? matchingNode : null; + } + + private static SyntaxNode? GetNodeContainingTargetNode(SyntaxNode matchingNode) + { + switch (matchingNode.Kind()) { - // Caret has to be after the attributes if the symbol has any. - var lastAttributes = matchingNode.ChildNodes().LastOrDefault(n => n is AttributeListSyntax); - var start = lastAttributes?.GetLastToken().GetNextToken().SpanStart ?? - matchingNode.SpanStart; + case SyntaxKind.InvocationExpression: + return ((InvocationExpressionSyntax)matchingNode).Expression; - if (position < start) - { - return false; - } + case SyntaxKind.ElementAccessExpression: + return ((ElementAccessExpressionSyntax)matchingNode).ArgumentList; - // If the symbol has a parameter list, then the caret shouldn't be past the end of it. - var parameterList = matchingNode.ChildNodes().LastOrDefault(n => n is ParameterListSyntax); - if (parameterList != null) - { - return position <= parameterList.FullSpan.End; - } + case SyntaxKind.ObjectCreationExpression: + return ((ObjectCreationExpressionSyntax)matchingNode).Type; - // Case we haven't handled yet. Just assume we're in the header. - return true; + case SyntaxKind.ConstructorDeclaration: + case SyntaxKind.IndexerDeclaration: + case SyntaxKind.ThisConstructorInitializer: + case SyntaxKind.BaseConstructorInitializer: + case SyntaxKind.Attribute: + case SyntaxKind.DelegateDeclaration: + case SyntaxKind.NameMemberCref: + return matchingNode; + + default: + return null; } + } - public override SyntaxNode? FindNodeToUpdate(Document document, SyntaxNode node) + public override async Task ChangeSignatureAsync( + Document document, + ISymbol declarationSymbol, + SyntaxNode potentiallyUpdatedNode, + SyntaxNode originalNode, + SignatureChange signaturePermutation, + LineFormattingOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var updatedNode = potentiallyUpdatedNode as CSharpSyntaxNode; + + // Update tags. + if (updatedNode?.Kind() + is SyntaxKind.MethodDeclaration + or SyntaxKind.ConstructorDeclaration + or SyntaxKind.IndexerDeclaration + or SyntaxKind.DelegateDeclaration + or SyntaxKind.RecordStructDeclaration + or SyntaxKind.RecordDeclaration + or SyntaxKind.StructDeclaration + or SyntaxKind.ClassDeclaration) { - if (_updatableNodeKinds.Contains(node.Kind())) + var updatedLeadingTrivia = await UpdateParamTagsInLeadingTriviaAsync(document, updatedNode, declarationSymbol, signaturePermutation, fallbackOptions, cancellationToken).ConfigureAwait(false); + if (updatedLeadingTrivia != default && !updatedLeadingTrivia.IsEmpty) { - return node; + updatedNode = updatedNode.WithLeadingTrivia(updatedLeadingTrivia); } + } - // TODO: file bug about this: var invocation = csnode.Ancestors().FirstOrDefault(a => a.Kind == SyntaxKind.InvocationExpression); - var matchingNode = node.AncestorsAndSelf().FirstOrDefault(n => _updatableAncestorKinds.Contains(n.Kind())); - if (matchingNode == null) - { - return null; - } + // Update declarations parameter lists + if (updatedNode is MethodDeclarationSyntax method) + { + var updatedParameters = UpdateDeclaration(method.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); + return method.WithParameterList(method.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); + } - var nodeContainingOriginal = GetNodeContainingTargetNode(matchingNode); - if (nodeContainingOriginal == null) - { - return null; - } + if (updatedNode is TypeDeclarationSyntax { ParameterList: not null } typeWithParameters) + { + var updatedParameters = UpdateDeclaration(typeWithParameters.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); + return typeWithParameters.WithParameterList(typeWithParameters.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); + } - return node.AncestorsAndSelf().Any(n => n == nodeContainingOriginal) ? matchingNode : null; + if (updatedNode is LocalFunctionStatementSyntax localFunction) + { + var updatedParameters = UpdateDeclaration(localFunction.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); + return localFunction.WithParameterList(localFunction.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); } - private static SyntaxNode? GetNodeContainingTargetNode(SyntaxNode matchingNode) + if (updatedNode is ConstructorDeclarationSyntax constructor) { - switch (matchingNode.Kind()) - { - case SyntaxKind.InvocationExpression: - return ((InvocationExpressionSyntax)matchingNode).Expression; - - case SyntaxKind.ElementAccessExpression: - return ((ElementAccessExpressionSyntax)matchingNode).ArgumentList; - - case SyntaxKind.ObjectCreationExpression: - return ((ObjectCreationExpressionSyntax)matchingNode).Type; - - case SyntaxKind.ConstructorDeclaration: - case SyntaxKind.IndexerDeclaration: - case SyntaxKind.ThisConstructorInitializer: - case SyntaxKind.BaseConstructorInitializer: - case SyntaxKind.Attribute: - case SyntaxKind.DelegateDeclaration: - case SyntaxKind.NameMemberCref: - return matchingNode; - - default: - return null; - } + var updatedParameters = UpdateDeclaration(constructor.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); + return constructor.WithParameterList(constructor.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); } - public override async Task ChangeSignatureAsync( - Document document, - ISymbol declarationSymbol, - SyntaxNode potentiallyUpdatedNode, - SyntaxNode originalNode, - SignatureChange signaturePermutation, - LineFormattingOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - var updatedNode = potentiallyUpdatedNode as CSharpSyntaxNode; - - // Update tags. - if (updatedNode?.Kind() - is SyntaxKind.MethodDeclaration - or SyntaxKind.ConstructorDeclaration - or SyntaxKind.IndexerDeclaration - or SyntaxKind.DelegateDeclaration - or SyntaxKind.RecordStructDeclaration - or SyntaxKind.RecordDeclaration - or SyntaxKind.StructDeclaration - or SyntaxKind.ClassDeclaration) - { - var updatedLeadingTrivia = await UpdateParamTagsInLeadingTriviaAsync(document, updatedNode, declarationSymbol, signaturePermutation, fallbackOptions, cancellationToken).ConfigureAwait(false); - if (updatedLeadingTrivia != default && !updatedLeadingTrivia.IsEmpty) - { - updatedNode = updatedNode.WithLeadingTrivia(updatedLeadingTrivia); - } - } + if (updatedNode is IndexerDeclarationSyntax indexer) + { + var updatedParameters = UpdateDeclaration(indexer.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); + return indexer.WithParameterList(indexer.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); + } - // Update declarations parameter lists - if (updatedNode is MethodDeclarationSyntax method) - { - var updatedParameters = UpdateDeclaration(method.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); - return method.WithParameterList(method.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); - } + if (updatedNode is DelegateDeclarationSyntax delegateDeclaration) + { + var updatedParameters = UpdateDeclaration(delegateDeclaration.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); + return delegateDeclaration.WithParameterList(delegateDeclaration.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); + } - if (updatedNode is TypeDeclarationSyntax { ParameterList: not null } typeWithParameters) + if (updatedNode is AnonymousMethodExpressionSyntax anonymousMethod) + { + // Delegates may omit parameters in C# + if (anonymousMethod.ParameterList == null) { - var updatedParameters = UpdateDeclaration(typeWithParameters.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); - return typeWithParameters.WithParameterList(typeWithParameters.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); + return anonymousMethod; } - if (updatedNode is LocalFunctionStatementSyntax localFunction) - { - var updatedParameters = UpdateDeclaration(localFunction.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); - return localFunction.WithParameterList(localFunction.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); - } + var updatedParameters = UpdateDeclaration(anonymousMethod.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); + return anonymousMethod.WithParameterList(anonymousMethod.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); + } - if (updatedNode is ConstructorDeclarationSyntax constructor) + if (updatedNode is SimpleLambdaExpressionSyntax lambda) + { + if (signaturePermutation.UpdatedConfiguration.ToListOfParameters().Any()) { - var updatedParameters = UpdateDeclaration(constructor.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); - return constructor.WithParameterList(constructor.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); + var updatedParameters = UpdateDeclaration([lambda.Parameter], signaturePermutation, CreateNewParameterSyntax); + return ParenthesizedLambdaExpression( + lambda.AsyncKeyword, + ParameterList(updatedParameters), + lambda.ArrowToken, + lambda.Body); } - - if (updatedNode is IndexerDeclarationSyntax indexer) + else { - var updatedParameters = UpdateDeclaration(indexer.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); - return indexer.WithParameterList(indexer.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); - } + // No parameters. Change to a parenthesized lambda expression + var emptyParameterList = ParameterList() + .WithLeadingTrivia(lambda.Parameter.GetLeadingTrivia()) + .WithTrailingTrivia(lambda.Parameter.GetTrailingTrivia()); - if (updatedNode is DelegateDeclarationSyntax delegateDeclaration) - { - var updatedParameters = UpdateDeclaration(delegateDeclaration.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); - return delegateDeclaration.WithParameterList(delegateDeclaration.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); + return ParenthesizedLambdaExpression(lambda.AsyncKeyword, emptyParameterList, lambda.ArrowToken, lambda.Body); } + } - if (updatedNode is AnonymousMethodExpressionSyntax anonymousMethod) - { - // Delegates may omit parameters in C# - if (anonymousMethod.ParameterList == null) - { - return anonymousMethod; - } - - var updatedParameters = UpdateDeclaration(anonymousMethod.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax); - return anonymousMethod.WithParameterList(anonymousMethod.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation)); - } + if (updatedNode is ParenthesizedLambdaExpressionSyntax parenLambda) + { + var doNotSkipParameterType = parenLambda.ParameterList.Parameters.FirstOrDefault()?.Type != null; + + var updatedParameters = UpdateDeclaration( + parenLambda.ParameterList.Parameters, + signaturePermutation, + p => CreateNewParameterSyntax(p, !doNotSkipParameterType)); + return parenLambda.WithParameterList(parenLambda.ParameterList.WithParameters(updatedParameters)); + } - if (updatedNode is SimpleLambdaExpressionSyntax lambda) + // Handle references in crefs + if (updatedNode is NameMemberCrefSyntax nameMemberCref) + { + if (nameMemberCref.Parameters == null || + !nameMemberCref.Parameters.Parameters.Any()) { - if (signaturePermutation.UpdatedConfiguration.ToListOfParameters().Any()) - { - var updatedParameters = UpdateDeclaration([lambda.Parameter], signaturePermutation, CreateNewParameterSyntax); - return ParenthesizedLambdaExpression( - lambda.AsyncKeyword, - ParameterList(updatedParameters), - lambda.ArrowToken, - lambda.Body); - } - else - { - // No parameters. Change to a parenthesized lambda expression - var emptyParameterList = ParameterList() - .WithLeadingTrivia(lambda.Parameter.GetLeadingTrivia()) - .WithTrailingTrivia(lambda.Parameter.GetTrailingTrivia()); - - return ParenthesizedLambdaExpression(lambda.AsyncKeyword, emptyParameterList, lambda.ArrowToken, lambda.Body); - } + return nameMemberCref; } - if (updatedNode is ParenthesizedLambdaExpressionSyntax parenLambda) - { - var doNotSkipParameterType = parenLambda.ParameterList.Parameters.FirstOrDefault()?.Type != null; + var newParameters = UpdateDeclaration(nameMemberCref.Parameters.Parameters, signaturePermutation, CreateNewCrefParameterSyntax); - var updatedParameters = UpdateDeclaration( - parenLambda.ParameterList.Parameters, + var newCrefParameterList = nameMemberCref.Parameters.WithParameters(newParameters); + return nameMemberCref.WithParameters(newCrefParameterList); + } + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + // Update reference site argument lists + if (updatedNode is InvocationExpressionSyntax invocation) + { + var symbolInfo = semanticModel.GetSymbolInfo((InvocationExpressionSyntax)originalNode, cancellationToken); + + return invocation.WithArgumentList( + await UpdateArgumentListAsync( + declarationSymbol, signaturePermutation, - p => CreateNewParameterSyntax(p, !doNotSkipParameterType)); - return parenLambda.WithParameterList(parenLambda.ParameterList.WithParameters(updatedParameters)); - } + invocation.ArgumentList, + symbolInfo.Symbol is IMethodSymbol { MethodKind: MethodKind.ReducedExtension }, + IsParamsArrayExpanded(semanticModel, invocation, symbolInfo, cancellationToken), + document, + originalNode.SpanStart, + cancellationToken).ConfigureAwait(false)); + } - // Handle references in crefs - if (updatedNode is NameMemberCrefSyntax nameMemberCref) + // Handles both ObjectCreationExpressionSyntax and ImplicitObjectCreationExpressionSyntax + if (updatedNode is BaseObjectCreationExpressionSyntax objCreation) + { + if (objCreation.ArgumentList == null) { - if (nameMemberCref.Parameters == null || - !nameMemberCref.Parameters.Parameters.Any()) - { - return nameMemberCref; - } + return updatedNode; + } - var newParameters = UpdateDeclaration(nameMemberCref.Parameters.Parameters, signaturePermutation, CreateNewCrefParameterSyntax); + var symbolInfo = semanticModel.GetSymbolInfo((BaseObjectCreationExpressionSyntax)originalNode, cancellationToken); - var newCrefParameterList = nameMemberCref.Parameters.WithParameters(newParameters); - return nameMemberCref.WithParameters(newCrefParameterList); - } + return objCreation.WithArgumentList( + await UpdateArgumentListAsync( + declarationSymbol, + signaturePermutation, + objCreation.ArgumentList, + isReducedExtensionMethod: false, + IsParamsArrayExpanded(semanticModel, objCreation, symbolInfo, cancellationToken), + document, + originalNode.SpanStart, + cancellationToken).ConfigureAwait(false)); + } - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (updatedNode is ConstructorInitializerSyntax constructorInit) + { + var symbolInfo = semanticModel.GetSymbolInfo((ConstructorInitializerSyntax)originalNode, cancellationToken); - // Update reference site argument lists - if (updatedNode is InvocationExpressionSyntax invocation) - { - var symbolInfo = semanticModel.GetSymbolInfo((InvocationExpressionSyntax)originalNode, cancellationToken); - - return invocation.WithArgumentList( - await UpdateArgumentListAsync( - declarationSymbol, - signaturePermutation, - invocation.ArgumentList, - symbolInfo.Symbol is IMethodSymbol { MethodKind: MethodKind.ReducedExtension }, - IsParamsArrayExpanded(semanticModel, invocation, symbolInfo, cancellationToken), - document, - originalNode.SpanStart, - cancellationToken).ConfigureAwait(false)); - } + return constructorInit.WithArgumentList( + await UpdateArgumentListAsync( + declarationSymbol, + signaturePermutation, + constructorInit.ArgumentList, + isReducedExtensionMethod: false, + IsParamsArrayExpanded(semanticModel, constructorInit, symbolInfo, cancellationToken), + document, + originalNode.SpanStart, + cancellationToken).ConfigureAwait(false)); + } - // Handles both ObjectCreationExpressionSyntax and ImplicitObjectCreationExpressionSyntax - if (updatedNode is BaseObjectCreationExpressionSyntax objCreation) - { - if (objCreation.ArgumentList == null) - { - return updatedNode; - } - - var symbolInfo = semanticModel.GetSymbolInfo((BaseObjectCreationExpressionSyntax)originalNode, cancellationToken); - - return objCreation.WithArgumentList( - await UpdateArgumentListAsync( - declarationSymbol, - signaturePermutation, - objCreation.ArgumentList, - isReducedExtensionMethod: false, - IsParamsArrayExpanded(semanticModel, objCreation, symbolInfo, cancellationToken), - document, - originalNode.SpanStart, - cancellationToken).ConfigureAwait(false)); - } + if (updatedNode is ElementAccessExpressionSyntax elementAccess) + { + var symbolInfo = semanticModel.GetSymbolInfo((ElementAccessExpressionSyntax)originalNode, cancellationToken); - if (updatedNode is ConstructorInitializerSyntax constructorInit) - { - var symbolInfo = semanticModel.GetSymbolInfo((ConstructorInitializerSyntax)originalNode, cancellationToken); - - return constructorInit.WithArgumentList( - await UpdateArgumentListAsync( - declarationSymbol, - signaturePermutation, - constructorInit.ArgumentList, - isReducedExtensionMethod: false, - IsParamsArrayExpanded(semanticModel, constructorInit, symbolInfo, cancellationToken), - document, - originalNode.SpanStart, - cancellationToken).ConfigureAwait(false)); - } + return elementAccess.WithArgumentList( + await UpdateArgumentListAsync( + declarationSymbol, + signaturePermutation, + elementAccess.ArgumentList, + isReducedExtensionMethod: false, + IsParamsArrayExpanded(semanticModel, elementAccess, symbolInfo, cancellationToken), + document, + originalNode.SpanStart, + cancellationToken).ConfigureAwait(false)); + } - if (updatedNode is ElementAccessExpressionSyntax elementAccess) - { - var symbolInfo = semanticModel.GetSymbolInfo((ElementAccessExpressionSyntax)originalNode, cancellationToken); - - return elementAccess.WithArgumentList( - await UpdateArgumentListAsync( - declarationSymbol, - signaturePermutation, - elementAccess.ArgumentList, - isReducedExtensionMethod: false, - IsParamsArrayExpanded(semanticModel, elementAccess, symbolInfo, cancellationToken), - document, - originalNode.SpanStart, - cancellationToken).ConfigureAwait(false)); - } + if (updatedNode is AttributeSyntax attribute) + { + var symbolInfo = semanticModel.GetSymbolInfo((AttributeSyntax)originalNode, cancellationToken); - if (updatedNode is AttributeSyntax attribute) + if (attribute.ArgumentList == null) { - var symbolInfo = semanticModel.GetSymbolInfo((AttributeSyntax)originalNode, cancellationToken); - - if (attribute.ArgumentList == null) - { - return updatedNode; - } - - return attribute.WithArgumentList( - await UpdateAttributeArgumentListAsync( - declarationSymbol, - signaturePermutation, - attribute.ArgumentList, - isReducedExtensionMethod: false, - IsParamsArrayExpanded(semanticModel, attribute, symbolInfo, cancellationToken), - document, - originalNode.SpanStart, - cancellationToken).ConfigureAwait(false)); + return updatedNode; } - Debug.Assert(false, "Unknown reference location"); - return null; + return attribute.WithArgumentList( + await UpdateAttributeArgumentListAsync( + declarationSymbol, + signaturePermutation, + attribute.ArgumentList, + isReducedExtensionMethod: false, + IsParamsArrayExpanded(semanticModel, attribute, symbolInfo, cancellationToken), + document, + originalNode.SpanStart, + cancellationToken).ConfigureAwait(false)); } - private async Task UpdateArgumentListAsync( - ISymbol declarationSymbol, - SignatureChange signaturePermutation, - T argumentList, - bool isReducedExtensionMethod, - bool isParamsArrayExpanded, - Document document, - int position, - CancellationToken cancellationToken) where T : BaseArgumentListSyntax - { - // Reorders and removes arguments - // e.g. P(a, b, c) ==> P(c, a) - var newArguments = PermuteArgumentList( - declarationSymbol, - argumentList.Arguments, - signaturePermutation.WithoutAddedParameters(), - isReducedExtensionMethod); - - // Adds new arguments into the updated list - // e.g. P(c, a) ==> P(x, c, a, y) - newArguments = await AddNewArgumentsToListAsync( - declarationSymbol, - newArguments, - argumentList.Arguments, - signaturePermutation, - isReducedExtensionMethod, - isParamsArrayExpanded, - generateAttributeArguments: false, - document, - position, - cancellationToken).ConfigureAwait(false); - - return (T)argumentList - .WithArguments(newArguments) - .WithAdditionalAnnotations(changeSignatureFormattingAnnotation); - } - - private async Task UpdateAttributeArgumentListAsync( - ISymbol declarationSymbol, - SignatureChange signaturePermutation, - AttributeArgumentListSyntax argumentList, - bool isReducedExtensionMethod, - bool isParamsArrayExpanded, - Document document, - int position, - CancellationToken cancellationToken) - { - var newArguments = PermuteAttributeArgumentList( - declarationSymbol, - argumentList.Arguments, - signaturePermutation.WithoutAddedParameters()); - - newArguments = await AddNewArgumentsToListAsync( - declarationSymbol, - newArguments, - argumentList.Arguments, - signaturePermutation, - isReducedExtensionMethod, - isParamsArrayExpanded, - generateAttributeArguments: true, - document, - position, - cancellationToken).ConfigureAwait(false); + Debug.Assert(false, "Unknown reference location"); + return null; + } - return argumentList - .WithArguments(newArguments) - .WithAdditionalAnnotations(changeSignatureFormattingAnnotation); + private async Task UpdateArgumentListAsync( + ISymbol declarationSymbol, + SignatureChange signaturePermutation, + T argumentList, + bool isReducedExtensionMethod, + bool isParamsArrayExpanded, + Document document, + int position, + CancellationToken cancellationToken) where T : BaseArgumentListSyntax + { + // Reorders and removes arguments + // e.g. P(a, b, c) ==> P(c, a) + var newArguments = PermuteArgumentList( + declarationSymbol, + argumentList.Arguments, + signaturePermutation.WithoutAddedParameters(), + isReducedExtensionMethod); + + // Adds new arguments into the updated list + // e.g. P(c, a) ==> P(x, c, a, y) + newArguments = await AddNewArgumentsToListAsync( + declarationSymbol, + newArguments, + argumentList.Arguments, + signaturePermutation, + isReducedExtensionMethod, + isParamsArrayExpanded, + generateAttributeArguments: false, + document, + position, + cancellationToken).ConfigureAwait(false); + + return (T)argumentList + .WithArguments(newArguments) + .WithAdditionalAnnotations(changeSignatureFormattingAnnotation); + } + + private async Task UpdateAttributeArgumentListAsync( + ISymbol declarationSymbol, + SignatureChange signaturePermutation, + AttributeArgumentListSyntax argumentList, + bool isReducedExtensionMethod, + bool isParamsArrayExpanded, + Document document, + int position, + CancellationToken cancellationToken) + { + var newArguments = PermuteAttributeArgumentList( + declarationSymbol, + argumentList.Arguments, + signaturePermutation.WithoutAddedParameters()); + + newArguments = await AddNewArgumentsToListAsync( + declarationSymbol, + newArguments, + argumentList.Arguments, + signaturePermutation, + isReducedExtensionMethod, + isParamsArrayExpanded, + generateAttributeArguments: true, + document, + position, + cancellationToken).ConfigureAwait(false); + + return argumentList + .WithArguments(newArguments) + .WithAdditionalAnnotations(changeSignatureFormattingAnnotation); + } + + private static bool IsParamsArrayExpanded(SemanticModel semanticModel, SyntaxNode node, SymbolInfo symbolInfo, CancellationToken cancellationToken) + { + if (symbolInfo.Symbol == null) + { + return false; } - private static bool IsParamsArrayExpanded(SemanticModel semanticModel, SyntaxNode node, SymbolInfo symbolInfo, CancellationToken cancellationToken) + int argumentCount; + bool lastArgumentIsNamed; + ExpressionSyntax lastArgumentExpression; + + if (node is AttributeSyntax attribute) { - if (symbolInfo.Symbol == null) + if (attribute.ArgumentList == null) { return false; } - int argumentCount; - bool lastArgumentIsNamed; - ExpressionSyntax lastArgumentExpression; + argumentCount = attribute.ArgumentList.Arguments.Count; + lastArgumentIsNamed = attribute.ArgumentList.Arguments.LastOrDefault()?.NameColon != null || + attribute.ArgumentList.Arguments.LastOrDefault()?.NameEquals != null; - if (node is AttributeSyntax attribute) + var lastArgument = attribute.ArgumentList.Arguments.LastOrDefault(); + if (lastArgument == null) { - if (attribute.ArgumentList == null) - { - return false; - } - - argumentCount = attribute.ArgumentList.Arguments.Count; - lastArgumentIsNamed = attribute.ArgumentList.Arguments.LastOrDefault()?.NameColon != null || - attribute.ArgumentList.Arguments.LastOrDefault()?.NameEquals != null; - - var lastArgument = attribute.ArgumentList.Arguments.LastOrDefault(); - if (lastArgument == null) - { - return false; - } - - lastArgumentExpression = lastArgument.Expression; + return false; } - else + + lastArgumentExpression = lastArgument.Expression; + } + else + { + BaseArgumentListSyntax? argumentList = node switch + { + InvocationExpressionSyntax invocation => invocation.ArgumentList, + BaseObjectCreationExpressionSyntax objectCreation => objectCreation.ArgumentList, + ConstructorInitializerSyntax constructorInitializer => constructorInitializer.ArgumentList, + ElementAccessExpressionSyntax elementAccess => elementAccess.ArgumentList, + _ => throw ExceptionUtilities.UnexpectedValue(node.Kind()) + }; + + if (argumentList == null) + { + return false; + } + + argumentCount = argumentList.Arguments.Count; + lastArgumentIsNamed = argumentList.Arguments.LastOrDefault()?.NameColon != null; + + var lastArgument = argumentList.Arguments.LastOrDefault(); + if (lastArgument == null) { - BaseArgumentListSyntax? argumentList = node switch - { - InvocationExpressionSyntax invocation => invocation.ArgumentList, - BaseObjectCreationExpressionSyntax objectCreation => objectCreation.ArgumentList, - ConstructorInitializerSyntax constructorInitializer => constructorInitializer.ArgumentList, - ElementAccessExpressionSyntax elementAccess => elementAccess.ArgumentList, - _ => throw ExceptionUtilities.UnexpectedValue(node.Kind()) - }; - - if (argumentList == null) - { - return false; - } - - argumentCount = argumentList.Arguments.Count; - lastArgumentIsNamed = argumentList.Arguments.LastOrDefault()?.NameColon != null; - - var lastArgument = argumentList.Arguments.LastOrDefault(); - if (lastArgument == null) - { - return false; - } - - lastArgumentExpression = lastArgument.Expression; + return false; } - return IsParamsArrayExpandedHelper(symbolInfo.Symbol, argumentCount, lastArgumentIsNamed, semanticModel, lastArgumentExpression, cancellationToken); + lastArgumentExpression = lastArgument.Expression; } - private static ParameterSyntax CreateNewParameterSyntax(AddedParameter addedParameter) - => CreateNewParameterSyntax(addedParameter, skipParameterType: false); + return IsParamsArrayExpandedHelper(symbolInfo.Symbol, argumentCount, lastArgumentIsNamed, semanticModel, lastArgumentExpression, cancellationToken); + } - private static ParameterSyntax CreateNewParameterSyntax(AddedParameter addedParameter, bool skipParameterType) - { - var equalsValueClause = addedParameter.HasDefaultValue - ? EqualsValueClause(ParseExpression(addedParameter.DefaultValue)) - : null; + private static ParameterSyntax CreateNewParameterSyntax(AddedParameter addedParameter) + => CreateNewParameterSyntax(addedParameter, skipParameterType: false); - return Parameter( - attributeLists: default, - modifiers: default, - type: skipParameterType - ? null - : addedParameter.Type.GenerateTypeSyntax(), - Identifier(addedParameter.Name), - @default: equalsValueClause); - } + private static ParameterSyntax CreateNewParameterSyntax(AddedParameter addedParameter, bool skipParameterType) + { + var equalsValueClause = addedParameter.HasDefaultValue + ? EqualsValueClause(ParseExpression(addedParameter.DefaultValue)) + : null; + + return Parameter( + attributeLists: default, + modifiers: default, + type: skipParameterType + ? null + : addedParameter.Type.GenerateTypeSyntax(), + Identifier(addedParameter.Name), + @default: equalsValueClause); + } - private static CrefParameterSyntax CreateNewCrefParameterSyntax(AddedParameter addedParameter) - => CrefParameter(type: addedParameter.Type.GenerateTypeSyntax()) - .WithLeadingTrivia(ElasticSpace); + private static CrefParameterSyntax CreateNewCrefParameterSyntax(AddedParameter addedParameter) + => CrefParameter(type: addedParameter.Type.GenerateTypeSyntax()) + .WithLeadingTrivia(ElasticSpace); - private SeparatedSyntaxList UpdateDeclaration( - SeparatedSyntaxList list, - SignatureChange updatedSignature, - Func createNewParameterMethod) where T : SyntaxNode + private SeparatedSyntaxList UpdateDeclaration( + SeparatedSyntaxList list, + SignatureChange updatedSignature, + Func createNewParameterMethod) where T : SyntaxNode + { + var (parameters, separators) = base.UpdateDeclarationBase(list, updatedSignature, createNewParameterMethod); + return SeparatedList(parameters, separators); + } + + protected override T TransferLeadingWhitespaceTrivia(T newArgument, SyntaxNode oldArgument) + { + var oldTrivia = oldArgument.GetLeadingTrivia(); + var oldOnlyHasWhitespaceTrivia = oldTrivia.All(t => t.IsKind(SyntaxKind.WhitespaceTrivia)); + + var newTrivia = newArgument.GetLeadingTrivia(); + var newOnlyHasWhitespaceTrivia = newTrivia.All(t => t.IsKind(SyntaxKind.WhitespaceTrivia)); + + if (oldOnlyHasWhitespaceTrivia && newOnlyHasWhitespaceTrivia) { - var (parameters, separators) = base.UpdateDeclarationBase(list, updatedSignature, createNewParameterMethod); - return SeparatedList(parameters, separators); + newArgument = newArgument.WithLeadingTrivia(oldTrivia); } - protected override T TransferLeadingWhitespaceTrivia(T newArgument, SyntaxNode oldArgument) - { - var oldTrivia = oldArgument.GetLeadingTrivia(); - var oldOnlyHasWhitespaceTrivia = oldTrivia.All(t => t.IsKind(SyntaxKind.WhitespaceTrivia)); + return newArgument; + } - var newTrivia = newArgument.GetLeadingTrivia(); - var newOnlyHasWhitespaceTrivia = newTrivia.All(t => t.IsKind(SyntaxKind.WhitespaceTrivia)); + private async Task> AddNewArgumentsToListAsync( + ISymbol declarationSymbol, + SeparatedSyntaxList newArguments, + SeparatedSyntaxList originalArguments, + SignatureChange signaturePermutation, + bool isReducedExtensionMethod, + bool isParamsArrayExpanded, + bool generateAttributeArguments, + Document document, + int position, + CancellationToken cancellationToken) + where TArgumentSyntax : SyntaxNode + { + var newArgumentList = await AddNewArgumentsToListAsync( + declarationSymbol, newArguments, + signaturePermutation, isReducedExtensionMethod, + isParamsArrayExpanded, generateAttributeArguments, + document, position, cancellationToken).ConfigureAwait(false); + + return SeparatedList( + TransferLeadingWhitespaceTrivia(newArgumentList, originalArguments), + newArgumentList.GetSeparators()); + } - if (oldOnlyHasWhitespaceTrivia && newOnlyHasWhitespaceTrivia) - { - newArgument = newArgument.WithLeadingTrivia(oldTrivia); - } + private SeparatedSyntaxList PermuteAttributeArgumentList( + ISymbol declarationSymbol, + SeparatedSyntaxList arguments, + SignatureChange updatedSignature) + { + var newArguments = PermuteArguments(declarationSymbol, arguments.Select(a => UnifiedArgumentSyntax.Create(a)).ToImmutableArray(), + updatedSignature); + var numSeparatorsToSkip = arguments.Count - newArguments.Length; - return newArgument; - } + // copy whitespace trivia from original position + var newArgumentsWithTrivia = TransferLeadingWhitespaceTrivia( + newArguments.Select(a => (AttributeArgumentSyntax)(UnifiedArgumentSyntax)a), arguments); + + return SeparatedList(newArgumentsWithTrivia, GetSeparators(arguments, numSeparatorsToSkip)); + } + + private SeparatedSyntaxList PermuteArgumentList( + ISymbol declarationSymbol, + SeparatedSyntaxList arguments, + SignatureChange updatedSignature, + bool isReducedExtensionMethod = false) + { + var newArguments = PermuteArguments( + declarationSymbol, + arguments.Select(a => UnifiedArgumentSyntax.Create(a)).ToImmutableArray(), + updatedSignature, + isReducedExtensionMethod); + + // copy whitespace trivia from original position + var newArgumentsWithTrivia = TransferLeadingWhitespaceTrivia( + newArguments.Select(a => (ArgumentSyntax)(UnifiedArgumentSyntax)a), arguments); + + var numSeparatorsToSkip = arguments.Count - newArguments.Length; + return SeparatedList(newArgumentsWithTrivia, GetSeparators(arguments, numSeparatorsToSkip)); + } - private async Task> AddNewArgumentsToListAsync( - ISymbol declarationSymbol, - SeparatedSyntaxList newArguments, - SeparatedSyntaxList originalArguments, - SignatureChange signaturePermutation, - bool isReducedExtensionMethod, - bool isParamsArrayExpanded, - bool generateAttributeArguments, - Document document, - int position, - CancellationToken cancellationToken) - where TArgumentSyntax : SyntaxNode + private ImmutableArray TransferLeadingWhitespaceTrivia(IEnumerable newArguments, SeparatedSyntaxList oldArguments) + where T : SyntaxNode + where U : SyntaxNode + { + var result = ImmutableArray.CreateBuilder(); + var index = 0; + foreach (var newArgument in newArguments) { - var newArgumentList = await AddNewArgumentsToListAsync( - declarationSymbol, newArguments, - signaturePermutation, isReducedExtensionMethod, - isParamsArrayExpanded, generateAttributeArguments, - document, position, cancellationToken).ConfigureAwait(false); + result.Add(index < oldArguments.Count + ? TransferLeadingWhitespaceTrivia(newArgument, oldArguments[index]) + : newArgument); - return SeparatedList( - TransferLeadingWhitespaceTrivia(newArgumentList, originalArguments), - newArgumentList.GetSeparators()); + index++; } - private SeparatedSyntaxList PermuteAttributeArgumentList( - ISymbol declarationSymbol, - SeparatedSyntaxList arguments, - SignatureChange updatedSignature) + return result.ToImmutable(); + } + + private async ValueTask> UpdateParamTagsInLeadingTriviaAsync( + Document document, CSharpSyntaxNode node, ISymbol declarationSymbol, SignatureChange updatedSignature, LineFormattingOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + if (!node.HasLeadingTrivia) { - var newArguments = PermuteArguments(declarationSymbol, arguments.Select(a => UnifiedArgumentSyntax.Create(a)).ToImmutableArray(), - updatedSignature); - var numSeparatorsToSkip = arguments.Count - newArguments.Length; + return []; + } - // copy whitespace trivia from original position - var newArgumentsWithTrivia = TransferLeadingWhitespaceTrivia( - newArguments.Select(a => (AttributeArgumentSyntax)(UnifiedArgumentSyntax)a), arguments); + var paramNodes = node + .DescendantNodes(descendIntoTrivia: true) + .OfType() + .Where(e => e.StartTag.Name.ToString() == DocumentationCommentXmlNames.ParameterElementName); - return SeparatedList(newArgumentsWithTrivia, GetSeparators(arguments, numSeparatorsToSkip)); + var permutedParamNodes = VerifyAndPermuteParamNodes(paramNodes, declarationSymbol, updatedSignature); + if (permutedParamNodes.IsEmpty) + { + return []; } - private SeparatedSyntaxList PermuteArgumentList( - ISymbol declarationSymbol, - SeparatedSyntaxList arguments, - SignatureChange updatedSignature, - bool isReducedExtensionMethod = false) - { - var newArguments = PermuteArguments( - declarationSymbol, - arguments.Select(a => UnifiedArgumentSyntax.Create(a)).ToImmutableArray(), - updatedSignature, - isReducedExtensionMethod); + var options = await document.GetLineFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + return GetPermutedDocCommentTrivia(node, permutedParamNodes, document.Project.Services, options); + } - // copy whitespace trivia from original position - var newArgumentsWithTrivia = TransferLeadingWhitespaceTrivia( - newArguments.Select(a => (ArgumentSyntax)(UnifiedArgumentSyntax)a), arguments); + private ImmutableArray VerifyAndPermuteParamNodes(IEnumerable paramNodes, ISymbol declarationSymbol, SignatureChange updatedSignature) + { + // Only reorder if count and order match originally. + var originalParameters = updatedSignature.OriginalConfiguration.ToListOfParameters(); + var reorderedParameters = updatedSignature.UpdatedConfiguration.ToListOfParameters(); - var numSeparatorsToSkip = arguments.Count - newArguments.Length; - return SeparatedList(newArgumentsWithTrivia, GetSeparators(arguments, numSeparatorsToSkip)); - } + var declaredParameters = GetParameters(declarationSymbol); - private ImmutableArray TransferLeadingWhitespaceTrivia(IEnumerable newArguments, SeparatedSyntaxList oldArguments) - where T : SyntaxNode - where U : SyntaxNode + if (paramNodes.Count() != declaredParameters.Length) { - var result = ImmutableArray.CreateBuilder(); - var index = 0; - foreach (var newArgument in newArguments) - { - result.Add(index < oldArguments.Count - ? TransferLeadingWhitespaceTrivia(newArgument, oldArguments[index]) - : newArgument); - - index++; - } + return []; + } - return result.ToImmutable(); + // No parameters originally, so no param nodes to permute. + if (declaredParameters.Length == 0) + { + return []; } - private async ValueTask> UpdateParamTagsInLeadingTriviaAsync( - Document document, CSharpSyntaxNode node, ISymbol declarationSymbol, SignatureChange updatedSignature, LineFormattingOptionsProvider fallbackOptions, CancellationToken cancellationToken) + var dictionary = new Dictionary(); + var i = 0; + foreach (var paramNode in paramNodes) { - if (!node.HasLeadingTrivia) + var nameAttribute = paramNode.StartTag.Attributes.FirstOrDefault(a => a.Name.ToString().Equals("name", StringComparison.OrdinalIgnoreCase)); + if (nameAttribute == null) { return []; } - var paramNodes = node - .DescendantNodes(descendIntoTrivia: true) - .OfType() - .Where(e => e.StartTag.Name.ToString() == DocumentationCommentXmlNames.ParameterElementName); - - var permutedParamNodes = VerifyAndPermuteParamNodes(paramNodes, declarationSymbol, updatedSignature); - if (permutedParamNodes.IsEmpty) + var identifier = nameAttribute.DescendantNodes(descendIntoTrivia: true).OfType().FirstOrDefault(); + if (identifier == null || identifier.ToString() != declaredParameters.ElementAt(i).Name) { return []; } - var options = await document.GetLineFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - return GetPermutedDocCommentTrivia(node, permutedParamNodes, document.Project.Services, options); + dictionary.Add(originalParameters[i].Name.ToString(), paramNode); + i++; } - private ImmutableArray VerifyAndPermuteParamNodes(IEnumerable paramNodes, ISymbol declarationSymbol, SignatureChange updatedSignature) + // Everything lines up, so permute them. + var permutedParams = ArrayBuilder.GetInstance(); + foreach (var parameter in reorderedParameters) { - // Only reorder if count and order match originally. - var originalParameters = updatedSignature.OriginalConfiguration.ToListOfParameters(); - var reorderedParameters = updatedSignature.UpdatedConfiguration.ToListOfParameters(); - - var declaredParameters = GetParameters(declarationSymbol); - - if (paramNodes.Count() != declaredParameters.Length) + if (dictionary.TryGetValue(parameter.Name, out var permutedParam)) { - return []; + permutedParams.Add(permutedParam); } - - // No parameters originally, so no param nodes to permute. - if (declaredParameters.Length == 0) + else { - return []; + permutedParams.Add(XmlElement( + XmlElementStartTag( + XmlName("param"), + [XmlNameAttribute(parameter.Name)]), + XmlElementEndTag(XmlName("param")))); } + } - var dictionary = new Dictionary(); - var i = 0; - foreach (var paramNode in paramNodes) - { - var nameAttribute = paramNode.StartTag.Attributes.FirstOrDefault(a => a.Name.ToString().Equals("name", StringComparison.OrdinalIgnoreCase)); - if (nameAttribute == null) - { - return []; - } - - var identifier = nameAttribute.DescendantNodes(descendIntoTrivia: true).OfType().FirstOrDefault(); - if (identifier == null || identifier.ToString() != declaredParameters.ElementAt(i).Name) - { - return []; - } - - dictionary.Add(originalParameters[i].Name.ToString(), paramNode); - i++; - } + return permutedParams.ToImmutableAndFree(); + } - // Everything lines up, so permute them. - var permutedParams = ArrayBuilder.GetInstance(); - foreach (var parameter in reorderedParameters) - { - if (dictionary.TryGetValue(parameter.Name, out var permutedParam)) - { - permutedParams.Add(permutedParam); - } - else - { - permutedParams.Add(XmlElement( - XmlElementStartTag( - XmlName("param"), - [XmlNameAttribute(parameter.Name)]), - XmlElementEndTag(XmlName("param")))); - } - } + public override async Task> DetermineCascadedSymbolsFromDelegateInvokeAsync( + IMethodSymbol symbol, + Document document, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - return permutedParams.ToImmutableAndFree(); - } + using var _ = ArrayBuilder.GetInstance(out var convertedMethodNodes); - public override async Task> DetermineCascadedSymbolsFromDelegateInvokeAsync( - IMethodSymbol symbol, - Document document, - CancellationToken cancellationToken) + foreach (var node in root.DescendantNodes()) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (!node.IsKind(SyntaxKind.IdentifierName) || + !semanticModel.GetMemberGroup(node, cancellationToken).Any()) + { + continue; + } - using var _ = ArrayBuilder.GetInstance(out var convertedMethodNodes); + var convertedType = (ISymbol?)semanticModel.GetTypeInfo(node, cancellationToken).ConvertedType; + convertedType = convertedType?.OriginalDefinition; - foreach (var node in root.DescendantNodes()) + if (convertedType != null) { - if (!node.IsKind(SyntaxKind.IdentifierName) || - !semanticModel.GetMemberGroup(node, cancellationToken).Any()) - { - continue; - } - - var convertedType = (ISymbol?)semanticModel.GetTypeInfo(node, cancellationToken).ConvertedType; - convertedType = convertedType?.OriginalDefinition; - - if (convertedType != null) - { - convertedType = await SymbolFinder.FindSourceDefinitionAsync(convertedType, document.Project.Solution, cancellationToken).ConfigureAwait(false) - ?? convertedType; - } - - if (Equals(convertedType, symbol.ContainingType)) - convertedMethodNodes.Add(node); + convertedType = await SymbolFinder.FindSourceDefinitionAsync(convertedType, document.Project.Solution, cancellationToken).ConfigureAwait(false) + ?? convertedType; } - var convertedMethodGroups = convertedMethodNodes - .Select(n => semanticModel.GetSymbolInfo(n, cancellationToken).Symbol) - .WhereNotNull() - .ToImmutableArray(); - - return convertedMethodGroups; + if (Equals(convertedType, symbol.ContainingType)) + convertedMethodNodes.Add(node); } - protected override IEnumerable GetFormattingRules(Document document) - => Formatter.GetDefaultFormattingRules(document).Concat(new ChangeSignatureFormattingRule()); + var convertedMethodGroups = convertedMethodNodes + .Select(n => semanticModel.GetSymbolInfo(n, cancellationToken).Symbol) + .WhereNotNull() + .ToImmutableArray(); - protected override TArgumentSyntax AddNameToArgument(TArgumentSyntax newArgument, string name) - { - return newArgument switch - { - ArgumentSyntax a => (TArgumentSyntax)(SyntaxNode)a.WithNameColon(NameColon(name)), - AttributeArgumentSyntax a => (TArgumentSyntax)(SyntaxNode)a.WithNameColon(NameColon(name)), - _ => throw ExceptionUtilities.UnexpectedValue(newArgument.Kind()) - }; - } + return convertedMethodGroups; + } + + protected override IEnumerable GetFormattingRules(Document document) + => Formatter.GetDefaultFormattingRules(document).Concat(new ChangeSignatureFormattingRule()); - protected override TArgumentSyntax CreateExplicitParamsArrayFromIndividualArguments(SeparatedSyntaxList newArguments, int indexInExistingList, IParameterSymbol parameterSymbol) + protected override TArgumentSyntax AddNameToArgument(TArgumentSyntax newArgument, string name) + { + return newArgument switch { - RoslynDebug.Assert(parameterSymbol.IsParams); + ArgumentSyntax a => (TArgumentSyntax)(SyntaxNode)a.WithNameColon(NameColon(name)), + AttributeArgumentSyntax a => (TArgumentSyntax)(SyntaxNode)a.WithNameColon(NameColon(name)), + _ => throw ExceptionUtilities.UnexpectedValue(newArgument.Kind()) + }; + } - // These arguments are part of a params array, and should not have any modifiers, making it okay to just use their expressions. - var listOfArguments = SeparatedList(newArguments.Skip(indexInExistingList).Select(a => ((ArgumentSyntax)(SyntaxNode)a).Expression), newArguments.GetSeparators().Skip(indexInExistingList)); - var initializerExpression = InitializerExpression(SyntaxKind.ArrayInitializerExpression, listOfArguments); - var objectCreation = ArrayCreationExpression((ArrayTypeSyntax)parameterSymbol.Type.GenerateTypeSyntax(), initializerExpression); - return (TArgumentSyntax)(SyntaxNode)Argument(objectCreation); - } + protected override TArgumentSyntax CreateExplicitParamsArrayFromIndividualArguments(SeparatedSyntaxList newArguments, int indexInExistingList, IParameterSymbol parameterSymbol) + { + RoslynDebug.Assert(parameterSymbol.IsParams); - protected override bool SupportsOptionalAndParamsArrayParametersSimultaneously() - { - return true; - } + // These arguments are part of a params array, and should not have any modifiers, making it okay to just use their expressions. + var listOfArguments = SeparatedList(newArguments.Skip(indexInExistingList).Select(a => ((ArgumentSyntax)(SyntaxNode)a).Expression), newArguments.GetSeparators().Skip(indexInExistingList)); + var initializerExpression = InitializerExpression(SyntaxKind.ArrayInitializerExpression, listOfArguments); + var objectCreation = ArrayCreationExpression((ArrayTypeSyntax)parameterSymbol.Type.GenerateTypeSyntax(), initializerExpression); + return (TArgumentSyntax)(SyntaxNode)Argument(objectCreation); + } - protected override SyntaxToken CommaTokenWithElasticSpace() - => Token(SyntaxKind.CommaToken).WithTrailingTrivia(ElasticSpace); + protected override bool SupportsOptionalAndParamsArrayParametersSimultaneously() + { + return true; + } - protected override bool TryGetRecordPrimaryConstructor(INamedTypeSymbol typeSymbol, [NotNullWhen(true)] out IMethodSymbol? primaryConstructor) - => typeSymbol.TryGetPrimaryConstructor(out primaryConstructor); + protected override SyntaxToken CommaTokenWithElasticSpace() + => Token(SyntaxKind.CommaToken).WithTrailingTrivia(ElasticSpace); - protected override ImmutableArray GetParameters(ISymbol declarationSymbol) - { - var declaredParameters = declarationSymbol.GetParameters(); - if (declarationSymbol is INamedTypeSymbol namedTypeSymbol && - namedTypeSymbol.TryGetPrimaryConstructor(out var primaryConstructor)) - { - declaredParameters = primaryConstructor.Parameters; - } + protected override bool TryGetRecordPrimaryConstructor(INamedTypeSymbol typeSymbol, [NotNullWhen(true)] out IMethodSymbol? primaryConstructor) + => typeSymbol.TryGetPrimaryConstructor(out primaryConstructor); - return declaredParameters; + protected override ImmutableArray GetParameters(ISymbol declarationSymbol) + { + var declaredParameters = declarationSymbol.GetParameters(); + if (declarationSymbol is INamedTypeSymbol namedTypeSymbol && + namedTypeSymbol.TryGetPrimaryConstructor(out var primaryConstructor)) + { + declaredParameters = primaryConstructor.Parameters; } + + return declaredParameters; } } diff --git a/src/Features/CSharp/Portable/ChangeSignature/ChangeSignatureFormattingRule.cs b/src/Features/CSharp/Portable/ChangeSignature/ChangeSignatureFormattingRule.cs index e7580161c5ab5..364c17012ef24 100644 --- a/src/Features/CSharp/Portable/ChangeSignature/ChangeSignatureFormattingRule.cs +++ b/src/Features/CSharp/Portable/ChangeSignature/ChangeSignatureFormattingRule.cs @@ -12,51 +12,50 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.ChangeSignature +namespace Microsoft.CodeAnalysis.CSharp.ChangeSignature; + +internal sealed class ChangeSignatureFormattingRule : BaseFormattingRule { - internal sealed class ChangeSignatureFormattingRule : BaseFormattingRule + private static readonly ImmutableArray s_allowableKinds = + [ + SyntaxKind.ParameterList, + SyntaxKind.ArgumentList, + SyntaxKind.BracketedParameterList, + SyntaxKind.BracketedArgumentList, + SyntaxKind.AttributeArgumentList, + ]; + + public override void AddIndentBlockOperations(List list, SyntaxNode node, in NextIndentBlockOperationAction nextOperation) { - private static readonly ImmutableArray s_allowableKinds = - [ - SyntaxKind.ParameterList, - SyntaxKind.ArgumentList, - SyntaxKind.BracketedParameterList, - SyntaxKind.BracketedArgumentList, - SyntaxKind.AttributeArgumentList, - ]; - - public override void AddIndentBlockOperations(List list, SyntaxNode node, in NextIndentBlockOperationAction nextOperation) - { - nextOperation.Invoke(); + nextOperation.Invoke(); - if (s_allowableKinds.Contains(node.Kind())) - { - AddChangeSignatureIndentOperation(list, node); - } + if (s_allowableKinds.Contains(node.Kind())) + { + AddChangeSignatureIndentOperation(list, node); } + } - private static void AddChangeSignatureIndentOperation(List list, SyntaxNode node) + private static void AddChangeSignatureIndentOperation(List list, SyntaxNode node) + { + if (node.Parent != null) { - if (node.Parent != null) - { - var baseToken = node.Parent.GetFirstToken(); - var startToken = node.GetFirstToken(); - var endToken = node.GetLastToken(); - var span = CommonFormattingHelpers.GetSpanIncludingTrailingAndLeadingTriviaOfAdjacentTokens(startToken, endToken); - span = TextSpan.FromBounds(Math.Max(baseToken.Span.End, span.Start), span.End); - - list.Add(FormattingOperations.CreateRelativeIndentBlockOperation(baseToken, startToken, endToken, span, indentationDelta: 1, option: IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine)); - } + var baseToken = node.Parent.GetFirstToken(); + var startToken = node.GetFirstToken(); + var endToken = node.GetLastToken(); + var span = CommonFormattingHelpers.GetSpanIncludingTrailingAndLeadingTriviaOfAdjacentTokens(startToken, endToken); + span = TextSpan.FromBounds(Math.Max(baseToken.Span.End, span.Start), span.End); + + list.Add(FormattingOperations.CreateRelativeIndentBlockOperation(baseToken, startToken, endToken, span, indentationDelta: 1, option: IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine)); } + } - public override AdjustNewLinesOperation GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + public override AdjustNewLinesOperation GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + { + if (previousToken.Kind() == SyntaxKind.CommaToken && s_allowableKinds.Contains(previousToken.Parent.Kind())) { - if (previousToken.Kind() == SyntaxKind.CommaToken && s_allowableKinds.Contains(previousToken.Parent.Kind())) - { - return FormattingOperations.CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines); - } - - return base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation); + return FormattingOperations.CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines); } + + return base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation); } } diff --git a/src/Features/CSharp/Portable/ChangeSignature/UnifiedArgumentSyntax.cs b/src/Features/CSharp/Portable/ChangeSignature/UnifiedArgumentSyntax.cs index 5fbe549a9293c..816284f803b5f 100644 --- a/src/Features/CSharp/Portable/ChangeSignature/UnifiedArgumentSyntax.cs +++ b/src/Features/CSharp/Portable/ChangeSignature/UnifiedArgumentSyntax.cs @@ -9,83 +9,82 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.ChangeSignature +namespace Microsoft.CodeAnalysis.CSharp.ChangeSignature; + +internal readonly struct UnifiedArgumentSyntax : IUnifiedArgumentSyntax { - internal readonly struct UnifiedArgumentSyntax : IUnifiedArgumentSyntax - { - private readonly SyntaxNode _argument; + private readonly SyntaxNode _argument; - private UnifiedArgumentSyntax(SyntaxNode argument) - { - Debug.Assert(argument.Kind() is SyntaxKind.Argument or SyntaxKind.AttributeArgument); - _argument = argument; - } + private UnifiedArgumentSyntax(SyntaxNode argument) + { + Debug.Assert(argument.Kind() is SyntaxKind.Argument or SyntaxKind.AttributeArgument); + _argument = argument; + } - public static IUnifiedArgumentSyntax Create(ArgumentSyntax argument) - => new UnifiedArgumentSyntax(argument); + public static IUnifiedArgumentSyntax Create(ArgumentSyntax argument) + => new UnifiedArgumentSyntax(argument); - public static IUnifiedArgumentSyntax Create(AttributeArgumentSyntax argument) - => new UnifiedArgumentSyntax(argument); + public static IUnifiedArgumentSyntax Create(AttributeArgumentSyntax argument) + => new UnifiedArgumentSyntax(argument); - public SyntaxNode NameColon + public SyntaxNode NameColon + { + get { - get - { - return _argument is ArgumentSyntax argument - ? argument.NameColon - : ((AttributeArgumentSyntax)_argument).NameColon; - } + return _argument is ArgumentSyntax argument + ? argument.NameColon + : ((AttributeArgumentSyntax)_argument).NameColon; } + } - public IUnifiedArgumentSyntax WithNameColon(SyntaxNode nameColonSyntax) - { - Debug.Assert(nameColonSyntax is NameColonSyntax); + public IUnifiedArgumentSyntax WithNameColon(SyntaxNode nameColonSyntax) + { + Debug.Assert(nameColonSyntax is NameColonSyntax); - return _argument is ArgumentSyntax argument - ? Create(argument.WithNameColon((NameColonSyntax)nameColonSyntax)) - : Create(((AttributeArgumentSyntax)_argument).WithNameColon((NameColonSyntax)nameColonSyntax)); - } + return _argument is ArgumentSyntax argument + ? Create(argument.WithNameColon((NameColonSyntax)nameColonSyntax)) + : Create(((AttributeArgumentSyntax)_argument).WithNameColon((NameColonSyntax)nameColonSyntax)); + } - public string GetName() - => NameColon == null ? string.Empty : ((NameColonSyntax)NameColon).Name.Identifier.ValueText; + public string GetName() + => NameColon == null ? string.Empty : ((NameColonSyntax)NameColon).Name.Identifier.ValueText; - public IUnifiedArgumentSyntax WithName(string name) - { - return _argument is ArgumentSyntax argument - ? Create(argument.WithNameColon(SyntaxFactory.NameColon(SyntaxFactory.IdentifierName(name)))) - : Create(((AttributeArgumentSyntax)_argument).WithNameColon(SyntaxFactory.NameColon(SyntaxFactory.IdentifierName(name)))); - } + public IUnifiedArgumentSyntax WithName(string name) + { + return _argument is ArgumentSyntax argument + ? Create(argument.WithNameColon(SyntaxFactory.NameColon(SyntaxFactory.IdentifierName(name)))) + : Create(((AttributeArgumentSyntax)_argument).WithNameColon(SyntaxFactory.NameColon(SyntaxFactory.IdentifierName(name)))); + } - public IUnifiedArgumentSyntax WithAdditionalAnnotations(SyntaxAnnotation annotation) - => new UnifiedArgumentSyntax(_argument.WithAdditionalAnnotations(annotation)); + public IUnifiedArgumentSyntax WithAdditionalAnnotations(SyntaxAnnotation annotation) + => new UnifiedArgumentSyntax(_argument.WithAdditionalAnnotations(annotation)); - public SyntaxNode Expression + public SyntaxNode Expression + { + get { - get - { - return _argument is ArgumentSyntax argument - ? argument.Expression - : ((AttributeArgumentSyntax)_argument).Expression; - } + return _argument is ArgumentSyntax argument + ? argument.Expression + : ((AttributeArgumentSyntax)_argument).Expression; } + } - public bool IsDefault + public bool IsDefault + { + get { - get - { - return _argument == null; - } + return _argument == null; } + } - public bool IsNamed + public bool IsNamed + { + get { - get - { - return NameColon != null; - } + return NameColon != null; } - - public static explicit operator SyntaxNode(UnifiedArgumentSyntax unified) - => unified._argument; } + + public static explicit operator SyntaxNode(UnifiedArgumentSyntax unified) + => unified._argument; } diff --git a/src/Features/CSharp/Portable/CodeFixes/GenerateEnumMember/GenerateEnumMemberCodeFixProvider.cs b/src/Features/CSharp/Portable/CodeFixes/GenerateEnumMember/GenerateEnumMemberCodeFixProvider.cs index 9fe6b5fbb1255..00ede73eb5763 100644 --- a/src/Features/CSharp/Portable/CodeFixes/GenerateEnumMember/GenerateEnumMemberCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/CodeFixes/GenerateEnumMember/GenerateEnumMemberCodeFixProvider.cs @@ -15,31 +15,30 @@ using Microsoft.CodeAnalysis.GenerateMember.GenerateEnumMember; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.GenerateEnumMember -{ - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateEnumMember), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateConstructor)] - internal class GenerateEnumMemberCodeFixProvider : AbstractGenerateMemberCodeFixProvider - { - private const string CS0117 = nameof(CS0117); // error CS0117: 'Color' does not contain a definition for 'Red' - private const string CS1061 = nameof(CS1061); // error CS1061: 'Color' does not contain a definition for 'Red' and no accessible extension method 'Red' accepting a first argument of type 'Color' could be found +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.GenerateEnumMember; - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public GenerateEnumMemberCodeFixProvider() - { - } +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateEnumMember), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateConstructor)] +internal class GenerateEnumMemberCodeFixProvider : AbstractGenerateMemberCodeFixProvider +{ + private const string CS0117 = nameof(CS0117); // error CS0117: 'Color' does not contain a definition for 'Red' + private const string CS1061 = nameof(CS1061); // error CS1061: 'Color' does not contain a definition for 'Red' and no accessible extension method 'Red' accepting a first argument of type 'Color' could be found - public override ImmutableArray FixableDiagnosticIds { get; } = - [CS0117, CS1061]; + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public GenerateEnumMemberCodeFixProvider() + { + } - protected override Task> GetCodeActionsAsync(Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var service = document.GetRequiredLanguageService(); - return service.GenerateEnumMemberAsync(document, node, fallbackOptions, cancellationToken); - } + public override ImmutableArray FixableDiagnosticIds { get; } = + [CS0117, CS1061]; - protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) - => node is IdentifierNameSyntax; + protected override Task> GetCodeActionsAsync(Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var service = document.GetRequiredLanguageService(); + return service.GenerateEnumMemberAsync(document, node, fallbackOptions, cancellationToken); } + + protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) + => node is IdentifierNameSyntax; } diff --git a/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateConversionCodeFixProvider.cs b/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateConversionCodeFixProvider.cs index 4339685456339..fe99b42575431 100644 --- a/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateConversionCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateConversionCodeFixProvider.cs @@ -16,54 +16,53 @@ using Microsoft.CodeAnalysis.GenerateMember.GenerateParameterizedMember; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.GenerateMethod +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.GenerateMethod; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateConversion), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateEnumMember)] +internal class GenerateConversionCodeFixProvider : AbstractGenerateMemberCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateConversion), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateEnumMember)] - internal class GenerateConversionCodeFixProvider : AbstractGenerateMemberCodeFixProvider - { - private const string CS0029 = nameof(CS0029); // error CS0029: Cannot implicitly convert type 'type' to 'type' - private const string CS0030 = nameof(CS0030); // error CS0030: Cannot convert type 'type' to 'type' + private const string CS0029 = nameof(CS0029); // error CS0029: Cannot implicitly convert type 'type' to 'type' + private const string CS0030 = nameof(CS0030); // error CS0030: Cannot convert type 'type' to 'type' - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public GenerateConversionCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public GenerateConversionCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds - { - get { return [CS0029, CS0030]; } - } + public override ImmutableArray FixableDiagnosticIds + { + get { return [CS0029, CS0030]; } + } - protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) - { - return node.Kind() - is SyntaxKind.IdentifierName - or SyntaxKind.MethodDeclaration - or SyntaxKind.InvocationExpression - or SyntaxKind.CastExpression || - node is LiteralExpressionSyntax || - node is SimpleNameSyntax || - node is ExpressionSyntax; - } + protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) + { + return node.Kind() + is SyntaxKind.IdentifierName + or SyntaxKind.MethodDeclaration + or SyntaxKind.InvocationExpression + or SyntaxKind.CastExpression || + node is LiteralExpressionSyntax || + node is SimpleNameSyntax || + node is ExpressionSyntax; + } - protected override SyntaxNode? GetTargetNode(SyntaxNode node) - { - if (node is InvocationExpressionSyntax invocation) - return invocation.Expression.GetRightmostName(); + protected override SyntaxNode? GetTargetNode(SyntaxNode node) + { + if (node is InvocationExpressionSyntax invocation) + return invocation.Expression.GetRightmostName(); - if (node is MemberBindingExpressionSyntax memberBindingExpression) - return memberBindingExpression.Name; + if (node is MemberBindingExpressionSyntax memberBindingExpression) + return memberBindingExpression.Name; - return node; - } + return node; + } - protected override Task> GetCodeActionsAsync( - Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var service = document.GetRequiredLanguageService(); - return service.GenerateConversionAsync(document, node, fallbackOptions, cancellationToken); - } + protected override Task> GetCodeActionsAsync( + Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var service = document.GetRequiredLanguageService(); + return service.GenerateConversionAsync(document, node, fallbackOptions, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateDeconstructMethodCodeFixProvider.cs b/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateDeconstructMethodCodeFixProvider.cs index 1116c98fe80fa..9cd82d9dd31cf 100644 --- a/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateDeconstructMethodCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateDeconstructMethodCodeFixProvider.cs @@ -16,100 +16,99 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.GenerateDeconstructMethod +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.GenerateDeconstructMethod; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateDeconstructMethod), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateEnumMember)] +internal class GenerateDeconstructMethodCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateDeconstructMethod), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateEnumMember)] - internal class GenerateDeconstructMethodCodeFixProvider : CodeFixProvider + private const string CS8129 = nameof(CS8129); // No suitable Deconstruct instance or extension method was found... + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public GenerateDeconstructMethodCodeFixProvider() { - private const string CS8129 = nameof(CS8129); // No suitable Deconstruct instance or extension method was found... + } - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public GenerateDeconstructMethodCodeFixProvider() + public sealed override ImmutableArray FixableDiagnosticIds => [CS8129]; + + public override FixAllProvider GetFixAllProvider() + { + // Fix All is not supported by this code fix + return null; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + // Not supported in REPL + if (context.Document.Project.IsSubmission) { + return; } - public sealed override ImmutableArray FixableDiagnosticIds => [CS8129]; + var document = context.Document; + var cancellationToken = context.CancellationToken; + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var span = context.Span; + var token = root.FindToken(span.Start); + + var deconstruction = token.GetAncestors() + .FirstOrDefault(n => n.Kind() is SyntaxKind.SimpleAssignmentExpression or SyntaxKind.ForEachVariableStatement or SyntaxKind.PositionalPatternClause); - public override FixAllProvider GetFixAllProvider() + if (deconstruction is null) { - // Fix All is not supported by this code fix - return null; + Debug.Fail("The diagnostic can only be produced in context of a deconstruction-assignment, deconstruction-foreach or deconstruction-positionalpattern"); + return; } - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + DeconstructionInfo info; + ITypeSymbol type; + SyntaxNode target; + switch (deconstruction) { - // Not supported in REPL - if (context.Document.Project.IsSubmission) - { - return; - } - - var document = context.Document; - var cancellationToken = context.CancellationToken; - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var span = context.Span; - var token = root.FindToken(span.Start); - - var deconstruction = token.GetAncestors() - .FirstOrDefault(n => n.Kind() is SyntaxKind.SimpleAssignmentExpression or SyntaxKind.ForEachVariableStatement or SyntaxKind.PositionalPatternClause); - - if (deconstruction is null) - { - Debug.Fail("The diagnostic can only be produced in context of a deconstruction-assignment, deconstruction-foreach or deconstruction-positionalpattern"); - return; - } - - var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - DeconstructionInfo info; - ITypeSymbol type; - SyntaxNode target; - switch (deconstruction) - { - case ForEachVariableStatementSyntax @foreach: - info = model.GetDeconstructionInfo(@foreach); - type = model.GetForEachStatementInfo(@foreach).ElementType; - target = @foreach.Variable; - break; - case AssignmentExpressionSyntax assignment: - info = model.GetDeconstructionInfo(assignment); - type = model.GetTypeInfo(assignment.Right).Type; - target = assignment.Left; - break; - case PositionalPatternClauseSyntax positionalPattern: - info = default; - type = model.GetTypeInfo(deconstruction.Parent).Type; - target = deconstruction; - break; - default: - throw ExceptionUtilities.Unreachable(); - } - - if (type?.Kind != SymbolKind.NamedType) - { - return; - } - - if (info.Method != null || !info.Nested.IsEmpty) - { - // There is already a Deconstruct method, or we have a nesting situation - return; - } - - // Checking that Subpatterns of deconstruction are ConstantPatternSyntax because for override of TryMakeParameters in CSharpGenerateDeconstructMethodService - // Subpatterns are cast to ConstantPatternSyntax for use of GenerateNameForExpression and GetTypeInfo - if (deconstruction is PositionalPatternClauseSyntax positionalPatternClause && positionalPatternClause.Subpatterns.Any(p => p.Pattern is not ConstantPatternSyntax)) - { - return; - } - - var service = document.GetLanguageService(); - var codeActions = await service.GenerateDeconstructMethodAsync(document, target, (INamedTypeSymbol)type, context.Options, cancellationToken).ConfigureAwait(false); - - Debug.Assert(!codeActions.IsDefault); - context.RegisterFixes(codeActions, context.Diagnostics); + case ForEachVariableStatementSyntax @foreach: + info = model.GetDeconstructionInfo(@foreach); + type = model.GetForEachStatementInfo(@foreach).ElementType; + target = @foreach.Variable; + break; + case AssignmentExpressionSyntax assignment: + info = model.GetDeconstructionInfo(assignment); + type = model.GetTypeInfo(assignment.Right).Type; + target = assignment.Left; + break; + case PositionalPatternClauseSyntax positionalPattern: + info = default; + type = model.GetTypeInfo(deconstruction.Parent).Type; + target = deconstruction; + break; + default: + throw ExceptionUtilities.Unreachable(); } + + if (type?.Kind != SymbolKind.NamedType) + { + return; + } + + if (info.Method != null || !info.Nested.IsEmpty) + { + // There is already a Deconstruct method, or we have a nesting situation + return; + } + + // Checking that Subpatterns of deconstruction are ConstantPatternSyntax because for override of TryMakeParameters in CSharpGenerateDeconstructMethodService + // Subpatterns are cast to ConstantPatternSyntax for use of GenerateNameForExpression and GetTypeInfo + if (deconstruction is PositionalPatternClauseSyntax positionalPatternClause && positionalPatternClause.Subpatterns.Any(p => p.Pattern is not ConstantPatternSyntax)) + { + return; + } + + var service = document.GetLanguageService(); + var codeActions = await service.GenerateDeconstructMethodAsync(document, target, (INamedTypeSymbol)type, context.Options, cancellationToken).ConfigureAwait(false); + + Debug.Assert(!codeActions.IsDefault); + context.RegisterFixes(codeActions, context.Diagnostics); } } diff --git a/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateMethodCodeFixProvider.cs b/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateMethodCodeFixProvider.cs index badc414fe67fd..7b01c1784564c 100644 --- a/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateMethodCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/CodeFixes/GenerateMethod/GenerateMethodCodeFixProvider.cs @@ -16,77 +16,76 @@ using Microsoft.CodeAnalysis.GenerateMember.GenerateParameterizedMember; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.GenerateMethod +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.GenerateMethod; + +internal static class GenerateMethodDiagnosticIds { - internal static class GenerateMethodDiagnosticIds - { - private const string CS0103 = nameof(CS0103); // error CS0103: Error The name 'Goo' does not exist in the current context - private const string CS0117 = nameof(CS0117); // error CS0117: 'Class' does not contain a definition for 'Goo' - private const string CS0118 = nameof(CS0118); // error CS0118: 'X' is a namespace but is used like a variable - private const string CS0122 = nameof(CS0122); // error CS0122: 'Class' is inaccessible due to its protection level. - private const string CS0305 = nameof(CS0305); // error CS0305: Using the generic method 'CA.M()' requires 1 type arguments - private const string CS0308 = nameof(CS0308); // error CS0308: The non-generic method 'Program.Goo()' cannot be used with type arguments - private const string CS0539 = nameof(CS0539); // error CS0539: 'A.Goo()' in explicit interface declaration is not a member of interface - private const string CS1061 = nameof(CS1061); // error CS1061: Error 'Class' does not contain a definition for 'Goo' and no extension method 'Goo' - private const string CS1501 = nameof(CS1501); // error CS1501: No overload for method 'M' takes 1 arguments - private const string CS1503 = nameof(CS1503); // error CS1503: Argument 1: cannot convert from 'double' to 'int' - private const string CS1660 = nameof(CS1660); // error CS1660: Cannot convert lambda expression to type 'string[]' because it is not a delegate type - private const string CS1739 = nameof(CS1739); // error CS1739: The best overload for 'M' does not have a parameter named 'x' - private const string CS7036 = nameof(CS7036); // error CS7036: There is no argument given that corresponds to the required parameter 'x' of 'C.M(int)' - private const string CS1955 = nameof(CS1955); // error CS1955: Non-invocable member 'Goo' cannot be used like a method. - private const string CS0123 = nameof(CS0123); // error CS0123: No overload for 'OnChanged' matches delegate 'NotifyCollectionChangedEventHandler' + private const string CS0103 = nameof(CS0103); // error CS0103: Error The name 'Goo' does not exist in the current context + private const string CS0117 = nameof(CS0117); // error CS0117: 'Class' does not contain a definition for 'Goo' + private const string CS0118 = nameof(CS0118); // error CS0118: 'X' is a namespace but is used like a variable + private const string CS0122 = nameof(CS0122); // error CS0122: 'Class' is inaccessible due to its protection level. + private const string CS0305 = nameof(CS0305); // error CS0305: Using the generic method 'CA.M()' requires 1 type arguments + private const string CS0308 = nameof(CS0308); // error CS0308: The non-generic method 'Program.Goo()' cannot be used with type arguments + private const string CS0539 = nameof(CS0539); // error CS0539: 'A.Goo()' in explicit interface declaration is not a member of interface + private const string CS1061 = nameof(CS1061); // error CS1061: Error 'Class' does not contain a definition for 'Goo' and no extension method 'Goo' + private const string CS1501 = nameof(CS1501); // error CS1501: No overload for method 'M' takes 1 arguments + private const string CS1503 = nameof(CS1503); // error CS1503: Argument 1: cannot convert from 'double' to 'int' + private const string CS1660 = nameof(CS1660); // error CS1660: Cannot convert lambda expression to type 'string[]' because it is not a delegate type + private const string CS1739 = nameof(CS1739); // error CS1739: The best overload for 'M' does not have a parameter named 'x' + private const string CS7036 = nameof(CS7036); // error CS7036: There is no argument given that corresponds to the required parameter 'x' of 'C.M(int)' + private const string CS1955 = nameof(CS1955); // error CS1955: Non-invocable member 'Goo' cannot be used like a method. + private const string CS0123 = nameof(CS0123); // error CS0123: No overload for 'OnChanged' matches delegate 'NotifyCollectionChangedEventHandler' - public static readonly ImmutableArray FixableDiagnosticIds = - [CS0103, CS0117, CS0118, CS0122, CS0305, CS0308, CS0539, CS1061, CS1501, CS1503, CS1660, CS1739, CS7036, CS1955, CS0123]; - } + public static readonly ImmutableArray FixableDiagnosticIds = + [CS0103, CS0117, CS0118, CS0122, CS0305, CS0308, CS0539, CS1061, CS1501, CS1503, CS1660, CS1739, CS7036, CS1955, CS0123]; +} - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateMethod), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateEnumMember)] - [ExtensionOrder(Before = PredefinedCodeFixProviderNames.PopulateSwitch)] - [ExtensionOrder(Before = PredefinedCodeFixProviderNames.GenerateVariable)] - internal sealed class GenerateMethodCodeFixProvider : AbstractGenerateMemberCodeFixProvider +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateMethod), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateEnumMember)] +[ExtensionOrder(Before = PredefinedCodeFixProviderNames.PopulateSwitch)] +[ExtensionOrder(Before = PredefinedCodeFixProviderNames.GenerateVariable)] +internal sealed class GenerateMethodCodeFixProvider : AbstractGenerateMemberCodeFixProvider +{ + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public GenerateMethodCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public GenerateMethodCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds { get; } = - GenerateMethodDiagnosticIds.FixableDiagnosticIds; + public override ImmutableArray FixableDiagnosticIds { get; } = + GenerateMethodDiagnosticIds.FixableDiagnosticIds; - protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) - { - return node.Kind() - is SyntaxKind.IdentifierName - or SyntaxKind.MethodDeclaration - or SyntaxKind.InvocationExpression - or SyntaxKind.CastExpression || - node is LiteralExpressionSyntax || - node is SimpleNameSyntax || - node is ExpressionSyntax; - } + protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) + { + return node.Kind() + is SyntaxKind.IdentifierName + or SyntaxKind.MethodDeclaration + or SyntaxKind.InvocationExpression + or SyntaxKind.CastExpression || + node is LiteralExpressionSyntax || + node is SimpleNameSyntax || + node is ExpressionSyntax; + } - protected override SyntaxNode? GetTargetNode(SyntaxNode node) + protected override SyntaxNode? GetTargetNode(SyntaxNode node) + { + switch (node) { - switch (node) - { - case InvocationExpressionSyntax invocation: - return invocation.Expression.GetRightmostName(); - case MemberBindingExpressionSyntax memberBindingExpression: - return memberBindingExpression.Name; - case AssignmentExpressionSyntax assignment: - return assignment.Right; - } - - return node; + case InvocationExpressionSyntax invocation: + return invocation.Expression.GetRightmostName(); + case MemberBindingExpressionSyntax memberBindingExpression: + return memberBindingExpression.Name; + case AssignmentExpressionSyntax assignment: + return assignment.Right; } - protected override Task> GetCodeActionsAsync( - Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var service = document.GetRequiredLanguageService(); - return service.GenerateMethodAsync(document, node, fallbackOptions, cancellationToken); - } + return node; + } + + protected override Task> GetCodeActionsAsync( + Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var service = document.GetRequiredLanguageService(); + return service.GenerateMethodAsync(document, node, fallbackOptions, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/CodeFixes/GenerateType/GenerateTypeCodeFixProvider.cs b/src/Features/CSharp/Portable/CodeFixes/GenerateType/GenerateTypeCodeFixProvider.cs index fae639b90f8cc..b7fb93904a8dc 100644 --- a/src/Features/CSharp/Portable/CodeFixes/GenerateType/GenerateTypeCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/CodeFixes/GenerateType/GenerateTypeCodeFixProvider.cs @@ -17,55 +17,54 @@ using Microsoft.CodeAnalysis.GenerateType; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.GenerateType +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.GenerateType; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateType), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateVariable)] +internal class GenerateTypeCodeFixProvider : AbstractGenerateMemberCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateType), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateVariable)] - internal class GenerateTypeCodeFixProvider : AbstractGenerateMemberCodeFixProvider + private const string CS0103 = nameof(CS0103); // error CS0103: The name 'Goo' does not exist in the current context + private const string CS0117 = nameof(CS0117); // error CS0117: 'x' does not contain a definition for 'y' + private const string CS0234 = nameof(CS0234); // error CS0234: The type or namespace name 'C' does not exist in the namespace 'N' (are you missing an assembly reference?) + private const string CS0246 = nameof(CS0246); // error CS0246: The type or namespace name 'T' could not be found (are you missing a using directive or an assembly reference?) + private const string CS0305 = nameof(CS0305); // error CS0305: Using the generic type 'C' requires 1 type arguments + private const string CS0308 = nameof(CS0308); // error CS0308: The non-generic type 'A' cannot be used with type arguments + private const string CS0426 = nameof(CS0426); // error CS0426: The type name 'S' does not exist in the type 'Program' + private const string CS0616 = nameof(CS0616); // error CS0616: 'x' is not an attribute class + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public GenerateTypeCodeFixProvider() { - private const string CS0103 = nameof(CS0103); // error CS0103: The name 'Goo' does not exist in the current context - private const string CS0117 = nameof(CS0117); // error CS0117: 'x' does not contain a definition for 'y' - private const string CS0234 = nameof(CS0234); // error CS0234: The type or namespace name 'C' does not exist in the namespace 'N' (are you missing an assembly reference?) - private const string CS0246 = nameof(CS0246); // error CS0246: The type or namespace name 'T' could not be found (are you missing a using directive or an assembly reference?) - private const string CS0305 = nameof(CS0305); // error CS0305: Using the generic type 'C' requires 1 type arguments - private const string CS0308 = nameof(CS0308); // error CS0308: The non-generic type 'A' cannot be used with type arguments - private const string CS0426 = nameof(CS0426); // error CS0426: The type name 'S' does not exist in the type 'Program' - private const string CS0616 = nameof(CS0616); // error CS0616: 'x' is not an attribute class + } - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public GenerateTypeCodeFixProvider() - { - } + public override ImmutableArray FixableDiagnosticIds + { + get { return [CS0103, CS0117, CS0234, CS0246, CS0305, CS0308, CS0426, CS0616, IDEDiagnosticIds.UnboundIdentifierId]; } + } - public override ImmutableArray FixableDiagnosticIds + protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) + { + switch (node) { - get { return [CS0103, CS0117, CS0234, CS0246, CS0305, CS0308, CS0426, CS0616, IDEDiagnosticIds.UnboundIdentifierId]; } + case QualifiedNameSyntax _: + return true; + case SimpleNameSyntax simple: + return !simple.IsParentKind(SyntaxKind.QualifiedName); + case MemberAccessExpressionSyntax _: + return true; } - protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) - { - switch (node) - { - case QualifiedNameSyntax _: - return true; - case SimpleNameSyntax simple: - return !simple.IsParentKind(SyntaxKind.QualifiedName); - case MemberAccessExpressionSyntax _: - return true; - } - - return false; - } + return false; + } - protected override SyntaxNode? GetTargetNode(SyntaxNode node) - => ((ExpressionSyntax)node).GetRightmostName(); + protected override SyntaxNode? GetTargetNode(SyntaxNode node) + => ((ExpressionSyntax)node).GetRightmostName(); - protected override Task> GetCodeActionsAsync( - Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var service = document.GetRequiredLanguageService(); - return service.GenerateTypeAsync(document, node, fallbackOptions, cancellationToken); - } + protected override Task> GetCodeActionsAsync( + Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var service = document.GetRequiredLanguageService(); + return service.GenerateTypeAsync(document, node, fallbackOptions, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/CodeFixes/Suppression/CSharpSuppressionCodeFixProvider.cs b/src/Features/CSharp/Portable/CodeFixes/Suppression/CSharpSuppressionCodeFixProvider.cs index d84a6f75db692..e1d5eb92ca431 100644 --- a/src/Features/CSharp/Portable/CodeFixes/Suppression/CSharpSuppressionCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/CodeFixes/Suppression/CSharpSuppressionCodeFixProvider.cs @@ -21,233 +21,232 @@ using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.Suppression; + +[ExportConfigurationFixProvider(PredefinedConfigurationFixProviderNames.Suppression, LanguageNames.CSharp), Shared] +internal class CSharpSuppressionCodeFixProvider : AbstractSuppressionCodeFixProvider { - [ExportConfigurationFixProvider(PredefinedConfigurationFixProviderNames.Suppression, LanguageNames.CSharp), Shared] - internal class CSharpSuppressionCodeFixProvider : AbstractSuppressionCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpSuppressionCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpSuppressionCodeFixProvider() - { - } + } - protected override SyntaxTriviaList CreatePragmaRestoreDirectiveTrivia(Diagnostic diagnostic, Func formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken) + protected override SyntaxTriviaList CreatePragmaRestoreDirectiveTrivia(Diagnostic diagnostic, Func formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken) + { + var restoreKeyword = SyntaxFactory.Token(SyntaxKind.RestoreKeyword); + return CreatePragmaDirectiveTrivia(restoreKeyword, diagnostic, formatNode, needsLeadingEndOfLine, needsTrailingEndOfLine, cancellationToken); + } + + protected override SyntaxTriviaList CreatePragmaDisableDirectiveTrivia( + Diagnostic diagnostic, Func formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken) + { + var disableKeyword = SyntaxFactory.Token(SyntaxKind.DisableKeyword); + return CreatePragmaDirectiveTrivia(disableKeyword, diagnostic, formatNode, needsLeadingEndOfLine, needsTrailingEndOfLine, cancellationToken); + } + + private static SyntaxTriviaList CreatePragmaDirectiveTrivia( + SyntaxToken disableOrRestoreKeyword, Diagnostic diagnostic, Func formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken) + { + var diagnosticId = GetOrMapDiagnosticId(diagnostic, out var includeTitle); + var id = SyntaxFactory.IdentifierName(diagnosticId); + var ids = new SeparatedSyntaxList().Add(id); + var pragmaDirective = SyntaxFactory.PragmaWarningDirectiveTrivia(disableOrRestoreKeyword, ids, true); + pragmaDirective = (PragmaWarningDirectiveTriviaSyntax)formatNode(pragmaDirective, cancellationToken); + var pragmaDirectiveTrivia = SyntaxFactory.Trivia(pragmaDirective); + var endOfLineTrivia = SyntaxFactory.CarriageReturnLineFeed; + var triviaList = SyntaxFactory.TriviaList(pragmaDirectiveTrivia); + + var title = includeTitle ? diagnostic.Descriptor.Title.ToString(CultureInfo.CurrentUICulture) : null; + if (!string.IsNullOrWhiteSpace(title)) { - var restoreKeyword = SyntaxFactory.Token(SyntaxKind.RestoreKeyword); - return CreatePragmaDirectiveTrivia(restoreKeyword, diagnostic, formatNode, needsLeadingEndOfLine, needsTrailingEndOfLine, cancellationToken); + var titleComment = SyntaxFactory.Comment(string.Format(" // {0}", title)).WithAdditionalAnnotations(Formatter.Annotation); + triviaList = triviaList.Add(titleComment); } - protected override SyntaxTriviaList CreatePragmaDisableDirectiveTrivia( - Diagnostic diagnostic, Func formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken) + if (needsLeadingEndOfLine) { - var disableKeyword = SyntaxFactory.Token(SyntaxKind.DisableKeyword); - return CreatePragmaDirectiveTrivia(disableKeyword, diagnostic, formatNode, needsLeadingEndOfLine, needsTrailingEndOfLine, cancellationToken); + triviaList = triviaList.Insert(0, endOfLineTrivia); } - private static SyntaxTriviaList CreatePragmaDirectiveTrivia( - SyntaxToken disableOrRestoreKeyword, Diagnostic diagnostic, Func formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken) + if (needsTrailingEndOfLine) { - var diagnosticId = GetOrMapDiagnosticId(diagnostic, out var includeTitle); - var id = SyntaxFactory.IdentifierName(diagnosticId); - var ids = new SeparatedSyntaxList().Add(id); - var pragmaDirective = SyntaxFactory.PragmaWarningDirectiveTrivia(disableOrRestoreKeyword, ids, true); - pragmaDirective = (PragmaWarningDirectiveTriviaSyntax)formatNode(pragmaDirective, cancellationToken); - var pragmaDirectiveTrivia = SyntaxFactory.Trivia(pragmaDirective); - var endOfLineTrivia = SyntaxFactory.CarriageReturnLineFeed; - var triviaList = SyntaxFactory.TriviaList(pragmaDirectiveTrivia); - - var title = includeTitle ? diagnostic.Descriptor.Title.ToString(CultureInfo.CurrentUICulture) : null; - if (!string.IsNullOrWhiteSpace(title)) - { - var titleComment = SyntaxFactory.Comment(string.Format(" // {0}", title)).WithAdditionalAnnotations(Formatter.Annotation); - triviaList = triviaList.Add(titleComment); - } + triviaList = triviaList.Add(endOfLineTrivia); + } - if (needsLeadingEndOfLine) - { - triviaList = triviaList.Insert(0, endOfLineTrivia); - } + return triviaList; + } - if (needsTrailingEndOfLine) - { - triviaList = triviaList.Add(endOfLineTrivia); - } + protected override string DefaultFileExtension => ".cs"; - return triviaList; - } + protected override string SingleLineCommentStart => "//"; - protected override string DefaultFileExtension => ".cs"; + protected override bool IsAttributeListWithAssemblyAttributes(SyntaxNode node) + { + return node is AttributeListSyntax attributeList && + attributeList.Target != null && + attributeList.Target.Identifier.Kind() == SyntaxKind.AssemblyKeyword; + } - protected override string SingleLineCommentStart => "//"; + protected override bool IsEndOfLine(SyntaxTrivia trivia) + => trivia.Kind() is SyntaxKind.EndOfLineTrivia or SyntaxKind.SingleLineDocumentationCommentTrivia; + + protected override bool IsEndOfFileToken(SyntaxToken token) + => token.Kind() == SyntaxKind.EndOfFileToken; + + protected override SyntaxNode AddGlobalSuppressMessageAttribute( + SyntaxNode newRoot, + ISymbol targetSymbol, + INamedTypeSymbol suppressMessageAttribute, + Diagnostic diagnostic, + SolutionServices services, + SyntaxFormattingOptions options, + IAddImportsService addImportsService, + CancellationToken cancellationToken) + { + var compilationRoot = (CompilationUnitSyntax)newRoot; + var isFirst = !compilationRoot.AttributeLists.Any(); - protected override bool IsAttributeListWithAssemblyAttributes(SyntaxNode node) - { - return node is AttributeListSyntax attributeList && - attributeList.Target != null && - attributeList.Target.Identifier.Kind() == SyntaxKind.AssemblyKeyword; - } + var attributeName = suppressMessageAttribute.GenerateNameSyntax() + .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); - protected override bool IsEndOfLine(SyntaxTrivia trivia) - => trivia.Kind() is SyntaxKind.EndOfLineTrivia or SyntaxKind.SingleLineDocumentationCommentTrivia; - - protected override bool IsEndOfFileToken(SyntaxToken token) - => token.Kind() == SyntaxKind.EndOfFileToken; - - protected override SyntaxNode AddGlobalSuppressMessageAttribute( - SyntaxNode newRoot, - ISymbol targetSymbol, - INamedTypeSymbol suppressMessageAttribute, - Diagnostic diagnostic, - SolutionServices services, - SyntaxFormattingOptions options, - IAddImportsService addImportsService, - CancellationToken cancellationToken) - { - var compilationRoot = (CompilationUnitSyntax)newRoot; - var isFirst = !compilationRoot.AttributeLists.Any(); + compilationRoot = compilationRoot.AddAttributeLists( + CreateAttributeList( + targetSymbol, + attributeName, + diagnostic, + isAssemblyAttribute: true, + leadingTrivia: default)); - var attributeName = suppressMessageAttribute.GenerateNameSyntax() - .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); + if (isFirst && !newRoot.HasLeadingTrivia) + compilationRoot = compilationRoot.WithLeadingTrivia(SyntaxFactory.Comment(GlobalSuppressionsFileHeaderComment)); - compilationRoot = compilationRoot.AddAttributeLists( - CreateAttributeList( - targetSymbol, - attributeName, - diagnostic, - isAssemblyAttribute: true, - leadingTrivia: default)); + return compilationRoot; + } - if (isFirst && !newRoot.HasLeadingTrivia) - compilationRoot = compilationRoot.WithLeadingTrivia(SyntaxFactory.Comment(GlobalSuppressionsFileHeaderComment)); + protected override SyntaxNode AddLocalSuppressMessageAttribute( + SyntaxNode targetNode, ISymbol targetSymbol, INamedTypeSymbol suppressMessageAttribute, Diagnostic diagnostic) + { + var memberNode = (MemberDeclarationSyntax)targetNode; - return compilationRoot; + SyntaxTriviaList leadingTriviaForAttributeList; + if (!memberNode.GetAttributes().Any()) + { + leadingTriviaForAttributeList = memberNode.GetLeadingTrivia(); + memberNode = memberNode.WithoutLeadingTrivia(); } - - protected override SyntaxNode AddLocalSuppressMessageAttribute( - SyntaxNode targetNode, ISymbol targetSymbol, INamedTypeSymbol suppressMessageAttribute, Diagnostic diagnostic) + else { - var memberNode = (MemberDeclarationSyntax)targetNode; - - SyntaxTriviaList leadingTriviaForAttributeList; - if (!memberNode.GetAttributes().Any()) - { - leadingTriviaForAttributeList = memberNode.GetLeadingTrivia(); - memberNode = memberNode.WithoutLeadingTrivia(); - } - else - { - leadingTriviaForAttributeList = default; - } - - var attributeName = suppressMessageAttribute.GenerateNameSyntax(); - var attributeList = CreateAttributeList( - targetSymbol, attributeName, diagnostic, isAssemblyAttribute: false, leadingTrivia: leadingTriviaForAttributeList); - return memberNode.AddAttributeLists(attributeList); + leadingTriviaForAttributeList = default; } - private static AttributeListSyntax CreateAttributeList( - ISymbol targetSymbol, - NameSyntax attributeName, - Diagnostic diagnostic, - bool isAssemblyAttribute, - SyntaxTriviaList leadingTrivia) - { - var attributeArguments = CreateAttributeArguments(targetSymbol, diagnostic, isAssemblyAttribute); + var attributeName = suppressMessageAttribute.GenerateNameSyntax(); + var attributeList = CreateAttributeList( + targetSymbol, attributeName, diagnostic, isAssemblyAttribute: false, leadingTrivia: leadingTriviaForAttributeList); + return memberNode.AddAttributeLists(attributeList); + } - var attributes = new SeparatedSyntaxList() - .Add(SyntaxFactory.Attribute(attributeName, attributeArguments)); + private static AttributeListSyntax CreateAttributeList( + ISymbol targetSymbol, + NameSyntax attributeName, + Diagnostic diagnostic, + bool isAssemblyAttribute, + SyntaxTriviaList leadingTrivia) + { + var attributeArguments = CreateAttributeArguments(targetSymbol, diagnostic, isAssemblyAttribute); - AttributeListSyntax attributeList; - if (isAssemblyAttribute) - { - var targetSpecifier = SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.AssemblyKeyword)); - attributeList = SyntaxFactory.AttributeList(targetSpecifier, attributes); - } - else - { - attributeList = SyntaxFactory.AttributeList(attributes); - } + var attributes = new SeparatedSyntaxList() + .Add(SyntaxFactory.Attribute(attributeName, attributeArguments)); - return attributeList.WithLeadingTrivia(leadingTrivia); + AttributeListSyntax attributeList; + if (isAssemblyAttribute) + { + var targetSpecifier = SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.AssemblyKeyword)); + attributeList = SyntaxFactory.AttributeList(targetSpecifier, attributes); } - - private static AttributeArgumentListSyntax CreateAttributeArguments(ISymbol targetSymbol, Diagnostic diagnostic, bool isAssemblyAttribute) + else { - // SuppressMessage("Rule Category", "Rule Id", Justification = nameof(Justification), Scope = nameof(Scope), Target = nameof(Target)) - var category = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(diagnostic.Descriptor.Category)); - var categoryArgument = SyntaxFactory.AttributeArgument(category); + attributeList = SyntaxFactory.AttributeList(attributes); + } - var title = diagnostic.Descriptor.Title.ToString(CultureInfo.CurrentUICulture); - var ruleIdText = string.IsNullOrWhiteSpace(title) ? diagnostic.Id : string.Format("{0}:{1}", diagnostic.Id, title); - var ruleId = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(ruleIdText)); - var ruleIdArgument = SyntaxFactory.AttributeArgument(ruleId); + return attributeList.WithLeadingTrivia(leadingTrivia); + } - var justificationExpr = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(FeaturesResources.Pending)); - var justificationArgument = SyntaxFactory.AttributeArgument(SyntaxFactory.NameEquals("Justification"), nameColon: null, expression: justificationExpr); + private static AttributeArgumentListSyntax CreateAttributeArguments(ISymbol targetSymbol, Diagnostic diagnostic, bool isAssemblyAttribute) + { + // SuppressMessage("Rule Category", "Rule Id", Justification = nameof(Justification), Scope = nameof(Scope), Target = nameof(Target)) + var category = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(diagnostic.Descriptor.Category)); + var categoryArgument = SyntaxFactory.AttributeArgument(category); - var attributeArgumentList = SyntaxFactory.AttributeArgumentList().AddArguments(categoryArgument, ruleIdArgument, justificationArgument); + var title = diagnostic.Descriptor.Title.ToString(CultureInfo.CurrentUICulture); + var ruleIdText = string.IsNullOrWhiteSpace(title) ? diagnostic.Id : string.Format("{0}:{1}", diagnostic.Id, title); + var ruleId = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(ruleIdText)); + var ruleIdArgument = SyntaxFactory.AttributeArgument(ruleId); - if (isAssemblyAttribute) - { - var scopeString = GetScopeString(targetSymbol.Kind); - if (scopeString != null) - { - var scopeExpr = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(scopeString)); - var scopeArgument = SyntaxFactory.AttributeArgument(SyntaxFactory.NameEquals("Scope"), nameColon: null, expression: scopeExpr); - - var targetString = GetTargetString(targetSymbol); - var targetExpr = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(targetString)); - var targetArgument = SyntaxFactory.AttributeArgument(SyntaxFactory.NameEquals("Target"), nameColon: null, expression: targetExpr); - - attributeArgumentList = attributeArgumentList.AddArguments(scopeArgument, targetArgument); - } - } + var justificationExpr = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(FeaturesResources.Pending)); + var justificationArgument = SyntaxFactory.AttributeArgument(SyntaxFactory.NameEquals("Justification"), nameColon: null, expression: justificationExpr); - return attributeArgumentList; - } + var attributeArgumentList = SyntaxFactory.AttributeArgumentList().AddArguments(categoryArgument, ruleIdArgument, justificationArgument); - protected override bool IsSingleAttributeInAttributeList(SyntaxNode attribute) + if (isAssemblyAttribute) { - if (attribute is AttributeSyntax attributeSyntax) + var scopeString = GetScopeString(targetSymbol.Kind); + if (scopeString != null) { - return attributeSyntax.Parent is AttributeListSyntax attributeList && attributeList.Attributes.Count == 1; - } + var scopeExpr = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(scopeString)); + var scopeArgument = SyntaxFactory.AttributeArgument(SyntaxFactory.NameEquals("Scope"), nameColon: null, expression: scopeExpr); - return false; - } + var targetString = GetTargetString(targetSymbol); + var targetExpr = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(targetString)); + var targetArgument = SyntaxFactory.AttributeArgument(SyntaxFactory.NameEquals("Target"), nameColon: null, expression: targetExpr); - protected override bool IsAnyPragmaDirectiveForId(SyntaxTrivia trivia, string id, out bool enableDirective, out bool hasMultipleIds) - { - if (trivia.Kind() == SyntaxKind.PragmaWarningDirectiveTrivia) - { - var pragmaWarning = (PragmaWarningDirectiveTriviaSyntax)trivia.GetStructure(); - enableDirective = pragmaWarning.DisableOrRestoreKeyword.Kind() == SyntaxKind.RestoreKeyword; - hasMultipleIds = pragmaWarning.ErrorCodes.Count > 1; - return pragmaWarning.ErrorCodes.Any(n => n.ToString() == id); + attributeArgumentList = attributeArgumentList.AddArguments(scopeArgument, targetArgument); } + } + + return attributeArgumentList; + } - enableDirective = false; - hasMultipleIds = false; - return false; + protected override bool IsSingleAttributeInAttributeList(SyntaxNode attribute) + { + if (attribute is AttributeSyntax attributeSyntax) + { + return attributeSyntax.Parent is AttributeListSyntax attributeList && attributeList.Attributes.Count == 1; } - protected override SyntaxTrivia TogglePragmaDirective(SyntaxTrivia trivia) + return false; + } + + protected override bool IsAnyPragmaDirectiveForId(SyntaxTrivia trivia, string id, out bool enableDirective, out bool hasMultipleIds) + { + if (trivia.Kind() == SyntaxKind.PragmaWarningDirectiveTrivia) { var pragmaWarning = (PragmaWarningDirectiveTriviaSyntax)trivia.GetStructure(); - var currentKeyword = pragmaWarning.DisableOrRestoreKeyword; - var toggledKeywordKind = currentKeyword.Kind() == SyntaxKind.DisableKeyword ? SyntaxKind.RestoreKeyword : SyntaxKind.DisableKeyword; - var toggledToken = SyntaxFactory.Token(currentKeyword.LeadingTrivia, toggledKeywordKind, currentKeyword.TrailingTrivia); - var newPragmaWarning = pragmaWarning.WithDisableOrRestoreKeyword(toggledToken); - return SyntaxFactory.Trivia(newPragmaWarning); + enableDirective = pragmaWarning.DisableOrRestoreKeyword.Kind() == SyntaxKind.RestoreKeyword; + hasMultipleIds = pragmaWarning.ErrorCodes.Count > 1; + return pragmaWarning.ErrorCodes.Any(n => n.ToString() == id); } - protected override SyntaxNode GetContainingStatement(SyntaxToken token) - // If we can't get a containing statement, such as for expression bodied members, then - // return the arrow clause instead - => (SyntaxNode)token.GetAncestor() ?? token.GetAncestor(); + enableDirective = false; + hasMultipleIds = false; + return false; + } - protected override bool TokenHasTrailingLineContinuationChar(SyntaxToken token) - => false; + protected override SyntaxTrivia TogglePragmaDirective(SyntaxTrivia trivia) + { + var pragmaWarning = (PragmaWarningDirectiveTriviaSyntax)trivia.GetStructure(); + var currentKeyword = pragmaWarning.DisableOrRestoreKeyword; + var toggledKeywordKind = currentKeyword.Kind() == SyntaxKind.DisableKeyword ? SyntaxKind.RestoreKeyword : SyntaxKind.DisableKeyword; + var toggledToken = SyntaxFactory.Token(currentKeyword.LeadingTrivia, toggledKeywordKind, currentKeyword.TrailingTrivia); + var newPragmaWarning = pragmaWarning.WithDisableOrRestoreKeyword(toggledToken); + return SyntaxFactory.Trivia(newPragmaWarning); } + + protected override SyntaxNode GetContainingStatement(SyntaxToken token) + // If we can't get a containing statement, such as for expression bodied members, then + // return the arrow clause instead + => (SyntaxNode)token.GetAncestor() ?? token.GetAncestor(); + + protected override bool TokenHasTrailingLineContinuationChar(SyntaxToken token) + => false; } diff --git a/src/Features/CSharp/Portable/CodeLens/CSharpCodeLensDisplayInfoService.cs b/src/Features/CSharp/Portable/CodeLens/CSharpCodeLensDisplayInfoService.cs index 85f4710c38643..b4fd833d963e0 100644 --- a/src/Features/CSharp/Portable/CodeLens/CSharpCodeLensDisplayInfoService.cs +++ b/src/Features/CSharp/Portable/CodeLens/CSharpCodeLensDisplayInfoService.cs @@ -12,187 +12,186 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.CodeLens +namespace Microsoft.CodeAnalysis.CSharp.CodeLens; + +[ExportLanguageService(typeof(ICodeLensDisplayInfoService), LanguageNames.CSharp), Shared] +internal sealed class CSharpCodeLensDisplayInfoService : ICodeLensDisplayInfoService { - [ExportLanguageService(typeof(ICodeLensDisplayInfoService), LanguageNames.CSharp), Shared] - internal sealed class CSharpCodeLensDisplayInfoService : ICodeLensDisplayInfoService - { - private static readonly SymbolDisplayFormat Format = - SymbolDisplayFormat.CSharpErrorMessageFormat.RemoveMemberOptions( - SymbolDisplayMemberOptions.IncludeExplicitInterface); + private static readonly SymbolDisplayFormat Format = + SymbolDisplayFormat.CSharpErrorMessageFormat.RemoveMemberOptions( + SymbolDisplayMemberOptions.IncludeExplicitInterface); - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpCodeLensDisplayInfoService() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpCodeLensDisplayInfoService() + { + } - /// - /// Returns the node that should be displayed - /// - public SyntaxNode GetDisplayNode(SyntaxNode node) + /// + /// Returns the node that should be displayed + /// + public SyntaxNode GetDisplayNode(SyntaxNode node) + { + while (true) { - while (true) + switch (node.Kind()) { - switch (node.Kind()) - { - // LocalDeclarations do not have symbols themselves, you need a variable declarator - case SyntaxKind.LocalDeclarationStatement: - var localDeclarationNode = (LocalDeclarationStatementSyntax)node; - node = localDeclarationNode.Declaration.Variables.FirstOrDefault(); - continue; - - // Field and event declarations do not have symbols themselves, you need a variable declarator - case SyntaxKind.FieldDeclaration: - case SyntaxKind.EventFieldDeclaration: - var fieldNode = (BaseFieldDeclarationSyntax)node; - node = fieldNode.Declaration.Variables.FirstOrDefault(); - continue; - - // Variable is a field without access modifier. Parent is FieldDeclaration - case SyntaxKind.VariableDeclaration: - node = node.Parent; - continue; - - // Built in types - case SyntaxKind.PredefinedType: - node = node.Parent; + // LocalDeclarations do not have symbols themselves, you need a variable declarator + case SyntaxKind.LocalDeclarationStatement: + var localDeclarationNode = (LocalDeclarationStatementSyntax)node; + node = localDeclarationNode.Declaration.Variables.FirstOrDefault(); + continue; + + // Field and event declarations do not have symbols themselves, you need a variable declarator + case SyntaxKind.FieldDeclaration: + case SyntaxKind.EventFieldDeclaration: + var fieldNode = (BaseFieldDeclarationSyntax)node; + node = fieldNode.Declaration.Variables.FirstOrDefault(); + continue; + + // Variable is a field without access modifier. Parent is FieldDeclaration + case SyntaxKind.VariableDeclaration: + node = node.Parent; + continue; + + // Built in types + case SyntaxKind.PredefinedType: + node = node.Parent; + continue; + + case SyntaxKind.MultiLineDocumentationCommentTrivia: + case SyntaxKind.SingleLineDocumentationCommentTrivia: + // For DocumentationCommentTrivia node, node.Parent is null. Obtain parent through ParentTrivia.Token + if (node.IsStructuredTrivia) + { + var structuredTriviaSyntax = (StructuredTriviaSyntax)node; + node = structuredTriviaSyntax.ParentTrivia.Token.Parent; continue; + } - case SyntaxKind.MultiLineDocumentationCommentTrivia: - case SyntaxKind.SingleLineDocumentationCommentTrivia: - // For DocumentationCommentTrivia node, node.Parent is null. Obtain parent through ParentTrivia.Token - if (node.IsStructuredTrivia) - { - var structuredTriviaSyntax = (StructuredTriviaSyntax)node; - node = structuredTriviaSyntax.ParentTrivia.Token.Parent; - continue; - } - - return null; + return null; - default: - return node; - } + default: + return node; } } + } - /// - /// Gets the DisplayName for the given node. - /// - public string GetDisplayName(SemanticModel semanticModel, SyntaxNode node) + /// + /// Gets the DisplayName for the given node. + /// + public string GetDisplayName(SemanticModel semanticModel, SyntaxNode node) + { + if (node == null) { - if (node == null) - { - return FeaturesResources.paren_Unknown_paren; - } + return FeaturesResources.paren_Unknown_paren; + } - if (CSharpSyntaxFacts.Instance.IsGlobalAssemblyAttribute(node)) - { - return "assembly: " + node.ConvertToSingleLine(); - } + if (CSharpSyntaxFacts.Instance.IsGlobalAssemblyAttribute(node)) + { + return "assembly: " + node.ConvertToSingleLine(); + } - // Don't discriminate between getters and setters for indexers - if (node.Parent.IsKind(SyntaxKind.AccessorList) && - node.Parent.Parent.IsKind(SyntaxKind.IndexerDeclaration)) - { - return GetDisplayName(semanticModel, node.Parent.Parent); - } + // Don't discriminate between getters and setters for indexers + if (node.Parent.IsKind(SyntaxKind.AccessorList) && + node.Parent.Parent.IsKind(SyntaxKind.IndexerDeclaration)) + { + return GetDisplayName(semanticModel, node.Parent.Parent); + } - switch (node.Kind()) - { - case SyntaxKind.ConstructorDeclaration: - { - // The constructor's name will be the name of the class, not ctor like we want - var symbol = semanticModel.GetDeclaredSymbol(node); - var displayName = symbol.ToDisplayString(Format); - var openParenIndex = displayName.IndexOf('('); - var lastDotBeforeOpenParenIndex = displayName.LastIndexOf('.', openParenIndex, openParenIndex); + switch (node.Kind()) + { + case SyntaxKind.ConstructorDeclaration: + { + // The constructor's name will be the name of the class, not ctor like we want + var symbol = semanticModel.GetDeclaredSymbol(node); + var displayName = symbol.ToDisplayString(Format); + var openParenIndex = displayName.IndexOf('('); + var lastDotBeforeOpenParenIndex = displayName.LastIndexOf('.', openParenIndex, openParenIndex); - var constructorName = symbol.IsStatic ? "cctor" : "ctor"; + var constructorName = symbol.IsStatic ? "cctor" : "ctor"; - return displayName[..(lastDotBeforeOpenParenIndex + 1)] + - constructorName + - displayName[openParenIndex..]; - } + return displayName[..(lastDotBeforeOpenParenIndex + 1)] + + constructorName + + displayName[openParenIndex..]; + } - case SyntaxKind.IndexerDeclaration: - { - // The name will be "namespace.class.this[type] - we want "namespace.class[type] Indexer" - var symbol = semanticModel.GetDeclaredSymbol(node); - var displayName = symbol.ToDisplayString(Format); - var openBracketIndex = displayName.IndexOf('['); - var lastDotBeforeOpenBracketIndex = displayName.LastIndexOf('.', openBracketIndex, openBracketIndex); - - return displayName[..lastDotBeforeOpenBracketIndex] + - displayName[openBracketIndex..] + - " Indexer"; - } + case SyntaxKind.IndexerDeclaration: + { + // The name will be "namespace.class.this[type] - we want "namespace.class[type] Indexer" + var symbol = semanticModel.GetDeclaredSymbol(node); + var displayName = symbol.ToDisplayString(Format); + var openBracketIndex = displayName.IndexOf('['); + var lastDotBeforeOpenBracketIndex = displayName.LastIndexOf('.', openBracketIndex, openBracketIndex); + + return displayName[..lastDotBeforeOpenBracketIndex] + + displayName[openBracketIndex..] + + " Indexer"; + } - case SyntaxKind.OperatorDeclaration: - { - // The name will be "namespace.class.operator +(type)" - we want namespace.class.+(type) Operator - var symbol = semanticModel.GetDeclaredSymbol(node); - var displayName = symbol.ToDisplayString(Format); - var spaceIndex = displayName.IndexOf(' '); - var lastDotBeforeSpaceIndex = displayName.LastIndexOf('.', spaceIndex, spaceIndex); - - return displayName[..(lastDotBeforeSpaceIndex + 1)] + - displayName[(spaceIndex + 1)..] + - " Operator"; - } + case SyntaxKind.OperatorDeclaration: + { + // The name will be "namespace.class.operator +(type)" - we want namespace.class.+(type) Operator + var symbol = semanticModel.GetDeclaredSymbol(node); + var displayName = symbol.ToDisplayString(Format); + var spaceIndex = displayName.IndexOf(' '); + var lastDotBeforeSpaceIndex = displayName.LastIndexOf('.', spaceIndex, spaceIndex); + + return displayName[..(lastDotBeforeSpaceIndex + 1)] + + displayName[(spaceIndex + 1)..] + + " Operator"; + } - case SyntaxKind.ConversionOperatorDeclaration: - { - // The name will be "namespace.class.operator +(type)" - we want namespace.class.+(type) Operator - var symbol = semanticModel.GetDeclaredSymbol(node); - var displayName = symbol.ToDisplayString(Format); - var firstSpaceIndex = displayName.IndexOf(' '); - var secondSpaceIndex = displayName.IndexOf(' ', firstSpaceIndex + 1); - var lastDotBeforeSpaceIndex = displayName.LastIndexOf('.', firstSpaceIndex, firstSpaceIndex); - - return displayName[..(lastDotBeforeSpaceIndex + 1)] + - displayName[(secondSpaceIndex + 1)..] + - " Operator"; - } + case SyntaxKind.ConversionOperatorDeclaration: + { + // The name will be "namespace.class.operator +(type)" - we want namespace.class.+(type) Operator + var symbol = semanticModel.GetDeclaredSymbol(node); + var displayName = symbol.ToDisplayString(Format); + var firstSpaceIndex = displayName.IndexOf(' '); + var secondSpaceIndex = displayName.IndexOf(' ', firstSpaceIndex + 1); + var lastDotBeforeSpaceIndex = displayName.LastIndexOf('.', firstSpaceIndex, firstSpaceIndex); + + return displayName[..(lastDotBeforeSpaceIndex + 1)] + + displayName[(secondSpaceIndex + 1)..] + + " Operator"; + } - case SyntaxKind.UsingDirective: - { - // We want to see usings formatted as simply "Using", prefaced by the namespace they are in - var enclosingScopeString = GetEnclosingScopeString(node, semanticModel, Format); - return string.IsNullOrEmpty(enclosingScopeString) ? "Using" : enclosingScopeString + " Using"; - } + case SyntaxKind.UsingDirective: + { + // We want to see usings formatted as simply "Using", prefaced by the namespace they are in + var enclosingScopeString = GetEnclosingScopeString(node, semanticModel, Format); + return string.IsNullOrEmpty(enclosingScopeString) ? "Using" : enclosingScopeString + " Using"; + } - case SyntaxKind.ExternAliasDirective: - { - // We want to see aliases formatted as "Alias", prefaced by their enclosing scope, if any - var enclosingScopeString = GetEnclosingScopeString(node, semanticModel, Format); - return string.IsNullOrEmpty(enclosingScopeString) ? "Alias" : enclosingScopeString + " Alias"; - } + case SyntaxKind.ExternAliasDirective: + { + // We want to see aliases formatted as "Alias", prefaced by their enclosing scope, if any + var enclosingScopeString = GetEnclosingScopeString(node, semanticModel, Format); + return string.IsNullOrEmpty(enclosingScopeString) ? "Alias" : enclosingScopeString + " Alias"; + } - default: - { - var symbol = semanticModel.GetDeclaredSymbol(node); - return symbol != null ? symbol.ToDisplayString(Format) : FeaturesResources.paren_Unknown_paren; - } - } + default: + { + var symbol = semanticModel.GetDeclaredSymbol(node); + return symbol != null ? symbol.ToDisplayString(Format) : FeaturesResources.paren_Unknown_paren; + } } + } - private static string GetEnclosingScopeString(SyntaxNode node, SemanticModel semanticModel, SymbolDisplayFormat symbolDisplayFormat) + private static string GetEnclosingScopeString(SyntaxNode node, SemanticModel semanticModel, SymbolDisplayFormat symbolDisplayFormat) + { + var scopeNode = node; + while (scopeNode != null && !SyntaxFacts.IsNamespaceMemberDeclaration(scopeNode.Kind())) { - var scopeNode = node; - while (scopeNode != null && !SyntaxFacts.IsNamespaceMemberDeclaration(scopeNode.Kind())) - { - scopeNode = scopeNode.Parent; - } - - if (scopeNode == null) - { - return null; - } + scopeNode = scopeNode.Parent; + } - var scopeSymbol = semanticModel.GetDeclaredSymbol(scopeNode); - return scopeSymbol.ToDisplayString(symbolDisplayFormat); + if (scopeNode == null) + { + return null; } + + var scopeSymbol = semanticModel.GetDeclaredSymbol(scopeNode); + return scopeSymbol.ToDisplayString(symbolDisplayFormat); } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/AddAwait/CSharpAddAwaitCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/AddAwait/CSharpAddAwaitCodeRefactoringProvider.cs index 66c5a719d2d67..b39a24a690165 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/AddAwait/CSharpAddAwaitCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/AddAwait/CSharpAddAwaitCodeRefactoringProvider.cs @@ -10,42 +10,41 @@ using Microsoft.CodeAnalysis.CodeRefactorings.AddAwait; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.AddAwait +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.AddAwait; + +/// +/// This refactoring complements the AddAwait fixer. It allows adding `await` and `await ... .ConfigureAwait(false)` even there is no compiler error to trigger the fixer. +/// +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.AddAwait), Shared] +internal partial class CSharpAddAwaitCodeRefactoringProvider : AbstractAddAwaitCodeRefactoringProvider { - /// - /// This refactoring complements the AddAwait fixer. It allows adding `await` and `await ... .ConfigureAwait(false)` even there is no compiler error to trigger the fixer. - /// - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.AddAwait), Shared] - internal partial class CSharpAddAwaitCodeRefactoringProvider : AbstractAddAwaitCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpAddAwaitCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpAddAwaitCodeRefactoringProvider() - { - } + } - protected override string GetTitle() - => CSharpFeaturesResources.Add_await; + protected override string GetTitle() + => CSharpFeaturesResources.Add_await; - protected override string GetTitleWithConfigureAwait() - => CSharpFeaturesResources.Add_await_and_ConfigureAwaitFalse; + protected override string GetTitleWithConfigureAwait() + => CSharpFeaturesResources.Add_await_and_ConfigureAwaitFalse; - protected override bool IsInAsyncContext(SyntaxNode node) + protected override bool IsInAsyncContext(SyntaxNode node) + { + foreach (var current in node.Ancestors()) { - foreach (var current in node.Ancestors()) + switch (current.Kind()) { - switch (current.Kind()) - { - case SyntaxKind.ParenthesizedLambdaExpression: - case SyntaxKind.SimpleLambdaExpression: - case SyntaxKind.AnonymousMethodExpression: - return ((AnonymousFunctionExpressionSyntax)current).AsyncKeyword != default; - case SyntaxKind.MethodDeclaration: - return ((MethodDeclarationSyntax)current).Modifiers.Any(SyntaxKind.AsyncKeyword); - } + case SyntaxKind.ParenthesizedLambdaExpression: + case SyntaxKind.SimpleLambdaExpression: + case SyntaxKind.AnonymousMethodExpression: + return ((AnonymousFunctionExpressionSyntax)current).AsyncKeyword != default; + case SyntaxKind.MethodDeclaration: + return ((MethodDeclarationSyntax)current).Modifiers.Any(SyntaxKind.AsyncKeyword); } - - return false; } + + return false; } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/AddMissingImports/CSharpAddMissingImportsRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/AddMissingImports/CSharpAddMissingImportsRefactoringProvider.cs index e0619340c681b..42cc1adc9a67e 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/AddMissingImports/CSharpAddMissingImportsRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/AddMissingImports/CSharpAddMissingImportsRefactoringProvider.cs @@ -7,13 +7,12 @@ using Microsoft.CodeAnalysis.AddMissingImports; using Microsoft.CodeAnalysis.CodeRefactorings; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.AddMissingImports +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.AddMissingImports; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.AddMissingImports), Shared] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal class CSharpAddMissingImportsRefactoringProvider() : AbstractAddMissingImportsRefactoringProvider() { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.AddMissingImports), Shared] - [method: ImportingConstructor] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal class CSharpAddMissingImportsRefactoringProvider() : AbstractAddMissingImportsRefactoringProvider() - { - protected override string CodeActionTitle => CSharpFeaturesResources.Add_missing_usings; - } + protected override string CodeActionTitle => CSharpFeaturesResources.Add_missing_usings; } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/CSharpRefactoringHelpersService.cs b/src/Features/CSharp/Portable/CodeRefactorings/CSharpRefactoringHelpersService.cs index be1c4f49787b8..5a690e4083cab 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/CSharpRefactoringHelpersService.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/CSharpRefactoringHelpersService.cs @@ -17,99 +17,98 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings; + +[ExportLanguageService(typeof(IRefactoringHelpersService), LanguageNames.CSharp), Shared] +internal class CSharpRefactoringHelpersService : AbstractRefactoringHelpersService { - [ExportLanguageService(typeof(IRefactoringHelpersService), LanguageNames.CSharp), Shared] - internal class CSharpRefactoringHelpersService : AbstractRefactoringHelpersService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpRefactoringHelpersService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpRefactoringHelpersService() - { - } + } - protected override IHeaderFacts HeaderFacts => CSharpHeaderFacts.Instance; + protected override IHeaderFacts HeaderFacts => CSharpHeaderFacts.Instance; - public override bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration) - { - var token = root.FindToken(position); - var typeDecl = token.GetAncestor(); - typeDeclaration = typeDecl; + public override bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration) + { + var token = root.FindToken(position); + var typeDecl = token.GetAncestor(); + typeDeclaration = typeDecl; - if (typeDecl == null) - return false; + if (typeDecl == null) + return false; - RoslynDebug.AssertNotNull(typeDeclaration); - if (position < typeDecl.OpenBraceToken.Span.End || - position > typeDecl.CloseBraceToken.Span.Start) - { - return false; - } + RoslynDebug.AssertNotNull(typeDeclaration); + if (position < typeDecl.OpenBraceToken.Span.End || + position > typeDecl.CloseBraceToken.Span.Start) + { + return false; + } - var line = sourceText.Lines.GetLineFromPosition(position); - if (!line.IsEmptyOrWhitespace()) - return false; + var line = sourceText.Lines.GetLineFromPosition(position); + if (!line.IsEmptyOrWhitespace()) + return false; - var member = typeDecl.Members.FirstOrDefault(d => d.FullSpan.Contains(position)); - if (member == null) - { - // There are no members, or we're after the last member. - return true; - } - else + var member = typeDecl.Members.FirstOrDefault(d => d.FullSpan.Contains(position)); + if (member == null) + { + // There are no members, or we're after the last member. + return true; + } + else + { + // We're within a member. Make sure we're in the leading whitespace of + // the member. + if (position < member.SpanStart) { - // We're within a member. Make sure we're in the leading whitespace of - // the member. - if (position < member.SpanStart) + foreach (var trivia in member.GetLeadingTrivia()) { - foreach (var trivia in member.GetLeadingTrivia()) - { - if (!trivia.IsWhitespaceOrEndOfLine()) - return false; - - if (trivia.FullSpan.Contains(position)) - return true; - } + if (!trivia.IsWhitespaceOrEndOfLine()) + return false; + + if (trivia.FullSpan.Contains(position)) + return true; } } - - return false; } - protected override IEnumerable ExtractNodesSimple(SyntaxNode? node, ISyntaxFactsService syntaxFacts) + return false; + } + + protected override IEnumerable ExtractNodesSimple(SyntaxNode? node, ISyntaxFactsService syntaxFacts) + { + if (node == null) { - if (node == null) - { - yield break; - } + yield break; + } - foreach (var extractedNode in base.ExtractNodesSimple(node, syntaxFacts)) - { - yield return extractedNode; - } + foreach (var extractedNode in base.ExtractNodesSimple(node, syntaxFacts)) + { + yield return extractedNode; + } - // `var a = b;` - // -> `var a = b`; - if (node is LocalDeclarationStatementSyntax localDeclaration) - { - yield return localDeclaration.Declaration; - } + // `var a = b;` + // -> `var a = b`; + if (node is LocalDeclarationStatementSyntax localDeclaration) + { + yield return localDeclaration.Declaration; + } - // var `a = b`; - if (node is VariableDeclaratorSyntax declarator) + // var `a = b`; + if (node is VariableDeclaratorSyntax declarator) + { + var declaration = declarator.Parent; + if (declaration?.Parent is LocalDeclarationStatementSyntax localDeclarationStatement) { - var declaration = declarator.Parent; - if (declaration?.Parent is LocalDeclarationStatementSyntax localDeclarationStatement) + var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); + if (variables.Count == 1) { - var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); - if (variables.Count == 1) - { - // -> `var a = b`; - yield return declaration; - - // -> `var a = b;` - yield return localDeclarationStatement; - } + // -> `var a = b`; + yield return declaration; + + // -> `var a = b;` + yield return localDeclarationStatement; } } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/EnableNullable/EnableNullableCodeRefactoringProvider.FixAllProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/EnableNullable/EnableNullableCodeRefactoringProvider.FixAllProvider.cs index 3c395ce51e71b..7dd06526a4517 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/EnableNullable/EnableNullableCodeRefactoringProvider.FixAllProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/EnableNullable/EnableNullableCodeRefactoringProvider.FixAllProvider.cs @@ -13,64 +13,63 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using FixAllScope = Microsoft.CodeAnalysis.CodeFixes.FixAllScope; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.EnableNullable +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.EnableNullable; + +internal partial class EnableNullableCodeRefactoringProvider : CodeRefactoringProvider { - internal partial class EnableNullableCodeRefactoringProvider : CodeRefactoringProvider + internal sealed override CodeAnalysis.CodeRefactorings.FixAllProvider? GetFixAllProvider() + => FixAllProvider.Instance; + + private sealed class FixAllProvider : CodeAnalysis.CodeRefactorings.FixAllProvider { - internal sealed override CodeAnalysis.CodeRefactorings.FixAllProvider? GetFixAllProvider() - => FixAllProvider.Instance; + public static readonly FixAllProvider Instance = new(); - private sealed class FixAllProvider : CodeAnalysis.CodeRefactorings.FixAllProvider + private FixAllProvider() { - public static readonly FixAllProvider Instance = new(); + } - private FixAllProvider() - { - } + public override IEnumerable GetSupportedFixAllScopes() + => ImmutableArray.Create(FixAllScope.Solution); - public override IEnumerable GetSupportedFixAllScopes() - => ImmutableArray.Create(FixAllScope.Solution); + public override Task GetFixAsync(FixAllContext fixAllContext) + { + Debug.Assert(fixAllContext.Scope == FixAllScope.Solution); + return Task.FromResult(new FixAllCodeAction(EnableNullableReferenceTypesInSolutionAsync)); - public override Task GetFixAsync(FixAllContext fixAllContext) + async Task EnableNullableReferenceTypesInSolutionAsync( + CodeActionPurpose purpose, IProgress progress, CancellationToken cancellationToken) { - Debug.Assert(fixAllContext.Scope == FixAllScope.Solution); - return Task.FromResult(new FixAllCodeAction(EnableNullableReferenceTypesInSolutionAsync)); - - async Task EnableNullableReferenceTypesInSolutionAsync( - CodeActionPurpose purpose, IProgress progress, CancellationToken cancellationToken) + var solution = fixAllContext.Solution; + foreach (var projectId in solution.ProjectIds) { - var solution = fixAllContext.Solution; - foreach (var projectId in solution.ProjectIds) - { - var project = solution.GetRequiredProject(projectId); - if (!ShouldOfferRefactoring(project)) - continue; - - solution = await EnableNullableReferenceTypesAsync(project, purpose, - fixAllContext.GetOptionsProvider(), progress, fixAllContext.CancellationToken).ConfigureAwait(false); - } + var project = solution.GetRequiredProject(projectId); + if (!ShouldOfferRefactoring(project)) + continue; - return solution; + solution = await EnableNullableReferenceTypesAsync(project, purpose, + fixAllContext.GetOptionsProvider(), progress, fixAllContext.CancellationToken).ConfigureAwait(false); } + + return solution; } + } - private sealed class FixAllCodeAction(Func, CancellationToken, Task> createChangedSolution) - : CodeAction.SolutionChangeAction( - CSharpFeaturesResources.Enable_nullable_reference_types_in_solution, - (progress, cancellationToken) => createChangedSolution(CodeActionPurpose.Apply, progress, cancellationToken), - nameof(CSharpFeaturesResources.Enable_nullable_reference_types_in_solution)) - { - private readonly Func, CancellationToken, Task> _createChangedSolution = createChangedSolution; + private sealed class FixAllCodeAction(Func, CancellationToken, Task> createChangedSolution) + : CodeAction.SolutionChangeAction( + CSharpFeaturesResources.Enable_nullable_reference_types_in_solution, + (progress, cancellationToken) => createChangedSolution(CodeActionPurpose.Apply, progress, cancellationToken), + nameof(CSharpFeaturesResources.Enable_nullable_reference_types_in_solution)) + { + private readonly Func, CancellationToken, Task> _createChangedSolution = createChangedSolution; - protected override async Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) - { - var changedSolution = await _createChangedSolution( - CodeActionPurpose.Preview, CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false); - if (changedSolution is null) - return []; + protected override async Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) + { + var changedSolution = await _createChangedSolution( + CodeActionPurpose.Preview, CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false); + if (changedSolution is null) + return []; - return new CodeActionOperation[] { new ApplyChangesOperation(changedSolution) }; - } + return new CodeActionOperation[] { new ApplyChangesOperation(changedSolution) }; } } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/EnableNullable/EnableNullableCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/EnableNullable/EnableNullableCodeRefactoringProvider.cs index c88d4906f38f3..dbcd36b4ab619 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/EnableNullable/EnableNullableCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/EnableNullable/EnableNullableCodeRefactoringProvider.cs @@ -14,261 +14,260 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.EnableNullable +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.EnableNullable; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.EnableNullable), Shared] +internal partial class EnableNullableCodeRefactoringProvider : CodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.EnableNullable), Shared] - internal partial class EnableNullableCodeRefactoringProvider : CodeRefactoringProvider + private static readonly Func s_isNullableDirectiveTriviaPredicate = + directive => directive.IsKind(SyntaxKind.NullableDirectiveTrivia); + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public EnableNullableCodeRefactoringProvider() + { + } + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - private static readonly Func s_isNullableDirectiveTriviaPredicate = - directive => directive.IsKind(SyntaxKind.NullableDirectiveTrivia); + var (document, textSpan, cancellationToken) = context; + if (!textSpan.IsEmpty) + return; - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public EnableNullableCodeRefactoringProvider() + if (!ShouldOfferRefactoring(document.Project)) { + return; } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(textSpan.Start, findInsideTrivia: true); + if (token.IsKind(SyntaxKind.EndOfDirectiveToken)) + token = root.FindToken(textSpan.Start - 1, findInsideTrivia: true); + + if (token.Kind() is not (SyntaxKind.EnableKeyword or SyntaxKind.RestoreKeyword or SyntaxKind.DisableKeyword or SyntaxKind.NullableKeyword or SyntaxKind.HashToken) || + token.Parent is not NullableDirectiveTriviaSyntax nullableDirectiveTrivia) { - var (document, textSpan, cancellationToken) = context; - if (!textSpan.IsEmpty) - return; + return; + } - if (!ShouldOfferRefactoring(document.Project)) - { - return; - } + context.RegisterRefactoring(new CustomCodeAction( + (purpose, progress, cancellationToken) => EnableNullableReferenceTypesAsync(document.Project, purpose, context.Options, progress, cancellationToken))); + } - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(textSpan.Start, findInsideTrivia: true); - if (token.IsKind(SyntaxKind.EndOfDirectiveToken)) - token = root.FindToken(textSpan.Start - 1, findInsideTrivia: true); + private static bool ShouldOfferRefactoring(Project project) + => project is + { + ParseOptions: CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp8 }, + CompilationOptions.NullableContextOptions: NullableContextOptions.Disable, + }; - if (token.Kind() is not (SyntaxKind.EnableKeyword or SyntaxKind.RestoreKeyword or SyntaxKind.DisableKeyword or SyntaxKind.NullableKeyword or SyntaxKind.HashToken) || - token.Parent is not NullableDirectiveTriviaSyntax nullableDirectiveTrivia) - { - return; - } + private static async Task EnableNullableReferenceTypesAsync( + Project project, CodeActionPurpose purpose, CodeActionOptionsProvider fallbackOptions, IProgress _, CancellationToken cancellationToken) + { + var solution = project.Solution; + foreach (var document in project.Documents) + { + if (await document.IsGeneratedCodeAsync(cancellationToken).ConfigureAwait(false)) + continue; - context.RegisterRefactoring(new CustomCodeAction( - (purpose, progress, cancellationToken) => EnableNullableReferenceTypesAsync(document.Project, purpose, context.Options, progress, cancellationToken))); + var updatedDocumentRoot = await EnableNullableReferenceTypesAsync(document, fallbackOptions, cancellationToken).ConfigureAwait(false); + solution = solution.WithDocumentSyntaxRoot(document.Id, updatedDocumentRoot); } - private static bool ShouldOfferRefactoring(Project project) - => project is - { - ParseOptions: CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp8 }, - CompilationOptions.NullableContextOptions: NullableContextOptions.Disable, - }; - - private static async Task EnableNullableReferenceTypesAsync( - Project project, CodeActionPurpose purpose, CodeActionOptionsProvider fallbackOptions, IProgress _, CancellationToken cancellationToken) + if (purpose is CodeActionPurpose.Apply) { - var solution = project.Solution; - foreach (var document in project.Documents) - { - if (await document.IsGeneratedCodeAsync(cancellationToken).ConfigureAwait(false)) - continue; - - var updatedDocumentRoot = await EnableNullableReferenceTypesAsync(document, fallbackOptions, cancellationToken).ConfigureAwait(false); - solution = solution.WithDocumentSyntaxRoot(document.Id, updatedDocumentRoot); - } + var compilationOptions = (CSharpCompilationOptions)project.CompilationOptions!; + solution = solution.WithProjectCompilationOptions(project.Id, compilationOptions.WithNullableContextOptions(NullableContextOptions.Enable)); + } - if (purpose is CodeActionPurpose.Apply) - { - var compilationOptions = (CSharpCompilationOptions)project.CompilationOptions!; - solution = solution.WithProjectCompilationOptions(project.Id, compilationOptions.WithNullableContextOptions(NullableContextOptions.Enable)); - } + return solution; + } - return solution; + private static async Task EnableNullableReferenceTypesAsync(Document document, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var firstToken = GetFirstTokenOfInterest(root); + if (firstToken.IsKind(SyntaxKind.None)) + { + // The document has no content, so it's fine to change the nullable context + return root; } - private static async Task EnableNullableReferenceTypesAsync(Document document, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var firstToken = GetFirstTokenOfInterest(root); - if (firstToken.IsKind(SyntaxKind.None)) - { - // The document has no content, so it's fine to change the nullable context - return root; - } + // Update #nullable directives that already exist in the document + (root, firstToken) = RewriteExistingDirectives(root, firstToken); - // Update #nullable directives that already exist in the document - (root, firstToken) = RewriteExistingDirectives(root, firstToken); + // Update existing documents to retain their original semantics + // + // * Add '#nullable disable' if the document didn't specify other semantics + // * Remove leading '#nullable restore' (was '#nullable enable' prior to rewrite in the previous step) + // * Otherwise, leave existing '#nullable' directive since it will control the initial semantics for the document + return await DisableNullableReferenceTypesInExistingDocumentIfNecessaryAsync(document, root, firstToken, fallbackOptions, cancellationToken).ConfigureAwait(false); + } - // Update existing documents to retain their original semantics - // - // * Add '#nullable disable' if the document didn't specify other semantics - // * Remove leading '#nullable restore' (was '#nullable enable' prior to rewrite in the previous step) - // * Otherwise, leave existing '#nullable' directive since it will control the initial semantics for the document - return await DisableNullableReferenceTypesInExistingDocumentIfNecessaryAsync(document, root, firstToken, fallbackOptions, cancellationToken).ConfigureAwait(false); + private static (SyntaxNode root, SyntaxToken firstToken) RewriteExistingDirectives(SyntaxNode root, SyntaxToken firstToken) + { + var firstNonDirectiveToken = root.GetFirstToken(); + var firstDirective = root.GetFirstDirective(s_isNullableDirectiveTriviaPredicate); + if (firstNonDirectiveToken.IsKind(SyntaxKind.None) && firstDirective is null) + { + // The document has no semantic content, and also has no nullable directives to update + return (root, firstToken); } - private static (SyntaxNode root, SyntaxToken firstToken) RewriteExistingDirectives(SyntaxNode root, SyntaxToken firstToken) + // Update all prior nullable directives + var directives = new List(); + for (var directive = firstDirective; directive is not null; directive = directive.GetNextDirective(s_isNullableDirectiveTriviaPredicate)) { - var firstNonDirectiveToken = root.GetFirstToken(); - var firstDirective = root.GetFirstDirective(s_isNullableDirectiveTriviaPredicate); - if (firstNonDirectiveToken.IsKind(SyntaxKind.None) && firstDirective is null) - { - // The document has no semantic content, and also has no nullable directives to update - return (root, firstToken); - } + directives.Add((NullableDirectiveTriviaSyntax)directive); + } - // Update all prior nullable directives - var directives = new List(); - for (var directive = firstDirective; directive is not null; directive = directive.GetNextDirective(s_isNullableDirectiveTriviaPredicate)) + var updatedRoot = root.ReplaceNodes( + directives, + (originalNode, rewrittenNode) => { - directives.Add((NullableDirectiveTriviaSyntax)directive); - } - - var updatedRoot = root.ReplaceNodes( - directives, - (originalNode, rewrittenNode) => + if (originalNode.SettingToken.IsKind(SyntaxKind.DisableKeyword)) { - if (originalNode.SettingToken.IsKind(SyntaxKind.DisableKeyword)) - { - // 'disable' keeps its meaning - return rewrittenNode; - } - - if (originalNode.SettingToken.IsKind(SyntaxKind.RestoreKeyword)) - { - return rewrittenNode.WithSettingToken(SyntaxFactory.Token(SyntaxKind.DisableKeyword).WithTriviaFrom(rewrittenNode.SettingToken)); - } - - if (originalNode.SettingToken.IsKind(SyntaxKind.EnableKeyword)) - { - return rewrittenNode.WithSettingToken(SyntaxFactory.Token(SyntaxKind.RestoreKeyword).WithTriviaFrom(rewrittenNode.SettingToken)); - } - - Debug.Fail("Unexpected state?"); + // 'disable' keeps its meaning return rewrittenNode; - }); + } - return (updatedRoot, GetFirstTokenOfInterest(updatedRoot)); - } + if (originalNode.SettingToken.IsKind(SyntaxKind.RestoreKeyword)) + { + return rewrittenNode.WithSettingToken(SyntaxFactory.Token(SyntaxKind.DisableKeyword).WithTriviaFrom(rewrittenNode.SettingToken)); + } - private static async Task DisableNullableReferenceTypesInExistingDocumentIfNecessaryAsync(Document document, SyntaxNode root, SyntaxToken firstToken, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var options = await document.GetCSharpCodeFixOptionsProviderAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var newLine = SyntaxFactory.EndOfLine(options.NewLine); + if (originalNode.SettingToken.IsKind(SyntaxKind.EnableKeyword)) + { + return rewrittenNode.WithSettingToken(SyntaxFactory.Token(SyntaxKind.RestoreKeyword).WithTriviaFrom(rewrittenNode.SettingToken)); + } - // Add a new '#nullable disable' to the top of each file - if (!HasLeadingNullableDirective(root, out var leadingDirective)) - { - var nullableDisableTrivia = SyntaxFactory.Trivia(SyntaxFactory.NullableDirectiveTrivia(SyntaxFactory.Token(SyntaxKind.DisableKeyword).WithPrependedLeadingTrivia(SyntaxFactory.ElasticSpace), isActive: true)); + Debug.Fail("Unexpected state?"); + return rewrittenNode; + }); - var existingTriviaList = firstToken.LeadingTrivia; - var insertionIndex = GetInsertionPoint(existingTriviaList); + return (updatedRoot, GetFirstTokenOfInterest(updatedRoot)); + } - return root.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(existingTriviaList.InsertRange(insertionIndex, [nullableDisableTrivia, newLine, newLine]))); - } - else if (leadingDirective.SettingToken.IsKind(SyntaxKind.RestoreKeyword) && leadingDirective.TargetToken.IsKind(SyntaxKind.None)) - { - // Remove the leading `#nullable restore` directive because it's redundant. Since there is no - // RemoveTrivia call, we replace the trivia with an empty marker. - return root.ReplaceTrivia(leadingDirective.ParentTrivia, SyntaxFactory.ElasticMarker); - } - else - { - // No need to add a '#nullable disable' directive because the file already starts with an unconditional - // '#nullable' directive that will override it. - return root; - } - } + private static async Task DisableNullableReferenceTypesInExistingDocumentIfNecessaryAsync(Document document, SyntaxNode root, SyntaxToken firstToken, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var options = await document.GetCSharpCodeFixOptionsProviderAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var newLine = SyntaxFactory.EndOfLine(options.NewLine); - private static int GetInsertionPoint(SyntaxTriviaList list) + // Add a new '#nullable disable' to the top of each file + if (!HasLeadingNullableDirective(root, out var leadingDirective)) { - var insertionPoint = list.Count; - for (var i = list.Count - 1; i >= 0; i--) - { - switch (list[i].Kind()) - { - case SyntaxKind.WhitespaceTrivia: - case SyntaxKind.EndOfLineTrivia: - case SyntaxKind.SingleLineCommentTrivia: - case SyntaxKind.MultiLineCommentTrivia: - continue; - - case SyntaxKind.SingleLineDocumentationCommentTrivia: - case SyntaxKind.MultiLineDocumentationCommentTrivia: - // Insert before the documentation comment - insertionPoint = i; - continue; - - default: - return insertionPoint; - } - } + var nullableDisableTrivia = SyntaxFactory.Trivia(SyntaxFactory.NullableDirectiveTrivia(SyntaxFactory.Token(SyntaxKind.DisableKeyword).WithPrependedLeadingTrivia(SyntaxFactory.ElasticSpace), isActive: true)); + + var existingTriviaList = firstToken.LeadingTrivia; + var insertionIndex = GetInsertionPoint(existingTriviaList); - return insertionPoint; + return root.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(existingTriviaList.InsertRange(insertionIndex, [nullableDisableTrivia, newLine, newLine]))); + } + else if (leadingDirective.SettingToken.IsKind(SyntaxKind.RestoreKeyword) && leadingDirective.TargetToken.IsKind(SyntaxKind.None)) + { + // Remove the leading `#nullable restore` directive because it's redundant. Since there is no + // RemoveTrivia call, we replace the trivia with an empty marker. + return root.ReplaceTrivia(leadingDirective.ParentTrivia, SyntaxFactory.ElasticMarker); + } + else + { + // No need to add a '#nullable disable' directive because the file already starts with an unconditional + // '#nullable' directive that will override it. + return root; } + } - private static SyntaxToken GetFirstTokenOfInterest(SyntaxNode root) + private static int GetInsertionPoint(SyntaxTriviaList list) + { + var insertionPoint = list.Count; + for (var i = list.Count - 1; i >= 0; i--) { - var firstToken = root.GetFirstToken(includeDirectives: true); - if (firstToken.IsKind(SyntaxKind.None)) + switch (list[i].Kind()) { - return firstToken; - } + case SyntaxKind.WhitespaceTrivia: + case SyntaxKind.EndOfLineTrivia: + case SyntaxKind.SingleLineCommentTrivia: + case SyntaxKind.MultiLineCommentTrivia: + continue; - if (firstToken.IsKind(SyntaxKind.HashToken) && firstToken.Parent.IsKind(SyntaxKind.RegionDirectiveTrivia)) - { - // If the file starts with a #region/#endregion that contains no semantic content (e.g. just a file - // header), skip it. - var nextToken = firstToken.Parent.GetLastToken(includeDirectives: true).GetNextToken(includeDirectives: true); - if (nextToken.IsKind(SyntaxKind.HashToken) && nextToken.Parent.IsKind(SyntaxKind.EndRegionDirectiveTrivia)) - { - firstToken = nextToken.Parent.GetLastToken(includeDirectives: true).GetNextToken(includeDirectives: true); - } + case SyntaxKind.SingleLineDocumentationCommentTrivia: + case SyntaxKind.MultiLineDocumentationCommentTrivia: + // Insert before the documentation comment + insertionPoint = i; + continue; + + default: + return insertionPoint; } + } + + return insertionPoint; + } + private static SyntaxToken GetFirstTokenOfInterest(SyntaxNode root) + { + var firstToken = root.GetFirstToken(includeDirectives: true); + if (firstToken.IsKind(SyntaxKind.None)) + { return firstToken; } - private static bool HasLeadingNullableDirective(SyntaxNode root, [NotNullWhen(true)] out NullableDirectiveTriviaSyntax? leadingNullableDirective) + if (firstToken.IsKind(SyntaxKind.HashToken) && firstToken.Parent.IsKind(SyntaxKind.RegionDirectiveTrivia)) { - // A leading nullable directive is a '#nullable' directive which precedes any conditional directives ('#if') - // or code (non-trivia). - var firstRelevantDirective = root.GetFirstDirective(static directive => directive.Kind() is SyntaxKind.NullableDirectiveTrivia or SyntaxKind.IfDirectiveTrivia); - if (firstRelevantDirective is NullableDirectiveTriviaSyntax nullableDirective - && nullableDirective.TargetToken.IsKind(SyntaxKind.None)) + // If the file starts with a #region/#endregion that contains no semantic content (e.g. just a file + // header), skip it. + var nextToken = firstToken.Parent.GetLastToken(includeDirectives: true).GetNextToken(includeDirectives: true); + if (nextToken.IsKind(SyntaxKind.HashToken) && nextToken.Parent.IsKind(SyntaxKind.EndRegionDirectiveTrivia)) { - var firstSemanticToken = root.GetFirstToken(); - if (firstSemanticToken.IsKind(SyntaxKind.None) || firstSemanticToken.SpanStart > nullableDirective.Span.End) - { - leadingNullableDirective = nullableDirective; - return true; - } + firstToken = nextToken.Parent.GetLastToken(includeDirectives: true).GetNextToken(includeDirectives: true); } - - leadingNullableDirective = null; - return false; } - private enum CodeActionPurpose + return firstToken; + } + + private static bool HasLeadingNullableDirective(SyntaxNode root, [NotNullWhen(true)] out NullableDirectiveTriviaSyntax? leadingNullableDirective) + { + // A leading nullable directive is a '#nullable' directive which precedes any conditional directives ('#if') + // or code (non-trivia). + var firstRelevantDirective = root.GetFirstDirective(static directive => directive.Kind() is SyntaxKind.NullableDirectiveTrivia or SyntaxKind.IfDirectiveTrivia); + if (firstRelevantDirective is NullableDirectiveTriviaSyntax nullableDirective + && nullableDirective.TargetToken.IsKind(SyntaxKind.None)) { - Preview, - Apply, + var firstSemanticToken = root.GetFirstToken(); + if (firstSemanticToken.IsKind(SyntaxKind.None) || firstSemanticToken.SpanStart > nullableDirective.Span.End) + { + leadingNullableDirective = nullableDirective; + return true; + } } - private sealed class CustomCodeAction( - Func, CancellationToken, Task> createChangedSolution) - : CodeAction.SolutionChangeAction( - CSharpFeaturesResources.Enable_nullable_reference_types_in_project, - (progress, cancellationToken) => createChangedSolution(CodeActionPurpose.Apply, progress, cancellationToken), - nameof(CSharpFeaturesResources.Enable_nullable_reference_types_in_project)) - { - private readonly Func, CancellationToken, Task> _createChangedSolution = createChangedSolution; + leadingNullableDirective = null; + return false; + } - protected override async Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) - { - var changedSolution = await _createChangedSolution(CodeActionPurpose.Preview, CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false); - if (changedSolution is null) - return []; + private enum CodeActionPurpose + { + Preview, + Apply, + } - return new CodeActionOperation[] { new ApplyChangesOperation(changedSolution) }; - } + private sealed class CustomCodeAction( + Func, CancellationToken, Task> createChangedSolution) + : CodeAction.SolutionChangeAction( + CSharpFeaturesResources.Enable_nullable_reference_types_in_project, + (progress, cancellationToken) => createChangedSolution(CodeActionPurpose.Apply, progress, cancellationToken), + nameof(CSharpFeaturesResources.Enable_nullable_reference_types_in_project)) + { + private readonly Func, CancellationToken, Task> _createChangedSolution = createChangedSolution; + + protected override async Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) + { + var changedSolution = await _createChangedSolution(CodeActionPurpose.Preview, CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false); + if (changedSolution is null) + return []; + + return new CodeActionOperation[] { new ApplyChangesOperation(changedSolution) }; } } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/ExtractClass/CSharpExtractClassCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/ExtractClass/CSharpExtractClassCodeRefactoringProvider.cs index b860964e66efa..f0f2654be8e3b 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/ExtractClass/CSharpExtractClassCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/ExtractClass/CSharpExtractClassCodeRefactoringProvider.cs @@ -13,36 +13,35 @@ using Microsoft.CodeAnalysis.ExtractClass; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ExtractClass +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ExtractClass; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ExtractClass), Shared] +[ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.ExtractInterface)] +[ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.UseExpressionBody)] +internal class CSharpExtractClassCodeRefactoringProvider : AbstractExtractClassRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ExtractClass), Shared] - [ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.ExtractInterface)] - [ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.UseExpressionBody)] - internal class CSharpExtractClassCodeRefactoringProvider : AbstractExtractClassRefactoringProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpExtractClassCodeRefactoringProvider() + : base(null) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpExtractClassCodeRefactoringProvider() - : base(null) - { - } - - /// - /// Test purpose only. - /// - [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] - internal CSharpExtractClassCodeRefactoringProvider(IExtractClassOptionsService optionsService) - : base(optionsService) - { - } + } - protected override async Task GetSelectedClassDeclarationAsync(CodeRefactoringContext context) - { - var relaventNodes = await context.GetRelevantNodesAsync().ConfigureAwait(false); - return relaventNodes.FirstOrDefault(); - } + /// + /// Test purpose only. + /// + [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] + internal CSharpExtractClassCodeRefactoringProvider(IExtractClassOptionsService optionsService) + : base(optionsService) + { + } - protected override Task> GetSelectedNodesAsync(CodeRefactoringContext context) - => NodeSelectionHelpers.GetSelectedDeclarationsOrVariablesAsync(context); + protected override async Task GetSelectedClassDeclarationAsync(CodeRefactoringContext context) + { + var relaventNodes = await context.GetRelevantNodesAsync().ConfigureAwait(false); + return relaventNodes.FirstOrDefault(); } + + protected override Task> GetSelectedNodesAsync(CodeRefactoringContext context) + => NodeSelectionHelpers.GetSelectedDeclarationsOrVariablesAsync(context); } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/InlineMethod/CSharpInlineMethodRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/InlineMethod/CSharpInlineMethodRefactoringProvider.cs index 4201de430905f..923c2d561d687 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/InlineMethod/CSharpInlineMethodRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/InlineMethod/CSharpInlineMethodRefactoringProvider.cs @@ -12,126 +12,125 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.InlineMethod; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.InlineMethod +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.InlineMethod; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InlineMethod), Shared] +[Export(typeof(CSharpInlineMethodRefactoringProvider))] +internal sealed class CSharpInlineMethodRefactoringProvider + : AbstractInlineMethodRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InlineMethod), Shared] - [Export(typeof(CSharpInlineMethodRefactoringProvider))] - internal sealed class CSharpInlineMethodRefactoringProvider - : AbstractInlineMethodRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpInlineMethodRefactoringProvider() + : base(CSharpSyntaxFacts.Instance, CSharpSemanticFactsService.Instance) { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpInlineMethodRefactoringProvider() - : base(CSharpSyntaxFacts.Instance, CSharpSemanticFactsService.Instance) - { - } + } - protected override ExpressionSyntax? GetRawInlineExpression(BaseMethodDeclarationSyntax methodDeclarationSyntax) + protected override ExpressionSyntax? GetRawInlineExpression(BaseMethodDeclarationSyntax methodDeclarationSyntax) + { + var blockSyntaxNode = methodDeclarationSyntax.Body; + if (blockSyntaxNode != null) { - var blockSyntaxNode = methodDeclarationSyntax.Body; - if (blockSyntaxNode != null) + // 1. If it is an ordinary method with block + if (blockSyntaxNode.Statements is [var statementSyntax]) { - // 1. If it is an ordinary method with block - if (blockSyntaxNode.Statements is [var statementSyntax]) + return statementSyntax switch { - return statementSyntax switch - { - // Note: For this case this will return null in Callee() - // void Caller() { Callee(); } - // void Callee() { return; } - // Refactoring won't be provided for this case. - ReturnStatementSyntax returnStatementSyntax => returnStatementSyntax.Expression, - ExpressionStatementSyntax expressionStatementSyntax => expressionStatementSyntax.Expression, - ThrowStatementSyntax throwStatementSyntax => throwStatementSyntax.Expression, - _ => null - }; - } - } - else - { - // 2. If it is an Arrow Expression - return methodDeclarationSyntax.ExpressionBody?.Expression; + // Note: For this case this will return null in Callee() + // void Caller() { Callee(); } + // void Callee() { return; } + // Refactoring won't be provided for this case. + ReturnStatementSyntax returnStatementSyntax => returnStatementSyntax.Expression, + ExpressionStatementSyntax expressionStatementSyntax => expressionStatementSyntax.Expression, + ThrowStatementSyntax throwStatementSyntax => throwStatementSyntax.Expression, + _ => null + }; } - - return null; + } + else + { + // 2. If it is an Arrow Expression + return methodDeclarationSyntax.ExpressionBody?.Expression; } - protected override SyntaxNode GenerateTypeSyntax(ITypeSymbol symbol, bool allowVar) - => symbol.GenerateTypeSyntax(allowVar); + return null; + } - protected override ExpressionSyntax GenerateLiteralExpression(ITypeSymbol typeSymbol, object? value) - => ExpressionGenerator.GenerateExpression(CSharpSyntaxGenerator.Instance, typeSymbol, value, canUseFieldReference: true); + protected override SyntaxNode GenerateTypeSyntax(ITypeSymbol symbol, bool allowVar) + => symbol.GenerateTypeSyntax(allowVar); - protected override bool IsFieldDeclarationSyntax(SyntaxNode node) - => node.IsKind(SyntaxKind.FieldDeclaration); + protected override ExpressionSyntax GenerateLiteralExpression(ITypeSymbol typeSymbol, object? value) + => ExpressionGenerator.GenerateExpression(CSharpSyntaxGenerator.Instance, typeSymbol, value, canUseFieldReference: true); - protected override bool IsValidExpressionUnderExpressionStatement(ExpressionSyntax expressionNode) - { - // C# Expression Statements defined in the language reference - // expression_statement - // : statement_expression ';' - // ; - // - // statement_expression - // : invocation_expression - // | null_conditional_invocation_expression - // | object_creation_expression - // | assignment - // | post_increment_expression - // | post_decrement_expression - // | pre_increment_expression - // | pre_decrement_expression - // | await_expression - // ; - var isNullConditionalInvocationExpression = IsNullConditionalInvocationExpression(expressionNode); + protected override bool IsFieldDeclarationSyntax(SyntaxNode node) + => node.IsKind(SyntaxKind.FieldDeclaration); - return expressionNode.IsKind(SyntaxKind.InvocationExpression) - || isNullConditionalInvocationExpression - || expressionNode is AssignmentExpressionSyntax - || expressionNode.Kind() - is SyntaxKind.InvocationExpression - or SyntaxKind.ObjectCreationExpression - or SyntaxKind.PreIncrementExpression - or SyntaxKind.PreDecrementExpression - or SyntaxKind.PostIncrementExpression - or SyntaxKind.PostDecrementExpression - or SyntaxKind.AwaitExpression; - } + protected override bool IsValidExpressionUnderExpressionStatement(ExpressionSyntax expressionNode) + { + // C# Expression Statements defined in the language reference + // expression_statement + // : statement_expression ';' + // ; + // + // statement_expression + // : invocation_expression + // | null_conditional_invocation_expression + // | object_creation_expression + // | assignment + // | post_increment_expression + // | post_decrement_expression + // | pre_increment_expression + // | pre_decrement_expression + // | await_expression + // ; + var isNullConditionalInvocationExpression = IsNullConditionalInvocationExpression(expressionNode); - protected override bool CanBeReplacedByThrowExpression(SyntaxNode syntaxNode) - { - // C# Throw Expression definition in language reference: - // 'A throw expression is permitted in only the following syntactic contexts: - // As the second or third operand of a ternary conditional operator ?: - // As the second operand of a null coalescing operator ?? - // As the body of an expression-bodied lambda or method.' - return syntaxNode.Parent switch - { - ConditionalExpressionSyntax conditionalExpressionSyntax - => syntaxNode.Equals(conditionalExpressionSyntax.WhenTrue) || - syntaxNode.Equals(conditionalExpressionSyntax.WhenFalse), - BinaryExpressionSyntax(kind: SyntaxKind.CoalesceExpression) binaryExpressionSyntax - => syntaxNode.Equals(binaryExpressionSyntax.Right), - LambdaExpressionSyntax lambdaExpressionSyntax - => lambdaExpressionSyntax.ExpressionBody != null, - var parent => parent.IsKind(SyntaxKind.ArrowExpressionClause), - }; - } + return expressionNode.IsKind(SyntaxKind.InvocationExpression) + || isNullConditionalInvocationExpression + || expressionNode is AssignmentExpressionSyntax + || expressionNode.Kind() + is SyntaxKind.InvocationExpression + or SyntaxKind.ObjectCreationExpression + or SyntaxKind.PreIncrementExpression + or SyntaxKind.PreDecrementExpression + or SyntaxKind.PostIncrementExpression + or SyntaxKind.PostDecrementExpression + or SyntaxKind.AwaitExpression; + } - private static bool IsNullConditionalInvocationExpression(ExpressionSyntax expressionSyntax) + protected override bool CanBeReplacedByThrowExpression(SyntaxNode syntaxNode) + { + // C# Throw Expression definition in language reference: + // 'A throw expression is permitted in only the following syntactic contexts: + // As the second or third operand of a ternary conditional operator ?: + // As the second operand of a null coalescing operator ?? + // As the body of an expression-bodied lambda or method.' + return syntaxNode.Parent switch { - // Check if the expression syntax is like an invocation expression nested inside ConditionalAccessExpressionSyntax. - // For example: a?.b.c() - // - // If the expression is ended with an invocation - // (if the expressions in the middle are not ConditionalAccessExpressionSyntax), - // like a?.b.e.c(), the syntax tree would be - // ConditionalAccessExpressionSyntax -> InvocationExpression. - // And in case of example like a?.b?.d?.c(); - // This is case it would be - // ConditionalAccessExpressionSyntax -> ConditionalAccessExpressionSyntax -> ... -> InvocationExpression. - return expressionSyntax is ConditionalAccessExpressionSyntax { WhenNotNull: var whenNotNull } && - (whenNotNull.IsKind(SyntaxKind.InvocationExpression) || IsNullConditionalInvocationExpression(whenNotNull)); - } + ConditionalExpressionSyntax conditionalExpressionSyntax + => syntaxNode.Equals(conditionalExpressionSyntax.WhenTrue) || + syntaxNode.Equals(conditionalExpressionSyntax.WhenFalse), + BinaryExpressionSyntax(kind: SyntaxKind.CoalesceExpression) binaryExpressionSyntax + => syntaxNode.Equals(binaryExpressionSyntax.Right), + LambdaExpressionSyntax lambdaExpressionSyntax + => lambdaExpressionSyntax.ExpressionBody != null, + var parent => parent.IsKind(SyntaxKind.ArrowExpressionClause), + }; + } + + private static bool IsNullConditionalInvocationExpression(ExpressionSyntax expressionSyntax) + { + // Check if the expression syntax is like an invocation expression nested inside ConditionalAccessExpressionSyntax. + // For example: a?.b.c() + // + // If the expression is ended with an invocation + // (if the expressions in the middle are not ConditionalAccessExpressionSyntax), + // like a?.b.e.c(), the syntax tree would be + // ConditionalAccessExpressionSyntax -> InvocationExpression. + // And in case of example like a?.b?.d?.c(); + // This is case it would be + // ConditionalAccessExpressionSyntax -> ConditionalAccessExpressionSyntax -> ... -> InvocationExpression. + return expressionSyntax is ConditionalAccessExpressionSyntax { WhenNotNull: var whenNotNull } && + (whenNotNull.IsKind(SyntaxKind.InvocationExpression) || IsNullConditionalInvocationExpression(whenNotNull)); } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/InlineTemporary/InlineTemporaryCodeRefactoringProvider.ReferenceRewriter.cs b/src/Features/CSharp/Portable/CodeRefactorings/InlineTemporary/InlineTemporaryCodeRefactoringProvider.ReferenceRewriter.cs index ab1c89aa71565..56cb42acf8635 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/InlineTemporary/InlineTemporaryCodeRefactoringProvider.ReferenceRewriter.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/InlineTemporary/InlineTemporaryCodeRefactoringProvider.ReferenceRewriter.cs @@ -8,110 +8,109 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.InlineTemporary +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.InlineTemporary; + +internal partial class CSharpInlineTemporaryCodeRefactoringProvider { - internal partial class CSharpInlineTemporaryCodeRefactoringProvider + private class ReferenceRewriter : CSharpSyntaxRewriter { - private class ReferenceRewriter : CSharpSyntaxRewriter + private readonly ISet _conflictReferences; + private readonly ISet _nonConflictReferences; + private readonly ExpressionSyntax _expressionToInline; + private readonly CancellationToken _cancellationToken; + + private ReferenceRewriter( + ISet conflictReferences, + ISet nonConflictReferences, + ExpressionSyntax expressionToInline, + CancellationToken cancellationToken) { - private readonly ISet _conflictReferences; - private readonly ISet _nonConflictReferences; - private readonly ExpressionSyntax _expressionToInline; - private readonly CancellationToken _cancellationToken; - - private ReferenceRewriter( - ISet conflictReferences, - ISet nonConflictReferences, - ExpressionSyntax expressionToInline, - CancellationToken cancellationToken) - { - _conflictReferences = conflictReferences; - _nonConflictReferences = nonConflictReferences; - _expressionToInline = expressionToInline; - _cancellationToken = cancellationToken; - } + _conflictReferences = conflictReferences; + _nonConflictReferences = nonConflictReferences; + _expressionToInline = expressionToInline; + _cancellationToken = cancellationToken; + } - private ExpressionSyntax UpdateIdentifier(IdentifierNameSyntax node) - { - _cancellationToken.ThrowIfCancellationRequested(); + private ExpressionSyntax UpdateIdentifier(IdentifierNameSyntax node) + { + _cancellationToken.ThrowIfCancellationRequested(); - if (_conflictReferences.Contains(node)) - return node.Update(node.Identifier.WithAdditionalAnnotations(CreateConflictAnnotation())); + if (_conflictReferences.Contains(node)) + return node.Update(node.Identifier.WithAdditionalAnnotations(CreateConflictAnnotation())); - if (_nonConflictReferences.Contains(node)) - return _expressionToInline; + if (_nonConflictReferences.Contains(node)) + return _expressionToInline; - return node; - } + return node; + } - public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) - { - var result = UpdateIdentifier(node); - return result == _expressionToInline - ? result.WithTriviaFrom(node) - : result; - } + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) + { + var result = UpdateIdentifier(node); + return result == _expressionToInline + ? result.WithTriviaFrom(node) + : result; + } - public override SyntaxNode? VisitAnonymousObjectMemberDeclarator(AnonymousObjectMemberDeclaratorSyntax node) + public override SyntaxNode? VisitAnonymousObjectMemberDeclarator(AnonymousObjectMemberDeclaratorSyntax node) + { + if (node.NameEquals == null && + node.Expression is IdentifierNameSyntax identifier && + _nonConflictReferences.Contains(identifier)) { - if (node.NameEquals == null && - node.Expression is IdentifierNameSyntax identifier && - _nonConflictReferences.Contains(identifier)) - { - - // Special case inlining into anonymous types to ensure that we keep property names: - // - // E.g. - // int x = 42; - // var a = new { x; }; - // - // Should become: - // var a = new { x = 42; }; - return node.Update( - SyntaxFactory.NameEquals(identifier), UpdateIdentifier(identifier)).WithTriviaFrom(node); - } - - return base.VisitAnonymousObjectMemberDeclarator(node); - } - public override SyntaxNode? VisitArgument(ArgumentSyntax node) - { - if (node.Parent is TupleExpressionSyntax tupleExpression && - ShouldAddTupleMemberName(node, out var identifier) && - tupleExpression.Arguments.Count(a => ShouldAddTupleMemberName(a, out _)) == 1) - { - return node.Update( - SyntaxFactory.NameColon(identifier), node.RefKindKeyword, UpdateIdentifier(identifier)).WithTriviaFrom(node); - } - - return base.VisitArgument(node); + // Special case inlining into anonymous types to ensure that we keep property names: + // + // E.g. + // int x = 42; + // var a = new { x; }; + // + // Should become: + // var a = new { x = 42; }; + return node.Update( + SyntaxFactory.NameEquals(identifier), UpdateIdentifier(identifier)).WithTriviaFrom(node); } - private bool ShouldAddTupleMemberName(ArgumentSyntax node, [NotNullWhen(true)] out IdentifierNameSyntax? identifier) + return base.VisitAnonymousObjectMemberDeclarator(node); + } + + public override SyntaxNode? VisitArgument(ArgumentSyntax node) + { + if (node.Parent is TupleExpressionSyntax tupleExpression && + ShouldAddTupleMemberName(node, out var identifier) && + tupleExpression.Arguments.Count(a => ShouldAddTupleMemberName(a, out _)) == 1) { - if (node.NameColon == null && - node.Expression is IdentifierNameSyntax id && - _nonConflictReferences.Contains(id) && - !SyntaxFacts.IsReservedTupleElementName(id.Identifier.ValueText)) - { - identifier = id; - return true; - } - - identifier = null; - return false; + return node.Update( + SyntaxFactory.NameColon(identifier), node.RefKindKeyword, UpdateIdentifier(identifier)).WithTriviaFrom(node); } - public static SyntaxNode Visit( - SyntaxNode scope, - ISet conflictReferences, - ISet nonConflictReferences, - ExpressionSyntax expressionToInline, - CancellationToken cancellationToken) + return base.VisitArgument(node); + } + + private bool ShouldAddTupleMemberName(ArgumentSyntax node, [NotNullWhen(true)] out IdentifierNameSyntax? identifier) + { + if (node.NameColon == null && + node.Expression is IdentifierNameSyntax id && + _nonConflictReferences.Contains(id) && + !SyntaxFacts.IsReservedTupleElementName(id.Identifier.ValueText)) { - var rewriter = new ReferenceRewriter(conflictReferences, nonConflictReferences, expressionToInline, cancellationToken); - return rewriter.Visit(scope); + identifier = id; + return true; } + + identifier = null; + return false; + } + + public static SyntaxNode Visit( + SyntaxNode scope, + ISet conflictReferences, + ISet nonConflictReferences, + ExpressionSyntax expressionToInline, + CancellationToken cancellationToken) + { + var rewriter = new ReferenceRewriter(conflictReferences, nonConflictReferences, expressionToInline, cancellationToken); + return rewriter.Visit(scope); } } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/InlineTemporary/InlineTemporaryCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/InlineTemporary/InlineTemporaryCodeRefactoringProvider.cs index f6f3d36f2505d..62e881f45a9c6 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/InlineTemporary/InlineTemporaryCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/InlineTemporary/InlineTemporaryCodeRefactoringProvider.cs @@ -22,437 +22,436 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.InlineTemporary +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.InlineTemporary; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InlineTemporary), Shared] +internal partial class CSharpInlineTemporaryCodeRefactoringProvider + : AbstractInlineTemporaryCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InlineTemporary), Shared] - internal partial class CSharpInlineTemporaryCodeRefactoringProvider - : AbstractInlineTemporaryCodeRefactoringProvider + private static readonly SyntaxAnnotation DefinitionAnnotation = new(); + private static readonly SyntaxAnnotation ReferenceAnnotation = new(); + private static readonly SyntaxAnnotation ExpressionAnnotation = new(); + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpInlineTemporaryCodeRefactoringProvider() { - private static readonly SyntaxAnnotation DefinitionAnnotation = new(); - private static readonly SyntaxAnnotation ReferenceAnnotation = new(); - private static readonly SyntaxAnnotation ExpressionAnnotation = new(); + } - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpInlineTemporaryCodeRefactoringProvider() + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, _, cancellationToken) = context; + if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) { + return; } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var variableDeclarator = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (variableDeclarator == null) { - var (document, _, cancellationToken) = context; - if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) - { - return; - } + return; + } - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (variableDeclarator.Parent is not VariableDeclarationSyntax variableDeclaration || + variableDeclaration.Parent is not LocalDeclarationStatementSyntax localDeclarationStatement) + { + return; + } - var variableDeclarator = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (variableDeclarator == null) - { - return; - } + if (variableDeclarator.Initializer == null || + variableDeclarator.Initializer.Value.IsMissing || + variableDeclarator.Initializer.Value.IsKind(SyntaxKind.StackAllocArrayCreationExpression)) + { + return; + } - if (variableDeclarator.Parent is not VariableDeclarationSyntax variableDeclaration || - variableDeclaration.Parent is not LocalDeclarationStatementSyntax localDeclarationStatement) - { - return; - } + if (variableDeclaration.Type.Kind() == SyntaxKind.RefType) + { + // TODO: inlining ref returns: + // https://github.com/dotnet/roslyn/issues/17132 + return; + } - if (variableDeclarator.Initializer == null || - variableDeclarator.Initializer.Value.IsMissing || - variableDeclarator.Initializer.Value.IsKind(SyntaxKind.StackAllocArrayCreationExpression)) - { - return; - } + if (localDeclarationStatement.ContainsDiagnostics || + localDeclarationStatement.UsingKeyword != default) + { + return; + } - if (variableDeclaration.Type.Kind() == SyntaxKind.RefType) - { - // TODO: inlining ref returns: - // https://github.com/dotnet/roslyn/issues/17132 - return; - } + var references = await GetReferenceLocationsAsync(document, variableDeclarator, cancellationToken).ConfigureAwait(false); + if (!references.Any()) + return; + + // If the variable is itself referenced in its own initializer then don't offer anything here. This + // practically does not occur (though the language allows it), and it only serves to add a huge amount + // of complexity to this feature. + if (references.Any(static (r, variableDeclarator) => variableDeclarator.Initializer.Span.Contains(r.Span), variableDeclarator)) + return; + + context.RegisterRefactoring( + CodeAction.Create( + FeaturesResources.Inline_temporary_variable, + c => InlineTemporaryAsync(document, variableDeclarator, c), + nameof(FeaturesResources.Inline_temporary_variable)), + variableDeclarator.Span); + } - if (localDeclarationStatement.ContainsDiagnostics || - localDeclarationStatement.UsingKeyword != default) - { - return; - } + private static bool HasConflict(IdentifierNameSyntax identifier, VariableDeclaratorSyntax variableDeclarator) + { + // TODO: Check for more conflict types. + if (identifier.SpanStart < variableDeclarator.SpanStart) + return true; - var references = await GetReferenceLocationsAsync(document, variableDeclarator, cancellationToken).ConfigureAwait(false); - if (!references.Any()) - return; - - // If the variable is itself referenced in its own initializer then don't offer anything here. This - // practically does not occur (though the language allows it), and it only serves to add a huge amount - // of complexity to this feature. - if (references.Any(static (r, variableDeclarator) => variableDeclarator.Initializer.Span.Contains(r.Span), variableDeclarator)) - return; - - context.RegisterRefactoring( - CodeAction.Create( - FeaturesResources.Inline_temporary_variable, - c => InlineTemporaryAsync(document, variableDeclarator, c), - nameof(FeaturesResources.Inline_temporary_variable)), - variableDeclarator.Span); - } + var identifierNode = identifier.WalkUpParentheses(); + if (IsInDeconstructionAssignmentLeft(identifierNode)) + return true; - private static bool HasConflict(IdentifierNameSyntax identifier, VariableDeclaratorSyntax variableDeclarator) + if (identifierNode?.Parent is ArgumentSyntax argument) { - // TODO: Check for more conflict types. - if (identifier.SpanStart < variableDeclarator.SpanStart) - return true; - - var identifierNode = identifier.WalkUpParentheses(); - if (IsInDeconstructionAssignmentLeft(identifierNode)) - return true; - - if (identifierNode?.Parent is ArgumentSyntax argument) - { - if (argument.RefOrOutKeyword.Kind() != SyntaxKind.None) - return true; - } - else if (identifierNode.Parent.Kind() is - SyntaxKind.PreDecrementExpression or - SyntaxKind.PreIncrementExpression or - SyntaxKind.PostDecrementExpression or - SyntaxKind.PostIncrementExpression or - SyntaxKind.AddressOfExpression) - { + if (argument.RefOrOutKeyword.Kind() != SyntaxKind.None) return true; - } - else if (identifierNode.Parent is AssignmentExpressionSyntax binaryExpression && - binaryExpression.Left == identifierNode) - { - return true; - } - - return false; } - - private static SyntaxAnnotation CreateConflictAnnotation() - => ConflictAnnotation.Create(CSharpFeaturesResources.Conflict_s_detected); - - private static async Task InlineTemporaryAsync(Document document, VariableDeclaratorSyntax declarator, CancellationToken cancellationToken) + else if (identifierNode.Parent.Kind() is + SyntaxKind.PreDecrementExpression or + SyntaxKind.PreIncrementExpression or + SyntaxKind.PostDecrementExpression or + SyntaxKind.PostIncrementExpression or + SyntaxKind.AddressOfExpression) { - // Create the expression that we're actually going to inline. - var expressionToInline = await CreateExpressionToInlineAsync(document, declarator, cancellationToken).ConfigureAwait(false); - expressionToInline = expressionToInline.WithoutTrivia().Parenthesize().WithAdditionalAnnotations(Simplifier.Annotation, ExpressionAnnotation); - - // Annotate the variable declarator so that we can get back to it later. - document = await document.ReplaceNodeAsync(declarator, declarator.WithAdditionalAnnotations(DefinitionAnnotation), cancellationToken).ConfigureAwait(false); + return true; + } + else if (identifierNode.Parent is AssignmentExpressionSyntax binaryExpression && + binaryExpression.Left == identifierNode) + { + return true; + } - declarator = await FindDeclaratorAsync(document, cancellationToken).ConfigureAwait(false); + return false; + } - // Collect the identifier names for each reference. - var allReferences = await GetReferenceLocationsAsync(document, declarator, cancellationToken).ConfigureAwait(false); + private static SyntaxAnnotation CreateConflictAnnotation() + => ConflictAnnotation.Create(CSharpFeaturesResources.Conflict_s_detected); - // Add referenceAnnotations to identifier nodes being replaced. - document = await document.ReplaceNodesAsync( - allReferences, - (o, n) => n.WithAdditionalAnnotations(ReferenceAnnotation), - cancellationToken).ConfigureAwait(false); + private static async Task InlineTemporaryAsync(Document document, VariableDeclaratorSyntax declarator, CancellationToken cancellationToken) + { + // Create the expression that we're actually going to inline. + var expressionToInline = await CreateExpressionToInlineAsync(document, declarator, cancellationToken).ConfigureAwait(false); + expressionToInline = expressionToInline.WithoutTrivia().Parenthesize().WithAdditionalAnnotations(Simplifier.Annotation, ExpressionAnnotation); - declarator = await FindDeclaratorAsync(document, cancellationToken).ConfigureAwait(false); + // Annotate the variable declarator so that we can get back to it later. + document = await document.ReplaceNodeAsync(declarator, declarator.WithAdditionalAnnotations(DefinitionAnnotation), cancellationToken).ConfigureAwait(false); - allReferences = await FindReferenceAnnotatedNodesAsync(document, cancellationToken).ConfigureAwait(false); + declarator = await FindDeclaratorAsync(document, cancellationToken).ConfigureAwait(false); - var conflictReferences = allReferences.Where(n => HasConflict(n, declarator)).ToSet(); - var nonConflictReferences = allReferences.Where(n => !conflictReferences.Contains(n)).ToSet(); + // Collect the identifier names for each reference. + var allReferences = await GetReferenceLocationsAsync(document, declarator, cancellationToken).ConfigureAwait(false); - // Checks to see if inlining the temporary variable may change the code's meaning. This can only apply if the variable has two or more - // references. We later use this heuristic to determine whether or not to display a warning message to the user. - var mayContainSideEffects = allReferences.Count() > 1 && - MayContainSideEffects(declarator.Initializer.Value); + // Add referenceAnnotations to identifier nodes being replaced. + document = await document.ReplaceNodesAsync( + allReferences, + (o, n) => n.WithAdditionalAnnotations(ReferenceAnnotation), + cancellationToken).ConfigureAwait(false); - var scope = GetScope(declarator); - var newScope = ReferenceRewriter.Visit(scope, conflictReferences, nonConflictReferences, expressionToInline, cancellationToken); + declarator = await FindDeclaratorAsync(document, cancellationToken).ConfigureAwait(false); - document = await document.ReplaceNodeAsync(scope, newScope, cancellationToken).ConfigureAwait(false); + allReferences = await FindReferenceAnnotatedNodesAsync(document, cancellationToken).ConfigureAwait(false); - declarator = await FindDeclaratorAsync(document, cancellationToken).ConfigureAwait(false); - newScope = GetScope(declarator); + var conflictReferences = allReferences.Where(n => HasConflict(n, declarator)).ToSet(); + var nonConflictReferences = allReferences.Where(n => !conflictReferences.Contains(n)).ToSet(); - // Note that we only remove the local declaration if there weren't any conflicts, - if (conflictReferences.Count == 0) - { - // No semantic conflicts, we can remove the definition. - document = await document.ReplaceNodeAsync( - newScope, RemoveDeclaratorFromScope(declarator, newScope), cancellationToken).ConfigureAwait(false); - } + // Checks to see if inlining the temporary variable may change the code's meaning. This can only apply if the variable has two or more + // references. We later use this heuristic to determine whether or not to display a warning message to the user. + var mayContainSideEffects = allReferences.Count() > 1 && + MayContainSideEffects(declarator.Initializer.Value); - // Finally, check all the places we inlined an expression and add some final warnings there if appropriate. - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var topmostParentingExpressions = root.GetAnnotatedNodes(ExpressionAnnotation) - .OfType() - .Select(e => GetTopMostParentingExpression(e)); + var scope = GetScope(declarator); + var newScope = ReferenceRewriter.Visit(scope, conflictReferences, nonConflictReferences, expressionToInline, cancellationToken); - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + document = await document.ReplaceNodeAsync(scope, newScope, cancellationToken).ConfigureAwait(false); - // Make each topmost parenting statement or Equals Clause Expressions semantically explicit. - document = await document.ReplaceNodesAsync(topmostParentingExpressions, (o, n) => - { - // warn when inlining into a conditional expression, as the inlined expression will not be executed. - if (semanticModel.GetSymbolInfo(o, cancellationToken).Symbol is IMethodSymbol { IsConditional: true }) - { - n = n.WithAdditionalAnnotations( - WarningAnnotation.Create(CSharpFeaturesResources.Warning_Inlining_temporary_into_conditional_method_call)); - } + declarator = await FindDeclaratorAsync(document, cancellationToken).ConfigureAwait(false); + newScope = GetScope(declarator); - // If the refactoring may potentially change the code's semantics, display a warning message to the user. - // on the first inlined location. - if (mayContainSideEffects) - { - n = n.WithAdditionalAnnotations( - WarningAnnotation.Create(CSharpFeaturesResources.Warning_Inlining_temporary_variable_may_change_code_meaning)); - mayContainSideEffects = false; - } + // Note that we only remove the local declaration if there weren't any conflicts, + if (conflictReferences.Count == 0) + { + // No semantic conflicts, we can remove the definition. + document = await document.ReplaceNodeAsync( + newScope, RemoveDeclaratorFromScope(declarator, newScope), cancellationToken).ConfigureAwait(false); + } - return n; - }, cancellationToken).ConfigureAwait(false); + // Finally, check all the places we inlined an expression and add some final warnings there if appropriate. + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var topmostParentingExpressions = root.GetAnnotatedNodes(ExpressionAnnotation) + .OfType() + .Select(e => GetTopMostParentingExpression(e)); - return document; - } + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - private static bool MayContainSideEffects(SyntaxNode expression) + // Make each topmost parenting statement or Equals Clause Expressions semantically explicit. + document = await document.ReplaceNodesAsync(topmostParentingExpressions, (o, n) => { - // Checks to see if inlining the temporary variable may change the code's semantics. - // This is not meant to be an exhaustive check; it's more like a heuristic for obvious cases we know may cause side effects. - - var descendantNodesAndSelf = expression.DescendantNodesAndSelf(); - - // Object creation: - // e.g.: - // var [||]c = new C(); - // c.P = 1; - // var x = c; - // After refactoring: - // new C().P = 1; - // var x = new C(); - - // Invocation: - // e.g. - let method M return a new instance of an object containing property P: - // var [||]c = M(); - // c.P = 0; - // var x = c; - // After refactoring: - // M().P = 0; - // var x = M(); - if (descendantNodesAndSelf.Any(n => n.Kind() is SyntaxKind.ObjectCreationExpression or SyntaxKind.InvocationExpression)) + // warn when inlining into a conditional expression, as the inlined expression will not be executed. + if (semanticModel.GetSymbolInfo(o, cancellationToken).Symbol is IMethodSymbol { IsConditional: true }) { - return true; + n = n.WithAdditionalAnnotations( + WarningAnnotation.Create(CSharpFeaturesResources.Warning_Inlining_temporary_into_conditional_method_call)); } - // Assume if we reach here that the refactoring won't cause side effects. - return false; - } + // If the refactoring may potentially change the code's semantics, display a warning message to the user. + // on the first inlined location. + if (mayContainSideEffects) + { + n = n.WithAdditionalAnnotations( + WarningAnnotation.Create(CSharpFeaturesResources.Warning_Inlining_temporary_variable_may_change_code_meaning)); + mayContainSideEffects = false; + } - private static async Task FindDeclaratorAsync(Document document, CancellationToken cancellationToken) - => await FindNodeWithAnnotationAsync(document, DefinitionAnnotation, cancellationToken).ConfigureAwait(false); + return n; + }, cancellationToken).ConfigureAwait(false); - private static async Task FindNodeWithAnnotationAsync(Document document, SyntaxAnnotation annotation, CancellationToken cancellationToken) - where T : SyntaxNode - { - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - return root - .GetAnnotatedNodesAndTokens(annotation) - .Single() - .AsNode() as T; - } + return document; + } - private static async Task> FindReferenceAnnotatedNodesAsync(Document document, CancellationToken cancellationToken) + private static bool MayContainSideEffects(SyntaxNode expression) + { + // Checks to see if inlining the temporary variable may change the code's semantics. + // This is not meant to be an exhaustive check; it's more like a heuristic for obvious cases we know may cause side effects. + + var descendantNodesAndSelf = expression.DescendantNodesAndSelf(); + + // Object creation: + // e.g.: + // var [||]c = new C(); + // c.P = 1; + // var x = c; + // After refactoring: + // new C().P = 1; + // var x = new C(); + + // Invocation: + // e.g. - let method M return a new instance of an object containing property P: + // var [||]c = M(); + // c.P = 0; + // var x = c; + // After refactoring: + // M().P = 0; + // var x = M(); + if (descendantNodesAndSelf.Any(n => n.Kind() is SyntaxKind.ObjectCreationExpression or SyntaxKind.InvocationExpression)) { - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - return root.GetAnnotatedNodesAndTokens(ReferenceAnnotation).Select(n => (IdentifierNameSyntax)n.AsNode()).ToImmutableArray(); + return true; } - private static SyntaxNode GetScope(VariableDeclaratorSyntax variableDeclarator) - { - var variableDeclaration = (VariableDeclarationSyntax)variableDeclarator.Parent; - var localDeclaration = (LocalDeclarationStatementSyntax)variableDeclaration.Parent; - var scope = localDeclaration.Parent; + // Assume if we reach here that the refactoring won't cause side effects. + return false; + } + + private static async Task FindDeclaratorAsync(Document document, CancellationToken cancellationToken) + => await FindNodeWithAnnotationAsync(document, DefinitionAnnotation, cancellationToken).ConfigureAwait(false); - while (scope.IsKind(SyntaxKind.LabeledStatement)) - scope = scope.Parent; + private static async Task FindNodeWithAnnotationAsync(Document document, SyntaxAnnotation annotation, CancellationToken cancellationToken) + where T : SyntaxNode + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + return root + .GetAnnotatedNodesAndTokens(annotation) + .Single() + .AsNode() as T; + } - var parentExpressions = scope.AncestorsAndSelf().OfType(); - scope = parentExpressions.LastOrDefault()?.Parent ?? scope; + private static async Task> FindReferenceAnnotatedNodesAsync(Document document, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + return root.GetAnnotatedNodesAndTokens(ReferenceAnnotation).Select(n => (IdentifierNameSyntax)n.AsNode()).ToImmutableArray(); + } - if (scope.IsKind(SyntaxKind.GlobalStatement)) - scope = scope.Parent; + private static SyntaxNode GetScope(VariableDeclaratorSyntax variableDeclarator) + { + var variableDeclaration = (VariableDeclarationSyntax)variableDeclarator.Parent; + var localDeclaration = (LocalDeclarationStatementSyntax)variableDeclaration.Parent; + var scope = localDeclaration.Parent; - return scope; - } + while (scope.IsKind(SyntaxKind.LabeledStatement)) + scope = scope.Parent; - private static VariableDeclaratorSyntax FindDeclarator(SyntaxNode node) - { - var annotatedNodesOrTokens = node.GetAnnotatedNodesAndTokens(DefinitionAnnotation).ToList(); - Debug.Assert(annotatedNodesOrTokens.Count == 1, "Only a single variable declarator should have been annotated."); + var parentExpressions = scope.AncestorsAndSelf().OfType(); + scope = parentExpressions.LastOrDefault()?.Parent ?? scope; - return (VariableDeclaratorSyntax)annotatedNodesOrTokens.First().AsNode(); - } + if (scope.IsKind(SyntaxKind.GlobalStatement)) + scope = scope.Parent; - private static SyntaxNode RemoveDeclaratorFromVariableList(VariableDeclaratorSyntax variableDeclarator, VariableDeclarationSyntax variableDeclaration) - { - Debug.Assert(variableDeclaration.Variables.Count > 1); - Debug.Assert(variableDeclaration.Variables.Contains(variableDeclarator)); + return scope; + } - var localDeclaration = (LocalDeclarationStatementSyntax)variableDeclaration.Parent; - var scope = GetScope(variableDeclarator); + private static VariableDeclaratorSyntax FindDeclarator(SyntaxNode node) + { + var annotatedNodesOrTokens = node.GetAnnotatedNodesAndTokens(DefinitionAnnotation).ToList(); + Debug.Assert(annotatedNodesOrTokens.Count == 1, "Only a single variable declarator should have been annotated."); - var newLocalDeclaration = variableDeclarator.GetLeadingTrivia().Any(t => t.IsDirective) - ? localDeclaration.RemoveNode(variableDeclarator, SyntaxRemoveOptions.KeepExteriorTrivia) - : localDeclaration.RemoveNode(variableDeclarator, SyntaxRemoveOptions.KeepNoTrivia); + return (VariableDeclaratorSyntax)annotatedNodesOrTokens.First().AsNode(); + } - return scope.ReplaceNode( - localDeclaration, - newLocalDeclaration.WithAdditionalAnnotations(Formatter.Annotation)); - } + private static SyntaxNode RemoveDeclaratorFromVariableList(VariableDeclaratorSyntax variableDeclarator, VariableDeclarationSyntax variableDeclaration) + { + Debug.Assert(variableDeclaration.Variables.Count > 1); + Debug.Assert(variableDeclaration.Variables.Contains(variableDeclarator)); - private static SyntaxNode RemoveDeclaratorFromScope(VariableDeclaratorSyntax variableDeclarator, SyntaxNode scope) - { - var variableDeclaration = (VariableDeclarationSyntax)variableDeclarator.Parent; + var localDeclaration = (LocalDeclarationStatementSyntax)variableDeclaration.Parent; + var scope = GetScope(variableDeclarator); - // If there is more than one variable declarator, remove this one from the variable declaration. - if (variableDeclaration.Variables.Count > 1) - return RemoveDeclaratorFromVariableList(variableDeclarator, variableDeclaration); + var newLocalDeclaration = variableDeclarator.GetLeadingTrivia().Any(t => t.IsDirective) + ? localDeclaration.RemoveNode(variableDeclarator, SyntaxRemoveOptions.KeepExteriorTrivia) + : localDeclaration.RemoveNode(variableDeclarator, SyntaxRemoveOptions.KeepNoTrivia); - var localDeclaration = (LocalDeclarationStatementSyntax)variableDeclaration.Parent; + return scope.ReplaceNode( + localDeclaration, + newLocalDeclaration.WithAdditionalAnnotations(Formatter.Annotation)); + } - // There's only one variable declarator, so we'll remove the local declaration - // statement entirely. This means that we'll concatenate the leading and trailing - // trivia of this declaration and move it to the next statement. - var leadingTrivia = localDeclaration - .GetLeadingTrivia() - .Reverse() - .SkipWhile(t => t.IsKind(SyntaxKind.WhitespaceTrivia)) - .Reverse() - .ToSyntaxTriviaList(); + private static SyntaxNode RemoveDeclaratorFromScope(VariableDeclaratorSyntax variableDeclarator, SyntaxNode scope) + { + var variableDeclaration = (VariableDeclarationSyntax)variableDeclarator.Parent; - var trailingTrivia = localDeclaration - .GetTrailingTrivia() - .SkipWhile(t => t is (kind: SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia)) - .ToSyntaxTriviaList(); + // If there is more than one variable declarator, remove this one from the variable declaration. + if (variableDeclaration.Variables.Count > 1) + return RemoveDeclaratorFromVariableList(variableDeclarator, variableDeclaration); - var newLeadingTrivia = leadingTrivia.Concat(trailingTrivia); + var localDeclaration = (LocalDeclarationStatementSyntax)variableDeclaration.Parent; - var nextToken = localDeclaration.GetLastToken().GetNextTokenOrEndOfFile(); - var newNextToken = nextToken.WithPrependedLeadingTrivia(newLeadingTrivia) - .WithAdditionalAnnotations(Formatter.Annotation); + // There's only one variable declarator, so we'll remove the local declaration + // statement entirely. This means that we'll concatenate the leading and trailing + // trivia of this declaration and move it to the next statement. + var leadingTrivia = localDeclaration + .GetLeadingTrivia() + .Reverse() + .SkipWhile(t => t.IsKind(SyntaxKind.WhitespaceTrivia)) + .Reverse() + .ToSyntaxTriviaList(); - var newScope = scope.ReplaceToken(nextToken, newNextToken); + var trailingTrivia = localDeclaration + .GetTrailingTrivia() + .SkipWhile(t => t is (kind: SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia)) + .ToSyntaxTriviaList(); - var newLocalDeclaration = (LocalDeclarationStatementSyntax)FindDeclarator(newScope).Parent.Parent; + var newLeadingTrivia = leadingTrivia.Concat(trailingTrivia); - // If the local is parented by a label statement, we can't remove this statement. Instead, - // we'll replace the local declaration with an empty expression statement. - if (newLocalDeclaration?.Parent is LabeledStatementSyntax labeledStatement) - { - var newLabeledStatement = labeledStatement.ReplaceNode(newLocalDeclaration, SyntaxFactory.ParseStatement("")); - return newScope.ReplaceNode(labeledStatement, newLabeledStatement); - } + var nextToken = localDeclaration.GetLastToken().GetNextTokenOrEndOfFile(); + var newNextToken = nextToken.WithPrependedLeadingTrivia(newLeadingTrivia) + .WithAdditionalAnnotations(Formatter.Annotation); - // If the local is parented by a global statement, we need to remove the parent global statement. - if (newLocalDeclaration?.Parent is GlobalStatementSyntax globalStatement) - { - return newScope.RemoveNode(globalStatement, SyntaxRemoveOptions.KeepNoTrivia); - } + var newScope = scope.ReplaceToken(nextToken, newNextToken); + + var newLocalDeclaration = (LocalDeclarationStatementSyntax)FindDeclarator(newScope).Parent.Parent; - return newScope.RemoveNode(newLocalDeclaration, SyntaxRemoveOptions.KeepNoTrivia); + // If the local is parented by a label statement, we can't remove this statement. Instead, + // we'll replace the local declaration with an empty expression statement. + if (newLocalDeclaration?.Parent is LabeledStatementSyntax labeledStatement) + { + var newLabeledStatement = labeledStatement.ReplaceNode(newLocalDeclaration, SyntaxFactory.ParseStatement("")); + return newScope.ReplaceNode(labeledStatement, newLabeledStatement); } - private static async Task CreateExpressionToInlineAsync( - Document document, - VariableDeclaratorSyntax variableDeclarator, - CancellationToken cancellationToken) + // If the local is parented by a global statement, we need to remove the parent global statement. + if (newLocalDeclaration?.Parent is GlobalStatementSyntax globalStatement) { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var expression = variableDeclarator.Initializer.Value.WalkDownParentheses(); + return newScope.RemoveNode(globalStatement, SyntaxRemoveOptions.KeepNoTrivia); + } - var expressionToInline = CreateExpressionToInline(); + return newScope.RemoveNode(newLocalDeclaration, SyntaxRemoveOptions.KeepNoTrivia); + } - // If we are moving something multiline to a new location, add the formatter annotation to it so we can - // attempt to ensure it gets properly indented/dedented there. - if (!text.AreOnSameLine(expression.SpanStart, expression.Span.End)) - expressionToInline = expressionToInline.WithAdditionalAnnotations(Formatter.Annotation); + private static async Task CreateExpressionToInlineAsync( + Document document, + VariableDeclaratorSyntax variableDeclarator, + CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var expression = variableDeclarator.Initializer.Value.WalkDownParentheses(); - return expressionToInline; + var expressionToInline = CreateExpressionToInline(); - ExpressionSyntax CreateExpressionToInline() - { - var isVar = ((VariableDeclarationSyntax)variableDeclarator.Parent).Type.IsVar; - var localSymbol = (ILocalSymbol)semanticModel.GetDeclaredSymbol(variableDeclarator, cancellationToken); + // If we are moving something multiline to a new location, add the formatter annotation to it so we can + // attempt to ensure it gets properly indented/dedented there. + if (!text.AreOnSameLine(expression.SpanStart, expression.Span.End)) + expressionToInline = expressionToInline.WithAdditionalAnnotations(Formatter.Annotation); - if (expression is ImplicitObjectCreationExpressionSyntax implicitCreation) - { - // Consider: C c = new(); Console.WriteLine(c.ToString()); - // Inlining result should be: Console.WriteLine(new C().ToString()); instead of Console.WriteLine(new().ToString()); - // This condition converts implicit object creation expression to normal object creation expression. + return expressionToInline; - var type = localSymbol.Type.GenerateTypeSyntax(); - return SyntaxFactory.ObjectCreationExpression(implicitCreation.NewKeyword, type, implicitCreation.ArgumentList, implicitCreation.Initializer); - } - else if (expression is InitializerExpressionSyntax(SyntaxKind.ArrayInitializerExpression) arrayInitializer) - { - // If this is an array initializer, we need to transform it into an array creation - // expression for inlining. + ExpressionSyntax CreateExpressionToInline() + { + var isVar = ((VariableDeclarationSyntax)variableDeclarator.Parent).Type.IsVar; + var localSymbol = (ILocalSymbol)semanticModel.GetDeclaredSymbol(variableDeclarator, cancellationToken); + + if (expression is ImplicitObjectCreationExpressionSyntax implicitCreation) + { + // Consider: C c = new(); Console.WriteLine(c.ToString()); + // Inlining result should be: Console.WriteLine(new C().ToString()); instead of Console.WriteLine(new().ToString()); + // This condition converts implicit object creation expression to normal object creation expression. - var arrayType = (ArrayTypeSyntax)localSymbol.Type.GenerateTypeSyntax(); + var type = localSymbol.Type.GenerateTypeSyntax(); + return SyntaxFactory.ObjectCreationExpression(implicitCreation.NewKeyword, type, implicitCreation.ArgumentList, implicitCreation.Initializer); + } + else if (expression is InitializerExpressionSyntax(SyntaxKind.ArrayInitializerExpression) arrayInitializer) + { + // If this is an array initializer, we need to transform it into an array creation + // expression for inlining. - // Add any non-whitespace trailing trivia from the equals clause to the type. - var equalsToken = variableDeclarator.Initializer.EqualsToken; - if (equalsToken.HasTrailingTrivia) - { - var trailingTrivia = equalsToken.TrailingTrivia.SkipInitialWhitespace(); - if (trailingTrivia.Any()) - arrayType = arrayType.WithTrailingTrivia(trailingTrivia); - } + var arrayType = (ArrayTypeSyntax)localSymbol.Type.GenerateTypeSyntax(); - return SyntaxFactory.ArrayCreationExpression(arrayType, arrayInitializer); - } - else if (isVar && expression is ObjectCreationExpressionSyntax or ArrayCreationExpressionSyntax or CastExpressionSyntax) + // Add any non-whitespace trailing trivia from the equals clause to the type. + var equalsToken = variableDeclarator.Initializer.EqualsToken; + if (equalsToken.HasTrailingTrivia) { - // if we have `var x = new Y();` there's no need to do any casting as the type is indicated - // directly in the existing code. The same holds for `new Y[]` or `(Y)...` - return expression; + var trailingTrivia = equalsToken.TrailingTrivia.SkipInitialWhitespace(); + if (trailingTrivia.Any()) + arrayType = arrayType.WithTrailingTrivia(trailingTrivia); } - else - { - if (localSymbol.Type.ContainsAnonymousType() || localSymbol.Type is IErrorTypeSymbol { Name: null or "" }) - return expression; - return expression.Cast(localSymbol.Type); - } + return SyntaxFactory.ArrayCreationExpression(arrayType, arrayInitializer); + } + else if (isVar && expression is ObjectCreationExpressionSyntax or ArrayCreationExpressionSyntax or CastExpressionSyntax) + { + // if we have `var x = new Y();` there's no need to do any casting as the type is indicated + // directly in the existing code. The same holds for `new Y[]` or `(Y)...` + return expression; + } + else + { + if (localSymbol.Type.ContainsAnonymousType() || localSymbol.Type is IErrorTypeSymbol { Name: null or "" }) + return expression; + + return expression.Cast(localSymbol.Type); } } + } - private static SyntaxNode GetTopMostParentingExpression(ExpressionSyntax expression) - => expression.AncestorsAndSelf().OfType().Last(); + private static SyntaxNode GetTopMostParentingExpression(ExpressionSyntax expression) + => expression.AncestorsAndSelf().OfType().Last(); - private static bool IsInDeconstructionAssignmentLeft(ExpressionSyntax node) - { - var parent = node.Parent; - while (parent.Kind() is SyntaxKind.ParenthesizedExpression or SyntaxKind.CastExpression) - parent = parent.Parent; + private static bool IsInDeconstructionAssignmentLeft(ExpressionSyntax node) + { + var parent = node.Parent; + while (parent.Kind() is SyntaxKind.ParenthesizedExpression or SyntaxKind.CastExpression) + parent = parent.Parent; - while (parent.IsKind(SyntaxKind.Argument)) + while (parent.IsKind(SyntaxKind.Argument)) + { + parent = parent.Parent; + if (!parent.IsKind(SyntaxKind.TupleExpression)) { - parent = parent.Parent; - if (!parent.IsKind(SyntaxKind.TupleExpression)) - { - return false; - } - else if (parent?.Parent is AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignment) - { - return assignment.Left == parent; - } - - parent = parent.Parent; + return false; + } + else if (parent?.Parent is AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignment) + { + return assignment.Left == parent; } - return false; + parent = parent.Parent; } + + return false; } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/MoveStaticMembers/CSharpMoveStaticMembersRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/MoveStaticMembers/CSharpMoveStaticMembersRefactoringProvider.cs index c5e636d2a11ae..2eccf87bc7229 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/MoveStaticMembers/CSharpMoveStaticMembersRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/MoveStaticMembers/CSharpMoveStaticMembersRefactoringProvider.cs @@ -10,18 +10,17 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.MoveStaticMembers; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.MoveStaticMembers +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.MoveStaticMembers; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MoveStaticMembers), Shared] +internal class CSharpMoveStaticMembersRefactoringProvider : AbstractMoveStaticMembersRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MoveStaticMembers), Shared] - internal class CSharpMoveStaticMembersRefactoringProvider : AbstractMoveStaticMembersRefactoringProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpMoveStaticMembersRefactoringProvider() : base() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpMoveStaticMembersRefactoringProvider() : base() - { - } - - protected override Task> GetSelectedNodesAsync(CodeRefactoringContext context) - => NodeSelectionHelpers.GetSelectedDeclarationsOrVariablesAsync(context); } + + protected override Task> GetSelectedNodesAsync(CodeRefactoringContext context) + => NodeSelectionHelpers.GetSelectedDeclarationsOrVariablesAsync(context); } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/MoveType/CSharpMoveTypeService.cs b/src/Features/CSharp/Portable/CodeRefactorings/MoveType/CSharpMoveTypeService.cs index 7717b4d20dd47..8bb6e600e12a5 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/MoveType/CSharpMoveTypeService.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/MoveType/CSharpMoveTypeService.cs @@ -14,19 +14,18 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.MoveType +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.MoveType; + +[ExportLanguageService(typeof(IMoveTypeService), LanguageNames.CSharp), Shared] +internal class CSharpMoveTypeService : + AbstractMoveTypeService { - [ExportLanguageService(typeof(IMoveTypeService), LanguageNames.CSharp), Shared] - internal class CSharpMoveTypeService : - AbstractMoveTypeService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpMoveTypeService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpMoveTypeService() - { - } - - protected override async Task GetRelevantNodeAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken) - => await document.TryGetRelevantNodeAsync(textSpan, cancellationToken).ConfigureAwait(false); } + + protected override async Task GetRelevantNodeAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken) + => await document.TryGetRelevantNodeAsync(textSpan, cancellationToken).ConfigureAwait(false); } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/NodeSelectionHelpers.cs b/src/Features/CSharp/Portable/CodeRefactorings/NodeSelectionHelpers.cs index 1494f6d1f3700..b265de9d7c22c 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/NodeSelectionHelpers.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/NodeSelectionHelpers.cs @@ -13,57 +13,56 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings; + +internal static class NodeSelectionHelpers { - internal static class NodeSelectionHelpers + internal static async Task> GetSelectedDeclarationsOrVariablesAsync(CodeRefactoringContext context) { - internal static async Task> GetSelectedDeclarationsOrVariablesAsync(CodeRefactoringContext context) + var (document, span, cancellationToken) = context; + if (span.IsEmpty) { - var (document, span, cancellationToken) = context; - if (span.IsEmpty) + // if the span is empty then we are only selecting one "member" (which could include a field which declared multiple actual members) + // Consider: + // MemberDeclaration: member that can be declared in type (those are the ones we can pull up) + // VariableDeclaratorSyntax: for fields the MemberDeclaration can actually represent multiple declarations, e.g. `int a = 0, b = 1;`. + // ..Since the user might want to select & pull up only one of them (e.g. `int a = 0, [|b = 1|];` we also look for closest VariableDeclaratorSyntax. + var memberDeclaration = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (memberDeclaration == null) { - // if the span is empty then we are only selecting one "member" (which could include a field which declared multiple actual members) - // Consider: - // MemberDeclaration: member that can be declared in type (those are the ones we can pull up) - // VariableDeclaratorSyntax: for fields the MemberDeclaration can actually represent multiple declarations, e.g. `int a = 0, b = 1;`. - // ..Since the user might want to select & pull up only one of them (e.g. `int a = 0, [|b = 1|];` we also look for closest VariableDeclaratorSyntax. - var memberDeclaration = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (memberDeclaration == null) - { - // could not find a member, we may be directly on a variable declaration - var varDeclarator = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - return varDeclarator == null - ? [] - : [varDeclarator]; - } - else - { - return memberDeclaration switch - { - FieldDeclarationSyntax fieldDeclaration => fieldDeclaration.Declaration.Variables.AsImmutable(), - EventFieldDeclarationSyntax eventFieldDeclaration => eventFieldDeclaration.Declaration.Variables.AsImmutable(), - IncompleteMemberSyntax or GlobalStatementSyntax => [], - _ => [memberDeclaration], - }; - } + // could not find a member, we may be directly on a variable declaration + var varDeclarator = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + return varDeclarator == null + ? [] + : [varDeclarator]; } else { - // if the span is non-empty, then we get potentially multiple members - // Note: even though this method handles the empty span case, we don't use it because it doesn't correctly - // pick up on keywords before the declaration, such as "public static int". - // We could potentially use it for every case if that behavior changes - var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var members = await CSharpSelectedMembers.Instance.GetSelectedMembersAsync(tree, span, allowPartialSelection: true, cancellationToken).ConfigureAwait(false); - // if we get a node that would not have an obtainable symbol (such as the ones below) - // we return an empty list instead of filtering so we don't get other potentially - // malformed syntax nodes. - // Consider pub[||] static int Foo; - // Which has 2 member nodes (an incomplete and a field), but we'd only expect one - return members.Any(m => m is GlobalStatementSyntax or IncompleteMemberSyntax) - ? [] - : members; + return memberDeclaration switch + { + FieldDeclarationSyntax fieldDeclaration => fieldDeclaration.Declaration.Variables.AsImmutable(), + EventFieldDeclarationSyntax eventFieldDeclaration => eventFieldDeclaration.Declaration.Variables.AsImmutable(), + IncompleteMemberSyntax or GlobalStatementSyntax => [], + _ => [memberDeclaration], + }; } } + else + { + // if the span is non-empty, then we get potentially multiple members + // Note: even though this method handles the empty span case, we don't use it because it doesn't correctly + // pick up on keywords before the declaration, such as "public static int". + // We could potentially use it for every case if that behavior changes + var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var members = await CSharpSelectedMembers.Instance.GetSelectedMembersAsync(tree, span, allowPartialSelection: true, cancellationToken).ConfigureAwait(false); + // if we get a node that would not have an obtainable symbol (such as the ones below) + // we return an empty list instead of filtering so we don't get other potentially + // malformed syntax nodes. + // Consider pub[||] static int Foo; + // Which has 2 member nodes (an incomplete and a field), but we'd only expect one + return members.Any(m => m is GlobalStatementSyntax or IncompleteMemberSyntax) + ? [] + : members; + } } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/PullMemberUp/CSharpPullMemberUpCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/PullMemberUp/CSharpPullMemberUpCodeRefactoringProvider.cs index 3807056a55d58..e3bc65a4a3f0d 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/PullMemberUp/CSharpPullMemberUpCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/PullMemberUp/CSharpPullMemberUpCodeRefactoringProvider.cs @@ -15,19 +15,18 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.PullMemberUp +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.PullMemberUp; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.PullMemberUp), Shared] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] +internal class CSharpPullMemberUpCodeRefactoringProvider(IPullMemberUpOptionsService service) : AbstractPullMemberUpRefactoringProvider(service) { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.PullMemberUp), Shared] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] - internal class CSharpPullMemberUpCodeRefactoringProvider(IPullMemberUpOptionsService service) : AbstractPullMemberUpRefactoringProvider(service) + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpPullMemberUpCodeRefactoringProvider() : this(service: null) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpPullMemberUpCodeRefactoringProvider() : this(service: null) - { - } - - protected override Task> GetSelectedNodesAsync(CodeRefactoringContext context) - => NodeSelectionHelpers.GetSelectedDeclarationsOrVariablesAsync(context); } + + protected override Task> GetSelectedNodesAsync(CodeRefactoringContext context) + => NodeSelectionHelpers.GetSelectedDeclarationsOrVariablesAsync(context); } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpChangeNamespaceService.cs b/src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpChangeNamespaceService.cs index 9c27cc06a833e..6ade75c9f0742 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpChangeNamespaceService.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpChangeNamespaceService.cs @@ -21,453 +21,452 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ChangeNamespace +namespace Microsoft.CodeAnalysis.CSharp.ChangeNamespace; + +[ExportLanguageService(typeof(IChangeNamespaceService), LanguageNames.CSharp), Shared] +internal sealed class CSharpChangeNamespaceService : + AbstractChangeNamespaceService { - [ExportLanguageService(typeof(IChangeNamespaceService), LanguageNames.CSharp), Shared] - internal sealed class CSharpChangeNamespaceService : - AbstractChangeNamespaceService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpChangeNamespaceService() + { + } + + protected override async Task> GetValidContainersFromAllLinkedDocumentsAsync( + Document document, + SyntaxNode container, + CancellationToken cancellationToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpChangeNamespaceService() + if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles + || document.IsGeneratedCode(cancellationToken)) { + return default; } - protected override async Task> GetValidContainersFromAllLinkedDocumentsAsync( - Document document, - SyntaxNode container, - CancellationToken cancellationToken) + TextSpan containerSpan; + if (container is BaseNamespaceDeclarationSyntax) { - if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles - || document.IsGeneratedCode(cancellationToken)) - { - return default; - } + containerSpan = container.Span; + } + else if (container is CompilationUnitSyntax) + { + // A compilation unit as container means user want to move all its members from global to some namespace. + // We use an empty span to indicate this case. + containerSpan = default; + } + else + { + throw ExceptionUtilities.Unreachable(); + } - TextSpan containerSpan; - if (container is BaseNamespaceDeclarationSyntax) - { - containerSpan = container.Span; - } - else if (container is CompilationUnitSyntax) - { - // A compilation unit as container means user want to move all its members from global to some namespace. - // We use an empty span to indicate this case. - containerSpan = default; - } - else - { - throw ExceptionUtilities.Unreachable(); - } + if (!IsSupportedLinkedDocument(document, out var allDocumentIds)) + return default; - if (!IsSupportedLinkedDocument(document, out var allDocumentIds)) - return default; + return await TryGetApplicableContainersFromAllDocumentsAsync( + document.Project.Solution, allDocumentIds, containerSpan, cancellationToken).ConfigureAwait(false); + } - return await TryGetApplicableContainersFromAllDocumentsAsync( - document.Project.Solution, allDocumentIds, containerSpan, cancellationToken).ConfigureAwait(false); - } + protected override string GetDeclaredNamespace(SyntaxNode container) + { + if (container is CompilationUnitSyntax) + return string.Empty; - protected override string GetDeclaredNamespace(SyntaxNode container) - { - if (container is CompilationUnitSyntax) - return string.Empty; + if (container is BaseNamespaceDeclarationSyntax namespaceDecl) + return CSharpSyntaxGenerator.Instance.GetName(namespaceDecl); - if (container is BaseNamespaceDeclarationSyntax namespaceDecl) - return CSharpSyntaxGenerator.Instance.GetName(namespaceDecl); + throw ExceptionUtilities.Unreachable(); + } - throw ExceptionUtilities.Unreachable(); - } + protected override SyntaxList GetMemberDeclarationsInContainer(SyntaxNode container) + { + if (container is BaseNamespaceDeclarationSyntax namespaceDecl) + return namespaceDecl.Members; - protected override SyntaxList GetMemberDeclarationsInContainer(SyntaxNode container) - { - if (container is BaseNamespaceDeclarationSyntax namespaceDecl) - return namespaceDecl.Members; + if (container is CompilationUnitSyntax compilationUnit) + return compilationUnit.Members; - if (container is CompilationUnitSyntax compilationUnit) - return compilationUnit.Members; + throw ExceptionUtilities.Unreachable(); + } - throw ExceptionUtilities.Unreachable(); + /// + /// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the namespace to be changed. + /// If this reference is the right side of a qualified name, the new node returned would be the entire qualified name. Depends on + /// whether is provided, the name in the new node might be qualified with this new namespace instead. + /// + /// A reference to a type declared inside the namespace to be changed, which is calculated based on results from + /// `SymbolFinder.FindReferencesAsync`. + /// If specified, and the reference is qualified with namespace, the namespace part of original reference + /// will be replaced with given namespace in the new node. + /// The node to be replaced. This might be an ancestor of original reference. + /// The replacement node. + public override bool TryGetReplacementReferenceSyntax( + SyntaxNode reference, + ImmutableArray newNamespaceParts, + ISyntaxFactsService syntaxFacts, + [NotNullWhen(returnValue: true)] out SyntaxNode? oldNode, + [NotNullWhen(returnValue: true)] out SyntaxNode? newNode) + { + if (reference is not SimpleNameSyntax nameRef) + { + oldNode = newNode = null; + return false; } - /// - /// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the namespace to be changed. - /// If this reference is the right side of a qualified name, the new node returned would be the entire qualified name. Depends on - /// whether is provided, the name in the new node might be qualified with this new namespace instead. - /// - /// A reference to a type declared inside the namespace to be changed, which is calculated based on results from - /// `SymbolFinder.FindReferencesAsync`. - /// If specified, and the reference is qualified with namespace, the namespace part of original reference - /// will be replaced with given namespace in the new node. - /// The node to be replaced. This might be an ancestor of original reference. - /// The replacement node. - public override bool TryGetReplacementReferenceSyntax( - SyntaxNode reference, - ImmutableArray newNamespaceParts, - ISyntaxFactsService syntaxFacts, - [NotNullWhen(returnValue: true)] out SyntaxNode? oldNode, - [NotNullWhen(returnValue: true)] out SyntaxNode? newNode) + // A few different cases are handled here: + // + // 1. When the reference is not qualified (i.e. just a simple name), then there's nothing need to be done. + // And both old and new will point to the original reference. + // + // 2. When the new namespace is not specified, we don't need to change the qualified part of reference. + // Both old and new will point to the qualified reference. + // + // 3. When the new namespace is "", i.e. we are moving type referenced by name here to global namespace. + // As a result, we need replace qualified reference with the simple name. + // + // 4. When the namespace is specified and not "", i.e. we are moving referenced type to a different non-global + // namespace. We need to replace the qualified reference with a new qualified reference (which is qualified + // with new namespace.) + // + // Note that qualified type name can appear in QualifiedNameSyntax or MemberAccessSyntax, so we need to handle both cases. + + if (syntaxFacts.IsRightOfQualifiedName(nameRef)) { - if (reference is not SimpleNameSyntax nameRef) - { - oldNode = newNode = null; - return false; - } + RoslynDebug.Assert(nameRef.Parent is object); + oldNode = nameRef.Parent; + var aliasQualifier = GetAliasQualifier(oldNode); - // A few different cases are handled here: - // - // 1. When the reference is not qualified (i.e. just a simple name), then there's nothing need to be done. - // And both old and new will point to the original reference. - // - // 2. When the new namespace is not specified, we don't need to change the qualified part of reference. - // Both old and new will point to the qualified reference. - // - // 3. When the new namespace is "", i.e. we are moving type referenced by name here to global namespace. - // As a result, we need replace qualified reference with the simple name. - // - // 4. When the namespace is specified and not "", i.e. we are moving referenced type to a different non-global - // namespace. We need to replace the qualified reference with a new qualified reference (which is qualified - // with new namespace.) - // - // Note that qualified type name can appear in QualifiedNameSyntax or MemberAccessSyntax, so we need to handle both cases. - - if (syntaxFacts.IsRightOfQualifiedName(nameRef)) - { - RoslynDebug.Assert(nameRef.Parent is object); - oldNode = nameRef.Parent; - var aliasQualifier = GetAliasQualifier(oldNode); - - if (!TryGetGlobalQualifiedName(newNamespaceParts, nameRef, aliasQualifier, out newNode)) - { - var qualifiedNamespaceName = CreateNamespaceAsQualifiedName(newNamespaceParts, aliasQualifier, newNamespaceParts.Length - 1); - newNode = SyntaxFactory.QualifiedName(qualifiedNamespaceName, nameRef.WithoutTrivia()); - } - - // We might lose some trivia associated with children of `oldNode`. - newNode = newNode.WithTriviaFrom(oldNode); - return true; - } - else if (syntaxFacts.IsNameOfSimpleMemberAccessExpression(nameRef) || - syntaxFacts.IsNameOfMemberBindingExpression(nameRef)) + if (!TryGetGlobalQualifiedName(newNamespaceParts, nameRef, aliasQualifier, out newNode)) { - RoslynDebug.Assert(nameRef.Parent is object); - oldNode = nameRef.Parent; - var aliasQualifier = GetAliasQualifier(oldNode); - - if (!TryGetGlobalQualifiedName(newNamespaceParts, nameRef, aliasQualifier, out newNode)) - { - var memberAccessNamespaceName = CreateNamespaceAsMemberAccess(newNamespaceParts, aliasQualifier, newNamespaceParts.Length - 1); - newNode = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, memberAccessNamespaceName, nameRef.WithoutTrivia()); - } - - // We might lose some trivia associated with children of `oldNode`. - newNode = newNode.WithTriviaFrom(oldNode); - return true; - } - else if (nameRef.Parent is NameMemberCrefSyntax crefName && crefName.Parent is QualifiedCrefSyntax qualifiedCref) - { - // This is the case where the reference is the right most part of a qualified name in `cref`. - // for example, `` and ``. - // This is the form of `cref` we need to handle as a spacial case when changing namespace name or - // changing namespace from non-global to global, other cases in these 2 scenarios can be handled in the - // same way we handle non cref references, for example, `` and ``. - - var container = qualifiedCref.Container; - var aliasQualifier = GetAliasQualifier(container); - - if (TryGetGlobalQualifiedName(newNamespaceParts, nameRef, aliasQualifier, out newNode)) - { - // We will replace entire `QualifiedCrefSyntax` with a `TypeCrefSyntax`, - // which is a alias qualified simple name, similar to the regular case above. - oldNode = qualifiedCref; - newNode = SyntaxFactory.TypeCref((AliasQualifiedNameSyntax)newNode!); - } - else - { - // if the new namespace is not global, then we just need to change the container in `QualifiedCrefSyntax`, - // which is just a regular namespace node, no cref node involve here. - oldNode = container; - newNode = CreateNamespaceAsQualifiedName(newNamespaceParts, aliasQualifier, newNamespaceParts.Length - 1); - } - - return true; + var qualifiedNamespaceName = CreateNamespaceAsQualifiedName(newNamespaceParts, aliasQualifier, newNamespaceParts.Length - 1); + newNode = SyntaxFactory.QualifiedName(qualifiedNamespaceName, nameRef.WithoutTrivia()); } - // Simple name reference, nothing to be done. - // The name will be resolved by adding proper import. - oldNode = newNode = nameRef; - return false; + // We might lose some trivia associated with children of `oldNode`. + newNode = newNode.WithTriviaFrom(oldNode); + return true; } - - private static bool TryGetGlobalQualifiedName( - ImmutableArray newNamespaceParts, - SimpleNameSyntax nameNode, - string? aliasQualifier, - [NotNullWhen(returnValue: true)] out SyntaxNode? newNode) + else if (syntaxFacts.IsNameOfSimpleMemberAccessExpression(nameRef) || + syntaxFacts.IsNameOfMemberBindingExpression(nameRef)) { - if (IsGlobalNamespace(newNamespaceParts)) + RoslynDebug.Assert(nameRef.Parent is object); + oldNode = nameRef.Parent; + var aliasQualifier = GetAliasQualifier(oldNode); + + if (!TryGetGlobalQualifiedName(newNamespaceParts, nameRef, aliasQualifier, out newNode)) { - // If new namespace is "", then name will be declared in global namespace. - // We will replace qualified reference with simple name qualified with alias (global if it's not alias qualified) - var aliasNode = aliasQualifier?.ToIdentifierName() ?? SyntaxFactory.IdentifierName(SyntaxFactory.Token(SyntaxKind.GlobalKeyword)); - newNode = SyntaxFactory.AliasQualifiedName(aliasNode, nameNode.WithoutTrivia()); - return true; + var memberAccessNamespaceName = CreateNamespaceAsMemberAccess(newNamespaceParts, aliasQualifier, newNamespaceParts.Length - 1); + newNode = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, memberAccessNamespaceName, nameRef.WithoutTrivia()); } - newNode = null; - return false; + // We might lose some trivia associated with children of `oldNode`. + newNode = newNode.WithTriviaFrom(oldNode); + return true; } - - /// - /// Try to change the namespace declaration based on the following rules: - /// - if neither declared nor target namespace are "" (i.e. global namespace), - /// then we try to change the name of the namespace. - /// - if declared namespace is "", then we try to move all types declared - /// in global namespace in the document into a new namespace declaration. - /// - if target namespace is "", then we try to move all members in declared - /// namespace to global namespace (i.e. remove the namespace declaration). - /// - protected override CompilationUnitSyntax ChangeNamespaceDeclaration( - CompilationUnitSyntax root, - ImmutableArray declaredNamespaceParts, - ImmutableArray targetNamespaceParts) + else if (nameRef.Parent is NameMemberCrefSyntax crefName && crefName.Parent is QualifiedCrefSyntax qualifiedCref) { - Debug.Assert(!declaredNamespaceParts.IsDefault && !targetNamespaceParts.IsDefault); - var container = root.GetAnnotatedNodes(ContainerAnnotation).Single(); + // This is the case where the reference is the right most part of a qualified name in `cref`. + // for example, `` and ``. + // This is the form of `cref` we need to handle as a spacial case when changing namespace name or + // changing namespace from non-global to global, other cases in these 2 scenarios can be handled in the + // same way we handle non cref references, for example, `` and ``. - if (container is CompilationUnitSyntax compilationUnit) + var container = qualifiedCref.Container; + var aliasQualifier = GetAliasQualifier(container); + + if (TryGetGlobalQualifiedName(newNamespaceParts, nameRef, aliasQualifier, out newNode)) { - // Move everything from global namespace to a namespace declaration - Debug.Assert(IsGlobalNamespace(declaredNamespaceParts)); - return MoveMembersFromGlobalToNamespace(compilationUnit, targetNamespaceParts); + // We will replace entire `QualifiedCrefSyntax` with a `TypeCrefSyntax`, + // which is a alias qualified simple name, similar to the regular case above. + oldNode = qualifiedCref; + newNode = SyntaxFactory.TypeCref((AliasQualifiedNameSyntax)newNode!); } - - if (container is BaseNamespaceDeclarationSyntax namespaceDecl) + else { - // Move everything to global namespace - if (IsGlobalNamespace(targetNamespaceParts)) - return MoveMembersFromNamespaceToGlobal(root, namespaceDecl); - - // Change namespace name - return root.ReplaceNode( - namespaceDecl, - namespaceDecl.WithName( - CreateNamespaceAsQualifiedName(targetNamespaceParts, aliasQualifier: null, targetNamespaceParts.Length - 1) - .WithTriviaFrom(namespaceDecl.Name).WithAdditionalAnnotations(WarningAnnotation)) - .WithoutAnnotations(ContainerAnnotation)); // Make sure to remove the annotation we added + // if the new namespace is not global, then we just need to change the container in `QualifiedCrefSyntax`, + // which is just a regular namespace node, no cref node involve here. + oldNode = container; + newNode = CreateNamespaceAsQualifiedName(newNamespaceParts, aliasQualifier, newNamespaceParts.Length - 1); } - throw ExceptionUtilities.Unreachable(); + return true; } - private static CompilationUnitSyntax MoveMembersFromNamespaceToGlobal( - CompilationUnitSyntax root, BaseNamespaceDeclarationSyntax namespaceDecl) + // Simple name reference, nothing to be done. + // The name will be resolved by adding proper import. + oldNode = newNode = nameRef; + return false; + } + + private static bool TryGetGlobalQualifiedName( + ImmutableArray newNamespaceParts, + SimpleNameSyntax nameNode, + string? aliasQualifier, + [NotNullWhen(returnValue: true)] out SyntaxNode? newNode) + { + if (IsGlobalNamespace(newNamespaceParts)) { - var (namespaceOpeningTrivia, namespaceClosingTrivia) = - GetOpeningAndClosingTriviaOfNamespaceDeclaration(namespaceDecl); - var members = namespaceDecl.Members; - var eofToken = root.EndOfFileToken - .WithAdditionalAnnotations(WarningAnnotation); - - // Try to preserve trivia from original namespace declaration. - // If there's any member inside the declaration, we attach them to the - // first and last member, otherwise, simply attach all to the EOF token. - if (members.Count > 0) - { - var first = members.First(); - var firstWithTrivia = first.WithPrependedLeadingTrivia(namespaceOpeningTrivia); - members = members.Replace(first, firstWithTrivia); + // If new namespace is "", then name will be declared in global namespace. + // We will replace qualified reference with simple name qualified with alias (global if it's not alias qualified) + var aliasNode = aliasQualifier?.ToIdentifierName() ?? SyntaxFactory.IdentifierName(SyntaxFactory.Token(SyntaxKind.GlobalKeyword)); + newNode = SyntaxFactory.AliasQualifiedName(aliasNode, nameNode.WithoutTrivia()); + return true; + } - var last = members.Last(); - var lastWithTrivia = last.WithAppendedTrailingTrivia(namespaceClosingTrivia); - members = members.Replace(last, lastWithTrivia); - } - else - { - eofToken = eofToken.WithPrependedLeadingTrivia( - namespaceOpeningTrivia.Concat(namespaceClosingTrivia)); - } + newNode = null; + return false; + } - // Moving inner imports out of the namespace declaration can lead to a break in semantics. - // For example: - // - // namespace A.B.C - // { - // using D.E.F; - // } - // - // The using of D.E.F is looked up with in the context of A.B.C first. If it's moved outside, - // it may fail to resolve. - - return root.Update( - root.Externs.AddRange(namespaceDecl.Externs), - root.Usings.AddRange(namespaceDecl.Usings), - root.AttributeLists, - root.Members.ReplaceRange(namespaceDecl, members), - eofToken); + /// + /// Try to change the namespace declaration based on the following rules: + /// - if neither declared nor target namespace are "" (i.e. global namespace), + /// then we try to change the name of the namespace. + /// - if declared namespace is "", then we try to move all types declared + /// in global namespace in the document into a new namespace declaration. + /// - if target namespace is "", then we try to move all members in declared + /// namespace to global namespace (i.e. remove the namespace declaration). + /// + protected override CompilationUnitSyntax ChangeNamespaceDeclaration( + CompilationUnitSyntax root, + ImmutableArray declaredNamespaceParts, + ImmutableArray targetNamespaceParts) + { + Debug.Assert(!declaredNamespaceParts.IsDefault && !targetNamespaceParts.IsDefault); + var container = root.GetAnnotatedNodes(ContainerAnnotation).Single(); + + if (container is CompilationUnitSyntax compilationUnit) + { + // Move everything from global namespace to a namespace declaration + Debug.Assert(IsGlobalNamespace(declaredNamespaceParts)); + return MoveMembersFromGlobalToNamespace(compilationUnit, targetNamespaceParts); } - private static CompilationUnitSyntax MoveMembersFromGlobalToNamespace(CompilationUnitSyntax compilationUnit, ImmutableArray targetNamespaceParts) + if (container is BaseNamespaceDeclarationSyntax namespaceDecl) { - Debug.Assert(!compilationUnit.Members.Any(m => m is BaseNamespaceDeclarationSyntax)); - - var targetNamespaceDecl = SyntaxFactory.NamespaceDeclaration( - name: CreateNamespaceAsQualifiedName(targetNamespaceParts, aliasQualifier: null, targetNamespaceParts.Length - 1) - .WithAdditionalAnnotations(WarningAnnotation), - externs: default, - usings: default, - members: compilationUnit.Members); - return compilationUnit.WithMembers(new SyntaxList(targetNamespaceDecl)) - .WithoutAnnotations(ContainerAnnotation); // Make sure to remove the annotation we added + // Move everything to global namespace + if (IsGlobalNamespace(targetNamespaceParts)) + return MoveMembersFromNamespaceToGlobal(root, namespaceDecl); + + // Change namespace name + return root.ReplaceNode( + namespaceDecl, + namespaceDecl.WithName( + CreateNamespaceAsQualifiedName(targetNamespaceParts, aliasQualifier: null, targetNamespaceParts.Length - 1) + .WithTriviaFrom(namespaceDecl.Name).WithAdditionalAnnotations(WarningAnnotation)) + .WithoutAnnotations(ContainerAnnotation)); // Make sure to remove the annotation we added } - /// - /// For the node specified by to be applicable container, it must be a namespace - /// declaration or a compilation unit, contain no partial declarations and meet the following additional - /// requirements: - /// - /// - If a namespace declaration: - /// 1. It doesn't contain or is nested in other namespace declarations - /// 2. The name of the namespace is valid (i.e. no errors) - /// - /// - If a compilation unit (i.e. is empty), there must be no namespace declaration - /// inside (i.e. all members are declared in global namespace) - /// - protected override async Task TryGetApplicableContainerFromSpanAsync(Document document, TextSpan span, CancellationToken cancellationToken) + throw ExceptionUtilities.Unreachable(); + } + + private static CompilationUnitSyntax MoveMembersFromNamespaceToGlobal( + CompilationUnitSyntax root, BaseNamespaceDeclarationSyntax namespaceDecl) + { + var (namespaceOpeningTrivia, namespaceClosingTrivia) = + GetOpeningAndClosingTriviaOfNamespaceDeclaration(namespaceDecl); + var members = namespaceDecl.Members; + var eofToken = root.EndOfFileToken + .WithAdditionalAnnotations(WarningAnnotation); + + // Try to preserve trivia from original namespace declaration. + // If there's any member inside the declaration, we attach them to the + // first and last member, otherwise, simply attach all to the EOF token. + if (members.Count > 0) { - var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - Contract.ThrowIfNull(syntaxRoot); - var compilationUnit = (CompilationUnitSyntax)syntaxRoot; - SyntaxNode? container = null; - - // Empty span means that user wants to move all types declared in the document to a new namespace. - // This action is only supported when everything in the document is declared in global namespace, - // which we use the number of namespace declaration nodes to decide. - if (span.IsEmpty) - { - if (ContainsNamespaceDeclaration(compilationUnit)) - return null; + var first = members.First(); + var firstWithTrivia = first.WithPrependedLeadingTrivia(namespaceOpeningTrivia); + members = members.Replace(first, firstWithTrivia); - container = compilationUnit; - } - else - { - // Otherwise, the span should contain a namespace declaration node, which must be the only one - // in the entire syntax spine to enable the change namespace operation. - if (!compilationUnit.Span.Contains(span)) - return null; + var last = members.Last(); + var lastWithTrivia = last.WithAppendedTrailingTrivia(namespaceClosingTrivia); + members = members.Replace(last, lastWithTrivia); + } + else + { + eofToken = eofToken.WithPrependedLeadingTrivia( + namespaceOpeningTrivia.Concat(namespaceClosingTrivia)); + } - var node = compilationUnit.FindNode(span, getInnermostNodeForTie: true); + // Moving inner imports out of the namespace declaration can lead to a break in semantics. + // For example: + // + // namespace A.B.C + // { + // using D.E.F; + // } + // + // The using of D.E.F is looked up with in the context of A.B.C first. If it's moved outside, + // it may fail to resolve. + + return root.Update( + root.Externs.AddRange(namespaceDecl.Externs), + root.Usings.AddRange(namespaceDecl.Usings), + root.AttributeLists, + root.Members.ReplaceRange(namespaceDecl, members), + eofToken); + } - var namespaceDecls = node.AncestorsAndSelf().OfType().ToImmutableArray(); - if (namespaceDecls.Length != 1) - return null; + private static CompilationUnitSyntax MoveMembersFromGlobalToNamespace(CompilationUnitSyntax compilationUnit, ImmutableArray targetNamespaceParts) + { + Debug.Assert(!compilationUnit.Members.Any(m => m is BaseNamespaceDeclarationSyntax)); + + var targetNamespaceDecl = SyntaxFactory.NamespaceDeclaration( + name: CreateNamespaceAsQualifiedName(targetNamespaceParts, aliasQualifier: null, targetNamespaceParts.Length - 1) + .WithAdditionalAnnotations(WarningAnnotation), + externs: default, + usings: default, + members: compilationUnit.Members); + return compilationUnit.WithMembers(new SyntaxList(targetNamespaceDecl)) + .WithoutAnnotations(ContainerAnnotation); // Make sure to remove the annotation we added + } - var namespaceDecl = namespaceDecls[0]; - if (namespaceDecl == null) - return null; + /// + /// For the node specified by to be applicable container, it must be a namespace + /// declaration or a compilation unit, contain no partial declarations and meet the following additional + /// requirements: + /// + /// - If a namespace declaration: + /// 1. It doesn't contain or is nested in other namespace declarations + /// 2. The name of the namespace is valid (i.e. no errors) + /// + /// - If a compilation unit (i.e. is empty), there must be no namespace declaration + /// inside (i.e. all members are declared in global namespace) + /// + protected override async Task TryGetApplicableContainerFromSpanAsync(Document document, TextSpan span, CancellationToken cancellationToken) + { + var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(syntaxRoot); + var compilationUnit = (CompilationUnitSyntax)syntaxRoot; + SyntaxNode? container = null; + + // Empty span means that user wants to move all types declared in the document to a new namespace. + // This action is only supported when everything in the document is declared in global namespace, + // which we use the number of namespace declaration nodes to decide. + if (span.IsEmpty) + { + if (ContainsNamespaceDeclaration(compilationUnit)) + return null; - if (namespaceDecl.Name.GetDiagnostics().Any(diag => diag.DefaultSeverity == DiagnosticSeverity.Error)) - return null; + container = compilationUnit; + } + else + { + // Otherwise, the span should contain a namespace declaration node, which must be the only one + // in the entire syntax spine to enable the change namespace operation. + if (!compilationUnit.Span.Contains(span)) + return null; - if (ContainsNamespaceDeclaration(node)) - return null; + var node = compilationUnit.FindNode(span, getInnermostNodeForTie: true); - container = namespaceDecl; - } + var namespaceDecls = node.AncestorsAndSelf().OfType().ToImmutableArray(); + if (namespaceDecls.Length != 1) + return null; - var containsPartial = - await ContainsPartialTypeWithMultipleDeclarationsAsync(document, container, cancellationToken).ConfigureAwait(false); + var namespaceDecl = namespaceDecls[0]; + if (namespaceDecl == null) + return null; - if (containsPartial) + if (namespaceDecl.Name.GetDiagnostics().Any(diag => diag.DefaultSeverity == DiagnosticSeverity.Error)) return null; - return container; + if (ContainsNamespaceDeclaration(node)) + return null; - static bool ContainsNamespaceDeclaration(SyntaxNode node) - => node.DescendantNodes(n => n is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax) - .OfType().Any(); + container = namespaceDecl; } - private static string? GetAliasQualifier(SyntaxNode? name) + var containsPartial = + await ContainsPartialTypeWithMultipleDeclarationsAsync(document, container, cancellationToken).ConfigureAwait(false); + + if (containsPartial) + return null; + + return container; + + static bool ContainsNamespaceDeclaration(SyntaxNode node) + => node.DescendantNodes(n => n is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax) + .OfType().Any(); + } + + private static string? GetAliasQualifier(SyntaxNode? name) + { + while (true) { - while (true) + switch (name) { - switch (name) - { - case QualifiedNameSyntax qualifiedNameNode: - name = qualifiedNameNode.Left; - continue; - case MemberAccessExpressionSyntax memberAccessNode: - name = memberAccessNode.Expression; - continue; - case AliasQualifiedNameSyntax aliasQualifiedNameNode: - return aliasQualifiedNameNode.Alias.Identifier.ValueText; - } - - return null; + case QualifiedNameSyntax qualifiedNameNode: + name = qualifiedNameNode.Left; + continue; + case MemberAccessExpressionSyntax memberAccessNode: + name = memberAccessNode.Expression; + continue; + case AliasQualifiedNameSyntax aliasQualifiedNameNode: + return aliasQualifiedNameNode.Alias.Identifier.ValueText; } - } - private static NameSyntax CreateNamespaceAsQualifiedName(ImmutableArray namespaceParts, string? aliasQualifier, int index) - { - var part = namespaceParts[index].EscapeIdentifier(); - Debug.Assert(part.Length > 0); + return null; + } + } - var namePiece = SyntaxFactory.IdentifierName(part); + private static NameSyntax CreateNamespaceAsQualifiedName(ImmutableArray namespaceParts, string? aliasQualifier, int index) + { + var part = namespaceParts[index].EscapeIdentifier(); + Debug.Assert(part.Length > 0); - if (index == 0) - return aliasQualifier == null ? namePiece : SyntaxFactory.AliasQualifiedName(aliasQualifier, namePiece); + var namePiece = SyntaxFactory.IdentifierName(part); - return SyntaxFactory.QualifiedName(CreateNamespaceAsQualifiedName(namespaceParts, aliasQualifier, index - 1), namePiece); - } + if (index == 0) + return aliasQualifier == null ? namePiece : SyntaxFactory.AliasQualifiedName(aliasQualifier, namePiece); - private static ExpressionSyntax CreateNamespaceAsMemberAccess(ImmutableArray namespaceParts, string? aliasQualifier, int index) - { - var part = namespaceParts[index].EscapeIdentifier(); - Debug.Assert(part.Length > 0); + return SyntaxFactory.QualifiedName(CreateNamespaceAsQualifiedName(namespaceParts, aliasQualifier, index - 1), namePiece); + } - var namePiece = SyntaxFactory.IdentifierName(part); + private static ExpressionSyntax CreateNamespaceAsMemberAccess(ImmutableArray namespaceParts, string? aliasQualifier, int index) + { + var part = namespaceParts[index].EscapeIdentifier(); + Debug.Assert(part.Length > 0); - if (index == 0) - { - return aliasQualifier == null - ? namePiece - : SyntaxFactory.AliasQualifiedName(aliasQualifier, namePiece); - } + var namePiece = SyntaxFactory.IdentifierName(part); - return SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - CreateNamespaceAsMemberAccess(namespaceParts, aliasQualifier, index - 1), - namePiece); + if (index == 0) + { + return aliasQualifier == null + ? namePiece + : SyntaxFactory.AliasQualifiedName(aliasQualifier, namePiece); } - /// - /// return trivia attached to namespace declaration. - /// Leading trivia of the node and trivia around opening brace, as well as - /// trivia around closing brace are concatenated together respectively. - /// - private static (ImmutableArray openingTrivia, ImmutableArray closingTrivia) - GetOpeningAndClosingTriviaOfNamespaceDeclaration(BaseNamespaceDeclarationSyntax baseNamespace) - { - var openingBuilder = ArrayBuilder.GetInstance(); - var closingBuilder = ArrayBuilder.GetInstance(); + return SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + CreateNamespaceAsMemberAccess(namespaceParts, aliasQualifier, index - 1), + namePiece); + } - openingBuilder.AddRange(baseNamespace.GetLeadingTrivia()); + /// + /// return trivia attached to namespace declaration. + /// Leading trivia of the node and trivia around opening brace, as well as + /// trivia around closing brace are concatenated together respectively. + /// + private static (ImmutableArray openingTrivia, ImmutableArray closingTrivia) + GetOpeningAndClosingTriviaOfNamespaceDeclaration(BaseNamespaceDeclarationSyntax baseNamespace) + { + var openingBuilder = ArrayBuilder.GetInstance(); + var closingBuilder = ArrayBuilder.GetInstance(); - if (baseNamespace is NamespaceDeclarationSyntax namespaceDeclaration) - { - openingBuilder.AddRange(namespaceDeclaration.OpenBraceToken.LeadingTrivia); - openingBuilder.AddRange(namespaceDeclaration.OpenBraceToken.TrailingTrivia); + openingBuilder.AddRange(baseNamespace.GetLeadingTrivia()); - closingBuilder.AddRange(namespaceDeclaration.CloseBraceToken.LeadingTrivia); - closingBuilder.AddRange(namespaceDeclaration.CloseBraceToken.TrailingTrivia); - } - else if (baseNamespace is FileScopedNamespaceDeclarationSyntax fileScopedNamespace) - { - openingBuilder.AddRange(fileScopedNamespace.SemicolonToken.TrailingTrivia); - } + if (baseNamespace is NamespaceDeclarationSyntax namespaceDeclaration) + { + openingBuilder.AddRange(namespaceDeclaration.OpenBraceToken.LeadingTrivia); + openingBuilder.AddRange(namespaceDeclaration.OpenBraceToken.TrailingTrivia); - return (openingBuilder.ToImmutableAndFree(), closingBuilder.ToImmutableAndFree()); + closingBuilder.AddRange(namespaceDeclaration.CloseBraceToken.LeadingTrivia); + closingBuilder.AddRange(namespaceDeclaration.CloseBraceToken.TrailingTrivia); } + else if (baseNamespace is FileScopedNamespaceDeclarationSyntax fileScopedNamespace) + { + openingBuilder.AddRange(fileScopedNamespace.SemicolonToken.TrailingTrivia); + } + + return (openingBuilder.ToImmutableAndFree(), closingBuilder.ToImmutableAndFree()); } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpSyncNamespaceCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpSyncNamespaceCodeRefactoringProvider.cs index a18d0f1c39500..0dcbb8db822a5 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpSyncNamespaceCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpSyncNamespaceCodeRefactoringProvider.cs @@ -14,52 +14,51 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.SyncNamespace +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.SyncNamespace; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.SyncNamespace), Shared] +internal sealed class CSharpSyncNamespaceCodeRefactoringProvider + : AbstractSyncNamespaceCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.SyncNamespace), Shared] - internal sealed class CSharpSyncNamespaceCodeRefactoringProvider - : AbstractSyncNamespaceCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpSyncNamespaceCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpSyncNamespaceCodeRefactoringProvider() - { - } + } - protected override async Task TryGetApplicableInvocationNodeAsync(Document document, TextSpan span, CancellationToken cancellationToken) - { - if (!span.IsEmpty) - return null; + protected override async Task TryGetApplicableInvocationNodeAsync(Document document, TextSpan span, CancellationToken cancellationToken) + { + if (!span.IsEmpty) + return null; - if (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) is not CompilationUnitSyntax compilationUnit) - return null; + if (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) is not CompilationUnitSyntax compilationUnit) + return null; - var position = span.Start; - var namespaceDecls = compilationUnit.DescendantNodes(n => n is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax) - .OfType().ToImmutableArray(); + var position = span.Start; + var namespaceDecls = compilationUnit.DescendantNodes(n => n is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax) + .OfType().ToImmutableArray(); - if (namespaceDecls.Length == 1 && compilationUnit.Members.Count == 1) - { - var namespaceDeclaration = namespaceDecls[0]; - if (namespaceDeclaration.Name.Span.IntersectsWith(position)) - return namespaceDeclaration; - } + if (namespaceDecls.Length == 1 && compilationUnit.Members.Count == 1) + { + var namespaceDeclaration = namespaceDecls[0]; + if (namespaceDeclaration.Name.Span.IntersectsWith(position)) + return namespaceDeclaration; + } - if (namespaceDecls.Length == 0) - { - var firstMemberDeclarationName = compilationUnit.Members.FirstOrDefault().GetNameToken(); + if (namespaceDecls.Length == 0) + { + var firstMemberDeclarationName = compilationUnit.Members.FirstOrDefault().GetNameToken(); - if (firstMemberDeclarationName != default - && firstMemberDeclarationName.Span.IntersectsWith(position)) - { - return compilationUnit; - } + if (firstMemberDeclarationName != default + && firstMemberDeclarationName.Span.IntersectsWith(position)) + { + return compilationUnit; } - - return null; } - protected override string EscapeIdentifier(string identifier) - => identifier.EscapeIdentifier(); + return null; } + + protected override string EscapeIdentifier(string identifier) + => identifier.EscapeIdentifier(); } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/AbstractUseTypeCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/AbstractUseTypeCodeRefactoringProvider.cs index e3b24dcd00820..17961795b60b3 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/AbstractUseTypeCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/AbstractUseTypeCodeRefactoringProvider.cs @@ -20,116 +20,115 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Simplification; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.UseType +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.UseType; + +internal abstract class AbstractUseTypeCodeRefactoringProvider : CodeRefactoringProvider { - internal abstract class AbstractUseTypeCodeRefactoringProvider : CodeRefactoringProvider + protected abstract string Title { get; } + protected abstract Task HandleDeclarationAsync(Document document, SyntaxEditor editor, TypeSyntax type, CancellationToken cancellationToken); + protected abstract TypeSyntax FindAnalyzableType(SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken); + protected abstract TypeStyleResult AnalyzeTypeName(TypeSyntax typeName, SemanticModel semanticModel, CSharpSimplifierOptions options, CancellationToken cancellationToken); + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - protected abstract string Title { get; } - protected abstract Task HandleDeclarationAsync(Document document, SyntaxEditor editor, TypeSyntax type, CancellationToken cancellationToken); - protected abstract TypeSyntax FindAnalyzableType(SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken); - protected abstract TypeStyleResult AnalyzeTypeName(TypeSyntax typeName, SemanticModel semanticModel, CSharpSimplifierOptions options, CancellationToken cancellationToken); + var (document, textSpan, cancellationToken) = context; + if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) + { + return; + } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + var declaration = await GetDeclarationAsync(context).ConfigureAwait(false); + if (declaration == null) { - var (document, textSpan, cancellationToken) = context; - if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) - { - return; - } - - var declaration = await GetDeclarationAsync(context).ConfigureAwait(false); - if (declaration == null) - { - return; - } - - Debug.Assert(declaration.Kind() is SyntaxKind.VariableDeclaration or SyntaxKind.ForEachStatement or SyntaxKind.DeclarationExpression); - - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var declaredType = FindAnalyzableType(declaration, semanticModel, cancellationToken); - if (declaredType == null) - { - return; - } - - var simplifierOptions = (CSharpSimplifierOptions)await document.GetSimplifierOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); - var typeStyle = AnalyzeTypeName(declaredType, semanticModel, simplifierOptions, cancellationToken); - if (typeStyle.IsStylePreferred && typeStyle.Notification.Severity != ReportDiagnostic.Suppress) - { - // the analyzer would handle this. So we do not. - return; - } - - if (!typeStyle.CanConvert()) - { - return; - } - - context.RegisterRefactoring( - CodeAction.Create( - Title, - c => UpdateDocumentAsync(document, declaredType, c), - Title), - declaredType.Span); + return; } - private static async Task GetDeclarationAsync(CodeRefactoringContext context) + Debug.Assert(declaration.Kind() is SyntaxKind.VariableDeclaration or SyntaxKind.ForEachStatement or SyntaxKind.DeclarationExpression); + + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var declaredType = FindAnalyzableType(declaration, semanticModel, cancellationToken); + if (declaredType == null) { - // We want to provide refactoring for changing the Type of newly introduced variables in following cases: - // - DeclarationExpressionSyntax: `"42".TryParseInt32(out var number)` - // - VariableDeclarationSyntax: General field / variable declaration statement `var number = 42` - // - ForEachStatementSyntax: The variable that gets introduced by foreach `foreach(var number in numbers)` - // - // In addition to providing the refactoring when the whole node (i.e. the node that introduces the new variable) in question is selected - // we also want to enable it when only the type node is selected because this refactoring changes the type. We still have to make sure - // we're only working on TypeNodes for in above-mentioned situations. - // - // For foreach we need to guard against selecting just the expression because it is also of type `TypeSyntax`. - - var declNode = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (declNode != null) - return declNode; - - var variableNode = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (variableNode != null) - return variableNode; - - // `ref var` is a bit of an interesting construct. 'ref' looks like a modifier, but is actually a - // type-syntax. Ensure the user can get the feature anywhere on this construct - var type = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (type?.Parent is RefTypeSyntax) - type = (TypeSyntax)type.Parent; - - if (type?.Parent is VariableDeclarationSyntax) - return type.Parent; - - var foreachStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (foreachStatement != null) - return foreachStatement; - - var syntaxFacts = context.Document.GetLanguageService(); - - var typeNode = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - var typeNodeParent = typeNode?.Parent; - if (typeNodeParent != null && - (typeNodeParent.Kind() is SyntaxKind.DeclarationExpression or SyntaxKind.VariableDeclaration || - (typeNodeParent.IsKind(SyntaxKind.ForEachStatement) && !syntaxFacts.IsExpressionOfForeach(typeNode)))) - { - return typeNodeParent; - } - - return null; + return; } - private async Task UpdateDocumentAsync(Document document, TypeSyntax type, CancellationToken cancellationToken) + var simplifierOptions = (CSharpSimplifierOptions)await document.GetSimplifierOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); + var typeStyle = AnalyzeTypeName(declaredType, semanticModel, simplifierOptions, cancellationToken); + if (typeStyle.IsStylePreferred && typeStyle.Notification.Severity != ReportDiagnostic.Suppress) { - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var editor = new SyntaxEditor(root, document.Project.Solution.Services); + // the analyzer would handle this. So we do not. + return; + } + + if (!typeStyle.CanConvert()) + { + return; + } - await HandleDeclarationAsync(document, editor, type, cancellationToken).ConfigureAwait(false); + context.RegisterRefactoring( + CodeAction.Create( + Title, + c => UpdateDocumentAsync(document, declaredType, c), + Title), + declaredType.Span); + } - var newRoot = editor.GetChangedRoot(); - return document.WithSyntaxRoot(newRoot); + private static async Task GetDeclarationAsync(CodeRefactoringContext context) + { + // We want to provide refactoring for changing the Type of newly introduced variables in following cases: + // - DeclarationExpressionSyntax: `"42".TryParseInt32(out var number)` + // - VariableDeclarationSyntax: General field / variable declaration statement `var number = 42` + // - ForEachStatementSyntax: The variable that gets introduced by foreach `foreach(var number in numbers)` + // + // In addition to providing the refactoring when the whole node (i.e. the node that introduces the new variable) in question is selected + // we also want to enable it when only the type node is selected because this refactoring changes the type. We still have to make sure + // we're only working on TypeNodes for in above-mentioned situations. + // + // For foreach we need to guard against selecting just the expression because it is also of type `TypeSyntax`. + + var declNode = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (declNode != null) + return declNode; + + var variableNode = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (variableNode != null) + return variableNode; + + // `ref var` is a bit of an interesting construct. 'ref' looks like a modifier, but is actually a + // type-syntax. Ensure the user can get the feature anywhere on this construct + var type = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (type?.Parent is RefTypeSyntax) + type = (TypeSyntax)type.Parent; + + if (type?.Parent is VariableDeclarationSyntax) + return type.Parent; + + var foreachStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (foreachStatement != null) + return foreachStatement; + + var syntaxFacts = context.Document.GetLanguageService(); + + var typeNode = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + var typeNodeParent = typeNode?.Parent; + if (typeNodeParent != null && + (typeNodeParent.Kind() is SyntaxKind.DeclarationExpression or SyntaxKind.VariableDeclaration || + (typeNodeParent.IsKind(SyntaxKind.ForEachStatement) && !syntaxFacts.IsExpressionOfForeach(typeNode)))) + { + return typeNodeParent; } + + return null; + } + + private async Task UpdateDocumentAsync(Document document, TypeSyntax type, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var editor = new SyntaxEditor(root, document.Project.Solution.Services); + + await HandleDeclarationAsync(document, editor, type, cancellationToken).ConfigureAwait(false); + + var newRoot = editor.GetChangedRoot(); + return document.WithSyntaxRoot(newRoot); } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/UseExplicitTypeCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/UseExplicitTypeCodeRefactoringProvider.cs index f093ba1f6af8f..0d7d13b86098e 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/UseExplicitTypeCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/UseExplicitTypeCodeRefactoringProvider.cs @@ -17,27 +17,26 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.UseExplicitType +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.UseExplicitType; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseExplicitType), Shared] +internal class UseExplicitTypeCodeRefactoringProvider : AbstractUseTypeCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseExplicitType), Shared] - internal class UseExplicitTypeCodeRefactoringProvider : AbstractUseTypeCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public UseExplicitTypeCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public UseExplicitTypeCodeRefactoringProvider() - { - } + } - protected override string Title - => CSharpAnalyzersResources.Use_explicit_type; + protected override string Title + => CSharpAnalyzersResources.Use_explicit_type; - protected override TypeSyntax FindAnalyzableType(SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken) - => CSharpUseExplicitTypeHelper.Instance.FindAnalyzableType(node, semanticModel, cancellationToken); + protected override TypeSyntax FindAnalyzableType(SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken) + => CSharpUseExplicitTypeHelper.Instance.FindAnalyzableType(node, semanticModel, cancellationToken); - protected override TypeStyleResult AnalyzeTypeName(TypeSyntax typeName, SemanticModel semanticModel, CSharpSimplifierOptions options, CancellationToken cancellationToken) - => CSharpUseExplicitTypeHelper.Instance.AnalyzeTypeName(typeName, semanticModel, options, cancellationToken); + protected override TypeStyleResult AnalyzeTypeName(TypeSyntax typeName, SemanticModel semanticModel, CSharpSimplifierOptions options, CancellationToken cancellationToken) + => CSharpUseExplicitTypeHelper.Instance.AnalyzeTypeName(typeName, semanticModel, options, cancellationToken); - protected override Task HandleDeclarationAsync(Document document, SyntaxEditor editor, TypeSyntax node, CancellationToken cancellationToken) - => UseExplicitTypeCodeFixProvider.HandleDeclarationAsync(document, editor, node, cancellationToken); - } + protected override Task HandleDeclarationAsync(Document document, SyntaxEditor editor, TypeSyntax node, CancellationToken cancellationToken) + => UseExplicitTypeCodeFixProvider.HandleDeclarationAsync(document, editor, node, cancellationToken); } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/UseImplicitTypeCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/UseImplicitTypeCodeRefactoringProvider.cs index 17f922876f708..0c6197e5e3f3a 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/UseImplicitTypeCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/UseExplicitOrImplicitType/UseImplicitTypeCodeRefactoringProvider.cs @@ -16,30 +16,29 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Editing; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.UseImplicitType +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.UseImplicitType; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseImplicitType), Shared] +internal partial class UseImplicitTypeCodeRefactoringProvider : AbstractUseTypeCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseImplicitType), Shared] - internal partial class UseImplicitTypeCodeRefactoringProvider : AbstractUseTypeCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public UseImplicitTypeCodeRefactoringProvider() + { + } + + protected override string Title + => CSharpAnalyzersResources.Use_implicit_type; + + protected override TypeSyntax FindAnalyzableType(SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken) + => CSharpUseImplicitTypeHelper.Instance.FindAnalyzableType(node, semanticModel, cancellationToken); + + protected override TypeStyleResult AnalyzeTypeName(TypeSyntax typeName, SemanticModel semanticModel, CSharpSimplifierOptions options, CancellationToken cancellationToken) + => CSharpUseImplicitTypeHelper.Instance.AnalyzeTypeName(typeName, semanticModel, options, cancellationToken); + + protected override Task HandleDeclarationAsync(Document document, SyntaxEditor editor, TypeSyntax type, CancellationToken cancellationToken) { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public UseImplicitTypeCodeRefactoringProvider() - { - } - - protected override string Title - => CSharpAnalyzersResources.Use_implicit_type; - - protected override TypeSyntax FindAnalyzableType(SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken) - => CSharpUseImplicitTypeHelper.Instance.FindAnalyzableType(node, semanticModel, cancellationToken); - - protected override TypeStyleResult AnalyzeTypeName(TypeSyntax typeName, SemanticModel semanticModel, CSharpSimplifierOptions options, CancellationToken cancellationToken) - => CSharpUseImplicitTypeHelper.Instance.AnalyzeTypeName(typeName, semanticModel, options, cancellationToken); - - protected override Task HandleDeclarationAsync(Document document, SyntaxEditor editor, TypeSyntax type, CancellationToken cancellationToken) - { - UseImplicitTypeCodeFixProvider.ReplaceTypeWithVar(editor, type); - return Task.CompletedTask; - } + UseImplicitTypeCodeFixProvider.ReplaceTypeWithVar(editor, type); + return Task.CompletedTask; } } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/UseRecursivePatterns/UseRecursivePatternsCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/UseRecursivePatterns/UseRecursivePatternsCodeRefactoringProvider.cs index f252ea0bb4efa..85290c7f16274 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/UseRecursivePatterns/UseRecursivePatternsCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/UseRecursivePatterns/UseRecursivePatternsCodeRefactoringProvider.cs @@ -25,541 +25,601 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.UseRecursivePatterns +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.UseRecursivePatterns; + +using static SyntaxFactory; +using static SyntaxKind; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseRecursivePatterns), Shared] +internal sealed class UseRecursivePatternsCodeRefactoringProvider : SyntaxEditorBasedCodeRefactoringProvider { - using static SyntaxFactory; - using static SyntaxKind; + private static readonly PatternSyntax s_trueConstantPattern = ConstantPattern(LiteralExpression(TrueLiteralExpression)); + private static readonly PatternSyntax s_falseConstantPattern = ConstantPattern(LiteralExpression(FalseLiteralExpression)); - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseRecursivePatterns), Shared] - internal sealed class UseRecursivePatternsCodeRefactoringProvider : SyntaxEditorBasedCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public UseRecursivePatternsCodeRefactoringProvider() { - private static readonly PatternSyntax s_trueConstantPattern = ConstantPattern(LiteralExpression(TrueLiteralExpression)); - private static readonly PatternSyntax s_falseConstantPattern = ConstantPattern(LiteralExpression(FalseLiteralExpression)); + } - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public UseRecursivePatternsCodeRefactoringProvider() - { - } + protected override ImmutableArray SupportedFixAllScopes => AllFixAllScopes; - protected override ImmutableArray SupportedFixAllScopes => AllFixAllScopes; + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, textSpan, cancellationToken) = context; + if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) + return; + + if (textSpan.Length > 0) + return; + + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root.SyntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp9) + return; + + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var node = root.FindToken(textSpan.Start).Parent; + var replacementFunc = GetReplacementFunc(node, model); + if (replacementFunc is null) + return; + + context.RegisterRefactoring( + CodeAction.Create( + CSharpFeaturesResources.Use_recursive_patterns, + _ => Task.FromResult(document.WithSyntaxRoot(replacementFunc(root))), + nameof(CSharpFeaturesResources.Use_recursive_patterns))); + } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + private static Func? GetReplacementFunc(SyntaxNode? node, SemanticModel model) + => node switch { - var (document, textSpan, cancellationToken) = context; - if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) - return; - - if (textSpan.Length > 0) - return; - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (root.SyntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp9) - return; - - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var node = root.FindToken(textSpan.Start).Parent; - var replacementFunc = GetReplacementFunc(node, model); - if (replacementFunc is null) - return; - - context.RegisterRefactoring( - CodeAction.Create( - CSharpFeaturesResources.Use_recursive_patterns, - _ => Task.FromResult(document.WithSyntaxRoot(replacementFunc(root))), - nameof(CSharpFeaturesResources.Use_recursive_patterns))); + BinaryExpressionSyntax(LogicalAndExpression) logicalAnd => CombineLogicalAndOperands(logicalAnd, model), + CasePatternSwitchLabelSyntax { WhenClause: { } whenClause } switchLabel => CombineWhenClauseCondition(switchLabel.Pattern, whenClause.Condition, model), + SwitchExpressionArmSyntax { WhenClause: { } whenClause } switchArm => CombineWhenClauseCondition(switchArm.Pattern, whenClause.Condition, model), + WhenClauseSyntax { Parent: CasePatternSwitchLabelSyntax switchLabel } whenClause => CombineWhenClauseCondition(switchLabel.Pattern, whenClause.Condition, model), + WhenClauseSyntax { Parent: SwitchExpressionArmSyntax switchArm } whenClause => CombineWhenClauseCondition(switchArm.Pattern, whenClause.Condition, model), + _ => null + }; + + private static bool IsFixableNode(SyntaxNode node) + => node switch + { + BinaryExpressionSyntax(LogicalAndExpression) => true, + CasePatternSwitchLabelSyntax { WhenClause: { } } => true, + SwitchExpressionArmSyntax { WhenClause: { } } => true, + WhenClauseSyntax { Parent: CasePatternSwitchLabelSyntax } => true, + WhenClauseSyntax { Parent: SwitchExpressionArmSyntax } => true, + _ => false + }; + + private static Func? CombineLogicalAndOperands(BinaryExpressionSyntax logicalAnd, SemanticModel model) + { + if (TryDetermineReceiver(logicalAnd.Left, model) is not var (leftReceiver, leftTarget, leftFlipped) || + TryDetermineReceiver(logicalAnd.Right, model) is not var (rightReceiver, rightTarget, rightFlipped)) + { + return null; } - private static Func? GetReplacementFunc(SyntaxNode? node, SemanticModel model) - => node switch - { - BinaryExpressionSyntax(LogicalAndExpression) logicalAnd => CombineLogicalAndOperands(logicalAnd, model), - CasePatternSwitchLabelSyntax { WhenClause: { } whenClause } switchLabel => CombineWhenClauseCondition(switchLabel.Pattern, whenClause.Condition, model), - SwitchExpressionArmSyntax { WhenClause: { } whenClause } switchArm => CombineWhenClauseCondition(switchArm.Pattern, whenClause.Condition, model), - WhenClauseSyntax { Parent: CasePatternSwitchLabelSyntax switchLabel } whenClause => CombineWhenClauseCondition(switchLabel.Pattern, whenClause.Condition, model), - WhenClauseSyntax { Parent: SwitchExpressionArmSyntax switchArm } whenClause => CombineWhenClauseCondition(switchArm.Pattern, whenClause.Condition, model), - _ => null - }; - - private static bool IsFixableNode(SyntaxNode node) - => node switch + // If we have an is-expression on the left, first we check if there is a variable designation that's been used on the right-hand-side, + // in which case, we'll convert and move the check inside the existing pattern, if possible. + // For instance, `e is C c && c.p == 0` is converted to `e is C { p: 0 } c` + if (leftTarget.Parent is IsPatternExpressionSyntax isPatternExpression && + TryFindVariableDesignation(isPatternExpression.Pattern, rightReceiver, model) is var (containingPattern, rightNamesOpt)) + { + Debug.Assert(leftTarget == isPatternExpression.Pattern); + Debug.Assert(leftReceiver == isPatternExpression.Expression); + return root => { - BinaryExpressionSyntax(LogicalAndExpression) => true, - CasePatternSwitchLabelSyntax { WhenClause: { } } => true, - SwitchExpressionArmSyntax { WhenClause: { } } => true, - WhenClauseSyntax { Parent: CasePatternSwitchLabelSyntax } => true, - WhenClauseSyntax { Parent: SwitchExpressionArmSyntax } => true, - _ => false + var rightPattern = CreatePattern(rightReceiver, rightTarget, rightFlipped); + var rewrittenPattern = RewriteContainingPattern(containingPattern, rightPattern, rightNamesOpt); + var replacement = isPatternExpression.ReplaceNode(containingPattern, rewrittenPattern); + return root.ReplaceNode(logicalAnd, AdjustBinaryExpressionOperands(logicalAnd, replacement)); }; + } - private static Func? CombineLogicalAndOperands(BinaryExpressionSyntax logicalAnd, SemanticModel model) + if (TryGetCommonReceiver(leftReceiver, rightReceiver, leftTarget, rightTarget, model) is var (commonReceiver, leftNames, rightNames)) { - if (TryDetermineReceiver(logicalAnd.Left, model) is not var (leftReceiver, leftTarget, leftFlipped) || - TryDetermineReceiver(logicalAnd.Right, model) is not var (rightReceiver, rightTarget, rightFlipped)) - { - return null; - } - - // If we have an is-expression on the left, first we check if there is a variable designation that's been used on the right-hand-side, - // in which case, we'll convert and move the check inside the existing pattern, if possible. - // For instance, `e is C c && c.p == 0` is converted to `e is C { p: 0 } c` - if (leftTarget.Parent is IsPatternExpressionSyntax isPatternExpression && - TryFindVariableDesignation(isPatternExpression.Pattern, rightReceiver, model) is var (containingPattern, rightNamesOpt)) + return root => { - Debug.Assert(leftTarget == isPatternExpression.Pattern); - Debug.Assert(leftReceiver == isPatternExpression.Expression); - return root => + // It's possible we decided to discard a pattern due to it being redundant (such as a null check + // combined with a property check belonging to the same field we confirmed not being null). + // For instance 'cf != null && cf.C != 0', the left null check doesn't add more information than the + // right expression because the is pattern `cf is { C: not 0 }` already checks for null implicitly + if (leftNames.Length == 0) { - var rightPattern = CreatePattern(rightReceiver, rightTarget, rightFlipped); - var rewrittenPattern = RewriteContainingPattern(containingPattern, rightPattern, rightNamesOpt); - var replacement = isPatternExpression.ReplaceNode(containingPattern, rewrittenPattern); + var rightSubpattern = CreateSubpattern(rightNames, CreatePattern(rightReceiver, rightTarget, rightFlipped)); + var replacement = IsPatternExpression(commonReceiver, RecursivePattern(rightSubpattern)); return root.ReplaceNode(logicalAnd, AdjustBinaryExpressionOperands(logicalAnd, replacement)); - }; - } - - if (TryGetCommonReceiver(leftReceiver, rightReceiver, model) is var (commonReceiver, leftNames, rightNames)) - { - return root => + } + else if (rightNames.Length == 0) + { + var leftSubpattern = CreateSubpattern(leftNames, CreatePattern(leftReceiver, leftTarget, leftFlipped)); + var replacement = IsPatternExpression(commonReceiver, RecursivePattern(leftSubpattern)); + return root.ReplaceNode(logicalAnd, AdjustBinaryExpressionOperands(logicalAnd, replacement)); + } + else { var leftSubpattern = CreateSubpattern(leftNames, CreatePattern(leftReceiver, leftTarget, leftFlipped)); var rightSubpattern = CreateSubpattern(rightNames, CreatePattern(rightReceiver, rightTarget, rightFlipped)); var replacement = IsPatternExpression(commonReceiver, RecursivePattern(leftSubpattern, rightSubpattern)); return root.ReplaceNode(logicalAnd, AdjustBinaryExpressionOperands(logicalAnd, replacement)); - }; - } + } + }; + } - return null; + return null; - static SyntaxNode AdjustBinaryExpressionOperands(BinaryExpressionSyntax logicalAnd, ExpressionSyntax replacement) - { - // If there's a `&&` on the left, we have picked the right-hand-side for the combination. - // In which case, we should replace that instead of the whole `&&` operator in a chain. - // For instance, `expr && a.b == 1 && a.c == 2` is converted to `expr && a is { b: 1, c: 2 }` - if (logicalAnd.Left is BinaryExpressionSyntax(LogicalAndExpression) leftExpression) - replacement = leftExpression.WithRight(replacement); - return replacement.ConvertToSingleLine().WithAdditionalAnnotations(Formatter.Annotation); - } + static SyntaxNode AdjustBinaryExpressionOperands(BinaryExpressionSyntax logicalAnd, ExpressionSyntax replacement) + { + // If there's a `&&` on the left, we have picked the right-hand-side for the combination. + // In which case, we should replace that instead of the whole `&&` operator in a chain. + // For instance, `expr && a.b == 1 && a.c == 2` is converted to `expr && a is { b: 1, c: 2 }` + if (logicalAnd.Left is BinaryExpressionSyntax(LogicalAndExpression) leftExpression) + replacement = leftExpression.WithRight(replacement); + return replacement.ConvertToSingleLine().WithAdditionalAnnotations(Formatter.Annotation); } + } - private static Func? CombineWhenClauseCondition(PatternSyntax switchPattern, ExpressionSyntax condition, SemanticModel model) + private static Func? CombineWhenClauseCondition(PatternSyntax switchPattern, ExpressionSyntax condition, SemanticModel model) + { + if (TryDetermineReceiver(condition, model, inWhenClause: true) is not var (receiver, target, flipped) || + TryFindVariableDesignation(switchPattern, receiver, model) is not var (containingPattern, namesOpt)) { - if (TryDetermineReceiver(condition, model, inWhenClause: true) is not var (receiver, target, flipped) || - TryFindVariableDesignation(switchPattern, receiver, model) is not var (containingPattern, namesOpt)) - { - return null; - } - - return root => - { - var editor = new SyntaxEditor(root, CSharpSyntaxGenerator.Instance); - switch (receiver.GetRequiredParent().Parent) - { - // This is the leftmost `&&` operand in a when-clause. Remove the left-hand-side which we've just morphed in the switch pattern. - // For instance, `case { p: var v } when v.q == 1 && expr:` would be converted to `case { p: { q: 1 } } v when expr:` - case BinaryExpressionSyntax(LogicalAndExpression) logicalAnd: - editor.ReplaceNode(logicalAnd, logicalAnd.Right); - break; - // If we reach here, there's no other expression left in the when-clause. Remove. - // For instance, `case { p: var v } when v.q == 1:` would be converted to `case { p: { q: 1 } v }:` - case WhenClauseSyntax whenClause: - editor.RemoveNode(whenClause, SyntaxRemoveOptions.AddElasticMarker); - break; - case var v: - throw ExceptionUtilities.UnexpectedValue(v); - } - - var generatedPattern = CreatePattern(receiver, target, flipped); - var rewrittenPattern = RewriteContainingPattern(containingPattern, generatedPattern, namesOpt); - editor.ReplaceNode(containingPattern, rewrittenPattern); - return editor.GetChangedRoot(); - }; + return null; } - private static PatternSyntax RewriteContainingPattern( - PatternSyntax containingPattern, - PatternSyntax generatedPattern, - ImmutableArray namesOpt) + return root => { - // This is a variable designation match. We'll try to combine the generated - // pattern from the right-hand-side into the containing pattern of this designation. - var rewrittenPattern = namesOpt.IsDefault - // If there's no name, we will combine the pattern itself. - ? Combine(containingPattern, generatedPattern) - // Otherwise, we generate a subpattern per each name and rewrite as a recursive pattern. - : AddSubpattern(containingPattern, CreateSubpattern(namesOpt, generatedPattern)); - - return rewrittenPattern.ConvertToSingleLine().WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation); - - static PatternSyntax Combine(PatternSyntax containingPattern, PatternSyntax generatedPattern) + var editor = new SyntaxEditor(root, CSharpSyntaxGenerator.Instance); + switch (receiver.GetRequiredParent().Parent) { - // We know we have a var-pattern, declaration-pattern or a recursive-pattern on the left as the containing node of the variable designation. - // Depending on the generated pattern off of the expression on the right, we can give a better result by morphing it into the existing match. - return (containingPattern, generatedPattern) switch - { - // e.g. `e is var x && x is { p: 1 }` => `e is { p: 1 } x` - (VarPatternSyntax var, RecursivePatternSyntax { Designation: null } recursive) - => recursive.WithDesignation(var.Designation), - - // e.g. `e is C x && x is { p: 1 }` => `is C { p: 1 } x` - (DeclarationPatternSyntax decl, RecursivePatternSyntax { Type: null, Designation: null } recursive) - => recursive.WithType(decl.Type).WithDesignation(decl.Designation), - - // e.g. `e is { p: 1 } x && x is C` => `is C { p: 1 } x` - (RecursivePatternSyntax { Type: null } recursive, TypePatternSyntax type) - => recursive.WithType(type.Type), - - // e.g. `e is { p: 1 } x && x is { q: 2 }` => `e is { p: 1, q: 2 } x` - (RecursivePatternSyntax recursive, RecursivePatternSyntax { Type: null, Designation: null } other) - when recursive.PositionalPatternClause is null || other.PositionalPatternClause is null - => recursive - .WithPositionalPatternClause(recursive.PositionalPatternClause ?? other.PositionalPatternClause) - .WithPropertyPatternClause(Concat(recursive.PropertyPatternClause, other.PropertyPatternClause)), - - // In any other case, we fallback to an `and` pattern. - // UNDONE: This may result in a few unused variables which should be removed in a later pass. - _ => BinaryPattern(AndPattern, containingPattern.Parenthesize(), generatedPattern.Parenthesize()), - }; + // This is the leftmost `&&` operand in a when-clause. Remove the left-hand-side which we've just morphed in the switch pattern. + // For instance, `case { p: var v } when v.q == 1 && expr:` would be converted to `case { p: { q: 1 } } v when expr:` + case BinaryExpressionSyntax(LogicalAndExpression) logicalAnd: + editor.ReplaceNode(logicalAnd, logicalAnd.Right); + break; + // If we reach here, there's no other expression left in the when-clause. Remove. + // For instance, `case { p: var v } when v.q == 1:` would be converted to `case { p: { q: 1 } v }:` + case WhenClauseSyntax whenClause: + editor.RemoveNode(whenClause, SyntaxRemoveOptions.AddElasticMarker); + break; + case var v: + throw ExceptionUtilities.UnexpectedValue(v); } - static PatternSyntax AddSubpattern(PatternSyntax containingPattern, SubpatternSyntax subpattern) - { - return containingPattern switch - { - // e.g. `case var x when x.p is 1` => `case { p: 1 } x` - VarPatternSyntax p => RecursivePattern(type: null, subpattern, p.Designation), - - // e.g. `case Type x when x.p is 1` => `case Type { p: 1 } x` - DeclarationPatternSyntax p => RecursivePattern(p.Type, subpattern, p.Designation), + var generatedPattern = CreatePattern(receiver, target, flipped); + var rewrittenPattern = RewriteContainingPattern(containingPattern, generatedPattern, namesOpt); + editor.ReplaceNode(containingPattern, rewrittenPattern); + return editor.GetChangedRoot(); + }; + } - // e.g. `case { p: 1 } x when x.q is 2` => `case { p: 1, q: 2 } x` - RecursivePatternSyntax p => p.AddPropertyPatternClauseSubpatterns(subpattern), + private static PatternSyntax RewriteContainingPattern( + PatternSyntax containingPattern, + PatternSyntax generatedPattern, + ImmutableArray namesOpt) + { + // This is a variable designation match. We'll try to combine the generated + // pattern from the right-hand-side into the containing pattern of this designation. + var rewrittenPattern = namesOpt.IsDefault + // If there's no name, we will combine the pattern itself. + ? Combine(containingPattern, generatedPattern) + // Otherwise, we generate a subpattern per each name and rewrite as a recursive pattern. + : AddSubpattern(containingPattern, CreateSubpattern(namesOpt, generatedPattern)); - // We've already checked that the designation is contained in any of the above pattern forms. - var p => throw ExceptionUtilities.UnexpectedValue(p) - }; - } + return rewrittenPattern.ConvertToSingleLine().WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation); - static PropertyPatternClauseSyntax? Concat(PropertyPatternClauseSyntax? left, PropertyPatternClauseSyntax? right) + static PatternSyntax Combine(PatternSyntax containingPattern, PatternSyntax generatedPattern) + { + // We know we have a var-pattern, declaration-pattern or a recursive-pattern on the left as the containing node of the variable designation. + // Depending on the generated pattern off of the expression on the right, we can give a better result by morphing it into the existing match. + return (containingPattern, generatedPattern) switch { - if (left is null || right is null) - return left ?? right; - return left.WithSubpatterns(left.Subpatterns.AddRange(right.Subpatterns)); - } + // e.g. `e is var x && x is { p: 1 }` => `e is { p: 1 } x` + (VarPatternSyntax var, RecursivePatternSyntax { Designation: null } recursive) + => recursive.WithDesignation(var.Designation), + + // e.g. `e is C x && x is { p: 1 }` => `is C { p: 1 } x` + (DeclarationPatternSyntax decl, RecursivePatternSyntax { Type: null, Designation: null } recursive) + => recursive.WithType(decl.Type).WithDesignation(decl.Designation), + + // e.g. `e is { p: 1 } x && x is C` => `is C { p: 1 } x` + (RecursivePatternSyntax { Type: null } recursive, TypePatternSyntax type) + => recursive.WithType(type.Type), + + // e.g. `e is { p: 1 } x && x is { q: 2 }` => `e is { p: 1, q: 2 } x` + (RecursivePatternSyntax recursive, RecursivePatternSyntax { Type: null, Designation: null } other) + when recursive.PositionalPatternClause is null || other.PositionalPatternClause is null + => recursive + .WithPositionalPatternClause(recursive.PositionalPatternClause ?? other.PositionalPatternClause) + .WithPropertyPatternClause(Concat(recursive.PropertyPatternClause, other.PropertyPatternClause)), + + // In any other case, we fallback to an `and` pattern. + // UNDONE: This may result in a few unused variables which should be removed in a later pass. + _ => BinaryPattern(AndPattern, containingPattern.Parenthesize(), generatedPattern.Parenthesize()), + }; } - private static PatternSyntax CreatePattern(ExpressionSyntax originalReceiver, ExpressionOrPatternSyntax target, bool flipped) + static PatternSyntax AddSubpattern(PatternSyntax containingPattern, SubpatternSyntax subpattern) { - return target switch + return containingPattern switch { - // A pattern come from an `is` expression on either side of `&&` - PatternSyntax pattern => pattern, - TypeSyntax type when originalReceiver.IsParentKind(IsExpression) => TypePattern(type), - // Otherwise, this is a constant. Depending on the original receiver, we create an appropriate pattern. - ExpressionSyntax constant => originalReceiver.Parent switch - { - BinaryExpressionSyntax(EqualsExpression) => ConstantPattern(constant), - BinaryExpressionSyntax(NotEqualsExpression) => UnaryPattern(ConstantPattern(constant)), - BinaryExpressionSyntax(GreaterThanExpression or - GreaterThanOrEqualExpression or - LessThanOrEqualExpression or - LessThanExpression) e - => RelationalPattern(flipped ? Flip(e.OperatorToken) : e.OperatorToken, constant), - var v => throw ExceptionUtilities.UnexpectedValue(v), - }, - var v => throw ExceptionUtilities.UnexpectedValue(v), - }; + // e.g. `case var x when x.p is 1` => `case { p: 1 } x` + VarPatternSyntax p => RecursivePattern(type: null, subpattern, p.Designation), - static SyntaxToken Flip(SyntaxToken token) - { - return Token(token.Kind() switch - { - LessThanToken => GreaterThanToken, - LessThanEqualsToken => GreaterThanEqualsToken, - GreaterThanEqualsToken => LessThanEqualsToken, - GreaterThanToken => LessThanToken, - var v => throw ExceptionUtilities.UnexpectedValue(v) - }); - } + // e.g. `case Type x when x.p is 1` => `case Type { p: 1 } x` + DeclarationPatternSyntax p => RecursivePattern(p.Type, subpattern, p.Designation), + + // e.g. `case { p: 1 } x when x.q is 2` => `case { p: 1, q: 2 } x` + RecursivePatternSyntax p => p.AddPropertyPatternClauseSubpatterns(subpattern), + + // We've already checked that the designation is contained in any of the above pattern forms. + var p => throw ExceptionUtilities.UnexpectedValue(p) + }; } - private static (PatternSyntax ContainingPattern, ImmutableArray NamesOpt)? TryFindVariableDesignation( - PatternSyntax leftPattern, - ExpressionSyntax rightReceiver, - SemanticModel model) + static PropertyPatternClauseSyntax? Concat(PropertyPatternClauseSyntax? left, PropertyPatternClauseSyntax? right) { - using var _ = ArrayBuilder.GetInstance(out var names); - if (GetInnermostReceiver(rightReceiver, names, model) is not IdentifierNameSyntax identifierName) - return null; - - var designation = leftPattern.DescendantNodes() - .OfType() - .Where(d => d.Identifier.ValueText == identifierName.Identifier.ValueText) - .FirstOrDefault(); - - // Excluding list patterns because those cannot be combined with a recursive pattern. - if (designation is not { Parent: PatternSyntax(not SyntaxKind.ListPattern) containingPattern }) - return null; - - // Only the following patterns can directly contain a variable designation. - // Note: While a parenthesized designation can also contain other variables, - // it is not a pattern, so it would not get past the PatternSyntax test above. - Debug.Assert(containingPattern.Kind() is SyntaxKind.VarPattern or SyntaxKind.DeclarationPattern or SyntaxKind.RecursivePattern); - return (containingPattern, names.ToImmutableOrNull()); + if (left is null || right is null) + return left ?? right; + return left.WithSubpatterns(left.Subpatterns.AddRange(right.Subpatterns)); } + } - private static (ExpressionSyntax Receiver, ExpressionOrPatternSyntax Target, bool Flipped)? TryDetermineReceiver( - ExpressionSyntax node, - SemanticModel model, - bool inWhenClause = false) + private static PatternSyntax CreatePattern(ExpressionSyntax originalReceiver, ExpressionOrPatternSyntax target, bool flipped) + { + return target switch { - return node switch + // A pattern come from an `is` expression on either side of `&&` + PatternSyntax pattern => pattern, + TypeSyntax type when originalReceiver.IsParentKind(IsExpression) => TypePattern(type), + // Otherwise, this is a constant. Depending on the original receiver, we create an appropriate pattern. + ExpressionSyntax constant => originalReceiver.Parent switch { - // For comparison operators, after we have determined the - // constant operand, we rewrite it as a constant or relational pattern. - BinaryExpressionSyntax(EqualsExpression or - NotEqualsExpression or - GreaterThanExpression or + BinaryExpressionSyntax(EqualsExpression) => ConstantPattern(constant), + BinaryExpressionSyntax(NotEqualsExpression) => UnaryPattern(ConstantPattern(constant)), + BinaryExpressionSyntax(GreaterThanExpression or GreaterThanOrEqualExpression or LessThanOrEqualExpression or - LessThanExpression) expr - => TryDetermineConstant(expr, model), - - // If we found a `&&` here, there's two possibilities: - // - // 1) If we're in a when-clause, we look for the leftmost expression - // which we will try to combine with the switch arm/label pattern. - // For instance, we return `a` if we have `case when a && b && c`. - // - // 2) Otherwise, we will return the operand that *appears* to be on the left in the source. - // For instance, we return `a` if we have `x && a && b` with the cursor on the second operator. - // Since `&&` is left-associative, it's guaranteed to be the expression that we want. - // For simplicity, we won't descend into any parenthesized expression here. - // - BinaryExpressionSyntax(LogicalAndExpression) expr => TryDetermineReceiver(inWhenClause ? expr.Left : expr.Right, model, inWhenClause), - - // If we have an `is` operator, we'll try to combine the existing pattern/type with the other operand. - BinaryExpressionSyntax(IsExpression) { Right: NullableTypeSyntax type } expr => (expr.Left, type.ElementType, Flipped: false), - BinaryExpressionSyntax(IsExpression) { Right: TypeSyntax type } expr => (expr.Left, type, Flipped: false), - IsPatternExpressionSyntax expr => (expr.Expression, expr.Pattern, Flipped: false), - - // We treat any other expression as if they were compared to true/false. - // For instance, `a.b && !a.c` will be converted to `a is { b: true, c: false }` - PrefixUnaryExpressionSyntax(LogicalNotExpression) expr => (expr.Operand, s_falseConstantPattern, Flipped: false), - var expr => (expr, s_trueConstantPattern, Flipped: false), - }; + LessThanExpression) e + => RelationalPattern(flipped ? Flip(e.OperatorToken) : e.OperatorToken, constant), + var v => throw ExceptionUtilities.UnexpectedValue(v), + }, + var v => throw ExceptionUtilities.UnexpectedValue(v), + }; - static (ExpressionSyntax Expression, ExpressionSyntax Constant, bool Flipped)? TryDetermineConstant(BinaryExpressionSyntax node, SemanticModel model) + static SyntaxToken Flip(SyntaxToken token) + { + return Token(token.Kind() switch { - return (node.Left, node.Right) switch - { - var (left, right) when model.GetConstantValue(left).HasValue => (right, left, Flipped: true), - var (left, right) when model.GetConstantValue(right).HasValue => (left, right, Flipped: false), - _ => null - }; - } + LessThanToken => GreaterThanToken, + LessThanEqualsToken => GreaterThanEqualsToken, + GreaterThanEqualsToken => LessThanEqualsToken, + GreaterThanToken => LessThanToken, + var v => throw ExceptionUtilities.UnexpectedValue(v) + }); } + } - private static SubpatternSyntax CreateSubpattern(ImmutableArray names, PatternSyntax pattern) - { - Debug.Assert(!names.IsDefaultOrEmpty); + private static (PatternSyntax ContainingPattern, ImmutableArray NamesOpt)? TryFindVariableDesignation( + PatternSyntax leftPattern, + ExpressionSyntax rightReceiver, + SemanticModel model) + { + using var _ = ArrayBuilder.GetInstance(out var names); + if (GetInnermostReceiver(rightReceiver, names, model) is not IdentifierNameSyntax identifierName) + return null; - if (names.Length > 1 && names[0].SyntaxTree.Options.LanguageVersion() >= LanguageVersion.CSharp10) - { - ExpressionSyntax expression = names[^1]; - for (var i = names.Length - 2; i >= 0; i--) - expression = MemberAccessExpression(SimpleMemberAccessExpression, expression, names[i]); - return SyntaxFactory.Subpattern(ExpressionColon(expression, Token(ColonToken)), pattern); - } - else + var designation = leftPattern.DescendantNodes() + .OfType() + .Where(d => d.Identifier.ValueText == identifierName.Identifier.ValueText) + .FirstOrDefault(); + + // Excluding list patterns because those cannot be combined with a recursive pattern. + if (designation is not { Parent: PatternSyntax(not SyntaxKind.ListPattern) containingPattern }) + return null; + + // Only the following patterns can directly contain a variable designation. + // Note: While a parenthesized designation can also contain other variables, + // it is not a pattern, so it would not get past the PatternSyntax test above. + Debug.Assert(containingPattern.Kind() is SyntaxKind.VarPattern or SyntaxKind.DeclarationPattern or SyntaxKind.RecursivePattern); + return (containingPattern, names.ToImmutableOrNull()); + } + + private static (ExpressionSyntax Receiver, ExpressionOrPatternSyntax Target, bool Flipped)? TryDetermineReceiver( + ExpressionSyntax node, + SemanticModel model, + bool inWhenClause = false) + { + return node switch + { + // For comparison operators, after we have determined the + // constant operand, we rewrite it as a constant or relational pattern. + BinaryExpressionSyntax(EqualsExpression or + NotEqualsExpression or + GreaterThanExpression or + GreaterThanOrEqualExpression or + LessThanOrEqualExpression or + LessThanExpression) expr + => TryDetermineConstant(expr, model), + + // If we found a `&&` here, there's two possibilities: + // + // 1) If we're in a when-clause, we look for the leftmost expression + // which we will try to combine with the switch arm/label pattern. + // For instance, we return `a` if we have `case when a && b && c`. + // + // 2) Otherwise, we will return the operand that *appears* to be on the left in the source. + // For instance, we return `a` if we have `x && a && b` with the cursor on the second operator. + // Since `&&` is left-associative, it's guaranteed to be the expression that we want. + // For simplicity, we won't descend into any parenthesized expression here. + // + BinaryExpressionSyntax(LogicalAndExpression) expr => TryDetermineReceiver(inWhenClause ? expr.Left : expr.Right, model, inWhenClause), + + // If we have an `is` operator, we'll try to combine the existing pattern/type with the other operand. + BinaryExpressionSyntax(IsExpression) { Right: NullableTypeSyntax type } expr => (expr.Left, type.ElementType, Flipped: false), + BinaryExpressionSyntax(IsExpression) { Right: TypeSyntax type } expr => (expr.Left, type, Flipped: false), + IsPatternExpressionSyntax expr => (expr.Expression, expr.Pattern, Flipped: false), + + // We treat any other expression as if they were compared to true/false. + // For instance, `a.b && !a.c` will be converted to `a is { b: true, c: false }` + PrefixUnaryExpressionSyntax(LogicalNotExpression) expr => (expr.Operand, s_falseConstantPattern, Flipped: false), + var expr => (expr, s_trueConstantPattern, Flipped: false), + }; + + static (ExpressionSyntax Expression, ExpressionSyntax Constant, bool Flipped)? TryDetermineConstant(BinaryExpressionSyntax node, SemanticModel model) + { + return (node.Left, node.Right) switch { - var subpattern = Subpattern(names[0], pattern); - for (var i = 1; i < names.Length; i++) - subpattern = Subpattern(names[i], RecursivePattern(subpattern)); - return subpattern; - } + var (left, right) when model.GetConstantValue(left).HasValue => (right, left, Flipped: true), + var (left, right) when model.GetConstantValue(right).HasValue => (left, right, Flipped: false), + _ => null + }; } + } - private static SubpatternSyntax Subpattern(IdentifierNameSyntax name, PatternSyntax pattern) - => SyntaxFactory.Subpattern(NameColon(name), pattern); + private static SubpatternSyntax CreateSubpattern(ImmutableArray names, PatternSyntax pattern) + { + Debug.Assert(!names.IsDefaultOrEmpty); - private static RecursivePatternSyntax RecursivePattern(params SubpatternSyntax[] subpatterns) - => SyntaxFactory.RecursivePattern(type: null, positionalPatternClause: null, PropertyPatternClause([.. subpatterns]), designation: null); + if (names.Length > 1 && names[0].SyntaxTree.Options.LanguageVersion() >= LanguageVersion.CSharp10) + { + ExpressionSyntax expression = names[^1]; + for (var i = names.Length - 2; i >= 0; i--) + expression = MemberAccessExpression(SimpleMemberAccessExpression, expression, names[i]); + return SyntaxFactory.Subpattern(ExpressionColon(expression, Token(ColonToken)), pattern); + } + else + { + var subpattern = Subpattern(names[0], pattern); + for (var i = 1; i < names.Length; i++) + subpattern = Subpattern(names[i], RecursivePattern(subpattern)); + return subpattern; + } + } + + private static SubpatternSyntax Subpattern(IdentifierNameSyntax name, PatternSyntax pattern) + => SyntaxFactory.Subpattern(NameColon(name), pattern); + + private static RecursivePatternSyntax RecursivePattern(params SubpatternSyntax[] subpatterns) + => SyntaxFactory.RecursivePattern(type: null, positionalPatternClause: null, PropertyPatternClause([.. subpatterns]), designation: null); - private static RecursivePatternSyntax RecursivePattern(TypeSyntax? type, SubpatternSyntax subpattern, VariableDesignationSyntax? designation) - => SyntaxFactory.RecursivePattern(type, positionalPatternClause: null, PropertyPatternClause([subpattern]), designation); + private static RecursivePatternSyntax RecursivePattern(TypeSyntax? type, SubpatternSyntax subpattern, VariableDesignationSyntax? designation) + => SyntaxFactory.RecursivePattern(type, positionalPatternClause: null, PropertyPatternClause([subpattern]), designation); - private static RecursivePatternSyntax RecursivePattern(SubpatternSyntax subpattern) - => RecursivePattern(type: null, subpattern, designation: null); + private static RecursivePatternSyntax RecursivePattern(SubpatternSyntax subpattern) + => RecursivePattern(type: null, subpattern, designation: null); + + /// + /// Obtain the outermost common receiver between two expressions. This can succeed with a null 'CommonReceiver' + /// in the case that the common receiver is the 'implicit this'. + /// + private static (ExpressionSyntax CommonReceiver, ImmutableArray LeftNames, ImmutableArray RightNames)? TryGetCommonReceiver( + ExpressionSyntax left, + ExpressionSyntax right, + ExpressionOrPatternSyntax leftTarget, + ExpressionOrPatternSyntax rightTarget, + SemanticModel model) + { + using var _1 = ArrayBuilder.GetInstance(out var leftNames); + using var _2 = ArrayBuilder.GetInstance(out var rightNames); - /// - /// Obtain the outermost common receiver between two expressions. This can succeed with a null 'CommonReceiver' - /// in the case that the common receiver is the 'implicit this'. - /// - private static (ExpressionSyntax CommonReceiver, ImmutableArray LeftNames, ImmutableArray RightNames)? TryGetCommonReceiver( - ExpressionSyntax left, - ExpressionSyntax right, - SemanticModel model) + if (!TryGetInnermostReceiver(left, leftNames, out var leftReceiver, model) || + !TryGetInnermostReceiver(right, rightNames, out var rightReceiver, model) || + !AreEquivalent(leftReceiver, rightReceiver)) // We must have a common starting point to proceed. { - using var _1 = ArrayBuilder.GetInstance(out var leftNames); - using var _2 = ArrayBuilder.GetInstance(out var rightNames); + return null; + } - if (!TryGetInnermostReceiver(left, leftNames, out var leftReceiver, model) || - !TryGetInnermostReceiver(right, rightNames, out var rightReceiver, model) || - !AreEquivalent(leftReceiver, rightReceiver)) // We must have a common starting point to proceed. + var commonReceiver = leftReceiver; + + // To reduce noise on superfluous subpatterns and avoid duplicates, skip any common name in the path. + var lastName = SkipCommonNames(leftNames, rightNames); + if (lastName is not null) + { + // If there were some common names in the path, we rewrite the receiver to include those. + // For instance, in `a.b.c && a.b.d`, we have `b` as the last common name in the path, + // So we want `a.b` as the receiver so that we convert it to `a.b is { c: true, d: true }`. + commonReceiver = GetInnermostReceiver(left, lastName, static (identifierName, lastName) => identifierName != lastName); + } + + // If the common receiver is null, there might still be one in cases like these: + // `MyClassField != null && MyClassField.prop != 0`. In this case, the left expression doesn't say + // anything new to the second one so it should be discarded, but MyClassField should still act as the + // receiver instead of the implicit this so we get + // `MyClassField is { prop: not 0 }` instead of `this is { MyClassField: not null, MyClassField.prop: not 0 }` + // We need to cover this case for either side of the expression by detecting a null check on either side + if (AreEquivalent(leftNames[^1], rightNames[^1])) + { + var leftIsNullCheck = IsNullCheck(leftTarget.Parent); + var rightIsNullCheck = IsNullCheck(rightTarget.Parent); + + if (leftIsNullCheck) { - return null; + lastName = rightNames[^1]; + commonReceiver = GetInnermostReceiver(right, lastName, static (identifierName, lastName) => identifierName != lastName); + rightNames.Clip(rightNames.Count - 1); + return (commonReceiver ?? ThisExpression(), ImmutableArray.Empty, rightNames.ToImmutable()); } - var commonReceiver = leftReceiver; - - // To reduce noise on superfluous subpatterns and avoid duplicates, skip any common name in the path. - var lastName = SkipCommonNames(leftNames, rightNames); - if (lastName is not null) + if (rightIsNullCheck) { - // If there were some common names in the path, we rewrite the receiver to include those. - // For instance, in `a.b.c && a.b.d`, we have `b` as the last common name in the path, - // So we want `a.b` as the receiver so that we convert it to `a.b is { c: true, d: true }`. + lastName = leftNames[^1]; commonReceiver = GetInnermostReceiver(left, lastName, static (identifierName, lastName) => identifierName != lastName); + leftNames.Clip(leftNames.Count - 1); + return (commonReceiver ?? ThisExpression(), leftNames.ToImmutable(), ImmutableArray.Empty); } + } - // If the common receiver is null, it's an implicit `this` reference in source. - // For instance, `prop == 1 && field == 2` would be converted to `this is { prop: 1, field: 2 }` - return (commonReceiver ?? ThisExpression(), leftNames.ToImmutable(), rightNames.ToImmutable()); + // If the common receiver is null and we can't find a redundant pattern in the case above, + // it's an implicit `this` reference in source. + // For instance, `prop == 1 && field == 2` would be converted to `this is { prop: 1, field: 2 }` + return (commonReceiver ?? ThisExpression(), leftNames.ToImmutable(), rightNames.ToImmutable()); - static bool TryGetInnermostReceiver(ExpressionSyntax node, ArrayBuilder builder, [NotNullWhen(true)] out ExpressionSyntax? receiver, SemanticModel model) - { - receiver = GetInnermostReceiver(node, builder, model); - return builder.Any(); - } + static bool TryGetInnermostReceiver(ExpressionSyntax node, ArrayBuilder builder, [NotNullWhen(true)] out ExpressionSyntax? receiver, SemanticModel model) + { + receiver = GetInnermostReceiver(node, builder, model); + return builder.Any(); + } - static IdentifierNameSyntax? SkipCommonNames(ArrayBuilder leftNames, ArrayBuilder rightNames) + static IdentifierNameSyntax? SkipCommonNames(ArrayBuilder leftNames, ArrayBuilder rightNames) + { + IdentifierNameSyntax? lastName = null; + int leftIndex, rightIndex; + // Note: we don't want to skip the first name to still be able to convert to a subpattern, hence checking `> 0` below. + for (leftIndex = leftNames.Count - 1, rightIndex = rightNames.Count - 1; leftIndex > 0 && rightIndex > 0; leftIndex--, rightIndex--) { - IdentifierNameSyntax? lastName = null; - int leftIndex, rightIndex; - // Note: we don't want to skip the first name to still be able to convert to a subpattern, hence checking `> 0` below. - for (leftIndex = leftNames.Count - 1, rightIndex = rightNames.Count - 1; leftIndex > 0 && rightIndex > 0; leftIndex--, rightIndex--) - { - var leftName = leftNames[leftIndex]; - var rightName = rightNames[rightIndex]; - if (!AreEquivalent(leftName, rightName)) - break; - lastName = leftName; - } - - leftNames.Clip(leftIndex + 1); - rightNames.Clip(rightIndex + 1); - return lastName; + var leftName = leftNames[leftIndex]; + var rightName = rightNames[rightIndex]; + if (!AreEquivalent(leftName, rightName)) + break; + lastName = leftName; } + + leftNames.Clip(leftIndex + 1); + rightNames.Clip(rightIndex + 1); + return lastName; } - private static ExpressionSyntax? GetInnermostReceiver(ExpressionSyntax node, ArrayBuilder builder, SemanticModel model) + static bool IsNullCheck(SyntaxNode? exp) { - return GetInnermostReceiver(node, model, CanConvertToSubpattern, builder); - - static bool CanConvertToSubpattern(IdentifierNameSyntax name, SemanticModel model) + if (exp is BinaryExpressionSyntax(NotEqualsExpression) binaryExpression) { - return model.GetSymbolInfo(name).Symbol is - { - IsStatic: false, - Kind: SymbolKind.Property or SymbolKind.Field, - ContainingType: not { SpecialType: SpecialType.System_Nullable_T } - }; + if (binaryExpression.Left.Kind() == NullLiteralExpression || binaryExpression.Right.Kind() == NullLiteralExpression) + return true; } + + return false; } + } - private static ExpressionSyntax? GetInnermostReceiver( - ExpressionSyntax node, TArg arg, - Func canConvertToSubpattern, - ArrayBuilder? builder = null) + private static ExpressionSyntax? GetInnermostReceiver(ExpressionSyntax node, ArrayBuilder builder, SemanticModel model) + { + return GetInnermostReceiver(node, model, CanConvertToSubpattern, builder); + + static bool CanConvertToSubpattern(IdentifierNameSyntax name, SemanticModel model) { - return GetInnermostReceiver(node); + return model.GetSymbolInfo(name).Symbol is + { + IsStatic: false, + Kind: SymbolKind.Property or SymbolKind.Field, + ContainingType: not { SpecialType: SpecialType.System_Nullable_T } + }; + } + } + + private static ExpressionSyntax? GetInnermostReceiver( + ExpressionSyntax node, TArg arg, + Func canConvertToSubpattern, + ArrayBuilder? builder = null) + { + return GetInnermostReceiver(node); - ExpressionSyntax? GetInnermostReceiver(ExpressionSyntax node) + ExpressionSyntax? GetInnermostReceiver(ExpressionSyntax node) + { + switch (node) { - switch (node) - { - case IdentifierNameSyntax name - when canConvertToSubpattern(name, arg): - builder?.Add(name); - // This is a member reference with an implicit `this` receiver. - // We know this is true because we already checked canConvertToSubpattern. - // Any other name outside the receiver position is captured in the cases below. - return null; - - case MemberBindingExpressionSyntax { Name: IdentifierNameSyntax name } - when canConvertToSubpattern(name, arg): - builder?.Add(name); - // We only reach here from a parent conditional-access. - // Returning null here means that all the names on the right were convertible to a property pattern. - return null; - - case MemberAccessExpressionSyntax(SimpleMemberAccessExpression) { Name: IdentifierNameSyntax name } memberAccess - when canConvertToSubpattern(name, arg) && !memberAccess.Expression.IsKind(SyntaxKind.BaseExpression): - builder?.Add(name); - // For a simple member access we simply record the name and descend into the expression on the left-hand-side. - return GetInnermostReceiver(memberAccess.Expression); - - case ConditionalAccessExpressionSyntax conditionalAccess: - // For a conditional access, first we need to verify the right-hand-side is convertible to a property pattern. - var right = GetInnermostReceiver(conditionalAccess.WhenNotNull); - if (right is not null) - { - // If it has it's own receiver other than a member-binding expression, we return this node as the receiver. - // For instance, if we had `a?.M().b`, the name `b` is already captured, so we need to return `a?.M()` as the innermost receiver. - // If there was no name, this call returns itself, e.g. in `a?.M()` the receiver is the entire existing conditional access. - return conditionalAccess.WithWhenNotNull(right); - } - // Otherwise, descend into the the expression on the left-hand-side. - return GetInnermostReceiver(conditionalAccess.Expression); - - default: - return node; - } + case IdentifierNameSyntax name + when canConvertToSubpattern(name, arg): + builder?.Add(name); + // This is a member reference with an implicit `this` receiver. + // We know this is true because we already checked canConvertToSubpattern. + // Any other name outside the receiver position is captured in the cases below. + return null; + + case MemberBindingExpressionSyntax { Name: IdentifierNameSyntax name } + when canConvertToSubpattern(name, arg): + builder?.Add(name); + // We only reach here from a parent conditional-access. + // Returning null here means that all the names on the right were convertible to a property pattern. + return null; + + case MemberAccessExpressionSyntax(SimpleMemberAccessExpression) { Name: IdentifierNameSyntax name } memberAccess + when canConvertToSubpattern(name, arg) && !memberAccess.Expression.IsKind(SyntaxKind.BaseExpression): + builder?.Add(name); + // For a simple member access we simply record the name and descend into the expression on the left-hand-side. + return GetInnermostReceiver(memberAccess.Expression); + + case ConditionalAccessExpressionSyntax conditionalAccess: + // For a conditional access, first we need to verify the right-hand-side is convertible to a property pattern. + var right = GetInnermostReceiver(conditionalAccess.WhenNotNull); + if (right is not null) + { + // If it has it's own receiver other than a member-binding expression, we return this node as the receiver. + // For instance, if we had `a?.M().b`, the name `b` is already captured, so we need to return `a?.M()` as the innermost receiver. + // If there was no name, this call returns itself, e.g. in `a?.M()` the receiver is the entire existing conditional access. + return conditionalAccess.WithWhenNotNull(right); + } + // Otherwise, descend into the the expression on the left-hand-side. + return GetInnermostReceiver(conditionalAccess.Expression); + + default: + return node; } } + } - protected override async Task FixAllAsync( - Document document, - ImmutableArray fixAllSpans, - SyntaxEditor editor, - CodeActionOptionsProvider optionsProvider, - string? equivalenceKey, - CancellationToken cancellationToken) + protected override async Task FixAllAsync( + Document document, + ImmutableArray fixAllSpans, + SyntaxEditor editor, + CodeActionOptionsProvider optionsProvider, + string? equivalenceKey, + CancellationToken cancellationToken) + { + // Get all the descendant nodes to refactor. + // NOTE: We need to realize the nodes with 'ToArray' call here + // to ensure we strongly hold onto the nodes so that 'TrackNodes' + // invoked below, which does tracking based off a ConditionalWeakTable, + // tracks the nodes for the entire duration of this method. + var nodes = editor.OriginalRoot.DescendantNodes().Where(IsFixableNode).ToArray(); + + // We're going to be continually editing this tree. Track all the nodes we + // care about so we can find them across each edit. + document = document.WithSyntaxRoot(editor.OriginalRoot.TrackNodes(nodes)); + + // Process all nodes to refactor in reverse to ensure nested nodes + // are processed before the outer nodes to refactor. + foreach (var originalNode in nodes.Reverse()) { - // Get all the descendant nodes to refactor. - // NOTE: We need to realize the nodes with 'ToArray' call here - // to ensure we strongly hold onto the nodes so that 'TrackNodes' - // invoked below, which does tracking based off a ConditionalWeakTable, - // tracks the nodes for the entire duration of this method. - var nodes = editor.OriginalRoot.DescendantNodes().Where(IsFixableNode).ToArray(); - - // We're going to be continually editing this tree. Track all the nodes we - // care about so we can find them across each edit. - document = document.WithSyntaxRoot(editor.OriginalRoot.TrackNodes(nodes)); - - // Process all nodes to refactor in reverse to ensure nested nodes - // are processed before the outer nodes to refactor. - foreach (var originalNode in nodes.Reverse()) - { - // Only process nodes fully within a fixAllSpan - if (!fixAllSpans.Any(fixAllSpan => fixAllSpan.Contains(originalNode.Span))) - continue; + // Only process nodes fully within a fixAllSpan + if (!fixAllSpans.Any(fixAllSpan => fixAllSpan.Contains(originalNode.Span))) + continue; - // Get current root, current node to refactor and semantic model. - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var currentNode = root.GetCurrentNodes(originalNode).SingleOrDefault(); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var replacementFunc = GetReplacementFunc(currentNode, semanticModel); - if (replacementFunc == null) - continue; + // Get current root, current node to refactor and semantic model. + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var currentNode = root.GetCurrentNodes(originalNode).SingleOrDefault(); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - document = document.WithSyntaxRoot(replacementFunc(root)); - } + var replacementFunc = GetReplacementFunc(currentNode, semanticModel); + if (replacementFunc == null) + continue; - var updatedRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(editor.OriginalRoot, updatedRoot); + document = document.WithSyntaxRoot(replacementFunc(root)); } + + var updatedRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(editor.OriginalRoot, updatedRoot); } } diff --git a/src/Features/CSharp/Portable/CommentSelection/CSharpCommentSelectionService.cs b/src/Features/CSharp/Portable/CommentSelection/CSharpCommentSelectionService.cs index 06eaf46d2fad0..ed6da05b0efce 100644 --- a/src/Features/CSharp/Portable/CommentSelection/CSharpCommentSelectionService.cs +++ b/src/Features/CSharp/Portable/CommentSelection/CSharpCommentSelectionService.cs @@ -9,20 +9,19 @@ using Microsoft.CodeAnalysis.CommentSelection; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.Editor.CSharp.CommentSelection +namespace Microsoft.CodeAnalysis.Editor.CSharp.CommentSelection; + +[ExportLanguageService(typeof(ICommentSelectionService), LanguageNames.CSharp), Shared] +internal class CSharpCommentSelectionService : AbstractCommentSelectionService { - [ExportLanguageService(typeof(ICommentSelectionService), LanguageNames.CSharp), Shared] - internal class CSharpCommentSelectionService : AbstractCommentSelectionService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpCommentSelectionService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpCommentSelectionService() - { - } - - public override string SingleLineCommentString => "//"; - public override bool SupportsBlockComment => true; - public override string BlockCommentStartString => "/*"; - public override string BlockCommentEndString => "*/"; } + + public override string SingleLineCommentString => "//"; + public override bool SupportsBlockComment => true; + public override string BlockCommentStartString => "/*"; + public override string BlockCommentEndString => "*/"; } diff --git a/src/Features/CSharp/Portable/Completion/CSharpCompletionService.cs b/src/Features/CSharp/Portable/Completion/CSharpCompletionService.cs index 9d75d773579b4..019b7edbe2e75 100644 --- a/src/Features/CSharp/Portable/Completion/CSharpCompletionService.cs +++ b/src/Features/CSharp/Portable/Completion/CSharpCompletionService.cs @@ -12,58 +12,57 @@ using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion +namespace Microsoft.CodeAnalysis.CSharp.Completion; + +internal sealed class CSharpCompletionService : CommonCompletionService { - internal sealed class CSharpCompletionService : CommonCompletionService + [ExportLanguageServiceFactory(typeof(CompletionService), LanguageNames.CSharp), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + internal sealed class Factory(IAsynchronousOperationListenerProvider listenerProvider) : ILanguageServiceFactory { - [ExportLanguageServiceFactory(typeof(CompletionService), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class Factory(IAsynchronousOperationListenerProvider listenerProvider) : ILanguageServiceFactory - { - private readonly IAsynchronousOperationListenerProvider _listenerProvider = listenerProvider; + private readonly IAsynchronousOperationListenerProvider _listenerProvider = listenerProvider; - [Obsolete(MefConstruction.FactoryMethodMessage, error: true)] - public ILanguageService CreateLanguageService(HostLanguageServices languageServices) - => new CSharpCompletionService(languageServices.LanguageServices.SolutionServices, _listenerProvider); - } + [Obsolete(MefConstruction.FactoryMethodMessage, error: true)] + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + => new CSharpCompletionService(languageServices.LanguageServices.SolutionServices, _listenerProvider); + } - private CompletionRules _latestRules = CompletionRules.Default; + private CompletionRules _latestRules = CompletionRules.Default; - private CSharpCompletionService(SolutionServices services, IAsynchronousOperationListenerProvider listenerProvider) - : base(services, listenerProvider) - { - } + private CSharpCompletionService(SolutionServices services, IAsynchronousOperationListenerProvider listenerProvider) + : base(services, listenerProvider) + { + } - public override string Language => LanguageNames.CSharp; + public override string Language => LanguageNames.CSharp; - public override TextSpan GetDefaultCompletionListSpan(SourceText text, int caretPosition) - => CompletionUtilities.GetCompletionItemSpan(text, caretPosition); + public override TextSpan GetDefaultCompletionListSpan(SourceText text, int caretPosition) + => CompletionUtilities.GetCompletionItemSpan(text, caretPosition); - internal override CompletionRules GetRules(CompletionOptions options) - { - var enterRule = options.EnterKeyBehavior; - var snippetRule = options.SnippetsBehavior; + internal override CompletionRules GetRules(CompletionOptions options) + { + var enterRule = options.EnterKeyBehavior; + var snippetRule = options.SnippetsBehavior; - // Although EnterKeyBehavior is a per-language setting, the meaning of an unset setting (Default) differs between C# and VB - // In C# the default means Never to maintain previous behavior - if (enterRule == EnterKeyRule.Default) - { - enterRule = EnterKeyRule.Never; - } + // Although EnterKeyBehavior is a per-language setting, the meaning of an unset setting (Default) differs between C# and VB + // In C# the default means Never to maintain previous behavior + if (enterRule == EnterKeyRule.Default) + { + enterRule = EnterKeyRule.Never; + } - if (snippetRule == SnippetsRule.Default) - { - snippetRule = SnippetsRule.AlwaysInclude; - } + if (snippetRule == SnippetsRule.Default) + { + snippetRule = SnippetsRule.AlwaysInclude; + } - // use interlocked + stored rules to reduce # of times this gets created when option is different than default - var newRules = _latestRules.WithDefaultEnterKeyRule(enterRule) - .WithSnippetsRule(snippetRule); + // use interlocked + stored rules to reduce # of times this gets created when option is different than default + var newRules = _latestRules.WithDefaultEnterKeyRule(enterRule) + .WithSnippetsRule(snippetRule); - Interlocked.Exchange(ref _latestRules, newRules); + Interlocked.Exchange(ref _latestRules, newRules); - return newRules; - } + return newRules; } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/AggregateEmbeddedLanguageCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/AggregateEmbeddedLanguageCompletionProvider.cs index e6a1c776838fc..901569b5482ae 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/AggregateEmbeddedLanguageCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/AggregateEmbeddedLanguageCompletionProvider.cs @@ -10,15 +10,14 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(AggregateEmbeddedLanguageCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(ExtensionMethodImportCompletionProvider))] +[Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class AggregateEmbeddedLanguageCompletionProvider([ImportMany] IEnumerable> languageServices) : AbstractAggregateEmbeddedLanguageCompletionProvider(languageServices, LanguageNames.CSharp) { - [ExportCompletionProvider(nameof(AggregateEmbeddedLanguageCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(ExtensionMethodImportCompletionProvider))] - [Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class AggregateEmbeddedLanguageCompletionProvider([ImportMany] IEnumerable> languageServices) : AbstractAggregateEmbeddedLanguageCompletionProvider(languageServices, LanguageNames.CSharp) - { - internal override string Language => LanguageNames.CSharp; - } + internal override string Language => LanguageNames.CSharp; } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/AttributeNamedParameterCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/AttributeNamedParameterCompletionProvider.cs index 25e3718304912..61b65f0f72745 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/AttributeNamedParameterCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/AttributeNamedParameterCompletionProvider.cs @@ -21,260 +21,259 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(AttributeNamedParameterCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(FirstBuiltInCompletionProvider))] +[Shared] +internal class AttributeNamedParameterCompletionProvider : LSPCompletionProvider { - [ExportCompletionProvider(nameof(AttributeNamedParameterCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(FirstBuiltInCompletionProvider))] - [Shared] - internal class AttributeNamedParameterCompletionProvider : LSPCompletionProvider - { - private const string EqualsString = "="; - private const string SpaceEqualsString = " ="; - private const string ColonString = ":"; + private const string EqualsString = "="; + private const string SpaceEqualsString = " ="; + private const string ColonString = ":"; - private static readonly CompletionItemRules _spaceItemFilterRule = CompletionItemRules.Default.WithFilterCharacterRule( - CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ' ')); + private static readonly CompletionItemRules _spaceItemFilterRule = CompletionItemRules.Default.WithFilterCharacterRule( + CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ' ')); - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public AttributeNamedParameterCompletionProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public AttributeNamedParameterCompletionProvider() + { + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; - public override async Task ProvideCompletionsAsync(CompletionContext context) + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - try - { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (syntaxTree.IsInNonUserCode(position, cancellationToken)) - { - return; - } + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (syntaxTree.IsInNonUserCode(position, cancellationToken)) + { + return; + } - var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); - token = token.GetPreviousTokenIfTouchingWord(position); + var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); + token = token.GetPreviousTokenIfTouchingWord(position); - if (token.Kind() is not (SyntaxKind.OpenParenToken or SyntaxKind.CommaToken)) - { - return; - } + if (token.Kind() is not (SyntaxKind.OpenParenToken or SyntaxKind.CommaToken)) + { + return; + } - if (token.Parent!.Parent is not AttributeSyntax attributeSyntax || token.Parent is not AttributeArgumentListSyntax attributeArgumentList) - { - return; - } + if (token.Parent!.Parent is not AttributeSyntax attributeSyntax || token.Parent is not AttributeArgumentListSyntax attributeArgumentList) + { + return; + } - if (IsAfterNameColonArgument(token) || IsAfterNameEqualsArgument(token)) - { - context.IsExclusive = true; - } + if (IsAfterNameColonArgument(token) || IsAfterNameEqualsArgument(token)) + { + context.IsExclusive = true; + } - // We actually want to collect two sets of named parameters to present the user. The - // normal named parameters that come from the attribute constructors. These will be - // presented like "goo:". And also the named parameters that come from the writable - // fields/properties in the attribute. These will be presented like "bar =". + // We actually want to collect two sets of named parameters to present the user. The + // normal named parameters that come from the attribute constructors. These will be + // presented like "goo:". And also the named parameters that come from the writable + // fields/properties in the attribute. These will be presented like "bar =". - var existingNamedParameters = GetExistingNamedParameters(attributeArgumentList, position); + var existingNamedParameters = GetExistingNamedParameters(attributeArgumentList, position); - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(attributeSyntax, cancellationToken).ConfigureAwait(false); - var nameColonItems = GetNameColonItems(context, semanticModel, token, attributeSyntax, existingNamedParameters); - var nameEqualsItems = GetNameEqualsItems(context, semanticModel, token, attributeSyntax, existingNamedParameters); + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(attributeSyntax, cancellationToken).ConfigureAwait(false); + var nameColonItems = GetNameColonItems(context, semanticModel, token, attributeSyntax, existingNamedParameters); + var nameEqualsItems = GetNameEqualsItems(context, semanticModel, token, attributeSyntax, existingNamedParameters); - context.AddItems(nameEqualsItems); + context.AddItems(nameEqualsItems); - // If we're after a name= parameter, then we only want to show name= parameters. - // Otherwise, show name: parameters too. - if (!IsAfterNameEqualsArgument(token)) - { - context.AddItems(nameColonItems); - } - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e)) + // If we're after a name= parameter, then we only want to show name= parameters. + // Otherwise, show name: parameters too. + if (!IsAfterNameEqualsArgument(token)) { - // nop + context.AddItems(nameColonItems); } } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e)) + { + // nop + } + } - private static bool IsAfterNameColonArgument(SyntaxToken token) + private static bool IsAfterNameColonArgument(SyntaxToken token) + { + if (token.Kind() == SyntaxKind.CommaToken && token.Parent is AttributeArgumentListSyntax argumentList) { - if (token.Kind() == SyntaxKind.CommaToken && token.Parent is AttributeArgumentListSyntax argumentList) + foreach (var item in argumentList.Arguments.GetWithSeparators()) { - foreach (var item in argumentList.Arguments.GetWithSeparators()) + if (item.IsToken && item.AsToken() == token) { - if (item.IsToken && item.AsToken() == token) - { - return false; - } - - var node = (AttributeArgumentSyntax?)item.AsNode(); - if (node?.NameColon != null) - { - return true; - } + return false; } - } - return false; + var node = (AttributeArgumentSyntax?)item.AsNode(); + if (node?.NameColon != null) + { + return true; + } + } } - private static bool IsAfterNameEqualsArgument(SyntaxToken token) + return false; + } + + private static bool IsAfterNameEqualsArgument(SyntaxToken token) + { + if (token.Kind() == SyntaxKind.CommaToken && token.Parent is AttributeArgumentListSyntax argumentList) { - if (token.Kind() == SyntaxKind.CommaToken && token.Parent is AttributeArgumentListSyntax argumentList) + foreach (var item in argumentList.Arguments.GetWithSeparators()) { - foreach (var item in argumentList.Arguments.GetWithSeparators()) + if (item.IsToken && item.AsToken() == token) { - if (item.IsToken && item.AsToken() == token) - { - return false; - } - - var node = (AttributeArgumentSyntax?)item.AsNode(); - if (node?.NameEquals != null) - { - return true; - } + return false; } - } - return false; + var node = (AttributeArgumentSyntax?)item.AsNode(); + if (node?.NameEquals != null) + { + return true; + } + } } - private static ImmutableArray GetNameEqualsItems( - CompletionContext context, SemanticModel semanticModel, - SyntaxToken token, AttributeSyntax attributeSyntax, ISet existingNamedParameters) - { - var attributeNamedParameters = GetAttributeNamedParameters(semanticModel, context.Position, attributeSyntax, context.CancellationToken); - var unspecifiedNamedParameters = attributeNamedParameters.Where(p => !existingNamedParameters.Contains(p.Name)); - - var rightToken = semanticModel.SyntaxTree.FindTokenOnRightOfPosition(context.Position, context.CancellationToken); - var displayTextSuffix = rightToken.IsKind(SyntaxKind.EqualsToken) ? null : SpaceEqualsString; - - var q = from p in attributeNamedParameters - where !existingNamedParameters.Contains(p.Name) - select SymbolCompletionItem.CreateWithSymbolId( - displayText: p.Name.ToIdentifierToken().ToString(), - displayTextSuffix: displayTextSuffix, - insertionText: null, - symbols: ImmutableArray.Create(p), - contextPosition: token.SpanStart, - sortText: p.Name, - rules: _spaceItemFilterRule); - return q.ToImmutableArray(); - } + return false; + } - private static IEnumerable GetNameColonItems( - CompletionContext context, SemanticModel semanticModel, SyntaxToken token, AttributeSyntax attributeSyntax, ISet existingNamedParameters) - { - var parameterLists = GetParameterLists(semanticModel, context.Position, attributeSyntax, context.CancellationToken); - parameterLists = parameterLists.Where(pl => IsValid(pl, existingNamedParameters)); - - var rightToken = semanticModel.SyntaxTree.FindTokenOnRightOfPosition(context.Position, context.CancellationToken); - var displayTextSuffix = rightToken.IsKind(SyntaxKind.ColonToken) ? null : ColonString; - - return from pl in parameterLists - from p in pl - where !existingNamedParameters.Contains(p.Name) - select SymbolCompletionItem.CreateWithSymbolId( - displayText: p.Name.ToIdentifierToken().ToString(), - displayTextSuffix: displayTextSuffix, - insertionText: null, - symbols: ImmutableArray.Create(p), - contextPosition: token.SpanStart, - sortText: p.Name, - rules: CompletionItemRules.Default); - } + private static ImmutableArray GetNameEqualsItems( + CompletionContext context, SemanticModel semanticModel, + SyntaxToken token, AttributeSyntax attributeSyntax, ISet existingNamedParameters) + { + var attributeNamedParameters = GetAttributeNamedParameters(semanticModel, context.Position, attributeSyntax, context.CancellationToken); + var unspecifiedNamedParameters = attributeNamedParameters.Where(p => !existingNamedParameters.Contains(p.Name)); + + var rightToken = semanticModel.SyntaxTree.FindTokenOnRightOfPosition(context.Position, context.CancellationToken); + var displayTextSuffix = rightToken.IsKind(SyntaxKind.EqualsToken) ? null : SpaceEqualsString; + + var q = from p in attributeNamedParameters + where !existingNamedParameters.Contains(p.Name) + select SymbolCompletionItem.CreateWithSymbolId( + displayText: p.Name.ToIdentifierToken().ToString(), + displayTextSuffix: displayTextSuffix, + insertionText: null, + symbols: ImmutableArray.Create(p), + contextPosition: token.SpanStart, + sortText: p.Name, + rules: _spaceItemFilterRule); + return q.ToImmutableArray(); + } + + private static IEnumerable GetNameColonItems( + CompletionContext context, SemanticModel semanticModel, SyntaxToken token, AttributeSyntax attributeSyntax, ISet existingNamedParameters) + { + var parameterLists = GetParameterLists(semanticModel, context.Position, attributeSyntax, context.CancellationToken); + parameterLists = parameterLists.Where(pl => IsValid(pl, existingNamedParameters)); + + var rightToken = semanticModel.SyntaxTree.FindTokenOnRightOfPosition(context.Position, context.CancellationToken); + var displayTextSuffix = rightToken.IsKind(SyntaxKind.ColonToken) ? null : ColonString; + + return from pl in parameterLists + from p in pl + where !existingNamedParameters.Contains(p.Name) + select SymbolCompletionItem.CreateWithSymbolId( + displayText: p.Name.ToIdentifierToken().ToString(), + displayTextSuffix: displayTextSuffix, + insertionText: null, + symbols: ImmutableArray.Create(p), + contextPosition: token.SpanStart, + sortText: p.Name, + rules: CompletionItemRules.Default); + } + + internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); - internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + private static bool IsValid(ImmutableArray parameterList, ISet existingNamedParameters) + => existingNamedParameters.Except(parameterList.Select(p => p.Name)).IsEmpty(); - private static bool IsValid(ImmutableArray parameterList, ISet existingNamedParameters) - => existingNamedParameters.Except(parameterList.Select(p => p.Name)).IsEmpty(); + private static ISet GetExistingNamedParameters(AttributeArgumentListSyntax argumentList, int position) + { + var existingArguments1 = + argumentList.Arguments.Where(a => a.Span.End <= position) + .Where(a => a.NameColon != null) + .Select(a => a.NameColon!.Name.Identifier.ValueText); + var existingArguments2 = + argumentList.Arguments.Where(a => a.Span.End <= position) + .Where(a => a.NameEquals != null) + .Select(a => a.NameEquals!.Name.Identifier.ValueText); + + return existingArguments1.Concat(existingArguments2).ToSet(); + } - private static ISet GetExistingNamedParameters(AttributeArgumentListSyntax argumentList, int position) + private static IEnumerable> GetParameterLists( + SemanticModel semanticModel, + int position, + AttributeSyntax attribute, + CancellationToken cancellationToken) + { + var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); + if (within != null && semanticModel.GetTypeInfo(attribute, cancellationToken).Type is INamedTypeSymbol attributeType) { - var existingArguments1 = - argumentList.Arguments.Where(a => a.Span.End <= position) - .Where(a => a.NameColon != null) - .Select(a => a.NameColon!.Name.Identifier.ValueText); - var existingArguments2 = - argumentList.Arguments.Where(a => a.Span.End <= position) - .Where(a => a.NameEquals != null) - .Select(a => a.NameEquals!.Name.Identifier.ValueText); - - return existingArguments1.Concat(existingArguments2).ToSet(); + return attributeType.InstanceConstructors.Where(c => c.IsAccessibleWithin(within)) + .Select(c => c.Parameters); } - private static IEnumerable> GetParameterLists( - SemanticModel semanticModel, - int position, - AttributeSyntax attribute, - CancellationToken cancellationToken) - { - var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); - if (within != null && semanticModel.GetTypeInfo(attribute, cancellationToken).Type is INamedTypeSymbol attributeType) - { - return attributeType.InstanceConstructors.Where(c => c.IsAccessibleWithin(within)) - .Select(c => c.Parameters); - } + return SpecializedCollections.EmptyEnumerable>(); + } - return SpecializedCollections.EmptyEnumerable>(); - } + private static IEnumerable GetAttributeNamedParameters( + SemanticModel semanticModel, + int position, + AttributeSyntax attribute, + CancellationToken cancellationToken) + { + var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); + var attributeType = (INamedTypeSymbol?)semanticModel.GetTypeInfo(attribute, cancellationToken).Type; + Contract.ThrowIfNull(attributeType); + return attributeType.GetAttributeNamedParameters(semanticModel.Compilation, within); + } - private static IEnumerable GetAttributeNamedParameters( - SemanticModel semanticModel, - int position, - AttributeSyntax attribute, - CancellationToken cancellationToken) - { - var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); - var attributeType = (INamedTypeSymbol?)semanticModel.GetTypeInfo(attribute, cancellationToken).Type; - Contract.ThrowIfNull(attributeType); - return attributeType.GetAttributeNamedParameters(semanticModel.Compilation, within); - } + protected override Task GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + => Task.FromResult(GetTextChange(selectedItem, ch)); - protected override Task GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) - => Task.FromResult(GetTextChange(selectedItem, ch)); + private static TextChange? GetTextChange(CompletionItem selectedItem, char? ch) + { + var displayText = selectedItem.DisplayText + selectedItem.DisplayTextSuffix; - private static TextChange? GetTextChange(CompletionItem selectedItem, char? ch) + if (ch != null) { - var displayText = selectedItem.DisplayText + selectedItem.DisplayTextSuffix; - - if (ch != null) + // If user types a space, do not complete the " =" (space and equals) at the end of a named parameter. The + // typed space character will be passed through to the editor, and they can then type the '='. + if (ch == ' ' && displayText.EndsWith(SpaceEqualsString, StringComparison.Ordinal)) { - // If user types a space, do not complete the " =" (space and equals) at the end of a named parameter. The - // typed space character will be passed through to the editor, and they can then type the '='. - if (ch == ' ' && displayText.EndsWith(SpaceEqualsString, StringComparison.Ordinal)) - { - return new TextChange(selectedItem.Span, displayText.Remove(displayText.Length - SpaceEqualsString.Length)); - } - - // If the user types '=', do not complete the '=' at the end of the named parameter because the typed '=' - // will be passed through to the editor. - if (ch == '=' && displayText.EndsWith(EqualsString, StringComparison.Ordinal)) - { - return new TextChange(selectedItem.Span, displayText.Remove(displayText.Length - EqualsString.Length)); - } + return new TextChange(selectedItem.Span, displayText.Remove(displayText.Length - SpaceEqualsString.Length)); + } - // If the user types ':', do not complete the ':' at the end of the named parameter because the typed ':' - // will be passed through to the editor. - if (ch == ':' && displayText.EndsWith(ColonString, StringComparison.Ordinal)) - { - return new TextChange(selectedItem.Span, displayText.Remove(displayText.Length - ColonString.Length)); - } + // If the user types '=', do not complete the '=' at the end of the named parameter because the typed '=' + // will be passed through to the editor. + if (ch == '=' && displayText.EndsWith(EqualsString, StringComparison.Ordinal)) + { + return new TextChange(selectedItem.Span, displayText.Remove(displayText.Length - EqualsString.Length)); } - return new TextChange(selectedItem.Span, displayText); + // If the user types ':', do not complete the ':' at the end of the named parameter because the typed ':' + // will be passed through to the editor. + if (ch == ':' && displayText.EndsWith(ColonString, StringComparison.Ordinal)) + { + return new TextChange(selectedItem.Span, displayText.Remove(displayText.Length - ColonString.Length)); + } } + + return new TextChange(selectedItem.Span, displayText); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/AwaitCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/AwaitCompletionProvider.cs index 9d5079823dcd4..5504f78dc3ba9 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/AwaitCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/AwaitCompletionProvider.cs @@ -17,104 +17,103 @@ using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(AwaitCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(KeywordCompletionProvider))] +[Shared] +internal sealed class AwaitCompletionProvider : AbstractAwaitCompletionProvider { - [ExportCompletionProvider(nameof(AwaitCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(KeywordCompletionProvider))] - [Shared] - internal sealed class AwaitCompletionProvider : AbstractAwaitCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public AwaitCompletionProvider() + : base(CSharpSyntaxFacts.Instance) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public AwaitCompletionProvider() - : base(CSharpSyntaxFacts.Instance) - { - } + } + + internal override string Language => LanguageNames.CSharp; + public override ImmutableHashSet TriggerCharacters => CompletionUtilities.CommonTriggerCharactersWithArgumentList; - internal override string Language => LanguageNames.CSharp; - public override ImmutableHashSet TriggerCharacters => CompletionUtilities.CommonTriggerCharactersWithArgumentList; + protected override bool IsAwaitKeywordContext(SyntaxContext syntaxContext) + => base.IsAwaitKeywordContext(syntaxContext); - protected override bool IsAwaitKeywordContext(SyntaxContext syntaxContext) - => base.IsAwaitKeywordContext(syntaxContext); + /// + /// Gets the span start where async keyword should go. + /// + protected override int GetSpanStart(SyntaxNode declaration) + { + return declaration switch + { + MethodDeclarationSyntax method => method.ReturnType.SpanStart, + LocalFunctionStatementSyntax local => local.ReturnType.SpanStart, + AnonymousMethodExpressionSyntax anonymous => anonymous.DelegateKeyword.SpanStart, + // If we have an explicit lambda return type, async should go just before it. Otherwise, it should go before parameter list. + // static [|async|] (a) => .... + // static [|async|] ExplicitReturnType (a) => .... + ParenthesizedLambdaExpressionSyntax parenthesizedLambda => (parenthesizedLambda.ReturnType as SyntaxNode ?? parenthesizedLambda.ParameterList).SpanStart, + SimpleLambdaExpressionSyntax simpleLambda => simpleLambda.Parameter.SpanStart, + _ => throw ExceptionUtilities.UnexpectedValue(declaration.Kind()) + }; + } + + protected override SyntaxNode? GetAsyncSupportingDeclaration(SyntaxToken token) + { + // In a case like + // someTask.$$ + // await Test(); + // someTask.await Test() is parsed as a local function statement. + // We skip this and look further up in the hierarchy. + var parent = token.Parent; + if (parent == null) + return null; - /// - /// Gets the span start where async keyword should go. - /// - protected override int GetSpanStart(SyntaxNode declaration) + if (parent is QualifiedNameSyntax { Parent: LocalFunctionStatementSyntax localFunction } qualifiedName && + localFunction.ReturnType == qualifiedName) { - return declaration switch - { - MethodDeclarationSyntax method => method.ReturnType.SpanStart, - LocalFunctionStatementSyntax local => local.ReturnType.SpanStart, - AnonymousMethodExpressionSyntax anonymous => anonymous.DelegateKeyword.SpanStart, - // If we have an explicit lambda return type, async should go just before it. Otherwise, it should go before parameter list. - // static [|async|] (a) => .... - // static [|async|] ExplicitReturnType (a) => .... - ParenthesizedLambdaExpressionSyntax parenthesizedLambda => (parenthesizedLambda.ReturnType as SyntaxNode ?? parenthesizedLambda.ParameterList).SpanStart, - SimpleLambdaExpressionSyntax simpleLambda => simpleLambda.Parameter.SpanStart, - _ => throw ExceptionUtilities.UnexpectedValue(declaration.Kind()) - }; + parent = localFunction; } - protected override SyntaxNode? GetAsyncSupportingDeclaration(SyntaxToken token) + return parent.AncestorsAndSelf().FirstOrDefault(node => node.IsAsyncSupportingFunctionSyntax()); + } + + protected override SyntaxNode? GetExpressionToPlaceAwaitInFrontOf(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + { + var dotToken = GetDotTokenLeftOfPosition(syntaxTree, position, cancellationToken); + return dotToken?.Parent switch { - // In a case like - // someTask.$$ - // await Test(); - // someTask.await Test() is parsed as a local function statement. - // We skip this and look further up in the hierarchy. - var parent = token.Parent; - if (parent == null) - return null; + // Don't support conditional access someTask?.$$ or c?.TaskReturning().$$ because there is no good completion until + // await? is supported by the language https://github.com/dotnet/csharplang/issues/35 + MemberAccessExpressionSyntax memberAccess => memberAccess.GetParentConditionalAccessExpression() is null ? memberAccess : null, + // someTask.$$. + RangeExpressionSyntax range => range.LeftOperand, + // special cases, where parsing is misleading. Such cases are handled in GetTypeSymbolOfExpression. + QualifiedNameSyntax qualifiedName => qualifiedName.Left, + _ => null, + }; + } - if (parent is QualifiedNameSyntax { Parent: LocalFunctionStatementSyntax localFunction } qualifiedName && - localFunction.ReturnType == qualifiedName) - { - parent = localFunction; - } + protected override SyntaxToken? GetDotTokenLeftOfPosition(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + => CompletionUtilities.GetDotTokenLeftOfPosition(syntaxTree, position, cancellationToken); - return parent.AncestorsAndSelf().FirstOrDefault(node => node.IsAsyncSupportingFunctionSyntax()); + protected override ITypeSymbol? GetTypeSymbolOfExpression(SemanticModel semanticModel, SyntaxNode potentialAwaitableExpression, CancellationToken cancellationToken) + { + if (potentialAwaitableExpression is MemberAccessExpressionSyntax memberAccess) + { + var memberAccessExpression = memberAccess.Expression.WalkDownParentheses(); + // In cases like Task.$$ semanticModel.GetTypeInfo returns Task, but + // we don't want to suggest await here. We look up the symbol of the "Task" part + // and return null if it is a NamedType. + var symbol = semanticModel.GetSymbolInfo(memberAccessExpression, cancellationToken).Symbol; + return symbol is ITypeSymbol ? null : semanticModel.GetTypeInfo(memberAccessExpression, cancellationToken).Type; } - - protected override SyntaxNode? GetExpressionToPlaceAwaitInFrontOf(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + else if (potentialAwaitableExpression is ExpressionSyntax expression && + expression.ShouldNameExpressionBeTreatedAsExpressionInsteadOfType(semanticModel, out _, out var container)) { - var dotToken = GetDotTokenLeftOfPosition(syntaxTree, position, cancellationToken); - return dotToken?.Parent switch - { - // Don't support conditional access someTask?.$$ or c?.TaskReturning().$$ because there is no good completion until - // await? is supported by the language https://github.com/dotnet/csharplang/issues/35 - MemberAccessExpressionSyntax memberAccess => memberAccess.GetParentConditionalAccessExpression() is null ? memberAccess : null, - // someTask.$$. - RangeExpressionSyntax range => range.LeftOperand, - // special cases, where parsing is misleading. Such cases are handled in GetTypeSymbolOfExpression. - QualifiedNameSyntax qualifiedName => qualifiedName.Left, - _ => null, - }; + return container; } - - protected override SyntaxToken? GetDotTokenLeftOfPosition(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) - => CompletionUtilities.GetDotTokenLeftOfPosition(syntaxTree, position, cancellationToken); - - protected override ITypeSymbol? GetTypeSymbolOfExpression(SemanticModel semanticModel, SyntaxNode potentialAwaitableExpression, CancellationToken cancellationToken) + else { - if (potentialAwaitableExpression is MemberAccessExpressionSyntax memberAccess) - { - var memberAccessExpression = memberAccess.Expression.WalkDownParentheses(); - // In cases like Task.$$ semanticModel.GetTypeInfo returns Task, but - // we don't want to suggest await here. We look up the symbol of the "Task" part - // and return null if it is a NamedType. - var symbol = semanticModel.GetSymbolInfo(memberAccessExpression, cancellationToken).Symbol; - return symbol is ITypeSymbol ? null : semanticModel.GetTypeInfo(memberAccessExpression, cancellationToken).Type; - } - else if (potentialAwaitableExpression is ExpressionSyntax expression && - expression.ShouldNameExpressionBeTreatedAsExpressionInsteadOfType(semanticModel, out _, out var container)) - { - return container; - } - else - { - return semanticModel.GetTypeInfo(potentialAwaitableExpression, cancellationToken).Type; - } + return semanticModel.GetTypeInfo(potentialAwaitableExpression, cancellationToken).Type; } } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/CSharpSuggestionModeCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/CSharpSuggestionModeCompletionProvider.cs index d5cd2f863d06f..807ca7b7abd3a 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/CSharpSuggestionModeCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/CSharpSuggestionModeCompletionProvider.cs @@ -20,220 +20,219 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(CSharpSuggestionModeCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(ObjectAndWithInitializerCompletionProvider))] +[Shared] +internal class CSharpSuggestionModeCompletionProvider : AbstractSuggestionModeCompletionProvider { - [ExportCompletionProvider(nameof(CSharpSuggestionModeCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(ObjectAndWithInitializerCompletionProvider))] - [Shared] - internal class CSharpSuggestionModeCompletionProvider : AbstractSuggestionModeCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpSuggestionModeCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpSuggestionModeCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - protected override async Task GetSuggestionModeItemAsync( - Document document, int position, TextSpan itemSpan, CompletionTrigger trigger, CancellationToken cancellationToken = default) + protected override async Task GetSuggestionModeItemAsync( + Document document, int position, TextSpan itemSpan, CompletionTrigger trigger, CancellationToken cancellationToken = default) + { + if (trigger.Kind != CompletionTriggerKind.Snippets) { - if (trigger.Kind != CompletionTriggerKind.Snippets) - { - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var token = tree - .FindTokenOnLeftOfPosition(position, cancellationToken) - .GetPreviousTokenIfTouchingWord(position); + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var token = tree + .FindTokenOnLeftOfPosition(position, cancellationToken) + .GetPreviousTokenIfTouchingWord(position); - if (token.Kind() == SyntaxKind.None) - return null; + if (token.Kind() == SyntaxKind.None) + return null; - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(token.Parent, cancellationToken).ConfigureAwait(false); - var typeInferrer = document.GetRequiredLanguageService(); - if (IsLambdaExpression(semanticModel, tree, position, token, typeInferrer, cancellationToken)) - { - return CreateSuggestionModeItem(CSharpFeaturesResources.lambda_expression, CSharpFeaturesResources.Autoselect_disabled_due_to_potential_lambda_declaration); - } - else if (IsAnonymousObjectCreation(token)) - { - return CreateSuggestionModeItem(CSharpFeaturesResources.member_name, CSharpFeaturesResources.Autoselect_disabled_due_to_possible_explicitly_named_anonymous_type_member_creation); - } - else if (IsPotentialPatternVariableDeclaration(tree.FindTokenOnLeftOfPosition(position, cancellationToken))) - { - return CreateSuggestionModeItem(CSharpFeaturesResources.pattern_variable, CSharpFeaturesResources.Autoselect_disabled_due_to_potential_pattern_variable_declaration); - } - else if (token.IsPreProcessorExpressionContext()) - { - return CreateEmptySuggestionModeItem(); - } - else if (token.IsKindOrHasMatchingText(SyntaxKind.FromKeyword) || token.IsKindOrHasMatchingText(SyntaxKind.JoinKeyword)) - { - return CreateSuggestionModeItem(CSharpFeaturesResources.range_variable, CSharpFeaturesResources.Autoselect_disabled_due_to_potential_range_variable_declaration); - } - else if (tree.IsNamespaceDeclarationNameContext(position, cancellationToken)) - { - return CreateSuggestionModeItem(CSharpFeaturesResources.namespace_name, CSharpFeaturesResources.Autoselect_disabled_due_to_namespace_declaration); - } - else if (tree.IsPartialTypeDeclarationNameContext(position, cancellationToken, out var typeDeclaration)) + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(token.Parent, cancellationToken).ConfigureAwait(false); + var typeInferrer = document.GetRequiredLanguageService(); + if (IsLambdaExpression(semanticModel, tree, position, token, typeInferrer, cancellationToken)) + { + return CreateSuggestionModeItem(CSharpFeaturesResources.lambda_expression, CSharpFeaturesResources.Autoselect_disabled_due_to_potential_lambda_declaration); + } + else if (IsAnonymousObjectCreation(token)) + { + return CreateSuggestionModeItem(CSharpFeaturesResources.member_name, CSharpFeaturesResources.Autoselect_disabled_due_to_possible_explicitly_named_anonymous_type_member_creation); + } + else if (IsPotentialPatternVariableDeclaration(tree.FindTokenOnLeftOfPosition(position, cancellationToken))) + { + return CreateSuggestionModeItem(CSharpFeaturesResources.pattern_variable, CSharpFeaturesResources.Autoselect_disabled_due_to_potential_pattern_variable_declaration); + } + else if (token.IsPreProcessorExpressionContext()) + { + return CreateEmptySuggestionModeItem(); + } + else if (token.IsKindOrHasMatchingText(SyntaxKind.FromKeyword) || token.IsKindOrHasMatchingText(SyntaxKind.JoinKeyword)) + { + return CreateSuggestionModeItem(CSharpFeaturesResources.range_variable, CSharpFeaturesResources.Autoselect_disabled_due_to_potential_range_variable_declaration); + } + else if (tree.IsNamespaceDeclarationNameContext(position, cancellationToken)) + { + return CreateSuggestionModeItem(CSharpFeaturesResources.namespace_name, CSharpFeaturesResources.Autoselect_disabled_due_to_namespace_declaration); + } + else if (tree.IsPartialTypeDeclarationNameContext(position, cancellationToken, out var typeDeclaration)) + { + switch (typeDeclaration.Keyword.Kind()) { - switch (typeDeclaration.Keyword.Kind()) - { - case SyntaxKind.ClassKeyword: - return CreateSuggestionModeItem(CSharpFeaturesResources.class_name, CSharpFeaturesResources.Autoselect_disabled_due_to_type_declaration); + case SyntaxKind.ClassKeyword: + return CreateSuggestionModeItem(CSharpFeaturesResources.class_name, CSharpFeaturesResources.Autoselect_disabled_due_to_type_declaration); - case SyntaxKind.StructKeyword: - return CreateSuggestionModeItem(CSharpFeaturesResources.struct_name, CSharpFeaturesResources.Autoselect_disabled_due_to_type_declaration); + case SyntaxKind.StructKeyword: + return CreateSuggestionModeItem(CSharpFeaturesResources.struct_name, CSharpFeaturesResources.Autoselect_disabled_due_to_type_declaration); - case SyntaxKind.InterfaceKeyword: - return CreateSuggestionModeItem(CSharpFeaturesResources.interface_name, CSharpFeaturesResources.Autoselect_disabled_due_to_type_declaration); - } - } - else if (tree.IsPossibleDeconstructionDesignation(position, cancellationToken)) - { - return CreateSuggestionModeItem(CSharpFeaturesResources.designation_name, - CSharpFeaturesResources.Autoselect_disabled_due_to_possible_deconstruction_declaration); + case SyntaxKind.InterfaceKeyword: + return CreateSuggestionModeItem(CSharpFeaturesResources.interface_name, CSharpFeaturesResources.Autoselect_disabled_due_to_type_declaration); } } + else if (tree.IsPossibleDeconstructionDesignation(position, cancellationToken)) + { + return CreateSuggestionModeItem(CSharpFeaturesResources.designation_name, + CSharpFeaturesResources.Autoselect_disabled_due_to_possible_deconstruction_declaration); + } + } - return null; + return null; + } + + private static bool IsAnonymousObjectCreation(SyntaxToken token) + { + if (token.Parent is AnonymousObjectCreationExpressionSyntax) + { + // We'll show the builder after an open brace or comma, because that's where the + // user can start declaring new named parts. + return token.Kind() is SyntaxKind.OpenBraceToken or SyntaxKind.CommaToken; } - private static bool IsAnonymousObjectCreation(SyntaxToken token) + return false; + } + + private static bool IsLambdaExpression(SemanticModel semanticModel, SyntaxTree tree, int position, SyntaxToken token, ITypeInferenceService typeInferrer, CancellationToken cancellationToken) + { + // Not after `new` + if (token.IsKind(SyntaxKind.NewKeyword) && token.Parent.IsKind(SyntaxKind.ObjectCreationExpression)) { - if (token.Parent is AnonymousObjectCreationExpressionSyntax) - { - // We'll show the builder after an open brace or comma, because that's where the - // user can start declaring new named parts. - return token.Kind() is SyntaxKind.OpenBraceToken or SyntaxKind.CommaToken; - } + return false; + } + // Typing a generic type parameter, the tree might look like a binary expression around the < token. + // If we infer a delegate type here (because that's what on the other side of the binop), + // ignore it. + if (token.Kind() == SyntaxKind.LessThanToken && token.Parent is BinaryExpressionSyntax) + { return false; } - private static bool IsLambdaExpression(SemanticModel semanticModel, SyntaxTree tree, int position, SyntaxToken token, ITypeInferenceService typeInferrer, CancellationToken cancellationToken) + // We might be in the arguments to a parenthesized lambda + if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken) { - // Not after `new` - if (token.IsKind(SyntaxKind.NewKeyword) && token.Parent.IsKind(SyntaxKind.ObjectCreationExpression)) + if (token.Parent is not null and ParameterListSyntax) { - return false; - } - - // Typing a generic type parameter, the tree might look like a binary expression around the < token. - // If we infer a delegate type here (because that's what on the other side of the binop), - // ignore it. - if (token.Kind() == SyntaxKind.LessThanToken && token.Parent is BinaryExpressionSyntax) - { - return false; + return token.Parent.Parent is not null and ParenthesizedLambdaExpressionSyntax; } + } - // We might be in the arguments to a parenthesized lambda - if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken) - { - if (token.Parent is not null and ParameterListSyntax) - { - return token.Parent.Parent is not null and ParenthesizedLambdaExpressionSyntax; - } - } + // A lambda that is being typed may be parsed as a tuple without names + // For example, "(a, b" could be the start of either a tuple or lambda + // But "(a: b, c" cannot be a lambda + if (tree.IsPossibleTupleContext(token, position) && + token.Parent is TupleExpressionSyntax tupleExpression && + !tupleExpression.HasNames()) + { + position = token.Parent.SpanStart; + } - // A lambda that is being typed may be parsed as a tuple without names - // For example, "(a, b" could be the start of either a tuple or lambda - // But "(a: b, c" cannot be a lambda - if (tree.IsPossibleTupleContext(token, position) && - token.Parent is TupleExpressionSyntax tupleExpression && - !tupleExpression.HasNames()) - { - position = token.Parent.SpanStart; - } + // Walk up a single level to allow for typing the beginning of a lambda: + // new AssemblyLoadEventHandler(($$ + if (token.Kind() == SyntaxKind.OpenParenToken && + token.GetRequiredParent().Kind() == SyntaxKind.ParenthesizedExpression) + { + position = token.GetRequiredParent().SpanStart; + } - // Walk up a single level to allow for typing the beginning of a lambda: - // new AssemblyLoadEventHandler(($$ - if (token.Kind() == SyntaxKind.OpenParenToken && - token.GetRequiredParent().Kind() == SyntaxKind.ParenthesizedExpression) - { - position = token.GetRequiredParent().SpanStart; - } + // WorkItem 834609: Automatic brace completion inserts the closing paren, making it + // like a cast. + if (token.Kind() == SyntaxKind.OpenParenToken && + token.GetRequiredParent().Kind() == SyntaxKind.CastExpression) + { + position = token.GetRequiredParent().SpanStart; + } - // WorkItem 834609: Automatic brace completion inserts the closing paren, making it - // like a cast. - if (token.Kind() == SyntaxKind.OpenParenToken && - token.GetRequiredParent().Kind() == SyntaxKind.CastExpression) - { - position = token.GetRequiredParent().SpanStart; - } + // In the following situation, the type inferrer will infer Task to support target type preselection + // Action a = Task.$$ + // We need to explicitly exclude invocation/member access from suggestion mode + var previousToken = token.GetPreviousTokenIfTouchingWord(position); + if (previousToken.IsKind(SyntaxKind.DotToken) && + previousToken.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression)) + { + return false; + } - // In the following situation, the type inferrer will infer Task to support target type preselection - // Action a = Task.$$ - // We need to explicitly exclude invocation/member access from suggestion mode - var previousToken = token.GetPreviousTokenIfTouchingWord(position); - if (previousToken.IsKind(SyntaxKind.DotToken) && - previousToken.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression)) + // async lambda: + // Goo(async($$ + // Goo(async(p1, $$ + if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken && token.Parent.IsKind(SyntaxKind.ArgumentList) + && token.Parent.Parent is InvocationExpressionSyntax invocation + && invocation.Expression is IdentifierNameSyntax identifier) + { + if (identifier.Identifier.IsKindOrHasMatchingText(SyntaxKind.AsyncKeyword)) { - return false; + return true; } + } - // async lambda: - // Goo(async($$ - // Goo(async(p1, $$ - if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken && token.Parent.IsKind(SyntaxKind.ArgumentList) - && token.Parent.Parent is InvocationExpressionSyntax invocation - && invocation.Expression is IdentifierNameSyntax identifier) - { - if (identifier.Identifier.IsKindOrHasMatchingText(SyntaxKind.AsyncKeyword)) - { - return true; - } - } + // If we're an argument to a function with multiple overloads, + // open the builder if any overload takes a delegate at our argument position + var inferredTypeInfo = typeInferrer.GetTypeInferenceInfo(semanticModel, position, cancellationToken: cancellationToken); + return inferredTypeInfo.Any(static (type, semanticModel) => GetDelegateType(type, semanticModel.Compilation).IsDelegateType(), semanticModel); + } - // If we're an argument to a function with multiple overloads, - // open the builder if any overload takes a delegate at our argument position - var inferredTypeInfo = typeInferrer.GetTypeInferenceInfo(semanticModel, position, cancellationToken: cancellationToken); - return inferredTypeInfo.Any(static (type, semanticModel) => GetDelegateType(type, semanticModel.Compilation).IsDelegateType(), semanticModel); + private static ITypeSymbol? GetDelegateType(TypeInferenceInfo typeInferenceInfo, Compilation compilation) + { + var typeSymbol = typeInferenceInfo.InferredType; + if (typeInferenceInfo.IsParams && typeInferenceInfo.InferredType.IsArrayType()) + { + typeSymbol = ((IArrayTypeSymbol)typeInferenceInfo.InferredType).ElementType; } - private static ITypeSymbol? GetDelegateType(TypeInferenceInfo typeInferenceInfo, Compilation compilation) - { - var typeSymbol = typeInferenceInfo.InferredType; - if (typeInferenceInfo.IsParams && typeInferenceInfo.InferredType.IsArrayType()) - { - typeSymbol = ((IArrayTypeSymbol)typeInferenceInfo.InferredType).ElementType; - } + return typeSymbol.GetDelegateType(compilation); + } - return typeSymbol.GetDelegateType(compilation); + private static bool IsPotentialPatternVariableDeclaration(SyntaxToken token) + { + var patternSyntax = token.GetAncestor(); + if (patternSyntax == null) + { + return false; } - private static bool IsPotentialPatternVariableDeclaration(SyntaxToken token) + for (var current = patternSyntax; current != null; current = current.Parent as PatternSyntax) { - var patternSyntax = token.GetAncestor(); - if (patternSyntax == null) + // Patterns containing 'or' cannot contain valid variable declarations, e.g. 'e is 1 or int $$' + if (current.IsKind(SyntaxKind.OrPattern)) { return false; } - for (var current = patternSyntax; current != null; current = current.Parent as PatternSyntax) + // Patterns containing 'not' cannot be valid variable declarations, e.g. 'e is not int $$' and 'e is not (1 and int $$)' + if (current.IsKind(SyntaxKind.NotPattern)) { - // Patterns containing 'or' cannot contain valid variable declarations, e.g. 'e is 1 or int $$' - if (current.IsKind(SyntaxKind.OrPattern)) - { - return false; - } - - // Patterns containing 'not' cannot be valid variable declarations, e.g. 'e is not int $$' and 'e is not (1 and int $$)' - if (current.IsKind(SyntaxKind.NotPattern)) - { - return false; - } - } - - // e is int o$$ - // e is { P: 1 } o$$ - var lastTokenInPattern = patternSyntax.GetLastToken(); - if (lastTokenInPattern.Parent is SingleVariableDesignationSyntax variableDesignationSyntax && - token.Parent == variableDesignationSyntax) - { - return patternSyntax is DeclarationPatternSyntax or RecursivePatternSyntax; + return false; } + } - return false; + // e is int o$$ + // e is { P: 1 } o$$ + var lastTokenInPattern = patternSyntax.GetLastToken(); + if (lastTokenInPattern.Parent is SingleVariableDesignationSyntax variableDesignationSyntax && + token.Parent == variableDesignationSyntax) + { + return patternSyntax is DeclarationPatternSyntax or RecursivePatternSyntax; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/CompletionUtilities.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/CompletionUtilities.cs index 31085bebcafcc..050e5fc80acf1 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/CompletionUtilities.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/CompletionUtilities.cs @@ -13,177 +13,176 @@ using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +internal static class CompletionUtilities { - internal static class CompletionUtilities - { - internal static TextSpan GetCompletionItemSpan(SourceText text, int position) - => CommonCompletionUtilities.GetWordSpan(text, position, IsCompletionItemStartCharacter, IsWordCharacter); + internal static TextSpan GetCompletionItemSpan(SourceText text, int position) + => CommonCompletionUtilities.GetWordSpan(text, position, IsCompletionItemStartCharacter, IsWordCharacter); - public static bool IsWordStartCharacter(char ch) - => SyntaxFacts.IsIdentifierStartCharacter(ch); + public static bool IsWordStartCharacter(char ch) + => SyntaxFacts.IsIdentifierStartCharacter(ch); - public static bool IsWordCharacter(char ch) - => SyntaxFacts.IsIdentifierStartCharacter(ch) || SyntaxFacts.IsIdentifierPartCharacter(ch); + public static bool IsWordCharacter(char ch) + => SyntaxFacts.IsIdentifierStartCharacter(ch) || SyntaxFacts.IsIdentifierPartCharacter(ch); - public static bool IsCompletionItemStartCharacter(char ch) - => ch == '@' || IsWordCharacter(ch); + public static bool IsCompletionItemStartCharacter(char ch) + => ch == '@' || IsWordCharacter(ch); - public static bool TreatAsDot(SyntaxToken token, int characterPosition) - { - if (token.Kind() == SyntaxKind.DotToken) - return true; + public static bool TreatAsDot(SyntaxToken token, int characterPosition) + { + if (token.Kind() == SyntaxKind.DotToken) + return true; - // if we're right after the first dot in .. then that's considered completion on dot. - if (token.Kind() == SyntaxKind.DotDotToken && token.SpanStart == characterPosition) - return true; + // if we're right after the first dot in .. then that's considered completion on dot. + if (token.Kind() == SyntaxKind.DotDotToken && token.SpanStart == characterPosition) + return true; - return false; - } + return false; + } - public static SyntaxToken? GetDotTokenLeftOfPosition(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) - { - var tokenOnLeft = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken, includeSkipped: true); - var dotToken = tokenOnLeft.GetPreviousTokenIfTouchingWord(position); + public static SyntaxToken? GetDotTokenLeftOfPosition(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + { + var tokenOnLeft = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken, includeSkipped: true); + var dotToken = tokenOnLeft.GetPreviousTokenIfTouchingWord(position); - // Has to be a . or a .. token - if (!TreatAsDot(dotToken, position - 1)) - return null; + // Has to be a . or a .. token + if (!TreatAsDot(dotToken, position - 1)) + return null; - // don't want to trigger after a number. All other cases after dot are ok. - if (dotToken.GetPreviousToken().Kind() == SyntaxKind.NumericLiteralToken) - return null; + // don't want to trigger after a number. All other cases after dot are ok. + if (dotToken.GetPreviousToken().Kind() == SyntaxKind.NumericLiteralToken) + return null; - return dotToken; + return dotToken; + } + + internal static bool IsTriggerCharacter(SourceText text, int characterPosition, in CompletionOptions options) + { + var ch = text[characterPosition]; + + // Trigger off of a normal `.`, but not off of `..` + if (ch == '.' && !(characterPosition >= 1 && text[characterPosition - 1] == '.')) + { + return true; } - internal static bool IsTriggerCharacter(SourceText text, int characterPosition, in CompletionOptions options) + // Trigger for directive + if (ch == '#') { - var ch = text[characterPosition]; + return true; + } - // Trigger off of a normal `.`, but not off of `..` - if (ch == '.' && !(characterPosition >= 1 && text[characterPosition - 1] == '.')) - { - return true; - } + // Trigger on pointer member access + if (ch == '>' && characterPosition >= 1 && text[characterPosition - 1] == '-') + { + return true; + } - // Trigger for directive - if (ch == '#') - { - return true; - } + // Trigger on alias name + if (ch == ':' && characterPosition >= 1 && text[characterPosition - 1] == ':') + { + return true; + } - // Trigger on pointer member access - if (ch == '>' && characterPosition >= 1 && text[characterPosition - 1] == '-') - { - return true; - } + if (options.TriggerOnTypingLetters && IsStartingNewWord(text, characterPosition)) + { + return true; + } - // Trigger on alias name - if (ch == ':' && characterPosition >= 1 && text[characterPosition - 1] == ':') - { - return true; - } + return false; + } - if (options.TriggerOnTypingLetters && IsStartingNewWord(text, characterPosition)) - { - return true; - } + /// + /// Tells if we are in positions like this: #nullable $$ or #pragma warning $$ + /// + internal static bool IsCompilerDirectiveTriggerCharacter(SourceText text, int characterPosition) + { + while (text[characterPosition] == ' ' || + char.IsLetter(text[characterPosition])) + { + characterPosition--; - return false; + if (characterPosition < 0) + return false; } - /// - /// Tells if we are in positions like this: #nullable $$ or #pragma warning $$ - /// - internal static bool IsCompilerDirectiveTriggerCharacter(SourceText text, int characterPosition) - { - while (text[characterPosition] == ' ' || - char.IsLetter(text[characterPosition])) - { - characterPosition--; + return text[characterPosition] == '#'; + } - if (characterPosition < 0) - return false; - } + internal static ImmutableHashSet CommonTriggerCharacters { get; } = ['.', '#', '>', ':']; - return text[characterPosition] == '#'; - } + internal static ImmutableHashSet CommonTriggerCharactersWithArgumentList { get; } = ['.', '#', '>', ':', '(', '[', ' ']; - internal static ImmutableHashSet CommonTriggerCharacters { get; } = ['.', '#', '>', ':']; + internal static bool IsTriggerCharacterOrArgumentListCharacter(SourceText text, int characterPosition, in CompletionOptions options) + => IsTriggerCharacter(text, characterPosition, options) || IsArgumentListCharacter(text, characterPosition); - internal static ImmutableHashSet CommonTriggerCharactersWithArgumentList { get; } = ['.', '#', '>', ':', '(', '[', ' ']; + private static bool IsArgumentListCharacter(SourceText text, int characterPosition) + => IsArgumentListCharacter(text[characterPosition]); - internal static bool IsTriggerCharacterOrArgumentListCharacter(SourceText text, int characterPosition, in CompletionOptions options) - => IsTriggerCharacter(text, characterPosition, options) || IsArgumentListCharacter(text, characterPosition); + internal static bool IsArgumentListCharacter(char ch) + => ch is '(' or '[' or ' '; - private static bool IsArgumentListCharacter(SourceText text, int characterPosition) - => IsArgumentListCharacter(text[characterPosition]); + internal static bool IsTriggerAfterSpaceOrStartOfWordCharacter(SourceText text, int characterPosition, in CompletionOptions options) + { + // Bring up on space or at the start of a word. + var ch = text[characterPosition]; + return SpaceTypedNotBeforeWord(ch, text, characterPosition) || + (IsStartingNewWord(text, characterPosition) && options.TriggerOnTypingLetters); + } - internal static bool IsArgumentListCharacter(char ch) - => ch is '(' or '[' or ' '; + internal static ImmutableHashSet SpaceTriggerCharacter => [' ']; - internal static bool IsTriggerAfterSpaceOrStartOfWordCharacter(SourceText text, int characterPosition, in CompletionOptions options) - { - // Bring up on space or at the start of a word. - var ch = text[characterPosition]; - return SpaceTypedNotBeforeWord(ch, text, characterPosition) || - (IsStartingNewWord(text, characterPosition) && options.TriggerOnTypingLetters); - } + private static bool SpaceTypedNotBeforeWord(char ch, SourceText text, int characterPosition) + => ch == ' ' && (characterPosition == text.Length - 1 || !IsWordStartCharacter(text[characterPosition + 1])); - internal static ImmutableHashSet SpaceTriggerCharacter => [' ']; + public static bool IsStartingNewWord(SourceText text, int characterPosition) + { + return CommonCompletionUtilities.IsStartingNewWord( + text, characterPosition, IsWordStartCharacter, IsWordCharacter); + } - private static bool SpaceTypedNotBeforeWord(char ch, SourceText text, int characterPosition) - => ch == ' ' && (characterPosition == text.Length - 1 || !IsWordStartCharacter(text[characterPosition + 1])); + public static (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText( + ISymbol symbol, SyntaxContext context) + { + var insertionText = GetInsertionText(symbol, context); + var suffix = symbol.GetArity() == 0 ? "" : "<>"; - public static bool IsStartingNewWord(SourceText text, int characterPosition) - { - return CommonCompletionUtilities.IsStartingNewWord( - text, characterPosition, IsWordStartCharacter, IsWordCharacter); - } + return (insertionText, suffix, insertionText); + } - public static (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText( - ISymbol symbol, SyntaxContext context) + public static string GetInsertionText(ISymbol symbol, SyntaxContext context) + { + if (CommonCompletionUtilities.TryRemoveAttributeSuffix(symbol, context, out var name)) { - var insertionText = GetInsertionText(symbol, context); - var suffix = symbol.GetArity() == 0 ? "" : "<>"; - - return (insertionText, suffix, insertionText); + // Cannot escape Attribute name with the suffix removed. Only use the name with + // the suffix removed if it does not need to be escaped. + if (name.Equals(name.EscapeIdentifier())) + { + return name; + } } - public static string GetInsertionText(ISymbol symbol, SyntaxContext context) + if (symbol.Kind == SymbolKind.Label && + symbol.DeclaringSyntaxReferences[0].GetSyntax().Kind() == SyntaxKind.DefaultSwitchLabel) { - if (CommonCompletionUtilities.TryRemoveAttributeSuffix(symbol, context, out var name)) - { - // Cannot escape Attribute name with the suffix removed. Only use the name with - // the suffix removed if it does not need to be escaped. - if (name.Equals(name.EscapeIdentifier())) - { - return name; - } - } + return symbol.Name; + } - if (symbol.Kind == SymbolKind.Label && - symbol.DeclaringSyntaxReferences[0].GetSyntax().Kind() == SyntaxKind.DefaultSwitchLabel) - { - return symbol.Name; - } + return symbol.Name.EscapeIdentifier(isQueryContext: context.IsInQuery); + } - return symbol.Name.EscapeIdentifier(isQueryContext: context.IsInQuery); + public static int GetTargetCaretPositionForMethod(MethodDeclarationSyntax methodDeclaration) + { + if (methodDeclaration.Body == null) + { + return methodDeclaration.GetLocation().SourceSpan.End; } - - public static int GetTargetCaretPositionForMethod(MethodDeclarationSyntax methodDeclaration) + else { - if (methodDeclaration.Body == null) - { - return methodDeclaration.GetLocation().SourceSpan.End; - } - else - { - // move to the end of the last statement in the method - var lastStatement = methodDeclaration.Body.Statements.Last(); - return lastStatement.GetLocation().SourceSpan.End; - } + // move to the end of the last statement in the method + var lastStatement = methodDeclaration.Body.Statements.Last(); + return lastStatement.GetLocation().SourceSpan.End; } } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/CrefCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/CrefCompletionProvider.cs index 8c36caba85bde..aa5e0c25faf17 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/CrefCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/CrefCompletionProvider.cs @@ -24,374 +24,373 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(CrefCompletionProvider), LanguageNames.CSharp), Shared] +[ExtensionOrder(After = nameof(EnumAndCompletionListTagCompletionProvider))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CrefCompletionProvider() : AbstractCrefCompletionProvider { - [ExportCompletionProvider(nameof(CrefCompletionProvider), LanguageNames.CSharp), Shared] - [ExtensionOrder(After = nameof(EnumAndCompletionListTagCompletionProvider))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class CrefCompletionProvider() : AbstractCrefCompletionProvider - { - private static readonly SymbolDisplayFormat QualifiedCrefFormat = - new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, - propertyStyle: SymbolDisplayPropertyStyle.NameOnly, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - parameterOptions: SymbolDisplayParameterOptions.None, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.ExpandValueTuple); + private static readonly SymbolDisplayFormat QualifiedCrefFormat = + new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, + propertyStyle: SymbolDisplayPropertyStyle.NameOnly, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + parameterOptions: SymbolDisplayParameterOptions.None, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.ExpandValueTuple); - private static readonly SymbolDisplayFormat CrefFormat = - QualifiedCrefFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + private static readonly SymbolDisplayFormat CrefFormat = + QualifiedCrefFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - private static readonly SymbolDisplayFormat MinimalParameterTypeFormat = - SymbolDisplayFormat.MinimallyQualifiedFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.ExpandValueTuple); + private static readonly SymbolDisplayFormat MinimalParameterTypeFormat = + SymbolDisplayFormat.MinimallyQualifiedFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.ExpandValueTuple); - private Action? _testSpeculativeNodeCallback; + private Action? _testSpeculativeNodeCallback; - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; - public override async Task ProvideCompletionsAsync(CompletionContext context) + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - try - { - var document = context.Document; - var position = context.Position; - var options = context.CompletionOptions; - var cancellationToken = context.CancellationToken; + var document = context.Document; + var position = context.Position; + var options = context.CompletionOptions; + var cancellationToken = context.CancellationToken; - var (token, semanticModel, symbols) = await GetSymbolsAsync(document, position, options, cancellationToken).ConfigureAwait(false); + var (token, semanticModel, symbols) = await GetSymbolsAsync(document, position, options, cancellationToken).ConfigureAwait(false); - if (symbols.IsDefaultOrEmpty) - return; + if (symbols.IsDefaultOrEmpty) + return; - Contract.ThrowIfNull(semanticModel); + Contract.ThrowIfNull(semanticModel); - context.IsExclusive = true; + context.IsExclusive = true; - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var span = GetCompletionItemSpan(text, position); - var serializedOptions = ImmutableArray.Create(new KeyValuePair(HideAdvancedMembers, options.HideAdvancedMembers.ToString())); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var span = GetCompletionItemSpan(text, position); + var serializedOptions = ImmutableArray.Create(new KeyValuePair(HideAdvancedMembers, options.HideAdvancedMembers.ToString())); - var items = CreateCompletionItems(semanticModel, symbols, token, position, serializedOptions); + var items = CreateCompletionItems(semanticModel, symbols, token, position, serializedOptions); - context.AddItems(items); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) - { - } + context.AddItems(items); } - - protected override async Task<(SyntaxToken, SemanticModel?, ImmutableArray)> GetSymbolsAsync( - Document document, int position, CompletionOptions options, CancellationToken cancellationToken) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) { - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (!tree.IsEntirelyWithinCrefSyntax(position, cancellationToken)) - return default; + } + } - var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDocumentationComments: true) - .GetPreviousTokenIfTouchingWord(position); + protected override async Task<(SyntaxToken, SemanticModel?, ImmutableArray)> GetSymbolsAsync( + Document document, int position, CompletionOptions options, CancellationToken cancellationToken) + { + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (!tree.IsEntirelyWithinCrefSyntax(position, cancellationToken)) + return default; - // To get a Speculative SemanticModel (which is much faster), we need to - // walk up to the node the DocumentationTrivia is attached to. - var parentNode = token.Parent?.FirstAncestorOrSelf()?.ParentTrivia.Token.Parent; - _testSpeculativeNodeCallback?.Invoke(parentNode); - if (parentNode == null) - return default; + var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDocumentationComments: true) + .GetPreviousTokenIfTouchingWord(position); - var semanticModel = await document.ReuseExistingSpeculativeModelAsync( - parentNode, cancellationToken).ConfigureAwait(false); + // To get a Speculative SemanticModel (which is much faster), we need to + // walk up to the node the DocumentationTrivia is attached to. + var parentNode = token.Parent?.FirstAncestorOrSelf()?.ParentTrivia.Token.Parent; + _testSpeculativeNodeCallback?.Invoke(parentNode); + if (parentNode == null) + return default; - var symbols = GetSymbols(token, semanticModel, cancellationToken) - .FilterToVisibleAndBrowsableSymbols(options.HideAdvancedMembers, semanticModel.Compilation); + var semanticModel = await document.ReuseExistingSpeculativeModelAsync( + parentNode, cancellationToken).ConfigureAwait(false); - return (token, semanticModel, symbols); - } + var symbols = GetSymbols(token, semanticModel, cancellationToken) + .FilterToVisibleAndBrowsableSymbols(options.HideAdvancedMembers, semanticModel.Compilation); - private static bool IsCrefStartContext(SyntaxToken token) - { - // cases: - // (); - if (typeDeclaration != null) + return []; + } + + private static ImmutableArray GetUnqualifiedSymbols( + SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var result); + result.AddRange(semanticModel.LookupSymbols(token.SpanStart)); + + // LookupSymbols doesn't return indexers or operators because they can't be referred to by name. + // So, try to find the innermost type declaration and return its operators and indexers + var typeDeclaration = token.Parent?.FirstAncestorOrSelf(); + if (typeDeclaration != null) + { + var type = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken); + if (type != null) { - var type = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken); - if (type != null) + foreach (var baseType in type.GetBaseTypesAndThis()) { - foreach (var baseType in type.GetBaseTypesAndThis()) + foreach (var member in baseType.GetMembers()) { - foreach (var member in baseType.GetMembers()) + if ((member.IsIndexer() || member.IsUserDefinedOperator()) && + member.IsAccessibleWithin(type)) { - if ((member.IsIndexer() || member.IsUserDefinedOperator()) && - member.IsAccessibleWithin(type)) - { - result.Add(member); - } + result.Add(member); } } } } - - return result.ToImmutableAndClear(); } - private static ImmutableArray GetQualifiedSymbols( - QualifiedCrefSyntax parent, SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken) - { - var leftType = semanticModel.GetTypeInfo(parent.Container, cancellationToken).Type; - var leftSymbol = semanticModel.GetSymbolInfo(parent.Container, cancellationToken).Symbol; + return result.ToImmutableAndClear(); + } - var container = (leftSymbol ?? leftType) as INamespaceOrTypeSymbol; + private static ImmutableArray GetQualifiedSymbols( + QualifiedCrefSyntax parent, SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken) + { + var leftType = semanticModel.GetTypeInfo(parent.Container, cancellationToken).Type; + var leftSymbol = semanticModel.GetSymbolInfo(parent.Container, cancellationToken).Symbol; - using var _ = ArrayBuilder.GetInstance(out var result); - result.AddRange(semanticModel.LookupSymbols(token.SpanStart, container)); + var container = (leftSymbol ?? leftType) as INamespaceOrTypeSymbol; - if (container is INamedTypeSymbol namedTypeContainer) - result.AddRange(namedTypeContainer.InstanceConstructors); + using var _ = ArrayBuilder.GetInstance(out var result); + result.AddRange(semanticModel.LookupSymbols(token.SpanStart, container)); - return result.ToImmutableAndClear(); - } + if (container is INamedTypeSymbol namedTypeContainer) + result.AddRange(namedTypeContainer.InstanceConstructors); - private static TextSpan GetCompletionItemSpan(SourceText text, int position) - { - return CommonCompletionUtilities.GetWordSpan( - text, - position, - ch => CompletionUtilities.IsCompletionItemStartCharacter(ch) || ch == '{', - ch => CompletionUtilities.IsWordCharacter(ch) || ch is '{' or '}'); - } + return result.ToImmutableAndClear(); + } - private static IEnumerable CreateCompletionItems( - SemanticModel semanticModel, ImmutableArray symbols, SyntaxToken token, int position, ImmutableArray> options) - { - using var _ = SharedPools.Default().GetPooledObject(out var builder); + private static TextSpan GetCompletionItemSpan(SourceText text, int position) + { + return CommonCompletionUtilities.GetWordSpan( + text, + position, + ch => CompletionUtilities.IsCompletionItemStartCharacter(ch) || ch == '{', + ch => CompletionUtilities.IsWordCharacter(ch) || ch is '{' or '}'); + } - foreach (var group in symbols.GroupBy(s => s.Name)) - { - var groupCount = group.Count(); - foreach (var symbol in group.OrderBy(s => s.GetArity())) - { - // Include the arity in the sort text so that we show types/methods from least arity to most arity. - var sortText = $"{symbol.Name}`{symbol.GetArity():000}"; - - // For every symbol, we create an item that uses the regular CrefFormat, - // which uses intrinsic type keywords - yield return CreateItem(semanticModel, symbol, groupCount, token, position, builder, sortText, options, CrefFormat); - if (TryCreateSpecialTypeItem(semanticModel, symbol, token, position, builder, options, out var item)) - yield return item; - } - } - } + private static IEnumerable CreateCompletionItems( + SemanticModel semanticModel, ImmutableArray symbols, SyntaxToken token, int position, ImmutableArray> options) + { + using var _ = SharedPools.Default().GetPooledObject(out var builder); - private static bool TryCreateSpecialTypeItem( - SemanticModel semanticModel, ISymbol symbol, SyntaxToken token, int position, StringBuilder builder, - ImmutableArray> options, [NotNullWhen(true)] out CompletionItem? item) + foreach (var group in symbols.GroupBy(s => s.Name)) { - // If the type is a SpecialType, create an additional item using - // its actual name (as opposed to intrinsic type keyword) - var typeSymbol = symbol as ITypeSymbol; - if (typeSymbol.IsSpecialType()) + var groupCount = group.Count(); + foreach (var symbol in group.OrderBy(s => s.GetArity())) { - item = CreateItem(semanticModel, symbol, groupCount: 1, token, position, builder, builder.ToString(), options, QualifiedCrefFormat); - return true; + // Include the arity in the sort text so that we show types/methods from least arity to most arity. + var sortText = $"{symbol.Name}`{symbol.GetArity():000}"; + + // For every symbol, we create an item that uses the regular CrefFormat, + // which uses intrinsic type keywords + yield return CreateItem(semanticModel, symbol, groupCount, token, position, builder, sortText, options, CrefFormat); + if (TryCreateSpecialTypeItem(semanticModel, symbol, token, position, builder, options, out var item)) + yield return item; } + } + } - item = null; - return false; + private static bool TryCreateSpecialTypeItem( + SemanticModel semanticModel, ISymbol symbol, SyntaxToken token, int position, StringBuilder builder, + ImmutableArray> options, [NotNullWhen(true)] out CompletionItem? item) + { + // If the type is a SpecialType, create an additional item using + // its actual name (as opposed to intrinsic type keyword) + var typeSymbol = symbol as ITypeSymbol; + if (typeSymbol.IsSpecialType()) + { + item = CreateItem(semanticModel, symbol, groupCount: 1, token, position, builder, builder.ToString(), options, QualifiedCrefFormat); + return true; } - private static CompletionItem CreateItem( - SemanticModel semanticModel, - ISymbol symbol, - int groupCount, - SyntaxToken token, - int position, - StringBuilder builder, - string sortText, - ImmutableArray> options, - SymbolDisplayFormat unqualifiedCrefFormat) + item = null; + return false; + } + + private static CompletionItem CreateItem( + SemanticModel semanticModel, + ISymbol symbol, + int groupCount, + SyntaxToken token, + int position, + StringBuilder builder, + string sortText, + ImmutableArray> options, + SymbolDisplayFormat unqualifiedCrefFormat) + { + builder.Clear(); + if (symbol is INamespaceOrTypeSymbol && token.IsKind(SyntaxKind.DotToken)) { - builder.Clear(); - if (symbol is INamespaceOrTypeSymbol && token.IsKind(SyntaxKind.DotToken)) - { - // Handle qualified namespace and type names. - builder.Append(symbol.ToDisplayString(QualifiedCrefFormat)); - } - else - { - // Handle unqualified namespace and type names, or member names. + // Handle qualified namespace and type names. + builder.Append(symbol.ToDisplayString(QualifiedCrefFormat)); + } + else + { + // Handle unqualified namespace and type names, or member names. - builder.Append(symbol.ToMinimalDisplayString(semanticModel, token.SpanStart, unqualifiedCrefFormat)); + builder.Append(symbol.ToMinimalDisplayString(semanticModel, token.SpanStart, unqualifiedCrefFormat)); - var parameters = symbol.GetParameters(); + var parameters = symbol.GetParameters(); - // if this has parameters, then add them here. Otherwise, if this is a method without parameters, but - // there are overloads of it, then also add the parameters to disambiguate. - if (parameters.Length > 0 || - (symbol is IMethodSymbol && groupCount >= 2)) - { - // Note: we intentionally don't add the "params" modifier for any parameters. + // if this has parameters, then add them here. Otherwise, if this is a method without parameters, but + // there are overloads of it, then also add the parameters to disambiguate. + if (parameters.Length > 0 || + (symbol is IMethodSymbol && groupCount >= 2)) + { + // Note: we intentionally don't add the "params" modifier for any parameters. - builder.Append(symbol.IsIndexer() ? '[' : '('); - builder.AppendJoinedValues(", ", parameters, - (p, builder) => + builder.Append(symbol.IsIndexer() ? '[' : '('); + builder.AppendJoinedValues(", ", parameters, + (p, builder) => + { + builder.Append(p.RefKind switch { - builder.Append(p.RefKind switch - { - RefKind.Ref => "ref ", - RefKind.Out => "out ", - RefKind.In => "in ", - RefKind.RefReadOnlyParameter => "ref readonly ", - _ => "", - }); - builder.Append(p.Type.ToMinimalDisplayString(semanticModel, position, MinimalParameterTypeFormat)); + RefKind.Ref => "ref ", + RefKind.Out => "out ", + RefKind.In => "in ", + RefKind.RefReadOnlyParameter => "ref readonly ", + _ => "", }); - builder.Append(symbol.IsIndexer() ? ']' : ')'); - } + builder.Append(p.Type.ToMinimalDisplayString(semanticModel, position, MinimalParameterTypeFormat)); + }); + builder.Append(symbol.IsIndexer() ? ']' : ')'); } - - return CreateItemFromBuilder(symbol, position, builder, sortText, options); } - private static CompletionItem CreateItemFromBuilder( - ISymbol symbol, int position, StringBuilder builder, string sortText, ImmutableArray> options) - { - var insertionText = builder - .Replace('<', '{') - .Replace('>', '}') - .ToString(); - - return SymbolCompletionItem.CreateWithNameAndKind( - displayText: insertionText, - displayTextSuffix: "", - insertionText: insertionText, - symbols: ImmutableArray.Create(symbol), - contextPosition: position, - sortText: sortText, - filterText: insertionText, - properties: options, - rules: GetRules(insertionText)); - } + return CreateItemFromBuilder(symbol, position, builder, sortText, options); + } - private static readonly CharacterSetModificationRule s_WithoutOpenBrace = CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '{'); - private static readonly CharacterSetModificationRule s_WithoutOpenParen = CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '('); + private static CompletionItem CreateItemFromBuilder( + ISymbol symbol, int position, StringBuilder builder, string sortText, ImmutableArray> options) + { + var insertionText = builder + .Replace('<', '{') + .Replace('>', '}') + .ToString(); + + return SymbolCompletionItem.CreateWithNameAndKind( + displayText: insertionText, + displayTextSuffix: "", + insertionText: insertionText, + symbols: ImmutableArray.Create(symbol), + contextPosition: position, + sortText: sortText, + filterText: insertionText, + properties: options, + rules: GetRules(insertionText)); + } - private static CompletionItemRules GetRules(string displayText) - { - var commitRules = ImmutableArray.Empty; + private static readonly CharacterSetModificationRule s_WithoutOpenBrace = CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '{'); + private static readonly CharacterSetModificationRule s_WithoutOpenParen = CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '('); - if (displayText.Contains("{")) - { - commitRules = commitRules.Add(s_WithoutOpenBrace); - } + private static CompletionItemRules GetRules(string displayText) + { + var commitRules = ImmutableArray.Empty; - if (displayText.Contains("(")) - { - commitRules = commitRules.Add(s_WithoutOpenParen); - } + if (displayText.Contains("{")) + { + commitRules = commitRules.Add(s_WithoutOpenBrace); + } - if (commitRules.IsEmpty) - { - return CompletionItemRules.Default; - } - else - { - return CompletionItemRules.Default.WithCommitCharacterRules(commitRules); - } + if (displayText.Contains("(")) + { + commitRules = commitRules.Add(s_WithoutOpenParen); } - protected override Task GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + if (commitRules.IsEmpty) { - if (!SymbolCompletionItem.TryGetInsertionText(selectedItem, out var insertionText)) - { - insertionText = selectedItem.DisplayText; - } + return CompletionItemRules.Default; + } + else + { + return CompletionItemRules.Default.WithCommitCharacterRules(commitRules); + } + } - return Task.FromResult(new TextChange(selectedItem.Span, insertionText)); + protected override Task GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + { + if (!SymbolCompletionItem.TryGetInsertionText(selectedItem, out var insertionText)) + { + insertionText = selectedItem.DisplayText; } - internal TestAccessor GetTestAccessor() - => new(this); + return Task.FromResult(new TextChange(selectedItem.Span, insertionText)); + } - internal readonly struct TestAccessor(CrefCompletionProvider crefCompletionProvider) - { - private readonly CrefCompletionProvider _crefCompletionProvider = crefCompletionProvider; + internal TestAccessor GetTestAccessor() + => new(this); - public void SetSpeculativeNodeCallback(Action value) - => _crefCompletionProvider._testSpeculativeNodeCallback = value; - } + internal readonly struct TestAccessor(CrefCompletionProvider crefCompletionProvider) + { + private readonly CrefCompletionProvider _crefCompletionProvider = crefCompletionProvider; + + public void SetSpeculativeNodeCallback(Action value) + => _crefCompletionProvider._testSpeculativeNodeCallback = value; } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameCompletionProvider.cs index d2504423cbd6c..e6f199978569a 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameCompletionProvider.cs @@ -21,94 +21,93 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(DeclarationNameCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(TupleNameCompletionProvider))] +[Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal partial class DeclarationNameCompletionProvider([ImportMany] IEnumerable> recommenders) : LSPCompletionProvider { - [ExportCompletionProvider(nameof(DeclarationNameCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(TupleNameCompletionProvider))] - [Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal partial class DeclarationNameCompletionProvider([ImportMany] IEnumerable> recommenders) : LSPCompletionProvider - { - private ImmutableArray> Recommenders { get; } = ExtensionOrderer.Order(recommenders).ToImmutableArray(); + private ImmutableArray> Recommenders { get; } = ExtensionOrderer.Order(recommenders).ToImmutableArray(); - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, insertedCharacterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, insertedCharacterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; - public override async Task ProvideCompletionsAsync(CompletionContext completionContext) + public override async Task ProvideCompletionsAsync(CompletionContext completionContext) + { + try { - try + var position = completionContext.Position; + var document = completionContext.Document; + var cancellationToken = completionContext.CancellationToken; + + if (!completionContext.CompletionOptions.ShowNameSuggestions) { - var position = completionContext.Position; - var document = completionContext.Document; - var cancellationToken = completionContext.CancellationToken; + return; + } - if (!completionContext.CompletionOptions.ShowNameSuggestions) - { - return; - } + var context = (CSharpSyntaxContext)await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); + if (context.IsInNonUserCode) + { + return; + } - var context = (CSharpSyntaxContext)await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); - if (context.IsInNonUserCode) + // Do not show name suggestions for unbound "async" or "yield" identifier. + // Most likely user is using it as keyword, so name suggestion will just interfere them + if (context.TargetToken.IsKindOrHasMatchingText(SyntaxKind.AsyncKeyword) || + context.TargetToken.IsKindOrHasMatchingText(SyntaxKind.YieldKeyword)) + { + if (context.SemanticModel.GetSymbolInfo(context.TargetToken).GetAnySymbol() is null) { return; } + } - // Do not show name suggestions for unbound "async" or "yield" identifier. - // Most likely user is using it as keyword, so name suggestion will just interfere them - if (context.TargetToken.IsKindOrHasMatchingText(SyntaxKind.AsyncKeyword) || - context.TargetToken.IsKindOrHasMatchingText(SyntaxKind.YieldKeyword)) - { - if (context.SemanticModel.GetSymbolInfo(context.TargetToken).GetAnySymbol() is null) - { - return; - } - } + var nameInfo = await NameDeclarationInfo.GetDeclarationInfoAsync(document, position, cancellationToken).ConfigureAwait(false); + using var _ = PooledHashSet.GetInstance(out var suggestedNames); - var nameInfo = await NameDeclarationInfo.GetDeclarationInfoAsync(document, position, cancellationToken).ConfigureAwait(false); - using var _ = PooledHashSet.GetInstance(out var suggestedNames); + var sortValue = 0; + foreach (var recommender in Recommenders) + { + var names = await recommender.Value.ProvideRecommendedNamesAsync(completionContext, document, context, nameInfo, cancellationToken).ConfigureAwait(false); - var sortValue = 0; - foreach (var recommender in Recommenders) + foreach (var (name, glyph) in names) { - var names = await recommender.Value.ProvideRecommendedNamesAsync(completionContext, document, context, nameInfo, cancellationToken).ConfigureAwait(false); - - foreach (var (name, glyph) in names) + if (suggestedNames.Add(name)) { - if (suggestedNames.Add(name)) - { - // We've produced items in the desired order, add a sort text to each item to prevent alphabetization - completionContext.AddItem(CreateCompletionItem(name, glyph, sortValue.ToString("D8"))); - sortValue++; - } + // We've produced items in the desired order, add a sort text to each item to prevent alphabetization + completionContext.AddItem(CreateCompletionItem(name, glyph, sortValue.ToString("D8"))); + sortValue++; } } + } - if (suggestedNames.Count == 0) - return; + if (suggestedNames.Count == 0) + return; - completionContext.SuggestionModeItem = CommonCompletionItem.Create( - CSharpFeaturesResources.Name, displayTextSuffix: "", CompletionItemRules.Default); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) - { - // nop - } + completionContext.SuggestionModeItem = CommonCompletionItem.Create( + CSharpFeaturesResources.Name, displayTextSuffix: "", CompletionItemRules.Default); } - - private static CompletionItem CreateCompletionItem(string name, Glyph glyph, string sortText) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) { - return CommonCompletionItem.Create( - name, - displayTextSuffix: "", - CompletionItemRules.Default, - glyph: glyph, - sortText: sortText, - description: CSharpFeaturesResources.Suggested_name.ToSymbolDisplayParts()); + // nop } } + + private static CompletionItem CreateCompletionItem(string name, Glyph glyph, string sortText) + { + return CommonCompletionItem.Create( + name, + displayTextSuffix: "", + CompletionItemRules.Default, + glyph: glyph, + sortText: sortText, + description: CSharpFeaturesResources.Suggested_name.ToSymbolDisplayParts()); + } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameInfo.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameInfo.cs index 74b68da8f5824..c5a71ac74e662 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameInfo.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameInfo.cs @@ -14,657 +14,656 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles.SymbolSpecification; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers.DeclarationName +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers.DeclarationName; + +internal readonly struct NameDeclarationInfo( + ImmutableArray possibleSymbolKinds, + Accessibility? accessibility, + DeclarationModifiers declarationModifiers = default, + ITypeSymbol? type = null, + IAliasSymbol? alias = null, + ISymbol? symbol = null) { - internal readonly struct NameDeclarationInfo( - ImmutableArray possibleSymbolKinds, - Accessibility? accessibility, - DeclarationModifiers declarationModifiers = default, - ITypeSymbol? type = null, - IAliasSymbol? alias = null, - ISymbol? symbol = null) - { - private static readonly ImmutableArray s_parameterSyntaxKind = - [new SymbolKindOrTypeKind(SymbolKind.Parameter)]; + private static readonly ImmutableArray s_parameterSyntaxKind = + [new SymbolKindOrTypeKind(SymbolKind.Parameter)]; - private static readonly ImmutableArray s_propertySyntaxKind = - [new SymbolKindOrTypeKind(SymbolKind.Property)]; + private static readonly ImmutableArray s_propertySyntaxKind = + [new SymbolKindOrTypeKind(SymbolKind.Property)]; - private readonly ImmutableArray _possibleSymbolKinds = possibleSymbolKinds; + private readonly ImmutableArray _possibleSymbolKinds = possibleSymbolKinds; - public readonly DeclarationModifiers Modifiers = declarationModifiers; - public readonly Accessibility? DeclaredAccessibility = accessibility; + public readonly DeclarationModifiers Modifiers = declarationModifiers; + public readonly Accessibility? DeclaredAccessibility = accessibility; - public readonly ITypeSymbol? Type = type; - public readonly IAliasSymbol? Alias = alias; - public readonly ISymbol? Symbol = symbol; + public readonly ITypeSymbol? Type = type; + public readonly IAliasSymbol? Alias = alias; + public readonly ISymbol? Symbol = symbol; - public ImmutableArray PossibleSymbolKinds => _possibleSymbolKinds.NullToEmpty(); + public ImmutableArray PossibleSymbolKinds => _possibleSymbolKinds.NullToEmpty(); - public static async Task GetDeclarationInfoAsync(Document document, int position, CancellationToken cancellationToken) + public static async Task GetDeclarationInfoAsync(Document document, int position, CancellationToken cancellationToken) + { + var info = await GetDeclarationInfoWorkerAsync(document, position, cancellationToken).ConfigureAwait(false); + + // if we bound to some error type, and that error type itself didn't start with an uppercase letter, then + // it's almost certainly just an error case where the user was referencing something that was not a type. + // for example: + // + // goo $$ + // goo = ... + // + // This syntactically looks like a type, but really isn't. We don't want to offer anything here as it's far + // more likely to be an error rather than a true new declaration. + if (info.Type is IErrorTypeSymbol { Name.Length: > 0 } && + !char.IsUpper(info.Type.Name[0])) { - var info = await GetDeclarationInfoWorkerAsync(document, position, cancellationToken).ConfigureAwait(false); - - // if we bound to some error type, and that error type itself didn't start with an uppercase letter, then - // it's almost certainly just an error case where the user was referencing something that was not a type. - // for example: - // - // goo $$ - // goo = ... - // - // This syntactically looks like a type, but really isn't. We don't want to offer anything here as it's far - // more likely to be an error rather than a true new declaration. - if (info.Type is IErrorTypeSymbol { Name.Length: > 0 } && - !char.IsUpper(info.Type.Name[0])) - { - return default; - } - - return info; + return default; } - private static async Task GetDeclarationInfoWorkerAsync(Document document, int position, CancellationToken cancellationToken) - { - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken).GetPreviousTokenIfTouchingWord(position); - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(token.SpanStart, cancellationToken).ConfigureAwait(false); - var typeInferenceService = document.GetRequiredLanguageService(); - - if (IsTupleTypeElement(token, semanticModel, cancellationToken, out var result) - || IsPrimaryConstructorParameter(token, semanticModel, cancellationToken, out result) - || IsParameterDeclaration(token, semanticModel, cancellationToken, out result) - || IsTypeParameterDeclaration(token, out result) - || IsLocalFunctionDeclaration(token, semanticModel, cancellationToken, out result) - || IsLocalVariableDeclaration(token, semanticModel, cancellationToken, out result) - || IsEmbeddedVariableDeclaration(token, semanticModel, cancellationToken, out result) - || IsForEachVariableDeclaration(token, semanticModel, cancellationToken, out result) - || IsIncompleteMemberDeclaration(token, semanticModel, cancellationToken, out result) - || IsFieldDeclaration(token, semanticModel, cancellationToken, out result) - || IsMethodDeclaration(token, semanticModel, cancellationToken, out result) - || IsPropertyDeclaration(token, semanticModel, cancellationToken, out result) - || IsPossibleOutVariableDeclaration(token, semanticModel, typeInferenceService, cancellationToken, out result) - || IsTupleLiteralElement(token, semanticModel, cancellationToken, out result) - || IsPossibleLocalVariableOrFunctionDeclaration(token, semanticModel, cancellationToken, out result) - || IsPatternMatching(token, semanticModel, cancellationToken, out result)) - { - return result; - } + return info; + } - return default; + private static async Task GetDeclarationInfoWorkerAsync(Document document, int position, CancellationToken cancellationToken) + { + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken).GetPreviousTokenIfTouchingWord(position); + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(token.SpanStart, cancellationToken).ConfigureAwait(false); + var typeInferenceService = document.GetRequiredLanguageService(); + + if (IsTupleTypeElement(token, semanticModel, cancellationToken, out var result) + || IsPrimaryConstructorParameter(token, semanticModel, cancellationToken, out result) + || IsParameterDeclaration(token, semanticModel, cancellationToken, out result) + || IsTypeParameterDeclaration(token, out result) + || IsLocalFunctionDeclaration(token, semanticModel, cancellationToken, out result) + || IsLocalVariableDeclaration(token, semanticModel, cancellationToken, out result) + || IsEmbeddedVariableDeclaration(token, semanticModel, cancellationToken, out result) + || IsForEachVariableDeclaration(token, semanticModel, cancellationToken, out result) + || IsIncompleteMemberDeclaration(token, semanticModel, cancellationToken, out result) + || IsFieldDeclaration(token, semanticModel, cancellationToken, out result) + || IsMethodDeclaration(token, semanticModel, cancellationToken, out result) + || IsPropertyDeclaration(token, semanticModel, cancellationToken, out result) + || IsPossibleOutVariableDeclaration(token, semanticModel, typeInferenceService, cancellationToken, out result) + || IsTupleLiteralElement(token, semanticModel, cancellationToken, out result) + || IsPossibleLocalVariableOrFunctionDeclaration(token, semanticModel, cancellationToken, out result) + || IsPatternMatching(token, semanticModel, cancellationToken, out result)) + { + return result; } - private static bool IsTupleTypeElement( - SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) + return default; + } + + private static bool IsTupleTypeElement( + SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + result = IsFollowingTypeOrComma( + token, + semanticModel, + tupleElement => tupleElement.Type, + _ => default(SyntaxTokenList), + _ => [new SymbolKindOrTypeKind(SymbolKind.Local)], cancellationToken); + + return result.Type != null; + } + + private static bool IsTupleLiteralElement( + SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + // Incomplete code like + // void Do() + // { + // (System.Array array, System.Action $$ + // gets parsed as a tuple expression. We can figure out the type in such cases. + // For a legit tuple expression we can't provide any completion. + if (token.GetAncestor(node => node.IsKind(SyntaxKind.TupleExpression)) != null) { - result = IsFollowingTypeOrComma( + result = IsFollowingTypeOrComma( token, semanticModel, - tupleElement => tupleElement.Type, + GetNodeDenotingTheTypeOfTupleArgument, _ => default(SyntaxTokenList), _ => [new SymbolKindOrTypeKind(SymbolKind.Local)], cancellationToken); - return result.Type != null; } - private static bool IsTupleLiteralElement( - SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) - { - // Incomplete code like - // void Do() - // { - // (System.Array array, System.Action $$ - // gets parsed as a tuple expression. We can figure out the type in such cases. - // For a legit tuple expression we can't provide any completion. - if (token.GetAncestor(node => node.IsKind(SyntaxKind.TupleExpression)) != null) - { - result = IsFollowingTypeOrComma( - token, - semanticModel, - GetNodeDenotingTheTypeOfTupleArgument, - _ => default(SyntaxTokenList), - _ => [new SymbolKindOrTypeKind(SymbolKind.Local)], cancellationToken); - return result.Type != null; - } - - result = default; - return false; - } + result = default; + return false; + } - private static bool IsPossibleOutVariableDeclaration(SyntaxToken token, SemanticModel semanticModel, - ITypeInferenceService typeInferenceService, CancellationToken cancellationToken, out NameDeclarationInfo result) + private static bool IsPossibleOutVariableDeclaration(SyntaxToken token, SemanticModel semanticModel, + ITypeInferenceService typeInferenceService, CancellationToken cancellationToken, out NameDeclarationInfo result) + { + if (token.IsKind(SyntaxKind.IdentifierToken) && + token.Parent.IsKind(SyntaxKind.IdentifierName)) { - if (token.IsKind(SyntaxKind.IdentifierToken) && - token.Parent.IsKind(SyntaxKind.IdentifierName)) - { - var argument = token.Parent.Parent as ArgumentSyntax // var is child of ArgumentSyntax, eg. Goo(out var $$ - ?? token.Parent.Parent?.Parent as ArgumentSyntax; // var is child of DeclarationExpression + var argument = token.Parent.Parent as ArgumentSyntax // var is child of ArgumentSyntax, eg. Goo(out var $$ + ?? token.Parent.Parent?.Parent as ArgumentSyntax; // var is child of DeclarationExpression - // under ArgumentSyntax, eg. Goo(out var a$$ - if (argument is { RefOrOutKeyword: SyntaxToken(SyntaxKind.OutKeyword) }) + // under ArgumentSyntax, eg. Goo(out var a$$ + if (argument is { RefOrOutKeyword: SyntaxToken(SyntaxKind.OutKeyword) }) + { + var type = typeInferenceService.InferType(semanticModel, argument.SpanStart, objectAsDefault: false, cancellationToken: cancellationToken); + if (type != null) { - var type = typeInferenceService.InferType(semanticModel, argument.SpanStart, objectAsDefault: false, cancellationToken: cancellationToken); - if (type != null) - { - var parameter = CSharpSemanticFacts.Instance.FindParameterForArgument( - semanticModel, argument, allowUncertainCandidates: true, allowParams: false, cancellationToken); - - result = new NameDeclarationInfo( - [new SymbolKindOrTypeKind(SymbolKind.Local)], - Accessibility.NotApplicable, - type: type, - symbol: parameter); - return true; - } + var parameter = CSharpSemanticFacts.Instance.FindParameterForArgument( + semanticModel, argument, allowUncertainCandidates: true, allowParams: false, cancellationToken); + + result = new NameDeclarationInfo( + [new SymbolKindOrTypeKind(SymbolKind.Local)], + Accessibility.NotApplicable, + type: type, + symbol: parameter); + return true; } } + } + + result = default; + return false; + } - result = default; + private static bool IsPossibleLocalVariableOrFunctionDeclaration( + SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + result = IsLastTokenOfType( + token, semanticModel, + e => e.Expression, + _ => default, + _ => [new SymbolKindOrTypeKind(SymbolKind.Local), new SymbolKindOrTypeKind(MethodKind.LocalFunction)], + cancellationToken, + out var expression); + + if (result.Type is null || expression is null) return false; - } - private static bool IsPossibleLocalVariableOrFunctionDeclaration( - SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) + // we have something like `x.y $$`. + // + // For this to actually be the start of a local declaration or function x.y needs to bind to an actual + // type symbol, not just any arbitrary expression that might have a type (e.g. `Console.BackgroundColor $$). + var symbol = semanticModel.GetSymbolInfo(expression, cancellationToken).GetAnySymbol(); + return symbol is ITypeSymbol; + } + + private static bool IsPropertyDeclaration(SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + result = IsLastTokenOfType( + token, + semanticModel, + m => m.Type, + m => m.Modifiers, + GetPossibleMemberDeclarations, + cancellationToken); + + return result.Type != null; + } + + private static bool IsMethodDeclaration(SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + result = IsLastTokenOfType( + token, + semanticModel, + m => m.ReturnType, + m => m.Modifiers, + GetPossibleMemberDeclarations, + cancellationToken); + + return result.Type != null; + } + + private static NameDeclarationInfo IsFollowingTypeOrComma( + SyntaxToken token, + SemanticModel semanticModel, + Func typeSyntaxGetter, + Func modifierGetter, + Func> possibleDeclarationComputer, + CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + if (!IsPossibleTypeToken(token) && !token.IsKind(SyntaxKind.CommaToken)) { - result = IsLastTokenOfType( - token, semanticModel, - e => e.Expression, - _ => default, - _ => [new SymbolKindOrTypeKind(SymbolKind.Local), new SymbolKindOrTypeKind(MethodKind.LocalFunction)], - cancellationToken, - out var expression); - - if (result.Type is null || expression is null) - return false; - - // we have something like `x.y $$`. - // - // For this to actually be the start of a local declaration or function x.y needs to bind to an actual - // type symbol, not just any arbitrary expression that might have a type (e.g. `Console.BackgroundColor $$). - var symbol = semanticModel.GetSymbolInfo(expression, cancellationToken).GetAnySymbol(); - return symbol is ITypeSymbol; + return default; } - private static bool IsPropertyDeclaration(SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) + var target = token.GetAncestor(); + if (target == null) { - result = IsLastTokenOfType( - token, - semanticModel, - m => m.Type, - m => m.Modifiers, - GetPossibleMemberDeclarations, - cancellationToken); + return default; + } - return result.Type != null; + if (token.IsKind(SyntaxKind.CommaToken) && token.Parent != target) + { + return default; } - private static bool IsMethodDeclaration(SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) + var typeSyntax = typeSyntaxGetter(target); + if (typeSyntax == null) { - result = IsLastTokenOfType( - token, - semanticModel, - m => m.ReturnType, - m => m.Modifiers, - GetPossibleMemberDeclarations, - cancellationToken); + return default; + } - return result.Type != null; + if (!token.IsKind(SyntaxKind.CommaToken) && token != typeSyntax.GetLastToken()) + { + return default; } - private static NameDeclarationInfo IsFollowingTypeOrComma( - SyntaxToken token, - SemanticModel semanticModel, - Func typeSyntaxGetter, - Func modifierGetter, - Func> possibleDeclarationComputer, - CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + var modifiers = modifierGetter(target); + if (modifiers == null) { - if (!IsPossibleTypeToken(token) && !token.IsKind(SyntaxKind.CommaToken)) - { - return default; - } + return default; + } - var target = token.GetAncestor(); - if (target == null) - { - return default; - } + return new NameDeclarationInfo( + possibleDeclarationComputer(GetDeclarationModifiers(modifiers.Value)), + GetAccessibility(modifiers.Value), + GetDeclarationModifiers(modifiers.Value), + semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type, + semanticModel.GetAliasInfo(typeSyntax, cancellationToken)); + } - if (token.IsKind(SyntaxKind.CommaToken) && token.Parent != target) - { - return default; - } + private static NameDeclarationInfo IsLastTokenOfType( + SyntaxToken token, + SemanticModel semanticModel, + Func typeSyntaxGetter, + Func modifierGetter, + Func> possibleDeclarationComputer, + CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + return IsLastTokenOfType(token, semanticModel, typeSyntaxGetter, modifierGetter, possibleDeclarationComputer, cancellationToken, out _); + } - var typeSyntax = typeSyntaxGetter(target); - if (typeSyntax == null) - { - return default; - } + private static NameDeclarationInfo IsLastTokenOfType( + SyntaxToken token, + SemanticModel semanticModel, + Func typeSyntaxGetter, + Func modifierGetter, + Func> possibleDeclarationComputer, + CancellationToken cancellationToken, + out SyntaxNode? typeSyntax) where TSyntaxNode : SyntaxNode + { + typeSyntax = null; + if (!IsPossibleTypeToken(token)) + return default; - if (!token.IsKind(SyntaxKind.CommaToken) && token != typeSyntax.GetLastToken()) - { - return default; - } + var target = token.GetAncestor(); + if (target == null) + return default; - var modifiers = modifierGetter(target); - if (modifiers == null) - { - return default; - } + typeSyntax = typeSyntaxGetter(target); + if (typeSyntax == null || token != typeSyntax.GetLastToken()) + return default; - return new NameDeclarationInfo( - possibleDeclarationComputer(GetDeclarationModifiers(modifiers.Value)), - GetAccessibility(modifiers.Value), - GetDeclarationModifiers(modifiers.Value), - semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type, - semanticModel.GetAliasInfo(typeSyntax, cancellationToken)); - } + var modifiers = modifierGetter(target); - private static NameDeclarationInfo IsLastTokenOfType( - SyntaxToken token, - SemanticModel semanticModel, - Func typeSyntaxGetter, - Func modifierGetter, - Func> possibleDeclarationComputer, - CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - return IsLastTokenOfType(token, semanticModel, typeSyntaxGetter, modifierGetter, possibleDeclarationComputer, cancellationToken, out _); - } + return new NameDeclarationInfo( + possibleDeclarationComputer(GetDeclarationModifiers(modifiers)), + GetAccessibility(modifiers), + GetDeclarationModifiers(modifiers), + semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type, + semanticModel.GetAliasInfo(typeSyntax, cancellationToken)); + } - private static NameDeclarationInfo IsLastTokenOfType( - SyntaxToken token, - SemanticModel semanticModel, - Func typeSyntaxGetter, - Func modifierGetter, - Func> possibleDeclarationComputer, - CancellationToken cancellationToken, - out SyntaxNode? typeSyntax) where TSyntaxNode : SyntaxNode - { - typeSyntax = null; - if (!IsPossibleTypeToken(token)) - return default; - - var target = token.GetAncestor(); - if (target == null) - return default; - - typeSyntax = typeSyntaxGetter(target); - if (typeSyntax == null || token != typeSyntax.GetLastToken()) - return default; - - var modifiers = modifierGetter(target); - - return new NameDeclarationInfo( - possibleDeclarationComputer(GetDeclarationModifiers(modifiers)), - GetAccessibility(modifiers), - GetDeclarationModifiers(modifiers), - semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type, - semanticModel.GetAliasInfo(typeSyntax, cancellationToken)); - } + private static bool IsFieldDeclaration(SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + result = IsFollowingTypeOrComma(token, semanticModel, + v => v.Type, + v => v.Parent is FieldDeclarationSyntax f ? f.Modifiers : null, + GetPossibleMemberDeclarations, + cancellationToken); + return result.Type != null; + } - private static bool IsFieldDeclaration(SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) - { - result = IsFollowingTypeOrComma(token, semanticModel, - v => v.Type, - v => v.Parent is FieldDeclarationSyntax f ? f.Modifiers : null, - GetPossibleMemberDeclarations, - cancellationToken); - return result.Type != null; - } + private static bool IsIncompleteMemberDeclaration(SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + result = IsLastTokenOfType(token, semanticModel, + i => i.Type, + i => i.Modifiers, + GetPossibleMemberDeclarations, + cancellationToken); + return result.Type != null; + } - private static bool IsIncompleteMemberDeclaration(SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) - { - result = IsLastTokenOfType(token, semanticModel, - i => i.Type, - i => i.Modifiers, - GetPossibleMemberDeclarations, - cancellationToken); - return result.Type != null; - } + private static bool IsLocalFunctionDeclaration(SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + result = IsLastTokenOfType(token, semanticModel, + typeSyntaxGetter: f => f.ReturnType, + modifierGetter: f => f.Modifiers, + possibleDeclarationComputer: GetPossibleLocalDeclarations, + cancellationToken); + return result.Type != null; + } - private static bool IsLocalFunctionDeclaration(SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) + private static bool IsLocalVariableDeclaration(SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + // If we only have a type, this can still end up being a local function (depending on the modifiers). + var possibleDeclarationComputer = token.IsKind(SyntaxKind.CommaToken) + ? (Func>) + (_ => [new SymbolKindOrTypeKind(SymbolKind.Local)]) + : GetPossibleLocalDeclarations; + + result = IsFollowingTypeOrComma(token, semanticModel, + typeSyntaxGetter: v => v.Type, + modifierGetter: v => v.Parent is LocalDeclarationStatementSyntax localDeclaration + ? localDeclaration.Modifiers + : null, // Return null to bail out. + possibleDeclarationComputer, + cancellationToken); + if (result.Type != null) { - result = IsLastTokenOfType(token, semanticModel, - typeSyntaxGetter: f => f.ReturnType, - modifierGetter: f => f.Modifiers, - possibleDeclarationComputer: GetPossibleLocalDeclarations, - cancellationToken); - return result.Type != null; + return true; } - private static bool IsLocalVariableDeclaration(SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) + // If the type has a trailing question mark, we may parse it as a conditional access expression. + // We will use the condition as the type to bind; we won't make the type we bind nullable + // because we ignore nullability when generating names anyways + if (token.IsKind(SyntaxKind.QuestionToken) && + token.Parent is ConditionalExpressionSyntax conditionalExpressionSyntax) { - // If we only have a type, this can still end up being a local function (depending on the modifiers). - var possibleDeclarationComputer = token.IsKind(SyntaxKind.CommaToken) - ? (Func>) - (_ => [new SymbolKindOrTypeKind(SymbolKind.Local)]) - : GetPossibleLocalDeclarations; - - result = IsFollowingTypeOrComma(token, semanticModel, - typeSyntaxGetter: v => v.Type, - modifierGetter: v => v.Parent is LocalDeclarationStatementSyntax localDeclaration - ? localDeclaration.Modifiers - : null, // Return null to bail out. - possibleDeclarationComputer, - cancellationToken); - if (result.Type != null) + var symbolInfo = semanticModel.GetSymbolInfo(conditionalExpressionSyntax.Condition); + + if (symbolInfo.GetAnySymbol() is ITypeSymbol type) { + var alias = semanticModel.GetAliasInfo(conditionalExpressionSyntax.Condition, cancellationToken); + + result = new NameDeclarationInfo( + possibleDeclarationComputer(default), + accessibility: null, + type: type, + alias: alias); return true; } + } - // If the type has a trailing question mark, we may parse it as a conditional access expression. - // We will use the condition as the type to bind; we won't make the type we bind nullable - // because we ignore nullability when generating names anyways - if (token.IsKind(SyntaxKind.QuestionToken) && - token.Parent is ConditionalExpressionSyntax conditionalExpressionSyntax) - { - var symbolInfo = semanticModel.GetSymbolInfo(conditionalExpressionSyntax.Condition); - - if (symbolInfo.GetAnySymbol() is ITypeSymbol type) - { - var alias = semanticModel.GetAliasInfo(conditionalExpressionSyntax.Condition, cancellationToken); + return false; + } - result = new NameDeclarationInfo( - possibleDeclarationComputer(default), - accessibility: null, - type: type, - alias: alias); - return true; - } - } + private static bool IsEmbeddedVariableDeclaration(SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + result = IsFollowingTypeOrComma(token, semanticModel, + typeSyntaxGetter: v => v.Type, + modifierGetter: v => v.Parent is UsingStatementSyntax or ForStatementSyntax + ? default(SyntaxTokenList) + : null, // Return null to bail out. + possibleDeclarationComputer: d => [new SymbolKindOrTypeKind(SymbolKind.Local)], + cancellationToken); + return result.Type != null; + } - return false; - } + private static bool IsForEachVariableDeclaration(SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + // This is parsed as ForEachVariableStatementSyntax: + // foreach (int $$ + result = IsLastTokenOfType(token, semanticModel, + typeSyntaxGetter: f => + f is ForEachStatementSyntax forEachStatement ? forEachStatement.Type : + f is ForEachVariableStatementSyntax forEachVariableStatement ? forEachVariableStatement.Variable : + null, // Return null to bail out. + modifierGetter: f => default, + possibleDeclarationComputer: d => [new SymbolKindOrTypeKind(SymbolKind.Local)], + cancellationToken); + return result.Type != null; + } - private static bool IsEmbeddedVariableDeclaration(SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) + private static bool IsTypeParameterDeclaration(SyntaxToken token, out NameDeclarationInfo result) + { + if (token.Kind() is SyntaxKind.LessThanToken or SyntaxKind.CommaToken && + token.Parent.IsKind(SyntaxKind.TypeParameterList)) { - result = IsFollowingTypeOrComma(token, semanticModel, - typeSyntaxGetter: v => v.Type, - modifierGetter: v => v.Parent is UsingStatementSyntax or ForStatementSyntax - ? default(SyntaxTokenList) - : null, // Return null to bail out. - possibleDeclarationComputer: d => [new SymbolKindOrTypeKind(SymbolKind.Local)], - cancellationToken); - return result.Type != null; - } + result = new NameDeclarationInfo( + [new SymbolKindOrTypeKind(SymbolKind.TypeParameter)], + Accessibility.NotApplicable); - private static bool IsForEachVariableDeclaration(SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) - { - // This is parsed as ForEachVariableStatementSyntax: - // foreach (int $$ - result = IsLastTokenOfType(token, semanticModel, - typeSyntaxGetter: f => - f is ForEachStatementSyntax forEachStatement ? forEachStatement.Type : - f is ForEachVariableStatementSyntax forEachVariableStatement ? forEachVariableStatement.Variable : - null, // Return null to bail out. - modifierGetter: f => default, - possibleDeclarationComputer: d => [new SymbolKindOrTypeKind(SymbolKind.Local)], - cancellationToken); - return result.Type != null; + return true; } - private static bool IsTypeParameterDeclaration(SyntaxToken token, out NameDeclarationInfo result) + result = default; + return false; + } + + private static bool IsPrimaryConstructorParameter(SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + result = IsLastTokenOfType( + token, semanticModel, + p => p.Type, + _ => default, + _ => s_propertySyntaxKind, + cancellationToken); + + if (result.Type != null && + token.GetAncestor()?.Parent?.Parent is (kind: SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration)) { - if (token.Kind() is SyntaxKind.LessThanToken or SyntaxKind.CommaToken && - token.Parent.IsKind(SyntaxKind.TypeParameterList)) - { - result = new NameDeclarationInfo( - [new SymbolKindOrTypeKind(SymbolKind.TypeParameter)], - Accessibility.NotApplicable); + return true; + } - return true; - } + result = default; + return false; + } - result = default; - return false; - } + private static bool IsParameterDeclaration(SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + result = IsLastTokenOfType( + token, semanticModel, + p => p.Type, + _ => default, + _ => s_parameterSyntaxKind, + cancellationToken); + return result.Type != null; + } - private static bool IsPrimaryConstructorParameter(SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) + private static bool IsPatternMatching(SyntaxToken token, SemanticModel semanticModel, + CancellationToken cancellationToken, out NameDeclarationInfo result) + { + result = default; + if (token.Parent.IsParentKind(SyntaxKind.IsExpression)) { - result = IsLastTokenOfType( + result = IsLastTokenOfType( token, semanticModel, - p => p.Type, + b => b.Right, _ => default, - _ => s_propertySyntaxKind, + _ => s_parameterSyntaxKind, cancellationToken); - - if (result.Type != null && - token.GetAncestor()?.Parent?.Parent is (kind: SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration)) - { - return true; - } - - result = default; - return false; } - - private static bool IsParameterDeclaration(SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) + else if (token.Parent.IsParentKind(SyntaxKind.CaseSwitchLabel)) { - result = IsLastTokenOfType( + result = IsLastTokenOfType( token, semanticModel, - p => p.Type, + b => b.Value, _ => default, _ => s_parameterSyntaxKind, cancellationToken); - return result.Type != null; } - - private static bool IsPatternMatching(SyntaxToken token, SemanticModel semanticModel, - CancellationToken cancellationToken, out NameDeclarationInfo result) + else if (token.Parent.IsParentKind(SyntaxKind.DeclarationPattern)) { - result = default; - if (token.Parent.IsParentKind(SyntaxKind.IsExpression)) - { - result = IsLastTokenOfType( - token, semanticModel, - b => b.Right, - _ => default, - _ => s_parameterSyntaxKind, - cancellationToken); - } - else if (token.Parent.IsParentKind(SyntaxKind.CaseSwitchLabel)) - { - result = IsLastTokenOfType( - token, semanticModel, - b => b.Value, - _ => default, - _ => s_parameterSyntaxKind, - cancellationToken); - } - else if (token.Parent.IsParentKind(SyntaxKind.DeclarationPattern)) - { - result = IsLastTokenOfType( - token, semanticModel, - b => b.Type, - _ => default, - _ => s_parameterSyntaxKind, - cancellationToken); - } - - return result.Type != null; + result = IsLastTokenOfType( + token, semanticModel, + b => b.Type, + _ => default, + _ => s_parameterSyntaxKind, + cancellationToken); } - private static bool IsPossibleTypeToken(SyntaxToken token) - => token.Kind() is - SyntaxKind.IdentifierToken or - SyntaxKind.GreaterThanToken or - SyntaxKind.CloseBracketToken or - SyntaxKind.QuestionToken || - token.Parent.IsKind(SyntaxKind.PredefinedType); - - private static ImmutableArray GetPossibleMemberDeclarations(DeclarationModifiers modifiers) - { - if (modifiers.IsConst || modifiers.IsReadOnly) - { - return [new SymbolKindOrTypeKind(SymbolKind.Field)]; - } + return result.Type != null; + } - var possibleTypes = ImmutableArray.Create( - new SymbolKindOrTypeKind(SymbolKind.Field), - new SymbolKindOrTypeKind(SymbolKind.Property), - new SymbolKindOrTypeKind(MethodKind.Ordinary)); + private static bool IsPossibleTypeToken(SyntaxToken token) + => token.Kind() is + SyntaxKind.IdentifierToken or + SyntaxKind.GreaterThanToken or + SyntaxKind.CloseBracketToken or + SyntaxKind.QuestionToken || + token.Parent.IsKind(SyntaxKind.PredefinedType); - if (modifiers.IsAbstract || modifiers.IsVirtual || modifiers.IsSealed || modifiers.IsOverride) - { - possibleTypes = possibleTypes.Remove(new SymbolKindOrTypeKind(SymbolKind.Field)); - } + private static ImmutableArray GetPossibleMemberDeclarations(DeclarationModifiers modifiers) + { + if (modifiers.IsConst || modifiers.IsReadOnly) + { + return [new SymbolKindOrTypeKind(SymbolKind.Field)]; + } - if (modifiers.IsAsync || modifiers.IsPartial) - { - // Fields and properties cannot be async or partial. - possibleTypes = possibleTypes.Remove(new SymbolKindOrTypeKind(SymbolKind.Field)); - possibleTypes = possibleTypes.Remove(new SymbolKindOrTypeKind(SymbolKind.Property)); - } + var possibleTypes = ImmutableArray.Create( + new SymbolKindOrTypeKind(SymbolKind.Field), + new SymbolKindOrTypeKind(SymbolKind.Property), + new SymbolKindOrTypeKind(MethodKind.Ordinary)); - return possibleTypes; + if (modifiers.IsAbstract || modifiers.IsVirtual || modifiers.IsSealed || modifiers.IsOverride) + { + possibleTypes = possibleTypes.Remove(new SymbolKindOrTypeKind(SymbolKind.Field)); } - private static ImmutableArray GetPossibleLocalDeclarations(DeclarationModifiers modifiers) + if (modifiers.IsAsync || modifiers.IsPartial) { - return - modifiers.IsConst - ? [new SymbolKindOrTypeKind(SymbolKind.Local)] : - modifiers.IsAsync || modifiers.IsUnsafe - ? [new SymbolKindOrTypeKind(MethodKind.LocalFunction)] : - [new SymbolKindOrTypeKind(SymbolKind.Local), new SymbolKindOrTypeKind(MethodKind.LocalFunction)]; + // Fields and properties cannot be async or partial. + possibleTypes = possibleTypes.Remove(new SymbolKindOrTypeKind(SymbolKind.Field)); + possibleTypes = possibleTypes.Remove(new SymbolKindOrTypeKind(SymbolKind.Property)); } - private static DeclarationModifiers GetDeclarationModifiers(SyntaxTokenList modifiers) - { - var declarationModifiers = new DeclarationModifiers(); - foreach (var modifer in modifiers) - { - switch (modifer.Kind()) - { - case SyntaxKind.StaticKeyword: - declarationModifiers = declarationModifiers.WithIsStatic(true); - continue; - case SyntaxKind.AbstractKeyword: - declarationModifiers = declarationModifiers.WithIsAbstract(true); - continue; - case SyntaxKind.NewKeyword: - declarationModifiers = declarationModifiers.WithIsNew(true); - continue; - case SyntaxKind.UnsafeKeyword: - declarationModifiers = declarationModifiers.WithIsUnsafe(true); - continue; - case SyntaxKind.ReadOnlyKeyword: - declarationModifiers = declarationModifiers.WithIsReadOnly(true); - continue; - case SyntaxKind.VirtualKeyword: - declarationModifiers = declarationModifiers.WithIsVirtual(true); - continue; - case SyntaxKind.OverrideKeyword: - declarationModifiers = declarationModifiers.WithIsOverride(true); - continue; - case SyntaxKind.SealedKeyword: - declarationModifiers = declarationModifiers.WithIsSealed(true); - continue; - case SyntaxKind.ConstKeyword: - declarationModifiers = declarationModifiers.WithIsConst(true); - continue; - case SyntaxKind.AsyncKeyword: - declarationModifiers = declarationModifiers.WithAsync(true); - continue; - case SyntaxKind.PartialKeyword: - declarationModifiers = declarationModifiers.WithPartial(true); - continue; - } - } + return possibleTypes; + } - return declarationModifiers; - } + private static ImmutableArray GetPossibleLocalDeclarations(DeclarationModifiers modifiers) + { + return + modifiers.IsConst + ? [new SymbolKindOrTypeKind(SymbolKind.Local)] : + modifiers.IsAsync || modifiers.IsUnsafe + ? [new SymbolKindOrTypeKind(MethodKind.LocalFunction)] : + [new SymbolKindOrTypeKind(SymbolKind.Local), new SymbolKindOrTypeKind(MethodKind.LocalFunction)]; + } - private static Accessibility? GetAccessibility(SyntaxTokenList modifiers) + private static DeclarationModifiers GetDeclarationModifiers(SyntaxTokenList modifiers) + { + var declarationModifiers = new DeclarationModifiers(); + foreach (var modifer in modifiers) { - for (var i = modifiers.Count - 1; i >= 0; i--) + switch (modifer.Kind()) { - var modifier = modifiers[i]; - switch (modifier.Kind()) - { - case SyntaxKind.PrivateKeyword: - return Accessibility.Private; - case SyntaxKind.PublicKeyword: - return Accessibility.Public; - case SyntaxKind.ProtectedKeyword: - return Accessibility.Protected; - case SyntaxKind.InternalKeyword: - return Accessibility.Internal; - } + case SyntaxKind.StaticKeyword: + declarationModifiers = declarationModifiers.WithIsStatic(true); + continue; + case SyntaxKind.AbstractKeyword: + declarationModifiers = declarationModifiers.WithIsAbstract(true); + continue; + case SyntaxKind.NewKeyword: + declarationModifiers = declarationModifiers.WithIsNew(true); + continue; + case SyntaxKind.UnsafeKeyword: + declarationModifiers = declarationModifiers.WithIsUnsafe(true); + continue; + case SyntaxKind.ReadOnlyKeyword: + declarationModifiers = declarationModifiers.WithIsReadOnly(true); + continue; + case SyntaxKind.VirtualKeyword: + declarationModifiers = declarationModifiers.WithIsVirtual(true); + continue; + case SyntaxKind.OverrideKeyword: + declarationModifiers = declarationModifiers.WithIsOverride(true); + continue; + case SyntaxKind.SealedKeyword: + declarationModifiers = declarationModifiers.WithIsSealed(true); + continue; + case SyntaxKind.ConstKeyword: + declarationModifiers = declarationModifiers.WithIsConst(true); + continue; + case SyntaxKind.AsyncKeyword: + declarationModifiers = declarationModifiers.WithAsync(true); + continue; + case SyntaxKind.PartialKeyword: + declarationModifiers = declarationModifiers.WithPartial(true); + continue; } - - return null; } - private static SyntaxNode? GetNodeDenotingTheTypeOfTupleArgument(ArgumentSyntax argumentSyntax) + return declarationModifiers; + } + + private static Accessibility? GetAccessibility(SyntaxTokenList modifiers) + { + for (var i = modifiers.Count - 1; i >= 0; i--) { - switch (argumentSyntax.Expression?.Kind()) + var modifier = modifiers[i]; + switch (modifier.Kind()) { - case SyntaxKind.DeclarationExpression: - // The parser found a declaration as in (System.Action action, System.Array a$$) - // we need the type part of the declaration expression. - return ((DeclarationExpressionSyntax)argumentSyntax.Expression).Type; - default: - // We assume the parser found something that represents something named, - // e.g. a MemberAccessExpression as in (System.Action action, System.Array $$) - // We also assume that this name could be resolved to a type. - return argumentSyntax.Expression; + case SyntaxKind.PrivateKeyword: + return Accessibility.Private; + case SyntaxKind.PublicKeyword: + return Accessibility.Public; + case SyntaxKind.ProtectedKeyword: + return Accessibility.Protected; + case SyntaxKind.InternalKeyword: + return Accessibility.Internal; } } - public static Glyph GetGlyph(SymbolKind kind, Accessibility? declaredAccessibility) - { - var publicIcon = kind switch - { - SymbolKind.Field => Glyph.FieldPublic, - SymbolKind.Local => Glyph.Local, - SymbolKind.Method => Glyph.MethodPublic, - SymbolKind.Parameter => Glyph.Parameter, - SymbolKind.Property => Glyph.PropertyPublic, - SymbolKind.RangeVariable => Glyph.RangeVariable, - SymbolKind.TypeParameter => Glyph.TypeParameter, - _ => throw ExceptionUtilities.UnexpectedValue(kind), - }; - - switch (declaredAccessibility) - { - case Accessibility.Private: - publicIcon += Glyph.ClassPrivate - Glyph.ClassPublic; - break; - - case Accessibility.Protected: - case Accessibility.ProtectedAndInternal: - case Accessibility.ProtectedOrInternal: - publicIcon += Glyph.ClassProtected - Glyph.ClassPublic; - break; - - case Accessibility.Internal: - publicIcon += Glyph.ClassInternal - Glyph.ClassPublic; - break; - } + return null; + } - return publicIcon; + private static SyntaxNode? GetNodeDenotingTheTypeOfTupleArgument(ArgumentSyntax argumentSyntax) + { + switch (argumentSyntax.Expression?.Kind()) + { + case SyntaxKind.DeclarationExpression: + // The parser found a declaration as in (System.Action action, System.Array a$$) + // we need the type part of the declaration expression. + return ((DeclarationExpressionSyntax)argumentSyntax.Expression).Type; + default: + // We assume the parser found something that represents something named, + // e.g. a MemberAccessExpression as in (System.Action action, System.Array $$) + // We also assume that this name could be resolved to a type. + return argumentSyntax.Expression; } + } - public static SymbolKind GetSymbolKind(SymbolKindOrTypeKind symbolKindOrTypeKind) + public static Glyph GetGlyph(SymbolKind kind, Accessibility? declaredAccessibility) + { + var publicIcon = kind switch + { + SymbolKind.Field => Glyph.FieldPublic, + SymbolKind.Local => Glyph.Local, + SymbolKind.Method => Glyph.MethodPublic, + SymbolKind.Parameter => Glyph.Parameter, + SymbolKind.Property => Glyph.PropertyPublic, + SymbolKind.RangeVariable => Glyph.RangeVariable, + SymbolKind.TypeParameter => Glyph.TypeParameter, + _ => throw ExceptionUtilities.UnexpectedValue(kind), + }; + + switch (declaredAccessibility) { - // There's no special glyph for local functions. - // We don't need to differentiate them at this point. - return symbolKindOrTypeKind.SymbolKind.HasValue ? symbolKindOrTypeKind.SymbolKind.Value : - symbolKindOrTypeKind.MethodKind.HasValue ? SymbolKind.Method : - throw ExceptionUtilities.Unreachable(); + case Accessibility.Private: + publicIcon += Glyph.ClassPrivate - Glyph.ClassPublic; + break; + + case Accessibility.Protected: + case Accessibility.ProtectedAndInternal: + case Accessibility.ProtectedOrInternal: + publicIcon += Glyph.ClassProtected - Glyph.ClassPublic; + break; + + case Accessibility.Internal: + publicIcon += Glyph.ClassInternal - Glyph.ClassPublic; + break; } + + return publicIcon; + } + + public static SymbolKind GetSymbolKind(SymbolKindOrTypeKind symbolKindOrTypeKind) + { + // There's no special glyph for local functions. + // We don't need to differentiate them at this point. + return symbolKindOrTypeKind.SymbolKind.HasValue ? symbolKindOrTypeKind.SymbolKind.Value : + symbolKindOrTypeKind.MethodKind.HasValue ? SymbolKind.Method : + throw ExceptionUtilities.Unreachable(); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameRecommender.NameGenerator.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameRecommender.NameGenerator.cs index 19fcd26a866c7..9633b63dbe629 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameRecommender.NameGenerator.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameRecommender.NameGenerator.cs @@ -10,121 +10,120 @@ using Microsoft.CodeAnalysis.Text; using Words = System.Collections.Immutable.ImmutableArray; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers.DeclarationName +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers.DeclarationName; + +internal partial class DeclarationNameRecommender { - internal partial class DeclarationNameRecommender + internal class NameGenerator { - internal class NameGenerator + private const char DefaultInterfacePrefix = 'I'; + private const char DefaultGenericParameterPrefix = 'T'; + + internal static ImmutableArray GetBaseNames(ITypeSymbol type, bool pluralize) { - private const char DefaultInterfacePrefix = 'I'; - private const char DefaultGenericParameterPrefix = 'T'; + var baseName = TryRemoveKnownPrefixes(type); + using var parts = TemporaryArray.Empty; + StringBreaker.AddWordParts(baseName, ref parts.AsRef()); + var result = GetInterleavedPatterns(parts, baseName, pluralize); - internal static ImmutableArray GetBaseNames(ITypeSymbol type, bool pluralize) - { - var baseName = TryRemoveKnownPrefixes(type); - using var parts = TemporaryArray.Empty; - StringBreaker.AddWordParts(baseName, ref parts.AsRef()); - var result = GetInterleavedPatterns(parts, baseName, pluralize); + return result; + } - return result; + internal static ImmutableArray GetBaseNames(IAliasSymbol alias) + { + var name = alias.Name; + if (alias.Target.IsType && + ((INamedTypeSymbol)alias.Target).IsInterfaceType() && + CanRemovePrefix(name, DefaultInterfacePrefix)) + { + name = name[1..]; } - internal static ImmutableArray GetBaseNames(IAliasSymbol alias) - { - var name = alias.Name; - if (alias.Target.IsType && - ((INamedTypeSymbol)alias.Target).IsInterfaceType() && - CanRemovePrefix(name, DefaultInterfacePrefix)) - { - name = name[1..]; - } + using var breaks = TemporaryArray.Empty; + StringBreaker.AddWordParts(name, ref breaks.AsRef()); + var result = GetInterleavedPatterns(breaks, name, pluralize: false); + return result; + } - using var breaks = TemporaryArray.Empty; - StringBreaker.AddWordParts(name, ref breaks.AsRef()); - var result = GetInterleavedPatterns(breaks, name, pluralize: false); - return result; - } + private static ImmutableArray GetInterleavedPatterns( + in TemporaryArray breaks, string baseName, bool pluralize) + { + using var result = TemporaryArray.Empty; + var breakCount = breaks.Count; + result.Add(GetWords(0, breakCount, breaks, baseName, pluralize)); - private static ImmutableArray GetInterleavedPatterns( - in TemporaryArray breaks, string baseName, bool pluralize) + for (var length = breakCount - 1; length > 0; length--) { - using var result = TemporaryArray.Empty; - var breakCount = breaks.Count; - result.Add(GetWords(0, breakCount, breaks, baseName, pluralize)); + // going forward + result.Add(GetLongestForwardSubsequence(length, breaks, baseName, pluralize)); - for (var length = breakCount - 1; length > 0; length--) - { - // going forward - result.Add(GetLongestForwardSubsequence(length, breaks, baseName, pluralize)); + // going backward + result.Add(GetLongestBackwardSubsequence(length, breaks, baseName, pluralize)); + } - // going backward - result.Add(GetLongestBackwardSubsequence(length, breaks, baseName, pluralize)); - } + return result.ToImmutableAndClear(); + } - return result.ToImmutableAndClear(); - } + private static Words GetLongestBackwardSubsequence(int length, in TemporaryArray breaks, string baseName, bool pluralize) + { + var breakCount = breaks.Count; + var start = breakCount - length; + return GetWords(start, breakCount, breaks, baseName, pluralize); + } + + private static Words GetLongestForwardSubsequence(int length, in TemporaryArray breaks, string baseName, bool pluralize) + => GetWords(0, length, breaks, baseName, pluralize); - private static Words GetLongestBackwardSubsequence(int length, in TemporaryArray breaks, string baseName, bool pluralize) + private static Words GetWords(int start, int end, in TemporaryArray breaks, string baseName, bool pluralize) + { + using var result = TemporaryArray.Empty; + // Add all the words but the last one + for (; start < end; start++) { - var breakCount = breaks.Count; - var start = breakCount - length; - return GetWords(start, breakCount, breaks, baseName, pluralize); + var @break = breaks[start]; + var text = baseName.Substring(@break.Start, @break.Length); + if (pluralize && start == end - 1) + { + // Pluralize the last word if necessary + result.Add(text.Pluralize()); + } + else + { + result.Add(text); + } } - private static Words GetLongestForwardSubsequence(int length, in TemporaryArray breaks, string baseName, bool pluralize) - => GetWords(0, length, breaks, baseName, pluralize); + return result.ToImmutableAndClear(); + } - private static Words GetWords(int start, int end, in TemporaryArray breaks, string baseName, bool pluralize) + //Tries to remove "I" prefix from interfaces and "T" prefix from generic parameter names + private static string TryRemoveKnownPrefixes(ITypeSymbol type) + { + var name = type.Name; + + if (type.TypeKind == TypeKind.Interface) { - using var result = TemporaryArray.Empty; - // Add all the words but the last one - for (; start < end; start++) + if (CanRemovePrefix(name, DefaultInterfacePrefix)) { - var @break = breaks[start]; - var text = baseName.Substring(@break.Start, @break.Length); - if (pluralize && start == end - 1) - { - // Pluralize the last word if necessary - result.Add(text.Pluralize()); - } - else - { - result.Add(text); - } + return name[1..]; } - - return result.ToImmutableAndClear(); } - //Tries to remove "I" prefix from interfaces and "T" prefix from generic parameter names - private static string TryRemoveKnownPrefixes(ITypeSymbol type) + if (type.TypeKind == TypeKind.TypeParameter) { - var name = type.Name; - - if (type.TypeKind == TypeKind.Interface) + if (CanRemovePrefix(name, DefaultGenericParameterPrefix)) { - if (CanRemovePrefix(name, DefaultInterfacePrefix)) - { - return name[1..]; - } + return name[1..]; } - - if (type.TypeKind == TypeKind.TypeParameter) + else if (name is [DefaultGenericParameterPrefix]) { - if (CanRemovePrefix(name, DefaultGenericParameterPrefix)) - { - return name[1..]; - } - else if (name is [DefaultGenericParameterPrefix]) - { - return ITypeSymbolExtensions.DefaultParameterName; - } + return ITypeSymbolExtensions.DefaultParameterName; } - - return type.CreateParameterName(); } - } - private static bool CanRemovePrefix(string name, char prefix) => name.Length > 1 && name[0] == prefix && char.IsUpper(name[1]); + return type.CreateParameterName(); + } } + + private static bool CanRemovePrefix(string name, char prefix) => name.Length > 1 && name[0] == prefix && char.IsUpper(name[1]); } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameRecommender.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameRecommender.cs index 3c1c18bcb7473..3d88f7a352b7e 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameRecommender.cs @@ -25,315 +25,314 @@ using Microsoft.CodeAnalysis.Shared.Naming; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers.DeclarationName +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers.DeclarationName; + +[ExportDeclarationNameRecommender(nameof(DeclarationNameRecommender)), Shared] +internal sealed partial class DeclarationNameRecommender : IDeclarationNameRecommender { - [ExportDeclarationNameRecommender(nameof(DeclarationNameRecommender)), Shared] - internal sealed partial class DeclarationNameRecommender : IDeclarationNameRecommender + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DeclarationNameRecommender() + { } + + public async Task> ProvideRecommendedNamesAsync( + CompletionContext completionContext, + Document document, + CSharpSyntaxContext context, + NameDeclarationInfo nameInfo, + CancellationToken cancellationToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public DeclarationNameRecommender() - { } - - public async Task> ProvideRecommendedNamesAsync( - CompletionContext completionContext, - Document document, - CSharpSyntaxContext context, - NameDeclarationInfo nameInfo, - CancellationToken cancellationToken) + using var _ = ArrayBuilder<(string, Glyph)>.GetInstance(out var result); + + // Suggest names from existing overloads. + if (nameInfo.PossibleSymbolKinds.Any(static k => k.SymbolKind == SymbolKind.Parameter)) + AddNamesFromExistingOverloads(context, nameInfo, result, cancellationToken); + + var names = GetBaseNames(context.SemanticModel, nameInfo).NullToEmpty(); + + // If we have a direct symbol this binds to, offer its name as a potential name here. + if (nameInfo.Symbol != null) + names = names.Insert(0, [nameInfo.Symbol.Name]); + + if (!names.IsDefaultOrEmpty) { - using var _ = ArrayBuilder<(string, Glyph)>.GetInstance(out var result); + var namingStyleOptions = await document.GetNamingStylePreferencesAsync(completionContext.CompletionOptions.NamingStyleFallbackOptions, cancellationToken).ConfigureAwait(false); + GetRecommendedNames(names, nameInfo, context, result, namingStyleOptions, cancellationToken); + } + + return result.ToImmutable(); + } - // Suggest names from existing overloads. - if (nameInfo.PossibleSymbolKinds.Any(static k => k.SymbolKind == SymbolKind.Parameter)) - AddNamesFromExistingOverloads(context, nameInfo, result, cancellationToken); + private ImmutableArray> GetBaseNames(SemanticModel semanticModel, NameDeclarationInfo nameInfo) + { + if (nameInfo.Alias != null) + return NameGenerator.GetBaseNames(nameInfo.Alias); - var names = GetBaseNames(context.SemanticModel, nameInfo).NullToEmpty(); + if (!IsValidType(nameInfo.Type)) + return default; - // If we have a direct symbol this binds to, offer its name as a potential name here. - if (nameInfo.Symbol != null) - names = names.Insert(0, [nameInfo.Symbol.Name]); + var (type, plural) = UnwrapType(nameInfo.Type, semanticModel.Compilation, wasPlural: false, seenTypes: []); - if (!names.IsDefaultOrEmpty) - { - var namingStyleOptions = await document.GetNamingStylePreferencesAsync(completionContext.CompletionOptions.NamingStyleFallbackOptions, cancellationToken).ConfigureAwait(false); - GetRecommendedNames(names, nameInfo, context, result, namingStyleOptions, cancellationToken); - } + var baseNames = NameGenerator.GetBaseNames(type, plural); + return baseNames; + } - return result.ToImmutable(); + private static bool IsValidType([NotNullWhen(true)] ITypeSymbol? type) + { + if (type == null) + { + return false; } - private ImmutableArray> GetBaseNames(SemanticModel semanticModel, NameDeclarationInfo nameInfo) + if (type.IsErrorType() && (type.Name == "var" || type.Name == string.Empty)) { - if (nameInfo.Alias != null) - return NameGenerator.GetBaseNames(nameInfo.Alias); + return false; + } - if (!IsValidType(nameInfo.Type)) - return default; + if (type.SpecialType == SpecialType.System_Void) + { + return false; + } - var (type, plural) = UnwrapType(nameInfo.Type, semanticModel.Compilation, wasPlural: false, seenTypes: []); + return !type.IsSpecialType(); + } - var baseNames = NameGenerator.GetBaseNames(type, plural); - return baseNames; + private (ITypeSymbol, bool plural) UnwrapType(ITypeSymbol type, Compilation compilation, bool wasPlural, HashSet seenTypes) + { + // Consider C : Task + // Visiting the C in Task will stack overflow + if (seenTypes.Contains(type)) + { + return (type, wasPlural); } - private static bool IsValidType([NotNullWhen(true)] ITypeSymbol? type) + // The main purpose of this is to prevent converting "string" to "chars", but it also simplifies logic for other basic types (int, double, object etc.) + if (type.IsSpecialType()) { - if (type == null) - { - return false; - } + return (type, wasPlural); + } - if (type.IsErrorType() && (type.Name == "var" || type.Name == string.Empty)) - { - return false; - } + seenTypes.AddRange(type.GetBaseTypesAndThis()); - if (type.SpecialType == SpecialType.System_Void) - { - return false; - } + if (type is IArrayTypeSymbol arrayType) + { + return UnwrapType(arrayType.ElementType, compilation, wasPlural: true, seenTypes: seenTypes); + } - return !type.IsSpecialType(); + if (type is IErrorTypeSymbol { TypeArguments: [var typeArgument] } && + LooksLikeWellKnownCollectionType(compilation, type.Name)) + { + return UnwrapType(typeArgument, compilation, wasPlural: true, seenTypes); } - private (ITypeSymbol, bool plural) UnwrapType(ITypeSymbol type, Compilation compilation, bool wasPlural, HashSet seenTypes) + if (type is INamedTypeSymbol namedType && namedType.OriginalDefinition != null) { - // Consider C : Task - // Visiting the C in Task will stack overflow - if (seenTypes.Contains(type)) + // if namedType contains a valid GetEnumerator method, we want collectionType to be the type of + // the "Current" property of this enumerator. For example: + // if namedType is a Span, collectionType should be Person. + var collectionType = namedType.GetMembers() + .OfType() + .FirstOrDefault(m => m.IsValidGetEnumerator() || m.IsValidGetAsyncEnumerator()) + ?.ReturnType?.GetMembers(WellKnownMemberNames.CurrentPropertyName) + .OfType().FirstOrDefault(p => p.GetMethod != null)?.Type; + + // This can happen for an un-implemented IEnumerable or IAsyncEnumerable. + collectionType ??= namedType.AllInterfaces.FirstOrDefault( + t => t.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T || + Equals(t.OriginalDefinition, compilation.IAsyncEnumerableOfTType()))?.TypeArguments[0]; + + if (collectionType is not null) { - return (type, wasPlural); - } + // Consider: Container : IEnumerable + // Container | + // We don't want to suggest the plural version of a type that can be used singularly + if (seenTypes.Contains(collectionType)) + { + return (type, wasPlural); + } - // The main purpose of this is to prevent converting "string" to "chars", but it also simplifies logic for other basic types (int, double, object etc.) - if (type.IsSpecialType()) - { - return (type, wasPlural); + return UnwrapType(collectionType, compilation, wasPlural: true, seenTypes: seenTypes); } - seenTypes.AddRange(type.GetBaseTypesAndThis()); + var originalDefinition = namedType.OriginalDefinition; + var taskOfTType = compilation.TaskOfTType(); + var valueTaskType = compilation.ValueTaskOfTType(); + var lazyOfTType = compilation.LazyOfTType(); - if (type is IArrayTypeSymbol arrayType) + if (Equals(originalDefinition, taskOfTType) || + Equals(originalDefinition, valueTaskType) || + Equals(originalDefinition, lazyOfTType) || + originalDefinition.SpecialType == SpecialType.System_Nullable_T) { - return UnwrapType(arrayType.ElementType, compilation, wasPlural: true, seenTypes: seenTypes); + return UnwrapType(namedType.TypeArguments[0], compilation, wasPlural: wasPlural, seenTypes: seenTypes); } + } + + return (type, wasPlural); + } + + private bool LooksLikeWellKnownCollectionType(Compilation compilation, string name) + { + // see if the user has something like `IEnumerable` (where IEnumerable doesn't bind). Weak + // heuristic. If there's a matching type under System.Collections with that name, then assume it's a + // collection and attempt to create a name from the type arg. + var system = compilation.GlobalNamespace.GetMembers(nameof(System)).OfType().FirstOrDefault(); + var systemCollections = system?.GetMembers(nameof(System.Collections)).OfType().FirstOrDefault(); + + // just check System.Collections, and it's immediate namespace children. This covers all the common cases + // like "Concurrent/Generic/Immutable/Specialized", and prevents having to worry about huge trees to walk. + if (systemCollections is not null) + { + if (Check(systemCollections, name)) + return true; - if (type is IErrorTypeSymbol { TypeArguments: [var typeArgument] } && - LooksLikeWellKnownCollectionType(compilation, type.Name)) + foreach (var childNamespace in systemCollections.GetNamespaceMembers()) { - return UnwrapType(typeArgument, compilation, wasPlural: true, seenTypes); + if (Check(childNamespace, name)) + return true; } + } - if (type is INamedTypeSymbol namedType && namedType.OriginalDefinition != null) - { - // if namedType contains a valid GetEnumerator method, we want collectionType to be the type of - // the "Current" property of this enumerator. For example: - // if namedType is a Span, collectionType should be Person. - var collectionType = namedType.GetMembers() - .OfType() - .FirstOrDefault(m => m.IsValidGetEnumerator() || m.IsValidGetAsyncEnumerator()) - ?.ReturnType?.GetMembers(WellKnownMemberNames.CurrentPropertyName) - .OfType().FirstOrDefault(p => p.GetMethod != null)?.Type; - - // This can happen for an un-implemented IEnumerable or IAsyncEnumerable. - collectionType ??= namedType.AllInterfaces.FirstOrDefault( - t => t.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T || - Equals(t.OriginalDefinition, compilation.IAsyncEnumerableOfTType()))?.TypeArguments[0]; - - if (collectionType is not null) - { - // Consider: Container : IEnumerable - // Container | - // We don't want to suggest the plural version of a type that can be used singularly - if (seenTypes.Contains(collectionType)) - { - return (type, wasPlural); - } + return false; - return UnwrapType(collectionType, compilation, wasPlural: true, seenTypes: seenTypes); - } + static bool Check(INamespaceSymbol? namespaceSymbol, string name) + => namespaceSymbol != null && namespaceSymbol.GetTypeMembers(name).Any(static t => t.DeclaredAccessibility == Accessibility.Public); + } - var originalDefinition = namedType.OriginalDefinition; - var taskOfTType = compilation.TaskOfTType(); - var valueTaskType = compilation.ValueTaskOfTType(); - var lazyOfTType = compilation.LazyOfTType(); + private static void GetRecommendedNames( + ImmutableArray> baseNames, + NameDeclarationInfo declarationInfo, + CSharpSyntaxContext context, + ArrayBuilder<(string, Glyph)> result, + NamingStylePreferences namingStyleOptions, + CancellationToken cancellationToken) + { + var rules = namingStyleOptions.CreateRules().NamingRules.AddRange(FallbackNamingRules.CompletionFallbackRules); - if (Equals(originalDefinition, taskOfTType) || - Equals(originalDefinition, valueTaskType) || - Equals(originalDefinition, lazyOfTType) || - originalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - return UnwrapType(namedType.TypeArguments[0], compilation, wasPlural: wasPlural, seenTypes: seenTypes); - } - } + var supplementaryRules = FallbackNamingRules.CompletionSupplementaryRules; + var semanticFactsService = context.GetRequiredLanguageService(); - return (type, wasPlural); - } + using var _1 = PooledHashSet.GetInstance(out var seenBaseNames); + using var _2 = PooledHashSet.GetInstance(out var seenUniqueNames); - private bool LooksLikeWellKnownCollectionType(Compilation compilation, string name) + foreach (var kind in declarationInfo.PossibleSymbolKinds) { - // see if the user has something like `IEnumerable` (where IEnumerable doesn't bind). Weak - // heuristic. If there's a matching type under System.Collections with that name, then assume it's a - // collection and attempt to create a name from the type arg. - var system = compilation.GlobalNamespace.GetMembers(nameof(System)).OfType().FirstOrDefault(); - var systemCollections = system?.GetMembers(nameof(System.Collections)).OfType().FirstOrDefault(); - - // just check System.Collections, and it's immediate namespace children. This covers all the common cases - // like "Concurrent/Generic/Immutable/Specialized", and prevents having to worry about huge trees to walk. - if (systemCollections is not null) - { - if (Check(systemCollections, name)) - return true; - - foreach (var childNamespace in systemCollections.GetNamespaceMembers()) - { - if (Check(childNamespace, name)) - return true; - } - } - - return false; - - static bool Check(INamespaceSymbol? namespaceSymbol, string name) - => namespaceSymbol != null && namespaceSymbol.GetTypeMembers(name).Any(static t => t.DeclaredAccessibility == Accessibility.Public); + ProcessRules(rules, firstMatchOnly: true, kind, baseNames, declarationInfo, context, result, semanticFactsService, seenBaseNames, seenUniqueNames, cancellationToken); + ProcessRules(supplementaryRules, firstMatchOnly: false, kind, baseNames, declarationInfo, context, result, semanticFactsService, seenBaseNames, seenUniqueNames, cancellationToken); } - private static void GetRecommendedNames( + static void ProcessRules( + ImmutableArray rules, + bool firstMatchOnly, + SymbolSpecification.SymbolKindOrTypeKind kind, ImmutableArray> baseNames, NameDeclarationInfo declarationInfo, CSharpSyntaxContext context, ArrayBuilder<(string, Glyph)> result, - NamingStylePreferences namingStyleOptions, + ISemanticFactsService semanticFactsService, + PooledHashSet seenBaseNames, + PooledHashSet seenUniqueNames, CancellationToken cancellationToken) { - var rules = namingStyleOptions.CreateRules().NamingRules.AddRange(FallbackNamingRules.CompletionFallbackRules); - - var supplementaryRules = FallbackNamingRules.CompletionSupplementaryRules; - var semanticFactsService = context.GetRequiredLanguageService(); - - using var _1 = PooledHashSet.GetInstance(out var seenBaseNames); - using var _2 = PooledHashSet.GetInstance(out var seenUniqueNames); - - foreach (var kind in declarationInfo.PossibleSymbolKinds) - { - ProcessRules(rules, firstMatchOnly: true, kind, baseNames, declarationInfo, context, result, semanticFactsService, seenBaseNames, seenUniqueNames, cancellationToken); - ProcessRules(supplementaryRules, firstMatchOnly: false, kind, baseNames, declarationInfo, context, result, semanticFactsService, seenBaseNames, seenUniqueNames, cancellationToken); - } - - static void ProcessRules( - ImmutableArray rules, - bool firstMatchOnly, - SymbolSpecification.SymbolKindOrTypeKind kind, - ImmutableArray> baseNames, - NameDeclarationInfo declarationInfo, - CSharpSyntaxContext context, - ArrayBuilder<(string, Glyph)> result, - ISemanticFactsService semanticFactsService, - PooledHashSet seenBaseNames, - PooledHashSet seenUniqueNames, - CancellationToken cancellationToken) + var modifiers = declarationInfo.Modifiers; + foreach (var rule in rules) { - var modifiers = declarationInfo.Modifiers; - foreach (var rule in rules) + if (rule.SymbolSpecification.AppliesTo(kind, declarationInfo.Modifiers, declarationInfo.DeclaredAccessibility)) { - if (rule.SymbolSpecification.AppliesTo(kind, declarationInfo.Modifiers, declarationInfo.DeclaredAccessibility)) + foreach (var baseName in baseNames) { - foreach (var baseName in baseNames) - { - var name = rule.NamingStyle.CreateName(baseName).EscapeIdentifier(context.IsInQuery); + var name = rule.NamingStyle.CreateName(baseName).EscapeIdentifier(context.IsInQuery); - // Don't add multiple items for the same name and only add valid identifiers - if (name.Length > 1 && - name != CodeAnalysis.Shared.Extensions.ITypeSymbolExtensions.DefaultParameterName && - CSharpSyntaxFacts.Instance.IsValidIdentifier(name) && - seenBaseNames.Add(name)) + // Don't add multiple items for the same name and only add valid identifiers + if (name.Length > 1 && + name != CodeAnalysis.Shared.Extensions.ITypeSymbolExtensions.DefaultParameterName && + CSharpSyntaxFacts.Instance.IsValidIdentifier(name) && + seenBaseNames.Add(name)) + { + var uniqueName = semanticFactsService.GenerateUniqueName( + context.SemanticModel, + context.TargetToken.GetRequiredParent(), + container: null, + baseName: name, + filter: s => IsRelevantSymbolKind(s), + usedNames: [], + cancellationToken: cancellationToken); + + if (seenUniqueNames.Add(uniqueName.Text)) { - var uniqueName = semanticFactsService.GenerateUniqueName( - context.SemanticModel, - context.TargetToken.GetRequiredParent(), - container: null, - baseName: name, - filter: s => IsRelevantSymbolKind(s), - usedNames: [], - cancellationToken: cancellationToken); - - if (seenUniqueNames.Add(uniqueName.Text)) - { - result.Add((uniqueName.Text, - NameDeclarationInfo.GetGlyph(NameDeclarationInfo.GetSymbolKind(kind), declarationInfo.DeclaredAccessibility))); - } + result.Add((uniqueName.Text, + NameDeclarationInfo.GetGlyph(NameDeclarationInfo.GetSymbolKind(kind), declarationInfo.DeclaredAccessibility))); } } + } - if (firstMatchOnly) - { - // Only consider the first matching specification for each potential symbol or type kind. - // https://github.com/dotnet/roslyn/issues/36248 - break; - } + if (firstMatchOnly) + { + // Only consider the first matching specification for each potential symbol or type kind. + // https://github.com/dotnet/roslyn/issues/36248 + break; } } } } + } - private static void AddNamesFromExistingOverloads( - CSharpSyntaxContext context, NameDeclarationInfo declarationInfo, ArrayBuilder<(string, Glyph)> result, CancellationToken cancellationToken) - { - var semanticModel = context.SemanticModel; - var namedType = semanticModel.GetEnclosingNamedType(context.Position, cancellationToken); - if (namedType is null) - return; + private static void AddNamesFromExistingOverloads( + CSharpSyntaxContext context, NameDeclarationInfo declarationInfo, ArrayBuilder<(string, Glyph)> result, CancellationToken cancellationToken) + { + var semanticModel = context.SemanticModel; + var namedType = semanticModel.GetEnclosingNamedType(context.Position, cancellationToken); + if (namedType is null) + return; - var parameterSyntax = context.LeftToken.GetAncestor(n => n.IsKind(SyntaxKind.Parameter)) as ParameterSyntax; - if (parameterSyntax is not { Type: { } parameterType, Parent.Parent: BaseMethodDeclarationSyntax baseMethod }) - return; + var parameterSyntax = context.LeftToken.GetAncestor(n => n.IsKind(SyntaxKind.Parameter)) as ParameterSyntax; + if (parameterSyntax is not { Type: { } parameterType, Parent.Parent: BaseMethodDeclarationSyntax baseMethod }) + return; - var methodParameterType = semanticModel.GetTypeInfo(parameterType, cancellationToken).Type; - if (methodParameterType is null) - return; + var methodParameterType = semanticModel.GetTypeInfo(parameterType, cancellationToken).Type; + if (methodParameterType is null) + return; - var overloads = GetOverloads(namedType, baseMethod); - if (overloads.IsEmpty) - return; + var overloads = GetOverloads(namedType, baseMethod); + if (overloads.IsEmpty) + return; - var currentParameterNames = baseMethod.ParameterList.Parameters.Select(p => p.Identifier.ValueText).ToImmutableHashSet(); + var currentParameterNames = baseMethod.ParameterList.Parameters.Select(p => p.Identifier.ValueText).ToImmutableHashSet(); - foreach (var overload in overloads) + foreach (var overload in overloads) + { + foreach (var overloadParameter in overload.Parameters) { - foreach (var overloadParameter in overload.Parameters) + if (!currentParameterNames.Contains(overloadParameter.Name) && + methodParameterType.Equals(overloadParameter.Type, SymbolEqualityComparer.Default)) { - if (!currentParameterNames.Contains(overloadParameter.Name) && - methodParameterType.Equals(overloadParameter.Type, SymbolEqualityComparer.Default)) - { - result.Add((overloadParameter.Name, NameDeclarationInfo.GetGlyph(SymbolKind.Parameter, declarationInfo.DeclaredAccessibility))); - } + result.Add((overloadParameter.Name, NameDeclarationInfo.GetGlyph(SymbolKind.Parameter, declarationInfo.DeclaredAccessibility))); } } + } - return; + return; - // Local functions - static ImmutableArray GetOverloads(INamedTypeSymbol namedType, BaseMethodDeclarationSyntax baseMethod) + // Local functions + static ImmutableArray GetOverloads(INamedTypeSymbol namedType, BaseMethodDeclarationSyntax baseMethod) + { + return baseMethod switch { - return baseMethod switch - { - MethodDeclarationSyntax method => namedType.GetMembers(method.Identifier.ValueText).OfType().ToImmutableArray(), - ConstructorDeclarationSyntax constructor => namedType.GetMembers(WellKnownMemberNames.InstanceConstructorName).OfType().ToImmutableArray(), - _ => [] - }; - } + MethodDeclarationSyntax method => namedType.GetMembers(method.Identifier.ValueText).OfType().ToImmutableArray(), + ConstructorDeclarationSyntax constructor => namedType.GetMembers(WellKnownMemberNames.InstanceConstructorName).OfType().ToImmutableArray(), + _ => [] + }; } + } - /// - /// Check if the symbol is a relevant kind. - /// Only relevant if symbol could cause a conflict with a local variable. - /// - private static bool IsRelevantSymbolKind(ISymbol symbol) - { - return symbol.Kind is SymbolKind.Local or - SymbolKind.Parameter or - SymbolKind.RangeVariable; - } + /// + /// Check if the symbol is a relevant kind. + /// Only relevant if symbol could cause a conflict with a local variable. + /// + private static bool IsRelevantSymbolKind(ISymbol symbol) + { + return symbol.Kind is SymbolKind.Local or + SymbolKind.Parameter or + SymbolKind.RangeVariable; } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/ExportDeclarationNameRecommenderAttribute.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/ExportDeclarationNameRecommenderAttribute.cs index e274031c37da3..73425ab54a3cc 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/ExportDeclarationNameRecommenderAttribute.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/ExportDeclarationNameRecommenderAttribute.cs @@ -5,12 +5,11 @@ using System; using System.Composition; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers.DeclarationName +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers.DeclarationName; + +[MetadataAttribute] +[AttributeUsage(AttributeTargets.Class)] +internal sealed class ExportDeclarationNameRecommenderAttribute(string name) : ExportAttribute(typeof(IDeclarationNameRecommender)) { - [MetadataAttribute] - [AttributeUsage(AttributeTargets.Class)] - internal sealed class ExportDeclarationNameRecommenderAttribute(string name) : ExportAttribute(typeof(IDeclarationNameRecommender)) - { - public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); - } + public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/IDeclarationNameRecommender.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/IDeclarationNameRecommender.cs index 2649be5178afa..d2b6817b48537 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/IDeclarationNameRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/IDeclarationNameRecommender.cs @@ -8,15 +8,14 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers.DeclarationName +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers.DeclarationName; + +internal interface IDeclarationNameRecommender { - internal interface IDeclarationNameRecommender - { - Task> ProvideRecommendedNamesAsync( - CompletionContext completionContext, - Document document, - CSharpSyntaxContext context, - NameDeclarationInfo nameInfo, - CancellationToken cancellationToken); - } + Task> ProvideRecommendedNamesAsync( + CompletionContext completionContext, + Document document, + CSharpSyntaxContext context, + NameDeclarationInfo nameInfo, + CancellationToken cancellationToken); } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/EnumAndCompletionListTagCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/EnumAndCompletionListTagCompletionProvider.cs index e233c0e3a88f8..3b8a512286267 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/EnumAndCompletionListTagCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/EnumAndCompletionListTagCompletionProvider.cs @@ -21,335 +21,334 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(EnumAndCompletionListTagCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(CSharpSuggestionModeCompletionProvider))] +[Shared] +internal partial class EnumAndCompletionListTagCompletionProvider : LSPCompletionProvider { - [ExportCompletionProvider(nameof(EnumAndCompletionListTagCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(CSharpSuggestionModeCompletionProvider))] - [Shared] - internal partial class EnumAndCompletionListTagCompletionProvider : LSPCompletionProvider - { - private static readonly CompletionItemRules s_enumTypeRules = - CompletionItemRules.Default.WithCommitCharacterRules([CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, '.')]) - .WithMatchPriority(MatchPriority.Preselect) - .WithSelectionBehavior(CompletionItemSelectionBehavior.HardSelection); + private static readonly CompletionItemRules s_enumTypeRules = + CompletionItemRules.Default.WithCommitCharacterRules([CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, '.')]) + .WithMatchPriority(MatchPriority.Preselect) + .WithSelectionBehavior(CompletionItemSelectionBehavior.HardSelection); - private static readonly ImmutableHashSet s_triggerCharacters = [' ', '[', '(', '~']; + private static readonly ImmutableHashSet s_triggerCharacters = [' ', '[', '(', '~']; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EnumAndCompletionListTagCompletionProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public EnumAndCompletionListTagCompletionProvider() + { + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - { - // Bring up on space or at the start of a word, or after a ( or [. - // - // Note: we don't want to bring this up after traditional enum operators like & or |. - // That's because we don't like the experience where the enum appears directly after the - // operator. Instead, the user normally types and we will bring up the list - // then. - return - text[characterPosition] is ' ' or '[' or '(' or '~' || - options.TriggerOnTypingLetters && CompletionUtilities.IsStartingNewWord(text, characterPosition); - } + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + { + // Bring up on space or at the start of a word, or after a ( or [. + // + // Note: we don't want to bring this up after traditional enum operators like & or |. + // That's because we don't like the experience where the enum appears directly after the + // operator. Instead, the user normally types and we will bring up the list + // then. + return + text[characterPosition] is ' ' or '[' or '(' or '~' || + options.TriggerOnTypingLetters && CompletionUtilities.IsStartingNewWord(text, characterPosition); + } - public override ImmutableHashSet TriggerCharacters => s_triggerCharacters; + public override ImmutableHashSet TriggerCharacters => s_triggerCharacters; - public override async Task ProvideCompletionsAsync(CompletionContext context) + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - try - { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (tree.IsInNonUserCode(position, cancellationToken)) - return; + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (tree.IsInNonUserCode(position, cancellationToken)) + return; - var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); - var semanticModel = syntaxContext.SemanticModel; + var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); + var semanticModel = syntaxContext.SemanticModel; - if (syntaxContext.IsTaskLikeTypeContext) - return; + if (syntaxContext.IsTaskLikeTypeContext) + return; - var token = syntaxContext.TargetToken; - if (token.IsMandatoryNamedParameterPosition()) - return; + var token = syntaxContext.TargetToken; + if (token.IsMandatoryNamedParameterPosition()) + return; - // Don't show up within member access - // This previously worked because the type inferrer didn't work - // in member access expressions. - // The regular SymbolCompletionProvider will handle completion after . - if (token.IsKind(SyntaxKind.DotToken)) - return; + // Don't show up within member access + // This previously worked because the type inferrer didn't work + // in member access expressions. + // The regular SymbolCompletionProvider will handle completion after . + if (token.IsKind(SyntaxKind.DotToken)) + return; - var typeInferenceService = document.GetLanguageService(); - Contract.ThrowIfNull(typeInferenceService, nameof(typeInferenceService)); + var typeInferenceService = document.GetLanguageService(); + Contract.ThrowIfNull(typeInferenceService, nameof(typeInferenceService)); - var infos = typeInferenceService.GetTypeInferenceInfo(semanticModel, position, cancellationToken); + var infos = typeInferenceService.GetTypeInferenceInfo(semanticModel, position, cancellationToken); - if (infos.Length == 0) - infos = [new TypeInferenceInfo(semanticModel.Compilation.ObjectType)]; + if (infos.Length == 0) + infos = [new TypeInferenceInfo(semanticModel.Compilation.ObjectType)]; - foreach (var (type, isParams) in infos) - await HandleSingleTypeAsync(context, semanticModel, token, type, isParams, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, ErrorSeverity.General)) - { - throw ExceptionUtilities.Unreachable(); - } + foreach (var (type, isParams) in infos) + await HandleSingleTypeAsync(context, semanticModel, token, type, isParams, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, ErrorSeverity.General)) + { + throw ExceptionUtilities.Unreachable(); } + } + + private static async Task HandleSingleTypeAsync( + CompletionContext context, SemanticModel semanticModel, SyntaxToken token, ITypeSymbol type, bool isParams, CancellationToken cancellationToken) + { + if (isParams && type is IArrayTypeSymbol arrayType) + type = arrayType.ElementType; - private static async Task HandleSingleTypeAsync( - CompletionContext context, SemanticModel semanticModel, SyntaxToken token, ITypeSymbol type, bool isParams, CancellationToken cancellationToken) + // If we have a Nullable, unwrap it. + if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) { - if (isParams && type is IArrayTypeSymbol arrayType) - type = arrayType.ElementType; + var typeArg = type.GetTypeArguments().FirstOrDefault(); + if (typeArg == null) + return; - // If we have a Nullable, unwrap it. - if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - var typeArg = type.GetTypeArguments().FirstOrDefault(); - if (typeArg == null) - return; + type = typeArg; + } - type = typeArg; - } + // When true, this completion provider shows both the type (e.g. DayOfWeek) and its qualified members (e.g. + // DayOfWeek.Friday). We set this to false for enum-like cases (static members of structs and classes) so we + // only show the qualified members in these cases. + var isEnumOrCompletionListType = true; + var position = context.Position; + var enclosingNamedType = semanticModel.GetEnclosingNamedType(position, cancellationToken); + if (type.TypeKind != TypeKind.Enum) + { + var enumType = TryGetEnumTypeInEnumInitializer(semanticModel, token, type, cancellationToken) ?? + TryGetCompletionListType(type, enclosingNamedType, semanticModel.Compilation); - // When true, this completion provider shows both the type (e.g. DayOfWeek) and its qualified members (e.g. - // DayOfWeek.Friday). We set this to false for enum-like cases (static members of structs and classes) so we - // only show the qualified members in these cases. - var isEnumOrCompletionListType = true; - var position = context.Position; - var enclosingNamedType = semanticModel.GetEnclosingNamedType(position, cancellationToken); - if (type.TypeKind != TypeKind.Enum) + if (enumType == null) { - var enumType = TryGetEnumTypeInEnumInitializer(semanticModel, token, type, cancellationToken) ?? - TryGetCompletionListType(type, enclosingNamedType, semanticModel.Compilation); + if (context.Trigger.Kind == CompletionTriggerKind.Insertion && s_triggerCharacters.Contains(context.Trigger.Character)) + { + // This completion provider understands static members of matching types, but doesn't + // proactively trigger completion for them to avoid interfering with common typing patterns. + return; + } + // If this isn't an enum or marked with completionlist, also check if it contains static members of + // a matching type. These 'enum-like' types have similar characteristics to enum completion, but do + // not show the containing type as a separate item in completion. + isEnumOrCompletionListType = false; + enumType = TryGetTypeWithStaticMembers(type); if (enumType == null) { - if (context.Trigger.Kind == CompletionTriggerKind.Insertion && s_triggerCharacters.Contains(context.Trigger.Character)) - { - // This completion provider understands static members of matching types, but doesn't - // proactively trigger completion for them to avoid interfering with common typing patterns. - return; - } - - // If this isn't an enum or marked with completionlist, also check if it contains static members of - // a matching type. These 'enum-like' types have similar characteristics to enum completion, but do - // not show the containing type as a separate item in completion. - isEnumOrCompletionListType = false; - enumType = TryGetTypeWithStaticMembers(type); - if (enumType == null) - { - return; - } + return; } - - type = enumType; } - var hideAdvancedMembers = context.CompletionOptions.HideAdvancedMembers; - if (!type.IsEditorBrowsable(hideAdvancedMembers, semanticModel.Compilation)) - return; + type = enumType; + } + + var hideAdvancedMembers = context.CompletionOptions.HideAdvancedMembers; + if (!type.IsEditorBrowsable(hideAdvancedMembers, semanticModel.Compilation)) + return; + + // Does type have any aliases? + var alias = await type.FindApplicableAliasAsync(position, semanticModel, cancellationToken).ConfigureAwait(false); - // Does type have any aliases? - var alias = await type.FindApplicableAliasAsync(position, semanticModel, cancellationToken).ConfigureAwait(false); + var displayText = alias != null + ? alias.Name + : type.ToMinimalDisplayString(semanticModel, position); - var displayText = alias != null - ? alias.Name - : type.ToMinimalDisplayString(semanticModel, position); + // Add the enum itself. + var symbol = alias ?? type; + var sortText = symbol.Name; - // Add the enum itself. - var symbol = alias ?? type; - var sortText = symbol.Name; + if (isEnumOrCompletionListType) + { + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( + displayText, + displayTextSuffix: "", + symbols: ImmutableArray.Create(symbol), + rules: s_enumTypeRules, + contextPosition: position, + sortText: sortText)); + } + + // And now all the accessible members of the enum. + if (type.TypeKind == TypeKind.Enum) + { + // We'll want to build a list of the actual enum members and all accessible instances of that enum, too + var index = 0; - if (isEnumOrCompletionListType) + var fields = type.GetMembers().OfType().Where(f => f.IsConst).Where(f => f.HasConstantValue); + foreach (var field in fields.OrderBy(f => IntegerUtilities.ToInt64(f.ConstantValue))) { + index++; + if (!field.IsEditorBrowsable(hideAdvancedMembers, semanticModel.Compilation)) + continue; + + // Use enum member name as an additional filter text, which would promote this item + // during matching when user types member name only, like "Red" instead of + // "Colors.Red" + var memberDisplayName = $"{displayText}.{field.Name}"; + var additionalFilterTexts = ImmutableArray.Create(field.Name); + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( - displayText, + displayText: memberDisplayName, displayTextSuffix: "", - symbols: ImmutableArray.Create(symbol), - rules: s_enumTypeRules, + symbols: ImmutableArray.Create(field), + rules: CompletionItemRules.Default, contextPosition: position, - sortText: sortText)); + sortText: $"{sortText}_{index:0000}", + filterText: memberDisplayName, + tags: WellKnownTagArrays.TargetTypeMatch) + .WithAdditionalFilterTexts(additionalFilterTexts)); } - - // And now all the accessible members of the enum. - if (type.TypeKind == TypeKind.Enum) + } + else if (enclosingNamedType is not null) + { + // Build a list of the members with the same type as the target + foreach (var member in type.GetMembers()) { - // We'll want to build a list of the actual enum members and all accessible instances of that enum, too - var index = 0; + if (!member.CanBeReferencedByName) + continue; - var fields = type.GetMembers().OfType().Where(f => f.IsConst).Where(f => f.HasConstantValue); - foreach (var field in fields.OrderBy(f => IntegerUtilities.ToInt64(f.ConstantValue))) + ISymbol staticSymbol; + ITypeSymbol symbolType; + if (member is IFieldSymbol { IsStatic: true } field) { - index++; - if (!field.IsEditorBrowsable(hideAdvancedMembers, semanticModel.Compilation)) - continue; - - // Use enum member name as an additional filter text, which would promote this item - // during matching when user types member name only, like "Red" instead of - // "Colors.Red" - var memberDisplayName = $"{displayText}.{field.Name}"; - var additionalFilterTexts = ImmutableArray.Create(field.Name); - - context.AddItem(SymbolCompletionItem.CreateWithSymbolId( - displayText: memberDisplayName, - displayTextSuffix: "", - symbols: ImmutableArray.Create(field), - rules: CompletionItemRules.Default, - contextPosition: position, - sortText: $"{sortText}_{index:0000}", - filterText: memberDisplayName, - tags: WellKnownTagArrays.TargetTypeMatch) - .WithAdditionalFilterTexts(additionalFilterTexts)); + staticSymbol = field; + symbolType = field.Type; } - } - else if (enclosingNamedType is not null) - { - // Build a list of the members with the same type as the target - foreach (var member in type.GetMembers()) + else if (member is IPropertySymbol { IsStatic: true, IsIndexer: false } property) + { + staticSymbol = property; + symbolType = property.Type; + } + else { - if (!member.CanBeReferencedByName) - continue; + // Only fields and properties are supported for static member matching + continue; + } - ISymbol staticSymbol; - ITypeSymbol symbolType; - if (member is IFieldSymbol { IsStatic: true } field) - { - staticSymbol = field; - symbolType = field.Type; - } - else if (member is IPropertySymbol { IsStatic: true, IsIndexer: false } property) - { - staticSymbol = property; - symbolType = property.Type; - } - else - { - // Only fields and properties are supported for static member matching - continue; - } + // We only show static properties/fields of compatible type if containing type is NOT marked with completionlist tag. + if (!isEnumOrCompletionListType && !SymbolEqualityComparer.Default.Equals(type, symbolType)) + { + continue; + } - // We only show static properties/fields of compatible type if containing type is NOT marked with completionlist tag. - if (!isEnumOrCompletionListType && !SymbolEqualityComparer.Default.Equals(type, symbolType)) - { - continue; - } + if (!staticSymbol.IsAccessibleWithin(enclosingNamedType) || + !staticSymbol.IsEditorBrowsable(hideAdvancedMembers, semanticModel.Compilation)) + { + continue; + } - if (!staticSymbol.IsAccessibleWithin(enclosingNamedType) || - !staticSymbol.IsEditorBrowsable(hideAdvancedMembers, semanticModel.Compilation)) - { - continue; - } + // Use member name as an additional filter text, which would promote this item + // during matching when user types member name only, like "Empty" instead of + // "ImmutableArray.Empty" + var memberDisplayName = $"{displayText}.{staticSymbol.Name}"; + var additionalFilterTexts = ImmutableArray.Create(staticSymbol.Name); - // Use member name as an additional filter text, which would promote this item - // during matching when user types member name only, like "Empty" instead of - // "ImmutableArray.Empty" - var memberDisplayName = $"{displayText}.{staticSymbol.Name}"; - var additionalFilterTexts = ImmutableArray.Create(staticSymbol.Name); - - context.AddItem(SymbolCompletionItem.CreateWithSymbolId( - displayText: memberDisplayName, - displayTextSuffix: "", - symbols: ImmutableArray.Create(staticSymbol), - rules: CompletionItemRules.Default, - contextPosition: position, - sortText: memberDisplayName, - filterText: memberDisplayName, - tags: WellKnownTagArrays.TargetTypeMatch) - .WithAdditionalFilterTexts(additionalFilterTexts)); - } + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( + displayText: memberDisplayName, + displayTextSuffix: "", + symbols: ImmutableArray.Create(staticSymbol), + rules: CompletionItemRules.Default, + contextPosition: position, + sortText: memberDisplayName, + filterText: memberDisplayName, + tags: WellKnownTagArrays.TargetTypeMatch) + .WithAdditionalFilterTexts(additionalFilterTexts)); } } + } - private static ITypeSymbol? TryGetEnumTypeInEnumInitializer( - SemanticModel semanticModel, SyntaxToken token, - ITypeSymbol type, CancellationToken cancellationToken) + private static ITypeSymbol? TryGetEnumTypeInEnumInitializer( + SemanticModel semanticModel, SyntaxToken token, + ITypeSymbol type, CancellationToken cancellationToken) + { + // https://github.com/dotnet/roslyn/issues/5419 + // + // 14.3: "Within an enum member initializer, values of other enum members are always + // treated as having the type of their underlying type" + + // i.e. if we have "enum E { X, Y, Z = X | + // then we want to offer the enum after the |. However, the compiler will report this + // as an 'int' type, not the enum type. + + // See if we're after a common enum-combining operator. + if (token.Kind() is SyntaxKind.BarToken or + SyntaxKind.AmpersandToken or + SyntaxKind.CaretToken) { - // https://github.com/dotnet/roslyn/issues/5419 - // - // 14.3: "Within an enum member initializer, values of other enum members are always - // treated as having the type of their underlying type" - - // i.e. if we have "enum E { X, Y, Z = X | - // then we want to offer the enum after the |. However, the compiler will report this - // as an 'int' type, not the enum type. - - // See if we're after a common enum-combining operator. - if (token.Kind() is SyntaxKind.BarToken or - SyntaxKind.AmpersandToken or - SyntaxKind.CaretToken) + // See if the type we're looking at is the underlying type for the enum we're contained in. + var containingType = semanticModel.GetEnclosingNamedType(token.SpanStart, cancellationToken); + if (containingType?.TypeKind == TypeKind.Enum && + type.Equals(containingType.EnumUnderlyingType)) { - // See if the type we're looking at is the underlying type for the enum we're contained in. - var containingType = semanticModel.GetEnclosingNamedType(token.SpanStart, cancellationToken); - if (containingType?.TypeKind == TypeKind.Enum && - type.Equals(containingType.EnumUnderlyingType)) + // If so, walk back to the token before the operator token and see if it binds to a member + // of this enum. + var previousToken = token.GetPreviousToken(); + if (previousToken.Parent != null) { - // If so, walk back to the token before the operator token and see if it binds to a member - // of this enum. - var previousToken = token.GetPreviousToken(); - if (previousToken.Parent != null) + var symbol = semanticModel.GetSymbolInfo(previousToken.Parent, cancellationToken).Symbol; + + if (symbol?.Kind == SymbolKind.Field && + containingType.Equals(symbol.ContainingType)) { - var symbol = semanticModel.GetSymbolInfo(previousToken.Parent, cancellationToken).Symbol; - - if (symbol?.Kind == SymbolKind.Field && - containingType.Equals(symbol.ContainingType)) - { - // If so, then offer this as a place for enum completion for the enum we're currently - // inside of. - return containingType; - } + // If so, then offer this as a place for enum completion for the enum we're currently + // inside of. + return containingType; } } } - - return null; } - internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + return null; + } - private static INamedTypeSymbol? TryGetCompletionListType(ITypeSymbol type, INamedTypeSymbol? within, Compilation compilation) - { - if (within == null) - return null; + internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); - // PERF: None of the SpecialTypes include tags, - // so we don't even need to load the documentation. - if (type.IsSpecialType()) - return null; + private static INamedTypeSymbol? TryGetCompletionListType(ITypeSymbol type, INamedTypeSymbol? within, Compilation compilation) + { + if (within == null) + return null; - // PERF: Avoid parsing XML unless the text contains the word "completionlist". - var xmlText = type.GetDocumentationCommentXml(); - if (xmlText == null || !xmlText.Contains(DocumentationCommentXmlNames.CompletionListElementName)) - return null; + // PERF: None of the SpecialTypes include tags, + // so we don't even need to load the documentation. + if (type.IsSpecialType()) + return null; - var documentation = CodeAnalysis.Shared.Utilities.DocumentationComment.FromXmlFragment(xmlText); + // PERF: Avoid parsing XML unless the text contains the word "completionlist". + var xmlText = type.GetDocumentationCommentXml(); + if (xmlText == null || !xmlText.Contains(DocumentationCommentXmlNames.CompletionListElementName)) + return null; - var completionListType = documentation.CompletionListCref != null - ? DocumentationCommentId.GetSymbolsForDeclarationId(documentation.CompletionListCref, compilation).OfType().FirstOrDefault() - : null; + var documentation = CodeAnalysis.Shared.Utilities.DocumentationComment.FromXmlFragment(xmlText); - return completionListType != null && completionListType.IsAccessibleWithin(within) - ? completionListType - : null; - } + var completionListType = documentation.CompletionListCref != null + ? DocumentationCommentId.GetSymbolsForDeclarationId(documentation.CompletionListCref, compilation).OfType().FirstOrDefault() + : null; - private static INamedTypeSymbol? TryGetTypeWithStaticMembers(ITypeSymbol type) - { - // The reference type might be nullable, so we need to remove the annotation. - // Otherwise, we will end up with items like "string?.Empty". - if (type.TypeKind is TypeKind.Struct or TypeKind.Class) - return type.WithNullableAnnotation(NullableAnnotation.NotAnnotated) as INamedTypeSymbol; + return completionListType != null && completionListType.IsAccessibleWithin(within) + ? completionListType + : null; + } - return null; - } + private static INamedTypeSymbol? TryGetTypeWithStaticMembers(ITypeSymbol type) + { + // The reference type might be nullable, so we need to remove the annotation. + // Otherwise, we will end up with items like "string?.Empty". + if (type.TypeKind is TypeKind.Struct or TypeKind.Class) + return type.WithNullableAnnotation(NullableAnnotation.NotAnnotated) as INamedTypeSymbol; + + return null; } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.CompletionSymbolDisplay.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.CompletionSymbolDisplay.cs index 15844a3ffa07d..e36d5298f34aa 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.CompletionSymbolDisplay.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.CompletionSymbolDisplay.cs @@ -8,121 +8,120 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +internal partial class ExplicitInterfaceMemberCompletionProvider { - internal partial class ExplicitInterfaceMemberCompletionProvider + private static class CompletionSymbolDisplay { - private static class CompletionSymbolDisplay - { - public static string ToDisplayString(ISymbol symbol) - => symbol switch - { - IEventSymbol eventSymbol => ToDisplayString(eventSymbol), - IPropertySymbol propertySymbol => ToDisplayString(propertySymbol), - IMethodSymbol methodSymbol => ToDisplayString(methodSymbol), - _ => "" // This shouldn't happen. - }; - - private static string ToDisplayString(IEventSymbol symbol) - => symbol.Name; - - private static string ToDisplayString(IPropertySymbol symbol) + public static string ToDisplayString(ISymbol symbol) + => symbol switch { - using var _ = PooledStringBuilder.GetInstance(out var builder); + IEventSymbol eventSymbol => ToDisplayString(eventSymbol), + IPropertySymbol propertySymbol => ToDisplayString(propertySymbol), + IMethodSymbol methodSymbol => ToDisplayString(methodSymbol), + _ => "" // This shouldn't happen. + }; - if (symbol.IsIndexer) - { - builder.Append("this"); - } - else - { - builder.Append(symbol.Name); - } + private static string ToDisplayString(IEventSymbol symbol) + => symbol.Name; - if (symbol.Parameters.Length > 0) - { - builder.Append('['); - AddParameters(symbol.Parameters, builder); - builder.Append(']'); - } + private static string ToDisplayString(IPropertySymbol symbol) + { + using var _ = PooledStringBuilder.GetInstance(out var builder); - return builder.ToString(); + if (symbol.IsIndexer) + { + builder.Append("this"); } - - private static string ToDisplayString(IMethodSymbol symbol) + else { - using var _ = PooledStringBuilder.GetInstance(out var builder); - switch (symbol.MethodKind) - { - case MethodKind.Ordinary: - builder.Append(symbol.Name); - break; - case MethodKind.UserDefinedOperator: - case MethodKind.BuiltinOperator: - AppendOperatorKeywords(symbol, builder); - builder.Append(SyntaxFacts.GetText(SyntaxFacts.GetOperatorKind(symbol.MetadataName))); - break; - case MethodKind.Conversion: - AppendOperatorKeywords(symbol, builder); - AddType(symbol.ReturnType, builder); - break; - } + builder.Append(symbol.Name); + } - AddTypeArguments(symbol, builder); - builder.Append('('); + if (symbol.Parameters.Length > 0) + { + builder.Append('['); AddParameters(symbol.Parameters, builder); - builder.Append(')'); - return builder.ToString(); + builder.Append(']'); + } + + return builder.ToString(); + } + + private static string ToDisplayString(IMethodSymbol symbol) + { + using var _ = PooledStringBuilder.GetInstance(out var builder); + switch (symbol.MethodKind) + { + case MethodKind.Ordinary: + builder.Append(symbol.Name); + break; + case MethodKind.UserDefinedOperator: + case MethodKind.BuiltinOperator: + AppendOperatorKeywords(symbol, builder); + builder.Append(SyntaxFacts.GetText(SyntaxFacts.GetOperatorKind(symbol.MetadataName))); + break; + case MethodKind.Conversion: + AppendOperatorKeywords(symbol, builder); + AddType(symbol.ReturnType, builder); + break; + } - static void AppendOperatorKeywords(IMethodSymbol symbol, StringBuilder builder) + AddTypeArguments(symbol, builder); + builder.Append('('); + AddParameters(symbol.Parameters, builder); + builder.Append(')'); + return builder.ToString(); + + static void AppendOperatorKeywords(IMethodSymbol symbol, StringBuilder builder) + { + builder.Append("operator "); + if (SyntaxFacts.IsCheckedOperator(symbol.MetadataName)) { - builder.Append("operator "); - if (SyntaxFacts.IsCheckedOperator(symbol.MetadataName)) - { - builder.Append("checked "); - } + builder.Append("checked "); } } + } - private static void AddParameters(ImmutableArray parameters, StringBuilder builder) + private static void AddParameters(ImmutableArray parameters, StringBuilder builder) + { + builder.AppendJoinedValues(", ", parameters, static (parameter, builder) => { - builder.AppendJoinedValues(", ", parameters, static (parameter, builder) => + builder.Append(parameter.RefKind switch { - builder.Append(parameter.RefKind switch - { - RefKind.Out => "out ", - RefKind.Ref => "ref ", - RefKind.In => "in ", - _ => "" - }); - - if (parameter.IsParams) - { - builder.Append("params "); - } - - AddType(parameter.Type, builder); - builder.Append($" {parameter.Name.EscapeIdentifier()}"); + RefKind.Out => "out ", + RefKind.Ref => "ref ", + RefKind.In => "in ", + _ => "" }); - } - private static void AddTypeArguments(IMethodSymbol symbol, StringBuilder builder) - { - if (symbol.TypeArguments.Length > 0) + if (parameter.IsParams) { - builder.Append('<'); - builder.AppendJoinedValues(", ", symbol.TypeArguments, static (symbol, builder) => builder.Append(symbol.Name.EscapeIdentifier())); - builder.Append('>'); + builder.Append("params "); } + + AddType(parameter.Type, builder); + builder.Append($" {parameter.Name.EscapeIdentifier()}"); + }); + } + + private static void AddTypeArguments(IMethodSymbol symbol, StringBuilder builder) + { + if (symbol.TypeArguments.Length > 0) + { + builder.Append('<'); + builder.AppendJoinedValues(", ", symbol.TypeArguments, static (symbol, builder) => builder.Append(symbol.Name.EscapeIdentifier())); + builder.Append('>'); } + } - private static void AddType(ITypeSymbol symbol, StringBuilder builder) + private static void AddType(ITypeSymbol symbol, StringBuilder builder) + { + builder.Append(symbol.ToNameDisplayString()); + if (symbol.NullableAnnotation == NullableAnnotation.Annotated) { - builder.Append(symbol.ToNameDisplayString()); - if (symbol.NullableAnnotation == NullableAnnotation.Annotated) - { - builder.Append('?'); - } + builder.Append('?'); } } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.cs index 33e096df447fd..c6019183e78ef 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.cs @@ -19,122 +19,121 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(ExplicitInterfaceMemberCompletionProvider), LanguageNames.CSharp), Shared] +[ExtensionOrder(After = nameof(UnnamedSymbolCompletionProvider))] +internal partial class ExplicitInterfaceMemberCompletionProvider : LSPCompletionProvider { - [ExportCompletionProvider(nameof(ExplicitInterfaceMemberCompletionProvider), LanguageNames.CSharp), Shared] - [ExtensionOrder(After = nameof(UnnamedSymbolCompletionProvider))] - internal partial class ExplicitInterfaceMemberCompletionProvider : LSPCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ExplicitInterfaceMemberCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ExplicitInterfaceMemberCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => text[characterPosition] == '.'; + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => text[characterPosition] == '.'; - public override ImmutableHashSet TriggerCharacters { get; } = ['.']; + public override ImmutableHashSet TriggerCharacters { get; } = ['.']; - public override async Task ProvideCompletionsAsync(CompletionContext context) + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - try - { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); - var semanticFacts = document.GetRequiredLanguageService(); + var syntaxFacts = document.GetRequiredLanguageService(); + var semanticFacts = document.GetRequiredLanguageService(); - if (syntaxFacts.IsInNonUserCode(syntaxTree, position, cancellationToken) || - syntaxFacts.IsPreProcessorDirectiveContext(syntaxTree, position, cancellationToken)) - { - return; - } + if (syntaxFacts.IsInNonUserCode(syntaxTree, position, cancellationToken) || + syntaxFacts.IsPreProcessorDirectiveContext(syntaxTree, position, cancellationToken)) + { + return; + } - var targetToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken) - .GetPreviousTokenIfTouchingWord(position); + var targetToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken) + .GetPreviousTokenIfTouchingWord(position); - if (!syntaxTree.IsRightOfDotOrArrowOrColonColon(position, targetToken, cancellationToken)) - return; + if (!syntaxTree.IsRightOfDotOrArrowOrColonColon(position, targetToken, cancellationToken)) + return; - var node = targetToken.Parent; - if (node is not ExplicitInterfaceSpecifierSyntax specifierNode) - return; + var node = targetToken.Parent; + if (node is not ExplicitInterfaceSpecifierSyntax specifierNode) + return; - // Bind the interface name which is to the left of the dot - var name = specifierNode.Name; + // Bind the interface name which is to the left of the dot + var name = specifierNode.Name; - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); - var symbol = semanticModel.GetSymbolInfo(name, cancellationToken).Symbol as ITypeSymbol; - if (symbol?.TypeKind != TypeKind.Interface) - return; + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); + var symbol = semanticModel.GetSymbolInfo(name, cancellationToken).Symbol as ITypeSymbol; + if (symbol?.TypeKind != TypeKind.Interface) + return; + + // We're going to create a entry for each one, including the signature + var namePosition = name.SpanStart; + foreach (var member in symbol.GetMembers()) + { + if (!member.IsAbstract && !member.IsVirtual) + continue; - // We're going to create a entry for each one, including the signature - var namePosition = name.SpanStart; - foreach (var member in symbol.GetMembers()) + if (member.IsAccessor() || + member.Kind == SymbolKind.NamedType || + !semanticModel.IsAccessible(node.SpanStart, member)) { - if (!member.IsAbstract && !member.IsVirtual) - continue; - - if (member.IsAccessor() || - member.Kind == SymbolKind.NamedType || - !semanticModel.IsAccessible(node.SpanStart, member)) - { - continue; - } - - var memberString = CompletionSymbolDisplay.ToDisplayString(member); - - // Split the member string into two parts (generally the name, and the signature portion). We want - // the split so that other features (like spell-checking), only look at the name portion. - var (displayText, displayTextSuffix) = SplitMemberName(memberString); - - context.AddItem(SymbolCompletionItem.CreateWithSymbolId( - displayText, - displayTextSuffix, - insertionText: memberString, - symbols: ImmutableArray.Create(member), - contextPosition: position, - rules: CompletionItemRules.Default)); + continue; } - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) - { - // nop + + var memberString = CompletionSymbolDisplay.ToDisplayString(member); + + // Split the member string into two parts (generally the name, and the signature portion). We want + // the split so that other features (like spell-checking), only look at the name portion. + var (displayText, displayTextSuffix) = SplitMemberName(memberString); + + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( + displayText, + displayTextSuffix, + insertionText: memberString, + symbols: ImmutableArray.Create(member), + contextPosition: position, + rules: CompletionItemRules.Default)); } } - - private static (string text, string suffix) SplitMemberName(string memberString) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) { - for (var i = 0; i < memberString.Length; i++) - { - if (!SyntaxFacts.IsIdentifierPartCharacter(memberString[i])) - return (memberString[0..i], memberString[i..]); - } - - return (memberString, ""); + // nop } + } - internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); - - public override Task GetTextChangeAsync( - Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + private static (string text, string suffix) SplitMemberName(string memberString) + { + for (var i = 0; i < memberString.Length; i++) { - // If the user is typing a punctuation portion of the signature, then just emit the name. i.e. if the - // member is `Contains(string key)`, then typing `<` should just emit `Contains` and not - // `Contains(string key)<` - return Task.FromResult(new TextChange( - selectedItem.Span, - ch is '(' or '[' or '<' - ? selectedItem.DisplayText - : SymbolCompletionItem.GetInsertionText(selectedItem))); + if (!SyntaxFacts.IsIdentifierPartCharacter(memberString[i])) + return (memberString[0..i], memberString[i..]); } + + return (memberString, ""); + } + + internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + + public override Task GetTextChangeAsync( + Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + { + // If the user is typing a punctuation portion of the signature, then just emit the name. i.e. if the + // member is `Contains(string key)`, then typing `<` should just emit `Contains` and not + // `Contains(string key)<` + return Task.FromResult(new TextChange( + selectedItem.Span, + ch is '(' or '[' or '<' + ? selectedItem.DisplayText + : SymbolCompletionItem.GetInsertionText(selectedItem))); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceTypeCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceTypeCompletionProvider.cs index df26a884f5357..125daeff7ec09 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceTypeCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceTypeCompletionProvider.cs @@ -21,147 +21,146 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(ExplicitInterfaceTypeCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(ExplicitInterfaceMemberCompletionProvider))] +[Shared] +internal partial class ExplicitInterfaceTypeCompletionProvider : AbstractSymbolCompletionProvider { - [ExportCompletionProvider(nameof(ExplicitInterfaceTypeCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(ExplicitInterfaceMemberCompletionProvider))] - [Shared] - internal partial class ExplicitInterfaceTypeCompletionProvider : AbstractSymbolCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ExplicitInterfaceTypeCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ExplicitInterfaceTypeCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, insertedCharacterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, insertedCharacterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; - protected override (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(ISymbol symbol, CSharpSyntaxContext context) - => CompletionUtilities.GetDisplayAndSuffixAndInsertionText(symbol, context); + protected override (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(ISymbol symbol, CSharpSyntaxContext context) + => CompletionUtilities.GetDisplayAndSuffixAndInsertionText(symbol, context); - public override async Task ProvideCompletionsAsync(CompletionContext context) + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - try - { - var completionCount = context.Items.Count; - await base.ProvideCompletionsAsync(context).ConfigureAwait(false); - - if (completionCount < context.Items.Count) - { - // If we added any items, then add a suggestion mode item as this is a location - // where a member name could be written, and we should not interfere with that. - context.SuggestionModeItem = CreateSuggestionModeItem( - CSharpFeaturesResources.member_name, - CSharpFeaturesResources.Autoselect_disabled_due_to_member_declaration); - } - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) + var completionCount = context.Items.Count; + await base.ProvideCompletionsAsync(context).ConfigureAwait(false); + + if (completionCount < context.Items.Count) { - // nop + // If we added any items, then add a suggestion mode item as this is a location + // where a member name could be written, and we should not interfere with that. + context.SuggestionModeItem = CreateSuggestionModeItem( + CSharpFeaturesResources.member_name, + CSharpFeaturesResources.Autoselect_disabled_due_to_member_declaration); } } - - protected override Task> GetSymbolsAsync( - CompletionContext? completionContext, CSharpSyntaxContext context, int position, CompletionOptions options, CancellationToken cancellationToken) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) { - var targetToken = context.TargetToken; + // nop + } + } + + protected override Task> GetSymbolsAsync( + CompletionContext? completionContext, CSharpSyntaxContext context, int position, CompletionOptions options, CancellationToken cancellationToken) + { + var targetToken = context.TargetToken; - // Don't want to offer this after "async" (even though the compiler may parse that as a type). - if (SyntaxFacts.GetContextualKeywordKind(targetToken.ValueText) == SyntaxKind.AsyncKeyword) - return SpecializedTasks.EmptyImmutableArray(); + // Don't want to offer this after "async" (even though the compiler may parse that as a type). + if (SyntaxFacts.GetContextualKeywordKind(targetToken.ValueText) == SyntaxKind.AsyncKeyword) + return SpecializedTasks.EmptyImmutableArray(); - var potentialTypeNode = targetToken.Parent; - if (targetToken.IsKind(SyntaxKind.GreaterThanToken) && potentialTypeNode is TypeArgumentListSyntax typeArgumentList) - potentialTypeNode = typeArgumentList.Parent; + var potentialTypeNode = targetToken.Parent; + if (targetToken.IsKind(SyntaxKind.GreaterThanToken) && potentialTypeNode is TypeArgumentListSyntax typeArgumentList) + potentialTypeNode = typeArgumentList.Parent; - var typeNode = potentialTypeNode as TypeSyntax; + var typeNode = potentialTypeNode as TypeSyntax; - while (typeNode != null) + while (typeNode != null) + { + if (typeNode.Parent is TypeSyntax parentType && parentType.Span.End < position) { - if (typeNode.Parent is TypeSyntax parentType && parentType.Span.End < position) - { - typeNode = parentType; - } - else - { - break; - } + typeNode = parentType; } + else + { + break; + } + } - if (typeNode == null) - return SpecializedTasks.EmptyImmutableArray(); - - // We weren't after something that looked like a type. - var tokenBeforeType = typeNode.GetFirstToken().GetPreviousToken(); - - if (!IsPreviousTokenValid(tokenBeforeType)) - return SpecializedTasks.EmptyImmutableArray(); + if (typeNode == null) + return SpecializedTasks.EmptyImmutableArray(); - var typeDeclaration = typeNode.GetAncestor(); - if (typeDeclaration == null) - return SpecializedTasks.EmptyImmutableArray(); + // We weren't after something that looked like a type. + var tokenBeforeType = typeNode.GetFirstToken().GetPreviousToken(); - // Looks syntactically good. See what interfaces our containing class/struct/interface has - Debug.Assert(IsClassOrStructOrInterfaceOrRecord(typeDeclaration)); + if (!IsPreviousTokenValid(tokenBeforeType)) + return SpecializedTasks.EmptyImmutableArray(); - var semanticModel = context.SemanticModel; - var namedType = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken); - Contract.ThrowIfNull(namedType); + var typeDeclaration = typeNode.GetAncestor(); + if (typeDeclaration == null) + return SpecializedTasks.EmptyImmutableArray(); - using var _ = PooledHashSet.GetInstance(out var interfaceSet); - foreach (var directInterface in namedType.Interfaces) - { - interfaceSet.Add(directInterface); - interfaceSet.AddRange(directInterface.AllInterfaces); - } + // Looks syntactically good. See what interfaces our containing class/struct/interface has + Debug.Assert(IsClassOrStructOrInterfaceOrRecord(typeDeclaration)); - return Task.FromResult(interfaceSet.SelectAsArray(t => new SymbolAndSelectionInfo(Symbol: t, Preselect: false))); - } + var semanticModel = context.SemanticModel; + var namedType = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken); + Contract.ThrowIfNull(namedType); - private static bool IsPreviousTokenValid(SyntaxToken tokenBeforeType) + using var _ = PooledHashSet.GetInstance(out var interfaceSet); + foreach (var directInterface in namedType.Interfaces) { - if (tokenBeforeType.Kind() == SyntaxKind.AsyncKeyword) - { - tokenBeforeType = tokenBeforeType.GetPreviousToken(); - } + interfaceSet.Add(directInterface); + interfaceSet.AddRange(directInterface.AllInterfaces); + } - if (tokenBeforeType.Kind() == SyntaxKind.OpenBraceToken) - { - // Show us after the open brace for a class/struct/interface - return IsClassOrStructOrInterfaceOrRecord(tokenBeforeType.GetRequiredParent()); - } + return Task.FromResult(interfaceSet.SelectAsArray(t => new SymbolAndSelectionInfo(Symbol: t, Preselect: false))); + } - if (tokenBeforeType.Kind() is SyntaxKind.CloseBraceToken or - SyntaxKind.SemicolonToken) - { - // Check that we're after a class/struct/interface member. - var memberDeclaration = tokenBeforeType.GetAncestor(); - return memberDeclaration?.GetLastToken() == tokenBeforeType && - IsClassOrStructOrInterfaceOrRecord(memberDeclaration.GetRequiredParent()); - } + private static bool IsPreviousTokenValid(SyntaxToken tokenBeforeType) + { + if (tokenBeforeType.Kind() == SyntaxKind.AsyncKeyword) + { + tokenBeforeType = tokenBeforeType.GetPreviousToken(); + } - return false; + if (tokenBeforeType.Kind() == SyntaxKind.OpenBraceToken) + { + // Show us after the open brace for a class/struct/interface + return IsClassOrStructOrInterfaceOrRecord(tokenBeforeType.GetRequiredParent()); } - private static bool IsClassOrStructOrInterfaceOrRecord(SyntaxNode node) - => node.Kind() is SyntaxKind.ClassDeclaration or SyntaxKind.StructDeclaration or - SyntaxKind.InterfaceDeclaration or SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration; - - protected override CompletionItem CreateItem( - CompletionContext completionContext, - string displayText, - string displayTextSuffix, - string insertionText, - ImmutableArray symbols, - CSharpSyntaxContext context, - SupportedPlatformData? supportedPlatformData) + if (tokenBeforeType.Kind() is SyntaxKind.CloseBraceToken or + SyntaxKind.SemicolonToken) { - return CreateItemDefault(displayText, displayTextSuffix, insertionText, symbols, context, supportedPlatformData); + // Check that we're after a class/struct/interface member. + var memberDeclaration = tokenBeforeType.GetAncestor(); + return memberDeclaration?.GetLastToken() == tokenBeforeType && + IsClassOrStructOrInterfaceOrRecord(memberDeclaration.GetRequiredParent()); } + + return false; + } + + private static bool IsClassOrStructOrInterfaceOrRecord(SyntaxNode node) + => node.Kind() is SyntaxKind.ClassDeclaration or SyntaxKind.StructDeclaration or + SyntaxKind.InterfaceDeclaration or SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration; + + protected override CompletionItem CreateItem( + CompletionContext completionContext, + string displayText, + string displayTextSuffix, + string insertionText, + ImmutableArray symbols, + CSharpSyntaxContext context, + SupportedPlatformData? supportedPlatformData) + { + return CreateItemDefault(displayText, displayTextSuffix, insertionText, symbols, context, supportedPlatformData); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ExternAliasCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ExternAliasCompletionProvider.cs index f4fb29715dbcb..d96114941c2fd 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ExternAliasCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ExternAliasCompletionProvider.cs @@ -17,81 +17,80 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(ExternAliasCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(SnippetCompletionProvider))] +[Shared] +internal class ExternAliasCompletionProvider : LSPCompletionProvider { - [ExportCompletionProvider(nameof(ExternAliasCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(SnippetCompletionProvider))] - [Shared] - internal class ExternAliasCompletionProvider : LSPCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ExternAliasCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ExternAliasCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; - public override async Task ProvideCompletionsAsync(CompletionContext context) + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - try - { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (tree.IsInNonUserCode(position, cancellationToken)) - { - return; - } + if (tree.IsInNonUserCode(position, cancellationToken)) + { + return; + } - var targetToken = tree - .FindTokenOnLeftOfPosition(position, cancellationToken) - .GetPreviousTokenIfTouchingWord(position); + var targetToken = tree + .FindTokenOnLeftOfPosition(position, cancellationToken) + .GetPreviousTokenIfTouchingWord(position); - if (!targetToken.IsKind(SyntaxKind.AliasKeyword) - && !(targetToken.IsKind(SyntaxKind.IdentifierToken) && targetToken.HasMatchingText(SyntaxKind.AliasKeyword))) - { - return; - } + if (!targetToken.IsKind(SyntaxKind.AliasKeyword) + && !(targetToken.IsKind(SyntaxKind.IdentifierToken) && targetToken.HasMatchingText(SyntaxKind.AliasKeyword))) + { + return; + } - if (targetToken.Parent.IsKind(SyntaxKind.ExternAliasDirective) - || (targetToken.Parent.IsKind(SyntaxKind.IdentifierName) && targetToken.Parent.IsParentKind(SyntaxKind.IncompleteMember))) - { - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var aliases = compilation.ExternalReferences.SelectMany(r => r.Properties.Aliases).ToSet(); + if (targetToken.Parent.IsKind(SyntaxKind.ExternAliasDirective) + || (targetToken.Parent.IsKind(SyntaxKind.IdentifierName) && targetToken.Parent.IsParentKind(SyntaxKind.IncompleteMember))) + { + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var aliases = compilation.ExternalReferences.SelectMany(r => r.Properties.Aliases).ToSet(); - if (aliases.Any()) - { - var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var usedAliases = root.ChildNodes().OfType() - .Where(e => !e.Identifier.IsMissing) - .Select(e => e.Identifier.ValueText); + if (aliases.Any()) + { + var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var usedAliases = root.ChildNodes().OfType() + .Where(e => !e.Identifier.IsMissing) + .Select(e => e.Identifier.ValueText); - aliases.RemoveRange(usedAliases); - aliases.Remove(MetadataReferenceProperties.GlobalAlias); + aliases.RemoveRange(usedAliases); + aliases.Remove(MetadataReferenceProperties.GlobalAlias); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - foreach (var alias in aliases) - { - context.AddItem(CommonCompletionItem.Create( - alias, displayTextSuffix: "", CompletionItemRules.Default, glyph: Glyph.Namespace)); - } + foreach (var alias in aliases) + { + context.AddItem(CommonCompletionItem.Create( + alias, displayTextSuffix: "", CompletionItemRules.Default, glyph: Glyph.Namespace)); } } } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) - { - // nop - } + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) + { + // nop } } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/FirstBuiltInCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/FirstBuiltInCompletionProvider.cs index 83e31999d025b..9111d64369768 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/FirstBuiltInCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/FirstBuiltInCompletionProvider.cs @@ -8,23 +8,22 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +/// +/// Provides a completion provider that always appears before any built-in completion provider. This completion +/// provider does not provide any completions. +/// +[ExportCompletionProvider(nameof(FirstBuiltInCompletionProvider), LanguageNames.CSharp)] +[Shared] +internal sealed class FirstBuiltInCompletionProvider : CompletionProvider { - /// - /// Provides a completion provider that always appears before any built-in completion provider. This completion - /// provider does not provide any completions. - /// - [ExportCompletionProvider(nameof(FirstBuiltInCompletionProvider), LanguageNames.CSharp)] - [Shared] - internal sealed class FirstBuiltInCompletionProvider : CompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public FirstBuiltInCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public FirstBuiltInCompletionProvider() - { - } - - public override Task ProvideCompletionsAsync(CompletionContext context) - => Task.CompletedTask; } + + public override Task ProvideCompletionsAsync(CompletionContext context) + => Task.CompletedTask; } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/FunctionPointerUnmanagedCallingConventionCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/FunctionPointerUnmanagedCallingConventionCompletionProvider.cs index 41c13a700847c..be5cce2b08186 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/FunctionPointerUnmanagedCallingConventionCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/FunctionPointerUnmanagedCallingConventionCompletionProvider.cs @@ -20,123 +20,122 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(FunctionPointerUnmanagedCallingConventionCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(AggregateEmbeddedLanguageCompletionProvider))] +[Shared] +internal partial class FunctionPointerUnmanagedCallingConventionCompletionProvider : LSPCompletionProvider { - [ExportCompletionProvider(nameof(FunctionPointerUnmanagedCallingConventionCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(AggregateEmbeddedLanguageCompletionProvider))] - [Shared] - internal partial class FunctionPointerUnmanagedCallingConventionCompletionProvider : LSPCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public FunctionPointerUnmanagedCallingConventionCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public FunctionPointerUnmanagedCallingConventionCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; - private static readonly ImmutableArray s_predefinedCallingConventions = ["Cdecl", "Fastcall", "Thiscall", "Stdcall"]; + private static readonly ImmutableArray s_predefinedCallingConventions = ["Cdecl", "Fastcall", "Thiscall", "Stdcall"]; - public override async Task ProvideCompletionsAsync(CompletionContext context) + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - try + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; + + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (syntaxTree.IsInNonUserCode(position, cancellationToken)) { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; - - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (syntaxTree.IsInNonUserCode(position, cancellationToken)) - { - return; - } - - var token = syntaxTree - .FindTokenOnLeftOfPosition(position, cancellationToken) - .GetPreviousTokenIfTouchingWord(position); - - if (token.Kind() is not (SyntaxKind.OpenBracketToken or SyntaxKind.CommaToken)) - { - return; - } - - if (token.Parent is not FunctionPointerUnmanagedCallingConventionListSyntax callingConventionList) - { - return; - } - - var contextPosition = token.SpanStart; - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(callingConventionList, cancellationToken).ConfigureAwait(false); - - var completionItems = new HashSet(CompletionItemComparer.Instance); - AddTypes(completionItems, contextPosition, semanticModel, cancellationToken); - - // Even if we didn't have types, there are four magic calling conventions recognized regardless. - // We add these after doing the type lookup so if we had types we can show that instead - foreach (var callingConvention in s_predefinedCallingConventions) - { - completionItems.Add(CompletionItem.Create(callingConvention, tags: GlyphTags.GetTags(Glyph.Keyword))); - } - - context.AddItems(completionItems); + return; } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) + + var token = syntaxTree + .FindTokenOnLeftOfPosition(position, cancellationToken) + .GetPreviousTokenIfTouchingWord(position); + + if (token.Kind() is not (SyntaxKind.OpenBracketToken or SyntaxKind.CommaToken)) { - // nop + return; } - } - private static void AddTypes(HashSet completionItems, int contextPosition, SemanticModel semanticModel, CancellationToken cancellationToken) - { - // We have to find the set of types that meet the criteria listed in - // https://github.com/dotnet/csharplang/blob/main/proposals/csharp-9.0/function-pointers.md#mapping-the-calling_convention_specifier-to-a-callkind - // We skip the check of an type being in the core assembly since that's not really necessary for our work. - var compilerServicesNamespace = semanticModel.Compilation.GlobalNamespace.GetQualifiedNamespace("System.Runtime.CompilerServices"); - if (compilerServicesNamespace == null) + if (token.Parent is not FunctionPointerUnmanagedCallingConventionListSyntax callingConventionList) { return; } - foreach (var type in compilerServicesNamespace.GetTypeMembers()) + var contextPosition = token.SpanStart; + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(callingConventionList, cancellationToken).ConfigureAwait(false); + + var completionItems = new HashSet(CompletionItemComparer.Instance); + AddTypes(completionItems, contextPosition, semanticModel, cancellationToken); + + // Even if we didn't have types, there are four magic calling conventions recognized regardless. + // We add these after doing the type lookup so if we had types we can show that instead + foreach (var callingConvention in s_predefinedCallingConventions) { - cancellationToken.ThrowIfCancellationRequested(); - - const string CallConvPrefix = "CallConv"; - - if (type.DeclaredAccessibility == Accessibility.Public && type.Name.StartsWith(CallConvPrefix)) - { - var displayName = type.Name[CallConvPrefix.Length..]; - completionItems.Add( - SymbolCompletionItem.CreateWithSymbolId( - displayName, - ImmutableArray.Create(type), - rules: CompletionItemRules.Default, - contextPosition)); - } + completionItems.Add(CompletionItem.Create(callingConvention, tags: GlyphTags.GetTags(Glyph.Keyword))); } + + context.AddItems(completionItems); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) + { + // nop } + } - internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + private static void AddTypes(HashSet completionItems, int contextPosition, SemanticModel semanticModel, CancellationToken cancellationToken) + { + // We have to find the set of types that meet the criteria listed in + // https://github.com/dotnet/csharplang/blob/main/proposals/csharp-9.0/function-pointers.md#mapping-the-calling_convention_specifier-to-a-callkind + // We skip the check of an type being in the core assembly since that's not really necessary for our work. + var compilerServicesNamespace = semanticModel.Compilation.GlobalNamespace.GetQualifiedNamespace("System.Runtime.CompilerServices"); + if (compilerServicesNamespace == null) + { + return; + } - private class CompletionItemComparer : IEqualityComparer + foreach (var type in compilerServicesNamespace.GetTypeMembers()) { - public static readonly IEqualityComparer Instance = new CompletionItemComparer(); + cancellationToken.ThrowIfCancellationRequested(); - public bool Equals(CompletionItem? x, CompletionItem? y) - { - return x?.DisplayText == y?.DisplayText; - } + const string CallConvPrefix = "CallConv"; - public int GetHashCode(CompletionItem obj) + if (type.DeclaredAccessibility == Accessibility.Public && type.Name.StartsWith(CallConvPrefix)) { - return obj?.DisplayText.GetHashCode() ?? 0; + var displayName = type.Name[CallConvPrefix.Length..]; + completionItems.Add( + SymbolCompletionItem.CreateWithSymbolId( + displayName, + ImmutableArray.Create(type), + rules: CompletionItemRules.Default, + contextPosition)); } } } + + internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + + private class CompletionItemComparer : IEqualityComparer + { + public static readonly IEqualityComparer Instance = new CompletionItemComparer(); + + public bool Equals(CompletionItem? x, CompletionItem? y) + { + return x?.DisplayText == y?.DisplayText; + } + + public int GetHashCode(CompletionItem obj) + { + return obj?.DisplayText.GetHashCode() ?? 0; + } + } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ExtensionMethodImportCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ExtensionMethodImportCompletionProvider.cs index 5114d89d495dd..01180808fe0e0 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ExtensionMethodImportCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ExtensionMethodImportCompletionProvider.cs @@ -13,52 +13,51 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(ExtensionMethodImportCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(TypeImportCompletionProvider))] +[Shared] +internal sealed class ExtensionMethodImportCompletionProvider : AbstractExtensionMethodImportCompletionProvider { - [ExportCompletionProvider(nameof(ExtensionMethodImportCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(TypeImportCompletionProvider))] - [Shared] - internal sealed class ExtensionMethodImportCompletionProvider : AbstractExtensionMethodImportCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ExtensionMethodImportCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ExtensionMethodImportCompletionProvider() - { - } - - internal override string Language => LanguageNames.CSharp; + } - protected override string GenericSuffix => "<>"; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + protected override string GenericSuffix => "<>"; - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); - protected override bool IsFinalSemicolonOfUsingOrExtern(SyntaxNode directive, SyntaxToken token) - { - if (token.IsKind(SyntaxKind.None) || token.IsMissing) - return false; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; - return directive switch - { - UsingDirectiveSyntax usingDirective => usingDirective.SemicolonToken == token, - ExternAliasDirectiveSyntax externAliasDirective => externAliasDirective.SemicolonToken == token, - _ => false, - }; - } + protected override bool IsFinalSemicolonOfUsingOrExtern(SyntaxNode directive, SyntaxToken token) + { + if (token.IsKind(SyntaxKind.None) || token.IsMissing) + return false; - protected override Task ShouldProvideParenthesisCompletionAsync( - Document document, - CompletionItem item, - char? commitKey, - CancellationToken cancellationToken) - // Ideally we should check if the inferred type for this location is delegate to decide whether to add parenthesis or not - // However, for an extension method like - // static class C { public static int ToInt(this Bar b) => 1; } - // it can only be used as like: bar.ToInt(); - // Func x = bar.ToInt or Func x = bar.ToInt is illegal. It can't be assign to delegate. - // Therefore at here we always assume the user always wants to add parenthesis. - => Task.FromResult(commitKey is ';' or '.'); + return directive switch + { + UsingDirectiveSyntax usingDirective => usingDirective.SemicolonToken == token, + ExternAliasDirectiveSyntax externAliasDirective => externAliasDirective.SemicolonToken == token, + _ => false, + }; } + + protected override Task ShouldProvideParenthesisCompletionAsync( + Document document, + CompletionItem item, + char? commitKey, + CancellationToken cancellationToken) + // Ideally we should check if the inferred type for this location is delegate to decide whether to add parenthesis or not + // However, for an extension method like + // static class C { public static int ToInt(this Bar b) => 1; } + // it can only be used as like: bar.ToInt(); + // Func x = bar.ToInt or Func x = bar.ToInt is illegal. It can't be assign to delegate. + // Therefore at here we always assume the user always wants to add parenthesis. + => Task.FromResult(commitKey is ';' or '.'); } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs index ceed532da2c64..a30ad09ef2d06 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs @@ -15,55 +15,54 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(TypeImportCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(PropertySubpatternCompletionProvider))] +[Shared] +internal sealed class TypeImportCompletionProvider : AbstractTypeImportCompletionProvider { - [ExportCompletionProvider(nameof(TypeImportCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(PropertySubpatternCompletionProvider))] - [Shared] - internal sealed class TypeImportCompletionProvider : AbstractTypeImportCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public TypeImportCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public TypeImportCompletionProvider() - { - } - - internal override string Language => LanguageNames.CSharp; + } - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + internal override string Language => LanguageNames.CSharp; - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); - protected override bool IsFinalSemicolonOfUsingOrExtern(SyntaxNode directive, SyntaxToken token) - { - if (token.IsKind(SyntaxKind.None) || token.IsMissing) - return false; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; - return directive switch - { - UsingDirectiveSyntax usingDirective => usingDirective.SemicolonToken == token, - ExternAliasDirectiveSyntax externAliasDirective => externAliasDirective.SemicolonToken == token, - _ => false, - }; - } + protected override bool IsFinalSemicolonOfUsingOrExtern(SyntaxNode directive, SyntaxToken token) + { + if (token.IsKind(SyntaxKind.None) || token.IsMissing) + return false; - protected override async Task ShouldProvideParenthesisCompletionAsync( - Document document, - CompletionItem item, - char? commitKey, - CancellationToken cancellationToken) + return directive switch { - if (commitKey is ';' or '.') - { - // Only consider add '()' if the type is used under object creation context - var position = item.Span.Start; - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); - return syntaxTree.IsObjectCreationTypeContext(position, leftToken, cancellationToken); - } + UsingDirectiveSyntax usingDirective => usingDirective.SemicolonToken == token, + ExternAliasDirectiveSyntax externAliasDirective => externAliasDirective.SemicolonToken == token, + _ => false, + }; + } - return false; + protected override async Task ShouldProvideParenthesisCompletionAsync( + Document document, + CompletionItem item, + char? commitKey, + CancellationToken cancellationToken) + { + if (commitKey is ';' or '.') + { + // Only consider add '()' if the type is used under object creation context + var position = item.Span.Start; + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); + return syntaxTree.IsObjectCreationTypeContext(position, leftToken, cancellationToken); } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionServiceFactory.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionServiceFactory.cs index 6a9a8bfe998c4..7adcefd4bad9d 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionServiceFactory.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionServiceFactory.cs @@ -9,28 +9,27 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.TestHooks; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportLanguageServiceFactory(typeof(ITypeImportCompletionService), LanguageNames.CSharp), Shared] +internal sealed class TypeImportCompletionServiceFactory : ILanguageServiceFactory { - [ExportLanguageServiceFactory(typeof(ITypeImportCompletionService), LanguageNames.CSharp), Shared] - internal sealed class TypeImportCompletionServiceFactory : ILanguageServiceFactory + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public TypeImportCompletionServiceFactory() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public TypeImportCompletionServiceFactory() - { - } + } - public ILanguageService CreateLanguageService(HostLanguageServices languageServices) - => new CSharpTypeImportCompletionService(languageServices.LanguageServices.SolutionServices); + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + => new CSharpTypeImportCompletionService(languageServices.LanguageServices.SolutionServices); - private class CSharpTypeImportCompletionService(SolutionServices services) : AbstractTypeImportCompletionService(services) - { - protected override string GenericTypeSuffix - => "<>"; + private class CSharpTypeImportCompletionService(SolutionServices services) : AbstractTypeImportCompletionService(services) + { + protected override string GenericTypeSuffix + => "<>"; - protected override bool IsCaseSensitive => true; + protected override bool IsCaseSensitive => true; - protected override string Language => LanguageNames.CSharp; - } + protected override string Language => LanguageNames.CSharp; } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/InternalsVisibleToCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/InternalsVisibleToCompletionProvider.cs index 200c86243c2e1..e0b4cf5e49c36 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/InternalsVisibleToCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/InternalsVisibleToCompletionProvider.cs @@ -11,52 +11,51 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(InternalsVisibleToCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(DeclarationNameCompletionProvider))] +[Shared] +internal sealed class InternalsVisibleToCompletionProvider : AbstractInternalsVisibleToCompletionProvider { - [ExportCompletionProvider(nameof(InternalsVisibleToCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(DeclarationNameCompletionProvider))] - [Shared] - internal sealed class InternalsVisibleToCompletionProvider : AbstractInternalsVisibleToCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public InternalsVisibleToCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public InternalsVisibleToCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - protected override IImmutableList GetAssemblyScopedAttributeSyntaxNodesOfDocument(SyntaxNode documentRoot) + protected override IImmutableList GetAssemblyScopedAttributeSyntaxNodesOfDocument(SyntaxNode documentRoot) + { + var builder = (ImmutableList.Builder?)null; + if (documentRoot is CompilationUnitSyntax compilationUnit) { - var builder = (ImmutableList.Builder?)null; - if (documentRoot is CompilationUnitSyntax compilationUnit) + foreach (var attributeList in compilationUnit.AttributeLists) { - foreach (var attributeList in compilationUnit.AttributeLists) - { - // For most documents the compilationUnit.AttributeLists should be empty. - // Therefore we delay initialization of the builder - builder ??= ImmutableList.CreateBuilder(); - builder.AddRange(attributeList.Attributes); - } + // For most documents the compilationUnit.AttributeLists should be empty. + // Therefore we delay initialization of the builder + builder ??= ImmutableList.CreateBuilder(); + builder.AddRange(attributeList.Attributes); } - - return builder == null - ? [] - : builder.ToImmutable(); } - protected override SyntaxNode? GetConstructorArgumentOfInternalsVisibleToAttribute(SyntaxNode internalsVisibleToAttribute) - { - var arguments = ((AttributeSyntax)internalsVisibleToAttribute).ArgumentList!.Arguments; - // InternalsVisibleTo has only one constructor argument. - // https://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.internalsvisibletoattribute.internalsvisibletoattribute(v=vs.110).aspx - // We can assume that this is the assemblyName argument. - return arguments.Count > 0 - ? arguments[0].Expression - : null; - } + return builder == null + ? [] + : builder.ToImmutable(); + } - protected override bool ShouldTriggerAfterQuotes(SourceText text, int insertedCharacterPosition) - => CompletionUtilities.IsStartingNewWord(text, insertedCharacterPosition); + protected override SyntaxNode? GetConstructorArgumentOfInternalsVisibleToAttribute(SyntaxNode internalsVisibleToAttribute) + { + var arguments = ((AttributeSyntax)internalsVisibleToAttribute).ArgumentList!.Arguments; + // InternalsVisibleTo has only one constructor argument. + // https://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.internalsvisibletoattribute.internalsvisibletoattribute(v=vs.110).aspx + // We can assume that this is the assemblyName argument. + return arguments.Count > 0 + ? arguments[0].Expression + : null; } + + protected override bool ShouldTriggerAfterQuotes(SourceText text, int insertedCharacterPosition) + => CompletionUtilities.IsStartingNewWord(text, insertedCharacterPosition); } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs index fb5d452dcf629..c4d4255edb937 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs @@ -13,189 +13,188 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(KeywordCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(NamedParameterCompletionProvider))] +[Shared] +internal class KeywordCompletionProvider : AbstractKeywordCompletionProvider { - [ExportCompletionProvider(nameof(KeywordCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(NamedParameterCompletionProvider))] - [Shared] - internal class KeywordCompletionProvider : AbstractKeywordCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public KeywordCompletionProvider() + : base( + [ + new AbstractKeywordRecommender(), + new AddKeywordRecommender(), + new AliasKeywordRecommender(), + new AndKeywordRecommender(), + new AnnotationsKeywordRecommender(), + new AscendingKeywordRecommender(), + new AsKeywordRecommender(), + new AssemblyKeywordRecommender(), + new AsyncKeywordRecommender(), + new BaseKeywordRecommender(), + new BoolKeywordRecommender(), + new BreakKeywordRecommender(), + new ByKeywordRecommender(), + new ByteKeywordRecommender(), + new CaseKeywordRecommender(), + new CatchKeywordRecommender(), + new CharKeywordRecommender(), + new CheckedKeywordRecommender(), + new ChecksumKeywordRecommender(), + new ClassKeywordRecommender(), + new ConstKeywordRecommender(), + new ContinueKeywordRecommender(), + new DecimalKeywordRecommender(), + new DefaultKeywordRecommender(), + new DefineKeywordRecommender(), + new DelegateKeywordRecommender(), + new DescendingKeywordRecommender(), + new DisableKeywordRecommender(), + new DoKeywordRecommender(), + new DoubleKeywordRecommender(), + new DynamicKeywordRecommender(), + new ElifKeywordRecommender(), + new ElseKeywordRecommender(), + new EnableKeywordRecommender(), + new EndIfKeywordRecommender(), + new EndRegionKeywordRecommender(), + new EnumKeywordRecommender(), + new EqualsKeywordRecommender(), + new ErrorKeywordRecommender(), + new EventKeywordRecommender(), + new ExplicitKeywordRecommender(), + new ExternKeywordRecommender(), + new FalseKeywordRecommender(), + new FieldKeywordRecommender(), + new FileKeywordRecommender(), + new FinallyKeywordRecommender(), + new FixedKeywordRecommender(), + new FloatKeywordRecommender(), + new ForEachKeywordRecommender(), + new ForKeywordRecommender(), + new FromKeywordRecommender(), + new GetKeywordRecommender(), + new GlobalKeywordRecommender(), + new GotoKeywordRecommender(), + new GroupKeywordRecommender(), + new HiddenKeywordRecommender(), + new IfKeywordRecommender(), + new ImplicitKeywordRecommender(), + new InitKeywordRecommender(), + new InKeywordRecommender(), + new InterfaceKeywordRecommender(), + new InternalKeywordRecommender(), + new IntKeywordRecommender(), + new IntoKeywordRecommender(), + new IsKeywordRecommender(), + new JoinKeywordRecommender(), + new LetKeywordRecommender(), + new LineKeywordRecommender(), + new LoadKeywordRecommender(), + new LockKeywordRecommender(), + new LongKeywordRecommender(), + new ManagedKeywordRecommender(), + new MethodKeywordRecommender(), + new ModuleKeywordRecommender(), + new NameOfKeywordRecommender(), + new NamespaceKeywordRecommender(), + new NewKeywordRecommender(), + new NintKeywordRecommender(), + new NotKeywordRecommender(), + new NotNullKeywordRecommender(), + new NuintKeywordRecommender(), + new NullableKeywordRecommender(), + new NullKeywordRecommender(), + new ObjectKeywordRecommender(), + new OnKeywordRecommender(), + new OperatorKeywordRecommender(), + new OrderByKeywordRecommender(), + new OrKeywordRecommender(), + new OutKeywordRecommender(), + new OverrideKeywordRecommender(), + new ParamKeywordRecommender(), + new ParamsKeywordRecommender(), + new PartialKeywordRecommender(), + new PragmaKeywordRecommender(), + new PrivateKeywordRecommender(), + new PropertyKeywordRecommender(), + new ProtectedKeywordRecommender(), + new PublicKeywordRecommender(), + new ReadOnlyKeywordRecommender(), + new RecordKeywordRecommender(), + new ReferenceKeywordRecommender(), + new RefKeywordRecommender(), + new RegionKeywordRecommender(), + new RemoveKeywordRecommender(), + new RequiredKeywordRecommender(), + new RestoreKeywordRecommender(), + new ReturnKeywordRecommender(), + new SByteKeywordRecommender(), + new ScopedKeywordRecommender(), + new SealedKeywordRecommender(), + new SelectKeywordRecommender(), + new SetKeywordRecommender(), + new ShortKeywordRecommender(), + new SizeOfKeywordRecommender(), + new StackAllocKeywordRecommender(), + new StaticKeywordRecommender(), + new StringKeywordRecommender(), + new StructKeywordRecommender(), + new SwitchKeywordRecommender(), + new ThisKeywordRecommender(), + new ThrowKeywordRecommender(), + new TrueKeywordRecommender(), + new TryKeywordRecommender(), + new TypeKeywordRecommender(), + new TypeOfKeywordRecommender(), + new TypeVarKeywordRecommender(), + new UIntKeywordRecommender(), + new ULongKeywordRecommender(), + new UncheckedKeywordRecommender(), + new UndefKeywordRecommender(), + new UnmanagedKeywordRecommender(), + new UnsafeKeywordRecommender(), + new UShortKeywordRecommender(), + new UsingKeywordRecommender(), + new VarKeywordRecommender(), + new VirtualKeywordRecommender(), + new VoidKeywordRecommender(), + new VolatileKeywordRecommender(), + new WarningKeywordRecommender(), + new WarningsKeywordRecommender(), + new WhenKeywordRecommender(), + new WhereKeywordRecommender(), + new WhileKeywordRecommender(), + new WithKeywordRecommender(), + new YieldKeywordRecommender(), + ]) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public KeywordCompletionProvider() - : base( - [ - new AbstractKeywordRecommender(), - new AddKeywordRecommender(), - new AliasKeywordRecommender(), - new AndKeywordRecommender(), - new AnnotationsKeywordRecommender(), - new AscendingKeywordRecommender(), - new AsKeywordRecommender(), - new AssemblyKeywordRecommender(), - new AsyncKeywordRecommender(), - new BaseKeywordRecommender(), - new BoolKeywordRecommender(), - new BreakKeywordRecommender(), - new ByKeywordRecommender(), - new ByteKeywordRecommender(), - new CaseKeywordRecommender(), - new CatchKeywordRecommender(), - new CharKeywordRecommender(), - new CheckedKeywordRecommender(), - new ChecksumKeywordRecommender(), - new ClassKeywordRecommender(), - new ConstKeywordRecommender(), - new ContinueKeywordRecommender(), - new DecimalKeywordRecommender(), - new DefaultKeywordRecommender(), - new DefineKeywordRecommender(), - new DelegateKeywordRecommender(), - new DescendingKeywordRecommender(), - new DisableKeywordRecommender(), - new DoKeywordRecommender(), - new DoubleKeywordRecommender(), - new DynamicKeywordRecommender(), - new ElifKeywordRecommender(), - new ElseKeywordRecommender(), - new EnableKeywordRecommender(), - new EndIfKeywordRecommender(), - new EndRegionKeywordRecommender(), - new EnumKeywordRecommender(), - new EqualsKeywordRecommender(), - new ErrorKeywordRecommender(), - new EventKeywordRecommender(), - new ExplicitKeywordRecommender(), - new ExternKeywordRecommender(), - new FalseKeywordRecommender(), - new FieldKeywordRecommender(), - new FileKeywordRecommender(), - new FinallyKeywordRecommender(), - new FixedKeywordRecommender(), - new FloatKeywordRecommender(), - new ForEachKeywordRecommender(), - new ForKeywordRecommender(), - new FromKeywordRecommender(), - new GetKeywordRecommender(), - new GlobalKeywordRecommender(), - new GotoKeywordRecommender(), - new GroupKeywordRecommender(), - new HiddenKeywordRecommender(), - new IfKeywordRecommender(), - new ImplicitKeywordRecommender(), - new InitKeywordRecommender(), - new InKeywordRecommender(), - new InterfaceKeywordRecommender(), - new InternalKeywordRecommender(), - new IntKeywordRecommender(), - new IntoKeywordRecommender(), - new IsKeywordRecommender(), - new JoinKeywordRecommender(), - new LetKeywordRecommender(), - new LineKeywordRecommender(), - new LoadKeywordRecommender(), - new LockKeywordRecommender(), - new LongKeywordRecommender(), - new ManagedKeywordRecommender(), - new MethodKeywordRecommender(), - new ModuleKeywordRecommender(), - new NameOfKeywordRecommender(), - new NamespaceKeywordRecommender(), - new NewKeywordRecommender(), - new NintKeywordRecommender(), - new NotKeywordRecommender(), - new NotNullKeywordRecommender(), - new NuintKeywordRecommender(), - new NullableKeywordRecommender(), - new NullKeywordRecommender(), - new ObjectKeywordRecommender(), - new OnKeywordRecommender(), - new OperatorKeywordRecommender(), - new OrderByKeywordRecommender(), - new OrKeywordRecommender(), - new OutKeywordRecommender(), - new OverrideKeywordRecommender(), - new ParamKeywordRecommender(), - new ParamsKeywordRecommender(), - new PartialKeywordRecommender(), - new PragmaKeywordRecommender(), - new PrivateKeywordRecommender(), - new PropertyKeywordRecommender(), - new ProtectedKeywordRecommender(), - new PublicKeywordRecommender(), - new ReadOnlyKeywordRecommender(), - new RecordKeywordRecommender(), - new ReferenceKeywordRecommender(), - new RefKeywordRecommender(), - new RegionKeywordRecommender(), - new RemoveKeywordRecommender(), - new RequiredKeywordRecommender(), - new RestoreKeywordRecommender(), - new ReturnKeywordRecommender(), - new SByteKeywordRecommender(), - new ScopedKeywordRecommender(), - new SealedKeywordRecommender(), - new SelectKeywordRecommender(), - new SetKeywordRecommender(), - new ShortKeywordRecommender(), - new SizeOfKeywordRecommender(), - new StackAllocKeywordRecommender(), - new StaticKeywordRecommender(), - new StringKeywordRecommender(), - new StructKeywordRecommender(), - new SwitchKeywordRecommender(), - new ThisKeywordRecommender(), - new ThrowKeywordRecommender(), - new TrueKeywordRecommender(), - new TryKeywordRecommender(), - new TypeKeywordRecommender(), - new TypeOfKeywordRecommender(), - new TypeVarKeywordRecommender(), - new UIntKeywordRecommender(), - new ULongKeywordRecommender(), - new UncheckedKeywordRecommender(), - new UndefKeywordRecommender(), - new UnmanagedKeywordRecommender(), - new UnsafeKeywordRecommender(), - new UShortKeywordRecommender(), - new UsingKeywordRecommender(), - new VarKeywordRecommender(), - new VirtualKeywordRecommender(), - new VoidKeywordRecommender(), - new VolatileKeywordRecommender(), - new WarningKeywordRecommender(), - new WarningsKeywordRecommender(), - new WhenKeywordRecommender(), - new WhereKeywordRecommender(), - new WhileKeywordRecommender(), - new WithKeywordRecommender(), - new YieldKeywordRecommender(), - ]) - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options) || - CompletionUtilities.IsCompilerDirectiveTriggerCharacter(text, characterPosition); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options) || + CompletionUtilities.IsCompilerDirectiveTriggerCharacter(text, characterPosition); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters.Add(' '); + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters.Add(' '); - private static readonly CompletionItemRules s_tupleRules = CompletionItemRules.Default. - WithCommitCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ':')); + private static readonly CompletionItemRules s_tupleRules = CompletionItemRules.Default. + WithCommitCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ':')); - protected override CompletionItem CreateItem(RecommendedKeyword keyword, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var rules = context.IsPossibleTupleContext ? s_tupleRules : CompletionItemRules.Default; + protected override CompletionItem CreateItem(RecommendedKeyword keyword, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var rules = context.IsPossibleTupleContext ? s_tupleRules : CompletionItemRules.Default; - return CommonCompletionItem.Create( - displayText: keyword.Keyword, - displayTextSuffix: "", - description: keyword.DescriptionFactory(cancellationToken), - glyph: Glyph.Keyword, - rules: rules.WithMatchPriority(keyword.MatchPriority) - .WithFormatOnCommit(keyword.ShouldFormatOnCommit)); - } + return CommonCompletionItem.Create( + displayText: keyword.Keyword, + displayTextSuffix: "", + description: keyword.DescriptionFactory(cancellationToken), + glyph: Glyph.Keyword, + rules: rules.WithMatchPriority(keyword.MatchPriority) + .WithFormatOnCommit(keyword.ShouldFormatOnCommit)); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/LastBuiltInCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/LastBuiltInCompletionProvider.cs index 2a35f65a5dc1c..ea958b4258316 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/LastBuiltInCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/LastBuiltInCompletionProvider.cs @@ -8,24 +8,23 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +/// +/// Provides a completion provider that always appears after all built-in completion providers. This completion +/// provider does not provide any completions. +/// +[ExportCompletionProvider(nameof(LastBuiltInCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(ReferenceDirectiveCompletionProvider))] +[Shared] +internal sealed class LastBuiltInCompletionProvider : CompletionProvider { - /// - /// Provides a completion provider that always appears after all built-in completion providers. This completion - /// provider does not provide any completions. - /// - [ExportCompletionProvider(nameof(LastBuiltInCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(ReferenceDirectiveCompletionProvider))] - [Shared] - internal sealed class LastBuiltInCompletionProvider : CompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LastBuiltInCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public LastBuiltInCompletionProvider() - { - } - - public override Task ProvideCompletionsAsync(CompletionContext context) - => Task.CompletedTask; } + + public override Task ProvideCompletionsAsync(CompletionContext context) + => Task.CompletedTask; } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/NamedParameterCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/NamedParameterCompletionProvider.cs index 1354072c7a200..77f957b583757 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/NamedParameterCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/NamedParameterCompletionProvider.cs @@ -24,268 +24,267 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(NamedParameterCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(AttributeNamedParameterCompletionProvider))] +[Shared] +internal partial class NamedParameterCompletionProvider : LSPCompletionProvider, IEqualityComparer { - [ExportCompletionProvider(nameof(NamedParameterCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(AttributeNamedParameterCompletionProvider))] - [Shared] - internal partial class NamedParameterCompletionProvider : LSPCompletionProvider, IEqualityComparer + private const string ColonString = ":"; + + // Explicitly remove ":" from the set of filter characters because (by default) + // any character that appears in DisplayText gets treated as a filter char. + private static readonly CompletionItemRules s_rules = CompletionItemRules.Default + .WithFilterCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ':')); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public NamedParameterCompletionProvider() { - private const string ColonString = ":"; + } + + internal override string Language => LanguageNames.CSharp; - // Explicitly remove ":" from the set of filter characters because (by default) - // any character that appears in DisplayText gets treated as a filter char. - private static readonly CompletionItemRules s_rules = CompletionItemRules.Default - .WithFilterCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ':')); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public NamedParameterCompletionProvider() + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; + + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - } + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; + + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (syntaxTree.IsInNonUserCode(position, cancellationToken)) + { + return; + } - internal override string Language => LanguageNames.CSharp; + var token = syntaxTree + .FindTokenOnLeftOfPosition(position, cancellationToken) + .GetPreviousTokenIfTouchingWord(position); - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + if (token.Kind() is not (SyntaxKind.OpenParenToken or SyntaxKind.OpenBracketToken or SyntaxKind.CommaToken)) + { + return; + } - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; + if (token.Parent is not BaseArgumentListSyntax argumentList) + { + return; + } - public override async Task ProvideCompletionsAsync(CompletionContext context) - { - try + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(argumentList, cancellationToken).ConfigureAwait(false); + var parameterLists = GetParameterLists(semanticModel, position, argumentList.Parent!, cancellationToken); + if (parameterLists == null) { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; - - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (syntaxTree.IsInNonUserCode(position, cancellationToken)) - { - return; - } - - var token = syntaxTree - .FindTokenOnLeftOfPosition(position, cancellationToken) - .GetPreviousTokenIfTouchingWord(position); - - if (token.Kind() is not (SyntaxKind.OpenParenToken or SyntaxKind.OpenBracketToken or SyntaxKind.CommaToken)) - { - return; - } - - if (token.Parent is not BaseArgumentListSyntax argumentList) - { - return; - } - - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(argumentList, cancellationToken).ConfigureAwait(false); - var parameterLists = GetParameterLists(semanticModel, position, argumentList.Parent!, cancellationToken); - if (parameterLists == null) - { - return; - } - - var existingNamedParameters = GetExistingNamedParameters(argumentList, position); - parameterLists = parameterLists.Where(pl => IsValid(pl, existingNamedParameters)); - - var unspecifiedParameters = parameterLists.SelectMany(pl => pl) - .Where(p => !existingNamedParameters.Contains(p.Name)) - .Distinct(this); - - if (!unspecifiedParameters.Any()) - { - return; - } - - // Consider refining this logic to mandate completion with an argument name, if preceded by an out-of-position name - // See https://github.com/dotnet/roslyn/issues/20657 - var languageVersion = document.Project.ParseOptions!.LanguageVersion(); - if (languageVersion < LanguageVersion.CSharp7_2 && token.IsMandatoryNamedParameterPosition()) - { - context.IsExclusive = true; - } - - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - - foreach (var parameter in unspecifiedParameters) - { - // Note: the filter text does not include the ':'. We want to ensure that if - // the user types the name exactly (up to the colon) that it is selected as an - // exact match. - var escapedName = parameter.Name.ToIdentifierToken().ToString(); - - context.AddItem(SymbolCompletionItem.CreateWithSymbolId( - displayText: escapedName, - displayTextSuffix: ColonString, - symbols: ImmutableArray.Create(parameter), - rules: s_rules.WithMatchPriority(SymbolMatchPriority.PreferNamedArgument), - contextPosition: token.SpanStart, - filterText: escapedName)); - } + return; } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) + + var existingNamedParameters = GetExistingNamedParameters(argumentList, position); + parameterLists = parameterLists.Where(pl => IsValid(pl, existingNamedParameters)); + + var unspecifiedParameters = parameterLists.SelectMany(pl => pl) + .Where(p => !existingNamedParameters.Contains(p.Name)) + .Distinct(this); + + if (!unspecifiedParameters.Any()) { - // nop + return; } - } - internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + // Consider refining this logic to mandate completion with an argument name, if preceded by an out-of-position name + // See https://github.com/dotnet/roslyn/issues/20657 + var languageVersion = document.Project.ParseOptions!.LanguageVersion(); + if (languageVersion < LanguageVersion.CSharp7_2 && token.IsMandatoryNamedParameterPosition()) + { + context.IsExclusive = true; + } + + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - private static bool IsValid(ImmutableArray parameterList, ISet existingNamedParameters) + foreach (var parameter in unspecifiedParameters) + { + // Note: the filter text does not include the ':'. We want to ensure that if + // the user types the name exactly (up to the colon) that it is selected as an + // exact match. + var escapedName = parameter.Name.ToIdentifierToken().ToString(); + + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( + displayText: escapedName, + displayTextSuffix: ColonString, + symbols: ImmutableArray.Create(parameter), + rules: s_rules.WithMatchPriority(SymbolMatchPriority.PreferNamedArgument), + contextPosition: token.SpanStart, + filterText: escapedName)); + } + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) { - // A parameter list is valid if it has parameters that match in name all the existing - // named parameters that have been provided. - return existingNamedParameters.Except(parameterList.Select(p => p.Name)).IsEmpty(); + // nop } + } + + internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + + private static bool IsValid(ImmutableArray parameterList, ISet existingNamedParameters) + { + // A parameter list is valid if it has parameters that match in name all the existing + // named parameters that have been provided. + return existingNamedParameters.Except(parameterList.Select(p => p.Name)).IsEmpty(); + } + + private static ISet GetExistingNamedParameters(BaseArgumentListSyntax argumentList, int position) + { + var existingArguments = argumentList.Arguments.Where(a => a.Span.End <= position && a.NameColon != null) + .Select(a => a.NameColon!.Name.Identifier.ValueText); + + return existingArguments.ToSet(); + } - private static ISet GetExistingNamedParameters(BaseArgumentListSyntax argumentList, int position) + private static IEnumerable>? GetParameterLists( + SemanticModel semanticModel, + int position, + SyntaxNode invocableNode, + CancellationToken cancellationToken) + { + return invocableNode switch { - var existingArguments = argumentList.Arguments.Where(a => a.Span.End <= position && a.NameColon != null) - .Select(a => a.NameColon!.Name.Identifier.ValueText); + InvocationExpressionSyntax invocationExpression => GetInvocationExpressionParameterLists(semanticModel, position, invocationExpression, cancellationToken), + ConstructorInitializerSyntax constructorInitializer => GetConstructorInitializerParameterLists(semanticModel, position, constructorInitializer, cancellationToken), + ElementAccessExpressionSyntax elementAccessExpression => GetElementAccessExpressionParameterLists(semanticModel, position, elementAccessExpression, cancellationToken), + BaseObjectCreationExpressionSyntax objectCreationExpression => GetObjectCreationExpressionParameterLists(semanticModel, position, objectCreationExpression, cancellationToken), + PrimaryConstructorBaseTypeSyntax baseType => GetPrimaryConstructorParameterLists(semanticModel, baseType, cancellationToken), + _ => null, + }; + } - return existingArguments.ToSet(); + private static IEnumerable>? GetObjectCreationExpressionParameterLists( + SemanticModel semanticModel, + int position, + BaseObjectCreationExpressionSyntax objectCreationExpression, + CancellationToken cancellationToken) + { + var within = semanticModel.GetEnclosingNamedType(position, cancellationToken); + if (semanticModel.GetTypeInfo(objectCreationExpression, cancellationToken).Type is INamedTypeSymbol type && within != null && type.TypeKind != TypeKind.Delegate) + { + return type.InstanceConstructors.Where(c => c.IsAccessibleWithin(within)) + .Select(c => c.Parameters); } - private static IEnumerable>? GetParameterLists( - SemanticModel semanticModel, - int position, - SyntaxNode invocableNode, - CancellationToken cancellationToken) + return null; + } + + private static IEnumerable>? GetElementAccessExpressionParameterLists( + SemanticModel semanticModel, + int position, + ElementAccessExpressionSyntax elementAccessExpression, + CancellationToken cancellationToken) + { + var expressionSymbol = semanticModel.GetSymbolInfo(elementAccessExpression.Expression, cancellationToken).GetAnySymbol(); + var expressionType = semanticModel.GetTypeInfo(elementAccessExpression.Expression, cancellationToken).Type; + + if (expressionSymbol != null && expressionType != null) { - return invocableNode switch + var indexers = semanticModel.LookupSymbols(position, expressionType, WellKnownMemberNames.Indexer).OfType(); + var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); + if (within != null) { - InvocationExpressionSyntax invocationExpression => GetInvocationExpressionParameterLists(semanticModel, position, invocationExpression, cancellationToken), - ConstructorInitializerSyntax constructorInitializer => GetConstructorInitializerParameterLists(semanticModel, position, constructorInitializer, cancellationToken), - ElementAccessExpressionSyntax elementAccessExpression => GetElementAccessExpressionParameterLists(semanticModel, position, elementAccessExpression, cancellationToken), - BaseObjectCreationExpressionSyntax objectCreationExpression => GetObjectCreationExpressionParameterLists(semanticModel, position, objectCreationExpression, cancellationToken), - PrimaryConstructorBaseTypeSyntax baseType => GetPrimaryConstructorParameterLists(semanticModel, baseType, cancellationToken), - _ => null, - }; + return indexers.Where(i => i.IsAccessibleWithin(within, throughType: expressionType)) + .Select(i => i.Parameters); + } } - private static IEnumerable>? GetObjectCreationExpressionParameterLists( - SemanticModel semanticModel, - int position, - BaseObjectCreationExpressionSyntax objectCreationExpression, - CancellationToken cancellationToken) + return null; + } + + private static IEnumerable>? GetConstructorInitializerParameterLists( + SemanticModel semanticModel, + int position, + ConstructorInitializerSyntax constructorInitializer, + CancellationToken cancellationToken) + { + var within = semanticModel.GetEnclosingNamedType(position, cancellationToken); + if (within is { TypeKind: TypeKind.Struct or TypeKind.Class }) { - var within = semanticModel.GetEnclosingNamedType(position, cancellationToken); - if (semanticModel.GetTypeInfo(objectCreationExpression, cancellationToken).Type is INamedTypeSymbol type && within != null && type.TypeKind != TypeKind.Delegate) + var type = constructorInitializer.Kind() == SyntaxKind.BaseConstructorInitializer + ? within.BaseType + : within; + + if (type != null) { return type.InstanceConstructors.Where(c => c.IsAccessibleWithin(within)) .Select(c => c.Parameters); } - - return null; } - private static IEnumerable>? GetElementAccessExpressionParameterLists( - SemanticModel semanticModel, - int position, - ElementAccessExpressionSyntax elementAccessExpression, - CancellationToken cancellationToken) - { - var expressionSymbol = semanticModel.GetSymbolInfo(elementAccessExpression.Expression, cancellationToken).GetAnySymbol(); - var expressionType = semanticModel.GetTypeInfo(elementAccessExpression.Expression, cancellationToken).Type; - - if (expressionSymbol != null && expressionType != null) - { - var indexers = semanticModel.LookupSymbols(position, expressionType, WellKnownMemberNames.Indexer).OfType(); - var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); - if (within != null) - { - return indexers.Where(i => i.IsAccessibleWithin(within, throughType: expressionType)) - .Select(i => i.Parameters); - } - } + return null; + } + private static IEnumerable>? GetPrimaryConstructorParameterLists( + SemanticModel semanticModel, + PrimaryConstructorBaseTypeSyntax baseType, + CancellationToken cancellationToken) + { + var baseList = baseType.Parent; + if (baseList?.Parent is not BaseTypeDeclarationSyntax typeDeclaration) return null; - } - - private static IEnumerable>? GetConstructorInitializerParameterLists( - SemanticModel semanticModel, - int position, - ConstructorInitializerSyntax constructorInitializer, - CancellationToken cancellationToken) - { - var within = semanticModel.GetEnclosingNamedType(position, cancellationToken); - if (within is { TypeKind: TypeKind.Struct or TypeKind.Class }) - { - var type = constructorInitializer.Kind() == SyntaxKind.BaseConstructorInitializer - ? within.BaseType - : within; - - if (type != null) - { - return type.InstanceConstructors.Where(c => c.IsAccessibleWithin(within)) - .Select(c => c.Parameters); - } - } + var within = semanticModel.GetRequiredDeclaredSymbol(typeDeclaration, cancellationToken); + if (within is null) return null; - } - private static IEnumerable>? GetPrimaryConstructorParameterLists( - SemanticModel semanticModel, - PrimaryConstructorBaseTypeSyntax baseType, - CancellationToken cancellationToken) - { - var baseList = baseType.Parent; - if (baseList?.Parent is not BaseTypeDeclarationSyntax typeDeclaration) - return null; - - var within = semanticModel.GetRequiredDeclaredSymbol(typeDeclaration, cancellationToken); - if (within is null) - return null; - - var type = semanticModel.GetTypeInfo(baseType.Type, cancellationToken).Type as INamedTypeSymbol; - return type?.InstanceConstructors - .Where(m => m.IsAccessibleWithin(within)) - .Select(m => m.Parameters); - } + var type = semanticModel.GetTypeInfo(baseType.Type, cancellationToken).Type as INamedTypeSymbol; + return type?.InstanceConstructors + .Where(m => m.IsAccessibleWithin(within)) + .Select(m => m.Parameters); + } - private static IEnumerable>? GetInvocationExpressionParameterLists( - SemanticModel semanticModel, - int position, - InvocationExpressionSyntax invocationExpression, - CancellationToken cancellationToken) + private static IEnumerable>? GetInvocationExpressionParameterLists( + SemanticModel semanticModel, + int position, + InvocationExpressionSyntax invocationExpression, + CancellationToken cancellationToken) + { + var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); + if (within != null) { - var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); - if (within != null) + var methodGroup = semanticModel.GetMemberGroup(invocationExpression.Expression, cancellationToken).OfType(); + var expressionType = semanticModel.GetTypeInfo(invocationExpression.Expression, cancellationToken).Type as INamedTypeSymbol; + + if (methodGroup.Any()) { - var methodGroup = semanticModel.GetMemberGroup(invocationExpression.Expression, cancellationToken).OfType(); - var expressionType = semanticModel.GetTypeInfo(invocationExpression.Expression, cancellationToken).Type as INamedTypeSymbol; - - if (methodGroup.Any()) - { - return methodGroup.Where(m => m.IsAccessibleWithin(within)) - .Select(m => m.Parameters); - } - else if (expressionType.IsDelegateType()) - { - var delegateType = expressionType; - return SpecializedCollections.SingletonEnumerable(delegateType.DelegateInvokeMethod!.Parameters); - } + return methodGroup.Where(m => m.IsAccessibleWithin(within)) + .Select(m => m.Parameters); + } + else if (expressionType.IsDelegateType()) + { + var delegateType = expressionType; + return SpecializedCollections.SingletonEnumerable(delegateType.DelegateInvokeMethod!.Parameters); } - - return null; } - bool IEqualityComparer.Equals(IParameterSymbol? x, IParameterSymbol? y) - => x == y || x != null && y != null && x.Name.Equals(y.Name); + return null; + } - int IEqualityComparer.GetHashCode(IParameterSymbol obj) - => obj.Name.GetHashCode(); + bool IEqualityComparer.Equals(IParameterSymbol? x, IParameterSymbol? y) + => x == y || x != null && y != null && x.Name.Equals(y.Name); - protected override Task GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) - { - return Task.FromResult(new TextChange( - selectedItem.Span, - // Insert extra colon if committing with '(' only: "method(parameter:(" is preferred to "method(parameter(". - // In all other cases, do not add extra colon. Note that colon is already added if committing with ':'. - ch == '(' ? selectedItem.GetEntireDisplayText() : selectedItem.DisplayText)); - } + int IEqualityComparer.GetHashCode(IParameterSymbol obj) + => obj.Name.GetHashCode(); + + protected override Task GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + { + return Task.FromResult(new TextChange( + selectedItem.Span, + // Insert extra colon if committing with '(' only: "method(parameter:(" is preferred to "method(parameter(". + // In all other cases, do not add extra colon. Note that colon is already added if committing with ':'. + ch == '(' ? selectedItem.GetEntireDisplayText() : selectedItem.DisplayText)); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ObjectAndWithInitializerCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ObjectAndWithInitializerCompletionProvider.cs index d0defbf8ed13a..863fbe433b903 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ObjectAndWithInitializerCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ObjectAndWithInitializerCompletionProvider.cs @@ -19,195 +19,194 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +// Provides symbol completions in object and 'with' initializers: +// - new() { $$ +// - new C() { $$ +// - expr with { $$ +[ExportCompletionProvider(nameof(ObjectAndWithInitializerCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(ObjectCreationCompletionProvider))] +[Shared] +internal class ObjectAndWithInitializerCompletionProvider : AbstractObjectInitializerCompletionProvider { - // Provides symbol completions in object and 'with' initializers: - // - new() { $$ - // - new C() { $$ - // - expr with { $$ - [ExportCompletionProvider(nameof(ObjectAndWithInitializerCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(ObjectCreationCompletionProvider))] - [Shared] - internal class ObjectAndWithInitializerCompletionProvider : AbstractObjectInitializerCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ObjectAndWithInitializerCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ObjectAndWithInitializerCompletionProvider() + } + + internal override string Language => LanguageNames.CSharp; + + protected override async Task IsExclusiveAsync(Document document, int position, CancellationToken cancellationToken) + { + // We're exclusive if this context could only be an object initializer and not also a + // collection initializer. If we're initializing something that could be initialized as + // an object or as a collection, say we're not exclusive. That way the rest of + // intellisense can be used in the collection initializer. + // + // Consider this case: + + // class c : IEnumerable + // { + // public void Add(int addend) { } + // public int goo; + // } + + // void goo() + // { + // var b = new c {| + // } + + // There we could initialize b using either an object initializer or a collection + // initializer. Since we don't know which the user will use, we'll be non-exclusive, so + // the other providers can help the user write the collection initializer, if they want + // to. + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + + if (tree.IsInNonUserCode(position, cancellationToken)) { + return false; } - internal override string Language => LanguageNames.CSharp; + var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken); + token = token.GetPreviousTokenIfTouchingWord(position); - protected override async Task IsExclusiveAsync(Document document, int position, CancellationToken cancellationToken) + if (token.Parent == null) { - // We're exclusive if this context could only be an object initializer and not also a - // collection initializer. If we're initializing something that could be initialized as - // an object or as a collection, say we're not exclusive. That way the rest of - // intellisense can be used in the collection initializer. - // - // Consider this case: - - // class c : IEnumerable - // { - // public void Add(int addend) { } - // public int goo; - // } - - // void goo() - // { - // var b = new c {| - // } - - // There we could initialize b using either an object initializer or a collection - // initializer. Since we don't know which the user will use, we'll be non-exclusive, so - // the other providers can help the user write the collection initializer, if they want - // to. - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - - if (tree.IsInNonUserCode(position, cancellationToken)) - { - return false; - } + return false; + } - var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken); - token = token.GetPreviousTokenIfTouchingWord(position); + if (token.Parent.Parent is not ExpressionSyntax expression) + { + return false; + } - if (token.Parent == null) - { - return false; - } + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(expression, cancellationToken).ConfigureAwait(false); + var initializedType = semanticModel.GetTypeInfo(expression, cancellationToken).Type; + if (initializedType == null) + { + return false; + } - if (token.Parent.Parent is not ExpressionSyntax expression) - { - return false; - } + var enclosingSymbol = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); + // Non-exclusive if initializedType can be initialized as a collection. + if (initializedType.CanSupportCollectionInitializer(enclosingSymbol)) + { + return false; + } - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(expression, cancellationToken).ConfigureAwait(false); - var initializedType = semanticModel.GetTypeInfo(expression, cancellationToken).Type; - if (initializedType == null) - { - return false; - } + // By default, only our member names will show up. + return true; + } - var enclosingSymbol = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); - // Non-exclusive if initializedType can be initialized as a collection. - if (initializedType.CanSupportCollectionInitializer(enclosingSymbol)) - { - return false; - } + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options) || text[characterPosition] == ' '; - // By default, only our member names will show up. - return true; - } + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters.Add(' '); - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options) || text[characterPosition] == ' '; + protected override Tuple? GetInitializedType( + Document document, SemanticModel semanticModel, int position, CancellationToken cancellationToken) + { + var tree = semanticModel.SyntaxTree; + if (tree.IsInNonUserCode(position, cancellationToken)) + { + return null; + } - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters.Add(' '); + var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken); + token = token.GetPreviousTokenIfTouchingWord(position); - protected override Tuple? GetInitializedType( - Document document, SemanticModel semanticModel, int position, CancellationToken cancellationToken) + if (token.Kind() is not SyntaxKind.CommaToken and not SyntaxKind.OpenBraceToken) { - var tree = semanticModel.SyntaxTree; - if (tree.IsInNonUserCode(position, cancellationToken)) - { - return null; - } + return null; + } - var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken); - token = token.GetPreviousTokenIfTouchingWord(position); + if (token.Parent == null || token.Parent.Parent == null) + { + return null; + } - if (token.Kind() is not SyntaxKind.CommaToken and not SyntaxKind.OpenBraceToken) - { - return null; - } + // If we got a comma, we can syntactically find out if we're in an ObjectInitializerExpression or WithExpression + if (token.Kind() == SyntaxKind.CommaToken && + token.Parent.Kind() is not (SyntaxKind.ObjectInitializerExpression or SyntaxKind.WithInitializerExpression)) + { + return null; + } - if (token.Parent == null || token.Parent.Parent == null) - { - return null; - } + var type = GetInitializedType(token, document, semanticModel, cancellationToken); + if (type is null) + { + return null; + } - // If we got a comma, we can syntactically find out if we're in an ObjectInitializerExpression or WithExpression - if (token.Kind() == SyntaxKind.CommaToken && - token.Parent.Kind() is not (SyntaxKind.ObjectInitializerExpression or SyntaxKind.WithInitializerExpression)) - { - return null; - } + return Tuple.Create(type, token.GetLocation()); + } - var type = GetInitializedType(token, document, semanticModel, cancellationToken); - if (type is null) - { - return null; - } + private static ITypeSymbol? GetInitializedType(SyntaxToken token, Document document, SemanticModel semanticModel, CancellationToken cancellationToken) + { + var parent = token.Parent?.Parent; - return Tuple.Create(type, token.GetLocation()); + // new() { $$ + // new Goo { $$ + if (parent is (kind: SyntaxKind.ObjectCreationExpression or SyntaxKind.ImplicitObjectCreationExpression)) + { + return semanticModel.GetTypeInfo(parent, cancellationToken).Type; } - private static ITypeSymbol? GetInitializedType(SyntaxToken token, Document document, SemanticModel semanticModel, CancellationToken cancellationToken) + // Nested: new Goo { bar = { $$ + if (parent.IsKind(SyntaxKind.SimpleAssignmentExpression)) { - var parent = token.Parent?.Parent; - - // new() { $$ - // new Goo { $$ - if (parent is (kind: SyntaxKind.ObjectCreationExpression or SyntaxKind.ImplicitObjectCreationExpression)) - { - return semanticModel.GetTypeInfo(parent, cancellationToken).Type; - } + // Use the type inferrer to get the type being initialized. + var typeInferenceService = document.GetRequiredLanguageService(); + var parentInitializer = token.GetAncestor()!; + return typeInferenceService.InferType(semanticModel, parentInitializer, objectAsDefault: false, cancellationToken: cancellationToken); + } - // Nested: new Goo { bar = { $$ - if (parent.IsKind(SyntaxKind.SimpleAssignmentExpression)) - { - // Use the type inferrer to get the type being initialized. - var typeInferenceService = document.GetRequiredLanguageService(); - var parentInitializer = token.GetAncestor()!; - return typeInferenceService.InferType(semanticModel, parentInitializer, objectAsDefault: false, cancellationToken: cancellationToken); - } + // expr with { $$ + if (parent is WithExpressionSyntax withExpression) + { + return semanticModel.GetTypeInfo(withExpression.Expression, cancellationToken).Type; + } - // expr with { $$ - if (parent is WithExpressionSyntax withExpression) - { - return semanticModel.GetTypeInfo(withExpression.Expression, cancellationToken).Type; - } + return null; + } - return null; - } + protected override HashSet GetInitializedMembers(SyntaxTree tree, int position, CancellationToken cancellationToken) + { + var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken) + .GetPreviousTokenIfTouchingWord(position); - protected override HashSet GetInitializedMembers(SyntaxTree tree, int position, CancellationToken cancellationToken) + // We should have gotten back a { or , + if (token.Kind() is SyntaxKind.CommaToken or SyntaxKind.OpenBraceToken) { - var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken) - .GetPreviousTokenIfTouchingWord(position); - - // We should have gotten back a { or , - if (token.Kind() is SyntaxKind.CommaToken or SyntaxKind.OpenBraceToken) + if (token.Parent != null) { - if (token.Parent != null) - { - if (token.Parent is InitializerExpressionSyntax initializer) - { - return new HashSet(initializer.Expressions.OfType() - .Where(b => b.OperatorToken.Kind() == SyntaxKind.EqualsToken) - .Select(b => b.Left) - .OfType() - .Select(i => i.Identifier.ValueText)); - } + if (token.Parent is InitializerExpressionSyntax initializer) + { + return new HashSet(initializer.Expressions.OfType() + .Where(b => b.OperatorToken.Kind() == SyntaxKind.EqualsToken) + .Select(b => b.Left) + .OfType() + .Select(i => i.Identifier.ValueText)); } } - - return []; } - protected override bool IsInitializable(ISymbol member, INamedTypeSymbol containingType) - { - if (member is IPropertySymbol property && property.Parameters.Any(static p => !p.IsOptional)) - { - return false; - } + return []; + } - return base.IsInitializable(member, containingType); + protected override bool IsInitializable(ISymbol member, INamedTypeSymbol containingType) + { + if (member is IPropertySymbol property && property.Parameters.Any(static p => !p.IsOptional)) + { + return false; } - protected override string EscapeIdentifier(ISymbol symbol) - => symbol.Name.EscapeIdentifier(); + return base.IsInitializable(member, containingType); } + + protected override string EscapeIdentifier(ISymbol symbol) + => symbol.Name.EscapeIdentifier(); } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ObjectCreationCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ObjectCreationCompletionProvider.cs index 729d18613cf36..133f30b9a9c26 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ObjectCreationCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ObjectCreationCompletionProvider.cs @@ -19,130 +19,129 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(ObjectCreationCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(ExplicitInterfaceTypeCompletionProvider))] +[Shared] +internal partial class ObjectCreationCompletionProvider : AbstractObjectCreationCompletionProvider { - [ExportCompletionProvider(nameof(ObjectCreationCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(ExplicitInterfaceTypeCompletionProvider))] - [Shared] - internal partial class ObjectCreationCompletionProvider : AbstractObjectCreationCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ObjectCreationCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ObjectCreationCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, characterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, characterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; - protected override SyntaxNode? GetObjectCreationNewExpression(SyntaxTree tree, int position, CancellationToken cancellationToken) + protected override SyntaxNode? GetObjectCreationNewExpression(SyntaxTree tree, int position, CancellationToken cancellationToken) + { + if (tree != null) { - if (tree != null) + if (!tree.IsInNonUserCode(position, cancellationToken)) { - if (!tree.IsInNonUserCode(position, cancellationToken)) + var tokenOnLeftOfPosition = tree.FindTokenOnLeftOfPosition(position, cancellationToken); + var newToken = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position); + + // Only after 'new'. + if (newToken.Kind() == SyntaxKind.NewKeyword) { - var tokenOnLeftOfPosition = tree.FindTokenOnLeftOfPosition(position, cancellationToken); - var newToken = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position); - - // Only after 'new'. - if (newToken.Kind() == SyntaxKind.NewKeyword) - { - // Only if the 'new' belongs to an object creation expression (and isn't a 'new' - // modifier on a member). - if (tree.IsObjectCreationTypeContext(position, tokenOnLeftOfPosition, cancellationToken)) - return newToken.Parent as ExpressionSyntax; - } + // Only if the 'new' belongs to an object creation expression (and isn't a 'new' + // modifier on a member). + if (tree.IsObjectCreationTypeContext(position, tokenOnLeftOfPosition, cancellationToken)) + return newToken.Parent as ExpressionSyntax; } } - - return null; } - protected override async Task> GetSymbolsAsync( - CompletionContext? completionContext, CSharpSyntaxContext context, int position, CompletionOptions options, CancellationToken cancellationToken) - { - var result = await base.GetSymbolsAsync(completionContext, context, position, options, cancellationToken).ConfigureAwait(false); - if (result.Any()) - { - var type = (ITypeSymbol)result.Single().Symbol; - var alias = await type.FindApplicableAliasAsync(position, context.SemanticModel, cancellationToken).ConfigureAwait(false); - if (alias != null) - return [new SymbolAndSelectionInfo(alias, result.Single().Preselect)]; - } - - return result; - } + return null; + } - protected override (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(ISymbol symbol, CSharpSyntaxContext context) + protected override async Task> GetSymbolsAsync( + CompletionContext? completionContext, CSharpSyntaxContext context, int position, CompletionOptions options, CancellationToken cancellationToken) + { + var result = await base.GetSymbolsAsync(completionContext, context, position, options, cancellationToken).ConfigureAwait(false); + if (result.Any()) { - if (symbol is IAliasSymbol) - { - return (symbol.Name, "", symbol.Name); - } + var type = (ITypeSymbol)result.Single().Symbol; + var alias = await type.FindApplicableAliasAsync(position, context.SemanticModel, cancellationToken).ConfigureAwait(false); + if (alias != null) + return [new SymbolAndSelectionInfo(alias, result.Single().Preselect)]; + } - // typeSymbol may be a symbol that is nullable if the place we are assigning to is null, for example - // - // object? o = new | - // - // We strip the top-level nullability so we don't end up suggesting "new object?" here. Nested nullability would still - // be generated. - if (symbol is ITypeSymbol typeSymbol) - { - symbol = typeSymbol.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - } + return result; + } - var displayString = symbol.ToMinimalDisplayString(context.SemanticModel, context.Position); - return (displayString, suffix: "", displayString); + protected override (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(ISymbol symbol, CSharpSyntaxContext context) + { + if (symbol is IAliasSymbol) + { + return (symbol.Name, "", symbol.Name); } - private static readonly CompletionItemRules s_arrayRules = - CompletionItemRules.Create( - commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, ' ', '(', '[')], - matchPriority: MatchPriority.Default, - selectionBehavior: CompletionItemSelectionBehavior.SoftSelection); - - private static readonly CompletionItemRules s_objectRules = - CompletionItemRules.Create( - commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, ' ', '(', '[', ';', '.')], - matchPriority: MatchPriority.Preselect, - selectionBehavior: CompletionItemSelectionBehavior.HardSelection); - - private static readonly CompletionItemRules s_defaultRules = - CompletionItemRules.Create( - commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, ' ', '(', '[', '{', ';', '.')], - matchPriority: MatchPriority.Preselect, - selectionBehavior: CompletionItemSelectionBehavior.HardSelection); - - protected override CompletionItemRules GetCompletionItemRules(ImmutableArray symbols) + // typeSymbol may be a symbol that is nullable if the place we are assigning to is null, for example + // + // object? o = new | + // + // We strip the top-level nullability so we don't end up suggesting "new object?" here. Nested nullability would still + // be generated. + if (symbol is ITypeSymbol typeSymbol) { - var preselect = symbols.Any(static t => t.Preselect); - if (!preselect) - return s_arrayRules; - - // SPECIAL: If the preselected symbol is System.Object, don't commit on '{'. - // Otherwise, it is cumbersome to type an anonymous object when the target type is object. - // The user would get 'new object {' rather than 'new {'. Since object doesn't have any - // properties, the user never really wants to commit 'new object {' anyway. - var namedTypeSymbol = symbols.Length > 0 ? symbols[0].Symbol as INamedTypeSymbol : null; - if (namedTypeSymbol?.SpecialType == SpecialType.System_Object) - return s_objectRules; - - return s_defaultRules; + symbol = typeSymbol.WithNullableAnnotation(NullableAnnotation.NotAnnotated); } - protected override string GetInsertionText(CompletionItem item, char ch) - { - if (ch is ';' or '.') - { - CompletionProvidersLogger.LogCustomizedCommitToAddParenthesis(ch); - return SymbolCompletionItem.GetInsertionText(item) + "()"; - } + var displayString = symbol.ToMinimalDisplayString(context.SemanticModel, context.Position); + return (displayString, suffix: "", displayString); + } + + private static readonly CompletionItemRules s_arrayRules = + CompletionItemRules.Create( + commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, ' ', '(', '[')], + matchPriority: MatchPriority.Default, + selectionBehavior: CompletionItemSelectionBehavior.SoftSelection); + + private static readonly CompletionItemRules s_objectRules = + CompletionItemRules.Create( + commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, ' ', '(', '[', ';', '.')], + matchPriority: MatchPriority.Preselect, + selectionBehavior: CompletionItemSelectionBehavior.HardSelection); + + private static readonly CompletionItemRules s_defaultRules = + CompletionItemRules.Create( + commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, ' ', '(', '[', '{', ';', '.')], + matchPriority: MatchPriority.Preselect, + selectionBehavior: CompletionItemSelectionBehavior.HardSelection); + + protected override CompletionItemRules GetCompletionItemRules(ImmutableArray symbols) + { + var preselect = symbols.Any(static t => t.Preselect); + if (!preselect) + return s_arrayRules; + + // SPECIAL: If the preselected symbol is System.Object, don't commit on '{'. + // Otherwise, it is cumbersome to type an anonymous object when the target type is object. + // The user would get 'new object {' rather than 'new {'. Since object doesn't have any + // properties, the user never really wants to commit 'new object {' anyway. + var namedTypeSymbol = symbols.Length > 0 ? symbols[0].Symbol as INamedTypeSymbol : null; + if (namedTypeSymbol?.SpecialType == SpecialType.System_Object) + return s_objectRules; + + return s_defaultRules; + } - return base.GetInsertionText(item, ch); + protected override string GetInsertionText(CompletionItem item, char ch) + { + if (ch is ';' or '.') + { + CompletionProvidersLogger.LogCustomizedCommitToAddParenthesis(ch); + return SymbolCompletionItem.GetInsertionText(item) + "()"; } + + return base.GetInsertionText(item, ch); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider.cs index 827bb9a84c7a1..08d87a7c769bd 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider.cs @@ -19,188 +19,187 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +/// +/// Provides completion for uncommon unnamed symbols, like conversions, indexer and operators. These completion +/// items will be brought up with dot like normal, but will end up inserting more than just a name into +/// the editor. For example, committing a conversion will insert the conversion prior to the expression being +/// dotted off of. +/// +[ExportCompletionProvider(nameof(UnnamedSymbolCompletionProvider), LanguageNames.CSharp), Shared] +[ExtensionOrder(After = nameof(SymbolCompletionProvider))] +internal partial class UnnamedSymbolCompletionProvider : LSPCompletionProvider { /// - /// Provides completion for uncommon unnamed symbols, like conversions, indexer and operators. These completion - /// items will be brought up with dot like normal, but will end up inserting more than just a name into - /// the editor. For example, committing a conversion will insert the conversion prior to the expression being - /// dotted off of. + /// CompletionItems for indexers/operators should be sorted below other suggestions like methods or properties + /// of the type. We accomplish this by placing a character known to be greater than all other normal identifier + /// characters as the start of our item's name. This doesn't affect what we insert though as all derived + /// providers have specialized logic for what they need to do. + /// + private const string SortingPrefix = "\uFFFD"; + + /// + /// Used to store what sort of unnamed symbol a completion item represents. + /// + internal const string KindName = "Kind"; + internal const string IndexerKindName = "Indexer"; + internal const string OperatorKindName = "Operator"; + internal const string ConversionKindName = "Conversion"; + + /// + /// Used to store the doc comment for some operators/conversions. This is because some of them will be + /// synthesized, so there will be no symbol we can recover after the fact in . /// - [ExportCompletionProvider(nameof(UnnamedSymbolCompletionProvider), LanguageNames.CSharp), Shared] - [ExtensionOrder(After = nameof(SymbolCompletionProvider))] - internal partial class UnnamedSymbolCompletionProvider : LSPCompletionProvider + private const string DocumentationCommentXmlName = "DocumentationCommentXml"; + + [ImportingConstructor] + [System.Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public UnnamedSymbolCompletionProvider() { - /// - /// CompletionItems for indexers/operators should be sorted below other suggestions like methods or properties - /// of the type. We accomplish this by placing a character known to be greater than all other normal identifier - /// characters as the start of our item's name. This doesn't affect what we insert though as all derived - /// providers have specialized logic for what they need to do. - /// - private const string SortingPrefix = "\uFFFD"; - - /// - /// Used to store what sort of unnamed symbol a completion item represents. - /// - internal const string KindName = "Kind"; - internal const string IndexerKindName = "Indexer"; - internal const string OperatorKindName = "Operator"; - internal const string ConversionKindName = "Conversion"; - - /// - /// Used to store the doc comment for some operators/conversions. This is because some of them will be - /// synthesized, so there will be no symbol we can recover after the fact in . - /// - private const string DocumentationCommentXmlName = "DocumentationCommentXml"; - - [ImportingConstructor] - [System.Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public UnnamedSymbolCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override ImmutableHashSet TriggerCharacters => ['.']; + public override ImmutableHashSet TriggerCharacters => ['.']; - public override bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options) - => text[insertedCharacterPosition] == '.'; + public override bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options) + => text[insertedCharacterPosition] == '.'; - /// - /// We keep operators sorted in a specific order. We don't want to sort them alphabetically, but instead want - /// to keep things like == and != together. - /// - private static string SortText(int sortingGroupIndex, string sortTextSymbolPart) - => $"{SortingPrefix}{sortingGroupIndex:000}_{sortTextSymbolPart}"; + /// + /// We keep operators sorted in a specific order. We don't want to sort them alphabetically, but instead want + /// to keep things like == and != together. + /// + private static string SortText(int sortingGroupIndex, string sortTextSymbolPart) + => $"{SortingPrefix}{sortingGroupIndex:000}_{sortTextSymbolPart}"; - /// - /// Gets the dot-like token we're after, and also the start of the expression we'd want to place any text before. - /// - private static (SyntaxToken dotLikeToken, int expressionStart) GetDotAndExpressionStart(SyntaxNode root, int position, CancellationToken cancellationToken) - { - if (CompletionUtilities.GetDotTokenLeftOfPosition(root.SyntaxTree, position, cancellationToken) is not SyntaxToken dotToken) - return default; + /// + /// Gets the dot-like token we're after, and also the start of the expression we'd want to place any text before. + /// + private static (SyntaxToken dotLikeToken, int expressionStart) GetDotAndExpressionStart(SyntaxNode root, int position, CancellationToken cancellationToken) + { + if (CompletionUtilities.GetDotTokenLeftOfPosition(root.SyntaxTree, position, cancellationToken) is not SyntaxToken dotToken) + return default; - // if we have `.Name`, we want to get the parent member-access of that to find the starting position. - // Otherwise, if we have .. then we want the left side of that to find the starting position. - var expression = dotToken.Kind() == SyntaxKind.DotToken - ? dotToken.Parent as ExpressionSyntax - : (dotToken.Parent as RangeExpressionSyntax)?.LeftOperand; + // if we have `.Name`, we want to get the parent member-access of that to find the starting position. + // Otherwise, if we have .. then we want the left side of that to find the starting position. + var expression = dotToken.Kind() == SyntaxKind.DotToken + ? dotToken.Parent as ExpressionSyntax + : (dotToken.Parent as RangeExpressionSyntax)?.LeftOperand; - if (expression == null) - return default; + if (expression == null) + return default; - // If we're after a ?. find the root of that conditional to find the start position of the expression. - expression = expression.GetRootConditionalAccessExpression() ?? expression; - return (dotToken, expression.SpanStart); - } + // If we're after a ?. find the root of that conditional to find the start position of the expression. + expression = expression.GetRootConditionalAccessExpression() ?? expression; + return (dotToken, expression.SpanStart); + } - public override async Task ProvideCompletionsAsync(CompletionContext context) - { - var cancellationToken = context.CancellationToken; - var document = context.Document; - var position = context.Position; + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + var cancellationToken = context.CancellationToken; + var document = context.Document; + var position = context.Position; - // Escape hatch feature flag to let us disable this feature remotely if we run into any issues with it, - if (context.CompletionOptions.UnnamedSymbolCompletionDisabled) - return; + // Escape hatch feature flag to let us disable this feature remotely if we run into any issues with it, + if (context.CompletionOptions.UnnamedSymbolCompletionDisabled) + return; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var dotAndExprStart = GetDotAndExpressionStart(root, position, cancellationToken); - if (dotAndExprStart == default) - return; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var dotAndExprStart = GetDotAndExpressionStart(root, position, cancellationToken); + if (dotAndExprStart == default) + return; - var recommender = document.GetRequiredLanguageService(); - var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); - var semanticModel = syntaxContext.SemanticModel; + var recommender = document.GetRequiredLanguageService(); + var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); + var semanticModel = syntaxContext.SemanticModel; - var options = context.CompletionOptions.ToRecommendationServiceOptions(); - var recommendedSymbols = recommender.GetRecommendedSymbolsInContext(syntaxContext, options, cancellationToken); + var options = context.CompletionOptions.ToRecommendationServiceOptions(); + var recommendedSymbols = recommender.GetRecommendedSymbolsInContext(syntaxContext, options, cancellationToken); - AddUnnamedSymbols(context, position, semanticModel, recommendedSymbols.UnnamedSymbols, cancellationToken); - } + AddUnnamedSymbols(context, position, semanticModel, recommendedSymbols.UnnamedSymbols, cancellationToken); + } - private void AddUnnamedSymbols( - CompletionContext context, int position, SemanticModel semanticModel, ImmutableArray unnamedSymbols, CancellationToken cancellationToken) - { - // Add one 'this[]' entry for all the indexers this type may have. - AddIndexers(context, unnamedSymbols.WhereAsArray(s => s.IsIndexer())); + private void AddUnnamedSymbols( + CompletionContext context, int position, SemanticModel semanticModel, ImmutableArray unnamedSymbols, CancellationToken cancellationToken) + { + // Add one 'this[]' entry for all the indexers this type may have. + AddIndexers(context, unnamedSymbols.WhereAsArray(s => s.IsIndexer())); - // Group all the related operators and add a single completion entry per group. - var operatorGroups = unnamedSymbols.WhereAsArray(s => s.IsUserDefinedOperator()).GroupBy(op => op.Name); - foreach (var opGroup in operatorGroups) - AddOperatorGroup(context, opGroup.Key, opGroup); + // Group all the related operators and add a single completion entry per group. + var operatorGroups = unnamedSymbols.WhereAsArray(s => s.IsUserDefinedOperator()).GroupBy(op => op.Name); + foreach (var opGroup in operatorGroups) + AddOperatorGroup(context, opGroup.Key, opGroup); - foreach (var symbol in unnamedSymbols) - { - cancellationToken.ThrowIfCancellationRequested(); + foreach (var symbol in unnamedSymbols) + { + cancellationToken.ThrowIfCancellationRequested(); - if (symbol.IsConversion()) - AddConversion(context, semanticModel, position, (IMethodSymbol)symbol); - } + if (symbol.IsConversion()) + AddConversion(context, semanticModel, position, (IMethodSymbol)symbol); } + } - public override Task GetChangeAsync( - Document document, - CompletionItem item, - char? commitKey, - CancellationToken cancellationToken) + public override Task GetChangeAsync( + Document document, + CompletionItem item, + char? commitKey, + CancellationToken cancellationToken) + { + var kind = item.GetProperty(KindName); + return kind switch { - var kind = item.GetProperty(KindName); - return kind switch - { - IndexerKindName => GetIndexerChangeAsync(document, item, cancellationToken), - OperatorKindName => GetOperatorChangeAsync(document, item, cancellationToken), - ConversionKindName => GetConversionChangeAsync(document, item, cancellationToken), - _ => throw ExceptionUtilities.UnexpectedValue(kind), - }; - } + IndexerKindName => GetIndexerChangeAsync(document, item, cancellationToken), + OperatorKindName => GetOperatorChangeAsync(document, item, cancellationToken), + ConversionKindName => GetConversionChangeAsync(document, item, cancellationToken), + _ => throw ExceptionUtilities.UnexpectedValue(kind), + }; + } - internal override async Task GetDescriptionAsync( - Document document, - CompletionItem item, - CompletionOptions options, - SymbolDescriptionOptions displayOptions, - CancellationToken cancellationToken) + internal override async Task GetDescriptionAsync( + Document document, + CompletionItem item, + CompletionOptions options, + SymbolDescriptionOptions displayOptions, + CancellationToken cancellationToken) + { + var kind = item.GetProperty(KindName); + return kind switch { - var kind = item.GetProperty(KindName); - return kind switch - { - IndexerKindName => await GetIndexerDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false), - OperatorKindName => await GetOperatorDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false), - ConversionKindName => await GetConversionDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false), - _ => throw ExceptionUtilities.UnexpectedValue(kind), - }; - } + IndexerKindName => await GetIndexerDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false), + OperatorKindName => await GetOperatorDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false), + ConversionKindName => await GetConversionDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false), + _ => throw ExceptionUtilities.UnexpectedValue(kind), + }; + } - private static Task ReplaceTextAfterOperatorAsync(Document document, CompletionItem item, string text, CancellationToken cancellationToken) - => ReplaceTextAfterOperatorAsync(document, item, text, keepQuestion: false, positionOffset: 0, cancellationToken); + private static Task ReplaceTextAfterOperatorAsync(Document document, CompletionItem item, string text, CancellationToken cancellationToken) + => ReplaceTextAfterOperatorAsync(document, item, text, keepQuestion: false, positionOffset: 0, cancellationToken); - private static async Task ReplaceTextAfterOperatorAsync( - Document document, - CompletionItem item, - string text, - bool keepQuestion, - int positionOffset, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var position = SymbolCompletionItem.GetContextPosition(item); - - var (dotToken, _) = GetDotAndExpressionStart(root, position, cancellationToken); - var questionToken = dotToken.GetPreviousToken().Kind() == SyntaxKind.QuestionToken - ? dotToken.GetPreviousToken() - : (SyntaxToken?)null; - - var replacementStart = !keepQuestion && questionToken != null - ? questionToken.Value.SpanStart - : dotToken.SpanStart; - var newPosition = replacementStart + text.Length + positionOffset; - - var tokenOnLeft = root.FindTokenOnLeftOfPosition(position, includeSkipped: true); - return CompletionChange.Create( - new TextChange(TextSpan.FromBounds(replacementStart, tokenOnLeft.Span.End), text), - newPosition); - } + private static async Task ReplaceTextAfterOperatorAsync( + Document document, + CompletionItem item, + string text, + bool keepQuestion, + int positionOffset, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var position = SymbolCompletionItem.GetContextPosition(item); + + var (dotToken, _) = GetDotAndExpressionStart(root, position, cancellationToken); + var questionToken = dotToken.GetPreviousToken().Kind() == SyntaxKind.QuestionToken + ? dotToken.GetPreviousToken() + : (SyntaxToken?)null; + + var replacementStart = !keepQuestion && questionToken != null + ? questionToken.Value.SpanStart + : dotToken.SpanStart; + var newPosition = replacementStart + text.Length + positionOffset; + + var tokenOnLeft = root.FindTokenOnLeftOfPosition(position, includeSkipped: true); + return CompletionChange.Create( + new TextChange(TextSpan.FromBounds(replacementStart, tokenOnLeft.Span.End), text), + newPosition); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Conversions.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Conversions.cs index 1cc5fb3cfecf8..8cc38ccdd5414 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Conversions.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Conversions.cs @@ -19,149 +19,148 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +internal partial class UnnamedSymbolCompletionProvider { - internal partial class UnnamedSymbolCompletionProvider - { - // Place conversions before operators. - private const int ConversionSortingGroupIndex = 1; + // Place conversions before operators. + private const int ConversionSortingGroupIndex = 1; - /// - /// Tag to let us know we need to rehydrate the conversion from the parameter and return type. - /// - private const string RehydrateName = "Rehydrate"; - private static readonly ImmutableArray> s_conversionProperties = - [new KeyValuePair(KindName, ConversionKindName)]; + /// + /// Tag to let us know we need to rehydrate the conversion from the parameter and return type. + /// + private const string RehydrateName = "Rehydrate"; + private static readonly ImmutableArray> s_conversionProperties = + [new KeyValuePair(KindName, ConversionKindName)]; - // We set conversion items' match priority to "Deprioritize" so completion selects other symbols over it when user starts typing. - // e.g. method symbol `Should` should be selected over `(short)` when "sh" is typed. - private static readonly CompletionItemRules s_conversionRules = CompletionItemRules.Default.WithMatchPriority(MatchPriority.Deprioritize); + // We set conversion items' match priority to "Deprioritize" so completion selects other symbols over it when user starts typing. + // e.g. method symbol `Should` should be selected over `(short)` when "sh" is typed. + private static readonly CompletionItemRules s_conversionRules = CompletionItemRules.Default.WithMatchPriority(MatchPriority.Deprioritize); - private static void AddConversion(CompletionContext context, SemanticModel semanticModel, int position, IMethodSymbol conversion) - { - var (symbols, properties) = GetConversionSymbolsAndProperties(context, conversion); - - var targetTypeName = conversion.ReturnType.ToMinimalDisplayString(semanticModel, position); - context.AddItem(SymbolCompletionItem.CreateWithSymbolId( - displayTextPrefix: "(", - displayText: targetTypeName, - displayTextSuffix: ")", - filterText: targetTypeName, - sortText: SortText(ConversionSortingGroupIndex, targetTypeName), - glyph: Glyph.Operator, - symbols: symbols, - rules: s_conversionRules, - contextPosition: position, - properties: properties, - isComplexTextEdit: true)); - } + private static void AddConversion(CompletionContext context, SemanticModel semanticModel, int position, IMethodSymbol conversion) + { + var (symbols, properties) = GetConversionSymbolsAndProperties(context, conversion); + + var targetTypeName = conversion.ReturnType.ToMinimalDisplayString(semanticModel, position); + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( + displayTextPrefix: "(", + displayText: targetTypeName, + displayTextSuffix: ")", + filterText: targetTypeName, + sortText: SortText(ConversionSortingGroupIndex, targetTypeName), + glyph: Glyph.Operator, + symbols: symbols, + rules: s_conversionRules, + contextPosition: position, + properties: properties, + isComplexTextEdit: true)); + } + + private static (ImmutableArray symbols, ImmutableArray> properties) GetConversionSymbolsAndProperties( + CompletionContext context, IMethodSymbol conversion) + { + // If it's a non-synthesized method, then we can just encode it as is. + if (conversion is not CodeGenerationSymbol) + return (ImmutableArray.Create(conversion), s_conversionProperties); + + // Otherwise, encode the constituent parts so we can recover it in GetConversionDescriptionAsync; + using var _ = ArrayBuilder>.GetInstance(out var builder); + + builder.AddRange(s_conversionProperties); + builder.Add(new KeyValuePair(RehydrateName, RehydrateName)); + builder.Add(new KeyValuePair(DocumentationCommentXmlName, conversion.GetDocumentationCommentXml(cancellationToken: context.CancellationToken) ?? "")); + var symbols = ImmutableArray.Create(conversion.ContainingType, conversion.Parameters.First().Type, conversion.ReturnType); + return (symbols, builder.ToImmutable()); + } - private static (ImmutableArray symbols, ImmutableArray> properties) GetConversionSymbolsAndProperties( - CompletionContext context, IMethodSymbol conversion) + private static async Task GetConversionChangeAsync( + Document document, CompletionItem item, CancellationToken cancellationToken) + { + var position = SymbolCompletionItem.GetContextPosition(item); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var (dotToken, _) = GetDotAndExpressionStart(root, position, cancellationToken); + + var questionToken = dotToken.GetPreviousToken().Kind() == SyntaxKind.QuestionToken + ? dotToken.GetPreviousToken() + : (SyntaxToken?)null; + + var expression = (ExpressionSyntax)dotToken.GetRequiredParent(); + expression = expression.GetRootConditionalAccessExpression() ?? expression; + + using var _ = ArrayBuilder.GetInstance(out var builder); + + // First, add the cast prior to the expression. + var castText = $"(({item.DisplayText})"; + builder.Add(new TextChange(new TextSpan(expression.SpanStart, 0), castText)); + + // The expression went up to either a `.`, `..`, `?.` or `?..` + // + // In the case of `expr.` produce `((T)expr)$$` + // + // In the case of `expr..` produce ((T)expr)$$. + // + // In the case of `expr?.` produce `((T)expr)?$$` + if (questionToken == null) { - // If it's a non-synthesized method, then we can just encode it as is. - if (conversion is not CodeGenerationSymbol) - return (ImmutableArray.Create(conversion), s_conversionProperties); - - // Otherwise, encode the constituent parts so we can recover it in GetConversionDescriptionAsync; - using var _ = ArrayBuilder>.GetInstance(out var builder); - - builder.AddRange(s_conversionProperties); - builder.Add(new KeyValuePair(RehydrateName, RehydrateName)); - builder.Add(new KeyValuePair(DocumentationCommentXmlName, conversion.GetDocumentationCommentXml(cancellationToken: context.CancellationToken) ?? "")); - var symbols = ImmutableArray.Create(conversion.ContainingType, conversion.Parameters.First().Type, conversion.ReturnType); - return (symbols, builder.ToImmutable()); + // Always eat the first dot in `.` or `..` and replace that with the paren. + builder.Add(new TextChange(new TextSpan(dotToken.SpanStart, 1), ")")); } - - private static async Task GetConversionChangeAsync( - Document document, CompletionItem item, CancellationToken cancellationToken) + else { - var position = SymbolCompletionItem.GetContextPosition(item); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var (dotToken, _) = GetDotAndExpressionStart(root, position, cancellationToken); - - var questionToken = dotToken.GetPreviousToken().Kind() == SyntaxKind.QuestionToken - ? dotToken.GetPreviousToken() - : (SyntaxToken?)null; - - var expression = (ExpressionSyntax)dotToken.GetRequiredParent(); - expression = expression.GetRootConditionalAccessExpression() ?? expression; - - using var _ = ArrayBuilder.GetInstance(out var builder); - - // First, add the cast prior to the expression. - var castText = $"(({item.DisplayText})"; - builder.Add(new TextChange(new TextSpan(expression.SpanStart, 0), castText)); - - // The expression went up to either a `.`, `..`, `?.` or `?..` - // - // In the case of `expr.` produce `((T)expr)$$` - // - // In the case of `expr..` produce ((T)expr)$$. - // - // In the case of `expr?.` produce `((T)expr)?$$` - if (questionToken == null) - { - // Always eat the first dot in `.` or `..` and replace that with the paren. - builder.Add(new TextChange(new TextSpan(dotToken.SpanStart, 1), ")")); - } - else - { - // Place a paren before the question. - builder.Add(new TextChange(new TextSpan(questionToken.Value.SpanStart, 0), ")")); - // then remove the first dot that comes after. - builder.Add(new TextChange(new TextSpan(dotToken.SpanStart, 1), "")); - } + // Place a paren before the question. + builder.Add(new TextChange(new TextSpan(questionToken.Value.SpanStart, 0), ")")); + // then remove the first dot that comes after. + builder.Add(new TextChange(new TextSpan(dotToken.SpanStart, 1), "")); + } - // If the user partially wrote out the conversion type, delete what they've written. - var tokenOnLeft = root.FindTokenOnLeftOfPosition(position, includeSkipped: true); - if (CSharpSyntaxFacts.Instance.IsWord(tokenOnLeft)) - builder.Add(new TextChange(tokenOnLeft.Span, "")); + // If the user partially wrote out the conversion type, delete what they've written. + var tokenOnLeft = root.FindTokenOnLeftOfPosition(position, includeSkipped: true); + if (CSharpSyntaxFacts.Instance.IsWord(tokenOnLeft)) + builder.Add(new TextChange(tokenOnLeft.Span, "")); - var newText = text.WithChanges(builder); - var allChanges = builder.ToImmutable(); + var newText = text.WithChanges(builder); + var allChanges = builder.ToImmutable(); - // Collapse all text changes down to a single change (for clients that only care about that), but also keep - // all the individual changes around for clients that prefer the fine-grained information. - return CompletionChange.Create( - CodeAnalysis.Completion.Utilities.Collapse(newText, allChanges), - allChanges); - } + // Collapse all text changes down to a single change (for clients that only care about that), but also keep + // all the individual changes around for clients that prefer the fine-grained information. + return CompletionChange.Create( + CodeAnalysis.Completion.Utilities.Collapse(newText, allChanges), + allChanges); + } - private static async Task GetConversionDescriptionAsync(Document document, CompletionItem item, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - { - var conversion = await TryRehydrateAsync(document, item, cancellationToken).ConfigureAwait(false); - if (conversion == null) - return null; + private static async Task GetConversionDescriptionAsync(Document document, CompletionItem item, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + { + var conversion = await TryRehydrateAsync(document, item, cancellationToken).ConfigureAwait(false); + if (conversion == null) + return null; - return await SymbolCompletionItem.GetDescriptionForSymbolsAsync( - item, document, [conversion], displayOptions, cancellationToken).ConfigureAwait(false); - } + return await SymbolCompletionItem.GetDescriptionForSymbolsAsync( + item, document, [conversion], displayOptions, cancellationToken).ConfigureAwait(false); + } - private static async Task TryRehydrateAsync(Document document, CompletionItem item, CancellationToken cancellationToken) + private static async Task TryRehydrateAsync(Document document, CompletionItem item, CancellationToken cancellationToken) + { + // If we're need to rehydrate the conversion, pull out the necessary parts. + if (item.TryGetProperty(RehydrateName, out var _)) { - // If we're need to rehydrate the conversion, pull out the necessary parts. - if (item.TryGetProperty(RehydrateName, out var _)) - { - var symbols = await SymbolCompletionItem.GetSymbolsAsync(item, document, cancellationToken).ConfigureAwait(false); - if (symbols is [INamedTypeSymbol containingType, ITypeSymbol fromType, ITypeSymbol toType]) - { - return CodeGenerationSymbolFactory.CreateConversionSymbol( - toType: toType, - fromType: CodeGenerationSymbolFactory.CreateParameterSymbol(fromType, "value"), - containingType: containingType, - documentationCommentXml: item.GetProperty(DocumentationCommentXmlName)); - } - - return null; - } - else + var symbols = await SymbolCompletionItem.GetSymbolsAsync(item, document, cancellationToken).ConfigureAwait(false); + if (symbols is [INamedTypeSymbol containingType, ITypeSymbol fromType, ITypeSymbol toType]) { - // Otherwise, just go retrieve the conversion directly. - var symbols = await SymbolCompletionItem.GetSymbolsAsync(item, document, cancellationToken).ConfigureAwait(false); - return symbols.Length == 1 ? symbols.Single() : null; + return CodeGenerationSymbolFactory.CreateConversionSymbol( + toType: toType, + fromType: CodeGenerationSymbolFactory.CreateParameterSymbol(fromType, "value"), + containingType: containingType, + documentationCommentXml: item.GetProperty(DocumentationCommentXmlName)); } + + return null; + } + else + { + // Otherwise, just go retrieve the conversion directly. + var symbols = await SymbolCompletionItem.GetSymbolsAsync(item, document, cancellationToken).ConfigureAwait(false); + return symbols.Length == 1 ? symbols.Single() : null; } } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Indexers.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Indexers.cs index f66779db027e3..caa0d240ab0dd 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Indexers.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Indexers.cs @@ -10,35 +10,34 @@ using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +internal partial class UnnamedSymbolCompletionProvider { - internal partial class UnnamedSymbolCompletionProvider - { - private readonly ImmutableArray> IndexerProperties = - [new KeyValuePair(KindName, IndexerKindName)]; + private readonly ImmutableArray> IndexerProperties = + [new KeyValuePair(KindName, IndexerKindName)]; - private void AddIndexers(CompletionContext context, ImmutableArray indexers) - { - if (indexers.Length == 0) - return; + private void AddIndexers(CompletionContext context, ImmutableArray indexers) + { + if (indexers.Length == 0) + return; - context.AddItem(SymbolCompletionItem.CreateWithSymbolId( - displayText: "this", - displayTextSuffix: "[]", - filterText: "this", - sortText: "this", - symbols: indexers, - rules: CompletionItemRules.Default, - contextPosition: context.Position, - properties: IndexerProperties, - isComplexTextEdit: true)); - } + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( + displayText: "this", + displayTextSuffix: "[]", + filterText: "this", + sortText: "this", + symbols: indexers, + rules: CompletionItemRules.Default, + contextPosition: context.Position, + properties: IndexerProperties, + isComplexTextEdit: true)); + } - // Remove the dot, but leave the ? if one is there. Place the caret one space back so it is between the braces. - private static Task GetIndexerChangeAsync(Document document, CompletionItem item, CancellationToken cancellationToken) - => ReplaceTextAfterOperatorAsync(document, item, text: "[]", keepQuestion: true, positionOffset: -1, cancellationToken); + // Remove the dot, but leave the ? if one is there. Place the caret one space back so it is between the braces. + private static Task GetIndexerChangeAsync(Document document, CompletionItem item, CancellationToken cancellationToken) + => ReplaceTextAfterOperatorAsync(document, item, text: "[]", keepQuestion: true, positionOffset: -1, cancellationToken); - private static Task GetIndexerDescriptionAsync(Document document, CompletionItem item, SymbolDescriptionOptions options, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, options, cancellationToken); - } + private static Task GetIndexerDescriptionAsync(Document document, CompletionItem item, SymbolDescriptionOptions options, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, options, cancellationToken); } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Operators.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Operators.cs index 12237f92abd66..1b0480be7bd30 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Operators.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Operators.cs @@ -15,166 +15,165 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +internal partial class UnnamedSymbolCompletionProvider { - internal partial class UnnamedSymbolCompletionProvider + [Flags] + private enum OperatorPosition { - [Flags] - private enum OperatorPosition - { - None = 0, - Prefix = 1, - Infix = 2, - Postfix = 4, - } + None = 0, + Prefix = 1, + Infix = 2, + Postfix = 4, + } + + // Place operators after conversions. + private readonly int OperatorSortingGroupIndex = 2; + + private readonly string OperatorName = nameof(OperatorName); + private readonly ImmutableArray> OperatorProperties = + [new KeyValuePair(KindName, OperatorKindName)]; + + /// + /// Ordered in the order we want to display operators in the completion list. + /// + private static readonly ImmutableArray<(string name, OperatorPosition position)> s_operatorInfo = + [ + (WellKnownMemberNames.EqualityOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.InequalityOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.GreaterThanOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.GreaterThanOrEqualOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.LessThanOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.LessThanOrEqualOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.LogicalNotOperatorName, OperatorPosition.Prefix), + (WellKnownMemberNames.AdditionOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.SubtractionOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.MultiplyOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.DivisionOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.ModulusOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.IncrementOperatorName, OperatorPosition.Prefix | OperatorPosition.Postfix), + (WellKnownMemberNames.DecrementOperatorName, OperatorPosition.Prefix | OperatorPosition.Postfix), + (WellKnownMemberNames.UnaryPlusOperatorName, OperatorPosition.Prefix), + (WellKnownMemberNames.UnaryNegationOperatorName, OperatorPosition.Prefix), + (WellKnownMemberNames.BitwiseAndOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.BitwiseOrOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.ExclusiveOrOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.LeftShiftOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.RightShiftOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.UnsignedRightShiftOperatorName, OperatorPosition.Infix), + (WellKnownMemberNames.OnesComplementOperatorName, OperatorPosition.Prefix), + ]; + + /// + /// Mapping from operator name to info about it. + /// + private static readonly Dictionary s_operatorNameToInfo = []; + + private static readonly CompletionItemRules s_operatorRules; + + static UnnamedSymbolCompletionProvider() + { + // Collect all the characters used in C# operators and make them filter characters and not commit + // characters. We want people to be able to write `x.=` and have that filter down to operators like `==` and + // `!=` so they can select and commit them. + using var _ = PooledHashSet.GetInstance(out var filterCharacters); - // Place operators after conversions. - private readonly int OperatorSortingGroupIndex = 2; - - private readonly string OperatorName = nameof(OperatorName); - private readonly ImmutableArray> OperatorProperties = - [new KeyValuePair(KindName, OperatorKindName)]; - - /// - /// Ordered in the order we want to display operators in the completion list. - /// - private static readonly ImmutableArray<(string name, OperatorPosition position)> s_operatorInfo = - [ - (WellKnownMemberNames.EqualityOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.InequalityOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.GreaterThanOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.GreaterThanOrEqualOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.LessThanOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.LessThanOrEqualOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.LogicalNotOperatorName, OperatorPosition.Prefix), - (WellKnownMemberNames.AdditionOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.SubtractionOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.MultiplyOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.DivisionOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.ModulusOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.IncrementOperatorName, OperatorPosition.Prefix | OperatorPosition.Postfix), - (WellKnownMemberNames.DecrementOperatorName, OperatorPosition.Prefix | OperatorPosition.Postfix), - (WellKnownMemberNames.UnaryPlusOperatorName, OperatorPosition.Prefix), - (WellKnownMemberNames.UnaryNegationOperatorName, OperatorPosition.Prefix), - (WellKnownMemberNames.BitwiseAndOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.BitwiseOrOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.ExclusiveOrOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.LeftShiftOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.RightShiftOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.UnsignedRightShiftOperatorName, OperatorPosition.Infix), - (WellKnownMemberNames.OnesComplementOperatorName, OperatorPosition.Prefix), - ]; - - /// - /// Mapping from operator name to info about it. - /// - private static readonly Dictionary s_operatorNameToInfo = []; - - private static readonly CompletionItemRules s_operatorRules; - - static UnnamedSymbolCompletionProvider() + for (var i = 0; i < s_operatorInfo.Length; i++) { - // Collect all the characters used in C# operators and make them filter characters and not commit - // characters. We want people to be able to write `x.=` and have that filter down to operators like `==` and - // `!=` so they can select and commit them. - using var _ = PooledHashSet.GetInstance(out var filterCharacters); + var (opName, position) = s_operatorInfo[i]; + var opText = GetOperatorText(opName); + s_operatorNameToInfo[opName] = (sortOrder: i, position); - for (var i = 0; i < s_operatorInfo.Length; i++) + foreach (var ch in opText) { - var (opName, position) = s_operatorInfo[i]; - var opText = GetOperatorText(opName); - s_operatorNameToInfo[opName] = (sortOrder: i, position); - - foreach (var ch in opText) - { - if (!char.IsLetterOrDigit(ch)) - filterCharacters.Add(ch); - } + if (!char.IsLetterOrDigit(ch)) + filterCharacters.Add(ch); } - - var opCharacters = ImmutableArray.CreateRange(filterCharacters); - s_operatorRules = CompletionItemRules.Default - .WithFilterCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Add, opCharacters)) - .WithCommitCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, opCharacters)); } - private void AddOperatorGroup(CompletionContext context, string opName, IEnumerable operators) - { - if (!s_operatorNameToInfo.TryGetValue(opName, out var sortOrderAndPosition)) - return; - - var displayText = GetOperatorText(opName); - context.AddItem(SymbolCompletionItem.CreateWithSymbolId( - displayText: displayText, - displayTextSuffix: null, - inlineDescription: GetOperatorInlineDescription(opName), - filterText: displayText, - sortText: SortText(OperatorSortingGroupIndex, $"{sortOrderAndPosition.sortOrder:000}"), - symbols: operators.ToImmutableArray(), - rules: s_operatorRules, - contextPosition: context.Position, - properties: [.. OperatorProperties, new KeyValuePair(OperatorName, opName)], - isComplexTextEdit: true)); - } + var opCharacters = ImmutableArray.CreateRange(filterCharacters); + s_operatorRules = CompletionItemRules.Default + .WithFilterCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Add, opCharacters)) + .WithCommitCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, opCharacters)); + } - private static string GetOperatorText(string opName) - => SyntaxFacts.GetText(SyntaxFacts.GetOperatorKind(opName)); + private void AddOperatorGroup(CompletionContext context, string opName, IEnumerable operators) + { + if (!s_operatorNameToInfo.TryGetValue(opName, out var sortOrderAndPosition)) + return; + + var displayText = GetOperatorText(opName); + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( + displayText: displayText, + displayTextSuffix: null, + inlineDescription: GetOperatorInlineDescription(opName), + filterText: displayText, + sortText: SortText(OperatorSortingGroupIndex, $"{sortOrderAndPosition.sortOrder:000}"), + symbols: operators.ToImmutableArray(), + rules: s_operatorRules, + contextPosition: context.Position, + properties: [.. OperatorProperties, new KeyValuePair(OperatorName, opName)], + isComplexTextEdit: true)); + } - private async Task GetOperatorChangeAsync( - Document document, CompletionItem item, CancellationToken cancellationToken) - { - var opName = item.GetProperty(OperatorName); - var opPosition = GetOperatorPosition(opName); + private static string GetOperatorText(string opName) + => SyntaxFacts.GetText(SyntaxFacts.GetOperatorKind(opName)); - if (opPosition.HasFlag(OperatorPosition.Infix)) - return await ReplaceTextAfterOperatorAsync(document, item, text: $" {item.DisplayText} ", cancellationToken).ConfigureAwait(false); + private async Task GetOperatorChangeAsync( + Document document, CompletionItem item, CancellationToken cancellationToken) + { + var opName = item.GetProperty(OperatorName); + var opPosition = GetOperatorPosition(opName); - if (opPosition.HasFlag(OperatorPosition.Postfix)) - return await ReplaceTextAfterOperatorAsync(document, item, text: $"{item.DisplayText} ", cancellationToken).ConfigureAwait(false); + if (opPosition.HasFlag(OperatorPosition.Infix)) + return await ReplaceTextAfterOperatorAsync(document, item, text: $" {item.DisplayText} ", cancellationToken).ConfigureAwait(false); - if (opPosition.HasFlag(OperatorPosition.Prefix)) - { - var position = SymbolCompletionItem.GetContextPosition(item); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var (dotLikeToken, expressionStart) = GetDotAndExpressionStart(root, position, cancellationToken); - - // Place the new operator before the expression, and delete the dot. - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var replacement = item.DisplayText + text.ToString(TextSpan.FromBounds(expressionStart, dotLikeToken.SpanStart)); - var fullTextChange = new TextChange( - TextSpan.FromBounds( - expressionStart, - dotLikeToken.Kind() == SyntaxKind.DotDotToken ? dotLikeToken.Span.Start + 1 : dotLikeToken.Span.End), - replacement); - - var newPosition = expressionStart + replacement.Length; - return CompletionChange.Create(fullTextChange, newPosition); - } + if (opPosition.HasFlag(OperatorPosition.Postfix)) + return await ReplaceTextAfterOperatorAsync(document, item, text: $"{item.DisplayText} ", cancellationToken).ConfigureAwait(false); - throw ExceptionUtilities.UnexpectedValue(opPosition); + if (opPosition.HasFlag(OperatorPosition.Prefix)) + { + var position = SymbolCompletionItem.GetContextPosition(item); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var (dotLikeToken, expressionStart) = GetDotAndExpressionStart(root, position, cancellationToken); + + // Place the new operator before the expression, and delete the dot. + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var replacement = item.DisplayText + text.ToString(TextSpan.FromBounds(expressionStart, dotLikeToken.SpanStart)); + var fullTextChange = new TextChange( + TextSpan.FromBounds( + expressionStart, + dotLikeToken.Kind() == SyntaxKind.DotDotToken ? dotLikeToken.Span.Start + 1 : dotLikeToken.Span.End), + replacement); + + var newPosition = expressionStart + replacement.Length; + return CompletionChange.Create(fullTextChange, newPosition); } - private static OperatorPosition GetOperatorPosition(string operatorName) - => s_operatorNameToInfo[operatorName].position; + throw ExceptionUtilities.UnexpectedValue(opPosition); + } - private static Task GetOperatorDescriptionAsync(Document document, CompletionItem item, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + private static OperatorPosition GetOperatorPosition(string operatorName) + => s_operatorNameToInfo[operatorName].position; - private static string GetOperatorInlineDescription(string opName) - { - var opText = GetOperatorText(opName); - var opPosition = GetOperatorPosition(opName); + private static Task GetOperatorDescriptionAsync(Document document, CompletionItem item, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); - if (opPosition.HasFlag(OperatorPosition.Postfix)) - return $"x{opText}"; + private static string GetOperatorInlineDescription(string opName) + { + var opText = GetOperatorText(opName); + var opPosition = GetOperatorPosition(opName); - if (opPosition.HasFlag(OperatorPosition.Infix)) - return $"x {opText} y"; + if (opPosition.HasFlag(OperatorPosition.Postfix)) + return $"x{opText}"; - if (opPosition.HasFlag(OperatorPosition.Prefix)) - return $"{opText}x"; + if (opPosition.HasFlag(OperatorPosition.Infix)) + return $"x {opText} y"; - return opText; - } + if (opPosition.HasFlag(OperatorPosition.Prefix)) + return $"{opText}x"; + + return opText; } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/OverrideCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/OverrideCompletionProvider.cs index 458a40188fd21..3903b0e06656c 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/OverrideCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/OverrideCompletionProvider.cs @@ -16,230 +16,229 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(OverrideCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(PreprocessorCompletionProvider))] +[Shared] +internal partial class OverrideCompletionProvider : AbstractOverrideCompletionProvider { - [ExportCompletionProvider(nameof(OverrideCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(PreprocessorCompletionProvider))] - [Shared] - internal partial class OverrideCompletionProvider : AbstractOverrideCompletionProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public OverrideCompletionProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public OverrideCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - protected override SyntaxNode GetSyntax(SyntaxToken token) - { - return token.GetAncestor() - ?? token.GetAncestor() - ?? token.GetAncestor() - ?? token.GetAncestor() - ?? (SyntaxNode?)token.GetAncestor() - ?? throw ExceptionUtilities.UnexpectedValue(token); - } + protected override SyntaxNode GetSyntax(SyntaxToken token) + { + return token.GetAncestor() + ?? token.GetAncestor() + ?? token.GetAncestor() + ?? token.GetAncestor() + ?? (SyntaxNode?)token.GetAncestor() + ?? throw ExceptionUtilities.UnexpectedValue(token); + } - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, characterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, characterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; - protected override SyntaxToken GetToken(CompletionItem completionItem, SyntaxTree tree, CancellationToken cancellationToken) + protected override SyntaxToken GetToken(CompletionItem completionItem, SyntaxTree tree, CancellationToken cancellationToken) + { + var tokenSpanEnd = MemberInsertionCompletionItem.GetTokenSpanEnd(completionItem); + return tree.FindTokenOnLeftOfPosition(tokenSpanEnd, cancellationToken); + } + + public override bool TryDetermineReturnType(SyntaxToken startToken, SemanticModel semanticModel, CancellationToken cancellationToken, out ITypeSymbol? returnType, out SyntaxToken nextToken) + { + nextToken = startToken; + returnType = null; + if (startToken.Parent is TypeSyntax typeSyntax) { - var tokenSpanEnd = MemberInsertionCompletionItem.GetTokenSpanEnd(completionItem); - return tree.FindTokenOnLeftOfPosition(tokenSpanEnd, cancellationToken); + // 'partial' is actually an identifier. If we see it just bail. This does mean + // we won't handle overrides that actually return a type called 'partial'. And + // not a single tear was shed. + if (typeSyntax is IdentifierNameSyntax identifierName && + identifierName.Identifier.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword)) + { + return false; + } + + returnType = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type; + nextToken = typeSyntax.GetFirstToken().GetPreviousToken(); } - public override bool TryDetermineReturnType(SyntaxToken startToken, SemanticModel semanticModel, CancellationToken cancellationToken, out ITypeSymbol? returnType, out SyntaxToken nextToken) + return true; + } + + public override bool TryDetermineModifiers( + SyntaxToken startToken, + SourceText text, + int startLine, + out Accessibility seenAccessibility, + out DeclarationModifiers modifiers) + { + var token = startToken; + var parentMember = token.Parent; + modifiers = default; + seenAccessibility = Accessibility.NotApplicable; + + if (parentMember is null) + return false; + + // Keep walking backwards as long as we're still within our parent member. + while (token != default) { - nextToken = startToken; - returnType = null; - if (startToken.Parent is TypeSyntax typeSyntax) + if (token.SpanStart < parentMember.SpanStart) { - // 'partial' is actually an identifier. If we see it just bail. This does mean - // we won't handle overrides that actually return a type called 'partial'. And - // not a single tear was shed. - if (typeSyntax is IdentifierNameSyntax identifierName && - identifierName.Identifier.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword)) - { + // moved before the start of the member we're in. If previous member's token is on the same line, + // we bail out as our replacement will delete the entire line we're on. + if (IsOnStartLine(token.SpanStart, text, startLine)) return false; - } - returnType = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type; - nextToken = typeSyntax.GetFirstToken().GetPreviousToken(); + break; } - return true; - } + // Ok to hit a `]` if it's the end of attributes on this member. + if (token.Kind() == SyntaxKind.CloseBracketToken) + { + if (token.Parent is not AttributeListSyntax) + return false; - public override bool TryDetermineModifiers( - SyntaxToken startToken, - SourceText text, - int startLine, - out Accessibility seenAccessibility, - out DeclarationModifiers modifiers) - { - var token = startToken; - var parentMember = token.Parent; - modifiers = default; - seenAccessibility = Accessibility.NotApplicable; + break; + } - if (parentMember is null) + // We only accept tokens that precede us on the same line. Splitting across multiple lines is too niche + // to want to support, and more likely indicates a case of broken code that the user is in the middle of + // fixing up. + if (!IsOnStartLine(token.SpanStart, text, startLine)) return false; - // Keep walking backwards as long as we're still within our parent member. - while (token != default) + switch (token.Kind()) { - if (token.SpanStart < parentMember.SpanStart) - { - // moved before the start of the member we're in. If previous member's token is on the same line, - // we bail out as our replacement will delete the entire line we're on. - if (IsOnStartLine(token.SpanStart, text, startLine)) - return false; + // Standard modifier cases we accept. + case SyntaxKind.AbstractKeyword: + modifiers = modifiers.WithIsAbstract(true); + break; + case SyntaxKind.ExternKeyword: + modifiers = modifiers.WithIsExtern(true); + break; + case SyntaxKind.OverrideKeyword: + modifiers = modifiers.WithIsOverride(true); + break; + case SyntaxKind.RequiredKeyword: + modifiers = modifiers.WithIsRequired(true); + break; + case SyntaxKind.SealedKeyword: + modifiers = modifiers.WithIsSealed(true); + break; + case SyntaxKind.UnsafeKeyword: + modifiers = modifiers.WithIsUnsafe(true); break; - } - // Ok to hit a `]` if it's the end of attributes on this member. - if (token.Kind() == SyntaxKind.CloseBracketToken) - { - if (token.Parent is not AttributeListSyntax) - return false; + // Accessibility modifiers we accept. + case SyntaxKind.PublicKeyword: + seenAccessibility = seenAccessibility == Accessibility.NotApplicable + ? Accessibility.Public + : seenAccessibility; break; - } - - // We only accept tokens that precede us on the same line. Splitting across multiple lines is too niche - // to want to support, and more likely indicates a case of broken code that the user is in the middle of - // fixing up. - if (!IsOnStartLine(token.SpanStart, text, startLine)) + case SyntaxKind.PrivateKeyword: + seenAccessibility = seenAccessibility switch + { + Accessibility.NotApplicable => Accessibility.Private, + // If we see private AND protected, filter for private protected + Accessibility.Protected => Accessibility.ProtectedAndInternal, + _ => seenAccessibility, + }; + break; + case SyntaxKind.InternalKeyword: + // If we see internal AND protected, filter for protected internal + seenAccessibility = seenAccessibility switch + { + Accessibility.NotApplicable => Accessibility.Internal, + Accessibility.Protected => Accessibility.ProtectedOrInternal, + _ => seenAccessibility, + }; + break; + case SyntaxKind.ProtectedKeyword: + seenAccessibility = seenAccessibility switch + { + Accessibility.NotApplicable => Accessibility.Protected, + // If we see protected AND internal, filter for protected internal. + Accessibility.Internal => Accessibility.ProtectedOrInternal, + // Or if we see private AND protected, filter for private protected + Accessibility.Private => Accessibility.ProtectedAndInternal, + _ => seenAccessibility, + }; + break; + default: + // If we hit anything else, then this token is not valid for override completions, and we can just bail here. return false; - - switch (token.Kind()) - { - // Standard modifier cases we accept. - - case SyntaxKind.AbstractKeyword: - modifiers = modifiers.WithIsAbstract(true); - break; - case SyntaxKind.ExternKeyword: - modifiers = modifiers.WithIsExtern(true); - break; - case SyntaxKind.OverrideKeyword: - modifiers = modifiers.WithIsOverride(true); - break; - case SyntaxKind.RequiredKeyword: - modifiers = modifiers.WithIsRequired(true); - break; - case SyntaxKind.SealedKeyword: - modifiers = modifiers.WithIsSealed(true); - break; - case SyntaxKind.UnsafeKeyword: - modifiers = modifiers.WithIsUnsafe(true); - break; - - // Accessibility modifiers we accept. - - case SyntaxKind.PublicKeyword: - seenAccessibility = seenAccessibility == Accessibility.NotApplicable - ? Accessibility.Public - : seenAccessibility; - break; - case SyntaxKind.PrivateKeyword: - seenAccessibility = seenAccessibility switch - { - Accessibility.NotApplicable => Accessibility.Private, - // If we see private AND protected, filter for private protected - Accessibility.Protected => Accessibility.ProtectedAndInternal, - _ => seenAccessibility, - }; - break; - case SyntaxKind.InternalKeyword: - // If we see internal AND protected, filter for protected internal - seenAccessibility = seenAccessibility switch - { - Accessibility.NotApplicable => Accessibility.Internal, - Accessibility.Protected => Accessibility.ProtectedOrInternal, - _ => seenAccessibility, - }; - break; - case SyntaxKind.ProtectedKeyword: - seenAccessibility = seenAccessibility switch - { - Accessibility.NotApplicable => Accessibility.Protected, - // If we see protected AND internal, filter for protected internal. - Accessibility.Internal => Accessibility.ProtectedOrInternal, - // Or if we see private AND protected, filter for private protected - Accessibility.Private => Accessibility.ProtectedAndInternal, - _ => seenAccessibility, - }; - break; - default: - // If we hit anything else, then this token is not valid for override completions, and we can just bail here. - return false; - } - - token = token.GetPreviousToken(); } - // Have to at least found the override token for us to offer override-completion. - return modifiers.IsOverride; + token = token.GetPreviousToken(); } - public override SyntaxToken FindStartingToken(SyntaxTree tree, int position, CancellationToken cancellationToken) + // Have to at least found the override token for us to offer override-completion. + return modifiers.IsOverride; + } + + public override SyntaxToken FindStartingToken(SyntaxTree tree, int position, CancellationToken cancellationToken) + { + var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken); + return token.GetPreviousTokenIfTouchingWord(position); + } + + public override ImmutableArray FilterOverrides(ImmutableArray members, ITypeSymbol? returnType) + { + if (returnType == null) { - var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken); - return token.GetPreviousTokenIfTouchingWord(position); + return members; } - public override ImmutableArray FilterOverrides(ImmutableArray members, ITypeSymbol? returnType) - { - if (returnType == null) - { - return members; - } + var filteredMembers = members.WhereAsArray(m => + SymbolEquivalenceComparer.Instance.Equals(GetReturnType(m), returnType)); - var filteredMembers = members.WhereAsArray(m => - SymbolEquivalenceComparer.Instance.Equals(GetReturnType(m), returnType)); + // Don't filter by return type if we would then have nothing to show. + // This way, the user gets completion even if they speculatively typed the wrong return type + return filteredMembers.Length > 0 ? filteredMembers : members; + } - // Don't filter by return type if we would then have nothing to show. - // This way, the user gets completion even if they speculatively typed the wrong return type - return filteredMembers.Length > 0 ? filteredMembers : members; + protected override int GetTargetCaretPosition(SyntaxNode caretTarget) + { + // Inserted Event declarations are a single line, so move to the end of the line. + if (caretTarget is EventFieldDeclarationSyntax) + { + return caretTarget.GetLocation().SourceSpan.End; } - - protected override int GetTargetCaretPosition(SyntaxNode caretTarget) + else if (caretTarget is MethodDeclarationSyntax methodDeclaration) { - // Inserted Event declarations are a single line, so move to the end of the line. - if (caretTarget is EventFieldDeclarationSyntax) - { - return caretTarget.GetLocation().SourceSpan.End; - } - else if (caretTarget is MethodDeclarationSyntax methodDeclaration) - { - return CompletionUtilities.GetTargetCaretPositionForMethod(methodDeclaration); - } - else if (caretTarget is BasePropertyDeclarationSyntax propertyDeclaration) + return CompletionUtilities.GetTargetCaretPositionForMethod(methodDeclaration); + } + else if (caretTarget is BasePropertyDeclarationSyntax propertyDeclaration) + { + // property: no accessors; move to the end of the declaration + if (propertyDeclaration.AccessorList != null && propertyDeclaration.AccessorList.Accessors.Any()) { - // property: no accessors; move to the end of the declaration - if (propertyDeclaration.AccessorList != null && propertyDeclaration.AccessorList.Accessors.Any()) - { - // move to the end of the last statement of the first accessor - var firstAccessor = propertyDeclaration.AccessorList.Accessors[0]; - var firstAccessorStatement = (SyntaxNode?)firstAccessor.Body?.Statements.LastOrDefault() ?? - firstAccessor.ExpressionBody!.Expression; - return firstAccessorStatement.GetLocation().SourceSpan.End; - } - else - { - return propertyDeclaration.GetLocation().SourceSpan.End; - } + // move to the end of the last statement of the first accessor + var firstAccessor = propertyDeclaration.AccessorList.Accessors[0]; + var firstAccessorStatement = (SyntaxNode?)firstAccessor.Body?.Statements.LastOrDefault() ?? + firstAccessor.ExpressionBody!.Expression; + return firstAccessorStatement.GetLocation().SourceSpan.End; } else { - throw ExceptionUtilities.Unreachable(); + return propertyDeclaration.GetLocation().SourceSpan.End; } } + else + { + throw ExceptionUtilities.Unreachable(); + } } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/PartialMethodCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/PartialMethodCompletionProvider.cs index e5b709dc73709..6a08720c0bcec 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/PartialMethodCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/PartialMethodCompletionProvider.cs @@ -19,126 +19,125 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(PartialMethodCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(OverrideCompletionProvider))] +[Shared] +internal partial class PartialMethodCompletionProvider : AbstractPartialMethodCompletionProvider { - [ExportCompletionProvider(nameof(PartialMethodCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(OverrideCompletionProvider))] - [Shared] - internal partial class PartialMethodCompletionProvider : AbstractPartialMethodCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public PartialMethodCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public PartialMethodCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - protected override bool IncludeAccessibility(IMethodSymbol method, CancellationToken cancellationToken) + protected override bool IncludeAccessibility(IMethodSymbol method, CancellationToken cancellationToken) + { + var declaration = (MethodDeclarationSyntax)method.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); + foreach (var mod in declaration.Modifiers) { - var declaration = (MethodDeclarationSyntax)method.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); - foreach (var mod in declaration.Modifiers) + switch (mod.Kind()) { - switch (mod.Kind()) - { - case SyntaxKind.PublicKeyword: - case SyntaxKind.ProtectedKeyword: - case SyntaxKind.InternalKeyword: - case SyntaxKind.PrivateKeyword: - return true; - } + case SyntaxKind.PublicKeyword: + case SyntaxKind.ProtectedKeyword: + case SyntaxKind.InternalKeyword: + case SyntaxKind.PrivateKeyword: + return true; } - - return false; } - protected override SyntaxNode GetSyntax(SyntaxToken token) - { - return token.GetAncestor() - ?? token.GetAncestor() - ?? token.GetAncestor() - ?? token.GetAncestor() - ?? (SyntaxNode?)token.GetAncestor() - ?? throw ExceptionUtilities.UnexpectedValue(token); - } + return false; + } - protected override int GetTargetCaretPosition(SyntaxNode caretTarget) - { - var methodDeclaration = (MethodDeclarationSyntax)caretTarget; - return CompletionUtilities.GetTargetCaretPositionForMethod(methodDeclaration); - } + protected override SyntaxNode GetSyntax(SyntaxToken token) + { + return token.GetAncestor() + ?? token.GetAncestor() + ?? token.GetAncestor() + ?? token.GetAncestor() + ?? (SyntaxNode?)token.GetAncestor() + ?? throw ExceptionUtilities.UnexpectedValue(token); + } - protected override SyntaxToken GetToken(CompletionItem completionItem, SyntaxTree tree, CancellationToken cancellationToken) - { - var tokenSpanEnd = MemberInsertionCompletionItem.GetTokenSpanEnd(completionItem); - return tree.FindTokenOnLeftOfPosition(tokenSpanEnd, cancellationToken); - } + protected override int GetTargetCaretPosition(SyntaxNode caretTarget) + { + var methodDeclaration = (MethodDeclarationSyntax)caretTarget; + return CompletionUtilities.GetTargetCaretPositionForMethod(methodDeclaration); + } - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => text[characterPosition] == ' ' || - options.TriggerOnTypingLetters && CompletionUtilities.IsStartingNewWord(text, characterPosition); + protected override SyntaxToken GetToken(CompletionItem completionItem, SyntaxTree tree, CancellationToken cancellationToken) + { + var tokenSpanEnd = MemberInsertionCompletionItem.GetTokenSpanEnd(completionItem); + return tree.FindTokenOnLeftOfPosition(tokenSpanEnd, cancellationToken); + } - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => text[characterPosition] == ' ' || + options.TriggerOnTypingLetters && CompletionUtilities.IsStartingNewWord(text, characterPosition); - protected override bool IsPartial(IMethodSymbol method) - { - var declarations = method.DeclaringSyntaxReferences.Select(r => r.GetSyntax()).OfType(); - return declarations.Any(d => d.Body == null && d.Modifiers.Any(SyntaxKind.PartialKeyword)); - } + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; - protected override bool IsPartialMethodCompletionContext(SyntaxTree tree, int position, CancellationToken cancellationToken, out DeclarationModifiers modifiers, out SyntaxToken token) - { - var touchingToken = tree.FindTokenOnLeftOfPosition(position, cancellationToken); - var targetToken = touchingToken.GetPreviousTokenIfTouchingWord(position); - var text = tree.GetText(cancellationToken); - - token = targetToken; + protected override bool IsPartial(IMethodSymbol method) + { + var declarations = method.DeclaringSyntaxReferences.Select(r => r.GetSyntax()).OfType(); + return declarations.Any(d => d.Body == null && d.Modifiers.Any(SyntaxKind.PartialKeyword)); + } - modifiers = default; + protected override bool IsPartialMethodCompletionContext(SyntaxTree tree, int position, CancellationToken cancellationToken, out DeclarationModifiers modifiers, out SyntaxToken token) + { + var touchingToken = tree.FindTokenOnLeftOfPosition(position, cancellationToken); + var targetToken = touchingToken.GetPreviousTokenIfTouchingWord(position); + var text = tree.GetText(cancellationToken); - if (targetToken.Kind() is SyntaxKind.VoidKeyword or SyntaxKind.PartialKeyword || - (targetToken.Kind() == SyntaxKind.IdentifierToken && targetToken.HasMatchingText(SyntaxKind.PartialKeyword))) - { - return !IsOnSameLine(touchingToken.GetNextToken(), touchingToken, text) && - VerifyModifiers(tree, position, cancellationToken, out modifiers); - } + token = targetToken; - return false; - } + modifiers = default; - private static bool VerifyModifiers(SyntaxTree tree, int position, CancellationToken cancellationToken, out DeclarationModifiers modifiers) + if (targetToken.Kind() is SyntaxKind.VoidKeyword or SyntaxKind.PartialKeyword || + (targetToken.Kind() == SyntaxKind.IdentifierToken && targetToken.HasMatchingText(SyntaxKind.PartialKeyword))) { - var touchingToken = tree.FindTokenOnLeftOfPosition(position, cancellationToken); - var token = touchingToken.GetPreviousToken(); + return !IsOnSameLine(touchingToken.GetNextToken(), touchingToken, text) && + VerifyModifiers(tree, position, cancellationToken, out modifiers); + } - var foundPartial = touchingToken.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword); - var foundAsync = false; + return false; + } - while (IsOnSameLine(token, touchingToken, tree.GetText(cancellationToken))) - { - if (token.IsKindOrHasMatchingText(SyntaxKind.AsyncKeyword)) - { - foundAsync = true; - } + private static bool VerifyModifiers(SyntaxTree tree, int position, CancellationToken cancellationToken, out DeclarationModifiers modifiers) + { + var touchingToken = tree.FindTokenOnLeftOfPosition(position, cancellationToken); + var token = touchingToken.GetPreviousToken(); - foundPartial = foundPartial || token.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword); + var foundPartial = touchingToken.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword); + var foundAsync = false; - token = token.GetPreviousToken(); + while (IsOnSameLine(token, touchingToken, tree.GetText(cancellationToken))) + { + if (token.IsKindOrHasMatchingText(SyntaxKind.AsyncKeyword)) + { + foundAsync = true; } - modifiers = new DeclarationModifiers(isAsync: foundAsync, isPartial: true); + foundPartial = foundPartial || token.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword); - return foundPartial; + token = token.GetPreviousToken(); } - private static bool IsOnSameLine(SyntaxToken syntaxToken, SyntaxToken touchingToken, SourceText text) - { - return !syntaxToken.IsKind(SyntaxKind.None) - && !touchingToken.IsKind(SyntaxKind.None) - && text.Lines.IndexOf(syntaxToken.SpanStart) == text.Lines.IndexOf(touchingToken.SpanStart); - } + modifiers = new DeclarationModifiers(isAsync: foundAsync, isPartial: true); - protected override string GetDisplayText(IMethodSymbol method, SemanticModel semanticModel, int position) - => method.ToMinimalDisplayString(semanticModel, position, SignatureDisplayFormat); + return foundPartial; } + + private static bool IsOnSameLine(SyntaxToken syntaxToken, SyntaxToken touchingToken, SourceText text) + { + return !syntaxToken.IsKind(SyntaxKind.None) + && !touchingToken.IsKind(SyntaxKind.None) + && text.Lines.IndexOf(syntaxToken.SpanStart) == text.Lines.IndexOf(touchingToken.SpanStart); + } + + protected override string GetDisplayText(IMethodSymbol method, SemanticModel semanticModel, int position) + => method.ToMinimalDisplayString(semanticModel, position, SignatureDisplayFormat); } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/PartialTypeCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/PartialTypeCompletionProvider.cs index 6b30b986f1e19..c804f80406573 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/PartialTypeCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/PartialTypeCompletionProvider.cs @@ -18,78 +18,77 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(PartialTypeCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(PartialMethodCompletionProvider))] +[Shared] +internal partial class PartialTypeCompletionProvider : AbstractPartialTypeCompletionProvider { - [ExportCompletionProvider(nameof(PartialTypeCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(PartialMethodCompletionProvider))] - [Shared] - internal partial class PartialTypeCompletionProvider : AbstractPartialTypeCompletionProvider + private const string InsertionTextOnLessThan = nameof(InsertionTextOnLessThan); + + private static readonly SymbolDisplayFormat _symbolFormatWithGenerics = + new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, + genericsOptions: + SymbolDisplayGenericsOptions.IncludeTypeParameters | + SymbolDisplayGenericsOptions.IncludeVariance, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | + SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + private static readonly SymbolDisplayFormat _symbolFormatWithoutGenerics = + _symbolFormatWithGenerics.WithGenericsOptions(SymbolDisplayGenericsOptions.None); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public PartialTypeCompletionProvider() { - private const string InsertionTextOnLessThan = nameof(InsertionTextOnLessThan); - - private static readonly SymbolDisplayFormat _symbolFormatWithGenerics = - new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, - genericsOptions: - SymbolDisplayGenericsOptions.IncludeTypeParameters | - SymbolDisplayGenericsOptions.IncludeVariance, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - private static readonly SymbolDisplayFormat _symbolFormatWithoutGenerics = - _symbolFormatWithGenerics.WithGenericsOptions(SymbolDisplayGenericsOptions.None); - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public PartialTypeCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => text[characterPosition] == ' ' || - options.TriggerOnTypingLetters && CompletionUtilities.IsStartingNewWord(text, characterPosition); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => text[characterPosition] == ' ' || + options.TriggerOnTypingLetters && CompletionUtilities.IsStartingNewWord(text, characterPosition); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter; - protected override SyntaxNode? GetPartialTypeSyntaxNode(SyntaxTree tree, int position, CancellationToken cancellationToken) - => tree.IsPartialTypeDeclarationNameContext(position, cancellationToken, out var declaration) ? declaration : null; + protected override SyntaxNode? GetPartialTypeSyntaxNode(SyntaxTree tree, int position, CancellationToken cancellationToken) + => tree.IsPartialTypeDeclarationNameContext(position, cancellationToken, out var declaration) ? declaration : null; - protected override (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(INamedTypeSymbol symbol, CSharpSyntaxContext context) - { - var displayAndInsertionText = symbol.ToMinimalDisplayString(context.SemanticModel, context.Position, _symbolFormatWithGenerics); - return (displayAndInsertionText, "", displayAndInsertionText); - } + protected override (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(INamedTypeSymbol symbol, CSharpSyntaxContext context) + { + var displayAndInsertionText = symbol.ToMinimalDisplayString(context.SemanticModel, context.Position, _symbolFormatWithGenerics); + return (displayAndInsertionText, "", displayAndInsertionText); + } - protected override IEnumerable? LookupCandidateSymbols(CSharpSyntaxContext context, INamedTypeSymbol declaredSymbol, CancellationToken cancellationToken) - { - var candidates = base.LookupCandidateSymbols(context, declaredSymbol, cancellationToken); + protected override IEnumerable? LookupCandidateSymbols(CSharpSyntaxContext context, INamedTypeSymbol declaredSymbol, CancellationToken cancellationToken) + { + var candidates = base.LookupCandidateSymbols(context, declaredSymbol, cancellationToken); - // The base class applies a broad filter when finding candidates, but since C# requires - // that all parts have the "partial" modifier, the results can be trimmed further here. - return candidates?.Where(symbol => symbol.DeclaringSyntaxReferences.Any(static (reference, cancellationToken) => IsPartialTypeDeclaration(reference.GetSyntax(cancellationToken)), cancellationToken)); - } + // The base class applies a broad filter when finding candidates, but since C# requires + // that all parts have the "partial" modifier, the results can be trimmed further here. + return candidates?.Where(symbol => symbol.DeclaringSyntaxReferences.Any(static (reference, cancellationToken) => IsPartialTypeDeclaration(reference.GetSyntax(cancellationToken)), cancellationToken)); + } - private static bool IsPartialTypeDeclaration(SyntaxNode syntax) - => syntax is BaseTypeDeclarationSyntax declarationSyntax && declarationSyntax.Modifiers.Any(SyntaxKind.PartialKeyword); + private static bool IsPartialTypeDeclaration(SyntaxNode syntax) + => syntax is BaseTypeDeclarationSyntax declarationSyntax && declarationSyntax.Modifiers.Any(SyntaxKind.PartialKeyword); - protected override ImmutableArray> GetProperties(INamedTypeSymbol symbol, CSharpSyntaxContext context) - => [new KeyValuePair(InsertionTextOnLessThan, symbol.Name.EscapeIdentifier())]; + protected override ImmutableArray> GetProperties(INamedTypeSymbol symbol, CSharpSyntaxContext context) + => [new KeyValuePair(InsertionTextOnLessThan, symbol.Name.EscapeIdentifier())]; - public override async Task GetTextChangeAsync( - Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + public override async Task GetTextChangeAsync( + Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + { + if (ch == '<') { - if (ch == '<') + if (selectedItem.TryGetProperty(InsertionTextOnLessThan, out var insertionText)) { - if (selectedItem.TryGetProperty(InsertionTextOnLessThan, out var insertionText)) - { - return new TextChange(selectedItem.Span, insertionText); - } + return new TextChange(selectedItem.Span, insertionText); } - - return await base.GetTextChangeAsync(document, selectedItem, ch, cancellationToken).ConfigureAwait(false); } + + return await base.GetTextChangeAsync(document, selectedItem, ch, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/PreprocessorCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/PreprocessorCompletionProvider.cs index ceaafbc868203..d97697233e05b 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/PreprocessorCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/PreprocessorCompletionProvider.cs @@ -11,24 +11,23 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(PreprocessorCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(ExternAliasCompletionProvider))] +[Shared] +internal class PreprocessorCompletionProvider : AbstractPreprocessorCompletionProvider { - [ExportCompletionProvider(nameof(PreprocessorCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(ExternAliasCompletionProvider))] - [Shared] - internal class PreprocessorCompletionProvider : AbstractPreprocessorCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public PreprocessorCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public PreprocessorCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; - } + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/PropertySubPatternCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/PropertySubPatternCompletionProvider.cs index e4a307e091ce2..ec19bbc3cca08 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/PropertySubPatternCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/PropertySubPatternCompletionProvider.cs @@ -21,197 +21,196 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(PropertySubpatternCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(InternalsVisibleToCompletionProvider))] +[Shared] +internal class PropertySubpatternCompletionProvider : LSPCompletionProvider { - [ExportCompletionProvider(nameof(PropertySubpatternCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(InternalsVisibleToCompletionProvider))] - [Shared] - internal class PropertySubpatternCompletionProvider : LSPCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public PropertySubpatternCompletionProvider() + { + } + + internal override string Language => LanguageNames.CSharp; + + // Examples: + // is { $$ + // is { Property.$$ + // is { Property.Property2.$$ + public override async Task ProvideCompletionsAsync(CompletionContext context) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public PropertySubpatternCompletionProvider() + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + + // For `is { Property.Property2.$$`, we get: + // - the property pattern clause `{ ... }` and + // - the member access before the last dot `Property.Property2` (or null) + var (propertyPatternClause, memberAccess) = TryGetPropertyPatternClause(tree, position, cancellationToken); + if (propertyPatternClause is null) { + return; } - internal override string Language => LanguageNames.CSharp; + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); + var propertyPatternType = semanticModel.GetTypeInfo((PatternSyntax)propertyPatternClause.Parent!, cancellationToken).ConvertedType; + // For simple property patterns, the type we want is the "input type" of the property pattern, ie the type of `c` in `c is { $$ }`. + // For extended property patterns, we get the type by following the chain of members that we have so far, ie + // the type of `c.Property` for `c is { Property.$$ }` and the type of `c.Property1.Property2` for `c is { Property1.Property2.$$ }`. + var type = GetMemberAccessType(propertyPatternType, memberAccess, document, semanticModel, position); - // Examples: - // is { $$ - // is { Property.$$ - // is { Property.Property2.$$ - public override async Task ProvideCompletionsAsync(CompletionContext context) + if (type is null) { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - - // For `is { Property.Property2.$$`, we get: - // - the property pattern clause `{ ... }` and - // - the member access before the last dot `Property.Property2` (or null) - var (propertyPatternClause, memberAccess) = TryGetPropertyPatternClause(tree, position, cancellationToken); - if (propertyPatternClause is null) - { - return; - } + return; + } - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); - var propertyPatternType = semanticModel.GetTypeInfo((PatternSyntax)propertyPatternClause.Parent!, cancellationToken).ConvertedType; - // For simple property patterns, the type we want is the "input type" of the property pattern, ie the type of `c` in `c is { $$ }`. - // For extended property patterns, we get the type by following the chain of members that we have so far, ie - // the type of `c.Property` for `c is { Property.$$ }` and the type of `c.Property1.Property2` for `c is { Property1.Property2.$$ }`. - var type = GetMemberAccessType(propertyPatternType, memberAccess, document, semanticModel, position); + // Find the members that can be tested. + var members = GetCandidatePropertiesAndFields(document, semanticModel, position, type); + members = members.WhereAsArray(m => m.IsEditorBrowsable(context.CompletionOptions.HideAdvancedMembers, semanticModel.Compilation)); - if (type is null) - { - return; - } + if (memberAccess is null) + { + // Filter out those members that have already been typed as simple (not extended) properties + var alreadyTestedMembers = new HashSet(propertyPatternClause.Subpatterns.Select( + p => p.NameColon?.Name.Identifier.ValueText).Where(s => !string.IsNullOrEmpty(s))!); - // Find the members that can be tested. - var members = GetCandidatePropertiesAndFields(document, semanticModel, position, type); - members = members.WhereAsArray(m => m.IsEditorBrowsable(context.CompletionOptions.HideAdvancedMembers, semanticModel.Compilation)); + members = members.WhereAsArray(m => !alreadyTestedMembers.Contains(m.Name)); + } - if (memberAccess is null) - { - // Filter out those members that have already been typed as simple (not extended) properties - var alreadyTestedMembers = new HashSet(propertyPatternClause.Subpatterns.Select( - p => p.NameColon?.Name.Identifier.ValueText).Where(s => !string.IsNullOrEmpty(s))!); + foreach (var member in members) + { + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( + displayText: member.Name.EscapeIdentifier(), + displayTextSuffix: "", + insertionText: null, + symbols: ImmutableArray.Create(member), + contextPosition: context.Position, + rules: s_rules)); + } - members = members.WhereAsArray(m => !alreadyTestedMembers.Contains(m.Name)); - } + return; - foreach (var member in members) + // We have to figure out the type of the extended property ourselves, because + // the semantic model could not provide the answer we want in incomplete syntax: + // `c is { X. }` + static ITypeSymbol? GetMemberAccessType(ITypeSymbol? type, ExpressionSyntax? expression, Document document, SemanticModel semanticModel, int position) + { + if (expression is null) { - context.AddItem(SymbolCompletionItem.CreateWithSymbolId( - displayText: member.Name.EscapeIdentifier(), - displayTextSuffix: "", - insertionText: null, - symbols: ImmutableArray.Create(member), - contextPosition: context.Position, - rules: s_rules)); + return type; } - - return; - - // We have to figure out the type of the extended property ourselves, because - // the semantic model could not provide the answer we want in incomplete syntax: - // `c is { X. }` - static ITypeSymbol? GetMemberAccessType(ITypeSymbol? type, ExpressionSyntax? expression, Document document, SemanticModel semanticModel, int position) + else if (expression is MemberAccessExpressionSyntax memberAccess) { - if (expression is null) - { - return type; - } - else if (expression is MemberAccessExpressionSyntax memberAccess) - { - type = GetMemberAccessType(type, memberAccess.Expression, document, semanticModel, position); - return GetMemberType(type, name: memberAccess.Name.Identifier.ValueText, document, semanticModel, position); - } - else if (expression is IdentifierNameSyntax identifier) - { - return GetMemberType(type, name: identifier.Identifier.ValueText, document, semanticModel, position); - } - - throw ExceptionUtilities.Unreachable(); + type = GetMemberAccessType(type, memberAccess.Expression, document, semanticModel, position); + return GetMemberType(type, name: memberAccess.Name.Identifier.ValueText, document, semanticModel, position); } - - static ITypeSymbol? GetMemberType(ITypeSymbol? type, string name, Document document, SemanticModel semanticModel, int position) + else if (expression is IdentifierNameSyntax identifier) { - var members = GetCandidatePropertiesAndFields(document, semanticModel, position, type); - var matches = members.WhereAsArray(m => m.Name == name); - if (matches.Length != 1) - { - return null; - } - - return matches[0] switch - { - IPropertySymbol property => property.Type, - IFieldSymbol field => field.Type, - _ => throw ExceptionUtilities.Unreachable(), - }; + return GetMemberType(type, name: identifier.Identifier.ValueText, document, semanticModel, position); } - static ImmutableArray GetCandidatePropertiesAndFields(Document document, SemanticModel semanticModel, int position, ITypeSymbol? type) - { - var members = semanticModel.LookupSymbols(position, type); - return members.WhereAsArray(m => m.CanBeReferencedByName && - IsFieldOrReadableProperty(m) && - !m.IsImplicitlyDeclared && - !m.IsStatic); - } + throw ExceptionUtilities.Unreachable(); } - private static bool IsFieldOrReadableProperty(ISymbol symbol) + static ITypeSymbol? GetMemberType(ITypeSymbol? type, string name, Document document, SemanticModel semanticModel, int position) { - if (symbol.IsKind(SymbolKind.Field)) + var members = GetCandidatePropertiesAndFields(document, semanticModel, position, type); + var matches = members.WhereAsArray(m => m.Name == name); + if (matches.Length != 1) { - return true; + return null; } - if (symbol.IsKind(SymbolKind.Property) && !((IPropertySymbol)symbol).IsWriteOnly) + return matches[0] switch { - return true; - } + IPropertySymbol property => property.Type, + IFieldSymbol field => field.Type, + _ => throw ExceptionUtilities.Unreachable(), + }; + } - return false; + static ImmutableArray GetCandidatePropertiesAndFields(Document document, SemanticModel semanticModel, int position, ITypeSymbol? type) + { + var members = semanticModel.LookupSymbols(position, type); + return members.WhereAsArray(m => m.CanBeReferencedByName && + IsFieldOrReadableProperty(m) && + !m.IsImplicitlyDeclared && + !m.IsStatic); + } + } + + private static bool IsFieldOrReadableProperty(ISymbol symbol) + { + if (symbol.IsKind(SymbolKind.Field)) + { + return true; + } + + if (symbol.IsKind(SymbolKind.Property) && !((IPropertySymbol)symbol).IsWriteOnly) + { + return true; } - internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + return false; + } + + internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); - private static readonly CompletionItemRules s_rules = CompletionItemRules.Create(enterKeyRule: EnterKeyRule.Never); + private static readonly CompletionItemRules s_rules = CompletionItemRules.Create(enterKeyRule: EnterKeyRule.Never); - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options) || text[characterPosition] == ' '; + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options) || text[characterPosition] == ' '; - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters.Add(' '); + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters.Add(' '); - private static (PropertyPatternClauseSyntax?, ExpressionSyntax?) TryGetPropertyPatternClause(SyntaxTree tree, int position, CancellationToken cancellationToken) + private static (PropertyPatternClauseSyntax?, ExpressionSyntax?) TryGetPropertyPatternClause(SyntaxTree tree, int position, CancellationToken cancellationToken) + { + if (tree.IsInNonUserCode(position, cancellationToken)) { - if (tree.IsInNonUserCode(position, cancellationToken)) - { - return default; - } + return default; + } - var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken); - token = token.GetPreviousTokenIfTouchingWord(position); + var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken); + token = token.GetPreviousTokenIfTouchingWord(position); - if (token.Kind() is SyntaxKind.CommaToken or SyntaxKind.OpenBraceToken) - { - return token.Parent is PropertyPatternClauseSyntax { Parent: PatternSyntax } propertyPatternClause - ? (propertyPatternClause, null) - : default; - } + if (token.Kind() is SyntaxKind.CommaToken or SyntaxKind.OpenBraceToken) + { + return token.Parent is PropertyPatternClauseSyntax { Parent: PatternSyntax } propertyPatternClause + ? (propertyPatternClause, null) + : default; + } + + if (token.IsKind(SyntaxKind.DotToken)) + { + // is { Property1.$$ } + // is { Property1.$$ Property1.Property2: ... } // typing before an existing pattern + return token.Parent is MemberAccessExpressionSyntax memberAccess && IsExtendedPropertyPattern(memberAccess, out var propertyPatternClause) + ? (propertyPatternClause, memberAccess.Expression) + : default; + } - if (token.IsKind(SyntaxKind.DotToken)) + return default; + + bool IsExtendedPropertyPattern(MemberAccessExpressionSyntax memberAccess, [NotNullWhen(true)] out PropertyPatternClauseSyntax? propertyPatternClause) + { + while (memberAccess.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression)) { - // is { Property1.$$ } - // is { Property1.$$ Property1.Property2: ... } // typing before an existing pattern - return token.Parent is MemberAccessExpressionSyntax memberAccess && IsExtendedPropertyPattern(memberAccess, out var propertyPatternClause) - ? (propertyPatternClause, memberAccess.Expression) - : default; + memberAccess = (MemberAccessExpressionSyntax)memberAccess.Parent; } - return default; - - bool IsExtendedPropertyPattern(MemberAccessExpressionSyntax memberAccess, [NotNullWhen(true)] out PropertyPatternClauseSyntax? propertyPatternClause) + if (memberAccess is { Parent.Parent: SubpatternSyntax { Parent: PropertyPatternClauseSyntax found } }) { - while (memberAccess.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression)) - { - memberAccess = (MemberAccessExpressionSyntax)memberAccess.Parent; - } - - if (memberAccess is { Parent.Parent: SubpatternSyntax { Parent: PropertyPatternClauseSyntax found } }) - { - propertyPatternClause = found; - return true; - } - - propertyPatternClause = null; - return false; + propertyPatternClause = found; + return true; } + + propertyPatternClause = null; + return false; } } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/DirectiveCompletionProviderUtilities.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/DirectiveCompletionProviderUtilities.cs index 540499fd9e773..80bdf49c9529d 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/DirectiveCompletionProviderUtilities.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/DirectiveCompletionProviderUtilities.cs @@ -5,29 +5,28 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +internal static class DirectiveCompletionProviderUtilities { - internal static class DirectiveCompletionProviderUtilities + internal static bool TryGetStringLiteralToken(SyntaxTree tree, int position, SyntaxKind directiveKind, out SyntaxToken stringLiteral, CancellationToken cancellationToken) { - internal static bool TryGetStringLiteralToken(SyntaxTree tree, int position, SyntaxKind directiveKind, out SyntaxToken stringLiteral, CancellationToken cancellationToken) + if (tree.IsEntirelyWithinStringLiteral(position, cancellationToken)) { - if (tree.IsEntirelyWithinStringLiteral(position, cancellationToken)) + var token = tree.GetRoot(cancellationToken).FindToken(position, findInsideTrivia: true); + if (token.Kind() is SyntaxKind.EndOfDirectiveToken or SyntaxKind.EndOfFileToken) { - var token = tree.GetRoot(cancellationToken).FindToken(position, findInsideTrivia: true); - if (token.Kind() is SyntaxKind.EndOfDirectiveToken or SyntaxKind.EndOfFileToken) - { - token = token.GetPreviousToken(includeSkipped: true, includeDirectives: true); - } - - if (token.Kind() == SyntaxKind.StringLiteralToken && token.Parent!.Kind() == directiveKind) - { - stringLiteral = token; - return true; - } + token = token.GetPreviousToken(includeSkipped: true, includeDirectives: true); } - stringLiteral = default; - return false; + if (token.Kind() == SyntaxKind.StringLiteralToken && token.Parent!.Kind() == directiveKind) + { + stringLiteral = token; + return true; + } } + + stringLiteral = default; + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/LoadDirectiveCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/LoadDirectiveCompletionProvider.cs index b84329b7276f0..beb00f7d5af3e 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/LoadDirectiveCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/LoadDirectiveCompletionProvider.cs @@ -10,22 +10,21 @@ using Microsoft.CodeAnalysis.CSharp.Completion.CompletionProviders.Snippets; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(LoadDirectiveCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(CSharpSnippetCompletionProvider))] +[Shared] +internal sealed class LoadDirectiveCompletionProvider : AbstractLoadDirectiveCompletionProvider { - [ExportCompletionProvider(nameof(LoadDirectiveCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(CSharpSnippetCompletionProvider))] - [Shared] - internal sealed class LoadDirectiveCompletionProvider : AbstractLoadDirectiveCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LoadDirectiveCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public LoadDirectiveCompletionProvider() - { - } + } - protected override string DirectiveName => "load"; + protected override string DirectiveName => "load"; - protected override bool TryGetStringLiteralToken(SyntaxTree tree, int position, out SyntaxToken stringLiteral, CancellationToken cancellationToken) - => DirectiveCompletionProviderUtilities.TryGetStringLiteralToken(tree, position, SyntaxKind.LoadDirectiveTrivia, out stringLiteral, cancellationToken); - } + protected override bool TryGetStringLiteralToken(SyntaxTree tree, int position, out SyntaxToken stringLiteral, CancellationToken cancellationToken) + => DirectiveCompletionProviderUtilities.TryGetStringLiteralToken(tree, position, SyntaxKind.LoadDirectiveTrivia, out stringLiteral, cancellationToken); } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/ReferenceDirectiveCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/ReferenceDirectiveCompletionProvider.cs index d7cb3382d84d6..73e52ce71957d 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/ReferenceDirectiveCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/Scripting/ReferenceDirectiveCompletionProvider.cs @@ -9,22 +9,21 @@ using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(ReferenceDirectiveCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(LoadDirectiveCompletionProvider))] +[Shared] +internal sealed class ReferenceDirectiveCompletionProvider : AbstractReferenceDirectiveCompletionProvider { - [ExportCompletionProvider(nameof(ReferenceDirectiveCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(LoadDirectiveCompletionProvider))] - [Shared] - internal sealed class ReferenceDirectiveCompletionProvider : AbstractReferenceDirectiveCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ReferenceDirectiveCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ReferenceDirectiveCompletionProvider() - { - } + } - protected override string DirectiveName => "r"; + protected override string DirectiveName => "r"; - protected override bool TryGetStringLiteralToken(SyntaxTree tree, int position, out SyntaxToken stringLiteral, CancellationToken cancellationToken) - => DirectiveCompletionProviderUtilities.TryGetStringLiteralToken(tree, position, SyntaxKind.ReferenceDirectiveTrivia, out stringLiteral, cancellationToken); - } + protected override bool TryGetStringLiteralToken(SyntaxTree tree, int position, out SyntaxToken stringLiteral, CancellationToken cancellationToken) + => DirectiveCompletionProviderUtilities.TryGetStringLiteralToken(tree, position, SyntaxKind.ReferenceDirectiveTrivia, out stringLiteral, cancellationToken); } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs index b7d183b7f8585..d4528663391b0 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs @@ -24,173 +24,172 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(SnippetCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(CrefCompletionProvider))] +[Shared] +internal sealed class SnippetCompletionProvider : LSPCompletionProvider { - [ExportCompletionProvider(nameof(SnippetCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(CrefCompletionProvider))] - [Shared] - internal sealed class SnippetCompletionProvider : LSPCompletionProvider + private static readonly HashSet s_snippetsWithReplacements = + [ + "class", + "cw", + "ctor", + "else", + "enum", + "for", + "forr", + "foreach", + "if", + "interface", + "lock", + "prop", + "propg", + "sim", + "struct", + "svm", + "while" + ]; + + internal override bool IsSnippetProvider => true; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public SnippetCompletionProvider() { - private static readonly HashSet s_snippetsWithReplacements = - [ - "class", - "cw", - "ctor", - "else", - "enum", - "for", - "forr", - "foreach", - "if", - "interface", - "lock", - "prop", - "propg", - "sim", - "struct", - "svm", - "while" - ]; - - internal override bool IsSnippetProvider => true; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public SnippetCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; - public override async Task ProvideCompletionsAsync(CompletionContext context) + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - try - { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; - using (Logger.LogBlock(FunctionId.Completion_SnippetCompletionProvider_GetItemsWorker_CSharp, cancellationToken)) + using (Logger.LogBlock(FunctionId.Completion_SnippetCompletionProvider_GetItemsWorker_CSharp, cancellationToken)) + { + // TODO (https://github.com/dotnet/roslyn/issues/5107): Enable in Interactive. + var solution = document.Project.Solution; + if (!solution.CanApplyChange(ApplyChangesKind.ChangeDocument) || + solution.WorkspaceKind is WorkspaceKind.Debugger or WorkspaceKind.Interactive) { - // TODO (https://github.com/dotnet/roslyn/issues/5107): Enable in Interactive. - var solution = document.Project.Solution; - if (!solution.CanApplyChange(ApplyChangesKind.ChangeDocument) || - solution.WorkspaceKind is WorkspaceKind.Debugger or WorkspaceKind.Interactive) - { - return; - } - - context.AddItems(await document.GetUnionItemsFromDocumentAndLinkedDocumentsAsync( - UnionCompletionItemComparer.Instance, - d => GetSnippetsForDocumentAsync(d, context, cancellationToken)).ConfigureAwait(false)); + return; } - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) - { - // nop + + context.AddItems(await document.GetUnionItemsFromDocumentAndLinkedDocumentsAsync( + UnionCompletionItemComparer.Instance, + d => GetSnippetsForDocumentAsync(d, context, cancellationToken)).ConfigureAwait(false)); } } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) + { + // nop + } + } - private static async Task> GetSnippetsForDocumentAsync( - Document document, CompletionContext completionContext, CancellationToken cancellationToken) + private static async Task> GetSnippetsForDocumentAsync( + Document document, CompletionContext completionContext, CancellationToken cancellationToken) + { + var position = completionContext.Position; + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + var semanticFacts = document.GetRequiredLanguageService(); + + var root = syntaxTree.GetRoot(cancellationToken); + var leftToken = root.FindTokenOnLeftOfPosition(position, includeDirectives: true); + var targetToken = leftToken.GetPreviousTokenIfTouchingWord(position); + + if (syntaxFacts.IsInNonUserCode(syntaxTree, position, cancellationToken) || + syntaxTree.IsRightOfDotOrArrowOrColonColon(position, targetToken, cancellationToken) || + syntaxFacts.GetContainingTypeDeclaration(root, position) is EnumDeclarationSyntax || + syntaxTree.IsPossibleTupleContext(leftToken, position)) { - var position = completionContext.Position; - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); - var semanticFacts = document.GetRequiredLanguageService(); - - var root = syntaxTree.GetRoot(cancellationToken); - var leftToken = root.FindTokenOnLeftOfPosition(position, includeDirectives: true); - var targetToken = leftToken.GetPreviousTokenIfTouchingWord(position); - - if (syntaxFacts.IsInNonUserCode(syntaxTree, position, cancellationToken) || - syntaxTree.IsRightOfDotOrArrowOrColonColon(position, targetToken, cancellationToken) || - syntaxFacts.GetContainingTypeDeclaration(root, position) is EnumDeclarationSyntax || - syntaxTree.IsPossibleTupleContext(leftToken, position)) - { - return []; - } + return []; + } - var context = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); - var semanticModel = context.SemanticModel; + var context = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); + var semanticModel = context.SemanticModel; - if (syntaxFacts.IsPreProcessorDirectiveContext(syntaxTree, position, cancellationToken)) + if (syntaxFacts.IsPreProcessorDirectiveContext(syntaxTree, position, cancellationToken)) + { + var directive = leftToken.GetAncestor(); + Contract.ThrowIfNull(directive); + + if (directive.DirectiveNameToken.Kind() is not ( + SyntaxKind.IfKeyword or + SyntaxKind.RegionKeyword or + SyntaxKind.ElseKeyword or + SyntaxKind.ElifKeyword or + SyntaxKind.ErrorKeyword or + SyntaxKind.LineKeyword or + SyntaxKind.PragmaKeyword or + SyntaxKind.EndIfKeyword or + SyntaxKind.UndefKeyword or + SyntaxKind.EndRegionKeyword or + SyntaxKind.WarningKeyword)) { - var directive = leftToken.GetAncestor(); - Contract.ThrowIfNull(directive); - - if (directive.DirectiveNameToken.Kind() is not ( - SyntaxKind.IfKeyword or - SyntaxKind.RegionKeyword or - SyntaxKind.ElseKeyword or - SyntaxKind.ElifKeyword or - SyntaxKind.ErrorKeyword or - SyntaxKind.LineKeyword or - SyntaxKind.PragmaKeyword or - SyntaxKind.EndIfKeyword or - SyntaxKind.UndefKeyword or - SyntaxKind.EndRegionKeyword or - SyntaxKind.WarningKeyword)) - { - return GetSnippetCompletionItems( - completionContext, document.Project.Solution.Services, semanticModel, isPreProcessorContext: true); - } + return GetSnippetCompletionItems( + completionContext, document.Project.Solution.Services, semanticModel, isPreProcessorContext: true); } - else + } + else + { + if (semanticFacts.IsGlobalStatementContext(semanticModel, position, cancellationToken) || + semanticFacts.IsExpressionContext(semanticModel, position, cancellationToken) || + semanticFacts.IsStatementContext(semanticModel, position, cancellationToken) || + semanticFacts.IsTypeContext(semanticModel, position, cancellationToken) || + semanticFacts.IsTypeDeclarationContext(semanticModel, position, cancellationToken) || + semanticFacts.IsNamespaceContext(semanticModel, position, cancellationToken) || + semanticFacts.IsNamespaceDeclarationNameContext(semanticModel, position, cancellationToken) || + semanticFacts.IsMemberDeclarationContext(semanticModel, position, cancellationToken) || + semanticFacts.IsLabelContext(semanticModel, position, cancellationToken)) { - if (semanticFacts.IsGlobalStatementContext(semanticModel, position, cancellationToken) || - semanticFacts.IsExpressionContext(semanticModel, position, cancellationToken) || - semanticFacts.IsStatementContext(semanticModel, position, cancellationToken) || - semanticFacts.IsTypeContext(semanticModel, position, cancellationToken) || - semanticFacts.IsTypeDeclarationContext(semanticModel, position, cancellationToken) || - semanticFacts.IsNamespaceContext(semanticModel, position, cancellationToken) || - semanticFacts.IsNamespaceDeclarationNameContext(semanticModel, position, cancellationToken) || - semanticFacts.IsMemberDeclarationContext(semanticModel, position, cancellationToken) || - semanticFacts.IsLabelContext(semanticModel, position, cancellationToken)) - { - return GetSnippetCompletionItems( - completionContext, document.Project.Solution.Services, semanticModel, isPreProcessorContext: false); - } + return GetSnippetCompletionItems( + completionContext, document.Project.Solution.Services, semanticModel, isPreProcessorContext: false); } - - return []; } - private static ImmutableArray GetSnippetCompletionItems( - CompletionContext context, SolutionServices services, SemanticModel semanticModel, bool isPreProcessorContext) - { - var service = services.GetLanguageServices(semanticModel.Language).GetService(); - if (service == null) - return []; + return []; + } - var snippets = service.GetSnippetsIfAvailable(); - if (context.CompletionOptions.ShouldShowNewSnippetExperience(context.Document)) - { - snippets = snippets.Where(snippet => !s_snippetsWithReplacements.Contains(snippet.Shortcut)); - } + private static ImmutableArray GetSnippetCompletionItems( + CompletionContext context, SolutionServices services, SemanticModel semanticModel, bool isPreProcessorContext) + { + var service = services.GetLanguageServices(semanticModel.Language).GetService(); + if (service == null) + return []; - if (isPreProcessorContext) - { - snippets = snippets.Where(snippet => snippet.Shortcut != null && snippet.Shortcut.StartsWith("#", StringComparison.Ordinal)); - } + var snippets = service.GetSnippetsIfAvailable(); + if (context.CompletionOptions.ShouldShowNewSnippetExperience(context.Document)) + { + snippets = snippets.Where(snippet => !s_snippetsWithReplacements.Contains(snippet.Shortcut)); + } - return snippets.SelectAsArray(snippet => - { - var rules = CompletionItemRules.Default.WithFormatOnCommit(service.ShouldFormatSnippet(snippet)); - - return CommonCompletionItem.Create( - displayText: isPreProcessorContext ? snippet.Shortcut[1..] : snippet.Shortcut, - displayTextSuffix: "", - sortText: isPreProcessorContext ? snippet.Shortcut[1..] : snippet.Shortcut, - description: (snippet.Title + Environment.NewLine + snippet.Description).ToSymbolDisplayParts(), - glyph: Glyph.Snippet, - rules: rules); - }); + if (isPreProcessorContext) + { + snippets = snippets.Where(snippet => snippet.Shortcut != null && snippet.Shortcut.StartsWith("#", StringComparison.Ordinal)); } + + return snippets.SelectAsArray(snippet => + { + var rules = CompletionItemRules.Default.WithFormatOnCommit(service.ShouldFormatSnippet(snippet)); + + return CommonCompletionItem.Create( + displayText: isPreProcessorContext ? snippet.Shortcut[1..] : snippet.Shortcut, + displayTextSuffix: "", + sortText: isPreProcessorContext ? snippet.Shortcut[1..] : snippet.Shortcut, + description: (snippet.Title + Environment.NewLine + snippet.Description).ToSymbolDisplayParts(), + glyph: Glyph.Snippet, + rules: rules); + }); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/Snippets/CSharpSnippetCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/Snippets/CSharpSnippetCompletionProvider.cs index cb1c3932f6707..9575132c7f441 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/Snippets/CSharpSnippetCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/Snippets/CSharpSnippetCompletionProvider.cs @@ -10,17 +10,16 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Snippets; -namespace Microsoft.CodeAnalysis.CSharp.Completion.CompletionProviders.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Completion.CompletionProviders.Snippets; + +[ExportCompletionProvider(nameof(CSharpSnippetCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(FunctionPointerUnmanagedCallingConventionCompletionProvider))] +[Shared] +internal class CSharpSnippetCompletionProvider : AbstractSnippetCompletionProvider { - [ExportCompletionProvider(nameof(CSharpSnippetCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(FunctionPointerUnmanagedCallingConventionCompletionProvider))] - [Shared] - internal class CSharpSnippetCompletionProvider : AbstractSnippetCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpSnippetCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpSnippetCompletionProvider() - { - } } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/SpeculativeTCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/SpeculativeTCompletionProvider.cs index 2b68abe85f9db..2c8e318fda40b 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/SpeculativeTCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/SpeculativeTCompletionProvider.cs @@ -19,150 +19,149 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(SpeculativeTCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(AwaitCompletionProvider))] +[Shared] +internal class SpeculativeTCompletionProvider : LSPCompletionProvider { - [ExportCompletionProvider(nameof(SpeculativeTCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(AwaitCompletionProvider))] - [Shared] - internal class SpeculativeTCompletionProvider : LSPCompletionProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public SpeculativeTCompletionProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public SpeculativeTCompletionProvider() - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; - public override async Task ProvideCompletionsAsync(CompletionContext context) + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - try - { - var document = context.Document; - var cancellationToken = context.CancellationToken; - - var showSpeculativeT = await document.IsValidContextForDocumentOrLinkedDocumentsAsync( - (doc, ct) => ShouldShowSpeculativeTCompletionItemAsync(doc, context, ct), - cancellationToken).ConfigureAwait(false); - - if (showSpeculativeT) - { - const string T = nameof(T); - context.AddItem(CommonCompletionItem.Create( - T, displayTextSuffix: "", CompletionItemRules.Default, glyph: Glyph.TypeParameter)); - } - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) + var document = context.Document; + var cancellationToken = context.CancellationToken; + + var showSpeculativeT = await document.IsValidContextForDocumentOrLinkedDocumentsAsync( + (doc, ct) => ShouldShowSpeculativeTCompletionItemAsync(doc, context, ct), + cancellationToken).ConfigureAwait(false); + + if (showSpeculativeT) { - // nop + const string T = nameof(T); + context.AddItem(CommonCompletionItem.Create( + T, displayTextSuffix: "", CompletionItemRules.Default, glyph: Glyph.TypeParameter)); } } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) + { + // nop + } + } - private static async Task ShouldShowSpeculativeTCompletionItemAsync(Document document, CompletionContext completionContext, CancellationToken cancellationToken) + private static async Task ShouldShowSpeculativeTCompletionItemAsync(Document document, CompletionContext completionContext, CancellationToken cancellationToken) + { + var position = completionContext.Position; + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (syntaxTree.IsInNonUserCode(position, cancellationToken) || + syntaxTree.IsPreProcessorDirectiveContext(position, cancellationToken)) { - var position = completionContext.Position; - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (syntaxTree.IsInNonUserCode(position, cancellationToken) || - syntaxTree.IsPreProcessorDirectiveContext(position, cancellationToken)) - { - return false; - } + return false; + } - // We could be in the middle of a ref/generic/tuple type, instead of a simple T case. - // If we managed to walk out and get a different SpanStart, we treat it as a simple $$T case. + // We could be in the middle of a ref/generic/tuple type, instead of a simple T case. + // If we managed to walk out and get a different SpanStart, we treat it as a simple $$T case. - var context = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); + var context = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); - if (context.IsTaskLikeTypeContext) - return false; + if (context.IsTaskLikeTypeContext) + return false; - var spanStart = position; - while (true) - { - var oldSpanStart = spanStart; + var spanStart = position; + while (true) + { + var oldSpanStart = spanStart; - spanStart = WalkOutOfGenericType(syntaxTree, spanStart, context.SemanticModel, cancellationToken); - spanStart = WalkOutOfTupleType(syntaxTree, spanStart, cancellationToken); - spanStart = WalkOutOfRefType(syntaxTree, spanStart, cancellationToken); + spanStart = WalkOutOfGenericType(syntaxTree, spanStart, context.SemanticModel, cancellationToken); + spanStart = WalkOutOfTupleType(syntaxTree, spanStart, cancellationToken); + spanStart = WalkOutOfRefType(syntaxTree, spanStart, cancellationToken); - if (spanStart == oldSpanStart) - { - break; - } + if (spanStart == oldSpanStart) + { + break; } - - return IsStartOfSpeculativeTContext(syntaxTree, spanStart, cancellationToken); } - private static bool IsStartOfSpeculativeTContext(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) - { - var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); + return IsStartOfSpeculativeTContext(syntaxTree, spanStart, cancellationToken); + } - return syntaxTree.IsMemberDeclarationContext(position, context: null, SyntaxKindSet.AllMemberModifiers, SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: true, cancellationToken) || - syntaxTree.IsStatementContext(position, token, cancellationToken) || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - syntaxTree.IsGlobalStatementContext(position, cancellationToken) || - syntaxTree.IsDelegateReturnTypeContext(position, token); - } + private static bool IsStartOfSpeculativeTContext(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + { + var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); + + return syntaxTree.IsMemberDeclarationContext(position, context: null, SyntaxKindSet.AllMemberModifiers, SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: true, cancellationToken) || + syntaxTree.IsStatementContext(position, token, cancellationToken) || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + syntaxTree.IsGlobalStatementContext(position, cancellationToken) || + syntaxTree.IsDelegateReturnTypeContext(position, token); + } - private static int WalkOutOfGenericType(SyntaxTree syntaxTree, int position, SemanticModel semanticModel, CancellationToken cancellationToken) + private static int WalkOutOfGenericType(SyntaxTree syntaxTree, int position, SemanticModel semanticModel, CancellationToken cancellationToken) + { + var spanStart = position; + var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); + + if (syntaxTree.IsGenericTypeArgumentContext(position, token, cancellationToken, semanticModel)) { - var spanStart = position; - var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); + if (syntaxTree.IsInPartiallyWrittenGeneric(spanStart, cancellationToken, out var nameToken)) + { + spanStart = nameToken.SpanStart; + } - if (syntaxTree.IsGenericTypeArgumentContext(position, token, cancellationToken, semanticModel)) + // If the user types Goo()?.SpanStart ?? spanStart; - } - - var tokenLeftOfGenericName = syntaxTree.FindTokenOnLeftOfPosition(spanStart, cancellationToken); - if (tokenLeftOfGenericName.IsKind(SyntaxKind.DotToken) && tokenLeftOfGenericName.Parent.IsKind(SyntaxKind.QualifiedName)) - { - spanStart = tokenLeftOfGenericName.Parent.SpanStart; - } + spanStart = token.GetAncestor()?.SpanStart ?? spanStart; } - return spanStart; + var tokenLeftOfGenericName = syntaxTree.FindTokenOnLeftOfPosition(spanStart, cancellationToken); + if (tokenLeftOfGenericName.IsKind(SyntaxKind.DotToken) && tokenLeftOfGenericName.Parent.IsKind(SyntaxKind.QualifiedName)) + { + spanStart = tokenLeftOfGenericName.Parent.SpanStart; + } } - private static int WalkOutOfRefType(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) - { - var prevToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken) - .GetPreviousTokenIfTouchingWord(position); + return spanStart; + } - if (prevToken.Kind() is SyntaxKind.RefKeyword or SyntaxKind.ReadOnlyKeyword && prevToken.Parent.IsKind(SyntaxKind.RefType)) - { - return prevToken.SpanStart; - } + private static int WalkOutOfRefType(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + { + var prevToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken) + .GetPreviousTokenIfTouchingWord(position); - return position; + if (prevToken.Kind() is SyntaxKind.RefKeyword or SyntaxKind.ReadOnlyKeyword && prevToken.Parent.IsKind(SyntaxKind.RefType)) + { + return prevToken.SpanStart; } - private static int WalkOutOfTupleType(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) - { - var prevToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken) - .GetPreviousTokenIfTouchingWord(position); + return position; + } - if (prevToken.IsPossibleTupleOpenParenOrComma()) - { - return prevToken.Parent!.SpanStart; - } + private static int WalkOutOfTupleType(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + { + var prevToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken) + .GetPreviousTokenIfTouchingWord(position); - return position; + if (prevToken.IsPossibleTupleOpenParenOrComma()) + { + return prevToken.Parent!.SpanStart; } + + return position; } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/SymbolCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/SymbolCompletionProvider.cs index ac50763000748..ac22c1d582b72 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/SymbolCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/SymbolCompletionProvider.cs @@ -19,265 +19,264 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(SymbolCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(SpeculativeTCompletionProvider))] +[Shared] +internal sealed class SymbolCompletionProvider : AbstractRecommendationServiceBasedCompletionProvider { - [ExportCompletionProvider(nameof(SymbolCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(SpeculativeTCompletionProvider))] - [Shared] - internal sealed class SymbolCompletionProvider : AbstractRecommendationServiceBasedCompletionProvider - { - private static readonly Dictionary<(bool importDirective, bool preselect, bool tupleLiteral), CompletionItemRules> s_cachedRules = []; + private static readonly Dictionary<(bool importDirective, bool preselect, bool tupleLiteral), CompletionItemRules> s_cachedRules = []; - static SymbolCompletionProvider() + static SymbolCompletionProvider() + { + for (var importDirective = 0; importDirective < 2; importDirective++) { - for (var importDirective = 0; importDirective < 2; importDirective++) + for (var preselect = 0; preselect < 2; preselect++) { - for (var preselect = 0; preselect < 2; preselect++) + for (var tupleLiteral = 0; tupleLiteral < 2; tupleLiteral++) { - for (var tupleLiteral = 0; tupleLiteral < 2; tupleLiteral++) - { - var context = (importDirective: importDirective == 1, preselect: preselect == 1, tupleLiteral: tupleLiteral == 1); - s_cachedRules[context] = MakeRule(context); - } + var context = (importDirective: importDirective == 1, preselect: preselect == 1, tupleLiteral: tupleLiteral == 1); + s_cachedRules[context] = MakeRule(context); } } + } - return; + return; - static CompletionItemRules MakeRule((bool importDirective, bool preselect, bool tupleLiteral) context) - { - // '<' should not filter the completion list, even though it's in generic items like IList<> - var generalBaseline = CompletionItemRules.Default. - WithFilterCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '<')); + static CompletionItemRules MakeRule((bool importDirective, bool preselect, bool tupleLiteral) context) + { + // '<' should not filter the completion list, even though it's in generic items like IList<> + var generalBaseline = CompletionItemRules.Default. + WithFilterCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '<')); - var importDirectiveBaseline = CompletionItemRules.Create(commitCharacterRules: - [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, '.', ';')]); + var importDirectiveBaseline = CompletionItemRules.Create(commitCharacterRules: + [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, '.', ';')]); - var rule = context.importDirective ? importDirectiveBaseline : generalBaseline; + var rule = context.importDirective ? importDirectiveBaseline : generalBaseline; - if (context.preselect) - rule = rule.WithSelectionBehavior(CompletionItemSelectionBehavior.HardSelection); + if (context.preselect) + rule = rule.WithSelectionBehavior(CompletionItemSelectionBehavior.HardSelection); - if (context.tupleLiteral) - rule = rule.WithCommitCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ':')); + if (context.tupleLiteral) + rule = rule.WithCommitCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ':')); - return rule; - } + return rule; } + } - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public SymbolCompletionProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public SymbolCompletionProvider() + { + } - public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharactersWithArgumentList; + public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharactersWithArgumentList; - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - protected override CompletionItemSelectionBehavior PreselectedItemSelectionBehavior => CompletionItemSelectionBehavior.HardSelection; + protected override CompletionItemSelectionBehavior PreselectedItemSelectionBehavior => CompletionItemSelectionBehavior.HardSelection; - protected override string GetFilterText(ISymbol symbol, string displayText, CSharpSyntaxContext context) - => GetFilterTextDefault(symbol, displayText, context); + protected override string GetFilterText(ISymbol symbol, string displayText, CSharpSyntaxContext context) + => GetFilterTextDefault(symbol, displayText, context); - protected override async Task ShouldPreselectInferredTypesAsync( - CompletionContext? context, - int position, - CompletionOptions options, - CancellationToken cancellationToken) - { - if (context is null) - return true; + protected override async Task ShouldPreselectInferredTypesAsync( + CompletionContext? context, + int position, + CompletionOptions options, + CancellationToken cancellationToken) + { + if (context is null) + return true; - // Avoid preselection & hard selection when triggered via insertion in an argument list. - // If an item is hard selected, then a user trying to type MethodCall() will get - // MethodCall(someVariable) instead. We need only soft selected items to prevent this. - return !await IsTriggeredInArgumentListAsync(context, position, options, cancellationToken).ConfigureAwait(false); - } + // Avoid preselection & hard selection when triggered via insertion in an argument list. + // If an item is hard selected, then a user trying to type MethodCall() will get + // MethodCall(someVariable) instead. We need only soft selected items to prevent this. + return !await IsTriggeredInArgumentListAsync(context, position, options, cancellationToken).ConfigureAwait(false); + } - protected override async Task ShouldProvideAvailableSymbolsInCurrentContextAsync( - CompletionContext? completionContext, - CSharpSyntaxContext context, - int position, - CompletionOptions options, - CancellationToken cancellationToken) - { - if (completionContext is null) - return true; + protected override async Task ShouldProvideAvailableSymbolsInCurrentContextAsync( + CompletionContext? completionContext, + CSharpSyntaxContext context, + int position, + CompletionOptions options, + CancellationToken cancellationToken) + { + if (completionContext is null) + return true; - // If we are triggered in argument list, provide symbols only when the invoked method accept any arguments. - if (await IsTriggeredInArgumentListAsync(completionContext, position, options, cancellationToken).ConfigureAwait(false) is false) - return true; + // If we are triggered in argument list, provide symbols only when the invoked method accept any arguments. + if (await IsTriggeredInArgumentListAsync(completionContext, position, options, cancellationToken).ConfigureAwait(false) is false) + return true; - return !context.InferredTypes.IsEmpty || IsTopNodeInPrimaryConstructorArgumentList(); + return !context.InferredTypes.IsEmpty || IsTopNodeInPrimaryConstructorArgumentList(); - // Special case for argument of base record primary constructor as a workaround - // for bug https://github.com/dotnet/roslyn/issues/70803 - bool IsTopNodeInPrimaryConstructorArgumentList() - => context.TargetToken.Parent?.Parent?.IsKind(SyntaxKind.PrimaryConstructorBaseType) is true; - } + // Special case for argument of base record primary constructor as a workaround + // for bug https://github.com/dotnet/roslyn/issues/70803 + bool IsTopNodeInPrimaryConstructorArgumentList() + => context.TargetToken.Parent?.Parent?.IsKind(SyntaxKind.PrimaryConstructorBaseType) is true; + } - private static async Task IsTriggeredInArgumentListAsync( - CompletionContext completionContext, - int position, - CompletionOptions options, - CancellationToken cancellationToken) + private static async Task IsTriggeredInArgumentListAsync( + CompletionContext completionContext, + int position, + CompletionOptions options, + CancellationToken cancellationToken) + { + if (options.TriggerInArgumentLists) { - if (options.TriggerInArgumentLists) + if (completionContext.Trigger.Kind == CompletionTriggerKind.Insertion && + position > 0 && + await IsTriggerInArgumentListAsync(completionContext.Document, position - 1, cancellationToken).ConfigureAwait(false) == true) { - if (completionContext.Trigger.Kind == CompletionTriggerKind.Insertion && - position > 0 && - await IsTriggerInArgumentListAsync(completionContext.Document, position - 1, cancellationToken).ConfigureAwait(false) == true) - { - return true; - } + return true; } - - return false; } - protected override bool IsInstrinsic(ISymbol s) - => s is ITypeSymbol ts && ts.IsIntrinsicType(); + return false; + } + + protected override bool IsInstrinsic(ISymbol s) + => s is ITypeSymbol ts && ts.IsIntrinsicType(); - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - { - return options.TriggerInArgumentLists - ? CompletionUtilities.IsTriggerCharacterOrArgumentListCharacter(text, characterPosition, options) - : CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); - } + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + { + return options.TriggerInArgumentLists + ? CompletionUtilities.IsTriggerCharacterOrArgumentListCharacter(text, characterPosition, options) + : CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + } - internal override async Task IsSyntacticTriggerCharacterAsync(Document document, int caretPosition, CompletionTrigger trigger, CompletionOptions options, CancellationToken cancellationToken) + internal override async Task IsSyntacticTriggerCharacterAsync(Document document, int caretPosition, CompletionTrigger trigger, CompletionOptions options, CancellationToken cancellationToken) + { + if (trigger.Kind == CompletionTriggerKind.Insertion && caretPosition > 0) { - if (trigger.Kind == CompletionTriggerKind.Insertion && caretPosition > 0) + var result = await IsTriggerOnDotAsync(document, caretPosition - 1, cancellationToken).ConfigureAwait(false); + if (result.HasValue) + return result.Value; + + if (options.TriggerInArgumentLists) { - var result = await IsTriggerOnDotAsync(document, caretPosition - 1, cancellationToken).ConfigureAwait(false); + result = await IsTriggerInArgumentListAsync(document, caretPosition - 1, cancellationToken).ConfigureAwait(false); if (result.HasValue) return result.Value; - - if (options.TriggerInArgumentLists) - { - result = await IsTriggerInArgumentListAsync(document, caretPosition - 1, cancellationToken).ConfigureAwait(false); - if (result.HasValue) - return result.Value; - } } - - // By default we want to proceed with triggering completion if we have items. - return true; } - protected override bool IsTriggerOnDot(SyntaxToken token, int characterPosition) - { - if (!CompletionUtilities.TreatAsDot(token, characterPosition)) - return false; + // By default we want to proceed with triggering completion if we have items. + return true; + } - // don't want to trigger after a number. All other cases after dot are ok. - return token.GetPreviousToken().Kind() != SyntaxKind.NumericLiteralToken; - } + protected override bool IsTriggerOnDot(SyntaxToken token, int characterPosition) + { + if (!CompletionUtilities.TreatAsDot(token, characterPosition)) + return false; - /// if not an argument list character, otherwise whether the trigger is in an argument list. - private static async Task IsTriggerInArgumentListAsync(Document document, int characterPosition, CancellationToken cancellationToken) + // don't want to trigger after a number. All other cases after dot are ok. + return token.GetPreviousToken().Kind() != SyntaxKind.NumericLiteralToken; + } + + /// if not an argument list character, otherwise whether the trigger is in an argument list. + private static async Task IsTriggerInArgumentListAsync(Document document, int characterPosition, CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (!CompletionUtilities.IsArgumentListCharacter(text[characterPosition])) { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (!CompletionUtilities.IsArgumentListCharacter(text[characterPosition])) - { - return null; - } + return null; + } - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(characterPosition); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(characterPosition); - if (token.Parent?.Kind() is not (SyntaxKind.ArgumentList or SyntaxKind.BracketedArgumentList or SyntaxKind.AttributeArgumentList or SyntaxKind.ArrayRankSpecifier)) - { - return false; - } + if (token.Parent?.Kind() is not (SyntaxKind.ArgumentList or SyntaxKind.BracketedArgumentList or SyntaxKind.AttributeArgumentList or SyntaxKind.ArrayRankSpecifier)) + { + return false; + } - // Be careful, e.g. if we're in a comment before the token - if (token.Span.End > characterPosition + 1) - { - return false; - } + // Be careful, e.g. if we're in a comment before the token + if (token.Span.End > characterPosition + 1) + { + return false; + } - // Only allow spaces between the end of the token and the trigger character - for (var i = token.Span.End; i < characterPosition; i++) + // Only allow spaces between the end of the token and the trigger character + for (var i = token.Span.End; i < characterPosition; i++) + { + if (text[i] != ' ') { - if (text[i] != ' ') - { - return false; - } + return false; } - - return true; } - protected override (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(ISymbol symbol, CSharpSyntaxContext context) - => CompletionUtilities.GetDisplayAndSuffixAndInsertionText(symbol, context); + return true; + } - protected override CompletionItemRules GetCompletionItemRules(ImmutableArray symbols, CSharpSyntaxContext context) - { - var preselect = symbols.Any(static t => t.Preselect); - s_cachedRules.TryGetValue(ValueTuple.Create(context.IsLeftSideOfImportAliasDirective, preselect, context.IsPossibleTupleContext), out var rule); + protected override (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(ISymbol symbol, CSharpSyntaxContext context) + => CompletionUtilities.GetDisplayAndSuffixAndInsertionText(symbol, context); - return rule ?? CompletionItemRules.Default; - } + protected override CompletionItemRules GetCompletionItemRules(ImmutableArray symbols, CSharpSyntaxContext context) + { + var preselect = symbols.Any(static t => t.Preselect); + s_cachedRules.TryGetValue(ValueTuple.Create(context.IsLeftSideOfImportAliasDirective, preselect, context.IsPossibleTupleContext), out var rule); + + return rule ?? CompletionItemRules.Default; + } - protected override CompletionItem CreateItem( - CompletionContext completionContext, - string displayText, - string displayTextSuffix, - string insertionText, - ImmutableArray symbols, - CSharpSyntaxContext context, - SupportedPlatformData? supportedPlatformData) + protected override CompletionItem CreateItem( + CompletionContext completionContext, + string displayText, + string displayTextSuffix, + string insertionText, + ImmutableArray symbols, + CSharpSyntaxContext context, + SupportedPlatformData? supportedPlatformData) + { + var item = base.CreateItem( + completionContext, + displayText, + displayTextSuffix, + insertionText, + symbols, + context, + supportedPlatformData); + + var symbol = symbols[0].Symbol; + // If it is a method symbol, also consider appending parenthesis when later, it is committed by using special characters. + // 2 cases are excluded. + // 1. If it is invoked under Nameof Context. + // For example: var a = nameof(Bar$$) + // In this case, if later committed by semicolon, we should have + // var a = nameof(Bar); + // 2. If the inferred type is delegate or function pointer. + // e.g. Action c = Bar$$ + // In this case, if later committed by semicolon, we should have + // e.g. Action c = = Bar; + if (symbol.IsKind(SymbolKind.Method) && !context.IsNameOfContext) { - var item = base.CreateItem( - completionContext, - displayText, - displayTextSuffix, - insertionText, - symbols, - context, - supportedPlatformData); - - var symbol = symbols[0].Symbol; - // If it is a method symbol, also consider appending parenthesis when later, it is committed by using special characters. - // 2 cases are excluded. - // 1. If it is invoked under Nameof Context. - // For example: var a = nameof(Bar$$) - // In this case, if later committed by semicolon, we should have - // var a = nameof(Bar); - // 2. If the inferred type is delegate or function pointer. - // e.g. Action c = Bar$$ - // In this case, if later committed by semicolon, we should have - // e.g. Action c = = Bar; - if (symbol.IsKind(SymbolKind.Method) && !context.IsNameOfContext) - { - var isInferredTypeDelegateOrFunctionPointer = context.InferredTypes.Any(static type => type.IsDelegateType() || type.IsFunctionPointerType()); - if (!isInferredTypeDelegateOrFunctionPointer) - { - item = SymbolCompletionItem.AddShouldProvideParenthesisCompletion(item); - } - } - else if (symbol.IsKind(SymbolKind.NamedType) || symbol is IAliasSymbol aliasSymbol && aliasSymbol.Target.IsType) + var isInferredTypeDelegateOrFunctionPointer = context.InferredTypes.Any(static type => type.IsDelegateType() || type.IsFunctionPointerType()); + if (!isInferredTypeDelegateOrFunctionPointer) { - // If this is a type symbol/alias symbol, also consider appending parenthesis when later, it is committed by using special characters, - // and the type is used as constructor - if (context.IsObjectCreationTypeContext) - item = SymbolCompletionItem.AddShouldProvideParenthesisCompletion(item); + item = SymbolCompletionItem.AddShouldProvideParenthesisCompletion(item); } - - return item; } - - protected override string GetInsertionText(CompletionItem item, char ch) + else if (symbol.IsKind(SymbolKind.NamedType) || symbol is IAliasSymbol aliasSymbol && aliasSymbol.Target.IsType) { - if (ch is ';' or '.' && SymbolCompletionItem.GetShouldProvideParenthesisCompletion(item)) - { - CompletionProvidersLogger.LogCustomizedCommitToAddParenthesis(ch); - return SymbolCompletionItem.GetInsertionText(item) + "()"; - } + // If this is a type symbol/alias symbol, also consider appending parenthesis when later, it is committed by using special characters, + // and the type is used as constructor + if (context.IsObjectCreationTypeContext) + item = SymbolCompletionItem.AddShouldProvideParenthesisCompletion(item); + } + + return item; + } - return base.GetInsertionText(item, ch); + protected override string GetInsertionText(CompletionItem item, char ch) + { + if (ch is ';' or '.' && SymbolCompletionItem.GetShouldProvideParenthesisCompletion(item)) + { + CompletionProvidersLogger.LogCustomizedCommitToAddParenthesis(ch); + return SymbolCompletionItem.GetInsertionText(item) + "()"; } + + return base.GetInsertionText(item, ch); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/TupleNameCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/TupleNameCompletionProvider.cs index 2965c28885fd9..22bc9cde7db4d 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/TupleNameCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/TupleNameCompletionProvider.cs @@ -20,106 +20,105 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(TupleNameCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(XmlDocCommentCompletionProvider))] +[Shared] +internal class TupleNameCompletionProvider : LSPCompletionProvider { - [ExportCompletionProvider(nameof(TupleNameCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(XmlDocCommentCompletionProvider))] - [Shared] - internal class TupleNameCompletionProvider : LSPCompletionProvider - { - private const string ColonString = ":"; + private const string ColonString = ":"; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public TupleNameCompletionProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public TupleNameCompletionProvider() + { + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override async Task ProvideCompletionsAsync(CompletionContext completionContext) + public override async Task ProvideCompletionsAsync(CompletionContext completionContext) + { + try { - try - { - var document = completionContext.Document; - var cancellationToken = completionContext.CancellationToken; + var document = completionContext.Document; + var cancellationToken = completionContext.CancellationToken; - var context = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false) as CSharpSyntaxContext; - Contract.ThrowIfNull(context); + var context = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false) as CSharpSyntaxContext; + Contract.ThrowIfNull(context); - var semanticModel = context.SemanticModel; + var semanticModel = context.SemanticModel; - var index = GetElementIndex(context); - if (index == null) - { - return; - } - - var typeInferrer = document.GetRequiredLanguageService(); - var inferredTypes = typeInferrer.InferTypes(semanticModel, context.TargetToken.Parent!.SpanStart, cancellationToken) - .Where(t => t.IsTupleType) - .Cast() - .ToImmutableArray(); - - AddItems(inferredTypes, index.Value, completionContext, context.TargetToken.Parent.SpanStart); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) + var index = GetElementIndex(context); + if (index == null) { - // nop + return; } - } - private static int? GetElementIndex(CSharpSyntaxContext context) + var typeInferrer = document.GetRequiredLanguageService(); + var inferredTypes = typeInferrer.InferTypes(semanticModel, context.TargetToken.Parent!.SpanStart, cancellationToken) + .Where(t => t.IsTupleType) + .Cast() + .ToImmutableArray(); + + AddItems(inferredTypes, index.Value, completionContext, context.TargetToken.Parent.SpanStart); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) { - var token = context.TargetToken; - if (token.IsKind(SyntaxKind.OpenParenToken)) - { - if (token.Parent is (kind: SyntaxKind.ParenthesizedExpression or SyntaxKind.TupleExpression or SyntaxKind.CastExpression)) - { - return 0; - } - } + // nop + } + } - if (token.IsKind(SyntaxKind.CommaToken) && token.Parent is TupleExpressionSyntax tupleExpr) + private static int? GetElementIndex(CSharpSyntaxContext context) + { + var token = context.TargetToken; + if (token.IsKind(SyntaxKind.OpenParenToken)) + { + if (token.Parent is (kind: SyntaxKind.ParenthesizedExpression or SyntaxKind.TupleExpression or SyntaxKind.CastExpression)) { - return (tupleExpr.Arguments.GetWithSeparators().IndexOf(context.TargetToken) + 1) / 2; + return 0; } + } - return null; + if (token.IsKind(SyntaxKind.CommaToken) && token.Parent is TupleExpressionSyntax tupleExpr) + { + return (tupleExpr.Arguments.GetWithSeparators().IndexOf(context.TargetToken) + 1) / 2; } - private static void AddItems(ImmutableArray inferredTypes, int index, CompletionContext context, int spanStart) + return null; + } + + private static void AddItems(ImmutableArray inferredTypes, int index, CompletionContext context, int spanStart) + { + foreach (var type in inferredTypes) { - foreach (var type in inferredTypes) + if (index >= type.TupleElements.Length) { - if (index >= type.TupleElements.Length) - { - return; - } - - // Note: the filter text does not include the ':'. We want to ensure that if - // the user types the name exactly (up to the colon) that it is selected as an - // exact match. - - var field = type.TupleElements[index]; - - context.AddItem(SymbolCompletionItem.CreateWithSymbolId( - displayText: field.Name, - displayTextSuffix: ColonString, - symbols: ImmutableArray.Create(field), - rules: CompletionItemRules.Default, - contextPosition: spanStart, - filterText: field.Name)); + return; } - } - protected override Task GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) - { - return Task.FromResult(new TextChange( - selectedItem.Span, - selectedItem.DisplayText)); + // Note: the filter text does not include the ':'. We want to ensure that if + // the user types the name exactly (up to the colon) that it is selected as an + // exact match. + + var field = type.TupleElements[index]; + + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( + displayText: field.Name, + displayTextSuffix: ColonString, + symbols: ImmutableArray.Create(field), + rules: CompletionItemRules.Default, + contextPosition: spanStart, + filterText: field.Name)); } + } - public override ImmutableHashSet TriggerCharacters => []; + protected override Task GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + { + return Task.FromResult(new TextChange( + selectedItem.Span, + selectedItem.DisplayText)); } + + public override ImmutableHashSet TriggerCharacters => []; } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/XmlDocCommentCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/XmlDocCommentCompletionProvider.cs index 7f1d03651c38b..0b9617d7fa77f 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/XmlDocCommentCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/XmlDocCommentCompletionProvider.cs @@ -22,372 +22,371 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers -{ - using static DocumentationCommentXmlNames; +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +using static DocumentationCommentXmlNames; - [ExportCompletionProvider(nameof(XmlDocCommentCompletionProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(PartialTypeCompletionProvider))] - [Shared] - internal partial class XmlDocCommentCompletionProvider : AbstractDocCommentCompletionProvider +[ExportCompletionProvider(nameof(XmlDocCommentCompletionProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(PartialTypeCompletionProvider))] +[Shared] +internal partial class XmlDocCommentCompletionProvider : AbstractDocCommentCompletionProvider +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public XmlDocCommentCompletionProvider() : base(s_defaultRules) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public XmlDocCommentCompletionProvider() : base(s_defaultRules) - { - } + } - internal override string Language => LanguageNames.CSharp; + internal override string Language => LanguageNames.CSharp; - public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) - => text[characterPosition] is ('<' or '"') || - CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, characterPosition, options); + public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options) + => text[characterPosition] is ('<' or '"') || + CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, characterPosition, options); - public override ImmutableHashSet TriggerCharacters { get; } = ['<', '"', ' ']; + public override ImmutableHashSet TriggerCharacters { get; } = ['<', '"', ' ']; - protected override async Task?> GetItemsWorkerAsync( - Document document, int position, - CompletionTrigger trigger, CancellationToken cancellationToken) + protected override async Task?> GetItemsWorkerAsync( + Document document, int position, + CompletionTrigger trigger, CancellationToken cancellationToken) + { + try { - try + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken); + var parentTrivia = token.GetAncestor(); + + if (parentTrivia == null) { - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken); - var parentTrivia = token.GetAncestor(); + return null; + } - if (parentTrivia == null) - { - return null; - } + var attachedToken = parentTrivia.ParentTrivia.Token; + if (attachedToken.Kind() == SyntaxKind.None) + { + return null; + } + + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(attachedToken.Parent, cancellationToken).ConfigureAwait(false); - var attachedToken = parentTrivia.ParentTrivia.Token; - if (attachedToken.Kind() == SyntaxKind.None) + ISymbol? declaredSymbol = null; + var memberDeclaration = attachedToken.GetAncestor(); + if (memberDeclaration != null) + { + declaredSymbol = semanticModel.GetDeclaredSymbol(memberDeclaration, cancellationToken); + } + else + { + var typeDeclaration = attachedToken.GetAncestor(); + if (typeDeclaration != null) { - return null; + declaredSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken); } + } - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(attachedToken.Parent, cancellationToken).ConfigureAwait(false); + if (IsAttributeNameContext(token, position, out var elementName, out var existingAttributes)) + { + return GetAttributeItems(elementName, existingAttributes); + } - ISymbol? declaredSymbol = null; - var memberDeclaration = attachedToken.GetAncestor(); - if (memberDeclaration != null) - { - declaredSymbol = semanticModel.GetDeclaredSymbol(memberDeclaration, cancellationToken); - } - else - { - var typeDeclaration = attachedToken.GetAncestor(); - if (typeDeclaration != null) - { - declaredSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken); - } - } + var wasTriggeredAfterSpace = trigger.Kind == CompletionTriggerKind.Insertion && trigger.Character == ' '; + if (wasTriggeredAfterSpace) + { + // Nothing below this point should triggered by a space character + // (only attribute names should be triggered by ) + return null; + } - if (IsAttributeNameContext(token, position, out var elementName, out var existingAttributes)) - { - return GetAttributeItems(elementName, existingAttributes); - } + if (IsAttributeValueContext(token, out elementName, out var attributeName)) + { + return GetAttributeValueItems(declaredSymbol, elementName, attributeName); + } - var wasTriggeredAfterSpace = trigger.Kind == CompletionTriggerKind.Insertion && trigger.Character == ' '; - if (wasTriggeredAfterSpace) - { - // Nothing below this point should triggered by a space character - // (only attribute names should be triggered by ) - return null; - } + if (trigger.Kind == CompletionTriggerKind.Insertion && trigger.Character != '<') + { + // With the use of IsTriggerAfterSpaceOrStartOfWordCharacter, the code below is much + // too aggressive at suggesting tags, so exit early before degrading the experience + return null; + } + + var items = new List(); - if (IsAttributeValueContext(token, out elementName, out var attributeName)) + if (token.Parent?.Kind() is SyntaxKind.XmlEmptyElement or SyntaxKind.XmlText || + (token.Parent.IsKind(SyntaxKind.XmlElementEndTag) && token.IsKind(SyntaxKind.GreaterThanToken)) || + (token.Parent.IsKind(SyntaxKind.XmlName) && token.Parent.IsParentKind(SyntaxKind.XmlEmptyElement))) + { + // The user is typing inside an XmlElement + if (token.Parent.IsParentKind(SyntaxKind.XmlElement) || + token.Parent.Parent.IsParentKind(SyntaxKind.XmlElement)) { - return GetAttributeValueItems(declaredSymbol, elementName, attributeName); + // Avoid including language keywords when following < or (); - - if (token.Parent?.Kind() is SyntaxKind.XmlEmptyElement or SyntaxKind.XmlText || - (token.Parent.IsKind(SyntaxKind.XmlElementEndTag) && token.IsKind(SyntaxKind.GreaterThanToken)) || - (token.Parent.IsKind(SyntaxKind.XmlName) && token.Parent.IsParentKind(SyntaxKind.XmlEmptyElement))) + if (token.Parent.IsParentKind(SyntaxKind.XmlEmptyElement) && + token.Parent.Parent!.Parent is XmlElementSyntax nestedXmlElement) { - // The user is typing inside an XmlElement - if (token.Parent.IsParentKind(SyntaxKind.XmlElement) || - token.Parent.Parent.IsParentKind(SyntaxKind.XmlElement)) - { - // Avoid including language keywords when following < or (); + AddXmlElementItems(items, startTag); } + + items.AddRange(GetAlwaysVisibleItems()); + return items; + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken, ErrorSeverity.General)) + { + return SpecializedCollections.EmptyEnumerable(); } + } - private void AddXmlElementItems(List items, XmlElementStartTagSyntax startTag) + private void AddXmlElementItems(List items, XmlElementStartTagSyntax startTag) + { + var xmlElementName = startTag.Name.LocalName.ValueText; + if (xmlElementName == ListElementName) { - var xmlElementName = startTag.Name.LocalName.ValueText; - if (xmlElementName == ListElementName) - { - items.AddRange(GetListItems()); - } - else if (xmlElementName == ListHeaderElementName) - { - items.AddRange(GetListHeaderItems()); - } - else if (xmlElementName == ItemElementName) - { - items.AddRange(GetItemTagItems()); - } + items.AddRange(GetListItems()); + } + else if (xmlElementName == ListHeaderElementName) + { + items.AddRange(GetListHeaderItems()); + } + else if (xmlElementName == ItemElementName) + { + items.AddRange(GetItemTagItems()); } + } + + private bool IsAttributeNameContext(SyntaxToken token, int position, [NotNullWhen(true)] out string? elementName, [NotNullWhen(true)] out ISet? attributeNames) + { + elementName = null; - private bool IsAttributeNameContext(SyntaxToken token, int position, [NotNullWhen(true)] out string? elementName, [NotNullWhen(true)] out ISet? attributeNames) + if (token.IsKind(SyntaxKind.XmlTextLiteralToken) && string.IsNullOrWhiteSpace(token.Text)) { - elementName = null; + // Unlike VB, the C# lexer has a preference for leading trivia. In the following example... + // + // /// ); - var attributes = default(SyntaxList); + if (token.IsKind(SyntaxKind.IdentifierToken) && token.Parent.IsKind(SyntaxKind.XmlName)) + { + // attributes) GetElementNameAndAttributes(SyntaxNode node) + { + XmlNameSyntax? nameSyntax; + SyntaxList attributes; - private static (string? name, SyntaxList attributes) GetElementNameAndAttributes(SyntaxNode node) + switch (node) { - XmlNameSyntax? nameSyntax; - SyntaxList attributes; + // Self contained empty element + case XmlEmptyElementSyntax emptyElementSyntax: + nameSyntax = emptyElementSyntax.Name; + attributes = emptyElementSyntax.Attributes; + break; + + // Parent node of a non-empty element: + case XmlElementSyntax elementSyntax: + // Defer to the start-tag logic + return GetElementNameAndAttributes(elementSyntax.StartTag); + + // Start tag of a non-empty element: + case XmlElementStartTagSyntax startTagSyntax: + nameSyntax = startTagSyntax.Name; + attributes = startTagSyntax.Attributes; + break; + + default: + nameSyntax = null; + attributes = default; + break; + } - switch (node) - { - // Self contained empty element - case XmlEmptyElementSyntax emptyElementSyntax: - nameSyntax = emptyElementSyntax.Name; - attributes = emptyElementSyntax.Attributes; - break; - - // Parent node of a non-empty element: - case XmlElementSyntax elementSyntax: - // Defer to the start-tag logic - return GetElementNameAndAttributes(elementSyntax.StartTag); - - // Start tag of a non-empty element: - case XmlElementStartTagSyntax startTagSyntax: - nameSyntax = startTagSyntax.Name; - attributes = startTagSyntax.Attributes; - break; - - default: - nameSyntax = null; - attributes = default; - break; - } + return (name: nameSyntax?.LocalName.ValueText, attributes); + } - return (name: nameSyntax?.LocalName.ValueText, attributes); + private static bool IsAttributeValueContext(SyntaxToken token, [NotNullWhen(true)] out string? tagName, [NotNullWhen(true)] out string? attributeName) + { + XmlAttributeSyntax? attributeSyntax; + if (token.Parent.IsKind(SyntaxKind.IdentifierName) && + token.Parent?.Parent is XmlNameAttributeSyntax xmlName) + { + // Handle the special 'name' attributes: name="bar$$ + attributeSyntax = xmlName; } - - private static bool IsAttributeValueContext(SyntaxToken token, [NotNullWhen(true)] out string? tagName, [NotNullWhen(true)] out string? attributeName) + else if (token.IsKind(SyntaxKind.XmlTextLiteralToken) && + token.Parent is XmlTextAttributeSyntax xmlText) { - XmlAttributeSyntax? attributeSyntax; - if (token.Parent.IsKind(SyntaxKind.IdentifierName) && - token.Parent?.Parent is XmlNameAttributeSyntax xmlName) - { - // Handle the special 'name' attributes: name="bar$$ - attributeSyntax = xmlName; - } - else if (token.IsKind(SyntaxKind.XmlTextLiteralToken) && - token.Parent is XmlTextAttributeSyntax xmlText) + // Handle the other general text attributes: foo="bar$$ + attributeSyntax = xmlText; + } + else if (token.Parent.IsKind(SyntaxKind.XmlNameAttribute, out attributeSyntax) || + token.Parent.IsKind(SyntaxKind.XmlTextAttribute, out attributeSyntax)) + { + // When there's no attribute value yet, the parent attribute is returned: + // name="$$ + // foo="$$ + if (token != attributeSyntax.StartQuoteToken) { - // Handle the other general text attributes: foo="bar$$ - attributeSyntax = xmlText; + attributeSyntax = null; } - else if (token.Parent.IsKind(SyntaxKind.XmlNameAttribute, out attributeSyntax) || - token.Parent.IsKind(SyntaxKind.XmlTextAttribute, out attributeSyntax)) + } + + if (attributeSyntax != null) + { + attributeName = attributeSyntax.Name.LocalName.ValueText; + + var emptyElement = attributeSyntax.GetAncestor(); + if (emptyElement != null) { - // When there's no attribute value yet, the parent attribute is returned: - // name="$$ - // foo="$$ - if (token != attributeSyntax.StartQuoteToken) - { - attributeSyntax = null; - } + // Empty element tags: + tagName = emptyElement.Name.LocalName.Text; + return true; } - if (attributeSyntax != null) + var startTagSyntax = token.GetAncestor(); + if (startTagSyntax != null) { - attributeName = attributeSyntax.Name.LocalName.ValueText; - - var emptyElement = attributeSyntax.GetAncestor(); - if (emptyElement != null) - { - // Empty element tags: - tagName = emptyElement.Name.LocalName.Text; - return true; - } - - var startTagSyntax = token.GetAncestor(); - if (startTagSyntax != null) - { - // Non-empty element start tags: - tagName = startTagSyntax.Name.LocalName.Text; - return true; - } + // Non-empty element start tags: + tagName = startTagSyntax.Name.LocalName.Text; + return true; } - - attributeName = null; - tagName = null; - return false; } - protected override IEnumerable GetKeywordNames() - { - yield return SyntaxFacts.GetText(SyntaxKind.NullKeyword); - yield return SyntaxFacts.GetText(SyntaxKind.StaticKeyword); - yield return SyntaxFacts.GetText(SyntaxKind.VirtualKeyword); - yield return SyntaxFacts.GetText(SyntaxKind.TrueKeyword); - yield return SyntaxFacts.GetText(SyntaxKind.FalseKeyword); - yield return SyntaxFacts.GetText(SyntaxKind.AbstractKeyword); - yield return SyntaxFacts.GetText(SyntaxKind.SealedKeyword); - yield return SyntaxFacts.GetText(SyntaxKind.AsyncKeyword); - yield return SyntaxFacts.GetText(SyntaxKind.AwaitKeyword); - } + attributeName = null; + tagName = null; + return false; + } - protected override IEnumerable GetExistingTopLevelElementNames(DocumentationCommentTriviaSyntax syntax) - => syntax.Content.Select(GetElementName).WhereNotNull(); + protected override IEnumerable GetKeywordNames() + { + yield return SyntaxFacts.GetText(SyntaxKind.NullKeyword); + yield return SyntaxFacts.GetText(SyntaxKind.StaticKeyword); + yield return SyntaxFacts.GetText(SyntaxKind.VirtualKeyword); + yield return SyntaxFacts.GetText(SyntaxKind.TrueKeyword); + yield return SyntaxFacts.GetText(SyntaxKind.FalseKeyword); + yield return SyntaxFacts.GetText(SyntaxKind.AbstractKeyword); + yield return SyntaxFacts.GetText(SyntaxKind.SealedKeyword); + yield return SyntaxFacts.GetText(SyntaxKind.AsyncKeyword); + yield return SyntaxFacts.GetText(SyntaxKind.AwaitKeyword); + } - protected override IEnumerable GetExistingTopLevelAttributeValues(DocumentationCommentTriviaSyntax syntax, string elementName, string attributeName) + protected override IEnumerable GetExistingTopLevelElementNames(DocumentationCommentTriviaSyntax syntax) + => syntax.Content.Select(GetElementName).WhereNotNull(); + + protected override IEnumerable GetExistingTopLevelAttributeValues(DocumentationCommentTriviaSyntax syntax, string elementName, string attributeName) + { + var attributeValues = SpecializedCollections.EmptyEnumerable(); + + foreach (var node in syntax.Content) { - var attributeValues = SpecializedCollections.EmptyEnumerable(); + (var name, var attributes) = GetElementNameAndAttributes(node); - foreach (var node in syntax.Content) + if (name == elementName) { - (var name, var attributes) = GetElementNameAndAttributes(node); - - if (name == elementName) - { - attributeValues = attributeValues.Concat( - attributes.Where(attribute => GetAttributeName(attribute) == attributeName) - .Select(GetAttributeValue)); - } + attributeValues = attributeValues.Concat( + attributes.Where(attribute => GetAttributeName(attribute) == attributeName) + .Select(GetAttributeValue)); } - - return attributeValues; } - private string? GetElementName(XmlNodeSyntax node) => GetElementNameAndAttributes(node).name; + return attributeValues; + } + + private string? GetElementName(XmlNodeSyntax node) => GetElementNameAndAttributes(node).name; - private string GetAttributeName(XmlAttributeSyntax attribute) => attribute.Name.LocalName.ValueText; + private string GetAttributeName(XmlAttributeSyntax attribute) => attribute.Name.LocalName.ValueText; - private string? GetAttributeValue(XmlAttributeSyntax attribute) + private string? GetAttributeValue(XmlAttributeSyntax attribute) + { + switch (attribute) { - switch (attribute) - { - case XmlTextAttributeSyntax textAttribute: - // Decode any XML enities and concatentate the results - return textAttribute.TextTokens.GetValueText(); + case XmlTextAttributeSyntax textAttribute: + // Decode any XML enities and concatentate the results + return textAttribute.TextTokens.GetValueText(); - case XmlNameAttributeSyntax nameAttribute: - return nameAttribute.Identifier.Identifier.ValueText; + case XmlNameAttributeSyntax nameAttribute: + return nameAttribute.Identifier.Identifier.ValueText; - default: - return null; - } + default: + return null; } + } - protected override ImmutableArray GetParameters(ISymbol declarationSymbol) + protected override ImmutableArray GetParameters(ISymbol declarationSymbol) + { + var declaredParameters = declarationSymbol.GetParameters(); + if (declarationSymbol is INamedTypeSymbol namedTypeSymbol) { - var declaredParameters = declarationSymbol.GetParameters(); - if (declarationSymbol is INamedTypeSymbol namedTypeSymbol) + if (namedTypeSymbol.TryGetPrimaryConstructor(out var primaryConstructor)) { - if (namedTypeSymbol.TryGetPrimaryConstructor(out var primaryConstructor)) - { - declaredParameters = primaryConstructor.Parameters; - } - else if (namedTypeSymbol is { DelegateInvokeMethod.Parameters: var delegateInvokeParameters }) - { - declaredParameters = delegateInvokeParameters; - } + declaredParameters = primaryConstructor.Parameters; + } + else if (namedTypeSymbol is { DelegateInvokeMethod.Parameters: var delegateInvokeParameters }) + { + declaredParameters = delegateInvokeParameters; } - - return declaredParameters; } - private static readonly CompletionItemRules s_defaultRules = - CompletionItemRules.Create( - filterCharacterRules: FilterRules, - commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Add, '>', '\t')], - enterKeyRule: EnterKeyRule.Never); + return declaredParameters; } + + private static readonly CompletionItemRules s_defaultRules = + CompletionItemRules.Create( + filterCharacterRules: FilterRules, + commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Add, '>', '\t')], + enterKeyRule: EnterKeyRule.Never); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractKeywordRecommender.cs index 98e2512f429e9..b5fa226cdc805 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractKeywordRecommender.cs @@ -7,68 +7,67 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class AbstractKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class AbstractKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.OverrideKeyword, - }; + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.OverrideKeyword, + }; - private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.OverrideKeyword, + }; + + private static readonly ISet s_validTypeModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - SyntaxKind.ExternKeyword, SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword, SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, - SyntaxKind.StaticKeyword, SyntaxKind.UnsafeKeyword, - SyntaxKind.OverrideKeyword, + SyntaxKind.FileKeyword, }; - private static readonly ISet s_validTypeModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.FileKeyword, - }; - - public AbstractKeywordRecommender() - : base(SyntaxKind.AbstractKeyword) - { - } + public AbstractKeywordRecommender() + : base(SyntaxKind.AbstractKeyword) + { + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsGlobalStatementContext || - context.IsMemberDeclarationContext( - validModifiers: s_validNonInterfaceMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: s_validInterfaceMemberModifiers, - validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken) || - context.IsTypeDeclarationContext( - validModifiers: s_validTypeModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsGlobalStatementContext || + context.IsMemberDeclarationContext( + validModifiers: s_validNonInterfaceMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: s_validInterfaceMemberModifiers, + validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken) || + context.IsTypeDeclarationContext( + validModifiers: s_validTypeModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractNativeIntegerKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractNativeIntegerKeywordRecommender.cs index 7c45b977eea88..f6d153f6f9630 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractNativeIntegerKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractNativeIntegerKeywordRecommender.cs @@ -7,35 +7,34 @@ using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal abstract class AbstractNativeIntegerKeywordRecommender : IKeywordRecommender { - internal abstract class AbstractNativeIntegerKeywordRecommender : IKeywordRecommender - { - protected abstract RecommendedKeyword Keyword { get; } + protected abstract RecommendedKeyword Keyword { get; } - private static bool IsValidContext(CSharpSyntaxContext context) + private static bool IsValidContext(CSharpSyntaxContext context) + { + if (context.IsTaskLikeTypeContext || + context.IsGenericConstraintContext || + context.IsAttributeNameContext) { - if (context.IsTaskLikeTypeContext || - context.IsGenericConstraintContext || - context.IsAttributeNameContext) - { - return false; - } - - if (context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsPossibleTupleContext || - context.IsAtStartOfPattern || - context.IsUsingAliasTypeContext || - (context.IsTypeContext && !context.IsEnumBaseListContext)) - { - return true; - } + return false; + } - return context.IsLocalVariableDeclarationContext; + if (context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsPossibleTupleContext || + context.IsAtStartOfPattern || + context.IsUsingAliasTypeContext || + (context.IsTypeContext && !context.IsEnumBaseListContext)) + { + return true; } - public ImmutableArray RecommendKeywords(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => IsValidContext(context) ? [Keyword] : []; + return context.IsLocalVariableDeclarationContext; } + + public ImmutableArray RecommendKeywords(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => IsValidContext(context) ? [Keyword] : []; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractSpecialTypePreselectingKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractSpecialTypePreselectingKeywordRecommender.cs index a1e4f86044e2f..fb2e7e54ca4a6 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractSpecialTypePreselectingKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractSpecialTypePreselectingKeywordRecommender.cs @@ -9,66 +9,65 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal abstract class AbstractSpecialTypePreselectingKeywordRecommender( + SyntaxKind keywordKind, + bool isValidInPreprocessorContext = false, + bool shouldFormatOnCommit = false) : AbstractSyntacticSingleKeywordRecommender(keywordKind, isValidInPreprocessorContext, shouldFormatOnCommit) { - internal abstract class AbstractSpecialTypePreselectingKeywordRecommender( - SyntaxKind keywordKind, - bool isValidInPreprocessorContext = false, - bool shouldFormatOnCommit = false) : AbstractSyntacticSingleKeywordRecommender(keywordKind, isValidInPreprocessorContext, shouldFormatOnCommit) - { - protected abstract SpecialType SpecialType { get; } - protected abstract bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken); + protected abstract SpecialType SpecialType { get; } + protected abstract bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken); - // When the keyword is the inferred type in this context, we should treat it like its corresponding type symbol - // in terms of MatchPripority, so the selection can be determined by how well it matches the filter text instead, - // e.g. selecting "string" over "String" when user typed "str". - protected override int PreselectMatchPriority => SymbolMatchPriority.PreferType; + // When the keyword is the inferred type in this context, we should treat it like its corresponding type symbol + // in terms of MatchPripority, so the selection can be determined by how well it matches the filter text instead, + // e.g. selecting "string" over "String" when user typed "str". + protected override int PreselectMatchPriority => SymbolMatchPriority.PreferType; - protected override bool ShouldPreselect(CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.InferredTypes.Any(static (t, self) => t.SpecialType == self.SpecialType, this); + protected override bool ShouldPreselect(CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.InferredTypes.Any(static (t, self) => t.SpecialType == self.SpecialType, this); - protected sealed override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - // Filter out all special-types from locations where we think we only want something task-like. - if (context.IsTaskLikeTypeContext) - return false; + protected sealed override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // Filter out all special-types from locations where we think we only want something task-like. + if (context.IsTaskLikeTypeContext) + return false; - return IsValidContextWorker(position, context, cancellationToken) || - IsAfterRefOrReadonlyInTopLevelOrMemberDeclaration(context, position, cancellationToken); - } + return IsValidContextWorker(position, context, cancellationToken) || + IsAfterRefOrReadonlyInTopLevelOrMemberDeclaration(context, position, cancellationToken); + } - private static bool IsAfterRefOrReadonlyInTopLevelOrMemberDeclaration(CSharpSyntaxContext context, int position, CancellationToken cancellationToken) + private static bool IsAfterRefOrReadonlyInTopLevelOrMemberDeclaration(CSharpSyntaxContext context, int position, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + if (!syntaxTree.IsAfterKeyword(position, SyntaxKind.RefKeyword, cancellationToken) && + !syntaxTree.IsAfterKeyword(position, SyntaxKind.ReadOnlyKeyword, cancellationToken)) { - var syntaxTree = context.SyntaxTree; - if (!syntaxTree.IsAfterKeyword(position, SyntaxKind.RefKeyword, cancellationToken) && - !syntaxTree.IsAfterKeyword(position, SyntaxKind.ReadOnlyKeyword, cancellationToken)) - { - return false; - } + return false; + } - var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); - token = token.GetPreviousTokenIfTouchingWord(position); + var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); + token = token.GetPreviousTokenIfTouchingWord(position); - // if we have `readonly` move backwards to see if we have `ref readonly`. - if (token.Kind() is SyntaxKind.ReadOnlyKeyword) - token = syntaxTree.FindTokenOnLeftOfPosition(token.SpanStart, cancellationToken); + // if we have `readonly` move backwards to see if we have `ref readonly`. + if (token.Kind() is SyntaxKind.ReadOnlyKeyword) + token = syntaxTree.FindTokenOnLeftOfPosition(token.SpanStart, cancellationToken); - // if we're not after `ref` or `ref readonly` then don't offer a type-keyword here. - if (token.Kind() != SyntaxKind.RefKeyword) - return false; + // if we're not after `ref` or `ref readonly` then don't offer a type-keyword here. + if (token.Kind() != SyntaxKind.RefKeyword) + return false; - // If we're inside a type, this is always to have a ref/readonly type name. - var containingType = token.GetAncestor(); - if (containingType != null) - return true; + // If we're inside a type, this is always to have a ref/readonly type name. + var containingType = token.GetAncestor(); + if (containingType != null) + return true; - // If not in a type, but in a namespace, this is not ok to have a ref/readonly type name. - var containingNamespace = token.GetAncestor(); - if (containingNamespace != null) - return false; + // If not in a type, but in a namespace, this is not ok to have a ref/readonly type name. + var containingNamespace = token.GetAncestor(); + if (containingNamespace != null) + return false; - // otherwise, we're at top level. Can have a ref/readonly top-level local/function. - return true; - } + // otherwise, we're at top level. Can have a ref/readonly top-level local/function. + return true; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractSyntacticSingleKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractSyntacticSingleKeywordRecommender.cs index 073990048e263..19a7dc31b564d 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractSyntacticSingleKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AbstractSyntacticSingleKeywordRecommender.cs @@ -8,78 +8,77 @@ using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal abstract partial class AbstractSyntacticSingleKeywordRecommender : IKeywordRecommender { - internal abstract partial class AbstractSyntacticSingleKeywordRecommender : IKeywordRecommender - { - public readonly SyntaxKind KeywordKind; - private readonly bool _isValidInPreprocessorContext; + public readonly SyntaxKind KeywordKind; + private readonly bool _isValidInPreprocessorContext; - private readonly ImmutableArray _keywordPriorityRecommendedKeywords; - private readonly ImmutableArray _defaultPriorityRecommendedKeywords; + private readonly ImmutableArray _keywordPriorityRecommendedKeywords; + private readonly ImmutableArray _defaultPriorityRecommendedKeywords; - /// - /// Matching priority for the provided item when returns . - /// - protected virtual int DefaultMatchPriority => MatchPriority.Default; + /// + /// Matching priority for the provided item when returns . + /// + protected virtual int DefaultMatchPriority => MatchPriority.Default; - protected virtual int PreselectMatchPriority => SymbolMatchPriority.Keyword; + protected virtual int PreselectMatchPriority => SymbolMatchPriority.Keyword; - protected AbstractSyntacticSingleKeywordRecommender( - SyntaxKind keywordKind, - bool isValidInPreprocessorContext = false, - bool shouldFormatOnCommit = false) - { - KeywordKind = keywordKind; - _isValidInPreprocessorContext = isValidInPreprocessorContext; - - _keywordPriorityRecommendedKeywords = [new RecommendedKeyword(SyntaxFacts.GetText(keywordKind), - shouldFormatOnCommit: shouldFormatOnCommit, - matchPriority: PreselectMatchPriority)]; - _defaultPriorityRecommendedKeywords = [new RecommendedKeyword(SyntaxFacts.GetText(keywordKind), - shouldFormatOnCommit: shouldFormatOnCommit, - matchPriority: DefaultMatchPriority)]; - } + protected AbstractSyntacticSingleKeywordRecommender( + SyntaxKind keywordKind, + bool isValidInPreprocessorContext = false, + bool shouldFormatOnCommit = false) + { + KeywordKind = keywordKind; + _isValidInPreprocessorContext = isValidInPreprocessorContext; + + _keywordPriorityRecommendedKeywords = [new RecommendedKeyword(SyntaxFacts.GetText(keywordKind), + shouldFormatOnCommit: shouldFormatOnCommit, + matchPriority: PreselectMatchPriority)]; + _defaultPriorityRecommendedKeywords = [new RecommendedKeyword(SyntaxFacts.GetText(keywordKind), + shouldFormatOnCommit: shouldFormatOnCommit, + matchPriority: DefaultMatchPriority)]; + } - protected abstract bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken); + protected abstract bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken); - public ImmutableArray RecommendKeywords( - int position, - CSharpSyntaxContext context, - CancellationToken cancellationToken) - { - var syntaxKind = RecommendKeyword(position, context, cancellationToken); - if (!syntaxKind.HasValue) - return []; + public ImmutableArray RecommendKeywords( + int position, + CSharpSyntaxContext context, + CancellationToken cancellationToken) + { + var syntaxKind = RecommendKeyword(position, context, cancellationToken); + if (!syntaxKind.HasValue) + return []; - return ShouldPreselect(context, cancellationToken) - ? _keywordPriorityRecommendedKeywords - : _defaultPriorityRecommendedKeywords; - } + return ShouldPreselect(context, cancellationToken) + ? _keywordPriorityRecommendedKeywords + : _defaultPriorityRecommendedKeywords; + } - protected virtual bool ShouldPreselect(CSharpSyntaxContext context, CancellationToken cancellationToken) => false; + protected virtual bool ShouldPreselect(CSharpSyntaxContext context, CancellationToken cancellationToken) => false; - private SyntaxKind? RecommendKeyword(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + private SyntaxKind? RecommendKeyword(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // NOTE: The collector ensures that we're not in "NonUserCode" like comments, strings, inactive code + // for perf reasons. + if (!_isValidInPreprocessorContext && + context.IsPreProcessorDirectiveContext) { - // NOTE: The collector ensures that we're not in "NonUserCode" like comments, strings, inactive code - // for perf reasons. - if (!_isValidInPreprocessorContext && - context.IsPreProcessorDirectiveContext) - { - return null; - } - - return IsValidContext(position, context, cancellationToken) ? KeywordKind : null; + return null; } - internal TestAccessor GetTestAccessor() => new(this); + return IsValidContext(position, context, cancellationToken) ? KeywordKind : null; + } + + internal TestAccessor GetTestAccessor() => new(this); - internal readonly struct TestAccessor(AbstractSyntacticSingleKeywordRecommender recommender) - { - private readonly AbstractSyntacticSingleKeywordRecommender _recommender = recommender; + internal readonly struct TestAccessor(AbstractSyntacticSingleKeywordRecommender recommender) + { + private readonly AbstractSyntacticSingleKeywordRecommender _recommender = recommender; - public ImmutableArray RecommendKeywords(int position, CSharpSyntaxContext context) - => _recommender.RecommendKeywords(position, context, CancellationToken.None); - } + public ImmutableArray RecommendKeywords(int position, CSharpSyntaxContext context) + => _recommender.RecommendKeywords(position, context, CancellationToken.None); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AddKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AddKeywordRecommender.cs index 30d290dc7fb36..1435aa60d1a65 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AddKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AddKeywordRecommender.cs @@ -6,16 +6,15 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class AddKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class AddKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public AddKeywordRecommender() + : base(SyntaxKind.AddKeyword) { - public AddKeywordRecommender() - : base(SyntaxKind.AddKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.AddKeyword); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.AddKeyword); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AliasKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AliasKeywordRecommender.cs index c591fbb72a45f..f3f6d2c5f56ea 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AliasKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AliasKeywordRecommender.cs @@ -7,30 +7,29 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class AliasKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class AliasKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public AliasKeywordRecommender() + : base(SyntaxKind.AliasKeyword) { - public AliasKeywordRecommender() - : base(SyntaxKind.AliasKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - // cases: - // extern | - // extern a| - var token = context.TargetToken; + } - if (token.Kind() == SyntaxKind.ExternKeyword) - { - // members can be 'extern' but we don't want - // 'alias' to show up in a 'type'. - return token.GetAncestor() == null; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // cases: + // extern | + // extern a| + var token = context.TargetToken; - return false; + if (token.Kind() == SyntaxKind.ExternKeyword) + { + // members can be 'extern' but we don't want + // 'alias' to show up in a 'type'. + return token.GetAncestor() == null; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AndKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AndKeywordRecommender.cs index 56d97ac008e8b..b38e4af55dd06 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AndKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AndKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class AndKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class AndKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public AndKeywordRecommender() + : base(SyntaxKind.AndKeyword) { - public AndKeywordRecommender() - : base(SyntaxKind.AndKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsAtEndOfPattern; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsAtEndOfPattern; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AnnotationsKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AnnotationsKeywordRecommender.cs index 73c1c14db2608..3137f750a709c 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AnnotationsKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AnnotationsKeywordRecommender.cs @@ -5,27 +5,26 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class AnnotationsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class AnnotationsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public AnnotationsKeywordRecommender() + : base(SyntaxKind.AnnotationsKeyword, isValidInPreprocessorContext: true) { - public AnnotationsKeywordRecommender() - : base(SyntaxKind.AnnotationsKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var previousToken1 = context.TargetToken; - var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); - var previousToken3 = previousToken2.GetPreviousToken(includeSkipped: true); + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var previousToken1 = context.TargetToken; + var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); + var previousToken3 = previousToken2.GetPreviousToken(includeSkipped: true); - // # nullable enable | - // # nullable enable a| - return - (previousToken1.Kind() == SyntaxKind.EnableKeyword || previousToken1.Kind() == SyntaxKind.DisableKeyword || previousToken1.Kind() == SyntaxKind.RestoreKeyword) && - previousToken2.Kind() == SyntaxKind.NullableKeyword && - previousToken3.Kind() == SyntaxKind.HashToken; - } + // # nullable enable | + // # nullable enable a| + return + (previousToken1.Kind() == SyntaxKind.EnableKeyword || previousToken1.Kind() == SyntaxKind.DisableKeyword || previousToken1.Kind() == SyntaxKind.RestoreKeyword) && + previousToken2.Kind() == SyntaxKind.NullableKeyword && + previousToken3.Kind() == SyntaxKind.HashToken; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsKeywordRecommender.cs index 0d8a3ce789a9a..4bd98f826ca8d 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class AsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class AsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public AsKeywordRecommender() + : base(SyntaxKind.AsKeyword) { - public AsKeywordRecommender() - : base(SyntaxKind.AsKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => !context.IsInNonUserCode && context.IsIsOrAsOrSwitchOrWithExpressionContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => !context.IsInNonUserCode && context.IsIsOrAsOrSwitchOrWithExpressionContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AscendingKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AscendingKeywordRecommender.cs index df4b7ae0f3e99..7676ae1f91bad 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AscendingKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AscendingKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class AscendingKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class AscendingKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public AscendingKeywordRecommender() + : base(SyntaxKind.AscendingKeyword) { - public AscendingKeywordRecommender() - : base(SyntaxKind.AscendingKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.TargetToken.IsOrderByDirectionContext(); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.TargetToken.IsOrderByDirectionContext(); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AssemblyKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AssemblyKeywordRecommender.cs index 0852eedeceda6..511ecb03a91c7 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AssemblyKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AssemblyKeywordRecommender.cs @@ -7,42 +7,41 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class AssemblyKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class AssemblyKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public AssemblyKeywordRecommender() + : base(SyntaxKind.AssemblyKeyword) { - public AssemblyKeywordRecommender() - : base(SyntaxKind.AssemblyKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var token = context.TargetToken; + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var token = context.TargetToken; - if (token.Kind() == SyntaxKind.OpenBracketToken && - token.GetRequiredParent().Kind() == SyntaxKind.AttributeList) + if (token.Kind() == SyntaxKind.OpenBracketToken && + token.GetRequiredParent().Kind() == SyntaxKind.AttributeList) + { + var attributeList = token.GetRequiredParent(); + var parentSyntax = attributeList.Parent; + switch (parentSyntax) { - var attributeList = token.GetRequiredParent(); - var parentSyntax = attributeList.Parent; - switch (parentSyntax) - { - case CompilationUnitSyntax: - case BaseNamespaceDeclarationSyntax: - // The case where the parent of attributeList is (Class/Interface/Enum/Struct)DeclarationSyntax, like: - // [$$ - // class Goo { - // for these cases is necessary check if they Parent is CompilationUnitSyntax - case BaseTypeDeclarationSyntax baseType when baseType.Parent is CompilationUnitSyntax: - // The case where the parent of attributeList is IncompleteMemberSyntax(See test: ), like: - // [$$ - // for that case is necessary check if they Parent is CompilationUnitSyntax - case IncompleteMemberSyntax incompleteMember when incompleteMember.Parent is CompilationUnitSyntax: - return true; - } + case CompilationUnitSyntax: + case BaseNamespaceDeclarationSyntax: + // The case where the parent of attributeList is (Class/Interface/Enum/Struct)DeclarationSyntax, like: + // [$$ + // class Goo { + // for these cases is necessary check if they Parent is CompilationUnitSyntax + case BaseTypeDeclarationSyntax baseType when baseType.Parent is CompilationUnitSyntax: + // The case where the parent of attributeList is IncompleteMemberSyntax(See test: ), like: + // [$$ + // for that case is necessary check if they Parent is CompilationUnitSyntax + case IncompleteMemberSyntax incompleteMember when incompleteMember.Parent is CompilationUnitSyntax: + return true; } - - return false; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs index 7a0d042b8d61f..938e52df3c476 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs @@ -8,43 +8,42 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class AsyncKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class AsyncKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public AsyncKeywordRecommender() + : base(SyntaxKind.AsyncKeyword, isValidInPreprocessorContext: false) { - public AsyncKeywordRecommender() - : base(SyntaxKind.AsyncKeyword, isValidInPreprocessorContext: false) - { - } + } - private static readonly ISet s_validLocalFunctionModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword - }; + private static readonly ISet s_validLocalFunctionModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.TargetToken.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword) || + context.PrecedingModifiers.Contains(SyntaxKind.AsyncKeyword)) { - if (context.TargetToken.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword) || - context.PrecedingModifiers.Contains(SyntaxKind.AsyncKeyword)) - { - return false; - } - - return InMemberDeclarationContext(position, context, cancellationToken) - || context.SyntaxTree.IsLambdaDeclarationContext(position, otherModifier: SyntaxKind.StaticKeyword, cancellationToken) - || context.SyntaxTree.IsLocalFunctionDeclarationContext(position, s_validLocalFunctionModifiers, cancellationToken); + return false; } - private static bool InMemberDeclarationContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return context.IsGlobalStatementContext - || context.SyntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) - || context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: true, - cancellationToken: cancellationToken); - } + return InMemberDeclarationContext(position, context, cancellationToken) + || context.SyntaxTree.IsLambdaDeclarationContext(position, otherModifier: SyntaxKind.StaticKeyword, cancellationToken) + || context.SyntaxTree.IsLocalFunctionDeclarationContext(position, s_validLocalFunctionModifiers, cancellationToken); + } + + private static bool InMemberDeclarationContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return context.IsGlobalStatementContext + || context.SyntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) + || context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: true, + cancellationToken: cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BaseKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BaseKeywordRecommender.cs index 8f04ff52b2afc..1395a08503574 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BaseKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BaseKeywordRecommender.cs @@ -8,61 +8,60 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class BaseKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class BaseKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public BaseKeywordRecommender() + : base(SyntaxKind.BaseKeyword) { - public BaseKeywordRecommender() - : base(SyntaxKind.BaseKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // We need to at least be in a type declaration context. This prevents us from showing + // calls to 'base' in things like top level repl statements and whatnot. + if (context.ContainingTypeDeclaration != null) { - // We need to at least be in a type declaration context. This prevents us from showing - // calls to 'base' in things like top level repl statements and whatnot. - if (context.ContainingTypeDeclaration != null) - { - return - IsConstructorInitializerContext(context) || - IsInstanceExpressionOrStatement(context); - } - - return false; + return + IsConstructorInitializerContext(context) || + IsInstanceExpressionOrStatement(context); } - private static bool IsInstanceExpressionOrStatement(CSharpSyntaxContext context) - { - if (context.IsInstanceContext) - { - return context.IsNonAttributeExpressionContext || context.IsStatementContext; - } + return false; + } - return false; + private static bool IsInstanceExpressionOrStatement(CSharpSyntaxContext context) + { + if (context.IsInstanceContext) + { + return context.IsNonAttributeExpressionContext || context.IsStatementContext; } - private static bool IsConstructorInitializerContext(CSharpSyntaxContext context) - { - // cases: - // Goo() : | + return false; + } - var token = context.TargetToken; + private static bool IsConstructorInitializerContext(CSharpSyntaxContext context) + { + // cases: + // Goo() : | - if (token.Kind() == SyntaxKind.ColonToken && - token.Parent is ConstructorInitializerSyntax && - token.Parent.IsParentKind(SyntaxKind.ConstructorDeclaration) && - token.Parent.Parent?.Parent is (kind: SyntaxKind.ClassDeclaration or SyntaxKind.RecordDeclaration)) - { - var constructor = token.GetRequiredAncestor(); - if (constructor.Modifiers.Any(SyntaxKind.StaticKeyword)) - { - return false; - } + var token = context.TargetToken; - return true; + if (token.Kind() == SyntaxKind.ColonToken && + token.Parent is ConstructorInitializerSyntax && + token.Parent.IsParentKind(SyntaxKind.ConstructorDeclaration) && + token.Parent.Parent?.Parent is (kind: SyntaxKind.ClassDeclaration or SyntaxKind.RecordDeclaration)) + { + var constructor = token.GetRequiredAncestor(); + if (constructor.Modifiers.Any(SyntaxKind.StaticKeyword)) + { + return false; } - return false; + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BoolKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BoolKeywordRecommender.cs index 3e64c7d69a4a2..d8a0e47124241 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BoolKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BoolKeywordRecommender.cs @@ -9,48 +9,47 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class BoolKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class BoolKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public BoolKeywordRecommender() + : base(SyntaxKind.BoolKeyword) { - public BoolKeywordRecommender() - : base(SyntaxKind.BoolKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_Boolean; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_Boolean; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BreakKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BreakKeywordRecommender.cs index fa1a081d14c35..0b2408309c513 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BreakKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/BreakKeywordRecommender.cs @@ -8,48 +8,47 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class BreakKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class BreakKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public BreakKeywordRecommender() + : base(SyntaxKind.BreakKeyword) { - public BreakKeywordRecommender() - : base(SyntaxKind.BreakKeyword) - { - } + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + IsInBreakableConstructContext(context) || + context.TargetToken.IsAfterYieldKeyword(); + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + private static bool IsInBreakableConstructContext(CSharpSyntaxContext context) + { + if (!context.IsStatementContext) { - return - IsInBreakableConstructContext(context) || - context.TargetToken.IsAfterYieldKeyword(); + return false; } - private static bool IsInBreakableConstructContext(CSharpSyntaxContext context) + // allowed if we're inside a loop/switch construct. + + var token = context.LeftToken; + foreach (var v in token.GetAncestors()) { - if (!context.IsStatementContext) + if (v is AnonymousFunctionExpressionSyntax) { + // if we hit a lambda while walking up, then we can't + // 'continue' any outer loops. return false; } - // allowed if we're inside a loop/switch construct. - - var token = context.LeftToken; - foreach (var v in token.GetAncestors()) + if (v.IsBreakableConstruct()) { - if (v is AnonymousFunctionExpressionSyntax) - { - // if we hit a lambda while walking up, then we can't - // 'continue' any outer loops. - return false; - } - - if (v.IsBreakableConstruct()) - { - return true; - } + return true; } - - return false; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ByKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ByKeywordRecommender.cs index f7e13fc47b1f8..e0a41695d0bb2 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ByKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ByKeywordRecommender.cs @@ -8,47 +8,46 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ByKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ByKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ByKeywordRecommender() + : base(SyntaxKind.ByKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) { - public ByKeywordRecommender() - : base(SyntaxKind.ByKeyword) + // cases: + // group e | + // group e b| + + var token = context.LeftToken; + var group = token.GetAncestor(); + + if (group == null) { + return false; } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + var lastToken = group.GroupExpression.GetLastToken(includeSkipped: true); + + // group e | + if (!token.IntersectsWith(position) && + token == lastToken) { - // cases: - // group e | - // group e b| - - var token = context.LeftToken; - var group = token.GetAncestor(); - - if (group == null) - { - return false; - } - - var lastToken = group.GroupExpression.GetLastToken(includeSkipped: true); - - // group e | - if (!token.IntersectsWith(position) && - token == lastToken) - { - return true; - } - - // group e b| - if (token.IntersectsWith(position) && - token.Kind() == SyntaxKind.IdentifierToken && - token.GetPreviousToken(includeSkipped: true) == lastToken) - { - return true; - } + return true; + } - return false; + // group e b| + if (token.IntersectsWith(position) && + token.Kind() == SyntaxKind.IdentifierToken && + token.GetPreviousToken(includeSkipped: true) == lastToken) + { + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ByteKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ByteKeywordRecommender.cs index b671089b40a73..c9b5ebfd4d1aa 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ByteKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ByteKeywordRecommender.cs @@ -9,49 +9,48 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ByteKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class ByteKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public ByteKeywordRecommender() + : base(SyntaxKind.ByteKeyword) { - public ByteKeywordRecommender() - : base(SyntaxKind.ByteKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsEnumBaseListContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_Byte; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsEnumBaseListContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_Byte; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CaseKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CaseKeywordRecommender.cs index eeb434e47f27e..d45fa895ef8ac 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CaseKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CaseKeywordRecommender.cs @@ -7,35 +7,34 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class CaseKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class CaseKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public CaseKeywordRecommender() + : base(SyntaxKind.CaseKeyword) { - public CaseKeywordRecommender() - : base(SyntaxKind.CaseKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.TargetToken.IsSwitchLabelContext() || - IsAfterGotoInSwitchContext(context); - } + } - internal static bool IsAfterGotoInSwitchContext(CSharpSyntaxContext context) - { - var token = context.TargetToken; + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.TargetToken.IsSwitchLabelContext() || + IsAfterGotoInSwitchContext(context); + } - if (token.Kind() == SyntaxKind.GotoKeyword && - token.GetAncestor() != null) - { - // todo: what if we're in a lambda... or a try/finally or - // something? Might want to filter this out. - return true; - } + internal static bool IsAfterGotoInSwitchContext(CSharpSyntaxContext context) + { + var token = context.TargetToken; - return false; + if (token.Kind() == SyntaxKind.GotoKeyword && + token.GetAncestor() != null) + { + // todo: what if we're in a lambda... or a try/finally or + // something? Might want to filter this out. + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CatchKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CatchKeywordRecommender.cs index 312195f786e15..3662ee41ebed9 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CatchKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CatchKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class CatchKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class CatchKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public CatchKeywordRecommender() + : base(SyntaxKind.CatchKeyword) { - public CatchKeywordRecommender() - : base(SyntaxKind.CatchKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.SyntaxTree.IsCatchOrFinallyContext(position, context.LeftToken); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.SyntaxTree.IsCatchOrFinallyContext(position, context.LeftToken); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CharKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CharKeywordRecommender.cs index 4fe0233b438a4..7f1da322b9aa7 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CharKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CharKeywordRecommender.cs @@ -9,48 +9,47 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class CharKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class CharKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public CharKeywordRecommender() + : base(SyntaxKind.CharKeyword) { - public CharKeywordRecommender() - : base(SyntaxKind.CharKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_Char; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_Char; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CheckedKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CheckedKeywordRecommender.cs index f9d3d136ff3ae..606e0dfa120ee 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CheckedKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/CheckedKeywordRecommender.cs @@ -8,60 +8,59 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class CheckedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class CheckedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public CheckedKeywordRecommender() + : base(SyntaxKind.CheckedKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) { - public CheckedKeywordRecommender() - : base(SyntaxKind.CheckedKeyword) + if (context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsNonAttributeExpressionContext) { + return true; } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + var targetToken = context.TargetToken; + + if (targetToken.Kind() == SyntaxKind.OperatorKeyword) { - if (context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsNonAttributeExpressionContext) + var previousPossiblySkippedToken = targetToken.GetPreviousToken(includeSkipped: true); + + if (previousPossiblySkippedToken.IsLastTokenOfNode()) { return true; } - var targetToken = context.TargetToken; + SyntaxToken previousToken; - if (targetToken.Kind() == SyntaxKind.OperatorKeyword) + if (previousPossiblySkippedToken.IsLastTokenOfNode()) { - var previousPossiblySkippedToken = targetToken.GetPreviousToken(includeSkipped: true); + var firstSpecifierToken = previousPossiblySkippedToken.GetRequiredAncestor().GetFirstToken(includeSkipped: true); - if (previousPossiblySkippedToken.IsLastTokenOfNode()) + if (firstSpecifierToken.GetPreviousToken(includeSkipped: true).IsLastTokenOfNode()) { return true; } - SyntaxToken previousToken; - - if (previousPossiblySkippedToken.IsLastTokenOfNode()) - { - var firstSpecifierToken = previousPossiblySkippedToken.GetRequiredAncestor().GetFirstToken(includeSkipped: true); - - if (firstSpecifierToken.GetPreviousToken(includeSkipped: true).IsLastTokenOfNode()) - { - return true; - } - - previousToken = firstSpecifierToken.GetPreviousToken(includeSkipped: false); - } - else - { - previousToken = targetToken.GetPreviousToken(includeSkipped: false); - } - - if (previousToken.Kind() == SyntaxKind.ExplicitKeyword) - { - return true; - } + previousToken = firstSpecifierToken.GetPreviousToken(includeSkipped: false); + } + else + { + previousToken = targetToken.GetPreviousToken(includeSkipped: false); } - return false; + if (previousToken.Kind() == SyntaxKind.ExplicitKeyword) + { + return true; + } } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ChecksumKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ChecksumKeywordRecommender.cs index 756543cdf9cf2..24c0523e6d6ca 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ChecksumKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ChecksumKeywordRecommender.cs @@ -5,25 +5,24 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ChecksumKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ChecksumKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ChecksumKeywordRecommender() + : base(SyntaxKind.ChecksumKeyword, isValidInPreprocessorContext: true) { - public ChecksumKeywordRecommender() - : base(SyntaxKind.ChecksumKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - // # pragma | - // # pragma w| - var previousToken1 = context.TargetToken; - var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // # pragma | + // # pragma w| + var previousToken1 = context.TargetToken; + var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); - return - previousToken1.Kind() == SyntaxKind.PragmaKeyword && - previousToken2.Kind() == SyntaxKind.HashToken; - } + return + previousToken1.Kind() == SyntaxKind.PragmaKeyword && + previousToken2.Kind() == SyntaxKind.HashToken; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ClassKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ClassKeywordRecommender.cs index c99da0e3bab3e..9102e13a1777b 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ClassKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ClassKeywordRecommender.cs @@ -7,41 +7,40 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders -{ - internal class ClassKeywordRecommender : AbstractSyntacticSingleKeywordRecommender - { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.AbstractKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.FileKeyword, - }; +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; - public ClassKeywordRecommender() - : base(SyntaxKind.ClassKeyword) +internal class ClassKeywordRecommender : AbstractSyntacticSingleKeywordRecommender +{ + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - } + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.AbstractKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.FileKeyword, + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsGlobalStatementContext || - context.IsTypeDeclarationContext( - validModifiers: s_validModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: true, - cancellationToken: cancellationToken) || - context.IsRecordDeclarationContext(s_validModifiers, cancellationToken) || - syntaxTree.IsTypeParameterConstraintStartContext(position, context.LeftToken); - } + public ClassKeywordRecommender() + : base(SyntaxKind.ClassKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsGlobalStatementContext || + context.IsTypeDeclarationContext( + validModifiers: s_validModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: true, + cancellationToken: cancellationToken) || + context.IsRecordDeclarationContext(s_validModifiers, cancellationToken) || + syntaxTree.IsTypeParameterConstraintStartContext(position, context.LeftToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ConstKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ConstKeywordRecommender.cs index da69567ec5a9a..7c0fe78ee4912 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ConstKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ConstKeywordRecommender.cs @@ -7,60 +7,59 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ConstKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ConstKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PrivateKeyword, - }; + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PrivateKeyword, + }; - private static readonly ISet s_validGlobalModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PrivateKeyword, - }; + private static readonly ISet s_validGlobalModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PrivateKeyword, + }; - public ConstKeywordRecommender() - : base(SyntaxKind.ConstKeyword) - { - } + public ConstKeywordRecommender() + : base(SyntaxKind.ConstKeyword) + { + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - IsMemberDeclarationContext(context, cancellationToken) || - IsLocalVariableDeclaration(context); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + IsMemberDeclarationContext(context, cancellationToken) || + IsLocalVariableDeclaration(context); + } - private static bool IsMemberDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, s_validGlobalModifiers, cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: s_validModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + private static bool IsMemberDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, s_validGlobalModifiers, cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: s_validModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); + } - private static bool IsLocalVariableDeclaration(CSharpSyntaxContext context) - { - // cases: - // void Goo() { - // | - // - // | - return - context.IsStatementContext || - context.IsGlobalStatementContext; - } + private static bool IsLocalVariableDeclaration(CSharpSyntaxContext context) + { + // cases: + // void Goo() { + // | + // + // | + return + context.IsStatementContext || + context.IsGlobalStatementContext; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ContinueKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ContinueKeywordRecommender.cs index 9d2fba4053202..6ec1cc64b0e1b 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ContinueKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ContinueKeywordRecommender.cs @@ -8,41 +8,40 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ContinueKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ContinueKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ContinueKeywordRecommender() + : base(SyntaxKind.ContinueKeyword) { - public ContinueKeywordRecommender() - : base(SyntaxKind.ContinueKeyword) + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (!context.IsStatementContext) { + return false; } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + // allowed if we're inside a loop construct. + + var leaf = context.LeftToken; + foreach (var v in leaf.GetAncestors()) { - if (!context.IsStatementContext) + if (v is AnonymousFunctionExpressionSyntax) { + // if we hit a lambda while walking up, then we can't + // 'continue' any outer loops. return false; } - // allowed if we're inside a loop construct. - - var leaf = context.LeftToken; - foreach (var v in leaf.GetAncestors()) + if (v.IsContinuableConstruct()) { - if (v is AnonymousFunctionExpressionSyntax) - { - // if we hit a lambda while walking up, then we can't - // 'continue' any outer loops. - return false; - } - - if (v.IsContinuableConstruct()) - { - return true; - } + return true; } - - return false; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DecimalKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DecimalKeywordRecommender.cs index cf260f85dc637..89c52e2e156fa 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DecimalKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DecimalKeywordRecommender.cs @@ -9,48 +9,47 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class DecimalKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class DecimalKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public DecimalKeywordRecommender() + : base(SyntaxKind.DecimalKeyword) { - public DecimalKeywordRecommender() - : base(SyntaxKind.DecimalKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_Decimal; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_Decimal; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DefaultKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DefaultKeywordRecommender.cs index 1c967855b2c86..b79464d1034c0 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DefaultKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DefaultKeywordRecommender.cs @@ -6,40 +6,39 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class DefaultKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class DefaultKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public DefaultKeywordRecommender() + : base(SyntaxKind.DefaultKeyword, isValidInPreprocessorContext: true) { - public DefaultKeywordRecommender() - : base(SyntaxKind.DefaultKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - IsValidPreProcessorContext(context) || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsAnyExpressionContext || - context.TargetToken.IsSwitchLabelContext() || - context.SyntaxTree.IsTypeParameterConstraintStartContext(position, context.LeftToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + IsValidPreProcessorContext(context) || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsAnyExpressionContext || + context.TargetToken.IsSwitchLabelContext() || + context.SyntaxTree.IsTypeParameterConstraintStartContext(position, context.LeftToken); + } - private static bool IsValidPreProcessorContext(CSharpSyntaxContext context) - { - // cases: - // #line | - // #line d| - // # line | - // # line d| + private static bool IsValidPreProcessorContext(CSharpSyntaxContext context) + { + // cases: + // #line | + // #line d| + // # line | + // # line d| - var previousToken1 = context.TargetToken; - var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); + var previousToken1 = context.TargetToken; + var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); - return - previousToken1.Kind() == SyntaxKind.LineKeyword && - previousToken2.Kind() == SyntaxKind.HashToken; - } + return + previousToken1.Kind() == SyntaxKind.LineKeyword && + previousToken2.Kind() == SyntaxKind.HashToken; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DefineKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DefineKeywordRecommender.cs index 2156109c5bc2c..695838bb564b1 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DefineKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DefineKeywordRecommender.cs @@ -6,17 +6,16 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class DefineKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class DefineKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public DefineKeywordRecommender() + : base(SyntaxKind.DefineKeyword, isValidInPreprocessorContext: true) { - public DefineKeywordRecommender() - : base(SyntaxKind.DefineKeyword, isValidInPreprocessorContext: true) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsPreProcessorKeywordContext && - context.SyntaxTree.IsBeforeFirstToken(position, cancellationToken); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsPreProcessorKeywordContext && + context.SyntaxTree.IsBeforeFirstToken(position, cancellationToken); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DescendingKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DescendingKeywordRecommender.cs index 2db3f3bd5ced8..2b3a0c1c0f2ae 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DescendingKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DescendingKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class DescendingKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class DescendingKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public DescendingKeywordRecommender() + : base(SyntaxKind.DescendingKeyword) { - public DescendingKeywordRecommender() - : base(SyntaxKind.DescendingKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.TargetToken.IsOrderByDirectionContext(); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.TargetToken.IsOrderByDirectionContext(); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DisableKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DisableKeywordRecommender.cs index c983c9a9dc7e1..ad3b3dd970523 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DisableKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DisableKeywordRecommender.cs @@ -5,35 +5,34 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class DisableKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class DisableKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public DisableKeywordRecommender() + : base(SyntaxKind.DisableKeyword, isValidInPreprocessorContext: true) { - public DisableKeywordRecommender() - : base(SyntaxKind.DisableKeyword, isValidInPreprocessorContext: true) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var previousToken1 = context.TargetToken; - var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); - var previousToken3 = previousToken2.GetPreviousToken(includeSkipped: true); + } - if (previousToken1.Kind() == SyntaxKind.NullableKeyword && - previousToken2.Kind() == SyntaxKind.HashToken) - { - // # nullable | - // # nullable d| - return true; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var previousToken1 = context.TargetToken; + var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); + var previousToken3 = previousToken2.GetPreviousToken(includeSkipped: true); - // # pragma warning | - // # pragma warning d| - return - previousToken1.Kind() == SyntaxKind.WarningKeyword && - previousToken2.Kind() == SyntaxKind.PragmaKeyword && - previousToken3.Kind() == SyntaxKind.HashToken; + if (previousToken1.Kind() == SyntaxKind.NullableKeyword && + previousToken2.Kind() == SyntaxKind.HashToken) + { + // # nullable | + // # nullable d| + return true; } + + // # pragma warning | + // # pragma warning d| + return + previousToken1.Kind() == SyntaxKind.WarningKeyword && + previousToken2.Kind() == SyntaxKind.PragmaKeyword && + previousToken3.Kind() == SyntaxKind.HashToken; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DoKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DoKeywordRecommender.cs index 659fa146087d7..c08d2634c328e 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DoKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DoKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class DoKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class DoKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public DoKeywordRecommender() + : base(SyntaxKind.DoKeyword) { - public DoKeywordRecommender() - : base(SyntaxKind.DoKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsStatementContext || context.IsGlobalStatementContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsStatementContext || context.IsGlobalStatementContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DoubleKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DoubleKeywordRecommender.cs index 52ceaa961b4ad..d3186c219d6c3 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DoubleKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DoubleKeywordRecommender.cs @@ -9,48 +9,47 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class DoubleKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class DoubleKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public DoubleKeywordRecommender() + : base(SyntaxKind.DoubleKeyword) { - public DoubleKeywordRecommender() - : base(SyntaxKind.DoubleKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_Double; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_Double; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DynamicKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DynamicKeywordRecommender.cs index cf18c939aae7e..293458e9899f1 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DynamicKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DynamicKeywordRecommender.cs @@ -9,67 +9,66 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class DynamicKeywordRecommender : IKeywordRecommender { - internal class DynamicKeywordRecommender : IKeywordRecommender + private static bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) { - private static bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + if (context.IsPreProcessorDirectiveContext || + context.IsTaskLikeTypeContext) { - if (context.IsPreProcessorDirectiveContext || - context.IsTaskLikeTypeContext) - { - return false; - } - - return IsDynamicTypeContext(position, context, cancellationToken); + return false; } - public ImmutableArray RecommendKeywords(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return IsValidContext(position, context, cancellationToken) - ? [new RecommendedKeyword("dynamic")] - : []; - } + return IsDynamicTypeContext(position, context, cancellationToken); + } - protected static bool IsDynamicTypeContext( - int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; + public ImmutableArray RecommendKeywords(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return IsValidContext(position, context, cancellationToken) + ? [new RecommendedKeyword("dynamic")] + : []; + } - // first do quick exit check - if (syntaxTree.IsDefinitelyNotTypeContext(position, cancellationToken)) - { - return false; - } + protected static bool IsDynamicTypeContext( + int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; - return - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsDefiniteCastTypeContext || - syntaxTree.IsPossibleCastTypeContext(position, context.LeftToken, cancellationToken) || - context.IsObjectCreationTypeContext || - context.IsGenericTypeArgumentContext || - context.IsFunctionPointerTypeArgumentContext || - context.IsIsOrAsTypeContext || - syntaxTree.IsDefaultExpressionContext(position, context.LeftToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - IsAfterRefTypeContext(context) || - context.IsLocalVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsDelegateReturnTypeContext || - context.IsUsingAliasTypeContext || - context.IsPossibleTupleContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); + // first do quick exit check + if (syntaxTree.IsDefinitelyNotTypeContext(position, cancellationToken)) + { + return false; } - private static bool IsAfterRefTypeContext(CSharpSyntaxContext context) - => context.TargetToken.Kind() is SyntaxKind.RefKeyword or SyntaxKind.ReadOnlyKeyword && - context.TargetToken.Parent.IsKind(SyntaxKind.RefType); + return + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsDefiniteCastTypeContext || + syntaxTree.IsPossibleCastTypeContext(position, context.LeftToken, cancellationToken) || + context.IsObjectCreationTypeContext || + context.IsGenericTypeArgumentContext || + context.IsFunctionPointerTypeArgumentContext || + context.IsIsOrAsTypeContext || + syntaxTree.IsDefaultExpressionContext(position, context.LeftToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + IsAfterRefTypeContext(context) || + context.IsLocalVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsDelegateReturnTypeContext || + context.IsUsingAliasTypeContext || + context.IsPossibleTupleContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + private static bool IsAfterRefTypeContext(CSharpSyntaxContext context) + => context.TargetToken.Kind() is SyntaxKind.RefKeyword or SyntaxKind.ReadOnlyKeyword && + context.TargetToken.Parent.IsKind(SyntaxKind.RefType); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ElifKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ElifKeywordRecommender.cs index 545ccbc1b7320..5ff98840b21f1 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ElifKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ElifKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ElifKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ElifKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ElifKeywordRecommender() + : base(SyntaxKind.ElifKeyword, isValidInPreprocessorContext: true) { - public ElifKeywordRecommender() - : base(SyntaxKind.ElifKeyword, isValidInPreprocessorContext: true) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsPreProcessorKeywordContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsPreProcessorKeywordContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ElseKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ElseKeywordRecommender.cs index 180ded4ebaa02..2fe2b078a1522 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ElseKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ElseKeywordRecommender.cs @@ -7,44 +7,43 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ElseKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ElseKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ElseKeywordRecommender() + : base(SyntaxKind.ElseKeyword, isValidInPreprocessorContext: true) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) { - public ElseKeywordRecommender() - : base(SyntaxKind.ElseKeyword, isValidInPreprocessorContext: true) + if (context.IsPreProcessorKeywordContext) { + return true; } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + var token = context.TargetToken; + + // We have to consider all ancestor if statements of the last token until we find a match for this 'else': + // while (true) + // if (true) + // while (true) + // if (true) + // Console.WriteLine(); + // else + // Console.WriteLine(); + // $$ + foreach (var ifStatement in token.GetAncestors()) { - if (context.IsPreProcessorKeywordContext) + // If there's a missing token at the end of the statement, it's incomplete and we do not offer 'else'. + // context.TargetToken does not include zero width so in that case these will never be equal. + if (ifStatement.Statement.GetLastToken(includeZeroWidth: true) == token) { return true; } - - var token = context.TargetToken; - - // We have to consider all ancestor if statements of the last token until we find a match for this 'else': - // while (true) - // if (true) - // while (true) - // if (true) - // Console.WriteLine(); - // else - // Console.WriteLine(); - // $$ - foreach (var ifStatement in token.GetAncestors()) - { - // If there's a missing token at the end of the statement, it's incomplete and we do not offer 'else'. - // context.TargetToken does not include zero width so in that case these will never be equal. - if (ifStatement.Statement.GetLastToken(includeZeroWidth: true) == token) - { - return true; - } - } - - return false; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EnableKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EnableKeywordRecommender.cs index 52775802bd7e2..fc345c8f8eec4 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EnableKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EnableKeywordRecommender.cs @@ -5,36 +5,35 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class EnableKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class EnableKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public EnableKeywordRecommender() + : base(SyntaxKind.EnableKeyword, isValidInPreprocessorContext: true) { - public EnableKeywordRecommender() - : base(SyntaxKind.EnableKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var previousToken1 = context.TargetToken; - var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var previousToken1 = context.TargetToken; + var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); - // # nullable | - // # nullable e| - if (previousToken1.Kind() == SyntaxKind.NullableKeyword && - previousToken2.Kind() == SyntaxKind.HashToken) - { - return true; - } + // # nullable | + // # nullable e| + if (previousToken1.Kind() == SyntaxKind.NullableKeyword && + previousToken2.Kind() == SyntaxKind.HashToken) + { + return true; + } - var previousToken3 = previousToken2.GetPreviousToken(includeSkipped: true); + var previousToken3 = previousToken2.GetPreviousToken(includeSkipped: true); - return - // # pragma warning | - // # pragma warning e| - previousToken1.Kind() == SyntaxKind.WarningKeyword && - previousToken2.Kind() == SyntaxKind.PragmaKeyword && - previousToken3.Kind() == SyntaxKind.HashToken; - } + return + // # pragma warning | + // # pragma warning e| + previousToken1.Kind() == SyntaxKind.WarningKeyword && + previousToken2.Kind() == SyntaxKind.PragmaKeyword && + previousToken3.Kind() == SyntaxKind.HashToken; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EndIfKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EndIfKeywordRecommender.cs index b0cdae4ac4c25..3f88e5985b0eb 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EndIfKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EndIfKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class EndIfKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class EndIfKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public EndIfKeywordRecommender() + : base(SyntaxKind.EndIfKeyword, isValidInPreprocessorContext: true) { - public EndIfKeywordRecommender() - : base(SyntaxKind.EndIfKeyword, isValidInPreprocessorContext: true) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsPreProcessorKeywordContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsPreProcessorKeywordContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EndRegionKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EndRegionKeywordRecommender.cs index 1479d3cdb5ee1..2ce9c088fca51 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EndRegionKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EndRegionKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class EndRegionKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class EndRegionKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public EndRegionKeywordRecommender() + : base(SyntaxKind.EndRegionKeyword, isValidInPreprocessorContext: true, shouldFormatOnCommit: true) { - public EndRegionKeywordRecommender() - : base(SyntaxKind.EndRegionKeyword, isValidInPreprocessorContext: true, shouldFormatOnCommit: true) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsPreProcessorKeywordContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsPreProcessorKeywordContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EnumKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EnumKeywordRecommender.cs index 9b1435cd8d0e8..d2c0ff0abe005 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EnumKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EnumKeywordRecommender.cs @@ -7,32 +7,31 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders -{ - internal class EnumKeywordRecommender : AbstractSyntacticSingleKeywordRecommender - { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.InternalKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - }; +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; - public EnumKeywordRecommender() - : base(SyntaxKind.EnumKeyword) +internal class EnumKeywordRecommender : AbstractSyntacticSingleKeywordRecommender +{ + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - } + SyntaxKind.InternalKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsGlobalStatementContext || - context.IsTypeDeclarationContext( - validModifiers: s_validModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + public EnumKeywordRecommender() + : base(SyntaxKind.EnumKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsGlobalStatementContext || + context.IsTypeDeclarationContext( + validModifiers: s_validModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EqualsKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EqualsKeywordRecommender.cs index c851556fe9e78..6273639451bc5 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EqualsKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EqualsKeywordRecommender.cs @@ -7,39 +7,38 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class EqualsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class EqualsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public EqualsKeywordRecommender() + : base(SyntaxKind.EqualsKeyword) { - public EqualsKeywordRecommender() - : base(SyntaxKind.EqualsKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - // cases: - // join a in expr o1 | - // join a in expr o1 e| + } - var token = context.TargetToken; + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // cases: + // join a in expr o1 | + // join a in expr o1 e| - var join = token.GetAncestor(); - if (join == null) - { - return false; - } + var token = context.TargetToken; - var lastToken = join.LeftExpression.GetLastToken(includeSkipped: true); + var join = token.GetAncestor(); + if (join == null) + { + return false; + } - // join a in expr | - if (join.LeftExpression.Width() > 0 && - token == lastToken) - { - return true; - } + var lastToken = join.LeftExpression.GetLastToken(includeSkipped: true); - return false; + // join a in expr | + if (join.LeftExpression.Width() > 0 && + token == lastToken) + { + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ErrorKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ErrorKeywordRecommender.cs index f26545bc35cc5..c8aac6fad50b5 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ErrorKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ErrorKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ErrorKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ErrorKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ErrorKeywordRecommender() + : base(SyntaxKind.ErrorKeyword, isValidInPreprocessorContext: true) { - public ErrorKeywordRecommender() - : base(SyntaxKind.ErrorKeyword, isValidInPreprocessorContext: true) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsPreProcessorKeywordContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsPreProcessorKeywordContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EventKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EventKeywordRecommender.cs index 1f1bc7c9f905d..6d45bf7986115 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EventKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EventKeywordRecommender.cs @@ -8,45 +8,44 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders -{ - internal class EventKeywordRecommender : AbstractSyntacticSingleKeywordRecommender - { - private static readonly ISet s_validClassModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.VirtualKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.OverrideKeyword, - SyntaxKind.AbstractKeyword, - SyntaxKind.ExternKeyword, - SyntaxKind.UnsafeKeyword - }; - - private static readonly ISet s_validStructModifiers = new HashSet(s_validClassModifiers, SyntaxFacts.EqualityComparer) - { - SyntaxKind.ReadOnlyKeyword, - }; +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; - public EventKeywordRecommender() - : base(SyntaxKind.EventKeyword) +internal class EventKeywordRecommender : AbstractSyntacticSingleKeywordRecommender +{ + private static readonly ISet s_validClassModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - } + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.VirtualKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.OverrideKeyword, + SyntaxKind.AbstractKeyword, + SyntaxKind.ExternKeyword, + SyntaxKind.UnsafeKeyword + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + private static readonly ISet s_validStructModifiers = new HashSet(s_validClassModifiers, SyntaxFacts.EqualityComparer) { - var syntaxTree = context.SyntaxTree; - return - (context.IsGlobalStatementContext && syntaxTree.IsScript()) || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext(validModifiers: s_validClassModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken) || - context.IsMemberDeclarationContext(validModifiers: s_validStructModifiers, validTypeDeclarations: SyntaxKindSet.StructOnlyTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken) || - context.IsMemberAttributeContext(SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, cancellationToken); - } + SyntaxKind.ReadOnlyKeyword, + }; + + public EventKeywordRecommender() + : base(SyntaxKind.EventKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + (context.IsGlobalStatementContext && syntaxTree.IsScript()) || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext(validModifiers: s_validClassModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken) || + context.IsMemberDeclarationContext(validModifiers: s_validStructModifiers, validTypeDeclarations: SyntaxKindSet.StructOnlyTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken) || + context.IsMemberAttributeContext(SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ExplicitKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ExplicitKeywordRecommender.cs index a4196849016a2..b0dbf2efebe31 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ExplicitKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ExplicitKeywordRecommender.cs @@ -7,53 +7,52 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ExplicitKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ExplicitKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.StaticKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ExternKeyword, + SyntaxKind.UnsafeKeyword, + }; + + private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.StaticKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.AbstractKeyword, + SyntaxKind.UnsafeKeyword, + }; + + public ExplicitKeywordRecommender() + : base(SyntaxKind.ExplicitKeyword) { - private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.StaticKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ExternKeyword, - SyntaxKind.UnsafeKeyword, - }; - - private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.StaticKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.AbstractKeyword, - SyntaxKind.UnsafeKeyword, - }; - - public ExplicitKeywordRecommender() - : base(SyntaxKind.ExplicitKeyword) + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.IsMemberDeclarationContext(validModifiers: s_validNonInterfaceMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) { - } + // operators must be both public and static + var modifiers = context.PrecedingModifiers; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + return + modifiers.Contains(SyntaxKind.PublicKeyword) && + modifiers.Contains(SyntaxKind.StaticKeyword); + } + else if (context.IsMemberDeclarationContext(validModifiers: s_validInterfaceMemberModifiers, validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) { - if (context.IsMemberDeclarationContext(validModifiers: s_validNonInterfaceMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) - { - // operators must be both public and static - var modifiers = context.PrecedingModifiers; - - return - modifiers.Contains(SyntaxKind.PublicKeyword) && - modifiers.Contains(SyntaxKind.StaticKeyword); - } - else if (context.IsMemberDeclarationContext(validModifiers: s_validInterfaceMemberModifiers, validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) - { - // operators must be both abstract and static - var modifiers = context.PrecedingModifiers; - - return - modifiers.Contains(SyntaxKind.AbstractKeyword) && - modifiers.Contains(SyntaxKind.StaticKeyword); - } - - return false; + // operators must be both abstract and static + var modifiers = context.PrecedingModifiers; + + return + modifiers.Contains(SyntaxKind.AbstractKeyword) && + modifiers.Contains(SyntaxKind.StaticKeyword); } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ExternKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ExternKeywordRecommender.cs index 4d0babd256bb2..4826f6fe419f2 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ExternKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ExternKeywordRecommender.cs @@ -8,108 +8,107 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ExternKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ExternKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.OverrideKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.VirtualKeyword, + }; + + private static readonly ISet s_validGlobalModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + }; + + private static readonly ISet s_validLocalFunctionModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword + }; + + public ExternKeywordRecommender() + : base(SyntaxKind.ExternKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + IsExternAliasContext(context) || + (context.IsGlobalStatementContext && syntaxTree.IsScript()) || + syntaxTree.IsGlobalMemberDeclarationContext(position, s_validGlobalModifiers, cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: s_validModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken) || + context.SyntaxTree.IsLocalFunctionDeclarationContext(position, s_validLocalFunctionModifiers, cancellationToken); + } + + private static bool IsExternAliasContext(CSharpSyntaxContext context) { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.OverrideKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.VirtualKeyword, - }; - - private static readonly ISet s_validGlobalModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword, - }; - - private static readonly ISet s_validLocalFunctionModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword - }; - - public ExternKeywordRecommender() - : base(SyntaxKind.ExternKeyword) + // cases: + // root: | + + // root: e| + + // extern alias a; + // | + + // extern alias a; + // e| + + // all the above, but inside a namespace. + // usings and other constructs *cannot* precede. + + var token = context.TargetToken; + + // root: | + if (token.Kind() == SyntaxKind.None) { + // root namespace + return true; } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + if (token.Kind() == SyntaxKind.OpenBraceToken && + token.Parent.IsKind(SyntaxKind.NamespaceDeclaration)) { - var syntaxTree = context.SyntaxTree; - return - IsExternAliasContext(context) || - (context.IsGlobalStatementContext && syntaxTree.IsScript()) || - syntaxTree.IsGlobalMemberDeclarationContext(position, s_validGlobalModifiers, cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: s_validModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken) || - context.SyntaxTree.IsLocalFunctionDeclarationContext(position, s_validLocalFunctionModifiers, cancellationToken); + return true; } - private static bool IsExternAliasContext(CSharpSyntaxContext context) + // namespace N; + // | + if (token.Kind() == SyntaxKind.SemicolonToken && + token.Parent.IsKind(SyntaxKind.FileScopedNamespaceDeclaration)) { - // cases: - // root: | - - // root: e| - - // extern alias a; - // | - - // extern alias a; - // e| - - // all the above, but inside a namespace. - // usings and other constructs *cannot* precede. - - var token = context.TargetToken; - - // root: | - if (token.Kind() == SyntaxKind.None) - { - // root namespace - return true; - } - - if (token.Kind() == SyntaxKind.OpenBraceToken && - token.Parent.IsKind(SyntaxKind.NamespaceDeclaration)) - { - return true; - } - - // namespace N; - // | - if (token.Kind() == SyntaxKind.SemicolonToken && - token.Parent.IsKind(SyntaxKind.FileScopedNamespaceDeclaration)) - { - return true; - } - - // extern alias a; - // | - if (token.Kind() == SyntaxKind.SemicolonToken && - token.Parent.IsKind(SyntaxKind.ExternAliasDirective)) - { - return true; - } - - return false; + return true; } + + // extern alias a; + // | + if (token.Kind() == SyntaxKind.SemicolonToken && + token.Parent.IsKind(SyntaxKind.ExternAliasDirective)) + { + return true; + } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FalseKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FalseKeywordRecommender.cs index f29761a5dfe70..6007dab4dc58e 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FalseKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FalseKeywordRecommender.cs @@ -6,23 +6,22 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class FalseKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class FalseKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public FalseKeywordRecommender() + : base(SyntaxKind.FalseKeyword, isValidInPreprocessorContext: true) { - public FalseKeywordRecommender() - : base(SyntaxKind.FalseKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsAnyExpressionContext || - context.IsPreProcessorExpressionContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.TargetToken.IsUnaryOperatorContext(); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsAnyExpressionContext || + context.IsPreProcessorExpressionContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.TargetToken.IsUnaryOperatorContext(); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FieldKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FieldKeywordRecommender.cs index 09849de6abacd..40e75cb897832 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FieldKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FieldKeywordRecommender.cs @@ -6,26 +6,25 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class FieldKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class FieldKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + // interfaces don't have members that you can put a [field:] attribute on + private static readonly ISet s_validTypeDeclarations = new HashSet(SyntaxFacts.EqualityComparer) { - // interfaces don't have members that you can put a [field:] attribute on - private static readonly ISet s_validTypeDeclarations = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.StructDeclaration, - SyntaxKind.ClassDeclaration, - SyntaxKind.RecordDeclaration, - SyntaxKind.RecordStructDeclaration, - SyntaxKind.EnumDeclaration, - }; - - public FieldKeywordRecommender() - : base(SyntaxKind.FieldKeyword) - { - } + SyntaxKind.StructDeclaration, + SyntaxKind.ClassDeclaration, + SyntaxKind.RecordDeclaration, + SyntaxKind.RecordStructDeclaration, + SyntaxKind.EnumDeclaration, + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsMemberAttributeContext(s_validTypeDeclarations, cancellationToken); + public FieldKeywordRecommender() + : base(SyntaxKind.FieldKeyword) + { } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsMemberAttributeContext(s_validTypeDeclarations, cancellationToken); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FinallyKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FinallyKeywordRecommender.cs index 2eed6a983adc6..ab711d32b68ec 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FinallyKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FinallyKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class FinallyKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class FinallyKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public FinallyKeywordRecommender() + : base(SyntaxKind.FinallyKeyword) { - public FinallyKeywordRecommender() - : base(SyntaxKind.FinallyKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.SyntaxTree.IsCatchOrFinallyContext(position, context.LeftToken); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.SyntaxTree.IsCatchOrFinallyContext(position, context.LeftToken); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FixedKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FixedKeywordRecommender.cs index a969c31701c5f..a7577b1acabfe 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FixedKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FixedKeywordRecommender.cs @@ -7,37 +7,36 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class FixedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class FixedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.UnsafeKeyword, - }; - - public FixedKeywordRecommender() - : base(SyntaxKind.FixedKeyword) - { - } + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.UnsafeKeyword, + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => IsUnsafeStatementContext(context) || IsMemberDeclarationContext(context, cancellationToken); + public FixedKeywordRecommender() + : base(SyntaxKind.FixedKeyword) + { + } - private static bool IsMemberDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.TargetToken.IsUnsafeContext() && - (context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext(validModifiers: s_validModifiers, validTypeDeclarations: SyntaxKindSet.StructOnlyTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => IsUnsafeStatementContext(context) || IsMemberDeclarationContext(context, cancellationToken); - private static bool IsUnsafeStatementContext(CSharpSyntaxContext context) - => context.TargetToken.IsUnsafeContext() && context.IsStatementContext; + private static bool IsMemberDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.TargetToken.IsUnsafeContext() && + (context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext(validModifiers: s_validModifiers, validTypeDeclarations: SyntaxKindSet.StructOnlyTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)); } + + private static bool IsUnsafeStatementContext(CSharpSyntaxContext context) + => context.TargetToken.IsUnsafeContext() && context.IsStatementContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FloatKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FloatKeywordRecommender.cs index 2b862fbf9420b..5fd2119e67386 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FloatKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FloatKeywordRecommender.cs @@ -9,48 +9,47 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class FloatKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class FloatKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public FloatKeywordRecommender() + : base(SyntaxKind.FloatKeyword) { - public FloatKeywordRecommender() - : base(SyntaxKind.FloatKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_Single; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_Single; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ForEachKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ForEachKeywordRecommender.cs index 53061c9af80ce..a6205dc9fc236 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ForEachKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ForEachKeywordRecommender.cs @@ -5,21 +5,20 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ForEachKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ForEachKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ForEachKeywordRecommender() + : base(SyntaxKind.ForEachKeyword) { - public ForEachKeywordRecommender() - : base(SyntaxKind.ForEachKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsAwaitStatementContext(position, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsAwaitStatementContext(position, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ForKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ForKeywordRecommender.cs index fbacf22097077..f9650905da950 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ForKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ForKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ForKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ForKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ForKeywordRecommender() + : base(SyntaxKind.ForKeyword) { - public ForKeywordRecommender() - : base(SyntaxKind.ForKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsStatementContext || context.IsGlobalStatementContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsStatementContext || context.IsGlobalStatementContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FromKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FromKeywordRecommender.cs index 765db9cf77ee3..92e2383fa433a 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FromKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/FromKeywordRecommender.cs @@ -5,21 +5,20 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class FromKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class FromKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public FromKeywordRecommender() + : base(SyntaxKind.FromKeyword) { - public FromKeywordRecommender() - : base(SyntaxKind.FromKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsGlobalStatementContext || - syntaxTree.IsValidContextForFromClause(position, context.LeftToken, cancellationToken, semanticModelOpt: context.SemanticModel); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsGlobalStatementContext || + syntaxTree.IsValidContextForFromClause(position, context.LeftToken, cancellationToken, semanticModelOpt: context.SemanticModel); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GetKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GetKeywordRecommender.cs index 587a2832056d8..0167db80144ac 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GetKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GetKeywordRecommender.cs @@ -6,20 +6,19 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class GetKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class GetKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public GetKeywordRecommender() + : base(SyntaxKind.GetKeyword) { - public GetKeywordRecommender() - : base(SyntaxKind.GetKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.GetKeyword) || - context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.GetKeyword); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.GetKeyword) || + context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.GetKeyword); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GlobalKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GlobalKeywordRecommender.cs index 5414dacc0871b..f2d7e65824754 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GlobalKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GlobalKeywordRecommender.cs @@ -7,30 +7,29 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class GlobalKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class GlobalKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public GlobalKeywordRecommender() + : base(SyntaxKind.GlobalKeyword) { - public GlobalKeywordRecommender() - : base(SyntaxKind.GlobalKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; + } - if (syntaxTree.IsMemberDeclarationContext(position, context.LeftToken)) - { - var token = context.TargetToken; - if (token.GetAncestor() == null) - return true; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; - return - context.IsTypeContext || - context.IsEnumBaseListContext || - UsingKeywordRecommender.IsUsingDirectiveContext(context, forGlobalKeyword: true, cancellationToken); + if (syntaxTree.IsMemberDeclarationContext(position, context.LeftToken)) + { + var token = context.TargetToken; + if (token.GetAncestor() == null) + return true; } + + return + context.IsTypeContext || + context.IsEnumBaseListContext || + UsingKeywordRecommender.IsUsingDirectiveContext(context, forGlobalKeyword: true, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GotoKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GotoKeywordRecommender.cs index c01df9a2cc38f..9e606dbe09025 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GotoKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GotoKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class GotoKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class GotoKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public GotoKeywordRecommender() + : base(SyntaxKind.GotoKeyword) { - public GotoKeywordRecommender() - : base(SyntaxKind.GotoKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsStatementContext || context.IsGlobalStatementContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsStatementContext || context.IsGlobalStatementContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GroupKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GroupKeywordRecommender.cs index 41a40e20001de..4980092241f3c 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GroupKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GroupKeywordRecommender.cs @@ -6,28 +6,27 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class GroupKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class GroupKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public GroupKeywordRecommender() + : base(SyntaxKind.GroupKeyword) { - public GroupKeywordRecommender() - : base(SyntaxKind.GroupKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var token = context.TargetToken; + } - // var q = from x in y - // | - if (!token.IntersectsWith(position) && - token.IsLastTokenOfQueryClause()) - { - return true; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var token = context.TargetToken; - return false; + // var q = from x in y + // | + if (!token.IntersectsWith(position) && + token.IsLastTokenOfQueryClause()) + { + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/HiddenKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/HiddenKeywordRecommender.cs index bb8941b593a7c..022299a64f1f4 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/HiddenKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/HiddenKeywordRecommender.cs @@ -5,29 +5,28 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class HiddenKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class HiddenKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public HiddenKeywordRecommender() + : base(SyntaxKind.HiddenKeyword, isValidInPreprocessorContext: true) { - public HiddenKeywordRecommender() - : base(SyntaxKind.HiddenKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - // cases: - // #line | - // #line h| - // # line | - // # line h| + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // cases: + // #line | + // #line h| + // # line | + // # line h| - var previousToken1 = context.TargetToken; - var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); + var previousToken1 = context.TargetToken; + var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); - return - previousToken1.Kind() == SyntaxKind.LineKeyword && - previousToken2.Kind() == SyntaxKind.HashToken; - } + return + previousToken1.Kind() == SyntaxKind.LineKeyword && + previousToken2.Kind() == SyntaxKind.HashToken; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IfKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IfKeywordRecommender.cs index 3b1ec26a42f6a..1499e9b475ada 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IfKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IfKeywordRecommender.cs @@ -5,21 +5,20 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class IfKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class IfKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public IfKeywordRecommender() + : base(SyntaxKind.IfKeyword, isValidInPreprocessorContext: true) { - public IfKeywordRecommender() - : base(SyntaxKind.IfKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsPreProcessorKeywordContext || - context.IsStatementContext || - context.IsGlobalStatementContext; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsPreProcessorKeywordContext || + context.IsStatementContext || + context.IsGlobalStatementContext; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ImplicitKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ImplicitKeywordRecommender.cs index 7b3e6d80398c7..91665dc9014c9 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ImplicitKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ImplicitKeywordRecommender.cs @@ -7,53 +7,52 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ImplicitKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ImplicitKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.StaticKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ExternKeyword, + SyntaxKind.UnsafeKeyword, + }; + + private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.StaticKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.AbstractKeyword, + SyntaxKind.UnsafeKeyword, + }; + + public ImplicitKeywordRecommender() + : base(SyntaxKind.ImplicitKeyword) { - private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.StaticKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ExternKeyword, - SyntaxKind.UnsafeKeyword, - }; - - private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.StaticKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.AbstractKeyword, - SyntaxKind.UnsafeKeyword, - }; - - public ImplicitKeywordRecommender() - : base(SyntaxKind.ImplicitKeyword) + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.IsMemberDeclarationContext(validModifiers: s_validNonInterfaceMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) { - } + // operators must be both public and static + var modifiers = context.PrecedingModifiers; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + return + modifiers.Contains(SyntaxKind.PublicKeyword) && + modifiers.Contains(SyntaxKind.StaticKeyword); + } + else if (context.IsMemberDeclarationContext(validModifiers: s_validInterfaceMemberModifiers, validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) { - if (context.IsMemberDeclarationContext(validModifiers: s_validNonInterfaceMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) - { - // operators must be both public and static - var modifiers = context.PrecedingModifiers; - - return - modifiers.Contains(SyntaxKind.PublicKeyword) && - modifiers.Contains(SyntaxKind.StaticKeyword); - } - else if (context.IsMemberDeclarationContext(validModifiers: s_validInterfaceMemberModifiers, validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) - { - // operators must be both abstract and static - var modifiers = context.PrecedingModifiers; - - return - modifiers.Contains(SyntaxKind.AbstractKeyword) && - modifiers.Contains(SyntaxKind.StaticKeyword); - } - - return false; + // operators must be both abstract and static + var modifiers = context.PrecedingModifiers; + + return + modifiers.Contains(SyntaxKind.AbstractKeyword) && + modifiers.Contains(SyntaxKind.StaticKeyword); } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InKeywordRecommender.cs index 99c38072b920c..25ebc0dc3647e 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InKeywordRecommender.cs @@ -8,138 +8,137 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class InKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class InKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public InKeywordRecommender() + : base(SyntaxKind.InKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + IsValidContextInForEachClause(context) || + IsValidContextInFromClause(context, cancellationToken) || + IsValidContextInJoinClause(context, cancellationToken) || + IsInParameterModifierContext(position, context) || + syntaxTree.IsAnonymousMethodParameterModifierContext(position, context.LeftToken) || + syntaxTree.IsPossibleLambdaParameterModifierContext(position, context.LeftToken, cancellationToken) || + context.TargetToken.IsConstructorOrMethodParameterArgumentContext() || + context.TargetToken.IsTypeParameterVarianceContext(); + } + + private static bool IsInParameterModifierContext(int position, CSharpSyntaxContext context) { - public InKeywordRecommender() - : base(SyntaxKind.InKeyword) + if (context.SyntaxTree.IsParameterModifierContext( + position, context.LeftToken, includeOperators: true, out var parameterIndex, out var previousModifier)) { + if (previousModifier is SyntaxKind.None or SyntaxKind.ScopedKeyword) + { + return true; + } + + if (previousModifier == SyntaxKind.ThisKeyword && + parameterIndex == 0 && + context.SyntaxTree.IsPossibleExtensionMethodContext(context.LeftToken)) + { + return true; + } } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + return false; + } + + private static bool IsValidContextInForEachClause(CSharpSyntaxContext context) + { + // cases: + // foreach (var v | + // foreach (var v i| + // foreach (var (x, y) | + + var token = context.TargetToken; + + if (token.Kind() == SyntaxKind.IdentifierToken) { - var syntaxTree = context.SyntaxTree; - return - IsValidContextInForEachClause(context) || - IsValidContextInFromClause(context, cancellationToken) || - IsValidContextInJoinClause(context, cancellationToken) || - IsInParameterModifierContext(position, context) || - syntaxTree.IsAnonymousMethodParameterModifierContext(position, context.LeftToken) || - syntaxTree.IsPossibleLambdaParameterModifierContext(position, context.LeftToken, cancellationToken) || - context.TargetToken.IsConstructorOrMethodParameterArgumentContext() || - context.TargetToken.IsTypeParameterVarianceContext(); + if (token.Parent is ForEachStatementSyntax statement && token == statement.Identifier) + { + return true; + } } - - private static bool IsInParameterModifierContext(int position, CSharpSyntaxContext context) + else if (token.Kind() == SyntaxKind.CloseParenToken) { - if (context.SyntaxTree.IsParameterModifierContext( - position, context.LeftToken, includeOperators: true, out var parameterIndex, out var previousModifier)) + var statement = token.GetAncestor(); + if (statement != null && token.Span.End == statement.Variable.Span.End) { - if (previousModifier is SyntaxKind.None or SyntaxKind.ScopedKeyword) - { - return true; - } - - if (previousModifier == SyntaxKind.ThisKeyword && - parameterIndex == 0 && - context.SyntaxTree.IsPossibleExtensionMethodContext(context.LeftToken)) - { - return true; - } + return true; } - - return false; } - private static bool IsValidContextInForEachClause(CSharpSyntaxContext context) - { - // cases: - // foreach (var v | - // foreach (var v i| - // foreach (var (x, y) | + return false; + } - var token = context.TargetToken; + private static bool IsValidContextInFromClause(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var token = context.TargetToken; - if (token.Kind() == SyntaxKind.IdentifierToken) + if (token.Kind() == SyntaxKind.IdentifierToken) + { + // case: + // from x | + if (token.GetPreviousToken(includeSkipped: true).IsKindOrHasMatchingText(SyntaxKind.FromKeyword)) { - if (token.Parent is ForEachStatementSyntax statement && token == statement.Identifier) + var typeSyntax = token.Parent as TypeSyntax; + if (!typeSyntax.IsPotentialTypeName(context.SemanticModel, cancellationToken)) { return true; } } - else if (token.Kind() == SyntaxKind.CloseParenToken) + + if (token.Parent is FromClauseSyntax fromClause) { - var statement = token.GetAncestor(); - if (statement != null && token.Span.End == statement.Variable.Span.End) + // case: + // from int x | + if (token == fromClause.Identifier && fromClause.Type != null) { return true; } } - - return false; } - private static bool IsValidContextInFromClause(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var token = context.TargetToken; + return false; + } + + private static bool IsValidContextInJoinClause(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var token = context.TargetToken; - if (token.Kind() == SyntaxKind.IdentifierToken) + if (token.Kind() == SyntaxKind.IdentifierToken) + { + var joinClause = token.Parent?.FirstAncestorOrSelf(); + if (joinClause != null) { // case: - // from x | - if (token.GetPreviousToken(includeSkipped: true).IsKindOrHasMatchingText(SyntaxKind.FromKeyword)) + // join int x | + if (token == joinClause.Identifier && joinClause.Type != null) { - var typeSyntax = token.Parent as TypeSyntax; - if (!typeSyntax.IsPotentialTypeName(context.SemanticModel, cancellationToken)) - { - return true; - } + return true; } - if (token.Parent is FromClauseSyntax fromClause) + // case: + // join x | + if (joinClause.Type != null && + joinClause.Type is IdentifierNameSyntax joinIdentifier && + token == joinIdentifier.Identifier && + !joinClause.Type.IsPotentialTypeName(context.SemanticModel, cancellationToken)) { - // case: - // from int x | - if (token == fromClause.Identifier && fromClause.Type != null) - { - return true; - } + return true; } } - - return false; } - private static bool IsValidContextInJoinClause(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var token = context.TargetToken; - - if (token.Kind() == SyntaxKind.IdentifierToken) - { - var joinClause = token.Parent?.FirstAncestorOrSelf(); - if (joinClause != null) - { - // case: - // join int x | - if (token == joinClause.Identifier && joinClause.Type != null) - { - return true; - } - - // case: - // join x | - if (joinClause.Type != null && - joinClause.Type is IdentifierNameSyntax joinIdentifier && - token == joinIdentifier.Identifier && - !joinClause.Type.IsPotentialTypeName(context.SemanticModel, cancellationToken)) - { - return true; - } - } - } - - return false; - } + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InitKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InitKeywordRecommender.cs index 9a082055d7d1f..3121422afe03f 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InitKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InitKeywordRecommender.cs @@ -6,20 +6,19 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class InitKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class InitKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public InitKeywordRecommender() + : base(SyntaxKind.InitKeyword) { - public InitKeywordRecommender() - : base(SyntaxKind.InitKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.InitKeyword) || - context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.InitKeyword); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.InitKeyword) || + context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.InitKeyword); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IntKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IntKeywordRecommender.cs index 276aa02ce91f0..5a9a0c468585e 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IntKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IntKeywordRecommender.cs @@ -9,49 +9,48 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class IntKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class IntKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public IntKeywordRecommender() + : base(SyntaxKind.IntKeyword) { - public IntKeywordRecommender() - : base(SyntaxKind.IntKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsEnumBaseListContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_Int32; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsEnumBaseListContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_Int32; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InterfaceKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InterfaceKeywordRecommender.cs index 1ae4fb99cb1f8..ccd6bab4d1484 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InterfaceKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InterfaceKeywordRecommender.cs @@ -7,33 +7,32 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders -{ - internal class InterfaceKeywordRecommender : AbstractSyntacticSingleKeywordRecommender - { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.InternalKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.UnsafeKeyword - }; +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; - public InterfaceKeywordRecommender() - : base(SyntaxKind.InterfaceKeyword) +internal class InterfaceKeywordRecommender : AbstractSyntacticSingleKeywordRecommender +{ + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - } + SyntaxKind.InternalKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.UnsafeKeyword + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsGlobalStatementContext || - context.IsTypeDeclarationContext( - validModifiers: s_validModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: true, - cancellationToken: cancellationToken); - } + public InterfaceKeywordRecommender() + : base(SyntaxKind.InterfaceKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsGlobalStatementContext || + context.IsTypeDeclarationContext( + validModifiers: s_validModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: true, + cancellationToken: cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InternalKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InternalKeywordRecommender.cs index f4fdd98e574d7..6836a4e78275d 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InternalKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InternalKeywordRecommender.cs @@ -7,65 +7,64 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class InternalKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class InternalKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public InternalKeywordRecommender() + : base(SyntaxKind.InternalKeyword) { - public InternalKeywordRecommender() - : base(SyntaxKind.InternalKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsGlobalStatementContext || - IsValidContextForAccessor(context) || - IsValidContextForType(context, cancellationToken) || - IsValidContextForMember(context, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsGlobalStatementContext || + IsValidContextForAccessor(context) || + IsValidContextForType(context, cancellationToken) || + IsValidContextForMember(context, cancellationToken); + } - private static bool IsValidContextForAccessor(CSharpSyntaxContext context) + private static bool IsValidContextForAccessor(CSharpSyntaxContext context) + { + if (context.TargetToken.IsAccessorDeclarationContext(context.Position) || + context.TargetToken.IsAccessorDeclarationContext(context.Position)) { - if (context.TargetToken.IsAccessorDeclarationContext(context.Position) || - context.TargetToken.IsAccessorDeclarationContext(context.Position)) - { - return CheckPreviousAccessibilityModifiers(context); - } - - return false; + return CheckPreviousAccessibilityModifiers(context); } - private static bool IsValidContextForMember(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - if (context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext(validModifiers: SyntaxKindSet.AllMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) - { - return CheckPreviousAccessibilityModifiers(context); - } - - return false; - } + return false; + } - private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) + private static bool IsValidContextForMember(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext(validModifiers: SyntaxKindSet.AllMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) { - if (context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) - { - return CheckPreviousAccessibilityModifiers(context); - } - - return false; + return CheckPreviousAccessibilityModifiers(context); } - private static bool CheckPreviousAccessibilityModifiers(CSharpSyntaxContext context) + return false; + } + + private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) { - // internal things can be protected. - var precedingModifiers = context.PrecedingModifiers; - return - !precedingModifiers.Contains(SyntaxKind.FileKeyword) && - !precedingModifiers.Contains(SyntaxKind.PublicKeyword) && - !precedingModifiers.Contains(SyntaxKind.InternalKeyword) && - !precedingModifiers.Contains(SyntaxKind.PrivateKeyword); + return CheckPreviousAccessibilityModifiers(context); } + + return false; + } + + private static bool CheckPreviousAccessibilityModifiers(CSharpSyntaxContext context) + { + // internal things can be protected. + var precedingModifiers = context.PrecedingModifiers; + return + !precedingModifiers.Contains(SyntaxKind.FileKeyword) && + !precedingModifiers.Contains(SyntaxKind.PublicKeyword) && + !precedingModifiers.Contains(SyntaxKind.InternalKeyword) && + !precedingModifiers.Contains(SyntaxKind.PrivateKeyword); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IntoKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IntoKeywordRecommender.cs index 918412268aca6..4f12793e4f4e6 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IntoKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IntoKeywordRecommender.cs @@ -8,119 +8,118 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class IntoKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class IntoKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public IntoKeywordRecommender() + : base(SyntaxKind.IntoKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + IsValidContextForJoin(context) || + IsValidContextForSelect(context) || + IsValidContextForGroup(context); + } + + private static bool IsValidContextForSelect(CSharpSyntaxContext context) { - public IntoKeywordRecommender() - : base(SyntaxKind.IntoKeyword) + var token = context.TargetToken; + + var select = token.GetAncestor(); + if (select == null) { + return false; } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + if (select.Expression.Width() == 0) { - return - IsValidContextForJoin(context) || - IsValidContextForSelect(context) || - IsValidContextForGroup(context); + return false; } - private static bool IsValidContextForSelect(CSharpSyntaxContext context) + // cases: + // select x.| + // select x.i| + var lastCompleteToken = token.GetPreviousTokenIfTouchingWord(context.Position); + if (lastCompleteToken.Kind() == SyntaxKind.DotToken) { - var token = context.TargetToken; - - var select = token.GetAncestor(); - if (select == null) - { - return false; - } + return false; + } - if (select.Expression.Width() == 0) - { - return false; - } + var lastToken = select.Expression.GetLastToken(includeSkipped: true); + if (lastToken == token) + { + return true; + } - // cases: - // select x.| - // select x.i| - var lastCompleteToken = token.GetPreviousTokenIfTouchingWord(context.Position); - if (lastCompleteToken.Kind() == SyntaxKind.DotToken) - { - return false; - } + return false; + } - var lastToken = select.Expression.GetLastToken(includeSkipped: true); - if (lastToken == token) - { - return true; - } + private static bool IsValidContextForGroup(CSharpSyntaxContext context) + { + var token = context.TargetToken; + var group = token.GetAncestor(); + if (group == null) + { return false; } - private static bool IsValidContextForGroup(CSharpSyntaxContext context) + if (group.ByExpression.Width() == 0 || + group.GroupExpression.Width() == 0) { - var token = context.TargetToken; + return false; + } - var group = token.GetAncestor(); - if (group == null) - { - return false; - } + var lastToken = group.ByExpression.GetLastToken(includeSkipped: true); - if (group.ByExpression.Width() == 0 || - group.GroupExpression.Width() == 0) - { - return false; - } + if (lastToken == token) + { + return true; + } - var lastToken = group.ByExpression.GetLastToken(includeSkipped: true); + return false; + } - if (lastToken == token) - { - return true; - } + private static bool IsValidContextForJoin(CSharpSyntaxContext context) + { + // cases: + // join a in expr o1 equals o2 | + // join a in expr o1 equals o2 i| - return false; - } + var token = context.TargetToken; + var join = token.GetAncestor(); - private static bool IsValidContextForJoin(CSharpSyntaxContext context) + if (join == null) { - // cases: - // join a in expr o1 equals o2 | - // join a in expr o1 equals o2 i| - - var token = context.TargetToken; - var join = token.GetAncestor(); - - if (join == null) + // happens for: + // join a in expr on o1 equals o2 e| + if (!token.IntersectsWith(context.Position)) { - // happens for: - // join a in expr on o1 equals o2 e| - if (!token.IntersectsWith(context.Position)) - { - return false; - } - - token = token.GetPreviousToken(includeSkipped: true); - join = token.GetAncestor(); - - if (join == null) - { - return false; - } + return false; } - var lastToken = join.RightExpression.GetLastToken(includeSkipped: true); + token = token.GetPreviousToken(includeSkipped: true); + join = token.GetAncestor(); - // join a in expr on o1 equals o2 | - if (token == lastToken && - !lastToken.IntersectsWith(context.Position)) + if (join == null) { - return true; + return false; } + } - return false; + var lastToken = join.RightExpression.GetLastToken(includeSkipped: true); + + // join a in expr on o1 equals o2 | + if (token == lastToken && + !lastToken.IntersectsWith(context.Position)) + { + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IsKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IsKeywordRecommender.cs index a0be59e2e3331..4f7bcfc2072c5 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IsKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/IsKeywordRecommender.cs @@ -5,20 +5,19 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class IsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class IsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public IsKeywordRecommender() + : base(SyntaxKind.IsKeyword) { - public IsKeywordRecommender() - : base(SyntaxKind.IsKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - // cases: - // expr | - return !context.IsInNonUserCode && context.IsIsOrAsOrSwitchOrWithExpressionContext; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // cases: + // expr | + return !context.IsInNonUserCode && context.IsIsOrAsOrSwitchOrWithExpressionContext; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/JoinKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/JoinKeywordRecommender.cs index 74da68077fcca..5b292040505d4 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/JoinKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/JoinKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class JoinKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class JoinKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public JoinKeywordRecommender() + : base(SyntaxKind.JoinKeyword) { - public JoinKeywordRecommender() - : base(SyntaxKind.JoinKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.SyntaxTree.IsValidContextForJoinClause(position, context.LeftToken); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.SyntaxTree.IsValidContextForJoinClause(position, context.LeftToken); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LetKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LetKeywordRecommender.cs index fb34d1e93ed83..9880d572bc173 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LetKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LetKeywordRecommender.cs @@ -6,28 +6,27 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class LetKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class LetKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public LetKeywordRecommender() + : base(SyntaxKind.LetKeyword) { - public LetKeywordRecommender() - : base(SyntaxKind.LetKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var token = context.TargetToken; + } - // var q = from x in y - // | - if (!token.IntersectsWith(position) && - token.IsLastTokenOfQueryClause()) - { - return true; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var token = context.TargetToken; - return false; + // var q = from x in y + // | + if (!token.IntersectsWith(position) && + token.IsLastTokenOfQueryClause()) + { + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LineKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LineKeywordRecommender.cs index 64497261be260..645eca23f69ce 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LineKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LineKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class LineKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class LineKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public LineKeywordRecommender() + : base(SyntaxKind.LineKeyword, isValidInPreprocessorContext: true) { - public LineKeywordRecommender() - : base(SyntaxKind.LineKeyword, isValidInPreprocessorContext: true) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsPreProcessorKeywordContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsPreProcessorKeywordContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LoadKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LoadKeywordRecommender.cs index 4d2ae972ec9ea..41f1a6bbcbb78 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LoadKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LoadKeywordRecommender.cs @@ -6,22 +6,21 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class LoadKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class LoadKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public LoadKeywordRecommender() + : base(SyntaxKind.LoadKeyword, isValidInPreprocessorContext: true) { - public LoadKeywordRecommender() - : base(SyntaxKind.LoadKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsPreProcessorKeywordContext && - syntaxTree.IsScript() && - syntaxTree.IsBeforeFirstToken(position, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsPreProcessorKeywordContext && + syntaxTree.IsScript() && + syntaxTree.IsBeforeFirstToken(position, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LockKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LockKeywordRecommender.cs index 52aa3185567d2..687866b7874af 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LockKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LockKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class LockKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class LockKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public LockKeywordRecommender() + : base(SyntaxKind.LockKeyword) { - public LockKeywordRecommender() - : base(SyntaxKind.LockKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsStatementContext || context.IsGlobalStatementContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsStatementContext || context.IsGlobalStatementContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LongKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LongKeywordRecommender.cs index 35a23aa544e07..54cc4818a037d 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LongKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/LongKeywordRecommender.cs @@ -9,49 +9,48 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class LongKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class LongKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public LongKeywordRecommender() + : base(SyntaxKind.LongKeyword) { - public LongKeywordRecommender() - : base(SyntaxKind.LongKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsEnumBaseListContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_Int64; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsEnumBaseListContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_Int64; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ManagedKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ManagedKeywordRecommender.cs index a8d4f80556a70..843244716dd46 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ManagedKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ManagedKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ManagedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ManagedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ManagedKeywordRecommender() + : base(SyntaxKind.ManagedKeyword) { - public ManagedKeywordRecommender() - : base(SyntaxKind.ManagedKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.SyntaxTree.IsFunctionPointerCallingConventionContext(context.TargetToken); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.SyntaxTree.IsFunctionPointerCallingConventionContext(context.TargetToken); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ModuleKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ModuleKeywordRecommender.cs index 58f9938af41ea..a1d40d6d268a4 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ModuleKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ModuleKeywordRecommender.cs @@ -8,26 +8,25 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ModuleKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ModuleKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ModuleKeywordRecommender() + : base(SyntaxKind.ModuleKeyword) { - public ModuleKeywordRecommender() - : base(SyntaxKind.ModuleKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.IsTypeAttributeContext(cancellationToken)) { - if (context.IsTypeAttributeContext(cancellationToken)) - { - var token = context.LeftToken; - var type = token.GetAncestor(); - - return type == null || type.IsParentKind(SyntaxKind.CompilationUnit); - } + var token = context.LeftToken; + var type = token.GetAncestor(); - return false; + return type == null || type.IsParentKind(SyntaxKind.CompilationUnit); } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NameOfKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NameOfKeywordRecommender.cs index 008fb8b2e5207..53616b594e0bd 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NameOfKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NameOfKeywordRecommender.cs @@ -8,21 +8,20 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class NameOfKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class NameOfKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public NameOfKeywordRecommender() + : base(SyntaxKind.NameOfKeyword) { - public NameOfKeywordRecommender() - : base(SyntaxKind.NameOfKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsAnyExpressionContext || - context.IsStatementContext || - context.IsGlobalStatementContext; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsAnyExpressionContext || + context.IsStatementContext || + context.IsGlobalStatementContext; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NamespaceKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NamespaceKeywordRecommender.cs index 588e16db32266..4708ade62a49d 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NamespaceKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NamespaceKeywordRecommender.cs @@ -8,157 +8,156 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class NamespaceKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class NamespaceKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public NamespaceKeywordRecommender() + : base(SyntaxKind.NamespaceKeyword) { - public NamespaceKeywordRecommender() - : base(SyntaxKind.NamespaceKeyword) - { - } + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + // namespaces are illegal in interactive code: + if (syntaxTree.IsScript()) { - var syntaxTree = context.SyntaxTree; + return false; + } - // namespaces are illegal in interactive code: - if (syntaxTree.IsScript()) - { - return false; - } + // cases: + // root: | - // cases: - // root: | + // root: n| - // root: n| + // extern alias a; + // | - // extern alias a; - // | + // extern alias a; + // n| - // extern alias a; - // n| + // using Goo; + // | - // using Goo; - // | + // using Goo; + // n| - // using Goo; - // n| + // using Goo = Bar; + // | - // using Goo = Bar; - // | + // using Goo = Bar; + // n| - // using Goo = Bar; - // n| + // namespace N {} + // | - // namespace N {} - // | + // namespace N {} + // n| - // namespace N {} - // n| + // class C {} + // | - // class C {} - // | + // class C {} + // n| - // class C {} - // n| + var leftToken = context.LeftToken; + var token = context.TargetToken; - var leftToken = context.LeftToken; - var token = context.TargetToken; + // root: n| - // root: n| + // ns Goo { n| - // ns Goo { n| + // extern alias a; + // n| - // extern alias a; - // n| + // using Goo; + // n| - // using Goo; - // n| + // using Goo = Bar; + // n| - // using Goo = Bar; - // n| + // a namespace can't come before usings/externs + // a child namespace can't come before usings/externs + var nextToken = leftToken.GetNextToken(includeSkipped: true); + if (nextToken.IsUsingOrExternKeyword() || + (nextToken.Kind() == SyntaxKind.GlobalKeyword && nextToken.GetAncestor()?.GlobalKeyword == nextToken)) + { + return false; + } - // a namespace can't come before usings/externs - // a child namespace can't come before usings/externs - var nextToken = leftToken.GetNextToken(includeSkipped: true); - if (nextToken.IsUsingOrExternKeyword() || - (nextToken.Kind() == SyntaxKind.GlobalKeyword && nextToken.GetAncestor()?.GlobalKeyword == nextToken)) + // root: | + if (token.Kind() == SyntaxKind.None) + { + // root namespace + var root = (CompilationUnitSyntax)syntaxTree.GetRoot(cancellationToken); + if (root.Externs.Count > 0 || + root.Usings.Count > 0) { return false; } - // root: | - if (token.Kind() == SyntaxKind.None) - { - // root namespace - var root = (CompilationUnitSyntax)syntaxTree.GetRoot(cancellationToken); - if (root.Externs.Count > 0 || - root.Usings.Count > 0) - { - return false; - } - - return true; - } + return true; + } - if (token.Kind() == SyntaxKind.OpenBraceToken && - token.Parent.IsKind(SyntaxKind.NamespaceDeclaration)) - { - return true; - } + if (token.Kind() == SyntaxKind.OpenBraceToken && + token.Parent.IsKind(SyntaxKind.NamespaceDeclaration)) + { + return true; + } - // extern alias a; - // | + // extern alias a; + // | - // using Goo; - // | - if (token.Kind() == SyntaxKind.SemicolonToken) + // using Goo; + // | + if (token.Kind() == SyntaxKind.SemicolonToken) + { + if (token.Parent is (kind: SyntaxKind.ExternAliasDirective or SyntaxKind.UsingDirective) && + !token.Parent.Parent.IsKind(SyntaxKind.FileScopedNamespaceDeclaration)) { - if (token.Parent is (kind: SyntaxKind.ExternAliasDirective or SyntaxKind.UsingDirective) && - !token.Parent.Parent.IsKind(SyntaxKind.FileScopedNamespaceDeclaration)) - { - return true; - } + return true; } + } - // class C {} - // | - if (token.Kind() == SyntaxKind.CloseBraceToken) + // class C {} + // | + if (token.Kind() == SyntaxKind.CloseBraceToken) + { + if (token.Parent is TypeDeclarationSyntax && + token.Parent.Parent is not TypeDeclarationSyntax) { - if (token.Parent is TypeDeclarationSyntax && - token.Parent.Parent is not TypeDeclarationSyntax) - { - return true; - } - else if (token.Parent.IsKind(SyntaxKind.NamespaceDeclaration)) - { - return true; - } + return true; } - - // delegate void D(); - // | - - if (token.Kind() == SyntaxKind.SemicolonToken) + else if (token.Parent.IsKind(SyntaxKind.NamespaceDeclaration)) { - if (token.Parent.IsKind(SyntaxKind.DelegateDeclaration) && - token.Parent.Parent is not TypeDeclarationSyntax) - { - return true; - } + return true; } + } - // [assembly: goo] - // | + // delegate void D(); + // | - if (token.Kind() == SyntaxKind.CloseBracketToken && - token.Parent.IsKind(SyntaxKind.AttributeList) && - token.Parent.IsParentKind(SyntaxKind.CompilationUnit)) + if (token.Kind() == SyntaxKind.SemicolonToken) + { + if (token.Parent.IsKind(SyntaxKind.DelegateDeclaration) && + token.Parent.Parent is not TypeDeclarationSyntax) { return true; } + } - return false; + // [assembly: goo] + // | + + if (token.Kind() == SyntaxKind.CloseBracketToken && + token.Parent.IsKind(SyntaxKind.AttributeList) && + token.Parent.IsParentKind(SyntaxKind.CompilationUnit)) + { + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NewKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NewKeywordRecommender.cs index cd3ce2e67e635..785798b60afa2 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NewKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NewKeywordRecommender.cs @@ -11,106 +11,105 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class NewKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class NewKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.AbstractKeyword, + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.ReadOnlyKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.VirtualKeyword, + SyntaxKind.VolatileKeyword, + }; + + protected static readonly ISet ValidTypeModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.AbstractKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword + }; + + public NewKeywordRecommender() + : base(SyntaxKind.NewKeyword) { - private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.AbstractKeyword, - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.ReadOnlyKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.VirtualKeyword, - SyntaxKind.VolatileKeyword, - }; + } - protected static readonly ISet ValidTypeModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.AbstractKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword - }; + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + IsNewConstraintContext(context) || + context.IsAnyExpressionContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + IsMemberDeclarationContext(context, cancellationToken) || + IsTypeDeclarationContext(context, cancellationToken); + } - public NewKeywordRecommender() - : base(SyntaxKind.NewKeyword) + private static bool IsTypeDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.IsTypeDeclarationContext(validModifiers: ValidTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) { + // we must be on a nested type. + var token = context.LeftToken; + return token.GetAncestors() + .Any(t => token.SpanStart > t.OpenBraceToken.Span.End && + token.Span.End < t.CloseBraceToken.SpanStart); } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - IsNewConstraintContext(context) || - context.IsAnyExpressionContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - IsMemberDeclarationContext(context, cancellationToken) || - IsTypeDeclarationContext(context, cancellationToken); - } + return false; + } - private static bool IsTypeDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - if (context.IsTypeDeclarationContext(validModifiers: ValidTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) - { - // we must be on a nested type. - var token = context.LeftToken; - return token.GetAncestors() - .Any(t => token.SpanStart > t.OpenBraceToken.Span.End && - token.Span.End < t.CloseBraceToken.SpanStart); - } + private static bool IsMemberDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: s_validMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); + } - return false; - } + private static bool IsNewConstraintContext(CSharpSyntaxContext context) + { + // cases: + // where T : | + // where T : class, | + // where T : Goo, | + // note: 'new()' can't come after a 'struct' constraint. - private static bool IsMemberDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) + if (context.SyntaxTree.IsTypeParameterConstraintStartContext(context.Position, context.LeftToken)) { - return - context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: s_validMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); + return true; } - private static bool IsNewConstraintContext(CSharpSyntaxContext context) - { - // cases: - // where T : | - // where T : class, | - // where T : Goo, | - // note: 'new()' can't come after a 'struct' constraint. + var token = context.TargetToken; - if (context.SyntaxTree.IsTypeParameterConstraintStartContext(context.Position, context.LeftToken)) + if (token.Kind() == SyntaxKind.CommaToken && + token.Parent is TypeParameterConstraintClauseSyntax constraintClause) + { + if (!constraintClause.Constraints + .OfType() + .Any(c => c.ClassOrStructKeyword.Kind() == SyntaxKind.StructKeyword)) { return true; } - - var token = context.TargetToken; - - if (token.Kind() == SyntaxKind.CommaToken && - token.Parent is TypeParameterConstraintClauseSyntax constraintClause) - { - if (!constraintClause.Constraints - .OfType() - .Any(c => c.ClassOrStructKeyword.Kind() == SyntaxKind.StructKeyword)) - { - return true; - } - } - - return false; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NintKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NintKeywordRecommender.cs index 4bb5daa43cf10..b477f416dec11 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NintKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NintKeywordRecommender.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Completion.Providers; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal sealed class NintKeywordRecommender : AbstractNativeIntegerKeywordRecommender { - internal sealed class NintKeywordRecommender : AbstractNativeIntegerKeywordRecommender - { - protected override RecommendedKeyword Keyword => new("nint"); - } + protected override RecommendedKeyword Keyword => new("nint"); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NotKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NotKeywordRecommender.cs index a1b561af3eb26..580ac26655f37 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NotKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NotKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class NotKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class NotKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public NotKeywordRecommender() + : base(SyntaxKind.NotKeyword) { - public NotKeywordRecommender() - : base(SyntaxKind.NotKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsAtStartOfPattern; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsAtStartOfPattern; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NotnullKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NotnullKeywordRecommender.cs index 849f712187bf2..16d25b78936d1 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NotnullKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NotnullKeywordRecommender.cs @@ -7,15 +7,14 @@ using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class NotNullKeywordRecommender : IKeywordRecommender { - internal class NotNullKeywordRecommender : IKeywordRecommender + public ImmutableArray RecommendKeywords(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) { - public ImmutableArray RecommendKeywords(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return context.SyntaxTree.IsTypeParameterConstraintContext(position, context.LeftToken) - ? [new RecommendedKeyword("notnull")] - : []; - } + return context.SyntaxTree.IsTypeParameterConstraintContext(position, context.LeftToken) + ? [new RecommendedKeyword("notnull")] + : []; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NuintKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NuintKeywordRecommender.cs index 2252679a8e5ee..f30ea1a94d681 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NuintKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NuintKeywordRecommender.cs @@ -5,17 +5,16 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Completion.Providers; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal sealed class NuintKeywordRecommender : AbstractNativeIntegerKeywordRecommender { - internal sealed class NuintKeywordRecommender : AbstractNativeIntegerKeywordRecommender - { - /// - /// We set the of this item less than the default value so that completion selects - /// the keyword over it as the user starts typing. Being able to type with just nu is ingrained in muscle memory and is more important to maintain versus - /// strict adherence to our normal textual matching procedure. The user can always still get this item simply - /// by typing one additional character and unambiguously referring to nui. - /// - protected override RecommendedKeyword Keyword => new("nuint", matchPriority: MatchPriority.Default - 1); - } + /// + /// We set the of this item less than the default value so that completion selects + /// the keyword over it as the user starts typing. Being able to type with just nu is ingrained in muscle memory and is more important to maintain versus + /// strict adherence to our normal textual matching procedure. The user can always still get this item simply + /// by typing one additional character and unambiguously referring to nui. + /// + protected override RecommendedKeyword Keyword => new("nuint", matchPriority: MatchPriority.Default - 1); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NullKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NullKeywordRecommender.cs index eb889da9bd9a5..38de129e0c3e9 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NullKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NullKeywordRecommender.cs @@ -6,18 +6,17 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class NullKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class NullKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public NullKeywordRecommender() + : base(SyntaxKind.NullKeyword) { - public NullKeywordRecommender() - : base(SyntaxKind.NullKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsAnyExpressionContext || - context.IsStatementContext || - context.IsGlobalStatementContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsAnyExpressionContext || + context.IsStatementContext || + context.IsGlobalStatementContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NullableKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NullableKeywordRecommender.cs index f013ce3452831..de2074a42b9d3 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NullableKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NullableKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class NullableKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class NullableKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public NullableKeywordRecommender() + : base(SyntaxKind.NullableKeyword, isValidInPreprocessorContext: true) { - public NullableKeywordRecommender() - : base(SyntaxKind.NullableKeyword, isValidInPreprocessorContext: true) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsPreProcessorKeywordContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsPreProcessorKeywordContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ObjectKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ObjectKeywordRecommender.cs index a55dfc9fcc6c3..88e691ef9650b 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ObjectKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ObjectKeywordRecommender.cs @@ -9,47 +9,46 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ObjectKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class ObjectKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public ObjectKeywordRecommender() + : base(SyntaxKind.ObjectKeyword) { - public ObjectKeywordRecommender() - : base(SyntaxKind.ObjectKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsNonAttributeExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsTypeOfExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsDefaultExpressionContext(position, context.LeftToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_Object; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsNonAttributeExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsTypeOfExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsDefaultExpressionContext(position, context.LeftToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_Object; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OnKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OnKeywordRecommender.cs index b41bc0cc57e13..3a837c275a228 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OnKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OnKeywordRecommender.cs @@ -7,43 +7,42 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class OnKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class OnKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public OnKeywordRecommender() + : base(SyntaxKind.OnKeyword) { - public OnKeywordRecommender() - : base(SyntaxKind.OnKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - // cases: - // join a in expr | - // join a in expr o| - // join a.b c in expr | - // join a.b c in expr o| + } - var token = context.TargetToken; + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // cases: + // join a in expr | + // join a in expr o| + // join a.b c in expr | + // join a.b c in expr o| - var join = token.GetAncestor(); - if (join == null) - { - return false; - } + var token = context.TargetToken; - // join a in expr | - // join a.b c in expr | + var join = token.GetAncestor(); + if (join == null) + { + return false; + } - var lastToken = join.InExpression.GetLastToken(includeSkipped: true); + // join a in expr | + // join a.b c in expr | - if (join.InExpression.Width() > 0 && - token == lastToken) - { - return true; - } + var lastToken = join.InExpression.GetLastToken(includeSkipped: true); - return false; + if (join.InExpression.Width() > 0 && + token == lastToken) + { + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OperatorKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OperatorKeywordRecommender.cs index afb5acd79cb55..d84e2761e63d2 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OperatorKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OperatorKeywordRecommender.cs @@ -5,25 +5,24 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class OperatorKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class OperatorKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public OperatorKeywordRecommender() + : base(SyntaxKind.OperatorKeyword) { - public OperatorKeywordRecommender() - : base(SyntaxKind.OperatorKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - // cases: - // public static implicit | - // public static explicit | - var token = context.TargetToken; + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // cases: + // public static implicit | + // public static explicit | + var token = context.TargetToken; - return - token.Kind() is SyntaxKind.ImplicitKeyword or - SyntaxKind.ExplicitKeyword; - } + return + token.Kind() is SyntaxKind.ImplicitKeyword or + SyntaxKind.ExplicitKeyword; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OrKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OrKeywordRecommender.cs index 2ad3c83b763d4..325167c1cb05b 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OrKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OrKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class OrKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class OrKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public OrKeywordRecommender() + : base(SyntaxKind.OrKeyword) { - public OrKeywordRecommender() - : base(SyntaxKind.OrKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsAtEndOfPattern; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsAtEndOfPattern; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OrderByKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OrderByKeywordRecommender.cs index 1fc2366ce0383..dbd6cd10a614e 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OrderByKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OrderByKeywordRecommender.cs @@ -6,28 +6,27 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class OrderByKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class OrderByKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public OrderByKeywordRecommender() + : base(SyntaxKind.OrderByKeyword) { - public OrderByKeywordRecommender() - : base(SyntaxKind.OrderByKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var token = context.TargetToken; + } - // var q = from x in y - // | - if (!token.IntersectsWith(position) && - token.IsLastTokenOfQueryClause()) - { - return true; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var token = context.TargetToken; - return false; + // var q = from x in y + // | + if (!token.IntersectsWith(position) && + token.IsLastTokenOfQueryClause()) + { + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OutKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OutKeywordRecommender.cs index 3dc302e095c0a..1197922281e17 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OutKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OutKeywordRecommender.cs @@ -5,33 +5,32 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class OutKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class OutKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public OutKeywordRecommender() + : base(SyntaxKind.OutKeyword) { - public OutKeywordRecommender() - : base(SyntaxKind.OutKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; - return - context.TargetToken.IsTypeParameterVarianceContext() || - IsOutParameterModifierContext(position, context) || - syntaxTree.IsAnonymousMethodParameterModifierContext(position, context.LeftToken) || - syntaxTree.IsPossibleLambdaParameterModifierContext(position, context.LeftToken, cancellationToken) || - context.TargetToken.IsConstructorOrMethodParameterArgumentContext() || - context.TargetToken.IsXmlCrefParameterModifierContext(); - } + return + context.TargetToken.IsTypeParameterVarianceContext() || + IsOutParameterModifierContext(position, context) || + syntaxTree.IsAnonymousMethodParameterModifierContext(position, context.LeftToken) || + syntaxTree.IsPossibleLambdaParameterModifierContext(position, context.LeftToken, cancellationToken) || + context.TargetToken.IsConstructorOrMethodParameterArgumentContext() || + context.TargetToken.IsXmlCrefParameterModifierContext(); + } - private static bool IsOutParameterModifierContext(int position, CSharpSyntaxContext context) - { - return context.SyntaxTree.IsParameterModifierContext( - position, context.LeftToken, includeOperators: false, out _, out var previousModifier) && - previousModifier is SyntaxKind.None or SyntaxKind.ScopedKeyword; - } + private static bool IsOutParameterModifierContext(int position, CSharpSyntaxContext context) + { + return context.SyntaxTree.IsParameterModifierContext( + position, context.LeftToken, includeOperators: false, out _, out var previousModifier) && + previousModifier is SyntaxKind.None or SyntaxKind.ScopedKeyword; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OverrideKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OverrideKeywordRecommender.cs index 908a64139adee..754299cd041cb 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OverrideKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OverrideKeywordRecommender.cs @@ -7,40 +7,39 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class OverrideKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class OverrideKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.AbstractKeyword, - }; + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.AbstractKeyword, + }; - public OverrideKeywordRecommender() - : base(SyntaxKind.OverrideKeyword) - { - } + public OverrideKeywordRecommender() + : base(SyntaxKind.OverrideKeyword) + { + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (!context.IsMemberDeclarationContext( + validModifiers: s_validMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken)) { - if (!context.IsMemberDeclarationContext( - validModifiers: s_validMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken)) - { - return false; - } - - var modifiers = context.PrecedingModifiers; - return !modifiers.Contains(SyntaxKind.PrivateKeyword) || modifiers.Contains(SyntaxKind.ProtectedKeyword); + return false; } + + var modifiers = context.PrecedingModifiers; + return !modifiers.Contains(SyntaxKind.PrivateKeyword) || modifiers.Contains(SyntaxKind.ProtectedKeyword); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ParamKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ParamKeywordRecommender.cs index 8736423e17c8e..bee669cd214ed 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ParamKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ParamKeywordRecommender.cs @@ -7,30 +7,29 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ParamKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ParamKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ParamKeywordRecommender() + : base(SyntaxKind.ParamKeyword) { - public ParamKeywordRecommender() - : base(SyntaxKind.ParamKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var token = context.TargetToken; + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var token = context.TargetToken; - if (token.Kind() == SyntaxKind.OpenBracketToken && - token.Parent.IsKind(SyntaxKind.AttributeList)) + if (token.Kind() == SyntaxKind.OpenBracketToken && + token.Parent.IsKind(SyntaxKind.AttributeList)) + { + if (token.GetAncestor() != null || + token.GetAncestor() != null) { - if (token.GetAncestor() != null || - token.GetAncestor() != null) - { - return true; - } + return true; } - - return false; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ParamsKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ParamsKeywordRecommender.cs index 530ae8d8eec65..51bca79b76354 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ParamsKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ParamsKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ParamsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ParamsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ParamsKeywordRecommender() + : base(SyntaxKind.ParamsKeyword) { - public ParamsKeywordRecommender() - : base(SyntaxKind.ParamsKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.SyntaxTree.IsParamsModifierContext(position, context.LeftToken, cancellationToken); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.SyntaxTree.IsParamsModifierContext(position, context.LeftToken, cancellationToken); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PartialKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PartialKeywordRecommender.cs index 9d56754689912..6534b421b2715 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PartialKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PartialKeywordRecommender.cs @@ -11,55 +11,54 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class PartialKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class PartialKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.AsyncKeyword, - SyntaxKind.StaticKeyword - }; + SyntaxKind.AsyncKeyword, + SyntaxKind.StaticKeyword + }; - public PartialKeywordRecommender() - : base(SyntaxKind.PartialKeyword) - { - } + public PartialKeywordRecommender() + : base(SyntaxKind.PartialKeyword) + { + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsGlobalStatementContext || - IsMemberDeclarationContext(context, cancellationToken) || - IsTypeDeclarationContext(context, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsGlobalStatementContext || + IsMemberDeclarationContext(context, cancellationToken) || + IsTypeDeclarationContext(context, cancellationToken); + } - private static bool IsMemberDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) + private static bool IsMemberDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.IsMemberDeclarationContext(validModifiers: s_validMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) { - if (context.IsMemberDeclarationContext(validModifiers: s_validMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) - { - var token = context.LeftToken; - var decl = token.GetRequiredAncestor(); - - // partial methods must be in partial types - if (!decl.Modifiers.Any(t => t.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword))) - { - return false; - } + var token = context.LeftToken; + var decl = token.GetRequiredAncestor(); - return true; + // partial methods must be in partial types + if (!decl.Modifiers.Any(t => t.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword))) + { + return false; } - return false; + return true; } - private static bool IsTypeDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return context.IsTypeDeclarationContext( - validModifiers: SyntaxKindSet.AllTypeModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + return false; + } + + private static bool IsTypeDeclarationContext(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return context.IsTypeDeclarationContext( + validModifiers: SyntaxKindSet.AllTypeModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PragmaKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PragmaKeywordRecommender.cs index 6af63d3620fc3..343e7a168d418 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PragmaKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PragmaKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class PragmaKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class PragmaKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public PragmaKeywordRecommender() + : base(SyntaxKind.PragmaKeyword, isValidInPreprocessorContext: true) { - public PragmaKeywordRecommender() - : base(SyntaxKind.PragmaKeyword, isValidInPreprocessorContext: true) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsPreProcessorKeywordContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsPreProcessorKeywordContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PrivateKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PrivateKeywordRecommender.cs index 35d229925d69c..dd696e8abad99 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PrivateKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PrivateKeywordRecommender.cs @@ -8,83 +8,82 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class PrivateKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class PrivateKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public PrivateKeywordRecommender() + : base(SyntaxKind.PrivateKeyword) { - public PrivateKeywordRecommender() - : base(SyntaxKind.PrivateKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - (context.IsGlobalStatementContext && context.SyntaxTree.IsScript()) || - IsValidContextForAccessor(context) || - IsValidContextForType(context, cancellationToken) || - IsValidContextForMember(context, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + (context.IsGlobalStatementContext && context.SyntaxTree.IsScript()) || + IsValidContextForAccessor(context) || + IsValidContextForType(context, cancellationToken) || + IsValidContextForMember(context, cancellationToken); + } - private static bool IsValidContextForAccessor(CSharpSyntaxContext context) + private static bool IsValidContextForAccessor(CSharpSyntaxContext context) + { + if (context.TargetToken.IsAccessorDeclarationContext(context.Position) || + context.TargetToken.IsAccessorDeclarationContext(context.Position)) { - if (context.TargetToken.IsAccessorDeclarationContext(context.Position) || - context.TargetToken.IsAccessorDeclarationContext(context.Position)) - { - return CheckPreviousAccessibilityModifiers(context); - } - - return false; + return CheckPreviousAccessibilityModifiers(context); } - private static bool IsValidContextForMember(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - if (context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext(validModifiers: SyntaxKindSet.AllMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) - { - var modifiers = context.PrecedingModifiers; + return false; + } - // can't have private + abstract/virtual/override/sealed - if (modifiers.Contains(SyntaxKind.AbstractKeyword) || - modifiers.Contains(SyntaxKind.VirtualKeyword) || - modifiers.Contains(SyntaxKind.OverrideKeyword) || - modifiers.Contains(SyntaxKind.SealedKeyword)) - { - return false; - } + private static bool IsValidContextForMember(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext(validModifiers: SyntaxKindSet.AllMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) + { + var modifiers = context.PrecedingModifiers; - return CheckPreviousAccessibilityModifiers(context); + // can't have private + abstract/virtual/override/sealed + if (modifiers.Contains(SyntaxKind.AbstractKeyword) || + modifiers.Contains(SyntaxKind.VirtualKeyword) || + modifiers.Contains(SyntaxKind.OverrideKeyword) || + modifiers.Contains(SyntaxKind.SealedKeyword)) + { + return false; } - return false; + return CheckPreviousAccessibilityModifiers(context); } - private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) + return false; + } + + private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) { - if (context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) + // private things can't be in namespaces. + var typeDecl = context.ContainingTypeDeclaration; + if (typeDecl == null) { - // private things can't be in namespaces. - var typeDecl = context.ContainingTypeDeclaration; - if (typeDecl == null) - { - return false; - } - - return CheckPreviousAccessibilityModifiers(context); + return false; } - return false; + return CheckPreviousAccessibilityModifiers(context); } - private static bool CheckPreviousAccessibilityModifiers(CSharpSyntaxContext context) - { - // We can show up after 'protected'. - var precedingModifiers = context.PrecedingModifiers; - return - !precedingModifiers.Contains(SyntaxKind.FileKeyword) && - !precedingModifiers.Contains(SyntaxKind.PublicKeyword) && - !precedingModifiers.Contains(SyntaxKind.InternalKeyword) && - !precedingModifiers.Contains(SyntaxKind.PrivateKeyword); - } + return false; + } + + private static bool CheckPreviousAccessibilityModifiers(CSharpSyntaxContext context) + { + // We can show up after 'protected'. + var precedingModifiers = context.PrecedingModifiers; + return + !precedingModifiers.Contains(SyntaxKind.FileKeyword) && + !precedingModifiers.Contains(SyntaxKind.PublicKeyword) && + !precedingModifiers.Contains(SyntaxKind.InternalKeyword) && + !precedingModifiers.Contains(SyntaxKind.PrivateKeyword); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PropertyKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PropertyKeywordRecommender.cs index 5cfa8ecc6bd04..04f0d08e0b1d1 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PropertyKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PropertyKeywordRecommender.cs @@ -6,16 +6,15 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class PropertyKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class PropertyKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public PropertyKeywordRecommender() + : base(SyntaxKind.PropertyKeyword) { - public PropertyKeywordRecommender() - : base(SyntaxKind.PropertyKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsMemberAttributeContext(SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, cancellationToken); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsMemberAttributeContext(SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, cancellationToken); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ProtectedKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ProtectedKeywordRecommender.cs index 2f932ab16ce19..5241f912c44a8 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ProtectedKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ProtectedKeywordRecommender.cs @@ -7,69 +7,68 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ProtectedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ProtectedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ProtectedKeywordRecommender() + : base(SyntaxKind.ProtectedKeyword) { - public ProtectedKeywordRecommender() - : base(SyntaxKind.ProtectedKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - IsValidContextForAccessor(context) || - IsValidContextForType(context, cancellationToken) || - IsValidContextForMember(context, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + IsValidContextForAccessor(context) || + IsValidContextForType(context, cancellationToken) || + IsValidContextForMember(context, cancellationToken); + } - private static bool IsValidContextForAccessor(CSharpSyntaxContext context) + private static bool IsValidContextForAccessor(CSharpSyntaxContext context) + { + if (context.TargetToken.IsAccessorDeclarationContext(context.Position) || + context.TargetToken.IsAccessorDeclarationContext(context.Position)) { - if (context.TargetToken.IsAccessorDeclarationContext(context.Position) || - context.TargetToken.IsAccessorDeclarationContext(context.Position)) - { - return CheckPreviousAccessibilityModifiers(context); - } - - return false; + return CheckPreviousAccessibilityModifiers(context); } - private static bool IsValidContextForMember(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - if (context.IsMemberDeclarationContext(validModifiers: SyntaxKindSet.AllMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) - { - return CheckPreviousAccessibilityModifiers(context); - } + return false; + } - return false; + private static bool IsValidContextForMember(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.IsMemberDeclarationContext(validModifiers: SyntaxKindSet.AllMemberModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) + { + return CheckPreviousAccessibilityModifiers(context); } - private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) + return false; + } + + private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) { - if (context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) + // protected things can't be in namespaces. + var typeDecl = context.ContainingTypeDeclaration; + if (typeDecl == null) { - // protected things can't be in namespaces. - var typeDecl = context.ContainingTypeDeclaration; - if (typeDecl == null) - { - return false; - } - - return CheckPreviousAccessibilityModifiers(context); + return false; } - return false; + return CheckPreviousAccessibilityModifiers(context); } - private static bool CheckPreviousAccessibilityModifiers(CSharpSyntaxContext context) - { - // We can show up after 'internal' and 'private'. - var precedingModifiers = context.PrecedingModifiers; - return - !precedingModifiers.Contains(SyntaxKind.FileKeyword) && - !precedingModifiers.Contains(SyntaxKind.PublicKeyword) && - !precedingModifiers.Contains(SyntaxKind.ProtectedKeyword); - } + return false; + } + + private static bool CheckPreviousAccessibilityModifiers(CSharpSyntaxContext context) + { + // We can show up after 'internal' and 'private'. + var precedingModifiers = context.PrecedingModifiers; + return + !precedingModifiers.Contains(SyntaxKind.FileKeyword) && + !precedingModifiers.Contains(SyntaxKind.PublicKeyword) && + !precedingModifiers.Contains(SyntaxKind.ProtectedKeyword); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PublicKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PublicKeywordRecommender.cs index 474cf551ce3b1..095a3289e7738 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PublicKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/PublicKeywordRecommender.cs @@ -6,57 +6,56 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class PublicKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class PublicKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public PublicKeywordRecommender() + : base(SyntaxKind.PublicKeyword) { - public PublicKeywordRecommender() - : base(SyntaxKind.PublicKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsGlobalStatementContext || - IsValidContextForType(context, cancellationToken) || - IsValidContextForMember(context, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsGlobalStatementContext || + IsValidContextForType(context, cancellationToken) || + IsValidContextForMember(context, cancellationToken); + } - private static bool IsValidContextForMember(CSharpSyntaxContext context, CancellationToken cancellationToken) + private static bool IsValidContextForMember(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken)) { - if (context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken)) - { - return CheckPreviousAccessibilityModifiers(context); - } - - return false; + return CheckPreviousAccessibilityModifiers(context); } - private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - if (context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) - { - return CheckPreviousAccessibilityModifiers(context); - } - - return false; - } + return false; + } - private static bool CheckPreviousAccessibilityModifiers(CSharpSyntaxContext context) + private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken)) { - var precedingModifiers = context.PrecedingModifiers; - return - !precedingModifiers.Contains(SyntaxKind.FileKeyword) && - !precedingModifiers.Contains(SyntaxKind.PublicKeyword) && - !precedingModifiers.Contains(SyntaxKind.InternalKeyword) && - !precedingModifiers.Contains(SyntaxKind.ProtectedKeyword) && - !precedingModifiers.Contains(SyntaxKind.PrivateKeyword); + return CheckPreviousAccessibilityModifiers(context); } + + return false; + } + + private static bool CheckPreviousAccessibilityModifiers(CSharpSyntaxContext context) + { + var precedingModifiers = context.PrecedingModifiers; + return + !precedingModifiers.Contains(SyntaxKind.FileKeyword) && + !precedingModifiers.Contains(SyntaxKind.PublicKeyword) && + !precedingModifiers.Contains(SyntaxKind.InternalKeyword) && + !precedingModifiers.Contains(SyntaxKind.ProtectedKeyword) && + !precedingModifiers.Contains(SyntaxKind.PrivateKeyword); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReadOnlyKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReadOnlyKeywordRecommender.cs index e6096248b95d7..825d5186723d7 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReadOnlyKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReadOnlyKeywordRecommender.cs @@ -7,59 +7,58 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders -{ - internal class ReadOnlyKeywordRecommender : AbstractSyntacticSingleKeywordRecommender - { - private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.StaticKeyword, - }; +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; - public ReadOnlyKeywordRecommender() - : base(SyntaxKind.ReadOnlyKeyword) +internal class ReadOnlyKeywordRecommender : AbstractSyntacticSingleKeywordRecommender +{ + private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - } + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.StaticKeyword, + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsGlobalStatementContext || - IsRefReadOnlyContext(context) || - IsValidContextForType(context, cancellationToken) || - context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: s_validMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken) || - IsStructAccessorContext(context); - } + public ReadOnlyKeywordRecommender() + : base(SyntaxKind.ReadOnlyKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsGlobalStatementContext || + IsRefReadOnlyContext(context) || + IsValidContextForType(context, cancellationToken) || + context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: s_validMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken) || + IsStructAccessorContext(context); + } - private static bool IsRefReadOnlyContext(CSharpSyntaxContext context) - => context.TargetToken.IsKind(SyntaxKind.RefKeyword) && - (context.TargetToken.Parent.IsKind(SyntaxKind.RefType) || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsFunctionPointerTypeArgumentContext); + private static bool IsRefReadOnlyContext(CSharpSyntaxContext context) + => context.TargetToken.IsKind(SyntaxKind.RefKeyword) && + (context.TargetToken.Parent.IsKind(SyntaxKind.RefType) || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsFunctionPointerTypeArgumentContext); - private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: true, cancellationToken); - } + private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: true, cancellationToken); + } - private static bool IsStructAccessorContext(CSharpSyntaxContext context) - { - var type = context.ContainingTypeDeclaration; - return type is not null && - type.Kind() is SyntaxKind.StructDeclaration or SyntaxKind.RecordStructDeclaration && - context.TargetToken.IsAnyAccessorDeclarationContext(context.Position, SyntaxKind.ReadOnlyKeyword); - } + private static bool IsStructAccessorContext(CSharpSyntaxContext context) + { + var type = context.ContainingTypeDeclaration; + return type is not null && + type.Kind() is SyntaxKind.StructDeclaration or SyntaxKind.RecordStructDeclaration && + context.TargetToken.IsAnyAccessorDeclarationContext(context.Position, SyntaxKind.ReadOnlyKeyword); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RecordKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RecordKeywordRecommender.cs index f39b0e3d0b1de..e714ba81a1001 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RecordKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RecordKeywordRecommender.cs @@ -7,39 +7,38 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders -{ - internal class RecordKeywordRecommender : AbstractSyntacticSingleKeywordRecommender - { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.AbstractKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.ReadOnlyKeyword, - SyntaxKind.FileKeyword, - }; +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; - public RecordKeywordRecommender() - : base(SyntaxKind.RecordKeyword) +internal class RecordKeywordRecommender : AbstractSyntacticSingleKeywordRecommender +{ + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - } + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.AbstractKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.ReadOnlyKeyword, + SyntaxKind.FileKeyword, + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsGlobalStatementContext || - context.IsTypeDeclarationContext( - validModifiers: s_validModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: true, - cancellationToken: cancellationToken); - } + public RecordKeywordRecommender() + : base(SyntaxKind.RecordKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsGlobalStatementContext || + context.IsTypeDeclarationContext( + validModifiers: s_validModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: true, + cancellationToken: cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RefKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RefKeywordRecommender.cs index 19a0cc01d967d..5ddb772b775a1 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RefKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RefKeywordRecommender.cs @@ -8,191 +8,190 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class RefKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class RefKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public RefKeywordRecommender() + : base(SyntaxKind.RefKeyword) { - public RefKeywordRecommender() - : base(SyntaxKind.RefKeyword) - { - } + } - /// - /// Same as with ref specific exclusions - /// - private static readonly ISet RefMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.AbstractKeyword, - // SyntaxKind.AsyncKeyword, // async methods cannot be byref - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.OverrideKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - // SyntaxKind.ReadOnlyKeyword, // fields cannot be byref - SyntaxKind.SealedKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.VirtualKeyword, - // SyntaxKind.VolatileKeyword, // fields cannot be byref - }; - - /// - /// Same as with ref-specific exclusions - /// - private static readonly ISet RefGlobalMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - // SyntaxKind.AsyncKeyword, // async local functions cannot be byref - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.OverrideKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ReadOnlyKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.VolatileKeyword, - }; - - /// - /// Same as with ref-specific exclusions for C# script - /// - private static readonly ISet RefGlobalMemberScriptModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - // SyntaxKind.AsyncKeyword, // async methods cannot be byref - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.OverrideKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - // SyntaxKind.ReadOnlyKeyword, // fields cannot be byref - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword, - // SyntaxKind.VolatileKeyword, // fields cannot be byref - }; - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + /// + /// Same as with ref specific exclusions + /// + private static readonly ISet RefMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - var syntaxTree = context.SyntaxTree; - return - IsRefParameterModifierContext(position, context) || - IsValidContextForType(context, cancellationToken) || - syntaxTree.IsAnonymousMethodParameterModifierContext(position, context.LeftToken) || - syntaxTree.IsPossibleLambdaParameterModifierContext(position, context.LeftToken, cancellationToken) || - context.TargetToken.IsConstructorOrMethodParameterArgumentContext() || - context.TargetToken.IsXmlCrefParameterModifierContext() || - IsValidNewByRefContext(syntaxTree, position, context, cancellationToken); - } + SyntaxKind.AbstractKeyword, + // SyntaxKind.AsyncKeyword, // async methods cannot be byref + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.OverrideKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + // SyntaxKind.ReadOnlyKeyword, // fields cannot be byref + SyntaxKind.SealedKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.VirtualKeyword, + // SyntaxKind.VolatileKeyword, // fields cannot be byref + }; + + /// + /// Same as with ref-specific exclusions + /// + private static readonly ISet RefGlobalMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + // SyntaxKind.AsyncKeyword, // async local functions cannot be byref + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.OverrideKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ReadOnlyKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.VolatileKeyword, + }; + + /// + /// Same as with ref-specific exclusions for C# script + /// + private static readonly ISet RefGlobalMemberScriptModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + // SyntaxKind.AsyncKeyword, // async methods cannot be byref + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.OverrideKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + // SyntaxKind.ReadOnlyKeyword, // fields cannot be byref + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + // SyntaxKind.VolatileKeyword, // fields cannot be byref + }; + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + IsRefParameterModifierContext(position, context) || + IsValidContextForType(context, cancellationToken) || + syntaxTree.IsAnonymousMethodParameterModifierContext(position, context.LeftToken) || + syntaxTree.IsPossibleLambdaParameterModifierContext(position, context.LeftToken, cancellationToken) || + context.TargetToken.IsConstructorOrMethodParameterArgumentContext() || + context.TargetToken.IsXmlCrefParameterModifierContext() || + IsValidNewByRefContext(syntaxTree, position, context, cancellationToken); + } - private static bool IsRefParameterModifierContext(int position, CSharpSyntaxContext context) + private static bool IsRefParameterModifierContext(int position, CSharpSyntaxContext context) + { + if (context.SyntaxTree.IsParameterModifierContext( + position, context.LeftToken, includeOperators: false, out var parameterIndex, out var previousModifier)) { - if (context.SyntaxTree.IsParameterModifierContext( - position, context.LeftToken, includeOperators: false, out var parameterIndex, out var previousModifier)) + if (previousModifier is SyntaxKind.None or SyntaxKind.ScopedKeyword) + { + return true; + } + + if (previousModifier == SyntaxKind.ThisKeyword && + parameterIndex == 0 && + context.SyntaxTree.IsPossibleExtensionMethodContext(context.LeftToken)) { - if (previousModifier is SyntaxKind.None or SyntaxKind.ScopedKeyword) - { - return true; - } - - if (previousModifier == SyntaxKind.ThisKeyword && - parameterIndex == 0 && - context.SyntaxTree.IsPossibleExtensionMethodContext(context.LeftToken)) - { - return true; - } + return true; } + } + + return false; + } + + private static bool IsValidNewByRefContext(SyntaxTree syntaxTree, int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + IsValidRefExpressionContext(context) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, syntaxTree.IsScript() ? RefGlobalMemberScriptModifiers : RefGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: RefMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: true, + cancellationToken: cancellationToken); + } - return false; + private static bool IsValidRefExpressionContext(CSharpSyntaxContext context) + { + // { + // ref var x ... + // + if (context.IsStatementContext) + { + return true; } - private static bool IsValidNewByRefContext(SyntaxTree syntaxTree, int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + // + // ref Goo(int x, ... + // + if (context.IsGlobalStatementContext) { - return - IsValidRefExpressionContext(context) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, syntaxTree.IsScript() ? RefGlobalMemberScriptModifiers : RefGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: RefMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: true, - cancellationToken: cancellationToken); + return true; } - private static bool IsValidRefExpressionContext(CSharpSyntaxContext context) + var token = context.TargetToken; + + switch (token.Kind()) { // { - // ref var x ... + // return ref ... // - if (context.IsStatementContext) - { + case SyntaxKind.ReturnKeyword: return true; - } + // scoped ref ... + case SyntaxKind.ScopedKeyword: + case SyntaxKind.IdentifierToken when token.Text == "scoped": + return true; + + // { + // () => ref ... // - // ref Goo(int x, ... - // - if (context.IsGlobalStatementContext) - { + case SyntaxKind.EqualsGreaterThanToken: return true; - } - var token = context.TargetToken; + // { + // for (ref var x ... + // + // foreach (ref var x ... + // + case SyntaxKind.OpenParenToken: + var previous = token.GetPreviousToken(includeSkipped: true); + return previous.Kind() is SyntaxKind.ForKeyword or SyntaxKind.ForEachKeyword; - switch (token.Kind()) - { - // { - // return ref ... - // - case SyntaxKind.ReturnKeyword: - return true; - - // scoped ref ... - case SyntaxKind.ScopedKeyword: - case SyntaxKind.IdentifierToken when token.Text == "scoped": - return true; - - // { - // () => ref ... - // - case SyntaxKind.EqualsGreaterThanToken: - return true; - - // { - // for (ref var x ... - // - // foreach (ref var x ... - // - case SyntaxKind.OpenParenToken: - var previous = token.GetPreviousToken(includeSkipped: true); - return previous.Kind() is SyntaxKind.ForKeyword or SyntaxKind.ForEachKeyword; - - // { - // ref var x = ref - // - case SyntaxKind.EqualsToken: - var parent = token.Parent; - return parent?.Kind() == SyntaxKind.SimpleAssignmentExpression - || parent?.Parent?.Kind() == SyntaxKind.VariableDeclarator; - - // { - // var x = true ? - // var x = true ? ref y : - case SyntaxKind.QuestionToken: - case SyntaxKind.ColonToken: - return token.Parent?.Kind() == SyntaxKind.ConditionalExpression; - } + // { + // ref var x = ref + // + case SyntaxKind.EqualsToken: + var parent = token.Parent; + return parent?.Kind() == SyntaxKind.SimpleAssignmentExpression + || parent?.Parent?.Kind() == SyntaxKind.VariableDeclarator; - return false; + // { + // var x = true ? + // var x = true ? ref y : + case SyntaxKind.QuestionToken: + case SyntaxKind.ColonToken: + return token.Parent?.Kind() == SyntaxKind.ConditionalExpression; } - private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: true, cancellationToken); - } + return false; + } + + private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: true, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReferenceKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReferenceKeywordRecommender.cs index ba1db0fdfa5ce..08ee7bb6211c7 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReferenceKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReferenceKeywordRecommender.cs @@ -6,22 +6,21 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ReferenceKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ReferenceKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ReferenceKeywordRecommender() + : base(SyntaxKind.ReferenceKeyword, isValidInPreprocessorContext: true) { - public ReferenceKeywordRecommender() - : base(SyntaxKind.ReferenceKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsPreProcessorKeywordContext && - syntaxTree.IsScript() && - syntaxTree.IsBeforeFirstToken(position, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsPreProcessorKeywordContext && + syntaxTree.IsScript() && + syntaxTree.IsBeforeFirstToken(position, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RegionKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RegionKeywordRecommender.cs index ac07f9cfdd5b2..82456ae718e0a 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RegionKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RegionKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class RegionKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class RegionKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public RegionKeywordRecommender() + : base(SyntaxKind.RegionKeyword, isValidInPreprocessorContext: true, shouldFormatOnCommit: true) { - public RegionKeywordRecommender() - : base(SyntaxKind.RegionKeyword, isValidInPreprocessorContext: true, shouldFormatOnCommit: true) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsPreProcessorKeywordContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsPreProcessorKeywordContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RemoveKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RemoveKeywordRecommender.cs index 655be379e3e7b..05fad0d3880bd 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RemoveKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RemoveKeywordRecommender.cs @@ -6,16 +6,15 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class RemoveKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class RemoveKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public RemoveKeywordRecommender() + : base(SyntaxKind.RemoveKeyword) { - public RemoveKeywordRecommender() - : base(SyntaxKind.RemoveKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.RemoveKeyword); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.RemoveKeyword); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RestoreKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RestoreKeywordRecommender.cs index 21c22aa0aa9bd..d061038fa0b36 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RestoreKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RestoreKeywordRecommender.cs @@ -5,31 +5,30 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class RestoreKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class RestoreKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public RestoreKeywordRecommender() + : base(SyntaxKind.RestoreKeyword, isValidInPreprocessorContext: true) { - public RestoreKeywordRecommender() - : base(SyntaxKind.RestoreKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var previousToken1 = context.TargetToken; - var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); - var previousToken3 = previousToken2.GetPreviousToken(includeSkipped: true); + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var previousToken1 = context.TargetToken; + var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); + var previousToken3 = previousToken2.GetPreviousToken(includeSkipped: true); - return - // # pragma warning | - // # pragma warning r| - (previousToken1.Kind() == SyntaxKind.WarningKeyword && - previousToken2.Kind() == SyntaxKind.PragmaKeyword && - previousToken3.Kind() == SyntaxKind.HashToken) || - // # nullable | - // # nullable r| - (previousToken1.Kind() == SyntaxKind.NullableKeyword && - previousToken2.Kind() == SyntaxKind.HashToken); - } + return + // # pragma warning | + // # pragma warning r| + (previousToken1.Kind() == SyntaxKind.WarningKeyword && + previousToken2.Kind() == SyntaxKind.PragmaKeyword && + previousToken3.Kind() == SyntaxKind.HashToken) || + // # nullable | + // # nullable r| + (previousToken1.Kind() == SyntaxKind.NullableKeyword && + previousToken2.Kind() == SyntaxKind.HashToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReturnKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReturnKeywordRecommender.cs index 6528bbdef7421..2935c117c9fc0 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReturnKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ReturnKeywordRecommender.cs @@ -8,39 +8,38 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ReturnKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ReturnKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ReturnKeywordRecommender() + : base(SyntaxKind.ReturnKeyword) { - public ReturnKeywordRecommender() - : base(SyntaxKind.ReturnKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsStatementContext || - context.IsRegularTopLevelStatementsContext() || - context.TargetToken.IsAfterYieldKeyword() || - IsAttributeContext(context, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsStatementContext || + context.IsRegularTopLevelStatementsContext() || + context.TargetToken.IsAfterYieldKeyword() || + IsAttributeContext(context, cancellationToken); + } - private static bool IsAttributeContext(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsMemberAttributeContext(SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, cancellationToken) || - (context.SyntaxTree.IsScript() && context.IsTypeAttributeContext(cancellationToken)) || - context.IsStatementAttributeContext() || - IsAccessorAttributeContext(); + private static bool IsAttributeContext(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsMemberAttributeContext(SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, cancellationToken) || + (context.SyntaxTree.IsScript() && context.IsTypeAttributeContext(cancellationToken)) || + context.IsStatementAttributeContext() || + IsAccessorAttributeContext(); - bool IsAccessorAttributeContext() - { - var token = context.TargetToken; - return token.Kind() == SyntaxKind.OpenBracketToken && - token.Parent is AttributeListSyntax && - token.Parent.Parent is AccessorDeclarationSyntax; - } + bool IsAccessorAttributeContext() + { + var token = context.TargetToken; + return token.Kind() == SyntaxKind.OpenBracketToken && + token.Parent is AttributeListSyntax && + token.Parent.Parent is AccessorDeclarationSyntax; } } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SByteKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SByteKeywordRecommender.cs index 6bf28fbe33a2b..8ed9464600648 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SByteKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SByteKeywordRecommender.cs @@ -9,49 +9,48 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class SByteKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class SByteKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public SByteKeywordRecommender() + : base(SyntaxKind.SByteKeyword) { - public SByteKeywordRecommender() - : base(SyntaxKind.SByteKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsNonAttributeExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsEnumBaseListContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_SByte; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsNonAttributeExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsEnumBaseListContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_SByte; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ScopedKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ScopedKeywordRecommender.cs index c9b969516e2e0..19b4956c80b6a 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ScopedKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ScopedKeywordRecommender.cs @@ -6,43 +6,42 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal sealed class ScopedKeywordRecommender() : AbstractSyntacticSingleKeywordRecommender(SyntaxKind.ScopedKeyword) { - internal sealed class ScopedKeywordRecommender() : AbstractSyntacticSingleKeywordRecommender(SyntaxKind.ScopedKeyword) + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + syntaxTree.IsParameterModifierContext(position, context.LeftToken, includeOperators: true, out _, out _) || + syntaxTree.IsAnonymousMethodParameterModifierContext(position, context.LeftToken) || + syntaxTree.IsPossibleLambdaParameterModifierContext(position, context.LeftToken, cancellationToken) || + IsValidScopedLocalContext(context); + } + + private static bool IsValidScopedLocalContext(CSharpSyntaxContext context) { - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + // scoped ref var x ... + if (context.IsStatementContext || context.IsGlobalStatementContext) { - var syntaxTree = context.SyntaxTree; - return - syntaxTree.IsParameterModifierContext(position, context.LeftToken, includeOperators: true, out _, out _) || - syntaxTree.IsAnonymousMethodParameterModifierContext(position, context.LeftToken) || - syntaxTree.IsPossibleLambdaParameterModifierContext(position, context.LeftToken, cancellationToken) || - IsValidScopedLocalContext(context); + return true; } - private static bool IsValidScopedLocalContext(CSharpSyntaxContext context) + var token = context.TargetToken; + switch (token.Kind()) { - // scoped ref var x ... - if (context.IsStatementContext || context.IsGlobalStatementContext) - { - return true; - } + // for (scoped ref var x ... + // foreach (scoped ... + case SyntaxKind.OpenParenToken: + var previous = token.GetPreviousToken(includeSkipped: true); + return previous.Kind() is SyntaxKind.ForKeyword or SyntaxKind.ForEachKeyword; - var token = context.TargetToken; - switch (token.Kind()) - { - // for (scoped ref var x ... - // foreach (scoped ... - case SyntaxKind.OpenParenToken: - var previous = token.GetPreviousToken(includeSkipped: true); - return previous.Kind() is SyntaxKind.ForKeyword or SyntaxKind.ForEachKeyword; - - // M(out scoped ..) - case SyntaxKind.OutKeyword: - return token.Parent is ArgumentSyntax; - } - - return false; + // M(out scoped ..) + case SyntaxKind.OutKeyword: + return token.Parent is ArgumentSyntax; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SealedKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SealedKeywordRecommender.cs index 520013ad2e6a6..47ea6d582d493 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SealedKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SealedKeywordRecommender.cs @@ -7,68 +7,67 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class SealedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class SealedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.OverrideKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.UnsafeKeyword, - }; + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.OverrideKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.UnsafeKeyword, + }; - private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.OverrideKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword, - }; + private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.OverrideKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + }; - private static readonly ISet s_validTypeModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.FileKeyword, - }; + private static readonly ISet s_validTypeModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.FileKeyword, + }; - public SealedKeywordRecommender() - : base(SyntaxKind.SealedKeyword) - { - } + public SealedKeywordRecommender() + : base(SyntaxKind.SealedKeyword) + { + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsGlobalStatementContext || - context.IsMemberDeclarationContext( - validModifiers: s_validNonInterfaceMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: s_validInterfaceMemberModifiers, - validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken) || - context.IsTypeDeclarationContext( - validModifiers: s_validTypeModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsGlobalStatementContext || + context.IsMemberDeclarationContext( + validModifiers: s_validNonInterfaceMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: s_validInterfaceMemberModifiers, + validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken) || + context.IsTypeDeclarationContext( + validModifiers: s_validTypeModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SelectKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SelectKeywordRecommender.cs index 5f0fd2cbb721c..628dc12cd2a45 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SelectKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SelectKeywordRecommender.cs @@ -6,34 +6,33 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class SelectKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class SelectKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public SelectKeywordRecommender() + : base(SyntaxKind.SelectKeyword) { - public SelectKeywordRecommender() - : base(SyntaxKind.SelectKeyword) + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var token = context.TargetToken; + + // for orderby, ascending is the default so select should be available in the orderby direction context + if (token.IsOrderByDirectionContext()) { + return true; } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + // var q = from x in y + // | + if (!token.IntersectsWith(position) && + token.IsLastTokenOfQueryClause()) { - var token = context.TargetToken; - - // for orderby, ascending is the default so select should be available in the orderby direction context - if (token.IsOrderByDirectionContext()) - { - return true; - } - - // var q = from x in y - // | - if (!token.IntersectsWith(position) && - token.IsLastTokenOfQueryClause()) - { - return true; - } - - return false; + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SetKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SetKeywordRecommender.cs index 3d9cd20a6d659..b814c8efe45b6 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SetKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SetKeywordRecommender.cs @@ -6,20 +6,19 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class SetKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class SetKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public SetKeywordRecommender() + : base(SyntaxKind.SetKeyword) { - public SetKeywordRecommender() - : base(SyntaxKind.SetKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.SetKeyword) || - context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.SetKeyword); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.SetKeyword) || + context.TargetToken.IsAccessorDeclarationContext(position, SyntaxKind.SetKeyword); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ShortKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ShortKeywordRecommender.cs index 98d449fcdd4bd..ec64422318929 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ShortKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ShortKeywordRecommender.cs @@ -9,49 +9,48 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ShortKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class ShortKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public ShortKeywordRecommender() + : base(SyntaxKind.ShortKeyword) { - public ShortKeywordRecommender() - : base(SyntaxKind.ShortKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsEnumBaseListContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_Int16; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsEnumBaseListContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_Int16; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SizeOfKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SizeOfKeywordRecommender.cs index d46e0a34c40b9..10817a23ddd0c 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SizeOfKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SizeOfKeywordRecommender.cs @@ -6,21 +6,20 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class SizeOfKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class SizeOfKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public SizeOfKeywordRecommender() + : base(SyntaxKind.SizeOfKeyword) { - public SizeOfKeywordRecommender() - : base(SyntaxKind.SizeOfKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsNonAttributeExpressionContext || - context.IsStatementContext || - context.IsGlobalStatementContext; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsNonAttributeExpressionContext || + context.IsStatementContext || + context.IsGlobalStatementContext; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StackAllocKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StackAllocKeywordRecommender.cs index 3e8975d80a1ad..79e12b7bc40c7 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StackAllocKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StackAllocKeywordRecommender.cs @@ -5,22 +5,21 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class StackAllocKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class StackAllocKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public StackAllocKeywordRecommender() + : base(SyntaxKind.StackAllocKeyword) { - public StackAllocKeywordRecommender() - : base(SyntaxKind.StackAllocKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - // Beginning with C# 8.0, stackalloc expression can be used inside other expressions - // whenever a Span or ReadOnlySpan variable is allowed. - return (context.IsAnyExpressionContext && !context.IsConstantExpressionContext) || - context.IsStatementContext || - context.IsGlobalStatementContext; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // Beginning with C# 8.0, stackalloc expression can be used inside other expressions + // whenever a Span or ReadOnlySpan variable is allowed. + return (context.IsAnyExpressionContext && !context.IsConstantExpressionContext) || + context.IsStatementContext || + context.IsGlobalStatementContext; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StaticKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StaticKeywordRecommender.cs index 8b6ac5fc86e4d..6ecd476970359 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StaticKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StaticKeywordRecommender.cs @@ -8,111 +8,110 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class StaticKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class StaticKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validTypeModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - private static readonly ISet s_validTypeModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.FileKeyword, - }; - - private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.AsyncKeyword, - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.ReadOnlyKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.VolatileKeyword, - }; + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.FileKeyword, + }; - private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.AbstractKeyword, - SyntaxKind.AsyncKeyword, - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.ReadOnlyKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.VolatileKeyword, - SyntaxKind.VirtualKeyword, - }; + private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.AsyncKeyword, + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.ReadOnlyKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.VolatileKeyword, + }; - private static readonly ISet s_validGlobalMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ReadOnlyKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.VolatileKeyword, - }; + private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.AbstractKeyword, + SyntaxKind.AsyncKeyword, + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.ReadOnlyKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.VolatileKeyword, + SyntaxKind.VirtualKeyword, + }; - private static readonly ISet s_validLocalFunctionModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.ExternKeyword, - SyntaxKind.AsyncKeyword, - SyntaxKind.UnsafeKeyword - }; + private static readonly ISet s_validGlobalMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ReadOnlyKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.VolatileKeyword, + }; - public StaticKeywordRecommender() - : base(SyntaxKind.StaticKeyword) - { - } + private static readonly ISet s_validLocalFunctionModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.ExternKeyword, + SyntaxKind.AsyncKeyword, + SyntaxKind.UnsafeKeyword + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsGlobalStatementContext || - context.TargetToken.IsUsingKeywordInUsingDirective() || - (context.TargetToken.IsKind(SyntaxKind.UsingKeyword) && context.TargetToken.Parent?.IsParentKind(SyntaxKind.GlobalStatement) == true) || - IsValidContextForType(context, cancellationToken) || - IsValidContextForMember(context, cancellationToken) || - context.SyntaxTree.IsLambdaDeclarationContext(position, otherModifier: SyntaxKind.AsyncKeyword, cancellationToken) || - context.SyntaxTree.IsLocalFunctionDeclarationContext(position, s_validLocalFunctionModifiers, cancellationToken); - } + public StaticKeywordRecommender() + : base(SyntaxKind.StaticKeyword) + { + } - private static bool IsValidContextForMember(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, s_validGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: s_validNonInterfaceMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: s_validInterfaceMemberModifiers, - validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsGlobalStatementContext || + context.TargetToken.IsUsingKeywordInUsingDirective() || + (context.TargetToken.IsKind(SyntaxKind.UsingKeyword) && context.TargetToken.Parent?.IsParentKind(SyntaxKind.GlobalStatement) == true) || + IsValidContextForType(context, cancellationToken) || + IsValidContextForMember(context, cancellationToken) || + context.SyntaxTree.IsLambdaDeclarationContext(position, otherModifier: SyntaxKind.AsyncKeyword, cancellationToken) || + context.SyntaxTree.IsLocalFunctionDeclarationContext(position, s_validLocalFunctionModifiers, cancellationToken); + } - private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return context.IsTypeDeclarationContext( - validModifiers: s_validTypeModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + private static bool IsValidContextForMember(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, s_validGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: s_validNonInterfaceMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: s_validInterfaceMemberModifiers, + validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken); - } + } + + private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return context.IsTypeDeclarationContext( + validModifiers: s_validTypeModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StringKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StringKeywordRecommender.cs index 58a03f823d435..d857bb1213275 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StringKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StringKeywordRecommender.cs @@ -11,47 +11,46 @@ using System; using System.Linq; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal sealed class StringKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal sealed class StringKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public StringKeywordRecommender() + : base(SyntaxKind.StringKeyword) { - public StringKeywordRecommender() - : base(SyntaxKind.StringKeyword) - { - } + } - protected override SpecialType SpecialType => SpecialType.System_String; + protected override SpecialType SpecialType => SpecialType.System_String; - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsTypeOfExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsDefaultExpressionContext(position, context.LeftToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken); - } + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsTypeOfExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsDefaultExpressionContext(position, context.LeftToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StructKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StructKeywordRecommender.cs index 004c337a4ca8c..d2697d8333a1a 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StructKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/StructKeywordRecommender.cs @@ -7,39 +7,38 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders -{ - internal class StructKeywordRecommender : AbstractSyntacticSingleKeywordRecommender - { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.InternalKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.RefKeyword, - SyntaxKind.ReadOnlyKeyword, - SyntaxKind.FileKeyword, - }; +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; - public StructKeywordRecommender() - : base(SyntaxKind.StructKeyword) +internal class StructKeywordRecommender : AbstractSyntacticSingleKeywordRecommender +{ + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - } + SyntaxKind.InternalKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.RefKeyword, + SyntaxKind.ReadOnlyKeyword, + SyntaxKind.FileKeyword, + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsGlobalStatementContext || - context.IsTypeDeclarationContext( - validModifiers: s_validModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: true, - cancellationToken: cancellationToken) || - context.IsRecordDeclarationContext(s_validModifiers, cancellationToken) || - syntaxTree.IsTypeParameterConstraintStartContext(position, context.LeftToken); - } + public StructKeywordRecommender() + : base(SyntaxKind.StructKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsGlobalStatementContext || + context.IsTypeDeclarationContext( + validModifiers: s_validModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: true, + cancellationToken: cancellationToken) || + context.IsRecordDeclarationContext(s_validModifiers, cancellationToken) || + syntaxTree.IsTypeParameterConstraintStartContext(position, context.LeftToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SwitchKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SwitchKeywordRecommender.cs index a3f2e7cdbdfbb..37dd1c2392afd 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SwitchKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/SwitchKeywordRecommender.cs @@ -5,21 +5,20 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class SwitchKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class SwitchKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public SwitchKeywordRecommender() + : base(SyntaxKind.SwitchKeyword) { - public SwitchKeywordRecommender() - : base(SyntaxKind.SwitchKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsIsOrAsOrSwitchOrWithExpressionContext; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsIsOrAsOrSwitchOrWithExpressionContext; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThisKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThisKeywordRecommender.cs index a15f5f743d141..5e199f9602345 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThisKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThisKeywordRecommender.cs @@ -9,81 +9,80 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ThisKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ThisKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ThisKeywordRecommender() + : base(SyntaxKind.ThisKeyword) { - public ThisKeywordRecommender() - : base(SyntaxKind.ThisKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - IsInstanceExpressionOrStatement(context) || - IsThisParameterModifierContext(context) || - IsConstructorInitializerContext(context); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + IsInstanceExpressionOrStatement(context) || + IsThisParameterModifierContext(context) || + IsConstructorInitializerContext(context); + } - private static bool IsInstanceExpressionOrStatement(CSharpSyntaxContext context) + private static bool IsInstanceExpressionOrStatement(CSharpSyntaxContext context) + { + if (context.IsInstanceContext) { - if (context.IsInstanceContext) - { - return context.IsNonAttributeExpressionContext || context.IsStatementContext; - } - - return false; + return context.IsNonAttributeExpressionContext || context.IsStatementContext; } - private static bool IsConstructorInitializerContext(CSharpSyntaxContext context) - { - // cases: - // Goo() : | + return false; + } - var token = context.TargetToken; + private static bool IsConstructorInitializerContext(CSharpSyntaxContext context) + { + // cases: + // Goo() : | - if (token.Kind() == SyntaxKind.ColonToken && - token.Parent is ConstructorInitializerSyntax && - token.Parent.IsParentKind(SyntaxKind.ConstructorDeclaration)) - { - var constructor = token.GetRequiredAncestor(); - if (constructor.Modifiers.Any(SyntaxKind.StaticKeyword)) - { - return false; - } + var token = context.TargetToken; - return true; + if (token.Kind() == SyntaxKind.ColonToken && + token.Parent is ConstructorInitializerSyntax && + token.Parent.IsParentKind(SyntaxKind.ConstructorDeclaration)) + { + var constructor = token.GetRequiredAncestor(); + if (constructor.Modifiers.Any(SyntaxKind.StaticKeyword)) + { + return false; } - return false; + return true; } - private static bool IsThisParameterModifierContext(CSharpSyntaxContext context) + return false; + } + + private static bool IsThisParameterModifierContext(CSharpSyntaxContext context) + { + if (context.SyntaxTree.IsParameterModifierContext( + context.Position, context.LeftToken, includeOperators: false, out var parameterIndex, out var previousModifier)) { - if (context.SyntaxTree.IsParameterModifierContext( - context.Position, context.LeftToken, includeOperators: false, out var parameterIndex, out var previousModifier)) + if (previousModifier is SyntaxKind.None or + SyntaxKind.RefKeyword or + SyntaxKind.InKeyword or + SyntaxKind.ReadOnlyKeyword) { - if (previousModifier is SyntaxKind.None or - SyntaxKind.RefKeyword or - SyntaxKind.InKeyword or - SyntaxKind.ReadOnlyKeyword) + if (parameterIndex == 0 && + context.SyntaxTree.IsPossibleExtensionMethodContext(context.LeftToken)) { - if (parameterIndex == 0 && - context.SyntaxTree.IsPossibleExtensionMethodContext(context.LeftToken)) - { - return true; - } + return true; } } - - return false; } - protected override bool ShouldPreselect(CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var outerType = context.SemanticModel.GetEnclosingNamedType(context.Position, cancellationToken); - return context.InferredTypes.Any(static (t, outerType) => Equals(t, outerType), outerType); - } + return false; + } + + protected override bool ShouldPreselect(CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var outerType = context.SemanticModel.GetEnclosingNamedType(context.Position, cancellationToken); + return context.InferredTypes.Any(static (t, outerType) => Equals(t, outerType), outerType); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThrowKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThrowKeywordRecommender.cs index 125285da8e96a..9b5c3719e4a22 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThrowKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThrowKeywordRecommender.cs @@ -5,43 +5,42 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ThrowKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class ThrowKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public ThrowKeywordRecommender() + : base(SyntaxKind.ThrowKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) { - public ThrowKeywordRecommender() - : base(SyntaxKind.ThrowKeyword) + if (context.IsStatementContext || context.IsGlobalStatementContext) { + return true; } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + // void M() => throw + if (context.TargetToken.IsKind(SyntaxKind.EqualsGreaterThanToken)) { - if (context.IsStatementContext || context.IsGlobalStatementContext) - { - return true; - } - - // void M() => throw - if (context.TargetToken.IsKind(SyntaxKind.EqualsGreaterThanToken)) - { - return true; - } - - // val ?? throw - if (context.TargetToken.IsKind(SyntaxKind.QuestionQuestionToken)) - { - return true; - } - - // expr ? throw : ... - // expr ? ... : throw - if (context.TargetToken.Kind() is SyntaxKind.QuestionToken or - SyntaxKind.ColonToken) - { - return context.TargetToken.Parent.IsKind(SyntaxKind.ConditionalExpression); - } - - return false; + return true; } + + // val ?? throw + if (context.TargetToken.IsKind(SyntaxKind.QuestionQuestionToken)) + { + return true; + } + + // expr ? throw : ... + // expr ? ... : throw + if (context.TargetToken.Kind() is SyntaxKind.QuestionToken or + SyntaxKind.ColonToken) + { + return context.TargetToken.Parent.IsKind(SyntaxKind.ConditionalExpression); + } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TrueKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TrueKeywordRecommender.cs index b417aac71d79a..87df7de42f896 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TrueKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TrueKeywordRecommender.cs @@ -6,23 +6,22 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class TrueKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class TrueKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public TrueKeywordRecommender() + : base(SyntaxKind.TrueKeyword, isValidInPreprocessorContext: true) { - public TrueKeywordRecommender() - : base(SyntaxKind.TrueKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsAnyExpressionContext || - context.IsPreProcessorExpressionContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.TargetToken.IsUnaryOperatorContext(); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsAnyExpressionContext || + context.IsPreProcessorExpressionContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.TargetToken.IsUnaryOperatorContext(); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TryKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TryKeywordRecommender.cs index c88607a1cadc7..61d43d87fb380 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TryKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TryKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class TryKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class TryKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public TryKeywordRecommender() + : base(SyntaxKind.TryKeyword) { - public TryKeywordRecommender() - : base(SyntaxKind.TryKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsStatementContext || context.IsGlobalStatementContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsStatementContext || context.IsGlobalStatementContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeKeywordRecommender.cs index 33d3eb451efd8..cbd7efb89671e 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class TypeKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class TypeKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public TypeKeywordRecommender() + : base(SyntaxKind.TypeKeyword) { - public TypeKeywordRecommender() - : base(SyntaxKind.TypeKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsTypeAttributeContext(cancellationToken); } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsTypeAttributeContext(cancellationToken); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeOfKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeOfKeywordRecommender.cs index fb8028c34edaf..715a2fa956187 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeOfKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeOfKeywordRecommender.cs @@ -8,29 +8,28 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class TypeOfKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class TypeOfKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public TypeOfKeywordRecommender() + : base(SyntaxKind.TypeOfKeyword) { - public TypeOfKeywordRecommender() - : base(SyntaxKind.TypeOfKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - (context.IsAnyExpressionContext && !context.IsConstantExpressionContext) || - context.IsStatementContext || - context.IsGlobalStatementContext || - IsAttributeArgumentContext(context); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + (context.IsAnyExpressionContext && !context.IsConstantExpressionContext) || + context.IsStatementContext || + context.IsGlobalStatementContext || + IsAttributeArgumentContext(context); + } - private static bool IsAttributeArgumentContext(CSharpSyntaxContext context) - { - return - context.IsAnyExpressionContext && - context.LeftToken.GetAncestor() != null; - } + private static bool IsAttributeArgumentContext(CSharpSyntaxContext context) + { + return + context.IsAnyExpressionContext && + context.LeftToken.GetAncestor() != null; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeVarKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeVarKeywordRecommender.cs index 2bb4b7528773b..50bbe50118d7c 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeVarKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/TypeVarKeywordRecommender.cs @@ -7,44 +7,43 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class TypeVarKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class TypeVarKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public TypeVarKeywordRecommender() + : base(SyntaxKind.TypeVarKeyword) { - public TypeVarKeywordRecommender() - : base(SyntaxKind.TypeVarKeyword) - { - } + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var token = context.TargetToken; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + if (token.Kind() == SyntaxKind.OpenBracketToken && + token.Parent.IsKind(SyntaxKind.AttributeList)) { - var token = context.TargetToken; + var typeParameters = token.GetAncestor(); + var type = typeParameters.GetAncestorOrThis(); - if (token.Kind() == SyntaxKind.OpenBracketToken && - token.Parent.IsKind(SyntaxKind.AttributeList)) + if (type != null && type.TypeParameterList == typeParameters) { - var typeParameters = token.GetAncestor(); - var type = typeParameters.GetAncestorOrThis(); - - if (type != null && type.TypeParameterList == typeParameters) - { - return true; - } - - var @delegate = typeParameters.GetAncestorOrThis(); - if (@delegate != null && @delegate.TypeParameterList == typeParameters) - { - return true; - } - - var method = typeParameters.GetAncestorOrThis(); - if (method != null && method.TypeParameterList == typeParameters) - { - return true; - } + return true; } - return false; + var @delegate = typeParameters.GetAncestorOrThis(); + if (@delegate != null && @delegate.TypeParameterList == typeParameters) + { + return true; + } + + var method = typeParameters.GetAncestorOrThis(); + if (method != null && method.TypeParameterList == typeParameters) + { + return true; + } } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UIntKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UIntKeywordRecommender.cs index f69da3c06743f..4a65540388bfe 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UIntKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UIntKeywordRecommender.cs @@ -9,49 +9,48 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class UIntKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class UIntKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public UIntKeywordRecommender() + : base(SyntaxKind.UIntKeyword) { - public UIntKeywordRecommender() - : base(SyntaxKind.UIntKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsEnumBaseListContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_UInt32; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsEnumBaseListContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_UInt32; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ULongKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ULongKeywordRecommender.cs index bb5884c9394dc..11b33880b5da6 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ULongKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ULongKeywordRecommender.cs @@ -9,49 +9,48 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class ULongKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class ULongKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public ULongKeywordRecommender() + : base(SyntaxKind.ULongKeyword) { - public ULongKeywordRecommender() - : base(SyntaxKind.ULongKeyword) - { - } - - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsEnumBaseListContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + } - protected override SpecialType SpecialType => SpecialType.System_UInt64; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsEnumBaseListContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_UInt64; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UShortKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UShortKeywordRecommender.cs index 1e9a0f9a578f8..d97ca289b7d34 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UShortKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UShortKeywordRecommender.cs @@ -10,55 +10,54 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class UShortKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender { - internal class UShortKeywordRecommender : AbstractSpecialTypePreselectingKeywordRecommender + public UShortKeywordRecommender() + : base(SyntaxKind.UShortKeyword) { - public UShortKeywordRecommender() - : base(SyntaxKind.UShortKeyword) - { - } - - /// - /// We set the of this item less than the default value so that - /// completion selects the keyword over it as the user starts typing. - /// - protected override int DefaultMatchPriority => MatchPriority.Default - 1; + } - protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsAnyExpressionContext || - context.IsDefiniteCastTypeContext || - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsObjectCreationTypeContext || - (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || - context.IsFunctionPointerTypeArgumentContext || - context.IsEnumBaseListContext || - context.IsIsOrAsTypeContext || - context.IsLocalVariableDeclarationContext || - context.IsFixedVariableDeclarationContext || - context.IsParameterTypeContext || - context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || - context.IsLocalFunctionDeclarationContext || - context.IsImplicitOrExplicitOperatorTypeContext || - context.IsPrimaryFunctionExpressionContext || - context.IsCrefContext || - context.IsUsingAliasTypeContext || - syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || - syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || - context.IsDelegateReturnTypeContext || - syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsPossibleTupleContext || - context.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AllMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + /// + /// We set the of this item less than the default value so that + /// completion selects the keyword over it as the user starts typing. + /// + protected override int DefaultMatchPriority => MatchPriority.Default - 1; - protected override SpecialType SpecialType => SpecialType.System_UInt16; + protected override bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsAnyExpressionContext || + context.IsDefiniteCastTypeContext || + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsObjectCreationTypeContext || + (context.IsGenericTypeArgumentContext && !context.TargetToken.GetRequiredParent().HasAncestor()) || + context.IsFunctionPointerTypeArgumentContext || + context.IsEnumBaseListContext || + context.IsIsOrAsTypeContext || + context.IsLocalVariableDeclarationContext || + context.IsFixedVariableDeclarationContext || + context.IsParameterTypeContext || + context.IsPossibleLambdaOrAnonymousMethodParameterTypeContext || + context.IsLocalFunctionDeclarationContext || + context.IsImplicitOrExplicitOperatorTypeContext || + context.IsPrimaryFunctionExpressionContext || + context.IsCrefContext || + context.IsUsingAliasTypeContext || + syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || + syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) || + context.IsDelegateReturnTypeContext || + syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsPossibleTupleContext || + context.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AllMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } + + protected override SpecialType SpecialType => SpecialType.System_UInt16; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UncheckedKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UncheckedKeywordRecommender.cs index 38480d478b626..9edcd2becf758 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UncheckedKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UncheckedKeywordRecommender.cs @@ -5,21 +5,20 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class UncheckedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class UncheckedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public UncheckedKeywordRecommender() + : base(SyntaxKind.UncheckedKeyword) { - public UncheckedKeywordRecommender() - : base(SyntaxKind.UncheckedKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsNonAttributeExpressionContext; - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsNonAttributeExpressionContext; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UndefKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UndefKeywordRecommender.cs index 2a30ed9b6d52c..18aacb19c9812 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UndefKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UndefKeywordRecommender.cs @@ -6,21 +6,20 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class UndefKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class UndefKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public UndefKeywordRecommender() + : base(SyntaxKind.UndefKeyword, isValidInPreprocessorContext: true) { - public UndefKeywordRecommender() - : base(SyntaxKind.UndefKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsPreProcessorKeywordContext && - syntaxTree.IsBeforeFirstToken(position, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsPreProcessorKeywordContext && + syntaxTree.IsBeforeFirstToken(position, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UnmanagedKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UnmanagedKeywordRecommender.cs index 774f87d665fb9..a1745d7c8f2e8 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UnmanagedKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UnmanagedKeywordRecommender.cs @@ -5,19 +5,18 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class UnmanagedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class UnmanagedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public UnmanagedKeywordRecommender() + : base(SyntaxKind.UnmanagedKeyword) { - public UnmanagedKeywordRecommender() - : base(SyntaxKind.UnmanagedKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return context.SyntaxTree.IsTypeParameterConstraintContext(position, context.LeftToken) || - context.SyntaxTree.IsFunctionPointerCallingConventionContext(context.TargetToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return context.SyntaxTree.IsTypeParameterConstraintContext(position, context.LeftToken) || + context.SyntaxTree.IsFunctionPointerCallingConventionContext(context.TargetToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UnsafeKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UnsafeKeywordRecommender.cs index 07b33e997f13a..df0bb5739fa09 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UnsafeKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UnsafeKeywordRecommender.cs @@ -7,79 +7,78 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class UnsafeKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class UnsafeKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validTypeModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - private static readonly ISet s_validTypeModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.AbstractKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.FileKeyword, - }; + SyntaxKind.AbstractKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.FileKeyword, + }; - private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.AbstractKeyword, - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.OverrideKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.ReadOnlyKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.VirtualKeyword, - SyntaxKind.VolatileKeyword, - }; + private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.AbstractKeyword, + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.OverrideKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.ReadOnlyKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.VirtualKeyword, + SyntaxKind.VolatileKeyword, + }; - private static readonly ISet s_validGlobalMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ReadOnlyKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.VolatileKeyword, - }; + private static readonly ISet s_validGlobalMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ReadOnlyKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.VolatileKeyword, + }; - private static readonly ISet s_validLocalFunctionModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.ExternKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.AsyncKeyword - }; + private static readonly ISet s_validLocalFunctionModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.ExternKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.AsyncKeyword + }; - public UnsafeKeywordRecommender() - : base(SyntaxKind.UnsafeKeyword) - { - } + public UnsafeKeywordRecommender() + : base(SyntaxKind.UnsafeKeyword) + { + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsTypeDeclarationContext(validModifiers: s_validTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken) || - syntaxTree.IsGlobalMemberDeclarationContext(position, s_validGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: s_validMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken) || - syntaxTree.IsLocalFunctionDeclarationContext(position, s_validLocalFunctionModifiers, cancellationToken) || - (context.IsInImportsDirective && context.TargetToken.Kind() is SyntaxKind.UsingKeyword or SyntaxKind.StaticKeyword); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsTypeDeclarationContext(validModifiers: s_validTypeModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: false, cancellationToken: cancellationToken) || + syntaxTree.IsGlobalMemberDeclarationContext(position, s_validGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: s_validMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken) || + syntaxTree.IsLocalFunctionDeclarationContext(position, s_validLocalFunctionModifiers, cancellationToken) || + (context.IsInImportsDirective && context.TargetToken.Kind() is SyntaxKind.UsingKeyword or SyntaxKind.StaticKeyword); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UsingKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UsingKeywordRecommender.cs index 7cc0f0983a6a5..4cdafe0a3aa7b 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UsingKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UsingKeywordRecommender.cs @@ -7,175 +7,174 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class UsingKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class UsingKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public UsingKeywordRecommender() + : base(SyntaxKind.UsingKeyword) { - public UsingKeywordRecommender() - : base(SyntaxKind.UsingKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - // cases: - // using (goo) { } - // using Goo; - // using Goo = Bar; - // await using (goo) { } - return - context.IsStatementContext || - context.IsGlobalStatementContext || - IsUsingDirectiveContext(context, forGlobalKeyword: false, cancellationToken) || - context.IsAwaitStatementContext(position, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // cases: + // using (goo) { } + // using Goo; + // using Goo = Bar; + // await using (goo) { } + return + context.IsStatementContext || + context.IsGlobalStatementContext || + IsUsingDirectiveContext(context, forGlobalKeyword: false, cancellationToken) || + context.IsAwaitStatementContext(position, cancellationToken); + } - internal static bool IsUsingDirectiveContext(CSharpSyntaxContext context, bool forGlobalKeyword, CancellationToken cancellationToken) - { - // cases: - // root: | + internal static bool IsUsingDirectiveContext(CSharpSyntaxContext context, bool forGlobalKeyword, CancellationToken cancellationToken) + { + // cases: + // root: | - // root: u| + // root: u| - // extern alias a; - // | + // extern alias a; + // | - // extern alias a; - // u| + // extern alias a; + // u| - // using Goo; - // | + // using Goo; + // | - // using Goo; - // u| + // using Goo; + // u| - // using Goo = Bar; - // | + // using Goo = Bar; + // | - // using Goo = Bar; - // u| + // using Goo = Bar; + // u| - // t valid: - // namespace N {} - // | + // t valid: + // namespace N {} + // | - // namespace N {} - // u| + // namespace N {} + // u| - // class C {} - // | + // class C {} + // | - // class C {} - // u| + // class C {} + // u| - // | - // extern alias a; + // | + // extern alias a; - // u| - // extern alias a; + // u| + // extern alias a; - var originalToken = context.LeftToken; - var token = context.TargetToken; + var originalToken = context.LeftToken; + var token = context.TargetToken; - // root: u| + // root: u| - // namespace N { u| + // namespace N { u| - // namespace N; u| + // namespace N; u| - // extern alias a; - // u| + // extern alias a; + // u| - // using Goo; - // u| + // using Goo; + // u| - // using Goo = Bar; - // u| + // using Goo = Bar; + // u| - // root: | - if (token.Kind() == SyntaxKind.None) - { - // root namespace + // root: | + if (token.Kind() == SyntaxKind.None) + { + // root namespace - return IsValidContextAtTheRoot(context, originalToken, cancellationToken); - } + return IsValidContextAtTheRoot(context, originalToken, cancellationToken); + } - if ((token.Kind() == SyntaxKind.OpenBraceToken && token.Parent.IsKind(SyntaxKind.NamespaceDeclaration)) - || (token.Kind() == SyntaxKind.SemicolonToken && token.Parent.IsKind(SyntaxKind.FileScopedNamespaceDeclaration))) + if ((token.Kind() == SyntaxKind.OpenBraceToken && token.Parent.IsKind(SyntaxKind.NamespaceDeclaration)) + || (token.Kind() == SyntaxKind.SemicolonToken && token.Parent.IsKind(SyntaxKind.FileScopedNamespaceDeclaration))) + { + // a child using can't come before externs + var nextToken = originalToken.GetNextToken(includeSkipped: true); + if (nextToken.Kind() == SyntaxKind.ExternKeyword) { - // a child using can't come before externs - var nextToken = originalToken.GetNextToken(includeSkipped: true); - if (nextToken.Kind() == SyntaxKind.ExternKeyword) - { - return false; - } + return false; + } + + return true; + } + // extern alias a; + // | + + // using Goo; + // | + if (token.Kind() == SyntaxKind.SemicolonToken) + { + if (token.Parent is (kind: SyntaxKind.ExternAliasDirective or SyntaxKind.UsingDirective)) + { return true; } + } + + // extern alias a; + // global | - // extern alias a; - // | + // global using Goo; + // global | - // using Goo; - // | - if (token.Kind() == SyntaxKind.SemicolonToken) + // global | + if (!forGlobalKeyword) + { + var previousToken = token.GetPreviousToken(includeSkipped: true); + if (previousToken.Kind() == SyntaxKind.None) { - if (token.Parent is (kind: SyntaxKind.ExternAliasDirective or SyntaxKind.UsingDirective)) + // root namespace + if (token.Kind() == SyntaxKind.GlobalKeyword) { return true; } + else if (token.Kind() == SyntaxKind.IdentifierToken && SyntaxFacts.GetContextualKeywordKind((string)token.Value!) == SyntaxKind.GlobalKeyword) + { + return IsValidContextAtTheRoot(context, originalToken, cancellationToken); + } } - - // extern alias a; - // global | - - // global using Goo; - // global | - - // global | - if (!forGlobalKeyword) + else if (previousToken.Kind() == SyntaxKind.SemicolonToken && + previousToken.Parent is (kind: SyntaxKind.ExternAliasDirective or SyntaxKind.UsingDirective)) { - var previousToken = token.GetPreviousToken(includeSkipped: true); - if (previousToken.Kind() == SyntaxKind.None) + if (token.Kind() == SyntaxKind.GlobalKeyword) { - // root namespace - if (token.Kind() == SyntaxKind.GlobalKeyword) - { - return true; - } - else if (token.Kind() == SyntaxKind.IdentifierToken && SyntaxFacts.GetContextualKeywordKind((string)token.Value!) == SyntaxKind.GlobalKeyword) - { - return IsValidContextAtTheRoot(context, originalToken, cancellationToken); - } + return true; } - else if (previousToken.Kind() == SyntaxKind.SemicolonToken && - previousToken.Parent is (kind: SyntaxKind.ExternAliasDirective or SyntaxKind.UsingDirective)) + else if (token.Kind() == SyntaxKind.IdentifierToken && SyntaxFacts.GetContextualKeywordKind((string)token.Value!) == SyntaxKind.GlobalKeyword) { - if (token.Kind() == SyntaxKind.GlobalKeyword) - { - return true; - } - else if (token.Kind() == SyntaxKind.IdentifierToken && SyntaxFacts.GetContextualKeywordKind((string)token.Value!) == SyntaxKind.GlobalKeyword) - { - return true; - } + return true; } } + } - return false; + return false; - static bool IsValidContextAtTheRoot(CSharpSyntaxContext context, SyntaxToken originalToken, CancellationToken cancellationToken) + static bool IsValidContextAtTheRoot(CSharpSyntaxContext context, SyntaxToken originalToken, CancellationToken cancellationToken) + { + // a using can't come before externs + var nextToken = originalToken.GetNextToken(includeSkipped: true); + if (nextToken.Kind() == SyntaxKind.ExternKeyword || + ((CompilationUnitSyntax)context.SyntaxTree.GetRoot(cancellationToken)).Externs.Count > 0) { - // a using can't come before externs - var nextToken = originalToken.GetNextToken(includeSkipped: true); - if (nextToken.Kind() == SyntaxKind.ExternKeyword || - ((CompilationUnitSyntax)context.SyntaxTree.GetRoot(cancellationToken)).Externs.Count > 0) - { - return false; - } - - return true; + return false; } + + return true; } } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VarKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VarKeywordRecommender.cs index 542523ea07ee3..1fb8f1fc15076 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VarKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VarKeywordRecommender.cs @@ -7,32 +7,31 @@ using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class VarKeywordRecommender : IKeywordRecommender { - internal class VarKeywordRecommender : IKeywordRecommender + public VarKeywordRecommender() { - public VarKeywordRecommender() - { - } + } - private static bool IsValidContext(CSharpSyntaxContext context) + private static bool IsValidContext(CSharpSyntaxContext context) + { + if (context.IsStatementContext || + context.IsGlobalStatementContext || + context.IsPossibleTupleContext || + context.IsAtStartOfPattern) { - if (context.IsStatementContext || - context.IsGlobalStatementContext || - context.IsPossibleTupleContext || - context.IsAtStartOfPattern) - { - return true; - } - - return context.IsLocalVariableDeclarationContext; + return true; } - public ImmutableArray RecommendKeywords(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return IsValidContext(context) - ? [new RecommendedKeyword("var")] - : []; - } + return context.IsLocalVariableDeclarationContext; + } + + public ImmutableArray RecommendKeywords(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return IsValidContext(context) + ? [new RecommendedKeyword("var")] + : []; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VirtualKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VirtualKeywordRecommender.cs index d341d5bd1cd7f..10ffd1b011c64 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VirtualKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VirtualKeywordRecommender.cs @@ -7,56 +7,55 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class VirtualKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class VirtualKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - private static readonly ISet s_validNonInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.UnsafeKeyword, - }; + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.UnsafeKeyword, + }; - private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.ExternKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword, - }; + private static readonly ISet s_validInterfaceMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.ExternKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + }; - public VirtualKeywordRecommender() - : base(SyntaxKind.VirtualKeyword) - { - } + public VirtualKeywordRecommender() + : base(SyntaxKind.VirtualKeyword) + { + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (!context.IsMemberDeclarationContext( + validModifiers: s_validNonInterfaceMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken) && + !context.IsMemberDeclarationContext( + validModifiers: s_validInterfaceMemberModifiers, + validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken)) { - if (!context.IsMemberDeclarationContext( - validModifiers: s_validNonInterfaceMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken) && - !context.IsMemberDeclarationContext( - validModifiers: s_validInterfaceMemberModifiers, - validTypeDeclarations: SyntaxKindSet.InterfaceOnlyTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken)) - { - return false; - } - - var modifiers = context.PrecedingModifiers; - return !modifiers.Contains(SyntaxKind.PrivateKeyword) || modifiers.Contains(SyntaxKind.ProtectedKeyword); + return false; } + + var modifiers = context.PrecedingModifiers; + return !modifiers.Contains(SyntaxKind.PrivateKeyword) || modifiers.Contains(SyntaxKind.ProtectedKeyword); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs index 81b9c076816d0..199bf26b52b66 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs @@ -8,109 +8,108 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal sealed class VoidKeywordRecommender() : AbstractSyntacticSingleKeywordRecommender(SyntaxKind.VoidKeyword) { - internal sealed class VoidKeywordRecommender() : AbstractSyntacticSingleKeywordRecommender(SyntaxKind.VoidKeyword) + private static readonly ISet s_validClassInterfaceRecordModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - private static readonly ISet s_validClassInterfaceRecordModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.VirtualKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.OverrideKeyword, - SyntaxKind.AbstractKeyword, - SyntaxKind.ExternKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.AsyncKeyword - }; + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.VirtualKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.OverrideKeyword, + SyntaxKind.AbstractKeyword, + SyntaxKind.ExternKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.AsyncKeyword + }; - private static readonly ISet s_validStructModifiers = new HashSet(s_validClassInterfaceRecordModifiers, SyntaxFacts.EqualityComparer) - { - SyntaxKind.ReadOnlyKeyword, - }; + private static readonly ISet s_validStructModifiers = new HashSet(s_validClassInterfaceRecordModifiers, SyntaxFacts.EqualityComparer) + { + SyntaxKind.ReadOnlyKeyword, + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var syntaxTree = context.SyntaxTree; - return - IsMemberReturnTypeContext(position, context, cancellationToken) || - context.IsGlobalStatementContext || - context.IsTypeOfExpressionContext || - syntaxTree.IsSizeOfExpressionContext(position, context.LeftToken) || - context.IsDelegateReturnTypeContext || - context.IsFunctionPointerTypeArgumentContext || - IsUnsafeLocalVariableDeclarationContext(context) || - IsUnsafeParameterTypeContext(context) || - IsUnsafeCastTypeContext(context) || - IsUnsafeDefaultExpressionContext(context) || - IsUnsafeUsingDirectiveContext(context) || - context.IsFixedVariableDeclarationContext || - context.SyntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.SyntaxTree.IsLocalFunctionDeclarationContext(position, cancellationToken); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + IsMemberReturnTypeContext(position, context, cancellationToken) || + context.IsGlobalStatementContext || + context.IsTypeOfExpressionContext || + syntaxTree.IsSizeOfExpressionContext(position, context.LeftToken) || + context.IsDelegateReturnTypeContext || + context.IsFunctionPointerTypeArgumentContext || + IsUnsafeLocalVariableDeclarationContext(context) || + IsUnsafeParameterTypeContext(context) || + IsUnsafeCastTypeContext(context) || + IsUnsafeDefaultExpressionContext(context) || + IsUnsafeUsingDirectiveContext(context) || + context.IsFixedVariableDeclarationContext || + context.SyntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.SyntaxTree.IsLocalFunctionDeclarationContext(position, cancellationToken); + } - private static bool IsUnsafeDefaultExpressionContext(CSharpSyntaxContext context) - { - return - context.TargetToken.IsUnsafeContext() && - context.SyntaxTree.IsDefaultExpressionContext(context.Position, context.LeftToken); - } + private static bool IsUnsafeDefaultExpressionContext(CSharpSyntaxContext context) + { + return + context.TargetToken.IsUnsafeContext() && + context.SyntaxTree.IsDefaultExpressionContext(context.Position, context.LeftToken); + } - private static bool IsUnsafeCastTypeContext(CSharpSyntaxContext context) + private static bool IsUnsafeCastTypeContext(CSharpSyntaxContext context) + { + if (context.TargetToken.IsUnsafeContext()) { - if (context.TargetToken.IsUnsafeContext()) + if (context.IsDefiniteCastTypeContext) { - if (context.IsDefiniteCastTypeContext) - { - return true; - } - - var token = context.TargetToken; - - if (token.Kind() == SyntaxKind.OpenParenToken && - token.Parent.IsKind(SyntaxKind.ParenthesizedExpression)) - { - return true; - } + return true; } - return false; - } - - private static bool IsUnsafeParameterTypeContext(CSharpSyntaxContext context) - { - return - context.TargetToken.IsUnsafeContext() && - context.IsParameterTypeContext; - } + var token = context.TargetToken; - private static bool IsUnsafeLocalVariableDeclarationContext(CSharpSyntaxContext context) - { - if (context.TargetToken.IsUnsafeContext()) + if (token.Kind() == SyntaxKind.OpenParenToken && + token.Parent.IsKind(SyntaxKind.ParenthesizedExpression)) { - return - context.IsLocalVariableDeclarationContext || - context.IsStatementContext; + return true; } - - return false; } - private static bool IsUnsafeUsingDirectiveContext(CSharpSyntaxContext context) + return false; + } + + private static bool IsUnsafeParameterTypeContext(CSharpSyntaxContext context) + { + return + context.TargetToken.IsUnsafeContext() && + context.IsParameterTypeContext; + } + + private static bool IsUnsafeLocalVariableDeclarationContext(CSharpSyntaxContext context) + { + if (context.TargetToken.IsUnsafeContext()) { return - context.IsUsingAliasTypeContext && - context.TargetToken.IsUnsafeContext(); + context.IsLocalVariableDeclarationContext || + context.IsStatementContext; } - private static bool IsMemberReturnTypeContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.SyntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext(validModifiers: s_validClassInterfaceRecordModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceRecordTypeDeclarations, canBePartial: true, cancellationToken) || - context.IsMemberDeclarationContext(validModifiers: s_validStructModifiers, validTypeDeclarations: SyntaxKindSet.StructOnlyTypeDeclarations, canBePartial: false, cancellationToken); + return false; } + + private static bool IsUnsafeUsingDirectiveContext(CSharpSyntaxContext context) + { + return + context.IsUsingAliasTypeContext && + context.TargetToken.IsUnsafeContext(); + } + + private static bool IsMemberReturnTypeContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.SyntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext(validModifiers: s_validClassInterfaceRecordModifiers, validTypeDeclarations: SyntaxKindSet.ClassInterfaceRecordTypeDeclarations, canBePartial: true, cancellationToken) || + context.IsMemberDeclarationContext(validModifiers: s_validStructModifiers, validTypeDeclarations: SyntaxKindSet.StructOnlyTypeDeclarations, canBePartial: false, cancellationToken); } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VolatileKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VolatileKeywordRecommender.cs index f57e25729602c..8267b48c2b984 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VolatileKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VolatileKeywordRecommender.cs @@ -8,35 +8,34 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders -{ - internal class VolatileKeywordRecommender : AbstractSyntacticSingleKeywordRecommender - { - private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.StaticKeyword, - }; +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; - public VolatileKeywordRecommender() - : base(SyntaxKind.VolatileKeyword) +internal class VolatileKeywordRecommender : AbstractSyntacticSingleKeywordRecommender +{ + private static readonly ISet s_validMemberModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - } + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.StaticKeyword, + }; - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - (context.IsGlobalStatementContext && context.SyntaxTree.IsScript()) || - context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || - context.IsMemberDeclarationContext( - validModifiers: s_validMemberModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: false, - cancellationToken: cancellationToken); - } + public VolatileKeywordRecommender() + : base(SyntaxKind.VolatileKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + (context.IsGlobalStatementContext && context.SyntaxTree.IsScript()) || + context.SyntaxTree.IsGlobalMemberDeclarationContext(context.Position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) || + context.IsMemberDeclarationContext( + validModifiers: s_validMemberModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: false, + cancellationToken: cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WarningKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WarningKeywordRecommender.cs index 2cbac147010d0..3e762bf3e53f8 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WarningKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WarningKeywordRecommender.cs @@ -5,31 +5,30 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class WarningKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class WarningKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public WarningKeywordRecommender() + : base(SyntaxKind.WarningKeyword, isValidInPreprocessorContext: true) { - public WarningKeywordRecommender() - : base(SyntaxKind.WarningKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + // # warning + if (context.IsPreProcessorKeywordContext) { - // # warning - if (context.IsPreProcessorKeywordContext) - { - return true; - } + return true; + } - // # pragma | - // # pragma w| - var previousToken1 = context.TargetToken; - var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); + // # pragma | + // # pragma w| + var previousToken1 = context.TargetToken; + var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); - return - previousToken1.Kind() == SyntaxKind.PragmaKeyword && - previousToken2.Kind() == SyntaxKind.HashToken; - } + return + previousToken1.Kind() == SyntaxKind.PragmaKeyword && + previousToken2.Kind() == SyntaxKind.HashToken; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WarningsKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WarningsKeywordRecommender.cs index 0c398e4a6a311..7a142ef1080d8 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WarningsKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WarningsKeywordRecommender.cs @@ -5,27 +5,26 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class WarningsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class WarningsKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public WarningsKeywordRecommender() + : base(SyntaxKind.WarningsKeyword, isValidInPreprocessorContext: true) { - public WarningsKeywordRecommender() - : base(SyntaxKind.WarningsKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - var previousToken1 = context.TargetToken; - var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); - var previousToken3 = previousToken2.GetPreviousToken(includeSkipped: true); + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var previousToken1 = context.TargetToken; + var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); + var previousToken3 = previousToken2.GetPreviousToken(includeSkipped: true); - // # nullable enable | - // # nullable enable w| - return - (previousToken1.Kind() == SyntaxKind.EnableKeyword || previousToken1.Kind() == SyntaxKind.DisableKeyword || previousToken1.Kind() == SyntaxKind.RestoreKeyword) && - previousToken2.Kind() == SyntaxKind.NullableKeyword && - previousToken3.Kind() == SyntaxKind.HashToken; - } + // # nullable enable | + // # nullable enable w| + return + (previousToken1.Kind() == SyntaxKind.EnableKeyword || previousToken1.Kind() == SyntaxKind.DisableKeyword || previousToken1.Kind() == SyntaxKind.RestoreKeyword) && + previousToken2.Kind() == SyntaxKind.NullableKeyword && + previousToken3.Kind() == SyntaxKind.HashToken; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhenKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhenKeywordRecommender.cs index ca3189382af93..04a5855d0138e 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhenKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhenKeywordRecommender.cs @@ -11,87 +11,86 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class WhenKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class WhenKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public WhenKeywordRecommender() + : base(SyntaxKind.WhenKeyword, isValidInPreprocessorContext: true) { - public WhenKeywordRecommender() - : base(SyntaxKind.WhenKeyword, isValidInPreprocessorContext: true) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return context.IsCatchFilterContext || - IsAfterCompleteExpressionOrPatternInCaseLabel(context) || - IsAtEndOfPatternInSwitchExpression(context); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return context.IsCatchFilterContext || + IsAfterCompleteExpressionOrPatternInCaseLabel(context) || + IsAtEndOfPatternInSwitchExpression(context); + } - private static bool IsAtEndOfPatternInSwitchExpression(CSharpSyntaxContext context) - { - if (!context.IsAtEndOfPattern) - return false; + private static bool IsAtEndOfPatternInSwitchExpression(CSharpSyntaxContext context) + { + if (!context.IsAtEndOfPattern) + return false; - // `x switch { SomePattern $$ - var pattern = context.TargetToken.GetAncestors().LastOrDefault(); - if (pattern?.Parent is SwitchExpressionArmSyntax) - return true; + // `x switch { SomePattern $$ + var pattern = context.TargetToken.GetAncestors().LastOrDefault(); + if (pattern?.Parent is SwitchExpressionArmSyntax) + return true; + return false; + } + + private static bool IsAfterCompleteExpressionOrPatternInCaseLabel(CSharpSyntaxContext context) + { + var switchLabel = context.TargetToken.GetAncestor(); + if (switchLabel == null) return false; - } - private static bool IsAfterCompleteExpressionOrPatternInCaseLabel(CSharpSyntaxContext context) + var expressionOrPattern = switchLabel.ChildNodes().FirstOrDefault(); + if (expressionOrPattern == null) { - var switchLabel = context.TargetToken.GetAncestor(); - if (switchLabel == null) - return false; - - var expressionOrPattern = switchLabel.ChildNodes().FirstOrDefault(); - if (expressionOrPattern == null) - { - // It must have been a default label. - return false; - } - - // Never show `when` after `var` in a pattern. It's virtually always going to be unhelpful as the user is - // far more likely to be writing `case var someName...` rather than typing `cae var when...` (in the case - // that `var` is a constant). In other words, it's fine to make that rare case have to manually type out - // `when` rather than risk having `when` pop up when it's not desired. - if (context.TargetToken.Text == "var") - return false; - - // If the last token is missing, the expression is incomplete - possibly because of missing parentheses, - // but not necessarily. We don't want to offer 'when' in those cases. Here are some examples that illustrate this: - // case | - // case x.| - // case 1 + | - // case (1 + 1 | - - // Also note that if there's a missing token inside the expression, that's fine and we do offer 'when': - // case (1 + ) | - - if (expressionOrPattern.GetLastToken(includeZeroWidth: true).IsMissing) - return false; - - var lastToken = expressionOrPattern.GetLastToken(includeZeroWidth: false); - - // We're writing past the end of a complete pattern. This is a place where 'when' could be added to add - // restrictions on the pattern. - if (lastToken == context.TargetToken) - return true; + // It must have been a default label. + return false; + } - // We're writing the last token of a pattern. In this case, we might either be writing a name for the pattern - // (like `case Wolf w:`) or we might be starting to write `when` (like `case Wolf when ...:`). - if (lastToken == context.LeftToken) - { - if (expressionOrPattern is DeclarationPatternSyntax) - return true; + // Never show `when` after `var` in a pattern. It's virtually always going to be unhelpful as the user is + // far more likely to be writing `case var someName...` rather than typing `cae var when...` (in the case + // that `var` is a constant). In other words, it's fine to make that rare case have to manually type out + // `when` rather than risk having `when` pop up when it's not desired. + if (context.TargetToken.Text == "var") + return false; + + // If the last token is missing, the expression is incomplete - possibly because of missing parentheses, + // but not necessarily. We don't want to offer 'when' in those cases. Here are some examples that illustrate this: + // case | + // case x.| + // case 1 + | + // case (1 + 1 | - if (expressionOrPattern is RecursivePatternSyntax) - return true; - } + // Also note that if there's a missing token inside the expression, that's fine and we do offer 'when': + // case (1 + ) | + if (expressionOrPattern.GetLastToken(includeZeroWidth: true).IsMissing) return false; + + var lastToken = expressionOrPattern.GetLastToken(includeZeroWidth: false); + + // We're writing past the end of a complete pattern. This is a place where 'when' could be added to add + // restrictions on the pattern. + if (lastToken == context.TargetToken) + return true; + + // We're writing the last token of a pattern. In this case, we might either be writing a name for the pattern + // (like `case Wolf w:`) or we might be starting to write `when` (like `case Wolf when ...:`). + if (lastToken == context.LeftToken) + { + if (expressionOrPattern is DeclarationPatternSyntax) + return true; + + if (expressionOrPattern is RecursivePatternSyntax) + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhereKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhereKeywordRecommender.cs index 863d222d03081..288947677d05c 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhereKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhereKeywordRecommender.cs @@ -9,127 +9,126 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class WhereKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class WhereKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public WhereKeywordRecommender() + : base(SyntaxKind.WhereKeyword) { - public WhereKeywordRecommender() - : base(SyntaxKind.WhereKeyword) - { - } + } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - { - return - IsQueryContext(context) || - IsTypeParameterConstraintContext(context); - } + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + IsQueryContext(context) || + IsTypeParameterConstraintContext(context); + } - private static bool IsTypeParameterConstraintContext(CSharpSyntaxContext context) - { - // cases: - // class C | - // class C : IGoo | - // class C where T : IGoo | - // delegate void D | - // delegate void D where T : IGoo | - // void Goo() | - // void Goo() where T : IGoo | + private static bool IsTypeParameterConstraintContext(CSharpSyntaxContext context) + { + // cases: + // class C | + // class C : IGoo | + // class C where T : IGoo | + // delegate void D | + // delegate void D where T : IGoo | + // void Goo() | + // void Goo() where T : IGoo | - var token = context.TargetToken; + var token = context.TargetToken; - // class C | + // class C | - if (token.Kind() == SyntaxKind.GreaterThanToken) + if (token.Kind() == SyntaxKind.GreaterThanToken) + { + var typeParameters = token.GetAncestor(); + if (typeParameters != null && token == typeParameters.GetLastToken(includeSkipped: true)) { - var typeParameters = token.GetAncestor(); - if (typeParameters != null && token == typeParameters.GetLastToken(includeSkipped: true)) + var decl = typeParameters.GetAncestorOrThis(); + if (decl != null && decl.TypeParameterList == typeParameters) { - var decl = typeParameters.GetAncestorOrThis(); - if (decl != null && decl.TypeParameterList == typeParameters) - { - return true; - } + return true; } } + } - // delegate void D() | - if (token.Kind() == SyntaxKind.CloseParenToken && - token.Parent.IsKind(SyntaxKind.ParameterList) && - token.Parent.IsParentKind(SyntaxKind.DelegateDeclaration)) + // delegate void D() | + if (token.Kind() == SyntaxKind.CloseParenToken && + token.Parent.IsKind(SyntaxKind.ParameterList) && + token.Parent.IsParentKind(SyntaxKind.DelegateDeclaration)) + { + var decl = token.GetAncestor(); + if (decl != null && decl.TypeParameterList != null) { - var decl = token.GetAncestor(); - if (decl != null && decl.TypeParameterList != null) - { - return true; - } + return true; } + } - // void Goo() | + // void Goo() | - if (token.Kind() == SyntaxKind.CloseParenToken && - token.Parent.IsKind(SyntaxKind.ParameterList) && - token.Parent.IsParentKind(SyntaxKind.MethodDeclaration)) + if (token.Kind() == SyntaxKind.CloseParenToken && + token.Parent.IsKind(SyntaxKind.ParameterList) && + token.Parent.IsParentKind(SyntaxKind.MethodDeclaration)) + { + var decl = token.GetAncestor(); + if (decl != null && decl.Arity > 0) { - var decl = token.GetAncestor(); - if (decl != null && decl.Arity > 0) - { - return true; - } + return true; } + } - // class C : IGoo | - var baseList = token.GetAncestor(); - if (baseList?.Parent is TypeDeclarationSyntax typeDecl) + // class C : IGoo | + var baseList = token.GetAncestor(); + if (baseList?.Parent is TypeDeclarationSyntax typeDecl) + { + if (typeDecl.TypeParameterList != null && + baseList.Types.Any(t => token == t.GetLastToken(includeSkipped: true))) { - if (typeDecl.TypeParameterList != null && - baseList.Types.Any(t => token == t.GetLastToken(includeSkipped: true))) + // token is IdentifierName "where" + // only suggest "where" if token's previous token is also "where" + if (token.Parent is IdentifierNameSyntax && token.HasMatchingText(SyntaxKind.WhereKeyword)) { - // token is IdentifierName "where" - // only suggest "where" if token's previous token is also "where" - if (token.Parent is IdentifierNameSyntax && token.HasMatchingText(SyntaxKind.WhereKeyword)) - { - // Check for zero-width tokens in case there is a missing comma in the base list. - // For example: class C : Goo where where | - return token - .GetPreviousToken(includeZeroWidth: true) - .IsKindOrHasMatchingText(SyntaxKind.WhereKeyword); - } - - // System.| - // Not done typing the qualified name - if (token.IsKind(SyntaxKind.DotToken)) - { - return false; - } + // Check for zero-width tokens in case there is a missing comma in the base list. + // For example: class C : Goo where where | + return token + .GetPreviousToken(includeZeroWidth: true) + .IsKindOrHasMatchingText(SyntaxKind.WhereKeyword); + } - return true; + // System.| + // Not done typing the qualified name + if (token.IsKind(SyntaxKind.DotToken)) + { + return false; } - } - // class C where T : IGoo | - // delegate void D where T : IGoo | - if (token.IsLastTokenOfNode()) - { return true; } - - return false; } - private static bool IsQueryContext(CSharpSyntaxContext context) + // class C where T : IGoo | + // delegate void D where T : IGoo | + if (token.IsLastTokenOfNode()) { - var token = context.TargetToken; + return true; + } - // var q = from x in y - // | - if (!token.IntersectsWith(context.Position) && - token.IsLastTokenOfQueryClause()) - { - return true; - } + return false; + } + + private static bool IsQueryContext(CSharpSyntaxContext context) + { + var token = context.TargetToken; - return false; + // var q = from x in y + // | + if (!token.IntersectsWith(context.Position) && + token.IsLastTokenOfQueryClause()) + { + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhileKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhileKeywordRecommender.cs index 1fecea0ceedfb..b1fc429f226ba 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhileKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WhileKeywordRecommender.cs @@ -6,45 +6,44 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class WhileKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class WhileKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public WhileKeywordRecommender() + : base(SyntaxKind.WhileKeyword) { - public WhileKeywordRecommender() - : base(SyntaxKind.WhileKeyword) + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + if (context.IsStatementContext || + context.IsGlobalStatementContext) { + return true; } - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + // do { + // } | + + // do { + // } w| + + // Note: the case of + // do + // Goo(); + // | + // is taken care of in the IsStatementContext case. + + var token = context.TargetToken; + + if (token.Kind() == SyntaxKind.CloseBraceToken && + token.Parent.IsKind(SyntaxKind.Block) && + token.Parent.IsParentKind(SyntaxKind.DoStatement)) { - if (context.IsStatementContext || - context.IsGlobalStatementContext) - { - return true; - } - - // do { - // } | - - // do { - // } w| - - // Note: the case of - // do - // Goo(); - // | - // is taken care of in the IsStatementContext case. - - var token = context.TargetToken; - - if (token.Kind() == SyntaxKind.CloseBraceToken && - token.Parent.IsKind(SyntaxKind.Block) && - token.Parent.IsParentKind(SyntaxKind.DoStatement)) - { - return true; - } - - return false; + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WithKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WithKeywordRecommender.cs index 427eb0d9c5e39..de122a95edc0c 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WithKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/WithKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class WithKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class WithKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public WithKeywordRecommender() + : base(SyntaxKind.WithKeyword) { - public WithKeywordRecommender() - : base(SyntaxKind.WithKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => !context.IsInNonUserCode && context.IsIsOrAsOrSwitchOrWithExpressionContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => !context.IsInNonUserCode && context.IsIsOrAsOrSwitchOrWithExpressionContext; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/YieldKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/YieldKeywordRecommender.cs index 14f9a47ca20cd..8792b57a6e950 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/YieldKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/YieldKeywordRecommender.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal class YieldKeywordRecommender : AbstractSyntacticSingleKeywordRecommender { - internal class YieldKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + public YieldKeywordRecommender() + : base(SyntaxKind.YieldKeyword) { - public YieldKeywordRecommender() - : base(SyntaxKind.YieldKeyword) - { - } - - protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) - => context.IsStatementContext; } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsStatementContext; } diff --git a/src/Features/CSharp/Portable/Completion/Providers/ContextVariableArgumentProvider.cs b/src/Features/CSharp/Portable/Completion/Providers/ContextVariableArgumentProvider.cs index 8fd8b96510e1f..e311dc812db83 100644 --- a/src/Features/CSharp/Portable/Completion/Providers/ContextVariableArgumentProvider.cs +++ b/src/Features/CSharp/Portable/Completion/Providers/ContextVariableArgumentProvider.cs @@ -10,44 +10,43 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportArgumentProvider(nameof(ContextVariableArgumentProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(FirstBuiltInArgumentProvider))] +[Shared] +internal sealed class ContextVariableArgumentProvider : AbstractContextVariableArgumentProvider { - [ExportArgumentProvider(nameof(ContextVariableArgumentProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(FirstBuiltInArgumentProvider))] - [Shared] - internal sealed class ContextVariableArgumentProvider : AbstractContextVariableArgumentProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ContextVariableArgumentProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ContextVariableArgumentProvider() - { - } + } - protected override string ThisOrMeKeyword => SyntaxFacts.GetText(SyntaxKind.ThisKeyword); + protected override string ThisOrMeKeyword => SyntaxFacts.GetText(SyntaxKind.ThisKeyword); - protected override bool IsInstanceContext(SyntaxTree syntaxTree, SyntaxToken targetToken, SemanticModel semanticModel, CancellationToken cancellationToken) - => syntaxTree.IsInstanceContext(targetToken, semanticModel, cancellationToken); + protected override bool IsInstanceContext(SyntaxTree syntaxTree, SyntaxToken targetToken, SemanticModel semanticModel, CancellationToken cancellationToken) + => syntaxTree.IsInstanceContext(targetToken, semanticModel, cancellationToken); - public override async Task ProvideArgumentAsync(ArgumentContext context) + public override async Task ProvideArgumentAsync(ArgumentContext context) + { + await base.ProvideArgumentAsync(context).ConfigureAwait(false); + if (context.DefaultValue is not null) { - await base.ProvideArgumentAsync(context).ConfigureAwait(false); - if (context.DefaultValue is not null) + switch (context.Parameter.RefKind) { - switch (context.Parameter.RefKind) - { - case RefKind.Ref: - context.DefaultValue = "ref " + context.DefaultValue; - break; - - case RefKind.Out: - context.DefaultValue = "out " + context.DefaultValue; - break; - - case RefKind.In: - case RefKind.None: - default: - break; - } + case RefKind.Ref: + context.DefaultValue = "ref " + context.DefaultValue; + break; + + case RefKind.Out: + context.DefaultValue = "out " + context.DefaultValue; + break; + + case RefKind.In: + case RefKind.None: + default: + break; } } } diff --git a/src/Features/CSharp/Portable/Completion/Providers/DefaultArgumentProvider.cs b/src/Features/CSharp/Portable/Completion/Providers/DefaultArgumentProvider.cs index 87fcc97555f15..678c1d7046b04 100644 --- a/src/Features/CSharp/Portable/Completion/Providers/DefaultArgumentProvider.cs +++ b/src/Features/CSharp/Portable/Completion/Providers/DefaultArgumentProvider.cs @@ -9,51 +9,50 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportArgumentProvider(nameof(DefaultArgumentProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(OutVariableArgumentProvider))] +[Shared] +internal sealed class DefaultArgumentProvider : AbstractDefaultArgumentProvider { - [ExportArgumentProvider(nameof(DefaultArgumentProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(OutVariableArgumentProvider))] - [Shared] - internal sealed class DefaultArgumentProvider : AbstractDefaultArgumentProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DefaultArgumentProvider() + { + } + + public override Task ProvideArgumentAsync(ArgumentContext context) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public DefaultArgumentProvider() + if (context.PreviousValue is { }) { + context.DefaultValue = context.PreviousValue; } - - public override Task ProvideArgumentAsync(ArgumentContext context) + else if (context.Parameter.Type.IsReferenceType || context.Parameter.Type.IsNullable()) { - if (context.PreviousValue is { }) - { - context.DefaultValue = context.PreviousValue; - } - else if (context.Parameter.Type.IsReferenceType || context.Parameter.Type.IsNullable()) - { - context.DefaultValue = "null"; - } - else + context.DefaultValue = "null"; + } + else + { + context.DefaultValue = context.Parameter.Type.SpecialType switch { - context.DefaultValue = context.Parameter.Type.SpecialType switch - { - SpecialType.System_Boolean => "false", - SpecialType.System_Char => @"'\\0'", - SpecialType.System_Byte => "(byte)0", - SpecialType.System_SByte => "(sbyte)0", - SpecialType.System_Int16 => "(short)0", - SpecialType.System_UInt16 => "(ushort)0", - SpecialType.System_Int32 => "0", - SpecialType.System_UInt32 => "0U", - SpecialType.System_Int64 => "0L", - SpecialType.System_UInt64 => "0UL", - SpecialType.System_Decimal => "0.0m", - SpecialType.System_Single => "0.0f", - SpecialType.System_Double => "0.0", - _ => "default", - }; - } - - return Task.CompletedTask; + SpecialType.System_Boolean => "false", + SpecialType.System_Char => @"'\\0'", + SpecialType.System_Byte => "(byte)0", + SpecialType.System_SByte => "(sbyte)0", + SpecialType.System_Int16 => "(short)0", + SpecialType.System_UInt16 => "(ushort)0", + SpecialType.System_Int32 => "0", + SpecialType.System_UInt32 => "0U", + SpecialType.System_Int64 => "0L", + SpecialType.System_UInt64 => "0UL", + SpecialType.System_Decimal => "0.0m", + SpecialType.System_Single => "0.0f", + SpecialType.System_Double => "0.0", + _ => "default", + }; } + + return Task.CompletedTask; } } diff --git a/src/Features/CSharp/Portable/Completion/Providers/FirstBuiltInArgumentProvider.cs b/src/Features/CSharp/Portable/Completion/Providers/FirstBuiltInArgumentProvider.cs index 021589a96a14a..4fc31cfad3be6 100644 --- a/src/Features/CSharp/Portable/Completion/Providers/FirstBuiltInArgumentProvider.cs +++ b/src/Features/CSharp/Portable/Completion/Providers/FirstBuiltInArgumentProvider.cs @@ -8,23 +8,22 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +/// +/// Provides an argument provider that always appears before any built-in argument provider. This argument +/// provider does not provide any argument values. +/// +[ExportArgumentProvider(nameof(FirstBuiltInArgumentProvider), LanguageNames.CSharp)] +[Shared] +internal sealed class FirstBuiltInArgumentProvider : ArgumentProvider { - /// - /// Provides an argument provider that always appears before any built-in argument provider. This argument - /// provider does not provide any argument values. - /// - [ExportArgumentProvider(nameof(FirstBuiltInArgumentProvider), LanguageNames.CSharp)] - [Shared] - internal sealed class FirstBuiltInArgumentProvider : ArgumentProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public FirstBuiltInArgumentProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public FirstBuiltInArgumentProvider() - { - } - - public override Task ProvideArgumentAsync(ArgumentContext context) - => Task.CompletedTask; } + + public override Task ProvideArgumentAsync(ArgumentContext context) + => Task.CompletedTask; } diff --git a/src/Features/CSharp/Portable/Completion/Providers/LastBuiltInArgumentProvider.cs b/src/Features/CSharp/Portable/Completion/Providers/LastBuiltInArgumentProvider.cs index b5304cc6388ac..cb8ec74dc5a36 100644 --- a/src/Features/CSharp/Portable/Completion/Providers/LastBuiltInArgumentProvider.cs +++ b/src/Features/CSharp/Portable/Completion/Providers/LastBuiltInArgumentProvider.cs @@ -8,24 +8,23 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +/// +/// Provides an argumnet provider that always appears after all built-in argument providers. This argument +/// provider does not provide any argument values. +/// +[ExportArgumentProvider(nameof(LastBuiltInArgumentProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(DefaultArgumentProvider))] +[Shared] +internal sealed class LastBuiltInArgumentProvider : ArgumentProvider { - /// - /// Provides an argumnet provider that always appears after all built-in argument providers. This argument - /// provider does not provide any argument values. - /// - [ExportArgumentProvider(nameof(LastBuiltInArgumentProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(DefaultArgumentProvider))] - [Shared] - internal sealed class LastBuiltInArgumentProvider : ArgumentProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LastBuiltInArgumentProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public LastBuiltInArgumentProvider() - { - } - - public override Task ProvideArgumentAsync(ArgumentContext context) - => Task.CompletedTask; } + + public override Task ProvideArgumentAsync(ArgumentContext context) + => Task.CompletedTask; } diff --git a/src/Features/CSharp/Portable/Completion/Providers/OutVariableArgumentProvider.cs b/src/Features/CSharp/Portable/Completion/Providers/OutVariableArgumentProvider.cs index 862e7ecdcc792..825dfcc78c207 100644 --- a/src/Features/CSharp/Portable/Completion/Providers/OutVariableArgumentProvider.cs +++ b/src/Features/CSharp/Portable/Completion/Providers/OutVariableArgumentProvider.cs @@ -8,56 +8,55 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportArgumentProvider(nameof(OutVariableArgumentProvider), LanguageNames.CSharp)] +[ExtensionOrder(After = nameof(ContextVariableArgumentProvider))] +[Shared] +internal sealed class OutVariableArgumentProvider : ArgumentProvider { - [ExportArgumentProvider(nameof(OutVariableArgumentProvider), LanguageNames.CSharp)] - [ExtensionOrder(After = nameof(ContextVariableArgumentProvider))] - [Shared] - internal sealed class OutVariableArgumentProvider : ArgumentProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public OutVariableArgumentProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public OutVariableArgumentProvider() + } + + public override Task ProvideArgumentAsync(ArgumentContext context) + { + if (context.PreviousValue is not null) { + // This argument provider does not attempt to replace arguments already in code. + return Task.CompletedTask; } - public override Task ProvideArgumentAsync(ArgumentContext context) + if (context.Parameter.RefKind != RefKind.Out) { - if (context.PreviousValue is not null) - { - // This argument provider does not attempt to replace arguments already in code. - return Task.CompletedTask; - } - - if (context.Parameter.RefKind != RefKind.Out) - { - // This argument provider only considers 'out' parameters. - return Task.CompletedTask; - } - - // Since tihs provider runs after ContextVariableArgumentProvider, we know there is no suitable target in - // the current context. Instead, offer to declare a new variable. - var name = context.Parameter.Name; - if (SyntaxFacts.GetKeywordKind(name) != SyntaxKind.None - || SyntaxFacts.GetContextualKeywordKind(name) != SyntaxKind.None) - { - name = "@" + name; - } - - var syntax = SyntaxFactory.Argument( - nameColon: null, - refKindKeyword: SyntaxFactory.Token(SyntaxKind.OutKeyword), - SyntaxFactory.DeclarationExpression( - type: SyntaxFactory.IdentifierName("var"), - designation: SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier( - [], - contextualKind: SyntaxKind.None, - text: name, - valueText: context.Parameter.Name, - [])))); - - context.DefaultValue = syntax.NormalizeWhitespace().ToFullString(); + // This argument provider only considers 'out' parameters. return Task.CompletedTask; } + + // Since tihs provider runs after ContextVariableArgumentProvider, we know there is no suitable target in + // the current context. Instead, offer to declare a new variable. + var name = context.Parameter.Name; + if (SyntaxFacts.GetKeywordKind(name) != SyntaxKind.None + || SyntaxFacts.GetContextualKeywordKind(name) != SyntaxKind.None) + { + name = "@" + name; + } + + var syntax = SyntaxFactory.Argument( + nameColon: null, + refKindKeyword: SyntaxFactory.Token(SyntaxKind.OutKeyword), + SyntaxFactory.DeclarationExpression( + type: SyntaxFactory.IdentifierName("var"), + designation: SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier( + [], + contextualKind: SyntaxKind.None, + text: name, + valueText: context.Parameter.Name, + [])))); + + context.DefaultValue = syntax.NormalizeWhitespace().ToFullString(); + return Task.CompletedTask; } } diff --git a/src/Features/CSharp/Portable/ConvertAnonymousType/CSharpConvertAnonymousTypeToClassCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertAnonymousType/CSharpConvertAnonymousTypeToClassCodeRefactoringProvider.cs index e892ba7277786..5b3d77ee32f9a 100644 --- a/src/Features/CSharp/Portable/ConvertAnonymousType/CSharpConvertAnonymousTypeToClassCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertAnonymousType/CSharpConvertAnonymousTypeToClassCodeRefactoringProvider.cs @@ -10,68 +10,67 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.ConvertAnonymousType +namespace Microsoft.CodeAnalysis.CSharp.ConvertAnonymousType; + +[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertAnonymousTypeToClass), Shared] +internal class CSharpConvertAnonymousTypeToClassCodeRefactoringProvider : + AbstractConvertAnonymousTypeToClassCodeRefactoringProvider< + ExpressionSyntax, + NameSyntax, + IdentifierNameSyntax, + ObjectCreationExpressionSyntax, + AnonymousObjectCreationExpressionSyntax, + BaseNamespaceDeclarationSyntax> { - [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertAnonymousTypeToClass), Shared] - internal class CSharpConvertAnonymousTypeToClassCodeRefactoringProvider : - AbstractConvertAnonymousTypeToClassCodeRefactoringProvider< - ExpressionSyntax, - NameSyntax, - IdentifierNameSyntax, - ObjectCreationExpressionSyntax, - AnonymousObjectCreationExpressionSyntax, - BaseNamespaceDeclarationSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertAnonymousTypeToClassCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertAnonymousTypeToClassCodeRefactoringProvider() - { - } + } - protected override ObjectCreationExpressionSyntax CreateObjectCreationExpression( - NameSyntax nameNode, AnonymousObjectCreationExpressionSyntax anonymousObject) - { - return SyntaxFactory.ObjectCreationExpression( - nameNode, CreateArgumentList(anonymousObject), initializer: null); - } + protected override ObjectCreationExpressionSyntax CreateObjectCreationExpression( + NameSyntax nameNode, AnonymousObjectCreationExpressionSyntax anonymousObject) + { + return SyntaxFactory.ObjectCreationExpression( + nameNode, CreateArgumentList(anonymousObject), initializer: null); + } - private ArgumentListSyntax CreateArgumentList(AnonymousObjectCreationExpressionSyntax anonymousObject) - => SyntaxFactory.ArgumentList( - SyntaxFactory.Token(SyntaxKind.OpenParenToken).WithTriviaFrom(anonymousObject.OpenBraceToken), - CreateArguments(anonymousObject.Initializers), - SyntaxFactory.Token(SyntaxKind.CloseParenToken).WithTriviaFrom(anonymousObject.CloseBraceToken)); + private ArgumentListSyntax CreateArgumentList(AnonymousObjectCreationExpressionSyntax anonymousObject) + => SyntaxFactory.ArgumentList( + SyntaxFactory.Token(SyntaxKind.OpenParenToken).WithTriviaFrom(anonymousObject.OpenBraceToken), + CreateArguments(anonymousObject.Initializers), + SyntaxFactory.Token(SyntaxKind.CloseParenToken).WithTriviaFrom(anonymousObject.CloseBraceToken)); - private SeparatedSyntaxList CreateArguments(SeparatedSyntaxList initializers) - => SyntaxFactory.SeparatedList(CreateArguments(OmitTrailingComma(initializers.GetWithSeparators()))); + private SeparatedSyntaxList CreateArguments(SeparatedSyntaxList initializers) + => SyntaxFactory.SeparatedList(CreateArguments(OmitTrailingComma(initializers.GetWithSeparators()))); - private static SyntaxNodeOrTokenList OmitTrailingComma(SyntaxNodeOrTokenList list) + private static SyntaxNodeOrTokenList OmitTrailingComma(SyntaxNodeOrTokenList list) + { + // Trailing comma is allowed in initializer list, but disallowed in method calls. + if (list.Count == 0 || list.Count % 2 == 1) { - // Trailing comma is allowed in initializer list, but disallowed in method calls. - if (list.Count == 0 || list.Count % 2 == 1) - { - // The list is either empty, or does not end with a trailing comma. - return list; - } - - return list - .Replace( - list[^2], - list[^2].AsNode()! - .WithAppendedTrailingTrivia(list[^1].GetLeadingTrivia()) - .WithAppendedTrailingTrivia(list[^1].GetTrailingTrivia())) - .RemoveAt(list.Count - 1); + // The list is either empty, or does not end with a trailing comma. + return list; } - private SyntaxNodeOrTokenList CreateArguments(SyntaxNodeOrTokenList list) - => new(list.Select(CreateArgumentOrComma)); + return list + .Replace( + list[^2], + list[^2].AsNode()! + .WithAppendedTrailingTrivia(list[^1].GetLeadingTrivia()) + .WithAppendedTrailingTrivia(list[^1].GetTrailingTrivia())) + .RemoveAt(list.Count - 1); + } - private SyntaxNodeOrToken CreateArgumentOrComma(SyntaxNodeOrToken declOrComma) - => declOrComma.IsToken - ? declOrComma - : CreateArgument((AnonymousObjectMemberDeclaratorSyntax)declOrComma.AsNode()!); + private SyntaxNodeOrTokenList CreateArguments(SyntaxNodeOrTokenList list) + => new(list.Select(CreateArgumentOrComma)); - private static ArgumentSyntax CreateArgument(AnonymousObjectMemberDeclaratorSyntax decl) - => SyntaxFactory.Argument(decl.Expression); - } + private SyntaxNodeOrToken CreateArgumentOrComma(SyntaxNodeOrToken declOrComma) + => declOrComma.IsToken + ? declOrComma + : CreateArgument((AnonymousObjectMemberDeclaratorSyntax)declOrComma.AsNode()!); + + private static ArgumentSyntax CreateArgument(AnonymousObjectMemberDeclaratorSyntax decl) + => SyntaxFactory.Argument(decl.Expression); } diff --git a/src/Features/CSharp/Portable/ConvertAnonymousType/CSharpConvertAnonymousTypeToTupleCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertAnonymousType/CSharpConvertAnonymousTypeToTupleCodeRefactoringProvider.cs index 4469a0aecd9d1..89747fef64d27 100644 --- a/src/Features/CSharp/Portable/ConvertAnonymousType/CSharpConvertAnonymousTypeToTupleCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertAnonymousType/CSharpConvertAnonymousTypeToTupleCodeRefactoringProvider.cs @@ -10,43 +10,42 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.ConvertAnonymousType +namespace Microsoft.CodeAnalysis.CSharp.ConvertAnonymousType; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertAnonymousTypeToTuple), Shared] +internal class CSharpConvertAnonymousTypeToTupleCodeRefactoringProvider + : AbstractConvertAnonymousTypeToTupleCodeRefactoringProvider< + ExpressionSyntax, + TupleExpressionSyntax, + AnonymousObjectCreationExpressionSyntax> { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertAnonymousTypeToTuple), Shared] - internal class CSharpConvertAnonymousTypeToTupleCodeRefactoringProvider - : AbstractConvertAnonymousTypeToTupleCodeRefactoringProvider< - ExpressionSyntax, - TupleExpressionSyntax, - AnonymousObjectCreationExpressionSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertAnonymousTypeToTupleCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertAnonymousTypeToTupleCodeRefactoringProvider() - { - } - - protected override int GetInitializerCount(AnonymousObjectCreationExpressionSyntax anonymousType) - => anonymousType.Initializers.Count; - - protected override TupleExpressionSyntax ConvertToTuple(AnonymousObjectCreationExpressionSyntax anonCreation) - => SyntaxFactory.TupleExpression( - SyntaxFactory.Token(SyntaxKind.OpenParenToken).WithTriviaFrom(anonCreation.OpenBraceToken), - ConvertInitializers(anonCreation.Initializers), - SyntaxFactory.Token(SyntaxKind.CloseParenToken).WithTriviaFrom(anonCreation.CloseBraceToken)) - .WithPrependedLeadingTrivia(anonCreation.GetLeadingTrivia()); - - private static SeparatedSyntaxList ConvertInitializers(SeparatedSyntaxList initializers) - => SyntaxFactory.SeparatedList(initializers.Select(ConvertInitializer), initializers.GetSeparators()); - - private static ArgumentSyntax ConvertInitializer(AnonymousObjectMemberDeclaratorSyntax declarator) - => SyntaxFactory.Argument(ConvertName(declarator.NameEquals), default, declarator.Expression) - .WithTriviaFrom(declarator); - - private static NameColonSyntax? ConvertName(NameEqualsSyntax? nameEquals) - => nameEquals == null - ? null - : SyntaxFactory.NameColon( - nameEquals.Name, - SyntaxFactory.Token(SyntaxKind.ColonToken).WithTriviaFrom(nameEquals.EqualsToken)); } + + protected override int GetInitializerCount(AnonymousObjectCreationExpressionSyntax anonymousType) + => anonymousType.Initializers.Count; + + protected override TupleExpressionSyntax ConvertToTuple(AnonymousObjectCreationExpressionSyntax anonCreation) + => SyntaxFactory.TupleExpression( + SyntaxFactory.Token(SyntaxKind.OpenParenToken).WithTriviaFrom(anonCreation.OpenBraceToken), + ConvertInitializers(anonCreation.Initializers), + SyntaxFactory.Token(SyntaxKind.CloseParenToken).WithTriviaFrom(anonCreation.CloseBraceToken)) + .WithPrependedLeadingTrivia(anonCreation.GetLeadingTrivia()); + + private static SeparatedSyntaxList ConvertInitializers(SeparatedSyntaxList initializers) + => SyntaxFactory.SeparatedList(initializers.Select(ConvertInitializer), initializers.GetSeparators()); + + private static ArgumentSyntax ConvertInitializer(AnonymousObjectMemberDeclaratorSyntax declarator) + => SyntaxFactory.Argument(ConvertName(declarator.NameEquals), default, declarator.Expression) + .WithTriviaFrom(declarator); + + private static NameColonSyntax? ConvertName(NameEqualsSyntax? nameEquals) + => nameEquals == null + ? null + : SyntaxFactory.NameColon( + nameEquals.Name, + SyntaxFactory.Token(SyntaxKind.ColonToken).WithTriviaFrom(nameEquals.EqualsToken)); } diff --git a/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs index f722272f39024..ff57f22b3994c 100644 --- a/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs @@ -21,130 +21,129 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertAutoPropertyToFullProperty +namespace Microsoft.CodeAnalysis.CSharp.ConvertAutoPropertyToFullProperty; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertAutoPropertyToFullProperty), Shared] +internal class CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider : AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertAutoPropertyToFullProperty), Shared] - internal class CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider : AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider() - { - } + } + + protected override async Task GetFieldNameAsync(Document document, IPropertySymbol property, NamingStylePreferencesProvider fallbackOptions, CancellationToken cancellationToken) + { + var rule = await document.GetApplicableNamingRuleAsync( + new SymbolSpecification.SymbolKindOrTypeKind(SymbolKind.Field), + property.IsStatic ? DeclarationModifiers.Static : DeclarationModifiers.None, + Accessibility.Private, + fallbackOptions, + cancellationToken).ConfigureAwait(false); + + var fieldName = rule.NamingStyle.MakeCompliant(property.Name).First(); + return NameGenerator.GenerateUniqueName(fieldName, n => !(property.ContainingType.Name == n || property.ContainingType.GetMembers(n).Any())); + } + + protected override (SyntaxNode newGetAccessor, SyntaxNode newSetAccessor) GetNewAccessors( + CSharpCodeGenerationContextInfo info, SyntaxNode property, + string fieldName, SyntaxGenerator generator, CancellationToken cancellationToken) + { + // C# might have trivia with the accessors that needs to be preserved. + // so we will update the existing accessors instead of creating new ones + var accessorListSyntax = ((PropertyDeclarationSyntax)property).AccessorList; + var (getAccessor, setAccessor) = GetExistingAccessors(accessorListSyntax); + + var getAccessorStatement = generator.ReturnStatement(generator.IdentifierName(fieldName)); + var newGetter = GetUpdatedAccessor(info, getAccessor, getAccessorStatement, cancellationToken); - protected override async Task GetFieldNameAsync(Document document, IPropertySymbol property, NamingStylePreferencesProvider fallbackOptions, CancellationToken cancellationToken) + SyntaxNode newSetter = null; + if (setAccessor != null) { - var rule = await document.GetApplicableNamingRuleAsync( - new SymbolSpecification.SymbolKindOrTypeKind(SymbolKind.Field), - property.IsStatic ? DeclarationModifiers.Static : DeclarationModifiers.None, - Accessibility.Private, - fallbackOptions, - cancellationToken).ConfigureAwait(false); - - var fieldName = rule.NamingStyle.MakeCompliant(property.Name).First(); - return NameGenerator.GenerateUniqueName(fieldName, n => !(property.ContainingType.Name == n || property.ContainingType.GetMembers(n).Any())); + var setAccessorStatement = generator.ExpressionStatement(generator.AssignmentStatement( + generator.IdentifierName(fieldName), + generator.IdentifierName("value"))); + newSetter = GetUpdatedAccessor(info, setAccessor, setAccessorStatement, cancellationToken); } - protected override (SyntaxNode newGetAccessor, SyntaxNode newSetAccessor) GetNewAccessors( - CSharpCodeGenerationContextInfo info, SyntaxNode property, - string fieldName, SyntaxGenerator generator, CancellationToken cancellationToken) - { - // C# might have trivia with the accessors that needs to be preserved. - // so we will update the existing accessors instead of creating new ones - var accessorListSyntax = ((PropertyDeclarationSyntax)property).AccessorList; - var (getAccessor, setAccessor) = GetExistingAccessors(accessorListSyntax); + return (newGetAccessor: newGetter, newSetAccessor: newSetter); + } - var getAccessorStatement = generator.ReturnStatement(generator.IdentifierName(fieldName)); - var newGetter = GetUpdatedAccessor(info, getAccessor, getAccessorStatement, cancellationToken); + private static (AccessorDeclarationSyntax getAccessor, AccessorDeclarationSyntax setAccessor) + GetExistingAccessors(AccessorListSyntax accessorListSyntax) + => (accessorListSyntax.Accessors.FirstOrDefault(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)), + accessorListSyntax.Accessors.FirstOrDefault(a => a.Kind() is SyntaxKind.SetAccessorDeclaration or SyntaxKind.InitAccessorDeclaration)); - SyntaxNode newSetter = null; - if (setAccessor != null) - { - var setAccessorStatement = generator.ExpressionStatement(generator.AssignmentStatement( - generator.IdentifierName(fieldName), - generator.IdentifierName("value"))); - newSetter = GetUpdatedAccessor(info, setAccessor, setAccessorStatement, cancellationToken); - } + private static SyntaxNode GetUpdatedAccessor(CSharpCodeGenerationContextInfo info, + SyntaxNode accessor, SyntaxNode statement, CancellationToken cancellationToken) + { + var newAccessor = AddStatement(accessor, statement); + var accessorDeclarationSyntax = (AccessorDeclarationSyntax)newAccessor; - return (newGetAccessor: newGetter, newSetAccessor: newSetter); + var preference = info.Options.PreferExpressionBodiedAccessors.Value; + if (preference == ExpressionBodyPreference.Never) + { + return accessorDeclarationSyntax.WithSemicolonToken(default); } - private static (AccessorDeclarationSyntax getAccessor, AccessorDeclarationSyntax setAccessor) - GetExistingAccessors(AccessorListSyntax accessorListSyntax) - => (accessorListSyntax.Accessors.FirstOrDefault(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)), - accessorListSyntax.Accessors.FirstOrDefault(a => a.Kind() is SyntaxKind.SetAccessorDeclaration or SyntaxKind.InitAccessorDeclaration)); - - private static SyntaxNode GetUpdatedAccessor(CSharpCodeGenerationContextInfo info, - SyntaxNode accessor, SyntaxNode statement, CancellationToken cancellationToken) + if (!accessorDeclarationSyntax.Body.TryConvertToArrowExpressionBody( + accessorDeclarationSyntax.Kind(), info.LanguageVersion, preference, cancellationToken, + out var arrowExpression, out _)) { - var newAccessor = AddStatement(accessor, statement); - var accessorDeclarationSyntax = (AccessorDeclarationSyntax)newAccessor; + return accessorDeclarationSyntax.WithSemicolonToken(default); + } - var preference = info.Options.PreferExpressionBodiedAccessors.Value; - if (preference == ExpressionBodyPreference.Never) - { - return accessorDeclarationSyntax.WithSemicolonToken(default); - } + return accessorDeclarationSyntax + .WithExpressionBody(arrowExpression) + .WithBody(null) + .WithSemicolonToken(accessorDeclarationSyntax.SemicolonToken) + .WithAdditionalAnnotations(Formatter.Annotation); + } - if (!accessorDeclarationSyntax.Body.TryConvertToArrowExpressionBody( - accessorDeclarationSyntax.Kind(), info.LanguageVersion, preference, cancellationToken, - out var arrowExpression, out _)) - { - return accessorDeclarationSyntax.WithSemicolonToken(default); - } + internal static SyntaxNode AddStatement(SyntaxNode accessor, SyntaxNode statement) + { + var blockSyntax = SyntaxFactory.Block( + SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithLeadingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed), + new SyntaxList((StatementSyntax)statement), + SyntaxFactory.Token(SyntaxKind.CloseBraceToken) + .WithTrailingTrivia(((AccessorDeclarationSyntax)accessor).SemicolonToken.TrailingTrivia)); - return accessorDeclarationSyntax - .WithExpressionBody(arrowExpression) - .WithBody(null) - .WithSemicolonToken(accessorDeclarationSyntax.SemicolonToken) - .WithAdditionalAnnotations(Formatter.Annotation); - } + return ((AccessorDeclarationSyntax)accessor).WithBody(blockSyntax); + } - internal static SyntaxNode AddStatement(SyntaxNode accessor, SyntaxNode statement) - { - var blockSyntax = SyntaxFactory.Block( - SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithLeadingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed), - new SyntaxList((StatementSyntax)statement), - SyntaxFactory.Token(SyntaxKind.CloseBraceToken) - .WithTrailingTrivia(((AccessorDeclarationSyntax)accessor).SemicolonToken.TrailingTrivia)); + protected override SyntaxNode ConvertPropertyToExpressionBodyIfDesired( + CSharpCodeGenerationContextInfo info, SyntaxNode property) + { + var propertyDeclaration = (PropertyDeclarationSyntax)property; - return ((AccessorDeclarationSyntax)accessor).WithBody(blockSyntax); + var preference = info.Options.PreferExpressionBodiedProperties.Value; + if (preference == ExpressionBodyPreference.Never) + { + return propertyDeclaration.WithSemicolonToken(default); } - protected override SyntaxNode ConvertPropertyToExpressionBodyIfDesired( - CSharpCodeGenerationContextInfo info, SyntaxNode property) + // if there is a get accessors only, we can move the expression body to the property + if (propertyDeclaration.AccessorList?.Accessors.Count == 1 && + propertyDeclaration.AccessorList.Accessors[0].Kind() == SyntaxKind.GetAccessorDeclaration) { - var propertyDeclaration = (PropertyDeclarationSyntax)property; - - var preference = info.Options.PreferExpressionBodiedProperties.Value; - if (preference == ExpressionBodyPreference.Never) + var getAccessor = propertyDeclaration.AccessorList.Accessors[0]; + if (getAccessor.ExpressionBody != null) { - return propertyDeclaration.WithSemicolonToken(default); + return propertyDeclaration.WithExpressionBody(getAccessor.ExpressionBody) + .WithSemicolonToken(getAccessor.SemicolonToken) + .WithAccessorList(null); } - - // if there is a get accessors only, we can move the expression body to the property - if (propertyDeclaration.AccessorList?.Accessors.Count == 1 && - propertyDeclaration.AccessorList.Accessors[0].Kind() == SyntaxKind.GetAccessorDeclaration) - { - var getAccessor = propertyDeclaration.AccessorList.Accessors[0]; - if (getAccessor.ExpressionBody != null) - { - return propertyDeclaration.WithExpressionBody(getAccessor.ExpressionBody) - .WithSemicolonToken(getAccessor.SemicolonToken) - .WithAccessorList(null); - } - } - - return propertyDeclaration.WithSemicolonToken(default); } - protected override SyntaxNode GetTypeBlock(SyntaxNode syntaxNode) - => syntaxNode; + return propertyDeclaration.WithSemicolonToken(default); + } + + protected override SyntaxNode GetTypeBlock(SyntaxNode syntaxNode) + => syntaxNode; - protected override SyntaxNode GetInitializerValue(SyntaxNode property) - => ((PropertyDeclarationSyntax)property).Initializer?.Value; + protected override SyntaxNode GetInitializerValue(SyntaxNode property) + => ((PropertyDeclarationSyntax)property).Initializer?.Value; - protected override SyntaxNode GetPropertyWithoutInitializer(SyntaxNode property) - => ((PropertyDeclarationSyntax)property).WithInitializer(null); - } + protected override SyntaxNode GetPropertyWithoutInitializer(SyntaxNode property) + => ((PropertyDeclarationSyntax)property).WithInitializer(null); } diff --git a/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider.cs index 5738b9f74e7a2..687e9e66fc4a9 100644 --- a/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider.cs @@ -14,175 +14,174 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.ConvertBetweenRegularAndVerbatimString +namespace Microsoft.CodeAnalysis.CSharp.ConvertBetweenRegularAndVerbatimString; + +internal abstract class AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider< + TStringExpressionSyntax> + : CodeRefactoringProvider + where TStringExpressionSyntax : ExpressionSyntax { - internal abstract class AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider< - TStringExpressionSyntax> - : CodeRefactoringProvider - where TStringExpressionSyntax : ExpressionSyntax + private const char OpenBrace = '{'; + private const char CloseBrace = '}'; + protected const char DoubleQuote = '"'; + + protected abstract bool IsInterpolation { get; } + protected abstract bool IsAppropriateLiteralKind(TStringExpressionSyntax literalExpression); + protected abstract void AddSubStringTokens(TStringExpressionSyntax literalExpression, ArrayBuilder subTokens); + protected abstract bool IsVerbatim(TStringExpressionSyntax literalExpression); + protected abstract TStringExpressionSyntax CreateVerbatimStringExpression(IVirtualCharService charService, StringBuilder sb, TStringExpressionSyntax stringExpression); + protected abstract TStringExpressionSyntax CreateRegularStringExpression(IVirtualCharService charService, StringBuilder sb, TStringExpressionSyntax stringExpression); + + public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - private const char OpenBrace = '{'; - private const char CloseBrace = '}'; - protected const char DoubleQuote = '"'; - - protected abstract bool IsInterpolation { get; } - protected abstract bool IsAppropriateLiteralKind(TStringExpressionSyntax literalExpression); - protected abstract void AddSubStringTokens(TStringExpressionSyntax literalExpression, ArrayBuilder subTokens); - protected abstract bool IsVerbatim(TStringExpressionSyntax literalExpression); - protected abstract TStringExpressionSyntax CreateVerbatimStringExpression(IVirtualCharService charService, StringBuilder sb, TStringExpressionSyntax stringExpression); - protected abstract TStringExpressionSyntax CreateRegularStringExpression(IVirtualCharService charService, StringBuilder sb, TStringExpressionSyntax stringExpression); - - public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var literalExpression = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (literalExpression == null || !IsAppropriateLiteralKind(literalExpression)) - return; + var literalExpression = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (literalExpression == null || !IsAppropriateLiteralKind(literalExpression)) + return; - var (document, _, cancellationToken) = context; + var (document, _, cancellationToken) = context; - var charService = document.GetRequiredLanguageService(); + var charService = document.GetRequiredLanguageService(); - using var _ = ArrayBuilder.GetInstance(out var subStringTokens); + using var _ = ArrayBuilder.GetInstance(out var subStringTokens); - // First, ensure that we understand all text parts of the interpolation. - AddSubStringTokens(literalExpression, subStringTokens); - foreach (var subToken in subStringTokens) - { - var chars = charService.TryConvertToVirtualChars(subToken); - if (chars.IsDefault) - return; - } + // First, ensure that we understand all text parts of the interpolation. + AddSubStringTokens(literalExpression, subStringTokens); + foreach (var subToken in subStringTokens) + { + var chars = charService.TryConvertToVirtualChars(subToken); + if (chars.IsDefault) + return; + } - // Note: This is a generally useful feature on strings. But it's not likely to be something - // people want to use a lot. Make low priority so it doesn't interfere with more - // commonly useful refactorings. + // Note: This is a generally useful feature on strings. But it's not likely to be something + // people want to use a lot. Make low priority so it doesn't interfere with more + // commonly useful refactorings. - if (IsVerbatim(literalExpression)) - { - // always offer to convert from verbatim string to normal string. - context.RegisterRefactoring(CodeAction.Create( - CSharpFeaturesResources.Convert_to_regular_string, - c => ConvertToRegularStringAsync(document, literalExpression, c), - nameof(CSharpFeaturesResources.Convert_to_regular_string), - CodeActionPriority.Low)); - } - else if (ContainsSimpleEscape(charService, subStringTokens)) - { - // Offer to convert to a verbatim string if the normal string contains simple - // escapes that can be directly embedded in the verbatim string. - context.RegisterRefactoring(CodeAction.Create( - CSharpFeaturesResources.Convert_to_verbatim_string, - c => ConvertToVerbatimStringAsync(document, literalExpression, c), - nameof(CSharpFeaturesResources.Convert_to_verbatim_string), - CodeActionPriority.Low)); - } + if (IsVerbatim(literalExpression)) + { + // always offer to convert from verbatim string to normal string. + context.RegisterRefactoring(CodeAction.Create( + CSharpFeaturesResources.Convert_to_regular_string, + c => ConvertToRegularStringAsync(document, literalExpression, c), + nameof(CSharpFeaturesResources.Convert_to_regular_string), + CodeActionPriority.Low)); } - - private static async Task ConvertAsync( - Func convert, - Document document, TStringExpressionSyntax stringExpression, CancellationToken cancellationToken) + else if (ContainsSimpleEscape(charService, subStringTokens)) { - using var _ = PooledStringBuilder.GetInstance(out var sb); + // Offer to convert to a verbatim string if the normal string contains simple + // escapes that can be directly embedded in the verbatim string. + context.RegisterRefactoring(CodeAction.Create( + CSharpFeaturesResources.Convert_to_verbatim_string, + c => ConvertToVerbatimStringAsync(document, literalExpression, c), + nameof(CSharpFeaturesResources.Convert_to_verbatim_string), + CodeActionPriority.Low)); + } + } - var charService = document.GetRequiredLanguageService(); - var newStringExpression = convert(charService, sb, stringExpression).WithTriviaFrom(stringExpression); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + private static async Task ConvertAsync( + Func convert, + Document document, TStringExpressionSyntax stringExpression, CancellationToken cancellationToken) + { + using var _ = PooledStringBuilder.GetInstance(out var sb); - return document.WithSyntaxRoot(root.ReplaceNode(stringExpression, newStringExpression)); - } + var charService = document.GetRequiredLanguageService(); + var newStringExpression = convert(charService, sb, stringExpression).WithTriviaFrom(stringExpression); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + return document.WithSyntaxRoot(root.ReplaceNode(stringExpression, newStringExpression)); + } - private Task ConvertToVerbatimStringAsync(Document document, TStringExpressionSyntax stringExpression, CancellationToken cancellationToken) - => ConvertAsync(CreateVerbatimStringExpression, document, stringExpression, cancellationToken); + private Task ConvertToVerbatimStringAsync(Document document, TStringExpressionSyntax stringExpression, CancellationToken cancellationToken) + => ConvertAsync(CreateVerbatimStringExpression, document, stringExpression, cancellationToken); - private Task ConvertToRegularStringAsync(Document document, TStringExpressionSyntax stringExpression, CancellationToken cancellationToken) - => ConvertAsync(CreateRegularStringExpression, document, stringExpression, cancellationToken); + private Task ConvertToRegularStringAsync(Document document, TStringExpressionSyntax stringExpression, CancellationToken cancellationToken) + => ConvertAsync(CreateRegularStringExpression, document, stringExpression, cancellationToken); - protected void AddVerbatimStringText( - IVirtualCharService charService, StringBuilder sb, SyntaxToken stringToken) + protected void AddVerbatimStringText( + IVirtualCharService charService, StringBuilder sb, SyntaxToken stringToken) + { + var isInterpolation = IsInterpolation; + var chars = charService.TryConvertToVirtualChars(stringToken); + + foreach (var ch in chars) { - var isInterpolation = IsInterpolation; - var chars = charService.TryConvertToVirtualChars(stringToken); + // just build the verbatim string by concatenating all the chars in the original + // string. The only exceptions are double-quotes which need to be doubled up in the + // final string, and curlies which need to be doubled in interpolations. + ch.AppendTo(sb); - foreach (var ch in chars) - { - // just build the verbatim string by concatenating all the chars in the original - // string. The only exceptions are double-quotes which need to be doubled up in the - // final string, and curlies which need to be doubled in interpolations. + if (ShouldDouble(ch, isInterpolation)) ch.AppendTo(sb); + } - if (ShouldDouble(ch, isInterpolation)) - ch.AppendTo(sb); - } - - static bool ShouldDouble(VirtualChar ch, bool isInterpolation) - { - if (ch == DoubleQuote) - return true; + static bool ShouldDouble(VirtualChar ch, bool isInterpolation) + { + if (ch == DoubleQuote) + return true; - if (isInterpolation) - return IsOpenOrCloseBrace(ch); + if (isInterpolation) + return IsOpenOrCloseBrace(ch); - return false; - } + return false; } + } - private static bool IsOpenOrCloseBrace(VirtualChar ch) - => ch == OpenBrace || ch == CloseBrace; + private static bool IsOpenOrCloseBrace(VirtualChar ch) + => ch == OpenBrace || ch == CloseBrace; - protected void AddRegularStringText( - IVirtualCharService charService, StringBuilder sb, SyntaxToken stringToken) - { - var isInterpolation = IsInterpolation; - var chars = charService.TryConvertToVirtualChars(stringToken); + protected void AddRegularStringText( + IVirtualCharService charService, StringBuilder sb, SyntaxToken stringToken) + { + var isInterpolation = IsInterpolation; + var chars = charService.TryConvertToVirtualChars(stringToken); - foreach (var ch in chars) + foreach (var ch in chars) + { + if (charService.TryGetEscapeCharacter(ch, out var escaped)) { - if (charService.TryGetEscapeCharacter(ch, out var escaped)) - { - sb.Append('\\'); - sb.Append(escaped); - } - else - { - ch.AppendTo(sb); + sb.Append('\\'); + sb.Append(escaped); + } + else + { + ch.AppendTo(sb); - // if it's an interpolation, we need to double-up open/close braces. - if (isInterpolation && IsOpenOrCloseBrace(ch)) - ch.AppendTo(sb); - } + // if it's an interpolation, we need to double-up open/close braces. + if (isInterpolation && IsOpenOrCloseBrace(ch)) + ch.AppendTo(sb); } } + } - private static bool ContainsSimpleEscape( - IVirtualCharService charService, ArrayBuilder subTokens) + private static bool ContainsSimpleEscape( + IVirtualCharService charService, ArrayBuilder subTokens) + { + foreach (var subToken in subTokens) { - foreach (var subToken in subTokens) - { - var chars = charService.TryConvertToVirtualChars(subToken); + var chars = charService.TryConvertToVirtualChars(subToken); - // This was checked above. - Debug.Assert(!chars.IsDefault); - if (ContainsSimpleEscape(chars)) - return true; - } - - return false; + // This was checked above. + Debug.Assert(!chars.IsDefault); + if (ContainsSimpleEscape(chars)) + return true; } - private static bool ContainsSimpleEscape(VirtualCharSequence chars) + return false; + } + + private static bool ContainsSimpleEscape(VirtualCharSequence chars) + { + foreach (var ch in chars) { - foreach (var ch in chars) + // look for two-character escapes that start with \ . i.e. \n . Note: \0 + // cannot be encoded into a verbatim string, so don't offer to convert if we have + // that. + if (ch.Span.Length == 2 && ch.Rune.Value != 0) { - // look for two-character escapes that start with \ . i.e. \n . Note: \0 - // cannot be encoded into a verbatim string, so don't offer to convert if we have - // that. - if (ch.Span.Length == 2 && ch.Rune.Value != 0) - { - return true; - } + return true; } - - return false; } + + return false; } } diff --git a/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/ConvertBetweenRegularAndVerbatimInterpolatedStringCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/ConvertBetweenRegularAndVerbatimInterpolatedStringCodeRefactoringProvider.cs index e69778d9e002e..eb83188912560 100644 --- a/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/ConvertBetweenRegularAndVerbatimInterpolatedStringCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/ConvertBetweenRegularAndVerbatimInterpolatedStringCodeRefactoringProvider.cs @@ -11,84 +11,83 @@ using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.CSharp.ConvertBetweenRegularAndVerbatimString +namespace Microsoft.CodeAnalysis.CSharp.ConvertBetweenRegularAndVerbatimString; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertBetweenRegularAndVerbatimInterpolatedString), Shared] +[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString)] +internal class ConvertBetweenRegularAndVerbatimInterpolatedStringCodeRefactoringProvider + : AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertBetweenRegularAndVerbatimInterpolatedString), Shared] - [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString)] - internal class ConvertBetweenRegularAndVerbatimInterpolatedStringCodeRefactoringProvider - : AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public ConvertBetweenRegularAndVerbatimInterpolatedStringCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public ConvertBetweenRegularAndVerbatimInterpolatedStringCodeRefactoringProvider() - { - } + } - protected override bool IsInterpolation { get; } = true; + protected override bool IsInterpolation { get; } = true; - protected override bool IsAppropriateLiteralKind(InterpolatedStringExpressionSyntax literalExpression) - => true; + protected override bool IsAppropriateLiteralKind(InterpolatedStringExpressionSyntax literalExpression) + => true; - protected override void AddSubStringTokens(InterpolatedStringExpressionSyntax literalExpression, ArrayBuilder subStringTokens) + protected override void AddSubStringTokens(InterpolatedStringExpressionSyntax literalExpression, ArrayBuilder subStringTokens) + { + foreach (var content in literalExpression.Contents) { - foreach (var content in literalExpression.Contents) - { - if (content is InterpolatedStringTextSyntax textSyntax) - subStringTokens.Add(textSyntax.TextToken); - } + if (content is InterpolatedStringTextSyntax textSyntax) + subStringTokens.Add(textSyntax.TextToken); } + } - protected override bool IsVerbatim(InterpolatedStringExpressionSyntax literalExpression) - => literalExpression.StringStartToken.Kind() == SyntaxKind.InterpolatedVerbatimStringStartToken; + protected override bool IsVerbatim(InterpolatedStringExpressionSyntax literalExpression) + => literalExpression.StringStartToken.Kind() == SyntaxKind.InterpolatedVerbatimStringStartToken; - private static InterpolatedStringExpressionSyntax Convert( - IVirtualCharService charService, StringBuilder sb, InterpolatedStringExpressionSyntax stringExpression, - SyntaxKind newStartKind, Action addStringText) - { - using var _ = ArrayBuilder.GetInstance(out var newContents); + private static InterpolatedStringExpressionSyntax Convert( + IVirtualCharService charService, StringBuilder sb, InterpolatedStringExpressionSyntax stringExpression, + SyntaxKind newStartKind, Action addStringText) + { + using var _ = ArrayBuilder.GetInstance(out var newContents); - foreach (var content in stringExpression.Contents) + foreach (var content in stringExpression.Contents) + { + if (content is InterpolatedStringTextSyntax textSyntax) { - if (content is InterpolatedStringTextSyntax textSyntax) - { - // Ensure our temp builder is in a empty starting state. - sb.Clear(); + // Ensure our temp builder is in a empty starting state. + sb.Clear(); - addStringText(charService, sb, textSyntax.TextToken); - newContents.Add(textSyntax.WithTextToken(CreateTextToken(textSyntax.TextToken, sb))); - } - else - { - // not text (i.e. it's an interpolation). just add as is. - newContents.Add(content); - } + addStringText(charService, sb, textSyntax.TextToken); + newContents.Add(textSyntax.WithTextToken(CreateTextToken(textSyntax.TextToken, sb))); + } + else + { + // not text (i.e. it's an interpolation). just add as is. + newContents.Add(content); } + } - var startToken = stringExpression.StringStartToken; - var newStartToken = SyntaxFactory.Token( - leading: startToken.LeadingTrivia, - kind: newStartKind, - trailing: startToken.TrailingTrivia); + var startToken = stringExpression.StringStartToken; + var newStartToken = SyntaxFactory.Token( + leading: startToken.LeadingTrivia, + kind: newStartKind, + trailing: startToken.TrailingTrivia); - return stringExpression.Update( - newStartToken, - [.. newContents], - stringExpression.StringEndToken); - } + return stringExpression.Update( + newStartToken, + [.. newContents], + stringExpression.StringEndToken); + } - private static SyntaxToken CreateTextToken(SyntaxToken textToken, StringBuilder sb) - => SyntaxFactory.Token( - leading: textToken.LeadingTrivia, - SyntaxKind.InterpolatedStringTextToken, - sb.ToString(), valueText: "", - trailing: textToken.TrailingTrivia); + private static SyntaxToken CreateTextToken(SyntaxToken textToken, StringBuilder sb) + => SyntaxFactory.Token( + leading: textToken.LeadingTrivia, + SyntaxKind.InterpolatedStringTextToken, + sb.ToString(), valueText: "", + trailing: textToken.TrailingTrivia); - protected override InterpolatedStringExpressionSyntax CreateVerbatimStringExpression(IVirtualCharService charService, StringBuilder sb, InterpolatedStringExpressionSyntax stringExpression) - => Convert(charService, sb, stringExpression, - SyntaxKind.InterpolatedVerbatimStringStartToken, AddVerbatimStringText); + protected override InterpolatedStringExpressionSyntax CreateVerbatimStringExpression(IVirtualCharService charService, StringBuilder sb, InterpolatedStringExpressionSyntax stringExpression) + => Convert(charService, sb, stringExpression, + SyntaxKind.InterpolatedVerbatimStringStartToken, AddVerbatimStringText); - protected override InterpolatedStringExpressionSyntax CreateRegularStringExpression(IVirtualCharService charService, StringBuilder sb, InterpolatedStringExpressionSyntax stringExpression) - => Convert(charService, sb, stringExpression, - SyntaxKind.InterpolatedStringStartToken, AddRegularStringText); - } + protected override InterpolatedStringExpressionSyntax CreateRegularStringExpression(IVirtualCharService charService, StringBuilder sb, InterpolatedStringExpressionSyntax stringExpression) + => Convert(charService, sb, stringExpression, + SyntaxKind.InterpolatedStringStartToken, AddRegularStringText); } diff --git a/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/ConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/ConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider.cs index 370671a15dca9..f9920df3cd32c 100644 --- a/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/ConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertBetweenRegularAndVerbatimString/ConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider.cs @@ -11,54 +11,53 @@ using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.CSharp.ConvertBetweenRegularAndVerbatimString +namespace Microsoft.CodeAnalysis.CSharp.ConvertBetweenRegularAndVerbatimString; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertBetweenRegularAndVerbatimString), Shared] +[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString)] +internal class ConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider + : AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertBetweenRegularAndVerbatimString), Shared] - [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString)] - internal class ConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider - : AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public ConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public ConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider() - { - } - - protected override bool IsInterpolation { get; } = false; + } - protected override bool IsAppropriateLiteralKind(LiteralExpressionSyntax literalExpression) - => literalExpression.Kind() == SyntaxKind.StringLiteralExpression; + protected override bool IsInterpolation { get; } = false; - protected override void AddSubStringTokens(LiteralExpressionSyntax literalExpression, ArrayBuilder subStringTokens) - => subStringTokens.Add(literalExpression.Token); + protected override bool IsAppropriateLiteralKind(LiteralExpressionSyntax literalExpression) + => literalExpression.Kind() == SyntaxKind.StringLiteralExpression; - protected override bool IsVerbatim(LiteralExpressionSyntax literalExpression) - => CSharpSyntaxFacts.Instance.IsVerbatimStringLiteral(literalExpression.Token); + protected override void AddSubStringTokens(LiteralExpressionSyntax literalExpression, ArrayBuilder subStringTokens) + => subStringTokens.Add(literalExpression.Token); - protected override LiteralExpressionSyntax CreateVerbatimStringExpression(IVirtualCharService charService, StringBuilder sb, LiteralExpressionSyntax stringExpression) - { - sb.Append('@'); - sb.Append(DoubleQuote); - AddVerbatimStringText(charService, sb, stringExpression.Token); - sb.Append(DoubleQuote); + protected override bool IsVerbatim(LiteralExpressionSyntax literalExpression) + => CSharpSyntaxFacts.Instance.IsVerbatimStringLiteral(literalExpression.Token); - return stringExpression.WithToken(CreateStringToken(sb)); - } + protected override LiteralExpressionSyntax CreateVerbatimStringExpression(IVirtualCharService charService, StringBuilder sb, LiteralExpressionSyntax stringExpression) + { + sb.Append('@'); + sb.Append(DoubleQuote); + AddVerbatimStringText(charService, sb, stringExpression.Token); + sb.Append(DoubleQuote); - protected override LiteralExpressionSyntax CreateRegularStringExpression(IVirtualCharService charService, StringBuilder sb, LiteralExpressionSyntax stringExpression) - { - sb.Append(DoubleQuote); - AddRegularStringText(charService, sb, stringExpression.Token); - sb.Append(DoubleQuote); + return stringExpression.WithToken(CreateStringToken(sb)); + } - return stringExpression.WithToken(CreateStringToken(sb)); - } + protected override LiteralExpressionSyntax CreateRegularStringExpression(IVirtualCharService charService, StringBuilder sb, LiteralExpressionSyntax stringExpression) + { + sb.Append(DoubleQuote); + AddRegularStringText(charService, sb, stringExpression.Token); + sb.Append(DoubleQuote); - private static SyntaxToken CreateStringToken(StringBuilder sb) - => SyntaxFactory.Token( - leading: default, - SyntaxKind.StringLiteralToken, - sb.ToString(), valueText: "", - trailing: default); + return stringExpression.WithToken(CreateStringToken(sb)); } + + private static SyntaxToken CreateStringToken(StringBuilder sb) + => SyntaxFactory.Token( + leading: default, + SyntaxKind.StringLiteralToken, + sb.ToString(), valueText: "", + trailing: default); } diff --git a/src/Features/CSharp/Portable/ConvertCast/CSharpConvertDirectCastToTryCastCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertCast/CSharpConvertDirectCastToTryCastCodeRefactoringProvider.cs index e60ea133b6cce..6f7082166672b 100644 --- a/src/Features/CSharp/Portable/ConvertCast/CSharpConvertDirectCastToTryCastCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertCast/CSharpConvertDirectCastToTryCastCodeRefactoringProvider.cs @@ -10,61 +10,60 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -namespace Microsoft.CodeAnalysis.CSharp.ConvertCast +namespace Microsoft.CodeAnalysis.CSharp.ConvertCast; + +/// +/// Refactor: +/// var o = (object)1; +/// +/// Into: +/// var o = 1 as object; +/// +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertDirectCastToTryCast), Shared] +internal partial class CSharpConvertDirectCastToTryCastCodeRefactoringProvider + : AbstractConvertCastCodeRefactoringProvider { - /// - /// Refactor: - /// var o = (object)1; - /// - /// Into: - /// var o = 1 as object; - /// - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertDirectCastToTryCast), Shared] - internal partial class CSharpConvertDirectCastToTryCastCodeRefactoringProvider - : AbstractConvertCastCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertDirectCastToTryCastCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertDirectCastToTryCastCodeRefactoringProvider() - { - } + } - protected override string GetTitle() - => CSharpFeaturesResources.Change_to_as_expression; + protected override string GetTitle() + => CSharpFeaturesResources.Change_to_as_expression; - protected override int FromKind => (int)SyntaxKind.CastExpression; + protected override int FromKind => (int)SyntaxKind.CastExpression; - protected override TypeSyntax GetTypeNode(CastExpressionSyntax from) - => from.Type; + protected override TypeSyntax GetTypeNode(CastExpressionSyntax from) + => from.Type; - protected override BinaryExpressionSyntax ConvertExpression(CastExpressionSyntax castExpression, NullableContext nullableContext, bool isReferenceType) - { - var typeNode = castExpression.Type; - var expression = castExpression.Expression; + protected override BinaryExpressionSyntax ConvertExpression(CastExpressionSyntax castExpression, NullableContext nullableContext, bool isReferenceType) + { + var typeNode = castExpression.Type; + var expression = castExpression.Expression; - // Cannot use nullable reference types in `as` expression - // This check ensures we unwrap any nullables, e.g. - // `(string?)null` -> `null as string` - if (typeNode is NullableTypeSyntax nullableType && isReferenceType) - typeNode = nullableType.ElementType; + // Cannot use nullable reference types in `as` expression + // This check ensures we unwrap any nullables, e.g. + // `(string?)null` -> `null as string` + if (typeNode is NullableTypeSyntax nullableType && isReferenceType) + typeNode = nullableType.ElementType; - // Trivia handling - // #0 ( #1 Type #2 ) #3 expr #4 - // #0 #3 expr as #1 Type #2 #4 - // If #1 is present a new line is added after "as" because of elastic trivia on "as" - // #3 is kept with the expression and moves - typeNode = typeNode.WithLeadingTrivia(castExpression.OpenParenToken.TrailingTrivia); - var middleTrivia = castExpression.CloseParenToken.TrailingTrivia.SkipInitialWhitespace(); - var newLeadingTrivia = castExpression.GetLeadingTrivia().AddRange(middleTrivia); - var newTrailingTrivia = typeNode.GetTrailingTrivia().WithoutLeadingBlankLines().AddRange(expression.GetTrailingTrivia().WithoutLeadingBlankLines()); - expression = expression.WithoutTrailingTrivia(); - typeNode = typeNode.WithoutTrailingTrivia(); + // Trivia handling + // #0 ( #1 Type #2 ) #3 expr #4 + // #0 #3 expr as #1 Type #2 #4 + // If #1 is present a new line is added after "as" because of elastic trivia on "as" + // #3 is kept with the expression and moves + typeNode = typeNode.WithLeadingTrivia(castExpression.OpenParenToken.TrailingTrivia); + var middleTrivia = castExpression.CloseParenToken.TrailingTrivia.SkipInitialWhitespace(); + var newLeadingTrivia = castExpression.GetLeadingTrivia().AddRange(middleTrivia); + var newTrailingTrivia = typeNode.GetTrailingTrivia().WithoutLeadingBlankLines().AddRange(expression.GetTrailingTrivia().WithoutLeadingBlankLines()); + expression = expression.WithoutTrailingTrivia(); + typeNode = typeNode.WithoutTrailingTrivia(); - var asExpression = BinaryExpression(SyntaxKind.AsExpression, expression, typeNode) - .WithLeadingTrivia(newLeadingTrivia) - .WithTrailingTrivia(newTrailingTrivia); + var asExpression = BinaryExpression(SyntaxKind.AsExpression, expression, typeNode) + .WithLeadingTrivia(newLeadingTrivia) + .WithTrailingTrivia(newTrailingTrivia); - return asExpression; - } + return asExpression; } } diff --git a/src/Features/CSharp/Portable/ConvertCast/CSharpConvertTryCastToDirectCastCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertCast/CSharpConvertTryCastToDirectCastCodeRefactoringProvider.cs index 28de291db5754..e5e8ec880cc19 100644 --- a/src/Features/CSharp/Portable/ConvertCast/CSharpConvertTryCastToDirectCastCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertCast/CSharpConvertTryCastToDirectCastCodeRefactoringProvider.cs @@ -10,58 +10,57 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -namespace Microsoft.CodeAnalysis.CSharp.ConvertCast +namespace Microsoft.CodeAnalysis.CSharp.ConvertCast; + +/// +/// Refactor: +/// var o = 1 as object; +/// +/// Into: +/// var o = (object)1; +/// +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertTryCastToDirectCast), Shared] +internal partial class CSharpConvertTryCastToDirectCastCodeRefactoringProvider + : AbstractConvertCastCodeRefactoringProvider { - /// - /// Refactor: - /// var o = 1 as object; - /// - /// Into: - /// var o = (object)1; - /// - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertTryCastToDirectCast), Shared] - internal partial class CSharpConvertTryCastToDirectCastCodeRefactoringProvider - : AbstractConvertCastCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertTryCastToDirectCastCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertTryCastToDirectCastCodeRefactoringProvider() - { - } + } - protected override string GetTitle() - => CSharpFeaturesResources.Change_to_cast; + protected override string GetTitle() + => CSharpFeaturesResources.Change_to_cast; - protected override int FromKind => (int)SyntaxKind.AsExpression; + protected override int FromKind => (int)SyntaxKind.AsExpression; - protected override TypeSyntax GetTypeNode(BinaryExpressionSyntax expression) - => (TypeSyntax)expression.Right; + protected override TypeSyntax GetTypeNode(BinaryExpressionSyntax expression) + => (TypeSyntax)expression.Right; - protected override CastExpressionSyntax ConvertExpression(BinaryExpressionSyntax asExpression, NullableContext nullableContext, bool isReferenceType) - { - var expression = asExpression.Left; - var typeNode = GetTypeNode(asExpression); + protected override CastExpressionSyntax ConvertExpression(BinaryExpressionSyntax asExpression, NullableContext nullableContext, bool isReferenceType) + { + var expression = asExpression.Left; + var typeNode = GetTypeNode(asExpression); - // Trivia handling: - // #0 exp #1 as #2 Type #3 - // #0 #2 (Type)exp #1 #3 - // Some trivia in the middle (#1 and #2) is moved to the front or behind the expression - // #1 and #2 change their position in the expression (#2 goes in front to stay near the type and #1 to the end to stay near the expression) - var openParen = Token(SyntaxKind.OpenParenToken); - var closeParen = Token(SyntaxKind.CloseParenToken); - var newTrailingTrivia = asExpression.Left.GetTrailingTrivia().SkipInitialWhitespace().ToSyntaxTriviaList().AddRange(asExpression.GetTrailingTrivia()); - var newLeadingTrivia = asExpression.GetLeadingTrivia().AddRange(asExpression.OperatorToken.TrailingTrivia.SkipInitialWhitespace()); - typeNode = typeNode.WithoutTrailingTrivia(); + // Trivia handling: + // #0 exp #1 as #2 Type #3 + // #0 #2 (Type)exp #1 #3 + // Some trivia in the middle (#1 and #2) is moved to the front or behind the expression + // #1 and #2 change their position in the expression (#2 goes in front to stay near the type and #1 to the end to stay near the expression) + var openParen = Token(SyntaxKind.OpenParenToken); + var closeParen = Token(SyntaxKind.CloseParenToken); + var newTrailingTrivia = asExpression.Left.GetTrailingTrivia().SkipInitialWhitespace().ToSyntaxTriviaList().AddRange(asExpression.GetTrailingTrivia()); + var newLeadingTrivia = asExpression.GetLeadingTrivia().AddRange(asExpression.OperatorToken.TrailingTrivia.SkipInitialWhitespace()); + typeNode = typeNode.WithoutTrailingTrivia(); - // Make sure we make reference type nullable when converting expressions like `null as string` -> `(string?)null` - if (expression.IsKind(SyntaxKind.NullLiteralExpression) && nullableContext.HasFlag(NullableContext.AnnotationsEnabled) && isReferenceType) - typeNode = NullableType(typeNode, Token(SyntaxKind.QuestionToken)); + // Make sure we make reference type nullable when converting expressions like `null as string` -> `(string?)null` + if (expression.IsKind(SyntaxKind.NullLiteralExpression) && nullableContext.HasFlag(NullableContext.AnnotationsEnabled) && isReferenceType) + typeNode = NullableType(typeNode, Token(SyntaxKind.QuestionToken)); - var castExpression = CastExpression(openParen, typeNode, closeParen, expression.WithoutTrailingTrivia()) - .WithLeadingTrivia(newLeadingTrivia) - .WithTrailingTrivia(newTrailingTrivia); + var castExpression = CastExpression(openParen, typeNode, closeParen, expression.WithoutTrailingTrivia()) + .WithLeadingTrivia(newLeadingTrivia) + .WithTrailingTrivia(newTrailingTrivia); - return castExpression; - } + return castExpression; } } diff --git a/src/Features/CSharp/Portable/ConvertForEachToFor/CSharpConvertForEachToForCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertForEachToFor/CSharpConvertForEachToForCodeRefactoringProvider.cs index bfddf9bad2cad..a6f7e8c9a272d 100644 --- a/src/Features/CSharp/Portable/ConvertForEachToFor/CSharpConvertForEachToForCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertForEachToFor/CSharpConvertForEachToForCodeRefactoringProvider.cs @@ -16,139 +16,138 @@ using Microsoft.CodeAnalysis.Operations; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertForEachToFor +namespace Microsoft.CodeAnalysis.CSharp.ConvertForEachToFor; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertForEachToFor), Shared] +internal sealed class CSharpConvertForEachToForCodeRefactoringProvider : + AbstractConvertForEachToForCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertForEachToFor), Shared] - internal sealed class CSharpConvertForEachToForCodeRefactoringProvider : - AbstractConvertForEachToForCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertForEachToForCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertForEachToForCodeRefactoringProvider() + } + + protected override string Title => CSharpFeaturesResources.Convert_to_for; + + // https://github.com/dotnet/roslyn/issues/30584: Add tests for this scenario + protected override bool IsValid(ForEachStatementSyntax foreachStatement) + => foreachStatement.AwaitKeyword == default; + + protected override bool ValidLocation(ForEachInfo foreachInfo) + { + if (!foreachInfo.RequireCollectionStatement) { + return true; } - protected override string Title => CSharpFeaturesResources.Convert_to_for; + // for now, we don't support converting in embedded statement if + // new local declaration for collection is required. + // we can support this by using Introduce local variable service + // but the service is not currently written in a way that can be + // easily reused here. + return foreachInfo.ForEachStatement.Parent.IsKind(SyntaxKind.Block); + } - // https://github.com/dotnet/roslyn/issues/30584: Add tests for this scenario - protected override bool IsValid(ForEachStatementSyntax foreachStatement) - => foreachStatement.AwaitKeyword == default; + protected override (SyntaxNode start, SyntaxNode end) GetForEachBody(ForEachStatementSyntax foreachStatement) + => (foreachStatement.Statement, foreachStatement.Statement); - protected override bool ValidLocation(ForEachInfo foreachInfo) + protected override void ConvertToForStatement( + SemanticModel model, ForEachInfo foreachInfo, SyntaxEditor editor, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var generator = editor.Generator; + var foreachStatement = foreachInfo.ForEachStatement; + + var foreachCollectionExpression = foreachStatement.Expression; + var collectionVariable = GetCollectionVariableName( + model, generator, foreachInfo, foreachCollectionExpression, cancellationToken); + + var typeSymbol = foreachInfo.ExplicitCastInterface ?? + model.GetTypeInfo(foreachCollectionExpression, cancellationToken).Type ?? + model.Compilation.GetSpecialType(SpecialType.System_Object); + + var collectionStatementType = typeSymbol.GenerateTypeSyntax(); + + // first, see whether we need to introduce new statement to capture collection + IntroduceCollectionStatement( + foreachInfo, editor, collectionStatementType, foreachCollectionExpression, collectionVariable); + + var indexVariable = CreateUniqueName(foreachInfo.SemanticFacts, model, foreachStatement.Statement, "i", cancellationToken); + + // do not cast when the element is identity - fixes 'var x in T![]' under nullable context + var foreachStatementInfo = model.GetForEachStatementInfo(foreachStatement); + var donotCastElement = foreachStatementInfo.ElementConversion.IsIdentity; + + // put variable statement in body + var bodyStatement = GetForLoopBody(generator, foreachInfo, collectionVariable, indexVariable, donotCastElement); + + // create for statement from foreach statement + var forStatement = SyntaxFactory.ForStatement( + SyntaxFactory.VariableDeclaration( + model.Compilation.GetSpecialType(SpecialType.System_Int32).GenerateTypeSyntax(), + [SyntaxFactory.VariableDeclarator( + indexVariable.WithAdditionalAnnotations(RenameAnnotation.Create()), + argumentList: null, + SyntaxFactory.EqualsValueClause((ExpressionSyntax)generator.LiteralExpression(0)))]), + initializers: [], + (ExpressionSyntax)generator.LessThanExpression( + generator.IdentifierName(indexVariable), + generator.MemberAccessExpression(collectionVariable, foreachInfo.CountName)), + [SyntaxFactory.PostfixUnaryExpression( + SyntaxKind.PostIncrementExpression, SyntaxFactory.IdentifierName(indexVariable))], + bodyStatement); + + if (!foreachInfo.RequireCollectionStatement) { - if (!foreachInfo.RequireCollectionStatement) - { - return true; - } - - // for now, we don't support converting in embedded statement if - // new local declaration for collection is required. - // we can support this by using Introduce local variable service - // but the service is not currently written in a way that can be - // easily reused here. - return foreachInfo.ForEachStatement.Parent.IsKind(SyntaxKind.Block); + // move comments before "foreach" keyword to "for". if collection statement is introduced, + // it should have attached to the new collection statement, so no need to do it here. + forStatement = forStatement.WithLeadingTrivia(foreachStatement.GetLeadingTrivia()); } - protected override (SyntaxNode start, SyntaxNode end) GetForEachBody(ForEachStatementSyntax foreachStatement) - => (foreachStatement.Statement, foreachStatement.Statement); + // replace close parenthese from "foreach" statement + forStatement = forStatement.WithCloseParenToken(foreachStatement.CloseParenToken); + + editor.ReplaceNode(foreachStatement, forStatement); + } - protected override void ConvertToForStatement( - SemanticModel model, ForEachInfo foreachInfo, SyntaxEditor editor, CancellationToken cancellationToken) + private StatementSyntax GetForLoopBody( + SyntaxGenerator generator, ForEachInfo foreachInfo, SyntaxNode collectionVariableName, SyntaxToken indexVariable, bool donotCastElement) + { + var foreachStatement = foreachInfo.ForEachStatement; + if (foreachStatement.Statement is EmptyStatementSyntax) { - cancellationToken.ThrowIfCancellationRequested(); - - var generator = editor.Generator; - var foreachStatement = foreachInfo.ForEachStatement; - - var foreachCollectionExpression = foreachStatement.Expression; - var collectionVariable = GetCollectionVariableName( - model, generator, foreachInfo, foreachCollectionExpression, cancellationToken); - - var typeSymbol = foreachInfo.ExplicitCastInterface ?? - model.GetTypeInfo(foreachCollectionExpression, cancellationToken).Type ?? - model.Compilation.GetSpecialType(SpecialType.System_Object); - - var collectionStatementType = typeSymbol.GenerateTypeSyntax(); - - // first, see whether we need to introduce new statement to capture collection - IntroduceCollectionStatement( - foreachInfo, editor, collectionStatementType, foreachCollectionExpression, collectionVariable); - - var indexVariable = CreateUniqueName(foreachInfo.SemanticFacts, model, foreachStatement.Statement, "i", cancellationToken); - - // do not cast when the element is identity - fixes 'var x in T![]' under nullable context - var foreachStatementInfo = model.GetForEachStatementInfo(foreachStatement); - var donotCastElement = foreachStatementInfo.ElementConversion.IsIdentity; - - // put variable statement in body - var bodyStatement = GetForLoopBody(generator, foreachInfo, collectionVariable, indexVariable, donotCastElement); - - // create for statement from foreach statement - var forStatement = SyntaxFactory.ForStatement( - SyntaxFactory.VariableDeclaration( - model.Compilation.GetSpecialType(SpecialType.System_Int32).GenerateTypeSyntax(), - [SyntaxFactory.VariableDeclarator( - indexVariable.WithAdditionalAnnotations(RenameAnnotation.Create()), - argumentList: null, - SyntaxFactory.EqualsValueClause((ExpressionSyntax)generator.LiteralExpression(0)))]), - initializers: [], - (ExpressionSyntax)generator.LessThanExpression( - generator.IdentifierName(indexVariable), - generator.MemberAccessExpression(collectionVariable, foreachInfo.CountName)), - [SyntaxFactory.PostfixUnaryExpression( - SyntaxKind.PostIncrementExpression, SyntaxFactory.IdentifierName(indexVariable))], - bodyStatement); - - if (!foreachInfo.RequireCollectionStatement) - { - // move comments before "foreach" keyword to "for". if collection statement is introduced, - // it should have attached to the new collection statement, so no need to do it here. - forStatement = forStatement.WithLeadingTrivia(foreachStatement.GetLeadingTrivia()); - } + return foreachStatement.Statement; + } - // replace close parenthese from "foreach" statement - forStatement = forStatement.WithCloseParenToken(foreachStatement.CloseParenToken); + // create variable statement + var variableStatement = AddItemVariableDeclaration( + generator, foreachInfo.ForEachElementType.GenerateTypeSyntax(), + foreachStatement.Identifier, donotCastElement ? null : foreachInfo.ForEachElementType, + collectionVariableName, indexVariable); - editor.ReplaceNode(foreachStatement, forStatement); + var bodyBlock = foreachStatement.Statement is BlockSyntax block ? block : SyntaxFactory.Block(foreachStatement.Statement); + if (bodyBlock.Statements.Count == 0) + { + // If the block was empty, still put the new variable inside of it. This handles the case where the user + // writes the foreach and immediately decides to change it to a for-loop. Now they'll still have their + // variable to use in the body instead of having to write it again. + return bodyBlock.AddStatements(variableStatement); } - - private StatementSyntax GetForLoopBody( - SyntaxGenerator generator, ForEachInfo foreachInfo, SyntaxNode collectionVariableName, SyntaxToken indexVariable, bool donotCastElement) + else { - var foreachStatement = foreachInfo.ForEachStatement; - if (foreachStatement.Statement is EmptyStatementSyntax) + if (IsForEachVariableWrittenInside) { - return foreachStatement.Statement; + variableStatement = variableStatement.WithAdditionalAnnotations(CreateWarningAnnotation()); } - // create variable statement - var variableStatement = AddItemVariableDeclaration( - generator, foreachInfo.ForEachElementType.GenerateTypeSyntax(), - foreachStatement.Identifier, donotCastElement ? null : foreachInfo.ForEachElementType, - collectionVariableName, indexVariable); - - var bodyBlock = foreachStatement.Statement is BlockSyntax block ? block : SyntaxFactory.Block(foreachStatement.Statement); - if (bodyBlock.Statements.Count == 0) - { - // If the block was empty, still put the new variable inside of it. This handles the case where the user - // writes the foreach and immediately decides to change it to a for-loop. Now they'll still have their - // variable to use in the body instead of having to write it again. - return bodyBlock.AddStatements(variableStatement); - } - else - { - if (IsForEachVariableWrittenInside) - { - variableStatement = variableStatement.WithAdditionalAnnotations(CreateWarningAnnotation()); - } - - return bodyBlock.InsertNodesBefore( - bodyBlock.Statements[0], - SpecializedCollections.SingletonEnumerable(variableStatement)); - } + return bodyBlock.InsertNodesBefore( + bodyBlock.Statements[0], + SpecializedCollections.SingletonEnumerable(variableStatement)); } - - protected override bool IsSupported(ILocalSymbol foreachVariable, IForEachLoopOperation forEachOperation, ForEachStatementSyntax foreachStatement) - => true; } + + protected override bool IsSupported(ILocalSymbol foreachVariable, IForEachLoopOperation forEachOperation, ForEachStatementSyntax foreachStatement) + => true; } diff --git a/src/Features/CSharp/Portable/ConvertForToForEach/CSharpConvertForToForEachCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertForToForEach/CSharpConvertForToForEachCodeRefactoringProvider.cs index a3587b160f6ba..5cf54331654f3 100644 --- a/src/Features/CSharp/Portable/ConvertForToForEach/CSharpConvertForToForEachCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertForToForEach/CSharpConvertForToForEachCodeRefactoringProvider.cs @@ -10,125 +10,124 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.ConvertForToForEach +namespace Microsoft.CodeAnalysis.CSharp.ConvertForToForEach; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertForToForEach), Shared] +internal class CSharpConvertForToForEachCodeRefactoringProvider : + AbstractConvertForToForEachCodeRefactoringProvider< + StatementSyntax, + ForStatementSyntax, + ExpressionSyntax, + MemberAccessExpressionSyntax, + TypeSyntax, + VariableDeclaratorSyntax> { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertForToForEach), Shared] - internal class CSharpConvertForToForEachCodeRefactoringProvider : - AbstractConvertForToForEachCodeRefactoringProvider< - StatementSyntax, - ForStatementSyntax, - ExpressionSyntax, - MemberAccessExpressionSyntax, - TypeSyntax, - VariableDeclaratorSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertForToForEachCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertForToForEachCodeRefactoringProvider() - { - } + } + + protected override string GetTitle() + => CSharpFeaturesResources.Convert_to_foreach; + + protected override SyntaxList GetBodyStatements(ForStatementSyntax forStatement) + => forStatement.Statement is BlockSyntax block + ? block.Statements + : [forStatement.Statement]; + + protected override bool TryGetForStatementComponents( + ForStatementSyntax forStatement, + out SyntaxToken iterationVariable, + [NotNullWhen(true)] out ExpressionSyntax? initializer, + [NotNullWhen(true)] out MemberAccessExpressionSyntax? memberAccess, + out ExpressionSyntax? stepValueExpressionOpt, + CancellationToken cancellationToken) + { + // Look for very specific forms. Basically, only minor variations around: + // for (var i = 0; i < expr.Lenth; i++) - protected override string GetTitle() - => CSharpFeaturesResources.Convert_to_foreach; - - protected override SyntaxList GetBodyStatements(ForStatementSyntax forStatement) - => forStatement.Statement is BlockSyntax block - ? block.Statements - : [forStatement.Statement]; - - protected override bool TryGetForStatementComponents( - ForStatementSyntax forStatement, - out SyntaxToken iterationVariable, - [NotNullWhen(true)] out ExpressionSyntax? initializer, - [NotNullWhen(true)] out MemberAccessExpressionSyntax? memberAccess, - out ExpressionSyntax? stepValueExpressionOpt, - CancellationToken cancellationToken) + if (forStatement is { Declaration.Variables: [{ Initializer: not null } declarator], Condition.RawKind: (int)SyntaxKind.LessThanExpression, Incrementors.Count: 1 }) { - // Look for very specific forms. Basically, only minor variations around: - // for (var i = 0; i < expr.Lenth; i++) + iterationVariable = declarator.Identifier; + initializer = declarator.Initializer.Value; - if (forStatement is { Declaration.Variables: [{ Initializer: not null } declarator], Condition.RawKind: (int)SyntaxKind.LessThanExpression, Incrementors.Count: 1 }) + var binaryExpression = (BinaryExpressionSyntax)forStatement.Condition; + + // Look for: i < expr.Length + if (binaryExpression.Left is IdentifierNameSyntax identifierName && + identifierName.Identifier.ValueText == iterationVariable.ValueText && + binaryExpression.Right is MemberAccessExpressionSyntax right) { - iterationVariable = declarator.Identifier; - initializer = declarator.Initializer.Value; - - var binaryExpression = (BinaryExpressionSyntax)forStatement.Condition; - - // Look for: i < expr.Length - if (binaryExpression.Left is IdentifierNameSyntax identifierName && - identifierName.Identifier.ValueText == iterationVariable.ValueText && - binaryExpression.Right is MemberAccessExpressionSyntax right) - { - memberAccess = right; - return TryGetStepValue(iterationVariable, forStatement.Incrementors[0], out stepValueExpressionOpt); - } + memberAccess = right; + return TryGetStepValue(iterationVariable, forStatement.Incrementors[0], out stepValueExpressionOpt); } - - iterationVariable = default; - memberAccess = null; - initializer = null; - stepValueExpressionOpt = null; - return false; } - private static bool TryGetStepValue( - SyntaxToken iterationVariable, ExpressionSyntax incrementor, out ExpressionSyntax? stepValue) - { - // support - // x++ - // ++x - // x += constant_1 - - ExpressionSyntax operand; - switch (incrementor.Kind()) - { - case SyntaxKind.PostIncrementExpression: - operand = ((PostfixUnaryExpressionSyntax)incrementor).Operand; - stepValue = null; - break; - - case SyntaxKind.PreIncrementExpression: - operand = ((PrefixUnaryExpressionSyntax)incrementor).Operand; - stepValue = null; - break; - - case SyntaxKind.AddAssignmentExpression: - var assignment = (AssignmentExpressionSyntax)incrementor; - operand = assignment.Left; - stepValue = assignment.Right; - break; - - default: - stepValue = null; - return false; - } + iterationVariable = default; + memberAccess = null; + initializer = null; + stepValueExpressionOpt = null; + return false; + } - return operand is IdentifierNameSyntax identifierName && - identifierName.Identifier.ValueText == iterationVariable.ValueText; - } + private static bool TryGetStepValue( + SyntaxToken iterationVariable, ExpressionSyntax incrementor, out ExpressionSyntax? stepValue) + { + // support + // x++ + // ++x + // x += constant_1 - protected override SyntaxNode ConvertForNode( - ForStatementSyntax forStatement, - TypeSyntax? typeNode, - SyntaxToken foreachIdentifier, - ExpressionSyntax collectionExpression, - ITypeSymbol iterationVariableType) + ExpressionSyntax operand; + switch (incrementor.Kind()) { - typeNode ??= iterationVariableType.GenerateTypeSyntax(); - - return SyntaxFactory.ForEachStatement( - SyntaxFactory.Token(SyntaxKind.ForEachKeyword).WithTriviaFrom(forStatement.ForKeyword), - forStatement.OpenParenToken, - typeNode, - foreachIdentifier, - SyntaxFactory.Token(SyntaxKind.InKeyword), - collectionExpression, - forStatement.CloseParenToken, - forStatement.Statement); + case SyntaxKind.PostIncrementExpression: + operand = ((PostfixUnaryExpressionSyntax)incrementor).Operand; + stepValue = null; + break; + + case SyntaxKind.PreIncrementExpression: + operand = ((PrefixUnaryExpressionSyntax)incrementor).Operand; + stepValue = null; + break; + + case SyntaxKind.AddAssignmentExpression: + var assignment = (AssignmentExpressionSyntax)incrementor; + operand = assignment.Left; + stepValue = assignment.Right; + break; + + default: + stepValue = null; + return false; } - // C# has no special variable declarator forms that would cause us to not be able to convert. - protected override bool IsValidVariableDeclarator(VariableDeclaratorSyntax firstVariable) - => true; + return operand is IdentifierNameSyntax identifierName && + identifierName.Identifier.ValueText == iterationVariable.ValueText; } + + protected override SyntaxNode ConvertForNode( + ForStatementSyntax forStatement, + TypeSyntax? typeNode, + SyntaxToken foreachIdentifier, + ExpressionSyntax collectionExpression, + ITypeSymbol iterationVariableType) + { + typeNode ??= iterationVariableType.GenerateTypeSyntax(); + + return SyntaxFactory.ForEachStatement( + SyntaxFactory.Token(SyntaxKind.ForEachKeyword).WithTriviaFrom(forStatement.ForKeyword), + forStatement.OpenParenToken, + typeNode, + foreachIdentifier, + SyntaxFactory.Token(SyntaxKind.InKeyword), + collectionExpression, + forStatement.CloseParenToken, + forStatement.Statement); + } + + // C# has no special variable declarator forms that would cause us to not be able to convert. + protected override bool IsValidVariableDeclarator(VariableDeclaratorSyntax firstVariable) + => true; } diff --git a/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.Analyzer.cs b/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.Analyzer.cs index e3bb8e794d65f..83270a6b4732d 100644 --- a/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.Analyzer.cs +++ b/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.Analyzer.cs @@ -9,33 +9,32 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.CSharp.ConvertIfToSwitch +namespace Microsoft.CodeAnalysis.CSharp.ConvertIfToSwitch; + +internal sealed partial class CSharpConvertIfToSwitchCodeRefactoringProvider { - internal sealed partial class CSharpConvertIfToSwitchCodeRefactoringProvider + private sealed class CSharpAnalyzer(ISyntaxFacts syntaxFacts, Feature features) : Analyzer(syntaxFacts, features) { - private sealed class CSharpAnalyzer(ISyntaxFacts syntaxFacts, Feature features) : Analyzer(syntaxFacts, features) - { - public override bool HasUnreachableEndPoint(IOperation operation) - => !operation.SemanticModel.AnalyzeControlFlow(operation.Syntax).EndPointIsReachable; + public override bool HasUnreachableEndPoint(IOperation operation) + => !operation.SemanticModel.AnalyzeControlFlow(operation.Syntax).EndPointIsReachable; - // We do not offer a fix if the if-statement contains a break-statement, e.g. - // - // while (...) - // { - // if (...) { - // break; - // } - // } - // - // When the 'break' moves into the switch, it will have different flow control impact. - public override bool CanConvert(IConditionalOperation operation) - => !operation.SemanticModel.AnalyzeControlFlow(operation.Syntax).ExitPoints.Any(static n => n.IsKind(SyntaxKind.BreakStatement)); + // We do not offer a fix if the if-statement contains a break-statement, e.g. + // + // while (...) + // { + // if (...) { + // break; + // } + // } + // + // When the 'break' moves into the switch, it will have different flow control impact. + public override bool CanConvert(IConditionalOperation operation) + => !operation.SemanticModel.AnalyzeControlFlow(operation.Syntax).ExitPoints.Any(static n => n.IsKind(SyntaxKind.BreakStatement)); - public override bool CanImplicitlyConvert(SemanticModel semanticModel, SyntaxNode syntax, ITypeSymbol targetType) - { - return syntax is ExpressionSyntax expressionSyntax && - semanticModel.ClassifyConversion(expressionSyntax, targetType).IsImplicit; - } + public override bool CanImplicitlyConvert(SemanticModel semanticModel, SyntaxNode syntax, ITypeSymbol targetType) + { + return syntax is ExpressionSyntax expressionSyntax && + semanticModel.ClassifyConversion(expressionSyntax, targetType).IsImplicit; } } } diff --git a/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.Rewriting.cs b/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.Rewriting.cs index b548b50881ce9..b308de1dd5e14 100644 --- a/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.Rewriting.cs +++ b/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.Rewriting.cs @@ -13,134 +13,133 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertIfToSwitch -{ - using static SyntaxFactory; +namespace Microsoft.CodeAnalysis.CSharp.ConvertIfToSwitch; - internal sealed partial class CSharpConvertIfToSwitchCodeRefactoringProvider - { - private static readonly Dictionary s_operatorMap = new Dictionary - { - { BinaryOperatorKind.LessThan, SyntaxKind.LessThanToken }, - { BinaryOperatorKind.GreaterThan, SyntaxKind.GreaterThanToken }, - { BinaryOperatorKind.LessThanOrEqual, SyntaxKind.LessThanEqualsToken }, - { BinaryOperatorKind.GreaterThanOrEqual, SyntaxKind.GreaterThanEqualsToken }, - }; +using static SyntaxFactory; - public override SyntaxNode CreateSwitchExpressionStatement(SyntaxNode target, ImmutableArray sections, Feature feature) - { - return ReturnStatement( - SwitchExpression( - (ExpressionSyntax)target, - [.. sections.Select(section => AsSwitchExpressionArmSyntax(section, feature))])); - } +internal sealed partial class CSharpConvertIfToSwitchCodeRefactoringProvider +{ + private static readonly Dictionary s_operatorMap = new Dictionary + { + { BinaryOperatorKind.LessThan, SyntaxKind.LessThanToken }, + { BinaryOperatorKind.GreaterThan, SyntaxKind.GreaterThanToken }, + { BinaryOperatorKind.LessThanOrEqual, SyntaxKind.LessThanEqualsToken }, + { BinaryOperatorKind.GreaterThanOrEqual, SyntaxKind.GreaterThanEqualsToken }, + }; - private static SwitchExpressionArmSyntax AsSwitchExpressionArmSyntax(AnalyzedSwitchSection section, Feature feature) - { - if (section.Labels.IsDefault) - return SwitchExpressionArm(DiscardPattern(), AsExpressionSyntax(section.Body)); + public override SyntaxNode CreateSwitchExpressionStatement(SyntaxNode target, ImmutableArray sections, Feature feature) + { + return ReturnStatement( + SwitchExpression( + (ExpressionSyntax)target, + [.. sections.Select(section => AsSwitchExpressionArmSyntax(section, feature))])); + } - var pattern = AsPatternSyntax(section.Labels[0].Pattern, feature); - var whenClause = AsWhenClause(section.Labels[0]); + private static SwitchExpressionArmSyntax AsSwitchExpressionArmSyntax(AnalyzedSwitchSection section, Feature feature) + { + if (section.Labels.IsDefault) + return SwitchExpressionArm(DiscardPattern(), AsExpressionSyntax(section.Body)); - Debug.Assert(whenClause == null || section.Labels.Length == 1, "We shouldn't have guards when we're combining multiple cases into a single arm"); + var pattern = AsPatternSyntax(section.Labels[0].Pattern, feature); + var whenClause = AsWhenClause(section.Labels[0]); - for (var i = 1; i < section.Labels.Length; i++) - { - var label = section.Labels[i]; - Debug.Assert(label.Guards.Length == 0, "We shouldn't have guards when we're combining multiple cases into a single arm"); - var nextPattern = AsPatternSyntax(label.Pattern, feature); - pattern = BinaryPattern(SyntaxKind.OrPattern, pattern.Parenthesize(), nextPattern.Parenthesize()); - } + Debug.Assert(whenClause == null || section.Labels.Length == 1, "We shouldn't have guards when we're combining multiple cases into a single arm"); - return SwitchExpressionArm(pattern, whenClause, AsExpressionSyntax(section.Body)); + for (var i = 1; i < section.Labels.Length; i++) + { + var label = section.Labels[i]; + Debug.Assert(label.Guards.Length == 0, "We shouldn't have guards when we're combining multiple cases into a single arm"); + var nextPattern = AsPatternSyntax(label.Pattern, feature); + pattern = BinaryPattern(SyntaxKind.OrPattern, pattern.Parenthesize(), nextPattern.Parenthesize()); } - private static ExpressionSyntax AsExpressionSyntax(IOperation operation) - => operation switch - { - IReturnOperation { ReturnedValue: { } value } => (ExpressionSyntax)value.Syntax, - IThrowOperation { Exception: { } exception } => ThrowExpression((ExpressionSyntax)exception.Syntax), - IBlockOperation op => AsExpressionSyntax(op.Operations.Single()), - var v => throw ExceptionUtilities.UnexpectedValue(v.Kind) - }; + return SwitchExpressionArm(pattern, whenClause, AsExpressionSyntax(section.Body)); + } - public override SyntaxNode CreateSwitchStatement(IfStatementSyntax ifStatement, SyntaxNode expression, IEnumerable sectionList) + private static ExpressionSyntax AsExpressionSyntax(IOperation operation) + => operation switch { - var block = ifStatement.Statement as BlockSyntax; - return SwitchStatement( - switchKeyword: Token(SyntaxKind.SwitchKeyword).WithTriviaFrom(ifStatement.IfKeyword), - openParenToken: ifStatement.OpenParenToken, - expression: (ExpressionSyntax)expression, - closeParenToken: ifStatement.CloseParenToken.WithPrependedLeadingTrivia(ElasticMarker), - openBraceToken: block?.OpenBraceToken ?? Token(SyntaxKind.OpenBraceToken), - sections: [.. sectionList.Cast()], - closeBraceToken: block?.CloseBraceToken.WithoutLeadingTrivia() ?? Token(SyntaxKind.CloseBraceToken)); - } + IReturnOperation { ReturnedValue: { } value } => (ExpressionSyntax)value.Syntax, + IThrowOperation { Exception: { } exception } => ThrowExpression((ExpressionSyntax)exception.Syntax), + IBlockOperation op => AsExpressionSyntax(op.Operations.Single()), + var v => throw ExceptionUtilities.UnexpectedValue(v.Kind) + }; - private static WhenClauseSyntax? AsWhenClause(AnalyzedSwitchLabel label) - => AsWhenClause(label.Guards - .Select(e => e.WalkUpParentheses()) - .AggregateOrDefault((prev, current) => BinaryExpression(SyntaxKind.LogicalAndExpression, prev, current))); + public override SyntaxNode CreateSwitchStatement(IfStatementSyntax ifStatement, SyntaxNode expression, IEnumerable sectionList) + { + var block = ifStatement.Statement as BlockSyntax; + return SwitchStatement( + switchKeyword: Token(SyntaxKind.SwitchKeyword).WithTriviaFrom(ifStatement.IfKeyword), + openParenToken: ifStatement.OpenParenToken, + expression: (ExpressionSyntax)expression, + closeParenToken: ifStatement.CloseParenToken.WithPrependedLeadingTrivia(ElasticMarker), + openBraceToken: block?.OpenBraceToken ?? Token(SyntaxKind.OpenBraceToken), + sections: [.. sectionList.Cast()], + closeBraceToken: block?.CloseBraceToken.WithoutLeadingTrivia() ?? Token(SyntaxKind.CloseBraceToken)); + } - private static WhenClauseSyntax? AsWhenClause(ExpressionSyntax? expression) - => expression is null ? null : WhenClause(expression); + private static WhenClauseSyntax? AsWhenClause(AnalyzedSwitchLabel label) + => AsWhenClause(label.Guards + .Select(e => e.WalkUpParentheses()) + .AggregateOrDefault((prev, current) => BinaryExpression(SyntaxKind.LogicalAndExpression, prev, current))); - public override SyntaxNode AsSwitchLabelSyntax(AnalyzedSwitchLabel label, Feature feature) - => CasePatternSwitchLabel( - AsPatternSyntax(label.Pattern, feature), - AsWhenClause(label), - Token(SyntaxKind.ColonToken)); + private static WhenClauseSyntax? AsWhenClause(ExpressionSyntax? expression) + => expression is null ? null : WhenClause(expression); - private static PatternSyntax AsPatternSyntax(AnalyzedPattern pattern, Feature feature) - => pattern switch - { - AnalyzedPattern.And p => BinaryPattern(SyntaxKind.AndPattern, AsPatternSyntax(p.LeftPattern, feature).Parenthesize(), AsPatternSyntax(p.RightPattern, feature).Parenthesize()), - AnalyzedPattern.Constant p => ConstantPattern(p.ExpressionSyntax), - AnalyzedPattern.Source p => p.PatternSyntax, - AnalyzedPattern.Type p when feature.HasFlag(Feature.TypePattern) => TypePattern((TypeSyntax)p.IsExpressionSyntax.Right), - AnalyzedPattern.Type p => DeclarationPattern((TypeSyntax)p.IsExpressionSyntax.Right, DiscardDesignation()), - AnalyzedPattern.Relational p => RelationalPattern(Token(s_operatorMap[p.OperatorKind]), p.Value), - var p => throw ExceptionUtilities.UnexpectedValue(p) - }; - - public override IEnumerable AsSwitchSectionStatements(IOperation operation) + public override SyntaxNode AsSwitchLabelSyntax(AnalyzedSwitchLabel label, Feature feature) + => CasePatternSwitchLabel( + AsPatternSyntax(label.Pattern, feature), + AsWhenClause(label), + Token(SyntaxKind.ColonToken)); + + private static PatternSyntax AsPatternSyntax(AnalyzedPattern pattern, Feature feature) + => pattern switch { - var node = operation.Syntax; - Debug.Assert(operation.SemanticModel is not null); - var requiresBreak = operation.SemanticModel.AnalyzeControlFlow(node).EndPointIsReachable; - var requiresBlock = !operation.SemanticModel.AnalyzeDataFlow(node).VariablesDeclared.IsDefaultOrEmpty; + AnalyzedPattern.And p => BinaryPattern(SyntaxKind.AndPattern, AsPatternSyntax(p.LeftPattern, feature).Parenthesize(), AsPatternSyntax(p.RightPattern, feature).Parenthesize()), + AnalyzedPattern.Constant p => ConstantPattern(p.ExpressionSyntax), + AnalyzedPattern.Source p => p.PatternSyntax, + AnalyzedPattern.Type p when feature.HasFlag(Feature.TypePattern) => TypePattern((TypeSyntax)p.IsExpressionSyntax.Right), + AnalyzedPattern.Type p => DeclarationPattern((TypeSyntax)p.IsExpressionSyntax.Right, DiscardDesignation()), + AnalyzedPattern.Relational p => RelationalPattern(Token(s_operatorMap[p.OperatorKind]), p.Value), + var p => throw ExceptionUtilities.UnexpectedValue(p) + }; + + public override IEnumerable AsSwitchSectionStatements(IOperation operation) + { + var node = operation.Syntax; + Debug.Assert(operation.SemanticModel is not null); + var requiresBreak = operation.SemanticModel.AnalyzeControlFlow(node).EndPointIsReachable; + var requiresBlock = !operation.SemanticModel.AnalyzeDataFlow(node).VariablesDeclared.IsDefaultOrEmpty; - var statements = ArrayBuilder.GetInstance(); - if (node is BlockSyntax block) + var statements = ArrayBuilder.GetInstance(); + if (node is BlockSyntax block) + { + if (block.Statements.Count == 0) { - if (block.Statements.Count == 0) - { - statements.Add(BreakStatement()); - } - else if (requiresBlock) - { - statements.Add(requiresBreak ? block.AddStatements(BreakStatement()) : block); - } - else - { - statements.AddRange(block.Statements); - if (requiresBreak) - { - statements.Add(BreakStatement().WithLeadingTrivia(block.CloseBraceToken.LeadingTrivia)); - } - } + statements.Add(BreakStatement()); + } + else if (requiresBlock) + { + statements.Add(requiresBreak ? block.AddStatements(BreakStatement()) : block); } else { - statements.Add(node); + statements.AddRange(block.Statements); if (requiresBreak) { - statements.Add(BreakStatement()); + statements.Add(BreakStatement().WithLeadingTrivia(block.CloseBraceToken.LeadingTrivia)); } } - - return statements.ToArrayAndFree(); } + else + { + statements.Add(node); + if (requiresBreak) + { + statements.Add(BreakStatement()); + } + } + + return statements.ToArrayAndFree(); } } diff --git a/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.cs index 96fe41691ff3b..4dfe46d4d01ee 100644 --- a/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.cs @@ -14,48 +14,47 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.CSharp.ConvertIfToSwitch +namespace Microsoft.CodeAnalysis.CSharp.ConvertIfToSwitch; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertIfToSwitch), Shared] +internal sealed partial class CSharpConvertIfToSwitchCodeRefactoringProvider + : AbstractConvertIfToSwitchCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertIfToSwitch), Shared] - internal sealed partial class CSharpConvertIfToSwitchCodeRefactoringProvider - : AbstractConvertIfToSwitchCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertIfToSwitchCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertIfToSwitchCodeRefactoringProvider() - { - } + } - public override string GetTitle(bool forSwitchExpression) - => forSwitchExpression - ? CSharpFeaturesResources.Convert_to_switch_expression - : CSharpFeaturesResources.Convert_to_switch_statement; + public override string GetTitle(bool forSwitchExpression) + => forSwitchExpression + ? CSharpFeaturesResources.Convert_to_switch_expression + : CSharpFeaturesResources.Convert_to_switch_statement; - public override Analyzer CreateAnalyzer(ISyntaxFacts syntaxFacts, ParseOptions options) - { - var version = options.LanguageVersion(); - var features = - (version >= LanguageVersion.CSharp7 ? Feature.SourcePattern | Feature.IsTypePattern | Feature.CaseGuard : 0) | - (version >= LanguageVersion.CSharp8 ? Feature.SwitchExpression : 0) | - (version >= LanguageVersion.CSharp9 ? Feature.RelationalPattern | Feature.OrPattern | Feature.AndPattern | Feature.TypePattern : 0); - return new CSharpAnalyzer(syntaxFacts, features); - } + public override Analyzer CreateAnalyzer(ISyntaxFacts syntaxFacts, ParseOptions options) + { + var version = options.LanguageVersion(); + var features = + (version >= LanguageVersion.CSharp7 ? Feature.SourcePattern | Feature.IsTypePattern | Feature.CaseGuard : 0) | + (version >= LanguageVersion.CSharp8 ? Feature.SwitchExpression : 0) | + (version >= LanguageVersion.CSharp9 ? Feature.RelationalPattern | Feature.OrPattern | Feature.AndPattern | Feature.TypePattern : 0); + return new CSharpAnalyzer(syntaxFacts, features); + } - protected override SyntaxTriviaList GetLeadingTriviaToTransfer(SyntaxNode syntaxToRemove) + protected override SyntaxTriviaList GetLeadingTriviaToTransfer(SyntaxNode syntaxToRemove) + { + if (syntaxToRemove is (IfStatementSyntax or BlockSyntax) and { Parent: ElseClauseSyntax elseClause } && + elseClause.ElseKeyword.LeadingTrivia.Any(t => t.IsSingleOrMultiLineComment())) { - if (syntaxToRemove is (IfStatementSyntax or BlockSyntax) and { Parent: ElseClauseSyntax elseClause } && - elseClause.ElseKeyword.LeadingTrivia.Any(t => t.IsSingleOrMultiLineComment())) - { - // users sometimes write: - // - // // Comment - // else if (x == b) - // - // Attempt to move 'comment' over to the switch section. - return elseClause.ElseKeyword.LeadingTrivia; - } - - return default; + // users sometimes write: + // + // // Comment + // else if (x == b) + // + // Attempt to move 'comment' over to the switch section. + return elseClause.ElseKeyword.LeadingTrivia; } + + return default; } } diff --git a/src/Features/CSharp/Portable/ConvertLinq/CSharpConvertLinqQueryToForEachProvider.cs b/src/Features/CSharp/Portable/ConvertLinq/CSharpConvertLinqQueryToForEachProvider.cs index e479dfdc686fb..a012eb4b00093 100644 --- a/src/Features/CSharp/Portable/ConvertLinq/CSharpConvertLinqQueryToForEachProvider.cs +++ b/src/Features/CSharp/Portable/ConvertLinq/CSharpConvertLinqQueryToForEachProvider.cs @@ -22,962 +22,961 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Simplification; -namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq +namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertLinqQueryToForEach), Shared] +internal sealed class CSharpConvertLinqQueryToForEachProvider : AbstractConvertLinqQueryToForEachProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertLinqQueryToForEach), Shared] - internal sealed class CSharpConvertLinqQueryToForEachProvider : AbstractConvertLinqQueryToForEachProvider + private static readonly TypeSyntax VarNameIdentifier = SyntaxFactory.IdentifierName("var"); + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertLinqQueryToForEachProvider() { - private static readonly TypeSyntax VarNameIdentifier = SyntaxFactory.IdentifierName("var"); + } - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertLinqQueryToForEachProvider() - { - } + protected override string Title => CSharpFeaturesResources.Convert_to_foreach; - protected override string Title => CSharpFeaturesResources.Convert_to_foreach; + protected override bool TryConvert( + QueryExpressionSyntax queryExpression, + SemanticModel semanticModel, + ISemanticFactsService semanticFacts, + CancellationToken cancellationToken, + out DocumentUpdateInfo documentUpdateInfo) + => new Converter(semanticModel, semanticFacts, queryExpression, cancellationToken).TryConvert(out documentUpdateInfo); - protected override bool TryConvert( - QueryExpressionSyntax queryExpression, - SemanticModel semanticModel, - ISemanticFactsService semanticFacts, - CancellationToken cancellationToken, - out DocumentUpdateInfo documentUpdateInfo) - => new Converter(semanticModel, semanticFacts, queryExpression, cancellationToken).TryConvert(out documentUpdateInfo); + /// + /// Finds a QueryExpressionSyntax node for the span. + /// + protected override Task FindNodeToRefactorAsync(CodeRefactoringContext context) + => context.TryGetRelevantNodeAsync(); - /// - /// Finds a QueryExpressionSyntax node for the span. - /// - protected override Task FindNodeToRefactorAsync(CodeRefactoringContext context) - => context.TryGetRelevantNodeAsync(); + private sealed class Converter(SemanticModel semanticModel, ISemanticFactsService semanticFacts, QueryExpressionSyntax source, CancellationToken cancellationToken) + { + private readonly SemanticModel _semanticModel = semanticModel; + private readonly ISemanticFactsService _semanticFacts = semanticFacts; + private readonly CancellationToken _cancellationToken = cancellationToken; + private readonly QueryExpressionSyntax _source = source; + private readonly List _introducedLocalNames = []; - private sealed class Converter(SemanticModel semanticModel, ISemanticFactsService semanticFacts, QueryExpressionSyntax source, CancellationToken cancellationToken) + public bool TryConvert(out DocumentUpdateInfo documentUpdateInfo) { - private readonly SemanticModel _semanticModel = semanticModel; - private readonly ISemanticFactsService _semanticFacts = semanticFacts; - private readonly CancellationToken _cancellationToken = cancellationToken; - private readonly QueryExpressionSyntax _source = source; - private readonly List _introducedLocalNames = []; - - public bool TryConvert(out DocumentUpdateInfo documentUpdateInfo) - { - // Do not try refactoring queries with comments or conditional compilation in them. - // We can consider supporting queries with comments in the future. - if (_source.DescendantTrivia().Any(trivia => trivia is (kind: - SyntaxKind.SingleLineCommentTrivia or - SyntaxKind.MultiLineCommentTrivia or - SyntaxKind.MultiLineDocumentationCommentTrivia) || - _source.ContainsDirectives)) - { - documentUpdateInfo = null; - return false; - } - - // Bail out if there is no chance to convert it even with a local function. - if (!CanTryConvertToLocalFunction() || - !TryCreateStackFromQueryExpression(out var queryExpressionProcessingInfo)) - { - documentUpdateInfo = null; - return false; - } - - // GetDiagnostics is expensive. Move it to the end if there were no bail outs from the algorithm. - // TODO likely adding more semantic checks will perform checks we expect from GetDiagnostics - // We may consider removing GetDiagnostics. - // https://github.com/dotnet/roslyn/issues/25639 - if ((TryConvertInternal(queryExpressionProcessingInfo, out documentUpdateInfo) || - TryReplaceWithLocalFunction(queryExpressionProcessingInfo, out documentUpdateInfo)) && // second attempt: at least to a local function - !_semanticModel.GetDiagnostics(_source.Span, _cancellationToken).Any(static diagnostic => diagnostic.DefaultSeverity == DiagnosticSeverity.Error)) - { - if (!documentUpdateInfo.Source.IsParentKind(SyntaxKind.Block) && - documentUpdateInfo.Destinations.Length > 1) - { - documentUpdateInfo = new DocumentUpdateInfo(documentUpdateInfo.Source, SyntaxFactory.Block(documentUpdateInfo.Destinations)); - } - - return true; - } - + // Do not try refactoring queries with comments or conditional compilation in them. + // We can consider supporting queries with comments in the future. + if (_source.DescendantTrivia().Any(trivia => trivia is (kind: + SyntaxKind.SingleLineCommentTrivia or + SyntaxKind.MultiLineCommentTrivia or + SyntaxKind.MultiLineDocumentationCommentTrivia) || + _source.ContainsDirectives)) + { documentUpdateInfo = null; return false; } - private StatementSyntax ProcessClause( - CSharpSyntaxNode node, - StatementSyntax statement, - bool isLastClause, - bool hasExtraDeclarations, - out StatementSyntax extraStatementToAddAbove) + // Bail out if there is no chance to convert it even with a local function. + if (!CanTryConvertToLocalFunction() || + !TryCreateStackFromQueryExpression(out var queryExpressionProcessingInfo)) { - extraStatementToAddAbove = null; - switch (node.Kind()) - { - case SyntaxKind.WhereClause: - return SyntaxFactory.Block(SyntaxFactory.IfStatement(((WhereClauseSyntax)node).Condition.WithAdditionalAnnotations(Simplifier.Annotation).WithoutTrivia(), statement)); - case SyntaxKind.FromClause: - var fromClause = (FromClauseSyntax)node; - - // If we are processing the first from and - // there were joins and some evaluations were moved into declarations above the foreach - // Check if the declaration on the first fromclause should be moved for the evaluation above declarations already moved upfront. - ExpressionSyntax expression1; - if (isLastClause && hasExtraDeclarations && !IsLocalOrParameterSymbol(_source.FromClause.Expression)) - { - var expressionName = _semanticFacts.GenerateNameForExpression( - _semanticModel, - fromClause.Expression, - capitalize: false, - _cancellationToken); - var variable = GetFreeSymbolNameAndMarkUsed(expressionName); - extraStatementToAddAbove = CreateLocalDeclarationStatement(variable, fromClause.Expression, generateTypeFromExpression: false); - expression1 = SyntaxFactory.IdentifierName(variable); - } - else - { - expression1 = fromClause.Expression.WithoutTrivia(); - } - - return SyntaxFactory.ForEachStatement( - fromClause.Type ?? VarNameIdentifier, - fromClause.Identifier, - expression1, - WrapWithBlock(statement)); - case SyntaxKind.LetClause: - var letClause = (LetClauseSyntax)node; - return AddToBlockTop(CreateLocalDeclarationStatement(letClause.Identifier, letClause.Expression, generateTypeFromExpression: false), statement); - case SyntaxKind.JoinClause: - var joinClause = (JoinClauseSyntax)node; - if (joinClause.Into != null) - { - // This must be caught on the validation step. Therefore, here is an exception. - throw new ArgumentException("GroupJoin is not supported"); - } - else - { - ExpressionSyntax expression2; - if (IsLocalOrParameterSymbol(joinClause.InExpression)) - { - expression2 = joinClause.InExpression; - } - else - { - // Input: var q = from x in XX() from z in ZZ() join y in YY() on x equals y select x + y; - // Add - // var xx = XX(); - // var yy = YY(); - // Do not add for ZZ() - var expressionName = _semanticFacts.GenerateNameForExpression( - _semanticModel, - joinClause.InExpression, - capitalize: false, - _cancellationToken); - var variable = GetFreeSymbolNameAndMarkUsed(expressionName); - extraStatementToAddAbove = CreateLocalDeclarationStatement(variable, joinClause.InExpression, generateTypeFromExpression: false); - - // Replace YY() with yy declared above. - expression2 = SyntaxFactory.IdentifierName(variable); - } - - // Output for the join - // var yy == YY(); this goes to extraStatementToAddAbove - // ... - // foreach (var y in yy) - // { - // if (object.Equals(x, y)) - // { - // ... - return SyntaxFactory.Block( - SyntaxFactory.ForEachStatement( - joinClause.Type ?? VarNameIdentifier, - joinClause.Identifier, - expression2, - SyntaxFactory.Block( - SyntaxFactory.IfStatement( - SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword)), - SyntaxFactory.IdentifierName(nameof(object.Equals))), - SyntaxFactory.ArgumentList([ - SyntaxFactory.Argument(joinClause.LeftExpression), - SyntaxFactory.Argument(joinClause.RightExpression.WithoutTrailingTrivia())])), - statement)))).WithAdditionalAnnotations(Simplifier.Annotation); - } - case SyntaxKind.SelectClause: - // This is not the latest Select in the Query Expression - // There is a QueryBody with the Continuation as a parent. - var selectClause = (SelectClauseSyntax)node; - var identifier = ((QueryBodySyntax)selectClause.Parent).Continuation.Identifier; - return AddToBlockTop(CreateLocalDeclarationStatement(identifier, selectClause.Expression, generateTypeFromExpression: true), statement); - default: - throw new ArgumentException($"Unexpected node kind {node.Kind()}"); - } + documentUpdateInfo = null; + return false; } - private bool TryConvertInternal(QueryExpressionProcessingInfo queryExpressionProcessingInfo, out DocumentUpdateInfo documentUpdateInfo) + // GetDiagnostics is expensive. Move it to the end if there were no bail outs from the algorithm. + // TODO likely adding more semantic checks will perform checks we expect from GetDiagnostics + // We may consider removing GetDiagnostics. + // https://github.com/dotnet/roslyn/issues/25639 + if ((TryConvertInternal(queryExpressionProcessingInfo, out documentUpdateInfo) || + TryReplaceWithLocalFunction(queryExpressionProcessingInfo, out documentUpdateInfo)) && // second attempt: at least to a local function + !_semanticModel.GetDiagnostics(_source.Span, _cancellationToken).Any(static diagnostic => diagnostic.DefaultSeverity == DiagnosticSeverity.Error)) { - // (from a in b select a); - var parent = _source.WalkUpParentheses().Parent; - - switch (parent.Kind()) + if (!documentUpdateInfo.Source.IsParentKind(SyntaxKind.Block) && + documentUpdateInfo.Destinations.Length > 1) { - // return from a in b select a; - case SyntaxKind.ReturnStatement: - return TryConvertIfInReturnStatement((ReturnStatementSyntax)parent, queryExpressionProcessingInfo, out documentUpdateInfo); - // foreach(var x in from a in b select a) - case SyntaxKind.ForEachStatement: - return TryConvertIfInForEach((ForEachStatementSyntax)parent, queryExpressionProcessingInfo, out documentUpdateInfo); - // (from a in b select a).ToList(), (from a in b select a).Count(), etc. - case SyntaxKind.SimpleMemberAccessExpression: - return TryConvertIfInMemberAccessExpression((MemberAccessExpressionSyntax)parent, queryExpressionProcessingInfo, out documentUpdateInfo); + documentUpdateInfo = new DocumentUpdateInfo(documentUpdateInfo.Source, SyntaxFactory.Block(documentUpdateInfo.Destinations)); } - documentUpdateInfo = null; - return false; + return true; } - /// - /// Checks if the location of the query expression allows to convert it at least to a local function. - /// It still does not guarantees that the conversion can be performed. There can be bail outs of later stages. - /// - /// - private bool CanTryConvertToLocalFunction() + documentUpdateInfo = null; + return false; + } + + private StatementSyntax ProcessClause( + CSharpSyntaxNode node, + StatementSyntax statement, + bool isLastClause, + bool hasExtraDeclarations, + out StatementSyntax extraStatementToAddAbove) + { + extraStatementToAddAbove = null; + switch (node.Kind()) { - SyntaxNode currentNode = _source; - while (currentNode != null) - { - if (currentNode is StatementSyntax) + case SyntaxKind.WhereClause: + return SyntaxFactory.Block(SyntaxFactory.IfStatement(((WhereClauseSyntax)node).Condition.WithAdditionalAnnotations(Simplifier.Annotation).WithoutTrivia(), statement)); + case SyntaxKind.FromClause: + var fromClause = (FromClauseSyntax)node; + + // If we are processing the first from and + // there were joins and some evaluations were moved into declarations above the foreach + // Check if the declaration on the first fromclause should be moved for the evaluation above declarations already moved upfront. + ExpressionSyntax expression1; + if (isLastClause && hasExtraDeclarations && !IsLocalOrParameterSymbol(_source.FromClause.Expression)) { - return true; + var expressionName = _semanticFacts.GenerateNameForExpression( + _semanticModel, + fromClause.Expression, + capitalize: false, + _cancellationToken); + var variable = GetFreeSymbolNameAndMarkUsed(expressionName); + extraStatementToAddAbove = CreateLocalDeclarationStatement(variable, fromClause.Expression, generateTypeFromExpression: false); + expression1 = SyntaxFactory.IdentifierName(variable); + } + else + { + expression1 = fromClause.Expression.WithoutTrivia(); } - if (currentNode is ExpressionSyntax or - ArgumentSyntax or - ArgumentListSyntax or - EqualsValueClauseSyntax or - VariableDeclaratorSyntax or - VariableDeclarationSyntax) + return SyntaxFactory.ForEachStatement( + fromClause.Type ?? VarNameIdentifier, + fromClause.Identifier, + expression1, + WrapWithBlock(statement)); + case SyntaxKind.LetClause: + var letClause = (LetClauseSyntax)node; + return AddToBlockTop(CreateLocalDeclarationStatement(letClause.Identifier, letClause.Expression, generateTypeFromExpression: false), statement); + case SyntaxKind.JoinClause: + var joinClause = (JoinClauseSyntax)node; + if (joinClause.Into != null) { - currentNode = currentNode.Parent; + // This must be caught on the validation step. Therefore, here is an exception. + throw new ArgumentException("GroupJoin is not supported"); } else { - return false; + ExpressionSyntax expression2; + if (IsLocalOrParameterSymbol(joinClause.InExpression)) + { + expression2 = joinClause.InExpression; + } + else + { + // Input: var q = from x in XX() from z in ZZ() join y in YY() on x equals y select x + y; + // Add + // var xx = XX(); + // var yy = YY(); + // Do not add for ZZ() + var expressionName = _semanticFacts.GenerateNameForExpression( + _semanticModel, + joinClause.InExpression, + capitalize: false, + _cancellationToken); + var variable = GetFreeSymbolNameAndMarkUsed(expressionName); + extraStatementToAddAbove = CreateLocalDeclarationStatement(variable, joinClause.InExpression, generateTypeFromExpression: false); + + // Replace YY() with yy declared above. + expression2 = SyntaxFactory.IdentifierName(variable); + } + + // Output for the join + // var yy == YY(); this goes to extraStatementToAddAbove + // ... + // foreach (var y in yy) + // { + // if (object.Equals(x, y)) + // { + // ... + return SyntaxFactory.Block( + SyntaxFactory.ForEachStatement( + joinClause.Type ?? VarNameIdentifier, + joinClause.Identifier, + expression2, + SyntaxFactory.Block( + SyntaxFactory.IfStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword)), + SyntaxFactory.IdentifierName(nameof(object.Equals))), + SyntaxFactory.ArgumentList([ + SyntaxFactory.Argument(joinClause.LeftExpression), + SyntaxFactory.Argument(joinClause.RightExpression.WithoutTrailingTrivia())])), + statement)))).WithAdditionalAnnotations(Simplifier.Annotation); } - } + case SyntaxKind.SelectClause: + // This is not the latest Select in the Query Expression + // There is a QueryBody with the Continuation as a parent. + var selectClause = (SelectClauseSyntax)node; + var identifier = ((QueryBodySyntax)selectClause.Parent).Continuation.Identifier; + return AddToBlockTop(CreateLocalDeclarationStatement(identifier, selectClause.Expression, generateTypeFromExpression: true), statement); + default: + throw new ArgumentException($"Unexpected node kind {node.Kind()}"); + } + } - return false; + private bool TryConvertInternal(QueryExpressionProcessingInfo queryExpressionProcessingInfo, out DocumentUpdateInfo documentUpdateInfo) + { + // (from a in b select a); + var parent = _source.WalkUpParentheses().Parent; + + switch (parent.Kind()) + { + // return from a in b select a; + case SyntaxKind.ReturnStatement: + return TryConvertIfInReturnStatement((ReturnStatementSyntax)parent, queryExpressionProcessingInfo, out documentUpdateInfo); + // foreach(var x in from a in b select a) + case SyntaxKind.ForEachStatement: + return TryConvertIfInForEach((ForEachStatementSyntax)parent, queryExpressionProcessingInfo, out documentUpdateInfo); + // (from a in b select a).ToList(), (from a in b select a).Count(), etc. + case SyntaxKind.SimpleMemberAccessExpression: + return TryConvertIfInMemberAccessExpression((MemberAccessExpressionSyntax)parent, queryExpressionProcessingInfo, out documentUpdateInfo); } - private bool TryConvertIfInMemberAccessExpression( - MemberAccessExpressionSyntax memberAccessExpression, - QueryExpressionProcessingInfo queryExpressionProcessingInfo, - out DocumentUpdateInfo documentUpdateInfo) + documentUpdateInfo = null; + return false; + } + + /// + /// Checks if the location of the query expression allows to convert it at least to a local function. + /// It still does not guarantees that the conversion can be performed. There can be bail outs of later stages. + /// + /// + private bool CanTryConvertToLocalFunction() + { + SyntaxNode currentNode = _source; + while (currentNode != null) { - if (memberAccessExpression.Parent is InvocationExpressionSyntax invocationExpression) + if (currentNode is StatementSyntax) { - // This also covers generic names (i.e. with type arguments) like 'ToList'. - // The ValueText is still just 'ToList'. - switch (memberAccessExpression.Name.Identifier.ValueText) - { - case nameof(Enumerable.ToList): - return TryConvertIfInToListInvocation(invocationExpression, queryExpressionProcessingInfo, out documentUpdateInfo); - case nameof(Enumerable.Count): - return TryConvertIfInCountInvocation(invocationExpression, queryExpressionProcessingInfo, out documentUpdateInfo); - } + return true; } - documentUpdateInfo = null; - return false; + if (currentNode is ExpressionSyntax or + ArgumentSyntax or + ArgumentListSyntax or + EqualsValueClauseSyntax or + VariableDeclaratorSyntax or + VariableDeclarationSyntax) + { + currentNode = currentNode.Parent; + } + else + { + return false; + } } - private bool TryConvertIfInCountInvocation( - InvocationExpressionSyntax invocationExpression, - QueryExpressionProcessingInfo queryExpressionProcessingInfo, - out DocumentUpdateInfo documentUpdateInfo) + return false; + } + + private bool TryConvertIfInMemberAccessExpression( + MemberAccessExpressionSyntax memberAccessExpression, + QueryExpressionProcessingInfo queryExpressionProcessingInfo, + out DocumentUpdateInfo documentUpdateInfo) + { + if (memberAccessExpression.Parent is InvocationExpressionSyntax invocationExpression) { - if (_semanticModel.GetSymbolInfo(invocationExpression, _cancellationToken).Symbol is IMethodSymbol methodSymbol && - methodSymbol.Parameters.Length == 0 && - methodSymbol.ReturnType?.SpecialType == SpecialType.System_Int32 && - methodSymbol.RefKind == RefKind.None) - { - // before var count = (from a in b select a).Count(); - // after - // var count = 0; - // foreach (var a in b) - // { - // count++; - // } - return TryConvertIfInInvocation( - invocationExpression, - queryExpressionProcessingInfo, - IsInt, - (variableIdentifier, expression) => SyntaxFactory.ExpressionStatement( - SyntaxFactory.PostfixUnaryExpression(SyntaxKind.PostIncrementExpression, variableIdentifier)), // Generating 'count++' - SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)), // count = 0 - variableName: "count", - out documentUpdateInfo); + // This also covers generic names (i.e. with type arguments) like 'ToList'. + // The ValueText is still just 'ToList'. + switch (memberAccessExpression.Name.Identifier.ValueText) + { + case nameof(Enumerable.ToList): + return TryConvertIfInToListInvocation(invocationExpression, queryExpressionProcessingInfo, out documentUpdateInfo); + case nameof(Enumerable.Count): + return TryConvertIfInCountInvocation(invocationExpression, queryExpressionProcessingInfo, out documentUpdateInfo); } - - documentUpdateInfo = null; - return false; } - private bool TryConvertIfInToListInvocation( - InvocationExpressionSyntax invocationExpression, - QueryExpressionProcessingInfo queryExpressionProcessingInfo, - out DocumentUpdateInfo documentUpdateInfo) + documentUpdateInfo = null; + return false; + } + + private bool TryConvertIfInCountInvocation( + InvocationExpressionSyntax invocationExpression, + QueryExpressionProcessingInfo queryExpressionProcessingInfo, + out DocumentUpdateInfo documentUpdateInfo) + { + if (_semanticModel.GetSymbolInfo(invocationExpression, _cancellationToken).Symbol is IMethodSymbol methodSymbol && + methodSymbol.Parameters.Length == 0 && + methodSymbol.ReturnType?.SpecialType == SpecialType.System_Int32 && + methodSymbol.RefKind == RefKind.None) { - // before var list = (from a in b select a).ToList(); + // before var count = (from a in b select a).Count(); // after - // var list = new List(); + // var count = 0; // foreach (var a in b) // { - // list.Add(a) - // } - if (_semanticModel.GetSymbolInfo(invocationExpression, _cancellationToken).Symbol is IMethodSymbol methodSymbol && - methodSymbol.RefKind == RefKind.None && - IsList(methodSymbol.ReturnType) && - methodSymbol.Parameters.Length == 0) - { - return TryConvertIfInInvocation( - invocationExpression, - queryExpressionProcessingInfo, - IsList, - (listIdentifier, expression) => SyntaxFactory.ExpressionStatement(SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - listIdentifier, - SyntaxFactory.IdentifierName(nameof(IList.Add))), - SyntaxFactory.ArgumentList([SyntaxFactory.Argument(expression)]))), - SyntaxFactory.ObjectCreationExpression( - methodSymbol.GenerateReturnTypeSyntax().WithAdditionalAnnotations(Simplifier.Annotation), - SyntaxFactory.ArgumentList(), - initializer: null), - variableName: "list", - out documentUpdateInfo); - } - - documentUpdateInfo = null; - return false; + // count++; + // } + return TryConvertIfInInvocation( + invocationExpression, + queryExpressionProcessingInfo, + IsInt, + (variableIdentifier, expression) => SyntaxFactory.ExpressionStatement( + SyntaxFactory.PostfixUnaryExpression(SyntaxKind.PostIncrementExpression, variableIdentifier)), // Generating 'count++' + SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)), // count = 0 + variableName: "count", + out documentUpdateInfo); } - private bool IsInt(ITypeSymbol typeSymbol) - => typeSymbol.SpecialType == SpecialType.System_Int32; - - private bool IsList(ITypeSymbol typeSymbol) - => Equals(typeSymbol.OriginalDefinition, _semanticModel.Compilation.GetTypeByMetadataName(typeof(List<>).FullName)); + documentUpdateInfo = null; + return false; + } - private bool TryConvertIfInInvocation( - InvocationExpressionSyntax invocationExpression, - QueryExpressionProcessingInfo queryExpressionProcessingInfo, - Func typeCheckMethod, - Func leafExpressionCreationMethod, - ExpressionSyntax initializer, - string variableName, - out DocumentUpdateInfo documentUpdateInfo) + private bool TryConvertIfInToListInvocation( + InvocationExpressionSyntax invocationExpression, + QueryExpressionProcessingInfo queryExpressionProcessingInfo, + out DocumentUpdateInfo documentUpdateInfo) + { + // before var list = (from a in b select a).ToList(); + // after + // var list = new List(); + // foreach (var a in b) + // { + // list.Add(a) + // } + if (_semanticModel.GetSymbolInfo(invocationExpression, _cancellationToken).Symbol is IMethodSymbol methodSymbol && + methodSymbol.RefKind == RefKind.None && + IsList(methodSymbol.ReturnType) && + methodSymbol.Parameters.Length == 0) { - var parentStatement = invocationExpression.GetAncestorOrThis(); - if (parentStatement != null) - { - if (TryConvertIfInInvocationInternal( - invocationExpression, - typeCheckMethod, - parentStatement, - initializer, - variableName, - out var variable, - out var nodesBefore, - out var nodesAfter)) - { - var statements = GenerateStatements(expression => leafExpressionCreationMethod(variable, expression), queryExpressionProcessingInfo); - var list = new List(); - list.AddRange(nodesBefore); - list.AddRange(statements); - list.AddRange(nodesAfter); - documentUpdateInfo = new DocumentUpdateInfo(parentStatement, list); - return true; - } - } - - documentUpdateInfo = null; - return false; + return TryConvertIfInInvocation( + invocationExpression, + queryExpressionProcessingInfo, + IsList, + (listIdentifier, expression) => SyntaxFactory.ExpressionStatement(SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + listIdentifier, + SyntaxFactory.IdentifierName(nameof(IList.Add))), + SyntaxFactory.ArgumentList([SyntaxFactory.Argument(expression)]))), + SyntaxFactory.ObjectCreationExpression( + methodSymbol.GenerateReturnTypeSyntax().WithAdditionalAnnotations(Simplifier.Annotation), + SyntaxFactory.ArgumentList(), + initializer: null), + variableName: "list", + out documentUpdateInfo); } - private bool TryConvertIfInInvocationInternal( - InvocationExpressionSyntax invocationExpression, - Func typeCheckMethod, - StatementSyntax parentStatement, - ExpressionSyntax initializer, - string variableName, - out ExpressionSyntax variable, - out StatementSyntax[] nodesBefore, - out StatementSyntax[] nodesAfter) - { - var invocationParent = invocationExpression.WalkUpParentheses().Parent; - var symbolName = GetFreeSymbolNameAndMarkUsed(variableName); - - void Convert( - ExpressionSyntax variableExpression, - ExpressionSyntax expressionToVerifyType, - bool checkForLocalOrParameter, - out ExpressionSyntax variableLocal, - out StatementSyntax[] nodesBeforeLocal, - out StatementSyntax[] nodesAfterLocal) - { - // Check that we can re-use the local variable or parameter - if (typeCheckMethod(_semanticModel.GetTypeInfo(expressionToVerifyType, _cancellationToken).Type) && - (!checkForLocalOrParameter || IsLocalOrParameterSymbol(variableExpression))) - { - // before - // a = (from a in b select a).ToList(); or var a = (from a in b select a).ToList() - // after - // a = new List(); or var a = new List(); - // foreach(...) - variableLocal = variableExpression; - nodesBeforeLocal = [parentStatement.ReplaceNode(invocationExpression, initializer.WithAdditionalAnnotations(Simplifier.Annotation))]; - nodesAfterLocal = []; - } - else - { - // before - // IReadOnlyList a = (from a in b select a).ToList(); or an assignment - // after - // var list = new List(); or assignment - // foreach(...) - // IReadOnlyList a = list; - variableLocal = SyntaxFactory.IdentifierName(symbolName); - nodesBeforeLocal = new[] { CreateLocalDeclarationStatement(symbolName, initializer, generateTypeFromExpression: false) }; - nodesAfterLocal = [parentStatement.ReplaceNode(invocationExpression, variableLocal.WithAdditionalAnnotations(Simplifier.Annotation))]; - } - } + documentUpdateInfo = null; + return false; + } - switch (invocationParent.Kind()) - { - case SyntaxKind.EqualsValueClause: - // Avoid for(int i = (from x in a select x).Count(); i < 10; i++) - if (invocationParent?.Parent.Kind() is - SyntaxKind.VariableDeclarator or - SyntaxKind.VariableDeclaration or - SyntaxKind.LocalDeclarationStatement && - // Avoid int i = (from x in a select x).Count(), j = i; - ((VariableDeclarationSyntax)invocationParent.Parent.Parent).Variables.Count == 1) - { - var variableDeclarator = ((VariableDeclaratorSyntax)invocationParent.Parent); - Convert( - SyntaxFactory.IdentifierName(variableDeclarator.Identifier), - ((VariableDeclarationSyntax)variableDeclarator.Parent).Type, - checkForLocalOrParameter: false, - out variable, - out nodesBefore, - out nodesAfter); - return true; - } + private bool IsInt(ITypeSymbol typeSymbol) + => typeSymbol.SpecialType == SpecialType.System_Int32; - break; - case SyntaxKind.SimpleAssignmentExpression: - var assignmentExpression = (AssignmentExpressionSyntax)invocationParent; - if (assignmentExpression.Right.WalkDownParentheses() == invocationExpression) - { - Convert( - assignmentExpression.Left, - assignmentExpression.Left, - checkForLocalOrParameter: true, - out variable, - out nodesBefore, - out nodesAfter); - return true; - } + private bool IsList(ITypeSymbol typeSymbol) + => Equals(typeSymbol.OriginalDefinition, _semanticModel.Compilation.GetTypeByMetadataName(typeof(List<>).FullName)); - break; - case SyntaxKind.ReturnStatement: - // before return (from a in b select a).ToList(); - // after var list = new List(); - // foreach(...) - // return list; - variable = SyntaxFactory.IdentifierName(symbolName); - nodesBefore = new[] { CreateLocalDeclarationStatement(symbolName, initializer, generateTypeFromExpression: false) }; - nodesAfter = new[] { SyntaxFactory.ReturnStatement(variable).WithAdditionalAnnotations(Simplifier.Annotation) }; - return true; - // SyntaxKind.Argument: - // SyntaxKind.ArrowExpressionClause is not supported + private bool TryConvertIfInInvocation( + InvocationExpressionSyntax invocationExpression, + QueryExpressionProcessingInfo queryExpressionProcessingInfo, + Func typeCheckMethod, + Func leafExpressionCreationMethod, + ExpressionSyntax initializer, + string variableName, + out DocumentUpdateInfo documentUpdateInfo) + { + var parentStatement = invocationExpression.GetAncestorOrThis(); + if (parentStatement != null) + { + if (TryConvertIfInInvocationInternal( + invocationExpression, + typeCheckMethod, + parentStatement, + initializer, + variableName, + out var variable, + out var nodesBefore, + out var nodesAfter)) + { + var statements = GenerateStatements(expression => leafExpressionCreationMethod(variable, expression), queryExpressionProcessingInfo); + var list = new List(); + list.AddRange(nodesBefore); + list.AddRange(statements); + list.AddRange(nodesAfter); + documentUpdateInfo = new DocumentUpdateInfo(parentStatement, list); + return true; } - - // Will still try to replace with a local function above. - nodesBefore = null; - nodesAfter = null; - variable = null; - return false; } - private LocalDeclarationStatementSyntax CreateLocalDeclarationStatement( - SyntaxToken identifier, - ExpressionSyntax expression, - bool generateTypeFromExpression) - { - var typeSyntax = generateTypeFromExpression - ? _semanticModel.GetTypeInfo(expression, _cancellationToken).ConvertedType.GenerateTypeSyntax() - : VarNameIdentifier; - return SyntaxFactory.LocalDeclarationStatement( - SyntaxFactory.VariableDeclaration( - typeSyntax, - [SyntaxFactory.VariableDeclarator( - identifier, - argumentList: null, - SyntaxFactory.EqualsValueClause(expression))])).WithAdditionalAnnotations(Simplifier.Annotation); - } + documentUpdateInfo = null; + return false; + } - private bool TryReplaceWithLocalFunction(QueryExpressionProcessingInfo queryExpressionProcessingInfo, out DocumentUpdateInfo documentUpdateInfo) + private bool TryConvertIfInInvocationInternal( + InvocationExpressionSyntax invocationExpression, + Func typeCheckMethod, + StatementSyntax parentStatement, + ExpressionSyntax initializer, + string variableName, + out ExpressionSyntax variable, + out StatementSyntax[] nodesBefore, + out StatementSyntax[] nodesAfter) + { + var invocationParent = invocationExpression.WalkUpParentheses().Parent; + var symbolName = GetFreeSymbolNameAndMarkUsed(variableName); + + void Convert( + ExpressionSyntax variableExpression, + ExpressionSyntax expressionToVerifyType, + bool checkForLocalOrParameter, + out ExpressionSyntax variableLocal, + out StatementSyntax[] nodesBeforeLocal, + out StatementSyntax[] nodesAfterLocal) { - var parentStatement = _source.GetAncestorOrThis(); - if (parentStatement == null) - { - documentUpdateInfo = null; - return false; - } - - // before statement ... from a in select b ... - // after - // IEnumerable localFunction() - // { - // foreach(var a in b) - // { - // yield return a; - // } - // } - // statement ... localFunction(); - var returnTypeInfo = _semanticModel.GetTypeInfo(_source, _cancellationToken); - ITypeSymbol returnedType; - - if (returnTypeInfo.Type.OriginalDefinition?.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T) - { - returnedType = returnTypeInfo.Type; + // Check that we can re-use the local variable or parameter + if (typeCheckMethod(_semanticModel.GetTypeInfo(expressionToVerifyType, _cancellationToken).Type) && + (!checkForLocalOrParameter || IsLocalOrParameterSymbol(variableExpression))) + { + // before + // a = (from a in b select a).ToList(); or var a = (from a in b select a).ToList() + // after + // a = new List(); or var a = new List(); + // foreach(...) + variableLocal = variableExpression; + nodesBeforeLocal = [parentStatement.ReplaceNode(invocationExpression, initializer.WithAdditionalAnnotations(Simplifier.Annotation))]; + nodesAfterLocal = []; } else { - if (returnTypeInfo.ConvertedType.OriginalDefinition?.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T) - { - returnedType = returnTypeInfo.ConvertedType; - } - else - { - documentUpdateInfo = null; - return false; - } + // before + // IReadOnlyList a = (from a in b select a).ToList(); or an assignment + // after + // var list = new List(); or assignment + // foreach(...) + // IReadOnlyList a = list; + variableLocal = SyntaxFactory.IdentifierName(symbolName); + nodesBeforeLocal = new[] { CreateLocalDeclarationStatement(symbolName, initializer, generateTypeFromExpression: false) }; + nodesAfterLocal = [parentStatement.ReplaceNode(invocationExpression, variableLocal.WithAdditionalAnnotations(Simplifier.Annotation))]; } - - static StatementSyntax internalNodeMethod(ExpressionSyntax expression) - => SyntaxFactory.YieldStatement(SyntaxKind.YieldReturnStatement, expression); - - var statements = GenerateStatements(internalNodeMethod, queryExpressionProcessingInfo); - var localFunctionNamePrefix = _semanticFacts.GenerateNameForExpression( - _semanticModel, - _source, - capitalize: false, - _cancellationToken); - var localFunctionToken = GetFreeSymbolNameAndMarkUsed(localFunctionNamePrefix); - var localFunctionDeclaration = SyntaxFactory.LocalFunctionStatement( - modifiers: default, - returnType: returnedType.GenerateTypeSyntax().WithAdditionalAnnotations(Simplifier.Annotation), - identifier: localFunctionToken, - typeParameterList: null, - parameterList: SyntaxFactory.ParameterList(), - constraintClauses: default, - body: SyntaxFactory.Block( - SyntaxFactory.Token( - [], - SyntaxKind.OpenBraceToken, - [SyntaxFactory.EndOfLine(Environment.NewLine)]), - [.. statements], - SyntaxFactory.Token(SyntaxKind.CloseBraceToken)), - expressionBody: null); - - var localFunctionInvocation = SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName(localFunctionToken)).WithAdditionalAnnotations(Simplifier.Annotation); - var newParentExpressionStatement = parentStatement.ReplaceNode(_source.WalkUpParentheses(), localFunctionInvocation.WithAdditionalAnnotations(Simplifier.Annotation)); - documentUpdateInfo = new DocumentUpdateInfo(parentStatement, [localFunctionDeclaration, newParentExpressionStatement]); - return true; } - private SyntaxToken GetFreeSymbolNameAndMarkUsed(string prefix) + switch (invocationParent.Kind()) { - var freeToken = _semanticFacts.GenerateUniqueName(_semanticModel, _source, container: null, baseName: prefix, _introducedLocalNames, _cancellationToken); - _introducedLocalNames.Add(freeToken.ValueText); - return freeToken; - } - - private bool TryConvertIfInForEach( - ForEachStatementSyntax forEachStatement, - QueryExpressionProcessingInfo queryExpressionProcessingInfo, - out DocumentUpdateInfo documentUpdateInfo) - { - // before foreach(var x in from a in b select a) - - if (forEachStatement.Expression.WalkDownParentheses() != _source) - { - documentUpdateInfo = null; - return false; - } - - // check that the body of the forEach does not contain any identifiers from the query - foreach (var identifierName in queryExpressionProcessingInfo.IdentifierNames) - { - // Identifier from the foreach can already be in scope of the foreach statement. - if (forEachStatement.Identifier.ValueText != identifierName) + case SyntaxKind.EqualsValueClause: + // Avoid for(int i = (from x in a select x).Count(); i < 10; i++) + if (invocationParent?.Parent.Kind() is + SyntaxKind.VariableDeclarator or + SyntaxKind.VariableDeclaration or + SyntaxKind.LocalDeclarationStatement && + // Avoid int i = (from x in a select x).Count(), j = i; + ((VariableDeclarationSyntax)invocationParent.Parent.Parent).Variables.Count == 1) { - if (_semanticFacts.GenerateUniqueName( - _semanticModel, - location: forEachStatement.Statement, - container: forEachStatement.Statement, - baseName: identifierName, - usedNames: [], - _cancellationToken).ValueText != identifierName) - { - documentUpdateInfo = null; - return false; - } + var variableDeclarator = ((VariableDeclaratorSyntax)invocationParent.Parent); + Convert( + SyntaxFactory.IdentifierName(variableDeclarator.Identifier), + ((VariableDeclarationSyntax)variableDeclarator.Parent).Type, + checkForLocalOrParameter: false, + out variable, + out nodesBefore, + out nodesAfter); + return true; } - } - // If query does not contains identifier with the same name as declared in the foreach, - // declare this identifier in the body. - if (!queryExpressionProcessingInfo.ContainsIdentifier(forEachStatement.Identifier)) - { - documentUpdateInfo = ConvertIfInToForeachWithExtraVariableDeclaration(forEachStatement, queryExpressionProcessingInfo); - return true; - } - else - { - // The last select expression in the query returns this identifier: - // foreach(var thisIdentifier in from ....... select thisIdentifier) - // if thisIdentifier in foreach is var, it is OK - // if a type is specified for thisIdentifier in forEach, check that the type is the same as in the select expression - // foreach(MyType thisIdentifier in from ....... from MyType thisIdentifier ... select thisIdentifier) - - // The last clause in query stack must be SelectClauseSyntax. - var lastSelectExpression = ((SelectClauseSyntax)queryExpressionProcessingInfo.Stack.Peek()).Expression; - if (lastSelectExpression is IdentifierNameSyntax identifierName && - forEachStatement.Identifier.ValueText == identifierName.Identifier.ValueText && - queryExpressionProcessingInfo.IdentifierNames.Contains(identifierName.Identifier.ValueText)) + break; + case SyntaxKind.SimpleAssignmentExpression: + var assignmentExpression = (AssignmentExpressionSyntax)invocationParent; + if (assignmentExpression.Right.WalkDownParentheses() == invocationExpression) { - var forEachStatementTypeSymbolType = _semanticModel.GetTypeInfo(forEachStatement.Type, _cancellationToken).Type; - var lastSelectExpressionTypeInfo = _semanticModel.GetTypeInfo(lastSelectExpression, _cancellationToken); - if (Equals(lastSelectExpressionTypeInfo.ConvertedType, lastSelectExpressionTypeInfo.Type) && - Equals(lastSelectExpressionTypeInfo.ConvertedType, forEachStatementTypeSymbolType)) - { - documentUpdateInfo = ConvertIfInToForeachWithoutExtraVariableDeclaration(forEachStatement, queryExpressionProcessingInfo); - return true; - } + Convert( + assignmentExpression.Left, + assignmentExpression.Left, + checkForLocalOrParameter: true, + out variable, + out nodesBefore, + out nodesAfter); + return true; } - } - // in all other cases try to replace with a local function - this is called above. - documentUpdateInfo = null; - return false; + break; + case SyntaxKind.ReturnStatement: + // before return (from a in b select a).ToList(); + // after var list = new List(); + // foreach(...) + // return list; + variable = SyntaxFactory.IdentifierName(symbolName); + nodesBefore = new[] { CreateLocalDeclarationStatement(symbolName, initializer, generateTypeFromExpression: false) }; + nodesAfter = new[] { SyntaxFactory.ReturnStatement(variable).WithAdditionalAnnotations(Simplifier.Annotation) }; + return true; + // SyntaxKind.Argument: + // SyntaxKind.ArrowExpressionClause is not supported } - private DocumentUpdateInfo ConvertIfInToForeachWithExtraVariableDeclaration( - ForEachStatementSyntax forEachStatement, - QueryExpressionProcessingInfo queryExpressionProcessingInfo) - { - // before foreach(var x in from ... a) { dosomething(x); } - // after - // foreach (var a in ...) - // ... - // { - // var x = a; - // dosomething(x); - // } - var statements = GenerateStatements( - expression => AddToBlockTop(SyntaxFactory.LocalDeclarationStatement( + // Will still try to replace with a local function above. + nodesBefore = null; + nodesAfter = null; + variable = null; + return false; + } + + private LocalDeclarationStatementSyntax CreateLocalDeclarationStatement( + SyntaxToken identifier, + ExpressionSyntax expression, + bool generateTypeFromExpression) + { + var typeSyntax = generateTypeFromExpression + ? _semanticModel.GetTypeInfo(expression, _cancellationToken).ConvertedType.GenerateTypeSyntax() + : VarNameIdentifier; + return SyntaxFactory.LocalDeclarationStatement( SyntaxFactory.VariableDeclaration( - forEachStatement.Type, + typeSyntax, [SyntaxFactory.VariableDeclarator( - forEachStatement.Identifier, + identifier, argumentList: null, - SyntaxFactory.EqualsValueClause(expression))])), - forEachStatement.Statement).WithAdditionalAnnotations(Formatter.Annotation), queryExpressionProcessingInfo); - return new DocumentUpdateInfo(forEachStatement, statements); - } + SyntaxFactory.EqualsValueClause(expression))])).WithAdditionalAnnotations(Simplifier.Annotation); + } - private DocumentUpdateInfo ConvertIfInToForeachWithoutExtraVariableDeclaration( - ForEachStatementSyntax forEachStatement, - QueryExpressionProcessingInfo queryExpressionProcessingInfo) + private bool TryReplaceWithLocalFunction(QueryExpressionProcessingInfo queryExpressionProcessingInfo, out DocumentUpdateInfo documentUpdateInfo) + { + var parentStatement = _source.GetAncestorOrThis(); + if (parentStatement == null) { - // before - // foreach (var a in from a in b where a > 5 select a) - // { - // dosomething(a); - // } - // after - // foreach (var a in b) - // { - // if (a > 5) - // { - // dosomething(a); - // } - // } - var statements = GenerateStatements( - expression => forEachStatement.Statement.WithAdditionalAnnotations(Formatter.Annotation), - queryExpressionProcessingInfo); - return new DocumentUpdateInfo(forEachStatement, statements); + documentUpdateInfo = null; + return false; } - private bool TryConvertIfInReturnStatement( - ReturnStatementSyntax returnStatement, - QueryExpressionProcessingInfo queryExpressionProcessingInfo, - out DocumentUpdateInfo documentUpdateInfo) + // before statement ... from a in select b ... + // after + // IEnumerable localFunction() + // { + // foreach(var a in b) + // { + // yield return a; + // } + // } + // statement ... localFunction(); + var returnTypeInfo = _semanticModel.GetTypeInfo(_source, _cancellationToken); + ITypeSymbol returnedType; + + if (returnTypeInfo.Type.OriginalDefinition?.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T) + { + returnedType = returnTypeInfo.Type; + } + else { - // The conversion requires yield return which cannot be added to lambdas and anonymous method declarations. - if (IsWithinImmediateLambdaOrAnonymousMethod(returnStatement)) + if (returnTypeInfo.ConvertedType.OriginalDefinition?.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T) { - documentUpdateInfo = null; - return false; + returnedType = returnTypeInfo.ConvertedType; } - - var memberDeclarationNode = FindParentMemberDeclarationNode(returnStatement, out var declaredSymbol); - if (declaredSymbol is not IMethodSymbol methodSymbol) + else { documentUpdateInfo = null; return false; } + } - if (methodSymbol.ReturnType.OriginalDefinition?.SpecialType != SpecialType.System_Collections_Generic_IEnumerable_T) - { - documentUpdateInfo = null; - return false; - } + static StatementSyntax internalNodeMethod(ExpressionSyntax expression) + => SyntaxFactory.YieldStatement(SyntaxKind.YieldReturnStatement, expression); + + var statements = GenerateStatements(internalNodeMethod, queryExpressionProcessingInfo); + var localFunctionNamePrefix = _semanticFacts.GenerateNameForExpression( + _semanticModel, + _source, + capitalize: false, + _cancellationToken); + var localFunctionToken = GetFreeSymbolNameAndMarkUsed(localFunctionNamePrefix); + var localFunctionDeclaration = SyntaxFactory.LocalFunctionStatement( + modifiers: default, + returnType: returnedType.GenerateTypeSyntax().WithAdditionalAnnotations(Simplifier.Annotation), + identifier: localFunctionToken, + typeParameterList: null, + parameterList: SyntaxFactory.ParameterList(), + constraintClauses: default, + body: SyntaxFactory.Block( + SyntaxFactory.Token( + [], + SyntaxKind.OpenBraceToken, + [SyntaxFactory.EndOfLine(Environment.NewLine)]), + [.. statements], + SyntaxFactory.Token(SyntaxKind.CloseBraceToken)), + expressionBody: null); + + var localFunctionInvocation = SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName(localFunctionToken)).WithAdditionalAnnotations(Simplifier.Annotation); + var newParentExpressionStatement = parentStatement.ReplaceNode(_source.WalkUpParentheses(), localFunctionInvocation.WithAdditionalAnnotations(Simplifier.Annotation)); + documentUpdateInfo = new DocumentUpdateInfo(parentStatement, [localFunctionDeclaration, newParentExpressionStatement]); + return true; + } - // if there are more than one return in the method, convert to local function. - if (memberDeclarationNode.DescendantNodes().OfType().Count() == 1) - { - // before: return from a in b select a; - // after: - // foreach(var a in b) - // { - // yield return a; - // } - // - // yield break; - var statements = GenerateStatements((ExpressionSyntax expression) - => SyntaxFactory.YieldStatement(SyntaxKind.YieldReturnStatement, expression), queryExpressionProcessingInfo); - - // add an yield break to avoid throws after the return. - var yieldBreakStatement = SyntaxFactory.YieldStatement(SyntaxKind.YieldBreakStatement); - documentUpdateInfo = new DocumentUpdateInfo(returnStatement, statements.Concat(new[] { yieldBreakStatement })); - return true; - } + private SyntaxToken GetFreeSymbolNameAndMarkUsed(string prefix) + { + var freeToken = _semanticFacts.GenerateUniqueName(_semanticModel, _source, container: null, baseName: prefix, _introducedLocalNames, _cancellationToken); + _introducedLocalNames.Add(freeToken.ValueText); + return freeToken; + } + + private bool TryConvertIfInForEach( + ForEachStatementSyntax forEachStatement, + QueryExpressionProcessingInfo queryExpressionProcessingInfo, + out DocumentUpdateInfo documentUpdateInfo) + { + // before foreach(var x in from a in b select a) + if (forEachStatement.Expression.WalkDownParentheses() != _source) + { documentUpdateInfo = null; return false; } - // We may assume that the query is defined within a method, field, property and so on and it is declare just once. - private SyntaxNode FindParentMemberDeclarationNode(SyntaxNode node, out ISymbol declaredSymbol) + // check that the body of the forEach does not contain any identifiers from the query + foreach (var identifierName in queryExpressionProcessingInfo.IdentifierNames) { - declaredSymbol = _semanticModel.GetEnclosingSymbol(node.SpanStart, _cancellationToken); - return declaredSymbol.DeclaringSyntaxReferences.Single().GetSyntax(); + // Identifier from the foreach can already be in scope of the foreach statement. + if (forEachStatement.Identifier.ValueText != identifierName) + { + if (_semanticFacts.GenerateUniqueName( + _semanticModel, + location: forEachStatement.Statement, + container: forEachStatement.Statement, + baseName: identifierName, + usedNames: [], + _cancellationToken).ValueText != identifierName) + { + documentUpdateInfo = null; + return false; + } + } } - private bool TryCreateStackFromQueryExpression(out QueryExpressionProcessingInfo queryExpressionProcessingInfo) + // If query does not contains identifier with the same name as declared in the foreach, + // declare this identifier in the body. + if (!queryExpressionProcessingInfo.ContainsIdentifier(forEachStatement.Identifier)) { - queryExpressionProcessingInfo = new QueryExpressionProcessingInfo(_source.FromClause); - return TryProcessQueryBody(_source.Body, queryExpressionProcessingInfo); + documentUpdateInfo = ConvertIfInToForeachWithExtraVariableDeclaration(forEachStatement, queryExpressionProcessingInfo); + return true; } - - private StatementSyntax[] GenerateStatements( - Func leafExpressionCreationMethod, - QueryExpressionProcessingInfo queryExpressionProcessingInfo) + else { - StatementSyntax statement = null; - var stack = queryExpressionProcessingInfo.Stack; - // Executes syntax building methods from bottom to the top of the tree. - // Process last clause - if (stack.Any()) - { - var node = stack.Pop(); - if (node is SelectClauseSyntax selectClause) - { - statement = WrapWithBlock(leafExpressionCreationMethod(selectClause.Expression)); - } - else + // The last select expression in the query returns this identifier: + // foreach(var thisIdentifier in from ....... select thisIdentifier) + // if thisIdentifier in foreach is var, it is OK + // if a type is specified for thisIdentifier in forEach, check that the type is the same as in the select expression + // foreach(MyType thisIdentifier in from ....... from MyType thisIdentifier ... select thisIdentifier) + + // The last clause in query stack must be SelectClauseSyntax. + var lastSelectExpression = ((SelectClauseSyntax)queryExpressionProcessingInfo.Stack.Peek()).Expression; + if (lastSelectExpression is IdentifierNameSyntax identifierName && + forEachStatement.Identifier.ValueText == identifierName.Identifier.ValueText && + queryExpressionProcessingInfo.IdentifierNames.Contains(identifierName.Identifier.ValueText)) + { + var forEachStatementTypeSymbolType = _semanticModel.GetTypeInfo(forEachStatement.Type, _cancellationToken).Type; + var lastSelectExpressionTypeInfo = _semanticModel.GetTypeInfo(lastSelectExpression, _cancellationToken); + if (Equals(lastSelectExpressionTypeInfo.ConvertedType, lastSelectExpressionTypeInfo.Type) && + Equals(lastSelectExpressionTypeInfo.ConvertedType, forEachStatementTypeSymbolType)) { - throw new ArgumentException("Last node must me the select clause"); - } - } - - // Process all other clauses - var statements = new List(); - while (stack.Any()) - { - statement = ProcessClause( - stack.Pop(), - statement, - isLastClause: !stack.Any(), - hasExtraDeclarations: statements.Any(), - out var extraStatement); - if (extraStatement != null) - { - statements.Add(extraStatement); + documentUpdateInfo = ConvertIfInToForeachWithoutExtraVariableDeclaration(forEachStatement, queryExpressionProcessingInfo); + return true; } } - - // The stack was processed in the reverse order, but the extra statements should be provided in the direct order. - statements.Reverse(); - statements.Add(statement.WithAdditionalAnnotations(Simplifier.Annotation)); - return statements.ToArray(); } - private bool TryProcessQueryBody(QueryBodySyntax queryBody, QueryExpressionProcessingInfo queryExpressionProcessingInfo) - { - do - { - foreach (var queryClause in queryBody.Clauses) - { - switch (queryClause.Kind()) - { - case SyntaxKind.WhereClause: - queryExpressionProcessingInfo.Add(queryClause); - break; - case SyntaxKind.LetClause: - if (!queryExpressionProcessingInfo.TryAdd(queryClause, ((LetClauseSyntax)queryClause).Identifier)) - { - return false; - } - - break; - case SyntaxKind.FromClause: - var fromClause = (FromClauseSyntax)queryClause; - if (!queryExpressionProcessingInfo.TryAdd(queryClause, fromClause.Identifier)) - { - return false; - } + // in all other cases try to replace with a local function - this is called above. + documentUpdateInfo = null; + return false; + } - break; - case SyntaxKind.JoinClause: - var joinClause = (JoinClauseSyntax)queryClause; - if (joinClause.Into == null) // GroupJoin is not supported - { - if (queryExpressionProcessingInfo.TryAdd(joinClause, joinClause.Identifier)) - { - break; - } - } + private DocumentUpdateInfo ConvertIfInToForeachWithExtraVariableDeclaration( + ForEachStatementSyntax forEachStatement, + QueryExpressionProcessingInfo queryExpressionProcessingInfo) + { + // before foreach(var x in from ... a) { dosomething(x); } + // after + // foreach (var a in ...) + // ... + // { + // var x = a; + // dosomething(x); + // } + var statements = GenerateStatements( + expression => AddToBlockTop(SyntaxFactory.LocalDeclarationStatement( + SyntaxFactory.VariableDeclaration( + forEachStatement.Type, + [SyntaxFactory.VariableDeclarator( + forEachStatement.Identifier, + argumentList: null, + SyntaxFactory.EqualsValueClause(expression))])), + forEachStatement.Statement).WithAdditionalAnnotations(Formatter.Annotation), queryExpressionProcessingInfo); + return new DocumentUpdateInfo(forEachStatement, statements); + } - return false; - // OrderBy is not supported by foreach. - default: - return false; - } - } + private DocumentUpdateInfo ConvertIfInToForeachWithoutExtraVariableDeclaration( + ForEachStatementSyntax forEachStatement, + QueryExpressionProcessingInfo queryExpressionProcessingInfo) + { + // before + // foreach (var a in from a in b where a > 5 select a) + // { + // dosomething(a); + // } + // after + // foreach (var a in b) + // { + // if (a > 5) + // { + // dosomething(a); + // } + // } + var statements = GenerateStatements( + expression => forEachStatement.Statement.WithAdditionalAnnotations(Formatter.Annotation), + queryExpressionProcessingInfo); + return new DocumentUpdateInfo(forEachStatement, statements); + } - // GroupClause is not supported by the conversion - if (queryBody.SelectOrGroup is not SelectClauseSyntax selectClause) - { - return false; - } + private bool TryConvertIfInReturnStatement( + ReturnStatementSyntax returnStatement, + QueryExpressionProcessingInfo queryExpressionProcessingInfo, + out DocumentUpdateInfo documentUpdateInfo) + { + // The conversion requires yield return which cannot be added to lambdas and anonymous method declarations. + if (IsWithinImmediateLambdaOrAnonymousMethod(returnStatement)) + { + documentUpdateInfo = null; + return false; + } - if (_semanticModel.GetTypeInfo(selectClause.Expression, _cancellationToken).Type.ContainsAnonymousType()) - { - return false; - } + var memberDeclarationNode = FindParentMemberDeclarationNode(returnStatement, out var declaredSymbol); + if (declaredSymbol is not IMethodSymbol methodSymbol) + { + documentUpdateInfo = null; + return false; + } - queryExpressionProcessingInfo.Add(selectClause); - queryBody = queryBody.Continuation?.Body; - } while (queryBody != null); + if (methodSymbol.ReturnType.OriginalDefinition?.SpecialType != SpecialType.System_Collections_Generic_IEnumerable_T) + { + documentUpdateInfo = null; + return false; + } + // if there are more than one return in the method, convert to local function. + if (memberDeclarationNode.DescendantNodes().OfType().Count() == 1) + { + // before: return from a in b select a; + // after: + // foreach(var a in b) + // { + // yield return a; + // } + // + // yield break; + var statements = GenerateStatements((ExpressionSyntax expression) + => SyntaxFactory.YieldStatement(SyntaxKind.YieldReturnStatement, expression), queryExpressionProcessingInfo); + + // add an yield break to avoid throws after the return. + var yieldBreakStatement = SyntaxFactory.YieldStatement(SyntaxKind.YieldBreakStatement); + documentUpdateInfo = new DocumentUpdateInfo(returnStatement, statements.Concat(new[] { yieldBreakStatement })); return true; } - private static BlockSyntax AddToBlockTop(StatementSyntax newStatement, StatementSyntax statement) + documentUpdateInfo = null; + return false; + } + + // We may assume that the query is defined within a method, field, property and so on and it is declare just once. + private SyntaxNode FindParentMemberDeclarationNode(SyntaxNode node, out ISymbol declaredSymbol) + { + declaredSymbol = _semanticModel.GetEnclosingSymbol(node.SpanStart, _cancellationToken); + return declaredSymbol.DeclaringSyntaxReferences.Single().GetSyntax(); + } + + private bool TryCreateStackFromQueryExpression(out QueryExpressionProcessingInfo queryExpressionProcessingInfo) + { + queryExpressionProcessingInfo = new QueryExpressionProcessingInfo(_source.FromClause); + return TryProcessQueryBody(_source.Body, queryExpressionProcessingInfo); + } + + private StatementSyntax[] GenerateStatements( + Func leafExpressionCreationMethod, + QueryExpressionProcessingInfo queryExpressionProcessingInfo) + { + StatementSyntax statement = null; + var stack = queryExpressionProcessingInfo.Stack; + // Executes syntax building methods from bottom to the top of the tree. + // Process last clause + if (stack.Any()) { - if (statement is BlockSyntax block) + var node = stack.Pop(); + if (node is SelectClauseSyntax selectClause) { - return block.WithStatements(block.Statements.Insert(0, newStatement)); + statement = WrapWithBlock(leafExpressionCreationMethod(selectClause.Expression)); } else { - return SyntaxFactory.Block(newStatement, statement); + throw new ArgumentException("Last node must me the select clause"); } } - private bool IsLocalOrParameterSymbol(ExpressionSyntax expression) - => IsLocalOrParameterSymbol(_semanticModel.GetOperation(expression, _cancellationToken)); - - private static bool IsLocalOrParameterSymbol(IOperation operation) + // Process all other clauses + var statements = new List(); + while (stack.Any()) { - if (operation is IConversionOperation conversion && conversion.IsImplicit) - { - return IsLocalOrParameterSymbol(conversion.Operand); + statement = ProcessClause( + stack.Pop(), + statement, + isLastClause: !stack.Any(), + hasExtraDeclarations: statements.Any(), + out var extraStatement); + if (extraStatement != null) + { + statements.Add(extraStatement); } - - return operation.Kind is OperationKind.LocalReference or OperationKind.ParameterReference; } - private static BlockSyntax WrapWithBlock(StatementSyntax statement) - => statement is BlockSyntax block ? block : SyntaxFactory.Block(statement); + // The stack was processed in the reverse order, but the extra statements should be provided in the direct order. + statements.Reverse(); + statements.Add(statement.WithAdditionalAnnotations(Simplifier.Annotation)); + return statements.ToArray(); + } - // Checks if the node is within an immediate lambda or within an immediate anonymous method. - // 'lambda => node' returns true - // 'lambda => localfunction => node' returns false - // 'member => node' returns false - private static bool IsWithinImmediateLambdaOrAnonymousMethod(SyntaxNode node) + private bool TryProcessQueryBody(QueryBodySyntax queryBody, QueryExpressionProcessingInfo queryExpressionProcessingInfo) + { + do { - while (node != null) + foreach (var queryClause in queryBody.Clauses) { - switch (node.Kind()) + switch (queryClause.Kind()) { - case SyntaxKind.AnonymousMethodExpression: - case SyntaxKind.ParenthesizedLambdaExpression: - case SyntaxKind.SimpleLambdaExpression: - return true; - case SyntaxKind.LocalFunctionStatement: - return false; - default: - if (node is MemberDeclarationSyntax) + case SyntaxKind.WhereClause: + queryExpressionProcessingInfo.Add(queryClause); + break; + case SyntaxKind.LetClause: + if (!queryExpressionProcessingInfo.TryAdd(queryClause, ((LetClauseSyntax)queryClause).Identifier)) + { + return false; + } + + break; + case SyntaxKind.FromClause: + var fromClause = (FromClauseSyntax)queryClause; + if (!queryExpressionProcessingInfo.TryAdd(queryClause, fromClause.Identifier)) { return false; } break; + case SyntaxKind.JoinClause: + var joinClause = (JoinClauseSyntax)queryClause; + if (joinClause.Into == null) // GroupJoin is not supported + { + if (queryExpressionProcessingInfo.TryAdd(joinClause, joinClause.Identifier)) + { + break; + } + } + + return false; + // OrderBy is not supported by foreach. + default: + return false; } + } - node = node.Parent; + // GroupClause is not supported by the conversion + if (queryBody.SelectOrGroup is not SelectClauseSyntax selectClause) + { + return false; } - return false; + if (_semanticModel.GetTypeInfo(selectClause.Expression, _cancellationToken).Type.ContainsAnonymousType()) + { + return false; + } + + queryExpressionProcessingInfo.Add(selectClause); + queryBody = queryBody.Continuation?.Body; + } while (queryBody != null); + + return true; + } + + private static BlockSyntax AddToBlockTop(StatementSyntax newStatement, StatementSyntax statement) + { + if (statement is BlockSyntax block) + { + return block.WithStatements(block.Statements.Insert(0, newStatement)); } + else + { + return SyntaxFactory.Block(newStatement, statement); + } + } - private class QueryExpressionProcessingInfo + private bool IsLocalOrParameterSymbol(ExpressionSyntax expression) + => IsLocalOrParameterSymbol(_semanticModel.GetOperation(expression, _cancellationToken)); + + private static bool IsLocalOrParameterSymbol(IOperation operation) + { + if (operation is IConversionOperation conversion && conversion.IsImplicit) { - public Stack Stack { get; private set; } + return IsLocalOrParameterSymbol(conversion.Operand); + } - public HashSet IdentifierNames { get; private set; } + return operation.Kind is OperationKind.LocalReference or OperationKind.ParameterReference; + } - public QueryExpressionProcessingInfo(FromClauseSyntax fromClause) - { - Stack = new Stack(); - Stack.Push(fromClause); - IdentifierNames = [fromClause.Identifier.ValueText]; - } + private static BlockSyntax WrapWithBlock(StatementSyntax statement) + => statement is BlockSyntax block ? block : SyntaxFactory.Block(statement); - public bool TryAdd(CSharpSyntaxNode node, SyntaxToken identifier) + // Checks if the node is within an immediate lambda or within an immediate anonymous method. + // 'lambda => node' returns true + // 'lambda => localfunction => node' returns false + // 'member => node' returns false + private static bool IsWithinImmediateLambdaOrAnonymousMethod(SyntaxNode node) + { + while (node != null) + { + switch (node.Kind()) { - // Duplicate identifiers are not allowed. - // var q = from x in new[] { 1 } select x + 2 into x where x > 0 select 7 into y let x = ""aaa"" select x; - if (!IdentifierNames.Add(identifier.ValueText)) - { + case SyntaxKind.AnonymousMethodExpression: + case SyntaxKind.ParenthesizedLambdaExpression: + case SyntaxKind.SimpleLambdaExpression: + return true; + case SyntaxKind.LocalFunctionStatement: return false; - } + default: + if (node is MemberDeclarationSyntax) + { + return false; + } - Stack.Push(node); - return true; + break; } - public void Add(CSharpSyntaxNode node) => Stack.Push(node); + node = node.Parent; + } + + return false; + } + + private class QueryExpressionProcessingInfo + { + public Stack Stack { get; private set; } + + public HashSet IdentifierNames { get; private set; } - public bool ContainsIdentifier(SyntaxToken identifier) - => IdentifierNames.Contains(identifier.ValueText); + public QueryExpressionProcessingInfo(FromClauseSyntax fromClause) + { + Stack = new Stack(); + Stack.Push(fromClause); + IdentifierNames = [fromClause.Identifier.ValueText]; } + + public bool TryAdd(CSharpSyntaxNode node, SyntaxToken identifier) + { + // Duplicate identifiers are not allowed. + // var q = from x in new[] { 1 } select x + 2 into x where x > 0 select 7 into y let x = ""aaa"" select x; + if (!IdentifierNames.Add(identifier.ValueText)) + { + return false; + } + + Stack.Push(node); + return true; + } + + public void Add(CSharpSyntaxNode node) => Stack.Push(node); + + public bool ContainsIdentifier(SyntaxToken identifier) + => IdentifierNames.Contains(identifier.ValueText); } } } diff --git a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractConverter.cs b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractConverter.cs index 2e0388d2455e8..2602bf63af3ef 100644 --- a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractConverter.cs +++ b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractConverter.cs @@ -16,278 +16,277 @@ using Roslyn.Utilities; using SyntaxNodeOrTokenExtensions = Microsoft.CodeAnalysis.Shared.Extensions.SyntaxNodeOrTokenExtensions; -namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery +namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery; + +internal abstract class AbstractConverter(ForEachInfo forEachInfo) : IConverter { - internal abstract class AbstractConverter(ForEachInfo forEachInfo) : IConverter + public ForEachInfo ForEachInfo { get; } = forEachInfo; + + public abstract void Convert(SyntaxEditor editor, bool convertToQuery, CancellationToken cancellationToken); + + /// + /// Creates a query expression or a linq invocation expression. + /// + /// expression to be used into the last Select in the query expression or linq invocation. + /// extra leading tokens to be added to the select clause + /// extra trailing tokens to be added to the select clause + /// Flag indicating if a query expression should be generated + /// + protected ExpressionSyntax CreateQueryExpressionOrLinqInvocation( + ExpressionSyntax selectExpression, + IEnumerable leadingTokensForSelect, + IEnumerable trailingTokensForSelect, + bool convertToQuery) { - public ForEachInfo ForEachInfo { get; } = forEachInfo; + return convertToQuery + ? CreateQueryExpression(selectExpression, leadingTokensForSelect, trailingTokensForSelect) + : CreateLinqInvocationOrSimpleExpression(selectExpression, leadingTokensForSelect, trailingTokensForSelect); + } - public abstract void Convert(SyntaxEditor editor, bool convertToQuery, CancellationToken cancellationToken); + /// + /// Creates a query expression. + /// + /// expression to be used into the last 'select ...' in the query expression + /// extra leading tokens to be added to the select clause + /// extra trailing tokens to be added to the select clause + /// + private QueryExpressionSyntax CreateQueryExpression( + ExpressionSyntax selectExpression, + IEnumerable leadingTokensForSelect, + IEnumerable trailingTokensForSelect) + => SyntaxFactory.QueryExpression( + CreateFromClause(ForEachInfo.ForEachStatement, ForEachInfo.LeadingTokens.GetTrivia(), []), + SyntaxFactory.QueryBody( + [.. ForEachInfo.ConvertingExtendedNodes.Select(node => CreateQueryClause(node))], + SyntaxFactory.SelectClause(selectExpression) + .WithCommentsFrom(leadingTokensForSelect, ForEachInfo.TrailingTokens.Concat(trailingTokensForSelect)), + continuation: null)) // The current coverage of foreach statements to support does not need to use query continuations. + .WithAdditionalAnnotations(Formatter.Annotation); - /// - /// Creates a query expression or a linq invocation expression. - /// - /// expression to be used into the last Select in the query expression or linq invocation. - /// extra leading tokens to be added to the select clause - /// extra trailing tokens to be added to the select clause - /// Flag indicating if a query expression should be generated - /// - protected ExpressionSyntax CreateQueryExpressionOrLinqInvocation( - ExpressionSyntax selectExpression, - IEnumerable leadingTokensForSelect, - IEnumerable trailingTokensForSelect, - bool convertToQuery) + private static QueryClauseSyntax CreateQueryClause(ExtendedSyntaxNode node) + { + switch (node.Node.Kind()) { - return convertToQuery - ? CreateQueryExpression(selectExpression, leadingTokensForSelect, trailingTokensForSelect) - : CreateLinqInvocationOrSimpleExpression(selectExpression, leadingTokensForSelect, trailingTokensForSelect); + case SyntaxKind.VariableDeclarator: + var variable = (VariableDeclaratorSyntax)node.Node; + return SyntaxFactory.LetClause( + SyntaxFactory.Token(SyntaxKind.LetKeyword), + variable.Identifier, + variable.Initializer.EqualsToken, + variable.Initializer.Value) + .WithCommentsFrom(node.ExtraLeadingComments, node.ExtraTrailingComments); + + case SyntaxKind.ForEachStatement: + return CreateFromClause((ForEachStatementSyntax)node.Node, node.ExtraLeadingComments, node.ExtraTrailingComments); + + case SyntaxKind.IfStatement: + var ifStatement = (IfStatementSyntax)node.Node; + return SyntaxFactory.WhereClause( + SyntaxFactory.Token(SyntaxKind.WhereKeyword) + .WithCommentsFrom(ifStatement.IfKeyword.LeadingTrivia, ifStatement.IfKeyword.TrailingTrivia), + ifStatement.Condition.WithCommentsFrom(ifStatement.OpenParenToken, ifStatement.CloseParenToken)) + .WithCommentsFrom(node.ExtraLeadingComments, node.ExtraTrailingComments); } - /// - /// Creates a query expression. - /// - /// expression to be used into the last 'select ...' in the query expression - /// extra leading tokens to be added to the select clause - /// extra trailing tokens to be added to the select clause - /// - private QueryExpressionSyntax CreateQueryExpression( - ExpressionSyntax selectExpression, - IEnumerable leadingTokensForSelect, - IEnumerable trailingTokensForSelect) - => SyntaxFactory.QueryExpression( - CreateFromClause(ForEachInfo.ForEachStatement, ForEachInfo.LeadingTokens.GetTrivia(), []), - SyntaxFactory.QueryBody( - [.. ForEachInfo.ConvertingExtendedNodes.Select(node => CreateQueryClause(node))], - SyntaxFactory.SelectClause(selectExpression) - .WithCommentsFrom(leadingTokensForSelect, ForEachInfo.TrailingTokens.Concat(trailingTokensForSelect)), - continuation: null)) // The current coverage of foreach statements to support does not need to use query continuations. + throw ExceptionUtilities.Unreachable(); + } + + private static FromClauseSyntax CreateFromClause( + ForEachStatementSyntax forEachStatement, + IEnumerable extraLeadingTrivia, + IEnumerable extraTrailingTrivia) + => SyntaxFactory.FromClause( + fromKeyword: SyntaxFactory.Token(SyntaxKind.FromKeyword) + .WithCommentsFrom( + forEachStatement.ForEachKeyword.LeadingTrivia, + forEachStatement.ForEachKeyword.TrailingTrivia, + forEachStatement.OpenParenToken) + .KeepCommentsAndAddElasticMarkers(), + type: forEachStatement.Type.IsVar ? null : forEachStatement.Type, + identifier: forEachStatement.Type.IsVar + ? forEachStatement.Identifier.WithPrependedLeadingTrivia( + SyntaxNodeOrTokenExtensions.GetTrivia(forEachStatement.Type.GetFirstToken()) + .FilterComments(addElasticMarker: false)) + : forEachStatement.Identifier, + inKeyword: forEachStatement.InKeyword.KeepCommentsAndAddElasticMarkers(), + expression: forEachStatement.Expression) + .WithCommentsFrom(extraLeadingTrivia, extraTrailingTrivia, forEachStatement.CloseParenToken); + + /// + /// Creates a linq invocation expression. + /// + /// expression to be used in the last 'Select' invocation + /// extra leading tokens to be added to the select clause + /// extra trailing tokens to be added to the select clause + /// + private ExpressionSyntax CreateLinqInvocationOrSimpleExpression( + ExpressionSyntax selectExpression, + IEnumerable leadingTokensForSelect, + IEnumerable trailingTokensForSelect) + { + var foreachStatement = ForEachInfo.ForEachStatement; + selectExpression = selectExpression.WithCommentsFrom(leadingTokensForSelect, ForEachInfo.TrailingTokens.Concat(trailingTokensForSelect)); + var currentExtendedNodeIndex = 0; + + return CreateLinqInvocationOrSimpleExpression( + foreachStatement, + receiverForInvocation: foreachStatement.Expression, + selectExpression: selectExpression, + leadingCommentsTrivia: ForEachInfo.LeadingTokens.GetTrivia(), + trailingCommentsTrivia: [], + currentExtendedNodeIndex: ref currentExtendedNodeIndex) .WithAdditionalAnnotations(Formatter.Annotation); + } - private static QueryClauseSyntax CreateQueryClause(ExtendedSyntaxNode node) - { - switch (node.Node.Kind()) - { - case SyntaxKind.VariableDeclarator: - var variable = (VariableDeclaratorSyntax)node.Node; - return SyntaxFactory.LetClause( - SyntaxFactory.Token(SyntaxKind.LetKeyword), - variable.Identifier, - variable.Initializer.EqualsToken, - variable.Initializer.Value) - .WithCommentsFrom(node.ExtraLeadingComments, node.ExtraTrailingComments); + private ExpressionSyntax CreateLinqInvocationOrSimpleExpression( + ForEachStatementSyntax forEachStatement, + ExpressionSyntax receiverForInvocation, + IEnumerable leadingCommentsTrivia, + IEnumerable trailingCommentsTrivia, + ExpressionSyntax selectExpression, + ref int currentExtendedNodeIndex) + { + leadingCommentsTrivia = forEachStatement.ForEachKeyword.GetAllTrivia().Concat(leadingCommentsTrivia); - case SyntaxKind.ForEachStatement: - return CreateFromClause((ForEachStatementSyntax)node.Node, node.ExtraLeadingComments, node.ExtraTrailingComments); + // Recursively create linq invocations, possibly updating the receiver (Where invocations), to get the inner expression for + // the lambda body for the linq invocation to be created for this foreach statement. For example: + // + // INPUT: + // foreach (var n1 in c1) + // foreach (var n2 in c2) + // if (n1 > n2) + // yield return n1 + n2; + // + // OUTPUT: + // c1.SelectMany(n1 => c2.Where(n2 => n1 > n2).Select(n2 => n1 + n2)) + // + var hasForEachChild = false; + var lambdaBody = CreateLinqInvocationForExtendedNode(selectExpression, ref currentExtendedNodeIndex, ref receiverForInvocation, ref hasForEachChild); + var lambda = SyntaxFactory.SimpleLambdaExpression( + SyntaxFactory.Parameter( + forEachStatement.Identifier.WithPrependedLeadingTrivia( + SyntaxNodeOrTokenExtensions.GetTrivia(forEachStatement.Type.GetFirstToken()) + .FilterComments(addElasticMarker: false))), + lambdaBody) + .WithCommentsFrom(leadingCommentsTrivia, trailingCommentsTrivia, + forEachStatement.OpenParenToken, forEachStatement.InKeyword, forEachStatement.CloseParenToken); - case SyntaxKind.IfStatement: - var ifStatement = (IfStatementSyntax)node.Node; - return SyntaxFactory.WhereClause( - SyntaxFactory.Token(SyntaxKind.WhereKeyword) - .WithCommentsFrom(ifStatement.IfKeyword.LeadingTrivia, ifStatement.IfKeyword.TrailingTrivia), - ifStatement.Condition.WithCommentsFrom(ifStatement.OpenParenToken, ifStatement.CloseParenToken)) - .WithCommentsFrom(node.ExtraLeadingComments, node.ExtraTrailingComments); + // Create Select or SelectMany linq invocation for this foreach statement. For example: + // + // INPUT: + // foreach (var n1 in c1) + // ... + // + // OUTPUT: + // c1.Select(n1 => ... + // OR + // c1.SelectMany(n1 => ... + // + + var invokedMethodName = !hasForEachChild ? nameof(Enumerable.Select) : nameof(Enumerable.SelectMany); + + // Avoid `.Select(x => x)` + if (invokedMethodName == nameof(Enumerable.Select) && + lambdaBody is IdentifierNameSyntax identifier && + identifier.Identifier.ValueText == forEachStatement.Identifier.ValueText) + { + // Because we're dropping the lambda, any comments associated with it need to be preserved. + + var droppedTrivia = new List(); + foreach (var token in lambda.DescendantTokens()) + { + droppedTrivia.AddRange(token.GetAllTrivia().Where(t => !t.IsWhitespace())); } - throw ExceptionUtilities.Unreachable(); + return receiverForInvocation.WithAppendedTrailingTrivia(droppedTrivia); } - private static FromClauseSyntax CreateFromClause( - ForEachStatementSyntax forEachStatement, - IEnumerable extraLeadingTrivia, - IEnumerable extraTrailingTrivia) - => SyntaxFactory.FromClause( - fromKeyword: SyntaxFactory.Token(SyntaxKind.FromKeyword) - .WithCommentsFrom( - forEachStatement.ForEachKeyword.LeadingTrivia, - forEachStatement.ForEachKeyword.TrailingTrivia, - forEachStatement.OpenParenToken) - .KeepCommentsAndAddElasticMarkers(), - type: forEachStatement.Type.IsVar ? null : forEachStatement.Type, - identifier: forEachStatement.Type.IsVar - ? forEachStatement.Identifier.WithPrependedLeadingTrivia( - SyntaxNodeOrTokenExtensions.GetTrivia(forEachStatement.Type.GetFirstToken()) - .FilterComments(addElasticMarker: false)) - : forEachStatement.Identifier, - inKeyword: forEachStatement.InKeyword.KeepCommentsAndAddElasticMarkers(), - expression: forEachStatement.Expression) - .WithCommentsFrom(extraLeadingTrivia, extraTrailingTrivia, forEachStatement.CloseParenToken); + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + receiverForInvocation.Parenthesize(), + SyntaxFactory.IdentifierName(invokedMethodName)), + SyntaxFactory.ArgumentList([SyntaxFactory.Argument(lambda)])); + } - /// - /// Creates a linq invocation expression. - /// - /// expression to be used in the last 'Select' invocation - /// extra leading tokens to be added to the select clause - /// extra trailing tokens to be added to the select clause - /// - private ExpressionSyntax CreateLinqInvocationOrSimpleExpression( - ExpressionSyntax selectExpression, - IEnumerable leadingTokensForSelect, - IEnumerable trailingTokensForSelect) + /// + /// Creates a linq invocation expression for the node at the given index + /// or returns the if all extended nodes have been processed. + /// + /// Innermost select expression + /// Index into to be processed and updated. + /// Receiver for the generated linq invocation. Updated when processing an if statement. + /// Flag indicating if any of the processed is a . + private ExpressionSyntax CreateLinqInvocationForExtendedNode( + ExpressionSyntax selectExpression, + ref int extendedNodeIndex, + ref ExpressionSyntax receiver, + ref bool hasForEachChild) + { + // Check if we have converted all the descendant foreach/if statements. + // If so, we return the select expression. + if (extendedNodeIndex == ForEachInfo.ConvertingExtendedNodes.Length) { - var foreachStatement = ForEachInfo.ForEachStatement; - selectExpression = selectExpression.WithCommentsFrom(leadingTokensForSelect, ForEachInfo.TrailingTokens.Concat(trailingTokensForSelect)); - var currentExtendedNodeIndex = 0; - - return CreateLinqInvocationOrSimpleExpression( - foreachStatement, - receiverForInvocation: foreachStatement.Expression, - selectExpression: selectExpression, - leadingCommentsTrivia: ForEachInfo.LeadingTokens.GetTrivia(), - trailingCommentsTrivia: [], - currentExtendedNodeIndex: ref currentExtendedNodeIndex) - .WithAdditionalAnnotations(Formatter.Annotation); + return selectExpression; } - private ExpressionSyntax CreateLinqInvocationOrSimpleExpression( - ForEachStatementSyntax forEachStatement, - ExpressionSyntax receiverForInvocation, - IEnumerable leadingCommentsTrivia, - IEnumerable trailingCommentsTrivia, - ExpressionSyntax selectExpression, - ref int currentExtendedNodeIndex) + // Otherwise, convert the next foreach/if statement into a linq invocation. + var node = ForEachInfo.ConvertingExtendedNodes[extendedNodeIndex]; + switch (node.Node.Kind()) { - leadingCommentsTrivia = forEachStatement.ForEachKeyword.GetAllTrivia().Concat(leadingCommentsTrivia); - - // Recursively create linq invocations, possibly updating the receiver (Where invocations), to get the inner expression for - // the lambda body for the linq invocation to be created for this foreach statement. For example: + // Nested ForEach statement is converted into a nested Select or SelectMany linq invocation. For example: // // INPUT: // foreach (var n1 in c1) // foreach (var n2 in c2) - // if (n1 > n2) - // yield return n1 + n2; + // ... // // OUTPUT: - // c1.SelectMany(n1 => c2.Where(n2 => n1 > n2).Select(n2 => n1 + n2)) + // c1.SelectMany(n1 => c2.Select(n2 => ... // - var hasForEachChild = false; - var lambdaBody = CreateLinqInvocationForExtendedNode(selectExpression, ref currentExtendedNodeIndex, ref receiverForInvocation, ref hasForEachChild); - var lambda = SyntaxFactory.SimpleLambdaExpression( - SyntaxFactory.Parameter( - forEachStatement.Identifier.WithPrependedLeadingTrivia( - SyntaxNodeOrTokenExtensions.GetTrivia(forEachStatement.Type.GetFirstToken()) - .FilterComments(addElasticMarker: false))), - lambdaBody) - .WithCommentsFrom(leadingCommentsTrivia, trailingCommentsTrivia, - forEachStatement.OpenParenToken, forEachStatement.InKeyword, forEachStatement.CloseParenToken); + case SyntaxKind.ForEachStatement: + hasForEachChild = true; + var foreachStatement = (ForEachStatementSyntax)node.Node; + ++extendedNodeIndex; + return CreateLinqInvocationOrSimpleExpression( + foreachStatement, + receiverForInvocation: foreachStatement.Expression, + selectExpression: selectExpression, + leadingCommentsTrivia: node.ExtraLeadingComments, + trailingCommentsTrivia: node.ExtraTrailingComments, + currentExtendedNodeIndex: ref extendedNodeIndex); - // Create Select or SelectMany linq invocation for this foreach statement. For example: + // Nested If statement is converted into a Where method invocation on the current receiver. For example: // // INPUT: // foreach (var n1 in c1) - // ... + // if (n1 > 0) + // ... // // OUTPUT: - // c1.Select(n1 => ... - // OR - // c1.SelectMany(n1 => ... + // c1.Where(n1 => n1 > 0).Select(n1 => ... // + case SyntaxKind.IfStatement: + var ifStatement = (IfStatementSyntax)node.Node; + var parentForEachStatement = ifStatement.GetAncestor(); + var lambdaParameter = SyntaxFactory.Parameter(SyntaxFactory.Identifier(parentForEachStatement.Identifier.ValueText)); + var lambda = SyntaxFactory.SimpleLambdaExpression( + SyntaxFactory.Parameter( + SyntaxFactory.Identifier(parentForEachStatement.Identifier.ValueText)), + ifStatement.Condition.WithCommentsFrom(ifStatement.OpenParenToken, ifStatement.CloseParenToken)) + .WithCommentsFrom(ifStatement.IfKeyword.GetAllTrivia().Concat(node.ExtraLeadingComments), node.ExtraTrailingComments); - var invokedMethodName = !hasForEachChild ? nameof(Enumerable.Select) : nameof(Enumerable.SelectMany); - - // Avoid `.Select(x => x)` - if (invokedMethodName == nameof(Enumerable.Select) && - lambdaBody is IdentifierNameSyntax identifier && - identifier.Identifier.ValueText == forEachStatement.Identifier.ValueText) - { - // Because we're dropping the lambda, any comments associated with it need to be preserved. - - var droppedTrivia = new List(); - foreach (var token in lambda.DescendantTokens()) - { - droppedTrivia.AddRange(token.GetAllTrivia().Where(t => !t.IsWhitespace())); - } + receiver = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + receiver.Parenthesize(), + SyntaxFactory.IdentifierName(nameof(Enumerable.Where))), + SyntaxFactory.ArgumentList([SyntaxFactory.Argument(lambda)])); - return receiverForInvocation.WithAppendedTrailingTrivia(droppedTrivia); - } - - return SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - receiverForInvocation.Parenthesize(), - SyntaxFactory.IdentifierName(invokedMethodName)), - SyntaxFactory.ArgumentList([SyntaxFactory.Argument(lambda)])); + ++extendedNodeIndex; + return CreateLinqInvocationForExtendedNode(selectExpression, ref extendedNodeIndex, ref receiver, ref hasForEachChild); } - /// - /// Creates a linq invocation expression for the node at the given index - /// or returns the if all extended nodes have been processed. - /// - /// Innermost select expression - /// Index into to be processed and updated. - /// Receiver for the generated linq invocation. Updated when processing an if statement. - /// Flag indicating if any of the processed is a . - private ExpressionSyntax CreateLinqInvocationForExtendedNode( - ExpressionSyntax selectExpression, - ref int extendedNodeIndex, - ref ExpressionSyntax receiver, - ref bool hasForEachChild) - { - // Check if we have converted all the descendant foreach/if statements. - // If so, we return the select expression. - if (extendedNodeIndex == ForEachInfo.ConvertingExtendedNodes.Length) - { - return selectExpression; - } - - // Otherwise, convert the next foreach/if statement into a linq invocation. - var node = ForEachInfo.ConvertingExtendedNodes[extendedNodeIndex]; - switch (node.Node.Kind()) - { - // Nested ForEach statement is converted into a nested Select or SelectMany linq invocation. For example: - // - // INPUT: - // foreach (var n1 in c1) - // foreach (var n2 in c2) - // ... - // - // OUTPUT: - // c1.SelectMany(n1 => c2.Select(n2 => ... - // - case SyntaxKind.ForEachStatement: - hasForEachChild = true; - var foreachStatement = (ForEachStatementSyntax)node.Node; - ++extendedNodeIndex; - return CreateLinqInvocationOrSimpleExpression( - foreachStatement, - receiverForInvocation: foreachStatement.Expression, - selectExpression: selectExpression, - leadingCommentsTrivia: node.ExtraLeadingComments, - trailingCommentsTrivia: node.ExtraTrailingComments, - currentExtendedNodeIndex: ref extendedNodeIndex); - - // Nested If statement is converted into a Where method invocation on the current receiver. For example: - // - // INPUT: - // foreach (var n1 in c1) - // if (n1 > 0) - // ... - // - // OUTPUT: - // c1.Where(n1 => n1 > 0).Select(n1 => ... - // - case SyntaxKind.IfStatement: - var ifStatement = (IfStatementSyntax)node.Node; - var parentForEachStatement = ifStatement.GetAncestor(); - var lambdaParameter = SyntaxFactory.Parameter(SyntaxFactory.Identifier(parentForEachStatement.Identifier.ValueText)); - var lambda = SyntaxFactory.SimpleLambdaExpression( - SyntaxFactory.Parameter( - SyntaxFactory.Identifier(parentForEachStatement.Identifier.ValueText)), - ifStatement.Condition.WithCommentsFrom(ifStatement.OpenParenToken, ifStatement.CloseParenToken)) - .WithCommentsFrom(ifStatement.IfKeyword.GetAllTrivia().Concat(node.ExtraLeadingComments), node.ExtraTrailingComments); - - receiver = SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - receiver.Parenthesize(), - SyntaxFactory.IdentifierName(nameof(Enumerable.Where))), - SyntaxFactory.ArgumentList([SyntaxFactory.Argument(lambda)])); - - ++extendedNodeIndex; - return CreateLinqInvocationForExtendedNode(selectExpression, ref extendedNodeIndex, ref receiver, ref hasForEachChild); - } - - throw ExceptionUtilities.Unreachable(); - } + throw ExceptionUtilities.Unreachable(); } } diff --git a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractToMethodConverter.cs b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractToMethodConverter.cs index 4b3cc1c3b2875..ea8ae8a8da756 100644 --- a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractToMethodConverter.cs +++ b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractToMethodConverter.cs @@ -16,173 +16,172 @@ using Roslyn.Utilities; using SyntaxNodeOrTokenExtensions = Microsoft.CodeAnalysis.Shared.Extensions.SyntaxNodeOrTokenExtensions; -namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery +namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery; + +/// +/// Provides a conversion to query.Method() like query.ToList(), query.Count(). +/// +internal abstract class AbstractToMethodConverter( + ForEachInfo forEachInfo, + ExpressionSyntax selectExpression, + ExpressionSyntax modifyingExpression, + SyntaxTrivia[] trivia) : AbstractConverter(forEachInfo) { - /// - /// Provides a conversion to query.Method() like query.ToList(), query.Count(). - /// - internal abstract class AbstractToMethodConverter( - ForEachInfo forEachInfo, - ExpressionSyntax selectExpression, - ExpressionSyntax modifyingExpression, - SyntaxTrivia[] trivia) : AbstractConverter(forEachInfo) - { - // It is "item" for for "list.Add(item)" - // It can be anything for "counter++". It will be ingored in the case. - private readonly ExpressionSyntax _selectExpression = selectExpression; + // It is "item" for for "list.Add(item)" + // It can be anything for "counter++". It will be ingored in the case. + private readonly ExpressionSyntax _selectExpression = selectExpression; - // It is "list" for "list.Add(item)" - // It is "counter" for "counter++" - private readonly ExpressionSyntax _modifyingExpression = modifyingExpression; + // It is "list" for "list.Add(item)" + // It is "counter" for "counter++" + private readonly ExpressionSyntax _modifyingExpression = modifyingExpression; - // Trivia around "counter++;" or "list.Add(item);". Required to keep comments. - private readonly SyntaxTrivia[] _trivia = trivia; + // Trivia around "counter++;" or "list.Add(item);". Required to keep comments. + private readonly SyntaxTrivia[] _trivia = trivia; - protected abstract string MethodName { get; } + protected abstract string MethodName { get; } - protected abstract bool CanReplaceInitialization(ExpressionSyntax expressionSyntax, CancellationToken cancellationToken); + protected abstract bool CanReplaceInitialization(ExpressionSyntax expressionSyntax, CancellationToken cancellationToken); - protected abstract StatementSyntax CreateDefaultStatement(ExpressionSyntax queryOrLinqInvocationExpression, ExpressionSyntax expression); + protected abstract StatementSyntax CreateDefaultStatement(ExpressionSyntax queryOrLinqInvocationExpression, ExpressionSyntax expression); - public override void Convert(SyntaxEditor editor, bool convertToQuery, CancellationToken cancellationToken) - { - var queryOrLinqInvocationExpression = CreateQueryExpressionOrLinqInvocation( - _selectExpression, [], [], convertToQuery); + public override void Convert(SyntaxEditor editor, bool convertToQuery, CancellationToken cancellationToken) + { + var queryOrLinqInvocationExpression = CreateQueryExpressionOrLinqInvocation( + _selectExpression, [], [], convertToQuery); - var previous = ForEachInfo.ForEachStatement.GetPreviousStatement(); + var previous = ForEachInfo.ForEachStatement.GetPreviousStatement(); - if (previous != null && !previous.ContainsDirectives) + if (previous != null && !previous.ContainsDirectives) + { + switch (previous.Kind()) { - switch (previous.Kind()) - { - case SyntaxKind.LocalDeclarationStatement: - var variables = ((LocalDeclarationStatementSyntax)previous).Declaration.Variables; - var lastDeclaration = variables.Last(); - // Check if - // var ...., list = new List(); or var ..., counter = 0; - // is just before the foreach. - // If so, join the declaration with the query. - if (_modifyingExpression is IdentifierNameSyntax identifierName && - lastDeclaration.Identifier.ValueText.Equals(identifierName.Identifier.ValueText) && - CanReplaceInitialization(lastDeclaration.Initializer.Value, cancellationToken)) - { - Convert(lastDeclaration.Initializer.Value, variables.Count == 1 ? previous : lastDeclaration); - return; - } - - break; - - case SyntaxKind.ExpressionStatement: - // Check if - // list = new List(); or counter = 0; - // is just before the foreach. - // If so, join the assignment with the query. - if (((ExpressionStatementSyntax)previous).Expression is AssignmentExpressionSyntax assignmentExpression && - SymbolEquivalenceComparer.Instance.Equals( - ForEachInfo.SemanticModel.GetSymbolInfo(assignmentExpression.Left, cancellationToken).Symbol, - ForEachInfo.SemanticModel.GetSymbolInfo(_modifyingExpression, cancellationToken).Symbol) && - CanReplaceInitialization(assignmentExpression.Right, cancellationToken)) - { - Convert(assignmentExpression.Right, previous); - return; - } - - break; - } + case SyntaxKind.LocalDeclarationStatement: + var variables = ((LocalDeclarationStatementSyntax)previous).Declaration.Variables; + var lastDeclaration = variables.Last(); + // Check if + // var ...., list = new List(); or var ..., counter = 0; + // is just before the foreach. + // If so, join the declaration with the query. + if (_modifyingExpression is IdentifierNameSyntax identifierName && + lastDeclaration.Identifier.ValueText.Equals(identifierName.Identifier.ValueText) && + CanReplaceInitialization(lastDeclaration.Initializer.Value, cancellationToken)) + { + Convert(lastDeclaration.Initializer.Value, variables.Count == 1 ? previous : lastDeclaration); + return; + } + + break; + + case SyntaxKind.ExpressionStatement: + // Check if + // list = new List(); or counter = 0; + // is just before the foreach. + // If so, join the assignment with the query. + if (((ExpressionStatementSyntax)previous).Expression is AssignmentExpressionSyntax assignmentExpression && + SymbolEquivalenceComparer.Instance.Equals( + ForEachInfo.SemanticModel.GetSymbolInfo(assignmentExpression.Left, cancellationToken).Symbol, + ForEachInfo.SemanticModel.GetSymbolInfo(_modifyingExpression, cancellationToken).Symbol) && + CanReplaceInitialization(assignmentExpression.Right, cancellationToken)) + { + Convert(assignmentExpression.Right, previous); + return; + } + + break; } + } - // At least, we already can convert to - // list.AddRange(query) or counter += query.Count(); - editor.ReplaceNode( - ForEachInfo.ForEachStatement, - CreateDefaultStatement(queryOrLinqInvocationExpression, _modifyingExpression).WithAdditionalAnnotations(Formatter.Annotation)); + // At least, we already can convert to + // list.AddRange(query) or counter += query.Count(); + editor.ReplaceNode( + ForEachInfo.ForEachStatement, + CreateDefaultStatement(queryOrLinqInvocationExpression, _modifyingExpression).WithAdditionalAnnotations(Formatter.Annotation)); - return; + return; - void Convert(ExpressionSyntax replacingExpression, SyntaxNode nodeToRemoveIfFollowedByReturn) + void Convert(ExpressionSyntax replacingExpression, SyntaxNode nodeToRemoveIfFollowedByReturn) + { + SyntaxTrivia[] leadingTrivia; + + // Check if expressionAssigning is followed by a return statement. + var expresisonSymbol = ForEachInfo.SemanticModel.GetSymbolInfo(_modifyingExpression, cancellationToken).Symbol; + if (expresisonSymbol is ILocalSymbol && + ForEachInfo.ForEachStatement.GetNextStatement() is ReturnStatementSyntax returnStatement && + !returnStatement.ContainsDirectives && + SymbolEquivalenceComparer.Instance.Equals( + expresisonSymbol, ForEachInfo.SemanticModel.GetSymbolInfo(returnStatement.Expression, cancellationToken).Symbol)) { - SyntaxTrivia[] leadingTrivia; - - // Check if expressionAssigning is followed by a return statement. - var expresisonSymbol = ForEachInfo.SemanticModel.GetSymbolInfo(_modifyingExpression, cancellationToken).Symbol; - if (expresisonSymbol is ILocalSymbol && - ForEachInfo.ForEachStatement.GetNextStatement() is ReturnStatementSyntax returnStatement && - !returnStatement.ContainsDirectives && - SymbolEquivalenceComparer.Instance.Equals( - expresisonSymbol, ForEachInfo.SemanticModel.GetSymbolInfo(returnStatement.Expression, cancellationToken).Symbol)) - { - // Input: - // var list = new List(); or var counter = 0; - // foreach(...) - // { - // ... - // ... - // list.Add(item); or counter++; - // } - // return list; or return counter; - // - // Output: - // return queryGenerated.ToList(); or return queryGenerated.Count(); - replacingExpression = returnStatement.Expression; - leadingTrivia = GetTriviaFromNode(nodeToRemoveIfFollowedByReturn) - .Concat(SyntaxNodeOrTokenExtensions.GetTrivia(replacingExpression)).ToArray(); - editor.RemoveNode(nodeToRemoveIfFollowedByReturn); - } - else - { - leadingTrivia = SyntaxNodeOrTokenExtensions.GetTrivia(replacingExpression); - } - - editor.ReplaceNode( - replacingExpression, - CreateInvocationExpression(queryOrLinqInvocationExpression) - .WithCommentsFrom(leadingTrivia, _trivia).KeepCommentsAndAddElasticMarkers()); - editor.RemoveNode(ForEachInfo.ForEachStatement); + // Input: + // var list = new List(); or var counter = 0; + // foreach(...) + // { + // ... + // ... + // list.Add(item); or counter++; + // } + // return list; or return counter; + // + // Output: + // return queryGenerated.ToList(); or return queryGenerated.Count(); + replacingExpression = returnStatement.Expression; + leadingTrivia = GetTriviaFromNode(nodeToRemoveIfFollowedByReturn) + .Concat(SyntaxNodeOrTokenExtensions.GetTrivia(replacingExpression)).ToArray(); + editor.RemoveNode(nodeToRemoveIfFollowedByReturn); } - - static SyntaxTrivia[] GetTriviaFromVariableDeclarator(VariableDeclaratorSyntax variableDeclarator) - => SyntaxNodeOrTokenExtensions.GetTrivia(variableDeclarator.Identifier, variableDeclarator.Initializer.EqualsToken, variableDeclarator.Initializer.Value); - static SyntaxTrivia[] GetTriviaFromNode(SyntaxNode node) + else { - switch (node.Kind()) - { - case SyntaxKind.LocalDeclarationStatement: - - var localDeclaration = (LocalDeclarationStatementSyntax)node; - if (localDeclaration.Declaration.Variables.Count != 1) - { - throw ExceptionUtilities.Unreachable(); - } - - return new IEnumerable[] { - SyntaxNodeOrTokenExtensions.GetTrivia(localDeclaration.Declaration.Type), - GetTriviaFromVariableDeclarator(localDeclaration.Declaration.Variables[0]), - SyntaxNodeOrTokenExtensions.GetTrivia(localDeclaration.SemicolonToken)}.Flatten().ToArray(); - - case SyntaxKind.VariableDeclarator: - return GetTriviaFromVariableDeclarator((VariableDeclaratorSyntax)node); - - case SyntaxKind.ExpressionStatement: - if (((ExpressionStatementSyntax)node).Expression is AssignmentExpressionSyntax assignmentExpression) - { - return SyntaxNodeOrTokenExtensions.GetTrivia( - assignmentExpression.Left, assignmentExpression.OperatorToken, assignmentExpression.Right); - } - - break; - } - - throw ExceptionUtilities.Unreachable(); + leadingTrivia = SyntaxNodeOrTokenExtensions.GetTrivia(replacingExpression); } + + editor.ReplaceNode( + replacingExpression, + CreateInvocationExpression(queryOrLinqInvocationExpression) + .WithCommentsFrom(leadingTrivia, _trivia).KeepCommentsAndAddElasticMarkers()); + editor.RemoveNode(ForEachInfo.ForEachStatement); } - // query => query.Method() - // like query.Count() or query.ToList() - protected InvocationExpressionSyntax CreateInvocationExpression(ExpressionSyntax queryOrLinqInvocationExpression) - => SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - queryOrLinqInvocationExpression.Parenthesize(), - SyntaxFactory.IdentifierName(MethodName))).WithAdditionalAnnotations(Formatter.Annotation); + static SyntaxTrivia[] GetTriviaFromVariableDeclarator(VariableDeclaratorSyntax variableDeclarator) + => SyntaxNodeOrTokenExtensions.GetTrivia(variableDeclarator.Identifier, variableDeclarator.Initializer.EqualsToken, variableDeclarator.Initializer.Value); + static SyntaxTrivia[] GetTriviaFromNode(SyntaxNode node) + { + switch (node.Kind()) + { + case SyntaxKind.LocalDeclarationStatement: + + var localDeclaration = (LocalDeclarationStatementSyntax)node; + if (localDeclaration.Declaration.Variables.Count != 1) + { + throw ExceptionUtilities.Unreachable(); + } + + return new IEnumerable[] { + SyntaxNodeOrTokenExtensions.GetTrivia(localDeclaration.Declaration.Type), + GetTriviaFromVariableDeclarator(localDeclaration.Declaration.Variables[0]), + SyntaxNodeOrTokenExtensions.GetTrivia(localDeclaration.SemicolonToken)}.Flatten().ToArray(); + + case SyntaxKind.VariableDeclarator: + return GetTriviaFromVariableDeclarator((VariableDeclaratorSyntax)node); + + case SyntaxKind.ExpressionStatement: + if (((ExpressionStatementSyntax)node).Expression is AssignmentExpressionSyntax assignmentExpression) + { + return SyntaxNodeOrTokenExtensions.GetTrivia( + assignmentExpression.Left, assignmentExpression.OperatorToken, assignmentExpression.Right); + } + + break; + } + + throw ExceptionUtilities.Unreachable(); + } } + + // query => query.Method() + // like query.Count() or query.ToList() + protected InvocationExpressionSyntax CreateInvocationExpression(ExpressionSyntax queryOrLinqInvocationExpression) + => SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + queryOrLinqInvocationExpression.Parenthesize(), + SyntaxFactory.IdentifierName(MethodName))).WithAdditionalAnnotations(Formatter.Annotation); } diff --git a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/CSharpConvertForEachToLinqQueryProvider.cs b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/CSharpConvertForEachToLinqQueryProvider.cs index 55e510411b94b..4c16c562118c1 100644 --- a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/CSharpConvertForEachToLinqQueryProvider.cs +++ b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/CSharpConvertForEachToLinqQueryProvider.cs @@ -18,339 +18,338 @@ using Roslyn.Utilities; using SyntaxNodeOrTokenExtensions = Microsoft.CodeAnalysis.Shared.Extensions.SyntaxNodeOrTokenExtensions; -namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery +namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertForEachToLinqQuery), Shared] +internal sealed class CSharpConvertForEachToLinqQueryProvider + : AbstractConvertForEachToLinqQueryProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertForEachToLinqQuery), Shared] - internal sealed class CSharpConvertForEachToLinqQueryProvider - : AbstractConvertForEachToLinqQueryProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertForEachToLinqQueryProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertForEachToLinqQueryProvider() - { - } + } - protected override IConverter CreateDefaultConverter( - ForEachInfo forEachInfo) - => new DefaultConverter(forEachInfo); + protected override IConverter CreateDefaultConverter( + ForEachInfo forEachInfo) + => new DefaultConverter(forEachInfo); - protected override ForEachInfo CreateForEachInfo( - ForEachStatementSyntax forEachStatement, - SemanticModel semanticModel, - bool convertLocalDeclarations) + protected override ForEachInfo CreateForEachInfo( + ForEachStatementSyntax forEachStatement, + SemanticModel semanticModel, + bool convertLocalDeclarations) + { + var identifiersBuilder = ArrayBuilder.GetInstance(); + identifiersBuilder.Add(forEachStatement.Identifier); + var convertingNodesBuilder = ArrayBuilder.GetInstance(); + IEnumerable? statementsCannotBeConverted = null; + var trailingTokensBuilder = ArrayBuilder.GetInstance(); + var currentLeadingTokens = ArrayBuilder.GetInstance(); + + var current = forEachStatement.Statement; + // Traverse descendants of the forEachStatement. + // If a statement traversed can be converted into a query clause, + // a. Add it to convertingNodesBuilder. + // b. set the current to its nested statement and proceed. + // Otherwise, set statementsCannotBeConverted and stop processing. + while (statementsCannotBeConverted == null) { - var identifiersBuilder = ArrayBuilder.GetInstance(); - identifiersBuilder.Add(forEachStatement.Identifier); - var convertingNodesBuilder = ArrayBuilder.GetInstance(); - IEnumerable? statementsCannotBeConverted = null; - var trailingTokensBuilder = ArrayBuilder.GetInstance(); - var currentLeadingTokens = ArrayBuilder.GetInstance(); - - var current = forEachStatement.Statement; - // Traverse descendants of the forEachStatement. - // If a statement traversed can be converted into a query clause, - // a. Add it to convertingNodesBuilder. - // b. set the current to its nested statement and proceed. - // Otherwise, set statementsCannotBeConverted and stop processing. - while (statementsCannotBeConverted == null) + switch (current.Kind()) { - switch (current.Kind()) - { - case SyntaxKind.Block: - var block = (BlockSyntax)current; - // Keep comment trivia from braces to attach them to the qeury created. - currentLeadingTokens.Add(block.OpenBraceToken); - trailingTokensBuilder.Add(block.CloseBraceToken); - var array = block.Statements.ToArray(); - if (array.Length > 0) + case SyntaxKind.Block: + var block = (BlockSyntax)current; + // Keep comment trivia from braces to attach them to the qeury created. + currentLeadingTokens.Add(block.OpenBraceToken); + trailingTokensBuilder.Add(block.CloseBraceToken); + var array = block.Statements.ToArray(); + if (array.Length > 0) + { + // All except the last one can be local declaration statements like + // { + // var a = 0; + // var b = 0; + // if (x != y) <- this is the last one in the block. + // We can support it to be a complex foreach or if or whatever. So, set the current to it. + // ... + // } + for (var i = 0; i < array.Length - 1; i++) { - // All except the last one can be local declaration statements like - // { - // var a = 0; - // var b = 0; - // if (x != y) <- this is the last one in the block. - // We can support it to be a complex foreach or if or whatever. So, set the current to it. - // ... - // } - for (var i = 0; i < array.Length - 1; i++) + var statement = array[i]; + if (!(statement is LocalDeclarationStatementSyntax localDeclarationStatement && + TryProcessLocalDeclarationStatement(localDeclarationStatement))) { - var statement = array[i]; - if (!(statement is LocalDeclarationStatementSyntax localDeclarationStatement && - TryProcessLocalDeclarationStatement(localDeclarationStatement))) - { - // If this one is a local function declaration or has an empty initializer, stop processing. - statementsCannotBeConverted = array.Skip(i).ToArray(); - break; - } + // If this one is a local function declaration or has an empty initializer, stop processing. + statementsCannotBeConverted = array.Skip(i).ToArray(); + break; } - - // Process the last statement separately. - current = array.Last(); - } - else - { - // Silly case: the block is empty. Stop processing. - statementsCannotBeConverted = []; } - break; + // Process the last statement separately. + current = array.Last(); + } + else + { + // Silly case: the block is empty. Stop processing. + statementsCannotBeConverted = []; + } + + break; + + case SyntaxKind.ForEachStatement: + // foreach can always be converted to a from clause. + var currentForEachStatement = (ForEachStatementSyntax)current; + identifiersBuilder.Add(currentForEachStatement.Identifier); + convertingNodesBuilder.Add(new ExtendedSyntaxNode(currentForEachStatement, currentLeadingTokens.ToImmutableAndFree(), [])); + currentLeadingTokens = ArrayBuilder.GetInstance(); + // Proceed the loop with the nested statement. + current = currentForEachStatement.Statement; + break; - case SyntaxKind.ForEachStatement: - // foreach can always be converted to a from clause. - var currentForEachStatement = (ForEachStatementSyntax)current; - identifiersBuilder.Add(currentForEachStatement.Identifier); - convertingNodesBuilder.Add(new ExtendedSyntaxNode(currentForEachStatement, currentLeadingTokens.ToImmutableAndFree(), [])); + case SyntaxKind.IfStatement: + // Prepare conversion of 'if (condition)' into where clauses. + // Do not support if-else statements in the conversion. + var ifStatement = (IfStatementSyntax)current; + if (ifStatement.Else == null) + { + convertingNodesBuilder.Add(new ExtendedSyntaxNode( + ifStatement, currentLeadingTokens.ToImmutableAndFree(), [])); currentLeadingTokens = ArrayBuilder.GetInstance(); // Proceed the loop with the nested statement. - current = currentForEachStatement.Statement; + current = ifStatement.Statement; break; - - case SyntaxKind.IfStatement: - // Prepare conversion of 'if (condition)' into where clauses. - // Do not support if-else statements in the conversion. - var ifStatement = (IfStatementSyntax)current; - if (ifStatement.Else == null) - { - convertingNodesBuilder.Add(new ExtendedSyntaxNode( - ifStatement, currentLeadingTokens.ToImmutableAndFree(), [])); - currentLeadingTokens = ArrayBuilder.GetInstance(); - // Proceed the loop with the nested statement. - current = ifStatement.Statement; - break; - } - else - { - statementsCannotBeConverted = [current]; - break; - } - - case SyntaxKind.LocalDeclarationStatement: - // This is a situation with "var a = something;" is the innermost statements inside the loop. - var localDeclaration = (LocalDeclarationStatementSyntax)current; - if (TryProcessLocalDeclarationStatement(localDeclaration)) - { - statementsCannotBeConverted = []; - } - else - { - // As above, if there is an empty initializer, stop processing. - statementsCannotBeConverted = [current]; - } - + } + else + { + statementsCannotBeConverted = [current]; break; + } - case SyntaxKind.EmptyStatement: - // The innermost statement is an empty statement, stop processing - // Example: - // foreach(...) - // { - // ;<- empty statement - // } + case SyntaxKind.LocalDeclarationStatement: + // This is a situation with "var a = something;" is the innermost statements inside the loop. + var localDeclaration = (LocalDeclarationStatementSyntax)current; + if (TryProcessLocalDeclarationStatement(localDeclaration)) + { statementsCannotBeConverted = []; - break; - - default: - // If no specific case found, stop processing. + } + else + { + // As above, if there is an empty initializer, stop processing. statementsCannotBeConverted = [current]; - break; - } + } + + break; + + case SyntaxKind.EmptyStatement: + // The innermost statement is an empty statement, stop processing + // Example: + // foreach(...) + // { + // ;<- empty statement + // } + statementsCannotBeConverted = []; + break; + + default: + // If no specific case found, stop processing. + statementsCannotBeConverted = [current]; + break; } + } - // Trailing tokens are collected in the reverse order: from external block down to internal ones. Reverse them. - trailingTokensBuilder.ReverseContents(); + // Trailing tokens are collected in the reverse order: from external block down to internal ones. Reverse them. + trailingTokensBuilder.ReverseContents(); - return new ForEachInfo( - forEachStatement, - semanticModel, - convertingNodesBuilder.ToImmutableAndFree(), - identifiersBuilder.ToImmutableAndFree(), - statementsCannotBeConverted.ToImmutableArray(), - currentLeadingTokens.ToImmutableAndFree(), - trailingTokensBuilder.ToImmutableAndFree()); + return new ForEachInfo( + forEachStatement, + semanticModel, + convertingNodesBuilder.ToImmutableAndFree(), + identifiersBuilder.ToImmutableAndFree(), + statementsCannotBeConverted.ToImmutableArray(), + currentLeadingTokens.ToImmutableAndFree(), + trailingTokensBuilder.ToImmutableAndFree()); - // Try to prepare variable declarations to be converted into separate let clauses. - bool TryProcessLocalDeclarationStatement(LocalDeclarationStatementSyntax localDeclarationStatement) + // Try to prepare variable declarations to be converted into separate let clauses. + bool TryProcessLocalDeclarationStatement(LocalDeclarationStatementSyntax localDeclarationStatement) + { + if (!convertLocalDeclarations) { - if (!convertLocalDeclarations) - { - return false; - } + return false; + } - // Do not support declarations without initialization. - // int a = 0, b, c = 0; - if (localDeclarationStatement.Declaration.Variables.All(variable => variable.Initializer != null)) + // Do not support declarations without initialization. + // int a = 0, b, c = 0; + if (localDeclarationStatement.Declaration.Variables.All(variable => variable.Initializer != null)) + { + var localDeclarationLeadingTrivia = new IEnumerable[] { + currentLeadingTokens.ToImmutableAndFree().GetTrivia(), + localDeclarationStatement.Declaration.Type.GetLeadingTrivia(), + localDeclarationStatement.Declaration.Type.GetTrailingTrivia() }.Flatten(); + currentLeadingTokens = ArrayBuilder.GetInstance(); + var localDeclarationTrailingTrivia = SyntaxNodeOrTokenExtensions.GetTrivia(localDeclarationStatement.SemicolonToken); + var separators = localDeclarationStatement.Declaration.Variables.GetSeparators().ToArray(); + for (var i = 0; i < localDeclarationStatement.Declaration.Variables.Count; i++) { - var localDeclarationLeadingTrivia = new IEnumerable[] { - currentLeadingTokens.ToImmutableAndFree().GetTrivia(), - localDeclarationStatement.Declaration.Type.GetLeadingTrivia(), - localDeclarationStatement.Declaration.Type.GetTrailingTrivia() }.Flatten(); - currentLeadingTokens = ArrayBuilder.GetInstance(); - var localDeclarationTrailingTrivia = SyntaxNodeOrTokenExtensions.GetTrivia(localDeclarationStatement.SemicolonToken); - var separators = localDeclarationStatement.Declaration.Variables.GetSeparators().ToArray(); - for (var i = 0; i < localDeclarationStatement.Declaration.Variables.Count; i++) - { - var variable = localDeclarationStatement.Declaration.Variables[i]; - convertingNodesBuilder.Add(new ExtendedSyntaxNode( - variable, - i == 0 ? localDeclarationLeadingTrivia : separators[i - 1].TrailingTrivia, - i == localDeclarationStatement.Declaration.Variables.Count - 1 - ? localDeclarationTrailingTrivia - : separators[i].LeadingTrivia)); - identifiersBuilder.Add(variable.Identifier); - } - - return true; + var variable = localDeclarationStatement.Declaration.Variables[i]; + convertingNodesBuilder.Add(new ExtendedSyntaxNode( + variable, + i == 0 ? localDeclarationLeadingTrivia : separators[i - 1].TrailingTrivia, + i == localDeclarationStatement.Declaration.Variables.Count - 1 + ? localDeclarationTrailingTrivia + : separators[i].LeadingTrivia)); + identifiersBuilder.Add(variable.Identifier); } - return false; + return true; } + + return false; } + } - protected override bool TryBuildSpecificConverter( - ForEachInfo forEachInfo, - SemanticModel semanticModel, - StatementSyntax statementCannotBeConverted, - CancellationToken cancellationToken, - [NotNullWhen(true)] out IConverter? converter) + protected override bool TryBuildSpecificConverter( + ForEachInfo forEachInfo, + SemanticModel semanticModel, + StatementSyntax statementCannotBeConverted, + CancellationToken cancellationToken, + [NotNullWhen(true)] out IConverter? converter) + { + switch (statementCannotBeConverted.Kind()) { - switch (statementCannotBeConverted.Kind()) - { - case SyntaxKind.ExpressionStatement: - var expresisonStatement = (ExpressionStatementSyntax)statementCannotBeConverted; - var expression = expresisonStatement.Expression; - switch (expression.Kind()) - { - case SyntaxKind.PostIncrementExpression: + case SyntaxKind.ExpressionStatement: + var expresisonStatement = (ExpressionStatementSyntax)statementCannotBeConverted; + var expression = expresisonStatement.Expression; + switch (expression.Kind()) + { + case SyntaxKind.PostIncrementExpression: + // Input: + // foreach (var x in a) + // { + // ... + // c++; + // } + // Output: + // (from x in a ... select x).Count(); + // Here we put SyntaxFactory.IdentifierName(forEachStatement.Identifier) ('x' in the example) + // into the select clause. + var postfixUnaryExpression = (PostfixUnaryExpressionSyntax)expression; + var operand = postfixUnaryExpression.Operand; + converter = new ToCountConverter( + forEachInfo, + selectExpression: SyntaxFactory.IdentifierName(forEachInfo.ForEachStatement.Identifier), + modifyingExpression: operand, + trivia: SyntaxNodeOrTokenExtensions.GetTrivia( + operand, postfixUnaryExpression.OperatorToken, expresisonStatement.SemicolonToken)); + return true; + + case SyntaxKind.InvocationExpression: + var invocationExpression = (InvocationExpressionSyntax)expression; + // Check that there is 'list.Add(item)'. + if (invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression && + semanticModel.GetSymbolInfo(memberAccessExpression, cancellationToken).Symbol is IMethodSymbol methodSymbol && + TypeSymbolIsList(methodSymbol.ContainingType, semanticModel) && + methodSymbol.Name == nameof(IList.Add) && + methodSymbol.Parameters.Length == 1 && + invocationExpression.ArgumentList.Arguments.Count == 1) + { // Input: // foreach (var x in a) // { // ... - // c++; + // list.Add(...); // } // Output: - // (from x in a ... select x).Count(); - // Here we put SyntaxFactory.IdentifierName(forEachStatement.Identifier) ('x' in the example) - // into the select clause. - var postfixUnaryExpression = (PostfixUnaryExpressionSyntax)expression; - var operand = postfixUnaryExpression.Operand; - converter = new ToCountConverter( + // (from x in a ... select x).ToList(); + var selectExpression = invocationExpression.ArgumentList.Arguments.Single().Expression; + converter = new ToToListConverter( forEachInfo, - selectExpression: SyntaxFactory.IdentifierName(forEachInfo.ForEachStatement.Identifier), - modifyingExpression: operand, + selectExpression, + modifyingExpression: memberAccessExpression.Expression, trivia: SyntaxNodeOrTokenExtensions.GetTrivia( - operand, postfixUnaryExpression.OperatorToken, expresisonStatement.SemicolonToken)); + memberAccessExpression, + invocationExpression.ArgumentList.OpenParenToken, + invocationExpression.ArgumentList.CloseParenToken, + expresisonStatement.SemicolonToken)); return true; + } - case SyntaxKind.InvocationExpression: - var invocationExpression = (InvocationExpressionSyntax)expression; - // Check that there is 'list.Add(item)'. - if (invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression && - semanticModel.GetSymbolInfo(memberAccessExpression, cancellationToken).Symbol is IMethodSymbol methodSymbol && - TypeSymbolIsList(methodSymbol.ContainingType, semanticModel) && - methodSymbol.Name == nameof(IList.Add) && - methodSymbol.Parameters.Length == 1 && - invocationExpression.ArgumentList.Arguments.Count == 1) - { - // Input: - // foreach (var x in a) - // { - // ... - // list.Add(...); - // } - // Output: - // (from x in a ... select x).ToList(); - var selectExpression = invocationExpression.ArgumentList.Arguments.Single().Expression; - converter = new ToToListConverter( - forEachInfo, - selectExpression, - modifyingExpression: memberAccessExpression.Expression, - trivia: SyntaxNodeOrTokenExtensions.GetTrivia( - memberAccessExpression, - invocationExpression.ArgumentList.OpenParenToken, - invocationExpression.ArgumentList.CloseParenToken, - expresisonStatement.SemicolonToken)); - return true; - } - - break; - } + break; + } - break; + break; - case SyntaxKind.YieldReturnStatement: - var memberDeclarationSymbol = semanticModel.GetEnclosingSymbol( - forEachInfo.ForEachStatement.SpanStart, cancellationToken)!; + case SyntaxKind.YieldReturnStatement: + var memberDeclarationSymbol = semanticModel.GetEnclosingSymbol( + forEachInfo.ForEachStatement.SpanStart, cancellationToken)!; - // Using Single() is valid even for partial methods. - var memberDeclarationSyntax = memberDeclarationSymbol.DeclaringSyntaxReferences.Single().GetSyntax(); + // Using Single() is valid even for partial methods. + var memberDeclarationSyntax = memberDeclarationSymbol.DeclaringSyntaxReferences.Single().GetSyntax(); - var yieldStatementsCount = memberDeclarationSyntax.DescendantNodes().OfType() - // Exclude yield statements from nested local functions. - .Where(statement => Equals(semanticModel.GetEnclosingSymbol( - statement.SpanStart, cancellationToken), memberDeclarationSymbol)).Count(); + var yieldStatementsCount = memberDeclarationSyntax.DescendantNodes().OfType() + // Exclude yield statements from nested local functions. + .Where(statement => Equals(semanticModel.GetEnclosingSymbol( + statement.SpanStart, cancellationToken), memberDeclarationSymbol)).Count(); - if (forEachInfo.ForEachStatement?.Parent is BlockSyntax block && - block.Parent == memberDeclarationSyntax) + if (forEachInfo.ForEachStatement?.Parent is BlockSyntax block && + block.Parent == memberDeclarationSyntax) + { + // Check that + // a. There are either just a single 'yield return' or 'yield return' with 'yield break' just after. + // b. Those foreach and 'yield break' (if exists) are last statements in the method (do not count local function declaration statements). + var statementsOnBlockWithForEach = block.Statements + .Where(statement => statement.Kind() != SyntaxKind.LocalFunctionStatement).ToArray(); + var lastNonLocalFunctionStatement = statementsOnBlockWithForEach.Last(); + if (yieldStatementsCount == 1 && lastNonLocalFunctionStatement == forEachInfo.ForEachStatement) { - // Check that - // a. There are either just a single 'yield return' or 'yield return' with 'yield break' just after. - // b. Those foreach and 'yield break' (if exists) are last statements in the method (do not count local function declaration statements). - var statementsOnBlockWithForEach = block.Statements - .Where(statement => statement.Kind() != SyntaxKind.LocalFunctionStatement).ToArray(); - var lastNonLocalFunctionStatement = statementsOnBlockWithForEach.Last(); - if (yieldStatementsCount == 1 && lastNonLocalFunctionStatement == forEachInfo.ForEachStatement) - { - converter = new YieldReturnConverter( - forEachInfo, - (YieldStatementSyntax)statementCannotBeConverted, - yieldBreakStatement: null); - return true; - } - - // foreach() - // { - // yield return ...; - // } - // yield break; - // end of member - if (yieldStatementsCount == 2 && - lastNonLocalFunctionStatement.Kind() == SyntaxKind.YieldBreakStatement && - !lastNonLocalFunctionStatement.ContainsDirectives && - statementsOnBlockWithForEach[statementsOnBlockWithForEach.Length - 2] == forEachInfo.ForEachStatement) - { - // This removes the yield break. - converter = new YieldReturnConverter( - forEachInfo, - (YieldStatementSyntax)statementCannotBeConverted, - yieldBreakStatement: (YieldStatementSyntax)lastNonLocalFunctionStatement); - return true; - } + converter = new YieldReturnConverter( + forEachInfo, + (YieldStatementSyntax)statementCannotBeConverted, + yieldBreakStatement: null); + return true; } - break; - } + // foreach() + // { + // yield return ...; + // } + // yield break; + // end of member + if (yieldStatementsCount == 2 && + lastNonLocalFunctionStatement.Kind() == SyntaxKind.YieldBreakStatement && + !lastNonLocalFunctionStatement.ContainsDirectives && + statementsOnBlockWithForEach[statementsOnBlockWithForEach.Length - 2] == forEachInfo.ForEachStatement) + { + // This removes the yield break. + converter = new YieldReturnConverter( + forEachInfo, + (YieldStatementSyntax)statementCannotBeConverted, + yieldBreakStatement: (YieldStatementSyntax)lastNonLocalFunctionStatement); + return true; + } + } - converter = null; - return false; + break; } - protected override SyntaxNode AddLinqUsing( - IConverter converter, - SemanticModel semanticModel, - SyntaxNode root) - { - var namespaces = semanticModel.GetUsingNamespacesInScope(converter.ForEachInfo.ForEachStatement); - if (!namespaces.Any(namespaceSymbol => namespaceSymbol.Name == nameof(System.Linq) && - namespaceSymbol.ContainingNamespace.Name == nameof(System)) && - root is CompilationUnitSyntax compilationUnit) - { - return compilationUnit.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Linq"))); - } + converter = null; + return false; + } - return root; + protected override SyntaxNode AddLinqUsing( + IConverter converter, + SemanticModel semanticModel, + SyntaxNode root) + { + var namespaces = semanticModel.GetUsingNamespacesInScope(converter.ForEachInfo.ForEachStatement); + if (!namespaces.Any(namespaceSymbol => namespaceSymbol.Name == nameof(System.Linq) && + namespaceSymbol.ContainingNamespace.Name == nameof(System)) && + root is CompilationUnitSyntax compilationUnit) + { + return compilationUnit.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Linq"))); } - internal static bool TypeSymbolIsList(ITypeSymbol typeSymbol, SemanticModel semanticModel) - => Equals(typeSymbol?.OriginalDefinition, semanticModel.Compilation.ListOfTType()); + return root; } + + internal static bool TypeSymbolIsList(ITypeSymbol typeSymbol, SemanticModel semanticModel) + => Equals(typeSymbol?.OriginalDefinition, semanticModel.Compilation.ListOfTType()); } diff --git a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/DefaultConverter.cs b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/DefaultConverter.cs index ce1fb907abbd6..53c5142fa885b 100644 --- a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/DefaultConverter.cs +++ b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/DefaultConverter.cs @@ -14,86 +14,85 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; -namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery +namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery; + +internal sealed class DefaultConverter(ForEachInfo forEachInfo) : AbstractConverter(forEachInfo) { - internal sealed class DefaultConverter(ForEachInfo forEachInfo) : AbstractConverter(forEachInfo) + private static readonly TypeSyntax VarNameIdentifier = SyntaxFactory.IdentifierName("var"); + + public override void Convert(SyntaxEditor editor, bool convertToQuery, CancellationToken cancellationToken) { - private static readonly TypeSyntax VarNameIdentifier = SyntaxFactory.IdentifierName("var"); + // Filter out identifiers which are not used in statements. + var variableNamesReadInside = new HashSet(ForEachInfo.Statements + .SelectMany(statement => ForEachInfo.SemanticModel.AnalyzeDataFlow(statement).ReadInside).Select(symbol => symbol.Name)); + var identifiersUsedInStatements = ForEachInfo.Identifiers + .Where(identifier => variableNamesReadInside.Contains(identifier.ValueText)); - public override void Convert(SyntaxEditor editor, bool convertToQuery, CancellationToken cancellationToken) - { - // Filter out identifiers which are not used in statements. - var variableNamesReadInside = new HashSet(ForEachInfo.Statements - .SelectMany(statement => ForEachInfo.SemanticModel.AnalyzeDataFlow(statement).ReadInside).Select(symbol => symbol.Name)); - var identifiersUsedInStatements = ForEachInfo.Identifiers - .Where(identifier => variableNamesReadInside.Contains(identifier.ValueText)); + // If there is a single statement and it is a block, leave it as is. + // Otherwise, wrap with a block. + var block = WrapWithBlockIfNecessary( + ForEachInfo.Statements.SelectAsArray(statement => statement.KeepCommentsAndAddElasticMarkers())); - // If there is a single statement and it is a block, leave it as is. - // Otherwise, wrap with a block. - var block = WrapWithBlockIfNecessary( - ForEachInfo.Statements.SelectAsArray(statement => statement.KeepCommentsAndAddElasticMarkers())); + editor.ReplaceNode( + ForEachInfo.ForEachStatement, + CreateDefaultReplacementStatement(identifiersUsedInStatements, block, convertToQuery) + .WithAdditionalAnnotations(Formatter.Annotation)); + } - editor.ReplaceNode( - ForEachInfo.ForEachStatement, - CreateDefaultReplacementStatement(identifiersUsedInStatements, block, convertToQuery) - .WithAdditionalAnnotations(Formatter.Annotation)); + private StatementSyntax CreateDefaultReplacementStatement( + IEnumerable identifiers, + BlockSyntax block, + bool convertToQuery) + { + var identifiersCount = identifiers.Count(); + if (identifiersCount == 0) + { + // Generate foreach(var _ ... select new {}) + return SyntaxFactory.ForEachStatement( + VarNameIdentifier, + SyntaxFactory.Identifier("_"), + CreateQueryExpressionOrLinqInvocation( + SyntaxFactory.AnonymousObjectCreationExpression(), + [], + [], + convertToQuery), + block); } - - private StatementSyntax CreateDefaultReplacementStatement( - IEnumerable identifiers, - BlockSyntax block, - bool convertToQuery) + else if (identifiersCount == 1) { - var identifiersCount = identifiers.Count(); - if (identifiersCount == 0) - { - // Generate foreach(var _ ... select new {}) - return SyntaxFactory.ForEachStatement( - VarNameIdentifier, - SyntaxFactory.Identifier("_"), - CreateQueryExpressionOrLinqInvocation( - SyntaxFactory.AnonymousObjectCreationExpression(), - [], - [], - convertToQuery), - block); - } - else if (identifiersCount == 1) - { - // Generate foreach(var singleIdentifier from ... select singleIdentifier) - return SyntaxFactory.ForEachStatement( - VarNameIdentifier, - identifiers.Single(), - CreateQueryExpressionOrLinqInvocation( - SyntaxFactory.IdentifierName(identifiers.Single()), - [], - [], - convertToQuery), - block); - } - else - { - var tupleForSelectExpression = SyntaxFactory.TupleExpression( - [.. identifiers.Select( - identifier => SyntaxFactory.Argument(SyntaxFactory.IdentifierName(identifier)))]); - var declaration = SyntaxFactory.DeclarationExpression( - VarNameIdentifier, - SyntaxFactory.ParenthesizedVariableDesignation( - [.. identifiers.Select(SyntaxFactory.SingleVariableDesignation)])); - - // Generate foreach(var (a,b) ... select (a, b)) - return SyntaxFactory.ForEachVariableStatement( - declaration, - CreateQueryExpressionOrLinqInvocation( - tupleForSelectExpression, - [], - [], - convertToQuery), - block); - } + // Generate foreach(var singleIdentifier from ... select singleIdentifier) + return SyntaxFactory.ForEachStatement( + VarNameIdentifier, + identifiers.Single(), + CreateQueryExpressionOrLinqInvocation( + SyntaxFactory.IdentifierName(identifiers.Single()), + [], + [], + convertToQuery), + block); } + else + { + var tupleForSelectExpression = SyntaxFactory.TupleExpression( + [.. identifiers.Select( + identifier => SyntaxFactory.Argument(SyntaxFactory.IdentifierName(identifier)))]); + var declaration = SyntaxFactory.DeclarationExpression( + VarNameIdentifier, + SyntaxFactory.ParenthesizedVariableDesignation( + [.. identifiers.Select(SyntaxFactory.SingleVariableDesignation)])); - private static BlockSyntax WrapWithBlockIfNecessary(ImmutableArray statements) - => statements is [BlockSyntax block] ? block : SyntaxFactory.Block(statements); + // Generate foreach(var (a,b) ... select (a, b)) + return SyntaxFactory.ForEachVariableStatement( + declaration, + CreateQueryExpressionOrLinqInvocation( + tupleForSelectExpression, + [], + [], + convertToQuery), + block); + } } + + private static BlockSyntax WrapWithBlockIfNecessary(ImmutableArray statements) + => statements is [BlockSyntax block] ? block : SyntaxFactory.Block(statements); } diff --git a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/ToCountConverter.cs b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/ToCountConverter.cs index f038eed427b6e..67eab9c2dbb7e 100644 --- a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/ToCountConverter.cs +++ b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/ToCountConverter.cs @@ -9,40 +9,39 @@ using Microsoft.CodeAnalysis.ConvertLinq.ConvertForEachToLinqQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery +namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery; + +/// +/// Provides a conversion to query.Count(). +/// +internal sealed class ToCountConverter( + ForEachInfo forEachInfo, + ExpressionSyntax selectExpression, + ExpressionSyntax modifyingExpression, + SyntaxTrivia[] trivia) : AbstractToMethodConverter(forEachInfo, selectExpression, modifyingExpression, trivia) { - /// - /// Provides a conversion to query.Count(). - /// - internal sealed class ToCountConverter( - ForEachInfo forEachInfo, - ExpressionSyntax selectExpression, - ExpressionSyntax modifyingExpression, - SyntaxTrivia[] trivia) : AbstractToMethodConverter(forEachInfo, selectExpression, modifyingExpression, trivia) - { - protected override string MethodName => nameof(Enumerable.Count); + protected override string MethodName => nameof(Enumerable.Count); - // Checks that the expression is "0". - protected override bool CanReplaceInitialization( - ExpressionSyntax expression, - CancellationToken cancellationToken) - => expression is LiteralExpressionSyntax literalExpression && literalExpression.Token.ValueText == "0"; + // Checks that the expression is "0". + protected override bool CanReplaceInitialization( + ExpressionSyntax expression, + CancellationToken cancellationToken) + => expression is LiteralExpressionSyntax literalExpression && literalExpression.Token.ValueText == "0"; - /// Input: - /// foreach(...) - /// { - /// ... - /// ... - /// counter++; - /// } - /// - /// Output: - /// counter += queryGenerated.Count(); - protected override StatementSyntax CreateDefaultStatement(ExpressionSyntax queryOrLinqInvocationExpression, ExpressionSyntax expression) - => SyntaxFactory.ExpressionStatement( - SyntaxFactory.AssignmentExpression( - SyntaxKind.AddAssignmentExpression, - expression, - CreateInvocationExpression(queryOrLinqInvocationExpression))); - } + /// Input: + /// foreach(...) + /// { + /// ... + /// ... + /// counter++; + /// } + /// + /// Output: + /// counter += queryGenerated.Count(); + protected override StatementSyntax CreateDefaultStatement(ExpressionSyntax queryOrLinqInvocationExpression, ExpressionSyntax expression) + => SyntaxFactory.ExpressionStatement( + SyntaxFactory.AssignmentExpression( + SyntaxKind.AddAssignmentExpression, + expression, + CreateInvocationExpression(queryOrLinqInvocationExpression))); } diff --git a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/ToToListConverter.cs b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/ToToListConverter.cs index d4d2c793a377d..5785807a26f2f 100644 --- a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/ToToListConverter.cs +++ b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/ToToListConverter.cs @@ -10,46 +10,45 @@ using Microsoft.CodeAnalysis.ConvertLinq.ConvertForEachToLinqQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery +namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery; + +/// +/// Provides a conversion to query.ToList(). +/// +internal sealed class ToToListConverter( + ForEachInfo forEachInfo, + ExpressionSyntax selectExpression, + ExpressionSyntax modifyingExpression, + SyntaxTrivia[] trivia) : AbstractToMethodConverter(forEachInfo, selectExpression, modifyingExpression, trivia) { - /// - /// Provides a conversion to query.ToList(). - /// - internal sealed class ToToListConverter( - ForEachInfo forEachInfo, - ExpressionSyntax selectExpression, - ExpressionSyntax modifyingExpression, - SyntaxTrivia[] trivia) : AbstractToMethodConverter(forEachInfo, selectExpression, modifyingExpression, trivia) - { - protected override string MethodName => nameof(Enumerable.ToList); + protected override string MethodName => nameof(Enumerable.ToList); - /// Checks that the expression is "new List();" - /// Exclude "new List(a);" and new List() { 1, 2, 3} - protected override bool CanReplaceInitialization( - ExpressionSyntax expression, CancellationToken cancellationToken) - => expression is ObjectCreationExpressionSyntax objectCreationExpression && - ForEachInfo.SemanticModel.GetSymbolInfo(objectCreationExpression.Type, cancellationToken).Symbol is ITypeSymbol typeSymbol && - CSharpConvertForEachToLinqQueryProvider.TypeSymbolIsList(typeSymbol, ForEachInfo.SemanticModel) && - (objectCreationExpression.ArgumentList == null || !objectCreationExpression.ArgumentList.Arguments.Any()) && - (objectCreationExpression.Initializer == null || !objectCreationExpression.Initializer.Expressions.Any()); + /// Checks that the expression is "new List();" + /// Exclude "new List(a);" and new List() { 1, 2, 3} + protected override bool CanReplaceInitialization( + ExpressionSyntax expression, CancellationToken cancellationToken) + => expression is ObjectCreationExpressionSyntax objectCreationExpression && + ForEachInfo.SemanticModel.GetSymbolInfo(objectCreationExpression.Type, cancellationToken).Symbol is ITypeSymbol typeSymbol && + CSharpConvertForEachToLinqQueryProvider.TypeSymbolIsList(typeSymbol, ForEachInfo.SemanticModel) && + (objectCreationExpression.ArgumentList == null || !objectCreationExpression.ArgumentList.Arguments.Any()) && + (objectCreationExpression.Initializer == null || !objectCreationExpression.Initializer.Expressions.Any()); - /// Input: - /// foreach(...) - /// { - /// ... - /// ... - /// list.Add(item); - /// } - /// - /// Output: - /// list.AddRange(queryGenerated); - protected override StatementSyntax CreateDefaultStatement(ExpressionSyntax queryOrLinqInvocationExpression, ExpressionSyntax expression) - => SyntaxFactory.ExpressionStatement( - SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - expression, - SyntaxFactory.IdentifierName(nameof(List.AddRange))), - SyntaxFactory.ArgumentList([SyntaxFactory.Argument(queryOrLinqInvocationExpression)]))); - } + /// Input: + /// foreach(...) + /// { + /// ... + /// ... + /// list.Add(item); + /// } + /// + /// Output: + /// list.AddRange(queryGenerated); + protected override StatementSyntax CreateDefaultStatement(ExpressionSyntax queryOrLinqInvocationExpression, ExpressionSyntax expression) + => SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + expression, + SyntaxFactory.IdentifierName(nameof(List.AddRange))), + SyntaxFactory.ArgumentList([SyntaxFactory.Argument(queryOrLinqInvocationExpression)]))); } diff --git a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/YieldReturnConverter.cs b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/YieldReturnConverter.cs index 50c3c75bc0a49..2a750315f0c79 100644 --- a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/YieldReturnConverter.cs +++ b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/YieldReturnConverter.cs @@ -10,38 +10,37 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; -namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery +namespace Microsoft.CodeAnalysis.CSharp.ConvertLinq.ConvertForEachToLinqQuery; + +internal sealed class YieldReturnConverter( + ForEachInfo forEachInfo, + YieldStatementSyntax yieldReturnStatement, + YieldStatementSyntax yieldBreakStatement) : AbstractConverter(forEachInfo) { - internal sealed class YieldReturnConverter( - ForEachInfo forEachInfo, - YieldStatementSyntax yieldReturnStatement, - YieldStatementSyntax yieldBreakStatement) : AbstractConverter(forEachInfo) - { - private readonly YieldStatementSyntax _yieldReturnStatement = yieldReturnStatement; - private readonly YieldStatementSyntax _yieldBreakStatement = yieldBreakStatement; + private readonly YieldStatementSyntax _yieldReturnStatement = yieldReturnStatement; + private readonly YieldStatementSyntax _yieldBreakStatement = yieldBreakStatement; - public override void Convert(SyntaxEditor editor, bool convertToQuery, CancellationToken cancellationToken) - { - var queryOrLinqInvocationExpression = CreateQueryExpressionOrLinqInvocation( - selectExpression: _yieldReturnStatement.Expression, - leadingTokensForSelect: [_yieldReturnStatement.YieldKeyword, _yieldReturnStatement.ReturnOrBreakKeyword], - trailingTokensForSelect: _yieldBreakStatement != null - ? [_yieldReturnStatement.SemicolonToken, - _yieldBreakStatement.YieldKeyword, - _yieldBreakStatement.ReturnOrBreakKeyword, - _yieldBreakStatement.SemicolonToken] - : [_yieldReturnStatement.SemicolonToken], - convertToQuery: convertToQuery); + public override void Convert(SyntaxEditor editor, bool convertToQuery, CancellationToken cancellationToken) + { + var queryOrLinqInvocationExpression = CreateQueryExpressionOrLinqInvocation( + selectExpression: _yieldReturnStatement.Expression, + leadingTokensForSelect: [_yieldReturnStatement.YieldKeyword, _yieldReturnStatement.ReturnOrBreakKeyword], + trailingTokensForSelect: _yieldBreakStatement != null + ? [_yieldReturnStatement.SemicolonToken, + _yieldBreakStatement.YieldKeyword, + _yieldBreakStatement.ReturnOrBreakKeyword, + _yieldBreakStatement.SemicolonToken] + : [_yieldReturnStatement.SemicolonToken], + convertToQuery: convertToQuery); - editor.ReplaceNode( - ForEachInfo.ForEachStatement, - SyntaxFactory.ReturnStatement(queryOrLinqInvocationExpression).WithAdditionalAnnotations(Formatter.Annotation)); + editor.ReplaceNode( + ForEachInfo.ForEachStatement, + SyntaxFactory.ReturnStatement(queryOrLinqInvocationExpression).WithAdditionalAnnotations(Formatter.Annotation)); - // Delete the yield break just after the loop. - if (_yieldBreakStatement != null) - { - editor.RemoveNode(_yieldBreakStatement); - } + // Delete the yield break just after the loop. + if (_yieldBreakStatement != null) + { + editor.RemoveNode(_yieldBreakStatement); } } } diff --git a/src/Features/CSharp/Portable/ConvertNamespace/ConvertNamespaceCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertNamespace/ConvertNamespaceCodeRefactoringProvider.cs index d7e77f96e2d49..347a1cd7ca079 100644 --- a/src/Features/CSharp/Portable/ConvertNamespace/ConvertNamespaceCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertNamespace/ConvertNamespaceCodeRefactoringProvider.cs @@ -18,95 +18,94 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace -{ - using static ConvertNamespaceAnalysis; - using static ConvertNamespaceTransform; +namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace; + +using static ConvertNamespaceAnalysis; +using static ConvertNamespaceTransform; - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertNamespace), Shared] - internal class ConvertNamespaceCodeRefactoringProvider : SyntaxEditorBasedCodeRefactoringProvider +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertNamespace), Shared] +internal class ConvertNamespaceCodeRefactoringProvider : SyntaxEditorBasedCodeRefactoringProvider +{ + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public ConvertNamespaceCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public ConvertNamespaceCodeRefactoringProvider() - { - } + } - protected override ImmutableArray SupportedFixAllScopes - => [FixAllScope.Project, FixAllScope.Solution]; + protected override ImmutableArray SupportedFixAllScopes + => [FixAllScope.Project, FixAllScope.Solution]; - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, span, cancellationToken) = context; - if (!span.IsEmpty) - return; - - var position = span.Start; - var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(position); - var namespaceDecl = token.GetAncestor(); - if (namespaceDecl == null) - return; - - if (!IsValidPosition(namespaceDecl, position)) - return; - - var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.Options, cancellationToken).ConfigureAwait(false); - if (!CanOfferRefactoring(namespaceDecl, root, options, out var info)) - return; - - context.RegisterRefactoring(CodeAction.Create( - info.Value.title, c => ConvertAsync(document, namespaceDecl, options.GetFormattingOptions(), c), info.Value.equivalenceKey)); - } + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, span, cancellationToken) = context; + if (!span.IsEmpty) + return; + + var position = span.Start; + var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(position); + var namespaceDecl = token.GetAncestor(); + if (namespaceDecl == null) + return; + + if (!IsValidPosition(namespaceDecl, position)) + return; + + var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.Options, cancellationToken).ConfigureAwait(false); + if (!CanOfferRefactoring(namespaceDecl, root, options, out var info)) + return; + + context.RegisterRefactoring(CodeAction.Create( + info.Value.title, c => ConvertAsync(document, namespaceDecl, options.GetFormattingOptions(), c), info.Value.equivalenceKey)); + } - private static bool CanOfferRefactoring( - [NotNullWhen(true)] BaseNamespaceDeclarationSyntax? namespaceDecl, - CompilationUnitSyntax root, - CSharpCodeFixOptionsProvider options, - [NotNullWhen(true)] out (string title, string equivalenceKey)? info) - { - info = - CanOfferUseBlockScoped(options.NamespaceDeclarations, namespaceDecl, forAnalyzer: false) ? GetInfo(NamespaceDeclarationPreference.BlockScoped) : - CanOfferUseFileScoped(options.NamespaceDeclarations, root, namespaceDecl, forAnalyzer: false) ? GetInfo(NamespaceDeclarationPreference.FileScoped) : - null; + private static bool CanOfferRefactoring( + [NotNullWhen(true)] BaseNamespaceDeclarationSyntax? namespaceDecl, + CompilationUnitSyntax root, + CSharpCodeFixOptionsProvider options, + [NotNullWhen(true)] out (string title, string equivalenceKey)? info) + { + info = + CanOfferUseBlockScoped(options.NamespaceDeclarations, namespaceDecl, forAnalyzer: false) ? GetInfo(NamespaceDeclarationPreference.BlockScoped) : + CanOfferUseFileScoped(options.NamespaceDeclarations, root, namespaceDecl, forAnalyzer: false) ? GetInfo(NamespaceDeclarationPreference.FileScoped) : + null; - return info != null; - } + return info != null; + } - private static bool IsValidPosition(BaseNamespaceDeclarationSyntax baseDeclaration, int position) - { - if (position < baseDeclaration.SpanStart) - return false; + private static bool IsValidPosition(BaseNamespaceDeclarationSyntax baseDeclaration, int position) + { + if (position < baseDeclaration.SpanStart) + return false; - if (baseDeclaration is FileScopedNamespaceDeclarationSyntax fileScopedNamespace) - return position <= fileScopedNamespace.SemicolonToken.Span.End; + if (baseDeclaration is FileScopedNamespaceDeclarationSyntax fileScopedNamespace) + return position <= fileScopedNamespace.SemicolonToken.Span.End; - if (baseDeclaration is NamespaceDeclarationSyntax namespaceDeclaration) - return position <= namespaceDeclaration.Name.Span.End; + if (baseDeclaration is NamespaceDeclarationSyntax namespaceDeclaration) + return position <= namespaceDeclaration.Name.Span.End; - throw ExceptionUtilities.UnexpectedValue(baseDeclaration.Kind()); - } + throw ExceptionUtilities.UnexpectedValue(baseDeclaration.Kind()); + } - protected override async Task FixAllAsync( - Document document, - ImmutableArray fixAllSpans, - SyntaxEditor editor, - CodeActionOptionsProvider optionsProvider, - string? equivalenceKey, - CancellationToken cancellationToken) + protected override async Task FixAllAsync( + Document document, + ImmutableArray fixAllSpans, + SyntaxEditor editor, + CodeActionOptionsProvider optionsProvider, + string? equivalenceKey, + CancellationToken cancellationToken) + { + var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var options = await document.GetCSharpCodeFixOptionsProviderAsync(optionsProvider, cancellationToken).ConfigureAwait(false); + var namespaceDecl = root.DescendantNodes().OfType().FirstOrDefault(); + if (!CanOfferRefactoring(namespaceDecl, root, options, out var info) + || info.Value.equivalenceKey != equivalenceKey) { - var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var options = await document.GetCSharpCodeFixOptionsProviderAsync(optionsProvider, cancellationToken).ConfigureAwait(false); - var namespaceDecl = root.DescendantNodes().OfType().FirstOrDefault(); - if (!CanOfferRefactoring(namespaceDecl, root, options, out var info) - || info.Value.equivalenceKey != equivalenceKey) - { - return; - } - - document = await ConvertAsync(document, namespaceDecl, options.GetFormattingOptions(), cancellationToken).ConfigureAwait(false); - var newRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(editor.OriginalRoot, newRoot); + return; } + + document = await ConvertAsync(document, namespaceDecl, options.GetFormattingOptions(), cancellationToken).ConfigureAwait(false); + var newRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(editor.OriginalRoot, newRoot); } } diff --git a/src/Features/CSharp/Portable/ConvertProgram/ConvertProgramTransform_ProgramMain.cs b/src/Features/CSharp/Portable/ConvertProgram/ConvertProgramTransform_ProgramMain.cs index 72359c7888b49..a9cf044833bc5 100644 --- a/src/Features/CSharp/Portable/ConvertProgram/ConvertProgramTransform_ProgramMain.cs +++ b/src/Features/CSharp/Portable/ConvertProgram/ConvertProgramTransform_ProgramMain.cs @@ -16,155 +16,154 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram -{ - using static SyntaxFactory; +namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram; + +using static SyntaxFactory; - internal static partial class ConvertProgramTransform +internal static partial class ConvertProgramTransform +{ + public static async Task ConvertToProgramMainAsync(Document document, AccessibilityModifiersRequired accessibilityModifiersRequired, CancellationToken cancellationToken) { - public static async Task ConvertToProgramMainAsync(Document document, AccessibilityModifiersRequired accessibilityModifiersRequired, CancellationToken cancellationToken) + // While the analyze and refactoring check ensure we're in a well formed state for their needs, the 'new + // template' code just calls directly into this if the user prefers Program.Main. So check and make sure + // this is actually something we can convert before proceeding. + var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root.IsTopLevelProgram()) { - // While the analyze and refactoring check ensure we're in a well formed state for their needs, the 'new - // template' code just calls directly into this if the user prefers Program.Main. So check and make sure - // this is actually something we can convert before proceeding. - var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (root.IsTopLevelProgram()) - { - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - - var mainMethod = compilation.GetTopLevelStatementsMethod(); - if (mainMethod is not null) - { - var oldClassDeclaration = root.Members.OfType().FirstOrDefault(IsProgramClass); + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var classDeclaration = await GenerateProgramClassAsync( - document, oldClassDeclaration, mainMethod, accessibilityModifiersRequired, cancellationToken).ConfigureAwait(false); + var mainMethod = compilation.GetTopLevelStatementsMethod(); + if (mainMethod is not null) + { + var oldClassDeclaration = root.Members.OfType().FirstOrDefault(IsProgramClass); - var newRoot = root.RemoveNodes(root.Members.OfType().Skip(1), SyntaxGenerator.DefaultRemoveOptions); - if (oldClassDeclaration is not null) - { - Contract.ThrowIfNull(newRoot); - newRoot = newRoot.RemoveNode(oldClassDeclaration, SyntaxGenerator.DefaultRemoveOptions); - } + var classDeclaration = await GenerateProgramClassAsync( + document, oldClassDeclaration, mainMethod, accessibilityModifiersRequired, cancellationToken).ConfigureAwait(false); + var newRoot = root.RemoveNodes(root.Members.OfType().Skip(1), SyntaxGenerator.DefaultRemoveOptions); + if (oldClassDeclaration is not null) + { Contract.ThrowIfNull(newRoot); + newRoot = newRoot.RemoveNode(oldClassDeclaration, SyntaxGenerator.DefaultRemoveOptions); + } - var firstGlobalStatement = newRoot.Members.OfType().Single(); - newRoot = newRoot.ReplaceNode(firstGlobalStatement, classDeclaration); + Contract.ThrowIfNull(newRoot); - return document.WithSyntaxRoot(newRoot); - } - } + var firstGlobalStatement = newRoot.Members.OfType().Single(); + newRoot = newRoot.ReplaceNode(firstGlobalStatement, classDeclaration); - return document; + return document.WithSyntaxRoot(newRoot); + } } - private static bool IsProgramClass(ClassDeclarationSyntax declaration) - { - return declaration.Identifier.ValueText == WellKnownMemberNames.TopLevelStatementsEntryPointTypeName && - declaration.Modifiers.Any(SyntaxKind.PartialKeyword); - } + return document; + } - private static async Task GenerateProgramClassAsync( - Document document, - ClassDeclarationSyntax? oldClassDeclaration, - IMethodSymbol mainMethod, - AccessibilityModifiersRequired accessibilityModifiersRequired, - CancellationToken cancellationToken) - { - var programType = mainMethod.ContainingType; + private static bool IsProgramClass(ClassDeclarationSyntax declaration) + { + return declaration.Identifier.ValueText == WellKnownMemberNames.TopLevelStatementsEntryPointTypeName && + declaration.Modifiers.Any(SyntaxKind.PartialKeyword); + } - // Respect user settings on if they want explicit or implicit accessibility modifiers. - var useDeclaredAccessibity = accessibilityModifiersRequired is AccessibilityModifiersRequired.ForNonInterfaceMembers or AccessibilityModifiersRequired.Always; + private static async Task GenerateProgramClassAsync( + Document document, + ClassDeclarationSyntax? oldClassDeclaration, + IMethodSymbol mainMethod, + AccessibilityModifiersRequired accessibilityModifiersRequired, + CancellationToken cancellationToken) + { + var programType = mainMethod.ContainingType; - var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var generator = document.GetRequiredLanguageService(); + // Respect user settings on if they want explicit or implicit accessibility modifiers. + var useDeclaredAccessibity = accessibilityModifiersRequired is AccessibilityModifiersRequired.ForNonInterfaceMembers or AccessibilityModifiersRequired.Always; - // See if we have an existing part in another file. If so, we'll have to generate our declaration as partial. - var hasExistingPart = programType.DeclaringSyntaxReferences.Any(static (d, cancellationToken) => d.GetSyntax(cancellationToken) is TypeDeclarationSyntax, cancellationToken); + var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var generator = document.GetRequiredLanguageService(); - var method = (MethodDeclarationSyntax)generator.MethodDeclaration( - mainMethod, WellKnownMemberNames.EntryPointMethodName, - GenerateProgramMainStatements(root, out var leadingTrivia)); - method = method.WithReturnType(method.ReturnType.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation)); - method = (MethodDeclarationSyntax)generator.WithAccessibility( - method, useDeclaredAccessibity ? mainMethod.DeclaredAccessibility : Accessibility.NotApplicable); + // See if we have an existing part in another file. If so, we'll have to generate our declaration as partial. + var hasExistingPart = programType.DeclaringSyntaxReferences.Any(static (d, cancellationToken) => d.GetSyntax(cancellationToken) is TypeDeclarationSyntax, cancellationToken); - // Workaround for simplification not being ready when we generate a new file. Substitute System.String[] - // with string[]. - if (method.ParameterList.Parameters is [{ Type: ArrayTypeSyntax arrayType }]) - method = method.ReplaceNode(arrayType.ElementType, PredefinedType(Token(SyntaxKind.StringKeyword))); + var method = (MethodDeclarationSyntax)generator.MethodDeclaration( + mainMethod, WellKnownMemberNames.EntryPointMethodName, + GenerateProgramMainStatements(root, out var leadingTrivia)); + method = method.WithReturnType(method.ReturnType.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation)); + method = (MethodDeclarationSyntax)generator.WithAccessibility( + method, useDeclaredAccessibity ? mainMethod.DeclaredAccessibility : Accessibility.NotApplicable); - if (oldClassDeclaration is null) - { - // If we dodn't have any suitable class declaration in the same file then generate it - return FixupComments((ClassDeclarationSyntax)generator.ClassDeclaration( - WellKnownMemberNames.TopLevelStatementsEntryPointTypeName, - accessibility: useDeclaredAccessibity ? programType.DeclaredAccessibility : Accessibility.NotApplicable, - modifiers: hasExistingPart ? DeclarationModifiers.Partial : DeclarationModifiers.None, - members: new[] { method }).WithLeadingTrivia(leadingTrivia)); - } - else - { - // Otherwise just add new member and process leading trivia + // Workaround for simplification not being ready when we generate a new file. Substitute System.String[] + // with string[]. + if (method.ParameterList.Parameters is [{ Type: ArrayTypeSyntax arrayType }]) + method = method.ReplaceNode(arrayType.ElementType, PredefinedType(Token(SyntaxKind.StringKeyword))); - // Old class declaration is below top-level statements and is probably separated from them with a blank line (or several ones). - // So we want to remove all leading line to make class declaration begin from the first line of the file after applying refactoring - var oldTriviaWithoutBlankLines = oldClassDeclaration.GetLeadingTrivia().WithoutLeadingBlankLines(); - return oldClassDeclaration.WithMembers(oldClassDeclaration.Members.Add(method)) - .WithLeadingTrivia(oldTriviaWithoutBlankLines.Union(leadingTrivia)); - } + if (oldClassDeclaration is null) + { + // If we dodn't have any suitable class declaration in the same file then generate it + return FixupComments((ClassDeclarationSyntax)generator.ClassDeclaration( + WellKnownMemberNames.TopLevelStatementsEntryPointTypeName, + accessibility: useDeclaredAccessibity ? programType.DeclaredAccessibility : Accessibility.NotApplicable, + modifiers: hasExistingPart ? DeclarationModifiers.Partial : DeclarationModifiers.None, + members: new[] { method }).WithLeadingTrivia(leadingTrivia)); } - - private static ImmutableArray GenerateProgramMainStatements( - CompilationUnitSyntax root, out SyntaxTriviaList triviaToMove) + else { - using var _ = ArrayBuilder.GetInstance(out var statements); + // Otherwise just add new member and process leading trivia + + // Old class declaration is below top-level statements and is probably separated from them with a blank line (or several ones). + // So we want to remove all leading line to make class declaration begin from the first line of the file after applying refactoring + var oldTriviaWithoutBlankLines = oldClassDeclaration.GetLeadingTrivia().WithoutLeadingBlankLines(); + return oldClassDeclaration.WithMembers(oldClassDeclaration.Members.Add(method)) + .WithLeadingTrivia(oldTriviaWithoutBlankLines.Union(leadingTrivia)); + } + } - triviaToMove = default; - var first = true; - foreach (var globalStatement in root.Members.OfType()) + private static ImmutableArray GenerateProgramMainStatements( + CompilationUnitSyntax root, out SyntaxTriviaList triviaToMove) + { + using var _ = ArrayBuilder.GetInstance(out var statements); + + triviaToMove = default; + var first = true; + foreach (var globalStatement in root.Members.OfType()) + { + // Remove leading trivia from first statement. We'll move it to the Program type. Any directly attached + // comments though stay attached to the first statement. + var statement = globalStatement.Statement.WithAdditionalAnnotations(Formatter.Annotation); + if (first) { - // Remove leading trivia from first statement. We'll move it to the Program type. Any directly attached - // comments though stay attached to the first statement. - var statement = globalStatement.Statement.WithAdditionalAnnotations(Formatter.Annotation); - if (first) - { - first = false; + first = false; - triviaToMove = statement.GetLeadingTrivia(); - while (triviaToMove is [.., SyntaxTrivia(SyntaxKind.SingleLineCommentTrivia), SyntaxTrivia(SyntaxKind.EndOfLineTrivia)]) - triviaToMove = [.. triviaToMove.Take(triviaToMove.Count - 2)]; + triviaToMove = statement.GetLeadingTrivia(); + while (triviaToMove is [.., SyntaxTrivia(SyntaxKind.SingleLineCommentTrivia), SyntaxTrivia(SyntaxKind.EndOfLineTrivia)]) + triviaToMove = [.. triviaToMove.Take(triviaToMove.Count - 2)]; - var commentsToPreserve = TriviaList(statement.GetLeadingTrivia().Skip(triviaToMove.Count)); - statements.Add(FixupComments(statement.WithLeadingTrivia(commentsToPreserve))); - } - else - { - statements.Add(statement); - } + var commentsToPreserve = TriviaList(statement.GetLeadingTrivia().Skip(triviaToMove.Count)); + statements.Add(FixupComments(statement.WithLeadingTrivia(commentsToPreserve))); + } + else + { + statements.Add(statement); } - - return statements.ToImmutable(); } - private static TSyntaxNode FixupComments(TSyntaxNode node) where TSyntaxNode : SyntaxNode - { - // Remove comment explaining top level statements as it isn't relevant if the user switches back to full - // Program.Main form. - var leadingTrivia = node.GetLeadingTrivia(); - var comment = leadingTrivia.FirstOrNull( - c => c.Kind() is SyntaxKind.SingleLineCommentTrivia && c.ToString().Contains("https://aka.ms/new-console-template")); - if (comment == null) - return node; - - var commentIndex = leadingTrivia.IndexOf(comment.Value); - leadingTrivia = leadingTrivia.RemoveAt(commentIndex); + return statements.ToImmutable(); + } - while (commentIndex < leadingTrivia.Count && leadingTrivia[commentIndex].Kind() is SyntaxKind.EndOfLineTrivia) - leadingTrivia = leadingTrivia.RemoveAt(commentIndex); + private static TSyntaxNode FixupComments(TSyntaxNode node) where TSyntaxNode : SyntaxNode + { + // Remove comment explaining top level statements as it isn't relevant if the user switches back to full + // Program.Main form. + var leadingTrivia = node.GetLeadingTrivia(); + var comment = leadingTrivia.FirstOrNull( + c => c.Kind() is SyntaxKind.SingleLineCommentTrivia && c.ToString().Contains("https://aka.ms/new-console-template")); + if (comment == null) + return node; + + var commentIndex = leadingTrivia.IndexOf(comment.Value); + leadingTrivia = leadingTrivia.RemoveAt(commentIndex); + + while (commentIndex < leadingTrivia.Count && leadingTrivia[commentIndex].Kind() is SyntaxKind.EndOfLineTrivia) + leadingTrivia = leadingTrivia.RemoveAt(commentIndex); - return node.WithLeadingTrivia(leadingTrivia); - } + return node.WithLeadingTrivia(leadingTrivia); } } diff --git a/src/Features/CSharp/Portable/ConvertProgram/ConvertProgramTransform_TopLevelStatements.cs b/src/Features/CSharp/Portable/ConvertProgram/ConvertProgramTransform_TopLevelStatements.cs index 906e6de6e40c2..1e313a61b1226 100644 --- a/src/Features/CSharp/Portable/ConvertProgram/ConvertProgramTransform_TopLevelStatements.cs +++ b/src/Features/CSharp/Portable/ConvertProgram/ConvertProgramTransform_TopLevelStatements.cs @@ -20,244 +20,243 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram -{ - using static SyntaxFactory; +namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram; - internal static partial class ConvertProgramTransform - { - public static async Task ConvertToTopLevelStatementsAsync( - Document document, MethodDeclarationSyntax methodDeclaration, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var typeDeclaration = (TypeDeclarationSyntax?)methodDeclaration.Parent; - Contract.ThrowIfNull(typeDeclaration); // checked by analyzer +using static SyntaxFactory; - var generator = document.GetRequiredLanguageService(); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); +internal static partial class ConvertProgramTransform +{ + public static async Task ConvertToTopLevelStatementsAsync( + Document document, MethodDeclarationSyntax methodDeclaration, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var typeDeclaration = (TypeDeclarationSyntax?)methodDeclaration.Parent; + Contract.ThrowIfNull(typeDeclaration); // checked by analyzer - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var rootWithGlobalStatements = GetRootWithGlobalStatements( - semanticModel, generator, root, typeDeclaration, methodDeclaration, cancellationToken); + var generator = document.GetRequiredLanguageService(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // simple case. we were in a top level type to begin with. Nothing we need to do now. - if (typeDeclaration.Parent is not BaseNamespaceDeclarationSyntax namespaceDeclaration) - return document.WithSyntaxRoot(rootWithGlobalStatements); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var rootWithGlobalStatements = GetRootWithGlobalStatements( + semanticModel, generator, root, typeDeclaration, methodDeclaration, cancellationToken); - // We were parented by a namespace. Add using statements to bring in all the symbols that were - // previously visible within the namespace. Then remove any that we don't need once we've done that. - var cleanupOptions = await document.GetCodeCleanupOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + // simple case. we were in a top level type to begin with. Nothing we need to do now. + if (typeDeclaration.Parent is not BaseNamespaceDeclarationSyntax namespaceDeclaration) + return document.WithSyntaxRoot(rootWithGlobalStatements); - document = await AddUsingDirectivesAsync( - document, rootWithGlobalStatements, namespaceDeclaration, cleanupOptions, cancellationToken).ConfigureAwait(false); + // We were parented by a namespace. Add using statements to bring in all the symbols that were + // previously visible within the namespace. Then remove any that we don't need once we've done that. + var cleanupOptions = await document.GetCodeCleanupOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - // if we have a file scoped namespace after converting to top-level-statements, then convert it to a - // block-namespace. Top level statements and file-scoped-namespaces are not allowed together. - document = await ConvertFileScopedNamespaceAsync(document, cleanupOptions, cancellationToken).ConfigureAwait(false); + document = await AddUsingDirectivesAsync( + document, rootWithGlobalStatements, namespaceDeclaration, cleanupOptions, cancellationToken).ConfigureAwait(false); - return document; - } + // if we have a file scoped namespace after converting to top-level-statements, then convert it to a + // block-namespace. Top level statements and file-scoped-namespaces are not allowed together. + document = await ConvertFileScopedNamespaceAsync(document, cleanupOptions, cancellationToken).ConfigureAwait(false); - private static async Task ConvertFileScopedNamespaceAsync(Document document, CodeCleanupOptions cleanupOptions, CancellationToken cancellationToken) - { - var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - return root.Members.OfType().FirstOrDefault() is { } fileScopedNamespace - ? await ConvertNamespaceTransform.ConvertFileScopedNamespaceAsync(document, fileScopedNamespace, (CSharpSyntaxFormattingOptions)cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false) - : document; - } + return document; + } - private static async Task AddUsingDirectivesAsync( - Document document, SyntaxNode root, BaseNamespaceDeclarationSyntax namespaceDeclaration, CodeCleanupOptions options, CancellationToken cancellationToken) - { - var addImportsService = document.GetRequiredLanguageService(); - var removeImportsService = document.GetRequiredLanguageService(); + private static async Task ConvertFileScopedNamespaceAsync(Document document, CodeCleanupOptions cleanupOptions, CancellationToken cancellationToken) + { + var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + return root.Members.OfType().FirstOrDefault() is { } fileScopedNamespace + ? await ConvertNamespaceTransform.ConvertFileScopedNamespaceAsync(document, fileScopedNamespace, (CSharpSyntaxFormattingOptions)cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false) + : document; + } - var annotation = new SyntaxAnnotation(); - using var _ = ArrayBuilder.GetInstance(out var directives); - AddUsingDirectives(namespaceDeclaration.Name, annotation, directives); + private static async Task AddUsingDirectivesAsync( + Document document, SyntaxNode root, BaseNamespaceDeclarationSyntax namespaceDeclaration, CodeCleanupOptions options, CancellationToken cancellationToken) + { + var addImportsService = document.GetRequiredLanguageService(); + var removeImportsService = document.GetRequiredLanguageService(); - var generator = document.GetRequiredLanguageService(); + var annotation = new SyntaxAnnotation(); + using var _ = ArrayBuilder.GetInstance(out var directives); + AddUsingDirectives(namespaceDeclaration.Name, annotation, directives); - var documentWithImportsAdded = document.WithSyntaxRoot(addImportsService.AddImports( - compilation: null!, root, contextLocation: null, directives, generator, - options.AddImportOptions, - cancellationToken)); + var generator = document.GetRequiredLanguageService(); - return await removeImportsService.RemoveUnnecessaryImportsAsync( - documentWithImportsAdded, n => n.HasAnnotation(annotation), options.FormattingOptions, cancellationToken).ConfigureAwait(false); - } + var documentWithImportsAdded = document.WithSyntaxRoot(addImportsService.AddImports( + compilation: null!, root, contextLocation: null, directives, generator, + options.AddImportOptions, + cancellationToken)); - private static void AddUsingDirectives(NameSyntax name, SyntaxAnnotation annotation, ArrayBuilder directives) - { - if (name is QualifiedNameSyntax qualifiedName) - AddUsingDirectives(qualifiedName.Left, annotation, directives); + return await removeImportsService.RemoveUnnecessaryImportsAsync( + documentWithImportsAdded, n => n.HasAnnotation(annotation), options.FormattingOptions, cancellationToken).ConfigureAwait(false); + } - directives.Add(UsingDirective(name).WithAdditionalAnnotations(annotation)); - } + private static void AddUsingDirectives(NameSyntax name, SyntaxAnnotation annotation, ArrayBuilder directives) + { + if (name is QualifiedNameSyntax qualifiedName) + AddUsingDirectives(qualifiedName.Left, annotation, directives); - private static SyntaxNode GetRootWithGlobalStatements( - SemanticModel semanticModel, - SyntaxGenerator generator, - SyntaxNode root, - TypeDeclarationSyntax typeDeclaration, - MethodDeclarationSyntax methodDeclaration, - CancellationToken cancellationToken) - { - var editor = new SyntaxEditor(root, generator); - var globalStatements = GetGlobalStatements( - semanticModel, typeDeclaration, methodDeclaration, cancellationToken); + directives.Add(UsingDirective(name).WithAdditionalAnnotations(annotation)); + } - var namespaceDeclaration = typeDeclaration.Parent as BaseNamespaceDeclarationSyntax; - if (namespaceDeclaration != null && - namespaceDeclaration.Members.Count >= 2) - { - // Our parent namespace has another symbol in it. Keep the namespace declaration around, removing only - // the existing Program type from it. - editor.RemoveNode(typeDeclaration); - editor.ReplaceNode( - root, - (current, _) => - { - var currentRoot = (CompilationUnitSyntax)current; - return currentRoot.WithMembers(currentRoot.Members.InsertRange(0, globalStatements)); - }); - } - else if (namespaceDeclaration != null) - { - // we had a parent namespace, but we were the only thing in it. We can just remove the namespace entirely. + private static SyntaxNode GetRootWithGlobalStatements( + SemanticModel semanticModel, + SyntaxGenerator generator, + SyntaxNode root, + TypeDeclarationSyntax typeDeclaration, + MethodDeclarationSyntax methodDeclaration, + CancellationToken cancellationToken) + { + var editor = new SyntaxEditor(root, generator); + var globalStatements = GetGlobalStatements( + semanticModel, typeDeclaration, methodDeclaration, cancellationToken); - // If there was a file banner on the namespace, move it to the first statement. - var fileBanner = root.GetFirstToken() == namespaceDeclaration.GetFirstToken() - ? CSharpFileBannerFacts.Instance.GetFileBanner(root) - : default; - if (!fileBanner.IsDefaultOrEmpty && globalStatements.Length > 0) + var namespaceDeclaration = typeDeclaration.Parent as BaseNamespaceDeclarationSyntax; + if (namespaceDeclaration != null && + namespaceDeclaration.Members.Count >= 2) + { + // Our parent namespace has another symbol in it. Keep the namespace declaration around, removing only + // the existing Program type from it. + editor.RemoveNode(typeDeclaration); + editor.ReplaceNode( + root, + (current, _) => { - globalStatements = globalStatements.Replace( - globalStatements[0], - globalStatements[0].WithPrependedLeadingTrivia(fileBanner)); - } - - editor.ReplaceNode( - root, - root.ReplaceNode(namespaceDeclaration, globalStatements)); - } - else + var currentRoot = (CompilationUnitSyntax)current; + return currentRoot.WithMembers(currentRoot.Members.InsertRange(0, globalStatements)); + }); + } + else if (namespaceDeclaration != null) + { + // we had a parent namespace, but we were the only thing in it. We can just remove the namespace entirely. + + // If there was a file banner on the namespace, move it to the first statement. + var fileBanner = root.GetFirstToken() == namespaceDeclaration.GetFirstToken() + ? CSharpFileBannerFacts.Instance.GetFileBanner(root) + : default; + if (!fileBanner.IsDefaultOrEmpty && globalStatements.Length > 0) { - // type wasn't in a namespace. just remove the type and replace it with the new global statements. - editor.ReplaceNode( - root, root.ReplaceNode(typeDeclaration, globalStatements)); + globalStatements = globalStatements.Replace( + globalStatements[0], + globalStatements[0].WithPrependedLeadingTrivia(fileBanner)); } - return editor.GetChangedRoot(); + editor.ReplaceNode( + root, + root.ReplaceNode(namespaceDeclaration, globalStatements)); } - - private static ImmutableArray GetGlobalStatements( - SemanticModel semanticModel, - TypeDeclarationSyntax typeDeclaration, - MethodDeclarationSyntax methodDeclaration, - CancellationToken cancellationToken) + else { - using var _ = ArrayBuilder.GetInstance(out var statements); + // type wasn't in a namespace. just remove the type and replace it with the new global statements. + editor.ReplaceNode( + root, root.ReplaceNode(typeDeclaration, globalStatements)); + } - // First, process all fields and convert them to locals. We do this first as the locals need to be declared - // first in order for the main-method statements to reference them. + return editor.GetChangedRoot(); + } + + private static ImmutableArray GetGlobalStatements( + SemanticModel semanticModel, + TypeDeclarationSyntax typeDeclaration, + MethodDeclarationSyntax methodDeclaration, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var statements); + + // First, process all fields and convert them to locals. We do this first as the locals need to be declared + // first in order for the main-method statements to reference them. - foreach (var member in typeDeclaration.Members) + foreach (var member in typeDeclaration.Members) + { + // hit another member, must be a field/method. + if (member is FieldDeclarationSyntax fieldDeclaration) { - // hit another member, must be a field/method. - if (member is FieldDeclarationSyntax fieldDeclaration) - { - // Convert fields into local statements - statements.Add(LocalDeclarationStatement( - ConvertDeclaration(semanticModel, fieldDeclaration.Declaration, cancellationToken)) - .WithSemicolonToken(fieldDeclaration.SemicolonToken) - .WithTriviaFrom(fieldDeclaration)); - } + // Convert fields into local statements + statements.Add(LocalDeclarationStatement( + ConvertDeclaration(semanticModel, fieldDeclaration.Declaration, cancellationToken)) + .WithSemicolonToken(fieldDeclaration.SemicolonToken) + .WithTriviaFrom(fieldDeclaration)); } + } - // Then convert all remaining methods to local functions (except for 'Main', which becomes the global - // statements of the top-level program). - foreach (var member in typeDeclaration.Members) + // Then convert all remaining methods to local functions (except for 'Main', which becomes the global + // statements of the top-level program). + foreach (var member in typeDeclaration.Members) + { + if (member == methodDeclaration) { - if (member == methodDeclaration) - { - // when we hit the 'Main' method, then actually take all its nested statements and elevate them to - // top-level statements. - Contract.ThrowIfNull(methodDeclaration.Body); // checked by analyzer + // when we hit the 'Main' method, then actually take all its nested statements and elevate them to + // top-level statements. + Contract.ThrowIfNull(methodDeclaration.Body); // checked by analyzer - // move comments on the method to be on it's first statement. - if (methodDeclaration.Body.Statements.Count > 0) - statements.AddRange(methodDeclaration.Body.Statements[0].WithPrependedLeadingTrivia(methodDeclaration.GetLeadingTrivia())); + // move comments on the method to be on it's first statement. + if (methodDeclaration.Body.Statements.Count > 0) + statements.AddRange(methodDeclaration.Body.Statements[0].WithPrependedLeadingTrivia(methodDeclaration.GetLeadingTrivia())); - statements.AddRange(methodDeclaration.Body.Statements.Skip(1)); - } - else if (member is MethodDeclarationSyntax otherMethod) - { - // convert methods to local functions. - statements.Add(LocalFunctionStatement( - attributeLists: default, - modifiers: [.. otherMethod.Modifiers.Where(m => m.Kind() is SyntaxKind.AsyncKeyword or SyntaxKind.UnsafeKeyword)], - returnType: otherMethod.ReturnType, - identifier: otherMethod.Identifier, - typeParameterList: otherMethod.TypeParameterList, - parameterList: otherMethod.ParameterList, - constraintClauses: otherMethod.ConstraintClauses, - body: otherMethod.Body, - expressionBody: otherMethod.ExpressionBody).WithLeadingTrivia(otherMethod.GetLeadingTrivia())); - } - else if (member is not FieldDeclarationSyntax) - { - // checked by analyzer - throw ExceptionUtilities.Unreachable(); - } + statements.AddRange(methodDeclaration.Body.Statements.Skip(1)); } - - // Move the trivia on the type itself to the first statement we create. - if (statements.Count > 0) + else if (member is MethodDeclarationSyntax otherMethod) { - statements[0] = statements[0].WithPrependedLeadingTrivia(typeDeclaration.GetLeadingTrivia()); - - // If our first statement doesn't have any preceding newlines, then also attempt to take any whitespace - // on the namespace we're contained in. That way we have enough spaces between the first statement and - // any using directives. - if (!statements[0].GetLeadingTrivia().Any(t => t.Kind() is SyntaxKind.EndOfLineTrivia) && - typeDeclaration.Parent is NamespaceDeclarationSyntax namespaceDeclaration) - { - statements[0] = statements[0].WithPrependedLeadingTrivia( - namespaceDeclaration.GetLeadingTrivia().TakeWhile(t => t.Kind() is SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia)); - } + // convert methods to local functions. + statements.Add(LocalFunctionStatement( + attributeLists: default, + modifiers: [.. otherMethod.Modifiers.Where(m => m.Kind() is SyntaxKind.AsyncKeyword or SyntaxKind.UnsafeKeyword)], + returnType: otherMethod.ReturnType, + identifier: otherMethod.Identifier, + typeParameterList: otherMethod.TypeParameterList, + parameterList: otherMethod.ParameterList, + constraintClauses: otherMethod.ConstraintClauses, + body: otherMethod.Body, + expressionBody: otherMethod.ExpressionBody).WithLeadingTrivia(otherMethod.GetLeadingTrivia())); + } + else if (member is not FieldDeclarationSyntax) + { + // checked by analyzer + throw ExceptionUtilities.Unreachable(); } - - using var _1 = ArrayBuilder.GetInstance(out var globalStatements); - foreach (var statement in statements) - globalStatements.Add(GlobalStatement(statement).WithAdditionalAnnotations(Formatter.Annotation)); - - return globalStatements.ToImmutable(); } - private static VariableDeclarationSyntax ConvertDeclaration( - SemanticModel semanticModel, - VariableDeclarationSyntax declaration, - CancellationToken cancellationToken) + // Move the trivia on the type itself to the first statement we create. + if (statements.Count > 0) { - return declaration.ReplaceNodes( - declaration.Variables, - (v, _) => ConvertVariable(semanticModel, v, cancellationToken)); - } + statements[0] = statements[0].WithPrependedLeadingTrivia(typeDeclaration.GetLeadingTrivia()); - private static VariableDeclaratorSyntax ConvertVariable( - SemanticModel semanticModel, - VariableDeclaratorSyntax variable, - CancellationToken cancellationToken) - { - // If the field does not have an initialize, then generate it with one as locals do not get initialized by - // default like fields do. - if (variable.Initializer != null) - return variable; - - var field = (IFieldSymbol?)semanticModel.GetDeclaredSymbol(variable, cancellationToken); - Contract.ThrowIfNull(field); - return variable.WithInitializer(EqualsValueClause( - (ExpressionSyntax)CSharpSyntaxGenerator.Instance.DefaultExpression(field.Type))); + // If our first statement doesn't have any preceding newlines, then also attempt to take any whitespace + // on the namespace we're contained in. That way we have enough spaces between the first statement and + // any using directives. + if (!statements[0].GetLeadingTrivia().Any(t => t.Kind() is SyntaxKind.EndOfLineTrivia) && + typeDeclaration.Parent is NamespaceDeclarationSyntax namespaceDeclaration) + { + statements[0] = statements[0].WithPrependedLeadingTrivia( + namespaceDeclaration.GetLeadingTrivia().TakeWhile(t => t.Kind() is SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia)); + } } + + using var _1 = ArrayBuilder.GetInstance(out var globalStatements); + foreach (var statement in statements) + globalStatements.Add(GlobalStatement(statement).WithAdditionalAnnotations(Formatter.Annotation)); + + return globalStatements.ToImmutable(); + } + + private static VariableDeclarationSyntax ConvertDeclaration( + SemanticModel semanticModel, + VariableDeclarationSyntax declaration, + CancellationToken cancellationToken) + { + return declaration.ReplaceNodes( + declaration.Variables, + (v, _) => ConvertVariable(semanticModel, v, cancellationToken)); + } + + private static VariableDeclaratorSyntax ConvertVariable( + SemanticModel semanticModel, + VariableDeclaratorSyntax variable, + CancellationToken cancellationToken) + { + // If the field does not have an initialize, then generate it with one as locals do not get initialized by + // default like fields do. + if (variable.Initializer != null) + return variable; + + var field = (IFieldSymbol?)semanticModel.GetDeclaredSymbol(variable, cancellationToken); + Contract.ThrowIfNull(field); + return variable.WithInitializer(EqualsValueClause( + (ExpressionSyntax)CSharpSyntaxGenerator.Instance.DefaultExpression(field.Type))); } } diff --git a/src/Features/CSharp/Portable/ConvertProgram/ConvertToProgramMainCodeFixProvider.cs b/src/Features/CSharp/Portable/ConvertProgram/ConvertToProgramMainCodeFixProvider.cs index cfad18811794c..a73447f1cd54f 100644 --- a/src/Features/CSharp/Portable/ConvertProgram/ConvertToProgramMainCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/ConvertProgram/ConvertToProgramMainCodeFixProvider.cs @@ -15,43 +15,42 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram +namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram; + +using static ConvertProgramTransform; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertToProgramMain), Shared] +internal class ConvertToProgramMainCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - using static ConvertProgramTransform; + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConvertToProgramMainCodeFixProvider() + { + } + + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseProgramMainId]; - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertToProgramMain), Shared] - internal class ConvertToProgramMainCodeFixProvider : SyntaxEditorBasedCodeFixProvider + public override async Task RegisterCodeFixesAsync(CodeFixContext context) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConvertToProgramMainCodeFixProvider() - { - } - - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseProgramMainId]; - - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var cancellationToken = context.CancellationToken; - - var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.Options, cancellationToken).ConfigureAwait(false); - var priority = options.PreferTopLevelStatements.Notification.Severity == ReportDiagnostic.Hidden - ? CodeActionPriority.Low - : CodeActionPriority.Default; - - RegisterCodeFix(context, CSharpAnalyzersResources.Convert_to_Program_Main_style_program, nameof(ConvertToProgramMainCodeFixProvider), priority); - } - - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var fixedDocument = await ConvertToProgramMainAsync(document, options.AccessibilityModifiersRequired, cancellationToken).ConfigureAwait(false); - var fixedRoot = await fixedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - editor.ReplaceNode(editor.OriginalRoot, fixedRoot); - } + var document = context.Document; + var cancellationToken = context.CancellationToken; + + var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.Options, cancellationToken).ConfigureAwait(false); + var priority = options.PreferTopLevelStatements.Notification.Severity == ReportDiagnostic.Hidden + ? CodeActionPriority.Low + : CodeActionPriority.Default; + + RegisterCodeFix(context, CSharpAnalyzersResources.Convert_to_Program_Main_style_program, nameof(ConvertToProgramMainCodeFixProvider), priority); + } + + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var options = await document.GetCodeFixOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var fixedDocument = await ConvertToProgramMainAsync(document, options.AccessibilityModifiersRequired, cancellationToken).ConfigureAwait(false); + var fixedRoot = await fixedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + editor.ReplaceNode(editor.OriginalRoot, fixedRoot); } } diff --git a/src/Features/CSharp/Portable/ConvertProgram/ConvertToProgramMainCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertProgram/ConvertToProgramMainCodeRefactoringProvider.cs index 2054578938ce7..6c7d5c7e871d2 100644 --- a/src/Features/CSharp/Portable/ConvertProgram/ConvertToProgramMainCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertProgram/ConvertToProgramMainCodeRefactoringProvider.cs @@ -16,50 +16,49 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram -{ - using static ConvertProgramAnalysis; - using static ConvertProgramTransform; +namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram; + +using static ConvertProgramAnalysis; +using static ConvertProgramTransform; - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertToProgramMain), Shared] - internal class ConvertToProgramMainCodeRefactoringProvider : CodeRefactoringProvider +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertToProgramMain), Shared] +internal class ConvertToProgramMainCodeRefactoringProvider : CodeRefactoringProvider +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConvertToProgramMainCodeRefactoringProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConvertToProgramMainCodeRefactoringProvider() - { - } + } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, span, cancellationToken) = context; - if (!span.IsEmpty) - return; + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, span, cancellationToken) = context; + if (!span.IsEmpty) + return; - if (!IsApplication(document.Project.CompilationOptions!)) - return; + if (!IsApplication(document.Project.CompilationOptions!)) + return; - var position = span.Start; - var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var position = span.Start; + var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (!root.IsTopLevelProgram()) - return; + if (!root.IsTopLevelProgram()) + return; - var acceptableLocation = GetUseProgramMainDiagnosticLocation(root, isHidden: true); - if (!acceptableLocation.SourceSpan.IntersectsWith(position)) - return; + var acceptableLocation = GetUseProgramMainDiagnosticLocation(root, isHidden: true); + if (!acceptableLocation.SourceSpan.IntersectsWith(position)) + return; - var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.Options, cancellationToken).ConfigureAwait(false); + var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.Options, cancellationToken).ConfigureAwait(false); - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - if (!CanOfferUseProgramMain(options.PreferTopLevelStatements, root, compilation, forAnalyzer: false)) - return; + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + if (!CanOfferUseProgramMain(options.PreferTopLevelStatements, root, compilation, forAnalyzer: false)) + return; - context.RegisterRefactoring(CodeAction.Create( - CSharpAnalyzersResources.Convert_to_Program_Main_style_program, - c => ConvertToProgramMainAsync(document, options.AccessibilityModifiersRequired.Value, c), - nameof(CSharpAnalyzersResources.Convert_to_Program_Main_style_program), - CodeActionPriority.Low)); - } + context.RegisterRefactoring(CodeAction.Create( + CSharpAnalyzersResources.Convert_to_Program_Main_style_program, + c => ConvertToProgramMainAsync(document, options.AccessibilityModifiersRequired.Value, c), + nameof(CSharpAnalyzersResources.Convert_to_Program_Main_style_program), + CodeActionPriority.Low)); } } diff --git a/src/Features/CSharp/Portable/ConvertProgram/ConvertToTopLevelStatementsCodeFixProvider.cs b/src/Features/CSharp/Portable/ConvertProgram/ConvertToTopLevelStatementsCodeFixProvider.cs index d5a67c583703a..e345b6349b6cc 100644 --- a/src/Features/CSharp/Portable/ConvertProgram/ConvertToTopLevelStatementsCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/ConvertProgram/ConvertToTopLevelStatementsCodeFixProvider.cs @@ -17,44 +17,43 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram +namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram; + +using static ConvertProgramTransform; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertToTopLevelStatements), Shared] +internal class ConvertToTopLevelStatementsCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - using static ConvertProgramTransform; + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConvertToTopLevelStatementsCodeFixProvider() + { + } + + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseTopLevelStatementsId]; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var cancellationToken = context.CancellationToken; + + var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.Options, cancellationToken).ConfigureAwait(false); + var priority = options.PreferTopLevelStatements.Notification.Severity == ReportDiagnostic.Hidden + ? CodeActionPriority.Low + : CodeActionPriority.Default; + + RegisterCodeFix(context, CSharpAnalyzersResources.Convert_to_top_level_statements, nameof(ConvertToTopLevelStatementsCodeFixProvider), priority); + } - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ConvertToTopLevelStatements), Shared] - internal class ConvertToTopLevelStatementsCodeFixProvider : SyntaxEditorBasedCodeFixProvider + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConvertToTopLevelStatementsCodeFixProvider() - { - } - - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.UseTopLevelStatementsId]; - - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var cancellationToken = context.CancellationToken; - - var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.Options, cancellationToken).ConfigureAwait(false); - var priority = options.PreferTopLevelStatements.Notification.Severity == ReportDiagnostic.Hidden - ? CodeActionPriority.Low - : CodeActionPriority.Default; - - RegisterCodeFix(context, CSharpAnalyzersResources.Convert_to_top_level_statements, nameof(ConvertToTopLevelStatementsCodeFixProvider), priority); - } - - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var methodDeclaration = (MethodDeclarationSyntax)diagnostics[0].AdditionalLocations[0].FindNode(cancellationToken); - - var newDocument = await ConvertToTopLevelStatementsAsync(document, methodDeclaration, fallbackOptions, cancellationToken).ConfigureAwait(false); - var newRoot = await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - editor.ReplaceNode(editor.OriginalRoot, newRoot); - } + var methodDeclaration = (MethodDeclarationSyntax)diagnostics[0].AdditionalLocations[0].FindNode(cancellationToken); + + var newDocument = await ConvertToTopLevelStatementsAsync(document, methodDeclaration, fallbackOptions, cancellationToken).ConfigureAwait(false); + var newRoot = await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + editor.ReplaceNode(editor.OriginalRoot, newRoot); } } diff --git a/src/Features/CSharp/Portable/ConvertProgram/ConvertToTopLevelStatementsCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertProgram/ConvertToTopLevelStatementsCodeRefactoringProvider.cs index 3ef8dc17b3256..8a7d7442b3989 100644 --- a/src/Features/CSharp/Portable/ConvertProgram/ConvertToTopLevelStatementsCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertProgram/ConvertToTopLevelStatementsCodeRefactoringProvider.cs @@ -16,53 +16,52 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram +namespace Microsoft.CodeAnalysis.CSharp.ConvertProgram; + +using static ConvertProgramAnalysis; +using static ConvertProgramTransform; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertToTopLevelStatements), Shared] +internal class ConvertToTopLevelStatementsCodeRefactoringProvider : CodeRefactoringProvider { - using static ConvertProgramAnalysis; - using static ConvertProgramTransform; + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConvertToTopLevelStatementsCodeRefactoringProvider() + { + } - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertToTopLevelStatements), Shared] - internal class ConvertToTopLevelStatementsCodeRefactoringProvider : CodeRefactoringProvider + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConvertToTopLevelStatementsCodeRefactoringProvider() - { - } + var (document, span, cancellationToken) = context; - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + // can only suggest moving to top level statement on c# 9 or above. + if (document.Project.ParseOptions!.LanguageVersion() < LanguageVersion.CSharp9 || + !IsApplication(document.Project.CompilationOptions!)) { - var (document, span, cancellationToken) = context; - - // can only suggest moving to top level statement on c# 9 or above. - if (document.Project.ParseOptions!.LanguageVersion() < LanguageVersion.CSharp9 || - !IsApplication(document.Project.CompilationOptions!)) - { - return; - } - - var methodDeclaration = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (methodDeclaration is null) - return; + return; + } - var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.Options, cancellationToken).ConfigureAwait(false); - if (!CanOfferUseTopLevelStatements(options.PreferTopLevelStatements, forAnalyzer: false)) - return; + var methodDeclaration = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (methodDeclaration is null) + return; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var compilation = semanticModel.Compilation; + var options = await document.GetCSharpCodeFixOptionsProviderAsync(context.Options, cancellationToken).ConfigureAwait(false); + if (!CanOfferUseTopLevelStatements(options.PreferTopLevelStatements, forAnalyzer: false)) + return; - if (!IsProgramMainMethod(semanticModel, methodDeclaration, GetMainTypeName(compilation), cancellationToken, out var canConvert) || - !canConvert) - { - return; - } + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var compilation = semanticModel.Compilation; - context.RegisterRefactoring(CodeAction.Create( - CSharpAnalyzersResources.Convert_to_top_level_statements, - c => ConvertToTopLevelStatementsAsync(document, methodDeclaration, context.Options, c), - nameof(CSharpAnalyzersResources.Convert_to_top_level_statements), - CodeActionPriority.Low)); + if (!IsProgramMainMethod(semanticModel, methodDeclaration, GetMainTypeName(compilation), cancellationToken, out var canConvert) || + !canConvert) + { + return; } + + context.RegisterRefactoring(CodeAction.Create( + CSharpAnalyzersResources.Convert_to_top_level_statements, + c => ConvertToTopLevelStatementsAsync(document, methodDeclaration, context.Options, c), + nameof(CSharpAnalyzersResources.Convert_to_top_level_statements), + CodeActionPriority.Low)); } } diff --git a/src/Features/CSharp/Portable/ConvertToInterpolatedString/CSharpConvertPlaceholderToInterpolatedStringRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertToInterpolatedString/CSharpConvertPlaceholderToInterpolatedStringRefactoringProvider.cs index 5f338c8e78a46..4f144ea273af8 100644 --- a/src/Features/CSharp/Portable/ConvertToInterpolatedString/CSharpConvertPlaceholderToInterpolatedStringRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertToInterpolatedString/CSharpConvertPlaceholderToInterpolatedStringRefactoringProvider.cs @@ -8,26 +8,25 @@ using Microsoft.CodeAnalysis.ConvertToInterpolatedString; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.ConvertToInterpolatedString +namespace Microsoft.CodeAnalysis.CSharp.ConvertToInterpolatedString; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertPlaceholderToInterpolatedString), Shared] +internal partial class CSharpConvertPlaceholderToInterpolatedStringRefactoringProvider : + AbstractConvertPlaceholderToInterpolatedStringRefactoringProvider< + ExpressionSyntax, + LiteralExpressionSyntax, + InvocationExpressionSyntax, + InterpolatedStringExpressionSyntax, + ArgumentSyntax, + ArgumentListSyntax, + InterpolationSyntax> { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertPlaceholderToInterpolatedString), Shared] - internal partial class CSharpConvertPlaceholderToInterpolatedStringRefactoringProvider : - AbstractConvertPlaceholderToInterpolatedStringRefactoringProvider< - ExpressionSyntax, - LiteralExpressionSyntax, - InvocationExpressionSyntax, - InterpolatedStringExpressionSyntax, - ArgumentSyntax, - ArgumentListSyntax, - InterpolationSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpConvertPlaceholderToInterpolatedStringRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertPlaceholderToInterpolatedStringRefactoringProvider() - { - } - - protected override ExpressionSyntax ParseExpression(string text) - => SyntaxFactory.ParseExpression(text); } + + protected override ExpressionSyntax ParseExpression(string text) + => SyntaxFactory.ParseExpression(text); } diff --git a/src/Features/CSharp/Portable/ConvertToRawString/ConvertRegularStringToRawStringCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertToRawString/ConvertRegularStringToRawStringCodeRefactoringProvider.cs index 57260cc01560b..e1aa97a5a601b 100644 --- a/src/Features/CSharp/Portable/ConvertToRawString/ConvertRegularStringToRawStringCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertToRawString/ConvertRegularStringToRawStringCodeRefactoringProvider.cs @@ -16,359 +16,358 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ConvertToRawString +namespace Microsoft.CodeAnalysis.CSharp.ConvertToRawString; + +using static ConvertToRawStringHelpers; +using static SyntaxFactory; + +internal partial class ConvertRegularStringToRawStringProvider + : AbstractConvertStringProvider { - using static ConvertToRawStringHelpers; - using static SyntaxFactory; + public static readonly IConvertStringProvider Instance = new ConvertRegularStringToRawStringProvider(); - internal partial class ConvertRegularStringToRawStringProvider - : AbstractConvertStringProvider + private ConvertRegularStringToRawStringProvider() { - public static readonly IConvertStringProvider Instance = new ConvertRegularStringToRawStringProvider(); + } - private ConvertRegularStringToRawStringProvider() - { - } + protected override bool CheckSyntax(LiteralExpressionSyntax stringExpression) + => stringExpression.Kind() is SyntaxKind.StringLiteralExpression; - protected override bool CheckSyntax(LiteralExpressionSyntax stringExpression) - => stringExpression.Kind() is SyntaxKind.StringLiteralExpression; + protected override bool CanConvert( + ParsedDocument document, + LiteralExpressionSyntax stringExpression, + SyntaxFormattingOptions formattingOptions, + out CanConvertParams convertParams, + CancellationToken cancellationToken) + { + return CanConvertStringLiteral(stringExpression.Token, out convertParams); + } - protected override bool CanConvert( - ParsedDocument document, - LiteralExpressionSyntax stringExpression, - SyntaxFormattingOptions formattingOptions, - out CanConvertParams convertParams, - CancellationToken cancellationToken) - { - return CanConvertStringLiteral(stringExpression.Token, out convertParams); - } + private static bool CanConvertStringLiteral(SyntaxToken token, out CanConvertParams convertParams) + { + convertParams = default; + if (token.Kind() != SyntaxKind.StringLiteralToken) + return false; + + // Can't convert a string literal in a directive to a raw string. + if (IsInDirective(token.Parent)) + return false; + + var characters = CSharpVirtualCharService.Instance.TryConvertToVirtualChars(token); + + if (!ConvertToRawStringHelpers.CanConvert(characters)) + return false; - private static bool CanConvertStringLiteral(SyntaxToken token, out CanConvertParams convertParams) + // TODO(cyrusn): Should we offer this on empty strings... seems undesirable as you'd end with a gigantic + // three line alternative over just "" + if (characters.IsEmpty) + return false; + + var canBeSingleLine = CanBeSingleLine(characters); + var canBeMultiLineWithoutLeadingWhiteSpaces = false; + if (!canBeSingleLine) { - convertParams = default; - if (token.Kind() != SyntaxKind.StringLiteralToken) - return false; + // Users sometimes write verbatim string literals with a extra starting newline (or indentation) purely + // for aesthetic reasons. For example: + // + // var v = @" + // SELECT column1, column2, ... + // FROM table_name"; + // + // Converting this directly to a raw string will produce: + // + // var v = """ + // + // SELECT column1, column2, ... + // FROM table_name"; + // """ + // + // Check for this and offer instead to generate: + // + // var v = """ + // SELECT column1, column2, ... + // FROM table_name"; + // """ + // + // This changes the contents of the literal, but that can be fine for the domain the user is working in. + // Offer this, but let the user know that this will change runtime semantics. + canBeMultiLineWithoutLeadingWhiteSpaces = token.IsVerbatimStringLiteral() && + (HasLeadingWhitespace(characters) || HasTrailingWhitespace(characters)) && + CleanupWhitespace(characters).Length > 0; + } - // Can't convert a string literal in a directive to a raw string. - if (IsInDirective(token.Parent)) - return false; + // If we have escaped quotes in the string, then this is a good option to bubble up as something to convert + // to a raw string. Otherwise, still offer this refactoring, but at low priority as the user may be + // invoking this on lots of strings that they have no interest in converting. + var priority = AllEscapesAreQuotes(characters) ? CodeActionPriority.Default : CodeActionPriority.Low; - var characters = CSharpVirtualCharService.Instance.TryConvertToVirtualChars(token); + convertParams = new CanConvertParams(priority, canBeSingleLine, canBeMultiLineWithoutLeadingWhiteSpaces); + return true; - if (!ConvertToRawStringHelpers.CanConvert(characters)) - return false; + static bool HasLeadingWhitespace(VirtualCharSequence characters) + { + var index = 0; + while (index < characters.Length && IsCSharpWhitespace(characters[index])) + index++; - // TODO(cyrusn): Should we offer this on empty strings... seems undesirable as you'd end with a gigantic - // three line alternative over just "" - if (characters.IsEmpty) - return false; + return index < characters.Length && IsCSharpNewLine(characters[index]); + } - var canBeSingleLine = CanBeSingleLine(characters); - var canBeMultiLineWithoutLeadingWhiteSpaces = false; - if (!canBeSingleLine) - { - // Users sometimes write verbatim string literals with a extra starting newline (or indentation) purely - // for aesthetic reasons. For example: - // - // var v = @" - // SELECT column1, column2, ... - // FROM table_name"; - // - // Converting this directly to a raw string will produce: - // - // var v = """ - // - // SELECT column1, column2, ... - // FROM table_name"; - // """ - // - // Check for this and offer instead to generate: - // - // var v = """ - // SELECT column1, column2, ... - // FROM table_name"; - // """ - // - // This changes the contents of the literal, but that can be fine for the domain the user is working in. - // Offer this, but let the user know that this will change runtime semantics. - canBeMultiLineWithoutLeadingWhiteSpaces = token.IsVerbatimStringLiteral() && - (HasLeadingWhitespace(characters) || HasTrailingWhitespace(characters)) && - CleanupWhitespace(characters).Length > 0; - } + static bool HasTrailingWhitespace(VirtualCharSequence characters) + { + var index = characters.Length - 1; + while (index >= 0 && IsCSharpWhitespace(characters[index])) + index--; - // If we have escaped quotes in the string, then this is a good option to bubble up as something to convert - // to a raw string. Otherwise, still offer this refactoring, but at low priority as the user may be - // invoking this on lots of strings that they have no interest in converting. - var priority = AllEscapesAreQuotes(characters) ? CodeActionPriority.Default : CodeActionPriority.Low; + return index >= 0 && IsCSharpNewLine(characters[index]); + } + } - convertParams = new CanConvertParams(priority, canBeSingleLine, canBeMultiLineWithoutLeadingWhiteSpaces); - return true; + protected override LiteralExpressionSyntax Convert( + ParsedDocument document, + LiteralExpressionSyntax stringExpression, + ConvertToRawKind kind, + SyntaxFormattingOptions options, + CancellationToken cancellationToken) + { + var newToken = GetReplacementToken( + document, stringExpression.Token, kind, options, cancellationToken); + return stringExpression.WithToken(newToken); + } - static bool HasLeadingWhitespace(VirtualCharSequence characters) - { - var index = 0; - while (index < characters.Length && IsCSharpWhitespace(characters[index])) - index++; + private static SyntaxToken GetReplacementToken( + ParsedDocument parsedDocument, + SyntaxToken token, + ConvertToRawKind kind, + SyntaxFormattingOptions formattingOptions, + CancellationToken cancellationToken) + { + var characters = CSharpVirtualCharService.Instance.TryConvertToVirtualChars(token); + Contract.ThrowIfTrue(characters.IsDefaultOrEmpty); - return index < characters.Length && IsCSharpNewLine(characters[index]); - } + if (kind == ConvertToRawKind.SingleLine) + return ConvertToSingleLineRawString(); - static bool HasTrailingWhitespace(VirtualCharSequence characters) - { - var index = characters.Length - 1; - while (index >= 0 && IsCSharpWhitespace(characters[index])) - index--; + var indentationOptions = new IndentationOptions(formattingOptions); - return index >= 0 && IsCSharpNewLine(characters[index]); - } + var tokenLine = parsedDocument.Text.Lines.GetLineFromPosition(token.SpanStart); + if (token.SpanStart == tokenLine.Start) + { + // Special case. string token starting at the start of the line. This is a common pattern used for + // multi-line strings that don't want any indentation and have the start/end of the string at the same + // level (like unit tests). + // + // In this case, figure out what indentation we're normally like to put this string. Update *both* the + // contents *and* the starting quotes of the raw string. + var indenter = parsedDocument.LanguageServices.GetRequiredService(); + var indentationVal = indenter.GetIndentation(parsedDocument, tokenLine.LineNumber, indentationOptions, cancellationToken); + + var indentation = indentationVal.GetIndentationString(parsedDocument.Text, indentationOptions); + var newToken = ConvertToMultiLineRawIndentedString(indentation); + + newToken = newToken.WithLeadingTrivia(newToken.LeadingTrivia.Add(Whitespace(indentation))); + return newToken; } - - protected override LiteralExpressionSyntax Convert( - ParsedDocument document, - LiteralExpressionSyntax stringExpression, - ConvertToRawKind kind, - SyntaxFormattingOptions options, - CancellationToken cancellationToken) + else { - var newToken = GetReplacementToken( - document, stringExpression.Token, kind, options, cancellationToken); - return stringExpression.WithToken(newToken); + // otherwise this was a string literal on a line that already contains contents. Or it's a string + // literal on its own line, but indented some amount. Figure out the indentation of the contents from + // this, but leave the string literal starting at whatever position it's at. + var indentation = token.GetPreferredIndentation(parsedDocument, indentationOptions, cancellationToken); + return ConvertToMultiLineRawIndentedString(indentation); } - private static SyntaxToken GetReplacementToken( - ParsedDocument parsedDocument, - SyntaxToken token, - ConvertToRawKind kind, - SyntaxFormattingOptions formattingOptions, - CancellationToken cancellationToken) + SyntaxToken ConvertToSingleLineRawString() { - var characters = CSharpVirtualCharService.Instance.TryConvertToVirtualChars(token); - Contract.ThrowIfTrue(characters.IsDefaultOrEmpty); + // Have to make sure we have a delimiter longer than any quote sequence in the string. + var longestQuoteSequence = GetLongestQuoteSequence(characters); + var quoteDelimiterCount = Math.Max(3, longestQuoteSequence + 1); - if (kind == ConvertToRawKind.SingleLine) - return ConvertToSingleLineRawString(); + using var _ = PooledStringBuilder.GetInstance(out var builder); - var indentationOptions = new IndentationOptions(formattingOptions); + builder.Append('"', quoteDelimiterCount); - var tokenLine = parsedDocument.Text.Lines.GetLineFromPosition(token.SpanStart); - if (token.SpanStart == tokenLine.Start) - { - // Special case. string token starting at the start of the line. This is a common pattern used for - // multi-line strings that don't want any indentation and have the start/end of the string at the same - // level (like unit tests). - // - // In this case, figure out what indentation we're normally like to put this string. Update *both* the - // contents *and* the starting quotes of the raw string. - var indenter = parsedDocument.LanguageServices.GetRequiredService(); - var indentationVal = indenter.GetIndentation(parsedDocument, tokenLine.LineNumber, indentationOptions, cancellationToken); - - var indentation = indentationVal.GetIndentationString(parsedDocument.Text, indentationOptions); - var newToken = ConvertToMultiLineRawIndentedString(indentation); - - newToken = newToken.WithLeadingTrivia(newToken.LeadingTrivia.Add(Whitespace(indentation))); - return newToken; - } - else - { - // otherwise this was a string literal on a line that already contains contents. Or it's a string - // literal on its own line, but indented some amount. Figure out the indentation of the contents from - // this, but leave the string literal starting at whatever position it's at. - var indentation = token.GetPreferredIndentation(parsedDocument, indentationOptions, cancellationToken); - return ConvertToMultiLineRawIndentedString(indentation); - } + foreach (var ch in characters) + ch.AppendTo(builder); - SyntaxToken ConvertToSingleLineRawString() - { - // Have to make sure we have a delimiter longer than any quote sequence in the string. - var longestQuoteSequence = GetLongestQuoteSequence(characters); - var quoteDelimiterCount = Math.Max(3, longestQuoteSequence + 1); + builder.Append('"', quoteDelimiterCount); - using var _ = PooledStringBuilder.GetInstance(out var builder); + return Token( + token.LeadingTrivia, + SyntaxKind.SingleLineRawStringLiteralToken, + builder.ToString(), + characters.CreateString(), + token.TrailingTrivia); + } - builder.Append('"', quoteDelimiterCount); + SyntaxToken ConvertToMultiLineRawIndentedString(string indentation) + { + // If the user asked to remove whitespace then do so now. + if (kind == ConvertToRawKind.MultiLineWithoutLeadingWhitespace) + characters = CleanupWhitespace(characters); - foreach (var ch in characters) - ch.AppendTo(builder); + // Have to make sure we have a delimiter longer than any quote sequence in the string. + var longestQuoteSequence = GetLongestQuoteSequence(characters); + var quoteDelimiterCount = Math.Max(3, longestQuoteSequence + 1); - builder.Append('"', quoteDelimiterCount); + using var _ = PooledStringBuilder.GetInstance(out var builder); - return Token( - token.LeadingTrivia, - SyntaxKind.SingleLineRawStringLiteralToken, - builder.ToString(), - characters.CreateString(), - token.TrailingTrivia); - } + builder.Append('"', quoteDelimiterCount); + builder.Append(formattingOptions.NewLine); - SyntaxToken ConvertToMultiLineRawIndentedString(string indentation) + var atStartOfLine = true; + for (int i = 0, n = characters.Length; i < n; i++) { - // If the user asked to remove whitespace then do so now. - if (kind == ConvertToRawKind.MultiLineWithoutLeadingWhitespace) - characters = CleanupWhitespace(characters); - - // Have to make sure we have a delimiter longer than any quote sequence in the string. - var longestQuoteSequence = GetLongestQuoteSequence(characters); - var quoteDelimiterCount = Math.Max(3, longestQuoteSequence + 1); - - using var _ = PooledStringBuilder.GetInstance(out var builder); - - builder.Append('"', quoteDelimiterCount); - builder.Append(formattingOptions.NewLine); - - var atStartOfLine = true; - for (int i = 0, n = characters.Length; i < n; i++) + var ch = characters[i]; + if (IsCSharpNewLine(ch)) { - var ch = characters[i]; - if (IsCSharpNewLine(ch)) - { - ch.AppendTo(builder); - atStartOfLine = true; - continue; - } - - if (atStartOfLine) - { - builder.Append(indentation); - atStartOfLine = false; - } - ch.AppendTo(builder); + atStartOfLine = true; + continue; } - builder.Append(formattingOptions.NewLine); - builder.Append(indentation); - builder.Append('"', quoteDelimiterCount); + if (atStartOfLine) + { + builder.Append(indentation); + atStartOfLine = false; + } - return Token( - token.LeadingTrivia, - SyntaxKind.MultiLineRawStringLiteralToken, - builder.ToString(), - characters.CreateString(), - token.TrailingTrivia); + ch.AppendTo(builder); } + + builder.Append(formattingOptions.NewLine); + builder.Append(indentation); + builder.Append('"', quoteDelimiterCount); + + return Token( + token.LeadingTrivia, + SyntaxKind.MultiLineRawStringLiteralToken, + builder.ToString(), + characters.CreateString(), + token.TrailingTrivia); } + } - private static VirtualCharSequence CleanupWhitespace(VirtualCharSequence characters) - { - using var _ = ArrayBuilder.GetInstance(out var lines); + private static VirtualCharSequence CleanupWhitespace(VirtualCharSequence characters) + { + using var _ = ArrayBuilder.GetInstance(out var lines); - // First, determine all the lines in the content. - BreakIntoLines(characters, lines); + // First, determine all the lines in the content. + BreakIntoLines(characters, lines); - // Remove the leading and trailing line if they are all whitespace. - while (lines.Count > 0 && AllWhitespace(lines.First())) - lines.RemoveAt(0); + // Remove the leading and trailing line if they are all whitespace. + while (lines.Count > 0 && AllWhitespace(lines.First())) + lines.RemoveAt(0); - while (lines.Count > 0 && AllWhitespace(lines.Last())) - lines.RemoveAt(lines.Count - 1); + while (lines.Count > 0 && AllWhitespace(lines.Last())) + lines.RemoveAt(lines.Count - 1); - if (lines.Count == 0) - return VirtualCharSequence.Empty; + if (lines.Count == 0) + return VirtualCharSequence.Empty; - // Use the remaining lines to figure out what common whitespace we have. - var commonWhitespacePrefix = ComputeCommonWhitespacePrefix(lines); + // Use the remaining lines to figure out what common whitespace we have. + var commonWhitespacePrefix = ComputeCommonWhitespacePrefix(lines); - var result = ImmutableSegmentedList.CreateBuilder(); + var result = ImmutableSegmentedList.CreateBuilder(); - foreach (var line in lines) + foreach (var line in lines) + { + if (AllWhitespace(line)) { - if (AllWhitespace(line)) - { - // For an all-whitespace line, just add the trailing newlines on the line (if present). - AddRange(result, line.SkipWhile(IsCSharpWhitespace)); - } - else - { - // Normal line. Skip the common whitespace. - AddRange(result, line.Skip(commonWhitespacePrefix)); - } + // For an all-whitespace line, just add the trailing newlines on the line (if present). + AddRange(result, line.SkipWhile(IsCSharpWhitespace)); + } + else + { + // Normal line. Skip the common whitespace. + AddRange(result, line.Skip(commonWhitespacePrefix)); } - - // Remove all trailing whitespace and newlines from the final string. - while (result.Count > 0 && (IsCSharpNewLine(result[^1]) || IsCSharpWhitespace(result[^1]))) - result.RemoveAt(result.Count - 1); - - return VirtualCharSequence.Create(result.ToImmutable()); } - private static void AddRange(ImmutableSegmentedList.Builder result, VirtualCharSequence sequence) - { - foreach (var c in sequence) - result.Add(c); - } + // Remove all trailing whitespace and newlines from the final string. + while (result.Count > 0 && (IsCSharpNewLine(result[^1]) || IsCSharpWhitespace(result[^1]))) + result.RemoveAt(result.Count - 1); - private static int ComputeCommonWhitespacePrefix(ArrayBuilder lines) - { - var commonLeadingWhitespace = GetLeadingWhitespace(lines.First()); + return VirtualCharSequence.Create(result.ToImmutable()); + } - for (var i = 1; i < lines.Count; i++) - { - if (commonLeadingWhitespace.IsEmpty) - return 0; + private static void AddRange(ImmutableSegmentedList.Builder result, VirtualCharSequence sequence) + { + foreach (var c in sequence) + result.Add(c); + } - var currentLine = lines[i]; - if (AllWhitespace(currentLine)) - continue; + private static int ComputeCommonWhitespacePrefix(ArrayBuilder lines) + { + var commonLeadingWhitespace = GetLeadingWhitespace(lines.First()); - var currentLineLeadingWhitespace = GetLeadingWhitespace(currentLine); - commonLeadingWhitespace = ComputeCommonWhitespacePrefix(commonLeadingWhitespace, currentLineLeadingWhitespace); - } + for (var i = 1; i < lines.Count; i++) + { + if (commonLeadingWhitespace.IsEmpty) + return 0; - return commonLeadingWhitespace.Length; + var currentLine = lines[i]; + if (AllWhitespace(currentLine)) + continue; + + var currentLineLeadingWhitespace = GetLeadingWhitespace(currentLine); + commonLeadingWhitespace = ComputeCommonWhitespacePrefix(commonLeadingWhitespace, currentLineLeadingWhitespace); } - private static VirtualCharSequence ComputeCommonWhitespacePrefix( - VirtualCharSequence leadingWhitespace1, VirtualCharSequence leadingWhitespace2) - { - var length = Math.Min(leadingWhitespace1.Length, leadingWhitespace2.Length); + return commonLeadingWhitespace.Length; + } - var current = 0; - while (current < length && IsCSharpWhitespace(leadingWhitespace1[current]) && leadingWhitespace1[current].Rune == leadingWhitespace2[current].Rune) - current++; + private static VirtualCharSequence ComputeCommonWhitespacePrefix( + VirtualCharSequence leadingWhitespace1, VirtualCharSequence leadingWhitespace2) + { + var length = Math.Min(leadingWhitespace1.Length, leadingWhitespace2.Length); - return leadingWhitespace1.GetSubSequence(TextSpan.FromBounds(0, current)); - } + var current = 0; + while (current < length && IsCSharpWhitespace(leadingWhitespace1[current]) && leadingWhitespace1[current].Rune == leadingWhitespace2[current].Rune) + current++; - private static VirtualCharSequence GetLeadingWhitespace(VirtualCharSequence line) - { - var current = 0; - while (current < line.Length && IsCSharpWhitespace(line[current])) - current++; + return leadingWhitespace1.GetSubSequence(TextSpan.FromBounds(0, current)); + } - return line.GetSubSequence(TextSpan.FromBounds(0, current)); - } + private static VirtualCharSequence GetLeadingWhitespace(VirtualCharSequence line) + { + var current = 0; + while (current < line.Length && IsCSharpWhitespace(line[current])) + current++; - private static void BreakIntoLines(VirtualCharSequence characters, ArrayBuilder lines) - { - var index = 0; + return line.GetSubSequence(TextSpan.FromBounds(0, current)); + } - while (index < characters.Length) - lines.Add(GetNextLine(characters, ref index)); - } + private static void BreakIntoLines(VirtualCharSequence characters, ArrayBuilder lines) + { + var index = 0; - private static VirtualCharSequence GetNextLine( - VirtualCharSequence characters, - ref int index) - { - var end = index; - while (end < characters.Length && !IsCSharpNewLine(characters[end])) - end++; + while (index < characters.Length) + lines.Add(GetNextLine(characters, ref index)); + } - if (end != characters.Length) - end += IsCarriageReturnNewLine(characters, end) ? 2 : 1; + private static VirtualCharSequence GetNextLine( + VirtualCharSequence characters, + ref int index) + { + var end = index; + while (end < characters.Length && !IsCSharpNewLine(characters[end])) + end++; - var result = characters.GetSubSequence(TextSpan.FromBounds(index, end)); - index = end; - return result; - } + if (end != characters.Length) + end += IsCarriageReturnNewLine(characters, end) ? 2 : 1; - private static bool AllWhitespace(VirtualCharSequence line) - { - var index = 0; - while (index < line.Length && IsCSharpWhitespace(line[index])) - index++; + var result = characters.GetSubSequence(TextSpan.FromBounds(index, end)); + index = end; + return result; + } - return index == line.Length || IsCSharpNewLine(line[index]); - } + private static bool AllWhitespace(VirtualCharSequence line) + { + var index = 0; + while (index < line.Length && IsCSharpWhitespace(line[index])) + index++; + + return index == line.Length || IsCSharpNewLine(line[index]); } } diff --git a/src/Features/CSharp/Portable/ConvertTupleToStruct/CSharpConvertTupleToStructCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertTupleToStruct/CSharpConvertTupleToStructCodeRefactoringProvider.cs index 4a1b51c0d860b..f5f8300383464 100644 --- a/src/Features/CSharp/Portable/ConvertTupleToStruct/CSharpConvertTupleToStructCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertTupleToStruct/CSharpConvertTupleToStructCodeRefactoringProvider.cs @@ -8,42 +8,41 @@ using Microsoft.CodeAnalysis.ConvertTupleToStruct; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.ConvertTupleToStruct +namespace Microsoft.CodeAnalysis.CSharp.ConvertTupleToStruct; + +[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] +[ExportLanguageService(typeof(IConvertTupleToStructCodeRefactoringProvider), LanguageNames.CSharp)] +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertTupleToStruct), Shared] +internal class CSharpConvertTupleToStructCodeRefactoringProvider : + AbstractConvertTupleToStructCodeRefactoringProvider< + ExpressionSyntax, + NameSyntax, + IdentifierNameSyntax, + LiteralExpressionSyntax, + ObjectCreationExpressionSyntax, + TupleExpressionSyntax, + ArgumentSyntax, + TupleTypeSyntax, + TypeDeclarationSyntax, + BaseNamespaceDeclarationSyntax> { - [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] - [ExportLanguageService(typeof(IConvertTupleToStructCodeRefactoringProvider), LanguageNames.CSharp)] - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertTupleToStruct), Shared] - internal class CSharpConvertTupleToStructCodeRefactoringProvider : - AbstractConvertTupleToStructCodeRefactoringProvider< - ExpressionSyntax, - NameSyntax, - IdentifierNameSyntax, - LiteralExpressionSyntax, - ObjectCreationExpressionSyntax, - TupleExpressionSyntax, - ArgumentSyntax, - TupleTypeSyntax, - TypeDeclarationSyntax, - BaseNamespaceDeclarationSyntax> + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpConvertTupleToStructCodeRefactoringProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpConvertTupleToStructCodeRefactoringProvider() - { - } + } - protected override ArgumentSyntax GetArgumentWithChangedName(ArgumentSyntax argument, string name) - => argument.WithNameColon(ChangeName(argument.NameColon, name)); + protected override ArgumentSyntax GetArgumentWithChangedName(ArgumentSyntax argument, string name) + => argument.WithNameColon(ChangeName(argument.NameColon, name)); - private static NameColonSyntax? ChangeName(NameColonSyntax? nameColon, string name) + private static NameColonSyntax? ChangeName(NameColonSyntax? nameColon, string name) + { + if (nameColon == null) { - if (nameColon == null) - { - return null; - } - - var newName = SyntaxFactory.IdentifierName(name).WithTriviaFrom(nameColon.Name); - return nameColon.WithName(newName); + return null; } + + var newName = SyntaxFactory.IdentifierName(name).WithTriviaFrom(nameColon.Name); + return nameColon.WithName(newName); } } diff --git a/src/Features/CSharp/Portable/Debugging/BreakpointResolver.cs b/src/Features/CSharp/Portable/Debugging/BreakpointResolver.cs index bb611e27f0267..bbe8ec72f150a 100644 --- a/src/Features/CSharp/Portable/Debugging/BreakpointResolver.cs +++ b/src/Features/CSharp/Portable/Debugging/BreakpointResolver.cs @@ -15,85 +15,84 @@ using Microsoft.CodeAnalysis.Debugging; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Debugging +namespace Microsoft.CodeAnalysis.CSharp.Debugging; + +internal class BreakpointResolver(Solution solution, string text) : AbstractBreakpointResolver(solution, text, LanguageNames.CSharp, EqualityComparer.Default) { - internal class BreakpointResolver(Solution solution, string text) : AbstractBreakpointResolver(solution, text, LanguageNames.CSharp, EqualityComparer.Default) + protected override IEnumerable GetMembers(INamedTypeSymbol type, string name) { - protected override IEnumerable GetMembers(INamedTypeSymbol type, string name) - { - var members = type.GetMembers() - .Where(m => m.Name == name || - m.ExplicitInterfaceImplementations() - .Where(i => i.Name == name) - .Any()); + var members = type.GetMembers() + .Where(m => m.Name == name || + m.ExplicitInterfaceImplementations() + .Where(i => i.Name == name) + .Any()); - return (type.Name == name) ? members.Concat(type.Constructors) : members; - } + return (type.Name == name) ? members.Concat(type.Constructors) : members; + } - protected override bool HasMethodBody(IMethodSymbol method, CancellationToken cancellationToken) - { - var location = method.Locations.First(loc => loc.IsInSource); - var tree = location.SourceTree; - var token = tree.GetRoot(cancellationToken).FindToken(location.SourceSpan.Start); + protected override bool HasMethodBody(IMethodSymbol method, CancellationToken cancellationToken) + { + var location = method.Locations.First(loc => loc.IsInSource); + var tree = location.SourceTree; + var token = tree.GetRoot(cancellationToken).FindToken(location.SourceSpan.Start); - return token.GetAncestor().GetBody() != null; - } + return token.GetAncestor().GetBody() != null; + } - protected override void ParseText( - out IList nameParts, - out int? parameterCount) - { - var text = Text; + protected override void ParseText( + out IList nameParts, + out int? parameterCount) + { + var text = Text; - Debug.Assert(text != null); + Debug.Assert(text != null); - var name = SyntaxFactory.ParseName(text, consumeFullText: false); - var lengthOfParsedText = name.FullSpan.End; - var parameterList = SyntaxFactory.ParseParameterList(text, lengthOfParsedText, consumeFullText: false); - var foundIncompleteParameterList = false; + var name = SyntaxFactory.ParseName(text, consumeFullText: false); + var lengthOfParsedText = name.FullSpan.End; + var parameterList = SyntaxFactory.ParseParameterList(text, lengthOfParsedText, consumeFullText: false); + var foundIncompleteParameterList = false; - parameterCount = null; - if (!parameterList.IsMissing) + parameterCount = null; + if (!parameterList.IsMissing) + { + if (parameterList.OpenParenToken.IsMissing || parameterList.CloseParenToken.IsMissing) { - if (parameterList.OpenParenToken.IsMissing || parameterList.CloseParenToken.IsMissing) - { - foundIncompleteParameterList = true; - } - else - { - lengthOfParsedText += parameterList.FullSpan.End; - parameterCount = parameterList.Parameters.Count; - } + foundIncompleteParameterList = true; } - - // If there is remaining text to parse, attempt to eat a trailing semicolon. - if (lengthOfParsedText < text.Length) + else { - var token = SyntaxFactory.ParseToken(text, lengthOfParsedText); - if (token.IsKind(SyntaxKind.SemicolonToken)) - { - lengthOfParsedText += token.FullSpan.End; - } + lengthOfParsedText += parameterList.FullSpan.End; + parameterCount = parameterList.Parameters.Count; } + } - // It's not obvious, but this method can handle the case where name "IsMissing" (no suitable name was be parsed). - var parts = name.GetNameParts(); - - // If we could not parse a valid parameter list or there was additional trailing text that could not be - // interpreted, don't return any names or parameters. - // Also, "Break at Function" doesn't seem to support alias qualified names with the old language service, - // and aliases don't seem meaningful for the purposes of resolving symbols from source. Since we don't - // have precedent or a clear user scenario, we won't resolve any alias qualified names (alias qualified - // parameters are accepted, but we still only validate parameter count, similar to the old implementation). - if (!foundIncompleteParameterList && (lengthOfParsedText == text.Length) && - !parts.Any(p => p.IsKind(SyntaxKind.AliasQualifiedName))) - { - nameParts = parts.Cast().Select(p => new NameAndArity(p.Identifier.ValueText, p.Arity)).ToList(); - } - else + // If there is remaining text to parse, attempt to eat a trailing semicolon. + if (lengthOfParsedText < text.Length) + { + var token = SyntaxFactory.ParseToken(text, lengthOfParsedText); + if (token.IsKind(SyntaxKind.SemicolonToken)) { - nameParts = SpecializedCollections.EmptyList(); + lengthOfParsedText += token.FullSpan.End; } } + + // It's not obvious, but this method can handle the case where name "IsMissing" (no suitable name was be parsed). + var parts = name.GetNameParts(); + + // If we could not parse a valid parameter list or there was additional trailing text that could not be + // interpreted, don't return any names or parameters. + // Also, "Break at Function" doesn't seem to support alias qualified names with the old language service, + // and aliases don't seem meaningful for the purposes of resolving symbols from source. Since we don't + // have precedent or a clear user scenario, we won't resolve any alias qualified names (alias qualified + // parameters are accepted, but we still only validate parameter count, similar to the old implementation). + if (!foundIncompleteParameterList && (lengthOfParsedText == text.Length) && + !parts.Any(p => p.IsKind(SyntaxKind.AliasQualifiedName))) + { + nameParts = parts.Cast().Select(p => new NameAndArity(p.Identifier.ValueText, p.Arity)).ToList(); + } + else + { + nameParts = SpecializedCollections.EmptyList(); + } } } diff --git a/src/Features/CSharp/Portable/Debugging/CSharpBreakpointResolutionService.cs b/src/Features/CSharp/Portable/Debugging/CSharpBreakpointResolutionService.cs index 6807a35f11195..ffc88f2a2dc92 100644 --- a/src/Features/CSharp/Portable/Debugging/CSharpBreakpointResolutionService.cs +++ b/src/Features/CSharp/Portable/Debugging/CSharpBreakpointResolutionService.cs @@ -14,44 +14,43 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Debugging +namespace Microsoft.CodeAnalysis.CSharp.Debugging; + +[ExportLanguageService(typeof(IBreakpointResolutionService), LanguageNames.CSharp), Shared] +internal sealed class CSharpBreakpointResolutionService : IBreakpointResolutionService { - [ExportLanguageService(typeof(IBreakpointResolutionService), LanguageNames.CSharp), Shared] - internal sealed class CSharpBreakpointResolutionService : IBreakpointResolutionService + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpBreakpointResolutionService() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpBreakpointResolutionService() - { - } + } - /// - /// Returns null if a breakpoint can't be placed at the specified position. - /// - public async Task ResolveBreakpointAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken) + /// + /// Returns null if a breakpoint can't be placed at the specified position. + /// + public async Task ResolveBreakpointAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken) + { + try { - try + var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (tree == null || !BreakpointSpans.TryGetBreakpointSpan(tree, textSpan.Start, cancellationToken, out var span)) { - var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (tree == null || !BreakpointSpans.TryGetBreakpointSpan(tree, textSpan.Start, cancellationToken, out var span)) - { - return null; - } - - if (span.Length == 0) - { - return BreakpointResolutionResult.CreateLineResult(document); - } - - return BreakpointResolutionResult.CreateSpanResult(document, span); + return null; } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + + if (span.Length == 0) { - return null; + return BreakpointResolutionResult.CreateLineResult(document); } - } - public Task> ResolveBreakpointsAsync(Solution solution, string name, CancellationToken cancellationToken) - => new BreakpointResolver(solution, name).DoAsync(cancellationToken); + return BreakpointResolutionResult.CreateSpanResult(document, span); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + return null; + } } + + public Task> ResolveBreakpointsAsync(Solution solution, string name, CancellationToken cancellationToken) + => new BreakpointResolver(solution, name).DoAsync(cancellationToken); } diff --git a/src/Features/CSharp/Portable/Debugging/CSharpLanguageDebugInfoService.cs b/src/Features/CSharp/Portable/Debugging/CSharpLanguageDebugInfoService.cs index 6f33de994ef56..3797bd81cd940 100644 --- a/src/Features/CSharp/Portable/Debugging/CSharpLanguageDebugInfoService.cs +++ b/src/Features/CSharp/Portable/Debugging/CSharpLanguageDebugInfoService.cs @@ -11,24 +11,23 @@ using Microsoft.CodeAnalysis.Debugging; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Debugging +namespace Microsoft.CodeAnalysis.CSharp.Debugging; + +[ExportLanguageService(typeof(ILanguageDebugInfoService), LanguageNames.CSharp), Shared] +internal partial class CSharpLanguageDebugInfoService : ILanguageDebugInfoService { - [ExportLanguageService(typeof(ILanguageDebugInfoService), LanguageNames.CSharp), Shared] - internal partial class CSharpLanguageDebugInfoService : ILanguageDebugInfoService + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpLanguageDebugInfoService() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpLanguageDebugInfoService() - { - } + } - public Task GetLocationInfoAsync(Document document, int position, CancellationToken cancellationToken) - => LocationInfoGetter.GetInfoAsync(document, position, cancellationToken); + public Task GetLocationInfoAsync(Document document, int position, CancellationToken cancellationToken) + => LocationInfoGetter.GetInfoAsync(document, position, cancellationToken); - public Task GetDataTipInfoAsync( - Document document, int position, CancellationToken cancellationToken) - { - return DataTipInfoGetter.GetInfoAsync(document, position, cancellationToken); - } + public Task GetDataTipInfoAsync( + Document document, int position, CancellationToken cancellationToken) + { + return DataTipInfoGetter.GetInfoAsync(document, position, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.ExpressionType.cs b/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.ExpressionType.cs index caa2f3ee8ed66..9034f568fbd26 100644 --- a/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.ExpressionType.cs +++ b/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.ExpressionType.cs @@ -6,43 +6,42 @@ using System; -namespace Microsoft.CodeAnalysis.CSharp.Debugging +namespace Microsoft.CodeAnalysis.CSharp.Debugging; + +internal partial class CSharpProximityExpressionsService { - internal partial class CSharpProximityExpressionsService + // Flags used for "collecting" terms for proximity expressions. The flags are somewhat + // confusing. The key point to remember is that an expression will be placed in the result + // set if and only if ValidTerm is set. ValidExpression is used to indicate that while an + // expression may not be a ValidTerm by itself, it can be used as a sub-expression of a + // larger expression that IS a ValidTerm. (Note: ValidTerm implies ValidExpression) + // + // For example, consider the expression a[b+c]. The analysis of this expression starts at + // the ElementAccessExpression. The rules for an ElementAccessExpression say that the + // expression is a ValidTerm if and only if both the LHS('a' in this case) and the + // RHS('b+c') are valid ValidExpressions. The LHS is a ValidTerm, and the RHS is a binary + // operator-- this time AddExpression. The rules for AddExpression state that the expression + // is never a ValidTerm, but is a ValidExpression if both the LHS and the RHS are + // ValidExpressions. In this case, both 'b' and 'c' are ValidTerms (thus valid expressions), + // so 'a+b' is a ValidExpression (but not a ValidTerm), and finally 'a[b+c]' is considered a + // ValidTerm. So, the result of GetProximityExpressions for this expression would be: + // + // a + // + // b + // + // c + // + // a[b+c] + // + // (but not "b+c") + [Flags] + private enum ExpressionType { - // Flags used for "collecting" terms for proximity expressions. The flags are somewhat - // confusing. The key point to remember is that an expression will be placed in the result - // set if and only if ValidTerm is set. ValidExpression is used to indicate that while an - // expression may not be a ValidTerm by itself, it can be used as a sub-expression of a - // larger expression that IS a ValidTerm. (Note: ValidTerm implies ValidExpression) - // - // For example, consider the expression a[b+c]. The analysis of this expression starts at - // the ElementAccessExpression. The rules for an ElementAccessExpression say that the - // expression is a ValidTerm if and only if both the LHS('a' in this case) and the - // RHS('b+c') are valid ValidExpressions. The LHS is a ValidTerm, and the RHS is a binary - // operator-- this time AddExpression. The rules for AddExpression state that the expression - // is never a ValidTerm, but is a ValidExpression if both the LHS and the RHS are - // ValidExpressions. In this case, both 'b' and 'c' are ValidTerms (thus valid expressions), - // so 'a+b' is a ValidExpression (but not a ValidTerm), and finally 'a[b+c]' is considered a - // ValidTerm. So, the result of GetProximityExpressions for this expression would be: - // - // a - // - // b - // - // c - // - // a[b+c] - // - // (but not "b+c") - [Flags] - private enum ExpressionType - { - Invalid = 0x0, - ValidExpression = 0x1, + Invalid = 0x0, + ValidExpression = 0x1, - // Note: ValidTerm implies ValidExpression. - ValidTerm = 0x3 - } + // Note: ValidTerm implies ValidExpression. + ValidTerm = 0x3 } } diff --git a/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.RelevantExpressionsCollector.cs b/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.RelevantExpressionsCollector.cs index ec861ff160265..8a7a4a78a5bf7 100644 --- a/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.RelevantExpressionsCollector.cs +++ b/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.RelevantExpressionsCollector.cs @@ -8,151 +8,150 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Debugging +namespace Microsoft.CodeAnalysis.CSharp.Debugging; + +internal partial class CSharpProximityExpressionsService { - internal partial class CSharpProximityExpressionsService + private class RelevantExpressionsCollector(bool includeDeclarations, IList expressions) : CSharpSyntaxVisitor { - private class RelevantExpressionsCollector(bool includeDeclarations, IList expressions) : CSharpSyntaxVisitor + private readonly bool _includeDeclarations = includeDeclarations; + private readonly IList _expressions = expressions; + + public override void VisitLabeledStatement(LabeledStatementSyntax node) + => AddRelevantExpressions(node.Statement, _expressions, _includeDeclarations); + + public override void VisitExpressionStatement(ExpressionStatementSyntax node) + => AddExpressionTerms(node.Expression, _expressions); + + public override void VisitReturnStatement(ReturnStatementSyntax node) + => AddExpressionTerms(node.Expression, _expressions); + + public override void VisitThrowStatement(ThrowStatementSyntax node) + => AddExpressionTerms(node.Expression, _expressions); + + public override void VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node) { - private readonly bool _includeDeclarations = includeDeclarations; - private readonly IList _expressions = expressions; + // Here, we collect expression expressions from any/all initialization expressions + AddVariableExpressions(node.Declaration.Variables, _expressions); + } - public override void VisitLabeledStatement(LabeledStatementSyntax node) - => AddRelevantExpressions(node.Statement, _expressions, _includeDeclarations); + public override void VisitDoStatement(DoStatementSyntax node) + => AddExpressionTerms(node.Condition, _expressions); - public override void VisitExpressionStatement(ExpressionStatementSyntax node) - => AddExpressionTerms(node.Expression, _expressions); + public override void VisitLockStatement(LockStatementSyntax node) + => AddExpressionTerms(node.Expression, _expressions); - public override void VisitReturnStatement(ReturnStatementSyntax node) - => AddExpressionTerms(node.Expression, _expressions); + public override void VisitWhileStatement(WhileStatementSyntax node) + => AddExpressionTerms(node.Condition, _expressions); - public override void VisitThrowStatement(ThrowStatementSyntax node) - => AddExpressionTerms(node.Expression, _expressions); + public override void VisitIfStatement(IfStatementSyntax node) + => AddExpressionTerms(node.Condition, _expressions); - public override void VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node) + public override void VisitForStatement(ForStatementSyntax node) + { + if (node.Declaration != null) { - // Here, we collect expression expressions from any/all initialization expressions AddVariableExpressions(node.Declaration.Variables, _expressions); } - public override void VisitDoStatement(DoStatementSyntax node) - => AddExpressionTerms(node.Condition, _expressions); - - public override void VisitLockStatement(LockStatementSyntax node) - => AddExpressionTerms(node.Expression, _expressions); + node.Initializers.Do(i => AddExpressionTerms(i, _expressions)); + AddExpressionTerms(node.Condition, _expressions); + node.Incrementors.Do(i => AddExpressionTerms(i, _expressions)); + } - public override void VisitWhileStatement(WhileStatementSyntax node) - => AddExpressionTerms(node.Condition, _expressions); + public override void VisitForEachStatement(ForEachStatementSyntax node) + { + _expressions.Add(node.Identifier.ValueText); + AddExpressionTerms(node.Expression, _expressions); + } - public override void VisitIfStatement(IfStatementSyntax node) - => AddExpressionTerms(node.Condition, _expressions); + public override void VisitForEachVariableStatement(ForEachVariableStatementSyntax node) + { + AddVariableExpressions(node.Variable, _expressions); + AddExpressionTerms(node.Expression, _expressions); + } - public override void VisitForStatement(ForStatementSyntax node) + public override void VisitUsingStatement(UsingStatementSyntax node) + { + if (node.Declaration != null) { - if (node.Declaration != null) - { - AddVariableExpressions(node.Declaration.Variables, _expressions); - } - - node.Initializers.Do(i => AddExpressionTerms(i, _expressions)); - AddExpressionTerms(node.Condition, _expressions); - node.Incrementors.Do(i => AddExpressionTerms(i, _expressions)); + AddVariableExpressions(node.Declaration.Variables, _expressions); } - public override void VisitForEachStatement(ForEachStatementSyntax node) - { - _expressions.Add(node.Identifier.ValueText); - AddExpressionTerms(node.Expression, _expressions); - } + AddExpressionTerms(node.Expression, _expressions); + } - public override void VisitForEachVariableStatement(ForEachVariableStatementSyntax node) - { - AddVariableExpressions(node.Variable, _expressions); - AddExpressionTerms(node.Expression, _expressions); - } + public override void VisitSwitchStatement(SwitchStatementSyntax node) + => AddExpressionTerms(node.Expression, _expressions); - public override void VisitUsingStatement(UsingStatementSyntax node) + private void AddVariableExpressions( + SeparatedSyntaxList declarators, + IList expressions) + { + foreach (var declarator in declarators) { - if (node.Declaration != null) + if (_includeDeclarations) { - AddVariableExpressions(node.Declaration.Variables, _expressions); + expressions.Add(declarator.Identifier.ValueText); } - AddExpressionTerms(node.Expression, _expressions); + if (declarator.Initializer != null) + { + AddExpressionTerms(declarator.Initializer.Value, expressions); + } } + } - public override void VisitSwitchStatement(SwitchStatementSyntax node) - => AddExpressionTerms(node.Expression, _expressions); + private void AddVariableExpressions( + ExpressionSyntax component, + IList expressions) + { + if (!_includeDeclarations) + return; - private void AddVariableExpressions( - SeparatedSyntaxList declarators, - IList expressions) + switch (component.Kind()) { - foreach (var declarator in declarators) - { - if (_includeDeclarations) + case SyntaxKind.TupleExpression: { - expressions.Add(declarator.Identifier.ValueText); - } + var t = (TupleExpressionSyntax)component; + foreach (var a in t.Arguments) + { + AddVariableExpressions(a.Expression, expressions); + } - if (declarator.Initializer != null) + break; + } + case SyntaxKind.DeclarationExpression: { - AddExpressionTerms(declarator.Initializer.Value, expressions); + var t = (DeclarationExpressionSyntax)component; + AddVariableExpressions(t.Designation, expressions); + break; } - } } + } - private void AddVariableExpressions( - ExpressionSyntax component, - IList expressions) - { - if (!_includeDeclarations) - return; - - switch (component.Kind()) - { - case SyntaxKind.TupleExpression: - { - var t = (TupleExpressionSyntax)component; - foreach (var a in t.Arguments) - { - AddVariableExpressions(a.Expression, expressions); - } - - break; - } - case SyntaxKind.DeclarationExpression: - { - var t = (DeclarationExpressionSyntax)component; - AddVariableExpressions(t.Designation, expressions); - break; - } - } - } + private void AddVariableExpressions( + VariableDesignationSyntax component, + IList expressions) + { + if (!_includeDeclarations) + return; - private void AddVariableExpressions( - VariableDesignationSyntax component, - IList expressions) + switch (component.Kind()) { - if (!_includeDeclarations) - return; - - switch (component.Kind()) - { - case SyntaxKind.ParenthesizedVariableDesignation: - { - var t = (ParenthesizedVariableDesignationSyntax)component; - foreach (var v in t.Variables) - AddVariableExpressions(v, expressions); - break; - } - case SyntaxKind.SingleVariableDesignation: - { - var t = (SingleVariableDesignationSyntax)component; - expressions.Add(t.Identifier.ValueText); - break; - } - } + case SyntaxKind.ParenthesizedVariableDesignation: + { + var t = (ParenthesizedVariableDesignationSyntax)component; + foreach (var v in t.Variables) + AddVariableExpressions(v, expressions); + break; + } + case SyntaxKind.SingleVariableDesignation: + { + var t = (SingleVariableDesignationSyntax)component; + expressions.Add(t.Identifier.ValueText); + break; + } } } } diff --git a/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.Worker.cs b/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.Worker.cs index 0eee165692934..9f3b8c58aa4c5 100644 --- a/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.Worker.cs +++ b/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.Worker.cs @@ -12,296 +12,295 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Debugging +namespace Microsoft.CodeAnalysis.CSharp.Debugging; + +internal partial class CSharpProximityExpressionsService { - internal partial class CSharpProximityExpressionsService + internal class Worker(SyntaxTree syntaxTree, int position) { - internal class Worker(SyntaxTree syntaxTree, int position) - { - private readonly SyntaxTree _syntaxTree = syntaxTree; - private readonly int _position = position; + private readonly SyntaxTree _syntaxTree = syntaxTree; + private readonly int _position = position; - private StatementSyntax _parentStatement; - private SyntaxToken _token; - private readonly List _expressions = []; + private StatementSyntax _parentStatement; + private SyntaxToken _token; + private readonly List _expressions = []; - internal IList Do(CancellationToken cancellationToken) + internal IList Do(CancellationToken cancellationToken) + { + // First, find the containing statement. We'll want to add the expressions in this + // statement to the result. + _token = _syntaxTree.GetRoot(cancellationToken).FindToken(_position); + _parentStatement = _token.GetAncestor(); + if (_parentStatement == null) { - // First, find the containing statement. We'll want to add the expressions in this - // statement to the result. - _token = _syntaxTree.GetRoot(cancellationToken).FindToken(_position); - _parentStatement = _token.GetAncestor(); - if (_parentStatement == null) - { - return null; - } + return null; + } - AddRelevantExpressions(_parentStatement, _expressions, includeDeclarations: false); - AddPrecedingRelevantExpressions(); - AddFollowingRelevantExpressions(cancellationToken); - AddCurrentDeclaration(); - AddMethodParameters(); - AddIndexerParameters(); - AddCatchParameters(); - AddThisExpression(); - AddValueExpression(); + AddRelevantExpressions(_parentStatement, _expressions, includeDeclarations: false); + AddPrecedingRelevantExpressions(); + AddFollowingRelevantExpressions(cancellationToken); + AddCurrentDeclaration(); + AddMethodParameters(); + AddIndexerParameters(); + AddCatchParameters(); + AddThisExpression(); + AddValueExpression(); - var result = _expressions.Distinct().Where(e => e.Length > 0).ToList(); - return result.Count == 0 ? null : result; - } + var result = _expressions.Distinct().Where(e => e.Length > 0).ToList(); + return result.Count == 0 ? null : result; + } - private void AddValueExpression() + private void AddValueExpression() + { + // If we're in a setter/adder/remover then add "value". + if (_parentStatement.GetAncestorOrThis() is (kind: + SyntaxKind.SetAccessorDeclaration or + SyntaxKind.InitAccessorDeclaration or + SyntaxKind.AddAccessorDeclaration or + SyntaxKind.RemoveAccessorDeclaration)) { - // If we're in a setter/adder/remover then add "value". - if (_parentStatement.GetAncestorOrThis() is (kind: - SyntaxKind.SetAccessorDeclaration or - SyntaxKind.InitAccessorDeclaration or - SyntaxKind.AddAccessorDeclaration or - SyntaxKind.RemoveAccessorDeclaration)) - { - _expressions.Add("value"); - } + _expressions.Add("value"); } + } - private void AddThisExpression() + private void AddThisExpression() + { + // If it's an instance member, then also add "this". + var memberDeclaration = _parentStatement.GetAncestorOrThis(); + if (!memberDeclaration.IsKind(SyntaxKind.GlobalStatement) && !memberDeclaration.GetModifiers().Any(SyntaxKind.StaticKeyword)) { - // If it's an instance member, then also add "this". - var memberDeclaration = _parentStatement.GetAncestorOrThis(); - if (!memberDeclaration.IsKind(SyntaxKind.GlobalStatement) && !memberDeclaration.GetModifiers().Any(SyntaxKind.StaticKeyword)) - { - _expressions.Add("this"); - } + _expressions.Add("this"); } + } - private void AddCatchParameters() - { - var block = GetImmediatelyContainingBlock(); - - // if we're the start of a "catch(Goo e)" clause, then add "e". - if (block != null && block?.Parent is CatchClauseSyntax catchClause && - catchClause.Declaration != null && catchClause.Declaration.Identifier.Kind() != SyntaxKind.None) - { - _expressions.Add(catchClause.Declaration.Identifier.ValueText); - } - } + private void AddCatchParameters() + { + var block = GetImmediatelyContainingBlock(); - private BlockSyntax GetImmediatelyContainingBlock() + // if we're the start of a "catch(Goo e)" clause, then add "e". + if (block != null && block?.Parent is CatchClauseSyntax catchClause && + catchClause.Declaration != null && catchClause.Declaration.Identifier.Kind() != SyntaxKind.None) { - return IsFirstBlockStatement() - ? (BlockSyntax)_parentStatement.Parent - : _parentStatement is BlockSyntax block && block.OpenBraceToken == _token - ? (BlockSyntax)_parentStatement - : null; + _expressions.Add(catchClause.Declaration.Identifier.ValueText); } + } + + private BlockSyntax GetImmediatelyContainingBlock() + { + return IsFirstBlockStatement() + ? (BlockSyntax)_parentStatement.Parent + : _parentStatement is BlockSyntax block && block.OpenBraceToken == _token + ? (BlockSyntax)_parentStatement + : null; + } - private bool IsFirstBlockStatement() - => _parentStatement.Parent is BlockSyntax parentBlockOpt && parentBlockOpt.Statements.FirstOrDefault() == _parentStatement; + private bool IsFirstBlockStatement() + => _parentStatement.Parent is BlockSyntax parentBlockOpt && parentBlockOpt.Statements.FirstOrDefault() == _parentStatement; - private void AddCurrentDeclaration() + private void AddCurrentDeclaration() + { + if (_parentStatement is LocalDeclarationStatementSyntax) { - if (_parentStatement is LocalDeclarationStatementSyntax) - { - AddRelevantExpressions(_parentStatement, _expressions, includeDeclarations: true); - } + AddRelevantExpressions(_parentStatement, _expressions, includeDeclarations: true); } + } - private void AddMethodParameters() - { - // and we're the start of a method, then also add the parameters of that method to - // the proximity expressions. - var block = GetImmediatelyContainingBlock(); + private void AddMethodParameters() + { + // and we're the start of a method, then also add the parameters of that method to + // the proximity expressions. + var block = GetImmediatelyContainingBlock(); - if (block != null && block.Parent is MemberDeclarationSyntax memberDeclaration) - { - var parameterList = memberDeclaration.GetParameterList(); - AddParameters(parameterList); - } - else if (block is null - && _parentStatement.Parent is GlobalStatementSyntax { Parent: CompilationUnitSyntax compilationUnit } globalStatement - && compilationUnit.Members.FirstOrDefault() == globalStatement) - { - _expressions.Add("args"); - } + if (block != null && block.Parent is MemberDeclarationSyntax memberDeclaration) + { + var parameterList = memberDeclaration.GetParameterList(); + AddParameters(parameterList); } - - private void AddIndexerParameters() + else if (block is null + && _parentStatement.Parent is GlobalStatementSyntax { Parent: CompilationUnitSyntax compilationUnit } globalStatement + && compilationUnit.Members.FirstOrDefault() == globalStatement) { - var block = GetImmediatelyContainingBlock(); - - // and we're the start of a method, then also add the parameters of that method to - // the proximity expressions. - if (block != null && - block.Parent is AccessorDeclarationSyntax && - block.Parent.Parent is AccessorListSyntax && - block.Parent.Parent.Parent is IndexerDeclarationSyntax indexerDeclaration) - { - var parameterList = indexerDeclaration.ParameterList; - AddParameters(parameterList); - } + _expressions.Add("args"); } + } + + private void AddIndexerParameters() + { + var block = GetImmediatelyContainingBlock(); - private void AddParameters(BaseParameterListSyntax parameterList) + // and we're the start of a method, then also add the parameters of that method to + // the proximity expressions. + if (block != null && + block.Parent is AccessorDeclarationSyntax && + block.Parent.Parent is AccessorListSyntax && + block.Parent.Parent.Parent is IndexerDeclarationSyntax indexerDeclaration) { - if (parameterList != null) - { - _expressions.AddRange( - from p in parameterList.Parameters - select p.Identifier.ValueText); - } + var parameterList = indexerDeclaration.ParameterList; + AddParameters(parameterList); } + } - private void AddFollowingRelevantExpressions(CancellationToken cancellationToken) + private void AddParameters(BaseParameterListSyntax parameterList) + { + if (parameterList != null) { - var line = _syntaxTree.GetText(cancellationToken).Lines.IndexOf(_position); - - // If there's are more statements following us on the same line, then add them as - // well. - for (var nextStatement = _parentStatement.GetNextStatement(); - nextStatement != null && _syntaxTree.GetText(cancellationToken).Lines.IndexOf(nextStatement.SpanStart) == line; - nextStatement = nextStatement.GetNextStatement()) - { - AddRelevantExpressions(nextStatement, _expressions, includeDeclarations: false); - } + _expressions.AddRange( + from p in parameterList.Parameters + select p.Identifier.ValueText); } + } - private void AddPrecedingRelevantExpressions() + private void AddFollowingRelevantExpressions(CancellationToken cancellationToken) + { + var line = _syntaxTree.GetText(cancellationToken).Lines.IndexOf(_position); + + // If there's are more statements following us on the same line, then add them as + // well. + for (var nextStatement = _parentStatement.GetNextStatement(); + nextStatement != null && _syntaxTree.GetText(cancellationToken).Lines.IndexOf(nextStatement.SpanStart) == line; + nextStatement = nextStatement.GetNextStatement()) { - // If we're not the first statement in this block, - // and there's an expression or declaration statement directly above us, - // then add the expressions from that as well. + AddRelevantExpressions(nextStatement, _expressions, includeDeclarations: false); + } + } - StatementSyntax previousStatement; + private void AddPrecedingRelevantExpressions() + { + // If we're not the first statement in this block, + // and there's an expression or declaration statement directly above us, + // then add the expressions from that as well. - if (_parentStatement is BlockSyntax block && - block.CloseBraceToken == _token) - { - // If we're at the last brace of a block, use the last - // statement in the block. - previousStatement = block.Statements.LastOrDefault(); - } - else - { - previousStatement = _parentStatement.GetPreviousStatement(); - } + StatementSyntax previousStatement; - if (previousStatement != null) - { - switch (previousStatement.Kind()) - { - case SyntaxKind.ExpressionStatement: - case SyntaxKind.LocalDeclarationStatement: - AddRelevantExpressions(previousStatement, _expressions, includeDeclarations: true); - break; - case SyntaxKind.DoStatement: - AddExpressionTerms((previousStatement as DoStatementSyntax).Condition, _expressions); - AddLastStatementOfConstruct(previousStatement); - break; - case SyntaxKind.ForStatement: - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - case SyntaxKind.IfStatement: - case SyntaxKind.CheckedStatement: - case SyntaxKind.UncheckedStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.LockStatement: - case SyntaxKind.SwitchStatement: - case SyntaxKind.TryStatement: - case SyntaxKind.UsingStatement: - AddRelevantExpressions(previousStatement, _expressions, includeDeclarations: false); - AddLastStatementOfConstruct(previousStatement); - break; - default: - break; - } - } - else - { - // This is the first statement of the block. Go to the nearest enclosing statement and add its expressions - var statementAncestor = _parentStatement.Ancestors().OfType().FirstOrDefault(node => !node.IsKind(SyntaxKind.Block)); - if (statementAncestor != null) - { - AddRelevantExpressions(statementAncestor, _expressions, includeDeclarations: true); - } - } + if (_parentStatement is BlockSyntax block && + block.CloseBraceToken == _token) + { + // If we're at the last brace of a block, use the last + // statement in the block. + previousStatement = block.Statements.LastOrDefault(); } - - private void AddLastStatementOfConstruct(StatementSyntax statement) + else { - if (statement == null) - { - return; - } + previousStatement = _parentStatement.GetPreviousStatement(); + } - switch (statement.Kind()) + if (previousStatement != null) + { + switch (previousStatement.Kind()) { - case SyntaxKind.Block: - AddLastStatementOfConstruct((statement as BlockSyntax).Statements.LastOrDefault()); - break; - case SyntaxKind.BreakStatement: - case SyntaxKind.ContinueStatement: - AddLastStatementOfConstruct(statement.GetPreviousStatement()); - break; - case SyntaxKind.CheckedStatement: - case SyntaxKind.UncheckedStatement: - AddLastStatementOfConstruct((statement as CheckedStatementSyntax).Block); + case SyntaxKind.ExpressionStatement: + case SyntaxKind.LocalDeclarationStatement: + AddRelevantExpressions(previousStatement, _expressions, includeDeclarations: true); break; case SyntaxKind.DoStatement: - AddLastStatementOfConstruct((statement as DoStatementSyntax).Statement); + AddExpressionTerms((previousStatement as DoStatementSyntax).Condition, _expressions); + AddLastStatementOfConstruct(previousStatement); break; case SyntaxKind.ForStatement: - AddLastStatementOfConstruct((statement as ForStatementSyntax).Statement); - break; case SyntaxKind.ForEachStatement: case SyntaxKind.ForEachVariableStatement: - AddLastStatementOfConstruct((statement as CommonForEachStatementSyntax).Statement); - break; case SyntaxKind.IfStatement: - var ifStatement = statement as IfStatementSyntax; - AddLastStatementOfConstruct(ifStatement.Statement); - if (ifStatement.Else != null) - { - AddLastStatementOfConstruct(ifStatement.Else.Statement); - } - - break; + case SyntaxKind.CheckedStatement: + case SyntaxKind.UncheckedStatement: + case SyntaxKind.WhileStatement: case SyntaxKind.LockStatement: - AddLastStatementOfConstruct((statement as LockStatementSyntax).Statement); - break; case SyntaxKind.SwitchStatement: - var switchStatement = statement as SwitchStatementSyntax; - foreach (var section in switchStatement.Sections) - { - AddLastStatementOfConstruct(section.Statements.LastOrDefault()); - } - - break; case SyntaxKind.TryStatement: - var tryStatement = statement as TryStatementSyntax; - if (tryStatement.Finally != null) - { - AddLastStatementOfConstruct(tryStatement.Finally.Block); - } - else - { - AddLastStatementOfConstruct(tryStatement.Block); - foreach (var catchClause in tryStatement.Catches) - { - AddLastStatementOfConstruct(catchClause.Block); - } - } - - break; case SyntaxKind.UsingStatement: - AddLastStatementOfConstruct((statement as UsingStatementSyntax).Statement); - break; - case SyntaxKind.WhileStatement: - AddLastStatementOfConstruct((statement as WhileStatementSyntax).Statement); + AddRelevantExpressions(previousStatement, _expressions, includeDeclarations: false); + AddLastStatementOfConstruct(previousStatement); break; default: - AddRelevantExpressions(statement, _expressions, includeDeclarations: false); break; } } + else + { + // This is the first statement of the block. Go to the nearest enclosing statement and add its expressions + var statementAncestor = _parentStatement.Ancestors().OfType().FirstOrDefault(node => !node.IsKind(SyntaxKind.Block)); + if (statementAncestor != null) + { + AddRelevantExpressions(statementAncestor, _expressions, includeDeclarations: true); + } + } + } + + private void AddLastStatementOfConstruct(StatementSyntax statement) + { + if (statement == null) + { + return; + } + + switch (statement.Kind()) + { + case SyntaxKind.Block: + AddLastStatementOfConstruct((statement as BlockSyntax).Statements.LastOrDefault()); + break; + case SyntaxKind.BreakStatement: + case SyntaxKind.ContinueStatement: + AddLastStatementOfConstruct(statement.GetPreviousStatement()); + break; + case SyntaxKind.CheckedStatement: + case SyntaxKind.UncheckedStatement: + AddLastStatementOfConstruct((statement as CheckedStatementSyntax).Block); + break; + case SyntaxKind.DoStatement: + AddLastStatementOfConstruct((statement as DoStatementSyntax).Statement); + break; + case SyntaxKind.ForStatement: + AddLastStatementOfConstruct((statement as ForStatementSyntax).Statement); + break; + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + AddLastStatementOfConstruct((statement as CommonForEachStatementSyntax).Statement); + break; + case SyntaxKind.IfStatement: + var ifStatement = statement as IfStatementSyntax; + AddLastStatementOfConstruct(ifStatement.Statement); + if (ifStatement.Else != null) + { + AddLastStatementOfConstruct(ifStatement.Else.Statement); + } + + break; + case SyntaxKind.LockStatement: + AddLastStatementOfConstruct((statement as LockStatementSyntax).Statement); + break; + case SyntaxKind.SwitchStatement: + var switchStatement = statement as SwitchStatementSyntax; + foreach (var section in switchStatement.Sections) + { + AddLastStatementOfConstruct(section.Statements.LastOrDefault()); + } + + break; + case SyntaxKind.TryStatement: + var tryStatement = statement as TryStatementSyntax; + if (tryStatement.Finally != null) + { + AddLastStatementOfConstruct(tryStatement.Finally.Block); + } + else + { + AddLastStatementOfConstruct(tryStatement.Block); + foreach (var catchClause in tryStatement.Catches) + { + AddLastStatementOfConstruct(catchClause.Block); + } + } + + break; + case SyntaxKind.UsingStatement: + AddLastStatementOfConstruct((statement as UsingStatementSyntax).Statement); + break; + case SyntaxKind.WhileStatement: + AddLastStatementOfConstruct((statement as WhileStatementSyntax).Statement); + break; + default: + AddRelevantExpressions(statement, _expressions, includeDeclarations: false); + break; + } } } } diff --git a/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.cs b/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.cs index fd2ffb1304cb8..ce2fd49458d20 100644 --- a/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.cs +++ b/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService.cs @@ -17,102 +17,101 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Debugging +namespace Microsoft.CodeAnalysis.CSharp.Debugging; + +/// +/// Given a position in a source file, returns the expressions in close proximity that should +/// show up in the debugger 'autos' window. In general, the expressions we place into the autos +/// window are things that appear to be 'side effect free'. Note: because we only use the syntax +/// tree for this, it's possible for us to get this wrong. However, this should only happen in +/// code that behaves unexpectedly. For example, we will assume that "a + b" is side effect free +/// (when in practice it may not be). +/// +/// The general tactic we take is to add the expressions for the statements on the +/// line the debugger is currently at. We will also try to find the 'previous' statement as well +/// to add the expressions from that. The 'previous' statement is a bit of an interesting beast. +/// Consider, for example, if the user has just jumped out of a switch and is the statement +/// directly following it. What is the previous statement? Without keeping state, there's no way +/// to know. So, in this case, we treat all 'exit points' (i.e. the last statement of a switch +/// section) of the switch statement as the 'previous statement'. There are many cases like this +/// we need to handle. Basically anything that might have nested statements/blocks might +/// contribute to the 'previous statement' +/// +[ExportLanguageService(typeof(IProximityExpressionsService), LanguageNames.CSharp), Shared] +internal partial class CSharpProximityExpressionsService : IProximityExpressionsService { - /// - /// Given a position in a source file, returns the expressions in close proximity that should - /// show up in the debugger 'autos' window. In general, the expressions we place into the autos - /// window are things that appear to be 'side effect free'. Note: because we only use the syntax - /// tree for this, it's possible for us to get this wrong. However, this should only happen in - /// code that behaves unexpectedly. For example, we will assume that "a + b" is side effect free - /// (when in practice it may not be). - /// - /// The general tactic we take is to add the expressions for the statements on the - /// line the debugger is currently at. We will also try to find the 'previous' statement as well - /// to add the expressions from that. The 'previous' statement is a bit of an interesting beast. - /// Consider, for example, if the user has just jumped out of a switch and is the statement - /// directly following it. What is the previous statement? Without keeping state, there's no way - /// to know. So, in this case, we treat all 'exit points' (i.e. the last statement of a switch - /// section) of the switch statement as the 'previous statement'. There are many cases like this - /// we need to handle. Basically anything that might have nested statements/blocks might - /// contribute to the 'previous statement' - /// - [ExportLanguageService(typeof(IProximityExpressionsService), LanguageNames.CSharp), Shared] - internal partial class CSharpProximityExpressionsService : IProximityExpressionsService + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpProximityExpressionsService() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpProximityExpressionsService() + } + + public async Task IsValidAsync( + Document document, + int position, + string expressionValue, + CancellationToken cancellationToken) + { + var expression = SyntaxFactory.ParseExpression(expressionValue); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(position); + if (token.Kind() == SyntaxKind.CloseBraceToken && token.GetPreviousToken().Kind() != SyntaxKind.None) { + token = token.GetPreviousToken(); } - public async Task IsValidAsync( - Document document, - int position, - string expressionValue, - CancellationToken cancellationToken) + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var info = semanticModel.GetSpeculativeSymbolInfo(token.SpanStart, expression, SpeculativeBindingOption.BindAsExpression); + if (info.Symbol == null) { - var expression = SyntaxFactory.ParseExpression(expressionValue); - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(position); - if (token.Kind() == SyntaxKind.CloseBraceToken && token.GetPreviousToken().Kind() != SyntaxKind.None) - { - token = token.GetPreviousToken(); - } + return false; + } - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var info = semanticModel.GetSpeculativeSymbolInfo(token.SpanStart, expression, SpeculativeBindingOption.BindAsExpression); - if (info.Symbol == null) + // We seem to have bound successfully. However, if it bound to a local, then make + // sure that that local isn't after the statement that we're currently looking at. + if (info.Symbol.Kind == SymbolKind.Local) + { + var statement = info.Symbol.Locations.First().FindToken(cancellationToken).GetAncestor(); + if (statement != null && position < statement.SpanStart) { return false; } + } - // We seem to have bound successfully. However, if it bound to a local, then make - // sure that that local isn't after the statement that we're currently looking at. - if (info.Symbol.Kind == SymbolKind.Local) - { - var statement = info.Symbol.Locations.First().FindToken(cancellationToken).GetAncestor(); - if (statement != null && position < statement.SpanStart) - { - return false; - } - } + return true; + } - return true; + /// + /// Returns null indicating a failure. + /// + public async Task> GetProximityExpressionsAsync( + Document document, + int position, + CancellationToken cancellationToken) + { + try + { + var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + return GetProximityExpressions(tree, position, cancellationToken); } - - /// - /// Returns null indicating a failure. - /// - public async Task> GetProximityExpressionsAsync( - Document document, - int position, - CancellationToken cancellationToken) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - try - { - var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - return GetProximityExpressions(tree, position, cancellationToken); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return null; - } + return null; } + } - public static IList GetProximityExpressions(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) - => new Worker(syntaxTree, position).Do(cancellationToken); + public static IList GetProximityExpressions(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + => new Worker(syntaxTree, position).Do(cancellationToken); - [Obsolete($"Use {nameof(GetProximityExpressions)}.")] - private static IList Do(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) - => new Worker(syntaxTree, position).Do(cancellationToken); + [Obsolete($"Use {nameof(GetProximityExpressions)}.")] + private static IList Do(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + => new Worker(syntaxTree, position).Do(cancellationToken); - private static void AddRelevantExpressions( - StatementSyntax statement, - IList expressions, - bool includeDeclarations) - { - new RelevantExpressionsCollector(includeDeclarations, expressions).Visit(statement); - } + private static void AddRelevantExpressions( + StatementSyntax statement, + IList expressions, + bool includeDeclarations) + { + new RelevantExpressionsCollector(includeDeclarations, expressions).Visit(statement); } } diff --git a/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService_ExpressionTermCollector.cs b/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService_ExpressionTermCollector.cs index 30d19916e4b7d..7c12c5b0bf3d5 100644 --- a/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService_ExpressionTermCollector.cs +++ b/src/Features/CSharp/Portable/Debugging/CSharpProximityExpressionsService_ExpressionTermCollector.cs @@ -9,406 +9,405 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Debugging +namespace Microsoft.CodeAnalysis.CSharp.Debugging; + +internal partial class CSharpProximityExpressionsService { - internal partial class CSharpProximityExpressionsService + private static string ConvertToString(ExpressionSyntax expression) { - private static string ConvertToString(ExpressionSyntax expression) - { - var converted = expression.ConvertToSingleLine(); - return converted.ToString(); - } + var converted = expression.ConvertToSingleLine(); + return converted.ToString(); + } - private static void AddExpressionTerms(ExpressionSyntax expression, IList terms) + private static void AddExpressionTerms(ExpressionSyntax expression, IList terms) + { + // Check here rather than at all the call sites... + if (expression == null) { - // Check here rather than at all the call sites... - if (expression == null) - { - return; - } + return; + } - // Collect terms from this expression, which returns flags indicating the validity - // of this expression as a whole. - var expressionType = ExpressionType.Invalid; - AddSubExpressionTerms(expression, terms, ref expressionType); + // Collect terms from this expression, which returns flags indicating the validity + // of this expression as a whole. + var expressionType = ExpressionType.Invalid; + AddSubExpressionTerms(expression, terms, ref expressionType); - AddIfValidTerm(expression, expressionType, terms); - } + AddIfValidTerm(expression, expressionType, terms); + } - private static void AddIfValidTerm(ExpressionSyntax expression, ExpressionType type, IList terms) + private static void AddIfValidTerm(ExpressionSyntax expression, ExpressionType type, IList terms) + { + if (IsValidTerm(type)) { - if (IsValidTerm(type)) - { - // If this expression identified itself as a valid term, add it to the - // term table - terms.Add(ConvertToString(expression)); - } + // If this expression identified itself as a valid term, add it to the + // term table + terms.Add(ConvertToString(expression)); } + } - private static bool IsValidTerm(ExpressionType type) - => (type & ExpressionType.ValidTerm) == ExpressionType.ValidTerm; + private static bool IsValidTerm(ExpressionType type) + => (type & ExpressionType.ValidTerm) == ExpressionType.ValidTerm; - private static bool IsValidExpression(ExpressionType type) - => (type & ExpressionType.ValidExpression) == ExpressionType.ValidExpression; + private static bool IsValidExpression(ExpressionType type) + => (type & ExpressionType.ValidExpression) == ExpressionType.ValidExpression; - private static void AddSubExpressionTerms(ExpressionSyntax expression, IList terms, ref ExpressionType expressionType) + private static void AddSubExpressionTerms(ExpressionSyntax expression, IList terms, ref ExpressionType expressionType) + { + // Check here rather than at all the call sites... + if (expression == null) { - // Check here rather than at all the call sites... - if (expression == null) - { - return; - } + return; + } - switch (expression.Kind()) - { - case SyntaxKind.ThisExpression: - case SyntaxKind.BaseExpression: - // an op term is ok if it's a "this" or "base" op it allows us to see - // "this.goo" in the autos window note: it's not a VALIDTERM since we don't - // want "this" showing up in the auto's window twice. - expressionType = ExpressionType.ValidExpression; - return; - - case SyntaxKind.IdentifierName: - // Name nodes are always valid terms - expressionType = ExpressionType.ValidTerm; - return; - - case SyntaxKind.CharacterLiteralExpression: - case SyntaxKind.FalseLiteralExpression: - case SyntaxKind.NullLiteralExpression: - case SyntaxKind.NumericLiteralExpression: - case SyntaxKind.StringLiteralExpression: - case SyntaxKind.TrueLiteralExpression: - // Constants can make up a valid term, but we don't consider them valid - // terms themselves (since we don't want them to show up in the autos window - // on their own). - expressionType = ExpressionType.ValidExpression; - return; - - case SyntaxKind.CastExpression: - AddCastExpressionTerms((CastExpressionSyntax)expression, terms, ref expressionType); - return; - - case SyntaxKind.SimpleMemberAccessExpression: - case SyntaxKind.PointerMemberAccessExpression: - AddMemberAccessExpressionTerms((MemberAccessExpressionSyntax)expression, terms, ref expressionType); - return; - - case SyntaxKind.ObjectCreationExpression: - AddObjectCreationExpressionTerms((ObjectCreationExpressionSyntax)expression, terms, ref expressionType); - return; - - case SyntaxKind.ArrayCreationExpression: - AddArrayCreationExpressionTerms((ArrayCreationExpressionSyntax)expression, terms, ref expressionType); - return; - - case SyntaxKind.InvocationExpression: - AddInvocationExpressionTerms((InvocationExpressionSyntax)expression, terms, ref expressionType); - return; - } + switch (expression.Kind()) + { + case SyntaxKind.ThisExpression: + case SyntaxKind.BaseExpression: + // an op term is ok if it's a "this" or "base" op it allows us to see + // "this.goo" in the autos window note: it's not a VALIDTERM since we don't + // want "this" showing up in the auto's window twice. + expressionType = ExpressionType.ValidExpression; + return; - // +, -, ++, --, !, etc. - // - // This is a valid expression if it doesn't have obvious side effects (i.e. ++, --) - if (expression is PrefixUnaryExpressionSyntax prefixUnary) - { - AddPrefixUnaryExpressionTerms(prefixUnary, terms, ref expressionType); + case SyntaxKind.IdentifierName: + // Name nodes are always valid terms + expressionType = ExpressionType.ValidTerm; return; - } - if (expression is AwaitExpressionSyntax awaitExpression) - { - AddAwaitExpressionTerms(awaitExpression, terms, ref expressionType); + case SyntaxKind.CharacterLiteralExpression: + case SyntaxKind.FalseLiteralExpression: + case SyntaxKind.NullLiteralExpression: + case SyntaxKind.NumericLiteralExpression: + case SyntaxKind.StringLiteralExpression: + case SyntaxKind.TrueLiteralExpression: + // Constants can make up a valid term, but we don't consider them valid + // terms themselves (since we don't want them to show up in the autos window + // on their own). + expressionType = ExpressionType.ValidExpression; return; - } - if (expression is PostfixUnaryExpressionSyntax postfixExpression) - { - AddPostfixUnaryExpressionTerms(postfixExpression, terms, ref expressionType); + case SyntaxKind.CastExpression: + AddCastExpressionTerms((CastExpressionSyntax)expression, terms, ref expressionType); return; - } - if (expression is BinaryExpressionSyntax binaryExpression) - { - AddBinaryExpressionTerms(expression, binaryExpression.Left, binaryExpression.Right, terms, ref expressionType); + case SyntaxKind.SimpleMemberAccessExpression: + case SyntaxKind.PointerMemberAccessExpression: + AddMemberAccessExpressionTerms((MemberAccessExpressionSyntax)expression, terms, ref expressionType); return; - } - if (expression is AssignmentExpressionSyntax assignmentExpression) - { - AddBinaryExpressionTerms(expression, assignmentExpression.Left, assignmentExpression.Right, terms, ref expressionType); + case SyntaxKind.ObjectCreationExpression: + AddObjectCreationExpressionTerms((ObjectCreationExpressionSyntax)expression, terms, ref expressionType); return; - } - if (expression is ConditionalExpressionSyntax conditional) - { - AddConditionalExpressionTerms(conditional, terms, ref expressionType); + case SyntaxKind.ArrayCreationExpression: + AddArrayCreationExpressionTerms((ArrayCreationExpressionSyntax)expression, terms, ref expressionType); return; - } - if (expression is ParenthesizedExpressionSyntax parenthesizedExpression) - { - AddSubExpressionTerms(parenthesizedExpression.Expression, terms, ref expressionType); - } + case SyntaxKind.InvocationExpression: + AddInvocationExpressionTerms((InvocationExpressionSyntax)expression, terms, ref expressionType); + return; + } - expressionType = ExpressionType.Invalid; + // +, -, ++, --, !, etc. + // + // This is a valid expression if it doesn't have obvious side effects (i.e. ++, --) + if (expression is PrefixUnaryExpressionSyntax prefixUnary) + { + AddPrefixUnaryExpressionTerms(prefixUnary, terms, ref expressionType); + return; } - private static void AddCastExpressionTerms(CastExpressionSyntax castExpression, IList terms, ref ExpressionType expressionType) + if (expression is AwaitExpressionSyntax awaitExpression) { - // For a cast, just add the nested expression. Note: this is technically - // unsafe as the cast *may* have side effects. However, in practice this is - // extremely rare, so we allow for this since it's ok in the common case. + AddAwaitExpressionTerms(awaitExpression, terms, ref expressionType); + return; + } - var flags = ExpressionType.Invalid; + if (expression is PostfixUnaryExpressionSyntax postfixExpression) + { + AddPostfixUnaryExpressionTerms(postfixExpression, terms, ref expressionType); + return; + } - // Ask our subexpression for terms - AddSubExpressionTerms(castExpression.Expression, terms, ref flags); + if (expression is BinaryExpressionSyntax binaryExpression) + { + AddBinaryExpressionTerms(expression, binaryExpression.Left, binaryExpression.Right, terms, ref expressionType); + return; + } - // Is our expression a valid term? - AddIfValidTerm(castExpression.Expression, flags, terms); + if (expression is AssignmentExpressionSyntax assignmentExpression) + { + AddBinaryExpressionTerms(expression, assignmentExpression.Left, assignmentExpression.Right, terms, ref expressionType); + return; + } - // If the subexpression is a valid term, so is the cast expression - expressionType = flags; + if (expression is ConditionalExpressionSyntax conditional) + { + AddConditionalExpressionTerms(conditional, terms, ref expressionType); + return; } - private static void AddMemberAccessExpressionTerms(MemberAccessExpressionSyntax memberAccessExpression, IList terms, ref ExpressionType expressionType) + if (expression is ParenthesizedExpressionSyntax parenthesizedExpression) { - var flags = ExpressionType.Invalid; + AddSubExpressionTerms(parenthesizedExpression.Expression, terms, ref expressionType); + } - // These operators always have a RHS of a name node, which we know would - // "claim" to be a valid term, but is not valid without the LHS present. - // So, we don't bother collecting anything from the RHS... - AddSubExpressionTerms(memberAccessExpression.Expression, terms, ref flags); + expressionType = ExpressionType.Invalid; + } - // If the LHS says it's a valid term, then we add it ONLY if our PARENT - // is NOT another dot/arrow. This allows the expression 'a.b.c.d' to - // add both 'a.b.c.d' and 'a.b.c', but not 'a.b' and 'a'. - if (IsValidTerm(flags) && - memberAccessExpression.Parent?.Kind() is not SyntaxKind.SimpleMemberAccessExpression and not SyntaxKind.PointerMemberAccessExpression) - { - terms.Add(ConvertToString(memberAccessExpression.Expression)); - } + private static void AddCastExpressionTerms(CastExpressionSyntax castExpression, IList terms, ref ExpressionType expressionType) + { + // For a cast, just add the nested expression. Note: this is technically + // unsafe as the cast *may* have side effects. However, in practice this is + // extremely rare, so we allow for this since it's ok in the common case. - // And this expression itself is a valid term if the LHS is a valid - // expression, and its PARENT is not an invocation. - if (IsValidExpression(flags) && - !memberAccessExpression.IsParentKind(SyntaxKind.InvocationExpression)) - { - expressionType = ExpressionType.ValidTerm; - } - else - { - expressionType = ExpressionType.ValidExpression; - } - } + var flags = ExpressionType.Invalid; - private static void AddObjectCreationExpressionTerms(ObjectCreationExpressionSyntax objectionCreationExpression, IList terms, ref ExpressionType expressionType) - { - // Object creation can *definitely* cause side effects. So we initially - // mark this as something invalid. We allow it as a valid expr if all - // the sub arguments are valid terms. - expressionType = ExpressionType.Invalid; + // Ask our subexpression for terms + AddSubExpressionTerms(castExpression.Expression, terms, ref flags); - if (objectionCreationExpression.ArgumentList != null) - { - var flags = ExpressionType.Invalid; - AddArgumentTerms(objectionCreationExpression.ArgumentList, terms, ref flags); - - // If all arguments are terms, then this is possibly a valid expr that can be used - // somewhere higher in the stack. - if (IsValidTerm(flags)) - { - expressionType = ExpressionType.ValidExpression; - } - } + // Is our expression a valid term? + AddIfValidTerm(castExpression.Expression, flags, terms); + + // If the subexpression is a valid term, so is the cast expression + expressionType = flags; + } + + private static void AddMemberAccessExpressionTerms(MemberAccessExpressionSyntax memberAccessExpression, IList terms, ref ExpressionType expressionType) + { + var flags = ExpressionType.Invalid; + + // These operators always have a RHS of a name node, which we know would + // "claim" to be a valid term, but is not valid without the LHS present. + // So, we don't bother collecting anything from the RHS... + AddSubExpressionTerms(memberAccessExpression.Expression, terms, ref flags); + + // If the LHS says it's a valid term, then we add it ONLY if our PARENT + // is NOT another dot/arrow. This allows the expression 'a.b.c.d' to + // add both 'a.b.c.d' and 'a.b.c', but not 'a.b' and 'a'. + if (IsValidTerm(flags) && + memberAccessExpression.Parent?.Kind() is not SyntaxKind.SimpleMemberAccessExpression and not SyntaxKind.PointerMemberAccessExpression) + { + terms.Add(ConvertToString(memberAccessExpression.Expression)); } - private static void AddArrayCreationExpressionTerms( - ArrayCreationExpressionSyntax arrayCreationExpression, - IList terms, - ref ExpressionType expressionType) + // And this expression itself is a valid term if the LHS is a valid + // expression, and its PARENT is not an invocation. + if (IsValidExpression(flags) && + !memberAccessExpression.IsParentKind(SyntaxKind.InvocationExpression)) + { + expressionType = ExpressionType.ValidTerm; + } + else { - var validTerm = true; + expressionType = ExpressionType.ValidExpression; + } + } - if (arrayCreationExpression.Initializer != null) - { - var flags = ExpressionType.Invalid; - arrayCreationExpression.Initializer.Expressions.Do(e => AddSubExpressionTerms(e, terms, ref flags)); + private static void AddObjectCreationExpressionTerms(ObjectCreationExpressionSyntax objectionCreationExpression, IList terms, ref ExpressionType expressionType) + { + // Object creation can *definitely* cause side effects. So we initially + // mark this as something invalid. We allow it as a valid expr if all + // the sub arguments are valid terms. + expressionType = ExpressionType.Invalid; - validTerm &= IsValidTerm(flags); - } + if (objectionCreationExpression.ArgumentList != null) + { + var flags = ExpressionType.Invalid; + AddArgumentTerms(objectionCreationExpression.ArgumentList, terms, ref flags); - if (validTerm) + // If all arguments are terms, then this is possibly a valid expr that can be used + // somewhere higher in the stack. + if (IsValidTerm(flags)) { expressionType = ExpressionType.ValidExpression; } - else - { - expressionType = ExpressionType.Invalid; - } } + } - private static void AddInvocationExpressionTerms(InvocationExpressionSyntax invocationExpression, IList terms, ref ExpressionType expressionType) - { -#pragma warning disable IDE0059 // Unnecessary assignment of a value - // Invocations definitely have side effects. So we assume this - // is invalid initially; - expressionType = ExpressionType.Invalid; -#pragma warning restore IDE0059 // Unnecessary assignment of a value - ExpressionType leftFlags = ExpressionType.Invalid, rightFlags = ExpressionType.Invalid; - - AddSubExpressionTerms(invocationExpression.Expression, terms, ref leftFlags); - AddArgumentTerms(invocationExpression.ArgumentList, terms, ref rightFlags); + private static void AddArrayCreationExpressionTerms( + ArrayCreationExpressionSyntax arrayCreationExpression, + IList terms, + ref ExpressionType expressionType) + { + var validTerm = true; - AddIfValidTerm(invocationExpression.Expression, leftFlags, terms); + if (arrayCreationExpression.Initializer != null) + { + var flags = ExpressionType.Invalid; + arrayCreationExpression.Initializer.Expressions.Do(e => AddSubExpressionTerms(e, terms, ref flags)); - // We're valid if both children are... - expressionType = (leftFlags & rightFlags) & ExpressionType.ValidExpression; + validTerm &= IsValidTerm(flags); } - private static void AddPrefixUnaryExpressionTerms(PrefixUnaryExpressionSyntax prefixUnaryExpression, IList terms, ref ExpressionType expressionType) + if (validTerm) + { + expressionType = ExpressionType.ValidExpression; + } + else { expressionType = ExpressionType.Invalid; - var flags = ExpressionType.Invalid; + } + } - // Ask our subexpression for terms - AddSubExpressionTerms(prefixUnaryExpression.Operand, terms, ref flags); + private static void AddInvocationExpressionTerms(InvocationExpressionSyntax invocationExpression, IList terms, ref ExpressionType expressionType) + { +#pragma warning disable IDE0059 // Unnecessary assignment of a value + // Invocations definitely have side effects. So we assume this + // is invalid initially; + expressionType = ExpressionType.Invalid; +#pragma warning restore IDE0059 // Unnecessary assignment of a value + ExpressionType leftFlags = ExpressionType.Invalid, rightFlags = ExpressionType.Invalid; - // Is our expression a valid term? - AddIfValidTerm(prefixUnaryExpression.Operand, flags, terms); + AddSubExpressionTerms(invocationExpression.Expression, terms, ref leftFlags); + AddArgumentTerms(invocationExpression.ArgumentList, terms, ref rightFlags); - if (prefixUnaryExpression.Kind() is SyntaxKind.LogicalNotExpression or SyntaxKind.BitwiseNotExpression or SyntaxKind.UnaryMinusExpression or SyntaxKind.UnaryPlusExpression) - { - // We're a valid expression if our subexpression is... - expressionType = flags & ExpressionType.ValidExpression; - } - } + AddIfValidTerm(invocationExpression.Expression, leftFlags, terms); - private static void AddAwaitExpressionTerms(AwaitExpressionSyntax awaitExpression, IList terms, ref ExpressionType expressionType) - { - expressionType = ExpressionType.Invalid; - var flags = ExpressionType.Invalid; + // We're valid if both children are... + expressionType = (leftFlags & rightFlags) & ExpressionType.ValidExpression; + } - // Ask our subexpression for terms - AddSubExpressionTerms(awaitExpression.Expression, terms, ref flags); + private static void AddPrefixUnaryExpressionTerms(PrefixUnaryExpressionSyntax prefixUnaryExpression, IList terms, ref ExpressionType expressionType) + { + expressionType = ExpressionType.Invalid; + var flags = ExpressionType.Invalid; - // Is our expression a valid term? - AddIfValidTerm(awaitExpression.Expression, flags, terms); - } + // Ask our subexpression for terms + AddSubExpressionTerms(prefixUnaryExpression.Operand, terms, ref flags); - private static void AddPostfixUnaryExpressionTerms(PostfixUnaryExpressionSyntax postfixUnaryExpression, IList terms, ref ExpressionType expressionType) + // Is our expression a valid term? + AddIfValidTerm(prefixUnaryExpression.Operand, flags, terms); + + if (prefixUnaryExpression.Kind() is SyntaxKind.LogicalNotExpression or SyntaxKind.BitwiseNotExpression or SyntaxKind.UnaryMinusExpression or SyntaxKind.UnaryPlusExpression) { - // ++ and -- are the only postfix operators. Since they always have side - // effects, we never consider this an expression. - expressionType = ExpressionType.Invalid; + // We're a valid expression if our subexpression is... + expressionType = flags & ExpressionType.ValidExpression; + } + } - var flags = ExpressionType.Invalid; + private static void AddAwaitExpressionTerms(AwaitExpressionSyntax awaitExpression, IList terms, ref ExpressionType expressionType) + { + expressionType = ExpressionType.Invalid; + var flags = ExpressionType.Invalid; - // Ask our subexpression for terms - AddSubExpressionTerms(postfixUnaryExpression.Operand, terms, ref flags); + // Ask our subexpression for terms + AddSubExpressionTerms(awaitExpression.Expression, terms, ref flags); - // Is our expression a valid term? - AddIfValidTerm(postfixUnaryExpression.Operand, flags, terms); - } + // Is our expression a valid term? + AddIfValidTerm(awaitExpression.Expression, flags, terms); + } - private static void AddConditionalExpressionTerms(ConditionalExpressionSyntax conditionalExpression, IList terms, ref ExpressionType expressionType) - { - ExpressionType conditionFlags = ExpressionType.Invalid, trueFlags = ExpressionType.Invalid, falseFlags = ExpressionType.Invalid; + private static void AddPostfixUnaryExpressionTerms(PostfixUnaryExpressionSyntax postfixUnaryExpression, IList terms, ref ExpressionType expressionType) + { + // ++ and -- are the only postfix operators. Since they always have side + // effects, we never consider this an expression. + expressionType = ExpressionType.Invalid; - AddSubExpressionTerms(conditionalExpression.Condition, terms, ref conditionFlags); - AddSubExpressionTerms(conditionalExpression.WhenTrue, terms, ref trueFlags); - AddSubExpressionTerms(conditionalExpression.WhenFalse, terms, ref falseFlags); + var flags = ExpressionType.Invalid; - AddIfValidTerm(conditionalExpression.Condition, conditionFlags, terms); - AddIfValidTerm(conditionalExpression.WhenTrue, trueFlags, terms); - AddIfValidTerm(conditionalExpression.WhenFalse, falseFlags, terms); + // Ask our subexpression for terms + AddSubExpressionTerms(postfixUnaryExpression.Operand, terms, ref flags); - // We're valid if all children are... - expressionType = (conditionFlags & trueFlags & falseFlags) & ExpressionType.ValidExpression; - } + // Is our expression a valid term? + AddIfValidTerm(postfixUnaryExpression.Operand, flags, terms); + } - private static void AddBinaryExpressionTerms(ExpressionSyntax binaryExpression, ExpressionSyntax left, ExpressionSyntax right, IList terms, ref ExpressionType expressionType) - { - ExpressionType leftFlags = ExpressionType.Invalid, rightFlags = ExpressionType.Invalid; + private static void AddConditionalExpressionTerms(ConditionalExpressionSyntax conditionalExpression, IList terms, ref ExpressionType expressionType) + { + ExpressionType conditionFlags = ExpressionType.Invalid, trueFlags = ExpressionType.Invalid, falseFlags = ExpressionType.Invalid; - AddSubExpressionTerms(left, terms, ref leftFlags); - AddSubExpressionTerms(right, terms, ref rightFlags); + AddSubExpressionTerms(conditionalExpression.Condition, terms, ref conditionFlags); + AddSubExpressionTerms(conditionalExpression.WhenTrue, terms, ref trueFlags); + AddSubExpressionTerms(conditionalExpression.WhenFalse, terms, ref falseFlags); - if (IsValidTerm(leftFlags)) - { - terms.Add(ConvertToString(left)); - } + AddIfValidTerm(conditionalExpression.Condition, conditionFlags, terms); + AddIfValidTerm(conditionalExpression.WhenTrue, trueFlags, terms); + AddIfValidTerm(conditionalExpression.WhenFalse, falseFlags, terms); - if (IsValidTerm(rightFlags)) - { - terms.Add(ConvertToString(right)); - } + // We're valid if all children are... + expressionType = (conditionFlags & trueFlags & falseFlags) & ExpressionType.ValidExpression; + } - // Many sorts of binops (like +=) will definitely have side effects. We only - // consider this valid if it's a simple expression like +, -, etc. + private static void AddBinaryExpressionTerms(ExpressionSyntax binaryExpression, ExpressionSyntax left, ExpressionSyntax right, IList terms, ref ExpressionType expressionType) + { + ExpressionType leftFlags = ExpressionType.Invalid, rightFlags = ExpressionType.Invalid; - switch (binaryExpression.Kind()) - { - case SyntaxKind.AddExpression: - case SyntaxKind.SubtractExpression: - case SyntaxKind.MultiplyExpression: - case SyntaxKind.DivideExpression: - case SyntaxKind.ModuloExpression: - case SyntaxKind.LeftShiftExpression: - case SyntaxKind.RightShiftExpression: - case SyntaxKind.LogicalOrExpression: - case SyntaxKind.LogicalAndExpression: - case SyntaxKind.BitwiseOrExpression: - case SyntaxKind.BitwiseAndExpression: - case SyntaxKind.ExclusiveOrExpression: - case SyntaxKind.EqualsExpression: - case SyntaxKind.NotEqualsExpression: - case SyntaxKind.LessThanExpression: - case SyntaxKind.LessThanOrEqualExpression: - case SyntaxKind.GreaterThanExpression: - case SyntaxKind.GreaterThanOrEqualExpression: - case SyntaxKind.IsExpression: - case SyntaxKind.AsExpression: - case SyntaxKind.CoalesceExpression: - // We're valid if both children are... - expressionType = (leftFlags & rightFlags) & ExpressionType.ValidExpression; - return; - - default: - expressionType = ExpressionType.Invalid; - return; - } + AddSubExpressionTerms(left, terms, ref leftFlags); + AddSubExpressionTerms(right, terms, ref rightFlags); + + if (IsValidTerm(leftFlags)) + { + terms.Add(ConvertToString(left)); } - private static void AddArgumentTerms(ArgumentListSyntax argumentList, IList terms, ref ExpressionType expressionType) + if (IsValidTerm(rightFlags)) { - var validExpr = true; - var validTerm = true; + terms.Add(ConvertToString(right)); + } - // Process the list of expressions. This is probably a list of - // arguments to a function call(or a list of array index expressions) - foreach (var arg in argumentList.Arguments) - { - var flags = ExpressionType.Invalid; + // Many sorts of binops (like +=) will definitely have side effects. We only + // consider this valid if it's a simple expression like +, -, etc. - AddSubExpressionTerms(arg.Expression, terms, ref flags); - if (IsValidTerm(flags)) - { - terms.Add(ConvertToString(arg.Expression)); - } + switch (binaryExpression.Kind()) + { + case SyntaxKind.AddExpression: + case SyntaxKind.SubtractExpression: + case SyntaxKind.MultiplyExpression: + case SyntaxKind.DivideExpression: + case SyntaxKind.ModuloExpression: + case SyntaxKind.LeftShiftExpression: + case SyntaxKind.RightShiftExpression: + case SyntaxKind.LogicalOrExpression: + case SyntaxKind.LogicalAndExpression: + case SyntaxKind.BitwiseOrExpression: + case SyntaxKind.BitwiseAndExpression: + case SyntaxKind.ExclusiveOrExpression: + case SyntaxKind.EqualsExpression: + case SyntaxKind.NotEqualsExpression: + case SyntaxKind.LessThanExpression: + case SyntaxKind.LessThanOrEqualExpression: + case SyntaxKind.GreaterThanExpression: + case SyntaxKind.GreaterThanOrEqualExpression: + case SyntaxKind.IsExpression: + case SyntaxKind.AsExpression: + case SyntaxKind.CoalesceExpression: + // We're valid if both children are... + expressionType = (leftFlags & rightFlags) & ExpressionType.ValidExpression; + return; - validExpr &= IsValidExpression(flags); - validTerm &= IsValidTerm(flags); + default: + expressionType = ExpressionType.Invalid; + return; + } + } + + private static void AddArgumentTerms(ArgumentListSyntax argumentList, IList terms, ref ExpressionType expressionType) + { + var validExpr = true; + var validTerm = true; + + // Process the list of expressions. This is probably a list of + // arguments to a function call(or a list of array index expressions) + foreach (var arg in argumentList.Arguments) + { + var flags = ExpressionType.Invalid; + + AddSubExpressionTerms(arg.Expression, terms, ref flags); + if (IsValidTerm(flags)) + { + terms.Add(ConvertToString(arg.Expression)); } - // We're never a valid term if all arguments were valid terms. If not, we're a valid - // expression if all arguments where. Otherwise, we're just invalid. - expressionType = validTerm - ? ExpressionType.ValidTerm - : validExpr - ? ExpressionType.ValidExpression : ExpressionType.Invalid; + validExpr &= IsValidExpression(flags); + validTerm &= IsValidTerm(flags); } + + // We're never a valid term if all arguments were valid terms. If not, we're a valid + // expression if all arguments where. Otherwise, we're just invalid. + expressionType = validTerm + ? ExpressionType.ValidTerm + : validExpr + ? ExpressionType.ValidExpression : ExpressionType.Invalid; } } diff --git a/src/Features/CSharp/Portable/Debugging/DataTipInfoGetter.cs b/src/Features/CSharp/Portable/Debugging/DataTipInfoGetter.cs index 90d6c390519ab..c6068148d8f59 100644 --- a/src/Features/CSharp/Portable/Debugging/DataTipInfoGetter.cs +++ b/src/Features/CSharp/Portable/Debugging/DataTipInfoGetter.cs @@ -14,82 +14,81 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Debugging +namespace Microsoft.CodeAnalysis.CSharp.Debugging; + +internal static class DataTipInfoGetter { - internal static class DataTipInfoGetter + internal static async Task GetInfoAsync(Document document, int position, CancellationToken cancellationToken) { - internal static async Task GetInfoAsync(Document document, int position, CancellationToken cancellationToken) + try { - try + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root == null) { - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (root == null) - { - return default; - } - - var token = root.FindToken(position); - - if (token.Parent is not ExpressionSyntax expression) - { - return token.IsKind(SyntaxKind.IdentifierToken) - ? new DebugDataTipInfo(token.Span, text: null) - : default; - } + return default; + } - if (expression.IsAnyLiteralExpression()) - { - // If the user hovers over a literal, give them a DataTip for the type of the - // literal they're hovering over. - // Partial semantics should always be sufficient because the (unconverted) type - // of a literal can always easily be determined. - var (_, semanticModel) = await document.GetFullOrPartialSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var type = semanticModel.GetTypeInfo(expression, cancellationToken).Type; - return type == null - ? default - : new DebugDataTipInfo(expression.Span, type.ToNameDisplayString()); - } + var token = root.FindToken(position); - if (expression.IsRightSideOfDotOrArrow()) - { - var curr = expression.GetRootConditionalAccessExpression() ?? expression; - if (curr == expression) - { - // NB: Parent.Span, not Span as below. - return new DebugDataTipInfo(expression.Parent.Span, text: null); - } + if (token.Parent is not ExpressionSyntax expression) + { + return token.IsKind(SyntaxKind.IdentifierToken) + ? new DebugDataTipInfo(token.Span, text: null) + : default; + } - // NOTE: There may not be an ExpressionSyntax corresponding to the range we want. - // For example, for input a?.$$B?.C, we want span [|a?.B|]?.C. - return new DebugDataTipInfo(TextSpan.FromBounds(curr.SpanStart, expression.Span.End), text: null); - } + if (expression.IsAnyLiteralExpression()) + { + // If the user hovers over a literal, give them a DataTip for the type of the + // literal they're hovering over. + // Partial semantics should always be sufficient because the (unconverted) type + // of a literal can always easily be determined. + var (_, semanticModel) = await document.GetFullOrPartialSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var type = semanticModel.GetTypeInfo(expression, cancellationToken).Type; + return type == null + ? default + : new DebugDataTipInfo(expression.Span, type.ToNameDisplayString()); + } - // NOTE(cyrusn): This behavior is to mimic what we did in Dev10, I'm not sure if it's - // necessary or not. - if (expression is InvocationExpressionSyntax invocation) + if (expression.IsRightSideOfDotOrArrow()) + { + var curr = expression.GetRootConditionalAccessExpression() ?? expression; + if (curr == expression) { - expression = invocation.Expression; + // NB: Parent.Span, not Span as below. + return new DebugDataTipInfo(expression.Parent.Span, text: null); } - string textOpt = null; - if (expression is TypeSyntax typeSyntax && typeSyntax.IsVar) - { - // If the user is hovering over 'var', then pass back the full type name that 'var' - // binds to. - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var type = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type; - if (type != null) - { - textOpt = type.ToNameDisplayString(); - } - } + // NOTE: There may not be an ExpressionSyntax corresponding to the range we want. + // For example, for input a?.$$B?.C, we want span [|a?.B|]?.C. + return new DebugDataTipInfo(TextSpan.FromBounds(curr.SpanStart, expression.Span.End), text: null); + } - return new DebugDataTipInfo(expression.Span, textOpt); + // NOTE(cyrusn): This behavior is to mimic what we did in Dev10, I'm not sure if it's + // necessary or not. + if (expression is InvocationExpressionSyntax invocation) + { + expression = invocation.Expression; } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + + string textOpt = null; + if (expression is TypeSyntax typeSyntax && typeSyntax.IsVar) { - return default; + // If the user is hovering over 'var', then pass back the full type name that 'var' + // binds to. + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var type = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type; + if (type != null) + { + textOpt = type.ToNameDisplayString(); + } } + + return new DebugDataTipInfo(expression.Span, textOpt); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + return default; } } } diff --git a/src/Features/CSharp/Portable/Debugging/LocationInfoGetter.cs b/src/Features/CSharp/Portable/Debugging/LocationInfoGetter.cs index 04d38e038c392..ca2659203fde6 100644 --- a/src/Features/CSharp/Portable/Debugging/LocationInfoGetter.cs +++ b/src/Features/CSharp/Portable/Debugging/LocationInfoGetter.cs @@ -11,59 +11,58 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Debugging +namespace Microsoft.CodeAnalysis.CSharp.Debugging; + +internal static class LocationInfoGetter { - internal static class LocationInfoGetter + internal static async Task GetInfoAsync(Document document, int position, CancellationToken cancellationToken) { - internal static async Task GetInfoAsync(Document document, int position, CancellationToken cancellationToken) + // PERF: This method will be called synchronously on the UI thread for every breakpoint in the solution. + // Therefore, it is important that we make this call as cheap as possible. Rather than constructing a + // containing Symbol and using ToDisplayString (which might be more *correct*), we'll just do the best we + // can with Syntax. This approach is capable of providing parity with the pre-Roslyn implementation. + var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var syntaxFactsService = document.GetLanguageService(); + var memberDeclaration = syntaxFactsService.GetContainingMemberDeclaration(root, position, useFullSpan: true); + + // It might be reasonable to return an empty Name and a LineOffset from the beginning of the + // file for GlobalStatements. However, the only known caller (Breakpoints Window) doesn't + // appear to consume this information, so we'll just return the simplest thing (no location). + if ((memberDeclaration == null) || (memberDeclaration.Kind() == SyntaxKind.GlobalStatement)) { - // PERF: This method will be called synchronously on the UI thread for every breakpoint in the solution. - // Therefore, it is important that we make this call as cheap as possible. Rather than constructing a - // containing Symbol and using ToDisplayString (which might be more *correct*), we'll just do the best we - // can with Syntax. This approach is capable of providing parity with the pre-Roslyn implementation. - var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var syntaxFactsService = document.GetLanguageService(); - var memberDeclaration = syntaxFactsService.GetContainingMemberDeclaration(root, position, useFullSpan: true); + return default; + } - // It might be reasonable to return an empty Name and a LineOffset from the beginning of the - // file for GlobalStatements. However, the only known caller (Breakpoints Window) doesn't - // appear to consume this information, so we'll just return the simplest thing (no location). - if ((memberDeclaration == null) || (memberDeclaration.Kind() == SyntaxKind.GlobalStatement)) - { - return default; - } + // field or event field declarations may contain multiple variable declarators. Try finding the correct one. + // If the position does not point to one, try using the first one. + VariableDeclaratorSyntax fieldDeclarator = null; + if (memberDeclaration.Kind() is SyntaxKind.FieldDeclaration or SyntaxKind.EventFieldDeclaration) + { + var variableDeclarators = ((BaseFieldDeclarationSyntax)memberDeclaration).Declaration.Variables; - // field or event field declarations may contain multiple variable declarators. Try finding the correct one. - // If the position does not point to one, try using the first one. - VariableDeclaratorSyntax fieldDeclarator = null; - if (memberDeclaration.Kind() is SyntaxKind.FieldDeclaration or SyntaxKind.EventFieldDeclaration) + foreach (var declarator in variableDeclarators) { - var variableDeclarators = ((BaseFieldDeclarationSyntax)memberDeclaration).Declaration.Variables; - - foreach (var declarator in variableDeclarators) + if (declarator.FullSpan.Contains(position)) { - if (declarator.FullSpan.Contains(position)) - { - fieldDeclarator = declarator; - break; - } + fieldDeclarator = declarator; + break; } - - fieldDeclarator ??= variableDeclarators.Count > 0 ? variableDeclarators[0] : null; } - var name = syntaxFactsService.GetDisplayName(fieldDeclarator ?? memberDeclaration, - DisplayNameOptions.IncludeNamespaces | - DisplayNameOptions.IncludeParameters); + fieldDeclarator ??= variableDeclarators.Count > 0 ? variableDeclarators[0] : null; + } - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var lineNumber = text.Lines.GetLineFromPosition(position).LineNumber; - var accessor = memberDeclaration.GetAncestorOrThis(); - var memberLine = text.Lines.GetLineFromPosition(accessor?.SpanStart ?? memberDeclaration.SpanStart).LineNumber; - var lineOffset = lineNumber - memberLine; + var name = syntaxFactsService.GetDisplayName(fieldDeclarator ?? memberDeclaration, + DisplayNameOptions.IncludeNamespaces | + DisplayNameOptions.IncludeParameters); - return new DebugLocationInfo(name, lineOffset); - } + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var lineNumber = text.Lines.GetLineFromPosition(position).LineNumber; + var accessor = memberDeclaration.GetAncestorOrThis(); + var memberLine = text.Lines.GetLineFromPosition(accessor?.SpanStart ?? memberDeclaration.SpanStart).LineNumber; + var lineOffset = lineNumber - memberLine; + + return new DebugLocationInfo(name, lineOffset); } } diff --git a/src/Features/CSharp/Portable/DecompiledSource/CSharpDecompiledSourceFormattingRule.cs b/src/Features/CSharp/Portable/DecompiledSource/CSharpDecompiledSourceFormattingRule.cs index f71bb31bf957b..bb8eb59badd6f 100644 --- a/src/Features/CSharp/Portable/DecompiledSource/CSharpDecompiledSourceFormattingRule.cs +++ b/src/Features/CSharp/Portable/DecompiledSource/CSharpDecompiledSourceFormattingRule.cs @@ -6,48 +6,47 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting.Rules; -namespace Microsoft.CodeAnalysis.CSharp.DecompiledSource +namespace Microsoft.CodeAnalysis.CSharp.DecompiledSource; + +internal class CSharpDecompiledSourceFormattingRule : AbstractFormattingRule { - internal class CSharpDecompiledSourceFormattingRule : AbstractFormattingRule - { - public static readonly AbstractFormattingRule Instance = new CSharpDecompiledSourceFormattingRule(); + public static readonly AbstractFormattingRule Instance = new CSharpDecompiledSourceFormattingRule(); - private CSharpDecompiledSourceFormattingRule() - { - } + private CSharpDecompiledSourceFormattingRule() + { + } - public override AdjustNewLinesOperation? GetAdjustNewLinesOperation( - in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) - { - var operation = GetAdjustNewLinesOperation(previousToken, currentToken); - return operation ?? nextOperation.Invoke(in previousToken, in currentToken); - } + public override AdjustNewLinesOperation? GetAdjustNewLinesOperation( + in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + { + var operation = GetAdjustNewLinesOperation(previousToken, currentToken); + return operation ?? nextOperation.Invoke(in previousToken, in currentToken); + } - private static AdjustNewLinesOperation? GetAdjustNewLinesOperation(SyntaxToken previousToken, SyntaxToken currentToken) - { - // To help code not look too tightly packed, we place a blank line after every statement that ends with a - // `}` (unless it's also followed by another `}`). - if (previousToken.Kind() != SyntaxKind.CloseBraceToken) - return null; + private static AdjustNewLinesOperation? GetAdjustNewLinesOperation(SyntaxToken previousToken, SyntaxToken currentToken) + { + // To help code not look too tightly packed, we place a blank line after every statement that ends with a + // `}` (unless it's also followed by another `}`). + if (previousToken.Kind() != SyntaxKind.CloseBraceToken) + return null; - if (currentToken.Kind() == SyntaxKind.CloseBraceToken) - return null; + if (currentToken.Kind() == SyntaxKind.CloseBraceToken) + return null; - if (previousToken.Parent == null || currentToken.Parent == null) - return null; + if (previousToken.Parent == null || currentToken.Parent == null) + return null; - var previousStatement = previousToken.Parent.FirstAncestorOrSelf(); - var nextStatement = currentToken.Parent.FirstAncestorOrSelf(); + var previousStatement = previousToken.Parent.FirstAncestorOrSelf(); + var nextStatement = currentToken.Parent.FirstAncestorOrSelf(); - if (previousStatement == null || nextStatement == null || previousStatement == nextStatement) - return null; + if (previousStatement == null || nextStatement == null || previousStatement == nextStatement) + return null; - // Ensure that we're only updating the whitespace between statements. - if (previousStatement.GetLastToken() != previousToken || nextStatement.GetFirstToken() != currentToken) - return null; + // Ensure that we're only updating the whitespace between statements. + if (previousStatement.GetLastToken() != previousToken || nextStatement.GetFirstToken() != currentToken) + return null; - // Ensure a blank line between these two. - return FormattingOperations.CreateAdjustNewLinesOperation(2, AdjustNewLinesOption.ForceLines); - } + // Ensure a blank line between these two. + return FormattingOperations.CreateAdjustNewLinesOperation(2, AdjustNewLinesOption.ForceLines); } } diff --git a/src/Features/CSharp/Portable/DecompiledSource/CSharpDecompiledSourceService.cs b/src/Features/CSharp/Portable/DecompiledSource/CSharpDecompiledSourceService.cs index 7ceec38be0b5e..9ecb17847e198 100644 --- a/src/Features/CSharp/Portable/DecompiledSource/CSharpDecompiledSourceService.cs +++ b/src/Features/CSharp/Portable/DecompiledSource/CSharpDecompiledSourceService.cs @@ -19,115 +19,114 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.DecompiledSource +namespace Microsoft.CodeAnalysis.CSharp.DecompiledSource; + +[ExportLanguageService(typeof(IDecompiledSourceService), LanguageNames.CSharp), Shared] +internal class CSharpDecompiledSourceService : IDecompiledSourceService { - [ExportLanguageService(typeof(IDecompiledSourceService), LanguageNames.CSharp), Shared] - internal class CSharpDecompiledSourceService : IDecompiledSourceService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpDecompiledSourceService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpDecompiledSourceService() - { - } + } - public async Task AddSourceToAsync(Document document, Compilation symbolCompilation, ISymbol symbol, MetadataReference? metadataReference, string? assemblyLocation, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken) - { - // Get the name of the type the symbol is in - var containingOrThis = symbol.GetContainingTypeOrThis(); - var fullName = GetFullReflectionName(containingOrThis); + public async Task AddSourceToAsync(Document document, Compilation symbolCompilation, ISymbol symbol, MetadataReference? metadataReference, string? assemblyLocation, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken) + { + // Get the name of the type the symbol is in + var containingOrThis = symbol.GetContainingTypeOrThis(); + var fullName = GetFullReflectionName(containingOrThis); - // Decompile - var decompilationService = document.GetRequiredLanguageService(); - var decompiledDocument = decompilationService.PerformDecompilation(document, fullName, symbolCompilation, metadataReference, assemblyLocation); + // Decompile + var decompilationService = document.GetRequiredLanguageService(); + var decompiledDocument = decompilationService.PerformDecompilation(document, fullName, symbolCompilation, metadataReference, assemblyLocation); - if (decompiledDocument is null) - return null; + if (decompiledDocument is null) + return null; - document = decompiledDocument; + document = decompiledDocument; - document = await AddAssemblyInfoRegionAsync(document, symbol, decompilationService, cancellationToken).ConfigureAwait(false); + document = await AddAssemblyInfoRegionAsync(document, symbol, decompilationService, cancellationToken).ConfigureAwait(false); - // Convert XML doc comments to regular comments, just like MAS - var docCommentFormattingService = document.GetRequiredLanguageService(); - document = await ConvertDocCommentsToRegularCommentsAsync(document, docCommentFormattingService, cancellationToken).ConfigureAwait(false); + // Convert XML doc comments to regular comments, just like MAS + var docCommentFormattingService = document.GetRequiredLanguageService(); + document = await ConvertDocCommentsToRegularCommentsAsync(document, docCommentFormattingService, cancellationToken).ConfigureAwait(false); - return await FormatDocumentAsync(document, formattingOptions, cancellationToken).ConfigureAwait(false); - } + return await FormatDocumentAsync(document, formattingOptions, cancellationToken).ConfigureAwait(false); + } - public static async Task FormatDocumentAsync(Document document, SyntaxFormattingOptions options, CancellationToken cancellationToken) - { - var node = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + public static async Task FormatDocumentAsync(Document document, SyntaxFormattingOptions options, CancellationToken cancellationToken) + { + var node = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // Apply formatting rules - var formattedDoc = await Formatter.FormatAsync( - document, - SpecializedCollections.SingletonEnumerable(node.FullSpan), - options, - CSharpDecompiledSourceFormattingRule.Instance.Concat(Formatter.GetDefaultFormattingRules(document)), - cancellationToken).ConfigureAwait(false); + // Apply formatting rules + var formattedDoc = await Formatter.FormatAsync( + document, + SpecializedCollections.SingletonEnumerable(node.FullSpan), + options, + CSharpDecompiledSourceFormattingRule.Instance.Concat(Formatter.GetDefaultFormattingRules(document)), + cancellationToken).ConfigureAwait(false); - return formattedDoc; - } + return formattedDoc; + } - private static async Task AddAssemblyInfoRegionAsync(Document document, ISymbol symbol, IDecompilationService decompilationService, CancellationToken cancellationToken) - { - var assemblyInfo = MetadataAsSourceHelpers.GetAssemblyInfo(symbol.ContainingAssembly); - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var assemblyPath = MetadataAsSourceHelpers.GetAssemblyDisplay(compilation, symbol.ContainingAssembly); - - var regionTrivia = SyntaxFactory.RegionDirectiveTrivia(true) - .WithTrailingTrivia(new[] { SyntaxFactory.Space, SyntaxFactory.PreprocessingMessage(assemblyInfo) }); - - var decompilerVersion = decompilationService.GetDecompilerVersion(); - - var oldRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newRoot = oldRoot.WithLeadingTrivia(new[] - { - SyntaxFactory.Trivia(regionTrivia), - SyntaxFactory.CarriageReturnLineFeed, - SyntaxFactory.Comment("// " + assemblyPath), - SyntaxFactory.CarriageReturnLineFeed, - SyntaxFactory.Comment($"// Decompiled with ICSharpCode.Decompiler {decompilerVersion.FileVersion}"), - SyntaxFactory.CarriageReturnLineFeed, - SyntaxFactory.Trivia(SyntaxFactory.EndRegionDirectiveTrivia(true)), - SyntaxFactory.CarriageReturnLineFeed, - SyntaxFactory.CarriageReturnLineFeed - }); - - return document.WithSyntaxRoot(newRoot); - } + private static async Task AddAssemblyInfoRegionAsync(Document document, ISymbol symbol, IDecompilationService decompilationService, CancellationToken cancellationToken) + { + var assemblyInfo = MetadataAsSourceHelpers.GetAssemblyInfo(symbol.ContainingAssembly); + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var assemblyPath = MetadataAsSourceHelpers.GetAssemblyDisplay(compilation, symbol.ContainingAssembly); - private static async Task ConvertDocCommentsToRegularCommentsAsync(Document document, IDocumentationCommentFormattingService docCommentFormattingService, CancellationToken cancellationToken) - { - var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var regionTrivia = SyntaxFactory.RegionDirectiveTrivia(true) + .WithTrailingTrivia(new[] { SyntaxFactory.Space, SyntaxFactory.PreprocessingMessage(assemblyInfo) }); - var newSyntaxRoot = DocCommentConverter.ConvertToRegularComments(syntaxRoot, docCommentFormattingService, cancellationToken); + var decompilerVersion = decompilationService.GetDecompilerVersion(); - return document.WithSyntaxRoot(newSyntaxRoot); - } + var oldRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = oldRoot.WithLeadingTrivia(new[] + { + SyntaxFactory.Trivia(regionTrivia), + SyntaxFactory.CarriageReturnLineFeed, + SyntaxFactory.Comment("// " + assemblyPath), + SyntaxFactory.CarriageReturnLineFeed, + SyntaxFactory.Comment($"// Decompiled with ICSharpCode.Decompiler {decompilerVersion.FileVersion}"), + SyntaxFactory.CarriageReturnLineFeed, + SyntaxFactory.Trivia(SyntaxFactory.EndRegionDirectiveTrivia(true)), + SyntaxFactory.CarriageReturnLineFeed, + SyntaxFactory.CarriageReturnLineFeed + }); + + return document.WithSyntaxRoot(newRoot); + } - private static string GetFullReflectionName(INamedTypeSymbol? containingType) - { - var containingTypeStack = new Stack(); - var containingNamespaceStack = new Stack(); + private static async Task ConvertDocCommentsToRegularCommentsAsync(Document document, IDocumentationCommentFormattingService docCommentFormattingService, CancellationToken cancellationToken) + { + var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - for (INamespaceOrTypeSymbol? symbol = containingType; - symbol is not null and not INamespaceSymbol { IsGlobalNamespace: true }; - symbol = (INamespaceOrTypeSymbol?)symbol.ContainingType ?? symbol.ContainingNamespace) - { - if (symbol.ContainingType is not null) - containingTypeStack.Push(symbol.MetadataName); - else - containingNamespaceStack.Push(symbol.MetadataName); - } - - var result = string.Join(".", containingNamespaceStack); - if (containingTypeStack.Any()) - { - result += "+" + string.Join("+", containingTypeStack); - } + var newSyntaxRoot = DocCommentConverter.ConvertToRegularComments(syntaxRoot, docCommentFormattingService, cancellationToken); + + return document.WithSyntaxRoot(newSyntaxRoot); + } + + private static string GetFullReflectionName(INamedTypeSymbol? containingType) + { + var containingTypeStack = new Stack(); + var containingNamespaceStack = new Stack(); + + for (INamespaceOrTypeSymbol? symbol = containingType; + symbol is not null and not INamespaceSymbol { IsGlobalNamespace: true }; + symbol = (INamespaceOrTypeSymbol?)symbol.ContainingType ?? symbol.ContainingNamespace) + { + if (symbol.ContainingType is not null) + containingTypeStack.Push(symbol.MetadataName); + else + containingNamespaceStack.Push(symbol.MetadataName); + } - return result; + var result = string.Join(".", containingNamespaceStack); + if (containingTypeStack.Any()) + { + result += "+" + string.Join("+", containingTypeStack); } + + return result; } } diff --git a/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpPreferFrameworkTypeDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpPreferFrameworkTypeDiagnosticAnalyzer.cs index 22b871104675d..75955db0bda9c 100644 --- a/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpPreferFrameworkTypeDiagnosticAnalyzer.cs +++ b/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpPreferFrameworkTypeDiagnosticAnalyzer.cs @@ -8,33 +8,32 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.PreferFrameworkType; -namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.Analyzers +namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpPreferFrameworkTypeDiagnosticAnalyzer : + PreferFrameworkTypeDiagnosticAnalyzerBase< + SyntaxKind, + ExpressionSyntax, + TypeSyntax, + IdentifierNameSyntax, + PredefinedTypeSyntax> { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpPreferFrameworkTypeDiagnosticAnalyzer : - PreferFrameworkTypeDiagnosticAnalyzerBase< - SyntaxKind, - ExpressionSyntax, - TypeSyntax, - IdentifierNameSyntax, - PredefinedTypeSyntax> - { - protected override ImmutableArray SyntaxKindsOfInterest { get; } = - [SyntaxKind.PredefinedType, SyntaxKind.IdentifierName]; + protected override ImmutableArray SyntaxKindsOfInterest { get; } = + [SyntaxKind.PredefinedType, SyntaxKind.IdentifierName]; - /// - /// every predefined type keyword except void can be replaced by its framework type in code. - /// - protected override bool IsPredefinedTypeReplaceableWithFrameworkType(PredefinedTypeSyntax node) - => node.Keyword.Kind() != SyntaxKind.VoidKeyword; + /// + /// every predefined type keyword except void can be replaced by its framework type in code. + /// + protected override bool IsPredefinedTypeReplaceableWithFrameworkType(PredefinedTypeSyntax node) + => node.Keyword.Kind() != SyntaxKind.VoidKeyword; - // Only offer to change nint->System.IntPtr when it would preserve semantics exactly. - protected override bool IsIdentifierNameReplaceableWithFrameworkType(SemanticModel semanticModel, IdentifierNameSyntax node) - => (node.IsNint || node.IsNuint) && - semanticModel.SyntaxTree.Options.LanguageVersion() >= LanguageVersion.CSharp9 && - semanticModel.Compilation.SupportsRuntimeCapability(RuntimeCapability.NumericIntPtr); + // Only offer to change nint->System.IntPtr when it would preserve semantics exactly. + protected override bool IsIdentifierNameReplaceableWithFrameworkType(SemanticModel semanticModel, IdentifierNameSyntax node) + => (node.IsNint || node.IsNuint) && + semanticModel.SyntaxTree.Options.LanguageVersion() >= LanguageVersion.CSharp9 && + semanticModel.Compilation.SupportsRuntimeCapability(RuntimeCapability.NumericIntPtr); - protected override bool IsInMemberAccessOrCrefReferenceContext(ExpressionSyntax node) - => node.IsDirectChildOfMemberAccessExpression() || node.InsideCrefReference(); - } + protected override bool IsInMemberAccessOrCrefReferenceContext(ExpressionSyntax node) + => node.IsDirectChildOfMemberAccessExpression() || node.InsideCrefReference(); } diff --git a/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpSimplifyTypeNamesDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpSimplifyTypeNamesDiagnosticAnalyzer.cs index 5a70dd3b9058c..405c77201fbab 100644 --- a/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpSimplifyTypeNamesDiagnosticAnalyzer.cs +++ b/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpSimplifyTypeNamesDiagnosticAnalyzer.cs @@ -18,122 +18,121 @@ using Microsoft.CodeAnalysis.SimplifyTypeNames; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.SimplifyTypeNames +namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.SimplifyTypeNames; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpSimplifyTypeNamesDiagnosticAnalyzer + : SimplifyTypeNamesDiagnosticAnalyzerBase { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpSimplifyTypeNamesDiagnosticAnalyzer - : SimplifyTypeNamesDiagnosticAnalyzerBase + private static readonly ImmutableArray s_kindsOfInterest = + [ + SyntaxKind.QualifiedName, + SyntaxKind.AliasQualifiedName, + SyntaxKind.GenericName, + SyntaxKind.IdentifierName, + SyntaxKind.SimpleMemberAccessExpression, + SyntaxKind.QualifiedCref, + ]; + + protected override bool IsIgnoredCodeBlock(SyntaxNode codeBlock) { - private static readonly ImmutableArray s_kindsOfInterest = - [ - SyntaxKind.QualifiedName, - SyntaxKind.AliasQualifiedName, - SyntaxKind.GenericName, - SyntaxKind.IdentifierName, - SyntaxKind.SimpleMemberAccessExpression, - SyntaxKind.QualifiedCref, - ]; - - protected override bool IsIgnoredCodeBlock(SyntaxNode codeBlock) - { - // Avoid analysis of compilation units and types in AnalyzeCodeBlock. These nodes appear in code block - // callbacks when they include attributes, but analysis of the node at this level would block more efficient - // analysis of descendant members. - return codeBlock.Kind() is - SyntaxKind.CompilationUnit or - SyntaxKind.ClassDeclaration or - SyntaxKind.RecordDeclaration or - SyntaxKind.StructDeclaration or - SyntaxKind.RecordStructDeclaration or - SyntaxKind.InterfaceDeclaration or - SyntaxKind.DelegateDeclaration or - SyntaxKind.EnumDeclaration; - } + // Avoid analysis of compilation units and types in AnalyzeCodeBlock. These nodes appear in code block + // callbacks when they include attributes, but analysis of the node at this level would block more efficient + // analysis of descendant members. + return codeBlock.Kind() is + SyntaxKind.CompilationUnit or + SyntaxKind.ClassDeclaration or + SyntaxKind.RecordDeclaration or + SyntaxKind.StructDeclaration or + SyntaxKind.RecordStructDeclaration or + SyntaxKind.InterfaceDeclaration or + SyntaxKind.DelegateDeclaration or + SyntaxKind.EnumDeclaration; + } - protected override ImmutableArray AnalyzeCodeBlock(CodeBlockAnalysisContext context, SyntaxNode root) - { - Debug.Assert(context.CodeBlock.DescendantNodesAndSelf().Contains(root)); + protected override ImmutableArray AnalyzeCodeBlock(CodeBlockAnalysisContext context, SyntaxNode root) + { + Debug.Assert(context.CodeBlock.DescendantNodesAndSelf().Contains(root)); - var semanticModel = context.SemanticModel; - var cancellationToken = context.CancellationToken; + var semanticModel = context.SemanticModel; + var cancellationToken = context.CancellationToken; - var options = context.GetCSharpAnalyzerOptions().GetSimplifierOptions(); - if (ShouldSkipAnalysis(context.FilterTree, context.Options, context.SemanticModel.Compilation.Options, GetAllNotifications(options), cancellationToken)) - return []; + var options = context.GetCSharpAnalyzerOptions().GetSimplifierOptions(); + if (ShouldSkipAnalysis(context.FilterTree, context.Options, context.SemanticModel.Compilation.Options, GetAllNotifications(options), cancellationToken)) + return []; - using var simplifier = new TypeSyntaxSimplifierWalker(this, semanticModel, options, ignoredSpans: null, cancellationToken); - simplifier.Visit(root); - return simplifier.Diagnostics; - } + using var simplifier = new TypeSyntaxSimplifierWalker(this, semanticModel, options, context.Options, ignoredSpans: null, cancellationToken); + simplifier.Visit(root); + return simplifier.Diagnostics; + } - protected override ImmutableArray AnalyzeSemanticModel(SemanticModelAnalysisContext context, SyntaxNode root, TextSpanIntervalTree? codeBlockIntervalTree) - { - var options = context.GetCSharpAnalyzerOptions().GetSimplifierOptions(); - if (ShouldSkipAnalysis(context.FilterTree, context.Options, context.SemanticModel.Compilation.Options, GetAllNotifications(options), context.CancellationToken)) - return []; + protected override ImmutableArray AnalyzeSemanticModel(SemanticModelAnalysisContext context, SyntaxNode root, TextSpanIntervalTree? codeBlockIntervalTree) + { + var options = context.GetCSharpAnalyzerOptions().GetSimplifierOptions(); + if (ShouldSkipAnalysis(context.FilterTree, context.Options, context.SemanticModel.Compilation.Options, GetAllNotifications(options), context.CancellationToken)) + return []; - var simplifier = new TypeSyntaxSimplifierWalker(this, context.SemanticModel, options, ignoredSpans: codeBlockIntervalTree, context.CancellationToken); - simplifier.Visit(root); - return simplifier.Diagnostics; - } + var simplifier = new TypeSyntaxSimplifierWalker(this, context.SemanticModel, options, context.Options, ignoredSpans: codeBlockIntervalTree, context.CancellationToken); + simplifier.Visit(root); + return simplifier.Diagnostics; + } - internal override bool IsCandidate(SyntaxNode node) - => node != null && s_kindsOfInterest.Contains(node.Kind()); + internal override bool IsCandidate(SyntaxNode node) + => node != null && s_kindsOfInterest.Contains(node.Kind()); - internal override bool CanSimplifyTypeNameExpression( - SemanticModel model, SyntaxNode node, CSharpSimplifierOptions options, - out TextSpan issueSpan, out string diagnosticId, out bool inDeclaration, - CancellationToken cancellationToken) + internal override bool CanSimplifyTypeNameExpression( + SemanticModel model, SyntaxNode node, CSharpSimplifierOptions options, + out TextSpan issueSpan, out string diagnosticId, out bool inDeclaration, + CancellationToken cancellationToken) + { + inDeclaration = false; + issueSpan = default; + diagnosticId = IDEDiagnosticIds.SimplifyNamesDiagnosticId; + + if (node is MemberAccessExpressionSyntax memberAccess && memberAccess.Expression.IsKind(SyntaxKind.ThisExpression)) { - inDeclaration = false; - issueSpan = default; - diagnosticId = IDEDiagnosticIds.SimplifyNamesDiagnosticId; + // don't bother analyzing "this.Goo" expressions. They will be analyzed by + // the CSharpSimplifyThisOrMeDiagnosticAnalyzer. + return false; + } - if (node is MemberAccessExpressionSyntax memberAccess && memberAccess.Expression.IsKind(SyntaxKind.ThisExpression)) - { - // don't bother analyzing "this.Goo" expressions. They will be analyzed by - // the CSharpSimplifyThisOrMeDiagnosticAnalyzer. + if (node.ContainsDiagnostics) + { + return false; + } + + SyntaxNode replacementSyntax; + if (node is QualifiedCrefSyntax crefSyntax) + { + if (!QualifiedCrefSimplifier.Instance.TrySimplify(crefSyntax, model, options, out var replacement, out issueSpan, cancellationToken)) return false; - } - if (node.ContainsDiagnostics) - { + replacementSyntax = replacement; + } + else + { + if (!ExpressionSimplifier.Instance.TrySimplify((ExpressionSyntax)node, model, options, out var replacement, out issueSpan, cancellationToken)) return false; - } - - SyntaxNode replacementSyntax; - if (node is QualifiedCrefSyntax crefSyntax) - { - if (!QualifiedCrefSimplifier.Instance.TrySimplify(crefSyntax, model, options, out var replacement, out issueSpan, cancellationToken)) - return false; - - replacementSyntax = replacement; - } - else - { - if (!ExpressionSimplifier.Instance.TrySimplify((ExpressionSyntax)node, model, options, out var replacement, out issueSpan, cancellationToken)) - return false; - - replacementSyntax = replacement; - } - - // set proper diagnostic ids. - if (replacementSyntax.HasAnnotations(nameof(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInDeclaration))) - { - inDeclaration = true; - diagnosticId = IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId; - } - else if (replacementSyntax.HasAnnotations(nameof(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess))) - { - inDeclaration = false; - diagnosticId = IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId; - } - else if (node.Kind() == SyntaxKind.SimpleMemberAccessExpression) - { - diagnosticId = IDEDiagnosticIds.SimplifyMemberAccessDiagnosticId; - } - - return true; + + replacementSyntax = replacement; + } + + // set proper diagnostic ids. + if (replacementSyntax.HasAnnotations(nameof(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInDeclaration))) + { + inDeclaration = true; + diagnosticId = IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId; + } + else if (replacementSyntax.HasAnnotations(nameof(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess))) + { + inDeclaration = false; + diagnosticId = IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId; + } + else if (node.Kind() == SyntaxKind.SimpleMemberAccessExpression) + { + diagnosticId = IDEDiagnosticIds.SimplifyMemberAccessDiagnosticId; } + + return true; } } diff --git a/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpUnboundIdentifiersDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpUnboundIdentifiersDiagnosticAnalyzer.cs index 74f4ee1e6bb49..3aee52f8b24d2 100644 --- a/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpUnboundIdentifiersDiagnosticAnalyzer.cs +++ b/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpUnboundIdentifiersDiagnosticAnalyzer.cs @@ -10,22 +10,21 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics.AddImport; -namespace Microsoft.CodeAnalysis.CSharp.Diagnostics +namespace Microsoft.CodeAnalysis.CSharp.Diagnostics; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpUnboundIdentifiersDiagnosticAnalyzer : UnboundIdentifiersDiagnosticAnalyzerBase { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpUnboundIdentifiersDiagnosticAnalyzer : UnboundIdentifiersDiagnosticAnalyzerBase - { - private readonly LocalizableString _nameNotInContextMessageFormat = - new LocalizableResourceString(nameof(CSharpFeaturesResources.The_name_0_does_not_exist_in_the_current_context), CSharpFeaturesResources.ResourceManager, typeof(CSharpFeaturesResources)); + private readonly LocalizableString _nameNotInContextMessageFormat = + new LocalizableResourceString(nameof(CSharpFeaturesResources.The_name_0_does_not_exist_in_the_current_context), CSharpFeaturesResources.ResourceManager, typeof(CSharpFeaturesResources)); - private static readonly ImmutableArray s_kindsOfInterest = [SyntaxKind.IncompleteMember]; + private static readonly ImmutableArray s_kindsOfInterest = [SyntaxKind.IncompleteMember]; - protected override ImmutableArray SyntaxKindsOfInterest => s_kindsOfInterest; + protected override ImmutableArray SyntaxKindsOfInterest => s_kindsOfInterest; - protected override DiagnosticDescriptor DiagnosticDescriptor - => GetDiagnosticDescriptor(IDEDiagnosticIds.UnboundIdentifierId, _nameNotInContextMessageFormat); + protected override DiagnosticDescriptor DiagnosticDescriptor + => GetDiagnosticDescriptor(IDEDiagnosticIds.UnboundIdentifierId, _nameNotInContextMessageFormat); - protected override bool IsNameOf(SyntaxNode node) - => node.Parent is InvocationExpressionSyntax invocation && invocation.IsNameOfInvocation(); - } + protected override bool IsNameOf(SyntaxNode node) + => node.Parent is InvocationExpressionSyntax invocation && invocation.IsNameOfInvocation(); } diff --git a/src/Features/CSharp/Portable/Diagnostics/Analyzers/TypeSyntaxSimplifierWalker.cs b/src/Features/CSharp/Portable/Diagnostics/Analyzers/TypeSyntaxSimplifierWalker.cs index 9248c10b38f98..9fa9387d97ec1 100644 --- a/src/Features/CSharp/Portable/Diagnostics/Analyzers/TypeSyntaxSimplifierWalker.cs +++ b/src/Features/CSharp/Portable/Diagnostics/Analyzers/TypeSyntaxSimplifierWalker.cs @@ -9,289 +9,291 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Simplification; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.SimplifyTypeNames +namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.SimplifyTypeNames; + +internal class TypeSyntaxSimplifierWalker : CSharpSyntaxWalker, IDisposable { - internal class TypeSyntaxSimplifierWalker : CSharpSyntaxWalker, IDisposable + /// + /// This set contains the full names of types that have equivalent predefined names in the language. + /// + private static readonly ImmutableHashSet s_predefinedTypeMetadataNames = + ImmutableHashSet.Create( + StringComparer.Ordinal, + nameof(Boolean), + nameof(SByte), + nameof(Byte), + nameof(Int16), + nameof(UInt16), + nameof(Int32), + nameof(UInt32), + nameof(Int64), + nameof(UInt64), + nameof(Single), + nameof(Double), + nameof(Decimal), + nameof(String), + nameof(Char), + nameof(Object)); + + private readonly CSharpSimplifyTypeNamesDiagnosticAnalyzer _analyzer; + private readonly SemanticModel _semanticModel; + private readonly CSharpSimplifierOptions _options; + private readonly AnalyzerOptions _analyzerOptions; + private readonly TextSpanIntervalTree? _ignoredSpans; + private readonly CancellationToken _cancellationToken; + + private ImmutableArray.Builder? _diagnostics; + + /// + /// Set of type and namespace names that have an alias associated with them. i.e. if the + /// user has using X = System.DateTime, then DateTime will be in this set. + /// This is used so we can easily tell if we should try to simplify some identifier to an + /// alias when we encounter it. + /// + private readonly PooledHashSet _aliasedNames; + + public bool HasDiagnostics => _diagnostics?.Count > 0; + + public ImmutableArray Diagnostics => _diagnostics?.ToImmutable() ?? []; + + public ImmutableArray.Builder DiagnosticsBuilder { - /// - /// This set contains the full names of types that have equivalent predefined names in the language. - /// - private static readonly ImmutableHashSet s_predefinedTypeMetadataNames = - ImmutableHashSet.Create( - StringComparer.Ordinal, - nameof(Boolean), - nameof(SByte), - nameof(Byte), - nameof(Int16), - nameof(UInt16), - nameof(Int32), - nameof(UInt32), - nameof(Int64), - nameof(UInt64), - nameof(Single), - nameof(Double), - nameof(Decimal), - nameof(String), - nameof(Char), - nameof(Object)); - - private readonly CSharpSimplifyTypeNamesDiagnosticAnalyzer _analyzer; - private readonly SemanticModel _semanticModel; - private readonly CSharpSimplifierOptions _options; - private readonly TextSpanIntervalTree? _ignoredSpans; - private readonly CancellationToken _cancellationToken; - - private ImmutableArray.Builder? _diagnostics; - - /// - /// Set of type and namespace names that have an alias associated with them. i.e. if the - /// user has using X = System.DateTime, then DateTime will be in this set. - /// This is used so we can easily tell if we should try to simplify some identifier to an - /// alias when we encounter it. - /// - private readonly PooledHashSet _aliasedNames; - - public bool HasDiagnostics => _diagnostics?.Count > 0; - - public ImmutableArray Diagnostics => _diagnostics?.ToImmutable() ?? []; - - public ImmutableArray.Builder DiagnosticsBuilder + get { - get - { - if (_diagnostics is null) - Interlocked.CompareExchange(ref _diagnostics, ImmutableArray.CreateBuilder(), null); + if (_diagnostics is null) + Interlocked.CompareExchange(ref _diagnostics, ImmutableArray.CreateBuilder(), null); - return _diagnostics; - } + return _diagnostics; } + } + + public TypeSyntaxSimplifierWalker(CSharpSimplifyTypeNamesDiagnosticAnalyzer analyzer, SemanticModel semanticModel, CSharpSimplifierOptions options, AnalyzerOptions analyzerOptions, TextSpanIntervalTree? ignoredSpans, CancellationToken cancellationToken) + : base(SyntaxWalkerDepth.StructuredTrivia) + { + _analyzer = analyzer; + _semanticModel = semanticModel; + _options = options; + _analyzerOptions = analyzerOptions; + _ignoredSpans = ignoredSpans; + _cancellationToken = cancellationToken; + + var root = semanticModel.SyntaxTree.GetRoot(cancellationToken); + _aliasedNames = PooledHashSet.GetInstance(); + AddAliasedNames((CompilationUnitSyntax)root); + } - public TypeSyntaxSimplifierWalker(CSharpSimplifyTypeNamesDiagnosticAnalyzer analyzer, SemanticModel semanticModel, CSharpSimplifierOptions options, TextSpanIntervalTree? ignoredSpans, CancellationToken cancellationToken) - : base(SyntaxWalkerDepth.StructuredTrivia) + public void Dispose() + { + _aliasedNames.Free(); + } + + private void AddAliasedNames(CompilationUnitSyntax compilationUnit) + { + // Using `position: 0` gets all the global aliases defined in other files pulled in here. + var scopes = _semanticModel.GetImportScopes(position: 0, _cancellationToken); + foreach (var scope in scopes) { - _analyzer = analyzer; - _semanticModel = semanticModel; - _options = options; - _ignoredSpans = ignoredSpans; - _cancellationToken = cancellationToken; - - var root = semanticModel.SyntaxTree.GetRoot(cancellationToken); - _aliasedNames = PooledHashSet.GetInstance(); - AddAliasedNames((CompilationUnitSyntax)root); + foreach (var alias in scope.Aliases) + { + var name = alias.Target.Name; + if (!string.IsNullOrEmpty(name)) + _aliasedNames.Add(name); + } } - public void Dispose() + foreach (var usingDirective in compilationUnit.Usings) + AddAliasedName(usingDirective); + + foreach (var member in compilationUnit.Members) { - _aliasedNames.Free(); + if (member is BaseNamespaceDeclarationSyntax namespaceDeclaration) + AddAliasedNames(namespaceDeclaration); } - private void AddAliasedNames(CompilationUnitSyntax compilationUnit) + return; + + void AddAliasedName(UsingDirectiveSyntax usingDirective) { - // Using `position: 0` gets all the global aliases defined in other files pulled in here. - var scopes = _semanticModel.GetImportScopes(position: 0, _cancellationToken); - foreach (var scope in scopes) + if (usingDirective.Alias is not null && + usingDirective.Name?.GetRightmostName() is IdentifierNameSyntax identifierName) { - foreach (var alias in scope.Aliases) - { - var name = alias.Target.Name; - if (!string.IsNullOrEmpty(name)) - _aliasedNames.Add(name); - } + var identifierAlias = identifierName.Identifier.ValueText; + if (!string.IsNullOrEmpty(identifierAlias)) + _aliasedNames.Add(identifierAlias); } + } - foreach (var usingDirective in compilationUnit.Usings) + void AddAliasedNames(BaseNamespaceDeclarationSyntax namespaceDeclaration) + { + foreach (var usingDirective in namespaceDeclaration.Usings) AddAliasedName(usingDirective); - foreach (var member in compilationUnit.Members) + foreach (var member in namespaceDeclaration.Members) { - if (member is BaseNamespaceDeclarationSyntax namespaceDeclaration) - AddAliasedNames(namespaceDeclaration); + if (member is BaseNamespaceDeclarationSyntax memberNamespace) + AddAliasedNames(memberNamespace); } + } + } + public override void VisitQualifiedName(QualifiedNameSyntax node) + { + if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) + { return; - - void AddAliasedName(UsingDirectiveSyntax usingDirective) - { - if (usingDirective.Alias is not null && - usingDirective.Name?.GetRightmostName() is IdentifierNameSyntax identifierName) - { - var identifierAlias = identifierName.Identifier.ValueText; - if (!string.IsNullOrEmpty(identifierAlias)) - _aliasedNames.Add(identifierAlias); - } - } - - void AddAliasedNames(BaseNamespaceDeclarationSyntax namespaceDeclaration) - { - foreach (var usingDirective in namespaceDeclaration.Usings) - AddAliasedName(usingDirective); - - foreach (var member in namespaceDeclaration.Members) - { - if (member is BaseNamespaceDeclarationSyntax memberNamespace) - AddAliasedNames(memberNamespace); - } - } } - public override void VisitQualifiedName(QualifiedNameSyntax node) + if (node.IsKind(SyntaxKind.QualifiedName) && TrySimplify(node)) { - if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) - { - return; - } + // found a match. report it and stop processing. + return; + } - if (node.IsKind(SyntaxKind.QualifiedName) && TrySimplify(node)) - { - // found a match. report it and stop processing. - return; - } + // descend further. + DefaultVisit(node); + } - // descend further. - DefaultVisit(node); + public override void VisitAliasQualifiedName(AliasQualifiedNameSyntax node) + { + if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) + { + return; } - public override void VisitAliasQualifiedName(AliasQualifiedNameSyntax node) + if (node.IsKind(SyntaxKind.AliasQualifiedName) && TrySimplify(node)) { - if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) - { - return; - } + // found a match. report it and stop processing. + return; + } - if (node.IsKind(SyntaxKind.AliasQualifiedName) && TrySimplify(node)) - { - // found a match. report it and stop processing. - return; - } + // descend further. + DefaultVisit(node); + } - // descend further. - DefaultVisit(node); + public override void VisitGenericName(GenericNameSyntax node) + { + if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) + { + return; } - public override void VisitGenericName(GenericNameSyntax node) + if (TrySimplify(node)) { - if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) - { - return; - } + // found a match. report it and stop processing. + return; + } - if (TrySimplify(node)) - { - // found a match. report it and stop processing. - return; - } + // descend further. + DefaultVisit(node); + } - // descend further. - DefaultVisit(node); + public override void VisitIdentifierName(IdentifierNameSyntax node) + { + if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) + { + return; } - public override void VisitIdentifierName(IdentifierNameSyntax node) + // Always try to simplify identifiers with an 'Attribute' suffix. + // + // In other cases, don't bother looking at the right side of A.B or A::B. We will process those in + // one of our other top level Visit methods (like VisitQualifiedName). + var canTrySimplify = node.Identifier.ValueText.EndsWith("Attribute", StringComparison.Ordinal); + if (!canTrySimplify && !node.IsRightSideOfDotOrArrowOrColonColon()) { - if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) - { - return; - } + // The only possible simplifications to an unqualified identifier are replacement with an alias or + // replacement with a predefined type. + canTrySimplify = CanReplaceIdentifierWithAlias(node.Identifier.ValueText) + || CanReplaceIdentifierWithPredefinedType(node.Identifier.ValueText); + } - // Always try to simplify identifiers with an 'Attribute' suffix. - // - // In other cases, don't bother looking at the right side of A.B or A::B. We will process those in - // one of our other top level Visit methods (like VisitQualifiedName). - var canTrySimplify = node.Identifier.ValueText.EndsWith("Attribute", StringComparison.Ordinal); - if (!canTrySimplify && !node.IsRightSideOfDotOrArrowOrColonColon()) - { - // The only possible simplifications to an unqualified identifier are replacement with an alias or - // replacement with a predefined type. - canTrySimplify = CanReplaceIdentifierWithAlias(node.Identifier.ValueText) - || CanReplaceIdentifierWithPredefinedType(node.Identifier.ValueText); - } + if (canTrySimplify && TrySimplify(node)) + { + // found a match. report it and stop processing. + return; + } - if (canTrySimplify && TrySimplify(node)) - { - // found a match. report it and stop processing. - return; - } + // descend further. + DefaultVisit(node); + return; - // descend further. - DefaultVisit(node); - return; + // Local functions + bool CanReplaceIdentifierWithAlias(string identifier) + => _aliasedNames.Contains(identifier); - // Local functions - bool CanReplaceIdentifierWithAlias(string identifier) - => _aliasedNames.Contains(identifier); + static bool CanReplaceIdentifierWithPredefinedType(string identifier) + => s_predefinedTypeMetadataNames.Contains(identifier); + } - static bool CanReplaceIdentifierWithPredefinedType(string identifier) - => s_predefinedTypeMetadataNames.Contains(identifier); + public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node) + { + if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) + { + return; } - public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node) + if (node.IsKind(SyntaxKind.SimpleMemberAccessExpression) && TrySimplify(node)) { - if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) - { - return; - } + // found a match. report it and stop processing. + return; + } - if (node.IsKind(SyntaxKind.SimpleMemberAccessExpression) && TrySimplify(node)) - { - // found a match. report it and stop processing. - return; - } + // descend further. + DefaultVisit(node); + } - // descend further. - DefaultVisit(node); + public override void VisitQualifiedCref(QualifiedCrefSyntax node) + { + if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) + { + return; } - public override void VisitQualifiedCref(QualifiedCrefSyntax node) + // First, just try to simplify the top-most qualified-cref alone. If we're able to do + // this, then there's no need to process it's container. i.e. + // + // if we have and we simplify that to there's no + // point looking at `A.B`. + if (node.IsKind(SyntaxKind.QualifiedCref) && TrySimplify(node)) { - if (_ignoredSpans?.HasIntervalThatOverlapsWith(node.FullSpan.Start, node.FullSpan.Length) ?? false) - { - return; - } - - // First, just try to simplify the top-most qualified-cref alone. If we're able to do - // this, then there's no need to process it's container. i.e. - // - // if we have and we simplify that to there's no - // point looking at `A.B`. - if (node.IsKind(SyntaxKind.QualifiedCref) && TrySimplify(node)) - { - // found a match on the qualified cref itself. report it and keep processing. - } - else - { - // couldn't simplify the qualified cref itself. descend into the container portion - // as that might have portions that can be simplified. - Visit(node.Container); - } - - // unilaterally process the member portion of the qualified cref. These may have things - // like parameters that could be simplified. i.e. if we have: - // - // - // - // We can simplify both the qualified portion to just `C` and we can simplify the - // parameter to just `Y`. - Visit(node.Member); + // found a match on the qualified cref itself. report it and keep processing. } - - /// - /// This is the root helper that all other TrySimplify methods in this type must call - /// through once they think there is a good chance something is simplifiable. It does the - /// work of actually going through the real simplification system to validate that the - /// simplification is legal and does not affect semantics. - /// - private bool TrySimplify(SyntaxNode node) + else { - if (!_analyzer.TrySimplify(_semanticModel, node, out var diagnostic, _options, _cancellationToken)) - return false; - - DiagnosticsBuilder.Add(diagnostic); - return true; + // couldn't simplify the qualified cref itself. descend into the container portion + // as that might have portions that can be simplified. + Visit(node.Container); } + + // unilaterally process the member portion of the qualified cref. These may have things + // like parameters that could be simplified. i.e. if we have: + // + // + // + // We can simplify both the qualified portion to just `C` and we can simplify the + // parameter to just `Y`. + Visit(node.Member); + } + + /// + /// This is the root helper that all other TrySimplify methods in this type must call + /// through once they think there is a good chance something is simplifiable. It does the + /// work of actually going through the real simplification system to validate that the + /// simplification is legal and does not affect semantics. + /// + private bool TrySimplify(SyntaxNode node) + { + if (!_analyzer.TrySimplify(_semanticModel, node, out var diagnostic, _options, _analyzerOptions, _cancellationToken)) + return false; + + DiagnosticsBuilder.Add(diagnostic); + return true; } } diff --git a/src/Features/CSharp/Portable/Diagnostics/CSharpAnalyzerDriverService.cs b/src/Features/CSharp/Portable/Diagnostics/CSharpAnalyzerDriverService.cs index 82276b882ff7b..548602fd9182a 100644 --- a/src/Features/CSharp/Portable/Diagnostics/CSharpAnalyzerDriverService.cs +++ b/src/Features/CSharp/Portable/Diagnostics/CSharpAnalyzerDriverService.cs @@ -12,25 +12,24 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Diagnostics +namespace Microsoft.CodeAnalysis.CSharp.Diagnostics; + +[ExportLanguageService(typeof(IAnalyzerDriverService), LanguageNames.CSharp), Shared] +internal sealed class CSharpAnalyzerDriverService : IAnalyzerDriverService { - [ExportLanguageService(typeof(IAnalyzerDriverService), LanguageNames.CSharp), Shared] - internal sealed class CSharpAnalyzerDriverService : IAnalyzerDriverService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpAnalyzerDriverService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpAnalyzerDriverService() - { - } + } - public void ComputeDeclarationsInSpan( - SemanticModel model, - TextSpan span, - bool getSymbol, - ArrayBuilder builder, - CancellationToken cancellationToken) - { - CSharpDeclarationComputer.ComputeDeclarationsInSpan(model, span, getSymbol, builder, cancellationToken); - } + public void ComputeDeclarationsInSpan( + SemanticModel model, + TextSpan span, + bool getSymbol, + ArrayBuilder builder, + CancellationToken cancellationToken) + { + CSharpDeclarationComputer.ComputeDeclarationsInSpan(model, span, getSymbol, builder, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Diagnostics/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs b/src/Features/CSharp/Portable/Diagnostics/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs index fc44f1224319c..8a81b6d09d4b1 100644 --- a/src/Features/CSharp/Portable/Diagnostics/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs +++ b/src/Features/CSharp/Portable/Diagnostics/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs @@ -7,67 +7,66 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer; -namespace Microsoft.CodeAnalysis.CSharp.LanguageServer +namespace Microsoft.CodeAnalysis.CSharp.LanguageServer; + +// Keep in sync with IsBuildOnlyDiagnostic +// src\Compilers\CSharp\Portable\Errors\ErrorFacts.cs +[LspBuildOnlyDiagnostics( + LanguageNames.CSharp, + "CS1607", // ErrorCode.WRN_ALinkWarn: + "CS0169", // ErrorCode.WRN_UnreferencedField: + "CS0414", // ErrorCode.WRN_UnreferencedFieldAssg: + "CS0067", // ErrorCode.WRN_UnreferencedEvent: + "CS0649", // ErrorCode.WRN_UnassignedInternalField: + "CS0656", // ErrorCode.ERR_MissingPredefinedMember: + "CS0518", // ErrorCode.ERR_PredefinedTypeNotFound: + "CS5001", // ErrorCode.ERR_NoEntryPoint: + "CS0028", // ErrorCode.WRN_InvalidMainSig: + "CS0017", // ErrorCode.ERR_MultipleEntryPoints: + "CS7022", // ErrorCode.WRN_MainIgnored: + "CS1556", // ErrorCode.ERR_MainClassNotClass: + "CS0402", // ErrorCode.WRN_MainCantBeGeneric: + "CS1558", // ErrorCode.ERR_NoMainInClass: + "CS1555", // ErrorCode.ERR_MainClassNotFound: + "CS8892", // ErrorCode.WRN_SyncAndAsyncEntryPoints: + "CS0148", // ErrorCode.ERR_BadDelegateConstructor: + "CS8078", // ErrorCode.ERR_InsufficientStack: + "CS7038", // ErrorCode.ERR_ModuleEmitFailure: + "CS0204", // ErrorCode.ERR_TooManyLocals: + "CS0570", // ErrorCode.ERR_BindToBogus: + "CS8004", // ErrorCode.ERR_ExportedTypeConflictsWithDeclaration: + "CS8006", // ErrorCode.ERR_ForwardedTypeConflictsWithDeclaration: + "CS8005", // ErrorCode.ERR_ExportedTypesConflict: + "CS8008", // ErrorCode.ERR_ForwardedTypeConflictsWithExportedType: + "CS4007", // ErrorCode.ERR_ByRefTypeAndAwait: + "CS8178", // ErrorCode.ERR_RefReturningCallAndAwait: + "CS4013", // ErrorCode.ERR_SpecialByRefInLambda: + "CS1969", // ErrorCode.ERR_DynamicRequiredTypesMissing: + "CS9026", // ErrorCode.ERR_CannotBeConvertedToUtf8: + "CS9068", // ErrorCode.ERR_FileTypeNonUniquePath: + "CS9144", // ErrorCode.ERR_InterceptorSignatureMismatch + "CS9148", // ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter + "CS9149", // ErrorCode.ERR_InterceptorMustNotHaveThisParameter + "CS9153", // ErrorCode.ERR_DuplicateInterceptor + "CS9154", // ErrorCode.WRN_InterceptorSignatureMismatch, + "CS9155", // ErrorCode.ERR_InterceptorNotAccessible + "CS9156", // ErrorCode.ERR_InterceptorScopedMismatch + "CS9158", // ErrorCode.WRN_NullabilityMismatchInReturnTypeOnInterceptor + "CS9159", // ErrorCode.WRN_NullabilityMismatchInParameterTypeOnInterceptor + "CS9160", // ErrorCode.ERR_InterceptorCannotInterceptNameof + "CS9163", // ErrorCode.ERR_SymbolDefinedInAssembly + "CS9177", // ErrorCode.ERR_InterceptorArityNotCompatible + "CS9178", // ErrorCode.ERR_InterceptorCannotBeGeneric + "CS9207", // ErrorCode.ERR_InterceptableMethodMustBeOrdinary + "CS8419", // ErrorCode.ERR_PossibleAsyncIteratorWithoutYield + "CS8420" // ErrorCode.ERR_PossibleAsyncIteratorWithoutYieldOrAwait + )] +[Shared] +internal sealed class CSharpLspBuildOnlyDiagnostics : ILspBuildOnlyDiagnostics { - // Keep in sync with IsBuildOnlyDiagnostic - // src\Compilers\CSharp\Portable\Errors\ErrorFacts.cs - [LspBuildOnlyDiagnostics( - LanguageNames.CSharp, - "CS1607", // ErrorCode.WRN_ALinkWarn: - "CS0169", // ErrorCode.WRN_UnreferencedField: - "CS0414", // ErrorCode.WRN_UnreferencedFieldAssg: - "CS0067", // ErrorCode.WRN_UnreferencedEvent: - "CS0649", // ErrorCode.WRN_UnassignedInternalField: - "CS0656", // ErrorCode.ERR_MissingPredefinedMember: - "CS0518", // ErrorCode.ERR_PredefinedTypeNotFound: - "CS5001", // ErrorCode.ERR_NoEntryPoint: - "CS0028", // ErrorCode.WRN_InvalidMainSig: - "CS0017", // ErrorCode.ERR_MultipleEntryPoints: - "CS7022", // ErrorCode.WRN_MainIgnored: - "CS1556", // ErrorCode.ERR_MainClassNotClass: - "CS0402", // ErrorCode.WRN_MainCantBeGeneric: - "CS1558", // ErrorCode.ERR_NoMainInClass: - "CS1555", // ErrorCode.ERR_MainClassNotFound: - "CS8892", // ErrorCode.WRN_SyncAndAsyncEntryPoints: - "CS0148", // ErrorCode.ERR_BadDelegateConstructor: - "CS8078", // ErrorCode.ERR_InsufficientStack: - "CS7038", // ErrorCode.ERR_ModuleEmitFailure: - "CS0204", // ErrorCode.ERR_TooManyLocals: - "CS0570", // ErrorCode.ERR_BindToBogus: - "CS8004", // ErrorCode.ERR_ExportedTypeConflictsWithDeclaration: - "CS8006", // ErrorCode.ERR_ForwardedTypeConflictsWithDeclaration: - "CS8005", // ErrorCode.ERR_ExportedTypesConflict: - "CS8008", // ErrorCode.ERR_ForwardedTypeConflictsWithExportedType: - "CS4007", // ErrorCode.ERR_ByRefTypeAndAwait: - "CS8178", // ErrorCode.ERR_RefReturningCallAndAwait: - "CS4013", // ErrorCode.ERR_SpecialByRefInLambda: - "CS1969", // ErrorCode.ERR_DynamicRequiredTypesMissing: - "CS9026", // ErrorCode.ERR_CannotBeConvertedToUtf8: - "CS9068", // ErrorCode.ERR_FileTypeNonUniquePath: - "CS9144", // ErrorCode.ERR_InterceptorSignatureMismatch - "CS9148", // ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter - "CS9149", // ErrorCode.ERR_InterceptorMustNotHaveThisParameter - "CS9153", // ErrorCode.ERR_DuplicateInterceptor - "CS9154", // ErrorCode.WRN_InterceptorSignatureMismatch, - "CS9155", // ErrorCode.ERR_InterceptorNotAccessible - "CS9156", // ErrorCode.ERR_InterceptorScopedMismatch - "CS9158", // ErrorCode.WRN_NullabilityMismatchInReturnTypeOnInterceptor - "CS9159", // ErrorCode.WRN_NullabilityMismatchInParameterTypeOnInterceptor - "CS9160", // ErrorCode.ERR_InterceptorCannotInterceptNameof - "CS9163", // ErrorCode.ERR_SymbolDefinedInAssembly - "CS9177", // ErrorCode.ERR_InterceptorArityNotCompatible - "CS9178", // ErrorCode.ERR_InterceptorCannotBeGeneric - "CS9207", // ErrorCode.ERR_InterceptableMethodMustBeOrdinary - "CS8419", // ErrorCode.ERR_PossibleAsyncIteratorWithoutYield - "CS8420" // ErrorCode.ERR_PossibleAsyncIteratorWithoutYieldOrAwait - )] - [Shared] - internal sealed class CSharpLspBuildOnlyDiagnostics : ILspBuildOnlyDiagnostics + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpLspBuildOnlyDiagnostics() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpLspBuildOnlyDiagnostics() - { - } } } diff --git a/src/Features/CSharp/Portable/DocumentHighlighting/CSharpDocumentHighlightsService.cs b/src/Features/CSharp/Portable/DocumentHighlighting/CSharpDocumentHighlightsService.cs index 7ffebdb1b618f..8c621ef831a49 100644 --- a/src/Features/CSharp/Portable/DocumentHighlighting/CSharpDocumentHighlightsService.cs +++ b/src/Features/CSharp/Portable/DocumentHighlighting/CSharpDocumentHighlightsService.cs @@ -18,56 +18,55 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.DocumentHighlighting +namespace Microsoft.CodeAnalysis.CSharp.DocumentHighlighting; + +[ExportLanguageService(typeof(IDocumentHighlightsService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpDocumentHighlightsService( + [ImportMany] IEnumerable> services) : AbstractDocumentHighlightsService(LanguageNames.CSharp, + CSharpEmbeddedLanguagesProvider.Info, + CSharpSyntaxKinds.Instance, + services) { - [ExportLanguageService(typeof(IDocumentHighlightsService), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpDocumentHighlightsService( - [ImportMany] IEnumerable> services) : AbstractDocumentHighlightsService(LanguageNames.CSharp, - CSharpEmbeddedLanguagesProvider.Info, - CSharpSyntaxKinds.Instance, - services) + protected override async Task> GetAdditionalReferencesAsync( + Document document, ISymbol symbol, CancellationToken cancellationToken) { - protected override async Task> GetAdditionalReferencesAsync( - Document document, ISymbol symbol, CancellationToken cancellationToken) + // The FindRefs engine won't find references through 'var' for performance reasons. + // Also, they are not needed for things like rename/sig change, and the normal find refs + // feature. However, we would like the results to be highlighted to get a good experience + // while editing (especially since highlighting may have been invoked off of 'var' in + // the first place). + // + // So we look for the references through 'var' directly in this file and add them to the + // results found by the engine. + using var _ = ArrayBuilder.GetInstance(out var results); + + if (symbol is INamedTypeSymbol && symbol.Name != "var") { - // The FindRefs engine won't find references through 'var' for performance reasons. - // Also, they are not needed for things like rename/sig change, and the normal find refs - // feature. However, we would like the results to be highlighted to get a good experience - // while editing (especially since highlighting may have been invoked off of 'var' in - // the first place). - // - // So we look for the references through 'var' directly in this file and add them to the - // results found by the engine. - using var _ = ArrayBuilder.GetInstance(out var results); + var originalSymbol = symbol.OriginalDefinition; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (symbol is INamedTypeSymbol && symbol.Name != "var") - { - var originalSymbol = symbol.OriginalDefinition; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var descendants = root.DescendantNodes(); + var semanticModel = (SemanticModel?)null; - var descendants = root.DescendantNodes(); - var semanticModel = (SemanticModel?)null; + foreach (var type in descendants.OfType()) + { + cancellationToken.ThrowIfCancellationRequested(); - foreach (var type in descendants.OfType()) + if (type.IsVar) { - cancellationToken.ThrowIfCancellationRequested(); + semanticModel ??= await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (type.IsVar) - { - semanticModel ??= await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var boundSymbol = semanticModel.GetSymbolInfo(type, cancellationToken).Symbol; + boundSymbol = boundSymbol?.OriginalDefinition; - var boundSymbol = semanticModel.GetSymbolInfo(type, cancellationToken).Symbol; - boundSymbol = boundSymbol?.OriginalDefinition; - - if (originalSymbol.Equals(boundSymbol)) - results.Add(type.GetLocation()); - } + if (originalSymbol.Equals(boundSymbol)) + results.Add(type.GetLocation()); } } - - return results.ToImmutable(); } + + return results.ToImmutable(); } } diff --git a/src/Features/CSharp/Portable/DocumentationComments/CSharpDocumentationCommentFormattingService.cs b/src/Features/CSharp/Portable/DocumentationComments/CSharpDocumentationCommentFormattingService.cs index d5541287f0d74..4ee4086912483 100644 --- a/src/Features/CSharp/Portable/DocumentationComments/CSharpDocumentationCommentFormattingService.cs +++ b/src/Features/CSharp/Portable/DocumentationComments/CSharpDocumentationCommentFormattingService.cs @@ -9,15 +9,14 @@ using Microsoft.CodeAnalysis.DocumentationComments; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments +namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments; + +[ExportLanguageService(typeof(IDocumentationCommentFormattingService), LanguageNames.CSharp), Shared] +internal class CSharpDocumentationCommentFormattingService : AbstractDocumentationCommentFormattingService { - [ExportLanguageService(typeof(IDocumentationCommentFormattingService), LanguageNames.CSharp), Shared] - internal class CSharpDocumentationCommentFormattingService : AbstractDocumentationCommentFormattingService + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpDocumentationCommentFormattingService() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpDocumentationCommentFormattingService() - { - } } } diff --git a/src/Features/CSharp/Portable/DocumentationComments/CSharpDocumentationCommentSnippetService.cs b/src/Features/CSharp/Portable/DocumentationComments/CSharpDocumentationCommentSnippetService.cs index 763c723a8aa59..595adb0c151c4 100644 --- a/src/Features/CSharp/Portable/DocumentationComments/CSharpDocumentationCommentSnippetService.cs +++ b/src/Features/CSharp/Portable/DocumentationComments/CSharpDocumentationCommentSnippetService.cs @@ -18,347 +18,346 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments +namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments; + +[ExportLanguageService(typeof(IDocumentationCommentSnippetService), LanguageNames.CSharp), Shared] +internal class CSharpDocumentationCommentSnippetService : AbstractDocumentationCommentSnippetService { - [ExportLanguageService(typeof(IDocumentationCommentSnippetService), LanguageNames.CSharp), Shared] - internal class CSharpDocumentationCommentSnippetService : AbstractDocumentationCommentSnippetService + public override string DocumentationCommentCharacter => "/"; + + protected override bool AddIndent => true; + protected override string ExteriorTriviaText => "///"; + + private static readonly SymbolDisplayFormat s_format = + new( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | + SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpDocumentationCommentSnippetService() { - public override string DocumentationCommentCharacter => "/"; - - protected override bool AddIndent => true; - protected override string ExteriorTriviaText => "///"; - - private static readonly SymbolDisplayFormat s_format = - new( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpDocumentationCommentSnippetService() - { - } + } + + protected override MemberDeclarationSyntax? GetContainingMember(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + { + return syntaxTree.GetRoot(cancellationToken).FindToken(position).GetAncestor(); + } - protected override MemberDeclarationSyntax? GetContainingMember(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + protected override bool SupportsDocumentationComments(MemberDeclarationSyntax member) + { + switch (member.Kind()) { - return syntaxTree.GetRoot(cancellationToken).FindToken(position).GetAncestor(); + case SyntaxKind.ClassDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.RecordStructDeclaration: + case SyntaxKind.DelegateDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.EnumMemberDeclaration: + case SyntaxKind.FieldDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.ConstructorDeclaration: + case SyntaxKind.DestructorDeclaration: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.IndexerDeclaration: + case SyntaxKind.EventDeclaration: + case SyntaxKind.EventFieldDeclaration: + case SyntaxKind.OperatorDeclaration: + case SyntaxKind.ConversionOperatorDeclaration: + return true; + + default: + return false; } + } - protected override bool SupportsDocumentationComments(MemberDeclarationSyntax member) - { - switch (member.Kind()) - { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.RecordDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.StructDeclaration: - case SyntaxKind.RecordStructDeclaration: - case SyntaxKind.DelegateDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.EnumMemberDeclaration: - case SyntaxKind.FieldDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.ConstructorDeclaration: - case SyntaxKind.DestructorDeclaration: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.IndexerDeclaration: - case SyntaxKind.EventDeclaration: - case SyntaxKind.EventFieldDeclaration: - case SyntaxKind.OperatorDeclaration: - case SyntaxKind.ConversionOperatorDeclaration: - return true; + protected override bool HasDocumentationComment(MemberDeclarationSyntax member) + => member.GetFirstToken().LeadingTrivia.Any(t => t is (kind: SyntaxKind.SingleLineDocumentationCommentTrivia or SyntaxKind.MultiLineDocumentationCommentTrivia)); - default: - return false; - } - } + protected override int GetPrecedingDocumentationCommentCount(MemberDeclarationSyntax member) + { + var firstToken = member.GetFirstToken(); - protected override bool HasDocumentationComment(MemberDeclarationSyntax member) - => member.GetFirstToken().LeadingTrivia.Any(t => t is (kind: SyntaxKind.SingleLineDocumentationCommentTrivia or SyntaxKind.MultiLineDocumentationCommentTrivia)); + var count = firstToken.LeadingTrivia.Count(t => t.IsDocComment()); - protected override int GetPrecedingDocumentationCommentCount(MemberDeclarationSyntax member) + var previousToken = firstToken.GetPreviousToken(); + if (previousToken.Kind() != SyntaxKind.None) { - var firstToken = member.GetFirstToken(); + count += previousToken.TrailingTrivia.Count(t => t.IsDocComment()); + } + + return count; + } - var count = firstToken.LeadingTrivia.Count(t => t.IsDocComment()); + protected override List GetDocumentationCommentStubLines(MemberDeclarationSyntax member, string existingCommentText) + { + var list = new List + { + "/// ", + "///" + (existingCommentText.StartsWith(" ") ? existingCommentText : $" {existingCommentText}"), + "/// " + }; - var previousToken = firstToken.GetPreviousToken(); - if (previousToken.Kind() != SyntaxKind.None) + var typeParameterList = member.GetTypeParameterList(); + if (typeParameterList != null) + { + foreach (var typeParam in typeParameterList.Parameters) { - count += previousToken.TrailingTrivia.Count(t => t.IsDocComment()); + list.Add("/// "); } - - return count; } - protected override List GetDocumentationCommentStubLines(MemberDeclarationSyntax member, string existingCommentText) + var parameterList = member.GetParameterList(); + if (parameterList != null) { - var list = new List - { - "/// ", - "///" + (existingCommentText.StartsWith(" ") ? existingCommentText : $" {existingCommentText}"), - "/// " - }; - - var typeParameterList = member.GetTypeParameterList(); - if (typeParameterList != null) + foreach (var param in parameterList.Parameters) { - foreach (var typeParam in typeParameterList.Parameters) - { - list.Add("/// "); - } + list.Add("/// "); } + } - var parameterList = member.GetParameterList(); - if (parameterList != null) + if (member.Kind() is + SyntaxKind.MethodDeclaration or + SyntaxKind.IndexerDeclaration or + SyntaxKind.DelegateDeclaration or + SyntaxKind.OperatorDeclaration or + SyntaxKind.ConstructorDeclaration or + SyntaxKind.DestructorDeclaration) + { + var returnType = member.GetMemberType(); + if (returnType != null && + !(returnType is PredefinedTypeSyntax predefinedType && predefinedType.Keyword.IsKindOrHasMatchingText(SyntaxKind.VoidKeyword))) { - foreach (var param in parameterList.Parameters) - { - list.Add("/// "); - } + list.Add("/// "); } - if (member.Kind() is - SyntaxKind.MethodDeclaration or - SyntaxKind.IndexerDeclaration or - SyntaxKind.DelegateDeclaration or - SyntaxKind.OperatorDeclaration or - SyntaxKind.ConstructorDeclaration or - SyntaxKind.DestructorDeclaration) + foreach (var exceptionType in GetExceptions(member)) { - var returnType = member.GetMemberType(); - if (returnType != null && - !(returnType is PredefinedTypeSyntax predefinedType && predefinedType.Keyword.IsKindOrHasMatchingText(SyntaxKind.VoidKeyword))) - { - list.Add("/// "); - } - - foreach (var exceptionType in GetExceptions(member)) - { - list.Add(@$"/// "); - } + list.Add(@$"/// "); } - - return list; } - private static IEnumerable GetExceptions(SyntaxNode member) - { - var throwExpressionsAndStatements = member.DescendantNodes().Where(n => n.Kind() is SyntaxKind.ThrowExpression or SyntaxKind.ThrowStatement); + return list; + } - var usings = member.GetEnclosingUsingDirectives(); - var hasUsingSystem = usings.Any(u => u.Name is IdentifierNameSyntax { Identifier.ValueText: nameof(System) }); + private static IEnumerable GetExceptions(SyntaxNode member) + { + var throwExpressionsAndStatements = member.DescendantNodes().Where(n => n.Kind() is SyntaxKind.ThrowExpression or SyntaxKind.ThrowStatement); - using var _ = PooledHashSet.GetInstance(out var seenExceptionTypes); - foreach (var throwExpressionOrStatement in throwExpressionsAndStatements) + var usings = member.GetEnclosingUsingDirectives(); + var hasUsingSystem = usings.Any(u => u.Name is IdentifierNameSyntax { Identifier.ValueText: nameof(System) }); + + using var _ = PooledHashSet.GetInstance(out var seenExceptionTypes); + foreach (var throwExpressionOrStatement in throwExpressionsAndStatements) + { + var expression = throwExpressionOrStatement switch { - var expression = throwExpressionOrStatement switch - { - ThrowExpressionSyntax throwExpression => throwExpression.Expression, - ThrowStatementSyntax throwStatement => throwStatement.Expression, - _ => throw ExceptionUtilities.Unreachable() - }; + ThrowExpressionSyntax throwExpression => throwExpression.Expression, + ThrowStatementSyntax throwStatement => throwStatement.Expression, + _ => throw ExceptionUtilities.Unreachable() + }; - if (expression.IsKind(SyntaxKind.NullLiteralExpression)) + if (expression.IsKind(SyntaxKind.NullLiteralExpression)) + { + // `throw null;` throws NullReferenceException at runtime. + var exception = hasUsingSystem ? nameof(NullReferenceException) : $"{nameof(System)}.{nameof(NullReferenceException)}"; + if (seenExceptionTypes.Add(exception)) + yield return exception; + } + else if (expression is ObjectCreationExpressionSyntax { Type: TypeSyntax exceptionType }) + { + exceptionType = exceptionType.ConvertToSingleLine(); + if (!IsExceptionCaughtAndNotRethrown(hasUsingSystem, exceptionType)) { - // `throw null;` throws NullReferenceException at runtime. - var exception = hasUsingSystem ? nameof(NullReferenceException) : $"{nameof(System)}.{nameof(NullReferenceException)}"; + var exception = exceptionType.ToString(); if (seenExceptionTypes.Add(exception)) - yield return exception; - } - else if (expression is ObjectCreationExpressionSyntax { Type: TypeSyntax exceptionType }) - { - exceptionType = exceptionType.ConvertToSingleLine(); - if (!IsExceptionCaughtAndNotRethrown(hasUsingSystem, exceptionType)) - { - var exception = exceptionType.ToString(); - if (seenExceptionTypes.Add(exception)) - yield return exception.Replace('<', '{').Replace('>', '}'); - } + yield return exception.Replace('<', '{').Replace('>', '}'); } } } + } - private static bool IsExceptionCaughtAndNotRethrown(bool hasUsingSystem, TypeSyntax exceptionType) + private static bool IsExceptionCaughtAndNotRethrown(bool hasUsingSystem, TypeSyntax exceptionType) + { + for (SyntaxNode? current = exceptionType; current != null; current = current?.Parent) { - for (SyntaxNode? current = exceptionType; current != null; current = current?.Parent) + if (current is not BlockSyntax { Parent: TryStatementSyntax tryStatement } block || + tryStatement.Block != block || + block.DescendantNodes().OfType().Any(t => t.Expression is null)) { - if (current is not BlockSyntax { Parent: TryStatementSyntax tryStatement } block || - tryStatement.Block != block || - block.DescendantNodes().OfType().Any(t => t.Expression is null)) - { + continue; + } + + foreach (var catchClause in tryStatement.Catches) + { + if (catchClause.Filter != null) continue; - } - foreach (var catchClause in tryStatement.Catches) - { - if (catchClause.Filter != null) - continue; + // AN empty `catch { }` will always catch everything. + if (catchClause.Declaration == null) + return true; - // AN empty `catch { }` will always catch everything. - if (catchClause.Declaration == null) - return true; + // Poor mans equivalence check since we don't have semantics here. + if (SyntaxFactory.AreEquivalent(exceptionType, catchClause.Declaration.Type.ConvertToSingleLine())) + return true; - // Poor mans equivalence check since we don't have semantics here. - if (SyntaxFactory.AreEquivalent(exceptionType, catchClause.Declaration.Type.ConvertToSingleLine())) - return true; + if (hasUsingSystem && + catchClause.Declaration.Type is IdentifierNameSyntax { Identifier.ValueText: nameof(Exception) }) + { + return true; + } - if (hasUsingSystem && - catchClause.Declaration.Type is IdentifierNameSyntax { Identifier.ValueText: nameof(Exception) }) + if (catchClause.Declaration.Type is QualifiedNameSyntax { - return true; - } - - if (catchClause.Declaration.Type is QualifiedNameSyntax - { - Left: IdentifierNameSyntax { Identifier.ValueText: nameof(System) }, - Right: IdentifierNameSyntax { Identifier.ValueText: nameof(Exception) }, - }) - { - return true; - } + Left: IdentifierNameSyntax { Identifier.ValueText: nameof(System) }, + Right: IdentifierNameSyntax { Identifier.ValueText: nameof(Exception) }, + }) + { + return true; } } - - return false; } - protected override SyntaxToken GetTokenToRight( - SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) - { - if (position >= syntaxTree.GetText(cancellationToken).Length) - { - return default; - } - - return syntaxTree.GetRoot(cancellationToken).FindTokenOnRightOfPosition( - position, includeDirectives: true, includeDocumentationComments: true); - } + return false; + } - protected override SyntaxToken GetTokenToLeft( - SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + protected override SyntaxToken GetTokenToRight( + SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + { + if (position >= syntaxTree.GetText(cancellationToken).Length) { - if (position < 1) - { - return default; - } - - return syntaxTree.GetRoot(cancellationToken).FindTokenOnLeftOfPosition( - position - 1, includeDirectives: true, includeDocumentationComments: true, includeSkipped: true); + return default; } - protected override bool IsDocCommentNewLine(SyntaxToken token) - => token.RawKind == (int)SyntaxKind.XmlTextLiteralNewLineToken; - - protected override bool IsEndOfLineTrivia(SyntaxTrivia trivia) - => trivia.RawKind == (int)SyntaxKind.EndOfLineTrivia; + return syntaxTree.GetRoot(cancellationToken).FindTokenOnRightOfPosition( + position, includeDirectives: true, includeDocumentationComments: true); + } - protected override bool IsSingleExteriorTrivia(DocumentationCommentTriviaSyntax documentationComment, [NotNullWhen(true)] out string? existingCommentText) + protected override SyntaxToken GetTokenToLeft( + SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + { + if (position < 1) { - existingCommentText = null; - - if (IsMultilineDocComment(documentationComment)) - { - return false; - } + return default; + } - if (documentationComment.Content.Count != 1) - { - return false; - } + return syntaxTree.GetRoot(cancellationToken).FindTokenOnLeftOfPosition( + position - 1, includeDirectives: true, includeDocumentationComments: true, includeSkipped: true); + } - if (documentationComment.Content[0] is not XmlTextSyntax xmlText) - { - return false; - } + protected override bool IsDocCommentNewLine(SyntaxToken token) + => token.RawKind == (int)SyntaxKind.XmlTextLiteralNewLineToken; - var textTokens = xmlText.TextTokens; - if (!textTokens.Any()) - { - return false; - } + protected override bool IsEndOfLineTrivia(SyntaxTrivia trivia) + => trivia.RawKind == (int)SyntaxKind.EndOfLineTrivia; - var lastTextToken = textTokens.Last(); - var firstTextToken = textTokens.First(); + protected override bool IsSingleExteriorTrivia(DocumentationCommentTriviaSyntax documentationComment, [NotNullWhen(true)] out string? existingCommentText) + { + existingCommentText = null; - // We only allow more than one token if the first one is an actual comment, not whitespace - if (textTokens.Count != 1 && string.IsNullOrWhiteSpace(firstTextToken.ValueText)) - { - return false; - } + if (IsMultilineDocComment(documentationComment)) + { + return false; + } - // If there are two text tokens it means there is an existing comment that we want to - // preserve. - existingCommentText = textTokens.Count == 1 ? "" : firstTextToken.ValueText; + if (documentationComment.Content.Count != 1) + { + return false; + } - return lastTextToken.Kind() == SyntaxKind.XmlTextLiteralNewLineToken - && lastTextToken.TrailingTrivia.Count == 0 - && firstTextToken.LeadingTrivia is [(kind: SyntaxKind.DocumentationCommentExteriorTrivia) firstTrivia] - && firstTrivia.ToString() == ExteriorTriviaText; + if (documentationComment.Content[0] is not XmlTextSyntax xmlText) + { + return false; } - private static IList GetTextTokensFollowingExteriorTrivia(XmlTextSyntax xmlText) + var textTokens = xmlText.TextTokens; + if (!textTokens.Any()) { - var result = new List(); + return false; + } - var tokenList = xmlText.TextTokens; - foreach (var token in tokenList.Reverse()) - { - result.Add(token); + var lastTextToken = textTokens.Last(); + var firstTextToken = textTokens.First(); - if (token.LeadingTrivia.Any(SyntaxKind.DocumentationCommentExteriorTrivia)) - { - break; - } - } + // We only allow more than one token if the first one is an actual comment, not whitespace + if (textTokens.Count != 1 && string.IsNullOrWhiteSpace(firstTextToken.ValueText)) + { + return false; + } - result.Reverse(); + // If there are two text tokens it means there is an existing comment that we want to + // preserve. + existingCommentText = textTokens.Count == 1 ? "" : firstTextToken.ValueText; - return result; - } + return lastTextToken.Kind() == SyntaxKind.XmlTextLiteralNewLineToken + && lastTextToken.TrailingTrivia.Count == 0 + && firstTextToken.LeadingTrivia is [(kind: SyntaxKind.DocumentationCommentExteriorTrivia) firstTrivia] + && firstTrivia.ToString() == ExteriorTriviaText; + } - protected override bool EndsWithSingleExteriorTrivia(DocumentationCommentTriviaSyntax? documentationComment) + private static IList GetTextTokensFollowingExteriorTrivia(XmlTextSyntax xmlText) + { + var result = new List(); + + var tokenList = xmlText.TextTokens; + foreach (var token in tokenList.Reverse()) { - if (documentationComment == null) - { - return false; - } + result.Add(token); - if (IsMultilineDocComment(documentationComment)) + if (token.LeadingTrivia.Any(SyntaxKind.DocumentationCommentExteriorTrivia)) { - return false; + break; } + } - if (documentationComment.Content.LastOrDefault() is not XmlTextSyntax xmlText) - { - return false; - } + result.Reverse(); - var textTokens = GetTextTokensFollowingExteriorTrivia(xmlText); + return result; + } - if (textTokens.Any(t => !string.IsNullOrWhiteSpace(t.ToString()))) - { - return false; - } + protected override bool EndsWithSingleExteriorTrivia(DocumentationCommentTriviaSyntax? documentationComment) + { + if (documentationComment == null) + { + return false; + } + + if (IsMultilineDocComment(documentationComment)) + { + return false; + } + + if (documentationComment.Content.LastOrDefault() is not XmlTextSyntax xmlText) + { + return false; + } - var lastTextToken = textTokens.LastOrDefault(); - var firstTextToken = textTokens.FirstOrDefault(); + var textTokens = GetTextTokensFollowingExteriorTrivia(xmlText); - return lastTextToken.Kind() == SyntaxKind.XmlTextLiteralNewLineToken - && firstTextToken.LeadingTrivia.Count == 1 - && firstTextToken.LeadingTrivia.ElementAt(0).Kind() == SyntaxKind.DocumentationCommentExteriorTrivia - && firstTextToken.LeadingTrivia.ElementAt(0).ToString() == ExteriorTriviaText - && lastTextToken.TrailingTrivia.Count == 0; + if (textTokens.Any(t => !string.IsNullOrWhiteSpace(t.ToString()))) + { + return false; } - protected override bool IsMultilineDocComment(DocumentationCommentTriviaSyntax? documentationComment) - => documentationComment.IsMultilineDocComment(); + var lastTextToken = textTokens.LastOrDefault(); + var firstTextToken = textTokens.FirstOrDefault(); - protected override bool HasSkippedTrailingTrivia(SyntaxToken token) - => token.TrailingTrivia.Any(t => t.Kind() == SyntaxKind.SkippedTokensTrivia); + return lastTextToken.Kind() == SyntaxKind.XmlTextLiteralNewLineToken + && firstTextToken.LeadingTrivia.Count == 1 + && firstTextToken.LeadingTrivia.ElementAt(0).Kind() == SyntaxKind.DocumentationCommentExteriorTrivia + && firstTextToken.LeadingTrivia.ElementAt(0).ToString() == ExteriorTriviaText + && lastTextToken.TrailingTrivia.Count == 0; } + + protected override bool IsMultilineDocComment(DocumentationCommentTriviaSyntax? documentationComment) + => documentationComment.IsMultilineDocComment(); + + protected override bool HasSkippedTrailingTrivia(SyntaxToken token) + => token.TrailingTrivia.Any(t => t.Kind() == SyntaxKind.SkippedTokensTrivia); } diff --git a/src/Features/CSharp/Portable/DocumentationComments/DocCommentConverter.cs b/src/Features/CSharp/Portable/DocumentationComments/DocCommentConverter.cs index e4760ccff0759..d15c88cdce8e7 100644 --- a/src/Features/CSharp/Portable/DocumentationComments/DocCommentConverter.cs +++ b/src/Features/CSharp/Portable/DocumentationComments/DocCommentConverter.cs @@ -11,92 +11,91 @@ using Microsoft.CodeAnalysis.MetadataAsSource; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments +namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments; + +internal class DocCommentConverter : CSharpSyntaxRewriter { - internal class DocCommentConverter : CSharpSyntaxRewriter + private readonly IDocumentationCommentFormattingService _formattingService; + private readonly CancellationToken _cancellationToken; + + public static SyntaxNode ConvertToRegularComments(SyntaxNode node, IDocumentationCommentFormattingService formattingService, CancellationToken cancellationToken) { - private readonly IDocumentationCommentFormattingService _formattingService; - private readonly CancellationToken _cancellationToken; + var converter = new DocCommentConverter(formattingService, cancellationToken); - public static SyntaxNode ConvertToRegularComments(SyntaxNode node, IDocumentationCommentFormattingService formattingService, CancellationToken cancellationToken) - { - var converter = new DocCommentConverter(formattingService, cancellationToken); + return converter.Visit(node); + } - return converter.Visit(node); - } + private DocCommentConverter(IDocumentationCommentFormattingService formattingService, CancellationToken cancellationToken) + : base(visitIntoStructuredTrivia: false) + { + _formattingService = formattingService; + _cancellationToken = cancellationToken; + } - private DocCommentConverter(IDocumentationCommentFormattingService formattingService, CancellationToken cancellationToken) - : base(visitIntoStructuredTrivia: false) - { - _formattingService = formattingService; - _cancellationToken = cancellationToken; - } + public override SyntaxNode Visit(SyntaxNode node) + { + _cancellationToken.ThrowIfCancellationRequested(); - public override SyntaxNode Visit(SyntaxNode node) + if (node == null) { - _cancellationToken.ThrowIfCancellationRequested(); + return node; + } - if (node == null) - { - return node; - } + // Process children first + node = base.Visit(node); - // Process children first - node = base.Visit(node); + // Check the leading trivia for doc comments. + if (node.GetLeadingTrivia().Any(SyntaxKind.SingleLineDocumentationCommentTrivia)) + { + var newLeadingTrivia = new List(); - // Check the leading trivia for doc comments. - if (node.GetLeadingTrivia().Any(SyntaxKind.SingleLineDocumentationCommentTrivia)) + foreach (var trivia in node.GetLeadingTrivia()) { - var newLeadingTrivia = new List(); - - foreach (var trivia in node.GetLeadingTrivia()) + if (trivia.Kind() == SyntaxKind.SingleLineDocumentationCommentTrivia) { - if (trivia.Kind() == SyntaxKind.SingleLineDocumentationCommentTrivia) - { - var structuredTrivia = (DocumentationCommentTriviaSyntax)trivia.GetStructure(); - var commentLines = ConvertDocCommentToRegularComment(structuredTrivia).ToSyntaxTriviaList(); - - if (commentLines.Count > 0) - { - newLeadingTrivia.Add(SyntaxFactory.Comment("//")); - newLeadingTrivia.Add(SyntaxFactory.ElasticCarriageReturnLineFeed); + var structuredTrivia = (DocumentationCommentTriviaSyntax)trivia.GetStructure(); + var commentLines = ConvertDocCommentToRegularComment(structuredTrivia).ToSyntaxTriviaList(); - newLeadingTrivia.AddRange(commentLines); - } - } - else + if (commentLines.Count > 0) { - newLeadingTrivia.Add(trivia); + newLeadingTrivia.Add(SyntaxFactory.Comment("//")); + newLeadingTrivia.Add(SyntaxFactory.ElasticCarriageReturnLineFeed); + + newLeadingTrivia.AddRange(commentLines); } } - - node = node.WithLeadingTrivia(newLeadingTrivia); + else + { + newLeadingTrivia.Add(trivia); + } } - return node; + node = node.WithLeadingTrivia(newLeadingTrivia); } - private IEnumerable ConvertDocCommentToRegularComment(DocumentationCommentTriviaSyntax structuredTrivia) - { - var xmlFragment = DocumentationCommentUtilities.ExtractXMLFragment(structuredTrivia.ToFullString(), "///"); + return node; + } - var docComment = DocumentationComment.FromXmlFragment(xmlFragment); + private IEnumerable ConvertDocCommentToRegularComment(DocumentationCommentTriviaSyntax structuredTrivia) + { + var xmlFragment = DocumentationCommentUtilities.ExtractXMLFragment(structuredTrivia.ToFullString(), "///"); - var commentLines = AbstractMetadataAsSourceService.DocCommentFormatter.Format(_formattingService, docComment); + var docComment = DocumentationComment.FromXmlFragment(xmlFragment); - foreach (var line in commentLines) - { - if (!string.IsNullOrWhiteSpace(line)) - { - yield return SyntaxFactory.Comment("// " + line); - } - else - { - yield return SyntaxFactory.Comment("//"); - } + var commentLines = AbstractMetadataAsSourceService.DocCommentFormatter.Format(_formattingService, docComment); - yield return SyntaxFactory.ElasticCarriageReturnLineFeed; + foreach (var line in commentLines) + { + if (!string.IsNullOrWhiteSpace(line)) + { + yield return SyntaxFactory.Comment("// " + line); + } + else + { + yield return SyntaxFactory.Comment("//"); } + + yield return SyntaxFactory.ElasticCarriageReturnLineFeed; } } } diff --git a/src/Features/CSharp/Portable/EditAndContinue/BreakpointSpans.cs b/src/Features/CSharp/Portable/EditAndContinue/BreakpointSpans.cs index 361377dd039d7..5c033411ba311 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/BreakpointSpans.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/BreakpointSpans.cs @@ -11,979 +11,978 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +internal static class BreakpointSpans { - internal static class BreakpointSpans + public static bool TryGetBreakpointSpan(SyntaxTree tree, int position, CancellationToken cancellationToken, out TextSpan breakpointSpan) { - public static bool TryGetBreakpointSpan(SyntaxTree tree, int position, CancellationToken cancellationToken, out TextSpan breakpointSpan) + var source = tree.GetText(cancellationToken); + + // If the line is entirely whitespace, then don't set any breakpoint there. + var line = source.Lines.GetLineFromPosition(position); + if (IsBlank(line)) + { + breakpointSpan = default; + return false; + } + + // If the user is asking for breakpoint in an inactive region, then just create a line + // breakpoint there. + if (tree.IsInInactiveRegion(position, cancellationToken)) { - var source = tree.GetText(cancellationToken); + breakpointSpan = default; + return true; + } + + var root = tree.GetRoot(cancellationToken); + return TryGetClosestBreakpointSpan(root, position, minLength: 0, out breakpointSpan); + } - // If the line is entirely whitespace, then don't set any breakpoint there. - var line = source.Lines.GetLineFromPosition(position); - if (IsBlank(line)) + private static bool IsBlank(TextLine line) + { + var text = line.ToString(); + + for (var i = 0; i < text.Length; i++) + { + if (!SyntaxFacts.IsWhitespace(text[i])) { - breakpointSpan = default; return false; } + } - // If the user is asking for breakpoint in an inactive region, then just create a line - // breakpoint there. - if (tree.IsInInactiveRegion(position, cancellationToken)) - { - breakpointSpan = default; - return true; - } + return true; + } - var root = tree.GetRoot(cancellationToken); - return TryGetClosestBreakpointSpan(root, position, minLength: 0, out breakpointSpan); - } + /// + /// Given a syntax token determines a text span delimited by the closest applicable sequence points + /// encompassing the token. + /// + /// + /// In case there are multiple breakpoint spans starting at the given , + /// can be used to disambiguate between them. + /// The inner-most available span whose length is at least is returned. + /// + /// + /// If the span exists it is possible to place a breakpoint at the given position. + /// + public static bool TryGetClosestBreakpointSpan(SyntaxNode root, int position, int minLength, out TextSpan span) + { + var node = root.FindToken(position).Parent; + var candidate = (TextSpan?)null; - private static bool IsBlank(TextLine line) + while (node != null) { - var text = line.ToString(); - - for (var i = 0; i < text.Length; i++) + var breakpointSpan = TryCreateSpanForNode(node, position); + if (breakpointSpan.HasValue) { - if (!SyntaxFacts.IsWhitespace(text[i])) + span = breakpointSpan.Value; + if (span == default) { - return false; + break; } - } - return true; - } + // the new breakpoint span doesn't alight with the previously found breakpoint span, return the previous one: + if (candidate.HasValue && breakpointSpan.Value.Start != candidate.Value.Start) + { + span = candidate.Value; + return true; + } - /// - /// Given a syntax token determines a text span delimited by the closest applicable sequence points - /// encompassing the token. - /// - /// - /// In case there are multiple breakpoint spans starting at the given , - /// can be used to disambiguate between them. - /// The inner-most available span whose length is at least is returned. - /// - /// - /// If the span exists it is possible to place a breakpoint at the given position. - /// - public static bool TryGetClosestBreakpointSpan(SyntaxNode root, int position, int minLength, out TextSpan span) - { - var node = root.FindToken(position).Parent; - var candidate = (TextSpan?)null; - - while (node != null) - { - var breakpointSpan = TryCreateSpanForNode(node, position); - if (breakpointSpan.HasValue) + // The span length meets the requirement: + if (breakpointSpan.Value.Length >= minLength) { span = breakpointSpan.Value; - if (span == default) - { - break; - } - - // the new breakpoint span doesn't alight with the previously found breakpoint span, return the previous one: - if (candidate.HasValue && breakpointSpan.Value.Start != candidate.Value.Start) - { - span = candidate.Value; - return true; - } - - // The span length meets the requirement: - if (breakpointSpan.Value.Length >= minLength) - { - span = breakpointSpan.Value; - return true; - } - - candidate = breakpointSpan; + return true; } - node = node.Parent; + candidate = breakpointSpan; } - span = candidate.GetValueOrDefault(); - return candidate.HasValue; + node = node.Parent; } - private static TextSpan CreateSpan(SyntaxToken startToken, SyntaxToken endToken) - => TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End); + span = candidate.GetValueOrDefault(); + return candidate.HasValue; + } + + private static TextSpan CreateSpan(SyntaxToken startToken, SyntaxToken endToken) + => TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End); + + private static TextSpan CreateSpan(SyntaxNode node) + => CreateSpan(node.GetFirstToken(), node.GetLastToken()); - private static TextSpan CreateSpan(SyntaxNode node) - => CreateSpan(node.GetFirstToken(), node.GetLastToken()); + private static TextSpan CreateSpan(SyntaxNode node, SyntaxToken token) + => TextSpan.FromBounds(node.SpanStart, token.Span.End); - private static TextSpan CreateSpan(SyntaxNode node, SyntaxToken token) - => TextSpan.FromBounds(node.SpanStart, token.Span.End); + private static TextSpan CreateSpan(SyntaxToken token) + => TextSpan.FromBounds(token.SpanStart, token.Span.End); - private static TextSpan CreateSpan(SyntaxToken token) - => TextSpan.FromBounds(token.SpanStart, token.Span.End); + private static TextSpan CreateSpan(SyntaxTokenList startOpt, SyntaxNodeOrToken startFallbackOpt, SyntaxNodeOrToken endOpt) + { + Debug.Assert(startFallbackOpt != default || endOpt != default); - private static TextSpan CreateSpan(SyntaxTokenList startOpt, SyntaxNodeOrToken startFallbackOpt, SyntaxNodeOrToken endOpt) + int startPos; + if (startOpt.Count > 0) { - Debug.Assert(startFallbackOpt != default || endOpt != default); + startPos = startOpt.First().SpanStart; + } + else if (startFallbackOpt != default) + { + startPos = startFallbackOpt.SpanStart; + } + else + { + startPos = endOpt.SpanStart; + } - int startPos; - if (startOpt.Count > 0) - { - startPos = startOpt.First().SpanStart; - } - else if (startFallbackOpt != default) - { - startPos = startFallbackOpt.SpanStart; - } - else - { - startPos = endOpt.SpanStart; - } + int endPos; + if (endOpt != default) + { + endPos = GetEndPosition(endOpt); + } + else + { + endPos = GetEndPosition(startFallbackOpt); + } - int endPos; - if (endOpt != default) - { - endPos = GetEndPosition(endOpt); - } - else - { - endPos = GetEndPosition(startFallbackOpt); - } + return TextSpan.FromBounds(startPos, endPos); + } - return TextSpan.FromBounds(startPos, endPos); + private static int GetEndPosition(SyntaxNodeOrToken nodeOrToken) + { + if (nodeOrToken.IsToken) + { + return nodeOrToken.Span.End; } + else + { + return nodeOrToken.AsNode()!.GetLastToken().Span.End; + } + } - private static int GetEndPosition(SyntaxNodeOrToken nodeOrToken) + private static TextSpan? TryCreateSpanForNode(SyntaxNode node, int position) + { + if (node == null) { - if (nodeOrToken.IsToken) - { - return nodeOrToken.Span.End; - } - else - { - return nodeOrToken.AsNode()!.GetLastToken().Span.End; - } + return null; } - private static TextSpan? TryCreateSpanForNode(SyntaxNode node, int position) + switch (node.Kind()) { - if (node == null) - { - return null; - } + case SyntaxKind.MethodDeclaration: + case SyntaxKind.OperatorDeclaration: + case SyntaxKind.ConversionOperatorDeclaration: + case SyntaxKind.DestructorDeclaration: + var methodDeclaration = (BaseMethodDeclarationSyntax)node; + return (methodDeclaration.Body != null) ? CreateSpanForBlock(methodDeclaration.Body, position) : methodDeclaration.ExpressionBody?.Expression.Span; + + case SyntaxKind.ConstructorDeclaration: + return CreateSpanForConstructorDeclaration((ConstructorDeclarationSyntax)node, position); + + case SyntaxKind.RecordDeclaration: + case SyntaxKind.RecordStructDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.ClassDeclaration: + var typeDeclaration = (TypeDeclarationSyntax)node; + if (typeDeclaration.ParameterList != null) + { + // after brace or semicolon + // class C(...) {$$ ... } + // class C(...) ;$$ + if (position > LastNotMissing(typeDeclaration.SemicolonToken, typeDeclaration.OpenBraceToken).SpanStart) + { + return null; + } - switch (node.Kind()) - { - case SyntaxKind.MethodDeclaration: - case SyntaxKind.OperatorDeclaration: - case SyntaxKind.ConversionOperatorDeclaration: - case SyntaxKind.DestructorDeclaration: - var methodDeclaration = (BaseMethodDeclarationSyntax)node; - return (methodDeclaration.Body != null) ? CreateSpanForBlock(methodDeclaration.Body, position) : methodDeclaration.ExpressionBody?.Expression.Span; - - case SyntaxKind.ConstructorDeclaration: - return CreateSpanForConstructorDeclaration((ConstructorDeclarationSyntax)node, position); - - case SyntaxKind.RecordDeclaration: - case SyntaxKind.RecordStructDeclaration: - case SyntaxKind.StructDeclaration: - case SyntaxKind.ClassDeclaration: - var typeDeclaration = (TypeDeclarationSyntax)node; - if (typeDeclaration.ParameterList != null) + // on or after explicit base initializer: + // C(...) :$$ [|B(...)|], I + // C(...) : [|B(...)|], I where ... $$ + var baseInitializer = (PrimaryConstructorBaseTypeSyntax?)typeDeclaration.BaseList?.Types.FirstOrDefault(t => t.IsKind(SyntaxKind.PrimaryConstructorBaseType)); + if (baseInitializer != null && position > typeDeclaration.BaseList!.ColonToken.SpanStart) { - // after brace or semicolon - // class C(...) {$$ ... } - // class C(...) ;$$ - if (position > LastNotMissing(typeDeclaration.SemicolonToken, typeDeclaration.OpenBraceToken).SpanStart) - { - return null; - } + return CreateSpanForExplicitPrimaryConstructorInitializer(baseInitializer); + } - // on or after explicit base initializer: - // C(...) :$$ [|B(...)|], I - // C(...) : [|B(...)|], I where ... $$ - var baseInitializer = (PrimaryConstructorBaseTypeSyntax?)typeDeclaration.BaseList?.Types.FirstOrDefault(t => t.IsKind(SyntaxKind.PrimaryConstructorBaseType)); - if (baseInitializer != null && position > typeDeclaration.BaseList!.ColonToken.SpanStart) + // record properties and copy constructor + if (position >= typeDeclaration.Identifier.SpanStart && node is RecordDeclarationSyntax recordDeclaration) + { + // on identifier: + // record $$C(...) : B(...); + // record C$$(...) : B(...); + if (position <= typeDeclaration.ParameterList.SpanStart) { - return CreateSpanForExplicitPrimaryConstructorInitializer(baseInitializer); + // copy-constructor: [|C|] + return CreateSpanForCopyConstructor(recordDeclaration); } - // record properties and copy constructor - if (position >= typeDeclaration.Identifier.SpanStart && node is RecordDeclarationSyntax recordDeclaration) + // on parameter: + // record C(..., $$ int p, ...) : B(...); + if (position < typeDeclaration.ParameterList.CloseParenToken.Span.End) { - // on identifier: - // record $$C(...) : B(...); - // record C$$(...) : B(...); - if (position <= typeDeclaration.ParameterList.SpanStart) + var parameter = GetParameter(position, typeDeclaration.ParameterList.Parameters); + if (parameter != null) { - // copy-constructor: [|C|] - return CreateSpanForCopyConstructor(recordDeclaration); + // [A][|int p|] = default + return CreateSpanForRecordParameter(parameter); } - // on parameter: - // record C(..., $$ int p, ...) : B(...); - if (position < typeDeclaration.ParameterList.CloseParenToken.Span.End) + static ParameterSyntax? GetParameter(int position, SeparatedSyntaxList parameters) { - var parameter = GetParameter(position, typeDeclaration.ParameterList.Parameters); - if (parameter != null) + if (parameters.Count == 0) { - // [A][|int p|] = default - return CreateSpanForRecordParameter(parameter); + return null; } - static ParameterSyntax? GetParameter(int position, SeparatedSyntaxList parameters) + for (var i = 0; i < parameters.SeparatorCount; i++) { - if (parameters.Count == 0) - { - return null; - } - - for (var i = 0; i < parameters.SeparatorCount; i++) + var separator = parameters.GetSeparator(i); + if (position <= separator.SpanStart) { - var separator = parameters.GetSeparator(i); - if (position <= separator.SpanStart) - { - return parameters[i]; - } + return parameters[i]; } - - return parameters.Last(); } + + return parameters.Last(); } } - - // explicit base initializer - // C(...) : [|B(...)|] - // implicit base initializer - // [|C(...)|] - return (baseInitializer != null) - ? CreateSpanForExplicitPrimaryConstructorInitializer(baseInitializer) - : CreateSpanForImplicitPrimaryConstructorInitializer(typeDeclaration); } - return null; - - case SyntaxKind.VariableDeclarator: - // handled by the parent node - return null; - - case SyntaxKind.VariableDeclaration: - return TryCreateSpanForVariableDeclaration((VariableDeclarationSyntax)node, position); - - case SyntaxKind.EventFieldDeclaration: - case SyntaxKind.FieldDeclaration: - return TryCreateSpanForFieldDeclaration((BaseFieldDeclarationSyntax)node, position); - - case SyntaxKind.ElseClause: - return TryCreateSpanForNode(((ElseClauseSyntax)node).Statement, position); - - case SyntaxKind.CatchFilterClause: - return CreateSpan(node); - - case SyntaxKind.CatchClause: - return CreateSpanForCatchClause((CatchClauseSyntax)node); - - case SyntaxKind.FinallyClause: - return TryCreateSpanForNode(((FinallyClauseSyntax)node).Block, position); - - case SyntaxKind.CaseSwitchLabel: - case SyntaxKind.DefaultSwitchLabel: - return TryCreateSpanForSwitchLabel((SwitchLabelSyntax)node, position); - - case SyntaxKind.CasePatternSwitchLabel: - var caseClause = (CasePatternSwitchLabelSyntax)node; - return caseClause.WhenClause == null - ? TryCreateSpanForSwitchLabel((SwitchLabelSyntax)node, position) - : CreateSpan(caseClause.WhenClause); + // explicit base initializer + // C(...) : [|B(...)|] + // implicit base initializer + // [|C(...)|] + return (baseInitializer != null) + ? CreateSpanForExplicitPrimaryConstructorInitializer(baseInitializer) + : CreateSpanForImplicitPrimaryConstructorInitializer(typeDeclaration); + } - case SyntaxKind.SwitchExpressionArm: - var switchArm = (SwitchExpressionArmSyntax)node; - return createSpanForSwitchArm(switchArm); + return null; - TextSpan createSpanForSwitchArm(SwitchExpressionArmSyntax switchArm) - => CreateSpan((position <= switchArm.WhenClause?.FullSpan.End == true) ? switchArm.WhenClause : switchArm.Expression); + case SyntaxKind.VariableDeclarator: + // handled by the parent node + return null; - case SyntaxKind.SwitchExpression when - node is SwitchExpressionSyntax switchExpression && - switchExpression.Arms.Count > 0 && - position >= switchExpression.OpenBraceToken.Span.End && - position <= switchExpression.CloseBraceToken.Span.Start: - // This can occur if the cursor is on a separator. Find the nearest switch arm. - switchArm = switchExpression.Arms.LastOrDefault(arm => position >= arm.FullSpan.Start) ?? switchExpression.Arms.First(); - return createSpanForSwitchArm(switchArm); + case SyntaxKind.VariableDeclaration: + return TryCreateSpanForVariableDeclaration((VariableDeclarationSyntax)node, position); + + case SyntaxKind.EventFieldDeclaration: + case SyntaxKind.FieldDeclaration: + return TryCreateSpanForFieldDeclaration((BaseFieldDeclarationSyntax)node, position); + + case SyntaxKind.ElseClause: + return TryCreateSpanForNode(((ElseClauseSyntax)node).Statement, position); + + case SyntaxKind.CatchFilterClause: + return CreateSpan(node); + + case SyntaxKind.CatchClause: + return CreateSpanForCatchClause((CatchClauseSyntax)node); + + case SyntaxKind.FinallyClause: + return TryCreateSpanForNode(((FinallyClauseSyntax)node).Block, position); + + case SyntaxKind.CaseSwitchLabel: + case SyntaxKind.DefaultSwitchLabel: + return TryCreateSpanForSwitchLabel((SwitchLabelSyntax)node, position); + + case SyntaxKind.CasePatternSwitchLabel: + var caseClause = (CasePatternSwitchLabelSyntax)node; + return caseClause.WhenClause == null + ? TryCreateSpanForSwitchLabel((SwitchLabelSyntax)node, position) + : CreateSpan(caseClause.WhenClause); + + case SyntaxKind.SwitchExpressionArm: + var switchArm = (SwitchExpressionArmSyntax)node; + return createSpanForSwitchArm(switchArm); + + TextSpan createSpanForSwitchArm(SwitchExpressionArmSyntax switchArm) + => CreateSpan((position <= switchArm.WhenClause?.FullSpan.End == true) ? switchArm.WhenClause : switchArm.Expression); + + case SyntaxKind.SwitchExpression when + node is SwitchExpressionSyntax switchExpression && + switchExpression.Arms.Count > 0 && + position >= switchExpression.OpenBraceToken.Span.End && + position <= switchExpression.CloseBraceToken.Span.Start: + // This can occur if the cursor is on a separator. Find the nearest switch arm. + switchArm = switchExpression.Arms.LastOrDefault(arm => position >= arm.FullSpan.Start) ?? switchExpression.Arms.First(); + return createSpanForSwitchArm(switchArm); + + case SyntaxKind.WhenClause: + return CreateSpan(node); + + case SyntaxKind.GetAccessorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.InitAccessorDeclaration: + case SyntaxKind.AddAccessorDeclaration: + case SyntaxKind.RemoveAccessorDeclaration: + case SyntaxKind.UnknownAccessorDeclaration: + var accessor = (AccessorDeclarationSyntax)node; + if (accessor.ExpressionBody != null) + { + return CreateSpan(accessor.ExpressionBody.Expression); + } + else if (accessor.Body != null) + { + return TryCreateSpanForNode(accessor.Body, position); + } + else + { + return CreateSpanForAutoPropertyAccessor(accessor); + } - case SyntaxKind.WhenClause: - return CreateSpan(node); + case SyntaxKind.PropertyDeclaration: + var property = (PropertyDeclarationSyntax)node; - case SyntaxKind.GetAccessorDeclaration: - case SyntaxKind.SetAccessorDeclaration: - case SyntaxKind.InitAccessorDeclaration: - case SyntaxKind.AddAccessorDeclaration: - case SyntaxKind.RemoveAccessorDeclaration: - case SyntaxKind.UnknownAccessorDeclaration: - var accessor = (AccessorDeclarationSyntax)node; - if (accessor.ExpressionBody != null) - { - return CreateSpan(accessor.ExpressionBody.Expression); - } - else if (accessor.Body != null) - { - return TryCreateSpanForNode(accessor.Body, position); - } - else - { - return CreateSpanForAutoPropertyAccessor(accessor); - } + // Note that expression body, initializer and accessors are mutually exclusive. - case SyntaxKind.PropertyDeclaration: - var property = (PropertyDeclarationSyntax)node; + // int P => [|expr|] + if (property.ExpressionBody != null) + { + return property.ExpressionBody.Expression.Span; + } - // Note that expression body, initializer and accessors are mutually exclusive. + // int P { get; set; } = [|expr|] + if (property.Initializer != null && position >= property.Initializer.FullSpan.Start) + { + return property.Initializer.Value.Span; + } - // int P => [|expr|] - if (property.ExpressionBody != null) - { - return property.ExpressionBody.Expression.Span; - } + // properties without expression body have accessor list: + Contract.ThrowIfNull(property.AccessorList); - // int P { get; set; } = [|expr|] - if (property.Initializer != null && position >= property.Initializer.FullSpan.Start) - { - return property.Initializer.Value.Span; - } + // int P { get [|{|] ... } set { ... } } + // int P { [|get;|] [|set;|] } + return CreateSpanForAccessors(property.AccessorList.Accessors, position); - // properties without expression body have accessor list: - Contract.ThrowIfNull(property.AccessorList); + case SyntaxKind.IndexerDeclaration: + // int this[args] => [|expr|] + var indexer = (IndexerDeclarationSyntax)node; + if (indexer.ExpressionBody != null) + { + return indexer.ExpressionBody.Expression.Span; + } - // int P { get [|{|] ... } set { ... } } - // int P { [|get;|] [|set;|] } - return CreateSpanForAccessors(property.AccessorList.Accessors, position); + // indexers without expression body have accessor list: + Contract.ThrowIfNull(indexer.AccessorList); + + // int this[args] { get [|{|] ... } set { ... } } + return CreateSpanForAccessors(indexer.AccessorList.Accessors, position); + + case SyntaxKind.EventDeclaration: + // event Action P { add [|{|] ... } remove { ... } } + // event Action P { [|add;|] [|remove;|] } + var @event = (EventDeclarationSyntax)node; + return @event.AccessorList != null ? CreateSpanForAccessors(@event.AccessorList.Accessors, position) : null; + + case SyntaxKind.BaseConstructorInitializer: + case SyntaxKind.ThisConstructorInitializer: + return CreateSpanForExplicitConstructorInitializer((ConstructorInitializerSyntax)node); + + // Query clauses: + // + // Used when the user's initial location is on a query keyword itself (as + // opposed to inside an expression inside the query clause). It places the bp on the + // appropriate child expression in the clause. + + case SyntaxKind.FromClause: + var fromClause = (FromClauseSyntax)node; + return TryCreateSpanForNode(fromClause.Expression, position); + + case SyntaxKind.JoinClause: + var joinClause = (JoinClauseSyntax)node; + return TryCreateSpanForNode(joinClause.LeftExpression, position); + + case SyntaxKind.LetClause: + var letClause = (LetClauseSyntax)node; + return TryCreateSpanForNode(letClause.Expression, position); + + case SyntaxKind.WhereClause: + var whereClause = (WhereClauseSyntax)node; + return TryCreateSpanForNode(whereClause.Condition, position); + + case SyntaxKind.OrderByClause: + var orderByClause = (OrderByClauseSyntax)node; + return orderByClause.Orderings.Count > 0 + ? TryCreateSpanForNode(orderByClause.Orderings.First().Expression, position) + : null; + + case SyntaxKind.SelectClause: + var selectClause = (SelectClauseSyntax)node; + return TryCreateSpanForNode(selectClause.Expression, position); + + case SyntaxKind.GroupClause: + var groupClause = (GroupClauseSyntax)node; + return TryCreateSpanForNode(groupClause.GroupExpression, position); + + case SyntaxKind.LocalFunctionStatement: + var localFunction = (LocalFunctionStatementSyntax)node; + return (localFunction.Body != null) + ? TryCreateSpanForNode(localFunction.Body, position) + : TryCreateSpanForNode(localFunction.ExpressionBody!.Expression, position); + + default: + if (node is ExpressionSyntax expression) + { + return IsBreakableExpression(expression) ? CreateSpan(expression) : null; + } - case SyntaxKind.IndexerDeclaration: - // int this[args] => [|expr|] - var indexer = (IndexerDeclarationSyntax)node; - if (indexer.ExpressionBody != null) - { - return indexer.ExpressionBody.Expression.Span; - } + if (node is StatementSyntax statement) + { + return TryCreateSpanForStatement(statement, position); + } - // indexers without expression body have accessor list: - Contract.ThrowIfNull(indexer.AccessorList); - - // int this[args] { get [|{|] ... } set { ... } } - return CreateSpanForAccessors(indexer.AccessorList.Accessors, position); - - case SyntaxKind.EventDeclaration: - // event Action P { add [|{|] ... } remove { ... } } - // event Action P { [|add;|] [|remove;|] } - var @event = (EventDeclarationSyntax)node; - return @event.AccessorList != null ? CreateSpanForAccessors(@event.AccessorList.Accessors, position) : null; - - case SyntaxKind.BaseConstructorInitializer: - case SyntaxKind.ThisConstructorInitializer: - return CreateSpanForExplicitConstructorInitializer((ConstructorInitializerSyntax)node); - - // Query clauses: - // - // Used when the user's initial location is on a query keyword itself (as - // opposed to inside an expression inside the query clause). It places the bp on the - // appropriate child expression in the clause. - - case SyntaxKind.FromClause: - var fromClause = (FromClauseSyntax)node; - return TryCreateSpanForNode(fromClause.Expression, position); - - case SyntaxKind.JoinClause: - var joinClause = (JoinClauseSyntax)node; - return TryCreateSpanForNode(joinClause.LeftExpression, position); - - case SyntaxKind.LetClause: - var letClause = (LetClauseSyntax)node; - return TryCreateSpanForNode(letClause.Expression, position); - - case SyntaxKind.WhereClause: - var whereClause = (WhereClauseSyntax)node; - return TryCreateSpanForNode(whereClause.Condition, position); - - case SyntaxKind.OrderByClause: - var orderByClause = (OrderByClauseSyntax)node; - return orderByClause.Orderings.Count > 0 - ? TryCreateSpanForNode(orderByClause.Orderings.First().Expression, position) - : null; - - case SyntaxKind.SelectClause: - var selectClause = (SelectClauseSyntax)node; - return TryCreateSpanForNode(selectClause.Expression, position); - - case SyntaxKind.GroupClause: - var groupClause = (GroupClauseSyntax)node; - return TryCreateSpanForNode(groupClause.GroupExpression, position); - - case SyntaxKind.LocalFunctionStatement: - var localFunction = (LocalFunctionStatementSyntax)node; - return (localFunction.Body != null) - ? TryCreateSpanForNode(localFunction.Body, position) - : TryCreateSpanForNode(localFunction.ExpressionBody!.Expression, position); - - default: - if (node is ExpressionSyntax expression) - { - return IsBreakableExpression(expression) ? CreateSpan(expression) : null; - } + return null; + } + } - if (node is StatementSyntax statement) - { - return TryCreateSpanForStatement(statement, position); - } + internal static TextSpan? CreateSpanForConstructorDeclaration(ConstructorDeclarationSyntax constructorSyntax, int position) + { + if (constructorSyntax.ExpressionBody != null && + position > constructorSyntax.ExpressionBody.ArrowToken.Span.Start) + { + return constructorSyntax.ExpressionBody.Expression.Span; + } - return null; - } + if (constructorSyntax.Initializer != null) + { + return CreateSpanForExplicitConstructorInitializer(constructorSyntax.Initializer); } - internal static TextSpan? CreateSpanForConstructorDeclaration(ConstructorDeclarationSyntax constructorSyntax, int position) + // static ctor doesn't have a default initializer: + if (constructorSyntax.Modifiers.Any(SyntaxKind.StaticKeyword)) { - if (constructorSyntax.ExpressionBody != null && - position > constructorSyntax.ExpressionBody.ArrowToken.Span.Start) + if (constructorSyntax.ExpressionBody != null) { return constructorSyntax.ExpressionBody.Expression.Span; } - if (constructorSyntax.Initializer != null) + if (constructorSyntax.Body != null) { - return CreateSpanForExplicitConstructorInitializer(constructorSyntax.Initializer); + return CreateSpan(constructorSyntax.Body.OpenBraceToken); } - // static ctor doesn't have a default initializer: - if (constructorSyntax.Modifiers.Any(SyntaxKind.StaticKeyword)) - { - if (constructorSyntax.ExpressionBody != null) - { - return constructorSyntax.ExpressionBody.Expression.Span; - } + return null; + } - if (constructorSyntax.Body != null) - { - return CreateSpan(constructorSyntax.Body.OpenBraceToken); - } + return CreateSpanForImplicitConstructorInitializer(constructorSyntax); + } - return null; - } + internal static TextSpan CreateSpanForImplicitConstructorInitializer(ConstructorDeclarationSyntax constructor) + => CreateSpan(constructor.Modifiers, constructor.Identifier, constructor.ParameterList.CloseParenToken); - return CreateSpanForImplicitConstructorInitializer(constructorSyntax); - } + internal static IEnumerable GetActiveTokensForImplicitConstructorInitializer(ConstructorDeclarationSyntax constructor) + => constructor.Modifiers.Concat(SpecializedCollections.SingletonEnumerable(constructor.Identifier)).Concat(constructor.ParameterList.DescendantTokens()); - internal static TextSpan CreateSpanForImplicitConstructorInitializer(ConstructorDeclarationSyntax constructor) - => CreateSpan(constructor.Modifiers, constructor.Identifier, constructor.ParameterList.CloseParenToken); + internal static TextSpan CreateSpanForExplicitConstructorInitializer(ConstructorInitializerSyntax constructorInitializer) + => CreateSpan(constructorInitializer.ThisOrBaseKeyword, constructorInitializer.ArgumentList.CloseParenToken); - internal static IEnumerable GetActiveTokensForImplicitConstructorInitializer(ConstructorDeclarationSyntax constructor) - => constructor.Modifiers.Concat(SpecializedCollections.SingletonEnumerable(constructor.Identifier)).Concat(constructor.ParameterList.DescendantTokens()); + internal static IEnumerable GetActiveTokensForExplicitConstructorInitializer(ConstructorInitializerSyntax constructorInitializer) + => SpecializedCollections.SingletonEnumerable(constructorInitializer.ThisOrBaseKeyword).Concat(constructorInitializer.ArgumentList.DescendantTokens()); - internal static TextSpan CreateSpanForExplicitConstructorInitializer(ConstructorInitializerSyntax constructorInitializer) - => CreateSpan(constructorInitializer.ThisOrBaseKeyword, constructorInitializer.ArgumentList.CloseParenToken); + internal static TextSpan CreateSpanForImplicitPrimaryConstructorInitializer(TypeDeclarationSyntax typeDeclaration) + { + Debug.Assert(typeDeclaration.ParameterList != null); + return TextSpan.FromBounds(typeDeclaration.Identifier.SpanStart, typeDeclaration.ParameterList.Span.End); + } - internal static IEnumerable GetActiveTokensForExplicitConstructorInitializer(ConstructorInitializerSyntax constructorInitializer) - => SpecializedCollections.SingletonEnumerable(constructorInitializer.ThisOrBaseKeyword).Concat(constructorInitializer.ArgumentList.DescendantTokens()); + internal static IEnumerable GetActiveTokensForImplicitPrimaryConstructorInitializer(TypeDeclarationSyntax typeDeclaration) + { + Debug.Assert(typeDeclaration.ParameterList != null); + + yield return typeDeclaration.Identifier; - internal static TextSpan CreateSpanForImplicitPrimaryConstructorInitializer(TypeDeclarationSyntax typeDeclaration) + if (typeDeclaration.TypeParameterList != null) { - Debug.Assert(typeDeclaration.ParameterList != null); - return TextSpan.FromBounds(typeDeclaration.Identifier.SpanStart, typeDeclaration.ParameterList.Span.End); + foreach (var token in typeDeclaration.TypeParameterList.DescendantTokens()) + yield return token; } - internal static IEnumerable GetActiveTokensForImplicitPrimaryConstructorInitializer(TypeDeclarationSyntax typeDeclaration) - { - Debug.Assert(typeDeclaration.ParameterList != null); + foreach (var token in typeDeclaration.ParameterList.DescendantTokens()) + yield return token; + } - yield return typeDeclaration.Identifier; + internal static TextSpan CreateSpanForExplicitPrimaryConstructorInitializer(PrimaryConstructorBaseTypeSyntax baseTypeSyntax) + => baseTypeSyntax.Span; - if (typeDeclaration.TypeParameterList != null) - { - foreach (var token in typeDeclaration.TypeParameterList.DescendantTokens()) - yield return token; - } + internal static IEnumerable GetActiveTokensForExplicitPrimaryConstructorInitializer(PrimaryConstructorBaseTypeSyntax baseTypeSyntax) + => baseTypeSyntax.DescendantTokens(); + + internal static TextSpan CreateSpanForCopyConstructor(RecordDeclarationSyntax recordDeclaration) + => CreateSpan( + recordDeclaration.Identifier, + LastNotMissing(recordDeclaration.Identifier, recordDeclaration.TypeParameterList?.GreaterThanToken ?? default)); - foreach (var token in typeDeclaration.ParameterList.DescendantTokens()) + internal static IEnumerable GetActiveTokensForCopyConstructor(RecordDeclarationSyntax recordDeclaration) + { + yield return recordDeclaration.Identifier; + + if (recordDeclaration.TypeParameterList != null) + { + foreach (var token in recordDeclaration.TypeParameterList.DescendantTokens()) yield return token; } + } - internal static TextSpan CreateSpanForExplicitPrimaryConstructorInitializer(PrimaryConstructorBaseTypeSyntax baseTypeSyntax) - => baseTypeSyntax.Span; - - internal static IEnumerable GetActiveTokensForExplicitPrimaryConstructorInitializer(PrimaryConstructorBaseTypeSyntax baseTypeSyntax) - => baseTypeSyntax.DescendantTokens(); + internal static TextSpan CreateSpanForRecordParameter(ParameterSyntax parameter) + => CreateSpan(parameter.Modifiers, parameter.Type, parameter.Identifier); - internal static TextSpan CreateSpanForCopyConstructor(RecordDeclarationSyntax recordDeclaration) - => CreateSpan( - recordDeclaration.Identifier, - LastNotMissing(recordDeclaration.Identifier, recordDeclaration.TypeParameterList?.GreaterThanToken ?? default)); + internal static IEnumerable GetActiveTokensForRecordParameter(ParameterSyntax parameter) + { + foreach (var modifier in parameter.Modifiers) + yield return modifier; - internal static IEnumerable GetActiveTokensForCopyConstructor(RecordDeclarationSyntax recordDeclaration) + if (parameter.Type != null) { - yield return recordDeclaration.Identifier; - - if (recordDeclaration.TypeParameterList != null) - { - foreach (var token in recordDeclaration.TypeParameterList.DescendantTokens()) - yield return token; - } + foreach (var token in parameter.Type.DescendantTokens()) + yield return token; } - internal static TextSpan CreateSpanForRecordParameter(ParameterSyntax parameter) - => CreateSpan(parameter.Modifiers, parameter.Type, parameter.Identifier); + yield return parameter.Identifier; + } - internal static IEnumerable GetActiveTokensForRecordParameter(ParameterSyntax parameter) - { - foreach (var modifier in parameter.Modifiers) - yield return modifier; + internal static TextSpan CreateSpanForAutoPropertyAccessor(AccessorDeclarationSyntax accessor) + => accessor.Span; - if (parameter.Type != null) - { - foreach (var token in parameter.Type.DescendantTokens()) - yield return token; - } + internal static IEnumerable GetActiveTokensForAutoPropertyAccessor(AccessorDeclarationSyntax accessor) + => accessor.DescendantTokens(); - yield return parameter.Identifier; - } - - internal static TextSpan CreateSpanForAutoPropertyAccessor(AccessorDeclarationSyntax accessor) - => accessor.Span; + private static TextSpan? TryCreateSpanForFieldDeclaration(BaseFieldDeclarationSyntax fieldDeclaration, int position) + => TryCreateSpanForVariableDeclaration(fieldDeclaration.Declaration, fieldDeclaration.Modifiers, fieldDeclaration.SemicolonToken, position); - internal static IEnumerable GetActiveTokensForAutoPropertyAccessor(AccessorDeclarationSyntax accessor) - => accessor.DescendantTokens(); + private static TextSpan? TryCreateSpanForSwitchLabel(SwitchLabelSyntax switchLabel, int position) + { + if (switchLabel.Parent is not SwitchSectionSyntax switchSection || switchSection.Statements.Count == 0) + { + return null; + } - private static TextSpan? TryCreateSpanForFieldDeclaration(BaseFieldDeclarationSyntax fieldDeclaration, int position) - => TryCreateSpanForVariableDeclaration(fieldDeclaration.Declaration, fieldDeclaration.Modifiers, fieldDeclaration.SemicolonToken, position); + return TryCreateSpanForNode(switchSection.Statements[0], position); + } - private static TextSpan? TryCreateSpanForSwitchLabel(SwitchLabelSyntax switchLabel, int position) + private static TextSpan CreateSpanForBlock(BlockSyntax block, int position) + { + // If the user was on the close curly of the block, then set the breakpoint + // there. Otherwise, set it on the open curly. + if (position >= block.OpenBraceToken.FullSpan.End) { - if (switchLabel.Parent is not SwitchSectionSyntax switchSection || switchSection.Statements.Count == 0) - { - return null; - } - - return TryCreateSpanForNode(switchSection.Statements[0], position); + return CreateSpan(block.CloseBraceToken); } - - private static TextSpan CreateSpanForBlock(BlockSyntax block, int position) + else { - // If the user was on the close curly of the block, then set the breakpoint - // there. Otherwise, set it on the open curly. - if (position >= block.OpenBraceToken.FullSpan.End) - { - return CreateSpan(block.CloseBraceToken); - } - else - { - return CreateSpan(block.OpenBraceToken); - } + return CreateSpan(block.OpenBraceToken); } + } - private static TextSpan? TryCreateSpanForStatement(StatementSyntax statement, int position) + private static TextSpan? TryCreateSpanForStatement(StatementSyntax statement, int position) + { + if (statement == null) { - if (statement == null) - { - return null; - } + return null; + } - switch (statement.Kind()) - { - case SyntaxKind.Block: - return CreateSpanForBlock((BlockSyntax)statement, position); - - case SyntaxKind.LocalDeclarationStatement: - // If the declaration has multiple variables then just set the breakpoint on the first - // variable declarator. Otherwise, set the breakpoint over this entire - // statement. - var declarationStatement = (LocalDeclarationStatementSyntax)statement; - return TryCreateSpanForVariableDeclaration(declarationStatement.Declaration, declarationStatement.Modifiers, - declarationStatement.SemicolonToken, position); - - case SyntaxKind.LabeledStatement: - // Create the breakpoint on the actual statement we are labeling: - var labeledStatement = (LabeledStatementSyntax)statement; - return TryCreateSpanForStatement(labeledStatement.Statement, position); - - case SyntaxKind.WhileStatement: - // Note: if the user was in the body of the while, then we would have hit its - // nested statement on the way up. This means we must be on the "while(expr)" - // part. Rather than putting a bp on the entire statement, just put it on the - // top portion. - var whileStatement = (WhileStatementSyntax)statement; - return CreateSpan(whileStatement, whileStatement.CloseParenToken); - - case SyntaxKind.DoStatement: - // Note: if the user was in the body of the while, then we would have hit its nested - // statement on the way up. This means we're either in the "while(expr)" portion or - // the "do" portion. - var doStatement = (DoStatementSyntax)statement; - if (position < doStatement.Statement.Span.Start) - { - return TryCreateSpanForStatement(doStatement.Statement, position); - } - else - { - return CreateSpan(doStatement.WhileKeyword, - LastNotMissing(doStatement.CloseParenToken, doStatement.SemicolonToken)); - } + switch (statement.Kind()) + { + case SyntaxKind.Block: + return CreateSpanForBlock((BlockSyntax)statement, position); + + case SyntaxKind.LocalDeclarationStatement: + // If the declaration has multiple variables then just set the breakpoint on the first + // variable declarator. Otherwise, set the breakpoint over this entire + // statement. + var declarationStatement = (LocalDeclarationStatementSyntax)statement; + return TryCreateSpanForVariableDeclaration(declarationStatement.Declaration, declarationStatement.Modifiers, + declarationStatement.SemicolonToken, position); + + case SyntaxKind.LabeledStatement: + // Create the breakpoint on the actual statement we are labeling: + var labeledStatement = (LabeledStatementSyntax)statement; + return TryCreateSpanForStatement(labeledStatement.Statement, position); + + case SyntaxKind.WhileStatement: + // Note: if the user was in the body of the while, then we would have hit its + // nested statement on the way up. This means we must be on the "while(expr)" + // part. Rather than putting a bp on the entire statement, just put it on the + // top portion. + var whileStatement = (WhileStatementSyntax)statement; + return CreateSpan(whileStatement, whileStatement.CloseParenToken); + + case SyntaxKind.DoStatement: + // Note: if the user was in the body of the while, then we would have hit its nested + // statement on the way up. This means we're either in the "while(expr)" portion or + // the "do" portion. + var doStatement = (DoStatementSyntax)statement; + if (position < doStatement.Statement.Span.Start) + { + return TryCreateSpanForStatement(doStatement.Statement, position); + } + else + { + return CreateSpan(doStatement.WhileKeyword, + LastNotMissing(doStatement.CloseParenToken, doStatement.SemicolonToken)); + } - case SyntaxKind.ForStatement: - // Note: if the user was in the body of the for, then we would have hit its nested - // statement on the way up. If they were in the condition or the incrementors, then - // we would have those on the way up as well (in TryCreateBreakpointSpanForExpression or - // CreateBreakpointSpanForVariableDeclarator). So the user must be on the 'for' - // itself. in that case, set the bp on the variable declaration or initializers - var forStatement = (ForStatementSyntax)statement; - if (forStatement.Declaration != null) - { - // for (int i = 0; ... - var firstVariable = forStatement.Declaration.Variables.FirstOrDefault(); - return CreateSpan(default, forStatement.Declaration.Type, firstVariable); - } - else if (forStatement.Initializers.Count > 0) - { - // for (i = 0; ... - return CreateSpan(forStatement.Initializers[0]); - } - else if (forStatement.Condition != null) - { - // for (; i > 0; ...) - return CreateSpan(forStatement.Condition); - } - else if (forStatement.Incrementors.Count > 0) - { - // for (;;...) - return CreateSpan(forStatement.Incrementors[0]); - } - else - { - // for (;;) - // - // In this case, just set the bp on the contained statement. - return TryCreateSpanForStatement(forStatement.Statement, position); - } + case SyntaxKind.ForStatement: + // Note: if the user was in the body of the for, then we would have hit its nested + // statement on the way up. If they were in the condition or the incrementors, then + // we would have those on the way up as well (in TryCreateBreakpointSpanForExpression or + // CreateBreakpointSpanForVariableDeclarator). So the user must be on the 'for' + // itself. in that case, set the bp on the variable declaration or initializers + var forStatement = (ForStatementSyntax)statement; + if (forStatement.Declaration != null) + { + // for (int i = 0; ... + var firstVariable = forStatement.Declaration.Variables.FirstOrDefault(); + return CreateSpan(default, forStatement.Declaration.Type, firstVariable); + } + else if (forStatement.Initializers.Count > 0) + { + // for (i = 0; ... + return CreateSpan(forStatement.Initializers[0]); + } + else if (forStatement.Condition != null) + { + // for (; i > 0; ...) + return CreateSpan(forStatement.Condition); + } + else if (forStatement.Incrementors.Count > 0) + { + // for (;;...) + return CreateSpan(forStatement.Incrementors[0]); + } + else + { + // for (;;) + // + // In this case, just set the bp on the contained statement. + return TryCreateSpanForStatement(forStatement.Statement, position); + } - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - // Note: if the user was in the body of the foreach, then we would have hit its - // nested statement on the way up. If they were in the expression then we would - // have hit that on the way up as well. In "foreach(var f in expr)" we allow a - // bp on "foreach", "var f" and "in". - var forEachStatement = (CommonForEachStatementSyntax)statement; - if (position < forEachStatement.OpenParenToken.Span.End || position > forEachStatement.CloseParenToken.SpanStart) - { - return CreateSpan(forEachStatement.ForEachKeyword); - } - else if (position < forEachStatement.InKeyword.FullSpan.Start) - { - if (forEachStatement.Kind() == SyntaxKind.ForEachStatement) - { - var simpleForEachStatement = (ForEachStatementSyntax)statement; - return CreateSpan(simpleForEachStatement.Type, simpleForEachStatement.Identifier); - } - else - { - return ((ForEachVariableStatementSyntax)statement).Variable.Span; - } - } - else if (position < forEachStatement.Expression.FullSpan.Start) + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + // Note: if the user was in the body of the foreach, then we would have hit its + // nested statement on the way up. If they were in the expression then we would + // have hit that on the way up as well. In "foreach(var f in expr)" we allow a + // bp on "foreach", "var f" and "in". + var forEachStatement = (CommonForEachStatementSyntax)statement; + if (position < forEachStatement.OpenParenToken.Span.End || position > forEachStatement.CloseParenToken.SpanStart) + { + return CreateSpan(forEachStatement.ForEachKeyword); + } + else if (position < forEachStatement.InKeyword.FullSpan.Start) + { + if (forEachStatement.Kind() == SyntaxKind.ForEachStatement) { - return CreateSpan(forEachStatement.InKeyword); + var simpleForEachStatement = (ForEachStatementSyntax)statement; + return CreateSpan(simpleForEachStatement.Type, simpleForEachStatement.Identifier); } else { - return CreateSpan(forEachStatement.Expression); + return ((ForEachVariableStatementSyntax)statement).Variable.Span; } + } + else if (position < forEachStatement.Expression.FullSpan.Start) + { + return CreateSpan(forEachStatement.InKeyword); + } + else + { + return CreateSpan(forEachStatement.Expression); + } - case SyntaxKind.UsingStatement: - var usingStatement = (UsingStatementSyntax)statement; - if (usingStatement.Declaration != null) - { - return TryCreateSpanForNode(usingStatement.Declaration, position); - } - else - { - return CreateSpan(usingStatement, usingStatement.CloseParenToken); - } + case SyntaxKind.UsingStatement: + var usingStatement = (UsingStatementSyntax)statement; + if (usingStatement.Declaration != null) + { + return TryCreateSpanForNode(usingStatement.Declaration, position); + } + else + { + return CreateSpan(usingStatement, usingStatement.CloseParenToken); + } - case SyntaxKind.FixedStatement: - var fixedStatement = (FixedStatementSyntax)statement; - return TryCreateSpanForNode(fixedStatement.Declaration, position); - - case SyntaxKind.CheckedStatement: - case SyntaxKind.UncheckedStatement: - var checkedStatement = (CheckedStatementSyntax)statement; - return TryCreateSpanForStatement(checkedStatement.Block, position); - - case SyntaxKind.UnsafeStatement: - var unsafeStatement = (UnsafeStatementSyntax)statement; - return TryCreateSpanForStatement(unsafeStatement.Block, position); - - case SyntaxKind.LockStatement: - // Note: if the user was in the body of the 'lock', then we would have hit its - // nested statement on the way up. This means we must be on the "lock(expr)" part. - // Rather than putting a bp on the entire statement, just put it on the top portion. - var lockStatement = (LockStatementSyntax)statement; - return CreateSpan(lockStatement, lockStatement.CloseParenToken); - - case SyntaxKind.IfStatement: - // Note: if the user was in the body of the 'if' or the 'else', then we would have - // hit its nested statement on the way up. This means we must be on the "if(expr)" - // part. Rather than putting a bp on the entire statement, just put it on the top - // portion. - var ifStatement = (IfStatementSyntax)statement; - return CreateSpan(ifStatement, ifStatement.CloseParenToken); - - case SyntaxKind.SwitchStatement: - // Note: Any nested statements in the switch will already have been hit on the - // way up. Similarly, hitting a 'case' label will already have been taken care - // of. So in this case, we just set the bp on the "switch(expr)" itself. - var switchStatement = (SwitchStatementSyntax)statement; - return CreateSpan(switchStatement, (switchStatement.CloseParenToken != default) ? switchStatement.CloseParenToken : switchStatement.Expression.GetLastToken()); - - case SyntaxKind.TryStatement: - // Note: if the user was in the body of the 'try', then we would have hit its nested - // statement on the way up. This means we must be on the "try" part. In this case, - // just set the BP on the start of the block. Note: if they were in a catch or - // finally section, then that will already have been taken care of above. - var tryStatement = (TryStatementSyntax)statement; - return TryCreateSpanForStatement(tryStatement.Block, position); - - // All these cases are handled by just putting a breakpoint over the entire - // statement - case SyntaxKind.GotoStatement: - case SyntaxKind.GotoCaseStatement: - case SyntaxKind.GotoDefaultStatement: - case SyntaxKind.BreakStatement: - case SyntaxKind.ContinueStatement: - case SyntaxKind.ReturnStatement: - case SyntaxKind.YieldReturnStatement: - case SyntaxKind.YieldBreakStatement: - case SyntaxKind.ThrowStatement: - case SyntaxKind.ExpressionStatement: - case SyntaxKind.EmptyStatement: - default: - // Fallback case. If it was none of the above types of statements, then we make a span - // over the entire statement. Note: this is not a very desirable thing to do (as - // statements can often span multiple lines. So, when possible, we should try to do - // better. - return CreateSpan(statement); - } + case SyntaxKind.FixedStatement: + var fixedStatement = (FixedStatementSyntax)statement; + return TryCreateSpanForNode(fixedStatement.Declaration, position); + + case SyntaxKind.CheckedStatement: + case SyntaxKind.UncheckedStatement: + var checkedStatement = (CheckedStatementSyntax)statement; + return TryCreateSpanForStatement(checkedStatement.Block, position); + + case SyntaxKind.UnsafeStatement: + var unsafeStatement = (UnsafeStatementSyntax)statement; + return TryCreateSpanForStatement(unsafeStatement.Block, position); + + case SyntaxKind.LockStatement: + // Note: if the user was in the body of the 'lock', then we would have hit its + // nested statement on the way up. This means we must be on the "lock(expr)" part. + // Rather than putting a bp on the entire statement, just put it on the top portion. + var lockStatement = (LockStatementSyntax)statement; + return CreateSpan(lockStatement, lockStatement.CloseParenToken); + + case SyntaxKind.IfStatement: + // Note: if the user was in the body of the 'if' or the 'else', then we would have + // hit its nested statement on the way up. This means we must be on the "if(expr)" + // part. Rather than putting a bp on the entire statement, just put it on the top + // portion. + var ifStatement = (IfStatementSyntax)statement; + return CreateSpan(ifStatement, ifStatement.CloseParenToken); + + case SyntaxKind.SwitchStatement: + // Note: Any nested statements in the switch will already have been hit on the + // way up. Similarly, hitting a 'case' label will already have been taken care + // of. So in this case, we just set the bp on the "switch(expr)" itself. + var switchStatement = (SwitchStatementSyntax)statement; + return CreateSpan(switchStatement, (switchStatement.CloseParenToken != default) ? switchStatement.CloseParenToken : switchStatement.Expression.GetLastToken()); + + case SyntaxKind.TryStatement: + // Note: if the user was in the body of the 'try', then we would have hit its nested + // statement on the way up. This means we must be on the "try" part. In this case, + // just set the BP on the start of the block. Note: if they were in a catch or + // finally section, then that will already have been taken care of above. + var tryStatement = (TryStatementSyntax)statement; + return TryCreateSpanForStatement(tryStatement.Block, position); + + // All these cases are handled by just putting a breakpoint over the entire + // statement + case SyntaxKind.GotoStatement: + case SyntaxKind.GotoCaseStatement: + case SyntaxKind.GotoDefaultStatement: + case SyntaxKind.BreakStatement: + case SyntaxKind.ContinueStatement: + case SyntaxKind.ReturnStatement: + case SyntaxKind.YieldReturnStatement: + case SyntaxKind.YieldBreakStatement: + case SyntaxKind.ThrowStatement: + case SyntaxKind.ExpressionStatement: + case SyntaxKind.EmptyStatement: + default: + // Fallback case. If it was none of the above types of statements, then we make a span + // over the entire statement. Note: this is not a very desirable thing to do (as + // statements can often span multiple lines. So, when possible, we should try to do + // better. + return CreateSpan(statement); } + } - private static SyntaxToken LastNotMissing(SyntaxToken token1, SyntaxToken token2) - => token2.IsKind(SyntaxKind.None) || token2.IsMissing ? token1 : token2; + private static SyntaxToken LastNotMissing(SyntaxToken token1, SyntaxToken token2) + => token2.IsKind(SyntaxKind.None) || token2.IsMissing ? token1 : token2; - private static TextSpan? TryCreateSpanForVariableDeclaration(VariableDeclarationSyntax declaration, int position) - => declaration.Parent!.Kind() switch - { - // parent node will handle: - SyntaxKind.LocalDeclarationStatement or SyntaxKind.EventFieldDeclaration or SyntaxKind.FieldDeclaration => null, + private static TextSpan? TryCreateSpanForVariableDeclaration(VariableDeclarationSyntax declaration, int position) + => declaration.Parent!.Kind() switch + { + // parent node will handle: + SyntaxKind.LocalDeclarationStatement or SyntaxKind.EventFieldDeclaration or SyntaxKind.FieldDeclaration => null, - _ => TryCreateSpanForVariableDeclaration(declaration, modifiersOpt: default, semicolonOpt: default, position), - }; + _ => TryCreateSpanForVariableDeclaration(declaration, modifiersOpt: default, semicolonOpt: default, position), + }; - private static TextSpan? TryCreateSpanForVariableDeclaration( - VariableDeclarationSyntax variableDeclaration, - SyntaxTokenList modifiersOpt, - SyntaxToken semicolonOpt, - int position) + private static TextSpan? TryCreateSpanForVariableDeclaration( + VariableDeclarationSyntax variableDeclaration, + SyntaxTokenList modifiersOpt, + SyntaxToken semicolonOpt, + int position) + { + if (variableDeclaration.Variables.Count == 0) { - if (variableDeclaration.Variables.Count == 0) - { - return null; - } + return null; + } - if (modifiersOpt.Any(SyntaxKind.ConstKeyword)) + if (modifiersOpt.Any(SyntaxKind.ConstKeyword)) + { + // no sequence points are emitted for const fields/locals + return default(TextSpan); + } + + if (variableDeclaration.Variables.Count == 1) + { + if (variableDeclaration.Variables[0].Initializer == null) { - // no sequence points are emitted for const fields/locals return default(TextSpan); } - if (variableDeclaration.Variables.Count == 1) - { - if (variableDeclaration.Variables[0].Initializer == null) - { - return default(TextSpan); - } + return CreateSpan(modifiersOpt, variableDeclaration, semicolonOpt); + } - return CreateSpan(modifiersOpt, variableDeclaration, semicolonOpt); - } + if (semicolonOpt != default && position > semicolonOpt.SpanStart) + { + position = variableDeclaration.SpanStart; + } - if (semicolonOpt != default && position > semicolonOpt.SpanStart) - { - position = variableDeclaration.SpanStart; - } + var variableDeclarator = FindClosestDeclaratorWithInitializer(variableDeclaration.Variables, position); + if (variableDeclarator == null) + { + return default(TextSpan); + } - var variableDeclarator = FindClosestDeclaratorWithInitializer(variableDeclaration.Variables, position); - if (variableDeclarator == null) - { - return default(TextSpan); - } + if (variableDeclarator == variableDeclaration.Variables[0]) + { + return CreateSpan(modifiersOpt, variableDeclaration, variableDeclarator); + } - if (variableDeclarator == variableDeclaration.Variables[0]) - { - return CreateSpan(modifiersOpt, variableDeclaration, variableDeclarator); - } + return CreateSpan(variableDeclarator); + } - return CreateSpan(variableDeclarator); + internal static TextSpan CreateSpanForVariableDeclarator( + VariableDeclaratorSyntax variableDeclarator, + SyntaxTokenList modifiers, + SyntaxToken semicolon) + { + if (variableDeclarator.Initializer == null || modifiers.Any(SyntaxKind.ConstKeyword)) + { + return default; } - internal static TextSpan CreateSpanForVariableDeclarator( - VariableDeclaratorSyntax variableDeclarator, - SyntaxTokenList modifiers, - SyntaxToken semicolon) + var variableDeclaration = (VariableDeclarationSyntax)variableDeclarator.Parent!; + if (variableDeclaration.Variables.Count == 1) { - if (variableDeclarator.Initializer == null || modifiers.Any(SyntaxKind.ConstKeyword)) - { - return default; - } + return CreateSpan(modifiers, variableDeclaration, semicolon); + } - var variableDeclaration = (VariableDeclarationSyntax)variableDeclarator.Parent!; - if (variableDeclaration.Variables.Count == 1) - { - return CreateSpan(modifiers, variableDeclaration, semicolon); - } + if (variableDeclarator == variableDeclaration.Variables[0]) + { + return CreateSpan(modifiers, variableDeclaration, variableDeclarator); + } - if (variableDeclarator == variableDeclaration.Variables[0]) - { - return CreateSpan(modifiers, variableDeclaration, variableDeclarator); - } + return CreateSpan(variableDeclarator); + } + + internal static IEnumerable GetActiveTokensForVariableDeclarator(VariableDeclaratorSyntax variableDeclarator, SyntaxTokenList modifiers, SyntaxToken semicolon) + { + if (variableDeclarator.Initializer == null || modifiers.Any(SyntaxKind.ConstKeyword)) + { + return SpecializedCollections.EmptyEnumerable(); + } - return CreateSpan(variableDeclarator); + // [|int F = 1;|] + var variableDeclaration = (VariableDeclarationSyntax)variableDeclarator.Parent!; + if (variableDeclaration.Variables.Count == 1) + { + return modifiers.Concat(variableDeclaration.DescendantTokens()).Concat(semicolon); } - internal static IEnumerable GetActiveTokensForVariableDeclarator(VariableDeclaratorSyntax variableDeclarator, SyntaxTokenList modifiers, SyntaxToken semicolon) + // [|int F = 1|], G = 2; + if (variableDeclarator == variableDeclaration.Variables[0]) { - if (variableDeclarator.Initializer == null || modifiers.Any(SyntaxKind.ConstKeyword)) + return modifiers.Concat(variableDeclaration.Type.DescendantTokens()).Concat(variableDeclarator.DescendantTokens()); + } + + // int F = 1, [|G = 2|]; + return variableDeclarator.DescendantTokens(); + } + + private static VariableDeclaratorSyntax? FindClosestDeclaratorWithInitializer(SeparatedSyntaxList declarators, int position) + { + var d = GetItemIndexByPosition(declarators, position); + var i = 0; + while (true) + { + var left = d - i; + var right = d + i; + if (left < 0 && right >= declarators.Count) { - return SpecializedCollections.EmptyEnumerable(); + return null; } - // [|int F = 1;|] - var variableDeclaration = (VariableDeclarationSyntax)variableDeclarator.Parent!; - if (variableDeclaration.Variables.Count == 1) + if (left >= 0 && declarators[left].Initializer != null) { - return modifiers.Concat(variableDeclaration.DescendantTokens()).Concat(semicolon); + return declarators[left]; } - // [|int F = 1|], G = 2; - if (variableDeclarator == variableDeclaration.Variables[0]) + if (right < declarators.Count && declarators[right].Initializer != null) { - return modifiers.Concat(variableDeclaration.Type.DescendantTokens()).Concat(variableDeclarator.DescendantTokens()); + return declarators[right]; } - // int F = 1, [|G = 2|]; - return variableDeclarator.DescendantTokens(); + i += 1; } + } - private static VariableDeclaratorSyntax? FindClosestDeclaratorWithInitializer(SeparatedSyntaxList declarators, int position) + private static int GetItemIndexByPosition(SeparatedSyntaxList list, int position) + where TNode : SyntaxNode + { + for (var i = list.SeparatorCount - 1; i >= 0; i--) { - var d = GetItemIndexByPosition(declarators, position); - var i = 0; - while (true) + if (position > list.GetSeparator(i).SpanStart) { - var left = d - i; - var right = d + i; - if (left < 0 && right >= declarators.Count) - { - return null; - } - - if (left >= 0 && declarators[left].Initializer != null) - { - return declarators[left]; - } - - if (right < declarators.Count && declarators[right].Initializer != null) - { - return declarators[right]; - } - - i += 1; + return i + 1; } } - private static int GetItemIndexByPosition(SeparatedSyntaxList list, int position) - where TNode : SyntaxNode - { - for (var i = list.SeparatorCount - 1; i >= 0; i--) - { - if (position > list.GetSeparator(i).SpanStart) - { - return i + 1; - } - } + return 0; + } - return 0; + private static TextSpan CreateSpanForCatchClause(CatchClauseSyntax catchClause) + { + if (catchClause.Filter != null) + { + return CreateSpan(catchClause.Filter); } - - private static TextSpan CreateSpanForCatchClause(CatchClauseSyntax catchClause) + else if (catchClause.Declaration != null) { - if (catchClause.Filter != null) - { - return CreateSpan(catchClause.Filter); - } - else if (catchClause.Declaration != null) - { - return CreateSpan(catchClause.CatchKeyword, catchClause.Declaration.CloseParenToken); - } - else - { - return CreateSpan(catchClause.CatchKeyword); - } + return CreateSpan(catchClause.CatchKeyword, catchClause.Declaration.CloseParenToken); } + else + { + return CreateSpan(catchClause.CatchKeyword); + } + } - /// - /// There are a few places where we allow breakpoints on expressions. - /// - /// 1) When the expression is the body of a lambda/method/operator/property/indexer. - /// 2) The expression is a breakable expression inside a query expression. - /// 3) The expression is in a for statement initializer, condition or incrementor. - /// 4) The expression is a foreach initializer. - /// 5) The expression is the value of an arm of a switch expression - /// - private static bool IsBreakableExpression(ExpressionSyntax expression) + /// + /// There are a few places where we allow breakpoints on expressions. + /// + /// 1) When the expression is the body of a lambda/method/operator/property/indexer. + /// 2) The expression is a breakable expression inside a query expression. + /// 3) The expression is in a for statement initializer, condition or incrementor. + /// 4) The expression is a foreach initializer. + /// 5) The expression is the value of an arm of a switch expression + /// + private static bool IsBreakableExpression(ExpressionSyntax expression) + { + if (expression == null || expression.Parent == null) { - if (expression == null || expression.Parent == null) - { - return false; - } + return false; + } - var parent = expression.Parent; - switch (parent.Kind()) - { - case SyntaxKind.ArrowExpressionClause: - Debug.Assert(((ArrowExpressionClauseSyntax)parent).Expression == expression); - return true; + var parent = expression.Parent; + switch (parent.Kind()) + { + case SyntaxKind.ArrowExpressionClause: + Debug.Assert(((ArrowExpressionClauseSyntax)parent).Expression == expression); + return true; - case SyntaxKind.SwitchExpressionArm: - Debug.Assert(((SwitchExpressionArmSyntax)parent).Expression == expression); - return true; + case SyntaxKind.SwitchExpressionArm: + Debug.Assert(((SwitchExpressionArmSyntax)parent).Expression == expression); + return true; - case SyntaxKind.ForStatement: - var forStatement = (ForStatementSyntax)parent; - return - forStatement.Initializers.Contains(expression) || - forStatement.Condition == expression || - forStatement.Incrementors.Contains(expression); + case SyntaxKind.ForStatement: + var forStatement = (ForStatementSyntax)parent; + return + forStatement.Initializers.Contains(expression) || + forStatement.Condition == expression || + forStatement.Incrementors.Contains(expression); - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - var forEachStatement = (CommonForEachStatementSyntax)parent; - return forEachStatement.Expression == expression; + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + var forEachStatement = (CommonForEachStatementSyntax)parent; + return forEachStatement.Expression == expression; - default: - return LambdaUtilities.IsLambdaBodyStatementOrExpression(expression); - } + default: + return LambdaUtilities.IsLambdaBodyStatementOrExpression(expression); } + } - private static TextSpan? CreateSpanForAccessors(SyntaxList accessors, int position) + private static TextSpan? CreateSpanForAccessors(SyntaxList accessors, int position) + { + for (var i = 0; i < accessors.Count; i++) { - for (var i = 0; i < accessors.Count; i++) + if (position <= accessors[i].FullSpan.End || i == accessors.Count - 1) { - if (position <= accessors[i].FullSpan.End || i == accessors.Count - 1) - { - return TryCreateSpanForNode(accessors[i], position); - } + return TryCreateSpanForNode(accessors[i], position); } - - return null; } + + return null; } } diff --git a/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs b/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs index ed17282030724..b2c1e3974176b 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs @@ -24,3050 +24,3049 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +internal sealed class CSharpEditAndContinueAnalyzer(Action? testFaultInjector = null) : AbstractEditAndContinueAnalyzer(testFaultInjector) { - internal sealed class CSharpEditAndContinueAnalyzer(Action? testFaultInjector = null) : AbstractEditAndContinueAnalyzer(testFaultInjector) + [ExportLanguageServiceFactory(typeof(IEditAndContinueAnalyzer), LanguageNames.CSharp), Shared] + internal sealed class Factory : ILanguageServiceFactory { - [ExportLanguageServiceFactory(typeof(IEditAndContinueAnalyzer), LanguageNames.CSharp), Shared] - internal sealed class Factory : ILanguageServiceFactory + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public Factory() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public Factory() - { - } - - public ILanguageService CreateLanguageService(HostLanguageServices languageServices) - { - return new CSharpEditAndContinueAnalyzer(testFaultInjector: null); - } } - #region Syntax Analysis - - private enum BlockPart + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - OpenBrace = DefaultStatementPart, - CloseBrace = 1, + return new CSharpEditAndContinueAnalyzer(testFaultInjector: null); } + } - private enum ForEachPart - { - ForEach = DefaultStatementPart, - VariableDeclaration = 1, - In = 2, - Expression = 3, - } + #region Syntax Analysis - private enum SwitchExpressionPart - { - WholeExpression = DefaultStatementPart, + private enum BlockPart + { + OpenBrace = DefaultStatementPart, + CloseBrace = 1, + } - // An active statement that covers IL generated for the decision tree: - // [|switch { , ..., }|] - // This active statement is never a leaf active statement (does not correspond to a breakpoint span). - SwitchBody = 1, - } + private enum ForEachPart + { + ForEach = DefaultStatementPart, + VariableDeclaration = 1, + In = 2, + Expression = 3, + } - /// - /// for methods, operators, constructors, destructors and accessors. - /// for field initializers. - /// for property initializers and expression bodies. - /// for indexer expression bodies. - /// for getter of an expression-bodied property/indexer. - /// for top-level statements. - /// for record copy-constructors. - /// for primary constructors. - /// for record primary constructor parameters. - /// - internal override bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode node, TextSpan activeSpan, out OneOrMany declarations) - { - var current = node; - while (current != null && current != root) - { - switch (current.Kind()) - { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.StructDeclaration: - var typeDeclaration = (TypeDeclarationSyntax)current; + private enum SwitchExpressionPart + { + WholeExpression = DefaultStatementPart, - // type declaration with primary constructor - if (typeDeclaration.ParameterList != null) - { - declarations = new(typeDeclaration.ParameterList); - return true; - } + // An active statement that covers IL generated for the decision tree: + // [|switch { , ..., }|] + // This active statement is never a leaf active statement (does not correspond to a breakpoint span). + SwitchBody = 1, + } - break; + /// + /// for methods, operators, constructors, destructors and accessors. + /// for field initializers. + /// for property initializers and expression bodies. + /// for indexer expression bodies. + /// for getter of an expression-bodied property/indexer. + /// for top-level statements. + /// for record copy-constructors. + /// for primary constructors. + /// for record primary constructor parameters. + /// + internal override bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode node, TextSpan activeSpan, out OneOrMany declarations) + { + var current = node; + while (current != null && current != root) + { + switch (current.Kind()) + { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.StructDeclaration: + var typeDeclaration = (TypeDeclarationSyntax)current; - case SyntaxKind.RecordDeclaration: - case SyntaxKind.RecordStructDeclaration: - var recordDeclaration = (RecordDeclarationSyntax)current; + // type declaration with primary constructor + if (typeDeclaration.ParameterList != null) + { + declarations = new(typeDeclaration.ParameterList); + return true; + } - declarations = (recordDeclaration.ParameterList != null && activeSpan.OverlapsWith(recordDeclaration.ParameterList.Span)) - ? new(recordDeclaration.ParameterList) : new(recordDeclaration); + break; - return true; + case SyntaxKind.RecordDeclaration: + case SyntaxKind.RecordStructDeclaration: + var recordDeclaration = (RecordDeclarationSyntax)current; - case SyntaxKind.MethodDeclaration: - case SyntaxKind.ConversionOperatorDeclaration: - case SyntaxKind.OperatorDeclaration: - case SyntaxKind.SetAccessorDeclaration: - case SyntaxKind.InitAccessorDeclaration: - case SyntaxKind.AddAccessorDeclaration: - case SyntaxKind.RemoveAccessorDeclaration: - case SyntaxKind.GetAccessorDeclaration: - case SyntaxKind.ConstructorDeclaration: - case SyntaxKind.DestructorDeclaration: - declarations = new(current); - return true; + declarations = (recordDeclaration.ParameterList != null && activeSpan.OverlapsWith(recordDeclaration.ParameterList.Span)) + ? new(recordDeclaration.ParameterList) : new(recordDeclaration); - case SyntaxKind.PropertyDeclaration: - // int P { get; } = [|initializer|]; - Debug.Assert(((PropertyDeclarationSyntax)current).Initializer != null); - declarations = new(current); - return true; + return true; - case SyntaxKind.FieldDeclaration: - case SyntaxKind.EventFieldDeclaration: - // Active statements encompassing modifiers or type correspond to the first initialized field. - // [|public static int F = 1|], G = 2; - declarations = new(((BaseFieldDeclarationSyntax)current).Declaration.Variables.First()); - return true; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.ConversionOperatorDeclaration: + case SyntaxKind.OperatorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.InitAccessorDeclaration: + case SyntaxKind.AddAccessorDeclaration: + case SyntaxKind.RemoveAccessorDeclaration: + case SyntaxKind.GetAccessorDeclaration: + case SyntaxKind.ConstructorDeclaration: + case SyntaxKind.DestructorDeclaration: + declarations = new(current); + return true; - case SyntaxKind.Parameter: + case SyntaxKind.PropertyDeclaration: + // int P { get; } = [|initializer|]; + Debug.Assert(((PropertyDeclarationSyntax)current).Initializer != null); + declarations = new(current); + return true; - if (current is { Parent.Parent: RecordDeclarationSyntax }) - { - declarations = new(current); - return true; - } + case SyntaxKind.FieldDeclaration: + case SyntaxKind.EventFieldDeclaration: + // Active statements encompassing modifiers or type correspond to the first initialized field. + // [|public static int F = 1|], G = 2; + declarations = new(((BaseFieldDeclarationSyntax)current).Declaration.Variables.First()); + return true; - break; + case SyntaxKind.Parameter: - case SyntaxKind.VariableDeclarator: - // public static int F = 1, [|G = 2|]; - Debug.Assert(current.Parent.IsKind(SyntaxKind.VariableDeclaration)); + if (current is { Parent.Parent: RecordDeclarationSyntax }) + { + declarations = new(current); + return true; + } - switch (current.Parent.Parent!.Kind()) - { - case SyntaxKind.FieldDeclaration: - case SyntaxKind.EventFieldDeclaration: - declarations = new(current); - return true; - } + break; - current = current.Parent; - break; + case SyntaxKind.VariableDeclarator: + // public static int F = 1, [|G = 2|]; + Debug.Assert(current.Parent.IsKind(SyntaxKind.VariableDeclaration)); - case SyntaxKind.ArrowExpressionClause: - // represents getter symbol declaration node of a property/indexer with expression body - if (current.Parent is (kind: SyntaxKind.PropertyDeclaration or SyntaxKind.IndexerDeclaration)) - { + switch (current.Parent.Parent!.Kind()) + { + case SyntaxKind.FieldDeclaration: + case SyntaxKind.EventFieldDeclaration: declarations = new(current); return true; - } + } - break; + current = current.Parent; + break; - case SyntaxKind.GlobalStatement: - Debug.Assert(current.Parent.IsKind(SyntaxKind.CompilationUnit)); - declarations = new(current.Parent); + case SyntaxKind.ArrowExpressionClause: + // represents getter symbol declaration node of a property/indexer with expression body + if (current.Parent is (kind: SyntaxKind.PropertyDeclaration or SyntaxKind.IndexerDeclaration)) + { + declarations = new(current); return true; - } + } + + break; - current = current.Parent; + case SyntaxKind.GlobalStatement: + Debug.Assert(current.Parent.IsKind(SyntaxKind.CompilationUnit)); + declarations = new(current.Parent); + return true; } - declarations = default; - return false; + current = current.Parent; } - internal override MemberBody? TryGetDeclarationBody(SyntaxNode node, ISymbol? symbol) - => SyntaxUtilities.TryGetDeclarationBody(node, symbol); + declarations = default; + return false; + } - internal override bool IsDeclarationWithSharedBody(SyntaxNode declaration, ISymbol member) - => member is IMethodSymbol { AssociatedSymbol: IPropertySymbol property } && property.IsSynthesizedAutoProperty(); + internal override MemberBody? TryGetDeclarationBody(SyntaxNode node, ISymbol? symbol) + => SyntaxUtilities.TryGetDeclarationBody(node, symbol); - protected override bool AreHandledEventsEqual(IMethodSymbol oldMethod, IMethodSymbol newMethod) - => true; + internal override bool IsDeclarationWithSharedBody(SyntaxNode declaration, ISymbol member) + => member is IMethodSymbol { AssociatedSymbol: IPropertySymbol property } && property.IsSynthesizedAutoProperty(); - protected override IEnumerable GetVariableUseSites(IEnumerable roots, ISymbol localOrParameter, SemanticModel model, CancellationToken cancellationToken) - { - Debug.Assert(localOrParameter is IParameterSymbol or ILocalSymbol or IRangeVariableSymbol); + protected override bool AreHandledEventsEqual(IMethodSymbol oldMethod, IMethodSymbol newMethod) + => true; - // not supported (it's non trivial to find all places where "this" is used): - Debug.Assert(!localOrParameter.IsThisParameter()); + protected override IEnumerable GetVariableUseSites(IEnumerable roots, ISymbol localOrParameter, SemanticModel model, CancellationToken cancellationToken) + { + Debug.Assert(localOrParameter is IParameterSymbol or ILocalSymbol or IRangeVariableSymbol); + + // not supported (it's non trivial to find all places where "this" is used): + Debug.Assert(!localOrParameter.IsThisParameter()); + + return from root in roots + from node in root.DescendantNodesAndSelf() + where node.IsKind(SyntaxKind.IdentifierName) + let nameSyntax = (IdentifierNameSyntax)node + where (string?)nameSyntax.Identifier.Value == localOrParameter.Name && + (model.GetSymbolInfo(nameSyntax, cancellationToken).Symbol?.Equals(localOrParameter) ?? false) + select node; + } - return from root in roots - from node in root.DescendantNodesAndSelf() - where node.IsKind(SyntaxKind.IdentifierName) - let nameSyntax = (IdentifierNameSyntax)node - where (string?)nameSyntax.Identifier.Value == localOrParameter.Name && - (model.GetSymbolInfo(nameSyntax, cancellationToken).Symbol?.Equals(localOrParameter) ?? false) - select node; - } + internal static SyntaxNode FindStatementAndPartner( + TextSpan span, + SyntaxNode body, + SyntaxNode? partnerBody, + out SyntaxNode? partnerStatement, + out int statementPart) + { + var position = span.Start; - internal static SyntaxNode FindStatementAndPartner( - TextSpan span, - SyntaxNode body, - SyntaxNode? partnerBody, - out SyntaxNode? partnerStatement, - out int statementPart) + if (!body.FullSpan.Contains(position)) { - var position = span.Start; + // invalid position, let's find a labeled node that encompasses the body: + position = body.SpanStart; + } - if (!body.FullSpan.Contains(position)) - { - // invalid position, let's find a labeled node that encompasses the body: - position = body.SpanStart; - } + SyntaxNode node; + if (partnerBody != null) + { + FindLeafNodeAndPartner(body, position, partnerBody, out node, out partnerStatement); + } + else + { + node = body.FindToken(position).Parent!; + partnerStatement = null; + } - SyntaxNode node; - if (partnerBody != null) - { - FindLeafNodeAndPartner(body, position, partnerBody, out node, out partnerStatement); - } - else - { - node = body.FindToken(position).Parent!; - partnerStatement = null; - } + while (true) + { + var isBody = node == body || LambdaUtilities.IsLambdaBodyStatementOrExpression(node); - while (true) + if (isBody || SyntaxComparer.Statement.HasLabel(node)) { - var isBody = node == body || LambdaUtilities.IsLambdaBodyStatementOrExpression(node); - - if (isBody || SyntaxComparer.Statement.HasLabel(node)) + switch (node.Kind()) { - switch (node.Kind()) - { - case SyntaxKind.Block: - statementPart = (int)GetStatementPart((BlockSyntax)node, position); - return node; - - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - Debug.Assert(!isBody); - statementPart = (int)GetStatementPart((CommonForEachStatementSyntax)node, position); - return node; + case SyntaxKind.Block: + statementPart = (int)GetStatementPart((BlockSyntax)node, position); + return node; - case SyntaxKind.DoStatement: - // The active statement of DoStatement node is the while condition, - // which is lexically not the closest breakpoint span (the body is). - // do { ... } [|while (condition);|] - Debug.Assert(position == ((DoStatementSyntax)node).WhileKeyword.SpanStart); - Debug.Assert(!isBody); - goto default; + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + Debug.Assert(!isBody); + statementPart = (int)GetStatementPart((CommonForEachStatementSyntax)node, position); + return node; - case SyntaxKind.PropertyDeclaration: - // The active span corresponding to a property declaration is the span corresponding to its initializer (if any), - // not the span corresponding to the accessor. - // int P { [|get;|] } = [||]; - Debug.Assert(position == ((PropertyDeclarationSyntax)node).Initializer!.SpanStart); - goto default; + case SyntaxKind.DoStatement: + // The active statement of DoStatement node is the while condition, + // which is lexically not the closest breakpoint span (the body is). + // do { ... } [|while (condition);|] + Debug.Assert(position == ((DoStatementSyntax)node).WhileKeyword.SpanStart); + Debug.Assert(!isBody); + goto default; - case SyntaxKind.VariableDeclaration: - // VariableDeclaration ::= TypeSyntax CommaSeparatedList(VariableDeclarator) - // - // The compiler places sequence points after each local variable initialization. - // The TypeSyntax is considered to be part of the first sequence span. - Debug.Assert(!isBody); + case SyntaxKind.PropertyDeclaration: + // The active span corresponding to a property declaration is the span corresponding to its initializer (if any), + // not the span corresponding to the accessor. + // int P { [|get;|] } = [||]; + Debug.Assert(position == ((PropertyDeclarationSyntax)node).Initializer!.SpanStart); + goto default; + + case SyntaxKind.VariableDeclaration: + // VariableDeclaration ::= TypeSyntax CommaSeparatedList(VariableDeclarator) + // + // The compiler places sequence points after each local variable initialization. + // The TypeSyntax is considered to be part of the first sequence span. + Debug.Assert(!isBody); + + node = ((VariableDeclarationSyntax)node).Variables.First(); + + if (partnerStatement != null) + { + partnerStatement = ((VariableDeclarationSyntax)partnerStatement).Variables.First(); + } - node = ((VariableDeclarationSyntax)node).Variables.First(); + statementPart = DefaultStatementPart; + return node; - if (partnerStatement != null) - { - partnerStatement = ((VariableDeclarationSyntax)partnerStatement).Variables.First(); - } + case SyntaxKind.SwitchExpression: + // An active statement that covers IL generated for the decision tree: + // [|switch { , ..., }|] + // This active statement is never a leaf active statement (does not correspond to a breakpoint span). - statementPart = DefaultStatementPart; + var switchExpression = (SwitchExpressionSyntax)node; + if (position == switchExpression.SwitchKeyword.SpanStart) + { + Debug.Assert(span.End == switchExpression.CloseBraceToken.Span.End); + statementPart = (int)SwitchExpressionPart.SwitchBody; return node; + } - case SyntaxKind.SwitchExpression: - // An active statement that covers IL generated for the decision tree: - // [|switch { , ..., }|] - // This active statement is never a leaf active statement (does not correspond to a breakpoint span). - - var switchExpression = (SwitchExpressionSyntax)node; - if (position == switchExpression.SwitchKeyword.SpanStart) - { - Debug.Assert(span.End == switchExpression.CloseBraceToken.Span.End); - statementPart = (int)SwitchExpressionPart.SwitchBody; - return node; - } - - // The switch expression itself can be (a part of) an active statement associated with the containing node - // For example, when it is used as a switch arm expression like so: - // switch { [|when switch { ... }|] ... } - Debug.Assert(position == switchExpression.Span.Start); - if (isBody) - { - goto default; - } - - // ascend to parent node: - break; - - case SyntaxKind.SwitchExpressionArm: - // An active statement may occur in the when clause and in the arm expression: - // [|when |] => [||] - // The former is covered by when-clause node - it's a labeled node. - // The latter isn't enclosed in a distinct labeled syntax node and thus needs to be covered - // by the arm node itself. - Debug.Assert(position == ((SwitchExpressionArmSyntax)node).Expression.SpanStart); - Debug.Assert(!isBody); + // The switch expression itself can be (a part of) an active statement associated with the containing node + // For example, when it is used as a switch arm expression like so: + // switch { [|when switch { ... }|] ... } + Debug.Assert(position == switchExpression.Span.Start); + if (isBody) + { goto default; + } - default: - statementPart = DefaultStatementPart; - return node; - } - } + // ascend to parent node: + break; - node = node.Parent!; - if (partnerStatement != null) - { - partnerStatement = partnerStatement.Parent; + case SyntaxKind.SwitchExpressionArm: + // An active statement may occur in the when clause and in the arm expression: + // [|when |] => [||] + // The former is covered by when-clause node - it's a labeled node. + // The latter isn't enclosed in a distinct labeled syntax node and thus needs to be covered + // by the arm node itself. + Debug.Assert(position == ((SwitchExpressionArmSyntax)node).Expression.SpanStart); + Debug.Assert(!isBody); + goto default; + + default: + statementPart = DefaultStatementPart; + return node; } } - } - - private static BlockPart GetStatementPart(BlockSyntax node, int position) - => position < node.OpenBraceToken.Span.End ? BlockPart.OpenBrace : BlockPart.CloseBrace; - private static TextSpan GetActiveSpan(BlockSyntax node, BlockPart part) - => part switch + node = node.Parent!; + if (partnerStatement != null) { - BlockPart.OpenBrace => node.OpenBraceToken.Span, - BlockPart.CloseBrace => node.CloseBraceToken.Span, - _ => throw ExceptionUtilities.UnexpectedValue(part), - }; - - private static ForEachPart GetStatementPart(CommonForEachStatementSyntax node, int position) - => position < node.OpenParenToken.SpanStart ? ForEachPart.ForEach : - position < node.InKeyword.SpanStart ? ForEachPart.VariableDeclaration : - position < node.Expression.SpanStart ? ForEachPart.In : - ForEachPart.Expression; - - private static TextSpan GetActiveSpan(ForEachStatementSyntax node, ForEachPart part) - => part switch - { - ForEachPart.ForEach => node.ForEachKeyword.Span, - ForEachPart.VariableDeclaration => TextSpan.FromBounds(node.Type.SpanStart, node.Identifier.Span.End), - ForEachPart.In => node.InKeyword.Span, - ForEachPart.Expression => node.Expression.Span, - _ => throw ExceptionUtilities.UnexpectedValue(part), - }; - - private static TextSpan GetActiveSpan(ForEachVariableStatementSyntax node, ForEachPart part) - => part switch - { - ForEachPart.ForEach => node.ForEachKeyword.Span, - ForEachPart.VariableDeclaration => TextSpan.FromBounds(node.Variable.SpanStart, node.Variable.Span.End), - ForEachPart.In => node.InKeyword.Span, - ForEachPart.Expression => node.Expression.Span, - _ => throw ExceptionUtilities.UnexpectedValue(part), - }; - - private static TextSpan GetActiveSpan(SwitchExpressionSyntax node, SwitchExpressionPart part) - => part switch - { - SwitchExpressionPart.WholeExpression => node.Span, - SwitchExpressionPart.SwitchBody => TextSpan.FromBounds(node.SwitchKeyword.SpanStart, node.CloseBraceToken.Span.End), - _ => throw ExceptionUtilities.UnexpectedValue(part), - }; + partnerStatement = partnerStatement.Parent; + } + } + } - private static bool AreEquivalentIgnoringLambdaBodies(SyntaxNode left, SyntaxNode right) + private static BlockPart GetStatementPart(BlockSyntax node, int position) + => position < node.OpenBraceToken.Span.End ? BlockPart.OpenBrace : BlockPart.CloseBrace; + + private static TextSpan GetActiveSpan(BlockSyntax node, BlockPart part) + => part switch { - // usual case: - if (SyntaxFactory.AreEquivalent(left, right)) - { - return true; - } + BlockPart.OpenBrace => node.OpenBraceToken.Span, + BlockPart.CloseBrace => node.CloseBraceToken.Span, + _ => throw ExceptionUtilities.UnexpectedValue(part), + }; + + private static ForEachPart GetStatementPart(CommonForEachStatementSyntax node, int position) + => position < node.OpenParenToken.SpanStart ? ForEachPart.ForEach : + position < node.InKeyword.SpanStart ? ForEachPart.VariableDeclaration : + position < node.Expression.SpanStart ? ForEachPart.In : + ForEachPart.Expression; + + private static TextSpan GetActiveSpan(ForEachStatementSyntax node, ForEachPart part) + => part switch + { + ForEachPart.ForEach => node.ForEachKeyword.Span, + ForEachPart.VariableDeclaration => TextSpan.FromBounds(node.Type.SpanStart, node.Identifier.Span.End), + ForEachPart.In => node.InKeyword.Span, + ForEachPart.Expression => node.Expression.Span, + _ => throw ExceptionUtilities.UnexpectedValue(part), + }; + + private static TextSpan GetActiveSpan(ForEachVariableStatementSyntax node, ForEachPart part) + => part switch + { + ForEachPart.ForEach => node.ForEachKeyword.Span, + ForEachPart.VariableDeclaration => TextSpan.FromBounds(node.Variable.SpanStart, node.Variable.Span.End), + ForEachPart.In => node.InKeyword.Span, + ForEachPart.Expression => node.Expression.Span, + _ => throw ExceptionUtilities.UnexpectedValue(part), + }; + + private static TextSpan GetActiveSpan(SwitchExpressionSyntax node, SwitchExpressionPart part) + => part switch + { + SwitchExpressionPart.WholeExpression => node.Span, + SwitchExpressionPart.SwitchBody => TextSpan.FromBounds(node.SwitchKeyword.SpanStart, node.CloseBraceToken.Span.End), + _ => throw ExceptionUtilities.UnexpectedValue(part), + }; - return LambdaUtilities.AreEquivalentIgnoringLambdaBodies(left, right); + private static bool AreEquivalentIgnoringLambdaBodies(SyntaxNode left, SyntaxNode right) + { + // usual case: + if (SyntaxFactory.AreEquivalent(left, right)) + { + return true; } - internal override bool IsClosureScope(SyntaxNode node) - => LambdaUtilities.IsClosureScope(node); + return LambdaUtilities.AreEquivalentIgnoringLambdaBodies(left, right); + } - internal override SyntaxNode GetCapturedParameterScope(SyntaxNode methodOrLambda) - => methodOrLambda switch - { - // lambda/local function parameter: - AnonymousFunctionExpressionSyntax lambda => lambda.Body, - // ctor parameter captured by a lambda in a ctor initializer: - ConstructorDeclarationSyntax ctor => ctor, - // block statement or arrow expression: - BaseMethodDeclarationSyntax method => method.Body ?? (SyntaxNode?)method.ExpressionBody!, - // primary constructor parameter: - ParameterListSyntax parameters => parameters.Parent!, - // top-level args: - CompilationUnitSyntax top => top, - _ => throw ExceptionUtilities.UnexpectedValue(methodOrLambda) - }; - - protected override LambdaBody? FindEnclosingLambdaBody(SyntaxNode encompassingAncestor, SyntaxNode node) - { - var current = node; - while (current != encompassingAncestor && current != null) - { - if (LambdaUtilities.IsLambdaBodyStatementOrExpression(current, out var body)) - { - return SyntaxUtilities.CreateLambdaBody(body); - } + internal override bool IsClosureScope(SyntaxNode node) + => LambdaUtilities.IsClosureScope(node); - current = current.Parent; + internal override SyntaxNode GetCapturedParameterScope(SyntaxNode methodOrLambda) + => methodOrLambda switch + { + // lambda/local function parameter: + AnonymousFunctionExpressionSyntax lambda => lambda.Body, + // ctor parameter captured by a lambda in a ctor initializer: + ConstructorDeclarationSyntax ctor => ctor, + // block statement or arrow expression: + BaseMethodDeclarationSyntax method => method.Body ?? (SyntaxNode?)method.ExpressionBody!, + // primary constructor parameter: + ParameterListSyntax parameters => parameters.Parent!, + // top-level args: + CompilationUnitSyntax top => top, + _ => throw ExceptionUtilities.UnexpectedValue(methodOrLambda) + }; + + protected override LambdaBody? FindEnclosingLambdaBody(SyntaxNode encompassingAncestor, SyntaxNode node) + { + var current = node; + while (current != encompassingAncestor && current != null) + { + if (LambdaUtilities.IsLambdaBodyStatementOrExpression(current, out var body)) + { + return SyntaxUtilities.CreateLambdaBody(body); } - return null; + current = current.Parent; } - protected override Match ComputeTopLevelMatch(SyntaxNode oldCompilationUnit, SyntaxNode newCompilationUnit) - => SyntaxComparer.TopLevel.ComputeMatch(oldCompilationUnit, newCompilationUnit); + return null; + } - protected override BidirectionalMap? ComputeParameterMap(SyntaxNode oldDeclaration, SyntaxNode newDeclaration) - => GetDeclarationParameterList(oldDeclaration) is { } oldParameterList && GetDeclarationParameterList(newDeclaration) is { } newParameterList ? - BidirectionalMap.FromMatch(SyntaxComparer.TopLevel.ComputeMatch(oldParameterList, newParameterList)) : null; + protected override Match ComputeTopLevelMatch(SyntaxNode oldCompilationUnit, SyntaxNode newCompilationUnit) + => SyntaxComparer.TopLevel.ComputeMatch(oldCompilationUnit, newCompilationUnit); - private static SyntaxNode? GetDeclarationParameterList(SyntaxNode declaration) - => declaration switch - { - ParameterListSyntax parameterList => parameterList, - AccessorDeclarationSyntax { Parent.Parent: IndexerDeclarationSyntax { ParameterList: var list } } => list, - ArrowExpressionClauseSyntax { Parent: { } memberDecl } => GetDeclarationParameterList(memberDecl), - _ => declaration.GetParameterList() - }; + protected override BidirectionalMap? ComputeParameterMap(SyntaxNode oldDeclaration, SyntaxNode newDeclaration) + => GetDeclarationParameterList(oldDeclaration) is { } oldParameterList && GetDeclarationParameterList(newDeclaration) is { } newParameterList ? + BidirectionalMap.FromMatch(SyntaxComparer.TopLevel.ComputeMatch(oldParameterList, newParameterList)) : null; - internal static Match ComputeBodyMatch(SyntaxNode oldBody, SyntaxNode newBody, IEnumerable>? knownMatches) + private static SyntaxNode? GetDeclarationParameterList(SyntaxNode declaration) + => declaration switch { - SyntaxUtilities.AssertIsBody(oldBody, allowLambda: true); - SyntaxUtilities.AssertIsBody(newBody, allowLambda: true); + ParameterListSyntax parameterList => parameterList, + AccessorDeclarationSyntax { Parent.Parent: IndexerDeclarationSyntax { ParameterList: var list } } => list, + ArrowExpressionClauseSyntax { Parent: { } memberDecl } => GetDeclarationParameterList(memberDecl), + _ => declaration.GetParameterList() + }; - if (oldBody is ExpressionSyntax || - newBody is ExpressionSyntax || - (oldBody.Parent.IsKind(SyntaxKind.LocalFunctionStatement) && newBody.Parent.IsKind(SyntaxKind.LocalFunctionStatement))) - { - Debug.Assert(oldBody is ExpressionSyntax or BlockSyntax); - Debug.Assert(newBody is ExpressionSyntax or BlockSyntax); - - // The matching algorithm requires the roots to match each other. - // Lambda bodies, field/property initializers, and method/property/indexer/operator expression-bodies may also be lambda expressions. - // Say we have oldBody 'x => x' and newBody 'F(x => x + 1)', then - // the algorithm would match 'x => x' to 'F(x => x + 1)' instead of - // matching 'x => x' to 'x => x + 1'. - - // We use the parent node as a root: - // - for field/property initializers the root is EqualsValueClause. - // - for member expression-bodies the root is ArrowExpressionClauseSyntax. - // - for block bodies the root is a method/operator/accessor declaration (only happens when matching expression body with a block body) - // - for lambdas the root is a LambdaExpression. - // - for query lambdas the root is the query clause containing the lambda (e.g. where). - // - for local functions the root is LocalFunctionStatement. - - static SyntaxNode GetMatchingRoot(SyntaxNode body) - { - var parent = body.Parent!; - // We could apply this change across all ArrowExpressionClause consistently not just for ones with LocalFunctionStatement parents - // but it would require an essential refactoring. - return parent.IsKind(SyntaxKind.ArrowExpressionClause) && parent.Parent.IsKind(SyntaxKind.LocalFunctionStatement) ? parent.Parent : parent; - } + internal static Match ComputeBodyMatch(SyntaxNode oldBody, SyntaxNode newBody, IEnumerable>? knownMatches) + { + SyntaxUtilities.AssertIsBody(oldBody, allowLambda: true); + SyntaxUtilities.AssertIsBody(newBody, allowLambda: true); - var oldRoot = GetMatchingRoot(oldBody); - var newRoot = GetMatchingRoot(newBody); - var comparer = new SyntaxComparer(oldRoot, newRoot, GetChildNodes(oldRoot, oldBody), GetChildNodes(newRoot, newBody), compareStatementSyntax: true); + if (oldBody is ExpressionSyntax || + newBody is ExpressionSyntax || + (oldBody.Parent.IsKind(SyntaxKind.LocalFunctionStatement) && newBody.Parent.IsKind(SyntaxKind.LocalFunctionStatement))) + { + Debug.Assert(oldBody is ExpressionSyntax or BlockSyntax); + Debug.Assert(newBody is ExpressionSyntax or BlockSyntax); + + // The matching algorithm requires the roots to match each other. + // Lambda bodies, field/property initializers, and method/property/indexer/operator expression-bodies may also be lambda expressions. + // Say we have oldBody 'x => x' and newBody 'F(x => x + 1)', then + // the algorithm would match 'x => x' to 'F(x => x + 1)' instead of + // matching 'x => x' to 'x => x + 1'. - return comparer.ComputeMatch(oldRoot, newRoot, knownMatches); + // We use the parent node as a root: + // - for field/property initializers the root is EqualsValueClause. + // - for member expression-bodies the root is ArrowExpressionClauseSyntax. + // - for block bodies the root is a method/operator/accessor declaration (only happens when matching expression body with a block body) + // - for lambdas the root is a LambdaExpression. + // - for query lambdas the root is the query clause containing the lambda (e.g. where). + // - for local functions the root is LocalFunctionStatement. + + static SyntaxNode GetMatchingRoot(SyntaxNode body) + { + var parent = body.Parent!; + // We could apply this change across all ArrowExpressionClause consistently not just for ones with LocalFunctionStatement parents + // but it would require an essential refactoring. + return parent.IsKind(SyntaxKind.ArrowExpressionClause) && parent.Parent.IsKind(SyntaxKind.LocalFunctionStatement) ? parent.Parent : parent; } - return SyntaxComparer.Statement.ComputeMatch(oldBody, newBody, knownMatches); + var oldRoot = GetMatchingRoot(oldBody); + var newRoot = GetMatchingRoot(newBody); + var comparer = new SyntaxComparer(oldRoot, newRoot, GetChildNodes(oldRoot, oldBody), GetChildNodes(newRoot, newBody), compareStatementSyntax: true); + + return comparer.ComputeMatch(oldRoot, newRoot, knownMatches); } - private static IEnumerable GetChildNodes(SyntaxNode root, SyntaxNode body) + return SyntaxComparer.Statement.ComputeMatch(oldBody, newBody, knownMatches); + } + + private static IEnumerable GetChildNodes(SyntaxNode root, SyntaxNode body) + { + if (root is LocalFunctionStatementSyntax localFunc) { - if (root is LocalFunctionStatementSyntax localFunc) + // local functions have multiple children we need to process for matches, but we won't automatically + // descend into them, assuming they're nested, so we override the default behaviour and return + // multiple children + foreach (var attributeList in localFunc.AttributeLists) { - // local functions have multiple children we need to process for matches, but we won't automatically - // descend into them, assuming they're nested, so we override the default behaviour and return - // multiple children - foreach (var attributeList in localFunc.AttributeLists) - { - yield return attributeList; - } + yield return attributeList; + } - yield return localFunc.ReturnType; + yield return localFunc.ReturnType; - if (localFunc.TypeParameterList is not null) - { - yield return localFunc.TypeParameterList; - } + if (localFunc.TypeParameterList is not null) + { + yield return localFunc.TypeParameterList; + } - yield return localFunc.ParameterList; + yield return localFunc.ParameterList; - if (localFunc.Body is not null) - { - yield return localFunc.Body; - } - else if (localFunc.ExpressionBody is not null) - { - // Skip the ArrowExpressionClause that is ExressionBody and just return the expression itself - yield return localFunc.ExpressionBody.Expression; - } + if (localFunc.Body is not null) + { + yield return localFunc.Body; } - else + else if (localFunc.ExpressionBody is not null) { - yield return body; + // Skip the ArrowExpressionClause that is ExressionBody and just return the expression itself + yield return localFunc.ExpressionBody.Expression; } } - - internal static bool TryMatchActiveStatement( - SyntaxNode oldBody, - SyntaxNode newBody, - SyntaxNode oldStatement, - [NotNullWhen(true)] out SyntaxNode? newStatement) + else { - // TODO: Consider mapping an expression body to an equivalent statement expression or return statement and vice versa. - // It would benefit transformations of expression bodies to block bodies of lambdas, methods, operators and properties. - // See https://github.com/dotnet/roslyn/issues/22696 + yield return body; + } + } - // field initializer, lambda and query expressions: - if (oldStatement == oldBody && !newBody.IsKind(SyntaxKind.Block)) - { - newStatement = newBody; - return true; - } + internal static bool TryMatchActiveStatement( + SyntaxNode oldBody, + SyntaxNode newBody, + SyntaxNode oldStatement, + [NotNullWhen(true)] out SyntaxNode? newStatement) + { + // TODO: Consider mapping an expression body to an equivalent statement expression or return statement and vice versa. + // It would benefit transformations of expression bodies to block bodies of lambdas, methods, operators and properties. + // See https://github.com/dotnet/roslyn/issues/22696 - newStatement = null; - return false; + // field initializer, lambda and query expressions: + if (oldStatement == oldBody && !newBody.IsKind(SyntaxKind.Block)) + { + newStatement = newBody; + return true; } - #endregion + newStatement = null; + return false; + } + + #endregion - #region Syntax and Semantic Utils + #region Syntax and Semantic Utils - protected override bool IsNamespaceDeclaration(SyntaxNode node) - => node is BaseNamespaceDeclarationSyntax; + protected override bool IsNamespaceDeclaration(SyntaxNode node) + => node is BaseNamespaceDeclarationSyntax; - private static bool IsTypeDeclaration(SyntaxNode node) - => node is BaseTypeDeclarationSyntax or DelegateDeclarationSyntax; + private static bool IsTypeDeclaration(SyntaxNode node) + => node is BaseTypeDeclarationSyntax or DelegateDeclarationSyntax; - protected override bool IsCompilationUnitWithGlobalStatements(SyntaxNode node) - => node is CompilationUnitSyntax unit && unit.ContainsGlobalStatements(); + protected override bool IsCompilationUnitWithGlobalStatements(SyntaxNode node) + => node is CompilationUnitSyntax unit && unit.ContainsGlobalStatements(); - protected override bool IsGlobalStatement(SyntaxNode node) - => node.IsKind(SyntaxKind.GlobalStatement); + protected override bool IsGlobalStatement(SyntaxNode node) + => node.IsKind(SyntaxKind.GlobalStatement); - protected override IEnumerable GetTopLevelTypeDeclarations(SyntaxNode compilationUnit) - { - using var _ = ArrayBuilder>.GetInstance(out var stack); + protected override IEnumerable GetTopLevelTypeDeclarations(SyntaxNode compilationUnit) + { + using var _ = ArrayBuilder>.GetInstance(out var stack); - stack.Add(((CompilationUnitSyntax)compilationUnit).Members); + stack.Add(((CompilationUnitSyntax)compilationUnit).Members); - while (stack.Count > 0) - { - var members = stack.Last(); - stack.RemoveLast(); + while (stack.Count > 0) + { + var members = stack.Last(); + stack.RemoveLast(); - foreach (var member in members) + foreach (var member in members) + { + if (IsTypeDeclaration(member)) { - if (IsTypeDeclaration(member)) - { - yield return member; - } + yield return member; + } - if (member is BaseNamespaceDeclarationSyntax namespaceMember) - { - stack.Add(namespaceMember.Members); - } + if (member is BaseNamespaceDeclarationSyntax namespaceMember) + { + stack.Add(namespaceMember.Members); } } } + } - protected override string LineDirectiveKeyword - => "line"; + protected override string LineDirectiveKeyword + => "line"; - protected override ushort LineDirectiveSyntaxKind - => (ushort)SyntaxKind.LineDirectiveTrivia; + protected override ushort LineDirectiveSyntaxKind + => (ushort)SyntaxKind.LineDirectiveTrivia; - protected override IEnumerable GetSyntaxSequenceEdits(ImmutableArray oldNodes, ImmutableArray newNodes) - => SyntaxComparer.GetSequenceEdits(oldNodes, newNodes); + protected override IEnumerable GetSyntaxSequenceEdits(ImmutableArray oldNodes, ImmutableArray newNodes) + => SyntaxComparer.GetSequenceEdits(oldNodes, newNodes); - internal override SyntaxNode EmptyCompilationUnit - => SyntaxFactory.CompilationUnit(); + internal override SyntaxNode EmptyCompilationUnit + => SyntaxFactory.CompilationUnit(); - // there are no experimental features at this time. - internal override bool ExperimentalFeaturesEnabled(SyntaxTree tree) - => false; + // there are no experimental features at this time. + internal override bool ExperimentalFeaturesEnabled(SyntaxTree tree) + => false; - protected override bool StatementLabelEquals(SyntaxNode node1, SyntaxNode node2) - => SyntaxComparer.Statement.GetLabel(node1) == SyntaxComparer.Statement.GetLabel(node2); + protected override bool StatementLabelEquals(SyntaxNode node1, SyntaxNode node2) + => SyntaxComparer.Statement.GetLabel(node1) == SyntaxComparer.Statement.GetLabel(node2); - protected override bool TryGetEnclosingBreakpointSpan(SyntaxToken token, out TextSpan span) - => BreakpointSpans.TryGetClosestBreakpointSpan(token.Parent!, token.SpanStart, minLength: token.Span.Length, out span); + protected override bool TryGetEnclosingBreakpointSpan(SyntaxToken token, out TextSpan span) + => BreakpointSpans.TryGetClosestBreakpointSpan(token.Parent!, token.SpanStart, minLength: token.Span.Length, out span); - protected override bool TryGetActiveSpan(SyntaxNode node, int statementPart, int minLength, out TextSpan span) + protected override bool TryGetActiveSpan(SyntaxNode node, int statementPart, int minLength, out TextSpan span) + { + switch (node.Kind()) { - switch (node.Kind()) - { - case SyntaxKind.ArrowExpressionClause: - // Member block body is matched with expression body, so we might be called with statement part of open or closed brace. - Debug.Assert(statementPart is DefaultStatementPart or (int)BlockPart.OpenBrace or (int)BlockPart.CloseBrace); - span = ((ArrowExpressionClauseSyntax)node).Expression.Span; - return true; + case SyntaxKind.ArrowExpressionClause: + // Member block body is matched with expression body, so we might be called with statement part of open or closed brace. + Debug.Assert(statementPart is DefaultStatementPart or (int)BlockPart.OpenBrace or (int)BlockPart.CloseBrace); + span = ((ArrowExpressionClauseSyntax)node).Expression.Span; + return true; - case SyntaxKind.Block: - span = GetActiveSpan((BlockSyntax)node, (BlockPart)statementPart); - return true; + case SyntaxKind.Block: + span = GetActiveSpan((BlockSyntax)node, (BlockPart)statementPart); + return true; - case SyntaxKind.ForEachStatement: - span = GetActiveSpan((ForEachStatementSyntax)node, (ForEachPart)statementPart); - return true; + case SyntaxKind.ForEachStatement: + span = GetActiveSpan((ForEachStatementSyntax)node, (ForEachPart)statementPart); + return true; - case SyntaxKind.ForEachVariableStatement: - span = GetActiveSpan((ForEachVariableStatementSyntax)node, (ForEachPart)statementPart); - return true; + case SyntaxKind.ForEachVariableStatement: + span = GetActiveSpan((ForEachVariableStatementSyntax)node, (ForEachPart)statementPart); + return true; - case SyntaxKind.DoStatement: - // The active statement of DoStatement node is the while condition, - // which is lexically not the closest breakpoint span (the body is). - // do { ... } [|while (condition);|] - Debug.Assert(statementPart == DefaultStatementPart); + case SyntaxKind.DoStatement: + // The active statement of DoStatement node is the while condition, + // which is lexically not the closest breakpoint span (the body is). + // do { ... } [|while (condition);|] + Debug.Assert(statementPart == DefaultStatementPart); - var doStatement = (DoStatementSyntax)node; - return BreakpointSpans.TryGetClosestBreakpointSpan(node, doStatement.WhileKeyword.SpanStart, minLength, out span); + var doStatement = (DoStatementSyntax)node; + return BreakpointSpans.TryGetClosestBreakpointSpan(node, doStatement.WhileKeyword.SpanStart, minLength, out span); - case SyntaxKind.PropertyDeclaration: - // The active span corresponding to a property declaration is the span corresponding to its initializer (if any), - // not the span corresponding to the accessor. - // int P { [|get;|] } = [||]; - Debug.Assert(statementPart == DefaultStatementPart); + case SyntaxKind.PropertyDeclaration: + // The active span corresponding to a property declaration is the span corresponding to its initializer (if any), + // not the span corresponding to the accessor. + // int P { [|get;|] } = [||]; + Debug.Assert(statementPart == DefaultStatementPart); - var propertyDeclaration = (PropertyDeclarationSyntax)node; + var propertyDeclaration = (PropertyDeclarationSyntax)node; - if (propertyDeclaration.Initializer != null && - BreakpointSpans.TryGetClosestBreakpointSpan(node, propertyDeclaration.Initializer.SpanStart, minLength, out span)) - { - return true; - } + if (propertyDeclaration.Initializer != null && + BreakpointSpans.TryGetClosestBreakpointSpan(node, propertyDeclaration.Initializer.SpanStart, minLength, out span)) + { + return true; + } - span = default; - return false; + span = default; + return false; - case SyntaxKind.SwitchExpression: - span = GetActiveSpan((SwitchExpressionSyntax)node, (SwitchExpressionPart)statementPart); - return true; + case SyntaxKind.SwitchExpression: + span = GetActiveSpan((SwitchExpressionSyntax)node, (SwitchExpressionPart)statementPart); + return true; - case SyntaxKind.SwitchExpressionArm: - // An active statement may occur in the when clause and in the arm expression: - // [|when |] => [||] - // The former is covered by when-clause node - it's a labeled node. - // The latter isn't enclosed in a distinct labeled syntax node and thus needs to be covered - // by the arm node itself. - Debug.Assert(statementPart == DefaultStatementPart); + case SyntaxKind.SwitchExpressionArm: + // An active statement may occur in the when clause and in the arm expression: + // [|when |] => [||] + // The former is covered by when-clause node - it's a labeled node. + // The latter isn't enclosed in a distinct labeled syntax node and thus needs to be covered + // by the arm node itself. + Debug.Assert(statementPart == DefaultStatementPart); - span = ((SwitchExpressionArmSyntax)node).Expression.Span; - return true; + span = ((SwitchExpressionArmSyntax)node).Expression.Span; + return true; - case SyntaxKind.ParameterList when node.Parent is TypeDeclarationSyntax typeDeclaration: - // The only case when an active statement is a parameter list is an active statement - // for an implicit constructor initializer of a type with primary constructor. - // In that case the span of the active statement starts before the parameter list - // (it includes the type name and type parameters). - span = BreakpointSpans.CreateSpanForImplicitPrimaryConstructorInitializer(typeDeclaration); - return true; + case SyntaxKind.ParameterList when node.Parent is TypeDeclarationSyntax typeDeclaration: + // The only case when an active statement is a parameter list is an active statement + // for an implicit constructor initializer of a type with primary constructor. + // In that case the span of the active statement starts before the parameter list + // (it includes the type name and type parameters). + span = BreakpointSpans.CreateSpanForImplicitPrimaryConstructorInitializer(typeDeclaration); + return true; - case SyntaxKind.RecordDeclaration: - case SyntaxKind.RecordStructDeclaration: - span = BreakpointSpans.CreateSpanForCopyConstructor((RecordDeclarationSyntax)node); - return true; + case SyntaxKind.RecordDeclaration: + case SyntaxKind.RecordStructDeclaration: + span = BreakpointSpans.CreateSpanForCopyConstructor((RecordDeclarationSyntax)node); + return true; - default: - // make sure all nodes that use statement parts are handled above: - Debug.Assert(statementPart == DefaultStatementPart); + default: + // make sure all nodes that use statement parts are handled above: + Debug.Assert(statementPart == DefaultStatementPart); - return BreakpointSpans.TryGetClosestBreakpointSpan(node, node.SpanStart, minLength, out span); - } + return BreakpointSpans.TryGetClosestBreakpointSpan(node, node.SpanStart, minLength, out span); } + } - public static (SyntaxNode node, int part) GetFirstBodyActiveStatement(SyntaxNode memberBody) - => (memberBody, memberBody.IsKind(SyntaxKind.Block) ? (int)BlockPart.OpenBrace : DefaultStatementPart); + public static (SyntaxNode node, int part) GetFirstBodyActiveStatement(SyntaxNode memberBody) + => (memberBody, memberBody.IsKind(SyntaxKind.Block) ? (int)BlockPart.OpenBrace : DefaultStatementPart); - protected override IEnumerable<(SyntaxNode statement, int statementPart)> EnumerateNearStatements(SyntaxNode statement) + protected override IEnumerable<(SyntaxNode statement, int statementPart)> EnumerateNearStatements(SyntaxNode statement) + { + var direction = +1; + SyntaxNodeOrToken nodeOrToken = statement; + var fieldOrPropertyModifiers = SyntaxUtilities.TryGetFieldOrPropertyModifiers(statement); + + while (true) { - var direction = +1; - SyntaxNodeOrToken nodeOrToken = statement; - var fieldOrPropertyModifiers = SyntaxUtilities.TryGetFieldOrPropertyModifiers(statement); + nodeOrToken = (direction < 0) ? nodeOrToken.GetPreviousSibling() : nodeOrToken.GetNextSibling(); - while (true) + if (nodeOrToken.RawKind == 0) { - nodeOrToken = (direction < 0) ? nodeOrToken.GetPreviousSibling() : nodeOrToken.GetNextSibling(); + var parent = statement.Parent; + if (parent == null) + { + yield break; + } - if (nodeOrToken.RawKind == 0) + switch (parent.Kind()) { - var parent = statement.Parent; - if (parent == null) - { - yield break; - } + case SyntaxKind.Block: + // The next sequence point hit after the last statement of a block is the closing brace: + yield return (parent, (int)(direction > 0 ? BlockPart.CloseBrace : BlockPart.OpenBrace)); + break; - switch (parent.Kind()) - { - case SyntaxKind.Block: - // The next sequence point hit after the last statement of a block is the closing brace: - yield return (parent, (int)(direction > 0 ? BlockPart.CloseBrace : BlockPart.OpenBrace)); - break; - - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - // The next sequence point hit after the body is the in keyword: - // [|foreach|] ([|variable-declaration|] [|in|] [|expression|]) [||] - yield return (parent, (int)ForEachPart.In); - break; - } + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + // The next sequence point hit after the body is the in keyword: + // [|foreach|] ([|variable-declaration|] [|in|] [|expression|]) [||] + yield return (parent, (int)ForEachPart.In); + break; + } - if (direction > 0) - { - nodeOrToken = statement; - direction = -1; - continue; - } + if (direction > 0) + { + nodeOrToken = statement; + direction = -1; + continue; + } - if (fieldOrPropertyModifiers.HasValue) - { - // We enumerated all members and none of them has an initializer. - // We don't have any better place where to place the span than the initial field. - // Consider: in non-partial classes we could find a single constructor. - // Otherwise, it would be confusing to select one arbitrarily. - yield return (statement, -1); - } + if (fieldOrPropertyModifiers.HasValue) + { + // We enumerated all members and none of them has an initializer. + // We don't have any better place where to place the span than the initial field. + // Consider: in non-partial classes we could find a single constructor. + // Otherwise, it would be confusing to select one arbitrarily. + yield return (statement, -1); + } - nodeOrToken = statement = parent; - fieldOrPropertyModifiers = SyntaxUtilities.TryGetFieldOrPropertyModifiers(statement); - direction = +1; + nodeOrToken = statement = parent; + fieldOrPropertyModifiers = SyntaxUtilities.TryGetFieldOrPropertyModifiers(statement); + direction = +1; - yield return (nodeOrToken.AsNode()!, DefaultStatementPart); + yield return (nodeOrToken.AsNode()!, DefaultStatementPart); + } + else + { + var node = nodeOrToken.AsNode(); + if (node == null) + { + continue; } - else + + if (fieldOrPropertyModifiers.HasValue) { - var node = nodeOrToken.AsNode(); - if (node == null) - { - continue; - } + var nodeModifiers = SyntaxUtilities.TryGetFieldOrPropertyModifiers(node); - if (fieldOrPropertyModifiers.HasValue) + if (!nodeModifiers.HasValue || + nodeModifiers.Value.Any(SyntaxKind.StaticKeyword) != fieldOrPropertyModifiers.Value.Any(SyntaxKind.StaticKeyword)) { - var nodeModifiers = SyntaxUtilities.TryGetFieldOrPropertyModifiers(node); - - if (!nodeModifiers.HasValue || - nodeModifiers.Value.Any(SyntaxKind.StaticKeyword) != fieldOrPropertyModifiers.Value.Any(SyntaxKind.StaticKeyword)) - { - continue; - } + continue; } + } - switch (node.Kind()) - { - case SyntaxKind.Block: - yield return (node, (int)(direction > 0 ? BlockPart.OpenBrace : BlockPart.CloseBrace)); - break; - - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - yield return (node, (int)ForEachPart.ForEach); - break; - } + switch (node.Kind()) + { + case SyntaxKind.Block: + yield return (node, (int)(direction > 0 ? BlockPart.OpenBrace : BlockPart.CloseBrace)); + break; - yield return (node, DefaultStatementPart); + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + yield return (node, (int)ForEachPart.ForEach); + break; } + + yield return (node, DefaultStatementPart); } } + } - protected override bool AreEquivalentActiveStatements(SyntaxNode oldStatement, SyntaxNode newStatement, int statementPart) + protected override bool AreEquivalentActiveStatements(SyntaxNode oldStatement, SyntaxNode newStatement, int statementPart) + { + if (oldStatement.Kind() != newStatement.Kind()) { - if (oldStatement.Kind() != newStatement.Kind()) - { - return false; - } + return false; + } - switch (oldStatement.Kind()) - { - case SyntaxKind.Block: - // closing brace of a using statement or a block that contains using local declarations: - if (statementPart == (int)BlockPart.CloseBrace) + switch (oldStatement.Kind()) + { + case SyntaxKind.Block: + // closing brace of a using statement or a block that contains using local declarations: + if (statementPart == (int)BlockPart.CloseBrace) + { + if (oldStatement.Parent is UsingStatementSyntax oldUsing) { - if (oldStatement.Parent is UsingStatementSyntax oldUsing) - { - return newStatement.Parent is UsingStatementSyntax newUsing && - AreEquivalentActiveStatements(oldUsing, newUsing); - } - - return HasEquivalentUsingDeclarations((BlockSyntax)oldStatement, (BlockSyntax)newStatement); + return newStatement.Parent is UsingStatementSyntax newUsing && + AreEquivalentActiveStatements(oldUsing, newUsing); } - return true; + return HasEquivalentUsingDeclarations((BlockSyntax)oldStatement, (BlockSyntax)newStatement); + } - case SyntaxKind.ConstructorDeclaration: - // The call could only change if the base type of the containing class changed. - return true; + return true; - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - // only check the expression, edits in the body and the variable declaration are allowed: - return AreEquivalentActiveStatements((CommonForEachStatementSyntax)oldStatement, (CommonForEachStatementSyntax)newStatement); + case SyntaxKind.ConstructorDeclaration: + // The call could only change if the base type of the containing class changed. + return true; - case SyntaxKind.IfStatement: - // only check the condition, edits in the body are allowed: - return AreEquivalentActiveStatements((IfStatementSyntax)oldStatement, (IfStatementSyntax)newStatement); + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + // only check the expression, edits in the body and the variable declaration are allowed: + return AreEquivalentActiveStatements((CommonForEachStatementSyntax)oldStatement, (CommonForEachStatementSyntax)newStatement); - case SyntaxKind.WhileStatement: - // only check the condition, edits in the body are allowed: - return AreEquivalentActiveStatements((WhileStatementSyntax)oldStatement, (WhileStatementSyntax)newStatement); + case SyntaxKind.IfStatement: + // only check the condition, edits in the body are allowed: + return AreEquivalentActiveStatements((IfStatementSyntax)oldStatement, (IfStatementSyntax)newStatement); - case SyntaxKind.DoStatement: - // only check the condition, edits in the body are allowed: - return AreEquivalentActiveStatements((DoStatementSyntax)oldStatement, (DoStatementSyntax)newStatement); + case SyntaxKind.WhileStatement: + // only check the condition, edits in the body are allowed: + return AreEquivalentActiveStatements((WhileStatementSyntax)oldStatement, (WhileStatementSyntax)newStatement); - case SyntaxKind.SwitchStatement: - return AreEquivalentActiveStatements((SwitchStatementSyntax)oldStatement, (SwitchStatementSyntax)newStatement); + case SyntaxKind.DoStatement: + // only check the condition, edits in the body are allowed: + return AreEquivalentActiveStatements((DoStatementSyntax)oldStatement, (DoStatementSyntax)newStatement); - case SyntaxKind.LockStatement: - return AreEquivalentActiveStatements((LockStatementSyntax)oldStatement, (LockStatementSyntax)newStatement); + case SyntaxKind.SwitchStatement: + return AreEquivalentActiveStatements((SwitchStatementSyntax)oldStatement, (SwitchStatementSyntax)newStatement); - case SyntaxKind.UsingStatement: - return AreEquivalentActiveStatements((UsingStatementSyntax)oldStatement, (UsingStatementSyntax)newStatement); + case SyntaxKind.LockStatement: + return AreEquivalentActiveStatements((LockStatementSyntax)oldStatement, (LockStatementSyntax)newStatement); - // fixed and for statements don't need special handling since the active statement is a variable declaration - default: - return AreEquivalentIgnoringLambdaBodies(oldStatement, newStatement); - } + case SyntaxKind.UsingStatement: + return AreEquivalentActiveStatements((UsingStatementSyntax)oldStatement, (UsingStatementSyntax)newStatement); + + // fixed and for statements don't need special handling since the active statement is a variable declaration + default: + return AreEquivalentIgnoringLambdaBodies(oldStatement, newStatement); } + } - private static bool HasEquivalentUsingDeclarations(BlockSyntax oldBlock, BlockSyntax newBlock) - { - var oldUsingDeclarations = oldBlock.Statements.Where(s => s is LocalDeclarationStatementSyntax l && l.UsingKeyword != default); - var newUsingDeclarations = newBlock.Statements.Where(s => s is LocalDeclarationStatementSyntax l && l.UsingKeyword != default); + private static bool HasEquivalentUsingDeclarations(BlockSyntax oldBlock, BlockSyntax newBlock) + { + var oldUsingDeclarations = oldBlock.Statements.Where(s => s is LocalDeclarationStatementSyntax l && l.UsingKeyword != default); + var newUsingDeclarations = newBlock.Statements.Where(s => s is LocalDeclarationStatementSyntax l && l.UsingKeyword != default); - return oldUsingDeclarations.SequenceEqual(newUsingDeclarations, AreEquivalentIgnoringLambdaBodies); - } + return oldUsingDeclarations.SequenceEqual(newUsingDeclarations, AreEquivalentIgnoringLambdaBodies); + } - private static bool AreEquivalentActiveStatements(IfStatementSyntax oldNode, IfStatementSyntax newNode) - { - // only check the condition, edits in the body are allowed: - return AreEquivalentIgnoringLambdaBodies(oldNode.Condition, newNode.Condition); - } + private static bool AreEquivalentActiveStatements(IfStatementSyntax oldNode, IfStatementSyntax newNode) + { + // only check the condition, edits in the body are allowed: + return AreEquivalentIgnoringLambdaBodies(oldNode.Condition, newNode.Condition); + } - private static bool AreEquivalentActiveStatements(WhileStatementSyntax oldNode, WhileStatementSyntax newNode) - { - // only check the condition, edits in the body are allowed: - return AreEquivalentIgnoringLambdaBodies(oldNode.Condition, newNode.Condition); - } + private static bool AreEquivalentActiveStatements(WhileStatementSyntax oldNode, WhileStatementSyntax newNode) + { + // only check the condition, edits in the body are allowed: + return AreEquivalentIgnoringLambdaBodies(oldNode.Condition, newNode.Condition); + } + + private static bool AreEquivalentActiveStatements(DoStatementSyntax oldNode, DoStatementSyntax newNode) + { + // only check the condition, edits in the body are allowed: + return AreEquivalentIgnoringLambdaBodies(oldNode.Condition, newNode.Condition); + } - private static bool AreEquivalentActiveStatements(DoStatementSyntax oldNode, DoStatementSyntax newNode) + private static bool AreEquivalentActiveStatements(SwitchStatementSyntax oldNode, SwitchStatementSyntax newNode) + { + // only check the expression, edits in the body are allowed, unless the switch expression contains patterns: + if (!AreEquivalentIgnoringLambdaBodies(oldNode.Expression, newNode.Expression)) { - // only check the condition, edits in the body are allowed: - return AreEquivalentIgnoringLambdaBodies(oldNode.Condition, newNode.Condition); + return false; } - private static bool AreEquivalentActiveStatements(SwitchStatementSyntax oldNode, SwitchStatementSyntax newNode) - { - // only check the expression, edits in the body are allowed, unless the switch expression contains patterns: - if (!AreEquivalentIgnoringLambdaBodies(oldNode.Expression, newNode.Expression)) - { - return false; - } + // Check that switch statement decision tree has not changed. + var hasDecitionTree = oldNode.Sections.Any(s => s.Labels.Any(l => l is CasePatternSwitchLabelSyntax)); + return !hasDecitionTree || AreEquivalentSwitchStatementDecisionTrees(oldNode, newNode); + } - // Check that switch statement decision tree has not changed. - var hasDecitionTree = oldNode.Sections.Any(s => s.Labels.Any(l => l is CasePatternSwitchLabelSyntax)); - return !hasDecitionTree || AreEquivalentSwitchStatementDecisionTrees(oldNode, newNode); - } + private static bool AreEquivalentActiveStatements(LockStatementSyntax oldNode, LockStatementSyntax newNode) + { + // only check the expression, edits in the body are allowed: + return AreEquivalentIgnoringLambdaBodies(oldNode.Expression, newNode.Expression); + } - private static bool AreEquivalentActiveStatements(LockStatementSyntax oldNode, LockStatementSyntax newNode) - { - // only check the expression, edits in the body are allowed: - return AreEquivalentIgnoringLambdaBodies(oldNode.Expression, newNode.Expression); - } + private static bool AreEquivalentActiveStatements(FixedStatementSyntax oldNode, FixedStatementSyntax newNode) + => AreEquivalentIgnoringLambdaBodies(oldNode.Declaration, newNode.Declaration); - private static bool AreEquivalentActiveStatements(FixedStatementSyntax oldNode, FixedStatementSyntax newNode) - => AreEquivalentIgnoringLambdaBodies(oldNode.Declaration, newNode.Declaration); + private static bool AreEquivalentActiveStatements(UsingStatementSyntax oldNode, UsingStatementSyntax newNode) + { + // only check the expression/declaration, edits in the body are allowed: + return AreEquivalentIgnoringLambdaBodies( + (SyntaxNode?)oldNode.Declaration ?? oldNode.Expression!, + (SyntaxNode?)newNode.Declaration ?? newNode.Expression!); + } - private static bool AreEquivalentActiveStatements(UsingStatementSyntax oldNode, UsingStatementSyntax newNode) + private static bool AreEquivalentActiveStatements(CommonForEachStatementSyntax oldNode, CommonForEachStatementSyntax newNode) + { + if (oldNode.Kind() != newNode.Kind() || !AreEquivalentIgnoringLambdaBodies(oldNode.Expression, newNode.Expression)) { - // only check the expression/declaration, edits in the body are allowed: - return AreEquivalentIgnoringLambdaBodies( - (SyntaxNode?)oldNode.Declaration ?? oldNode.Expression!, - (SyntaxNode?)newNode.Declaration ?? newNode.Expression!); + return false; } - private static bool AreEquivalentActiveStatements(CommonForEachStatementSyntax oldNode, CommonForEachStatementSyntax newNode) + switch (oldNode.Kind()) { - if (oldNode.Kind() != newNode.Kind() || !AreEquivalentIgnoringLambdaBodies(oldNode.Expression, newNode.Expression)) - { - return false; - } - - switch (oldNode.Kind()) - { - case SyntaxKind.ForEachStatement: return AreEquivalentIgnoringLambdaBodies(((ForEachStatementSyntax)oldNode).Type, ((ForEachStatementSyntax)newNode).Type); - case SyntaxKind.ForEachVariableStatement: return AreEquivalentIgnoringLambdaBodies(((ForEachVariableStatementSyntax)oldNode).Variable, ((ForEachVariableStatementSyntax)newNode).Variable); - default: throw ExceptionUtilities.UnexpectedValue(oldNode.Kind()); - } + case SyntaxKind.ForEachStatement: return AreEquivalentIgnoringLambdaBodies(((ForEachStatementSyntax)oldNode).Type, ((ForEachStatementSyntax)newNode).Type); + case SyntaxKind.ForEachVariableStatement: return AreEquivalentIgnoringLambdaBodies(((ForEachVariableStatementSyntax)oldNode).Variable, ((ForEachVariableStatementSyntax)newNode).Variable); + default: throw ExceptionUtilities.UnexpectedValue(oldNode.Kind()); } + } - private static bool AreSimilarActiveStatements(CommonForEachStatementSyntax oldNode, CommonForEachStatementSyntax newNode) - { - List? oldTokens = null; - List? newTokens = null; + private static bool AreSimilarActiveStatements(CommonForEachStatementSyntax oldNode, CommonForEachStatementSyntax newNode) + { + List? oldTokens = null; + List? newTokens = null; - SyntaxComparer.GetLocalNames(oldNode, ref oldTokens); - SyntaxComparer.GetLocalNames(newNode, ref newTokens); + SyntaxComparer.GetLocalNames(oldNode, ref oldTokens); + SyntaxComparer.GetLocalNames(newNode, ref newTokens); - // A valid foreach statement declares at least one variable. - RoslynDebug.Assert(oldTokens != null); - RoslynDebug.Assert(newTokens != null); + // A valid foreach statement declares at least one variable. + RoslynDebug.Assert(oldTokens != null); + RoslynDebug.Assert(newTokens != null); - return DeclareSameIdentifiers(oldTokens.ToArray(), newTokens.ToArray()); - } + return DeclareSameIdentifiers(oldTokens.ToArray(), newTokens.ToArray()); + } - protected override bool AreEquivalentImpl(SyntaxToken oldToken, SyntaxToken newToken) - => SyntaxFactory.AreEquivalent(oldToken, newToken); + protected override bool AreEquivalentImpl(SyntaxToken oldToken, SyntaxToken newToken) + => SyntaxFactory.AreEquivalent(oldToken, newToken); - internal override bool IsInterfaceDeclaration(SyntaxNode node) - => node.IsKind(SyntaxKind.InterfaceDeclaration); + internal override bool IsInterfaceDeclaration(SyntaxNode node) + => node.IsKind(SyntaxKind.InterfaceDeclaration); - internal override bool IsRecordDeclaration(SyntaxNode node) - => node.Kind() is SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration; + internal override bool IsRecordDeclaration(SyntaxNode node) + => node.Kind() is SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration; - internal override SyntaxNode? TryGetContainingTypeDeclaration(SyntaxNode node) - => node is CompilationUnitSyntax ? null : node.Parent!.FirstAncestorOrSelf(); + internal override SyntaxNode? TryGetContainingTypeDeclaration(SyntaxNode node) + => node is CompilationUnitSyntax ? null : node.Parent!.FirstAncestorOrSelf(); - internal override bool IsDeclarationWithInitializer(SyntaxNode declaration) - => declaration is VariableDeclaratorSyntax { Initializer: not null } or PropertyDeclarationSyntax { Initializer: not null }; + internal override bool IsDeclarationWithInitializer(SyntaxNode declaration) + => declaration is VariableDeclaratorSyntax { Initializer: not null } or PropertyDeclarationSyntax { Initializer: not null }; - internal override bool IsPrimaryConstructorDeclaration(SyntaxNode declaration) - => declaration.Parent is TypeDeclarationSyntax { ParameterList: var parameterList } && parameterList == declaration; + internal override bool IsPrimaryConstructorDeclaration(SyntaxNode declaration) + => declaration.Parent is TypeDeclarationSyntax { ParameterList: var parameterList } && parameterList == declaration; - internal override bool IsConstructorWithMemberInitializers(ISymbol symbol, CancellationToken cancellationToken) + internal override bool IsConstructorWithMemberInitializers(ISymbol symbol, CancellationToken cancellationToken) + { + if (symbol is not IMethodSymbol { MethodKind: MethodKind.Constructor or MethodKind.StaticConstructor } method) { - if (symbol is not IMethodSymbol { MethodKind: MethodKind.Constructor or MethodKind.StaticConstructor } method) - { - return false; - } - - // static constructor has initializers: - if (method.IsStatic) - { - return true; - } + return false; + } - // If primary constructor is present then no other constructor has member initializers: - if (GetPrimaryConstructor(method.ContainingType, cancellationToken) is { } primaryConstructor) - { - return symbol == primaryConstructor; - } + // static constructor has initializers: + if (method.IsStatic) + { + return true; + } - // Copy-constructor of a record does not have member initializers: - if (method.ContainingType.IsRecord && method.IsCopyConstructor()) - { - return false; - } + // If primary constructor is present then no other constructor has member initializers: + if (GetPrimaryConstructor(method.ContainingType, cancellationToken) is { } primaryConstructor) + { + return symbol == primaryConstructor; + } - // Default constructor has initializers unless the type is a struct. - // Struct with member initializers is required to have an explicit constructor. - if (method.IsImplicitlyDeclared) - { - return method.ContainingType.TypeKind != TypeKind.Struct; - } + // Copy-constructor of a record does not have member initializers: + if (method.ContainingType.IsRecord && method.IsCopyConstructor()) + { + return false; + } - var ctorInitializer = ((ConstructorDeclarationSyntax)symbol.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken)).Initializer; - if (method.ContainingType.TypeKind == TypeKind.Struct) - { - // constructor of a struct with implicit or this() initializer has member initializers: - return ctorInitializer is null or { ThisOrBaseKeyword: (kind: SyntaxKind.ThisKeyword), ArgumentList.Arguments: [] }; - } - else - { - // constructor of a class with implicit or base initializer has member initializers: - return ctorInitializer is null or (kind: SyntaxKind.BaseConstructorInitializer); - } + // Default constructor has initializers unless the type is a struct. + // Struct with member initializers is required to have an explicit constructor. + if (method.IsImplicitlyDeclared) + { + return method.ContainingType.TypeKind != TypeKind.Struct; } - internal override bool IsPartial(INamedTypeSymbol type) + var ctorInitializer = ((ConstructorDeclarationSyntax)symbol.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken)).Initializer; + if (method.ContainingType.TypeKind == TypeKind.Struct) + { + // constructor of a struct with implicit or this() initializer has member initializers: + return ctorInitializer is null or { ThisOrBaseKeyword: (kind: SyntaxKind.ThisKeyword), ArgumentList.Arguments: [] }; + } + else { - var syntaxRefs = type.DeclaringSyntaxReferences; - return syntaxRefs.Length > 1 - || ((BaseTypeDeclarationSyntax)syntaxRefs.Single().GetSyntax()).Modifiers.Any(SyntaxKind.PartialKeyword); + // constructor of a class with implicit or base initializer has member initializers: + return ctorInitializer is null or (kind: SyntaxKind.BaseConstructorInitializer); } + } + + internal override bool IsPartial(INamedTypeSymbol type) + { + var syntaxRefs = type.DeclaringSyntaxReferences; + return syntaxRefs.Length > 1 + || ((BaseTypeDeclarationSyntax)syntaxRefs.Single().GetSyntax()).Modifiers.Any(SyntaxKind.PartialKeyword); + } + + protected override SyntaxNode? GetSymbolDeclarationSyntax(ISymbol symbol, Func, SyntaxReference?> selector, CancellationToken cancellationToken) + { + // TODO: Workaround for https://github.com/dotnet/roslyn/issues/68510 + // symbol.IsImplicitlyDeclared should only be true if symbol.DeclaringSyntaxReferences is non-empty + + var syntax = symbol switch + { + IMethodSymbol + { + IsImplicitlyDeclared: true, + ContainingType.IsRecord: true, + Name: + WellKnownMemberNames.PrintMembersMethodName or + WellKnownMemberNames.ObjectEquals or + WellKnownMemberNames.ObjectGetHashCode or + WellKnownMemberNames.ObjectToString or + WellKnownMemberNames.DeconstructMethodName + } => null, + _ => selector(symbol.DeclaringSyntaxReferences)?.GetSyntax(cancellationToken) + }; + + // Use the parameter list to represent primary constructor declaration. + return symbol.Kind == SymbolKind.Method && syntax is TypeDeclarationSyntax { ParameterList: { } parameterList } ? parameterList : syntax; + } - protected override SyntaxNode? GetSymbolDeclarationSyntax(ISymbol symbol, Func, SyntaxReference?> selector, CancellationToken cancellationToken) + protected override ISymbol? GetDeclaredSymbol(SemanticModel model, SyntaxNode declaration, CancellationToken cancellationToken) + { + if (IsPrimaryConstructorDeclaration(declaration)) { - // TODO: Workaround for https://github.com/dotnet/roslyn/issues/68510 - // symbol.IsImplicitlyDeclared should only be true if symbol.DeclaringSyntaxReferences is non-empty + Contract.ThrowIfNull(declaration.Parent); + var recordType = (INamedTypeSymbol?)model.GetDeclaredSymbol(declaration.Parent, cancellationToken); + Contract.ThrowIfNull(recordType); + return recordType.InstanceConstructors.Single(ctor => ctor.DeclaringSyntaxReferences is [var syntaxRef] && syntaxRef.GetSyntax(cancellationToken) == declaration.Parent); + } - var syntax = symbol switch - { - IMethodSymbol - { - IsImplicitlyDeclared: true, - ContainingType.IsRecord: true, - Name: - WellKnownMemberNames.PrintMembersMethodName or - WellKnownMemberNames.ObjectEquals or - WellKnownMemberNames.ObjectGetHashCode or - WellKnownMemberNames.ObjectToString or - WellKnownMemberNames.DeconstructMethodName - } => null, - _ => selector(symbol.DeclaringSyntaxReferences)?.GetSyntax(cancellationToken) - }; + return model.GetDeclaredSymbol(declaration, cancellationToken); + } + + protected override OneOrMany<(ISymbol? oldSymbol, ISymbol? newSymbol)> GetEditedSymbols( + EditKind editKind, + SyntaxNode? oldNode, + SyntaxNode? newNode, + SemanticModel? oldModel, + SemanticModel newModel, + CancellationToken cancellationToken) + { + // Chnage in type of a field affects all its variable declarations. + if (oldNode is VariableDeclarationSyntax oldVariableDeclaration && newNode is VariableDeclarationSyntax newVariableDeclaration) + { + return AddFieldSymbolUpdates(oldVariableDeclaration.Variables, newVariableDeclaration.Variables); + } - // Use the parameter list to represent primary constructor declaration. - return symbol.Kind == SymbolKind.Method && syntax is TypeDeclarationSyntax { ParameterList: { } parameterList } ? parameterList : syntax; + // Change in attributes or modifiers of a field affects all its variable declarations. + if (oldNode is BaseFieldDeclarationSyntax oldField && newNode is BaseFieldDeclarationSyntax newField) + { + return AddFieldSymbolUpdates(oldField.Declaration.Variables, newField.Declaration.Variables); } - protected override ISymbol? GetDeclaredSymbol(SemanticModel model, SyntaxNode declaration, CancellationToken cancellationToken) + OneOrMany<(ISymbol? oldSymbol, ISymbol? newSymbol)> AddFieldSymbolUpdates(SeparatedSyntaxList oldVariables, SeparatedSyntaxList newVariables) { - if (IsPrimaryConstructorDeclaration(declaration)) + Debug.Assert(oldModel != null); + + if (oldVariables.Count == 1 && newVariables.Count == 1) { - Contract.ThrowIfNull(declaration.Parent); - var recordType = (INamedTypeSymbol?)model.GetDeclaredSymbol(declaration.Parent, cancellationToken); - Contract.ThrowIfNull(recordType); - return recordType.InstanceConstructors.Single(ctor => ctor.DeclaringSyntaxReferences is [var syntaxRef] && syntaxRef.GetSyntax(cancellationToken) == declaration.Parent); + return OneOrMany.Create((GetDeclaredSymbol(oldModel, oldVariables[0], cancellationToken), GetDeclaredSymbol(newModel, newVariables[0], cancellationToken))); } - return model.GetDeclaredSymbol(declaration, cancellationToken); + return OneOrMany.Create( + (from oldVariable in oldVariables + join newVariable in newVariables on oldVariable.Identifier.Text equals newVariable.Identifier.Text + select (GetDeclaredSymbol(oldModel, oldVariable, cancellationToken), GetDeclaredSymbol(newModel, newVariable, cancellationToken))).ToImmutableArray()); } - protected override OneOrMany<(ISymbol? oldSymbol, ISymbol? newSymbol)> GetEditedSymbols( - EditKind editKind, - SyntaxNode? oldNode, - SyntaxNode? newNode, - SemanticModel? oldModel, - SemanticModel newModel, - CancellationToken cancellationToken) + var oldSymbol = (oldNode != null) ? GetSymbolForEdit(oldNode, oldModel!, cancellationToken) : null; + var newSymbol = (newNode != null) ? GetSymbolForEdit(newNode, newModel, cancellationToken) : null; + + return (oldSymbol == null && newSymbol == null) + ? OneOrMany<(ISymbol?, ISymbol?)>.Empty + : OneOrMany.Create((oldSymbol, newSymbol)); + } + + protected override void AddSymbolEdits( + ref TemporaryArray<(ISymbol?, ISymbol?, EditKind)> result, + EditKind editKind, + SyntaxNode? oldNode, + ISymbol? oldSymbol, + SyntaxNode? newNode, + ISymbol? newSymbol, + SemanticModel? oldModel, + SemanticModel newModel, + Match topMatch, + IReadOnlyDictionary editMap, + SymbolInfoCache symbolCache, + CancellationToken cancellationToken) + { + if (oldNode is ParameterSyntax or TypeParameterSyntax or TypeParameterConstraintClauseSyntax || + newNode is ParameterSyntax or TypeParameterSyntax or TypeParameterConstraintClauseSyntax) { - // Chnage in type of a field affects all its variable declarations. - if (oldNode is VariableDeclarationSyntax oldVariableDeclaration && newNode is VariableDeclarationSyntax newVariableDeclaration) - { - return AddFieldSymbolUpdates(oldVariableDeclaration.Variables, newVariableDeclaration.Variables); - } + var oldContainingMemberOrType = GetParameterContainingMemberOrType(oldNode, newNode, oldModel, topMatch.ReverseMatches, cancellationToken); + var newContainingMemberOrType = GetParameterContainingMemberOrType(newNode, oldNode, newModel, topMatch.Matches, cancellationToken); + + var matchingNewContainingMemberOrType = GetSemanticallyMatchingNewSymbol(oldContainingMemberOrType, newContainingMemberOrType, newModel, symbolCache, cancellationToken); - // Change in attributes or modifiers of a field affects all its variable declarations. - if (oldNode is BaseFieldDeclarationSyntax oldField && newNode is BaseFieldDeclarationSyntax newField) + // Create candidate symbol edits to analyze: + // 1) An update of the containing member or type + // Produces an update edit of the containing member or type, or delete & insert edits if the signature changed. + // Reports rude edits for unsupported updates to the body (e.g. to active statements, lambdas, etc.). + // The containing member edit will cover any changes of the name, modifiers and attributes of the parameter. + // 2) Edit of the parameter itself + // Produces semantic edits for any synthesized members generated based on the parameter. + // Reports rude edits for unsupported renames, reoders, attribute and modifer changes. + // Does not result in a semantic edit for the parameter itself. + // 3) Edits of symbols synthesized based on the parameters that have declaring syntax. + // These members need to be analyzed for active statement mapping, type layout, etc. + // E.g. property accessors synthesized for record primary constructor parameters have bodies that may contain active statements. + + // If the signature of a property/indexer changed or a parameter of an indexer has been renamed we need to update all its accessors + if (oldContainingMemberOrType is IPropertySymbol oldPropertySymbol && + newContainingMemberOrType is IPropertySymbol newPropertySymbol && + (IsMemberOrDelegateReplaced(oldPropertySymbol, newPropertySymbol) || + oldSymbol != null && newSymbol != null && oldSymbol.Name != newSymbol.Name)) { - return AddFieldSymbolUpdates(oldField.Declaration.Variables, newField.Declaration.Variables); + AddMemberUpdate(ref result, oldPropertySymbol.GetMethod, newPropertySymbol.GetMethod, matchingNewContainingMemberOrType); + AddMemberUpdate(ref result, oldPropertySymbol.SetMethod, newPropertySymbol.SetMethod, matchingNewContainingMemberOrType); } - OneOrMany<(ISymbol? oldSymbol, ISymbol? newSymbol)> AddFieldSymbolUpdates(SeparatedSyntaxList oldVariables, SeparatedSyntaxList newVariables) + // record primary constructor parameter + if (oldNode is ParameterSyntax { Parent.Parent: RecordDeclarationSyntax } || + newNode is ParameterSyntax { Parent.Parent: RecordDeclarationSyntax }) { - Debug.Assert(oldModel != null); + Debug.Assert(matchingNewContainingMemberOrType == null); + + var oldSynthesizedAutoProperty = (IPropertySymbol?)oldSymbol?.ContainingType.GetMembers(oldSymbol.Name).FirstOrDefault(m => m.IsSynthesizedAutoProperty()); + var newSynthesizedAutoProperty = (IPropertySymbol?)newSymbol?.ContainingType.GetMembers(newSymbol.Name).FirstOrDefault(m => m.IsSynthesizedAutoProperty()); - if (oldVariables.Count == 1 && newVariables.Count == 1) + if (oldSynthesizedAutoProperty != null || newSynthesizedAutoProperty != null) { - return OneOrMany.Create((GetDeclaredSymbol(oldModel, oldVariables[0], cancellationToken), GetDeclaredSymbol(newModel, newVariables[0], cancellationToken))); + result.Add((oldSynthesizedAutoProperty, newSynthesizedAutoProperty, editKind)); + result.Add((oldSynthesizedAutoProperty?.GetMethod, newSynthesizedAutoProperty?.GetMethod, editKind)); + result.Add((oldSynthesizedAutoProperty?.SetMethod, newSynthesizedAutoProperty?.SetMethod, editKind)); } - - return OneOrMany.Create( - (from oldVariable in oldVariables - join newVariable in newVariables on oldVariable.Identifier.Text equals newVariable.Identifier.Text - select (GetDeclaredSymbol(oldModel, oldVariable, cancellationToken), GetDeclaredSymbol(newModel, newVariable, cancellationToken))).ToImmutableArray()); } - var oldSymbol = (oldNode != null) ? GetSymbolForEdit(oldNode, oldModel!, cancellationToken) : null; - var newSymbol = (newNode != null) ? GetSymbolForEdit(newNode, newModel, cancellationToken) : null; + AddMemberUpdate(ref result, oldContainingMemberOrType, newContainingMemberOrType, matchingNewContainingMemberOrType); - return (oldSymbol == null && newSymbol == null) - ? OneOrMany<(ISymbol?, ISymbol?)>.Empty - : OneOrMany.Create((oldSymbol, newSymbol)); - } + // Any change to a constraint should be analyzed as an update of the type parameter: + var isTypeConstraint = oldNode is TypeParameterConstraintClauseSyntax || newNode is TypeParameterConstraintClauseSyntax; - protected override void AddSymbolEdits( - ref TemporaryArray<(ISymbol?, ISymbol?, EditKind)> result, - EditKind editKind, - SyntaxNode? oldNode, - ISymbol? oldSymbol, - SyntaxNode? newNode, - ISymbol? newSymbol, - SemanticModel? oldModel, - SemanticModel newModel, - Match topMatch, - IReadOnlyDictionary editMap, - SymbolInfoCache symbolCache, - CancellationToken cancellationToken) - { - if (oldNode is ParameterSyntax or TypeParameterSyntax or TypeParameterConstraintClauseSyntax || - newNode is ParameterSyntax or TypeParameterSyntax or TypeParameterConstraintClauseSyntax) + if (matchingNewContainingMemberOrType != null) { - var oldContainingMemberOrType = GetParameterContainingMemberOrType(oldNode, newNode, oldModel, topMatch.ReverseMatches, cancellationToken); - var newContainingMemberOrType = GetParameterContainingMemberOrType(newNode, oldNode, newModel, topMatch.Matches, cancellationToken); - - var matchingNewContainingMemberOrType = GetSemanticallyMatchingNewSymbol(oldContainingMemberOrType, newContainingMemberOrType, newModel, symbolCache, cancellationToken); - - // Create candidate symbol edits to analyze: - // 1) An update of the containing member or type - // Produces an update edit of the containing member or type, or delete & insert edits if the signature changed. - // Reports rude edits for unsupported updates to the body (e.g. to active statements, lambdas, etc.). - // The containing member edit will cover any changes of the name, modifiers and attributes of the parameter. - // 2) Edit of the parameter itself - // Produces semantic edits for any synthesized members generated based on the parameter. - // Reports rude edits for unsupported renames, reoders, attribute and modifer changes. - // Does not result in a semantic edit for the parameter itself. - // 3) Edits of symbols synthesized based on the parameters that have declaring syntax. - // These members need to be analyzed for active statement mapping, type layout, etc. - // E.g. property accessors synthesized for record primary constructor parameters have bodies that may contain active statements. - - // If the signature of a property/indexer changed or a parameter of an indexer has been renamed we need to update all its accessors - if (oldContainingMemberOrType is IPropertySymbol oldPropertySymbol && - newContainingMemberOrType is IPropertySymbol newPropertySymbol && - (IsMemberOrDelegateReplaced(oldPropertySymbol, newPropertySymbol) || - oldSymbol != null && newSymbol != null && oldSymbol.Name != newSymbol.Name)) + // Map parameter to the corresponding semantically matching member. + // Since the signature of the member matches we can direcly map by parameter ordinal. + if (oldSymbol is IParameterSymbol oldParameter) { - AddMemberUpdate(ref result, oldPropertySymbol.GetMethod, newPropertySymbol.GetMethod, matchingNewContainingMemberOrType); - AddMemberUpdate(ref result, oldPropertySymbol.SetMethod, newPropertySymbol.SetMethod, matchingNewContainingMemberOrType); + newSymbol = matchingNewContainingMemberOrType.GetParameters()[oldParameter.Ordinal]; } - - // record primary constructor parameter - if (oldNode is ParameterSyntax { Parent.Parent: RecordDeclarationSyntax } || - newNode is ParameterSyntax { Parent.Parent: RecordDeclarationSyntax }) + else if (oldSymbol is ITypeParameterSymbol oldTypeParameter) { - Debug.Assert(matchingNewContainingMemberOrType == null); - - var oldSynthesizedAutoProperty = (IPropertySymbol?)oldSymbol?.ContainingType.GetMembers(oldSymbol.Name).FirstOrDefault(m => m.IsSynthesizedAutoProperty()); - var newSynthesizedAutoProperty = (IPropertySymbol?)newSymbol?.ContainingType.GetMembers(newSymbol.Name).FirstOrDefault(m => m.IsSynthesizedAutoProperty()); - - if (oldSynthesizedAutoProperty != null || newSynthesizedAutoProperty != null) - { - result.Add((oldSynthesizedAutoProperty, newSynthesizedAutoProperty, editKind)); - result.Add((oldSynthesizedAutoProperty?.GetMethod, newSynthesizedAutoProperty?.GetMethod, editKind)); - result.Add((oldSynthesizedAutoProperty?.SetMethod, newSynthesizedAutoProperty?.SetMethod, editKind)); - } + newSymbol = matchingNewContainingMemberOrType.GetTypeParameters()[oldTypeParameter.Ordinal]; } + } - AddMemberUpdate(ref result, oldContainingMemberOrType, newContainingMemberOrType, matchingNewContainingMemberOrType); + result.Add((oldSymbol, newSymbol, isTypeConstraint ? EditKind.Update : editKind)); + + return; + } - // Any change to a constraint should be analyzed as an update of the type parameter: - var isTypeConstraint = oldNode is TypeParameterConstraintClauseSyntax || newNode is TypeParameterConstraintClauseSyntax; + switch (editKind) + { + case EditKind.Reorder: + Contract.ThrowIfNull(oldNode); - if (matchingNewContainingMemberOrType != null) + if (IsGlobalStatement(oldNode)) { - // Map parameter to the corresponding semantically matching member. - // Since the signature of the member matches we can direcly map by parameter ordinal. - if (oldSymbol is IParameterSymbol oldParameter) - { - newSymbol = matchingNewContainingMemberOrType.GetParameters()[oldParameter.Ordinal]; - } - else if (oldSymbol is ITypeParameterSymbol oldTypeParameter) - { - newSymbol = matchingNewContainingMemberOrType.GetTypeParameters()[oldTypeParameter.Ordinal]; - } + // When global statements are reordered, we issue an update edit for the synthesized main method, which is what + // oldSymbol and newSymbol will point to + result.Add((oldSymbol, newSymbol, EditKind.Update)); + return; } - result.Add((oldSymbol, newSymbol, isTypeConstraint ? EditKind.Update : editKind)); + // Otherwise, we don't do any semantic checks for reordering + // and we don't need to report them to the compiler either. + // Consider: Currently symbol ordering changes are not reflected in metadata (Reflection will report original order). + // Consider: Reordering of fields is not allowed since it changes the layout of the type. + // This ordering should however not matter unless the type has explicit layout so we might want to allow it. + // We do not check changes to the order if they occur across multiple documents (the containing type is partial). + Debug.Assert(!IsDeclarationWithInitializer(oldNode!) && !IsDeclarationWithInitializer(newNode!)); return; - } - - switch (editKind) - { - case EditKind.Reorder: - Contract.ThrowIfNull(oldNode); - if (IsGlobalStatement(oldNode)) - { - // When global statements are reordered, we issue an update edit for the synthesized main method, which is what - // oldSymbol and newSymbol will point to - result.Add((oldSymbol, newSymbol, EditKind.Update)); - return; - } + case EditKind.Update: + Contract.ThrowIfNull(oldNode); + Contract.ThrowIfNull(newNode); + Contract.ThrowIfNull(oldModel); - // Otherwise, we don't do any semantic checks for reordering - // and we don't need to report them to the compiler either. - // Consider: Currently symbol ordering changes are not reflected in metadata (Reflection will report original order). + // Updates of a property/indexer/event node might affect its accessors. + // Return all affected symbols for these updates so that the changes in the accessor bodies get analyzed. - // Consider: Reordering of fields is not allowed since it changes the layout of the type. - // This ordering should however not matter unless the type has explicit layout so we might want to allow it. - // We do not check changes to the order if they occur across multiple documents (the containing type is partial). - Debug.Assert(!IsDeclarationWithInitializer(oldNode!) && !IsDeclarationWithInitializer(newNode!)); - return; + if (oldSymbol is IPropertySymbol oldPropertySymbol && newSymbol is IPropertySymbol newPropertySymbol) + { + // 1) Old or new property/indexer has an expression body: + // int this[...] => expr; + // int this[...] { get => expr; } + // int P => expr; + // int P { get => expr; } = init + // 2) Property/indexer declarations differ in readonly keyword. + // 3) Property signature changes + // 4) Property name changes - case EditKind.Update: - Contract.ThrowIfNull(oldNode); - Contract.ThrowIfNull(newNode); - Contract.ThrowIfNull(oldModel); + var oldHasExpressionBody = oldNode is PropertyDeclarationSyntax { ExpressionBody: not null } or IndexerDeclarationSyntax { ExpressionBody: not null }; + var newHasExpressionBody = newNode is PropertyDeclarationSyntax { ExpressionBody: not null } or IndexerDeclarationSyntax { ExpressionBody: not null }; - // Updates of a property/indexer/event node might affect its accessors. - // Return all affected symbols for these updates so that the changes in the accessor bodies get analyzed. + result.Add((oldPropertySymbol, newPropertySymbol, editKind)); - if (oldSymbol is IPropertySymbol oldPropertySymbol && newSymbol is IPropertySymbol newPropertySymbol) + if (oldPropertySymbol.GetMethod != null || newPropertySymbol.GetMethod != null) { - // 1) Old or new property/indexer has an expression body: - // int this[...] => expr; - // int this[...] { get => expr; } - // int P => expr; - // int P { get => expr; } = init - // 2) Property/indexer declarations differ in readonly keyword. - // 3) Property signature changes - // 4) Property name changes - - var oldHasExpressionBody = oldNode is PropertyDeclarationSyntax { ExpressionBody: not null } or IndexerDeclarationSyntax { ExpressionBody: not null }; - var newHasExpressionBody = newNode is PropertyDeclarationSyntax { ExpressionBody: not null } or IndexerDeclarationSyntax { ExpressionBody: not null }; - - result.Add((oldPropertySymbol, newPropertySymbol, editKind)); - - if (oldPropertySymbol.GetMethod != null || newPropertySymbol.GetMethod != null) - { - if (oldHasExpressionBody || - newHasExpressionBody || - DiffersInReadOnlyModifier(oldPropertySymbol.GetMethod, newPropertySymbol.GetMethod) || - IsMemberOrDelegateReplaced(oldPropertySymbol, newPropertySymbol)) - { - result.Add((oldPropertySymbol.GetMethod, newPropertySymbol.GetMethod, editKind)); - } - } - - if (oldPropertySymbol.SetMethod != null || newPropertySymbol.SetMethod != null) + if (oldHasExpressionBody || + newHasExpressionBody || + DiffersInReadOnlyModifier(oldPropertySymbol.GetMethod, newPropertySymbol.GetMethod) || + IsMemberOrDelegateReplaced(oldPropertySymbol, newPropertySymbol)) { - if (DiffersInReadOnlyModifier(oldPropertySymbol.SetMethod, newPropertySymbol.SetMethod) || - IsMemberOrDelegateReplaced(oldPropertySymbol, newPropertySymbol)) - { - result.Add((oldPropertySymbol.SetMethod, newPropertySymbol.SetMethod, editKind)); - } + result.Add((oldPropertySymbol.GetMethod, newPropertySymbol.GetMethod, editKind)); } - - return; } - if (oldSymbol is IEventSymbol oldEventSymbol && newSymbol is IEventSymbol newEventSymbol) + if (oldPropertySymbol.SetMethod != null || newPropertySymbol.SetMethod != null) { - // 1) Event declarations differ in readonly keyword. - // 2) Event signature changes - // 3) Event name changes - - result.Add((oldEventSymbol, newEventSymbol, editKind)); - - if (oldEventSymbol.AddMethod != null || newEventSymbol.AddMethod != null) + if (DiffersInReadOnlyModifier(oldPropertySymbol.SetMethod, newPropertySymbol.SetMethod) || + IsMemberOrDelegateReplaced(oldPropertySymbol, newPropertySymbol)) { - if (DiffersInReadOnlyModifier(oldEventSymbol.AddMethod, newEventSymbol.AddMethod) || - IsMemberOrDelegateReplaced(oldEventSymbol, newEventSymbol)) - { - result.Add((oldEventSymbol.AddMethod, newEventSymbol.AddMethod, editKind)); - } + result.Add((oldPropertySymbol.SetMethod, newPropertySymbol.SetMethod, editKind)); } + } - if (oldEventSymbol.RemoveMethod != null || newEventSymbol.RemoveMethod != null) - { - if (DiffersInReadOnlyModifier(oldEventSymbol.RemoveMethod, newEventSymbol.RemoveMethod) || - IsMemberOrDelegateReplaced(oldEventSymbol, newEventSymbol)) - { - result.Add((oldEventSymbol.RemoveMethod, newEventSymbol.RemoveMethod, editKind)); - } - } + return; + } - return; - } + if (oldSymbol is IEventSymbol oldEventSymbol && newSymbol is IEventSymbol newEventSymbol) + { + // 1) Event declarations differ in readonly keyword. + // 2) Event signature changes + // 3) Event name changes - static bool DiffersInReadOnlyModifier(IMethodSymbol? oldMethod, IMethodSymbol? newMethod) - => oldMethod != null && newMethod != null && oldMethod.IsReadOnly != newMethod.IsReadOnly; + result.Add((oldEventSymbol, newEventSymbol, editKind)); - // Update to a type declaration with primary constructor may also need to update - // the primary constructor, copy-constructor and/or synthesized record auto-properties. - // Active statements in bodies of these symbols lay within the type declaration node. - if (oldNode is TypeDeclarationSyntax oldTypeDeclaration && - newNode is TypeDeclarationSyntax newTypeDeclaration && - (oldTypeDeclaration.ParameterList != null || newTypeDeclaration.ParameterList != null)) + if (oldEventSymbol.AddMethod != null || newEventSymbol.AddMethod != null) { - if (oldSymbol is not INamedTypeSymbol oldType || newSymbol is not INamedTypeSymbol newType) + if (DiffersInReadOnlyModifier(oldEventSymbol.AddMethod, newEventSymbol.AddMethod) || + IsMemberOrDelegateReplaced(oldEventSymbol, newEventSymbol)) { - throw ExceptionUtilities.Unreachable(); + result.Add((oldEventSymbol.AddMethod, newEventSymbol.AddMethod, editKind)); } + } - // the type kind, attributes, constracints may have changed: - result.Add((oldSymbol, newSymbol, EditKind.Update)); - - var typeNameSpanChanged = - oldTypeDeclaration.Identifier.Span != newTypeDeclaration.Identifier.Span || - oldTypeDeclaration.TypeParameterList?.Span != newTypeDeclaration.TypeParameterList?.Span; - - // primary constructor active span: [|C(...)|] - if (typeNameSpanChanged || - oldTypeDeclaration.ParameterList?.Span != newTypeDeclaration.ParameterList?.Span) + if (oldEventSymbol.RemoveMethod != null || newEventSymbol.RemoveMethod != null) + { + if (DiffersInReadOnlyModifier(oldEventSymbol.RemoveMethod, newEventSymbol.RemoveMethod) || + IsMemberOrDelegateReplaced(oldEventSymbol, newEventSymbol)) { - var oldPrimaryConstructor = GetPrimaryConstructor(oldType, cancellationToken); - - // The matching constructor might not be specified using primary constructor syntax. - // Let the symbol resolution fill in the other constructor instead of specifying both symbols here. - var newPrimaryConstructor = (oldPrimaryConstructor == null) ? GetPrimaryConstructor(newType, cancellationToken) : null; - - result.Add((oldPrimaryConstructor, newPrimaryConstructor, EditKind.Update)); + result.Add((oldEventSymbol.RemoveMethod, newEventSymbol.RemoveMethod, editKind)); } + } - // copy constructor active span: [|C|] - if (typeNameSpanChanged && (oldNode.IsKind(SyntaxKind.RecordDeclaration) || newNode.IsKind(SyntaxKind.RecordDeclaration))) - { - var oldCopyConstructor = oldType.InstanceConstructors.FirstOrDefault(c => c.IsCopyConstructor()); - var newCopyConstructor = newType.InstanceConstructors.FirstOrDefault(c => c.IsCopyConstructor()); - Debug.Assert(oldCopyConstructor != null || newCopyConstructor != null); + return; + } - result.Add((oldCopyConstructor, newCopyConstructor, EditKind.Update)); - } + static bool DiffersInReadOnlyModifier(IMethodSymbol? oldMethod, IMethodSymbol? newMethod) + => oldMethod != null && newMethod != null && oldMethod.IsReadOnly != newMethod.IsReadOnly; - return; + // Update to a type declaration with primary constructor may also need to update + // the primary constructor, copy-constructor and/or synthesized record auto-properties. + // Active statements in bodies of these symbols lay within the type declaration node. + if (oldNode is TypeDeclarationSyntax oldTypeDeclaration && + newNode is TypeDeclarationSyntax newTypeDeclaration && + (oldTypeDeclaration.ParameterList != null || newTypeDeclaration.ParameterList != null)) + { + if (oldSymbol is not INamedTypeSymbol oldType || newSymbol is not INamedTypeSymbol newType) + { + throw ExceptionUtilities.Unreachable(); } - break; + // the type kind, attributes, constracints may have changed: + result.Add((oldSymbol, newSymbol, EditKind.Update)); - case EditKind.Delete: - case EditKind.Insert: - var node = oldNode ?? newNode; + var typeNameSpanChanged = + oldTypeDeclaration.Identifier.Span != newTypeDeclaration.Identifier.Span || + oldTypeDeclaration.TypeParameterList?.Span != newTypeDeclaration.TypeParameterList?.Span; - // If the entire block-bodied property/indexer is deleted/inserted (accessors and the list they are contained in), - // ignore this edit. We will have a semantic edit for the property/indexer itself. - if (node.IsKind(SyntaxKind.GetAccessorDeclaration)) + // primary constructor active span: [|C(...)|] + if (typeNameSpanChanged || + oldTypeDeclaration.ParameterList?.Span != newTypeDeclaration.ParameterList?.Span) { - Debug.Assert(node.Parent.IsKind(SyntaxKind.AccessorList)); + var oldPrimaryConstructor = GetPrimaryConstructor(oldType, cancellationToken); - if (HasEdit(editMap, node.Parent, editKind) && !HasEdit(editMap, node.Parent.Parent, editKind)) - { - return; - } - } + // The matching constructor might not be specified using primary constructor syntax. + // Let the symbol resolution fill in the other constructor instead of specifying both symbols here. + var newPrimaryConstructor = (oldPrimaryConstructor == null) ? GetPrimaryConstructor(newType, cancellationToken) : null; - // Inserting/deleting an expression-bodied property/indexer affects two symbols: - // the property/indexer itself and the getter. - // int this[...] => expr; - // int P => expr; - if (node is PropertyDeclarationSyntax { ExpressionBody: not null } or IndexerDeclarationSyntax { ExpressionBody: not null }) - { - var oldGetterSymbol = ((IPropertySymbol?)oldSymbol)?.GetMethod; - var newGetterSymbol = ((IPropertySymbol?)newSymbol)?.GetMethod; - result.Add((oldSymbol, newSymbol, editKind)); - result.Add((oldGetterSymbol, newGetterSymbol, editKind)); - return; + result.Add((oldPrimaryConstructor, newPrimaryConstructor, EditKind.Update)); } - // Inserting/deleting a type parameter constraint should result in an update of the corresponding type parameter symbol: - if (node.IsKind(SyntaxKind.TypeParameterConstraintClause)) + // copy constructor active span: [|C|] + if (typeNameSpanChanged && (oldNode.IsKind(SyntaxKind.RecordDeclaration) || newNode.IsKind(SyntaxKind.RecordDeclaration))) { - result.Add((oldSymbol, newSymbol, EditKind.Update)); - return; - } + var oldCopyConstructor = oldType.InstanceConstructors.FirstOrDefault(c => c.IsCopyConstructor()); + var newCopyConstructor = newType.InstanceConstructors.FirstOrDefault(c => c.IsCopyConstructor()); + Debug.Assert(oldCopyConstructor != null || newCopyConstructor != null); - // Inserting/deleting a global statement should result in an update of the implicit main method: - if (node.IsKind(SyntaxKind.GlobalStatement)) - { - result.Add((oldSymbol, newSymbol, EditKind.Update)); - return; + result.Add((oldCopyConstructor, newCopyConstructor, EditKind.Update)); } - // Inserting/deleting a primary constructor base initializer/base list is an update of the constructor/type, - // not a delete/insert of the constructor/type itself: - if (node is (kind: SyntaxKind.PrimaryConstructorBaseType or SyntaxKind.BaseList)) - { - result.Add((oldSymbol, newSymbol, EditKind.Update)); - return; - } + return; + } - break; + break; - case EditKind.Move: - Contract.ThrowIfNull(oldNode); - Contract.ThrowIfNull(newNode); - Contract.ThrowIfNull(oldModel); + case EditKind.Delete: + case EditKind.Insert: + var node = oldNode ?? newNode; - Debug.Assert(oldNode.RawKind == newNode.RawKind); - Debug.Assert(SupportsMove(oldNode)); - Debug.Assert(SupportsMove(newNode)); + // If the entire block-bodied property/indexer is deleted/inserted (accessors and the list they are contained in), + // ignore this edit. We will have a semantic edit for the property/indexer itself. + if (node.IsKind(SyntaxKind.GetAccessorDeclaration)) + { + Debug.Assert(node.Parent.IsKind(SyntaxKind.AccessorList)); - if (oldNode.IsKind(SyntaxKind.LocalFunctionStatement)) + if (HasEdit(editMap, node.Parent, editKind) && !HasEdit(editMap, node.Parent.Parent, editKind)) { return; } + } + // Inserting/deleting an expression-bodied property/indexer affects two symbols: + // the property/indexer itself and the getter. + // int this[...] => expr; + // int P => expr; + if (node is PropertyDeclarationSyntax { ExpressionBody: not null } or IndexerDeclarationSyntax { ExpressionBody: not null }) + { + var oldGetterSymbol = ((IPropertySymbol?)oldSymbol)?.GetMethod; + var newGetterSymbol = ((IPropertySymbol?)newSymbol)?.GetMethod; result.Add((oldSymbol, newSymbol, editKind)); + result.Add((oldGetterSymbol, newGetterSymbol, editKind)); return; - } + } - if ((editKind == EditKind.Delete ? oldSymbol : newSymbol) is null) - { - return; - } + // Inserting/deleting a type parameter constraint should result in an update of the corresponding type parameter symbol: + if (node.IsKind(SyntaxKind.TypeParameterConstraintClause)) + { + result.Add((oldSymbol, newSymbol, EditKind.Update)); + return; + } - result.Add((oldSymbol, newSymbol, editKind)); - } + // Inserting/deleting a global statement should result in an update of the implicit main method: + if (node.IsKind(SyntaxKind.GlobalStatement)) + { + result.Add((oldSymbol, newSymbol, EditKind.Update)); + return; + } - private ISymbol? GetSymbolForEdit( - SyntaxNode node, - SemanticModel model, - CancellationToken cancellationToken) - { - if (node.Kind() is SyntaxKind.UsingDirective or SyntaxKind.NamespaceDeclaration or SyntaxKind.FileScopedNamespaceDeclaration) - { - return null; - } + // Inserting/deleting a primary constructor base initializer/base list is an update of the constructor/type, + // not a delete/insert of the constructor/type itself: + if (node is (kind: SyntaxKind.PrimaryConstructorBaseType or SyntaxKind.BaseList)) + { + result.Add((oldSymbol, newSymbol, EditKind.Update)); + return; + } - if (node.IsKind(SyntaxKind.TypeParameterConstraintClause)) - { - var constraintClause = (TypeParameterConstraintClauseSyntax)node; - var symbolInfo = model.GetSymbolInfo(constraintClause.Name, cancellationToken); - return symbolInfo.Symbol; - } + break; - // Top level code always lives in a synthesized Main method - if (node.IsKind(SyntaxKind.GlobalStatement)) - { - return model.GetEnclosingSymbol(node.SpanStart, cancellationToken); - } + case EditKind.Move: + Contract.ThrowIfNull(oldNode); + Contract.ThrowIfNull(newNode); + Contract.ThrowIfNull(oldModel); - if (node is PrimaryConstructorBaseTypeSyntax primaryCtorBase) - { - return model.GetEnclosingSymbol(primaryCtorBase.ArgumentList.SpanStart, cancellationToken); - } + Debug.Assert(oldNode.RawKind == newNode.RawKind); + Debug.Assert(SupportsMove(oldNode)); + Debug.Assert(SupportsMove(newNode)); - if (node.IsKind(SyntaxKind.BaseList)) - { - Contract.ThrowIfNull(node.Parent); - node = node.Parent; - } + if (oldNode.IsKind(SyntaxKind.LocalFunctionStatement)) + { + return; + } - return GetDeclaredSymbol(model, node, cancellationToken); + result.Add((oldSymbol, newSymbol, editKind)); + return; } - private ISymbol? GetParameterContainingMemberOrType(SyntaxNode? node, SyntaxNode? otherNode, SemanticModel? model, IReadOnlyDictionary fromOtherMap, CancellationToken cancellationToken) + if ((editKind == EditKind.Delete ? oldSymbol : newSymbol) is null) { - Debug.Assert(node is null or ParameterSyntax or TypeParameterSyntax or TypeParameterConstraintClauseSyntax); - - // parameter list, member, or type declaration: - SyntaxNode? declaration; - - if (node == null) - { - Debug.Assert(otherNode != null); - - // A parameter-less method/indexer has a parameter list, but non-generic method does not have a type parameter list. - fromOtherMap.TryGetValue(GetContainingDeclaration(otherNode), out declaration); - } - else - { - declaration = GetContainingDeclaration(node); - } + return; + } - // Parent is a type for primary constructor parameters - if (declaration is BaseParameterListSyntax and not { Parent: TypeDeclarationSyntax }) - { - declaration = declaration.Parent; - } + result.Add((oldSymbol, newSymbol, editKind)); + } - return (declaration != null) ? GetDeclaredSymbol(model!, declaration, cancellationToken) : null; + private ISymbol? GetSymbolForEdit( + SyntaxNode node, + SemanticModel model, + CancellationToken cancellationToken) + { + if (node.Kind() is SyntaxKind.UsingDirective or SyntaxKind.NamespaceDeclaration or SyntaxKind.FileScopedNamespaceDeclaration) + { + return null; + } - static SyntaxNode GetContainingDeclaration(SyntaxNode node) - => node is TypeParameterSyntax ? node.Parent!.Parent! : node!.Parent!; + if (node.IsKind(SyntaxKind.TypeParameterConstraintClause)) + { + var constraintClause = (TypeParameterConstraintClauseSyntax)node; + var symbolInfo = model.GetSymbolInfo(constraintClause.Name, cancellationToken); + return symbolInfo.Symbol; } - private static bool SupportsMove(SyntaxNode node) - => node.IsKind(SyntaxKind.LocalFunctionStatement) || - IsTypeDeclaration(node) || - node is BaseNamespaceDeclarationSyntax; + // Top level code always lives in a synthesized Main method + if (node.IsKind(SyntaxKind.GlobalStatement)) + { + return model.GetEnclosingSymbol(node.SpanStart, cancellationToken); + } - internal override Func IsLambda - => LambdaUtilities.IsLambda; + if (node is PrimaryConstructorBaseTypeSyntax primaryCtorBase) + { + return model.GetEnclosingSymbol(primaryCtorBase.ArgumentList.SpanStart, cancellationToken); + } - internal override Func IsNotLambda - => LambdaUtilities.IsNotLambda; + if (node.IsKind(SyntaxKind.BaseList)) + { + Contract.ThrowIfNull(node.Parent); + node = node.Parent; + } - internal override bool IsLocalFunction(SyntaxNode node) - => node.IsKind(SyntaxKind.LocalFunctionStatement); + return GetDeclaredSymbol(model, node, cancellationToken); + } - internal override bool IsGenericLocalFunction(SyntaxNode node) - => node is LocalFunctionStatementSyntax { TypeParameterList: not null }; + private ISymbol? GetParameterContainingMemberOrType(SyntaxNode? node, SyntaxNode? otherNode, SemanticModel? model, IReadOnlyDictionary fromOtherMap, CancellationToken cancellationToken) + { + Debug.Assert(node is null or ParameterSyntax or TypeParameterSyntax or TypeParameterConstraintClauseSyntax); - internal override bool IsNestedFunction(SyntaxNode node) - => node is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax; + // parameter list, member, or type declaration: + SyntaxNode? declaration; - internal override bool TryGetLambdaBodies(SyntaxNode node, [NotNullWhen(true)] out LambdaBody? body1, out LambdaBody? body2) + if (node == null) { - if (LambdaUtilities.TryGetLambdaBodies(node, out var bodyNode1, out var bodyNode2)) - { - body1 = SyntaxUtilities.CreateLambdaBody(bodyNode1); - body2 = (bodyNode2 != null) ? SyntaxUtilities.CreateLambdaBody(bodyNode2) : null; - return true; - } + Debug.Assert(otherNode != null); - body1 = null; - body2 = null; - return false; + // A parameter-less method/indexer has a parameter list, but non-generic method does not have a type parameter list. + fromOtherMap.TryGetValue(GetContainingDeclaration(otherNode), out declaration); } - - internal override IMethodSymbol GetLambdaExpressionSymbol(SemanticModel model, SyntaxNode lambdaExpression, CancellationToken cancellationToken) + else { - var bodyExpression = LambdaUtilities.GetNestedFunctionBody(lambdaExpression); - return (IMethodSymbol)model.GetRequiredEnclosingSymbol(bodyExpression.SpanStart, cancellationToken); + declaration = GetContainingDeclaration(node); } - internal override SyntaxNode? GetContainingQueryExpression(SyntaxNode node) - => node.FirstAncestorOrSelf(); - - internal override bool QueryClauseLambdasTypeEquivalent(SemanticModel oldModel, SyntaxNode oldNode, SemanticModel newModel, SyntaxNode newNode, CancellationToken cancellationToken) + // Parent is a type for primary constructor parameters + if (declaration is BaseParameterListSyntax and not { Parent: TypeDeclarationSyntax }) { - switch (oldNode.Kind()) - { - case SyntaxKind.FromClause: - case SyntaxKind.LetClause: - case SyntaxKind.WhereClause: - case SyntaxKind.OrderByClause: - case SyntaxKind.JoinClause: - var oldQueryClauseInfo = oldModel.GetQueryClauseInfo((QueryClauseSyntax)oldNode, cancellationToken); - var newQueryClauseInfo = newModel.GetQueryClauseInfo((QueryClauseSyntax)newNode, cancellationToken); + declaration = declaration.Parent; + } + + return (declaration != null) ? GetDeclaredSymbol(model!, declaration, cancellationToken) : null; - return MemberOrDelegateSignaturesEquivalent(oldQueryClauseInfo.CastInfo.Symbol, newQueryClauseInfo.CastInfo.Symbol) && - MemberOrDelegateSignaturesEquivalent(oldQueryClauseInfo.OperationInfo.Symbol, newQueryClauseInfo.OperationInfo.Symbol); + static SyntaxNode GetContainingDeclaration(SyntaxNode node) + => node is TypeParameterSyntax ? node.Parent!.Parent! : node!.Parent!; + } - case SyntaxKind.AscendingOrdering: - case SyntaxKind.DescendingOrdering: - var oldOrderingInfo = oldModel.GetSymbolInfo(oldNode, cancellationToken); - var newOrderingInfo = newModel.GetSymbolInfo(newNode, cancellationToken); + private static bool SupportsMove(SyntaxNode node) + => node.IsKind(SyntaxKind.LocalFunctionStatement) || + IsTypeDeclaration(node) || + node is BaseNamespaceDeclarationSyntax; - return MemberOrDelegateSignaturesEquivalent(oldOrderingInfo.Symbol, newOrderingInfo.Symbol); + internal override Func IsLambda + => LambdaUtilities.IsLambda; - case SyntaxKind.SelectClause: - var oldSelectInfo = oldModel.GetSymbolInfo(oldNode, cancellationToken); - var newSelectInfo = newModel.GetSymbolInfo(newNode, cancellationToken); + internal override Func IsNotLambda + => LambdaUtilities.IsNotLambda; - // Changing reduced select clause to a non-reduced form or vice versa - // adds/removes a call to Select method, which is a supported change. + internal override bool IsLocalFunction(SyntaxNode node) + => node.IsKind(SyntaxKind.LocalFunctionStatement); - return oldSelectInfo.Symbol == null || - newSelectInfo.Symbol == null || - MemberOrDelegateSignaturesEquivalent(oldSelectInfo.Symbol, newSelectInfo.Symbol); + internal override bool IsGenericLocalFunction(SyntaxNode node) + => node is LocalFunctionStatementSyntax { TypeParameterList: not null }; - case SyntaxKind.GroupClause: - var oldGroupInfo = oldModel.GetSymbolInfo(oldNode, cancellationToken); - var newGroupInfo = newModel.GetSymbolInfo(newNode, cancellationToken); - return GroupBySignatureEquivalent(oldGroupInfo.Symbol as IMethodSymbol, newGroupInfo.Symbol as IMethodSymbol); + internal override bool IsNestedFunction(SyntaxNode node) + => node is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax; - default: - return true; - } + internal override bool TryGetLambdaBodies(SyntaxNode node, [NotNullWhen(true)] out LambdaBody? body1, out LambdaBody? body2) + { + if (LambdaUtilities.TryGetLambdaBodies(node, out var bodyNode1, out var bodyNode2)) + { + body1 = SyntaxUtilities.CreateLambdaBody(bodyNode1); + body2 = (bodyNode2 != null) ? SyntaxUtilities.CreateLambdaBody(bodyNode2) : null; + return true; } - private static bool GroupBySignatureEquivalent(IMethodSymbol? oldMethod, IMethodSymbol? newMethod) + body1 = null; + body2 = null; + return false; + } + + internal override IMethodSymbol GetLambdaExpressionSymbol(SemanticModel model, SyntaxNode lambdaExpression, CancellationToken cancellationToken) + { + var bodyExpression = LambdaUtilities.GetNestedFunctionBody(lambdaExpression); + return (IMethodSymbol)model.GetRequiredEnclosingSymbol(bodyExpression.SpanStart, cancellationToken); + } + + internal override SyntaxNode? GetContainingQueryExpression(SyntaxNode node) + => node.FirstAncestorOrSelf(); + + internal override bool QueryClauseLambdasTypeEquivalent(SemanticModel oldModel, SyntaxNode oldNode, SemanticModel newModel, SyntaxNode newNode, CancellationToken cancellationToken) + { + switch (oldNode.Kind()) { - // C# spec paragraph 7.16.2.6 "Groupby clauses": - // - // A query expression of the form - // from x in e group v by k - // is translated into - // (e).GroupBy(x => k, x => v) - // except when v is the identifier x, the translation is - // (e).GroupBy(x => k) - // - // Possible signatures: - // C> GroupBy(Func keySelector); - // C> GroupBy(Func keySelector, Func elementSelector); + case SyntaxKind.FromClause: + case SyntaxKind.LetClause: + case SyntaxKind.WhereClause: + case SyntaxKind.OrderByClause: + case SyntaxKind.JoinClause: + var oldQueryClauseInfo = oldModel.GetQueryClauseInfo((QueryClauseSyntax)oldNode, cancellationToken); + var newQueryClauseInfo = newModel.GetQueryClauseInfo((QueryClauseSyntax)newNode, cancellationToken); - if (oldMethod == newMethod) - { - return true; - } + return MemberOrDelegateSignaturesEquivalent(oldQueryClauseInfo.CastInfo.Symbol, newQueryClauseInfo.CastInfo.Symbol) && + MemberOrDelegateSignaturesEquivalent(oldQueryClauseInfo.OperationInfo.Symbol, newQueryClauseInfo.OperationInfo.Symbol); - if (oldMethod == null || newMethod == null) - { - return false; - } + case SyntaxKind.AscendingOrdering: + case SyntaxKind.DescendingOrdering: + var oldOrderingInfo = oldModel.GetSymbolInfo(oldNode, cancellationToken); + var newOrderingInfo = newModel.GetSymbolInfo(newNode, cancellationToken); - if (!TypesEquivalent(oldMethod.ReturnType, newMethod.ReturnType, exact: false)) - { - return false; - } + return MemberOrDelegateSignaturesEquivalent(oldOrderingInfo.Symbol, newOrderingInfo.Symbol); - var oldParameters = oldMethod.Parameters; - var newParameters = newMethod.Parameters; + case SyntaxKind.SelectClause: + var oldSelectInfo = oldModel.GetSymbolInfo(oldNode, cancellationToken); + var newSelectInfo = newModel.GetSymbolInfo(newNode, cancellationToken); - Debug.Assert(oldParameters.Length is 1 or 2); - Debug.Assert(newParameters.Length is 1 or 2); + // Changing reduced select clause to a non-reduced form or vice versa + // adds/removes a call to Select method, which is a supported change. - // The types of the lambdas have to be the same if present. - // The element selector may be added/removed. + return oldSelectInfo.Symbol == null || + newSelectInfo.Symbol == null || + MemberOrDelegateSignaturesEquivalent(oldSelectInfo.Symbol, newSelectInfo.Symbol); - if (!ParameterTypesEquivalent(oldParameters[0], newParameters[0], exact: false)) - { - return false; - } + case SyntaxKind.GroupClause: + var oldGroupInfo = oldModel.GetSymbolInfo(oldNode, cancellationToken); + var newGroupInfo = newModel.GetSymbolInfo(newNode, cancellationToken); + return GroupBySignatureEquivalent(oldGroupInfo.Symbol as IMethodSymbol, newGroupInfo.Symbol as IMethodSymbol); - if (oldParameters.Length == newParameters.Length && newParameters.Length == 2) - { - return ParameterTypesEquivalent(oldParameters[1], newParameters[1], exact: false); - } + default: + return true; + } + } + private static bool GroupBySignatureEquivalent(IMethodSymbol? oldMethod, IMethodSymbol? newMethod) + { + // C# spec paragraph 7.16.2.6 "Groupby clauses": + // + // A query expression of the form + // from x in e group v by k + // is translated into + // (e).GroupBy(x => k, x => v) + // except when v is the identifier x, the translation is + // (e).GroupBy(x => k) + // + // Possible signatures: + // C> GroupBy(Func keySelector); + // C> GroupBy(Func keySelector, Func elementSelector); + + if (oldMethod == newMethod) + { return true; } - #endregion - - #region Diagnostic Info + if (oldMethod == null || newMethod == null) + { + return false; + } - protected override SymbolDisplayFormat ErrorDisplayFormat => SymbolDisplayFormat.CSharpErrorMessageFormat; + if (!TypesEquivalent(oldMethod.ReturnType, newMethod.ReturnType, exact: false)) + { + return false; + } - protected override TextSpan? TryGetDiagnosticSpan(SyntaxNode node, EditKind editKind) - => TryGetDiagnosticSpanImpl(node, editKind); + var oldParameters = oldMethod.Parameters; + var newParameters = newMethod.Parameters; - internal static new TextSpan GetDiagnosticSpan(SyntaxNode node, EditKind editKind) - => TryGetDiagnosticSpanImpl(node, editKind) ?? node.Span; + Debug.Assert(oldParameters.Length is 1 or 2); + Debug.Assert(newParameters.Length is 1 or 2); - private static TextSpan? TryGetDiagnosticSpanImpl(SyntaxNode node, EditKind editKind) - => TryGetDiagnosticSpanImpl(node.Kind(), node, editKind); + // The types of the lambdas have to be the same if present. + // The element selector may be added/removed. - // internal for testing; kind is passed explicitly for testing as well - internal static TextSpan? TryGetDiagnosticSpanImpl(SyntaxKind kind, SyntaxNode node, EditKind editKind) + if (!ParameterTypesEquivalent(oldParameters[0], newParameters[0], exact: false)) { - switch (kind) - { - case SyntaxKind.CompilationUnit: - var unit = (CompilationUnitSyntax)node; + return false; + } - // When deleting something from a compilation unit we just report diagnostics for the last global statement - var globalStatements = unit.Members.OfType(); - var globalNode = - (editKind == EditKind.Delete ? globalStatements.LastOrDefault() : globalStatements.FirstOrDefault()) ?? - unit.ChildNodes().FirstOrDefault(); + if (oldParameters.Length == newParameters.Length && newParameters.Length == 2) + { + return ParameterTypesEquivalent(oldParameters[1], newParameters[1], exact: false); + } - if (globalNode == null) - { - return null; - } + return true; + } - return GetDiagnosticSpan(globalNode, editKind); + #endregion - case SyntaxKind.GlobalStatement: - return node.Span; + #region Diagnostic Info - case SyntaxKind.ExternAliasDirective: - case SyntaxKind.UsingDirective: - return node.Span; + protected override SymbolDisplayFormat ErrorDisplayFormat => SymbolDisplayFormat.CSharpErrorMessageFormat; - case SyntaxKind.NamespaceDeclaration: - case SyntaxKind.FileScopedNamespaceDeclaration: - var ns = (BaseNamespaceDeclarationSyntax)node; - return TextSpan.FromBounds(ns.NamespaceKeyword.SpanStart, ns.Name.Span.End); + protected override TextSpan? TryGetDiagnosticSpan(SyntaxNode node, EditKind editKind) + => TryGetDiagnosticSpanImpl(node, editKind); - case SyntaxKind.ClassDeclaration: - case SyntaxKind.StructDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.RecordDeclaration: - case SyntaxKind.RecordStructDeclaration: - var typeDeclaration = (TypeDeclarationSyntax)node; - return GetDiagnosticSpan(typeDeclaration.Modifiers, typeDeclaration.Keyword, - typeDeclaration.TypeParameterList ?? (SyntaxNodeOrToken)typeDeclaration.Identifier); + internal static new TextSpan GetDiagnosticSpan(SyntaxNode node, EditKind editKind) + => TryGetDiagnosticSpanImpl(node, editKind) ?? node.Span; - case SyntaxKind.BaseList: - var baseList = (BaseListSyntax)node; - return baseList.Types.Span; + private static TextSpan? TryGetDiagnosticSpanImpl(SyntaxNode node, EditKind editKind) + => TryGetDiagnosticSpanImpl(node.Kind(), node, editKind); - case SyntaxKind.EnumDeclaration: - var enumDeclaration = (EnumDeclarationSyntax)node; - return GetDiagnosticSpan(enumDeclaration.Modifiers, enumDeclaration.EnumKeyword, enumDeclaration.Identifier); + // internal for testing; kind is passed explicitly for testing as well + internal static TextSpan? TryGetDiagnosticSpanImpl(SyntaxKind kind, SyntaxNode node, EditKind editKind) + { + switch (kind) + { + case SyntaxKind.CompilationUnit: + var unit = (CompilationUnitSyntax)node; - case SyntaxKind.DelegateDeclaration: - var delegateDeclaration = (DelegateDeclarationSyntax)node; - return GetDiagnosticSpan(delegateDeclaration.Modifiers, delegateDeclaration.DelegateKeyword, delegateDeclaration.ParameterList); + // When deleting something from a compilation unit we just report diagnostics for the last global statement + var globalStatements = unit.Members.OfType(); + var globalNode = + (editKind == EditKind.Delete ? globalStatements.LastOrDefault() : globalStatements.FirstOrDefault()) ?? + unit.ChildNodes().FirstOrDefault(); - case SyntaxKind.FieldDeclaration: - var fieldDeclaration = (BaseFieldDeclarationSyntax)node; - return GetDiagnosticSpan(fieldDeclaration.Modifiers, fieldDeclaration.Declaration, fieldDeclaration.Declaration); + if (globalNode == null) + { + return null; + } - case SyntaxKind.EventFieldDeclaration: - var eventFieldDeclaration = (EventFieldDeclarationSyntax)node; - return GetDiagnosticSpan(eventFieldDeclaration.Modifiers, eventFieldDeclaration.EventKeyword, eventFieldDeclaration.Declaration); + return GetDiagnosticSpan(globalNode, editKind); - case SyntaxKind.VariableDeclaration: + case SyntaxKind.GlobalStatement: + return node.Span; + + case SyntaxKind.ExternAliasDirective: + case SyntaxKind.UsingDirective: + return node.Span; + + case SyntaxKind.NamespaceDeclaration: + case SyntaxKind.FileScopedNamespaceDeclaration: + var ns = (BaseNamespaceDeclarationSyntax)node; + return TextSpan.FromBounds(ns.NamespaceKeyword.SpanStart, ns.Name.Span.End); + + case SyntaxKind.ClassDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.RecordStructDeclaration: + var typeDeclaration = (TypeDeclarationSyntax)node; + return GetDiagnosticSpan(typeDeclaration.Modifiers, typeDeclaration.Keyword, + typeDeclaration.TypeParameterList ?? (SyntaxNodeOrToken)typeDeclaration.Identifier); + + case SyntaxKind.BaseList: + var baseList = (BaseListSyntax)node; + return baseList.Types.Span; + + case SyntaxKind.EnumDeclaration: + var enumDeclaration = (EnumDeclarationSyntax)node; + return GetDiagnosticSpan(enumDeclaration.Modifiers, enumDeclaration.EnumKeyword, enumDeclaration.Identifier); + + case SyntaxKind.DelegateDeclaration: + var delegateDeclaration = (DelegateDeclarationSyntax)node; + return GetDiagnosticSpan(delegateDeclaration.Modifiers, delegateDeclaration.DelegateKeyword, delegateDeclaration.ParameterList); + + case SyntaxKind.FieldDeclaration: + var fieldDeclaration = (BaseFieldDeclarationSyntax)node; + return GetDiagnosticSpan(fieldDeclaration.Modifiers, fieldDeclaration.Declaration, fieldDeclaration.Declaration); + + case SyntaxKind.EventFieldDeclaration: + var eventFieldDeclaration = (EventFieldDeclarationSyntax)node; + return GetDiagnosticSpan(eventFieldDeclaration.Modifiers, eventFieldDeclaration.EventKeyword, eventFieldDeclaration.Declaration); + + case SyntaxKind.VariableDeclaration: + return TryGetDiagnosticSpanImpl(node.Parent!, editKind); + + case SyntaxKind.VariableDeclarator: + return node.Span; + + case SyntaxKind.MethodDeclaration: + var methodDeclaration = (MethodDeclarationSyntax)node; + return GetDiagnosticSpan(methodDeclaration.Modifiers, methodDeclaration.ReturnType, methodDeclaration.ParameterList); + + case SyntaxKind.ConversionOperatorDeclaration: + var conversionOperatorDeclaration = (ConversionOperatorDeclarationSyntax)node; + return GetDiagnosticSpan(conversionOperatorDeclaration.Modifiers, conversionOperatorDeclaration.ImplicitOrExplicitKeyword, conversionOperatorDeclaration.ParameterList); + + case SyntaxKind.OperatorDeclaration: + var operatorDeclaration = (OperatorDeclarationSyntax)node; + return GetDiagnosticSpan(operatorDeclaration.Modifiers, operatorDeclaration.ReturnType, operatorDeclaration.ParameterList); + + case SyntaxKind.ConstructorDeclaration: + var constructorDeclaration = (ConstructorDeclarationSyntax)node; + return GetDiagnosticSpan(constructorDeclaration.Modifiers, constructorDeclaration.Identifier, constructorDeclaration.ParameterList); + + case SyntaxKind.DestructorDeclaration: + var destructorDeclaration = (DestructorDeclarationSyntax)node; + return GetDiagnosticSpan(destructorDeclaration.Modifiers, destructorDeclaration.TildeToken, destructorDeclaration.ParameterList); + + case SyntaxKind.PropertyDeclaration: + var propertyDeclaration = (PropertyDeclarationSyntax)node; + return GetDiagnosticSpan(propertyDeclaration.Modifiers, propertyDeclaration.Type, propertyDeclaration.Identifier); + + case SyntaxKind.IndexerDeclaration: + var indexerDeclaration = (IndexerDeclarationSyntax)node; + return GetDiagnosticSpan(indexerDeclaration.Modifiers, indexerDeclaration.Type, indexerDeclaration.ParameterList); + + case SyntaxKind.EventDeclaration: + var eventDeclaration = (EventDeclarationSyntax)node; + return GetDiagnosticSpan(eventDeclaration.Modifiers, eventDeclaration.EventKeyword, eventDeclaration.Identifier); + + case SyntaxKind.EnumMemberDeclaration: + return node.Span; + + case SyntaxKind.GetAccessorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.InitAccessorDeclaration: + case SyntaxKind.AddAccessorDeclaration: + case SyntaxKind.RemoveAccessorDeclaration: + case SyntaxKind.UnknownAccessorDeclaration: + var accessorDeclaration = (AccessorDeclarationSyntax)node; + return GetDiagnosticSpan(accessorDeclaration.Modifiers, accessorDeclaration.Keyword, accessorDeclaration.Keyword); + + case SyntaxKind.TypeParameterConstraintClause: + var constraint = (TypeParameterConstraintClauseSyntax)node; + return TextSpan.FromBounds(constraint.WhereKeyword.SpanStart, constraint.Constraints.Last().Span.End); + + case SyntaxKind.TypeParameter: + var typeParameter = (TypeParameterSyntax)node; + return typeParameter.Identifier.Span; + + case SyntaxKind.AccessorList: + case SyntaxKind.TypeParameterList: + case SyntaxKind.ParameterList: + case SyntaxKind.BracketedParameterList: + if (editKind == EditKind.Delete) + { return TryGetDiagnosticSpanImpl(node.Parent!, editKind); - - case SyntaxKind.VariableDeclarator: + } + else + { return node.Span; + } - case SyntaxKind.MethodDeclaration: - var methodDeclaration = (MethodDeclarationSyntax)node; - return GetDiagnosticSpan(methodDeclaration.Modifiers, methodDeclaration.ReturnType, methodDeclaration.ParameterList); + case SyntaxKind.Parameter: + var parameter = (ParameterSyntax)node; + // Lambda parameters don't have types or modifiers, so the parameter is the only node + var startNode = parameter.Type ?? (SyntaxNode)parameter; + return GetDiagnosticSpan(parameter.Modifiers, startNode, parameter); - case SyntaxKind.ConversionOperatorDeclaration: - var conversionOperatorDeclaration = (ConversionOperatorDeclarationSyntax)node; - return GetDiagnosticSpan(conversionOperatorDeclaration.Modifiers, conversionOperatorDeclaration.ImplicitOrExplicitKeyword, conversionOperatorDeclaration.ParameterList); + case SyntaxKind.PrimaryConstructorBaseType: + case SyntaxKind.AttributeList: + case SyntaxKind.Attribute: + return node.Span; - case SyntaxKind.OperatorDeclaration: - var operatorDeclaration = (OperatorDeclarationSyntax)node; - return GetDiagnosticSpan(operatorDeclaration.Modifiers, operatorDeclaration.ReturnType, operatorDeclaration.ParameterList); + case SyntaxKind.ArrowExpressionClause: + return TryGetDiagnosticSpanImpl(node.Parent!, editKind); - case SyntaxKind.ConstructorDeclaration: - var constructorDeclaration = (ConstructorDeclarationSyntax)node; - return GetDiagnosticSpan(constructorDeclaration.Modifiers, constructorDeclaration.Identifier, constructorDeclaration.ParameterList); + // We only need a diagnostic span if reporting an error for a child statement. + // The following statements may have child statements. - case SyntaxKind.DestructorDeclaration: - var destructorDeclaration = (DestructorDeclarationSyntax)node; - return GetDiagnosticSpan(destructorDeclaration.Modifiers, destructorDeclaration.TildeToken, destructorDeclaration.ParameterList); + case SyntaxKind.Block: + return ((BlockSyntax)node).OpenBraceToken.Span; - case SyntaxKind.PropertyDeclaration: - var propertyDeclaration = (PropertyDeclarationSyntax)node; - return GetDiagnosticSpan(propertyDeclaration.Modifiers, propertyDeclaration.Type, propertyDeclaration.Identifier); + case SyntaxKind.UsingStatement: + var usingStatement = (UsingStatementSyntax)node; + return TextSpan.FromBounds(usingStatement.UsingKeyword.SpanStart, usingStatement.CloseParenToken.Span.End); - case SyntaxKind.IndexerDeclaration: - var indexerDeclaration = (IndexerDeclarationSyntax)node; - return GetDiagnosticSpan(indexerDeclaration.Modifiers, indexerDeclaration.Type, indexerDeclaration.ParameterList); + case SyntaxKind.FixedStatement: + var fixedStatement = (FixedStatementSyntax)node; + return TextSpan.FromBounds(fixedStatement.FixedKeyword.SpanStart, fixedStatement.CloseParenToken.Span.End); - case SyntaxKind.EventDeclaration: - var eventDeclaration = (EventDeclarationSyntax)node; - return GetDiagnosticSpan(eventDeclaration.Modifiers, eventDeclaration.EventKeyword, eventDeclaration.Identifier); + case SyntaxKind.LockStatement: + var lockStatement = (LockStatementSyntax)node; + return TextSpan.FromBounds(lockStatement.LockKeyword.SpanStart, lockStatement.CloseParenToken.Span.End); - case SyntaxKind.EnumMemberDeclaration: - return node.Span; + case SyntaxKind.StackAllocArrayCreationExpression: + return ((StackAllocArrayCreationExpressionSyntax)node).StackAllocKeyword.Span; - case SyntaxKind.GetAccessorDeclaration: - case SyntaxKind.SetAccessorDeclaration: - case SyntaxKind.InitAccessorDeclaration: - case SyntaxKind.AddAccessorDeclaration: - case SyntaxKind.RemoveAccessorDeclaration: - case SyntaxKind.UnknownAccessorDeclaration: - var accessorDeclaration = (AccessorDeclarationSyntax)node; - return GetDiagnosticSpan(accessorDeclaration.Modifiers, accessorDeclaration.Keyword, accessorDeclaration.Keyword); + case SyntaxKind.ImplicitStackAllocArrayCreationExpression: + return ((ImplicitStackAllocArrayCreationExpressionSyntax)node).StackAllocKeyword.Span; - case SyntaxKind.TypeParameterConstraintClause: - var constraint = (TypeParameterConstraintClauseSyntax)node; - return TextSpan.FromBounds(constraint.WhereKeyword.SpanStart, constraint.Constraints.Last().Span.End); + case SyntaxKind.TryStatement: + return ((TryStatementSyntax)node).TryKeyword.Span; - case SyntaxKind.TypeParameter: - var typeParameter = (TypeParameterSyntax)node; - return typeParameter.Identifier.Span; - - case SyntaxKind.AccessorList: - case SyntaxKind.TypeParameterList: - case SyntaxKind.ParameterList: - case SyntaxKind.BracketedParameterList: - if (editKind == EditKind.Delete) - { - return TryGetDiagnosticSpanImpl(node.Parent!, editKind); - } - else - { - return node.Span; - } + case SyntaxKind.CatchClause: + return ((CatchClauseSyntax)node).CatchKeyword.Span; - case SyntaxKind.Parameter: - var parameter = (ParameterSyntax)node; - // Lambda parameters don't have types or modifiers, so the parameter is the only node - var startNode = parameter.Type ?? (SyntaxNode)parameter; - return GetDiagnosticSpan(parameter.Modifiers, startNode, parameter); + case SyntaxKind.CatchDeclaration: + case SyntaxKind.CatchFilterClause: + return node.Span; - case SyntaxKind.PrimaryConstructorBaseType: - case SyntaxKind.AttributeList: - case SyntaxKind.Attribute: - return node.Span; + case SyntaxKind.FinallyClause: + return ((FinallyClauseSyntax)node).FinallyKeyword.Span; - case SyntaxKind.ArrowExpressionClause: - return TryGetDiagnosticSpanImpl(node.Parent!, editKind); + case SyntaxKind.IfStatement: + var ifStatement = (IfStatementSyntax)node; + return TextSpan.FromBounds(ifStatement.IfKeyword.SpanStart, ifStatement.CloseParenToken.Span.End); - // We only need a diagnostic span if reporting an error for a child statement. - // The following statements may have child statements. + case SyntaxKind.ElseClause: + return ((ElseClauseSyntax)node).ElseKeyword.Span; - case SyntaxKind.Block: - return ((BlockSyntax)node).OpenBraceToken.Span; + case SyntaxKind.SwitchStatement: + var switchStatement = (SwitchStatementSyntax)node; + return TextSpan.FromBounds(switchStatement.SwitchKeyword.SpanStart, + (switchStatement.CloseParenToken != default) ? switchStatement.CloseParenToken.Span.End : switchStatement.Expression.Span.End); - case SyntaxKind.UsingStatement: - var usingStatement = (UsingStatementSyntax)node; - return TextSpan.FromBounds(usingStatement.UsingKeyword.SpanStart, usingStatement.CloseParenToken.Span.End); + case SyntaxKind.SwitchSection: + return ((SwitchSectionSyntax)node).Labels.Last().Span; - case SyntaxKind.FixedStatement: - var fixedStatement = (FixedStatementSyntax)node; - return TextSpan.FromBounds(fixedStatement.FixedKeyword.SpanStart, fixedStatement.CloseParenToken.Span.End); + case SyntaxKind.WhileStatement: + var whileStatement = (WhileStatementSyntax)node; + return TextSpan.FromBounds(whileStatement.WhileKeyword.SpanStart, whileStatement.CloseParenToken.Span.End); - case SyntaxKind.LockStatement: - var lockStatement = (LockStatementSyntax)node; - return TextSpan.FromBounds(lockStatement.LockKeyword.SpanStart, lockStatement.CloseParenToken.Span.End); + case SyntaxKind.DoStatement: + return ((DoStatementSyntax)node).DoKeyword.Span; - case SyntaxKind.StackAllocArrayCreationExpression: - return ((StackAllocArrayCreationExpressionSyntax)node).StackAllocKeyword.Span; + case SyntaxKind.ForStatement: + var forStatement = (ForStatementSyntax)node; + return TextSpan.FromBounds(forStatement.ForKeyword.SpanStart, forStatement.CloseParenToken.Span.End); - case SyntaxKind.ImplicitStackAllocArrayCreationExpression: - return ((ImplicitStackAllocArrayCreationExpressionSyntax)node).StackAllocKeyword.Span; + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + var commonForEachStatement = (CommonForEachStatementSyntax)node; + return TextSpan.FromBounds( + (commonForEachStatement.AwaitKeyword.Span.Length > 0) ? commonForEachStatement.AwaitKeyword.SpanStart : commonForEachStatement.ForEachKeyword.SpanStart, + commonForEachStatement.CloseParenToken.Span.End); - case SyntaxKind.TryStatement: - return ((TryStatementSyntax)node).TryKeyword.Span; + case SyntaxKind.LabeledStatement: + return ((LabeledStatementSyntax)node).Identifier.Span; - case SyntaxKind.CatchClause: - return ((CatchClauseSyntax)node).CatchKeyword.Span; + case SyntaxKind.CheckedStatement: + case SyntaxKind.UncheckedStatement: + return ((CheckedStatementSyntax)node).Keyword.Span; - case SyntaxKind.CatchDeclaration: - case SyntaxKind.CatchFilterClause: - return node.Span; + case SyntaxKind.UnsafeStatement: + return ((UnsafeStatementSyntax)node).UnsafeKeyword.Span; - case SyntaxKind.FinallyClause: - return ((FinallyClauseSyntax)node).FinallyKeyword.Span; + case SyntaxKind.LocalFunctionStatement: + var lfd = (LocalFunctionStatementSyntax)node; + return lfd.Identifier.Span; - case SyntaxKind.IfStatement: - var ifStatement = (IfStatementSyntax)node; - return TextSpan.FromBounds(ifStatement.IfKeyword.SpanStart, ifStatement.CloseParenToken.Span.End); + case SyntaxKind.YieldBreakStatement: + case SyntaxKind.YieldReturnStatement: + case SyntaxKind.ReturnStatement: + case SyntaxKind.ThrowStatement: + case SyntaxKind.ExpressionStatement: + case SyntaxKind.EmptyStatement: + case SyntaxKind.GotoStatement: + case SyntaxKind.GotoCaseStatement: + case SyntaxKind.GotoDefaultStatement: + case SyntaxKind.BreakStatement: + case SyntaxKind.ContinueStatement: + return node.Span; - case SyntaxKind.ElseClause: - return ((ElseClauseSyntax)node).ElseKeyword.Span; + case SyntaxKind.LocalDeclarationStatement: + var localDeclarationStatement = (LocalDeclarationStatementSyntax)node; + return CombineSpans(localDeclarationStatement.AwaitKeyword.Span, localDeclarationStatement.UsingKeyword.Span, node.Span); - case SyntaxKind.SwitchStatement: - var switchStatement = (SwitchStatementSyntax)node; - return TextSpan.FromBounds(switchStatement.SwitchKeyword.SpanStart, - (switchStatement.CloseParenToken != default) ? switchStatement.CloseParenToken.Span.End : switchStatement.Expression.Span.End); + case SyntaxKind.AwaitExpression: + return ((AwaitExpressionSyntax)node).AwaitKeyword.Span; - case SyntaxKind.SwitchSection: - return ((SwitchSectionSyntax)node).Labels.Last().Span; + case SyntaxKind.AnonymousObjectCreationExpression: + return ((AnonymousObjectCreationExpressionSyntax)node).NewKeyword.Span; - case SyntaxKind.WhileStatement: - var whileStatement = (WhileStatementSyntax)node; - return TextSpan.FromBounds(whileStatement.WhileKeyword.SpanStart, whileStatement.CloseParenToken.Span.End); + case SyntaxKind.ParenthesizedLambdaExpression: + return ((ParenthesizedLambdaExpressionSyntax)node).ParameterList.Span; - case SyntaxKind.DoStatement: - return ((DoStatementSyntax)node).DoKeyword.Span; + case SyntaxKind.SimpleLambdaExpression: + return ((SimpleLambdaExpressionSyntax)node).Parameter.Span; - case SyntaxKind.ForStatement: - var forStatement = (ForStatementSyntax)node; - return TextSpan.FromBounds(forStatement.ForKeyword.SpanStart, forStatement.CloseParenToken.Span.End); + case SyntaxKind.AnonymousMethodExpression: + return ((AnonymousMethodExpressionSyntax)node).DelegateKeyword.Span; - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - var commonForEachStatement = (CommonForEachStatementSyntax)node; - return TextSpan.FromBounds( - (commonForEachStatement.AwaitKeyword.Span.Length > 0) ? commonForEachStatement.AwaitKeyword.SpanStart : commonForEachStatement.ForEachKeyword.SpanStart, - commonForEachStatement.CloseParenToken.Span.End); + case SyntaxKind.QueryExpression: + return ((QueryExpressionSyntax)node).FromClause.FromKeyword.Span; - case SyntaxKind.LabeledStatement: - return ((LabeledStatementSyntax)node).Identifier.Span; + case SyntaxKind.QueryBody: + var queryBody = (QueryBodySyntax)node; + return TryGetDiagnosticSpanImpl(queryBody.Clauses.FirstOrDefault() ?? queryBody.Parent!, editKind); - case SyntaxKind.CheckedStatement: - case SyntaxKind.UncheckedStatement: - return ((CheckedStatementSyntax)node).Keyword.Span; - - case SyntaxKind.UnsafeStatement: - return ((UnsafeStatementSyntax)node).UnsafeKeyword.Span; - - case SyntaxKind.LocalFunctionStatement: - var lfd = (LocalFunctionStatementSyntax)node; - return lfd.Identifier.Span; - - case SyntaxKind.YieldBreakStatement: - case SyntaxKind.YieldReturnStatement: - case SyntaxKind.ReturnStatement: - case SyntaxKind.ThrowStatement: - case SyntaxKind.ExpressionStatement: - case SyntaxKind.EmptyStatement: - case SyntaxKind.GotoStatement: - case SyntaxKind.GotoCaseStatement: - case SyntaxKind.GotoDefaultStatement: - case SyntaxKind.BreakStatement: - case SyntaxKind.ContinueStatement: - return node.Span; + case SyntaxKind.QueryContinuation: + return ((QueryContinuationSyntax)node).IntoKeyword.Span; - case SyntaxKind.LocalDeclarationStatement: - var localDeclarationStatement = (LocalDeclarationStatementSyntax)node; - return CombineSpans(localDeclarationStatement.AwaitKeyword.Span, localDeclarationStatement.UsingKeyword.Span, node.Span); + case SyntaxKind.FromClause: + return ((FromClauseSyntax)node).FromKeyword.Span; - case SyntaxKind.AwaitExpression: - return ((AwaitExpressionSyntax)node).AwaitKeyword.Span; + case SyntaxKind.JoinClause: + return ((JoinClauseSyntax)node).JoinKeyword.Span; - case SyntaxKind.AnonymousObjectCreationExpression: - return ((AnonymousObjectCreationExpressionSyntax)node).NewKeyword.Span; + case SyntaxKind.JoinIntoClause: + return ((JoinIntoClauseSyntax)node).IntoKeyword.Span; - case SyntaxKind.ParenthesizedLambdaExpression: - return ((ParenthesizedLambdaExpressionSyntax)node).ParameterList.Span; + case SyntaxKind.LetClause: + return ((LetClauseSyntax)node).LetKeyword.Span; - case SyntaxKind.SimpleLambdaExpression: - return ((SimpleLambdaExpressionSyntax)node).Parameter.Span; + case SyntaxKind.WhereClause: + return ((WhereClauseSyntax)node).WhereKeyword.Span; - case SyntaxKind.AnonymousMethodExpression: - return ((AnonymousMethodExpressionSyntax)node).DelegateKeyword.Span; + case SyntaxKind.OrderByClause: + return ((OrderByClauseSyntax)node).OrderByKeyword.Span; - case SyntaxKind.QueryExpression: - return ((QueryExpressionSyntax)node).FromClause.FromKeyword.Span; + case SyntaxKind.AscendingOrdering: + case SyntaxKind.DescendingOrdering: + return node.Span; - case SyntaxKind.QueryBody: - var queryBody = (QueryBodySyntax)node; - return TryGetDiagnosticSpanImpl(queryBody.Clauses.FirstOrDefault() ?? queryBody.Parent!, editKind); + case SyntaxKind.SelectClause: + return ((SelectClauseSyntax)node).SelectKeyword.Span; - case SyntaxKind.QueryContinuation: - return ((QueryContinuationSyntax)node).IntoKeyword.Span; + case SyntaxKind.GroupClause: + return ((GroupClauseSyntax)node).GroupKeyword.Span; - case SyntaxKind.FromClause: - return ((FromClauseSyntax)node).FromKeyword.Span; + case SyntaxKind.IsPatternExpression: + case SyntaxKind.TupleType: + case SyntaxKind.TupleExpression: + case SyntaxKind.DeclarationExpression: + case SyntaxKind.RefType: + case SyntaxKind.RefExpression: + case SyntaxKind.DeclarationPattern: + case SyntaxKind.SimpleAssignmentExpression: + case SyntaxKind.WhenClause: + case SyntaxKind.SingleVariableDesignation: + case SyntaxKind.CasePatternSwitchLabel: + return node.Span; - case SyntaxKind.JoinClause: - return ((JoinClauseSyntax)node).JoinKeyword.Span; + case SyntaxKind.SwitchExpression: + return ((SwitchExpressionSyntax)node).SwitchKeyword.Span; - case SyntaxKind.JoinIntoClause: - return ((JoinIntoClauseSyntax)node).IntoKeyword.Span; + case SyntaxKind.SwitchExpressionArm: + return ((SwitchExpressionArmSyntax)node).EqualsGreaterThanToken.Span; - case SyntaxKind.LetClause: - return ((LetClauseSyntax)node).LetKeyword.Span; + default: + return null; + } + } - case SyntaxKind.WhereClause: - return ((WhereClauseSyntax)node).WhereKeyword.Span; + private static TextSpan GetDiagnosticSpan(SyntaxTokenList modifiers, SyntaxNodeOrToken start, SyntaxNodeOrToken end) + => TextSpan.FromBounds((modifiers.Count != 0) ? modifiers.First().SpanStart : start.SpanStart, end.Span.End); - case SyntaxKind.OrderByClause: - return ((OrderByClauseSyntax)node).OrderByKeyword.Span; + private static TextSpan CombineSpans(TextSpan first, TextSpan second, TextSpan defaultSpan) + => (first.Length > 0 && second.Length > 0) ? TextSpan.FromBounds(first.Start, second.End) : (first.Length > 0) ? first : (second.Length > 0) ? second : defaultSpan; - case SyntaxKind.AscendingOrdering: - case SyntaxKind.DescendingOrdering: - return node.Span; + internal override TextSpan GetLambdaParameterDiagnosticSpan(SyntaxNode lambda, int ordinal) + { + Debug.Assert(ordinal >= 0); - case SyntaxKind.SelectClause: - return ((SelectClauseSyntax)node).SelectKeyword.Span; - - case SyntaxKind.GroupClause: - return ((GroupClauseSyntax)node).GroupKeyword.Span; - - case SyntaxKind.IsPatternExpression: - case SyntaxKind.TupleType: - case SyntaxKind.TupleExpression: - case SyntaxKind.DeclarationExpression: - case SyntaxKind.RefType: - case SyntaxKind.RefExpression: - case SyntaxKind.DeclarationPattern: - case SyntaxKind.SimpleAssignmentExpression: - case SyntaxKind.WhenClause: - case SyntaxKind.SingleVariableDesignation: - case SyntaxKind.CasePatternSwitchLabel: - return node.Span; + switch (lambda.Kind()) + { + case SyntaxKind.ParenthesizedLambdaExpression: + return ((ParenthesizedLambdaExpressionSyntax)lambda).ParameterList.Parameters[ordinal].Identifier.Span; - case SyntaxKind.SwitchExpression: - return ((SwitchExpressionSyntax)node).SwitchKeyword.Span; + case SyntaxKind.SimpleLambdaExpression: + Debug.Assert(ordinal == 0); + return ((SimpleLambdaExpressionSyntax)lambda).Parameter.Identifier.Span; - case SyntaxKind.SwitchExpressionArm: - return ((SwitchExpressionArmSyntax)node).EqualsGreaterThanToken.Span; + case SyntaxKind.AnonymousMethodExpression: + // since we are given a parameter ordinal there has to be a parameter list: + return ((AnonymousMethodExpressionSyntax)lambda).ParameterList!.Parameters[ordinal].Identifier.Span; - default: - return null; - } + default: + return lambda.Span; } + } + + internal override string GetDisplayName(INamedTypeSymbol symbol) + => symbol.TypeKind switch + { + TypeKind.Struct => symbol.IsRecord ? CSharpFeaturesResources.record_struct : CSharpFeaturesResources.struct_, + TypeKind.Class => symbol.IsRecord ? CSharpFeaturesResources.record_ : FeaturesResources.class_, + _ => base.GetDisplayName(symbol) + }; - private static TextSpan GetDiagnosticSpan(SyntaxTokenList modifiers, SyntaxNodeOrToken start, SyntaxNodeOrToken end) - => TextSpan.FromBounds((modifiers.Count != 0) ? modifiers.First().SpanStart : start.SpanStart, end.Span.End); + internal override string GetDisplayName(IEventSymbol symbol) + => symbol.AddMethod?.IsImplicitlyDeclared != false ? CSharpFeaturesResources.event_field : base.GetDisplayName(symbol); - private static TextSpan CombineSpans(TextSpan first, TextSpan second, TextSpan defaultSpan) - => (first.Length > 0 && second.Length > 0) ? TextSpan.FromBounds(first.Start, second.End) : (first.Length > 0) ? first : (second.Length > 0) ? second : defaultSpan; + internal override string GetDisplayName(IPropertySymbol symbol) + => symbol.IsIndexer ? CSharpFeaturesResources.indexer : base.GetDisplayName(symbol); - internal override TextSpan GetLambdaParameterDiagnosticSpan(SyntaxNode lambda, int ordinal) + internal override string GetDisplayName(IMethodSymbol symbol) + => symbol.MethodKind switch + { + MethodKind.PropertyGet => symbol.AssociatedSymbol is IPropertySymbol { IsIndexer: true } ? CSharpFeaturesResources.indexer_getter : CSharpFeaturesResources.property_getter, + MethodKind.PropertySet => symbol.AssociatedSymbol is IPropertySymbol { IsIndexer: true } ? CSharpFeaturesResources.indexer_setter : CSharpFeaturesResources.property_setter, + MethodKind.StaticConstructor => FeaturesResources.static_constructor, + MethodKind.Destructor => CSharpFeaturesResources.destructor, + MethodKind.Conversion => CSharpFeaturesResources.conversion_operator, + MethodKind.LocalFunction => FeaturesResources.local_function, + MethodKind.LambdaMethod => CSharpFeaturesResources.lambda, + MethodKind.Ordinary when symbol.Name == WellKnownMemberNames.TopLevelStatementsEntryPointMethodName => CSharpFeaturesResources.top_level_code, + _ => base.GetDisplayName(symbol) + }; + + protected override string? TryGetDisplayName(SyntaxNode node, EditKind editKind) + => TryGetDisplayNameImpl(node, editKind); + + internal static new string? GetDisplayName(SyntaxNode node, EditKind editKind) + => TryGetDisplayNameImpl(node, editKind) ?? throw ExceptionUtilities.UnexpectedValue(node.Kind()); + + internal static string? TryGetDisplayNameImpl(SyntaxNode node, EditKind editKind) + { + switch (node.Kind()) { - Debug.Assert(ordinal >= 0); + // top-level - switch (lambda.Kind()) - { - case SyntaxKind.ParenthesizedLambdaExpression: - return ((ParenthesizedLambdaExpressionSyntax)lambda).ParameterList.Parameters[ordinal].Identifier.Span; + case SyntaxKind.CompilationUnit: + return CSharpFeaturesResources.top_level_code; - case SyntaxKind.SimpleLambdaExpression: - Debug.Assert(ordinal == 0); - return ((SimpleLambdaExpressionSyntax)lambda).Parameter.Identifier.Span; + case SyntaxKind.GlobalStatement: + return CSharpFeaturesResources.top_level_statement; - case SyntaxKind.AnonymousMethodExpression: - // since we are given a parameter ordinal there has to be a parameter list: - return ((AnonymousMethodExpressionSyntax)lambda).ParameterList!.Parameters[ordinal].Identifier.Span; + case SyntaxKind.ExternAliasDirective: + return CSharpFeaturesResources.extern_alias; - default: - return lambda.Span; - } - } + case SyntaxKind.UsingDirective: + // Dev12 distinguishes using alias from using namespace and reports different errors for removing alias. + // None of these changes are allowed anyways, so let's keep it simple. + return CSharpFeaturesResources.using_directive; - internal override string GetDisplayName(INamedTypeSymbol symbol) - => symbol.TypeKind switch - { - TypeKind.Struct => symbol.IsRecord ? CSharpFeaturesResources.record_struct : CSharpFeaturesResources.struct_, - TypeKind.Class => symbol.IsRecord ? CSharpFeaturesResources.record_ : FeaturesResources.class_, - _ => base.GetDisplayName(symbol) - }; + case SyntaxKind.NamespaceDeclaration: + case SyntaxKind.FileScopedNamespaceDeclaration: + return FeaturesResources.namespace_; - internal override string GetDisplayName(IEventSymbol symbol) - => symbol.AddMethod?.IsImplicitlyDeclared != false ? CSharpFeaturesResources.event_field : base.GetDisplayName(symbol); + case SyntaxKind.ClassDeclaration: + return FeaturesResources.class_; - internal override string GetDisplayName(IPropertySymbol symbol) - => symbol.IsIndexer ? CSharpFeaturesResources.indexer : base.GetDisplayName(symbol); + case SyntaxKind.StructDeclaration: + return CSharpFeaturesResources.struct_; - internal override string GetDisplayName(IMethodSymbol symbol) - => symbol.MethodKind switch - { - MethodKind.PropertyGet => symbol.AssociatedSymbol is IPropertySymbol { IsIndexer: true } ? CSharpFeaturesResources.indexer_getter : CSharpFeaturesResources.property_getter, - MethodKind.PropertySet => symbol.AssociatedSymbol is IPropertySymbol { IsIndexer: true } ? CSharpFeaturesResources.indexer_setter : CSharpFeaturesResources.property_setter, - MethodKind.StaticConstructor => FeaturesResources.static_constructor, - MethodKind.Destructor => CSharpFeaturesResources.destructor, - MethodKind.Conversion => CSharpFeaturesResources.conversion_operator, - MethodKind.LocalFunction => FeaturesResources.local_function, - MethodKind.LambdaMethod => CSharpFeaturesResources.lambda, - MethodKind.Ordinary when symbol.Name == WellKnownMemberNames.TopLevelStatementsEntryPointMethodName => CSharpFeaturesResources.top_level_code, - _ => base.GetDisplayName(symbol) - }; + case SyntaxKind.InterfaceDeclaration: + return FeaturesResources.interface_; - protected override string? TryGetDisplayName(SyntaxNode node, EditKind editKind) - => TryGetDisplayNameImpl(node, editKind); + case SyntaxKind.RecordDeclaration: + return CSharpFeaturesResources.record_; - internal static new string? GetDisplayName(SyntaxNode node, EditKind editKind) - => TryGetDisplayNameImpl(node, editKind) ?? throw ExceptionUtilities.UnexpectedValue(node.Kind()); + case SyntaxKind.RecordStructDeclaration: + return CSharpFeaturesResources.record_struct; - internal static string? TryGetDisplayNameImpl(SyntaxNode node, EditKind editKind) - { - switch (node.Kind()) - { - // top-level + case SyntaxKind.EnumDeclaration: + return FeaturesResources.enum_; - case SyntaxKind.CompilationUnit: - return CSharpFeaturesResources.top_level_code; + case SyntaxKind.DelegateDeclaration: + return FeaturesResources.delegate_; - case SyntaxKind.GlobalStatement: - return CSharpFeaturesResources.top_level_statement; + case SyntaxKind.FieldDeclaration: + var declaration = (FieldDeclarationSyntax)node; + return declaration.Modifiers.Any(SyntaxKind.ConstKeyword) ? FeaturesResources.const_field : FeaturesResources.field; - case SyntaxKind.ExternAliasDirective: - return CSharpFeaturesResources.extern_alias; + case SyntaxKind.EventFieldDeclaration: + return CSharpFeaturesResources.event_field; - case SyntaxKind.UsingDirective: - // Dev12 distinguishes using alias from using namespace and reports different errors for removing alias. - // None of these changes are allowed anyways, so let's keep it simple. - return CSharpFeaturesResources.using_directive; + case SyntaxKind.VariableDeclaration: + case SyntaxKind.VariableDeclarator: + return TryGetDisplayNameImpl(node.Parent!, editKind); - case SyntaxKind.NamespaceDeclaration: - case SyntaxKind.FileScopedNamespaceDeclaration: - return FeaturesResources.namespace_; + case SyntaxKind.MethodDeclaration: + return FeaturesResources.method; - case SyntaxKind.ClassDeclaration: - return FeaturesResources.class_; + case SyntaxKind.ConversionOperatorDeclaration: + return CSharpFeaturesResources.conversion_operator; - case SyntaxKind.StructDeclaration: - return CSharpFeaturesResources.struct_; + case SyntaxKind.OperatorDeclaration: + return FeaturesResources.operator_; - case SyntaxKind.InterfaceDeclaration: - return FeaturesResources.interface_; + case SyntaxKind.ConstructorDeclaration: + var ctor = (ConstructorDeclarationSyntax)node; + return ctor.Modifiers.Any(SyntaxKind.StaticKeyword) ? FeaturesResources.static_constructor : FeaturesResources.constructor; - case SyntaxKind.RecordDeclaration: - return CSharpFeaturesResources.record_; + case SyntaxKind.DestructorDeclaration: + return CSharpFeaturesResources.destructor; - case SyntaxKind.RecordStructDeclaration: - return CSharpFeaturesResources.record_struct; + case SyntaxKind.PropertyDeclaration: + return SyntaxUtilities.HasBackingField((PropertyDeclarationSyntax)node) ? FeaturesResources.auto_property : FeaturesResources.property_; - case SyntaxKind.EnumDeclaration: - return FeaturesResources.enum_; + case SyntaxKind.IndexerDeclaration: + return CSharpFeaturesResources.indexer; - case SyntaxKind.DelegateDeclaration: - return FeaturesResources.delegate_; + case SyntaxKind.EventDeclaration: + return FeaturesResources.event_; - case SyntaxKind.FieldDeclaration: - var declaration = (FieldDeclarationSyntax)node; - return declaration.Modifiers.Any(SyntaxKind.ConstKeyword) ? FeaturesResources.const_field : FeaturesResources.field; + case SyntaxKind.EnumMemberDeclaration: + return FeaturesResources.enum_value; - case SyntaxKind.EventFieldDeclaration: - return CSharpFeaturesResources.event_field; + case SyntaxKind.GetAccessorDeclaration: + if (node.Parent!.Parent!.IsKind(SyntaxKind.PropertyDeclaration)) + { + return CSharpFeaturesResources.property_getter; + } + else + { + RoslynDebug.Assert(node.Parent.Parent.IsKind(SyntaxKind.IndexerDeclaration)); + return CSharpFeaturesResources.indexer_getter; + } - case SyntaxKind.VariableDeclaration: - case SyntaxKind.VariableDeclarator: - return TryGetDisplayNameImpl(node.Parent!, editKind); + case SyntaxKind.InitAccessorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + if (node.Parent!.Parent!.IsKind(SyntaxKind.PropertyDeclaration)) + { + return CSharpFeaturesResources.property_setter; + } + else + { + RoslynDebug.Assert(node.Parent.Parent.IsKind(SyntaxKind.IndexerDeclaration)); + return CSharpFeaturesResources.indexer_setter; + } - case SyntaxKind.MethodDeclaration: - return FeaturesResources.method; + case SyntaxKind.AddAccessorDeclaration: + case SyntaxKind.RemoveAccessorDeclaration: + return FeaturesResources.event_accessor; - case SyntaxKind.ConversionOperatorDeclaration: - return CSharpFeaturesResources.conversion_operator; + case SyntaxKind.ArrowExpressionClause: + return node.Parent!.Kind() switch + { + SyntaxKind.PropertyDeclaration => CSharpFeaturesResources.property_getter, + SyntaxKind.IndexerDeclaration => CSharpFeaturesResources.indexer_getter, + _ => null + }; - case SyntaxKind.OperatorDeclaration: - return FeaturesResources.operator_; + case SyntaxKind.TypeParameterConstraintClause: + return FeaturesResources.type_constraint; - case SyntaxKind.ConstructorDeclaration: - var ctor = (ConstructorDeclarationSyntax)node; - return ctor.Modifiers.Any(SyntaxKind.StaticKeyword) ? FeaturesResources.static_constructor : FeaturesResources.constructor; + case SyntaxKind.TypeParameterList: + case SyntaxKind.TypeParameter: + return FeaturesResources.type_parameter; - case SyntaxKind.DestructorDeclaration: - return CSharpFeaturesResources.destructor; + case SyntaxKind.Parameter: + return FeaturesResources.parameter; - case SyntaxKind.PropertyDeclaration: - return SyntaxUtilities.HasBackingField((PropertyDeclarationSyntax)node) ? FeaturesResources.auto_property : FeaturesResources.property_; + case SyntaxKind.ParameterList: + return node.Parent is TypeDeclarationSyntax ? FeaturesResources.constructor : null; - case SyntaxKind.IndexerDeclaration: - return CSharpFeaturesResources.indexer; + case SyntaxKind.AttributeList: + return FeaturesResources.attribute; - case SyntaxKind.EventDeclaration: - return FeaturesResources.event_; + case SyntaxKind.Attribute: + return FeaturesResources.attribute; - case SyntaxKind.EnumMemberDeclaration: - return FeaturesResources.enum_value; + case SyntaxKind.AttributeTargetSpecifier: + return CSharpFeaturesResources.attribute_target; - case SyntaxKind.GetAccessorDeclaration: - if (node.Parent!.Parent!.IsKind(SyntaxKind.PropertyDeclaration)) - { - return CSharpFeaturesResources.property_getter; - } - else - { - RoslynDebug.Assert(node.Parent.Parent.IsKind(SyntaxKind.IndexerDeclaration)); - return CSharpFeaturesResources.indexer_getter; - } + // statement: - case SyntaxKind.InitAccessorDeclaration: - case SyntaxKind.SetAccessorDeclaration: - if (node.Parent!.Parent!.IsKind(SyntaxKind.PropertyDeclaration)) - { - return CSharpFeaturesResources.property_setter; - } - else - { - RoslynDebug.Assert(node.Parent.Parent.IsKind(SyntaxKind.IndexerDeclaration)); - return CSharpFeaturesResources.indexer_setter; - } + case SyntaxKind.TryStatement: + return CSharpFeaturesResources.try_block; - case SyntaxKind.AddAccessorDeclaration: - case SyntaxKind.RemoveAccessorDeclaration: - return FeaturesResources.event_accessor; + case SyntaxKind.CatchClause: + case SyntaxKind.CatchDeclaration: + return CSharpFeaturesResources.catch_clause; - case SyntaxKind.ArrowExpressionClause: - return node.Parent!.Kind() switch - { - SyntaxKind.PropertyDeclaration => CSharpFeaturesResources.property_getter, - SyntaxKind.IndexerDeclaration => CSharpFeaturesResources.indexer_getter, - _ => null - }; + case SyntaxKind.CatchFilterClause: + return CSharpFeaturesResources.filter_clause; - case SyntaxKind.TypeParameterConstraintClause: - return FeaturesResources.type_constraint; + case SyntaxKind.FinallyClause: + return CSharpFeaturesResources.finally_clause; - case SyntaxKind.TypeParameterList: - case SyntaxKind.TypeParameter: - return FeaturesResources.type_parameter; + case SyntaxKind.FixedStatement: + return CSharpFeaturesResources.fixed_statement; - case SyntaxKind.Parameter: - return FeaturesResources.parameter; + case SyntaxKind.UsingStatement: + return CSharpFeaturesResources.using_statement; - case SyntaxKind.ParameterList: - return node.Parent is TypeDeclarationSyntax ? FeaturesResources.constructor : null; + case SyntaxKind.LockStatement: + return CSharpFeaturesResources.lock_statement; - case SyntaxKind.AttributeList: - return FeaturesResources.attribute; + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + return CSharpFeaturesResources.foreach_statement; - case SyntaxKind.Attribute: - return FeaturesResources.attribute; + case SyntaxKind.CheckedStatement: + return CSharpFeaturesResources.checked_statement; - case SyntaxKind.AttributeTargetSpecifier: - return CSharpFeaturesResources.attribute_target; + case SyntaxKind.UncheckedStatement: + return CSharpFeaturesResources.unchecked_statement; - // statement: + case SyntaxKind.YieldBreakStatement: + return CSharpFeaturesResources.yield_break_statement; - case SyntaxKind.TryStatement: - return CSharpFeaturesResources.try_block; + case SyntaxKind.YieldReturnStatement: + return CSharpFeaturesResources.yield_return_statement; - case SyntaxKind.CatchClause: - case SyntaxKind.CatchDeclaration: - return CSharpFeaturesResources.catch_clause; + case SyntaxKind.AwaitExpression: + return CSharpFeaturesResources.await_expression; - case SyntaxKind.CatchFilterClause: - return CSharpFeaturesResources.filter_clause; + case SyntaxKind.ParenthesizedLambdaExpression: + case SyntaxKind.SimpleLambdaExpression: + return CSharpFeaturesResources.lambda; - case SyntaxKind.FinallyClause: - return CSharpFeaturesResources.finally_clause; + case SyntaxKind.AnonymousMethodExpression: + return CSharpFeaturesResources.anonymous_method; - case SyntaxKind.FixedStatement: - return CSharpFeaturesResources.fixed_statement; + case SyntaxKind.FromClause: + return CSharpFeaturesResources.from_clause; - case SyntaxKind.UsingStatement: - return CSharpFeaturesResources.using_statement; + case SyntaxKind.JoinClause: + case SyntaxKind.JoinIntoClause: + return CSharpFeaturesResources.join_clause; - case SyntaxKind.LockStatement: - return CSharpFeaturesResources.lock_statement; + case SyntaxKind.LetClause: + return CSharpFeaturesResources.let_clause; - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - return CSharpFeaturesResources.foreach_statement; + case SyntaxKind.WhereClause: + return CSharpFeaturesResources.where_clause; - case SyntaxKind.CheckedStatement: - return CSharpFeaturesResources.checked_statement; + case SyntaxKind.OrderByClause: + case SyntaxKind.AscendingOrdering: + case SyntaxKind.DescendingOrdering: + return CSharpFeaturesResources.orderby_clause; - case SyntaxKind.UncheckedStatement: - return CSharpFeaturesResources.unchecked_statement; + case SyntaxKind.SelectClause: + return CSharpFeaturesResources.select_clause; - case SyntaxKind.YieldBreakStatement: - return CSharpFeaturesResources.yield_break_statement; + case SyntaxKind.GroupClause: + return CSharpFeaturesResources.groupby_clause; - case SyntaxKind.YieldReturnStatement: - return CSharpFeaturesResources.yield_return_statement; + case SyntaxKind.QueryBody: + return CSharpFeaturesResources.query_body; - case SyntaxKind.AwaitExpression: - return CSharpFeaturesResources.await_expression; + case SyntaxKind.QueryContinuation: + return CSharpFeaturesResources.into_clause; - case SyntaxKind.ParenthesizedLambdaExpression: - case SyntaxKind.SimpleLambdaExpression: - return CSharpFeaturesResources.lambda; + case SyntaxKind.IsPatternExpression: + return CSharpFeaturesResources.is_pattern; - case SyntaxKind.AnonymousMethodExpression: - return CSharpFeaturesResources.anonymous_method; - - case SyntaxKind.FromClause: - return CSharpFeaturesResources.from_clause; - - case SyntaxKind.JoinClause: - case SyntaxKind.JoinIntoClause: - return CSharpFeaturesResources.join_clause; - - case SyntaxKind.LetClause: - return CSharpFeaturesResources.let_clause; + case SyntaxKind.SimpleAssignmentExpression: + if (((AssignmentExpressionSyntax)node).IsDeconstruction()) + { + return CSharpFeaturesResources.deconstruction; + } + else + { + throw ExceptionUtilities.UnexpectedValue(node.Kind()); + } - case SyntaxKind.WhereClause: - return CSharpFeaturesResources.where_clause; + case SyntaxKind.TupleType: + case SyntaxKind.TupleExpression: + return CSharpFeaturesResources.tuple; - case SyntaxKind.OrderByClause: - case SyntaxKind.AscendingOrdering: - case SyntaxKind.DescendingOrdering: - return CSharpFeaturesResources.orderby_clause; + case SyntaxKind.LocalFunctionStatement: + return CSharpFeaturesResources.local_function; - case SyntaxKind.SelectClause: - return CSharpFeaturesResources.select_clause; + case SyntaxKind.DeclarationExpression: + return CSharpFeaturesResources.out_var; - case SyntaxKind.GroupClause: - return CSharpFeaturesResources.groupby_clause; + case SyntaxKind.RefType: + case SyntaxKind.RefExpression: + return CSharpFeaturesResources.ref_local_or_expression; - case SyntaxKind.QueryBody: - return CSharpFeaturesResources.query_body; + case SyntaxKind.SwitchStatement: + return CSharpFeaturesResources.switch_statement; - case SyntaxKind.QueryContinuation: - return CSharpFeaturesResources.into_clause; + case SyntaxKind.LocalDeclarationStatement: + if (((LocalDeclarationStatementSyntax)node).UsingKeyword.IsKind(SyntaxKind.UsingKeyword)) + { + return CSharpFeaturesResources.using_declaration; + } - case SyntaxKind.IsPatternExpression: - return CSharpFeaturesResources.is_pattern; + return CSharpFeaturesResources.local_variable_declaration; - case SyntaxKind.SimpleAssignmentExpression: - if (((AssignmentExpressionSyntax)node).IsDeconstruction()) - { - return CSharpFeaturesResources.deconstruction; - } - else - { - throw ExceptionUtilities.UnexpectedValue(node.Kind()); - } + default: + return null; + } + } - case SyntaxKind.TupleType: - case SyntaxKind.TupleExpression: - return CSharpFeaturesResources.tuple; + protected override string GetSuspensionPointDisplayName(SyntaxNode node, EditKind editKind) + { + switch (node.Kind()) + { + case SyntaxKind.ForEachStatement: + Debug.Assert(((CommonForEachStatementSyntax)node).AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword)); + return CSharpFeaturesResources.asynchronous_foreach_statement; - case SyntaxKind.LocalFunctionStatement: - return CSharpFeaturesResources.local_function; + case SyntaxKind.VariableDeclarator: + RoslynDebug.Assert(((LocalDeclarationStatementSyntax)node.Parent!.Parent!).AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword)); + return CSharpFeaturesResources.asynchronous_using_declaration; - case SyntaxKind.DeclarationExpression: - return CSharpFeaturesResources.out_var; + default: + return base.GetSuspensionPointDisplayName(node, editKind); + } + } - case SyntaxKind.RefType: - case SyntaxKind.RefExpression: - return CSharpFeaturesResources.ref_local_or_expression; + #endregion - case SyntaxKind.SwitchStatement: - return CSharpFeaturesResources.switch_statement; + #region Top-Level Syntactic Rude Edits - case SyntaxKind.LocalDeclarationStatement: - if (((LocalDeclarationStatementSyntax)node).UsingKeyword.IsKind(SyntaxKind.UsingKeyword)) - { - return CSharpFeaturesResources.using_declaration; - } - - return CSharpFeaturesResources.local_variable_declaration; - - default: - return null; - } + private readonly struct EditClassifier + { + private readonly CSharpEditAndContinueAnalyzer _analyzer; + private readonly ArrayBuilder _diagnostics; + private readonly Match? _match; + private readonly SyntaxNode? _oldNode; + private readonly SyntaxNode? _newNode; + private readonly EditKind _kind; + private readonly TextSpan? _span; + + public EditClassifier( + CSharpEditAndContinueAnalyzer analyzer, + ArrayBuilder diagnostics, + SyntaxNode? oldNode, + SyntaxNode? newNode, + EditKind kind, + Match? match = null, + TextSpan? span = null) + { + RoslynDebug.Assert(oldNode != null || newNode != null); + + // if the node is deleted we have map that can be used to closest new ancestor + RoslynDebug.Assert(newNode != null || match != null); + + _analyzer = analyzer; + _diagnostics = diagnostics; + _oldNode = oldNode; + _newNode = newNode; + _kind = kind; + _span = span; + _match = match; } - protected override string GetSuspensionPointDisplayName(SyntaxNode node, EditKind editKind) + private void ReportError(RudeEditKind kind, SyntaxNode? spanNode = null, SyntaxNode? displayNode = null) { - switch (node.Kind()) - { - case SyntaxKind.ForEachStatement: - Debug.Assert(((CommonForEachStatementSyntax)node).AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword)); - return CSharpFeaturesResources.asynchronous_foreach_statement; - - case SyntaxKind.VariableDeclarator: - RoslynDebug.Assert(((LocalDeclarationStatementSyntax)node.Parent!.Parent!).AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword)); - return CSharpFeaturesResources.asynchronous_using_declaration; + var span = (spanNode != null) ? GetDiagnosticSpan(spanNode, _kind) : GetSpan(); + var node = displayNode ?? _newNode ?? _oldNode; + var displayName = GetDisplayName(node!, _kind); - default: - return base.GetSuspensionPointDisplayName(node, editKind); - } + _diagnostics.Add(new RudeEditDiagnostic(kind, span, node, arguments: [displayName])); } - #endregion - - #region Top-Level Syntactic Rude Edits - - private readonly struct EditClassifier + private TextSpan GetSpan() { - private readonly CSharpEditAndContinueAnalyzer _analyzer; - private readonly ArrayBuilder _diagnostics; - private readonly Match? _match; - private readonly SyntaxNode? _oldNode; - private readonly SyntaxNode? _newNode; - private readonly EditKind _kind; - private readonly TextSpan? _span; - - public EditClassifier( - CSharpEditAndContinueAnalyzer analyzer, - ArrayBuilder diagnostics, - SyntaxNode? oldNode, - SyntaxNode? newNode, - EditKind kind, - Match? match = null, - TextSpan? span = null) + if (_span.HasValue) { - RoslynDebug.Assert(oldNode != null || newNode != null); - - // if the node is deleted we have map that can be used to closest new ancestor - RoslynDebug.Assert(newNode != null || match != null); - - _analyzer = analyzer; - _diagnostics = diagnostics; - _oldNode = oldNode; - _newNode = newNode; - _kind = kind; - _span = span; - _match = match; + return _span.Value; } - private void ReportError(RudeEditKind kind, SyntaxNode? spanNode = null, SyntaxNode? displayNode = null) + if (_newNode == null) { - var span = (spanNode != null) ? GetDiagnosticSpan(spanNode, _kind) : GetSpan(); - var node = displayNode ?? _newNode ?? _oldNode; - var displayName = GetDisplayName(node!, _kind); - - _diagnostics.Add(new RudeEditDiagnostic(kind, span, node, arguments: [displayName])); + return _analyzer.GetDeletedNodeDiagnosticSpan(_match!.Matches, _oldNode!); } - private TextSpan GetSpan() - { - if (_span.HasValue) - { - return _span.Value; - } - - if (_newNode == null) - { - return _analyzer.GetDeletedNodeDiagnosticSpan(_match!.Matches, _oldNode!); - } - - return GetDiagnosticSpan(_newNode, _kind); - } + return GetDiagnosticSpan(_newNode, _kind); + } - public void ClassifyEdit() + public void ClassifyEdit() + { + switch (_kind) { - switch (_kind) - { - case EditKind.Delete: - ClassifyDelete(_oldNode!); - return; + case EditKind.Delete: + ClassifyDelete(_oldNode!); + return; - case EditKind.Update: - ClassifyUpdate(_newNode!); - return; + case EditKind.Update: + ClassifyUpdate(_newNode!); + return; - case EditKind.Move: - ClassifyMove(_newNode!); - return; + case EditKind.Move: + ClassifyMove(_newNode!); + return; - case EditKind.Insert: - ClassifyInsert(_newNode!); - return; + case EditKind.Insert: + ClassifyInsert(_newNode!); + return; - case EditKind.Reorder: - ClassifyReorder(_newNode!); - return; + case EditKind.Reorder: + ClassifyReorder(_newNode!); + return; - default: - throw ExceptionUtilities.UnexpectedValue(_kind); - } + default: + throw ExceptionUtilities.UnexpectedValue(_kind); } + } - private void ClassifyMove(SyntaxNode newNode) + private void ClassifyMove(SyntaxNode newNode) + { + if (SupportsMove(newNode)) { - if (SupportsMove(newNode)) - { - return; - } + return; + } + + ReportError(RudeEditKind.Move); + } - ReportError(RudeEditKind.Move); + private void ClassifyReorder(SyntaxNode newNode) + { + if (_newNode.IsKind(SyntaxKind.LocalFunctionStatement)) + { + return; } - private void ClassifyReorder(SyntaxNode newNode) + switch (newNode.Kind()) { - if (_newNode.IsKind(SyntaxKind.LocalFunctionStatement)) - { + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.FieldDeclaration: + case SyntaxKind.EventFieldDeclaration: + case SyntaxKind.VariableDeclarator: + // Maybe we could allow changing order of field declarations unless the containing type layout is sequential. + ReportError(RudeEditKind.Move); return; - } - - switch (newNode.Kind()) - { - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.FieldDeclaration: - case SyntaxKind.EventFieldDeclaration: - case SyntaxKind.VariableDeclarator: - // Maybe we could allow changing order of field declarations unless the containing type layout is sequential. - ReportError(RudeEditKind.Move); - return; - case SyntaxKind.EnumMemberDeclaration: - // To allow this change we would need to check that values of all fields of the enum - // are preserved, or make sure we can update all method bodies that accessed those that changed. - ReportError(RudeEditKind.Move); - return; + case SyntaxKind.EnumMemberDeclaration: + // To allow this change we would need to check that values of all fields of the enum + // are preserved, or make sure we can update all method bodies that accessed those that changed. + ReportError(RudeEditKind.Move); + return; - case SyntaxKind.TypeParameter: - ReportError(RudeEditKind.Move); - return; - } + case SyntaxKind.TypeParameter: + ReportError(RudeEditKind.Move); + return; } + } - private void ClassifyInsert(SyntaxNode node) + private void ClassifyInsert(SyntaxNode node) + { + switch (node.Kind()) { - switch (node.Kind()) - { - case SyntaxKind.ExternAliasDirective: - ReportError(RudeEditKind.Insert); - return; + case SyntaxKind.ExternAliasDirective: + ReportError(RudeEditKind.Insert); + return; - case SyntaxKind.Attribute: - case SyntaxKind.AttributeList: - // To allow inserting of attributes we need to check if the inserted attribute - // is a pseudo-custom attribute that CLR allows us to change, or if it is a compiler well-know attribute - // that affects the generated IL, so we defer those checks until semantic analysis. + case SyntaxKind.Attribute: + case SyntaxKind.AttributeList: + // To allow inserting of attributes we need to check if the inserted attribute + // is a pseudo-custom attribute that CLR allows us to change, or if it is a compiler well-know attribute + // that affects the generated IL, so we defer those checks until semantic analysis. - // Unless the attribute is a module/assembly attribute - if (node.IsParentKind(SyntaxKind.CompilationUnit) || node.Parent.IsParentKind(SyntaxKind.CompilationUnit)) - { - ReportError(RudeEditKind.Insert); - } + // Unless the attribute is a module/assembly attribute + if (node.IsParentKind(SyntaxKind.CompilationUnit) || node.Parent.IsParentKind(SyntaxKind.CompilationUnit)) + { + ReportError(RudeEditKind.Insert); + } - return; - } + return; } + } - private void ClassifyDelete(SyntaxNode oldNode) + private void ClassifyDelete(SyntaxNode oldNode) + { + switch (oldNode.Kind()) { - switch (oldNode.Kind()) - { - case SyntaxKind.ExternAliasDirective: - // To allow removal of declarations we would need to update method bodies that - // were previously binding to them but now are binding to another symbol that was previously hidden. - ReportError(RudeEditKind.Delete); - return; + case SyntaxKind.ExternAliasDirective: + // To allow removal of declarations we would need to update method bodies that + // were previously binding to them but now are binding to another symbol that was previously hidden. + ReportError(RudeEditKind.Delete); + return; - case SyntaxKind.AttributeList: - case SyntaxKind.Attribute: - // To allow removal of attributes we need to check if the removed attribute - // is a pseudo-custom attribute that CLR does not allow us to change, or if it is a compiler well-know attribute - // that affects the generated IL, so we defer those checks until semantic analysis. + case SyntaxKind.AttributeList: + case SyntaxKind.Attribute: + // To allow removal of attributes we need to check if the removed attribute + // is a pseudo-custom attribute that CLR does not allow us to change, or if it is a compiler well-know attribute + // that affects the generated IL, so we defer those checks until semantic analysis. - // Unless the attribute is a module/assembly attribute - if (oldNode.IsParentKind(SyntaxKind.CompilationUnit) || oldNode.Parent.IsParentKind(SyntaxKind.CompilationUnit)) - { - ReportError(RudeEditKind.Delete); - } + // Unless the attribute is a module/assembly attribute + if (oldNode.IsParentKind(SyntaxKind.CompilationUnit) || oldNode.Parent.IsParentKind(SyntaxKind.CompilationUnit)) + { + ReportError(RudeEditKind.Delete); + } - return; - } + return; } + } - private void ClassifyUpdate(SyntaxNode newNode) + private void ClassifyUpdate(SyntaxNode newNode) + { + switch (newNode.Kind()) { - switch (newNode.Kind()) - { - case SyntaxKind.ExternAliasDirective: - ReportError(RudeEditKind.Update); - return; + case SyntaxKind.ExternAliasDirective: + ReportError(RudeEditKind.Update); + return; - case SyntaxKind.Attribute: - // To allow update of attributes we need to check if the updated attribute - // is a pseudo-custom attribute that CLR allows us to change, or if it is a compiler well-know attribute - // that affects the generated IL, so we defer those checks until semantic analysis. + case SyntaxKind.Attribute: + // To allow update of attributes we need to check if the updated attribute + // is a pseudo-custom attribute that CLR allows us to change, or if it is a compiler well-know attribute + // that affects the generated IL, so we defer those checks until semantic analysis. - // Unless the attribute is a module/assembly attribute - if (newNode.IsParentKind(SyntaxKind.CompilationUnit) || newNode.Parent.IsParentKind(SyntaxKind.CompilationUnit)) - { - ReportError(RudeEditKind.Update); - } + // Unless the attribute is a module/assembly attribute + if (newNode.IsParentKind(SyntaxKind.CompilationUnit) || newNode.Parent.IsParentKind(SyntaxKind.CompilationUnit)) + { + ReportError(RudeEditKind.Update); + } - return; - } + return; } } + } - internal override void ReportTopLevelSyntacticRudeEdits( - ArrayBuilder diagnostics, - Match match, - Edit edit, - Dictionary editMap) + internal override void ReportTopLevelSyntacticRudeEdits( + ArrayBuilder diagnostics, + Match match, + Edit edit, + Dictionary editMap) + { + if (HasParentEdit(editMap, edit)) { - if (HasParentEdit(editMap, edit)) - { - return; - } - - var classifier = new EditClassifier(this, diagnostics, edit.OldNode, edit.NewNode, edit.Kind, match); - classifier.ClassifyEdit(); + return; } - internal override bool HasUnsupportedOperation(IEnumerable nodes, [NotNullWhen(true)] out SyntaxNode? unsupportedNode, out RudeEditKind rudeEdit) - { - // Disallow editing the body even if the change is only in trivia. - // The compiler might emit extra temp local variables, which would change stack layout and cause the CLR to fail. + var classifier = new EditClassifier(this, diagnostics, edit.OldNode, edit.NewNode, edit.Kind, match); + classifier.ClassifyEdit(); + } - foreach (var node in nodes) + internal override bool HasUnsupportedOperation(IEnumerable nodes, [NotNullWhen(true)] out SyntaxNode? unsupportedNode, out RudeEditKind rudeEdit) + { + // Disallow editing the body even if the change is only in trivia. + // The compiler might emit extra temp local variables, which would change stack layout and cause the CLR to fail. + + foreach (var node in nodes) + { + if (node.Kind() is SyntaxKind.StackAllocArrayCreationExpression or SyntaxKind.ImplicitStackAllocArrayCreationExpression) { - if (node.Kind() is SyntaxKind.StackAllocArrayCreationExpression or SyntaxKind.ImplicitStackAllocArrayCreationExpression) - { - unsupportedNode = node; - rudeEdit = RudeEditKind.StackAllocUpdate; - return true; - } + unsupportedNode = node; + rudeEdit = RudeEditKind.StackAllocUpdate; + return true; } - - unsupportedNode = null; - rudeEdit = RudeEditKind.None; - return false; } - #endregion + unsupportedNode = null; + rudeEdit = RudeEditKind.None; + return false; + } - #region Semantic Rude Edits + #endregion - internal override void ReportInsertedMemberSymbolRudeEdits(ArrayBuilder diagnostics, ISymbol newSymbol, SyntaxNode newNode, bool insertingIntoExistingContainingType) - { - Debug.Assert(IsMember(newSymbol)); + #region Semantic Rude Edits - var rudeEditKind = newSymbol switch - { - // Inserting extern member into a new or existing type is not allowed. - { IsExtern: true } - => RudeEditKind.InsertExtern, + internal override void ReportInsertedMemberSymbolRudeEdits(ArrayBuilder diagnostics, ISymbol newSymbol, SyntaxNode newNode, bool insertingIntoExistingContainingType) + { + Debug.Assert(IsMember(newSymbol)); + + var rudeEditKind = newSymbol switch + { + // Inserting extern member into a new or existing type is not allowed. + { IsExtern: true } + => RudeEditKind.InsertExtern, - // All rude edits below only apply when inserting into an existing type (not when the type itself is inserted): - _ when !insertingIntoExistingContainingType => RudeEditKind.None, + // All rude edits below only apply when inserting into an existing type (not when the type itself is inserted): + _ when !insertingIntoExistingContainingType => RudeEditKind.None, - // inserting any nested type is allowed - INamedTypeSymbol => RudeEditKind.None, + // inserting any nested type is allowed + INamedTypeSymbol => RudeEditKind.None, - // Inserting virtual or interface member into an existing type is not allowed. - { IsVirtual: true } or { IsOverride: true } or { IsAbstract: true } - => RudeEditKind.InsertVirtual, + // Inserting virtual or interface member into an existing type is not allowed. + { IsVirtual: true } or { IsOverride: true } or { IsAbstract: true } + => RudeEditKind.InsertVirtual, - // Inserting destructor to an existing type is not allowed. - IMethodSymbol { MethodKind: MethodKind.Destructor } - => RudeEditKind.Insert, + // Inserting destructor to an existing type is not allowed. + IMethodSymbol { MethodKind: MethodKind.Destructor } + => RudeEditKind.Insert, - // Inserting operator to an existing type is not allowed. - IMethodSymbol { MethodKind: MethodKind.Conversion or MethodKind.UserDefinedOperator } - => RudeEditKind.InsertOperator, + // Inserting operator to an existing type is not allowed. + IMethodSymbol { MethodKind: MethodKind.Conversion or MethodKind.UserDefinedOperator } + => RudeEditKind.InsertOperator, - // Inserting a method that explictly implements an interface method into an existing type is not allowed. - IMethodSymbol { ExplicitInterfaceImplementations.IsEmpty: false } - => RudeEditKind.InsertMethodWithExplicitInterfaceSpecifier, + // Inserting a method that explictly implements an interface method into an existing type is not allowed. + IMethodSymbol { ExplicitInterfaceImplementations.IsEmpty: false } + => RudeEditKind.InsertMethodWithExplicitInterfaceSpecifier, - // TODO: Inserting non-virtual member to an interface (https://github.com/dotnet/roslyn/issues/37128) - { ContainingType.TypeKind: TypeKind.Interface } - => RudeEditKind.InsertIntoInterface, + // TODO: Inserting non-virtual member to an interface (https://github.com/dotnet/roslyn/issues/37128) + { ContainingType.TypeKind: TypeKind.Interface } + => RudeEditKind.InsertIntoInterface, - // Inserting a field into an enum: + // Inserting a field into an enum: #pragma warning disable format // https://github.com/dotnet/roslyn/issues/54759 - IFieldSymbol { ContainingType.TypeKind: TypeKind.Enum } - => RudeEditKind.Insert, + IFieldSymbol { ContainingType.TypeKind: TypeKind.Enum } + => RudeEditKind.Insert, #pragma warning restore format - _ => RudeEditKind.None - }; + _ => RudeEditKind.None + }; - if (rudeEditKind != RudeEditKind.None) - { - diagnostics.Add(new RudeEditDiagnostic( - rudeEditKind, - GetDiagnosticSpan(newNode, EditKind.Insert), - newNode, - arguments: [GetDisplayName(newNode, EditKind.Insert)])); - } + if (rudeEditKind != RudeEditKind.None) + { + diagnostics.Add(new RudeEditDiagnostic( + rudeEditKind, + GetDiagnosticSpan(newNode, EditKind.Insert), + newNode, + arguments: [GetDisplayName(newNode, EditKind.Insert)])); } + } + + #endregion - #endregion + #region Exception Handling Rude Edits - #region Exception Handling Rude Edits + /// + /// Return nodes that represent exception handlers encompassing the given active statement node. + /// + protected override List GetExceptionHandlingAncestors(SyntaxNode node, SyntaxNode root, bool isNonLeaf) + { + var result = new List(); - /// - /// Return nodes that represent exception handlers encompassing the given active statement node. - /// - protected override List GetExceptionHandlingAncestors(SyntaxNode node, SyntaxNode root, bool isNonLeaf) + var current = node; + while (current != root) { - var result = new List(); + var kind = current.Kind(); - var current = node; - while (current != root) + switch (kind) { - var kind = current.Kind(); - - switch (kind) - { - case SyntaxKind.TryStatement: - if (isNonLeaf) - { - result.Add(current); - } - - break; - - case SyntaxKind.CatchClause: - case SyntaxKind.FinallyClause: + case SyntaxKind.TryStatement: + if (isNonLeaf) + { result.Add(current); + } - // skip try: - RoslynDebug.Assert(current.Parent is object); - RoslynDebug.Assert(current.Parent.Kind() == SyntaxKind.TryStatement); - current = current.Parent; + break; - break; + case SyntaxKind.CatchClause: + case SyntaxKind.FinallyClause: + result.Add(current); - // stop at type declaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.StructDeclaration: - case SyntaxKind.RecordDeclaration: - case SyntaxKind.RecordStructDeclaration: - return result; - } + // skip try: + RoslynDebug.Assert(current.Parent is object); + RoslynDebug.Assert(current.Parent.Kind() == SyntaxKind.TryStatement); + current = current.Parent; - // stop at lambda: - if (LambdaUtilities.IsLambda(current)) - { + break; + + // stop at type declaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.RecordStructDeclaration: return result; - } + } - Debug.Assert(current.Parent != null); - current = current.Parent; + // stop at lambda: + if (LambdaUtilities.IsLambda(current)) + { + return result; } - return result; + Debug.Assert(current.Parent != null); + current = current.Parent; } - internal override void ReportEnclosingExceptionHandlingRudeEdits( - ArrayBuilder diagnostics, - IEnumerable> exceptionHandlingEdits, - SyntaxNode oldStatement, - TextSpan newStatementSpan) + return result; + } + + internal override void ReportEnclosingExceptionHandlingRudeEdits( + ArrayBuilder diagnostics, + IEnumerable> exceptionHandlingEdits, + SyntaxNode oldStatement, + TextSpan newStatementSpan) + { + foreach (var edit in exceptionHandlingEdits) { - foreach (var edit in exceptionHandlingEdits) - { - // try/catch/finally have distinct labels so only the nodes of the same kind may match: - Debug.Assert(edit.Kind != EditKind.Update || edit.OldNode.RawKind == edit.NewNode.RawKind); + // try/catch/finally have distinct labels so only the nodes of the same kind may match: + Debug.Assert(edit.Kind != EditKind.Update || edit.OldNode.RawKind == edit.NewNode.RawKind); - if (edit.Kind != EditKind.Update || !AreExceptionClausesEquivalent(edit.OldNode, edit.NewNode)) - { - AddAroundActiveStatementRudeDiagnostic(diagnostics, edit.OldNode, edit.NewNode, newStatementSpan); - } + if (edit.Kind != EditKind.Update || !AreExceptionClausesEquivalent(edit.OldNode, edit.NewNode)) + { + AddAroundActiveStatementRudeDiagnostic(diagnostics, edit.OldNode, edit.NewNode, newStatementSpan); } } + } - private static bool AreExceptionClausesEquivalent(SyntaxNode oldNode, SyntaxNode newNode) + private static bool AreExceptionClausesEquivalent(SyntaxNode oldNode, SyntaxNode newNode) + { + switch (oldNode.Kind()) { - switch (oldNode.Kind()) - { - case SyntaxKind.TryStatement: - var oldTryStatement = (TryStatementSyntax)oldNode; - var newTryStatement = (TryStatementSyntax)newNode; - return SyntaxFactory.AreEquivalent(oldTryStatement.Finally, newTryStatement.Finally) - && SyntaxFactory.AreEquivalent(oldTryStatement.Catches, newTryStatement.Catches); - - case SyntaxKind.CatchClause: - case SyntaxKind.FinallyClause: - return SyntaxFactory.AreEquivalent(oldNode, newNode); - - default: - throw ExceptionUtilities.UnexpectedValue(oldNode.Kind()); - } + case SyntaxKind.TryStatement: + var oldTryStatement = (TryStatementSyntax)oldNode; + var newTryStatement = (TryStatementSyntax)newNode; + return SyntaxFactory.AreEquivalent(oldTryStatement.Finally, newTryStatement.Finally) + && SyntaxFactory.AreEquivalent(oldTryStatement.Catches, newTryStatement.Catches); + + case SyntaxKind.CatchClause: + case SyntaxKind.FinallyClause: + return SyntaxFactory.AreEquivalent(oldNode, newNode); + + default: + throw ExceptionUtilities.UnexpectedValue(oldNode.Kind()); } + } - /// - /// An active statement (leaf or not) inside a "catch" makes the catch block read-only. - /// An active statement (leaf or not) inside a "finally" makes the whole try/catch/finally block read-only. - /// An active statement (non leaf) inside a "try" makes the catch/finally block read-only. - /// - /// - /// Exception handling regions are only needed to be tracked if they contain user code. - /// and using generate finally blocks, - /// but they do not contain non-hidden sequence points. - /// - /// An exception handling ancestor of an active statement node. - /// - /// True if all child nodes of the are contained in the exception region represented by the . - /// - protected override TextSpan GetExceptionHandlingRegion(SyntaxNode node, out bool coversAllChildren) - { - TryStatementSyntax tryStatement; - switch (node.Kind()) - { - case SyntaxKind.TryStatement: - tryStatement = (TryStatementSyntax)node; - coversAllChildren = false; + /// + /// An active statement (leaf or not) inside a "catch" makes the catch block read-only. + /// An active statement (leaf or not) inside a "finally" makes the whole try/catch/finally block read-only. + /// An active statement (non leaf) inside a "try" makes the catch/finally block read-only. + /// + /// + /// Exception handling regions are only needed to be tracked if they contain user code. + /// and using generate finally blocks, + /// but they do not contain non-hidden sequence points. + /// + /// An exception handling ancestor of an active statement node. + /// + /// True if all child nodes of the are contained in the exception region represented by the . + /// + protected override TextSpan GetExceptionHandlingRegion(SyntaxNode node, out bool coversAllChildren) + { + TryStatementSyntax tryStatement; + switch (node.Kind()) + { + case SyntaxKind.TryStatement: + tryStatement = (TryStatementSyntax)node; + coversAllChildren = false; - if (tryStatement.Catches.Count == 0) - { - RoslynDebug.Assert(tryStatement.Finally != null); - return tryStatement.Finally.Span; - } + if (tryStatement.Catches.Count == 0) + { + RoslynDebug.Assert(tryStatement.Finally != null); + return tryStatement.Finally.Span; + } - return TextSpan.FromBounds( - tryStatement.Catches.First().SpanStart, - (tryStatement.Finally != null) - ? tryStatement.Finally.Span.End - : tryStatement.Catches.Last().Span.End); + return TextSpan.FromBounds( + tryStatement.Catches.First().SpanStart, + (tryStatement.Finally != null) + ? tryStatement.Finally.Span.End + : tryStatement.Catches.Last().Span.End); - case SyntaxKind.CatchClause: - coversAllChildren = true; - return node.Span; + case SyntaxKind.CatchClause: + coversAllChildren = true; + return node.Span; - case SyntaxKind.FinallyClause: - coversAllChildren = true; - tryStatement = (TryStatementSyntax)node.Parent!; - return tryStatement.Span; + case SyntaxKind.FinallyClause: + coversAllChildren = true; + tryStatement = (TryStatementSyntax)node.Parent!; + return tryStatement.Span; - default: - throw ExceptionUtilities.UnexpectedValue(node.Kind()); - } + default: + throw ExceptionUtilities.UnexpectedValue(node.Kind()); } + } - #endregion + #endregion - #region State Machines + #region State Machines - internal override bool IsStateMachineMethod(SyntaxNode declaration) - => SyntaxUtilities.IsAsyncDeclaration(declaration) || SyntaxUtilities.IsIterator(declaration); + internal override bool IsStateMachineMethod(SyntaxNode declaration) + => SyntaxUtilities.IsAsyncDeclaration(declaration) || SyntaxUtilities.IsIterator(declaration); - internal override void ReportStateMachineSuspensionPointRudeEdits(DiagnosticContext diagnosticContext, SyntaxNode oldNode, SyntaxNode newNode) + internal override void ReportStateMachineSuspensionPointRudeEdits(DiagnosticContext diagnosticContext, SyntaxNode oldNode, SyntaxNode newNode) + { + if (newNode.IsKind(SyntaxKind.AwaitExpression) && oldNode.IsKind(SyntaxKind.AwaitExpression)) { - if (newNode.IsKind(SyntaxKind.AwaitExpression) && oldNode.IsKind(SyntaxKind.AwaitExpression)) - { - var oldContainingStatementPart = FindContainingStatementPart(oldNode); - var newContainingStatementPart = FindContainingStatementPart(newNode); + var oldContainingStatementPart = FindContainingStatementPart(oldNode); + var newContainingStatementPart = FindContainingStatementPart(newNode); - // If the old statement has spilled state and the new doesn't the edit is ok. We'll just not use the spilled state. - if (!SyntaxFactory.AreEquivalent(oldContainingStatementPart, newContainingStatementPart) && - !HasNoSpilledState(newNode, newContainingStatementPart)) - { - diagnosticContext.Report(RudeEditKind.AwaitStatementUpdate, newContainingStatementPart.Span); - } + // If the old statement has spilled state and the new doesn't the edit is ok. We'll just not use the spilled state. + if (!SyntaxFactory.AreEquivalent(oldContainingStatementPart, newContainingStatementPart) && + !HasNoSpilledState(newNode, newContainingStatementPart)) + { + diagnosticContext.Report(RudeEditKind.AwaitStatementUpdate, newContainingStatementPart.Span); } } + } - private static SyntaxNode FindContainingStatementPart(SyntaxNode node) + private static SyntaxNode FindContainingStatementPart(SyntaxNode node) + { + while (true) { - while (true) + if (node is StatementSyntax statement) { - if (node is StatementSyntax statement) - { - return statement; - } - - RoslynDebug.Assert(node is object); - RoslynDebug.Assert(node.Parent is object); - switch (node.Parent.Kind()) - { - case SyntaxKind.ForStatement: - case SyntaxKind.ForEachStatement: - case SyntaxKind.IfStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.SwitchStatement: - case SyntaxKind.LockStatement: - case SyntaxKind.UsingStatement: - case SyntaxKind.ArrowExpressionClause: - return node; - } + return statement; + } - if (LambdaUtilities.IsLambdaBodyStatementOrExpression(node)) - { + RoslynDebug.Assert(node is object); + RoslynDebug.Assert(node.Parent is object); + switch (node.Parent.Kind()) + { + case SyntaxKind.ForStatement: + case SyntaxKind.ForEachStatement: + case SyntaxKind.IfStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.LockStatement: + case SyntaxKind.UsingStatement: + case SyntaxKind.ArrowExpressionClause: return node; - } + } - node = node.Parent; + if (LambdaUtilities.IsLambdaBodyStatementOrExpression(node)) + { + return node; } + + node = node.Parent; } + } + + private static bool HasNoSpilledState(SyntaxNode awaitExpression, SyntaxNode containingStatementPart) + { + Debug.Assert(awaitExpression.IsKind(SyntaxKind.AwaitExpression)); - private static bool HasNoSpilledState(SyntaxNode awaitExpression, SyntaxNode containingStatementPart) + // There is nothing within the statement part surrounding the await expression. + if (containingStatementPart == awaitExpression) { - Debug.Assert(awaitExpression.IsKind(SyntaxKind.AwaitExpression)); + return true; + } - // There is nothing within the statement part surrounding the await expression. - if (containingStatementPart == awaitExpression) - { - return true; - } + switch (containingStatementPart.Kind()) + { + case SyntaxKind.ExpressionStatement: + case SyntaxKind.ReturnStatement: + var expression = GetExpressionFromStatementPart(containingStatementPart); - switch (containingStatementPart.Kind()) - { - case SyntaxKind.ExpressionStatement: - case SyntaxKind.ReturnStatement: - var expression = GetExpressionFromStatementPart(containingStatementPart); + // await expr; + // return await expr; + if (expression == awaitExpression) + { + return true; + } - // await expr; - // return await expr; - if (expression == awaitExpression) - { - return true; - } + // identifier = await expr; + // return identifier = await expr; + return IsSimpleAwaitAssignment(expression, awaitExpression); - // identifier = await expr; - // return identifier = await expr; - return IsSimpleAwaitAssignment(expression, awaitExpression); + case SyntaxKind.VariableDeclaration: + // var idf = await expr in using, for, etc. + // EqualsValueClause -> VariableDeclarator -> VariableDeclaration + return awaitExpression.Parent!.Parent!.Parent == containingStatementPart; - case SyntaxKind.VariableDeclaration: - // var idf = await expr in using, for, etc. - // EqualsValueClause -> VariableDeclarator -> VariableDeclaration - return awaitExpression.Parent!.Parent!.Parent == containingStatementPart; + case SyntaxKind.LocalDeclarationStatement: + // var idf = await expr; + // EqualsValueClause -> VariableDeclarator -> VariableDeclaration -> LocalDeclarationStatement + return awaitExpression.Parent!.Parent!.Parent!.Parent == containingStatementPart; + } - case SyntaxKind.LocalDeclarationStatement: - // var idf = await expr; - // EqualsValueClause -> VariableDeclarator -> VariableDeclaration -> LocalDeclarationStatement - return awaitExpression.Parent!.Parent!.Parent!.Parent == containingStatementPart; - } + return IsSimpleAwaitAssignment(containingStatementPart, awaitExpression); + } - return IsSimpleAwaitAssignment(containingStatementPart, awaitExpression); + private static ExpressionSyntax GetExpressionFromStatementPart(SyntaxNode statement) + { + switch (statement.Kind()) + { + case SyntaxKind.ExpressionStatement: + return ((ExpressionStatementSyntax)statement).Expression; + + case SyntaxKind.ReturnStatement: + // Must have an expression since we are only inspecting at statements that contain an expression. + return ((ReturnStatementSyntax)statement).Expression!; + + default: + throw ExceptionUtilities.UnexpectedValue(statement.Kind()); } + } - private static ExpressionSyntax GetExpressionFromStatementPart(SyntaxNode statement) + private static bool IsSimpleAwaitAssignment(SyntaxNode node, SyntaxNode awaitExpression) + { + if (node is AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignment) { - switch (statement.Kind()) - { - case SyntaxKind.ExpressionStatement: - return ((ExpressionStatementSyntax)statement).Expression; + return assignment.Left.IsKind(SyntaxKind.IdentifierName) && assignment.Right == awaitExpression; + } - case SyntaxKind.ReturnStatement: - // Must have an expression since we are only inspecting at statements that contain an expression. - return ((ReturnStatementSyntax)statement).Expression!; + return false; + } - default: - throw ExceptionUtilities.UnexpectedValue(statement.Kind()); - } - } + #endregion + + #region Rude Edits around Active Statement + + internal override void ReportOtherRudeEditsAroundActiveStatement( + ArrayBuilder diagnostics, + IReadOnlyDictionary reverseMap, + SyntaxNode oldActiveStatement, + DeclarationBody oldBody, + SyntaxNode newActiveStatement, + DeclarationBody newBody, + bool isNonLeaf) + { + ReportRudeEditsForSwitchWhenClauses(diagnostics, oldActiveStatement, newActiveStatement); + ReportRudeEditsForAncestorsDeclaringInterStatementTemps(diagnostics, reverseMap, oldActiveStatement, oldBody.EncompassingAncestor, newActiveStatement, newBody.EncompassingAncestor); + ReportRudeEditsForCheckedStatements(diagnostics, oldActiveStatement, newActiveStatement, isNonLeaf); + } - private static bool IsSimpleAwaitAssignment(SyntaxNode node, SyntaxNode awaitExpression) + /// + /// Reports rude edits when an active statement is a when clause in a switch statement and any of the switch cases or the switch value changed. + /// This is necessary since the switch emits long-lived synthesized variables to store results of pattern evaluations. + /// These synthesized variables are mapped to the slots of the new methods via ordinals. The mapping preserves the values of these variables as long as + /// exactly the same variables are emitted for the new switch as they were for the old one and their order didn't change either. + /// This is guaranteed if none of the case clauses have changed. + /// + private void ReportRudeEditsForSwitchWhenClauses(ArrayBuilder diagnostics, SyntaxNode oldActiveStatement, SyntaxNode newActiveStatement) + { + if (!oldActiveStatement.IsKind(SyntaxKind.WhenClause)) { - if (node is AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignment) - { - return assignment.Left.IsKind(SyntaxKind.IdentifierName) && assignment.Right == awaitExpression; - } + return; + } - return false; + // switch expression does not have sequence points (active statements): + if (oldActiveStatement.Parent!.Parent!.Parent is not SwitchStatementSyntax oldSwitch) + { + return; } - #endregion + // switch statement does not match switch expression, so it must be part of a switch statement as well. + var newSwitch = (SwitchStatementSyntax)newActiveStatement.Parent!.Parent!.Parent!; - #region Rude Edits around Active Statement + // when clauses can only match other when clauses: + Debug.Assert(newActiveStatement.IsKind(SyntaxKind.WhenClause)); - internal override void ReportOtherRudeEditsAroundActiveStatement( - ArrayBuilder diagnostics, - IReadOnlyDictionary reverseMap, - SyntaxNode oldActiveStatement, - DeclarationBody oldBody, - SyntaxNode newActiveStatement, - DeclarationBody newBody, - bool isNonLeaf) - { - ReportRudeEditsForSwitchWhenClauses(diagnostics, oldActiveStatement, newActiveStatement); - ReportRudeEditsForAncestorsDeclaringInterStatementTemps(diagnostics, reverseMap, oldActiveStatement, oldBody.EncompassingAncestor, newActiveStatement, newBody.EncompassingAncestor); - ReportRudeEditsForCheckedStatements(diagnostics, oldActiveStatement, newActiveStatement, isNonLeaf); - } - - /// - /// Reports rude edits when an active statement is a when clause in a switch statement and any of the switch cases or the switch value changed. - /// This is necessary since the switch emits long-lived synthesized variables to store results of pattern evaluations. - /// These synthesized variables are mapped to the slots of the new methods via ordinals. The mapping preserves the values of these variables as long as - /// exactly the same variables are emitted for the new switch as they were for the old one and their order didn't change either. - /// This is guaranteed if none of the case clauses have changed. - /// - private void ReportRudeEditsForSwitchWhenClauses(ArrayBuilder diagnostics, SyntaxNode oldActiveStatement, SyntaxNode newActiveStatement) - { - if (!oldActiveStatement.IsKind(SyntaxKind.WhenClause)) - { - return; - } + if (!AreEquivalentIgnoringLambdaBodies(oldSwitch.Expression, newSwitch.Expression)) + { + AddRudeUpdateAroundActiveStatement(diagnostics, newSwitch); + } - // switch expression does not have sequence points (active statements): - if (oldActiveStatement.Parent!.Parent!.Parent is not SwitchStatementSyntax oldSwitch) - { - return; - } + if (!AreEquivalentSwitchStatementDecisionTrees(oldSwitch, newSwitch)) + { + diagnostics.Add(new RudeEditDiagnostic( + RudeEditKind.UpdateAroundActiveStatement, + GetDiagnosticSpan(newSwitch, EditKind.Update), + newSwitch, + [CSharpFeaturesResources.switch_statement_case_clause])); + } + } - // switch statement does not match switch expression, so it must be part of a switch statement as well. - var newSwitch = (SwitchStatementSyntax)newActiveStatement.Parent!.Parent!.Parent!; + private static bool AreEquivalentSwitchStatementDecisionTrees(SwitchStatementSyntax oldSwitch, SwitchStatementSyntax newSwitch) + => oldSwitch.Sections.SequenceEqual(newSwitch.Sections, AreSwitchSectionsEquivalent); - // when clauses can only match other when clauses: - Debug.Assert(newActiveStatement.IsKind(SyntaxKind.WhenClause)); + private static bool AreSwitchSectionsEquivalent(SwitchSectionSyntax oldSection, SwitchSectionSyntax newSection) + => oldSection.Labels.SequenceEqual(newSection.Labels, AreLabelsEquivalent); - if (!AreEquivalentIgnoringLambdaBodies(oldSwitch.Expression, newSwitch.Expression)) - { - AddRudeUpdateAroundActiveStatement(diagnostics, newSwitch); - } + private static bool AreLabelsEquivalent(SwitchLabelSyntax oldLabel, SwitchLabelSyntax newLabel) + { + if (oldLabel is CasePatternSwitchLabelSyntax oldCasePatternLabel && + newLabel is CasePatternSwitchLabelSyntax newCasePatternLabel) + { + // ignore the actual when expressions: + return SyntaxFactory.AreEquivalent(oldCasePatternLabel.Pattern, newCasePatternLabel.Pattern) && + (oldCasePatternLabel.WhenClause != null) == (newCasePatternLabel.WhenClause != null); + } + else + { + return SyntaxFactory.AreEquivalent(oldLabel, newLabel); + } + } - if (!AreEquivalentSwitchStatementDecisionTrees(oldSwitch, newSwitch)) - { - diagnostics.Add(new RudeEditDiagnostic( - RudeEditKind.UpdateAroundActiveStatement, - GetDiagnosticSpan(newSwitch, EditKind.Update), - newSwitch, - [CSharpFeaturesResources.switch_statement_case_clause])); - } + private void ReportRudeEditsForCheckedStatements( + ArrayBuilder diagnostics, + SyntaxNode oldActiveStatement, + SyntaxNode newActiveStatement, + bool isNonLeaf) + { + // checked context can't be changed around non-leaf active statement: + if (!isNonLeaf) + { + return; } - private static bool AreEquivalentSwitchStatementDecisionTrees(SwitchStatementSyntax oldSwitch, SwitchStatementSyntax newSwitch) - => oldSwitch.Sections.SequenceEqual(newSwitch.Sections, AreSwitchSectionsEquivalent); + // Changing checked context around an internal active statement may change the instructions + // executed after method calls in the active statement but before the next sequence point. + // Since the debugger remaps the IP at the first sequence point following a call instruction + // allowing overflow context to be changed may lead to execution of code with old semantics. - private static bool AreSwitchSectionsEquivalent(SwitchSectionSyntax oldSection, SwitchSectionSyntax newSection) - => oldSection.Labels.SequenceEqual(newSection.Labels, AreLabelsEquivalent); + var oldCheckedStatement = TryGetCheckedStatementAncestor(oldActiveStatement); + var newCheckedStatement = TryGetCheckedStatementAncestor(newActiveStatement); - private static bool AreLabelsEquivalent(SwitchLabelSyntax oldLabel, SwitchLabelSyntax newLabel) + bool isRude; + if (oldCheckedStatement == null || newCheckedStatement == null) { - if (oldLabel is CasePatternSwitchLabelSyntax oldCasePatternLabel && - newLabel is CasePatternSwitchLabelSyntax newCasePatternLabel) - { - // ignore the actual when expressions: - return SyntaxFactory.AreEquivalent(oldCasePatternLabel.Pattern, newCasePatternLabel.Pattern) && - (oldCasePatternLabel.WhenClause != null) == (newCasePatternLabel.WhenClause != null); - } - else - { - return SyntaxFactory.AreEquivalent(oldLabel, newLabel); - } + isRude = oldCheckedStatement != newCheckedStatement; } - - private void ReportRudeEditsForCheckedStatements( - ArrayBuilder diagnostics, - SyntaxNode oldActiveStatement, - SyntaxNode newActiveStatement, - bool isNonLeaf) + else { - // checked context can't be changed around non-leaf active statement: - if (!isNonLeaf) - { - return; - } + isRude = oldCheckedStatement.Kind() != newCheckedStatement.Kind(); + } - // Changing checked context around an internal active statement may change the instructions - // executed after method calls in the active statement but before the next sequence point. - // Since the debugger remaps the IP at the first sequence point following a call instruction - // allowing overflow context to be changed may lead to execution of code with old semantics. + if (isRude) + { + AddAroundActiveStatementRudeDiagnostic(diagnostics, oldCheckedStatement, newCheckedStatement, newActiveStatement.Span); + } + } - var oldCheckedStatement = TryGetCheckedStatementAncestor(oldActiveStatement); - var newCheckedStatement = TryGetCheckedStatementAncestor(newActiveStatement); + private static CheckedStatementSyntax? TryGetCheckedStatementAncestor(SyntaxNode? node) + { + // Ignoring lambda boundaries since checked context flows through. - bool isRude; - if (oldCheckedStatement == null || newCheckedStatement == null) - { - isRude = oldCheckedStatement != newCheckedStatement; - } - else + while (node != null) + { + switch (node.Kind()) { - isRude = oldCheckedStatement.Kind() != newCheckedStatement.Kind(); + case SyntaxKind.CheckedStatement: + case SyntaxKind.UncheckedStatement: + return (CheckedStatementSyntax)node; } - if (isRude) - { - AddAroundActiveStatementRudeDiagnostic(diagnostics, oldCheckedStatement, newCheckedStatement, newActiveStatement.Span); - } + node = node.Parent; } - private static CheckedStatementSyntax? TryGetCheckedStatementAncestor(SyntaxNode? node) - { - // Ignoring lambda boundaries since checked context flows through. + return null; + } - while (node != null) - { - switch (node.Kind()) - { - case SyntaxKind.CheckedStatement: - case SyntaxKind.UncheckedStatement: - return (CheckedStatementSyntax)node; - } + private void ReportRudeEditsForAncestorsDeclaringInterStatementTemps( + ArrayBuilder diagnostics, + IReadOnlyDictionary reverseMap, + SyntaxNode oldActiveStatement, + SyntaxNode oldEncompassingAncestor, + SyntaxNode newActiveStatement, + SyntaxNode newEncompassingAncestor) + { + // Rude Edits for fixed/using/lock/foreach statements that are added/updated around an active statement. + // Although such changes are technically possible, they might lead to confusion since + // the temporary variables these statements generate won't be properly initialized. + // + // We use a simple algorithm to match each new node with its old counterpart. + // If all nodes match this algorithm is linear, otherwise it's quadratic. + // + // Unlike exception regions matching where we use LCS, we allow reordering of the statements. + + ReportUnmatchedStatements( + diagnostics, + reverseMap, + n => n.IsKind(SyntaxKind.LockStatement), + oldActiveStatement, + oldEncompassingAncestor, + newActiveStatement, + newEncompassingAncestor, + areEquivalent: AreEquivalentActiveStatements, + areSimilar: null); + + ReportUnmatchedStatements( + diagnostics, + reverseMap, + n => n.IsKind(SyntaxKind.FixedStatement), + oldActiveStatement, + oldEncompassingAncestor, + newActiveStatement, + newEncompassingAncestor, + areEquivalent: AreEquivalentActiveStatements, + areSimilar: (n1, n2) => DeclareSameIdentifiers(n1.Declaration.Variables, n2.Declaration.Variables)); + + // Using statements with declaration do not introduce compiler generated temporary. + ReportUnmatchedStatements( + diagnostics, + reverseMap, + n => n is UsingStatementSyntax usingStatement && usingStatement.Declaration is null, + oldActiveStatement, + oldEncompassingAncestor, + newActiveStatement, + newEncompassingAncestor, + areEquivalent: AreEquivalentActiveStatements, + areSimilar: null); + + ReportUnmatchedStatements( + diagnostics, + reverseMap, + n => n.Kind() is SyntaxKind.ForEachStatement or SyntaxKind.ForEachVariableStatement, + oldActiveStatement, + oldEncompassingAncestor, + newActiveStatement, + newEncompassingAncestor, + areEquivalent: AreEquivalentActiveStatements, + areSimilar: AreSimilarActiveStatements); + } - node = node.Parent; - } + private static bool DeclareSameIdentifiers(SeparatedSyntaxList oldVariables, SeparatedSyntaxList newVariables) + => DeclareSameIdentifiers(oldVariables.Select(v => v.Identifier).ToArray(), newVariables.Select(v => v.Identifier).ToArray()); - return null; + private static bool DeclareSameIdentifiers(SyntaxToken[] oldVariables, SyntaxToken[] newVariables) + { + if (oldVariables.Length != newVariables.Length) + { + return false; } - private void ReportRudeEditsForAncestorsDeclaringInterStatementTemps( - ArrayBuilder diagnostics, - IReadOnlyDictionary reverseMap, - SyntaxNode oldActiveStatement, - SyntaxNode oldEncompassingAncestor, - SyntaxNode newActiveStatement, - SyntaxNode newEncompassingAncestor) - { - // Rude Edits for fixed/using/lock/foreach statements that are added/updated around an active statement. - // Although such changes are technically possible, they might lead to confusion since - // the temporary variables these statements generate won't be properly initialized. - // - // We use a simple algorithm to match each new node with its old counterpart. - // If all nodes match this algorithm is linear, otherwise it's quadratic. - // - // Unlike exception regions matching where we use LCS, we allow reordering of the statements. - - ReportUnmatchedStatements( - diagnostics, - reverseMap, - n => n.IsKind(SyntaxKind.LockStatement), - oldActiveStatement, - oldEncompassingAncestor, - newActiveStatement, - newEncompassingAncestor, - areEquivalent: AreEquivalentActiveStatements, - areSimilar: null); - - ReportUnmatchedStatements( - diagnostics, - reverseMap, - n => n.IsKind(SyntaxKind.FixedStatement), - oldActiveStatement, - oldEncompassingAncestor, - newActiveStatement, - newEncompassingAncestor, - areEquivalent: AreEquivalentActiveStatements, - areSimilar: (n1, n2) => DeclareSameIdentifiers(n1.Declaration.Variables, n2.Declaration.Variables)); - - // Using statements with declaration do not introduce compiler generated temporary. - ReportUnmatchedStatements( - diagnostics, - reverseMap, - n => n is UsingStatementSyntax usingStatement && usingStatement.Declaration is null, - oldActiveStatement, - oldEncompassingAncestor, - newActiveStatement, - newEncompassingAncestor, - areEquivalent: AreEquivalentActiveStatements, - areSimilar: null); - - ReportUnmatchedStatements( - diagnostics, - reverseMap, - n => n.Kind() is SyntaxKind.ForEachStatement or SyntaxKind.ForEachVariableStatement, - oldActiveStatement, - oldEncompassingAncestor, - newActiveStatement, - newEncompassingAncestor, - areEquivalent: AreEquivalentActiveStatements, - areSimilar: AreSimilarActiveStatements); - } - - private static bool DeclareSameIdentifiers(SeparatedSyntaxList oldVariables, SeparatedSyntaxList newVariables) - => DeclareSameIdentifiers(oldVariables.Select(v => v.Identifier).ToArray(), newVariables.Select(v => v.Identifier).ToArray()); - - private static bool DeclareSameIdentifiers(SyntaxToken[] oldVariables, SyntaxToken[] newVariables) - { - if (oldVariables.Length != newVariables.Length) + for (var i = 0; i < oldVariables.Length; i++) + { + if (!SyntaxFactory.AreEquivalent(oldVariables[i], newVariables[i])) { return false; } - - for (var i = 0; i < oldVariables.Length; i++) - { - if (!SyntaxFactory.AreEquivalent(oldVariables[i], newVariables[i])) - { - return false; - } - } - - return true; } - #endregion + return true; } + + #endregion } diff --git a/src/Features/CSharp/Portable/EditAndContinue/SyntaxComparer.cs b/src/Features/CSharp/Portable/EditAndContinue/SyntaxComparer.cs index 8397092a413fb..b3472fac53302 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/SyntaxComparer.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/SyntaxComparer.cs @@ -13,1660 +13,1659 @@ using Microsoft.CodeAnalysis.Differencing; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +/// +/// Creates a syntax comparer +/// +/// The root node to start comparisons from +/// The new root node to compare against +/// Child nodes that should always be compared +/// New child nodes to compare against +/// Whether this comparer is in "statement mode" +internal sealed class SyntaxComparer( + SyntaxNode? oldRoot, + SyntaxNode? newRoot, + IEnumerable? oldRootChildren, + IEnumerable? newRootChildren, + bool compareStatementSyntax) : AbstractSyntaxComparer(oldRoot, newRoot, oldRootChildren, newRootChildren, compareStatementSyntax) { + internal static readonly SyntaxComparer TopLevel = new(null, null, null, null, compareStatementSyntax: false); + internal static readonly SyntaxComparer Statement = new(null, null, null, null, compareStatementSyntax: true); + + protected override bool IsLambdaBodyStatementOrExpression(SyntaxNode node) + => LambdaUtilities.IsLambdaBodyStatementOrExpression(node); + + #region Labels + /// - /// Creates a syntax comparer + /// Assumptions: + /// - Each listed label corresponds to one or more syntax kinds. + /// - Nodes with same labels might produce Update edits, nodes with different labels don't. + /// - If is true for a label then all its possible parent labels must precede the label. + /// (i.e. both MethodDeclaration and TypeDeclaration must precede TypeParameter label). + /// - All descendants of a node whose kind is listed here will be ignored regardless of their labels /// - /// The root node to start comparisons from - /// The new root node to compare against - /// Child nodes that should always be compared - /// New child nodes to compare against - /// Whether this comparer is in "statement mode" - internal sealed class SyntaxComparer( - SyntaxNode? oldRoot, - SyntaxNode? newRoot, - IEnumerable? oldRootChildren, - IEnumerable? newRootChildren, - bool compareStatementSyntax) : AbstractSyntaxComparer(oldRoot, newRoot, oldRootChildren, newRootChildren, compareStatementSyntax) + internal enum Label { - internal static readonly SyntaxComparer TopLevel = new(null, null, null, null, compareStatementSyntax: false); - internal static readonly SyntaxComparer Statement = new(null, null, null, null, compareStatementSyntax: true); - - protected override bool IsLambdaBodyStatementOrExpression(SyntaxNode node) - => LambdaUtilities.IsLambdaBodyStatementOrExpression(node); - - #region Labels - - /// - /// Assumptions: - /// - Each listed label corresponds to one or more syntax kinds. - /// - Nodes with same labels might produce Update edits, nodes with different labels don't. - /// - If is true for a label then all its possible parent labels must precede the label. - /// (i.e. both MethodDeclaration and TypeDeclaration must precede TypeParameter label). - /// - All descendants of a node whose kind is listed here will be ignored regardless of their labels - /// - internal enum Label + // Top level syntax kinds + CompilationUnit, + + GlobalStatement, + + NamespaceDeclaration, + ExternAliasDirective, // tied to parent + UsingDirective, // tied to parent + + TypeDeclaration, + EnumDeclaration, + BaseList, // tied to parent + PrimaryConstructorBase, // tied to parent + DelegateDeclaration, + + FieldDeclaration, // tied to parent + FieldVariableDeclaration, // tied to parent + FieldVariableDeclarator, // tied to parent + + MethodDeclaration, // tied to parent + OperatorDeclaration, // tied to parent + ConversionOperatorDeclaration, // tied to parent + ConstructorDeclaration, // tied to parent + DestructorDeclaration, // tied to parent + PropertyDeclaration, // tied to parent + IndexerDeclaration, // tied to parent + EventDeclaration, // tied to parent + EnumMemberDeclaration, // tied to parent + ArrowExpressionClause, // tied to parent + + AccessorList, // tied to parent + AccessorDeclaration, // tied to parent + + // Statement syntax kinds + Block, + CheckedStatement, + UnsafeStatement, + + TryStatement, + CatchClause, // tied to parent + CatchDeclaration, // tied to parent + CatchFilterClause, // tied to parent + FinallyClause, // tied to parent + ForStatement, + ForStatementPart, // tied to parent + ForEachStatement, + UsingStatementWithExpression, + UsingStatementWithDeclarations, + FixedStatement, + LockStatement, + WhileStatement, + DoStatement, + IfStatement, + ElseClause, // tied to parent + + SwitchStatement, + SwitchSection, + CasePatternSwitchLabel, // tied to parent + SwitchExpression, + SwitchExpressionArm, // tied to parent + WhenClause, // tied to parent + + YieldReturnStatement, // tied to parent + YieldBreakStatement, // tied to parent + GotoStatement, + GotoCaseStatement, + BreakContinueStatement, + ReturnThrowStatement, + ExpressionStatement, + + LabeledStatement, + + // TODO: + // Ideally we could declare LocalVariableDeclarator tied to the first enclosing node that defines local scope (block, foreach, etc.) + // Also consider handling LocalDeclarationStatement as just a bag of variable declarators, + // so that variable declarators contained in one can be matched with variable declarators contained in the other. + LocalDeclarationStatement, // tied to parent + LocalVariableDeclaration, // tied to parent + LocalVariableDeclarator, // tied to parent + + SingleVariableDesignation, + AwaitExpression, + NestedFunction, + + FromClause, + QueryBody, + FromClauseLambda, // tied to parent + LetClauseLambda, // tied to parent + WhereClauseLambda, // tied to parent + OrderByClause, // tied to parent + OrderingLambda, // tied to parent + SelectClauseLambda, // tied to parent + JoinClauseLambda, // tied to parent + JoinIntoClause, // tied to parent + GroupClauseLambda, // tied to parent + QueryContinuation, // tied to parent + + // Syntax kinds that are common to both statement and top level + TypeParameterList, // tied to parent + TypeParameterConstraintClause, // tied to parent + TypeParameter, // tied to parent + ParameterList, // tied to parent + BracketedParameterList, // tied to parent + Parameter, // tied to parent + AttributeList, // tied to parent + Attribute, // tied to parent + + // helpers: + Count, + Ignored = IgnoredNode + } + + /// + /// Return 1 if it is desirable to report two edits (delete and insert) rather than a move edit + /// when the node changes its parent. + /// + private static int TiedToAncestor(Label label) + { + switch (label) { - // Top level syntax kinds - CompilationUnit, - - GlobalStatement, - - NamespaceDeclaration, - ExternAliasDirective, // tied to parent - UsingDirective, // tied to parent - - TypeDeclaration, - EnumDeclaration, - BaseList, // tied to parent - PrimaryConstructorBase, // tied to parent - DelegateDeclaration, - - FieldDeclaration, // tied to parent - FieldVariableDeclaration, // tied to parent - FieldVariableDeclarator, // tied to parent - - MethodDeclaration, // tied to parent - OperatorDeclaration, // tied to parent - ConversionOperatorDeclaration, // tied to parent - ConstructorDeclaration, // tied to parent - DestructorDeclaration, // tied to parent - PropertyDeclaration, // tied to parent - IndexerDeclaration, // tied to parent - EventDeclaration, // tied to parent - EnumMemberDeclaration, // tied to parent - ArrowExpressionClause, // tied to parent - - AccessorList, // tied to parent - AccessorDeclaration, // tied to parent - - // Statement syntax kinds - Block, - CheckedStatement, - UnsafeStatement, - - TryStatement, - CatchClause, // tied to parent - CatchDeclaration, // tied to parent - CatchFilterClause, // tied to parent - FinallyClause, // tied to parent - ForStatement, - ForStatementPart, // tied to parent - ForEachStatement, - UsingStatementWithExpression, - UsingStatementWithDeclarations, - FixedStatement, - LockStatement, - WhileStatement, - DoStatement, - IfStatement, - ElseClause, // tied to parent - - SwitchStatement, - SwitchSection, - CasePatternSwitchLabel, // tied to parent - SwitchExpression, - SwitchExpressionArm, // tied to parent - WhenClause, // tied to parent - - YieldReturnStatement, // tied to parent - YieldBreakStatement, // tied to parent - GotoStatement, - GotoCaseStatement, - BreakContinueStatement, - ReturnThrowStatement, - ExpressionStatement, - - LabeledStatement, - - // TODO: - // Ideally we could declare LocalVariableDeclarator tied to the first enclosing node that defines local scope (block, foreach, etc.) - // Also consider handling LocalDeclarationStatement as just a bag of variable declarators, - // so that variable declarators contained in one can be matched with variable declarators contained in the other. - LocalDeclarationStatement, // tied to parent - LocalVariableDeclaration, // tied to parent - LocalVariableDeclarator, // tied to parent - - SingleVariableDesignation, - AwaitExpression, - NestedFunction, - - FromClause, - QueryBody, - FromClauseLambda, // tied to parent - LetClauseLambda, // tied to parent - WhereClauseLambda, // tied to parent - OrderByClause, // tied to parent - OrderingLambda, // tied to parent - SelectClauseLambda, // tied to parent - JoinClauseLambda, // tied to parent - JoinIntoClause, // tied to parent - GroupClauseLambda, // tied to parent - QueryContinuation, // tied to parent - - // Syntax kinds that are common to both statement and top level - TypeParameterList, // tied to parent - TypeParameterConstraintClause, // tied to parent - TypeParameter, // tied to parent - ParameterList, // tied to parent - BracketedParameterList, // tied to parent - Parameter, // tied to parent - AttributeList, // tied to parent - Attribute, // tied to parent - - // helpers: - Count, - Ignored = IgnoredNode + // Top level syntax + case Label.ExternAliasDirective: + case Label.UsingDirective: + case Label.FieldDeclaration: + case Label.FieldVariableDeclaration: + case Label.FieldVariableDeclarator: + case Label.MethodDeclaration: + case Label.OperatorDeclaration: + case Label.ConversionOperatorDeclaration: + case Label.ConstructorDeclaration: + case Label.DestructorDeclaration: + case Label.PropertyDeclaration: + case Label.ArrowExpressionClause: + case Label.IndexerDeclaration: + case Label.EventDeclaration: + case Label.EnumMemberDeclaration: + case Label.BaseList: + case Label.AccessorDeclaration: + case Label.AccessorList: + case Label.TypeParameterList: + case Label.TypeParameter: + case Label.TypeParameterConstraintClause: + case Label.ParameterList: + case Label.BracketedParameterList: + case Label.Parameter: + case Label.AttributeList: + case Label.Attribute: + return 1; + + // Statement syntax + case Label.LocalDeclarationStatement: + case Label.LocalVariableDeclaration: + case Label.LocalVariableDeclarator: + case Label.GotoCaseStatement: + case Label.BreakContinueStatement: + case Label.ElseClause: + case Label.CatchClause: + case Label.CatchDeclaration: + case Label.CatchFilterClause: + case Label.FinallyClause: + case Label.ForStatementPart: + case Label.YieldReturnStatement: + case Label.YieldBreakStatement: + case Label.FromClauseLambda: + case Label.LetClauseLambda: + case Label.WhereClauseLambda: + case Label.OrderByClause: + case Label.OrderingLambda: + case Label.SelectClauseLambda: + case Label.JoinClauseLambda: + case Label.JoinIntoClause: + case Label.GroupClauseLambda: + case Label.QueryContinuation: + case Label.CasePatternSwitchLabel: + case Label.WhenClause: + case Label.SwitchExpressionArm: + return 1; + + default: + return 0; } + } + + internal override int Classify(int kind, SyntaxNode? node, out bool isLeaf) + => (int)Classify((SyntaxKind)kind, node, out isLeaf); + + internal Label Classify(SyntaxKind kind, SyntaxNode? node, out bool isLeaf) + { + isLeaf = false; - /// - /// Return 1 if it is desirable to report two edits (delete and insert) rather than a move edit - /// when the node changes its parent. - /// - private static int TiedToAncestor(Label label) + // If the node is a for loop Initializer, Condition, or Incrementor expression we label it as "ForStatementPart". + // We need to capture it in the match since these expressions can be "active statements" and as such we need to map them. + // + // The parent is not available only when comparing nodes for value equality. + if (node != null && node.Parent.IsKind(SyntaxKind.ForStatement) && node is ExpressionSyntax) { - switch (label) - { - // Top level syntax - case Label.ExternAliasDirective: - case Label.UsingDirective: - case Label.FieldDeclaration: - case Label.FieldVariableDeclaration: - case Label.FieldVariableDeclarator: - case Label.MethodDeclaration: - case Label.OperatorDeclaration: - case Label.ConversionOperatorDeclaration: - case Label.ConstructorDeclaration: - case Label.DestructorDeclaration: - case Label.PropertyDeclaration: - case Label.ArrowExpressionClause: - case Label.IndexerDeclaration: - case Label.EventDeclaration: - case Label.EnumMemberDeclaration: - case Label.BaseList: - case Label.AccessorDeclaration: - case Label.AccessorList: - case Label.TypeParameterList: - case Label.TypeParameter: - case Label.TypeParameterConstraintClause: - case Label.ParameterList: - case Label.BracketedParameterList: - case Label.Parameter: - case Label.AttributeList: - case Label.Attribute: - return 1; - - // Statement syntax - case Label.LocalDeclarationStatement: - case Label.LocalVariableDeclaration: - case Label.LocalVariableDeclarator: - case Label.GotoCaseStatement: - case Label.BreakContinueStatement: - case Label.ElseClause: - case Label.CatchClause: - case Label.CatchDeclaration: - case Label.CatchFilterClause: - case Label.FinallyClause: - case Label.ForStatementPart: - case Label.YieldReturnStatement: - case Label.YieldBreakStatement: - case Label.FromClauseLambda: - case Label.LetClauseLambda: - case Label.WhereClauseLambda: - case Label.OrderByClause: - case Label.OrderingLambda: - case Label.SelectClauseLambda: - case Label.JoinClauseLambda: - case Label.JoinIntoClause: - case Label.GroupClauseLambda: - case Label.QueryContinuation: - case Label.CasePatternSwitchLabel: - case Label.WhenClause: - case Label.SwitchExpressionArm: - return 1; - - default: - return 0; - } + return Label.ForStatementPart; } - internal override int Classify(int kind, SyntaxNode? node, out bool isLeaf) - => (int)Classify((SyntaxKind)kind, node, out isLeaf); + // ************************************ + // Top and statement syntax + // ************************************ - internal Label Classify(SyntaxKind kind, SyntaxNode? node, out bool isLeaf) + // These nodes can appear during top level and statement processing, so we put them in this first + // switch for simplicity. Statement specific, and top level specific cases are handled below. + switch (kind) { - isLeaf = false; + case SyntaxKind.CompilationUnit: + return Label.CompilationUnit; - // If the node is a for loop Initializer, Condition, or Incrementor expression we label it as "ForStatementPart". - // We need to capture it in the match since these expressions can be "active statements" and as such we need to map them. - // - // The parent is not available only when comparing nodes for value equality. - if (node != null && node.Parent.IsKind(SyntaxKind.ForStatement) && node is ExpressionSyntax) - { - return Label.ForStatementPart; - } + case SyntaxKind.TypeParameterList: + return Label.TypeParameterList; - // ************************************ - // Top and statement syntax - // ************************************ + case SyntaxKind.TypeParameterConstraintClause: + return Label.TypeParameterConstraintClause; - // These nodes can appear during top level and statement processing, so we put them in this first - // switch for simplicity. Statement specific, and top level specific cases are handled below. - switch (kind) - { - case SyntaxKind.CompilationUnit: - return Label.CompilationUnit; + case SyntaxKind.TypeParameter: + // not a leaf because an attribute may be applied + return Label.TypeParameter; - case SyntaxKind.TypeParameterList: - return Label.TypeParameterList; + case SyntaxKind.BracketedParameterList: + return Label.BracketedParameterList; - case SyntaxKind.TypeParameterConstraintClause: - return Label.TypeParameterConstraintClause; + case SyntaxKind.ParameterList: + return Label.ParameterList; - case SyntaxKind.TypeParameter: - // not a leaf because an attribute may be applied - return Label.TypeParameter; + case SyntaxKind.Parameter: + return Label.Parameter; - case SyntaxKind.BracketedParameterList: - return Label.BracketedParameterList; + case SyntaxKind.ConstructorDeclaration: + // Root when matching constructor bodies. + return Label.ConstructorDeclaration; + } - case SyntaxKind.ParameterList: - return Label.ParameterList; + if (_compareStatementSyntax) + { + return ClassifyStatementSyntax(kind, node, out isLeaf); + } - case SyntaxKind.Parameter: - return Label.Parameter; + return ClassifyTopSyntax(kind, node, out isLeaf); + } - case SyntaxKind.ConstructorDeclaration: - // Root when matching constructor bodies. - return Label.ConstructorDeclaration; - } + private static Label ClassifyStatementSyntax(SyntaxKind kind, SyntaxNode? node, out bool isLeaf) + { + isLeaf = false; + + // ************************************ + // Statement syntax + // ************************************ + + // These nodes could potentially be seen as top level syntax, but we only want them labelled + // during statement syntax so they have to be kept separate. + // + // For example when top level sees something like this: + // + // private int X => new Func(() => { return 1 })(); + // + // It needs to go through the entire lambda to know if that property def has changed + // but if we start labelling things, like ReturnStatement in the above example, then + // it will stop. Given that a block bodied lambda can have any statements a user likes + // the whole set has to be dealt with separately. + switch (kind) + { + // Notes: + // A descendant of a leaf node may be a labeled node that we don't want to visit if + // we are comparing its parent node (used for lambda bodies). + // + // Expressions are ignored but they may contain nodes that should be matched by tree comparer. + // (e.g. lambdas, declaration expressions). Descending to these nodes is handled in EnumerateChildren. - if (_compareStatementSyntax) - { - return ClassifyStatementSyntax(kind, node, out isLeaf); - } + case SyntaxKind.NamespaceDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.RecordStructDeclaration: + // These declarations can come after global statements so we want to stop statement matching + // because no global statements can come after them + isLeaf = true; + return Label.Ignored; + + case SyntaxKind.LocalDeclarationStatement: + return Label.LocalDeclarationStatement; + + case SyntaxKind.SingleVariableDesignation: + return Label.SingleVariableDesignation; + + case SyntaxKind.LabeledStatement: + return Label.LabeledStatement; - return ClassifyTopSyntax(kind, node, out isLeaf); - } + case SyntaxKind.EmptyStatement: + isLeaf = true; + return Label.ExpressionStatement; - private static Label ClassifyStatementSyntax(SyntaxKind kind, SyntaxNode? node, out bool isLeaf) - { - isLeaf = false; + case SyntaxKind.GotoStatement: + isLeaf = true; + return Label.GotoStatement; - // ************************************ - // Statement syntax - // ************************************ - - // These nodes could potentially be seen as top level syntax, but we only want them labelled - // during statement syntax so they have to be kept separate. - // - // For example when top level sees something like this: - // - // private int X => new Func(() => { return 1 })(); - // - // It needs to go through the entire lambda to know if that property def has changed - // but if we start labelling things, like ReturnStatement in the above example, then - // it will stop. Given that a block bodied lambda can have any statements a user likes - // the whole set has to be dealt with separately. - switch (kind) - { - // Notes: - // A descendant of a leaf node may be a labeled node that we don't want to visit if - // we are comparing its parent node (used for lambda bodies). - // - // Expressions are ignored but they may contain nodes that should be matched by tree comparer. - // (e.g. lambdas, declaration expressions). Descending to these nodes is handled in EnumerateChildren. - - case SyntaxKind.NamespaceDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.StructDeclaration: - case SyntaxKind.RecordDeclaration: - case SyntaxKind.RecordStructDeclaration: - // These declarations can come after global statements so we want to stop statement matching - // because no global statements can come after them - isLeaf = true; - return Label.Ignored; + case SyntaxKind.GotoCaseStatement: + case SyntaxKind.GotoDefaultStatement: + isLeaf = true; + return Label.GotoCaseStatement; - case SyntaxKind.LocalDeclarationStatement: - return Label.LocalDeclarationStatement; + case SyntaxKind.BreakStatement: + case SyntaxKind.ContinueStatement: + isLeaf = true; + return Label.BreakContinueStatement; - case SyntaxKind.SingleVariableDesignation: - return Label.SingleVariableDesignation; + case SyntaxKind.ReturnStatement: + case SyntaxKind.ThrowStatement: + return Label.ReturnThrowStatement; - case SyntaxKind.LabeledStatement: - return Label.LabeledStatement; + case SyntaxKind.ExpressionStatement: + return Label.ExpressionStatement; - case SyntaxKind.EmptyStatement: - isLeaf = true; - return Label.ExpressionStatement; + case SyntaxKind.YieldBreakStatement: + // yield break is distinct from yield return as it does not suspend the state machine in a resumable state + return Label.YieldBreakStatement; - case SyntaxKind.GotoStatement: - isLeaf = true; - return Label.GotoStatement; + case SyntaxKind.YieldReturnStatement: + return Label.YieldReturnStatement; - case SyntaxKind.GotoCaseStatement: - case SyntaxKind.GotoDefaultStatement: - isLeaf = true; - return Label.GotoCaseStatement; + case SyntaxKind.DoStatement: + return Label.DoStatement; - case SyntaxKind.BreakStatement: - case SyntaxKind.ContinueStatement: - isLeaf = true; - return Label.BreakContinueStatement; + case SyntaxKind.WhileStatement: + return Label.WhileStatement; - case SyntaxKind.ReturnStatement: - case SyntaxKind.ThrowStatement: - return Label.ReturnThrowStatement; + case SyntaxKind.ForStatement: + return Label.ForStatement; - case SyntaxKind.ExpressionStatement: - return Label.ExpressionStatement; + case SyntaxKind.ForEachVariableStatement: + case SyntaxKind.ForEachStatement: + return Label.ForEachStatement; - case SyntaxKind.YieldBreakStatement: - // yield break is distinct from yield return as it does not suspend the state machine in a resumable state - return Label.YieldBreakStatement; + case SyntaxKind.UsingStatement: + // We need to distinguish using statements with expression or single variable declaration from ones with multiple variable declarations. + // The former generate a single try-finally block, the latter one for each variable. The finally blocks need to match since they + // affect state machine state matching. For simplicity we do not match single-declaration to expression, we just treat usings + // with declarations entirely separately from usings with expressions. + // + // The parent is not available only when comparing nodes for value equality. + // In that case it doesn't matter what label the node has as long as it has some. + return node is UsingStatementSyntax { Declaration: not null } ? Label.UsingStatementWithDeclarations : Label.UsingStatementWithExpression; - case SyntaxKind.YieldReturnStatement: - return Label.YieldReturnStatement; + case SyntaxKind.FixedStatement: + return Label.FixedStatement; - case SyntaxKind.DoStatement: - return Label.DoStatement; + case SyntaxKind.CheckedStatement: + case SyntaxKind.UncheckedStatement: + return Label.CheckedStatement; - case SyntaxKind.WhileStatement: - return Label.WhileStatement; + case SyntaxKind.UnsafeStatement: + return Label.UnsafeStatement; - case SyntaxKind.ForStatement: - return Label.ForStatement; + case SyntaxKind.LockStatement: + return Label.LockStatement; - case SyntaxKind.ForEachVariableStatement: - case SyntaxKind.ForEachStatement: - return Label.ForEachStatement; + case SyntaxKind.IfStatement: + return Label.IfStatement; - case SyntaxKind.UsingStatement: - // We need to distinguish using statements with expression or single variable declaration from ones with multiple variable declarations. - // The former generate a single try-finally block, the latter one for each variable. The finally blocks need to match since they - // affect state machine state matching. For simplicity we do not match single-declaration to expression, we just treat usings - // with declarations entirely separately from usings with expressions. - // - // The parent is not available only when comparing nodes for value equality. - // In that case it doesn't matter what label the node has as long as it has some. - return node is UsingStatementSyntax { Declaration: not null } ? Label.UsingStatementWithDeclarations : Label.UsingStatementWithExpression; + case SyntaxKind.ElseClause: + return Label.ElseClause; - case SyntaxKind.FixedStatement: - return Label.FixedStatement; + case SyntaxKind.SwitchStatement: + return Label.SwitchStatement; - case SyntaxKind.CheckedStatement: - case SyntaxKind.UncheckedStatement: - return Label.CheckedStatement; + case SyntaxKind.SwitchSection: + return Label.SwitchSection; - case SyntaxKind.UnsafeStatement: - return Label.UnsafeStatement; + case SyntaxKind.CaseSwitchLabel: + case SyntaxKind.DefaultSwitchLabel: + // Switch labels are included in the "value" of the containing switch section. + // We don't need to analyze case expressions. + isLeaf = true; + return Label.Ignored; - case SyntaxKind.LockStatement: - return Label.LockStatement; + case SyntaxKind.WhenClause: + return Label.WhenClause; - case SyntaxKind.IfStatement: - return Label.IfStatement; + case SyntaxKind.CasePatternSwitchLabel: + return Label.CasePatternSwitchLabel; - case SyntaxKind.ElseClause: - return Label.ElseClause; + case SyntaxKind.SwitchExpression: + return Label.SwitchExpression; - case SyntaxKind.SwitchStatement: - return Label.SwitchStatement; + case SyntaxKind.SwitchExpressionArm: + return Label.SwitchExpressionArm; - case SyntaxKind.SwitchSection: - return Label.SwitchSection; + case SyntaxKind.TryStatement: + return Label.TryStatement; - case SyntaxKind.CaseSwitchLabel: - case SyntaxKind.DefaultSwitchLabel: - // Switch labels are included in the "value" of the containing switch section. - // We don't need to analyze case expressions. - isLeaf = true; - return Label.Ignored; + case SyntaxKind.CatchClause: + return Label.CatchClause; - case SyntaxKind.WhenClause: - return Label.WhenClause; + case SyntaxKind.CatchDeclaration: + // the declarator of the exception variable + return Label.CatchDeclaration; - case SyntaxKind.CasePatternSwitchLabel: - return Label.CasePatternSwitchLabel; + case SyntaxKind.CatchFilterClause: + return Label.CatchFilterClause; - case SyntaxKind.SwitchExpression: - return Label.SwitchExpression; + case SyntaxKind.FinallyClause: + return Label.FinallyClause; - case SyntaxKind.SwitchExpressionArm: - return Label.SwitchExpressionArm; + case SyntaxKind.FromClause: + // The first from clause of a query is not a lambda. + // We have to assign it a label different from "FromClauseLambda" + // so that we won't match lambda-from to non-lambda-from. + // + // Since FromClause declares range variables we need to include it in the map, + // so that we are able to map range variable declarations. + // Therefore we assign it a dedicated label. + // + // The parent is not available only when comparing nodes for value equality. + // In that case it doesn't matter what label the node has as long as it has some. + if (node == null || node.Parent.IsKind(SyntaxKind.QueryExpression)) + { + return Label.FromClause; + } - case SyntaxKind.TryStatement: - return Label.TryStatement; + return Label.FromClauseLambda; + + case SyntaxKind.QueryBody: + return Label.QueryBody; + + case SyntaxKind.QueryContinuation: + return Label.QueryContinuation; + + case SyntaxKind.LetClause: + return Label.LetClauseLambda; + + case SyntaxKind.WhereClause: + return Label.WhereClauseLambda; + + case SyntaxKind.OrderByClause: + return Label.OrderByClause; + + case SyntaxKind.AscendingOrdering: + case SyntaxKind.DescendingOrdering: + return Label.OrderingLambda; + + case SyntaxKind.SelectClause: + return Label.SelectClauseLambda; + + case SyntaxKind.JoinClause: + return Label.JoinClauseLambda; + + case SyntaxKind.JoinIntoClause: + return Label.JoinIntoClause; + + case SyntaxKind.GroupClause: + return Label.GroupClauseLambda; + + case SyntaxKind.IdentifierName: + case SyntaxKind.QualifiedName: + case SyntaxKind.GenericName: + case SyntaxKind.TypeArgumentList: + case SyntaxKind.AliasQualifiedName: + case SyntaxKind.PredefinedType: + case SyntaxKind.PointerType: + case SyntaxKind.NullableType: + case SyntaxKind.TupleType: + case SyntaxKind.RefType: + case SyntaxKind.OmittedTypeArgument: + case SyntaxKind.NameColon: + case SyntaxKind.OmittedArraySizeExpression: + case SyntaxKind.ThisExpression: + case SyntaxKind.BaseExpression: + case SyntaxKind.ArgListExpression: + case SyntaxKind.NumericLiteralExpression: + case SyntaxKind.StringLiteralExpression: + case SyntaxKind.CharacterLiteralExpression: + case SyntaxKind.TrueLiteralExpression: + case SyntaxKind.FalseLiteralExpression: + case SyntaxKind.NullLiteralExpression: + case SyntaxKind.TypeOfExpression: + case SyntaxKind.SizeOfExpression: + case SyntaxKind.DefaultExpression: + case SyntaxKind.ConstantPattern: + case SyntaxKind.DiscardDesignation: + // can't contain a lambda/await/anonymous type: + isLeaf = true; + return Label.Ignored; + + case SyntaxKind.AwaitExpression: + return Label.AwaitExpression; + + case SyntaxKind.ParenthesizedLambdaExpression: + case SyntaxKind.SimpleLambdaExpression: + case SyntaxKind.AnonymousMethodExpression: + case SyntaxKind.LocalFunctionStatement: + return Label.NestedFunction; + + case SyntaxKind.VariableDeclaration: + return Label.LocalVariableDeclaration; + + case SyntaxKind.VariableDeclarator: + return Label.LocalVariableDeclarator; + + case SyntaxKind.Block: + return Label.Block; + } - case SyntaxKind.CatchClause: - return Label.CatchClause; + // If we got this far, its an unlabelled node. Since just about any node can + // contain a lambda, isLeaf must be false for statement syntax. + return Label.Ignored; + } - case SyntaxKind.CatchDeclaration: - // the declarator of the exception variable - return Label.CatchDeclaration; + private static Label ClassifyTopSyntax(SyntaxKind kind, SyntaxNode? node, out bool isLeaf) + { + isLeaf = false; - case SyntaxKind.CatchFilterClause: - return Label.CatchFilterClause; + // ************************************ + // Top syntax + // ************************************ - case SyntaxKind.FinallyClause: - return Label.FinallyClause; + // More the most part these nodes will only appear in top syntax but its easier to + // keep them separate so we can more easily discern was is shared, above. + switch (kind) + { + case SyntaxKind.GlobalStatement: + isLeaf = true; + return Label.GlobalStatement; - case SyntaxKind.FromClause: - // The first from clause of a query is not a lambda. - // We have to assign it a label different from "FromClauseLambda" - // so that we won't match lambda-from to non-lambda-from. - // - // Since FromClause declares range variables we need to include it in the map, - // so that we are able to map range variable declarations. - // Therefore we assign it a dedicated label. - // - // The parent is not available only when comparing nodes for value equality. - // In that case it doesn't matter what label the node has as long as it has some. - if (node == null || node.Parent.IsKind(SyntaxKind.QueryExpression)) - { - return Label.FromClause; - } + case SyntaxKind.ExternAliasDirective: + isLeaf = true; + return Label.ExternAliasDirective; - return Label.FromClauseLambda; - - case SyntaxKind.QueryBody: - return Label.QueryBody; - - case SyntaxKind.QueryContinuation: - return Label.QueryContinuation; - - case SyntaxKind.LetClause: - return Label.LetClauseLambda; - - case SyntaxKind.WhereClause: - return Label.WhereClauseLambda; - - case SyntaxKind.OrderByClause: - return Label.OrderByClause; - - case SyntaxKind.AscendingOrdering: - case SyntaxKind.DescendingOrdering: - return Label.OrderingLambda; - - case SyntaxKind.SelectClause: - return Label.SelectClauseLambda; - - case SyntaxKind.JoinClause: - return Label.JoinClauseLambda; - - case SyntaxKind.JoinIntoClause: - return Label.JoinIntoClause; - - case SyntaxKind.GroupClause: - return Label.GroupClauseLambda; - - case SyntaxKind.IdentifierName: - case SyntaxKind.QualifiedName: - case SyntaxKind.GenericName: - case SyntaxKind.TypeArgumentList: - case SyntaxKind.AliasQualifiedName: - case SyntaxKind.PredefinedType: - case SyntaxKind.PointerType: - case SyntaxKind.NullableType: - case SyntaxKind.TupleType: - case SyntaxKind.RefType: - case SyntaxKind.OmittedTypeArgument: - case SyntaxKind.NameColon: - case SyntaxKind.OmittedArraySizeExpression: - case SyntaxKind.ThisExpression: - case SyntaxKind.BaseExpression: - case SyntaxKind.ArgListExpression: - case SyntaxKind.NumericLiteralExpression: - case SyntaxKind.StringLiteralExpression: - case SyntaxKind.CharacterLiteralExpression: - case SyntaxKind.TrueLiteralExpression: - case SyntaxKind.FalseLiteralExpression: - case SyntaxKind.NullLiteralExpression: - case SyntaxKind.TypeOfExpression: - case SyntaxKind.SizeOfExpression: - case SyntaxKind.DefaultExpression: - case SyntaxKind.ConstantPattern: - case SyntaxKind.DiscardDesignation: - // can't contain a lambda/await/anonymous type: - isLeaf = true; - return Label.Ignored; + case SyntaxKind.UsingDirective: + isLeaf = true; + return Label.UsingDirective; - case SyntaxKind.AwaitExpression: - return Label.AwaitExpression; + case SyntaxKind.NamespaceDeclaration: + case SyntaxKind.FileScopedNamespaceDeclaration: + return Label.NamespaceDeclaration; - case SyntaxKind.ParenthesizedLambdaExpression: - case SyntaxKind.SimpleLambdaExpression: - case SyntaxKind.AnonymousMethodExpression: - case SyntaxKind.LocalFunctionStatement: - return Label.NestedFunction; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.RecordStructDeclaration: + return Label.TypeDeclaration; - case SyntaxKind.VariableDeclaration: - return Label.LocalVariableDeclaration; + case SyntaxKind.BaseList: + return Label.BaseList; - case SyntaxKind.VariableDeclarator: - return Label.LocalVariableDeclarator; + case SyntaxKind.PrimaryConstructorBaseType: + // For top syntax, primary constructor base initializer is a leaf node + isLeaf = true; + return Label.PrimaryConstructorBase; - case SyntaxKind.Block: - return Label.Block; - } + case SyntaxKind.MethodDeclaration: + return Label.MethodDeclaration; - // If we got this far, its an unlabelled node. Since just about any node can - // contain a lambda, isLeaf must be false for statement syntax. - return Label.Ignored; - } + case SyntaxKind.EnumDeclaration: + return Label.EnumDeclaration; - private static Label ClassifyTopSyntax(SyntaxKind kind, SyntaxNode? node, out bool isLeaf) - { - isLeaf = false; + case SyntaxKind.DelegateDeclaration: + return Label.DelegateDeclaration; - // ************************************ - // Top syntax - // ************************************ + case SyntaxKind.FieldDeclaration: + case SyntaxKind.EventFieldDeclaration: + return Label.FieldDeclaration; - // More the most part these nodes will only appear in top syntax but its easier to - // keep them separate so we can more easily discern was is shared, above. - switch (kind) - { - case SyntaxKind.GlobalStatement: - isLeaf = true; - return Label.GlobalStatement; + case SyntaxKind.ConversionOperatorDeclaration: + return Label.ConversionOperatorDeclaration; - case SyntaxKind.ExternAliasDirective: - isLeaf = true; - return Label.ExternAliasDirective; + case SyntaxKind.OperatorDeclaration: + return Label.OperatorDeclaration; - case SyntaxKind.UsingDirective: - isLeaf = true; - return Label.UsingDirective; + case SyntaxKind.DestructorDeclaration: + isLeaf = true; + return Label.DestructorDeclaration; - case SyntaxKind.NamespaceDeclaration: - case SyntaxKind.FileScopedNamespaceDeclaration: - return Label.NamespaceDeclaration; + case SyntaxKind.PropertyDeclaration: + return Label.PropertyDeclaration; - case SyntaxKind.ClassDeclaration: - case SyntaxKind.StructDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.RecordDeclaration: - case SyntaxKind.RecordStructDeclaration: - return Label.TypeDeclaration; + case SyntaxKind.IndexerDeclaration: + return Label.IndexerDeclaration; - case SyntaxKind.BaseList: - return Label.BaseList; + case SyntaxKind.ArrowExpressionClause: + if (node?.Parent is (kind: SyntaxKind.PropertyDeclaration or SyntaxKind.IndexerDeclaration)) + return Label.ArrowExpressionClause; - case SyntaxKind.PrimaryConstructorBaseType: - // For top syntax, primary constructor base initializer is a leaf node - isLeaf = true; - return Label.PrimaryConstructorBase; + break; - case SyntaxKind.MethodDeclaration: - return Label.MethodDeclaration; + case SyntaxKind.EventDeclaration: + return Label.EventDeclaration; - case SyntaxKind.EnumDeclaration: - return Label.EnumDeclaration; + case SyntaxKind.EnumMemberDeclaration: + // not a leaf because an attribute may be applied + return Label.EnumMemberDeclaration; - case SyntaxKind.DelegateDeclaration: - return Label.DelegateDeclaration; + case SyntaxKind.AccessorList: + return Label.AccessorList; - case SyntaxKind.FieldDeclaration: - case SyntaxKind.EventFieldDeclaration: - return Label.FieldDeclaration; + case SyntaxKind.GetAccessorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.InitAccessorDeclaration: + case SyntaxKind.AddAccessorDeclaration: + case SyntaxKind.RemoveAccessorDeclaration: + isLeaf = true; + return Label.AccessorDeclaration; - case SyntaxKind.ConversionOperatorDeclaration: - return Label.ConversionOperatorDeclaration; + // Note: These last two do actually appear as statement syntax, but mean something + // different and hence have a different label + case SyntaxKind.VariableDeclaration: + return Label.FieldVariableDeclaration; - case SyntaxKind.OperatorDeclaration: - return Label.OperatorDeclaration; + case SyntaxKind.VariableDeclarator: + // For top syntax, a variable declarator is a leaf node + isLeaf = true; + return Label.FieldVariableDeclarator; - case SyntaxKind.DestructorDeclaration: - isLeaf = true; - return Label.DestructorDeclaration; + case SyntaxKind.AttributeList: + // Only module/assembly attributes are labelled + if (node is not null && node.IsParentKind(SyntaxKind.CompilationUnit)) + { + return Label.AttributeList; + } - case SyntaxKind.PropertyDeclaration: - return Label.PropertyDeclaration; + break; - case SyntaxKind.IndexerDeclaration: - return Label.IndexerDeclaration; + case SyntaxKind.Attribute: + // Only module/assembly attributes are labelled + if (node is { Parent: { } parent } && parent.IsParentKind(SyntaxKind.CompilationUnit)) + { + isLeaf = true; + return Label.Attribute; + } - case SyntaxKind.ArrowExpressionClause: - if (node?.Parent is (kind: SyntaxKind.PropertyDeclaration or SyntaxKind.IndexerDeclaration)) - return Label.ArrowExpressionClause; + break; + } - break; + // If we got this far, its an unlabelled node. For top + // syntax, we don't need to descend into any ignored nodes + isLeaf = true; + return Label.Ignored; + } - case SyntaxKind.EventDeclaration: - return Label.EventDeclaration; + // internal for testing + internal bool HasLabel(SyntaxKind kind) + => Classify(kind, node: null, out _) != Label.Ignored; - case SyntaxKind.EnumMemberDeclaration: - // not a leaf because an attribute may be applied - return Label.EnumMemberDeclaration; + protected internal override int LabelCount + => (int)Label.Count; - case SyntaxKind.AccessorList: - return Label.AccessorList; + protected internal override int TiedToAncestor(int label) + => TiedToAncestor((Label)label); - case SyntaxKind.GetAccessorDeclaration: - case SyntaxKind.SetAccessorDeclaration: - case SyntaxKind.InitAccessorDeclaration: - case SyntaxKind.AddAccessorDeclaration: - case SyntaxKind.RemoveAccessorDeclaration: - isLeaf = true; - return Label.AccessorDeclaration; + #endregion - // Note: These last two do actually appear as statement syntax, but mean something - // different and hence have a different label - case SyntaxKind.VariableDeclaration: - return Label.FieldVariableDeclaration; + #region Comparisons - case SyntaxKind.VariableDeclarator: - // For top syntax, a variable declarator is a leaf node - isLeaf = true; - return Label.FieldVariableDeclarator; + public override bool ValuesEqual(SyntaxNode left, SyntaxNode right) + { + Func? ignoreChildFunction; + switch (left.Kind()) + { + // all syntax kinds with a method body child: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.ConversionOperatorDeclaration: + case SyntaxKind.OperatorDeclaration: + case SyntaxKind.ConstructorDeclaration: + case SyntaxKind.DestructorDeclaration: + case SyntaxKind.GetAccessorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.InitAccessorDeclaration: + case SyntaxKind.AddAccessorDeclaration: + case SyntaxKind.RemoveAccessorDeclaration: + // When comparing method bodies we need to NOT ignore VariableDeclaration and VariableDeclarator children, + // but when comparing field definitions we should ignore VariableDeclarations children. + + var leftBody = GetBody(left); + var rightBody = GetBody(right); + + if (!SyntaxFactory.AreEquivalent(leftBody, rightBody, null)) + { + return false; + } - case SyntaxKind.AttributeList: - // Only module/assembly attributes are labelled - if (node is not null && node.IsParentKind(SyntaxKind.CompilationUnit)) - { - return Label.AttributeList; - } + ignoreChildFunction = childKind => childKind == SyntaxKind.Block || childKind == SyntaxKind.ArrowExpressionClause || HasLabel(childKind); + break; - break; + case SyntaxKind.SwitchSection: + return Equal((SwitchSectionSyntax)left, (SwitchSectionSyntax)right); - case SyntaxKind.Attribute: - // Only module/assembly attributes are labelled - if (node is { Parent: { } parent } && parent.IsParentKind(SyntaxKind.CompilationUnit)) - { - isLeaf = true; - return Label.Attribute; - } + case SyntaxKind.ForStatement: + // The only children of ForStatement are labeled nodes and punctuation. + return true; - break; - } + default: + if (HasChildren(left)) + { + ignoreChildFunction = childKind => HasLabel(childKind); + } + else + { + ignoreChildFunction = null; + } - // If we got this far, its an unlabelled node. For top - // syntax, we don't need to descend into any ignored nodes - isLeaf = true; - return Label.Ignored; + break; } - // internal for testing - internal bool HasLabel(SyntaxKind kind) - => Classify(kind, node: null, out _) != Label.Ignored; - - protected internal override int LabelCount - => (int)Label.Count; - - protected internal override int TiedToAncestor(int label) - => TiedToAncestor((Label)label); + return SyntaxFactory.AreEquivalent(left, right, ignoreChildFunction); + } - #endregion + private bool Equal(SwitchSectionSyntax left, SwitchSectionSyntax right) + { + return SyntaxFactory.AreEquivalent(left.Labels, right.Labels, null) + && SyntaxFactory.AreEquivalent(left.Statements, right.Statements, ignoreChildNode: HasLabel); + } - #region Comparisons + private static SyntaxNode? GetBody(SyntaxNode node) + { + switch (node) + { + case BaseMethodDeclarationSyntax baseMethodDeclarationSyntax: return baseMethodDeclarationSyntax.Body ?? (SyntaxNode?)baseMethodDeclarationSyntax.ExpressionBody?.Expression; + case AccessorDeclarationSyntax accessorDeclarationSyntax: return accessorDeclarationSyntax.Body ?? (SyntaxNode?)accessorDeclarationSyntax.ExpressionBody?.Expression; + default: throw ExceptionUtilities.UnexpectedValue(node); + } + } - public override bool ValuesEqual(SyntaxNode left, SyntaxNode right) + protected override bool TryComputeWeightedDistance(SyntaxNode leftNode, SyntaxNode rightNode, out double distance) + { + switch (leftNode.Kind()) { - Func? ignoreChildFunction; - switch (left.Kind()) - { - // all syntax kinds with a method body child: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.ConversionOperatorDeclaration: - case SyntaxKind.OperatorDeclaration: - case SyntaxKind.ConstructorDeclaration: - case SyntaxKind.DestructorDeclaration: - case SyntaxKind.GetAccessorDeclaration: - case SyntaxKind.SetAccessorDeclaration: - case SyntaxKind.InitAccessorDeclaration: - case SyntaxKind.AddAccessorDeclaration: - case SyntaxKind.RemoveAccessorDeclaration: - // When comparing method bodies we need to NOT ignore VariableDeclaration and VariableDeclarator children, - // but when comparing field definitions we should ignore VariableDeclarations children. - - var leftBody = GetBody(left); - var rightBody = GetBody(right); - - if (!SyntaxFactory.AreEquivalent(leftBody, rightBody, null)) - { - return false; - } + case SyntaxKind.VariableDeclarator: + distance = ComputeDistance( + ((VariableDeclaratorSyntax)leftNode).Identifier, + ((VariableDeclaratorSyntax)rightNode).Identifier); + return true; - ignoreChildFunction = childKind => childKind == SyntaxKind.Block || childKind == SyntaxKind.ArrowExpressionClause || HasLabel(childKind); - break; + case SyntaxKind.ForStatement: + var leftFor = (ForStatementSyntax)leftNode; + var rightFor = (ForStatementSyntax)rightNode; + distance = ComputeWeightedDistance(leftFor, rightFor); + return true; - case SyntaxKind.SwitchSection: - return Equal((SwitchSectionSyntax)left, (SwitchSectionSyntax)right); + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + { - case SyntaxKind.ForStatement: - // The only children of ForStatement are labeled nodes and punctuation. + var leftForEach = (CommonForEachStatementSyntax)leftNode; + var rightForEach = (CommonForEachStatementSyntax)rightNode; + distance = ComputeWeightedDistance(leftForEach, rightForEach); return true; + } + + case SyntaxKind.UsingStatement: + { + var leftUsing = (UsingStatementSyntax)leftNode; + var rightUsing = (UsingStatementSyntax)rightNode; - default: - if (HasChildren(left)) + if (leftUsing.Declaration != null && rightUsing.Declaration != null) { - ignoreChildFunction = childKind => HasLabel(childKind); + distance = ComputeWeightedDistance( + leftUsing.Declaration, + leftUsing.Statement, + rightUsing.Declaration, + rightUsing.Statement); } else { - ignoreChildFunction = null; + distance = ComputeWeightedDistance( + (SyntaxNode?)leftUsing.Expression ?? leftUsing.Declaration!, + leftUsing.Statement, + (SyntaxNode?)rightUsing.Expression ?? rightUsing.Declaration!, + rightUsing.Statement); } - break; - } + return true; + } - return SyntaxFactory.AreEquivalent(left, right, ignoreChildFunction); - } + case SyntaxKind.UsingDirective: + { + var leftUsing = (UsingDirectiveSyntax)leftNode; + var rightUsing = (UsingDirectiveSyntax)rightNode; - private bool Equal(SwitchSectionSyntax left, SwitchSectionSyntax right) - { - return SyntaxFactory.AreEquivalent(left.Labels, right.Labels, null) - && SyntaxFactory.AreEquivalent(left.Statements, right.Statements, ignoreChildNode: HasLabel); - } + // For now, just compute the distances of both the alias and name and combine their weights + // 50/50. We could consider weighting the alias more heavily. i.e. if you have `using X = ...` + // and `using X = ...` it's more likely that this is the same alias, and just the name portion + // changed versus thinking that some other using became this alias. + distance = + ComputeDistance(leftUsing.Alias, rightUsing.Alias) + + ComputeDistance(leftUsing.NamespaceOrType, rightUsing.NamespaceOrType); - private static SyntaxNode? GetBody(SyntaxNode node) - { - switch (node) - { - case BaseMethodDeclarationSyntax baseMethodDeclarationSyntax: return baseMethodDeclarationSyntax.Body ?? (SyntaxNode?)baseMethodDeclarationSyntax.ExpressionBody?.Expression; - case AccessorDeclarationSyntax accessorDeclarationSyntax: return accessorDeclarationSyntax.Body ?? (SyntaxNode?)accessorDeclarationSyntax.ExpressionBody?.Expression; - default: throw ExceptionUtilities.UnexpectedValue(node); - } - } + // Consider two usings that only differ by presence/absence of 'global' to be a near match. + if (leftUsing.GlobalKeyword.IsKind(SyntaxKind.None) != rightUsing.GlobalKeyword.IsKind(SyntaxKind.None)) + distance += EpsilonDist; - protected override bool TryComputeWeightedDistance(SyntaxNode leftNode, SyntaxNode rightNode, out double distance) - { - switch (leftNode.Kind()) - { - case SyntaxKind.VariableDeclarator: - distance = ComputeDistance( - ((VariableDeclaratorSyntax)leftNode).Identifier, - ((VariableDeclaratorSyntax)rightNode).Identifier); - return true; + // Consider two usings that only differ by presence/absence of 'unsafe' to be a near match. + if (leftUsing.UnsafeKeyword.IsKind(SyntaxKind.None) != rightUsing.UnsafeKeyword.IsKind(SyntaxKind.None)) + distance += EpsilonDist; - case SyntaxKind.ForStatement: - var leftFor = (ForStatementSyntax)leftNode; - var rightFor = (ForStatementSyntax)rightNode; - distance = ComputeWeightedDistance(leftFor, rightFor); return true; + } - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - { - - var leftForEach = (CommonForEachStatementSyntax)leftNode; - var rightForEach = (CommonForEachStatementSyntax)rightNode; - distance = ComputeWeightedDistance(leftForEach, rightForEach); - return true; - } - - case SyntaxKind.UsingStatement: - { - var leftUsing = (UsingStatementSyntax)leftNode; - var rightUsing = (UsingStatementSyntax)rightNode; - - if (leftUsing.Declaration != null && rightUsing.Declaration != null) - { - distance = ComputeWeightedDistance( - leftUsing.Declaration, - leftUsing.Statement, - rightUsing.Declaration, - rightUsing.Statement); - } - else - { - distance = ComputeWeightedDistance( - (SyntaxNode?)leftUsing.Expression ?? leftUsing.Declaration!, - leftUsing.Statement, - (SyntaxNode?)rightUsing.Expression ?? rightUsing.Declaration!, - rightUsing.Statement); - } - - return true; - } + case SyntaxKind.LockStatement: + var leftLock = (LockStatementSyntax)leftNode; + var rightLock = (LockStatementSyntax)rightNode; + distance = ComputeWeightedDistance(leftLock.Expression, leftLock.Statement, rightLock.Expression, rightLock.Statement); + return true; - case SyntaxKind.UsingDirective: - { - var leftUsing = (UsingDirectiveSyntax)leftNode; - var rightUsing = (UsingDirectiveSyntax)rightNode; - - // For now, just compute the distances of both the alias and name and combine their weights - // 50/50. We could consider weighting the alias more heavily. i.e. if you have `using X = ...` - // and `using X = ...` it's more likely that this is the same alias, and just the name portion - // changed versus thinking that some other using became this alias. - distance = - ComputeDistance(leftUsing.Alias, rightUsing.Alias) + - ComputeDistance(leftUsing.NamespaceOrType, rightUsing.NamespaceOrType); - - // Consider two usings that only differ by presence/absence of 'global' to be a near match. - if (leftUsing.GlobalKeyword.IsKind(SyntaxKind.None) != rightUsing.GlobalKeyword.IsKind(SyntaxKind.None)) - distance += EpsilonDist; - - // Consider two usings that only differ by presence/absence of 'unsafe' to be a near match. - if (leftUsing.UnsafeKeyword.IsKind(SyntaxKind.None) != rightUsing.UnsafeKeyword.IsKind(SyntaxKind.None)) - distance += EpsilonDist; - - return true; - } + case SyntaxKind.FixedStatement: + var leftFixed = (FixedStatementSyntax)leftNode; + var rightFixed = (FixedStatementSyntax)rightNode; + distance = ComputeWeightedDistance(leftFixed.Declaration, leftFixed.Statement, rightFixed.Declaration, rightFixed.Statement); + return true; - case SyntaxKind.LockStatement: - var leftLock = (LockStatementSyntax)leftNode; - var rightLock = (LockStatementSyntax)rightNode; - distance = ComputeWeightedDistance(leftLock.Expression, leftLock.Statement, rightLock.Expression, rightLock.Statement); - return true; + case SyntaxKind.WhileStatement: + var leftWhile = (WhileStatementSyntax)leftNode; + var rightWhile = (WhileStatementSyntax)rightNode; + distance = ComputeWeightedDistance(leftWhile.Condition, leftWhile.Statement, rightWhile.Condition, rightWhile.Statement); + return true; - case SyntaxKind.FixedStatement: - var leftFixed = (FixedStatementSyntax)leftNode; - var rightFixed = (FixedStatementSyntax)rightNode; - distance = ComputeWeightedDistance(leftFixed.Declaration, leftFixed.Statement, rightFixed.Declaration, rightFixed.Statement); - return true; + case SyntaxKind.DoStatement: + var leftDo = (DoStatementSyntax)leftNode; + var rightDo = (DoStatementSyntax)rightNode; + distance = ComputeWeightedDistance(leftDo.Condition, leftDo.Statement, rightDo.Condition, rightDo.Statement); + return true; - case SyntaxKind.WhileStatement: - var leftWhile = (WhileStatementSyntax)leftNode; - var rightWhile = (WhileStatementSyntax)rightNode; - distance = ComputeWeightedDistance(leftWhile.Condition, leftWhile.Statement, rightWhile.Condition, rightWhile.Statement); - return true; + case SyntaxKind.IfStatement: + var leftIf = (IfStatementSyntax)leftNode; + var rightIf = (IfStatementSyntax)rightNode; + distance = ComputeWeightedDistance(leftIf.Condition, leftIf.Statement, rightIf.Condition, rightIf.Statement); + return true; - case SyntaxKind.DoStatement: - var leftDo = (DoStatementSyntax)leftNode; - var rightDo = (DoStatementSyntax)rightNode; - distance = ComputeWeightedDistance(leftDo.Condition, leftDo.Statement, rightDo.Condition, rightDo.Statement); - return true; + case SyntaxKind.Block: + var leftBlock = (BlockSyntax)leftNode; + var rightBlock = (BlockSyntax)rightNode; + return TryComputeWeightedDistance(leftBlock, rightBlock, out distance); - case SyntaxKind.IfStatement: - var leftIf = (IfStatementSyntax)leftNode; - var rightIf = (IfStatementSyntax)rightNode; - distance = ComputeWeightedDistance(leftIf.Condition, leftIf.Statement, rightIf.Condition, rightIf.Statement); - return true; + case SyntaxKind.CatchClause: + distance = ComputeWeightedDistance((CatchClauseSyntax)leftNode, (CatchClauseSyntax)rightNode); + return true; - case SyntaxKind.Block: - var leftBlock = (BlockSyntax)leftNode; - var rightBlock = (BlockSyntax)rightNode; - return TryComputeWeightedDistance(leftBlock, rightBlock, out distance); + case SyntaxKind.ParenthesizedLambdaExpression: + case SyntaxKind.SimpleLambdaExpression: + case SyntaxKind.AnonymousMethodExpression: + case SyntaxKind.LocalFunctionStatement: + distance = ComputeWeightedDistanceOfNestedFunctions(leftNode, rightNode); + return true; - case SyntaxKind.CatchClause: - distance = ComputeWeightedDistance((CatchClauseSyntax)leftNode, (CatchClauseSyntax)rightNode); - return true; + case SyntaxKind.SingleVariableDesignation: + distance = ComputeWeightedDistance((SingleVariableDesignationSyntax)leftNode, (SingleVariableDesignationSyntax)rightNode); + return true; - case SyntaxKind.ParenthesizedLambdaExpression: - case SyntaxKind.SimpleLambdaExpression: - case SyntaxKind.AnonymousMethodExpression: - case SyntaxKind.LocalFunctionStatement: - distance = ComputeWeightedDistanceOfNestedFunctions(leftNode, rightNode); - return true; + case SyntaxKind.TypeParameterConstraintClause: + distance = ComputeDistance((TypeParameterConstraintClauseSyntax)leftNode, (TypeParameterConstraintClauseSyntax)rightNode); + return true; - case SyntaxKind.SingleVariableDesignation: - distance = ComputeWeightedDistance((SingleVariableDesignationSyntax)leftNode, (SingleVariableDesignationSyntax)rightNode); - return true; + case SyntaxKind.TypeParameter: + distance = ComputeDistance((TypeParameterSyntax)leftNode, (TypeParameterSyntax)rightNode); + return true; - case SyntaxKind.TypeParameterConstraintClause: - distance = ComputeDistance((TypeParameterConstraintClauseSyntax)leftNode, (TypeParameterConstraintClauseSyntax)rightNode); - return true; + case SyntaxKind.Parameter: + distance = ComputeDistance((ParameterSyntax)leftNode, (ParameterSyntax)rightNode); + return true; - case SyntaxKind.TypeParameter: - distance = ComputeDistance((TypeParameterSyntax)leftNode, (TypeParameterSyntax)rightNode); - return true; + case SyntaxKind.AttributeList: + distance = ComputeDistance((AttributeListSyntax)leftNode, (AttributeListSyntax)rightNode); + return true; - case SyntaxKind.Parameter: - distance = ComputeDistance((ParameterSyntax)leftNode, (ParameterSyntax)rightNode); - return true; + case SyntaxKind.Attribute: + distance = ComputeDistance((AttributeSyntax)leftNode, (AttributeSyntax)rightNode); + return true; - case SyntaxKind.AttributeList: - distance = ComputeDistance((AttributeListSyntax)leftNode, (AttributeListSyntax)rightNode); - return true; + default: + var leftName = TryGetName(leftNode); + var rightName = TryGetName(rightNode); + Contract.ThrowIfFalse(rightName.HasValue == leftName.HasValue); - case SyntaxKind.Attribute: - distance = ComputeDistance((AttributeSyntax)leftNode, (AttributeSyntax)rightNode); + if (leftName.HasValue) + { + distance = ComputeDistance(leftName.Value, rightName!.Value); return true; + } + else + { + distance = 0; + return false; + } + } + } - default: - var leftName = TryGetName(leftNode); - var rightName = TryGetName(rightNode); - Contract.ThrowIfFalse(rightName.HasValue == leftName.HasValue); + private static double ComputeWeightedDistanceOfNestedFunctions(SyntaxNode leftNode, SyntaxNode rightNode) + { + GetNestedFunctionsParts(leftNode, out var leftParameters, out var leftAsync, out var leftBody, out var leftModifiers, out var leftReturnType, out var leftIdentifier, out var leftTypeParameters); + GetNestedFunctionsParts(rightNode, out var rightParameters, out var rightAsync, out var rightBody, out var rightModifiers, out var rightReturnType, out var rightIdentifier, out var rightTypeParameters); - if (leftName.HasValue) - { - distance = ComputeDistance(leftName.Value, rightName!.Value); - return true; - } - else - { - distance = 0; - return false; - } - } + if ((leftAsync.Kind() == SyntaxKind.AsyncKeyword) != (rightAsync.Kind() == SyntaxKind.AsyncKeyword)) + { + return 1.0; } - private static double ComputeWeightedDistanceOfNestedFunctions(SyntaxNode leftNode, SyntaxNode rightNode) - { - GetNestedFunctionsParts(leftNode, out var leftParameters, out var leftAsync, out var leftBody, out var leftModifiers, out var leftReturnType, out var leftIdentifier, out var leftTypeParameters); - GetNestedFunctionsParts(rightNode, out var rightParameters, out var rightAsync, out var rightBody, out var rightModifiers, out var rightReturnType, out var rightIdentifier, out var rightTypeParameters); + var modifierDistance = ComputeDistance(leftModifiers, rightModifiers); + var returnTypeDistance = ComputeDistance(leftReturnType, rightReturnType); + var identifierDistance = ComputeDistance(leftIdentifier, rightIdentifier); + var typeParameterDistance = ComputeDistance(leftTypeParameters, rightTypeParameters); + var parameterDistance = ComputeDistance(leftParameters, rightParameters); + var bodyDistance = ComputeDistance(leftBody, rightBody); + + return + modifierDistance * 0.1 + + returnTypeDistance * 0.1 + + identifierDistance * 0.2 + + typeParameterDistance * 0.2 + + parameterDistance * 0.2 + + bodyDistance * 0.2; + } - if ((leftAsync.Kind() == SyntaxKind.AsyncKeyword) != (rightAsync.Kind() == SyntaxKind.AsyncKeyword)) - { - return 1.0; - } + private static void GetNestedFunctionsParts( + SyntaxNode nestedFunction, + out IEnumerable parameters, + out SyntaxToken asyncKeyword, + out SyntaxNode body, + out SyntaxTokenList modifiers, + out TypeSyntax? returnType, + out SyntaxToken identifier, + out TypeParameterListSyntax? typeParameters) + { + switch (nestedFunction.Kind()) + { + case SyntaxKind.SimpleLambdaExpression: + var simple = (SimpleLambdaExpressionSyntax)nestedFunction; + parameters = simple.Parameter.DescendantTokens(); + asyncKeyword = simple.AsyncKeyword; + body = simple.Body; + modifiers = default; + returnType = null; + identifier = default; + typeParameters = null; + break; + + case SyntaxKind.ParenthesizedLambdaExpression: + var parenthesized = (ParenthesizedLambdaExpressionSyntax)nestedFunction; + parameters = GetDescendantTokensIgnoringSeparators(parenthesized.ParameterList.Parameters); + asyncKeyword = parenthesized.AsyncKeyword; + body = parenthesized.Body; + modifiers = default; + returnType = null; + identifier = default; + typeParameters = null; + break; + + case SyntaxKind.AnonymousMethodExpression: + var anonymous = (AnonymousMethodExpressionSyntax)nestedFunction; + if (anonymous.ParameterList != null) + { + parameters = GetDescendantTokensIgnoringSeparators(anonymous.ParameterList.Parameters); + } + else + { + parameters = SpecializedCollections.EmptyEnumerable(); + } - var modifierDistance = ComputeDistance(leftModifiers, rightModifiers); - var returnTypeDistance = ComputeDistance(leftReturnType, rightReturnType); - var identifierDistance = ComputeDistance(leftIdentifier, rightIdentifier); - var typeParameterDistance = ComputeDistance(leftTypeParameters, rightTypeParameters); - var parameterDistance = ComputeDistance(leftParameters, rightParameters); - var bodyDistance = ComputeDistance(leftBody, rightBody); - - return - modifierDistance * 0.1 + - returnTypeDistance * 0.1 + - identifierDistance * 0.2 + - typeParameterDistance * 0.2 + - parameterDistance * 0.2 + - bodyDistance * 0.2; + asyncKeyword = anonymous.AsyncKeyword; + body = anonymous.Block; + modifiers = default; + returnType = null; + identifier = default; + typeParameters = null; + break; + + case SyntaxKind.LocalFunctionStatement: + var localFunction = (LocalFunctionStatementSyntax)nestedFunction; + parameters = GetDescendantTokensIgnoringSeparators(localFunction.ParameterList.Parameters); + asyncKeyword = default; + body = (SyntaxNode?)localFunction.Body ?? localFunction.ExpressionBody!; + modifiers = localFunction.Modifiers; + returnType = localFunction.ReturnType; + identifier = localFunction.Identifier; + typeParameters = localFunction.TypeParameterList; + break; + + default: + throw ExceptionUtilities.UnexpectedValue(nestedFunction.Kind()); } + } - private static void GetNestedFunctionsParts( - SyntaxNode nestedFunction, - out IEnumerable parameters, - out SyntaxToken asyncKeyword, - out SyntaxNode body, - out SyntaxTokenList modifiers, - out TypeSyntax? returnType, - out SyntaxToken identifier, - out TypeParameterListSyntax? typeParameters) + private bool TryComputeWeightedDistance(BlockSyntax leftBlock, BlockSyntax rightBlock, out double distance) + { + // No block can be matched with the root block. + // Note that in constructors the root is the constructor declaration, since we need to include + // the constructor initializer in the match. + if (leftBlock.Parent == null || + rightBlock.Parent == null || + leftBlock.Parent.IsKind(SyntaxKind.ConstructorDeclaration) || + rightBlock.Parent.IsKind(SyntaxKind.ConstructorDeclaration)) { - switch (nestedFunction.Kind()) - { - case SyntaxKind.SimpleLambdaExpression: - var simple = (SimpleLambdaExpressionSyntax)nestedFunction; - parameters = simple.Parameter.DescendantTokens(); - asyncKeyword = simple.AsyncKeyword; - body = simple.Body; - modifiers = default; - returnType = null; - identifier = default; - typeParameters = null; - break; - - case SyntaxKind.ParenthesizedLambdaExpression: - var parenthesized = (ParenthesizedLambdaExpressionSyntax)nestedFunction; - parameters = GetDescendantTokensIgnoringSeparators(parenthesized.ParameterList.Parameters); - asyncKeyword = parenthesized.AsyncKeyword; - body = parenthesized.Body; - modifiers = default; - returnType = null; - identifier = default; - typeParameters = null; - break; - - case SyntaxKind.AnonymousMethodExpression: - var anonymous = (AnonymousMethodExpressionSyntax)nestedFunction; - if (anonymous.ParameterList != null) - { - parameters = GetDescendantTokensIgnoringSeparators(anonymous.ParameterList.Parameters); - } - else - { - parameters = SpecializedCollections.EmptyEnumerable(); - } + distance = 0.0; + return true; + } - asyncKeyword = anonymous.AsyncKeyword; - body = anonymous.Block; - modifiers = default; - returnType = null; - identifier = default; - typeParameters = null; - break; - - case SyntaxKind.LocalFunctionStatement: - var localFunction = (LocalFunctionStatementSyntax)nestedFunction; - parameters = GetDescendantTokensIgnoringSeparators(localFunction.ParameterList.Parameters); - asyncKeyword = default; - body = (SyntaxNode?)localFunction.Body ?? localFunction.ExpressionBody!; - modifiers = localFunction.Modifiers; - returnType = localFunction.ReturnType; - identifier = localFunction.Identifier; - typeParameters = localFunction.TypeParameterList; - break; - - default: - throw ExceptionUtilities.UnexpectedValue(nestedFunction.Kind()); - } + if (GetLabel(leftBlock.Parent) != GetLabel(rightBlock.Parent)) + { + distance = 0.2 + 0.8 * ComputeWeightedBlockDistance(leftBlock, rightBlock); + return true; } - private bool TryComputeWeightedDistance(BlockSyntax leftBlock, BlockSyntax rightBlock, out double distance) + switch (leftBlock.Parent.Kind()) { - // No block can be matched with the root block. - // Note that in constructors the root is the constructor declaration, since we need to include - // the constructor initializer in the match. - if (leftBlock.Parent == null || - rightBlock.Parent == null || - leftBlock.Parent.IsKind(SyntaxKind.ConstructorDeclaration) || - rightBlock.Parent.IsKind(SyntaxKind.ConstructorDeclaration)) - { - distance = 0.0; + case SyntaxKind.IfStatement: + case SyntaxKind.ForEachStatement: + case SyntaxKind.ForEachVariableStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.FixedStatement: + case SyntaxKind.LockStatement: + case SyntaxKind.UsingStatement: + case SyntaxKind.SwitchSection: + case SyntaxKind.ParenthesizedLambdaExpression: + case SyntaxKind.SimpleLambdaExpression: + case SyntaxKind.AnonymousMethodExpression: + case SyntaxKind.LocalFunctionStatement: + // value distance of the block body is included: + distance = GetDistance(leftBlock.Parent, rightBlock.Parent); return true; - } - if (GetLabel(leftBlock.Parent) != GetLabel(rightBlock.Parent)) - { - distance = 0.2 + 0.8 * ComputeWeightedBlockDistance(leftBlock, rightBlock); - return true; - } + case SyntaxKind.CatchClause: + var leftCatch = (CatchClauseSyntax)leftBlock.Parent; + var rightCatch = (CatchClauseSyntax)rightBlock.Parent; + if (leftCatch.Declaration == null && leftCatch.Filter == null && + rightCatch.Declaration == null && rightCatch.Filter == null) + { + var leftTry = (TryStatementSyntax)leftCatch.Parent!; + var rightTry = (TryStatementSyntax)rightCatch.Parent!; - switch (leftBlock.Parent.Kind()) - { - case SyntaxKind.IfStatement: - case SyntaxKind.ForEachStatement: - case SyntaxKind.ForEachVariableStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.FixedStatement: - case SyntaxKind.LockStatement: - case SyntaxKind.UsingStatement: - case SyntaxKind.SwitchSection: - case SyntaxKind.ParenthesizedLambdaExpression: - case SyntaxKind.SimpleLambdaExpression: - case SyntaxKind.AnonymousMethodExpression: - case SyntaxKind.LocalFunctionStatement: + distance = 0.5 * ComputeValueDistance(leftTry.Block, rightTry.Block) + + 0.5 * ComputeValueDistance(leftBlock, rightBlock); + } + else + { // value distance of the block body is included: distance = GetDistance(leftBlock.Parent, rightBlock.Parent); - return true; - - case SyntaxKind.CatchClause: - var leftCatch = (CatchClauseSyntax)leftBlock.Parent; - var rightCatch = (CatchClauseSyntax)rightBlock.Parent; - if (leftCatch.Declaration == null && leftCatch.Filter == null && - rightCatch.Declaration == null && rightCatch.Filter == null) - { - var leftTry = (TryStatementSyntax)leftCatch.Parent!; - var rightTry = (TryStatementSyntax)rightCatch.Parent!; - - distance = 0.5 * ComputeValueDistance(leftTry.Block, rightTry.Block) + - 0.5 * ComputeValueDistance(leftBlock, rightBlock); - } - else - { - // value distance of the block body is included: - distance = GetDistance(leftBlock.Parent, rightBlock.Parent); - } + } - return true; + return true; - case SyntaxKind.Block: - case SyntaxKind.LabeledStatement: - case SyntaxKind.GlobalStatement: - distance = ComputeWeightedBlockDistance(leftBlock, rightBlock); - return true; + case SyntaxKind.Block: + case SyntaxKind.LabeledStatement: + case SyntaxKind.GlobalStatement: + distance = ComputeWeightedBlockDistance(leftBlock, rightBlock); + return true; - case SyntaxKind.UnsafeStatement: - case SyntaxKind.CheckedStatement: - case SyntaxKind.UncheckedStatement: - case SyntaxKind.ElseClause: - case SyntaxKind.FinallyClause: - case SyntaxKind.TryStatement: - distance = 0.2 * ComputeValueDistance(leftBlock, rightBlock); - return true; + case SyntaxKind.UnsafeStatement: + case SyntaxKind.CheckedStatement: + case SyntaxKind.UncheckedStatement: + case SyntaxKind.ElseClause: + case SyntaxKind.FinallyClause: + case SyntaxKind.TryStatement: + distance = 0.2 * ComputeValueDistance(leftBlock, rightBlock); + return true; - default: - throw ExceptionUtilities.UnexpectedValue(leftBlock.Parent.Kind()); - } + default: + throw ExceptionUtilities.UnexpectedValue(leftBlock.Parent.Kind()); } + } - private double ComputeWeightedDistance(SingleVariableDesignationSyntax leftNode, SingleVariableDesignationSyntax rightNode) - { - var distance = ComputeDistance(leftNode, rightNode); - double parentDistance; - - if (leftNode.Parent != null && - rightNode.Parent != null && - GetLabel(leftNode.Parent) == GetLabel(rightNode.Parent)) - { - parentDistance = ComputeDistance(leftNode.Parent, rightNode.Parent); - } - else - { - parentDistance = 1; - } + private double ComputeWeightedDistance(SingleVariableDesignationSyntax leftNode, SingleVariableDesignationSyntax rightNode) + { + var distance = ComputeDistance(leftNode, rightNode); + double parentDistance; - return 0.5 * parentDistance + 0.5 * distance; + if (leftNode.Parent != null && + rightNode.Parent != null && + GetLabel(leftNode.Parent) == GetLabel(rightNode.Parent)) + { + parentDistance = ComputeDistance(leftNode.Parent, rightNode.Parent); } - - private static double ComputeWeightedBlockDistance(BlockSyntax leftBlock, BlockSyntax rightBlock) + else { - if (TryComputeLocalsDistance(leftBlock, rightBlock, out var distance)) - { - return distance; - } - - return ComputeValueDistance(leftBlock, rightBlock); + parentDistance = 1; } - private static double ComputeWeightedDistance(CatchClauseSyntax left, CatchClauseSyntax right) + return 0.5 * parentDistance + 0.5 * distance; + } + + private static double ComputeWeightedBlockDistance(BlockSyntax leftBlock, BlockSyntax rightBlock) + { + if (TryComputeLocalsDistance(leftBlock, rightBlock, out var distance)) { - var blockDistance = ComputeDistance(left.Block, right.Block); - var distance = CombineOptional(blockDistance, left.Declaration, right.Declaration, left.Filter, right.Filter); - return AdjustForLocalsInBlock(distance, left.Block, right.Block, localsWeight: 0.3); + return distance; } - private static double ComputeWeightedDistance( - CommonForEachStatementSyntax leftCommonForEach, - CommonForEachStatementSyntax rightCommonForEach) - { - var statementDistance = ComputeDistance(leftCommonForEach.Statement, rightCommonForEach.Statement); - var expressionDistance = ComputeDistance(leftCommonForEach.Expression, rightCommonForEach.Expression); + return ComputeValueDistance(leftBlock, rightBlock); + } - List? leftLocals = null; - List? rightLocals = null; - GetLocalNames(leftCommonForEach, ref leftLocals); - GetLocalNames(rightCommonForEach, ref rightLocals); + private static double ComputeWeightedDistance(CatchClauseSyntax left, CatchClauseSyntax right) + { + var blockDistance = ComputeDistance(left.Block, right.Block); + var distance = CombineOptional(blockDistance, left.Declaration, right.Declaration, left.Filter, right.Filter); + return AdjustForLocalsInBlock(distance, left.Block, right.Block, localsWeight: 0.3); + } - var localNamesDistance = ComputeDistance(leftLocals, rightLocals); + private static double ComputeWeightedDistance( + CommonForEachStatementSyntax leftCommonForEach, + CommonForEachStatementSyntax rightCommonForEach) + { + var statementDistance = ComputeDistance(leftCommonForEach.Statement, rightCommonForEach.Statement); + var expressionDistance = ComputeDistance(leftCommonForEach.Expression, rightCommonForEach.Expression); - var distance = localNamesDistance * 0.6 + expressionDistance * 0.2 + statementDistance * 0.2; - return AdjustForLocalsInBlock(distance, leftCommonForEach.Statement, rightCommonForEach.Statement, localsWeight: 0.6); - } + List? leftLocals = null; + List? rightLocals = null; + GetLocalNames(leftCommonForEach, ref leftLocals); + GetLocalNames(rightCommonForEach, ref rightLocals); - private static double ComputeWeightedDistance(ForStatementSyntax left, ForStatementSyntax right) - { - var statementDistance = ComputeDistance(left.Statement, right.Statement); - var conditionDistance = ComputeDistance(left.Condition, right.Condition); + var localNamesDistance = ComputeDistance(leftLocals, rightLocals); - var incDistance = ComputeDistance( - GetDescendantTokensIgnoringSeparators(left.Incrementors), GetDescendantTokensIgnoringSeparators(right.Incrementors)); + var distance = localNamesDistance * 0.6 + expressionDistance * 0.2 + statementDistance * 0.2; + return AdjustForLocalsInBlock(distance, leftCommonForEach.Statement, rightCommonForEach.Statement, localsWeight: 0.6); + } - var distance = conditionDistance * 0.3 + incDistance * 0.3 + statementDistance * 0.4; - if (TryComputeLocalsDistance(left.Declaration, right.Declaration, out var localsDistance)) - { - distance = distance * 0.4 + localsDistance * 0.6; - } + private static double ComputeWeightedDistance(ForStatementSyntax left, ForStatementSyntax right) + { + var statementDistance = ComputeDistance(left.Statement, right.Statement); + var conditionDistance = ComputeDistance(left.Condition, right.Condition); - return distance; - } + var incDistance = ComputeDistance( + GetDescendantTokensIgnoringSeparators(left.Incrementors), GetDescendantTokensIgnoringSeparators(right.Incrementors)); - private static double ComputeWeightedDistance( - VariableDeclarationSyntax leftVariables, - StatementSyntax leftStatement, - VariableDeclarationSyntax rightVariables, - StatementSyntax rightStatement) + var distance = conditionDistance * 0.3 + incDistance * 0.3 + statementDistance * 0.4; + if (TryComputeLocalsDistance(left.Declaration, right.Declaration, out var localsDistance)) { - var distance = ComputeDistance(leftStatement, rightStatement); - // Put maximum weight behind the variables declared in the header of the statement. - if (TryComputeLocalsDistance(leftVariables, rightVariables, out var localsDistance)) - { - distance = distance * 0.4 + localsDistance * 0.6; - } - - // If the statement is a block that declares local variables, - // weight them more than the rest of the statement. - return AdjustForLocalsInBlock(distance, leftStatement, rightStatement, localsWeight: 0.2); + distance = distance * 0.4 + localsDistance * 0.6; } - private static double ComputeWeightedDistance( - SyntaxNode? leftHeader, - StatementSyntax leftStatement, - SyntaxNode? rightHeader, - StatementSyntax rightStatement) - { - var headerDistance = ComputeDistance(leftHeader, rightHeader); - var statementDistance = ComputeDistance(leftStatement, rightStatement); - var distance = headerDistance * 0.6 + statementDistance * 0.4; + return distance; + } - return AdjustForLocalsInBlock(distance, leftStatement, rightStatement, localsWeight: 0.5); + private static double ComputeWeightedDistance( + VariableDeclarationSyntax leftVariables, + StatementSyntax leftStatement, + VariableDeclarationSyntax rightVariables, + StatementSyntax rightStatement) + { + var distance = ComputeDistance(leftStatement, rightStatement); + // Put maximum weight behind the variables declared in the header of the statement. + if (TryComputeLocalsDistance(leftVariables, rightVariables, out var localsDistance)) + { + distance = distance * 0.4 + localsDistance * 0.6; } - private static double AdjustForLocalsInBlock( - double distance, - StatementSyntax leftStatement, - StatementSyntax rightStatement, - double localsWeight) - { - // If the statement is a block that declares local variables, - // weight them more than the rest of the statement. - if (leftStatement.Kind() == SyntaxKind.Block && rightStatement.Kind() == SyntaxKind.Block) - { - if (TryComputeLocalsDistance((BlockSyntax)leftStatement, (BlockSyntax)rightStatement, out var localsDistance)) - { - return localsDistance * localsWeight + distance * (1 - localsWeight); - } - } + // If the statement is a block that declares local variables, + // weight them more than the rest of the statement. + return AdjustForLocalsInBlock(distance, leftStatement, rightStatement, localsWeight: 0.2); + } - return distance; - } + private static double ComputeWeightedDistance( + SyntaxNode? leftHeader, + StatementSyntax leftStatement, + SyntaxNode? rightHeader, + StatementSyntax rightStatement) + { + var headerDistance = ComputeDistance(leftHeader, rightHeader); + var statementDistance = ComputeDistance(leftStatement, rightStatement); + var distance = headerDistance * 0.6 + statementDistance * 0.4; - private static bool TryComputeLocalsDistance(VariableDeclarationSyntax? left, VariableDeclarationSyntax? right, out double distance) - { - List? leftLocals = null; - List? rightLocals = null; + return AdjustForLocalsInBlock(distance, leftStatement, rightStatement, localsWeight: 0.5); + } - if (left != null) + private static double AdjustForLocalsInBlock( + double distance, + StatementSyntax leftStatement, + StatementSyntax rightStatement, + double localsWeight) + { + // If the statement is a block that declares local variables, + // weight them more than the rest of the statement. + if (leftStatement.Kind() == SyntaxKind.Block && rightStatement.Kind() == SyntaxKind.Block) + { + if (TryComputeLocalsDistance((BlockSyntax)leftStatement, (BlockSyntax)rightStatement, out var localsDistance)) { - GetLocalNames(left, ref leftLocals); + return localsDistance * localsWeight + distance * (1 - localsWeight); } + } - if (right != null) - { - GetLocalNames(right, ref rightLocals); - } + return distance; + } - if (leftLocals == null || rightLocals == null) - { - distance = 0; - return false; - } + private static bool TryComputeLocalsDistance(VariableDeclarationSyntax? left, VariableDeclarationSyntax? right, out double distance) + { + List? leftLocals = null; + List? rightLocals = null; - distance = ComputeDistance(leftLocals, rightLocals); - return true; + if (left != null) + { + GetLocalNames(left, ref leftLocals); } - private static bool TryComputeLocalsDistance(BlockSyntax left, BlockSyntax right, out double distance) + if (right != null) { - List? leftLocals = null; - List? rightLocals = null; - - GetLocalNames(left, ref leftLocals); GetLocalNames(right, ref rightLocals); + } - if (leftLocals == null || rightLocals == null) - { - distance = 0; - return false; - } + if (leftLocals == null || rightLocals == null) + { + distance = 0; + return false; + } - distance = ComputeDistance(leftLocals, rightLocals); - return true; + distance = ComputeDistance(leftLocals, rightLocals); + return true; + } + + private static bool TryComputeLocalsDistance(BlockSyntax left, BlockSyntax right, out double distance) + { + List? leftLocals = null; + List? rightLocals = null; + + GetLocalNames(left, ref leftLocals); + GetLocalNames(right, ref rightLocals); + + if (leftLocals == null || rightLocals == null) + { + distance = 0; + return false; } - // Doesn't include variables declared in declaration expressions - // Consider including them (https://github.com/dotnet/roslyn/issues/37460). - private static void GetLocalNames(BlockSyntax block, ref List? result) + distance = ComputeDistance(leftLocals, rightLocals); + return true; + } + + // Doesn't include variables declared in declaration expressions + // Consider including them (https://github.com/dotnet/roslyn/issues/37460). + private static void GetLocalNames(BlockSyntax block, ref List? result) + { + foreach (var child in block.ChildNodes()) { - foreach (var child in block.ChildNodes()) + if (child is LocalDeclarationStatementSyntax localDecl) { - if (child is LocalDeclarationStatementSyntax localDecl) - { - GetLocalNames(localDecl.Declaration, ref result); - } + GetLocalNames(localDecl.Declaration, ref result); } } + } - // Doesn't include variables declared in declaration expressions - // Consider including them (https://github.com/dotnet/roslyn/issues/37460). - private static void GetLocalNames(VariableDeclarationSyntax localDeclaration, ref List? result) + // Doesn't include variables declared in declaration expressions + // Consider including them (https://github.com/dotnet/roslyn/issues/37460). + private static void GetLocalNames(VariableDeclarationSyntax localDeclaration, ref List? result) + { + foreach (var local in localDeclaration.Variables) { - foreach (var local in localDeclaration.Variables) - { - GetLocalNames(local.Identifier, ref result); - } + GetLocalNames(local.Identifier, ref result); } + } - internal static void GetLocalNames(CommonForEachStatementSyntax commonForEach, ref List? result) + internal static void GetLocalNames(CommonForEachStatementSyntax commonForEach, ref List? result) + { + switch (commonForEach.Kind()) { - switch (commonForEach.Kind()) - { - case SyntaxKind.ForEachStatement: - GetLocalNames(((ForEachStatementSyntax)commonForEach).Identifier, ref result); - return; + case SyntaxKind.ForEachStatement: + GetLocalNames(((ForEachStatementSyntax)commonForEach).Identifier, ref result); + return; - case SyntaxKind.ForEachVariableStatement: - var forEachVariable = (ForEachVariableStatementSyntax)commonForEach; - GetLocalNames(forEachVariable.Variable, ref result); - return; + case SyntaxKind.ForEachVariableStatement: + var forEachVariable = (ForEachVariableStatementSyntax)commonForEach; + GetLocalNames(forEachVariable.Variable, ref result); + return; - default: - throw ExceptionUtilities.UnexpectedValue(commonForEach.Kind()); - } + default: + throw ExceptionUtilities.UnexpectedValue(commonForEach.Kind()); } + } - private static void GetLocalNames(ExpressionSyntax expression, ref List? result) + private static void GetLocalNames(ExpressionSyntax expression, ref List? result) + { + switch (expression.Kind()) { - switch (expression.Kind()) - { - case SyntaxKind.DeclarationExpression: - var declarationExpression = (DeclarationExpressionSyntax)expression; - var localDeclaration = declarationExpression.Designation; - GetLocalNames(localDeclaration, ref result); - return; - - case SyntaxKind.TupleExpression: - var tupleExpression = (TupleExpressionSyntax)expression; - foreach (var argument in tupleExpression.Arguments) - { - GetLocalNames(argument.Expression, ref result); - } + case SyntaxKind.DeclarationExpression: + var declarationExpression = (DeclarationExpressionSyntax)expression; + var localDeclaration = declarationExpression.Designation; + GetLocalNames(localDeclaration, ref result); + return; + + case SyntaxKind.TupleExpression: + var tupleExpression = (TupleExpressionSyntax)expression; + foreach (var argument in tupleExpression.Arguments) + { + GetLocalNames(argument.Expression, ref result); + } - return; + return; - default: - // Do nothing for node that cannot have variable declarations inside. - return; - } + default: + // Do nothing for node that cannot have variable declarations inside. + return; } + } - private static void GetLocalNames(VariableDesignationSyntax designation, ref List? result) + private static void GetLocalNames(VariableDesignationSyntax designation, ref List? result) + { + switch (designation.Kind()) { - switch (designation.Kind()) - { - case SyntaxKind.SingleVariableDesignation: - GetLocalNames(((SingleVariableDesignationSyntax)designation).Identifier, ref result); - return; - - case SyntaxKind.ParenthesizedVariableDesignation: - var parenthesizedVariableDesignation = (ParenthesizedVariableDesignationSyntax)designation; - foreach (var variableDesignation in parenthesizedVariableDesignation.Variables) - { - GetLocalNames(variableDesignation, ref result); - } + case SyntaxKind.SingleVariableDesignation: + GetLocalNames(((SingleVariableDesignationSyntax)designation).Identifier, ref result); + return; - return; + case SyntaxKind.ParenthesizedVariableDesignation: + var parenthesizedVariableDesignation = (ParenthesizedVariableDesignationSyntax)designation; + foreach (var variableDesignation in parenthesizedVariableDesignation.Variables) + { + GetLocalNames(variableDesignation, ref result); + } - case SyntaxKind.DiscardDesignation: - return; + return; - default: - throw ExceptionUtilities.UnexpectedValue(designation.Kind()); - } - } + case SyntaxKind.DiscardDesignation: + return; - private static void GetLocalNames(SyntaxToken syntaxToken, [NotNull] ref List? result) - { - result ??= []; - result.Add(syntaxToken); + default: + throw ExceptionUtilities.UnexpectedValue(designation.Kind()); } + } - private static double CombineOptional( - double distance0, - SyntaxNode? left1, - SyntaxNode? right1, - SyntaxNode? left2, - SyntaxNode? right2, - double weight0 = 0.8, - double weight1 = 0.5) - { - var one = left1 != null || right1 != null; - var two = left2 != null || right2 != null; + private static void GetLocalNames(SyntaxToken syntaxToken, [NotNull] ref List? result) + { + result ??= []; + result.Add(syntaxToken); + } - if (!one && !two) - { - return distance0; - } + private static double CombineOptional( + double distance0, + SyntaxNode? left1, + SyntaxNode? right1, + SyntaxNode? left2, + SyntaxNode? right2, + double weight0 = 0.8, + double weight1 = 0.5) + { + var one = left1 != null || right1 != null; + var two = left2 != null || right2 != null; - var distance1 = ComputeDistance(left1, right1); - var distance2 = ComputeDistance(left2, right2); + if (!one && !two) + { + return distance0; + } - double d; - if (one && two) - { - d = distance1 * weight1 + distance2 * (1 - weight1); - } - else if (one) - { - d = distance1; - } - else - { - d = distance2; - } + var distance1 = ComputeDistance(left1, right1); + var distance2 = ComputeDistance(left2, right2); - return distance0 * weight0 + d * (1 - weight0); + double d; + if (one && two) + { + d = distance1 * weight1 + distance2 * (1 - weight1); + } + else if (one) + { + d = distance1; } + else + { + d = distance2; + } + + return distance0 * weight0 + d * (1 - weight0); + } - private static SyntaxNodeOrToken? TryGetName(SyntaxNode node) + private static SyntaxNodeOrToken? TryGetName(SyntaxNode node) + { + switch (node.Kind()) { - switch (node.Kind()) - { - case SyntaxKind.ExternAliasDirective: - return ((ExternAliasDirectiveSyntax)node).Identifier; + case SyntaxKind.ExternAliasDirective: + return ((ExternAliasDirectiveSyntax)node).Identifier; - case SyntaxKind.UsingDirective: - return ((UsingDirectiveSyntax)node).NamespaceOrType; + case SyntaxKind.UsingDirective: + return ((UsingDirectiveSyntax)node).NamespaceOrType; - case SyntaxKind.NamespaceDeclaration: - case SyntaxKind.FileScopedNamespaceDeclaration: - return ((BaseNamespaceDeclarationSyntax)node).Name; + case SyntaxKind.NamespaceDeclaration: + case SyntaxKind.FileScopedNamespaceDeclaration: + return ((BaseNamespaceDeclarationSyntax)node).Name; - case SyntaxKind.ClassDeclaration: - case SyntaxKind.StructDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.RecordDeclaration: - case SyntaxKind.RecordStructDeclaration: - return ((TypeDeclarationSyntax)node).Identifier; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.RecordStructDeclaration: + return ((TypeDeclarationSyntax)node).Identifier; - case SyntaxKind.EnumDeclaration: - return ((EnumDeclarationSyntax)node).Identifier; + case SyntaxKind.EnumDeclaration: + return ((EnumDeclarationSyntax)node).Identifier; - case SyntaxKind.DelegateDeclaration: - return ((DelegateDeclarationSyntax)node).Identifier; + case SyntaxKind.DelegateDeclaration: + return ((DelegateDeclarationSyntax)node).Identifier; - case SyntaxKind.FieldDeclaration: - case SyntaxKind.EventFieldDeclaration: - case SyntaxKind.VariableDeclaration: - return null; + case SyntaxKind.FieldDeclaration: + case SyntaxKind.EventFieldDeclaration: + case SyntaxKind.VariableDeclaration: + return null; - case SyntaxKind.VariableDeclarator: - return ((VariableDeclaratorSyntax)node).Identifier; + case SyntaxKind.VariableDeclarator: + return ((VariableDeclaratorSyntax)node).Identifier; - case SyntaxKind.MethodDeclaration: - return ((MethodDeclarationSyntax)node).Identifier; + case SyntaxKind.MethodDeclaration: + return ((MethodDeclarationSyntax)node).Identifier; - case SyntaxKind.ConversionOperatorDeclaration: - return ((ConversionOperatorDeclarationSyntax)node).Type; + case SyntaxKind.ConversionOperatorDeclaration: + return ((ConversionOperatorDeclarationSyntax)node).Type; - case SyntaxKind.OperatorDeclaration: - return ((OperatorDeclarationSyntax)node).OperatorToken; + case SyntaxKind.OperatorDeclaration: + return ((OperatorDeclarationSyntax)node).OperatorToken; - case SyntaxKind.ConstructorDeclaration: - return ((ConstructorDeclarationSyntax)node).Identifier; + case SyntaxKind.ConstructorDeclaration: + return ((ConstructorDeclarationSyntax)node).Identifier; - case SyntaxKind.DestructorDeclaration: - return ((DestructorDeclarationSyntax)node).Identifier; + case SyntaxKind.DestructorDeclaration: + return ((DestructorDeclarationSyntax)node).Identifier; - case SyntaxKind.PropertyDeclaration: - return ((PropertyDeclarationSyntax)node).Identifier; + case SyntaxKind.PropertyDeclaration: + return ((PropertyDeclarationSyntax)node).Identifier; - case SyntaxKind.IndexerDeclaration: - return null; + case SyntaxKind.IndexerDeclaration: + return null; - case SyntaxKind.ArrowExpressionClause: - return null; + case SyntaxKind.ArrowExpressionClause: + return null; - case SyntaxKind.EventDeclaration: - return ((EventDeclarationSyntax)node).Identifier; + case SyntaxKind.EventDeclaration: + return ((EventDeclarationSyntax)node).Identifier; - case SyntaxKind.EnumMemberDeclaration: - return ((EnumMemberDeclarationSyntax)node).Identifier; + case SyntaxKind.EnumMemberDeclaration: + return ((EnumMemberDeclarationSyntax)node).Identifier; - case SyntaxKind.GetAccessorDeclaration: - case SyntaxKind.SetAccessorDeclaration: - case SyntaxKind.InitAccessorDeclaration: - return null; + case SyntaxKind.GetAccessorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.InitAccessorDeclaration: + return null; - case SyntaxKind.TypeParameterConstraintClause: - return ((TypeParameterConstraintClauseSyntax)node).Name.Identifier; + case SyntaxKind.TypeParameterConstraintClause: + return ((TypeParameterConstraintClauseSyntax)node).Name.Identifier; - case SyntaxKind.TypeParameter: - return ((TypeParameterSyntax)node).Identifier; + case SyntaxKind.TypeParameter: + return ((TypeParameterSyntax)node).Identifier; - case SyntaxKind.TypeParameterList: - case SyntaxKind.ParameterList: - case SyntaxKind.BracketedParameterList: - return null; + case SyntaxKind.TypeParameterList: + case SyntaxKind.ParameterList: + case SyntaxKind.BracketedParameterList: + return null; - case SyntaxKind.Parameter: - return ((ParameterSyntax)node).Identifier; + case SyntaxKind.Parameter: + return ((ParameterSyntax)node).Identifier; - case SyntaxKind.AttributeList: - return ((AttributeListSyntax)node).Target; + case SyntaxKind.AttributeList: + return ((AttributeListSyntax)node).Target; - case SyntaxKind.Attribute: - return ((AttributeSyntax)node).Name; + case SyntaxKind.Attribute: + return ((AttributeSyntax)node).Name; - default: - return null; - } + default: + return null; } + } + + public sealed override double GetDistance(SyntaxNode oldNode, SyntaxNode newNode) + { + Debug.Assert(GetLabel(oldNode) == GetLabel(newNode) && GetLabel(oldNode) != IgnoredNode); - public sealed override double GetDistance(SyntaxNode oldNode, SyntaxNode newNode) + if (oldNode == newNode) { - Debug.Assert(GetLabel(oldNode) == GetLabel(newNode) && GetLabel(oldNode) != IgnoredNode); + return ExactMatchDist; + } - if (oldNode == newNode) + if (TryComputeWeightedDistance(oldNode, newNode, out var weightedDistance)) + { + if (weightedDistance == ExactMatchDist && !SyntaxFactory.AreEquivalent(oldNode, newNode)) { - return ExactMatchDist; + weightedDistance = EpsilonDist; } - if (TryComputeWeightedDistance(oldNode, newNode, out var weightedDistance)) - { - if (weightedDistance == ExactMatchDist && !SyntaxFactory.AreEquivalent(oldNode, newNode)) - { - weightedDistance = EpsilonDist; - } + return weightedDistance; + } - return weightedDistance; - } + return ComputeValueDistance(oldNode, newNode); + } - return ComputeValueDistance(oldNode, newNode); + internal static double ComputeValueDistance(SyntaxNode? oldNode, SyntaxNode? newNode) + { + if (SyntaxFactory.AreEquivalent(oldNode, newNode)) + { + return ExactMatchDist; } - internal static double ComputeValueDistance(SyntaxNode? oldNode, SyntaxNode? newNode) - { - if (SyntaxFactory.AreEquivalent(oldNode, newNode)) - { - return ExactMatchDist; - } + var distance = ComputeDistance(oldNode, newNode); - var distance = ComputeDistance(oldNode, newNode); + // We don't want to return an exact match, because there + // must be something different, since we got here + return (distance == ExactMatchDist) ? EpsilonDist : distance; + } - // We don't want to return an exact match, because there - // must be something different, since we got here - return (distance == ExactMatchDist) ? EpsilonDist : distance; - } + internal static double ComputeDistance(SyntaxNodeOrToken oldNodeOrToken, SyntaxNodeOrToken newNodeOrToken) + { + Debug.Assert(newNodeOrToken.IsToken == oldNodeOrToken.IsToken); - internal static double ComputeDistance(SyntaxNodeOrToken oldNodeOrToken, SyntaxNodeOrToken newNodeOrToken) + double distance; + if (oldNodeOrToken.IsToken) { - Debug.Assert(newNodeOrToken.IsToken == oldNodeOrToken.IsToken); + var leftToken = oldNodeOrToken.AsToken(); + var rightToken = newNodeOrToken.AsToken(); - double distance; - if (oldNodeOrToken.IsToken) - { - var leftToken = oldNodeOrToken.AsToken(); - var rightToken = newNodeOrToken.AsToken(); - - distance = ComputeDistance(leftToken, rightToken); - Debug.Assert(!SyntaxFactory.AreEquivalent(leftToken, rightToken) || distance == ExactMatchDist); - } - else - { - var leftNode = oldNodeOrToken.AsNode(); - var rightNode = newNodeOrToken.AsNode(); - - distance = ComputeDistance(leftNode, rightNode); - Debug.Assert(!SyntaxFactory.AreEquivalent(leftNode, rightNode) || distance == ExactMatchDist); - } + distance = ComputeDistance(leftToken, rightToken); + Debug.Assert(!SyntaxFactory.AreEquivalent(leftToken, rightToken) || distance == ExactMatchDist); + } + else + { + var leftNode = oldNodeOrToken.AsNode(); + var rightNode = newNodeOrToken.AsNode(); - return distance; + distance = ComputeDistance(leftNode, rightNode); + Debug.Assert(!SyntaxFactory.AreEquivalent(leftNode, rightNode) || distance == ExactMatchDist); } - /// - /// Enumerates tokens of all nodes in the list. Doesn't include separators. - /// - internal static IEnumerable GetDescendantTokensIgnoringSeparators(SeparatedSyntaxList list) - where TSyntaxNode : SyntaxNode + return distance; + } + + /// + /// Enumerates tokens of all nodes in the list. Doesn't include separators. + /// + internal static IEnumerable GetDescendantTokensIgnoringSeparators(SeparatedSyntaxList list) + where TSyntaxNode : SyntaxNode + { + foreach (var node in list) { - foreach (var node in list) + foreach (var token in node.DescendantTokens()) { - foreach (var token in node.DescendantTokens()) - { - yield return token; - } + yield return token; } } + } - /// - /// Calculates the distance between two syntax nodes, disregarding trivia. - /// - /// - /// Distance is a number within [0, 1], the smaller the more similar the nodes are. - /// - public static double ComputeDistance(SyntaxNode? oldNode, SyntaxNode? newNode) + /// + /// Calculates the distance between two syntax nodes, disregarding trivia. + /// + /// + /// Distance is a number within [0, 1], the smaller the more similar the nodes are. + /// + public static double ComputeDistance(SyntaxNode? oldNode, SyntaxNode? newNode) + { + if (oldNode == null || newNode == null) { - if (oldNode == null || newNode == null) - { - return (oldNode == newNode) ? 0.0 : 1.0; - } - - return ComputeDistance(oldNode.DescendantTokens(), newNode.DescendantTokens()); + return (oldNode == newNode) ? 0.0 : 1.0; } - /// - /// Calculates the distance between two syntax tokens, disregarding trivia. - /// - /// - /// Distance is a number within [0, 1], the smaller the more similar the tokens are. - /// - public static double ComputeDistance(SyntaxToken oldToken, SyntaxToken newToken) - => LongestCommonSubstring.ComputePrefixDistance( - oldToken.Text, Math.Min(oldToken.Text.Length, LongestCommonSubsequence.MaxSequenceLengthForDistanceCalculation), - newToken.Text, Math.Min(newToken.Text.Length, LongestCommonSubsequence.MaxSequenceLengthForDistanceCalculation)); - - private static ImmutableArray CreateArrayForDistanceCalculation(IEnumerable? enumerable) - => enumerable is null ? [] : enumerable.Take(LongestCommonSubsequence.MaxSequenceLengthForDistanceCalculation).ToImmutableArray(); - - /// - /// Calculates the distance between two sequences of syntax tokens, disregarding trivia. - /// - /// - /// Distance is a number within [0, 1], the smaller the more similar the sequences are. - /// - public static double ComputeDistance(IEnumerable? oldTokens, IEnumerable? newTokens) - => LcsTokens.Instance.ComputeDistance(CreateArrayForDistanceCalculation(oldTokens), CreateArrayForDistanceCalculation(newTokens)); - - /// - /// Calculates the distance between two sequences of syntax nodes, disregarding trivia. - /// - /// - /// Distance is a number within [0, 1], the smaller the more similar the sequences are. - /// - public static double ComputeDistance(IEnumerable? oldNodes, IEnumerable? newNodes) - => LcsNodes.Instance.ComputeDistance(CreateArrayForDistanceCalculation(oldNodes), CreateArrayForDistanceCalculation(newNodes)); - - /// - /// Calculates the edits that transform one sequence of syntax nodes to another, disregarding trivia. - /// - public static IEnumerable GetSequenceEdits(IEnumerable? oldNodes, IEnumerable? newNodes) - => LcsNodes.Instance.GetEdits(oldNodes.AsImmutableOrEmpty(), newNodes.AsImmutableOrEmpty()); - - /// - /// Calculates the edits that transform one sequence of syntax nodes to another, disregarding trivia. - /// - public static IEnumerable GetSequenceEdits(ImmutableArray oldNodes, ImmutableArray newNodes) - => LcsNodes.Instance.GetEdits(oldNodes.NullToEmpty(), newNodes.NullToEmpty()); - - /// - /// Calculates the edits that transform one sequence of syntax tokens to another, disregarding trivia. - /// - public static IEnumerable GetSequenceEdits(IEnumerable? oldTokens, IEnumerable? newTokens) - => LcsTokens.Instance.GetEdits(oldTokens.AsImmutableOrEmpty(), newTokens.AsImmutableOrEmpty()); - - /// - /// Calculates the edits that transform one sequence of syntax tokens to another, disregarding trivia. - /// - public static IEnumerable GetSequenceEdits(ImmutableArray oldTokens, ImmutableArray newTokens) - => LcsTokens.Instance.GetEdits(oldTokens.NullToEmpty(), newTokens.NullToEmpty()); - - private sealed class LcsTokens : LongestCommonImmutableArraySubsequence - { - internal static readonly LcsTokens Instance = new LcsTokens(); + return ComputeDistance(oldNode.DescendantTokens(), newNode.DescendantTokens()); + } - protected override bool Equals(SyntaxToken oldElement, SyntaxToken newElement) - => SyntaxFactory.AreEquivalent(oldElement, newElement); - } + /// + /// Calculates the distance between two syntax tokens, disregarding trivia. + /// + /// + /// Distance is a number within [0, 1], the smaller the more similar the tokens are. + /// + public static double ComputeDistance(SyntaxToken oldToken, SyntaxToken newToken) + => LongestCommonSubstring.ComputePrefixDistance( + oldToken.Text, Math.Min(oldToken.Text.Length, LongestCommonSubsequence.MaxSequenceLengthForDistanceCalculation), + newToken.Text, Math.Min(newToken.Text.Length, LongestCommonSubsequence.MaxSequenceLengthForDistanceCalculation)); - private sealed class LcsNodes : LongestCommonImmutableArraySubsequence - { - internal static readonly LcsNodes Instance = new LcsNodes(); + private static ImmutableArray CreateArrayForDistanceCalculation(IEnumerable? enumerable) + => enumerable is null ? [] : enumerable.Take(LongestCommonSubsequence.MaxSequenceLengthForDistanceCalculation).ToImmutableArray(); - protected override bool Equals(SyntaxNode oldElement, SyntaxNode newElement) - => SyntaxFactory.AreEquivalent(oldElement, newElement); - } + /// + /// Calculates the distance between two sequences of syntax tokens, disregarding trivia. + /// + /// + /// Distance is a number within [0, 1], the smaller the more similar the sequences are. + /// + public static double ComputeDistance(IEnumerable? oldTokens, IEnumerable? newTokens) + => LcsTokens.Instance.ComputeDistance(CreateArrayForDistanceCalculation(oldTokens), CreateArrayForDistanceCalculation(newTokens)); - #endregion + /// + /// Calculates the distance between two sequences of syntax nodes, disregarding trivia. + /// + /// + /// Distance is a number within [0, 1], the smaller the more similar the sequences are. + /// + public static double ComputeDistance(IEnumerable? oldNodes, IEnumerable? newNodes) + => LcsNodes.Instance.ComputeDistance(CreateArrayForDistanceCalculation(oldNodes), CreateArrayForDistanceCalculation(newNodes)); + + /// + /// Calculates the edits that transform one sequence of syntax nodes to another, disregarding trivia. + /// + public static IEnumerable GetSequenceEdits(IEnumerable? oldNodes, IEnumerable? newNodes) + => LcsNodes.Instance.GetEdits(oldNodes.AsImmutableOrEmpty(), newNodes.AsImmutableOrEmpty()); + + /// + /// Calculates the edits that transform one sequence of syntax nodes to another, disregarding trivia. + /// + public static IEnumerable GetSequenceEdits(ImmutableArray oldNodes, ImmutableArray newNodes) + => LcsNodes.Instance.GetEdits(oldNodes.NullToEmpty(), newNodes.NullToEmpty()); + + /// + /// Calculates the edits that transform one sequence of syntax tokens to another, disregarding trivia. + /// + public static IEnumerable GetSequenceEdits(IEnumerable? oldTokens, IEnumerable? newTokens) + => LcsTokens.Instance.GetEdits(oldTokens.AsImmutableOrEmpty(), newTokens.AsImmutableOrEmpty()); + + /// + /// Calculates the edits that transform one sequence of syntax tokens to another, disregarding trivia. + /// + public static IEnumerable GetSequenceEdits(ImmutableArray oldTokens, ImmutableArray newTokens) + => LcsTokens.Instance.GetEdits(oldTokens.NullToEmpty(), newTokens.NullToEmpty()); + + private sealed class LcsTokens : LongestCommonImmutableArraySubsequence + { + internal static readonly LcsTokens Instance = new LcsTokens(); + + protected override bool Equals(SyntaxToken oldElement, SyntaxToken newElement) + => SyntaxFactory.AreEquivalent(oldElement, newElement); + } + + private sealed class LcsNodes : LongestCommonImmutableArraySubsequence + { + internal static readonly LcsNodes Instance = new LcsNodes(); + + protected override bool Equals(SyntaxNode oldElement, SyntaxNode newElement) + => SyntaxFactory.AreEquivalent(oldElement, newElement); } + + #endregion } diff --git a/src/Features/CSharp/Portable/EditAndContinue/SyntaxUtilities.cs b/src/Features/CSharp/Portable/EditAndContinue/SyntaxUtilities.cs index 00d92ee24c1d2..a425fe95b93df 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/SyntaxUtilities.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/SyntaxUtilities.cs @@ -11,237 +11,236 @@ using Microsoft.CodeAnalysis.EditAndContinue; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +internal static partial class SyntaxUtilities { - internal static partial class SyntaxUtilities + public static LambdaBody CreateLambdaBody(SyntaxNode node) + => new CSharpLambdaBody(node); + + public static MemberBody? TryGetDeclarationBody(SyntaxNode node, ISymbol? symbol) + => node switch + { + MethodDeclarationSyntax methodDeclaration => CreateSimpleBody(BlockOrExpression(methodDeclaration.Body, methodDeclaration.ExpressionBody)), + ConversionOperatorDeclarationSyntax conversionDeclaration => CreateSimpleBody(BlockOrExpression(conversionDeclaration.Body, conversionDeclaration.ExpressionBody)), + OperatorDeclarationSyntax operatorDeclaration => CreateSimpleBody(BlockOrExpression(operatorDeclaration.Body, operatorDeclaration.ExpressionBody)), + DestructorDeclarationSyntax destructorDeclaration => CreateSimpleBody(BlockOrExpression(destructorDeclaration.Body, destructorDeclaration.ExpressionBody)), + + AccessorDeclarationSyntax accessorDeclaration + => BlockOrExpression(accessorDeclaration.Body, accessorDeclaration.ExpressionBody) != null + ? new PropertyOrIndexerAccessorWithExplicitBodyDeclarationBody(accessorDeclaration) + : new ExplicitAutoPropertyAccessorDeclarationBody(accessorDeclaration), + + // We associate the body of expression-bodied property/indexer with the ArrowExpressionClause + // since that's the syntax node associated with the getter symbol. + // This approach makes it possible to change the expression body to an explicit getter and vice versa (both are method symbols). + // + // The property/indexer itself is considered to not have a body unless the property has an initializer. + ArrowExpressionClauseSyntax { Parent: (kind: SyntaxKind.PropertyDeclaration) or (kind: SyntaxKind.IndexerDeclaration) } arrowExpression + => new PropertyOrIndexerWithExplicitBodyDeclarationBody((BasePropertyDeclarationSyntax)arrowExpression.Parent!), + + PropertyDeclarationSyntax { Initializer: { } propertyInitializer } + => CreateSimpleBody(propertyInitializer.Value), + + ConstructorDeclarationSyntax constructorDeclaration when constructorDeclaration.Body != null || constructorDeclaration.ExpressionBody != null + => constructorDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword) + ? CreateSimpleBody(BlockOrExpression(constructorDeclaration.Body, constructorDeclaration.ExpressionBody)) + : (constructorDeclaration.Initializer != null) + ? new OrdinaryInstanceConstructorWithExplicitInitializerDeclarationBody(constructorDeclaration) + : new OrdinaryInstanceConstructorWithImplicitInitializerDeclarationBody(constructorDeclaration), + + CompilationUnitSyntax unit when unit.ContainsGlobalStatements() + => new TopLevelCodeDeclarationBody(unit), + + VariableDeclaratorSyntax { Parent.Parent: BaseFieldDeclarationSyntax fieldDeclaration, Initializer: { } } variableDeclarator + when !fieldDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword) + => new FieldWithInitializerDeclarationBody(variableDeclarator), + + ParameterListSyntax { Parent: TypeDeclarationSyntax typeDeclaration } + => typeDeclaration is { BaseList.Types: [PrimaryConstructorBaseTypeSyntax { }, ..] } + ? new PrimaryConstructorWithExplicitInitializerDeclarationBody(typeDeclaration) + : new PrimaryConstructorWithImplicitInitializerDeclarationBody(typeDeclaration), + + // Record type itself does not have a body, create body only when the declaration represents copy constructor: + RecordDeclarationSyntax recordDeclarationSyntax when symbol is not INamedTypeSymbol + => new CopyConstructorDeclarationBody(recordDeclarationSyntax), + + // Parameters themselves do not have a body, the synthesized property accessors do: + ParameterSyntax { Parent.Parent: RecordDeclarationSyntax } parameterSyntax when symbol is not IParameterSymbol + => new RecordParameterDeclarationBody(parameterSyntax), + + _ => null + }; + + internal static MemberBody? CreateSimpleBody(SyntaxNode? body) { - public static LambdaBody CreateLambdaBody(SyntaxNode node) - => new CSharpLambdaBody(node); - - public static MemberBody? TryGetDeclarationBody(SyntaxNode node, ISymbol? symbol) - => node switch - { - MethodDeclarationSyntax methodDeclaration => CreateSimpleBody(BlockOrExpression(methodDeclaration.Body, methodDeclaration.ExpressionBody)), - ConversionOperatorDeclarationSyntax conversionDeclaration => CreateSimpleBody(BlockOrExpression(conversionDeclaration.Body, conversionDeclaration.ExpressionBody)), - OperatorDeclarationSyntax operatorDeclaration => CreateSimpleBody(BlockOrExpression(operatorDeclaration.Body, operatorDeclaration.ExpressionBody)), - DestructorDeclarationSyntax destructorDeclaration => CreateSimpleBody(BlockOrExpression(destructorDeclaration.Body, destructorDeclaration.ExpressionBody)), - - AccessorDeclarationSyntax accessorDeclaration - => BlockOrExpression(accessorDeclaration.Body, accessorDeclaration.ExpressionBody) != null - ? new PropertyOrIndexerAccessorWithExplicitBodyDeclarationBody(accessorDeclaration) - : new ExplicitAutoPropertyAccessorDeclarationBody(accessorDeclaration), - - // We associate the body of expression-bodied property/indexer with the ArrowExpressionClause - // since that's the syntax node associated with the getter symbol. - // This approach makes it possible to change the expression body to an explicit getter and vice versa (both are method symbols). - // - // The property/indexer itself is considered to not have a body unless the property has an initializer. - ArrowExpressionClauseSyntax { Parent: (kind: SyntaxKind.PropertyDeclaration) or (kind: SyntaxKind.IndexerDeclaration) } arrowExpression - => new PropertyOrIndexerWithExplicitBodyDeclarationBody((BasePropertyDeclarationSyntax)arrowExpression.Parent!), - - PropertyDeclarationSyntax { Initializer: { } propertyInitializer } - => CreateSimpleBody(propertyInitializer.Value), - - ConstructorDeclarationSyntax constructorDeclaration when constructorDeclaration.Body != null || constructorDeclaration.ExpressionBody != null - => constructorDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword) - ? CreateSimpleBody(BlockOrExpression(constructorDeclaration.Body, constructorDeclaration.ExpressionBody)) - : (constructorDeclaration.Initializer != null) - ? new OrdinaryInstanceConstructorWithExplicitInitializerDeclarationBody(constructorDeclaration) - : new OrdinaryInstanceConstructorWithImplicitInitializerDeclarationBody(constructorDeclaration), - - CompilationUnitSyntax unit when unit.ContainsGlobalStatements() - => new TopLevelCodeDeclarationBody(unit), - - VariableDeclaratorSyntax { Parent.Parent: BaseFieldDeclarationSyntax fieldDeclaration, Initializer: { } } variableDeclarator - when !fieldDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword) - => new FieldWithInitializerDeclarationBody(variableDeclarator), - - ParameterListSyntax { Parent: TypeDeclarationSyntax typeDeclaration } - => typeDeclaration is { BaseList.Types: [PrimaryConstructorBaseTypeSyntax { }, ..] } - ? new PrimaryConstructorWithExplicitInitializerDeclarationBody(typeDeclaration) - : new PrimaryConstructorWithImplicitInitializerDeclarationBody(typeDeclaration), - - // Record type itself does not have a body, create body only when the declaration represents copy constructor: - RecordDeclarationSyntax recordDeclarationSyntax when symbol is not INamedTypeSymbol - => new CopyConstructorDeclarationBody(recordDeclarationSyntax), - - // Parameters themselves do not have a body, the synthesized property accessors do: - ParameterSyntax { Parent.Parent: RecordDeclarationSyntax } parameterSyntax when symbol is not IParameterSymbol - => new RecordParameterDeclarationBody(parameterSyntax), - - _ => null - }; - - internal static MemberBody? CreateSimpleBody(SyntaxNode? body) - { - if (body == null) - { - return null; - } - - AssertIsBody(body, allowLambda: false); - return new SimpleMemberBody(body); + if (body == null) + { + return null; } - public static SyntaxNode? BlockOrExpression(BlockSyntax? blockBody, ArrowExpressionClauseSyntax? expressionBody) - => (SyntaxNode?)blockBody ?? expressionBody?.Expression; - - [Conditional("DEBUG")] - public static void AssertIsBody(SyntaxNode syntax, bool allowLambda) - { - // lambda/query - if (LambdaUtilities.IsLambdaBody(syntax)) - { - Debug.Assert(allowLambda); - Debug.Assert(syntax is ExpressionSyntax or BlockSyntax); - return; - } - - // block body - if (syntax is BlockSyntax) - { - return; - } - - // expression body - if (syntax is ExpressionSyntax { Parent: ArrowExpressionClauseSyntax }) - { - return; - } - - // field initializer - if (syntax is ExpressionSyntax { Parent.Parent: VariableDeclaratorSyntax }) - { - return; - } - - // property initializer - if (syntax is ExpressionSyntax { Parent.Parent: PropertyDeclarationSyntax }) - { - return; - } - - // special case for top level statements, which have no containing block other than the compilation unit - if (syntax is CompilationUnitSyntax unit && unit.ContainsGlobalStatements()) - { - return; - } - - Debug.Assert(false); - } + AssertIsBody(body, allowLambda: false); + return new SimpleMemberBody(body); + } - public static bool ContainsGlobalStatements(this CompilationUnitSyntax compilationUnit) - => compilationUnit.Members is [GlobalStatementSyntax, ..]; + public static SyntaxNode? BlockOrExpression(BlockSyntax? blockBody, ArrowExpressionClauseSyntax? expressionBody) + => (SyntaxNode?)blockBody ?? expressionBody?.Expression; - public static bool Any(TypeParameterListSyntax? list) - => list != null && list.ChildNodesAndTokens().Count != 0; + [Conditional("DEBUG")] + public static void AssertIsBody(SyntaxNode syntax, bool allowLambda) + { + // lambda/query + if (LambdaUtilities.IsLambdaBody(syntax)) + { + Debug.Assert(allowLambda); + Debug.Assert(syntax is ExpressionSyntax or BlockSyntax); + return; + } + + // block body + if (syntax is BlockSyntax) + { + return; + } - public static SyntaxNode? TryGetEffectiveGetterBody(SyntaxNode declaration) + // expression body + if (syntax is ExpressionSyntax { Parent: ArrowExpressionClauseSyntax }) { - if (declaration is PropertyDeclarationSyntax property) - { - return TryGetEffectiveGetterBody(property.ExpressionBody, property.AccessorList); - } + return; + } - if (declaration is IndexerDeclarationSyntax indexer) - { - return TryGetEffectiveGetterBody(indexer.ExpressionBody, indexer.AccessorList); - } + // field initializer + if (syntax is ExpressionSyntax { Parent.Parent: VariableDeclaratorSyntax }) + { + return; + } - return null; + // property initializer + if (syntax is ExpressionSyntax { Parent.Parent: PropertyDeclarationSyntax }) + { + return; } - public static SyntaxNode? TryGetEffectiveGetterBody(ArrowExpressionClauseSyntax? propertyBody, AccessorListSyntax? accessorList) + // special case for top level statements, which have no containing block other than the compilation unit + if (syntax is CompilationUnitSyntax unit && unit.ContainsGlobalStatements()) { - if (propertyBody != null) - { - return propertyBody.Expression; - } + return; + } - var firstGetter = accessorList?.Accessors.Where(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)).FirstOrDefault(); - if (firstGetter == null) - { - return null; - } + Debug.Assert(false); + } - return (SyntaxNode?)firstGetter.Body ?? firstGetter.ExpressionBody?.Expression; + public static bool ContainsGlobalStatements(this CompilationUnitSyntax compilationUnit) + => compilationUnit.Members is [GlobalStatementSyntax, ..]; + + public static bool Any(TypeParameterListSyntax? list) + => list != null && list.ChildNodesAndTokens().Count != 0; + + public static SyntaxNode? TryGetEffectiveGetterBody(SyntaxNode declaration) + { + if (declaration is PropertyDeclarationSyntax property) + { + return TryGetEffectiveGetterBody(property.ExpressionBody, property.AccessorList); } - public static SyntaxTokenList? TryGetFieldOrPropertyModifiers(SyntaxNode node) + if (declaration is IndexerDeclarationSyntax indexer) { - if (node is FieldDeclarationSyntax fieldDecl) - return fieldDecl.Modifiers; + return TryGetEffectiveGetterBody(indexer.ExpressionBody, indexer.AccessorList); + } - if (node is PropertyDeclarationSyntax propertyDecl) - return propertyDecl.Modifiers; + return null; + } - return null; + public static SyntaxNode? TryGetEffectiveGetterBody(ArrowExpressionClauseSyntax? propertyBody, AccessorListSyntax? accessorList) + { + if (propertyBody != null) + { + return propertyBody.Expression; } - public static bool IsParameterlessConstructor(SyntaxNode declaration) + var firstGetter = accessorList?.Accessors.Where(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)).FirstOrDefault(); + if (firstGetter == null) { - if (declaration is not ConstructorDeclarationSyntax ctor) - { - return false; - } + return null; + } + + return (SyntaxNode?)firstGetter.Body ?? firstGetter.ExpressionBody?.Expression; + } - return ctor.ParameterList.Parameters.Count == 0; + public static SyntaxTokenList? TryGetFieldOrPropertyModifiers(SyntaxNode node) + { + if (node is FieldDeclarationSyntax fieldDecl) + return fieldDecl.Modifiers; + + if (node is PropertyDeclarationSyntax propertyDecl) + return propertyDecl.Modifiers; + + return null; + } + + public static bool IsParameterlessConstructor(SyntaxNode declaration) + { + if (declaration is not ConstructorDeclarationSyntax ctor) + { + return false; } - public static bool HasBackingField(PropertyDeclarationSyntax property) + return ctor.ParameterList.Parameters.Count == 0; + } + + public static bool HasBackingField(PropertyDeclarationSyntax property) + { + if (property.Modifiers.Any(SyntaxKind.AbstractKeyword) || + property.Modifiers.Any(SyntaxKind.ExternKeyword)) { - if (property.Modifiers.Any(SyntaxKind.AbstractKeyword) || - property.Modifiers.Any(SyntaxKind.ExternKeyword)) - { - return false; - } + return false; + } + + return property.ExpressionBody == null + && property.AccessorList!.Accessors.Any(e => e.Body == null && e.ExpressionBody == null); + } - return property.ExpressionBody == null - && property.AccessorList!.Accessors.Any(e => e.Body == null && e.ExpressionBody == null); + /// + /// True if the specified declaration node is an async method, anonymous function, lambda, local function. + /// + public static bool IsAsyncDeclaration(SyntaxNode declaration) + { + // lambdas and anonymous functions + if (declaration is AnonymousFunctionExpressionSyntax anonymousFunction) + { + return anonymousFunction.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword); } - /// - /// True if the specified declaration node is an async method, anonymous function, lambda, local function. - /// - public static bool IsAsyncDeclaration(SyntaxNode declaration) - { - // lambdas and anonymous functions - if (declaration is AnonymousFunctionExpressionSyntax anonymousFunction) - { - return anonymousFunction.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword); - } - - // expression bodied methods/local functions: - if (declaration.IsKind(SyntaxKind.ArrowExpressionClause)) - { - Contract.ThrowIfNull(declaration.Parent); - declaration = declaration.Parent; - } - - return declaration switch - { - MethodDeclarationSyntax method => method.Modifiers.Any(SyntaxKind.AsyncKeyword), - LocalFunctionStatementSyntax localFunction => localFunction.Modifiers.Any(SyntaxKind.AsyncKeyword), - _ => false - }; + // expression bodied methods/local functions: + if (declaration.IsKind(SyntaxKind.ArrowExpressionClause)) + { + Contract.ThrowIfNull(declaration.Parent); + declaration = declaration.Parent; } - /// - /// Returns a list of all await expressions, await foreach statements, await using declarations and yield statements in the given body, - /// in the order in which they occur. - /// - /// - /// for await expressions, - /// for yield return statements, - /// for await foreach statements, - /// for await using declarators. - /// for await using statements. - /// - public static IEnumerable GetSuspensionPoints(SyntaxNode body) - => body.DescendantNodesAndSelf(LambdaUtilities.IsNotLambda).Where(SyntaxBindingUtilities.BindsToResumableStateMachineState); - - // Presence of yield break or yield return indicates state machine, but yield break does not bind to a resumable state. - public static bool IsIterator(SyntaxNode body) - => body.DescendantNodesAndSelf(LambdaUtilities.IsNotLambda).Any(n => n is YieldStatementSyntax); + return declaration switch + { + MethodDeclarationSyntax method => method.Modifiers.Any(SyntaxKind.AsyncKeyword), + LocalFunctionStatementSyntax localFunction => localFunction.Modifiers.Any(SyntaxKind.AsyncKeyword), + _ => false + }; } + + /// + /// Returns a list of all await expressions, await foreach statements, await using declarations and yield statements in the given body, + /// in the order in which they occur. + /// + /// + /// for await expressions, + /// for yield return statements, + /// for await foreach statements, + /// for await using declarators. + /// for await using statements. + /// + public static IEnumerable GetSuspensionPoints(SyntaxNode body) + => body.DescendantNodesAndSelf(LambdaUtilities.IsNotLambda).Where(SyntaxBindingUtilities.BindsToResumableStateMachineState); + + // Presence of yield break or yield return indicates state machine, but yield break does not bind to a resumable state. + public static bool IsIterator(SyntaxNode body) + => body.DescendantNodesAndSelf(LambdaUtilities.IsNotLambda).Any(n => n is YieldStatementSyntax); } diff --git a/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpEmbeddedLanguagesProvider.cs b/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpEmbeddedLanguagesProvider.cs index 2713f4d98446d..b9e463b2499b2 100644 --- a/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpEmbeddedLanguagesProvider.cs +++ b/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpEmbeddedLanguagesProvider.cs @@ -10,24 +10,23 @@ using Microsoft.CodeAnalysis.EmbeddedLanguages; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages.LanguageServices -{ - [ExportLanguageService(typeof(IEmbeddedLanguagesProvider), LanguageNames.CSharp, ServiceLayer.Default), Shared] - internal class CSharpEmbeddedLanguagesProvider : AbstractEmbeddedLanguagesProvider - { - public static readonly EmbeddedLanguageInfo Info = new( - CSharpSyntaxFacts.Instance, - CSharpSemanticFactsService.Instance, - CSharpVirtualCharService.Instance); +namespace Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages.LanguageServices; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpEmbeddedLanguagesProvider() - : base(Info) - { - } +[ExportLanguageService(typeof(IEmbeddedLanguagesProvider), LanguageNames.CSharp, ServiceLayer.Default), Shared] +internal class CSharpEmbeddedLanguagesProvider : AbstractEmbeddedLanguagesProvider +{ + public static readonly EmbeddedLanguageInfo Info = new( + CSharpSyntaxFacts.Instance, + CSharpSemanticFactsService.Instance, + CSharpVirtualCharService.Instance); - public override string EscapeText(string text, SyntaxToken token) - => EmbeddedLanguageUtilities.EscapeText(text, token); + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpEmbeddedLanguagesProvider() + : base(Info) + { } + + public override string EscapeText(string text, SyntaxToken token) + => EmbeddedLanguageUtilities.EscapeText(text, token); } diff --git a/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDetectionAnalyzer.cs b/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDetectionAnalyzer.cs index a980c2de38478..bb4f5846ca51f 100644 --- a/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDetectionAnalyzer.cs +++ b/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDetectionAnalyzer.cs @@ -6,14 +6,13 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Features.EmbeddedLanguages.Json.LanguageServices; -namespace Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages +namespace Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpJsonDetectionAnalyzer : AbstractJsonDetectionAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpJsonDetectionAnalyzer : AbstractJsonDetectionAnalyzer + public CSharpJsonDetectionAnalyzer() + : base(CSharpEmbeddedLanguagesProvider.Info) { - public CSharpJsonDetectionAnalyzer() - : base(CSharpEmbeddedLanguagesProvider.Info) - { - } } } diff --git a/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDetectionCodeFixProvider.cs b/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDetectionCodeFixProvider.cs index 52f45201f2206..a323333b473fc 100644 --- a/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDetectionCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDetectionCodeFixProvider.cs @@ -11,19 +11,18 @@ using Microsoft.CodeAnalysis.Features.EmbeddedLanguages.Json.LanguageServices; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages +namespace Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.JsonDetection), Shared] +internal class CSharpJsonDetectionCodeFixProvider : AbstractJsonDetectionCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.JsonDetection), Shared] - internal class CSharpJsonDetectionCodeFixProvider : AbstractJsonDetectionCodeFixProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpJsonDetectionCodeFixProvider() + : base(CSharpEmbeddedLanguagesProvider.Info) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpJsonDetectionCodeFixProvider() - : base(CSharpEmbeddedLanguagesProvider.Info) - { - } - - protected override void AddComment(SyntaxEditor editor, SyntaxToken stringLiteral, string commentContents) - => EmbeddedLanguageUtilities.AddComment(editor, stringLiteral, commentContents); } + + protected override void AddComment(SyntaxEditor editor, SyntaxToken stringLiteral, string commentContents) + => EmbeddedLanguageUtilities.AddComment(editor, stringLiteral, commentContents); } diff --git a/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDiagnosticAnalyzer.cs index 675af4912ac47..082395d0f78bd 100644 --- a/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDiagnosticAnalyzer.cs +++ b/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpJsonDiagnosticAnalyzer.cs @@ -6,14 +6,13 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Features.EmbeddedLanguages.Json.LanguageServices; -namespace Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages +namespace Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpJsonDiagnosticAnalyzer : AbstractJsonDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpJsonDiagnosticAnalyzer : AbstractJsonDiagnosticAnalyzer + public CSharpJsonDiagnosticAnalyzer() + : base(CSharpEmbeddedLanguagesProvider.Info) { - public CSharpJsonDiagnosticAnalyzer() - : base(CSharpEmbeddedLanguagesProvider.Info) - { - } } } diff --git a/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpRegexDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpRegexDiagnosticAnalyzer.cs index 4058db4658589..70e07201d26ce 100644 --- a/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpRegexDiagnosticAnalyzer.cs +++ b/src/Features/CSharp/Portable/EmbeddedLanguages/CSharpRegexDiagnosticAnalyzer.cs @@ -6,14 +6,13 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Features.EmbeddedLanguages.RegularExpressions.LanguageServices; -namespace Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages +namespace Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpRegexDiagnosticAnalyzer : AbstractRegexDiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpRegexDiagnosticAnalyzer : AbstractRegexDiagnosticAnalyzer + public CSharpRegexDiagnosticAnalyzer() + : base(CSharpEmbeddedLanguagesProvider.Info) { - public CSharpRegexDiagnosticAnalyzer() - : base(CSharpEmbeddedLanguagesProvider.Info) - { - } } } diff --git a/src/Features/CSharp/Portable/EmbeddedLanguages/Classification/CSharpEmbeddedLanguageClassificationServiceFactory.cs b/src/Features/CSharp/Portable/EmbeddedLanguages/Classification/CSharpEmbeddedLanguageClassificationServiceFactory.cs index c8465c3c6f800..0481447d2ccff 100644 --- a/src/Features/CSharp/Portable/EmbeddedLanguages/Classification/CSharpEmbeddedLanguageClassificationServiceFactory.cs +++ b/src/Features/CSharp/Portable/EmbeddedLanguages/Classification/CSharpEmbeddedLanguageClassificationServiceFactory.cs @@ -11,13 +11,12 @@ using Microsoft.CodeAnalysis.EmbeddedLanguages; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Classification +namespace Microsoft.CodeAnalysis.CSharp.Classification; + +[ExportLanguageService(typeof(IEmbeddedLanguageClassificationService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpEmbeddedLanguageClassificationService( + [ImportMany] IEnumerable> classifiers) : AbstractEmbeddedLanguageClassificationService(LanguageNames.CSharp, CSharpEmbeddedLanguagesProvider.Info, CSharpSyntaxKinds.Instance, CSharpFallbackEmbeddedLanguageClassifier.Instance, classifiers) { - [ExportLanguageService(typeof(IEmbeddedLanguageClassificationService), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpEmbeddedLanguageClassificationService( - [ImportMany] IEnumerable> classifiers) : AbstractEmbeddedLanguageClassificationService(LanguageNames.CSharp, CSharpEmbeddedLanguagesProvider.Info, CSharpSyntaxKinds.Instance, CSharpFallbackEmbeddedLanguageClassifier.Instance, classifiers) - { - } } diff --git a/src/Features/CSharp/Portable/EmbeddedLanguages/Classification/CSharpFallbackEmbeddedLanguageClassifier.cs b/src/Features/CSharp/Portable/EmbeddedLanguages/Classification/CSharpFallbackEmbeddedLanguageClassifier.cs index 375a79194fc88..fc0a8c3be8bfe 100644 --- a/src/Features/CSharp/Portable/EmbeddedLanguages/Classification/CSharpFallbackEmbeddedLanguageClassifier.cs +++ b/src/Features/CSharp/Portable/EmbeddedLanguages/Classification/CSharpFallbackEmbeddedLanguageClassifier.cs @@ -5,15 +5,14 @@ using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages.LanguageServices; -namespace Microsoft.CodeAnalysis.CSharp.Classification +namespace Microsoft.CodeAnalysis.CSharp.Classification; + +internal class CSharpFallbackEmbeddedLanguageClassifier : AbstractFallbackEmbeddedLanguageClassifier { - internal class CSharpFallbackEmbeddedLanguageClassifier : AbstractFallbackEmbeddedLanguageClassifier - { - public static readonly CSharpFallbackEmbeddedLanguageClassifier Instance = new(); + public static readonly CSharpFallbackEmbeddedLanguageClassifier Instance = new(); - private CSharpFallbackEmbeddedLanguageClassifier() - : base(CSharpEmbeddedLanguagesProvider.Info) - { - } + private CSharpFallbackEmbeddedLanguageClassifier() + : base(CSharpEmbeddedLanguagesProvider.Info) + { } } diff --git a/src/Features/CSharp/Portable/EmbeddedLanguages/EmbeddedLanguageUtilities.cs b/src/Features/CSharp/Portable/EmbeddedLanguages/EmbeddedLanguageUtilities.cs index 317e754a4970c..28450ef4b4f65 100644 --- a/src/Features/CSharp/Portable/EmbeddedLanguages/EmbeddedLanguageUtilities.cs +++ b/src/Features/CSharp/Portable/EmbeddedLanguages/EmbeddedLanguageUtilities.cs @@ -5,33 +5,32 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Features.EmbeddedLanguages +namespace Microsoft.CodeAnalysis.CSharp.Features.EmbeddedLanguages; + +internal static class EmbeddedLanguageUtilities { - internal static class EmbeddedLanguageUtilities + internal static void AddComment(SyntaxEditor editor, SyntaxToken stringLiteral, string commentContents) { - internal static void AddComment(SyntaxEditor editor, SyntaxToken stringLiteral, string commentContents) - { - var triviaList = SyntaxFactory.TriviaList( - SyntaxFactory.Comment($"/*{commentContents}*/"), - SyntaxFactory.ElasticSpace); - var newStringLiteral = stringLiteral.WithLeadingTrivia( - stringLiteral.LeadingTrivia.AddRange(triviaList)); - var parent = stringLiteral.GetRequiredParent(); - editor.ReplaceNode( - parent, - parent.ReplaceToken(stringLiteral, newStringLiteral)); - } + var triviaList = SyntaxFactory.TriviaList( + SyntaxFactory.Comment($"/*{commentContents}*/"), + SyntaxFactory.ElasticSpace); + var newStringLiteral = stringLiteral.WithLeadingTrivia( + stringLiteral.LeadingTrivia.AddRange(triviaList)); + var parent = stringLiteral.GetRequiredParent(); + editor.ReplaceNode( + parent, + parent.ReplaceToken(stringLiteral, newStringLiteral)); + } - public static string EscapeText(string text, SyntaxToken token) - { - // This function is called when Completion needs to escape something its going to insert into the user's - // string token. This means that we only have to escape things that completion could insert. In this case, - // the only regex character that is relevant is the \ character, and it's only relevant if we insert into a - // normal string and not a verbatim string. There are no other regex characters that completion will - // produce that need any escaping. - return token.Kind() is SyntaxKind.StringLiteralToken or SyntaxKind.Utf8StringLiteralToken && !token.IsVerbatimStringLiteral() - ? text.Replace(@"\", @"\\") - : text; - } + public static string EscapeText(string text, SyntaxToken token) + { + // This function is called when Completion needs to escape something its going to insert into the user's + // string token. This means that we only have to escape things that completion could insert. In this case, + // the only regex character that is relevant is the \ character, and it's only relevant if we insert into a + // normal string and not a verbatim string. There are no other regex characters that completion will + // produce that need any escaping. + return token.Kind() is SyntaxKind.StringLiteralToken or SyntaxKind.Utf8StringLiteralToken && !token.IsVerbatimStringLiteral() + ? text.Replace(@"\", @"\\") + : text; } } diff --git a/src/Features/CSharp/Portable/EncapsulateField/CSharpEncapsulateFieldService.cs b/src/Features/CSharp/Portable/EncapsulateField/CSharpEncapsulateFieldService.cs index 78108817e12eb..c7be2c182fab3 100644 --- a/src/Features/CSharp/Portable/EncapsulateField/CSharpEncapsulateFieldService.cs +++ b/src/Features/CSharp/Portable/EncapsulateField/CSharpEncapsulateFieldService.cs @@ -23,187 +23,186 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.EncapsulateField +namespace Microsoft.CodeAnalysis.CSharp.EncapsulateField; + +[ExportLanguageService(typeof(AbstractEncapsulateFieldService), LanguageNames.CSharp), Shared] +internal class CSharpEncapsulateFieldService : AbstractEncapsulateFieldService { - [ExportLanguageService(typeof(AbstractEncapsulateFieldService), LanguageNames.CSharp), Shared] - internal class CSharpEncapsulateFieldService : AbstractEncapsulateFieldService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpEncapsulateFieldService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpEncapsulateFieldService() - { - } - - protected override async Task RewriteFieldNameAndAccessibilityAsync(string originalFieldName, bool makePrivate, Document document, SyntaxAnnotation declarationAnnotation, CodeAndImportGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + } - var declarator = root.GetAnnotatedNodes(declarationAnnotation).FirstOrDefault(); + protected override async Task RewriteFieldNameAndAccessibilityAsync(string originalFieldName, bool makePrivate, Document document, SyntaxAnnotation declarationAnnotation, CodeAndImportGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // There may be no field to rewrite if this document is part of a set of linked files - // and the declaration is not conditionally compiled in this document's project. - if (declarator == null) - { - return root; - } + var declarator = root.GetAnnotatedNodes(declarationAnnotation).FirstOrDefault(); - var tempAnnotation = new SyntaxAnnotation(); - var escapedName = originalFieldName.EscapeIdentifier(); - var newIdentifier = SyntaxFactory.Identifier( - leading: [SyntaxFactory.ElasticMarker], - contextualKind: SyntaxKind.IdentifierName, - text: escapedName, - valueText: originalFieldName, - trailing: [SyntaxFactory.ElasticMarker]) - .WithTrailingTrivia(declarator.Identifier.TrailingTrivia) - .WithLeadingTrivia(declarator.Identifier.LeadingTrivia); + // There may be no field to rewrite if this document is part of a set of linked files + // and the declaration is not conditionally compiled in this document's project. + if (declarator == null) + { + return root; + } - var updatedDeclarator = declarator.WithIdentifier(newIdentifier).WithAdditionalAnnotations(tempAnnotation); + var tempAnnotation = new SyntaxAnnotation(); + var escapedName = originalFieldName.EscapeIdentifier(); + var newIdentifier = SyntaxFactory.Identifier( + leading: [SyntaxFactory.ElasticMarker], + contextualKind: SyntaxKind.IdentifierName, + text: escapedName, + valueText: originalFieldName, + trailing: [SyntaxFactory.ElasticMarker]) + .WithTrailingTrivia(declarator.Identifier.TrailingTrivia) + .WithLeadingTrivia(declarator.Identifier.LeadingTrivia); - root = root.ReplaceNode(declarator, updatedDeclarator); - document = document.WithSyntaxRoot(root); + var updatedDeclarator = declarator.WithIdentifier(newIdentifier).WithAdditionalAnnotations(tempAnnotation); - var declaration = root.GetAnnotatedNodes(tempAnnotation).First().Parent as VariableDeclarationSyntax; + root = root.ReplaceNode(declarator, updatedDeclarator); + document = document.WithSyntaxRoot(root); - if (declaration.Variables.Count == 1) - { - var fieldSyntax = declaration.Parent as FieldDeclarationSyntax; + var declaration = root.GetAnnotatedNodes(tempAnnotation).First().Parent as VariableDeclarationSyntax; - var modifierKinds = new[] { SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, SyntaxKind.InternalKeyword, SyntaxKind.PublicKeyword }; + if (declaration.Variables.Count == 1) + { + var fieldSyntax = declaration.Parent as FieldDeclarationSyntax; - if (makePrivate) - { - var modifiers = SpecializedCollections.SingletonEnumerable(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)) - .Concat(fieldSyntax.Modifiers.Where(m => !modifierKinds.Contains(m.Kind()))); + var modifierKinds = new[] { SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, SyntaxKind.InternalKeyword, SyntaxKind.PublicKeyword }; - root = root.ReplaceNode(fieldSyntax, fieldSyntax - .WithModifiers([.. modifiers]) - .WithAdditionalAnnotations(Formatter.Annotation) - .WithLeadingTrivia(fieldSyntax.GetLeadingTrivia()) - .WithTrailingTrivia(fieldSyntax.GetTrailingTrivia())); - } - } - else if (declaration.Variables.Count > 1 && makePrivate) + if (makePrivate) { - document = document.WithSyntaxRoot(root); - var codeGenService = document.GetLanguageService(); - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - declarator = root.GetAnnotatedNodes(tempAnnotation).First(); - declaration = declarator.Parent as VariableDeclarationSyntax; - - var field = semanticModel.GetDeclaredSymbol(declarator, cancellationToken) as IFieldSymbol; - - var fieldToAdd = declarationAnnotation.AddAnnotationToSymbol(CodeGenerationSymbolFactory.CreateFieldSymbol( - field.GetAttributes(), - Accessibility.Private, - new DeclarationModifiers(isStatic: field.IsStatic, isReadOnly: field.IsReadOnly, isConst: field.IsConst), - field.Type, - field.Name, - field.HasConstantValue, - field.ConstantValue, - declarator.Initializer)); - - var withField = await codeGenService.AddFieldAsync( - new CodeGenerationSolutionContext( - document.Project.Solution, - CodeGenerationContext.Default, - fallbackOptions), - field.ContainingType, - fieldToAdd, - cancellationToken).ConfigureAwait(false); - root = await withField.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - declarator = root.GetAnnotatedNodes(tempAnnotation).First(); - declaration = declarator.Parent as VariableDeclarationSyntax; - - return root.RemoveNode(declarator, SyntaxRemoveOptions.KeepNoTrivia); + var modifiers = SpecializedCollections.SingletonEnumerable(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)) + .Concat(fieldSyntax.Modifiers.Where(m => !modifierKinds.Contains(m.Kind()))); + + root = root.ReplaceNode(fieldSyntax, fieldSyntax + .WithModifiers([.. modifiers]) + .WithAdditionalAnnotations(Formatter.Annotation) + .WithLeadingTrivia(fieldSyntax.GetLeadingTrivia()) + .WithTrailingTrivia(fieldSyntax.GetTrailingTrivia())); } - - return root; } - - protected override async Task> GetFieldsAsync(Document document, TextSpan span, CancellationToken cancellationToken) + else if (declaration.Variables.Count > 1 && makePrivate) { + document = document.WithSyntaxRoot(root); + var codeGenService = document.GetLanguageService(); var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var fields = root.DescendantNodes(d => d.Span.IntersectsWith(span)) - .OfType() - .Where(n => n.Span.IntersectsWith(span)); + root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + declarator = root.GetAnnotatedNodes(tempAnnotation).First(); + declaration = declarator.Parent as VariableDeclarationSyntax; + + var field = semanticModel.GetDeclaredSymbol(declarator, cancellationToken) as IFieldSymbol; + + var fieldToAdd = declarationAnnotation.AddAnnotationToSymbol(CodeGenerationSymbolFactory.CreateFieldSymbol( + field.GetAttributes(), + Accessibility.Private, + new DeclarationModifiers(isStatic: field.IsStatic, isReadOnly: field.IsReadOnly, isConst: field.IsConst), + field.Type, + field.Name, + field.HasConstantValue, + field.ConstantValue, + declarator.Initializer)); + + var withField = await codeGenService.AddFieldAsync( + new CodeGenerationSolutionContext( + document.Project.Solution, + CodeGenerationContext.Default, + fallbackOptions), + field.ContainingType, + fieldToAdd, + cancellationToken).ConfigureAwait(false); + root = await withField.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + declarator = root.GetAnnotatedNodes(tempAnnotation).First(); + declaration = declarator.Parent as VariableDeclarationSyntax; + + return root.RemoveNode(declarator, SyntaxRemoveOptions.KeepNoTrivia); + } - var declarations = fields.Where(CanEncapsulate).Select(f => f.Declaration); + return root; + } - IEnumerable declarators; - if (span.IsEmpty) - { - // no selection, get all variables - declarators = declarations.SelectMany(d => d.Variables); - } - else - { - // has selection, get only the ones that are included in the selection - declarators = declarations.SelectMany(d => d.Variables.Where(v => v.Span.IntersectsWith(span))); - } + protected override async Task> GetFieldsAsync(Document document, TextSpan span, CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - return declarators.Select(d => semanticModel.GetDeclaredSymbol(d, cancellationToken) as IFieldSymbol) - .WhereNotNull() - .Where(f => f.Name.Length != 0) - .ToImmutableArray(); + var fields = root.DescendantNodes(d => d.Span.IntersectsWith(span)) + .OfType() + .Where(n => n.Span.IntersectsWith(span)); + + var declarations = fields.Where(CanEncapsulate).Select(f => f.Declaration); + + IEnumerable declarators; + if (span.IsEmpty) + { + // no selection, get all variables + declarators = declarations.SelectMany(d => d.Variables); } + else + { + // has selection, get only the ones that are included in the selection + declarators = declarations.SelectMany(d => d.Variables.Where(v => v.Span.IntersectsWith(span))); + } + + return declarators.Select(d => semanticModel.GetDeclaredSymbol(d, cancellationToken) as IFieldSymbol) + .WhereNotNull() + .Where(f => f.Name.Length != 0) + .ToImmutableArray(); + } - private bool CanEncapsulate(FieldDeclarationSyntax field) - => field.Parent is TypeDeclarationSyntax; + private bool CanEncapsulate(FieldDeclarationSyntax field) + => field.Parent is TypeDeclarationSyntax; - protected override (string fieldName, string propertyName) GenerateFieldAndPropertyNames(IFieldSymbol field) + protected override (string fieldName, string propertyName) GenerateFieldAndPropertyNames(IFieldSymbol field) + { + // Special case: if the field is "new", we will preserve its original name and the new keyword. + if (field.DeclaredAccessibility == Accessibility.Private || IsNew(field)) { - // Special case: if the field is "new", we will preserve its original name and the new keyword. - if (field.DeclaredAccessibility == Accessibility.Private || IsNew(field)) + // Create some capitalized version of the field name for the property + return (field.Name, MakeUnique(GeneratePropertyName(field.Name), field.ContainingType)); + } + else + { + // Generate the new property name using the rules from 695042 + var newPropertyName = GeneratePropertyName(field.Name); + + if (newPropertyName == field.Name) { - // Create some capitalized version of the field name for the property - return (field.Name, MakeUnique(GeneratePropertyName(field.Name), field.ContainingType)); + // If we wind up with the field's old name, give the field the unique version of its current name. + return (MakeUnique(GenerateFieldName(field.Name), field.ContainingType), newPropertyName); } - else + + // Otherwise, ensure the property's name is unique. + newPropertyName = MakeUnique(newPropertyName, field.ContainingType); + var newFieldName = GenerateFieldName(newPropertyName); + + // If converting the new property's name into a field name results in the old field name, we're done. + if (newFieldName == field.Name) { - // Generate the new property name using the rules from 695042 - var newPropertyName = GeneratePropertyName(field.Name); - - if (newPropertyName == field.Name) - { - // If we wind up with the field's old name, give the field the unique version of its current name. - return (MakeUnique(GenerateFieldName(field.Name), field.ContainingType), newPropertyName); - } - - // Otherwise, ensure the property's name is unique. - newPropertyName = MakeUnique(newPropertyName, field.ContainingType); - var newFieldName = GenerateFieldName(newPropertyName); - - // If converting the new property's name into a field name results in the old field name, we're done. - if (newFieldName == field.Name) - { - return (newFieldName, newPropertyName); - } - - // Otherwise, ensure the new field name is unique. - return (MakeUnique(newFieldName, field.ContainingType), newPropertyName); + return (newFieldName, newPropertyName); } - } - private static bool IsNew(IFieldSymbol field) - => field.DeclaringSyntaxReferences.Any(static d => d.GetSyntax().GetAncestor().Modifiers.Any(SyntaxKind.NewKeyword)); + // Otherwise, ensure the new field name is unique. + return (MakeUnique(newFieldName, field.ContainingType), newPropertyName); + } + } - private static string GenerateFieldName(string correspondingPropertyName) - => char.ToLower(correspondingPropertyName[0]).ToString() + correspondingPropertyName[1..]; + private static bool IsNew(IFieldSymbol field) + => field.DeclaringSyntaxReferences.Any(static d => d.GetSyntax().GetAncestor().Modifiers.Any(SyntaxKind.NewKeyword)); - protected static string MakeUnique(string baseName, INamedTypeSymbol containingType) - { - var containingTypeMemberNames = containingType.GetAccessibleMembersInThisAndBaseTypes(containingType).Select(m => m.Name); - return NameGenerator.GenerateUniqueName(baseName, containingTypeMemberNames.ToSet(), StringComparer.Ordinal); - } + private static string GenerateFieldName(string correspondingPropertyName) + => char.ToLower(correspondingPropertyName[0]).ToString() + correspondingPropertyName[1..]; - internal override IEnumerable GetConstructorNodes(INamedTypeSymbol containingType) - => containingType.Constructors.SelectMany(c => c.DeclaringSyntaxReferences.Select(d => d.GetSyntax())); + protected static string MakeUnique(string baseName, INamedTypeSymbol containingType) + { + var containingTypeMemberNames = containingType.GetAccessibleMembersInThisAndBaseTypes(containingType).Select(m => m.Name); + return NameGenerator.GenerateUniqueName(baseName, containingTypeMemberNames.ToSet(), StringComparer.Ordinal); } + + internal override IEnumerable GetConstructorNodes(INamedTypeSymbol containingType) + => containingType.Constructors.SelectMany(c => c.DeclaringSyntaxReferences.Select(d => d.GetSyntax())); } diff --git a/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/IPythiaDeclarationNameRecommenderImplmentation.cs b/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/IPythiaDeclarationNameRecommenderImplmentation.cs index c8b96c0daa7dd..ed42934a2c692 100644 --- a/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/IPythiaDeclarationNameRecommenderImplmentation.cs +++ b/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/IPythiaDeclarationNameRecommenderImplmentation.cs @@ -9,29 +9,28 @@ using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.CSharp.ExternalAccess.Pythia.Api +namespace Microsoft.CodeAnalysis.CSharp.ExternalAccess.Pythia.Api; + +internal interface IPythiaDeclarationNameRecommenderImplementation { - internal interface IPythiaDeclarationNameRecommenderImplementation - { - /// - /// Order of returned recommendation decides the order of those items in completion list - /// - public Task> ProvideRecommendationsAsync(PythiaDeclarationNameContext context, CancellationToken cancellationToken); - } + /// + /// Order of returned recommendation decides the order of those items in completion list + /// + public Task> ProvideRecommendationsAsync(PythiaDeclarationNameContext context, CancellationToken cancellationToken); +} - internal readonly struct PythiaDeclarationNameContext(CSharpSyntaxContext context) - { - private readonly CSharpSyntaxContext _context = context; +internal readonly struct PythiaDeclarationNameContext(CSharpSyntaxContext context) +{ + private readonly CSharpSyntaxContext _context = context; - public Document Document => _context.Document; + public Document Document => _context.Document; - public int Position => _context.Position; + public int Position => _context.Position; - public SemanticModel SemanticModel => _context.SemanticModel; + public SemanticModel SemanticModel => _context.SemanticModel; - /// - /// The token to the left of . This token may be touching the position. - /// - public SyntaxToken LeftToken => _context.LeftToken; - } + /// + /// The token to the left of . This token may be touching the position. + /// + public SyntaxToken LeftToken => _context.LeftToken; } diff --git a/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/IPythiaSignatureHelpProviderImplementation.cs b/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/IPythiaSignatureHelpProviderImplementation.cs index cd997de281091..ee7fb648f3754 100644 --- a/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/IPythiaSignatureHelpProviderImplementation.cs +++ b/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/IPythiaSignatureHelpProviderImplementation.cs @@ -7,10 +7,9 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api; + +internal interface IPythiaSignatureHelpProviderImplementation { - internal interface IPythiaSignatureHelpProviderImplementation - { - Task<(ImmutableArray items, int? selectedItemIndex)> GetMethodGroupItemsAndSelectionAsync(ImmutableArray accessibleMethods, Document document, InvocationExpressionSyntax invocationExpression, SemanticModel semanticModel, SymbolInfo currentSymbol, CancellationToken cancellationToken); - } + Task<(ImmutableArray items, int? selectedItemIndex)> GetMethodGroupItemsAndSelectionAsync(ImmutableArray accessibleMethods, Document document, InvocationExpressionSyntax invocationExpression, SemanticModel semanticModel, SymbolInfo currentSymbol, CancellationToken cancellationToken); } diff --git a/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/PythiaSignatureHelpItemWrapper.cs b/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/PythiaSignatureHelpItemWrapper.cs index 6cf06dd0b997f..deb42a0126c6d 100644 --- a/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/PythiaSignatureHelpItemWrapper.cs +++ b/src/Features/CSharp/Portable/ExternalAccess/Pythia/Api/PythiaSignatureHelpItemWrapper.cs @@ -6,21 +6,20 @@ using Microsoft.CodeAnalysis.CSharp.SignatureHelp; using Microsoft.CodeAnalysis.SignatureHelp; -namespace Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api +namespace Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api; + +internal readonly struct PythiaSignatureHelpItemWrapper(SignatureHelpItem underlyingObject) { - internal readonly struct PythiaSignatureHelpItemWrapper(SignatureHelpItem underlyingObject) - { - internal readonly SignatureHelpItem UnderlyingObject = underlyingObject; + internal readonly SignatureHelpItem UnderlyingObject = underlyingObject; - public static SymbolDisplayPart CreateTextDisplayPart(string text) - => new SymbolDisplayPart(SymbolDisplayPartKind.Text, null, text); + public static SymbolDisplayPart CreateTextDisplayPart(string text) + => new SymbolDisplayPart(SymbolDisplayPartKind.Text, null, text); - public static PythiaSignatureHelpItemWrapper CreateFromMethodGroupMethod( - Document document, - IMethodSymbol method, - int position, - SemanticModel semanticModel, - IList descriptionParts) - => new PythiaSignatureHelpItemWrapper(AbstractOrdinaryMethodSignatureHelpProvider.ConvertMethodGroupMethod(document, method, position, semanticModel, descriptionParts)); - } + public static PythiaSignatureHelpItemWrapper CreateFromMethodGroupMethod( + Document document, + IMethodSymbol method, + int position, + SemanticModel semanticModel, + IList descriptionParts) + => new PythiaSignatureHelpItemWrapper(AbstractOrdinaryMethodSignatureHelpProvider.ConvertMethodGroupMethod(document, method, position, semanticModel, descriptionParts)); } diff --git a/src/Features/CSharp/Portable/ExternalAccess/Pythia/PythiaDeclarationNameRecommender.cs b/src/Features/CSharp/Portable/ExternalAccess/Pythia/PythiaDeclarationNameRecommender.cs index ce09a44de96d2..36f28304ef94a 100644 --- a/src/Features/CSharp/Portable/ExternalAccess/Pythia/PythiaDeclarationNameRecommender.cs +++ b/src/Features/CSharp/Portable/ExternalAccess/Pythia/PythiaDeclarationNameRecommender.cs @@ -15,32 +15,31 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.CSharp.ExternalAccess.Pythia +namespace Microsoft.CodeAnalysis.CSharp.ExternalAccess.Pythia; + +[ExportDeclarationNameRecommender(nameof(PythiaDeclarationNameRecommender)), Shared] +[ExtensionOrder(Before = nameof(DeclarationNameRecommender))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class PythiaDeclarationNameRecommender([Import(AllowDefault = true)] Lazy? implementation) : IDeclarationNameRecommender { - [ExportDeclarationNameRecommender(nameof(PythiaDeclarationNameRecommender)), Shared] - [ExtensionOrder(Before = nameof(DeclarationNameRecommender))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class PythiaDeclarationNameRecommender([Import(AllowDefault = true)] Lazy? implementation) : IDeclarationNameRecommender - { - private readonly Lazy? _lazyImplementation = implementation; + private readonly Lazy? _lazyImplementation = implementation; - public async Task> ProvideRecommendedNamesAsync( - CompletionContext completionContext, - Document document, - CSharpSyntaxContext syntaxContext, - NameDeclarationInfo nameInfo, - CancellationToken cancellationToken) - { - if (_lazyImplementation is null || nameInfo.PossibleSymbolKinds.IsEmpty) - return []; + public async Task> ProvideRecommendedNamesAsync( + CompletionContext completionContext, + Document document, + CSharpSyntaxContext syntaxContext, + NameDeclarationInfo nameInfo, + CancellationToken cancellationToken) + { + if (_lazyImplementation is null || nameInfo.PossibleSymbolKinds.IsEmpty) + return []; - var context = new PythiaDeclarationNameContext(syntaxContext); - var result = await _lazyImplementation.Value.ProvideRecommendationsAsync(context, cancellationToken).ConfigureAwait(false); + var context = new PythiaDeclarationNameContext(syntaxContext); + var result = await _lazyImplementation.Value.ProvideRecommendationsAsync(context, cancellationToken).ConfigureAwait(false); - // We just pick the first possible symbol kind for glyph. - return result.SelectAsArray( - name => (name, NameDeclarationInfo.GetGlyph(NameDeclarationInfo.GetSymbolKind(nameInfo.PossibleSymbolKinds[0]), nameInfo.DeclaredAccessibility))); - } + // We just pick the first possible symbol kind for glyph. + return result.SelectAsArray( + name => (name, NameDeclarationInfo.GetGlyph(NameDeclarationInfo.GetSymbolKind(nameInfo.PossibleSymbolKinds[0]), nameInfo.DeclaredAccessibility))); } } diff --git a/src/Features/CSharp/Portable/ExternalAccess/Pythia/PythiaSignatureHelpProvider.cs b/src/Features/CSharp/Portable/ExternalAccess/Pythia/PythiaSignatureHelpProvider.cs index 799368793c673..bc5689d7b345c 100644 --- a/src/Features/CSharp/Portable/ExternalAccess/Pythia/PythiaSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/ExternalAccess/Pythia/PythiaSignatureHelpProvider.cs @@ -13,31 +13,30 @@ using System; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.ExternalAccess.Pythia +namespace Microsoft.CodeAnalysis.ExternalAccess.Pythia; + +/// +/// Ensure this is ordered before the regular invocation signature help provider. +/// We must replace the entire list of results, including both Pythia and non-Pythia recommendations. +/// +[ExportSignatureHelpProvider(nameof(PythiaSignatureHelpProvider), LanguageNames.CSharp), Shared] +[ExtensionOrder(Before = nameof(InvocationExpressionSignatureHelpProvider))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class PythiaSignatureHelpProvider(Lazy implementation) : InvocationExpressionSignatureHelpProviderBase { - /// - /// Ensure this is ordered before the regular invocation signature help provider. - /// We must replace the entire list of results, including both Pythia and non-Pythia recommendations. - /// - [ExportSignatureHelpProvider(nameof(PythiaSignatureHelpProvider), LanguageNames.CSharp), Shared] - [ExtensionOrder(Before = nameof(InvocationExpressionSignatureHelpProvider))] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class PythiaSignatureHelpProvider(Lazy implementation) : InvocationExpressionSignatureHelpProviderBase - { - private readonly Lazy _lazyImplementation = implementation; + private readonly Lazy _lazyImplementation = implementation; - internal override async Task<(ImmutableArray items, int? selectedItemIndex)> GetMethodGroupItemsAndSelectionAsync( - ImmutableArray accessibleMethods, - Document document, - InvocationExpressionSyntax invocationExpression, - SemanticModel semanticModel, - SymbolInfo symbolInfo, - IMethodSymbol? currentSymbol, - CancellationToken cancellationToken) - { - var (items, selectedItemIndex) = await _lazyImplementation.Value.GetMethodGroupItemsAndSelectionAsync(accessibleMethods, document, invocationExpression, semanticModel, symbolInfo, cancellationToken).ConfigureAwait(false); - return (items.SelectAsArray(item => item.UnderlyingObject), selectedItemIndex); - } + internal override async Task<(ImmutableArray items, int? selectedItemIndex)> GetMethodGroupItemsAndSelectionAsync( + ImmutableArray accessibleMethods, + Document document, + InvocationExpressionSyntax invocationExpression, + SemanticModel semanticModel, + SymbolInfo symbolInfo, + IMethodSymbol? currentSymbol, + CancellationToken cancellationToken) + { + var (items, selectedItemIndex) = await _lazyImplementation.Value.GetMethodGroupItemsAndSelectionAsync(accessibleMethods, document, invocationExpression, semanticModel, symbolInfo, cancellationToken).ConfigureAwait(false); + return (items.SelectAsArray(item => item.UnderlyingObject), selectedItemIndex); } } diff --git a/src/Features/CSharp/Portable/ExtractInterface/CSharpExtractInterfaceService.cs b/src/Features/CSharp/Portable/ExtractInterface/CSharpExtractInterfaceService.cs index 58e85cdac18b7..65093c40adbab 100644 --- a/src/Features/CSharp/Portable/ExtractInterface/CSharpExtractInterfaceService.cs +++ b/src/Features/CSharp/Portable/ExtractInterface/CSharpExtractInterfaceService.cs @@ -21,61 +21,60 @@ using Microsoft.CodeAnalysis.CodeRefactorings; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractInterface +namespace Microsoft.CodeAnalysis.CSharp.ExtractInterface; + +[ExportLanguageService(typeof(AbstractExtractInterfaceService), LanguageNames.CSharp), Shared] +internal sealed class CSharpExtractInterfaceService : AbstractExtractInterfaceService { - [ExportLanguageService(typeof(AbstractExtractInterfaceService), LanguageNames.CSharp), Shared] - internal sealed class CSharpExtractInterfaceService : AbstractExtractInterfaceService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpExtractInterfaceService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpExtractInterfaceService() - { - } + } - protected override async Task GetTypeDeclarationAsync(Document document, int position, TypeDiscoveryRule typeDiscoveryRule, CancellationToken cancellationToken) - { - var span = new TextSpan(position, 0); - var typeDeclarationNode = await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); + protected override async Task GetTypeDeclarationAsync(Document document, int position, TypeDiscoveryRule typeDiscoveryRule, CancellationToken cancellationToken) + { + var span = new TextSpan(position, 0); + var typeDeclarationNode = await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); - // If TypeDiscoverRule is set to TypeDeclaration, a position anywhere inside of the - // declaration enclosure is valid. In this case check to see if there is a type declaration ancestor - // of the focused node. - if (typeDeclarationNode == null && typeDiscoveryRule == TypeDiscoveryRule.TypeDeclaration) - { - var relevantNode = await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); - return relevantNode.GetAncestor(); - } + // If TypeDiscoverRule is set to TypeDeclaration, a position anywhere inside of the + // declaration enclosure is valid. In this case check to see if there is a type declaration ancestor + // of the focused node. + if (typeDeclarationNode == null && typeDiscoveryRule == TypeDiscoveryRule.TypeDeclaration) + { + var relevantNode = await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); + return relevantNode.GetAncestor(); + } - return typeDeclarationNode; + return typeDeclarationNode; - } + } - internal override string GetContainingNamespaceDisplay(INamedTypeSymbol typeSymbol, CompilationOptions compilationOptions) - { - return typeSymbol.ContainingNamespace.IsGlobalNamespace - ? string.Empty - : typeSymbol.ContainingNamespace.ToDisplayString(); - } + internal override string GetContainingNamespaceDisplay(INamedTypeSymbol typeSymbol, CompilationOptions compilationOptions) + { + return typeSymbol.ContainingNamespace.IsGlobalNamespace + ? string.Empty + : typeSymbol.ContainingNamespace.ToDisplayString(); + } - internal override bool IsExtractableMember(ISymbol m) - => base.IsExtractableMember(m) && !m.ExplicitInterfaceImplementations().Any(); + internal override bool IsExtractableMember(ISymbol m) + => base.IsExtractableMember(m) && !m.ExplicitInterfaceImplementations().Any(); - internal override bool ShouldIncludeAccessibilityModifier(SyntaxNode typeNode) - { - var typeDeclaration = typeNode as TypeDeclarationSyntax; - return typeDeclaration.Modifiers.Any(m => SyntaxFacts.IsAccessibilityModifier(m.Kind())); - } + internal override bool ShouldIncludeAccessibilityModifier(SyntaxNode typeNode) + { + var typeDeclaration = typeNode as TypeDeclarationSyntax; + return typeDeclaration.Modifiers.Any(m => SyntaxFacts.IsAccessibilityModifier(m.Kind())); + } - protected override Task UpdateMembersWithExplicitImplementationsAsync( - Solution unformattedSolution, IReadOnlyList documentIds, - INamedTypeSymbol extractedInterface, INamedTypeSymbol typeToExtractFrom, - IEnumerable includedMembers, ImmutableDictionary symbolToDeclarationMap, - CancellationToken cancellationToken) - { - // In C#, member implementations do not always need - // to be explicitly added. It's safe enough to return - // the passed in solution - return Task.FromResult(unformattedSolution); - } + protected override Task UpdateMembersWithExplicitImplementationsAsync( + Solution unformattedSolution, IReadOnlyList documentIds, + INamedTypeSymbol extractedInterface, INamedTypeSymbol typeToExtractFrom, + IEnumerable includedMembers, ImmutableDictionary symbolToDeclarationMap, + CancellationToken cancellationToken) + { + // In C#, member implementations do not always need + // to be explicitly added. It's safe enough to return + // the passed in solution + return Task.FromResult(unformattedSolution); } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.Analyzer.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.Analyzer.cs index ec46ec2b07df1..030509decb409 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.Analyzer.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.Analyzer.cs @@ -10,68 +10,67 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.ExtractMethod; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal partial class CSharpMethodExtractor { - internal partial class CSharpMethodExtractor + private class CSharpAnalyzer(CSharpSelectionResult selectionResult, bool localFunction, CancellationToken cancellationToken) : Analyzer(selectionResult, localFunction, cancellationToken) { - private class CSharpAnalyzer(CSharpSelectionResult selectionResult, bool localFunction, CancellationToken cancellationToken) : Analyzer(selectionResult, localFunction, cancellationToken) + private static readonly HashSet s_nonNoisySyntaxKindSet = new HashSet(new int[] { (int)SyntaxKind.WhitespaceTrivia, (int)SyntaxKind.EndOfLineTrivia }); + + public static AnalyzerResult Analyze(CSharpSelectionResult selectionResult, bool localFunction, CancellationToken cancellationToken) { - private static readonly HashSet s_nonNoisySyntaxKindSet = new HashSet(new int[] { (int)SyntaxKind.WhitespaceTrivia, (int)SyntaxKind.EndOfLineTrivia }); + var analyzer = new CSharpAnalyzer(selectionResult, localFunction, cancellationToken); + return analyzer.Analyze(); + } - public static AnalyzerResult Analyze(CSharpSelectionResult selectionResult, bool localFunction, CancellationToken cancellationToken) - { - var analyzer = new CSharpAnalyzer(selectionResult, localFunction, cancellationToken); - return analyzer.Analyze(); - } + protected override bool TreatOutAsRef => false; - protected override bool TreatOutAsRef => false; + protected override VariableInfo CreateFromSymbol( + Compilation compilation, + ISymbol symbol, + ITypeSymbol type, + VariableStyle style, + bool variableDeclared) + { + return CreateFromSymbolCommon(compilation, symbol, type, style, s_nonNoisySyntaxKindSet); + } - protected override VariableInfo CreateFromSymbol( - Compilation compilation, - ISymbol symbol, - ITypeSymbol type, - VariableStyle style, - bool variableDeclared) + protected override ITypeSymbol GetRangeVariableType(SemanticModel model, IRangeVariableSymbol symbol) + { + var info = model.GetSpeculativeTypeInfo(SelectionResult.FinalSpan.Start, SyntaxFactory.ParseName(symbol.Name), SpeculativeBindingOption.BindAsExpression); + if (Microsoft.CodeAnalysis.Shared.Extensions.ISymbolExtensions.IsErrorType(info.Type)) { - return CreateFromSymbolCommon(compilation, symbol, type, style, s_nonNoisySyntaxKindSet); + return null; } - protected override ITypeSymbol GetRangeVariableType(SemanticModel model, IRangeVariableSymbol symbol) - { - var info = model.GetSpeculativeTypeInfo(SelectionResult.FinalSpan.Start, SyntaxFactory.ParseName(symbol.Name), SpeculativeBindingOption.BindAsExpression); - if (Microsoft.CodeAnalysis.Shared.Extensions.ISymbolExtensions.IsErrorType(info.Type)) - { - return null; - } + return info.Type == null || info.Type.SpecialType == Microsoft.CodeAnalysis.SpecialType.System_Object + ? info.Type + : info.ConvertedType; + } - return info.Type == null || info.Type.SpecialType == Microsoft.CodeAnalysis.SpecialType.System_Object - ? info.Type - : info.ConvertedType; - } + protected override bool ContainsReturnStatementInSelectedCode(IEnumerable jumpOutOfRegionStatements) + => jumpOutOfRegionStatements.Where(n => n is ReturnStatementSyntax).Any(); - protected override bool ContainsReturnStatementInSelectedCode(IEnumerable jumpOutOfRegionStatements) - => jumpOutOfRegionStatements.Where(n => n is ReturnStatementSyntax).Any(); + protected override bool ReadOnlyFieldAllowed() + { + var scope = SelectionResult.GetContainingScopeOf(); + return scope == null; + } - protected override bool ReadOnlyFieldAllowed() - { - var scope = SelectionResult.GetContainingScopeOf(); - return scope == null; - } + protected override ITypeSymbol GetSymbolType(SemanticModel semanticModel, ISymbol symbol) + { + var selectionOperation = semanticModel.GetOperation(SelectionResult.GetContainingScope()); - protected override ITypeSymbol GetSymbolType(SemanticModel semanticModel, ISymbol symbol) + // Check if null is possibly assigned to the symbol. If it is, leave nullable annotation as is, otherwise + // we can modify the annotation to be NotAnnotated to code that more likely matches the user's intent. + if (selectionOperation is not null && + NullableHelpers.IsSymbolAssignedPossiblyNullValue(semanticModel, selectionOperation, symbol) == false) { - var selectionOperation = semanticModel.GetOperation(SelectionResult.GetContainingScope()); - - // Check if null is possibly assigned to the symbol. If it is, leave nullable annotation as is, otherwise - // we can modify the annotation to be NotAnnotated to code that more likely matches the user's intent. - if (selectionOperation is not null && - NullableHelpers.IsSymbolAssignedPossiblyNullValue(semanticModel, selectionOperation, symbol) == false) - { - return base.GetSymbolType(semanticModel, symbol).WithNullableAnnotation(NullableAnnotation.NotAnnotated); - } - - return base.GetSymbolType(semanticModel, symbol); + return base.GetSymbolType(semanticModel, symbol).WithNullableAnnotation(NullableAnnotation.NotAnnotated); } + + return base.GetSymbolType(semanticModel, symbol); } } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.CallSiteContainerRewriter.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.CallSiteContainerRewriter.cs index 1ae9ab87d497a..61d7617c5d049 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.CallSiteContainerRewriter.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.CallSiteContainerRewriter.cs @@ -14,424 +14,423 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal partial class CSharpMethodExtractor { - internal partial class CSharpMethodExtractor + private abstract partial class CSharpCodeGenerator { - private abstract partial class CSharpCodeGenerator + private sealed class CallSiteContainerRewriter : CSharpSyntaxRewriter { - private sealed class CallSiteContainerRewriter : CSharpSyntaxRewriter + private readonly SyntaxNode _outmostCallSiteContainer; + private readonly HashSet _variableToRemoveMap; + private readonly SyntaxNode _firstStatementOrFieldToReplace; + private readonly SyntaxNode _lastStatementOrFieldToReplace; + private readonly ImmutableArray _statementsOrMemberOrAccessorToInsert; + + public CallSiteContainerRewriter( + SyntaxNode outmostCallSiteContainer, + HashSet variableToRemoveMap, + SyntaxNode firstStatementOrFieldToReplace, + SyntaxNode lastStatementOrFieldToReplace, + ImmutableArray statementsOrFieldToInsert) { - private readonly SyntaxNode _outmostCallSiteContainer; - private readonly HashSet _variableToRemoveMap; - private readonly SyntaxNode _firstStatementOrFieldToReplace; - private readonly SyntaxNode _lastStatementOrFieldToReplace; - private readonly ImmutableArray _statementsOrMemberOrAccessorToInsert; - - public CallSiteContainerRewriter( - SyntaxNode outmostCallSiteContainer, - HashSet variableToRemoveMap, - SyntaxNode firstStatementOrFieldToReplace, - SyntaxNode lastStatementOrFieldToReplace, - ImmutableArray statementsOrFieldToInsert) - { - Contract.ThrowIfNull(outmostCallSiteContainer); - Contract.ThrowIfNull(variableToRemoveMap); - Contract.ThrowIfNull(firstStatementOrFieldToReplace); - Contract.ThrowIfNull(lastStatementOrFieldToReplace); - Contract.ThrowIfTrue(statementsOrFieldToInsert.IsDefaultOrEmpty); + Contract.ThrowIfNull(outmostCallSiteContainer); + Contract.ThrowIfNull(variableToRemoveMap); + Contract.ThrowIfNull(firstStatementOrFieldToReplace); + Contract.ThrowIfNull(lastStatementOrFieldToReplace); + Contract.ThrowIfTrue(statementsOrFieldToInsert.IsDefaultOrEmpty); - _outmostCallSiteContainer = outmostCallSiteContainer; + _outmostCallSiteContainer = outmostCallSiteContainer; - _variableToRemoveMap = variableToRemoveMap; - _firstStatementOrFieldToReplace = firstStatementOrFieldToReplace; - _lastStatementOrFieldToReplace = lastStatementOrFieldToReplace; - _statementsOrMemberOrAccessorToInsert = statementsOrFieldToInsert; + _variableToRemoveMap = variableToRemoveMap; + _firstStatementOrFieldToReplace = firstStatementOrFieldToReplace; + _lastStatementOrFieldToReplace = lastStatementOrFieldToReplace; + _statementsOrMemberOrAccessorToInsert = statementsOrFieldToInsert; - Contract.ThrowIfFalse(_firstStatementOrFieldToReplace.Parent == _lastStatementOrFieldToReplace.Parent - || CSharpSyntaxFacts.Instance.AreStatementsInSameContainer(_firstStatementOrFieldToReplace, _lastStatementOrFieldToReplace)); - } + Contract.ThrowIfFalse(_firstStatementOrFieldToReplace.Parent == _lastStatementOrFieldToReplace.Parent + || CSharpSyntaxFacts.Instance.AreStatementsInSameContainer(_firstStatementOrFieldToReplace, _lastStatementOrFieldToReplace)); + } - public SyntaxNode Generate() - => Visit(_outmostCallSiteContainer); + public SyntaxNode Generate() + => Visit(_outmostCallSiteContainer); - private SyntaxNode ContainerOfStatementsOrFieldToReplace => _firstStatementOrFieldToReplace.Parent; + private SyntaxNode ContainerOfStatementsOrFieldToReplace => _firstStatementOrFieldToReplace.Parent; - public override SyntaxNode VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node) + public override SyntaxNode VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node) + { + node = (LocalDeclarationStatementSyntax)base.VisitLocalDeclarationStatement(node); + var list = new List(); + var triviaList = new List(); + // go through each var decls in decl statement + foreach (var variable in node.Declaration.Variables) { - node = (LocalDeclarationStatementSyntax)base.VisitLocalDeclarationStatement(node); - var list = new List(); - var triviaList = new List(); - // go through each var decls in decl statement - foreach (var variable in node.Declaration.Variables) + if (_variableToRemoveMap.HasSyntaxAnnotation(variable)) { - if (_variableToRemoveMap.HasSyntaxAnnotation(variable)) - { - // if it had initialization, it shouldn't reach here. - Contract.ThrowIfFalse(variable.Initializer == null); - - // we don't remove trivia around tokens we remove - triviaList.AddRange(variable.GetLeadingTrivia()); - triviaList.AddRange(variable.GetTrailingTrivia()); - continue; - } - - if (triviaList.Count > 0) - { - list.Add(variable.WithPrependedLeadingTrivia(triviaList)); - triviaList.Clear(); - continue; - } - - list.Add(variable); - } + // if it had initialization, it shouldn't reach here. + Contract.ThrowIfFalse(variable.Initializer == null); - if (list.Count == 0) - { - // nothing has survived. remove this from the list - if (triviaList.Count == 0) - { - return null; - } - - // well, there are trivia associated with the node. - // we can't just delete the node since then, we will lose - // the trivia. unfortunately, it is not easy to attach the trivia - // to next token. for now, create an empty statement and associate the - // trivia to the statement - - // TODO : think about a way to move the trivia to next token. - return SyntaxFactory.EmptyStatement(SyntaxFactory.Token([.. triviaList], SyntaxKind.SemicolonToken, [SyntaxFactory.ElasticMarker])); + // we don't remove trivia around tokens we remove + triviaList.AddRange(variable.GetLeadingTrivia()); + triviaList.AddRange(variable.GetTrailingTrivia()); + continue; } - if (list.Count == node.Declaration.Variables.Count) + if (triviaList.Count > 0) { - // nothing has changed, return as it is - return node; + list.Add(variable.WithPrependedLeadingTrivia(triviaList)); + triviaList.Clear(); + continue; } - // TODO : fix how it manipulate trivia later - - // if there is left over syntax trivia, it will be attached to leading trivia - // of semicolon - return - SyntaxFactory.LocalDeclarationStatement( - node.Modifiers, - SyntaxFactory.VariableDeclaration( - node.Declaration.Type, - [.. list]), - node.SemicolonToken.WithPrependedLeadingTrivia(triviaList)); + list.Add(variable); } - // for every kind of extract methods - public override SyntaxNode VisitBlock(BlockSyntax node) + if (list.Count == 0) { - if (node != ContainerOfStatementsOrFieldToReplace) + // nothing has survived. remove this from the list + if (triviaList.Count == 0) { - // make sure we visit nodes under the block - return base.VisitBlock(node); + return null; } - return node.WithStatements([.. VisitList(ReplaceStatements(node.Statements))]); + // well, there are trivia associated with the node. + // we can't just delete the node since then, we will lose + // the trivia. unfortunately, it is not easy to attach the trivia + // to next token. for now, create an empty statement and associate the + // trivia to the statement + + // TODO : think about a way to move the trivia to next token. + return SyntaxFactory.EmptyStatement(SyntaxFactory.Token([.. triviaList], SyntaxKind.SemicolonToken, [SyntaxFactory.ElasticMarker])); } - public override SyntaxNode VisitSwitchSection(SwitchSectionSyntax node) + if (list.Count == node.Declaration.Variables.Count) { - if (node != ContainerOfStatementsOrFieldToReplace) - { - // make sure we visit nodes under the switch section - return base.VisitSwitchSection(node); - } - - return node.WithStatements([.. VisitList(ReplaceStatements(node.Statements))]); + // nothing has changed, return as it is + return node; } - // only for single statement or expression - public override SyntaxNode VisitLabeledStatement(LabeledStatementSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitLabeledStatement(node); - } + // TODO : fix how it manipulate trivia later + + // if there is left over syntax trivia, it will be attached to leading trivia + // of semicolon + return + SyntaxFactory.LocalDeclarationStatement( + node.Modifiers, + SyntaxFactory.VariableDeclaration( + node.Declaration.Type, + [.. list]), + node.SemicolonToken.WithPrependedLeadingTrivia(triviaList)); + } - return node.WithStatement(ReplaceStatementIfNeeded(node.Statement)); + // for every kind of extract methods + public override SyntaxNode VisitBlock(BlockSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + // make sure we visit nodes under the block + return base.VisitBlock(node); } - public override SyntaxNode VisitElseClause(ElseClauseSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitElseClause(node); - } + return node.WithStatements([.. VisitList(ReplaceStatements(node.Statements))]); + } - return node.WithStatement(ReplaceStatementIfNeeded(node.Statement)); + public override SyntaxNode VisitSwitchSection(SwitchSectionSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + // make sure we visit nodes under the switch section + return base.VisitSwitchSection(node); } - public override SyntaxNode VisitIfStatement(IfStatementSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitIfStatement(node); - } + return node.WithStatements([.. VisitList(ReplaceStatements(node.Statements))]); + } - return node.WithCondition(VisitNode(node.Condition)) - .WithStatement(ReplaceStatementIfNeeded(node.Statement)) - .WithElse(VisitNode(node.Else)); + // only for single statement or expression + public override SyntaxNode VisitLabeledStatement(LabeledStatementSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitLabeledStatement(node); } - public override SyntaxNode VisitLockStatement(LockStatementSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitLockStatement(node); - } + return node.WithStatement(ReplaceStatementIfNeeded(node.Statement)); + } - return node.WithExpression(VisitNode(node.Expression)) - .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + public override SyntaxNode VisitElseClause(ElseClauseSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitElseClause(node); } - public override SyntaxNode VisitFixedStatement(FixedStatementSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitFixedStatement(node); - } + return node.WithStatement(ReplaceStatementIfNeeded(node.Statement)); + } - return node.WithDeclaration(VisitNode(node.Declaration)) - .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + public override SyntaxNode VisitIfStatement(IfStatementSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitIfStatement(node); } - public override SyntaxNode VisitUsingStatement(UsingStatementSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitUsingStatement(node); - } + return node.WithCondition(VisitNode(node.Condition)) + .WithStatement(ReplaceStatementIfNeeded(node.Statement)) + .WithElse(VisitNode(node.Else)); + } - return node.WithDeclaration(VisitNode(node.Declaration)) - .WithExpression(VisitNode(node.Expression)) - .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + public override SyntaxNode VisitLockStatement(LockStatementSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitLockStatement(node); } - public override SyntaxNode VisitForEachStatement(ForEachStatementSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitForEachStatement(node); - } + return node.WithExpression(VisitNode(node.Expression)) + .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + } - return node.WithExpression(VisitNode(node.Expression)) - .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + public override SyntaxNode VisitFixedStatement(FixedStatementSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitFixedStatement(node); } - public override SyntaxNode VisitForEachVariableStatement(ForEachVariableStatementSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitForEachVariableStatement(node); - } + return node.WithDeclaration(VisitNode(node.Declaration)) + .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + } - return node.WithExpression(VisitNode(node.Expression)) - .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + public override SyntaxNode VisitUsingStatement(UsingStatementSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitUsingStatement(node); } - public override SyntaxNode VisitForStatement(ForStatementSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitForStatement(node); - } + return node.WithDeclaration(VisitNode(node.Declaration)) + .WithExpression(VisitNode(node.Expression)) + .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + } - return node.WithDeclaration(VisitNode(node.Declaration)) - .WithInitializers(VisitList(node.Initializers)) - .WithCondition(VisitNode(node.Condition)) - .WithIncrementors(VisitList(node.Incrementors)) - .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + public override SyntaxNode VisitForEachStatement(ForEachStatementSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitForEachStatement(node); } - public override SyntaxNode VisitDoStatement(DoStatementSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitDoStatement(node); - } + return node.WithExpression(VisitNode(node.Expression)) + .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + } - return node.WithStatement(ReplaceStatementIfNeeded(node.Statement)) - .WithCondition(VisitNode(node.Condition)); + public override SyntaxNode VisitForEachVariableStatement(ForEachVariableStatementSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitForEachVariableStatement(node); } - public override SyntaxNode VisitWhileStatement(WhileStatementSyntax node) + return node.WithExpression(VisitNode(node.Expression)) + .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + } + + public override SyntaxNode VisitForStatement(ForStatementSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitWhileStatement(node); - } + return base.VisitForStatement(node); + } + + return node.WithDeclaration(VisitNode(node.Declaration)) + .WithInitializers(VisitList(node.Initializers)) + .WithCondition(VisitNode(node.Condition)) + .WithIncrementors(VisitList(node.Incrementors)) + .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + } - return node.WithCondition(VisitNode(node.Condition)) - .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + public override SyntaxNode VisitDoStatement(DoStatementSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitDoStatement(node); } - private TNode VisitNode(TNode node) where TNode : SyntaxNode - => (TNode)Visit(node); + return node.WithStatement(ReplaceStatementIfNeeded(node.Statement)) + .WithCondition(VisitNode(node.Condition)); + } - private StatementSyntax ReplaceStatementIfNeeded(StatementSyntax statement) + public override SyntaxNode VisitWhileStatement(WhileStatementSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) { - Contract.ThrowIfNull(statement); + return base.VisitWhileStatement(node); + } - // if all three same - if ((statement != _firstStatementOrFieldToReplace) || (_firstStatementOrFieldToReplace != _lastStatementOrFieldToReplace)) - return statement; + return node.WithCondition(VisitNode(node.Condition)) + .WithStatement(ReplaceStatementIfNeeded(node.Statement)); + } - // replace one statement with another - if (_statementsOrMemberOrAccessorToInsert.Length == 1) - return _statementsOrMemberOrAccessorToInsert.Cast().Single(); + private TNode VisitNode(TNode node) where TNode : SyntaxNode + => (TNode)Visit(node); - // replace one statement with multiple statements (see bug # 6310) - var statements = _statementsOrMemberOrAccessorToInsert.CastArray(); - return SyntaxFactory.Block(statements); - } + private StatementSyntax ReplaceStatementIfNeeded(StatementSyntax statement) + { + Contract.ThrowIfNull(statement); - private SyntaxList ReplaceList(SyntaxList list) - where TSyntax : SyntaxNode - { - // okay, this visit contains the statement - var newList = new List(list); + // if all three same + if ((statement != _firstStatementOrFieldToReplace) || (_firstStatementOrFieldToReplace != _lastStatementOrFieldToReplace)) + return statement; - var firstIndex = newList.FindIndex(s => s == _firstStatementOrFieldToReplace); - Contract.ThrowIfFalse(firstIndex >= 0); + // replace one statement with another + if (_statementsOrMemberOrAccessorToInsert.Length == 1) + return _statementsOrMemberOrAccessorToInsert.Cast().Single(); - var lastIndex = newList.FindIndex(s => s == _lastStatementOrFieldToReplace); - Contract.ThrowIfFalse(lastIndex >= 0); + // replace one statement with multiple statements (see bug # 6310) + var statements = _statementsOrMemberOrAccessorToInsert.CastArray(); + return SyntaxFactory.Block(statements); + } - Contract.ThrowIfFalse(firstIndex <= lastIndex); + private SyntaxList ReplaceList(SyntaxList list) + where TSyntax : SyntaxNode + { + // okay, this visit contains the statement + var newList = new List(list); - // remove statement that must be removed - newList.RemoveRange(firstIndex, lastIndex - firstIndex + 1); + var firstIndex = newList.FindIndex(s => s == _firstStatementOrFieldToReplace); + Contract.ThrowIfFalse(firstIndex >= 0); - // add new statements to replace - newList.InsertRange(firstIndex, _statementsOrMemberOrAccessorToInsert.Cast()); + var lastIndex = newList.FindIndex(s => s == _lastStatementOrFieldToReplace); + Contract.ThrowIfFalse(lastIndex >= 0); - return [.. newList]; - } + Contract.ThrowIfFalse(firstIndex <= lastIndex); - private SyntaxList ReplaceStatements(SyntaxList statements) - => ReplaceList(statements); + // remove statement that must be removed + newList.RemoveRange(firstIndex, lastIndex - firstIndex + 1); - private SyntaxList ReplaceAccessors(SyntaxList accessors) - => ReplaceList(accessors); + // add new statements to replace + newList.InsertRange(firstIndex, _statementsOrMemberOrAccessorToInsert.Cast()); - private SyntaxList ReplaceMembers(SyntaxList members, bool global) - { - // okay, this visit contains the statement - var newMembers = new List(members); + return [.. newList]; + } - var firstMemberIndex = newMembers.FindIndex(s => s == (global ? _firstStatementOrFieldToReplace.Parent : _firstStatementOrFieldToReplace)); - Contract.ThrowIfFalse(firstMemberIndex >= 0); + private SyntaxList ReplaceStatements(SyntaxList statements) + => ReplaceList(statements); - var lastMemberIndex = newMembers.FindIndex(s => s == (global ? _lastStatementOrFieldToReplace.Parent : _lastStatementOrFieldToReplace)); - Contract.ThrowIfFalse(lastMemberIndex >= 0); + private SyntaxList ReplaceAccessors(SyntaxList accessors) + => ReplaceList(accessors); - Contract.ThrowIfFalse(firstMemberIndex <= lastMemberIndex); + private SyntaxList ReplaceMembers(SyntaxList members, bool global) + { + // okay, this visit contains the statement + var newMembers = new List(members); - // remove statement that must be removed - newMembers.RemoveRange(firstMemberIndex, lastMemberIndex - firstMemberIndex + 1); + var firstMemberIndex = newMembers.FindIndex(s => s == (global ? _firstStatementOrFieldToReplace.Parent : _firstStatementOrFieldToReplace)); + Contract.ThrowIfFalse(firstMemberIndex >= 0); - // add new statements to replace - newMembers.InsertRange(firstMemberIndex, - _statementsOrMemberOrAccessorToInsert.Select(s => global ? SyntaxFactory.GlobalStatement((StatementSyntax)s) : (MemberDeclarationSyntax)s)); + var lastMemberIndex = newMembers.FindIndex(s => s == (global ? _lastStatementOrFieldToReplace.Parent : _lastStatementOrFieldToReplace)); + Contract.ThrowIfFalse(lastMemberIndex >= 0); - return [.. newMembers]; - } + Contract.ThrowIfFalse(firstMemberIndex <= lastMemberIndex); - public override SyntaxNode VisitGlobalStatement(GlobalStatementSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitGlobalStatement(node); - } + // remove statement that must be removed + newMembers.RemoveRange(firstMemberIndex, lastMemberIndex - firstMemberIndex + 1); - return node.WithStatement(ReplaceStatementIfNeeded(node.Statement)); - } + // add new statements to replace + newMembers.InsertRange(firstMemberIndex, + _statementsOrMemberOrAccessorToInsert.Select(s => global ? SyntaxFactory.GlobalStatement((StatementSyntax)s) : (MemberDeclarationSyntax)s)); - public override SyntaxNode VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitInterfaceDeclaration(node); - } + return [.. newMembers]; + } - return GetUpdatedTypeDeclaration(node); + public override SyntaxNode VisitGlobalStatement(GlobalStatementSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitGlobalStatement(node); } - public override SyntaxNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitConstructorDeclaration(node); - } + return node.WithStatement(ReplaceStatementIfNeeded(node.Statement)); + } - Contract.ThrowIfFalse(_firstStatementOrFieldToReplace == _lastStatementOrFieldToReplace); - return node.WithInitializer((ConstructorInitializerSyntax)_statementsOrMemberOrAccessorToInsert.Single()); + public override SyntaxNode VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitInterfaceDeclaration(node); } - public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitClassDeclaration(node); - } + return GetUpdatedTypeDeclaration(node); + } - return GetUpdatedTypeDeclaration(node); + public override SyntaxNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitConstructorDeclaration(node); } - public override SyntaxNode VisitRecordDeclaration(RecordDeclarationSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitRecordDeclaration(node); - } + Contract.ThrowIfFalse(_firstStatementOrFieldToReplace == _lastStatementOrFieldToReplace); + return node.WithInitializer((ConstructorInitializerSyntax)_statementsOrMemberOrAccessorToInsert.Single()); + } - return GetUpdatedTypeDeclaration(node); + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitClassDeclaration(node); } - public override SyntaxNode VisitStructDeclaration(StructDeclarationSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitStructDeclaration(node); - } + return GetUpdatedTypeDeclaration(node); + } - return GetUpdatedTypeDeclaration(node); + public override SyntaxNode VisitRecordDeclaration(RecordDeclarationSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitRecordDeclaration(node); } - public override SyntaxNode VisitAccessorList(AccessorListSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace) - { - return base.VisitAccessorList(node); - } + return GetUpdatedTypeDeclaration(node); + } - var newAccessors = VisitList(ReplaceAccessors(node.Accessors)); - return node.WithAccessors(newAccessors); + public override SyntaxNode VisitStructDeclaration(StructDeclarationSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitStructDeclaration(node); } - public override SyntaxNode VisitCompilationUnit(CompilationUnitSyntax node) - { - if (node != ContainerOfStatementsOrFieldToReplace.Parent) - { - // make sure we visit nodes under the block - return base.VisitCompilationUnit(node); - } + return GetUpdatedTypeDeclaration(node); + } - var newMembers = VisitList(ReplaceMembers(node.Members, global: true)); - return node.WithMembers(newMembers); + public override SyntaxNode VisitAccessorList(AccessorListSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace) + { + return base.VisitAccessorList(node); } - private SyntaxNode GetUpdatedTypeDeclaration(TypeDeclarationSyntax node) + var newAccessors = VisitList(ReplaceAccessors(node.Accessors)); + return node.WithAccessors(newAccessors); + } + + public override SyntaxNode VisitCompilationUnit(CompilationUnitSyntax node) + { + if (node != ContainerOfStatementsOrFieldToReplace.Parent) { - var newMembers = VisitList(ReplaceMembers(node.Members, global: false)); - return node.WithMembers(newMembers); + // make sure we visit nodes under the block + return base.VisitCompilationUnit(node); } + + var newMembers = VisitList(ReplaceMembers(node.Members, global: true)); + return node.WithMembers(newMembers); + } + + private SyntaxNode GetUpdatedTypeDeclaration(TypeDeclarationSyntax node) + { + var newMembers = VisitList(ReplaceMembers(node.Members, global: false)); + return node.WithMembers(newMembers); } } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.ExpressionCodeGenerator.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.ExpressionCodeGenerator.cs index 186767f5f9270..0730034b535fc 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.ExpressionCodeGenerator.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.ExpressionCodeGenerator.cs @@ -18,168 +18,167 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal partial class CSharpMethodExtractor { - internal partial class CSharpMethodExtractor + private partial class CSharpCodeGenerator { - private partial class CSharpCodeGenerator + private sealed class ExpressionCodeGenerator( + CSharpSelectionResult selectionResult, + AnalyzerResult analyzerResult, + CSharpCodeGenerationOptions options, + bool localFunction) : CSharpCodeGenerator(selectionResult, analyzerResult, options, localFunction) { - private sealed class ExpressionCodeGenerator( - CSharpSelectionResult selectionResult, - AnalyzerResult analyzerResult, - CSharpCodeGenerationOptions options, - bool localFunction) : CSharpCodeGenerator(selectionResult, analyzerResult, options, localFunction) + protected override SyntaxToken CreateMethodName() { - protected override SyntaxToken CreateMethodName() - { - var methodName = GenerateMethodNameFromUserPreference(); + var methodName = GenerateMethodNameFromUserPreference(); + + var containingScope = this.SelectionResult.GetContainingScope(); - var containingScope = this.SelectionResult.GetContainingScope(); + methodName = GetMethodNameBasedOnExpression(methodName, containingScope); - methodName = GetMethodNameBasedOnExpression(methodName, containingScope); + var semanticModel = SemanticDocument.SemanticModel; + var nameGenerator = new UniqueNameGenerator(semanticModel); + return SyntaxFactory.Identifier(nameGenerator.CreateUniqueMethodName(containingScope, methodName)); + } - var semanticModel = SemanticDocument.SemanticModel; - var nameGenerator = new UniqueNameGenerator(semanticModel); - return SyntaxFactory.Identifier(nameGenerator.CreateUniqueMethodName(containingScope, methodName)); + private static string GetMethodNameBasedOnExpression(string methodName, SyntaxNode expression) + { + if (expression.Parent != null && + expression.Parent.Kind() == SyntaxKind.EqualsValueClause && + expression.Parent.Parent != null && + expression.Parent.Parent.Kind() == SyntaxKind.VariableDeclarator) + { + var name = ((VariableDeclaratorSyntax)expression.Parent.Parent).Identifier.ValueText; + return (name != null && name.Length > 0) ? MakeMethodName("Get", name, methodName.Equals(NewMethodCamelCaseStr)) : methodName; } - private static string GetMethodNameBasedOnExpression(string methodName, SyntaxNode expression) + if (expression is MemberAccessExpressionSyntax memberAccess) { - if (expression.Parent != null && - expression.Parent.Kind() == SyntaxKind.EqualsValueClause && - expression.Parent.Parent != null && - expression.Parent.Parent.Kind() == SyntaxKind.VariableDeclarator) - { - var name = ((VariableDeclaratorSyntax)expression.Parent.Parent).Identifier.ValueText; - return (name != null && name.Length > 0) ? MakeMethodName("Get", name, methodName.Equals(NewMethodCamelCaseStr)) : methodName; - } + expression = memberAccess.Name; + } - if (expression is MemberAccessExpressionSyntax memberAccess) - { - expression = memberAccess.Name; - } + if (expression is NameSyntax) + { + SimpleNameSyntax unqualifiedName; - if (expression is NameSyntax) + switch (expression.Kind()) { - SimpleNameSyntax unqualifiedName; - - switch (expression.Kind()) - { - case SyntaxKind.IdentifierName: - case SyntaxKind.GenericName: - unqualifiedName = (SimpleNameSyntax)expression; - break; - case SyntaxKind.QualifiedName: - unqualifiedName = ((QualifiedNameSyntax)expression).Right; - break; - case SyntaxKind.AliasQualifiedName: - unqualifiedName = ((AliasQualifiedNameSyntax)expression).Name; - break; - default: - throw new System.NotSupportedException("Unexpected name kind: " + expression.Kind().ToString()); - } - - var unqualifiedNameIdentifierValueText = unqualifiedName.Identifier.ValueText; - return (unqualifiedNameIdentifierValueText != null && unqualifiedNameIdentifierValueText.Length > 0) ? - MakeMethodName("Get", unqualifiedNameIdentifierValueText, methodName.Equals(NewMethodCamelCaseStr)) : methodName; + case SyntaxKind.IdentifierName: + case SyntaxKind.GenericName: + unqualifiedName = (SimpleNameSyntax)expression; + break; + case SyntaxKind.QualifiedName: + unqualifiedName = ((QualifiedNameSyntax)expression).Right; + break; + case SyntaxKind.AliasQualifiedName: + unqualifiedName = ((AliasQualifiedNameSyntax)expression).Name; + break; + default: + throw new System.NotSupportedException("Unexpected name kind: " + expression.Kind().ToString()); } - return methodName; + var unqualifiedNameIdentifierValueText = unqualifiedName.Identifier.ValueText; + return (unqualifiedNameIdentifierValueText != null && unqualifiedNameIdentifierValueText.Length > 0) ? + MakeMethodName("Get", unqualifiedNameIdentifierValueText, methodName.Equals(NewMethodCamelCaseStr)) : methodName; } - protected override ImmutableArray GetInitialStatementsForMethodDefinitions() - { - Contract.ThrowIfFalse(this.SelectionResult.SelectionInExpression); + return methodName; + } - // special case for array initializer - var returnType = AnalyzerResult.ReturnType; - var containingScope = this.SelectionResult.GetContainingScope(); + protected override ImmutableArray GetInitialStatementsForMethodDefinitions() + { + Contract.ThrowIfFalse(this.SelectionResult.SelectionInExpression); - ExpressionSyntax expression; - if (returnType.TypeKind == TypeKind.Array && containingScope is InitializerExpressionSyntax) - { - var typeSyntax = returnType.GenerateTypeSyntax(); + // special case for array initializer + var returnType = AnalyzerResult.ReturnType; + var containingScope = this.SelectionResult.GetContainingScope(); - expression = SyntaxFactory.ArrayCreationExpression(typeSyntax as ArrayTypeSyntax, containingScope as InitializerExpressionSyntax); - } - else - { - expression = containingScope as ExpressionSyntax; - } + ExpressionSyntax expression; + if (returnType.TypeKind == TypeKind.Array && containingScope is InitializerExpressionSyntax) + { + var typeSyntax = returnType.GenerateTypeSyntax(); - if (AnalyzerResult.HasReturnType) - { - return [SyntaxFactory.ReturnStatement( - WrapInCheckedExpressionIfNeeded(expression))]; - } - else - { - return [SyntaxFactory.ExpressionStatement( - WrapInCheckedExpressionIfNeeded(expression))]; - } + expression = SyntaxFactory.ArrayCreationExpression(typeSyntax as ArrayTypeSyntax, containingScope as InitializerExpressionSyntax); } - - private ExpressionSyntax WrapInCheckedExpressionIfNeeded(ExpressionSyntax expression) + else { - var kind = this.SelectionResult.UnderCheckedExpressionContext(); - if (kind == SyntaxKind.None) - { - return expression; - } + expression = containingScope as ExpressionSyntax; + } - return SyntaxFactory.CheckedExpression(kind, expression); + if (AnalyzerResult.HasReturnType) + { + return [SyntaxFactory.ReturnStatement( + WrapInCheckedExpressionIfNeeded(expression))]; + } + else + { + return [SyntaxFactory.ExpressionStatement( + WrapInCheckedExpressionIfNeeded(expression))]; } + } - protected override SyntaxNode GetFirstStatementOrInitializerSelectedAtCallSite() + private ExpressionSyntax WrapInCheckedExpressionIfNeeded(ExpressionSyntax expression) + { + var kind = this.SelectionResult.UnderCheckedExpressionContext(); + if (kind == SyntaxKind.None) { - var scope = (SyntaxNode)this.SelectionResult.GetContainingScopeOf(); - scope ??= this.SelectionResult.GetContainingScopeOf(); + return expression; + } - scope ??= this.SelectionResult.GetContainingScopeOf(); + return SyntaxFactory.CheckedExpression(kind, expression); + } - // This is similar to FieldDeclaration case but we only want to do this - // if the member has an expression body. - scope ??= this.SelectionResult.GetContainingScopeOf().Parent; + protected override SyntaxNode GetFirstStatementOrInitializerSelectedAtCallSite() + { + var scope = (SyntaxNode)this.SelectionResult.GetContainingScopeOf(); + scope ??= this.SelectionResult.GetContainingScopeOf(); - return scope; - } + scope ??= this.SelectionResult.GetContainingScopeOf(); - protected override SyntaxNode GetLastStatementOrInitializerSelectedAtCallSite() - => GetFirstStatementOrInitializerSelectedAtCallSite(); + // This is similar to FieldDeclaration case but we only want to do this + // if the member has an expression body. + scope ??= this.SelectionResult.GetContainingScopeOf().Parent; - protected override async Task GetStatementOrInitializerContainingInvocationToExtractedMethodAsync(CancellationToken cancellationToken) - { - var enclosingStatement = GetFirstStatementOrInitializerSelectedAtCallSite(); + return scope; + } - var callSignature = CreateCallSignature().WithAdditionalAnnotations(CallSiteAnnotation); + protected override SyntaxNode GetLastStatementOrInitializerSelectedAtCallSite() + => GetFirstStatementOrInitializerSelectedAtCallSite(); - var sourceNode = this.SelectionResult.GetContainingScope(); - Contract.ThrowIfTrue( - sourceNode.Parent is MemberAccessExpressionSyntax memberAccessExpression && memberAccessExpression.Name == sourceNode, - "invalid scope. given scope is not an expression"); + protected override async Task GetStatementOrInitializerContainingInvocationToExtractedMethodAsync(CancellationToken cancellationToken) + { + var enclosingStatement = GetFirstStatementOrInitializerSelectedAtCallSite(); - // To lower the chances that replacing sourceNode with callSignature will break the user's - // code, we make the enclosing statement semantically explicit. This ends up being a little - // bit more work because we need to annotate the sourceNode so that we can get back to it - // after rewriting the enclosing statement. - var updatedDocument = SemanticDocument.Document; - var sourceNodeAnnotation = new SyntaxAnnotation(); - var enclosingStatementAnnotation = new SyntaxAnnotation(); - var newEnclosingStatement = enclosingStatement - .ReplaceNode(sourceNode, sourceNode.WithAdditionalAnnotations(sourceNodeAnnotation)) - .WithAdditionalAnnotations(enclosingStatementAnnotation); + var callSignature = CreateCallSignature().WithAdditionalAnnotations(CallSiteAnnotation); - updatedDocument = await updatedDocument.ReplaceNodeAsync(enclosingStatement, newEnclosingStatement, cancellationToken).ConfigureAwait(false); + var sourceNode = this.SelectionResult.GetContainingScope(); + Contract.ThrowIfTrue( + sourceNode.Parent is MemberAccessExpressionSyntax memberAccessExpression && memberAccessExpression.Name == sourceNode, + "invalid scope. given scope is not an expression"); - var updatedRoot = await updatedDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - newEnclosingStatement = updatedRoot.GetAnnotatedNodesAndTokens(enclosingStatementAnnotation).Single().AsNode(); + // To lower the chances that replacing sourceNode with callSignature will break the user's + // code, we make the enclosing statement semantically explicit. This ends up being a little + // bit more work because we need to annotate the sourceNode so that we can get back to it + // after rewriting the enclosing statement. + var updatedDocument = SemanticDocument.Document; + var sourceNodeAnnotation = new SyntaxAnnotation(); + var enclosingStatementAnnotation = new SyntaxAnnotation(); + var newEnclosingStatement = enclosingStatement + .ReplaceNode(sourceNode, sourceNode.WithAdditionalAnnotations(sourceNodeAnnotation)) + .WithAdditionalAnnotations(enclosingStatementAnnotation); - // because of the complexification we cannot guarantee that there is only one annotation. - // however complexification of names is prepended, so the last annotation should be the original one. - sourceNode = updatedRoot.GetAnnotatedNodesAndTokens(sourceNodeAnnotation).Last().AsNode(); + updatedDocument = await updatedDocument.ReplaceNodeAsync(enclosingStatement, newEnclosingStatement, cancellationToken).ConfigureAwait(false); - return newEnclosingStatement.ReplaceNode(sourceNode, callSignature); - } + var updatedRoot = await updatedDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + newEnclosingStatement = updatedRoot.GetAnnotatedNodesAndTokens(enclosingStatementAnnotation).Single().AsNode(); + + // because of the complexification we cannot guarantee that there is only one annotation. + // however complexification of names is prepended, so the last annotation should be the original one. + sourceNode = updatedRoot.GetAnnotatedNodesAndTokens(sourceNodeAnnotation).Last().AsNode(); + + return newEnclosingStatement.ReplaceNode(sourceNode, callSignature); } } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.MultipleStatementsCodeGenerator.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.MultipleStatementsCodeGenerator.cs index ac9332953484c..508f03bd15ebe 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.MultipleStatementsCodeGenerator.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.MultipleStatementsCodeGenerator.cs @@ -15,79 +15,78 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal partial class CSharpMethodExtractor { - internal partial class CSharpMethodExtractor + private partial class CSharpCodeGenerator { - private partial class CSharpCodeGenerator + public sealed class MultipleStatementsCodeGenerator( + CSharpSelectionResult selectionResult, + AnalyzerResult analyzerResult, + CSharpCodeGenerationOptions options, + bool localFunction) : CSharpCodeGenerator(selectionResult, analyzerResult, options, localFunction) { - public sealed class MultipleStatementsCodeGenerator( - CSharpSelectionResult selectionResult, - AnalyzerResult analyzerResult, - CSharpCodeGenerationOptions options, - bool localFunction) : CSharpCodeGenerator(selectionResult, analyzerResult, options, localFunction) + protected override SyntaxToken CreateMethodName() + => GenerateMethodNameForStatementGenerators(); + + protected override ImmutableArray GetInitialStatementsForMethodDefinitions() { - protected override SyntaxToken CreateMethodName() - => GenerateMethodNameForStatementGenerators(); + var firstSeen = false; + var firstStatementUnderContainer = this.SelectionResult.GetFirstStatementUnderContainer(); + var lastStatementUnderContainer = this.SelectionResult.GetLastStatementUnderContainer(); - protected override ImmutableArray GetInitialStatementsForMethodDefinitions() + using var _ = ArrayBuilder.GetInstance(out var list); + foreach (var statement in GetStatementsFromContainer(firstStatementUnderContainer.Parent)) { - var firstSeen = false; - var firstStatementUnderContainer = this.SelectionResult.GetFirstStatementUnderContainer(); - var lastStatementUnderContainer = this.SelectionResult.GetLastStatementUnderContainer(); - - using var _ = ArrayBuilder.GetInstance(out var list); - foreach (var statement in GetStatementsFromContainer(firstStatementUnderContainer.Parent)) + // reset first seen + if (!firstSeen) { - // reset first seen - if (!firstSeen) - { - firstSeen = statement == firstStatementUnderContainer; - } - - // continue until we see the first statement - if (!firstSeen) - { - continue; - } - - list.Add(statement); - - // exit if we see last statement - if (statement == lastStatementUnderContainer) - { - break; - } + firstSeen = statement == firstStatementUnderContainer; } - return list.ToImmutable(); - } + // continue until we see the first statement + if (!firstSeen) + { + continue; + } - private static IEnumerable GetStatementsFromContainer(SyntaxNode node) - { - Contract.ThrowIfNull(node); - Contract.ThrowIfFalse(node.IsStatementContainerNode()); + list.Add(statement); - return node switch + // exit if we see last statement + if (statement == lastStatementUnderContainer) { - BlockSyntax blockNode => blockNode.Statements, - SwitchSectionSyntax switchSectionNode => switchSectionNode.Statements, - GlobalStatementSyntax globalStatement => ((CompilationUnitSyntax)globalStatement.Parent).Members.OfType().Select(globalStatement => globalStatement.Statement), - _ => throw ExceptionUtilities.UnexpectedValue(node), - }; + break; + } } - protected override SyntaxNode GetFirstStatementOrInitializerSelectedAtCallSite() - => this.SelectionResult.GetFirstStatementUnderContainer(); + return list.ToImmutable(); + } - protected override SyntaxNode GetLastStatementOrInitializerSelectedAtCallSite() - => this.SelectionResult.GetLastStatementUnderContainer(); + private static IEnumerable GetStatementsFromContainer(SyntaxNode node) + { + Contract.ThrowIfNull(node); + Contract.ThrowIfFalse(node.IsStatementContainerNode()); - protected override Task GetStatementOrInitializerContainingInvocationToExtractedMethodAsync(CancellationToken cancellationToken) + return node switch { - var statement = GetStatementContainingInvocationToExtractedMethodWorker(); - return Task.FromResult(statement.WithAdditionalAnnotations(CallSiteAnnotation)); - } + BlockSyntax blockNode => blockNode.Statements, + SwitchSectionSyntax switchSectionNode => switchSectionNode.Statements, + GlobalStatementSyntax globalStatement => ((CompilationUnitSyntax)globalStatement.Parent).Members.OfType().Select(globalStatement => globalStatement.Statement), + _ => throw ExceptionUtilities.UnexpectedValue(node), + }; + } + + protected override SyntaxNode GetFirstStatementOrInitializerSelectedAtCallSite() + => this.SelectionResult.GetFirstStatementUnderContainer(); + + protected override SyntaxNode GetLastStatementOrInitializerSelectedAtCallSite() + => this.SelectionResult.GetLastStatementUnderContainer(); + + protected override Task GetStatementOrInitializerContainingInvocationToExtractedMethodAsync(CancellationToken cancellationToken) + { + var statement = GetStatementContainingInvocationToExtractedMethodWorker(); + return Task.FromResult(statement.WithAdditionalAnnotations(CallSiteAnnotation)); } } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.SingleStatementCodeGenerator.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.SingleStatementCodeGenerator.cs index 37d33960e7a88..4fd625c110cfc 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.SingleStatementCodeGenerator.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.SingleStatementCodeGenerator.cs @@ -12,42 +12,41 @@ using Microsoft.CodeAnalysis.ExtractMethod; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal partial class CSharpMethodExtractor { - internal partial class CSharpMethodExtractor + private partial class CSharpCodeGenerator { - private partial class CSharpCodeGenerator + public sealed class SingleStatementCodeGenerator( + CSharpSelectionResult selectionResult, + AnalyzerResult analyzerResult, + CSharpCodeGenerationOptions options, + bool localFunction) : CSharpCodeGenerator(selectionResult, analyzerResult, options, localFunction) { - public sealed class SingleStatementCodeGenerator( - CSharpSelectionResult selectionResult, - AnalyzerResult analyzerResult, - CSharpCodeGenerationOptions options, - bool localFunction) : CSharpCodeGenerator(selectionResult, analyzerResult, options, localFunction) + protected override SyntaxToken CreateMethodName() => GenerateMethodNameForStatementGenerators(); + + protected override ImmutableArray GetInitialStatementsForMethodDefinitions() + { + Contract.ThrowIfFalse(this.SelectionResult.IsExtractMethodOnSingleStatement()); + + return [this.SelectionResult.GetFirstStatement()]; + } + + protected override SyntaxNode GetFirstStatementOrInitializerSelectedAtCallSite() + => this.SelectionResult.GetFirstStatement(); + + protected override SyntaxNode GetLastStatementOrInitializerSelectedAtCallSite() + { + // it is a single statement case. either first statement is same as last statement or + // last statement belongs (embedded statement) to the first statement. + return this.SelectionResult.GetFirstStatement(); + } + + protected override Task GetStatementOrInitializerContainingInvocationToExtractedMethodAsync(CancellationToken cancellationToken) { - protected override SyntaxToken CreateMethodName() => GenerateMethodNameForStatementGenerators(); - - protected override ImmutableArray GetInitialStatementsForMethodDefinitions() - { - Contract.ThrowIfFalse(this.SelectionResult.IsExtractMethodOnSingleStatement()); - - return [this.SelectionResult.GetFirstStatement()]; - } - - protected override SyntaxNode GetFirstStatementOrInitializerSelectedAtCallSite() - => this.SelectionResult.GetFirstStatement(); - - protected override SyntaxNode GetLastStatementOrInitializerSelectedAtCallSite() - { - // it is a single statement case. either first statement is same as last statement or - // last statement belongs (embedded statement) to the first statement. - return this.SelectionResult.GetFirstStatement(); - } - - protected override Task GetStatementOrInitializerContainingInvocationToExtractedMethodAsync(CancellationToken cancellationToken) - { - var statement = GetStatementContainingInvocationToExtractedMethodWorker(); - return Task.FromResult(statement.WithAdditionalAnnotations(CallSiteAnnotation)); - } + var statement = GetStatementContainingInvocationToExtractedMethodWorker(); + return Task.FromResult(statement.WithAdditionalAnnotations(CallSiteAnnotation)); } } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.cs index 0adef91010031..eeee5ca192274 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.cs @@ -27,800 +27,799 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod -{ - using static SyntaxFactory; +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +using static SyntaxFactory; - internal partial class CSharpMethodExtractor +internal partial class CSharpMethodExtractor +{ + private abstract partial class CSharpCodeGenerator : CodeGenerator { - private abstract partial class CSharpCodeGenerator : CodeGenerator + private readonly SyntaxToken _methodName; + + private const string NewMethodPascalCaseStr = "NewMethod"; + private const string NewMethodCamelCaseStr = "newMethod"; + + public static Task GenerateAsync( + InsertionPoint insertionPoint, + CSharpSelectionResult selectionResult, + AnalyzerResult analyzerResult, + CSharpCodeGenerationOptions options, + bool localFunction, + CancellationToken cancellationToken) { - private readonly SyntaxToken _methodName; + var codeGenerator = Create(selectionResult, analyzerResult, options, localFunction); + return codeGenerator.GenerateAsync(insertionPoint, cancellationToken); + } - private const string NewMethodPascalCaseStr = "NewMethod"; - private const string NewMethodCamelCaseStr = "newMethod"; + public static CSharpCodeGenerator Create( + CSharpSelectionResult selectionResult, + AnalyzerResult analyzerResult, + CSharpCodeGenerationOptions options, + bool localFunction) + { + if (selectionResult.SelectionInExpression) + return new ExpressionCodeGenerator(selectionResult, analyzerResult, options, localFunction); - public static Task GenerateAsync( - InsertionPoint insertionPoint, - CSharpSelectionResult selectionResult, - AnalyzerResult analyzerResult, - CSharpCodeGenerationOptions options, - bool localFunction, - CancellationToken cancellationToken) - { - var codeGenerator = Create(selectionResult, analyzerResult, options, localFunction); - return codeGenerator.GenerateAsync(insertionPoint, cancellationToken); - } + if (selectionResult.IsExtractMethodOnSingleStatement()) + return new SingleStatementCodeGenerator(selectionResult, analyzerResult, options, localFunction); - public static CSharpCodeGenerator Create( - CSharpSelectionResult selectionResult, - AnalyzerResult analyzerResult, - CSharpCodeGenerationOptions options, - bool localFunction) - { - if (selectionResult.SelectionInExpression) - return new ExpressionCodeGenerator(selectionResult, analyzerResult, options, localFunction); + if (selectionResult.IsExtractMethodOnMultipleStatements()) + return new MultipleStatementsCodeGenerator(selectionResult, analyzerResult, options, localFunction); - if (selectionResult.IsExtractMethodOnSingleStatement()) - return new SingleStatementCodeGenerator(selectionResult, analyzerResult, options, localFunction); + throw ExceptionUtilities.UnexpectedValue(selectionResult); + } - if (selectionResult.IsExtractMethodOnMultipleStatements()) - return new MultipleStatementsCodeGenerator(selectionResult, analyzerResult, options, localFunction); + protected CSharpCodeGenerator( + CSharpSelectionResult selectionResult, + AnalyzerResult analyzerResult, + CSharpCodeGenerationOptions options, + bool localFunction) + : base(selectionResult, analyzerResult, options, localFunction) + { + Contract.ThrowIfFalse(SemanticDocument == selectionResult.SemanticDocument); - throw ExceptionUtilities.UnexpectedValue(selectionResult); - } + var nameToken = CreateMethodName(); + _methodName = nameToken.WithAdditionalAnnotations(MethodNameAnnotation); + } - protected CSharpCodeGenerator( - CSharpSelectionResult selectionResult, - AnalyzerResult analyzerResult, - CSharpCodeGenerationOptions options, - bool localFunction) - : base(selectionResult, analyzerResult, options, localFunction) - { - Contract.ThrowIfFalse(SemanticDocument == selectionResult.SemanticDocument); + public override OperationStatus> GetNewMethodStatements(SyntaxNode insertionPointNode, CancellationToken cancellationToken) + { + var statements = CreateMethodBody(insertionPointNode, cancellationToken); + var status = CheckActiveStatements(statements); + return status.With(statements.CastArray()); + } - var nameToken = CreateMethodName(); - _methodName = nameToken.WithAdditionalAnnotations(MethodNameAnnotation); - } + protected override IMethodSymbol GenerateMethodDefinition( + SyntaxNode insertionPointNode, CancellationToken cancellationToken) + { + var statements = CreateMethodBody(insertionPointNode, cancellationToken); + statements = WrapInCheckStatementIfNeeded(statements); + + var methodSymbol = CodeGenerationSymbolFactory.CreateMethodSymbol( + attributes: [], + accessibility: Accessibility.Private, + modifiers: CreateMethodModifiers(), + returnType: AnalyzerResult.ReturnType, + refKind: AnalyzerResult.ReturnsByRef ? RefKind.Ref : RefKind.None, + explicitInterfaceImplementations: default, + name: _methodName.ToString(), + typeParameters: CreateMethodTypeParameters(), + parameters: CreateMethodParameters(), + statements: statements.CastArray(), + methodKind: this.LocalFunction ? MethodKind.LocalFunction : MethodKind.Ordinary); + + return MethodDefinitionAnnotation.AddAnnotationToSymbol( + Formatter.Annotation.AddAnnotationToSymbol(methodSymbol)); + } - public override OperationStatus> GetNewMethodStatements(SyntaxNode insertionPointNode, CancellationToken cancellationToken) - { - var statements = CreateMethodBody(insertionPointNode, cancellationToken); - var status = CheckActiveStatements(statements); - return status.With(statements.CastArray()); - } + protected override async Task GenerateBodyForCallSiteContainerAsync( + SyntaxNode insertionPointNode, + SyntaxNode container, + CancellationToken cancellationToken) + { + var variableMapToRemove = CreateVariableDeclarationToRemoveMap( + AnalyzerResult.GetVariablesToMoveIntoMethodDefinition(cancellationToken), cancellationToken); + var firstStatementToRemove = GetFirstStatementOrInitializerSelectedAtCallSite(); + var lastStatementToRemove = GetLastStatementOrInitializerSelectedAtCallSite(); - protected override IMethodSymbol GenerateMethodDefinition( - SyntaxNode insertionPointNode, CancellationToken cancellationToken) - { - var statements = CreateMethodBody(insertionPointNode, cancellationToken); - statements = WrapInCheckStatementIfNeeded(statements); - - var methodSymbol = CodeGenerationSymbolFactory.CreateMethodSymbol( - attributes: [], - accessibility: Accessibility.Private, - modifiers: CreateMethodModifiers(), - returnType: AnalyzerResult.ReturnType, - refKind: AnalyzerResult.ReturnsByRef ? RefKind.Ref : RefKind.None, - explicitInterfaceImplementations: default, - name: _methodName.ToString(), - typeParameters: CreateMethodTypeParameters(), - parameters: CreateMethodParameters(), - statements: statements.CastArray(), - methodKind: this.LocalFunction ? MethodKind.LocalFunction : MethodKind.Ordinary); - - return MethodDefinitionAnnotation.AddAnnotationToSymbol( - Formatter.Annotation.AddAnnotationToSymbol(methodSymbol)); - } + Contract.ThrowIfFalse(firstStatementToRemove.Parent == lastStatementToRemove.Parent + || CSharpSyntaxFacts.Instance.AreStatementsInSameContainer(firstStatementToRemove, lastStatementToRemove)); - protected override async Task GenerateBodyForCallSiteContainerAsync( - SyntaxNode insertionPointNode, - SyntaxNode container, - CancellationToken cancellationToken) - { - var variableMapToRemove = CreateVariableDeclarationToRemoveMap( - AnalyzerResult.GetVariablesToMoveIntoMethodDefinition(cancellationToken), cancellationToken); - var firstStatementToRemove = GetFirstStatementOrInitializerSelectedAtCallSite(); - var lastStatementToRemove = GetLastStatementOrInitializerSelectedAtCallSite(); + var statementsToInsert = await CreateStatementsOrInitializerToInsertAtCallSiteAsync( + insertionPointNode, cancellationToken).ConfigureAwait(false); - Contract.ThrowIfFalse(firstStatementToRemove.Parent == lastStatementToRemove.Parent - || CSharpSyntaxFacts.Instance.AreStatementsInSameContainer(firstStatementToRemove, lastStatementToRemove)); + var callSiteGenerator = new CallSiteContainerRewriter( + container, + variableMapToRemove, + firstStatementToRemove, + lastStatementToRemove, + statementsToInsert); - var statementsToInsert = await CreateStatementsOrInitializerToInsertAtCallSiteAsync( - insertionPointNode, cancellationToken).ConfigureAwait(false); + return container.CopyAnnotationsTo(callSiteGenerator.Generate()).WithAdditionalAnnotations(Formatter.Annotation); + } - var callSiteGenerator = new CallSiteContainerRewriter( - container, - variableMapToRemove, - firstStatementToRemove, - lastStatementToRemove, - statementsToInsert); + private async Task> CreateStatementsOrInitializerToInsertAtCallSiteAsync( + SyntaxNode insertionPointNode, CancellationToken cancellationToken) + { + var selectedNode = GetFirstStatementOrInitializerSelectedAtCallSite(); - return container.CopyAnnotationsTo(callSiteGenerator.Generate()).WithAdditionalAnnotations(Formatter.Annotation); + // field initializer, constructor initializer, expression bodied member case + if (selectedNode is ConstructorInitializerSyntax or FieldDeclarationSyntax || + IsExpressionBodiedMember(selectedNode) || + IsExpressionBodiedAccessor(selectedNode)) + { + var statement = await GetStatementOrInitializerContainingInvocationToExtractedMethodAsync(cancellationToken).ConfigureAwait(false); + return [statement]; } - private async Task> CreateStatementsOrInitializerToInsertAtCallSiteAsync( - SyntaxNode insertionPointNode, CancellationToken cancellationToken) - { - var selectedNode = GetFirstStatementOrInitializerSelectedAtCallSite(); + // regular case + var semanticModel = SemanticDocument.SemanticModel; + var postProcessor = new PostProcessor(semanticModel, insertionPointNode.SpanStart); - // field initializer, constructor initializer, expression bodied member case - if (selectedNode is ConstructorInitializerSyntax or FieldDeclarationSyntax || - IsExpressionBodiedMember(selectedNode) || - IsExpressionBodiedAccessor(selectedNode)) - { - var statement = await GetStatementOrInitializerContainingInvocationToExtractedMethodAsync(cancellationToken).ConfigureAwait(false); - return [statement]; - } + var statements = AddSplitOrMoveDeclarationOutStatementsToCallSite(cancellationToken); + statements = postProcessor.MergeDeclarationStatements(statements); + statements = AddAssignmentStatementToCallSite(statements, cancellationToken); + statements = await AddInvocationAtCallSiteAsync(statements, cancellationToken).ConfigureAwait(false); + statements = AddReturnIfUnreachable(statements); + + return statements.CastArray(); + } - // regular case - var semanticModel = SemanticDocument.SemanticModel; - var postProcessor = new PostProcessor(semanticModel, insertionPointNode.SpanStart); + protected override bool ShouldLocalFunctionCaptureParameter(SyntaxNode node) + => node.SyntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp8; - var statements = AddSplitOrMoveDeclarationOutStatementsToCallSite(cancellationToken); - statements = postProcessor.MergeDeclarationStatements(statements); - statements = AddAssignmentStatementToCallSite(statements, cancellationToken); - statements = await AddInvocationAtCallSiteAsync(statements, cancellationToken).ConfigureAwait(false); - statements = AddReturnIfUnreachable(statements); + private static bool IsExpressionBodiedMember(SyntaxNode node) + => node is MemberDeclarationSyntax member && member.GetExpressionBody() != null; - return statements.CastArray(); - } + private static bool IsExpressionBodiedAccessor(SyntaxNode node) + => node is AccessorDeclarationSyntax accessor && accessor.ExpressionBody != null; - protected override bool ShouldLocalFunctionCaptureParameter(SyntaxNode node) - => node.SyntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp8; + private SimpleNameSyntax CreateMethodNameForInvocation() + { + return AnalyzerResult.MethodTypeParametersInDeclaration.Count == 0 + ? SyntaxFactory.IdentifierName(_methodName) + : SyntaxFactory.GenericName(_methodName, SyntaxFactory.TypeArgumentList(CreateMethodCallTypeVariables())); + } - private static bool IsExpressionBodiedMember(SyntaxNode node) - => node is MemberDeclarationSyntax member && member.GetExpressionBody() != null; + private SeparatedSyntaxList CreateMethodCallTypeVariables() + { + Contract.ThrowIfTrue(AnalyzerResult.MethodTypeParametersInDeclaration.Count == 0); - private static bool IsExpressionBodiedAccessor(SyntaxNode node) - => node is AccessorDeclarationSyntax accessor && accessor.ExpressionBody != null; + // propagate any type variable used in extracted code + return [.. AnalyzerResult.MethodTypeParametersInDeclaration.Select(m => SyntaxFactory.ParseTypeName(m.Name))]; + } - private SimpleNameSyntax CreateMethodNameForInvocation() - { - return AnalyzerResult.MethodTypeParametersInDeclaration.Count == 0 - ? SyntaxFactory.IdentifierName(_methodName) - : SyntaxFactory.GenericName(_methodName, SyntaxFactory.TypeArgumentList(CreateMethodCallTypeVariables())); - } + protected override SyntaxNode GetCallSiteContainerFromOutermostMoveInVariable(CancellationToken cancellationToken) + { + var outmostVariable = GetOutermostVariableToMoveIntoMethodDefinition(cancellationToken); + if (outmostVariable == null) + return null; - private SeparatedSyntaxList CreateMethodCallTypeVariables() - { - Contract.ThrowIfTrue(AnalyzerResult.MethodTypeParametersInDeclaration.Count == 0); + var idToken = outmostVariable.GetIdentifierTokenAtDeclaration(SemanticDocument); + var declStatement = idToken.GetAncestor(); + Contract.ThrowIfNull(declStatement); + Contract.ThrowIfFalse(declStatement.Parent.IsStatementContainerNode()); - // propagate any type variable used in extracted code - return [.. AnalyzerResult.MethodTypeParametersInDeclaration.Select(m => SyntaxFactory.ParseTypeName(m.Name))]; - } + return declStatement.Parent; + } - protected override SyntaxNode GetCallSiteContainerFromOutermostMoveInVariable(CancellationToken cancellationToken) - { - var outmostVariable = GetOutermostVariableToMoveIntoMethodDefinition(cancellationToken); - if (outmostVariable == null) - return null; + private DeclarationModifiers CreateMethodModifiers() + { + var isUnsafe = this.SelectionResult.ShouldPutUnsafeModifier(); + var isAsync = this.SelectionResult.ShouldPutAsyncModifier(); + var isStatic = !AnalyzerResult.UseInstanceMember; + var isReadOnly = AnalyzerResult.ShouldBeReadOnly; - var idToken = outmostVariable.GetIdentifierTokenAtDeclaration(SemanticDocument); - var declStatement = idToken.GetAncestor(); - Contract.ThrowIfNull(declStatement); - Contract.ThrowIfFalse(declStatement.Parent.IsStatementContainerNode()); + // Static local functions are only supported in C# 8.0 and later + var languageVersion = SemanticDocument.SyntaxTree.Options.LanguageVersion(); - return declStatement.Parent; + if (LocalFunction && (!Options.PreferStaticLocalFunction.Value || languageVersion < LanguageVersion.CSharp8)) + { + isStatic = false; } - private DeclarationModifiers CreateMethodModifiers() + // UseInstanceMember will be false for interface members, but extracting a non-static + // member to a static member has a very different meaning for interfaces so we need + // an extra check here. + if (!LocalFunction && IsNonStaticInterfaceMember()) { - var isUnsafe = this.SelectionResult.ShouldPutUnsafeModifier(); - var isAsync = this.SelectionResult.ShouldPutAsyncModifier(); - var isStatic = !AnalyzerResult.UseInstanceMember; - var isReadOnly = AnalyzerResult.ShouldBeReadOnly; - - // Static local functions are only supported in C# 8.0 and later - var languageVersion = SemanticDocument.SyntaxTree.Options.LanguageVersion(); + isStatic = false; + } - if (LocalFunction && (!Options.PreferStaticLocalFunction.Value || languageVersion < LanguageVersion.CSharp8)) - { - isStatic = false; - } + return new DeclarationModifiers( + isUnsafe: isUnsafe, + isAsync: isAsync, + isStatic: isStatic, + isReadOnly: isReadOnly); + } - // UseInstanceMember will be false for interface members, but extracting a non-static - // member to a static member has a very different meaning for interfaces so we need - // an extra check here. - if (!LocalFunction && IsNonStaticInterfaceMember()) - { - isStatic = false; - } + private bool IsNonStaticInterfaceMember() + { + var typeDecl = SelectionResult.GetContainingScopeOf(); + if (typeDecl is null) + return false; - return new DeclarationModifiers( - isUnsafe: isUnsafe, - isAsync: isAsync, - isStatic: isStatic, - isReadOnly: isReadOnly); - } + if (!typeDecl.IsKind(SyntaxKind.InterfaceDeclaration)) + return false; - private bool IsNonStaticInterfaceMember() - { - var typeDecl = SelectionResult.GetContainingScopeOf(); - if (typeDecl is null) - return false; + var memberDecl = SelectionResult.GetContainingScopeOf(); + if (memberDecl is null) + return false; - if (!typeDecl.IsKind(SyntaxKind.InterfaceDeclaration)) - return false; + return !memberDecl.Modifiers.Any(SyntaxKind.StaticKeyword); + } - var memberDecl = SelectionResult.GetContainingScopeOf(); - if (memberDecl is null) - return false; + private static SyntaxKind GetParameterRefSyntaxKind(ParameterBehavior parameterBehavior) + { + return parameterBehavior == ParameterBehavior.Ref + ? SyntaxKind.RefKeyword + : parameterBehavior == ParameterBehavior.Out ? + SyntaxKind.OutKeyword : SyntaxKind.None; + } - return !memberDecl.Modifiers.Any(SyntaxKind.StaticKeyword); - } + private ImmutableArray CreateMethodBody( + SyntaxNode insertionPoint, CancellationToken cancellationToken) + { + var statements = GetInitialStatementsForMethodDefinitions(); - private static SyntaxKind GetParameterRefSyntaxKind(ParameterBehavior parameterBehavior) - { - return parameterBehavior == ParameterBehavior.Ref - ? SyntaxKind.RefKeyword - : parameterBehavior == ParameterBehavior.Out ? - SyntaxKind.OutKeyword : SyntaxKind.None; - } + statements = SplitOrMoveDeclarationIntoMethodDefinition(insertionPoint, statements, cancellationToken); + statements = MoveDeclarationOutFromMethodDefinition(statements, cancellationToken); + statements = AppendReturnStatementIfNeeded(statements); + statements = CleanupCode(statements); - private ImmutableArray CreateMethodBody( - SyntaxNode insertionPoint, CancellationToken cancellationToken) - { - var statements = GetInitialStatementsForMethodDefinitions(); - - statements = SplitOrMoveDeclarationIntoMethodDefinition(insertionPoint, statements, cancellationToken); - statements = MoveDeclarationOutFromMethodDefinition(statements, cancellationToken); - statements = AppendReturnStatementIfNeeded(statements); - statements = CleanupCode(statements); + return statements; + } + private ImmutableArray WrapInCheckStatementIfNeeded(ImmutableArray statements) + { + var kind = this.SelectionResult.UnderCheckedStatementContext(); + if (kind == SyntaxKind.None) return statements; - } - private ImmutableArray WrapInCheckStatementIfNeeded(ImmutableArray statements) - { - var kind = this.SelectionResult.UnderCheckedStatementContext(); - if (kind == SyntaxKind.None) - return statements; + return statements is [BlockSyntax block] + ? [SyntaxFactory.CheckedStatement(kind, block)] + : [SyntaxFactory.CheckedStatement(kind, SyntaxFactory.Block(statements))]; + } - return statements is [BlockSyntax block] - ? [SyntaxFactory.CheckedStatement(kind, block)] - : [SyntaxFactory.CheckedStatement(kind, SyntaxFactory.Block(statements))]; - } + private static ImmutableArray CleanupCode(ImmutableArray statements) + { + statements = PostProcessor.RemoveRedundantBlock(statements); + statements = PostProcessor.RemoveDeclarationAssignmentPattern(statements); + statements = PostProcessor.RemoveInitializedDeclarationAndReturnPattern(statements); - private static ImmutableArray CleanupCode(ImmutableArray statements) - { - statements = PostProcessor.RemoveRedundantBlock(statements); - statements = PostProcessor.RemoveDeclarationAssignmentPattern(statements); - statements = PostProcessor.RemoveInitializedDeclarationAndReturnPattern(statements); + return statements; + } - return statements; - } + private static OperationStatus CheckActiveStatements(ImmutableArray statements) + { + if (statements.IsEmpty) + return OperationStatus.NoActiveStatement; - private static OperationStatus CheckActiveStatements(ImmutableArray statements) - { - if (statements.IsEmpty) - return OperationStatus.NoActiveStatement; + if (statements is [ReturnStatementSyntax { Expression: null }]) + return OperationStatus.NoActiveStatement; - if (statements is [ReturnStatementSyntax { Expression: null }]) - return OperationStatus.NoActiveStatement; + // Look for at least one non local-variable-decl statement, or at least one local variable with an initializer. + foreach (var statement in statements) + { + if (statement is not LocalDeclarationStatementSyntax declStatement) + return OperationStatus.SucceededStatus; - // Look for at least one non local-variable-decl statement, or at least one local variable with an initializer. - foreach (var statement in statements) + foreach (var variable in declStatement.Declaration.Variables) { - if (statement is not LocalDeclarationStatementSyntax declStatement) + if (variable.Initializer != null) return OperationStatus.SucceededStatus; - - foreach (var variable in declStatement.Declaration.Variables) - { - if (variable.Initializer != null) - return OperationStatus.SucceededStatus; - } } - - return OperationStatus.NoActiveStatement; } - private ImmutableArray MoveDeclarationOutFromMethodDefinition( - ImmutableArray statements, CancellationToken cancellationToken) - { - using var _ = ArrayBuilder.GetInstance(out var result); + return OperationStatus.NoActiveStatement; + } + + private ImmutableArray MoveDeclarationOutFromMethodDefinition( + ImmutableArray statements, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var result); - var variableToRemoveMap = CreateVariableDeclarationToRemoveMap( - AnalyzerResult.GetVariablesToMoveOutToCallSiteOrDelete(cancellationToken), cancellationToken); + var variableToRemoveMap = CreateVariableDeclarationToRemoveMap( + AnalyzerResult.GetVariablesToMoveOutToCallSiteOrDelete(cancellationToken), cancellationToken); - statements = statements.SelectAsArray(s => FixDeclarationExpressionsAndDeclarationPatterns(s, variableToRemoveMap)); + statements = statements.SelectAsArray(s => FixDeclarationExpressionsAndDeclarationPatterns(s, variableToRemoveMap)); - foreach (var statement in statements) + foreach (var statement in statements) + { + if (statement is not LocalDeclarationStatementSyntax declarationStatement || declarationStatement.Declaration.Variables.FullSpan.IsEmpty) { - if (statement is not LocalDeclarationStatementSyntax declarationStatement || declarationStatement.Declaration.Variables.FullSpan.IsEmpty) - { - // if given statement is not decl statement. - result.Add(statement); - continue; - } + // if given statement is not decl statement. + result.Add(statement); + continue; + } - var expressionStatements = new List(); - var list = new List(); - var triviaList = new List(); + var expressionStatements = new List(); + var list = new List(); + var triviaList = new List(); - // When we modify the declaration to an initialization we have to preserve the leading trivia - var firstVariableToAttachTrivia = true; + // When we modify the declaration to an initialization we have to preserve the leading trivia + var firstVariableToAttachTrivia = true; - // go through each var decls in decl statement, and create new assignment if - // variable is initialized at decl. - foreach (var variableDeclaration in declarationStatement.Declaration.Variables) + // go through each var decls in decl statement, and create new assignment if + // variable is initialized at decl. + foreach (var variableDeclaration in declarationStatement.Declaration.Variables) + { + if (variableToRemoveMap.HasSyntaxAnnotation(variableDeclaration)) { - if (variableToRemoveMap.HasSyntaxAnnotation(variableDeclaration)) + if (variableDeclaration.Initializer != null) { - if (variableDeclaration.Initializer != null) - { - var identifier = ApplyTriviaFromDeclarationToAssignmentIdentifier(declarationStatement, firstVariableToAttachTrivia, variableDeclaration); + var identifier = ApplyTriviaFromDeclarationToAssignmentIdentifier(declarationStatement, firstVariableToAttachTrivia, variableDeclaration); - // move comments with the variable here - expressionStatements.Add(CreateAssignmentExpressionStatement(identifier, variableDeclaration.Initializer.Value)); - } - else - { - // we don't remove trivia around tokens we remove - triviaList.AddRange(variableDeclaration.GetLeadingTrivia()); - triviaList.AddRange(variableDeclaration.GetTrailingTrivia()); - } - - firstVariableToAttachTrivia = false; - continue; + // move comments with the variable here + expressionStatements.Add(CreateAssignmentExpressionStatement(identifier, variableDeclaration.Initializer.Value)); } - - // Prepend the trivia from the declarations without initialization to the next persisting variable declaration - if (triviaList.Count > 0) + else { - list.Add(variableDeclaration.WithPrependedLeadingTrivia(triviaList)); - triviaList.Clear(); - firstVariableToAttachTrivia = false; - continue; + // we don't remove trivia around tokens we remove + triviaList.AddRange(variableDeclaration.GetLeadingTrivia()); + triviaList.AddRange(variableDeclaration.GetTrailingTrivia()); } firstVariableToAttachTrivia = false; - list.Add(variableDeclaration); + continue; } - if (list.Count == 0 && triviaList.Count > 0) + // Prepend the trivia from the declarations without initialization to the next persisting variable declaration + if (triviaList.Count > 0) { - // well, there are trivia associated with the node. - // we can't just delete the node since then, we will lose - // the trivia. unfortunately, it is not easy to attach the trivia - // to next token. for now, create an empty statement and associate the - // trivia to the statement - - // TODO : think about a way to trivia attached to next token - result.Add(EmptyStatement(Token([.. triviaList], SyntaxKind.SemicolonToken, [ElasticMarker]))); + list.Add(variableDeclaration.WithPrependedLeadingTrivia(triviaList)); triviaList.Clear(); + firstVariableToAttachTrivia = false; + continue; } - // return survived var decls - if (list.Count > 0) - { - result.Add(SyntaxFactory.LocalDeclarationStatement( - declarationStatement.Modifiers, - SyntaxFactory.VariableDeclaration( - declarationStatement.Declaration.Type, - [.. list]), - declarationStatement.SemicolonToken.WithPrependedLeadingTrivia(triviaList))); - triviaList.Clear(); - } + firstVariableToAttachTrivia = false; + list.Add(variableDeclaration); + } - // return any expression statement if there was any - result.AddRange(expressionStatements); + if (list.Count == 0 && triviaList.Count > 0) + { + // well, there are trivia associated with the node. + // we can't just delete the node since then, we will lose + // the trivia. unfortunately, it is not easy to attach the trivia + // to next token. for now, create an empty statement and associate the + // trivia to the statement + + // TODO : think about a way to trivia attached to next token + result.Add(EmptyStatement(Token([.. triviaList], SyntaxKind.SemicolonToken, [ElasticMarker]))); + triviaList.Clear(); + } + + // return survived var decls + if (list.Count > 0) + { + result.Add(SyntaxFactory.LocalDeclarationStatement( + declarationStatement.Modifiers, + SyntaxFactory.VariableDeclaration( + declarationStatement.Declaration.Type, + [.. list]), + declarationStatement.SemicolonToken.WithPrependedLeadingTrivia(triviaList))); + triviaList.Clear(); } - return result.ToImmutable(); + // return any expression statement if there was any + result.AddRange(expressionStatements); } - /// - /// If the statement has an out var declaration expression for a variable which - /// needs to be removed, we need to turn it into a plain out parameter, so that - /// it doesn't declare a duplicate variable. - /// If the statement has a pattern declaration (such as 3 is int i) for a variable - /// which needs to be removed, we will annotate it as a conflict, since we don't have - /// a better refactoring. - /// - private static StatementSyntax FixDeclarationExpressionsAndDeclarationPatterns(StatementSyntax statement, - HashSet variablesToRemove) - { - var replacements = new Dictionary(); + return result.ToImmutable(); + } + + /// + /// If the statement has an out var declaration expression for a variable which + /// needs to be removed, we need to turn it into a plain out parameter, so that + /// it doesn't declare a duplicate variable. + /// If the statement has a pattern declaration (such as 3 is int i) for a variable + /// which needs to be removed, we will annotate it as a conflict, since we don't have + /// a better refactoring. + /// + private static StatementSyntax FixDeclarationExpressionsAndDeclarationPatterns(StatementSyntax statement, + HashSet variablesToRemove) + { + var replacements = new Dictionary(); - var declarations = statement.DescendantNodes() - .Where(n => n.Kind() is SyntaxKind.DeclarationExpression or SyntaxKind.DeclarationPattern); + var declarations = statement.DescendantNodes() + .Where(n => n.Kind() is SyntaxKind.DeclarationExpression or SyntaxKind.DeclarationPattern); - foreach (var node in declarations) + foreach (var node in declarations) + { + switch (node.Kind()) { - switch (node.Kind()) - { - case SyntaxKind.DeclarationExpression: + case SyntaxKind.DeclarationExpression: + { + var declaration = (DeclarationExpressionSyntax)node; + if (declaration.Designation.Kind() != SyntaxKind.SingleVariableDesignation) { - var declaration = (DeclarationExpressionSyntax)node; - if (declaration.Designation.Kind() != SyntaxKind.SingleVariableDesignation) - { - break; - } - - var designation = (SingleVariableDesignationSyntax)declaration.Designation; - var name = designation.Identifier.ValueText; - if (variablesToRemove.HasSyntaxAnnotation(designation)) - { - var newLeadingTrivia = new SyntaxTriviaList(); - newLeadingTrivia = newLeadingTrivia.AddRange(declaration.Type.GetLeadingTrivia()); - newLeadingTrivia = newLeadingTrivia.AddRange(declaration.Type.GetTrailingTrivia()); - newLeadingTrivia = newLeadingTrivia.AddRange(designation.GetLeadingTrivia()); - - replacements.Add(declaration, SyntaxFactory.IdentifierName(designation.Identifier) - .WithLeadingTrivia(newLeadingTrivia)); - } - break; } - case SyntaxKind.DeclarationPattern: + var designation = (SingleVariableDesignationSyntax)declaration.Designation; + var name = designation.Identifier.ValueText; + if (variablesToRemove.HasSyntaxAnnotation(designation)) { - var pattern = (DeclarationPatternSyntax)node; - if (!variablesToRemove.HasSyntaxAnnotation(pattern)) - { - break; - } - - // We don't have a good refactoring for this, so we just annotate the conflict - // For instance, when a local declared by a pattern declaration (`3 is int i`) is - // used outside the block we're trying to extract. - if (pattern.Designation is not SingleVariableDesignationSyntax designation) - { - break; - } - - var identifier = designation.Identifier; - var annotation = ConflictAnnotation.Create(CSharpFeaturesResources.Conflict_s_detected); - var newIdentifier = identifier.WithAdditionalAnnotations(annotation); - var newDesignation = designation.WithIdentifier(newIdentifier); - replacements.Add(pattern, pattern.WithDesignation(newDesignation)); + var newLeadingTrivia = new SyntaxTriviaList(); + newLeadingTrivia = newLeadingTrivia.AddRange(declaration.Type.GetLeadingTrivia()); + newLeadingTrivia = newLeadingTrivia.AddRange(declaration.Type.GetTrailingTrivia()); + newLeadingTrivia = newLeadingTrivia.AddRange(designation.GetLeadingTrivia()); + replacements.Add(declaration, SyntaxFactory.IdentifierName(designation.Identifier) + .WithLeadingTrivia(newLeadingTrivia)); + } + + break; + } + + case SyntaxKind.DeclarationPattern: + { + var pattern = (DeclarationPatternSyntax)node; + if (!variablesToRemove.HasSyntaxAnnotation(pattern)) + { break; } - } - } - return statement.ReplaceNodes(replacements.Keys, (orig, partiallyReplaced) => replacements[orig]); + // We don't have a good refactoring for this, so we just annotate the conflict + // For instance, when a local declared by a pattern declaration (`3 is int i`) is + // used outside the block we're trying to extract. + if (pattern.Designation is not SingleVariableDesignationSyntax designation) + { + break; + } + + var identifier = designation.Identifier; + var annotation = ConflictAnnotation.Create(CSharpFeaturesResources.Conflict_s_detected); + var newIdentifier = identifier.WithAdditionalAnnotations(annotation); + var newDesignation = designation.WithIdentifier(newIdentifier); + replacements.Add(pattern, pattern.WithDesignation(newDesignation)); + + break; + } + } } - private static SyntaxToken ApplyTriviaFromDeclarationToAssignmentIdentifier(LocalDeclarationStatementSyntax declarationStatement, bool firstVariableToAttachTrivia, VariableDeclaratorSyntax variable) - { - var identifier = variable.Identifier; - var typeSyntax = declarationStatement.Declaration.Type; - if (firstVariableToAttachTrivia && typeSyntax != null) - { - var identifierLeadingTrivia = new SyntaxTriviaList(); + return statement.ReplaceNodes(replacements.Keys, (orig, partiallyReplaced) => replacements[orig]); + } - if (typeSyntax.HasLeadingTrivia) - { - identifierLeadingTrivia = identifierLeadingTrivia.AddRange(typeSyntax.GetLeadingTrivia()); - } + private static SyntaxToken ApplyTriviaFromDeclarationToAssignmentIdentifier(LocalDeclarationStatementSyntax declarationStatement, bool firstVariableToAttachTrivia, VariableDeclaratorSyntax variable) + { + var identifier = variable.Identifier; + var typeSyntax = declarationStatement.Declaration.Type; + if (firstVariableToAttachTrivia && typeSyntax != null) + { + var identifierLeadingTrivia = new SyntaxTriviaList(); - identifierLeadingTrivia = identifierLeadingTrivia.AddRange(identifier.LeadingTrivia); - identifier = identifier.WithLeadingTrivia(identifierLeadingTrivia); + if (typeSyntax.HasLeadingTrivia) + { + identifierLeadingTrivia = identifierLeadingTrivia.AddRange(typeSyntax.GetLeadingTrivia()); } - return identifier; + identifierLeadingTrivia = identifierLeadingTrivia.AddRange(identifier.LeadingTrivia); + identifier = identifier.WithLeadingTrivia(identifierLeadingTrivia); } - private ImmutableArray SplitOrMoveDeclarationIntoMethodDefinition( - SyntaxNode insertionPointNode, - ImmutableArray statements, - CancellationToken cancellationToken) - { - var semanticModel = SemanticDocument.SemanticModel; - var postProcessor = new PostProcessor(semanticModel, insertionPointNode.SpanStart); + return identifier; + } + + private ImmutableArray SplitOrMoveDeclarationIntoMethodDefinition( + SyntaxNode insertionPointNode, + ImmutableArray statements, + CancellationToken cancellationToken) + { + var semanticModel = SemanticDocument.SemanticModel; + var postProcessor = new PostProcessor(semanticModel, insertionPointNode.SpanStart); + + var declStatements = CreateDeclarationStatements(AnalyzerResult.GetVariablesToSplitOrMoveIntoMethodDefinition(cancellationToken), cancellationToken); + declStatements = postProcessor.MergeDeclarationStatements(declStatements); - var declStatements = CreateDeclarationStatements(AnalyzerResult.GetVariablesToSplitOrMoveIntoMethodDefinition(cancellationToken), cancellationToken); - declStatements = postProcessor.MergeDeclarationStatements(declStatements); + return declStatements.Concat(statements); + } + + private static ExpressionSyntax CreateAssignmentExpression(SyntaxToken identifier, ExpressionSyntax rvalue) + { + return SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName(identifier), + rvalue); + } - return declStatements.Concat(statements); + protected override bool LastStatementOrHasReturnStatementInReturnableConstruct() + { + var lastStatement = GetLastStatementOrInitializerSelectedAtCallSite(); + var container = lastStatement.GetAncestorsOrThis().FirstOrDefault(n => n.IsReturnableConstruct()); + if (container == null) + { + // case such as field initializer + return false; } - private static ExpressionSyntax CreateAssignmentExpression(SyntaxToken identifier, ExpressionSyntax rvalue) + var blockBody = container.GetBlockBody(); + if (blockBody == null) { - return SyntaxFactory.AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - SyntaxFactory.IdentifierName(identifier), - rvalue); + // such as expression lambda. there is no statement + return false; } - protected override bool LastStatementOrHasReturnStatementInReturnableConstruct() + // check whether it is last statement except return statement + var statements = blockBody.Statements; + if (statements.Last() == lastStatement) { - var lastStatement = GetLastStatementOrInitializerSelectedAtCallSite(); - var container = lastStatement.GetAncestorsOrThis().FirstOrDefault(n => n.IsReturnableConstruct()); - if (container == null) - { - // case such as field initializer - return false; - } + return true; + } - var blockBody = container.GetBlockBody(); - if (blockBody == null) - { - // such as expression lambda. there is no statement - return false; - } + var index = statements.IndexOf((StatementSyntax)lastStatement); + return statements[index + 1].Kind() == SyntaxKind.ReturnStatement; + } - // check whether it is last statement except return statement - var statements = blockBody.Statements; - if (statements.Last() == lastStatement) - { - return true; - } + protected override SyntaxToken CreateIdentifier(string name) + => SyntaxFactory.Identifier(name); - var index = statements.IndexOf((StatementSyntax)lastStatement); - return statements[index + 1].Kind() == SyntaxKind.ReturnStatement; - } + protected override StatementSyntax CreateReturnStatement(string identifierName = null) + { + return string.IsNullOrEmpty(identifierName) + ? SyntaxFactory.ReturnStatement() + : SyntaxFactory.ReturnStatement(SyntaxFactory.IdentifierName(identifierName)); + } + + protected override ExpressionSyntax CreateCallSignature() + { + var methodName = CreateMethodNameForInvocation().WithAdditionalAnnotations(Simplifier.Annotation); + var isLocalFunction = LocalFunction && ShouldLocalFunctionCaptureParameter(SemanticDocument.Root); - protected override SyntaxToken CreateIdentifier(string name) - => SyntaxFactory.Identifier(name); + using var _ = ArrayBuilder.GetInstance(out var arguments); - protected override StatementSyntax CreateReturnStatement(string identifierName = null) + foreach (var argument in AnalyzerResult.MethodParameters) { - return string.IsNullOrEmpty(identifierName) - ? SyntaxFactory.ReturnStatement() - : SyntaxFactory.ReturnStatement(SyntaxFactory.IdentifierName(identifierName)); + if (!isLocalFunction || !argument.CanBeCapturedByLocalFunction) + { + var modifier = GetParameterRefSyntaxKind(argument.ParameterModifier); + var refOrOut = modifier == SyntaxKind.None ? default : Token(modifier); + arguments.Add(Argument(IdentifierName(argument.Name)).WithRefOrOutKeyword(refOrOut)); + } } - protected override ExpressionSyntax CreateCallSignature() + var invocation = (ExpressionSyntax)InvocationExpression(methodName, ArgumentList([.. arguments])); + if (this.SelectionResult.ShouldPutAsyncModifier()) { - var methodName = CreateMethodNameForInvocation().WithAdditionalAnnotations(Simplifier.Annotation); - var isLocalFunction = LocalFunction && ShouldLocalFunctionCaptureParameter(SemanticDocument.Root); - - using var _ = ArrayBuilder.GetInstance(out var arguments); - - foreach (var argument in AnalyzerResult.MethodParameters) + if (this.SelectionResult.ShouldCallConfigureAwaitFalse()) { - if (!isLocalFunction || !argument.CanBeCapturedByLocalFunction) + if (AnalyzerResult.ReturnType.GetMembers().Any(static x => x is IMethodSymbol + { + Name: nameof(Task.ConfigureAwait), + Parameters: [{ Type.SpecialType: SpecialType.System_Boolean }], + })) { - var modifier = GetParameterRefSyntaxKind(argument.ParameterModifier); - var refOrOut = modifier == SyntaxKind.None ? default : Token(modifier); - arguments.Add(Argument(IdentifierName(argument.Name)).WithRefOrOutKeyword(refOrOut)); + invocation = InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + invocation, + IdentifierName(nameof(Task.ConfigureAwait))), + ArgumentList([Argument(LiteralExpression(SyntaxKind.FalseLiteralExpression))])); } } - var invocation = (ExpressionSyntax)InvocationExpression(methodName, ArgumentList([.. arguments])); - if (this.SelectionResult.ShouldPutAsyncModifier()) - { - if (this.SelectionResult.ShouldCallConfigureAwaitFalse()) - { - if (AnalyzerResult.ReturnType.GetMembers().Any(static x => x is IMethodSymbol - { - Name: nameof(Task.ConfigureAwait), - Parameters: [{ Type.SpecialType: SpecialType.System_Boolean }], - })) - { - invocation = InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - invocation, - IdentifierName(nameof(Task.ConfigureAwait))), - ArgumentList([Argument(LiteralExpression(SyntaxKind.FalseLiteralExpression))])); - } - } + invocation = AwaitExpression(invocation); + } - invocation = AwaitExpression(invocation); - } + if (AnalyzerResult.ReturnsByRef) + invocation = RefExpression(invocation); - if (AnalyzerResult.ReturnsByRef) - invocation = RefExpression(invocation); + return invocation; + } - return invocation; - } + protected override StatementSyntax CreateAssignmentExpressionStatement(SyntaxToken identifier, ExpressionSyntax rvalue) + => SyntaxFactory.ExpressionStatement(CreateAssignmentExpression(identifier, rvalue)); - protected override StatementSyntax CreateAssignmentExpressionStatement(SyntaxToken identifier, ExpressionSyntax rvalue) - => SyntaxFactory.ExpressionStatement(CreateAssignmentExpression(identifier, rvalue)); + protected override StatementSyntax CreateDeclarationStatement( + VariableInfo variable, + ExpressionSyntax initialValue, + CancellationToken cancellationToken) + { + var type = variable.GetVariableType(); + var typeNode = type.GenerateTypeSyntax(); - protected override StatementSyntax CreateDeclarationStatement( - VariableInfo variable, - ExpressionSyntax initialValue, - CancellationToken cancellationToken) - { - var type = variable.GetVariableType(); - var typeNode = type.GenerateTypeSyntax(); + var originalIdentifierToken = variable.GetOriginalIdentifierToken(cancellationToken); - var originalIdentifierToken = variable.GetOriginalIdentifierToken(cancellationToken); + // Hierarchy being checked for to see if a using keyword is needed is + // Token -> VariableDeclarator -> VariableDeclaration -> LocalDeclaration + var usingKeyword = originalIdentifierToken.Parent?.Parent?.Parent is LocalDeclarationStatementSyntax { UsingKeyword.FullSpan.IsEmpty: false } + ? SyntaxFactory.Token(SyntaxKind.UsingKeyword) + : default; - // Hierarchy being checked for to see if a using keyword is needed is - // Token -> VariableDeclarator -> VariableDeclaration -> LocalDeclaration - var usingKeyword = originalIdentifierToken.Parent?.Parent?.Parent is LocalDeclarationStatementSyntax { UsingKeyword.FullSpan.IsEmpty: false } - ? SyntaxFactory.Token(SyntaxKind.UsingKeyword) - : default; + var equalsValueClause = initialValue == null ? null : SyntaxFactory.EqualsValueClause(value: initialValue); - var equalsValueClause = initialValue == null ? null : SyntaxFactory.EqualsValueClause(value: initialValue); + return SyntaxFactory.LocalDeclarationStatement( + SyntaxFactory.VariableDeclaration(typeNode) + .AddVariables(SyntaxFactory.VariableDeclarator(SyntaxFactory.Identifier(variable.Name)) + .WithInitializer(equalsValueClause))) + .WithUsingKeyword(usingKeyword); + } - return SyntaxFactory.LocalDeclarationStatement( - SyntaxFactory.VariableDeclaration(typeNode) - .AddVariables(SyntaxFactory.VariableDeclarator(SyntaxFactory.Identifier(variable.Name)) - .WithInitializer(equalsValueClause))) - .WithUsingKeyword(usingKeyword); - } + protected override async Task CreateGeneratedCodeAsync( + SemanticDocument newDocument, CancellationToken cancellationToken) + { + // in hybrid code cases such as extract method, formatter will have some difficulties on where it breaks lines in two. + // here, we explicitly insert newline at the end of "{" of auto generated method decl so that anchor knows how to find out + // indentation of inserted statements (from users code) with user code style preserved + var root = newDocument.Root; + var methodDefinition = root.GetAnnotatedNodes(MethodDefinitionAnnotation).First(); - protected override async Task CreateGeneratedCodeAsync( - SemanticDocument newDocument, CancellationToken cancellationToken) + SyntaxNode newMethodDefinition = methodDefinition switch { - // in hybrid code cases such as extract method, formatter will have some difficulties on where it breaks lines in two. - // here, we explicitly insert newline at the end of "{" of auto generated method decl so that anchor knows how to find out - // indentation of inserted statements (from users code) with user code style preserved - var root = newDocument.Root; - var methodDefinition = root.GetAnnotatedNodes(MethodDefinitionAnnotation).First(); + MethodDeclarationSyntax method => TweakNewLinesInMethod(method), + LocalFunctionStatementSyntax localFunction => TweakNewLinesInMethod(localFunction), + _ => throw new NotSupportedException("SyntaxNode expected to be MethodDeclarationSyntax or LocalFunctionStatementSyntax."), + }; - SyntaxNode newMethodDefinition = methodDefinition switch - { - MethodDeclarationSyntax method => TweakNewLinesInMethod(method), - LocalFunctionStatementSyntax localFunction => TweakNewLinesInMethod(localFunction), - _ => throw new NotSupportedException("SyntaxNode expected to be MethodDeclarationSyntax or LocalFunctionStatementSyntax."), - }; - - newDocument = await newDocument.WithSyntaxRootAsync( - root.ReplaceNode(methodDefinition, newMethodDefinition), cancellationToken).ConfigureAwait(false); + newDocument = await newDocument.WithSyntaxRootAsync( + root.ReplaceNode(methodDefinition, newMethodDefinition), cancellationToken).ConfigureAwait(false); - return await base.CreateGeneratedCodeAsync(newDocument, cancellationToken).ConfigureAwait(false); - } + return await base.CreateGeneratedCodeAsync(newDocument, cancellationToken).ConfigureAwait(false); + } - private static MethodDeclarationSyntax TweakNewLinesInMethod(MethodDeclarationSyntax method) - => TweakNewLinesInMethod(method, method.Body, method.ExpressionBody); + private static MethodDeclarationSyntax TweakNewLinesInMethod(MethodDeclarationSyntax method) + => TweakNewLinesInMethod(method, method.Body, method.ExpressionBody); - private static LocalFunctionStatementSyntax TweakNewLinesInMethod(LocalFunctionStatementSyntax method) - => TweakNewLinesInMethod(method, method.Body, method.ExpressionBody); + private static LocalFunctionStatementSyntax TweakNewLinesInMethod(LocalFunctionStatementSyntax method) + => TweakNewLinesInMethod(method, method.Body, method.ExpressionBody); - private static TDeclarationNode TweakNewLinesInMethod(TDeclarationNode method, BlockSyntax body, ArrowExpressionClauseSyntax expressionBody) where TDeclarationNode : SyntaxNode + private static TDeclarationNode TweakNewLinesInMethod(TDeclarationNode method, BlockSyntax body, ArrowExpressionClauseSyntax expressionBody) where TDeclarationNode : SyntaxNode + { + if (body != null) { - if (body != null) - { - return method.ReplaceToken( - body.OpenBraceToken, - body.OpenBraceToken.WithAppendedTrailingTrivia( - SpecializedCollections.SingletonEnumerable(SyntaxFactory.ElasticCarriageReturnLineFeed))); - } - else if (expressionBody != null) - { - return method.ReplaceToken( - expressionBody.ArrowToken, - expressionBody.ArrowToken.WithPrependedLeadingTrivia( - SpecializedCollections.SingletonEnumerable(SyntaxFactory.ElasticCarriageReturnLineFeed))); - } - else - { - return method; - } + return method.ReplaceToken( + body.OpenBraceToken, + body.OpenBraceToken.WithAppendedTrailingTrivia( + SpecializedCollections.SingletonEnumerable(SyntaxFactory.ElasticCarriageReturnLineFeed))); } - - protected StatementSyntax GetStatementContainingInvocationToExtractedMethodWorker() + else if (expressionBody != null) { - var callSignature = CreateCallSignature(); - - if (AnalyzerResult.HasReturnType) - { - Contract.ThrowIfTrue(AnalyzerResult.HasVariableToUseAsReturnValue); - return SyntaxFactory.ReturnStatement(callSignature); - } - - return SyntaxFactory.ExpressionStatement(callSignature); + return method.ReplaceToken( + expressionBody.ArrowToken, + expressionBody.ArrowToken.WithPrependedLeadingTrivia( + SpecializedCollections.SingletonEnumerable(SyntaxFactory.ElasticCarriageReturnLineFeed))); + } + else + { + return method; } + } - protected override async Task UpdateMethodAfterGenerationAsync( - SemanticDocument originalDocument, - IMethodSymbol methodSymbol, - CancellationToken cancellationToken) + protected StatementSyntax GetStatementContainingInvocationToExtractedMethodWorker() + { + var callSignature = CreateCallSignature(); + + if (AnalyzerResult.HasReturnType) { - // Only need to update for nullable reference types in return - if (methodSymbol.ReturnType.NullableAnnotation != NullableAnnotation.Annotated) - return originalDocument; + Contract.ThrowIfTrue(AnalyzerResult.HasVariableToUseAsReturnValue); + return SyntaxFactory.ReturnStatement(callSignature); + } - var syntaxNode = originalDocument.Root.GetAnnotatedNodesAndTokens(MethodDefinitionAnnotation).FirstOrDefault().AsNode(); - var nodeIsMethodOrLocalFunction = syntaxNode is MethodDeclarationSyntax or LocalFunctionStatementSyntax; - if (!nodeIsMethodOrLocalFunction) - return originalDocument; + return SyntaxFactory.ExpressionStatement(callSignature); + } - var nullableReturnOperations = CheckReturnOperations(syntaxNode, originalDocument, cancellationToken); - if (nullableReturnOperations is not null) - return nullableReturnOperations; + protected override async Task UpdateMethodAfterGenerationAsync( + SemanticDocument originalDocument, + IMethodSymbol methodSymbol, + CancellationToken cancellationToken) + { + // Only need to update for nullable reference types in return + if (methodSymbol.ReturnType.NullableAnnotation != NullableAnnotation.Annotated) + return originalDocument; - var returnType = syntaxNode is MethodDeclarationSyntax method ? method.ReturnType : ((LocalFunctionStatementSyntax)syntaxNode).ReturnType; - var newDocument = await GenerateNewDocumentAsync(methodSymbol, returnType, originalDocument, cancellationToken).ConfigureAwait(false); + var syntaxNode = originalDocument.Root.GetAnnotatedNodesAndTokens(MethodDefinitionAnnotation).FirstOrDefault().AsNode(); + var nodeIsMethodOrLocalFunction = syntaxNode is MethodDeclarationSyntax or LocalFunctionStatementSyntax; + if (!nodeIsMethodOrLocalFunction) + return originalDocument; - return await SemanticDocument.CreateAsync(newDocument, cancellationToken).ConfigureAwait(false); + var nullableReturnOperations = CheckReturnOperations(syntaxNode, originalDocument, cancellationToken); + if (nullableReturnOperations is not null) + return nullableReturnOperations; - static bool ReturnOperationBelongsToMethod(SyntaxNode returnOperationSyntax, SyntaxNode methodSyntax) - { - var enclosingMethod = returnOperationSyntax.FirstAncestorOrSelf(n => n switch - { - BaseMethodDeclarationSyntax _ => true, - AnonymousFunctionExpressionSyntax _ => true, - LocalFunctionStatementSyntax _ => true, - _ => false - }); + var returnType = syntaxNode is MethodDeclarationSyntax method ? method.ReturnType : ((LocalFunctionStatementSyntax)syntaxNode).ReturnType; + var newDocument = await GenerateNewDocumentAsync(methodSymbol, returnType, originalDocument, cancellationToken).ConfigureAwait(false); - return enclosingMethod == methodSyntax; - } + return await SemanticDocument.CreateAsync(newDocument, cancellationToken).ConfigureAwait(false); - static SemanticDocument CheckReturnOperations( - SyntaxNode node, - SemanticDocument originalDocument, - CancellationToken cancellationToken) + static bool ReturnOperationBelongsToMethod(SyntaxNode returnOperationSyntax, SyntaxNode methodSyntax) + { + var enclosingMethod = returnOperationSyntax.FirstAncestorOrSelf(n => n switch { - var semanticModel = originalDocument.SemanticModel; + BaseMethodDeclarationSyntax _ => true, + AnonymousFunctionExpressionSyntax _ => true, + LocalFunctionStatementSyntax _ => true, + _ => false + }); - var methodOperation = semanticModel.GetOperation(node, cancellationToken); - var returnOperations = methodOperation.DescendantsAndSelf().OfType(); + return enclosingMethod == methodSyntax; + } - foreach (var returnOperation in returnOperations) - { - // If the return statement is located in a nested local function or lambda it - // shouldn't contribute to the nullability of the extracted method's return type - if (!ReturnOperationBelongsToMethod(returnOperation.Syntax, methodOperation.Syntax)) - continue; - - var syntax = returnOperation.ReturnedValue?.Syntax ?? returnOperation.Syntax; - var returnTypeInfo = semanticModel.GetTypeInfo(syntax, cancellationToken); - if (returnTypeInfo.Nullability.FlowState == NullableFlowState.MaybeNull) - { - // Flow state shows that return is correctly nullable - return originalDocument; - } - } + static SemanticDocument CheckReturnOperations( + SyntaxNode node, + SemanticDocument originalDocument, + CancellationToken cancellationToken) + { + var semanticModel = originalDocument.SemanticModel; - return null; - } + var methodOperation = semanticModel.GetOperation(node, cancellationToken); + var returnOperations = methodOperation.DescendantsAndSelf().OfType(); - static async Task GenerateNewDocumentAsync( - IMethodSymbol methodSymbol, - TypeSyntax returnType, - SemanticDocument originalDocument, - CancellationToken cancellationToken) + foreach (var returnOperation in returnOperations) { - // Return type can be updated to not be null - var newType = methodSymbol.ReturnType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - - var oldRoot = await originalDocument.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newRoot = oldRoot.ReplaceNode(returnType, newType.GenerateTypeSyntax()); + // If the return statement is located in a nested local function or lambda it + // shouldn't contribute to the nullability of the extracted method's return type + if (!ReturnOperationBelongsToMethod(returnOperation.Syntax, methodOperation.Syntax)) + continue; - return originalDocument.Document.WithSyntaxRoot(newRoot); + var syntax = returnOperation.ReturnedValue?.Syntax ?? returnOperation.Syntax; + var returnTypeInfo = semanticModel.GetTypeInfo(syntax, cancellationToken); + if (returnTypeInfo.Nullability.FlowState == NullableFlowState.MaybeNull) + { + // Flow state shows that return is correctly nullable + return originalDocument; + } } + + return null; } - protected SyntaxToken GenerateMethodNameForStatementGenerators() + static async Task GenerateNewDocumentAsync( + IMethodSymbol methodSymbol, + TypeSyntax returnType, + SemanticDocument originalDocument, + CancellationToken cancellationToken) { - var semanticModel = SemanticDocument.SemanticModel; - var nameGenerator = new UniqueNameGenerator(semanticModel); - var scope = this.SelectionResult.GetContainingScope(); + // Return type can be updated to not be null + var newType = methodSymbol.ReturnType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - // If extracting a local function, we want to ensure all local variables are considered when generating a unique name. - if (LocalFunction) - { - scope = this.SelectionResult.GetFirstTokenInSelection().Parent; - } + var oldRoot = await originalDocument.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = oldRoot.ReplaceNode(returnType, newType.GenerateTypeSyntax()); + + return originalDocument.Document.WithSyntaxRoot(newRoot); + } + } - return SyntaxFactory.Identifier(nameGenerator.CreateUniqueMethodName(scope, GenerateMethodNameFromUserPreference())); + protected SyntaxToken GenerateMethodNameForStatementGenerators() + { + var semanticModel = SemanticDocument.SemanticModel; + var nameGenerator = new UniqueNameGenerator(semanticModel); + var scope = this.SelectionResult.GetContainingScope(); + + // If extracting a local function, we want to ensure all local variables are considered when generating a unique name. + if (LocalFunction) + { + scope = this.SelectionResult.GetFirstTokenInSelection().Parent; } - protected string GenerateMethodNameFromUserPreference() + return SyntaxFactory.Identifier(nameGenerator.CreateUniqueMethodName(scope, GenerateMethodNameFromUserPreference())); + } + + protected string GenerateMethodNameFromUserPreference() + { + var methodName = NewMethodPascalCaseStr; + if (!LocalFunction) { - var methodName = NewMethodPascalCaseStr; - if (!LocalFunction) - { - return methodName; - } + return methodName; + } - // For local functions, pascal case and camel case should be the most common and therefore we only consider those cases. - var localFunctionPreferences = Options.NamingStyle.SymbolSpecifications.Where(symbol => symbol.AppliesTo(new SymbolSpecification.SymbolKindOrTypeKind(MethodKind.LocalFunction), CreateMethodModifiers(), null)); + // For local functions, pascal case and camel case should be the most common and therefore we only consider those cases. + var localFunctionPreferences = Options.NamingStyle.SymbolSpecifications.Where(symbol => symbol.AppliesTo(new SymbolSpecification.SymbolKindOrTypeKind(MethodKind.LocalFunction), CreateMethodModifiers(), null)); - var namingRules = Options.NamingStyle.Rules.NamingRules; - var localFunctionKind = new SymbolSpecification.SymbolKindOrTypeKind(MethodKind.LocalFunction); - if (LocalFunction) + var namingRules = Options.NamingStyle.Rules.NamingRules; + var localFunctionKind = new SymbolSpecification.SymbolKindOrTypeKind(MethodKind.LocalFunction); + if (LocalFunction) + { + if (namingRules.Any(static (rule, arg) => rule.NamingStyle.CapitalizationScheme.Equals(Capitalization.CamelCase) && rule.SymbolSpecification.AppliesTo(arg.localFunctionKind, arg.self.CreateMethodModifiers(), null), (self: this, localFunctionKind))) { - if (namingRules.Any(static (rule, arg) => rule.NamingStyle.CapitalizationScheme.Equals(Capitalization.CamelCase) && rule.SymbolSpecification.AppliesTo(arg.localFunctionKind, arg.self.CreateMethodModifiers(), null), (self: this, localFunctionKind))) - { - methodName = NewMethodCamelCaseStr; - } + methodName = NewMethodCamelCaseStr; } - - // We default to pascal case. - return methodName; } + + // We default to pascal case. + return methodName; } } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.PostProcessor.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.PostProcessor.cs index 2ba087c2cf1e6..51c77bc339f61 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.PostProcessor.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.PostProcessor.cs @@ -12,293 +12,292 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal partial class CSharpMethodExtractor { - internal partial class CSharpMethodExtractor + private class PostProcessor { - private class PostProcessor + private readonly SemanticModel _semanticModel; + private readonly int _contextPosition; + + public PostProcessor(SemanticModel semanticModel, int contextPosition) { - private readonly SemanticModel _semanticModel; - private readonly int _contextPosition; + Contract.ThrowIfNull(semanticModel); - public PostProcessor(SemanticModel semanticModel, int contextPosition) - { - Contract.ThrowIfNull(semanticModel); + _semanticModel = semanticModel; + _contextPosition = contextPosition; + } - _semanticModel = semanticModel; - _contextPosition = contextPosition; + public static ImmutableArray RemoveRedundantBlock(ImmutableArray statements) + { + // it must have only one statement + if (statements.Count() != 1) + { + return statements; } - public static ImmutableArray RemoveRedundantBlock(ImmutableArray statements) + // that statement must be a block + if (statements.Single() is not BlockSyntax block) { - // it must have only one statement - if (statements.Count() != 1) - { - return statements; - } + return statements; + } - // that statement must be a block - if (statements.Single() is not BlockSyntax block) - { - return statements; - } + // we have a block, remove them + return RemoveRedundantBlock(block); + } - // we have a block, remove them - return RemoveRedundantBlock(block); + private static ImmutableArray RemoveRedundantBlock(BlockSyntax block) + { + // if block doesn't have any statement + if (block.Statements.Count == 0) + { + // either remove the block if it doesn't have any trivia, or return as it is if + // there are trivia attached to block + return (block.OpenBraceToken.GetAllTrivia().IsEmpty() && block.CloseBraceToken.GetAllTrivia().IsEmpty()) + ? [] + : [block]; } - private static ImmutableArray RemoveRedundantBlock(BlockSyntax block) - { - // if block doesn't have any statement - if (block.Statements.Count == 0) - { - // either remove the block if it doesn't have any trivia, or return as it is if - // there are trivia attached to block - return (block.OpenBraceToken.GetAllTrivia().IsEmpty() && block.CloseBraceToken.GetAllTrivia().IsEmpty()) - ? [] - : [block]; - } + // okay transfer asset attached to block to statements + var firstStatement = block.Statements.First(); + var firstToken = firstStatement.GetFirstToken(includeZeroWidth: true); + var firstTokenWithAsset = block.OpenBraceToken.CopyAnnotationsTo(firstToken).WithPrependedLeadingTrivia(block.OpenBraceToken.GetAllTrivia()); - // okay transfer asset attached to block to statements - var firstStatement = block.Statements.First(); - var firstToken = firstStatement.GetFirstToken(includeZeroWidth: true); - var firstTokenWithAsset = block.OpenBraceToken.CopyAnnotationsTo(firstToken).WithPrependedLeadingTrivia(block.OpenBraceToken.GetAllTrivia()); + var lastStatement = block.Statements.Last(); + var lastToken = lastStatement.GetLastToken(includeZeroWidth: true); + var lastTokenWithAsset = block.CloseBraceToken.CopyAnnotationsTo(lastToken).WithAppendedTrailingTrivia(block.CloseBraceToken.GetAllTrivia()); - var lastStatement = block.Statements.Last(); - var lastToken = lastStatement.GetLastToken(includeZeroWidth: true); - var lastTokenWithAsset = block.CloseBraceToken.CopyAnnotationsTo(lastToken).WithAppendedTrailingTrivia(block.CloseBraceToken.GetAllTrivia()); + // create new block with new tokens + block = block.ReplaceTokens([firstToken, lastToken], (o, c) => (o == firstToken) ? firstTokenWithAsset : lastTokenWithAsset); - // create new block with new tokens - block = block.ReplaceTokens([firstToken, lastToken], (o, c) => (o == firstToken) ? firstTokenWithAsset : lastTokenWithAsset); + // return only statements without the wrapping block + return ImmutableArray.CreateRange(block.Statements); + } - // return only statements without the wrapping block - return ImmutableArray.CreateRange(block.Statements); + public ImmutableArray MergeDeclarationStatements(ImmutableArray statements) + { + if (statements.FirstOrDefault() == null) + { + return statements; } - public ImmutableArray MergeDeclarationStatements(ImmutableArray statements) + return MergeDeclarationStatementsWorker(statements); + } + + private ImmutableArray MergeDeclarationStatementsWorker(ImmutableArray statements) + { + using var _ = ArrayBuilder.GetInstance(out var result); + + var map = new Dictionary>(); + foreach (var statement in statements) { - if (statements.FirstOrDefault() == null) + if (!IsDeclarationMergable(statement)) { - return statements; + result.AddRange(GetMergedDeclarationStatements(map)); + result.Add(statement); + continue; } - return MergeDeclarationStatementsWorker(statements); + AppendDeclarationStatementToMap(statement as LocalDeclarationStatementSyntax, map); } - private ImmutableArray MergeDeclarationStatementsWorker(ImmutableArray statements) - { - using var _ = ArrayBuilder.GetInstance(out var result); + // merge leftover + if (map.Count > 0) + result.AddRange(GetMergedDeclarationStatements(map)); - var map = new Dictionary>(); - foreach (var statement in statements) - { - if (!IsDeclarationMergable(statement)) - { - result.AddRange(GetMergedDeclarationStatements(map)); - result.Add(statement); - continue; - } + return result.ToImmutable(); + } - AppendDeclarationStatementToMap(statement as LocalDeclarationStatementSyntax, map); - } + private void AppendDeclarationStatementToMap( + LocalDeclarationStatementSyntax statement, + Dictionary> map) + { + Contract.ThrowIfNull(statement); - // merge leftover - if (map.Count > 0) - result.AddRange(GetMergedDeclarationStatements(map)); + var type = _semanticModel.GetSpeculativeTypeInfo(_contextPosition, statement.Declaration.Type, SpeculativeBindingOption.BindAsTypeOrNamespace).Type; + Contract.ThrowIfNull(type); - return result.ToImmutable(); - } + map.GetOrAdd(type, _ => []).Add(statement); + } - private void AppendDeclarationStatementToMap( - LocalDeclarationStatementSyntax statement, - Dictionary> map) + private static IEnumerable GetMergedDeclarationStatements( + Dictionary> map) + { + foreach (var keyValuePair in map) { - Contract.ThrowIfNull(statement); + Contract.ThrowIfFalse(keyValuePair.Value.Count > 0); - var type = _semanticModel.GetSpeculativeTypeInfo(_contextPosition, statement.Declaration.Type, SpeculativeBindingOption.BindAsTypeOrNamespace).Type; - Contract.ThrowIfNull(type); - - map.GetOrAdd(type, _ => []).Add(statement); - } - - private static IEnumerable GetMergedDeclarationStatements( - Dictionary> map) - { - foreach (var keyValuePair in map) + // merge all variable decl for current type + var variables = new List(); + foreach (var statement in keyValuePair.Value) { - Contract.ThrowIfFalse(keyValuePair.Value.Count > 0); - - // merge all variable decl for current type - var variables = new List(); - foreach (var statement in keyValuePair.Value) + foreach (var variable in statement.Declaration.Variables) { - foreach (var variable in statement.Declaration.Variables) - { - variables.Add(variable); - } + variables.Add(variable); } - - // and create one decl statement - // use type name from the first decl statement - yield return - SyntaxFactory.LocalDeclarationStatement( - SyntaxFactory.VariableDeclaration(keyValuePair.Value.First().Declaration.Type, [.. variables])); } - map.Clear(); + // and create one decl statement + // use type name from the first decl statement + yield return + SyntaxFactory.LocalDeclarationStatement( + SyntaxFactory.VariableDeclaration(keyValuePair.Value.First().Declaration.Type, [.. variables])); } - private bool IsDeclarationMergable(StatementSyntax statement) - { - Contract.ThrowIfNull(statement); + map.Clear(); + } - // to be mergable, statement must be - // 1. decl statement without any extra info - // 2. no initialization on any of its decls - // 3. no trivia except whitespace - // 4. type must be known + private bool IsDeclarationMergable(StatementSyntax statement) + { + Contract.ThrowIfNull(statement); - if (statement is not LocalDeclarationStatementSyntax declarationStatement) - { - return false; - } + // to be mergable, statement must be + // 1. decl statement without any extra info + // 2. no initialization on any of its decls + // 3. no trivia except whitespace + // 4. type must be known - if (declarationStatement.Modifiers.Count > 0 || - declarationStatement.IsConst || - declarationStatement.IsMissing) - { - return false; - } + if (statement is not LocalDeclarationStatementSyntax declarationStatement) + { + return false; + } - if (ContainsAnyInitialization(declarationStatement)) - { - return false; - } + if (declarationStatement.Modifiers.Count > 0 || + declarationStatement.IsConst || + declarationStatement.IsMissing) + { + return false; + } - if (!ContainsOnlyWhitespaceTrivia(declarationStatement)) - { - return false; - } + if (ContainsAnyInitialization(declarationStatement)) + { + return false; + } - var semanticInfo = _semanticModel.GetSpeculativeTypeInfo(_contextPosition, declarationStatement.Declaration.Type, SpeculativeBindingOption.BindAsTypeOrNamespace).Type; - if (semanticInfo == null || - semanticInfo.TypeKind is TypeKind.Error or TypeKind.Unknown) - { - return false; - } + if (!ContainsOnlyWhitespaceTrivia(declarationStatement)) + { + return false; + } - return true; + var semanticInfo = _semanticModel.GetSpeculativeTypeInfo(_contextPosition, declarationStatement.Declaration.Type, SpeculativeBindingOption.BindAsTypeOrNamespace).Type; + if (semanticInfo == null || + semanticInfo.TypeKind is TypeKind.Error or TypeKind.Unknown) + { + return false; } - private static bool ContainsAnyInitialization(LocalDeclarationStatementSyntax statement) + return true; + } + + private static bool ContainsAnyInitialization(LocalDeclarationStatementSyntax statement) + { + foreach (var variable in statement.Declaration.Variables) { - foreach (var variable in statement.Declaration.Variables) + if (variable.Initializer != null) { - if (variable.Initializer != null) - { - return true; - } + return true; } - - return false; } - private static bool ContainsOnlyWhitespaceTrivia(StatementSyntax statement) + return false; + } + + private static bool ContainsOnlyWhitespaceTrivia(StatementSyntax statement) + { + foreach (var token in statement.DescendantTokens()) { - foreach (var token in statement.DescendantTokens()) + foreach (var trivia in token.LeadingTrivia.Concat(token.TrailingTrivia)) { - foreach (var trivia in token.LeadingTrivia.Concat(token.TrailingTrivia)) + if (trivia.Kind() is not SyntaxKind.WhitespaceTrivia and + not SyntaxKind.EndOfLineTrivia) { - if (trivia.Kind() is not SyntaxKind.WhitespaceTrivia and - not SyntaxKind.EndOfLineTrivia) - { - return false; - } + return false; } } + } + + return true; + } - return true; + public static ImmutableArray RemoveInitializedDeclarationAndReturnPattern(ImmutableArray statements) + { + // if we have inline temp variable as service, we could just use that service here. + // since it is not a service right now, do very simple clean up + if (statements.ElementAtOrDefault(2) != null) + { + return statements; } - public static ImmutableArray RemoveInitializedDeclarationAndReturnPattern(ImmutableArray statements) + if (statements.ElementAtOrDefault(0) is not LocalDeclarationStatementSyntax declaration || statements.ElementAtOrDefault(1) is not ReturnStatementSyntax returnStatement) { - // if we have inline temp variable as service, we could just use that service here. - // since it is not a service right now, do very simple clean up - if (statements.ElementAtOrDefault(2) != null) - { - return statements; - } + return statements; + } - if (statements.ElementAtOrDefault(0) is not LocalDeclarationStatementSyntax declaration || statements.ElementAtOrDefault(1) is not ReturnStatementSyntax returnStatement) - { - return statements; - } + if (declaration.Declaration == null || + declaration.Declaration.Variables.Count != 1 || + declaration.Declaration.Variables[0].Initializer == null || + declaration.Declaration.Variables[0].Initializer.Value == null || + declaration.Declaration.Variables[0].Initializer.Value is StackAllocArrayCreationExpressionSyntax || + returnStatement.Expression == null) + { + return statements; + } - if (declaration.Declaration == null || - declaration.Declaration.Variables.Count != 1 || - declaration.Declaration.Variables[0].Initializer == null || - declaration.Declaration.Variables[0].Initializer.Value == null || - declaration.Declaration.Variables[0].Initializer.Value is StackAllocArrayCreationExpressionSyntax || - returnStatement.Expression == null) - { - return statements; - } + if (!ContainsOnlyWhitespaceTrivia(declaration) || + !ContainsOnlyWhitespaceTrivia(returnStatement)) + { + return statements; + } - if (!ContainsOnlyWhitespaceTrivia(declaration) || - !ContainsOnlyWhitespaceTrivia(returnStatement)) - { - return statements; - } + var variableName = declaration.Declaration.Variables[0].Identifier.ToString(); + if (returnStatement.Expression.ToString() != variableName) + { + return statements; + } - var variableName = declaration.Declaration.Variables[0].Identifier.ToString(); - if (returnStatement.Expression.ToString() != variableName) - { - return statements; - } + return [SyntaxFactory.ReturnStatement(declaration.Declaration.Variables[0].Initializer.Value)]; + } - return [SyntaxFactory.ReturnStatement(declaration.Declaration.Variables[0].Initializer.Value)]; + public static ImmutableArray RemoveDeclarationAssignmentPattern(ImmutableArray statements) + { + if (statements.ElementAtOrDefault(0) is not LocalDeclarationStatementSyntax declaration || statements.ElementAtOrDefault(1) is not ExpressionStatementSyntax assignment) + { + return statements; } - public static ImmutableArray RemoveDeclarationAssignmentPattern(ImmutableArray statements) + if (ContainsAnyInitialization(declaration) || + declaration.Declaration == null || + declaration.Declaration.Variables.Count != 1 || + assignment.Expression == null || + assignment.Expression.Kind() != SyntaxKind.SimpleAssignmentExpression) { - if (statements.ElementAtOrDefault(0) is not LocalDeclarationStatementSyntax declaration || statements.ElementAtOrDefault(1) is not ExpressionStatementSyntax assignment) - { - return statements; - } - - if (ContainsAnyInitialization(declaration) || - declaration.Declaration == null || - declaration.Declaration.Variables.Count != 1 || - assignment.Expression == null || - assignment.Expression.Kind() != SyntaxKind.SimpleAssignmentExpression) - { - return statements; - } - - if (!ContainsOnlyWhitespaceTrivia(declaration) || - !ContainsOnlyWhitespaceTrivia(assignment)) - { - return statements; - } + return statements; + } - var variableName = declaration.Declaration.Variables[0].Identifier.ToString(); + if (!ContainsOnlyWhitespaceTrivia(declaration) || + !ContainsOnlyWhitespaceTrivia(assignment)) + { + return statements; + } - var assignmentExpression = assignment.Expression as AssignmentExpressionSyntax; - if (assignmentExpression.Left == null || - assignmentExpression.Right == null || - assignmentExpression.Left.ToString() != variableName) - { - return statements; - } + var variableName = declaration.Declaration.Variables[0].Identifier.ToString(); - var variable = declaration.Declaration.Variables[0].WithInitializer(SyntaxFactory.EqualsValueClause(assignmentExpression.Right)); - return - [ - declaration.WithDeclaration( - declaration.Declaration.WithVariables([variable])), - .. statements.Skip(2), - ]; + var assignmentExpression = assignment.Expression as AssignmentExpressionSyntax; + if (assignmentExpression.Left == null || + assignmentExpression.Right == null || + assignmentExpression.Left.ToString() != variableName) + { + return statements; } + + var variable = declaration.Declaration.Variables[0].WithInitializer(SyntaxFactory.EqualsValueClause(assignmentExpression.Right)); + return + [ + declaration.WithDeclaration( + declaration.Declaration.WithVariables([variable])), + .. statements.Skip(2), + ]; } } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.TriviaResult.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.TriviaResult.cs index 776560c84b68a..0d85ae254fd28 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.TriviaResult.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.TriviaResult.cs @@ -13,172 +13,171 @@ using Microsoft.CodeAnalysis.ExtractMethod; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal partial class CSharpMethodExtractor { - internal partial class CSharpMethodExtractor + private class CSharpTriviaResult : TriviaResult { - private class CSharpTriviaResult : TriviaResult + public static async Task ProcessAsync(CSharpSelectionResult selectionResult, CancellationToken cancellationToken) + { + var preservationService = selectionResult.SemanticDocument.Document.Project.Services.GetService(); + var root = selectionResult.SemanticDocument.Root; + var result = preservationService.SaveTriviaAroundSelection(root, selectionResult.FinalSpan); + return new CSharpTriviaResult( + await selectionResult.SemanticDocument.WithSyntaxRootAsync(result.Root, cancellationToken).ConfigureAwait(false), + result); + } + + private CSharpTriviaResult(SemanticDocument document, ITriviaSavedResult result) + : base(document, result, (int)SyntaxKind.EndOfLineTrivia, (int)SyntaxKind.WhitespaceTrivia) + { + } + + protected override AnnotationResolver GetAnnotationResolver(SyntaxNode callsite, SyntaxNode method) { - public static async Task ProcessAsync(CSharpSelectionResult selectionResult, CancellationToken cancellationToken) + var isMethodOrLocalFunction = method is MethodDeclarationSyntax or LocalFunctionStatementSyntax; + if (callsite == null || !isMethodOrLocalFunction) { - var preservationService = selectionResult.SemanticDocument.Document.Project.Services.GetService(); - var root = selectionResult.SemanticDocument.Root; - var result = preservationService.SaveTriviaAroundSelection(root, selectionResult.FinalSpan); - return new CSharpTriviaResult( - await selectionResult.SemanticDocument.WithSyntaxRootAsync(result.Root, cancellationToken).ConfigureAwait(false), - result); + return null; } - private CSharpTriviaResult(SemanticDocument document, ITriviaSavedResult result) - : base(document, result, (int)SyntaxKind.EndOfLineTrivia, (int)SyntaxKind.WhitespaceTrivia) + return (node, location, annotation) => AnnotationResolver(node, location, annotation, callsite, method); + } + + protected override TriviaResolver GetTriviaResolver(SyntaxNode method) + { + var isMethodOrLocalFunction = method is MethodDeclarationSyntax or LocalFunctionStatementSyntax; + if (!isMethodOrLocalFunction) { + return null; } - protected override AnnotationResolver GetAnnotationResolver(SyntaxNode callsite, SyntaxNode method) - { - var isMethodOrLocalFunction = method is MethodDeclarationSyntax or LocalFunctionStatementSyntax; - if (callsite == null || !isMethodOrLocalFunction) - { - return null; - } + return (location, tokenPair, triviaMap) => TriviaResolver(location, tokenPair, triviaMap, method); + } - return (node, location, annotation) => AnnotationResolver(node, location, annotation, callsite, method); + private static SyntaxToken AnnotationResolver( + SyntaxNode node, + TriviaLocation location, + SyntaxAnnotation annotation, + SyntaxNode callsite, + SyntaxNode method) + { + var token = node.GetAnnotatedNodesAndTokens(annotation).FirstOrDefault().AsToken(); + if (token.RawKind != 0) + { + return token; } - protected override TriviaResolver GetTriviaResolver(SyntaxNode method) + var (body, expressionBody, semicolonToken) = GetResolverElements(method); + return location switch { - var isMethodOrLocalFunction = method is MethodDeclarationSyntax or LocalFunctionStatementSyntax; - if (!isMethodOrLocalFunction) - { - return null; - } + TriviaLocation.BeforeBeginningOfSpan => callsite.GetFirstToken(includeZeroWidth: true).GetPreviousToken(includeZeroWidth: true), + TriviaLocation.AfterEndOfSpan => callsite.GetLastToken(includeZeroWidth: true).GetNextToken(includeZeroWidth: true), + TriviaLocation.AfterBeginningOfSpan => body != null + ? body.OpenBraceToken.GetNextToken(includeZeroWidth: true) + : expressionBody.ArrowToken.GetNextToken(includeZeroWidth: true), + TriviaLocation.BeforeEndOfSpan => body != null + ? body.CloseBraceToken.GetPreviousToken(includeZeroWidth: true) + : semicolonToken, + _ => throw ExceptionUtilities.UnexpectedValue(location) + }; + } - return (location, tokenPair, triviaMap) => TriviaResolver(location, tokenPair, triviaMap, method); - } + private IEnumerable TriviaResolver( + TriviaLocation location, + PreviousNextTokenPair tokenPair, + Dictionary triviaMap, + SyntaxNode method) + { + // Resolve trivia at the edge of the selection. simple case is easy to deal with, but complex cases where + // elastic trivia and user trivia are mixed (hybrid case) and we want to preserve some part of user coding style + // but not others can be dealt with here. - private static SyntaxToken AnnotationResolver( - SyntaxNode node, - TriviaLocation location, - SyntaxAnnotation annotation, - SyntaxNode callsite, - SyntaxNode method) + var (body, expressionBody, semicolonToken) = GetResolverElements(method); + + // method has no statement in them. so basically two trivia list now pointing to same thing. "{" and "}" + if (body != null) { - var token = node.GetAnnotatedNodesAndTokens(annotation).FirstOrDefault().AsToken(); - if (token.RawKind != 0) + if (tokenPair.PreviousToken == body.OpenBraceToken && + tokenPair.NextToken == body.CloseBraceToken) { - return token; + return (location == TriviaLocation.AfterBeginningOfSpan) + ? SpecializedCollections.SingletonEnumerable(SyntaxFactory.ElasticMarker) + : SpecializedCollections.EmptyEnumerable(); } - - var (body, expressionBody, semicolonToken) = GetResolverElements(method); - return location switch - { - TriviaLocation.BeforeBeginningOfSpan => callsite.GetFirstToken(includeZeroWidth: true).GetPreviousToken(includeZeroWidth: true), - TriviaLocation.AfterEndOfSpan => callsite.GetLastToken(includeZeroWidth: true).GetNextToken(includeZeroWidth: true), - TriviaLocation.AfterBeginningOfSpan => body != null - ? body.OpenBraceToken.GetNextToken(includeZeroWidth: true) - : expressionBody.ArrowToken.GetNextToken(includeZeroWidth: true), - TriviaLocation.BeforeEndOfSpan => body != null - ? body.CloseBraceToken.GetPreviousToken(includeZeroWidth: true) - : semicolonToken, - _ => throw ExceptionUtilities.UnexpectedValue(location) - }; } - - private IEnumerable TriviaResolver( - TriviaLocation location, - PreviousNextTokenPair tokenPair, - Dictionary triviaMap, - SyntaxNode method) + else { - // Resolve trivia at the edge of the selection. simple case is easy to deal with, but complex cases where - // elastic trivia and user trivia are mixed (hybrid case) and we want to preserve some part of user coding style - // but not others can be dealt with here. - - var (body, expressionBody, semicolonToken) = GetResolverElements(method); - - // method has no statement in them. so basically two trivia list now pointing to same thing. "{" and "}" - if (body != null) + if (tokenPair.PreviousToken == expressionBody.ArrowToken && + tokenPair.NextToken.GetPreviousToken() == semicolonToken) { - if (tokenPair.PreviousToken == body.OpenBraceToken && - tokenPair.NextToken == body.CloseBraceToken) - { - return (location == TriviaLocation.AfterBeginningOfSpan) - ? SpecializedCollections.SingletonEnumerable(SyntaxFactory.ElasticMarker) - : SpecializedCollections.EmptyEnumerable(); - } - } - else - { - if (tokenPair.PreviousToken == expressionBody.ArrowToken && - tokenPair.NextToken.GetPreviousToken() == semicolonToken) - { - return (location == TriviaLocation.AfterBeginningOfSpan) - ? SpecializedCollections.SingletonEnumerable(SyntaxFactory.ElasticMarker) - : SpecializedCollections.EmptyEnumerable(); - } + return (location == TriviaLocation.AfterBeginningOfSpan) + ? SpecializedCollections.SingletonEnumerable(SyntaxFactory.ElasticMarker) + : SpecializedCollections.EmptyEnumerable(); } + } - triviaMap.TryGetValue(tokenPair.PreviousToken, out var previousTriviaPair); - var trailingTrivia = previousTriviaPair.TrailingTrivia ?? SpecializedCollections.EmptyEnumerable(); + triviaMap.TryGetValue(tokenPair.PreviousToken, out var previousTriviaPair); + var trailingTrivia = previousTriviaPair.TrailingTrivia ?? SpecializedCollections.EmptyEnumerable(); - triviaMap.TryGetValue(tokenPair.NextToken, out var nextTriviaPair); - var leadingTrivia = nextTriviaPair.LeadingTrivia ?? SpecializedCollections.EmptyEnumerable(); + triviaMap.TryGetValue(tokenPair.NextToken, out var nextTriviaPair); + var leadingTrivia = nextTriviaPair.LeadingTrivia ?? SpecializedCollections.EmptyEnumerable(); - var list = trailingTrivia.Concat(leadingTrivia); + var list = trailingTrivia.Concat(leadingTrivia); - return location switch - { - TriviaLocation.BeforeBeginningOfSpan => FilterBeforeBeginningOfSpan(tokenPair, list), - TriviaLocation.AfterEndOfSpan => FilterTriviaList(list.Concat(tokenPair.NextToken.LeadingTrivia)), - TriviaLocation.AfterBeginningOfSpan => FilterTriviaList(AppendTrailingTrivia(tokenPair).Concat(list).Concat(tokenPair.NextToken.LeadingTrivia)), - TriviaLocation.BeforeEndOfSpan => FilterTriviaList(tokenPair.PreviousToken.TrailingTrivia.Concat(list).Concat(tokenPair.NextToken.LeadingTrivia)), - _ => throw ExceptionUtilities.UnexpectedValue(location), - }; - } - - private static (BlockSyntax body, ArrowExpressionClauseSyntax expressionBody, SyntaxToken semicolonToken) GetResolverElements(SyntaxNode method) + return location switch { - return method switch - { - MethodDeclarationSyntax methodDeclaration => (methodDeclaration.Body, methodDeclaration.ExpressionBody, methodDeclaration.SemicolonToken), - LocalFunctionStatementSyntax localFunctionDeclaration => (localFunctionDeclaration.Body, localFunctionDeclaration.ExpressionBody, localFunctionDeclaration.SemicolonToken), - _ => throw ExceptionUtilities.UnexpectedValue(method) - }; - } + TriviaLocation.BeforeBeginningOfSpan => FilterBeforeBeginningOfSpan(tokenPair, list), + TriviaLocation.AfterEndOfSpan => FilterTriviaList(list.Concat(tokenPair.NextToken.LeadingTrivia)), + TriviaLocation.AfterBeginningOfSpan => FilterTriviaList(AppendTrailingTrivia(tokenPair).Concat(list).Concat(tokenPair.NextToken.LeadingTrivia)), + TriviaLocation.BeforeEndOfSpan => FilterTriviaList(tokenPair.PreviousToken.TrailingTrivia.Concat(list).Concat(tokenPair.NextToken.LeadingTrivia)), + _ => throw ExceptionUtilities.UnexpectedValue(location), + }; + } - private IEnumerable FilterBeforeBeginningOfSpan(PreviousNextTokenPair tokenPair, IEnumerable list) + private static (BlockSyntax body, ArrowExpressionClauseSyntax expressionBody, SyntaxToken semicolonToken) GetResolverElements(SyntaxNode method) + { + return method switch { - var allList = FilterTriviaList(tokenPair.PreviousToken.TrailingTrivia.Concat(list).Concat(AppendLeadingTrivia(tokenPair))); + MethodDeclarationSyntax methodDeclaration => (methodDeclaration.Body, methodDeclaration.ExpressionBody, methodDeclaration.SemicolonToken), + LocalFunctionStatementSyntax localFunctionDeclaration => (localFunctionDeclaration.Body, localFunctionDeclaration.ExpressionBody, localFunctionDeclaration.SemicolonToken), + _ => throw ExceptionUtilities.UnexpectedValue(method) + }; + } - if (tokenPair.PreviousToken.RawKind == (int)SyntaxKind.OpenBraceToken) - { - return RemoveBlankLines(allList); - } + private IEnumerable FilterBeforeBeginningOfSpan(PreviousNextTokenPair tokenPair, IEnumerable list) + { + var allList = FilterTriviaList(tokenPair.PreviousToken.TrailingTrivia.Concat(list).Concat(AppendLeadingTrivia(tokenPair))); - return allList; + if (tokenPair.PreviousToken.RawKind == (int)SyntaxKind.OpenBraceToken) + { + return RemoveBlankLines(allList); } - private static IEnumerable AppendLeadingTrivia(PreviousNextTokenPair tokenPair) - { - if (tokenPair.PreviousToken.RawKind is ((int)SyntaxKind.OpenBraceToken) or - ((int)SyntaxKind.SemicolonToken)) - { - return tokenPair.NextToken.LeadingTrivia; - } + return allList; + } - return SpecializedCollections.EmptyEnumerable(); + private static IEnumerable AppendLeadingTrivia(PreviousNextTokenPair tokenPair) + { + if (tokenPair.PreviousToken.RawKind is ((int)SyntaxKind.OpenBraceToken) or + ((int)SyntaxKind.SemicolonToken)) + { + return tokenPair.NextToken.LeadingTrivia; } - private static IEnumerable AppendTrailingTrivia(PreviousNextTokenPair tokenPair) - { - if (tokenPair.PreviousToken.RawKind is ((int)SyntaxKind.OpenBraceToken) or - ((int)SyntaxKind.SemicolonToken)) - { - return tokenPair.PreviousToken.TrailingTrivia; - } + return SpecializedCollections.EmptyEnumerable(); + } - return SpecializedCollections.EmptyEnumerable(); + private static IEnumerable AppendTrailingTrivia(PreviousNextTokenPair tokenPair) + { + if (tokenPair.PreviousToken.RawKind is ((int)SyntaxKind.OpenBraceToken) or + ((int)SyntaxKind.SemicolonToken)) + { + return tokenPair.PreviousToken.TrailingTrivia; } + + return SpecializedCollections.EmptyEnumerable(); } } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.cs index 8a7d560c793ea..0db1910e9e56d 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.cs @@ -19,187 +19,186 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal partial class CSharpMethodExtractor(CSharpSelectionResult result, ExtractMethodGenerationOptions options, bool localFunction) + : MethodExtractor(result, options, localFunction) { - internal partial class CSharpMethodExtractor(CSharpSelectionResult result, ExtractMethodGenerationOptions options, bool localFunction) - : MethodExtractor(result, options, localFunction) + protected override CodeGenerator CreateCodeGenerator(AnalyzerResult analyzerResult) + => CSharpCodeGenerator.Create(this.OriginalSelectionResult, analyzerResult, (CSharpCodeGenerationOptions)this.Options.CodeGenerationOptions, this.LocalFunction); + + protected override AnalyzerResult Analyze(CSharpSelectionResult selectionResult, bool localFunction, CancellationToken cancellationToken) + => CSharpAnalyzer.Analyze(selectionResult, localFunction, cancellationToken); + + protected override SyntaxNode GetInsertionPointNode( + AnalyzerResult analyzerResult, CancellationToken cancellationToken) { - protected override CodeGenerator CreateCodeGenerator(AnalyzerResult analyzerResult) - => CSharpCodeGenerator.Create(this.OriginalSelectionResult, analyzerResult, (CSharpCodeGenerationOptions)this.Options.CodeGenerationOptions, this.LocalFunction); + var originalSpanStart = OriginalSelectionResult.OriginalSpan.Start; + Contract.ThrowIfFalse(originalSpanStart >= 0); - protected override AnalyzerResult Analyze(CSharpSelectionResult selectionResult, bool localFunction, CancellationToken cancellationToken) - => CSharpAnalyzer.Analyze(selectionResult, localFunction, cancellationToken); + var document = this.OriginalSelectionResult.SemanticDocument; + var root = document.Root; - protected override SyntaxNode GetInsertionPointNode( - AnalyzerResult analyzerResult, CancellationToken cancellationToken) + if (LocalFunction) { - var originalSpanStart = OriginalSelectionResult.OriginalSpan.Start; - Contract.ThrowIfFalse(originalSpanStart >= 0); + // If we are extracting a local function and are within a local function, then we want the new function to be created within the + // existing local function instead of the overarching method. + var outermostCapturedVariable = analyzerResult.GetOutermostVariableToMoveIntoMethodDefinition(cancellationToken); + var baseNode = outermostCapturedVariable != null + ? outermostCapturedVariable.GetIdentifierTokenAtDeclaration(document).Parent + : this.OriginalSelectionResult.GetOutermostCallSiteContainerToProcess(cancellationToken); + + if (baseNode is CompilationUnitSyntax) + { + // In some sort of global statement. Have to special case these a bit for script files. + var globalStatement = root.FindToken(originalSpanStart).GetAncestor(); + if (globalStatement is null) + return null; - var document = this.OriginalSelectionResult.SemanticDocument; - var root = document.Root; + return GetInsertionPointForGlobalStatement(globalStatement, globalStatement); + } - if (LocalFunction) + var currentNode = baseNode; + while (currentNode is not null) { - // If we are extracting a local function and are within a local function, then we want the new function to be created within the - // existing local function instead of the overarching method. - var outermostCapturedVariable = analyzerResult.GetOutermostVariableToMoveIntoMethodDefinition(cancellationToken); - var baseNode = outermostCapturedVariable != null - ? outermostCapturedVariable.GetIdentifierTokenAtDeclaration(document).Parent - : this.OriginalSelectionResult.GetOutermostCallSiteContainerToProcess(cancellationToken); - - if (baseNode is CompilationUnitSyntax) + if (currentNode is AnonymousFunctionExpressionSyntax anonymousFunction) { - // In some sort of global statement. Have to special case these a bit for script files. - var globalStatement = root.FindToken(originalSpanStart).GetAncestor(); - if (globalStatement is null) - return null; + if (OriginalSelectionWithin(anonymousFunction.Body) || OriginalSelectionWithin(anonymousFunction.ExpressionBody)) + return currentNode; - return GetInsertionPointForGlobalStatement(globalStatement, globalStatement); + if (!OriginalSelectionResult.OriginalSpan.Contains(anonymousFunction.Span)) + { + // If we encountered a function but the selection isn't within the body, it's likely the user + // is attempting to move the function (which is behavior that is supported). Stop looking up the + // tree and assume the encapsulating member is the right place to put the local function. This is to help + // maintain the behavior introduced with https://github.com/dotnet/roslyn/pull/41377 + break; + } } - var currentNode = baseNode; - while (currentNode is not null) + if (currentNode is LocalFunctionStatementSyntax localFunction) { - if (currentNode is AnonymousFunctionExpressionSyntax anonymousFunction) - { - if (OriginalSelectionWithin(anonymousFunction.Body) || OriginalSelectionWithin(anonymousFunction.ExpressionBody)) - return currentNode; - - if (!OriginalSelectionResult.OriginalSpan.Contains(anonymousFunction.Span)) - { - // If we encountered a function but the selection isn't within the body, it's likely the user - // is attempting to move the function (which is behavior that is supported). Stop looking up the - // tree and assume the encapsulating member is the right place to put the local function. This is to help - // maintain the behavior introduced with https://github.com/dotnet/roslyn/pull/41377 - break; - } - } + if (OriginalSelectionWithin(localFunction.ExpressionBody) || OriginalSelectionWithin(localFunction.Body)) + return currentNode; - if (currentNode is LocalFunctionStatementSyntax localFunction) + if (!OriginalSelectionResult.OriginalSpan.Contains(localFunction.Span)) { - if (OriginalSelectionWithin(localFunction.ExpressionBody) || OriginalSelectionWithin(localFunction.Body)) - return currentNode; - - if (!OriginalSelectionResult.OriginalSpan.Contains(localFunction.Span)) - { - // If we encountered a function but the selection isn't within the body, it's likely the user - // is attempting to move the function (which is behavior that is supported). Stop looking up the - // tree and assume the encapsulating member is the right place to put the local function. This is to help - // maintain the behavior introduced with https://github.com/dotnet/roslyn/pull/41377 - break; - } + // If we encountered a function but the selection isn't within the body, it's likely the user + // is attempting to move the function (which is behavior that is supported). Stop looking up the + // tree and assume the encapsulating member is the right place to put the local function. This is to help + // maintain the behavior introduced with https://github.com/dotnet/roslyn/pull/41377 + break; } + } - if (currentNode is AccessorDeclarationSyntax) - return currentNode; + if (currentNode is AccessorDeclarationSyntax) + return currentNode; - if (currentNode is BaseMethodDeclarationSyntax) - return currentNode; + if (currentNode is BaseMethodDeclarationSyntax) + return currentNode; - if (currentNode is GlobalStatementSyntax globalStatement) + if (currentNode is GlobalStatementSyntax globalStatement) + { + // check whether the global statement is a statement container + if (!globalStatement.Statement.IsStatementContainerNode() && !root.SyntaxTree.IsScript()) { - // check whether the global statement is a statement container - if (!globalStatement.Statement.IsStatementContainerNode() && !root.SyntaxTree.IsScript()) - { - // The extracted function will be a new global statement - return globalStatement.Parent; - } - - return globalStatement.Statement; + // The extracted function will be a new global statement + return globalStatement.Parent; } - currentNode = currentNode.Parent; + return globalStatement.Statement; } - return null; - } - else - { - var baseToken = root.FindToken(originalSpanStart); - var memberNode = baseToken.GetAncestor(); - Contract.ThrowIfNull(memberNode); - Contract.ThrowIfTrue(memberNode.Kind() == SyntaxKind.NamespaceDeclaration); - - if (memberNode is GlobalStatementSyntax globalStatement) - return GetInsertionPointForGlobalStatement(globalStatement, memberNode); - - return memberNode; + currentNode = currentNode.Parent; } - SyntaxNode GetInsertionPointForGlobalStatement(GlobalStatementSyntax globalStatement, MemberDeclarationSyntax memberNode) - { - // check whether we are extracting whole global statement out - if (OriginalSelectionResult.FinalSpan.Contains(memberNode.Span)) - return globalStatement.Parent; + return null; + } + else + { + var baseToken = root.FindToken(originalSpanStart); + var memberNode = baseToken.GetAncestor(); + Contract.ThrowIfNull(memberNode); + Contract.ThrowIfTrue(memberNode.Kind() == SyntaxKind.NamespaceDeclaration); - // check whether the global statement is a statement container - if (!globalStatement.Statement.IsStatementContainerNode() && !root.SyntaxTree.IsScript()) - { - // The extracted function will be a new global statement - return globalStatement.Parent; - } + if (memberNode is GlobalStatementSyntax globalStatement) + return GetInsertionPointForGlobalStatement(globalStatement, memberNode); - return globalStatement.Statement; - } + return memberNode; } - private bool OriginalSelectionWithin(SyntaxNode node) + SyntaxNode GetInsertionPointForGlobalStatement(GlobalStatementSyntax globalStatement, MemberDeclarationSyntax memberNode) { - if (node is null) + // check whether we are extracting whole global statement out + if (OriginalSelectionResult.FinalSpan.Contains(memberNode.Span)) + return globalStatement.Parent; + + // check whether the global statement is a statement container + if (!globalStatement.Statement.IsStatementContainerNode() && !root.SyntaxTree.IsScript()) { - return false; + // The extracted function will be a new global statement + return globalStatement.Parent; } - return node.Span.Contains(OriginalSelectionResult.OriginalSpan); + return globalStatement.Statement; } + } - protected override async Task PreserveTriviaAsync(CSharpSelectionResult selectionResult, CancellationToken cancellationToken) - => await CSharpTriviaResult.ProcessAsync(selectionResult, cancellationToken).ConfigureAwait(false); + private bool OriginalSelectionWithin(SyntaxNode node) + { + if (node is null) + { + return false; + } + + return node.Span.Contains(OriginalSelectionResult.OriginalSpan); + } - protected override Task GenerateCodeAsync(InsertionPoint insertionPoint, CSharpSelectionResult selectionResult, AnalyzerResult analyzeResult, CodeGenerationOptions options, CancellationToken cancellationToken) - => CSharpCodeGenerator.GenerateAsync(insertionPoint, selectionResult, analyzeResult, (CSharpCodeGenerationOptions)options, LocalFunction, cancellationToken); + protected override async Task PreserveTriviaAsync(CSharpSelectionResult selectionResult, CancellationToken cancellationToken) + => await CSharpTriviaResult.ProcessAsync(selectionResult, cancellationToken).ConfigureAwait(false); - protected override AbstractFormattingRule GetCustomFormattingRule(Document document) - => FormattingRule.Instance; + protected override Task GenerateCodeAsync(InsertionPoint insertionPoint, CSharpSelectionResult selectionResult, AnalyzerResult analyzeResult, CodeGenerationOptions options, CancellationToken cancellationToken) + => CSharpCodeGenerator.GenerateAsync(insertionPoint, selectionResult, analyzeResult, (CSharpCodeGenerationOptions)options, LocalFunction, cancellationToken); - protected override SyntaxToken? GetInvocationNameToken(IEnumerable methodNames) - => methodNames.FirstOrNull(t => !t.Parent.IsKind(SyntaxKind.MethodDeclaration)); + protected override AbstractFormattingRule GetCustomFormattingRule(Document document) + => FormattingRule.Instance; - protected override SyntaxNode ParseTypeName(string name) - => SyntaxFactory.ParseTypeName(name); + protected override SyntaxToken? GetInvocationNameToken(IEnumerable methodNames) + => methodNames.FirstOrNull(t => !t.Parent.IsKind(SyntaxKind.MethodDeclaration)); - protected override async Task<(Document document, SyntaxToken? invocationNameToken)> InsertNewLineBeforeLocalFunctionIfNecessaryAsync( - Document document, - SyntaxToken? invocationNameToken, - SyntaxNode methodDefinition, - CancellationToken cancellationToken) + protected override SyntaxNode ParseTypeName(string name) + => SyntaxFactory.ParseTypeName(name); + + protected override async Task<(Document document, SyntaxToken? invocationNameToken)> InsertNewLineBeforeLocalFunctionIfNecessaryAsync( + Document document, + SyntaxToken? invocationNameToken, + SyntaxNode methodDefinition, + CancellationToken cancellationToken) + { + // Checking to see if there is already an empty line before the local method declaration. + var leadingTrivia = methodDefinition.GetLeadingTrivia(); + if (!leadingTrivia.Any(t => t.IsKind(SyntaxKind.EndOfLineTrivia)) && !methodDefinition.FindTokenOnLeftOfPosition(methodDefinition.SpanStart).IsKind(SyntaxKind.OpenBraceToken)) { - // Checking to see if there is already an empty line before the local method declaration. - var leadingTrivia = methodDefinition.GetLeadingTrivia(); - if (!leadingTrivia.Any(t => t.IsKind(SyntaxKind.EndOfLineTrivia)) && !methodDefinition.FindTokenOnLeftOfPosition(methodDefinition.SpanStart).IsKind(SyntaxKind.OpenBraceToken)) + var originalMethodDefinition = methodDefinition; + var newLine = Options.LineFormattingOptions.NewLine; + methodDefinition = methodDefinition.WithPrependedLeadingTrivia(SpecializedCollections.SingletonEnumerable(SyntaxFactory.EndOfLine(newLine))); + + if (!originalMethodDefinition.FindTokenOnLeftOfPosition(originalMethodDefinition.SpanStart).TrailingTrivia.Any(SyntaxKind.EndOfLineTrivia)) { - var originalMethodDefinition = methodDefinition; - var newLine = Options.LineFormattingOptions.NewLine; + // Add a second new line since there were no line endings in the original form methodDefinition = methodDefinition.WithPrependedLeadingTrivia(SpecializedCollections.SingletonEnumerable(SyntaxFactory.EndOfLine(newLine))); + } - if (!originalMethodDefinition.FindTokenOnLeftOfPosition(originalMethodDefinition.SpanStart).TrailingTrivia.Any(SyntaxKind.EndOfLineTrivia)) - { - // Add a second new line since there were no line endings in the original form - methodDefinition = methodDefinition.WithPrependedLeadingTrivia(SpecializedCollections.SingletonEnumerable(SyntaxFactory.EndOfLine(newLine))); - } - - // Generating the new document and associated variables. - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - document = document.WithSyntaxRoot(root.ReplaceNode(originalMethodDefinition, methodDefinition)); + // Generating the new document and associated variables. + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + document = document.WithSyntaxRoot(root.ReplaceNode(originalMethodDefinition, methodDefinition)); - var newRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (invocationNameToken != null) - invocationNameToken = newRoot.FindToken(invocationNameToken.Value.SpanStart); - } - - return (document, invocationNameToken); + if (invocationNameToken != null) + invocationNameToken = newRoot.FindToken(invocationNameToken.Value.SpanStart); } + + return (document, invocationNameToken); } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.ExpressionResult.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.ExpressionResult.cs index 4d9805db95920..122e77f172a6c 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.ExpressionResult.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.ExpressionResult.cs @@ -14,135 +14,134 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal partial class CSharpSelectionResult { - internal partial class CSharpSelectionResult + private class ExpressionResult( + TextSpan originalSpan, + TextSpan finalSpan, + ExtractMethodOptions options, + bool selectionInExpression, + SemanticDocument document, + SyntaxAnnotation firstTokenAnnotation, + SyntaxAnnotation lastTokenAnnotation, + bool selectionChanged) : CSharpSelectionResult( + originalSpan, finalSpan, options, selectionInExpression, document, firstTokenAnnotation, lastTokenAnnotation, selectionChanged) { - private class ExpressionResult( - TextSpan originalSpan, - TextSpan finalSpan, - ExtractMethodOptions options, - bool selectionInExpression, - SemanticDocument document, - SyntaxAnnotation firstTokenAnnotation, - SyntaxAnnotation lastTokenAnnotation, - bool selectionChanged) : CSharpSelectionResult( - originalSpan, finalSpan, options, selectionInExpression, document, firstTokenAnnotation, lastTokenAnnotation, selectionChanged) + public override bool ContainingScopeHasAsyncKeyword() + => false; + + public override SyntaxNode? GetContainingScope() { - public override bool ContainingScopeHasAsyncKeyword() - => false; + Contract.ThrowIfNull(SemanticDocument); + Contract.ThrowIfFalse(SelectionInExpression); + + var firstToken = GetFirstTokenInSelection(); + var lastToken = GetLastTokenInSelection(); + var scope = firstToken.GetCommonRoot(lastToken).GetAncestorOrThis(); + if (scope == null) + return null; + + return CSharpSyntaxFacts.Instance.GetRootStandaloneExpression(scope); + } - public override SyntaxNode? GetContainingScope() + public override (ITypeSymbol? returnType, bool returnsByRef) GetReturnType() + { + if (GetContainingScope() is not ExpressionSyntax node) { - Contract.ThrowIfNull(SemanticDocument); - Contract.ThrowIfFalse(SelectionInExpression); + throw ExceptionUtilities.Unreachable(); + } - var firstToken = GetFirstTokenInSelection(); - var lastToken = GetLastTokenInSelection(); - var scope = firstToken.GetCommonRoot(lastToken).GetAncestorOrThis(); - if (scope == null) - return null; + var model = SemanticDocument.SemanticModel; - return CSharpSyntaxFacts.Instance.GetRootStandaloneExpression(scope); + // special case for array initializer and explicit cast + if (node.IsArrayInitializer()) + { + var variableDeclExpression = node.GetAncestorOrThis(); + if (variableDeclExpression != null) + return (model.GetTypeInfo(variableDeclExpression.Type).Type, returnsByRef: false); } - public override (ITypeSymbol? returnType, bool returnsByRef) GetReturnType() + if (node.IsExpressionInCast()) { - if (GetContainingScope() is not ExpressionSyntax node) - { - throw ExceptionUtilities.Unreachable(); - } + // bug # 12774 and # 4780 + // if the expression is under cast, we use the heuristic below + // 1. if regular binding returns a meaningful type, we use it as it is + // 2. if it doesn't, even if the cast itself wasn't included in the selection, we will treat it + // as it was in the selection + var (regularType, returnsByRef) = GetRegularExpressionType(model, node); + if (regularType != null) + return (regularType, returnsByRef); + + if (node.Parent is CastExpressionSyntax castExpression) + return (model.GetTypeInfo(castExpression).Type, returnsByRef: false); + } + + return GetRegularExpressionType(model, node); + } + + private static (ITypeSymbol? typeSymbol, bool returnsByRef) GetRegularExpressionType(SemanticModel semanticModel, ExpressionSyntax node) + { + // regular case. always use ConvertedType to get implicit conversion right. + var expression = node.GetUnparenthesizedExpression(); + var returnsByRef = false; + if (expression is RefExpressionSyntax refExpression) + { + expression = refExpression.Expression; + returnsByRef = true; + } - var model = SemanticDocument.SemanticModel; + var typeSymbol = GetRegularExpressionTypeWorker(); + return (typeSymbol, returnsByRef); + + ITypeSymbol? GetRegularExpressionTypeWorker() + { + var info = semanticModel.GetTypeInfo(expression); + var conv = semanticModel.GetConversion(expression); - // special case for array initializer and explicit cast - if (node.IsArrayInitializer()) + if (info.ConvertedType == null || info.ConvertedType.IsErrorType()) { - var variableDeclExpression = node.GetAncestorOrThis(); - if (variableDeclExpression != null) - return (model.GetTypeInfo(variableDeclExpression.Type).Type, returnsByRef: false); + // there is no implicit conversion involved. no need to go further + return info.GetTypeWithAnnotatedNullability(); } - if (node.IsExpressionInCast()) + // always use converted type if method group + if ((!node.IsKind(SyntaxKind.ObjectCreationExpression) && semanticModel.GetMemberGroup(expression).Length > 0) || + IsCoClassImplicitConversion(info, conv, semanticModel.Compilation.CoClassType())) { - // bug # 12774 and # 4780 - // if the expression is under cast, we use the heuristic below - // 1. if regular binding returns a meaningful type, we use it as it is - // 2. if it doesn't, even if the cast itself wasn't included in the selection, we will treat it - // as it was in the selection - var (regularType, returnsByRef) = GetRegularExpressionType(model, node); - if (regularType != null) - return (regularType, returnsByRef); - - if (node.Parent is CastExpressionSyntax castExpression) - return (model.GetTypeInfo(castExpression).Type, returnsByRef: false); + return info.GetConvertedTypeWithAnnotatedNullability(); } - return GetRegularExpressionType(model, node); - } - - private static (ITypeSymbol? typeSymbol, bool returnsByRef) GetRegularExpressionType(SemanticModel semanticModel, ExpressionSyntax node) - { - // regular case. always use ConvertedType to get implicit conversion right. - var expression = node.GetUnparenthesizedExpression(); - var returnsByRef = false; - if (expression is RefExpressionSyntax refExpression) + // check implicit conversion + if (conv.IsImplicit && (conv.IsConstantExpression || conv.IsEnumeration)) { - expression = refExpression.Expression; - returnsByRef = true; + return info.GetConvertedTypeWithAnnotatedNullability(); } - var typeSymbol = GetRegularExpressionTypeWorker(); - return (typeSymbol, returnsByRef); - - ITypeSymbol? GetRegularExpressionTypeWorker() + // use FormattableString if conversion between String and FormattableString + if (info.Type?.SpecialType == SpecialType.System_String && + info.ConvertedType?.IsFormattableStringOrIFormattable() == true) { - var info = semanticModel.GetTypeInfo(expression); - var conv = semanticModel.GetConversion(expression); - - if (info.ConvertedType == null || info.ConvertedType.IsErrorType()) - { - // there is no implicit conversion involved. no need to go further - return info.GetTypeWithAnnotatedNullability(); - } - - // always use converted type if method group - if ((!node.IsKind(SyntaxKind.ObjectCreationExpression) && semanticModel.GetMemberGroup(expression).Length > 0) || - IsCoClassImplicitConversion(info, conv, semanticModel.Compilation.CoClassType())) - { - return info.GetConvertedTypeWithAnnotatedNullability(); - } - - // check implicit conversion - if (conv.IsImplicit && (conv.IsConstantExpression || conv.IsEnumeration)) - { - return info.GetConvertedTypeWithAnnotatedNullability(); - } - - // use FormattableString if conversion between String and FormattableString - if (info.Type?.SpecialType == SpecialType.System_String && - info.ConvertedType?.IsFormattableStringOrIFormattable() == true) - { - return info.GetConvertedTypeWithAnnotatedNullability(); - } - - // always try to use type that is more specific than object type if possible. - return !info.Type.IsObjectType() ? info.GetTypeWithAnnotatedNullability() : info.GetConvertedTypeWithAnnotatedNullability(); + return info.GetConvertedTypeWithAnnotatedNullability(); } + + // always try to use type that is more specific than object type if possible. + return !info.Type.IsObjectType() ? info.GetTypeWithAnnotatedNullability() : info.GetConvertedTypeWithAnnotatedNullability(); } } + } - private static bool IsCoClassImplicitConversion(TypeInfo info, Conversion conversion, ISymbol? coclassSymbol) + private static bool IsCoClassImplicitConversion(TypeInfo info, Conversion conversion, ISymbol? coclassSymbol) + { + if (!conversion.IsImplicit || + info.ConvertedType == null || + info.ConvertedType.TypeKind != TypeKind.Interface) { - if (!conversion.IsImplicit || - info.ConvertedType == null || - info.ConvertedType.TypeKind != TypeKind.Interface) - { - return false; - } - - // let's see whether this interface has coclass attribute - return info.ConvertedType.GetAttributes().Any(static (c, coclassSymbol) => c.AttributeClass?.Equals(coclassSymbol) == true, coclassSymbol); + return false; } + + // let's see whether this interface has coclass attribute + return info.ConvertedType.GetAttributes().Any(static (c, coclassSymbol) => c.AttributeClass?.Equals(coclassSymbol) == true, coclassSymbol); } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.StatementResult.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.StatementResult.cs index 37655df1f8c16..72feb50438609 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.StatementResult.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.StatementResult.cs @@ -11,94 +11,93 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal partial class CSharpSelectionResult { - internal partial class CSharpSelectionResult + private class StatementResult( + TextSpan originalSpan, + TextSpan finalSpan, + ExtractMethodOptions options, + bool selectionInExpression, + SemanticDocument document, + SyntaxAnnotation firstTokenAnnotation, + SyntaxAnnotation lastTokenAnnotation, + bool selectionChanged) : CSharpSelectionResult( + originalSpan, finalSpan, options, selectionInExpression, document, firstTokenAnnotation, lastTokenAnnotation, selectionChanged) { - private class StatementResult( - TextSpan originalSpan, - TextSpan finalSpan, - ExtractMethodOptions options, - bool selectionInExpression, - SemanticDocument document, - SyntaxAnnotation firstTokenAnnotation, - SyntaxAnnotation lastTokenAnnotation, - bool selectionChanged) : CSharpSelectionResult( - originalSpan, finalSpan, options, selectionInExpression, document, firstTokenAnnotation, lastTokenAnnotation, selectionChanged) + public override bool ContainingScopeHasAsyncKeyword() { - public override bool ContainingScopeHasAsyncKeyword() - { - var node = GetContainingScope(); - - return node switch - { - AccessorDeclarationSyntax _ => false, - MethodDeclarationSyntax method => method.Modifiers.Any(SyntaxKind.AsyncKeyword), - ParenthesizedLambdaExpressionSyntax lambda => lambda.AsyncKeyword.Kind() == SyntaxKind.AsyncKeyword, - SimpleLambdaExpressionSyntax lambda => lambda.AsyncKeyword.Kind() == SyntaxKind.AsyncKeyword, - AnonymousMethodExpressionSyntax anonymous => anonymous.AsyncKeyword.Kind() == SyntaxKind.AsyncKeyword, - _ => false, - }; - } + var node = GetContainingScope(); - public override SyntaxNode GetContainingScope() + return node switch { - Contract.ThrowIfNull(SemanticDocument); - Contract.ThrowIfTrue(SelectionInExpression); + AccessorDeclarationSyntax _ => false, + MethodDeclarationSyntax method => method.Modifiers.Any(SyntaxKind.AsyncKeyword), + ParenthesizedLambdaExpressionSyntax lambda => lambda.AsyncKeyword.Kind() == SyntaxKind.AsyncKeyword, + SimpleLambdaExpressionSyntax lambda => lambda.AsyncKeyword.Kind() == SyntaxKind.AsyncKeyword, + AnonymousMethodExpressionSyntax anonymous => anonymous.AsyncKeyword.Kind() == SyntaxKind.AsyncKeyword, + _ => false, + }; + } - // it contains statements - var firstToken = GetFirstTokenInSelection(); - return firstToken.GetAncestors().FirstOrDefault(n => - { - return n is AccessorDeclarationSyntax or - LocalFunctionStatementSyntax or - BaseMethodDeclarationSyntax or - AccessorDeclarationSyntax or - ParenthesizedLambdaExpressionSyntax or - SimpleLambdaExpressionSyntax or - AnonymousMethodExpressionSyntax or - CompilationUnitSyntax; - }); - } + public override SyntaxNode GetContainingScope() + { + Contract.ThrowIfNull(SemanticDocument); + Contract.ThrowIfTrue(SelectionInExpression); - public override (ITypeSymbol returnType, bool returnsByRef) GetReturnType() + // it contains statements + var firstToken = GetFirstTokenInSelection(); + return firstToken.GetAncestors().FirstOrDefault(n => { - Contract.ThrowIfTrue(SelectionInExpression); + return n is AccessorDeclarationSyntax or + LocalFunctionStatementSyntax or + BaseMethodDeclarationSyntax or + AccessorDeclarationSyntax or + ParenthesizedLambdaExpressionSyntax or + SimpleLambdaExpressionSyntax or + AnonymousMethodExpressionSyntax or + CompilationUnitSyntax; + }); + } + + public override (ITypeSymbol returnType, bool returnsByRef) GetReturnType() + { + Contract.ThrowIfTrue(SelectionInExpression); - var node = GetContainingScope(); - var semanticModel = SemanticDocument.SemanticModel; + var node = GetContainingScope(); + var semanticModel = SemanticDocument.SemanticModel; - switch (node) - { - case AccessorDeclarationSyntax access: - // property or event case - if (access.Parent == null || access.Parent.Parent == null) - return default; + switch (node) + { + case AccessorDeclarationSyntax access: + // property or event case + if (access.Parent == null || access.Parent.Parent == null) + return default; - return semanticModel.GetDeclaredSymbol(access.Parent.Parent) switch - { - IPropertySymbol propertySymbol => (propertySymbol.Type, propertySymbol.ReturnsByRef), - IEventSymbol eventSymbol => (eventSymbol.Type, false), - _ => default, - }; + return semanticModel.GetDeclaredSymbol(access.Parent.Parent) switch + { + IPropertySymbol propertySymbol => (propertySymbol.Type, propertySymbol.ReturnsByRef), + IEventSymbol eventSymbol => (eventSymbol.Type, false), + _ => default, + }; - case MethodDeclarationSyntax methodDeclaration: - { - return semanticModel.GetDeclaredSymbol(methodDeclaration) is not IMethodSymbol method - ? default - : (method.ReturnType, method.ReturnsByRef); - } + case MethodDeclarationSyntax methodDeclaration: + { + return semanticModel.GetDeclaredSymbol(methodDeclaration) is not IMethodSymbol method + ? default + : (method.ReturnType, method.ReturnsByRef); + } - case AnonymousFunctionExpressionSyntax function: - { - return semanticModel.GetSymbolInfo(function).Symbol is not IMethodSymbol method - ? default - : (method.ReturnType, method.ReturnsByRef); - } + case AnonymousFunctionExpressionSyntax function: + { + return semanticModel.GetSymbolInfo(function).Symbol is not IMethodSymbol method + ? default + : (method.ReturnType, method.ReturnsByRef); + } - default: - return default; - } + default: + return default; } } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.cs index acd3b853d7c25..ab6d17cf184df 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionResult.cs @@ -18,234 +18,233 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal abstract partial class CSharpSelectionResult : SelectionResult { - internal abstract partial class CSharpSelectionResult : SelectionResult + public static async Task CreateAsync( + TextSpan originalSpan, + TextSpan finalSpan, + ExtractMethodOptions options, + bool selectionInExpression, + SemanticDocument document, + SyntaxToken firstToken, + SyntaxToken lastToken, + bool selectionChanged, + CancellationToken cancellationToken) { - public static async Task CreateAsync( - TextSpan originalSpan, - TextSpan finalSpan, - ExtractMethodOptions options, - bool selectionInExpression, - SemanticDocument document, - SyntaxToken firstToken, - SyntaxToken lastToken, - bool selectionChanged, - CancellationToken cancellationToken) - { - Contract.ThrowIfNull(document); - - var firstTokenAnnotation = new SyntaxAnnotation(); - var lastTokenAnnotation = new SyntaxAnnotation(); + Contract.ThrowIfNull(document); - var root = await document.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newDocument = await SemanticDocument.CreateAsync(document.Document.WithSyntaxRoot(AddAnnotations( - root, - new[] - { - (firstToken, firstTokenAnnotation), - (lastToken, lastTokenAnnotation) - })), cancellationToken).ConfigureAwait(false); + var firstTokenAnnotation = new SyntaxAnnotation(); + var lastTokenAnnotation = new SyntaxAnnotation(); - if (selectionInExpression) - { - return new ExpressionResult( - originalSpan, finalSpan, options, selectionInExpression, - newDocument, firstTokenAnnotation, lastTokenAnnotation, selectionChanged); - } - else + var root = await document.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newDocument = await SemanticDocument.CreateAsync(document.Document.WithSyntaxRoot(AddAnnotations( + root, + new[] { - return new StatementResult( - originalSpan, finalSpan, options, selectionInExpression, - newDocument, firstTokenAnnotation, lastTokenAnnotation, selectionChanged); - } - } + (firstToken, firstTokenAnnotation), + (lastToken, lastTokenAnnotation) + })), cancellationToken).ConfigureAwait(false); - protected CSharpSelectionResult( - TextSpan originalSpan, - TextSpan finalSpan, - ExtractMethodOptions options, - bool selectionInExpression, - SemanticDocument document, - SyntaxAnnotation firstTokenAnnotation, - SyntaxAnnotation lastTokenAnnotation, - bool selectionChanged) - : base(originalSpan, finalSpan, options, selectionInExpression, - document, firstTokenAnnotation, lastTokenAnnotation, selectionChanged) + if (selectionInExpression) { + return new ExpressionResult( + originalSpan, finalSpan, options, selectionInExpression, + newDocument, firstTokenAnnotation, lastTokenAnnotation, selectionChanged); } - - protected override ISyntaxFacts SyntaxFacts - => CSharpSyntaxFacts.Instance; - - public override SyntaxNode GetNodeForDataFlowAnalysis() + else { - var node = base.GetNodeForDataFlowAnalysis(); - - // If we're returning a value by ref we actually want to do the analysis on the underlying expression. - return node is RefExpressionSyntax refExpression - ? refExpression.Expression - : node; + return new StatementResult( + originalSpan, finalSpan, options, selectionInExpression, + newDocument, firstTokenAnnotation, lastTokenAnnotation, selectionChanged); } + } - protected override bool UnderAnonymousOrLocalMethod(SyntaxToken token, SyntaxToken firstToken, SyntaxToken lastToken) - { - for (var current = token.Parent; current != null; current = current.Parent) - { - if (current is MemberDeclarationSyntax) - return false; - - if (current is - SimpleLambdaExpressionSyntax or - ParenthesizedLambdaExpressionSyntax or - AnonymousMethodExpressionSyntax or - LocalFunctionStatementSyntax) - { - // make sure the selection contains the lambda - return firstToken.SpanStart <= current.GetFirstToken().SpanStart && - current.GetLastToken().Span.End <= lastToken.Span.End; - } - } + protected CSharpSelectionResult( + TextSpan originalSpan, + TextSpan finalSpan, + ExtractMethodOptions options, + bool selectionInExpression, + SemanticDocument document, + SyntaxAnnotation firstTokenAnnotation, + SyntaxAnnotation lastTokenAnnotation, + bool selectionChanged) + : base(originalSpan, finalSpan, options, selectionInExpression, + document, firstTokenAnnotation, lastTokenAnnotation, selectionChanged) + { + } - return false; - } + protected override ISyntaxFacts SyntaxFacts + => CSharpSyntaxFacts.Instance; - public override SyntaxNode GetOutermostCallSiteContainerToProcess(CancellationToken cancellationToken) - { - if (this.SelectionInExpression) - { - var container = this.GetInnermostStatementContainer(); + public override SyntaxNode GetNodeForDataFlowAnalysis() + { + var node = base.GetNodeForDataFlowAnalysis(); - Contract.ThrowIfNull(container); - Contract.ThrowIfFalse(container.IsStatementContainerNode() || - container is TypeDeclarationSyntax || - container is ConstructorDeclarationSyntax || - container is CompilationUnitSyntax); + // If we're returning a value by ref we actually want to do the analysis on the underlying expression. + return node is RefExpressionSyntax refExpression + ? refExpression.Expression + : node; + } - return container; - } + protected override bool UnderAnonymousOrLocalMethod(SyntaxToken token, SyntaxToken firstToken, SyntaxToken lastToken) + { + for (var current = token.Parent; current != null; current = current.Parent) + { + if (current is MemberDeclarationSyntax) + return false; - if (this.IsExtractMethodOnSingleStatement()) + if (current is + SimpleLambdaExpressionSyntax or + ParenthesizedLambdaExpressionSyntax or + AnonymousMethodExpressionSyntax or + LocalFunctionStatementSyntax) { - var firstStatement = this.GetFirstStatement(); - return firstStatement.Parent; + // make sure the selection contains the lambda + return firstToken.SpanStart <= current.GetFirstToken().SpanStart && + current.GetLastToken().Span.End <= lastToken.Span.End; } + } - if (this.IsExtractMethodOnMultipleStatements()) - { - var firstStatement = this.GetFirstStatementUnderContainer(); - var container = firstStatement.Parent; - if (container is GlobalStatementSyntax) - return container.Parent; + return false; + } - return container; - } + public override SyntaxNode GetOutermostCallSiteContainerToProcess(CancellationToken cancellationToken) + { + if (this.SelectionInExpression) + { + var container = this.GetInnermostStatementContainer(); - throw ExceptionUtilities.Unreachable(); + Contract.ThrowIfNull(container); + Contract.ThrowIfFalse(container.IsStatementContainerNode() || + container is TypeDeclarationSyntax || + container is ConstructorDeclarationSyntax || + container is CompilationUnitSyntax); + + return container; } - public override StatementSyntax GetFirstStatementUnderContainer() + if (this.IsExtractMethodOnSingleStatement()) { - Contract.ThrowIfTrue(SelectionInExpression); + var firstStatement = this.GetFirstStatement(); + return firstStatement.Parent; + } - var firstToken = GetFirstTokenInSelection(); - var statement = firstToken.Parent.GetStatementUnderContainer(); - Contract.ThrowIfNull(statement); + if (this.IsExtractMethodOnMultipleStatements()) + { + var firstStatement = this.GetFirstStatementUnderContainer(); + var container = firstStatement.Parent; + if (container is GlobalStatementSyntax) + return container.Parent; - return statement; + return container; } - public override StatementSyntax GetLastStatementUnderContainer() - { - Contract.ThrowIfTrue(SelectionInExpression); + throw ExceptionUtilities.Unreachable(); + } - var lastToken = GetLastTokenInSelection(); - var statement = lastToken.Parent.GetStatementUnderContainer(); + public override StatementSyntax GetFirstStatementUnderContainer() + { + Contract.ThrowIfTrue(SelectionInExpression); - Contract.ThrowIfNull(statement); - var firstStatementUnderContainer = GetFirstStatementUnderContainer(); - Contract.ThrowIfFalse(CSharpSyntaxFacts.Instance.AreStatementsInSameContainer(statement, firstStatementUnderContainer)); + var firstToken = GetFirstTokenInSelection(); + var statement = firstToken.Parent.GetStatementUnderContainer(); + Contract.ThrowIfNull(statement); - return statement; - } + return statement; + } - public SyntaxNode GetInnermostStatementContainer() - { - Contract.ThrowIfFalse(SelectionInExpression); - var containingScope = GetContainingScope(); - var statements = containingScope.GetAncestorsOrThis(); - StatementSyntax last = null; + public override StatementSyntax GetLastStatementUnderContainer() + { + Contract.ThrowIfTrue(SelectionInExpression); - foreach (var statement in statements) - { - if (statement.IsStatementContainerNode()) - { - return statement; - } + var lastToken = GetLastTokenInSelection(); + var statement = lastToken.Parent.GetStatementUnderContainer(); - last = statement; - } + Contract.ThrowIfNull(statement); + var firstStatementUnderContainer = GetFirstStatementUnderContainer(); + Contract.ThrowIfFalse(CSharpSyntaxFacts.Instance.AreStatementsInSameContainer(statement, firstStatementUnderContainer)); - // expression bodied member case - var expressionBodiedMember = GetContainingScopeOf(); - if (expressionBodiedMember != null) - { - // the class/struct declaration is the innermost statement container, since the - // member does not have a block body - return GetContainingScopeOf(); - } + return statement; + } - // constructor initializer case - var constructorInitializer = GetContainingScopeOf(); - if (constructorInitializer != null) - { - return constructorInitializer.Parent; - } + public SyntaxNode GetInnermostStatementContainer() + { + Contract.ThrowIfFalse(SelectionInExpression); + var containingScope = GetContainingScope(); + var statements = containingScope.GetAncestorsOrThis(); + StatementSyntax last = null; - // field initializer case - var field = GetContainingScopeOf(); - if (field != null) + foreach (var statement in statements) + { + if (statement.IsStatementContainerNode()) { - return field.Parent; + return statement; } - Contract.ThrowIfFalse(last.IsParentKind(SyntaxKind.GlobalStatement)); - Contract.ThrowIfFalse(last.Parent.IsParentKind(SyntaxKind.CompilationUnit)); - return last.Parent.Parent; + last = statement; } - public bool ShouldPutUnsafeModifier() + // expression bodied member case + var expressionBodiedMember = GetContainingScopeOf(); + if (expressionBodiedMember != null) { - var token = GetFirstTokenInSelection(); - var ancestors = token.GetAncestors(); + // the class/struct declaration is the innermost statement container, since the + // member does not have a block body + return GetContainingScopeOf(); + } - // if enclosing type contains unsafe keyword, we don't need to put it again - if (ancestors.Where(a => CSharp.SyntaxFacts.IsTypeDeclaration(a.Kind())) - .Cast() - .Any(m => m.GetModifiers().Any(SyntaxKind.UnsafeKeyword))) - { - return false; - } + // constructor initializer case + var constructorInitializer = GetContainingScopeOf(); + if (constructorInitializer != null) + { + return constructorInitializer.Parent; + } - return token.Parent.IsUnsafeContext(); + // field initializer case + var field = GetContainingScopeOf(); + if (field != null) + { + return field.Parent; } - public SyntaxKind UnderCheckedExpressionContext() - => UnderCheckedContext(); + Contract.ThrowIfFalse(last.IsParentKind(SyntaxKind.GlobalStatement)); + Contract.ThrowIfFalse(last.Parent.IsParentKind(SyntaxKind.CompilationUnit)); + return last.Parent.Parent; + } - public SyntaxKind UnderCheckedStatementContext() - => UnderCheckedContext(); + public bool ShouldPutUnsafeModifier() + { + var token = GetFirstTokenInSelection(); + var ancestors = token.GetAncestors(); - private SyntaxKind UnderCheckedContext() where T : SyntaxNode + // if enclosing type contains unsafe keyword, we don't need to put it again + if (ancestors.Where(a => CSharp.SyntaxFacts.IsTypeDeclaration(a.Kind())) + .Cast() + .Any(m => m.GetModifiers().Any(SyntaxKind.UnsafeKeyword))) { - var token = GetFirstTokenInSelection(); - var contextNode = token.Parent.GetAncestor(); - if (contextNode == null) - { - return SyntaxKind.None; - } + return false; + } + + return token.Parent.IsUnsafeContext(); + } + + public SyntaxKind UnderCheckedExpressionContext() + => UnderCheckedContext(); - return contextNode.Kind(); + public SyntaxKind UnderCheckedStatementContext() + => UnderCheckedContext(); + + private SyntaxKind UnderCheckedContext() where T : SyntaxNode + { + var token = GetFirstTokenInSelection(); + var contextNode = token.Parent.GetAncestor(); + if (contextNode == null) + { + return SyntaxKind.None; } + + return contextNode.Kind(); } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionValidator.Validator.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionValidator.Validator.cs index 39030c5672ecb..8356d2c27b00a 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionValidator.Validator.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionValidator.Validator.cs @@ -8,68 +8,67 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod -{ - internal partial class CSharpSelectionValidator - { - public static bool Check(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) - => node switch - { - ExpressionSyntax expression => CheckExpression(semanticModel, expression, cancellationToken), - BlockSyntax block => CheckBlock(block), - StatementSyntax statement => CheckStatement(statement), - GlobalStatementSyntax _ => CheckGlobalStatement(), - _ => false, - }; - - private static bool CheckGlobalStatement() - => true; +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; - private static bool CheckBlock(BlockSyntax block) - { - // TODO(cyrusn): Is it intentional that fixed statement is not in this list? - return block.Parent is BlockSyntax or - DoStatementSyntax or - ElseClauseSyntax or - CommonForEachStatementSyntax or - ForStatementSyntax or - IfStatementSyntax or - LockStatementSyntax or - UsingStatementSyntax or - WhileStatementSyntax; - } - - private static bool CheckExpression(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) +internal partial class CSharpSelectionValidator +{ + public static bool Check(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => node switch { - cancellationToken.ThrowIfCancellationRequested(); + ExpressionSyntax expression => CheckExpression(semanticModel, expression, cancellationToken), + BlockSyntax block => CheckBlock(block), + StatementSyntax statement => CheckStatement(statement), + GlobalStatementSyntax _ => CheckGlobalStatement(), + _ => false, + }; - // TODO(cyrusn): This is probably unnecessary. What we should be doing is binding - // the type of the expression and seeing if it contains an anonymous type. - if (expression is AnonymousObjectCreationExpressionSyntax) - { - return false; - } - - return expression.CanReplaceWithRValue(semanticModel, cancellationToken); - } + private static bool CheckGlobalStatement() + => true; - private static bool CheckStatement(StatementSyntax statement) - => statement is CheckedStatementSyntax or + private static bool CheckBlock(BlockSyntax block) + { + // TODO(cyrusn): Is it intentional that fixed statement is not in this list? + return block.Parent is BlockSyntax or DoStatementSyntax or - EmptyStatementSyntax or - ExpressionStatementSyntax or - FixedStatementSyntax or + ElseClauseSyntax or CommonForEachStatementSyntax or ForStatementSyntax or IfStatementSyntax or - LocalDeclarationStatementSyntax or LockStatementSyntax or - ReturnStatementSyntax or - SwitchStatementSyntax or - ThrowStatementSyntax or - TryStatementSyntax or - UnsafeStatementSyntax or UsingStatementSyntax or WhileStatementSyntax; } + + private static bool CheckExpression(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + // TODO(cyrusn): This is probably unnecessary. What we should be doing is binding + // the type of the expression and seeing if it contains an anonymous type. + if (expression is AnonymousObjectCreationExpressionSyntax) + { + return false; + } + + return expression.CanReplaceWithRValue(semanticModel, cancellationToken); + } + + private static bool CheckStatement(StatementSyntax statement) + => statement is CheckedStatementSyntax or + DoStatementSyntax or + EmptyStatementSyntax or + ExpressionStatementSyntax or + FixedStatementSyntax or + CommonForEachStatementSyntax or + ForStatementSyntax or + IfStatementSyntax or + LocalDeclarationStatementSyntax or + LockStatementSyntax or + ReturnStatementSyntax or + SwitchStatementSyntax or + ThrowStatementSyntax or + TryStatementSyntax or + UnsafeStatementSyntax or + UsingStatementSyntax or + WhileStatementSyntax; } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionValidator.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionValidator.cs index 3903388ff5b04..c27ebc93ca693 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionValidator.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionValidator.cs @@ -18,533 +18,532 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal partial class CSharpSelectionValidator( + SemanticDocument document, + TextSpan textSpan, + ExtractMethodOptions options, + bool localFunction) : SelectionValidator(document, textSpan, options) { - internal partial class CSharpSelectionValidator( - SemanticDocument document, - TextSpan textSpan, - ExtractMethodOptions options, - bool localFunction) : SelectionValidator(document, textSpan, options) - { - private readonly bool _localFunction = localFunction; + private readonly bool _localFunction = localFunction; - public override async Task<(CSharpSelectionResult, OperationStatus)> GetValidSelectionAsync(CancellationToken cancellationToken) + public override async Task<(CSharpSelectionResult, OperationStatus)> GetValidSelectionAsync(CancellationToken cancellationToken) + { + if (!ContainsValidSelection) + return (null, OperationStatus.FailedWithUnknownReason); + + var text = SemanticDocument.Text; + var root = SemanticDocument.Root; + var model = SemanticDocument.SemanticModel; + var doc = SemanticDocument; + + // go through pipe line and calculate information about the user selection + var selectionInfo = GetInitialSelectionInfo(root, text); + selectionInfo = AssignInitialFinalTokens(selectionInfo, root, cancellationToken); + selectionInfo = AdjustFinalTokensBasedOnContext(selectionInfo, model, cancellationToken); + selectionInfo = AssignFinalSpan(selectionInfo, text); + selectionInfo = ApplySpecialCases(selectionInfo, text, SemanticDocument.SyntaxTree.Options, _localFunction); + selectionInfo = CheckErrorCasesAndAppendDescriptions(selectionInfo, root); + + // there was a fatal error that we couldn't even do negative preview, return error result + if (selectionInfo.Status.Failed) + return (null, selectionInfo.Status); + + var controlFlowSpan = GetControlFlowSpan(selectionInfo); + if (!selectionInfo.SelectionInExpression) { - if (!ContainsValidSelection) - return (null, OperationStatus.FailedWithUnknownReason); + var statementRange = GetStatementRangeContainedInSpan(root, controlFlowSpan, cancellationToken); + if (statementRange == null) + { + selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Cannot_determine_valid_range_of_statements_to_extract)); + return (null, selectionInfo.Status); + } - var text = SemanticDocument.Text; - var root = SemanticDocument.Root; - var model = SemanticDocument.SemanticModel; - var doc = SemanticDocument; + var isFinalSpanSemanticallyValid = IsFinalSpanSemanticallyValidSpan(model, controlFlowSpan, statementRange.Value, cancellationToken); + if (!isFinalSpanSemanticallyValid) + { + // check control flow only if we are extracting statement level, not expression + // level. you can not have goto that moves control out of scope in expression level + // (even in lambda) + selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: true, CSharpFeaturesResources.Not_all_code_paths_return)); + } + } - // go through pipe line and calculate information about the user selection - var selectionInfo = GetInitialSelectionInfo(root, text); - selectionInfo = AssignInitialFinalTokens(selectionInfo, root, cancellationToken); - selectionInfo = AdjustFinalTokensBasedOnContext(selectionInfo, model, cancellationToken); - selectionInfo = AssignFinalSpan(selectionInfo, text); - selectionInfo = ApplySpecialCases(selectionInfo, text, SemanticDocument.SyntaxTree.Options, _localFunction); - selectionInfo = CheckErrorCasesAndAppendDescriptions(selectionInfo, root); + var selectionChanged = selectionInfo.FirstTokenInOriginalSpan != selectionInfo.FirstTokenInFinalSpan || selectionInfo.LastTokenInOriginalSpan != selectionInfo.LastTokenInFinalSpan; + + var result = await CSharpSelectionResult.CreateAsync( + selectionInfo.OriginalSpan, + selectionInfo.FinalSpan, + Options, + selectionInfo.SelectionInExpression, + doc, + selectionInfo.FirstTokenInFinalSpan, + selectionInfo.LastTokenInFinalSpan, + selectionChanged, + cancellationToken).ConfigureAwait(false); + return (result, selectionInfo.Status); + } - // there was a fatal error that we couldn't even do negative preview, return error result - if (selectionInfo.Status.Failed) - return (null, selectionInfo.Status); + private SelectionInfo ApplySpecialCases(SelectionInfo selectionInfo, SourceText text, ParseOptions options, bool localFunction) + { + if (selectionInfo.Status.Failed) + return selectionInfo; - var controlFlowSpan = GetControlFlowSpan(selectionInfo); - if (!selectionInfo.SelectionInExpression) + if (selectionInfo.CommonRootFromOriginalSpan.IsKind(SyntaxKind.CompilationUnit) + || selectionInfo.CommonRootFromOriginalSpan.IsParentKind(SyntaxKind.GlobalStatement)) + { + // Cannot extract a local function from a global statement in script code + if (localFunction && options is { Kind: SourceCodeKind.Script }) { - var statementRange = GetStatementRangeContainedInSpan(root, controlFlowSpan, cancellationToken); - if (statementRange == null) - { - selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Cannot_determine_valid_range_of_statements_to_extract)); - return (null, selectionInfo.Status); - } - - var isFinalSpanSemanticallyValid = IsFinalSpanSemanticallyValidSpan(model, controlFlowSpan, statementRange.Value, cancellationToken); - if (!isFinalSpanSemanticallyValid) - { - // check control flow only if we are extracting statement level, not expression - // level. you can not have goto that moves control out of scope in expression level - // (even in lambda) - selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: true, CSharpFeaturesResources.Not_all_code_paths_return)); - } + return selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_cannot_include_global_statements)); } - var selectionChanged = selectionInfo.FirstTokenInOriginalSpan != selectionInfo.FirstTokenInFinalSpan || selectionInfo.LastTokenInOriginalSpan != selectionInfo.LastTokenInFinalSpan; - - var result = await CSharpSelectionResult.CreateAsync( - selectionInfo.OriginalSpan, - selectionInfo.FinalSpan, - Options, - selectionInfo.SelectionInExpression, - doc, - selectionInfo.FirstTokenInFinalSpan, - selectionInfo.LastTokenInFinalSpan, - selectionChanged, - cancellationToken).ConfigureAwait(false); - return (result, selectionInfo.Status); + // Cannot extract a method from a top-level statement in normal code + if (!localFunction && options is { Kind: SourceCodeKind.Regular }) + { + return selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_cannot_include_top_level_statements)); + } } - private SelectionInfo ApplySpecialCases(SelectionInfo selectionInfo, SourceText text, ParseOptions options, bool localFunction) + if (_localFunction) { - if (selectionInfo.Status.Failed) - return selectionInfo; - - if (selectionInfo.CommonRootFromOriginalSpan.IsKind(SyntaxKind.CompilationUnit) - || selectionInfo.CommonRootFromOriginalSpan.IsParentKind(SyntaxKind.GlobalStatement)) + foreach (var ancestor in selectionInfo.CommonRootFromOriginalSpan.AncestorsAndSelf()) { - // Cannot extract a local function from a global statement in script code - if (localFunction && options is { Kind: SourceCodeKind.Script }) + if (ancestor.Kind() is SyntaxKind.BaseConstructorInitializer or SyntaxKind.ThisConstructorInitializer) { - return selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_cannot_include_global_statements)); + return selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_cannot_be_in_constructor_initializer)); } - // Cannot extract a method from a top-level statement in normal code - if (!localFunction && options is { Kind: SourceCodeKind.Regular }) + if (ancestor is AnonymousFunctionExpressionSyntax) { - return selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_cannot_include_top_level_statements)); + break; } } + } - if (_localFunction) - { - foreach (var ancestor in selectionInfo.CommonRootFromOriginalSpan.AncestorsAndSelf()) - { - if (ancestor.Kind() is SyntaxKind.BaseConstructorInitializer or SyntaxKind.ThisConstructorInitializer) - { - return selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_cannot_be_in_constructor_initializer)); - } + if (!selectionInfo.SelectionInExpression) + { + return selectionInfo; + } - if (ancestor is AnonymousFunctionExpressionSyntax) - { - break; - } - } - } + var expressionNode = selectionInfo.FirstTokenInFinalSpan.GetCommonRoot(selectionInfo.LastTokenInFinalSpan); + if (expressionNode is not AssignmentExpressionSyntax assign) + return selectionInfo; - if (!selectionInfo.SelectionInExpression) - { - return selectionInfo; - } + // make sure there is a visible token at right side expression + if (assign.Right.GetLastToken().Kind() == SyntaxKind.None) + { + return selectionInfo; + } - var expressionNode = selectionInfo.FirstTokenInFinalSpan.GetCommonRoot(selectionInfo.LastTokenInFinalSpan); - if (expressionNode is not AssignmentExpressionSyntax assign) - return selectionInfo; + return AssignFinalSpan(selectionInfo.With(s => s.FirstTokenInFinalSpan = assign.Right.GetFirstToken(includeZeroWidth: true)) + .With(s => s.LastTokenInFinalSpan = assign.Right.GetLastToken(includeZeroWidth: true)), + text); + } - // make sure there is a visible token at right side expression - if (assign.Right.GetLastToken().Kind() == SyntaxKind.None) - { - return selectionInfo; - } + private static TextSpan GetControlFlowSpan(SelectionInfo selectionInfo) + => TextSpan.FromBounds(selectionInfo.FirstTokenInFinalSpan.SpanStart, selectionInfo.LastTokenInFinalSpan.Span.End); + + private static SelectionInfo AdjustFinalTokensBasedOnContext( + SelectionInfo selectionInfo, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + if (selectionInfo.Status.Failed) + return selectionInfo; - return AssignFinalSpan(selectionInfo.With(s => s.FirstTokenInFinalSpan = assign.Right.GetFirstToken(includeZeroWidth: true)) - .With(s => s.LastTokenInFinalSpan = assign.Right.GetLastToken(includeZeroWidth: true)), - text); + // don't need to adjust anything if it is multi-statements case + if (!selectionInfo.SelectionInExpression && !selectionInfo.SelectionInSingleStatement) + { + return selectionInfo; } - private static TextSpan GetControlFlowSpan(SelectionInfo selectionInfo) - => TextSpan.FromBounds(selectionInfo.FirstTokenInFinalSpan.SpanStart, selectionInfo.LastTokenInFinalSpan.Span.End); + // get the node that covers the selection + var node = selectionInfo.FirstTokenInFinalSpan.GetCommonRoot(selectionInfo.LastTokenInFinalSpan); - private static SelectionInfo AdjustFinalTokensBasedOnContext( - SelectionInfo selectionInfo, - SemanticModel semanticModel, - CancellationToken cancellationToken) + var validNode = Check(semanticModel, node, cancellationToken); + if (validNode) { - if (selectionInfo.Status.Failed) - return selectionInfo; + return selectionInfo; + } - // don't need to adjust anything if it is multi-statements case - if (!selectionInfo.SelectionInExpression && !selectionInfo.SelectionInSingleStatement) - { - return selectionInfo; - } + var firstValidNode = node.GetAncestors().FirstOrDefault(n => Check(semanticModel, n, cancellationToken)); + if (firstValidNode == null) + { + // couldn't find any valid node + return selectionInfo.WithStatus(s => new OperationStatus(succeeded: false, CSharpFeaturesResources.Selection_does_not_contain_a_valid_node)) + .With(s => s.FirstTokenInFinalSpan = default) + .With(s => s.LastTokenInFinalSpan = default); + } - // get the node that covers the selection - var node = selectionInfo.FirstTokenInFinalSpan.GetCommonRoot(selectionInfo.LastTokenInFinalSpan); + firstValidNode = (firstValidNode.Parent is ExpressionStatementSyntax) ? firstValidNode.Parent : firstValidNode; - var validNode = Check(semanticModel, node, cancellationToken); - if (validNode) - { - return selectionInfo; - } + return selectionInfo.With(s => s.SelectionInExpression = firstValidNode is ExpressionSyntax) + .With(s => s.SelectionInSingleStatement = firstValidNode is StatementSyntax) + .With(s => s.FirstTokenInFinalSpan = firstValidNode.GetFirstToken(includeZeroWidth: true)) + .With(s => s.LastTokenInFinalSpan = firstValidNode.GetLastToken(includeZeroWidth: true)); + } - var firstValidNode = node.GetAncestors().FirstOrDefault(n => Check(semanticModel, n, cancellationToken)); - if (firstValidNode == null) - { - // couldn't find any valid node - return selectionInfo.WithStatus(s => new OperationStatus(succeeded: false, CSharpFeaturesResources.Selection_does_not_contain_a_valid_node)) - .With(s => s.FirstTokenInFinalSpan = default) - .With(s => s.LastTokenInFinalSpan = default); - } + private SelectionInfo GetInitialSelectionInfo(SyntaxNode root, SourceText text) + { + var adjustedSpan = GetAdjustedSpan(text, OriginalSpan); - firstValidNode = (firstValidNode.Parent is ExpressionStatementSyntax) ? firstValidNode.Parent : firstValidNode; + var firstTokenInSelection = root.FindTokenOnRightOfPosition(adjustedSpan.Start, includeSkipped: false); + var lastTokenInSelection = root.FindTokenOnLeftOfPosition(adjustedSpan.End, includeSkipped: false); - return selectionInfo.With(s => s.SelectionInExpression = firstValidNode is ExpressionSyntax) - .With(s => s.SelectionInSingleStatement = firstValidNode is StatementSyntax) - .With(s => s.FirstTokenInFinalSpan = firstValidNode.GetFirstToken(includeZeroWidth: true)) - .With(s => s.LastTokenInFinalSpan = firstValidNode.GetLastToken(includeZeroWidth: true)); + if (firstTokenInSelection.Kind() == SyntaxKind.None || lastTokenInSelection.Kind() == SyntaxKind.None) + { + return new SelectionInfo { Status = new OperationStatus(succeeded: false, FeaturesResources.Invalid_selection), OriginalSpan = adjustedSpan }; } - private SelectionInfo GetInitialSelectionInfo(SyntaxNode root, SourceText text) + if (!adjustedSpan.Contains(firstTokenInSelection.Span) && !adjustedSpan.Contains(lastTokenInSelection.Span)) { - var adjustedSpan = GetAdjustedSpan(text, OriginalSpan); - - var firstTokenInSelection = root.FindTokenOnRightOfPosition(adjustedSpan.Start, includeSkipped: false); - var lastTokenInSelection = root.FindTokenOnLeftOfPosition(adjustedSpan.End, includeSkipped: false); - - if (firstTokenInSelection.Kind() == SyntaxKind.None || lastTokenInSelection.Kind() == SyntaxKind.None) - { - return new SelectionInfo { Status = new OperationStatus(succeeded: false, FeaturesResources.Invalid_selection), OriginalSpan = adjustedSpan }; - } - - if (!adjustedSpan.Contains(firstTokenInSelection.Span) && !adjustedSpan.Contains(lastTokenInSelection.Span)) + return new SelectionInfo { - return new SelectionInfo - { - Status = new OperationStatus(succeeded: false, FeaturesResources.Selection_does_not_contain_a_valid_token), - OriginalSpan = adjustedSpan, - FirstTokenInOriginalSpan = firstTokenInSelection, - LastTokenInOriginalSpan = lastTokenInSelection - }; - } + Status = new OperationStatus(succeeded: false, FeaturesResources.Selection_does_not_contain_a_valid_token), + OriginalSpan = adjustedSpan, + FirstTokenInOriginalSpan = firstTokenInSelection, + LastTokenInOriginalSpan = lastTokenInSelection + }; + } - if (!UnderValidContext(firstTokenInSelection) || !UnderValidContext(lastTokenInSelection)) + if (!UnderValidContext(firstTokenInSelection) || !UnderValidContext(lastTokenInSelection)) + { + return new SelectionInfo { - return new SelectionInfo - { - Status = new OperationStatus(succeeded: false, FeaturesResources.No_valid_selection_to_perform_extraction), - OriginalSpan = adjustedSpan, - FirstTokenInOriginalSpan = firstTokenInSelection, - LastTokenInOriginalSpan = lastTokenInSelection - }; - } - - var commonRoot = firstTokenInSelection.GetCommonRoot(lastTokenInSelection); + Status = new OperationStatus(succeeded: false, FeaturesResources.No_valid_selection_to_perform_extraction), + OriginalSpan = adjustedSpan, + FirstTokenInOriginalSpan = firstTokenInSelection, + LastTokenInOriginalSpan = lastTokenInSelection + }; + } - if (commonRoot == null) - { - return new SelectionInfo - { - Status = new OperationStatus(succeeded: false, FeaturesResources.No_common_root_node_for_extraction), - OriginalSpan = adjustedSpan, - FirstTokenInOriginalSpan = firstTokenInSelection, - LastTokenInOriginalSpan = lastTokenInSelection - }; - } + var commonRoot = firstTokenInSelection.GetCommonRoot(lastTokenInSelection); - if (!commonRoot.ContainedInValidType()) + if (commonRoot == null) + { + return new SelectionInfo { - return new SelectionInfo - { - Status = new OperationStatus(succeeded: false, FeaturesResources.Selection_not_contained_inside_a_type), - OriginalSpan = adjustedSpan, - FirstTokenInOriginalSpan = firstTokenInSelection, - LastTokenInOriginalSpan = lastTokenInSelection - }; - } + Status = new OperationStatus(succeeded: false, FeaturesResources.No_common_root_node_for_extraction), + OriginalSpan = adjustedSpan, + FirstTokenInOriginalSpan = firstTokenInSelection, + LastTokenInOriginalSpan = lastTokenInSelection + }; + } - var selectionInExpression = commonRoot is ExpressionSyntax; - if (!selectionInExpression && !commonRoot.UnderValidContext()) + if (!commonRoot.ContainedInValidType()) + { + return new SelectionInfo { - return new SelectionInfo - { - Status = new OperationStatus(succeeded: false, FeaturesResources.No_valid_selection_to_perform_extraction), - OriginalSpan = adjustedSpan, - FirstTokenInOriginalSpan = firstTokenInSelection, - LastTokenInOriginalSpan = lastTokenInSelection - }; - } + Status = new OperationStatus(succeeded: false, FeaturesResources.Selection_not_contained_inside_a_type), + OriginalSpan = adjustedSpan, + FirstTokenInOriginalSpan = firstTokenInSelection, + LastTokenInOriginalSpan = lastTokenInSelection + }; + } + var selectionInExpression = commonRoot is ExpressionSyntax; + if (!selectionInExpression && !commonRoot.UnderValidContext()) + { return new SelectionInfo { - Status = OperationStatus.SucceededStatus, + Status = new OperationStatus(succeeded: false, FeaturesResources.No_valid_selection_to_perform_extraction), OriginalSpan = adjustedSpan, - CommonRootFromOriginalSpan = commonRoot, - SelectionInExpression = selectionInExpression, FirstTokenInOriginalSpan = firstTokenInSelection, LastTokenInOriginalSpan = lastTokenInSelection }; } - private static bool UnderValidContext(SyntaxToken token) - => token.GetAncestors().Any(n => CheckTopLevel(n, token.Span)); + return new SelectionInfo + { + Status = OperationStatus.SucceededStatus, + OriginalSpan = adjustedSpan, + CommonRootFromOriginalSpan = commonRoot, + SelectionInExpression = selectionInExpression, + FirstTokenInOriginalSpan = firstTokenInSelection, + LastTokenInOriginalSpan = lastTokenInSelection + }; + } + + private static bool UnderValidContext(SyntaxToken token) + => token.GetAncestors().Any(n => CheckTopLevel(n, token.Span)); - private static bool CheckTopLevel(SyntaxNode node, TextSpan span) + private static bool CheckTopLevel(SyntaxNode node, TextSpan span) + { + switch (node) { - switch (node) - { - case BlockSyntax block: - return ContainsInBlockBody(block, span); - case ArrowExpressionClauseSyntax expressionBodiedMember: - return ContainsInExpressionBodiedMemberBody(expressionBodiedMember, span); - case FieldDeclarationSyntax field: + case BlockSyntax block: + return ContainsInBlockBody(block, span); + case ArrowExpressionClauseSyntax expressionBodiedMember: + return ContainsInExpressionBodiedMemberBody(expressionBodiedMember, span); + case FieldDeclarationSyntax field: + { + foreach (var variable in field.Declaration.Variables) { - foreach (var variable in field.Declaration.Variables) + if (variable.Initializer != null && variable.Initializer.Span.Contains(span)) { - if (variable.Initializer != null && variable.Initializer.Span.Contains(span)) - { - return true; - } + return true; } - - break; } - case GlobalStatementSyntax _: - return true; - case ConstructorInitializerSyntax constructorInitializer: - return constructorInitializer.ContainsInArgument(span); - } + break; + } - return false; + case GlobalStatementSyntax _: + return true; + case ConstructorInitializerSyntax constructorInitializer: + return constructorInitializer.ContainsInArgument(span); } - private static bool ContainsInBlockBody(BlockSyntax block, TextSpan textSpan) - { - if (block == null) - { - return false; - } + return false; + } - var blockSpan = TextSpan.FromBounds(block.OpenBraceToken.Span.End, block.CloseBraceToken.SpanStart); - return blockSpan.Contains(textSpan); + private static bool ContainsInBlockBody(BlockSyntax block, TextSpan textSpan) + { + if (block == null) + { + return false; } - private static bool ContainsInExpressionBodiedMemberBody(ArrowExpressionClauseSyntax expressionBodiedMember, TextSpan textSpan) - { - if (expressionBodiedMember == null) - { - return false; - } + var blockSpan = TextSpan.FromBounds(block.OpenBraceToken.Span.End, block.CloseBraceToken.SpanStart); + return blockSpan.Contains(textSpan); + } - var expressionBodiedMemberBody = TextSpan.FromBounds(expressionBodiedMember.Expression.SpanStart, expressionBodiedMember.Expression.Span.End); - return expressionBodiedMemberBody.Contains(textSpan); + private static bool ContainsInExpressionBodiedMemberBody(ArrowExpressionClauseSyntax expressionBodiedMember, TextSpan textSpan) + { + if (expressionBodiedMember == null) + { + return false; } - private static SelectionInfo CheckErrorCasesAndAppendDescriptions( - SelectionInfo selectionInfo, - SyntaxNode root) + var expressionBodiedMemberBody = TextSpan.FromBounds(expressionBodiedMember.Expression.SpanStart, expressionBodiedMember.Expression.Span.End); + return expressionBodiedMemberBody.Contains(textSpan); + } + + private static SelectionInfo CheckErrorCasesAndAppendDescriptions( + SelectionInfo selectionInfo, + SyntaxNode root) + { + if (selectionInfo.Status.Failed) + return selectionInfo; + + if (selectionInfo.FirstTokenInFinalSpan.IsMissing || selectionInfo.LastTokenInFinalSpan.IsMissing) { - if (selectionInfo.Status.Failed) - return selectionInfo; + selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Contains_invalid_selection)); + } - if (selectionInfo.FirstTokenInFinalSpan.IsMissing || selectionInfo.LastTokenInFinalSpan.IsMissing) - { - selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Contains_invalid_selection)); - } + // get the node that covers the selection + var commonNode = selectionInfo.FirstTokenInFinalSpan.GetCommonRoot(selectionInfo.LastTokenInFinalSpan); - // get the node that covers the selection - var commonNode = selectionInfo.FirstTokenInFinalSpan.GetCommonRoot(selectionInfo.LastTokenInFinalSpan); + if ((selectionInfo.SelectionInExpression || selectionInfo.SelectionInSingleStatement) && commonNode.HasDiagnostics()) + { + selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.The_selection_contains_syntactic_errors)); + } - if ((selectionInfo.SelectionInExpression || selectionInfo.SelectionInSingleStatement) && commonNode.HasDiagnostics()) - { - selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.The_selection_contains_syntactic_errors)); - } + var tokens = root.DescendantTokens(selectionInfo.FinalSpan); + if (tokens.ContainPreprocessorCrossOver(selectionInfo.FinalSpan)) + { + selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: true, CSharpFeaturesResources.Selection_can_not_cross_over_preprocessor_directives)); + } - var tokens = root.DescendantTokens(selectionInfo.FinalSpan); - if (tokens.ContainPreprocessorCrossOver(selectionInfo.FinalSpan)) - { - selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: true, CSharpFeaturesResources.Selection_can_not_cross_over_preprocessor_directives)); - } + // TODO : check whether this can be handled by control flow analysis engine + if (tokens.Any(t => t.Kind() == SyntaxKind.YieldKeyword)) + { + selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: true, CSharpFeaturesResources.Selection_can_not_contain_a_yield_statement)); + } - // TODO : check whether this can be handled by control flow analysis engine - if (tokens.Any(t => t.Kind() == SyntaxKind.YieldKeyword)) - { - selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: true, CSharpFeaturesResources.Selection_can_not_contain_a_yield_statement)); - } + // TODO : check behavior of control flow analysis engine around exception and exception handling. + if (tokens.ContainArgumentlessThrowWithoutEnclosingCatch(selectionInfo.FinalSpan)) + { + selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: true, CSharpFeaturesResources.Selection_can_not_contain_throw_statement)); + } - // TODO : check behavior of control flow analysis engine around exception and exception handling. - if (tokens.ContainArgumentlessThrowWithoutEnclosingCatch(selectionInfo.FinalSpan)) - { - selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: true, CSharpFeaturesResources.Selection_can_not_contain_throw_statement)); - } + if (selectionInfo.SelectionInExpression && commonNode.PartOfConstantInitializerExpression()) + { + selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_can_not_be_part_of_constant_initializer_expression)); + } - if (selectionInfo.SelectionInExpression && commonNode.PartOfConstantInitializerExpression()) - { - selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_can_not_be_part_of_constant_initializer_expression)); - } + if (commonNode.IsUnsafeContext()) + { + selectionInfo = selectionInfo.WithStatus(s => s.With(s.Succeeded, CSharpFeaturesResources.The_selected_code_is_inside_an_unsafe_context)); + } - if (commonNode.IsUnsafeContext()) - { - selectionInfo = selectionInfo.WithStatus(s => s.With(s.Succeeded, CSharpFeaturesResources.The_selected_code_is_inside_an_unsafe_context)); - } + // For now patterns are being blanket disabled for extract method. This issue covers designing extractions for them + // and re-enabling this. + // https://github.com/dotnet/roslyn/issues/9244 + if (commonNode.Kind() == SyntaxKind.IsPatternExpression) + { + selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_can_not_contain_a_pattern_expression)); + } - // For now patterns are being blanket disabled for extract method. This issue covers designing extractions for them - // and re-enabling this. - // https://github.com/dotnet/roslyn/issues/9244 - if (commonNode.Kind() == SyntaxKind.IsPatternExpression) - { - selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_can_not_contain_a_pattern_expression)); - } + return selectionInfo; + } + private static SelectionInfo AssignInitialFinalTokens(SelectionInfo selectionInfo, SyntaxNode root, CancellationToken cancellationToken) + { + if (selectionInfo.Status.Failed) return selectionInfo; - } - private static SelectionInfo AssignInitialFinalTokens(SelectionInfo selectionInfo, SyntaxNode root, CancellationToken cancellationToken) + if (selectionInfo.SelectionInExpression) { - if (selectionInfo.Status.Failed) - return selectionInfo; - - if (selectionInfo.SelectionInExpression) - { - // simple expression case - return selectionInfo.With(s => s.FirstTokenInFinalSpan = s.CommonRootFromOriginalSpan.GetFirstToken(includeZeroWidth: true)) - .With(s => s.LastTokenInFinalSpan = s.CommonRootFromOriginalSpan.GetLastToken(includeZeroWidth: true)); - } + // simple expression case + return selectionInfo.With(s => s.FirstTokenInFinalSpan = s.CommonRootFromOriginalSpan.GetFirstToken(includeZeroWidth: true)) + .With(s => s.LastTokenInFinalSpan = s.CommonRootFromOriginalSpan.GetLastToken(includeZeroWidth: true)); + } - var range = GetStatementRangeContainingSpan( - CSharpSyntaxFacts.Instance, - root, TextSpan.FromBounds(selectionInfo.FirstTokenInOriginalSpan.SpanStart, selectionInfo.LastTokenInOriginalSpan.Span.End), - cancellationToken); + var range = GetStatementRangeContainingSpan( + CSharpSyntaxFacts.Instance, + root, TextSpan.FromBounds(selectionInfo.FirstTokenInOriginalSpan.SpanStart, selectionInfo.LastTokenInOriginalSpan.Span.End), + cancellationToken); - if (range == null) - { - return selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.No_valid_statement_range_to_extract)); - } + if (range == null) + { + return selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.No_valid_statement_range_to_extract)); + } - var statement1 = range.Value.Item1; - var statement2 = range.Value.Item2; + var statement1 = range.Value.Item1; + var statement2 = range.Value.Item2; - if (statement1 == statement2) + if (statement1 == statement2) + { + // check one more time to see whether it is an expression case + var expression = selectionInfo.CommonRootFromOriginalSpan.GetAncestor(); + if (expression != null && statement1.Span.Contains(expression.Span)) { - // check one more time to see whether it is an expression case - var expression = selectionInfo.CommonRootFromOriginalSpan.GetAncestor(); - if (expression != null && statement1.Span.Contains(expression.Span)) - { - return selectionInfo.With(s => s.SelectionInExpression = true) - .With(s => s.FirstTokenInFinalSpan = expression.GetFirstToken(includeZeroWidth: true)) - .With(s => s.LastTokenInFinalSpan = expression.GetLastToken(includeZeroWidth: true)); - } - - // single statement case - return selectionInfo.With(s => s.SelectionInSingleStatement = true) - .With(s => s.FirstTokenInFinalSpan = statement1.GetFirstToken(includeZeroWidth: true)) - .With(s => s.LastTokenInFinalSpan = statement1.GetLastToken(includeZeroWidth: true)); + return selectionInfo.With(s => s.SelectionInExpression = true) + .With(s => s.FirstTokenInFinalSpan = expression.GetFirstToken(includeZeroWidth: true)) + .With(s => s.LastTokenInFinalSpan = expression.GetLastToken(includeZeroWidth: true)); } - // move only statements inside of the block - return selectionInfo.With(s => s.FirstTokenInFinalSpan = statement1.GetFirstToken(includeZeroWidth: true)) - .With(s => s.LastTokenInFinalSpan = statement2.GetLastToken(includeZeroWidth: true)); + // single statement case + return selectionInfo.With(s => s.SelectionInSingleStatement = true) + .With(s => s.FirstTokenInFinalSpan = statement1.GetFirstToken(includeZeroWidth: true)) + .With(s => s.LastTokenInFinalSpan = statement1.GetLastToken(includeZeroWidth: true)); } - private static SelectionInfo AssignFinalSpan(SelectionInfo selectionInfo, SourceText text) - { - if (selectionInfo.Status.Failed) - return selectionInfo; + // move only statements inside of the block + return selectionInfo.With(s => s.FirstTokenInFinalSpan = statement1.GetFirstToken(includeZeroWidth: true)) + .With(s => s.LastTokenInFinalSpan = statement2.GetLastToken(includeZeroWidth: true)); + } + + private static SelectionInfo AssignFinalSpan(SelectionInfo selectionInfo, SourceText text) + { + if (selectionInfo.Status.Failed) + return selectionInfo; - // set final span - var start = (selectionInfo.FirstTokenInOriginalSpan == selectionInfo.FirstTokenInFinalSpan) - ? Math.Min(selectionInfo.FirstTokenInOriginalSpan.SpanStart, selectionInfo.OriginalSpan.Start) - : selectionInfo.FirstTokenInFinalSpan.FullSpan.Start; + // set final span + var start = (selectionInfo.FirstTokenInOriginalSpan == selectionInfo.FirstTokenInFinalSpan) + ? Math.Min(selectionInfo.FirstTokenInOriginalSpan.SpanStart, selectionInfo.OriginalSpan.Start) + : selectionInfo.FirstTokenInFinalSpan.FullSpan.Start; - var end = (selectionInfo.LastTokenInOriginalSpan == selectionInfo.LastTokenInFinalSpan) - ? Math.Max(selectionInfo.LastTokenInOriginalSpan.Span.End, selectionInfo.OriginalSpan.End) - : selectionInfo.LastTokenInFinalSpan.FullSpan.End; + var end = (selectionInfo.LastTokenInOriginalSpan == selectionInfo.LastTokenInFinalSpan) + ? Math.Max(selectionInfo.LastTokenInOriginalSpan.Span.End, selectionInfo.OriginalSpan.End) + : selectionInfo.LastTokenInFinalSpan.FullSpan.End; - return selectionInfo.With(s => s.FinalSpan = GetAdjustedSpan(text, TextSpan.FromBounds(start, end))); - } + return selectionInfo.With(s => s.FinalSpan = GetAdjustedSpan(text, TextSpan.FromBounds(start, end))); + } - public override bool ContainsNonReturnExitPointsStatements(IEnumerable jumpsOutOfRegion) - => jumpsOutOfRegion.Where(n => n is not ReturnStatementSyntax).Any(); + public override bool ContainsNonReturnExitPointsStatements(IEnumerable jumpsOutOfRegion) + => jumpsOutOfRegion.Where(n => n is not ReturnStatementSyntax).Any(); - public override IEnumerable GetOuterReturnStatements(SyntaxNode commonRoot, IEnumerable jumpsOutOfRegion) + public override IEnumerable GetOuterReturnStatements(SyntaxNode commonRoot, IEnumerable jumpsOutOfRegion) + { + var returnStatements = jumpsOutOfRegion.Where(s => s is ReturnStatementSyntax); + + var container = commonRoot.GetAncestorsOrThis().Where(a => a.IsReturnableConstruct()).FirstOrDefault(); + if (container == null) { - var returnStatements = jumpsOutOfRegion.Where(s => s is ReturnStatementSyntax); + return SpecializedCollections.EmptyEnumerable(); + } - var container = commonRoot.GetAncestorsOrThis().Where(a => a.IsReturnableConstruct()).FirstOrDefault(); - if (container == null) - { - return SpecializedCollections.EmptyEnumerable(); - } + var returnableConstructPairs = returnStatements.Select(r => (r, r.GetAncestors().Where(a => a.IsReturnableConstruct()).FirstOrDefault())) + .Where(p => p.Item2 != null); - var returnableConstructPairs = returnStatements.Select(r => (r, r.GetAncestors().Where(a => a.IsReturnableConstruct()).FirstOrDefault())) - .Where(p => p.Item2 != null); + // now filter return statements to only include the one under outmost container + return returnableConstructPairs.Where(p => p.Item2 == container).Select(p => p.Item1); + } - // now filter return statements to only include the one under outmost container - return returnableConstructPairs.Where(p => p.Item2 == container).Select(p => p.Item1); + public override bool IsFinalSpanSemanticallyValidSpan( + SyntaxNode root, TextSpan textSpan, + IEnumerable returnStatements, CancellationToken cancellationToken) + { + // return statement shouldn't contain any return value + if (returnStatements.Cast().Any(r => r.Expression != null)) + { + return false; } - public override bool IsFinalSpanSemanticallyValidSpan( - SyntaxNode root, TextSpan textSpan, - IEnumerable returnStatements, CancellationToken cancellationToken) + var lastToken = root.FindToken(textSpan.End); + if (lastToken.Kind() == SyntaxKind.None) { - // return statement shouldn't contain any return value - if (returnStatements.Cast().Any(r => r.Expression != null)) - { - return false; - } + return false; + } - var lastToken = root.FindToken(textSpan.End); - if (lastToken.Kind() == SyntaxKind.None) - { - return false; - } + var container = lastToken.GetAncestors().FirstOrDefault(n => n.IsReturnableConstruct()); + if (container == null) + { + return false; + } - var container = lastToken.GetAncestors().FirstOrDefault(n => n.IsReturnableConstruct()); - if (container == null) - { - return false; - } + var body = container.GetBlockBody(); + if (body == null) + { + return false; + } - var body = container.GetBlockBody(); - if (body == null) - { - return false; - } + // make sure that next token of the last token in the selection is the close braces of containing block + if (body.CloseBraceToken != lastToken.GetNextToken(includeZeroWidth: true)) + { + return false; + } - // make sure that next token of the last token in the selection is the close braces of containing block - if (body.CloseBraceToken != lastToken.GetNextToken(includeZeroWidth: true)) - { - return false; - } + // alright, for these constructs, it must be okay to be extracted + switch (container.Kind()) + { + case SyntaxKind.AnonymousMethodExpression: + case SyntaxKind.SimpleLambdaExpression: + case SyntaxKind.ParenthesizedLambdaExpression: + return true; + } - // alright, for these constructs, it must be okay to be extracted - switch (container.Kind()) - { - case SyntaxKind.AnonymousMethodExpression: - case SyntaxKind.SimpleLambdaExpression: - case SyntaxKind.ParenthesizedLambdaExpression: - return true; - } + // now, only method is okay to be extracted out + if (body.Parent is not MethodDeclarationSyntax method) + { + return false; + } - // now, only method is okay to be extracted out - if (body.Parent is not MethodDeclarationSyntax method) - { - return false; - } + // make sure this method doesn't have return type. + return method.ReturnType is PredefinedTypeSyntax p && + p.Keyword.Kind() == SyntaxKind.VoidKeyword; + } - // make sure this method doesn't have return type. - return method.ReturnType is PredefinedTypeSyntax p && - p.Keyword.Kind() == SyntaxKind.VoidKeyword; + private static TextSpan GetAdjustedSpan(SourceText text, TextSpan textSpan) + { + // beginning of a file + if (textSpan.IsEmpty || textSpan.End == 0) + { + return textSpan; } - private static TextSpan GetAdjustedSpan(SourceText text, TextSpan textSpan) + // if it is a start of new line, make it belong to previous line + var line = text.Lines.GetLineFromPosition(textSpan.End); + if (line.Start != textSpan.End) { - // beginning of a file - if (textSpan.IsEmpty || textSpan.End == 0) - { - return textSpan; - } - - // if it is a start of new line, make it belong to previous line - var line = text.Lines.GetLineFromPosition(textSpan.End); - if (line.Start != textSpan.End) - { - return textSpan; - } + return textSpan; + } - // get previous line - Contract.ThrowIfFalse(line.LineNumber > 0); - var previousLine = text.Lines[line.LineNumber - 1]; + // get previous line + Contract.ThrowIfFalse(line.LineNumber > 0); + var previousLine = text.Lines[line.LineNumber - 1]; - // if the span is past the end of the line (ie, in whitespace) then - // return to the end of the line including whitespace - if (textSpan.Start > previousLine.End) - { - return TextSpan.FromBounds(textSpan.Start, previousLine.EndIncludingLineBreak); - } - - return TextSpan.FromBounds(textSpan.Start, previousLine.End); + // if the span is past the end of the line (ie, in whitespace) then + // return to the end of the line including whitespace + if (textSpan.Start > previousLine.End) + { + return TextSpan.FromBounds(textSpan.Start, previousLine.EndIncludingLineBreak); } + + return TextSpan.FromBounds(textSpan.Start, previousLine.End); } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpSyntaxTriviaService.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpSyntaxTriviaService.cs index daa9eca43484a..3bc94b8d4458b 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpSyntaxTriviaService.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpSyntaxTriviaService.cs @@ -4,15 +4,14 @@ using Microsoft.CodeAnalysis.ExtractMethod; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal class CSharpSyntaxTriviaService : AbstractSyntaxTriviaService { - internal class CSharpSyntaxTriviaService : AbstractSyntaxTriviaService - { - public static readonly CSharpSyntaxTriviaService Instance = new CSharpSyntaxTriviaService(); + public static readonly CSharpSyntaxTriviaService Instance = new CSharpSyntaxTriviaService(); - private CSharpSyntaxTriviaService() - : base((int)SyntaxKind.EndOfLineTrivia) - { - } + private CSharpSyntaxTriviaService() + : base((int)SyntaxKind.EndOfLineTrivia) + { } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpSyntaxTriviaServiceFactory.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpSyntaxTriviaServiceFactory.cs index 7100573644be7..d7caf713426a3 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpSyntaxTriviaServiceFactory.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpSyntaxTriviaServiceFactory.cs @@ -10,18 +10,17 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +[ExportLanguageServiceFactory(typeof(ISyntaxTriviaService), LanguageNames.CSharp), Shared] +internal class CSharpSyntaxTriviaServiceFactory : ILanguageServiceFactory { - [ExportLanguageServiceFactory(typeof(ISyntaxTriviaService), LanguageNames.CSharp), Shared] - internal class CSharpSyntaxTriviaServiceFactory : ILanguageServiceFactory + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpSyntaxTriviaServiceFactory() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpSyntaxTriviaServiceFactory() - { - } - - public ILanguageService CreateLanguageService(HostLanguageServices provider) - => CSharpSyntaxTriviaService.Instance; } + + public ILanguageService CreateLanguageService(HostLanguageServices provider) + => CSharpSyntaxTriviaService.Instance; } diff --git a/src/Features/CSharp/Portable/ExtractMethod/Extensions.cs b/src/Features/CSharp/Portable/ExtractMethod/Extensions.cs index cdc77b6651211..77a88bd897411 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/Extensions.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/Extensions.cs @@ -14,271 +14,270 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod +namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod; + +internal static class Extensions { - internal static class Extensions + [return: NotNullIfNotNull(nameof(node))] + public static ExpressionSyntax? GetUnparenthesizedExpression(this ExpressionSyntax? node) { - [return: NotNullIfNotNull(nameof(node))] - public static ExpressionSyntax? GetUnparenthesizedExpression(this ExpressionSyntax? node) + if (node is not ParenthesizedExpressionSyntax parenthesizedExpression) { - if (node is not ParenthesizedExpressionSyntax parenthesizedExpression) - { - return node; - } - - return GetUnparenthesizedExpression(parenthesizedExpression.Expression); + return node; } - public static StatementSyntax? GetStatementUnderContainer(this SyntaxNode node) - { - Contract.ThrowIfNull(node); - - for (var current = node; current is object; current = current.Parent) - { - if (current.Parent != null && - current.Parent.IsStatementContainerNode()) - { - return current as StatementSyntax; - } - } - - return null; - } - - public static StatementSyntax GetParentLabeledStatementIfPossible(this SyntaxNode node) - => (StatementSyntax)((node.Parent is LabeledStatementSyntax) ? node.Parent : node); + return GetUnparenthesizedExpression(parenthesizedExpression.Expression); + } - public static bool IsStatementContainerNode([NotNullWhen(returnValue: true)] this SyntaxNode? node) - => node is BlockSyntax or SwitchSectionSyntax or GlobalStatementSyntax; + public static StatementSyntax? GetStatementUnderContainer(this SyntaxNode node) + { + Contract.ThrowIfNull(node); - public static BlockSyntax? GetBlockBody(this SyntaxNode? node) + for (var current = node; current is object; current = current.Parent) { - switch (node) + if (current.Parent != null && + current.Parent.IsStatementContainerNode()) { - case BaseMethodDeclarationSyntax m: return m.Body; - case AccessorDeclarationSyntax a: return a.Body; - case SimpleLambdaExpressionSyntax s: return s.Body as BlockSyntax; - case ParenthesizedLambdaExpressionSyntax p: return p.Body as BlockSyntax; - case AnonymousMethodExpressionSyntax a: return a.Block; - default: return null; + return current as StatementSyntax; } } - public static bool UnderValidContext(this SyntaxNode node) - { - Contract.ThrowIfNull(node); + return null; + } - if (!node.GetAncestorsOrThis().Any(predicate)) - { - return false; - } + public static StatementSyntax GetParentLabeledStatementIfPossible(this SyntaxNode node) + => (StatementSyntax)((node.Parent is LabeledStatementSyntax) ? node.Parent : node); - return true; + public static bool IsStatementContainerNode([NotNullWhen(returnValue: true)] this SyntaxNode? node) + => node is BlockSyntax or SwitchSectionSyntax or GlobalStatementSyntax; - bool predicate(SyntaxNode n) - { - if (n is BaseMethodDeclarationSyntax or - AccessorDeclarationSyntax or - BlockSyntax or - GlobalStatementSyntax or - CompilationUnitSyntax) - { - return true; - } - - if (n is ConstructorInitializerSyntax constructorInitializer) - { - return constructorInitializer.ContainsInArgument(node.Span); - } + public static BlockSyntax? GetBlockBody(this SyntaxNode? node) + { + switch (node) + { + case BaseMethodDeclarationSyntax m: return m.Body; + case AccessorDeclarationSyntax a: return a.Body; + case SimpleLambdaExpressionSyntax s: return s.Body as BlockSyntax; + case ParenthesizedLambdaExpressionSyntax p: return p.Body as BlockSyntax; + case AnonymousMethodExpressionSyntax a: return a.Block; + default: return null; + } + } - return false; - } + public static bool UnderValidContext(this SyntaxNode node) + { + Contract.ThrowIfNull(node); + + if (!node.GetAncestorsOrThis().Any(predicate)) + { + return false; } - public static bool ContainsInArgument(this ConstructorInitializerSyntax initializer, TextSpan textSpan) + return true; + + bool predicate(SyntaxNode n) { - if (initializer == null) + if (n is BaseMethodDeclarationSyntax or + AccessorDeclarationSyntax or + BlockSyntax or + GlobalStatementSyntax or + CompilationUnitSyntax) { - return false; + return true; } - return initializer.ArgumentList.Arguments.Any(a => a.Span.Contains(textSpan)); - } - - public static bool ContainedInValidType(this SyntaxNode node) - { - Contract.ThrowIfNull(node); - foreach (var ancestor in node.AncestorsAndSelf()) + if (n is ConstructorInitializerSyntax constructorInitializer) { - if (ancestor is TypeDeclarationSyntax) - { - return true; - } - - if (ancestor is NamespaceDeclarationSyntax) - { - return false; - } + return constructorInitializer.ContainsInArgument(node.Span); } - return true; + return false; } + } - public static bool PartOfConstantInitializerExpression(this SyntaxNode node) + public static bool ContainsInArgument(this ConstructorInitializerSyntax initializer, TextSpan textSpan) + { + if (initializer == null) { - return node.PartOfConstantInitializerExpression(n => n.Modifiers) || - node.PartOfConstantInitializerExpression(n => n.Modifiers); + return false; } - private static bool PartOfConstantInitializerExpression(this SyntaxNode node, Func modifiersGetter) where T : SyntaxNode + return initializer.ArgumentList.Arguments.Any(a => a.Span.Contains(textSpan)); + } + + public static bool ContainedInValidType(this SyntaxNode node) + { + Contract.ThrowIfNull(node); + foreach (var ancestor in node.AncestorsAndSelf()) { - var decl = node.GetAncestor(); - if (decl == null) + if (ancestor is TypeDeclarationSyntax) { - return false; + return true; } - if (!modifiersGetter(decl).Any(SyntaxKind.ConstKeyword)) + if (ancestor is NamespaceDeclarationSyntax) { return false; } + } - // we are under decl with const modifier, check we are part of initializer expression - var equal = node.GetAncestor(); - if (equal == null) - { - return false; - } + return true; + } - return equal.Value != null && equal.Value.Span.Contains(node.Span); + public static bool PartOfConstantInitializerExpression(this SyntaxNode node) + { + return node.PartOfConstantInitializerExpression(n => n.Modifiers) || + node.PartOfConstantInitializerExpression(n => n.Modifiers); + } + + private static bool PartOfConstantInitializerExpression(this SyntaxNode node, Func modifiersGetter) where T : SyntaxNode + { + var decl = node.GetAncestor(); + if (decl == null) + { + return false; } - public static bool ContainArgumentlessThrowWithoutEnclosingCatch(this IEnumerable tokens, TextSpan textSpan) + if (!modifiersGetter(decl).Any(SyntaxKind.ConstKeyword)) { - foreach (var token in tokens) - { - if (token.Kind() != SyntaxKind.ThrowKeyword) - { - continue; - } - - if (token.Parent is not ThrowStatementSyntax throwStatement || throwStatement.Expression != null) - { - continue; - } - - var catchClause = token.GetAncestor(); - if (catchClause == null || !textSpan.Contains(catchClause.Span)) - { - return true; - } - } + return false; + } + // we are under decl with const modifier, check we are part of initializer expression + var equal = node.GetAncestor(); + if (equal == null) + { return false; } - public static bool ContainPreprocessorCrossOver(this IEnumerable tokens, TextSpan textSpan) + return equal.Value != null && equal.Value.Span.Contains(node.Span); + } + + public static bool ContainArgumentlessThrowWithoutEnclosingCatch(this IEnumerable tokens, TextSpan textSpan) + { + foreach (var token in tokens) { - var activeRegions = 0; - var activeIfs = 0; + if (token.Kind() != SyntaxKind.ThrowKeyword) + { + continue; + } - foreach (var trivia in tokens.GetAllTrivia()) + if (token.Parent is not ThrowStatementSyntax throwStatement || throwStatement.Expression != null) { - if (!textSpan.Contains(trivia.Span)) - { - continue; - } - - switch (trivia.Kind()) - { - case SyntaxKind.RegionDirectiveTrivia: - activeRegions++; - break; - case SyntaxKind.EndRegionDirectiveTrivia: - if (activeRegions <= 0) - { - return true; - } - - activeRegions--; - break; - case SyntaxKind.IfDirectiveTrivia: - activeIfs++; - break; - case SyntaxKind.EndIfDirectiveTrivia: - if (activeIfs <= 0) - { - return true; - } - - activeIfs--; - break; - case SyntaxKind.ElseDirectiveTrivia: - case SyntaxKind.ElifDirectiveTrivia: - if (activeIfs <= 0) - { - return true; - } - - break; - } + continue; } - return activeIfs != 0 || activeRegions != 0; + var catchClause = token.GetAncestor(); + if (catchClause == null || !textSpan.Contains(catchClause.Span)) + { + return true; + } } - public static IEnumerable GetAllTrivia(this IEnumerable tokens) + return false; + } + + public static bool ContainPreprocessorCrossOver(this IEnumerable tokens, TextSpan textSpan) + { + var activeRegions = 0; + var activeIfs = 0; + + foreach (var trivia in tokens.GetAllTrivia()) { - foreach (var token in tokens) + if (!textSpan.Contains(trivia.Span)) + { + continue; + } + + switch (trivia.Kind()) { - foreach (var trivia in token.LeadingTrivia) - { - yield return trivia; - } - - foreach (var trivia in token.TrailingTrivia) - { - yield return trivia; - } + case SyntaxKind.RegionDirectiveTrivia: + activeRegions++; + break; + case SyntaxKind.EndRegionDirectiveTrivia: + if (activeRegions <= 0) + { + return true; + } + + activeRegions--; + break; + case SyntaxKind.IfDirectiveTrivia: + activeIfs++; + break; + case SyntaxKind.EndIfDirectiveTrivia: + if (activeIfs <= 0) + { + return true; + } + + activeIfs--; + break; + case SyntaxKind.ElseDirectiveTrivia: + case SyntaxKind.ElifDirectiveTrivia: + if (activeIfs <= 0) + { + return true; + } + + break; } } - public static bool HasSyntaxAnnotation(this HashSet set, SyntaxNode node) - => set.Any(a => node.GetAnnotatedNodesAndTokens(a).Any()); + return activeIfs != 0 || activeRegions != 0; + } - public static bool HasHybridTriviaBetween(this SyntaxToken token1, SyntaxToken token2) + public static IEnumerable GetAllTrivia(this IEnumerable tokens) + { + foreach (var token in tokens) { - if (token1.TrailingTrivia.Any(t => !t.IsElastic())) + foreach (var trivia in token.LeadingTrivia) { - return true; + yield return trivia; } - if (token2.LeadingTrivia.Any(t => !t.IsElastic())) + foreach (var trivia in token.TrailingTrivia) { - return true; + yield return trivia; } + } + } - return false; + public static bool HasSyntaxAnnotation(this HashSet set, SyntaxNode node) + => set.Any(a => node.GetAnnotatedNodesAndTokens(a).Any()); + + public static bool HasHybridTriviaBetween(this SyntaxToken token1, SyntaxToken token2) + { + if (token1.TrailingTrivia.Any(t => !t.IsElastic())) + { + return true; } - public static bool IsArrayInitializer([NotNullWhen(returnValue: true)] this SyntaxNode? node) - => node is InitializerExpressionSyntax && node.Parent is EqualsValueClauseSyntax; + if (token2.LeadingTrivia.Any(t => !t.IsElastic())) + { + return true; + } - public static bool IsExpressionInCast([NotNullWhen(returnValue: true)] this SyntaxNode? node) - => node is ExpressionSyntax && node.Parent is CastExpressionSyntax; + return false; + } - public static bool IsObjectType(this ITypeSymbol? type) - => type == null || type.SpecialType == SpecialType.System_Object; + public static bool IsArrayInitializer([NotNullWhen(returnValue: true)] this SyntaxNode? node) + => node is InitializerExpressionSyntax && node.Parent is EqualsValueClauseSyntax; - public static bool BetweenFieldAndNonFieldMember(this SyntaxToken token1, SyntaxToken token2) - { - if (token1.RawKind != (int)SyntaxKind.SemicolonToken || !(token1.Parent is FieldDeclarationSyntax)) - { - return false; - } + public static bool IsExpressionInCast([NotNullWhen(returnValue: true)] this SyntaxNode? node) + => node is ExpressionSyntax && node.Parent is CastExpressionSyntax; + + public static bool IsObjectType(this ITypeSymbol? type) + => type == null || type.SpecialType == SpecialType.System_Object; - var field = token2.GetAncestor(); - return field == null; + public static bool BetweenFieldAndNonFieldMember(this SyntaxToken token1, SyntaxToken token2) + { + if (token1.RawKind != (int)SyntaxKind.SemicolonToken || !(token1.Parent is FieldDeclarationSyntax)) + { + return false; } + + var field = token2.GetAncestor(); + return field == null; } } diff --git a/src/Features/CSharp/Portable/FindUsages/CSharpFindUsagesLSPService.cs b/src/Features/CSharp/Portable/FindUsages/CSharpFindUsagesLSPService.cs index 129b740698032..8ee0d86c919dc 100644 --- a/src/Features/CSharp/Portable/FindUsages/CSharpFindUsagesLSPService.cs +++ b/src/Features/CSharp/Portable/FindUsages/CSharpFindUsagesLSPService.cs @@ -7,15 +7,14 @@ using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.FindUsages +namespace Microsoft.CodeAnalysis.CSharp.FindUsages; + +[ExportLanguageService(typeof(IFindUsagesLSPService), LanguageNames.CSharp), Shared] +internal class CSharpFindUsagesLSPService : AbstractFindUsagesService { - [ExportLanguageService(typeof(IFindUsagesLSPService), LanguageNames.CSharp), Shared] - internal class CSharpFindUsagesLSPService : AbstractFindUsagesService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpFindUsagesLSPService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpFindUsagesLSPService() - { - } } } diff --git a/src/Features/CSharp/Portable/FindUsages/CSharpFindUsagesService.cs b/src/Features/CSharp/Portable/FindUsages/CSharpFindUsagesService.cs index 72a2df52c457d..3fff1e1cf2314 100644 --- a/src/Features/CSharp/Portable/FindUsages/CSharpFindUsagesService.cs +++ b/src/Features/CSharp/Portable/FindUsages/CSharpFindUsagesService.cs @@ -7,15 +7,14 @@ using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.FindUsages +namespace Microsoft.CodeAnalysis.CSharp.FindUsages; + +[ExportLanguageService(typeof(IFindUsagesService), LanguageNames.CSharp), Shared] +internal class CSharpFindUsagesService : AbstractFindUsagesService { - [ExportLanguageService(typeof(IFindUsagesService), LanguageNames.CSharp), Shared] - internal class CSharpFindUsagesService : AbstractFindUsagesService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpFindUsagesService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpFindUsagesService() - { - } } } diff --git a/src/Features/CSharp/Portable/Formatting/CSharpAccessibilityModifiersNewDocumentFormattingProvider.cs b/src/Features/CSharp/Portable/Formatting/CSharpAccessibilityModifiersNewDocumentFormattingProvider.cs index 670ebeeb1e215..e43d6fbe50178 100644 --- a/src/Features/CSharp/Portable/Formatting/CSharpAccessibilityModifiersNewDocumentFormattingProvider.cs +++ b/src/Features/CSharp/Portable/Formatting/CSharpAccessibilityModifiersNewDocumentFormattingProvider.cs @@ -16,63 +16,62 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Formatting +namespace Microsoft.CodeAnalysis.Formatting; + +[ExportNewDocumentFormattingProvider(LanguageNames.CSharp), Shared] +internal class CSharpAccessibilityModifiersNewDocumentFormattingProvider : INewDocumentFormattingProvider { - [ExportNewDocumentFormattingProvider(LanguageNames.CSharp), Shared] - internal class CSharpAccessibilityModifiersNewDocumentFormattingProvider : INewDocumentFormattingProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpAccessibilityModifiersNewDocumentFormattingProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpAccessibilityModifiersNewDocumentFormattingProvider() - { - } + } - public async Task FormatNewDocumentAsync(Document document, Document? hintDocument, CodeCleanupOptions options, CancellationToken cancellationToken) + public async Task FormatNewDocumentAsync(Document document, Document? hintDocument, CodeCleanupOptions options, CancellationToken cancellationToken) + { + var accessibilityPreferences = options.FormattingOptions.AccessibilityModifiersRequired; + if (accessibilityPreferences == AccessibilityModifiersRequired.Never) { - var accessibilityPreferences = options.FormattingOptions.AccessibilityModifiersRequired; - if (accessibilityPreferences == AccessibilityModifiersRequired.Never) - { - return document; - } - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); - var typeDeclarations = root.DescendantNodes().Where(node => syntaxFacts.IsTypeDeclaration(node)); - var editor = new SyntaxEditor(root, document.Project.Solution.Services); + return document; + } - var service = document.GetRequiredLanguageService(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + var typeDeclarations = root.DescendantNodes().Where(node => syntaxFacts.IsTypeDeclaration(node)); + var editor = new SyntaxEditor(root, document.Project.Solution.Services); - foreach (var declaration in typeDeclarations) - { - if (!service.ShouldUpdateAccessibilityModifier(CSharpAccessibilityFacts.Instance, declaration, accessibilityPreferences, out _, out _)) - continue; + var service = document.GetRequiredLanguageService(); - // Since we format each document as they are added to a project we can't assume we know about all - // of the files that are coming, so we have to opt out of changing partial classes. This especially - // manifests when creating new projects as we format before we have a project at all, so we could get a - // situation like this: - // - // File1.cs: - // partial class C { } - // File2.cs: - // public partial class C { } - // - // When we see File1, we don't know about File2, so would add an internal modifier, which would result in a compile - // error. - var modifiers = syntaxFacts.GetModifiers(declaration); - CSharpAccessibilityFacts.GetAccessibilityAndModifiers(modifiers, out _, out var declarationModifiers, out _); - if (declarationModifiers.IsPartial) - continue; + foreach (var declaration in typeDeclarations) + { + if (!service.ShouldUpdateAccessibilityModifier(CSharpAccessibilityFacts.Instance, declaration, accessibilityPreferences, out _, out _)) + continue; - var type = semanticModel.GetDeclaredSymbol(declaration, cancellationToken); - if (type == null) - continue; + // Since we format each document as they are added to a project we can't assume we know about all + // of the files that are coming, so we have to opt out of changing partial classes. This especially + // manifests when creating new projects as we format before we have a project at all, so we could get a + // situation like this: + // + // File1.cs: + // partial class C { } + // File2.cs: + // public partial class C { } + // + // When we see File1, we don't know about File2, so would add an internal modifier, which would result in a compile + // error. + var modifiers = syntaxFacts.GetModifiers(declaration); + CSharpAccessibilityFacts.GetAccessibilityAndModifiers(modifiers, out _, out var declarationModifiers, out _); + if (declarationModifiers.IsPartial) + continue; - AddAccessibilityModifiersHelpers.UpdateDeclaration(editor, type, declaration); - } + var type = semanticModel.GetDeclaredSymbol(declaration, cancellationToken); + if (type == null) + continue; - return document.WithSyntaxRoot(editor.GetChangedRoot()); + AddAccessibilityModifiersHelpers.UpdateDeclaration(editor, type, declaration); } + + return document.WithSyntaxRoot(editor.GetChangedRoot()); } } diff --git a/src/Features/CSharp/Portable/Formatting/CSharpNamespaceDeclarationNewDocumentFormattingProvider.cs b/src/Features/CSharp/Portable/Formatting/CSharpNamespaceDeclarationNewDocumentFormattingProvider.cs index c00144b6d0e68..1656f6bf0b571 100644 --- a/src/Features/CSharp/Portable/Formatting/CSharpNamespaceDeclarationNewDocumentFormattingProvider.cs +++ b/src/Features/CSharp/Portable/Formatting/CSharpNamespaceDeclarationNewDocumentFormattingProvider.cs @@ -17,43 +17,42 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Formatting +namespace Microsoft.CodeAnalysis.CSharp.Formatting; + +[ExportNewDocumentFormattingProvider(LanguageNames.CSharp), Shared] +internal class CSharpNamespaceDeclarationNewDocumentFormattingProvider : INewDocumentFormattingProvider { - [ExportNewDocumentFormattingProvider(LanguageNames.CSharp), Shared] - internal class CSharpNamespaceDeclarationNewDocumentFormattingProvider : INewDocumentFormattingProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpNamespaceDeclarationNewDocumentFormattingProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpNamespaceDeclarationNewDocumentFormattingProvider() - { - } + } - public async Task FormatNewDocumentAsync(Document document, Document? hintDocument, CodeCleanupOptions options, CancellationToken cancellationToken) - { - var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + public async Task FormatNewDocumentAsync(Document document, Document? hintDocument, CodeCleanupOptions options, CancellationToken cancellationToken) + { + var root = (CompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var formattingOptions = (CSharpSyntaxFormattingOptions)options.FormattingOptions; + var formattingOptions = (CSharpSyntaxFormattingOptions)options.FormattingOptions; - var namespaces = GetNamespacesToReplace(document, root, formattingOptions.NamespaceDeclarations).ToList(); - if (namespaces.Count != 1) - return document; + var namespaces = GetNamespacesToReplace(document, root, formattingOptions.NamespaceDeclarations).ToList(); + if (namespaces.Count != 1) + return document; - return await ConvertNamespaceTransform.ConvertAsync(document, namespaces[0], formattingOptions, cancellationToken).ConfigureAwait(false); - } + return await ConvertNamespaceTransform.ConvertAsync(document, namespaces[0], formattingOptions, cancellationToken).ConfigureAwait(false); + } - private static IEnumerable GetNamespacesToReplace(Document document, CompilationUnitSyntax root, CodeStyleOption2 option) - { - var syntaxFacts = document.GetRequiredLanguageService(); - var declarations = root.DescendantNodes().OfType(); + private static IEnumerable GetNamespacesToReplace(Document document, CompilationUnitSyntax root, CodeStyleOption2 option) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var declarations = root.DescendantNodes().OfType(); - foreach (var declaration in declarations) + foreach (var declaration in declarations) + { + // Passing in forAnalyzer: true means we'll only get a result if the declaration doesn't match the preferences + if (ConvertNamespaceAnalysis.CanOfferUseBlockScoped(option, declaration, forAnalyzer: true) || + ConvertNamespaceAnalysis.CanOfferUseFileScoped(option, root, declaration, forAnalyzer: true)) { - // Passing in forAnalyzer: true means we'll only get a result if the declaration doesn't match the preferences - if (ConvertNamespaceAnalysis.CanOfferUseBlockScoped(option, declaration, forAnalyzer: true) || - ConvertNamespaceAnalysis.CanOfferUseFileScoped(option, root, declaration, forAnalyzer: true)) - { - yield return declaration; - } + yield return declaration; } } } diff --git a/src/Features/CSharp/Portable/Formatting/CSharpNewDocumentFormattingService.cs b/src/Features/CSharp/Portable/Formatting/CSharpNewDocumentFormattingService.cs index a9d0ac1d8eac8..f795c4882bd3b 100644 --- a/src/Features/CSharp/Portable/Formatting/CSharpNewDocumentFormattingService.cs +++ b/src/Features/CSharp/Portable/Formatting/CSharpNewDocumentFormattingService.cs @@ -9,14 +9,13 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.Formatting +namespace Microsoft.CodeAnalysis.CSharp.Formatting; + +[ExportLanguageService(typeof(INewDocumentFormattingService), LanguageNames.CSharp)] +[Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpNewDocumentFormattingService([ImportMany] IEnumerable> providers) : AbstractNewDocumentFormattingService(providers) { - [ExportLanguageService(typeof(INewDocumentFormattingService), LanguageNames.CSharp)] - [Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpNewDocumentFormattingService([ImportMany] IEnumerable> providers) : AbstractNewDocumentFormattingService(providers) - { - protected override string Language => LanguageNames.CSharp; - } + protected override string Language => LanguageNames.CSharp; } diff --git a/src/Features/CSharp/Portable/Formatting/CSharpOrganizeUsingsNewDocumentFormattingProvider.cs b/src/Features/CSharp/Portable/Formatting/CSharpOrganizeUsingsNewDocumentFormattingProvider.cs index ca50ae5aed207..48b9da8e651d3 100644 --- a/src/Features/CSharp/Portable/Formatting/CSharpOrganizeUsingsNewDocumentFormattingProvider.cs +++ b/src/Features/CSharp/Portable/Formatting/CSharpOrganizeUsingsNewDocumentFormattingProvider.cs @@ -15,24 +15,23 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Simplification; -namespace Microsoft.CodeAnalysis.CSharp.Formatting +namespace Microsoft.CodeAnalysis.CSharp.Formatting; + +[ExportNewDocumentFormattingProvider(LanguageNames.CSharp), Shared] +internal class CSharpOrganizeUsingsNewDocumentFormattingProvider : INewDocumentFormattingProvider { - [ExportNewDocumentFormattingProvider(LanguageNames.CSharp), Shared] - internal class CSharpOrganizeUsingsNewDocumentFormattingProvider : INewDocumentFormattingProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpOrganizeUsingsNewDocumentFormattingProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpOrganizeUsingsNewDocumentFormattingProvider() - { - } + } - public async Task FormatNewDocumentAsync(Document document, Document? hintDocument, CodeCleanupOptions options, CancellationToken cancellationToken) - { - var organizeImportsService = document.GetRequiredLanguageService(); - var organizedDocument = await organizeImportsService.OrganizeImportsAsync(document, options.GetOrganizeImportsOptions(), cancellationToken).ConfigureAwait(false); + public async Task FormatNewDocumentAsync(Document document, Document? hintDocument, CodeCleanupOptions options, CancellationToken cancellationToken) + { + var organizeImportsService = document.GetRequiredLanguageService(); + var organizedDocument = await organizeImportsService.OrganizeImportsAsync(document, options.GetOrganizeImportsOptions(), cancellationToken).ConfigureAwait(false); - return await MisplacedUsingDirectivesCodeFixProvider.TransformDocumentIfRequiredAsync( - organizedDocument, options.SimplifierOptions, options.AddImportOptions.UsingDirectivePlacement, cancellationToken).ConfigureAwait(false); - } + return await MisplacedUsingDirectivesCodeFixProvider.TransformDocumentIfRequiredAsync( + organizedDocument, options.SimplifierOptions, options.AddImportOptions.UsingDirectivePlacement, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Features/CSharp/Portable/Formatting/CSharpUseProgramMainNewDocumentFormattingProvider.cs b/src/Features/CSharp/Portable/Formatting/CSharpUseProgramMainNewDocumentFormattingProvider.cs index 42e8f5892490c..aa3c2b3a3be6c 100644 --- a/src/Features/CSharp/Portable/Formatting/CSharpUseProgramMainNewDocumentFormattingProvider.cs +++ b/src/Features/CSharp/Portable/Formatting/CSharpUseProgramMainNewDocumentFormattingProvider.cs @@ -13,26 +13,25 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.CodeStyle; -namespace Microsoft.CodeAnalysis.CSharp.Formatting +namespace Microsoft.CodeAnalysis.CSharp.Formatting; + +[ExportNewDocumentFormattingProvider(LanguageNames.CSharp), Shared] +internal class CSharpUseProgramMainNewDocumentFormattingProvider : INewDocumentFormattingProvider { - [ExportNewDocumentFormattingProvider(LanguageNames.CSharp), Shared] - internal class CSharpUseProgramMainNewDocumentFormattingProvider : INewDocumentFormattingProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpUseProgramMainNewDocumentFormattingProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpUseProgramMainNewDocumentFormattingProvider() - { - } + } - public async Task FormatNewDocumentAsync(Document document, Document? hintDocument, CodeCleanupOptions options, CancellationToken cancellationToken) - { - // if the user prefers Program.Main style instead, then attempt to convert a template with - // top-level-statements to that form. - var option = ((CSharpSyntaxFormattingOptions)options.FormattingOptions).PreferTopLevelStatements; - if (option.Value) - return document; + public async Task FormatNewDocumentAsync(Document document, Document? hintDocument, CodeCleanupOptions options, CancellationToken cancellationToken) + { + // if the user prefers Program.Main style instead, then attempt to convert a template with + // top-level-statements to that form. + var option = ((CSharpSyntaxFormattingOptions)options.FormattingOptions).PreferTopLevelStatements; + if (option.Value) + return document; - return await ConvertProgramTransform.ConvertToProgramMainAsync(document, options.FormattingOptions.AccessibilityModifiersRequired, cancellationToken).ConfigureAwait(false); - } + return await ConvertProgramTransform.ConvertToProgramMainAsync(document, options.FormattingOptions.AccessibilityModifiersRequired, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Features/CSharp/Portable/FullyQualify/CSharpFullyQualifyCodeFixProvider.cs b/src/Features/CSharp/Portable/FullyQualify/CSharpFullyQualifyCodeFixProvider.cs index 4fa645209ed35..28c554c7db1bf 100644 --- a/src/Features/CSharp/Portable/FullyQualify/CSharpFullyQualifyCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/FullyQualify/CSharpFullyQualifyCodeFixProvider.cs @@ -9,44 +9,43 @@ using Microsoft.CodeAnalysis.CodeFixes.FullyQualify; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.FullyQualify +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.FullyQualify; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.FullyQualify), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)] +internal sealed class CSharpFullyQualifyCodeFixProvider : AbstractFullyQualifyCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.FullyQualify), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)] - internal sealed class CSharpFullyQualifyCodeFixProvider : AbstractFullyQualifyCodeFixProvider + /// + /// name does not exist in context + /// + private const string CS0103 = nameof(CS0103); + + /// + /// 'reference' is an ambiguous reference between 'identifier' and 'identifier' + /// + private const string CS0104 = nameof(CS0104); + + /// + /// type or namespace could not be found + /// + private const string CS0246 = nameof(CS0246); + + /// + /// wrong number of type args + /// + private const string CS0305 = nameof(CS0305); + + /// + /// The non-generic type 'A' cannot be used with type arguments + /// + private const string CS0308 = nameof(CS0308); + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpFullyQualifyCodeFixProvider() { - /// - /// name does not exist in context - /// - private const string CS0103 = nameof(CS0103); - - /// - /// 'reference' is an ambiguous reference between 'identifier' and 'identifier' - /// - private const string CS0104 = nameof(CS0104); - - /// - /// type or namespace could not be found - /// - private const string CS0246 = nameof(CS0246); - - /// - /// wrong number of type args - /// - private const string CS0305 = nameof(CS0305); - - /// - /// The non-generic type 'A' cannot be used with type arguments - /// - private const string CS0308 = nameof(CS0308); - - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpFullyQualifyCodeFixProvider() - { - } - - public override ImmutableArray FixableDiagnosticIds { get; } = - [CS0103, CS0104, CS0246, CS0305, CS0308, IDEDiagnosticIds.UnboundIdentifierId]; } + + public override ImmutableArray FixableDiagnosticIds { get; } = + [CS0103, CS0104, CS0246, CS0305, CS0308, IDEDiagnosticIds.UnboundIdentifierId]; } diff --git a/src/Features/CSharp/Portable/FullyQualify/CSharpFullyQualifyService.cs b/src/Features/CSharp/Portable/FullyQualify/CSharpFullyQualifyService.cs index a856c4a57fcb1..4002b668e5fba 100644 --- a/src/Features/CSharp/Portable/FullyQualify/CSharpFullyQualifyService.cs +++ b/src/Features/CSharp/Portable/FullyQualify/CSharpFullyQualifyService.cs @@ -12,58 +12,57 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.FullyQualify +namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.FullyQualify; + +[ExportLanguageService(typeof(IFullyQualifyService), LanguageNames.CSharp), Shared] +internal sealed class CSharpFullyQualifyService : AbstractFullyQualifyService { - [ExportLanguageService(typeof(IFullyQualifyService), LanguageNames.CSharp), Shared] - internal sealed class CSharpFullyQualifyService : AbstractFullyQualifyService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpFullyQualifyService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpFullyQualifyService() - { - } + } - protected override bool CanFullyQualify(SyntaxNode node, [NotNullWhen(true)] out SimpleNameSyntax? simpleName) - { - simpleName = node as SimpleNameSyntax; - if (simpleName is null) - return false; + protected override bool CanFullyQualify(SyntaxNode node, [NotNullWhen(true)] out SimpleNameSyntax? simpleName) + { + simpleName = node as SimpleNameSyntax; + if (simpleName is null) + return false; - if (!simpleName.LooksLikeStandaloneTypeName()) - return false; + if (!simpleName.LooksLikeStandaloneTypeName()) + return false; - if (!simpleName.CanBeReplacedWithAnyName()) - return false; + if (!simpleName.CanBeReplacedWithAnyName()) + return false; - return true; - } - - protected override async Task ReplaceNodeAsync(SimpleNameSyntax simpleName, string containerName, bool resultingSymbolIsType, CancellationToken cancellationToken) - { - var leadingTrivia = simpleName.GetLeadingTrivia(); - var newName = simpleName.WithLeadingTrivia(SyntaxTriviaList.Empty); + return true; + } - var qualifiedName = SyntaxFactory.QualifiedName(SyntaxFactory.ParseName(containerName), newName) - .WithLeadingTrivia(leadingTrivia); + protected override async Task ReplaceNodeAsync(SimpleNameSyntax simpleName, string containerName, bool resultingSymbolIsType, CancellationToken cancellationToken) + { + var leadingTrivia = simpleName.GetLeadingTrivia(); + var newName = simpleName.WithLeadingTrivia(SyntaxTriviaList.Empty); - var syntaxTree = simpleName.SyntaxTree; - var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var qualifiedName = SyntaxFactory.QualifiedName(SyntaxFactory.ParseName(containerName), newName) + .WithLeadingTrivia(leadingTrivia); - // If the name is a type that is part of a using directive, eg. "using Math" then we can go further and - // instead of just changing to "using System.Math", we can make it "using static System.Math" and avoid the - // CS0138 that would result from the former. Don't do this for using aliases though as `static` and using - // aliases cannot be combined. - if (resultingSymbolIsType && - simpleName.Parent is UsingDirectiveSyntax { Alias: null, StaticKeyword.RawKind: 0 } usingDirective) - { - var newUsingDirective = usingDirective - .WithStaticKeyword(SyntaxFactory.Token(SyntaxKind.StaticKeyword)) - .WithName(qualifiedName); + var syntaxTree = simpleName.SyntaxTree; + var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - return root.ReplaceNode(usingDirective, newUsingDirective); - } + // If the name is a type that is part of a using directive, eg. "using Math" then we can go further and + // instead of just changing to "using System.Math", we can make it "using static System.Math" and avoid the + // CS0138 that would result from the former. Don't do this for using aliases though as `static` and using + // aliases cannot be combined. + if (resultingSymbolIsType && + simpleName.Parent is UsingDirectiveSyntax { Alias: null, StaticKeyword.RawKind: 0 } usingDirective) + { + var newUsingDirective = usingDirective + .WithStaticKeyword(SyntaxFactory.Token(SyntaxKind.StaticKeyword)) + .WithName(qualifiedName); - return root.ReplaceNode(simpleName, qualifiedName); + return root.ReplaceNode(usingDirective, newUsingDirective); } + + return root.ReplaceNode(simpleName, qualifiedName); } } diff --git a/src/Features/CSharp/Portable/GenerateConstructor/CSharpGenerateConstructorService.cs b/src/Features/CSharp/Portable/GenerateConstructor/CSharpGenerateConstructorService.cs index aaa0269fa68b7..ea8a690293034 100644 --- a/src/Features/CSharp/Portable/GenerateConstructor/CSharpGenerateConstructorService.cs +++ b/src/Features/CSharp/Portable/GenerateConstructor/CSharpGenerateConstructorService.cs @@ -16,184 +16,183 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.GenerateConstructor +namespace Microsoft.CodeAnalysis.CSharp.GenerateConstructor; + +[ExportLanguageService(typeof(IGenerateConstructorService), LanguageNames.CSharp), Shared] +internal class CSharpGenerateConstructorService + : AbstractGenerateConstructorService { - [ExportLanguageService(typeof(IGenerateConstructorService), LanguageNames.CSharp), Shared] - internal class CSharpGenerateConstructorService - : AbstractGenerateConstructorService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGenerateConstructorService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGenerateConstructorService() - { - } + } + + protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) + => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); - protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) - => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); + protected override bool IsSimpleNameGeneration(SemanticDocument document, SyntaxNode node, CancellationToken cancellationToken) + => node is SimpleNameSyntax; - protected override bool IsSimpleNameGeneration(SemanticDocument document, SyntaxNode node, CancellationToken cancellationToken) - => node is SimpleNameSyntax; + protected override bool IsConstructorInitializerGeneration(SemanticDocument document, SyntaxNode node, CancellationToken cancellationToken) + => node is ConstructorInitializerSyntax; - protected override bool IsConstructorInitializerGeneration(SemanticDocument document, SyntaxNode node, CancellationToken cancellationToken) - => node is ConstructorInitializerSyntax; + protected override bool IsImplicitObjectCreation(SemanticDocument document, SyntaxNode node, CancellationToken cancellationToken) + => node is ImplicitObjectCreationExpressionSyntax; - protected override bool IsImplicitObjectCreation(SemanticDocument document, SyntaxNode node, CancellationToken cancellationToken) - => node is ImplicitObjectCreationExpressionSyntax; + protected override bool TryInitializeConstructorInitializerGeneration( + SemanticDocument document, + SyntaxNode node, + CancellationToken cancellationToken, + out SyntaxToken token, + out ImmutableArray arguments, + out INamedTypeSymbol typeToGenerateIn) + { + var constructorInitializer = (ConstructorInitializerSyntax)node; - protected override bool TryInitializeConstructorInitializerGeneration( - SemanticDocument document, - SyntaxNode node, - CancellationToken cancellationToken, - out SyntaxToken token, - out ImmutableArray arguments, - out INamedTypeSymbol typeToGenerateIn) + if (!constructorInitializer.ArgumentList.CloseParenToken.IsMissing) { - var constructorInitializer = (ConstructorInitializerSyntax)node; + token = constructorInitializer.ThisOrBaseKeyword; + arguments = GetArguments(constructorInitializer.ArgumentList.Arguments); + + var semanticModel = document.SemanticModel; + var currentType = semanticModel.GetEnclosingNamedType(constructorInitializer.SpanStart, cancellationToken); + typeToGenerateIn = constructorInitializer.IsKind(SyntaxKind.ThisConstructorInitializer) + ? currentType + : currentType.BaseType.OriginalDefinition; + return typeToGenerateIn != null; + } + + token = default; + arguments = default; + typeToGenerateIn = null; + return false; + } + + private static ImmutableArray GetArguments(SeparatedSyntaxList arguments) + => arguments.SelectAsArray(a => new Argument(a.GetRefKind(), a.NameColon?.Name.Identifier.ValueText, a.Expression)); + + private static ImmutableArray GetArguments(SeparatedSyntaxList arguments) + => arguments.SelectAsArray(a => new Argument( + refKind: RefKind.None, + a.NameEquals?.Name.Identifier.ValueText ?? a.NameColon?.Name.Identifier.ValueText, + a.Expression)); + + protected override bool TryInitializeSimpleNameGenerationState( + SemanticDocument document, + SyntaxNode node, + CancellationToken cancellationToken, + out SyntaxToken token, + out ImmutableArray arguments, + out INamedTypeSymbol typeToGenerateIn) + { + var simpleName = (SimpleNameSyntax)node; + var fullName = simpleName.IsRightSideOfQualifiedName() + ? (NameSyntax)simpleName.Parent + : simpleName; - if (!constructorInitializer.ArgumentList.CloseParenToken.IsMissing) + if (fullName.Parent is ObjectCreationExpressionSyntax objectCreationExpression) + { + if (objectCreationExpression.ArgumentList != null && + !objectCreationExpression.ArgumentList.CloseParenToken.IsMissing) { - token = constructorInitializer.ThisOrBaseKeyword; - arguments = GetArguments(constructorInitializer.ArgumentList.Arguments); - - var semanticModel = document.SemanticModel; - var currentType = semanticModel.GetEnclosingNamedType(constructorInitializer.SpanStart, cancellationToken); - typeToGenerateIn = constructorInitializer.IsKind(SyntaxKind.ThisConstructorInitializer) - ? currentType - : currentType.BaseType.OriginalDefinition; + var symbolInfo = document.SemanticModel.GetSymbolInfo(objectCreationExpression.Type, cancellationToken); + token = simpleName.Identifier; + arguments = GetArguments(objectCreationExpression.ArgumentList.Arguments); + typeToGenerateIn = symbolInfo.GetAnySymbol() as INamedTypeSymbol; return typeToGenerateIn != null; } - - token = default; - arguments = default; - typeToGenerateIn = null; - return false; } - private static ImmutableArray GetArguments(SeparatedSyntaxList arguments) - => arguments.SelectAsArray(a => new Argument(a.GetRefKind(), a.NameColon?.Name.Identifier.ValueText, a.Expression)); - - private static ImmutableArray GetArguments(SeparatedSyntaxList arguments) - => arguments.SelectAsArray(a => new Argument( - refKind: RefKind.None, - a.NameEquals?.Name.Identifier.ValueText ?? a.NameColon?.Name.Identifier.ValueText, - a.Expression)); - - protected override bool TryInitializeSimpleNameGenerationState( - SemanticDocument document, - SyntaxNode node, - CancellationToken cancellationToken, - out SyntaxToken token, - out ImmutableArray arguments, - out INamedTypeSymbol typeToGenerateIn) - { - var simpleName = (SimpleNameSyntax)node; - var fullName = simpleName.IsRightSideOfQualifiedName() - ? (NameSyntax)simpleName.Parent - : simpleName; + token = default; + arguments = default; + typeToGenerateIn = null; + return false; + } - if (fullName.Parent is ObjectCreationExpressionSyntax objectCreationExpression) + protected override bool TryInitializeSimpleAttributeNameGenerationState( + SemanticDocument document, + SyntaxNode node, + CancellationToken cancellationToken, + out SyntaxToken token, + out ImmutableArray arguments, + out INamedTypeSymbol typeToGenerateIn) + { + var simpleName = (SimpleNameSyntax)node; + var fullName = simpleName.IsRightSideOfQualifiedName() + ? (NameSyntax)simpleName.Parent + : simpleName; + + if (fullName.Parent is AttributeSyntax attribute) + { + if (attribute.ArgumentList != null && + !attribute.ArgumentList.CloseParenToken.IsMissing) { - if (objectCreationExpression.ArgumentList != null && - !objectCreationExpression.ArgumentList.CloseParenToken.IsMissing) + var symbolInfo = document.SemanticModel.GetSymbolInfo(attribute, cancellationToken); + if (symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure && !symbolInfo.CandidateSymbols.IsEmpty) { - var symbolInfo = document.SemanticModel.GetSymbolInfo(objectCreationExpression.Type, cancellationToken); token = simpleName.Identifier; - arguments = GetArguments(objectCreationExpression.ArgumentList.Arguments); - typeToGenerateIn = symbolInfo.GetAnySymbol() as INamedTypeSymbol; + arguments = GetArguments(attribute.ArgumentList.Arguments); + + typeToGenerateIn = symbolInfo.CandidateSymbols.FirstOrDefault().ContainingSymbol as INamedTypeSymbol; return typeToGenerateIn != null; } } - - token = default; - arguments = default; - typeToGenerateIn = null; - return false; } - protected override bool TryInitializeSimpleAttributeNameGenerationState( - SemanticDocument document, - SyntaxNode node, - CancellationToken cancellationToken, - out SyntaxToken token, - out ImmutableArray arguments, - out INamedTypeSymbol typeToGenerateIn) - { - var simpleName = (SimpleNameSyntax)node; - var fullName = simpleName.IsRightSideOfQualifiedName() - ? (NameSyntax)simpleName.Parent - : simpleName; - - if (fullName.Parent is AttributeSyntax attribute) - { - if (attribute.ArgumentList != null && - !attribute.ArgumentList.CloseParenToken.IsMissing) - { - var symbolInfo = document.SemanticModel.GetSymbolInfo(attribute, cancellationToken); - if (symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure && !symbolInfo.CandidateSymbols.IsEmpty) - { - token = simpleName.Identifier; - arguments = GetArguments(attribute.ArgumentList.Arguments); - - typeToGenerateIn = symbolInfo.CandidateSymbols.FirstOrDefault().ContainingSymbol as INamedTypeSymbol; - return typeToGenerateIn != null; - } - } - } - - token = default; - arguments = default; - typeToGenerateIn = null; - return false; - } + token = default; + arguments = default; + typeToGenerateIn = null; + return false; + } - protected override bool TryInitializeImplicitObjectCreation(SemanticDocument document, - SyntaxNode node, - CancellationToken cancellationToken, - out SyntaxToken token, - out ImmutableArray arguments, - out INamedTypeSymbol typeToGenerateIn) + protected override bool TryInitializeImplicitObjectCreation(SemanticDocument document, + SyntaxNode node, + CancellationToken cancellationToken, + out SyntaxToken token, + out ImmutableArray arguments, + out INamedTypeSymbol typeToGenerateIn) + { + var implicitObjectCreation = (ImplicitObjectCreationExpressionSyntax)node; + if (implicitObjectCreation.ArgumentList != null && + !implicitObjectCreation.ArgumentList.CloseParenToken.IsMissing) { - var implicitObjectCreation = (ImplicitObjectCreationExpressionSyntax)node; - if (implicitObjectCreation.ArgumentList != null && - !implicitObjectCreation.ArgumentList.CloseParenToken.IsMissing) + var typeInfo = document.SemanticModel.GetTypeInfo(implicitObjectCreation, cancellationToken); + if (typeInfo.Type is INamedTypeSymbol typeSymbol) { - var typeInfo = document.SemanticModel.GetTypeInfo(implicitObjectCreation, cancellationToken); - if (typeInfo.Type is INamedTypeSymbol typeSymbol) - { - token = implicitObjectCreation.NewKeyword; - arguments = GetArguments(implicitObjectCreation.ArgumentList.Arguments); - typeToGenerateIn = typeSymbol; - return true; - } + token = implicitObjectCreation.NewKeyword; + arguments = GetArguments(implicitObjectCreation.ArgumentList.Arguments); + typeToGenerateIn = typeSymbol; + return true; } - - token = default; - arguments = default; - typeToGenerateIn = null; - return false; } - protected override string GenerateNameForExpression(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) - => semanticModel.GenerateNameForExpression(expression, capitalize: false, cancellationToken: cancellationToken); + token = default; + arguments = default; + typeToGenerateIn = null; + return false; + } - protected override ITypeSymbol GetArgumentType(SemanticModel semanticModel, Argument argument, CancellationToken cancellationToken) - => InternalExtensions.DetermineParameterType(argument.Expression, semanticModel, cancellationToken); + protected override string GenerateNameForExpression(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) + => semanticModel.GenerateNameForExpression(expression, capitalize: false, cancellationToken: cancellationToken); - protected override bool IsConversionImplicit(Compilation compilation, ITypeSymbol sourceType, ITypeSymbol targetType) - => compilation.ClassifyConversion(sourceType, targetType).IsImplicit; + protected override ITypeSymbol GetArgumentType(SemanticModel semanticModel, Argument argument, CancellationToken cancellationToken) + => InternalExtensions.DetermineParameterType(argument.Expression, semanticModel, cancellationToken); - protected override IMethodSymbol GetCurrentConstructor(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) - => token.GetAncestor() is { } constructor ? semanticModel.GetDeclaredSymbol(constructor, cancellationToken) : null; + protected override bool IsConversionImplicit(Compilation compilation, ITypeSymbol sourceType, ITypeSymbol targetType) + => compilation.ClassifyConversion(sourceType, targetType).IsImplicit; - protected override IMethodSymbol GetDelegatedConstructor(SemanticModel semanticModel, IMethodSymbol constructor, CancellationToken cancellationToken) - { - if (constructor.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken) is ConstructorDeclarationSyntax constructorDeclarationSyntax && - constructorDeclarationSyntax.Initializer.IsKind(SyntaxKind.ThisConstructorInitializer)) - { - return semanticModel.GetSymbolInfo(constructorDeclarationSyntax.Initializer, cancellationToken).Symbol as IMethodSymbol; - } + protected override IMethodSymbol GetCurrentConstructor(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) + => token.GetAncestor() is { } constructor ? semanticModel.GetDeclaredSymbol(constructor, cancellationToken) : null; - return null; + protected override IMethodSymbol GetDelegatedConstructor(SemanticModel semanticModel, IMethodSymbol constructor, CancellationToken cancellationToken) + { + if (constructor.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken) is ConstructorDeclarationSyntax constructorDeclarationSyntax && + constructorDeclarationSyntax.Initializer.IsKind(SyntaxKind.ThisConstructorInitializer)) + { + return semanticModel.GetSymbolInfo(constructorDeclarationSyntax.Initializer, cancellationToken).Symbol as IMethodSymbol; } + + return null; } } diff --git a/src/Features/CSharp/Portable/GenerateConstructor/GenerateConstructorCodeFixProvider.cs b/src/Features/CSharp/Portable/GenerateConstructor/GenerateConstructorCodeFixProvider.cs index 1cb1a2317a63b..f784052d9d3b8 100644 --- a/src/Features/CSharp/Portable/GenerateConstructor/GenerateConstructorCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/GenerateConstructor/GenerateConstructorCodeFixProvider.cs @@ -19,57 +19,56 @@ using Microsoft.CodeAnalysis.GenerateMember.GenerateConstructor; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.GenerateConstructor +namespace Microsoft.CodeAnalysis.CSharp.GenerateConstructor; + +/// +/// This gives users a way to generate constructors for an existing +/// type when a user tries to 'new' up an instance of that type with a set of parameter that does +/// not match any existing constructor. i.e. it is the equivalent of 'Generate-Method' but for +/// constructors. Parameters for the constructor will be picked in a manner similar to Generate- +/// Method. However, this type will also attempt to hook up those parameters to existing fields +/// and properties, or pass them to a this/base constructor if available. +/// +/// Importantly, this type is not responsible for generating constructors for a type based on +/// the user selecting some fields/properties of that type. Nor is it responsible for generating +/// derived class constructors for all unmatched base class constructors in a type hierarchy. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateConstructor), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.FullyQualify)] +internal class GenerateConstructorCodeFixProvider : AbstractGenerateMemberCodeFixProvider { - /// - /// This gives users a way to generate constructors for an existing - /// type when a user tries to 'new' up an instance of that type with a set of parameter that does - /// not match any existing constructor. i.e. it is the equivalent of 'Generate-Method' but for - /// constructors. Parameters for the constructor will be picked in a manner similar to Generate- - /// Method. However, this type will also attempt to hook up those parameters to existing fields - /// and properties, or pass them to a this/base constructor if available. - /// - /// Importantly, this type is not responsible for generating constructors for a type based on - /// the user selecting some fields/properties of that type. Nor is it responsible for generating - /// derived class constructors for all unmatched base class constructors in a type hierarchy. - /// - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateConstructor), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.FullyQualify)] - internal class GenerateConstructorCodeFixProvider : AbstractGenerateMemberCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public GenerateConstructorCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public GenerateConstructorCodeFixProvider() - { - } + } - public override ImmutableArray FixableDiagnosticIds => GenerateConstructorDiagnosticIds.AllDiagnosticIds; + public override ImmutableArray FixableDiagnosticIds => GenerateConstructorDiagnosticIds.AllDiagnosticIds; - protected override Task> GetCodeActionsAsync( - Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var service = document.GetLanguageService(); - return service.GenerateConstructorAsync(document, node, fallbackOptions, cancellationToken); - } + protected override Task> GetCodeActionsAsync( + Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var service = document.GetLanguageService(); + return service.GenerateConstructorAsync(document, node, fallbackOptions, cancellationToken); + } - protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) - { - return node is BaseObjectCreationExpressionSyntax or - ConstructorInitializerSyntax or - AttributeSyntax; - } + protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) + { + return node is BaseObjectCreationExpressionSyntax or + ConstructorInitializerSyntax or + AttributeSyntax; + } - protected override SyntaxNode GetTargetNode(SyntaxNode node) + protected override SyntaxNode GetTargetNode(SyntaxNode node) + { + switch (node) { - switch (node) - { - case ObjectCreationExpressionSyntax objectCreationNode: - return objectCreationNode.Type.GetRightmostName(); - case AttributeSyntax attributeNode: - return attributeNode.Name; - } - - return node; + case ObjectCreationExpressionSyntax objectCreationNode: + return objectCreationNode.Type.GetRightmostName(); + case AttributeSyntax attributeNode: + return attributeNode.Name; } + + return node; } } diff --git a/src/Features/CSharp/Portable/GenerateConstructorFromMembers/CSharpGenerateConstructorFromMembersCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/GenerateConstructorFromMembers/CSharpGenerateConstructorFromMembersCodeRefactoringProvider.cs index 24b47f87f3057..acc547afb0035 100644 --- a/src/Features/CSharp/Portable/GenerateConstructorFromMembers/CSharpGenerateConstructorFromMembersCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/GenerateConstructorFromMembers/CSharpGenerateConstructorFromMembersCodeRefactoringProvider.cs @@ -22,83 +22,82 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Linq; -namespace Microsoft.CodeAnalysis.CSharp.GenerateConstructorFromMembers +namespace Microsoft.CodeAnalysis.CSharp.GenerateConstructorFromMembers; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.GenerateConstructorFromMembers), Shared] +[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.GenerateEqualsAndGetHashCodeFromMembers)] +[IntentProvider(WellKnownIntents.GenerateConstructor, LanguageNames.CSharp)] +internal sealed class CSharpGenerateConstructorFromMembersCodeRefactoringProvider + : AbstractGenerateConstructorFromMembersCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.GenerateConstructorFromMembers), Shared] - [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.GenerateEqualsAndGetHashCodeFromMembers)] - [IntentProvider(WellKnownIntents.GenerateConstructor, LanguageNames.CSharp)] - internal sealed class CSharpGenerateConstructorFromMembersCodeRefactoringProvider - : AbstractGenerateConstructorFromMembersCodeRefactoringProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGenerateConstructorFromMembersCodeRefactoringProvider() + { + } + + /// + /// For testing purposes only. + /// + [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] + internal CSharpGenerateConstructorFromMembersCodeRefactoringProvider(IPickMembersService pickMembersService_forTesting) + : base(pickMembersService_forTesting) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGenerateConstructorFromMembersCodeRefactoringProvider() - { - } - - /// - /// For testing purposes only. - /// - [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] - internal CSharpGenerateConstructorFromMembersCodeRefactoringProvider(IPickMembersService pickMembersService_forTesting) - : base(pickMembersService_forTesting) - { - } - - protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) - => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); - - protected override string ToDisplayString(IParameterSymbol parameter, SymbolDisplayFormat format) - => SymbolDisplay.ToDisplayString(parameter, format); - - protected override async ValueTask PrefersThrowExpressionAsync(Document document, SimplifierOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var options = (CSharpSimplifierOptions)await document.GetSimplifierOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - return options.PreferThrowExpression.Value; - } - - protected override IFieldSymbol? TryMapToWritableInstanceField(IPropertySymbol property, CancellationToken cancellationToken) - { - var containingType = property.ContainingType; - if (property.DeclaringSyntaxReferences.Length == 0) - return null; - - if (property.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken) is not PropertyDeclarationSyntax propertyDeclaration) - return null; - - var getAccessor = propertyDeclaration.AccessorList?.Accessors.FirstOrDefault(a => a.Kind() == SyntaxKind.GetAccessorDeclaration); - var body = propertyDeclaration.ExpressionBody ?? getAccessor?.ExpressionBody ?? (SyntaxNode?)getAccessor?.Body; - - var accessedMemberName = GetAccessedMemberName(body); - if (accessedMemberName is null) - return null; - - return property.ContainingType.GetMembers(accessedMemberName).FirstOrDefault() as IFieldSymbol; - } - - private static string? GetAccessedMemberName(SyntaxNode? body) - { - // Finally found a name. - if (body is IdentifierNameSyntax identifierName) - return identifierName.Identifier.ValueText; - - // `this.name`, recurse into `name` - if (body is MemberAccessExpressionSyntax { Expression: ThisExpressionSyntax } memberAccessExpress) - return GetAccessedMemberName(memberAccessExpress.Name); - - // `return this.name;` - if (body is ReturnStatementSyntax returnStatement) - return GetAccessedMemberName(returnStatement.Expression); - - // `=> this.name;` - if (body is ArrowExpressionClauseSyntax arrowExpression) - return GetAccessedMemberName(arrowExpression.Expression); - - // { return this.name; } - if (body is BlockSyntax { Statements.Count: > 0 } block) - return GetAccessedMemberName(block.Statements.First()); + } + + protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) + => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); + + protected override string ToDisplayString(IParameterSymbol parameter, SymbolDisplayFormat format) + => SymbolDisplay.ToDisplayString(parameter, format); + + protected override async ValueTask PrefersThrowExpressionAsync(Document document, SimplifierOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var options = (CSharpSimplifierOptions)await document.GetSimplifierOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + return options.PreferThrowExpression.Value; + } + + protected override IFieldSymbol? TryMapToWritableInstanceField(IPropertySymbol property, CancellationToken cancellationToken) + { + var containingType = property.ContainingType; + if (property.DeclaringSyntaxReferences.Length == 0) + return null; + if (property.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken) is not PropertyDeclarationSyntax propertyDeclaration) return null; - } + + var getAccessor = propertyDeclaration.AccessorList?.Accessors.FirstOrDefault(a => a.Kind() == SyntaxKind.GetAccessorDeclaration); + var body = propertyDeclaration.ExpressionBody ?? getAccessor?.ExpressionBody ?? (SyntaxNode?)getAccessor?.Body; + + var accessedMemberName = GetAccessedMemberName(body); + if (accessedMemberName is null) + return null; + + return property.ContainingType.GetMembers(accessedMemberName).FirstOrDefault() as IFieldSymbol; + } + + private static string? GetAccessedMemberName(SyntaxNode? body) + { + // Finally found a name. + if (body is IdentifierNameSyntax identifierName) + return identifierName.Identifier.ValueText; + + // `this.name`, recurse into `name` + if (body is MemberAccessExpressionSyntax { Expression: ThisExpressionSyntax } memberAccessExpress) + return GetAccessedMemberName(memberAccessExpress.Name); + + // `return this.name;` + if (body is ReturnStatementSyntax returnStatement) + return GetAccessedMemberName(returnStatement.Expression); + + // `=> this.name;` + if (body is ArrowExpressionClauseSyntax arrowExpression) + return GetAccessedMemberName(arrowExpression.Expression); + + // { return this.name; } + if (body is BlockSyntax { Statements.Count: > 0 } block) + return GetAccessedMemberName(block.Statements.First()); + + return null; } } diff --git a/src/Features/CSharp/Portable/GenerateDefaultConstructors/CSharpGenerateDefaultConstructorsCodeFixProvider.cs b/src/Features/CSharp/Portable/GenerateDefaultConstructors/CSharpGenerateDefaultConstructorsCodeFixProvider.cs index 670ce043f0800..c650876bd8a11 100644 --- a/src/Features/CSharp/Portable/GenerateDefaultConstructors/CSharpGenerateDefaultConstructorsCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/GenerateDefaultConstructors/CSharpGenerateDefaultConstructorsCodeFixProvider.cs @@ -10,25 +10,24 @@ using Microsoft.CodeAnalysis.GenerateDefaultConstructors; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.GenerateDefaultConstructors +namespace Microsoft.CodeAnalysis.CSharp.GenerateDefaultConstructors; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateDefaultConstructors), Shared] +internal class CSharpGenerateDefaultConstructorsCodeFixProvider : AbstractGenerateDefaultConstructorCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateDefaultConstructors), Shared] - internal class CSharpGenerateDefaultConstructorsCodeFixProvider : AbstractGenerateDefaultConstructorCodeFixProvider - { - private const string CS1729 = nameof(CS1729); // 'B' does not contain a constructor that takes 0 arguments CSharpConsoleApp3 C:\Users\cyrusn\source\repos\CSharpConsoleApp3\CSharpConsoleApp3\Program.cs 1 Active - private const string CS7036 = nameof(CS7036); // There is no argument given that corresponds to the required parameter 's' of 'B.B(string)' - private const string CS8983 = nameof(CS8983); // CS8983: A 'struct' with field initializers must include an explicitly declared constructor. + private const string CS1729 = nameof(CS1729); // 'B' does not contain a constructor that takes 0 arguments CSharpConsoleApp3 C:\Users\cyrusn\source\repos\CSharpConsoleApp3\CSharpConsoleApp3\Program.cs 1 Active + private const string CS7036 = nameof(CS7036); // There is no argument given that corresponds to the required parameter 's' of 'B.B(string)' + private const string CS8983 = nameof(CS8983); // CS8983: A 'struct' with field initializers must include an explicitly declared constructor. - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGenerateDefaultConstructorsCodeFixProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGenerateDefaultConstructorsCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds { get; } = - [CS1729, CS7036, CS8983]; + public override ImmutableArray FixableDiagnosticIds { get; } = + [CS1729, CS7036, CS8983]; - protected override SyntaxToken? TryGetTypeName(SyntaxNode typeDeclaration) - => (typeDeclaration as BaseTypeDeclarationSyntax)?.Identifier; - } + protected override SyntaxToken? TryGetTypeName(SyntaxNode typeDeclaration) + => (typeDeclaration as BaseTypeDeclarationSyntax)?.Identifier; } diff --git a/src/Features/CSharp/Portable/GenerateDefaultConstructors/CSharpGenerateDefaultConstructorsService.cs b/src/Features/CSharp/Portable/GenerateDefaultConstructors/CSharpGenerateDefaultConstructorsService.cs index c12d5ef9a8e9d..23fe06ecf003e 100644 --- a/src/Features/CSharp/Portable/GenerateDefaultConstructors/CSharpGenerateDefaultConstructorsService.cs +++ b/src/Features/CSharp/Portable/GenerateDefaultConstructors/CSharpGenerateDefaultConstructorsService.cs @@ -15,48 +15,47 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.GenerateDefaultConstructors +namespace Microsoft.CodeAnalysis.CSharp.GenerateDefaultConstructors; + +[ExportLanguageService(typeof(IGenerateDefaultConstructorsService), LanguageNames.CSharp), Shared] +internal class CSharpGenerateDefaultConstructorsService : AbstractGenerateDefaultConstructorsService { - [ExportLanguageService(typeof(IGenerateDefaultConstructorsService), LanguageNames.CSharp), Shared] - internal class CSharpGenerateDefaultConstructorsService : AbstractGenerateDefaultConstructorsService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGenerateDefaultConstructorsService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGenerateDefaultConstructorsService() - { - } + } - protected override bool TryInitializeState( - SemanticDocument semanticDocument, TextSpan textSpan, CancellationToken cancellationToken, - [NotNullWhen(true)] out INamedTypeSymbol? classType) - { - cancellationToken.ThrowIfCancellationRequested(); + protected override bool TryInitializeState( + SemanticDocument semanticDocument, TextSpan textSpan, CancellationToken cancellationToken, + [NotNullWhen(true)] out INamedTypeSymbol? classType) + { + cancellationToken.ThrowIfCancellationRequested(); - // Offer the feature if we're on the header / between members of the class/struct, - // or if we're on the first base-type of a class + // Offer the feature if we're on the header / between members of the class/struct, + // or if we're on the first base-type of a class - var helpers = semanticDocument.Document.GetRequiredLanguageService(); - if (helpers.IsOnTypeHeader(semanticDocument.Root, textSpan.Start, out var typeDeclaration) || - helpers.IsBetweenTypeMembers(semanticDocument.Text, semanticDocument.Root, textSpan.Start, out typeDeclaration)) - { - classType = semanticDocument.SemanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken) as INamedTypeSymbol; - return classType?.TypeKind is TypeKind.Class or TypeKind.Struct; - } + var helpers = semanticDocument.Document.GetRequiredLanguageService(); + if (helpers.IsOnTypeHeader(semanticDocument.Root, textSpan.Start, out var typeDeclaration) || + helpers.IsBetweenTypeMembers(semanticDocument.Text, semanticDocument.Root, textSpan.Start, out typeDeclaration)) + { + classType = semanticDocument.SemanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken) as INamedTypeSymbol; + return classType?.TypeKind is TypeKind.Class or TypeKind.Struct; + } - var syntaxTree = semanticDocument.SyntaxTree; - var node = semanticDocument.Root.FindToken(textSpan.Start).GetAncestor(); - if (node is { Parent: BaseTypeSyntax { Parent: BaseListSyntax { Types: [var firstType, ..] } baseList } }) + var syntaxTree = semanticDocument.SyntaxTree; + var node = semanticDocument.Root.FindToken(textSpan.Start).GetAncestor(); + if (node is { Parent: BaseTypeSyntax { Parent: BaseListSyntax { Types: [var firstType, ..] } baseList } }) + { + if (baseList.Parent is TypeDeclarationSyntax(SyntaxKind.ClassDeclaration or SyntaxKind.RecordDeclaration) parentTypeDecl && + firstType.Type == node) { - if (baseList.Parent is TypeDeclarationSyntax(SyntaxKind.ClassDeclaration or SyntaxKind.RecordDeclaration) parentTypeDecl && - firstType.Type == node) - { - classType = semanticDocument.SemanticModel.GetDeclaredSymbol(parentTypeDecl, cancellationToken); - return classType != null; - } + classType = semanticDocument.SemanticModel.GetDeclaredSymbol(parentTypeDecl, cancellationToken); + return classType != null; } - - classType = null; - return false; } + + classType = null; + return false; } } diff --git a/src/Features/CSharp/Portable/GenerateEqualsAndGetHashCodeFromMembers/CSharpGenerateEqualsAndGetHashCodeService.cs b/src/Features/CSharp/Portable/GenerateEqualsAndGetHashCodeFromMembers/CSharpGenerateEqualsAndGetHashCodeService.cs index 66950ed555e18..2eacfbc9d6da2 100644 --- a/src/Features/CSharp/Portable/GenerateEqualsAndGetHashCodeFromMembers/CSharpGenerateEqualsAndGetHashCodeService.cs +++ b/src/Features/CSharp/Portable/GenerateEqualsAndGetHashCodeFromMembers/CSharpGenerateEqualsAndGetHashCodeService.cs @@ -11,22 +11,21 @@ using Microsoft.CodeAnalysis.GenerateEqualsAndGetHashCodeFromMembers; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.GenerateEqualsAndGetHashCodeFromMembers +namespace Microsoft.CodeAnalysis.CSharp.GenerateEqualsAndGetHashCodeFromMembers; + +[ExportLanguageService(typeof(IGenerateEqualsAndGetHashCodeService), LanguageNames.CSharp), Shared] +internal class CSharpGenerateEqualsAndGetHashCodeService : AbstractGenerateEqualsAndGetHashCodeService { - [ExportLanguageService(typeof(IGenerateEqualsAndGetHashCodeService), LanguageNames.CSharp), Shared] - internal class CSharpGenerateEqualsAndGetHashCodeService : AbstractGenerateEqualsAndGetHashCodeService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGenerateEqualsAndGetHashCodeService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGenerateEqualsAndGetHashCodeService() - { - } + } - protected override bool TryWrapWithUnchecked(ImmutableArray statements, out ImmutableArray wrappedStatements) - { - wrappedStatements = [SyntaxFactory.CheckedStatement(SyntaxKind.UncheckedStatement, - SyntaxFactory.Block(statements.OfType()))]; - return true; - } + protected override bool TryWrapWithUnchecked(ImmutableArray statements, out ImmutableArray wrappedStatements) + { + wrappedStatements = [SyntaxFactory.CheckedStatement(SyntaxKind.UncheckedStatement, + SyntaxFactory.Block(statements.OfType()))]; + return true; } } diff --git a/src/Features/CSharp/Portable/GenerateMember/GenerateEnumMember/CSharpGenerateEnumMemberService.cs b/src/Features/CSharp/Portable/GenerateMember/GenerateEnumMember/CSharpGenerateEnumMemberService.cs index fa0af1bb9f04e..4b87fd8f46a2f 100644 --- a/src/Features/CSharp/Portable/GenerateMember/GenerateEnumMember/CSharpGenerateEnumMemberService.cs +++ b/src/Features/CSharp/Portable/GenerateMember/GenerateEnumMember/CSharpGenerateEnumMemberService.cs @@ -12,51 +12,50 @@ using Microsoft.CodeAnalysis.GenerateMember.GenerateEnumMember; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateEnumMember +namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateEnumMember; + +[ExportLanguageService(typeof(IGenerateEnumMemberService), LanguageNames.CSharp), Shared] +internal partial class CSharpGenerateEnumMemberService : + AbstractGenerateEnumMemberService { - [ExportLanguageService(typeof(IGenerateEnumMemberService), LanguageNames.CSharp), Shared] - internal partial class CSharpGenerateEnumMemberService : - AbstractGenerateEnumMemberService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGenerateEnumMemberService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGenerateEnumMemberService() - { - } + } - protected override bool IsIdentifierNameGeneration(SyntaxNode node) - => node is IdentifierNameSyntax; + protected override bool IsIdentifierNameGeneration(SyntaxNode node) + => node is IdentifierNameSyntax; - protected override bool TryInitializeIdentifierNameState( - SemanticDocument document, SimpleNameSyntax identifierName, CancellationToken cancellationToken, - out SyntaxToken identifierToken, out ExpressionSyntax simpleNameOrMemberAccessExpression) + protected override bool TryInitializeIdentifierNameState( + SemanticDocument document, SimpleNameSyntax identifierName, CancellationToken cancellationToken, + out SyntaxToken identifierToken, out ExpressionSyntax simpleNameOrMemberAccessExpression) + { + identifierToken = identifierName.Identifier; + if (identifierToken.ValueText != string.Empty && + !identifierName.IsVar) { - identifierToken = identifierName.Identifier; - if (identifierToken.ValueText != string.Empty && - !identifierName.IsVar) + simpleNameOrMemberAccessExpression = identifierName.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == identifierName + ? memberAccess + : identifierName; + + // If we're being invoked, then don't offer this, offer generate method instead. + // Note: we could offer to generate a field with a delegate type. However, that's + // very esoteric and probably not what most users want. + if (simpleNameOrMemberAccessExpression.Parent.Kind() + is SyntaxKind.InvocationExpression + or SyntaxKind.ObjectCreationExpression + or SyntaxKind.GotoStatement + or SyntaxKind.AliasQualifiedName) { - simpleNameOrMemberAccessExpression = identifierName.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == identifierName - ? memberAccess - : identifierName; - - // If we're being invoked, then don't offer this, offer generate method instead. - // Note: we could offer to generate a field with a delegate type. However, that's - // very esoteric and probably not what most users want. - if (simpleNameOrMemberAccessExpression.Parent.Kind() - is SyntaxKind.InvocationExpression - or SyntaxKind.ObjectCreationExpression - or SyntaxKind.GotoStatement - or SyntaxKind.AliasQualifiedName) - { - return false; - } - - return true; + return false; } - identifierToken = default; - simpleNameOrMemberAccessExpression = null; - return false; + return true; } + + identifierToken = default; + simpleNameOrMemberAccessExpression = null; + return false; } } diff --git a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpCommonGenerationServiceMethods.cs b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpCommonGenerationServiceMethods.cs index c09ee82e4938e..fde27166c3c6b 100644 --- a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpCommonGenerationServiceMethods.cs +++ b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpCommonGenerationServiceMethods.cs @@ -2,14 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateParameterizedMember +namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateParameterizedMember; + +internal static class CSharpCommonGenerationServiceMethods { - internal static class CSharpCommonGenerationServiceMethods - { - public static bool AreSpecialOptionsActive() - => false; + public static bool AreSpecialOptionsActive() + => false; - public static bool IsValidSymbol() - => false; - } + public static bool IsValidSymbol() + => false; } diff --git a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateConversionService.cs b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateConversionService.cs index d11ed53847e23..198271284b75b 100644 --- a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateConversionService.cs +++ b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateConversionService.cs @@ -18,212 +18,211 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateParameterizedMember +namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateParameterizedMember; + +[ExportLanguageService(typeof(IGenerateConversionService), LanguageNames.CSharp), Shared] +internal partial class CSharpGenerateConversionService : + AbstractGenerateConversionService { - [ExportLanguageService(typeof(IGenerateConversionService), LanguageNames.CSharp), Shared] - internal partial class CSharpGenerateConversionService : - AbstractGenerateConversionService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGenerateConversionService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGenerateConversionService() - { - } + } - protected override bool IsImplicitConversionGeneration(SyntaxNode node) - { - return node is ExpressionSyntax && - (node.Parent is AssignmentExpressionSyntax || node.Parent is EqualsValueClauseSyntax) && - !(node is CastExpressionSyntax) && - !(node is MemberAccessExpressionSyntax); - } + protected override bool IsImplicitConversionGeneration(SyntaxNode node) + { + return node is ExpressionSyntax && + (node.Parent is AssignmentExpressionSyntax || node.Parent is EqualsValueClauseSyntax) && + !(node is CastExpressionSyntax) && + !(node is MemberAccessExpressionSyntax); + } - protected override bool IsExplicitConversionGeneration(SyntaxNode node) - => node is CastExpressionSyntax; + protected override bool IsExplicitConversionGeneration(SyntaxNode node) + => node is CastExpressionSyntax; - protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) - => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); + protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) + => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); - protected override AbstractInvocationInfo CreateInvocationMethodInfo( - SemanticDocument document, AbstractGenerateParameterizedMemberService.State state) - { - return new CSharpGenerateParameterizedMemberService.InvocationExpressionInfo(document, state); - } + protected override AbstractInvocationInfo CreateInvocationMethodInfo( + SemanticDocument document, AbstractGenerateParameterizedMemberService.State state) + { + return new CSharpGenerateParameterizedMemberService.InvocationExpressionInfo(document, state); + } - protected override bool AreSpecialOptionsActive(SemanticModel semanticModel) - => CSharpCommonGenerationServiceMethods.AreSpecialOptionsActive(); + protected override bool AreSpecialOptionsActive(SemanticModel semanticModel) + => CSharpCommonGenerationServiceMethods.AreSpecialOptionsActive(); - protected override bool IsValidSymbol(ISymbol symbol, SemanticModel semanticModel) - => CSharpCommonGenerationServiceMethods.IsValidSymbol(); + protected override bool IsValidSymbol(ISymbol symbol, SemanticModel semanticModel) + => CSharpCommonGenerationServiceMethods.IsValidSymbol(); - protected override bool TryInitializeImplicitConversionState( - SemanticDocument document, - SyntaxNode expression, - ISet classInterfaceModuleStructTypes, - CancellationToken cancellationToken, - out SyntaxToken identifierToken, - out IMethodSymbol methodSymbol, - out INamedTypeSymbol typeToGenerateIn) + protected override bool TryInitializeImplicitConversionState( + SemanticDocument document, + SyntaxNode expression, + ISet classInterfaceModuleStructTypes, + CancellationToken cancellationToken, + out SyntaxToken identifierToken, + out IMethodSymbol methodSymbol, + out INamedTypeSymbol typeToGenerateIn) + { + if (TryGetConversionMethodAndTypeToGenerateIn(document, expression, classInterfaceModuleStructTypes, cancellationToken, out methodSymbol, out typeToGenerateIn)) { - if (TryGetConversionMethodAndTypeToGenerateIn(document, expression, classInterfaceModuleStructTypes, cancellationToken, out methodSymbol, out typeToGenerateIn)) - { - identifierToken = SyntaxFactory.Token( - default, - SyntaxKind.ImplicitKeyword, - WellKnownMemberNames.ImplicitConversionName, - WellKnownMemberNames.ImplicitConversionName, - default); - return true; - } - - identifierToken = default; - methodSymbol = null; - typeToGenerateIn = null; - return false; + identifierToken = SyntaxFactory.Token( + default, + SyntaxKind.ImplicitKeyword, + WellKnownMemberNames.ImplicitConversionName, + WellKnownMemberNames.ImplicitConversionName, + default); + return true; } - protected override bool TryInitializeExplicitConversionState( - SemanticDocument document, - SyntaxNode expression, - ISet classInterfaceModuleStructTypes, - CancellationToken cancellationToken, - out SyntaxToken identifierToken, - out IMethodSymbol methodSymbol, - out INamedTypeSymbol typeToGenerateIn) + identifierToken = default; + methodSymbol = null; + typeToGenerateIn = null; + return false; + } + + protected override bool TryInitializeExplicitConversionState( + SemanticDocument document, + SyntaxNode expression, + ISet classInterfaceModuleStructTypes, + CancellationToken cancellationToken, + out SyntaxToken identifierToken, + out IMethodSymbol methodSymbol, + out INamedTypeSymbol typeToGenerateIn) + { + if (TryGetConversionMethodAndTypeToGenerateIn(document, expression, classInterfaceModuleStructTypes, cancellationToken, out methodSymbol, out typeToGenerateIn)) { - if (TryGetConversionMethodAndTypeToGenerateIn(document, expression, classInterfaceModuleStructTypes, cancellationToken, out methodSymbol, out typeToGenerateIn)) - { - identifierToken = SyntaxFactory.Token( - default, - SyntaxKind.ImplicitKeyword, - WellKnownMemberNames.ExplicitConversionName, - WellKnownMemberNames.ExplicitConversionName, - default); - return true; - } - - identifierToken = default; - methodSymbol = null; - typeToGenerateIn = null; - return false; + identifierToken = SyntaxFactory.Token( + default, + SyntaxKind.ImplicitKeyword, + WellKnownMemberNames.ExplicitConversionName, + WellKnownMemberNames.ExplicitConversionName, + default); + return true; } - private static bool TryGetConversionMethodAndTypeToGenerateIn( - SemanticDocument document, - SyntaxNode expression, - ISet classInterfaceModuleStructTypes, - CancellationToken cancellationToken, - out IMethodSymbol methodSymbol, - out INamedTypeSymbol typeToGenerateIn) + identifierToken = default; + methodSymbol = null; + typeToGenerateIn = null; + return false; + } + + private static bool TryGetConversionMethodAndTypeToGenerateIn( + SemanticDocument document, + SyntaxNode expression, + ISet classInterfaceModuleStructTypes, + CancellationToken cancellationToken, + out IMethodSymbol methodSymbol, + out INamedTypeSymbol typeToGenerateIn) + { + if (expression is CastExpressionSyntax castExpression) { - if (expression is CastExpressionSyntax castExpression) - { - return TryGetExplicitConversionMethodAndTypeToGenerateIn( - document, - castExpression, - classInterfaceModuleStructTypes, - cancellationToken, - out methodSymbol, - out typeToGenerateIn); - } - - return TryGetImplicitConversionMethodAndTypeToGenerateIn( - document, - expression, - classInterfaceModuleStructTypes, + return TryGetExplicitConversionMethodAndTypeToGenerateIn( + document, + castExpression, + classInterfaceModuleStructTypes, cancellationToken, - out methodSymbol, - out typeToGenerateIn); + out methodSymbol, + out typeToGenerateIn); } - private static bool TryGetExplicitConversionMethodAndTypeToGenerateIn( - SemanticDocument document, - CastExpressionSyntax castExpression, - ISet classInterfaceModuleStructTypes, - CancellationToken cancellationToken, - out IMethodSymbol methodSymbol, - out INamedTypeSymbol typeToGenerateIn) - { - methodSymbol = null; - typeToGenerateIn = document.SemanticModel.GetTypeInfo(castExpression.Type, cancellationToken).Type as INamedTypeSymbol; - if (typeToGenerateIn == null - || document.SemanticModel.GetTypeInfo(castExpression.Expression, cancellationToken).Type is not INamedTypeSymbol parameterSymbol - || typeToGenerateIn.IsErrorType() - || parameterSymbol.IsErrorType()) - { - return false; - } - - methodSymbol = GenerateMethodSymbol(typeToGenerateIn, parameterSymbol); - - if (!ValidateTypeToGenerateIn( - typeToGenerateIn, - true, - classInterfaceModuleStructTypes)) - { - typeToGenerateIn = parameterSymbol; - } + return TryGetImplicitConversionMethodAndTypeToGenerateIn( + document, + expression, + classInterfaceModuleStructTypes, + cancellationToken, + out methodSymbol, + out typeToGenerateIn); + } - return true; + private static bool TryGetExplicitConversionMethodAndTypeToGenerateIn( + SemanticDocument document, + CastExpressionSyntax castExpression, + ISet classInterfaceModuleStructTypes, + CancellationToken cancellationToken, + out IMethodSymbol methodSymbol, + out INamedTypeSymbol typeToGenerateIn) + { + methodSymbol = null; + typeToGenerateIn = document.SemanticModel.GetTypeInfo(castExpression.Type, cancellationToken).Type as INamedTypeSymbol; + if (typeToGenerateIn == null + || document.SemanticModel.GetTypeInfo(castExpression.Expression, cancellationToken).Type is not INamedTypeSymbol parameterSymbol + || typeToGenerateIn.IsErrorType() + || parameterSymbol.IsErrorType()) + { + return false; } - private static bool TryGetImplicitConversionMethodAndTypeToGenerateIn( - SemanticDocument document, - SyntaxNode expression, - ISet classInterfaceModuleStructTypes, - CancellationToken cancellationToken, - out IMethodSymbol methodSymbol, - out INamedTypeSymbol typeToGenerateIn) + methodSymbol = GenerateMethodSymbol(typeToGenerateIn, parameterSymbol); + + if (!ValidateTypeToGenerateIn( + typeToGenerateIn, + true, + classInterfaceModuleStructTypes)) { - methodSymbol = null; - typeToGenerateIn = document.SemanticModel.GetTypeInfo(expression, cancellationToken).ConvertedType as INamedTypeSymbol; - if (typeToGenerateIn == null - || document.SemanticModel.GetTypeInfo(expression, cancellationToken).Type is not INamedTypeSymbol parameterSymbol - || typeToGenerateIn.IsErrorType() - || parameterSymbol.IsErrorType()) - { - return false; - } - - methodSymbol = GenerateMethodSymbol(typeToGenerateIn, parameterSymbol); - - if (!ValidateTypeToGenerateIn( - typeToGenerateIn, - true, - classInterfaceModuleStructTypes)) - { - typeToGenerateIn = parameterSymbol; - } + typeToGenerateIn = parameterSymbol; + } - return true; + return true; + } + + private static bool TryGetImplicitConversionMethodAndTypeToGenerateIn( + SemanticDocument document, + SyntaxNode expression, + ISet classInterfaceModuleStructTypes, + CancellationToken cancellationToken, + out IMethodSymbol methodSymbol, + out INamedTypeSymbol typeToGenerateIn) + { + methodSymbol = null; + typeToGenerateIn = document.SemanticModel.GetTypeInfo(expression, cancellationToken).ConvertedType as INamedTypeSymbol; + if (typeToGenerateIn == null + || document.SemanticModel.GetTypeInfo(expression, cancellationToken).Type is not INamedTypeSymbol parameterSymbol + || typeToGenerateIn.IsErrorType() + || parameterSymbol.IsErrorType()) + { + return false; } - private static IMethodSymbol GenerateMethodSymbol( - INamedTypeSymbol typeToGenerateIn, INamedTypeSymbol parameterSymbol) + methodSymbol = GenerateMethodSymbol(typeToGenerateIn, parameterSymbol); + + if (!ValidateTypeToGenerateIn( + typeToGenerateIn, + true, + classInterfaceModuleStructTypes)) { - // Remove any generic parameters - if (typeToGenerateIn.IsGenericType) - { - typeToGenerateIn = typeToGenerateIn.ConstructUnboundGenericType().ConstructedFrom; - } - - return CodeGenerationSymbolFactory.CreateMethodSymbol( - attributes: [], - accessibility: default, - modifiers: default, - returnType: typeToGenerateIn, - refKind: RefKind.None, - explicitInterfaceImplementations: default, - name: null, - typeParameters: [], - parameters: [CodeGenerationSymbolFactory.CreateParameterSymbol(parameterSymbol, "v")], - methodKind: MethodKind.Conversion); + typeToGenerateIn = parameterSymbol; } - protected override string GetImplicitConversionDisplayText(AbstractGenerateParameterizedMemberService.State state) - => string.Format(CSharpFeaturesResources.Generate_implicit_conversion_operator_in_0, state.TypeToGenerateIn.Name); + return true; + } - protected override string GetExplicitConversionDisplayText(AbstractGenerateParameterizedMemberService.State state) - => string.Format(CSharpFeaturesResources.Generate_explicit_conversion_operator_in_0, state.TypeToGenerateIn.Name); + private static IMethodSymbol GenerateMethodSymbol( + INamedTypeSymbol typeToGenerateIn, INamedTypeSymbol parameterSymbol) + { + // Remove any generic parameters + if (typeToGenerateIn.IsGenericType) + { + typeToGenerateIn = typeToGenerateIn.ConstructUnboundGenericType().ConstructedFrom; + } + + return CodeGenerationSymbolFactory.CreateMethodSymbol( + attributes: [], + accessibility: default, + modifiers: default, + returnType: typeToGenerateIn, + refKind: RefKind.None, + explicitInterfaceImplementations: default, + name: null, + typeParameters: [], + parameters: [CodeGenerationSymbolFactory.CreateParameterSymbol(parameterSymbol, "v")], + methodKind: MethodKind.Conversion); } + + protected override string GetImplicitConversionDisplayText(AbstractGenerateParameterizedMemberService.State state) + => string.Format(CSharpFeaturesResources.Generate_implicit_conversion_operator_in_0, state.TypeToGenerateIn.Name); + + protected override string GetExplicitConversionDisplayText(AbstractGenerateParameterizedMemberService.State state) + => string.Format(CSharpFeaturesResources.Generate_explicit_conversion_operator_in_0, state.TypeToGenerateIn.Name); } diff --git a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateDeconstructMethodService.cs b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateDeconstructMethodService.cs index 2f2561aa10c37..92cb73480134c 100644 --- a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateDeconstructMethodService.cs +++ b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateDeconstructMethodService.cs @@ -17,57 +17,56 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateMethod +namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateMethod; + +[ExportLanguageService(typeof(IGenerateDeconstructMemberService), LanguageNames.CSharp), Shared] +internal sealed class CSharpGenerateDeconstructMethodService : + AbstractGenerateDeconstructMethodService { - [ExportLanguageService(typeof(IGenerateDeconstructMemberService), LanguageNames.CSharp), Shared] - internal sealed class CSharpGenerateDeconstructMethodService : - AbstractGenerateDeconstructMethodService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGenerateDeconstructMethodService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGenerateDeconstructMethodService() - { - } + } - protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) - => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); + protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) + => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); - protected override AbstractInvocationInfo CreateInvocationMethodInfo(SemanticDocument document, AbstractGenerateParameterizedMemberService.State state) - => new CSharpGenerateParameterizedMemberService.InvocationExpressionInfo(document, state); + protected override AbstractInvocationInfo CreateInvocationMethodInfo(SemanticDocument document, AbstractGenerateParameterizedMemberService.State state) + => new CSharpGenerateParameterizedMemberService.InvocationExpressionInfo(document, state); - protected override bool AreSpecialOptionsActive(SemanticModel semanticModel) - => CSharpCommonGenerationServiceMethods.AreSpecialOptionsActive(); + protected override bool AreSpecialOptionsActive(SemanticModel semanticModel) + => CSharpCommonGenerationServiceMethods.AreSpecialOptionsActive(); - protected override bool IsValidSymbol(ISymbol symbol, SemanticModel semanticModel) - => CSharpCommonGenerationServiceMethods.IsValidSymbol(); + protected override bool IsValidSymbol(ISymbol symbol, SemanticModel semanticModel) + => CSharpCommonGenerationServiceMethods.IsValidSymbol(); - public override ImmutableArray TryMakeParameters(SemanticModel semanticModel, SyntaxNode target, CancellationToken cancellationToken) + public override ImmutableArray TryMakeParameters(SemanticModel semanticModel, SyntaxNode target, CancellationToken cancellationToken) + { + // For `if (this is C(0, 0))`, we 'll generate `Deconstruct(out int v1, out int v2)` + if (target is PositionalPatternClauseSyntax positionalPattern) { - // For `if (this is C(0, 0))`, we 'll generate `Deconstruct(out int v1, out int v2)` - if (target is PositionalPatternClauseSyntax positionalPattern) - { - // Code in GenerateDeconstructMethodCodeFixProvider has already checked that all subpatterns are ConstantPatternSyntax. - var namesBuilder = positionalPattern.Subpatterns.SelectAsArray(sub => - semanticModel.GenerateNameForExpression(((ConstantPatternSyntax)sub.Pattern).Expression, capitalize: false, cancellationToken)); + // Code in GenerateDeconstructMethodCodeFixProvider has already checked that all subpatterns are ConstantPatternSyntax. + var namesBuilder = positionalPattern.Subpatterns.SelectAsArray(sub => + semanticModel.GenerateNameForExpression(((ConstantPatternSyntax)sub.Pattern).Expression, capitalize: false, cancellationToken)); - var names = NameGenerator.EnsureUniqueness(namesBuilder); + var names = NameGenerator.EnsureUniqueness(namesBuilder); - return names.SelectAsArray((name, i) => CodeGenerationSymbolFactory.CreateParameterSymbol( - attributes: default, - refKind: RefKind.Out, - isParams: false, - type: semanticModel.GetTypeInfo(((ConstantPatternSyntax)positionalPattern.Subpatterns[i].Pattern).Expression, cancellationToken).Type ?? semanticModel.Compilation.GetSpecialType(SpecialType.System_Object), - name: name)); - } - else - { - var targetType = semanticModel.GetTypeInfo(target, cancellationToken: cancellationToken).Type; - if (targetType is not INamedTypeSymbol { IsTupleType: true, TupleElements: var tupleElements }) - return default; + return names.SelectAsArray((name, i) => CodeGenerationSymbolFactory.CreateParameterSymbol( + attributes: default, + refKind: RefKind.Out, + isParams: false, + type: semanticModel.GetTypeInfo(((ConstantPatternSyntax)positionalPattern.Subpatterns[i].Pattern).Expression, cancellationToken).Type ?? semanticModel.Compilation.GetSpecialType(SpecialType.System_Object), + name: name)); + } + else + { + var targetType = semanticModel.GetTypeInfo(target, cancellationToken: cancellationToken).Type; + if (targetType is not INamedTypeSymbol { IsTupleType: true, TupleElements: var tupleElements }) + return default; - return tupleElements.SelectAsArray(element => CodeGenerationSymbolFactory.CreateParameterSymbol( - attributes: default, RefKind.Out, isParams: false, element.Type, element.Name)); - } + return tupleElements.SelectAsArray(element => CodeGenerationSymbolFactory.CreateParameterSymbol( + attributes: default, RefKind.Out, isParams: false, element.Type, element.Name)); } } } diff --git a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateMethodService.cs b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateMethodService.cs index 7ddf9d0f434ca..9fa97b3e47cb2 100644 --- a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateMethodService.cs +++ b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateMethodService.cs @@ -18,151 +18,150 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateMethod +namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateMethod; + +[ExportLanguageService(typeof(IGenerateParameterizedMemberService), LanguageNames.CSharp), Shared] +internal sealed class CSharpGenerateMethodService : + AbstractGenerateMethodService { - [ExportLanguageService(typeof(IGenerateParameterizedMemberService), LanguageNames.CSharp), Shared] - internal sealed class CSharpGenerateMethodService : - AbstractGenerateMethodService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGenerateMethodService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGenerateMethodService() - { - } + } - protected override bool IsExplicitInterfaceGeneration(SyntaxNode node) - => node is MethodDeclarationSyntax; + protected override bool IsExplicitInterfaceGeneration(SyntaxNode node) + => node is MethodDeclarationSyntax; - protected override bool IsSimpleNameGeneration(SyntaxNode node) - => node is SimpleNameSyntax; + protected override bool IsSimpleNameGeneration(SyntaxNode node) + => node is SimpleNameSyntax; - protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) - => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); + protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) + => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); - protected override AbstractInvocationInfo CreateInvocationMethodInfo(SemanticDocument document, AbstractGenerateParameterizedMemberService.State state) - => new CSharpGenerateParameterizedMemberService.InvocationExpressionInfo(document, state); + protected override AbstractInvocationInfo CreateInvocationMethodInfo(SemanticDocument document, AbstractGenerateParameterizedMemberService.State state) + => new CSharpGenerateParameterizedMemberService.InvocationExpressionInfo(document, state); - protected override bool AreSpecialOptionsActive(SemanticModel semanticModel) - => CSharpCommonGenerationServiceMethods.AreSpecialOptionsActive(); + protected override bool AreSpecialOptionsActive(SemanticModel semanticModel) + => CSharpCommonGenerationServiceMethods.AreSpecialOptionsActive(); - protected override bool IsValidSymbol(ISymbol symbol, SemanticModel semanticModel) - => CSharpCommonGenerationServiceMethods.IsValidSymbol(); + protected override bool IsValidSymbol(ISymbol symbol, SemanticModel semanticModel) + => CSharpCommonGenerationServiceMethods.IsValidSymbol(); - protected override bool TryInitializeExplicitInterfaceState( - SemanticDocument document, - SyntaxNode node, - CancellationToken cancellationToken, - out SyntaxToken identifierToken, - out IMethodSymbol methodSymbol, - out INamedTypeSymbol typeToGenerateIn) - { - var methodDeclaration = (MethodDeclarationSyntax)node; - identifierToken = methodDeclaration.Identifier; + protected override bool TryInitializeExplicitInterfaceState( + SemanticDocument document, + SyntaxNode node, + CancellationToken cancellationToken, + out SyntaxToken identifierToken, + out IMethodSymbol methodSymbol, + out INamedTypeSymbol typeToGenerateIn) + { + var methodDeclaration = (MethodDeclarationSyntax)node; + identifierToken = methodDeclaration.Identifier; - if (methodDeclaration.ExplicitInterfaceSpecifier != null && - !methodDeclaration.ParameterList.OpenParenToken.IsMissing && - !methodDeclaration.ParameterList.CloseParenToken.IsMissing) + if (methodDeclaration.ExplicitInterfaceSpecifier != null && + !methodDeclaration.ParameterList.OpenParenToken.IsMissing && + !methodDeclaration.ParameterList.CloseParenToken.IsMissing) + { + var semanticModel = document.SemanticModel; + methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken); + if (methodSymbol != null && !methodSymbol.ExplicitInterfaceImplementations.Any()) { - var semanticModel = document.SemanticModel; - methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken); - if (methodSymbol != null && !methodSymbol.ExplicitInterfaceImplementations.Any()) - { - var semanticInfo = semanticModel.GetTypeInfo(methodDeclaration.ExplicitInterfaceSpecifier.Name, cancellationToken); - typeToGenerateIn = semanticInfo.Type as INamedTypeSymbol; - return typeToGenerateIn != null; - } + var semanticInfo = semanticModel.GetTypeInfo(methodDeclaration.ExplicitInterfaceSpecifier.Name, cancellationToken); + typeToGenerateIn = semanticInfo.Type as INamedTypeSymbol; + return typeToGenerateIn != null; } - - identifierToken = default; - methodSymbol = null; - typeToGenerateIn = null; - return false; } - protected override bool TryInitializeSimpleNameState( - SemanticDocument document, - SimpleNameSyntax simpleName, - CancellationToken cancellationToken, - out SyntaxToken identifierToken, - out ExpressionSyntax simpleNameOrMemberAccessExpression, - out InvocationExpressionSyntax invocationExpressionOpt, - out bool isInConditionalAccessExpression) + identifierToken = default; + methodSymbol = null; + typeToGenerateIn = null; + return false; + } + + protected override bool TryInitializeSimpleNameState( + SemanticDocument document, + SimpleNameSyntax simpleName, + CancellationToken cancellationToken, + out SyntaxToken identifierToken, + out ExpressionSyntax simpleNameOrMemberAccessExpression, + out InvocationExpressionSyntax invocationExpressionOpt, + out bool isInConditionalAccessExpression) + { + identifierToken = simpleName.Identifier; + + var memberAccess = simpleName?.Parent as MemberAccessExpressionSyntax; + var conditionalMemberAccess = simpleName?.Parent?.Parent?.Parent as ConditionalAccessExpressionSyntax; + var inConditionalMemberAccess = conditionalMemberAccess != null; + if (memberAccess != null) + { + simpleNameOrMemberAccessExpression = memberAccess; + } + else if (inConditionalMemberAccess) + { + simpleNameOrMemberAccessExpression = conditionalMemberAccess; + } + else { - identifierToken = simpleName.Identifier; + simpleNameOrMemberAccessExpression = simpleName; + } - var memberAccess = simpleName?.Parent as MemberAccessExpressionSyntax; - var conditionalMemberAccess = simpleName?.Parent?.Parent?.Parent as ConditionalAccessExpressionSyntax; - var inConditionalMemberAccess = conditionalMemberAccess != null; - if (memberAccess != null) + if (memberAccess == null || memberAccess.Name == simpleName) + { + if (simpleNameOrMemberAccessExpression.IsParentKind(SyntaxKind.InvocationExpression, out invocationExpressionOpt)) { - simpleNameOrMemberAccessExpression = memberAccess; + isInConditionalAccessExpression = inConditionalMemberAccess; + return !invocationExpressionOpt.ArgumentList.CloseParenToken.IsMissing; } - else if (inConditionalMemberAccess) + // We need to check that the tree is structured like so: + // ConditionalAccessExpressionSyntax + // -> InvocationExpressionSyntax + // -> MemberBindingExpressionSyntax + // and that the name at the end of this expression matches the simple name we were given + else if ((((simpleNameOrMemberAccessExpression as ConditionalAccessExpressionSyntax) + ?.WhenNotNull as InvocationExpressionSyntax) + ?.Expression as MemberBindingExpressionSyntax) + ?.Name == simpleName) { - simpleNameOrMemberAccessExpression = conditionalMemberAccess; + invocationExpressionOpt = (InvocationExpressionSyntax)((ConditionalAccessExpressionSyntax)simpleNameOrMemberAccessExpression).WhenNotNull; + isInConditionalAccessExpression = inConditionalMemberAccess; + return !invocationExpressionOpt.ArgumentList.CloseParenToken.IsMissing; } - else + else if (simpleName.IsKind(SyntaxKind.IdentifierName)) { - simpleNameOrMemberAccessExpression = simpleName; - } + // If we don't have an invocation node, then see if we can infer a delegate in + // this location. Check if this is a place where a delegate can go. Only do this + // for identifier names. for now. It gets really funky if you have to deal with + // a generic name here. - if (memberAccess == null || memberAccess.Name == simpleName) - { - if (simpleNameOrMemberAccessExpression.IsParentKind(SyntaxKind.InvocationExpression, out invocationExpressionOpt)) + // Can't assign into a method. + if (!simpleNameOrMemberAccessExpression.IsLeftSideOfAnyAssignExpression()) { + invocationExpressionOpt = null; isInConditionalAccessExpression = inConditionalMemberAccess; - return !invocationExpressionOpt.ArgumentList.CloseParenToken.IsMissing; - } - // We need to check that the tree is structured like so: - // ConditionalAccessExpressionSyntax - // -> InvocationExpressionSyntax - // -> MemberBindingExpressionSyntax - // and that the name at the end of this expression matches the simple name we were given - else if ((((simpleNameOrMemberAccessExpression as ConditionalAccessExpressionSyntax) - ?.WhenNotNull as InvocationExpressionSyntax) - ?.Expression as MemberBindingExpressionSyntax) - ?.Name == simpleName) - { - invocationExpressionOpt = (InvocationExpressionSyntax)((ConditionalAccessExpressionSyntax)simpleNameOrMemberAccessExpression).WhenNotNull; - isInConditionalAccessExpression = inConditionalMemberAccess; - return !invocationExpressionOpt.ArgumentList.CloseParenToken.IsMissing; - } - else if (simpleName.IsKind(SyntaxKind.IdentifierName)) - { - // If we don't have an invocation node, then see if we can infer a delegate in - // this location. Check if this is a place where a delegate can go. Only do this - // for identifier names. for now. It gets really funky if you have to deal with - // a generic name here. - - // Can't assign into a method. - if (!simpleNameOrMemberAccessExpression.IsLeftSideOfAnyAssignExpression()) - { - invocationExpressionOpt = null; - isInConditionalAccessExpression = inConditionalMemberAccess; - return true; - } + return true; } } - - identifierToken = default; - simpleNameOrMemberAccessExpression = null; - invocationExpressionOpt = null; - isInConditionalAccessExpression = false; - return false; } - protected override ITypeSymbol DetermineReturnTypeForSimpleNameOrMemberAccessExpression( - ITypeInferenceService typeInferenceService, - SemanticModel semanticModel, - ExpressionSyntax expression, - CancellationToken cancellationToken) - { - if (semanticModel.SyntaxTree.IsNameOfContext(expression.SpanStart, semanticModel, cancellationToken)) - { - return typeInferenceService.InferType(semanticModel, expression, true, cancellationToken); - } + identifierToken = default; + simpleNameOrMemberAccessExpression = null; + invocationExpressionOpt = null; + isInConditionalAccessExpression = false; + return false; + } - return null; + protected override ITypeSymbol DetermineReturnTypeForSimpleNameOrMemberAccessExpression( + ITypeInferenceService typeInferenceService, + SemanticModel semanticModel, + ExpressionSyntax expression, + CancellationToken cancellationToken) + { + if (semanticModel.SyntaxTree.IsNameOfContext(expression.SpanStart, semanticModel, cancellationToken)) + { + return typeInferenceService.InferType(semanticModel, expression, true, cancellationToken); } + + return null; } } diff --git a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateParameterizedMemberService.cs b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateParameterizedMemberService.cs index 903d6d376c704..06b21c73514f7 100644 --- a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateParameterizedMemberService.cs +++ b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateParameterizedMemberService.cs @@ -20,146 +20,145 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateMethod +namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateMethod; + +internal abstract class CSharpGenerateParameterizedMemberService : AbstractGenerateParameterizedMemberService + where TService : AbstractGenerateParameterizedMemberService { - internal abstract class CSharpGenerateParameterizedMemberService : AbstractGenerateParameterizedMemberService - where TService : AbstractGenerateParameterizedMemberService + internal sealed class InvocationExpressionInfo(SemanticDocument document, State state) : AbstractInvocationInfo(document, state) { - internal sealed class InvocationExpressionInfo(SemanticDocument document, State state) : AbstractInvocationInfo(document, state) + private readonly InvocationExpressionSyntax _invocationExpression = state.InvocationExpressionOpt; + + protected override ImmutableArray DetermineParameterNames(CancellationToken cancellationToken) { - private readonly InvocationExpressionSyntax _invocationExpression = state.InvocationExpressionOpt; + return Document.SemanticModel.GenerateParameterNames( + _invocationExpression.ArgumentList, cancellationToken); + } - protected override ImmutableArray DetermineParameterNames(CancellationToken cancellationToken) - { - return Document.SemanticModel.GenerateParameterNames( - _invocationExpression.ArgumentList, cancellationToken); - } + protected override RefKind DetermineRefKind(CancellationToken cancellationToken) + => _invocationExpression.IsParentKind(SyntaxKind.RefExpression) ? RefKind.Ref : RefKind.None; - protected override RefKind DetermineRefKind(CancellationToken cancellationToken) - => _invocationExpression.IsParentKind(SyntaxKind.RefExpression) ? RefKind.Ref : RefKind.None; + protected override ITypeSymbol DetermineReturnTypeWorker(CancellationToken cancellationToken) + { + // Defer to the type inferrer to figure out what the return type of this new method + // should be. + var typeInference = Document.Document.GetLanguageService(); + var inferredType = typeInference.InferType( + Document.SemanticModel, _invocationExpression, objectAsDefault: true, + name: State.IdentifierToken.ValueText, cancellationToken); + return inferredType; + } - protected override ITypeSymbol DetermineReturnTypeWorker(CancellationToken cancellationToken) + protected override ImmutableArray GetCapturedTypeParameters(CancellationToken cancellationToken) + { + var result = new List(); + var semanticModel = Document.SemanticModel; + foreach (var argument in _invocationExpression.ArgumentList.Arguments) { - // Defer to the type inferrer to figure out what the return type of this new method - // should be. - var typeInference = Document.Document.GetLanguageService(); - var inferredType = typeInference.InferType( - Document.SemanticModel, _invocationExpression, objectAsDefault: true, - name: State.IdentifierToken.ValueText, cancellationToken); - return inferredType; + var type = argument.DetermineParameterType(semanticModel, cancellationToken); + type.GetReferencedTypeParameters(result); } - protected override ImmutableArray GetCapturedTypeParameters(CancellationToken cancellationToken) + return result.ToImmutableArray(); + } + + protected override ImmutableArray GenerateTypeParameters(CancellationToken cancellationToken) + { + // Generate dummy type parameter names for a generic method. If the user is inside a + // generic method, and calls a generic method with type arguments from the outer + // method, then use those same names for the generated type parameters. + // + // TODO(cyrusn): If we do capture method type variables, then we should probably + // capture their constraints as well. + var genericName = (GenericNameSyntax)State.SimpleNameOpt; + var semanticModel = Document.SemanticModel; + + if (genericName.TypeArgumentList.Arguments.Count == 1) { - var result = new List(); - var semanticModel = Document.SemanticModel; - foreach (var argument in _invocationExpression.ArgumentList.Arguments) - { - var type = argument.DetermineParameterType(semanticModel, cancellationToken); - type.GetReferencedTypeParameters(result); - } + var typeParameter = GetUniqueTypeParameter( + genericName.TypeArgumentList.Arguments.First(), + s => !State.TypeToGenerateIn.GetAllTypeParameters().Any(static (t, s) => t.Name == s, s), + cancellationToken); - return result.ToImmutableArray(); + return [typeParameter]; } - - protected override ImmutableArray GenerateTypeParameters(CancellationToken cancellationToken) + else { - // Generate dummy type parameter names for a generic method. If the user is inside a - // generic method, and calls a generic method with type arguments from the outer - // method, then use those same names for the generated type parameters. - // - // TODO(cyrusn): If we do capture method type variables, then we should probably - // capture their constraints as well. - var genericName = (GenericNameSyntax)State.SimpleNameOpt; - var semanticModel = Document.SemanticModel; - - if (genericName.TypeArgumentList.Arguments.Count == 1) + using var _ = ArrayBuilder.GetInstance(out var list); + + var usedIdentifiers = new HashSet { "T" }; + foreach (var type in genericName.TypeArgumentList.Arguments) { var typeParameter = GetUniqueTypeParameter( - genericName.TypeArgumentList.Arguments.First(), - s => !State.TypeToGenerateIn.GetAllTypeParameters().Any(static (t, s) => t.Name == s, s), + type, + s => !usedIdentifiers.Contains(s) && !State.TypeToGenerateIn.GetAllTypeParameters().Any(static (t, s) => t.Name == s, s), cancellationToken); - return [typeParameter]; - } - else - { - using var _ = ArrayBuilder.GetInstance(out var list); - - var usedIdentifiers = new HashSet { "T" }; - foreach (var type in genericName.TypeArgumentList.Arguments) - { - var typeParameter = GetUniqueTypeParameter( - type, - s => !usedIdentifiers.Contains(s) && !State.TypeToGenerateIn.GetAllTypeParameters().Any(static (t, s) => t.Name == s, s), - cancellationToken); - - usedIdentifiers.Add(typeParameter.Name); - - list.Add(typeParameter); - } + usedIdentifiers.Add(typeParameter.Name); - return list.ToImmutable(); + list.Add(typeParameter); } + + return list.ToImmutable(); } + } + + private ITypeParameterSymbol GetUniqueTypeParameter( + TypeSyntax type, + Func isUnique, + CancellationToken cancellationToken) + { + var methodTypeParameter = GetMethodTypeParameter(type, cancellationToken); + return methodTypeParameter ?? CodeGenerationSymbolFactory.CreateTypeParameterSymbol(NameGenerator.GenerateUniqueName("T", isUnique)); + } - private ITypeParameterSymbol GetUniqueTypeParameter( - TypeSyntax type, - Func isUnique, - CancellationToken cancellationToken) + private ITypeParameterSymbol GetMethodTypeParameter(TypeSyntax type, CancellationToken cancellationToken) + { + if (type is IdentifierNameSyntax) { - var methodTypeParameter = GetMethodTypeParameter(type, cancellationToken); - return methodTypeParameter ?? CodeGenerationSymbolFactory.CreateTypeParameterSymbol(NameGenerator.GenerateUniqueName("T", isUnique)); + var info = Document.SemanticModel.GetTypeInfo(type, cancellationToken); + if (info.Type is ITypeParameterSymbol { TypeParameterKind: TypeParameterKind.Method } typeParameter) + return typeParameter; } - private ITypeParameterSymbol GetMethodTypeParameter(TypeSyntax type, CancellationToken cancellationToken) - { - if (type is IdentifierNameSyntax) - { - var info = Document.SemanticModel.GetTypeInfo(type, cancellationToken); - if (info.Type is ITypeParameterSymbol { TypeParameterKind: TypeParameterKind.Method } typeParameter) - return typeParameter; - } + return null; + } - return null; - } + protected override ImmutableArray DetermineParameterModifiers(CancellationToken cancellationToken) + => _invocationExpression.ArgumentList.Arguments.Select(a => a.GetRefKind()).ToImmutableArray(); - protected override ImmutableArray DetermineParameterModifiers(CancellationToken cancellationToken) - => _invocationExpression.ArgumentList.Arguments.Select(a => a.GetRefKind()).ToImmutableArray(); + protected override ImmutableArray DetermineParameterTypes(CancellationToken cancellationToken) + => _invocationExpression.ArgumentList.Arguments.Select(a => DetermineParameterType(a, cancellationToken)).ToImmutableArray(); - protected override ImmutableArray DetermineParameterTypes(CancellationToken cancellationToken) - => _invocationExpression.ArgumentList.Arguments.Select(a => DetermineParameterType(a, cancellationToken)).ToImmutableArray(); + private ITypeSymbol DetermineParameterType(ArgumentSyntax argument, CancellationToken cancellationToken) + => argument.DetermineParameterType(Document.SemanticModel, cancellationToken); - private ITypeSymbol DetermineParameterType(ArgumentSyntax argument, CancellationToken cancellationToken) - => argument.DetermineParameterType(Document.SemanticModel, cancellationToken); + protected override ImmutableArray DetermineParameterOptionality(CancellationToken cancellationToken) + => _invocationExpression.ArgumentList.Arguments.Select(a => false).ToImmutableArray(); - protected override ImmutableArray DetermineParameterOptionality(CancellationToken cancellationToken) - => _invocationExpression.ArgumentList.Arguments.Select(a => false).ToImmutableArray(); + protected override bool IsIdentifierName() + => State.SimpleNameOpt.Kind() == SyntaxKind.IdentifierName; - protected override bool IsIdentifierName() - => State.SimpleNameOpt.Kind() == SyntaxKind.IdentifierName; + protected override bool IsImplicitReferenceConversion(Compilation compilation, ITypeSymbol sourceType, ITypeSymbol targetType) + { + var conversion = compilation.ClassifyConversion(sourceType, targetType); + return conversion.IsImplicit && conversion.IsReference; + } - protected override bool IsImplicitReferenceConversion(Compilation compilation, ITypeSymbol sourceType, ITypeSymbol targetType) - { - var conversion = compilation.ClassifyConversion(sourceType, targetType); - return conversion.IsImplicit && conversion.IsReference; - } + protected override ImmutableArray DetermineTypeArguments(CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var result); - protected override ImmutableArray DetermineTypeArguments(CancellationToken cancellationToken) + if (State.SimpleNameOpt is GenericNameSyntax genericName) { - using var _ = ArrayBuilder.GetInstance(out var result); - - if (State.SimpleNameOpt is GenericNameSyntax genericName) + foreach (var typeArgument in genericName.TypeArgumentList.Arguments) { - foreach (var typeArgument in genericName.TypeArgumentList.Arguments) - { - var typeInfo = Document.SemanticModel.GetTypeInfo(typeArgument, cancellationToken); - result.Add(typeInfo.Type); - } + var typeInfo = Document.SemanticModel.GetTypeInfo(typeArgument, cancellationToken); + result.Add(typeInfo.Type); } - - return result.ToImmutable(); } + + return result.ToImmutable(); } } } diff --git a/src/Features/CSharp/Portable/GenerateMember/GenerateVariable/CSharpGenerateVariableService.cs b/src/Features/CSharp/Portable/GenerateMember/GenerateVariable/CSharpGenerateVariableService.cs index 993ed82f427c0..dff654471beb5 100644 --- a/src/Features/CSharp/Portable/GenerateMember/GenerateVariable/CSharpGenerateVariableService.cs +++ b/src/Features/CSharp/Portable/GenerateMember/GenerateVariable/CSharpGenerateVariableService.cs @@ -16,207 +16,206 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateVariable +namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateVariable; + +[ExportLanguageService(typeof(IGenerateVariableService), LanguageNames.CSharp), Shared] +internal partial class CSharpGenerateVariableService : + AbstractGenerateVariableService { - [ExportLanguageService(typeof(IGenerateVariableService), LanguageNames.CSharp), Shared] - internal partial class CSharpGenerateVariableService : - AbstractGenerateVariableService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGenerateVariableService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGenerateVariableService() - { - } + } - protected override bool IsExplicitInterfaceGeneration(SyntaxNode node) - => node is PropertyDeclarationSyntax; + protected override bool IsExplicitInterfaceGeneration(SyntaxNode node) + => node is PropertyDeclarationSyntax; - protected override bool IsIdentifierNameGeneration(SyntaxNode node) - => node is IdentifierNameSyntax identifierName && !IsProbablySyntacticConstruct(identifierName.Identifier); + protected override bool IsIdentifierNameGeneration(SyntaxNode node) + => node is IdentifierNameSyntax identifierName && !IsProbablySyntacticConstruct(identifierName.Identifier); - private static bool IsProbablySyntacticConstruct(SyntaxToken token) - { - // Technically all C# contextual keywords are valid member names. - // However some of them start various syntactic constructs - // and we don't want to show "Generate " codefix for them: - // 1. "from" starts LINQ expression - // 2. "nameof" is probably nameof(some_name) - // 3. "async" can start a delegate declaration - // 4. "await" starts await expression - // 5. "var" is used in constructions like "var x = ..." - // The list can be expanded in the future if necessary - // This method tells if the given SyntaxToken is one of the cases above - var contextualKind = SyntaxFacts.GetContextualKeywordKind(token.ValueText); - - return contextualKind is SyntaxKind.FromKeyword or - SyntaxKind.NameOfKeyword or - SyntaxKind.AsyncKeyword or - SyntaxKind.AwaitKeyword or - SyntaxKind.VarKeyword; - } + private static bool IsProbablySyntacticConstruct(SyntaxToken token) + { + // Technically all C# contextual keywords are valid member names. + // However some of them start various syntactic constructs + // and we don't want to show "Generate " codefix for them: + // 1. "from" starts LINQ expression + // 2. "nameof" is probably nameof(some_name) + // 3. "async" can start a delegate declaration + // 4. "await" starts await expression + // 5. "var" is used in constructions like "var x = ..." + // The list can be expanded in the future if necessary + // This method tells if the given SyntaxToken is one of the cases above + var contextualKind = SyntaxFacts.GetContextualKeywordKind(token.ValueText); + + return contextualKind is SyntaxKind.FromKeyword or + SyntaxKind.NameOfKeyword or + SyntaxKind.AsyncKeyword or + SyntaxKind.AwaitKeyword or + SyntaxKind.VarKeyword; + } - protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) - => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); + protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType) + => containingType.ContainingTypesOrSelfHasUnsafeKeyword(); - protected override bool TryInitializeExplicitInterfaceState( - SemanticDocument document, SyntaxNode node, CancellationToken cancellationToken, - out SyntaxToken identifierToken, out IPropertySymbol propertySymbol, out INamedTypeSymbol typeToGenerateIn) - { - var propertyDeclaration = (PropertyDeclarationSyntax)node; - identifierToken = propertyDeclaration.Identifier; + protected override bool TryInitializeExplicitInterfaceState( + SemanticDocument document, SyntaxNode node, CancellationToken cancellationToken, + out SyntaxToken identifierToken, out IPropertySymbol propertySymbol, out INamedTypeSymbol typeToGenerateIn) + { + var propertyDeclaration = (PropertyDeclarationSyntax)node; + identifierToken = propertyDeclaration.Identifier; - if (propertyDeclaration.ExplicitInterfaceSpecifier != null) + if (propertyDeclaration.ExplicitInterfaceSpecifier != null) + { + var semanticModel = document.SemanticModel; + propertySymbol = semanticModel.GetDeclaredSymbol(propertyDeclaration, cancellationToken); + if (propertySymbol != null && !propertySymbol.ExplicitInterfaceImplementations.Any()) { - var semanticModel = document.SemanticModel; - propertySymbol = semanticModel.GetDeclaredSymbol(propertyDeclaration, cancellationToken); - if (propertySymbol != null && !propertySymbol.ExplicitInterfaceImplementations.Any()) - { - var info = semanticModel.GetTypeInfo(propertyDeclaration.ExplicitInterfaceSpecifier.Name, cancellationToken); - typeToGenerateIn = info.Type as INamedTypeSymbol; - return typeToGenerateIn != null; - } + var info = semanticModel.GetTypeInfo(propertyDeclaration.ExplicitInterfaceSpecifier.Name, cancellationToken); + typeToGenerateIn = info.Type as INamedTypeSymbol; + return typeToGenerateIn != null; } - - identifierToken = default; - propertySymbol = null; - typeToGenerateIn = null; - return false; } - protected override bool TryInitializeIdentifierNameState( - SemanticDocument document, SimpleNameSyntax identifierName, CancellationToken cancellationToken, - out SyntaxToken identifierToken, out ExpressionSyntax simpleNameOrMemberAccessExpression, out bool isInExecutableBlock, out bool isConditionalAccessExpression) + identifierToken = default; + propertySymbol = null; + typeToGenerateIn = null; + return false; + } + + protected override bool TryInitializeIdentifierNameState( + SemanticDocument document, SimpleNameSyntax identifierName, CancellationToken cancellationToken, + out SyntaxToken identifierToken, out ExpressionSyntax simpleNameOrMemberAccessExpression, out bool isInExecutableBlock, out bool isConditionalAccessExpression) + { + identifierToken = identifierName.Identifier; + if (identifierToken.ValueText != string.Empty && + !IsProbablyGeneric(identifierName, cancellationToken)) { - identifierToken = identifierName.Identifier; - if (identifierToken.ValueText != string.Empty && - !IsProbablyGeneric(identifierName, cancellationToken)) + var memberAccess = identifierName.Parent as MemberAccessExpressionSyntax; + var conditionalMemberAccess = identifierName.Parent.Parent as ConditionalAccessExpressionSyntax; + if (memberAccess?.Name == identifierName) { - var memberAccess = identifierName.Parent as MemberAccessExpressionSyntax; - var conditionalMemberAccess = identifierName.Parent.Parent as ConditionalAccessExpressionSyntax; - if (memberAccess?.Name == identifierName) - { - simpleNameOrMemberAccessExpression = memberAccess; - } - else if ((conditionalMemberAccess?.WhenNotNull as MemberBindingExpressionSyntax)?.Name == identifierName) - { - simpleNameOrMemberAccessExpression = conditionalMemberAccess; - } - else - { - simpleNameOrMemberAccessExpression = identifierName; - } - - // If we're being invoked, then don't offer this, offer generate method instead. - // Note: we could offer to generate a field with a delegate type. However, that's - // very esoteric and probably not what most users want. - if (!IsLegal(document, simpleNameOrMemberAccessExpression, cancellationToken)) - { - isInExecutableBlock = false; - isConditionalAccessExpression = false; - return false; - } - - var block = identifierName.GetAncestor(); - isInExecutableBlock = block != null && !block.OverlapsHiddenPosition(cancellationToken); - isConditionalAccessExpression = conditionalMemberAccess != null; - return true; + simpleNameOrMemberAccessExpression = memberAccess; } - - identifierToken = default; - simpleNameOrMemberAccessExpression = null; - isInExecutableBlock = false; - isConditionalAccessExpression = false; - return false; - } - - private static bool IsProbablyGeneric(SimpleNameSyntax identifierName, CancellationToken cancellationToken) - { - if (identifierName.IsKind(SyntaxKind.GenericName)) + else if ((conditionalMemberAccess?.WhenNotNull as MemberBindingExpressionSyntax)?.Name == identifierName) + { + simpleNameOrMemberAccessExpression = conditionalMemberAccess; + } + else { - return true; + simpleNameOrMemberAccessExpression = identifierName; } - // We might have something of the form: Goo < Bar. - // In this case, we would want to generate offer a member called 'Goo'. however, if we have - // something like "Goo < string >" then that's clearly something generic and we don't want - // to offer to generate a member there. - var localRoot = identifierName.GetAncestor() ?? - identifierName.GetAncestor() ?? - identifierName.SyntaxTree.GetRoot(cancellationToken); + // If we're being invoked, then don't offer this, offer generate method instead. + // Note: we could offer to generate a field with a delegate type. However, that's + // very esoteric and probably not what most users want. + if (!IsLegal(document, simpleNameOrMemberAccessExpression, cancellationToken)) + { + isInExecutableBlock = false; + isConditionalAccessExpression = false; + return false; + } - // In order to figure this out (without writing our own parser), we just try to parse out a - // type name here. If we get a generic name back, without any errors, then we'll assume the - // user really is typing a generic name, and thus should not get recommendations to create a - // variable. - var localText = localRoot.ToString(); - var startIndex = identifierName.Span.Start - localRoot.Span.Start; + var block = identifierName.GetAncestor(); + isInExecutableBlock = block != null && !block.OverlapsHiddenPosition(cancellationToken); + isConditionalAccessExpression = conditionalMemberAccess != null; + return true; + } - var parsedType = SyntaxFactory.ParseTypeName(localText, startIndex, consumeFullText: false); + identifierToken = default; + simpleNameOrMemberAccessExpression = null; + isInExecutableBlock = false; + isConditionalAccessExpression = false; + return false; + } - return parsedType.IsKind(SyntaxKind.GenericName) && !parsedType.ContainsDiagnostics; + private static bool IsProbablyGeneric(SimpleNameSyntax identifierName, CancellationToken cancellationToken) + { + if (identifierName.IsKind(SyntaxKind.GenericName)) + { + return true; } - private static bool IsLegal( - SemanticDocument document, - ExpressionSyntax expression, - CancellationToken cancellationToken) - { - // TODO(cyrusn): Consider supporting this at some point. It is difficult because we'd - // need to replace the identifier typed with the fully qualified name of the field we - // were generating. - if (expression.IsParentKind(SyntaxKind.AttributeArgument)) - { - return false; - } + // We might have something of the form: Goo < Bar. + // In this case, we would want to generate offer a member called 'Goo'. however, if we have + // something like "Goo < string >" then that's clearly something generic and we don't want + // to offer to generate a member there. + var localRoot = identifierName.GetAncestor() ?? + identifierName.GetAncestor() ?? + identifierName.SyntaxTree.GetRoot(cancellationToken); - if (expression.IsParentKind(SyntaxKind.ConditionalAccessExpression)) - { - return true; - } + // In order to figure this out (without writing our own parser), we just try to parse out a + // type name here. If we get a generic name back, without any errors, then we'll assume the + // user really is typing a generic name, and thus should not get recommendations to create a + // variable. + var localText = localRoot.ToString(); + var startIndex = identifierName.Span.Start - localRoot.Span.Start; - if (expression.IsParentKind(SyntaxKind.IsPatternExpression)) - { - return true; - } + var parsedType = SyntaxFactory.ParseTypeName(localText, startIndex, consumeFullText: false); - if (expression.Parent is (kind: SyntaxKind.NameColon or SyntaxKind.ExpressionColon) && - expression.Parent.IsParentKind(SyntaxKind.Subpattern)) - { - return true; - } + return parsedType.IsKind(SyntaxKind.GenericName) && !parsedType.ContainsDiagnostics; + } - if (expression.IsParentKind(SyntaxKind.ConstantPattern)) - { - return true; - } + private static bool IsLegal( + SemanticDocument document, + ExpressionSyntax expression, + CancellationToken cancellationToken) + { + // TODO(cyrusn): Consider supporting this at some point. It is difficult because we'd + // need to replace the identifier typed with the fully qualified name of the field we + // were generating. + if (expression.IsParentKind(SyntaxKind.AttributeArgument)) + { + return false; + } - return expression.CanReplaceWithLValue(document.SemanticModel, cancellationToken); + if (expression.IsParentKind(SyntaxKind.ConditionalAccessExpression)) + { + return true; } - protected override bool TryConvertToLocalDeclaration(ITypeSymbol type, SyntaxToken identifierToken, SemanticModel semanticModel, CancellationToken cancellationToken, out SyntaxNode newRoot) + if (expression.IsParentKind(SyntaxKind.IsPatternExpression)) { - var token = identifierToken; - var node = identifierToken.Parent as IdentifierNameSyntax; - if (node.IsLeftSideOfAssignExpression() && node.Parent.IsParentKind(SyntaxKind.ExpressionStatement)) - { - var assignExpression = (AssignmentExpressionSyntax)node.Parent; - var expressionStatement = (StatementSyntax)assignExpression.Parent; + return true; + } - var declarationStatement = SyntaxFactory.LocalDeclarationStatement( - SyntaxFactory.VariableDeclaration( - type.GenerateTypeSyntax(), - [SyntaxFactory.VariableDeclarator(token, null, SyntaxFactory.EqualsValueClause( - assignExpression.OperatorToken, assignExpression.Right))])); - declarationStatement = declarationStatement.WithAdditionalAnnotations(Formatter.Annotation); + if (expression.Parent is (kind: SyntaxKind.NameColon or SyntaxKind.ExpressionColon) && + expression.Parent.IsParentKind(SyntaxKind.Subpattern)) + { + return true; + } - var root = token.GetAncestor(); - newRoot = root.ReplaceNode(expressionStatement, declarationStatement); + if (expression.IsParentKind(SyntaxKind.ConstantPattern)) + { + return true; + } - return true; - } + return expression.CanReplaceWithLValue(document.SemanticModel, cancellationToken); + } - newRoot = null; - return false; + protected override bool TryConvertToLocalDeclaration(ITypeSymbol type, SyntaxToken identifierToken, SemanticModel semanticModel, CancellationToken cancellationToken, out SyntaxNode newRoot) + { + var token = identifierToken; + var node = identifierToken.Parent as IdentifierNameSyntax; + if (node.IsLeftSideOfAssignExpression() && node.Parent.IsParentKind(SyntaxKind.ExpressionStatement)) + { + var assignExpression = (AssignmentExpressionSyntax)node.Parent; + var expressionStatement = (StatementSyntax)assignExpression.Parent; + + var declarationStatement = SyntaxFactory.LocalDeclarationStatement( + SyntaxFactory.VariableDeclaration( + type.GenerateTypeSyntax(), + [SyntaxFactory.VariableDeclarator(token, null, SyntaxFactory.EqualsValueClause( + assignExpression.OperatorToken, assignExpression.Right))])); + declarationStatement = declarationStatement.WithAdditionalAnnotations(Formatter.Annotation); + + var root = token.GetAncestor(); + newRoot = root.ReplaceNode(expressionStatement, declarationStatement); + + return true; } + + newRoot = null; + return false; } } diff --git a/src/Features/CSharp/Portable/GenerateType/CSharpGenerateTypeService.cs b/src/Features/CSharp/Portable/GenerateType/CSharpGenerateTypeService.cs index d1092a9fe65b7..2f4a2f2ada6e8 100644 --- a/src/Features/CSharp/Portable/GenerateType/CSharpGenerateTypeService.cs +++ b/src/Features/CSharp/Portable/GenerateType/CSharpGenerateTypeService.cs @@ -27,825 +27,824 @@ using Microsoft.CodeAnalysis.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.GenerateType +namespace Microsoft.CodeAnalysis.CSharp.GenerateType; + +[ExportLanguageService(typeof(IGenerateTypeService), LanguageNames.CSharp), Shared] +internal class CSharpGenerateTypeService : + AbstractGenerateTypeService { - [ExportLanguageService(typeof(IGenerateTypeService), LanguageNames.CSharp), Shared] - internal class CSharpGenerateTypeService : - AbstractGenerateTypeService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGenerateTypeService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGenerateTypeService() - { - } + } - protected override string DefaultFileExtension => ".cs"; + protected override string DefaultFileExtension => ".cs"; - protected override ExpressionSyntax GetLeftSideOfDot(SimpleNameSyntax simpleName) - => simpleName.GetLeftSideOfDot(); + protected override ExpressionSyntax GetLeftSideOfDot(SimpleNameSyntax simpleName) + => simpleName.GetLeftSideOfDot(); - protected override bool IsInCatchDeclaration(ExpressionSyntax expression) - => expression.IsParentKind(SyntaxKind.CatchDeclaration); + protected override bool IsInCatchDeclaration(ExpressionSyntax expression) + => expression.IsParentKind(SyntaxKind.CatchDeclaration); - protected override bool IsArrayElementType(ExpressionSyntax expression) - { - return expression.IsParentKind(SyntaxKind.ArrayType) && - expression.Parent.IsParentKind(SyntaxKind.ArrayCreationExpression); - } + protected override bool IsArrayElementType(ExpressionSyntax expression) + { + return expression.IsParentKind(SyntaxKind.ArrayType) && + expression.Parent.IsParentKind(SyntaxKind.ArrayCreationExpression); + } - protected override bool IsInValueTypeConstraintContext( - SemanticModel semanticModel, - ExpressionSyntax expression, - CancellationToken cancellationToken) + protected override bool IsInValueTypeConstraintContext( + SemanticModel semanticModel, + ExpressionSyntax expression, + CancellationToken cancellationToken) + { + if (expression is TypeSyntax typeSyntax && expression.Parent is TypeArgumentListSyntax typeArgumentList) { - if (expression is TypeSyntax typeSyntax && expression.Parent is TypeArgumentListSyntax typeArgumentList) + var symbolInfo = semanticModel.GetSymbolInfo(typeArgumentList.Parent, cancellationToken); + var symbol = symbolInfo.GetAnySymbol(); + if (symbol.IsConstructor()) { - var symbolInfo = semanticModel.GetSymbolInfo(typeArgumentList.Parent, cancellationToken); - var symbol = symbolInfo.GetAnySymbol(); - if (symbol.IsConstructor()) - { - symbol = symbol.ContainingType; - } - - var parameterIndex = typeArgumentList.Arguments.IndexOf(typeSyntax); - if (symbol is INamedTypeSymbol type) - { - type = type.OriginalDefinition; - var typeParameter = parameterIndex < type.TypeParameters.Length ? type.TypeParameters[parameterIndex] : null; - return typeParameter != null && typeParameter.HasValueTypeConstraint; - } + symbol = symbol.ContainingType; + } - if (symbol is IMethodSymbol method) - { - method = method.OriginalDefinition; - var typeParameter = parameterIndex < method.TypeParameters.Length ? method.TypeParameters[parameterIndex] : null; - return typeParameter != null && typeParameter.HasValueTypeConstraint; - } + var parameterIndex = typeArgumentList.Arguments.IndexOf(typeSyntax); + if (symbol is INamedTypeSymbol type) + { + type = type.OriginalDefinition; + var typeParameter = parameterIndex < type.TypeParameters.Length ? type.TypeParameters[parameterIndex] : null; + return typeParameter != null && typeParameter.HasValueTypeConstraint; } - return false; + if (symbol is IMethodSymbol method) + { + method = method.OriginalDefinition; + var typeParameter = parameterIndex < method.TypeParameters.Length ? method.TypeParameters[parameterIndex] : null; + return typeParameter != null && typeParameter.HasValueTypeConstraint; + } } - protected override bool IsInInterfaceList(ExpressionSyntax expression) + return false; + } + + protected override bool IsInInterfaceList(ExpressionSyntax expression) + { + if (expression is TypeSyntax && + expression.Parent is BaseTypeSyntax baseType && + baseType.Parent is BaseListSyntax baseList && + baseType.Type == expression) { - if (expression is TypeSyntax && - expression.Parent is BaseTypeSyntax baseType && - baseType.Parent is BaseListSyntax baseList && - baseType.Type == expression) + // If it's after the first item, then it's definitely an interface. + if (baseList.Types[0] != expression.Parent) { - // If it's after the first item, then it's definitely an interface. - if (baseList.Types[0] != expression.Parent) - { - return true; - } - - // If it's in the base list of an interface or struct, then it's definitely an - // interface. - return baseList?.Parent.Kind() is - SyntaxKind.InterfaceDeclaration or - SyntaxKind.StructDeclaration or - SyntaxKind.RecordStructDeclaration; + return true; } - if (expression is TypeSyntax && - expression.Parent is TypeConstraintSyntax typeConstraint && - typeConstraint.Parent is TypeParameterConstraintClauseSyntax constraintClause) - { - var index = constraintClause.Constraints.IndexOf(typeConstraint); + // If it's in the base list of an interface or struct, then it's definitely an + // interface. + return baseList?.Parent.Kind() is + SyntaxKind.InterfaceDeclaration or + SyntaxKind.StructDeclaration or + SyntaxKind.RecordStructDeclaration; + } - // If it's after the first item, then it's definitely an interface. - return index > 0; - } + if (expression is TypeSyntax && + expression.Parent is TypeConstraintSyntax typeConstraint && + typeConstraint.Parent is TypeParameterConstraintClauseSyntax constraintClause) + { + var index = constraintClause.Constraints.IndexOf(typeConstraint); + // If it's after the first item, then it's definitely an interface. + return index > 0; + } + + return false; + } + + protected override bool TryGetNameParts(ExpressionSyntax expression, out IList nameParts) + { + return expression.TryGetNameParts(out nameParts); + } + + protected override bool TryInitializeState( + SemanticDocument document, + SimpleNameSyntax simpleName, + CancellationToken cancellationToken, + out GenerateTypeServiceStateOptions generateTypeServiceStateOptions) + { + generateTypeServiceStateOptions = new GenerateTypeServiceStateOptions(); + + if (simpleName.IsVar) + { return false; } - protected override bool TryGetNameParts(ExpressionSyntax expression, out IList nameParts) + if (SyntaxFacts.IsAliasQualifier(simpleName)) { - return expression.TryGetNameParts(out nameParts); + return false; } - protected override bool TryInitializeState( - SemanticDocument document, - SimpleNameSyntax simpleName, - CancellationToken cancellationToken, - out GenerateTypeServiceStateOptions generateTypeServiceStateOptions) + // Never offer if we're in a using directive, unless its a static using. The feeling here is that it's highly + // unlikely that this would be a location where a user would be wanting to generate + // something. They're really just trying to reference something that exists but + // isn't available for some reason (i.e. a missing reference). + var usingDirectiveSyntax = simpleName.GetAncestorOrThis(); + if (usingDirectiveSyntax != null && usingDirectiveSyntax.StaticKeyword.Kind() != SyntaxKind.StaticKeyword) { - generateTypeServiceStateOptions = new GenerateTypeServiceStateOptions(); + return false; + } - if (simpleName.IsVar) + ExpressionSyntax nameOrMemberAccessExpression = null; + if (simpleName.IsRightSideOfDot()) + { + // This simplename comes from the cref + if (simpleName.IsParentKind(SyntaxKind.NameMemberCref)) { return false; } - if (SyntaxFacts.IsAliasQualifier(simpleName)) + nameOrMemberAccessExpression = generateTypeServiceStateOptions.NameOrMemberAccessExpression = (ExpressionSyntax)simpleName.Parent; + + // If we're on the right side of a dot, then the left side better be a name (and + // not an arbitrary expression). + var leftSideExpression = simpleName.GetLeftSideOfDot(); + if (leftSideExpression.Kind() is not ( + SyntaxKind.QualifiedName or + SyntaxKind.IdentifierName or + SyntaxKind.AliasQualifiedName or + SyntaxKind.GenericName or + SyntaxKind.SimpleMemberAccessExpression)) { return false; } + } + else + { + nameOrMemberAccessExpression = generateTypeServiceStateOptions.NameOrMemberAccessExpression = simpleName; + } - // Never offer if we're in a using directive, unless its a static using. The feeling here is that it's highly - // unlikely that this would be a location where a user would be wanting to generate - // something. They're really just trying to reference something that exists but - // isn't available for some reason (i.e. a missing reference). - var usingDirectiveSyntax = simpleName.GetAncestorOrThis(); - if (usingDirectiveSyntax != null && usingDirectiveSyntax.StaticKeyword.Kind() != SyntaxKind.StaticKeyword) + // BUG(5712): Don't offer generate type in an enum's base list. + if (nameOrMemberAccessExpression.Parent is BaseTypeSyntax && + nameOrMemberAccessExpression.Parent.IsParentKind(SyntaxKind.BaseList) && + ((BaseTypeSyntax)nameOrMemberAccessExpression.Parent).Type == nameOrMemberAccessExpression && + nameOrMemberAccessExpression.Parent.Parent.IsParentKind(SyntaxKind.EnumDeclaration)) + { + return false; + } + + // If we can guarantee it's a type only context, great. Otherwise, we may not want to + // provide this here. + var semanticModel = document.SemanticModel; + if (!SyntaxFacts.IsInNamespaceOrTypeContext(nameOrMemberAccessExpression)) + { + // Don't offer Generate Type in an expression context *unless* we're on the left + // side of a dot. In that case the user might be making a type that they're + // accessing a static off of. + var syntaxTree = semanticModel.SyntaxTree; + var start = nameOrMemberAccessExpression.SpanStart; + var tokenOnLeftOfStart = syntaxTree.FindTokenOnLeftOfPosition(start, cancellationToken); + var isExpressionContext = syntaxTree.IsExpressionContext(start, tokenOnLeftOfStart, attributes: true, cancellationToken: cancellationToken, semanticModel: semanticModel); + var isStatementContext = syntaxTree.IsStatementContext(start, tokenOnLeftOfStart, cancellationToken); + var isExpressionOrStatementContext = isExpressionContext || isStatementContext; + + // Delegate Type Creation is not allowed in Non Type Namespace Context + generateTypeServiceStateOptions.IsDelegateAllowed = false; + + if (!isExpressionOrStatementContext) { return false; } - ExpressionSyntax nameOrMemberAccessExpression = null; - if (simpleName.IsRightSideOfDot()) + if (!simpleName.IsLeftSideOfDot() && + !simpleName.IsInsideNameOfExpression(semanticModel, cancellationToken)) { - // This simplename comes from the cref - if (simpleName.IsParentKind(SyntaxKind.NameMemberCref)) + if (nameOrMemberAccessExpression == null || !nameOrMemberAccessExpression.IsKind(SyntaxKind.SimpleMemberAccessExpression) || !simpleName.IsRightSideOfDot()) { return false; } - nameOrMemberAccessExpression = generateTypeServiceStateOptions.NameOrMemberAccessExpression = (ExpressionSyntax)simpleName.Parent; - - // If we're on the right side of a dot, then the left side better be a name (and - // not an arbitrary expression). - var leftSideExpression = simpleName.GetLeftSideOfDot(); - if (leftSideExpression.Kind() is not ( - SyntaxKind.QualifiedName or - SyntaxKind.IdentifierName or - SyntaxKind.AliasQualifiedName or - SyntaxKind.GenericName or - SyntaxKind.SimpleMemberAccessExpression)) + var leftSymbol = semanticModel.GetSymbolInfo(((MemberAccessExpressionSyntax)nameOrMemberAccessExpression).Expression, cancellationToken).Symbol; + var token = simpleName.GetLastToken().GetNextToken(); + + // We let only the Namespace to be left of the Dot + if (leftSymbol == null || + !leftSymbol.IsKind(SymbolKind.Namespace) || + !token.IsKind(SyntaxKind.DotToken)) { return false; } - } - else - { - nameOrMemberAccessExpression = generateTypeServiceStateOptions.NameOrMemberAccessExpression = simpleName; - } - - // BUG(5712): Don't offer generate type in an enum's base list. - if (nameOrMemberAccessExpression.Parent is BaseTypeSyntax && - nameOrMemberAccessExpression.Parent.IsParentKind(SyntaxKind.BaseList) && - ((BaseTypeSyntax)nameOrMemberAccessExpression.Parent).Type == nameOrMemberAccessExpression && - nameOrMemberAccessExpression.Parent.Parent.IsParentKind(SyntaxKind.EnumDeclaration)) - { - return false; + else + { + generateTypeServiceStateOptions.IsMembersWithModule = true; + generateTypeServiceStateOptions.IsTypeGeneratedIntoNamespaceFromMemberAccess = true; + } } - // If we can guarantee it's a type only context, great. Otherwise, we may not want to - // provide this here. - var semanticModel = document.SemanticModel; - if (!SyntaxFacts.IsInNamespaceOrTypeContext(nameOrMemberAccessExpression)) + // Global Namespace + if (!generateTypeServiceStateOptions.IsTypeGeneratedIntoNamespaceFromMemberAccess && + !SyntaxFacts.IsInNamespaceOrTypeContext(simpleName)) { - // Don't offer Generate Type in an expression context *unless* we're on the left - // side of a dot. In that case the user might be making a type that they're - // accessing a static off of. - var syntaxTree = semanticModel.SyntaxTree; - var start = nameOrMemberAccessExpression.SpanStart; - var tokenOnLeftOfStart = syntaxTree.FindTokenOnLeftOfPosition(start, cancellationToken); - var isExpressionContext = syntaxTree.IsExpressionContext(start, tokenOnLeftOfStart, attributes: true, cancellationToken: cancellationToken, semanticModel: semanticModel); - var isStatementContext = syntaxTree.IsStatementContext(start, tokenOnLeftOfStart, cancellationToken); - var isExpressionOrStatementContext = isExpressionContext || isStatementContext; - - // Delegate Type Creation is not allowed in Non Type Namespace Context - generateTypeServiceStateOptions.IsDelegateAllowed = false; - - if (!isExpressionOrStatementContext) + var token = simpleName.GetLastToken().GetNextToken(); + if (token.IsKind(SyntaxKind.DotToken) && + simpleName.Parent == token.Parent) { - return false; + generateTypeServiceStateOptions.IsMembersWithModule = true; + generateTypeServiceStateOptions.IsTypeGeneratedIntoNamespaceFromMemberAccess = true; } + } + } - if (!simpleName.IsLeftSideOfDot() && - !simpleName.IsInsideNameOfExpression(semanticModel, cancellationToken)) - { - if (nameOrMemberAccessExpression == null || !nameOrMemberAccessExpression.IsKind(SyntaxKind.SimpleMemberAccessExpression) || !simpleName.IsRightSideOfDot()) - { - return false; - } - - var leftSymbol = semanticModel.GetSymbolInfo(((MemberAccessExpressionSyntax)nameOrMemberAccessExpression).Expression, cancellationToken).Symbol; - var token = simpleName.GetLastToken().GetNextToken(); - - // We let only the Namespace to be left of the Dot - if (leftSymbol == null || - !leftSymbol.IsKind(SymbolKind.Namespace) || - !token.IsKind(SyntaxKind.DotToken)) - { - return false; - } - else - { - generateTypeServiceStateOptions.IsMembersWithModule = true; - generateTypeServiceStateOptions.IsTypeGeneratedIntoNamespaceFromMemberAccess = true; - } - } + var fieldDeclaration = simpleName.GetAncestor(); + if (fieldDeclaration != null && + fieldDeclaration.Parent is CompilationUnitSyntax && + document.Document.SourceCodeKind == SourceCodeKind.Regular) + { + return false; + } - // Global Namespace - if (!generateTypeServiceStateOptions.IsTypeGeneratedIntoNamespaceFromMemberAccess && - !SyntaxFacts.IsInNamespaceOrTypeContext(simpleName)) + // Check to see if Module could be an option in the Type Generation in Cross Language Generation + var nextToken = simpleName.GetLastToken().GetNextToken(); + if (simpleName.IsLeftSideOfDot() || + nextToken.IsKind(SyntaxKind.DotToken)) + { + if (simpleName.IsRightSideOfDot()) + { + if (simpleName.Parent is QualifiedNameSyntax parent) { - var token = simpleName.GetLastToken().GetNextToken(); - if (token.IsKind(SyntaxKind.DotToken) && - simpleName.Parent == token.Parent) + var leftSymbol = semanticModel.GetSymbolInfo(parent.Left, cancellationToken).Symbol; + + if (leftSymbol != null && leftSymbol.IsKind(SymbolKind.Namespace)) { generateTypeServiceStateOptions.IsMembersWithModule = true; - generateTypeServiceStateOptions.IsTypeGeneratedIntoNamespaceFromMemberAccess = true; } } } + } - var fieldDeclaration = simpleName.GetAncestor(); - if (fieldDeclaration != null && - fieldDeclaration.Parent is CompilationUnitSyntax && - document.Document.SourceCodeKind == SourceCodeKind.Regular) + if (SyntaxFacts.IsInNamespaceOrTypeContext(nameOrMemberAccessExpression)) + { + if (nextToken.IsKind(SyntaxKind.DotToken)) { - return false; + // In Namespace or Type Context we cannot have Interface, Enum, Delegate as part of the Left Expression of a QualifiedName + generateTypeServiceStateOptions.IsDelegateAllowed = false; + generateTypeServiceStateOptions.IsInterfaceOrEnumNotAllowedInTypeContext = true; + generateTypeServiceStateOptions.IsMembersWithModule = true; } - // Check to see if Module could be an option in the Type Generation in Cross Language Generation - var nextToken = simpleName.GetLastToken().GetNextToken(); - if (simpleName.IsLeftSideOfDot() || - nextToken.IsKind(SyntaxKind.DotToken)) + // case: class Goo where T: MyType + if (nameOrMemberAccessExpression.GetAncestors().Any()) { - if (simpleName.IsRightSideOfDot()) - { - if (simpleName.Parent is QualifiedNameSyntax parent) - { - var leftSymbol = semanticModel.GetSymbolInfo(parent.Left, cancellationToken).Symbol; - - if (leftSymbol != null && leftSymbol.IsKind(SymbolKind.Namespace)) - { - generateTypeServiceStateOptions.IsMembersWithModule = true; - } - } - } + generateTypeServiceStateOptions.IsClassInterfaceTypes = true; + return true; } - if (SyntaxFacts.IsInNamespaceOrTypeContext(nameOrMemberAccessExpression)) + // Events + if (nameOrMemberAccessExpression.GetAncestors().Any() || + nameOrMemberAccessExpression.GetAncestors().Any()) { - if (nextToken.IsKind(SyntaxKind.DotToken)) - { - // In Namespace or Type Context we cannot have Interface, Enum, Delegate as part of the Left Expression of a QualifiedName - generateTypeServiceStateOptions.IsDelegateAllowed = false; - generateTypeServiceStateOptions.IsInterfaceOrEnumNotAllowedInTypeContext = true; - generateTypeServiceStateOptions.IsMembersWithModule = true; - } - - // case: class Goo where T: MyType - if (nameOrMemberAccessExpression.GetAncestors().Any()) + // Case : event goo name11 + // Only Delegate + if (simpleName.Parent is not null and not QualifiedNameSyntax) { - generateTypeServiceStateOptions.IsClassInterfaceTypes = true; + generateTypeServiceStateOptions.IsDelegateOnly = true; return true; } - // Events - if (nameOrMemberAccessExpression.GetAncestors().Any() || - nameOrMemberAccessExpression.GetAncestors().Any()) + // Case : event SomeSymbol.goo name11 + if (nameOrMemberAccessExpression is QualifiedNameSyntax) { - // Case : event goo name11 - // Only Delegate - if (simpleName.Parent is not null and not QualifiedNameSyntax) - { - generateTypeServiceStateOptions.IsDelegateOnly = true; - return true; - } - - // Case : event SomeSymbol.goo name11 - if (nameOrMemberAccessExpression is QualifiedNameSyntax) + // Only Namespace, Class, Struct and Module are allowed to contain Delegate + // Case : event Something.Mytype. Identifier + if (nextToken.IsKind(SyntaxKind.DotToken)) { - // Only Namespace, Class, Struct and Module are allowed to contain Delegate - // Case : event Something.Mytype. Identifier - if (nextToken.IsKind(SyntaxKind.DotToken)) + if (nameOrMemberAccessExpression.Parent is not null and QualifiedNameSyntax) { - if (nameOrMemberAccessExpression.Parent is not null and QualifiedNameSyntax) - { - return true; - } - - throw ExceptionUtilities.Unreachable(); - } - else - { - // Case : event Something. Identifier - generateTypeServiceStateOptions.IsDelegateOnly = true; return true; } + + throw ExceptionUtilities.Unreachable(); + } + else + { + // Case : event Something. Identifier + generateTypeServiceStateOptions.IsDelegateOnly = true; + return true; } } } - else + } + else + { + // MemberAccessExpression + if ((nameOrMemberAccessExpression.IsKind(SyntaxKind.SimpleMemberAccessExpression) || (nameOrMemberAccessExpression.Parent != null && nameOrMemberAccessExpression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression))) + && nameOrMemberAccessExpression.IsLeftSideOfDot()) { - // MemberAccessExpression - if ((nameOrMemberAccessExpression.IsKind(SyntaxKind.SimpleMemberAccessExpression) || (nameOrMemberAccessExpression.Parent != null && nameOrMemberAccessExpression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression))) - && nameOrMemberAccessExpression.IsLeftSideOfDot()) + // Check to see if the expression is part of Invocation Expression + ExpressionSyntax outerMostMemberAccessExpression = null; + if (nameOrMemberAccessExpression.IsKind(SyntaxKind.SimpleMemberAccessExpression)) { - // Check to see if the expression is part of Invocation Expression - ExpressionSyntax outerMostMemberAccessExpression = null; - if (nameOrMemberAccessExpression.IsKind(SyntaxKind.SimpleMemberAccessExpression)) - { - outerMostMemberAccessExpression = nameOrMemberAccessExpression; - } - else - { - Debug.Assert(nameOrMemberAccessExpression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression)); - outerMostMemberAccessExpression = (ExpressionSyntax)nameOrMemberAccessExpression.Parent; - } + outerMostMemberAccessExpression = nameOrMemberAccessExpression; + } + else + { + Debug.Assert(nameOrMemberAccessExpression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression)); + outerMostMemberAccessExpression = (ExpressionSyntax)nameOrMemberAccessExpression.Parent; + } - outerMostMemberAccessExpression = outerMostMemberAccessExpression.GetAncestorsOrThis().SkipWhile(n => n != null && n.IsKind(SyntaxKind.SimpleMemberAccessExpression)).FirstOrDefault(); - if (outerMostMemberAccessExpression is not null and InvocationExpressionSyntax) - { - generateTypeServiceStateOptions.IsEnumNotAllowed = true; - } + outerMostMemberAccessExpression = outerMostMemberAccessExpression.GetAncestorsOrThis().SkipWhile(n => n != null && n.IsKind(SyntaxKind.SimpleMemberAccessExpression)).FirstOrDefault(); + if (outerMostMemberAccessExpression is not null and InvocationExpressionSyntax) + { + generateTypeServiceStateOptions.IsEnumNotAllowed = true; } } + } - // Cases: - // // 1 - Function Address - // var s2 = new MyD2(goo); + // Cases: + // // 1 - Function Address + // var s2 = new MyD2(goo); - // // 2 - Delegate - // MyD1 d = null; - // var s1 = new MyD2(d); + // // 2 - Delegate + // MyD1 d = null; + // var s1 = new MyD2(d); - // // 3 - Action - // Action action1 = null; - // var s3 = new MyD2(action1); + // // 3 - Action + // Action action1 = null; + // var s3 = new MyD2(action1); - // // 4 - Func - // Func lambda = () => { return 0; }; - // var s4 = new MyD3(lambda); + // // 4 - Func + // Func lambda = () => { return 0; }; + // var s4 = new MyD3(lambda); - if (nameOrMemberAccessExpression.Parent is ObjectCreationExpressionSyntax) - { - var objectCreationExpressionOpt = generateTypeServiceStateOptions.ObjectCreationExpressionOpt = (ObjectCreationExpressionSyntax)nameOrMemberAccessExpression.Parent; + if (nameOrMemberAccessExpression.Parent is ObjectCreationExpressionSyntax) + { + var objectCreationExpressionOpt = generateTypeServiceStateOptions.ObjectCreationExpressionOpt = (ObjectCreationExpressionSyntax)nameOrMemberAccessExpression.Parent; - // Enum and Interface not Allowed in Object Creation Expression - generateTypeServiceStateOptions.IsInterfaceOrEnumNotAllowedInTypeContext = true; + // Enum and Interface not Allowed in Object Creation Expression + generateTypeServiceStateOptions.IsInterfaceOrEnumNotAllowedInTypeContext = true; - if (objectCreationExpressionOpt.ArgumentList != null) + if (objectCreationExpressionOpt.ArgumentList != null) + { + if (objectCreationExpressionOpt.ArgumentList.CloseParenToken.IsMissing) { - if (objectCreationExpressionOpt.ArgumentList.CloseParenToken.IsMissing) - { - return false; - } - - // Get the Method symbol for the Delegate to be created - if (generateTypeServiceStateOptions.IsDelegateAllowed && - objectCreationExpressionOpt.ArgumentList.Arguments is [{ Expression: (kind: not SyntaxKind.DeclarationExpression) expression }]) - { - generateTypeServiceStateOptions.DelegateCreationMethodSymbol = GetMethodSymbolIfPresent(semanticModel, expression, cancellationToken); - } - else - { - generateTypeServiceStateOptions.IsDelegateAllowed = false; - } + return false; } - if (objectCreationExpressionOpt.Initializer != null) + // Get the Method symbol for the Delegate to be created + if (generateTypeServiceStateOptions.IsDelegateAllowed && + objectCreationExpressionOpt.ArgumentList.Arguments is [{ Expression: (kind: not SyntaxKind.DeclarationExpression) expression }]) { - foreach (var expression in objectCreationExpressionOpt.Initializer.Expressions) - { - if (expression is not AssignmentExpressionSyntax simpleAssignmentExpression) - { - continue; - } - - if (simpleAssignmentExpression.Left is not SimpleNameSyntax name) - { - continue; - } - - generateTypeServiceStateOptions.PropertiesToGenerate.Add(name); - } + generateTypeServiceStateOptions.DelegateCreationMethodSymbol = GetMethodSymbolIfPresent(semanticModel, expression, cancellationToken); + } + else + { + generateTypeServiceStateOptions.IsDelegateAllowed = false; } } - if (generateTypeServiceStateOptions.IsDelegateAllowed) + if (objectCreationExpressionOpt.Initializer != null) { - // MyD1 z1 = goo; - if (nameOrMemberAccessExpression.Parent is VariableDeclarationSyntax variableDeclaration && - variableDeclaration.Variables.Count != 0) + foreach (var expression in objectCreationExpressionOpt.Initializer.Expressions) { - var firstVarDeclWithInitializer = variableDeclaration.Variables.FirstOrDefault(var => var.Initializer != null && var.Initializer.Value != null); - if (firstVarDeclWithInitializer != null && firstVarDeclWithInitializer.Initializer != null && firstVarDeclWithInitializer.Initializer.Value != null) + if (expression is not AssignmentExpressionSyntax simpleAssignmentExpression) { - generateTypeServiceStateOptions.DelegateCreationMethodSymbol = GetMethodSymbolIfPresent(semanticModel, firstVarDeclWithInitializer.Initializer.Value, cancellationToken); + continue; } - } - // var w1 = (MyD1)goo; - if (nameOrMemberAccessExpression.Parent is CastExpressionSyntax castExpression && - castExpression.Expression != null) - { - generateTypeServiceStateOptions.DelegateCreationMethodSymbol = GetMethodSymbolIfPresent(semanticModel, castExpression.Expression, cancellationToken); + if (simpleAssignmentExpression.Left is not SimpleNameSyntax name) + { + continue; + } + + generateTypeServiceStateOptions.PropertiesToGenerate.Add(name); } } - - return true; } - private static IMethodSymbol GetMethodSymbolIfPresent(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) + if (generateTypeServiceStateOptions.IsDelegateAllowed) { - if (expression == null) - { - return null; - } - - var memberGroup = semanticModel.GetMemberGroup(expression, cancellationToken); - if (memberGroup.Length != 0) + // MyD1 z1 = goo; + if (nameOrMemberAccessExpression.Parent is VariableDeclarationSyntax variableDeclaration && + variableDeclaration.Variables.Count != 0) { - return memberGroup.ElementAt(0).IsKind(SymbolKind.Method) ? (IMethodSymbol)memberGroup.ElementAt(0) : null; - } - - var expressionType = semanticModel.GetTypeInfo(expression, cancellationToken).Type; - if (expressionType.IsDelegateType()) - { - return ((INamedTypeSymbol)expressionType).DelegateInvokeMethod; + var firstVarDeclWithInitializer = variableDeclaration.Variables.FirstOrDefault(var => var.Initializer != null && var.Initializer.Value != null); + if (firstVarDeclWithInitializer != null && firstVarDeclWithInitializer.Initializer != null && firstVarDeclWithInitializer.Initializer.Value != null) + { + generateTypeServiceStateOptions.DelegateCreationMethodSymbol = GetMethodSymbolIfPresent(semanticModel, firstVarDeclWithInitializer.Initializer.Value, cancellationToken); + } } - var expressionSymbol = semanticModel.GetSymbolInfo(expression, cancellationToken).Symbol; - if (expressionSymbol.IsKind(SymbolKind.Method)) + // var w1 = (MyD1)goo; + if (nameOrMemberAccessExpression.Parent is CastExpressionSyntax castExpression && + castExpression.Expression != null) { - return (IMethodSymbol)expressionSymbol; + generateTypeServiceStateOptions.DelegateCreationMethodSymbol = GetMethodSymbolIfPresent(semanticModel, castExpression.Expression, cancellationToken); } - - return null; } - private static Accessibility DetermineAccessibilityConstraint( - State state, - SemanticModel semanticModel, - CancellationToken cancellationToken) - { - return semanticModel.DetermineAccessibilityConstraint( - state.NameOrMemberAccessExpression as TypeSyntax, cancellationToken); - } + return true; + } - private static bool AllContainingTypesArePublicOrProtected( - State state, - SemanticModel semanticModel, - CancellationToken cancellationToken) + private static IMethodSymbol GetMethodSymbolIfPresent(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) + { + if (expression == null) { - return semanticModel.AllContainingTypesArePublicOrProtected( - state.NameOrMemberAccessExpression as TypeSyntax, cancellationToken); + return null; } - protected override ImmutableArray GetTypeParameters( - State state, - SemanticModel semanticModel, - CancellationToken cancellationToken) + var memberGroup = semanticModel.GetMemberGroup(expression, cancellationToken); + if (memberGroup.Length != 0) { - if (state.SimpleName is GenericNameSyntax) - { - var genericName = (GenericNameSyntax)state.SimpleName; - var typeArguments = state.SimpleName.Arity == genericName.TypeArgumentList.Arguments.Count - ? genericName.TypeArgumentList.Arguments.OfType().ToList() - : Enumerable.Repeat(null, state.SimpleName.Arity); - return GetTypeParameters(state, semanticModel, typeArguments, cancellationToken); - } - - return []; + return memberGroup.ElementAt(0).IsKind(SymbolKind.Method) ? (IMethodSymbol)memberGroup.ElementAt(0) : null; } - protected override bool TryGetArgumentList(ObjectCreationExpressionSyntax objectCreationExpression, out IList argumentList) + var expressionType = semanticModel.GetTypeInfo(expression, cancellationToken).Type; + if (expressionType.IsDelegateType()) { - if (objectCreationExpression != null && objectCreationExpression.ArgumentList != null) - { - argumentList = objectCreationExpression.ArgumentList.Arguments.ToList(); - return true; - } - - argumentList = null; - return false; + return ((INamedTypeSymbol)expressionType).DelegateInvokeMethod; } - protected override IList GenerateParameterNames( - SemanticModel semanticModel, IList arguments, CancellationToken cancellationToken) + var expressionSymbol = semanticModel.GetSymbolInfo(expression, cancellationToken).Symbol; + if (expressionSymbol.IsKind(SymbolKind.Method)) { - return semanticModel.GenerateParameterNames(arguments, reservedNames: null, cancellationToken: cancellationToken); + return (IMethodSymbol)expressionSymbol; } - public override string GetRootNamespace(CompilationOptions options) - => string.Empty; + return null; + } - protected override bool IsInVariableTypeContext(ExpressionSyntax expression) - => false; + private static Accessibility DetermineAccessibilityConstraint( + State state, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + return semanticModel.DetermineAccessibilityConstraint( + state.NameOrMemberAccessExpression as TypeSyntax, cancellationToken); + } - protected override INamedTypeSymbol DetermineTypeToGenerateIn(SemanticModel semanticModel, SimpleNameSyntax simpleName, CancellationToken cancellationToken) - => semanticModel.GetEnclosingNamedType(simpleName.SpanStart, cancellationToken); + private static bool AllContainingTypesArePublicOrProtected( + State state, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + return semanticModel.AllContainingTypesArePublicOrProtected( + state.NameOrMemberAccessExpression as TypeSyntax, cancellationToken); + } - protected override Accessibility GetAccessibility(State state, SemanticModel semanticModel, bool intoNamespace, CancellationToken cancellationToken) + protected override ImmutableArray GetTypeParameters( + State state, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + if (state.SimpleName is GenericNameSyntax) { - var accessibility = DetermineDefaultAccessibility(state, semanticModel, intoNamespace, cancellationToken); - if (!state.IsTypeGeneratedIntoNamespaceFromMemberAccess) - { - var accessibilityConstraint = DetermineAccessibilityConstraint(state, semanticModel, cancellationToken); + var genericName = (GenericNameSyntax)state.SimpleName; + var typeArguments = state.SimpleName.Arity == genericName.TypeArgumentList.Arguments.Count + ? genericName.TypeArgumentList.Arguments.OfType().ToList() + : Enumerable.Repeat(null, state.SimpleName.Arity); + return GetTypeParameters(state, semanticModel, typeArguments, cancellationToken); + } - if (accessibilityConstraint is Accessibility.Public or - Accessibility.Internal) - { - accessibility = accessibilityConstraint; - } - else if (accessibilityConstraint is Accessibility.Protected or - Accessibility.ProtectedOrInternal) - { - // If nested type is declared in public type then we should generate public type instead of internal - accessibility = AllContainingTypesArePublicOrProtected(state, semanticModel, cancellationToken) - ? Accessibility.Public - : Accessibility.Internal; - } - } + return []; + } - return accessibility; + protected override bool TryGetArgumentList(ObjectCreationExpressionSyntax objectCreationExpression, out IList argumentList) + { + if (objectCreationExpression != null && objectCreationExpression.ArgumentList != null) + { + argumentList = objectCreationExpression.ArgumentList.Arguments.ToList(); + return true; } - protected override ITypeSymbol DetermineArgumentType(SemanticModel semanticModel, ArgumentSyntax argument, CancellationToken cancellationToken) - => argument.DetermineParameterType(semanticModel, cancellationToken); + argumentList = null; + return false; + } - protected override bool IsConversionImplicit(Compilation compilation, ITypeSymbol sourceType, ITypeSymbol targetType) - => compilation.ClassifyConversion(sourceType, targetType).IsImplicit; + protected override IList GenerateParameterNames( + SemanticModel semanticModel, IList arguments, CancellationToken cancellationToken) + { + return semanticModel.GenerateParameterNames(arguments, reservedNames: null, cancellationToken: cancellationToken); + } - public override async Task<(INamespaceSymbol, INamespaceOrTypeSymbol, Location)> GetOrGenerateEnclosingNamespaceSymbolAsync( - INamedTypeSymbol namedTypeSymbol, string[] containers, Document selectedDocument, SyntaxNode selectedDocumentRoot, CancellationToken cancellationToken) - { - var compilationUnit = (CompilationUnitSyntax)selectedDocumentRoot; - var semanticModel = await selectedDocument.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (containers.Length != 0) - { - // Search the NS declaration in the root - var containerList = new List(containers); - var enclosingNamespace = FindNamespaceInMemberDeclarations(compilationUnit.Members, indexDone: 0, containerList); - if (enclosingNamespace != null) - { - var enclosingNamespaceSymbol = semanticModel.GetSymbolInfo(enclosingNamespace.Name, cancellationToken); - if (enclosingNamespaceSymbol.Symbol is INamespaceSymbol namespaceSymbol) - return (namespaceSymbol, namedTypeSymbol, enclosingNamespace.GetLastToken().GetLocation()); - } - } + public override string GetRootNamespace(CompilationOptions options) + => string.Empty; - var globalNamespace = semanticModel.GetEnclosingNamespace(0, cancellationToken); - var rootNamespaceOrType = namedTypeSymbol.GenerateRootNamespaceOrType(containers); - var lastMember = compilationUnit.Members.LastOrDefault(); - var afterThisLocation = lastMember != null - ? semanticModel.SyntaxTree.GetLocation(new TextSpan(lastMember.Span.End, 0)) - : semanticModel.SyntaxTree.GetLocation(new TextSpan()); + protected override bool IsInVariableTypeContext(ExpressionSyntax expression) + => false; - return (globalNamespace, rootNamespaceOrType, afterThisLocation); - } + protected override INamedTypeSymbol DetermineTypeToGenerateIn(SemanticModel semanticModel, SimpleNameSyntax simpleName, CancellationToken cancellationToken) + => semanticModel.GetEnclosingNamedType(simpleName.SpanStart, cancellationToken); - private BaseNamespaceDeclarationSyntax FindNamespaceInMemberDeclarations(SyntaxList members, int indexDone, List containers) + protected override Accessibility GetAccessibility(State state, SemanticModel semanticModel, bool intoNamespace, CancellationToken cancellationToken) + { + var accessibility = DetermineDefaultAccessibility(state, semanticModel, intoNamespace, cancellationToken); + if (!state.IsTypeGeneratedIntoNamespaceFromMemberAccess) { - foreach (var member in members) + var accessibilityConstraint = DetermineAccessibilityConstraint(state, semanticModel, cancellationToken); + + if (accessibilityConstraint is Accessibility.Public or + Accessibility.Internal) { - if (member is BaseNamespaceDeclarationSyntax namespaceDeclaration) - { - var found = FindNamespaceInNamespace(namespaceDeclaration, indexDone, containers); - if (found != null) - return found; - } + accessibility = accessibilityConstraint; + } + else if (accessibilityConstraint is Accessibility.Protected or + Accessibility.ProtectedOrInternal) + { + // If nested type is declared in public type then we should generate public type instead of internal + accessibility = AllContainingTypesArePublicOrProtected(state, semanticModel, cancellationToken) + ? Accessibility.Public + : Accessibility.Internal; } - - return null; } - private BaseNamespaceDeclarationSyntax FindNamespaceInNamespace(BaseNamespaceDeclarationSyntax namespaceDecl, int indexDone, List containers) - { - if (namespaceDecl.Name is AliasQualifiedNameSyntax) - return null; + return accessibility; + } + + protected override ITypeSymbol DetermineArgumentType(SemanticModel semanticModel, ArgumentSyntax argument, CancellationToken cancellationToken) + => argument.DetermineParameterType(semanticModel, cancellationToken); - var namespaceContainers = new List(); - GetNamespaceContainers(namespaceDecl.Name, namespaceContainers); + protected override bool IsConversionImplicit(Compilation compilation, ITypeSymbol sourceType, ITypeSymbol targetType) + => compilation.ClassifyConversion(sourceType, targetType).IsImplicit; - if (namespaceContainers.Count + indexDone > containers.Count || - !IdentifierMatches(indexDone, namespaceContainers, containers)) + public override async Task<(INamespaceSymbol, INamespaceOrTypeSymbol, Location)> GetOrGenerateEnclosingNamespaceSymbolAsync( + INamedTypeSymbol namedTypeSymbol, string[] containers, Document selectedDocument, SyntaxNode selectedDocumentRoot, CancellationToken cancellationToken) + { + var compilationUnit = (CompilationUnitSyntax)selectedDocumentRoot; + var semanticModel = await selectedDocument.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (containers.Length != 0) + { + // Search the NS declaration in the root + var containerList = new List(containers); + var enclosingNamespace = FindNamespaceInMemberDeclarations(compilationUnit.Members, indexDone: 0, containerList); + if (enclosingNamespace != null) { - return null; + var enclosingNamespaceSymbol = semanticModel.GetSymbolInfo(enclosingNamespace.Name, cancellationToken); + if (enclosingNamespaceSymbol.Symbol is INamespaceSymbol namespaceSymbol) + return (namespaceSymbol, namedTypeSymbol, enclosingNamespace.GetLastToken().GetLocation()); } + } - indexDone += namespaceContainers.Count; - if (indexDone == containers.Count) - return namespaceDecl; + var globalNamespace = semanticModel.GetEnclosingNamespace(0, cancellationToken); + var rootNamespaceOrType = namedTypeSymbol.GenerateRootNamespaceOrType(containers); + var lastMember = compilationUnit.Members.LastOrDefault(); + var afterThisLocation = lastMember != null + ? semanticModel.SyntaxTree.GetLocation(new TextSpan(lastMember.Span.End, 0)) + : semanticModel.SyntaxTree.GetLocation(new TextSpan()); - return FindNamespaceInMemberDeclarations(namespaceDecl.Members, indexDone, containers); - } + return (globalNamespace, rootNamespaceOrType, afterThisLocation); + } - private static bool IdentifierMatches(int indexDone, List namespaceContainers, List containers) + private BaseNamespaceDeclarationSyntax FindNamespaceInMemberDeclarations(SyntaxList members, int indexDone, List containers) + { + foreach (var member in members) { - for (var i = 0; i < namespaceContainers.Count; ++i) + if (member is BaseNamespaceDeclarationSyntax namespaceDeclaration) { - if (namespaceContainers[i] != containers[indexDone + i]) - { - return false; - } + var found = FindNamespaceInNamespace(namespaceDeclaration, indexDone, containers); + if (found != null) + return found; } - - return true; } - private static void GetNamespaceContainers(NameSyntax name, List namespaceContainers) + return null; + } + + private BaseNamespaceDeclarationSyntax FindNamespaceInNamespace(BaseNamespaceDeclarationSyntax namespaceDecl, int indexDone, List containers) + { + if (namespaceDecl.Name is AliasQualifiedNameSyntax) + return null; + + var namespaceContainers = new List(); + GetNamespaceContainers(namespaceDecl.Name, namespaceContainers); + + if (namespaceContainers.Count + indexDone > containers.Count || + !IdentifierMatches(indexDone, namespaceContainers, containers)) { - if (name is QualifiedNameSyntax qualifiedName) - { - GetNamespaceContainers(qualifiedName.Left, namespaceContainers); - namespaceContainers.Add(qualifiedName.Right.Identifier.ValueText); - } - else - { - Debug.Assert(name is SimpleNameSyntax); - namespaceContainers.Add(((SimpleNameSyntax)name).Identifier.ValueText); - } + return null; } - internal override bool TryGetBaseList(ExpressionSyntax expression, out TypeKindOptions typeKindValue) - { - typeKindValue = TypeKindOptions.AllOptions; + indexDone += namespaceContainers.Count; + if (indexDone == containers.Count) + return namespaceDecl; - if (expression == null) + return FindNamespaceInMemberDeclarations(namespaceDecl.Members, indexDone, containers); + } + + private static bool IdentifierMatches(int indexDone, List namespaceContainers, List containers) + { + for (var i = 0; i < namespaceContainers.Count; ++i) + { + if (namespaceContainers[i] != containers[indexDone + i]) { return false; } + } - var node = expression as SyntaxNode; - - while (node != null) - { - if (node is BaseListSyntax) - { - if (node.Parent.Kind() is SyntaxKind.InterfaceDeclaration or SyntaxKind.StructDeclaration or SyntaxKind.RecordStructDeclaration) - { - typeKindValue = TypeKindOptions.Interface; - return true; - } + return true; + } - typeKindValue = TypeKindOptions.BaseList; - return true; - } + private static void GetNamespaceContainers(NameSyntax name, List namespaceContainers) + { + if (name is QualifiedNameSyntax qualifiedName) + { + GetNamespaceContainers(qualifiedName.Left, namespaceContainers); + namespaceContainers.Add(qualifiedName.Right.Identifier.ValueText); + } + else + { + Debug.Assert(name is SimpleNameSyntax); + namespaceContainers.Add(((SimpleNameSyntax)name).Identifier.ValueText); + } + } - node = node.Parent; - } + internal override bool TryGetBaseList(ExpressionSyntax expression, out TypeKindOptions typeKindValue) + { + typeKindValue = TypeKindOptions.AllOptions; + if (expression == null) + { return false; } - internal override bool IsPublicOnlyAccessibility(ExpressionSyntax expression, Project project) - { - if (expression == null) - return false; - - var node = expression as SyntaxNode; - SyntaxNode previousNode = null; + var node = expression as SyntaxNode; - while (node != null) + while (node != null) + { + if (node is BaseListSyntax) { - // Types in BaseList, Type Constraint or Member Types cannot be of more restricted accessibility than the declaring type - if (node is BaseListSyntax or TypeParameterConstraintClauseSyntax && - node.Parent != null && - node.Parent is TypeDeclarationSyntax) + if (node.Parent.Kind() is SyntaxKind.InterfaceDeclaration or SyntaxKind.StructDeclaration or SyntaxKind.RecordStructDeclaration) { - if (node.Parent is TypeDeclarationSyntax typeDecl) - { - // The Type Decl which contains the BaseList needs to contain Public - return typeDecl.GetModifiers().Any(SyntaxKind.PublicKeyword) && IsAllContainingTypeDeclsPublic(typeDecl); - } - - throw ExceptionUtilities.Unreachable(); - } - - if (node is EventDeclarationSyntax or EventFieldDeclarationSyntax && - node.Parent != null && - node.Parent is TypeDeclarationSyntax) - { - // Make sure the GFU is not inside the Accessors - // Make sure that Event Declaration themselves are Public in the first place - return previousNode is not AccessorListSyntax && - node.GetModifiers().Any(SyntaxKind.PublicKeyword) && - IsAllContainingTypeDeclsPublic(node); + typeKindValue = TypeKindOptions.Interface; + return true; } - previousNode = node; - node = node.Parent; + typeKindValue = TypeKindOptions.BaseList; + return true; } - return false; + node = node.Parent; } - private static bool IsAllContainingTypeDeclsPublic(SyntaxNode node) - { - // Make sure that all the containing Type Declarations are also Public - var containingTypeDeclarations = node.GetAncestors(); - return !containingTypeDeclarations.Any() - || containingTypeDeclarations.All(typedecl => typedecl.GetModifiers().Any(SyntaxKind.PublicKeyword)); - } + return false; + } - internal override bool IsGenericName(SimpleNameSyntax simpleName) - => simpleName is GenericNameSyntax; + internal override bool IsPublicOnlyAccessibility(ExpressionSyntax expression, Project project) + { + if (expression == null) + return false; - internal override bool IsSimpleName(ExpressionSyntax expression) - => expression is SimpleNameSyntax; + var node = expression as SyntaxNode; + SyntaxNode previousNode = null; - internal override async Task TryAddUsingsOrImportToDocumentAsync( - Solution updatedSolution, SyntaxNode modifiedRoot, Document document, SimpleNameSyntax simpleName, string includeUsingsOrImports, AddImportPlacementOptionsProvider fallbackOptions, CancellationToken cancellationToken) + while (node != null) { - // Nothing to include - if (string.IsNullOrWhiteSpace(includeUsingsOrImports)) + // Types in BaseList, Type Constraint or Member Types cannot be of more restricted accessibility than the declaring type + if (node is BaseListSyntax or TypeParameterConstraintClauseSyntax && + node.Parent != null && + node.Parent is TypeDeclarationSyntax) { - return updatedSolution; - } + if (node.Parent is TypeDeclarationSyntax typeDecl) + { + // The Type Decl which contains the BaseList needs to contain Public + return typeDecl.GetModifiers().Any(SyntaxKind.PublicKeyword) && IsAllContainingTypeDeclsPublic(typeDecl); + } - SyntaxNode root = null; - if (modifiedRoot == null) - { - root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + throw ExceptionUtilities.Unreachable(); } - else + + if (node is EventDeclarationSyntax or EventFieldDeclarationSyntax && + node.Parent != null && + node.Parent is TypeDeclarationSyntax) { - root = modifiedRoot; + // Make sure the GFU is not inside the Accessors + // Make sure that Event Declaration themselves are Public in the first place + return previousNode is not AccessorListSyntax && + node.GetModifiers().Any(SyntaxKind.PublicKeyword) && + IsAllContainingTypeDeclsPublic(node); } - if (root is CompilationUnitSyntax compilationRoot) - { - var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(includeUsingsOrImports)); + previousNode = node; + node = node.Parent; + } - // Check if the usings is already present - if (compilationRoot.Usings.Where(n => n != null && n.Alias == null) - .Select(n => n.Name.ToString()) - .Any(n => n.Equals(includeUsingsOrImports))) - { - return updatedSolution; - } + return false; + } - // Check if the GFU is triggered from the namespace same as the usings namespace - if (await IsWithinTheImportingNamespaceAsync(document, simpleName.SpanStart, includeUsingsOrImports, cancellationToken).ConfigureAwait(false)) - { - return updatedSolution; - } + private static bool IsAllContainingTypeDeclsPublic(SyntaxNode node) + { + // Make sure that all the containing Type Declarations are also Public + var containingTypeDeclarations = node.GetAncestors(); + return !containingTypeDeclarations.Any() + || containingTypeDeclarations.All(typedecl => typedecl.GetModifiers().Any(SyntaxKind.PublicKeyword)); + } - var addImportOptions = await document.GetAddImportPlacementOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var addedCompilationRoot = compilationRoot.AddUsingDirectives([usingDirective], addImportOptions.PlaceSystemNamespaceFirst, Formatter.Annotation); - updatedSolution = updatedSolution.WithDocumentSyntaxRoot(document.Id, addedCompilationRoot, PreservationMode.PreserveIdentity); - } + internal override bool IsGenericName(SimpleNameSyntax simpleName) + => simpleName is GenericNameSyntax; + + internal override bool IsSimpleName(ExpressionSyntax expression) + => expression is SimpleNameSyntax; + internal override async Task TryAddUsingsOrImportToDocumentAsync( + Solution updatedSolution, SyntaxNode modifiedRoot, Document document, SimpleNameSyntax simpleName, string includeUsingsOrImports, AddImportPlacementOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + // Nothing to include + if (string.IsNullOrWhiteSpace(includeUsingsOrImports)) + { return updatedSolution; } - private static ITypeSymbol GetPropertyType( - SimpleNameSyntax propertyName, - SemanticModel semanticModel, - ITypeInferenceService typeInference, - CancellationToken cancellationToken) + SyntaxNode root = null; + if (modifiedRoot == null) + { + root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + } + else + { + root = modifiedRoot; + } + + if (root is CompilationUnitSyntax compilationRoot) { - if (propertyName.Parent is AssignmentExpressionSyntax parentAssignment) + var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(includeUsingsOrImports)); + + // Check if the usings is already present + if (compilationRoot.Usings.Where(n => n != null && n.Alias == null) + .Select(n => n.Name.ToString()) + .Any(n => n.Equals(includeUsingsOrImports))) { - return typeInference.InferType( - semanticModel, parentAssignment.Left, objectAsDefault: true, cancellationToken: cancellationToken); + return updatedSolution; } - if (propertyName.Parent is IsPatternExpressionSyntax isPatternExpression) + // Check if the GFU is triggered from the namespace same as the usings namespace + if (await IsWithinTheImportingNamespaceAsync(document, simpleName.SpanStart, includeUsingsOrImports, cancellationToken).ConfigureAwait(false)) { - return typeInference.InferType( - semanticModel, isPatternExpression.Expression, objectAsDefault: true, cancellationToken: cancellationToken); + return updatedSolution; } - return null; + var addImportOptions = await document.GetAddImportPlacementOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var addedCompilationRoot = compilationRoot.AddUsingDirectives([usingDirective], addImportOptions.PlaceSystemNamespaceFirst, Formatter.Annotation); + updatedSolution = updatedSolution.WithDocumentSyntaxRoot(document.Id, addedCompilationRoot, PreservationMode.PreserveIdentity); } - private static IPropertySymbol CreatePropertySymbol( - SimpleNameSyntax propertyName, ITypeSymbol propertyType) + return updatedSolution; + } + + private static ITypeSymbol GetPropertyType( + SimpleNameSyntax propertyName, + SemanticModel semanticModel, + ITypeInferenceService typeInference, + CancellationToken cancellationToken) + { + if (propertyName.Parent is AssignmentExpressionSyntax parentAssignment) { - return CodeGenerationSymbolFactory.CreatePropertySymbol( - attributes: [], - accessibility: Accessibility.Public, - modifiers: new DeclarationModifiers(), - explicitInterfaceImplementations: default, - name: propertyName.Identifier.ValueText, - type: propertyType, - refKind: RefKind.None, - parameters: default, - getMethod: s_accessor, - setMethod: s_accessor, - isIndexer: false); + return typeInference.InferType( + semanticModel, parentAssignment.Left, objectAsDefault: true, cancellationToken: cancellationToken); } - private static readonly IMethodSymbol s_accessor = CodeGenerationSymbolFactory.CreateAccessorSymbol( - attributes: default, + if (propertyName.Parent is IsPatternExpressionSyntax isPatternExpression) + { + return typeInference.InferType( + semanticModel, isPatternExpression.Expression, objectAsDefault: true, cancellationToken: cancellationToken); + } + + return null; + } + + private static IPropertySymbol CreatePropertySymbol( + SimpleNameSyntax propertyName, ITypeSymbol propertyType) + { + return CodeGenerationSymbolFactory.CreatePropertySymbol( + attributes: [], accessibility: Accessibility.Public, - statements: default); + modifiers: new DeclarationModifiers(), + explicitInterfaceImplementations: default, + name: propertyName.Identifier.ValueText, + type: propertyType, + refKind: RefKind.None, + parameters: default, + getMethod: s_accessor, + setMethod: s_accessor, + isIndexer: false); + } - internal override bool TryGenerateProperty( - SimpleNameSyntax propertyName, - SemanticModel semanticModel, - ITypeInferenceService typeInference, - CancellationToken cancellationToken, - out IPropertySymbol property) + private static readonly IMethodSymbol s_accessor = CodeGenerationSymbolFactory.CreateAccessorSymbol( + attributes: default, + accessibility: Accessibility.Public, + statements: default); + + internal override bool TryGenerateProperty( + SimpleNameSyntax propertyName, + SemanticModel semanticModel, + ITypeInferenceService typeInference, + CancellationToken cancellationToken, + out IPropertySymbol property) + { + var propertyType = GetPropertyType(propertyName, semanticModel, typeInference, cancellationToken); + if (propertyType is null or IErrorTypeSymbol) { - var propertyType = GetPropertyType(propertyName, semanticModel, typeInference, cancellationToken); - if (propertyType is null or IErrorTypeSymbol) - { - property = CreatePropertySymbol(propertyName, semanticModel.Compilation.ObjectType); - return property != null; - } - - property = CreatePropertySymbol(propertyName, propertyType); + property = CreatePropertySymbol(propertyName, semanticModel.Compilation.ObjectType); return property != null; } + + property = CreatePropertySymbol(propertyName, propertyType); + return property != null; } } diff --git a/src/Features/CSharp/Portable/GenerateVariable/CSharpGenerateVariableCodeFixProvider.cs b/src/Features/CSharp/Portable/GenerateVariable/CSharpGenerateVariableCodeFixProvider.cs index 55875ace9caff..58d06f9ae15b1 100644 --- a/src/Features/CSharp/Portable/GenerateVariable/CSharpGenerateVariableCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/GenerateVariable/CSharpGenerateVariableCodeFixProvider.cs @@ -18,51 +18,50 @@ using Microsoft.CodeAnalysis.GenerateMember.GenerateVariable; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.GenerateVariable +namespace Microsoft.CodeAnalysis.CSharp.GenerateVariable; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateVariable), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateMethod)] +internal class CSharpGenerateVariableCodeFixProvider : AbstractGenerateMemberCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.GenerateVariable), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateMethod)] - internal class CSharpGenerateVariableCodeFixProvider : AbstractGenerateMemberCodeFixProvider - { - private const string CS1061 = nameof(CS1061); // error CS1061: 'C' does not contain a definition for 'Goo' and no extension method 'Goo' accepting a first argument of type 'C' could be found - private const string CS0103 = nameof(CS0103); // error CS0103: The name 'Goo' does not exist in the current context - private const string CS0117 = nameof(CS0117); // error CS0117: 'TestNs.Program' does not contain a definition for 'blah' - private const string CS0539 = nameof(CS0539); // error CS0539: 'Class.SomeProp' in explicit interface declaration is not a member of interface - private const string CS0246 = nameof(CS0246); // error CS0246: The type or namespace name 'Version' could not be found - private const string CS0120 = nameof(CS0120); // error CS0120: An object reference is required for the non-static field, method, or property 'A' - private const string CS0118 = nameof(CS0118); // error CS0118: 'C' is a type but is used like a variable + private const string CS1061 = nameof(CS1061); // error CS1061: 'C' does not contain a definition for 'Goo' and no extension method 'Goo' accepting a first argument of type 'C' could be found + private const string CS0103 = nameof(CS0103); // error CS0103: The name 'Goo' does not exist in the current context + private const string CS0117 = nameof(CS0117); // error CS0117: 'TestNs.Program' does not contain a definition for 'blah' + private const string CS0539 = nameof(CS0539); // error CS0539: 'Class.SomeProp' in explicit interface declaration is not a member of interface + private const string CS0246 = nameof(CS0246); // error CS0246: The type or namespace name 'Version' could not be found + private const string CS0120 = nameof(CS0120); // error CS0120: An object reference is required for the non-static field, method, or property 'A' + private const string CS0118 = nameof(CS0118); // error CS0118: 'C' is a type but is used like a variable - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpGenerateVariableCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpGenerateVariableCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds - => [CS1061, CS0103, CS0117, CS0539, CS0246, CS0120, CS0118]; + public override ImmutableArray FixableDiagnosticIds + => [CS1061, CS0103, CS0117, CS0539, CS0246, CS0120, CS0118]; - protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) - => node is SimpleNameSyntax or PropertyDeclarationSyntax or MemberBindingExpressionSyntax; + protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) + => node is SimpleNameSyntax or PropertyDeclarationSyntax or MemberBindingExpressionSyntax; - protected override SyntaxNode GetTargetNode(SyntaxNode node) + protected override SyntaxNode GetTargetNode(SyntaxNode node) + { + if (node.IsKind(SyntaxKind.MemberBindingExpression)) { - if (node.IsKind(SyntaxKind.MemberBindingExpression)) + var nameNode = node.ChildNodes().FirstOrDefault(n => n.IsKind(SyntaxKind.IdentifierName)); + if (nameNode != null) { - var nameNode = node.ChildNodes().FirstOrDefault(n => n.IsKind(SyntaxKind.IdentifierName)); - if (nameNode != null) - { - return nameNode; - } + return nameNode; } - - return base.GetTargetNode(node); } - protected override Task> GetCodeActionsAsync( - Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var service = document.GetLanguageService(); - return service.GenerateVariableAsync(document, node, fallbackOptions, cancellationToken); - } + return base.GetTargetNode(node); + } + + protected override Task> GetCodeActionsAsync( + Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var service = document.GetLanguageService(); + return service.GenerateVariableAsync(document, node, fallbackOptions, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/GoToDefinition/CSharpGoToDefinitionSymbolService.cs b/src/Features/CSharp/Portable/GoToDefinition/CSharpGoToDefinitionSymbolService.cs index dc7bf4286468e..15c8118bed05b 100644 --- a/src/Features/CSharp/Portable/GoToDefinition/CSharpGoToDefinitionSymbolService.cs +++ b/src/Features/CSharp/Portable/GoToDefinition/CSharpGoToDefinitionSymbolService.cs @@ -13,130 +13,129 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.GoToDefinition +namespace Microsoft.CodeAnalysis.CSharp.GoToDefinition; + +[ExportLanguageService(typeof(IGoToDefinitionSymbolService), LanguageNames.CSharp), Shared] +internal class CSharpGoToDefinitionSymbolService : AbstractGoToDefinitionSymbolService { - [ExportLanguageService(typeof(IGoToDefinitionSymbolService), LanguageNames.CSharp), Shared] - internal class CSharpGoToDefinitionSymbolService : AbstractGoToDefinitionSymbolService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpGoToDefinitionSymbolService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpGoToDefinitionSymbolService() - { - } + } - protected override ISymbol FindRelatedExplicitlyDeclaredSymbol(ISymbol symbol, Compilation compilation) - => symbol; + protected override ISymbol FindRelatedExplicitlyDeclaredSymbol(ISymbol symbol, Compilation compilation) + => symbol; - protected override int? GetTargetPositionIfControlFlow(SemanticModel semanticModel, SyntaxToken token) - { - var node = token.GetRequiredParent(); + protected override int? GetTargetPositionIfControlFlow(SemanticModel semanticModel, SyntaxToken token) + { + var node = token.GetRequiredParent(); - switch (token.Kind()) - { - case SyntaxKind.ContinueKeyword: - var foundContinuedLoop = TryFindContinuableConstruct(node); + switch (token.Kind()) + { + case SyntaxKind.ContinueKeyword: + var foundContinuedLoop = TryFindContinuableConstruct(node); - return foundContinuedLoop?.IsContinuableConstruct() == true - ? foundContinuedLoop.GetFirstToken().Span.Start - : null; + return foundContinuedLoop?.IsContinuableConstruct() == true + ? foundContinuedLoop.GetFirstToken().Span.Start + : null; - case SyntaxKind.BreakKeyword: - if (token.GetPreviousToken().IsKind(SyntaxKind.YieldKeyword)) - { - goto case SyntaxKind.YieldKeyword; - } + case SyntaxKind.BreakKeyword: + if (token.GetPreviousToken().IsKind(SyntaxKind.YieldKeyword)) + { + goto case SyntaxKind.YieldKeyword; + } - var foundBrokenLoop = TryFindBreakableConstruct(node); + var foundBrokenLoop = TryFindBreakableConstruct(node); - return foundBrokenLoop?.IsBreakableConstruct() == true - ? foundBrokenLoop.GetLastToken().Span.End - : null; + return foundBrokenLoop?.IsBreakableConstruct() == true + ? foundBrokenLoop.GetLastToken().Span.End + : null; - case SyntaxKind.YieldKeyword: - case SyntaxKind.ReturnKeyword: + case SyntaxKind.YieldKeyword: + case SyntaxKind.ReturnKeyword: + { + var foundReturnableConstruct = TryFindContainingReturnableConstruct(node); + if (foundReturnableConstruct is null) { - var foundReturnableConstruct = TryFindContainingReturnableConstruct(node); - if (foundReturnableConstruct is null) - { - return null; - } - - var symbol = semanticModel.GetDeclaredSymbol(foundReturnableConstruct); - if (symbol is null) - { - // for lambdas - return foundReturnableConstruct.GetFirstToken().Span.Start; - } - - return symbol.Locations.FirstOrDefault()?.SourceSpan.Start ?? 0; + return null; } - case SyntaxKind.GotoKeyword: - case SyntaxKind.DefaultKeyword: - case SyntaxKind.CaseKeyword: + var symbol = semanticModel.GetDeclaredSymbol(foundReturnableConstruct); + if (symbol is null) { - if (node.FirstAncestorOrSelf() is not GotoStatementSyntax gotoStatement) - return null; - - if (semanticModel.GetOperation(gotoStatement) is not IBranchOperation gotoOperation) - return null; - - Debug.Assert(gotoOperation is { BranchKind: BranchKind.GoTo }); - var target = gotoOperation.Target; - return target.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax()?.SpanStart; + // for lambdas + return foundReturnableConstruct.GetFirstToken().Span.Start; } - } - return null; + return symbol.Locations.FirstOrDefault()?.SourceSpan.Start ?? 0; + } - static SyntaxNode? TryFindContinuableConstruct(SyntaxNode? node) - { - while (node is not null && !node.IsContinuableConstruct()) + case SyntaxKind.GotoKeyword: + case SyntaxKind.DefaultKeyword: + case SyntaxKind.CaseKeyword: { - var kind = node.Kind(); + if (node.FirstAncestorOrSelf() is not GotoStatementSyntax gotoStatement) + return null; - if (node.IsReturnableConstruct() || - SyntaxFacts.GetTypeDeclarationKind(kind) != SyntaxKind.None) - { + if (semanticModel.GetOperation(gotoStatement) is not IBranchOperation gotoOperation) return null; - } - node = node.Parent; + Debug.Assert(gotoOperation is { BranchKind: BranchKind.GoTo }); + var target = gotoOperation.Target; + return target.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax()?.SpanStart; } + } - return node; - } + return null; - static SyntaxNode? TryFindBreakableConstruct(SyntaxNode? node) + static SyntaxNode? TryFindContinuableConstruct(SyntaxNode? node) + { + while (node is not null && !node.IsContinuableConstruct()) { - while (node is not null && !node.IsBreakableConstruct()) - { - if (node.IsReturnableConstruct() || - SyntaxFacts.GetTypeDeclarationKind(node.Kind()) != SyntaxKind.None) - { - return null; - } + var kind = node.Kind(); - node = node.Parent; + if (node.IsReturnableConstruct() || + SyntaxFacts.GetTypeDeclarationKind(kind) != SyntaxKind.None) + { + return null; } - return node; + node = node.Parent; } - static SyntaxNode? TryFindContainingReturnableConstruct(SyntaxNode? node) + return node; + } + + static SyntaxNode? TryFindBreakableConstruct(SyntaxNode? node) + { + while (node is not null && !node.IsBreakableConstruct()) { - while (node is not null && !node.IsReturnableConstruct()) + if (node.IsReturnableConstruct() || + SyntaxFacts.GetTypeDeclarationKind(node.Kind()) != SyntaxKind.None) { - if (SyntaxFacts.GetTypeDeclarationKind(node.Kind()) != SyntaxKind.None) - { - return null; - } + return null; + } - node = node.Parent; + node = node.Parent; + } + + return node; + } + + static SyntaxNode? TryFindContainingReturnableConstruct(SyntaxNode? node) + { + while (node is not null && !node.IsReturnableConstruct()) + { + if (SyntaxFacts.GetTypeDeclarationKind(node.Kind()) != SyntaxKind.None) + { + return null; } - return node; + node = node.Parent; } + + return node; } } } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/AsyncAwaitHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/AsyncAwaitHighlighter.cs index 116ec2d3904ea..ced8b66e42e30 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/AsyncAwaitHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/AsyncAwaitHighlighter.cs @@ -16,113 +16,112 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class AsyncAwaitHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class AsyncAwaitHighlighter : AbstractKeywordHighlighter + private static readonly ObjectPool> s_stackPool + = SharedPools.Default>(); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public AsyncAwaitHighlighter() { - private static readonly ObjectPool> s_stackPool - = SharedPools.Default>(); + } - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public AsyncAwaitHighlighter() + protected override bool IsHighlightableNode(SyntaxNode node) + => node.IsReturnableConstructOrTopLevelCompilationUnit(); + + protected override void AddHighlightsForNode(SyntaxNode node, List highlights, CancellationToken cancellationToken) + { + foreach (var current in WalkChildren(node)) { + cancellationToken.ThrowIfCancellationRequested(); + HighlightRelatedKeywords(current, highlights); } + } - protected override bool IsHighlightableNode(SyntaxNode node) - => node.IsReturnableConstructOrTopLevelCompilationUnit(); + private static IEnumerable WalkChildren(SyntaxNode node) + { + using var pooledObject = s_stackPool.GetPooledObject(); - protected override void AddHighlightsForNode(SyntaxNode node, List highlights, CancellationToken cancellationToken) - { - foreach (var current in WalkChildren(node)) - { - cancellationToken.ThrowIfCancellationRequested(); - HighlightRelatedKeywords(current, highlights); - } - } + var stack = pooledObject.Object; + stack.Push(node); - private static IEnumerable WalkChildren(SyntaxNode node) + while (stack.Count > 0) { - using var pooledObject = s_stackPool.GetPooledObject(); + var current = stack.Pop(); + yield return current; - var stack = pooledObject.Object; - stack.Push(node); - - while (stack.Count > 0) + // 'Reverse' isn't really necessary, but it means we walk the nodes in document + // order, which is nicer when debugging and understanding the results produced. + foreach (var child in current.ChildNodesAndTokens().Reverse()) { - var current = stack.Pop(); - yield return current; - - // 'Reverse' isn't really necessary, but it means we walk the nodes in document - // order, which is nicer when debugging and understanding the results produced. - foreach (var child in current.ChildNodesAndTokens().Reverse()) + if (child.IsNode) { - if (child.IsNode) + var childNode = child.AsNode(); + + // Only process children if they're not the start of another construct + // that async/await would be related to. + if (!childNode.IsReturnableConstruct()) { - var childNode = child.AsNode(); - - // Only process children if they're not the start of another construct - // that async/await would be related to. - if (!childNode.IsReturnableConstruct()) - { - stack.Push(childNode); - } + stack.Push(childNode); } } } } + } - private static bool HighlightRelatedKeywords(SyntaxNode node, List spans) - => node switch - { - MethodDeclarationSyntax methodDeclaration => TryAddAsyncModifier(methodDeclaration.Modifiers, spans), - LocalFunctionStatementSyntax localFunction => TryAddAsyncModifier(localFunction.Modifiers, spans), - AnonymousFunctionExpressionSyntax anonymousFunction => TryAddAsyncOrAwaitKeyword(anonymousFunction.AsyncKeyword, spans), - UsingStatementSyntax usingStatement => TryAddAsyncOrAwaitKeyword(usingStatement.AwaitKeyword, spans), - LocalDeclarationStatementSyntax localDeclaration => - localDeclaration.UsingKeyword.Kind() == SyntaxKind.UsingKeyword && TryAddAsyncOrAwaitKeyword(localDeclaration.AwaitKeyword, spans), - CommonForEachStatementSyntax forEachStatement => TryAddAsyncOrAwaitKeyword(forEachStatement.AwaitKeyword, spans), - AwaitExpressionSyntax awaitExpression => TryAddAsyncOrAwaitKeyword(awaitExpression.AwaitKeyword, spans), - _ => false, - }; - - private static bool TryAddAsyncModifier(SyntaxTokenList modifiers, List spans) + private static bool HighlightRelatedKeywords(SyntaxNode node, List spans) + => node switch + { + MethodDeclarationSyntax methodDeclaration => TryAddAsyncModifier(methodDeclaration.Modifiers, spans), + LocalFunctionStatementSyntax localFunction => TryAddAsyncModifier(localFunction.Modifiers, spans), + AnonymousFunctionExpressionSyntax anonymousFunction => TryAddAsyncOrAwaitKeyword(anonymousFunction.AsyncKeyword, spans), + UsingStatementSyntax usingStatement => TryAddAsyncOrAwaitKeyword(usingStatement.AwaitKeyword, spans), + LocalDeclarationStatementSyntax localDeclaration => + localDeclaration.UsingKeyword.Kind() == SyntaxKind.UsingKeyword && TryAddAsyncOrAwaitKeyword(localDeclaration.AwaitKeyword, spans), + CommonForEachStatementSyntax forEachStatement => TryAddAsyncOrAwaitKeyword(forEachStatement.AwaitKeyword, spans), + AwaitExpressionSyntax awaitExpression => TryAddAsyncOrAwaitKeyword(awaitExpression.AwaitKeyword, spans), + _ => false, + }; + + private static bool TryAddAsyncModifier(SyntaxTokenList modifiers, List spans) + { + foreach (var mod in modifiers) { - foreach (var mod in modifiers) + if (TryAddAsyncOrAwaitKeyword(mod, spans)) { - if (TryAddAsyncOrAwaitKeyword(mod, spans)) - { - return true; - } + return true; } - - return false; } - private static bool TryAddAsyncOrAwaitKeyword(SyntaxToken mod, List spans) + return false; + } + + private static bool TryAddAsyncOrAwaitKeyword(SyntaxToken mod, List spans) + { + if (mod.Kind() is SyntaxKind.AsyncKeyword or SyntaxKind.AwaitKeyword) { - if (mod.Kind() is SyntaxKind.AsyncKeyword or SyntaxKind.AwaitKeyword) - { - // Note if there is already a highlight for the previous token, merge it with this - // span. That way, we highlight nested awaits with a single span. + // Note if there is already a highlight for the previous token, merge it with this + // span. That way, we highlight nested awaits with a single span. - if (spans.Count > 0) + if (spans.Count > 0) + { + var previousToken = mod.GetPreviousToken(); + var lastSpan = spans[^1]; + if (lastSpan == previousToken.Span) { - var previousToken = mod.GetPreviousToken(); - var lastSpan = spans[^1]; - if (lastSpan == previousToken.Span) - { - spans[^1] = TextSpan.FromBounds(lastSpan.Start, mod.Span.End); - return true; - } + spans[^1] = TextSpan.FromBounds(lastSpan.Start, mod.Span.End); + return true; } - - spans.Add(mod.Span); - return true; } - return false; + spans.Add(mod.Span); + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/CheckedExpressionHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/CheckedExpressionHighlighter.cs index ee149464f227d..4845992ab9778 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/CheckedExpressionHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/CheckedExpressionHighlighter.cs @@ -13,18 +13,17 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class CheckedExpressionHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class CheckedExpressionHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CheckedExpressionHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CheckedExpressionHighlighter() - { - } - - protected override void AddHighlights(CheckedExpressionSyntax checkedExpressionSyntax, List highlights, CancellationToken cancellationToken) - => highlights.Add(checkedExpressionSyntax.Keyword.Span); } + + protected override void AddHighlights(CheckedExpressionSyntax checkedExpressionSyntax, List highlights, CancellationToken cancellationToken) + => highlights.Add(checkedExpressionSyntax.Keyword.Span); } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/CheckedStatementHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/CheckedStatementHighlighter.cs index 9188fa2b8f8fc..a09715e9a5c4e 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/CheckedStatementHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/CheckedStatementHighlighter.cs @@ -13,18 +13,17 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class CheckedStatementHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class CheckedStatementHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CheckedStatementHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CheckedStatementHighlighter() - { - } - - protected override void AddHighlights(CheckedStatementSyntax checkedStatement, List highlights, CancellationToken cancellationToken) - => highlights.Add(checkedStatement.Keyword.Span); } + + protected override void AddHighlights(CheckedStatementSyntax checkedStatement, List highlights, CancellationToken cancellationToken) + => highlights.Add(checkedStatement.Keyword.Span); } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/ConditionalPreprocessorHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/ConditionalPreprocessorHighlighter.cs index 9073c2b843fe9..8b6df00b2cc9d 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/ConditionalPreprocessorHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/ConditionalPreprocessorHighlighter.cs @@ -12,27 +12,26 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class ConditionalPreprocessorHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class ConditionalPreprocessorHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConditionalPreprocessorHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConditionalPreprocessorHighlighter() - { - } + } - protected override void AddHighlights( - DirectiveTriviaSyntax directive, List highlights, CancellationToken cancellationToken) + protected override void AddHighlights( + DirectiveTriviaSyntax directive, List highlights, CancellationToken cancellationToken) + { + var conditionals = directive.GetMatchingConditionalDirectives(cancellationToken); + foreach (var conditional in conditionals) { - var conditionals = directive.GetMatchingConditionalDirectives(cancellationToken); - foreach (var conditional in conditionals) - { - highlights.Add(TextSpan.FromBounds( - conditional.HashToken.SpanStart, - conditional.DirectiveNameToken.Span.End)); - } + highlights.Add(TextSpan.FromBounds( + conditional.HashToken.SpanStart, + conditional.DirectiveNameToken.Span.End)); } } } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/IfStatementHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/IfStatementHighlighter.cs index da50a02aeaea2..891730244270e 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/IfStatementHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/IfStatementHighlighter.cs @@ -17,69 +17,68 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class IfStatementHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class IfStatementHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public IfStatementHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public IfStatementHighlighter() - { - } + } - protected override void AddHighlights( - IfStatementSyntax ifStatement, List highlights, CancellationToken cancellationToken) + protected override void AddHighlights( + IfStatementSyntax ifStatement, List highlights, CancellationToken cancellationToken) + { + if (ifStatement.Parent.Kind() != SyntaxKind.ElseClause) { - if (ifStatement.Parent.Kind() != SyntaxKind.ElseClause) - { - ComputeSpans(ifStatement, highlights); - } + ComputeSpans(ifStatement, highlights); } + } + + private static void ComputeSpans( + IfStatementSyntax ifStatement, List highlights) + { + highlights.Add(ifStatement.IfKeyword.Span); - private static void ComputeSpans( - IfStatementSyntax ifStatement, List highlights) + // Loop to get all the else if parts + while (ifStatement != null && ifStatement.Else != null) { - highlights.Add(ifStatement.IfKeyword.Span); + // Check for 'else if' scenario' (the statement in the else clause is an if statement) + var elseKeyword = ifStatement.Else.ElseKeyword; - // Loop to get all the else if parts - while (ifStatement != null && ifStatement.Else != null) + if (ifStatement.Else.Statement is IfStatementSyntax elseIfStatement) { - // Check for 'else if' scenario' (the statement in the else clause is an if statement) - var elseKeyword = ifStatement.Else.ElseKeyword; - - if (ifStatement.Else.Statement is IfStatementSyntax elseIfStatement) + if (OnlySpacesBetween(elseKeyword, elseIfStatement.IfKeyword)) { - if (OnlySpacesBetween(elseKeyword, elseIfStatement.IfKeyword)) - { - // Highlight both else and if tokens if they are on the same line - highlights.Add(TextSpan.FromBounds( - elseKeyword.SpanStart, - elseIfStatement.IfKeyword.Span.End)); - } - else - { - // Highlight the else and if tokens separately - highlights.Add(elseKeyword.Span); - highlights.Add(elseIfStatement.IfKeyword.Span); - } - - // Continue the enumeration looking for more else blocks - ifStatement = elseIfStatement; + // Highlight both else and if tokens if they are on the same line + highlights.Add(TextSpan.FromBounds( + elseKeyword.SpanStart, + elseIfStatement.IfKeyword.Span.End)); } else { - // Highlight just the else and we're done + // Highlight the else and if tokens separately highlights.Add(elseKeyword.Span); - break; + highlights.Add(elseIfStatement.IfKeyword.Span); } + + // Continue the enumeration looking for more else blocks + ifStatement = elseIfStatement; + } + else + { + // Highlight just the else and we're done + highlights.Add(elseKeyword.Span); + break; } } + } - public static bool OnlySpacesBetween(SyntaxToken first, SyntaxToken second) - { - return first.TrailingTrivia.AsString().All(c => c == ' ') && - second.LeadingTrivia.AsString().All(c => c == ' '); - } + public static bool OnlySpacesBetween(SyntaxToken first, SyntaxToken second) + { + return first.TrailingTrivia.AsString().All(c => c == ' ') && + second.LeadingTrivia.AsString().All(c => c == ' '); } } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/LockStatementHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/LockStatementHighlighter.cs index deeb539d6f84f..9e05054a04bc4 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/LockStatementHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/LockStatementHighlighter.cs @@ -13,18 +13,17 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class LockStatementHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class LockStatementHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LockStatementHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public LockStatementHighlighter() - { - } - - protected override void AddHighlights(LockStatementSyntax lockStatement, List highlights, CancellationToken cancellationToken) - => highlights.Add(lockStatement.LockKeyword.Span); } + + protected override void AddHighlights(LockStatementSyntax lockStatement, List highlights, CancellationToken cancellationToken) + => highlights.Add(lockStatement.LockKeyword.Span); } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/LoopHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/LoopHighlighter.cs index f60cda400ed8f..89440de1778c6 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/LoopHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/LoopHighlighter.cs @@ -15,88 +15,87 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class LoopHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class LoopHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LoopHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public LoopHighlighter() - { - } + } - protected override bool IsHighlightableNode(SyntaxNode node) - => node.IsContinuableConstruct(); + protected override bool IsHighlightableNode(SyntaxNode node) + => node.IsContinuableConstruct(); - protected override void AddHighlightsForNode( - SyntaxNode node, List spans, CancellationToken cancellationToken) + protected override void AddHighlightsForNode( + SyntaxNode node, List spans, CancellationToken cancellationToken) + { + switch (node) { - switch (node) - { - case DoStatementSyntax doStatement: - HighlightDoStatement(doStatement, spans); - break; - case ForStatementSyntax forStatement: - HighlightForStatement(forStatement, spans); - break; - case CommonForEachStatementSyntax forEachStatement: - HighlightForEachStatement(forEachStatement, spans); - break; - case WhileStatementSyntax whileStatement: - HighlightWhileStatement(whileStatement, spans); - break; - } - - HighlightRelatedKeywords(node, spans, highlightBreaks: true, highlightContinues: true); + case DoStatementSyntax doStatement: + HighlightDoStatement(doStatement, spans); + break; + case ForStatementSyntax forStatement: + HighlightForStatement(forStatement, spans); + break; + case CommonForEachStatementSyntax forEachStatement: + HighlightForEachStatement(forEachStatement, spans); + break; + case WhileStatementSyntax whileStatement: + HighlightWhileStatement(whileStatement, spans); + break; } - private static void HighlightDoStatement(DoStatementSyntax statement, List spans) - { - spans.Add(statement.DoKeyword.Span); - spans.Add(statement.WhileKeyword.Span); - spans.Add(EmptySpan(statement.SemicolonToken.Span.End)); - } + HighlightRelatedKeywords(node, spans, highlightBreaks: true, highlightContinues: true); + } - private static void HighlightForStatement(ForStatementSyntax statement, List spans) - => spans.Add(statement.ForKeyword.Span); + private static void HighlightDoStatement(DoStatementSyntax statement, List spans) + { + spans.Add(statement.DoKeyword.Span); + spans.Add(statement.WhileKeyword.Span); + spans.Add(EmptySpan(statement.SemicolonToken.Span.End)); + } - private static void HighlightForEachStatement(CommonForEachStatementSyntax statement, List spans) - => spans.Add(statement.ForEachKeyword.Span); + private static void HighlightForStatement(ForStatementSyntax statement, List spans) + => spans.Add(statement.ForKeyword.Span); - private static void HighlightWhileStatement(WhileStatementSyntax statement, List spans) - => spans.Add(statement.WhileKeyword.Span); + private static void HighlightForEachStatement(CommonForEachStatementSyntax statement, List spans) + => spans.Add(statement.ForEachKeyword.Span); - /// - /// Finds all breaks and continues that are a child of this node, and adds the appropriate spans to the spans list. - /// - private static void HighlightRelatedKeywords(SyntaxNode node, List spans, - bool highlightBreaks, bool highlightContinues) - { - Debug.Assert(highlightBreaks || highlightContinues); + private static void HighlightWhileStatement(WhileStatementSyntax statement, List spans) + => spans.Add(statement.WhileKeyword.Span); - if (highlightBreaks && node is BreakStatementSyntax breakStatement) - { - spans.Add(breakStatement.BreakKeyword.Span); - spans.Add(EmptySpan(breakStatement.SemicolonToken.Span.End)); - } - else if (highlightContinues && node is ContinueStatementSyntax continueStatement) - { - spans.Add(continueStatement.ContinueKeyword.Span); - spans.Add(EmptySpan(continueStatement.SemicolonToken.Span.End)); - } - else + /// + /// Finds all breaks and continues that are a child of this node, and adds the appropriate spans to the spans list. + /// + private static void HighlightRelatedKeywords(SyntaxNode node, List spans, + bool highlightBreaks, bool highlightContinues) + { + Debug.Assert(highlightBreaks || highlightContinues); + + if (highlightBreaks && node is BreakStatementSyntax breakStatement) + { + spans.Add(breakStatement.BreakKeyword.Span); + spans.Add(EmptySpan(breakStatement.SemicolonToken.Span.End)); + } + else if (highlightContinues && node is ContinueStatementSyntax continueStatement) + { + spans.Add(continueStatement.ContinueKeyword.Span); + spans.Add(EmptySpan(continueStatement.SemicolonToken.Span.End)); + } + else + { + foreach (var child in node.ChildNodes()) { - foreach (var child in node.ChildNodes()) - { - var highlightBreaksForChild = highlightBreaks && !child.IsBreakableConstruct(); - var highlightContinuesForChild = highlightContinues && !child.IsContinuableConstruct(); + var highlightBreaksForChild = highlightBreaks && !child.IsBreakableConstruct(); + var highlightContinuesForChild = highlightContinues && !child.IsContinuableConstruct(); - // Only recurse if we have anything to do - if (highlightBreaksForChild || highlightContinuesForChild) - { - HighlightRelatedKeywords(child, spans, highlightBreaksForChild, highlightContinuesForChild); - } + // Only recurse if we have anything to do + if (highlightBreaksForChild || highlightContinuesForChild) + { + HighlightRelatedKeywords(child, spans, highlightBreaksForChild, highlightContinuesForChild); } } } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/RegionHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/RegionHighlighter.cs index ccd6a104e4422..f31378c09fe7a 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/RegionHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/RegionHighlighter.cs @@ -14,33 +14,32 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class RegionHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class RegionHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RegionHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public RegionHighlighter() - { - } + } - protected override void AddHighlights( - DirectiveTriviaSyntax directive, List highlights, CancellationToken cancellationToken) + protected override void AddHighlights( + DirectiveTriviaSyntax directive, List highlights, CancellationToken cancellationToken) + { + var matchingDirective = directive.GetMatchingDirective(cancellationToken); + if (matchingDirective == null) { - var matchingDirective = directive.GetMatchingDirective(cancellationToken); - if (matchingDirective == null) - { - return; - } + return; + } - highlights.Add(TextSpan.FromBounds( - directive.HashToken.SpanStart, - directive.DirectiveNameToken.Span.End)); + highlights.Add(TextSpan.FromBounds( + directive.HashToken.SpanStart, + directive.DirectiveNameToken.Span.End)); - highlights.Add(TextSpan.FromBounds( - matchingDirective.HashToken.SpanStart, - matchingDirective.DirectiveNameToken.Span.End)); - } + highlights.Add(TextSpan.FromBounds( + matchingDirective.HashToken.SpanStart, + matchingDirective.DirectiveNameToken.Span.End)); } } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/ReturnStatementHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/ReturnStatementHighlighter.cs index bdd9461ebc55d..e49e2afcad4cd 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/ReturnStatementHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/ReturnStatementHighlighter.cs @@ -16,56 +16,55 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class ReturnStatementHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class ReturnStatementHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ReturnStatementHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ReturnStatementHighlighter() - { - } - - protected override void AddHighlights( - ReturnStatementSyntax returnStatement, List spans, CancellationToken cancellationToken) - { - var parent = returnStatement - .GetAncestorsOrThis() - .FirstOrDefault(n => n.IsReturnableConstructOrTopLevelCompilationUnit()); + } - if (parent == null) - { - return; - } + protected override void AddHighlights( + ReturnStatementSyntax returnStatement, List spans, CancellationToken cancellationToken) + { + var parent = returnStatement + .GetAncestorsOrThis() + .FirstOrDefault(n => n.IsReturnableConstructOrTopLevelCompilationUnit()); - HighlightRelatedKeywords(parent, spans); + if (parent == null) + { + return; } - /// - /// Finds all returns that are children of this node, and adds the appropriate spans to the spans list. - /// - private static void HighlightRelatedKeywords(SyntaxNode node, List spans) + HighlightRelatedKeywords(parent, spans); + } + + /// + /// Finds all returns that are children of this node, and adds the appropriate spans to the spans list. + /// + private static void HighlightRelatedKeywords(SyntaxNode node, List spans) + { + switch (node) { - switch (node) - { - case ReturnStatementSyntax statement: - spans.Add(statement.ReturnKeyword.Span); - spans.Add(EmptySpan(statement.SemicolonToken.Span.End)); - break; - default: - foreach (var child in node.ChildNodesAndTokens()) - { - if (child.IsToken) - continue; + case ReturnStatementSyntax statement: + spans.Add(statement.ReturnKeyword.Span); + spans.Add(EmptySpan(statement.SemicolonToken.Span.End)); + break; + default: + foreach (var child in node.ChildNodesAndTokens()) + { + if (child.IsToken) + continue; - // Only recurse if we have anything to do - if (!child.AsNode().IsReturnableConstruct()) - HighlightRelatedKeywords(child.AsNode(), spans); - } + // Only recurse if we have anything to do + if (!child.AsNode().IsReturnableConstruct()) + HighlightRelatedKeywords(child.AsNode(), spans); + } - break; - } + break; } } } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/SwitchStatementHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/SwitchStatementHighlighter.cs index 268906ae4f13e..6df0cfd9a2a3c 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/SwitchStatementHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/SwitchStatementHighlighter.cs @@ -15,81 +15,80 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class SwitchStatementHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class SwitchStatementHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public SwitchStatementHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public SwitchStatementHighlighter() - { - } + } - protected override void AddHighlights( - SwitchStatementSyntax switchStatement, List spans, CancellationToken cancellationToken) - { - spans.Add(switchStatement.SwitchKeyword.Span); + protected override void AddHighlights( + SwitchStatementSyntax switchStatement, List spans, CancellationToken cancellationToken) + { + spans.Add(switchStatement.SwitchKeyword.Span); - foreach (var switchSection in switchStatement.Sections) + foreach (var switchSection in switchStatement.Sections) + { + foreach (var label in switchSection.Labels) { - foreach (var label in switchSection.Labels) - { - spans.Add(label.Keyword.Span); - spans.Add(EmptySpan(label.ColonToken.Span.End)); - } - - HighlightRelatedKeywords(switchSection, spans, highlightBreaks: true, highlightGotos: true); + spans.Add(label.Keyword.Span); + spans.Add(EmptySpan(label.ColonToken.Span.End)); } + + HighlightRelatedKeywords(switchSection, spans, highlightBreaks: true, highlightGotos: true); } + } - /// - /// Finds all breaks and continues that are a child of this node, and adds the appropriate spans to the spans - /// list. - /// - private static void HighlightRelatedKeywords(SyntaxNode node, List spans, - bool highlightBreaks, bool highlightGotos) - { - Debug.Assert(highlightBreaks || highlightGotos); + /// + /// Finds all breaks and continues that are a child of this node, and adds the appropriate spans to the spans + /// list. + /// + private static void HighlightRelatedKeywords(SyntaxNode node, List spans, + bool highlightBreaks, bool highlightGotos) + { + Debug.Assert(highlightBreaks || highlightGotos); - if (highlightBreaks && node is BreakStatementSyntax breakStatement) - { - spans.Add(breakStatement.BreakKeyword.Span); - spans.Add(EmptySpan(breakStatement.SemicolonToken.Span.End)); - } - else if (highlightGotos && node is GotoStatementSyntax gotoStatement) + if (highlightBreaks && node is BreakStatementSyntax breakStatement) + { + spans.Add(breakStatement.BreakKeyword.Span); + spans.Add(EmptySpan(breakStatement.SemicolonToken.Span.End)); + } + else if (highlightGotos && node is GotoStatementSyntax gotoStatement) + { + // We only want to highlight 'goto case' and 'goto default', not plain old goto statements, + // but if the label is missing, we do highlight 'goto' assuming it's more likely that + // the user is in the middle of typing 'goto case' or 'goto default'. + if (gotoStatement.Kind() is SyntaxKind.GotoCaseStatement or SyntaxKind.GotoDefaultStatement || + gotoStatement.Expression.IsMissing) { - // We only want to highlight 'goto case' and 'goto default', not plain old goto statements, - // but if the label is missing, we do highlight 'goto' assuming it's more likely that - // the user is in the middle of typing 'goto case' or 'goto default'. - if (gotoStatement.Kind() is SyntaxKind.GotoCaseStatement or SyntaxKind.GotoDefaultStatement || - gotoStatement.Expression.IsMissing) - { - var start = gotoStatement.GotoKeyword.SpanStart; - var end = !gotoStatement.CaseOrDefaultKeyword.IsKind(SyntaxKind.None) - ? gotoStatement.CaseOrDefaultKeyword.Span.End - : gotoStatement.GotoKeyword.Span.End; + var start = gotoStatement.GotoKeyword.SpanStart; + var end = !gotoStatement.CaseOrDefaultKeyword.IsKind(SyntaxKind.None) + ? gotoStatement.CaseOrDefaultKeyword.Span.End + : gotoStatement.GotoKeyword.Span.End; - spans.Add(TextSpan.FromBounds(start, end)); - spans.Add(EmptySpan(gotoStatement.SemicolonToken.Span.End)); - } + spans.Add(TextSpan.FromBounds(start, end)); + spans.Add(EmptySpan(gotoStatement.SemicolonToken.Span.End)); } - else + } + else + { + foreach (var childNodeOrToken in node.ChildNodesAndTokens()) { - foreach (var childNodeOrToken in node.ChildNodesAndTokens()) - { - if (childNodeOrToken.IsToken) - continue; + if (childNodeOrToken.IsToken) + continue; - var child = childNodeOrToken.AsNode(); - var highlightBreaksForChild = highlightBreaks && !child.IsBreakableConstruct(); - var highlightGotosForChild = highlightGotos && !child.IsKind(SyntaxKind.SwitchStatement); + var child = childNodeOrToken.AsNode(); + var highlightBreaksForChild = highlightBreaks && !child.IsBreakableConstruct(); + var highlightGotosForChild = highlightGotos && !child.IsKind(SyntaxKind.SwitchStatement); - // Only recurse if we have anything to do - if (highlightBreaksForChild || highlightGotosForChild) - { - HighlightRelatedKeywords(child, spans, highlightBreaksForChild, highlightGotosForChild); - } + // Only recurse if we have anything to do + if (highlightBreaksForChild || highlightGotosForChild) + { + HighlightRelatedKeywords(child, spans, highlightBreaksForChild, highlightGotosForChild); } } } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/TryStatementHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/TryStatementHighlighter.cs index 6ec69b0663639..ebb8b0c051a06 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/TryStatementHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/TryStatementHighlighter.cs @@ -13,36 +13,35 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class TryStatementHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class TryStatementHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public TryStatementHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public TryStatementHighlighter() - { - } + } + + protected override void AddHighlights( + TryStatementSyntax tryStatement, List highlights, CancellationToken cancellationToken) + { + highlights.Add(tryStatement.TryKeyword.Span); - protected override void AddHighlights( - TryStatementSyntax tryStatement, List highlights, CancellationToken cancellationToken) + foreach (var catchDeclaration in tryStatement.Catches) { - highlights.Add(tryStatement.TryKeyword.Span); + highlights.Add(catchDeclaration.CatchKeyword.Span); - foreach (var catchDeclaration in tryStatement.Catches) + if (catchDeclaration.Filter != null) { - highlights.Add(catchDeclaration.CatchKeyword.Span); - - if (catchDeclaration.Filter != null) - { - highlights.Add(catchDeclaration.Filter.WhenKeyword.Span); - } + highlights.Add(catchDeclaration.Filter.WhenKeyword.Span); } + } - if (tryStatement.Finally != null) - { - highlights.Add(tryStatement.Finally.FinallyKeyword.Span); - } + if (tryStatement.Finally != null) + { + highlights.Add(tryStatement.Finally.FinallyKeyword.Span); } } } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/UnsafeStatementHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/UnsafeStatementHighlighter.cs index 5efdcdb5b4075..573468c8869ad 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/UnsafeStatementHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/UnsafeStatementHighlighter.cs @@ -13,18 +13,17 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class UnsafeStatementHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class UnsafeStatementHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public UnsafeStatementHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public UnsafeStatementHighlighter() - { - } - - protected override void AddHighlights(UnsafeStatementSyntax unsafeStatement, List highlights, CancellationToken cancellationToken) - => highlights.Add(unsafeStatement.UnsafeKeyword.Span); } + + protected override void AddHighlights(UnsafeStatementSyntax unsafeStatement, List highlights, CancellationToken cancellationToken) + => highlights.Add(unsafeStatement.UnsafeKeyword.Span); } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/UsingStatementHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/UsingStatementHighlighter.cs index af584d4d2b002..970f305addd45 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/UsingStatementHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/UsingStatementHighlighter.cs @@ -13,18 +13,17 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class UsingStatementHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class UsingStatementHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public UsingStatementHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public UsingStatementHighlighter() - { - } - - protected override void AddHighlights(UsingStatementSyntax usingStatement, List highlights, CancellationToken cancellationToken) - => highlights.Add(usingStatement.UsingKeyword.Span); } + + protected override void AddHighlights(UsingStatementSyntax usingStatement, List highlights, CancellationToken cancellationToken) + => highlights.Add(usingStatement.UsingKeyword.Span); } diff --git a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/YieldStatementHighlighter.cs b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/YieldStatementHighlighter.cs index 1b44b35bd79cc..26afe3fb7819b 100644 --- a/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/YieldStatementHighlighter.cs +++ b/src/Features/CSharp/Portable/Highlighting/KeywordHighlighters/YieldStatementHighlighter.cs @@ -16,62 +16,61 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters +namespace Microsoft.CodeAnalysis.CSharp.KeywordHighlighting.KeywordHighlighters; + +[ExportHighlighter(LanguageNames.CSharp), Shared] +internal class YieldStatementHighlighter : AbstractKeywordHighlighter { - [ExportHighlighter(LanguageNames.CSharp), Shared] - internal class YieldStatementHighlighter : AbstractKeywordHighlighter + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public YieldStatementHighlighter() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public YieldStatementHighlighter() - { - } - - protected override void AddHighlights( - YieldStatementSyntax yieldStatement, List spans, CancellationToken cancellationToken) - { - var parent = yieldStatement - .GetAncestorsOrThis() - .FirstOrDefault(n => n.IsReturnableConstruct()); + } - if (parent == null) - { - return; - } + protected override void AddHighlights( + YieldStatementSyntax yieldStatement, List spans, CancellationToken cancellationToken) + { + var parent = yieldStatement + .GetAncestorsOrThis() + .FirstOrDefault(n => n.IsReturnableConstruct()); - HighlightRelatedKeywords(parent, spans); + if (parent == null) + { + return; } - /// - /// Finds all returns that are children of this node, and adds the appropriate spans to the spans list. - /// - private static void HighlightRelatedKeywords(SyntaxNode node, List spans) + HighlightRelatedKeywords(parent, spans); + } + + /// + /// Finds all returns that are children of this node, and adds the appropriate spans to the spans list. + /// + private static void HighlightRelatedKeywords(SyntaxNode node, List spans) + { + switch (node) { - switch (node) - { - case YieldStatementSyntax statement: - spans.Add( - TextSpan.FromBounds( - statement.YieldKeyword.SpanStart, - statement.ReturnOrBreakKeyword.Span.End)); + case YieldStatementSyntax statement: + spans.Add( + TextSpan.FromBounds( + statement.YieldKeyword.SpanStart, + statement.ReturnOrBreakKeyword.Span.End)); - spans.Add(EmptySpan(statement.SemicolonToken.Span.End)); - break; - default: - foreach (var child in node.ChildNodesAndTokens()) - { - if (child.IsToken) - continue; + spans.Add(EmptySpan(statement.SemicolonToken.Span.End)); + break; + default: + foreach (var child in node.ChildNodesAndTokens()) + { + if (child.IsToken) + continue; - // Only recurse if we have anything to do - if (!child.AsNode().IsReturnableConstruct()) - { - HighlightRelatedKeywords(child.AsNode(), spans); - } + // Only recurse if we have anything to do + if (!child.AsNode().IsReturnableConstruct()) + { + HighlightRelatedKeywords(child.AsNode(), spans); } + } - break; - } + break; } } } diff --git a/src/Features/CSharp/Portable/ImplementAbstractClass/CSharpImplementAbstractClassCodeFixProvider.cs b/src/Features/CSharp/Portable/ImplementAbstractClass/CSharpImplementAbstractClassCodeFixProvider.cs index 9c10e0beaf63b..89d58bf473ab2 100644 --- a/src/Features/CSharp/Portable/ImplementAbstractClass/CSharpImplementAbstractClassCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/ImplementAbstractClass/CSharpImplementAbstractClassCodeFixProvider.cs @@ -8,23 +8,22 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.ImplementAbstractClass; -namespace Microsoft.CodeAnalysis.CSharp.ImplementAbstractClass -{ - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ImplementAbstractClass), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateType)] - internal class CSharpImplementAbstractClassCodeFixProvider : - AbstractImplementAbstractClassCodeFixProvider - { - private const string CS0534 = nameof(CS0534); // 'Program' does not implement inherited abstract member 'Goo.bar()' +namespace Microsoft.CodeAnalysis.CSharp.ImplementAbstractClass; - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpImplementAbstractClassCodeFixProvider() - : base(CS0534) - { - } +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ImplementAbstractClass), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.GenerateType)] +internal class CSharpImplementAbstractClassCodeFixProvider : + AbstractImplementAbstractClassCodeFixProvider +{ + private const string CS0534 = nameof(CS0534); // 'Program' does not implement inherited abstract member 'Goo.bar()' - protected override SyntaxToken GetClassIdentifier(TypeDeclarationSyntax classNode) - => classNode.Identifier; + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpImplementAbstractClassCodeFixProvider() + : base(CS0534) + { } + + protected override SyntaxToken GetClassIdentifier(TypeDeclarationSyntax classNode) + => classNode.Identifier; } diff --git a/src/Features/CSharp/Portable/ImplementInterface/AbstractChangeImplementationCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ImplementInterface/AbstractChangeImplementationCodeRefactoringProvider.cs index 61bc005dec09c..8b2804599a52b 100644 --- a/src/Features/CSharp/Portable/ImplementInterface/AbstractChangeImplementationCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ImplementInterface/AbstractChangeImplementationCodeRefactoringProvider.cs @@ -18,248 +18,247 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface -{ - // A mapping from a concrete member to the interface members it implements. We need this to be - // Ordered so that we process members in a consistent order (esp. necessary during tests). For - // example, if an implicit method implements multiple interface methods, we want to walk those - // in the same order every time when converting the implicit method to multiple explicit - // methods. - using MemberImplementationMap = OrderedMultiDictionary; - - internal abstract class AbstractChangeImplementationCodeRefactoringProvider : CodeRefactoringProvider - { - private static readonly SymbolDisplayFormat NameAndTypeParametersFormat = - new SymbolDisplayFormat( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - protected abstract string Implement_0 { get; } - protected abstract string Implement_all_interfaces { get; } - protected abstract string Implement { get; } - - protected abstract bool CheckExplicitNameAllowsConversion(ExplicitInterfaceSpecifierSyntax? explicitName); - protected abstract bool CheckMemberCanBeConverted(ISymbol member); - protected abstract SyntaxNode ChangeImplementation(SyntaxGenerator generator, SyntaxNode currentDecl, ISymbol implMember, ISymbol interfaceMember); - protected abstract Task UpdateReferencesAsync(Project project, SolutionEditor solutionEditor, ISymbol implMember, INamedTypeSymbol containingType, CancellationToken cancellationToken); - - public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (container, explicitName, name) = await GetContainerAsync(context).ConfigureAwait(false); - if (container == null) - return; +namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface; - if (!CheckExplicitNameAllowsConversion(explicitName)) - return; +// A mapping from a concrete member to the interface members it implements. We need this to be +// Ordered so that we process members in a consistent order (esp. necessary during tests). For +// example, if an implicit method implements multiple interface methods, we want to walk those +// in the same order every time when converting the implicit method to multiple explicit +// methods. +using MemberImplementationMap = OrderedMultiDictionary; - var (document, _, cancellationToken) = context; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var member = semanticModel.GetDeclaredSymbol(container, cancellationToken); - Contract.ThrowIfNull(member); +internal abstract class AbstractChangeImplementationCodeRefactoringProvider : CodeRefactoringProvider +{ + private static readonly SymbolDisplayFormat NameAndTypeParametersFormat = + new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + protected abstract string Implement_0 { get; } + protected abstract string Implement_all_interfaces { get; } + protected abstract string Implement { get; } + + protected abstract bool CheckExplicitNameAllowsConversion(ExplicitInterfaceSpecifierSyntax? explicitName); + protected abstract bool CheckMemberCanBeConverted(ISymbol member); + protected abstract SyntaxNode ChangeImplementation(SyntaxGenerator generator, SyntaxNode currentDecl, ISymbol implMember, ISymbol interfaceMember); + protected abstract Task UpdateReferencesAsync(Project project, SolutionEditor solutionEditor, ISymbol implMember, INamedTypeSymbol containingType, CancellationToken cancellationToken); + + public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (container, explicitName, name) = await GetContainerAsync(context).ConfigureAwait(false); + if (container == null) + return; - if (!CheckMemberCanBeConverted(member)) - return; + if (!CheckExplicitNameAllowsConversion(explicitName)) + return; - var project = document.Project; + var (document, _, cancellationToken) = context; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var member = semanticModel.GetDeclaredSymbol(container, cancellationToken); + Contract.ThrowIfNull(member); - var directlyImplementedMembers = new MemberImplementationMap(); + if (!CheckMemberCanBeConverted(member)) + return; - // Grab the name off of the *interface* member being implemented, not the implementation - // member. Interface member names are the expected names that people expect to see - // (like "GetEnumerator"), instead of the auto-generated names that the compiler makes - // like: "System.IEnumerable.GetEnumerator" - var implementations = member.ExplicitOrImplicitInterfaceImplementations(); - if (implementations.IsEmpty) - return; + var project = document.Project; - directlyImplementedMembers.AddRange(member, implementations); - var firstImplName = implementations.First().Name; - var codeAction = CodeAction.Create( - string.Format(Implement_0, firstImplName), - cancellationToken => ChangeImplementationAsync(project, directlyImplementedMembers, cancellationToken), - nameof(Implement_0) + firstImplName); + var directlyImplementedMembers = new MemberImplementationMap(); - var containingType = member.ContainingType; - var interfaceTypes = directlyImplementedMembers.SelectMany(kvp => kvp.Value).Select( - s => s.ContainingType).Distinct().ToImmutableArray(); + // Grab the name off of the *interface* member being implemented, not the implementation + // member. Interface member names are the expected names that people expect to see + // (like "GetEnumerator"), instead of the auto-generated names that the compiler makes + // like: "System.IEnumerable.GetEnumerator" + var implementations = member.ExplicitOrImplicitInterfaceImplementations(); + if (implementations.IsEmpty) + return; - var implementedMembersFromSameInterfaces = GetImplementedMembers(containingType, interfaceTypes); - var implementedMembersFromAllInterfaces = GetImplementedMembers(containingType, containingType.AllInterfaces); + directlyImplementedMembers.AddRange(member, implementations); + var firstImplName = implementations.First().Name; + var codeAction = CodeAction.Create( + string.Format(Implement_0, firstImplName), + cancellationToken => ChangeImplementationAsync(project, directlyImplementedMembers, cancellationToken), + nameof(Implement_0) + firstImplName); - var offerForSameInterface = TotalCount(implementedMembersFromSameInterfaces) > TotalCount(directlyImplementedMembers); - var offerForAllInterfaces = TotalCount(implementedMembersFromAllInterfaces) > TotalCount(implementedMembersFromSameInterfaces); + var containingType = member.ContainingType; + var interfaceTypes = directlyImplementedMembers.SelectMany(kvp => kvp.Value).Select( + s => s.ContainingType).Distinct().ToImmutableArray(); - // If there's only one member in the interface we implement, and there are no other - // interfaces, then just offer to switch the implementation for this single member - if (!offerForSameInterface && !offerForAllInterfaces) - { - context.RegisterRefactoring(codeAction); - return; - } + var implementedMembersFromSameInterfaces = GetImplementedMembers(containingType, interfaceTypes); + var implementedMembersFromAllInterfaces = GetImplementedMembers(containingType, containingType.AllInterfaces); - // Otherwise, create a top level action to change the implementation, and offer this - // action, along with either/both of the other two. + var offerForSameInterface = TotalCount(implementedMembersFromSameInterfaces) > TotalCount(directlyImplementedMembers); + var offerForAllInterfaces = TotalCount(implementedMembersFromAllInterfaces) > TotalCount(implementedMembersFromSameInterfaces); - using var nestedActions = TemporaryArray.Empty; - nestedActions.Add(codeAction); + // If there's only one member in the interface we implement, and there are no other + // interfaces, then just offer to switch the implementation for this single member + if (!offerForSameInterface && !offerForAllInterfaces) + { + context.RegisterRefactoring(codeAction); + return; + } - if (offerForSameInterface) - { - var interfaceNames = interfaceTypes.Select(i => i.ToDisplayString(NameAndTypeParametersFormat)); - nestedActions.Add(CodeAction.Create( - string.Format(Implement_0, string.Join(", ", interfaceNames)), - cancellationToken => ChangeImplementationAsync(project, implementedMembersFromSameInterfaces, cancellationToken), - nameof(Implement_0) + string.Join(", ", interfaceNames))); - } + // Otherwise, create a top level action to change the implementation, and offer this + // action, along with either/both of the other two. - if (offerForAllInterfaces) - { - nestedActions.Add(CodeAction.Create( - Implement_all_interfaces, - cancellationToken => ChangeImplementationAsync(project, implementedMembersFromAllInterfaces, cancellationToken), - nameof(Implement_all_interfaces))); - } + using var nestedActions = TemporaryArray.Empty; + nestedActions.Add(codeAction); - context.RegisterRefactoring(CodeAction.Create( - Implement, nestedActions.ToImmutableAndClear(), isInlinable: true)); + if (offerForSameInterface) + { + var interfaceNames = interfaceTypes.Select(i => i.ToDisplayString(NameAndTypeParametersFormat)); + nestedActions.Add(CodeAction.Create( + string.Format(Implement_0, string.Join(", ", interfaceNames)), + cancellationToken => ChangeImplementationAsync(project, implementedMembersFromSameInterfaces, cancellationToken), + nameof(Implement_0) + string.Join(", ", interfaceNames))); } - private static async Task<(SyntaxNode?, ExplicitInterfaceSpecifierSyntax?, SyntaxToken)> GetContainerAsync(CodeRefactoringContext context) + if (offerForAllInterfaces) { - var (document, span, cancellationToken) = context; - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(span.Start); + nestedActions.Add(CodeAction.Create( + Implement_all_interfaces, + cancellationToken => ChangeImplementationAsync(project, implementedMembersFromAllInterfaces, cancellationToken), + nameof(Implement_all_interfaces))); + } - // Move back if the user is at: X.Goo$$( - if (span.IsEmpty && token.Kind() == SyntaxKind.OpenParenToken) - token = token.GetPreviousToken(); + context.RegisterRefactoring(CodeAction.Create( + Implement, nestedActions.ToImmutableAndClear(), isInlinable: true)); + } - // Offer the feature if the user is anywhere between the start of the explicit-impl of - // the member (if we have one) and the end if the identifier of the member. - var (container, explicitName, identifier) = GetContainer(token); - if (container == null) - return default; + private static async Task<(SyntaxNode?, ExplicitInterfaceSpecifierSyntax?, SyntaxToken)> GetContainerAsync(CodeRefactoringContext context) + { + var (document, span, cancellationToken) = context; - var applicableSpan = explicitName == null - ? identifier.FullSpan - : TextSpan.FromBounds(explicitName.FullSpan.Start, identifier.FullSpan.End); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(span.Start); - if (!applicableSpan.Contains(span)) - return default; + // Move back if the user is at: X.Goo$$( + if (span.IsEmpty && token.Kind() == SyntaxKind.OpenParenToken) + token = token.GetPreviousToken(); - return (container, explicitName, identifier); - } + // Offer the feature if the user is anywhere between the start of the explicit-impl of + // the member (if we have one) and the end if the identifier of the member. + var (container, explicitName, identifier) = GetContainer(token); + if (container == null) + return default; - private static (SyntaxNode? declaration, ExplicitInterfaceSpecifierSyntax?, SyntaxToken) GetContainer(SyntaxToken token) - { - for (var node = token.Parent; node != null; node = node.Parent) - { - var result = node switch - { - MethodDeclarationSyntax member => (member, member.ExplicitInterfaceSpecifier, member.Identifier), - PropertyDeclarationSyntax member => (member, member.ExplicitInterfaceSpecifier, member.Identifier), - EventDeclarationSyntax member => (member, member.ExplicitInterfaceSpecifier, member.Identifier), - _ => default((SyntaxNode member, ExplicitInterfaceSpecifierSyntax?, SyntaxToken)), - }; - - if (result.member != null) - return result; - } + var applicableSpan = explicitName == null + ? identifier.FullSpan + : TextSpan.FromBounds(explicitName.FullSpan.Start, identifier.FullSpan.End); + if (!applicableSpan.Contains(span)) return default; - } - private static int TotalCount(MemberImplementationMap dictionary) + return (container, explicitName, identifier); + } + + private static (SyntaxNode? declaration, ExplicitInterfaceSpecifierSyntax?, SyntaxToken) GetContainer(SyntaxToken token) + { + for (var node = token.Parent; node != null; node = node.Parent) { - var result = 0; - foreach (var (key, values) in dictionary) + var result = node switch { - result += values.Count; - } + MethodDeclarationSyntax member => (member, member.ExplicitInterfaceSpecifier, member.Identifier), + PropertyDeclarationSyntax member => (member, member.ExplicitInterfaceSpecifier, member.Identifier), + EventDeclarationSyntax member => (member, member.ExplicitInterfaceSpecifier, member.Identifier), + _ => default((SyntaxNode member, ExplicitInterfaceSpecifierSyntax?, SyntaxToken)), + }; + + if (result.member != null) + return result; + } + + return default; + } - return result; + private static int TotalCount(MemberImplementationMap dictionary) + { + var result = 0; + foreach (var (key, values) in dictionary) + { + result += values.Count; } - /// - /// Returns a mapping from members in our containing types to all the interface members (of - /// the sort we care about) that it implements. - /// - private MemberImplementationMap GetImplementedMembers(INamedTypeSymbol containingType, ImmutableArray interfaceTypes) + return result; + } + + /// + /// Returns a mapping from members in our containing types to all the interface members (of + /// the sort we care about) that it implements. + /// + private MemberImplementationMap GetImplementedMembers(INamedTypeSymbol containingType, ImmutableArray interfaceTypes) + { + var result = new MemberImplementationMap(); + foreach (var interfaceType in interfaceTypes) { - var result = new MemberImplementationMap(); - foreach (var interfaceType in interfaceTypes) + foreach (var interfaceMember in interfaceType.GetMembers()) { - foreach (var interfaceMember in interfaceType.GetMembers()) + var impl = containingType.FindImplementationForInterfaceMember(interfaceMember); + if (impl != null && + containingType.Equals(impl.ContainingType) && + CheckMemberCanBeConverted(impl) && + !impl.IsAccessor()) { - var impl = containingType.FindImplementationForInterfaceMember(interfaceMember); - if (impl != null && - containingType.Equals(impl.ContainingType) && - CheckMemberCanBeConverted(impl) && - !impl.IsAccessor()) - { - result.Add(impl, interfaceMember); - } + result.Add(impl, interfaceMember); } } - - return result; } - private async Task ChangeImplementationAsync( - Project project, MemberImplementationMap implMemberToInterfaceMembers, CancellationToken cancellationToken) + return result; + } + + private async Task ChangeImplementationAsync( + Project project, MemberImplementationMap implMemberToInterfaceMembers, CancellationToken cancellationToken) + { + var solution = project.Solution; + var solutionEditor = new SolutionEditor(solution); + + // First, we have to go through and find all the references to these interface + // implementation members. We may have to update them to preserve semantics. i.e. a + // call to goo.Bar() will be updated to `((IGoo)goo).Bar()` if we're switching to + // explicit implementation. + foreach (var (implMember, interfaceMembers) in implMemberToInterfaceMembers) { - var solution = project.Solution; - var solutionEditor = new SolutionEditor(solution); - - // First, we have to go through and find all the references to these interface - // implementation members. We may have to update them to preserve semantics. i.e. a - // call to goo.Bar() will be updated to `((IGoo)goo).Bar()` if we're switching to - // explicit implementation. - foreach (var (implMember, interfaceMembers) in implMemberToInterfaceMembers) - { - await UpdateReferencesAsync( - project, solutionEditor, implMember, - interfaceMembers.First().ContainingType, - cancellationToken).ConfigureAwait(false); - } + await UpdateReferencesAsync( + project, solutionEditor, implMember, + interfaceMembers.First().ContainingType, + cancellationToken).ConfigureAwait(false); + } - // Now, bucket all the implemented members by which document they appear in. - // That way, we can update all the members in a specific document in bulk. - var documentToImplDeclarations = new OrderedMultiDictionary interfaceMembers)>(); - foreach (var (implMember, interfaceMembers) in implMemberToInterfaceMembers) + // Now, bucket all the implemented members by which document they appear in. + // That way, we can update all the members in a specific document in bulk. + var documentToImplDeclarations = new OrderedMultiDictionary interfaceMembers)>(); + foreach (var (implMember, interfaceMembers) in implMemberToInterfaceMembers) + { + foreach (var location in implMember.Locations) { - foreach (var location in implMember.Locations) - { - if (location.SourceTree is null) - continue; + if (location.SourceTree is null) + continue; - var doc = solution.GetRequiredDocument(location.SourceTree); - var declToken = location.FindToken(cancellationToken); - var decl = GetContainer(declToken).declaration; - if (decl is null) - continue; + var doc = solution.GetRequiredDocument(location.SourceTree); + var declToken = location.FindToken(cancellationToken); + var decl = GetContainer(declToken).declaration; + if (decl is null) + continue; - documentToImplDeclarations.Add(doc, (decl, implMember, interfaceMembers)); - } + documentToImplDeclarations.Add(doc, (decl, implMember, interfaceMembers)); } + } - foreach (var (document, declsAndSymbol) in documentToImplDeclarations) - { - var editor = await solutionEditor.GetDocumentEditorAsync( - document.Id, cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + foreach (var (document, declsAndSymbol) in documentToImplDeclarations) + { + var editor = await solutionEditor.GetDocumentEditorAsync( + document.Id, cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - foreach (var (decl, implMember, interfaceMembers) in declsAndSymbol) - { - editor.ReplaceNode(decl, (currentDecl, g) => - interfaceMembers.Select(s => ChangeImplementation(g, currentDecl, implMember, s))); - } + foreach (var (decl, implMember, interfaceMembers) in declsAndSymbol) + { + editor.ReplaceNode(decl, (currentDecl, g) => + interfaceMembers.Select(s => ChangeImplementation(g, currentDecl, implMember, s))); } - - return solutionEditor.GetChangedSolution(); } + + return solutionEditor.GetChangedSolution(); } } diff --git a/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementExplicitlyCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementExplicitlyCodeRefactoringProvider.cs index f15b6c8fe62cd..6c9449741405d 100644 --- a/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementExplicitlyCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementExplicitlyCodeRefactoringProvider.cs @@ -18,166 +18,161 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface +namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ImplementInterfaceExplicitly), Shared] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal sealed class CSharpImplementExplicitlyCodeRefactoringProvider() : AbstractChangeImplementationCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ImplementInterfaceExplicitly), Shared] - internal class CSharpImplementExplicitlyCodeRefactoringProvider : - AbstractChangeImplementationCodeRefactoringProvider + protected override string Implement_0 => FeaturesResources.Implement_0_explicitly; + protected override string Implement_all_interfaces => FeaturesResources.Implement_all_interfaces_explicitly; + protected override string Implement => FeaturesResources.Implement_explicitly; + + // If we already have an explicit name, we can't change this to be explicit. + protected override bool CheckExplicitNameAllowsConversion(ExplicitInterfaceSpecifierSyntax? explicitName) + => explicitName == null; + + // If we don't implement any interface members we can't convert this to be explicit. + protected override bool CheckMemberCanBeConverted(ISymbol member) + => member.ImplicitInterfaceImplementations().Length > 0; + + protected override async Task UpdateReferencesAsync( + Project project, SolutionEditor solutionEditor, + ISymbol implMember, INamedTypeSymbol interfaceType, CancellationToken cancellationToken) { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpImplementExplicitlyCodeRefactoringProvider() - { - } + var solution = project.Solution; - protected override string Implement_0 => FeaturesResources.Implement_0_explicitly; - protected override string Implement_all_interfaces => FeaturesResources.Implement_all_interfaces_explicitly; - protected override string Implement => FeaturesResources.Implement_explicitly; + // We don't need to cascade in this search, we're only explicitly looking for direct + // calls to our instance member (and not anyone else already calling through the + // interface already). + // + // This can save a lot of extra time spent finding callers, especially for methods with + // high fan-out (like IDisposable.Dispose()). + var findRefsOptions = FindReferencesSearchOptions.Default with { Cascade = false }; + var references = await SymbolFinder.FindReferencesAsync( + implMember, solution, findRefsOptions, cancellationToken).ConfigureAwait(false); - // If we already have an explicit name, we can't change this to be explicit. - protected override bool CheckExplicitNameAllowsConversion(ExplicitInterfaceSpecifierSyntax? explicitName) - => explicitName == null; + var implReferences = references.FirstOrDefault(); + if (implReferences == null) + return; - // If we don't implement any interface members we can't convert this to be explicit. - protected override bool CheckMemberCanBeConverted(ISymbol member) - => member.ImplicitInterfaceImplementations().Length > 0; + var referenceByDocument = implReferences.Locations.GroupBy(loc => loc.Document); - protected override async Task UpdateReferencesAsync( - Project project, SolutionEditor solutionEditor, - ISymbol implMember, INamedTypeSymbol interfaceType, CancellationToken cancellationToken) + foreach (var group in referenceByDocument) { - var solution = project.Solution; - - // We don't need to cascade in this search, we're only explicitly looking for direct - // calls to our instance member (and not anyone else already calling through the - // interface already). - // - // This can save a lot of extra time spent finding callers, especially for methods with - // high fan-out (like IDisposable.Dispose()). - var findRefsOptions = FindReferencesSearchOptions.Default with { Cascade = false }; - var references = await SymbolFinder.FindReferencesAsync( - implMember, solution, findRefsOptions, cancellationToken).ConfigureAwait(false); - - var implReferences = references.FirstOrDefault(); - if (implReferences == null) - return; + var document = group.Key; + var syntaxFacts = document.GetRequiredLanguageService(); - var referenceByDocument = implReferences.Locations.GroupBy(loc => loc.Document); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var editor = await solutionEditor.GetDocumentEditorAsync( + document.Id, cancellationToken).ConfigureAwait(false); - foreach (var group in referenceByDocument) + foreach (var refLocation in group) { - var document = group.Key; - var syntaxFacts = document.GetRequiredLanguageService(); - - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var editor = await solutionEditor.GetDocumentEditorAsync( - document.Id, cancellationToken).ConfigureAwait(false); - - foreach (var refLocation in group) - { - if (refLocation.IsImplicit) - continue; - - var location = refLocation.Location; - if (!location.IsInSource) - continue; - - UpdateLocation( - semanticModel, interfaceType, editor, - syntaxFacts, location, cancellationToken); - } + if (refLocation.IsImplicit) + continue; + + var location = refLocation.Location; + if (!location.IsInSource) + continue; + + UpdateLocation( + semanticModel, interfaceType, editor, + syntaxFacts, location, cancellationToken); } } + } - private static void UpdateLocation( - SemanticModel semanticModel, INamedTypeSymbol interfaceType, - SyntaxEditor editor, ISyntaxFactsService syntaxFacts, - Location location, CancellationToken cancellationToken) - { - var identifierName = location.FindNode(getInnermostNodeForTie: true, cancellationToken); - if (identifierName == null || !syntaxFacts.IsIdentifierName(identifierName)) - return; + private static void UpdateLocation( + SemanticModel semanticModel, INamedTypeSymbol interfaceType, + SyntaxEditor editor, ISyntaxFactsService syntaxFacts, + Location location, CancellationToken cancellationToken) + { + var identifierName = location.FindNode(getInnermostNodeForTie: true, cancellationToken); + if (identifierName == null || !syntaxFacts.IsIdentifierName(identifierName)) + return; - var node = syntaxFacts.IsNameOfSimpleMemberAccessExpression(identifierName) || syntaxFacts.IsNameOfMemberBindingExpression(identifierName) - ? identifierName.Parent - : identifierName; + var node = syntaxFacts.IsNameOfSimpleMemberAccessExpression(identifierName) || syntaxFacts.IsNameOfMemberBindingExpression(identifierName) + ? identifierName.Parent + : identifierName; - RoslynDebug.Assert(node is object); - if (syntaxFacts.IsInvocationExpression(node.Parent)) - node = node.Parent; + RoslynDebug.Assert(node is object); + if (syntaxFacts.IsInvocationExpression(node.Parent)) + node = node.Parent; - var operation = semanticModel.GetOperation(node, cancellationToken); - var instance = operation switch - { - IMemberReferenceOperation memberReference => memberReference.Instance, - IInvocationOperation invocation => invocation.Instance, - _ => null, - }; + var operation = semanticModel.GetOperation(node, cancellationToken); + var instance = operation switch + { + IMemberReferenceOperation memberReference => memberReference.Instance, + IInvocationOperation invocation => invocation.Instance, + _ => null, + }; - if (instance == null) - return; + if (instance == null) + return; - if (instance.IsImplicit) - { - if (instance is IInstanceReferenceOperation instanceReference && - instanceReference.ReferenceKind != InstanceReferenceKind.ContainingTypeInstance) - { - return; - } - - // Accessing the member not off of . i.e just plain `Goo()`. Replace with - // ((IGoo)this).Goo(); - var generator = editor.Generator; - editor.ReplaceNode( - identifierName, - generator.MemberAccessExpression( - generator.AddParentheses(generator.CastExpression(interfaceType, generator.ThisExpression())), - identifierName.WithoutTrivia()).WithTriviaFrom(identifierName)); - } - else + // Have to make sure we've got a simple name for the rewrite below to be legal. + if (instance.IsImplicit && syntaxFacts.IsSimpleName(identifierName)) + { + if (instance is IInstanceReferenceOperation instanceReference && + instanceReference.ReferenceKind != InstanceReferenceKind.ContainingTypeInstance) { - // Accessing the member like `x.Goo()`. Replace with `((IGoo)x).Goo()` - editor.ReplaceNode( - instance.Syntax, (current, g) => - g.AddParentheses( - g.CastExpression(interfaceType, current.WithoutTrivia())).WithTriviaFrom(current)); + return; } - } - protected override SyntaxNode ChangeImplementation(SyntaxGenerator generator, SyntaxNode decl, ISymbol implMember, ISymbol interfaceMember) + // Accessing the member not off of . i.e just plain `Goo()`. Replace with + // ((IGoo)this).Goo(); + var generator = editor.Generator; + editor.ReplaceNode( + identifierName, + generator.MemberAccessExpression( + generator.AddParentheses(generator.CastExpression(interfaceType, generator.ThisExpression())), + identifierName.WithoutTrivia()).WithTriviaFrom(identifierName)); + } + else { - // If these signatures match on default values, then remove the defaults when converting to explicit - // (they're not legal in C#). If they don't match on defaults, then keep them in so that the user gets a - // warning (from us and the compiler) and considers what to do about this. - var removeDefaults = AllDefaultValuesMatch(implMember, interfaceMember); - return generator.WithExplicitInterfaceImplementations(decl, [interfaceMember], removeDefaults); + // Accessing the member like `x.Goo()`. Replace with `((IGoo)x).Goo()` + editor.ReplaceNode( + instance.Syntax, (current, g) => + g.AddParentheses( + g.CastExpression(interfaceType, current.WithoutTrivia())).WithTriviaFrom(current)); } + } + + protected override SyntaxNode ChangeImplementation(SyntaxGenerator generator, SyntaxNode decl, ISymbol implMember, ISymbol interfaceMember) + { + // If these signatures match on default values, then remove the defaults when converting to explicit + // (they're not legal in C#). If they don't match on defaults, then keep them in so that the user gets a + // warning (from us and the compiler) and considers what to do about this. + var removeDefaults = AllDefaultValuesMatch(implMember, interfaceMember); + return generator.WithExplicitInterfaceImplementations(decl, [interfaceMember], removeDefaults); + } - private static bool AllDefaultValuesMatch(ISymbol implMember, ISymbol interfaceMember) + private static bool AllDefaultValuesMatch(ISymbol implMember, ISymbol interfaceMember) + { + if (implMember is IMethodSymbol { Parameters: var implParameters } && + interfaceMember is IMethodSymbol { Parameters: var interfaceParameters }) { - if (implMember is IMethodSymbol { Parameters: var implParameters } && - interfaceMember is IMethodSymbol { Parameters: var interfaceParameters }) + for (int i = 0, n = Math.Max(implParameters.Length, interfaceParameters.Length); i < n; i++) { - for (int i = 0, n = Math.Max(implParameters.Length, interfaceParameters.Length); i < n; i++) - { - if (!DefaultValueMatches(implParameters[i], interfaceParameters[i])) - return false; - } + if (!DefaultValueMatches(implParameters[i], interfaceParameters[i])) + return false; } - - return true; } - private static bool DefaultValueMatches(IParameterSymbol parameterSymbol1, IParameterSymbol parameterSymbol2) - { - if (parameterSymbol1.HasExplicitDefaultValue != parameterSymbol2.HasExplicitDefaultValue) - return false; + return true; + } + + private static bool DefaultValueMatches(IParameterSymbol parameterSymbol1, IParameterSymbol parameterSymbol2) + { + if (parameterSymbol1.HasExplicitDefaultValue != parameterSymbol2.HasExplicitDefaultValue) + return false; - if (parameterSymbol1.HasExplicitDefaultValue) - return Equals(parameterSymbol1.ExplicitDefaultValue, parameterSymbol2.ExplicitDefaultValue); + if (parameterSymbol1.HasExplicitDefaultValue) + return Equals(parameterSymbol1.ExplicitDefaultValue, parameterSymbol2.ExplicitDefaultValue); - return true; - } + return true; } } diff --git a/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementImplicitlyCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementImplicitlyCodeRefactoringProvider.cs index 87a2963ec5ec0..7ac6e1f8d6849 100644 --- a/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementImplicitlyCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementImplicitlyCodeRefactoringProvider.cs @@ -12,53 +12,52 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface +namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ImplementInterfaceImplicitly), Shared] +internal class CSharpImplementImplicitlyCodeRefactoringProvider : + AbstractChangeImplementationCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ImplementInterfaceImplicitly), Shared] - internal class CSharpImplementImplicitlyCodeRefactoringProvider : - AbstractChangeImplementationCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpImplementImplicitlyCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpImplementImplicitlyCodeRefactoringProvider() - { - } + } - protected override string Implement_0 => FeaturesResources.Implement_0_implicitly; - protected override string Implement_all_interfaces => FeaturesResources.Implement_all_interfaces_implicitly; - protected override string Implement => FeaturesResources.Implement_implicitly; + protected override string Implement_0 => FeaturesResources.Implement_0_implicitly; + protected override string Implement_all_interfaces => FeaturesResources.Implement_all_interfaces_implicitly; + protected override string Implement => FeaturesResources.Implement_implicitly; - // We need to be an explicit impl in order to convert to implicit. - protected override bool CheckExplicitNameAllowsConversion(ExplicitInterfaceSpecifierSyntax? explicitName) - => explicitName != null; + // We need to be an explicit impl in order to convert to implicit. + protected override bool CheckExplicitNameAllowsConversion(ExplicitInterfaceSpecifierSyntax? explicitName) + => explicitName != null; - // If we don't implement any interface members explicitly we can't convert this to be - // implicit. - protected override bool CheckMemberCanBeConverted(ISymbol member) - { - var memberInterfaceImplementations = member.ExplicitInterfaceImplementations(); - if (memberInterfaceImplementations.Length == 0) - return false; - var containingTypeInterfaces = member.ContainingType.AllInterfaces; - if (containingTypeInterfaces.Length == 0) - return false; - return memberInterfaceImplementations.Any(static (impl, containingTypeInterfaces) => containingTypeInterfaces.Contains(impl.ContainingType), containingTypeInterfaces); - } + // If we don't implement any interface members explicitly we can't convert this to be + // implicit. + protected override bool CheckMemberCanBeConverted(ISymbol member) + { + var memberInterfaceImplementations = member.ExplicitInterfaceImplementations(); + if (memberInterfaceImplementations.Length == 0) + return false; + var containingTypeInterfaces = member.ContainingType.AllInterfaces; + if (containingTypeInterfaces.Length == 0) + return false; + return memberInterfaceImplementations.Any(static (impl, containingTypeInterfaces) => containingTypeInterfaces.Contains(impl.ContainingType), containingTypeInterfaces); + } - // When converting to implicit, we don't need to update any references. - protected override Task UpdateReferencesAsync(Project project, SolutionEditor solutionEditor, ISymbol implMember, INamedTypeSymbol containingType, CancellationToken cancellationToken) - => Task.CompletedTask; + // When converting to implicit, we don't need to update any references. + protected override Task UpdateReferencesAsync(Project project, SolutionEditor solutionEditor, ISymbol implMember, INamedTypeSymbol containingType, CancellationToken cancellationToken) + => Task.CompletedTask; - protected override SyntaxNode ChangeImplementation(SyntaxGenerator generator, SyntaxNode decl, ISymbol _1, ISymbol _2) - => generator.WithAccessibility(WithoutExplicitImpl(decl), Accessibility.Public); + protected override SyntaxNode ChangeImplementation(SyntaxGenerator generator, SyntaxNode decl, ISymbol _1, ISymbol _2) + => generator.WithAccessibility(WithoutExplicitImpl(decl), Accessibility.Public); - private static SyntaxNode WithoutExplicitImpl(SyntaxNode decl) - => decl switch - { - MethodDeclarationSyntax member => member.WithExplicitInterfaceSpecifier(null), - PropertyDeclarationSyntax member => member.WithExplicitInterfaceSpecifier(null), - EventDeclarationSyntax member => member.WithExplicitInterfaceSpecifier(null), - _ => throw ExceptionUtilities.UnexpectedValue(decl), - }; - } + private static SyntaxNode WithoutExplicitImpl(SyntaxNode decl) + => decl switch + { + MethodDeclarationSyntax member => member.WithExplicitInterfaceSpecifier(null), + PropertyDeclarationSyntax member => member.WithExplicitInterfaceSpecifier(null), + EventDeclarationSyntax member => member.WithExplicitInterfaceSpecifier(null), + _ => throw ExceptionUtilities.UnexpectedValue(decl), + }; } diff --git a/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementInterfaceCodeFixProvider.cs b/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementInterfaceCodeFixProvider.cs index cc30d3c07d31a..a77a53fd78a76 100644 --- a/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementInterfaceCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementInterfaceCodeFixProvider.cs @@ -15,56 +15,55 @@ using Microsoft.CodeAnalysis.ImplementType; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface +namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ImplementInterface), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.ImplementAbstractClass)] +internal class CSharpImplementInterfaceCodeFixProvider : CodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.ImplementInterface), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.ImplementAbstractClass)] - internal class CSharpImplementInterfaceCodeFixProvider : CodeFixProvider - { - private readonly Func _interfaceName = n => n.Parent is BaseTypeSyntax && n.Parent.Parent is BaseListSyntax && ((BaseTypeSyntax)n.Parent).Type == n; + private readonly Func _interfaceName = n => n.Parent is BaseTypeSyntax && n.Parent.Parent is BaseListSyntax && ((BaseTypeSyntax)n.Parent).Type == n; - private const string CS0535 = nameof(CS0535); // 'Program' does not implement interface member 'System.Collections.IEnumerable.GetEnumerator()' - private const string CS0737 = nameof(CS0737); // 'Class' does not implement interface member 'IInterface.M()'. 'Class.M()' cannot implement an interface member because it is not public. - private const string CS0738 = nameof(CS0738); // 'C' does not implement interface member 'I.Method1()'. 'B.Method1()' cannot implement 'I.Method1()' because it does not have the matching return type of 'void'. + private const string CS0535 = nameof(CS0535); // 'Program' does not implement interface member 'System.Collections.IEnumerable.GetEnumerator()' + private const string CS0737 = nameof(CS0737); // 'Class' does not implement interface member 'IInterface.M()'. 'Class.M()' cannot implement an interface member because it is not public. + private const string CS0738 = nameof(CS0738); // 'C' does not implement interface member 'I.Method1()'. 'B.Method1()' cannot implement 'I.Method1()' because it does not have the matching return type of 'void'. - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpImplementInterfaceCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpImplementInterfaceCodeFixProvider() + { + } - public sealed override ImmutableArray FixableDiagnosticIds { get; } - = [CS0535, CS0737, CS0738]; + public sealed override ImmutableArray FixableDiagnosticIds { get; } + = [CS0535, CS0737, CS0738]; - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var span = context.Span; - var cancellationToken = context.CancellationToken; - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var span = context.Span; + var cancellationToken = context.CancellationToken; - var token = root.FindToken(span.Start); - if (!token.Span.IntersectsWith(span)) - { - return; - } + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var service = document.GetRequiredLanguageService(); - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(span.Start); + if (!token.Span.IntersectsWith(span)) + { + return; + } - var actions = token.Parent.GetAncestorsOrThis() - .Where(_interfaceName) - .Select(n => service.GetCodeActions(document, context.Options.GetImplementTypeGenerationOptions(document.Project.Services), model, n, cancellationToken)) - .FirstOrDefault(a => !a.IsEmpty); + var service = document.GetRequiredLanguageService(); + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (actions.IsDefaultOrEmpty) - return; + var actions = token.Parent.GetAncestorsOrThis() + .Where(_interfaceName) + .Select(n => service.GetCodeActions(document, context.Options.GetImplementTypeGenerationOptions(document.Project.Services), model, n, cancellationToken)) + .FirstOrDefault(a => !a.IsEmpty); - context.RegisterFixes(actions, context.Diagnostics); - } + if (actions.IsDefaultOrEmpty) + return; - public sealed override FixAllProvider GetFixAllProvider() - => WellKnownFixAllProviders.BatchFixer; + context.RegisterFixes(actions, context.Diagnostics); } + + public sealed override FixAllProvider GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; } diff --git a/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementInterfaceService.cs b/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementInterfaceService.cs index 7dc7bd9162482..2bb6df09a4620 100644 --- a/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementInterfaceService.cs +++ b/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementInterfaceService.cs @@ -17,90 +17,89 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface +namespace Microsoft.CodeAnalysis.CSharp.ImplementInterface; + +[ExportLanguageService(typeof(IImplementInterfaceService), LanguageNames.CSharp), Shared] +internal class CSharpImplementInterfaceService : AbstractImplementInterfaceService { - [ExportLanguageService(typeof(IImplementInterfaceService), LanguageNames.CSharp), Shared] - internal class CSharpImplementInterfaceService : AbstractImplementInterfaceService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpImplementInterfaceService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpImplementInterfaceService() - { - } + } - protected override string ToDisplayString(IMethodSymbol disposeImplMethod, SymbolDisplayFormat format) - => SymbolDisplay.ToDisplayString(disposeImplMethod, format); + protected override string ToDisplayString(IMethodSymbol disposeImplMethod, SymbolDisplayFormat format) + => SymbolDisplay.ToDisplayString(disposeImplMethod, format); - protected override bool AllowDelegateAndEnumConstraints(ParseOptions options) - => options.LanguageVersion() >= LanguageVersion.CSharp7_3; + protected override bool AllowDelegateAndEnumConstraints(ParseOptions options) + => options.LanguageVersion() >= LanguageVersion.CSharp7_3; - protected override bool TryInitializeState( - Document document, SemanticModel model, SyntaxNode node, CancellationToken cancellationToken, - out SyntaxNode classOrStructDecl, out INamedTypeSymbol classOrStructType, out IEnumerable interfaceTypes) + protected override bool TryInitializeState( + Document document, SemanticModel model, SyntaxNode node, CancellationToken cancellationToken, + out SyntaxNode classOrStructDecl, out INamedTypeSymbol classOrStructType, out IEnumerable interfaceTypes) + { + if (!cancellationToken.IsCancellationRequested) { - if (!cancellationToken.IsCancellationRequested) + if (node is TypeSyntax interfaceNode && interfaceNode.Parent is BaseTypeSyntax baseType && + baseType.IsParentKind(SyntaxKind.BaseList) && + baseType.Type == interfaceNode) { - if (node is TypeSyntax interfaceNode && interfaceNode.Parent is BaseTypeSyntax baseType && - baseType.IsParentKind(SyntaxKind.BaseList) && - baseType.Type == interfaceNode) + if (interfaceNode.Parent.Parent?.Parent.Kind() is + SyntaxKind.ClassDeclaration or + SyntaxKind.StructDeclaration or + SyntaxKind.RecordDeclaration or + SyntaxKind.RecordStructDeclaration) { - if (interfaceNode.Parent.Parent?.Parent.Kind() is - SyntaxKind.ClassDeclaration or - SyntaxKind.StructDeclaration or - SyntaxKind.RecordDeclaration or - SyntaxKind.RecordStructDeclaration) + var interfaceSymbolInfo = model.GetSymbolInfo(interfaceNode, cancellationToken); + if (interfaceSymbolInfo.CandidateReason != CandidateReason.WrongArity) { - var interfaceSymbolInfo = model.GetSymbolInfo(interfaceNode, cancellationToken); - if (interfaceSymbolInfo.CandidateReason != CandidateReason.WrongArity) - { - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - if (interfaceSymbolInfo.GetAnySymbol() is INamedTypeSymbol interfaceType && interfaceType.TypeKind == TypeKind.Interface) - { - classOrStructDecl = interfaceNode.Parent.Parent.Parent as TypeDeclarationSyntax; - classOrStructType = model.GetDeclaredSymbol(classOrStructDecl, cancellationToken) as INamedTypeSymbol; - interfaceTypes = SpecializedCollections.SingletonEnumerable(interfaceType); + if (interfaceSymbolInfo.GetAnySymbol() is INamedTypeSymbol interfaceType && interfaceType.TypeKind == TypeKind.Interface) + { + classOrStructDecl = interfaceNode.Parent.Parent.Parent as TypeDeclarationSyntax; + classOrStructType = model.GetDeclaredSymbol(classOrStructDecl, cancellationToken) as INamedTypeSymbol; + interfaceTypes = SpecializedCollections.SingletonEnumerable(interfaceType); - return interfaceTypes != null && classOrStructType != null; - } + return interfaceTypes != null && classOrStructType != null; } } } } - - classOrStructDecl = null; - classOrStructType = null; - interfaceTypes = null; - return false; } - protected override bool CanImplementImplicitly => true; + classOrStructDecl = null; + classOrStructType = null; + interfaceTypes = null; + return false; + } - protected override bool HasHiddenExplicitImplementation => true; + protected override bool CanImplementImplicitly => true; - protected override SyntaxNode AddCommentInsideIfStatement(SyntaxNode ifStatement, SyntaxTriviaList trivia) - { - return ifStatement.ReplaceToken( - ifStatement.GetLastToken(), - ifStatement.GetLastToken().WithPrependedLeadingTrivia(trivia)); - } + protected override bool HasHiddenExplicitImplementation => true; - protected override SyntaxNode CreateFinalizer( - SyntaxGenerator g, INamedTypeSymbol classType, string disposeMethodDisplayString) - { - // ' Do not change this code... - // Dispose(false) - var disposeStatement = (StatementSyntax)AddComment(g, - string.Format(FeaturesResources.Do_not_change_this_code_Put_cleanup_code_in_0_method, disposeMethodDisplayString), - g.ExpressionStatement(g.InvocationExpression( - g.IdentifierName(nameof(IDisposable.Dispose)), - g.Argument(DisposingName, RefKind.None, g.FalseLiteralExpression())))); - - var methodDecl = SyntaxFactory.DestructorDeclaration(classType.Name).AddBodyStatements(disposeStatement); - - return AddComment(g, - string.Format(FeaturesResources.TODO_colon_override_finalizer_only_if_0_has_code_to_free_unmanaged_resources, disposeMethodDisplayString), - methodDecl); - } + protected override SyntaxNode AddCommentInsideIfStatement(SyntaxNode ifStatement, SyntaxTriviaList trivia) + { + return ifStatement.ReplaceToken( + ifStatement.GetLastToken(), + ifStatement.GetLastToken().WithPrependedLeadingTrivia(trivia)); + } + + protected override SyntaxNode CreateFinalizer( + SyntaxGenerator g, INamedTypeSymbol classType, string disposeMethodDisplayString) + { + // ' Do not change this code... + // Dispose(false) + var disposeStatement = (StatementSyntax)AddComment(g, + string.Format(FeaturesResources.Do_not_change_this_code_Put_cleanup_code_in_0_method, disposeMethodDisplayString), + g.ExpressionStatement(g.InvocationExpression( + g.IdentifierName(nameof(IDisposable.Dispose)), + g.Argument(DisposingName, RefKind.None, g.FalseLiteralExpression())))); + + var methodDecl = SyntaxFactory.DestructorDeclaration(classType.Name).AddBodyStatements(disposeStatement); + + return AddComment(g, + string.Format(FeaturesResources.TODO_colon_override_finalizer_only_if_0_has_code_to_free_unmanaged_resources, disposeMethodDisplayString), + methodDecl); } } diff --git a/src/Features/CSharp/Portable/InheritanceMargin/CSharpInheritanceMarginService.cs b/src/Features/CSharp/Portable/InheritanceMargin/CSharpInheritanceMarginService.cs index ebeb4db57affe..a62aed445f872 100644 --- a/src/Features/CSharp/Portable/InheritanceMargin/CSharpInheritanceMarginService.cs +++ b/src/Features/CSharp/Portable/InheritanceMargin/CSharpInheritanceMarginService.cs @@ -13,69 +13,68 @@ using Microsoft.CodeAnalysis.InheritanceMargin; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.InheritanceMargin +namespace Microsoft.CodeAnalysis.CSharp.InheritanceMargin; + +[ExportLanguageService(typeof(IInheritanceMarginService), LanguageNames.CSharp), Shared] +internal class CSharpInheritanceMarginService : AbstractInheritanceMarginService { - [ExportLanguageService(typeof(IInheritanceMarginService), LanguageNames.CSharp), Shared] - internal class CSharpInheritanceMarginService : AbstractInheritanceMarginService + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + [ImportingConstructor] + public CSharpInheritanceMarginService() { - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - [ImportingConstructor] - public CSharpInheritanceMarginService() - { - } + } - protected override string GlobalImportsTitle => CSharpFeaturesResources.Global_using_directives; + protected override string GlobalImportsTitle => CSharpFeaturesResources.Global_using_directives; - protected override ImmutableArray GetMembers(IEnumerable nodesToSearch) + protected override ImmutableArray GetMembers(IEnumerable nodesToSearch) + { + var typeDeclarationNodes = nodesToSearch.OfType(); + + using var _ = PooledObjects.ArrayBuilder.GetInstance(out var builder); + foreach (var typeDeclarationNode in typeDeclarationNodes) { - var typeDeclarationNodes = nodesToSearch.OfType(); + // 1. Add the type declaration node.(e.g. class, struct etc..) + // Use its identifier's position as the line number, since we want the margin to be placed with the identifier + builder.Add(typeDeclarationNode); - using var _ = PooledObjects.ArrayBuilder.GetInstance(out var builder); - foreach (var typeDeclarationNode in typeDeclarationNodes) + // 2. Add type members inside this type declaration. + foreach (var member in typeDeclarationNode.Members) { - // 1. Add the type declaration node.(e.g. class, struct etc..) - // Use its identifier's position as the line number, since we want the margin to be placed with the identifier - builder.Add(typeDeclarationNode); - - // 2. Add type members inside this type declaration. - foreach (var member in typeDeclarationNode.Members) + if (member.Kind() is + SyntaxKind.MethodDeclaration or + SyntaxKind.PropertyDeclaration or + SyntaxKind.EventDeclaration or + SyntaxKind.IndexerDeclaration or + SyntaxKind.OperatorDeclaration or + SyntaxKind.ConversionOperatorDeclaration) { - if (member.Kind() is - SyntaxKind.MethodDeclaration or - SyntaxKind.PropertyDeclaration or - SyntaxKind.EventDeclaration or - SyntaxKind.IndexerDeclaration or - SyntaxKind.OperatorDeclaration or - SyntaxKind.ConversionOperatorDeclaration) - { - builder.Add(member); - } + builder.Add(member); + } - // For multiple events that declared in the same EventFieldDeclaration, - // add all VariableDeclarators - if (member is EventFieldDeclarationSyntax eventFieldDeclarationNode) - { - builder.AddRange(eventFieldDeclarationNode.Declaration.Variables); - } + // For multiple events that declared in the same EventFieldDeclaration, + // add all VariableDeclarators + if (member is EventFieldDeclarationSyntax eventFieldDeclarationNode) + { + builder.AddRange(eventFieldDeclarationNode.Declaration.Variables); } } - - return builder.ToImmutableArray(); } - protected override SyntaxToken GetDeclarationToken(SyntaxNode declarationNode) - => declarationNode switch - { - MethodDeclarationSyntax methodDeclarationNode => methodDeclarationNode.Identifier, - PropertyDeclarationSyntax propertyDeclarationNode => propertyDeclarationNode.Identifier, - EventDeclarationSyntax eventDeclarationNode => eventDeclarationNode.Identifier, - VariableDeclaratorSyntax variableDeclaratorNode => variableDeclaratorNode.Identifier, - TypeDeclarationSyntax baseTypeDeclarationNode => baseTypeDeclarationNode.Identifier, - IndexerDeclarationSyntax indexerDeclarationNode => indexerDeclarationNode.ThisKeyword, - OperatorDeclarationSyntax operatorDeclarationNode => operatorDeclarationNode.OperatorToken, - ConversionOperatorDeclarationSyntax conversionOperatorDeclarationNode => conversionOperatorDeclarationNode.Type.GetFirstToken(), - // Shouldn't reach here since the input declaration nodes are coming from GetMembers() method above - _ => throw ExceptionUtilities.UnexpectedValue(declarationNode), - }; + return builder.ToImmutableArray(); } + + protected override SyntaxToken GetDeclarationToken(SyntaxNode declarationNode) + => declarationNode switch + { + MethodDeclarationSyntax methodDeclarationNode => methodDeclarationNode.Identifier, + PropertyDeclarationSyntax propertyDeclarationNode => propertyDeclarationNode.Identifier, + EventDeclarationSyntax eventDeclarationNode => eventDeclarationNode.Identifier, + VariableDeclaratorSyntax variableDeclaratorNode => variableDeclaratorNode.Identifier, + TypeDeclarationSyntax baseTypeDeclarationNode => baseTypeDeclarationNode.Identifier, + IndexerDeclarationSyntax indexerDeclarationNode => indexerDeclarationNode.ThisKeyword, + OperatorDeclarationSyntax operatorDeclarationNode => operatorDeclarationNode.OperatorToken, + ConversionOperatorDeclarationSyntax conversionOperatorDeclarationNode => conversionOperatorDeclarationNode.Type.GetFirstToken(), + // Shouldn't reach here since the input declaration nodes are coming from GetMembers() method above + _ => throw ExceptionUtilities.UnexpectedValue(declarationNode), + }; } diff --git a/src/Features/CSharp/Portable/InitializeParameter/CSharpAddParameterCheckCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/InitializeParameter/CSharpAddParameterCheckCodeRefactoringProvider.cs index 281de859fd508..08955f457e2d3 100644 --- a/src/Features/CSharp/Portable/InitializeParameter/CSharpAddParameterCheckCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/InitializeParameter/CSharpAddParameterCheckCodeRefactoringProvider.cs @@ -12,82 +12,81 @@ using Microsoft.CodeAnalysis.InitializeParameter; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -namespace Microsoft.CodeAnalysis.CSharp.InitializeParameter +namespace Microsoft.CodeAnalysis.CSharp.InitializeParameter; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.AddParameterCheck), Shared] +[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.ChangeSignature)] +internal sealed class CSharpAddParameterCheckCodeRefactoringProvider : + AbstractAddParameterCheckCodeRefactoringProvider< + BaseTypeDeclarationSyntax, + ParameterSyntax, + StatementSyntax, + ExpressionSyntax, + BinaryExpressionSyntax, + CSharpSimplifierOptions> { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.AddParameterCheck), Shared] - [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.ChangeSignature)] - internal sealed class CSharpAddParameterCheckCodeRefactoringProvider : - AbstractAddParameterCheckCodeRefactoringProvider< - BaseTypeDeclarationSyntax, - ParameterSyntax, - StatementSyntax, - ExpressionSyntax, - BinaryExpressionSyntax, - CSharpSimplifierOptions> + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpAddParameterCheckCodeRefactoringProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpAddParameterCheckCodeRefactoringProvider() - { - } + } - protected override bool IsFunctionDeclaration(SyntaxNode node) - => InitializeParameterHelpers.IsFunctionDeclaration(node); + protected override bool IsFunctionDeclaration(SyntaxNode node) + => InitializeParameterHelpers.IsFunctionDeclaration(node); - protected override SyntaxNode GetBody(SyntaxNode functionDeclaration) - => InitializeParameterHelpers.GetBody(functionDeclaration); + protected override SyntaxNode GetBody(SyntaxNode functionDeclaration) + => InitializeParameterHelpers.GetBody(functionDeclaration); - protected override void InsertStatement(SyntaxEditor editor, SyntaxNode functionDeclaration, bool returnsVoid, SyntaxNode? statementToAddAfter, StatementSyntax statement) - => InitializeParameterHelpers.InsertStatement(editor, functionDeclaration, returnsVoid, statementToAddAfter, statement); + protected override void InsertStatement(SyntaxEditor editor, SyntaxNode functionDeclaration, bool returnsVoid, SyntaxNode? statementToAddAfter, StatementSyntax statement) + => InitializeParameterHelpers.InsertStatement(editor, functionDeclaration, returnsVoid, statementToAddAfter, statement); - protected override bool IsImplicitConversion(Compilation compilation, ITypeSymbol source, ITypeSymbol destination) - => InitializeParameterHelpers.IsImplicitConversion(compilation, source, destination); + protected override bool IsImplicitConversion(Compilation compilation, ITypeSymbol source, ITypeSymbol destination) + => InitializeParameterHelpers.IsImplicitConversion(compilation, source, destination); - protected override bool CanOffer(SyntaxNode body) + protected override bool CanOffer(SyntaxNode body) + { + if (InitializeParameterHelpers.IsExpressionBody(body)) { - if (InitializeParameterHelpers.IsExpressionBody(body)) - { - return InitializeParameterHelpers.TryConvertExpressionBodyToStatement(body, - semicolonToken: Token(SyntaxKind.SemicolonToken), - createReturnStatementForExpression: false, - statement: out var _); - } - - return true; + return InitializeParameterHelpers.TryConvertExpressionBodyToStatement(body, + semicolonToken: Token(SyntaxKind.SemicolonToken), + createReturnStatementForExpression: false, + statement: out var _); } - protected override bool PrefersThrowExpression(CSharpSimplifierOptions options) - => options.PreferThrowExpression.Value; + return true; + } - protected override string EscapeResourceString(string input) - => input.Replace("\\", "\\\\").Replace("\"", "\\\""); + protected override bool PrefersThrowExpression(CSharpSimplifierOptions options) + => options.PreferThrowExpression.Value; - protected override StatementSyntax CreateParameterCheckIfStatement(ExpressionSyntax condition, StatementSyntax ifTrueStatement, CSharpSimplifierOptions options) - { - var withBlock = options.PreferBraces.Value == CodeAnalysis.CodeStyle.PreferBracesPreference.Always; - var singleLine = options.AllowEmbeddedStatementsOnSameLine.Value; - var closeParenToken = Token(SyntaxKind.CloseParenToken); - if (withBlock) - { - ifTrueStatement = Block(ifTrueStatement); - } - else if (singleLine) - { - // Any elastic trivia between the closing parenthesis of if and the statement must be removed - // to convince the formatter to keep everything on a single line. - // Note: ifTrueStatement and closeParenToken are generated, so there is no need to deal with any existing trivia. - closeParenToken = closeParenToken.WithTrailingTrivia(Space); - ifTrueStatement = ifTrueStatement.WithoutLeadingTrivia(); - } + protected override string EscapeResourceString(string input) + => input.Replace("\\", "\\\\").Replace("\"", "\\\""); - return IfStatement( - attributeLists: default, - ifKeyword: Token(SyntaxKind.IfKeyword), - openParenToken: Token(SyntaxKind.OpenParenToken), - condition: condition, - closeParenToken: closeParenToken, - statement: ifTrueStatement, - @else: null); + protected override StatementSyntax CreateParameterCheckIfStatement(ExpressionSyntax condition, StatementSyntax ifTrueStatement, CSharpSimplifierOptions options) + { + var withBlock = options.PreferBraces.Value == CodeAnalysis.CodeStyle.PreferBracesPreference.Always; + var singleLine = options.AllowEmbeddedStatementsOnSameLine.Value; + var closeParenToken = Token(SyntaxKind.CloseParenToken); + if (withBlock) + { + ifTrueStatement = Block(ifTrueStatement); } + else if (singleLine) + { + // Any elastic trivia between the closing parenthesis of if and the statement must be removed + // to convince the formatter to keep everything on a single line. + // Note: ifTrueStatement and closeParenToken are generated, so there is no need to deal with any existing trivia. + closeParenToken = closeParenToken.WithTrailingTrivia(Space); + ifTrueStatement = ifTrueStatement.WithoutLeadingTrivia(); + } + + return IfStatement( + attributeLists: default, + ifKeyword: Token(SyntaxKind.IfKeyword), + openParenToken: Token(SyntaxKind.OpenParenToken), + condition: condition, + closeParenToken: closeParenToken, + statement: ifTrueStatement, + @else: null); } } diff --git a/src/Features/CSharp/Portable/InitializeParameter/CSharpInitializeMemberFromParameterCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/InitializeParameter/CSharpInitializeMemberFromParameterCodeRefactoringProvider.cs index 337d218330f4d..a186ec2c13847 100644 --- a/src/Features/CSharp/Portable/InitializeParameter/CSharpInitializeMemberFromParameterCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/InitializeParameter/CSharpInitializeMemberFromParameterCodeRefactoringProvider.cs @@ -12,122 +12,121 @@ using Microsoft.CodeAnalysis.InitializeParameter; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.CSharp.InitializeParameter +namespace Microsoft.CodeAnalysis.CSharp.InitializeParameter; + +using static InitializeParameterHelpersCore; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InitializeMemberFromParameter), Shared] +[ExtensionOrder(Before = nameof(CSharpAddParameterCheckCodeRefactoringProvider))] +[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.Wrapping)] +internal sealed class CSharpInitializeMemberFromParameterCodeRefactoringProvider : + AbstractInitializeMemberFromParameterCodeRefactoringProvider< + BaseTypeDeclarationSyntax, + ParameterSyntax, + StatementSyntax, + ExpressionSyntax> { - using static InitializeParameterHelpersCore; - - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InitializeMemberFromParameter), Shared] - [ExtensionOrder(Before = nameof(CSharpAddParameterCheckCodeRefactoringProvider))] - [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.Wrapping)] - internal sealed class CSharpInitializeMemberFromParameterCodeRefactoringProvider : - AbstractInitializeMemberFromParameterCodeRefactoringProvider< - BaseTypeDeclarationSyntax, - ParameterSyntax, - StatementSyntax, - ExpressionSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpInitializeMemberFromParameterCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpInitializeMemberFromParameterCodeRefactoringProvider() - { - } + } - protected override bool IsFunctionDeclaration(SyntaxNode node) - => InitializeParameterHelpers.IsFunctionDeclaration(node); + protected override bool IsFunctionDeclaration(SyntaxNode node) + => InitializeParameterHelpers.IsFunctionDeclaration(node); - protected override SyntaxNode? TryGetLastStatement(IBlockOperation? blockStatement) - => InitializeParameterHelpers.TryGetLastStatement(blockStatement); + protected override SyntaxNode? TryGetLastStatement(IBlockOperation? blockStatement) + => InitializeParameterHelpers.TryGetLastStatement(blockStatement); - protected override void InsertStatement(SyntaxEditor editor, SyntaxNode functionDeclaration, bool returnsVoid, SyntaxNode? statementToAddAfter, StatementSyntax statement) - => InitializeParameterHelpers.InsertStatement(editor, functionDeclaration, returnsVoid, statementToAddAfter, statement); + protected override void InsertStatement(SyntaxEditor editor, SyntaxNode functionDeclaration, bool returnsVoid, SyntaxNode? statementToAddAfter, StatementSyntax statement) + => InitializeParameterHelpers.InsertStatement(editor, functionDeclaration, returnsVoid, statementToAddAfter, statement); - protected override bool IsImplicitConversion(Compilation compilation, ITypeSymbol source, ITypeSymbol destination) - => InitializeParameterHelpers.IsImplicitConversion(compilation, source, destination); + protected override bool IsImplicitConversion(Compilation compilation, ITypeSymbol source, ITypeSymbol destination) + => InitializeParameterHelpers.IsImplicitConversion(compilation, source, destination); - // Fields are always private by default in C#. - protected override Accessibility DetermineDefaultFieldAccessibility(INamedTypeSymbol containingType) - => Accessibility.Private; + // Fields are always private by default in C#. + protected override Accessibility DetermineDefaultFieldAccessibility(INamedTypeSymbol containingType) + => Accessibility.Private; - // Properties are always private by default in C#. - protected override Accessibility DetermineDefaultPropertyAccessibility() - => Accessibility.Private; + // Properties are always private by default in C#. + protected override Accessibility DetermineDefaultPropertyAccessibility() + => Accessibility.Private; - protected override SyntaxNode GetBody(SyntaxNode functionDeclaration) - => InitializeParameterHelpers.GetBody(functionDeclaration); + protected override SyntaxNode GetBody(SyntaxNode functionDeclaration) + => InitializeParameterHelpers.GetBody(functionDeclaration); - protected override SyntaxNode? GetAccessorBody(IMethodSymbol accessor, CancellationToken cancellationToken) - => InitializeParameterHelpers.GetAccessorBody(accessor, cancellationToken); + protected override SyntaxNode? GetAccessorBody(IMethodSymbol accessor, CancellationToken cancellationToken) + => InitializeParameterHelpers.GetAccessorBody(accessor, cancellationToken); - protected override SyntaxNode RemoveThrowNotImplemented(SyntaxNode node) - => InitializeParameterHelpers.RemoveThrowNotImplemented(node); + protected override SyntaxNode RemoveThrowNotImplemented(SyntaxNode node) + => InitializeParameterHelpers.RemoveThrowNotImplemented(node); - protected override bool TryUpdateTupleAssignment( - IBlockOperation? blockStatement, - IParameterSymbol parameter, - ISymbol fieldOrProperty, - SyntaxEditor editor) - { - if (blockStatement is null) - return false; + protected override bool TryUpdateTupleAssignment( + IBlockOperation? blockStatement, + IParameterSymbol parameter, + ISymbol fieldOrProperty, + SyntaxEditor editor) + { + if (blockStatement is null) + return false; - foreach (var (tupleLeft, tupleRight) in TryGetAssignmentExpressions(blockStatement)) + foreach (var (tupleLeft, tupleRight) in TryGetAssignmentExpressions(blockStatement)) + { + if (tupleLeft.Syntax is TupleExpressionSyntax tupleLeftSyntax && + tupleRight.Syntax is TupleExpressionSyntax tupleRightSyntax) { - if (tupleLeft.Syntax is TupleExpressionSyntax tupleLeftSyntax && - tupleRight.Syntax is TupleExpressionSyntax tupleRightSyntax) + var generator = editor.Generator; + foreach (var (sibling, before) in GetSiblingParameters(parameter)) { - var generator = editor.Generator; - foreach (var (sibling, before) in GetSiblingParameters(parameter)) + if (TryFindSiblingAssignment(tupleLeft, tupleRight, sibling, out var index)) { - if (TryFindSiblingAssignment(tupleLeft, tupleRight, sibling, out var index)) - { - // If we found assignment to a parameter before us, then add after that. - var insertionPosition = before ? index + 1 : index; - - var left = (ArgumentSyntax)generator.Argument(generator.MemberAccessExpression(generator.ThisExpression(), generator.IdentifierName(fieldOrProperty.Name))); - var right = (ArgumentSyntax)generator.Argument(generator.IdentifierName(parameter.Name)); - - editor.ReplaceNode( - tupleLeftSyntax, - tupleLeftSyntax.WithArguments(tupleLeftSyntax.Arguments.Insert(insertionPosition, left))); - editor.ReplaceNode( - tupleRightSyntax, - tupleRightSyntax.WithArguments(tupleRightSyntax.Arguments.Insert(insertionPosition, right))); - - return true; - } + // If we found assignment to a parameter before us, then add after that. + var insertionPosition = before ? index + 1 : index; + + var left = (ArgumentSyntax)generator.Argument(generator.MemberAccessExpression(generator.ThisExpression(), generator.IdentifierName(fieldOrProperty.Name))); + var right = (ArgumentSyntax)generator.Argument(generator.IdentifierName(parameter.Name)); + + editor.ReplaceNode( + tupleLeftSyntax, + tupleLeftSyntax.WithArguments(tupleLeftSyntax.Arguments.Insert(insertionPosition, left))); + editor.ReplaceNode( + tupleRightSyntax, + tupleRightSyntax.WithArguments(tupleRightSyntax.Arguments.Insert(insertionPosition, right))); + + return true; } } } - - return false; } - private static bool TryFindSiblingAssignment( - ITupleOperation tupleLeft, ITupleOperation tupleRight, IParameterSymbol sibling, out int index) + return false; + } + + private static bool TryFindSiblingAssignment( + ITupleOperation tupleLeft, ITupleOperation tupleRight, IParameterSymbol sibling, out int index) + { + for (int i = 0, n = tupleLeft.Elements.Length; i < n; i++) { - for (int i = 0, n = tupleLeft.Elements.Length; i < n; i++) - { - // rhs tuple has to directly reference the sibling parameter. lhs has to be a reference to a field/prop in this type. + // rhs tuple has to directly reference the sibling parameter. lhs has to be a reference to a field/prop in this type. - if (tupleRight.Elements[i] is IParameterReferenceOperation parameterReference && sibling.Equals(parameterReference.Parameter) && - IsFieldOrPropertyReference(tupleLeft.Elements[i], sibling.ContainingType, out _)) - { - index = i; - return true; - } + if (tupleRight.Elements[i] is IParameterReferenceOperation parameterReference && sibling.Equals(parameterReference.Parameter) && + IsFieldOrPropertyReference(tupleLeft.Elements[i], sibling.ContainingType, out _)) + { + index = i; + return true; } - - index = -1; - return false; } - private static IEnumerable<(ITupleOperation targetTuple, ITupleOperation valueTuple)> TryGetAssignmentExpressions(IBlockOperation blockOperation) + index = -1; + return false; + } + + private static IEnumerable<(ITupleOperation targetTuple, ITupleOperation valueTuple)> TryGetAssignmentExpressions(IBlockOperation blockOperation) + { + foreach (var operation in blockOperation.Operations) { - foreach (var operation in blockOperation.Operations) - { - if (TryGetPartsOfTupleAssignmentOperation(operation, out var targetTuple, out var valueTuple)) - yield return (targetTuple, valueTuple); - } + if (TryGetPartsOfTupleAssignmentOperation(operation, out var targetTuple, out var valueTuple)) + yield return (targetTuple, valueTuple); } } } diff --git a/src/Features/CSharp/Portable/InitializeParameter/InitializeParameterHelpers.cs b/src/Features/CSharp/Portable/InitializeParameter/InitializeParameterHelpers.cs index 598f06f313913..c107631da5f62 100644 --- a/src/Features/CSharp/Portable/InitializeParameter/InitializeParameterHelpers.cs +++ b/src/Features/CSharp/Portable/InitializeParameter/InitializeParameterHelpers.cs @@ -18,210 +18,209 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.InitializeParameter +namespace Microsoft.CodeAnalysis.CSharp.InitializeParameter; + +internal static class InitializeParameterHelpers { - internal static class InitializeParameterHelpers + public static bool IsFunctionDeclaration(SyntaxNode node) + => node is BaseMethodDeclarationSyntax + or LocalFunctionStatementSyntax + or AnonymousFunctionExpressionSyntax; + + public static SyntaxNode GetBody(SyntaxNode functionDeclaration) + => functionDeclaration switch + { + BaseMethodDeclarationSyntax methodDeclaration => (SyntaxNode?)methodDeclaration.Body ?? methodDeclaration.ExpressionBody!, + LocalFunctionStatementSyntax localFunction => (SyntaxNode?)localFunction.Body ?? localFunction.ExpressionBody!, + AnonymousFunctionExpressionSyntax anonymousFunction => anonymousFunction.Body, + _ => throw ExceptionUtilities.UnexpectedValue(functionDeclaration), + }; + + private static SyntaxToken? TryGetSemicolonToken(SyntaxNode functionDeclaration) + => functionDeclaration switch + { + BaseMethodDeclarationSyntax methodDeclaration => methodDeclaration.SemicolonToken, + LocalFunctionStatementSyntax localFunction => localFunction.SemicolonToken, + AnonymousFunctionExpressionSyntax _ => null, + _ => throw ExceptionUtilities.UnexpectedValue(functionDeclaration), + }; + + public static bool IsImplicitConversion(Compilation compilation, ITypeSymbol source, ITypeSymbol destination) + => compilation.ClassifyConversion(source: source, destination: destination).IsImplicit; + + public static SyntaxNode? TryGetLastStatement(IBlockOperation? blockStatement) + => blockStatement?.Syntax is BlockSyntax block + ? block.Statements.LastOrDefault() + : blockStatement?.Syntax; + + public static void InsertStatement( + SyntaxEditor editor, + SyntaxNode functionDeclaration, + bool returnsVoid, + SyntaxNode? statementToAddAfterOpt, + StatementSyntax statement) { - public static bool IsFunctionDeclaration(SyntaxNode node) - => node is BaseMethodDeclarationSyntax - or LocalFunctionStatementSyntax - or AnonymousFunctionExpressionSyntax; + var body = GetBody(functionDeclaration); - public static SyntaxNode GetBody(SyntaxNode functionDeclaration) - => functionDeclaration switch - { - BaseMethodDeclarationSyntax methodDeclaration => (SyntaxNode?)methodDeclaration.Body ?? methodDeclaration.ExpressionBody!, - LocalFunctionStatementSyntax localFunction => (SyntaxNode?)localFunction.Body ?? localFunction.ExpressionBody!, - AnonymousFunctionExpressionSyntax anonymousFunction => anonymousFunction.Body, - _ => throw ExceptionUtilities.UnexpectedValue(functionDeclaration), - }; - - private static SyntaxToken? TryGetSemicolonToken(SyntaxNode functionDeclaration) - => functionDeclaration switch - { - BaseMethodDeclarationSyntax methodDeclaration => methodDeclaration.SemicolonToken, - LocalFunctionStatementSyntax localFunction => localFunction.SemicolonToken, - AnonymousFunctionExpressionSyntax _ => null, - _ => throw ExceptionUtilities.UnexpectedValue(functionDeclaration), - }; - - public static bool IsImplicitConversion(Compilation compilation, ITypeSymbol source, ITypeSymbol destination) - => compilation.ClassifyConversion(source: source, destination: destination).IsImplicit; - - public static SyntaxNode? TryGetLastStatement(IBlockOperation? blockStatement) - => blockStatement?.Syntax is BlockSyntax block - ? block.Statements.LastOrDefault() - : blockStatement?.Syntax; - - public static void InsertStatement( - SyntaxEditor editor, - SyntaxNode functionDeclaration, - bool returnsVoid, - SyntaxNode? statementToAddAfterOpt, - StatementSyntax statement) + if (IsExpressionBody(body)) { - var body = GetBody(functionDeclaration); + var semicolonToken = TryGetSemicolonToken(functionDeclaration) ?? SyntaxFactory.Token(SyntaxKind.SemicolonToken); - if (IsExpressionBody(body)) + if (!TryConvertExpressionBodyToStatement(body, semicolonToken, !returnsVoid, out var convertedStatement)) { - var semicolonToken = TryGetSemicolonToken(functionDeclaration) ?? SyntaxFactory.Token(SyntaxKind.SemicolonToken); - - if (!TryConvertExpressionBodyToStatement(body, semicolonToken, !returnsVoid, out var convertedStatement)) - { - return; - } - - // Add the new statement as the first/last statement of the new block - // depending if we were asked to go after something or not. - editor.SetStatements(functionDeclaration, statementToAddAfterOpt == null - ? [statement, convertedStatement] - : [convertedStatement, statement]); + return; } - else if (body is BlockSyntax block) + + // Add the new statement as the first/last statement of the new block + // depending if we were asked to go after something or not. + editor.SetStatements(functionDeclaration, statementToAddAfterOpt == null + ? [statement, convertedStatement] + : [convertedStatement, statement]); + } + else if (body is BlockSyntax block) + { + // Look for the statement we were asked to go after. + var indexToAddAfter = block.Statements.IndexOf(s => s == statementToAddAfterOpt); + if (indexToAddAfter >= 0) { - // Look for the statement we were asked to go after. - var indexToAddAfter = block.Statements.IndexOf(s => s == statementToAddAfterOpt); - if (indexToAddAfter >= 0) - { - // If we find it, then insert the new statement after it. - editor.InsertAfter(block.Statements[indexToAddAfter], statement); - } - else if (block.Statements.Count > 0) - { - // Otherwise, if we have multiple statements already, then insert ourselves - // before the first one. - editor.InsertBefore(block.Statements[0], statement); - } - else - { - // Otherwise, we have no statements in this block. Add the new statement - // as the single statement the block will have. - Debug.Assert(block.Statements.Count == 0); - editor.ReplaceNode(block, (currentBlock, _) => ((BlockSyntax)currentBlock).AddStatements(statement)); - } - - // If the block was on a single line before, the format it so that the formatting - // engine will update it to go over multiple lines. Otherwise, we can end up in - // the strange state where the { and } tokens stay where they were originally, - // which will look very strange like: - // - // a => { - // if (...) { - // } }; - if (CSharpSyntaxFacts.Instance.IsOnSingleLine(block, fullSpan: false)) - { - editor.ReplaceNode( - block, - (currentBlock, _) => currentBlock.WithAdditionalAnnotations(Formatter.Annotation)); - } + // If we find it, then insert the new statement after it. + editor.InsertAfter(block.Statements[indexToAddAfter], statement); + } + else if (block.Statements.Count > 0) + { + // Otherwise, if we have multiple statements already, then insert ourselves + // before the first one. + editor.InsertBefore(block.Statements[0], statement); } else { - editor.SetStatements(functionDeclaration, ImmutableArray.Create(statement)); + // Otherwise, we have no statements in this block. Add the new statement + // as the single statement the block will have. + Debug.Assert(block.Statements.Count == 0); + editor.ReplaceNode(block, (currentBlock, _) => ((BlockSyntax)currentBlock).AddStatements(statement)); } - } - // either from an expression lambda or expression bodied member - public static bool IsExpressionBody(SyntaxNode body) - => body is ExpressionSyntax or ArrowExpressionClauseSyntax; - - public static bool TryConvertExpressionBodyToStatement( - SyntaxNode body, - SyntaxToken semicolonToken, - bool createReturnStatementForExpression, - [NotNullWhen(true)] out StatementSyntax? statement) - { - Debug.Assert(IsExpressionBody(body)); - - return body switch + // If the block was on a single line before, the format it so that the formatting + // engine will update it to go over multiple lines. Otherwise, we can end up in + // the strange state where the { and } tokens stay where they were originally, + // which will look very strange like: + // + // a => { + // if (...) { + // } }; + if (CSharpSyntaxFacts.Instance.IsOnSingleLine(block, fullSpan: false)) { - // If this is a => method, then we'll have to convert the method to have a block body. - ArrowExpressionClauseSyntax arrowClause => arrowClause.TryConvertToStatement(semicolonToken, createReturnStatementForExpression, out statement), - // must be an expression lambda - ExpressionSyntax expression => expression.TryConvertToStatement(semicolonToken, createReturnStatementForExpression, out statement), - _ => throw ExceptionUtilities.UnexpectedValue(body), - }; + editor.ReplaceNode( + block, + (currentBlock, _) => currentBlock.WithAdditionalAnnotations(Formatter.Annotation)); + } } - - public static SyntaxNode? GetAccessorBody(IMethodSymbol accessor, CancellationToken cancellationToken) + else { - var node = accessor.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); - if (node is AccessorDeclarationSyntax accessorDeclaration) - return accessorDeclaration.ExpressionBody ?? (SyntaxNode?)accessorDeclaration.Body; - - // `int Age => ...;` - if (node is ArrowExpressionClauseSyntax arrowExpression) - return arrowExpression; - - return null; + editor.SetStatements(functionDeclaration, ImmutableArray.Create(statement)); } + } + + // either from an expression lambda or expression bodied member + public static bool IsExpressionBody(SyntaxNode body) + => body is ExpressionSyntax or ArrowExpressionClauseSyntax; - public static SyntaxNode RemoveThrowNotImplemented(SyntaxNode node) - => node is PropertyDeclarationSyntax propertyDeclaration ? RemoveThrowNotImplemented(propertyDeclaration) : node; + public static bool TryConvertExpressionBodyToStatement( + SyntaxNode body, + SyntaxToken semicolonToken, + bool createReturnStatementForExpression, + [NotNullWhen(true)] out StatementSyntax? statement) + { + Debug.Assert(IsExpressionBody(body)); - public static PropertyDeclarationSyntax RemoveThrowNotImplemented(PropertyDeclarationSyntax propertyDeclaration) + return body switch { - if (propertyDeclaration.ExpressionBody != null) - { - var result = propertyDeclaration - .WithExpressionBody(null) - .WithSemicolonToken(default) - .AddAccessorListAccessors(SyntaxFactory - .AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))) - .WithTrailingTrivia(propertyDeclaration.SemicolonToken.TrailingTrivia) - .WithAdditionalAnnotations(Formatter.Annotation); - return result; - } + // If this is a => method, then we'll have to convert the method to have a block body. + ArrowExpressionClauseSyntax arrowClause => arrowClause.TryConvertToStatement(semicolonToken, createReturnStatementForExpression, out statement), + // must be an expression lambda + ExpressionSyntax expression => expression.TryConvertToStatement(semicolonToken, createReturnStatementForExpression, out statement), + _ => throw ExceptionUtilities.UnexpectedValue(body), + }; + } - if (propertyDeclaration.AccessorList != null) - { - var accessors = propertyDeclaration.AccessorList.Accessors.Select(RemoveThrowNotImplemented); - return propertyDeclaration.WithAccessorList( - propertyDeclaration.AccessorList.WithAccessors([.. accessors])); - } + public static SyntaxNode? GetAccessorBody(IMethodSymbol accessor, CancellationToken cancellationToken) + { + var node = accessor.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); + if (node is AccessorDeclarationSyntax accessorDeclaration) + return accessorDeclaration.ExpressionBody ?? (SyntaxNode?)accessorDeclaration.Body; - return propertyDeclaration; - } + // `int Age => ...;` + if (node is ArrowExpressionClauseSyntax arrowExpression) + return arrowExpression; + + return null; + } + + public static SyntaxNode RemoveThrowNotImplemented(SyntaxNode node) + => node is PropertyDeclarationSyntax propertyDeclaration ? RemoveThrowNotImplemented(propertyDeclaration) : node; - private static AccessorDeclarationSyntax RemoveThrowNotImplemented(AccessorDeclarationSyntax accessorDeclaration) + public static PropertyDeclarationSyntax RemoveThrowNotImplemented(PropertyDeclarationSyntax propertyDeclaration) + { + if (propertyDeclaration.ExpressionBody != null) { - var result = accessorDeclaration + var result = propertyDeclaration .WithExpressionBody(null) - .WithBody(null) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); - - return result.WithTrailingTrivia(accessorDeclaration.Body?.GetTrailingTrivia() ?? accessorDeclaration.SemicolonToken.TrailingTrivia); + .WithSemicolonToken(default) + .AddAccessorListAccessors(SyntaxFactory + .AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))) + .WithTrailingTrivia(propertyDeclaration.SemicolonToken.TrailingTrivia) + .WithAdditionalAnnotations(Formatter.Annotation); + return result; } - public static bool IsThrowNotImplementedProperty(Compilation compilation, IPropertySymbol property, CancellationToken cancellationToken) + if (propertyDeclaration.AccessorList != null) { - using var _ = ArrayBuilder.GetInstance(out var accessors); + var accessors = propertyDeclaration.AccessorList.Accessors.Select(RemoveThrowNotImplemented); + return propertyDeclaration.WithAccessorList( + propertyDeclaration.AccessorList.WithAccessors([.. accessors])); + } - if (property.GetMethod != null) - accessors.AddIfNotNull(GetAccessorBody(property.GetMethod, cancellationToken)); + return propertyDeclaration; + } - if (property.SetMethod != null) - accessors.AddIfNotNull(GetAccessorBody(property.SetMethod, cancellationToken)); + private static AccessorDeclarationSyntax RemoveThrowNotImplemented(AccessorDeclarationSyntax accessorDeclaration) + { + var result = accessorDeclaration + .WithExpressionBody(null) + .WithBody(null) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + + return result.WithTrailingTrivia(accessorDeclaration.Body?.GetTrailingTrivia() ?? accessorDeclaration.SemicolonToken.TrailingTrivia); + } + + public static bool IsThrowNotImplementedProperty(Compilation compilation, IPropertySymbol property, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var accessors); + + if (property.GetMethod != null) + accessors.AddIfNotNull(GetAccessorBody(property.GetMethod, cancellationToken)); - if (accessors.Count == 0) - return false; + if (property.SetMethod != null) + accessors.AddIfNotNull(GetAccessorBody(property.SetMethod, cancellationToken)); - foreach (var group in accessors.GroupBy(node => node.SyntaxTree)) + if (accessors.Count == 0) + return false; + + foreach (var group in accessors.GroupBy(node => node.SyntaxTree)) + { + var semanticModel = compilation.GetSemanticModel(group.Key); + foreach (var accessorBody in accessors) { - var semanticModel = compilation.GetSemanticModel(group.Key); - foreach (var accessorBody in accessors) - { - var operation = semanticModel.GetOperation(accessorBody, cancellationToken); - if (operation is null) - return false; - - if (!operation.IsSingleThrowNotImplementedOperation()) - return false; - } - } + var operation = semanticModel.GetOperation(accessorBody, cancellationToken); + if (operation is null) + return false; - return true; + if (!operation.IsSingleThrowNotImplementedOperation()) + return false; + } } + + return true; } } diff --git a/src/Features/CSharp/Portable/InlineHints/CSharpInlineHintsService.cs b/src/Features/CSharp/Portable/InlineHints/CSharpInlineHintsService.cs index 27d1eefde90bb..97ffaecd42a4f 100644 --- a/src/Features/CSharp/Portable/InlineHints/CSharpInlineHintsService.cs +++ b/src/Features/CSharp/Portable/InlineHints/CSharpInlineHintsService.cs @@ -7,18 +7,17 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.InlineHints; -namespace Microsoft.CodeAnalysis.CSharp.InlineHints +namespace Microsoft.CodeAnalysis.CSharp.InlineHints; + +/// +/// The service to locate all positions where inline hints should be placed. +/// +[ExportLanguageService(typeof(IInlineHintsService), LanguageNames.CSharp), Shared] +internal class CSharpInlineHintsService : AbstractInlineHintsService { - /// - /// The service to locate all positions where inline hints should be placed. - /// - [ExportLanguageService(typeof(IInlineHintsService), LanguageNames.CSharp), Shared] - internal class CSharpInlineHintsService : AbstractInlineHintsService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpInlineHintsService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpInlineHintsService() - { - } } } diff --git a/src/Features/CSharp/Portable/InlineHints/CSharpInlineParameterNameHintsService.cs b/src/Features/CSharp/Portable/InlineHints/CSharpInlineParameterNameHintsService.cs index 2738792d8c052..a3e711d372eca 100644 --- a/src/Features/CSharp/Portable/InlineHints/CSharpInlineParameterNameHintsService.cs +++ b/src/Features/CSharp/Portable/InlineHints/CSharpInlineParameterNameHintsService.cs @@ -12,97 +12,95 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.CSharp.InlineHints +namespace Microsoft.CodeAnalysis.CSharp.InlineHints; + +/// +/// The service to locate the positions in which the adornments should appear +/// as well as associate the adornments back to the parameter name +/// +[ExportLanguageService(typeof(IInlineParameterNameHintsService), LanguageNames.CSharp), Shared] +internal class CSharpInlineParameterNameHintsService : AbstractInlineParameterNameHintsService { + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpInlineParameterNameHintsService() + { + } - /// - /// The service to locate the positions in which the adornments should appear - /// as well as associate the adornments back to the parameter name - /// - [ExportLanguageService(typeof(IInlineParameterNameHintsService), LanguageNames.CSharp), Shared] - internal class CSharpInlineParameterNameHintsService : AbstractInlineParameterNameHintsService + protected override void AddAllParameterNameHintLocations( + SemanticModel semanticModel, + ISyntaxFactsService syntaxFacts, + SyntaxNode node, + ArrayBuilder<(int position, string? identifierArgument, IParameterSymbol? parameter, HintKind kind)> buffer, + CancellationToken cancellationToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpInlineParameterNameHintsService() + if (node is BaseArgumentListSyntax argumentList) { + AddArguments(semanticModel, syntaxFacts, buffer, argumentList, cancellationToken); } - - protected override void AddAllParameterNameHintLocations( - SemanticModel semanticModel, - ISyntaxFactsService syntaxFacts, - SyntaxNode node, - ArrayBuilder<(int position, string? identifierArgument, IParameterSymbol? parameter, HintKind kind)> buffer, - CancellationToken cancellationToken) + else if (node is AttributeArgumentListSyntax attributeArgumentList) { - if (node is BaseArgumentListSyntax argumentList) - { - AddArguments(semanticModel, syntaxFacts, buffer, argumentList, cancellationToken); - } - else if (node is AttributeArgumentListSyntax attributeArgumentList) - { - AddArguments(semanticModel, syntaxFacts, buffer, attributeArgumentList, cancellationToken); - } + AddArguments(semanticModel, syntaxFacts, buffer, attributeArgumentList, cancellationToken); } + } - private static void AddArguments( - SemanticModel semanticModel, - ISyntaxFactsService syntaxFacts, - ArrayBuilder<(int position, string? identifierArgument, IParameterSymbol? parameter, HintKind kind)> buffer, - AttributeArgumentListSyntax argumentList, - CancellationToken cancellationToken) + private static void AddArguments( + SemanticModel semanticModel, + ISyntaxFactsService syntaxFacts, + ArrayBuilder<(int position, string? identifierArgument, IParameterSymbol? parameter, HintKind kind)> buffer, + AttributeArgumentListSyntax argumentList, + CancellationToken cancellationToken) + { + foreach (var argument in argumentList.Arguments) { - foreach (var argument in argumentList.Arguments) - { - if (argument.NameEquals != null || argument.NameColon != null) - continue; + if (argument.NameEquals != null || argument.NameColon != null) + continue; - var parameter = argument.DetermineParameter(semanticModel, cancellationToken: cancellationToken); - var identifierArgument = GetIdentifierNameFromArgument(argument, syntaxFacts); - buffer.Add((argument.Span.Start, identifierArgument, parameter, GetKind(argument.Expression))); - } + var parameter = argument.DetermineParameter(semanticModel, cancellationToken: cancellationToken); + var identifierArgument = GetIdentifierNameFromArgument(argument, syntaxFacts); + buffer.Add((argument.Span.Start, identifierArgument, parameter, GetKind(argument.Expression))); } + } - private static void AddArguments( - SemanticModel semanticModel, - ISyntaxFactsService syntaxFacts, - ArrayBuilder<(int position, string? identifierArgument, IParameterSymbol? parameter, HintKind kind)> buffer, - BaseArgumentListSyntax argumentList, - CancellationToken cancellationToken) + private static void AddArguments( + SemanticModel semanticModel, + ISyntaxFactsService syntaxFacts, + ArrayBuilder<(int position, string? identifierArgument, IParameterSymbol? parameter, HintKind kind)> buffer, + BaseArgumentListSyntax argumentList, + CancellationToken cancellationToken) + { + foreach (var argument in argumentList.Arguments) { - foreach (var argument in argumentList.Arguments) - { - if (argument.NameColon != null) - continue; + if (argument.NameColon != null) + continue; - var parameter = argument.DetermineParameter(semanticModel, cancellationToken: cancellationToken); - var identifierArgument = GetIdentifierNameFromArgument(argument, syntaxFacts); - buffer.Add((argument.Span.Start, identifierArgument, parameter, GetKind(argument.Expression))); - } + var parameter = argument.DetermineParameter(semanticModel, cancellationToken: cancellationToken); + var identifierArgument = GetIdentifierNameFromArgument(argument, syntaxFacts); + buffer.Add((argument.Span.Start, identifierArgument, parameter, GetKind(argument.Expression))); } + } - private static HintKind GetKind(ExpressionSyntax arg) - => arg switch - { - LiteralExpressionSyntax or InterpolatedStringExpressionSyntax => HintKind.Literal, - ObjectCreationExpressionSyntax => HintKind.ObjectCreation, - CastExpressionSyntax cast => GetKind(cast.Expression), - PrefixUnaryExpressionSyntax prefix => GetKind(prefix.Operand), - // Treat `expr!` the same as `expr` (i.e. treat `!` as if it's just trivia). - PostfixUnaryExpressionSyntax(SyntaxKind.SuppressNullableWarningExpression) postfix => GetKind(postfix.Operand), - _ => HintKind.Other, - }; - - protected override bool IsIndexer(SyntaxNode node, IParameterSymbol parameter) + private static HintKind GetKind(ExpressionSyntax arg) + => arg switch { - return node is BracketedArgumentListSyntax; - } + LiteralExpressionSyntax or InterpolatedStringExpressionSyntax => HintKind.Literal, + ObjectCreationExpressionSyntax => HintKind.ObjectCreation, + CastExpressionSyntax cast => GetKind(cast.Expression), + PrefixUnaryExpressionSyntax prefix => GetKind(prefix.Operand), + // Treat `expr!` the same as `expr` (i.e. treat `!` as if it's just trivia). + PostfixUnaryExpressionSyntax(SyntaxKind.SuppressNullableWarningExpression) postfix => GetKind(postfix.Operand), + _ => HintKind.Other, + }; - protected override string GetReplacementText(string parameterName) - { - var keywordKind = SyntaxFacts.GetKeywordKind(parameterName); - var isReservedKeyword = SyntaxFacts.IsReservedKeyword(keywordKind); - return (isReservedKeyword ? "@" : string.Empty) + parameterName + ": "; - } + protected override bool IsIndexer(SyntaxNode node, IParameterSymbol parameter) + { + return node is BracketedArgumentListSyntax; + } + + protected override string GetReplacementText(string parameterName) + { + var keywordKind = SyntaxFacts.GetKeywordKind(parameterName); + var isReservedKeyword = SyntaxFacts.IsReservedKeyword(keywordKind); + return (isReservedKeyword ? "@" : string.Empty) + parameterName + ": "; } } diff --git a/src/Features/CSharp/Portable/InlineHints/CSharpInlineTypeHintsService.cs b/src/Features/CSharp/Portable/InlineHints/CSharpInlineTypeHintsService.cs index a62064f69bebd..f9fe844e1bd56 100644 --- a/src/Features/CSharp/Portable/InlineHints/CSharpInlineTypeHintsService.cs +++ b/src/Features/CSharp/Portable/InlineHints/CSharpInlineTypeHintsService.cs @@ -13,132 +13,131 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.InlineHints +namespace Microsoft.CodeAnalysis.CSharp.InlineHints; + +[ExportLanguageService(typeof(IInlineTypeHintsService), LanguageNames.CSharp), Shared] +internal sealed class CSharpInlineTypeHintsService : AbstractInlineTypeHintsService { - [ExportLanguageService(typeof(IInlineTypeHintsService), LanguageNames.CSharp), Shared] - internal sealed class CSharpInlineTypeHintsService : AbstractInlineTypeHintsService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpInlineTypeHintsService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpInlineTypeHintsService() - { - } + } - protected override TypeHint? TryGetTypeHint( - SemanticModel semanticModel, - SyntaxNode node, - bool displayAllOverride, - bool forImplicitVariableTypes, - bool forLambdaParameterTypes, - bool forImplicitObjectCreation, - CancellationToken cancellationToken) + protected override TypeHint? TryGetTypeHint( + SemanticModel semanticModel, + SyntaxNode node, + bool displayAllOverride, + bool forImplicitVariableTypes, + bool forLambdaParameterTypes, + bool forImplicitObjectCreation, + CancellationToken cancellationToken) + { + if (forImplicitVariableTypes || displayAllOverride) { - if (forImplicitVariableTypes || displayAllOverride) + if (node is VariableDeclarationSyntax { Type.IsVar: true } variableDeclaration && + variableDeclaration.Variables.Count == 1 && + !variableDeclaration.Variables[0].Identifier.IsMissing) { - if (node is VariableDeclarationSyntax { Type.IsVar: true } variableDeclaration && - variableDeclaration.Variables.Count == 1 && - !variableDeclaration.Variables[0].Identifier.IsMissing) - { - var type = semanticModel.GetTypeInfo(variableDeclaration.Type, cancellationToken).Type; - if (IsValidType(type)) - return CreateTypeHint(type, displayAllOverride, forImplicitVariableTypes, variableDeclaration.Type, variableDeclaration.Variables[0].Identifier); - } + var type = semanticModel.GetTypeInfo(variableDeclaration.Type, cancellationToken).Type; + if (IsValidType(type)) + return CreateTypeHint(type, displayAllOverride, forImplicitVariableTypes, variableDeclaration.Type, variableDeclaration.Variables[0].Identifier); + } - // We handle individual variables of ParenthesizedVariableDesignationSyntax separately. - // For example, in `var (x, y) = (0, "")`, we should `int` for `x` and `string` for `y`. - // It's redundant to show `(int, string)` for `var` - if (node is DeclarationExpressionSyntax { Type.IsVar: true, Designation: not ParenthesizedVariableDesignationSyntax } declarationExpression) - { - var type = semanticModel.GetTypeInfo(declarationExpression.Type, cancellationToken).Type; - if (IsValidType(type)) - return CreateTypeHint(type, displayAllOverride, forImplicitVariableTypes, declarationExpression.Type, declarationExpression.Designation); - } - else if (node is SingleVariableDesignationSyntax { Parent: not DeclarationPatternSyntax and not DeclarationExpressionSyntax } variableDesignation) - { - var local = semanticModel.GetDeclaredSymbol(variableDesignation, cancellationToken) as ILocalSymbol; - var type = local?.Type; - if (IsValidType(type)) - { - return node.Parent is VarPatternSyntax varPattern - ? CreateTypeHint(type, displayAllOverride, forImplicitVariableTypes, varPattern.VarKeyword, variableDesignation.Identifier) - : new(type, new TextSpan(variableDesignation.Identifier.SpanStart, 0), textChange: null, trailingSpace: true); - } - } - else if (node is ForEachStatementSyntax { Type.IsVar: true } forEachStatement) + // We handle individual variables of ParenthesizedVariableDesignationSyntax separately. + // For example, in `var (x, y) = (0, "")`, we should `int` for `x` and `string` for `y`. + // It's redundant to show `(int, string)` for `var` + if (node is DeclarationExpressionSyntax { Type.IsVar: true, Designation: not ParenthesizedVariableDesignationSyntax } declarationExpression) + { + var type = semanticModel.GetTypeInfo(declarationExpression.Type, cancellationToken).Type; + if (IsValidType(type)) + return CreateTypeHint(type, displayAllOverride, forImplicitVariableTypes, declarationExpression.Type, declarationExpression.Designation); + } + else if (node is SingleVariableDesignationSyntax { Parent: not DeclarationPatternSyntax and not DeclarationExpressionSyntax } variableDesignation) + { + var local = semanticModel.GetDeclaredSymbol(variableDesignation, cancellationToken) as ILocalSymbol; + var type = local?.Type; + if (IsValidType(type)) { - var info = semanticModel.GetForEachStatementInfo(forEachStatement); - var type = info.ElementType; - if (IsValidType(type)) - return CreateTypeHint(type, displayAllOverride, forImplicitVariableTypes, forEachStatement.Type, forEachStatement.Identifier); + return node.Parent is VarPatternSyntax varPattern + ? CreateTypeHint(type, displayAllOverride, forImplicitVariableTypes, varPattern.VarKeyword, variableDesignation.Identifier) + : new(type, new TextSpan(variableDesignation.Identifier.SpanStart, 0), textChange: null, trailingSpace: true); } } + else if (node is ForEachStatementSyntax { Type.IsVar: true } forEachStatement) + { + var info = semanticModel.GetForEachStatementInfo(forEachStatement); + var type = info.ElementType; + if (IsValidType(type)) + return CreateTypeHint(type, displayAllOverride, forImplicitVariableTypes, forEachStatement.Type, forEachStatement.Identifier); + } + } - if (forLambdaParameterTypes || displayAllOverride) + if (forLambdaParameterTypes || displayAllOverride) + { + if (node is ParameterSyntax { Type: null } parameterNode) { - if (node is ParameterSyntax { Type: null } parameterNode) + var span = new TextSpan(parameterNode.Identifier.SpanStart, 0); + var parameter = semanticModel.GetDeclaredSymbol(parameterNode, cancellationToken); + if (parameter?.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.AnonymousFunction } && + IsValidType(parameter?.Type)) { - var span = new TextSpan(parameterNode.Identifier.SpanStart, 0); - var parameter = semanticModel.GetDeclaredSymbol(parameterNode, cancellationToken); - if (parameter?.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.AnonymousFunction } && - IsValidType(parameter?.Type)) - { - return parameterNode.Parent?.Parent?.Kind() is SyntaxKind.ParenthesizedLambdaExpression - ? new TypeHint(parameter.Type, span, textChange: new TextChange(span, parameter.Type.ToDisplayString(s_minimalTypeStyle) + " "), trailingSpace: true) - : new TypeHint(parameter.Type, span, textChange: null, trailingSpace: true); - } + return parameterNode.Parent?.Parent?.Kind() is SyntaxKind.ParenthesizedLambdaExpression + ? new TypeHint(parameter.Type, span, textChange: new TextChange(span, parameter.Type.ToDisplayString(s_minimalTypeStyle) + " "), trailingSpace: true) + : new TypeHint(parameter.Type, span, textChange: null, trailingSpace: true); } } + } - if (forImplicitObjectCreation || displayAllOverride) + if (forImplicitObjectCreation || displayAllOverride) + { + if (node is ImplicitObjectCreationExpressionSyntax implicitNew) { - if (node is ImplicitObjectCreationExpressionSyntax implicitNew) + var type = semanticModel.GetTypeInfo(implicitNew, cancellationToken).Type; + if (IsValidType(type)) { - var type = semanticModel.GetTypeInfo(implicitNew, cancellationToken).Type; - if (IsValidType(type)) - { - var span = new TextSpan(implicitNew.NewKeyword.Span.End, 0); - return new(type, span, new TextChange(span, " " + type.ToDisplayString(s_minimalTypeStyle)), leadingSpace: true); - } + var span = new TextSpan(implicitNew.NewKeyword.Span.End, 0); + return new(type, span, new TextChange(span, " " + type.ToDisplayString(s_minimalTypeStyle)), leadingSpace: true); } } - - return null; } - private static TypeHint CreateTypeHint( - ITypeSymbol type, - bool displayAllOverride, - bool normalOption, - SyntaxNodeOrToken displayAllSpan, - SyntaxNodeOrToken normalSpan) - { - var span = GetSpan(displayAllOverride, normalOption, displayAllSpan, normalSpan); - // if this is a hint that is placed in-situ (i.e. it's not overwriting text like 'var'), then place - // a space after it to make things feel less cramped. - var trailingSpace = span.Length == 0; - return new TypeHint(type, span, new TextChange(displayAllSpan.Span, type.ToDisplayString(s_minimalTypeStyle)), trailingSpace: trailingSpace); - } + return null; + } - private static TextSpan GetSpan( - bool displayAllOverride, - bool normalOption, - SyntaxNodeOrToken displayAllSpan, - SyntaxNodeOrToken normalSpan) - { - // If we're showing this because the normal option is on, then place the hint prior to the node being marked. - if (normalOption) - return new TextSpan(normalSpan.SpanStart, 0); + private static TypeHint CreateTypeHint( + ITypeSymbol type, + bool displayAllOverride, + bool normalOption, + SyntaxNodeOrToken displayAllSpan, + SyntaxNodeOrToken normalSpan) + { + var span = GetSpan(displayAllOverride, normalOption, displayAllSpan, normalSpan); + // if this is a hint that is placed in-situ (i.e. it's not overwriting text like 'var'), then place + // a space after it to make things feel less cramped. + var trailingSpace = span.Length == 0; + return new TypeHint(type, span, new TextChange(displayAllSpan.Span, type.ToDisplayString(s_minimalTypeStyle)), trailingSpace: trailingSpace); + } - // Otherwise, we're showing because the user explicitly asked to see all hints. In that case, overwrite the - // provided span (i.e. overwrite 'var' with 'int') as this provides a cleaner view while the user is in this - // mode. - Contract.ThrowIfFalse(displayAllOverride); - return displayAllSpan.Span; - } + private static TextSpan GetSpan( + bool displayAllOverride, + bool normalOption, + SyntaxNodeOrToken displayAllSpan, + SyntaxNodeOrToken normalSpan) + { + // If we're showing this because the normal option is on, then place the hint prior to the node being marked. + if (normalOption) + return new TextSpan(normalSpan.SpanStart, 0); - private static bool IsValidType([NotNullWhen(true)] ITypeSymbol? type) - { - return type is not null or IErrorTypeSymbol && type.Name != "var"; - } + // Otherwise, we're showing because the user explicitly asked to see all hints. In that case, overwrite the + // provided span (i.e. overwrite 'var' with 'int') as this provides a cleaner view while the user is in this + // mode. + Contract.ThrowIfFalse(displayAllOverride); + return displayAllSpan.Span; + } + + private static bool IsValidType([NotNullWhen(true)] ITypeSymbol? type) + { + return type is not null or IErrorTypeSymbol && type.Name != "var"; } } diff --git a/src/Features/CSharp/Portable/IntroduceParameter/CSharpIntroduceParameterCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/IntroduceParameter/CSharpIntroduceParameterCodeRefactoringProvider.cs index c7aab28be4b67..cc2f7ada59ec5 100644 --- a/src/Features/CSharp/Portable/IntroduceParameter/CSharpIntroduceParameterCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/IntroduceParameter/CSharpIntroduceParameterCodeRefactoringProvider.cs @@ -9,38 +9,37 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.IntroduceParameter; -namespace Microsoft.CodeAnalysis.CSharp.IntroduceParameter +namespace Microsoft.CodeAnalysis.CSharp.IntroduceParameter; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.IntroduceParameter), Shared] +internal partial class CSharpIntroduceParameterCodeRefactoringProvider : AbstractIntroduceParameterCodeRefactoringProvider< + ExpressionSyntax, + InvocationExpressionSyntax, + ObjectCreationExpressionSyntax, + IdentifierNameSyntax, + ArgumentSyntax> { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.IntroduceParameter), Shared] - internal partial class CSharpIntroduceParameterCodeRefactoringProvider : AbstractIntroduceParameterCodeRefactoringProvider< - ExpressionSyntax, - InvocationExpressionSyntax, - ObjectCreationExpressionSyntax, - IdentifierNameSyntax, - ArgumentSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpIntroduceParameterCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpIntroduceParameterCodeRefactoringProvider() - { - } - - protected override SyntaxNode GenerateExpressionFromOptionalParameter(IParameterSymbol parameterSymbol) - { - return ExpressionGenerator.GenerateExpression(CSharpSyntaxGenerator.Instance, parameterSymbol.Type, parameterSymbol.ExplicitDefaultValue, canUseFieldReference: true); - } + } - protected override SyntaxNode? GetLocalDeclarationFromDeclarator(SyntaxNode variableDecl) - { - return variableDecl.Parent?.Parent as LocalDeclarationStatementSyntax; - } + protected override SyntaxNode GenerateExpressionFromOptionalParameter(IParameterSymbol parameterSymbol) + { + return ExpressionGenerator.GenerateExpression(CSharpSyntaxGenerator.Instance, parameterSymbol.Type, parameterSymbol.ExplicitDefaultValue, canUseFieldReference: true); + } - protected override bool IsDestructor(IMethodSymbol methodSymbol) - { - return false; - } + protected override SyntaxNode? GetLocalDeclarationFromDeclarator(SyntaxNode variableDecl) + { + return variableDecl.Parent?.Parent as LocalDeclarationStatementSyntax; + } - protected override SyntaxNode UpdateArgumentListSyntax(SyntaxNode argumentList, SeparatedSyntaxList arguments) - => ((ArgumentListSyntax)argumentList).WithArguments(arguments); + protected override bool IsDestructor(IMethodSymbol methodSymbol) + { + return false; } + + protected override SyntaxNode UpdateArgumentListSyntax(SyntaxNode argumentList, SeparatedSyntaxList arguments) + => ((ArgumentListSyntax)argumentList).WithArguments(arguments); } diff --git a/src/Features/CSharp/Portable/IntroduceUsingStatement/CSharpIntroduceUsingStatementCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/IntroduceUsingStatement/CSharpIntroduceUsingStatementCodeRefactoringProvider.cs index 2072e79fed69f..251d67e7eee03 100644 --- a/src/Features/CSharp/Portable/IntroduceUsingStatement/CSharpIntroduceUsingStatementCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/IntroduceUsingStatement/CSharpIntroduceUsingStatementCodeRefactoringProvider.cs @@ -11,73 +11,72 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.IntroduceUsingStatement +namespace Microsoft.CodeAnalysis.CSharp.IntroduceUsingStatement; + +[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.IntroduceUsingStatement), Shared] +internal sealed class CSharpIntroduceUsingStatementCodeRefactoringProvider + : AbstractIntroduceUsingStatementCodeRefactoringProvider { - [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.IntroduceUsingStatement), Shared] - internal sealed class CSharpIntroduceUsingStatementCodeRefactoringProvider - : AbstractIntroduceUsingStatementCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpIntroduceUsingStatementCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpIntroduceUsingStatementCodeRefactoringProvider() - { - } - - protected override string CodeActionTitle => CSharpFeaturesResources.Introduce_using_statement; + } - protected override bool HasCatchBlocks(TryStatementSyntax tryStatement) - => tryStatement.Catches.Count > 0; + protected override string CodeActionTitle => CSharpFeaturesResources.Introduce_using_statement; - protected override (SyntaxList tryStatements, SyntaxList finallyStatements) GetTryFinallyStatements(TryStatementSyntax tryStatement) - => (tryStatement.Block.Statements, tryStatement.Finally?.Block.Statements ?? default); + protected override bool HasCatchBlocks(TryStatementSyntax tryStatement) + => tryStatement.Catches.Count > 0; - protected override bool CanRefactorToContainBlockStatements(SyntaxNode parent) - => parent is BlockSyntax || parent is SwitchSectionSyntax || parent.IsEmbeddedStatementOwner(); + protected override (SyntaxList tryStatements, SyntaxList finallyStatements) GetTryFinallyStatements(TryStatementSyntax tryStatement) + => (tryStatement.Block.Statements, tryStatement.Finally?.Block.Statements ?? default); - protected override SyntaxList GetSurroundingStatements(LocalDeclarationStatementSyntax declarationStatement) - => declarationStatement.GetRequiredParent() switch - { - BlockSyntax block => block.Statements, - SwitchSectionSyntax switchSection => switchSection.Statements, - _ => [declarationStatement], - }; + protected override bool CanRefactorToContainBlockStatements(SyntaxNode parent) + => parent is BlockSyntax || parent is SwitchSectionSyntax || parent.IsEmbeddedStatementOwner(); - protected override SyntaxNode WithStatements(SyntaxNode parentOfStatementsToSurround, SyntaxList statements) + protected override SyntaxList GetSurroundingStatements(LocalDeclarationStatementSyntax declarationStatement) + => declarationStatement.GetRequiredParent() switch { - return - parentOfStatementsToSurround is BlockSyntax block ? block.WithStatements(statements) as SyntaxNode : - parentOfStatementsToSurround is SwitchSectionSyntax switchSection ? switchSection.WithStatements(statements) : - throw ExceptionUtilities.UnexpectedValue(parentOfStatementsToSurround); - } + BlockSyntax block => block.Statements, + SwitchSectionSyntax switchSection => switchSection.Statements, + _ => [declarationStatement], + }; - protected override StatementSyntax CreateUsingStatement(LocalDeclarationStatementSyntax declarationStatement, SyntaxList statementsToSurround) - => SyntaxFactory.UsingStatement( - SyntaxFactory.Token(SyntaxKind.UsingKeyword).WithLeadingTrivia(declarationStatement.GetLeadingTrivia()), - SyntaxFactory.Token(SyntaxKind.OpenParenToken), - declaration: declarationStatement.Declaration.WithoutTrivia(), - expression: null, // Declaration already has equals token and expression - SyntaxFactory.Token(SyntaxKind.CloseParenToken).WithTrailingTrivia(declarationStatement.GetTrailingTrivia()), - statement: SyntaxFactory.Block(statementsToSurround)); + protected override SyntaxNode WithStatements(SyntaxNode parentOfStatementsToSurround, SyntaxList statements) + { + return + parentOfStatementsToSurround is BlockSyntax block ? block.WithStatements(statements) as SyntaxNode : + parentOfStatementsToSurround is SwitchSectionSyntax switchSection ? switchSection.WithStatements(statements) : + throw ExceptionUtilities.UnexpectedValue(parentOfStatementsToSurround); + } - protected override bool TryCreateUsingLocalDeclaration( - ParseOptions options, - LocalDeclarationStatementSyntax declarationStatement, - [NotNullWhen(true)] out LocalDeclarationStatementSyntax? usingDeclarationStatement) - { - usingDeclarationStatement = null; + protected override StatementSyntax CreateUsingStatement(LocalDeclarationStatementSyntax declarationStatement, SyntaxList statementsToSurround) + => SyntaxFactory.UsingStatement( + SyntaxFactory.Token(SyntaxKind.UsingKeyword).WithLeadingTrivia(declarationStatement.GetLeadingTrivia()), + SyntaxFactory.Token(SyntaxKind.OpenParenToken), + declaration: declarationStatement.Declaration.WithoutTrivia(), + expression: null, // Declaration already has equals token and expression + SyntaxFactory.Token(SyntaxKind.CloseParenToken).WithTrailingTrivia(declarationStatement.GetTrailingTrivia()), + statement: SyntaxFactory.Block(statementsToSurround)); - // using-declarations are not allowed in switch sections (due to craziness with how switch section scoping works). - if (declarationStatement.Parent is SwitchSectionSyntax || - options.LanguageVersion() < LanguageVersion.CSharp8) - { - return false; - } + protected override bool TryCreateUsingLocalDeclaration( + ParseOptions options, + LocalDeclarationStatementSyntax declarationStatement, + [NotNullWhen(true)] out LocalDeclarationStatementSyntax? usingDeclarationStatement) + { + usingDeclarationStatement = null; - usingDeclarationStatement = declarationStatement - .WithoutLeadingTrivia() - .WithUsingKeyword(SyntaxFactory.Token(declarationStatement.GetLeadingTrivia(), SyntaxKind.UsingKeyword, [SyntaxFactory.Space])); - return true; + // using-declarations are not allowed in switch sections (due to craziness with how switch section scoping works). + if (declarationStatement.Parent is SwitchSectionSyntax || + options.LanguageVersion() < LanguageVersion.CSharp8) + { + return false; } + + usingDeclarationStatement = declarationStatement + .WithoutLeadingTrivia() + .WithUsingKeyword(SyntaxFactory.Token(declarationStatement.GetLeadingTrivia(), SyntaxKind.UsingKeyword, [SyntaxFactory.Space])); + return true; } } diff --git a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceLocalForExpressionCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceLocalForExpressionCodeRefactoringProvider.cs index bac675d55e1b4..b4217ef88fb91 100644 --- a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceLocalForExpressionCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceLocalForExpressionCodeRefactoringProvider.cs @@ -21,138 +21,137 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable +namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable; + +using static SyntaxFactory; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.IntroduceLocalForExpression), Shared] +internal class CSharpIntroduceLocalForExpressionCodeRefactoringProvider : + AbstractIntroduceLocalForExpressionCodeRefactoringProvider< + ExpressionSyntax, + StatementSyntax, + ExpressionStatementSyntax, + LocalDeclarationStatementSyntax> { - using static SyntaxFactory; - - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.IntroduceLocalForExpression), Shared] - internal class CSharpIntroduceLocalForExpressionCodeRefactoringProvider : - AbstractIntroduceLocalForExpressionCodeRefactoringProvider< - ExpressionSyntax, - StatementSyntax, - ExpressionStatementSyntax, - LocalDeclarationStatementSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpIntroduceLocalForExpressionCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpIntroduceLocalForExpressionCodeRefactoringProvider() + } + + protected override bool IsValid(ExpressionStatementSyntax expressionStatement, TextSpan span) + { + // Expression is likely too simple to want to offer to generate a local for. + // This leads to too many false cases where this is offered. + if (span.IsEmpty && + expressionStatement.SemicolonToken.IsMissing && + expressionStatement.Expression.IsKind(SyntaxKind.IdentifierName)) { + return false; } - protected override bool IsValid(ExpressionStatementSyntax expressionStatement, TextSpan span) - { - // Expression is likely too simple to want to offer to generate a local for. - // This leads to too many false cases where this is offered. - if (span.IsEmpty && - expressionStatement.SemicolonToken.IsMissing && - expressionStatement.Expression.IsKind(SyntaxKind.IdentifierName)) - { - return false; - } + // We don't want to offer new local for an assignmentExpression `a = 42` -> `int newA = a = 42` + return expressionStatement.Expression is not AssignmentExpressionSyntax; + } - // We don't want to offer new local for an assignmentExpression `a = 42` -> `int newA = a = 42` - return expressionStatement.Expression is not AssignmentExpressionSyntax; + protected override LocalDeclarationStatementSyntax FixupLocalDeclaration( + ExpressionStatementSyntax expressionStatement, LocalDeclarationStatementSyntax localDeclaration) + { + // If there wasn't a semicolon before, ensure the trailing trivia of the expression + // becomes the trailing trivia of a new semicolon that we add. + var semicolonToken = expressionStatement.SemicolonToken; + if (expressionStatement.SemicolonToken.IsMissing && localDeclaration is { Declaration.Variables: [{ Initializer.Value: { } value }, ..] }) + { + var expression = expressionStatement.Expression; + localDeclaration = localDeclaration.ReplaceNode(value, expression.WithoutLeadingTrivia()); + semicolonToken = Token(SyntaxKind.SemicolonToken).WithTrailingTrivia(expression.GetTrailingTrivia()); } - protected override LocalDeclarationStatementSyntax FixupLocalDeclaration( - ExpressionStatementSyntax expressionStatement, LocalDeclarationStatementSyntax localDeclaration) - { - // If there wasn't a semicolon before, ensure the trailing trivia of the expression - // becomes the trailing trivia of a new semicolon that we add. - var semicolonToken = expressionStatement.SemicolonToken; - if (expressionStatement.SemicolonToken.IsMissing && localDeclaration is { Declaration.Variables: [{ Initializer.Value: { } value }, ..] }) - { - var expression = expressionStatement.Expression; - localDeclaration = localDeclaration.ReplaceNode(value, expression.WithoutLeadingTrivia()); - semicolonToken = Token(SyntaxKind.SemicolonToken).WithTrailingTrivia(expression.GetTrailingTrivia()); - } + return localDeclaration.WithSemicolonToken(semicolonToken); + } - return localDeclaration.WithSemicolonToken(semicolonToken); + protected override ExpressionStatementSyntax FixupDeconstruction( + ExpressionStatementSyntax expressionStatement, ExpressionStatementSyntax deconstruction) + { + // If there wasn't a semicolon before, ensure the trailing trivia of the expression + // becomes the trailing trivia of a new semicolon that we add. + var semicolonToken = expressionStatement.SemicolonToken; + if (expressionStatement.SemicolonToken.IsMissing && deconstruction is { Expression: AssignmentExpressionSyntax binary }) + { + var expression = expressionStatement.Expression; + deconstruction = deconstruction.ReplaceNode(binary.Right, expression.WithoutLeadingTrivia()); + semicolonToken = Token(SyntaxKind.SemicolonToken).WithTrailingTrivia(expression.GetTrailingTrivia()); } - protected override ExpressionStatementSyntax FixupDeconstruction( - ExpressionStatementSyntax expressionStatement, ExpressionStatementSyntax deconstruction) + return deconstruction.WithSemicolonToken(semicolonToken); + } + + protected override async Task CreateTupleDeconstructionAsync( + Document document, + CodeActionOptionsProvider optionsProvider, + INamedTypeSymbol tupleType, + ExpressionSyntax expression, + CancellationToken cancellationToken) + { + var semanticFacts = document.GetRequiredLanguageService(); + var simplifierOptions = (CSharpSimplifierOptions)await document.GetSimplifierOptionsAsync(optionsProvider, cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var tupleUnderlyingType = tupleType.TupleUnderlyingType ?? tupleType; + + // Generate the names for the locals. For Tuples that have user provided names, keep that name. + // Otherwise, generate a reasonable local name for the type of the field, using our helpers. + var localTypesAndDesignations = tupleType.TupleElements.SelectAsArray((field, index, _) => + { + var name = field.Name.ToCamelCase(); + if (field.Name == tupleUnderlyingType.TupleElements[index].Name) + name = field.Type.GetLocalName(fallback: null) ?? name; + + var uniqueName = semanticFacts.GenerateUniqueLocalName(semanticModel, expression, container: null, name, cancellationToken); + var designation = SingleVariableDesignation(uniqueName); + return (type: field.Type, designation: (VariableDesignationSyntax)designation); + }, arg: /*unused*/false); + + return ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + CreateDeclarationExpression(), + expression)); + + ExpressionSyntax CreateDeclarationExpression() { - // If there wasn't a semicolon before, ensure the trailing trivia of the expression - // becomes the trailing trivia of a new semicolon that we add. - var semicolonToken = expressionStatement.SemicolonToken; - if (expressionStatement.SemicolonToken.IsMissing && deconstruction is { Expression: AssignmentExpressionSyntax binary }) + if (CanUseVar()) { - var expression = expressionStatement.Expression; - deconstruction = deconstruction.ReplaceNode(binary.Right, expression.WithoutLeadingTrivia()); - semicolonToken = Token(SyntaxKind.SemicolonToken).WithTrailingTrivia(expression.GetTrailingTrivia()); + return DeclarationExpression( + IdentifierName("var"), + ParenthesizedVariableDesignation( + [.. localTypesAndDesignations.Select(n => n.designation)])); + } + else + { + // otherwise, emit as `(T1 x, T2 y, T3 z) = ...`. Note, the 'T's will get simplified to 'var' if that matches the user's preference. + return TupleExpression([.. localTypesAndDesignations.Select(t => + Argument(DeclarationExpression(t.type.GenerateTypeSyntax(), t.designation)))]); } - return deconstruction.WithSemicolonToken(semicolonToken); - } - - protected override async Task CreateTupleDeconstructionAsync( - Document document, - CodeActionOptionsProvider optionsProvider, - INamedTypeSymbol tupleType, - ExpressionSyntax expression, - CancellationToken cancellationToken) - { - var semanticFacts = document.GetRequiredLanguageService(); - var simplifierOptions = (CSharpSimplifierOptions)await document.GetSimplifierOptionsAsync(optionsProvider, cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var tupleUnderlyingType = tupleType.TupleUnderlyingType ?? tupleType; - - // Generate the names for the locals. For Tuples that have user provided names, keep that name. - // Otherwise, generate a reasonable local name for the type of the field, using our helpers. - var localTypesAndDesignations = tupleType.TupleElements.SelectAsArray((field, index, _) => - { - var name = field.Name.ToCamelCase(); - if (field.Name == tupleUnderlyingType.TupleElements[index].Name) - name = field.Type.GetLocalName(fallback: null) ?? name; - - var uniqueName = semanticFacts.GenerateUniqueLocalName(semanticModel, expression, container: null, name, cancellationToken); - var designation = SingleVariableDesignation(uniqueName); - return (type: field.Type, designation: (VariableDesignationSyntax)designation); - }, arg: /*unused*/false); - - return ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - CreateDeclarationExpression(), - expression)); - - ExpressionSyntax CreateDeclarationExpression() + bool CanUseVar() { - if (CanUseVar()) - { - return DeclarationExpression( - IdentifierName("var"), - ParenthesizedVariableDesignation( - [.. localTypesAndDesignations.Select(n => n.designation)])); - } - else - { - // otherwise, emit as `(T1 x, T2 y, T3 z) = ...`. Note, the 'T's will get simplified to 'var' if that matches the user's preference. - return TupleExpression([.. localTypesAndDesignations.Select(t => - Argument(DeclarationExpression(t.type.GenerateTypeSyntax(), t.designation)))]); - } - - bool CanUseVar() - { - // check the user's 'var' preference. If it holds for this tuple type and expr, then emit as: - // `var (x, y, z) = ...`. - var varPreference = simplifierOptions.GetUseVarPreference(); - - // If the user likes 'var' for intrinsics, and all the elements would be intrinsic. Then use - var isIntrinsic = tupleType.TupleElements.All(f => f.Type?.SpecialType != SpecialType.None); - if (isIntrinsic) - return varPreference.HasFlag(UseVarPreference.ForBuiltInTypes); - - // now see if the type is apparent using the existing helper. - var isApparent = TypeStyleHelper.IsTypeApparentInAssignmentExpression(varPreference, expression, semanticModel, tupleType, cancellationToken); - if (isApparent) - return varPreference.HasFlag(UseVarPreference.WhenTypeIsApparent); - - // Finally, use 'var' if the user wants that for non-intrinsic, non-apparent cases. - return varPreference.HasFlag(UseVarPreference.Elsewhere); - } + // check the user's 'var' preference. If it holds for this tuple type and expr, then emit as: + // `var (x, y, z) = ...`. + var varPreference = simplifierOptions.GetUseVarPreference(); + + // If the user likes 'var' for intrinsics, and all the elements would be intrinsic. Then use + var isIntrinsic = tupleType.TupleElements.All(f => f.Type?.SpecialType != SpecialType.None); + if (isIntrinsic) + return varPreference.HasFlag(UseVarPreference.ForBuiltInTypes); + + // now see if the type is apparent using the existing helper. + var isApparent = TypeStyleHelper.IsTypeApparentInAssignmentExpression(varPreference, expression, semanticModel, tupleType, cancellationToken); + if (isApparent) + return varPreference.HasFlag(UseVarPreference.WhenTypeIsApparent); + + // Finally, use 'var' if the user wants that for non-intrinsic, non-apparent cases. + return varPreference.HasFlag(UseVarPreference.Elsewhere); } } } diff --git a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService.Rewriter.cs b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService.Rewriter.cs index 7abc7c3e6f682..a01b75fe98374 100644 --- a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService.Rewriter.cs +++ b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService.Rewriter.cs @@ -9,54 +9,53 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Simplification; -namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable +namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable; + +internal partial class CSharpIntroduceVariableService { - internal partial class CSharpIntroduceVariableService + private class Rewriter : CSharpSyntaxRewriter { - private class Rewriter : CSharpSyntaxRewriter + private readonly SyntaxAnnotation _replacementAnnotation = new SyntaxAnnotation(); + private readonly SyntaxNode _replacementNode; + private readonly ISet _matches; + + private Rewriter(SyntaxNode replacementNode, ISet matches) { - private readonly SyntaxAnnotation _replacementAnnotation = new SyntaxAnnotation(); - private readonly SyntaxNode _replacementNode; - private readonly ISet _matches; + _replacementNode = replacementNode; + _matches = matches; + } - private Rewriter(SyntaxNode replacementNode, ISet matches) + public override SyntaxNode Visit(SyntaxNode node) + { + if (node is ExpressionSyntax expression && + _matches.Contains(expression)) { - _replacementNode = replacementNode; - _matches = matches; + return _replacementNode + .WithLeadingTrivia(expression.GetLeadingTrivia()) + .WithTrailingTrivia(expression.GetTrailingTrivia()) + .WithAdditionalAnnotations(_replacementAnnotation); } - public override SyntaxNode Visit(SyntaxNode node) - { - if (node is ExpressionSyntax expression && - _matches.Contains(expression)) - { - return _replacementNode - .WithLeadingTrivia(expression.GetLeadingTrivia()) - .WithTrailingTrivia(expression.GetTrailingTrivia()) - .WithAdditionalAnnotations(_replacementAnnotation); - } - - return base.Visit(node); - } + return base.Visit(node); + } - public override SyntaxNode VisitParenthesizedExpression(ParenthesizedExpressionSyntax node) + public override SyntaxNode VisitParenthesizedExpression(ParenthesizedExpressionSyntax node) + { + var newNode = base.VisitParenthesizedExpression(node); + if (node != newNode && + newNode is ParenthesizedExpressionSyntax parenthesizedExpression) { - var newNode = base.VisitParenthesizedExpression(node); - if (node != newNode && - newNode is ParenthesizedExpressionSyntax parenthesizedExpression) + var innerExpression = parenthesizedExpression.OpenParenToken.GetNextToken().Parent; + if (innerExpression.HasAnnotation(_replacementAnnotation)) { - var innerExpression = parenthesizedExpression.OpenParenToken.GetNextToken().Parent; - if (innerExpression.HasAnnotation(_replacementAnnotation)) - { - return newNode.WithAdditionalAnnotations(Simplifier.Annotation); - } + return newNode.WithAdditionalAnnotations(Simplifier.Annotation); } - - return newNode; } - public static SyntaxNode Visit(SyntaxNode node, SyntaxNode replacementNode, ISet matches) - => new Rewriter(replacementNode, matches).Visit(node); + return newNode; } + + public static SyntaxNode Visit(SyntaxNode node, SyntaxNode replacementNode, ISet matches) + => new Rewriter(replacementNode, matches).Visit(node); } } diff --git a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService.cs b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService.cs index 35b9b9e1ec11a..f7f6d6944d909 100644 --- a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService.cs +++ b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService.cs @@ -16,142 +16,141 @@ using Microsoft.CodeAnalysis.IntroduceVariable; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable +namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable; + +[ExportLanguageService(typeof(IIntroduceVariableService), LanguageNames.CSharp), Shared] +internal partial class CSharpIntroduceVariableService : + AbstractIntroduceVariableService { - [ExportLanguageService(typeof(IIntroduceVariableService), LanguageNames.CSharp), Shared] - internal partial class CSharpIntroduceVariableService : - AbstractIntroduceVariableService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpIntroduceVariableService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpIntroduceVariableService() - { - } + } - protected override bool IsInNonFirstQueryClause(ExpressionSyntax expression) + protected override bool IsInNonFirstQueryClause(ExpressionSyntax expression) + { + var query = expression.GetAncestor(); + if (query != null) { - var query = expression.GetAncestor(); - if (query != null) + // Can't introduce for the first clause in a query. + var fromClause = expression.GetAncestor(); + if (fromClause == null || query.FromClause != fromClause) { - // Can't introduce for the first clause in a query. - var fromClause = expression.GetAncestor(); - if (fromClause == null || query.FromClause != fromClause) - { - return true; - } + return true; } - - return false; } - protected override bool IsInFieldInitializer(ExpressionSyntax expression) - { - return expression.GetAncestorOrThis() - .GetAncestorOrThis() != null; - } + return false; + } + + protected override bool IsInFieldInitializer(ExpressionSyntax expression) + { + return expression.GetAncestorOrThis() + .GetAncestorOrThis() != null; + } - protected override bool IsInParameterInitializer(ExpressionSyntax expression) - => expression.GetAncestorOrThis().IsParentKind(SyntaxKind.Parameter); + protected override bool IsInParameterInitializer(ExpressionSyntax expression) + => expression.GetAncestorOrThis().IsParentKind(SyntaxKind.Parameter); - protected override bool IsInConstructorInitializer(ExpressionSyntax expression) - => expression.GetAncestorOrThis() != null; + protected override bool IsInConstructorInitializer(ExpressionSyntax expression) + => expression.GetAncestorOrThis() != null; - protected override bool IsInAutoPropertyInitializer(ExpressionSyntax expression) - => expression.GetAncestorOrThis().IsParentKind(SyntaxKind.PropertyDeclaration); + protected override bool IsInAutoPropertyInitializer(ExpressionSyntax expression) + => expression.GetAncestorOrThis().IsParentKind(SyntaxKind.PropertyDeclaration); - protected override bool IsInExpressionBodiedMember(ExpressionSyntax expression) + protected override bool IsInExpressionBodiedMember(ExpressionSyntax expression) + { + // walk up until we find a nearest enclosing block or arrow expression. + for (SyntaxNode node = expression; node != null; node = node.Parent) { - // walk up until we find a nearest enclosing block or arrow expression. - for (SyntaxNode node = expression; node != null; node = node.Parent) + // If we are in an expression bodied member and if the expression has a block body, then, + // act as if we're in a block context and not in an expression body context at all. + if (node.IsKind(SyntaxKind.Block)) { - // If we are in an expression bodied member and if the expression has a block body, then, - // act as if we're in a block context and not in an expression body context at all. - if (node.IsKind(SyntaxKind.Block)) - { - return false; - } - else if (node.IsKind(SyntaxKind.ArrowExpressionClause)) - { - return true; - } + return false; + } + else if (node.IsKind(SyntaxKind.ArrowExpressionClause)) + { + return true; } - - return false; } - protected override bool IsInAttributeArgumentInitializer(ExpressionSyntax expression) + return false; + } + + protected override bool IsInAttributeArgumentInitializer(ExpressionSyntax expression) + { + // Don't call the base here. We want to let the user extract a constant if they've + // said "Goo(a = 10)" + var attributeArgument = expression.GetAncestorOrThis(); + if (attributeArgument != null) { - // Don't call the base here. We want to let the user extract a constant if they've - // said "Goo(a = 10)" - var attributeArgument = expression.GetAncestorOrThis(); - if (attributeArgument != null) + // Can't extract an attribute initializer if it contains an array initializer of any + // sort. Also, we can't extract if there's any typeof expression within it. + if (!expression.DepthFirstTraversal().Any(n => n.RawKind == (int)SyntaxKind.ArrayCreationExpression) && + !expression.DepthFirstTraversal().Any(n => n.RawKind == (int)SyntaxKind.TypeOfExpression)) { - // Can't extract an attribute initializer if it contains an array initializer of any - // sort. Also, we can't extract if there's any typeof expression within it. - if (!expression.DepthFirstTraversal().Any(n => n.RawKind == (int)SyntaxKind.ArrayCreationExpression) && - !expression.DepthFirstTraversal().Any(n => n.RawKind == (int)SyntaxKind.TypeOfExpression)) - { - var attributeDecl = attributeArgument.GetAncestorOrThis(); + var attributeDecl = attributeArgument.GetAncestorOrThis(); - // Also can't extract an attribute initializer if the attribute is a global one. - if (!attributeDecl.IsParentKind(SyntaxKind.CompilationUnit)) - { - return true; - } + // Also can't extract an attribute initializer if the attribute is a global one. + if (!attributeDecl.IsParentKind(SyntaxKind.CompilationUnit)) + { + return true; } } + } + + return false; + } + /// + /// Checks for conditions where we should not generate a variable for an expression + /// + protected override bool CanIntroduceVariableFor(ExpressionSyntax expression) + { + // (a) If that's the only expression in a statement. + // Otherwise we'll end up with something like "v;" which is not legal in C#. + if (expression.WalkUpParentheses().IsParentKind(SyntaxKind.ExpressionStatement)) + { return false; } - /// - /// Checks for conditions where we should not generate a variable for an expression - /// - protected override bool CanIntroduceVariableFor(ExpressionSyntax expression) + // (b) For Null Literals, as AllOccurrences could introduce semantic errors. + if (expression.IsKind(SyntaxKind.NullLiteralExpression)) { - // (a) If that's the only expression in a statement. - // Otherwise we'll end up with something like "v;" which is not legal in C#. - if (expression.WalkUpParentheses().IsParentKind(SyntaxKind.ExpressionStatement)) - { - return false; - } - - // (b) For Null Literals, as AllOccurrences could introduce semantic errors. - if (expression.IsKind(SyntaxKind.NullLiteralExpression)) - { - return false; - } - - // (c) For throw expressions. - if (expression.IsKind(SyntaxKind.ThrowExpression)) - { - return false; - } + return false; + } - return true; + // (c) For throw expressions. + if (expression.IsKind(SyntaxKind.ThrowExpression)) + { + return false; } - protected override IEnumerable GetContainingExecutableBlocks(ExpressionSyntax expression) - => expression.GetAncestorsOrThis(); + return true; + } - protected override IList GetInsertionIndices(TypeDeclarationSyntax destination, CancellationToken cancellationToken) - => destination.GetInsertionIndices(cancellationToken); + protected override IEnumerable GetContainingExecutableBlocks(ExpressionSyntax expression) + => expression.GetAncestorsOrThis(); - protected override bool CanReplace(ExpressionSyntax expression) - => true; + protected override IList GetInsertionIndices(TypeDeclarationSyntax destination, CancellationToken cancellationToken) + => destination.GetInsertionIndices(cancellationToken); - protected override bool IsExpressionInStaticLocalFunction(ExpressionSyntax expression) - { - var localFunction = expression.GetAncestor(); - return localFunction != null && localFunction.Modifiers.Any(SyntaxKind.StaticKeyword); - } + protected override bool CanReplace(ExpressionSyntax expression) + => true; - protected override TNode RewriteCore( - TNode node, - SyntaxNode replacementNode, - ISet matches) - { - return (TNode)Rewriter.Visit(node, replacementNode, matches); - } + protected override bool IsExpressionInStaticLocalFunction(ExpressionSyntax expression) + { + var localFunction = expression.GetAncestor(); + return localFunction != null && localFunction.Modifiers.Any(SyntaxKind.StaticKeyword); + } + + protected override TNode RewriteCore( + TNode node, + SyntaxNode replacementNode, + ISet matches) + { + return (TNode)Rewriter.Visit(node, replacementNode, matches); } } diff --git a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceField.cs b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceField.cs index a41fce2706556..16794661e4dda 100644 --- a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceField.cs +++ b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceField.cs @@ -14,185 +14,184 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Simplification; -namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable +namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable; + +internal partial class CSharpIntroduceVariableService { - internal partial class CSharpIntroduceVariableService + protected override Task IntroduceFieldAsync( + SemanticDocument document, + ExpressionSyntax expression, + bool allOccurrences, + bool isConstant, + CancellationToken cancellationToken) { - protected override Task IntroduceFieldAsync( - SemanticDocument document, - ExpressionSyntax expression, - bool allOccurrences, - bool isConstant, - CancellationToken cancellationToken) + var oldTypeDeclaration = expression.GetAncestorOrThis(); + + var oldType = oldTypeDeclaration != null + ? document.SemanticModel.GetDeclaredSymbol(oldTypeDeclaration, cancellationToken) + : document.SemanticModel.Compilation.ScriptClass; + var newNameToken = GenerateUniqueFieldName(document, expression, isConstant, cancellationToken); + var typeDisplayString = oldType.ToMinimalDisplayString(document.SemanticModel, expression.SpanStart); + + var newQualifiedName = oldTypeDeclaration != null + ? SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.ParseName(typeDisplayString), SyntaxFactory.IdentifierName(newNameToken)) + : (ExpressionSyntax)SyntaxFactory.IdentifierName(newNameToken); + + newQualifiedName = newQualifiedName.WithAdditionalAnnotations(Simplifier.Annotation); + + var newFieldDeclaration = SyntaxFactory.FieldDeclaration( + default, + MakeFieldModifiers(isConstant, inScript: oldType.IsScriptClass), + SyntaxFactory.VariableDeclaration( + GetTypeSymbol(document, expression, cancellationToken).GenerateTypeSyntax(), + [SyntaxFactory.VariableDeclarator( + newNameToken.WithAdditionalAnnotations(RenameAnnotation.Create()), + null, + SyntaxFactory.EqualsValueClause(expression.WithoutTrivia()))])).WithAdditionalAnnotations(Formatter.Annotation); + + if (oldTypeDeclaration != null) { - var oldTypeDeclaration = expression.GetAncestorOrThis(); - - var oldType = oldTypeDeclaration != null - ? document.SemanticModel.GetDeclaredSymbol(oldTypeDeclaration, cancellationToken) - : document.SemanticModel.Compilation.ScriptClass; - var newNameToken = GenerateUniqueFieldName(document, expression, isConstant, cancellationToken); - var typeDisplayString = oldType.ToMinimalDisplayString(document.SemanticModel, expression.SpanStart); - - var newQualifiedName = oldTypeDeclaration != null - ? SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.ParseName(typeDisplayString), SyntaxFactory.IdentifierName(newNameToken)) - : (ExpressionSyntax)SyntaxFactory.IdentifierName(newNameToken); - - newQualifiedName = newQualifiedName.WithAdditionalAnnotations(Simplifier.Annotation); - - var newFieldDeclaration = SyntaxFactory.FieldDeclaration( - default, - MakeFieldModifiers(isConstant, inScript: oldType.IsScriptClass), - SyntaxFactory.VariableDeclaration( - GetTypeSymbol(document, expression, cancellationToken).GenerateTypeSyntax(), - [SyntaxFactory.VariableDeclarator( - newNameToken.WithAdditionalAnnotations(RenameAnnotation.Create()), - null, - SyntaxFactory.EqualsValueClause(expression.WithoutTrivia()))])).WithAdditionalAnnotations(Formatter.Annotation); - - if (oldTypeDeclaration != null) - { - var newTypeDeclaration = Rewrite( - document, expression, newQualifiedName, document, oldTypeDeclaration, allOccurrences, cancellationToken); + var newTypeDeclaration = Rewrite( + document, expression, newQualifiedName, document, oldTypeDeclaration, allOccurrences, cancellationToken); - var insertionIndex = GetFieldInsertionIndex(isConstant, oldTypeDeclaration, newTypeDeclaration, cancellationToken); - var finalTypeDeclaration = InsertMember(newTypeDeclaration, newFieldDeclaration, insertionIndex); + var insertionIndex = GetFieldInsertionIndex(isConstant, oldTypeDeclaration, newTypeDeclaration, cancellationToken); + var finalTypeDeclaration = InsertMember(newTypeDeclaration, newFieldDeclaration, insertionIndex); - var newRoot = document.Root.ReplaceNode(oldTypeDeclaration, finalTypeDeclaration); - return Task.FromResult(document.Document.WithSyntaxRoot(newRoot)); - } - else - { - var oldCompilationUnit = (CompilationUnitSyntax)document.Root; - var newCompilationUnit = Rewrite( - document, expression, newQualifiedName, document, oldCompilationUnit, allOccurrences, cancellationToken); + var newRoot = document.Root.ReplaceNode(oldTypeDeclaration, finalTypeDeclaration); + return Task.FromResult(document.Document.WithSyntaxRoot(newRoot)); + } + else + { + var oldCompilationUnit = (CompilationUnitSyntax)document.Root; + var newCompilationUnit = Rewrite( + document, expression, newQualifiedName, document, oldCompilationUnit, allOccurrences, cancellationToken); - var insertionIndex = isConstant - ? DetermineConstantInsertPosition(oldCompilationUnit.Members, newCompilationUnit.Members) - : DetermineFieldInsertPosition(oldCompilationUnit.Members, newCompilationUnit.Members); + var insertionIndex = isConstant + ? DetermineConstantInsertPosition(oldCompilationUnit.Members, newCompilationUnit.Members) + : DetermineFieldInsertPosition(oldCompilationUnit.Members, newCompilationUnit.Members); - var newRoot = newCompilationUnit.WithMembers(newCompilationUnit.Members.Insert(insertionIndex, newFieldDeclaration)); - return Task.FromResult(document.Document.WithSyntaxRoot(newRoot)); - } + var newRoot = newCompilationUnit.WithMembers(newCompilationUnit.Members.Insert(insertionIndex, newFieldDeclaration)); + return Task.FromResult(document.Document.WithSyntaxRoot(newRoot)); } + } - protected override int DetermineConstantInsertPosition(TypeDeclarationSyntax oldType, TypeDeclarationSyntax newType) - => DetermineConstantInsertPosition(oldType.Members, newType.Members); + protected override int DetermineConstantInsertPosition(TypeDeclarationSyntax oldType, TypeDeclarationSyntax newType) + => DetermineConstantInsertPosition(oldType.Members, newType.Members); - protected static int DetermineConstantInsertPosition( - SyntaxList oldMembers, - SyntaxList newMembers) + protected static int DetermineConstantInsertPosition( + SyntaxList oldMembers, + SyntaxList newMembers) + { + // 1) Place the constant after the last constant. + // + // 2) If there is no constant, place it before the first field + // + // 3) If the first change is before either of those, then place before the first + // change + // + // 4) Otherwise, place it at the start. + var index = 0; + var lastConstantIndex = oldMembers.LastIndexOf(IsConstantField); + + if (lastConstantIndex >= 0) { - // 1) Place the constant after the last constant. - // - // 2) If there is no constant, place it before the first field - // - // 3) If the first change is before either of those, then place before the first - // change - // - // 4) Otherwise, place it at the start. - var index = 0; - var lastConstantIndex = oldMembers.LastIndexOf(IsConstantField); - - if (lastConstantIndex >= 0) - { - index = lastConstantIndex + 1; - } - else - { - var firstFieldIndex = oldMembers.IndexOf(member => member is FieldDeclarationSyntax); - if (firstFieldIndex >= 0) - { - index = firstFieldIndex; - } - } - - var firstChangeIndex = DetermineFirstChange(oldMembers, newMembers); - if (firstChangeIndex >= 0) + index = lastConstantIndex + 1; + } + else + { + var firstFieldIndex = oldMembers.IndexOf(member => member is FieldDeclarationSyntax); + if (firstFieldIndex >= 0) { - index = Math.Min(index, firstChangeIndex); + index = firstFieldIndex; } + } - return index; + var firstChangeIndex = DetermineFirstChange(oldMembers, newMembers); + if (firstChangeIndex >= 0) + { + index = Math.Min(index, firstChangeIndex); } - protected override int DetermineFieldInsertPosition(TypeDeclarationSyntax oldType, TypeDeclarationSyntax newType) - => DetermineFieldInsertPosition(oldType.Members, newType.Members); + return index; + } - protected static int DetermineFieldInsertPosition( - SyntaxList oldMembers, - SyntaxList newMembers) - { - // 1) Place the constant after the last field. - // - // 2) If there is no field, place it after the last constant - // - // 3) If the first change is before either of those, then place before the first - // change - // - // 4) Otherwise, place it at the start. - var index = 0; - var lastFieldIndex = oldMembers.LastIndexOf(member => member is FieldDeclarationSyntax); - if (lastFieldIndex >= 0) - { - index = lastFieldIndex + 1; - } - else - { - var lastConstantIndex = oldMembers.LastIndexOf(IsConstantField); - if (lastConstantIndex >= 0) - { - index = lastConstantIndex + 1; - } - } + protected override int DetermineFieldInsertPosition(TypeDeclarationSyntax oldType, TypeDeclarationSyntax newType) + => DetermineFieldInsertPosition(oldType.Members, newType.Members); - var firstChangeIndex = DetermineFirstChange(oldMembers, newMembers); - if (firstChangeIndex >= 0) + protected static int DetermineFieldInsertPosition( + SyntaxList oldMembers, + SyntaxList newMembers) + { + // 1) Place the constant after the last field. + // + // 2) If there is no field, place it after the last constant + // + // 3) If the first change is before either of those, then place before the first + // change + // + // 4) Otherwise, place it at the start. + var index = 0; + var lastFieldIndex = oldMembers.LastIndexOf(member => member is FieldDeclarationSyntax); + if (lastFieldIndex >= 0) + { + index = lastFieldIndex + 1; + } + else + { + var lastConstantIndex = oldMembers.LastIndexOf(IsConstantField); + if (lastConstantIndex >= 0) { - index = Math.Min(index, firstChangeIndex); + index = lastConstantIndex + 1; } + } - return index; + var firstChangeIndex = DetermineFirstChange(oldMembers, newMembers); + if (firstChangeIndex >= 0) + { + index = Math.Min(index, firstChangeIndex); } - private static bool IsConstantField(MemberDeclarationSyntax member) - => member is FieldDeclarationSyntax field && field.Modifiers.Any(SyntaxKind.ConstKeyword); + return index; + } + + private static bool IsConstantField(MemberDeclarationSyntax member) + => member is FieldDeclarationSyntax field && field.Modifiers.Any(SyntaxKind.ConstKeyword); - protected static int DetermineFirstChange(SyntaxList oldMembers, SyntaxList newMembers) + protected static int DetermineFirstChange(SyntaxList oldMembers, SyntaxList newMembers) + { + for (var i = 0; i < oldMembers.Count; i++) { - for (var i = 0; i < oldMembers.Count; i++) + if (!SyntaxFactory.AreEquivalent(oldMembers[i], newMembers[i], topLevel: false)) { - if (!SyntaxFactory.AreEquivalent(oldMembers[i], newMembers[i], topLevel: false)) - { - return i; - } + return i; } - - return -1; } - protected static TypeDeclarationSyntax InsertMember( - TypeDeclarationSyntax typeDeclaration, - MemberDeclarationSyntax memberDeclaration, - int index) + return -1; + } + + protected static TypeDeclarationSyntax InsertMember( + TypeDeclarationSyntax typeDeclaration, + MemberDeclarationSyntax memberDeclaration, + int index) + { + return typeDeclaration.WithMembers( + typeDeclaration.Members.Insert(index, memberDeclaration)); + } + + private static SyntaxTokenList MakeFieldModifiers(bool isConstant, bool inScript) + { + if (isConstant) { - return typeDeclaration.WithMembers( - typeDeclaration.Members.Insert(index, memberDeclaration)); + return [SyntaxFactory.Token(SyntaxKind.PrivateKeyword), SyntaxFactory.Token(SyntaxKind.ConstKeyword)]; } - - private static SyntaxTokenList MakeFieldModifiers(bool isConstant, bool inScript) + else if (inScript) { - if (isConstant) - { - return [SyntaxFactory.Token(SyntaxKind.PrivateKeyword), SyntaxFactory.Token(SyntaxKind.ConstKeyword)]; - } - else if (inScript) - { - return [SyntaxFactory.Token(SyntaxKind.PrivateKeyword), SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)]; - } - else - { - return [SyntaxFactory.Token(SyntaxKind.PrivateKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)]; - } + return [SyntaxFactory.Token(SyntaxKind.PrivateKeyword), SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)]; + } + else + { + return [SyntaxFactory.Token(SyntaxKind.PrivateKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)]; } } } diff --git a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceLocal.cs b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceLocal.cs index a7e61ea5a3bd6..ba1b5efdbcf11 100644 --- a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceLocal.cs +++ b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceLocal.cs @@ -18,443 +18,442 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable +namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable; + +internal partial class CSharpIntroduceVariableService { - internal partial class CSharpIntroduceVariableService + protected override async Task IntroduceLocalAsync( + SemanticDocument document, + ExpressionSyntax expression, + bool allOccurrences, + bool isConstant, + CancellationToken cancellationToken) { - protected override async Task IntroduceLocalAsync( - SemanticDocument document, - ExpressionSyntax expression, - bool allOccurrences, - bool isConstant, - CancellationToken cancellationToken) + var containerToGenerateInto = expression.Ancestors().FirstOrDefault(s => + s is BlockSyntax or ArrowExpressionClauseSyntax or LambdaExpressionSyntax); + + var newLocalNameToken = GenerateUniqueLocalName( + document, expression, isConstant, containerToGenerateInto, cancellationToken); + var newLocalName = SyntaxFactory.IdentifierName(newLocalNameToken); + + var modifiers = isConstant + ? SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ConstKeyword)) + : default; + + var declarationStatement = SyntaxFactory.LocalDeclarationStatement( + modifiers, + SyntaxFactory.VariableDeclaration( + GetTypeSyntax(document, expression, cancellationToken), + [SyntaxFactory.VariableDeclarator( + newLocalNameToken.WithAdditionalAnnotations(RenameAnnotation.Create()), + null, + SyntaxFactory.EqualsValueClause(expression.WithoutTrivia()))])); + + // If we're inserting into a multi-line parent, then add a newline after the local-var + // we're adding. That way we don't end up having it and the starting statement be on + // the same line (which will cause indentation to be computed incorrectly). + var text = await document.Document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (!text.AreOnSameLine(containerToGenerateInto.GetFirstToken(), containerToGenerateInto.GetLastToken())) { - var containerToGenerateInto = expression.Ancestors().FirstOrDefault(s => - s is BlockSyntax or ArrowExpressionClauseSyntax or LambdaExpressionSyntax); - - var newLocalNameToken = GenerateUniqueLocalName( - document, expression, isConstant, containerToGenerateInto, cancellationToken); - var newLocalName = SyntaxFactory.IdentifierName(newLocalNameToken); - - var modifiers = isConstant - ? SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ConstKeyword)) - : default; - - var declarationStatement = SyntaxFactory.LocalDeclarationStatement( - modifiers, - SyntaxFactory.VariableDeclaration( - GetTypeSyntax(document, expression, cancellationToken), - [SyntaxFactory.VariableDeclarator( - newLocalNameToken.WithAdditionalAnnotations(RenameAnnotation.Create()), - null, - SyntaxFactory.EqualsValueClause(expression.WithoutTrivia()))])); - - // If we're inserting into a multi-line parent, then add a newline after the local-var - // we're adding. That way we don't end up having it and the starting statement be on - // the same line (which will cause indentation to be computed incorrectly). - var text = await document.Document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (!text.AreOnSameLine(containerToGenerateInto.GetFirstToken(), containerToGenerateInto.GetLastToken())) - { - declarationStatement = declarationStatement.WithAppendedTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); - } - - switch (containerToGenerateInto) - { - case BlockSyntax block: - return await IntroduceLocalDeclarationIntoBlockAsync( - document, block, expression, newLocalName, declarationStatement, allOccurrences, cancellationToken).ConfigureAwait(false); - - case ArrowExpressionClauseSyntax arrowExpression: - // this will be null for expression-bodied properties & indexer (not for individual getters & setters, those do have a symbol), - // both of which are a shorthand for the getter and always return a value - var method = document.SemanticModel.GetDeclaredSymbol(arrowExpression.Parent, cancellationToken) as IMethodSymbol; - var createReturnStatement = true; - - if (method is not null) - createReturnStatement = !method.ReturnsVoid && !method.IsAsyncReturningVoidTask(document.SemanticModel.Compilation); - - return RewriteExpressionBodiedMemberAndIntroduceLocalDeclaration( - document, arrowExpression, expression, newLocalName, - declarationStatement, allOccurrences, createReturnStatement, cancellationToken); - - case LambdaExpressionSyntax lambda: - return IntroduceLocalDeclarationIntoLambda( - document, lambda, expression, newLocalName, declarationStatement, - allOccurrences, cancellationToken); - } - - throw new InvalidOperationException(); + declarationStatement = declarationStatement.WithAppendedTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); } - private Document IntroduceLocalDeclarationIntoLambda( - SemanticDocument document, - LambdaExpressionSyntax oldLambda, - ExpressionSyntax expression, - IdentifierNameSyntax newLocalName, - LocalDeclarationStatementSyntax declarationStatement, - bool allOccurrences, - CancellationToken cancellationToken) + switch (containerToGenerateInto) { - var oldBody = (ExpressionSyntax)oldLambda.Body; - var isEntireLambdaBodySelected = oldBody.Equals(expression.WalkUpParentheses()); + case BlockSyntax block: + return await IntroduceLocalDeclarationIntoBlockAsync( + document, block, expression, newLocalName, declarationStatement, allOccurrences, cancellationToken).ConfigureAwait(false); + + case ArrowExpressionClauseSyntax arrowExpression: + // this will be null for expression-bodied properties & indexer (not for individual getters & setters, those do have a symbol), + // both of which are a shorthand for the getter and always return a value + var method = document.SemanticModel.GetDeclaredSymbol(arrowExpression.Parent, cancellationToken) as IMethodSymbol; + var createReturnStatement = true; + + if (method is not null) + createReturnStatement = !method.ReturnsVoid && !method.IsAsyncReturningVoidTask(document.SemanticModel.Compilation); + + return RewriteExpressionBodiedMemberAndIntroduceLocalDeclaration( + document, arrowExpression, expression, newLocalName, + declarationStatement, allOccurrences, createReturnStatement, cancellationToken); + + case LambdaExpressionSyntax lambda: + return IntroduceLocalDeclarationIntoLambda( + document, lambda, expression, newLocalName, declarationStatement, + allOccurrences, cancellationToken); + } - var rewrittenBody = Rewrite( - document, expression, newLocalName, document, oldBody, allOccurrences, cancellationToken); + throw new InvalidOperationException(); + } - var shouldIncludeReturnStatement = ShouldIncludeReturnStatement(document, oldLambda, cancellationToken); - var newBody = GetNewBlockBodyForLambda( - declarationStatement, isEntireLambdaBodySelected, rewrittenBody, shouldIncludeReturnStatement); + private Document IntroduceLocalDeclarationIntoLambda( + SemanticDocument document, + LambdaExpressionSyntax oldLambda, + ExpressionSyntax expression, + IdentifierNameSyntax newLocalName, + LocalDeclarationStatementSyntax declarationStatement, + bool allOccurrences, + CancellationToken cancellationToken) + { + var oldBody = (ExpressionSyntax)oldLambda.Body; + var isEntireLambdaBodySelected = oldBody.Equals(expression.WalkUpParentheses()); - // Add an elastic newline so that the formatter will place this new lambda body across multiple lines. - newBody = newBody.WithOpenBraceToken(newBody.OpenBraceToken.WithAppendedTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed)) - .WithAdditionalAnnotations(Formatter.Annotation); + var rewrittenBody = Rewrite( + document, expression, newLocalName, document, oldBody, allOccurrences, cancellationToken); - var newLambda = oldLambda.WithBody(newBody); + var shouldIncludeReturnStatement = ShouldIncludeReturnStatement(document, oldLambda, cancellationToken); + var newBody = GetNewBlockBodyForLambda( + declarationStatement, isEntireLambdaBodySelected, rewrittenBody, shouldIncludeReturnStatement); - var newRoot = document.Root.ReplaceNode(oldLambda, newLambda); - return document.Document.WithSyntaxRoot(newRoot); - } + // Add an elastic newline so that the formatter will place this new lambda body across multiple lines. + newBody = newBody.WithOpenBraceToken(newBody.OpenBraceToken.WithAppendedTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed)) + .WithAdditionalAnnotations(Formatter.Annotation); - private static bool ShouldIncludeReturnStatement( - SemanticDocument document, - LambdaExpressionSyntax oldLambda, - CancellationToken cancellationToken) - { - if (document.SemanticModel.GetTypeInfo(oldLambda, cancellationToken).ConvertedType is INamedTypeSymbol delegateType && - delegateType.DelegateInvokeMethod != null) - { - if (delegateType.DelegateInvokeMethod.ReturnsVoid) - { - return false; - } + var newLambda = oldLambda.WithBody(newBody); - // Async lambdas with a Task or ValueTask return type don't need a return statement. - // e.g.: - // Func f = async x => await M2(); - // - // After refactoring: - // Func f = async x => - // { - // Task task = M2(); - // await task; - // }; - var compilation = document.SemanticModel.Compilation; - var delegateReturnType = delegateType.DelegateInvokeMethod.ReturnType; - if (oldLambda.AsyncKeyword != default && delegateReturnType != null) - { - if ((compilation.TaskType() != null && delegateReturnType.Equals(compilation.TaskType())) || - (compilation.ValueTaskType() != null && delegateReturnType.Equals(compilation.ValueTaskType()))) - { - return false; - } - } - } - - return true; - } + var newRoot = document.Root.ReplaceNode(oldLambda, newLambda); + return document.Document.WithSyntaxRoot(newRoot); + } - private static BlockSyntax GetNewBlockBodyForLambda( - LocalDeclarationStatementSyntax declarationStatement, - bool isEntireLambdaBodySelected, - ExpressionSyntax rewrittenBody, - bool includeReturnStatement) + private static bool ShouldIncludeReturnStatement( + SemanticDocument document, + LambdaExpressionSyntax oldLambda, + CancellationToken cancellationToken) + { + if (document.SemanticModel.GetTypeInfo(oldLambda, cancellationToken).ConvertedType is INamedTypeSymbol delegateType && + delegateType.DelegateInvokeMethod != null) { - if (includeReturnStatement) + if (delegateType.DelegateInvokeMethod.ReturnsVoid) { - // Case 1: The lambda has a non-void return type. - // e.g.: - // Func f = x => [|x + 1|]; - // - // After refactoring: - // Func f = x => - // { - // var v = x + 1; - // return v; - // }; - return SyntaxFactory.Block(declarationStatement, SyntaxFactory.ReturnStatement(rewrittenBody)); + return false; } - // For lambdas with void return types, we don't need to include the rewritten body if the entire lambda body - // was originally selected for refactoring, as the rewritten body should already be encompassed within the - // declaration statement. - if (isEntireLambdaBodySelected) + // Async lambdas with a Task or ValueTask return type don't need a return statement. + // e.g.: + // Func f = async x => await M2(); + // + // After refactoring: + // Func f = async x => + // { + // Task task = M2(); + // await task; + // }; + var compilation = document.SemanticModel.Compilation; + var delegateReturnType = delegateType.DelegateInvokeMethod.ReturnType; + if (oldLambda.AsyncKeyword != default && delegateReturnType != null) { - // Case 2a: The lambda has a void return type, and the user selects the entire lambda body. - // e.g.: - // Action goo = x => [|x.ToString()|]; - // - // After refactoring: - // Action goo = x => - // { - // string v = x.ToString(); - // }; - return SyntaxFactory.Block(declarationStatement); + if ((compilation.TaskType() != null && delegateReturnType.Equals(compilation.TaskType())) || + (compilation.ValueTaskType() != null && delegateReturnType.Equals(compilation.ValueTaskType()))) + { + return false; + } } + } - // Case 2b: The lambda has a void return type, and the user didn't select the entire lambda body. + return true; + } + + private static BlockSyntax GetNewBlockBodyForLambda( + LocalDeclarationStatementSyntax declarationStatement, + bool isEntireLambdaBodySelected, + ExpressionSyntax rewrittenBody, + bool includeReturnStatement) + { + if (includeReturnStatement) + { + // Case 1: The lambda has a non-void return type. // e.g.: - // Task.Run(() => File.Copy("src", [|Path.Combine("dir", "file")|])); + // Func f = x => [|x + 1|]; // // After refactoring: - // Task.Run(() => + // Func f = x => // { - // string destFileName = Path.Combine("dir", "file"); - // File.Copy("src", destFileName); - // }); - return SyntaxFactory.Block( - declarationStatement, - SyntaxFactory.ExpressionStatement(rewrittenBody, SyntaxFactory.Token(SyntaxKind.SemicolonToken))); + // var v = x + 1; + // return v; + // }; + return SyntaxFactory.Block(declarationStatement, SyntaxFactory.ReturnStatement(rewrittenBody)); } - private static TypeSyntax GetTypeSyntax(SemanticDocument document, ExpressionSyntax expression, CancellationToken cancellationToken) + // For lambdas with void return types, we don't need to include the rewritten body if the entire lambda body + // was originally selected for refactoring, as the rewritten body should already be encompassed within the + // declaration statement. + if (isEntireLambdaBodySelected) { - var typeSymbol = GetTypeSymbol(document, expression, cancellationToken); - return typeSymbol.GenerateTypeSyntax(); + // Case 2a: The lambda has a void return type, and the user selects the entire lambda body. + // e.g.: + // Action goo = x => [|x.ToString()|]; + // + // After refactoring: + // Action goo = x => + // { + // string v = x.ToString(); + // }; + return SyntaxFactory.Block(declarationStatement); } - private Document RewriteExpressionBodiedMemberAndIntroduceLocalDeclaration( - SemanticDocument document, - ArrowExpressionClauseSyntax arrowExpression, - ExpressionSyntax expression, - NameSyntax newLocalName, - LocalDeclarationStatementSyntax declarationStatement, - bool allOccurrences, - bool createReturnStatement, - CancellationToken cancellationToken) - { - var oldBody = arrowExpression; - var oldParentingNode = oldBody.Parent; - var leadingTrivia = oldBody.GetLeadingTrivia() - .AddRange(oldBody.ArrowToken.TrailingTrivia); - - var newExpression = Rewrite(document, expression, newLocalName, document, oldBody.Expression, allOccurrences, cancellationToken); + // Case 2b: The lambda has a void return type, and the user didn't select the entire lambda body. + // e.g.: + // Task.Run(() => File.Copy("src", [|Path.Combine("dir", "file")|])); + // + // After refactoring: + // Task.Run(() => + // { + // string destFileName = Path.Combine("dir", "file"); + // File.Copy("src", destFileName); + // }); + return SyntaxFactory.Block( + declarationStatement, + SyntaxFactory.ExpressionStatement(rewrittenBody, SyntaxFactory.Token(SyntaxKind.SemicolonToken))); + } - var convertedStatement = createReturnStatement - ? SyntaxFactory.ReturnStatement(newExpression) - : (StatementSyntax)SyntaxFactory.ExpressionStatement(newExpression); + private static TypeSyntax GetTypeSyntax(SemanticDocument document, ExpressionSyntax expression, CancellationToken cancellationToken) + { + var typeSymbol = GetTypeSymbol(document, expression, cancellationToken); + return typeSymbol.GenerateTypeSyntax(); + } - var newBody = SyntaxFactory.Block(declarationStatement, convertedStatement) - .WithLeadingTrivia(leadingTrivia) - .WithTrailingTrivia(oldBody.GetTrailingTrivia()); + private Document RewriteExpressionBodiedMemberAndIntroduceLocalDeclaration( + SemanticDocument document, + ArrowExpressionClauseSyntax arrowExpression, + ExpressionSyntax expression, + NameSyntax newLocalName, + LocalDeclarationStatementSyntax declarationStatement, + bool allOccurrences, + bool createReturnStatement, + CancellationToken cancellationToken) + { + var oldBody = arrowExpression; + var oldParentingNode = oldBody.Parent; + var leadingTrivia = oldBody.GetLeadingTrivia() + .AddRange(oldBody.ArrowToken.TrailingTrivia); - // Add an elastic newline so that the formatter will place this new block across multiple lines. - newBody = newBody.WithOpenBraceToken(newBody.OpenBraceToken.WithAppendedTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed)) - .WithAdditionalAnnotations(Formatter.Annotation); + var newExpression = Rewrite(document, expression, newLocalName, document, oldBody.Expression, allOccurrences, cancellationToken); - var newRoot = document.Root.ReplaceNode(oldParentingNode, WithBlockBody(oldParentingNode, newBody)); - return document.Document.WithSyntaxRoot(newRoot); - } + var convertedStatement = createReturnStatement + ? SyntaxFactory.ReturnStatement(newExpression) + : (StatementSyntax)SyntaxFactory.ExpressionStatement(newExpression); - private static SyntaxNode WithBlockBody(SyntaxNode node, BlockSyntax body) - { - switch (node) - { - case BasePropertyDeclarationSyntax baseProperty: - var accessorList = SyntaxFactory.AccessorList( - [SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, body)]); - return baseProperty - .TryWithExpressionBody(null) - .WithAccessorList(accessorList) - .TryWithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) - .WithTriviaFrom(baseProperty); - case AccessorDeclarationSyntax accessor: - return accessor - .WithExpressionBody(null) - .WithBody(body) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) - .WithTriviaFrom(accessor); - case BaseMethodDeclarationSyntax baseMethod: - return baseMethod - .WithExpressionBody(null) - .WithBody(body) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) - .WithTriviaFrom(baseMethod); - case LocalFunctionStatementSyntax localFunction: - return localFunction - .WithExpressionBody(null) - .WithBody(body) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) - .WithTriviaFrom(localFunction); - default: - throw ExceptionUtilities.UnexpectedValue(node); - } - } + var newBody = SyntaxFactory.Block(declarationStatement, convertedStatement) + .WithLeadingTrivia(leadingTrivia) + .WithTrailingTrivia(oldBody.GetTrailingTrivia()); - private async Task IntroduceLocalDeclarationIntoBlockAsync( - SemanticDocument document, - BlockSyntax block, - ExpressionSyntax expression, - NameSyntax newLocalName, - LocalDeclarationStatementSyntax declarationStatement, - bool allOccurrences, - CancellationToken cancellationToken) - { - declarationStatement = declarationStatement.WithAdditionalAnnotations(Formatter.Annotation); + // Add an elastic newline so that the formatter will place this new block across multiple lines. + newBody = newBody.WithOpenBraceToken(newBody.OpenBraceToken.WithAppendedTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed)) + .WithAdditionalAnnotations(Formatter.Annotation); - SyntaxNode scope = block; + var newRoot = document.Root.ReplaceNode(oldParentingNode, WithBlockBody(oldParentingNode, newBody)); + return document.Document.WithSyntaxRoot(newRoot); + } - // If we're within a non-static local function, our scope for the new local declaration is expanded to include the enclosing member. - var localFunction = block.GetAncestor(); - if (localFunction != null && !localFunction.Modifiers.Any(SyntaxKind.StaticKeyword)) - { - scope = block.GetAncestor(); - } + private static SyntaxNode WithBlockBody(SyntaxNode node, BlockSyntax body) + { + switch (node) + { + case BasePropertyDeclarationSyntax baseProperty: + var accessorList = SyntaxFactory.AccessorList( + [SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, body)]); + return baseProperty + .TryWithExpressionBody(null) + .WithAccessorList(accessorList) + .TryWithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) + .WithTriviaFrom(baseProperty); + case AccessorDeclarationSyntax accessor: + return accessor + .WithExpressionBody(null) + .WithBody(body) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) + .WithTriviaFrom(accessor); + case BaseMethodDeclarationSyntax baseMethod: + return baseMethod + .WithExpressionBody(null) + .WithBody(body) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) + .WithTriviaFrom(baseMethod); + case LocalFunctionStatementSyntax localFunction: + return localFunction + .WithExpressionBody(null) + .WithBody(body) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) + .WithTriviaFrom(localFunction); + default: + throw ExceptionUtilities.UnexpectedValue(node); + } + } - var matches = FindMatches(document, expression, document, scope, allOccurrences, cancellationToken); - Debug.Assert(matches.Contains(expression)); + private async Task IntroduceLocalDeclarationIntoBlockAsync( + SemanticDocument document, + BlockSyntax block, + ExpressionSyntax expression, + NameSyntax newLocalName, + LocalDeclarationStatementSyntax declarationStatement, + bool allOccurrences, + CancellationToken cancellationToken) + { + declarationStatement = declarationStatement.WithAdditionalAnnotations(Formatter.Annotation); - (document, matches) = await ComplexifyParentingStatementsAsync(document, matches, cancellationToken).ConfigureAwait(false); + SyntaxNode scope = block; - // Our original expression should have been one of the matches, which were tracked as part - // of complexification, so we can retrieve the latest version of the expression here. - expression = document.Root.GetCurrentNode(expression); + // If we're within a non-static local function, our scope for the new local declaration is expanded to include the enclosing member. + var localFunction = block.GetAncestor(); + if (localFunction != null && !localFunction.Modifiers.Any(SyntaxKind.StaticKeyword)) + { + scope = block.GetAncestor(); + } - var root = document.Root; - ISet allAffectedStatements = new HashSet(matches.SelectMany(expr => GetApplicableStatementAncestors(expr))); + var matches = FindMatches(document, expression, document, scope, allOccurrences, cancellationToken); + Debug.Assert(matches.Contains(expression)); - SyntaxNode innermostCommonBlock; + (document, matches) = await ComplexifyParentingStatementsAsync(document, matches, cancellationToken).ConfigureAwait(false); - var innermostStatements = new HashSet(matches.Select(expr => GetApplicableStatementAncestors(expr).First())); - if (innermostStatements.Count == 1) - { - // if there was only one match, or all the matches came from the same statement - var statement = innermostStatements.Single(); + // Our original expression should have been one of the matches, which were tracked as part + // of complexification, so we can retrieve the latest version of the expression here. + expression = document.Root.GetCurrentNode(expression); - // and the statement is an embedded statement without a block, we want to generate one - // around this statement rather than continue going up to find an actual block - if (!IsBlockLike(statement.Parent)) - { - root = root.TrackNodes(allAffectedStatements.Concat(new SyntaxNode[] { expression, statement })); - root = root.ReplaceNode(root.GetCurrentNode(statement), - SyntaxFactory.Block(root.GetCurrentNode(statement)).WithAdditionalAnnotations(Formatter.Annotation)); + var root = document.Root; + ISet allAffectedStatements = new HashSet(matches.SelectMany(expr => GetApplicableStatementAncestors(expr))); - expression = root.GetCurrentNode(expression); - allAffectedStatements = allAffectedStatements.Select(root.GetCurrentNode).ToSet(); + SyntaxNode innermostCommonBlock; - statement = root.GetCurrentNode(statement); - } + var innermostStatements = new HashSet(matches.Select(expr => GetApplicableStatementAncestors(expr).First())); + if (innermostStatements.Count == 1) + { + // if there was only one match, or all the matches came from the same statement + var statement = innermostStatements.Single(); - innermostCommonBlock = statement.Parent; - } - else + // and the statement is an embedded statement without a block, we want to generate one + // around this statement rather than continue going up to find an actual block + if (!IsBlockLike(statement.Parent)) { - innermostCommonBlock = innermostStatements.FindInnermostCommonNode(IsBlockLike); - } - - var firstStatementAffectedIndex = GetFirstStatementAffectedIndex(innermostCommonBlock, matches, GetStatements(innermostCommonBlock).IndexOf(allAffectedStatements.Contains)); + root = root.TrackNodes(allAffectedStatements.Concat(new SyntaxNode[] { expression, statement })); + root = root.ReplaceNode(root.GetCurrentNode(statement), + SyntaxFactory.Block(root.GetCurrentNode(statement)).WithAdditionalAnnotations(Formatter.Annotation)); - var newInnerMostBlock = Rewrite( - document, expression, newLocalName, document, innermostCommonBlock, allOccurrences, cancellationToken); + expression = root.GetCurrentNode(expression); + allAffectedStatements = allAffectedStatements.Select(root.GetCurrentNode).ToSet(); - var statements = InsertWithinTriviaOfNext(GetStatements(newInnerMostBlock), declarationStatement, firstStatementAffectedIndex); - var finalInnerMostBlock = WithStatements(newInnerMostBlock, statements); + statement = root.GetCurrentNode(statement); + } - var newRoot = root.ReplaceNode(innermostCommonBlock, finalInnerMostBlock); - return document.Document.WithSyntaxRoot(newRoot); + innermostCommonBlock = statement.Parent; } - - private static IEnumerable GetApplicableStatementAncestors(ExpressionSyntax expr) + else { - foreach (var statement in expr.GetAncestorsOrThis()) - { - // When determining where to put a local, we don't want to put it between the `else` - // and `if` of a compound if-statement. - - if (statement.Kind() == SyntaxKind.IfStatement && - statement.IsParentKind(SyntaxKind.ElseClause)) - { - continue; - } - - yield return statement; - } + innermostCommonBlock = innermostStatements.FindInnermostCommonNode(IsBlockLike); } - private static int GetFirstStatementAffectedIndex(SyntaxNode innermostCommonBlock, ISet matches, int firstStatementAffectedIndex) - { - // If a local function is involved, we have to make sure the new declaration is placed: - // 1. Before all calls to local functions that use the variable. - // 2. Before the local function(s) themselves. - // 3. Before all matches, i.e. places in the code where the new declaration will replace existing code. - // Cases (2) and (3) are already covered by the 'firstStatementAffectedIndex' parameter. Thus, all we have to do is ensure we consider (1) when - // determining where to place our new declaration. + var firstStatementAffectedIndex = GetFirstStatementAffectedIndex(innermostCommonBlock, matches, GetStatements(innermostCommonBlock).IndexOf(allAffectedStatements.Contains)); - // Find all the local functions within the scope that will use the new declaration. - var localFunctions = innermostCommonBlock.DescendantNodes().Where(node => node.IsKind(SyntaxKind.LocalFunctionStatement) && matches.Any(match => match.Span.OverlapsWith(node.Span))); + var newInnerMostBlock = Rewrite( + document, expression, newLocalName, document, innermostCommonBlock, allOccurrences, cancellationToken); - if (localFunctions.IsEmpty()) - { - return firstStatementAffectedIndex; - } + var statements = InsertWithinTriviaOfNext(GetStatements(newInnerMostBlock), declarationStatement, firstStatementAffectedIndex); + var finalInnerMostBlock = WithStatements(newInnerMostBlock, statements); - var localFunctionIdentifiers = localFunctions.Select(node => ((LocalFunctionStatementSyntax)node).Identifier.ValueText); + var newRoot = root.ReplaceNode(innermostCommonBlock, finalInnerMostBlock); + return document.Document.WithSyntaxRoot(newRoot); + } - // Find all calls to the applicable local functions within the scope. - var localFunctionCalls = innermostCommonBlock.DescendantNodes().Where(node => node is InvocationExpressionSyntax invocationExpression && - invocationExpression.Expression.GetRightmostName() != null && - !invocationExpression.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) && - localFunctionIdentifiers.Contains(invocationExpression.Expression.GetRightmostName().Identifier.ValueText)); + private static IEnumerable GetApplicableStatementAncestors(ExpressionSyntax expr) + { + foreach (var statement in expr.GetAncestorsOrThis()) + { + // When determining where to put a local, we don't want to put it between the `else` + // and `if` of a compound if-statement. - if (localFunctionCalls.IsEmpty()) + if (statement.Kind() == SyntaxKind.IfStatement && + statement.IsParentKind(SyntaxKind.ElseClause)) { - return firstStatementAffectedIndex; + continue; } - // Find which call is the earliest. - var earliestLocalFunctionCall = localFunctionCalls.Min(node => node.SpanStart); + yield return statement; + } + } + + private static int GetFirstStatementAffectedIndex(SyntaxNode innermostCommonBlock, ISet matches, int firstStatementAffectedIndex) + { + // If a local function is involved, we have to make sure the new declaration is placed: + // 1. Before all calls to local functions that use the variable. + // 2. Before the local function(s) themselves. + // 3. Before all matches, i.e. places in the code where the new declaration will replace existing code. + // Cases (2) and (3) are already covered by the 'firstStatementAffectedIndex' parameter. Thus, all we have to do is ensure we consider (1) when + // determining where to place our new declaration. - var statementsInBlock = GetStatements(innermostCommonBlock); + // Find all the local functions within the scope that will use the new declaration. + var localFunctions = innermostCommonBlock.DescendantNodes().Where(node => node.IsKind(SyntaxKind.LocalFunctionStatement) && matches.Any(match => match.Span.OverlapsWith(node.Span))); - // Check if our earliest call is before all local function declarations and all matches, and if so, place our new declaration there. - var earliestLocalFunctionCallIndex = statementsInBlock.IndexOf(s => s.Span.Contains(earliestLocalFunctionCall)); - return Math.Min(earliestLocalFunctionCallIndex, firstStatementAffectedIndex); + if (localFunctions.IsEmpty()) + { + return firstStatementAffectedIndex; } - private static SyntaxList InsertWithinTriviaOfNext( - SyntaxList oldStatements, - StatementSyntax newStatement, - int statementIndex) + var localFunctionIdentifiers = localFunctions.Select(node => ((LocalFunctionStatementSyntax)node).Identifier.ValueText); + + // Find all calls to the applicable local functions within the scope. + var localFunctionCalls = innermostCommonBlock.DescendantNodes().Where(node => node is InvocationExpressionSyntax invocationExpression && + invocationExpression.Expression.GetRightmostName() != null && + !invocationExpression.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) && + localFunctionIdentifiers.Contains(invocationExpression.Expression.GetRightmostName().Identifier.ValueText)); + + if (localFunctionCalls.IsEmpty()) { - var nextStatement = oldStatements.ElementAtOrDefault(statementIndex); - if (nextStatement == null) - return oldStatements.Insert(statementIndex, newStatement); + return firstStatementAffectedIndex; + } - // Grab all the trivia before the line the next statement is on and move it to the new node. + // Find which call is the earliest. + var earliestLocalFunctionCall = localFunctionCalls.Min(node => node.SpanStart); - var nextStatementLeading = nextStatement.GetLeadingTrivia(); - var precedingEndOfLine = nextStatementLeading.LastOrDefault(t => t.Kind() == SyntaxKind.EndOfLineTrivia); - if (precedingEndOfLine == default) - { - return oldStatements.ReplaceRange( - nextStatement, [newStatement, nextStatement]); - } + var statementsInBlock = GetStatements(innermostCommonBlock); - var endOfLineIndex = nextStatementLeading.IndexOf(precedingEndOfLine) + 1; + // Check if our earliest call is before all local function declarations and all matches, and if so, place our new declaration there. + var earliestLocalFunctionCallIndex = statementsInBlock.IndexOf(s => s.Span.Contains(earliestLocalFunctionCall)); + return Math.Min(earliestLocalFunctionCallIndex, firstStatementAffectedIndex); + } + + private static SyntaxList InsertWithinTriviaOfNext( + SyntaxList oldStatements, + StatementSyntax newStatement, + int statementIndex) + { + var nextStatement = oldStatements.ElementAtOrDefault(statementIndex); + if (nextStatement == null) + return oldStatements.Insert(statementIndex, newStatement); + + // Grab all the trivia before the line the next statement is on and move it to the new node. + var nextStatementLeading = nextStatement.GetLeadingTrivia(); + var precedingEndOfLine = nextStatementLeading.LastOrDefault(t => t.Kind() == SyntaxKind.EndOfLineTrivia); + if (precedingEndOfLine == default) + { return oldStatements.ReplaceRange( - nextStatement, - [ - newStatement.WithLeadingTrivia(nextStatementLeading.Take(endOfLineIndex)), - nextStatement.WithLeadingTrivia(nextStatementLeading.Skip(endOfLineIndex)), - ]); + nextStatement, [newStatement, nextStatement]); } - private static bool IsBlockLike(SyntaxNode node) => node is BlockSyntax or SwitchSectionSyntax; - - private static SyntaxList GetStatements(SyntaxNode blockLike) - => blockLike switch - { - BlockSyntax block => block.Statements, - SwitchSectionSyntax switchSection => switchSection.Statements, - _ => throw ExceptionUtilities.UnexpectedValue(blockLike), - }; + var endOfLineIndex = nextStatementLeading.IndexOf(precedingEndOfLine) + 1; - private static SyntaxNode WithStatements(SyntaxNode blockLike, SyntaxList statements) - => blockLike switch - { - BlockSyntax block => block.WithStatements(statements), - SwitchSectionSyntax switchSection => switchSection.WithStatements(statements), - _ => throw ExceptionUtilities.UnexpectedValue(blockLike), - }; + return oldStatements.ReplaceRange( + nextStatement, + [ + newStatement.WithLeadingTrivia(nextStatementLeading.Take(endOfLineIndex)), + nextStatement.WithLeadingTrivia(nextStatementLeading.Skip(endOfLineIndex)), + ]); } + + private static bool IsBlockLike(SyntaxNode node) => node is BlockSyntax or SwitchSectionSyntax; + + private static SyntaxList GetStatements(SyntaxNode blockLike) + => blockLike switch + { + BlockSyntax block => block.Statements, + SwitchSectionSyntax switchSection => switchSection.Statements, + _ => throw ExceptionUtilities.UnexpectedValue(blockLike), + }; + + private static SyntaxNode WithStatements(SyntaxNode blockLike, SyntaxList statements) + => blockLike switch + { + BlockSyntax block => block.WithStatements(statements), + SwitchSectionSyntax switchSection => switchSection.WithStatements(statements), + _ => throw ExceptionUtilities.UnexpectedValue(blockLike), + }; } diff --git a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceQueryLocal.cs b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceQueryLocal.cs index 0bc361be8b663..62387cde2865a 100644 --- a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceQueryLocal.cs +++ b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceQueryLocal.cs @@ -16,96 +16,95 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable +namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable; + +internal partial class CSharpIntroduceVariableService { - internal partial class CSharpIntroduceVariableService - { - private static bool IsAnyQueryClause(SyntaxNode node) - => node is QueryClauseSyntax or SelectOrGroupClauseSyntax; + private static bool IsAnyQueryClause(SyntaxNode node) + => node is QueryClauseSyntax or SelectOrGroupClauseSyntax; - protected override Task IntroduceQueryLocalAsync( - SemanticDocument document, ExpressionSyntax expression, bool allOccurrences, CancellationToken cancellationToken) - { - var oldOutermostQuery = expression.GetAncestorsOrThis().LastOrDefault(); + protected override Task IntroduceQueryLocalAsync( + SemanticDocument document, ExpressionSyntax expression, bool allOccurrences, CancellationToken cancellationToken) + { + var oldOutermostQuery = expression.GetAncestorsOrThis().LastOrDefault(); - var newLocalNameToken = GenerateUniqueLocalName( - document, expression, isConstant: false, - containerOpt: oldOutermostQuery, cancellationToken: cancellationToken); - var newLocalName = SyntaxFactory.IdentifierName(newLocalNameToken); + var newLocalNameToken = GenerateUniqueLocalName( + document, expression, isConstant: false, + containerOpt: oldOutermostQuery, cancellationToken: cancellationToken); + var newLocalName = SyntaxFactory.IdentifierName(newLocalNameToken); - var letClause = SyntaxFactory.LetClause( - newLocalNameToken.WithAdditionalAnnotations(RenameAnnotation.Create()), - expression).WithAdditionalAnnotations(Formatter.Annotation); + var letClause = SyntaxFactory.LetClause( + newLocalNameToken.WithAdditionalAnnotations(RenameAnnotation.Create()), + expression).WithAdditionalAnnotations(Formatter.Annotation); - var matches = FindMatches(document, expression, document, oldOutermostQuery, allOccurrences, cancellationToken); - var innermostClauses = new HashSet( - matches.Select(expr => expr.GetAncestorsOrThis().First(IsAnyQueryClause))); + var matches = FindMatches(document, expression, document, oldOutermostQuery, allOccurrences, cancellationToken); + var innermostClauses = new HashSet( + matches.Select(expr => expr.GetAncestorsOrThis().First(IsAnyQueryClause))); - if (innermostClauses.Count == 1) - { - // If there was only one match, or all the matches came from the same - // statement, then we want to place the declaration right above that - // statement. Note: we special case this because the statement we are going - // to go above might not be in a block and we may have to generate it - return Task.FromResult(IntroduceQueryLocalForSingleOccurrence( - document, expression, newLocalName, letClause, allOccurrences, cancellationToken)); - } + if (innermostClauses.Count == 1) + { + // If there was only one match, or all the matches came from the same + // statement, then we want to place the declaration right above that + // statement. Note: we special case this because the statement we are going + // to go above might not be in a block and we may have to generate it + return Task.FromResult(IntroduceQueryLocalForSingleOccurrence( + document, expression, newLocalName, letClause, allOccurrences, cancellationToken)); + } - var oldInnerMostCommonQuery = matches.FindInnermostCommonNode(); - var newInnerMostQuery = Rewrite( - document, expression, newLocalName, document, oldInnerMostCommonQuery, allOccurrences, cancellationToken); + var oldInnerMostCommonQuery = matches.FindInnermostCommonNode(); + var newInnerMostQuery = Rewrite( + document, expression, newLocalName, document, oldInnerMostCommonQuery, allOccurrences, cancellationToken); - var allAffectedClauses = new HashSet(matches.SelectMany(expr => expr.GetAncestorsOrThis().Where(IsAnyQueryClause))); + var allAffectedClauses = new HashSet(matches.SelectMany(expr => expr.GetAncestorsOrThis().Where(IsAnyQueryClause))); - var oldClauses = oldInnerMostCommonQuery.GetAllClauses(); - var newClauses = newInnerMostQuery.GetAllClauses(); + var oldClauses = oldInnerMostCommonQuery.GetAllClauses(); + var newClauses = newInnerMostQuery.GetAllClauses(); - var firstClauseAffectedInQuery = oldClauses.First(allAffectedClauses.Contains); - var firstClauseAffectedIndex = oldClauses.IndexOf(firstClauseAffectedInQuery); + var firstClauseAffectedInQuery = oldClauses.First(allAffectedClauses.Contains); + var firstClauseAffectedIndex = oldClauses.IndexOf(firstClauseAffectedInQuery); - var finalClauses = newClauses.Take(firstClauseAffectedIndex) - .Concat(letClause) - .Concat(newClauses.Skip(firstClauseAffectedIndex)).ToList(); + var finalClauses = newClauses.Take(firstClauseAffectedIndex) + .Concat(letClause) + .Concat(newClauses.Skip(firstClauseAffectedIndex)).ToList(); - var finalQuery = newInnerMostQuery.WithAllClauses(finalClauses); - var newRoot = document.Root.ReplaceNode(oldInnerMostCommonQuery, finalQuery); + var finalQuery = newInnerMostQuery.WithAllClauses(finalClauses); + var newRoot = document.Root.ReplaceNode(oldInnerMostCommonQuery, finalQuery); - return Task.FromResult(document.Document.WithSyntaxRoot(newRoot)); - } + return Task.FromResult(document.Document.WithSyntaxRoot(newRoot)); + } - private Document IntroduceQueryLocalForSingleOccurrence( - SemanticDocument document, - ExpressionSyntax expression, - NameSyntax newLocalName, - LetClauseSyntax letClause, - bool allOccurrences, - CancellationToken cancellationToken) - { - var oldClause = expression.GetAncestors().First(IsAnyQueryClause); - var newClause = Rewrite( - document, expression, newLocalName, document, oldClause, allOccurrences, cancellationToken); + private Document IntroduceQueryLocalForSingleOccurrence( + SemanticDocument document, + ExpressionSyntax expression, + NameSyntax newLocalName, + LetClauseSyntax letClause, + bool allOccurrences, + CancellationToken cancellationToken) + { + var oldClause = expression.GetAncestors().First(IsAnyQueryClause); + var newClause = Rewrite( + document, expression, newLocalName, document, oldClause, allOccurrences, cancellationToken); - var oldQuery = (QueryBodySyntax)oldClause.Parent; - var newQuery = GetNewQuery(oldQuery, oldClause, newClause, letClause); + var oldQuery = (QueryBodySyntax)oldClause.Parent; + var newQuery = GetNewQuery(oldQuery, oldClause, newClause, letClause); - var newRoot = document.Root.ReplaceNode(oldQuery, newQuery); - return document.Document.WithSyntaxRoot(newRoot); - } + var newRoot = document.Root.ReplaceNode(oldQuery, newQuery); + return document.Document.WithSyntaxRoot(newRoot); + } - private static QueryBodySyntax GetNewQuery( - QueryBodySyntax oldQuery, - SyntaxNode oldClause, - SyntaxNode newClause, - LetClauseSyntax letClause) - { - var oldClauses = oldQuery.GetAllClauses(); - var oldClauseIndex = oldClauses.IndexOf(oldClause); - - var newClauses = oldClauses.Take(oldClauseIndex) - .Concat(letClause) - .Concat(newClause) - .Concat(oldClauses.Skip(oldClauseIndex + 1)).ToList(); - return oldQuery.WithAllClauses(newClauses); - } + private static QueryBodySyntax GetNewQuery( + QueryBodySyntax oldQuery, + SyntaxNode oldClause, + SyntaxNode newClause, + LetClauseSyntax letClause) + { + var oldClauses = oldQuery.GetAllClauses(); + var oldClauseIndex = oldClauses.IndexOf(oldClause); + + var newClauses = oldClauses.Take(oldClauseIndex) + .Concat(letClause) + .Concat(newClause) + .Concat(oldClauses.Skip(oldClauseIndex + 1)).ToList(); + return oldQuery.WithAllClauses(newClauses); } } diff --git a/src/Features/CSharp/Portable/InvertConditional/CSharpInvertConditionalCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/InvertConditional/CSharpInvertConditionalCodeRefactoringProvider.cs index 4413b6fd60c88..e831515b4d20e 100644 --- a/src/Features/CSharp/Portable/InvertConditional/CSharpInvertConditionalCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/InvertConditional/CSharpInvertConditionalCodeRefactoringProvider.cs @@ -10,21 +10,20 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.InvertConditional; -namespace Microsoft.CodeAnalysis.CSharp.InvertConditional +namespace Microsoft.CodeAnalysis.CSharp.InvertConditional; + +[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InvertConditional), Shared] +internal class CSharpInvertConditionalCodeRefactoringProvider + : AbstractInvertConditionalCodeRefactoringProvider { - [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InvertConditional), Shared] - internal class CSharpInvertConditionalCodeRefactoringProvider - : AbstractInvertConditionalCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpInvertConditionalCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpInvertConditionalCodeRefactoringProvider() - { - } - - // Don't offer if the conditional is missing the colon and the conditional is too incomplete. - protected override bool ShouldOffer(ConditionalExpressionSyntax conditional) - => !conditional.ColonToken.IsMissing; } + + // Don't offer if the conditional is missing the colon and the conditional is too incomplete. + protected override bool ShouldOffer(ConditionalExpressionSyntax conditional) + => !conditional.ColonToken.IsMissing; } diff --git a/src/Features/CSharp/Portable/InvertIf/CSharpInvertIfCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/InvertIf/CSharpInvertIfCodeRefactoringProvider.cs index a71a7e60320dd..125273e608a2a 100644 --- a/src/Features/CSharp/Portable/InvertIf/CSharpInvertIfCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/InvertIf/CSharpInvertIfCodeRefactoringProvider.cs @@ -16,249 +16,248 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.InvertIf +namespace Microsoft.CodeAnalysis.CSharp.InvertIf; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InvertIf), Shared] +internal sealed class CSharpInvertIfCodeRefactoringProvider : AbstractInvertIfCodeRefactoringProvider< + SyntaxKind, StatementSyntax, IfStatementSyntax, StatementSyntax> { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InvertIf), Shared] - internal sealed class CSharpInvertIfCodeRefactoringProvider : AbstractInvertIfCodeRefactoringProvider< - SyntaxKind, StatementSyntax, IfStatementSyntax, StatementSyntax> + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpInvertIfCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpInvertIfCodeRefactoringProvider() - { - } + } - protected override string GetTitle() - => CSharpFeaturesResources.Invert_if; + protected override string GetTitle() + => CSharpFeaturesResources.Invert_if; - protected override bool IsElseless(IfStatementSyntax ifNode) - => ifNode.Else == null; + protected override bool IsElseless(IfStatementSyntax ifNode) + => ifNode.Else == null; - protected override bool CanInvert(IfStatementSyntax ifNode) - => ifNode?.Parent is (kind: SyntaxKind.Block or SyntaxKind.SwitchSection); + protected override bool CanInvert(IfStatementSyntax ifNode) + => ifNode?.Parent is (kind: SyntaxKind.Block or SyntaxKind.SwitchSection); - protected override SyntaxNode GetCondition(IfStatementSyntax ifNode) - => ifNode.Condition; + protected override SyntaxNode GetCondition(IfStatementSyntax ifNode) + => ifNode.Condition; - protected override StatementRange GetIfBodyStatementRange(IfStatementSyntax ifNode) - => new(ifNode.Statement, ifNode.Statement); + protected override StatementRange GetIfBodyStatementRange(IfStatementSyntax ifNode) + => new(ifNode.Statement, ifNode.Statement); - protected override bool IsStatementContainer(SyntaxNode node) - => node.Kind() is SyntaxKind.Block or SyntaxKind.SwitchSection; + protected override bool IsStatementContainer(SyntaxNode node) + => node.Kind() is SyntaxKind.Block or SyntaxKind.SwitchSection; - protected override bool IsNoOpSyntaxNode(SyntaxNode node) - => node.Kind() is SyntaxKind.Block or SyntaxKind.EmptyStatement; + protected override bool IsNoOpSyntaxNode(SyntaxNode node) + => node.Kind() is SyntaxKind.Block or SyntaxKind.EmptyStatement; - protected override bool IsExecutableStatement(SyntaxNode node) - => node is StatementSyntax; + protected override bool IsExecutableStatement(SyntaxNode node) + => node is StatementSyntax; - protected override StatementSyntax? GetNextStatement(StatementSyntax node) - => node.GetNextStatement(); + protected override StatementSyntax? GetNextStatement(StatementSyntax node) + => node.GetNextStatement(); - protected override StatementSyntax GetIfBody(IfStatementSyntax ifNode) - => ifNode.Statement; + protected override StatementSyntax GetIfBody(IfStatementSyntax ifNode) + => ifNode.Statement; - protected override StatementSyntax GetEmptyEmbeddedStatement() - => SyntaxFactory.Block(); + protected override StatementSyntax GetEmptyEmbeddedStatement() + => SyntaxFactory.Block(); - protected override StatementSyntax GetElseBody(IfStatementSyntax ifNode) - => ifNode.Else?.Statement ?? throw new InvalidOperationException(); + protected override StatementSyntax GetElseBody(IfStatementSyntax ifNode) + => ifNode.Else?.Statement ?? throw new InvalidOperationException(); - protected override bool CanControlFlowOut(SyntaxNode node) + protected override bool CanControlFlowOut(SyntaxNode node) + { + switch (node) { - switch (node) - { - case SwitchSectionSyntax: - case LocalFunctionStatementSyntax: - case AccessorDeclarationSyntax: - case MemberDeclarationSyntax: - case AnonymousFunctionExpressionSyntax: - case CommonForEachStatementSyntax: - case DoStatementSyntax: - case WhileStatementSyntax: - case ForStatementSyntax: - return false; - } - - return true; + case SwitchSectionSyntax: + case LocalFunctionStatementSyntax: + case AccessorDeclarationSyntax: + case MemberDeclarationSyntax: + case AnonymousFunctionExpressionSyntax: + case CommonForEachStatementSyntax: + case DoStatementSyntax: + case WhileStatementSyntax: + case ForStatementSyntax: + return false; } - protected override SyntaxList GetStatements(SyntaxNode node) - => node switch - { - BlockSyntax n => n.Statements, - SwitchSectionSyntax n => n.Statements, - _ => throw ExceptionUtilities.UnexpectedValue(node), - }; + return true; + } - protected override SyntaxKind? GetJumpStatementKind(SyntaxNode node) - => node switch - { - SwitchSectionSyntax - => SyntaxKind.BreakStatement, - LocalFunctionStatementSyntax or AccessorDeclarationSyntax or MemberDeclarationSyntax - => node.ContainsYield() ? SyntaxKind.YieldBreakStatement : SyntaxKind.ReturnStatement, - AnonymousFunctionExpressionSyntax - => SyntaxKind.ReturnStatement, - CommonForEachStatementSyntax or DoStatementSyntax or WhileStatementSyntax or ForStatementSyntax - => SyntaxKind.ContinueStatement, - _ => null, - }; - - protected override StatementSyntax GetJumpStatement(SyntaxKind kind) - => kind switch - { - SyntaxKind.ContinueStatement => SyntaxFactory.ContinueStatement(), - SyntaxKind.BreakStatement => SyntaxFactory.BreakStatement(), - SyntaxKind.ReturnStatement => SyntaxFactory.ReturnStatement(), - SyntaxKind.YieldBreakStatement => SyntaxFactory.YieldStatement(SyntaxKind.YieldBreakStatement), - _ => throw ExceptionUtilities.UnexpectedValue(kind), - }; - - protected override StatementSyntax AsEmbeddedStatement(IEnumerable statements, StatementSyntax original) + protected override SyntaxList GetStatements(SyntaxNode node) + => node switch { - var statementArray = statements.ToArray(); - if (statementArray.Length > 0) - { - statementArray[0] = statementArray[0].GetNodeWithoutLeadingBlankLines(); - } + BlockSyntax n => n.Statements, + SwitchSectionSyntax n => n.Statements, + _ => throw ExceptionUtilities.UnexpectedValue(node), + }; - return original is BlockSyntax block - ? block.WithStatements([.. statementArray]) - : statementArray.Length == 1 - ? statementArray[0] - : SyntaxFactory.Block(statementArray); + protected override SyntaxKind? GetJumpStatementKind(SyntaxNode node) + => node switch + { + SwitchSectionSyntax + => SyntaxKind.BreakStatement, + LocalFunctionStatementSyntax or AccessorDeclarationSyntax or MemberDeclarationSyntax + => node.ContainsYield() ? SyntaxKind.YieldBreakStatement : SyntaxKind.ReturnStatement, + AnonymousFunctionExpressionSyntax + => SyntaxKind.ReturnStatement, + CommonForEachStatementSyntax or DoStatementSyntax or WhileStatementSyntax or ForStatementSyntax + => SyntaxKind.ContinueStatement, + _ => null, + }; + + protected override StatementSyntax GetJumpStatement(SyntaxKind kind) + => kind switch + { + SyntaxKind.ContinueStatement => SyntaxFactory.ContinueStatement(), + SyntaxKind.BreakStatement => SyntaxFactory.BreakStatement(), + SyntaxKind.ReturnStatement => SyntaxFactory.ReturnStatement(), + SyntaxKind.YieldBreakStatement => SyntaxFactory.YieldStatement(SyntaxKind.YieldBreakStatement), + _ => throw ExceptionUtilities.UnexpectedValue(kind), + }; + + protected override StatementSyntax AsEmbeddedStatement(IEnumerable statements, StatementSyntax original) + { + var statementArray = statements.ToArray(); + if (statementArray.Length > 0) + { + statementArray[0] = statementArray[0].GetNodeWithoutLeadingBlankLines(); } - protected override IfStatementSyntax UpdateIf( - SourceText sourceText, - IfStatementSyntax ifNode, - SyntaxNode condition, - StatementSyntax trueStatement, - StatementSyntax? falseStatementOpt = null) + return original is BlockSyntax block + ? block.WithStatements([.. statementArray]) + : statementArray.Length == 1 + ? statementArray[0] + : SyntaxFactory.Block(statementArray); + } + + protected override IfStatementSyntax UpdateIf( + SourceText sourceText, + IfStatementSyntax ifNode, + SyntaxNode condition, + StatementSyntax trueStatement, + StatementSyntax? falseStatementOpt = null) + { + var isSingleLine = sourceText.AreOnSameLine(ifNode.GetFirstToken(), ifNode.GetLastToken()); + if (isSingleLine && falseStatementOpt != null) { - var isSingleLine = sourceText.AreOnSameLine(ifNode.GetFirstToken(), ifNode.GetLastToken()); - if (isSingleLine && falseStatementOpt != null) - { - // If statement is on a single line, and we're swapping the true/false parts. - // In that case, try to swap the trailing trivia between the true/false parts. - // That way the trailing comments/newlines at the end of hte 'if' stay there, - // and the spaces after the true-part stay where they are. - - (trueStatement, falseStatementOpt) = - (trueStatement.WithTrailingTrivia(falseStatementOpt.GetTrailingTrivia()), - falseStatementOpt.WithTrailingTrivia(trueStatement.GetTrailingTrivia())); - } + // If statement is on a single line, and we're swapping the true/false parts. + // In that case, try to swap the trailing trivia between the true/false parts. + // That way the trailing comments/newlines at the end of hte 'if' stay there, + // and the spaces after the true-part stay where they are. + + (trueStatement, falseStatementOpt) = + (trueStatement.WithTrailingTrivia(falseStatementOpt.GetTrailingTrivia()), + falseStatementOpt.WithTrailingTrivia(trueStatement.GetTrailingTrivia())); + } - var updatedIf = ifNode - .WithCondition((ExpressionSyntax)condition) - .WithStatement(trueStatement is IfStatementSyntax - ? SyntaxFactory.Block(trueStatement) - : trueStatement); + var updatedIf = ifNode + .WithCondition((ExpressionSyntax)condition) + .WithStatement(trueStatement is IfStatementSyntax + ? SyntaxFactory.Block(trueStatement) + : trueStatement); - if (ShouldKeepFalse(ifNode, falseStatementOpt)) - { - var elseClause = updatedIf.Else != null - ? updatedIf.Else.WithStatement(falseStatementOpt) - : SyntaxFactory.ElseClause(falseStatementOpt); + if (ShouldKeepFalse(ifNode, falseStatementOpt)) + { + var elseClause = updatedIf.Else != null + ? updatedIf.Else.WithStatement(falseStatementOpt) + : SyntaxFactory.ElseClause(falseStatementOpt); - updatedIf = updatedIf.WithElse(elseClause); - } - else - { - updatedIf = updatedIf.WithElse(null); - } + updatedIf = updatedIf.WithElse(elseClause); + } + else + { + updatedIf = updatedIf.WithElse(null); + } + + // If this is multiline, format things after we swap around the if/else. Because + // of all the different types of rewriting, we may need indentation fixed up and + // whatnot. Don't do this with single-line because we want to ensure as closely + // as possible that we've kept things on that single line. + return isSingleLine + ? updatedIf + : updatedIf.WithAdditionalAnnotations(Formatter.Annotation); + } - // If this is multiline, format things after we swap around the if/else. Because - // of all the different types of rewriting, we may need indentation fixed up and - // whatnot. Don't do this with single-line because we want to ensure as closely - // as possible that we've kept things on that single line. - return isSingleLine - ? updatedIf - : updatedIf.WithAdditionalAnnotations(Formatter.Annotation); + private static bool ShouldKeepFalse(IfStatementSyntax originalIfStatement, [NotNullWhen(returnValue: true)] StatementSyntax? falseStatement) + { + // The original false statement doesn't exist at all + // then no need to consider keeping it around + if (falseStatement is null) + { + return false; } - private static bool ShouldKeepFalse(IfStatementSyntax originalIfStatement, [NotNullWhen(returnValue: true)] StatementSyntax? falseStatement) + if (falseStatement is BlockSyntax falseBlock) { - // The original false statement doesn't exist at all - // then no need to consider keeping it around - if (falseStatement is null) + // Block false syntax with some statements included. + // If there are no statements, it's an empty + // block and we can get rid of it. + if (falseBlock.Statements.Any()) { - return false; + return true; } - if (falseStatement is BlockSyntax falseBlock) - { - // Block false syntax with some statements included. - // If there are no statements, it's an empty - // block and we can get rid of it. - if (falseBlock.Statements.Any()) - { - return true; - } - - // If the stateements for the else don't pass, we still need to check - // if there are comments from the original if that should be included. - // If so, pass them along to be copied to the new else block - return originalIfStatement.Statement is BlockSyntax block - && BlockHasComment(block); - } + // If the stateements for the else don't pass, we still need to check + // if there are comments from the original if that should be included. + // If so, pass them along to be copied to the new else block + return originalIfStatement.Statement is BlockSyntax block + && BlockHasComment(block); + } - // The statement is not expected to have children, so we know it's fine - // to consider this something that needs to be included. Such as - // a return statement, or other similar things for single line if/else. - return true; + // The statement is not expected to have children, so we know it's fine + // to consider this something that needs to be included. Such as + // a return statement, or other similar things for single line if/else. + return true; - static bool BlockHasComment(BlockSyntax block) - { - return block.CloseBraceToken.LeadingTrivia.Any(HasCommentTrivia) - || block.OpenBraceToken.TrailingTrivia.Any(HasCommentTrivia); - } + static bool BlockHasComment(BlockSyntax block) + { + return block.CloseBraceToken.LeadingTrivia.Any(HasCommentTrivia) + || block.OpenBraceToken.TrailingTrivia.Any(HasCommentTrivia); + } - static bool HasCommentTrivia(SyntaxTrivia trivia) - { - return trivia.Kind() is SyntaxKind.MultiLineCommentTrivia or SyntaxKind.SingleLineCommentTrivia; - } + static bool HasCommentTrivia(SyntaxTrivia trivia) + { + return trivia.Kind() is SyntaxKind.MultiLineCommentTrivia or SyntaxKind.SingleLineCommentTrivia; } + } - protected override SyntaxNode WithStatements(SyntaxNode node, IEnumerable statements) - => node switch - { - BlockSyntax n => n.WithStatements([.. statements]), - SwitchSectionSyntax n => n.WithStatements([.. statements]), - _ => throw ExceptionUtilities.UnexpectedValue(node), - }; + protected override SyntaxNode WithStatements(SyntaxNode node, IEnumerable statements) + => node switch + { + BlockSyntax n => n.WithStatements([.. statements]), + SwitchSectionSyntax n => n.WithStatements([.. statements]), + _ => throw ExceptionUtilities.UnexpectedValue(node), + }; - protected override IEnumerable UnwrapBlock(StatementSyntax ifBody) + protected override IEnumerable UnwrapBlock(StatementSyntax ifBody) + { + return ifBody is BlockSyntax block + ? block.Statements + : [ifBody]; + } + + protected override bool IsSingleStatementStatementRange(StatementRange statementRange) + { + if (statementRange.IsEmpty) { - return ifBody is BlockSyntax block - ? block.Statements - : [ifBody]; + return false; } - protected override bool IsSingleStatementStatementRange(StatementRange statementRange) + if (statementRange.FirstStatement != statementRange.LastStatement) { - if (statementRange.IsEmpty) - { - return false; - } - - if (statementRange.FirstStatement != statementRange.LastStatement) - { - return false; - } + return false; + } - return IsSingleStatement(statementRange.FirstStatement); + return IsSingleStatement(statementRange.FirstStatement); - static bool IsSingleStatement(StatementSyntax statement) + static bool IsSingleStatement(StatementSyntax statement) + { + if (statement is BlockSyntax block) { - if (statement is BlockSyntax block) - { - return block.Statements.Count == 1 && IsSingleStatement(block.Statements[0]); - } - - return true; + return block.Statements.Count == 1 && IsSingleStatement(block.Statements[0]); } + + return true; } } } diff --git a/src/Features/CSharp/Portable/InvertLogical/CSharpInvertLogicalCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/InvertLogical/CSharpInvertLogicalCodeRefactoringProvider.cs index f745e00bc79b8..625a85cb7aedb 100644 --- a/src/Features/CSharp/Portable/InvertLogical/CSharpInvertLogicalCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/InvertLogical/CSharpInvertLogicalCodeRefactoringProvider.cs @@ -8,22 +8,21 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.InvertLogical; -namespace Microsoft.CodeAnalysis.CSharp.InvertLogical +namespace Microsoft.CodeAnalysis.CSharp.InvertLogical; + +[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InvertLogical), Shared] +internal class CSharpInvertLogicalCodeRefactoringProvider : + AbstractInvertLogicalCodeRefactoringProvider { - [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InvertLogical), Shared] - internal class CSharpInvertLogicalCodeRefactoringProvider : - AbstractInvertLogicalCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpInvertLogicalCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpInvertLogicalCodeRefactoringProvider() - { - } - - protected override string GetOperatorText(SyntaxKind binaryExprKind) - => binaryExprKind == SyntaxKind.LogicalAndExpression - ? SyntaxFacts.GetText(SyntaxKind.AmpersandAmpersandToken) - : SyntaxFacts.GetText(SyntaxKind.BarBarToken); } + + protected override string GetOperatorText(SyntaxKind binaryExprKind) + => binaryExprKind == SyntaxKind.LogicalAndExpression + ? SyntaxFacts.GetText(SyntaxKind.AmpersandAmpersandToken) + : SyntaxFacts.GetText(SyntaxKind.BarBarToken); } diff --git a/src/Features/CSharp/Portable/LanguageServices/CSharpStructuralTypeDisplayService.cs b/src/Features/CSharp/Portable/LanguageServices/CSharpStructuralTypeDisplayService.cs index a1c30b0342152..3873059bed1dd 100644 --- a/src/Features/CSharp/Portable/LanguageServices/CSharpStructuralTypeDisplayService.cs +++ b/src/Features/CSharp/Portable/LanguageServices/CSharpStructuralTypeDisplayService.cs @@ -16,48 +16,47 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Editor.CSharp.LanguageServices +namespace Microsoft.CodeAnalysis.Editor.CSharp.LanguageServices; + +[ExportLanguageService(typeof(IStructuralTypeDisplayService), LanguageNames.CSharp), Shared] +internal class CSharpStructuralTypeDisplayService : AbstractStructuralTypeDisplayService { - [ExportLanguageService(typeof(IStructuralTypeDisplayService), LanguageNames.CSharp), Shared] - internal class CSharpStructuralTypeDisplayService : AbstractStructuralTypeDisplayService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpStructuralTypeDisplayService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpStructuralTypeDisplayService() - { - } + } - protected override ISyntaxFacts SyntaxFactsService => CSharpSyntaxFacts.Instance; + protected override ISyntaxFacts SyntaxFactsService => CSharpSyntaxFacts.Instance; - protected override ImmutableArray GetNormalAnonymousTypeParts( - INamedTypeSymbol anonymousType, SemanticModel semanticModel, int position) - { - using var _ = ArrayBuilder.GetInstance(out var members); + protected override ImmutableArray GetNormalAnonymousTypeParts( + INamedTypeSymbol anonymousType, SemanticModel semanticModel, int position) + { + using var _ = ArrayBuilder.GetInstance(out var members); - members.Add(Keyword(SyntaxFacts.GetText(SyntaxKind.NewKeyword))); - members.AddRange(Space()); - members.Add(Punctuation(SyntaxFacts.GetText(SyntaxKind.OpenBraceToken))); - members.AddRange(Space()); + members.Add(Keyword(SyntaxFacts.GetText(SyntaxKind.NewKeyword))); + members.AddRange(Space()); + members.Add(Punctuation(SyntaxFacts.GetText(SyntaxKind.OpenBraceToken))); + members.AddRange(Space()); - var first = true; - foreach (var property in anonymousType.GetValidAnonymousTypeProperties()) + var first = true; + foreach (var property in anonymousType.GetValidAnonymousTypeProperties()) + { + if (!first) { - if (!first) - { - members.Add(Punctuation(SyntaxFacts.GetText(SyntaxKind.CommaToken))); - members.AddRange(Space()); - } - - first = false; - members.AddRange(property.Type.ToMinimalDisplayParts(semanticModel, position, s_minimalWithoutExpandedTuples).Select(p => p.MassageErrorTypeNames("?"))); + members.Add(Punctuation(SyntaxFacts.GetText(SyntaxKind.CommaToken))); members.AddRange(Space()); - members.Add(new SymbolDisplayPart(SymbolDisplayPartKind.PropertyName, property, property.Name)); } + first = false; + members.AddRange(property.Type.ToMinimalDisplayParts(semanticModel, position, s_minimalWithoutExpandedTuples).Select(p => p.MassageErrorTypeNames("?"))); members.AddRange(Space()); - members.Add(Punctuation(SyntaxFacts.GetText(SyntaxKind.CloseBraceToken))); - - return members.ToImmutable(); + members.Add(new SymbolDisplayPart(SymbolDisplayPartKind.PropertyName, property, CSharpSyntaxFacts.Instance.EscapeIdentifier(property.Name))); } + + members.AddRange(Space()); + members.Add(Punctuation(SyntaxFacts.GetText(SyntaxKind.CloseBraceToken))); + + return members.ToImmutable(); } } diff --git a/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayService.SymbolDescriptionBuilder.cs b/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayService.SymbolDescriptionBuilder.cs index 0e5a6e28d66f3..86910b4cc60ce 100644 --- a/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayService.SymbolDescriptionBuilder.cs +++ b/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayService.SymbolDescriptionBuilder.cs @@ -13,201 +13,200 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.CSharp.LanguageServices +namespace Microsoft.CodeAnalysis.Editor.CSharp.LanguageServices; + +internal partial class CSharpSymbolDisplayService { - internal partial class CSharpSymbolDisplayService + protected class SymbolDescriptionBuilder( + SemanticModel semanticModel, + int position, + Host.LanguageServices languageServices, + SymbolDescriptionOptions options, + CancellationToken cancellationToken) : AbstractSymbolDescriptionBuilder(semanticModel, position, languageServices, options, cancellationToken) { - protected class SymbolDescriptionBuilder( - SemanticModel semanticModel, - int position, - Host.LanguageServices languageServices, - SymbolDescriptionOptions options, - CancellationToken cancellationToken) : AbstractSymbolDescriptionBuilder(semanticModel, position, languageServices, options, cancellationToken) + private static readonly SymbolDisplayFormat s_minimallyQualifiedFormat = SymbolDisplayFormat.MinimallyQualifiedFormat + .AddLocalOptions(SymbolDisplayLocalOptions.IncludeRef) + .AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseErrorTypeSymbolName) + .RemoveParameterOptions(SymbolDisplayParameterOptions.IncludeDefaultValue) + .WithKindOptions(SymbolDisplayKindOptions.None); + + private static readonly SymbolDisplayFormat s_minimallyQualifiedFormatWithConstants = s_minimallyQualifiedFormat + .AddLocalOptions(SymbolDisplayLocalOptions.IncludeConstantValue) + .AddMemberOptions(SymbolDisplayMemberOptions.IncludeConstantValue) + .AddParameterOptions(SymbolDisplayParameterOptions.IncludeDefaultValue); + + private static readonly SymbolDisplayFormat s_minimallyQualifiedFormatWithConstantsAndModifiers = s_minimallyQualifiedFormatWithConstants + .AddMemberOptions(SymbolDisplayMemberOptions.IncludeModifiers); + + protected override void AddDeprecatedPrefix() { - private static readonly SymbolDisplayFormat s_minimallyQualifiedFormat = SymbolDisplayFormat.MinimallyQualifiedFormat - .AddLocalOptions(SymbolDisplayLocalOptions.IncludeRef) - .AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseErrorTypeSymbolName) - .RemoveParameterOptions(SymbolDisplayParameterOptions.IncludeDefaultValue) - .WithKindOptions(SymbolDisplayKindOptions.None); + AddToGroup(SymbolDescriptionGroups.MainDescription, + Punctuation("["), + PlainText(CSharpFeaturesResources.deprecated), + Punctuation("]"), + Space()); + } - private static readonly SymbolDisplayFormat s_minimallyQualifiedFormatWithConstants = s_minimallyQualifiedFormat - .AddLocalOptions(SymbolDisplayLocalOptions.IncludeConstantValue) - .AddMemberOptions(SymbolDisplayMemberOptions.IncludeConstantValue) - .AddParameterOptions(SymbolDisplayParameterOptions.IncludeDefaultValue); + protected override void AddExtensionPrefix() + { + AddToGroup(SymbolDescriptionGroups.MainDescription, + Punctuation("("), + PlainText(CSharpFeaturesResources.extension), + Punctuation(")"), + Space()); + } - private static readonly SymbolDisplayFormat s_minimallyQualifiedFormatWithConstantsAndModifiers = s_minimallyQualifiedFormatWithConstants - .AddMemberOptions(SymbolDisplayMemberOptions.IncludeModifiers); + protected override void AddAwaitablePrefix() + { + AddToGroup(SymbolDescriptionGroups.MainDescription, + Punctuation("("), + PlainText(CSharpFeaturesResources.awaitable), + Punctuation(")"), + Space()); + } - protected override void AddDeprecatedPrefix() - { - AddToGroup(SymbolDescriptionGroups.MainDescription, - Punctuation("["), - PlainText(CSharpFeaturesResources.deprecated), - Punctuation("]"), - Space()); - } + protected override void AddAwaitableExtensionPrefix() + { + AddToGroup(SymbolDescriptionGroups.MainDescription, + Punctuation("("), + PlainText(CSharpFeaturesResources.awaitable_extension), + Punctuation(")"), + Space()); + } - protected override void AddExtensionPrefix() - { - AddToGroup(SymbolDescriptionGroups.MainDescription, - Punctuation("("), - PlainText(CSharpFeaturesResources.extension), - Punctuation(")"), - Space()); - } + protected override void AddEnumUnderlyingTypeSeparator() + { + AddToGroup(SymbolDescriptionGroups.MainDescription, + Space(), + Punctuation(":"), + Space()); + } - protected override void AddAwaitablePrefix() + protected override Task> GetInitializerSourcePartsAsync( + ISymbol symbol) + { + // Actually check for C# symbol types here. + if (symbol is IParameterSymbol parameter) { - AddToGroup(SymbolDescriptionGroups.MainDescription, - Punctuation("("), - PlainText(CSharpFeaturesResources.awaitable), - Punctuation(")"), - Space()); + return GetInitializerSourcePartsAsync(parameter); } - - protected override void AddAwaitableExtensionPrefix() + else if (symbol is ILocalSymbol local) { - AddToGroup(SymbolDescriptionGroups.MainDescription, - Punctuation("("), - PlainText(CSharpFeaturesResources.awaitable_extension), - Punctuation(")"), - Space()); + return GetInitializerSourcePartsAsync(local); } - - protected override void AddEnumUnderlyingTypeSeparator() + else if (symbol is IFieldSymbol field) { - AddToGroup(SymbolDescriptionGroups.MainDescription, - Space(), - Punctuation(":"), - Space()); + return GetInitializerSourcePartsAsync(field); } - protected override Task> GetInitializerSourcePartsAsync( - ISymbol symbol) - { - // Actually check for C# symbol types here. - if (symbol is IParameterSymbol parameter) - { - return GetInitializerSourcePartsAsync(parameter); - } - else if (symbol is ILocalSymbol local) - { - return GetInitializerSourcePartsAsync(local); - } - else if (symbol is IFieldSymbol field) - { - return GetInitializerSourcePartsAsync(field); - } + return SpecializedTasks.EmptyImmutableArray(); + } - return SpecializedTasks.EmptyImmutableArray(); - } + protected override ImmutableArray ToMinimalDisplayParts(ISymbol symbol, SemanticModel semanticModel, int position, SymbolDisplayFormat format) + => CodeAnalysis.CSharp.SymbolDisplay.ToMinimalDisplayParts(symbol, semanticModel, position, format); - protected override ImmutableArray ToMinimalDisplayParts(ISymbol symbol, SemanticModel semanticModel, int position, SymbolDisplayFormat format) - => CodeAnalysis.CSharp.SymbolDisplay.ToMinimalDisplayParts(symbol, semanticModel, position, format); + protected override string? GetNavigationHint(ISymbol? symbol) + => symbol == null ? null : CodeAnalysis.CSharp.SymbolDisplay.ToDisplayString(symbol, SymbolDisplayFormat.MinimallyQualifiedFormat); - protected override string? GetNavigationHint(ISymbol symbol) - => symbol == null ? null : CodeAnalysis.CSharp.SymbolDisplay.ToDisplayString(symbol, SymbolDisplayFormat.MinimallyQualifiedFormat); + private async Task> GetInitializerSourcePartsAsync( + IFieldSymbol symbol) + { + EqualsValueClauseSyntax? initializer = null; - private async Task> GetInitializerSourcePartsAsync( - IFieldSymbol symbol) + var variableDeclarator = await GetFirstDeclarationAsync(symbol).ConfigureAwait(false); + if (variableDeclarator != null) { - EqualsValueClauseSyntax? initializer = null; - - var variableDeclarator = await GetFirstDeclarationAsync(symbol).ConfigureAwait(false); - if (variableDeclarator != null) - { - initializer = variableDeclarator.Initializer; - } - - if (initializer == null) - { - var enumMemberDeclaration = await GetFirstDeclarationAsync(symbol).ConfigureAwait(false); - if (enumMemberDeclaration != null) - { - initializer = enumMemberDeclaration.EqualsValue; - } - } - - if (initializer != null) - { - return await GetInitializerSourcePartsAsync(initializer).ConfigureAwait(false); - } - - return []; + initializer = variableDeclarator.Initializer; } - private async Task> GetInitializerSourcePartsAsync( - ILocalSymbol symbol) + if (initializer == null) { - var syntax = await GetFirstDeclarationAsync(symbol).ConfigureAwait(false); - if (syntax != null) + var enumMemberDeclaration = await GetFirstDeclarationAsync(symbol).ConfigureAwait(false); + if (enumMemberDeclaration != null) { - return await GetInitializerSourcePartsAsync(syntax.Initializer).ConfigureAwait(false); + initializer = enumMemberDeclaration.EqualsValue; } + } - return []; + if (initializer != null) + { + return await GetInitializerSourcePartsAsync(initializer).ConfigureAwait(false); } - private async Task> GetInitializerSourcePartsAsync( - IParameterSymbol symbol) + return []; + } + + private async Task> GetInitializerSourcePartsAsync( + ILocalSymbol symbol) + { + var syntax = await GetFirstDeclarationAsync(symbol).ConfigureAwait(false); + if (syntax != null) { - var syntax = await GetFirstDeclarationAsync(symbol).ConfigureAwait(false); - if (syntax != null) - { - return await GetInitializerSourcePartsAsync(syntax.Default).ConfigureAwait(false); - } + return await GetInitializerSourcePartsAsync(syntax.Initializer).ConfigureAwait(false); + } + + return []; + } - return []; + private async Task> GetInitializerSourcePartsAsync( + IParameterSymbol symbol) + { + var syntax = await GetFirstDeclarationAsync(symbol).ConfigureAwait(false); + if (syntax != null) + { + return await GetInitializerSourcePartsAsync(syntax.Default).ConfigureAwait(false); } - private async Task GetFirstDeclarationAsync(ISymbol symbol) where T : SyntaxNode + return []; + } + + private async Task GetFirstDeclarationAsync(ISymbol symbol) where T : SyntaxNode + { + foreach (var syntaxRef in symbol.DeclaringSyntaxReferences) { - foreach (var syntaxRef in symbol.DeclaringSyntaxReferences) + var syntax = await syntaxRef.GetSyntaxAsync(CancellationToken).ConfigureAwait(false); + if (syntax is T tSyntax) { - var syntax = await syntaxRef.GetSyntaxAsync(CancellationToken).ConfigureAwait(false); - if (syntax is T tSyntax) - { - return tSyntax; - } + return tSyntax; } - - return null; } - private async Task> GetInitializerSourcePartsAsync( - EqualsValueClauseSyntax? equalsValue) + return null; + } + + private async Task> GetInitializerSourcePartsAsync( + EqualsValueClauseSyntax? equalsValue) + { + if (equalsValue != null && equalsValue.Value != null) { - if (equalsValue != null && equalsValue.Value != null) + var semanticModel = GetSemanticModel(equalsValue.SyntaxTree); + if (semanticModel != null) { - var semanticModel = GetSemanticModel(equalsValue.SyntaxTree); - if (semanticModel != null) - { - return await Classifier.GetClassifiedSymbolDisplayPartsAsync( - LanguageServices, semanticModel, equalsValue.Value.Span, - Options.ClassificationOptions, cancellationToken: CancellationToken).ConfigureAwait(false); - } + return await Classifier.GetClassifiedSymbolDisplayPartsAsync( + LanguageServices, semanticModel, equalsValue.Value.Span, + Options.ClassificationOptions, cancellationToken: CancellationToken).ConfigureAwait(false); } - - return []; } - protected override void AddCaptures(ISymbol symbol) + return []; + } + + protected override void AddCaptures(ISymbol symbol) + { + if (symbol is IMethodSymbol method && method.ContainingSymbol.IsKind(SymbolKind.Method)) { - if (symbol is IMethodSymbol method && method.ContainingSymbol.IsKind(SymbolKind.Method)) + var syntax = method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(); + if (syntax.IsKind(SyntaxKind.LocalFunctionStatement) || syntax is AnonymousFunctionExpressionSyntax) { - var syntax = method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(); - if (syntax.IsKind(SyntaxKind.LocalFunctionStatement) || syntax is AnonymousFunctionExpressionSyntax) - { - AddCaptures(syntax); - } + AddCaptures(syntax); } } + } - protected override SymbolDisplayFormat MinimallyQualifiedFormat => s_minimallyQualifiedFormat; + protected override SymbolDisplayFormat MinimallyQualifiedFormat => s_minimallyQualifiedFormat; - protected override SymbolDisplayFormat MinimallyQualifiedFormatWithConstants => s_minimallyQualifiedFormatWithConstants; + protected override SymbolDisplayFormat MinimallyQualifiedFormatWithConstants => s_minimallyQualifiedFormatWithConstants; - protected override SymbolDisplayFormat MinimallyQualifiedFormatWithConstantsAndModifiers => s_minimallyQualifiedFormatWithConstantsAndModifiers; - } + protected override SymbolDisplayFormat MinimallyQualifiedFormatWithConstantsAndModifiers => s_minimallyQualifiedFormatWithConstantsAndModifiers; } } diff --git a/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayService.cs b/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayService.cs index 35be62d5f6444..8918532c97b0f 100644 --- a/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayService.cs +++ b/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayService.cs @@ -6,11 +6,10 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.Editor.CSharp.LanguageServices +namespace Microsoft.CodeAnalysis.Editor.CSharp.LanguageServices; + +internal partial class CSharpSymbolDisplayService(Host.LanguageServices services) : AbstractSymbolDisplayService(services) { - internal partial class CSharpSymbolDisplayService(Host.LanguageServices services) : AbstractSymbolDisplayService(services) - { - protected override AbstractSymbolDescriptionBuilder CreateDescriptionBuilder(SemanticModel semanticModel, int position, SymbolDescriptionOptions options, CancellationToken cancellationToken) - => new SymbolDescriptionBuilder(semanticModel, position, LanguageServices, options, cancellationToken); - } + protected override AbstractSymbolDescriptionBuilder CreateDescriptionBuilder(SemanticModel semanticModel, int position, SymbolDescriptionOptions options, CancellationToken cancellationToken) + => new SymbolDescriptionBuilder(semanticModel, position, LanguageServices, options, cancellationToken); } diff --git a/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayServiceFactory.cs b/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayServiceFactory.cs index 4b9ed95474124..e3cbacffe323d 100644 --- a/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayServiceFactory.cs +++ b/src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayServiceFactory.cs @@ -10,18 +10,17 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.Editor.CSharp.LanguageServices +namespace Microsoft.CodeAnalysis.Editor.CSharp.LanguageServices; + +[ExportLanguageServiceFactory(typeof(ISymbolDisplayService), LanguageNames.CSharp), Shared] +internal partial class CSharpSymbolDisplayServiceFactory : ILanguageServiceFactory { - [ExportLanguageServiceFactory(typeof(ISymbolDisplayService), LanguageNames.CSharp), Shared] - internal partial class CSharpSymbolDisplayServiceFactory : ILanguageServiceFactory + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpSymbolDisplayServiceFactory() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpSymbolDisplayServiceFactory() - { - } - - public ILanguageService CreateLanguageService(HostLanguageServices provider) - => new CSharpSymbolDisplayService(provider.LanguageServices); } + + public ILanguageService CreateLanguageService(HostLanguageServices provider) + => new CSharpSymbolDisplayService(provider.LanguageServices); } diff --git a/src/Features/CSharp/Portable/LineSeparators/CSharpLineSeparatorService.cs b/src/Features/CSharp/Portable/LineSeparators/CSharpLineSeparatorService.cs index 001f5c56c88ab..75f87a9720437 100644 --- a/src/Features/CSharp/Portable/LineSeparators/CSharpLineSeparatorService.cs +++ b/src/Features/CSharp/Portable/LineSeparators/CSharpLineSeparatorService.cs @@ -17,339 +17,338 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.LineSeparators +namespace Microsoft.CodeAnalysis.CSharp.LineSeparators; + +[ExportLanguageService(typeof(ILineSeparatorService), LanguageNames.CSharp), Shared] +internal class CSharpLineSeparatorService : ILineSeparatorService { - [ExportLanguageService(typeof(ILineSeparatorService), LanguageNames.CSharp), Shared] - internal class CSharpLineSeparatorService : ILineSeparatorService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpLineSeparatorService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpLineSeparatorService() - { - } + } - /// - /// Given a tree returns line separator spans. - /// The operation may take fairly long time on a big tree so it is cancellable. - /// - public async Task> GetLineSeparatorsAsync( - Document document, - TextSpan textSpan, - CancellationToken cancellationToken) - { - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var node = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var spans); + /// + /// Given a tree returns line separator spans. + /// The operation may take fairly long time on a big tree so it is cancellable. + /// + public async Task> GetLineSeparatorsAsync( + Document document, + TextSpan textSpan, + CancellationToken cancellationToken) + { + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var node = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); + using var _ = ArrayBuilder.GetInstance(out var spans); - var blocks = node.Traverse(textSpan, IsSeparableContainer); + var blocks = node.Traverse(textSpan, IsSeparableContainer); - foreach (var block in blocks) - { - if (cancellationToken.IsCancellationRequested) - return []; + foreach (var block in blocks) + { + if (cancellationToken.IsCancellationRequested) + return []; - switch (block) - { - case TypeDeclarationSyntax typeBlock: - ProcessNodeList(typeBlock.Members, spans, cancellationToken); - continue; - case BaseNamespaceDeclarationSyntax namespaceBlock: - ProcessUsings(namespaceBlock.Usings, spans, cancellationToken); - ProcessNodeList(namespaceBlock.Members, spans, cancellationToken); - continue; - case CompilationUnitSyntax progBlock: - ProcessUsings(progBlock.Usings, spans, cancellationToken); - ProcessNodeList(progBlock.Members, spans, cancellationToken); - break; - } + switch (block) + { + case TypeDeclarationSyntax typeBlock: + ProcessNodeList(typeBlock.Members, spans, cancellationToken); + continue; + case BaseNamespaceDeclarationSyntax namespaceBlock: + ProcessUsings(namespaceBlock.Usings, spans, cancellationToken); + ProcessNodeList(namespaceBlock.Members, spans, cancellationToken); + continue; + case CompilationUnitSyntax progBlock: + ProcessUsings(progBlock.Usings, spans, cancellationToken); + ProcessNodeList(progBlock.Members, spans, cancellationToken); + break; } + } + + return spans.ToImmutable(); + } - return spans.ToImmutable(); + /// Node types that are interesting for line separation. + private static bool IsSeparableBlock(SyntaxNode node) + { + if (SyntaxFacts.IsTypeDeclaration(node.Kind())) + { + return true; } - /// Node types that are interesting for line separation. - private static bool IsSeparableBlock(SyntaxNode node) + switch (node.Kind()) { - if (SyntaxFacts.IsTypeDeclaration(node.Kind())) - { + case SyntaxKind.NamespaceDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.EventDeclaration: + case SyntaxKind.IndexerDeclaration: + case SyntaxKind.ConstructorDeclaration: + case SyntaxKind.DestructorDeclaration: + case SyntaxKind.OperatorDeclaration: + case SyntaxKind.ConversionOperatorDeclaration: return true; - } - switch (node.Kind()) - { - case SyntaxKind.NamespaceDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.EventDeclaration: - case SyntaxKind.IndexerDeclaration: - case SyntaxKind.ConstructorDeclaration: - case SyntaxKind.DestructorDeclaration: - case SyntaxKind.OperatorDeclaration: - case SyntaxKind.ConversionOperatorDeclaration: - return true; - - default: - return false; - } + default: + return false; } + } - /// Node types that may contain separable blocks. - private static bool IsSeparableContainer(SyntaxNode node) - => node is TypeDeclarationSyntax or BaseNamespaceDeclarationSyntax or CompilationUnitSyntax; + /// Node types that may contain separable blocks. + private static bool IsSeparableContainer(SyntaxNode node) + => node is TypeDeclarationSyntax or BaseNamespaceDeclarationSyntax or CompilationUnitSyntax; - private static bool IsBadType(SyntaxNode node) + private static bool IsBadType(SyntaxNode node) + { + if (node is TypeDeclarationSyntax typeDecl) { - if (node is TypeDeclarationSyntax typeDecl) + if (typeDecl.OpenBraceToken.IsMissing || + typeDecl.CloseBraceToken.IsMissing) { - if (typeDecl.OpenBraceToken.IsMissing || - typeDecl.CloseBraceToken.IsMissing) - { - return true; - } + return true; } - - return false; } - private static bool IsBadEnum(SyntaxNode node) + return false; + } + + private static bool IsBadEnum(SyntaxNode node) + { + if (node is EnumDeclarationSyntax enumDecl) { - if (node is EnumDeclarationSyntax enumDecl) + if (enumDecl.OpenBraceToken.IsMissing || + enumDecl.CloseBraceToken.IsMissing) { - if (enumDecl.OpenBraceToken.IsMissing || - enumDecl.CloseBraceToken.IsMissing) - { - return true; - } + return true; } - - return false; } - private static bool IsBadMethod(SyntaxNode node) + return false; + } + + private static bool IsBadMethod(SyntaxNode node) + { + if (node is MethodDeclarationSyntax methodDecl) { - if (node is MethodDeclarationSyntax methodDecl) + if (methodDecl.Body != null && + (methodDecl.Body.OpenBraceToken.IsMissing || + methodDecl.Body.CloseBraceToken.IsMissing)) { - if (methodDecl.Body != null && - (methodDecl.Body.OpenBraceToken.IsMissing || - methodDecl.Body.CloseBraceToken.IsMissing)) - { - return true; - } + return true; } - - return false; } - private static bool IsBadProperty(SyntaxNode node) - => IsBadAccessorList(node as PropertyDeclarationSyntax); + return false; + } - private static bool IsBadEvent(SyntaxNode node) - => IsBadAccessorList(node as EventDeclarationSyntax); + private static bool IsBadProperty(SyntaxNode node) + => IsBadAccessorList(node as PropertyDeclarationSyntax); - private static bool IsBadIndexer(SyntaxNode node) - => IsBadAccessorList(node as IndexerDeclarationSyntax); + private static bool IsBadEvent(SyntaxNode node) + => IsBadAccessorList(node as EventDeclarationSyntax); - private static bool IsBadAccessorList(BasePropertyDeclarationSyntax? baseProperty) - { - if (baseProperty?.AccessorList == null) - return false; + private static bool IsBadIndexer(SyntaxNode node) + => IsBadAccessorList(node as IndexerDeclarationSyntax); - return baseProperty.AccessorList.OpenBraceToken.IsMissing || - baseProperty.AccessorList.CloseBraceToken.IsMissing; - } + private static bool IsBadAccessorList(BasePropertyDeclarationSyntax? baseProperty) + { + if (baseProperty?.AccessorList == null) + return false; + + return baseProperty.AccessorList.OpenBraceToken.IsMissing || + baseProperty.AccessorList.CloseBraceToken.IsMissing; + } - private static bool IsBadConstructor(SyntaxNode node) + private static bool IsBadConstructor(SyntaxNode node) + { + if (node is ConstructorDeclarationSyntax constructorDecl) { - if (node is ConstructorDeclarationSyntax constructorDecl) + if (constructorDecl.Body != null && + (constructorDecl.Body.OpenBraceToken.IsMissing || + constructorDecl.Body.CloseBraceToken.IsMissing)) { - if (constructorDecl.Body != null && - (constructorDecl.Body.OpenBraceToken.IsMissing || - constructorDecl.Body.CloseBraceToken.IsMissing)) - { - return true; - } + return true; } - - return false; } - private static bool IsBadDestructor(SyntaxNode node) + return false; + } + + private static bool IsBadDestructor(SyntaxNode node) + { + if (node is DestructorDeclarationSyntax destructorDecl) { - if (node is DestructorDeclarationSyntax destructorDecl) + if (destructorDecl.Body != null && + (destructorDecl.Body.OpenBraceToken.IsMissing || + destructorDecl.Body.CloseBraceToken.IsMissing)) { - if (destructorDecl.Body != null && - (destructorDecl.Body.OpenBraceToken.IsMissing || - destructorDecl.Body.CloseBraceToken.IsMissing)) - { - return true; - } + return true; } - - return false; } - private static bool IsBadOperator(SyntaxNode node) + return false; + } + + private static bool IsBadOperator(SyntaxNode node) + { + if (node is OperatorDeclarationSyntax operatorDecl) { - if (node is OperatorDeclarationSyntax operatorDecl) + if (operatorDecl.Body != null && + (operatorDecl.Body.OpenBraceToken.IsMissing || + operatorDecl.Body.CloseBraceToken.IsMissing)) { - if (operatorDecl.Body != null && - (operatorDecl.Body.OpenBraceToken.IsMissing || - operatorDecl.Body.CloseBraceToken.IsMissing)) - { - return true; - } + return true; } - - return false; } - private static bool IsBadConversionOperator(SyntaxNode node) + return false; + } + + private static bool IsBadConversionOperator(SyntaxNode node) + { + if (node is ConversionOperatorDeclarationSyntax conversionDecl) { - if (node is ConversionOperatorDeclarationSyntax conversionDecl) + if (conversionDecl.Body != null && + (conversionDecl.Body.OpenBraceToken.IsMissing || + conversionDecl.Body.CloseBraceToken.IsMissing)) { - if (conversionDecl.Body != null && - (conversionDecl.Body.OpenBraceToken.IsMissing || - conversionDecl.Body.CloseBraceToken.IsMissing)) - { - return true; - } + return true; } + } - return false; + return false; + } + + private static bool IsBadNode(SyntaxNode node) + { + if (node is IncompleteMemberSyntax) + { + return true; } - private static bool IsBadNode(SyntaxNode node) + if (IsBadType(node) || + IsBadEnum(node) || + IsBadMethod(node) || + IsBadProperty(node) || + IsBadEvent(node) || + IsBadIndexer(node) || + IsBadConstructor(node) || + IsBadDestructor(node) || + IsBadOperator(node) || + IsBadConversionOperator(node)) { - if (node is IncompleteMemberSyntax) - { - return true; - } + return true; + } - if (IsBadType(node) || - IsBadEnum(node) || - IsBadMethod(node) || - IsBadProperty(node) || - IsBadEvent(node) || - IsBadIndexer(node) || - IsBadConstructor(node) || - IsBadDestructor(node) || - IsBadOperator(node) || - IsBadConversionOperator(node)) - { - return true; - } + return false; + } - return false; - } + private static void ProcessUsings(SyntaxList usings, ArrayBuilder spans, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(spans); - private static void ProcessUsings(SyntaxList usings, ArrayBuilder spans, CancellationToken cancellationToken) + if (usings.Any()) { - Contract.ThrowIfNull(spans); + AddLineSeparatorSpanForNode(usings.Last(), spans, cancellationToken); + } + } - if (usings.Any()) - { - AddLineSeparatorSpanForNode(usings.Last(), spans, cancellationToken); - } + /// + /// If node is separable and not the last in its container => add line separator after the node + /// If node is separable and not the first in its container => ensure separator before the node + /// last separable node in Program needs separator after it. + /// + private static void ProcessNodeList(SyntaxList children, ArrayBuilder spans, CancellationToken cancellationToken) where T : SyntaxNode + { + Contract.ThrowIfNull(spans); + + if (children.Count == 0) + { + // nothing to separate + return; } - /// - /// If node is separable and not the last in its container => add line separator after the node - /// If node is separable and not the first in its container => ensure separator before the node - /// last separable node in Program needs separator after it. - /// - private static void ProcessNodeList(SyntaxList children, ArrayBuilder spans, CancellationToken cancellationToken) where T : SyntaxNode + // first child needs no separator + var seenSeparator = true; + for (var i = 0; i < children.Count - 1; i++) { - Contract.ThrowIfNull(spans); + cancellationToken.ThrowIfCancellationRequested(); - if (children.Count == 0) - { - // nothing to separate - return; - } + var cur = children[i]; - // first child needs no separator - var seenSeparator = true; - for (var i = 0; i < children.Count - 1; i++) + if (!IsSeparableBlock(cur)) { - cancellationToken.ThrowIfCancellationRequested(); - - var cur = children[i]; - - if (!IsSeparableBlock(cur)) - { - seenSeparator = false; - } - else - { - if (!seenSeparator) - { - var prev = children[i - 1]; - AddLineSeparatorSpanForNode(prev, spans, cancellationToken); - } - - AddLineSeparatorSpanForNode(cur, spans, cancellationToken); - seenSeparator = true; - } + seenSeparator = false; } - - // last child may need separator only before it - var lastChild = children.Last(); - - if (IsSeparableBlock(lastChild)) + else { if (!seenSeparator) { - var nextToLast = children[children.Count - 2]; - AddLineSeparatorSpanForNode(nextToLast, spans, cancellationToken); + var prev = children[i - 1]; + AddLineSeparatorSpanForNode(prev, spans, cancellationToken); } - if (lastChild.IsParentKind(SyntaxKind.CompilationUnit)) - { - AddLineSeparatorSpanForNode(lastChild, spans, cancellationToken); - } + AddLineSeparatorSpanForNode(cur, spans, cancellationToken); + seenSeparator = true; } } - private static void AddLineSeparatorSpanForNode(SyntaxNode node, ArrayBuilder spans, CancellationToken cancellationToken) + // last child may need separator only before it + var lastChild = children.Last(); + + if (IsSeparableBlock(lastChild)) { - if (IsBadNode(node)) + if (!seenSeparator) { - return; + var nextToLast = children[children.Count - 2]; + AddLineSeparatorSpanForNode(nextToLast, spans, cancellationToken); } - var span = GetLineSeparatorSpanForNode(node); - - if (IsLegalSpanForLineSeparator(node.SyntaxTree, span, cancellationToken)) + if (lastChild.IsParentKind(SyntaxKind.CompilationUnit)) { - spans.Add(span); + AddLineSeparatorSpanForNode(lastChild, spans, cancellationToken); } } + } - private static bool IsLegalSpanForLineSeparator(SyntaxTree syntaxTree, TextSpan textSpan, CancellationToken cancellationToken) + private static void AddLineSeparatorSpanForNode(SyntaxNode node, ArrayBuilder spans, CancellationToken cancellationToken) + { + if (IsBadNode(node)) { - // A span is a legal location for a line separator if the following line - // contains only whitespace or the span is the last line in the buffer. + return; + } - var line = syntaxTree.GetText(cancellationToken).Lines.IndexOf(textSpan.End); - if (line == syntaxTree.GetText(cancellationToken).Lines.Count - 1) - { - return true; - } + var span = GetLineSeparatorSpanForNode(node); - if (string.IsNullOrWhiteSpace(syntaxTree.GetText(cancellationToken).Lines[line + 1].ToString())) - { - return true; - } + if (IsLegalSpanForLineSeparator(node.SyntaxTree, span, cancellationToken)) + { + spans.Add(span); + } + } - return false; + private static bool IsLegalSpanForLineSeparator(SyntaxTree syntaxTree, TextSpan textSpan, CancellationToken cancellationToken) + { + // A span is a legal location for a line separator if the following line + // contains only whitespace or the span is the last line in the buffer. + + var line = syntaxTree.GetText(cancellationToken).Lines.IndexOf(textSpan.End); + if (line == syntaxTree.GetText(cancellationToken).Lines.Count - 1) + { + return true; } - private static TextSpan GetLineSeparatorSpanForNode(SyntaxNode node) + if (string.IsNullOrWhiteSpace(syntaxTree.GetText(cancellationToken).Lines[line + 1].ToString())) { - // we only want to underline the node with a long line - // for this purpose the last token is as good as the whole node, but has - // simpler and typically single line geometry (so it will be easier to find "bottom") - return node.GetLastToken().Span; + return true; } + + return false; + } + + private static TextSpan GetLineSeparatorSpanForNode(SyntaxNode node) + { + // we only want to underline the node with a long line + // for this purpose the last token is as good as the whole node, but has + // simpler and typically single line geometry (so it will be easier to find "bottom") + return node.GetLastToken().Span; } } diff --git a/src/Features/CSharp/Portable/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeRefactoringProvider.cs index a9ecaa61c5be8..152fd933329a5 100644 --- a/src/Features/CSharp/Portable/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/MakeLocalFunctionStatic/MakeLocalFunctionStaticCodeRefactoringProvider.cs @@ -11,41 +11,40 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic +namespace Microsoft.CodeAnalysis.CSharp.MakeLocalFunctionStatic; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MakeLocalFunctionStatic), Shared] +internal sealed class MakeLocalFunctionStaticCodeRefactoringProvider : CodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MakeLocalFunctionStatic), Shared] - internal sealed class MakeLocalFunctionStaticCodeRefactoringProvider : CodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public MakeLocalFunctionStaticCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public MakeLocalFunctionStaticCodeRefactoringProvider() - { - } + } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, textSpan, cancellationToken) = context; + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, textSpan, cancellationToken) = context; - var syntaxTree = (await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false))!; - if (!MakeLocalFunctionStaticHelper.IsStaticLocalFunctionSupported(syntaxTree.Options.LanguageVersion())) - return; + var syntaxTree = (await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false))!; + if (!MakeLocalFunctionStaticHelper.IsStaticLocalFunctionSupported(syntaxTree.Options.LanguageVersion())) + return; - var localFunction = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (localFunction == null) - return; + var localFunction = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (localFunction == null) + return; - if (localFunction.Modifiers.Any(SyntaxKind.StaticKeyword)) - return; + if (localFunction.Modifiers.Any(SyntaxKind.StaticKeyword)) + return; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (MakeLocalFunctionStaticHelper.CanMakeLocalFunctionStaticByRefactoringCaptures(localFunction, semanticModel, out var captures)) - { - context.RegisterRefactoring(CodeAction.Create( - CSharpAnalyzersResources.Make_local_function_static, - cancellationToken => MakeLocalFunctionStaticCodeFixHelper.MakeLocalFunctionStaticAsync(document, localFunction, captures, context.Options, cancellationToken), - nameof(CSharpAnalyzersResources.Make_local_function_static))); - } + if (MakeLocalFunctionStaticHelper.CanMakeLocalFunctionStaticByRefactoringCaptures(localFunction, semanticModel, out var captures)) + { + context.RegisterRefactoring(CodeAction.Create( + CSharpAnalyzersResources.Make_local_function_static, + cancellationToken => MakeLocalFunctionStaticCodeFixHelper.MakeLocalFunctionStaticAsync(document, localFunction, captures, context.Options, cancellationToken), + nameof(CSharpAnalyzersResources.Make_local_function_static))); } } } diff --git a/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceService.cs b/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceService.cs index b351cdc7b1f9c..e045c730bcf0a 100644 --- a/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceService.cs +++ b/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceService.cs @@ -23,212 +23,211 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.MetadataAsSource +namespace Microsoft.CodeAnalysis.CSharp.MetadataAsSource; + +internal partial class CSharpMetadataAsSourceService : AbstractMetadataAsSourceService { - internal partial class CSharpMetadataAsSourceService : AbstractMetadataAsSourceService + private static readonly AbstractFormattingRule s_memberSeparationRule = new FormattingRule(); + public static readonly CSharpMetadataAsSourceService Instance = new CSharpMetadataAsSourceService(); + + private CSharpMetadataAsSourceService() { - private static readonly AbstractFormattingRule s_memberSeparationRule = new FormattingRule(); - public static readonly CSharpMetadataAsSourceService Instance = new CSharpMetadataAsSourceService(); + } - private CSharpMetadataAsSourceService() - { - } + protected override async Task AddAssemblyInfoRegionAsync(Document document, Compilation symbolCompilation, ISymbol symbol, CancellationToken cancellationToken) + { + var assemblyInfo = MetadataAsSourceHelpers.GetAssemblyInfo(symbol.ContainingAssembly); + var assemblyPath = MetadataAsSourceHelpers.GetAssemblyDisplay(symbolCompilation, symbol.ContainingAssembly); + + var regionTrivia = SyntaxFactory.RegionDirectiveTrivia(true) + .WithTrailingTrivia(new[] { SyntaxFactory.Space, SyntaxFactory.PreprocessingMessage(assemblyInfo) }); + + var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = oldRoot.WithPrependedLeadingTrivia( + SyntaxFactory.Trivia(regionTrivia), + SyntaxFactory.CarriageReturnLineFeed, + SyntaxFactory.Comment("// " + assemblyPath), + SyntaxFactory.CarriageReturnLineFeed, + SyntaxFactory.Trivia(SyntaxFactory.EndRegionDirectiveTrivia(true)), + SyntaxFactory.CarriageReturnLineFeed, + SyntaxFactory.CarriageReturnLineFeed); + + return document.WithSyntaxRoot(newRoot); + } - protected override async Task AddAssemblyInfoRegionAsync(Document document, Compilation symbolCompilation, ISymbol symbol, CancellationToken cancellationToken) - { - var assemblyInfo = MetadataAsSourceHelpers.GetAssemblyInfo(symbol.ContainingAssembly); - var assemblyPath = MetadataAsSourceHelpers.GetAssemblyDisplay(symbolCompilation, symbol.ContainingAssembly); - - var regionTrivia = SyntaxFactory.RegionDirectiveTrivia(true) - .WithTrailingTrivia(new[] { SyntaxFactory.Space, SyntaxFactory.PreprocessingMessage(assemblyInfo) }); - - var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newRoot = oldRoot.WithPrependedLeadingTrivia( - SyntaxFactory.Trivia(regionTrivia), - SyntaxFactory.CarriageReturnLineFeed, - SyntaxFactory.Comment("// " + assemblyPath), - SyntaxFactory.CarriageReturnLineFeed, - SyntaxFactory.Trivia(SyntaxFactory.EndRegionDirectiveTrivia(true)), - SyntaxFactory.CarriageReturnLineFeed, - SyntaxFactory.CarriageReturnLineFeed); - - return document.WithSyntaxRoot(newRoot); - } + protected override IEnumerable GetFormattingRules(Document document) + => s_memberSeparationRule.Concat(Formatter.GetDefaultFormattingRules(document)); - protected override IEnumerable GetFormattingRules(Document document) - => s_memberSeparationRule.Concat(Formatter.GetDefaultFormattingRules(document)); + protected override async Task ConvertDocCommentsToRegularCommentsAsync(Document document, IDocumentationCommentFormattingService docCommentFormattingService, CancellationToken cancellationToken) + { + var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - protected override async Task ConvertDocCommentsToRegularCommentsAsync(Document document, IDocumentationCommentFormattingService docCommentFormattingService, CancellationToken cancellationToken) - { - var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newSyntaxRoot = DocCommentConverter.ConvertToRegularComments(syntaxRoot, docCommentFormattingService, cancellationToken); - var newSyntaxRoot = DocCommentConverter.ConvertToRegularComments(syntaxRoot, docCommentFormattingService, cancellationToken); + return document.WithSyntaxRoot(newSyntaxRoot); + } - return document.WithSyntaxRoot(newSyntaxRoot); - } + protected override ImmutableArray GetReducers() + => [ + new CSharpNameReducer(), + new CSharpEscapingReducer(), + new CSharpParenthesizedExpressionReducer(), + new CSharpParenthesizedPatternReducer(), + new CSharpDefaultExpressionReducer(), + ]; + + /// + /// Adds #nullable enable and #nullable disable annotations to the file as necessary. Note that + /// this does not try to be 100% accurate, but rather it handles the most common cases out there. Specifically, + /// if a file contains any nullable annotated/not-annotated types, then we prefix the file with #nullable + /// enable. Then if we hit any members that explicitly have *oblivious* types, but no annotated or + /// non-annotated types, then we switch to #nullable disable for those specific members. + /// + /// This is technically innacurate for possible, but very uncommon cases. For example, if the user's code + /// explicitly did something like this: + /// + /// + /// public void Goo(string goo, + /// #nullable disable + /// string bar + /// #nullable enable + /// string baz); + /// + /// + /// Then we would be unable to handle that. However, this is highly unlikely to happen, and so we accept the + /// inaccuracy for the purpose of simplicity and for handling the much more common cases of either the entire + /// file being annotated, or the user individually disabling annotations at the member level. + /// + protected override async Task AddNullableRegionsAsync(Document document, CancellationToken cancellationToken) + { + var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var options = (CSharpParseOptions)tree.Options; - protected override ImmutableArray GetReducers() - => [ - new CSharpNameReducer(), - new CSharpEscapingReducer(), - new CSharpParenthesizedExpressionReducer(), - new CSharpParenthesizedPatternReducer(), - new CSharpDefaultExpressionReducer(), - ]; - - /// - /// Adds #nullable enable and #nullable disable annotations to the file as necessary. Note that - /// this does not try to be 100% accurate, but rather it handles the most common cases out there. Specifically, - /// if a file contains any nullable annotated/not-annotated types, then we prefix the file with #nullable - /// enable. Then if we hit any members that explicitly have *oblivious* types, but no annotated or - /// non-annotated types, then we switch to #nullable disable for those specific members. - /// - /// This is technically innacurate for possible, but very uncommon cases. For example, if the user's code - /// explicitly did something like this: - /// - /// - /// public void Goo(string goo, - /// #nullable disable - /// string bar - /// #nullable enable - /// string baz); - /// - /// - /// Then we would be unable to handle that. However, this is highly unlikely to happen, and so we accept the - /// inaccuracy for the purpose of simplicity and for handling the much more common cases of either the entire - /// file being annotated, or the user individually disabling annotations at the member level. - /// - protected override async Task AddNullableRegionsAsync(Document document, CancellationToken cancellationToken) - { - var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var options = (CSharpParseOptions)tree.Options; + // Only valid for C# 8 and above. + if (options.LanguageVersion < LanguageVersion.CSharp8) + return document; - // Only valid for C# 8 and above. - if (options.LanguageVersion < LanguageVersion.CSharp8) - return document; + var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var (_, annotatedOrNotAnnotated) = GetNullableAnnotations(root); - var (_, annotatedOrNotAnnotated) = GetNullableAnnotations(root); + // If there are no annotated or not-annotated types, then no need to add `#nullable enable`. + if (!annotatedOrNotAnnotated) + return document; - // If there are no annotated or not-annotated types, then no need to add `#nullable enable`. - if (!annotatedOrNotAnnotated) - return document; + var newRoot = AddNullableRegions(root, cancellationToken); + newRoot = newRoot.WithPrependedLeadingTrivia(CreateNullableTrivia(enable: true)); - var newRoot = AddNullableRegions(root, cancellationToken); - newRoot = newRoot.WithPrependedLeadingTrivia(CreateNullableTrivia(enable: true)); + return document.WithSyntaxRoot(newRoot); + } - return document.WithSyntaxRoot(newRoot); - } + private static (bool oblivious, bool annotatedOrNotAnnotated) GetNullableAnnotations(SyntaxNode node) + { + return (HasAnnotation(node, NullableSyntaxAnnotation.Oblivious), + HasAnnotation(node, NullableSyntaxAnnotation.AnnotatedOrNotAnnotated)); + } - private static (bool oblivious, bool annotatedOrNotAnnotated) GetNullableAnnotations(SyntaxNode node) - { - return (HasAnnotation(node, NullableSyntaxAnnotation.Oblivious), - HasAnnotation(node, NullableSyntaxAnnotation.AnnotatedOrNotAnnotated)); - } + private static bool HasAnnotation(SyntaxNode node, SyntaxAnnotation annotation) + { + // see if any child nodes have this annotation. Ignore anything in attributes (like `[Obsolete]void Goo()` + // as these are not impacted by `#nullable` regions. Instead, we only care about signature types. + var annotatedChildren = node.GetAnnotatedNodes(annotation); + return annotatedChildren.Any(n => n.GetAncestorOrThis() == null); + } - private static bool HasAnnotation(SyntaxNode node, SyntaxAnnotation annotation) - { - // see if any child nodes have this annotation. Ignore anything in attributes (like `[Obsolete]void Goo()` - // as these are not impacted by `#nullable` regions. Instead, we only care about signature types. - var annotatedChildren = node.GetAnnotatedNodes(annotation); - return annotatedChildren.Any(n => n.GetAncestorOrThis() == null); - } + private static SyntaxTrivia[] CreateNullableTrivia(bool enable) + { + var keyword = enable ? SyntaxKind.EnableKeyword : SyntaxKind.DisableKeyword; + return + [ + SyntaxFactory.Trivia(SyntaxFactory.NullableDirectiveTrivia(SyntaxFactory.Token(keyword), isActive: enable)), + SyntaxFactory.ElasticCarriageReturnLineFeed, + SyntaxFactory.ElasticCarriageReturnLineFeed, + ]; + } - private static SyntaxTrivia[] CreateNullableTrivia(bool enable) + private TSyntax AddNullableRegions(TSyntax node, CancellationToken cancellationToken) + where TSyntax : SyntaxNode + { + return node switch { - var keyword = enable ? SyntaxKind.EnableKeyword : SyntaxKind.DisableKeyword; - return - [ - SyntaxFactory.Trivia(SyntaxFactory.NullableDirectiveTrivia(SyntaxFactory.Token(keyword), isActive: enable)), - SyntaxFactory.ElasticCarriageReturnLineFeed, - SyntaxFactory.ElasticCarriageReturnLineFeed, - ]; - } + CompilationUnitSyntax compilationUnit => (TSyntax)(object)compilationUnit.WithMembers(AddNullableRegions(compilationUnit.Members, cancellationToken)), + NamespaceDeclarationSyntax ns => (TSyntax)(object)ns.WithMembers(AddNullableRegions(ns.Members, cancellationToken)), + TypeDeclarationSyntax type => (TSyntax)(object)AddNullableRegionsAroundTypeMembers(type, cancellationToken), + _ => node, + }; + } - private TSyntax AddNullableRegions(TSyntax node, CancellationToken cancellationToken) - where TSyntax : SyntaxNode - { - return node switch - { - CompilationUnitSyntax compilationUnit => (TSyntax)(object)compilationUnit.WithMembers(AddNullableRegions(compilationUnit.Members, cancellationToken)), - NamespaceDeclarationSyntax ns => (TSyntax)(object)ns.WithMembers(AddNullableRegions(ns.Members, cancellationToken)), - TypeDeclarationSyntax type => (TSyntax)(object)AddNullableRegionsAroundTypeMembers(type, cancellationToken), - _ => node, - }; - } + private SyntaxList AddNullableRegions( + SyntaxList members, + CancellationToken cancellationToken) + { + return [.. members.Select(m => AddNullableRegions(m, cancellationToken))]; + } - private SyntaxList AddNullableRegions( - SyntaxList members, - CancellationToken cancellationToken) - { - return [.. members.Select(m => AddNullableRegions(m, cancellationToken))]; - } + private TypeDeclarationSyntax AddNullableRegionsAroundTypeMembers( + TypeDeclarationSyntax type, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var builder); - private TypeDeclarationSyntax AddNullableRegionsAroundTypeMembers( - TypeDeclarationSyntax type, CancellationToken cancellationToken) - { - using var _ = ArrayBuilder.GetInstance(out var builder); + var currentlyEnabled = true; - var currentlyEnabled = true; + foreach (var member in type.Members) + { + cancellationToken.ThrowIfCancellationRequested(); - foreach (var member in type.Members) + if (member is BaseTypeDeclarationSyntax) { - cancellationToken.ThrowIfCancellationRequested(); - - if (member is BaseTypeDeclarationSyntax) - { - // if we hit a type, and we're currently disabled, then switch us back to enabled for that type. - // This ensures whenever we walk into a type-decl, we're always in the enabled-state. - builder.Add(TransitionTo(AddNullableRegions(member, cancellationToken), enabled: true, ref currentlyEnabled)); - continue; - } - - // we hit a member. see what sort of types it contained. - var (oblivious, annotatedOrNotAnnotated) = GetNullableAnnotations(member); - - // if we have null annotations, transition us back to the enabled state - if (annotatedOrNotAnnotated) - { - builder.Add(TransitionTo(member, enabled: true, ref currentlyEnabled)); - } - else if (oblivious) - { - // if we didn't have null annotations, and we had an explicit oblivious type, - // then definitely transition us to the disabled state - builder.Add(TransitionTo(member, enabled: false, ref currentlyEnabled)); - } - else - { - // had no types at all. no need to change state. - builder.Add(member); - } + // if we hit a type, and we're currently disabled, then switch us back to enabled for that type. + // This ensures whenever we walk into a type-decl, we're always in the enabled-state. + builder.Add(TransitionTo(AddNullableRegions(member, cancellationToken), enabled: true, ref currentlyEnabled)); + continue; } - var result = type.WithMembers([.. builder]); - if (!currentlyEnabled) + // we hit a member. see what sort of types it contained. + var (oblivious, annotatedOrNotAnnotated) = GetNullableAnnotations(member); + + // if we have null annotations, transition us back to the enabled state + if (annotatedOrNotAnnotated) { - // switch us back to enabled as we leave the type. - result = result.WithCloseBraceToken( - result.CloseBraceToken.WithPrependedLeadingTrivia(CreateNullableTrivia(enable: true))); + builder.Add(TransitionTo(member, enabled: true, ref currentlyEnabled)); } - - return result; - } - - private static MemberDeclarationSyntax TransitionTo(MemberDeclarationSyntax member, bool enabled, ref bool currentlyEnabled) - { - if (enabled == currentlyEnabled) + else if (oblivious) { - // already in the right state. don't start a #nullable region - return member; + // if we didn't have null annotations, and we had an explicit oblivious type, + // then definitely transition us to the disabled state + builder.Add(TransitionTo(member, enabled: false, ref currentlyEnabled)); } else { - // switch to the desired state and add the right trivia to the node. - currentlyEnabled = enabled; - return member.WithPrependedLeadingTrivia(CreateNullableTrivia(currentlyEnabled)); + // had no types at all. no need to change state. + builder.Add(member); } } + + var result = type.WithMembers([.. builder]); + if (!currentlyEnabled) + { + // switch us back to enabled as we leave the type. + result = result.WithCloseBraceToken( + result.CloseBraceToken.WithPrependedLeadingTrivia(CreateNullableTrivia(enable: true))); + } + + return result; + } + + private static MemberDeclarationSyntax TransitionTo(MemberDeclarationSyntax member, bool enabled, ref bool currentlyEnabled) + { + if (enabled == currentlyEnabled) + { + // already in the right state. don't start a #nullable region + return member; + } + else + { + // switch to the desired state and add the right trivia to the node. + currentlyEnabled = enabled; + return member.WithPrependedLeadingTrivia(CreateNullableTrivia(currentlyEnabled)); + } } } diff --git a/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceServiceFactory.cs b/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceServiceFactory.cs index b3df60eb6d3de..1c389bbc8b280 100644 --- a/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceServiceFactory.cs +++ b/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceServiceFactory.cs @@ -10,18 +10,17 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.MetadataAsSource; -namespace Microsoft.CodeAnalysis.CSharp.MetadataAsSource +namespace Microsoft.CodeAnalysis.CSharp.MetadataAsSource; + +[ExportLanguageServiceFactory(typeof(IMetadataAsSourceService), LanguageNames.CSharp), Shared] +internal class CSharpMetadataAsSourceServiceFactory : ILanguageServiceFactory { - [ExportLanguageServiceFactory(typeof(IMetadataAsSourceService), LanguageNames.CSharp), Shared] - internal class CSharpMetadataAsSourceServiceFactory : ILanguageServiceFactory + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpMetadataAsSourceServiceFactory() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpMetadataAsSourceServiceFactory() - { - } - - public ILanguageService CreateLanguageService(HostLanguageServices provider) - => CSharpMetadataAsSourceService.Instance; } + + public ILanguageService CreateLanguageService(HostLanguageServices provider) + => CSharpMetadataAsSourceService.Instance; } diff --git a/src/Features/CSharp/Portable/MetadataAsSource/FormattingRule.cs b/src/Features/CSharp/Portable/MetadataAsSource/FormattingRule.cs index 82ba90867b1cb..3c127e1f02910 100644 --- a/src/Features/CSharp/Portable/MetadataAsSource/FormattingRule.cs +++ b/src/Features/CSharp/Portable/MetadataAsSource/FormattingRule.cs @@ -11,59 +11,58 @@ using Microsoft.CodeAnalysis.Formatting.Rules; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.MetadataAsSource +namespace Microsoft.CodeAnalysis.CSharp.MetadataAsSource; + +internal partial class CSharpMetadataAsSourceService { - internal partial class CSharpMetadataAsSourceService + private class FormattingRule : AbstractMetadataFormattingRule { - private class FormattingRule : AbstractMetadataFormattingRule + protected override AdjustNewLinesOperation GetAdjustNewLinesOperationBetweenMembersAndUsings(SyntaxToken token1, SyntaxToken token2) { - protected override AdjustNewLinesOperation GetAdjustNewLinesOperationBetweenMembersAndUsings(SyntaxToken token1, SyntaxToken token2) - { - var previousToken = token1; - var currentToken = token2; - - // We are not between members or usings if the last token wasn't the end of a statement or if the current token - // is the end of a scope. - if ((previousToken.Kind() != SyntaxKind.SemicolonToken && previousToken.Kind() != SyntaxKind.CloseBraceToken) || - currentToken.Kind() == SyntaxKind.CloseBraceToken) - { - return null; - } - - SyntaxNode previousMember = FormattingRangeHelper.GetEnclosingMember(previousToken); - SyntaxNode nextMember = FormattingRangeHelper.GetEnclosingMember(currentToken); + var previousToken = token1; + var currentToken = token2; - // Is the previous statement an using directive? If so, treat it like a member to add - // the right number of lines. - if (previousToken.Kind() == SyntaxKind.SemicolonToken && previousToken.Parent.Kind() == SyntaxKind.UsingDirective) - { - previousMember = previousToken.Parent; - } + // We are not between members or usings if the last token wasn't the end of a statement or if the current token + // is the end of a scope. + if ((previousToken.Kind() != SyntaxKind.SemicolonToken && previousToken.Kind() != SyntaxKind.CloseBraceToken) || + currentToken.Kind() == SyntaxKind.CloseBraceToken) + { + return null; + } - if (previousMember == null || nextMember == null || previousMember == nextMember) - { - return null; - } + SyntaxNode previousMember = FormattingRangeHelper.GetEnclosingMember(previousToken); + SyntaxNode nextMember = FormattingRangeHelper.GetEnclosingMember(currentToken); - // If we have two members of the same kind, we won't insert a blank line - if (previousMember.Kind() == nextMember.Kind()) - { - return FormattingOperations.CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.ForceLines); - } + // Is the previous statement an using directive? If so, treat it like a member to add + // the right number of lines. + if (previousToken.Kind() == SyntaxKind.SemicolonToken && previousToken.Parent.Kind() == SyntaxKind.UsingDirective) + { + previousMember = previousToken.Parent; + } - // Force a blank line between the two nodes by counting the number of lines of - // trivia and adding one to it. - var triviaList = token1.TrailingTrivia.Concat(token2.LeadingTrivia); - return FormattingOperations.CreateAdjustNewLinesOperation(GetNumberOfLines(triviaList) + 1, AdjustNewLinesOption.ForceLines); + if (previousMember == null || nextMember == null || previousMember == nextMember) + { + return null; } - public override void AddAnchorIndentationOperations(List list, SyntaxNode node, in NextAnchorIndentationOperationAction nextOperation) + // If we have two members of the same kind, we won't insert a blank line + if (previousMember.Kind() == nextMember.Kind()) { - return; + return FormattingOperations.CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.ForceLines); } - protected override bool IsNewLine(char c) - => SyntaxFacts.IsNewLine(c); + // Force a blank line between the two nodes by counting the number of lines of + // trivia and adding one to it. + var triviaList = token1.TrailingTrivia.Concat(token2.LeadingTrivia); + return FormattingOperations.CreateAdjustNewLinesOperation(GetNumberOfLines(triviaList) + 1, AdjustNewLinesOption.ForceLines); + } + + public override void AddAnchorIndentationOperations(List list, SyntaxNode node, in NextAnchorIndentationOperationAction nextOperation) + { + return; } + + protected override bool IsNewLine(char c) + => SyntaxFacts.IsNewLine(c); } } diff --git a/src/Features/CSharp/Portable/MoveDeclarationNearReference/CSharpMoveDeclarationNearReferenceCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/MoveDeclarationNearReference/CSharpMoveDeclarationNearReferenceCodeRefactoringProvider.cs index c3e5054fe7025..412d6d3939504 100644 --- a/src/Features/CSharp/Portable/MoveDeclarationNearReference/CSharpMoveDeclarationNearReferenceCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/MoveDeclarationNearReference/CSharpMoveDeclarationNearReferenceCodeRefactoringProvider.cs @@ -10,16 +10,15 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.MoveDeclarationNearReference; -namespace Microsoft.CodeAnalysis.CSharp.MoveDeclarationNearReference +namespace Microsoft.CodeAnalysis.CSharp.MoveDeclarationNearReference; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MoveDeclarationNearReference), Shared] +[ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.InlineTemporary)] +internal class CSharpMoveDeclarationNearReferenceCodeRefactoringProvider : AbstractMoveDeclarationNearReferenceCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MoveDeclarationNearReference), Shared] - [ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.InlineTemporary)] - internal class CSharpMoveDeclarationNearReferenceCodeRefactoringProvider : AbstractMoveDeclarationNearReferenceCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpMoveDeclarationNearReferenceCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpMoveDeclarationNearReferenceCodeRefactoringProvider() - { - } } } diff --git a/src/Features/CSharp/Portable/MoveToNamespace/CSharpMoveToNamespaceService.cs b/src/Features/CSharp/Portable/MoveToNamespace/CSharpMoveToNamespaceService.cs index 37e180670d3eb..80b69757880ca 100644 --- a/src/Features/CSharp/Portable/MoveToNamespace/CSharpMoveToNamespaceService.cs +++ b/src/Features/CSharp/Portable/MoveToNamespace/CSharpMoveToNamespaceService.cs @@ -9,34 +9,33 @@ using Microsoft.CodeAnalysis.MoveToNamespace; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.MoveToNamespace +namespace Microsoft.CodeAnalysis.CSharp.MoveToNamespace; + +[ExportLanguageService(typeof(IMoveToNamespaceService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpMoveToNamespaceService( + [Import(AllowDefault = true)] IMoveToNamespaceOptionsService optionsService) : + AbstractMoveToNamespaceService(optionsService) { - [ExportLanguageService(typeof(IMoveToNamespaceService), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpMoveToNamespaceService( - [Import(AllowDefault = true)] IMoveToNamespaceOptionsService optionsService) : - AbstractMoveToNamespaceService(optionsService) - { - protected override string GetNamespaceName(SyntaxNode container) - => container switch - { - BaseNamespaceDeclarationSyntax namespaceSyntax => namespaceSyntax.Name.ToString(), - CompilationUnitSyntax _ => string.Empty, - _ => throw ExceptionUtilities.UnexpectedValue(container) - }; + protected override string GetNamespaceName(SyntaxNode container) + => container switch + { + BaseNamespaceDeclarationSyntax namespaceSyntax => namespaceSyntax.Name.ToString(), + CompilationUnitSyntax _ => string.Empty, + _ => throw ExceptionUtilities.UnexpectedValue(container) + }; - protected override bool IsContainedInNamespaceDeclaration(BaseNamespaceDeclarationSyntax baseNamespace, int position) + protected override bool IsContainedInNamespaceDeclaration(BaseNamespaceDeclarationSyntax baseNamespace, int position) + { + var namespaceDeclarationStart = baseNamespace.NamespaceKeyword.SpanStart; + var namespaceDeclarationEnd = baseNamespace switch { - var namespaceDeclarationStart = baseNamespace.NamespaceKeyword.SpanStart; - var namespaceDeclarationEnd = baseNamespace switch - { - NamespaceDeclarationSyntax namespaceDeclaration => namespaceDeclaration.OpenBraceToken.SpanStart, - FileScopedNamespaceDeclarationSyntax fileScopedNamespace => fileScopedNamespace.SemicolonToken.Span.End, - _ => throw ExceptionUtilities.UnexpectedValue(baseNamespace.Kind()), - }; + NamespaceDeclarationSyntax namespaceDeclaration => namespaceDeclaration.OpenBraceToken.SpanStart, + FileScopedNamespaceDeclarationSyntax fileScopedNamespace => fileScopedNamespace.SemicolonToken.Span.End, + _ => throw ExceptionUtilities.UnexpectedValue(baseNamespace.Kind()), + }; - return position >= namespaceDeclarationStart && position < namespaceDeclarationEnd; - } + return position >= namespaceDeclarationStart && position < namespaceDeclarationEnd; } } diff --git a/src/Features/CSharp/Portable/NameTupleElement/CSharpNameTupleElementCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/NameTupleElement/CSharpNameTupleElementCodeRefactoringProvider.cs index 93aa8eee009c3..3251cb7a6187b 100644 --- a/src/Features/CSharp/Portable/NameTupleElement/CSharpNameTupleElementCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/NameTupleElement/CSharpNameTupleElementCodeRefactoringProvider.cs @@ -11,19 +11,18 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.NameTupleElement; -namespace Microsoft.CodeAnalysis.CSharp.NameTupleElement +namespace Microsoft.CodeAnalysis.CSharp.NameTupleElement; + +[ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.NameTupleElement), Shared] +internal class CSharpNameTupleElementCodeRefactoringProvider : AbstractNameTupleElementCodeRefactoringProvider { - [ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.NameTupleElement), Shared] - internal class CSharpNameTupleElementCodeRefactoringProvider : AbstractNameTupleElementCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpNameTupleElementCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpNameTupleElementCodeRefactoringProvider() - { - } - - protected override ArgumentSyntax WithName(ArgumentSyntax argument, string argumentName) - => argument.WithNameColon(SyntaxFactory.NameColon(argumentName.ToIdentifierName())); } + + protected override ArgumentSyntax WithName(ArgumentSyntax argument, string argumentName) + => argument.WithNameColon(SyntaxFactory.NameColon(argumentName.ToIdentifierName())); } diff --git a/src/Features/CSharp/Portable/NavigationBar/CSharpNavigationBarItemService.cs b/src/Features/CSharp/Portable/NavigationBar/CSharpNavigationBarItemService.cs index 9cc954d00e1ef..f08ad9316eb71 100644 --- a/src/Features/CSharp/Portable/NavigationBar/CSharpNavigationBarItemService.cs +++ b/src/Features/CSharp/Portable/NavigationBar/CSharpNavigationBarItemService.cs @@ -19,246 +19,245 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.NavigationBar.RoslynNavigationBarItem; -namespace Microsoft.CodeAnalysis.CSharp.NavigationBar +namespace Microsoft.CodeAnalysis.CSharp.NavigationBar; + +[ExportLanguageService(typeof(INavigationBarItemService), LanguageNames.CSharp), Shared] +internal class CSharpNavigationBarItemService : AbstractNavigationBarItemService { - [ExportLanguageService(typeof(INavigationBarItemService), LanguageNames.CSharp), Shared] - internal class CSharpNavigationBarItemService : AbstractNavigationBarItemService + private static readonly SymbolDisplayFormat s_typeFormat = + SymbolDisplayFormat.CSharpErrorMessageFormat.AddGenericsOptions(SymbolDisplayGenericsOptions.IncludeVariance); + + private static readonly SymbolDisplayFormat s_memberFormat = + new(genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + memberOptions: SymbolDisplayMemberOptions.IncludeParameters | + SymbolDisplayMemberOptions.IncludeExplicitInterface, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, + parameterOptions: SymbolDisplayParameterOptions.IncludeType | + SymbolDisplayParameterOptions.IncludeName | + SymbolDisplayParameterOptions.IncludeDefaultValue | + SymbolDisplayParameterOptions.IncludeParamsRefOut, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | + SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpNavigationBarItemService() { - private static readonly SymbolDisplayFormat s_typeFormat = - SymbolDisplayFormat.CSharpErrorMessageFormat.AddGenericsOptions(SymbolDisplayGenericsOptions.IncludeVariance); - - private static readonly SymbolDisplayFormat s_memberFormat = - new(genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - memberOptions: SymbolDisplayMemberOptions.IncludeParameters | - SymbolDisplayMemberOptions.IncludeExplicitInterface, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, - parameterOptions: SymbolDisplayParameterOptions.IncludeType | - SymbolDisplayParameterOptions.IncludeName | - SymbolDisplayParameterOptions.IncludeDefaultValue | - SymbolDisplayParameterOptions.IncludeParamsRefOut, - miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | - SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral | - SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpNavigationBarItemService() - { - } + } - protected override async Task> GetItemsInCurrentProcessAsync( - Document document, bool supportsCodeGeneration, CancellationToken cancellationToken) - { - var typesInFile = await GetTypesInFileAsync(document, cancellationToken).ConfigureAwait(false); - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - return GetMembersInTypes(document.Project.Solution, tree, typesInFile, cancellationToken); - } + protected override async Task> GetItemsInCurrentProcessAsync( + Document document, bool supportsCodeGeneration, CancellationToken cancellationToken) + { + var typesInFile = await GetTypesInFileAsync(document, cancellationToken).ConfigureAwait(false); + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + return GetMembersInTypes(document.Project.Solution, tree, typesInFile, cancellationToken); + } - private static ImmutableArray GetMembersInTypes( - Solution solution, SyntaxTree tree, IEnumerable types, CancellationToken cancellationToken) + private static ImmutableArray GetMembersInTypes( + Solution solution, SyntaxTree tree, IEnumerable types, CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.NavigationBar_ItemService_GetMembersInTypes_CSharp, cancellationToken)) { - using (Logger.LogBlock(FunctionId.NavigationBar_ItemService_GetMembersInTypes_CSharp, cancellationToken)) + using var _1 = ArrayBuilder.GetInstance(out var items); + + foreach (var type in types) { - using var _1 = ArrayBuilder.GetInstance(out var items); + using var _2 = ArrayBuilder.GetInstance(out var memberItems); - foreach (var type in types) + foreach (var member in type.GetMembers()) { - using var _2 = ArrayBuilder.GetInstance(out var memberItems); - - foreach (var member in type.GetMembers()) + if (member.IsImplicitlyDeclared || + member.Kind == SymbolKind.NamedType || + IsAccessor(member)) { - if (member.IsImplicitlyDeclared || - member.Kind == SymbolKind.NamedType || - IsAccessor(member)) - { - continue; - } - - var method = member as IMethodSymbol; - if (method != null && method.PartialImplementationPart != null) - { - memberItems.AddIfNotNull(CreateItemForMember(solution, method, tree, cancellationToken)); - memberItems.AddIfNotNull(CreateItemForMember(solution, method.PartialImplementationPart, tree, cancellationToken)); - } - else - { - Debug.Assert(method == null || method.PartialDefinitionPart == null, "NavBar expected GetMembers to return partial method definition parts but the implementation part was returned."); - - memberItems.AddIfNotNull(CreateItemForMember(solution, member, tree, cancellationToken)); - } + continue; } - memberItems.Sort((x, y) => + var method = member as IMethodSymbol; + if (method != null && method.PartialImplementationPart != null) { - var textComparison = x.Text.CompareTo(y.Text); - return textComparison != 0 ? textComparison : x.Grayed.CompareTo(y.Grayed); - }); - - var spans = GetSymbolLocation(solution, type, tree, cancellationToken); - if (spans == null) - continue; + memberItems.AddIfNotNull(CreateItemForMember(solution, method, tree, cancellationToken)); + memberItems.AddIfNotNull(CreateItemForMember(solution, method.PartialImplementationPart, tree, cancellationToken)); + } + else + { + Debug.Assert(method == null || method.PartialDefinitionPart == null, "NavBar expected GetMembers to return partial method definition parts but the implementation part was returned."); - items.Add(new SymbolItem( - type.Name, - text: type.ToDisplayString(s_typeFormat), - glyph: type.GetGlyph(), - isObsolete: type.IsObsolete(), - spans.Value, - childItems: memberItems.ToImmutable())); + memberItems.AddIfNotNull(CreateItemForMember(solution, member, tree, cancellationToken)); + } } - items.Sort((x1, x2) => x1.Text.CompareTo(x2.Text)); - return items.ToImmutable(); + memberItems.Sort((x, y) => + { + var textComparison = x.Text.CompareTo(y.Text); + return textComparison != 0 ? textComparison : x.Grayed.CompareTo(y.Grayed); + }); + + var spans = GetSymbolLocation(solution, type, tree, cancellationToken); + if (spans == null) + continue; + + items.Add(new SymbolItem( + type.Name, + text: type.ToDisplayString(s_typeFormat), + glyph: type.GetGlyph(), + isObsolete: type.IsObsolete(), + spans.Value, + childItems: memberItems.ToImmutable())); } + + items.Sort((x1, x2) => x1.Text.CompareTo(x2.Text)); + return items.ToImmutable(); } + } - private static async Task> GetTypesInFileAsync(Document document, CancellationToken cancellationToken) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + private static async Task> GetTypesInFileAsync(Document document, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - return GetTypesInFile(semanticModel, cancellationToken); - } + return GetTypesInFile(semanticModel, cancellationToken); + } - private static IEnumerable GetTypesInFile(SemanticModel semanticModel, CancellationToken cancellationToken) + private static IEnumerable GetTypesInFile(SemanticModel semanticModel, CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.NavigationBar_ItemService_GetTypesInFile_CSharp, cancellationToken)) { - using (Logger.LogBlock(FunctionId.NavigationBar_ItemService_GetTypesInFile_CSharp, cancellationToken)) - { - var types = new HashSet(); - var nodesToVisit = new Stack(); + var types = new HashSet(); + var nodesToVisit = new Stack(); - nodesToVisit.Push(semanticModel.SyntaxTree.GetRoot(cancellationToken)); + nodesToVisit.Push(semanticModel.SyntaxTree.GetRoot(cancellationToken)); - while (!nodesToVisit.IsEmpty()) + while (!nodesToVisit.IsEmpty()) + { + if (cancellationToken.IsCancellationRequested) { - if (cancellationToken.IsCancellationRequested) - { - return SpecializedCollections.EmptyEnumerable(); - } - - var node = nodesToVisit.Pop(); - var type = GetType(semanticModel, node, cancellationToken); + return SpecializedCollections.EmptyEnumerable(); + } - if (type != null) - { - types.Add((INamedTypeSymbol)type); - } + var node = nodesToVisit.Pop(); + var type = GetType(semanticModel, node, cancellationToken); - if (node is BaseMethodDeclarationSyntax or - BasePropertyDeclarationSyntax or - BaseFieldDeclarationSyntax or - StatementSyntax or - ExpressionSyntax) - { - // quick bail out to prevent us from creating every nodes exist in current file - continue; - } + if (type != null) + { + types.Add((INamedTypeSymbol)type); + } - foreach (var child in node.ChildNodes()) - { - nodesToVisit.Push(child); - } + if (node is BaseMethodDeclarationSyntax or + BasePropertyDeclarationSyntax or + BaseFieldDeclarationSyntax or + StatementSyntax or + ExpressionSyntax) + { + // quick bail out to prevent us from creating every nodes exist in current file + continue; } - return types; + foreach (var child in node.ChildNodes()) + { + nodesToVisit.Push(child); + } } - } - private static ISymbol? GetType(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) - => node switch - { - BaseTypeDeclarationSyntax t => semanticModel.GetDeclaredSymbol(t, cancellationToken), - DelegateDeclarationSyntax d => semanticModel.GetDeclaredSymbol(d, cancellationToken), - _ => null, - }; + return types; + } + } - private static bool IsAccessor(ISymbol member) + private static ISymbol? GetType(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => node switch { - if (member.Kind == SymbolKind.Method) - { - var method = (IMethodSymbol)member; + BaseTypeDeclarationSyntax t => semanticModel.GetDeclaredSymbol(t, cancellationToken), + DelegateDeclarationSyntax d => semanticModel.GetDeclaredSymbol(d, cancellationToken), + _ => null, + }; - return method.MethodKind is MethodKind.PropertyGet or MethodKind.PropertySet; - } + private static bool IsAccessor(ISymbol member) + { + if (member.Kind == SymbolKind.Method) + { + var method = (IMethodSymbol)member; - return false; + return method.MethodKind is MethodKind.PropertyGet or MethodKind.PropertySet; } - private static RoslynNavigationBarItem? CreateItemForMember( - Solution solution, ISymbol member, SyntaxTree tree, CancellationToken cancellationToken) - { - var location = GetSymbolLocation(solution, member, tree, cancellationToken); - if (location == null) - return null; - - return new SymbolItem( - member.Name, - member.ToDisplayString(s_memberFormat), - member.GetGlyph(), - member.IsObsolete(), - location.Value); - } + return false; + } - private static SymbolItemLocation? GetSymbolLocation( - Solution solution, ISymbol symbol, SyntaxTree tree, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + private static RoslynNavigationBarItem? CreateItemForMember( + Solution solution, ISymbol member, SyntaxTree tree, CancellationToken cancellationToken) + { + var location = GetSymbolLocation(solution, member, tree, cancellationToken); + if (location == null) + return null; + + return new SymbolItem( + member.Name, + member.ToDisplayString(s_memberFormat), + member.GetGlyph(), + member.IsObsolete(), + location.Value); + } - if (symbol.Kind == SymbolKind.Field) - { - return symbol.ContainingType.TypeKind == TypeKind.Enum - ? GetSymbolLocation(solution, symbol, tree, static reference => GetEnumMemberSpan(reference)) - : GetSymbolLocation(solution, symbol, tree, static reference => GetFieldReferenceSpan(reference)); - } - else - { - return GetSymbolLocation(solution, symbol, tree, static reference => reference.Span); - } - } + private static SymbolItemLocation? GetSymbolLocation( + Solution solution, ISymbol symbol, SyntaxTree tree, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - private static TextSpan GetFieldReferenceSpan(SyntaxReference reference) + if (symbol.Kind == SymbolKind.Field) + { + return symbol.ContainingType.TypeKind == TypeKind.Enum + ? GetSymbolLocation(solution, symbol, tree, static reference => GetEnumMemberSpan(reference)) + : GetSymbolLocation(solution, symbol, tree, static reference => GetFieldReferenceSpan(reference)); + } + else { - var declaringNode = reference.GetSyntax(); + return GetSymbolLocation(solution, symbol, tree, static reference => reference.Span); + } + } - var spanStart = declaringNode.SpanStart; - var spanEnd = declaringNode.Span.End; + private static TextSpan GetFieldReferenceSpan(SyntaxReference reference) + { + var declaringNode = reference.GetSyntax(); - var fieldDeclaration = declaringNode.GetAncestor(); - if (fieldDeclaration != null) - { - var variables = fieldDeclaration.Declaration.Variables; + var spanStart = declaringNode.SpanStart; + var spanEnd = declaringNode.Span.End; - if (variables.FirstOrDefault() == declaringNode) - spanStart = fieldDeclaration.SpanStart; + var fieldDeclaration = declaringNode.GetAncestor(); + if (fieldDeclaration != null) + { + var variables = fieldDeclaration.Declaration.Variables; - if (variables.LastOrDefault() == declaringNode) - spanEnd = fieldDeclaration.Span.End; - } + if (variables.FirstOrDefault() == declaringNode) + spanStart = fieldDeclaration.SpanStart; - return TextSpan.FromBounds(spanStart, spanEnd); + if (variables.LastOrDefault() == declaringNode) + spanEnd = fieldDeclaration.Span.End; } - private static TextSpan GetEnumMemberSpan(SyntaxReference reference) + return TextSpan.FromBounds(spanStart, spanEnd); + } + + private static TextSpan GetEnumMemberSpan(SyntaxReference reference) + { + var declaringNode = reference.GetSyntax(); + if (declaringNode is EnumMemberDeclarationSyntax enumMember) { - var declaringNode = reference.GetSyntax(); - if (declaringNode is EnumMemberDeclarationSyntax enumMember) - { - var enumDeclaration = enumMember.GetAncestor(); + var enumDeclaration = enumMember.GetAncestor(); - if (enumDeclaration != null) + if (enumDeclaration != null) + { + var index = enumDeclaration.Members.IndexOf(enumMember); + if (index != -1 && index < enumDeclaration.Members.SeparatorCount) { - var index = enumDeclaration.Members.IndexOf(enumMember); - if (index != -1 && index < enumDeclaration.Members.SeparatorCount) - { - // Cool, we have a comma, so do it - var start = enumMember.SpanStart; - var end = enumDeclaration.Members.GetSeparator(index).Span.End; + // Cool, we have a comma, so do it + var start = enumMember.SpanStart; + var end = enumDeclaration.Members.GetSeparator(index).Span.End; - return TextSpan.FromBounds(start, end); - } + return TextSpan.FromBounds(start, end); } } - - return declaringNode.Span; } + + return declaringNode.Span; } } diff --git a/src/Features/CSharp/Portable/Organizing/CSharpOrganizingService.Rewriter.cs b/src/Features/CSharp/Portable/Organizing/CSharpOrganizingService.Rewriter.cs index 0d2a146d315b7..b5245fd9fd050 100644 --- a/src/Features/CSharp/Portable/Organizing/CSharpOrganizingService.Rewriter.cs +++ b/src/Features/CSharp/Portable/Organizing/CSharpOrganizingService.Rewriter.cs @@ -10,43 +10,42 @@ using System.Threading; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing +namespace Microsoft.CodeAnalysis.CSharp.Organizing; + +internal partial class CSharpOrganizingService { - internal partial class CSharpOrganizingService + private class Rewriter(CSharpOrganizingService treeOrganizer, IEnumerable organizers, SemanticModel semanticModel, CancellationToken cancellationToken) : CSharpSyntaxRewriter { - private class Rewriter(CSharpOrganizingService treeOrganizer, IEnumerable organizers, SemanticModel semanticModel, CancellationToken cancellationToken) : CSharpSyntaxRewriter + private readonly Func> _nodeToOrganizersGetter = treeOrganizer.GetNodeToOrganizers(organizers.ToList()); + private readonly SemanticModel _semanticModel = semanticModel; + private readonly CancellationToken _cancellationToken = cancellationToken; + + public override SyntaxNode DefaultVisit(SyntaxNode node) { - private readonly Func> _nodeToOrganizersGetter = treeOrganizer.GetNodeToOrganizers(organizers.ToList()); - private readonly SemanticModel _semanticModel = semanticModel; - private readonly CancellationToken _cancellationToken = cancellationToken; + _cancellationToken.ThrowIfCancellationRequested(); + return node; + } - public override SyntaxNode DefaultVisit(SyntaxNode node) - { - _cancellationToken.ThrowIfCancellationRequested(); - return node; - } + public override SyntaxNode Visit(SyntaxNode node) + { + _cancellationToken.ThrowIfCancellationRequested(); - public override SyntaxNode Visit(SyntaxNode node) + if (node == null) { - _cancellationToken.ThrowIfCancellationRequested(); - - if (node == null) - { - return null; - } - - // First, recurse into our children, updating them. - node = base.Visit(node); + return null; + } - // Now, try to update this new node itself. - var organizers = _nodeToOrganizersGetter(node); - foreach (var organizer in organizers) - { - node = organizer.OrganizeNode(_semanticModel, node, _cancellationToken); - } + // First, recurse into our children, updating them. + node = base.Visit(node); - return node; + // Now, try to update this new node itself. + var organizers = _nodeToOrganizersGetter(node); + foreach (var organizer in organizers) + { + node = organizer.OrganizeNode(_semanticModel, node, _cancellationToken); } + + return node; } } } diff --git a/src/Features/CSharp/Portable/Organizing/CSharpOrganizingService.cs b/src/Features/CSharp/Portable/Organizing/CSharpOrganizingService.cs index 44520f4425a71..ba170100983b5 100644 --- a/src/Features/CSharp/Portable/Organizing/CSharpOrganizingService.cs +++ b/src/Features/CSharp/Portable/Organizing/CSharpOrganizingService.cs @@ -14,19 +14,18 @@ using Microsoft.CodeAnalysis.Organizing; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing +namespace Microsoft.CodeAnalysis.CSharp.Organizing; + +[ExportLanguageService(typeof(IOrganizingService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal partial class CSharpOrganizingService( + [ImportMany] IEnumerable> organizers) : AbstractOrganizingService(organizers.Where(o => o.Metadata.Language == LanguageNames.CSharp).Select(o => o.Value)) { - [ExportLanguageService(typeof(IOrganizingService), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal partial class CSharpOrganizingService( - [ImportMany] IEnumerable> organizers) : AbstractOrganizingService(organizers.Where(o => o.Metadata.Language == LanguageNames.CSharp).Select(o => o.Value)) + protected override async Task ProcessAsync(Document document, IEnumerable organizers, CancellationToken cancellationToken) { - protected override async Task ProcessAsync(Document document, IEnumerable organizers, CancellationToken cancellationToken) - { - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var rewriter = new Rewriter(this, organizers, await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false), cancellationToken); - return document.WithSyntaxRoot(rewriter.Visit(root)); - } + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var rewriter = new Rewriter(this, organizers, await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false), cancellationToken); + return document.WithSyntaxRoot(rewriter.Visit(root)); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/ClassDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/ClassDeclarationOrganizer.cs index 76cdc1919b9e8..aed90d327de2a 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/ClassDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/ClassDeclarationOrganizer.cs @@ -11,33 +11,32 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class ClassDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class ClassDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ClassDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ClassDeclarationOrganizer() - { - } + } - protected override ClassDeclarationSyntax Organize( - ClassDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update( - syntax.AttributeLists, - ModifiersOrganizer.Organize(syntax.Modifiers), - syntax.Keyword, - syntax.Identifier, - syntax.TypeParameterList, - syntax.BaseList, - syntax.ConstraintClauses, - syntax.OpenBraceToken, - MemberDeclarationsOrganizer.Organize(syntax.Members, cancellationToken), - syntax.CloseBraceToken, - syntax.SemicolonToken); - } + protected override ClassDeclarationSyntax Organize( + ClassDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update( + syntax.AttributeLists, + ModifiersOrganizer.Organize(syntax.Modifiers), + syntax.Keyword, + syntax.Identifier, + syntax.TypeParameterList, + syntax.BaseList, + syntax.ConstraintClauses, + syntax.OpenBraceToken, + MemberDeclarationsOrganizer.Organize(syntax.Members, cancellationToken), + syntax.CloseBraceToken, + syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/ConstructorDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/ConstructorDeclarationOrganizer.cs index f991d251c26d0..c3cceda314a66 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/ConstructorDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/ConstructorDeclarationOrganizer.cs @@ -11,28 +11,27 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class ConstructorDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class ConstructorDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConstructorDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConstructorDeclarationOrganizer() - { - } + } - protected override ConstructorDeclarationSyntax Organize( - ConstructorDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update(syntax.AttributeLists, - ModifiersOrganizer.Organize(syntax.Modifiers), - syntax.Identifier, - syntax.ParameterList, - syntax.Initializer, - syntax.Body, - syntax.SemicolonToken); - } + protected override ConstructorDeclarationSyntax Organize( + ConstructorDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update(syntax.AttributeLists, + ModifiersOrganizer.Organize(syntax.Modifiers), + syntax.Identifier, + syntax.ParameterList, + syntax.Initializer, + syntax.Body, + syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/DestructorDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/DestructorDeclarationOrganizer.cs index a2ccb6854203c..d44464c4b0d2f 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/DestructorDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/DestructorDeclarationOrganizer.cs @@ -11,28 +11,27 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class DestructorDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class DestructorDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DestructorDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public DestructorDeclarationOrganizer() - { - } + } - protected override DestructorDeclarationSyntax Organize( - DestructorDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update(syntax.AttributeLists, - ModifiersOrganizer.Organize(syntax.Modifiers), - syntax.TildeToken, - syntax.Identifier, - syntax.ParameterList, - syntax.Body, - syntax.SemicolonToken); - } + protected override DestructorDeclarationSyntax Organize( + DestructorDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update(syntax.AttributeLists, + ModifiersOrganizer.Organize(syntax.Modifiers), + syntax.TildeToken, + syntax.Identifier, + syntax.ParameterList, + syntax.Body, + syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/EnumDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/EnumDeclarationOrganizer.cs index 9004d374e202a..07939386f625c 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/EnumDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/EnumDeclarationOrganizer.cs @@ -11,31 +11,30 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class EnumDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class EnumDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public EnumDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EnumDeclarationOrganizer() - { - } + } - protected override EnumDeclarationSyntax Organize( - EnumDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update( - syntax.AttributeLists, - ModifiersOrganizer.Organize(syntax.Modifiers), - syntax.EnumKeyword, - syntax.Identifier, - syntax.BaseList, - syntax.OpenBraceToken, - syntax.Members, - syntax.CloseBraceToken, - syntax.SemicolonToken); - } + protected override EnumDeclarationSyntax Organize( + EnumDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update( + syntax.AttributeLists, + ModifiersOrganizer.Organize(syntax.Modifiers), + syntax.EnumKeyword, + syntax.Identifier, + syntax.BaseList, + syntax.OpenBraceToken, + syntax.Members, + syntax.CloseBraceToken, + syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/EventDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/EventDeclarationOrganizer.cs index 2f3d4389e1fdd..e804b9c3c6bea 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/EventDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/EventDeclarationOrganizer.cs @@ -11,29 +11,28 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class EventDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class EventDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public EventDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EventDeclarationOrganizer() - { - } + } - protected override EventDeclarationSyntax Organize( - EventDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update(syntax.AttributeLists, - ModifiersOrganizer.Organize(syntax.Modifiers), - syntax.EventKeyword, - syntax.Type, - syntax.ExplicitInterfaceSpecifier, - syntax.Identifier, - syntax.AccessorList, - syntax.SemicolonToken); - } + protected override EventDeclarationSyntax Organize( + EventDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update(syntax.AttributeLists, + ModifiersOrganizer.Organize(syntax.Modifiers), + syntax.EventKeyword, + syntax.Type, + syntax.ExplicitInterfaceSpecifier, + syntax.Identifier, + syntax.AccessorList, + syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/EventFieldDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/EventFieldDeclarationOrganizer.cs index 465cebdc41b5b..22ea5f543b432 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/EventFieldDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/EventFieldDeclarationOrganizer.cs @@ -11,26 +11,25 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class EventFieldDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class EventFieldDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public EventFieldDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EventFieldDeclarationOrganizer() - { - } + } - protected override EventFieldDeclarationSyntax Organize( - EventFieldDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update(syntax.AttributeLists, - ModifiersOrganizer.Organize(syntax.Modifiers), - syntax.EventKeyword, - syntax.Declaration, - syntax.SemicolonToken); - } + protected override EventFieldDeclarationSyntax Organize( + EventFieldDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update(syntax.AttributeLists, + ModifiersOrganizer.Organize(syntax.Modifiers), + syntax.EventKeyword, + syntax.Declaration, + syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/FieldDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/FieldDeclarationOrganizer.cs index c10ba5be0b68a..71b4b39d1d7f1 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/FieldDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/FieldDeclarationOrganizer.cs @@ -11,25 +11,24 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class FieldDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class FieldDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public FieldDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public FieldDeclarationOrganizer() - { - } + } - protected override FieldDeclarationSyntax Organize( - FieldDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update(syntax.AttributeLists, - ModifiersOrganizer.Organize(syntax.Modifiers), - syntax.Declaration, - syntax.SemicolonToken); - } + protected override FieldDeclarationSyntax Organize( + FieldDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update(syntax.AttributeLists, + ModifiersOrganizer.Organize(syntax.Modifiers), + syntax.Declaration, + syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/IndexerDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/IndexerDeclarationOrganizer.cs index 15a2fd969111a..db8de974835a3 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/IndexerDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/IndexerDeclarationOrganizer.cs @@ -11,31 +11,30 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class IndexerDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class IndexerDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public IndexerDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public IndexerDeclarationOrganizer() - { - } + } - protected override IndexerDeclarationSyntax Organize( - IndexerDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update( - attributeLists: syntax.AttributeLists, - modifiers: ModifiersOrganizer.Organize(syntax.Modifiers), - type: syntax.Type, - explicitInterfaceSpecifier: syntax.ExplicitInterfaceSpecifier, - thisKeyword: syntax.ThisKeyword, - parameterList: syntax.ParameterList, - accessorList: syntax.AccessorList, - expressionBody: syntax.ExpressionBody, - semicolonToken: syntax.SemicolonToken); - } + protected override IndexerDeclarationSyntax Organize( + IndexerDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update( + attributeLists: syntax.AttributeLists, + modifiers: ModifiersOrganizer.Organize(syntax.Modifiers), + type: syntax.Type, + explicitInterfaceSpecifier: syntax.ExplicitInterfaceSpecifier, + thisKeyword: syntax.ThisKeyword, + parameterList: syntax.ParameterList, + accessorList: syntax.AccessorList, + expressionBody: syntax.ExpressionBody, + semicolonToken: syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/InterfaceDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/InterfaceDeclarationOrganizer.cs index 0a07df99de76b..70cb6b1c81a1e 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/InterfaceDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/InterfaceDeclarationOrganizer.cs @@ -11,33 +11,32 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class InterfaceDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class InterfaceDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public InterfaceDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public InterfaceDeclarationOrganizer() - { - } + } - protected override InterfaceDeclarationSyntax Organize( - InterfaceDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update( - syntax.AttributeLists, - ModifiersOrganizer.Organize(syntax.Modifiers), - syntax.Keyword, - syntax.Identifier, - syntax.TypeParameterList, - syntax.BaseList, - syntax.ConstraintClauses, - syntax.OpenBraceToken, - MemberDeclarationsOrganizer.Organize(syntax.Members, cancellationToken), - syntax.CloseBraceToken, - syntax.SemicolonToken); - } + protected override InterfaceDeclarationSyntax Organize( + InterfaceDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update( + syntax.AttributeLists, + ModifiersOrganizer.Organize(syntax.Modifiers), + syntax.Keyword, + syntax.Identifier, + syntax.TypeParameterList, + syntax.BaseList, + syntax.ConstraintClauses, + syntax.OpenBraceToken, + MemberDeclarationsOrganizer.Organize(syntax.Members, cancellationToken), + syntax.CloseBraceToken, + syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/MemberDeclarationsOrganizer.Comparer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/MemberDeclarationsOrganizer.Comparer.cs index f680e861a8b11..5d07417102747 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/MemberDeclarationsOrganizer.Comparer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/MemberDeclarationsOrganizer.Comparer.cs @@ -12,188 +12,187 @@ using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +internal partial class MemberDeclarationsOrganizer { - internal partial class MemberDeclarationsOrganizer + private class Comparer : IComparer { - private class Comparer : IComparer + // TODO(cyrusn): Allow users to specify the ordering they want + private enum OuterOrdering { - // TODO(cyrusn): Allow users to specify the ordering they want - private enum OuterOrdering + Fields, + EventFields, + Constructors, + Destructors, + Properties, + Events, + Indexers, + Operators, + ConversionOperators, + Methods, + Types, + Remaining + } + + private enum InnerOrdering + { + StaticInstance, + Accessibility, + Name + } + + private enum Accessibility + { + Public, + Protected, + ProtectedOrInternal, + Internal, + Private + } + + public int Compare(MemberDeclarationSyntax x, MemberDeclarationSyntax y) + { + if (x == y) { - Fields, - EventFields, - Constructors, - Destructors, - Properties, - Events, - Indexers, - Operators, - ConversionOperators, - Methods, - Types, - Remaining + return 0; } - private enum InnerOrdering + var xOuterOrdering = GetOuterOrdering(x); + var yOuterOrdering = GetOuterOrdering(y); + + var compare = xOuterOrdering - yOuterOrdering; + if (compare != 0) { - StaticInstance, - Accessibility, - Name + return compare; } - private enum Accessibility + if (xOuterOrdering == OuterOrdering.Remaining) + { + return 1; + } + else if (yOuterOrdering == OuterOrdering.Remaining) { - Public, - Protected, - ProtectedOrInternal, - Internal, - Private + return -1; } - public int Compare(MemberDeclarationSyntax x, MemberDeclarationSyntax y) + if (xOuterOrdering == OuterOrdering.Fields || yOuterOrdering == OuterOrdering.Fields) { - if (x == y) + // Fields with initializers can't be reordered relative to + // themselves due to ordering issues. + var xHasInitializer = ((FieldDeclarationSyntax)x).Declaration.Variables.Any(v => v.Initializer != null); + var yHasInitializer = ((FieldDeclarationSyntax)y).Declaration.Variables.Any(v => v.Initializer != null); + if (xHasInitializer && yHasInitializer) { return 0; } + } - var xOuterOrdering = GetOuterOrdering(x); - var yOuterOrdering = GetOuterOrdering(y); - - var compare = xOuterOrdering - yOuterOrdering; - if (compare != 0) - { - return compare; - } - - if (xOuterOrdering == OuterOrdering.Remaining) - { - return 1; - } - else if (yOuterOrdering == OuterOrdering.Remaining) - { - return -1; - } + var xIsStatic = x.GetModifiers().Any(SyntaxKind.StaticKeyword); + var yIsStatic = y.GetModifiers().Any(SyntaxKind.StaticKeyword); - if (xOuterOrdering == OuterOrdering.Fields || yOuterOrdering == OuterOrdering.Fields) - { - // Fields with initializers can't be reordered relative to - // themselves due to ordering issues. - var xHasInitializer = ((FieldDeclarationSyntax)x).Declaration.Variables.Any(v => v.Initializer != null); - var yHasInitializer = ((FieldDeclarationSyntax)y).Declaration.Variables.Any(v => v.Initializer != null); - if (xHasInitializer && yHasInitializer) - { - return 0; - } - } + if ((compare = Comparer.Default.Inverse().Compare(xIsStatic, yIsStatic)) != 0) + { + return compare; + } - var xIsStatic = x.GetModifiers().Any(SyntaxKind.StaticKeyword); - var yIsStatic = y.GetModifiers().Any(SyntaxKind.StaticKeyword); + var xAccessibility = GetAccessibility(x); + var yAccessibility = GetAccessibility(y); + if ((compare = xAccessibility - yAccessibility) != 0) + { + return compare; + } - if ((compare = Comparer.Default.Inverse().Compare(xIsStatic, yIsStatic)) != 0) - { - return compare; - } + var xName = ShouldCompareByName(x) ? x.GetNameToken() : default; + var yName = ShouldCompareByName(y) ? y.GetNameToken() : default; - var xAccessibility = GetAccessibility(x); - var yAccessibility = GetAccessibility(y); - if ((compare = xAccessibility - yAccessibility) != 0) - { - return compare; - } + if ((compare = TokenComparer.NormalInstance.Compare(xName, yName)) != 0) + { + return compare; + } - var xName = ShouldCompareByName(x) ? x.GetNameToken() : default; - var yName = ShouldCompareByName(y) ? y.GetNameToken() : default; + // Their names were the same. Order them by arity at this point. + return x.GetArity() - y.GetArity(); + } - if ((compare = TokenComparer.NormalInstance.Compare(xName, yName)) != 0) - { - return compare; - } + private static Accessibility GetAccessibility(MemberDeclarationSyntax x) + { + var xModifiers = x.GetModifiers(); - // Their names were the same. Order them by arity at this point. - return x.GetArity() - y.GetArity(); + if (xModifiers.Any(SyntaxKind.PublicKeyword)) + { + return Accessibility.Public; } - - private static Accessibility GetAccessibility(MemberDeclarationSyntax x) + else if (xModifiers.Any(SyntaxKind.ProtectedKeyword) && xModifiers.Any(SyntaxKind.InternalKeyword)) { - var xModifiers = x.GetModifiers(); - - if (xModifiers.Any(SyntaxKind.PublicKeyword)) - { - return Accessibility.Public; - } - else if (xModifiers.Any(SyntaxKind.ProtectedKeyword) && xModifiers.Any(SyntaxKind.InternalKeyword)) - { - return Accessibility.ProtectedOrInternal; - } - else if (xModifiers.Any(SyntaxKind.InternalKeyword)) - { - return Accessibility.Internal; - } - else if (xModifiers.Any(SyntaxKind.ProtectedKeyword)) - { - return Accessibility.Protected; - } - else - { - return Accessibility.Private; - } + return Accessibility.ProtectedOrInternal; + } + else if (xModifiers.Any(SyntaxKind.InternalKeyword)) + { + return Accessibility.Internal; + } + else if (xModifiers.Any(SyntaxKind.ProtectedKeyword)) + { + return Accessibility.Protected; + } + else + { + return Accessibility.Private; } + } - private static OuterOrdering GetOuterOrdering(MemberDeclarationSyntax x) + private static OuterOrdering GetOuterOrdering(MemberDeclarationSyntax x) + { + switch (x.Kind()) { - switch (x.Kind()) - { - case SyntaxKind.FieldDeclaration: - return OuterOrdering.Fields; - case SyntaxKind.EventFieldDeclaration: - return OuterOrdering.EventFields; - case SyntaxKind.ConstructorDeclaration: - return OuterOrdering.Constructors; - case SyntaxKind.DestructorDeclaration: - return OuterOrdering.Destructors; - case SyntaxKind.PropertyDeclaration: - return OuterOrdering.Properties; - case SyntaxKind.EventDeclaration: - return OuterOrdering.Events; - case SyntaxKind.IndexerDeclaration: - return OuterOrdering.Indexers; - case SyntaxKind.OperatorDeclaration: - return OuterOrdering.Operators; - case SyntaxKind.ConversionOperatorDeclaration: - return OuterOrdering.ConversionOperators; - case SyntaxKind.MethodDeclaration: - return OuterOrdering.Methods; - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.StructDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.DelegateDeclaration: - case SyntaxKind.RecordDeclaration: - case SyntaxKind.RecordStructDeclaration: - return OuterOrdering.Types; - default: - return OuterOrdering.Remaining; - } + case SyntaxKind.FieldDeclaration: + return OuterOrdering.Fields; + case SyntaxKind.EventFieldDeclaration: + return OuterOrdering.EventFields; + case SyntaxKind.ConstructorDeclaration: + return OuterOrdering.Constructors; + case SyntaxKind.DestructorDeclaration: + return OuterOrdering.Destructors; + case SyntaxKind.PropertyDeclaration: + return OuterOrdering.Properties; + case SyntaxKind.EventDeclaration: + return OuterOrdering.Events; + case SyntaxKind.IndexerDeclaration: + return OuterOrdering.Indexers; + case SyntaxKind.OperatorDeclaration: + return OuterOrdering.Operators; + case SyntaxKind.ConversionOperatorDeclaration: + return OuterOrdering.ConversionOperators; + case SyntaxKind.MethodDeclaration: + return OuterOrdering.Methods; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.DelegateDeclaration: + case SyntaxKind.RecordDeclaration: + case SyntaxKind.RecordStructDeclaration: + return OuterOrdering.Types; + default: + return OuterOrdering.Remaining; } + } - private static bool ShouldCompareByName(MemberDeclarationSyntax x) + private static bool ShouldCompareByName(MemberDeclarationSyntax x) + { + // Constructors, destructors, indexers and operators should not be sorted by name. + // Note: Conversion operators should not be sorted by name either, but it's not + // necessary to deal with that here, because GetNameToken cannot return a + // name for them (there's only a NameSyntax, not a Token). + switch (x.Kind()) { - // Constructors, destructors, indexers and operators should not be sorted by name. - // Note: Conversion operators should not be sorted by name either, but it's not - // necessary to deal with that here, because GetNameToken cannot return a - // name for them (there's only a NameSyntax, not a Token). - switch (x.Kind()) - { - case SyntaxKind.ConstructorDeclaration: - case SyntaxKind.DestructorDeclaration: - case SyntaxKind.IndexerDeclaration: - case SyntaxKind.OperatorDeclaration: - return false; - default: - return true; - } + case SyntaxKind.ConstructorDeclaration: + case SyntaxKind.DestructorDeclaration: + case SyntaxKind.IndexerDeclaration: + case SyntaxKind.OperatorDeclaration: + return false; + default: + return true; } } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/MemberDeclarationsOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/MemberDeclarationsOrganizer.cs index 79cb7c836d34e..fc8c1642de4d9 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/MemberDeclarationsOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/MemberDeclarationsOrganizer.cs @@ -13,124 +13,123 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +internal static partial class MemberDeclarationsOrganizer { - internal static partial class MemberDeclarationsOrganizer + public static SyntaxList Organize( + SyntaxList members, + CancellationToken cancellationToken) { - public static SyntaxList Organize( - SyntaxList members, - CancellationToken cancellationToken) - { - // Break the list of members up into groups based on the PP - // directives between them. - var groups = members.SplitNodesOnPreprocessorBoundaries(cancellationToken); + // Break the list of members up into groups based on the PP + // directives between them. + var groups = members.SplitNodesOnPreprocessorBoundaries(cancellationToken); - // Go into each group and organize them. We'll then have a list of - // lists. Flatten that list and return that. - var sortedGroups = groups.Select(OrganizeMemberGroup).Flatten().ToList(); - - if (sortedGroups.SequenceEqual(members)) - { - return members; - } + // Go into each group and organize them. We'll then have a list of + // lists. Flatten that list and return that. + var sortedGroups = groups.Select(OrganizeMemberGroup).Flatten().ToList(); - return [.. sortedGroups]; + if (sortedGroups.SequenceEqual(members)) + { + return members; } - private static void TransferTrivia( - IList originalList, - IList finalList) where TSyntaxNode : SyntaxNode - { - Debug.Assert(originalList.Count == finalList.Count); + return [.. sortedGroups]; + } + + private static void TransferTrivia( + IList originalList, + IList finalList) where TSyntaxNode : SyntaxNode + { + Debug.Assert(originalList.Count == finalList.Count); - if (originalList.Count >= 2) + if (originalList.Count >= 2) + { + // Ok, we wanted to reorder the list. But we're definitely not done right now. While + // most of the list will look fine, we will have issues with the first node. First, + // we don't want to move any pp directives or banners that are on the first node. + // Second, it can often be the case that the node doesn't even have any trivia. We + // want to follow the user's style. So we find the node that was in the index that + // the first node moved to, and we attempt to keep an appropriate amount of + // whitespace based on that. + + // If the first node didn't move, then we don't need to do any of this special fixup + // logic. + if (originalList[0] != finalList[0]) { - // Ok, we wanted to reorder the list. But we're definitely not done right now. While - // most of the list will look fine, we will have issues with the first node. First, - // we don't want to move any pp directives or banners that are on the first node. - // Second, it can often be the case that the node doesn't even have any trivia. We - // want to follow the user's style. So we find the node that was in the index that - // the first node moved to, and we attempt to keep an appropriate amount of - // whitespace based on that. - - // If the first node didn't move, then we don't need to do any of this special fixup - // logic. - if (originalList[0] != finalList[0]) - { - // First. Strip any pp directives or banners on the first node. They have to - // move to the first node in the final list. - CopyBanner(originalList, finalList); - - // Now, we need to fix up the first node wherever it is in the final list. We - // need to strip it of its banner, and we need to add additional whitespace to - // match the user's style - - FixupOriginalFirstNode(originalList, finalList); - } + // First. Strip any pp directives or banners on the first node. They have to + // move to the first node in the final list. + CopyBanner(originalList, finalList); + + // Now, we need to fix up the first node wherever it is in the final list. We + // need to strip it of its banner, and we need to add additional whitespace to + // match the user's style + + FixupOriginalFirstNode(originalList, finalList); } } + } - private static void FixupOriginalFirstNode(IList originalList, IList finalList) where TSyntaxNode : SyntaxNode - { - // Now, find the original node in the final list. - var originalFirstNode = originalList[0]; - var indexInFinalList = finalList.IndexOf(originalFirstNode); + private static void FixupOriginalFirstNode(IList originalList, IList finalList) where TSyntaxNode : SyntaxNode + { + // Now, find the original node in the final list. + var originalFirstNode = originalList[0]; + var indexInFinalList = finalList.IndexOf(originalFirstNode); - // Find the initial node we had at that same index. - var originalNodeAtThatIndex = originalList[indexInFinalList]; + // Find the initial node we had at that same index. + var originalNodeAtThatIndex = originalList[indexInFinalList]; - // If that node had blank lines above it, then place that number of blank - // lines before the first node in the final list. - var blankLines = originalNodeAtThatIndex.GetLeadingBlankLines(); + // If that node had blank lines above it, then place that number of blank + // lines before the first node in the final list. + var blankLines = originalNodeAtThatIndex.GetLeadingBlankLines(); - originalFirstNode = originalFirstNode.GetNodeWithoutLeadingBannerAndPreprocessorDirectives() - .WithPrependedLeadingTrivia(blankLines); + originalFirstNode = originalFirstNode.GetNodeWithoutLeadingBannerAndPreprocessorDirectives() + .WithPrependedLeadingTrivia(blankLines); - finalList[indexInFinalList] = originalFirstNode; - } + finalList[indexInFinalList] = originalFirstNode; + } - private static void CopyBanner( - IList originalList, - IList finalList) where TSyntaxNode : SyntaxNode - { - // First. Strip any pp directives or banners on the first node. They - // have to stay at the top of the list. - var banner = originalList[0].GetLeadingBannerAndPreprocessorDirectives(); - - // Now, we want to remove any blank lines from the new first node and then - // reattach the banner. - var finalFirstNode = finalList[0]; - finalFirstNode = finalFirstNode.GetNodeWithoutLeadingBlankLines(); - finalFirstNode = finalFirstNode.WithLeadingTrivia(banner.Concat(finalFirstNode.GetLeadingTrivia())); - - // Place the updated first node back in the result list - finalList[0] = finalFirstNode; - } + private static void CopyBanner( + IList originalList, + IList finalList) where TSyntaxNode : SyntaxNode + { + // First. Strip any pp directives or banners on the first node. They + // have to stay at the top of the list. + var banner = originalList[0].GetLeadingBannerAndPreprocessorDirectives(); + + // Now, we want to remove any blank lines from the new first node and then + // reattach the banner. + var finalFirstNode = finalList[0]; + finalFirstNode = finalFirstNode.GetNodeWithoutLeadingBlankLines(); + finalFirstNode = finalFirstNode.WithLeadingTrivia(banner.Concat(finalFirstNode.GetLeadingTrivia())); + + // Place the updated first node back in the result list + finalList[0] = finalFirstNode; + } - private static IList OrganizeMemberGroup(IList members) + private static IList OrganizeMemberGroup(IList members) + { + if (members.Count > 1) { - if (members.Count > 1) + var initialList = new List(members); + + var finalList = initialList.OrderBy(new Comparer()).ToList(); + + if (!finalList.SequenceEqual(initialList)) { - var initialList = new List(members); - - var finalList = initialList.OrderBy(new Comparer()).ToList(); - - if (!finalList.SequenceEqual(initialList)) - { - // Ok, we wanted to reorder the list. But we're definitely not done right now. - // While most of the list will look fine, we will have issues with the first - // node. First, we don't want to move any pp directives or banners that are on - // the first node. Second, it can often be the case that the node doesn't even - // have any trivia. We want to follow the user's style. So we find the node that - // was in the index that the first node moved to, and we attempt to keep an - // appropriate amount of whitespace based on that. - TransferTrivia(initialList, finalList); - - return finalList; - } + // Ok, we wanted to reorder the list. But we're definitely not done right now. + // While most of the list will look fine, we will have issues with the first + // node. First, we don't want to move any pp directives or banners that are on + // the first node. Second, it can often be the case that the node doesn't even + // have any trivia. We want to follow the user's style. So we find the node that + // was in the index that the first node moved to, and we attempt to keep an + // appropriate amount of whitespace based on that. + TransferTrivia(initialList, finalList); + + return finalList; } - - return members; } + + return members; } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/MethodDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/MethodDeclarationOrganizer.cs index 3faba485fff2a..d6e500a21e8a7 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/MethodDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/MethodDeclarationOrganizer.cs @@ -11,33 +11,32 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class MethodDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class MethodDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public MethodDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public MethodDeclarationOrganizer() - { - } + } - protected override MethodDeclarationSyntax Organize( - MethodDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update( - attributeLists: syntax.AttributeLists, - modifiers: ModifiersOrganizer.Organize(syntax.Modifiers), - returnType: syntax.ReturnType, - explicitInterfaceSpecifier: syntax.ExplicitInterfaceSpecifier, - identifier: syntax.Identifier, - typeParameterList: syntax.TypeParameterList, - parameterList: syntax.ParameterList, - constraintClauses: syntax.ConstraintClauses, - body: syntax.Body, - expressionBody: syntax.ExpressionBody, - semicolonToken: syntax.SemicolonToken); - } + protected override MethodDeclarationSyntax Organize( + MethodDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update( + attributeLists: syntax.AttributeLists, + modifiers: ModifiersOrganizer.Organize(syntax.Modifiers), + returnType: syntax.ReturnType, + explicitInterfaceSpecifier: syntax.ExplicitInterfaceSpecifier, + identifier: syntax.Identifier, + typeParameterList: syntax.TypeParameterList, + parameterList: syntax.ParameterList, + constraintClauses: syntax.ConstraintClauses, + body: syntax.Body, + expressionBody: syntax.ExpressionBody, + semicolonToken: syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/ModifiersOrganizer.Comparer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/ModifiersOrganizer.Comparer.cs index 82638318c7618..7808882e3740b 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/ModifiersOrganizer.Comparer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/ModifiersOrganizer.Comparer.cs @@ -9,47 +9,46 @@ using System.Collections.Immutable; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +internal partial class ModifiersOrganizer { - internal partial class ModifiersOrganizer + private class Comparer : IComparer { - private class Comparer : IComparer + // TODO(cyrusn): Allow users to specify the ordering they want + private enum Ordering { - // TODO(cyrusn): Allow users to specify the ordering they want - private enum Ordering - { - Accessibility, - StaticInstance, - Remainder - } + Accessibility, + StaticInstance, + Remainder + } - public int Compare(SyntaxToken x, SyntaxToken y) + public int Compare(SyntaxToken x, SyntaxToken y) + { + if (x.Kind() == y.Kind()) { - if (x.Kind() == y.Kind()) - { - return 0; - } - - return ComparerWithState.CompareTo(x, y, s_comparers); + return 0; } - private static readonly ImmutableArray> s_comparers = - [t => t.Kind() == SyntaxKind.PartialKeyword, t => GetOrdering(t)]; + return ComparerWithState.CompareTo(x, y, s_comparers); + } + + private static readonly ImmutableArray> s_comparers = + [t => t.Kind() == SyntaxKind.PartialKeyword, t => GetOrdering(t)]; - private static Ordering GetOrdering(SyntaxToken token) + private static Ordering GetOrdering(SyntaxToken token) + { + switch (token.Kind()) { - switch (token.Kind()) - { - case SyntaxKind.StaticKeyword: - return Ordering.StaticInstance; - case SyntaxKind.PrivateKeyword: - case SyntaxKind.ProtectedKeyword: - case SyntaxKind.InternalKeyword: - case SyntaxKind.PublicKeyword: - return Ordering.Accessibility; - default: - return Ordering.Remainder; - } + case SyntaxKind.StaticKeyword: + return Ordering.StaticInstance; + case SyntaxKind.PrivateKeyword: + case SyntaxKind.ProtectedKeyword: + case SyntaxKind.InternalKeyword: + case SyntaxKind.PublicKeyword: + return Ordering.Accessibility; + default: + return Ordering.Remainder; } } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/ModifiersOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/ModifiersOrganizer.cs index 268fd8812e50f..d4cec0353fc69 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/ModifiersOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/ModifiersOrganizer.cs @@ -10,28 +10,27 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +internal static partial class ModifiersOrganizer { - internal static partial class ModifiersOrganizer + public static SyntaxTokenList Organize(SyntaxTokenList modifiers) { - public static SyntaxTokenList Organize(SyntaxTokenList modifiers) + if (modifiers.Count > 1 && !modifiers.SpansPreprocessorDirective()) { - if (modifiers.Count > 1 && !modifiers.SpansPreprocessorDirective()) - { - var initialList = new List(modifiers); - var leadingTrivia = initialList.First().LeadingTrivia; - initialList[0] = initialList[0].WithLeadingTrivia(SpecializedCollections.EmptyEnumerable()); + var initialList = new List(modifiers); + var leadingTrivia = initialList.First().LeadingTrivia; + initialList[0] = initialList[0].WithLeadingTrivia(SpecializedCollections.EmptyEnumerable()); - var finalList = initialList.OrderBy(new Comparer()).ToList(); - if (!initialList.SequenceEqual(finalList)) - { - finalList[0] = finalList[0].WithLeadingTrivia(leadingTrivia); + var finalList = initialList.OrderBy(new Comparer()).ToList(); + if (!initialList.SequenceEqual(finalList)) + { + finalList[0] = finalList[0].WithLeadingTrivia(leadingTrivia); - return finalList.ToSyntaxTokenList(); - } + return finalList.ToSyntaxTokenList(); } - - return modifiers; } + + return modifiers; } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/OperatorDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/OperatorDeclarationOrganizer.cs index 1209b5bba3307..de7347d41bc33 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/OperatorDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/OperatorDeclarationOrganizer.cs @@ -11,29 +11,28 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class OperatorDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class OperatorDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public OperatorDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public OperatorDeclarationOrganizer() - { - } + } - protected override OperatorDeclarationSyntax Organize( - OperatorDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update(syntax.AttributeLists, - ModifiersOrganizer.Organize(syntax.Modifiers), - syntax.ReturnType, - syntax.OperatorKeyword, - syntax.OperatorToken, - syntax.ParameterList, - syntax.Body, - syntax.SemicolonToken); - } + protected override OperatorDeclarationSyntax Organize( + OperatorDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update(syntax.AttributeLists, + ModifiersOrganizer.Organize(syntax.Modifiers), + syntax.ReturnType, + syntax.OperatorKeyword, + syntax.OperatorToken, + syntax.ParameterList, + syntax.Body, + syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/PropertyDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/PropertyDeclarationOrganizer.cs index 081a0cae80134..46e0cea009483 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/PropertyDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/PropertyDeclarationOrganizer.cs @@ -11,22 +11,21 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class PropertyDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class PropertyDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public PropertyDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public PropertyDeclarationOrganizer() - { - } + } - protected override PropertyDeclarationSyntax Organize( - PropertyDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.WithModifiers(ModifiersOrganizer.Organize(syntax.Modifiers)); - } + protected override PropertyDeclarationSyntax Organize( + PropertyDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.WithModifiers(ModifiersOrganizer.Organize(syntax.Modifiers)); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/RecordDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/RecordDeclarationOrganizer.cs index c839d9b8bd3ca..d805b06bb77e9 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/RecordDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/RecordDeclarationOrganizer.cs @@ -9,35 +9,34 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class RecordDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class RecordDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RecordDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public RecordDeclarationOrganizer() - { - } + } - protected override RecordDeclarationSyntax Organize( - RecordDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update( - syntax.AttributeLists, - ModifiersOrganizer.Organize(syntax.Modifiers), - syntax.Keyword, - syntax.ClassOrStructKeyword, - syntax.Identifier, - syntax.TypeParameterList, - syntax.ParameterList, - syntax.BaseList, - syntax.ConstraintClauses, - syntax.OpenBraceToken, - MemberDeclarationsOrganizer.Organize(syntax.Members, cancellationToken), - syntax.CloseBraceToken, - syntax.SemicolonToken); - } + protected override RecordDeclarationSyntax Organize( + RecordDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update( + syntax.AttributeLists, + ModifiersOrganizer.Organize(syntax.Modifiers), + syntax.Keyword, + syntax.ClassOrStructKeyword, + syntax.Identifier, + syntax.TypeParameterList, + syntax.ParameterList, + syntax.BaseList, + syntax.ConstraintClauses, + syntax.OpenBraceToken, + MemberDeclarationsOrganizer.Organize(syntax.Members, cancellationToken), + syntax.CloseBraceToken, + syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/Organizing/Organizers/StructDeclarationOrganizer.cs b/src/Features/CSharp/Portable/Organizing/Organizers/StructDeclarationOrganizer.cs index 8b552b91a048f..4e941c4707a19 100644 --- a/src/Features/CSharp/Portable/Organizing/Organizers/StructDeclarationOrganizer.cs +++ b/src/Features/CSharp/Portable/Organizing/Organizers/StructDeclarationOrganizer.cs @@ -11,33 +11,32 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Organizing.Organizers; -namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers +namespace Microsoft.CodeAnalysis.CSharp.Organizing.Organizers; + +[ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] +internal class StructDeclarationOrganizer : AbstractSyntaxNodeOrganizer { - [ExportSyntaxNodeOrganizer(LanguageNames.CSharp), Shared] - internal class StructDeclarationOrganizer : AbstractSyntaxNodeOrganizer + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public StructDeclarationOrganizer() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public StructDeclarationOrganizer() - { - } + } - protected override StructDeclarationSyntax Organize( - StructDeclarationSyntax syntax, - CancellationToken cancellationToken) - { - return syntax.Update( - syntax.AttributeLists, - ModifiersOrganizer.Organize(syntax.Modifiers), - syntax.Keyword, - syntax.Identifier, - syntax.TypeParameterList, - syntax.BaseList, - syntax.ConstraintClauses, - syntax.OpenBraceToken, - MemberDeclarationsOrganizer.Organize(syntax.Members, cancellationToken), - syntax.CloseBraceToken, - syntax.SemicolonToken); - } + protected override StructDeclarationSyntax Organize( + StructDeclarationSyntax syntax, + CancellationToken cancellationToken) + { + return syntax.Update( + syntax.AttributeLists, + ModifiersOrganizer.Organize(syntax.Modifiers), + syntax.Keyword, + syntax.Identifier, + syntax.TypeParameterList, + syntax.BaseList, + syntax.ConstraintClauses, + syntax.OpenBraceToken, + MemberDeclarationsOrganizer.Organize(syntax.Members, cancellationToken), + syntax.CloseBraceToken, + syntax.SemicolonToken); } } diff --git a/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProvider.cs b/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProvider.cs index afdc5867433f5..2c06e936fc06a 100644 --- a/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProvider.cs +++ b/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProvider.cs @@ -19,153 +19,152 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.QuickInfo +namespace Microsoft.CodeAnalysis.CSharp.QuickInfo; + +[ExportQuickInfoProvider(QuickInfoProviderNames.DiagnosticAnalyzer, LanguageNames.CSharp), Shared] +// This provider needs to run before the semantic quick info provider, because of the SuppressMessage attribute handling +// If it runs after it, BuildQuickInfoAsync is not called. This is not covered by a test. +[ExtensionOrder(Before = QuickInfoProviderNames.Semantic)] +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal class CSharpDiagnosticAnalyzerQuickInfoProvider(DiagnosticAnalyzerInfoCache.SharedGlobalCache globalCache) : CommonQuickInfoProvider { - [ExportQuickInfoProvider(QuickInfoProviderNames.DiagnosticAnalyzer, LanguageNames.CSharp), Shared] - // This provider needs to run before the semantic quick info provider, because of the SuppressMessage attribute handling - // If it runs after it, BuildQuickInfoAsync is not called. This is not covered by a test. - [ExtensionOrder(Before = QuickInfoProviderNames.Semantic)] - [method: ImportingConstructor] - [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - internal class CSharpDiagnosticAnalyzerQuickInfoProvider(DiagnosticAnalyzerInfoCache.SharedGlobalCache globalCache) : CommonQuickInfoProvider + private readonly DiagnosticAnalyzerInfoCache _diagnosticAnalyzerInfoCache = globalCache.AnalyzerInfoCache; + + protected override async Task BuildQuickInfoAsync( + QuickInfoContext context, + SyntaxToken token) { - private readonly DiagnosticAnalyzerInfoCache _diagnosticAnalyzerInfoCache = globalCache.AnalyzerInfoCache; + var document = context.Document; + return GetQuickinfoForPragmaWarning(document, token) ?? + (await GetQuickInfoForSuppressMessageAttributeAsync(document, token, context.CancellationToken).ConfigureAwait(false)); + } - protected override async Task BuildQuickInfoAsync( - QuickInfoContext context, - SyntaxToken token) - { - var document = context.Document; - return GetQuickinfoForPragmaWarning(document, token) ?? - (await GetQuickInfoForSuppressMessageAttributeAsync(document, token, context.CancellationToken).ConfigureAwait(false)); - } + protected override Task BuildQuickInfoAsync( + CommonQuickInfoContext context, + SyntaxToken token) + { + // TODO: This provider currently needs access to Document/Project to compute applicable analyzers + // and provide quick info, which is not available in CommonQuickInfoContext. + return Task.FromResult(null); + } - protected override Task BuildQuickInfoAsync( - CommonQuickInfoContext context, - SyntaxToken token) + private QuickInfoItem? GetQuickinfoForPragmaWarning(Document document, SyntaxToken token) + { + var errorCodeNode = token.Parent switch + { + PragmaWarningDirectiveTriviaSyntax directive + => token.IsKind(SyntaxKind.EndOfDirectiveToken) + ? directive.ErrorCodes.LastOrDefault() + : directive.ErrorCodes.FirstOrDefault(), + { Parent: PragmaWarningDirectiveTriviaSyntax } node => node, + _ => null, + }; + if (errorCodeNode is null) { - // TODO: This provider currently needs access to Document/Project to compute applicable analyzers - // and provide quick info, which is not available in CommonQuickInfoContext. - return Task.FromResult(null); + return null; } - private QuickInfoItem? GetQuickinfoForPragmaWarning(Document document, SyntaxToken token) + // https://docs.microsoft.com/en-US/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning + // warning-list: A comma-separated list of warning numbers. The "CS" prefix is optional. + // errorCodeNode is single error code from the comma separated list + var errorCode = errorCodeNode switch { - var errorCodeNode = token.Parent switch - { - PragmaWarningDirectiveTriviaSyntax directive - => token.IsKind(SyntaxKind.EndOfDirectiveToken) - ? directive.ErrorCodes.LastOrDefault() - : directive.ErrorCodes.FirstOrDefault(), - { Parent: PragmaWarningDirectiveTriviaSyntax } node => node, - _ => null, - }; - if (errorCodeNode is null) - { - return null; - } - - // https://docs.microsoft.com/en-US/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning - // warning-list: A comma-separated list of warning numbers. The "CS" prefix is optional. - // errorCodeNode is single error code from the comma separated list - var errorCode = errorCodeNode switch - { - // case CS0219 or SA0012: - IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText, - // case 0219 or 219: - // Take the number and add the "CS" prefix. - LiteralExpressionSyntax(SyntaxKind.NumericLiteralExpression) literal - => int.TryParse(literal.Token.ValueText, out var errorCodeNumber) - ? $"CS{errorCodeNumber:0000}" - : literal.Token.ValueText, - _ => null, - }; - if (errorCode is null) - { - return null; - } - - return GetQuickInfoFromSupportedDiagnosticsOfProjectAnalyzers(document, errorCode, errorCodeNode.Span); + // case CS0219 or SA0012: + IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText, + // case 0219 or 219: + // Take the number and add the "CS" prefix. + LiteralExpressionSyntax(SyntaxKind.NumericLiteralExpression) literal + => int.TryParse(literal.Token.ValueText, out var errorCodeNumber) + ? $"CS{errorCodeNumber:0000}" + : literal.Token.ValueText, + _ => null, + }; + if (errorCode is null) + { + return null; } - private async Task GetQuickInfoForSuppressMessageAttributeAsync( - Document document, - SyntaxToken token, - CancellationToken cancellationToken) + return GetQuickInfoFromSupportedDiagnosticsOfProjectAnalyzers(document, errorCode, errorCodeNode.Span); + } + + private async Task GetQuickInfoForSuppressMessageAttributeAsync( + Document document, + SyntaxToken token, + CancellationToken cancellationToken) + { + // SuppressMessageAttribute docs + // https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.suppressmessageattribute + var suppressMessageCheckIdArgument = token.GetAncestor() switch { - // SuppressMessageAttribute docs - // https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.suppressmessageattribute - var suppressMessageCheckIdArgument = token.GetAncestor() switch + AttributeArgumentSyntax { - AttributeArgumentSyntax + Parent: AttributeArgumentListSyntax { - Parent: AttributeArgumentListSyntax + Arguments: var arguments, + Parent: AttributeSyntax { - Arguments: var arguments, - Parent: AttributeSyntax - { - Name: var attributeName - } + Name: var attributeName } - } argument when - attributeName.IsSuppressMessageAttribute() && - (argument.NameColon is null - ? arguments.IndexOf(argument) == 1 // Positional argument "checkId" - : argument.NameColon.Name.Identifier.ValueText == "checkId") // Named argument "checkId" - => argument, - _ => null, - }; - - if (suppressMessageCheckIdArgument != null) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var checkIdObject = semanticModel.GetConstantValue(suppressMessageCheckIdArgument.Expression, cancellationToken); - if (checkIdObject.HasValue && checkIdObject.Value is string checkId) - { - var errorCode = checkId.ExtractErrorCodeFromCheckId(); - return GetQuickInfoFromSupportedDiagnosticsOfProjectAnalyzers(document, errorCode, suppressMessageCheckIdArgument.Span); } - } + } argument when + attributeName.IsSuppressMessageAttribute() && + (argument.NameColon is null + ? arguments.IndexOf(argument) == 1 // Positional argument "checkId" + : argument.NameColon.Name.Identifier.ValueText == "checkId") // Named argument "checkId" + => argument, + _ => null, + }; - return null; - } - - private QuickInfoItem? GetQuickInfoFromSupportedDiagnosticsOfProjectAnalyzers(Document document, - string errorCode, TextSpan location) + if (suppressMessageCheckIdArgument != null) { - var hostAnalyzers = document.Project.Solution.SolutionState.Analyzers; - var groupedDiagnostics = hostAnalyzers.GetDiagnosticDescriptorsPerReference(_diagnosticAnalyzerInfoCache, document.Project).Values; - var supportedDiagnostics = groupedDiagnostics.SelectMany(d => d); - var diagnosticDescriptor = supportedDiagnostics.FirstOrDefault(d => d.Id == errorCode); - if (diagnosticDescriptor != null) + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var checkIdObject = semanticModel.GetConstantValue(suppressMessageCheckIdArgument.Expression, cancellationToken); + if (checkIdObject.HasValue && checkIdObject.Value is string checkId) { - return CreateQuickInfo(location, diagnosticDescriptor); + var errorCode = checkId.ExtractErrorCodeFromCheckId(); + return GetQuickInfoFromSupportedDiagnosticsOfProjectAnalyzers(document, errorCode, suppressMessageCheckIdArgument.Span); } - - return null; } - private static QuickInfoItem CreateQuickInfo(TextSpan location, DiagnosticDescriptor descriptor, - params TextSpan[] relatedSpans) + return null; + } + + private QuickInfoItem? GetQuickInfoFromSupportedDiagnosticsOfProjectAnalyzers(Document document, + string errorCode, TextSpan location) + { + var hostAnalyzers = document.Project.Solution.SolutionState.Analyzers; + var groupedDiagnostics = hostAnalyzers.GetDiagnosticDescriptorsPerReference(_diagnosticAnalyzerInfoCache, document.Project).Values; + var supportedDiagnostics = groupedDiagnostics.SelectMany(d => d); + var diagnosticDescriptor = supportedDiagnostics.FirstOrDefault(d => d.Id == errorCode); + if (diagnosticDescriptor != null) { - var description = - descriptor.Title.ToStringOrNull() ?? - descriptor.Description.ToStringOrNull() ?? - descriptor.MessageFormat.ToStringOrNull() ?? - descriptor.Id; - var idTag = !string.IsNullOrWhiteSpace(descriptor.HelpLinkUri) - ? new TaggedText(TextTags.Text, descriptor.Id, TaggedTextStyle.None, descriptor.HelpLinkUri, descriptor.HelpLinkUri) - : new TaggedText(TextTags.Text, descriptor.Id); - return QuickInfoItem.Create(location, sections: new[] - { - QuickInfoSection.Create(QuickInfoSectionKinds.Description, new[] - { - idTag, - new TaggedText(TextTags.Punctuation, ":"), - new TaggedText(TextTags.Space, " "), - new TaggedText(TextTags.Text, description) - }.ToImmutableArray()) - }.ToImmutableArray(), relatedSpans: relatedSpans.ToImmutableArray()); + return CreateQuickInfo(location, diagnosticDescriptor); } + + return null; + } + + private static QuickInfoItem CreateQuickInfo(TextSpan location, DiagnosticDescriptor descriptor, + params TextSpan[] relatedSpans) + { + var description = + descriptor.Title.ToStringOrNull() ?? + descriptor.Description.ToStringOrNull() ?? + descriptor.MessageFormat.ToStringOrNull() ?? + descriptor.Id; + var idTag = !string.IsNullOrWhiteSpace(descriptor.HelpLinkUri) + ? new TaggedText(TextTags.Text, descriptor.Id, TaggedTextStyle.None, descriptor.HelpLinkUri, descriptor.HelpLinkUri) + : new TaggedText(TextTags.Text, descriptor.Id); + return QuickInfoItem.Create(location, sections: new[] + { + QuickInfoSection.Create(QuickInfoSectionKinds.Description, new[] + { + idTag, + new TaggedText(TextTags.Punctuation, ":"), + new TaggedText(TextTags.Space, " "), + new TaggedText(TextTags.Text, description) + }.ToImmutableArray()) + }.ToImmutableArray(), relatedSpans: relatedSpans.ToImmutableArray()); } } diff --git a/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProviderExtensions.cs b/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProviderExtensions.cs index 6c1017a8d8e49..4b5d3d946df5f 100644 --- a/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProviderExtensions.cs +++ b/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProviderExtensions.cs @@ -7,45 +7,44 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.QuickInfo +namespace Microsoft.CodeAnalysis.CSharp.QuickInfo; + +internal static class CSharpDiagnosticAnalyzerQuickInfoProviderExtensions { - internal static class CSharpDiagnosticAnalyzerQuickInfoProviderExtensions + public static string? ToStringOrNull(this LocalizableString @this) { - public static string? ToStringOrNull(this LocalizableString @this) + var result = @this.ToString(); + if (string.IsNullOrWhiteSpace(result)) { - var result = @this.ToString(); - if (string.IsNullOrWhiteSpace(result)) - { - return null; - } - - return result; + return null; } - public static bool IsSuppressMessageAttribute(this NameSyntax? name) - { - if (name == null) - { - return false; - } - - var nameValue = name.GetNameToken().ValueText; - var stringComparer = StringComparer.Ordinal; - return - stringComparer.Equals(nameValue, nameof(SuppressMessageAttribute)) || - stringComparer.Equals(nameValue, "SuppressMessage"); - } + return result; + } - public static string ExtractErrorCodeFromCheckId(this string checkId) + public static bool IsSuppressMessageAttribute(this NameSyntax? name) + { + if (name == null) { - // checkId short and long name rules: - // https://docs.microsoft.com/en-us/visualstudio/code-quality/in-source-suppression-overview?view=vs-2019#suppressmessage-attribute - var position = checkId.IndexOf(':'); - var errorCode = position == -1 - ? checkId - : checkId[..position]; - errorCode = errorCode.Trim(); - return errorCode; + return false; } + + var nameValue = name.GetNameToken().ValueText; + var stringComparer = StringComparer.Ordinal; + return + stringComparer.Equals(nameValue, nameof(SuppressMessageAttribute)) || + stringComparer.Equals(nameValue, "SuppressMessage"); + } + + public static string ExtractErrorCodeFromCheckId(this string checkId) + { + // checkId short and long name rules: + // https://docs.microsoft.com/en-us/visualstudio/code-quality/in-source-suppression-overview?view=vs-2019#suppressmessage-attribute + var position = checkId.IndexOf(':'); + var errorCode = position == -1 + ? checkId + : checkId[..position]; + errorCode = errorCode.Trim(); + return errorCode; } } diff --git a/src/Features/CSharp/Portable/QuickInfo/CSharpQuickInfoSevice.cs b/src/Features/CSharp/Portable/QuickInfo/CSharpQuickInfoSevice.cs index 4d683e6bdaba2..6be617c52aa45 100644 --- a/src/Features/CSharp/Portable/QuickInfo/CSharpQuickInfoSevice.cs +++ b/src/Features/CSharp/Portable/QuickInfo/CSharpQuickInfoSevice.cs @@ -8,27 +8,26 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.QuickInfo; -namespace Microsoft.CodeAnalysis.CSharp.QuickInfo +namespace Microsoft.CodeAnalysis.CSharp.QuickInfo; + +[ExportLanguageServiceFactory(typeof(QuickInfoService), LanguageNames.CSharp), Shared] +internal class CSharpQuickInfoServiceFactory : ILanguageServiceFactory { - [ExportLanguageServiceFactory(typeof(QuickInfoService), LanguageNames.CSharp), Shared] - internal class CSharpQuickInfoServiceFactory : ILanguageServiceFactory + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpQuickInfoServiceFactory() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpQuickInfoServiceFactory() - { - } - - public ILanguageService CreateLanguageService(HostLanguageServices languageServices) - => new CSharpQuickInfoService(languageServices.LanguageServices); } - internal class CSharpQuickInfoService : QuickInfoServiceWithProviders + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + => new CSharpQuickInfoService(languageServices.LanguageServices); +} + +internal class CSharpQuickInfoService : QuickInfoServiceWithProviders +{ + internal CSharpQuickInfoService(LanguageServices services) + : base(services) { - internal CSharpQuickInfoService(LanguageServices services) - : base(services) - { - } } } diff --git a/src/Features/CSharp/Portable/QuickInfo/CSharpSemanticQuickInfoProvider.cs b/src/Features/CSharp/Portable/QuickInfo/CSharpSemanticQuickInfoProvider.cs index 468493d737c17..275923aec259f 100644 --- a/src/Features/CSharp/Portable/QuickInfo/CSharpSemanticQuickInfoProvider.cs +++ b/src/Features/CSharp/Portable/QuickInfo/CSharpSemanticQuickInfoProvider.cs @@ -11,121 +11,120 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.QuickInfo; -namespace Microsoft.CodeAnalysis.CSharp.QuickInfo +namespace Microsoft.CodeAnalysis.CSharp.QuickInfo; + +[ExportQuickInfoProvider(QuickInfoProviderNames.Semantic, LanguageNames.CSharp), Shared] +internal class CSharpSemanticQuickInfoProvider : CommonSemanticQuickInfoProvider { - [ExportQuickInfoProvider(QuickInfoProviderNames.Semantic, LanguageNames.CSharp), Shared] - internal class CSharpSemanticQuickInfoProvider : CommonSemanticQuickInfoProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpSemanticQuickInfoProvider() + { + } + + /// + /// If the token is the '=>' in a lambda, or the 'delegate' in an anonymous function, + /// return the syntax for the lambda or anonymous function. + /// + protected override bool GetBindableNodeForTokenIndicatingLambda(SyntaxToken token, [NotNullWhen(returnValue: true)] out SyntaxNode? found) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpSemanticQuickInfoProvider() + if (token.IsKind(SyntaxKind.EqualsGreaterThanToken) + && token.Parent is (kind: SyntaxKind.ParenthesizedLambdaExpression or SyntaxKind.SimpleLambdaExpression)) + { + // () => + found = token.Parent; + return true; + } + else if (token.IsKind(SyntaxKind.DelegateKeyword) && token.Parent.IsKind(SyntaxKind.AnonymousMethodExpression)) { + // delegate (...) { ... } + found = token.Parent; + return true; } - /// - /// If the token is the '=>' in a lambda, or the 'delegate' in an anonymous function, - /// return the syntax for the lambda or anonymous function. - /// - protected override bool GetBindableNodeForTokenIndicatingLambda(SyntaxToken token, [NotNullWhen(returnValue: true)] out SyntaxNode? found) + found = null; + return false; + } + + protected override bool GetBindableNodeForTokenIndicatingPossibleIndexerAccess(SyntaxToken token, [NotNullWhen(returnValue: true)] out SyntaxNode? found) + { + if (token.Kind() is SyntaxKind.CloseBracketToken or SyntaxKind.OpenBracketToken && + token.Parent?.Parent.IsKind(SyntaxKind.ElementAccessExpression) == true) { - if (token.IsKind(SyntaxKind.EqualsGreaterThanToken) - && token.Parent is (kind: SyntaxKind.ParenthesizedLambdaExpression or SyntaxKind.SimpleLambdaExpression)) - { - // () => - found = token.Parent; - return true; - } - else if (token.IsKind(SyntaxKind.DelegateKeyword) && token.Parent.IsKind(SyntaxKind.AnonymousMethodExpression)) - { - // delegate (...) { ... } - found = token.Parent; - return true; - } - - found = null; - return false; + found = token.Parent.Parent; + return true; } - protected override bool GetBindableNodeForTokenIndicatingPossibleIndexerAccess(SyntaxToken token, [NotNullWhen(returnValue: true)] out SyntaxNode? found) + found = null; + return false; + } + + protected override bool GetBindableNodeForTokenIndicatingMemberAccess(SyntaxToken token, out SyntaxToken found) + { + if (token.IsKind(SyntaxKind.DotToken) && + token.Parent is MemberAccessExpressionSyntax memberAccess) { - if (token.Kind() is SyntaxKind.CloseBracketToken or SyntaxKind.OpenBracketToken && - token.Parent?.Parent.IsKind(SyntaxKind.ElementAccessExpression) == true) - { - found = token.Parent.Parent; - return true; - } - - found = null; - return false; + found = memberAccess.Name.Identifier; + return true; } - protected override bool GetBindableNodeForTokenIndicatingMemberAccess(SyntaxToken token, out SyntaxToken found) + found = default; + return false; + } + + protected override bool ShouldCheckPreviousToken(SyntaxToken token) + => !token.Parent.IsKind(SyntaxKind.XmlCrefAttribute); + + protected override NullableFlowState GetNullabilityAnalysis(SemanticModel semanticModel, ISymbol symbol, SyntaxNode node, CancellationToken cancellationToken) + { + // Anything less than C# 8 we just won't show anything, even if the compiler could theoretically give analysis + var parseOptions = (CSharpParseOptions)semanticModel.SyntaxTree!.Options; + if (parseOptions.LanguageVersion < LanguageVersion.CSharp8) { - if (token.IsKind(SyntaxKind.DotToken) && - token.Parent is MemberAccessExpressionSyntax memberAccess) - { - found = memberAccess.Name.Identifier; - return true; - } - - found = default; - return false; + return NullableFlowState.None; } - protected override bool ShouldCheckPreviousToken(SyntaxToken token) - => !token.Parent.IsKind(SyntaxKind.XmlCrefAttribute); + // If the user doesn't have nullable enabled, don't show anything. For now we're not trying to be more precise if the user has just annotations or just + // warnings. If the user has annotations off then things that are oblivious might become non-null (which is a lie) and if the user has warnings off then + // that probably implies they're not actually trying to know if their code is correct. We can revisit this if we have specific user scenarios. + var nullableContext = semanticModel.GetNullableContext(node.SpanStart); + if (!nullableContext.WarningsEnabled() || !nullableContext.AnnotationsEnabled()) + { + return NullableFlowState.None; + } - protected override NullableFlowState GetNullabilityAnalysis(SemanticModel semanticModel, ISymbol symbol, SyntaxNode node, CancellationToken cancellationToken) + // Although GetTypeInfo can return nullability for uses of all sorts of things, it's not always useful for quick info. + // For example, if you have a call to a method with a nullable return, the fact it can be null is already captured + // in the return type shown -- there's no flow analysis information there. + switch (symbol) { - // Anything less than C# 8 we just won't show anything, even if the compiler could theoretically give analysis - var parseOptions = (CSharpParseOptions)semanticModel.SyntaxTree!.Options; - if (parseOptions.LanguageVersion < LanguageVersion.CSharp8) - { - return NullableFlowState.None; - } - - // If the user doesn't have nullable enabled, don't show anything. For now we're not trying to be more precise if the user has just annotations or just - // warnings. If the user has annotations off then things that are oblivious might become non-null (which is a lie) and if the user has warnings off then - // that probably implies they're not actually trying to know if their code is correct. We can revisit this if we have specific user scenarios. - var nullableContext = semanticModel.GetNullableContext(node.SpanStart); - if (!nullableContext.WarningsEnabled() || !nullableContext.AnnotationsEnabled()) - { - return NullableFlowState.None; - } - - // Although GetTypeInfo can return nullability for uses of all sorts of things, it's not always useful for quick info. - // For example, if you have a call to a method with a nullable return, the fact it can be null is already captured - // in the return type shown -- there's no flow analysis information there. - switch (symbol) - { - // Ignore constant values for nullability flow state - case IFieldSymbol { HasConstantValue: true }: return default; - case ILocalSymbol { HasConstantValue: true }: return default; - - // Symbols with useful quick info - case IFieldSymbol _: - case ILocalSymbol _: - case IParameterSymbol _: - case IPropertySymbol _: - case IRangeVariableSymbol _: - break; - - default: - return default; - } - - var typeInfo = semanticModel.GetTypeInfo(node, cancellationToken); - - // Nullability is a reference type only feature, value types can use - // something like "int?" to be nullable but that ends up encasing as - // Nullable, which isn't exactly the same. To avoid confusion and - // extra noise, we won't show nullable flow state for value types - if (typeInfo.Type?.IsValueType == true) - { + // Ignore constant values for nullability flow state + case IFieldSymbol { HasConstantValue: true }: return default; + case ILocalSymbol { HasConstantValue: true }: return default; + + // Symbols with useful quick info + case IFieldSymbol _: + case ILocalSymbol _: + case IParameterSymbol _: + case IPropertySymbol _: + case IRangeVariableSymbol _: + break; + + default: return default; - } + } + + var typeInfo = semanticModel.GetTypeInfo(node, cancellationToken); - return typeInfo.Nullability.FlowState; + // Nullability is a reference type only feature, value types can use + // something like "int?" to be nullable but that ends up encasing as + // Nullable, which isn't exactly the same. To avoid confusion and + // extra noise, we won't show nullable flow state for value types + if (typeInfo.Type?.IsValueType == true) + { + return default; } + + return typeInfo.Nullability.FlowState; } } diff --git a/src/Features/CSharp/Portable/QuickInfo/CSharpSyntacticQuickInfoProvider.cs b/src/Features/CSharp/Portable/QuickInfo/CSharpSyntacticQuickInfoProvider.cs index 50edbaf5b6672..aefc132ca7965 100644 --- a/src/Features/CSharp/Portable/QuickInfo/CSharpSyntacticQuickInfoProvider.cs +++ b/src/Features/CSharp/Portable/QuickInfo/CSharpSyntacticQuickInfoProvider.cs @@ -15,150 +15,149 @@ using Microsoft.CodeAnalysis.QuickInfo; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.QuickInfo +namespace Microsoft.CodeAnalysis.CSharp.QuickInfo; + +[ExportQuickInfoProvider(QuickInfoProviderNames.Syntactic, LanguageNames.CSharp), Shared] +[ExtensionOrder(After = QuickInfoProviderNames.Semantic)] +internal class CSharpSyntacticQuickInfoProvider : CommonQuickInfoProvider { - [ExportQuickInfoProvider(QuickInfoProviderNames.Syntactic, LanguageNames.CSharp), Shared] - [ExtensionOrder(After = QuickInfoProviderNames.Semantic)] - internal class CSharpSyntacticQuickInfoProvider : CommonQuickInfoProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpSyntacticQuickInfoProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpSyntacticQuickInfoProvider() + } + + protected override Task BuildQuickInfoAsync( + QuickInfoContext context, + SyntaxToken token) + => Task.FromResult(BuildQuickInfo(token, context.CancellationToken)); + + protected override Task BuildQuickInfoAsync( + CommonQuickInfoContext context, + SyntaxToken token) + => Task.FromResult(BuildQuickInfo(token, context.CancellationToken)); + + private static QuickInfoItem? BuildQuickInfo(SyntaxToken token, CancellationToken cancellationToken) + { + switch (token.Kind()) { + case SyntaxKind.CloseBraceToken: + return BuildQuickInfoCloseBrace(token); + case SyntaxKind.HashToken: + case SyntaxKind.EndRegionKeyword: + case SyntaxKind.EndIfKeyword: + case SyntaxKind.ElseKeyword: + case SyntaxKind.ElifKeyword: + case SyntaxKind.EndOfDirectiveToken: + return BuildQuickInfoDirectives(token, cancellationToken); + default: + return null; } + } - protected override Task BuildQuickInfoAsync( - QuickInfoContext context, - SyntaxToken token) - => Task.FromResult(BuildQuickInfo(token, context.CancellationToken)); - - protected override Task BuildQuickInfoAsync( - CommonQuickInfoContext context, - SyntaxToken token) - => Task.FromResult(BuildQuickInfo(token, context.CancellationToken)); + private static QuickInfoItem? BuildQuickInfoCloseBrace(SyntaxToken token) + { + // Don't show for interpolations + if (token.Parent is InterpolationSyntax interpolation && + interpolation.CloseBraceToken == token) + { + return null; + } - private static QuickInfoItem? BuildQuickInfo(SyntaxToken token, CancellationToken cancellationToken) + // Now check if we can find an open brace. + var parent = token.Parent!; + var openBrace = parent.ChildNodesAndTokens().FirstOrDefault(n => n.Kind() == SyntaxKind.OpenBraceToken).AsToken(); + if (openBrace.Kind() != SyntaxKind.OpenBraceToken) { - switch (token.Kind()) - { - case SyntaxKind.CloseBraceToken: - return BuildQuickInfoCloseBrace(token); - case SyntaxKind.HashToken: - case SyntaxKind.EndRegionKeyword: - case SyntaxKind.EndIfKeyword: - case SyntaxKind.ElseKeyword: - case SyntaxKind.ElifKeyword: - case SyntaxKind.EndOfDirectiveToken: - return BuildQuickInfoDirectives(token, cancellationToken); - default: - return null; - } + return null; } - private static QuickInfoItem? BuildQuickInfoCloseBrace(SyntaxToken token) + var spanStart = parent.SpanStart; + var spanEnd = openBrace.Span.End; + + // If the parent is a scope block, check and include nearby comments around the open brace + // LeadingTrivia is preferred + if (IsScopeBlock(parent)) { - // Don't show for interpolations - if (token.Parent is InterpolationSyntax interpolation && - interpolation.CloseBraceToken == token) - { - return null; - } + MarkInterestedSpanNearbyScopeBlock(parent, openBrace, ref spanStart, ref spanEnd); + } + // If the parent is a child of a property/method declaration, object/array creation, or control flow node, + // then walk up one higher so we can show more useful context + else if (parent.GetFirstToken() == openBrace) + { + // parent.Parent must be non-null, because for GetFirstToken() to have returned something it would have had to walk up to its parent + spanStart = parent.Parent!.SpanStart; + } - // Now check if we can find an open brace. - var parent = token.Parent!; - var openBrace = parent.ChildNodesAndTokens().FirstOrDefault(n => n.Kind() == SyntaxKind.OpenBraceToken).AsToken(); - if (openBrace.Kind() != SyntaxKind.OpenBraceToken) - { - return null; - } + // encode document spans that correspond to the text to show + var spans = ImmutableArray.Create(TextSpan.FromBounds(spanStart, spanEnd)); + return QuickInfoItem.Create(token.Span, relatedSpans: spans); + } - var spanStart = parent.SpanStart; - var spanEnd = openBrace.Span.End; + private static bool IsScopeBlock(SyntaxNode node) + => node.IsKind(SyntaxKind.Block) + && node.Parent?.Kind() is SyntaxKind.Block or SyntaxKind.SwitchSection or SyntaxKind.GlobalStatement; - // If the parent is a scope block, check and include nearby comments around the open brace - // LeadingTrivia is preferred - if (IsScopeBlock(parent)) - { - MarkInterestedSpanNearbyScopeBlock(parent, openBrace, ref spanStart, ref spanEnd); - } - // If the parent is a child of a property/method declaration, object/array creation, or control flow node, - // then walk up one higher so we can show more useful context - else if (parent.GetFirstToken() == openBrace) - { - // parent.Parent must be non-null, because for GetFirstToken() to have returned something it would have had to walk up to its parent - spanStart = parent.Parent!.SpanStart; - } + private static void MarkInterestedSpanNearbyScopeBlock(SyntaxNode block, SyntaxToken openBrace, ref int spanStart, ref int spanEnd) + { + var searchListAbove = openBrace.LeadingTrivia.Reverse(); + if (TryFindFurthestNearbyComment(ref searchListAbove, out var nearbyComment)) + { + spanStart = nearbyComment.SpanStart; + return; + } - // encode document spans that correspond to the text to show - var spans = ImmutableArray.Create(TextSpan.FromBounds(spanStart, spanEnd)); - return QuickInfoItem.Create(token.Span, relatedSpans: spans); + var nextToken = block.FindToken(openBrace.FullSpan.End); + var searchListBelow = nextToken.LeadingTrivia; + if (TryFindFurthestNearbyComment(ref searchListBelow, out nearbyComment)) + { + spanEnd = nearbyComment.Span.End; + return; } + } - private static bool IsScopeBlock(SyntaxNode node) - => node.IsKind(SyntaxKind.Block) - && node.Parent?.Kind() is SyntaxKind.Block or SyntaxKind.SwitchSection or SyntaxKind.GlobalStatement; + private static bool TryFindFurthestNearbyComment(ref T triviaSearchList, out SyntaxTrivia nearbyTrivia) + where T : IEnumerable + { + nearbyTrivia = default; - private static void MarkInterestedSpanNearbyScopeBlock(SyntaxNode block, SyntaxToken openBrace, ref int spanStart, ref int spanEnd) + foreach (var trivia in triviaSearchList) { - var searchListAbove = openBrace.LeadingTrivia.Reverse(); - if (TryFindFurthestNearbyComment(ref searchListAbove, out var nearbyComment)) + if (trivia.IsSingleOrMultiLineComment()) { - spanStart = nearbyComment.SpanStart; - return; + nearbyTrivia = trivia; } - - var nextToken = block.FindToken(openBrace.FullSpan.End); - var searchListBelow = nextToken.LeadingTrivia; - if (TryFindFurthestNearbyComment(ref searchListBelow, out nearbyComment)) + else if (trivia.Kind() is not SyntaxKind.WhitespaceTrivia and not SyntaxKind.EndOfLineTrivia) { - spanEnd = nearbyComment.Span.End; - return; + break; } } - private static bool TryFindFurthestNearbyComment(ref T triviaSearchList, out SyntaxTrivia nearbyTrivia) - where T : IEnumerable - { - nearbyTrivia = default; + return nearbyTrivia.IsSingleOrMultiLineComment(); + } - foreach (var trivia in triviaSearchList) + private static QuickInfoItem? BuildQuickInfoDirectives(SyntaxToken token, CancellationToken cancellationToken) + { + if (token.Parent is DirectiveTriviaSyntax directiveTrivia) + { + if (directiveTrivia is EndRegionDirectiveTriviaSyntax) { - if (trivia.IsSingleOrMultiLineComment()) - { - nearbyTrivia = trivia; - } - else if (trivia.Kind() is not SyntaxKind.WhitespaceTrivia and not SyntaxKind.EndOfLineTrivia) - { - break; - } + var regionStart = directiveTrivia.GetMatchingDirective(cancellationToken); + if (regionStart is not null) + return QuickInfoItem.Create(token.Span, relatedSpans: [regionStart.Span]); } - - return nearbyTrivia.IsSingleOrMultiLineComment(); - } - - private static QuickInfoItem? BuildQuickInfoDirectives(SyntaxToken token, CancellationToken cancellationToken) - { - if (token.Parent is DirectiveTriviaSyntax directiveTrivia) + else if (directiveTrivia is ElifDirectiveTriviaSyntax or ElseDirectiveTriviaSyntax or EndIfDirectiveTriviaSyntax) { - if (directiveTrivia is EndRegionDirectiveTriviaSyntax) - { - var regionStart = directiveTrivia.GetMatchingDirective(cancellationToken); - if (regionStart is not null) - return QuickInfoItem.Create(token.Span, relatedSpans: [regionStart.Span]); - } - else if (directiveTrivia is ElifDirectiveTriviaSyntax or ElseDirectiveTriviaSyntax or EndIfDirectiveTriviaSyntax) - { - var matchingDirectives = directiveTrivia.GetMatchingConditionalDirectives(cancellationToken); - var matchesBefore = matchingDirectives - .TakeWhile(d => d.SpanStart < directiveTrivia.SpanStart) - .Select(d => d.Span) - .ToImmutableArray(); - if (matchesBefore.Length > 0) - return QuickInfoItem.Create(token.Span, relatedSpans: matchesBefore); - } + var matchingDirectives = directiveTrivia.GetMatchingConditionalDirectives(cancellationToken); + var matchesBefore = matchingDirectives + .TakeWhile(d => d.SpanStart < directiveTrivia.SpanStart) + .Select(d => d.Span) + .ToImmutableArray(); + if (matchesBefore.Length > 0) + return QuickInfoItem.Create(token.Span, relatedSpans: matchesBefore); } - - return null; } + + return null; } } diff --git a/src/Features/CSharp/Portable/RemoveUnusedVariable/CSharpRemoveUnusedVariableCodeFixProvider.cs b/src/Features/CSharp/Portable/RemoveUnusedVariable/CSharpRemoveUnusedVariableCodeFixProvider.cs index 97504f3a794ab..af248b4bc338d 100644 --- a/src/Features/CSharp/Portable/RemoveUnusedVariable/CSharpRemoveUnusedVariableCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/RemoveUnusedVariable/CSharpRemoveUnusedVariableCodeFixProvider.cs @@ -14,73 +14,72 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.RemoveUnusedVariable; -namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedVariable +namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedVariable; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnusedVariable), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)] +internal partial class CSharpRemoveUnusedVariableCodeFixProvider : AbstractRemoveUnusedVariableCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnusedVariable), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)] - internal partial class CSharpRemoveUnusedVariableCodeFixProvider : AbstractRemoveUnusedVariableCodeFixProvider - { - public const string CS0168 = nameof(CS0168); - public const string CS0219 = nameof(CS0219); + public const string CS0168 = nameof(CS0168); + public const string CS0219 = nameof(CS0219); - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpRemoveUnusedVariableCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpRemoveUnusedVariableCodeFixProvider() + { + } - public sealed override ImmutableArray FixableDiagnosticIds - => [CS0168, CS0219]; + public sealed override ImmutableArray FixableDiagnosticIds + => [CS0168, CS0219]; - protected override bool IsCatchDeclarationIdentifier(SyntaxToken token) - => token.Parent is CatchDeclarationSyntax catchDeclaration && catchDeclaration.Identifier == token; + protected override bool IsCatchDeclarationIdentifier(SyntaxToken token) + => token.Parent is CatchDeclarationSyntax catchDeclaration && catchDeclaration.Identifier == token; - protected override SyntaxNode GetNodeToRemoveOrReplace(SyntaxNode node) + protected override SyntaxNode GetNodeToRemoveOrReplace(SyntaxNode node) + { + node = node.Parent; + if (node.Kind() == SyntaxKind.SimpleAssignmentExpression) { - node = node.Parent; - if (node.Kind() == SyntaxKind.SimpleAssignmentExpression) + var parent = node.Parent; + if (parent.Kind() == SyntaxKind.ExpressionStatement) { - var parent = node.Parent; - if (parent.Kind() == SyntaxKind.ExpressionStatement) - { - return parent; - } - else - { - return node; - } + return parent; } - - return null; - } - - protected override void RemoveOrReplaceNode(SyntaxEditor editor, SyntaxNode node, IBlockFactsService blockFacts) - { - switch (node.Kind()) + else { - case SyntaxKind.SimpleAssignmentExpression: - editor.ReplaceNode(node, ((AssignmentExpressionSyntax)node).Right); - return; - default: - RemoveNode(editor, node.IsParentKind(SyntaxKind.GlobalStatement) ? node.Parent : node, blockFacts); - return; + return node; } } - protected override SeparatedSyntaxList GetVariables(LocalDeclarationStatementSyntax localDeclarationStatement) - => localDeclarationStatement.Declaration.Variables; + return null; + } - protected override bool ShouldOfferFixForLocalDeclaration(IBlockFactsService blockFacts, SyntaxNode node) + protected override void RemoveOrReplaceNode(SyntaxEditor editor, SyntaxNode node, IBlockFactsService blockFacts) + { + switch (node.Kind()) { - // If the fix location is not for a local declaration then we can allow it (eg, when inside a for - // or catch). - if (node.Parent?.Parent is not LocalDeclarationStatementSyntax localDeclaration) - return true; - - // Local declarations must be parented by an executable block, or global statement, otherwise - // removing them would be invalid (and more than likely crash the fixer) - return localDeclaration.Parent is GlobalStatementSyntax || - blockFacts.IsExecutableBlock(localDeclaration.Parent); + case SyntaxKind.SimpleAssignmentExpression: + editor.ReplaceNode(node, ((AssignmentExpressionSyntax)node).Right); + return; + default: + RemoveNode(editor, node.IsParentKind(SyntaxKind.GlobalStatement) ? node.Parent : node, blockFacts); + return; } } + + protected override SeparatedSyntaxList GetVariables(LocalDeclarationStatementSyntax localDeclarationStatement) + => localDeclarationStatement.Declaration.Variables; + + protected override bool ShouldOfferFixForLocalDeclaration(IBlockFactsService blockFacts, SyntaxNode node) + { + // If the fix location is not for a local declaration then we can allow it (eg, when inside a for + // or catch). + if (node.Parent?.Parent is not LocalDeclarationStatementSyntax localDeclaration) + return true; + + // Local declarations must be parented by an executable block, or global statement, otherwise + // removing them would be invalid (and more than likely crash the fixer) + return localDeclaration.Parent is GlobalStatementSyntax || + blockFacts.IsExecutableBlock(localDeclaration.Parent); + } } diff --git a/src/Features/CSharp/Portable/Rename/CSharpRenameIssuesService.cs b/src/Features/CSharp/Portable/Rename/CSharpRenameIssuesService.cs index 539aad515685d..188b35719f45d 100644 --- a/src/Features/CSharp/Portable/Rename/CSharpRenameIssuesService.cs +++ b/src/Features/CSharp/Portable/Rename/CSharpRenameIssuesService.cs @@ -10,68 +10,67 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Rename; -namespace Microsoft.CodeAnalysis.CSharp.Rename +namespace Microsoft.CodeAnalysis.CSharp.Rename; + +[ExportLanguageService(typeof(IRenameIssuesService), LanguageNames.CSharp), Shared] +internal sealed class CSharpRenameIssuesService : IRenameIssuesService { - [ExportLanguageService(typeof(IRenameIssuesService), LanguageNames.CSharp), Shared] - internal sealed class CSharpRenameIssuesService : IRenameIssuesService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpRenameIssuesService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpRenameIssuesService() + } + + public bool CheckLanguageSpecificIssues( + SemanticModel semanticModel, ISymbol symbol, SyntaxToken triggerToken, [NotNullWhen(true)] out string? langError) + { + if (triggerToken.IsTypeNamedDynamic() && + symbol.Kind == SymbolKind.DynamicType) { + langError = FeaturesResources.You_cannot_rename_this_element; + return true; } - public bool CheckLanguageSpecificIssues( - SemanticModel semanticModel, ISymbol symbol, SyntaxToken triggerToken, [NotNullWhen(true)] out string? langError) + if (IsTypeNamedVarInVariableOrFieldDeclaration(triggerToken)) { - if (triggerToken.IsTypeNamedDynamic() && - symbol.Kind == SymbolKind.DynamicType) + // To check if var in this context is a real type, or the keyword, we need to + // speculatively bind the identifier "var". If it returns a symbol, it's a real type, + // if not, it's the keyword. + // see bugs 659683 (compiler API) and 659705 (rename/workspace api) for examples + var symbolForVar = semanticModel.GetSpeculativeSymbolInfo( + triggerToken.SpanStart, + triggerToken.Parent!, + SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol; + + if (symbolForVar == null) { langError = FeaturesResources.You_cannot_rename_this_element; return true; } - - if (IsTypeNamedVarInVariableOrFieldDeclaration(triggerToken)) - { - // To check if var in this context is a real type, or the keyword, we need to - // speculatively bind the identifier "var". If it returns a symbol, it's a real type, - // if not, it's the keyword. - // see bugs 659683 (compiler API) and 659705 (rename/workspace api) for examples - var symbolForVar = semanticModel.GetSpeculativeSymbolInfo( - triggerToken.SpanStart, - triggerToken.Parent!, - SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol; - - if (symbolForVar == null) - { - langError = FeaturesResources.You_cannot_rename_this_element; - return true; - } - } - - langError = null; - return false; } - private static bool IsTypeNamedVarInVariableOrFieldDeclaration(SyntaxToken token) + langError = null; + return false; + } + + private static bool IsTypeNamedVarInVariableOrFieldDeclaration(SyntaxToken token) + { + var parent = token.Parent; + if (parent.IsKind(SyntaxKind.IdentifierName)) { - var parent = token.Parent; - if (parent.IsKind(SyntaxKind.IdentifierName)) + TypeSyntax? declaredType = null; + if (parent?.Parent is VariableDeclarationSyntax varDecl) { - TypeSyntax? declaredType = null; - if (parent?.Parent is VariableDeclarationSyntax varDecl) - { - declaredType = varDecl.Type; - } - else if (parent?.Parent is FieldDeclarationSyntax fieldDecl) - { - declaredType = fieldDecl.Declaration.Type; - } - - return declaredType == parent && token.Text == "var"; + declaredType = varDecl.Type; + } + else if (parent?.Parent is FieldDeclarationSyntax fieldDecl) + { + declaredType = fieldDecl.Declaration.Type; } - return false; + return declaredType == parent && token.Text == "var"; } + + return false; } } diff --git a/src/Features/CSharp/Portable/ReplaceDocCommentTextWithTag/CSharpReplaceDocCommentTextWithTagCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ReplaceDocCommentTextWithTag/CSharpReplaceDocCommentTextWithTagCodeRefactoringProvider.cs index a6df23867cc11..6e5d85420ba78 100644 --- a/src/Features/CSharp/Portable/ReplaceDocCommentTextWithTag/CSharpReplaceDocCommentTextWithTagCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ReplaceDocCommentTextWithTag/CSharpReplaceDocCommentTextWithTagCodeRefactoringProvider.cs @@ -9,28 +9,27 @@ using Microsoft.CodeAnalysis.ReplaceDocCommentTextWithTag; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.ReplaceDocCommentTextWithTag +namespace Microsoft.CodeAnalysis.CSharp.ReplaceDocCommentTextWithTag; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ReplaceDocCommentTextWithTag), Shared] +internal sealed class CSharpReplaceDocCommentTextWithTagCodeRefactoringProvider : + AbstractReplaceDocCommentTextWithTagCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ReplaceDocCommentTextWithTag), Shared] - internal sealed class CSharpReplaceDocCommentTextWithTagCodeRefactoringProvider : - AbstractReplaceDocCommentTextWithTagCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpReplaceDocCommentTextWithTagCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpReplaceDocCommentTextWithTagCodeRefactoringProvider() - { - } + } - protected override bool IsXmlTextToken(SyntaxToken token) - => token.Kind() is SyntaxKind.XmlTextLiteralToken or SyntaxKind.XmlTextLiteralNewLineToken; + protected override bool IsXmlTextToken(SyntaxToken token) + => token.Kind() is SyntaxKind.XmlTextLiteralToken or SyntaxKind.XmlTextLiteralNewLineToken; - protected override bool IsInXMLAttribute(SyntaxToken token) - => token.GetRequiredParent().Kind() is SyntaxKind.XmlCrefAttribute or SyntaxKind.XmlNameAttribute or SyntaxKind.XmlTextAttribute; + protected override bool IsInXMLAttribute(SyntaxToken token) + => token.GetRequiredParent().Kind() is SyntaxKind.XmlCrefAttribute or SyntaxKind.XmlNameAttribute or SyntaxKind.XmlTextAttribute; - protected override bool IsKeyword(string text) - => SyntaxFacts.IsKeywordKind(SyntaxFacts.GetKeywordKind(text)) || SyntaxFacts.IsContextualKeyword(SyntaxFacts.GetContextualKeywordKind(text)); + protected override bool IsKeyword(string text) + => SyntaxFacts.IsKeywordKind(SyntaxFacts.GetKeywordKind(text)) || SyntaxFacts.IsContextualKeyword(SyntaxFacts.GetContextualKeywordKind(text)); - protected override SyntaxNode ParseExpression(string text) - => SyntaxFactory.ParseExpression(text); - } + protected override SyntaxNode ParseExpression(string text) + => SyntaxFactory.ParseExpression(text); } diff --git a/src/Features/CSharp/Portable/ReplaceMethodWithProperty/CSharpReplaceMethodWithPropertyService.cs b/src/Features/CSharp/Portable/ReplaceMethodWithProperty/CSharpReplaceMethodWithPropertyService.cs index a2c8a5106d446..72a1f9d82fd06 100644 --- a/src/Features/CSharp/Portable/ReplaceMethodWithProperty/CSharpReplaceMethodWithPropertyService.cs +++ b/src/Features/CSharp/Portable/ReplaceMethodWithProperty/CSharpReplaceMethodWithPropertyService.cs @@ -19,385 +19,384 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.ReplaceMethodWithProperty; -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ReplaceMethodWithProperty +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ReplaceMethodWithProperty; + +[ExportLanguageService(typeof(IReplaceMethodWithPropertyService), LanguageNames.CSharp), Shared] +internal class CSharpReplaceMethodWithPropertyService : AbstractReplaceMethodWithPropertyService, IReplaceMethodWithPropertyService { - [ExportLanguageService(typeof(IReplaceMethodWithPropertyService), LanguageNames.CSharp), Shared] - internal class CSharpReplaceMethodWithPropertyService : AbstractReplaceMethodWithPropertyService, IReplaceMethodWithPropertyService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpReplaceMethodWithPropertyService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpReplaceMethodWithPropertyService() + } + + public void RemoveSetMethod(SyntaxEditor editor, SyntaxNode setMethodDeclaration) + => editor.RemoveNode(setMethodDeclaration); + + public void ReplaceGetMethodWithProperty( + CodeGenerationOptions options, + ParseOptions parseOptions, + SyntaxEditor editor, + SemanticModel semanticModel, + GetAndSetMethods getAndSetMethods, + string propertyName, bool nameChanged, + CancellationToken cancellationToken) + { + if (getAndSetMethods.GetMethodDeclaration is not MethodDeclarationSyntax getMethodDeclaration) { + return; } - public void RemoveSetMethod(SyntaxEditor editor, SyntaxNode setMethodDeclaration) - => editor.RemoveNode(setMethodDeclaration); - - public void ReplaceGetMethodWithProperty( - CodeGenerationOptions options, - ParseOptions parseOptions, - SyntaxEditor editor, - SemanticModel semanticModel, - GetAndSetMethods getAndSetMethods, - string propertyName, bool nameChanged, - CancellationToken cancellationToken) - { - if (getAndSetMethods.GetMethodDeclaration is not MethodDeclarationSyntax getMethodDeclaration) - { - return; - } + var languageVersion = parseOptions.LanguageVersion(); + var newProperty = ConvertMethodsToProperty( + (CSharpCodeGenerationOptions)options, languageVersion, + semanticModel, editor.Generator, + getAndSetMethods, propertyName, nameChanged, cancellationToken); - var languageVersion = parseOptions.LanguageVersion(); - var newProperty = ConvertMethodsToProperty( - (CSharpCodeGenerationOptions)options, languageVersion, - semanticModel, editor.Generator, - getAndSetMethods, propertyName, nameChanged, cancellationToken); + editor.ReplaceNode(getMethodDeclaration, newProperty); + } - editor.ReplaceNode(getMethodDeclaration, newProperty); - } + public static SyntaxNode ConvertMethodsToProperty( + CSharpCodeGenerationOptions options, LanguageVersion languageVersion, + SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, + string propertyName, bool nameChanged, CancellationToken cancellationToken) + { + var propertyDeclaration = ConvertMethodsToPropertyWorker( + options, languageVersion, semanticModel, + generator, getAndSetMethods, propertyName, nameChanged, cancellationToken); - public static SyntaxNode ConvertMethodsToProperty( - CSharpCodeGenerationOptions options, LanguageVersion languageVersion, - SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, - string propertyName, bool nameChanged, CancellationToken cancellationToken) + var expressionBodyPreference = options.PreferExpressionBodiedProperties.Value; + if (expressionBodyPreference != ExpressionBodyPreference.Never) { - var propertyDeclaration = ConvertMethodsToPropertyWorker( - options, languageVersion, semanticModel, - generator, getAndSetMethods, propertyName, nameChanged, cancellationToken); - - var expressionBodyPreference = options.PreferExpressionBodiedProperties.Value; - if (expressionBodyPreference != ExpressionBodyPreference.Never) + if (propertyDeclaration.AccessorList is { Accessors: [(kind: SyntaxKind.GetAccessorDeclaration) getAccessor] }) { - if (propertyDeclaration.AccessorList is { Accessors: [(kind: SyntaxKind.GetAccessorDeclaration) getAccessor] }) + if (getAccessor.ExpressionBody != null) { - if (getAccessor.ExpressionBody != null) - { - return propertyDeclaration.WithExpressionBody(getAccessor.ExpressionBody) - .WithSemicolonToken(getAccessor.SemicolonToken) - .WithAccessorList(null); - } - else if (getAccessor.Body != null && - getAccessor.Body.TryConvertToArrowExpressionBody( - propertyDeclaration.Kind(), languageVersion, expressionBodyPreference, cancellationToken, - out var arrowExpression, out var semicolonToken)) - { - return propertyDeclaration.WithExpressionBody(arrowExpression) - .WithSemicolonToken(semicolonToken) - .WithAccessorList(null); - } + return propertyDeclaration.WithExpressionBody(getAccessor.ExpressionBody) + .WithSemicolonToken(getAccessor.SemicolonToken) + .WithAccessorList(null); } - } - else - { - if (propertyDeclaration.ExpressionBody != null && - propertyDeclaration.ExpressionBody.TryConvertToBlock( - propertyDeclaration.SemicolonToken, - createReturnStatementForExpression: true, - block: out var block)) + else if (getAccessor.Body != null && + getAccessor.Body.TryConvertToArrowExpressionBody( + propertyDeclaration.Kind(), languageVersion, expressionBodyPreference, cancellationToken, + out var arrowExpression, out var semicolonToken)) { - var accessor = - SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) - .WithBody(block); - - var accessorList = SyntaxFactory.AccessorList([accessor]); - return propertyDeclaration.WithAccessorList(accessorList) - .WithExpressionBody(null) - .WithSemicolonToken(default); + return propertyDeclaration.WithExpressionBody(arrowExpression) + .WithSemicolonToken(semicolonToken) + .WithAccessorList(null); } } - - return propertyDeclaration; } - - public static PropertyDeclarationSyntax ConvertMethodsToPropertyWorker( - CSharpCodeGenerationOptions options, LanguageVersion languageVersion, - SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, - string propertyName, bool nameChanged, CancellationToken cancellationToken) + else { - var getMethodDeclaration = (MethodDeclarationSyntax)getAndSetMethods.GetMethodDeclaration; - var setMethodDeclaration = getAndSetMethods.SetMethodDeclaration as MethodDeclarationSyntax; - var getAccessor = CreateGetAccessor(getAndSetMethods, options, languageVersion, cancellationToken); - var setAccessor = CreateSetAccessor(semanticModel, generator, getAndSetMethods, options, languageVersion, cancellationToken); - - var nameToken = GetPropertyName(getMethodDeclaration.Identifier, propertyName, nameChanged); - var warning = GetWarning(getAndSetMethods); - if (warning != null) + if (propertyDeclaration.ExpressionBody != null && + propertyDeclaration.ExpressionBody.TryConvertToBlock( + propertyDeclaration.SemicolonToken, + createReturnStatementForExpression: true, + block: out var block)) { - nameToken = nameToken.WithAdditionalAnnotations(WarningAnnotation.Create(warning)); - } - - var property = SyntaxFactory.PropertyDeclaration( - getMethodDeclaration.AttributeLists, getMethodDeclaration.Modifiers, - getMethodDeclaration.ReturnType, getMethodDeclaration.ExplicitInterfaceSpecifier, - nameToken, accessorList: null); - - // copy 'unsafe' from the set method, if it hasn't been already copied from the get method - if (setMethodDeclaration?.Modifiers.Any(SyntaxKind.UnsafeKeyword) == true - && !property.Modifiers.Any(SyntaxKind.UnsafeKeyword)) - { - property = property.AddModifiers(SyntaxFactory.Token(SyntaxKind.UnsafeKeyword)); + var accessor = + SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithBody(block); + + var accessorList = SyntaxFactory.AccessorList([accessor]); + return propertyDeclaration.WithAccessorList(accessorList) + .WithExpressionBody(null) + .WithSemicolonToken(default); } + } - property = SetLeadingTrivia( - CSharpSyntaxFacts.Instance, getAndSetMethods, property); + return propertyDeclaration; + } - var accessorList = SyntaxFactory.AccessorList([getAccessor]); - if (setAccessor != null) - { - accessorList = accessorList.AddAccessors(setAccessor); - } + public static PropertyDeclarationSyntax ConvertMethodsToPropertyWorker( + CSharpCodeGenerationOptions options, LanguageVersion languageVersion, + SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, + string propertyName, bool nameChanged, CancellationToken cancellationToken) + { + var getMethodDeclaration = (MethodDeclarationSyntax)getAndSetMethods.GetMethodDeclaration; + var setMethodDeclaration = getAndSetMethods.SetMethodDeclaration as MethodDeclarationSyntax; + var getAccessor = CreateGetAccessor(getAndSetMethods, options, languageVersion, cancellationToken); + var setAccessor = CreateSetAccessor(semanticModel, generator, getAndSetMethods, options, languageVersion, cancellationToken); + + var nameToken = GetPropertyName(getMethodDeclaration.Identifier, propertyName, nameChanged); + var warning = GetWarning(getAndSetMethods); + if (warning != null) + { + nameToken = nameToken.WithAdditionalAnnotations(WarningAnnotation.Create(warning)); + } - property = property.WithAccessorList(accessorList); + var property = SyntaxFactory.PropertyDeclaration( + getMethodDeclaration.AttributeLists, getMethodDeclaration.Modifiers, + getMethodDeclaration.ReturnType, getMethodDeclaration.ExplicitInterfaceSpecifier, + nameToken, accessorList: null); - return property.WithAdditionalAnnotations(Formatter.Annotation); + // copy 'unsafe' from the set method, if it hasn't been already copied from the get method + if (setMethodDeclaration?.Modifiers.Any(SyntaxKind.UnsafeKeyword) == true + && !property.Modifiers.Any(SyntaxKind.UnsafeKeyword)) + { + property = property.AddModifiers(SyntaxFactory.Token(SyntaxKind.UnsafeKeyword)); } - private static SyntaxToken GetPropertyName(SyntaxToken identifier, string propertyName, bool nameChanged) + property = SetLeadingTrivia( + CSharpSyntaxFacts.Instance, getAndSetMethods, property); + + var accessorList = SyntaxFactory.AccessorList([getAccessor]); + if (setAccessor != null) { - return nameChanged - ? SyntaxFactory.Identifier(propertyName) - : identifier; + accessorList = accessorList.AddAccessors(setAccessor); } - private static AccessorDeclarationSyntax CreateGetAccessor( - GetAndSetMethods getAndSetMethods, CSharpCodeGenerationOptions options, LanguageVersion languageVersion, CancellationToken cancellationToken) - { - var accessorDeclaration = CreateGetAccessorWorker(getAndSetMethods); + property = property.WithAccessorList(accessorList); - return UseExpressionOrBlockBodyIfDesired( - options, languageVersion, accessorDeclaration, cancellationToken); - } + return property.WithAdditionalAnnotations(Formatter.Annotation); + } - private static AccessorDeclarationSyntax UseExpressionOrBlockBodyIfDesired( - CSharpCodeGenerationOptions options, LanguageVersion languageVersion, - AccessorDeclarationSyntax accessorDeclaration, CancellationToken cancellationToken) + private static SyntaxToken GetPropertyName(SyntaxToken identifier, string propertyName, bool nameChanged) + { + return nameChanged + ? SyntaxFactory.Identifier(propertyName) + : identifier; + } + + private static AccessorDeclarationSyntax CreateGetAccessor( + GetAndSetMethods getAndSetMethods, CSharpCodeGenerationOptions options, LanguageVersion languageVersion, CancellationToken cancellationToken) + { + var accessorDeclaration = CreateGetAccessorWorker(getAndSetMethods); + + return UseExpressionOrBlockBodyIfDesired( + options, languageVersion, accessorDeclaration, cancellationToken); + } + + private static AccessorDeclarationSyntax UseExpressionOrBlockBodyIfDesired( + CSharpCodeGenerationOptions options, LanguageVersion languageVersion, + AccessorDeclarationSyntax accessorDeclaration, CancellationToken cancellationToken) + { + var expressionBodyPreference = options.PreferExpressionBodiedAccessors.Value; + if (accessorDeclaration?.Body != null && expressionBodyPreference != ExpressionBodyPreference.Never) { - var expressionBodyPreference = options.PreferExpressionBodiedAccessors.Value; - if (accessorDeclaration?.Body != null && expressionBodyPreference != ExpressionBodyPreference.Never) + if (accessorDeclaration.Body.TryConvertToArrowExpressionBody( + accessorDeclaration.Kind(), languageVersion, expressionBodyPreference, cancellationToken, + out var arrowExpression, out var semicolonToken)) { - if (accessorDeclaration.Body.TryConvertToArrowExpressionBody( - accessorDeclaration.Kind(), languageVersion, expressionBodyPreference, cancellationToken, - out var arrowExpression, out var semicolonToken)) - { - return accessorDeclaration.WithBody(null) - .WithExpressionBody(arrowExpression) - .WithSemicolonToken(semicolonToken) - .WithAdditionalAnnotations(Formatter.Annotation); - } + return accessorDeclaration.WithBody(null) + .WithExpressionBody(arrowExpression) + .WithSemicolonToken(semicolonToken) + .WithAdditionalAnnotations(Formatter.Annotation); } - else if (accessorDeclaration?.ExpressionBody != null && expressionBodyPreference == ExpressionBodyPreference.Never) + } + else if (accessorDeclaration?.ExpressionBody != null && expressionBodyPreference == ExpressionBodyPreference.Never) + { + if (accessorDeclaration.ExpressionBody.TryConvertToBlock( + accessorDeclaration.SemicolonToken, + createReturnStatementForExpression: accessorDeclaration.Kind() == SyntaxKind.GetAccessorDeclaration, + block: out var block)) { - if (accessorDeclaration.ExpressionBody.TryConvertToBlock( - accessorDeclaration.SemicolonToken, - createReturnStatementForExpression: accessorDeclaration.Kind() == SyntaxKind.GetAccessorDeclaration, - block: out var block)) - { - return accessorDeclaration.WithExpressionBody(null) - .WithSemicolonToken(default) - .WithBody(block) - .WithAdditionalAnnotations(Formatter.Annotation); - } + return accessorDeclaration.WithExpressionBody(null) + .WithSemicolonToken(default) + .WithBody(block) + .WithAdditionalAnnotations(Formatter.Annotation); } - - return accessorDeclaration; } - private static AccessorDeclarationSyntax CreateGetAccessorWorker(GetAndSetMethods getAndSetMethods) + return accessorDeclaration; + } + + private static AccessorDeclarationSyntax CreateGetAccessorWorker(GetAndSetMethods getAndSetMethods) + { + var getMethodDeclaration = getAndSetMethods.GetMethodDeclaration as MethodDeclarationSyntax; + + var accessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration); + + if (getMethodDeclaration.ExpressionBody != null) { - var getMethodDeclaration = getAndSetMethods.GetMethodDeclaration as MethodDeclarationSyntax; + return accessor.WithExpressionBody(getMethodDeclaration.ExpressionBody) + .WithSemicolonToken(getMethodDeclaration.SemicolonToken); + } - var accessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration); + if (getMethodDeclaration.SemicolonToken.Kind() != SyntaxKind.None) + { + return accessor.WithSemicolonToken(getMethodDeclaration.SemicolonToken); + } - if (getMethodDeclaration.ExpressionBody != null) - { - return accessor.WithExpressionBody(getMethodDeclaration.ExpressionBody) - .WithSemicolonToken(getMethodDeclaration.SemicolonToken); - } + if (getMethodDeclaration.Body != null) + { + return accessor.WithBody(getMethodDeclaration.Body.WithAdditionalAnnotations(Formatter.Annotation)); + } - if (getMethodDeclaration.SemicolonToken.Kind() != SyntaxKind.None) - { - return accessor.WithSemicolonToken(getMethodDeclaration.SemicolonToken); - } + return accessor; + } - if (getMethodDeclaration.Body != null) - { - return accessor.WithBody(getMethodDeclaration.Body.WithAdditionalAnnotations(Formatter.Annotation)); - } + private static AccessorDeclarationSyntax CreateSetAccessor( + SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, + CSharpCodeGenerationOptions options, LanguageVersion languageVersion, CancellationToken cancellationToken) + { + var accessorDeclaration = CreateSetAccessorWorker(semanticModel, generator, getAndSetMethods); + return UseExpressionOrBlockBodyIfDesired(options, languageVersion, accessorDeclaration, cancellationToken); + } - return accessor; + private static AccessorDeclarationSyntax CreateSetAccessorWorker( + SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods) + { + var setMethod = getAndSetMethods.SetMethod; + if (getAndSetMethods.SetMethodDeclaration is not MethodDeclarationSyntax setMethodDeclaration || setMethod?.Parameters.Length != 1) + { + return null; } - private static AccessorDeclarationSyntax CreateSetAccessor( - SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, - CSharpCodeGenerationOptions options, LanguageVersion languageVersion, CancellationToken cancellationToken) + var getMethod = getAndSetMethods.GetMethod; + var accessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration); + + if (getMethod.DeclaredAccessibility != setMethod.DeclaredAccessibility) { - var accessorDeclaration = CreateSetAccessorWorker(semanticModel, generator, getAndSetMethods); - return UseExpressionOrBlockBodyIfDesired(options, languageVersion, accessorDeclaration, cancellationToken); + accessor = (AccessorDeclarationSyntax)generator.WithAccessibility(accessor, setMethod.DeclaredAccessibility); } - private static AccessorDeclarationSyntax CreateSetAccessorWorker( - SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods) + if (setMethodDeclaration.ExpressionBody != null) { - var setMethod = getAndSetMethods.SetMethod; - if (getAndSetMethods.SetMethodDeclaration is not MethodDeclarationSyntax setMethodDeclaration || setMethod?.Parameters.Length != 1) - { - return null; - } + var oldExpressionBody = setMethodDeclaration.ExpressionBody; + var expression = ReplaceReferencesToParameterWithValue( + semanticModel, setMethod.Parameters[0], oldExpressionBody.Expression); - var getMethod = getAndSetMethods.GetMethod; - var accessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration); + return accessor.WithExpressionBody(oldExpressionBody.WithExpression(expression)) + .WithSemicolonToken(setMethodDeclaration.SemicolonToken); + } - if (getMethod.DeclaredAccessibility != setMethod.DeclaredAccessibility) - { - accessor = (AccessorDeclarationSyntax)generator.WithAccessibility(accessor, setMethod.DeclaredAccessibility); - } + if (setMethodDeclaration.SemicolonToken.Kind() != SyntaxKind.None) + { + return accessor.WithSemicolonToken(setMethodDeclaration.SemicolonToken); + } - if (setMethodDeclaration.ExpressionBody != null) - { - var oldExpressionBody = setMethodDeclaration.ExpressionBody; - var expression = ReplaceReferencesToParameterWithValue( - semanticModel, setMethod.Parameters[0], oldExpressionBody.Expression); + if (setMethodDeclaration.Body != null) + { + var body = ReplaceReferencesToParameterWithValue(semanticModel, setMethod.Parameters[0], setMethodDeclaration.Body); + return accessor.WithBody(body.WithAdditionalAnnotations(Formatter.Annotation)); + } - return accessor.WithExpressionBody(oldExpressionBody.WithExpression(expression)) - .WithSemicolonToken(setMethodDeclaration.SemicolonToken); - } + return accessor; + } - if (setMethodDeclaration.SemicolonToken.Kind() != SyntaxKind.None) - { - return accessor.WithSemicolonToken(setMethodDeclaration.SemicolonToken); - } + private static TNode ReplaceReferencesToParameterWithValue(SemanticModel semanticModel, IParameterSymbol parameter, TNode node) + where TNode : SyntaxNode + { + var rewriter = new Rewriter(semanticModel, parameter); + return (TNode)rewriter.Visit(node); + } - if (setMethodDeclaration.Body != null) + private class Rewriter(SemanticModel semanticModel, IParameterSymbol parameter) : CSharpSyntaxRewriter + { + private readonly SemanticModel _semanticModel = semanticModel; + private readonly IParameterSymbol _parameter = parameter; + + public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) + { + if (_parameter.Equals(_semanticModel.GetSymbolInfo(node).Symbol)) { - var body = ReplaceReferencesToParameterWithValue(semanticModel, setMethod.Parameters[0], setMethodDeclaration.Body); - return accessor.WithBody(body.WithAdditionalAnnotations(Formatter.Annotation)); + return SyntaxFactory.IdentifierName("value").WithTriviaFrom(node); } - return accessor; + return node; } + } - private static TNode ReplaceReferencesToParameterWithValue(SemanticModel semanticModel, IParameterSymbol parameter, TNode node) - where TNode : SyntaxNode + // We use the callback form if "ReplaceNode" here because we want to see the + // invocation expression after any rewrites we already did when rewriting previous + // 'get' references. + private static readonly Action s_replaceGetReferenceInvocation = + (editor, invocation, nameNode, newName) => editor.ReplaceNode(invocation, (i, g) => { - var rewriter = new Rewriter(semanticModel, parameter); - return (TNode)rewriter.Visit(node); - } + var currentInvocation = (InvocationExpressionSyntax)i; - private class Rewriter(SemanticModel semanticModel, IParameterSymbol parameter) : CSharpSyntaxRewriter - { - private readonly SemanticModel _semanticModel = semanticModel; - private readonly IParameterSymbol _parameter = parameter; + var currentName = currentInvocation.Expression.GetRightmostName(); + return currentInvocation.Expression.ReplaceNode(currentName, newName); + }); - public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) + private static readonly Action s_replaceSetReferenceInvocation = + (editor, invocation, nameNode, newName) => + { + if (invocation.ArgumentList?.Arguments.Count != 1 || + invocation.ArgumentList.Arguments[0].Expression.Kind() == SyntaxKind.DeclarationExpression) { - if (_parameter.Equals(_semanticModel.GetSymbolInfo(node).Symbol)) - { - return SyntaxFactory.IdentifierName("value").WithTriviaFrom(node); - } - - return node; + var annotation = ConflictAnnotation.Create(FeaturesResources.Only_methods_with_a_single_argument_which_is_not_an_out_variable_declaration_can_be_replaced_with_a_property); + editor.ReplaceNode(nameNode, newName.WithIdentifier(newName.Identifier.WithAdditionalAnnotations(annotation))); + return; } - } - // We use the callback form if "ReplaceNode" here because we want to see the - // invocation expression after any rewrites we already did when rewriting previous - // 'get' references. - private static readonly Action s_replaceGetReferenceInvocation = - (editor, invocation, nameNode, newName) => editor.ReplaceNode(invocation, (i, g) => + // We use the callback form if "ReplaceNode" here because we want to see the + // invocation expression after any rewrites we already did when rewriting the + // 'get' references. + editor.ReplaceNode(invocation, (i, g) => { var currentInvocation = (InvocationExpressionSyntax)i; + // looks like a.b.Goo(arg) => a.b.NewName = arg + nameNode = currentInvocation.Expression.GetRightmostName(); + currentInvocation = (InvocationExpressionSyntax)g.ReplaceNode(currentInvocation, nameNode, newName); - var currentName = currentInvocation.Expression.GetRightmostName(); - return currentInvocation.Expression.ReplaceNode(currentName, newName); - }); + // Wrap the argument in parentheses (in order to not introduce any precedence problems). + // But also add a simplification annotation so we can remove the parens if possible. + var argumentExpression = currentInvocation.ArgumentList.Arguments[0].Expression.Parenthesize(); - private static readonly Action s_replaceSetReferenceInvocation = - (editor, invocation, nameNode, newName) => - { - if (invocation.ArgumentList?.Arguments.Count != 1 || - invocation.ArgumentList.Arguments[0].Expression.Kind() == SyntaxKind.DeclarationExpression) - { - var annotation = ConflictAnnotation.Create(FeaturesResources.Only_methods_with_a_single_argument_which_is_not_an_out_variable_declaration_can_be_replaced_with_a_property); - editor.ReplaceNode(nameNode, newName.WithIdentifier(newName.Identifier.WithAdditionalAnnotations(annotation))); - return; - } + var expression = SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, currentInvocation.Expression, argumentExpression); - // We use the callback form if "ReplaceNode" here because we want to see the - // invocation expression after any rewrites we already did when rewriting the - // 'get' references. - editor.ReplaceNode(invocation, (i, g) => - { - var currentInvocation = (InvocationExpressionSyntax)i; - // looks like a.b.Goo(arg) => a.b.NewName = arg - nameNode = currentInvocation.Expression.GetRightmostName(); - currentInvocation = (InvocationExpressionSyntax)g.ReplaceNode(currentInvocation, nameNode, newName); - - // Wrap the argument in parentheses (in order to not introduce any precedence problems). - // But also add a simplification annotation so we can remove the parens if possible. - var argumentExpression = currentInvocation.ArgumentList.Arguments[0].Expression.Parenthesize(); - - var expression = SyntaxFactory.AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, currentInvocation.Expression, argumentExpression); + return expression.Parenthesize(); + }); + }; - return expression.Parenthesize(); - }); - }; + public void ReplaceGetReference(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged) + => ReplaceInvocation(editor, nameToken, propertyName, nameChanged, s_replaceGetReferenceInvocation); - public void ReplaceGetReference(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged) - => ReplaceInvocation(editor, nameToken, propertyName, nameChanged, s_replaceGetReferenceInvocation); + public void ReplaceSetReference(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged) + => ReplaceInvocation(editor, nameToken, propertyName, nameChanged, s_replaceSetReferenceInvocation); - public void ReplaceSetReference(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged) - => ReplaceInvocation(editor, nameToken, propertyName, nameChanged, s_replaceSetReferenceInvocation); + public static void ReplaceInvocation(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged, + Action replace) + { + if (nameToken.Kind() != SyntaxKind.IdentifierToken) + { + return; + } - public static void ReplaceInvocation(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged, - Action replace) + if (nameToken.Parent is not IdentifierNameSyntax nameNode) { - if (nameToken.Kind() != SyntaxKind.IdentifierToken) - { - return; - } + return; + } - if (nameToken.Parent is not IdentifierNameSyntax nameNode) - { - return; - } + var invocation = nameNode?.FirstAncestorOrSelf(); - var invocation = nameNode?.FirstAncestorOrSelf(); + var newName = nameNode; + if (nameChanged) + { + newName = SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(propertyName)); + } - var newName = nameNode; - if (nameChanged) - { - newName = SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(propertyName)); - } + newName = newName.WithTriviaFrom(invocation is null ? nameToken.Parent : invocation); - newName = newName.WithTriviaFrom(invocation is null ? nameToken.Parent : invocation); + var invocationExpression = invocation?.Expression; + if (!IsInvocationName(nameNode, invocationExpression)) + { + // Wasn't invoked. Change the name, but report a conflict. + var annotation = ConflictAnnotation.Create(FeaturesResources.Non_invoked_method_cannot_be_replaced_with_property); + editor.ReplaceNode(nameNode, newName.WithIdentifier(newName.Identifier.WithAdditionalAnnotations(annotation))); + return; + } - var invocationExpression = invocation?.Expression; - if (!IsInvocationName(nameNode, invocationExpression)) - { - // Wasn't invoked. Change the name, but report a conflict. - var annotation = ConflictAnnotation.Create(FeaturesResources.Non_invoked_method_cannot_be_replaced_with_property); - editor.ReplaceNode(nameNode, newName.WithIdentifier(newName.Identifier.WithAdditionalAnnotations(annotation))); - return; - } + // It was invoked. Remove the invocation, and also change the name if necessary. + replace(editor, invocation, nameNode, newName); + } - // It was invoked. Remove the invocation, and also change the name if necessary. - replace(editor, invocation, nameNode, newName); + private static bool IsInvocationName(IdentifierNameSyntax nameNode, ExpressionSyntax invocationExpression) + { + if (invocationExpression == nameNode) + { + return true; } - private static bool IsInvocationName(IdentifierNameSyntax nameNode, ExpressionSyntax invocationExpression) + if (nameNode.IsAnyMemberAccessExpressionName() && nameNode.Parent == invocationExpression) { - if (invocationExpression == nameNode) - { - return true; - } - - if (nameNode.IsAnyMemberAccessExpressionName() && nameNode.Parent == invocationExpression) - { - return true; - } - - return false; + return true; } + + return false; } } diff --git a/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.ConvertValueToParamRewriter.cs b/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.ConvertValueToParamRewriter.cs index fe100b7018b33..e74db0438dfc1 100644 --- a/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.ConvertValueToParamRewriter.cs +++ b/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.ConvertValueToParamRewriter.cs @@ -6,34 +6,33 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.ReplacePropertyWithMethods +namespace Microsoft.CodeAnalysis.CSharp.ReplacePropertyWithMethods; + +internal partial class CSharpReplacePropertyWithMethodsService { - internal partial class CSharpReplacePropertyWithMethodsService + private class ConvertValueToParamRewriter : CSharpSyntaxRewriter { - private class ConvertValueToParamRewriter : CSharpSyntaxRewriter - { - public static readonly CSharpSyntaxRewriter Instance = new ConvertValueToParamRewriter(); + public static readonly CSharpSyntaxRewriter Instance = new ConvertValueToParamRewriter(); - private ConvertValueToParamRewriter() - { - } - - private static XmlNameSyntax ConvertToParam(XmlNameSyntax name) - => name.ReplaceToken(name.LocalName, SyntaxFactory.Identifier("param")); + private ConvertValueToParamRewriter() + { + } - public override SyntaxNode VisitXmlElementStartTag(XmlElementStartTagSyntax node) - { - if (!IsValueName(node.Name)) - return base.VisitXmlElementStartTag(node); + private static XmlNameSyntax ConvertToParam(XmlNameSyntax name) + => name.ReplaceToken(name.LocalName, SyntaxFactory.Identifier("param")); - return node.ReplaceNode(node.Name, ConvertToParam(node.Name)) - .AddAttributes(SyntaxFactory.XmlNameAttribute("value")); - } + public override SyntaxNode VisitXmlElementStartTag(XmlElementStartTagSyntax node) + { + if (!IsValueName(node.Name)) + return base.VisitXmlElementStartTag(node); - public override SyntaxNode VisitXmlElementEndTag(XmlElementEndTagSyntax node) - => IsValueName(node.Name) - ? node.ReplaceNode(node.Name, ConvertToParam(node.Name)) - : base.VisitXmlElementEndTag(node); + return node.ReplaceNode(node.Name, ConvertToParam(node.Name)) + .AddAttributes(SyntaxFactory.XmlNameAttribute("value")); } + + public override SyntaxNode VisitXmlElementEndTag(XmlElementEndTagSyntax node) + => IsValueName(node.Name) + ? node.ReplaceNode(node.Name, ConvertToParam(node.Name)) + : base.VisitXmlElementEndTag(node); } } diff --git a/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.ConvertValueToReturnsRewriter.cs b/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.ConvertValueToReturnsRewriter.cs index 69c7715c52046..250be35ffc3b5 100644 --- a/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.ConvertValueToReturnsRewriter.cs +++ b/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.ConvertValueToReturnsRewriter.cs @@ -6,30 +6,29 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.CodeAnalysis.CSharp.ReplacePropertyWithMethods +namespace Microsoft.CodeAnalysis.CSharp.ReplacePropertyWithMethods; + +internal partial class CSharpReplacePropertyWithMethodsService { - internal partial class CSharpReplacePropertyWithMethodsService + private class ConvertValueToReturnsRewriter : CSharpSyntaxRewriter { - private class ConvertValueToReturnsRewriter : CSharpSyntaxRewriter - { - public static readonly CSharpSyntaxRewriter Instance = new ConvertValueToReturnsRewriter(); + public static readonly CSharpSyntaxRewriter Instance = new ConvertValueToReturnsRewriter(); - private ConvertValueToReturnsRewriter() - { - } + private ConvertValueToReturnsRewriter() + { + } - private static XmlNameSyntax ConvertToReturns(XmlNameSyntax name) - => name.ReplaceToken(name.LocalName, SyntaxFactory.Identifier("returns")); + private static XmlNameSyntax ConvertToReturns(XmlNameSyntax name) + => name.ReplaceToken(name.LocalName, SyntaxFactory.Identifier("returns")); - public override SyntaxNode VisitXmlElementStartTag(XmlElementStartTagSyntax node) - => IsValueName(node.Name) - ? node.ReplaceNode(node.Name, ConvertToReturns(node.Name)) - : base.VisitXmlElementStartTag(node); + public override SyntaxNode VisitXmlElementStartTag(XmlElementStartTagSyntax node) + => IsValueName(node.Name) + ? node.ReplaceNode(node.Name, ConvertToReturns(node.Name)) + : base.VisitXmlElementStartTag(node); - public override SyntaxNode VisitXmlElementEndTag(XmlElementEndTagSyntax node) - => IsValueName(node.Name) - ? node.ReplaceNode(node.Name, ConvertToReturns(node.Name)) - : base.VisitXmlElementEndTag(node); - } + public override SyntaxNode VisitXmlElementEndTag(XmlElementEndTagSyntax node) + => IsValueName(node.Name) + ? node.ReplaceNode(node.Name, ConvertToReturns(node.Name)) + : base.VisitXmlElementEndTag(node); } } diff --git a/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.cs b/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.cs index cb941a3774b21..1316a5b5038c2 100644 --- a/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.cs +++ b/src/Features/CSharp/Portable/ReplacePropertyWithMethods/CSharpReplacePropertyWithMethodsService.cs @@ -21,331 +21,330 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ReplacePropertyWithMethods +namespace Microsoft.CodeAnalysis.CSharp.ReplacePropertyWithMethods; + +[ExportLanguageService(typeof(IReplacePropertyWithMethodsService), LanguageNames.CSharp), Shared] +internal partial class CSharpReplacePropertyWithMethodsService : + AbstractReplacePropertyWithMethodsService { - [ExportLanguageService(typeof(IReplacePropertyWithMethodsService), LanguageNames.CSharp), Shared] - internal partial class CSharpReplacePropertyWithMethodsService : - AbstractReplacePropertyWithMethodsService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpReplacePropertyWithMethodsService() + { + } + + public override async Task> GetReplacementMembersAsync( + Document document, + IPropertySymbol property, + SyntaxNode propertyDeclarationNode, + IFieldSymbol propertyBackingField, + string desiredGetMethodName, + string desiredSetMethodName, + CodeGenerationOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + if (propertyDeclarationNode is not PropertyDeclarationSyntax propertyDeclaration) + return []; + + var options = (CSharpCodeGenerationOptions)await document.GetCodeGenerationOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var languageVersion = syntaxTree.Options.LanguageVersion(); + + return ConvertPropertyToMembers( + languageVersion, + SyntaxGenerator.GetGenerator(document), property, + propertyDeclaration, propertyBackingField, + options.PreferExpressionBodiedMethods.Value, desiredGetMethodName, desiredSetMethodName, + cancellationToken); + } + + private static ImmutableArray ConvertPropertyToMembers( + LanguageVersion languageVersion, + SyntaxGenerator generator, + IPropertySymbol property, + PropertyDeclarationSyntax propertyDeclaration, + IFieldSymbol? propertyBackingField, + ExpressionBodyPreference expressionBodyPreference, + string desiredGetMethodName, + string desiredSetMethodName, + CancellationToken cancellationToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpReplacePropertyWithMethodsService() + using var _ = ArrayBuilder.GetInstance(out var result); + + if (propertyBackingField != null) { + var initializer = propertyDeclaration.Initializer?.Value; + result.Add(generator.FieldDeclaration(propertyBackingField, initializer)); } - public override async Task> GetReplacementMembersAsync( - Document document, - IPropertySymbol property, - SyntaxNode propertyDeclarationNode, - IFieldSymbol propertyBackingField, - string desiredGetMethodName, - string desiredSetMethodName, - CodeGenerationOptionsProvider fallbackOptions, - CancellationToken cancellationToken) + var getMethod = property.GetMethod; + if (getMethod != null) { - if (propertyDeclarationNode is not PropertyDeclarationSyntax propertyDeclaration) - return []; - - var options = (CSharpCodeGenerationOptions)await document.GetCodeGenerationOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var languageVersion = syntaxTree.Options.LanguageVersion(); + result.Add(GetGetMethod( + languageVersion, + generator, propertyDeclaration, propertyBackingField, + getMethod, desiredGetMethodName, expressionBodyPreference, + cancellationToken)); + } - return ConvertPropertyToMembers( + var setMethod = property.SetMethod; + if (setMethod != null) + { + result.Add(GetSetMethod( languageVersion, - SyntaxGenerator.GetGenerator(document), property, - propertyDeclaration, propertyBackingField, - options.PreferExpressionBodiedMethods.Value, desiredGetMethodName, desiredSetMethodName, - cancellationToken); + generator, propertyDeclaration, propertyBackingField, + setMethod, desiredSetMethodName, expressionBodyPreference, + cancellationToken)); } - private static ImmutableArray ConvertPropertyToMembers( - LanguageVersion languageVersion, - SyntaxGenerator generator, - IPropertySymbol property, - PropertyDeclarationSyntax propertyDeclaration, - IFieldSymbol? propertyBackingField, - ExpressionBodyPreference expressionBodyPreference, - string desiredGetMethodName, - string desiredSetMethodName, - CancellationToken cancellationToken) + return result.ToImmutable(); + } + + private static SyntaxNode GetSetMethod( + LanguageVersion languageVersion, + SyntaxGenerator generator, + PropertyDeclarationSyntax propertyDeclaration, + IFieldSymbol? propertyBackingField, + IMethodSymbol setMethod, + string desiredSetMethodName, + ExpressionBodyPreference expressionBodyPreference, + CancellationToken cancellationToken) + { + var methodDeclaration = GetSetMethodWorker(); + + // The analyzer doesn't report diagnostics when the trivia contains preprocessor directives, so it's safe + // to copy the complete leading trivia to both generated methods. + methodDeclaration = CopyLeadingTrivia(propertyDeclaration, methodDeclaration, ConvertValueToParamRewriter.Instance); + + return UseExpressionOrBlockBodyIfDesired( + languageVersion, methodDeclaration, expressionBodyPreference, + createReturnStatementForExpression: false, cancellationToken); + + MethodDeclarationSyntax GetSetMethodWorker() { - using var _ = ArrayBuilder.GetInstance(out var result); + var setAccessorDeclaration = (AccessorDeclarationSyntax)setMethod.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); + var methodDeclaration = (MethodDeclarationSyntax)generator.MethodDeclaration(setMethod, desiredSetMethodName); - if (propertyBackingField != null) + // property has unsafe, but generator didn't add it to the method, so we have to add it here + if (propertyDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword) + && !methodDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword)) { - var initializer = propertyDeclaration.Initializer?.Value; - result.Add(generator.FieldDeclaration(propertyBackingField, initializer)); + methodDeclaration = methodDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.UnsafeKeyword)); } - var getMethod = property.GetMethod; - if (getMethod != null) + methodDeclaration = methodDeclaration.WithAttributeLists(setAccessorDeclaration.AttributeLists); + + if (setAccessorDeclaration.Body != null) { - result.Add(GetGetMethod( - languageVersion, - generator, propertyDeclaration, propertyBackingField, - getMethod, desiredGetMethodName, expressionBodyPreference, - cancellationToken)); + return methodDeclaration.WithBody(setAccessorDeclaration.Body) + .WithAdditionalAnnotations(Formatter.Annotation); } - - var setMethod = property.SetMethod; - if (setMethod != null) + else if (setAccessorDeclaration.ExpressionBody != null) { - result.Add(GetSetMethod( - languageVersion, - generator, propertyDeclaration, propertyBackingField, - setMethod, desiredSetMethodName, expressionBodyPreference, - cancellationToken)); + return methodDeclaration.WithBody(null) + .WithExpressionBody(setAccessorDeclaration.ExpressionBody) + .WithSemicolonToken(setAccessorDeclaration.SemicolonToken); + } + else if (propertyBackingField != null) + { + return methodDeclaration.WithBody(SyntaxFactory.Block( + (StatementSyntax)generator.ExpressionStatement( + generator.AssignmentStatement( + GetFieldReference(generator, propertyBackingField), + generator.IdentifierName("value"))))); } - return result.ToImmutable(); + return methodDeclaration; } + } - private static SyntaxNode GetSetMethod( - LanguageVersion languageVersion, - SyntaxGenerator generator, - PropertyDeclarationSyntax propertyDeclaration, - IFieldSymbol? propertyBackingField, - IMethodSymbol setMethod, - string desiredSetMethodName, - ExpressionBodyPreference expressionBodyPreference, - CancellationToken cancellationToken) - { - var methodDeclaration = GetSetMethodWorker(); + private static SyntaxNode GetGetMethod( + LanguageVersion languageVersion, + SyntaxGenerator generator, + PropertyDeclarationSyntax propertyDeclaration, + IFieldSymbol? propertyBackingField, + IMethodSymbol getMethod, + string desiredGetMethodName, + ExpressionBodyPreference expressionBodyPreference, + CancellationToken cancellationToken) + { + var methodDeclaration = GetGetMethodWorker(); - // The analyzer doesn't report diagnostics when the trivia contains preprocessor directives, so it's safe - // to copy the complete leading trivia to both generated methods. - methodDeclaration = CopyLeadingTrivia(propertyDeclaration, methodDeclaration, ConvertValueToParamRewriter.Instance); + methodDeclaration = CopyLeadingTrivia(propertyDeclaration, methodDeclaration, ConvertValueToReturnsRewriter.Instance); - return UseExpressionOrBlockBodyIfDesired( - languageVersion, methodDeclaration, expressionBodyPreference, - createReturnStatementForExpression: false, cancellationToken); + return UseExpressionOrBlockBodyIfDesired( + languageVersion, methodDeclaration, expressionBodyPreference, + createReturnStatementForExpression: true, cancellationToken); - MethodDeclarationSyntax GetSetMethodWorker() + MethodDeclarationSyntax GetGetMethodWorker() + { + var methodDeclaration = (MethodDeclarationSyntax)generator.MethodDeclaration(getMethod, desiredGetMethodName); + + // property has unsafe, but generator didn't add it to the method, so we have to add it here + if (propertyDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword) + && !methodDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword)) { - var setAccessorDeclaration = (AccessorDeclarationSyntax)setMethod.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); - var methodDeclaration = (MethodDeclarationSyntax)generator.MethodDeclaration(setMethod, desiredSetMethodName); + methodDeclaration = methodDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.UnsafeKeyword)); + } - // property has unsafe, but generator didn't add it to the method, so we have to add it here - if (propertyDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword) - && !methodDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword)) - { - methodDeclaration = methodDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.UnsafeKeyword)); - } + if (propertyDeclaration.ExpressionBody != null) + { + return methodDeclaration.WithBody(null) + .WithExpressionBody(propertyDeclaration.ExpressionBody) + .WithSemicolonToken(propertyDeclaration.SemicolonToken); + } + else + { + var getAccessorDeclaration = (AccessorDeclarationSyntax)getMethod.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); - methodDeclaration = methodDeclaration.WithAttributeLists(setAccessorDeclaration.AttributeLists); + methodDeclaration = methodDeclaration.WithAttributeLists(getAccessorDeclaration.AttributeLists); - if (setAccessorDeclaration.Body != null) + if (getAccessorDeclaration?.ExpressionBody != null) { - return methodDeclaration.WithBody(setAccessorDeclaration.Body) - .WithAdditionalAnnotations(Formatter.Annotation); + return methodDeclaration.WithBody(null) + .WithExpressionBody(getAccessorDeclaration.ExpressionBody) + .WithSemicolonToken(getAccessorDeclaration.SemicolonToken); } - else if (setAccessorDeclaration.ExpressionBody != null) + else if (getAccessorDeclaration?.Body != null) { - return methodDeclaration.WithBody(null) - .WithExpressionBody(setAccessorDeclaration.ExpressionBody) - .WithSemicolonToken(setAccessorDeclaration.SemicolonToken); + return methodDeclaration.WithBody(getAccessorDeclaration.Body) + .WithAdditionalAnnotations(Formatter.Annotation); } else if (propertyBackingField != null) { - return methodDeclaration.WithBody(SyntaxFactory.Block( - (StatementSyntax)generator.ExpressionStatement( - generator.AssignmentStatement( - GetFieldReference(generator, propertyBackingField), - generator.IdentifierName("value"))))); + var fieldReference = GetFieldReference(generator, propertyBackingField); + return methodDeclaration.WithBody( + SyntaxFactory.Block( + (StatementSyntax)generator.ReturnStatement(fieldReference))); } - - return methodDeclaration; } + + return methodDeclaration; } + } + + private static MethodDeclarationSyntax CopyLeadingTrivia( + PropertyDeclarationSyntax propertyDeclaration, + MethodDeclarationSyntax methodDeclaration, + CSharpSyntaxRewriter documentationCommentRewriter) + { + var leadingTrivia = propertyDeclaration.GetLeadingTrivia(); + return methodDeclaration.WithLeadingTrivia(leadingTrivia.Select(trivia => ConvertTrivia(trivia, documentationCommentRewriter))); + } - private static SyntaxNode GetGetMethod( - LanguageVersion languageVersion, - SyntaxGenerator generator, - PropertyDeclarationSyntax propertyDeclaration, - IFieldSymbol? propertyBackingField, - IMethodSymbol getMethod, - string desiredGetMethodName, - ExpressionBodyPreference expressionBodyPreference, - CancellationToken cancellationToken) + private static SyntaxTrivia ConvertTrivia(SyntaxTrivia trivia, CSharpSyntaxRewriter rewriter) + { + if (trivia.Kind() is SyntaxKind.MultiLineDocumentationCommentTrivia or + SyntaxKind.SingleLineDocumentationCommentTrivia) { - var methodDeclaration = GetGetMethodWorker(); + return ConvertDocumentationComment(trivia, rewriter); + } - methodDeclaration = CopyLeadingTrivia(propertyDeclaration, methodDeclaration, ConvertValueToReturnsRewriter.Instance); + return trivia; + } - return UseExpressionOrBlockBodyIfDesired( - languageVersion, methodDeclaration, expressionBodyPreference, - createReturnStatementForExpression: true, cancellationToken); + private static SyntaxTrivia ConvertDocumentationComment(SyntaxTrivia trivia, CSharpSyntaxRewriter rewriter) + { + var structure = trivia.GetStructure(); + var rewritten = rewriter.Visit(structure); + Contract.ThrowIfNull(rewritten); + return SyntaxFactory.Trivia((StructuredTriviaSyntax)rewritten); + } - MethodDeclarationSyntax GetGetMethodWorker() + private static SyntaxNode UseExpressionOrBlockBodyIfDesired( + LanguageVersion languageVersion, + MethodDeclarationSyntax methodDeclaration, + ExpressionBodyPreference expressionBodyPreference, + bool createReturnStatementForExpression, + CancellationToken cancellationToken) + { + if (methodDeclaration.Body != null && expressionBodyPreference != ExpressionBodyPreference.Never) + { + if (methodDeclaration.Body.TryConvertToArrowExpressionBody( + methodDeclaration.Kind(), languageVersion, expressionBodyPreference, cancellationToken, + out var arrowExpression, out var semicolonToken)) { - var methodDeclaration = (MethodDeclarationSyntax)generator.MethodDeclaration(getMethod, desiredGetMethodName); - - // property has unsafe, but generator didn't add it to the method, so we have to add it here - if (propertyDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword) - && !methodDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword)) - { - methodDeclaration = methodDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.UnsafeKeyword)); - } - - if (propertyDeclaration.ExpressionBody != null) - { - return methodDeclaration.WithBody(null) - .WithExpressionBody(propertyDeclaration.ExpressionBody) - .WithSemicolonToken(propertyDeclaration.SemicolonToken); - } - else - { - var getAccessorDeclaration = (AccessorDeclarationSyntax)getMethod.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); - - methodDeclaration = methodDeclaration.WithAttributeLists(getAccessorDeclaration.AttributeLists); - - if (getAccessorDeclaration?.ExpressionBody != null) - { - return methodDeclaration.WithBody(null) - .WithExpressionBody(getAccessorDeclaration.ExpressionBody) - .WithSemicolonToken(getAccessorDeclaration.SemicolonToken); - } - else if (getAccessorDeclaration?.Body != null) - { - return methodDeclaration.WithBody(getAccessorDeclaration.Body) - .WithAdditionalAnnotations(Formatter.Annotation); - } - else if (propertyBackingField != null) - { - var fieldReference = GetFieldReference(generator, propertyBackingField); - return methodDeclaration.WithBody( - SyntaxFactory.Block( - (StatementSyntax)generator.ReturnStatement(fieldReference))); - } - } - - return methodDeclaration; + return methodDeclaration.WithBody(null) + .WithExpressionBody(arrowExpression) + .WithSemicolonToken(semicolonToken) + .WithAdditionalAnnotations(Formatter.Annotation); } } - - private static MethodDeclarationSyntax CopyLeadingTrivia( - PropertyDeclarationSyntax propertyDeclaration, - MethodDeclarationSyntax methodDeclaration, - CSharpSyntaxRewriter documentationCommentRewriter) - { - var leadingTrivia = propertyDeclaration.GetLeadingTrivia(); - return methodDeclaration.WithLeadingTrivia(leadingTrivia.Select(trivia => ConvertTrivia(trivia, documentationCommentRewriter))); - } - - private static SyntaxTrivia ConvertTrivia(SyntaxTrivia trivia, CSharpSyntaxRewriter rewriter) + else if (methodDeclaration.ExpressionBody != null && expressionBodyPreference == ExpressionBodyPreference.Never) { - if (trivia.Kind() is SyntaxKind.MultiLineDocumentationCommentTrivia or - SyntaxKind.SingleLineDocumentationCommentTrivia) + if (methodDeclaration.ExpressionBody.TryConvertToBlock( + methodDeclaration.SemicolonToken, createReturnStatementForExpression, out var block)) { - return ConvertDocumentationComment(trivia, rewriter); + return methodDeclaration.WithExpressionBody(null) + .WithSemicolonToken(default) + .WithBody(block) + .WithAdditionalAnnotations(Formatter.Annotation); } - - return trivia; } - private static SyntaxTrivia ConvertDocumentationComment(SyntaxTrivia trivia, CSharpSyntaxRewriter rewriter) - { - var structure = trivia.GetStructure(); - var rewritten = rewriter.Visit(structure); - Contract.ThrowIfNull(rewritten); - return SyntaxFactory.Trivia((StructuredTriviaSyntax)rewritten); - } + return methodDeclaration; + } - private static SyntaxNode UseExpressionOrBlockBodyIfDesired( - LanguageVersion languageVersion, - MethodDeclarationSyntax methodDeclaration, - ExpressionBodyPreference expressionBodyPreference, - bool createReturnStatementForExpression, - CancellationToken cancellationToken) - { - if (methodDeclaration.Body != null && expressionBodyPreference != ExpressionBodyPreference.Never) - { - if (methodDeclaration.Body.TryConvertToArrowExpressionBody( - methodDeclaration.Kind(), languageVersion, expressionBodyPreference, cancellationToken, - out var arrowExpression, out var semicolonToken)) - { - return methodDeclaration.WithBody(null) - .WithExpressionBody(arrowExpression) - .WithSemicolonToken(semicolonToken) - .WithAdditionalAnnotations(Formatter.Annotation); - } - } - else if (methodDeclaration.ExpressionBody != null && expressionBodyPreference == ExpressionBodyPreference.Never) - { - if (methodDeclaration.ExpressionBody.TryConvertToBlock( - methodDeclaration.SemicolonToken, createReturnStatementForExpression, out var block)) - { - return methodDeclaration.WithExpressionBody(null) - .WithSemicolonToken(default) - .WithBody(block) - .WithAdditionalAnnotations(Formatter.Annotation); - } - } + /// + /// Used by the documentation comment rewriters to identify top-level <value> nodes. + /// + private static bool IsValueName(XmlNameSyntax name) + => name.Prefix == null && + name.LocalName.ValueText == "value"; - return methodDeclaration; - } + public override SyntaxNode GetPropertyNodeToReplace(SyntaxNode propertyDeclaration) + { + // For C# we'll have the property declaration that we want to replace. + return propertyDeclaration; + } - /// - /// Used by the documentation comment rewriters to identify top-level <value> nodes. - /// - private static bool IsValueName(XmlNameSyntax name) - => name.Prefix == null && - name.LocalName.ValueText == "value"; + protected override NameMemberCrefSyntax? TryGetCrefSyntax(IdentifierNameSyntax identifierName) + => identifierName.Parent as NameMemberCrefSyntax; - public override SyntaxNode GetPropertyNodeToReplace(SyntaxNode propertyDeclaration) + protected override NameMemberCrefSyntax CreateCrefSyntax(NameMemberCrefSyntax originalCref, SyntaxToken identifierToken, SyntaxNode? parameterType) + { + CrefParameterListSyntax parameterList; + if (parameterType is TypeSyntax typeSyntax) { - // For C# we'll have the property declaration that we want to replace. - return propertyDeclaration; + var parameter = SyntaxFactory.CrefParameter(typeSyntax); + parameterList = SyntaxFactory.CrefParameterList([parameter]); } - - protected override NameMemberCrefSyntax? TryGetCrefSyntax(IdentifierNameSyntax identifierName) - => identifierName.Parent as NameMemberCrefSyntax; - - protected override NameMemberCrefSyntax CreateCrefSyntax(NameMemberCrefSyntax originalCref, SyntaxToken identifierToken, SyntaxNode? parameterType) + else { - CrefParameterListSyntax parameterList; - if (parameterType is TypeSyntax typeSyntax) - { - var parameter = SyntaxFactory.CrefParameter(typeSyntax); - parameterList = SyntaxFactory.CrefParameterList([parameter]); - } - else - { - parameterList = SyntaxFactory.CrefParameterList(); - } - - // XmlCrefAttribute replaces with {T}, which is required for C# documentation comments - var crefAttribute = SyntaxFactory.XmlCrefAttribute( - SyntaxFactory.NameMemberCref(SyntaxFactory.IdentifierName(identifierToken), parameterList)); - return (NameMemberCrefSyntax)crefAttribute.Cref; + parameterList = SyntaxFactory.CrefParameterList(); } - protected override ExpressionSyntax UnwrapCompoundAssignment( - SyntaxNode compoundAssignment, ExpressionSyntax readExpression) - { - var parent = (AssignmentExpressionSyntax)compoundAssignment; + // XmlCrefAttribute replaces with {T}, which is required for C# documentation comments + var crefAttribute = SyntaxFactory.XmlCrefAttribute( + SyntaxFactory.NameMemberCref(SyntaxFactory.IdentifierName(identifierToken), parameterList)); + return (NameMemberCrefSyntax)crefAttribute.Cref; + } - var operatorKind = parent.Kind() switch - { - SyntaxKind.AddAssignmentExpression => SyntaxKind.AddExpression, - SyntaxKind.AndAssignmentExpression => SyntaxKind.BitwiseAndExpression, - SyntaxKind.CoalesceAssignmentExpression => SyntaxKind.CoalesceExpression, - SyntaxKind.DivideAssignmentExpression => SyntaxKind.DivideExpression, - SyntaxKind.ExclusiveOrAssignmentExpression => SyntaxKind.ExclusiveOrExpression, - SyntaxKind.LeftShiftAssignmentExpression => SyntaxKind.LeftShiftExpression, - SyntaxKind.ModuloAssignmentExpression => SyntaxKind.ModuloExpression, - SyntaxKind.MultiplyAssignmentExpression => SyntaxKind.MultiplyExpression, - SyntaxKind.OrAssignmentExpression => SyntaxKind.BitwiseOrExpression, - SyntaxKind.RightShiftAssignmentExpression => SyntaxKind.RightShiftExpression, - SyntaxKind.SubtractAssignmentExpression => SyntaxKind.SubtractExpression, - SyntaxKind.UnsignedRightShiftAssignmentExpression => SyntaxKind.UnsignedRightShiftExpression, - _ => SyntaxKind.None, - }; - - if (operatorKind is SyntaxKind.None) - return parent; - - return SyntaxFactory.BinaryExpression(operatorKind, readExpression, parent.Right.Parenthesize()); - } + protected override ExpressionSyntax UnwrapCompoundAssignment( + SyntaxNode compoundAssignment, ExpressionSyntax readExpression) + { + var parent = (AssignmentExpressionSyntax)compoundAssignment; + + var operatorKind = parent.Kind() switch + { + SyntaxKind.AddAssignmentExpression => SyntaxKind.AddExpression, + SyntaxKind.AndAssignmentExpression => SyntaxKind.BitwiseAndExpression, + SyntaxKind.CoalesceAssignmentExpression => SyntaxKind.CoalesceExpression, + SyntaxKind.DivideAssignmentExpression => SyntaxKind.DivideExpression, + SyntaxKind.ExclusiveOrAssignmentExpression => SyntaxKind.ExclusiveOrExpression, + SyntaxKind.LeftShiftAssignmentExpression => SyntaxKind.LeftShiftExpression, + SyntaxKind.ModuloAssignmentExpression => SyntaxKind.ModuloExpression, + SyntaxKind.MultiplyAssignmentExpression => SyntaxKind.MultiplyExpression, + SyntaxKind.OrAssignmentExpression => SyntaxKind.BitwiseOrExpression, + SyntaxKind.RightShiftAssignmentExpression => SyntaxKind.RightShiftExpression, + SyntaxKind.SubtractAssignmentExpression => SyntaxKind.SubtractExpression, + SyntaxKind.UnsignedRightShiftAssignmentExpression => SyntaxKind.UnsignedRightShiftExpression, + _ => SyntaxKind.None, + }; + + if (operatorKind is SyntaxKind.None) + return parent; + + return SyntaxFactory.BinaryExpression(operatorKind, readExpression, parent.Right.Parenthesize()); } } diff --git a/src/Features/CSharp/Portable/ReverseForStatement/CSharpReverseForStatementCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ReverseForStatement/CSharpReverseForStatementCodeRefactoringProvider.cs index a1b78c1b98a4a..2da8982c62456 100644 --- a/src/Features/CSharp/Portable/ReverseForStatement/CSharpReverseForStatementCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ReverseForStatement/CSharpReverseForStatementCodeRefactoringProvider.cs @@ -17,383 +17,382 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.ReverseForStatement +namespace Microsoft.CodeAnalysis.CSharp.ReverseForStatement; + +using static IntegerUtilities; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ReverseForStatement), Shared] +internal class CSharpReverseForStatementCodeRefactoringProvider : CodeRefactoringProvider { - using static IntegerUtilities; + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpReverseForStatementCodeRefactoringProvider() + { + } - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ReverseForStatement), Shared] - internal class CSharpReverseForStatementCodeRefactoringProvider : CodeRefactoringProvider + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpReverseForStatementCodeRefactoringProvider() + var forStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (forStatement == null) + return; + + // We support the following cases + // + // for (var x = start; x < end ; x++) + // for (... ; ... ; ++x) + // for (... ; x <= end; ...) + // for (... ; ... ; x += 1) + // + // for (var x = end ; x >= start; x--) + // for (... ; ... ; --x) + // for (... ; ... ; x -= 1) + + var declaration = forStatement.Declaration; + if (declaration == null || + declaration.Variables.Count != 1 || + forStatement.Incrementors.Count != 1) { + return; } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var forStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (forStatement == null) - return; + var variable = declaration.Variables[0]; + var after = forStatement.Incrementors[0]; - // We support the following cases - // - // for (var x = start; x < end ; x++) - // for (... ; ... ; ++x) - // for (... ; x <= end; ...) - // for (... ; ... ; x += 1) - // - // for (var x = end ; x >= start; x--) - // for (... ; ... ; --x) - // for (... ; ... ; x -= 1) - - var declaration = forStatement.Declaration; - if (declaration == null || - declaration.Variables.Count != 1 || - forStatement.Incrementors.Count != 1) + if (forStatement.Condition is not BinaryExpressionSyntax condition) + return; + + var (document, _, cancellationToken) = context; + if (MatchesIncrementPattern(variable, condition, after, out var start, out var equals, out var end) || + MatchesDecrementPattern(variable, condition, after, out end, out start)) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (IsUnsignedBoundary(semanticModel, variable, start, end, cancellationToken)) { + // Don't allow reversing when you have unsigned types and are on the start/end + // of the legal values for that type. i.e. `for (byte i = 0; i < 10; i++)` it's + // not trivial to reverse this. return; } - var variable = declaration.Variables[0]; - var after = forStatement.Incrementors[0]; + context.RegisterRefactoring( + CodeAction.Create( + CSharpFeaturesResources.Reverse_for_statement, + c => ReverseForStatementAsync(document, forStatement, c), + nameof(CSharpFeaturesResources.Reverse_for_statement))); + } + } - if (forStatement.Condition is not BinaryExpressionSyntax condition) - return; + private static bool IsUnsignedBoundary( + SemanticModel semanticModel, VariableDeclaratorSyntax variable, + ExpressionSyntax start, ExpressionSyntax end, CancellationToken cancellationToken) + { + var local = semanticModel.GetDeclaredSymbol(variable, cancellationToken) as ILocalSymbol; + var startValue = semanticModel.GetConstantValue(start, cancellationToken); + var endValue = semanticModel.GetConstantValue(end, cancellationToken); - var (document, _, cancellationToken) = context; - if (MatchesIncrementPattern(variable, condition, after, out var start, out var equals, out var end) || - MatchesDecrementPattern(variable, condition, after, out end, out start)) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (IsUnsignedBoundary(semanticModel, variable, start, end, cancellationToken)) - { - // Don't allow reversing when you have unsigned types and are on the start/end - // of the legal values for that type. i.e. `for (byte i = 0; i < 10; i++)` it's - // not trivial to reverse this. - return; - } + return local?.Type.SpecialType switch + { + SpecialType.System_Byte => IsUnsignedBoundary(startValue, endValue, byte.MaxValue), + SpecialType.System_UInt16 => IsUnsignedBoundary(startValue, endValue, ushort.MaxValue), + SpecialType.System_UInt32 => IsUnsignedBoundary(startValue, endValue, uint.MaxValue), + SpecialType.System_UInt64 => IsUnsignedBoundary(startValue, endValue, ulong.MaxValue), + _ => false, + }; + } - context.RegisterRefactoring( - CodeAction.Create( - CSharpFeaturesResources.Reverse_for_statement, - c => ReverseForStatementAsync(document, forStatement, c), - nameof(CSharpFeaturesResources.Reverse_for_statement))); - } - } + private static bool IsUnsignedBoundary(Optional startValue, Optional endValue, ulong maxValue) + => ValueEquals(startValue, 0) || ValueEquals(endValue, maxValue); - private static bool IsUnsignedBoundary( - SemanticModel semanticModel, VariableDeclaratorSyntax variable, - ExpressionSyntax start, ExpressionSyntax end, CancellationToken cancellationToken) - { - var local = semanticModel.GetDeclaredSymbol(variable, cancellationToken) as ILocalSymbol; - var startValue = semanticModel.GetConstantValue(start, cancellationToken); - var endValue = semanticModel.GetConstantValue(end, cancellationToken); + private static bool ValueEquals(Optional valueOpt, ulong value) + => valueOpt.HasValue && IsIntegral(valueOpt.Value) && ToUInt64(valueOpt.Value) == value; - return local?.Type.SpecialType switch - { - SpecialType.System_Byte => IsUnsignedBoundary(startValue, endValue, byte.MaxValue), - SpecialType.System_UInt16 => IsUnsignedBoundary(startValue, endValue, ushort.MaxValue), - SpecialType.System_UInt32 => IsUnsignedBoundary(startValue, endValue, uint.MaxValue), - SpecialType.System_UInt64 => IsUnsignedBoundary(startValue, endValue, ulong.MaxValue), - _ => false, - }; - } + private static bool MatchesIncrementPattern( + VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, ExpressionSyntax after, + [NotNullWhen(true)] out ExpressionSyntax? start, out bool equals, [NotNullWhen(true)] out ExpressionSyntax? end) + { + equals = default; + end = null; + return IsIncrementInitializer(variable, out start) && + IsIncrementCondition(variable, condition, out equals, out end) && + IsIncrementAfter(variable, after); + } - private static bool IsUnsignedBoundary(Optional startValue, Optional endValue, ulong maxValue) - => ValueEquals(startValue, 0) || ValueEquals(endValue, maxValue); + private static bool MatchesDecrementPattern( + VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, ExpressionSyntax after, + [NotNullWhen(true)] out ExpressionSyntax? end, [NotNullWhen(true)] out ExpressionSyntax? start) + { + start = null; + return IsDecrementInitializer(variable, out end) && + IsDecrementCondition(variable, condition, out start) && + IsDecrementAfter(variable, after); + } - private static bool ValueEquals(Optional valueOpt, ulong value) - => valueOpt.HasValue && IsIntegral(valueOpt.Value) && ToUInt64(valueOpt.Value) == value; + private static bool IsIncrementInitializer(VariableDeclaratorSyntax variable, [NotNullWhen(true)] out ExpressionSyntax? start) + { + start = variable.Initializer?.Value; + return start != null; + } - private static bool MatchesIncrementPattern( - VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, ExpressionSyntax after, - [NotNullWhen(true)] out ExpressionSyntax? start, out bool equals, [NotNullWhen(true)] out ExpressionSyntax? end) + private static bool IsIncrementCondition( + VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, + out bool equals, [NotNullWhen(true)] out ExpressionSyntax? end) + { + // i < ... i <= ... + if (condition.Kind() is SyntaxKind.LessThanExpression or + SyntaxKind.LessThanOrEqualExpression) { - equals = default; - end = null; - return IsIncrementInitializer(variable, out start) && - IsIncrementCondition(variable, condition, out equals, out end) && - IsIncrementAfter(variable, after); + end = condition.Right; + equals = condition.Kind() == SyntaxKind.LessThanOrEqualExpression; + return IsVariableReference(variable, condition.Left); } - private static bool MatchesDecrementPattern( - VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, ExpressionSyntax after, - [NotNullWhen(true)] out ExpressionSyntax? end, [NotNullWhen(true)] out ExpressionSyntax? start) + // ... > i ... >= i + if (condition.Kind() is SyntaxKind.GreaterThanExpression or + SyntaxKind.GreaterThanOrEqualExpression) { - start = null; - return IsDecrementInitializer(variable, out end) && - IsDecrementCondition(variable, condition, out start) && - IsDecrementAfter(variable, after); + end = condition.Left; + equals = condition.Kind() == SyntaxKind.GreaterThanOrEqualExpression; + return IsVariableReference(variable, condition.Right); } - private static bool IsIncrementInitializer(VariableDeclaratorSyntax variable, [NotNullWhen(true)] out ExpressionSyntax? start) + end = null; + equals = default; + return false; + } + + private static bool IsIncrementAfter( + VariableDeclaratorSyntax variable, ExpressionSyntax after) + { + // i++ + // ++i + // i += 1 + if (after is PostfixUnaryExpressionSyntax postfixUnary && + postfixUnary.Kind() == SyntaxKind.PostIncrementExpression && + IsVariableReference(variable, postfixUnary.Operand)) { - start = variable.Initializer?.Value; - return start != null; + return true; } - private static bool IsIncrementCondition( - VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, - out bool equals, [NotNullWhen(true)] out ExpressionSyntax? end) + if (after is PrefixUnaryExpressionSyntax prefixUnary && + prefixUnary.Kind() == SyntaxKind.PreIncrementExpression && + IsVariableReference(variable, prefixUnary.Operand)) { - // i < ... i <= ... - if (condition.Kind() is SyntaxKind.LessThanExpression or - SyntaxKind.LessThanOrEqualExpression) - { - end = condition.Right; - equals = condition.Kind() == SyntaxKind.LessThanOrEqualExpression; - return IsVariableReference(variable, condition.Left); - } - - // ... > i ... >= i - if (condition.Kind() is SyntaxKind.GreaterThanExpression or - SyntaxKind.GreaterThanOrEqualExpression) - { - end = condition.Left; - equals = condition.Kind() == SyntaxKind.GreaterThanOrEqualExpression; - return IsVariableReference(variable, condition.Right); - } - - end = null; - equals = default; - return false; + return true; } - private static bool IsIncrementAfter( - VariableDeclaratorSyntax variable, ExpressionSyntax after) + if (after is AssignmentExpressionSyntax assignment && + assignment.Kind() == SyntaxKind.AddAssignmentExpression && + IsVariableReference(variable, assignment.Left) && + IsLiteralOne(assignment.Right)) { - // i++ - // ++i - // i += 1 - if (after is PostfixUnaryExpressionSyntax postfixUnary && - postfixUnary.Kind() == SyntaxKind.PostIncrementExpression && - IsVariableReference(variable, postfixUnary.Operand)) - { - return true; - } - - if (after is PrefixUnaryExpressionSyntax prefixUnary && - prefixUnary.Kind() == SyntaxKind.PreIncrementExpression && - IsVariableReference(variable, prefixUnary.Operand)) - { - return true; - } + return true; + } - if (after is AssignmentExpressionSyntax assignment && - assignment.Kind() == SyntaxKind.AddAssignmentExpression && - IsVariableReference(variable, assignment.Left) && - IsLiteralOne(assignment.Right)) - { - return true; - } + return false; + } - return false; - } + private static bool IsLiteralOne(ExpressionSyntax expression) + => expression.WalkDownParentheses() is LiteralExpressionSyntax literal && literal.Token.Value is 1; - private static bool IsLiteralOne(ExpressionSyntax expression) - => expression.WalkDownParentheses() is LiteralExpressionSyntax literal && literal.Token.Value is 1; + private static bool IsDecrementInitializer( + VariableDeclaratorSyntax variable, [NotNullWhen(true)] out ExpressionSyntax? end) + { + end = variable.Initializer?.Value; + return end != null; + } - private static bool IsDecrementInitializer( - VariableDeclaratorSyntax variable, [NotNullWhen(true)] out ExpressionSyntax? end) + private static bool IsDecrementCondition( + VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, + [NotNullWhen(true)] out ExpressionSyntax? start) + { + // i >= ... + if (condition.Kind() == SyntaxKind.GreaterThanOrEqualExpression) { - end = variable.Initializer?.Value; - return end != null; + start = condition.Right; + return IsVariableReference(variable, condition.Left); } - private static bool IsDecrementCondition( - VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, - [NotNullWhen(true)] out ExpressionSyntax? start) + // ... <= i + if (condition.Kind() == SyntaxKind.LessThanOrEqualExpression) { - // i >= ... - if (condition.Kind() == SyntaxKind.GreaterThanOrEqualExpression) - { - start = condition.Right; - return IsVariableReference(variable, condition.Left); - } + start = condition.Left; + return IsVariableReference(variable, condition.Right); + } - // ... <= i - if (condition.Kind() == SyntaxKind.LessThanOrEqualExpression) - { - start = condition.Left; - return IsVariableReference(variable, condition.Right); - } + start = null; + return false; + } - start = null; - return false; + private static bool IsDecrementAfter( + VariableDeclaratorSyntax variable, ExpressionSyntax after) + { + // i-- + // --i + // i -= 1 + if (after is PostfixUnaryExpressionSyntax postfixUnary && + postfixUnary.Kind() == SyntaxKind.PostDecrementExpression && + IsVariableReference(variable, postfixUnary.Operand)) + { + return true; } - private static bool IsDecrementAfter( - VariableDeclaratorSyntax variable, ExpressionSyntax after) + if (after is PrefixUnaryExpressionSyntax prefixUnary && + prefixUnary.Kind() == SyntaxKind.PreDecrementExpression && + IsVariableReference(variable, prefixUnary.Operand)) { - // i-- - // --i - // i -= 1 - if (after is PostfixUnaryExpressionSyntax postfixUnary && - postfixUnary.Kind() == SyntaxKind.PostDecrementExpression && - IsVariableReference(variable, postfixUnary.Operand)) - { - return true; - } + return true; + } - if (after is PrefixUnaryExpressionSyntax prefixUnary && - prefixUnary.Kind() == SyntaxKind.PreDecrementExpression && - IsVariableReference(variable, prefixUnary.Operand)) - { - return true; - } + if (after is AssignmentExpressionSyntax assignment && + assignment.Kind() == SyntaxKind.SubtractAssignmentExpression && + IsVariableReference(variable, assignment.Left) && + IsLiteralOne(assignment.Right)) + { + return true; + } - if (after is AssignmentExpressionSyntax assignment && - assignment.Kind() == SyntaxKind.SubtractAssignmentExpression && - IsVariableReference(variable, assignment.Left) && - IsLiteralOne(assignment.Right)) - { - return true; - } + return false; + } - return false; - } + private static bool IsVariableReference(VariableDeclaratorSyntax variable, ExpressionSyntax expr) + => expr.WalkDownParentheses() is IdentifierNameSyntax identifier && + identifier.Identifier.ValueText == variable.Identifier.ValueText; - private static bool IsVariableReference(VariableDeclaratorSyntax variable, ExpressionSyntax expr) - => expr.WalkDownParentheses() is IdentifierNameSyntax identifier && - identifier.Identifier.ValueText == variable.Identifier.ValueText; + private static async Task ReverseForStatementAsync( + Document document, ForStatementSyntax forStatement, CancellationToken cancellationToken) + { + var variable = forStatement.Declaration!.Variables[0]; + var condition = (BinaryExpressionSyntax)forStatement.Condition!; + var after = forStatement.Incrementors[0]; - private static async Task ReverseForStatementAsync( - Document document, ForStatementSyntax forStatement, CancellationToken cancellationToken) - { - var variable = forStatement.Declaration!.Variables[0]; - var condition = (BinaryExpressionSyntax)forStatement.Condition!; - var after = forStatement.Incrementors[0]; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var editor = new SyntaxEditor(root, document.Project.Solution.Services); + var generator = editor.Generator; + if (MatchesIncrementPattern( + variable, condition, after, + out var start, out var equals, out var end)) + { + // for (var x = start ; x < end ; ...) => + // for (var x = end - 1; x >= start; ...) + // + // for (var x = start; x <= end ; ...) => + // for (var x = end ; x >= start; ...) => - var editor = new SyntaxEditor(root, document.Project.Solution.Services); - var generator = editor.Generator; - if (MatchesIncrementPattern( - variable, condition, after, - out var start, out var equals, out var end)) - { - // for (var x = start ; x < end ; ...) => - // for (var x = end - 1; x >= start; ...) - // - // for (var x = start; x <= end ; ...) => - // for (var x = end ; x >= start; ...) => - - var newStart = equals - ? end - : (ExpressionSyntax)generator.SubtractExpression(end, generator.LiteralExpression(1)); - - editor.ReplaceNode(variable.Initializer!.Value, Reduce(newStart)); - editor.ReplaceNode(condition, Reduce(Invert(variable, condition, start))); - } - else if (MatchesDecrementPattern(variable, condition, after, out end, out start)) - { - // for (var x = end; x >= start; x--) => - // for (var x = start; x <= end; x--) - editor.ReplaceNode(variable.Initializer!.Value, Reduce(start)); - editor.ReplaceNode(condition, Reduce(Invert(variable, condition, end))); - } - else - { - throw new InvalidOperationException(); - } + var newStart = equals + ? end + : (ExpressionSyntax)generator.SubtractExpression(end, generator.LiteralExpression(1)); - editor.ReplaceNode(after, InvertAfter(after)); - return document.WithSyntaxRoot(editor.GetChangedRoot()); + editor.ReplaceNode(variable.Initializer!.Value, Reduce(newStart)); + editor.ReplaceNode(condition, Reduce(Invert(variable, condition, start))); + } + else if (MatchesDecrementPattern(variable, condition, after, out end, out start)) + { + // for (var x = end; x >= start; x--) => + // for (var x = start; x <= end; x--) + editor.ReplaceNode(variable.Initializer!.Value, Reduce(start)); + editor.ReplaceNode(condition, Reduce(Invert(variable, condition, end))); } + else + { + throw new InvalidOperationException(); + } + + editor.ReplaceNode(after, InvertAfter(after)); + return document.WithSyntaxRoot(editor.GetChangedRoot()); + } - private static ExpressionSyntax Reduce(ExpressionSyntax expr) + private static ExpressionSyntax Reduce(ExpressionSyntax expr) + { + expr = expr.WalkDownParentheses(); + + if (expr is BinaryExpressionSyntax outerBinary) { - expr = expr.WalkDownParentheses(); + var reducedLeft = Reduce(outerBinary.Left); + var reducedRight = Reduce(outerBinary.Right); - if (expr is BinaryExpressionSyntax outerBinary) + // (... + 1) - 1 => ... + // (... - 1) + 1 => ... { - var reducedLeft = Reduce(outerBinary.Left); - var reducedRight = Reduce(outerBinary.Right); - - // (... + 1) - 1 => ... - // (... - 1) + 1 => ... + if (reducedLeft is BinaryExpressionSyntax innerLeft && + IsLiteralOne(innerLeft.Right) && + IsLiteralOne(reducedRight)) { - if (reducedLeft is BinaryExpressionSyntax innerLeft && - IsLiteralOne(innerLeft.Right) && - IsLiteralOne(reducedRight)) + if ((outerBinary.Kind() == SyntaxKind.SubtractExpression && innerLeft.Kind() == SyntaxKind.AddExpression) || + (outerBinary.Kind() == SyntaxKind.AddExpression && innerLeft.Kind() == SyntaxKind.SubtractExpression)) { - if ((outerBinary.Kind() == SyntaxKind.SubtractExpression && innerLeft.Kind() == SyntaxKind.AddExpression) || - (outerBinary.Kind() == SyntaxKind.AddExpression && innerLeft.Kind() == SyntaxKind.SubtractExpression)) - { - return Reduce(innerLeft.Left); - } + return Reduce(innerLeft.Left); } } + } - // v <= x - 1 => v < x - // x - 1 >= v => x > v + // v <= x - 1 => v < x + // x - 1 >= v => x > v + { + if (outerBinary.Kind() == SyntaxKind.LessThanOrEqualExpression && + reducedRight is BinaryExpressionSyntax innerRight && + innerRight.Kind() == SyntaxKind.SubtractExpression && + IsLiteralOne(innerRight.Right)) { - if (outerBinary.Kind() == SyntaxKind.LessThanOrEqualExpression && - reducedRight is BinaryExpressionSyntax innerRight && - innerRight.Kind() == SyntaxKind.SubtractExpression && - IsLiteralOne(innerRight.Right)) - { - var newOperator = SyntaxFactory.Token(SyntaxKind.LessThanToken).WithTriviaFrom(outerBinary.OperatorToken); - return Reduce(outerBinary.WithRight(innerRight.Left) - .WithOperatorToken(newOperator)); - } + var newOperator = SyntaxFactory.Token(SyntaxKind.LessThanToken).WithTriviaFrom(outerBinary.OperatorToken); + return Reduce(outerBinary.WithRight(innerRight.Left) + .WithOperatorToken(newOperator)); + } - if (outerBinary.Kind() == SyntaxKind.GreaterThanOrEqualExpression && - reducedLeft is BinaryExpressionSyntax innerLeft && - innerLeft.Kind() == SyntaxKind.SubtractExpression && - IsLiteralOne(innerLeft.Right)) - { - var newOperator = SyntaxFactory.Token(SyntaxKind.GreaterThanToken).WithTriviaFrom(outerBinary.OperatorToken); - return Reduce(outerBinary.WithRight(innerLeft.Left) - .WithOperatorToken(newOperator)); - } + if (outerBinary.Kind() == SyntaxKind.GreaterThanOrEqualExpression && + reducedLeft is BinaryExpressionSyntax innerLeft && + innerLeft.Kind() == SyntaxKind.SubtractExpression && + IsLiteralOne(innerLeft.Right)) + { + var newOperator = SyntaxFactory.Token(SyntaxKind.GreaterThanToken).WithTriviaFrom(outerBinary.OperatorToken); + return Reduce(outerBinary.WithRight(innerLeft.Left) + .WithOperatorToken(newOperator)); } } - - return expr.WithAdditionalAnnotations(Formatter.Annotation); } - private static BinaryExpressionSyntax Invert( - VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, ExpressionSyntax operand) - { - var (left, right) = IsVariableReference(variable, condition.Left) - ? (condition.Left, operand) - : (operand, condition.Right); + return expr.WithAdditionalAnnotations(Formatter.Annotation); + } - var newOperatorKind = condition.Kind() is SyntaxKind.LessThanExpression or SyntaxKind.LessThanOrEqualExpression - ? SyntaxKind.GreaterThanEqualsToken - : SyntaxKind.LessThanEqualsToken; + private static BinaryExpressionSyntax Invert( + VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, ExpressionSyntax operand) + { + var (left, right) = IsVariableReference(variable, condition.Left) + ? (condition.Left, operand) + : (operand, condition.Right); - var newExpressionKind = newOperatorKind == SyntaxKind.GreaterThanEqualsToken - ? SyntaxKind.GreaterThanOrEqualExpression - : SyntaxKind.LessThanOrEqualExpression; + var newOperatorKind = condition.Kind() is SyntaxKind.LessThanExpression or SyntaxKind.LessThanOrEqualExpression + ? SyntaxKind.GreaterThanEqualsToken + : SyntaxKind.LessThanEqualsToken; - var newOperator = SyntaxFactory.Token(newOperatorKind).WithTriviaFrom(condition.OperatorToken); - return SyntaxFactory.BinaryExpression(newExpressionKind, left, newOperator, right); - } + var newExpressionKind = newOperatorKind == SyntaxKind.GreaterThanEqualsToken + ? SyntaxKind.GreaterThanOrEqualExpression + : SyntaxKind.LessThanOrEqualExpression; - private static ExpressionSyntax InvertAfter(ExpressionSyntax after) + var newOperator = SyntaxFactory.Token(newOperatorKind).WithTriviaFrom(condition.OperatorToken); + return SyntaxFactory.BinaryExpression(newExpressionKind, left, newOperator, right); + } + + private static ExpressionSyntax InvertAfter(ExpressionSyntax after) + { + var opToken = after switch { - var opToken = after switch - { - PostfixUnaryExpressionSyntax postfixUnary => postfixUnary.OperatorToken, - PrefixUnaryExpressionSyntax prefixUnary => prefixUnary.OperatorToken, - AssignmentExpressionSyntax assignment => assignment.OperatorToken, - _ => throw ExceptionUtilities.UnexpectedValue(after.Kind()) - }; + PostfixUnaryExpressionSyntax postfixUnary => postfixUnary.OperatorToken, + PrefixUnaryExpressionSyntax prefixUnary => prefixUnary.OperatorToken, + AssignmentExpressionSyntax assignment => assignment.OperatorToken, + _ => throw ExceptionUtilities.UnexpectedValue(after.Kind()) + }; - var newKind = opToken.Kind() switch - { - SyntaxKind.MinusMinusToken => SyntaxKind.PlusPlusToken, - SyntaxKind.PlusPlusToken => SyntaxKind.MinusMinusToken, - SyntaxKind.PlusEqualsToken => SyntaxKind.MinusEqualsToken, - SyntaxKind.MinusEqualsToken => SyntaxKind.PlusEqualsToken, - _ => throw ExceptionUtilities.UnexpectedValue(opToken.Kind()) - }; - - var newOpToken = SyntaxFactory.Token(newKind).WithTriviaFrom(opToken); - return after.ReplaceToken(opToken, newOpToken); - } + var newKind = opToken.Kind() switch + { + SyntaxKind.MinusMinusToken => SyntaxKind.PlusPlusToken, + SyntaxKind.PlusPlusToken => SyntaxKind.MinusMinusToken, + SyntaxKind.PlusEqualsToken => SyntaxKind.MinusEqualsToken, + SyntaxKind.MinusEqualsToken => SyntaxKind.PlusEqualsToken, + _ => throw ExceptionUtilities.UnexpectedValue(opToken.Kind()) + }; + + var newOpToken = SyntaxFactory.Token(newKind).WithTriviaFrom(opToken); + return after.ReplaceToken(opToken, newOpToken); } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/AbstractCSharpSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/AbstractCSharpSignatureHelpProvider.cs index 487442665d13d..0c96f4a475229 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/AbstractCSharpSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/AbstractCSharpSignatureHelpProvider.cs @@ -19,64 +19,63 @@ using Microsoft.CodeAnalysis.SignatureHelp; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +internal abstract partial class AbstractCSharpSignatureHelpProvider : AbstractSignatureHelpProvider { - internal abstract partial class AbstractCSharpSignatureHelpProvider : AbstractSignatureHelpProvider - { - private static readonly SymbolDisplayFormat s_allowDefaultLiteralFormat = SymbolDisplayFormat.MinimallyQualifiedFormat - .AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral); + private static readonly SymbolDisplayFormat s_allowDefaultLiteralFormat = SymbolDisplayFormat.MinimallyQualifiedFormat + .AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral); - protected AbstractCSharpSignatureHelpProvider() - { - } + protected AbstractCSharpSignatureHelpProvider() + { + } - protected static SymbolDisplayPart Keyword(SyntaxKind kind) - => new SymbolDisplayPart(SymbolDisplayPartKind.Keyword, null, SyntaxFacts.GetText(kind)); + protected static SymbolDisplayPart Keyword(SyntaxKind kind) + => new SymbolDisplayPart(SymbolDisplayPartKind.Keyword, null, SyntaxFacts.GetText(kind)); - protected static SymbolDisplayPart Operator(SyntaxKind kind) - => new SymbolDisplayPart(SymbolDisplayPartKind.Operator, null, SyntaxFacts.GetText(kind)); + protected static SymbolDisplayPart Operator(SyntaxKind kind) + => new SymbolDisplayPart(SymbolDisplayPartKind.Operator, null, SyntaxFacts.GetText(kind)); - protected static SymbolDisplayPart Punctuation(SyntaxKind kind) - => new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, SyntaxFacts.GetText(kind)); + protected static SymbolDisplayPart Punctuation(SyntaxKind kind) + => new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, SyntaxFacts.GetText(kind)); - protected static SymbolDisplayPart Text(string text) - => new SymbolDisplayPart(SymbolDisplayPartKind.Text, null, text); + protected static SymbolDisplayPart Text(string text) + => new SymbolDisplayPart(SymbolDisplayPartKind.Text, null, text); - protected static SymbolDisplayPart Space() - => new SymbolDisplayPart(SymbolDisplayPartKind.Space, null, " "); + protected static SymbolDisplayPart Space() + => new SymbolDisplayPart(SymbolDisplayPartKind.Space, null, " "); - protected static SymbolDisplayPart NewLine() - => new SymbolDisplayPart(SymbolDisplayPartKind.LineBreak, null, "\r\n"); + protected static SymbolDisplayPart NewLine() + => new SymbolDisplayPart(SymbolDisplayPartKind.LineBreak, null, "\r\n"); - private static readonly IList _separatorParts = - [ - Punctuation(SyntaxKind.CommaToken), - Space() - ]; + private static readonly IList _separatorParts = + [ + Punctuation(SyntaxKind.CommaToken), + Space() + ]; - protected static IList GetSeparatorParts() => _separatorParts; + protected static IList GetSeparatorParts() => _separatorParts; - protected static SignatureHelpSymbolParameter Convert( - IParameterSymbol parameter, - SemanticModel semanticModel, - int position, - IDocumentationCommentFormattingService formatter) - { - return new SignatureHelpSymbolParameter( - parameter.Name, - parameter.IsOptional, - parameter.GetDocumentationPartsFactory(semanticModel, position, formatter), - parameter.ToMinimalDisplayParts(semanticModel, position, s_allowDefaultLiteralFormat)); - } + protected static SignatureHelpSymbolParameter Convert( + IParameterSymbol parameter, + SemanticModel semanticModel, + int position, + IDocumentationCommentFormattingService formatter) + { + return new SignatureHelpSymbolParameter( + parameter.Name, + parameter.IsOptional, + parameter.GetDocumentationPartsFactory(semanticModel, position, formatter), + parameter.ToMinimalDisplayParts(semanticModel, position, s_allowDefaultLiteralFormat)); + } - /// - /// We no longer show awaitable usage text in SignatureHelp, but IntelliCode expects this - /// method to exist. - /// - [Obsolete("Expected to exist by IntelliCode. This can be removed once their unnecessary use of this is removed.")] + /// + /// We no longer show awaitable usage text in SignatureHelp, but IntelliCode expects this + /// method to exist. + /// + [Obsolete("Expected to exist by IntelliCode. This can be removed once their unnecessary use of this is removed.")] #pragma warning disable CA1822 // Mark members as static - see obsolete message above. - protected IList GetAwaitableUsage(IMethodSymbol method, SemanticModel semanticModel, int position) + protected IList GetAwaitableUsage(IMethodSymbol method, SemanticModel semanticModel, int position) #pragma warning restore CA1822 // Mark members as static - => SpecializedCollections.EmptyList(); - } + => SpecializedCollections.EmptyList(); } diff --git a/src/Features/CSharp/Portable/SignatureHelp/AbstractOrdinaryMethodSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/AbstractOrdinaryMethodSignatureHelpProvider.cs index 4e46bdc7a29f1..e1ac88896801d 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/AbstractOrdinaryMethodSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/AbstractOrdinaryMethodSignatureHelpProvider.cs @@ -10,82 +10,81 @@ using Microsoft.CodeAnalysis.SignatureHelp; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +internal abstract class AbstractOrdinaryMethodSignatureHelpProvider : AbstractCSharpSignatureHelpProvider { - internal abstract class AbstractOrdinaryMethodSignatureHelpProvider : AbstractCSharpSignatureHelpProvider + internal static SignatureHelpItem ConvertMethodGroupMethod( + Document document, + IMethodSymbol method, + int position, + SemanticModel semanticModel) { - internal static SignatureHelpItem ConvertMethodGroupMethod( - Document document, - IMethodSymbol method, - int position, - SemanticModel semanticModel) - { - return ConvertMethodGroupMethod(document, method, position, semanticModel, descriptionParts: null); - } - - internal static SignatureHelpItem ConvertMethodGroupMethod( - Document document, - IMethodSymbol method, - int position, - SemanticModel semanticModel, - IList? descriptionParts) - { - var structuralTypeDisplayService = document.GetRequiredLanguageService(); - var documentationCommentFormattingService = document.GetRequiredLanguageService(); + return ConvertMethodGroupMethod(document, method, position, semanticModel, descriptionParts: null); + } - return CreateItemImpl( - method, semanticModel, position, - structuralTypeDisplayService, - method.IsParams(), - c => method.OriginalDefinition.GetDocumentationParts(semanticModel, position, documentationCommentFormattingService, c), - GetMethodGroupPreambleParts(method, semanticModel, position), - GetSeparatorParts(), - GetMethodGroupPostambleParts(), - method.Parameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList(), - descriptionParts: descriptionParts); - } + internal static SignatureHelpItem ConvertMethodGroupMethod( + Document document, + IMethodSymbol method, + int position, + SemanticModel semanticModel, + IList? descriptionParts) + { + var structuralTypeDisplayService = document.GetRequiredLanguageService(); + var documentationCommentFormattingService = document.GetRequiredLanguageService(); - private static IList GetMethodGroupPreambleParts( - IMethodSymbol method, - SemanticModel semanticModel, - int position) - { - var result = new List(); + return CreateItemImpl( + method, semanticModel, position, + structuralTypeDisplayService, + method.IsParams(), + c => method.OriginalDefinition.GetDocumentationParts(semanticModel, position, documentationCommentFormattingService, c), + GetMethodGroupPreambleParts(method, semanticModel, position), + GetSeparatorParts(), + GetMethodGroupPostambleParts(), + method.Parameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList(), + descriptionParts: descriptionParts); + } - var awaitable = method.GetOriginalUnreducedDefinition().IsAwaitableNonDynamic(semanticModel, position); - var extension = method.GetOriginalUnreducedDefinition().IsExtensionMethod(); + private static IList GetMethodGroupPreambleParts( + IMethodSymbol method, + SemanticModel semanticModel, + int position) + { + var result = new List(); - if (awaitable && extension) - { - result.Add(Punctuation(SyntaxKind.OpenParenToken)); - result.Add(Text(CSharpFeaturesResources.awaitable)); - result.Add(Punctuation(SyntaxKind.CommaToken)); - result.Add(Text(CSharpFeaturesResources.extension)); - result.Add(Punctuation(SyntaxKind.CloseParenToken)); - result.Add(Space()); - } - else if (awaitable) - { - result.Add(Punctuation(SyntaxKind.OpenParenToken)); - result.Add(Text(CSharpFeaturesResources.awaitable)); - result.Add(Punctuation(SyntaxKind.CloseParenToken)); - result.Add(Space()); - } - else if (extension) - { - result.Add(Punctuation(SyntaxKind.OpenParenToken)); - result.Add(Text(CSharpFeaturesResources.extension)); - result.Add(Punctuation(SyntaxKind.CloseParenToken)); - result.Add(Space()); - } + var awaitable = method.GetOriginalUnreducedDefinition().IsAwaitableNonDynamic(semanticModel, position); + var extension = method.GetOriginalUnreducedDefinition().IsExtensionMethod(); - result.AddRange(method.ToMinimalDisplayParts(semanticModel, position, MinimallyQualifiedWithoutParametersFormat)); + if (awaitable && extension) + { result.Add(Punctuation(SyntaxKind.OpenParenToken)); - - return result; + result.Add(Text(CSharpFeaturesResources.awaitable)); + result.Add(Punctuation(SyntaxKind.CommaToken)); + result.Add(Text(CSharpFeaturesResources.extension)); + result.Add(Punctuation(SyntaxKind.CloseParenToken)); + result.Add(Space()); + } + else if (awaitable) + { + result.Add(Punctuation(SyntaxKind.OpenParenToken)); + result.Add(Text(CSharpFeaturesResources.awaitable)); + result.Add(Punctuation(SyntaxKind.CloseParenToken)); + result.Add(Space()); + } + else if (extension) + { + result.Add(Punctuation(SyntaxKind.OpenParenToken)); + result.Add(Text(CSharpFeaturesResources.extension)); + result.Add(Punctuation(SyntaxKind.CloseParenToken)); + result.Add(Space()); } - private static IList GetMethodGroupPostambleParts() - => SpecializedCollections.SingletonList(Punctuation(SyntaxKind.CloseParenToken)); + result.AddRange(method.ToMinimalDisplayParts(semanticModel, position, MinimallyQualifiedWithoutParametersFormat)); + result.Add(Punctuation(SyntaxKind.OpenParenToken)); + + return result; } + + private static IList GetMethodGroupPostambleParts() + => SpecializedCollections.SingletonList(Punctuation(SyntaxKind.CloseParenToken)); } diff --git a/src/Features/CSharp/Portable/SignatureHelp/AttributeSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/AttributeSignatureHelpProvider.cs index ead4faa9e88d4..ceac2f1a40932 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/AttributeSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/AttributeSignatureHelpProvider.cs @@ -20,215 +20,214 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +[ExportSignatureHelpProvider("AttributeSignatureHelpProvider", LanguageNames.CSharp), Shared] +internal partial class AttributeSignatureHelpProvider : AbstractCSharpSignatureHelpProvider { - [ExportSignatureHelpProvider("AttributeSignatureHelpProvider", LanguageNames.CSharp), Shared] - internal partial class AttributeSignatureHelpProvider : AbstractCSharpSignatureHelpProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public AttributeSignatureHelpProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public AttributeSignatureHelpProvider() - { - } + } - public override bool IsTriggerCharacter(char ch) - => ch is '(' or ','; + public override bool IsTriggerCharacter(char ch) + => ch is '(' or ','; - public override bool IsRetriggerCharacter(char ch) - => ch == ')'; + public override bool IsRetriggerCharacter(char ch) + => ch == ')'; - private bool TryGetAttributeExpression( - SyntaxNode root, - int position, - ISyntaxFactsService syntaxFacts, - SignatureHelpTriggerReason triggerReason, - CancellationToken cancellationToken, - [NotNullWhen(true)] out AttributeSyntax? attribute) + private bool TryGetAttributeExpression( + SyntaxNode root, + int position, + ISyntaxFactsService syntaxFacts, + SignatureHelpTriggerReason triggerReason, + CancellationToken cancellationToken, + [NotNullWhen(true)] out AttributeSyntax? attribute) + { + if (!CommonSignatureHelpUtilities.TryGetSyntax( + root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out attribute)) { - if (!CommonSignatureHelpUtilities.TryGetSyntax( - root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out attribute)) - { - return false; - } - - return attribute.ArgumentList != null; + return false; } - private bool IsTriggerToken(SyntaxToken token) + return attribute.ArgumentList != null; + } + + private bool IsTriggerToken(SyntaxToken token) + { + return !token.IsKind(SyntaxKind.None) && + token.ValueText.Length == 1 && + IsTriggerCharacter(token.ValueText[0]) && + token.Parent is AttributeArgumentListSyntax && + token.Parent.Parent is AttributeSyntax; + } + + private static bool IsArgumentListToken(AttributeSyntax expression, SyntaxToken token) + { + return expression.ArgumentList != null && + expression.ArgumentList.Span.Contains(token.SpanStart) && + token != expression.ArgumentList.CloseParenToken; + } + + protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (!TryGetAttributeExpression(root, position, document.GetRequiredLanguageService(), triggerInfo.TriggerReason, cancellationToken, out var attribute)) { - return !token.IsKind(SyntaxKind.None) && - token.ValueText.Length == 1 && - IsTriggerCharacter(token.ValueText[0]) && - token.Parent is AttributeArgumentListSyntax && - token.Parent.Parent is AttributeSyntax; + return null; } - private static bool IsArgumentListToken(AttributeSyntax expression, SyntaxToken token) + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(attribute, cancellationToken).ConfigureAwait(false); + if (semanticModel.GetTypeInfo(attribute, cancellationToken).Type is not INamedTypeSymbol attributeType) { - return expression.ArgumentList != null && - expression.ArgumentList.Span.Contains(token.SpanStart) && - token != expression.ArgumentList.CloseParenToken; + return null; } - protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) + var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); + if (within == null) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (!TryGetAttributeExpression(root, position, document.GetRequiredLanguageService(), triggerInfo.TriggerReason, cancellationToken, out var attribute)) - { - return null; - } - - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(attribute, cancellationToken).ConfigureAwait(false); - if (semanticModel.GetTypeInfo(attribute, cancellationToken).Type is not INamedTypeSymbol attributeType) - { - return null; - } + return null; + } - var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); - if (within == null) - { - return null; - } + var accessibleConstructors = attributeType.InstanceConstructors + .WhereAsArray(c => c.IsAccessibleWithin(within)) + .FilterToVisibleAndBrowsableSymbols(options.HideAdvancedMembers, semanticModel.Compilation) + .Sort(semanticModel, attribute.SpanStart); - var accessibleConstructors = attributeType.InstanceConstructors - .WhereAsArray(c => c.IsAccessibleWithin(within)) - .FilterToVisibleAndBrowsableSymbols(options.HideAdvancedMembers, semanticModel.Compilation) - .Sort(semanticModel, attribute.SpanStart); + if (!accessibleConstructors.Any()) + { + return null; + } - if (!accessibleConstructors.Any()) - { - return null; - } + var structuralTypeDisplayService = document.GetRequiredLanguageService(); + var documentationCommentFormatter = document.GetRequiredLanguageService(); + var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(attribute.ArgumentList!); + var syntaxFacts = document.GetRequiredLanguageService(); - var structuralTypeDisplayService = document.GetRequiredLanguageService(); - var documentationCommentFormatter = document.GetRequiredLanguageService(); - var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(attribute.ArgumentList!); - var syntaxFacts = document.GetRequiredLanguageService(); + var symbolInfo = semanticModel.GetSymbolInfo(attribute, cancellationToken); + var selectedItem = TryGetSelectedIndex(accessibleConstructors, symbolInfo.Symbol); - var symbolInfo = semanticModel.GetSymbolInfo(attribute, cancellationToken); - var selectedItem = TryGetSelectedIndex(accessibleConstructors, symbolInfo.Symbol); + return CreateSignatureHelpItems(accessibleConstructors.Select(c => + Convert(c, within, attribute, semanticModel, structuralTypeDisplayService, documentationCommentFormatter, cancellationToken)).ToList(), + textSpan, GetCurrentArgumentState(root, position, syntaxFacts, textSpan, cancellationToken), selectedItem, parameterIndexOverride: -1); + } - return CreateSignatureHelpItems(accessibleConstructors.Select(c => - Convert(c, within, attribute, semanticModel, structuralTypeDisplayService, documentationCommentFormatter, cancellationToken)).ToList(), - textSpan, GetCurrentArgumentState(root, position, syntaxFacts, textSpan, cancellationToken), selectedItem, parameterIndexOverride: -1); + private SignatureHelpState? GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken) + { + if (TryGetAttributeExpression(root, position, syntaxFacts, SignatureHelpTriggerReason.InvokeSignatureHelpCommand, cancellationToken, out var expression) && + currentSpan.Start == SignatureHelpUtilities.GetSignatureHelpSpan(expression.ArgumentList!).Start) + { + return SignatureHelpUtilities.GetSignatureHelpState(expression.ArgumentList!, position); } - private SignatureHelpState? GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken) - { - if (TryGetAttributeExpression(root, position, syntaxFacts, SignatureHelpTriggerReason.InvokeSignatureHelpCommand, cancellationToken, out var expression) && - currentSpan.Start == SignatureHelpUtilities.GetSignatureHelpSpan(expression.ArgumentList!).Start) - { - return SignatureHelpUtilities.GetSignatureHelpState(expression.ArgumentList!, position); - } + return null; + } - return null; - } + private static SignatureHelpItem Convert( + IMethodSymbol constructor, + ISymbol within, + AttributeSyntax attribute, + SemanticModel semanticModel, + IStructuralTypeDisplayService structuralTypeDisplayService, + IDocumentationCommentFormattingService documentationCommentFormatter, + CancellationToken cancellationToken) + { + var position = attribute.SpanStart; + var namedParameters = constructor.ContainingType.GetAttributeNamedParameters(semanticModel.Compilation, within) + .OrderBy(s => s.Name) + .ToList(); + + var isVariadic = + constructor.Parameters is [.., { IsParams: true }] && namedParameters.Count == 0; + + var item = CreateItem( + constructor, semanticModel, position, + structuralTypeDisplayService, + isVariadic, + constructor.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormatter), + GetPreambleParts(constructor, semanticModel, position), + GetSeparatorParts(), + GetPostambleParts(), + GetParameters(constructor, semanticModel, position, namedParameters, documentationCommentFormatter, cancellationToken)); + return item; + } - private static SignatureHelpItem Convert( - IMethodSymbol constructor, - ISymbol within, - AttributeSyntax attribute, - SemanticModel semanticModel, - IStructuralTypeDisplayService structuralTypeDisplayService, - IDocumentationCommentFormattingService documentationCommentFormatter, - CancellationToken cancellationToken) + private static IList GetParameters( + IMethodSymbol constructor, + SemanticModel semanticModel, + int position, + IList namedParameters, + IDocumentationCommentFormattingService documentationCommentFormatter, + CancellationToken cancellationToken) + { + var result = new List(); + foreach (var parameter in constructor.Parameters) { - var position = attribute.SpanStart; - var namedParameters = constructor.ContainingType.GetAttributeNamedParameters(semanticModel.Compilation, within) - .OrderBy(s => s.Name) - .ToList(); - - var isVariadic = - constructor.Parameters is [.., { IsParams: true }] && namedParameters.Count == 0; - - var item = CreateItem( - constructor, semanticModel, position, - structuralTypeDisplayService, - isVariadic, - constructor.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormatter), - GetPreambleParts(constructor, semanticModel, position), - GetSeparatorParts(), - GetPostambleParts(), - GetParameters(constructor, semanticModel, position, namedParameters, documentationCommentFormatter, cancellationToken)); - return item; + result.Add(Convert(parameter, semanticModel, position, documentationCommentFormatter)); } - private static IList GetParameters( - IMethodSymbol constructor, - SemanticModel semanticModel, - int position, - IList namedParameters, - IDocumentationCommentFormattingService documentationCommentFormatter, - CancellationToken cancellationToken) + for (var i = 0; i < namedParameters.Count; i++) { - var result = new List(); - foreach (var parameter in constructor.Parameters) - { - result.Add(Convert(parameter, semanticModel, position, documentationCommentFormatter)); - } + cancellationToken.ThrowIfCancellationRequested(); - for (var i = 0; i < namedParameters.Count; i++) + var namedParameter = namedParameters[i]; + + var type = namedParameter is IFieldSymbol ? ((IFieldSymbol)namedParameter).Type : ((IPropertySymbol)namedParameter).Type; + + var displayParts = new List { - cancellationToken.ThrowIfCancellationRequested(); - - var namedParameter = namedParameters[i]; - - var type = namedParameter is IFieldSymbol ? ((IFieldSymbol)namedParameter).Type : ((IPropertySymbol)namedParameter).Type; - - var displayParts = new List - { - new SymbolDisplayPart( - namedParameter is IFieldSymbol ? SymbolDisplayPartKind.FieldName : SymbolDisplayPartKind.PropertyName, - namedParameter, namedParameter.Name.ToIdentifierToken().ToString()), - Space(), - Punctuation(SyntaxKind.EqualsToken), - Space() - }; - displayParts.AddRange(type.ToMinimalDisplayParts(semanticModel, position)); - - result.Add(new SignatureHelpSymbolParameter( - namedParameter.Name, - isOptional: true, - documentationFactory: namedParameter.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormatter), - displayParts: displayParts, - prefixDisplayParts: GetParameterPrefixDisplayParts(i))); - } - - return result; + new SymbolDisplayPart( + namedParameter is IFieldSymbol ? SymbolDisplayPartKind.FieldName : SymbolDisplayPartKind.PropertyName, + namedParameter, namedParameter.Name.ToIdentifierToken().ToString()), + Space(), + Punctuation(SyntaxKind.EqualsToken), + Space() + }; + displayParts.AddRange(type.ToMinimalDisplayParts(semanticModel, position)); + + result.Add(new SignatureHelpSymbolParameter( + namedParameter.Name, + isOptional: true, + documentationFactory: namedParameter.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormatter), + displayParts: displayParts, + prefixDisplayParts: GetParameterPrefixDisplayParts(i))); } - private static List? GetParameterPrefixDisplayParts(int i) - { - if (i == 0) - { - return - [ - new SymbolDisplayPart(SymbolDisplayPartKind.Text, null, CSharpFeaturesResources.Properties), - Punctuation(SyntaxKind.ColonToken), - Space() - ]; - } + return result; + } - return null; + private static List? GetParameterPrefixDisplayParts(int i) + { + if (i == 0) + { + return + [ + new SymbolDisplayPart(SymbolDisplayPartKind.Text, null, CSharpFeaturesResources.Properties), + Punctuation(SyntaxKind.ColonToken), + Space() + ]; } - private static IList GetPreambleParts( - IMethodSymbol method, - SemanticModel semanticModel, - int position) - { - var result = new List(); + return null; + } - result.AddRange(method.ContainingType.ToMinimalDisplayParts(semanticModel, position)); - result.Add(Punctuation(SyntaxKind.OpenParenToken)); + private static IList GetPreambleParts( + IMethodSymbol method, + SemanticModel semanticModel, + int position) + { + var result = new List(); - return result; - } + result.AddRange(method.ContainingType.ToMinimalDisplayParts(semanticModel, position)); + result.Add(Punctuation(SyntaxKind.OpenParenToken)); - private static IList GetPostambleParts() - { - return SpecializedCollections.SingletonList( - Punctuation(SyntaxKind.CloseParenToken)); - } + return result; + } + + private static IList GetPostambleParts() + { + return SpecializedCollections.SingletonList( + Punctuation(SyntaxKind.CloseParenToken)); } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/ConstructorInitializerSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/ConstructorInitializerSignatureHelpProvider.cs index 32dd587c18017..6e3da4f3cb31c 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/ConstructorInitializerSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/ConstructorInitializerSignatureHelpProvider.cs @@ -19,154 +19,153 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +[ExportSignatureHelpProvider("ConstructorInitializerSignatureHelpProvider", LanguageNames.CSharp), Shared] +internal partial class ConstructorInitializerSignatureHelpProvider : AbstractCSharpSignatureHelpProvider { - [ExportSignatureHelpProvider("ConstructorInitializerSignatureHelpProvider", LanguageNames.CSharp), Shared] - internal partial class ConstructorInitializerSignatureHelpProvider : AbstractCSharpSignatureHelpProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ConstructorInitializerSignatureHelpProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ConstructorInitializerSignatureHelpProvider() - { - } - - public override bool IsTriggerCharacter(char ch) - => ch is '(' or ','; + } - public override bool IsRetriggerCharacter(char ch) - => ch == ')'; + public override bool IsTriggerCharacter(char ch) + => ch is '(' or ','; - private async Task TryGetConstructorInitializerAsync( - Document document, - int position, - SignatureHelpTriggerReason triggerReason, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); + public override bool IsRetriggerCharacter(char ch) + => ch == ')'; - if (!CommonSignatureHelpUtilities.TryGetSyntax( - root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out ConstructorInitializerSyntax? initializer)) - { - return null; - } - - if (initializer.ArgumentList is null) - return null; + private async Task TryGetConstructorInitializerAsync( + Document document, + int position, + SignatureHelpTriggerReason triggerReason, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); - return initializer; + if (!CommonSignatureHelpUtilities.TryGetSyntax( + root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out ConstructorInitializerSyntax? initializer)) + { + return null; } - private bool IsTriggerToken(SyntaxToken token) - => SignatureHelpUtilities.IsTriggerParenOrComma(token, IsTriggerCharacter); + if (initializer.ArgumentList is null) + return null; - private static bool IsArgumentListToken(ConstructorInitializerSyntax expression, SyntaxToken token) - { - return expression.ArgumentList != null && - expression.ArgumentList.Span.Contains(token.SpanStart) && - token != expression.ArgumentList.CloseParenToken; - } + return initializer; + } - protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) - { - var constructorInitializer = await TryGetConstructorInitializerAsync( - document, position, triggerInfo.TriggerReason, cancellationToken).ConfigureAwait(false); - if (constructorInitializer == null) - return null; + private bool IsTriggerToken(SyntaxToken token) + => SignatureHelpUtilities.IsTriggerParenOrComma(token, IsTriggerCharacter); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var within = semanticModel.GetEnclosingNamedType(position, cancellationToken); - if (within == null) - return null; + private static bool IsArgumentListToken(ConstructorInitializerSyntax expression, SyntaxToken token) + { + return expression.ArgumentList != null && + expression.ArgumentList.Span.Contains(token.SpanStart) && + token != expression.ArgumentList.CloseParenToken; + } - if (within.TypeKind is not TypeKind.Struct and not TypeKind.Class) - return null; + protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) + { + var constructorInitializer = await TryGetConstructorInitializerAsync( + document, position, triggerInfo.TriggerReason, cancellationToken).ConfigureAwait(false); + if (constructorInitializer == null) + return null; - var type = constructorInitializer.Kind() == SyntaxKind.BaseConstructorInitializer - ? within.BaseType - : within; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var within = semanticModel.GetEnclosingNamedType(position, cancellationToken); + if (within == null) + return null; - if (type == null) - return null; + if (within.TypeKind is not TypeKind.Struct and not TypeKind.Class) + return null; - // get the candidate methods - var currentConstructor = semanticModel.GetDeclaredSymbol(constructorInitializer.Parent!, cancellationToken); + var type = constructorInitializer.Kind() == SyntaxKind.BaseConstructorInitializer + ? within.BaseType + : within; - var constructors = type.InstanceConstructors - .WhereAsArray(c => c.IsAccessibleWithin(within) && !c.Equals(currentConstructor)) - .WhereAsArray(c => c.IsEditorBrowsable(options.HideAdvancedMembers, semanticModel.Compilation)) - .Sort(semanticModel, constructorInitializer.SpanStart); + if (type == null) + return null; - if (!constructors.Any()) - return null; + // get the candidate methods + var currentConstructor = semanticModel.GetDeclaredSymbol(constructorInitializer.Parent!, cancellationToken); - var (currentSymbol, parameterIndexOverride) = new LightweightOverloadResolution(semanticModel, position, constructorInitializer.ArgumentList.Arguments) - .RefineOverloadAndPickParameter(semanticModel.GetSymbolInfo(constructorInitializer, cancellationToken), constructors); + var constructors = type.InstanceConstructors + .WhereAsArray(c => c.IsAccessibleWithin(within) && !c.Equals(currentConstructor)) + .WhereAsArray(c => c.IsEditorBrowsable(options.HideAdvancedMembers, semanticModel.Compilation)) + .Sort(semanticModel, constructorInitializer.SpanStart); - // present items and select - var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(constructorInitializer.ArgumentList); - var structuralTypeDisplayService = document.GetRequiredLanguageService(); - var documentationCommentFormattingService = document.GetRequiredLanguageService(); + if (!constructors.Any()) + return null; - var items = constructors.SelectAsArray(m => Convert(m, constructorInitializer.ArgumentList.OpenParenToken, semanticModel, structuralTypeDisplayService, documentationCommentFormattingService)); - var selectedItem = TryGetSelectedIndex(constructors, currentSymbol); + var (currentSymbol, parameterIndexOverride) = new LightweightOverloadResolution(semanticModel, position, constructorInitializer.ArgumentList.Arguments) + .RefineOverloadAndPickParameter(semanticModel.GetSymbolInfo(constructorInitializer, cancellationToken), constructors); - var argumentState = await GetCurrentArgumentStateAsync( - document, position, textSpan, cancellationToken).ConfigureAwait(false); - return CreateSignatureHelpItems(items, textSpan, argumentState, selectedItem, parameterIndexOverride); - } + // present items and select + var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(constructorInitializer.ArgumentList); + var structuralTypeDisplayService = document.GetRequiredLanguageService(); + var documentationCommentFormattingService = document.GetRequiredLanguageService(); - private async Task GetCurrentArgumentStateAsync( - Document document, int position, TextSpan currentSpan, CancellationToken cancellationToken) - { - var initializer = await TryGetConstructorInitializerAsync( - document, position, SignatureHelpTriggerReason.InvokeSignatureHelpCommand, cancellationToken).ConfigureAwait(false); - if (initializer is { ArgumentList: not null } && - currentSpan.Start == SignatureHelpUtilities.GetSignatureHelpSpan(initializer.ArgumentList).Start) - { - return SignatureHelpUtilities.GetSignatureHelpState(initializer.ArgumentList, position); - } + var items = constructors.SelectAsArray(m => Convert(m, constructorInitializer.ArgumentList.OpenParenToken, semanticModel, structuralTypeDisplayService, documentationCommentFormattingService)); + var selectedItem = TryGetSelectedIndex(constructors, currentSymbol); - return null; - } + var argumentState = await GetCurrentArgumentStateAsync( + document, position, textSpan, cancellationToken).ConfigureAwait(false); + return CreateSignatureHelpItems(items, textSpan, argumentState, selectedItem, parameterIndexOverride); + } - private static SignatureHelpItem Convert( - IMethodSymbol constructor, - SyntaxToken openToken, - SemanticModel semanticModel, - IStructuralTypeDisplayService structuralTypeDisplayService, - IDocumentationCommentFormattingService documentationCommentFormattingService) + private async Task GetCurrentArgumentStateAsync( + Document document, int position, TextSpan currentSpan, CancellationToken cancellationToken) + { + var initializer = await TryGetConstructorInitializerAsync( + document, position, SignatureHelpTriggerReason.InvokeSignatureHelpCommand, cancellationToken).ConfigureAwait(false); + if (initializer is { ArgumentList: not null } && + currentSpan.Start == SignatureHelpUtilities.GetSignatureHelpSpan(initializer.ArgumentList).Start) { - var position = openToken.SpanStart; - var item = CreateItem( - constructor, semanticModel, position, - structuralTypeDisplayService, - constructor.IsParams(), - constructor.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormattingService), - GetPreambleParts(constructor, semanticModel, position), - GetSeparatorParts(), - GetPostambleParts(), - constructor.Parameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList()); - return item; + return SignatureHelpUtilities.GetSignatureHelpState(initializer.ArgumentList, position); } - private static IList GetPreambleParts( - IMethodSymbol method, - SemanticModel semanticModel, - int position) - { - var result = new List(); + return null; + } - result.AddRange(method.ContainingType.ToMinimalDisplayParts(semanticModel, position)); - result.Add(Punctuation(SyntaxKind.OpenParenToken)); + private static SignatureHelpItem Convert( + IMethodSymbol constructor, + SyntaxToken openToken, + SemanticModel semanticModel, + IStructuralTypeDisplayService structuralTypeDisplayService, + IDocumentationCommentFormattingService documentationCommentFormattingService) + { + var position = openToken.SpanStart; + var item = CreateItem( + constructor, semanticModel, position, + structuralTypeDisplayService, + constructor.IsParams(), + constructor.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormattingService), + GetPreambleParts(constructor, semanticModel, position), + GetSeparatorParts(), + GetPostambleParts(), + constructor.Parameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList()); + return item; + } - return result; - } + private static IList GetPreambleParts( + IMethodSymbol method, + SemanticModel semanticModel, + int position) + { + var result = new List(); - private static IList GetPostambleParts() - { - return SpecializedCollections.SingletonList( - Punctuation(SyntaxKind.CloseParenToken)); - } + result.AddRange(method.ContainingType.ToMinimalDisplayParts(semanticModel, position)); + result.Add(Punctuation(SyntaxKind.OpenParenToken)); + + return result; + } + + private static IList GetPostambleParts() + { + return SpecializedCollections.SingletonList( + Punctuation(SyntaxKind.CloseParenToken)); } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/ElementAccessExpressionSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/ElementAccessExpressionSignatureHelpProvider.cs index c7261ead35a11..9bfd87398bf72 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/ElementAccessExpressionSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/ElementAccessExpressionSignatureHelpProvider.cs @@ -22,386 +22,385 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +[ExportSignatureHelpProvider("ElementAccessExpressionSignatureHelpProvider", LanguageNames.CSharp), Shared] +internal sealed class ElementAccessExpressionSignatureHelpProvider : AbstractCSharpSignatureHelpProvider { - [ExportSignatureHelpProvider("ElementAccessExpressionSignatureHelpProvider", LanguageNames.CSharp), Shared] - internal sealed class ElementAccessExpressionSignatureHelpProvider : AbstractCSharpSignatureHelpProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ElementAccessExpressionSignatureHelpProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ElementAccessExpressionSignatureHelpProvider() - { - } + } - public override bool IsTriggerCharacter(char ch) - => IsTriggerCharacterInternal(ch); + public override bool IsTriggerCharacter(char ch) + => IsTriggerCharacterInternal(ch); - private static bool IsTriggerCharacterInternal(char ch) - => ch is '[' or ','; + private static bool IsTriggerCharacterInternal(char ch) + => ch is '[' or ','; - public override bool IsRetriggerCharacter(char ch) - => ch == ']'; + public override bool IsRetriggerCharacter(char ch) + => ch == ']'; - private static bool TryGetElementAccessExpression(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, [NotNullWhen(true)] out ExpressionSyntax? identifier, out SyntaxToken openBrace) + private static bool TryGetElementAccessExpression(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, [NotNullWhen(true)] out ExpressionSyntax? identifier, out SyntaxToken openBrace) + { + return CompleteElementAccessExpression.TryGetSyntax(root, position, syntaxFacts, triggerReason, cancellationToken, out identifier, out openBrace) || + IncompleteElementAccessExpression.TryGetSyntax(root, position, syntaxFacts, triggerReason, cancellationToken, out identifier, out openBrace) || + ConditionalAccessExpression.TryGetSyntax(root, position, syntaxFacts, triggerReason, cancellationToken, out identifier, out openBrace); + } + + protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (!TryGetElementAccessExpression(root, position, document.GetRequiredLanguageService(), triggerInfo.TriggerReason, cancellationToken, out var expression, out var openBrace)) { - return CompleteElementAccessExpression.TryGetSyntax(root, position, syntaxFacts, triggerReason, cancellationToken, out identifier, out openBrace) || - IncompleteElementAccessExpression.TryGetSyntax(root, position, syntaxFacts, triggerReason, cancellationToken, out identifier, out openBrace) || - ConditionalAccessExpression.TryGetSyntax(root, position, syntaxFacts, triggerReason, cancellationToken, out identifier, out openBrace); + return null; } - protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var expressionSymbol = semanticModel.GetSymbolInfo(expression, cancellationToken).GetAnySymbol(); + // goo?[$$] + if (expressionSymbol is INamedTypeSymbol namedType) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (!TryGetElementAccessExpression(root, position, document.GetRequiredLanguageService(), triggerInfo.TriggerReason, cancellationToken, out var expression, out var openBrace)) + if (namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T && + expression.IsKind(SyntaxKind.NullableType) && + expression.IsChildNode(a => a.ElementType)) { - return null; - } - - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var expressionSymbol = semanticModel.GetSymbolInfo(expression, cancellationToken).GetAnySymbol(); - // goo?[$$] - if (expressionSymbol is INamedTypeSymbol namedType) - { - if (namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T && - expression.IsKind(SyntaxKind.NullableType) && - expression.IsChildNode(a => a.ElementType)) - { - // Speculatively bind the type part of the nullable as an expression - var nullableTypeSyntax = (NullableTypeSyntax)expression; - var speculativeBinding = semanticModel.GetSpeculativeSymbolInfo(position, nullableTypeSyntax.ElementType, SpeculativeBindingOption.BindAsExpression); - expressionSymbol = speculativeBinding.GetAnySymbol(); - expression = nullableTypeSyntax.ElementType; - } + // Speculatively bind the type part of the nullable as an expression + var nullableTypeSyntax = (NullableTypeSyntax)expression; + var speculativeBinding = semanticModel.GetSpeculativeSymbolInfo(position, nullableTypeSyntax.ElementType, SpeculativeBindingOption.BindAsExpression); + expressionSymbol = speculativeBinding.GetAnySymbol(); + expression = nullableTypeSyntax.ElementType; } + } - if (expressionSymbol is not null and INamedTypeSymbol) - { - return null; - } + if (expressionSymbol is not null and INamedTypeSymbol) + { + return null; + } - if (!TryGetIndexers(position, semanticModel, expression, cancellationToken, out var indexers, out var expressionType) && - !TryGetComIndexers(semanticModel, expression, cancellationToken, out indexers, out expressionType)) - { - return null; - } + if (!TryGetIndexers(position, semanticModel, expression, cancellationToken, out var indexers, out var expressionType) && + !TryGetComIndexers(semanticModel, expression, cancellationToken, out indexers, out expressionType)) + { + return null; + } - var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); - if (within == null) - { - return null; - } + var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); + if (within == null) + { + return null; + } - var accessibleIndexers = indexers.WhereAsArray( - m => m.IsAccessibleWithin(within, throughType: expressionType)); - if (!accessibleIndexers.Any()) - { - return null; - } + var accessibleIndexers = indexers.WhereAsArray( + m => m.IsAccessibleWithin(within, throughType: expressionType)); + if (!accessibleIndexers.Any()) + { + return null; + } - accessibleIndexers = accessibleIndexers.FilterToVisibleAndBrowsableSymbols(options.HideAdvancedMembers, semanticModel.Compilation) - .Sort(semanticModel, expression.SpanStart); + accessibleIndexers = accessibleIndexers.FilterToVisibleAndBrowsableSymbols(options.HideAdvancedMembers, semanticModel.Compilation) + .Sort(semanticModel, expression.SpanStart); - var structuralTypeDisplayService = document.GetRequiredLanguageService(); - var documentationCommentFormattingService = document.GetRequiredLanguageService(); - var textSpan = GetTextSpan(expression, openBrace); - var syntaxFacts = document.GetRequiredLanguageService(); + var structuralTypeDisplayService = document.GetRequiredLanguageService(); + var documentationCommentFormattingService = document.GetRequiredLanguageService(); + var textSpan = GetTextSpan(expression, openBrace); + var syntaxFacts = document.GetRequiredLanguageService(); - return CreateSignatureHelpItems(accessibleIndexers.Select(p => - Convert(p, openBrace, semanticModel, structuralTypeDisplayService, documentationCommentFormattingService)).ToList(), - textSpan, GetCurrentArgumentState(root, position, syntaxFacts, textSpan, cancellationToken), selectedItemIndex: null, parameterIndexOverride: -1); - } + return CreateSignatureHelpItems(accessibleIndexers.Select(p => + Convert(p, openBrace, semanticModel, structuralTypeDisplayService, documentationCommentFormattingService)).ToList(), + textSpan, GetCurrentArgumentState(root, position, syntaxFacts, textSpan, cancellationToken), selectedItemIndex: null, parameterIndexOverride: -1); + } - private static TextSpan GetTextSpan(ExpressionSyntax expression, SyntaxToken openBracket) + private static TextSpan GetTextSpan(ExpressionSyntax expression, SyntaxToken openBracket) + { + if (openBracket.Parent is BracketedArgumentListSyntax) { - if (openBracket.Parent is BracketedArgumentListSyntax) + if (expression.Parent is ConditionalAccessExpressionSyntax conditional) { - if (expression.Parent is ConditionalAccessExpressionSyntax conditional) - { - return TextSpan.FromBounds(conditional.Span.Start, openBracket.FullSpan.End); - } - else - { - return CompleteElementAccessExpression.GetTextSpan(openBracket); - } + return TextSpan.FromBounds(conditional.Span.Start, openBracket.FullSpan.End); } - else if (openBracket.Parent is ArrayRankSpecifierSyntax) + else { - return IncompleteElementAccessExpression.GetTextSpan(expression, openBracket); + return CompleteElementAccessExpression.GetTextSpan(openBracket); } + } + else if (openBracket.Parent is ArrayRankSpecifierSyntax) + { + return IncompleteElementAccessExpression.GetTextSpan(expression, openBracket); + } - throw ExceptionUtilities.Unreachable(); + throw ExceptionUtilities.Unreachable(); + } + + private static SignatureHelpState? GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken) + { + if (!TryGetElementAccessExpression( + root, + position, + syntaxFacts, + SignatureHelpTriggerReason.InvokeSignatureHelpCommand, + cancellationToken, + out var expression, + out var openBracket) || + currentSpan.Start != expression.SpanStart) + { + return null; } - private static SignatureHelpState? GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken) + // If the user is actively typing, it's likely that we're in a broken state and the + // syntax tree will be incorrect. Because of this we need to synthesize a new + // bracketed argument list so we can correctly map the cursor to the current argument + // and then we need to account for this and offset the position check accordingly. + int offset; + BracketedArgumentListSyntax argumentList; + var newBracketedArgumentList = SyntaxFactory.ParseBracketedArgumentList(openBracket.Parent!.ToString()); + if (expression.Parent is ConditionalAccessExpressionSyntax) { - if (!TryGetElementAccessExpression( - root, - position, - syntaxFacts, - SignatureHelpTriggerReason.InvokeSignatureHelpCommand, - cancellationToken, - out var expression, - out var openBracket) || - currentSpan.Start != expression.SpanStart) - { - return null; - } + // The typed code looks like: ?[ + var elementBinding = SyntaxFactory.ElementBindingExpression(newBracketedArgumentList); + var conditionalAccessExpression = SyntaxFactory.ConditionalAccessExpression(expression, elementBinding); + offset = expression.SpanStart - conditionalAccessExpression.SpanStart; + argumentList = ((ElementBindingExpressionSyntax)conditionalAccessExpression.WhenNotNull).ArgumentList; + } + else + { + // The typed code looks like: + // [ + // or + // ?[ + var elementAccessExpression = SyntaxFactory.ElementAccessExpression(expression, newBracketedArgumentList); + offset = expression.SpanStart - elementAccessExpression.SpanStart; + argumentList = elementAccessExpression.ArgumentList; + } - // If the user is actively typing, it's likely that we're in a broken state and the - // syntax tree will be incorrect. Because of this we need to synthesize a new - // bracketed argument list so we can correctly map the cursor to the current argument - // and then we need to account for this and offset the position check accordingly. - int offset; - BracketedArgumentListSyntax argumentList; - var newBracketedArgumentList = SyntaxFactory.ParseBracketedArgumentList(openBracket.Parent!.ToString()); - if (expression.Parent is ConditionalAccessExpressionSyntax) - { - // The typed code looks like: ?[ - var elementBinding = SyntaxFactory.ElementBindingExpression(newBracketedArgumentList); - var conditionalAccessExpression = SyntaxFactory.ConditionalAccessExpression(expression, elementBinding); - offset = expression.SpanStart - conditionalAccessExpression.SpanStart; - argumentList = ((ElementBindingExpressionSyntax)conditionalAccessExpression.WhenNotNull).ArgumentList; - } - else - { - // The typed code looks like: - // [ - // or - // ?[ - var elementAccessExpression = SyntaxFactory.ElementAccessExpression(expression, newBracketedArgumentList); - offset = expression.SpanStart - elementAccessExpression.SpanStart; - argumentList = elementAccessExpression.ArgumentList; - } + position -= offset; + return SignatureHelpUtilities.GetSignatureHelpState(argumentList, position); + } - position -= offset; - return SignatureHelpUtilities.GetSignatureHelpState(argumentList, position); - } + private static bool TryGetComIndexers( + SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken, + out ImmutableArray indexers, out ITypeSymbol? expressionType) + { + indexers = semanticModel.GetMemberGroup(expression, cancellationToken) + .OfType() + .ToImmutableArray(); - private static bool TryGetComIndexers( - SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken, - out ImmutableArray indexers, out ITypeSymbol? expressionType) + if (indexers.Any() && expression is MemberAccessExpressionSyntax memberAccessExpression) { - indexers = semanticModel.GetMemberGroup(expression, cancellationToken) - .OfType() - .ToImmutableArray(); + expressionType = semanticModel.GetTypeInfo(memberAccessExpression.Expression, cancellationToken).Type!; + return true; + } - if (indexers.Any() && expression is MemberAccessExpressionSyntax memberAccessExpression) - { - expressionType = semanticModel.GetTypeInfo(memberAccessExpression.Expression, cancellationToken).Type!; - return true; - } + expressionType = null; + return false; + } - expressionType = null; + private static bool TryGetIndexers( + int position, SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken, + out ImmutableArray indexers, out ITypeSymbol? expressionType) + { + expressionType = semanticModel.GetTypeInfo(expression, cancellationToken).Type; + + if (expressionType == null) + { + indexers = []; return false; } - private static bool TryGetIndexers( - int position, SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken, - out ImmutableArray indexers, out ITypeSymbol? expressionType) + if (expressionType is IErrorTypeSymbol errorType) { - expressionType = semanticModel.GetTypeInfo(expression, cancellationToken).Type; + // If `expression` is a QualifiedNameSyntax then GetTypeInfo().Type won't have any CandidateSymbols, so + // we should then fall back to getting the actual symbol for the expression. + expressionType = errorType.CandidateSymbols.FirstOrDefault().GetSymbolType() + ?? semanticModel.GetSymbolInfo(expression).GetAnySymbol().GetSymbolType(); + } - if (expressionType == null) - { - indexers = []; - return false; - } + indexers = semanticModel.LookupSymbols(position, expressionType, WellKnownMemberNames.Indexer) + .OfType() + .ToImmutableArray(); + return true; + } - if (expressionType is IErrorTypeSymbol errorType) - { - // If `expression` is a QualifiedNameSyntax then GetTypeInfo().Type won't have any CandidateSymbols, so - // we should then fall back to getting the actual symbol for the expression. - expressionType = errorType.CandidateSymbols.FirstOrDefault().GetSymbolType() - ?? semanticModel.GetSymbolInfo(expression).GetAnySymbol().GetSymbolType(); - } + private static SignatureHelpItem Convert( + IPropertySymbol indexer, + SyntaxToken openToken, + SemanticModel semanticModel, + IStructuralTypeDisplayService structuralTypeDisplayService, + IDocumentationCommentFormattingService documentationCommentFormattingService) + { + var position = openToken.SpanStart; + var item = CreateItem(indexer, semanticModel, position, + structuralTypeDisplayService, + indexer.IsParams(), + indexer.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormattingService), + GetPreambleParts(indexer, position, semanticModel), + GetSeparatorParts(), + GetPostambleParts(), + indexer.Parameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList()); + return item; + } - indexers = semanticModel.LookupSymbols(position, expressionType, WellKnownMemberNames.Indexer) - .OfType() - .ToImmutableArray(); - return true; - } + private static IList GetPreambleParts( + IPropertySymbol indexer, + int position, + SemanticModel semanticModel) + { + var result = new List(); - private static SignatureHelpItem Convert( - IPropertySymbol indexer, - SyntaxToken openToken, - SemanticModel semanticModel, - IStructuralTypeDisplayService structuralTypeDisplayService, - IDocumentationCommentFormattingService documentationCommentFormattingService) + if (indexer.ReturnsByRef) { - var position = openToken.SpanStart; - var item = CreateItem(indexer, semanticModel, position, - structuralTypeDisplayService, - indexer.IsParams(), - indexer.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormattingService), - GetPreambleParts(indexer, position, semanticModel), - GetSeparatorParts(), - GetPostambleParts(), - indexer.Parameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList()); - return item; + result.Add(Keyword(SyntaxKind.RefKeyword)); + result.Add(Space()); + } + else if (indexer.ReturnsByRefReadonly) + { + result.Add(Keyword(SyntaxKind.RefKeyword)); + result.Add(Space()); + result.Add(Keyword(SyntaxKind.ReadOnlyKeyword)); + result.Add(Space()); } - private static IList GetPreambleParts( - IPropertySymbol indexer, - int position, - SemanticModel semanticModel) + result.AddRange(indexer.Type.ToMinimalDisplayParts(semanticModel, position)); + result.Add(Space()); + result.AddRange(indexer.ContainingType.ToMinimalDisplayParts(semanticModel, position)); + + if (indexer.Name != WellKnownMemberNames.Indexer) { - var result = new List(); + result.Add(Punctuation(SyntaxKind.DotToken)); + result.Add(new SymbolDisplayPart(SymbolDisplayPartKind.PropertyName, indexer, indexer.Name)); + } - if (indexer.ReturnsByRef) - { - result.Add(Keyword(SyntaxKind.RefKeyword)); - result.Add(Space()); - } - else if (indexer.ReturnsByRefReadonly) - { - result.Add(Keyword(SyntaxKind.RefKeyword)); - result.Add(Space()); - result.Add(Keyword(SyntaxKind.ReadOnlyKeyword)); - result.Add(Space()); - } + result.Add(Punctuation(SyntaxKind.OpenBracketToken)); - result.AddRange(indexer.Type.ToMinimalDisplayParts(semanticModel, position)); - result.Add(Space()); - result.AddRange(indexer.ContainingType.ToMinimalDisplayParts(semanticModel, position)); + return result; + } - if (indexer.Name != WellKnownMemberNames.Indexer) - { - result.Add(Punctuation(SyntaxKind.DotToken)); - result.Add(new SymbolDisplayPart(SymbolDisplayPartKind.PropertyName, indexer, indexer.Name)); - } + private static IList GetPostambleParts() + { + return SpecializedCollections.SingletonList( + Punctuation(SyntaxKind.CloseBracketToken)); + } - result.Add(Punctuation(SyntaxKind.OpenBracketToken)); + private static class CompleteElementAccessExpression + { + internal static bool IsTriggerToken(SyntaxToken token) + { + return !token.IsKind(SyntaxKind.None) && + token.ValueText.Length == 1 && + IsTriggerCharacterInternal(token.ValueText[0]) && + token.Parent is BracketedArgumentListSyntax && + token.Parent.Parent is ElementAccessExpressionSyntax; + } - return result; + internal static bool IsArgumentListToken(ElementAccessExpressionSyntax expression, SyntaxToken token) + { + return expression.ArgumentList.Span.Contains(token.SpanStart) && + token != expression.ArgumentList.CloseBracketToken; } - private static IList GetPostambleParts() + internal static TextSpan GetTextSpan(SyntaxToken openBracket) { - return SpecializedCollections.SingletonList( - Punctuation(SyntaxKind.CloseBracketToken)); + Contract.ThrowIfFalse(openBracket.Parent is BracketedArgumentListSyntax && + (openBracket.Parent.Parent is ElementAccessExpressionSyntax || openBracket.Parent.Parent is ElementBindingExpressionSyntax)); + return SignatureHelpUtilities.GetSignatureHelpSpan((BracketedArgumentListSyntax)openBracket.Parent); } - private static class CompleteElementAccessExpression + internal static bool TryGetSyntax(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, [NotNullWhen(true)] out ExpressionSyntax? identifier, out SyntaxToken openBrace) { - internal static bool IsTriggerToken(SyntaxToken token) + if (CommonSignatureHelpUtilities.TryGetSyntax( + root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out ElementAccessExpressionSyntax? elementAccessExpression)) { - return !token.IsKind(SyntaxKind.None) && - token.ValueText.Length == 1 && - IsTriggerCharacterInternal(token.ValueText[0]) && - token.Parent is BracketedArgumentListSyntax && - token.Parent.Parent is ElementAccessExpressionSyntax; + identifier = elementAccessExpression.Expression; + openBrace = elementAccessExpression.ArgumentList.OpenBracketToken; + return true; } - internal static bool IsArgumentListToken(ElementAccessExpressionSyntax expression, SyntaxToken token) - { - return expression.ArgumentList.Span.Contains(token.SpanStart) && - token != expression.ArgumentList.CloseBracketToken; - } + identifier = null; + openBrace = default; + return false; + } + } - internal static TextSpan GetTextSpan(SyntaxToken openBracket) - { - Contract.ThrowIfFalse(openBracket.Parent is BracketedArgumentListSyntax && - (openBracket.Parent.Parent is ElementAccessExpressionSyntax || openBracket.Parent.Parent is ElementBindingExpressionSyntax)); - return SignatureHelpUtilities.GetSignatureHelpSpan((BracketedArgumentListSyntax)openBracket.Parent); - } + /// Error tolerance case for + /// "goo[$$]" or "goo?[$$]" + /// which is parsed as an ArrayTypeSyntax variable declaration instead of an ElementAccessExpression + private static class IncompleteElementAccessExpression + { + internal static bool IsArgumentListToken(ArrayTypeSyntax node, SyntaxToken token) + { + return node.RankSpecifiers.Span.Contains(token.SpanStart) && + token != node.RankSpecifiers.First().CloseBracketToken; + } - internal static bool TryGetSyntax(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, [NotNullWhen(true)] out ExpressionSyntax? identifier, out SyntaxToken openBrace) - { - if (CommonSignatureHelpUtilities.TryGetSyntax( - root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out ElementAccessExpressionSyntax? elementAccessExpression)) - { - identifier = elementAccessExpression.Expression; - openBrace = elementAccessExpression.ArgumentList.OpenBracketToken; - return true; - } - - identifier = null; - openBrace = default; - return false; - } + internal static bool IsTriggerToken(SyntaxToken token) + { + return !token.IsKind(SyntaxKind.None) && + token.ValueText.Length == 1 && + IsTriggerCharacterInternal(token.ValueText[0]) && + token.Parent is ArrayRankSpecifierSyntax; } - /// Error tolerance case for - /// "goo[$$]" or "goo?[$$]" - /// which is parsed as an ArrayTypeSyntax variable declaration instead of an ElementAccessExpression - private static class IncompleteElementAccessExpression + internal static TextSpan GetTextSpan(SyntaxNode expression, SyntaxToken openBracket) { - internal static bool IsArgumentListToken(ArrayTypeSyntax node, SyntaxToken token) - { - return node.RankSpecifiers.Span.Contains(token.SpanStart) && - token != node.RankSpecifiers.First().CloseBracketToken; - } + Contract.ThrowIfFalse(openBracket.Parent is ArrayRankSpecifierSyntax && openBracket.Parent.Parent is ArrayTypeSyntax); + return TextSpan.FromBounds(expression.SpanStart, openBracket.Parent.Span.End); + } - internal static bool IsTriggerToken(SyntaxToken token) + internal static bool TryGetSyntax(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, [NotNullWhen(true)] out ExpressionSyntax? identifier, out SyntaxToken openBrace) + { + if (CommonSignatureHelpUtilities.TryGetSyntax( + root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out ArrayTypeSyntax? arrayTypeSyntax)) { - return !token.IsKind(SyntaxKind.None) && - token.ValueText.Length == 1 && - IsTriggerCharacterInternal(token.ValueText[0]) && - token.Parent is ArrayRankSpecifierSyntax; + identifier = arrayTypeSyntax.ElementType; + openBrace = arrayTypeSyntax.RankSpecifiers.First().OpenBracketToken; + return true; } - internal static TextSpan GetTextSpan(SyntaxNode expression, SyntaxToken openBracket) - { - Contract.ThrowIfFalse(openBracket.Parent is ArrayRankSpecifierSyntax && openBracket.Parent.Parent is ArrayTypeSyntax); - return TextSpan.FromBounds(expression.SpanStart, openBracket.Parent.Span.End); - } + identifier = null; + openBrace = default; + return false; + } + } - internal static bool TryGetSyntax(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, [NotNullWhen(true)] out ExpressionSyntax? identifier, out SyntaxToken openBrace) - { - if (CommonSignatureHelpUtilities.TryGetSyntax( - root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out ArrayTypeSyntax? arrayTypeSyntax)) - { - identifier = arrayTypeSyntax.ElementType; - openBrace = arrayTypeSyntax.RankSpecifiers.First().OpenBracketToken; - return true; - } - - identifier = null; - openBrace = default; - return false; - } + /// Error tolerance case for + /// "new String()?[$$]" + /// which is parsed as a BracketedArgumentListSyntax parented by an ElementBindingExpressionSyntax parented by a ConditionalAccessExpressionSyntax + private static class ConditionalAccessExpression + { + internal static bool IsTriggerToken(SyntaxToken token) + { + return !token.IsKind(SyntaxKind.None) && + token.ValueText.Length == 1 && + IsTriggerCharacterInternal(token.ValueText[0]) && + token.Parent is BracketedArgumentListSyntax && + token.Parent.Parent is ElementBindingExpressionSyntax && + token.Parent.Parent.Parent is ConditionalAccessExpressionSyntax; } - /// Error tolerance case for - /// "new String()?[$$]" - /// which is parsed as a BracketedArgumentListSyntax parented by an ElementBindingExpressionSyntax parented by a ConditionalAccessExpressionSyntax - private static class ConditionalAccessExpression + internal static bool IsArgumentListToken(ElementBindingExpressionSyntax expression, SyntaxToken token) { - internal static bool IsTriggerToken(SyntaxToken token) - { - return !token.IsKind(SyntaxKind.None) && - token.ValueText.Length == 1 && - IsTriggerCharacterInternal(token.ValueText[0]) && - token.Parent is BracketedArgumentListSyntax && - token.Parent.Parent is ElementBindingExpressionSyntax && - token.Parent.Parent.Parent is ConditionalAccessExpressionSyntax; - } + return expression.ArgumentList.Span.Contains(token.SpanStart) && + token != expression.ArgumentList.CloseBracketToken; + } - internal static bool IsArgumentListToken(ElementBindingExpressionSyntax expression, SyntaxToken token) + internal static bool TryGetSyntax(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, [NotNullWhen(true)] out ExpressionSyntax? identifier, out SyntaxToken openBrace) + { + if (CommonSignatureHelpUtilities.TryGetSyntax( + root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out ElementBindingExpressionSyntax? elementBindingExpression)) { - return expression.ArgumentList.Span.Contains(token.SpanStart) && - token != expression.ArgumentList.CloseBracketToken; - } + // Find the first conditional access expression that starts left of our open bracket + var conditionalAccess = elementBindingExpression.FirstAncestorOrSelf( + (c, elementBindingExpression) => c.SpanStart < elementBindingExpression.SpanStart, elementBindingExpression)!; - internal static bool TryGetSyntax(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, [NotNullWhen(true)] out ExpressionSyntax? identifier, out SyntaxToken openBrace) - { - if (CommonSignatureHelpUtilities.TryGetSyntax( - root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out ElementBindingExpressionSyntax? elementBindingExpression)) - { - // Find the first conditional access expression that starts left of our open bracket - var conditionalAccess = elementBindingExpression.FirstAncestorOrSelf( - (c, elementBindingExpression) => c.SpanStart < elementBindingExpression.SpanStart, elementBindingExpression)!; - - identifier = conditionalAccess.Expression; - openBrace = elementBindingExpression.ArgumentList.OpenBracketToken; - - return true; - } - - identifier = null; - openBrace = default; - return false; + identifier = conditionalAccess.Expression; + openBrace = elementBindingExpression.ArgumentList.OpenBracketToken; + + return true; } + + identifier = null; + openBrace = default; + return false; } } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/GenericNamePartiallyWrittenSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/GenericNamePartiallyWrittenSignatureHelpProvider.cs index 2ed09039c368f..c9dea3cc7a383 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/GenericNamePartiallyWrittenSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/GenericNamePartiallyWrittenSignatureHelpProvider.cs @@ -12,26 +12,25 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +[ExportSignatureHelpProvider("GenericNamePartiallyWrittenSignatureHelpProvider", LanguageNames.CSharp), Shared] +internal class GenericNamePartiallyWrittenSignatureHelpProvider : GenericNameSignatureHelpProvider { - [ExportSignatureHelpProvider("GenericNamePartiallyWrittenSignatureHelpProvider", LanguageNames.CSharp), Shared] - internal class GenericNamePartiallyWrittenSignatureHelpProvider : GenericNameSignatureHelpProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public GenericNamePartiallyWrittenSignatureHelpProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public GenericNamePartiallyWrittenSignatureHelpProvider() - { - } + } - protected override bool TryGetGenericIdentifier(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, out SyntaxToken genericIdentifier, out SyntaxToken lessThanToken) - => root.SyntaxTree.IsInPartiallyWrittenGeneric(position, cancellationToken, out genericIdentifier, out lessThanToken); + protected override bool TryGetGenericIdentifier(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, out SyntaxToken genericIdentifier, out SyntaxToken lessThanToken) + => root.SyntaxTree.IsInPartiallyWrittenGeneric(position, cancellationToken, out genericIdentifier, out lessThanToken); - protected override TextSpan GetTextSpan(SyntaxToken genericIdentifier, SyntaxToken lessThanToken) - { - var lastToken = genericIdentifier.FindLastTokenOfPartialGenericName(); - var nextToken = lastToken.GetNextNonZeroWidthTokenOrEndOfFile(); - Contract.ThrowIfTrue(nextToken.Kind() == 0); - return TextSpan.FromBounds(genericIdentifier.SpanStart, nextToken.SpanStart); - } + protected override TextSpan GetTextSpan(SyntaxToken genericIdentifier, SyntaxToken lessThanToken) + { + var lastToken = genericIdentifier.FindLastTokenOfPartialGenericName(); + var nextToken = lastToken.GetNextNonZeroWidthTokenOrEndOfFile(); + Contract.ThrowIfTrue(nextToken.Kind() == 0); + return TextSpan.FromBounds(genericIdentifier.SpanStart, nextToken.SpanStart); } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider.cs index 0b31915d3bc10..262c0ecb92165 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider.cs @@ -20,280 +20,279 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +[ExportSignatureHelpProvider("GenericNameSignatureHelpProvider", LanguageNames.CSharp), Shared] +internal partial class GenericNameSignatureHelpProvider : AbstractCSharpSignatureHelpProvider { - [ExportSignatureHelpProvider("GenericNameSignatureHelpProvider", LanguageNames.CSharp), Shared] - internal partial class GenericNameSignatureHelpProvider : AbstractCSharpSignatureHelpProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public GenericNameSignatureHelpProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public GenericNameSignatureHelpProvider() - { - } + } - public override bool IsTriggerCharacter(char ch) - => ch is '<' or ','; + public override bool IsTriggerCharacter(char ch) + => ch is '<' or ','; - public override bool IsRetriggerCharacter(char ch) - => ch == '>'; + public override bool IsRetriggerCharacter(char ch) + => ch == '>'; - protected virtual bool TryGetGenericIdentifier( - SyntaxNode root, int position, - ISyntaxFactsService syntaxFacts, - SignatureHelpTriggerReason triggerReason, - CancellationToken cancellationToken, - out SyntaxToken genericIdentifier, - out SyntaxToken lessThanToken) + protected virtual bool TryGetGenericIdentifier( + SyntaxNode root, int position, + ISyntaxFactsService syntaxFacts, + SignatureHelpTriggerReason triggerReason, + CancellationToken cancellationToken, + out SyntaxToken genericIdentifier, + out SyntaxToken lessThanToken) + { + if (CommonSignatureHelpUtilities.TryGetSyntax( + root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out GenericNameSyntax? name)) { - if (CommonSignatureHelpUtilities.TryGetSyntax( - root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out GenericNameSyntax? name)) - { - genericIdentifier = name.Identifier; - lessThanToken = name.TypeArgumentList.LessThanToken; - return true; - } - - genericIdentifier = default; - lessThanToken = default; - return false; + genericIdentifier = name.Identifier; + lessThanToken = name.TypeArgumentList.LessThanToken; + return true; } - private bool IsTriggerToken(SyntaxToken token) - { - return !token.IsKind(SyntaxKind.None) && - token.ValueText.Length == 1 && - IsTriggerCharacter(token.ValueText[0]) && - token.Parent is TypeArgumentListSyntax && - token.Parent.Parent is GenericNameSyntax; - } + genericIdentifier = default; + lessThanToken = default; + return false; + } - private bool IsArgumentListToken(GenericNameSyntax node, SyntaxToken token) + private bool IsTriggerToken(SyntaxToken token) + { + return !token.IsKind(SyntaxKind.None) && + token.ValueText.Length == 1 && + IsTriggerCharacter(token.ValueText[0]) && + token.Parent is TypeArgumentListSyntax && + token.Parent.Parent is GenericNameSyntax; + } + + private bool IsArgumentListToken(GenericNameSyntax node, SyntaxToken token) + { + return node.TypeArgumentList != null && + node.TypeArgumentList.Span.Contains(token.SpanStart) && + token != node.TypeArgumentList.GreaterThanToken; + } + + protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (!TryGetGenericIdentifier(root, position, document.GetRequiredLanguageService(), triggerInfo.TriggerReason, cancellationToken, + out var genericIdentifier, out var lessThanToken)) { - return node.TypeArgumentList != null && - node.TypeArgumentList.Span.Contains(token.SpanStart) && - token != node.TypeArgumentList.GreaterThanToken; + return null; } - protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) + if (genericIdentifier.Parent is not SimpleNameSyntax simpleName) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (!TryGetGenericIdentifier(root, position, document.GetRequiredLanguageService(), triggerInfo.TriggerReason, cancellationToken, - out var genericIdentifier, out var lessThanToken)) - { - return null; - } + return null; + } - if (genericIdentifier.Parent is not SimpleNameSyntax simpleName) - { - return null; - } + var beforeDotExpression = simpleName.IsRightSideOfDot() ? simpleName.GetLeftSideOfDot() : null; - var beforeDotExpression = simpleName.IsRightSideOfDot() ? simpleName.GetLeftSideOfDot() : null; + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(simpleName, cancellationToken).ConfigureAwait(false); - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(simpleName, cancellationToken).ConfigureAwait(false); + var leftSymbol = beforeDotExpression == null + ? null + : semanticModel.GetSymbolInfo(beforeDotExpression, cancellationToken).GetAnySymbol() as INamespaceOrTypeSymbol; + var leftType = beforeDotExpression == null + ? null + : semanticModel.GetTypeInfo(beforeDotExpression, cancellationToken).Type as INamespaceOrTypeSymbol; - var leftSymbol = beforeDotExpression == null - ? null - : semanticModel.GetSymbolInfo(beforeDotExpression, cancellationToken).GetAnySymbol() as INamespaceOrTypeSymbol; - var leftType = beforeDotExpression == null - ? null - : semanticModel.GetTypeInfo(beforeDotExpression, cancellationToken).Type as INamespaceOrTypeSymbol; + var leftContainer = leftSymbol ?? leftType; - var leftContainer = leftSymbol ?? leftType; + var isBaseAccess = beforeDotExpression is BaseExpressionSyntax; + var namespacesOrTypesOnly = SyntaxFacts.IsInNamespaceOrTypeContext(simpleName); + var includeExtensions = leftSymbol == null && leftType != null; + var name = genericIdentifier.ValueText; + var symbols = isBaseAccess + ? semanticModel.LookupBaseMembers(position, name) + : namespacesOrTypesOnly + ? semanticModel.LookupNamespacesAndTypes(position, leftContainer, name) + : semanticModel.LookupSymbols(position, leftContainer, name, includeExtensions); - var isBaseAccess = beforeDotExpression is BaseExpressionSyntax; - var namespacesOrTypesOnly = SyntaxFacts.IsInNamespaceOrTypeContext(simpleName); - var includeExtensions = leftSymbol == null && leftType != null; - var name = genericIdentifier.ValueText; - var symbols = isBaseAccess - ? semanticModel.LookupBaseMembers(position, name) - : namespacesOrTypesOnly - ? semanticModel.LookupNamespacesAndTypes(position, leftContainer, name) - : semanticModel.LookupSymbols(position, leftContainer, name, includeExtensions); + var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); + if (within == null) + { + return null; + } - var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); - if (within == null) - { - return null; - } + var accessibleSymbols = + symbols.WhereAsArray(s => s.GetArity() > 0) + .WhereAsArray(s => s is INamedTypeSymbol or IMethodSymbol) + .FilterToVisibleAndBrowsableSymbols(options.HideAdvancedMembers, semanticModel.Compilation) + .Sort(semanticModel, genericIdentifier.SpanStart); - var accessibleSymbols = - symbols.WhereAsArray(s => s.GetArity() > 0) - .WhereAsArray(s => s is INamedTypeSymbol or IMethodSymbol) - .FilterToVisibleAndBrowsableSymbols(options.HideAdvancedMembers, semanticModel.Compilation) - .Sort(semanticModel, genericIdentifier.SpanStart); + if (!accessibleSymbols.Any()) + { + return null; + } - if (!accessibleSymbols.Any()) - { - return null; - } + var structuralTypeDisplayService = document.GetRequiredLanguageService(); + var documentationCommentFormattingService = document.GetRequiredLanguageService(); + var textSpan = GetTextSpan(genericIdentifier, lessThanToken); + var syntaxFacts = document.GetRequiredLanguageService(); - var structuralTypeDisplayService = document.GetRequiredLanguageService(); - var documentationCommentFormattingService = document.GetRequiredLanguageService(); - var textSpan = GetTextSpan(genericIdentifier, lessThanToken); - var syntaxFacts = document.GetRequiredLanguageService(); + return CreateSignatureHelpItems(accessibleSymbols.Select(s => + Convert(s, lessThanToken, semanticModel, structuralTypeDisplayService, documentationCommentFormattingService)).ToList(), + textSpan, GetCurrentArgumentState(root, position, syntaxFacts, cancellationToken), selectedItemIndex: null, parameterIndexOverride: -1); + } - return CreateSignatureHelpItems(accessibleSymbols.Select(s => - Convert(s, lessThanToken, semanticModel, structuralTypeDisplayService, documentationCommentFormattingService)).ToList(), - textSpan, GetCurrentArgumentState(root, position, syntaxFacts, cancellationToken), selectedItemIndex: null, parameterIndexOverride: -1); + private SignatureHelpState? GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken) + { + if (!TryGetGenericIdentifier(root, position, syntaxFacts, SignatureHelpTriggerReason.InvokeSignatureHelpCommand, cancellationToken, + out var genericIdentifier, out _)) + { + return null; } - private SignatureHelpState? GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken) + if (genericIdentifier.TryParseGenericName(cancellationToken, out var genericName)) { - if (!TryGetGenericIdentifier(root, position, syntaxFacts, SignatureHelpTriggerReason.InvokeSignatureHelpCommand, cancellationToken, - out var genericIdentifier, out _)) - { - return null; - } + // Because we synthesized the generic name, it will have an index starting at 0 + // instead of at the actual position it's at in the text. Because of this, we need to + // offset the position we are checking accordingly. + var offset = genericIdentifier.SpanStart - genericName.SpanStart; + position -= offset; + return SignatureHelpUtilities.GetSignatureHelpState(genericName.TypeArgumentList, position); + } - if (genericIdentifier.TryParseGenericName(cancellationToken, out var genericName)) - { - // Because we synthesized the generic name, it will have an index starting at 0 - // instead of at the actual position it's at in the text. Because of this, we need to - // offset the position we are checking accordingly. - var offset = genericIdentifier.SpanStart - genericName.SpanStart; - position -= offset; - return SignatureHelpUtilities.GetSignatureHelpState(genericName.TypeArgumentList, position); - } + return null; + } - return null; - } + protected virtual TextSpan GetTextSpan(SyntaxToken genericIdentifier, SyntaxToken lessThanToken) + { + Contract.ThrowIfFalse(lessThanToken.Parent is TypeArgumentListSyntax && lessThanToken.Parent.Parent is GenericNameSyntax); + return SignatureHelpUtilities.GetSignatureHelpSpan(((GenericNameSyntax)lessThanToken.Parent.Parent).TypeArgumentList); + } - protected virtual TextSpan GetTextSpan(SyntaxToken genericIdentifier, SyntaxToken lessThanToken) + private static SignatureHelpItem Convert( + ISymbol symbol, + SyntaxToken lessThanToken, + SemanticModel semanticModel, + IStructuralTypeDisplayService structuralTypeDisplayService, + IDocumentationCommentFormattingService documentationCommentFormattingService) + { + var position = lessThanToken.SpanStart; + + SignatureHelpItem item; + if (symbol is INamedTypeSymbol namedType) { - Contract.ThrowIfFalse(lessThanToken.Parent is TypeArgumentListSyntax && lessThanToken.Parent.Parent is GenericNameSyntax); - return SignatureHelpUtilities.GetSignatureHelpSpan(((GenericNameSyntax)lessThanToken.Parent.Parent).TypeArgumentList); + item = CreateItem( + symbol, semanticModel, position, + structuralTypeDisplayService, + false, + symbol.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormattingService), + GetPreambleParts(namedType, semanticModel, position), + GetSeparatorParts(), + GetPostambleParts(), + namedType.TypeParameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList()); } - - private static SignatureHelpItem Convert( - ISymbol symbol, - SyntaxToken lessThanToken, - SemanticModel semanticModel, - IStructuralTypeDisplayService structuralTypeDisplayService, - IDocumentationCommentFormattingService documentationCommentFormattingService) + else { - var position = lessThanToken.SpanStart; + var method = (IMethodSymbol)symbol; + item = CreateItem( + symbol, semanticModel, position, + structuralTypeDisplayService, + false, + c => symbol.GetDocumentationParts(semanticModel, position, documentationCommentFormattingService, c), + GetPreambleParts(method, semanticModel, position), + GetSeparatorParts(), + GetPostambleParts(method, semanticModel, position), + method.TypeParameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList()); + } - SignatureHelpItem item; - if (symbol is INamedTypeSymbol namedType) - { - item = CreateItem( - symbol, semanticModel, position, - structuralTypeDisplayService, - false, - symbol.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormattingService), - GetPreambleParts(namedType, semanticModel, position), - GetSeparatorParts(), - GetPostambleParts(), - namedType.TypeParameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList()); - } - else - { - var method = (IMethodSymbol)symbol; - item = CreateItem( - symbol, semanticModel, position, - structuralTypeDisplayService, - false, - c => symbol.GetDocumentationParts(semanticModel, position, documentationCommentFormattingService, c), - GetPreambleParts(method, semanticModel, position), - GetSeparatorParts(), - GetPostambleParts(method, semanticModel, position), - method.TypeParameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList()); - } + return item; + } - return item; - } + private static readonly SymbolDisplayFormat s_minimallyQualifiedFormat = + SymbolDisplayFormat.MinimallyQualifiedFormat.WithGenericsOptions( + SymbolDisplayFormat.MinimallyQualifiedFormat.GenericsOptions | SymbolDisplayGenericsOptions.IncludeVariance); - private static readonly SymbolDisplayFormat s_minimallyQualifiedFormat = - SymbolDisplayFormat.MinimallyQualifiedFormat.WithGenericsOptions( - SymbolDisplayFormat.MinimallyQualifiedFormat.GenericsOptions | SymbolDisplayGenericsOptions.IncludeVariance); + private static SignatureHelpSymbolParameter Convert( + ITypeParameterSymbol parameter, + SemanticModel semanticModel, + int position, + IDocumentationCommentFormattingService formatter) + { + return new SignatureHelpSymbolParameter( + parameter.Name, + isOptional: false, + documentationFactory: parameter.GetDocumentationPartsFactory(semanticModel, position, formatter), + displayParts: parameter.ToMinimalDisplayParts(semanticModel, position, s_minimallyQualifiedFormat), + selectedDisplayParts: GetSelectedDisplayParts(parameter, semanticModel, position)); + } - private static SignatureHelpSymbolParameter Convert( - ITypeParameterSymbol parameter, - SemanticModel semanticModel, - int position, - IDocumentationCommentFormattingService formatter) - { - return new SignatureHelpSymbolParameter( - parameter.Name, - isOptional: false, - documentationFactory: parameter.GetDocumentationPartsFactory(semanticModel, position, formatter), - displayParts: parameter.ToMinimalDisplayParts(semanticModel, position, s_minimallyQualifiedFormat), - selectedDisplayParts: GetSelectedDisplayParts(parameter, semanticModel, position)); - } + private static IList GetSelectedDisplayParts( + ITypeParameterSymbol typeParam, + SemanticModel semanticModel, + int position) + { + var parts = new List(); - private static IList GetSelectedDisplayParts( - ITypeParameterSymbol typeParam, - SemanticModel semanticModel, - int position) + if (TypeParameterHasConstraints(typeParam)) { - var parts = new List(); + parts.Add(Space()); + parts.Add(Keyword(SyntaxKind.WhereKeyword)); + parts.Add(Space()); - if (TypeParameterHasConstraints(typeParam)) - { - parts.Add(Space()); - parts.Add(Keyword(SyntaxKind.WhereKeyword)); - parts.Add(Space()); + parts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.TypeParameterName, typeParam, typeParam.Name)); - parts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.TypeParameterName, typeParam, typeParam.Name)); + parts.Add(Space()); + parts.Add(Punctuation(SyntaxKind.ColonToken)); + parts.Add(Space()); - parts.Add(Space()); - parts.Add(Punctuation(SyntaxKind.ColonToken)); - parts.Add(Space()); + var needComma = false; - var needComma = false; + // class/struct constraint must be first + if (typeParam.HasReferenceTypeConstraint) + { + parts.Add(Keyword(SyntaxKind.ClassKeyword)); + needComma = true; + } + else if (typeParam.HasUnmanagedTypeConstraint) + { + parts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Keyword, null, "unmanaged")); + needComma = true; + } + else if (typeParam.HasValueTypeConstraint) + { + parts.Add(Keyword(SyntaxKind.StructKeyword)); + needComma = true; + } - // class/struct constraint must be first - if (typeParam.HasReferenceTypeConstraint) - { - parts.Add(Keyword(SyntaxKind.ClassKeyword)); - needComma = true; - } - else if (typeParam.HasUnmanagedTypeConstraint) - { - parts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Keyword, null, "unmanaged")); - needComma = true; - } - else if (typeParam.HasValueTypeConstraint) + foreach (var baseType in typeParam.ConstraintTypes) + { + if (needComma) { - parts.Add(Keyword(SyntaxKind.StructKeyword)); - needComma = true; + parts.Add(Punctuation(SyntaxKind.CommaToken)); + parts.Add(Space()); } - foreach (var baseType in typeParam.ConstraintTypes) - { - if (needComma) - { - parts.Add(Punctuation(SyntaxKind.CommaToken)); - parts.Add(Space()); - } - - parts.AddRange(baseType.ToMinimalDisplayParts(semanticModel, position)); - needComma = true; - } + parts.AddRange(baseType.ToMinimalDisplayParts(semanticModel, position)); + needComma = true; + } - // ctor constraint must be last - if (typeParam.HasConstructorConstraint) + // ctor constraint must be last + if (typeParam.HasConstructorConstraint) + { + if (needComma) { - if (needComma) - { - parts.Add(Punctuation(SyntaxKind.CommaToken)); - parts.Add(Space()); - } - - parts.Add(Keyword(SyntaxKind.NewKeyword)); - parts.Add(Punctuation(SyntaxKind.OpenParenToken)); - parts.Add(Punctuation(SyntaxKind.CloseParenToken)); + parts.Add(Punctuation(SyntaxKind.CommaToken)); + parts.Add(Space()); } - } - return parts; + parts.Add(Keyword(SyntaxKind.NewKeyword)); + parts.Add(Punctuation(SyntaxKind.OpenParenToken)); + parts.Add(Punctuation(SyntaxKind.CloseParenToken)); + } } - private static bool TypeParameterHasConstraints(ITypeParameterSymbol typeParam) - { - return !typeParam.ConstraintTypes.IsDefaultOrEmpty || typeParam.HasConstructorConstraint || - typeParam.HasReferenceTypeConstraint || typeParam.HasValueTypeConstraint; - } + return parts; + } + + private static bool TypeParameterHasConstraints(ITypeParameterSymbol typeParam) + { + return !typeParam.ConstraintTypes.IsDefaultOrEmpty || typeParam.HasConstructorConstraint || + typeParam.HasReferenceTypeConstraint || typeParam.HasValueTypeConstraint; } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider_Method.cs b/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider_Method.cs index a1ed06c35b714..c1072cfa05d42 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider_Method.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider_Method.cs @@ -5,95 +5,94 @@ using System.Collections.Generic; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +internal partial class GenericNameSignatureHelpProvider { - internal partial class GenericNameSignatureHelpProvider + private static IList GetPreambleParts( + IMethodSymbol method, + SemanticModel semanticModel, + int position) { - private static IList GetPreambleParts( - IMethodSymbol method, - SemanticModel semanticModel, - int position) - { - var result = new List(); - - var awaitable = method.GetOriginalUnreducedDefinition().IsAwaitableNonDynamic(semanticModel, position); - var extension = method.GetOriginalUnreducedDefinition().IsExtensionMethod(); + var result = new List(); - if (awaitable && extension) - { - result.Add(Punctuation(SyntaxKind.OpenParenToken)); - result.Add(Text(CSharpFeaturesResources.awaitable)); - result.Add(Punctuation(SyntaxKind.CommaToken)); - result.Add(Text(CSharpFeaturesResources.extension)); - result.Add(Punctuation(SyntaxKind.CloseParenToken)); - result.Add(Space()); - } - else if (awaitable) - { - result.Add(Punctuation(SyntaxKind.OpenParenToken)); - result.Add(Text(CSharpFeaturesResources.awaitable)); - result.Add(Punctuation(SyntaxKind.CloseParenToken)); - result.Add(Space()); - } - else if (extension) - { - result.Add(Punctuation(SyntaxKind.OpenParenToken)); - result.Add(Text(CSharpFeaturesResources.extension)); - result.Add(Punctuation(SyntaxKind.CloseParenToken)); - result.Add(Space()); - } + var awaitable = method.GetOriginalUnreducedDefinition().IsAwaitableNonDynamic(semanticModel, position); + var extension = method.GetOriginalUnreducedDefinition().IsExtensionMethod(); - result.AddRange(method.ReturnType.ToMinimalDisplayParts(semanticModel, position)); + if (awaitable && extension) + { + result.Add(Punctuation(SyntaxKind.OpenParenToken)); + result.Add(Text(CSharpFeaturesResources.awaitable)); + result.Add(Punctuation(SyntaxKind.CommaToken)); + result.Add(Text(CSharpFeaturesResources.extension)); + result.Add(Punctuation(SyntaxKind.CloseParenToken)); result.Add(Space()); - var containingType = GetContainingType(method); - if (containingType != null) - { - result.AddRange(containingType.ToMinimalDisplayParts(semanticModel, position)); - result.Add(Punctuation(SyntaxKind.DotToken)); - } + } + else if (awaitable) + { + result.Add(Punctuation(SyntaxKind.OpenParenToken)); + result.Add(Text(CSharpFeaturesResources.awaitable)); + result.Add(Punctuation(SyntaxKind.CloseParenToken)); + result.Add(Space()); + } + else if (extension) + { + result.Add(Punctuation(SyntaxKind.OpenParenToken)); + result.Add(Text(CSharpFeaturesResources.extension)); + result.Add(Punctuation(SyntaxKind.CloseParenToken)); + result.Add(Space()); + } + + result.AddRange(method.ReturnType.ToMinimalDisplayParts(semanticModel, position)); + result.Add(Space()); + var containingType = GetContainingType(method); + if (containingType != null) + { + result.AddRange(containingType.ToMinimalDisplayParts(semanticModel, position)); + result.Add(Punctuation(SyntaxKind.DotToken)); + } + + result.Add(new SymbolDisplayPart(SymbolDisplayPartKind.MethodName, method, method.Name)); + result.Add(Punctuation(SyntaxKind.LessThanToken)); - result.Add(new SymbolDisplayPart(SymbolDisplayPartKind.MethodName, method, method.Name)); - result.Add(Punctuation(SyntaxKind.LessThanToken)); + return result; + } + private static ITypeSymbol? GetContainingType(IMethodSymbol method) + { + var result = method.ReceiverType; + if (result is not INamedTypeSymbol namedTypeSymbol || !namedTypeSymbol.IsScriptClass) + { return result; } - - private static ITypeSymbol? GetContainingType(IMethodSymbol method) + else { - var result = method.ReceiverType; - if (result is not INamedTypeSymbol namedTypeSymbol || !namedTypeSymbol.IsScriptClass) - { - return result; - } - else - { - return null; - } + return null; } + } - private static IList GetPostambleParts(IMethodSymbol method, SemanticModel semanticModel, int position) + private static IList GetPostambleParts(IMethodSymbol method, SemanticModel semanticModel, int position) + { + var result = new List { - var result = new List - { - Punctuation(SyntaxKind.GreaterThanToken), - Punctuation(SyntaxKind.OpenParenToken) - }; + Punctuation(SyntaxKind.GreaterThanToken), + Punctuation(SyntaxKind.OpenParenToken) + }; - var first = true; - foreach (var parameter in method.Parameters) + var first = true; + foreach (var parameter in method.Parameters) + { + if (!first) { - if (!first) - { - result.Add(Punctuation(SyntaxKind.CommaToken)); - result.Add(Space()); - } - - first = false; - result.AddRange(parameter.ToMinimalDisplayParts(semanticModel, position)); + result.Add(Punctuation(SyntaxKind.CommaToken)); + result.Add(Space()); } - result.Add(Punctuation(SyntaxKind.CloseParenToken)); - return result; + first = false; + result.AddRange(parameter.ToMinimalDisplayParts(semanticModel, position)); } + + result.Add(Punctuation(SyntaxKind.CloseParenToken)); + return result; } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider_NamedType.cs b/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider_NamedType.cs index 5ae9b88221f81..95db63b5b30c9 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider_NamedType.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/GenericNameSignatureHelpProvider_NamedType.cs @@ -5,27 +5,26 @@ using System.Collections.Generic; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +internal partial class GenericNameSignatureHelpProvider { - internal partial class GenericNameSignatureHelpProvider + private static IList GetPreambleParts( + INamedTypeSymbol namedType, + SemanticModel semanticModel, + int position) { - private static IList GetPreambleParts( - INamedTypeSymbol namedType, - SemanticModel semanticModel, - int position) - { - var result = new List(); + var result = new List(); - result.AddRange(namedType.ToMinimalDisplayParts(semanticModel, position, MinimallyQualifiedWithoutTypeParametersFormat)); - result.Add(Punctuation(SyntaxKind.LessThanToken)); + result.AddRange(namedType.ToMinimalDisplayParts(semanticModel, position, MinimallyQualifiedWithoutTypeParametersFormat)); + result.Add(Punctuation(SyntaxKind.LessThanToken)); - return result; - } + return result; + } - private static IList GetPostambleParts() - { - return SpecializedCollections.SingletonList( - Punctuation(SyntaxKind.GreaterThanToken)); - } + private static IList GetPostambleParts() + { + return SpecializedCollections.SingletonList( + Punctuation(SyntaxKind.GreaterThanToken)); } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/InitializerExpressionSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/InitializerExpressionSignatureHelpProvider.cs index 446d8c33a9ed8..f2ca003cd62cd 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/InitializerExpressionSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/InitializerExpressionSignatureHelpProvider.cs @@ -15,81 +15,80 @@ using Microsoft.CodeAnalysis.SignatureHelp; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +[ExportSignatureHelpProvider(nameof(InitializerExpressionSignatureHelpProvider), LanguageNames.CSharp), Shared] +internal partial class InitializerExpressionSignatureHelpProvider : AbstractOrdinaryMethodSignatureHelpProvider { - [ExportSignatureHelpProvider(nameof(InitializerExpressionSignatureHelpProvider), LanguageNames.CSharp), Shared] - internal partial class InitializerExpressionSignatureHelpProvider : AbstractOrdinaryMethodSignatureHelpProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public InitializerExpressionSignatureHelpProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public InitializerExpressionSignatureHelpProvider() - { - } + } - public override bool IsTriggerCharacter(char ch) - => ch is '{' or ','; + public override bool IsTriggerCharacter(char ch) + => ch is '{' or ','; - public override bool IsRetriggerCharacter(char ch) - => ch == '}'; + public override bool IsRetriggerCharacter(char ch) + => ch == '}'; - private bool TryGetInitializerExpression( - SyntaxNode root, - int position, - ISyntaxFactsService syntaxFacts, - SignatureHelpTriggerReason triggerReason, - CancellationToken cancellationToken, - [NotNullWhen(true)] out InitializerExpressionSyntax? expression) - { - return CommonSignatureHelpUtilities.TryGetSyntax(root, position, syntaxFacts, triggerReason, IsTriggerToken, IsInitializerExpressionToken, cancellationToken, out expression) && - expression != null; - } + private bool TryGetInitializerExpression( + SyntaxNode root, + int position, + ISyntaxFactsService syntaxFacts, + SignatureHelpTriggerReason triggerReason, + CancellationToken cancellationToken, + [NotNullWhen(true)] out InitializerExpressionSyntax? expression) + { + return CommonSignatureHelpUtilities.TryGetSyntax(root, position, syntaxFacts, triggerReason, IsTriggerToken, IsInitializerExpressionToken, cancellationToken, out expression) && + expression != null; + } - private bool IsTriggerToken(SyntaxToken token) - => !token.IsKind(SyntaxKind.None) && - token.ValueText.Length == 1 && - IsTriggerCharacter(token.ValueText[0]) && - token.Parent is InitializerExpressionSyntax; + private bool IsTriggerToken(SyntaxToken token) + => !token.IsKind(SyntaxKind.None) && + token.ValueText.Length == 1 && + IsTriggerCharacter(token.ValueText[0]) && + token.Parent is InitializerExpressionSyntax; - private static bool IsInitializerExpressionToken(InitializerExpressionSyntax expression, SyntaxToken token) - => expression.Span.Contains(token.SpanStart) && token != expression.CloseBraceToken; + private static bool IsInitializerExpressionToken(InitializerExpressionSyntax expression, SyntaxToken token) + => expression.Span.Contains(token.SpanStart) && token != expression.CloseBraceToken; - protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (!TryGetInitializerExpression(root, position, document.GetRequiredLanguageService(), triggerInfo.TriggerReason, cancellationToken, out var initializerExpression)) - return null; + protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (!TryGetInitializerExpression(root, position, document.GetRequiredLanguageService(), triggerInfo.TriggerReason, cancellationToken, out var initializerExpression)) + return null; - var addMethods = await CommonSignatureHelpUtilities.GetCollectionInitializerAddMethodsAsync( - document, initializerExpression, options, cancellationToken).ConfigureAwait(false); - if (addMethods.IsDefaultOrEmpty) - { - return null; - } + var addMethods = await CommonSignatureHelpUtilities.GetCollectionInitializerAddMethodsAsync( + document, initializerExpression, options, cancellationToken).ConfigureAwait(false); + if (addMethods.IsDefaultOrEmpty) + { + return null; + } - var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(initializerExpression); - var syntaxFacts = document.GetRequiredLanguageService(); + var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(initializerExpression); + var syntaxFacts = document.GetRequiredLanguageService(); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - return CreateCollectionInitializerSignatureHelpItems(addMethods.Select(s => - ConvertMethodGroupMethod(document, s, initializerExpression.OpenBraceToken.SpanStart, semanticModel)).ToList(), - textSpan, GetCurrentArgumentState(root, position, syntaxFacts, textSpan, cancellationToken)); - } + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + return CreateCollectionInitializerSignatureHelpItems(addMethods.Select(s => + ConvertMethodGroupMethod(document, s, initializerExpression.OpenBraceToken.SpanStart, semanticModel)).ToList(), + textSpan, GetCurrentArgumentState(root, position, syntaxFacts, textSpan, cancellationToken)); + } - private SignatureHelpState? GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken) + private SignatureHelpState? GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken) + { + if (TryGetInitializerExpression( + root, + position, + syntaxFacts, + SignatureHelpTriggerReason.InvokeSignatureHelpCommand, + cancellationToken, + out var expression) && + currentSpan.Start == SignatureHelpUtilities.GetSignatureHelpSpan(expression).Start) { - if (TryGetInitializerExpression( - root, - position, - syntaxFacts, - SignatureHelpTriggerReason.InvokeSignatureHelpCommand, - cancellationToken, - out var expression) && - currentSpan.Start == SignatureHelpUtilities.GetSignatureHelpSpan(expression).Start) - { - return SignatureHelpUtilities.GetSignatureHelpState(expression, position); - } - - return null; + return SignatureHelpUtilities.GetSignatureHelpState(expression, position); } + + return null; } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProvider.cs index b292b58a5f0fa..a19398ff193ae 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProvider.cs @@ -16,168 +16,167 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +[ExportSignatureHelpProvider("InvocationExpressionSignatureHelpProvider", LanguageNames.CSharp), Shared] +internal sealed class InvocationExpressionSignatureHelpProvider : InvocationExpressionSignatureHelpProviderBase +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public InvocationExpressionSignatureHelpProvider() + { + } +} + +internal partial class InvocationExpressionSignatureHelpProviderBase : AbstractOrdinaryMethodSignatureHelpProvider { - [ExportSignatureHelpProvider("InvocationExpressionSignatureHelpProvider", LanguageNames.CSharp), Shared] - internal sealed class InvocationExpressionSignatureHelpProvider : InvocationExpressionSignatureHelpProviderBase + public override bool IsTriggerCharacter(char ch) + => ch is '(' or ','; + + public override bool IsRetriggerCharacter(char ch) + => ch == ')'; + + private async Task TryGetInvocationExpressionAsync(Document document, int position, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public InvocationExpressionSignatureHelpProvider() + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + + if (!CommonSignatureHelpUtilities.TryGetSyntax( + root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out InvocationExpressionSyntax? expression)) { + return null; } + + if (expression.ArgumentList is null) + return null; + + return expression; + } + + private bool IsTriggerToken(SyntaxToken token) + => SignatureHelpUtilities.IsTriggerParenOrComma(token, IsTriggerCharacter); + + private static bool IsArgumentListToken(InvocationExpressionSyntax expression, SyntaxToken token) + { + return expression.ArgumentList.Span.Contains(token.SpanStart) && + token != expression.ArgumentList.CloseParenToken; } - internal partial class InvocationExpressionSignatureHelpProviderBase : AbstractOrdinaryMethodSignatureHelpProvider + protected override async Task GetItemsWorkerAsync( + Document document, + int position, + SignatureHelpTriggerInfo triggerInfo, + SignatureHelpOptions options, + CancellationToken cancellationToken) { - public override bool IsTriggerCharacter(char ch) - => ch is '(' or ','; + var invocationExpression = await TryGetInvocationExpressionAsync( + document, position, triggerInfo.TriggerReason, cancellationToken).ConfigureAwait(false); + if (invocationExpression is null) + return null; - public override bool IsRetriggerCharacter(char ch) - => ch == ')'; + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(invocationExpression, cancellationToken).ConfigureAwait(false); + var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); + if (within == null) + return null; - private async Task TryGetInvocationExpressionAsync(Document document, int position, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken) + var invokedType = semanticModel.GetTypeInfo(invocationExpression.Expression, cancellationToken).Type; + if (invokedType is INamedTypeSymbol { TypeKind: TypeKind.Delegate } or IFunctionPointerTypeSymbol) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); + return await GetItemsWorkerForDelegateOrFunctionPointerAsync(document, position, invocationExpression, within, cancellationToken).ConfigureAwait(false); + } - if (!CommonSignatureHelpUtilities.TryGetSyntax( - root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out InvocationExpressionSyntax? expression)) - { - return null; - } + // get the candidate methods + var symbolDisplayService = document.GetLanguageService(); + var methods = semanticModel + .GetMemberGroup(invocationExpression.Expression, cancellationToken) + .OfType() + .ToImmutableArray() + .FilterToVisibleAndBrowsableSymbols(options.HideAdvancedMembers, semanticModel.Compilation); + methods = GetAccessibleMethods(invocationExpression, semanticModel, within, methods, cancellationToken); + methods = methods.Sort(semanticModel, invocationExpression.SpanStart); + + if (methods.Length == 0) + return null; - if (expression.ArgumentList is null) - return null; + // guess the best candidate if needed and determine parameter index + var symbolInfo = semanticModel.GetSymbolInfo(invocationExpression, cancellationToken); + var (currentSymbol, parameterIndexOverride) = new LightweightOverloadResolution(semanticModel, position, invocationExpression.ArgumentList.Arguments) + .RefineOverloadAndPickParameter(symbolInfo, methods); - return expression; - } + // if the symbol could be bound, replace that item in the symbol list + if (currentSymbol?.IsGenericMethod == true) + methods = methods.SelectAsArray(m => Equals(currentSymbol.OriginalDefinition, m) ? currentSymbol : m); + + // present items and select + var (items, selectedItem) = await GetMethodGroupItemsAndSelectionAsync( + methods, document, invocationExpression, semanticModel, symbolInfo, currentSymbol, cancellationToken).ConfigureAwait(false); + + var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(invocationExpression.ArgumentList); + var argumentState = await GetCurrentArgumentStateAsync( + document, position, textSpan, cancellationToken).ConfigureAwait(false); + return CreateSignatureHelpItems(items, textSpan, argumentState, selectedItem, parameterIndexOverride); + } - private bool IsTriggerToken(SyntaxToken token) - => SignatureHelpUtilities.IsTriggerParenOrComma(token, IsTriggerCharacter); + protected async Task GetItemsWorkerForDelegateOrFunctionPointerAsync( + Document document, + int position, + InvocationExpressionSyntax invocationExpression, + ISymbol within, + CancellationToken cancellationToken) + { + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(invocationExpression, cancellationToken).ConfigureAwait(false); - private static bool IsArgumentListToken(InvocationExpressionSyntax expression, SyntaxToken token) + var invokedType = semanticModel.GetTypeInfo(invocationExpression.Expression, cancellationToken).Type; + IMethodSymbol? currentSymbol; + if (invokedType is INamedTypeSymbol { TypeKind: TypeKind.Delegate } expressionType) { - return expression.ArgumentList.Span.Contains(token.SpanStart) && - token != expression.ArgumentList.CloseParenToken; + currentSymbol = GetDelegateInvokeMethod(invocationExpression, semanticModel, within, expressionType, cancellationToken); } - - protected override async Task GetItemsWorkerAsync( - Document document, - int position, - SignatureHelpTriggerInfo triggerInfo, - SignatureHelpOptions options, - CancellationToken cancellationToken) + else if (invokedType is IFunctionPointerTypeSymbol functionPointerType) { - var invocationExpression = await TryGetInvocationExpressionAsync( - document, position, triggerInfo.TriggerReason, cancellationToken).ConfigureAwait(false); - if (invocationExpression is null) - return null; - - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(invocationExpression, cancellationToken).ConfigureAwait(false); - var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken); - if (within == null) - return null; - - var invokedType = semanticModel.GetTypeInfo(invocationExpression.Expression, cancellationToken).Type; - if (invokedType is INamedTypeSymbol { TypeKind: TypeKind.Delegate } or IFunctionPointerTypeSymbol) - { - return await GetItemsWorkerForDelegateOrFunctionPointerAsync(document, position, invocationExpression, within, cancellationToken).ConfigureAwait(false); - } - - // get the candidate methods - var symbolDisplayService = document.GetLanguageService(); - var methods = semanticModel - .GetMemberGroup(invocationExpression.Expression, cancellationToken) - .OfType() - .ToImmutableArray() - .FilterToVisibleAndBrowsableSymbols(options.HideAdvancedMembers, semanticModel.Compilation); - methods = GetAccessibleMethods(invocationExpression, semanticModel, within, methods, cancellationToken); - methods = methods.Sort(semanticModel, invocationExpression.SpanStart); - - if (methods.Length == 0) - return null; - - // guess the best candidate if needed and determine parameter index - var symbolInfo = semanticModel.GetSymbolInfo(invocationExpression, cancellationToken); - var (currentSymbol, parameterIndexOverride) = new LightweightOverloadResolution(semanticModel, position, invocationExpression.ArgumentList.Arguments) - .RefineOverloadAndPickParameter(symbolInfo, methods); - - // if the symbol could be bound, replace that item in the symbol list - if (currentSymbol?.IsGenericMethod == true) - methods = methods.SelectAsArray(m => Equals(currentSymbol.OriginalDefinition, m) ? currentSymbol : m); - - // present items and select - var (items, selectedItem) = await GetMethodGroupItemsAndSelectionAsync( - methods, document, invocationExpression, semanticModel, symbolInfo, currentSymbol, cancellationToken).ConfigureAwait(false); - - var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(invocationExpression.ArgumentList); - var argumentState = await GetCurrentArgumentStateAsync( - document, position, textSpan, cancellationToken).ConfigureAwait(false); - return CreateSignatureHelpItems(items, textSpan, argumentState, selectedItem, parameterIndexOverride); + currentSymbol = functionPointerType.Signature; } - - protected async Task GetItemsWorkerForDelegateOrFunctionPointerAsync( - Document document, - int position, - InvocationExpressionSyntax invocationExpression, - ISymbol within, - CancellationToken cancellationToken) + else { - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(invocationExpression, cancellationToken).ConfigureAwait(false); - - var invokedType = semanticModel.GetTypeInfo(invocationExpression.Expression, cancellationToken).Type; - IMethodSymbol? currentSymbol; - if (invokedType is INamedTypeSymbol { TypeKind: TypeKind.Delegate } expressionType) - { - currentSymbol = GetDelegateInvokeMethod(invocationExpression, semanticModel, within, expressionType, cancellationToken); - } - else if (invokedType is IFunctionPointerTypeSymbol functionPointerType) - { - currentSymbol = functionPointerType.Signature; - } - else - { - throw ExceptionUtilities.Unreachable(); - } - - if (currentSymbol is null) - { - return null; - } - - // determine parameter index - var parameterIndexOverride = new LightweightOverloadResolution(semanticModel, position, invocationExpression.ArgumentList.Arguments) - .FindParameterIndexIfCompatibleMethod(currentSymbol); - - // present item and select - var structuralTypeDisplayService = document.Project.Services.GetRequiredService(); - var documentationCommentFormattingService = document.GetRequiredLanguageService(); - - var items = GetDelegateOrFunctionPointerInvokeItems(invocationExpression, currentSymbol, - semanticModel, structuralTypeDisplayService, documentationCommentFormattingService, out var selectedItem, cancellationToken); - - var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(invocationExpression.ArgumentList); - var argumentState = await GetCurrentArgumentStateAsync( - document, position, textSpan, cancellationToken).ConfigureAwait(false); - return CreateSignatureHelpItems(items, textSpan, argumentState, selectedItem, parameterIndexOverride); + throw ExceptionUtilities.Unreachable(); } - private async Task GetCurrentArgumentStateAsync( - Document document, - int position, - TextSpan currentSpan, - CancellationToken cancellationToken) + if (currentSymbol is null) { - var expression = await TryGetInvocationExpressionAsync( - document, position, SignatureHelpTriggerReason.InvokeSignatureHelpCommand, cancellationToken).ConfigureAwait(false); - if (expression is { ArgumentList: not null } && - currentSpan.Start == SignatureHelpUtilities.GetSignatureHelpSpan(expression.ArgumentList).Start) - { - return SignatureHelpUtilities.GetSignatureHelpState(expression.ArgumentList, position); - } - return null; } + + // determine parameter index + var parameterIndexOverride = new LightweightOverloadResolution(semanticModel, position, invocationExpression.ArgumentList.Arguments) + .FindParameterIndexIfCompatibleMethod(currentSymbol); + + // present item and select + var structuralTypeDisplayService = document.Project.Services.GetRequiredService(); + var documentationCommentFormattingService = document.GetRequiredLanguageService(); + + var items = GetDelegateOrFunctionPointerInvokeItems(invocationExpression, currentSymbol, + semanticModel, structuralTypeDisplayService, documentationCommentFormattingService, out var selectedItem, cancellationToken); + + var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(invocationExpression.ArgumentList); + var argumentState = await GetCurrentArgumentStateAsync( + document, position, textSpan, cancellationToken).ConfigureAwait(false); + return CreateSignatureHelpItems(items, textSpan, argumentState, selectedItem, parameterIndexOverride); + } + + private async Task GetCurrentArgumentStateAsync( + Document document, + int position, + TextSpan currentSpan, + CancellationToken cancellationToken) + { + var expression = await TryGetInvocationExpressionAsync( + document, position, SignatureHelpTriggerReason.InvokeSignatureHelpCommand, cancellationToken).ConfigureAwait(false); + if (expression is { ArgumentList: not null } && + currentSpan.Start == SignatureHelpUtilities.GetSignatureHelpSpan(expression.ArgumentList).Start) + { + return SignatureHelpUtilities.GetSignatureHelpState(expression.ArgumentList, position); + } + + return null; } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_DelegateAndFunctionPointerInvoke.cs b/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_DelegateAndFunctionPointerInvoke.cs index d802315bce837..faef82d3e0eb4 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_DelegateAndFunctionPointerInvoke.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_DelegateAndFunctionPointerInvoke.cs @@ -12,93 +12,92 @@ using Microsoft.CodeAnalysis.SignatureHelp; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +internal partial class InvocationExpressionSignatureHelpProviderBase { - internal partial class InvocationExpressionSignatureHelpProviderBase + private static IMethodSymbol? GetDelegateInvokeMethod( + InvocationExpressionSyntax invocationExpression, SemanticModel semanticModel, ISymbol within, + INamedTypeSymbol delegateType, CancellationToken cancellationToken) { - private static IMethodSymbol? GetDelegateInvokeMethod( - InvocationExpressionSyntax invocationExpression, SemanticModel semanticModel, ISymbol within, - INamedTypeSymbol delegateType, CancellationToken cancellationToken) + var invokeMethod = delegateType.DelegateInvokeMethod; + if (invokeMethod == null) { - var invokeMethod = delegateType.DelegateInvokeMethod; - if (invokeMethod == null) - { - return null; - } - - // Events can only be invoked directly from the class they were declared in. - var expressionSymbol = semanticModel.GetSymbolInfo(invocationExpression.Expression, cancellationToken).GetAnySymbol(); - if (expressionSymbol.IsKind(SymbolKind.Event) && - !expressionSymbol.ContainingType.OriginalDefinition.Equals(within.OriginalDefinition)) - { - return null; - } - - return invokeMethod; + return null; } - private static IList GetDelegateOrFunctionPointerInvokeItems(InvocationExpressionSyntax invocationExpression, IMethodSymbol invokeMethod, SemanticModel semanticModel, IStructuralTypeDisplayService structuralTypeDisplayService, IDocumentationCommentFormattingService documentationCommentFormattingService, out int? selectedItem, CancellationToken cancellationToken) + // Events can only be invoked directly from the class they were declared in. + var expressionSymbol = semanticModel.GetSymbolInfo(invocationExpression.Expression, cancellationToken).GetAnySymbol(); + if (expressionSymbol.IsKind(SymbolKind.Event) && + !expressionSymbol.ContainingType.OriginalDefinition.Equals(within.OriginalDefinition)) { - var position = invocationExpression.SpanStart; - var item = CreateItem( - invokeMethod, semanticModel, position, - structuralTypeDisplayService, - isVariadic: invokeMethod.IsParams(), - documentationFactory: null, - prefixParts: GetDelegateOrFunctionPointerInvokePreambleParts(invokeMethod, semanticModel, position), - separatorParts: GetSeparatorParts(), - suffixParts: GetDelegateOrFunctionPointerInvokePostambleParts(), - parameters: GetDelegateOrFunctionPointerInvokeParameters(invokeMethod, semanticModel, position, documentationCommentFormattingService, cancellationToken)); - - // Since we're returning a single item, we can selected it as the "best one". - selectedItem = 0; - - return SpecializedCollections.SingletonList(item); + return null; } - private static IList GetDelegateOrFunctionPointerInvokePreambleParts(IMethodSymbol invokeMethod, SemanticModel semanticModel, int position) + return invokeMethod; + } + + private static IList GetDelegateOrFunctionPointerInvokeItems(InvocationExpressionSyntax invocationExpression, IMethodSymbol invokeMethod, SemanticModel semanticModel, IStructuralTypeDisplayService structuralTypeDisplayService, IDocumentationCommentFormattingService documentationCommentFormattingService, out int? selectedItem, CancellationToken cancellationToken) + { + var position = invocationExpression.SpanStart; + var item = CreateItem( + invokeMethod, semanticModel, position, + structuralTypeDisplayService, + isVariadic: invokeMethod.IsParams(), + documentationFactory: null, + prefixParts: GetDelegateOrFunctionPointerInvokePreambleParts(invokeMethod, semanticModel, position), + separatorParts: GetSeparatorParts(), + suffixParts: GetDelegateOrFunctionPointerInvokePostambleParts(), + parameters: GetDelegateOrFunctionPointerInvokeParameters(invokeMethod, semanticModel, position, documentationCommentFormattingService, cancellationToken)); + + // Since we're returning a single item, we can selected it as the "best one". + selectedItem = 0; + + return SpecializedCollections.SingletonList(item); + } + + private static IList GetDelegateOrFunctionPointerInvokePreambleParts(IMethodSymbol invokeMethod, SemanticModel semanticModel, int position) + { + var displayParts = new List(); + displayParts.AddRange(invokeMethod.ReturnType.ToMinimalDisplayParts(semanticModel, position)); + displayParts.Add(Space()); + + if (invokeMethod.MethodKind == MethodKind.FunctionPointerSignature) { - var displayParts = new List(); - displayParts.AddRange(invokeMethod.ReturnType.ToMinimalDisplayParts(semanticModel, position)); - displayParts.Add(Space()); - - if (invokeMethod.MethodKind == MethodKind.FunctionPointerSignature) - { - displayParts.Add(Keyword(SyntaxKind.DelegateKeyword)); - displayParts.Add(Operator(SyntaxKind.AsteriskToken)); - } - else - { - displayParts.AddRange(invokeMethod.ContainingType.ToMinimalDisplayParts(semanticModel, position)); - } - - displayParts.Add(Punctuation(SyntaxKind.OpenParenToken)); - - return displayParts; + displayParts.Add(Keyword(SyntaxKind.DelegateKeyword)); + displayParts.Add(Operator(SyntaxKind.AsteriskToken)); } - - private static IList GetDelegateOrFunctionPointerInvokeParameters( - IMethodSymbol invokeMethod, SemanticModel semanticModel, int position, IDocumentationCommentFormattingService formattingService, CancellationToken cancellationToken) + else { - var result = new List(); - - foreach (var parameter in invokeMethod.Parameters) - { - cancellationToken.ThrowIfCancellationRequested(); - result.Add(new SignatureHelpSymbolParameter( - parameter.Name, - parameter.IsOptional, - parameter.GetDocumentationPartsFactory(semanticModel, position, formattingService), - parameter.ToMinimalDisplayParts(semanticModel, position))); - } - - return result; + displayParts.AddRange(invokeMethod.ContainingType.ToMinimalDisplayParts(semanticModel, position)); } - private static IList GetDelegateOrFunctionPointerInvokePostambleParts() + displayParts.Add(Punctuation(SyntaxKind.OpenParenToken)); + + return displayParts; + } + + private static IList GetDelegateOrFunctionPointerInvokeParameters( + IMethodSymbol invokeMethod, SemanticModel semanticModel, int position, IDocumentationCommentFormattingService formattingService, CancellationToken cancellationToken) + { + var result = new List(); + + foreach (var parameter in invokeMethod.Parameters) { - return SpecializedCollections.SingletonList( - Punctuation(SyntaxKind.CloseParenToken)); + cancellationToken.ThrowIfCancellationRequested(); + result.Add(new SignatureHelpSymbolParameter( + parameter.Name, + parameter.IsOptional, + parameter.GetDocumentationPartsFactory(semanticModel, position, formattingService), + parameter.ToMinimalDisplayParts(semanticModel, position))); } + + return result; + } + + private static IList GetDelegateOrFunctionPointerInvokePostambleParts() + { + return SpecializedCollections.SingletonList( + Punctuation(SyntaxKind.CloseParenToken)); } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_MethodGroup.cs b/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_MethodGroup.cs index 3e4ebd99390c3..cf677926ca6f8 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_MethodGroup.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_MethodGroup.cs @@ -13,99 +13,98 @@ using Microsoft.CodeAnalysis.SignatureHelp; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +internal partial class InvocationExpressionSignatureHelpProviderBase { - internal partial class InvocationExpressionSignatureHelpProviderBase + internal virtual Task<(ImmutableArray items, int? selectedItemIndex)> GetMethodGroupItemsAndSelectionAsync( + ImmutableArray accessibleMethods, + Document document, + InvocationExpressionSyntax invocationExpression, + SemanticModel semanticModel, + SymbolInfo symbolInfo, + IMethodSymbol? currentSymbol, + CancellationToken cancellationToken) { - internal virtual Task<(ImmutableArray items, int? selectedItemIndex)> GetMethodGroupItemsAndSelectionAsync( - ImmutableArray accessibleMethods, - Document document, - InvocationExpressionSyntax invocationExpression, - SemanticModel semanticModel, - SymbolInfo symbolInfo, - IMethodSymbol? currentSymbol, - CancellationToken cancellationToken) - { - var items = accessibleMethods.SelectAsArray(method => ConvertMethodGroupMethod( - document, method, invocationExpression.SpanStart, semanticModel)); - var selectedItemIndex = TryGetSelectedIndex(accessibleMethods, currentSymbol); - return Task.FromResult((items, selectedItemIndex)); - } + var items = accessibleMethods.SelectAsArray(method => ConvertMethodGroupMethod( + document, method, invocationExpression.SpanStart, semanticModel)); + var selectedItemIndex = TryGetSelectedIndex(accessibleMethods, currentSymbol); + return Task.FromResult((items, selectedItemIndex)); + } - private static ImmutableArray GetAccessibleMethods( - InvocationExpressionSyntax invocationExpression, - SemanticModel semanticModel, - ISymbol within, - IEnumerable methodGroup, - CancellationToken cancellationToken) + private static ImmutableArray GetAccessibleMethods( + InvocationExpressionSyntax invocationExpression, + SemanticModel semanticModel, + ISymbol within, + IEnumerable methodGroup, + CancellationToken cancellationToken) + { + ITypeSymbol? throughType = null; + if (invocationExpression.Expression is MemberAccessExpressionSyntax memberAccess) { - ITypeSymbol? throughType = null; - if (invocationExpression.Expression is MemberAccessExpressionSyntax memberAccess) - { - var throughExpression = memberAccess.Expression; - var throughSymbol = semanticModel.GetSymbolInfo(throughExpression, cancellationToken).GetAnySymbol(); + var throughExpression = memberAccess.Expression; + var throughSymbol = semanticModel.GetSymbolInfo(throughExpression, cancellationToken).GetAnySymbol(); - // if it is via a base expression "base.", we know the "throughType" is the base class but - // we need to be able to tell between "base.M()" and "new Base().M()". - // currently, Access check methods do not differentiate between them. - // so handle "base." primary-expression here by nulling out "throughType" - if (throughExpression is not BaseExpressionSyntax) - { - throughType = semanticModel.GetTypeInfo(throughExpression, cancellationToken).Type; - } - - // SyntaxKind.IdentifierName is for basic case, e.g. "MyClass.MyStaticMethod(...)" - // SyntaxKind.SimpleMemberAccessExpression is for not imported types, e.g. "MyNamespace.MyClass.MyStaticMethod(...)" - // SyntaxKind.PredefinedType is for built-in types, e.g. "string.Equals(...)" - var includeInstance = throughExpression.Kind() is not (SyntaxKind.IdentifierName or SyntaxKind.SimpleMemberAccessExpression or SyntaxKind.PredefinedType) || - semanticModel.LookupSymbols(throughExpression.SpanStart, name: throughSymbol?.Name).Any(static s => s is not INamedTypeSymbol) || - (throughSymbol is not INamespaceOrTypeSymbol && semanticModel.LookupSymbols(throughExpression.SpanStart, container: throughSymbol?.ContainingType).Any(static s => s is not INamedTypeSymbol)); - - var includeStatic = throughSymbol is INamedTypeSymbol || - (throughExpression.IsKind(SyntaxKind.IdentifierName) && - semanticModel.LookupNamespacesAndTypes(throughExpression.SpanStart, name: throughSymbol?.Name).Any(static (t, throughType) => Equals(t.GetSymbolType(), throughType), throughType)); - - Contract.ThrowIfFalse(includeInstance || includeStatic); - methodGroup = methodGroup.Where(m => (m.IsStatic && includeStatic) || (!m.IsStatic && includeInstance)); - } - else if (invocationExpression.Expression is SimpleNameSyntax && - invocationExpression.IsInStaticContext()) + // if it is via a base expression "base.", we know the "throughType" is the base class but + // we need to be able to tell between "base.M()" and "new Base().M()". + // currently, Access check methods do not differentiate between them. + // so handle "base." primary-expression here by nulling out "throughType" + if (throughExpression is not BaseExpressionSyntax) { - // We always need to include local functions regardless of whether they are static. - methodGroup = methodGroup.Where(m => m.IsStatic || m is IMethodSymbol { MethodKind: MethodKind.LocalFunction }); + throughType = semanticModel.GetTypeInfo(throughExpression, cancellationToken).Type; } - var accessibleMethods = methodGroup.Where(m => m.IsAccessibleWithin(within, throughType: throughType)).ToImmutableArrayOrEmpty(); - if (accessibleMethods.Length == 0) - { - return accessibleMethods; - } + // SyntaxKind.IdentifierName is for basic case, e.g. "MyClass.MyStaticMethod(...)" + // SyntaxKind.SimpleMemberAccessExpression is for not imported types, e.g. "MyNamespace.MyClass.MyStaticMethod(...)" + // SyntaxKind.PredefinedType is for built-in types, e.g. "string.Equals(...)" + var includeInstance = throughExpression.Kind() is not (SyntaxKind.IdentifierName or SyntaxKind.SimpleMemberAccessExpression or SyntaxKind.PredefinedType) || + semanticModel.LookupSymbols(throughExpression.SpanStart, name: throughSymbol?.Name).Any(static s => s is not INamedTypeSymbol) || + (throughSymbol is not INamespaceOrTypeSymbol && semanticModel.LookupSymbols(throughExpression.SpanStart, container: throughSymbol?.ContainingType).Any(static s => s is not INamedTypeSymbol)); + + var includeStatic = throughSymbol is INamedTypeSymbol || + (throughExpression.IsKind(SyntaxKind.IdentifierName) && + semanticModel.LookupNamespacesAndTypes(throughExpression.SpanStart, name: throughSymbol?.Name).Any(static (t, throughType) => Equals(t.GetSymbolType(), throughType), throughType)); - var methodSet = accessibleMethods.ToSet(); - return accessibleMethods.Where(m => !IsHiddenByOtherMethod(m, methodSet)).ToImmutableArrayOrEmpty(); + Contract.ThrowIfFalse(includeInstance || includeStatic); + methodGroup = methodGroup.Where(m => (m.IsStatic && includeStatic) || (!m.IsStatic && includeInstance)); } + else if (invocationExpression.Expression is SimpleNameSyntax && + invocationExpression.IsInStaticContext()) + { + // We always need to include local functions regardless of whether they are static. + methodGroup = methodGroup.Where(m => m.IsStatic || m is IMethodSymbol { MethodKind: MethodKind.LocalFunction }); + } + + var accessibleMethods = methodGroup.Where(m => m.IsAccessibleWithin(within, throughType: throughType)).ToImmutableArrayOrEmpty(); + if (accessibleMethods.Length == 0) + { + return accessibleMethods; + } + + var methodSet = accessibleMethods.ToSet(); + return accessibleMethods.Where(m => !IsHiddenByOtherMethod(m, methodSet)).ToImmutableArrayOrEmpty(); + } - private static bool IsHiddenByOtherMethod(IMethodSymbol method, ISet methodSet) + private static bool IsHiddenByOtherMethod(IMethodSymbol method, ISet methodSet) + { + foreach (var m in methodSet) { - foreach (var m in methodSet) + if (!Equals(m, method)) { - if (!Equals(m, method)) + if (IsHiddenBy(method, m)) { - if (IsHiddenBy(method, m)) - { - return true; - } + return true; } } - - return false; } - private static bool IsHiddenBy(IMethodSymbol method1, IMethodSymbol method2) - { - // If they have the same parameter types and the same parameter names, then the - // constructed method is hidden by the unconstructed one. - return method2.IsMoreSpecificThan(method1) == true; - } + return false; + } + + private static bool IsHiddenBy(IMethodSymbol method1, IMethodSymbol method2) + { + // If they have the same parameter types and the same parameter names, then the + // constructed method is hidden by the unconstructed one. + return method2.IsMoreSpecificThan(method1) == true; } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider.cs index 06b7aff1bb3c5..bfa6cb523d8fe 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider.cs @@ -16,138 +16,137 @@ using Microsoft.CodeAnalysis.SignatureHelp; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +[ExportSignatureHelpProvider("ObjectCreationExpressionSignatureHelpProvider", LanguageNames.CSharp), Shared] +internal partial class ObjectCreationExpressionSignatureHelpProvider : AbstractCSharpSignatureHelpProvider { - [ExportSignatureHelpProvider("ObjectCreationExpressionSignatureHelpProvider", LanguageNames.CSharp), Shared] - internal partial class ObjectCreationExpressionSignatureHelpProvider : AbstractCSharpSignatureHelpProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ObjectCreationExpressionSignatureHelpProvider() + { + } + + public override bool IsTriggerCharacter(char ch) + => ch is '(' or ','; + + public override bool IsRetriggerCharacter(char ch) + => ch == ')'; + + private async Task TryGetObjectCreationExpressionAsync( + Document document, + int position, + SignatureHelpTriggerReason triggerReason, + CancellationToken cancellationToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ObjectCreationExpressionSignatureHelpProvider() + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + + if (!CommonSignatureHelpUtilities.TryGetSyntax( + root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out BaseObjectCreationExpressionSyntax? expression)) { + return null; } - public override bool IsTriggerCharacter(char ch) - => ch is '(' or ','; + if (expression.ArgumentList is null) + return null; - public override bool IsRetriggerCharacter(char ch) - => ch == ')'; + return expression; + } - private async Task TryGetObjectCreationExpressionAsync( - Document document, - int position, - SignatureHelpTriggerReason triggerReason, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); + private bool IsTriggerToken(SyntaxToken token) + => SignatureHelpUtilities.IsTriggerParenOrComma(token, IsTriggerCharacter); - if (!CommonSignatureHelpUtilities.TryGetSyntax( - root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out BaseObjectCreationExpressionSyntax? expression)) - { - return null; - } + private static bool IsArgumentListToken(BaseObjectCreationExpressionSyntax expression, SyntaxToken token) + { + return expression.ArgumentList != null && + expression.ArgumentList.Span.Contains(token.SpanStart) && + token != expression.ArgumentList.CloseParenToken; + } - if (expression.ArgumentList is null) - return null; + protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var objectCreationExpression = await TryGetObjectCreationExpressionAsync( + document, position, triggerInfo.TriggerReason, cancellationToken).ConfigureAwait(false); + if (objectCreationExpression is not { ArgumentList: not null }) + return null; - return expression; - } + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(objectCreationExpression, cancellationToken).ConfigureAwait(false); + if (semanticModel.GetTypeInfo(objectCreationExpression, cancellationToken).Type is not INamedTypeSymbol type) + return null; - private bool IsTriggerToken(SyntaxToken token) - => SignatureHelpUtilities.IsTriggerParenOrComma(token, IsTriggerCharacter); + var within = semanticModel.GetEnclosingNamedType(position, cancellationToken); + if (within == null) + return null; - private static bool IsArgumentListToken(BaseObjectCreationExpressionSyntax expression, SyntaxToken token) - { - return expression.ArgumentList != null && - expression.ArgumentList.Span.Contains(token.SpanStart) && - token != expression.ArgumentList.CloseParenToken; - } + var symbolDisplayService = document.GetLanguageService(); + if (type.TypeKind == TypeKind.Delegate) + return await GetItemsWorkerForDelegateAsync(document, position, objectCreationExpression, type, cancellationToken).ConfigureAwait(false); - protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var objectCreationExpression = await TryGetObjectCreationExpressionAsync( - document, position, triggerInfo.TriggerReason, cancellationToken).ConfigureAwait(false); - if (objectCreationExpression is not { ArgumentList: not null }) - return null; - - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(objectCreationExpression, cancellationToken).ConfigureAwait(false); - if (semanticModel.GetTypeInfo(objectCreationExpression, cancellationToken).Type is not INamedTypeSymbol type) - return null; - - var within = semanticModel.GetEnclosingNamedType(position, cancellationToken); - if (within == null) - return null; - - var symbolDisplayService = document.GetLanguageService(); - if (type.TypeKind == TypeKind.Delegate) - return await GetItemsWorkerForDelegateAsync(document, position, objectCreationExpression, type, cancellationToken).ConfigureAwait(false); - - // get the candidate methods - var methods = type.InstanceConstructors - .WhereAsArray(c => c.IsAccessibleWithin(within)) - .WhereAsArray(s => s.IsEditorBrowsable(options.HideAdvancedMembers, semanticModel.Compilation)) - .Sort(semanticModel, objectCreationExpression.SpanStart); - - if (!methods.Any()) - return null; - - // guess the best candidate if needed and determine parameter index - var (currentSymbol, parameterIndexOverride) = new LightweightOverloadResolution(semanticModel, position, objectCreationExpression.ArgumentList.Arguments) - .RefineOverloadAndPickParameter(semanticModel.GetSymbolInfo(objectCreationExpression, cancellationToken), methods); - - // present items and select - var structuralTypeDisplayService = document.Project.Services.GetRequiredService(); - var documentationCommentFormattingService = document.GetRequiredLanguageService(); - - var items = methods.SelectAsArray(c => - ConvertNormalTypeConstructor(c, objectCreationExpression, semanticModel, structuralTypeDisplayService, documentationCommentFormattingService)); - - var selectedItem = TryGetSelectedIndex(methods, currentSymbol); - - var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(objectCreationExpression.ArgumentList); - var argumentState = await GetCurrentArgumentStateAsync( - document, position, textSpan, cancellationToken).ConfigureAwait(false); - return CreateSignatureHelpItems(items, textSpan, argumentState, selectedItem, parameterIndexOverride); - } + // get the candidate methods + var methods = type.InstanceConstructors + .WhereAsArray(c => c.IsAccessibleWithin(within)) + .WhereAsArray(s => s.IsEditorBrowsable(options.HideAdvancedMembers, semanticModel.Compilation)) + .Sort(semanticModel, objectCreationExpression.SpanStart); - private async Task GetItemsWorkerForDelegateAsync(Document document, int position, BaseObjectCreationExpressionSyntax objectCreationExpression, - INamedTypeSymbol type, CancellationToken cancellationToken) - { - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(objectCreationExpression, cancellationToken).ConfigureAwait(false); - Debug.Assert(type.TypeKind == TypeKind.Delegate); - Debug.Assert(objectCreationExpression.ArgumentList is not null); - - var invokeMethod = type.DelegateInvokeMethod; - if (invokeMethod is null) - return null; - - // determine parameter index - var parameterIndexOverride = new LightweightOverloadResolution(semanticModel, position, objectCreationExpression.ArgumentList.Arguments) - .FindParameterIndexIfCompatibleMethod(invokeMethod); - - // present item and select - var structuralTypeDisplayService = document.Project.Services.GetRequiredService(); - var items = ConvertDelegateTypeConstructor(objectCreationExpression, invokeMethod, semanticModel, structuralTypeDisplayService, position); - var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(objectCreationExpression.ArgumentList); - var argumentState = await GetCurrentArgumentStateAsync( - document, position, textSpan, cancellationToken).ConfigureAwait(false); - return CreateSignatureHelpItems(items, textSpan, argumentState, selectedItemIndex: 0, parameterIndexOverride); - } + if (!methods.Any()) + return null; - private async Task GetCurrentArgumentStateAsync( - Document document, int position, TextSpan currentSpan, CancellationToken cancellationToken) - { - var expression = await TryGetObjectCreationExpressionAsync( - document, position, SignatureHelpTriggerReason.InvokeSignatureHelpCommand, cancellationToken).ConfigureAwait(false); - if (expression is { ArgumentList: not null } && - currentSpan.Start == SignatureHelpUtilities.GetSignatureHelpSpan(expression.ArgumentList).Start) - { - return SignatureHelpUtilities.GetSignatureHelpState(expression.ArgumentList, position); - } + // guess the best candidate if needed and determine parameter index + var (currentSymbol, parameterIndexOverride) = new LightweightOverloadResolution(semanticModel, position, objectCreationExpression.ArgumentList.Arguments) + .RefineOverloadAndPickParameter(semanticModel.GetSymbolInfo(objectCreationExpression, cancellationToken), methods); + + // present items and select + var structuralTypeDisplayService = document.Project.Services.GetRequiredService(); + var documentationCommentFormattingService = document.GetRequiredLanguageService(); + + var items = methods.SelectAsArray(c => + ConvertNormalTypeConstructor(c, objectCreationExpression, semanticModel, structuralTypeDisplayService, documentationCommentFormattingService)); + var selectedItem = TryGetSelectedIndex(methods, currentSymbol); + + var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(objectCreationExpression.ArgumentList); + var argumentState = await GetCurrentArgumentStateAsync( + document, position, textSpan, cancellationToken).ConfigureAwait(false); + return CreateSignatureHelpItems(items, textSpan, argumentState, selectedItem, parameterIndexOverride); + } + + private async Task GetItemsWorkerForDelegateAsync(Document document, int position, BaseObjectCreationExpressionSyntax objectCreationExpression, + INamedTypeSymbol type, CancellationToken cancellationToken) + { + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(objectCreationExpression, cancellationToken).ConfigureAwait(false); + Debug.Assert(type.TypeKind == TypeKind.Delegate); + Debug.Assert(objectCreationExpression.ArgumentList is not null); + + var invokeMethod = type.DelegateInvokeMethod; + if (invokeMethod is null) return null; + + // determine parameter index + var parameterIndexOverride = new LightweightOverloadResolution(semanticModel, position, objectCreationExpression.ArgumentList.Arguments) + .FindParameterIndexIfCompatibleMethod(invokeMethod); + + // present item and select + var structuralTypeDisplayService = document.Project.Services.GetRequiredService(); + var items = ConvertDelegateTypeConstructor(objectCreationExpression, invokeMethod, semanticModel, structuralTypeDisplayService, position); + var textSpan = SignatureHelpUtilities.GetSignatureHelpSpan(objectCreationExpression.ArgumentList); + var argumentState = await GetCurrentArgumentStateAsync( + document, position, textSpan, cancellationToken).ConfigureAwait(false); + return CreateSignatureHelpItems(items, textSpan, argumentState, selectedItemIndex: 0, parameterIndexOverride); + } + + private async Task GetCurrentArgumentStateAsync( + Document document, int position, TextSpan currentSpan, CancellationToken cancellationToken) + { + var expression = await TryGetObjectCreationExpressionAsync( + document, position, SignatureHelpTriggerReason.InvokeSignatureHelpCommand, cancellationToken).ConfigureAwait(false); + if (expression is { ArgumentList: not null } && + currentSpan.Start == SignatureHelpUtilities.GetSignatureHelpSpan(expression.ArgumentList).Start) + { + return SignatureHelpUtilities.GetSignatureHelpState(expression.ArgumentList, position); } + + return null; } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider_DelegateType.cs b/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider_DelegateType.cs index 5b1eca96e1f2b..8edc55b68a139 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider_DelegateType.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider_DelegateType.cs @@ -9,79 +9,78 @@ using Microsoft.CodeAnalysis.SignatureHelp; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +internal partial class ObjectCreationExpressionSignatureHelpProvider { - internal partial class ObjectCreationExpressionSignatureHelpProvider + private static ImmutableArray ConvertDelegateTypeConstructor( + BaseObjectCreationExpressionSyntax objectCreationExpression, + IMethodSymbol invokeMethod, + SemanticModel semanticModel, + IStructuralTypeDisplayService structuralTypeDisplayService, + int position) { - private static ImmutableArray ConvertDelegateTypeConstructor( - BaseObjectCreationExpressionSyntax objectCreationExpression, - IMethodSymbol invokeMethod, - SemanticModel semanticModel, - IStructuralTypeDisplayService structuralTypeDisplayService, - int position) - { - var item = CreateItem( - invokeMethod, semanticModel, - objectCreationExpression.SpanStart, - structuralTypeDisplayService, - isVariadic: false, - documentationFactory: null, - prefixParts: GetDelegateTypePreambleParts(invokeMethod, semanticModel, position), - separatorParts: GetSeparatorParts(), - suffixParts: GetDelegateTypePostambleParts(), - parameters: GetDelegateTypeParameters(invokeMethod, semanticModel, position)); + var item = CreateItem( + invokeMethod, semanticModel, + objectCreationExpression.SpanStart, + structuralTypeDisplayService, + isVariadic: false, + documentationFactory: null, + prefixParts: GetDelegateTypePreambleParts(invokeMethod, semanticModel, position), + separatorParts: GetSeparatorParts(), + suffixParts: GetDelegateTypePostambleParts(), + parameters: GetDelegateTypeParameters(invokeMethod, semanticModel, position)); - return [item]; - } + return [item]; + } - private static IList GetDelegateTypePreambleParts(IMethodSymbol invokeMethod, SemanticModel semanticModel, int position) - { - var result = new List(); + private static IList GetDelegateTypePreambleParts(IMethodSymbol invokeMethod, SemanticModel semanticModel, int position) + { + var result = new List(); - result.AddRange(invokeMethod.ContainingType.ToMinimalDisplayParts(semanticModel, position)); - result.Add(Punctuation(SyntaxKind.OpenParenToken)); + result.AddRange(invokeMethod.ContainingType.ToMinimalDisplayParts(semanticModel, position)); + result.Add(Punctuation(SyntaxKind.OpenParenToken)); - return result; - } + return result; + } - private static IList GetDelegateTypeParameters(IMethodSymbol invokeMethod, SemanticModel semanticModel, int position) - { - const string TargetName = "target"; + private static IList GetDelegateTypeParameters(IMethodSymbol invokeMethod, SemanticModel semanticModel, int position) + { + const string TargetName = "target"; - var parts = new List(); - parts.AddRange(invokeMethod.ReturnType.ToMinimalDisplayParts(semanticModel, position)); - parts.Add(Space()); - parts.Add(Punctuation(SyntaxKind.OpenParenToken)); + var parts = new List(); + parts.AddRange(invokeMethod.ReturnType.ToMinimalDisplayParts(semanticModel, position)); + parts.Add(Space()); + parts.Add(Punctuation(SyntaxKind.OpenParenToken)); - var first = true; - foreach (var parameter in invokeMethod.Parameters) + var first = true; + foreach (var parameter in invokeMethod.Parameters) + { + if (!first) { - if (!first) - { - parts.Add(Punctuation(SyntaxKind.CommaToken)); - parts.Add(Space()); - } - - first = false; - parts.AddRange(parameter.Type.ToMinimalDisplayParts(semanticModel, position)); + parts.Add(Punctuation(SyntaxKind.CommaToken)); + parts.Add(Space()); } - parts.Add(Punctuation(SyntaxKind.CloseParenToken)); - parts.Add(Space()); - parts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.ParameterName, null, TargetName)); - - return SpecializedCollections.SingletonList( - new SignatureHelpSymbolParameter( - TargetName, - isOptional: false, - documentationFactory: null, - displayParts: parts)); + first = false; + parts.AddRange(parameter.Type.ToMinimalDisplayParts(semanticModel, position)); } - private static IList GetDelegateTypePostambleParts() - { - return SpecializedCollections.SingletonList( - Punctuation(SyntaxKind.CloseParenToken)); - } + parts.Add(Punctuation(SyntaxKind.CloseParenToken)); + parts.Add(Space()); + parts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.ParameterName, null, TargetName)); + + return SpecializedCollections.SingletonList( + new SignatureHelpSymbolParameter( + TargetName, + isOptional: false, + documentationFactory: null, + displayParts: parts)); + } + + private static IList GetDelegateTypePostambleParts() + { + return SpecializedCollections.SingletonList( + Punctuation(SyntaxKind.CloseParenToken)); } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider_NormalType.cs b/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider_NormalType.cs index 4ca70cfc8df3b..a966e3ea8ac79 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider_NormalType.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/ObjectCreationExpressionSignatureHelpProvider_NormalType.cs @@ -12,48 +12,47 @@ using Microsoft.CodeAnalysis.SignatureHelp; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +internal partial class ObjectCreationExpressionSignatureHelpProvider { - internal partial class ObjectCreationExpressionSignatureHelpProvider + private static SignatureHelpItem ConvertNormalTypeConstructor( + IMethodSymbol constructor, + BaseObjectCreationExpressionSyntax objectCreationExpression, + SemanticModel semanticModel, + IStructuralTypeDisplayService structuralTypeDisplayService, + IDocumentationCommentFormattingService documentationCommentFormattingService) + { + var position = objectCreationExpression.SpanStart; + var item = CreateItem( + constructor, semanticModel, position, + structuralTypeDisplayService, + constructor.IsParams(), + constructor.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormattingService), + GetNormalTypePreambleParts(constructor, semanticModel, position), + GetSeparatorParts(), + GetNormalTypePostambleParts(), + constructor.Parameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList()); + + return item; + } + + private static IList GetNormalTypePreambleParts( + IMethodSymbol method, + SemanticModel semanticModel, + int position) + { + var result = new List(); + + result.AddRange(method.ContainingType.ToMinimalDisplayParts(semanticModel, position)); + result.Add(Punctuation(SyntaxKind.OpenParenToken)); + + return result; + } + + private static IList GetNormalTypePostambleParts() { - private static SignatureHelpItem ConvertNormalTypeConstructor( - IMethodSymbol constructor, - BaseObjectCreationExpressionSyntax objectCreationExpression, - SemanticModel semanticModel, - IStructuralTypeDisplayService structuralTypeDisplayService, - IDocumentationCommentFormattingService documentationCommentFormattingService) - { - var position = objectCreationExpression.SpanStart; - var item = CreateItem( - constructor, semanticModel, position, - structuralTypeDisplayService, - constructor.IsParams(), - constructor.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormattingService), - GetNormalTypePreambleParts(constructor, semanticModel, position), - GetSeparatorParts(), - GetNormalTypePostambleParts(), - constructor.Parameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)).ToList()); - - return item; - } - - private static IList GetNormalTypePreambleParts( - IMethodSymbol method, - SemanticModel semanticModel, - int position) - { - var result = new List(); - - result.AddRange(method.ContainingType.ToMinimalDisplayParts(semanticModel, position)); - result.Add(Punctuation(SyntaxKind.OpenParenToken)); - - return result; - } - - private static IList GetNormalTypePostambleParts() - { - return SpecializedCollections.SingletonList( - Punctuation(SyntaxKind.CloseParenToken)); - } + return SpecializedCollections.SingletonList( + Punctuation(SyntaxKind.CloseParenToken)); } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/SignatureHelpUtilities.cs b/src/Features/CSharp/Portable/SignatureHelp/SignatureHelpUtilities.cs index f71bc3187c5c5..a71ae5cf6db5a 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/SignatureHelpUtilities.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/SignatureHelpUtilities.cs @@ -11,132 +11,131 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +internal static class SignatureHelpUtilities { - internal static class SignatureHelpUtilities + private static readonly Func s_getBaseArgumentListOpenToken = list => list.GetOpenToken(); + private static readonly Func s_getTypeArgumentListOpenToken = list => list.LessThanToken; + private static readonly Func s_getInitializerExpressionOpenToken = e => e.OpenBraceToken; + private static readonly Func s_getAttributeArgumentListOpenToken = list => list.OpenParenToken; + + private static readonly Func s_getBaseArgumentListCloseToken = list => list.GetCloseToken(); + private static readonly Func s_getTypeArgumentListCloseToken = list => list.GreaterThanToken; + private static readonly Func s_getInitializerExpressionCloseToken = e => e.CloseBraceToken; + private static readonly Func s_getAttributeArgumentListCloseToken = list => list.CloseParenToken; + + private static readonly Func s_getBaseArgumentListArgumentsWithSeparators = + list => list.Arguments.GetWithSeparators(); + private static readonly Func s_getTypeArgumentListArgumentsWithSeparators = + list => list.Arguments.GetWithSeparators(); + private static readonly Func s_getInitializerExpressionArgumentsWithSeparators = + e => e.Expressions.GetWithSeparators(); + private static readonly Func s_getAttributeArgumentListArgumentsWithSeparators = + list => list.Arguments.GetWithSeparators(); + + private static readonly Func> s_getBaseArgumentListNames = + list => list.Arguments.Select(argument => argument.NameColon?.Name.Identifier.ValueText); + private static readonly Func> s_getTypeArgumentListNames = + list => list.Arguments.Select(a => (string?)null); + private static readonly Func> s_getInitializerExpressionNames = + e => e.Expressions.Select(a => (string?)null); + private static readonly Func> s_getAttributeArgumentListNames = + list => list.Arguments.Select( + argument => argument.NameColon?.Name.Identifier.ValueText ?? argument.NameEquals?.Name.Identifier.ValueText); + + public static SignatureHelpState? GetSignatureHelpState(BaseArgumentListSyntax argumentList, int position) { - private static readonly Func s_getBaseArgumentListOpenToken = list => list.GetOpenToken(); - private static readonly Func s_getTypeArgumentListOpenToken = list => list.LessThanToken; - private static readonly Func s_getInitializerExpressionOpenToken = e => e.OpenBraceToken; - private static readonly Func s_getAttributeArgumentListOpenToken = list => list.OpenParenToken; - - private static readonly Func s_getBaseArgumentListCloseToken = list => list.GetCloseToken(); - private static readonly Func s_getTypeArgumentListCloseToken = list => list.GreaterThanToken; - private static readonly Func s_getInitializerExpressionCloseToken = e => e.CloseBraceToken; - private static readonly Func s_getAttributeArgumentListCloseToken = list => list.CloseParenToken; - - private static readonly Func s_getBaseArgumentListArgumentsWithSeparators = - list => list.Arguments.GetWithSeparators(); - private static readonly Func s_getTypeArgumentListArgumentsWithSeparators = - list => list.Arguments.GetWithSeparators(); - private static readonly Func s_getInitializerExpressionArgumentsWithSeparators = - e => e.Expressions.GetWithSeparators(); - private static readonly Func s_getAttributeArgumentListArgumentsWithSeparators = - list => list.Arguments.GetWithSeparators(); - - private static readonly Func> s_getBaseArgumentListNames = - list => list.Arguments.Select(argument => argument.NameColon?.Name.Identifier.ValueText); - private static readonly Func> s_getTypeArgumentListNames = - list => list.Arguments.Select(a => (string?)null); - private static readonly Func> s_getInitializerExpressionNames = - e => e.Expressions.Select(a => (string?)null); - private static readonly Func> s_getAttributeArgumentListNames = - list => list.Arguments.Select( - argument => argument.NameColon?.Name.Identifier.ValueText ?? argument.NameEquals?.Name.Identifier.ValueText); - - public static SignatureHelpState? GetSignatureHelpState(BaseArgumentListSyntax argumentList, int position) - { - return CommonSignatureHelpUtilities.GetSignatureHelpState( - argumentList, position, - s_getBaseArgumentListOpenToken, - s_getBaseArgumentListCloseToken, - s_getBaseArgumentListArgumentsWithSeparators, - s_getBaseArgumentListNames); - } + return CommonSignatureHelpUtilities.GetSignatureHelpState( + argumentList, position, + s_getBaseArgumentListOpenToken, + s_getBaseArgumentListCloseToken, + s_getBaseArgumentListArgumentsWithSeparators, + s_getBaseArgumentListNames); + } - internal static SignatureHelpState? GetSignatureHelpState(TypeArgumentListSyntax argumentList, int position) - { - return CommonSignatureHelpUtilities.GetSignatureHelpState( - argumentList, position, - s_getTypeArgumentListOpenToken, - s_getTypeArgumentListCloseToken, - s_getTypeArgumentListArgumentsWithSeparators, - s_getTypeArgumentListNames); - } + internal static SignatureHelpState? GetSignatureHelpState(TypeArgumentListSyntax argumentList, int position) + { + return CommonSignatureHelpUtilities.GetSignatureHelpState( + argumentList, position, + s_getTypeArgumentListOpenToken, + s_getTypeArgumentListCloseToken, + s_getTypeArgumentListArgumentsWithSeparators, + s_getTypeArgumentListNames); + } - internal static SignatureHelpState? GetSignatureHelpState(InitializerExpressionSyntax argumentList, int position) - { - return CommonSignatureHelpUtilities.GetSignatureHelpState( - argumentList, position, - s_getInitializerExpressionOpenToken, - s_getInitializerExpressionCloseToken, - s_getInitializerExpressionArgumentsWithSeparators, - s_getInitializerExpressionNames); - } + internal static SignatureHelpState? GetSignatureHelpState(InitializerExpressionSyntax argumentList, int position) + { + return CommonSignatureHelpUtilities.GetSignatureHelpState( + argumentList, position, + s_getInitializerExpressionOpenToken, + s_getInitializerExpressionCloseToken, + s_getInitializerExpressionArgumentsWithSeparators, + s_getInitializerExpressionNames); + } - internal static SignatureHelpState? GetSignatureHelpState(AttributeArgumentListSyntax argumentList, int position) - { - return CommonSignatureHelpUtilities.GetSignatureHelpState( - argumentList, position, - s_getAttributeArgumentListOpenToken, - s_getAttributeArgumentListCloseToken, - s_getAttributeArgumentListArgumentsWithSeparators, - s_getAttributeArgumentListNames); - } + internal static SignatureHelpState? GetSignatureHelpState(AttributeArgumentListSyntax argumentList, int position) + { + return CommonSignatureHelpUtilities.GetSignatureHelpState( + argumentList, position, + s_getAttributeArgumentListOpenToken, + s_getAttributeArgumentListCloseToken, + s_getAttributeArgumentListArgumentsWithSeparators, + s_getAttributeArgumentListNames); + } - internal static TextSpan GetSignatureHelpSpan(BaseArgumentListSyntax argumentList) - => CommonSignatureHelpUtilities.GetSignatureHelpSpan(argumentList, s_getBaseArgumentListCloseToken); + internal static TextSpan GetSignatureHelpSpan(BaseArgumentListSyntax argumentList) + => CommonSignatureHelpUtilities.GetSignatureHelpSpan(argumentList, s_getBaseArgumentListCloseToken); - internal static TextSpan GetSignatureHelpSpan(TypeArgumentListSyntax argumentList) - => CommonSignatureHelpUtilities.GetSignatureHelpSpan(argumentList, s_getTypeArgumentListCloseToken); + internal static TextSpan GetSignatureHelpSpan(TypeArgumentListSyntax argumentList) + => CommonSignatureHelpUtilities.GetSignatureHelpSpan(argumentList, s_getTypeArgumentListCloseToken); - internal static TextSpan GetSignatureHelpSpan(InitializerExpressionSyntax initializer) - => CommonSignatureHelpUtilities.GetSignatureHelpSpan(initializer, initializer.SpanStart, s_getInitializerExpressionCloseToken); + internal static TextSpan GetSignatureHelpSpan(InitializerExpressionSyntax initializer) + => CommonSignatureHelpUtilities.GetSignatureHelpSpan(initializer, initializer.SpanStart, s_getInitializerExpressionCloseToken); - internal static TextSpan GetSignatureHelpSpan(AttributeArgumentListSyntax argumentList) - => CommonSignatureHelpUtilities.GetSignatureHelpSpan(argumentList, s_getAttributeArgumentListCloseToken); + internal static TextSpan GetSignatureHelpSpan(AttributeArgumentListSyntax argumentList) + => CommonSignatureHelpUtilities.GetSignatureHelpSpan(argumentList, s_getAttributeArgumentListCloseToken); - internal static bool IsTriggerParenOrComma(SyntaxToken token, Func isTriggerCharacter) where TSyntaxNode : SyntaxNode + internal static bool IsTriggerParenOrComma(SyntaxToken token, Func isTriggerCharacter) where TSyntaxNode : SyntaxNode + { + // Don't dismiss if the user types ( to start a parenthesized expression or tuple + // Note that the tuple initially parses as a parenthesized expression + if (token.IsKind(SyntaxKind.OpenParenToken) && + token.Parent is ParenthesizedExpressionSyntax parenExpr) { - // Don't dismiss if the user types ( to start a parenthesized expression or tuple - // Note that the tuple initially parses as a parenthesized expression - if (token.IsKind(SyntaxKind.OpenParenToken) && - token.Parent is ParenthesizedExpressionSyntax parenExpr) + var parenthesizedExpr = parenExpr.WalkUpParentheses(); + if (parenthesizedExpr.Parent is ArgumentSyntax) { - var parenthesizedExpr = parenExpr.WalkUpParentheses(); - if (parenthesizedExpr.Parent is ArgumentSyntax) + var parent = parenthesizedExpr.Parent; + var grandParent = parent.Parent; + if (grandParent is ArgumentListSyntax && grandParent.Parent is TSyntaxNode) { - var parent = parenthesizedExpr.Parent; - var grandParent = parent.Parent; - if (grandParent is ArgumentListSyntax && grandParent.Parent is TSyntaxNode) - { - // Argument to TSyntaxNode's argument list - return true; - } - else - { - // Argument to a tuple in TSyntaxNode's argument list - return grandParent is TupleExpressionSyntax && parenthesizedExpr.GetAncestor() != null; - } + // Argument to TSyntaxNode's argument list + return true; } else { - // Not an argument - return false; + // Argument to a tuple in TSyntaxNode's argument list + return grandParent is TupleExpressionSyntax && parenthesizedExpr.GetAncestor() != null; } } - - // Don't dismiss if the user types ',' to add a member to a tuple - if (token.IsKind(SyntaxKind.CommaToken) && token.Parent is TupleExpressionSyntax && token.GetAncestor() != null) + else { - return true; + // Not an argument + return false; } + } - return !token.IsKind(SyntaxKind.None) && - token.ValueText.Length == 1 && - isTriggerCharacter(token.ValueText[0]) && - token.Parent is ArgumentListSyntax && - token.Parent.Parent is TSyntaxNode; + // Don't dismiss if the user types ',' to add a member to a tuple + if (token.IsKind(SyntaxKind.CommaToken) && token.Parent is TupleExpressionSyntax && token.GetAncestor() != null) + { + return true; } + + return !token.IsKind(SyntaxKind.None) && + token.ValueText.Length == 1 && + isTriggerCharacter(token.ValueText[0]) && + token.Parent is ArgumentListSyntax && + token.Parent.Parent is TSyntaxNode; } } diff --git a/src/Features/CSharp/Portable/SignatureHelp/TupleConstructionSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/TupleConstructionSignatureHelpProvider.cs index 7b79ce7da6c5b..46e5ea5c11db1 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/TupleConstructionSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/TupleConstructionSignatureHelpProvider.cs @@ -18,219 +18,218 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp; + +[ExportSignatureHelpProvider("TupleSignatureHelpProvider", LanguageNames.CSharp), Shared] +internal class TupleConstructionSignatureHelpProvider : AbstractCSharpSignatureHelpProvider { - [ExportSignatureHelpProvider("TupleSignatureHelpProvider", LanguageNames.CSharp), Shared] - internal class TupleConstructionSignatureHelpProvider : AbstractCSharpSignatureHelpProvider + private static readonly Func s_getOpenToken = e => e.OpenParenToken; + private static readonly Func s_getCloseToken = e => e.CloseParenToken; + private static readonly Func s_getArgumentsWithSeparators = e => e.Arguments.GetWithSeparators(); + private static readonly Func> s_getArgumentNames = e => e.Arguments.Select(a => a.NameColon?.Name.Identifier.ValueText ?? string.Empty); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public TupleConstructionSignatureHelpProvider() + { + } + + private SignatureHelpState? GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken) { - private static readonly Func s_getOpenToken = e => e.OpenParenToken; - private static readonly Func s_getCloseToken = e => e.CloseParenToken; - private static readonly Func s_getArgumentsWithSeparators = e => e.Arguments.GetWithSeparators(); - private static readonly Func> s_getArgumentNames = e => e.Arguments.Select(a => a.NameColon?.Name.Identifier.ValueText ?? string.Empty); - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public TupleConstructionSignatureHelpProvider() + if (GetOuterMostTupleExpressionInSpan(root, position, syntaxFacts, currentSpan, cancellationToken, out var expression)) { + return CommonSignatureHelpUtilities.GetSignatureHelpState(expression, position, + getOpenToken: s_getOpenToken, + getCloseToken: s_getCloseToken, + getArgumentsWithSeparators: s_getArgumentsWithSeparators, + getArgumentNames: s_getArgumentNames); } - private SignatureHelpState? GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken) + if (GetOuterMostParenthesizedExpressionInSpan(root, position, syntaxFacts, currentSpan, cancellationToken, out var parenthesizedExpression)) { - if (GetOuterMostTupleExpressionInSpan(root, position, syntaxFacts, currentSpan, cancellationToken, out var expression)) + if (currentSpan.Start == parenthesizedExpression.SpanStart) { - return CommonSignatureHelpUtilities.GetSignatureHelpState(expression, position, - getOpenToken: s_getOpenToken, - getCloseToken: s_getCloseToken, - getArgumentsWithSeparators: s_getArgumentsWithSeparators, - getArgumentNames: s_getArgumentNames); + return new SignatureHelpState( + argumentIndex: 0, + argumentCount: 0, + argumentName: string.Empty, + argumentNames: default); } - - if (GetOuterMostParenthesizedExpressionInSpan(root, position, syntaxFacts, currentSpan, cancellationToken, out var parenthesizedExpression)) - { - if (currentSpan.Start == parenthesizedExpression.SpanStart) - { - return new SignatureHelpState( - argumentIndex: 0, - argumentCount: 0, - argumentName: string.Empty, - argumentNames: default); - } - } - - return null; } - private bool GetOuterMostTupleExpressionInSpan(SyntaxNode root, int position, - ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken, [NotNullWhen(true)] out TupleExpressionSyntax? result) + return null; + } + + private bool GetOuterMostTupleExpressionInSpan(SyntaxNode root, int position, + ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken, [NotNullWhen(true)] out TupleExpressionSyntax? result) + { + result = null; + while (TryGetTupleExpression(SignatureHelpTriggerReason.InvokeSignatureHelpCommand, + root, position, syntaxFacts, cancellationToken, out var expression)) { - result = null; - while (TryGetTupleExpression(SignatureHelpTriggerReason.InvokeSignatureHelpCommand, - root, position, syntaxFacts, cancellationToken, out var expression)) + if (!currentSpan.Contains(expression.Span)) { - if (!currentSpan.Contains(expression.Span)) - { - break; - } - - result = expression; - position = expression.SpanStart; + break; } - return result != null; + result = expression; + position = expression.SpanStart; } - private bool GetOuterMostParenthesizedExpressionInSpan(SyntaxNode root, int position, - ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken, [NotNullWhen(true)] out ParenthesizedExpressionSyntax? result) + return result != null; + } + + private bool GetOuterMostParenthesizedExpressionInSpan(SyntaxNode root, int position, + ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken, [NotNullWhen(true)] out ParenthesizedExpressionSyntax? result) + { + result = null; + while (TryGetParenthesizedExpression(SignatureHelpTriggerReason.InvokeSignatureHelpCommand, + root, position, syntaxFacts, cancellationToken, out var expression)) { - result = null; - while (TryGetParenthesizedExpression(SignatureHelpTriggerReason.InvokeSignatureHelpCommand, - root, position, syntaxFacts, cancellationToken, out var expression)) + if (!currentSpan.Contains(expression.Span)) { - if (!currentSpan.Contains(expression.Span)) - { - break; - } - - result = expression; - position = expression.SpanStart; + break; } - return result != null; + result = expression; + position = expression.SpanStart; } - public override Boolean IsRetriggerCharacter(Char ch) - => ch == ')'; + return result != null; + } - public override Boolean IsTriggerCharacter(Char ch) - => ch is '(' or ','; + public override Boolean IsRetriggerCharacter(Char ch) + => ch == ')'; - protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + public override Boolean IsTriggerCharacter(Char ch) + => ch is '(' or ','; - var syntaxFacts = document.GetRequiredLanguageService(); - var typeInferrer = document.GetRequiredLanguageService(); - var inferredTypes = FindNearestTupleConstructionWithInferrableType(root, semanticModel, position, triggerInfo, - typeInferrer, syntaxFacts, cancellationToken, out var targetExpression); + protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, SignatureHelpOptions options, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (inferredTypes == null || !inferredTypes.Any()) - { - return null; - } + var syntaxFacts = document.GetRequiredLanguageService(); + var typeInferrer = document.GetRequiredLanguageService(); + var inferredTypes = FindNearestTupleConstructionWithInferrableType(root, semanticModel, position, triggerInfo, + typeInferrer, syntaxFacts, cancellationToken, out var targetExpression); - return CreateItems(position, root, syntaxFacts, targetExpression!, semanticModel, inferredTypes, cancellationToken); + if (inferredTypes == null || !inferredTypes.Any()) + { + return null; } - private IEnumerable? FindNearestTupleConstructionWithInferrableType(SyntaxNode root, SemanticModel semanticModel, int position, SignatureHelpTriggerInfo triggerInfo, - ITypeInferenceService typeInferrer, ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken, out ExpressionSyntax? targetExpression) - { - // Walk upward through TupleExpressionSyntax/ParenthsizedExpressionSyntax looking for a - // place where we can infer a tuple type. - ParenthesizedExpressionSyntax? parenthesizedExpression = null; - while (TryGetTupleExpression(triggerInfo.TriggerReason, root, position, syntaxFacts, cancellationToken, out var tupleExpression) || - TryGetParenthesizedExpression(triggerInfo.TriggerReason, root, position, syntaxFacts, cancellationToken, out parenthesizedExpression)) - { - targetExpression = (ExpressionSyntax?)tupleExpression ?? parenthesizedExpression; - var inferredTypes = typeInferrer.InferTypes(semanticModel, targetExpression!.SpanStart, cancellationToken); + return CreateItems(position, root, syntaxFacts, targetExpression!, semanticModel, inferredTypes, cancellationToken); + } - var tupleTypes = inferredTypes.Where(t => t.IsTupleType).OfType().ToList(); - if (tupleTypes.Any()) - { - return tupleTypes; - } + private IEnumerable? FindNearestTupleConstructionWithInferrableType(SyntaxNode root, SemanticModel semanticModel, int position, SignatureHelpTriggerInfo triggerInfo, + ITypeInferenceService typeInferrer, ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken, out ExpressionSyntax? targetExpression) + { + // Walk upward through TupleExpressionSyntax/ParenthsizedExpressionSyntax looking for a + // place where we can infer a tuple type. + ParenthesizedExpressionSyntax? parenthesizedExpression = null; + while (TryGetTupleExpression(triggerInfo.TriggerReason, root, position, syntaxFacts, cancellationToken, out var tupleExpression) || + TryGetParenthesizedExpression(triggerInfo.TriggerReason, root, position, syntaxFacts, cancellationToken, out parenthesizedExpression)) + { + targetExpression = (ExpressionSyntax?)tupleExpression ?? parenthesizedExpression; + var inferredTypes = typeInferrer.InferTypes(semanticModel, targetExpression!.SpanStart, cancellationToken); - position = targetExpression.GetFirstToken().SpanStart; + var tupleTypes = inferredTypes.Where(t => t.IsTupleType).OfType().ToList(); + if (tupleTypes.Any()) + { + return tupleTypes; } - targetExpression = null; - return null; + position = targetExpression.GetFirstToken().SpanStart; } - private SignatureHelpItems? CreateItems(int position, SyntaxNode root, ISyntaxFactsService syntaxFacts, - SyntaxNode targetExpression, SemanticModel semanticModel, IEnumerable tupleTypes, CancellationToken cancellationToken) - { - var prefixParts = SpecializedCollections.SingletonEnumerable(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, "(")).ToTaggedText(); - var suffixParts = SpecializedCollections.SingletonEnumerable(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, ")")).ToTaggedText(); - var separatorParts = GetSeparatorParts().ToTaggedText(); + targetExpression = null; + return null; + } - var items = tupleTypes.Select(tupleType => Convert( - tupleType, prefixParts, suffixParts, separatorParts, semanticModel, position)) - .ToList(); + private SignatureHelpItems? CreateItems(int position, SyntaxNode root, ISyntaxFactsService syntaxFacts, + SyntaxNode targetExpression, SemanticModel semanticModel, IEnumerable tupleTypes, CancellationToken cancellationToken) + { + var prefixParts = SpecializedCollections.SingletonEnumerable(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, "(")).ToTaggedText(); + var suffixParts = SpecializedCollections.SingletonEnumerable(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, ")")).ToTaggedText(); + var separatorParts = GetSeparatorParts().ToTaggedText(); - var state = GetCurrentArgumentState(root, position, syntaxFacts, targetExpression.FullSpan, cancellationToken); - return CreateSignatureHelpItems(items, targetExpression.Span, state, selectedItemIndex: null, parameterIndexOverride: -1); - } + var items = tupleTypes.Select(tupleType => Convert( + tupleType, prefixParts, suffixParts, separatorParts, semanticModel, position)) + .ToList(); - private static SignatureHelpItem Convert(INamedTypeSymbol tupleType, ImmutableArray prefixParts, ImmutableArray suffixParts, - ImmutableArray separatorParts, SemanticModel semanticModel, int position) - { - return new SymbolKeySignatureHelpItem( - symbol: tupleType, - isVariadic: false, - documentationFactory: null, - prefixParts: prefixParts, - separatorParts: separatorParts, - suffixParts: suffixParts, - parameters: ConvertTupleMembers(tupleType, semanticModel, position), - descriptionParts: null); - } + var state = GetCurrentArgumentState(root, position, syntaxFacts, targetExpression.FullSpan, cancellationToken); + return CreateSignatureHelpItems(items, targetExpression.Span, state, selectedItemIndex: null, parameterIndexOverride: -1); + } - private static IEnumerable ConvertTupleMembers(INamedTypeSymbol tupleType, SemanticModel semanticModel, int position) + private static SignatureHelpItem Convert(INamedTypeSymbol tupleType, ImmutableArray prefixParts, ImmutableArray suffixParts, + ImmutableArray separatorParts, SemanticModel semanticModel, int position) + { + return new SymbolKeySignatureHelpItem( + symbol: tupleType, + isVariadic: false, + documentationFactory: null, + prefixParts: prefixParts, + separatorParts: separatorParts, + suffixParts: suffixParts, + parameters: ConvertTupleMembers(tupleType, semanticModel, position), + descriptionParts: null); + } + + private static IEnumerable ConvertTupleMembers(INamedTypeSymbol tupleType, SemanticModel semanticModel, int position) + { + var spacePart = Space(); + var result = new List(); + foreach (var element in tupleType.TupleElements) { - var spacePart = Space(); - var result = new List(); - foreach (var element in tupleType.TupleElements) + // The display name for each element. + // Empty strings for elements not explicitly declared + var elementName = element.IsImplicitlyDeclared ? string.Empty : element.Name; + + var typeParts = element.Type.ToMinimalDisplayParts(semanticModel, position).ToList(); + if (!string.IsNullOrEmpty(elementName)) { - // The display name for each element. - // Empty strings for elements not explicitly declared - var elementName = element.IsImplicitlyDeclared ? string.Empty : element.Name; - - var typeParts = element.Type.ToMinimalDisplayParts(semanticModel, position).ToList(); - if (!string.IsNullOrEmpty(elementName)) - { - typeParts.Add(spacePart); - typeParts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.PropertyName, null, elementName)); - } - - result.Add(new SignatureHelpParameter(name: string.Empty, isOptional: false, documentationFactory: null, displayParts: typeParts)); + typeParts.Add(spacePart); + typeParts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.PropertyName, null, elementName)); } - return result; + result.Add(new SignatureHelpParameter(name: string.Empty, isOptional: false, documentationFactory: null, displayParts: typeParts)); } - private bool TryGetTupleExpression(SignatureHelpTriggerReason triggerReason, SyntaxNode root, int position, - ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken, [NotNullWhen(true)] out TupleExpressionSyntax? tupleExpression) - { - return CommonSignatureHelpUtilities.TryGetSyntax(root, position, syntaxFacts, triggerReason, IsTupleExpressionTriggerToken, - IsTupleArgumentListToken, cancellationToken, out tupleExpression); - } + return result; + } - private bool IsTupleExpressionTriggerToken(SyntaxToken token) - => SignatureHelpUtilities.IsTriggerParenOrComma(token, IsTriggerCharacter); + private bool TryGetTupleExpression(SignatureHelpTriggerReason triggerReason, SyntaxNode root, int position, + ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken, [NotNullWhen(true)] out TupleExpressionSyntax? tupleExpression) + { + return CommonSignatureHelpUtilities.TryGetSyntax(root, position, syntaxFacts, triggerReason, IsTupleExpressionTriggerToken, + IsTupleArgumentListToken, cancellationToken, out tupleExpression); + } - private static bool IsTupleArgumentListToken(TupleExpressionSyntax? tupleExpression, SyntaxToken token) - { - return tupleExpression != null && - tupleExpression.Arguments.FullSpan.Contains(token.SpanStart) && - token != tupleExpression.CloseParenToken; - } + private bool IsTupleExpressionTriggerToken(SyntaxToken token) + => SignatureHelpUtilities.IsTriggerParenOrComma(token, IsTriggerCharacter); - private bool TryGetParenthesizedExpression(SignatureHelpTriggerReason triggerReason, SyntaxNode root, int position, - ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken, [NotNullWhen(true)] out ParenthesizedExpressionSyntax? parenthesizedExpression) - { - return CommonSignatureHelpUtilities.TryGetSyntax(root, position, syntaxFacts, triggerReason, - IsParenthesizedExpressionTriggerToken, IsParenthesizedExpressionToken, cancellationToken, out parenthesizedExpression); - } + private static bool IsTupleArgumentListToken(TupleExpressionSyntax? tupleExpression, SyntaxToken token) + { + return tupleExpression != null && + tupleExpression.Arguments.FullSpan.Contains(token.SpanStart) && + token != tupleExpression.CloseParenToken; + } - private bool IsParenthesizedExpressionTriggerToken(SyntaxToken token) - => token.IsKind(SyntaxKind.OpenParenToken) && token.Parent is ParenthesizedExpressionSyntax; + private bool TryGetParenthesizedExpression(SignatureHelpTriggerReason triggerReason, SyntaxNode root, int position, + ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken, [NotNullWhen(true)] out ParenthesizedExpressionSyntax? parenthesizedExpression) + { + return CommonSignatureHelpUtilities.TryGetSyntax(root, position, syntaxFacts, triggerReason, + IsParenthesizedExpressionTriggerToken, IsParenthesizedExpressionToken, cancellationToken, out parenthesizedExpression); + } - private static bool IsParenthesizedExpressionToken(ParenthesizedExpressionSyntax? expr, SyntaxToken token) - { - return expr != null && - expr.FullSpan.Contains(token.SpanStart) && - token != expr.CloseParenToken; - } + private bool IsParenthesizedExpressionTriggerToken(SyntaxToken token) + => token.IsKind(SyntaxKind.OpenParenToken) && token.Parent is ParenthesizedExpressionSyntax; + + private static bool IsParenthesizedExpressionToken(ParenthesizedExpressionSyntax? expr, SyntaxToken token) + { + return expr != null && + expr.FullSpan.Contains(token.SpanStart) && + token != expr.CloseParenToken; } } diff --git a/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeCodeFixProvider.cs b/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeCodeFixProvider.cs index 73290b9af15e0..4e24f230b0011 100644 --- a/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeCodeFixProvider.cs @@ -10,33 +10,32 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.SimplifyThisOrMe; -namespace Microsoft.CodeAnalysis.CSharp.SimplifyThisOrMe +namespace Microsoft.CodeAnalysis.CSharp.SimplifyThisOrMe; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SimplifyThisOrMe), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.RemoveUnnecessaryCast)] +internal partial class CSharpSimplifyThisOrMeCodeFixProvider + : AbstractSimplifyThisOrMeCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SimplifyThisOrMe), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.RemoveUnnecessaryCast)] - internal partial class CSharpSimplifyThisOrMeCodeFixProvider - : AbstractSimplifyThisOrMeCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpSimplifyThisOrMeCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpSimplifyThisOrMeCodeFixProvider() - { - } + } - protected override string GetTitle() - => CSharpFeaturesResources.Remove_this_qualification; + protected override string GetTitle() + => CSharpFeaturesResources.Remove_this_qualification; - protected override SyntaxNode Rewrite(SyntaxNode root, ISet memberAccessNodes) - => new Rewriter(memberAccessNodes).Visit(root); + protected override SyntaxNode Rewrite(SyntaxNode root, ISet memberAccessNodes) + => new Rewriter(memberAccessNodes).Visit(root); - private class Rewriter(ISet memberAccessNodes) : CSharpSyntaxRewriter - { - private readonly ISet _memberAccessNodes = memberAccessNodes; + private class Rewriter(ISet memberAccessNodes) : CSharpSyntaxRewriter + { + private readonly ISet _memberAccessNodes = memberAccessNodes; - public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node) - => _memberAccessNodes.Contains(node) - ? node.GetNameWithTriviaMoved() - : base.VisitMemberAccessExpression(node); - } + public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node) + => _memberAccessNodes.Contains(node) + ? node.GetNameWithTriviaMoved() + : base.VisitMemberAccessExpression(node); } } diff --git a/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeDiagnosticAnalyzer.cs index c7eb7fe8d2ae5..84123695e8202 100644 --- a/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeDiagnosticAnalyzer.cs +++ b/src/Features/CSharp/Portable/SimplifyThisOrMe/CSharpSimplifyThisOrMeDiagnosticAnalyzer.cs @@ -12,22 +12,21 @@ using Microsoft.CodeAnalysis.Simplification.Simplifiers; using Microsoft.CodeAnalysis.SimplifyThisOrMe; -namespace Microsoft.CodeAnalysis.CSharp.SimplifyThisOrMe +namespace Microsoft.CodeAnalysis.CSharp.SimplifyThisOrMe; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpSimplifyThisOrMeDiagnosticAnalyzer + : AbstractSimplifyThisOrMeDiagnosticAnalyzer< + SyntaxKind, + ExpressionSyntax, + ThisExpressionSyntax, + MemberAccessExpressionSyntax> { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpSimplifyThisOrMeDiagnosticAnalyzer - : AbstractSimplifyThisOrMeDiagnosticAnalyzer< - SyntaxKind, - ExpressionSyntax, - ThisExpressionSyntax, - MemberAccessExpressionSyntax> - { - protected override ISyntaxKinds SyntaxKinds => CSharpSyntaxKinds.Instance; + protected override ISyntaxKinds SyntaxKinds => CSharpSyntaxKinds.Instance; - protected override ISimplification Simplification - => CSharpSimplification.Instance; + protected override ISimplification Simplification + => CSharpSimplification.Instance; - protected override AbstractMemberAccessExpressionSimplifier Simplifier - => MemberAccessExpressionSimplifier.Instance; - } + protected override AbstractMemberAccessExpressionSimplifier Simplifier + => MemberAccessExpressionSimplifier.Instance; } diff --git a/src/Features/CSharp/Portable/SimplifyTypeNames/SimplifyTypeNamesCodeFixProvider.cs b/src/Features/CSharp/Portable/SimplifyTypeNames/SimplifyTypeNamesCodeFixProvider.cs index 920ecb64e4b36..1bda964715cf1 100644 --- a/src/Features/CSharp/Portable/SimplifyTypeNames/SimplifyTypeNamesCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/SimplifyTypeNames/SimplifyTypeNamesCodeFixProvider.cs @@ -16,51 +16,50 @@ using Microsoft.CodeAnalysis.SimplifyTypeNames; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.SimplifyTypeNames +namespace Microsoft.CodeAnalysis.CSharp.SimplifyTypeNames; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SimplifyNames), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.RemoveUnnecessaryCast)] +internal partial class SimplifyTypeNamesCodeFixProvider : AbstractSimplifyTypeNamesCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SimplifyNames), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.RemoveUnnecessaryCast)] - internal partial class SimplifyTypeNamesCodeFixProvider : AbstractSimplifyTypeNamesCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public SimplifyTypeNamesCodeFixProvider() + : base(new CSharpSimplifyTypeNamesDiagnosticAnalyzer()) { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public SimplifyTypeNamesCodeFixProvider() - : base(new CSharpSimplifyTypeNamesDiagnosticAnalyzer()) - { - } + } - protected override string GetTitle(string diagnosticId, string nodeText) + protected override string GetTitle(string diagnosticId, string nodeText) + { + switch (diagnosticId) { - switch (diagnosticId) - { - case IDEDiagnosticIds.SimplifyNamesDiagnosticId: - case IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId: - return string.Format(CSharpFeaturesResources.Simplify_name_0, nodeText); + case IDEDiagnosticIds.SimplifyNamesDiagnosticId: + case IDEDiagnosticIds.PreferBuiltInOrFrameworkTypeDiagnosticId: + return string.Format(CSharpFeaturesResources.Simplify_name_0, nodeText); - case IDEDiagnosticIds.SimplifyMemberAccessDiagnosticId: - return string.Format(CSharpFeaturesResources.Simplify_member_access_0, nodeText); + case IDEDiagnosticIds.SimplifyMemberAccessDiagnosticId: + return string.Format(CSharpFeaturesResources.Simplify_member_access_0, nodeText); - default: - throw ExceptionUtilities.UnexpectedValue(diagnosticId); - } + default: + throw ExceptionUtilities.UnexpectedValue(diagnosticId); } + } - protected override SyntaxNode AddSimplificationAnnotationTo(SyntaxNode expressionSyntax) - { - // Add the DoNotAllowVarAnnotation annotation. All the code fixer - // does is pass the tagged node to the simplifier. And we do *not* - // ever want the simplifier to produce 'var' in the 'Simplify type - // names' fixer. only the 'Use var' fixer should produce 'var'. - var annotatedexpressionSyntax = expressionSyntax.WithAdditionalAnnotations( - Simplifier.Annotation, Formatter.Annotation, DoNotAllowVarAnnotation.Annotation); - - if (annotatedexpressionSyntax.Kind() is SyntaxKind.IsExpression or SyntaxKind.AsExpression) - { - var right = ((BinaryExpressionSyntax)annotatedexpressionSyntax).Right; - annotatedexpressionSyntax = annotatedexpressionSyntax.ReplaceNode(right, right.WithAdditionalAnnotations(Simplifier.Annotation)); - } + protected override SyntaxNode AddSimplificationAnnotationTo(SyntaxNode expressionSyntax) + { + // Add the DoNotAllowVarAnnotation annotation. All the code fixer + // does is pass the tagged node to the simplifier. And we do *not* + // ever want the simplifier to produce 'var' in the 'Simplify type + // names' fixer. only the 'Use var' fixer should produce 'var'. + var annotatedexpressionSyntax = expressionSyntax.WithAdditionalAnnotations( + Simplifier.Annotation, Formatter.Annotation, DoNotAllowVarAnnotation.Annotation); - return annotatedexpressionSyntax; + if (annotatedexpressionSyntax.Kind() is SyntaxKind.IsExpression or SyntaxKind.AsExpression) + { + var right = ((BinaryExpressionSyntax)annotatedexpressionSyntax).Right; + annotatedexpressionSyntax = annotatedexpressionSyntax.ReplaceNode(right, right.WithAdditionalAnnotations(Simplifier.Annotation)); } + + return annotatedexpressionSyntax; } } diff --git a/src/Features/CSharp/Portable/Snippets/AbstractCSharpAutoPropertySnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/AbstractCSharpAutoPropertySnippetProvider.cs index 9f3eda115251a..bcc6c780bf9e7 100644 --- a/src/Features/CSharp/Portable/Snippets/AbstractCSharpAutoPropertySnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/AbstractCSharpAutoPropertySnippetProvider.cs @@ -20,75 +20,74 @@ using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +internal abstract class AbstractCSharpAutoPropertySnippetProvider : AbstractPropertySnippetProvider { - internal abstract class AbstractCSharpAutoPropertySnippetProvider : AbstractPropertySnippetProvider - { - protected virtual AccessorDeclarationSyntax? GenerateGetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator) - => (AccessorDeclarationSyntax)generator.GetAccessorDeclaration(); + protected virtual AccessorDeclarationSyntax? GenerateGetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator) + => (AccessorDeclarationSyntax)generator.GetAccessorDeclaration(); - protected virtual AccessorDeclarationSyntax? GenerateSetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator) - => (AccessorDeclarationSyntax)generator.SetAccessorDeclaration(); + protected virtual AccessorDeclarationSyntax? GenerateSetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator) + => (AccessorDeclarationSyntax)generator.SetAccessorDeclaration(); - protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) - { - return context.SyntaxContext.SyntaxTree.IsMemberDeclarationContext(context.Position, (CSharpSyntaxContext)context.SyntaxContext, - SyntaxKindSet.AllMemberModifiers, SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: true, cancellationToken); - } + protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) + { + return context.SyntaxContext.SyntaxTree.IsMemberDeclarationContext(context.Position, (CSharpSyntaxContext)context.SyntaxContext, + SyntaxKindSet.AllMemberModifiers, SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: true, cancellationToken); + } - protected override async Task GenerateSnippetSyntaxAsync(Document document, int position, CancellationToken cancellationToken) + protected override async Task GenerateSnippetSyntaxAsync(Document document, int position, CancellationToken cancellationToken) + { + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var generator = SyntaxGenerator.GetGenerator(document); + var identifierName = NameGenerator.GenerateUniqueName("MyProperty", + n => semanticModel.LookupSymbols(position, name: n).IsEmpty); + var syntaxContext = CSharpSyntaxContext.CreateContext(document, semanticModel, position, cancellationToken); + var accessors = new AccessorDeclarationSyntax?[] { - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var generator = SyntaxGenerator.GetGenerator(document); - var identifierName = NameGenerator.GenerateUniqueName("MyProperty", - n => semanticModel.LookupSymbols(position, name: n).IsEmpty); - var syntaxContext = CSharpSyntaxContext.CreateContext(document, semanticModel, position, cancellationToken); - var accessors = new AccessorDeclarationSyntax?[] - { - GenerateGetAccessorDeclaration(syntaxContext, generator), - GenerateSetAccessorDeclaration(syntaxContext, generator), - }; - - SyntaxTokenList modifiers = default; + GenerateGetAccessorDeclaration(syntaxContext, generator), + GenerateSetAccessorDeclaration(syntaxContext, generator), + }; - // If there are no preceding accessibility modifiers create default `public` one - if (!syntaxContext.PrecedingModifiers.Any(SyntaxFacts.IsAccessibilityModifier)) - { - modifiers = SyntaxTokenList.Create(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); - } + SyntaxTokenList modifiers = default; - return SyntaxFactory.PropertyDeclaration( - attributeLists: default, - modifiers: modifiers, - type: compilation.GetSpecialType(SpecialType.System_Int32).GenerateTypeSyntax(allowVar: false), - explicitInterfaceSpecifier: null, - identifier: identifierName.ToIdentifierToken(), - accessorList: SyntaxFactory.AccessorList([.. accessors.Where(a => a is not null)!])); - } - - protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + // If there are no preceding accessibility modifiers create default `public` one + if (!syntaxContext.PrecedingModifiers.Any(SyntaxFacts.IsAccessibilityModifier)) { - var propertyDeclaration = (PropertyDeclarationSyntax)caretTarget; - return propertyDeclaration.AccessorList!.CloseBraceToken.Span.End; + modifiers = SyntaxTokenList.Create(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); } - protected override ImmutableArray GetPlaceHolderLocationsList(SyntaxNode node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) - { - using var _ = ArrayBuilder.GetInstance(out var arrayBuilder); - var propertyDeclaration = (PropertyDeclarationSyntax)node; - var identifier = propertyDeclaration.Identifier; - var type = propertyDeclaration.Type; + return SyntaxFactory.PropertyDeclaration( + attributeLists: default, + modifiers: modifiers, + type: compilation.GetSpecialType(SpecialType.System_Int32).GenerateTypeSyntax(allowVar: false), + explicitInterfaceSpecifier: null, + identifier: identifierName.ToIdentifierToken(), + accessorList: SyntaxFactory.AccessorList([.. accessors.Where(a => a is not null)!])); + } - arrayBuilder.Add(new SnippetPlaceholder(type.ToString(), type.SpanStart)); - arrayBuilder.Add(new SnippetPlaceholder(identifier.ValueText, identifier.SpanStart)); - return arrayBuilder.ToImmutableArray(); - } + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + { + var propertyDeclaration = (PropertyDeclarationSyntax)caretTarget; + return propertyDeclaration.AccessorList!.CloseBraceToken.Span.End; + } - protected override SyntaxNode? FindAddedSnippetSyntaxNode(SyntaxNode root, int position, Func isCorrectContainer) - { - var node = root.FindNode(TextSpan.FromBounds(position, position)); - return node.GetAncestorOrThis(); - } + protected override ImmutableArray GetPlaceHolderLocationsList(SyntaxNode node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var arrayBuilder); + var propertyDeclaration = (PropertyDeclarationSyntax)node; + var identifier = propertyDeclaration.Identifier; + var type = propertyDeclaration.Type; + + arrayBuilder.Add(new SnippetPlaceholder(type.ToString(), type.SpanStart)); + arrayBuilder.Add(new SnippetPlaceholder(identifier.ValueText, identifier.SpanStart)); + return arrayBuilder.ToImmutableArray(); + } + + protected override SyntaxNode? FindAddedSnippetSyntaxNode(SyntaxNode root, int position, Func isCorrectContainer) + { + var node = root.FindNode(TextSpan.FromBounds(position, position)); + return node.GetAncestorOrThis(); } } diff --git a/src/Features/CSharp/Portable/Snippets/AbstractCSharpMainMethodSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/AbstractCSharpMainMethodSnippetProvider.cs index 576768d3ecba0..2a8cecf5d794a 100644 --- a/src/Features/CSharp/Portable/Snippets/AbstractCSharpMainMethodSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/AbstractCSharpMainMethodSnippetProvider.cs @@ -9,37 +9,36 @@ using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Snippets.SnippetProviders; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +internal abstract class AbstractCSharpMainMethodSnippetProvider : AbstractMainMethodSnippetProvider { - internal abstract class AbstractCSharpMainMethodSnippetProvider : AbstractMainMethodSnippetProvider + protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) { - protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) + var semanticModel = context.SyntaxContext.SemanticModel; + var syntaxContext = (CSharpSyntaxContext)context.SyntaxContext; + + if (!syntaxContext.IsMemberDeclarationContext( + validModifiers: SyntaxKindSet.AccessibilityModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: true, + cancellationToken: cancellationToken)) + { + return false; + } + + // Syntactically correct position, now semantic checks + + var enclosingTypeSymbol = semanticModel.GetDeclaredSymbol(syntaxContext.ContainingTypeDeclaration!, cancellationToken); + + // If there are any members with name `Main` in enclosing type, inserting `Main` method will create an error + if (enclosingTypeSymbol is not null && + !semanticModel.LookupSymbols(context.Position, container: enclosingTypeSymbol, name: WellKnownMemberNames.EntryPointMethodName).IsEmpty) { - var semanticModel = context.SyntaxContext.SemanticModel; - var syntaxContext = (CSharpSyntaxContext)context.SyntaxContext; - - if (!syntaxContext.IsMemberDeclarationContext( - validModifiers: SyntaxKindSet.AccessibilityModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: true, - cancellationToken: cancellationToken)) - { - return false; - } - - // Syntactically correct position, now semantic checks - - var enclosingTypeSymbol = semanticModel.GetDeclaredSymbol(syntaxContext.ContainingTypeDeclaration!, cancellationToken); - - // If there are any members with name `Main` in enclosing type, inserting `Main` method will create an error - if (enclosingTypeSymbol is not null && - !semanticModel.LookupSymbols(context.Position, container: enclosingTypeSymbol, name: WellKnownMemberNames.EntryPointMethodName).IsEmpty) - { - return false; - } - - // If compilation already has top-level statements, suppress showing `Main` method snippets - return semanticModel.Compilation.GetTopLevelStatementsMethod() is null; + return false; } + + // If compilation already has top-level statements, suppress showing `Main` method snippets + return semanticModel.Compilation.GetTopLevelStatementsMethod() is null; } } diff --git a/src/Features/CSharp/Portable/Snippets/AbstractCSharpTypeSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/AbstractCSharpTypeSnippetProvider.cs index 25f873de67efe..3f07f8c9a589f 100644 --- a/src/Features/CSharp/Portable/Snippets/AbstractCSharpTypeSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/AbstractCSharpTypeSnippetProvider.cs @@ -22,99 +22,98 @@ using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +internal abstract class AbstractCSharpTypeSnippetProvider : AbstractTypeSnippetProvider { - internal abstract class AbstractCSharpTypeSnippetProvider : AbstractTypeSnippetProvider - { - protected abstract ISet ValidModifiers { get; } + protected abstract ISet ValidModifiers { get; } - protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) - { - var syntaxContext = (CSharpSyntaxContext)context.SyntaxContext; - - return - syntaxContext.IsGlobalStatementContext || - syntaxContext.IsTypeDeclarationContext( - validModifiers: ValidModifiers, - validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, - canBePartial: true, - cancellationToken: cancellationToken); - } + protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) + { + var syntaxContext = (CSharpSyntaxContext)context.SyntaxContext; + + return + syntaxContext.IsGlobalStatementContext || + syntaxContext.IsTypeDeclarationContext( + validModifiers: ValidModifiers, + validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, + canBePartial: true, + cancellationToken: cancellationToken); + } - protected override async Task GetAccessibilityModifiersChangeAsync(Document document, int position, CancellationToken cancellationToken) - { - if (!await AreAccessibilityModifiersRequiredAsync(document, cancellationToken).ConfigureAwait(false)) - return null; + protected override async Task GetAccessibilityModifiersChangeAsync(Document document, int position, CancellationToken cancellationToken) + { + if (!await AreAccessibilityModifiersRequiredAsync(document, cancellationToken).ConfigureAwait(false)) + return null; - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (tree.GetPrecedingModifiers(position, cancellationToken).Any(SyntaxFacts.IsAccessibilityModifier)) - return null; + if (tree.GetPrecedingModifiers(position, cancellationToken).Any(SyntaxFacts.IsAccessibilityModifier)) + return null; - var targetToken = tree.FindTokenOnLeftOfPosition(position, cancellationToken).GetPreviousTokenIfTouchingWord(position); - var targetPosition = position; + var targetToken = tree.FindTokenOnLeftOfPosition(position, cancellationToken).GetPreviousTokenIfTouchingWord(position); + var targetPosition = position; - var analyzerOptionsProvider = await document.GetAnalyzerOptionsProviderAsync(cancellationToken).ConfigureAwait(false); - var preferredModifierOrderString = analyzerOptionsProvider.GetAnalyzerConfigOptions().GetOption(CSharpCodeStyleOptions.PreferredModifierOrder).Value; + var analyzerOptionsProvider = await document.GetAnalyzerOptionsProviderAsync(cancellationToken).ConfigureAwait(false); + var preferredModifierOrderString = analyzerOptionsProvider.GetAnalyzerConfigOptions().GetOption(CSharpCodeStyleOptions.PreferredModifierOrder).Value; - if (CSharpOrderModifiersHelper.Instance.TryGetOrComputePreferredOrder(preferredModifierOrderString, out var preferredOrder) && - preferredOrder.TryGetValue((int)SyntaxKind.PublicKeyword, out var publicModifierOrder)) + if (CSharpOrderModifiersHelper.Instance.TryGetOrComputePreferredOrder(preferredModifierOrderString, out var preferredOrder) && + preferredOrder.TryGetValue((int)SyntaxKind.PublicKeyword, out var publicModifierOrder)) + { + while (targetToken.IsPotentialModifier(out var modifierKind)) { - while (targetToken.IsPotentialModifier(out var modifierKind)) + if (preferredOrder.TryGetValue((int)modifierKind, out var targetTokenOrder) && + targetTokenOrder > publicModifierOrder) { - if (preferredOrder.TryGetValue((int)modifierKind, out var targetTokenOrder) && - targetTokenOrder > publicModifierOrder) - { - targetPosition = targetToken.SpanStart; - } - - targetToken = targetToken.GetPreviousToken(); + targetPosition = targetToken.SpanStart; } + + targetToken = targetToken.GetPreviousToken(); } + } - // If we are right after 'partial' token we need to insert modifier before it - targetPosition = targetToken.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword) ? targetToken.SpanStart : targetPosition; + // If we are right after 'partial' token we need to insert modifier before it + targetPosition = targetToken.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword) ? targetToken.SpanStart : targetPosition; - return new TextChange(TextSpan.FromBounds(targetPosition, targetPosition), SyntaxFacts.GetText(SyntaxKind.PublicKeyword) + " "); - } + return new TextChange(TextSpan.FromBounds(targetPosition, targetPosition), SyntaxFacts.GetText(SyntaxKind.PublicKeyword) + " "); + } - protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) - { - var typeDeclaration = (BaseTypeDeclarationSyntax)caretTarget; - var triviaSpan = typeDeclaration.CloseBraceToken.LeadingTrivia.Span; - var line = sourceText.Lines.GetLineFromPosition(triviaSpan.Start); - // Getting the location at the end of the line before the newline. - return line.Span.End; - } + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + { + var typeDeclaration = (BaseTypeDeclarationSyntax)caretTarget; + var triviaSpan = typeDeclaration.CloseBraceToken.LeadingTrivia.Span; + var line = sourceText.Lines.GetLineFromPosition(triviaSpan.Start); + // Getting the location at the end of the line before the newline. + return line.Span.End; + } - protected override SyntaxNode? FindAddedSnippetSyntaxNode(SyntaxNode root, int position, Func isCorrectContainer) - { - var node = root.FindNode(TextSpan.FromBounds(position, position)); - return node.GetAncestorOrThis(); - } + protected override SyntaxNode? FindAddedSnippetSyntaxNode(SyntaxNode root, int position, Func isCorrectContainer) + { + var node = root.FindNode(TextSpan.FromBounds(position, position)); + return node.GetAncestorOrThis(); + } - protected override async Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var snippet = root.GetAnnotatedNodes(FindSnippetAnnotation).FirstOrDefault(); + protected override async Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var snippet = root.GetAnnotatedNodes(FindSnippetAnnotation).FirstOrDefault(); - if (snippet is not BaseTypeDeclarationSyntax originalTypeDeclaration) - return document; + if (snippet is not BaseTypeDeclarationSyntax originalTypeDeclaration) + return document; - var syntaxFormattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions: null, cancellationToken).ConfigureAwait(false); - var indentationString = CSharpSnippetHelpers.GetBlockLikeIndentationString(document, originalTypeDeclaration.OpenBraceToken.SpanStart, syntaxFormattingOptions, cancellationToken); + var syntaxFormattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions: null, cancellationToken).ConfigureAwait(false); + var indentationString = CSharpSnippetHelpers.GetBlockLikeIndentationString(document, originalTypeDeclaration.OpenBraceToken.SpanStart, syntaxFormattingOptions, cancellationToken); - var newTypeDeclaration = originalTypeDeclaration.WithCloseBraceToken( - originalTypeDeclaration.CloseBraceToken.WithPrependedLeadingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, indentationString))); + var newTypeDeclaration = originalTypeDeclaration.WithCloseBraceToken( + originalTypeDeclaration.CloseBraceToken.WithPrependedLeadingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, indentationString))); - var newRoot = root.ReplaceNode(originalTypeDeclaration, newTypeDeclaration.WithAdditionalAnnotations(CursorAnnotation, FindSnippetAnnotation)); - return document.WithSyntaxRoot(newRoot); - } + var newRoot = root.ReplaceNode(originalTypeDeclaration, newTypeDeclaration.WithAdditionalAnnotations(CursorAnnotation, FindSnippetAnnotation)); + return document.WithSyntaxRoot(newRoot); + } - protected override void GetTypeDeclarationIdentifier(SyntaxNode node, out SyntaxToken identifier) - { - var typeDeclaration = (BaseTypeDeclarationSyntax)node; - identifier = typeDeclaration.Identifier; - } + protected override void GetTypeDeclarationIdentifier(SyntaxNode node, out SyntaxToken identifier) + { + var typeDeclaration = (BaseTypeDeclarationSyntax)node; + identifier = typeDeclaration.Identifier; } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpClassSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpClassSnippetProvider.cs index 7fbe0e154eedb..60f1e7e449909 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpClassSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpClassSnippetProvider.cs @@ -15,49 +15,48 @@ using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Snippets.SnippetProviders; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpClassSnippetProvider : AbstractCSharpTypeSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpClassSnippetProvider : AbstractCSharpTypeSnippetProvider + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.NewKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.AbstractKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.FileKeyword, + }; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpClassSnippetProvider() + { + } + + public override string Identifier => "class"; + + public override string Description => FeaturesResources.class_; + + protected override ISet ValidModifiers => s_validModifiers; + + protected override async Task GenerateTypeDeclarationAsync(Document document, int position, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var name = NameGenerator.GenerateUniqueName("MyClass", name => semanticModel.LookupSymbols(position, name: name).IsEmpty); + return generator.ClassDeclaration(name); + } + + protected override Func GetSnippetContainerFunction(ISyntaxFacts syntaxFacts) { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.NewKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.AbstractKeyword, - SyntaxKind.SealedKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.FileKeyword, - }; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpClassSnippetProvider() - { - } - - public override string Identifier => "class"; - - public override string Description => FeaturesResources.class_; - - protected override ISet ValidModifiers => s_validModifiers; - - protected override async Task GenerateTypeDeclarationAsync(Document document, int position, CancellationToken cancellationToken) - { - var generator = SyntaxGenerator.GetGenerator(document); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var name = NameGenerator.GenerateUniqueName("MyClass", name => semanticModel.LookupSymbols(position, name: name).IsEmpty); - return generator.ClassDeclaration(name); - } - - protected override Func GetSnippetContainerFunction(ISyntaxFacts syntaxFacts) - { - return syntaxFacts.IsClassDeclaration; - } + return syntaxFacts.IsClassDeclaration; } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpConsoleSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpConsoleSnippetProvider.cs index 9921040c6380f..e4c00b087dd43 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpConsoleSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpConsoleSnippetProvider.cs @@ -23,21 +23,20 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpConsoleSnippetProvider : AbstractConsoleSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpConsoleSnippetProvider : AbstractConsoleSnippetProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpConsoleSnippetProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpConsoleSnippetProvider() - { - } + } - protected override SyntaxNode? GetAsyncSupportingDeclaration(SyntaxToken token) - { - var node = token.GetAncestor(node => node.IsAsyncSupportingFunctionSyntax()); - return node; - } + protected override SyntaxNode? GetAsyncSupportingDeclaration(SyntaxToken token) + { + var node = token.GetAncestor(node => node.IsAsyncSupportingFunctionSyntax()); + return node; } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpConstructorSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpConstructorSnippetProvider.cs index 30c8becb4d9c7..986e051f00d80 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpConstructorSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpConstructorSnippetProvider.cs @@ -21,80 +21,79 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpConstructorSnippetProvider : AbstractConstructorSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpConstructorSnippetProvider : AbstractConstructorSnippetProvider + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.InternalKeyword, - SyntaxKind.StaticKeyword, - }; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpConstructorSnippetProvider() - { - } + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.InternalKeyword, + SyntaxKind.StaticKeyword, + }; - protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) - { - var syntaxContext = (CSharpSyntaxContext)context.SyntaxContext; + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpConstructorSnippetProvider() + { + } - var precedingModifiers = syntaxContext.PrecedingModifiers; + protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) + { + var syntaxContext = (CSharpSyntaxContext)context.SyntaxContext; - if (!(precedingModifiers.All(SyntaxFacts.IsAccessibilityModifier) || - precedingModifiers.Count == 1 && precedingModifiers.Single() == SyntaxKind.StaticKeyword)) - { - return false; - } + var precedingModifiers = syntaxContext.PrecedingModifiers; - return - syntaxContext.IsMemberDeclarationContext( - validModifiers: s_validModifiers, - validTypeDeclarations: SyntaxKindSet.ClassStructRecordTypeDeclarations, - canBePartial: true, - cancellationToken: cancellationToken); + if (!(precedingModifiers.All(SyntaxFacts.IsAccessibilityModifier) || + precedingModifiers.Count == 1 && precedingModifiers.Single() == SyntaxKind.StaticKeyword)) + { + return false; } - protected override async Task GenerateSnippetTextChangeAsync(Document document, int position, CancellationToken cancellationToken) - { - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); - var syntaxContext = (CSharpSyntaxContext)document.GetRequiredLanguageService().CreateContext(document, semanticModel, position, cancellationToken); + return + syntaxContext.IsMemberDeclarationContext( + validModifiers: s_validModifiers, + validTypeDeclarations: SyntaxKindSet.ClassStructRecordTypeDeclarations, + canBePartial: true, + cancellationToken: cancellationToken); + } - var containingType = syntaxContext.ContainingTypeDeclaration; - Contract.ThrowIfNull(containingType); + protected override async Task GenerateSnippetTextChangeAsync(Document document, int position, CancellationToken cancellationToken) + { + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); + var syntaxContext = (CSharpSyntaxContext)document.GetRequiredLanguageService().CreateContext(document, semanticModel, position, cancellationToken); - var containingTypeSymbol = semanticModel.GetDeclaredSymbol(containingType, cancellationToken); - Contract.ThrowIfNull(containingTypeSymbol); + var containingType = syntaxContext.ContainingTypeDeclaration; + Contract.ThrowIfNull(containingType); - var generator = SyntaxGenerator.GetGenerator(document); - var constructorDeclaration = generator.ConstructorDeclaration( - containingTypeName: containingType.Identifier.ToString(), - accessibility: syntaxContext.PrecedingModifiers.Any() ? Accessibility.NotApplicable : (containingTypeSymbol.IsAbstract ? Accessibility.Protected : Accessibility.Public)); + var containingTypeSymbol = semanticModel.GetDeclaredSymbol(containingType, cancellationToken); + Contract.ThrowIfNull(containingTypeSymbol); - return new TextChange(TextSpan.FromBounds(position, position), constructorDeclaration.NormalizeWhitespace().ToFullString()); - } + var generator = SyntaxGenerator.GetGenerator(document); + var constructorDeclaration = generator.ConstructorDeclaration( + containingTypeName: containingType.Identifier.ToString(), + accessibility: syntaxContext.PrecedingModifiers.Any() ? Accessibility.NotApplicable : (containingTypeSymbol.IsAbstract ? Accessibility.Protected : Accessibility.Public)); - protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) - { - return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( - caretTarget, - static d => d.Body!, - sourceText); - } + return new TextChange(TextSpan.FromBounds(position, position), constructorDeclaration.NormalizeWhitespace().ToFullString()); + } - protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) - { - return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( - document, - FindSnippetAnnotation, - static d => d.Body!, - cancellationToken); - } + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + { + return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( + caretTarget, + static d => d.Body!, + sourceText); + } + + protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) + { + return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( + document, + FindSnippetAnnotation, + static d => d.Body!, + cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpElseSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpElseSnippetProvider.cs index c0d597b77046f..f7edde19d6865 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpElseSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpElseSnippetProvider.cs @@ -14,68 +14,67 @@ using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal class CSharpElseSnippetProvider : AbstractElseSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal class CSharpElseSnippetProvider : AbstractElseSnippetProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpElseSnippetProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpElseSnippetProvider() - { - } + } - protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) - { - var syntaxContext = context.SyntaxContext; - var token = syntaxContext.TargetToken; + protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) + { + var syntaxContext = context.SyntaxContext; + var token = syntaxContext.TargetToken; - // We have to consider all ancestor if statements of the last token until we find a match for this 'else': - // while (true) - // if (true) - // while (true) - // if (true) - // Console.WriteLine(); - // else - // Console.WriteLine(); - // $$ - var isAfterIfStatement = false; + // We have to consider all ancestor if statements of the last token until we find a match for this 'else': + // while (true) + // if (true) + // while (true) + // if (true) + // Console.WriteLine(); + // else + // Console.WriteLine(); + // $$ + var isAfterIfStatement = false; - foreach (var ifStatement in token.GetAncestors()) + foreach (var ifStatement in token.GetAncestors()) + { + // If there's a missing token at the end of the statement, it's incomplete and we do not offer 'else'. + // context.TargetToken does not include zero width so in that case these will never be equal. + if (ifStatement.Statement.GetLastToken(includeZeroWidth: true) == token) { - // If there's a missing token at the end of the statement, it's incomplete and we do not offer 'else'. - // context.TargetToken does not include zero width so in that case these will never be equal. - if (ifStatement.Statement.GetLastToken(includeZeroWidth: true) == token) - { - isAfterIfStatement = true; - break; - } + isAfterIfStatement = true; + break; } - - return isAfterIfStatement && base.IsValidSnippetLocation(in context, cancellationToken); } - protected override Task GenerateSnippetTextChangeAsync(Document document, int position, CancellationToken cancellationToken) - { - var elseClause = SyntaxFactory.ElseClause(SyntaxFactory.Block()); - return Task.FromResult(new TextChange(TextSpan.FromBounds(position, position), elseClause.ToFullString())); - } + return isAfterIfStatement && base.IsValidSnippetLocation(in context, cancellationToken); + } - protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) - { - return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( - caretTarget, - static c => (BlockSyntax)c.Statement, - sourceText); - } + protected override Task GenerateSnippetTextChangeAsync(Document document, int position, CancellationToken cancellationToken) + { + var elseClause = SyntaxFactory.ElseClause(SyntaxFactory.Block()); + return Task.FromResult(new TextChange(TextSpan.FromBounds(position, position), elseClause.ToFullString())); + } - protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) - { - return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( - document, - FindSnippetAnnotation, - static c => (BlockSyntax)c.Statement, - cancellationToken); - } + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + { + return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( + caretTarget, + static c => (BlockSyntax)c.Statement, + sourceText); + } + + protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) + { + return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( + document, + FindSnippetAnnotation, + static c => (BlockSyntax)c.Statement, + cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpEnumSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpEnumSnippetProvider.cs index 548cfcec95583..31509a7bf38db 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpEnumSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpEnumSnippetProvider.cs @@ -15,43 +15,42 @@ using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Snippets.SnippetProviders; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpEnumSnippetProvider : AbstractCSharpTypeSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpEnumSnippetProvider : AbstractCSharpTypeSnippetProvider + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.InternalKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.FileKeyword, + }; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpEnumSnippetProvider() + { + } + + public override string Identifier => "enum"; + public override string Description => FeaturesResources.enum_; + + protected override ISet ValidModifiers => s_validModifiers; + + protected override async Task GenerateTypeDeclarationAsync(Document document, int position, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var name = NameGenerator.GenerateUniqueName("MyEnum", name => semanticModel.LookupSymbols(position, name: name).IsEmpty); + return generator.EnumDeclaration(name); + } + + protected override Func GetSnippetContainerFunction(ISyntaxFacts syntaxFacts) { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.InternalKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.FileKeyword, - }; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpEnumSnippetProvider() - { - } - - public override string Identifier => "enum"; - public override string Description => FeaturesResources.enum_; - - protected override ISet ValidModifiers => s_validModifiers; - - protected override async Task GenerateTypeDeclarationAsync(Document document, int position, CancellationToken cancellationToken) - { - var generator = SyntaxGenerator.GetGenerator(document); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var name = NameGenerator.GenerateUniqueName("MyEnum", name => semanticModel.LookupSymbols(position, name: name).IsEmpty); - return generator.EnumDeclaration(name); - } - - protected override Func GetSnippetContainerFunction(ISyntaxFacts syntaxFacts) - { - return syntaxFacts.IsEnumDeclaration; - } + return syntaxFacts.IsEnumDeclaration; } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpForEachLoopSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpForEachLoopSnippetProvider.cs index e5807937f1f01..67eeec1f62388 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpForEachLoopSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpForEachLoopSnippetProvider.cs @@ -20,123 +20,122 @@ using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpForEachLoopSnippetProvider : AbstractForEachLoopSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpForEachLoopSnippetProvider : AbstractForEachLoopSnippetProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpForEachLoopSnippetProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpForEachLoopSnippetProvider() - { - } - - protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) - { - var syntaxContext = context.SyntaxContext; - var token = syntaxContext.TargetToken; - - // Allow `foreach` snippet after `await` as expression statement - // So `await $$` is a valid position, but `var result = await $$` is not - // The second check if for case when completions are invoked after `await` in non-async context. In such cases parser treats `await` as identifier - if (token is { RawKind: (int)SyntaxKind.AwaitKeyword, Parent: ExpressionSyntax { Parent: ExpressionStatementSyntax } } || - token is { RawKind: (int)SyntaxKind.IdentifierToken, ValueText: "await", Parent: IdentifierNameSyntax { Parent: ExpressionStatementSyntax } }) - { - return true; - } - - return base.IsValidSnippetLocation(in context, cancellationToken); - } + } - protected override SyntaxNode GenerateStatement(SyntaxGenerator generator, SyntaxContext syntaxContext, InlineExpressionInfo? inlineExpressionInfo) + protected override bool IsValidSnippetLocation(in SnippetContext context, CancellationToken cancellationToken) + { + var syntaxContext = context.SyntaxContext; + var token = syntaxContext.TargetToken; + + // Allow `foreach` snippet after `await` as expression statement + // So `await $$` is a valid position, but `var result = await $$` is not + // The second check if for case when completions are invoked after `await` in non-async context. In such cases parser treats `await` as identifier + if (token is { RawKind: (int)SyntaxKind.AwaitKeyword, Parent: ExpressionSyntax { Parent: ExpressionStatementSyntax } } || + token is { RawKind: (int)SyntaxKind.IdentifierToken, ValueText: "await", Parent: IdentifierNameSyntax { Parent: ExpressionStatementSyntax } }) { - var semanticModel = syntaxContext.SemanticModel; - var position = syntaxContext.Position; - - var varIdentifier = SyntaxFactory.IdentifierName("var"); - var collectionIdentifier = (ExpressionSyntax?)inlineExpressionInfo?.Node; - - if (collectionIdentifier is null) - { - var isAsync = syntaxContext.TargetToken is { RawKind: (int)SyntaxKind.AwaitKeyword } or { RawKind: (int)SyntaxKind.IdentifierToken, ValueText: "await" }; - var enumerationSymbol = semanticModel.LookupSymbols(position).FirstOrDefault(symbol => symbol.GetSymbolType() is { } symbolType && - (isAsync ? symbolType.CanBeAsynchronouslyEnumerated(semanticModel.Compilation) : symbolType.CanBeEnumerated()) && - symbol.Kind is SymbolKind.Local or SymbolKind.Field or SymbolKind.Parameter or SymbolKind.Property); - collectionIdentifier = enumerationSymbol is null - ? SyntaxFactory.IdentifierName("collection") - : SyntaxFactory.IdentifierName(enumerationSymbol.Name); - } - - var itemString = NameGenerator.GenerateUniqueName( - "item", name => semanticModel.LookupSymbols(position, name: name).IsEmpty); - - ForEachStatementSyntax forEachStatement; - - if (inlineExpressionInfo is { TypeInfo: var typeInfo } && - typeInfo.Type!.CanBeAsynchronouslyEnumerated(semanticModel.Compilation)) - { - forEachStatement = SyntaxFactory.ForEachStatement( - SyntaxFactory.Token(SyntaxKind.AwaitKeyword), - SyntaxFactory.Token(SyntaxKind.ForEachKeyword), - SyntaxFactory.Token(SyntaxKind.OpenParenToken), - varIdentifier, - SyntaxFactory.Identifier(itemString), - SyntaxFactory.Token(SyntaxKind.InKeyword), - collectionIdentifier.WithoutLeadingTrivia(), - SyntaxFactory.Token(SyntaxKind.CloseParenToken), - SyntaxFactory.Block()); - } - else - { - forEachStatement = SyntaxFactory.ForEachStatement( - varIdentifier, - itemString, - collectionIdentifier.WithoutLeadingTrivia(), - SyntaxFactory.Block()); - } - - return forEachStatement.NormalizeWhitespace(); + return true; } - /// - /// Goes through each piece of the foreach statement and extracts the identifiers - /// as well as their locations to create SnippetPlaceholder's of each. - /// - protected override ImmutableArray GetPlaceHolderLocationsList(SyntaxNode node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) - { - using var _ = ArrayBuilder.GetInstance(out var arrayBuilder); - GetPartsOfForEachStatement(node, out var identifier, out var expression, out var _1); - arrayBuilder.Add(new SnippetPlaceholder(identifier.ToString(), identifier.SpanStart)); + return base.IsValidSnippetLocation(in context, cancellationToken); + } - if (!ConstructedFromInlineExpression) - arrayBuilder.Add(new SnippetPlaceholder(expression.ToString(), expression.SpanStart)); + protected override SyntaxNode GenerateStatement(SyntaxGenerator generator, SyntaxContext syntaxContext, InlineExpressionInfo? inlineExpressionInfo) + { + var semanticModel = syntaxContext.SemanticModel; + var position = syntaxContext.Position; - return arrayBuilder.ToImmutableArray(); - } + var varIdentifier = SyntaxFactory.IdentifierName("var"); + var collectionIdentifier = (ExpressionSyntax?)inlineExpressionInfo?.Node; - protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + if (collectionIdentifier is null) { - return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( - caretTarget, - static s => (BlockSyntax)s.Statement, - sourceText); + var isAsync = syntaxContext.TargetToken is { RawKind: (int)SyntaxKind.AwaitKeyword } or { RawKind: (int)SyntaxKind.IdentifierToken, ValueText: "await" }; + var enumerationSymbol = semanticModel.LookupSymbols(position).FirstOrDefault(symbol => symbol.GetSymbolType() is { } symbolType && + (isAsync ? symbolType.CanBeAsynchronouslyEnumerated(semanticModel.Compilation) : symbolType.CanBeEnumerated()) && + symbol.Kind is SymbolKind.Local or SymbolKind.Field or SymbolKind.Parameter or SymbolKind.Property); + collectionIdentifier = enumerationSymbol is null + ? SyntaxFactory.IdentifierName("collection") + : SyntaxFactory.IdentifierName(enumerationSymbol.Name); } - protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) + var itemString = NameGenerator.GenerateUniqueName( + "item", name => semanticModel.LookupSymbols(position, name: name).IsEmpty); + + ForEachStatementSyntax forEachStatement; + + if (inlineExpressionInfo is { TypeInfo: var typeInfo } && + typeInfo.Type!.CanBeAsynchronouslyEnumerated(semanticModel.Compilation)) { - return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( - document, - FindSnippetAnnotation, - static s => (BlockSyntax)s.Statement, - cancellationToken); + forEachStatement = SyntaxFactory.ForEachStatement( + SyntaxFactory.Token(SyntaxKind.AwaitKeyword), + SyntaxFactory.Token(SyntaxKind.ForEachKeyword), + SyntaxFactory.Token(SyntaxKind.OpenParenToken), + varIdentifier, + SyntaxFactory.Identifier(itemString), + SyntaxFactory.Token(SyntaxKind.InKeyword), + collectionIdentifier.WithoutLeadingTrivia(), + SyntaxFactory.Token(SyntaxKind.CloseParenToken), + SyntaxFactory.Block()); } - - private static void GetPartsOfForEachStatement(SyntaxNode node, out SyntaxToken identifier, out SyntaxNode expression, out SyntaxNode statement) + else { - var forEachStatement = (ForEachStatementSyntax)node; - identifier = forEachStatement.Identifier; - expression = forEachStatement.Expression; - statement = forEachStatement.Statement; + forEachStatement = SyntaxFactory.ForEachStatement( + varIdentifier, + itemString, + collectionIdentifier.WithoutLeadingTrivia(), + SyntaxFactory.Block()); } + + return forEachStatement.NormalizeWhitespace(); + } + + /// + /// Goes through each piece of the foreach statement and extracts the identifiers + /// as well as their locations to create SnippetPlaceholder's of each. + /// + protected override ImmutableArray GetPlaceHolderLocationsList(SyntaxNode node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var arrayBuilder); + GetPartsOfForEachStatement(node, out var identifier, out var expression, out var _1); + arrayBuilder.Add(new SnippetPlaceholder(identifier.ToString(), identifier.SpanStart)); + + if (!ConstructedFromInlineExpression) + arrayBuilder.Add(new SnippetPlaceholder(expression.ToString(), expression.SpanStart)); + + return arrayBuilder.ToImmutableArray(); + } + + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + { + return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( + caretTarget, + static s => (BlockSyntax)s.Statement, + sourceText); + } + + protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) + { + return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( + document, + FindSnippetAnnotation, + static s => (BlockSyntax)s.Statement, + cancellationToken); + } + + private static void GetPartsOfForEachStatement(SyntaxNode node, out SyntaxToken identifier, out SyntaxNode expression, out SyntaxNode statement) + { + var forEachStatement = (ForEachStatementSyntax)node; + identifier = forEachStatement.Identifier; + expression = forEachStatement.Expression; + statement = forEachStatement.Statement; } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpIfSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpIfSnippetProvider.cs index 2023dc666fec8..d826b1ee686fc 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpIfSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpIfSnippetProvider.cs @@ -13,38 +13,37 @@ using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpIfSnippetProvider : AbstractIfSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpIfSnippetProvider : AbstractIfSnippetProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpIfSnippetProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpIfSnippetProvider() - { - } + } - protected override SyntaxNode GetCondition(SyntaxNode node) - { - var ifStatement = (IfStatementSyntax)node; - return ifStatement.Condition; - } + protected override SyntaxNode GetCondition(SyntaxNode node) + { + var ifStatement = (IfStatementSyntax)node; + return ifStatement.Condition; + } - protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) - { - return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( - caretTarget, - static s => (BlockSyntax)s.Statement, - sourceText); - } + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + { + return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( + caretTarget, + static s => (BlockSyntax)s.Statement, + sourceText); + } - protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) - { - return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( - document, - FindSnippetAnnotation, - static s => (BlockSyntax)s.Statement, - cancellationToken); - } + protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) + { + return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( + document, + FindSnippetAnnotation, + static s => (BlockSyntax)s.Statement, + cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpIntMainSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpIntMainSnippetProvider.cs index 26b571b94cffa..5d69ace161a82 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpIntMainSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpIntMainSnippetProvider.cs @@ -19,60 +19,59 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpIntMainSnippetProvider : AbstractCSharpMainMethodSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpIntMainSnippetProvider : AbstractCSharpMainMethodSnippetProvider - { - public override string Identifier => "sim"; + public override string Identifier => "sim"; - public override string Description => CSharpFeaturesResources.static_int_Main; + public override string Description => CSharpFeaturesResources.static_int_Main; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpIntMainSnippetProvider() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpIntMainSnippetProvider() + { + } - protected override SyntaxNode GenerateReturnType(SyntaxGenerator generator) - => generator.TypeExpression(SpecialType.System_Int32); + protected override SyntaxNode GenerateReturnType(SyntaxGenerator generator) + => generator.TypeExpression(SpecialType.System_Int32); - protected override IEnumerable GenerateInnerStatements(SyntaxGenerator generator) - { - var returnStatement = generator.ReturnStatement(generator.LiteralExpression(0)); - return SpecializedCollections.SingletonEnumerable(returnStatement); - } + protected override IEnumerable GenerateInnerStatements(SyntaxGenerator generator) + { + var returnStatement = generator.ReturnStatement(generator.LiteralExpression(0)); + return SpecializedCollections.SingletonEnumerable(returnStatement); + } - protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) - { - var methodDeclaration = (MethodDeclarationSyntax)caretTarget; - var body = methodDeclaration.Body!; - var returnStatement = body.Statements.First(); + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + { + var methodDeclaration = (MethodDeclarationSyntax)caretTarget; + var body = methodDeclaration.Body!; + var returnStatement = body.Statements.First(); - var triviaSpan = returnStatement.GetLeadingTrivia().Span; - var line = sourceText.Lines.GetLineFromPosition(triviaSpan.Start); - // Getting the location at the end of the line before the newline. - return line.Span.End; - } + var triviaSpan = returnStatement.GetLeadingTrivia().Span; + var line = sourceText.Lines.GetLineFromPosition(triviaSpan.Start); + // Getting the location at the end of the line before the newline. + return line.Span.End; + } - protected override async Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var snippetNode = root.GetAnnotatedNodes(FindSnippetAnnotation).FirstOrDefault(); + protected override async Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var snippetNode = root.GetAnnotatedNodes(FindSnippetAnnotation).FirstOrDefault(); - if (snippetNode is not MethodDeclarationSyntax methodDeclaration) - return document; + if (snippetNode is not MethodDeclarationSyntax methodDeclaration) + return document; - var body = methodDeclaration.Body!; - var returnStatement = body.Statements.First(); + var body = methodDeclaration.Body!; + var returnStatement = body.Statements.First(); - var syntaxFormattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions: null, cancellationToken).ConfigureAwait(false); - var indentationString = CSharpSnippetHelpers.GetBlockLikeIndentationString(document, body.OpenBraceToken.SpanStart, syntaxFormattingOptions, cancellationToken); + var syntaxFormattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions: null, cancellationToken).ConfigureAwait(false); + var indentationString = CSharpSnippetHelpers.GetBlockLikeIndentationString(document, body.OpenBraceToken.SpanStart, syntaxFormattingOptions, cancellationToken); - var updatedReturnStatement = returnStatement.WithPrependedLeadingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, indentationString)); - var updatedRoot = root.ReplaceNode(returnStatement, updatedReturnStatement); + var updatedReturnStatement = returnStatement.WithPrependedLeadingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, indentationString)); + var updatedRoot = root.ReplaceNode(returnStatement, updatedReturnStatement); - return document.WithSyntaxRoot(updatedRoot); - } + return document.WithSyntaxRoot(updatedRoot); } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpInterfaceSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpInterfaceSnippetProvider.cs index 9cbb0ff3fbcec..6774f33d9a7e7 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpInterfaceSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpInterfaceSnippetProvider.cs @@ -15,45 +15,44 @@ using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Snippets.SnippetProviders; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpInterfaceSnippetProvider : AbstractCSharpTypeSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpInterfaceSnippetProvider : AbstractCSharpTypeSnippetProvider + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.InternalKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.FileKeyword, + }; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpInterfaceSnippetProvider() + { + } + + public override string Identifier => "interface"; + + public override string Description => FeaturesResources.interface_; + + protected override ISet ValidModifiers => s_validModifiers; + + protected override async Task GenerateTypeDeclarationAsync(Document document, int position, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var name = NameGenerator.GenerateUniqueName("MyInterface", name => semanticModel.LookupSymbols(position, name: name).IsEmpty); + return generator.InterfaceDeclaration(name); + } + + protected override Func GetSnippetContainerFunction(ISyntaxFacts syntaxFacts) { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.InternalKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.FileKeyword, - }; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpInterfaceSnippetProvider() - { - } - - public override string Identifier => "interface"; - - public override string Description => FeaturesResources.interface_; - - protected override ISet ValidModifiers => s_validModifiers; - - protected override async Task GenerateTypeDeclarationAsync(Document document, int position, CancellationToken cancellationToken) - { - var generator = SyntaxGenerator.GetGenerator(document); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var name = NameGenerator.GenerateUniqueName("MyInterface", name => semanticModel.LookupSymbols(position, name: name).IsEmpty); - return generator.InterfaceDeclaration(name); - } - - protected override Func GetSnippetContainerFunction(ISyntaxFacts syntaxFacts) - { - return syntaxFacts.IsInterfaceDeclaration; - } + return syntaxFacts.IsInterfaceDeclaration; } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpLockSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpLockSnippetProvider.cs index ed120367d28cd..0ef47e1701195 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpLockSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpLockSnippetProvider.cs @@ -14,43 +14,42 @@ using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpLockSnippetProvider : AbstractLockSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpLockSnippetProvider : AbstractLockSnippetProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpLockSnippetProvider() + { + } + + public override string Identifier => "lock"; + + public override string Description => CSharpFeaturesResources.lock_statement; + + protected override ImmutableArray GetPlaceHolderLocationsList(SyntaxNode node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) + { + var lockStatement = (LockStatementSyntax)node; + var expression = lockStatement.Expression; + return [new SnippetPlaceholder(expression.ToString(), expression.SpanStart)]; + } + + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + { + return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( + caretTarget, + static s => (BlockSyntax)s.Statement, + sourceText); + } + + protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpLockSnippetProvider() - { - } - - public override string Identifier => "lock"; - - public override string Description => CSharpFeaturesResources.lock_statement; - - protected override ImmutableArray GetPlaceHolderLocationsList(SyntaxNode node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) - { - var lockStatement = (LockStatementSyntax)node; - var expression = lockStatement.Expression; - return [new SnippetPlaceholder(expression.ToString(), expression.SpanStart)]; - } - - protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) - { - return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( - caretTarget, - static s => (BlockSyntax)s.Statement, - sourceText); - } - - protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) - { - return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( - document, - FindSnippetAnnotation, - static s => (BlockSyntax)s.Statement, - cancellationToken); - } + return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( + document, + FindSnippetAnnotation, + static s => (BlockSyntax)s.Statement, + cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpPropSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpPropSnippetProvider.cs index 7eb04862ae689..786e6d5be97a5 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpPropSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpPropSnippetProvider.cs @@ -11,32 +11,31 @@ using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Snippets.SnippetProviders; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpPropSnippetProvider : AbstractCSharpAutoPropertySnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpPropSnippetProvider : AbstractCSharpAutoPropertySnippetProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpPropSnippetProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpPropSnippetProvider() - { - } + } - public override string Identifier => "prop"; + public override string Identifier => "prop"; - public override string Description => FeaturesResources.property_; + public override string Description => FeaturesResources.property_; - protected override AccessorDeclarationSyntax? GenerateSetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator) + protected override AccessorDeclarationSyntax? GenerateSetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator) + { + // Having a property with `set` accessor in a readonly struct leads to a compiler error. + // So if user executes snippet inside a readonly struct the right thing to do is to not generate `set` accessor at all + if (syntaxContext.ContainingTypeDeclaration is StructDeclarationSyntax structDeclaration && + structDeclaration.Modifiers.Any(SyntaxKind.ReadOnlyKeyword)) { - // Having a property with `set` accessor in a readonly struct leads to a compiler error. - // So if user executes snippet inside a readonly struct the right thing to do is to not generate `set` accessor at all - if (syntaxContext.ContainingTypeDeclaration is StructDeclarationSyntax structDeclaration && - structDeclaration.Modifiers.Any(SyntaxKind.ReadOnlyKeyword)) - { - return null; - } - - return base.GenerateSetAccessorDeclaration(syntaxContext, generator); + return null; } + + return base.GenerateSetAccessorDeclaration(syntaxContext, generator); } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpPropgSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpPropgSnippetProvider.cs index a61dd1232ca01..8a96f8423b0ad 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpPropgSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpPropgSnippetProvider.cs @@ -11,40 +11,39 @@ using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Snippets.SnippetProviders; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal class CSharpPropgSnippetProvider : AbstractCSharpAutoPropertySnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal class CSharpPropgSnippetProvider : AbstractCSharpAutoPropertySnippetProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpPropgSnippetProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpPropgSnippetProvider() - { - } + } - public override string Identifier => "propg"; + public override string Identifier => "propg"; - public override string Description => FeaturesResources.get_only_property; + public override string Description => FeaturesResources.get_only_property; - protected override AccessorDeclarationSyntax? GenerateSetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator) + protected override AccessorDeclarationSyntax? GenerateSetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator) + { + // Interface cannot have properties with `private set` accessor. + // So if we are inside an interface, we just return null here. + // This causes the caller to just skip this `set` accessor + if (syntaxContext.ContainingTypeDeclaration is InterfaceDeclarationSyntax) { - // Interface cannot have properties with `private set` accessor. - // So if we are inside an interface, we just return null here. - // This causes the caller to just skip this `set` accessor - if (syntaxContext.ContainingTypeDeclaration is InterfaceDeclarationSyntax) - { - return null; - } - - // Having a property with `set` accessor in a readonly struct leads to a compiler error. - // So if user executes snippet inside a readonly struct the right thing to do is to not generate `set` accessor at all - if (syntaxContext.ContainingTypeDeclaration is StructDeclarationSyntax structDeclaration && - structDeclaration.Modifiers.Any(SyntaxKind.ReadOnlyKeyword)) - { - return null; - } - - return (AccessorDeclarationSyntax)generator.SetAccessorDeclaration(Accessibility.Private); + return null; } + + // Having a property with `set` accessor in a readonly struct leads to a compiler error. + // So if user executes snippet inside a readonly struct the right thing to do is to not generate `set` accessor at all + if (syntaxContext.ContainingTypeDeclaration is StructDeclarationSyntax structDeclaration && + structDeclaration.Modifiers.Any(SyntaxKind.ReadOnlyKeyword)) + { + return null; + } + + return (AccessorDeclarationSyntax)generator.SetAccessorDeclaration(Accessibility.Private); } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpPropiSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpPropiSnippetProvider.cs index 20ffc714b1efc..7e2f6a5523769 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpPropiSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpPropiSnippetProvider.cs @@ -11,22 +11,21 @@ using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Snippets.SnippetProviders; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal class CSharpPropiSnippetProvider : AbstractCSharpAutoPropertySnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal class CSharpPropiSnippetProvider : AbstractCSharpAutoPropertySnippetProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpPropiSnippetProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpPropiSnippetProvider() - { - } + } - public override string Identifier => "propi"; + public override string Identifier => "propi"; - public override string Description => CSharpFeaturesResources.init_only_property; + public override string Description => CSharpFeaturesResources.init_only_property; - protected override AccessorDeclarationSyntax? GenerateSetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator) - => SyntaxFactory.AccessorDeclaration(SyntaxKind.InitAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); - } + protected override AccessorDeclarationSyntax? GenerateSetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator) + => SyntaxFactory.AccessorDeclaration(SyntaxKind.InitAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpSnippetService.cs b/src/Features/CSharp/Portable/Snippets/CSharpSnippetService.cs index 772ce98a6b3e1..16590ff577a6c 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpSnippetService.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpSnippetService.cs @@ -9,12 +9,11 @@ using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Snippets.SnippetProviders; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportLanguageService(typeof(ISnippetService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class CSharpSnippetService([ImportMany] IEnumerable> snippetProviders) : AbstractSnippetService(snippetProviders) { - [ExportLanguageService(typeof(ISnippetService), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class CSharpSnippetService([ImportMany] IEnumerable> snippetProviders) : AbstractSnippetService(snippetProviders) - { - } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpStructSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpStructSnippetProvider.cs index 54a6b40ee892f..6a208cd578d57 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpStructSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpStructSnippetProvider.cs @@ -15,47 +15,46 @@ using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Snippets.SnippetProviders; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpStructSnippetProvider : AbstractCSharpTypeSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpStructSnippetProvider : AbstractCSharpTypeSnippetProvider + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.InternalKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.RefKeyword, + SyntaxKind.ReadOnlyKeyword, + SyntaxKind.FileKeyword, + }; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpStructSnippetProvider() + { + } + + public override string Identifier => "struct"; + + public override string Description => FeaturesResources.struct_; + + protected override ISet ValidModifiers => s_validModifiers; + + protected override async Task GenerateTypeDeclarationAsync(Document document, int position, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var name = NameGenerator.GenerateUniqueName("MyStruct", name => semanticModel.LookupSymbols(position, name: name).IsEmpty); + return generator.StructDeclaration(name); + } + + protected override Func GetSnippetContainerFunction(ISyntaxFacts syntaxFacts) { - private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) - { - SyntaxKind.InternalKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.UnsafeKeyword, - SyntaxKind.RefKeyword, - SyntaxKind.ReadOnlyKeyword, - SyntaxKind.FileKeyword, - }; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpStructSnippetProvider() - { - } - - public override string Identifier => "struct"; - - public override string Description => FeaturesResources.struct_; - - protected override ISet ValidModifiers => s_validModifiers; - - protected override async Task GenerateTypeDeclarationAsync(Document document, int position, CancellationToken cancellationToken) - { - var generator = SyntaxGenerator.GetGenerator(document); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var name = NameGenerator.GenerateUniqueName("MyStruct", name => semanticModel.LookupSymbols(position, name: name).IsEmpty); - return generator.StructDeclaration(name); - } - - protected override Func GetSnippetContainerFunction(ISyntaxFacts syntaxFacts) - { - return syntaxFacts.IsStructDeclaration; - } + return syntaxFacts.IsStructDeclaration; } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpVoidMainSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpVoidMainSnippetProvider.cs index b2d52d282d484..036c046cd3230 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpVoidMainSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpVoidMainSnippetProvider.cs @@ -16,42 +16,41 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpVoidMainSnippetProvider : AbstractCSharpMainMethodSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpVoidMainSnippetProvider : AbstractCSharpMainMethodSnippetProvider + public override string Identifier => "svm"; + + public override string Description => CSharpFeaturesResources.static_void_Main; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpVoidMainSnippetProvider() + { + } + + protected override SyntaxNode GenerateReturnType(SyntaxGenerator generator) + => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)); + + protected override IEnumerable GenerateInnerStatements(SyntaxGenerator generator) + => SpecializedCollections.EmptyEnumerable(); + + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + { + return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( + caretTarget, + static d => d.Body!, + sourceText); + } + + protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) { - public override string Identifier => "svm"; - - public override string Description => CSharpFeaturesResources.static_void_Main; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpVoidMainSnippetProvider() - { - } - - protected override SyntaxNode GenerateReturnType(SyntaxGenerator generator) - => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)); - - protected override IEnumerable GenerateInnerStatements(SyntaxGenerator generator) - => SpecializedCollections.EmptyEnumerable(); - - protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) - { - return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( - caretTarget, - static d => d.Body!, - sourceText); - } - - protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) - { - return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( - document, - FindSnippetAnnotation, - static m => m.Body!, - cancellationToken); - } + return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( + document, + FindSnippetAnnotation, + static m => m.Body!, + cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpWhileLoopSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpWhileLoopSnippetProvider.cs index 25e98d22f3b14..6f5928a0d38b8 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpWhileLoopSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpWhileLoopSnippetProvider.cs @@ -13,38 +13,37 @@ using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Snippets +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +internal sealed class CSharpWhileLoopSnippetProvider : AbstractWhileLoopSnippetProvider { - [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] - internal sealed class CSharpWhileLoopSnippetProvider : AbstractWhileLoopSnippetProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpWhileLoopSnippetProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpWhileLoopSnippetProvider() - { - } + } - protected override SyntaxNode GetCondition(SyntaxNode node) - { - var whileStatement = (WhileStatementSyntax)node; - return whileStatement.Condition; - } + protected override SyntaxNode GetCondition(SyntaxNode node) + { + var whileStatement = (WhileStatementSyntax)node; + return whileStatement.Condition; + } - protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) - { - return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( - caretTarget, - static s => (BlockSyntax)s.Statement, - sourceText); - } + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText) + { + return CSharpSnippetHelpers.GetTargetCaretPositionInBlock( + caretTarget, + static s => (BlockSyntax)s.Statement, + sourceText); + } - protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) - { - return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( - document, - FindSnippetAnnotation, - static s => (BlockSyntax)s.Statement, - cancellationToken); - } + protected override Task AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken) + { + return CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( + document, + FindSnippetAnnotation, + static s => (BlockSyntax)s.Statement, + cancellationToken); } } diff --git a/src/Features/CSharp/Portable/SolutionCrawler/CSharpDocumentDifferenceService.cs b/src/Features/CSharp/Portable/SolutionCrawler/CSharpDocumentDifferenceService.cs index ecd2d3b6e3185..27d621f018b42 100644 --- a/src/Features/CSharp/Portable/SolutionCrawler/CSharpDocumentDifferenceService.cs +++ b/src/Features/CSharp/Portable/SolutionCrawler/CSharpDocumentDifferenceService.cs @@ -9,15 +9,14 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.SolutionCrawler; -namespace Microsoft.CodeAnalysis.CSharp.SolutionCrawler +namespace Microsoft.CodeAnalysis.CSharp.SolutionCrawler; + +[ExportLanguageService(typeof(IDocumentDifferenceService), LanguageNames.CSharp), Shared] +internal class CSharpDocumentDifferenceService : AbstractDocumentDifferenceService { - [ExportLanguageService(typeof(IDocumentDifferenceService), LanguageNames.CSharp), Shared] - internal class CSharpDocumentDifferenceService : AbstractDocumentDifferenceService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpDocumentDifferenceService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpDocumentDifferenceService() - { - } } } diff --git a/src/Features/CSharp/Portable/SpellCheck/CSharpSpellcheckCodeFixProvider.cs b/src/Features/CSharp/Portable/SpellCheck/CSharpSpellcheckCodeFixProvider.cs index be4250e91dee2..6077181f81b03 100644 --- a/src/Features/CSharp/Portable/SpellCheck/CSharpSpellcheckCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/SpellCheck/CSharpSpellcheckCodeFixProvider.cs @@ -14,46 +14,45 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.SpellCheck; -namespace Microsoft.CodeAnalysis.CSharp.SpellCheck +namespace Microsoft.CodeAnalysis.CSharp.SpellCheck; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SpellCheck), Shared] +[ExtensionOrder(After = PredefinedCodeFixProviderNames.RemoveUnnecessaryCast)] +internal partial class CSharpSpellCheckCodeFixProvider : AbstractSpellCheckCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.SpellCheck), Shared] - [ExtensionOrder(After = PredefinedCodeFixProviderNames.RemoveUnnecessaryCast)] - internal partial class CSharpSpellCheckCodeFixProvider : AbstractSpellCheckCodeFixProvider - { - private const string CS0426 = nameof(CS0426); // The type name '0' does not exist in the type '1' - private const string CS1520 = nameof(CS1520); // Method must have a return type + private const string CS0426 = nameof(CS0426); // The type name '0' does not exist in the type '1' + private const string CS1520 = nameof(CS1520); // Method must have a return type - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpSpellCheckCodeFixProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpSpellCheckCodeFixProvider() + { + } - public override ImmutableArray FixableDiagnosticIds { get; } = - AddImportDiagnosticIds.FixableDiagnosticIds.Concat( - GenerateMethodDiagnosticIds.FixableDiagnosticIds).Concat( - ImmutableArray.Create(CS0426, CS1520)); + public override ImmutableArray FixableDiagnosticIds { get; } = + AddImportDiagnosticIds.FixableDiagnosticIds.Concat( + GenerateMethodDiagnosticIds.FixableDiagnosticIds).Concat( + ImmutableArray.Create(CS0426, CS1520)); - protected override bool ShouldSpellCheck(SimpleNameSyntax name) - => !name.IsVar; + protected override bool ShouldSpellCheck(SimpleNameSyntax name) + => !name.IsVar; - protected override bool DescendIntoChildren(SyntaxNode arg) - { - // Don't dive into type argument lists. We don't want to report spell checking - // fixes for type args when we're called on an outer generic type. - return arg is not TypeArgumentListSyntax; - } + protected override bool DescendIntoChildren(SyntaxNode arg) + { + // Don't dive into type argument lists. We don't want to report spell checking + // fixes for type args when we're called on an outer generic type. + return arg is not TypeArgumentListSyntax; + } - protected override bool IsGeneric(SyntaxToken token) - => token.GetNextToken().Kind() == SyntaxKind.LessThanToken; + protected override bool IsGeneric(SyntaxToken token) + => token.GetNextToken().Kind() == SyntaxKind.LessThanToken; - protected override bool IsGeneric(SimpleNameSyntax nameNode) - => nameNode is GenericNameSyntax; + protected override bool IsGeneric(SimpleNameSyntax nameNode) + => nameNode is GenericNameSyntax; - protected override bool IsGeneric(CompletionItem completionItem) - => completionItem.DisplayTextSuffix == "<>"; + protected override bool IsGeneric(CompletionItem completionItem) + => completionItem.DisplayTextSuffix == "<>"; - protected override SyntaxToken CreateIdentifier(SyntaxToken nameToken, string newName) - => SyntaxFactory.Identifier(newName).WithTriviaFrom(nameToken); - } + protected override SyntaxToken CreateIdentifier(SyntaxToken nameToken, string newName) + => SyntaxFactory.Identifier(newName).WithTriviaFrom(nameToken); } diff --git a/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpIfLikeStatementGenerator.cs b/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpIfLikeStatementGenerator.cs index 543b6e9c5783c..b9c07e20eea64 100644 --- a/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpIfLikeStatementGenerator.cs +++ b/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpIfLikeStatementGenerator.cs @@ -13,166 +13,165 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.SplitOrMergeIfStatements; -namespace Microsoft.CodeAnalysis.CSharp.SplitOrMergeIfStatements +namespace Microsoft.CodeAnalysis.CSharp.SplitOrMergeIfStatements; + +[ExportLanguageService(typeof(IIfLikeStatementGenerator), LanguageNames.CSharp), Shared] +internal sealed class CSharpIfLikeStatementGenerator : IIfLikeStatementGenerator { - [ExportLanguageService(typeof(IIfLikeStatementGenerator), LanguageNames.CSharp), Shared] - internal sealed class CSharpIfLikeStatementGenerator : IIfLikeStatementGenerator + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpIfLikeStatementGenerator() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpIfLikeStatementGenerator() - { - } + } - public bool IsIfOrElseIf(SyntaxNode node) => node is IfStatementSyntax; + public bool IsIfOrElseIf(SyntaxNode node) => node is IfStatementSyntax; - public bool IsCondition(SyntaxNode expression, out SyntaxNode ifOrElseIf) + public bool IsCondition(SyntaxNode expression, out SyntaxNode ifOrElseIf) + { + if (expression.Parent is IfStatementSyntax ifStatement && ifStatement.Condition == expression) { - if (expression.Parent is IfStatementSyntax ifStatement && ifStatement.Condition == expression) - { - ifOrElseIf = ifStatement; - return true; - } - - ifOrElseIf = null; - return false; + ifOrElseIf = ifStatement; + return true; } - public bool IsElseIfClause(SyntaxNode node, out SyntaxNode parentIfOrElseIf) - { - if (node is IfStatementSyntax && node.Parent is ElseClauseSyntax) - { - parentIfOrElseIf = (IfStatementSyntax)node.Parent.Parent; - return true; - } - - parentIfOrElseIf = null; - return false; - } + ifOrElseIf = null; + return false; + } - public bool HasElseIfClause(SyntaxNode ifOrElseIf, out SyntaxNode elseIfClause) + public bool IsElseIfClause(SyntaxNode node, out SyntaxNode parentIfOrElseIf) + { + if (node is IfStatementSyntax && node.Parent is ElseClauseSyntax) { - var ifStatement = (IfStatementSyntax)ifOrElseIf; - if (ifStatement.Else?.Statement is IfStatementSyntax elseIfStatement) - { - elseIfClause = elseIfStatement; - return true; - } - - elseIfClause = null; - return false; + parentIfOrElseIf = (IfStatementSyntax)node.Parent.Parent; + return true; } - public SyntaxNode GetCondition(SyntaxNode ifOrElseIf) + parentIfOrElseIf = null; + return false; + } + + public bool HasElseIfClause(SyntaxNode ifOrElseIf, out SyntaxNode elseIfClause) + { + var ifStatement = (IfStatementSyntax)ifOrElseIf; + if (ifStatement.Else?.Statement is IfStatementSyntax elseIfStatement) { - var ifStatement = (IfStatementSyntax)ifOrElseIf; - return ifStatement.Condition; + elseIfClause = elseIfStatement; + return true; } - public SyntaxNode GetRootIfStatement(SyntaxNode ifOrElseIf) - { - var ifStatement = (IfStatementSyntax)ifOrElseIf; + elseIfClause = null; + return false; + } - while (ifStatement.Parent is ElseClauseSyntax elseClause) - { - ifStatement = (IfStatementSyntax)elseClause.Parent; - } + public SyntaxNode GetCondition(SyntaxNode ifOrElseIf) + { + var ifStatement = (IfStatementSyntax)ifOrElseIf; + return ifStatement.Condition; + } - return ifStatement; - } + public SyntaxNode GetRootIfStatement(SyntaxNode ifOrElseIf) + { + var ifStatement = (IfStatementSyntax)ifOrElseIf; - public ImmutableArray GetElseIfAndElseClauses(SyntaxNode ifOrElseIf) + while (ifStatement.Parent is ElseClauseSyntax elseClause) { - var ifStatement = (IfStatementSyntax)ifOrElseIf; - - var builder = ImmutableArray.CreateBuilder(); + ifStatement = (IfStatementSyntax)elseClause.Parent; + } - while (ifStatement.Else?.Statement is IfStatementSyntax elseIfStatement) - { - builder.Add(elseIfStatement); - ifStatement = elseIfStatement; - } + return ifStatement; + } - if (ifStatement.Else != null) - { - builder.Add(ifStatement.Else); - } + public ImmutableArray GetElseIfAndElseClauses(SyntaxNode ifOrElseIf) + { + var ifStatement = (IfStatementSyntax)ifOrElseIf; - return builder.ToImmutable(); - } + var builder = ImmutableArray.CreateBuilder(); - public SyntaxNode WithCondition(SyntaxNode ifOrElseIf, SyntaxNode condition) + while (ifStatement.Else?.Statement is IfStatementSyntax elseIfStatement) { - var ifStatement = (IfStatementSyntax)ifOrElseIf; - return ifStatement.WithCondition((ExpressionSyntax)condition); + builder.Add(elseIfStatement); + ifStatement = elseIfStatement; } - public SyntaxNode WithStatementInBlock(SyntaxNode ifOrElseIf, SyntaxNode statement) + if (ifStatement.Else != null) { - var ifStatement = (IfStatementSyntax)ifOrElseIf; - return ifStatement.WithStatement(SyntaxFactory.Block((StatementSyntax)statement)); + builder.Add(ifStatement.Else); } - public SyntaxNode WithStatementsOf(SyntaxNode ifOrElseIf, SyntaxNode otherIfOrElseIf) - { - var ifStatement = (IfStatementSyntax)ifOrElseIf; - var otherIfStatement = (IfStatementSyntax)otherIfOrElseIf; - return ifStatement.WithStatement(otherIfStatement.Statement); - } + return builder.ToImmutable(); + } + + public SyntaxNode WithCondition(SyntaxNode ifOrElseIf, SyntaxNode condition) + { + var ifStatement = (IfStatementSyntax)ifOrElseIf; + return ifStatement.WithCondition((ExpressionSyntax)condition); + } + + public SyntaxNode WithStatementInBlock(SyntaxNode ifOrElseIf, SyntaxNode statement) + { + var ifStatement = (IfStatementSyntax)ifOrElseIf; + return ifStatement.WithStatement(SyntaxFactory.Block((StatementSyntax)statement)); + } + + public SyntaxNode WithStatementsOf(SyntaxNode ifOrElseIf, SyntaxNode otherIfOrElseIf) + { + var ifStatement = (IfStatementSyntax)ifOrElseIf; + var otherIfStatement = (IfStatementSyntax)otherIfOrElseIf; + return ifStatement.WithStatement(otherIfStatement.Statement); + } - public SyntaxNode WithElseIfAndElseClausesOf(SyntaxNode ifStatement, SyntaxNode otherIfStatement) - => ((IfStatementSyntax)ifStatement).WithElse(((IfStatementSyntax)otherIfStatement).Else); + public SyntaxNode WithElseIfAndElseClausesOf(SyntaxNode ifStatement, SyntaxNode otherIfStatement) + => ((IfStatementSyntax)ifStatement).WithElse(((IfStatementSyntax)otherIfStatement).Else); - public SyntaxNode ToIfStatement(SyntaxNode ifOrElseIf) - => ifOrElseIf; + public SyntaxNode ToIfStatement(SyntaxNode ifOrElseIf) + => ifOrElseIf; - public SyntaxNode ToElseIfClause(SyntaxNode ifOrElseIf) - => ((IfStatementSyntax)ifOrElseIf).WithElse(null); + public SyntaxNode ToElseIfClause(SyntaxNode ifOrElseIf) + => ((IfStatementSyntax)ifOrElseIf).WithElse(null); - public void InsertElseIfClause(SyntaxEditor editor, SyntaxNode afterIfOrElseIf, SyntaxNode elseIfClause) + public void InsertElseIfClause(SyntaxEditor editor, SyntaxNode afterIfOrElseIf, SyntaxNode elseIfClause) + { + editor.ReplaceNode(afterIfOrElseIf, (currentNode, _) => { - editor.ReplaceNode(afterIfOrElseIf, (currentNode, _) => + var ifStatement = (IfStatementSyntax)currentNode; + var elseIfStatement = (IfStatementSyntax)elseIfClause; + + var newElseIfStatement = elseIfStatement.WithElse(ifStatement.Else); + var newIfStatement = ifStatement.WithElse(SyntaxFactory.ElseClause(newElseIfStatement)); + + if (ifStatement.Else == null && ContainsEmbeddedIfStatement(ifStatement)) { - var ifStatement = (IfStatementSyntax)currentNode; - var elseIfStatement = (IfStatementSyntax)elseIfClause; - - var newElseIfStatement = elseIfStatement.WithElse(ifStatement.Else); - var newIfStatement = ifStatement.WithElse(SyntaxFactory.ElseClause(newElseIfStatement)); - - if (ifStatement.Else == null && ContainsEmbeddedIfStatement(ifStatement)) - { - // If the if statement contains an embedded if statement (not wrapped inside a block), adding an else - // clause might introduce a dangling else problem (the 'else' would bind to the inner if statement), - // so if there used to be no else clause, we'll insert a new block to prevent that. - newIfStatement = newIfStatement.WithStatement(SyntaxFactory.Block(newIfStatement.Statement)); - } - - return newIfStatement; - }); - } + // If the if statement contains an embedded if statement (not wrapped inside a block), adding an else + // clause might introduce a dangling else problem (the 'else' would bind to the inner if statement), + // so if there used to be no else clause, we'll insert a new block to prevent that. + newIfStatement = newIfStatement.WithStatement(SyntaxFactory.Block(newIfStatement.Statement)); + } + + return newIfStatement; + }); + } - public void RemoveElseIfClause(SyntaxEditor editor, SyntaxNode elseIfClause) + public void RemoveElseIfClause(SyntaxEditor editor, SyntaxNode elseIfClause) + { + editor.ReplaceNode(elseIfClause.Parent.Parent, (currentNode, _) => { - editor.ReplaceNode(elseIfClause.Parent.Parent, (currentNode, _) => - { - var parentIfStatement = (IfStatementSyntax)currentNode; - var elseClause = parentIfStatement.Else; - var elseIfStatement = (IfStatementSyntax)elseClause.Statement; - return parentIfStatement.WithElse(elseIfStatement.Else); - }); - } + var parentIfStatement = (IfStatementSyntax)currentNode; + var elseClause = parentIfStatement.Else; + var elseIfStatement = (IfStatementSyntax)elseClause.Statement; + return parentIfStatement.WithElse(elseIfStatement.Else); + }); + } - private static bool ContainsEmbeddedIfStatement(IfStatementSyntax ifStatement) + private static bool ContainsEmbeddedIfStatement(IfStatementSyntax ifStatement) + { + for (var statement = ifStatement.Statement; statement.IsEmbeddedStatementOwner(); statement = statement.GetEmbeddedStatement()) { - for (var statement = ifStatement.Statement; statement.IsEmbeddedStatementOwner(); statement = statement.GetEmbeddedStatement()) + if (statement.IsKind(SyntaxKind.IfStatement)) { - if (statement.IsKind(SyntaxKind.IfStatement)) - { - return true; - } + return true; } - - return false; } + + return false; } } diff --git a/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpMergeConsecutiveIfStatementsCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpMergeConsecutiveIfStatementsCodeRefactoringProvider.cs index 076d0e9fbd1d6..4d49a4342fbad 100644 --- a/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpMergeConsecutiveIfStatementsCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpMergeConsecutiveIfStatementsCodeRefactoringProvider.cs @@ -12,53 +12,52 @@ using Microsoft.CodeAnalysis.SplitOrMergeIfStatements; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.SplitOrMergeIfStatements +namespace Microsoft.CodeAnalysis.CSharp.SplitOrMergeIfStatements; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MergeConsecutiveIfStatements), Shared] +internal sealed class CSharpMergeConsecutiveIfStatementsCodeRefactoringProvider + : AbstractMergeConsecutiveIfStatementsCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MergeConsecutiveIfStatements), Shared] - internal sealed class CSharpMergeConsecutiveIfStatementsCodeRefactoringProvider - : AbstractMergeConsecutiveIfStatementsCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpMergeConsecutiveIfStatementsCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpMergeConsecutiveIfStatementsCodeRefactoringProvider() - { - } + } - protected override bool IsApplicableSpan(SyntaxNode node, TextSpan span, out SyntaxNode ifOrElseIf) + protected override bool IsApplicableSpan(SyntaxNode node, TextSpan span, out SyntaxNode ifOrElseIf) + { + if (node is IfStatementSyntax ifStatement) { - if (node is IfStatementSyntax ifStatement) + // Cases: + // 1. Position is at a child token of an if statement with no selection (e.g. 'if' keyword, a parenthesis) + // 2. Selection around the 'if' keyword + // 3. Selection around the header - from 'if' keyword to the end of the condition + // 4. Selection around the whole if statement *excluding* its else clause - from 'if' keyword to the end of its statement + if (span.Length == 0 || + span.IsAround(ifStatement.IfKeyword) || + span.IsAround(ifStatement.IfKeyword, ifStatement.CloseParenToken) || + span.IsAround(ifStatement.IfKeyword, ifStatement.Statement)) { - // Cases: - // 1. Position is at a child token of an if statement with no selection (e.g. 'if' keyword, a parenthesis) - // 2. Selection around the 'if' keyword - // 3. Selection around the header - from 'if' keyword to the end of the condition - // 4. Selection around the whole if statement *excluding* its else clause - from 'if' keyword to the end of its statement - if (span.Length == 0 || - span.IsAround(ifStatement.IfKeyword) || - span.IsAround(ifStatement.IfKeyword, ifStatement.CloseParenToken) || - span.IsAround(ifStatement.IfKeyword, ifStatement.Statement)) - { - ifOrElseIf = ifStatement; - return true; - } + ifOrElseIf = ifStatement; + return true; } + } - if (node is ElseClauseSyntax elseClause && elseClause.Statement is IfStatementSyntax elseIfStatement) + if (node is ElseClauseSyntax elseClause && elseClause.Statement is IfStatementSyntax elseIfStatement) + { + // 5. Position is at a child token of an else clause with no selection ('else' keyword) + // 6. Selection around the header including the 'else' keyword - from 'else' keyword to the end of the condition + // 7. Selection from the 'else' keyword to the end of the if statement's statement + if (span.Length == 0 || + span.IsAround(elseClause.ElseKeyword, elseIfStatement.CloseParenToken) || + span.IsAround(elseClause.ElseKeyword, elseIfStatement.Statement)) { - // 5. Position is at a child token of an else clause with no selection ('else' keyword) - // 6. Selection around the header including the 'else' keyword - from 'else' keyword to the end of the condition - // 7. Selection from the 'else' keyword to the end of the if statement's statement - if (span.Length == 0 || - span.IsAround(elseClause.ElseKeyword, elseIfStatement.CloseParenToken) || - span.IsAround(elseClause.ElseKeyword, elseIfStatement.Statement)) - { - ifOrElseIf = elseIfStatement; - return true; - } + ifOrElseIf = elseIfStatement; + return true; } - - ifOrElseIf = null; - return false; } + + ifOrElseIf = null; + return false; } } diff --git a/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpMergeNestedIfStatementsCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpMergeNestedIfStatementsCodeRefactoringProvider.cs index f93d4c587e18b..50a2d63cab33d 100644 --- a/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpMergeNestedIfStatementsCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpMergeNestedIfStatementsCodeRefactoringProvider.cs @@ -12,53 +12,52 @@ using Microsoft.CodeAnalysis.SplitOrMergeIfStatements; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.SplitOrMergeIfStatements +namespace Microsoft.CodeAnalysis.CSharp.SplitOrMergeIfStatements; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MergeNestedIfStatements), Shared] +internal sealed class CSharpMergeNestedIfStatementsCodeRefactoringProvider + : AbstractMergeNestedIfStatementsCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MergeNestedIfStatements), Shared] - internal sealed class CSharpMergeNestedIfStatementsCodeRefactoringProvider - : AbstractMergeNestedIfStatementsCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpMergeNestedIfStatementsCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpMergeNestedIfStatementsCodeRefactoringProvider() - { - } + } - protected override bool IsApplicableSpan(SyntaxNode node, TextSpan span, out SyntaxNode ifOrElseIf) + protected override bool IsApplicableSpan(SyntaxNode node, TextSpan span, out SyntaxNode ifOrElseIf) + { + if (node is IfStatementSyntax ifStatement) { - if (node is IfStatementSyntax ifStatement) + // Cases: + // 1. Position is at a child token of an if statement with no selection (e.g. 'if' keyword, a parenthesis) + // 2. Selection around the 'if' keyword + // 3. Selection around the header - from 'if' keyword to the end of the condition + // 4. Selection around the whole if statement + if (span.Length == 0 || + span.IsAround(ifStatement.IfKeyword) || + span.IsAround(ifStatement.IfKeyword, ifStatement.CloseParenToken) || + span.IsAround(ifStatement.IfKeyword, ifStatement)) { - // Cases: - // 1. Position is at a child token of an if statement with no selection (e.g. 'if' keyword, a parenthesis) - // 2. Selection around the 'if' keyword - // 3. Selection around the header - from 'if' keyword to the end of the condition - // 4. Selection around the whole if statement - if (span.Length == 0 || - span.IsAround(ifStatement.IfKeyword) || - span.IsAround(ifStatement.IfKeyword, ifStatement.CloseParenToken) || - span.IsAround(ifStatement.IfKeyword, ifStatement)) - { - ifOrElseIf = ifStatement; - return true; - } + ifOrElseIf = ifStatement; + return true; } + } - if (node is ElseClauseSyntax elseClause && elseClause.Statement is IfStatementSyntax elseIfStatement) + if (node is ElseClauseSyntax elseClause && elseClause.Statement is IfStatementSyntax elseIfStatement) + { + // 5. Position is at a child token of an else clause with no selection ('else' keyword) + // 6. Selection around the header including the 'else' keyword - from 'else' keyword to the end of the condition + // 7. Selection from the 'else' keyword to the end of the if statement + if (span.Length == 0 || + span.IsAround(elseClause.ElseKeyword, elseIfStatement.CloseParenToken) || + span.IsAround(elseClause.ElseKeyword, elseIfStatement)) { - // 5. Position is at a child token of an else clause with no selection ('else' keyword) - // 6. Selection around the header including the 'else' keyword - from 'else' keyword to the end of the condition - // 7. Selection from the 'else' keyword to the end of the if statement - if (span.Length == 0 || - span.IsAround(elseClause.ElseKeyword, elseIfStatement.CloseParenToken) || - span.IsAround(elseClause.ElseKeyword, elseIfStatement)) - { - ifOrElseIf = elseIfStatement; - return true; - } + ifOrElseIf = elseIfStatement; + return true; } - - ifOrElseIf = null; - return false; } + + ifOrElseIf = null; + return false; } } diff --git a/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpSplitIntoConsecutiveIfStatementsCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpSplitIntoConsecutiveIfStatementsCodeRefactoringProvider.cs index 5286c1804b6a6..257d791698fe4 100644 --- a/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpSplitIntoConsecutiveIfStatementsCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpSplitIntoConsecutiveIfStatementsCodeRefactoringProvider.cs @@ -9,17 +9,16 @@ using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.SplitOrMergeIfStatements; -namespace Microsoft.CodeAnalysis.CSharp.SplitOrMergeIfStatements +namespace Microsoft.CodeAnalysis.CSharp.SplitOrMergeIfStatements; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.SplitIntoConsecutiveIfStatements), Shared] +[ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.InvertLogical, Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] +internal sealed class CSharpSplitIntoConsecutiveIfStatementsCodeRefactoringProvider + : AbstractSplitIntoConsecutiveIfStatementsCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.SplitIntoConsecutiveIfStatements), Shared] - [ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.InvertLogical, Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] - internal sealed class CSharpSplitIntoConsecutiveIfStatementsCodeRefactoringProvider - : AbstractSplitIntoConsecutiveIfStatementsCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpSplitIntoConsecutiveIfStatementsCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpSplitIntoConsecutiveIfStatementsCodeRefactoringProvider() - { - } } } diff --git a/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpSplitIntoNestedIfStatementsCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpSplitIntoNestedIfStatementsCodeRefactoringProvider.cs index d7ab97302fb1b..d6109a20cd221 100644 --- a/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpSplitIntoNestedIfStatementsCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/SplitOrMergeIfStatements/CSharpSplitIntoNestedIfStatementsCodeRefactoringProvider.cs @@ -9,17 +9,16 @@ using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.SplitOrMergeIfStatements; -namespace Microsoft.CodeAnalysis.CSharp.SplitOrMergeIfStatements +namespace Microsoft.CodeAnalysis.CSharp.SplitOrMergeIfStatements; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.SplitIntoNestedIfStatements), Shared] +[ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.InvertLogical, Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] +internal sealed class CSharpSplitIntoNestedIfStatementsCodeRefactoringProvider + : AbstractSplitIntoNestedIfStatementsCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.SplitIntoNestedIfStatements), Shared] - [ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.InvertLogical, Before = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] - internal sealed class CSharpSplitIntoNestedIfStatementsCodeRefactoringProvider - : AbstractSplitIntoNestedIfStatementsCodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpSplitIntoNestedIfStatementsCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpSplitIntoNestedIfStatementsCodeRefactoringProvider() - { - } } } diff --git a/src/Features/CSharp/Portable/SplitStringLiteral/InterpolatedStringSplitter.cs b/src/Features/CSharp/Portable/SplitStringLiteral/InterpolatedStringSplitter.cs index 9d02b6d09d571..6fa367b2bc8a8 100644 --- a/src/Features/CSharp/Portable/SplitStringLiteral/InterpolatedStringSplitter.cs +++ b/src/Features/CSharp/Portable/SplitStringLiteral/InterpolatedStringSplitter.cs @@ -12,83 +12,82 @@ using Microsoft.CodeAnalysis.Indentation; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.SplitStringLiteral +namespace Microsoft.CodeAnalysis.CSharp.SplitStringLiteral; + +internal abstract partial class StringSplitter { - internal abstract partial class StringSplitter + private sealed class InterpolatedStringSplitter( + ParsedDocument document, + int position, + InterpolatedStringExpressionSyntax interpolatedStringExpression, + IndentationOptions indentationOptions, + CancellationToken cancellationToken) : StringSplitter(document, position, indentationOptions, cancellationToken) { - private sealed class InterpolatedStringSplitter( - ParsedDocument document, - int position, - InterpolatedStringExpressionSyntax interpolatedStringExpression, - IndentationOptions indentationOptions, - CancellationToken cancellationToken) : StringSplitter(document, position, indentationOptions, cancellationToken) - { - private readonly InterpolatedStringExpressionSyntax _interpolatedStringExpression = interpolatedStringExpression; + private readonly InterpolatedStringExpressionSyntax _interpolatedStringExpression = interpolatedStringExpression; - protected override SyntaxNode GetNodeToReplace() => _interpolatedStringExpression; + protected override SyntaxNode GetNodeToReplace() => _interpolatedStringExpression; - // Don't offer on $@"" strings and raw string literals. They support newlines directly in their content. - protected override bool CheckToken() - => _interpolatedStringExpression.StringStartToken.Kind() == SyntaxKind.InterpolatedStringStartToken; + // Don't offer on $@"" strings and raw string literals. They support newlines directly in their content. + protected override bool CheckToken() + => _interpolatedStringExpression.StringStartToken.Kind() == SyntaxKind.InterpolatedStringStartToken; - protected override BinaryExpressionSyntax CreateSplitString() - { - var contents = _interpolatedStringExpression.Contents.ToList(); + protected override BinaryExpressionSyntax CreateSplitString() + { + var contents = _interpolatedStringExpression.Contents.ToList(); - var beforeSplitContents = new List(); - var afterSplitContents = new List(); + var beforeSplitContents = new List(); + var afterSplitContents = new List(); - foreach (var content in contents) + foreach (var content in contents) + { + if (content.Span.End <= CursorPosition) { - if (content.Span.End <= CursorPosition) - { - // Content is entirely before the cursor. Nothing needs to be done to it. - beforeSplitContents.Add(content); - } - else if (content.Span.Start >= CursorPosition) - { - // Content is entirely after the cursor. Nothing needs to be done to it. - afterSplitContents.Add(content); - } - else - { - // Content crosses the cursor. Need to split it. - beforeSplitContents.Add(CreateInterpolatedStringText(content.SpanStart, CursorPosition)); - afterSplitContents.Insert(0, CreateInterpolatedStringText(CursorPosition, content.Span.End)); - } + // Content is entirely before the cursor. Nothing needs to be done to it. + beforeSplitContents.Add(content); } + else if (content.Span.Start >= CursorPosition) + { + // Content is entirely after the cursor. Nothing needs to be done to it. + afterSplitContents.Add(content); + } + else + { + // Content crosses the cursor. Need to split it. + beforeSplitContents.Add(CreateInterpolatedStringText(content.SpanStart, CursorPosition)); + afterSplitContents.Insert(0, CreateInterpolatedStringText(CursorPosition, content.Span.End)); + } + } - var leftExpression = SyntaxFactory.InterpolatedStringExpression( - _interpolatedStringExpression.StringStartToken, - [.. beforeSplitContents], - SyntaxFactory.Token(SyntaxKind.InterpolatedStringEndToken) - .WithTrailingTrivia(SyntaxFactory.ElasticSpace)); - - var rightExpression = SyntaxFactory.InterpolatedStringExpression( - SyntaxFactory.Token(SyntaxKind.InterpolatedStringStartToken), - [.. afterSplitContents], - _interpolatedStringExpression.StringEndToken); + var leftExpression = SyntaxFactory.InterpolatedStringExpression( + _interpolatedStringExpression.StringStartToken, + [.. beforeSplitContents], + SyntaxFactory.Token(SyntaxKind.InterpolatedStringEndToken) + .WithTrailingTrivia(SyntaxFactory.ElasticSpace)); - return SyntaxFactory.BinaryExpression( - SyntaxKind.AddExpression, - leftExpression, - PlusNewLineToken, - rightExpression.WithAdditionalAnnotations(RightNodeAnnotation)); - } + var rightExpression = SyntaxFactory.InterpolatedStringExpression( + SyntaxFactory.Token(SyntaxKind.InterpolatedStringStartToken), + [.. afterSplitContents], + _interpolatedStringExpression.StringEndToken); - private InterpolatedStringTextSyntax CreateInterpolatedStringText(int start, int end) - { - var content = Document.Text.ToString(TextSpan.FromBounds(start, end)); - return SyntaxFactory.InterpolatedStringText( - SyntaxFactory.Token( - leading: default, - kind: SyntaxKind.InterpolatedStringTextToken, - text: content, - valueText: "", - trailing: default)); - } + return SyntaxFactory.BinaryExpression( + SyntaxKind.AddExpression, + leftExpression, + PlusNewLineToken, + rightExpression.WithAdditionalAnnotations(RightNodeAnnotation)); + } - protected override int StringOpenQuoteLength() => "$\"".Length; + private InterpolatedStringTextSyntax CreateInterpolatedStringText(int start, int end) + { + var content = Document.Text.ToString(TextSpan.FromBounds(start, end)); + return SyntaxFactory.InterpolatedStringText( + SyntaxFactory.Token( + leading: default, + kind: SyntaxKind.InterpolatedStringTextToken, + text: content, + valueText: "", + trailing: default)); } + + protected override int StringOpenQuoteLength() => "$\"".Length; } } diff --git a/src/Features/CSharp/Portable/SplitStringLiteral/SimpleStringSplitter.cs b/src/Features/CSharp/Portable/SplitStringLiteral/SimpleStringSplitter.cs index 577785cef28fd..b5aa061a216dd 100644 --- a/src/Features/CSharp/Portable/SplitStringLiteral/SimpleStringSplitter.cs +++ b/src/Features/CSharp/Portable/SplitStringLiteral/SimpleStringSplitter.cs @@ -10,69 +10,68 @@ using Microsoft.CodeAnalysis.Indentation; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.SplitStringLiteral +namespace Microsoft.CodeAnalysis.CSharp.SplitStringLiteral; + +internal abstract partial class StringSplitter { - internal abstract partial class StringSplitter + private sealed class SimpleStringSplitter( + ParsedDocument document, + int position, + SyntaxToken token, + in IndentationOptions indentationOptions, + CancellationToken cancellationToken) : StringSplitter(document, position, indentationOptions, cancellationToken) { - private sealed class SimpleStringSplitter( - ParsedDocument document, - int position, - SyntaxToken token, - in IndentationOptions indentationOptions, - CancellationToken cancellationToken) : StringSplitter(document, position, indentationOptions, cancellationToken) - { - private const char QuoteCharacter = '"'; - private readonly SyntaxToken _token = token; + private const char QuoteCharacter = '"'; + private readonly SyntaxToken _token = token; - // Don't split @"" strings. They already support directly embedding newlines. - // Don't split UTF-8 strings if the cursor is after the quote. - protected override bool CheckToken() - => !_token.IsVerbatimStringLiteral() && !CursorIsAfterQuotesInUtf8String(); + // Don't split @"" strings. They already support directly embedding newlines. + // Don't split UTF-8 strings if the cursor is after the quote. + protected override bool CheckToken() + => !_token.IsVerbatimStringLiteral() && !CursorIsAfterQuotesInUtf8String(); - private bool CursorIsAfterQuotesInUtf8String() - { - return _token.IsKind(SyntaxKind.Utf8StringLiteralToken) && CursorPosition >= _token.Span.End - "u8".Length; - } - - protected override SyntaxNode GetNodeToReplace() => _token.Parent; + private bool CursorIsAfterQuotesInUtf8String() + { + return _token.IsKind(SyntaxKind.Utf8StringLiteralToken) && CursorPosition >= _token.Span.End - "u8".Length; + } - protected override BinaryExpressionSyntax CreateSplitString() - { - // TODO(cyrusn): Deal with the positoin being after a \ character - var prefix = Document.Text.GetSubText(TextSpan.FromBounds(_token.SpanStart, CursorPosition)).ToString(); - var suffix = Document.Text.GetSubText(TextSpan.FromBounds(CursorPosition, _token.Span.End)).ToString(); + protected override SyntaxNode GetNodeToReplace() => _token.Parent; - // If we're spliting a UTF-8 string we need to keep the u8 suffix on the first part. We copy whatever - // the user had on the second part, for consistency. - var firstTokenSuffix = _token.Kind() == SyntaxKind.Utf8StringLiteralToken - ? Document.Text.GetSubText(TextSpan.FromBounds(_token.Span.End - "u8".Length, _token.Span.End)).ToString() - : ""; + protected override BinaryExpressionSyntax CreateSplitString() + { + // TODO(cyrusn): Deal with the positoin being after a \ character + var prefix = Document.Text.GetSubText(TextSpan.FromBounds(_token.SpanStart, CursorPosition)).ToString(); + var suffix = Document.Text.GetSubText(TextSpan.FromBounds(CursorPosition, _token.Span.End)).ToString(); - var firstToken = SyntaxFactory.Token( - _token.LeadingTrivia, - _token.Kind(), - text: prefix + QuoteCharacter + firstTokenSuffix, - valueText: "", - trailing: [SyntaxFactory.ElasticSpace]); + // If we're spliting a UTF-8 string we need to keep the u8 suffix on the first part. We copy whatever + // the user had on the second part, for consistency. + var firstTokenSuffix = _token.Kind() == SyntaxKind.Utf8StringLiteralToken + ? Document.Text.GetSubText(TextSpan.FromBounds(_token.Span.End - "u8".Length, _token.Span.End)).ToString() + : ""; - var secondToken = SyntaxFactory.Token( - default, - _token.Kind(), - text: QuoteCharacter + suffix, - valueText: "", - trailing: _token.TrailingTrivia); + var firstToken = SyntaxFactory.Token( + _token.LeadingTrivia, + _token.Kind(), + text: prefix + QuoteCharacter + firstTokenSuffix, + valueText: "", + trailing: [SyntaxFactory.ElasticSpace]); - var leftExpression = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, firstToken); - var rightExpression = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, secondToken); + var secondToken = SyntaxFactory.Token( + default, + _token.Kind(), + text: QuoteCharacter + suffix, + valueText: "", + trailing: _token.TrailingTrivia); - return SyntaxFactory.BinaryExpression( - SyntaxKind.AddExpression, - leftExpression, - PlusNewLineToken, - rightExpression.WithAdditionalAnnotations(RightNodeAnnotation)); - } + var leftExpression = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, firstToken); + var rightExpression = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, secondToken); - protected override int StringOpenQuoteLength() => "\"".Length; + return SyntaxFactory.BinaryExpression( + SyntaxKind.AddExpression, + leftExpression, + PlusNewLineToken, + rightExpression.WithAdditionalAnnotations(RightNodeAnnotation)); } + + protected override int StringOpenQuoteLength() => "\"".Length; } } diff --git a/src/Features/CSharp/Portable/SplitStringLiteral/StringSplitter.cs b/src/Features/CSharp/Portable/SplitStringLiteral/StringSplitter.cs index d31f8bb3d3f68..ee8be956f56da 100644 --- a/src/Features/CSharp/Portable/SplitStringLiteral/StringSplitter.cs +++ b/src/Features/CSharp/Portable/SplitStringLiteral/StringSplitter.cs @@ -10,133 +10,132 @@ using Microsoft.CodeAnalysis.Indentation; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CSharp.SplitStringLiteral +namespace Microsoft.CodeAnalysis.CSharp.SplitStringLiteral; + +internal abstract partial class StringSplitter( + ParsedDocument document, int position, + in IndentationOptions indentationOptions, + CancellationToken cancellationToken) { - internal abstract partial class StringSplitter( + protected readonly SyntaxAnnotation RightNodeAnnotation = new(); + + protected readonly ParsedDocument Document = document; + protected readonly int CursorPosition = position; + protected readonly IndentationOptions IndentationOptions = indentationOptions; + protected readonly CancellationToken CancellationToken = cancellationToken; + protected readonly SyntaxToken PlusNewLineToken = SyntaxFactory.Token( + leading: default, + SyntaxKind.PlusToken, + [SyntaxFactory.EndOfLine( + indentationOptions.FormattingOptions.NewLine)]); + + protected int TabSize => IndentationOptions.FormattingOptions.TabSize; + protected bool UseTabs => IndentationOptions.FormattingOptions.UseTabs; + + public static StringSplitter? TryCreate( ParsedDocument document, int position, in IndentationOptions indentationOptions, CancellationToken cancellationToken) { - protected readonly SyntaxAnnotation RightNodeAnnotation = new(); - - protected readonly ParsedDocument Document = document; - protected readonly int CursorPosition = position; - protected readonly IndentationOptions IndentationOptions = indentationOptions; - protected readonly CancellationToken CancellationToken = cancellationToken; - protected readonly SyntaxToken PlusNewLineToken = SyntaxFactory.Token( - leading: default, - SyntaxKind.PlusToken, - [SyntaxFactory.EndOfLine( - indentationOptions.FormattingOptions.NewLine)]); - - protected int TabSize => IndentationOptions.FormattingOptions.TabSize; - protected bool UseTabs => IndentationOptions.FormattingOptions.UseTabs; - - public static StringSplitter? TryCreate( - ParsedDocument document, int position, - in IndentationOptions indentationOptions, - CancellationToken cancellationToken) + var token = document.Root.FindToken(position); + + if (token.Kind() is SyntaxKind.StringLiteralToken or SyntaxKind.Utf8StringLiteralToken) { - var token = document.Root.FindToken(position); - - if (token.Kind() is SyntaxKind.StringLiteralToken or SyntaxKind.Utf8StringLiteralToken) - { - return new SimpleStringSplitter( - document, position, token, indentationOptions, cancellationToken); - } - - var interpolatedStringExpression = TryGetInterpolatedStringExpression(token, position); - if (interpolatedStringExpression != null) - { - return new InterpolatedStringSplitter( - document, position, interpolatedStringExpression, indentationOptions, cancellationToken); - } - - return null; + return new SimpleStringSplitter( + document, position, token, indentationOptions, cancellationToken); } - private static InterpolatedStringExpressionSyntax? TryGetInterpolatedStringExpression( - SyntaxToken token, int position) + var interpolatedStringExpression = TryGetInterpolatedStringExpression(token, position); + if (interpolatedStringExpression != null) { - if (token.Kind() is SyntaxKind.InterpolatedStringTextToken or SyntaxKind.InterpolatedStringEndToken || - IsInterpolationOpenBrace(token, position)) - { - return token.GetAncestor(); - } - - return null; + return new InterpolatedStringSplitter( + document, position, interpolatedStringExpression, indentationOptions, cancellationToken); } - private static bool IsInterpolationOpenBrace(SyntaxToken token, int position) + return null; + } + + private static InterpolatedStringExpressionSyntax? TryGetInterpolatedStringExpression( + SyntaxToken token, int position) + { + if (token.Kind() is SyntaxKind.InterpolatedStringTextToken or SyntaxKind.InterpolatedStringEndToken || + IsInterpolationOpenBrace(token, position)) { - return token.Kind() == SyntaxKind.OpenBraceToken && - token.Parent.IsKind(SyntaxKind.Interpolation) && - position == token.SpanStart; + return token.GetAncestor(); } - protected abstract int StringOpenQuoteLength(); + return null; + } + + private static bool IsInterpolationOpenBrace(SyntaxToken token, int position) + { + return token.Kind() == SyntaxKind.OpenBraceToken && + token.Parent.IsKind(SyntaxKind.Interpolation) && + position == token.SpanStart; + } + + protected abstract int StringOpenQuoteLength(); - protected abstract bool CheckToken(); + protected abstract bool CheckToken(); - protected abstract SyntaxNode GetNodeToReplace(); + protected abstract SyntaxNode GetNodeToReplace(); - protected abstract BinaryExpressionSyntax CreateSplitString(); + protected abstract BinaryExpressionSyntax CreateSplitString(); - public bool TrySplit([NotNullWhen(true)] out SyntaxNode? newRoot, out int newPosition) + public bool TrySplit([NotNullWhen(true)] out SyntaxNode? newRoot, out int newPosition) + { + var nodeToReplace = GetNodeToReplace(); + + if (CursorPosition <= nodeToReplace.SpanStart || CursorPosition >= nodeToReplace.Span.End) { - var nodeToReplace = GetNodeToReplace(); - - if (CursorPosition <= nodeToReplace.SpanStart || CursorPosition >= nodeToReplace.Span.End) - { - newRoot = null; - newPosition = 0; - return false; - } - - if (!CheckToken()) - { - newRoot = null; - newPosition = 0; - return false; - } - - (newRoot, newPosition) = SplitString(); - return true; + newRoot = null; + newPosition = 0; + return false; } - private (SyntaxNode root, int caretPosition) SplitString() + if (!CheckToken()) { - var splitString = CreateSplitString(); + newRoot = null; + newPosition = 0; + return false; + } - var nodeToReplace = GetNodeToReplace(); - var newRoot = Document.Root.ReplaceNode(nodeToReplace, splitString); - var rightExpression = newRoot.GetAnnotatedNodes(RightNodeAnnotation).Single(); + (newRoot, newPosition) = SplitString(); + return true; + } - var indentString = GetIndentString(newRoot); - var newRightExpression = rightExpression.WithLeadingTrivia(SyntaxFactory.ElasticWhitespace(indentString)); - var newRoot2 = newRoot.ReplaceNode(rightExpression, newRightExpression); + private (SyntaxNode root, int caretPosition) SplitString() + { + var splitString = CreateSplitString(); - return (newRoot2, rightExpression.Span.Start + indentString.Length + StringOpenQuoteLength()); - } + var nodeToReplace = GetNodeToReplace(); + var newRoot = Document.Root.ReplaceNode(nodeToReplace, splitString); + var rightExpression = newRoot.GetAnnotatedNodes(RightNodeAnnotation).Single(); - private string GetIndentString(SyntaxNode newRoot) - { - var indentationService = Document.LanguageServices.GetRequiredService(); - var originalLineNumber = Document.Text.Lines.GetLineFromPosition(CursorPosition).LineNumber; + var indentString = GetIndentString(newRoot); + var newRightExpression = rightExpression.WithLeadingTrivia(SyntaxFactory.ElasticWhitespace(indentString)); + var newRoot2 = newRoot.ReplaceNode(rightExpression, newRightExpression); - var newDocument = Document.WithChangedRoot(newRoot, CancellationToken); - var desiredIndentation = indentationService.GetIndentation( - newDocument, originalLineNumber + 1, IndentationOptions, CancellationToken); + return (newRoot2, rightExpression.Span.Start + indentString.Length + StringOpenQuoteLength()); + } - var newSourceText = newDocument.Text; - var baseLine = newSourceText.Lines.GetLineFromPosition(desiredIndentation.BasePosition); + private string GetIndentString(SyntaxNode newRoot) + { + var indentationService = Document.LanguageServices.GetRequiredService(); + var originalLineNumber = Document.Text.Lines.GetLineFromPosition(CursorPosition).LineNumber; - var baseOffsetInLineInPositions = desiredIndentation.BasePosition - baseLine.Start; - var baseOffsetInLineInColumns = baseLine.GetColumnFromLineOffset(baseOffsetInLineInPositions, TabSize); + var newDocument = Document.WithChangedRoot(newRoot, CancellationToken); + var desiredIndentation = indentationService.GetIndentation( + newDocument, originalLineNumber + 1, IndentationOptions, CancellationToken); - var indent = baseOffsetInLineInColumns + desiredIndentation.Offset; - var indentString = indent.CreateIndentationString(UseTabs, TabSize); - return indentString; - } + var newSourceText = newDocument.Text; + var baseLine = newSourceText.Lines.GetLineFromPosition(desiredIndentation.BasePosition); + + var baseOffsetInLineInPositions = desiredIndentation.BasePosition - baseLine.Start; + var baseOffsetInLineInColumns = baseLine.GetColumnFromLineOffset(baseOffsetInLineInPositions, TabSize); + + var indent = baseOffsetInLineInColumns + desiredIndentation.Offset; + var indentString = indent.CreateIndentationString(UseTabs, TabSize); + return indentString; } } diff --git a/src/Features/CSharp/Portable/StringIndentation/CSharpStringIndentationService.cs b/src/Features/CSharp/Portable/StringIndentation/CSharpStringIndentationService.cs index eba775eda31e8..61b5753854a91 100644 --- a/src/Features/CSharp/Portable/StringIndentation/CSharpStringIndentationService.cs +++ b/src/Features/CSharp/Portable/StringIndentation/CSharpStringIndentationService.cs @@ -16,172 +16,171 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.StringIndentation +namespace Microsoft.CodeAnalysis.CSharp.StringIndentation; + +[ExportLanguageService(typeof(IStringIndentationService), LanguageNames.CSharp), Shared] +internal sealed class CSharpStringIndentationService : IStringIndentationService { - [ExportLanguageService(typeof(IStringIndentationService), LanguageNames.CSharp), Shared] - internal sealed class CSharpStringIndentationService : IStringIndentationService + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpStringIndentationService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpStringIndentationService() - { - } + } - public async Task> GetStringIndentationRegionsAsync( - Document document, TextSpan textSpan, CancellationToken cancellationToken) - { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + public async Task> GetStringIndentationRegionsAsync( + Document document, TextSpan textSpan, CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var result); + using var _ = ArrayBuilder.GetInstance(out var result); - Recurse(text, root, textSpan, result, cancellationToken); + Recurse(text, root, textSpan, result, cancellationToken); - return result.ToImmutable(); - } + return result.ToImmutable(); + } - private static void Recurse( - SourceText text, - SyntaxNode root, - TextSpan textSpan, - ArrayBuilder result, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + private static void Recurse( + SourceText text, + SyntaxNode root, + TextSpan textSpan, + ArrayBuilder result, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - using var _ = ArrayBuilder.GetInstance(out var nodeStack); - nodeStack.Add(root); + using var _ = ArrayBuilder.GetInstance(out var nodeStack); + nodeStack.Add(root); - while (nodeStack.TryPop(out var node)) + while (nodeStack.TryPop(out var node)) + { + // DoNot' bother recursing into nodes that don't hit the requested span, they can never contribute + // regions of interest. + if (!node.Span.IntersectsWith(textSpan)) + continue; + + if (node is InterpolatedStringExpressionSyntax interpolatedString && + interpolatedString.StringStartToken.IsKind(SyntaxKind.InterpolatedMultiLineRawStringStartToken)) { - // DoNot' bother recursing into nodes that don't hit the requested span, they can never contribute - // regions of interest. - if (!node.Span.IntersectsWith(textSpan)) - continue; + ProcessInterpolatedStringExpression(text, interpolatedString, result, cancellationToken); + } - if (node is InterpolatedStringExpressionSyntax interpolatedString && - interpolatedString.StringStartToken.IsKind(SyntaxKind.InterpolatedMultiLineRawStringStartToken)) + foreach (var child in node.ChildNodesAndTokens().Reverse()) + { + if (child.IsNode) { - ProcessInterpolatedStringExpression(text, interpolatedString, result, cancellationToken); + nodeStack.Add(child.AsNode()!); } - - foreach (var child in node.ChildNodesAndTokens().Reverse()) + else if (child.IsToken) { - if (child.IsNode) - { - nodeStack.Add(child.AsNode()!); - } - else if (child.IsToken) - { - if (child.Kind() is SyntaxKind.MultiLineRawStringLiteralToken or SyntaxKind.Utf8MultiLineRawStringLiteralToken) - ProcessMultiLineRawStringLiteralToken(text, child.AsToken(), result, cancellationToken); - } + if (child.Kind() is SyntaxKind.MultiLineRawStringLiteralToken or SyntaxKind.Utf8MultiLineRawStringLiteralToken) + ProcessMultiLineRawStringLiteralToken(text, child.AsToken(), result, cancellationToken); } } } + } - private static void ProcessMultiLineRawStringLiteralToken( - SourceText text, SyntaxToken token, ArrayBuilder result, CancellationToken cancellationToken) - { - // Ignore strings with errors as we don't want to draw a line in a bad place that makes things even harder - // to understand. - if (token.ContainsDiagnostics && token.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) - return; + private static void ProcessMultiLineRawStringLiteralToken( + SourceText text, SyntaxToken token, ArrayBuilder result, CancellationToken cancellationToken) + { + // Ignore strings with errors as we don't want to draw a line in a bad place that makes things even harder + // to understand. + if (token.ContainsDiagnostics && token.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + return; - cancellationToken.ThrowIfCancellationRequested(); - if (!TryGetIndentSpan(text, (ExpressionSyntax)token.GetRequiredParent(), out _, out var indentSpan)) - return; + cancellationToken.ThrowIfCancellationRequested(); + if (!TryGetIndentSpan(text, (ExpressionSyntax)token.GetRequiredParent(), out _, out var indentSpan)) + return; - result.Add(new StringIndentationRegion(indentSpan)); - } + result.Add(new StringIndentationRegion(indentSpan)); + } - private static void ProcessInterpolatedStringExpression(SourceText text, InterpolatedStringExpressionSyntax interpolatedString, ArrayBuilder result, CancellationToken cancellationToken) + private static void ProcessInterpolatedStringExpression(SourceText text, InterpolatedStringExpressionSyntax interpolatedString, ArrayBuilder result, CancellationToken cancellationToken) + { + // Ignore strings with errors as we don't want to draw a line in a bad place that makes things even harder + // to understand. + if (interpolatedString.ContainsDiagnostics) { - // Ignore strings with errors as we don't want to draw a line in a bad place that makes things even harder - // to understand. - if (interpolatedString.ContainsDiagnostics) + var errors = interpolatedString.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error); + foreach (var error in errors) { - var errors = interpolatedString.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error); - foreach (var error in errors) - { - if (!IsInHole(interpolatedString, error.Location.SourceSpan)) - return; - } + if (!IsInHole(interpolatedString, error.Location.SourceSpan)) + return; } + } - cancellationToken.ThrowIfCancellationRequested(); - if (!TryGetIndentSpan(text, interpolatedString, out var offset, out var indentSpan)) - return; + cancellationToken.ThrowIfCancellationRequested(); + if (!TryGetIndentSpan(text, interpolatedString, out var offset, out var indentSpan)) + return; - using var _ = ArrayBuilder.GetInstance(out var builder); + using var _ = ArrayBuilder.GetInstance(out var builder); - foreach (var content in interpolatedString.Contents) + foreach (var content in interpolatedString.Contents) + { + if (content is InterpolationSyntax interpolation && + !IgnoreInterpolation(text, offset, interpolation)) { - if (content is InterpolationSyntax interpolation && - !IgnoreInterpolation(text, offset, interpolation)) - { - builder.Add(interpolation.Span); - } + builder.Add(interpolation.Span); } - - result.Add(new StringIndentationRegion(indentSpan, builder.ToImmutable())); } - private static bool IsInHole(InterpolatedStringExpressionSyntax interpolatedString, TextSpan sourceSpan) - { - foreach (var content in interpolatedString.Contents) - { - if (content is InterpolationSyntax && content.Span.Contains(sourceSpan)) - return true; - } + result.Add(new StringIndentationRegion(indentSpan, builder.ToImmutable())); + } - return false; + private static bool IsInHole(InterpolatedStringExpressionSyntax interpolatedString, TextSpan sourceSpan) + { + foreach (var content in interpolatedString.Contents) + { + if (content is InterpolationSyntax && content.Span.Contains(sourceSpan)) + return true; } - private static bool IgnoreInterpolation(SourceText text, int offset, InterpolationSyntax interpolation) - { - // We can ignore the hole if all the content of it is after the region's indentation level. - // In that case, it's fine to draw the line through the hole as it won't intersect any code - // (or show up on the right side of the line). + return false; + } - var holeStartLine = text.Lines.GetLineFromPosition(interpolation.SpanStart).LineNumber; - var holeEndLine = text.Lines.GetLineFromPosition(interpolation.Span.End).LineNumber; + private static bool IgnoreInterpolation(SourceText text, int offset, InterpolationSyntax interpolation) + { + // We can ignore the hole if all the content of it is after the region's indentation level. + // In that case, it's fine to draw the line through the hole as it won't intersect any code + // (or show up on the right side of the line). - for (var i = holeStartLine; i <= holeEndLine; i++) - { - var line = text.Lines[i]; - var currentLineOffset = line.GetFirstNonWhitespaceOffset(); + var holeStartLine = text.Lines.GetLineFromPosition(interpolation.SpanStart).LineNumber; + var holeEndLine = text.Lines.GetLineFromPosition(interpolation.Span.End).LineNumber; - if (currentLineOffset != null && currentLineOffset < offset) - return false; - } + for (var i = holeStartLine; i <= holeEndLine; i++) + { + var line = text.Lines[i]; + var currentLineOffset = line.GetFirstNonWhitespaceOffset(); - return true; + if (currentLineOffset != null && currentLineOffset < offset) + return false; } - private static bool TryGetIndentSpan(SourceText text, ExpressionSyntax expression, out int offset, out TextSpan indentSpan) - { - indentSpan = default; + return true; + } - // get the last line of the literal to determine the indentation string. - var lastLine = text.Lines.GetLineFromPosition(expression.Span.End); - var offsetOpt = lastLine.GetFirstNonWhitespaceOffset(); + private static bool TryGetIndentSpan(SourceText text, ExpressionSyntax expression, out int offset, out TextSpan indentSpan) + { + indentSpan = default; - // We should always have a non-null offset in a multi-line raw string without errors. - Contract.ThrowIfNull(offsetOpt); - offset = offsetOpt.Value; - if (offset == 0) - return false; + // get the last line of the literal to determine the indentation string. + var lastLine = text.Lines.GetLineFromPosition(expression.Span.End); + var offsetOpt = lastLine.GetFirstNonWhitespaceOffset(); - var firstLine = text.Lines.GetLineFromPosition(expression.SpanStart); + // We should always have a non-null offset in a multi-line raw string without errors. + Contract.ThrowIfNull(offsetOpt); + offset = offsetOpt.Value; + if (offset == 0) + return false; - // A literal without errors must span at least three lines. Like so: - // """ - // foo - // """ - Contract.ThrowIfTrue(lastLine.LineNumber - firstLine.LineNumber < 2); - indentSpan = TextSpan.FromBounds(firstLine.Start, lastLine.Start + offset); - return true; - } + var firstLine = text.Lines.GetLineFromPosition(expression.SpanStart); + + // A literal without errors must span at least three lines. Like so: + // """ + // foo + // """ + Contract.ThrowIfTrue(lastLine.LineNumber - firstLine.LineNumber < 2); + indentSpan = TextSpan.FromBounds(firstLine.Start, lastLine.Start + offset); + return true; } } diff --git a/src/Features/CSharp/Portable/Structure/CSharpBlockStructureProvider.cs b/src/Features/CSharp/Portable/Structure/CSharpBlockStructureProvider.cs index a680f96a5562f..e06dd0d316c72 100644 --- a/src/Features/CSharp/Portable/Structure/CSharpBlockStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/CSharpBlockStructureProvider.cs @@ -9,66 +9,66 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class CSharpBlockStructureProvider : AbstractBlockStructureProvider { - internal class CSharpBlockStructureProvider : AbstractBlockStructureProvider + private static ImmutableDictionary> CreateDefaultNodeProviderMap() { - private static ImmutableDictionary> CreateDefaultNodeProviderMap() - { - var builder = ImmutableDictionary.CreateBuilder>(); + var builder = ImmutableDictionary.CreateBuilder>(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); - return builder.ToImmutable(); - } + return builder.ToImmutable(); + } - private static ImmutableDictionary> CreateDefaultTriviaProviderMap() - { - var builder = ImmutableDictionary.CreateBuilder>(); + private static ImmutableDictionary> CreateDefaultTriviaProviderMap() + { + var builder = ImmutableDictionary.CreateBuilder>(); - builder.Add((int)SyntaxKind.DisabledTextTrivia, [new DisabledTextTriviaStructureProvider()]); - builder.Add((int)SyntaxKind.MultiLineCommentTrivia, [new MultilineCommentBlockStructureProvider()]); + builder.Add((int)SyntaxKind.DisabledTextTrivia, [new DisabledTextTriviaStructureProvider()]); + builder.Add((int)SyntaxKind.MultiLineCommentTrivia, [new MultilineCommentBlockStructureProvider()]); - return builder.ToImmutable(); - } + return builder.ToImmutable(); + } - internal CSharpBlockStructureProvider() - : base(CreateDefaultNodeProviderMap(), CreateDefaultTriviaProviderMap()) - { - } + internal CSharpBlockStructureProvider() + : base(CreateDefaultNodeProviderMap(), CreateDefaultTriviaProviderMap()) + { } } diff --git a/src/Features/CSharp/Portable/Structure/CSharpBlockStructureService.cs b/src/Features/CSharp/Portable/Structure/CSharpBlockStructureService.cs index 3345d099b91d3..4f3f2c1fd04e1 100644 --- a/src/Features/CSharp/Portable/Structure/CSharpBlockStructureService.cs +++ b/src/Features/CSharp/Portable/Structure/CSharpBlockStructureService.cs @@ -11,28 +11,27 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +[ExportLanguageServiceFactory(typeof(BlockStructureService), LanguageNames.CSharp), Shared] +internal class CSharpBlockStructureServiceFactory : ILanguageServiceFactory { - [ExportLanguageServiceFactory(typeof(BlockStructureService), LanguageNames.CSharp), Shared] - internal class CSharpBlockStructureServiceFactory : ILanguageServiceFactory + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpBlockStructureServiceFactory() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpBlockStructureServiceFactory() - { - } - - public ILanguageService CreateLanguageService(HostLanguageServices languageServices) - => new CSharpBlockStructureService(languageServices.LanguageServices.SolutionServices); } - internal class CSharpBlockStructureService(SolutionServices services) : BlockStructureServiceWithProviders(services) - { - protected override ImmutableArray GetBuiltInProviders() - { - return [new CSharpBlockStructureProvider()]; - } + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + => new CSharpBlockStructureService(languageServices.LanguageServices.SolutionServices); +} - public override string Language => LanguageNames.CSharp; +internal class CSharpBlockStructureService(SolutionServices services) : BlockStructureServiceWithProviders(services) +{ + protected override ImmutableArray GetBuiltInProviders() + { + return [new CSharpBlockStructureProvider()]; } + + public override string Language => LanguageNames.CSharp; } diff --git a/src/Features/CSharp/Portable/Structure/Providers/AccessorDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/AccessorDeclarationStructureProvider.cs index acec24d90d3ea..1ba98fef67012 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/AccessorDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/AccessorDeclarationStructureProvider.cs @@ -7,43 +7,42 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class AccessorDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class AccessorDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + AccessorDeclarationSyntax accessorDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - AccessorDeclarationSyntax accessorDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(accessorDeclaration, ref spans, options); + CSharpStructureHelpers.CollectCommentBlockSpans(accessorDeclaration, ref spans, options); - // fault tolerance - if (accessorDeclaration.Body == null || - accessorDeclaration.Body.OpenBraceToken.IsMissing || - accessorDeclaration.Body.CloseBraceToken.IsMissing) - { - return; - } + // fault tolerance + if (accessorDeclaration.Body == null || + accessorDeclaration.Body.OpenBraceToken.IsMissing || + accessorDeclaration.Body.CloseBraceToken.IsMissing) + { + return; + } - SyntaxNodeOrToken current = accessorDeclaration; - var nextSibling = current.GetNextSibling(); + SyntaxNodeOrToken current = accessorDeclaration; + var nextSibling = current.GetNextSibling(); - // Check IsNode to compress blank lines after this node if it is the last child of the parent. - // - // All accessor kinds are grouped together in Metadata as Source. - var compressEmptyLines = options.IsMetadataAsSource - && (!nextSibling.IsNode || nextSibling.AsNode() is AccessorDeclarationSyntax); + // Check IsNode to compress blank lines after this node if it is the last child of the parent. + // + // All accessor kinds are grouped together in Metadata as Source. + var compressEmptyLines = options.IsMetadataAsSource + && (!nextSibling.IsNode || nextSibling.AsNode() is AccessorDeclarationSyntax); - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - accessorDeclaration, - accessorDeclaration.Keyword, - compressEmptyLines: compressEmptyLines, - autoCollapse: true, - type: BlockTypes.Member, - isCollapsible: true)); - } + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + accessorDeclaration, + accessorDeclaration.Keyword, + compressEmptyLines: compressEmptyLines, + autoCollapse: true, + type: BlockTypes.Member, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/AnonymousMethodExpressionStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/AnonymousMethodExpressionStructureProvider.cs index 5541026c4c452..2d755c981f82f 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/AnonymousMethodExpressionStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/AnonymousMethodExpressionStructureProvider.cs @@ -7,43 +7,42 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class AnonymousMethodExpressionStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class AnonymousMethodExpressionStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + AnonymousMethodExpressionSyntax anonymousMethod, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - AnonymousMethodExpressionSyntax anonymousMethod, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + // fault tolerance + if (anonymousMethod.Block.IsMissing || + anonymousMethod.Block.OpenBraceToken.IsMissing || + anonymousMethod.Block.CloseBraceToken.IsMissing) { - // fault tolerance - if (anonymousMethod.Block.IsMissing || - anonymousMethod.Block.OpenBraceToken.IsMissing || - anonymousMethod.Block.CloseBraceToken.IsMissing) - { - return; - } + return; + } - var lastToken = CSharpStructureHelpers.GetLastInlineMethodBlockToken(anonymousMethod); - if (lastToken.Kind() == SyntaxKind.None) - { - return; - } + var lastToken = CSharpStructureHelpers.GetLastInlineMethodBlockToken(anonymousMethod); + if (lastToken.Kind() == SyntaxKind.None) + { + return; + } - var startToken = anonymousMethod.ParameterList != null - ? anonymousMethod.ParameterList.GetLastToken(includeZeroWidth: true) - : anonymousMethod.DelegateKeyword; + var startToken = anonymousMethod.ParameterList != null + ? anonymousMethod.ParameterList.GetLastToken(includeZeroWidth: true) + : anonymousMethod.DelegateKeyword; - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - anonymousMethod, - startToken, - lastToken, - compressEmptyLines: false, - autoCollapse: false, - type: BlockTypes.Expression, - isCollapsible: true)); - } + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + anonymousMethod, + startToken, + lastToken, + compressEmptyLines: false, + autoCollapse: false, + type: BlockTypes.Expression, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/AnonymousObjectCreationExpressionStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/AnonymousObjectCreationExpressionStructureProvider.cs index c59ada33ce058..757221af257a2 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/AnonymousObjectCreationExpressionStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/AnonymousObjectCreationExpressionStructureProvider.cs @@ -10,34 +10,33 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class AnonymousObjectCreationExpressionStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class AnonymousObjectCreationExpressionStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + AnonymousObjectCreationExpressionSyntax node, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - AnonymousObjectCreationExpressionSyntax node, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - // Node is something like: - // - // new - // { - // Field1 = ..., - // Field2 = ..., - // ... - // } - // - // The collapsed textspan should be from the end of new keyword to the end of the whole node - // And the hint span should be the entire node + // Node is something like: + // + // new + // { + // Field1 = ..., + // Field2 = ..., + // ... + // } + // + // The collapsed textspan should be from the end of new keyword to the end of the whole node + // And the hint span should be the entire node - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: TextSpan.FromBounds(node.NewKeyword.Span.End, node.Span.End), - hintSpan: node.Span, - type: BlockTypes.Expression)); - } + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: TextSpan.FromBounds(node.NewKeyword.Span.End, node.Span.End), + hintSpan: node.Span, + type: BlockTypes.Expression)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/ArgumentListStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/ArgumentListStructureProvider.cs new file mode 100644 index 0000000000000..02502254291ca --- /dev/null +++ b/src/Features/CSharp/Portable/Structure/Providers/ArgumentListStructureProvider.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Structure; + +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal sealed class ArgumentListStructureProvider : AbstractSyntaxNodeStructureProvider +{ + protected override void CollectBlockSpans(SyntaxToken previousToken, ArgumentListSyntax node, ref TemporaryArray spans, BlockStructureOptions options, CancellationToken cancellationToken) + { + if (!IsCandidate(node, cancellationToken)) + { + return; + } + + spans.Add(new BlockSpan( + type: BlockTypes.Expression, + isCollapsible: true, + node.Span)); + } + + private static bool IsCandidate(ArgumentListSyntax node, CancellationToken cancellationToken) + { + var openToken = node.OpenParenToken; + var closeToken = node.CloseParenToken; + if (openToken.IsMissing || closeToken.IsMissing) + { + return false; + } + + var text = node.SyntaxTree.GetText(cancellationToken); + var start = text.Lines.GetLinePosition(openToken.SpanStart).Line; + var end = text.Lines.GetLinePosition(closeToken.SpanStart).Line; + return end - start >= 2; + } +} diff --git a/src/Features/CSharp/Portable/Structure/Providers/ArrowExpressionClauseStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/ArrowExpressionClauseStructureProvider.cs index a0a45b8d14eb0..9b71b124c82f8 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/ArrowExpressionClauseStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/ArrowExpressionClauseStructureProvider.cs @@ -11,23 +11,22 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class ArrowExpressionClauseStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class ArrowExpressionClauseStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + ArrowExpressionClauseSyntax node, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - ArrowExpressionClauseSyntax node, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: TextSpan.FromBounds(previousToken.Span.End, node.Parent.Span.End), - hintSpan: node.Parent.Span, - type: BlockTypes.Nonstructural, - autoCollapse: !node.IsParentKind(SyntaxKind.LocalFunctionStatement))); - } + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: TextSpan.FromBounds(previousToken.Span.End, node.Parent.Span.End), + hintSpan: node.Parent.Span, + type: BlockTypes.Nonstructural, + autoCollapse: !node.IsParentKind(SyntaxKind.LocalFunctionStatement))); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/CollectionExpressionStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/CollectionExpressionStructureProvider.cs index 34c438e9456aa..88a807ef8695a 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/CollectionExpressionStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/CollectionExpressionStructureProvider.cs @@ -9,64 +9,63 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class CollectionExpressionStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class CollectionExpressionStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + CollectionExpressionSyntax node, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - CollectionExpressionSyntax node, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + if (node.Parent?.Parent is CollectionExpressionSyntax) { - if (node.Parent?.Parent is CollectionExpressionSyntax) - { - // We have something like: - // - // List> v = - // [ - // ... - // [ - // ... - // ], - // ... - // ]; - // - // In this case, we want to collapse the "[ ... ]," (including the comma). + // We have something like: + // + // List> v = + // [ + // ... + // [ + // ... + // ], + // ... + // ]; + // + // In this case, we want to collapse the "[ ... ]," (including the comma). - var nextToken = node.CloseBracketToken.GetNextToken(); - var end = nextToken.Kind() == SyntaxKind.CommaToken - ? nextToken.Span.End - : node.Span.End; + var nextToken = node.CloseBracketToken.GetNextToken(); + var end = nextToken.Kind() == SyntaxKind.CommaToken + ? nextToken.Span.End + : node.Span.End; - var textSpan = TextSpan.FromBounds(node.SpanStart, end); + var textSpan = TextSpan.FromBounds(node.SpanStart, end); - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: textSpan, - hintSpan: textSpan, - type: BlockTypes.Expression)); - } - else - { - // Parent is something like: - // - // List v = - // [ - // ... - // ]; - // - // The collapsed textspan should be from the = to the ] + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: textSpan, + hintSpan: textSpan, + type: BlockTypes.Expression)); + } + else + { + // Parent is something like: + // + // List v = + // [ + // ... + // ]; + // + // The collapsed textspan should be from the = to the ] - var textSpan = TextSpan.FromBounds(previousToken.Span.End, node.Span.End); + var textSpan = TextSpan.FromBounds(previousToken.Span.End, node.Span.End); - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: textSpan, - hintSpan: textSpan, - type: BlockTypes.Expression)); - } + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: textSpan, + hintSpan: textSpan, + type: BlockTypes.Expression)); } } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/CompilationUnitStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/CompilationUnitStructureProvider.cs index c3a4dcd2aa787..2d9e552b8d744 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/CompilationUnitStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/CompilationUnitStructureProvider.cs @@ -10,40 +10,39 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class CompilationUnitStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class CompilationUnitStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + CompilationUnitSyntax compilationUnit, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - CompilationUnitSyntax compilationUnit, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(compilationUnit, ref spans, options); + CSharpStructureHelpers.CollectCommentBlockSpans(compilationUnit, ref spans, options); - // extern aliases and usings are outlined in a single region - var externsAndUsings = new List(); - externsAndUsings.AddRange(compilationUnit.Externs); - externsAndUsings.AddRange(compilationUnit.Usings); - externsAndUsings.Sort((node1, node2) => node1.SpanStart.CompareTo(node2.SpanStart)); + // extern aliases and usings are outlined in a single region + var externsAndUsings = new List(); + externsAndUsings.AddRange(compilationUnit.Externs); + externsAndUsings.AddRange(compilationUnit.Usings); + externsAndUsings.Sort((node1, node2) => node1.SpanStart.CompareTo(node2.SpanStart)); - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - externsAndUsings, - compressEmptyLines: false, - autoCollapse: true, - type: BlockTypes.Imports, - isCollapsible: true, - isDefaultCollapsed: options.CollapseImportsWhenFirstOpened)); + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + externsAndUsings, + compressEmptyLines: false, + autoCollapse: true, + type: BlockTypes.Imports, + isCollapsible: true, + isDefaultCollapsed: options.CollapseImportsWhenFirstOpened)); - if (compilationUnit.Usings.Count > 0 || - compilationUnit.Externs.Count > 0 || - compilationUnit.Members.Count > 0 || - compilationUnit.AttributeLists.Count > 0) - { - CSharpStructureHelpers.CollectCommentBlockSpans(compilationUnit.EndOfFileToken.LeadingTrivia, ref spans); - } + if (compilationUnit.Usings.Count > 0 || + compilationUnit.Externs.Count > 0 || + compilationUnit.Members.Count > 0 || + compilationUnit.AttributeLists.Count > 0) + { + CSharpStructureHelpers.CollectCommentBlockSpans(compilationUnit.EndOfFileToken.LeadingTrivia, ref spans); } } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/ConstructorDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/ConstructorDeclarationStructureProvider.cs index 51a8e689a7133..fb8a0c224fc40 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/ConstructorDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/ConstructorDeclarationStructureProvider.cs @@ -7,43 +7,42 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class ConstructorDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class ConstructorDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + ConstructorDeclarationSyntax constructorDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - ConstructorDeclarationSyntax constructorDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(constructorDeclaration, ref spans, options); + CSharpStructureHelpers.CollectCommentBlockSpans(constructorDeclaration, ref spans, options); - // fault tolerance - if (constructorDeclaration.Body == null || - constructorDeclaration.Body.OpenBraceToken.IsMissing || - constructorDeclaration.Body.CloseBraceToken.IsMissing) - { - return; - } + // fault tolerance + if (constructorDeclaration.Body == null || + constructorDeclaration.Body.OpenBraceToken.IsMissing || + constructorDeclaration.Body.CloseBraceToken.IsMissing) + { + return; + } - SyntaxNodeOrToken current = constructorDeclaration; - var nextSibling = current.GetNextSibling(); + SyntaxNodeOrToken current = constructorDeclaration; + var nextSibling = current.GetNextSibling(); - // Check IsNode to compress blank lines after this node if it is the last child of the parent. - // - // Whitespace between constructors is collapsed in Metadata as Source. - var compressEmptyLines = options.IsMetadataAsSource - && (!nextSibling.IsNode || nextSibling.IsKind(SyntaxKind.ConstructorDeclaration)); + // Check IsNode to compress blank lines after this node if it is the last child of the parent. + // + // Whitespace between constructors is collapsed in Metadata as Source. + var compressEmptyLines = options.IsMetadataAsSource + && (!nextSibling.IsNode || nextSibling.IsKind(SyntaxKind.ConstructorDeclaration)); - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - constructorDeclaration, - constructorDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), - compressEmptyLines: compressEmptyLines, - autoCollapse: true, - type: BlockTypes.Member, - isCollapsible: true)); - } + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + constructorDeclaration, + constructorDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), + compressEmptyLines: compressEmptyLines, + autoCollapse: true, + type: BlockTypes.Member, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/ConversionOperatorDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/ConversionOperatorDeclarationStructureProvider.cs index 1fca42d7847c6..75e3e39affc47 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/ConversionOperatorDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/ConversionOperatorDeclarationStructureProvider.cs @@ -9,43 +9,42 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class ConversionOperatorDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class ConversionOperatorDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + ConversionOperatorDeclarationSyntax operatorDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - ConversionOperatorDeclarationSyntax operatorDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + CSharpStructureHelpers.CollectCommentBlockSpans(operatorDeclaration, ref spans, options); + + // fault tolerance + if (operatorDeclaration.Body == null || + operatorDeclaration.Body.OpenBraceToken.IsMissing || + operatorDeclaration.Body.CloseBraceToken.IsMissing) { - CSharpStructureHelpers.CollectCommentBlockSpans(operatorDeclaration, ref spans, options); - - // fault tolerance - if (operatorDeclaration.Body == null || - operatorDeclaration.Body.OpenBraceToken.IsMissing || - operatorDeclaration.Body.CloseBraceToken.IsMissing) - { - return; - } - - SyntaxNodeOrToken current = operatorDeclaration; - var nextSibling = current.GetNextSibling(); - - // Check IsNode to compress blank lines after this node if it is the last child of the parent. - // - // Whitespace between conversion operators is collapsed in Metadata as Source. - var compressEmptyLines = options.IsMetadataAsSource - && (!nextSibling.IsNode || nextSibling.IsKind(SyntaxKind.ConversionOperatorDeclaration)); - - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - operatorDeclaration, - operatorDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), - compressEmptyLines: compressEmptyLines, - autoCollapse: true, - type: BlockTypes.Member, - isCollapsible: true)); + return; } + + SyntaxNodeOrToken current = operatorDeclaration; + var nextSibling = current.GetNextSibling(); + + // Check IsNode to compress blank lines after this node if it is the last child of the parent. + // + // Whitespace between conversion operators is collapsed in Metadata as Source. + var compressEmptyLines = options.IsMetadataAsSource + && (!nextSibling.IsNode || nextSibling.IsKind(SyntaxKind.ConversionOperatorDeclaration)); + + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + operatorDeclaration, + operatorDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), + compressEmptyLines: compressEmptyLines, + autoCollapse: true, + type: BlockTypes.Member, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/DelegateDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/DelegateDeclarationStructureProvider.cs index 3f963743b6bcb..bc46e8d5e1cec 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/DelegateDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/DelegateDeclarationStructureProvider.cs @@ -7,18 +7,17 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class DelegateDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class DelegateDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + DelegateDeclarationSyntax delegateDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - DelegateDeclarationSyntax delegateDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(delegateDeclaration, ref spans, options); - } + CSharpStructureHelpers.CollectCommentBlockSpans(delegateDeclaration, ref spans, options); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/DestructorDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/DestructorDeclarationStructureProvider.cs index 7e8269264cdb2..effc47dd6864b 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/DestructorDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/DestructorDeclarationStructureProvider.cs @@ -7,34 +7,33 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class DestructorDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class DestructorDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + DestructorDeclarationSyntax destructorDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - DestructorDeclarationSyntax destructorDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(destructorDeclaration, ref spans, options); - - // fault tolerance - if (destructorDeclaration.Body == null || - destructorDeclaration.Body.OpenBraceToken.IsMissing || - destructorDeclaration.Body.CloseBraceToken.IsMissing) - { - return; - } + CSharpStructureHelpers.CollectCommentBlockSpans(destructorDeclaration, ref spans, options); - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - destructorDeclaration, - destructorDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), - compressEmptyLines: false, - autoCollapse: true, - type: BlockTypes.Member, - isCollapsible: true)); + // fault tolerance + if (destructorDeclaration.Body == null || + destructorDeclaration.Body.OpenBraceToken.IsMissing || + destructorDeclaration.Body.CloseBraceToken.IsMissing) + { + return; } + + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + destructorDeclaration, + destructorDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), + compressEmptyLines: false, + autoCollapse: true, + type: BlockTypes.Member, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/DisabledTextTriviaStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/DisabledTextTriviaStructureProvider.cs index 56e7069006532..82848583e45f4 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/DisabledTextTriviaStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/DisabledTextTriviaStructureProvider.cs @@ -8,128 +8,127 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal sealed class DisabledTextTriviaStructureProvider : AbstractSyntaxTriviaStructureProvider { - internal sealed class DisabledTextTriviaStructureProvider : AbstractSyntaxTriviaStructureProvider + public override void CollectBlockSpans( + SyntaxTrivia trivia, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - public override void CollectBlockSpans( - SyntaxTrivia trivia, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - Contract.ThrowIfNull(trivia.SyntaxTree); - CollectBlockSpans(trivia.SyntaxTree, trivia, ref spans, cancellationToken); - } - - public static void CollectBlockSpans( - SyntaxTree syntaxTree, SyntaxTrivia trivia, - ref TemporaryArray spans, CancellationToken cancellationToken) - { - // We'll always be leading trivia of some token. - var startPos = trivia.FullSpan.Start; - - var parentTriviaList = trivia.Token.LeadingTrivia; - var indexInParent = parentTriviaList.IndexOf(trivia); - - // Note: in some error cases (for example when all future tokens end up being skipped) - // the parser may end up attaching pre-processor directives as trailing trivia to a - // preceding token. - if (indexInParent < 0) - { - parentTriviaList = trivia.Token.TrailingTrivia; - indexInParent = parentTriviaList.IndexOf(trivia); - } + Contract.ThrowIfNull(trivia.SyntaxTree); + CollectBlockSpans(trivia.SyntaxTree, trivia, ref spans, cancellationToken); + } - if (indexInParent <= 0) - { - return; - } + public static void CollectBlockSpans( + SyntaxTree syntaxTree, SyntaxTrivia trivia, + ref TemporaryArray spans, CancellationToken cancellationToken) + { + // We'll always be leading trivia of some token. + var startPos = trivia.FullSpan.Start; - if (!parentTriviaList[indexInParent - 1].IsKind(SyntaxKind.IfDirectiveTrivia) && - !parentTriviaList[indexInParent - 1].IsKind(SyntaxKind.ElifDirectiveTrivia) && - !parentTriviaList[indexInParent - 1].IsKind(SyntaxKind.ElseDirectiveTrivia)) - { - return; - } + var parentTriviaList = trivia.Token.LeadingTrivia; + var indexInParent = parentTriviaList.IndexOf(trivia); - var endTrivia = GetCorrespondingEndTrivia(trivia, parentTriviaList, indexInParent); - var endPos = GetEndPositionExludingLastNewLine(syntaxTree, endTrivia, cancellationToken); + // Note: in some error cases (for example when all future tokens end up being skipped) + // the parser may end up attaching pre-processor directives as trailing trivia to a + // preceding token. + if (indexInParent < 0) + { + parentTriviaList = trivia.Token.TrailingTrivia; + indexInParent = parentTriviaList.IndexOf(trivia); + } - var span = TextSpan.FromBounds(startPos, endPos); - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: span, - type: BlockTypes.PreprocessorRegion, - bannerText: CSharpStructureHelpers.Ellipsis, - autoCollapse: true)); + if (indexInParent <= 0) + { + return; } - private static int GetEndPositionExludingLastNewLine(SyntaxTree syntaxTree, SyntaxTrivia trivia, CancellationToken cancellationToken) + if (!parentTriviaList[indexInParent - 1].IsKind(SyntaxKind.IfDirectiveTrivia) && + !parentTriviaList[indexInParent - 1].IsKind(SyntaxKind.ElifDirectiveTrivia) && + !parentTriviaList[indexInParent - 1].IsKind(SyntaxKind.ElseDirectiveTrivia)) { - var endPos = trivia.FullSpan.End; - var text = syntaxTree.GetText(cancellationToken); - return endPos >= 2 && text[endPos - 1] == '\n' && text[endPos - 2] == '\r' ? endPos - 2 : - endPos >= 1 && SyntaxFacts.IsNewLine(text[endPos - 1]) ? endPos - 1 : endPos; + return; } - private static SyntaxTrivia GetCorrespondingEndTrivia( - SyntaxTrivia trivia, SyntaxTriviaList triviaList, int index) + var endTrivia = GetCorrespondingEndTrivia(trivia, parentTriviaList, indexInParent); + var endPos = GetEndPositionExludingLastNewLine(syntaxTree, endTrivia, cancellationToken); + + var span = TextSpan.FromBounds(startPos, endPos); + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: span, + type: BlockTypes.PreprocessorRegion, + bannerText: CSharpStructureHelpers.Ellipsis, + autoCollapse: true)); + } + + private static int GetEndPositionExludingLastNewLine(SyntaxTree syntaxTree, SyntaxTrivia trivia, CancellationToken cancellationToken) + { + var endPos = trivia.FullSpan.End; + var text = syntaxTree.GetText(cancellationToken); + return endPos >= 2 && text[endPos - 1] == '\n' && text[endPos - 2] == '\r' ? endPos - 2 : + endPos >= 1 && SyntaxFacts.IsNewLine(text[endPos - 1]) ? endPos - 1 : endPos; + } + + private static SyntaxTrivia GetCorrespondingEndTrivia( + SyntaxTrivia trivia, SyntaxTriviaList triviaList, int index) + { + // Look through our parent token's trivia, to extend the span to the end of the last + // disabled trivia. + // + // The issue is that if there are other pre-processor directives (like #regions or + // #lines) mixed in the disabled code, they will be interleaved. Keep walking past + // them to the next thing that will actually end a disabled block. When we encounter + // one, we must also consider which opening block they end. In case of nested pre-processor + // directives, the inner most end block should match the inner most open block and so on. + + var nestedIfDirectiveTrivia = 0; + for (var i = index; i < triviaList.Count; i++) { - // Look through our parent token's trivia, to extend the span to the end of the last - // disabled trivia. - // - // The issue is that if there are other pre-processor directives (like #regions or - // #lines) mixed in the disabled code, they will be interleaved. Keep walking past - // them to the next thing that will actually end a disabled block. When we encounter - // one, we must also consider which opening block they end. In case of nested pre-processor - // directives, the inner most end block should match the inner most open block and so on. - - var nestedIfDirectiveTrivia = 0; - for (var i = index; i < triviaList.Count; i++) + var currentTrivia = triviaList[i]; + switch (currentTrivia.Kind()) { - var currentTrivia = triviaList[i]; - switch (currentTrivia.Kind()) - { - case SyntaxKind.IfDirectiveTrivia: - // Hit a nested #if directive. Keep track of this so we can ensure - // that our actual disabled region reached the right end point. - nestedIfDirectiveTrivia++; + case SyntaxKind.IfDirectiveTrivia: + // Hit a nested #if directive. Keep track of this so we can ensure + // that our actual disabled region reached the right end point. + nestedIfDirectiveTrivia++; + continue; + + case SyntaxKind.EndIfDirectiveTrivia: + if (nestedIfDirectiveTrivia > 0) + { + // This #endif corresponded to a nested #if, pop our stack + // and keep searching. + nestedIfDirectiveTrivia--; continue; + } + + // Found an #endif corresponding to our original #if/#elif/#else region we + // started with. Mark up to the trivia before this as the range to collapse. + return triviaList[i - 1]; + + case SyntaxKind.ElseDirectiveTrivia: + case SyntaxKind.ElifDirectiveTrivia: + if (nestedIfDirectiveTrivia > 0) + { + // This #else/#elif corresponded to a nested #if, ignore as + // they're not relevant to the original construct we started + // on. + continue; + } - case SyntaxKind.EndIfDirectiveTrivia: - if (nestedIfDirectiveTrivia > 0) - { - // This #endif corresponded to a nested #if, pop our stack - // and keep searching. - nestedIfDirectiveTrivia--; - continue; - } - - // Found an #endif corresponding to our original #if/#elif/#else region we - // started with. Mark up to the trivia before this as the range to collapse. - return triviaList[i - 1]; - - case SyntaxKind.ElseDirectiveTrivia: - case SyntaxKind.ElifDirectiveTrivia: - if (nestedIfDirectiveTrivia > 0) - { - // This #else/#elif corresponded to a nested #if, ignore as - // they're not relevant to the original construct we started - // on. - continue; - } - - // We found the next #else/#elif corresponding to our original #if/#elif/#else - // region we started with. Mark up to the trivia before this as the range - // to collapse. - return triviaList[i - 1]; - } + // We found the next #else/#elif corresponding to our original #if/#elif/#else + // region we started with. Mark up to the trivia before this as the range + // to collapse. + return triviaList[i - 1]; } - - // Couldn't find a future trivia to collapse up to. Just collapse the original - // disabled text trivia we started with. - return trivia; } + + // Couldn't find a future trivia to collapse up to. Just collapse the original + // disabled text trivia we started with. + return trivia; } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/DocumentationCommentStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/DocumentationCommentStructureProvider.cs index 03358b2fe44cf..bc8e968162f8f 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/DocumentationCommentStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/DocumentationCommentStructureProvider.cs @@ -9,42 +9,41 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class DocumentationCommentStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class DocumentationCommentStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + DocumentationCommentTriviaSyntax documentationComment, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - DocumentationCommentTriviaSyntax documentationComment, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + // In metadata as source we want to treat documentation comments slightly differently, and collapse them + // to just "..." in front of the decalaration they're attached to. That happens in CSharpStructureHelper.CollectCommentBlockSpans + // so we don't need to do anything here + if (options.IsMetadataAsSource) { - // In metadata as source we want to treat documentation comments slightly differently, and collapse them - // to just "..." in front of the decalaration they're attached to. That happens in CSharpStructureHelper.CollectCommentBlockSpans - // so we don't need to do anything here - if (options.IsMetadataAsSource) - { - return; - } - - var startPos = documentationComment.FullSpan.Start; - - // The trailing newline is included in XmlDocCommentSyntax, so we need to strip it. - var endPos = documentationComment.SpanStart + documentationComment.ToString().TrimEnd().Length; - - var span = TextSpan.FromBounds(startPos, endPos); - - var bannerLength = options.MaximumBannerLength; - var bannerText = CSharpFileBannerFacts.Instance.GetBannerText( - documentationComment, bannerLength, cancellationToken); - - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: span, - type: BlockTypes.Comment, - bannerText: bannerText, - autoCollapse: true)); + return; } + + var startPos = documentationComment.FullSpan.Start; + + // The trailing newline is included in XmlDocCommentSyntax, so we need to strip it. + var endPos = documentationComment.SpanStart + documentationComment.ToString().TrimEnd().Length; + + var span = TextSpan.FromBounds(startPos, endPos); + + var bannerLength = options.MaximumBannerLength; + var bannerText = CSharpFileBannerFacts.Instance.GetBannerText( + documentationComment, bannerLength, cancellationToken); + + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: span, + type: BlockTypes.Comment, + bannerText: bannerText, + autoCollapse: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/EnumDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/EnumDeclarationStructureProvider.cs index 9461b52e7f90d..00a2a12e79c7e 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/EnumDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/EnumDeclarationStructureProvider.cs @@ -7,46 +7,45 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class EnumDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class EnumDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + EnumDeclarationSyntax enumDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - EnumDeclarationSyntax enumDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(enumDeclaration, ref spans, options); + CSharpStructureHelpers.CollectCommentBlockSpans(enumDeclaration, ref spans, options); - if (!enumDeclaration.OpenBraceToken.IsMissing && - !enumDeclaration.CloseBraceToken.IsMissing) - { - SyntaxNodeOrToken current = enumDeclaration; - var nextSibling = current.GetNextSibling(); + if (!enumDeclaration.OpenBraceToken.IsMissing && + !enumDeclaration.CloseBraceToken.IsMissing) + { + SyntaxNodeOrToken current = enumDeclaration; + var nextSibling = current.GetNextSibling(); - // Check IsNode to compress blank lines after this node if it is the last child of the parent. - // - // Whitespace between type declarations is collapsed in Metadata as Source. - var compressEmptyLines = options.IsMetadataAsSource - && (!nextSibling.IsNode || nextSibling.AsNode() is BaseTypeDeclarationSyntax); + // Check IsNode to compress blank lines after this node if it is the last child of the parent. + // + // Whitespace between type declarations is collapsed in Metadata as Source. + var compressEmptyLines = options.IsMetadataAsSource + && (!nextSibling.IsNode || nextSibling.AsNode() is BaseTypeDeclarationSyntax); - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - enumDeclaration, - enumDeclaration.Identifier, - compressEmptyLines: compressEmptyLines, - autoCollapse: false, - type: BlockTypes.Member, - isCollapsible: true)); - } + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + enumDeclaration, + enumDeclaration.Identifier, + compressEmptyLines: compressEmptyLines, + autoCollapse: false, + type: BlockTypes.Member, + isCollapsible: true)); + } - // add any leading comments before the end of the type block - if (!enumDeclaration.CloseBraceToken.IsMissing) - { - var leadingTrivia = enumDeclaration.CloseBraceToken.LeadingTrivia; - CSharpStructureHelpers.CollectCommentBlockSpans(leadingTrivia, ref spans); - } + // add any leading comments before the end of the type block + if (!enumDeclaration.CloseBraceToken.IsMissing) + { + var leadingTrivia = enumDeclaration.CloseBraceToken.LeadingTrivia; + CSharpStructureHelpers.CollectCommentBlockSpans(leadingTrivia, ref spans); } } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/EnumMemberDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/EnumMemberDeclarationStructureProvider.cs index 91ec40a9dfc5a..6dfe41ae72637 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/EnumMemberDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/EnumMemberDeclarationStructureProvider.cs @@ -7,18 +7,17 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class EnumMemberDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class EnumMemberDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + EnumMemberDeclarationSyntax enumMemberDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - EnumMemberDeclarationSyntax enumMemberDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(enumMemberDeclaration, ref spans, options); - } + CSharpStructureHelpers.CollectCommentBlockSpans(enumMemberDeclaration, ref spans, options); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/EventDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/EventDeclarationStructureProvider.cs index 6ae4bcb2197d6..1ad4a2cf5945a 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/EventDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/EventDeclarationStructureProvider.cs @@ -7,44 +7,43 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class EventDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class EventDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + EventDeclarationSyntax eventDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - EventDeclarationSyntax eventDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(eventDeclaration, ref spans, options); + CSharpStructureHelpers.CollectCommentBlockSpans(eventDeclaration, ref spans, options); - // fault tolerance - if (eventDeclaration.AccessorList == null || - eventDeclaration.AccessorList.IsMissing || - eventDeclaration.AccessorList.OpenBraceToken.IsMissing || - eventDeclaration.AccessorList.CloseBraceToken.IsMissing) - { - return; - } + // fault tolerance + if (eventDeclaration.AccessorList == null || + eventDeclaration.AccessorList.IsMissing || + eventDeclaration.AccessorList.OpenBraceToken.IsMissing || + eventDeclaration.AccessorList.CloseBraceToken.IsMissing) + { + return; + } - SyntaxNodeOrToken current = eventDeclaration; - var nextSibling = current.GetNextSibling(); + SyntaxNodeOrToken current = eventDeclaration; + var nextSibling = current.GetNextSibling(); - // Check IsNode to compress blank lines after this node if it is the last child of the parent. - // - // Full events are grouped together with event field definitions in Metadata as Source. - var compressEmptyLines = options.IsMetadataAsSource - && (!nextSibling.IsNode || nextSibling.Kind() is SyntaxKind.EventDeclaration or SyntaxKind.EventFieldDeclaration); + // Check IsNode to compress blank lines after this node if it is the last child of the parent. + // + // Full events are grouped together with event field definitions in Metadata as Source. + var compressEmptyLines = options.IsMetadataAsSource + && (!nextSibling.IsNode || nextSibling.Kind() is SyntaxKind.EventDeclaration or SyntaxKind.EventFieldDeclaration); - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - eventDeclaration, - eventDeclaration.Identifier, - compressEmptyLines: compressEmptyLines, - autoCollapse: true, - type: BlockTypes.Member, - isCollapsible: true)); - } + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + eventDeclaration, + eventDeclaration.Identifier, + compressEmptyLines: compressEmptyLines, + autoCollapse: true, + type: BlockTypes.Member, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/EventFieldDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/EventFieldDeclarationStructureProvider.cs index 59dc197288819..8267a10bfb6cf 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/EventFieldDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/EventFieldDeclarationStructureProvider.cs @@ -7,18 +7,17 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class EventFieldDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class EventFieldDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + EventFieldDeclarationSyntax eventFieldDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - EventFieldDeclarationSyntax eventFieldDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(eventFieldDeclaration, ref spans, options); - } + CSharpStructureHelpers.CollectCommentBlockSpans(eventFieldDeclaration, ref spans, options); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/FieldDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/FieldDeclarationStructureProvider.cs index 9fc8d9f74baff..95c8d81fcc6bd 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/FieldDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/FieldDeclarationStructureProvider.cs @@ -7,18 +7,17 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class FieldDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class FieldDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + FieldDeclarationSyntax fieldDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - FieldDeclarationSyntax fieldDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(fieldDeclaration, ref spans, options); - } + CSharpStructureHelpers.CollectCommentBlockSpans(fieldDeclaration, ref spans, options); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/FileScopedNamespaceDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/FileScopedNamespaceDeclarationStructureProvider.cs index 9d2ea2b3be53c..824b70afdfad5 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/FileScopedNamespaceDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/FileScopedNamespaceDeclarationStructureProvider.cs @@ -9,32 +9,31 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class FileScopedNamespaceDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class FileScopedNamespaceDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + FileScopedNamespaceDeclarationSyntax fileScopedNamespaceDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - FileScopedNamespaceDeclarationSyntax fileScopedNamespaceDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - // add leading comments - CSharpStructureHelpers.CollectCommentBlockSpans(fileScopedNamespaceDeclaration, ref spans, options); - - // extern aliases and usings are outlined in a single region - var externsAndUsings = Enumerable.Union(fileScopedNamespaceDeclaration.Externs, fileScopedNamespaceDeclaration.Usings).ToImmutableArray(); + // add leading comments + CSharpStructureHelpers.CollectCommentBlockSpans(fileScopedNamespaceDeclaration, ref spans, options); - // add any leading comments before the extern aliases and usings - if (externsAndUsings.Any()) - { - CSharpStructureHelpers.CollectCommentBlockSpans(externsAndUsings.First(), ref spans, options); - } + // extern aliases and usings are outlined in a single region + var externsAndUsings = Enumerable.Union(fileScopedNamespaceDeclaration.Externs, fileScopedNamespaceDeclaration.Usings).ToImmutableArray(); - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - externsAndUsings, compressEmptyLines: false, autoCollapse: true, - type: BlockTypes.Imports, isCollapsible: true, isDefaultCollapsed: options.CollapseImportsWhenFirstOpened)); + // add any leading comments before the extern aliases and usings + if (externsAndUsings.Any()) + { + CSharpStructureHelpers.CollectCommentBlockSpans(externsAndUsings.First(), ref spans, options); } + + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + externsAndUsings, compressEmptyLines: false, autoCollapse: true, + type: BlockTypes.Imports, isCollapsible: true, isDefaultCollapsed: options.CollapseImportsWhenFirstOpened)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/IfDirectiveTriviaStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/IfDirectiveTriviaStructureProvider.cs index 52f2224ce6747..d2972162767f6 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/IfDirectiveTriviaStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/IfDirectiveTriviaStructureProvider.cs @@ -8,56 +8,55 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +/// +/// Adds structure guides for the portions of a #if directive that are active. The inactive sections already have +/// structure guides added through . +/// +internal sealed class IfDirectiveTriviaStructureProvider : AbstractSyntaxNodeStructureProvider { - /// - /// Adds structure guides for the portions of a #if directive that are active. The inactive sections already have - /// structure guides added through . - /// - internal sealed class IfDirectiveTriviaStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + IfDirectiveTriviaSyntax node, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - IfDirectiveTriviaSyntax node, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + var allRelatedDirectives = node.GetRelatedDirectives(); + SourceText? text = null; + + for (int i = 0, n = allRelatedDirectives.Count - 1; i < n; i++) { - var allRelatedDirectives = node.GetRelatedDirectives(); - SourceText? text = null; - - for (int i = 0, n = allRelatedDirectives.Count - 1; i < n; i++) - { - var directive = allRelatedDirectives[i]; - if (directive is not BranchingDirectiveTriviaSyntax branchingDirective) - continue; - - // if the branch isn't taken, we'll have disabled text. That's handled by DisabledTextTriviaStructureProvider - if (!branchingDirective.BranchTaken) - continue; - - var nextDirective = allRelatedDirectives[i + 1]; - text ??= node.SyntaxTree.GetText(cancellationToken); - - var startLineNumber = text.Lines.GetLineFromPosition(directive.SpanStart).LineNumber + 1; - var endLineNumber = text.Lines.GetLineFromPosition(nextDirective.SpanStart).LineNumber - 1; - if (startLineNumber >= endLineNumber) - continue; - - if (startLineNumber >= text.Lines.Count || endLineNumber < 0) - continue; - - var startLine = text.Lines[startLineNumber]; - var endLine = text.Lines[endLineNumber]; - - var span = TextSpan.FromBounds(startLine.Start, endLine.End); - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: span, - type: BlockTypes.PreprocessorRegion, - bannerText: CSharpStructureHelpers.Ellipsis, - autoCollapse: false)); - } + var directive = allRelatedDirectives[i]; + if (directive is not BranchingDirectiveTriviaSyntax branchingDirective) + continue; + + // if the branch isn't taken, we'll have disabled text. That's handled by DisabledTextTriviaStructureProvider + if (!branchingDirective.BranchTaken) + continue; + + var nextDirective = allRelatedDirectives[i + 1]; + text ??= node.SyntaxTree.GetText(cancellationToken); + + var startLineNumber = text.Lines.GetLineFromPosition(directive.SpanStart).LineNumber + 1; + var endLineNumber = text.Lines.GetLineFromPosition(nextDirective.SpanStart).LineNumber - 1; + if (startLineNumber >= endLineNumber) + continue; + + if (startLineNumber >= text.Lines.Count || endLineNumber < 0) + continue; + + var startLine = text.Lines[startLineNumber]; + var endLine = text.Lines[endLineNumber]; + + var span = TextSpan.FromBounds(startLine.Start, endLine.End); + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: span, + type: BlockTypes.PreprocessorRegion, + bannerText: CSharpStructureHelpers.Ellipsis, + autoCollapse: false)); } } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/IndexerDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/IndexerDeclarationStructureProvider.cs index c3149445fa95c..7ce664545f141 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/IndexerDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/IndexerDeclarationStructureProvider.cs @@ -7,44 +7,43 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class IndexerDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class IndexerDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + IndexerDeclarationSyntax indexerDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - IndexerDeclarationSyntax indexerDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(indexerDeclaration, ref spans, options); + CSharpStructureHelpers.CollectCommentBlockSpans(indexerDeclaration, ref spans, options); - // fault tolerance - if (indexerDeclaration.AccessorList == null || - indexerDeclaration.AccessorList.IsMissing || - indexerDeclaration.AccessorList.OpenBraceToken.IsMissing || - indexerDeclaration.AccessorList.CloseBraceToken.IsMissing) - { - return; - } + // fault tolerance + if (indexerDeclaration.AccessorList == null || + indexerDeclaration.AccessorList.IsMissing || + indexerDeclaration.AccessorList.OpenBraceToken.IsMissing || + indexerDeclaration.AccessorList.CloseBraceToken.IsMissing) + { + return; + } - SyntaxNodeOrToken current = indexerDeclaration; - var nextSibling = current.GetNextSibling(); + SyntaxNodeOrToken current = indexerDeclaration; + var nextSibling = current.GetNextSibling(); - // Check IsNode to compress blank lines after this node if it is the last child of the parent. - // - // Indexers are grouped together with properties in Metadata as Source. - var compressEmptyLines = options.IsMetadataAsSource - && (!nextSibling.IsNode || nextSibling.Kind() is SyntaxKind.IndexerDeclaration or SyntaxKind.PropertyDeclaration); + // Check IsNode to compress blank lines after this node if it is the last child of the parent. + // + // Indexers are grouped together with properties in Metadata as Source. + var compressEmptyLines = options.IsMetadataAsSource + && (!nextSibling.IsNode || nextSibling.Kind() is SyntaxKind.IndexerDeclaration or SyntaxKind.PropertyDeclaration); - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - indexerDeclaration, - indexerDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), - compressEmptyLines: compressEmptyLines, - autoCollapse: true, - type: BlockTypes.Member, - isCollapsible: true)); - } + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + indexerDeclaration, + indexerDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), + compressEmptyLines: compressEmptyLines, + autoCollapse: true, + type: BlockTypes.Member, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/InitializerExpressionStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/InitializerExpressionStructureProvider.cs index 6c82d8582e48f..4e169180072cb 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/InitializerExpressionStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/InitializerExpressionStructureProvider.cs @@ -10,61 +10,60 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class InitializerExpressionStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class InitializerExpressionStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + InitializerExpressionSyntax node, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - InitializerExpressionSyntax node, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + if (node.Parent is InitializerExpressionSyntax) { - if (node.Parent is InitializerExpressionSyntax) - { - // We have something like: - // - // new Dictionary - // { - // ... - // { - // ... - // }, - // ... - // } - // - // In this case, we want to collapse the "{ ... }," (including the comma). + // We have something like: + // + // new Dictionary + // { + // ... + // { + // ... + // }, + // ... + // } + // + // In this case, we want to collapse the "{ ... }," (including the comma). - var nextToken = node.CloseBraceToken.GetNextToken(); - var end = nextToken.Kind() == SyntaxKind.CommaToken - ? nextToken.Span.End - : node.Span.End; + var nextToken = node.CloseBraceToken.GetNextToken(); + var end = nextToken.Kind() == SyntaxKind.CommaToken + ? nextToken.Span.End + : node.Span.End; - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: TextSpan.FromBounds(node.SpanStart, end), - hintSpan: TextSpan.FromBounds(node.SpanStart, end), - type: BlockTypes.Expression)); - } - else - { - // Parent is something like: - // - // new Dictionary { - // ... - // } - // - // The collapsed textspan should be from the > to the } - // - // However, the hint span should be the entire object creation. + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: TextSpan.FromBounds(node.SpanStart, end), + hintSpan: TextSpan.FromBounds(node.SpanStart, end), + type: BlockTypes.Expression)); + } + else + { + // Parent is something like: + // + // new Dictionary { + // ... + // } + // + // The collapsed textspan should be from the > to the } + // + // However, the hint span should be the entire object creation. - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: TextSpan.FromBounds(previousToken.Span.End, node.Span.End), - hintSpan: node.Parent.Span, - type: BlockTypes.Expression)); - } + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: TextSpan.FromBounds(previousToken.Span.End, node.Span.End), + hintSpan: node.Parent.Span, + type: BlockTypes.Expression)); } } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/InterpolatedStringExpressionStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/InterpolatedStringExpressionStructureProvider.cs index 76d98bf372055..844b8cf20af1d 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/InterpolatedStringExpressionStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/InterpolatedStringExpressionStructureProvider.cs @@ -7,30 +7,29 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal sealed class InterpolatedStringExpressionStructureProvider : AbstractSyntaxNodeStructureProvider { - internal sealed class InterpolatedStringExpressionStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + InterpolatedStringExpressionSyntax node, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - InterpolatedStringExpressionSyntax node, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + if (node.StringStartToken.IsMissing || + node.StringEndToken.IsMissing) { - if (node.StringStartToken.IsMissing || - node.StringEndToken.IsMissing) - { - return; - } - - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: node.Span, - hintSpan: node.Span, - type: BlockTypes.Expression, - autoCollapse: true, - isDefaultCollapsed: false)); + return; } + + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: node.Span, + hintSpan: node.Span, + type: BlockTypes.Expression, + autoCollapse: true, + isDefaultCollapsed: false)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/MethodDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/MethodDeclarationStructureProvider.cs index 8973cde510b84..8416d83a3eb3f 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/MethodDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/MethodDeclarationStructureProvider.cs @@ -7,43 +7,42 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class MethodDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class MethodDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + MethodDeclarationSyntax methodDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - MethodDeclarationSyntax methodDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(methodDeclaration, ref spans, options); + CSharpStructureHelpers.CollectCommentBlockSpans(methodDeclaration, ref spans, options); - // fault tolerance - if (methodDeclaration.Body == null || - methodDeclaration.Body.OpenBraceToken.IsMissing || - methodDeclaration.Body.CloseBraceToken.IsMissing) - { - return; - } + // fault tolerance + if (methodDeclaration.Body == null || + methodDeclaration.Body.OpenBraceToken.IsMissing || + methodDeclaration.Body.CloseBraceToken.IsMissing) + { + return; + } - SyntaxNodeOrToken current = methodDeclaration; - var nextSibling = current.GetNextSibling(); + SyntaxNodeOrToken current = methodDeclaration; + var nextSibling = current.GetNextSibling(); - // Check IsNode to compress blank lines after this node if it is the last child of the parent. - // - // Whitespace between methods is collapsed in Metadata as Source. - var compressEmptyLines = options.IsMetadataAsSource - && (!nextSibling.IsNode || nextSibling.IsKind(SyntaxKind.MethodDeclaration)); + // Check IsNode to compress blank lines after this node if it is the last child of the parent. + // + // Whitespace between methods is collapsed in Metadata as Source. + var compressEmptyLines = options.IsMetadataAsSource + && (!nextSibling.IsNode || nextSibling.IsKind(SyntaxKind.MethodDeclaration)); - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - methodDeclaration, - methodDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), - compressEmptyLines: compressEmptyLines, - autoCollapse: true, - type: BlockTypes.Member, - isCollapsible: true)); - } + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + methodDeclaration, + methodDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), + compressEmptyLines: compressEmptyLines, + autoCollapse: true, + type: BlockTypes.Member, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/MultilineCommentBlockStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/MultilineCommentBlockStructureProvider.cs index 5677ff06e1c79..336fc2d30f5ae 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/MultilineCommentBlockStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/MultilineCommentBlockStructureProvider.cs @@ -7,24 +7,23 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class MultilineCommentBlockStructureProvider : AbstractSyntaxTriviaStructureProvider { - internal class MultilineCommentBlockStructureProvider : AbstractSyntaxTriviaStructureProvider + public override void CollectBlockSpans( + SyntaxTrivia trivia, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - public override void CollectBlockSpans( - SyntaxTrivia trivia, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - var span = new BlockSpan( - isCollapsible: true, - textSpan: trivia.Span, - hintSpan: trivia.Span, - type: BlockTypes.Comment, - bannerText: CSharpStructureHelpers.GetCommentBannerText(trivia), - autoCollapse: true); - spans.Add(span); - } + var span = new BlockSpan( + isCollapsible: true, + textSpan: trivia.Span, + hintSpan: trivia.Span, + type: BlockTypes.Comment, + bannerText: CSharpStructureHelpers.GetCommentBannerText(trivia), + autoCollapse: true); + spans.Add(span); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/NamespaceDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/NamespaceDeclarationStructureProvider.cs index 656716d2f0bb6..fef2227d66a83 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/NamespaceDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/NamespaceDeclarationStructureProvider.cs @@ -8,53 +8,52 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class NamespaceDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class NamespaceDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + NamespaceDeclarationSyntax namespaceDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - NamespaceDeclarationSyntax namespaceDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - // add leading comments - CSharpStructureHelpers.CollectCommentBlockSpans(namespaceDeclaration, ref spans, options); - - if (!namespaceDeclaration.OpenBraceToken.IsMissing && - !namespaceDeclaration.CloseBraceToken.IsMissing) - { - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - namespaceDeclaration, - namespaceDeclaration.Name.GetLastToken(includeZeroWidth: true), - compressEmptyLines: false, - autoCollapse: false, - type: BlockTypes.Namespace, - isCollapsible: true)); - } - - // extern aliases and usings are outlined in a single region - var externsAndUsings = Enumerable.Union(namespaceDeclaration.Externs, namespaceDeclaration.Usings) - .OrderBy(node => node.SpanStart) - .ToList(); - - // add any leading comments before the extern aliases and usings - if (externsAndUsings.Count > 0) - { - CSharpStructureHelpers.CollectCommentBlockSpans(externsAndUsings.First(), ref spans, options); - } + // add leading comments + CSharpStructureHelpers.CollectCommentBlockSpans(namespaceDeclaration, ref spans, options); + if (!namespaceDeclaration.OpenBraceToken.IsMissing && + !namespaceDeclaration.CloseBraceToken.IsMissing) + { spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - externsAndUsings, compressEmptyLines: false, autoCollapse: true, - type: BlockTypes.Imports, isCollapsible: true, isDefaultCollapsed: options.CollapseImportsWhenFirstOpened)); - - // finally, add any leading comments before the end of the namespace block - if (!namespaceDeclaration.CloseBraceToken.IsMissing) - { - CSharpStructureHelpers.CollectCommentBlockSpans( - namespaceDeclaration.CloseBraceToken.LeadingTrivia, ref spans); - } + namespaceDeclaration, + namespaceDeclaration.Name.GetLastToken(includeZeroWidth: true), + compressEmptyLines: false, + autoCollapse: false, + type: BlockTypes.Namespace, + isCollapsible: true)); + } + + // extern aliases and usings are outlined in a single region + var externsAndUsings = Enumerable.Union(namespaceDeclaration.Externs, namespaceDeclaration.Usings) + .OrderBy(node => node.SpanStart) + .ToList(); + + // add any leading comments before the extern aliases and usings + if (externsAndUsings.Count > 0) + { + CSharpStructureHelpers.CollectCommentBlockSpans(externsAndUsings.First(), ref spans, options); + } + + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + externsAndUsings, compressEmptyLines: false, autoCollapse: true, + type: BlockTypes.Imports, isCollapsible: true, isDefaultCollapsed: options.CollapseImportsWhenFirstOpened)); + + // finally, add any leading comments before the end of the namespace block + if (!namespaceDeclaration.CloseBraceToken.IsMissing) + { + CSharpStructureHelpers.CollectCommentBlockSpans( + namespaceDeclaration.CloseBraceToken.LeadingTrivia, ref spans); } } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/OperatorDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/OperatorDeclarationStructureProvider.cs index 28b11f3cc3b67..c1ea008c6cdc9 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/OperatorDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/OperatorDeclarationStructureProvider.cs @@ -7,43 +7,42 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class OperatorDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class OperatorDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + OperatorDeclarationSyntax operatorDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - OperatorDeclarationSyntax operatorDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(operatorDeclaration, ref spans, options); + CSharpStructureHelpers.CollectCommentBlockSpans(operatorDeclaration, ref spans, options); - // fault tolerance - if (operatorDeclaration.Body == null || - operatorDeclaration.Body.OpenBraceToken.IsMissing || - operatorDeclaration.Body.CloseBraceToken.IsMissing) - { - return; - } + // fault tolerance + if (operatorDeclaration.Body == null || + operatorDeclaration.Body.OpenBraceToken.IsMissing || + operatorDeclaration.Body.CloseBraceToken.IsMissing) + { + return; + } - SyntaxNodeOrToken current = operatorDeclaration; - var nextSibling = current.GetNextSibling(); + SyntaxNodeOrToken current = operatorDeclaration; + var nextSibling = current.GetNextSibling(); - // Check IsNode to compress blank lines after this node if it is the last child of the parent. - // - // Whitespace between operators is collapsed in Metadata as Source. - var compressEmptyLines = options.IsMetadataAsSource - && (!nextSibling.IsNode || nextSibling.IsKind(SyntaxKind.OperatorDeclaration)); + // Check IsNode to compress blank lines after this node if it is the last child of the parent. + // + // Whitespace between operators is collapsed in Metadata as Source. + var compressEmptyLines = options.IsMetadataAsSource + && (!nextSibling.IsNode || nextSibling.IsKind(SyntaxKind.OperatorDeclaration)); - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - operatorDeclaration, - operatorDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), - compressEmptyLines: compressEmptyLines, - autoCollapse: true, - type: BlockTypes.Member, - isCollapsible: true)); - } + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + operatorDeclaration, + operatorDeclaration.ParameterList.GetLastToken(includeZeroWidth: true), + compressEmptyLines: compressEmptyLines, + autoCollapse: true, + type: BlockTypes.Member, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/ParenthesizedLambdaExpressionStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/ParenthesizedLambdaExpressionStructureProvider.cs index 1b73b2afea3de..f5713569db889 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/ParenthesizedLambdaExpressionStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/ParenthesizedLambdaExpressionStructureProvider.cs @@ -7,44 +7,43 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class ParenthesizedLambdaExpressionStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class ParenthesizedLambdaExpressionStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + ParenthesizedLambdaExpressionSyntax lambdaExpression, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - ParenthesizedLambdaExpressionSyntax lambdaExpression, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + // fault tolerance + if (lambdaExpression.Body.IsMissing) { - // fault tolerance - if (lambdaExpression.Body.IsMissing) - { - return; - } - - if (lambdaExpression.Body is not BlockSyntax lambdaBlock || - lambdaBlock.OpenBraceToken.IsMissing || - lambdaBlock.CloseBraceToken.IsMissing) - { - return; - } + return; + } - var lastToken = CSharpStructureHelpers.GetLastInlineMethodBlockToken(lambdaExpression); - if (lastToken.Kind() == SyntaxKind.None) - { - return; - } + if (lambdaExpression.Body is not BlockSyntax lambdaBlock || + lambdaBlock.OpenBraceToken.IsMissing || + lambdaBlock.CloseBraceToken.IsMissing) + { + return; + } - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - lambdaExpression, - lambdaExpression.ArrowToken, - lastToken, - compressEmptyLines: false, - autoCollapse: false, - type: BlockTypes.Expression, - isCollapsible: true)); + var lastToken = CSharpStructureHelpers.GetLastInlineMethodBlockToken(lambdaExpression); + if (lastToken.Kind() == SyntaxKind.None) + { + return; } + + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + lambdaExpression, + lambdaExpression.ArrowToken, + lastToken, + compressEmptyLines: false, + autoCollapse: false, + type: BlockTypes.Expression, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/PropertyDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/PropertyDeclarationStructureProvider.cs index f0d2d198433a5..e06ca522d4034 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/PropertyDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/PropertyDeclarationStructureProvider.cs @@ -7,43 +7,42 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class PropertyDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class PropertyDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + PropertyDeclarationSyntax propertyDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - PropertyDeclarationSyntax propertyDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - CSharpStructureHelpers.CollectCommentBlockSpans(propertyDeclaration, ref spans, options); + CSharpStructureHelpers.CollectCommentBlockSpans(propertyDeclaration, ref spans, options); - // fault tolerance - if (propertyDeclaration.AccessorList == null || - propertyDeclaration.AccessorList.OpenBraceToken.IsMissing || - propertyDeclaration.AccessorList.CloseBraceToken.IsMissing) - { - return; - } + // fault tolerance + if (propertyDeclaration.AccessorList == null || + propertyDeclaration.AccessorList.OpenBraceToken.IsMissing || + propertyDeclaration.AccessorList.CloseBraceToken.IsMissing) + { + return; + } - SyntaxNodeOrToken current = propertyDeclaration; - var nextSibling = current.GetNextSibling(); + SyntaxNodeOrToken current = propertyDeclaration; + var nextSibling = current.GetNextSibling(); - // Check IsNode to compress blank lines after this node if it is the last child of the parent. - // - // Properties are grouped together with indexers in Metadata as Source. - var compressEmptyLines = options.IsMetadataAsSource - && (!nextSibling.IsNode || nextSibling.Kind() is SyntaxKind.PropertyDeclaration or SyntaxKind.IndexerDeclaration); + // Check IsNode to compress blank lines after this node if it is the last child of the parent. + // + // Properties are grouped together with indexers in Metadata as Source. + var compressEmptyLines = options.IsMetadataAsSource + && (!nextSibling.IsNode || nextSibling.Kind() is SyntaxKind.PropertyDeclaration or SyntaxKind.IndexerDeclaration); - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - propertyDeclaration, - propertyDeclaration.Identifier, - compressEmptyLines: compressEmptyLines, - autoCollapse: true, - type: BlockTypes.Member, - isCollapsible: true)); - } + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + propertyDeclaration, + propertyDeclaration.Identifier, + compressEmptyLines: compressEmptyLines, + autoCollapse: true, + type: BlockTypes.Member, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/RegionDirectiveStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/RegionDirectiveStructureProvider.cs index 0142942327c75..210b657c1f1b5 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/RegionDirectiveStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/RegionDirectiveStructureProvider.cs @@ -9,54 +9,53 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal sealed class RegionDirectiveStructureProvider : AbstractSyntaxNodeStructureProvider { - internal sealed class RegionDirectiveStructureProvider : AbstractSyntaxNodeStructureProvider + private static string GetBannerText(DirectiveTriviaSyntax simpleDirective) { - private static string GetBannerText(DirectiveTriviaSyntax simpleDirective) - { - var kw = simpleDirective.DirectiveNameToken; - var prefixLength = kw.Span.End - simpleDirective.Span.Start; - var text = simpleDirective.ToString()[prefixLength..].Trim(); + var kw = simpleDirective.DirectiveNameToken; + var prefixLength = kw.Span.End - simpleDirective.Span.Start; + var text = simpleDirective.ToString()[prefixLength..].Trim(); - if (text.Length == 0) - { - return simpleDirective.HashToken.ToString() + kw.ToString(); - } - else - { - return text; - } + if (text.Length == 0) + { + return simpleDirective.HashToken.ToString() + kw.ToString(); } + else + { + return text; + } + } - protected override void CollectBlockSpans( - SyntaxToken previousToken, - RegionDirectiveTriviaSyntax regionDirective, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + protected override void CollectBlockSpans( + SyntaxToken previousToken, + RegionDirectiveTriviaSyntax regionDirective, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) + { + var match = regionDirective.GetMatchingDirective(cancellationToken); + if (match != null) { - var match = regionDirective.GetMatchingDirective(cancellationToken); - if (match != null) - { - // Always auto-collapse regions for Metadata As Source. These generated files only have one region at - // the top of the file, which has content like the following: - // - // #region Assembly System.Runtime, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - // // C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.Runtime.dll - // #endregion - // - // For other files, auto-collapse regions based on the user option. - var autoCollapse = options.IsMetadataAsSource || options.CollapseRegionsWhenCollapsingToDefinitions; + // Always auto-collapse regions for Metadata As Source. These generated files only have one region at + // the top of the file, which has content like the following: + // + // #region Assembly System.Runtime, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + // // C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.Runtime.dll + // #endregion + // + // For other files, auto-collapse regions based on the user option. + var autoCollapse = options.IsMetadataAsSource || options.CollapseRegionsWhenCollapsingToDefinitions; - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: TextSpan.FromBounds(regionDirective.SpanStart, match.Span.End), - type: BlockTypes.PreprocessorRegion, - bannerText: GetBannerText(regionDirective), - autoCollapse: autoCollapse, - isDefaultCollapsed: options.CollapseRegionsWhenFirstOpened)); - } + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: TextSpan.FromBounds(regionDirective.SpanStart, match.Span.End), + type: BlockTypes.PreprocessorRegion, + bannerText: GetBannerText(regionDirective), + autoCollapse: autoCollapse, + isDefaultCollapsed: options.CollapseRegionsWhenFirstOpened)); } } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/SimpleLambdaExpressionStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/SimpleLambdaExpressionStructureProvider.cs index 191184c361181..365b8844df246 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/SimpleLambdaExpressionStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/SimpleLambdaExpressionStructureProvider.cs @@ -7,44 +7,43 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class SimpleLambdaExpressionStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class SimpleLambdaExpressionStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + SimpleLambdaExpressionSyntax lambdaExpression, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - SimpleLambdaExpressionSyntax lambdaExpression, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + // fault tolerance + if (lambdaExpression.Body.IsMissing) { - // fault tolerance - if (lambdaExpression.Body.IsMissing) - { - return; - } - - if (lambdaExpression.Body is not BlockSyntax lambdaBlock || - lambdaBlock.OpenBraceToken.IsMissing || - lambdaBlock.CloseBraceToken.IsMissing) - { - return; - } + return; + } - var lastToken = CSharpStructureHelpers.GetLastInlineMethodBlockToken(lambdaExpression); - if (lastToken.Kind() == SyntaxKind.None) - { - return; - } + if (lambdaExpression.Body is not BlockSyntax lambdaBlock || + lambdaBlock.OpenBraceToken.IsMissing || + lambdaBlock.CloseBraceToken.IsMissing) + { + return; + } - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - lambdaExpression, - lambdaExpression.ArrowToken, - lastToken, - compressEmptyLines: false, - autoCollapse: false, - type: BlockTypes.Expression, - isCollapsible: true)); + var lastToken = CSharpStructureHelpers.GetLastInlineMethodBlockToken(lambdaExpression); + if (lastToken.Kind() == SyntaxKind.None) + { + return; } + + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + lambdaExpression, + lambdaExpression.ArrowToken, + lastToken, + compressEmptyLines: false, + autoCollapse: false, + type: BlockTypes.Expression, + isCollapsible: true)); } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/StringLiteralExpressionStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/StringLiteralExpressionStructureProvider.cs index 78b65b2fda29a..c49df7585db03 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/StringLiteralExpressionStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/StringLiteralExpressionStructureProvider.cs @@ -8,47 +8,46 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal sealed class StringLiteralExpressionStructureProvider : AbstractSyntaxNodeStructureProvider { - internal sealed class StringLiteralExpressionStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + LiteralExpressionSyntax node, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - LiteralExpressionSyntax node, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + if (node.IsKind(SyntaxKind.StringLiteralExpression) && + !node.ContainsDiagnostics && + CouldBeMultiLine()) { - if (node.IsKind(SyntaxKind.StringLiteralExpression) && - !node.ContainsDiagnostics && - CouldBeMultiLine()) - { - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: node.Span, - hintSpan: node.Span, - type: BlockTypes.Expression, - autoCollapse: true, - isDefaultCollapsed: false)); - } + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: node.Span, + hintSpan: node.Span, + type: BlockTypes.Expression, + autoCollapse: true, + isDefaultCollapsed: false)); + } - return; + return; - bool CouldBeMultiLine() - { - if (node.Token.Kind() is SyntaxKind.MultiLineRawStringLiteralToken or SyntaxKind.Utf8MultiLineRawStringLiteralToken) - return true; - - if (node.Token.IsVerbatimStringLiteral()) - { - var span = node.Span; - var sourceText = node.SyntaxTree.GetText(cancellationToken); - return sourceText.Lines.GetLineFromPosition(span.Start).LineNumber != - sourceText.Lines.GetLineFromPosition(span.End).LineNumber; - } + bool CouldBeMultiLine() + { + if (node.Token.Kind() is SyntaxKind.MultiLineRawStringLiteralToken or SyntaxKind.Utf8MultiLineRawStringLiteralToken) + return true; - return false; + if (node.Token.IsVerbatimStringLiteral()) + { + var span = node.Span; + var sourceText = node.SyntaxTree.GetText(cancellationToken); + return sourceText.Lines.GetLineFromPosition(span.Start).LineNumber != + sourceText.Lines.GetLineFromPosition(span.End).LineNumber; } + + return false; } } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/SwitchStatementStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/SwitchStatementStructureProvider.cs index 66f1c01242a7b..49a8fc1f1ad66 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/SwitchStatementStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/SwitchStatementStructureProvider.cs @@ -8,36 +8,35 @@ using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class SwitchStatementStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class SwitchStatementStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + SwitchStatementSyntax node, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - SwitchStatementSyntax node, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) - { - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: TextSpan.FromBounds(node.CloseParenToken != default ? node.CloseParenToken.Span.End : node.Expression.Span.End, node.CloseBraceToken.Span.End), - hintSpan: node.Span, - type: BlockTypes.Conditional)); + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: TextSpan.FromBounds(node.CloseParenToken != default ? node.CloseParenToken.Span.End : node.Expression.Span.End, node.CloseBraceToken.Span.End), + hintSpan: node.Span, + type: BlockTypes.Conditional)); - foreach (var section in node.Sections) + foreach (var section in node.Sections) + { + if (section.Labels.Count > 0 && section.Statements.Count > 0) { - if (section.Labels.Count > 0 && section.Statements.Count > 0) - { - var start = section.Labels.Last().ColonToken.Span.End; - var end = section.Statements.Last().Span.End; + var start = section.Labels.Last().ColonToken.Span.End; + var end = section.Statements.Last().Span.End; - spans.Add(new BlockSpan( - isCollapsible: true, - textSpan: TextSpan.FromBounds(start, end), - hintSpan: section.Span, - type: BlockTypes.Statement)); - } + spans.Add(new BlockSpan( + isCollapsible: true, + textSpan: TextSpan.FromBounds(start, end), + hintSpan: section.Span, + type: BlockTypes.Statement)); } } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/TypeDeclarationStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/TypeDeclarationStructureProvider.cs index b07bdd503d6a7..6c434b518d5ab 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/TypeDeclarationStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/TypeDeclarationStructureProvider.cs @@ -10,52 +10,51 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Structure; -namespace Microsoft.CodeAnalysis.CSharp.Structure +namespace Microsoft.CodeAnalysis.CSharp.Structure; + +internal class TypeDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider { - internal class TypeDeclarationStructureProvider : AbstractSyntaxNodeStructureProvider + protected override void CollectBlockSpans( + SyntaxToken previousToken, + TypeDeclarationSyntax typeDeclaration, + ref TemporaryArray spans, + BlockStructureOptions options, + CancellationToken cancellationToken) { - protected override void CollectBlockSpans( - SyntaxToken previousToken, - TypeDeclarationSyntax typeDeclaration, - ref TemporaryArray spans, - BlockStructureOptions options, - CancellationToken cancellationToken) + CSharpStructureHelpers.CollectCommentBlockSpans(typeDeclaration, ref spans, options); + + if (!typeDeclaration.OpenBraceToken.IsMissing && + !typeDeclaration.CloseBraceToken.IsMissing) + { + var lastToken = typeDeclaration.TypeParameterList == null + ? typeDeclaration.Identifier + : typeDeclaration.TypeParameterList.GetLastToken(includeZeroWidth: true); + + SyntaxNodeOrToken current = typeDeclaration; + var nextSibling = current.GetNextSibling(); + + // Check IsNode to compress blank lines after this node if it is the last child of the parent. + // + // Collapse to Definitions doesn't collapse type nodes, but a Toggle All Outlining would collapse groups + // of types to the compressed form of not showing blank lines. All kinds of types are grouped together + // in Metadata as Source. + var compressEmptyLines = options.IsMetadataAsSource + && (!nextSibling.IsNode || nextSibling.AsNode() is BaseTypeDeclarationSyntax); + + spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( + typeDeclaration, + lastToken, + compressEmptyLines: compressEmptyLines, + autoCollapse: false, + type: BlockTypes.Type, + isCollapsible: true)); + } + + // add any leading comments before the end of the type block + if (!typeDeclaration.CloseBraceToken.IsMissing) { - CSharpStructureHelpers.CollectCommentBlockSpans(typeDeclaration, ref spans, options); - - if (!typeDeclaration.OpenBraceToken.IsMissing && - !typeDeclaration.CloseBraceToken.IsMissing) - { - var lastToken = typeDeclaration.TypeParameterList == null - ? typeDeclaration.Identifier - : typeDeclaration.TypeParameterList.GetLastToken(includeZeroWidth: true); - - SyntaxNodeOrToken current = typeDeclaration; - var nextSibling = current.GetNextSibling(); - - // Check IsNode to compress blank lines after this node if it is the last child of the parent. - // - // Collapse to Definitions doesn't collapse type nodes, but a Toggle All Outlining would collapse groups - // of types to the compressed form of not showing blank lines. All kinds of types are grouped together - // in Metadata as Source. - var compressEmptyLines = options.IsMetadataAsSource - && (!nextSibling.IsNode || nextSibling.AsNode() is BaseTypeDeclarationSyntax); - - spans.AddIfNotNull(CSharpStructureHelpers.CreateBlockSpan( - typeDeclaration, - lastToken, - compressEmptyLines: compressEmptyLines, - autoCollapse: false, - type: BlockTypes.Type, - isCollapsible: true)); - } - - // add any leading comments before the end of the type block - if (!typeDeclaration.CloseBraceToken.IsMissing) - { - var leadingTrivia = typeDeclaration.CloseBraceToken.LeadingTrivia; - CSharpStructureHelpers.CollectCommentBlockSpans(leadingTrivia, ref spans); - } + var leadingTrivia = typeDeclaration.CloseBraceToken.LeadingTrivia; + CSharpStructureHelpers.CollectCommentBlockSpans(leadingTrivia, ref spans); } } } diff --git a/src/Features/CSharp/Portable/SyncNamespaces/CSharpSyncNamespacesService.cs b/src/Features/CSharp/Portable/SyncNamespaces/CSharpSyncNamespacesService.cs index 90be66de2928a..fb4d389191d00 100644 --- a/src/Features/CSharp/Portable/SyncNamespaces/CSharpSyncNamespacesService.cs +++ b/src/Features/CSharp/Portable/SyncNamespaces/CSharpSyncNamespacesService.cs @@ -12,17 +12,16 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.SyncNamespaces; -namespace Microsoft.CodeAnalysis.CSharp.SyncNamespaces +namespace Microsoft.CodeAnalysis.CSharp.SyncNamespaces; + +[ExportLanguageService(typeof(ISyncNamespacesService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpSyncNamespacesService( + CSharpMatchFolderAndNamespaceDiagnosticAnalyzer diagnosticAnalyzer, + CSharpChangeNamespaceToMatchFolderCodeFixProvider codeFixProvider) : AbstractSyncNamespacesService { - [ExportLanguageService(typeof(ISyncNamespacesService), LanguageNames.CSharp), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class CSharpSyncNamespacesService( - CSharpMatchFolderAndNamespaceDiagnosticAnalyzer diagnosticAnalyzer, - CSharpChangeNamespaceToMatchFolderCodeFixProvider codeFixProvider) : AbstractSyncNamespacesService - { - public override AbstractMatchFolderAndNamespaceDiagnosticAnalyzer DiagnosticAnalyzer { get; } = diagnosticAnalyzer; + public override AbstractMatchFolderAndNamespaceDiagnosticAnalyzer DiagnosticAnalyzer { get; } = diagnosticAnalyzer; - public override AbstractChangeNamespaceToMatchFolderCodeFixProvider CodeFixProvider { get; } = codeFixProvider; - } + public override AbstractChangeNamespaceToMatchFolderCodeFixProvider CodeFixProvider { get; } = codeFixProvider; } diff --git a/src/Features/CSharp/Portable/TaskList/CSharpTaskListService.cs b/src/Features/CSharp/Portable/TaskList/CSharpTaskListService.cs index f7069d663357e..a2489176cb857 100644 --- a/src/Features/CSharp/Portable/TaskList/CSharpTaskListService.cs +++ b/src/Features/CSharp/Portable/TaskList/CSharpTaskListService.cs @@ -13,83 +13,82 @@ using Microsoft.CodeAnalysis.TaskList; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.TaskList +namespace Microsoft.CodeAnalysis.CSharp.TaskList; + +[ExportLanguageService(typeof(ITaskListService), LanguageNames.CSharp), Shared] +internal class CSharpTaskListService : AbstractTaskListService { - [ExportLanguageService(typeof(ITaskListService), LanguageNames.CSharp), Shared] - internal class CSharpTaskListService : AbstractTaskListService - { - private static readonly int s_multilineCommentPostfixLength = "*/".Length; - private const string SingleLineCommentPrefix = "//"; + private static readonly int s_multilineCommentPostfixLength = "*/".Length; + private const string SingleLineCommentPrefix = "//"; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpTaskListService() - { - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpTaskListService() + { + } - protected override void AppendTaskListItems( - ImmutableArray commentDescriptors, - SyntacticDocument document, - SyntaxTrivia trivia, - ArrayBuilder items) + protected override void AppendTaskListItems( + ImmutableArray commentDescriptors, + SyntacticDocument document, + SyntaxTrivia trivia, + ArrayBuilder items) + { + if (PreprocessorHasComment(trivia)) { - if (PreprocessorHasComment(trivia)) - { - var message = trivia.ToFullString(); + var message = trivia.ToFullString(); - var index = message.IndexOf(SingleLineCommentPrefix, StringComparison.Ordinal); - var start = trivia.FullSpan.Start + index; + var index = message.IndexOf(SingleLineCommentPrefix, StringComparison.Ordinal); + var start = trivia.FullSpan.Start + index; - AppendTaskListItemsOnSingleLine(commentDescriptors, document, message[index..], start, items); - return; - } - - if (IsSingleLineComment(trivia)) - { - ProcessMultilineComment(commentDescriptors, document, trivia, postfixLength: 0, items); - return; - } + AppendTaskListItemsOnSingleLine(commentDescriptors, document, message[index..], start, items); + return; + } - if (IsMultilineComment(trivia)) - { - ProcessMultilineComment(commentDescriptors, document, trivia, s_multilineCommentPostfixLength, items); - return; - } + if (IsSingleLineComment(trivia)) + { + ProcessMultilineComment(commentDescriptors, document, trivia, postfixLength: 0, items); + return; + } - throw ExceptionUtilities.Unreachable(); + if (IsMultilineComment(trivia)) + { + ProcessMultilineComment(commentDescriptors, document, trivia, s_multilineCommentPostfixLength, items); + return; } - protected override string GetNormalizedText(string message) - => message; + throw ExceptionUtilities.Unreachable(); + } - protected override bool IsIdentifierCharacter(char ch) - => SyntaxFacts.IsIdentifierPartCharacter(ch); + protected override string GetNormalizedText(string message) + => message; - protected override int GetCommentStartingIndex(string message) + protected override bool IsIdentifierCharacter(char ch) + => SyntaxFacts.IsIdentifierPartCharacter(ch); + + protected override int GetCommentStartingIndex(string message) + { + for (var i = 0; i < message.Length; i++) { - for (var i = 0; i < message.Length; i++) + var ch = message[i]; + if (!SyntaxFacts.IsWhitespace(ch) && + ch != '*' && ch != '/') { - var ch = message[i]; - if (!SyntaxFacts.IsWhitespace(ch) && - ch != '*' && ch != '/') - { - return i; - } + return i; } - - return message.Length; } - protected override bool PreprocessorHasComment(SyntaxTrivia trivia) - { - return trivia.Kind() != SyntaxKind.RegionDirectiveTrivia && - SyntaxFacts.IsPreprocessorDirective(trivia.Kind()) && trivia.ToString().IndexOf(SingleLineCommentPrefix, StringComparison.Ordinal) > 0; - } - - protected override bool IsSingleLineComment(SyntaxTrivia trivia) - => trivia.IsSingleLineComment() || trivia.IsSingleLineDocComment(); + return message.Length; + } - protected override bool IsMultilineComment(SyntaxTrivia trivia) - => trivia.IsMultiLineComment() || trivia.IsMultiLineDocComment(); + protected override bool PreprocessorHasComment(SyntaxTrivia trivia) + { + return trivia.Kind() != SyntaxKind.RegionDirectiveTrivia && + SyntaxFacts.IsPreprocessorDirective(trivia.Kind()) && trivia.ToString().IndexOf(SingleLineCommentPrefix, StringComparison.Ordinal) > 0; } + + protected override bool IsSingleLineComment(SyntaxTrivia trivia) + => trivia.IsSingleLineComment() || trivia.IsSingleLineDocComment(); + + protected override bool IsMultilineComment(SyntaxTrivia trivia) + => trivia.IsMultiLineComment() || trivia.IsMultiLineDocComment(); } diff --git a/src/Features/CSharp/Portable/UseAutoProperty/CSharpUseAutoPropertyCodeFixProvider.cs b/src/Features/CSharp/Portable/UseAutoProperty/CSharpUseAutoPropertyCodeFixProvider.cs index c2e7634fa531a..6a12f660c1851 100644 --- a/src/Features/CSharp/Portable/UseAutoProperty/CSharpUseAutoPropertyCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/UseAutoProperty/CSharpUseAutoPropertyCodeFixProvider.cs @@ -18,168 +18,167 @@ using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.UseAutoProperty; -namespace Microsoft.CodeAnalysis.CSharp.UseAutoProperty +namespace Microsoft.CodeAnalysis.CSharp.UseAutoProperty; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseAutoProperty), Shared] +internal class CSharpUseAutoPropertyCodeFixProvider + : AbstractUseAutoPropertyCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseAutoProperty), Shared] - internal class CSharpUseAutoPropertyCodeFixProvider - : AbstractUseAutoPropertyCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseAutoPropertyCodeFixProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseAutoPropertyCodeFixProvider() - { - } - - protected override PropertyDeclarationSyntax GetPropertyDeclaration(SyntaxNode node) - => (PropertyDeclarationSyntax)node; - - protected override SyntaxNode GetNodeToRemove(VariableDeclaratorSyntax declarator) - { - var fieldDeclaration = (FieldDeclarationSyntax)declarator.Parent.Parent; - var nodeToRemove = fieldDeclaration.Declaration.Variables.Count > 1 ? declarator : (SyntaxNode)fieldDeclaration; - return nodeToRemove; - } - - protected override async Task UpdatePropertyAsync( - Document propertyDocument, Compilation compilation, IFieldSymbol fieldSymbol, IPropertySymbol propertySymbol, - PropertyDeclarationSyntax propertyDeclaration, bool isWrittenOutsideOfConstructor, CancellationToken cancellationToken) - { - var project = propertyDocument.Project; - var trailingTrivia = propertyDeclaration.GetTrailingTrivia(); + } - var updatedProperty = propertyDeclaration.WithAccessorList(UpdateAccessorList(propertyDeclaration.AccessorList)) - .WithExpressionBody(null) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)); + protected override PropertyDeclarationSyntax GetPropertyDeclaration(SyntaxNode node) + => (PropertyDeclarationSyntax)node; - // We may need to add a setter if the field is written to outside of the constructor - // of it's class. - if (NeedsSetter(compilation, propertyDeclaration, isWrittenOutsideOfConstructor)) - { - var accessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); - var generator = SyntaxGenerator.GetGenerator(project); + protected override SyntaxNode GetNodeToRemove(VariableDeclaratorSyntax declarator) + { + var fieldDeclaration = (FieldDeclarationSyntax)declarator.Parent.Parent; + var nodeToRemove = fieldDeclaration.Declaration.Variables.Count > 1 ? declarator : (SyntaxNode)fieldDeclaration; + return nodeToRemove; + } - if (fieldSymbol.DeclaredAccessibility != propertySymbol.DeclaredAccessibility) - { - accessor = (AccessorDeclarationSyntax)generator.WithAccessibility(accessor, fieldSymbol.DeclaredAccessibility); - } + protected override async Task UpdatePropertyAsync( + Document propertyDocument, Compilation compilation, IFieldSymbol fieldSymbol, IPropertySymbol propertySymbol, + PropertyDeclarationSyntax propertyDeclaration, bool isWrittenOutsideOfConstructor, CancellationToken cancellationToken) + { + var project = propertyDocument.Project; + var trailingTrivia = propertyDeclaration.GetTrailingTrivia(); - var modifiers = SyntaxFactory.TokenList( - updatedProperty.Modifiers.Where(token => !token.IsKind(SyntaxKind.ReadOnlyKeyword))); + var updatedProperty = propertyDeclaration.WithAccessorList(UpdateAccessorList(propertyDeclaration.AccessorList)) + .WithExpressionBody(null) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)); - updatedProperty = updatedProperty.WithModifiers(modifiers) - .AddAccessorListAccessors(accessor); - } + // We may need to add a setter if the field is written to outside of the constructor + // of it's class. + if (NeedsSetter(compilation, propertyDeclaration, isWrittenOutsideOfConstructor)) + { + var accessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + var generator = SyntaxGenerator.GetGenerator(project); - var fieldInitializer = await GetFieldInitializerAsync(fieldSymbol, cancellationToken).ConfigureAwait(false); - if (fieldInitializer != null) + if (fieldSymbol.DeclaredAccessibility != propertySymbol.DeclaredAccessibility) { - updatedProperty = updatedProperty.WithInitializer(SyntaxFactory.EqualsValueClause(fieldInitializer)) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + accessor = (AccessorDeclarationSyntax)generator.WithAccessibility(accessor, fieldSymbol.DeclaredAccessibility); } - return updatedProperty.WithTrailingTrivia(trailingTrivia).WithAdditionalAnnotations(SpecializedFormattingAnnotation); + var modifiers = SyntaxFactory.TokenList( + updatedProperty.Modifiers.Where(token => !token.IsKind(SyntaxKind.ReadOnlyKeyword))); + + updatedProperty = updatedProperty.WithModifiers(modifiers) + .AddAccessorListAccessors(accessor); } - protected override IEnumerable GetFormattingRules(Document document) + var fieldInitializer = await GetFieldInitializerAsync(fieldSymbol, cancellationToken).ConfigureAwait(false); + if (fieldInitializer != null) { - var rules = new List { new SingleLinePropertyFormattingRule() }; - rules.AddRange(Formatter.GetDefaultFormattingRules(document)); - - return rules; + updatedProperty = updatedProperty.WithInitializer(SyntaxFactory.EqualsValueClause(fieldInitializer)) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); } - private class SingleLinePropertyFormattingRule : AbstractFormattingRule + return updatedProperty.WithTrailingTrivia(trailingTrivia).WithAdditionalAnnotations(SpecializedFormattingAnnotation); + } + + protected override IEnumerable GetFormattingRules(Document document) + { + var rules = new List { new SingleLinePropertyFormattingRule() }; + rules.AddRange(Formatter.GetDefaultFormattingRules(document)); + + return rules; + } + + private class SingleLinePropertyFormattingRule : AbstractFormattingRule + { + private static bool ForceSingleSpace(SyntaxToken previousToken, SyntaxToken currentToken) { - private static bool ForceSingleSpace(SyntaxToken previousToken, SyntaxToken currentToken) + if (currentToken.IsKind(SyntaxKind.OpenBraceToken) && currentToken.Parent.IsKind(SyntaxKind.AccessorList)) { - if (currentToken.IsKind(SyntaxKind.OpenBraceToken) && currentToken.Parent.IsKind(SyntaxKind.AccessorList)) - { - return true; - } - - if (previousToken.IsKind(SyntaxKind.OpenBraceToken) && previousToken.Parent.IsKind(SyntaxKind.AccessorList)) - { - return true; - } - - if (currentToken.IsKind(SyntaxKind.CloseBraceToken) && currentToken.Parent.IsKind(SyntaxKind.AccessorList)) - { - return true; - } - - return false; + return true; } - public override AdjustNewLinesOperation GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + if (previousToken.IsKind(SyntaxKind.OpenBraceToken) && previousToken.Parent.IsKind(SyntaxKind.AccessorList)) { - if (ForceSingleSpace(previousToken, currentToken)) - { - return null; - } - - return base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation); + return true; } - public override AdjustSpacesOperation GetAdjustSpacesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustSpacesOperation nextOperation) + if (currentToken.IsKind(SyntaxKind.CloseBraceToken) && currentToken.Parent.IsKind(SyntaxKind.AccessorList)) { - if (ForceSingleSpace(previousToken, currentToken)) - { - return new AdjustSpacesOperation(1, AdjustSpacesOption.ForceSpaces); - } - - return base.GetAdjustSpacesOperation(in previousToken, in currentToken, in nextOperation); + return true; } - } - private static async Task GetFieldInitializerAsync(IFieldSymbol fieldSymbol, CancellationToken cancellationToken) - { - var variableDeclarator = (VariableDeclaratorSyntax)await fieldSymbol.DeclaringSyntaxReferences[0].GetSyntaxAsync(cancellationToken).ConfigureAwait(false); - return variableDeclarator.Initializer?.Value; + return false; } - private static bool NeedsSetter(Compilation compilation, PropertyDeclarationSyntax propertyDeclaration, bool isWrittenOutsideOfConstructor) + public override AdjustNewLinesOperation GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) { - if (propertyDeclaration.AccessorList?.Accessors.Any(SyntaxKind.SetAccessorDeclaration) == true) + if (ForceSingleSpace(previousToken, currentToken)) { - // Already has a setter. - return false; + return null; } - if (!SupportsReadOnlyProperties(compilation)) + return base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation); + } + + public override AdjustSpacesOperation GetAdjustSpacesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustSpacesOperation nextOperation) + { + if (ForceSingleSpace(previousToken, currentToken)) { - // If the language doesn't have readonly properties, then we'll need a - // setter here. - return true; + return new AdjustSpacesOperation(1, AdjustSpacesOption.ForceSpaces); } - // If we're written outside a constructor we need a setter. - return isWrittenOutsideOfConstructor; + return base.GetAdjustSpacesOperation(in previousToken, in currentToken, in nextOperation); } + } - private static bool SupportsReadOnlyProperties(Compilation compilation) - => compilation.LanguageVersion() >= LanguageVersion.CSharp6; + private static async Task GetFieldInitializerAsync(IFieldSymbol fieldSymbol, CancellationToken cancellationToken) + { + var variableDeclarator = (VariableDeclaratorSyntax)await fieldSymbol.DeclaringSyntaxReferences[0].GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + return variableDeclarator.Initializer?.Value; + } - private static AccessorListSyntax UpdateAccessorList(AccessorListSyntax accessorList) + private static bool NeedsSetter(Compilation compilation, PropertyDeclarationSyntax propertyDeclaration, bool isWrittenOutsideOfConstructor) + { + if (propertyDeclaration.AccessorList?.Accessors.Any(SyntaxKind.SetAccessorDeclaration) == true) { - if (accessorList == null) - { - var getter = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); - return SyntaxFactory.AccessorList([getter]); - } + // Already has a setter. + return false; + } - return accessorList.WithAccessors([.. GetAccessors(accessorList.Accessors)]); + if (!SupportsReadOnlyProperties(compilation)) + { + // If the language doesn't have readonly properties, then we'll need a + // setter here. + return true; } - private static IEnumerable GetAccessors(SyntaxList accessors) + // If we're written outside a constructor we need a setter. + return isWrittenOutsideOfConstructor; + } + + private static bool SupportsReadOnlyProperties(Compilation compilation) + => compilation.LanguageVersion() >= LanguageVersion.CSharp6; + + private static AccessorListSyntax UpdateAccessorList(AccessorListSyntax accessorList) + { + if (accessorList == null) { - foreach (var accessor in accessors) - { - yield return accessor.WithBody(null) - .WithExpressionBody(null) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); - } + var getter = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + return SyntaxFactory.AccessorList([getter]); + } + + return accessorList.WithAccessors([.. GetAccessors(accessorList.Accessors)]); + } + + private static IEnumerable GetAccessors(SyntaxList accessors) + { + foreach (var accessor in accessors) + { + yield return accessor.WithBody(null) + .WithExpressionBody(null) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); } } } diff --git a/src/Features/CSharp/Portable/UseExpressionBody/UseExpressionBodyCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/UseExpressionBody/UseExpressionBodyCodeRefactoringProvider.cs index 90245c79561d2..6c9bb8f0e59e6 100644 --- a/src/Features/CSharp/Portable/UseExpressionBody/UseExpressionBodyCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/UseExpressionBody/UseExpressionBodyCodeRefactoringProvider.cs @@ -24,246 +24,245 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseExpressionBody), Shared] +[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.ExtractClass)] +internal class UseExpressionBodyCodeRefactoringProvider : SyntaxEditorBasedCodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseExpressionBody), Shared] - [ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.ExtractClass)] - internal class UseExpressionBodyCodeRefactoringProvider : SyntaxEditorBasedCodeRefactoringProvider - { - private static readonly ImmutableArray _helpers = UseExpressionBodyHelper.Helpers; + private static readonly ImmutableArray _helpers = UseExpressionBodyHelper.Helpers; - private static readonly BidirectionalMap<(UseExpressionBodyHelper helper, bool useExpressionBody), string> s_equivalenceKeyMap - = CreateEquivalanceKeyMap(UseExpressionBodyHelper.Helpers); + private static readonly BidirectionalMap<(UseExpressionBodyHelper helper, bool useExpressionBody), string> s_equivalenceKeyMap + = CreateEquivalanceKeyMap(UseExpressionBodyHelper.Helpers); - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public UseExpressionBodyCodeRefactoringProvider() - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public UseExpressionBodyCodeRefactoringProvider() + { + } - private static BidirectionalMap<(UseExpressionBodyHelper helper, bool useExpressionBody), string> CreateEquivalanceKeyMap( + private static BidirectionalMap<(UseExpressionBodyHelper helper, bool useExpressionBody), string> CreateEquivalanceKeyMap( + ImmutableArray helpers) + { + return new BidirectionalMap<(UseExpressionBodyHelper helper, bool useExpressionBody), string>(GetKeyValuePairs(helpers)); + + static IEnumerable> GetKeyValuePairs( ImmutableArray helpers) { - return new BidirectionalMap<(UseExpressionBodyHelper helper, bool useExpressionBody), string>(GetKeyValuePairs(helpers)); - - static IEnumerable> GetKeyValuePairs( - ImmutableArray helpers) + foreach (var helper in helpers) { - foreach (var helper in helpers) - { - yield return KeyValuePairUtil.Create((helper, useExpressionBody: true), helper.GetType().Name + "_UseExpressionBody"); - yield return KeyValuePairUtil.Create((helper, useExpressionBody: false), helper.GetType().Name + "_UseBlockBody"); - } + yield return KeyValuePairUtil.Create((helper, useExpressionBody: true), helper.GetType().Name + "_UseExpressionBody"); + yield return KeyValuePairUtil.Create((helper, useExpressionBody: false), helper.GetType().Name + "_UseBlockBody"); } } + } - protected override ImmutableArray SupportedFixAllScopes => AllFixAllScopes; - - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, textSpan, cancellationToken) = context; - if (textSpan.Length > 0) - return; - - var position = textSpan.Start; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var node = root.FindToken(position).Parent!; + protected override ImmutableArray SupportedFixAllScopes => AllFixAllScopes; - var containingLambda = node.FirstAncestorOrSelf(); - if (containingLambda != null && - node.AncestorsAndSelf().Contains(containingLambda.Body)) - { - // don't offer inside a lambda. Lambdas can be quite large, and it will be very noisy - // inside the body of one to be offering to use a block/expression body for the containing - // class member. - return; - } + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, textSpan, cancellationToken) = context; + if (textSpan.Length > 0) + return; - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var options = (CSharpCodeGenerationOptions)await document.GetCodeGenerationOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); + var position = textSpan.Start; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var node = root.FindToken(position).Parent!; - foreach (var helper in _helpers) - { - var declaration = TryGetDeclaration(helper, text, node, position); - if (declaration == null) - continue; - - var succeeded = TryComputeRefactoring(context, root, declaration, options, helper, cancellationToken); - if (succeeded) - return; - } + var containingLambda = node.FirstAncestorOrSelf(); + if (containingLambda != null && + node.AncestorsAndSelf().Contains(containingLambda.Body)) + { + // don't offer inside a lambda. Lambdas can be quite large, and it will be very noisy + // inside the body of one to be offering to use a block/expression body for the containing + // class member. + return; } - private static SyntaxNode? TryGetDeclaration( - UseExpressionBodyHelper helper, SourceText text, SyntaxNode node, int position) + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var options = (CSharpCodeGenerationOptions)await document.GetCodeGenerationOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); + + foreach (var helper in _helpers) { - var declaration = GetDeclaration(node, helper); + var declaration = TryGetDeclaration(helper, text, node, position); if (declaration == null) - return null; + continue; - if (position < declaration.SpanStart) - { - // The user is allowed to be before the starting point of this node, as long as - // they're only between the start of the node and the start of the same line the - // node starts on. This prevents unnecessarily showing this feature in areas like - // the comment of a method. - if (!text.AreOnSameLine(position, declaration.SpanStart)) - return null; - } - - return declaration; + var succeeded = TryComputeRefactoring(context, root, declaration, options, helper, cancellationToken); + if (succeeded) + return; } + } - private static bool TryComputeRefactoring( - CodeRefactoringContext context, SyntaxNode root, SyntaxNode declaration, - CSharpCodeGenerationOptions options, UseExpressionBodyHelper helper, CancellationToken cancellationToken) + private static SyntaxNode? TryGetDeclaration( + UseExpressionBodyHelper helper, SourceText text, SyntaxNode node, int position) + { + var declaration = GetDeclaration(node, helper); + if (declaration == null) + return null; + + if (position < declaration.SpanStart) { - var document = context.Document; - var preference = helper.GetExpressionBodyPreference(options); + // The user is allowed to be before the starting point of this node, as long as + // they're only between the start of the node and the start of the same line the + // node starts on. This prevents unnecessarily showing this feature in areas like + // the comment of a method. + if (!text.AreOnSameLine(position, declaration.SpanStart)) + return null; + } - var succeeded = false; - if (helper.CanOfferUseExpressionBody(preference, declaration, forAnalyzer: false, cancellationToken)) - { - context.RegisterRefactoring( - CodeAction.Create( - helper.UseExpressionBodyTitle.ToString(), - c => UpdateDocumentAsync( - document, root, declaration, helper, - useExpressionBody: true, cancellationToken: c), - s_equivalenceKeyMap[(helper, useExpressionBody: true)]), - declaration.Span); - succeeded = true; - } + return declaration; + } - if (helper.CanOfferUseBlockBody(preference, declaration, forAnalyzer: false, out _, out _)) - { - context.RegisterRefactoring( - CodeAction.Create( - helper.UseBlockBodyTitle.ToString(), - c => UpdateDocumentAsync( - document, root, declaration, helper, - useExpressionBody: false, cancellationToken: c), - s_equivalenceKeyMap[(helper, useExpressionBody: false)]), - declaration.Span); - succeeded = true; - } + private static bool TryComputeRefactoring( + CodeRefactoringContext context, SyntaxNode root, SyntaxNode declaration, + CSharpCodeGenerationOptions options, UseExpressionBodyHelper helper, CancellationToken cancellationToken) + { + var document = context.Document; + var preference = helper.GetExpressionBodyPreference(options); - return succeeded; + var succeeded = false; + if (helper.CanOfferUseExpressionBody(preference, declaration, forAnalyzer: false, cancellationToken)) + { + context.RegisterRefactoring( + CodeAction.Create( + helper.UseExpressionBodyTitle.ToString(), + c => UpdateDocumentAsync( + document, root, declaration, helper, + useExpressionBody: true, cancellationToken: c), + s_equivalenceKeyMap[(helper, useExpressionBody: true)]), + declaration.Span); + succeeded = true; } - private static SyntaxNode? GetDeclaration(SyntaxNode node, UseExpressionBodyHelper helper) + if (helper.CanOfferUseBlockBody(preference, declaration, forAnalyzer: false, out _, out _)) { - for (var current = node; current != null; current = current.Parent) - { - if (helper.SyntaxKinds.Contains(current.Kind())) - return current; - } - - return null; + context.RegisterRefactoring( + CodeAction.Create( + helper.UseBlockBodyTitle.ToString(), + c => UpdateDocumentAsync( + document, root, declaration, helper, + useExpressionBody: false, cancellationToken: c), + s_equivalenceKeyMap[(helper, useExpressionBody: false)]), + declaration.Span); + succeeded = true; } - private static async Task UpdateDocumentAsync( - Document document, SyntaxNode root, SyntaxNode declaration, - UseExpressionBodyHelper helper, bool useExpressionBody, - CancellationToken cancellationToken) + return succeeded; + } + + private static SyntaxNode? GetDeclaration(SyntaxNode node, UseExpressionBodyHelper helper) + { + for (var current = node; current != null; current = current.Parent) { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var newRoot = GetUpdatedRoot(semanticModel, root, declaration, helper, useExpressionBody, cancellationToken); - return document.WithSyntaxRoot(newRoot); + if (helper.SyntaxKinds.Contains(current.Kind())) + return current; } - private static SyntaxNode GetUpdatedRoot( - SemanticModel semanticModel, SyntaxNode root, SyntaxNode declaration, - UseExpressionBodyHelper helper, bool useExpressionBody, CancellationToken cancellationToken) - { - var updatedDeclaration = helper.Update(semanticModel, declaration, useExpressionBody, cancellationToken); + return null; + } - var parent = declaration is AccessorDeclarationSyntax - ? declaration.Parent - : declaration; - RoslynDebug.Assert(parent is object); - var updatedParent = parent.ReplaceNode(declaration, updatedDeclaration) - .WithAdditionalAnnotations(Formatter.Annotation); + private static async Task UpdateDocumentAsync( + Document document, SyntaxNode root, SyntaxNode declaration, + UseExpressionBodyHelper helper, bool useExpressionBody, + CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var newRoot = GetUpdatedRoot(semanticModel, root, declaration, helper, useExpressionBody, cancellationToken); + return document.WithSyntaxRoot(newRoot); + } - return root.ReplaceNode(parent, updatedParent); - } + private static SyntaxNode GetUpdatedRoot( + SemanticModel semanticModel, SyntaxNode root, SyntaxNode declaration, + UseExpressionBodyHelper helper, bool useExpressionBody, CancellationToken cancellationToken) + { + var updatedDeclaration = helper.Update(semanticModel, declaration, useExpressionBody, cancellationToken); - protected override async Task FixAllAsync( - Document document, + var parent = declaration is AccessorDeclarationSyntax + ? declaration.Parent + : declaration; + RoslynDebug.Assert(parent is object); + var updatedParent = parent.ReplaceNode(declaration, updatedDeclaration) + .WithAdditionalAnnotations(Formatter.Annotation); + + return root.ReplaceNode(parent, updatedParent); + } + + protected override async Task FixAllAsync( + Document document, + ImmutableArray fixAllSpans, + SyntaxEditor editor, + CodeActionOptionsProvider optionsProvider, + string? equivalenceKey, + CancellationToken cancellationToken) + { + Debug.Assert(equivalenceKey != null); + var (helper, useExpressionBody) = s_equivalenceKeyMap[equivalenceKey]; + + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var options = (CSharpCodeGenerationOptions)await document.GetCodeGenerationOptionsAsync(optionsProvider, cancellationToken).ConfigureAwait(false); + var declarationsToFix = GetDeclarationsToFix(fixAllSpans, root, helper, useExpressionBody, options, cancellationToken); + await FixDeclarationsAsync(document, editor, root, declarationsToFix.ToImmutableArray(), helper, useExpressionBody, cancellationToken).ConfigureAwait(false); + return; + + // Local functions. + static IEnumerable GetDeclarationsToFix( ImmutableArray fixAllSpans, - SyntaxEditor editor, - CodeActionOptionsProvider optionsProvider, - string? equivalenceKey, + SyntaxNode root, + UseExpressionBodyHelper helper, + bool useExpressionBody, + CSharpCodeGenerationOptions options, CancellationToken cancellationToken) { - Debug.Assert(equivalenceKey != null); - var (helper, useExpressionBody) = s_equivalenceKeyMap[equivalenceKey]; - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var options = (CSharpCodeGenerationOptions)await document.GetCodeGenerationOptionsAsync(optionsProvider, cancellationToken).ConfigureAwait(false); - var declarationsToFix = GetDeclarationsToFix(fixAllSpans, root, helper, useExpressionBody, options, cancellationToken); - await FixDeclarationsAsync(document, editor, root, declarationsToFix.ToImmutableArray(), helper, useExpressionBody, cancellationToken).ConfigureAwait(false); - return; - - // Local functions. - static IEnumerable GetDeclarationsToFix( - ImmutableArray fixAllSpans, - SyntaxNode root, - UseExpressionBodyHelper helper, - bool useExpressionBody, - CSharpCodeGenerationOptions options, - CancellationToken cancellationToken) + var preference = helper.GetExpressionBodyPreference(options); + foreach (var span in fixAllSpans) { - var preference = helper.GetExpressionBodyPreference(options); - foreach (var span in fixAllSpans) + var spanNode = root.FindNode(span); + + foreach (var node in spanNode.DescendantNodesAndSelf()) { - var spanNode = root.FindNode(span); + if (!helper.IsRelevantDeclarationNode(node) || !helper.SyntaxKinds.Contains(node.Kind())) + continue; - foreach (var node in spanNode.DescendantNodesAndSelf()) + if (useExpressionBody && helper.CanOfferUseExpressionBody(preference, node, forAnalyzer: false, cancellationToken)) { - if (!helper.IsRelevantDeclarationNode(node) || !helper.SyntaxKinds.Contains(node.Kind())) - continue; - - if (useExpressionBody && helper.CanOfferUseExpressionBody(preference, node, forAnalyzer: false, cancellationToken)) - { - yield return node; - } - else if (!useExpressionBody && helper.CanOfferUseBlockBody(preference, node, forAnalyzer: false, out _, out _)) - { - yield return node; - } + yield return node; + } + else if (!useExpressionBody && helper.CanOfferUseBlockBody(preference, node, forAnalyzer: false, out _, out _)) + { + yield return node; } } } + } - static async Task FixDeclarationsAsync( - Document document, - SyntaxEditor editor, - SyntaxNode root, - ImmutableArray declarationsToFix, - UseExpressionBodyHelper helper, - bool useExpressionBody, - CancellationToken cancellationToken) - { - // Track all the declaration nodes to be fixed so we can get the latest declaration node in the current root during updates. - var currentRoot = root.TrackNodes(declarationsToFix); - - // Process all declaration nodes in reverse to handle nested declaration updates properly. - foreach (var declaration in declarationsToFix.Reverse()) - { - // Get the current document, root, semanticModel and declaration. - document = document.WithSyntaxRoot(currentRoot); - currentRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var currentDeclaration = currentRoot.GetCurrentNodes(declaration).Single(); - - // Fix the current declaration and get updated current root - currentRoot = GetUpdatedRoot(semanticModel, currentRoot, currentDeclaration, helper, useExpressionBody, cancellationToken); - } + static async Task FixDeclarationsAsync( + Document document, + SyntaxEditor editor, + SyntaxNode root, + ImmutableArray declarationsToFix, + UseExpressionBodyHelper helper, + bool useExpressionBody, + CancellationToken cancellationToken) + { + // Track all the declaration nodes to be fixed so we can get the latest declaration node in the current root during updates. + var currentRoot = root.TrackNodes(declarationsToFix); - // Finally apply the latest current root to the editor. - editor.ReplaceNode(editor.OriginalRoot, currentRoot); + // Process all declaration nodes in reverse to handle nested declaration updates properly. + foreach (var declaration in declarationsToFix.Reverse()) + { + // Get the current document, root, semanticModel and declaration. + document = document.WithSyntaxRoot(currentRoot); + currentRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var currentDeclaration = currentRoot.GetCurrentNodes(declaration).Single(); + + // Fix the current declaration and get updated current root + currentRoot = GetUpdatedRoot(semanticModel, currentRoot, currentDeclaration, helper, useExpressionBody, cancellationToken); } + + // Finally apply the latest current root to the editor. + editor.ReplaceNode(editor.OriginalRoot, currentRoot); } } } diff --git a/src/Features/CSharp/Portable/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeRefactoringProvider.cs index 1cd73b6de0b48..7bfabecaf2835 100644 --- a/src/Features/CSharp/Portable/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/UseExpressionBodyForLambda/UseExpressionBodyForLambdaCodeRefactoringProvider.cs @@ -19,195 +19,194 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBodyForLambda +namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBodyForLambda; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseExpressionBodyForLambda), Shared] +internal sealed class UseExpressionBodyForLambdaCodeRefactoringProvider : CodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseExpressionBodyForLambda), Shared] - internal sealed class UseExpressionBodyForLambdaCodeRefactoringProvider : CodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public UseExpressionBodyForLambdaCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public UseExpressionBodyForLambdaCodeRefactoringProvider() - { - } + } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var document = context.Document; - var cancellationToken = context.CancellationToken; + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var document = context.Document; + var cancellationToken = context.CancellationToken; - var optionProvider = await document.GetAnalyzerOptionsProviderAsync(cancellationToken).ConfigureAwait(false); - var optionValue = UseExpressionBodyForLambdaHelpers.GetCodeStyleOption(optionProvider); + var optionProvider = await document.GetAnalyzerOptionsProviderAsync(cancellationToken).ConfigureAwait(false); + var optionValue = UseExpressionBodyForLambdaHelpers.GetCodeStyleOption(optionProvider); - var severity = UseExpressionBodyForLambdaHelpers.GetOptionSeverity(optionValue); - switch (severity) - { - case ReportDiagnostic.Suppress: - case ReportDiagnostic.Hidden: - // if the severity is Hidden that's equivalent to 'refactoring only', so we want - // to try to compute the refactoring here. - // - // If the severity is 'suppress', that means the user doesn't want the actual - // analyzer to run here. However, we can still check to see if we could offer - // the feature here as a refactoring. - await ComputeRefactoringsAsync(context, optionValue.Value, analyzerActive: false).ConfigureAwait(false); - return; - - case ReportDiagnostic.Error: - case ReportDiagnostic.Warn: - case ReportDiagnostic.Info: - // User has this option set at a level where we want it checked by the - // DiagnosticAnalyser and not the CodeRefactoringProvider. However, we still - // want to check if we want to offer the *reverse* refactoring here in this - // single location. - // - // For example, say this is the "use expression body" feature. If the user says - // they always prefer expression-bodies (with warning level), then we want the - // analyzer to always be checking for that. However, we still want to offer the - // refactoring to flip their code to use a block body here, just in case that - // was something they wanted to do as a one off (i.e. before adding new - // statements. - // - // TODO(cyrusn): Should we only do this for warn/info? Argument could be made - // that we shouldn't even offer to refactor in the reverse direction if it will - // just cause an error. That said, maybe this is just an intermediary step, and - // we shouldn't really be blocking the user from making it. - await ComputeRefactoringsAsync(context, optionValue.Value, analyzerActive: true).ConfigureAwait(false); - return; - } + var severity = UseExpressionBodyForLambdaHelpers.GetOptionSeverity(optionValue); + switch (severity) + { + case ReportDiagnostic.Suppress: + case ReportDiagnostic.Hidden: + // if the severity is Hidden that's equivalent to 'refactoring only', so we want + // to try to compute the refactoring here. + // + // If the severity is 'suppress', that means the user doesn't want the actual + // analyzer to run here. However, we can still check to see if we could offer + // the feature here as a refactoring. + await ComputeRefactoringsAsync(context, optionValue.Value, analyzerActive: false).ConfigureAwait(false); + return; + + case ReportDiagnostic.Error: + case ReportDiagnostic.Warn: + case ReportDiagnostic.Info: + // User has this option set at a level where we want it checked by the + // DiagnosticAnalyser and not the CodeRefactoringProvider. However, we still + // want to check if we want to offer the *reverse* refactoring here in this + // single location. + // + // For example, say this is the "use expression body" feature. If the user says + // they always prefer expression-bodies (with warning level), then we want the + // analyzer to always be checking for that. However, we still want to offer the + // refactoring to flip their code to use a block body here, just in case that + // was something they wanted to do as a one off (i.e. before adding new + // statements. + // + // TODO(cyrusn): Should we only do this for warn/info? Argument could be made + // that we shouldn't even offer to refactor in the reverse direction if it will + // just cause an error. That said, maybe this is just an intermediary step, and + // we shouldn't really be blocking the user from making it. + await ComputeRefactoringsAsync(context, optionValue.Value, analyzerActive: true).ConfigureAwait(false); + return; } + } - private static async Task ComputeRefactoringsAsync( - CodeRefactoringContext context, ExpressionBodyPreference option, bool analyzerActive) - { - var document = context.Document; - var span = context.Span; - var cancellationToken = context.CancellationToken; + private static async Task ComputeRefactoringsAsync( + CodeRefactoringContext context, ExpressionBodyPreference option, bool analyzerActive) + { + var document = context.Document; + var span = context.Span; + var cancellationToken = context.CancellationToken; - var computationTask = analyzerActive - ? ComputeOpposingRefactoringsWhenAnalyzerActiveAsync(document, span, option, cancellationToken) - : ComputeAllRefactoringsWhenAnalyzerInactiveAsync(document, span, cancellationToken); + var computationTask = analyzerActive + ? ComputeOpposingRefactoringsWhenAnalyzerActiveAsync(document, span, option, cancellationToken) + : ComputeAllRefactoringsWhenAnalyzerInactiveAsync(document, span, cancellationToken); - var codeActions = await computationTask.ConfigureAwait(false); - context.RegisterRefactorings(codeActions); - } + var codeActions = await computationTask.ConfigureAwait(false); + context.RegisterRefactorings(codeActions); + } - private static async Task> ComputeOpposingRefactoringsWhenAnalyzerActiveAsync( - Document document, TextSpan span, ExpressionBodyPreference option, CancellationToken cancellationToken) + private static async Task> ComputeOpposingRefactoringsWhenAnalyzerActiveAsync( + Document document, TextSpan span, ExpressionBodyPreference option, CancellationToken cancellationToken) + { + if (option == ExpressionBodyPreference.Never) { - if (option == ExpressionBodyPreference.Never) - { - // the user wants block-bodies (and the analyzer will be trying to enforce that). So - // the reverse of this is that we want to offer the refactoring to convert a - // block-body to an expression-body whenever possible. - return await ComputeRefactoringsAsync( - document, span, ExpressionBodyPreference.WhenPossible, cancellationToken).ConfigureAwait(false); - } - else if (option == ExpressionBodyPreference.WhenPossible) - { - // the user likes expression-bodies whenever possible, and the analyzer will be - // trying to enforce that. So the reverse of this is that we want to offer the - // refactoring to convert an expression-body to a block-body whenever possible. - return await ComputeRefactoringsAsync( - document, span, ExpressionBodyPreference.Never, cancellationToken).ConfigureAwait(false); - } - else if (option == ExpressionBodyPreference.WhenOnSingleLine) - { - // the user likes expression-bodies *if* the body would be on a single line. this - // means if we hit an block-body with an expression on a single line, then the - // analyzer will handle it for us. - - // So we need to handle the cases of either hitting an expression-body and wanting - // to convert it to a block-body *or* hitting an block-body over *multiple* lines and - // wanting to offer to convert to an expression-body. - - // Always offer to convert an expression to a block since the analyzer will never - // offer that. For this option setting. - var useBlockRefactorings = await ComputeRefactoringsAsync( - document, span, ExpressionBodyPreference.Never, cancellationToken).ConfigureAwait(false); - - var whenOnSingleLineRefactorings = await ComputeRefactoringsAsync( - document, span, ExpressionBodyPreference.WhenOnSingleLine, cancellationToken).ConfigureAwait(false); - if (whenOnSingleLineRefactorings.Length > 0) - { - // this block lambda would be converted to an expression lambda based on the - // analyzer alone. So we don't want to offer that as a refactoring ourselves. - return useBlockRefactorings; - } - - // The lambda block statement wasn't on a single line. So the analyzer would - // not offer to convert it to an expression body. So we should can offer that - // as a refactoring if possible. - var whenPossibleRefactorings = await ComputeRefactoringsAsync( - document, span, ExpressionBodyPreference.WhenPossible, cancellationToken).ConfigureAwait(false); - return useBlockRefactorings.AddRange(whenPossibleRefactorings); - } - else - { - throw ExceptionUtilities.UnexpectedValue(option); - } + // the user wants block-bodies (and the analyzer will be trying to enforce that). So + // the reverse of this is that we want to offer the refactoring to convert a + // block-body to an expression-body whenever possible. + return await ComputeRefactoringsAsync( + document, span, ExpressionBodyPreference.WhenPossible, cancellationToken).ConfigureAwait(false); } - - private static async Task> ComputeAllRefactoringsWhenAnalyzerInactiveAsync( - Document document, TextSpan span, CancellationToken cancellationToken) + else if (option == ExpressionBodyPreference.WhenPossible) { - // If the analyzer is inactive, then we want to offer refactorings in any viable - // direction. So we want to offer to convert expression-bodies to block-bodies, and - // vice-versa if applicable. - - var toExpressionBodyRefactorings = await ComputeRefactoringsAsync( - document, span, ExpressionBodyPreference.WhenPossible, cancellationToken).ConfigureAwait(false); - - var toBlockBodyRefactorings = await ComputeRefactoringsAsync( + // the user likes expression-bodies whenever possible, and the analyzer will be + // trying to enforce that. So the reverse of this is that we want to offer the + // refactoring to convert an expression-body to a block-body whenever possible. + return await ComputeRefactoringsAsync( document, span, ExpressionBodyPreference.Never, cancellationToken).ConfigureAwait(false); - - return toExpressionBodyRefactorings.AddRange(toBlockBodyRefactorings); } - - private static async Task> ComputeRefactoringsAsync( - Document document, TextSpan span, ExpressionBodyPreference option, CancellationToken cancellationToken) + else if (option == ExpressionBodyPreference.WhenOnSingleLine) { - var lambdaNode = await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); - if (lambdaNode == null) - { - return []; - } + // the user likes expression-bodies *if* the body would be on a single line. this + // means if we hit an block-body with an expression on a single line, then the + // analyzer will handle it for us. - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + // So we need to handle the cases of either hitting an expression-body and wanting + // to convert it to a block-body *or* hitting an block-body over *multiple* lines and + // wanting to offer to convert to an expression-body. - using var resultDisposer = ArrayBuilder.GetInstance(out var result); - if (UseExpressionBodyForLambdaHelpers.CanOfferUseExpressionBody(option, lambdaNode, root.GetLanguageVersion(), cancellationToken)) - { - var title = UseExpressionBodyForLambdaHelpers.UseExpressionBodyTitle.ToString(); - result.Add(CodeAction.Create( - title, - cancellationToken => UpdateDocumentAsync(document, root, lambdaNode, cancellationToken), - title)); - } + // Always offer to convert an expression to a block since the analyzer will never + // offer that. For this option setting. + var useBlockRefactorings = await ComputeRefactoringsAsync( + document, span, ExpressionBodyPreference.Never, cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (UseExpressionBodyForLambdaHelpers.CanOfferUseBlockBody(semanticModel, option, lambdaNode, cancellationToken)) + var whenOnSingleLineRefactorings = await ComputeRefactoringsAsync( + document, span, ExpressionBodyPreference.WhenOnSingleLine, cancellationToken).ConfigureAwait(false); + if (whenOnSingleLineRefactorings.Length > 0) { - var title = UseExpressionBodyForLambdaHelpers.UseBlockBodyTitle.ToString(); - result.Add(CodeAction.Create( - title, - cancellationToken => UpdateDocumentAsync(document, root, lambdaNode, cancellationToken), - title)); + // this block lambda would be converted to an expression lambda based on the + // analyzer alone. So we don't want to offer that as a refactoring ourselves. + return useBlockRefactorings; } - return result.ToImmutable(); + // The lambda block statement wasn't on a single line. So the analyzer would + // not offer to convert it to an expression body. So we should can offer that + // as a refactoring if possible. + var whenPossibleRefactorings = await ComputeRefactoringsAsync( + document, span, ExpressionBodyPreference.WhenPossible, cancellationToken).ConfigureAwait(false); + return useBlockRefactorings.AddRange(whenPossibleRefactorings); } + else + { + throw ExceptionUtilities.UnexpectedValue(option); + } + } + + private static async Task> ComputeAllRefactoringsWhenAnalyzerInactiveAsync( + Document document, TextSpan span, CancellationToken cancellationToken) + { + // If the analyzer is inactive, then we want to offer refactorings in any viable + // direction. So we want to offer to convert expression-bodies to block-bodies, and + // vice-versa if applicable. + + var toExpressionBodyRefactorings = await ComputeRefactoringsAsync( + document, span, ExpressionBodyPreference.WhenPossible, cancellationToken).ConfigureAwait(false); + + var toBlockBodyRefactorings = await ComputeRefactoringsAsync( + document, span, ExpressionBodyPreference.Never, cancellationToken).ConfigureAwait(false); - private static async Task UpdateDocumentAsync( - Document document, SyntaxNode root, LambdaExpressionSyntax declaration, CancellationToken cancellationToken) + return toExpressionBodyRefactorings.AddRange(toBlockBodyRefactorings); + } + + private static async Task> ComputeRefactoringsAsync( + Document document, TextSpan span, ExpressionBodyPreference option, CancellationToken cancellationToken) + { + var lambdaNode = await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); + if (lambdaNode == null) { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + return []; + } - // We're only replacing a single declaration in the refactoring. So pass 'declaration' - // as both the 'original' and 'current' declaration. - var updatedDeclaration = UseExpressionBodyForLambdaCodeActionHelpers.Update(semanticModel, declaration, declaration, cancellationToken); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + using var resultDisposer = ArrayBuilder.GetInstance(out var result); + if (UseExpressionBodyForLambdaHelpers.CanOfferUseExpressionBody(option, lambdaNode, root.GetLanguageVersion(), cancellationToken)) + { + var title = UseExpressionBodyForLambdaHelpers.UseExpressionBodyTitle.ToString(); + result.Add(CodeAction.Create( + title, + cancellationToken => UpdateDocumentAsync(document, root, lambdaNode, cancellationToken), + title)); + } - var newRoot = root.ReplaceNode(declaration, updatedDeclaration); - return document.WithSyntaxRoot(newRoot); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (UseExpressionBodyForLambdaHelpers.CanOfferUseBlockBody(semanticModel, option, lambdaNode, cancellationToken)) + { + var title = UseExpressionBodyForLambdaHelpers.UseBlockBodyTitle.ToString(); + result.Add(CodeAction.Create( + title, + cancellationToken => UpdateDocumentAsync(document, root, lambdaNode, cancellationToken), + title)); } + + return result.ToImmutable(); + } + + private static async Task UpdateDocumentAsync( + Document document, SyntaxNode root, LambdaExpressionSyntax declaration, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + // We're only replacing a single declaration in the refactoring. So pass 'declaration' + // as both the 'original' and 'current' declaration. + var updatedDeclaration = UseExpressionBodyForLambdaCodeActionHelpers.Update(semanticModel, declaration, declaration, cancellationToken); + + var newRoot = root.ReplaceNode(declaration, updatedDeclaration); + return document.WithSyntaxRoot(newRoot); } } diff --git a/src/Features/CSharp/Portable/UseNamedArguments/CSharpUseNamedArgumentsCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/UseNamedArguments/CSharpUseNamedArgumentsCodeRefactoringProvider.cs index 09dd11dc69a2b..f3225cb874cfc 100644 --- a/src/Features/CSharp/Portable/UseNamedArguments/CSharpUseNamedArgumentsCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/UseNamedArguments/CSharpUseNamedArgumentsCodeRefactoringProvider.cs @@ -12,94 +12,93 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.UseNamedArguments; -namespace Microsoft.CodeAnalysis.CSharp.UseNamedArguments +namespace Microsoft.CodeAnalysis.CSharp.UseNamedArguments; + +[ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseNamedArguments), Shared] +internal class CSharpUseNamedArgumentsCodeRefactoringProvider : AbstractUseNamedArgumentsCodeRefactoringProvider { - [ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.IntroduceVariable)] - [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseNamedArguments), Shared] - internal class CSharpUseNamedArgumentsCodeRefactoringProvider : AbstractUseNamedArgumentsCodeRefactoringProvider + private abstract class BaseAnalyzer : Analyzer + where TSyntax : SyntaxNode + where TSyntaxList : SyntaxNode { - private abstract class BaseAnalyzer : Analyzer - where TSyntax : SyntaxNode - where TSyntaxList : SyntaxNode - { - protected abstract ExpressionSyntax GetArgumentExpression(TSyntax argumentSyntax); + protected abstract ExpressionSyntax GetArgumentExpression(TSyntax argumentSyntax); - protected sealed override SyntaxNode? GetReceiver(SyntaxNode argument) - => argument.Parent?.Parent; + protected sealed override SyntaxNode? GetReceiver(SyntaxNode argument) + => argument.Parent?.Parent; - protected sealed override bool IsLegalToAddNamedArguments(ImmutableArray parameters, int argumentCount) - => !parameters.Last().IsParams || parameters.Length >= argumentCount; + protected sealed override bool IsLegalToAddNamedArguments(ImmutableArray parameters, int argumentCount) + => !parameters.Last().IsParams || parameters.Length >= argumentCount; - protected override bool SupportsNonTrailingNamedArguments(ParseOptions options) - => options.LanguageVersion() >= LanguageVersion.CSharp7_2; + protected override bool SupportsNonTrailingNamedArguments(ParseOptions options) + => options.LanguageVersion() >= LanguageVersion.CSharp7_2; - protected override bool IsImplicitIndexOrRangeIndexer(ImmutableArray parameters, TSyntax argument, SemanticModel semanticModel) + protected override bool IsImplicitIndexOrRangeIndexer(ImmutableArray parameters, TSyntax argument, SemanticModel semanticModel) + { + // There is no direct way to tell if an implicit range or index indexer was used. + // The heuristic we use here is to check if the parameter doesn't fit the method it's being used with. + // The easiest way to check that is to see if the argType only has at most an explicit conversion + // to the indexers parameter types. + + var argType = semanticModel.GetTypeInfo(GetArgumentExpression(argument)).Type; + if (argType?.ContainingNamespace is { Name: nameof(System), ContainingNamespace.IsGlobalNamespace: true } && + (argType.Name == "Range" || argType.Name == "Index")) { - // There is no direct way to tell if an implicit range or index indexer was used. - // The heuristic we use here is to check if the parameter doesn't fit the method it's being used with. - // The easiest way to check that is to see if the argType only has at most an explicit conversion - // to the indexers parameter types. - - var argType = semanticModel.GetTypeInfo(GetArgumentExpression(argument)).Type; - if (argType?.ContainingNamespace is { Name: nameof(System), ContainingNamespace.IsGlobalNamespace: true } && - (argType.Name == "Range" || argType.Name == "Index")) + var conversion = semanticModel.Compilation.ClassifyConversion(argType, parameters[0].Type); + if (!conversion.Exists || conversion.IsExplicit) { - var conversion = semanticModel.Compilation.ClassifyConversion(argType, parameters[0].Type); - if (!conversion.Exists || conversion.IsExplicit) - { - return true; - } + return true; } - - return false; } + + return false; } + } - private class ArgumentAnalyzer : - BaseAnalyzer - { - protected override bool IsPositionalArgument(ArgumentSyntax node) - => node.NameColon == null; + private class ArgumentAnalyzer : + BaseAnalyzer + { + protected override bool IsPositionalArgument(ArgumentSyntax node) + => node.NameColon == null; - protected override SeparatedSyntaxList GetArguments(BaseArgumentListSyntax argumentList) - => argumentList.Arguments; + protected override SeparatedSyntaxList GetArguments(BaseArgumentListSyntax argumentList) + => argumentList.Arguments; - protected override BaseArgumentListSyntax WithArguments( - BaseArgumentListSyntax argumentList, IEnumerable namedArguments, IEnumerable separators) - => argumentList.WithArguments(SyntaxFactory.SeparatedList(namedArguments, separators)); + protected override BaseArgumentListSyntax WithArguments( + BaseArgumentListSyntax argumentList, IEnumerable namedArguments, IEnumerable separators) + => argumentList.WithArguments(SyntaxFactory.SeparatedList(namedArguments, separators)); - protected override ArgumentSyntax WithName(ArgumentSyntax argument, string name) - => argument.WithNameColon(SyntaxFactory.NameColon(name.ToIdentifierName())); + protected override ArgumentSyntax WithName(ArgumentSyntax argument, string name) + => argument.WithNameColon(SyntaxFactory.NameColon(name.ToIdentifierName())); - protected override ExpressionSyntax GetArgumentExpression(ArgumentSyntax argumentSyntax) - => argumentSyntax.Expression; - } + protected override ExpressionSyntax GetArgumentExpression(ArgumentSyntax argumentSyntax) + => argumentSyntax.Expression; + } - private class AttributeArgumentAnalyzer : - BaseAnalyzer - { - protected override bool IsPositionalArgument(AttributeArgumentSyntax argument) - => argument.NameColon == null && argument.NameEquals == null; + private class AttributeArgumentAnalyzer : + BaseAnalyzer + { + protected override bool IsPositionalArgument(AttributeArgumentSyntax argument) + => argument.NameColon == null && argument.NameEquals == null; - protected override SeparatedSyntaxList GetArguments(AttributeArgumentListSyntax argumentList) - => argumentList.Arguments; + protected override SeparatedSyntaxList GetArguments(AttributeArgumentListSyntax argumentList) + => argumentList.Arguments; - protected override AttributeArgumentListSyntax WithArguments( - AttributeArgumentListSyntax argumentList, IEnumerable namedArguments, IEnumerable separators) - => argumentList.WithArguments(SyntaxFactory.SeparatedList(namedArguments, separators)); + protected override AttributeArgumentListSyntax WithArguments( + AttributeArgumentListSyntax argumentList, IEnumerable namedArguments, IEnumerable separators) + => argumentList.WithArguments(SyntaxFactory.SeparatedList(namedArguments, separators)); - protected override AttributeArgumentSyntax WithName(AttributeArgumentSyntax argument, string name) - => argument.WithNameColon(SyntaxFactory.NameColon(name.ToIdentifierName())); + protected override AttributeArgumentSyntax WithName(AttributeArgumentSyntax argument, string name) + => argument.WithNameColon(SyntaxFactory.NameColon(name.ToIdentifierName())); - protected override ExpressionSyntax GetArgumentExpression(AttributeArgumentSyntax argumentSyntax) - => argumentSyntax.Expression; - } + protected override ExpressionSyntax GetArgumentExpression(AttributeArgumentSyntax argumentSyntax) + => argumentSyntax.Expression; + } - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpUseNamedArgumentsCodeRefactoringProvider() - : base(new ArgumentAnalyzer(), new AttributeArgumentAnalyzer()) - { - } + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpUseNamedArgumentsCodeRefactoringProvider() + : base(new ArgumentAnalyzer(), new AttributeArgumentAnalyzer()) + { } } diff --git a/src/Features/CSharp/Portable/UsePatternMatching/CSharpIsAndCastCheckWithoutNameCodeFixProvider.cs b/src/Features/CSharp/Portable/UsePatternMatching/CSharpIsAndCastCheckWithoutNameCodeFixProvider.cs index 73279916b2770..f45e1cc283acc 100644 --- a/src/Features/CSharp/Portable/UsePatternMatching/CSharpIsAndCastCheckWithoutNameCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/UsePatternMatching/CSharpIsAndCastCheckWithoutNameCodeFixProvider.cs @@ -20,53 +20,52 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching +namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UsePatternMatchingIsAndCastCheckWithoutName), Shared] +internal partial class CSharpIsAndCastCheckWithoutNameCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UsePatternMatchingIsAndCastCheckWithoutName), Shared] - internal partial class CSharpIsAndCastCheckWithoutNameCodeFixProvider : SyntaxEditorBasedCodeFixProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public CSharpIsAndCastCheckWithoutNameCodeFixProvider() + : base(supportsFixAll: false) { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpIsAndCastCheckWithoutNameCodeFixProvider() - : base(supportsFixAll: false) - { - } + } - public override ImmutableArray FixableDiagnosticIds - => [IDEDiagnosticIds.InlineIsTypeWithoutNameCheckDiagnosticsId]; + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.InlineIsTypeWithoutNameCheckDiagnosticsId]; - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - context.RegisterCodeFix( - CodeAction.Create( - CSharpAnalyzersResources.Use_pattern_matching, - GetDocumentUpdater(context), - nameof(CSharpAnalyzersResources.Use_pattern_matching), - CodeActionPriority.Low), - context.Diagnostics); - return Task.CompletedTask; - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + context.RegisterCodeFix( + CodeAction.Create( + CSharpAnalyzersResources.Use_pattern_matching, + GetDocumentUpdater(context), + nameof(CSharpAnalyzersResources.Use_pattern_matching), + CodeActionPriority.Low), + context.Diagnostics); + return Task.CompletedTask; + } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - Debug.Assert(diagnostics.Length == 1); - var location = diagnostics[0].Location; - var isExpression = (BinaryExpressionSyntax)location.FindNode( - getInnermostNodeForTie: true, cancellationToken: cancellationToken); + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + Debug.Assert(diagnostics.Length == 1); + var location = diagnostics[0].Location; + var isExpression = (BinaryExpressionSyntax)location.FindNode( + getInnermostNodeForTie: true, cancellationToken: cancellationToken); - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var expressionTypeOpt = semanticModel.Compilation.ExpressionOfTType(); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var expressionTypeOpt = semanticModel.Compilation.ExpressionOfTType(); - var (matches, localName) = CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer.AnalyzeExpression( - semanticModel, isExpression, expressionTypeOpt, cancellationToken); + var (matches, localName) = CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer.AnalyzeExpression( + semanticModel, isExpression, expressionTypeOpt, cancellationToken); - var updatedSemanticModel = CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer.ReplaceMatches( - semanticModel, isExpression, localName, matches, cancellationToken); + var updatedSemanticModel = CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer.ReplaceMatches( + semanticModel, isExpression, localName, matches, cancellationToken); - var updatedRoot = updatedSemanticModel.SyntaxTree.GetRoot(cancellationToken); - editor.ReplaceNode(editor.OriginalRoot, updatedRoot); - } + var updatedRoot = updatedSemanticModel.SyntaxTree.GetRoot(cancellationToken); + editor.ReplaceNode(editor.OriginalRoot, updatedRoot); } } diff --git a/src/Features/CSharp/Portable/UsePatternMatching/CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/UsePatternMatching/CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer.cs index 325faa97b31cd..3022d60dab8de 100644 --- a/src/Features/CSharp/Portable/UsePatternMatching/CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer.cs +++ b/src/Features/CSharp/Portable/UsePatternMatching/CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer.cs @@ -20,251 +20,251 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching +namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching; + +/// +/// DiagnosticAnalyzer that looks for is-tests and cast-expressions, and offers to convert them +/// to use patterns. i.e. if the user has obj is TestFile && ((TestFile)obj).Name == "Test" +/// it will offer to convert that obj is TestFile file && file.Name == "Test". +/// +/// Complements (which does the same, +/// but only for code cases where the user has provided an appropriate variable name in +/// code that can be used). +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - /// - /// DiagnosticAnalyzer that looks for is-tests and cast-expressions, and offers to convert them - /// to use patterns. i.e. if the user has obj is TestFile && ((TestFile)obj).Name == "Test" - /// it will offer to convert that obj is TestFile file && file.Name == "Test". - /// - /// Complements (which does the same, - /// but only for code cases where the user has provided an appropriate variable name in - /// code that can be used). - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal class CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + private const string CS0165 = nameof(CS0165); // Use of unassigned local variable 's' + private const string CS0103 = nameof(CS0103); // Name of the variable doesn't live in context + private static readonly SyntaxAnnotation s_referenceAnnotation = new(); + + public static readonly CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer Instance = new(); + + public CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer() + : base(IDEDiagnosticIds.InlineIsTypeWithoutNameCheckDiagnosticsId, + EnforceOnBuildValues.InlineIsTypeWithoutName, + CSharpCodeStyleOptions.PreferPatternMatchingOverIsWithCastCheck, + new LocalizableResourceString( + nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) { - private const string CS0165 = nameof(CS0165); // Use of unassigned local variable 's' - private const string CS0103 = nameof(CS0103); // Name of the variable doesn't live in context - private static readonly SyntaxAnnotation s_referenceAnnotation = new(); - - public static readonly CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer Instance = new(); - - public CSharpIsAndCastCheckWithoutNameDiagnosticAnalyzer() - : base(IDEDiagnosticIds.InlineIsTypeWithoutNameCheckDiagnosticsId, - EnforceOnBuildValues.InlineIsTypeWithoutName, - CSharpCodeStyleOptions.PreferPatternMatchingOverIsWithCastCheck, - new LocalizableResourceString( - nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources))) + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected override void InitializeWorker(AnalysisContext context) + { + context.RegisterCompilationStartAction(context => { - } + var expressionTypeOpt = context.Compilation.ExpressionOfTType(); + context.RegisterSyntaxNodeAction(context => SyntaxNodeAction(context, expressionTypeOpt), SyntaxKind.IsExpression); + }); + } - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + private void SyntaxNodeAction( + SyntaxNodeAnalysisContext context, + INamedTypeSymbol? expressionType) + { + var cancellationToken = context.CancellationToken; + var semanticModel = context.SemanticModel; + var syntaxTree = semanticModel.SyntaxTree; - protected override void InitializeWorker(AnalysisContext context) + // "x is Type y" is only available in C# 7.0 and above. Don't offer this refactoring + // in projects targeting a lesser version. + if (syntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp7) { - context.RegisterCompilationStartAction(context => - { - var expressionTypeOpt = context.Compilation.ExpressionOfTType(); - context.RegisterSyntaxNodeAction(context => SyntaxNodeAction(context, expressionTypeOpt), SyntaxKind.IsExpression); - }); + return; } - private void SyntaxNodeAction( - SyntaxNodeAnalysisContext context, - INamedTypeSymbol? expressionType) + var styleOption = context.GetCSharpAnalyzerOptions().PreferPatternMatchingOverIsWithCastCheck; + if (!styleOption.Value || ShouldSkipAnalysis(context, styleOption.Notification)) { - var cancellationToken = context.CancellationToken; - var semanticModel = context.SemanticModel; - var syntaxTree = semanticModel.SyntaxTree; + // User has disabled this feature. + return; + } - // "x is Type y" is only available in C# 7.0 and above. Don't offer this refactoring - // in projects targeting a lesser version. - if (syntaxTree.Options.LanguageVersion() < LanguageVersion.CSharp7) - { - return; - } + var isExpression = (BinaryExpressionSyntax)context.Node; - var styleOption = context.GetCSharpAnalyzerOptions().PreferPatternMatchingOverIsWithCastCheck; - if (!styleOption.Value || ShouldSkipAnalysis(context, styleOption.Notification)) - { - // User has disabled this feature. - return; - } + // See if this is an 'is' expression that would be handled by the analyzer. If so + // we don't need to do anything. + if (CSharpIsAndCastCheckDiagnosticAnalyzer.TryGetPatternPieces( + isExpression, out _, out _, out _, out _)) + { + return; + } - var isExpression = (BinaryExpressionSyntax)context.Node; + var (matches, _) = AnalyzeExpression(semanticModel, isExpression, expressionType, cancellationToken); + if (matches == null || matches.Count == 0) + return; + + context.ReportDiagnostic( + DiagnosticHelper.Create( + Descriptor, isExpression.GetLocation(), + styleOption.Notification, + context.Options, + SpecializedCollections.EmptyCollection(), + ImmutableDictionary.Empty)); + } - // See if this is an 'is' expression that would be handled by the analyzer. If so - // we don't need to do anything. - if (CSharpIsAndCastCheckDiagnosticAnalyzer.TryGetPatternPieces( - isExpression, out _, out _, out _, out _)) - { - return; - } + public static (HashSet, string localName) AnalyzeExpression( + SemanticModel semanticModel, + BinaryExpressionSyntax isExpression, + INamedTypeSymbol? expressionType, + CancellationToken cancellationToken) + { + var container = GetContainer(isExpression); + if (container == null) + return default; - var (matches, _) = AnalyzeExpression(semanticModel, isExpression, expressionType, cancellationToken); - if (matches == null || matches.Count == 0) - return; + // Pattern matching is not supported in expression tree. So we can't fix this up. + if (CSharpSemanticFactsService.Instance.IsInExpressionTree(semanticModel, isExpression, expressionType, cancellationToken)) + return default; - context.ReportDiagnostic( - DiagnosticHelper.Create( - Descriptor, isExpression.GetLocation(), - styleOption.Notification, - SpecializedCollections.EmptyCollection(), - ImmutableDictionary.Empty)); - } + var expr = isExpression.Left.WalkDownParentheses(); + var type = (TypeSyntax)isExpression.Right; - public static (HashSet, string localName) AnalyzeExpression( - SemanticModel semanticModel, - BinaryExpressionSyntax isExpression, - INamedTypeSymbol? expressionType, - CancellationToken cancellationToken) + var typeSymbol = semanticModel.GetTypeInfo(type, cancellationToken).Type; + if (typeSymbol == null || typeSymbol.IsNullable()) { - var container = GetContainer(isExpression); - if (container == null) - return default; - - // Pattern matching is not supported in expression tree. So we can't fix this up. - if (CSharpSemanticFactsService.Instance.IsInExpressionTree(semanticModel, isExpression, expressionType, cancellationToken)) - return default; - - var expr = isExpression.Left.WalkDownParentheses(); - var type = (TypeSyntax)isExpression.Right; + // not legal to write "(x is int? y)" + return default; + } - var typeSymbol = semanticModel.GetTypeInfo(type, cancellationToken).Type; - if (typeSymbol == null || typeSymbol.IsNullable()) - { - // not legal to write "(x is int? y)" - return default; - } + // First, find all the potential cast locations we can replace with a reference to + // our new local. + var matches = new HashSet(); + AddMatches(container, expr, type, matches); - // First, find all the potential cast locations we can replace with a reference to - // our new local. - var matches = new HashSet(); - AddMatches(container, expr, type, matches); + if (matches.Count == 0) + { + return default; + } - if (matches.Count == 0) - { - return default; - } + // Find a reasonable name for the local we're going to make. It should ideally + // relate to the type the user is casting to, and it should not collide with anything + // in scope. + var reservedNames = semanticModel.LookupSymbols(isExpression.SpanStart) + .Concat(semanticModel.GetExistingSymbols(container, cancellationToken)) + .Select(s => s.Name) + .ToSet(); + + var localName = NameGenerator.EnsureUniqueness( + SyntaxGeneratorExtensions.GetLocalName(typeSymbol), + reservedNames).EscapeIdentifier(); + + // Now, go and actually try to make the change. This will allow us to see all the + // locations that we updated to see if that caused an error. + var tempMatches = new HashSet(); + foreach (var castExpression in matches.ToArray()) + { + tempMatches.Add(castExpression); + var updatedSemanticModel = ReplaceMatches( + semanticModel, isExpression, localName, tempMatches, cancellationToken); + tempMatches.Clear(); - // Find a reasonable name for the local we're going to make. It should ideally - // relate to the type the user is casting to, and it should not collide with anything - // in scope. - var reservedNames = semanticModel.LookupSymbols(isExpression.SpanStart) - .Concat(semanticModel.GetExistingSymbols(container, cancellationToken)) - .Select(s => s.Name) - .ToSet(); - - var localName = NameGenerator.EnsureUniqueness( - SyntaxGeneratorExtensions.GetLocalName(typeSymbol), - reservedNames).EscapeIdentifier(); - - // Now, go and actually try to make the change. This will allow us to see all the - // locations that we updated to see if that caused an error. - var tempMatches = new HashSet(); - foreach (var castExpression in matches.ToArray()) + var causesError = ReplacementCausesError(updatedSemanticModel, cancellationToken); + if (causesError) { - tempMatches.Add(castExpression); - var updatedSemanticModel = ReplaceMatches( - semanticModel, isExpression, localName, tempMatches, cancellationToken); - tempMatches.Clear(); - - var causesError = ReplacementCausesError(updatedSemanticModel, cancellationToken); - if (causesError) - { - matches.Remove(castExpression); - } + matches.Remove(castExpression); } - - return (matches, localName); } - private static bool ReplacementCausesError( - SemanticModel updatedSemanticModel, CancellationToken cancellationToken) - { - var root = updatedSemanticModel.SyntaxTree.GetRoot(cancellationToken); + return (matches, localName); + } - var currentNode = root.GetAnnotatedNodes(s_referenceAnnotation).Single(); - var diagnostics = updatedSemanticModel.GetDiagnostics(currentNode.Span, cancellationToken); + private static bool ReplacementCausesError( + SemanticModel updatedSemanticModel, CancellationToken cancellationToken) + { + var root = updatedSemanticModel.SyntaxTree.GetRoot(cancellationToken); - return diagnostics.Any(static d => d.Id is CS0165 or CS0103); - } + var currentNode = root.GetAnnotatedNodes(s_referenceAnnotation).Single(); + var diagnostics = updatedSemanticModel.GetDiagnostics(currentNode.Span, cancellationToken); - public static SemanticModel ReplaceMatches( - SemanticModel semanticModel, BinaryExpressionSyntax isExpression, - string localName, HashSet matches, - CancellationToken cancellationToken) + return diagnostics.Any(static d => d.Id is CS0165 or CS0103); + } + + public static SemanticModel ReplaceMatches( + SemanticModel semanticModel, BinaryExpressionSyntax isExpression, + string localName, HashSet matches, + CancellationToken cancellationToken) + { + var root = semanticModel.SyntaxTree.GetRoot(cancellationToken); + var editor = new SyntaxEditor(root, CSharpSyntaxGenerator.Instance); + + // now, replace "x is Y" with "x is Y y" and put a rename-annotation on 'y' so that + // the user can actually name the variable whatever they want. + var newLocalName = SyntaxFactory.Identifier(localName) + .WithAdditionalAnnotations(RenameAnnotation.Create()); + var isPattern = SyntaxFactory.IsPatternExpression( + isExpression.Left, isExpression.OperatorToken, + SyntaxFactory.DeclarationPattern((TypeSyntax)isExpression.Right.WithTrailingTrivia(SyntaxFactory.Space), + SyntaxFactory.SingleVariableDesignation(newLocalName))).WithTriviaFrom(isExpression); + + editor.ReplaceNode(isExpression, isPattern); + + // Now, go through all the "(Y)x" casts and replace them with just "y". + var localReference = SyntaxFactory.IdentifierName(localName); + foreach (var castExpression in matches) { - var root = semanticModel.SyntaxTree.GetRoot(cancellationToken); - var editor = new SyntaxEditor(root, CSharpSyntaxGenerator.Instance); - - // now, replace "x is Y" with "x is Y y" and put a rename-annotation on 'y' so that - // the user can actually name the variable whatever they want. - var newLocalName = SyntaxFactory.Identifier(localName) - .WithAdditionalAnnotations(RenameAnnotation.Create()); - var isPattern = SyntaxFactory.IsPatternExpression( - isExpression.Left, isExpression.OperatorToken, - SyntaxFactory.DeclarationPattern((TypeSyntax)isExpression.Right.WithTrailingTrivia(SyntaxFactory.Space), - SyntaxFactory.SingleVariableDesignation(newLocalName))).WithTriviaFrom(isExpression); - - editor.ReplaceNode(isExpression, isPattern); - - // Now, go through all the "(Y)x" casts and replace them with just "y". - var localReference = SyntaxFactory.IdentifierName(localName); - foreach (var castExpression in matches) - { - var castRoot = castExpression.WalkUpParentheses(); + var castRoot = castExpression.WalkUpParentheses(); - editor.ReplaceNode( - castRoot, - localReference.WithTriviaFrom(castRoot) - .WithAdditionalAnnotations(s_referenceAnnotation)); - } + editor.ReplaceNode( + castRoot, + localReference.WithTriviaFrom(castRoot) + .WithAdditionalAnnotations(s_referenceAnnotation)); + } - var changedRoot = editor.GetChangedRoot(); - var updatedSyntaxTree = semanticModel.SyntaxTree.WithRootAndOptions( - changedRoot, semanticModel.SyntaxTree.Options); + var changedRoot = editor.GetChangedRoot(); + var updatedSyntaxTree = semanticModel.SyntaxTree.WithRootAndOptions( + changedRoot, semanticModel.SyntaxTree.Options); - var updatedCompilation = semanticModel.Compilation.ReplaceSyntaxTree( - semanticModel.SyntaxTree, updatedSyntaxTree); + var updatedCompilation = semanticModel.Compilation.ReplaceSyntaxTree( + semanticModel.SyntaxTree, updatedSyntaxTree); #pragma warning disable RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer - return updatedCompilation.GetSemanticModel(updatedSyntaxTree); + return updatedCompilation.GetSemanticModel(updatedSyntaxTree); #pragma warning restore RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer - } + } - private static SyntaxNode? GetContainer(BinaryExpressionSyntax isExpression) + private static SyntaxNode? GetContainer(BinaryExpressionSyntax isExpression) + { + for (SyntaxNode? current = isExpression; current != null; current = current.Parent) { - for (SyntaxNode? current = isExpression; current != null; current = current.Parent) + switch (current) { - switch (current) - { - case StatementSyntax statement: - return statement; - case LambdaExpressionSyntax lambda: - return lambda.Body; - case ArrowExpressionClauseSyntax arrowExpression: - return arrowExpression.Expression; - case EqualsValueClauseSyntax equalsValue: - return equalsValue.Value; - } + case StatementSyntax statement: + return statement; + case LambdaExpressionSyntax lambda: + return lambda.Body; + case ArrowExpressionClauseSyntax arrowExpression: + return arrowExpression.Expression; + case EqualsValueClauseSyntax equalsValue: + return equalsValue.Value; } - - return null; } - private static void AddMatches( - SyntaxNode node, ExpressionSyntax expr, TypeSyntax type, HashSet matches) + return null; + } + + private static void AddMatches( + SyntaxNode node, ExpressionSyntax expr, TypeSyntax type, HashSet matches) + { + // Don't bother recursing down nodes that are before the type in the is-expression. + if (node.Span.End >= type.Span.End) { - // Don't bother recursing down nodes that are before the type in the is-expression. - if (node.Span.End >= type.Span.End) + if (node is CastExpressionSyntax castExpression) { - if (node is CastExpressionSyntax castExpression) + if (SyntaxFactory.AreEquivalent(castExpression.Type, type) && + SyntaxFactory.AreEquivalent(castExpression.Expression.WalkDownParentheses(), expr)) { - if (SyntaxFactory.AreEquivalent(castExpression.Type, type) && - SyntaxFactory.AreEquivalent(castExpression.Expression.WalkDownParentheses(), expr)) - { - matches.Add(castExpression); - } + matches.Add(castExpression); } + } - foreach (var child in node.ChildNodesAndTokens()) + foreach (var child in node.ChildNodesAndTokens()) + { + if (child.IsNode) { - if (child.IsNode) - { - AddMatches(child.AsNode()!, expr, type, matches); - } + AddMatches(child.AsNode()!, expr, type, matches); } } } diff --git a/src/Features/CSharp/Portable/Wrapping/BinaryExpression/CSharpBinaryExpressionWrapper.cs b/src/Features/CSharp/Portable/Wrapping/BinaryExpression/CSharpBinaryExpressionWrapper.cs index c8c321d4b6400..c910634427f1b 100644 --- a/src/Features/CSharp/Portable/Wrapping/BinaryExpression/CSharpBinaryExpressionWrapper.cs +++ b/src/Features/CSharp/Portable/Wrapping/BinaryExpression/CSharpBinaryExpressionWrapper.cs @@ -10,16 +10,15 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Wrapping.BinaryExpression; -namespace Microsoft.CodeAnalysis.CSharp.Wrapping.BinaryExpression +namespace Microsoft.CodeAnalysis.CSharp.Wrapping.BinaryExpression; + +internal class CSharpBinaryExpressionWrapper : AbstractBinaryExpressionWrapper { - internal class CSharpBinaryExpressionWrapper : AbstractBinaryExpressionWrapper + public CSharpBinaryExpressionWrapper() + : base(CSharpIndentationService.Instance, CSharpSyntaxFacts.Instance, CSharpExpressionPrecedenceService.Instance) { - public CSharpBinaryExpressionWrapper() - : base(CSharpIndentationService.Instance, CSharpSyntaxFacts.Instance, CSharpExpressionPrecedenceService.Instance) - { - } - - protected override SyntaxTriviaList GetNewLineBeforeOperatorTrivia(SyntaxTriviaList newLine) - => newLine; } + + protected override SyntaxTriviaList GetNewLineBeforeOperatorTrivia(SyntaxTriviaList newLine) + => newLine; } diff --git a/src/Features/CSharp/Portable/Wrapping/CSharpSyntaxWrappingOptions.cs b/src/Features/CSharp/Portable/Wrapping/CSharpSyntaxWrappingOptions.cs index 7c5a9d942eb2b..8b86f4bd4f357 100644 --- a/src/Features/CSharp/Portable/Wrapping/CSharpSyntaxWrappingOptions.cs +++ b/src/Features/CSharp/Portable/Wrapping/CSharpSyntaxWrappingOptions.cs @@ -9,28 +9,27 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Wrapping; -namespace Microsoft.CodeAnalysis.CSharp.Wrapping +namespace Microsoft.CodeAnalysis.CSharp.Wrapping; + +internal sealed class CSharpSyntaxWrappingOptions( + CSharpSyntaxFormattingOptions formattingOptions, + int wrappingColumn, + OperatorPlacementWhenWrappingPreference operatorPlacement, + bool newLinesForBracesInObjectCollectionArrayInitializers) : SyntaxWrappingOptions(formattingOptions, wrappingColumn, operatorPlacement) { - internal sealed class CSharpSyntaxWrappingOptions( - CSharpSyntaxFormattingOptions formattingOptions, - int wrappingColumn, - OperatorPlacementWhenWrappingPreference operatorPlacement, - bool newLinesForBracesInObjectCollectionArrayInitializers) : SyntaxWrappingOptions(formattingOptions, wrappingColumn, operatorPlacement) - { - public readonly bool NewLinesForBracesInObjectCollectionArrayInitializers = newLinesForBracesInObjectCollectionArrayInitializers; - } + public readonly bool NewLinesForBracesInObjectCollectionArrayInitializers = newLinesForBracesInObjectCollectionArrayInitializers; +} - internal static class CSharpSyntaxWrappingOptionsProviders +internal static class CSharpSyntaxWrappingOptionsProviders +{ + public static CSharpSyntaxWrappingOptions GetCSharpSyntaxWrappingOptions(this IOptionsReader options, CodeActionOptions fallbackOptions) { - public static CSharpSyntaxWrappingOptions GetCSharpSyntaxWrappingOptions(this IOptionsReader options, CodeActionOptions fallbackOptions) - { - var newLineBeforeOpenBraceDefault = ((CSharpSyntaxFormattingOptions)fallbackOptions.CleanupOptions.FormattingOptions).NewLines.ToNewLineBeforeOpenBracePlacement(); + var newLineBeforeOpenBraceDefault = ((CSharpSyntaxFormattingOptions)fallbackOptions.CleanupOptions.FormattingOptions).NewLines.ToNewLineBeforeOpenBracePlacement(); - return new( - new CSharpSyntaxFormattingOptions(options, (CSharpSyntaxFormattingOptions)fallbackOptions.CleanupOptions.FormattingOptions), - operatorPlacement: options.GetOption(CodeStyleOptions2.OperatorPlacementWhenWrapping, fallbackOptions.CodeStyleOptions.OperatorPlacementWhenWrapping), - wrappingColumn: fallbackOptions.WrappingColumn, - newLinesForBracesInObjectCollectionArrayInitializers: options.GetOption(CSharpFormattingOptions2.NewLineBeforeOpenBrace, newLineBeforeOpenBraceDefault).HasFlag(NewLineBeforeOpenBracePlacement.ObjectCollectionArrayInitializers)); - } + return new( + new CSharpSyntaxFormattingOptions(options, (CSharpSyntaxFormattingOptions)fallbackOptions.CleanupOptions.FormattingOptions), + operatorPlacement: options.GetOption(CodeStyleOptions2.OperatorPlacementWhenWrapping, fallbackOptions.CodeStyleOptions.OperatorPlacementWhenWrapping), + wrappingColumn: fallbackOptions.WrappingColumn, + newLinesForBracesInObjectCollectionArrayInitializers: options.GetOption(CSharpFormattingOptions2.NewLineBeforeOpenBrace, newLineBeforeOpenBraceDefault).HasFlag(NewLineBeforeOpenBracePlacement.ObjectCollectionArrayInitializers)); } } diff --git a/src/Features/CSharp/Portable/Wrapping/ChainedExpression/CSharpChainedExpressionWrapper.cs b/src/Features/CSharp/Portable/Wrapping/ChainedExpression/CSharpChainedExpressionWrapper.cs index 15f4b854f774f..53996b05b23b1 100644 --- a/src/Features/CSharp/Portable/Wrapping/ChainedExpression/CSharpChainedExpressionWrapper.cs +++ b/src/Features/CSharp/Portable/Wrapping/ChainedExpression/CSharpChainedExpressionWrapper.cs @@ -9,17 +9,16 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Wrapping.ChainedExpression; -namespace Microsoft.CodeAnalysis.CSharp.Wrapping.ChainedExpression +namespace Microsoft.CodeAnalysis.CSharp.Wrapping.ChainedExpression; + +internal class CSharpChainedExpressionWrapper : + AbstractChainedExpressionWrapper { - internal class CSharpChainedExpressionWrapper : - AbstractChainedExpressionWrapper + public CSharpChainedExpressionWrapper() + : base(CSharpIndentationService.Instance, CSharpSyntaxFacts.Instance) { - public CSharpChainedExpressionWrapper() - : base(CSharpIndentationService.Instance, CSharpSyntaxFacts.Instance) - { - } - - protected override SyntaxTriviaList GetNewLineBeforeOperatorTrivia(SyntaxTriviaList newLine) - => newLine; } + + protected override SyntaxTriviaList GetNewLineBeforeOperatorTrivia(SyntaxTriviaList newLine) + => newLine; } diff --git a/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/AbstractCSharpSeparatedSyntaxListWrapper.cs b/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/AbstractCSharpSeparatedSyntaxListWrapper.cs index 54dc545f04bf0..4a378f3e96270 100644 --- a/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/AbstractCSharpSeparatedSyntaxListWrapper.cs +++ b/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/AbstractCSharpSeparatedSyntaxListWrapper.cs @@ -5,16 +5,15 @@ using Microsoft.CodeAnalysis.CSharp.Indentation; using Microsoft.CodeAnalysis.Wrapping.SeparatedSyntaxList; -namespace Microsoft.CodeAnalysis.CSharp.Wrapping.SeparatedSyntaxList +namespace Microsoft.CodeAnalysis.CSharp.Wrapping.SeparatedSyntaxList; + +internal abstract class AbstractCSharpSeparatedSyntaxListWrapper + : AbstractSeparatedSyntaxListWrapper + where TListSyntax : SyntaxNode + where TListItemSyntax : SyntaxNode { - internal abstract class AbstractCSharpSeparatedSyntaxListWrapper - : AbstractSeparatedSyntaxListWrapper - where TListSyntax : SyntaxNode - where TListItemSyntax : SyntaxNode + protected AbstractCSharpSeparatedSyntaxListWrapper() + : base(CSharpIndentationService.Instance) { - protected AbstractCSharpSeparatedSyntaxListWrapper() - : base(CSharpIndentationService.Instance) - { - } } } diff --git a/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpArgumentWrapper.cs b/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpArgumentWrapper.cs index 0773623b975e6..0277014e68d70 100644 --- a/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpArgumentWrapper.cs +++ b/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpArgumentWrapper.cs @@ -12,118 +12,117 @@ using Microsoft.CodeAnalysis.Wrapping; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Wrapping.SeparatedSyntaxList +namespace Microsoft.CodeAnalysis.CSharp.Wrapping.SeparatedSyntaxList; + +internal partial class CSharpArgumentWrapper + : AbstractCSharpSeparatedSyntaxListWrapper { - internal partial class CSharpArgumentWrapper - : AbstractCSharpSeparatedSyntaxListWrapper - { - protected override string Align_wrapped_items => FeaturesResources.Align_wrapped_arguments; - protected override string Indent_all_items => FeaturesResources.Indent_all_arguments; - protected override string Indent_wrapped_items => FeaturesResources.Indent_wrapped_arguments; - protected override string Unwrap_all_items => FeaturesResources.Unwrap_all_arguments; - protected override string Unwrap_and_indent_all_items => FeaturesResources.Unwrap_and_indent_all_arguments; - protected override string Unwrap_list => FeaturesResources.Unwrap_argument_list; - protected override string Wrap_every_item => FeaturesResources.Wrap_every_argument; - protected override string Wrap_long_list => FeaturesResources.Wrap_long_argument_list; + protected override string Align_wrapped_items => FeaturesResources.Align_wrapped_arguments; + protected override string Indent_all_items => FeaturesResources.Indent_all_arguments; + protected override string Indent_wrapped_items => FeaturesResources.Indent_wrapped_arguments; + protected override string Unwrap_all_items => FeaturesResources.Unwrap_all_arguments; + protected override string Unwrap_and_indent_all_items => FeaturesResources.Unwrap_and_indent_all_arguments; + protected override string Unwrap_list => FeaturesResources.Unwrap_argument_list; + protected override string Wrap_every_item => FeaturesResources.Wrap_every_argument; + protected override string Wrap_long_list => FeaturesResources.Wrap_long_argument_list; - public override bool Supports_UnwrapGroup_WrapFirst_IndentRest => true; - public override bool Supports_WrapEveryGroup_UnwrapFirst => true; - public override bool Supports_WrapLongGroup_UnwrapFirst => true; + public override bool Supports_UnwrapGroup_WrapFirst_IndentRest => true; + public override bool Supports_WrapEveryGroup_UnwrapFirst => true; + public override bool Supports_WrapLongGroup_UnwrapFirst => true; - protected override bool ShouldMoveOpenBraceToNewLine(SyntaxWrappingOptions options) - => false; + protected override bool ShouldMoveOpenBraceToNewLine(SyntaxWrappingOptions options) + => false; - protected override bool ShouldMoveCloseBraceToNewLine - => false; + protected override bool ShouldMoveCloseBraceToNewLine + => false; - protected override SyntaxToken FirstToken(BaseArgumentListSyntax listSyntax) - => listSyntax.GetOpenToken(); + protected override SyntaxToken FirstToken(BaseArgumentListSyntax listSyntax) + => listSyntax.GetOpenToken(); - protected override SyntaxToken LastToken(BaseArgumentListSyntax listSyntax) - => listSyntax.GetCloseToken(); + protected override SyntaxToken LastToken(BaseArgumentListSyntax listSyntax) + => listSyntax.GetCloseToken(); - protected override SeparatedSyntaxList GetListItems(BaseArgumentListSyntax listSyntax) - => listSyntax.Arguments; + protected override SeparatedSyntaxList GetListItems(BaseArgumentListSyntax listSyntax) + => listSyntax.Arguments; - protected override BaseArgumentListSyntax? TryGetApplicableList(SyntaxNode node) - => node switch - { - InvocationExpressionSyntax invocationExpression => invocationExpression.ArgumentList, - ElementAccessExpressionSyntax elementAccessExpression => elementAccessExpression.ArgumentList, - BaseObjectCreationExpressionSyntax objectCreationExpression => objectCreationExpression.ArgumentList, - ConstructorInitializerSyntax constructorInitializer => constructorInitializer.ArgumentList, - _ => null, - }; - - protected override bool PositionIsApplicable( - SyntaxNode root, - int position, - SyntaxNode declaration, - bool containsSyntaxError, - BaseArgumentListSyntax listSyntax) + protected override BaseArgumentListSyntax? TryGetApplicableList(SyntaxNode node) + => node switch { - if (containsSyntaxError) - return false; + InvocationExpressionSyntax invocationExpression => invocationExpression.ArgumentList, + ElementAccessExpressionSyntax elementAccessExpression => elementAccessExpression.ArgumentList, + BaseObjectCreationExpressionSyntax objectCreationExpression => objectCreationExpression.ArgumentList, + ConstructorInitializerSyntax constructorInitializer => constructorInitializer.ArgumentList, + _ => null, + }; + + protected override bool PositionIsApplicable( + SyntaxNode root, + int position, + SyntaxNode declaration, + bool containsSyntaxError, + BaseArgumentListSyntax listSyntax) + { + if (containsSyntaxError) + return false; - var startToken = listSyntax.GetFirstToken(); + var startToken = listSyntax.GetFirstToken(); - if (declaration is InvocationExpressionSyntax or ElementAccessExpressionSyntax) - { - // If we have something like Foo(...) or this.Foo(...) allow anywhere in the Foo(...) - // section. - var expr = (declaration as InvocationExpressionSyntax)?.Expression ?? - ((ElementAccessExpressionSyntax)declaration).Expression; - var name = TryGetInvokedName(expr); + if (declaration is InvocationExpressionSyntax or ElementAccessExpressionSyntax) + { + // If we have something like Foo(...) or this.Foo(...) allow anywhere in the Foo(...) + // section. + var expr = (declaration as InvocationExpressionSyntax)?.Expression ?? + ((ElementAccessExpressionSyntax)declaration).Expression; + var name = TryGetInvokedName(expr); - startToken = name == null ? listSyntax.GetFirstToken() : name.GetFirstToken(); - } - else if (declaration is BaseObjectCreationExpressionSyntax) - { - // allow anywhere in `new Foo(...)` - startToken = declaration.GetFirstToken(); - } - else if (declaration is ConstructorInitializerSyntax constructorInitializer) - { - // allow anywhere in `this(...)` or `base(...)` - startToken = constructorInitializer.ThisOrBaseKeyword; - } + startToken = name == null ? listSyntax.GetFirstToken() : name.GetFirstToken(); + } + else if (declaration is BaseObjectCreationExpressionSyntax) + { + // allow anywhere in `new Foo(...)` + startToken = declaration.GetFirstToken(); + } + else if (declaration is ConstructorInitializerSyntax constructorInitializer) + { + // allow anywhere in `this(...)` or `base(...)` + startToken = constructorInitializer.ThisOrBaseKeyword; + } - var endToken = listSyntax.GetLastToken(); - var span = TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End); - if (!span.IntersectsWith(position)) - return false; + var endToken = listSyntax.GetLastToken(); + var span = TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End); + if (!span.IntersectsWith(position)) + return false; - // allow anywhere in the arg list, as long we don't end up walking through something - // complex like a lambda/anonymous function. - var token = root.FindToken(position); - if (token.GetRequiredParent().Ancestors().Contains(listSyntax)) + // allow anywhere in the arg list, as long we don't end up walking through something + // complex like a lambda/anonymous function. + var token = root.FindToken(position); + if (token.GetRequiredParent().Ancestors().Contains(listSyntax)) + { + for (var current = token.Parent; current != listSyntax; current = current?.Parent) { - for (var current = token.Parent; current != listSyntax; current = current?.Parent) - { - if (CSharpSyntaxFacts.Instance.IsAnonymousFunctionExpression(current)) - return false; - } + if (CSharpSyntaxFacts.Instance.IsAnonymousFunctionExpression(current)) + return false; } - - return true; } - private static ExpressionSyntax? TryGetInvokedName(ExpressionSyntax expr) - { - // `Foo(...)`. Allow up through the 'Foo' portion - if (expr is NameSyntax name) - return name; - - // `this[...]`. Allow up through the 'this' token. - if (expr is ThisExpressionSyntax or BaseExpressionSyntax) - return expr; - - // expr.Foo(...) or expr?.Foo(...) - // All up through the 'Foo' portion. - // - // Otherwise, only allow in the arg list. - return (expr as MemberAccessExpressionSyntax)?.Name ?? - (expr as MemberBindingExpressionSyntax)?.Name; - } + return true; + } + + private static ExpressionSyntax? TryGetInvokedName(ExpressionSyntax expr) + { + // `Foo(...)`. Allow up through the 'Foo' portion + if (expr is NameSyntax name) + return name; + + // `this[...]`. Allow up through the 'this' token. + if (expr is ThisExpressionSyntax or BaseExpressionSyntax) + return expr; + + // expr.Foo(...) or expr?.Foo(...) + // All up through the 'Foo' portion. + // + // Otherwise, only allow in the arg list. + return (expr as MemberAccessExpressionSyntax)?.Name ?? + (expr as MemberBindingExpressionSyntax)?.Name; } } diff --git a/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpInitializerExpressionWrapper.cs b/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpInitializerExpressionWrapper.cs index 34586cc30e8a9..1ca87e5cf30e0 100644 --- a/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpInitializerExpressionWrapper.cs +++ b/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpInitializerExpressionWrapper.cs @@ -6,51 +6,50 @@ using Microsoft.CodeAnalysis.Wrapping; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Wrapping.SeparatedSyntaxList +namespace Microsoft.CodeAnalysis.CSharp.Wrapping.SeparatedSyntaxList; + +internal sealed partial class CSharpInitializerExpressionWrapper + : AbstractCSharpSeparatedSyntaxListWrapper { - internal sealed partial class CSharpInitializerExpressionWrapper - : AbstractCSharpSeparatedSyntaxListWrapper - { - protected override string Indent_all_items => FeaturesResources.Indent_all_elements; - protected override string Unwrap_all_items => FeaturesResources.Unwrap_all_elements; - protected override string Unwrap_list => FeaturesResources.Unwrap_initializer; - protected override string Wrap_every_item => FeaturesResources.Wrap_initializer; - protected override string Wrap_long_list => FeaturesResources.Wrap_long_initializer; + protected override string Indent_all_items => FeaturesResources.Indent_all_elements; + protected override string Unwrap_all_items => FeaturesResources.Unwrap_all_elements; + protected override string Unwrap_list => FeaturesResources.Unwrap_initializer; + protected override string Wrap_every_item => FeaturesResources.Wrap_initializer; + protected override string Wrap_long_list => FeaturesResources.Wrap_long_initializer; - public override bool Supports_UnwrapGroup_WrapFirst_IndentRest => false; - public override bool Supports_WrapEveryGroup_UnwrapFirst => false; - public override bool Supports_WrapLongGroup_UnwrapFirst => false; + public override bool Supports_UnwrapGroup_WrapFirst_IndentRest => false; + public override bool Supports_WrapEveryGroup_UnwrapFirst => false; + public override bool Supports_WrapLongGroup_UnwrapFirst => false; - // unreachable as we explicitly declare that we don't support these scenarios. + // unreachable as we explicitly declare that we don't support these scenarios. - protected override string Align_wrapped_items => throw ExceptionUtilities.Unreachable(); - protected override string Indent_wrapped_items => throw ExceptionUtilities.Unreachable(); - protected override string Unwrap_and_indent_all_items => throw ExceptionUtilities.Unreachable(); + protected override string Align_wrapped_items => throw ExceptionUtilities.Unreachable(); + protected override string Indent_wrapped_items => throw ExceptionUtilities.Unreachable(); + protected override string Unwrap_and_indent_all_items => throw ExceptionUtilities.Unreachable(); - protected override bool ShouldMoveOpenBraceToNewLine(SyntaxWrappingOptions options) - => ((CSharpSyntaxWrappingOptions)options).NewLinesForBracesInObjectCollectionArrayInitializers; + protected override bool ShouldMoveOpenBraceToNewLine(SyntaxWrappingOptions options) + => ((CSharpSyntaxWrappingOptions)options).NewLinesForBracesInObjectCollectionArrayInitializers; - protected override bool ShouldMoveCloseBraceToNewLine - => true; + protected override bool ShouldMoveCloseBraceToNewLine + => true; - protected override SyntaxToken FirstToken(InitializerExpressionSyntax listSyntax) - => listSyntax.OpenBraceToken; + protected override SyntaxToken FirstToken(InitializerExpressionSyntax listSyntax) + => listSyntax.OpenBraceToken; - protected override SyntaxToken LastToken(InitializerExpressionSyntax listSyntax) - => listSyntax.CloseBraceToken; + protected override SyntaxToken LastToken(InitializerExpressionSyntax listSyntax) + => listSyntax.CloseBraceToken; - protected override SeparatedSyntaxList GetListItems(InitializerExpressionSyntax listSyntax) - => listSyntax.Expressions; + protected override SeparatedSyntaxList GetListItems(InitializerExpressionSyntax listSyntax) + => listSyntax.Expressions; - protected override InitializerExpressionSyntax? TryGetApplicableList(SyntaxNode node) - => node as InitializerExpressionSyntax; + protected override InitializerExpressionSyntax? TryGetApplicableList(SyntaxNode node) + => node as InitializerExpressionSyntax; - protected override bool PositionIsApplicable(SyntaxNode root, int position, SyntaxNode declaration, bool containsSyntaxError, InitializerExpressionSyntax listSyntax) - { - if (containsSyntaxError) - return false; + protected override bool PositionIsApplicable(SyntaxNode root, int position, SyntaxNode declaration, bool containsSyntaxError, InitializerExpressionSyntax listSyntax) + { + if (containsSyntaxError) + return false; - return listSyntax.Span.Contains(position); - } + return listSyntax.Span.Contains(position); } } diff --git a/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpParameterWrapper.cs b/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpParameterWrapper.cs index 91bf09b9c952d..ef5fcfa96253b 100644 --- a/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpParameterWrapper.cs +++ b/src/Features/CSharp/Portable/Wrapping/SeparatedSyntaxList/CSharpParameterWrapper.cs @@ -11,69 +11,68 @@ using Microsoft.CodeAnalysis.Wrapping; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CSharp.Wrapping.SeparatedSyntaxList +namespace Microsoft.CodeAnalysis.CSharp.Wrapping.SeparatedSyntaxList; + +internal partial class CSharpParameterWrapper + : AbstractCSharpSeparatedSyntaxListWrapper { - internal partial class CSharpParameterWrapper - : AbstractCSharpSeparatedSyntaxListWrapper - { - protected override string Align_wrapped_items => FeaturesResources.Align_wrapped_parameters; - protected override string Indent_all_items => FeaturesResources.Indent_all_parameters; - protected override string Indent_wrapped_items => FeaturesResources.Indent_wrapped_parameters; - protected override string Unwrap_all_items => FeaturesResources.Unwrap_all_parameters; - protected override string Unwrap_and_indent_all_items => FeaturesResources.Unwrap_and_indent_all_parameters; - protected override string Unwrap_list => FeaturesResources.Unwrap_parameter_list; - protected override string Wrap_every_item => FeaturesResources.Wrap_every_parameter; - protected override string Wrap_long_list => FeaturesResources.Wrap_long_parameter_list; + protected override string Align_wrapped_items => FeaturesResources.Align_wrapped_parameters; + protected override string Indent_all_items => FeaturesResources.Indent_all_parameters; + protected override string Indent_wrapped_items => FeaturesResources.Indent_wrapped_parameters; + protected override string Unwrap_all_items => FeaturesResources.Unwrap_all_parameters; + protected override string Unwrap_and_indent_all_items => FeaturesResources.Unwrap_and_indent_all_parameters; + protected override string Unwrap_list => FeaturesResources.Unwrap_parameter_list; + protected override string Wrap_every_item => FeaturesResources.Wrap_every_parameter; + protected override string Wrap_long_list => FeaturesResources.Wrap_long_parameter_list; - public override bool Supports_UnwrapGroup_WrapFirst_IndentRest => true; - public override bool Supports_WrapEveryGroup_UnwrapFirst => true; - public override bool Supports_WrapLongGroup_UnwrapFirst => true; + public override bool Supports_UnwrapGroup_WrapFirst_IndentRest => true; + public override bool Supports_WrapEveryGroup_UnwrapFirst => true; + public override bool Supports_WrapLongGroup_UnwrapFirst => true; - protected override bool ShouldMoveOpenBraceToNewLine(SyntaxWrappingOptions options) - => false; + protected override bool ShouldMoveOpenBraceToNewLine(SyntaxWrappingOptions options) + => false; - protected override bool ShouldMoveCloseBraceToNewLine - => false; + protected override bool ShouldMoveCloseBraceToNewLine + => false; - protected override SyntaxToken FirstToken(BaseParameterListSyntax listSyntax) - => listSyntax.GetOpenToken(); + protected override SyntaxToken FirstToken(BaseParameterListSyntax listSyntax) + => listSyntax.GetOpenToken(); - protected override SyntaxToken LastToken(BaseParameterListSyntax listSyntax) - => listSyntax.GetCloseToken(); + protected override SyntaxToken LastToken(BaseParameterListSyntax listSyntax) + => listSyntax.GetCloseToken(); - protected override SeparatedSyntaxList GetListItems(BaseParameterListSyntax listSyntax) - => listSyntax.Parameters; + protected override SeparatedSyntaxList GetListItems(BaseParameterListSyntax listSyntax) + => listSyntax.Parameters; - protected override BaseParameterListSyntax? TryGetApplicableList(SyntaxNode node) - => node.GetParameterList(); + protected override BaseParameterListSyntax? TryGetApplicableList(SyntaxNode node) + => node.GetParameterList(); - protected override bool PositionIsApplicable( - SyntaxNode root, int position, SyntaxNode declaration, bool containsSyntaxError, BaseParameterListSyntax listSyntax) - { - // CSharpSyntaxGenerator.GetParameterList synthesizes a parameter list for simple-lambdas. - // In that case, we're not applicable in that list. - if (declaration.Kind() == SyntaxKind.SimpleLambdaExpression) - return false; + protected override bool PositionIsApplicable( + SyntaxNode root, int position, SyntaxNode declaration, bool containsSyntaxError, BaseParameterListSyntax listSyntax) + { + // CSharpSyntaxGenerator.GetParameterList synthesizes a parameter list for simple-lambdas. + // In that case, we're not applicable in that list. + if (declaration.Kind() == SyntaxKind.SimpleLambdaExpression) + return false; - var generator = CSharpSyntaxGenerator.Instance; - var attributes = generator.GetAttributes(declaration); + var generator = CSharpSyntaxGenerator.Instance; + var attributes = generator.GetAttributes(declaration); - // We want to offer this feature in the header of the member. For now, we consider - // the header to be the part after the attributes, to the end of the parameter list. - var firstToken = attributes?.Count > 0 - ? attributes.Last().GetLastToken().GetNextToken() - : declaration.GetFirstToken(); + // We want to offer this feature in the header of the member. For now, we consider + // the header to be the part after the attributes, to the end of the parameter list. + var firstToken = attributes?.Count > 0 + ? attributes.Last().GetLastToken().GetNextToken() + : declaration.GetFirstToken(); - var lastToken = listSyntax.GetLastToken(); + var lastToken = listSyntax.GetLastToken(); - var headerSpan = TextSpan.FromBounds(firstToken.SpanStart, lastToken.Span.End); - if (!headerSpan.IntersectsWith(position)) - return false; + var headerSpan = TextSpan.FromBounds(firstToken.SpanStart, lastToken.Span.End); + if (!headerSpan.IntersectsWith(position)) + return false; - if (containsSyntaxError && ContainsOverlappingSyntaxError(declaration, headerSpan)) - return false; + if (containsSyntaxError && ContainsOverlappingSyntaxError(declaration, headerSpan)) + return false; - return true; - } + return true; } } diff --git a/src/Features/CSharpTest/UseRecursivePatterns/UseRecursivePatternsRefactoringTests.cs b/src/Features/CSharpTest/UseRecursivePatterns/UseRecursivePatternsRefactoringTests.cs index 5eacb7a1a6708..1d57cf4fa0694 100644 --- a/src/Features/CSharpTest/UseRecursivePatterns/UseRecursivePatternsRefactoringTests.cs +++ b/src/Features/CSharpTest/UseRecursivePatterns/UseRecursivePatternsRefactoringTests.cs @@ -124,6 +124,8 @@ public async Task TestLogicalAndExpression_Boolean(string actual, string expecte [Theory] [InlineData("this.P1 == 1 && 2 == this.P2", "this is { P1: 1, P2: 2 }")] + [InlineData("this.cf != null && this.cf.C != 0", "this.cf is { C: not 0 }")] + [InlineData("cf != null && cf.C != 0", "cf is { C: not 0 }")] [InlineData("this.P1 != 1 && 2 != this.P2", "this is { P1: not 1, P2: not 2 }")] [InlineData("this.CP1.P1 == 1 && 2 == this.CP2.P2", "this is { CP1: { P1: 1 }, CP2: { P2: 2 } }")] [InlineData("this.CP1.P1 != 1 && 2 != this.CP2.P2", "this is { CP1: { P1: not 1 }, CP2: { P2: not 2 } }")] @@ -242,6 +244,12 @@ class B public static C SCP1, SCP2; public static int SP1, SP2; public C m() { return null; } + public D cf = null; + } + + class D + { + public int C = 0; } }"; return entry is null ? markup : markup.Replace(entry, "[||]" + entry); diff --git a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.AddConstructorParameterResult.cs b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.AddConstructorParameterResult.cs index c11efae418d8a..f9264b5986113 100644 --- a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.AddConstructorParameterResult.cs +++ b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.AddConstructorParameterResult.cs @@ -4,18 +4,17 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers +namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers; + +internal partial class AddConstructorParametersFromMembersCodeRefactoringProvider { - internal partial class AddConstructorParametersFromMembersCodeRefactoringProvider + private readonly struct AddConstructorParameterResult( + ImmutableArray requiredParameterActions, + ImmutableArray optionalParameterActions, + bool useSubMenu) { - private readonly struct AddConstructorParameterResult( - ImmutableArray requiredParameterActions, - ImmutableArray optionalParameterActions, - bool useSubMenu) - { - internal readonly ImmutableArray RequiredParameterActions = requiredParameterActions; - internal readonly ImmutableArray OptionalParameterActions = optionalParameterActions; - internal readonly bool UseSubMenu = useSubMenu; - } + internal readonly ImmutableArray RequiredParameterActions = requiredParameterActions; + internal readonly ImmutableArray OptionalParameterActions = optionalParameterActions; + internal readonly bool UseSubMenu = useSubMenu; } } diff --git a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.AddConstructorParametersCodeAction.cs b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.AddConstructorParametersCodeAction.cs index 9594f71865356..d10d4e82f76f6 100644 --- a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.AddConstructorParametersCodeAction.cs +++ b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.AddConstructorParametersCodeAction.cs @@ -16,100 +16,99 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers +namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers; + +internal partial class AddConstructorParametersFromMembersCodeRefactoringProvider { - internal partial class AddConstructorParametersFromMembersCodeRefactoringProvider + private sealed class AddConstructorParametersCodeAction( + Document document, + CodeGenerationContextInfo info, + ConstructorCandidate constructorCandidate, + ISymbol containingType, + ImmutableArray missingParameters, + bool useSubMenuName) : CodeAction { - private sealed class AddConstructorParametersCodeAction( - Document document, - CodeGenerationContextInfo info, - ConstructorCandidate constructorCandidate, - ISymbol containingType, - ImmutableArray missingParameters, - bool useSubMenuName) : CodeAction - { - private readonly Document _document = document; - private readonly CodeGenerationContextInfo _info = info; - private readonly ConstructorCandidate _constructorCandidate = constructorCandidate; - private readonly ISymbol _containingType = containingType; - private readonly ImmutableArray _missingParameters = missingParameters; + private readonly Document _document = document; + private readonly CodeGenerationContextInfo _info = info; + private readonly ConstructorCandidate _constructorCandidate = constructorCandidate; + private readonly ISymbol _containingType = containingType; + private readonly ImmutableArray _missingParameters = missingParameters; - /// - /// If there is more than one constructor, the suggested actions will be split into two sub menus, - /// one for regular parameters and one for optional. This boolean is used by the Title property - /// to determine if the code action should be given the complete title or the sub menu title - /// - private readonly bool _useSubMenuName = useSubMenuName; + /// + /// If there is more than one constructor, the suggested actions will be split into two sub menus, + /// one for regular parameters and one for optional. This boolean is used by the Title property + /// to determine if the code action should be given the complete title or the sub menu title + /// + private readonly bool _useSubMenuName = useSubMenuName; - protected override Task GetChangedSolutionAsync( - IProgress progress, CancellationToken cancellationToken) - { - var services = _document.Project.Solution.Services; - var declarationService = _document.GetRequiredLanguageService(); - var constructor = declarationService.GetDeclarations( - _constructorCandidate.Constructor).Select(r => r.GetSyntax(cancellationToken)).First(); + protected override Task GetChangedSolutionAsync( + IProgress progress, CancellationToken cancellationToken) + { + var services = _document.Project.Solution.Services; + var declarationService = _document.GetRequiredLanguageService(); + var constructor = declarationService.GetDeclarations( + _constructorCandidate.Constructor).Select(r => r.GetSyntax(cancellationToken)).First(); - var codeGenerator = _document.GetRequiredLanguageService(); + var codeGenerator = _document.GetRequiredLanguageService(); - var newConstructor = constructor; - newConstructor = codeGenerator.AddParameters(newConstructor, _missingParameters, _info, cancellationToken); - newConstructor = codeGenerator.AddStatements(newConstructor, CreateAssignStatements(_constructorCandidate), _info, cancellationToken) - .WithAdditionalAnnotations(Formatter.Annotation); + var newConstructor = constructor; + newConstructor = codeGenerator.AddParameters(newConstructor, _missingParameters, _info, cancellationToken); + newConstructor = codeGenerator.AddStatements(newConstructor, CreateAssignStatements(_constructorCandidate), _info, cancellationToken) + .WithAdditionalAnnotations(Formatter.Annotation); - var syntaxTree = constructor.SyntaxTree; - var newRoot = syntaxTree.GetRoot(cancellationToken).ReplaceNode(constructor, newConstructor); + var syntaxTree = constructor.SyntaxTree; + var newRoot = syntaxTree.GetRoot(cancellationToken).ReplaceNode(constructor, newConstructor); - // Make sure we get the document that contains the constructor we just updated - var constructorDocument = _document.Project.GetDocument(syntaxTree); - Contract.ThrowIfNull(constructorDocument); + // Make sure we get the document that contains the constructor we just updated + var constructorDocument = _document.Project.GetDocument(syntaxTree); + Contract.ThrowIfNull(constructorDocument); - return Task.FromResult(constructorDocument.WithSyntaxRoot(newRoot).Project.Solution); - } + return Task.FromResult(constructorDocument.WithSyntaxRoot(newRoot).Project.Solution); + } - private IEnumerable CreateAssignStatements(ConstructorCandidate constructorCandidate) + private IEnumerable CreateAssignStatements(ConstructorCandidate constructorCandidate) + { + var factory = _document.GetRequiredLanguageService(); + for (var i = 0; i < _missingParameters.Length; ++i) { - var factory = _document.GetRequiredLanguageService(); - for (var i = 0; i < _missingParameters.Length; ++i) - { - var memberName = constructorCandidate.MissingMembers[i].Name; - var parameterName = _missingParameters[i].Name; - yield return factory.ExpressionStatement( - factory.AssignmentStatement( - factory.MemberAccessExpression(factory.ThisExpression(), factory.IdentifierName(memberName)), - factory.IdentifierName(parameterName))); - } + var memberName = constructorCandidate.MissingMembers[i].Name; + var parameterName = _missingParameters[i].Name; + yield return factory.ExpressionStatement( + factory.AssignmentStatement( + factory.MemberAccessExpression(factory.ThisExpression(), factory.IdentifierName(memberName)), + factory.IdentifierName(parameterName))); } + } - public override string Title + public override string Title + { + get { - get - { - var parameters = _constructorCandidate.Constructor.Parameters.Select(p => p.ToDisplayString(SimpleFormat)); - var parameterString = string.Join(", ", parameters); - var signature = $"{_containingType.Name}({parameterString})"; + var parameters = _constructorCandidate.Constructor.Parameters.Select(p => p.ToDisplayString(SimpleFormat)); + var parameterString = string.Join(", ", parameters); + var signature = $"{_containingType.Name}({parameterString})"; - if (_useSubMenuName) - { - return string.Format(CodeFixesResources.Add_to_0, signature); - } - else - { - return _missingParameters[0].IsOptional - ? string.Format(FeaturesResources.Add_optional_parameters_to_0, signature) - : string.Format(FeaturesResources.Add_parameters_to_0, signature); - } + if (_useSubMenuName) + { + return string.Format(CodeFixesResources.Add_to_0, signature); + } + else + { + return _missingParameters[0].IsOptional + ? string.Format(FeaturesResources.Add_optional_parameters_to_0, signature) + : string.Format(FeaturesResources.Add_parameters_to_0, signature); } } - - /// - /// A metadata name used by telemetry to distinguish between the different kinds of this code action. - /// This code action will perform 2 different actions depending on if missing parameters can be optional. - /// - /// In this case we don't want to use the title as it depends on the class name for the ctor. - /// - internal string ActionName => _missingParameters[0].IsOptional - ? nameof(FeaturesResources.Add_optional_parameters_to_0) - : nameof(FeaturesResources.Add_parameters_to_0); } + + /// + /// A metadata name used by telemetry to distinguish between the different kinds of this code action. + /// This code action will perform 2 different actions depending on if missing parameters can be optional. + /// + /// In this case we don't want to use the title as it depends on the class name for the ctor. + /// + internal string ActionName => _missingParameters[0].IsOptional + ? nameof(FeaturesResources.Add_optional_parameters_to_0) + : nameof(FeaturesResources.Add_parameters_to_0); } } diff --git a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.ConstructorCandidate.cs b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.ConstructorCandidate.cs index dbcf28553aac8..080c0e3fd8c23 100644 --- a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.ConstructorCandidate.cs +++ b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.ConstructorCandidate.cs @@ -4,15 +4,14 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers +namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers; + +internal partial class AddConstructorParametersFromMembersCodeRefactoringProvider { - internal partial class AddConstructorParametersFromMembersCodeRefactoringProvider + private readonly struct ConstructorCandidate(IMethodSymbol constructor, ImmutableArray missingMembers, ImmutableArray missingParameters) { - private readonly struct ConstructorCandidate(IMethodSymbol constructor, ImmutableArray missingMembers, ImmutableArray missingParameters) - { - public readonly IMethodSymbol Constructor = constructor; - public readonly ImmutableArray MissingMembers = missingMembers; - public readonly ImmutableArray MissingParameters = missingParameters; - } + public readonly IMethodSymbol Constructor = constructor; + public readonly ImmutableArray MissingMembers = missingMembers; + public readonly ImmutableArray MissingParameters = missingParameters; } } diff --git a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.State.cs b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.State.cs index 23029c5403f99..0631da4cd583f 100644 --- a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.State.cs +++ b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.State.cs @@ -13,133 +13,132 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers +namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers; + +internal partial class AddConstructorParametersFromMembersCodeRefactoringProvider { - internal partial class AddConstructorParametersFromMembersCodeRefactoringProvider + private class State { - private class State - { - public ImmutableArray ConstructorCandidates { get; private set; } + public ImmutableArray ConstructorCandidates { get; private set; } - [NotNull] - public INamedTypeSymbol? ContainingType { get; private set; } + [NotNull] + public INamedTypeSymbol? ContainingType { get; private set; } - public static async Task GenerateAsync( - ImmutableArray selectedMembers, - Document document, - NamingStylePreferencesProvider fallbackOptions, - CancellationToken cancellationToken) + public static async Task GenerateAsync( + ImmutableArray selectedMembers, + Document document, + NamingStylePreferencesProvider fallbackOptions, + CancellationToken cancellationToken) + { + var state = new State(); + if (!await state.TryInitializeAsync( + selectedMembers, document, fallbackOptions, cancellationToken).ConfigureAwait(false)) { - var state = new State(); - if (!await state.TryInitializeAsync( - selectedMembers, document, fallbackOptions, cancellationToken).ConfigureAwait(false)) - { - return null; - } - - return state; + return null; } - private async Task TryInitializeAsync( - ImmutableArray selectedMembers, - Document document, - NamingStylePreferencesProvider fallbackOptions, - CancellationToken cancellationToken) - { - ContainingType = selectedMembers[0].ContainingType; - - var rules = await document.GetNamingRulesAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var parametersForSelectedMembers = DetermineParameters(selectedMembers, rules); + return state; + } - if (!selectedMembers.All(IsWritableInstanceFieldOrProperty) || - ContainingType == null || - ContainingType.TypeKind == TypeKind.Interface || - parametersForSelectedMembers.IsEmpty) - { - return false; - } + private async Task TryInitializeAsync( + ImmutableArray selectedMembers, + Document document, + NamingStylePreferencesProvider fallbackOptions, + CancellationToken cancellationToken) + { + ContainingType = selectedMembers[0].ContainingType; - ConstructorCandidates = await GetConstructorCandidatesInfoAsync( - ContainingType, selectedMembers, document, parametersForSelectedMembers, cancellationToken).ConfigureAwait(false); + var rules = await document.GetNamingRulesAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var parametersForSelectedMembers = DetermineParameters(selectedMembers, rules); - return !ConstructorCandidates.IsEmpty; + if (!selectedMembers.All(IsWritableInstanceFieldOrProperty) || + ContainingType == null || + ContainingType.TypeKind == TypeKind.Interface || + parametersForSelectedMembers.IsEmpty) + { + return false; } - /// - /// Try to find all constructors in whose parameters - /// are a subset of the selected members by comparing name. - /// These constructors will not be considered as potential candidates: - /// - if the constructor's parameter list contains 'ref' or 'params' - /// - any constructor that has a params[] parameter - /// - deserialization constructor - /// - implicit default constructor - /// - private static async Task> GetConstructorCandidatesInfoAsync( - INamedTypeSymbol containingType, - ImmutableArray selectedMembers, - Document document, - ImmutableArray parametersForSelectedMembers, - CancellationToken cancellationToken) - { - using var _ = ArrayBuilder.GetInstance(out var applicableConstructors); + ConstructorCandidates = await GetConstructorCandidatesInfoAsync( + ContainingType, selectedMembers, document, parametersForSelectedMembers, cancellationToken).ConfigureAwait(false); + + return !ConstructorCandidates.IsEmpty; + } - foreach (var constructor in containingType.InstanceConstructors) + /// + /// Try to find all constructors in whose parameters + /// are a subset of the selected members by comparing name. + /// These constructors will not be considered as potential candidates: + /// - if the constructor's parameter list contains 'ref' or 'params' + /// - any constructor that has a params[] parameter + /// - deserialization constructor + /// - implicit default constructor + /// + private static async Task> GetConstructorCandidatesInfoAsync( + INamedTypeSymbol containingType, + ImmutableArray selectedMembers, + Document document, + ImmutableArray parametersForSelectedMembers, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var applicableConstructors); + + foreach (var constructor in containingType.InstanceConstructors) + { + if (await IsApplicableConstructorAsync( + constructor, document, parametersForSelectedMembers.SelectAsArray(p => p.Name), cancellationToken).ConfigureAwait(false)) { - if (await IsApplicableConstructorAsync( - constructor, document, parametersForSelectedMembers.SelectAsArray(p => p.Name), cancellationToken).ConfigureAwait(false)) - { - applicableConstructors.Add(CreateConstructorCandidate(parametersForSelectedMembers, selectedMembers, constructor)); - } + applicableConstructors.Add(CreateConstructorCandidate(parametersForSelectedMembers, selectedMembers, constructor)); } - - return applicableConstructors.ToImmutable(); } - private static async Task IsApplicableConstructorAsync(IMethodSymbol constructor, Document document, ImmutableArray parameterNamesForSelectedMembers, CancellationToken cancellationToken) - { - var constructorParams = constructor.Parameters; + return applicableConstructors.ToImmutable(); + } - if (constructorParams.Length == 2) + private static async Task IsApplicableConstructorAsync(IMethodSymbol constructor, Document document, ImmutableArray parameterNamesForSelectedMembers, CancellationToken cancellationToken) + { + var constructorParams = constructor.Parameters; + + if (constructorParams.Length == 2) + { + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var deserializationConstructorCheck = new DeserializationConstructorCheck(compilation); + if (deserializationConstructorCheck.IsDeserializationConstructor(constructor)) { - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var deserializationConstructorCheck = new DeserializationConstructorCheck(compilation); - if (deserializationConstructorCheck.IsDeserializationConstructor(constructor)) - { - return false; - } + return false; } - - return constructorParams.All(parameter => parameter.RefKind == RefKind.None) && - !constructor.IsImplicitlyDeclared && - !constructorParams.Any(static p => p.IsParams) && - !SelectedMembersAlreadyExistAsParameters(parameterNamesForSelectedMembers, constructorParams); } - private static bool SelectedMembersAlreadyExistAsParameters(ImmutableArray parameterNamesForSelectedMembers, ImmutableArray constructorParams) - => constructorParams.Length != 0 && - !parameterNamesForSelectedMembers.Except(constructorParams.Select(p => p.Name)).Any(); + return constructorParams.All(parameter => parameter.RefKind == RefKind.None) && + !constructor.IsImplicitlyDeclared && + !constructorParams.Any(static p => p.IsParams) && + !SelectedMembersAlreadyExistAsParameters(parameterNamesForSelectedMembers, constructorParams); + } + + private static bool SelectedMembersAlreadyExistAsParameters(ImmutableArray parameterNamesForSelectedMembers, ImmutableArray constructorParams) + => constructorParams.Length != 0 && + !parameterNamesForSelectedMembers.Except(constructorParams.Select(p => p.Name)).Any(); - private static ConstructorCandidate CreateConstructorCandidate(ImmutableArray parametersForSelectedMembers, ImmutableArray selectedMembers, IMethodSymbol constructor) - { - using var _0 = ArrayBuilder.GetInstance(out var missingParametersBuilder); - using var _1 = ArrayBuilder.GetInstance(out var missingMembersBuilder); + private static ConstructorCandidate CreateConstructorCandidate(ImmutableArray parametersForSelectedMembers, ImmutableArray selectedMembers, IMethodSymbol constructor) + { + using var _0 = ArrayBuilder.GetInstance(out var missingParametersBuilder); + using var _1 = ArrayBuilder.GetInstance(out var missingMembersBuilder); - var constructorParamNames = constructor.Parameters.SelectAsArray(p => p.Name); - var zippedParametersAndSelectedMembers = - parametersForSelectedMembers.Zip(selectedMembers, (parameter, selectedMember) => (parameter, selectedMember)); + var constructorParamNames = constructor.Parameters.SelectAsArray(p => p.Name); + var zippedParametersAndSelectedMembers = + parametersForSelectedMembers.Zip(selectedMembers, (parameter, selectedMember) => (parameter, selectedMember)); - foreach (var (parameter, selectedMember) in zippedParametersAndSelectedMembers) + foreach (var (parameter, selectedMember) in zippedParametersAndSelectedMembers) + { + if (!constructorParamNames.Contains(parameter.Name)) { - if (!constructorParamNames.Contains(parameter.Name)) - { - missingParametersBuilder.Add(parameter); - missingMembersBuilder.Add(selectedMember); - } + missingParametersBuilder.Add(parameter); + missingMembersBuilder.Add(selectedMember); } - - return new ConstructorCandidate( - constructor, missingMembersBuilder.ToImmutable(), missingParametersBuilder.ToImmutable()); } + + return new ConstructorCandidate( + constructor, missingMembersBuilder.ToImmutable(), missingParametersBuilder.ToImmutable()); } } } diff --git a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.cs b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.cs index 00d79c1a11e2b..ef233f3e5f4af 100644 --- a/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/AddConstructorParametersFromMembers/AddConstructorParametersFromMembersCodeRefactoringProvider.cs @@ -21,178 +21,177 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers +namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, + Name = PredefinedCodeRefactoringProviderNames.AddConstructorParametersFromMembers), Shared] +[ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.GenerateConstructorFromMembers, + Before = PredefinedCodeRefactoringProviderNames.GenerateOverrides)] +[IntentProvider(WellKnownIntents.AddConstructorParameter, LanguageNames.CSharp)] +internal partial class AddConstructorParametersFromMembersCodeRefactoringProvider : AbstractGenerateFromMembersCodeRefactoringProvider, IIntentProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, - Name = PredefinedCodeRefactoringProviderNames.AddConstructorParametersFromMembers), Shared] - [ExtensionOrder(After = PredefinedCodeRefactoringProviderNames.GenerateConstructorFromMembers, - Before = PredefinedCodeRefactoringProviderNames.GenerateOverrides)] - [IntentProvider(WellKnownIntents.AddConstructorParameter, LanguageNames.CSharp)] - internal partial class AddConstructorParametersFromMembersCodeRefactoringProvider : AbstractGenerateFromMembersCodeRefactoringProvider, IIntentProvider + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public AddConstructorParametersFromMembersCodeRefactoringProvider() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public AddConstructorParametersFromMembersCodeRefactoringProvider() + } + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, textSpan, cancellationToken) = context; + if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) { + return; } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + var result = await AddConstructorParametersFromMembersAsync(document, textSpan, context.Options, cancellationToken).ConfigureAwait(false); + if (result == null) { - var (document, textSpan, cancellationToken) = context; - if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) - { - return; - } - - var result = await AddConstructorParametersFromMembersAsync(document, textSpan, context.Options, cancellationToken).ConfigureAwait(false); - if (result == null) - { - return; - } - - var actions = GetGroupedActions(result.Value); - context.RegisterRefactorings(actions); + return; } - private static async Task AddConstructorParametersFromMembersAsync( - Document document, TextSpan textSpan, CodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) + var actions = GetGroupedActions(result.Value); + context.RegisterRefactorings(actions); + } + + private static async Task AddConstructorParametersFromMembersAsync( + Document document, TextSpan textSpan, CodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.Refactoring_GenerateFromMembers_AddConstructorParametersFromMembers, cancellationToken)) { - using (Logger.LogBlock(FunctionId.Refactoring_GenerateFromMembers_AddConstructorParametersFromMembers, cancellationToken)) - { - var info = await GetSelectedMemberInfoAsync( - document, - textSpan, - allowPartialSelection: true, - cancellationToken).ConfigureAwait(false); + var info = await GetSelectedMemberInfoAsync( + document, + textSpan, + allowPartialSelection: true, + cancellationToken).ConfigureAwait(false); - if (info != null) + if (info != null) + { + var state = await State.GenerateAsync(info.SelectedMembers, document, fallbackOptions, cancellationToken).ConfigureAwait(false); + if (state?.ConstructorCandidates != null && !state.ConstructorCandidates.IsEmpty) { - var state = await State.GenerateAsync(info.SelectedMembers, document, fallbackOptions, cancellationToken).ConfigureAwait(false); - if (state?.ConstructorCandidates != null && !state.ConstructorCandidates.IsEmpty) - { - var contextInfo = await document.GetCodeGenerationInfoAsync(CodeGenerationContext.Default, fallbackOptions, cancellationToken).ConfigureAwait(false); - return CreateCodeActions(document, contextInfo, state); - } + var contextInfo = await document.GetCodeGenerationInfoAsync(CodeGenerationContext.Default, fallbackOptions, cancellationToken).ConfigureAwait(false); + return CreateCodeActions(document, contextInfo, state); } - - return null; } + + return null; } + } - private static ImmutableArray GetGroupedActions(AddConstructorParameterResult result) + private static ImmutableArray GetGroupedActions(AddConstructorParameterResult result) + { + using var _ = ArrayBuilder.GetInstance(out var actions); + if (result.UseSubMenu) { - using var _ = ArrayBuilder.GetInstance(out var actions); - if (result.UseSubMenu) + if (!result.RequiredParameterActions.IsDefaultOrEmpty) { - if (!result.RequiredParameterActions.IsDefaultOrEmpty) - { - actions.Add(CodeAction.Create( - FeaturesResources.Add_parameter_to_constructor, - result.RequiredParameterActions.Cast(), - isInlinable: false)); - } - actions.Add(CodeAction.Create( - FeaturesResources.Add_optional_parameter_to_constructor, - result.OptionalParameterActions.Cast(), + FeaturesResources.Add_parameter_to_constructor, + result.RequiredParameterActions.Cast(), isInlinable: false)); } - else - { - // Not using submenus, this means we have at most a single of each action. - if (!result.RequiredParameterActions.IsDefaultOrEmpty) - { - actions.Add(result.RequiredParameterActions.Single()); - } - actions.Add(result.OptionalParameterActions.Single()); + actions.Add(CodeAction.Create( + FeaturesResources.Add_optional_parameter_to_constructor, + result.OptionalParameterActions.Cast(), + isInlinable: false)); + } + else + { + // Not using submenus, this means we have at most a single of each action. + if (!result.RequiredParameterActions.IsDefaultOrEmpty) + { + actions.Add(result.RequiredParameterActions.Single()); } - return actions.ToImmutable(); + actions.Add(result.OptionalParameterActions.Single()); } - private static AddConstructorParameterResult CreateCodeActions(Document document, CodeGenerationContextInfo info, State state) - { - using var _0 = ArrayBuilder.GetInstance(out var requiredParametersActions); - using var _1 = ArrayBuilder.GetInstance(out var optionalParametersActions); - var containingType = state.ContainingType; + return actions.ToImmutable(); + } - var useSubMenu = state.ConstructorCandidates.Length > 1; - foreach (var constructorCandidate in state.ConstructorCandidates) - { - if (CanHaveRequiredParameters(constructorCandidate.Constructor.Parameters)) - { - requiredParametersActions.Add(new AddConstructorParametersCodeAction( - document, - info, - constructorCandidate, - containingType, - constructorCandidate.MissingParameters, - useSubMenuName: useSubMenu)); - } + private static AddConstructorParameterResult CreateCodeActions(Document document, CodeGenerationContextInfo info, State state) + { + using var _0 = ArrayBuilder.GetInstance(out var requiredParametersActions); + using var _1 = ArrayBuilder.GetInstance(out var optionalParametersActions); + var containingType = state.ContainingType; - optionalParametersActions.Add(GetOptionalContructorParametersCodeAction( + var useSubMenu = state.ConstructorCandidates.Length > 1; + foreach (var constructorCandidate in state.ConstructorCandidates) + { + if (CanHaveRequiredParameters(constructorCandidate.Constructor.Parameters)) + { + requiredParametersActions.Add(new AddConstructorParametersCodeAction( document, info, constructorCandidate, containingType, + constructorCandidate.MissingParameters, useSubMenuName: useSubMenu)); } - return new AddConstructorParameterResult(requiredParametersActions.ToImmutable(), optionalParametersActions.ToImmutable(), useSubMenu); + optionalParametersActions.Add(GetOptionalContructorParametersCodeAction( + document, + info, + constructorCandidate, + containingType, + useSubMenuName: useSubMenu)); + } - // local functions - static bool CanHaveRequiredParameters(ImmutableArray parameters) - => parameters.Length == 0 || !parameters.Last().IsOptional; + return new AddConstructorParameterResult(requiredParametersActions.ToImmutable(), optionalParametersActions.ToImmutable(), useSubMenu); - static AddConstructorParametersCodeAction GetOptionalContructorParametersCodeAction(Document document, CodeGenerationContextInfo info, ConstructorCandidate constructorCandidate, INamedTypeSymbol containingType, bool useSubMenuName) - { - var missingOptionalParameters = constructorCandidate.MissingParameters.SelectAsArray( - p => CodeGenerationSymbolFactory.CreateParameterSymbol( - attributes: default, - refKind: p.RefKind, - isParams: p.IsParams, - type: p.Type, - name: p.Name, - isOptional: true, - hasDefaultValue: true)); - - return new AddConstructorParametersCodeAction( - document, info, constructorCandidate, containingType, missingOptionalParameters, useSubMenuName); - } - } + // local functions + static bool CanHaveRequiredParameters(ImmutableArray parameters) + => parameters.Length == 0 || !parameters.Last().IsOptional; - public async Task> ComputeIntentAsync( - Document priorDocument, - TextSpan priorSelection, - Document currentDocument, - IntentDataProvider intentDataProvider, - CancellationToken cancellationToken) + static AddConstructorParametersCodeAction GetOptionalContructorParametersCodeAction(Document document, CodeGenerationContextInfo info, ConstructorCandidate constructorCandidate, INamedTypeSymbol containingType, bool useSubMenuName) { - var addConstructorParametersResult = await AddConstructorParametersFromMembersAsync(priorDocument, priorSelection, intentDataProvider.FallbackOptions, cancellationToken).ConfigureAwait(false); - if (addConstructorParametersResult == null) - { - return []; - } + var missingOptionalParameters = constructorCandidate.MissingParameters.SelectAsArray( + p => CodeGenerationSymbolFactory.CreateParameterSymbol( + attributes: default, + refKind: p.RefKind, + isParams: p.IsParams, + type: p.Type, + name: p.Name, + isOptional: true, + hasDefaultValue: true)); + + return new AddConstructorParametersCodeAction( + document, info, constructorCandidate, containingType, missingOptionalParameters, useSubMenuName); + } + } - var actions = addConstructorParametersResult.Value.RequiredParameterActions.Concat(addConstructorParametersResult.Value.OptionalParameterActions); - if (actions.IsEmpty) - { - return []; - } + public async Task> ComputeIntentAsync( + Document priorDocument, + TextSpan priorSelection, + Document currentDocument, + IntentDataProvider intentDataProvider, + CancellationToken cancellationToken) + { + var addConstructorParametersResult = await AddConstructorParametersFromMembersAsync(priorDocument, priorSelection, intentDataProvider.FallbackOptions, cancellationToken).ConfigureAwait(false); + if (addConstructorParametersResult == null) + { + return []; + } - using var _ = ArrayBuilder.GetInstance(out var results); - foreach (var action in actions) - { - // Intents currently have no way to report progress. - var changedSolution = await action.GetChangedSolutionInternalAsync( - priorDocument.Project.Solution, CodeAnalysisProgress.None, postProcessChanges: true, cancellationToken).ConfigureAwait(false); - Contract.ThrowIfNull(changedSolution); - var intent = new IntentProcessorResult(changedSolution, [priorDocument.Id], action.Title, action.ActionName); - results.Add(intent); - } + var actions = addConstructorParametersResult.Value.RequiredParameterActions.Concat(addConstructorParametersResult.Value.OptionalParameterActions); + if (actions.IsEmpty) + { + return []; + } - return results.ToImmutable(); + using var _ = ArrayBuilder.GetInstance(out var results); + foreach (var action in actions) + { + // Intents currently have no way to report progress. + var changedSolution = await action.GetChangedSolutionInternalAsync( + priorDocument.Project.Solution, CodeAnalysisProgress.None, postProcessChanges: true, cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(changedSolution); + var intent = new IntentProcessorResult(changedSolution, [priorDocument.Id], action.Title, action.ActionName); + results.Add(intent); } + + return results.ToImmutable(); } } diff --git a/src/Features/Core/Portable/AddDebuggerDisplay/AbstractAddDebuggerDisplayCodeRefactoringProvider.cs b/src/Features/Core/Portable/AddDebuggerDisplay/AbstractAddDebuggerDisplayCodeRefactoringProvider.cs index ab965154a38c7..e6839321d88fc 100644 --- a/src/Features/Core/Portable/AddDebuggerDisplay/AbstractAddDebuggerDisplayCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/AddDebuggerDisplay/AbstractAddDebuggerDisplayCodeRefactoringProvider.cs @@ -12,169 +12,168 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Simplification; -namespace Microsoft.CodeAnalysis.AddDebuggerDisplay +namespace Microsoft.CodeAnalysis.AddDebuggerDisplay; + +internal abstract class AbstractAddDebuggerDisplayCodeRefactoringProvider< + TTypeDeclarationSyntax, + TMethodDeclarationSyntax> : CodeRefactoringProvider + where TTypeDeclarationSyntax : SyntaxNode + where TMethodDeclarationSyntax : SyntaxNode { - internal abstract class AbstractAddDebuggerDisplayCodeRefactoringProvider< - TTypeDeclarationSyntax, - TMethodDeclarationSyntax> : CodeRefactoringProvider - where TTypeDeclarationSyntax : SyntaxNode - where TMethodDeclarationSyntax : SyntaxNode - { - private const string DebuggerDisplayPrefix = "{"; - private const string DebuggerDisplayMethodName = "GetDebuggerDisplay"; - private const string DebuggerDisplaySuffix = "(),nq}"; + private const string DebuggerDisplayPrefix = "{"; + private const string DebuggerDisplayMethodName = "GetDebuggerDisplay"; + private const string DebuggerDisplaySuffix = "(),nq}"; - protected abstract bool CanNameofAccessNonPublicMembersFromAttributeArgument { get; } + protected abstract bool CanNameofAccessNonPublicMembersFromAttributeArgument { get; } - protected abstract bool SupportsConstantInterpolatedStrings(Document document); + protected abstract bool SupportsConstantInterpolatedStrings(Document document); - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, _, cancellationToken) = context; + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, _, cancellationToken) = context; - var typeAndPriority = - await GetRelevantTypeFromHeaderAsync(context).ConfigureAwait(false) ?? - await GetRelevantTypeFromMethodAsync(context).ConfigureAwait(false); + var typeAndPriority = + await GetRelevantTypeFromHeaderAsync(context).ConfigureAwait(false) ?? + await GetRelevantTypeFromMethodAsync(context).ConfigureAwait(false); - if (typeAndPriority == null) - return; + if (typeAndPriority == null) + return; - var (type, priority) = typeAndPriority.Value; + var (type, priority) = typeAndPriority.Value; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var compilation = semanticModel.Compilation; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var compilation = semanticModel.Compilation; - var debuggerAttributeTypeSymbol = compilation.GetTypeByMetadataName("System.Diagnostics.DebuggerDisplayAttribute"); - if (debuggerAttributeTypeSymbol is null) - return; + var debuggerAttributeTypeSymbol = compilation.GetTypeByMetadataName("System.Diagnostics.DebuggerDisplayAttribute"); + if (debuggerAttributeTypeSymbol is null) + return; - var typeSymbol = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(type, cancellationToken); + var typeSymbol = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(type, cancellationToken); - if (typeSymbol.IsStatic || !IsClassOrStruct(typeSymbol)) - return; + if (typeSymbol.IsStatic || !IsClassOrStruct(typeSymbol)) + return; - if (HasDebuggerDisplayAttribute(typeSymbol, compilation)) - return; + if (HasDebuggerDisplayAttribute(typeSymbol, compilation)) + return; - context.RegisterRefactoring(CodeAction.Create( - FeaturesResources.Add_DebuggerDisplay_attribute, - c => ApplyAsync(document, type, debuggerAttributeTypeSymbol, c), - nameof(FeaturesResources.Add_DebuggerDisplay_attribute), - priority)); - } + context.RegisterRefactoring(CodeAction.Create( + FeaturesResources.Add_DebuggerDisplay_attribute, + c => ApplyAsync(document, type, debuggerAttributeTypeSymbol, c), + nameof(FeaturesResources.Add_DebuggerDisplay_attribute), + priority)); + } - private static async Task<(TTypeDeclarationSyntax type, CodeActionPriority priority)?> GetRelevantTypeFromHeaderAsync(CodeRefactoringContext context) - { - var type = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (type is null) - return null; + private static async Task<(TTypeDeclarationSyntax type, CodeActionPriority priority)?> GetRelevantTypeFromHeaderAsync(CodeRefactoringContext context) + { + var type = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (type is null) + return null; - return (type, CodeActionPriority.Low); - } + return (type, CodeActionPriority.Low); + } - private static async Task<(TTypeDeclarationSyntax type, CodeActionPriority priority)?> GetRelevantTypeFromMethodAsync(CodeRefactoringContext context) - { - var (document, _, cancellationToken) = context; - var method = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (method == null) - return null; - - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var methodSymbol = (IMethodSymbol)semanticModel.GetRequiredDeclaredSymbol(method, cancellationToken); - - var isDebuggerDisplayMethod = IsDebuggerDisplayMethod(methodSymbol); - if (!isDebuggerDisplayMethod && !IsToStringMethod(methodSymbol)) - return null; - - // Show the feature if we're on a ToString or GetDebuggerDisplay method. For the former, - // have this be low-pri so we don't override more important light-bulb options. - var typeDecl = method.FirstAncestorOrSelf(); - if (typeDecl == null) - return null; - - var priority = isDebuggerDisplayMethod ? CodeActionPriority.Default : CodeActionPriority.Low; - return (typeDecl, priority); - } + private static async Task<(TTypeDeclarationSyntax type, CodeActionPriority priority)?> GetRelevantTypeFromMethodAsync(CodeRefactoringContext context) + { + var (document, _, cancellationToken) = context; + var method = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (method == null) + return null; + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var methodSymbol = (IMethodSymbol)semanticModel.GetRequiredDeclaredSymbol(method, cancellationToken); + + var isDebuggerDisplayMethod = IsDebuggerDisplayMethod(methodSymbol); + if (!isDebuggerDisplayMethod && !IsToStringMethod(methodSymbol)) + return null; + + // Show the feature if we're on a ToString or GetDebuggerDisplay method. For the former, + // have this be low-pri so we don't override more important light-bulb options. + var typeDecl = method.FirstAncestorOrSelf(); + if (typeDecl == null) + return null; + + var priority = isDebuggerDisplayMethod ? CodeActionPriority.Default : CodeActionPriority.Low; + return (typeDecl, priority); + } - private static bool IsToStringMethod(IMethodSymbol methodSymbol) - => methodSymbol is { Name: nameof(ToString), Arity: 0, Parameters.IsEmpty: true }; + private static bool IsToStringMethod(IMethodSymbol methodSymbol) + => methodSymbol is { Name: nameof(ToString), Arity: 0, Parameters.IsEmpty: true }; - private static bool IsDebuggerDisplayMethod(IMethodSymbol methodSymbol) - => methodSymbol is { Name: DebuggerDisplayMethodName, Arity: 0, Parameters.IsEmpty: true }; + private static bool IsDebuggerDisplayMethod(IMethodSymbol methodSymbol) + => methodSymbol is { Name: DebuggerDisplayMethodName, Arity: 0, Parameters.IsEmpty: true }; - private static bool IsClassOrStruct(ITypeSymbol typeSymbol) - => typeSymbol.TypeKind is TypeKind.Class or TypeKind.Struct; + private static bool IsClassOrStruct(ITypeSymbol typeSymbol) + => typeSymbol.TypeKind is TypeKind.Class or TypeKind.Struct; - private static bool HasDebuggerDisplayAttribute(ITypeSymbol typeSymbol, Compilation compilation) - => typeSymbol.GetAttributes() - .Select(data => data.AttributeClass) - .Contains(compilation.GetTypeByMetadataName("System.Diagnostics.DebuggerDisplayAttribute")); + private static bool HasDebuggerDisplayAttribute(ITypeSymbol typeSymbol, Compilation compilation) + => typeSymbol.GetAttributes() + .Select(data => data.AttributeClass) + .Contains(compilation.GetTypeByMetadataName("System.Diagnostics.DebuggerDisplayAttribute")); - private async Task ApplyAsync(Document document, TTypeDeclarationSyntax type, INamedTypeSymbol debuggerAttributeTypeSymbol, CancellationToken cancellationToken) - { - var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + private async Task ApplyAsync(Document document, TTypeDeclarationSyntax type, INamedTypeSymbol debuggerAttributeTypeSymbol, CancellationToken cancellationToken) + { + var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var editor = new SyntaxEditor(syntaxRoot, document.Project.Solution.Services); - var generator = editor.Generator; + var editor = new SyntaxEditor(syntaxRoot, document.Project.Solution.Services); + var generator = editor.Generator; - SyntaxNode attributeArgument; - if (CanNameofAccessNonPublicMembersFromAttributeArgument) + SyntaxNode attributeArgument; + if (CanNameofAccessNonPublicMembersFromAttributeArgument) + { + if (SupportsConstantInterpolatedStrings(document)) { - if (SupportsConstantInterpolatedStrings(document)) - { - attributeArgument = generator.InterpolatedStringExpression( - generator.CreateInterpolatedStringStartToken(isVerbatim: false), - new SyntaxNode[] - { - generator.InterpolatedStringText(generator.InterpolatedStringTextToken("{{", "{{")), - generator.Interpolation(generator.NameOfExpression(generator.IdentifierName(DebuggerDisplayMethodName))), - generator.InterpolatedStringText(generator.InterpolatedStringTextToken("(),nq}}", "(),nq}}")), - }, - generator.CreateInterpolatedStringEndToken()); - } - else - { - attributeArgument = generator.AddExpression( - generator.AddExpression( - generator.LiteralExpression(DebuggerDisplayPrefix), - generator.NameOfExpression(generator.IdentifierName(DebuggerDisplayMethodName))), - generator.LiteralExpression(DebuggerDisplaySuffix)); - } + attributeArgument = generator.InterpolatedStringExpression( + generator.CreateInterpolatedStringStartToken(isVerbatim: false), + new SyntaxNode[] + { + generator.InterpolatedStringText(generator.InterpolatedStringTextToken("{{", "{{")), + generator.Interpolation(generator.NameOfExpression(generator.IdentifierName(DebuggerDisplayMethodName))), + generator.InterpolatedStringText(generator.InterpolatedStringTextToken("(),nq}}", "(),nq}}")), + }, + generator.CreateInterpolatedStringEndToken()); } else { - attributeArgument = generator.LiteralExpression( - DebuggerDisplayPrefix + DebuggerDisplayMethodName + DebuggerDisplaySuffix); + attributeArgument = generator.AddExpression( + generator.AddExpression( + generator.LiteralExpression(DebuggerDisplayPrefix), + generator.NameOfExpression(generator.IdentifierName(DebuggerDisplayMethodName))), + generator.LiteralExpression(DebuggerDisplaySuffix)); } + } + else + { + attributeArgument = generator.LiteralExpression( + DebuggerDisplayPrefix + DebuggerDisplayMethodName + DebuggerDisplaySuffix); + } - var newAttribute = generator - .Attribute(generator.TypeExpression(debuggerAttributeTypeSymbol), [attributeArgument]) - .WithAdditionalAnnotations( - Simplifier.Annotation, - Simplifier.AddImportsAnnotation); - - editor.AddAttribute(type, newAttribute); + var newAttribute = generator + .Attribute(generator.TypeExpression(debuggerAttributeTypeSymbol), [attributeArgument]) + .WithAdditionalAnnotations( + Simplifier.Annotation, + Simplifier.AddImportsAnnotation); - var typeSymbol = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(type, cancellationToken); + editor.AddAttribute(type, newAttribute); - if (!typeSymbol.GetMembers().OfType().Any(IsDebuggerDisplayMethod)) - { - editor.AddMember(type, - generator.MethodDeclaration( - DebuggerDisplayMethodName, - returnType: generator.TypeExpression(SpecialType.System_String), - accessibility: Accessibility.Private, - statements: - [ - generator.ReturnStatement(generator.InvocationExpression( - generator.MemberAccessExpression( - generator.ThisExpression(), - generator.IdentifierName("ToString")))) - ])); - } + var typeSymbol = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(type, cancellationToken); - return document.WithSyntaxRoot(editor.GetChangedRoot()); + if (!typeSymbol.GetMembers().OfType().Any(IsDebuggerDisplayMethod)) + { + editor.AddMember(type, + generator.MethodDeclaration( + DebuggerDisplayMethodName, + returnType: generator.TypeExpression(SpecialType.System_String), + accessibility: Accessibility.Private, + statements: + [ + generator.ReturnStatement(generator.InvocationExpression( + generator.MemberAccessExpression( + generator.ThisExpression(), + generator.IdentifierName("ToString")))) + ])); } + + return document.WithSyntaxRoot(editor.GetChangedRoot()); } } diff --git a/src/Features/Core/Portable/AddFileBanner/AbstractAddFileBannerCodeRefactoringProvider.cs b/src/Features/Core/Portable/AddFileBanner/AbstractAddFileBannerCodeRefactoringProvider.cs index 5351bdeef2c58..c49e2d8fcdf05 100644 --- a/src/Features/Core/Portable/AddFileBanner/AbstractAddFileBannerCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/AddFileBanner/AbstractAddFileBannerCodeRefactoringProvider.cs @@ -22,194 +22,193 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddFileBanner +namespace Microsoft.CodeAnalysis.AddFileBanner; + +internal abstract class AbstractAddFileBannerCodeRefactoringProvider : SyntaxEditorBasedCodeRefactoringProvider { - internal abstract class AbstractAddFileBannerCodeRefactoringProvider : SyntaxEditorBasedCodeRefactoringProvider - { - private const string BannerFileNamePlaceholder = "{filename}"; + private const string BannerFileNamePlaceholder = "{filename}"; - protected abstract bool IsCommentStartCharacter(char ch); + protected abstract bool IsCommentStartCharacter(char ch); - protected abstract SyntaxTrivia CreateTrivia(SyntaxTrivia trivia, string text); + protected abstract SyntaxTrivia CreateTrivia(SyntaxTrivia trivia, string text); - protected sealed override ImmutableArray SupportedFixAllScopes { get; } - = [FixAllScope.Project, FixAllScope.Solution]; + protected sealed override ImmutableArray SupportedFixAllScopes { get; } + = [FixAllScope.Project, FixAllScope.Solution]; - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, span, cancellationToken) = context; + if (!span.IsEmpty) { - var (document, span, cancellationToken) = context; - if (!span.IsEmpty) - { - return; - } - - var formattingOptions = await document.GetDocumentFormattingOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrEmpty(formattingOptions.FileHeaderTemplate)) - { - // If we have a defined file header template, allow the analyzer and code fix to handle it - return; - } - - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); + return; + } - var position = span.Start; - var firstToken = root.GetFirstToken(); - if (!firstToken.FullSpan.IntersectsWith(position)) - { - return; - } + var formattingOptions = await document.GetDocumentFormattingOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); + if (!string.IsNullOrEmpty(formattingOptions.FileHeaderTemplate)) + { + // If we have a defined file header template, allow the analyzer and code fix to handle it + return; + } - if (HasExistingBanner(document, root)) - { - // Already has a banner. - return; - } + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - // Process the other documents in this document's project. Look at the - // ones that we can get a root from (without having to parse). Then - // look at the ones we'd need to parse. - var siblingDocumentsAndRoots = - document.Project.Documents - .Where(d => d != document) - .Select(d => - { - d.TryGetSyntaxRoot(out var siblingRoot); - return (document: d, root: siblingRoot); - }) - .OrderBy((t1, t2) => (t1.root != null) == (t2.root != null) ? 0 : t1.root != null ? -1 : 1); - - foreach (var (siblingDocument, siblingRoot) in siblingDocumentsAndRoots) - { - cancellationToken.ThrowIfCancellationRequested(); - - var siblingBanner = await TryGetBannerAsync(siblingDocument, siblingRoot, cancellationToken).ConfigureAwait(false); - if (siblingBanner.Length > 0 && !siblingDocument.IsGeneratedCode(cancellationToken)) - { - context.RegisterRefactoring( - CodeAction.Create( - CodeFixesResources.Add_file_header, - _ => AddBannerAsync(document, root, siblingDocument, siblingBanner), - equivalenceKey: GetEquivalenceKey(siblingDocument, siblingBanner)), - new Text.TextSpan(position, length: 0)); - return; - } - } + var position = span.Start; + var firstToken = root.GetFirstToken(); + if (!firstToken.FullSpan.IntersectsWith(position)) + { + return; } - private static bool HasExistingBanner(Document document, SyntaxNode root) + if (HasExistingBanner(document, root)) { - var bannerService = document.GetRequiredLanguageService(); - var banner = bannerService.GetFileBanner(root); - return banner.Length > 0; + // Already has a banner. + return; } - private static string GetEquivalenceKey(Document document, ImmutableArray banner) + // Process the other documents in this document's project. Look at the + // ones that we can get a root from (without having to parse). Then + // look at the ones we'd need to parse. + var siblingDocumentsAndRoots = + document.Project.Documents + .Where(d => d != document) + .Select(d => + { + d.TryGetSyntaxRoot(out var siblingRoot); + return (document: d, root: siblingRoot); + }) + .OrderBy((t1, t2) => (t1.root != null) == (t2.root != null) ? 0 : t1.root != null ? -1 : 1); + + foreach (var (siblingDocument, siblingRoot) in siblingDocumentsAndRoots) { - var bannerText = banner.Select(trivia => trivia.ToFullString()).Join(string.Empty); - - var fileName = IOUtilities.PerformIO(() => Path.GetFileName(document.FilePath)); - if (!string.IsNullOrEmpty(fileName)) - bannerText = bannerText.Replace(fileName, BannerFileNamePlaceholder); + cancellationToken.ThrowIfCancellationRequested(); - return bannerText; + var siblingBanner = await TryGetBannerAsync(siblingDocument, siblingRoot, cancellationToken).ConfigureAwait(false); + if (siblingBanner.Length > 0 && !siblingDocument.IsGeneratedCode(cancellationToken)) + { + context.RegisterRefactoring( + CodeAction.Create( + CodeFixesResources.Add_file_header, + _ => AddBannerAsync(document, root, siblingDocument, siblingBanner), + equivalenceKey: GetEquivalenceKey(siblingDocument, siblingBanner)), + new Text.TextSpan(position, length: 0)); + return; + } } + } - private static ImmutableArray GetBannerFromEquivalenceKey(string equivalenceKey, Document document) - { - var fileName = IOUtilities.PerformIO(() => Path.GetFileName(document.FilePath)); - if (!string.IsNullOrEmpty(fileName)) - equivalenceKey = equivalenceKey.Replace(BannerFileNamePlaceholder, fileName); + private static bool HasExistingBanner(Document document, SyntaxNode root) + { + var bannerService = document.GetRequiredLanguageService(); + var banner = bannerService.GetFileBanner(root); + return banner.Length > 0; + } + + private static string GetEquivalenceKey(Document document, ImmutableArray banner) + { + var bannerText = banner.Select(trivia => trivia.ToFullString()).Join(string.Empty); - var syntaxFacts = document.GetRequiredLanguageService(); - var token = syntaxFacts.ParseToken(equivalenceKey); + var fileName = IOUtilities.PerformIO(() => Path.GetFileName(document.FilePath)); + if (!string.IsNullOrEmpty(fileName)) + bannerText = bannerText.Replace(fileName, BannerFileNamePlaceholder); - var bannerService = document.GetRequiredLanguageService(); - return bannerService.GetFileBanner(token); - } + return bannerText; + } - private Task AddBannerAsync( - Document document, SyntaxNode root, - Document siblingDocument, ImmutableArray banner) - { - banner = UpdateEmbeddedFileNames(siblingDocument, document, banner); + private static ImmutableArray GetBannerFromEquivalenceKey(string equivalenceKey, Document document) + { + var fileName = IOUtilities.PerformIO(() => Path.GetFileName(document.FilePath)); + if (!string.IsNullOrEmpty(fileName)) + equivalenceKey = equivalenceKey.Replace(BannerFileNamePlaceholder, fileName); - var newRoot = root.WithPrependedLeadingTrivia(new SyntaxTriviaList(banner)); - return Task.FromResult(document.WithSyntaxRoot(newRoot)); - } + var syntaxFacts = document.GetRequiredLanguageService(); + var token = syntaxFacts.ParseToken(equivalenceKey); - /// - /// Looks at to see if it contains the name of - /// in it. If so, those names will be replaced with 's name. - /// - private ImmutableArray UpdateEmbeddedFileNames( - Document sourceDocument, Document destinationDocument, ImmutableArray banner) - { - var sourceName = IOUtilities.PerformIO(() => Path.GetFileName(sourceDocument.FilePath)); - var destinationName = IOUtilities.PerformIO(() => Path.GetFileName(destinationDocument.FilePath)); - if (string.IsNullOrEmpty(sourceName) || string.IsNullOrEmpty(destinationName)) - { - return banner; - } + var bannerService = document.GetRequiredLanguageService(); + return bannerService.GetFileBanner(token); + } - using var _ = ArrayBuilder.GetInstance(out var result); - foreach (var trivia in banner) - { - var updated = CreateTrivia(trivia, trivia.ToFullString().Replace(sourceName, destinationName)); - result.Add(updated); - } + private Task AddBannerAsync( + Document document, SyntaxNode root, + Document siblingDocument, ImmutableArray banner) + { + banner = UpdateEmbeddedFileNames(siblingDocument, document, banner); + + var newRoot = root.WithPrependedLeadingTrivia(new SyntaxTriviaList(banner)); + return Task.FromResult(document.WithSyntaxRoot(newRoot)); + } - return result.ToImmutable(); + /// + /// Looks at to see if it contains the name of + /// in it. If so, those names will be replaced with 's name. + /// + private ImmutableArray UpdateEmbeddedFileNames( + Document sourceDocument, Document destinationDocument, ImmutableArray banner) + { + var sourceName = IOUtilities.PerformIO(() => Path.GetFileName(sourceDocument.FilePath)); + var destinationName = IOUtilities.PerformIO(() => Path.GetFileName(destinationDocument.FilePath)); + if (string.IsNullOrEmpty(sourceName) || string.IsNullOrEmpty(destinationName)) + { + return banner; } - private async Task> TryGetBannerAsync( - Document document, SyntaxNode? root, CancellationToken cancellationToken) + using var _ = ArrayBuilder.GetInstance(out var result); + foreach (var trivia in banner) { - var bannerService = document.GetRequiredLanguageService(); - var syntaxFacts = document.GetRequiredLanguageService(); + var updated = CreateTrivia(trivia, trivia.ToFullString().Replace(sourceName, destinationName)); + result.Add(updated); + } - // If we have a tree already for this document, then just check to see - // if it has a banner. - if (root != null) - { - return bannerService.GetFileBanner(root); - } + return result.ToImmutable(); + } - // Didn't have a tree. Don't want to parse the file if we can avoid it. - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (text.Length == 0 || !IsCommentStartCharacter(text[0])) - { - // Didn't start with a comment character, don't bother looking at - // this file. - return []; - } + private async Task> TryGetBannerAsync( + Document document, SyntaxNode? root, CancellationToken cancellationToken) + { + var bannerService = document.GetRequiredLanguageService(); + var syntaxFacts = document.GetRequiredLanguageService(); - var token = syntaxFacts.ParseToken(text.ToString()); - return bannerService.GetFileBanner(token); + // If we have a tree already for this document, then just check to see + // if it has a banner. + if (root != null) + { + return bannerService.GetFileBanner(root); } - protected sealed override async Task FixAllAsync( - Document document, - ImmutableArray fixAllSpans, - SyntaxEditor editor, - CodeActionOptionsProvider optionsProvider, - string? equivalenceKey, - CancellationToken cancellationToken) + // Didn't have a tree. Don't want to parse the file if we can avoid it. + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (text.Length == 0 || !IsCommentStartCharacter(text[0])) { - Debug.Assert(equivalenceKey != null); + // Didn't start with a comment character, don't bother looking at + // this file. + return []; + } - // Bail out if the document to fix already has an existing banner. - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (HasExistingBanner(document, root)) - return; + var token = syntaxFacts.ParseToken(text.ToString()); + return bannerService.GetFileBanner(token); + } - // Get banner from the equivalence key. - var banner = GetBannerFromEquivalenceKey(equivalenceKey, document); - Debug.Assert(banner.Length > 0); + protected sealed override async Task FixAllAsync( + Document document, + ImmutableArray fixAllSpans, + SyntaxEditor editor, + CodeActionOptionsProvider optionsProvider, + string? equivalenceKey, + CancellationToken cancellationToken) + { + Debug.Assert(equivalenceKey != null); - // Finally add the banner to the document to be fixed. - var newRoot = root.WithPrependedLeadingTrivia(new SyntaxTriviaList(banner)); - editor.ReplaceNode(editor.OriginalRoot, newRoot); - } + // Bail out if the document to fix already has an existing banner. + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (HasExistingBanner(document, root)) + return; + + // Get banner from the equivalence key. + var banner = GetBannerFromEquivalenceKey(equivalenceKey, document); + Debug.Assert(banner.Length > 0); + + // Finally add the banner to the document to be fixed. + var newRoot = root.WithPrependedLeadingTrivia(new SyntaxTriviaList(banner)); + editor.ReplaceNode(editor.OriginalRoot, newRoot); } } diff --git a/src/Features/Core/Portable/AddFileBanner/AbstractAddFileBannerNewDocumentFormattingProvider.cs b/src/Features/Core/Portable/AddFileBanner/AbstractAddFileBannerNewDocumentFormattingProvider.cs index 5780caf049b2f..ef2f66fa7a6d5 100644 --- a/src/Features/Core/Portable/AddFileBanner/AbstractAddFileBannerNewDocumentFormattingProvider.cs +++ b/src/Features/Core/Portable/AddFileBanner/AbstractAddFileBannerNewDocumentFormattingProvider.cs @@ -12,45 +12,44 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.AddFileBanner +namespace Microsoft.CodeAnalysis.AddFileBanner; + +internal abstract class AbstractAddFileBannerNewDocumentFormattingProvider : INewDocumentFormattingProvider { - internal abstract class AbstractAddFileBannerNewDocumentFormattingProvider : INewDocumentFormattingProvider + protected abstract SyntaxGenerator SyntaxGenerator { get; } + protected abstract SyntaxGeneratorInternal SyntaxGeneratorInternal { get; } + protected abstract AbstractFileHeaderHelper FileHeaderHelper { get; } + + public async Task FormatNewDocumentAsync(Document document, Document? hintDocument, CodeCleanupOptions options, CancellationToken cancellationToken) { - protected abstract SyntaxGenerator SyntaxGenerator { get; } - protected abstract SyntaxGeneratorInternal SyntaxGeneratorInternal { get; } - protected abstract AbstractFileHeaderHelper FileHeaderHelper { get; } + var rootToFormat = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - public async Task FormatNewDocumentAsync(Document document, Document? hintDocument, CodeCleanupOptions options, CancellationToken cancellationToken) + // Apply file header preferences + var fileHeaderTemplate = options.DocumentFormattingOptions.FileHeaderTemplate; + if (!string.IsNullOrEmpty(fileHeaderTemplate)) { - var rootToFormat = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - // Apply file header preferences - var fileHeaderTemplate = options.DocumentFormattingOptions.FileHeaderTemplate; - if (!string.IsNullOrEmpty(fileHeaderTemplate)) - { - var newLineTrivia = SyntaxGeneratorInternal.EndOfLine(options.FormattingOptions.NewLine); - var rootWithFileHeader = await AbstractFileHeaderCodeFixProvider.GetTransformedSyntaxRootAsync( - SyntaxGenerator.SyntaxFacts, - FileHeaderHelper, - newLineTrivia, - document, - fileHeaderTemplate, - cancellationToken).ConfigureAwait(false); - - return document.WithSyntaxRoot(rootWithFileHeader); - } - else if (hintDocument is not null) - { - // If there is no file header preference, see if we can use the one in the hint document - var bannerService = hintDocument.GetRequiredLanguageService(); - var hintSyntaxRoot = await hintDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var fileBanner = bannerService.GetFileBanner(hintSyntaxRoot); - - var rootWithBanner = rootToFormat.WithPrependedLeadingTrivia(fileBanner); - return document.WithSyntaxRoot(rootWithBanner); - } - - return document; + var newLineTrivia = SyntaxGeneratorInternal.EndOfLine(options.FormattingOptions.NewLine); + var rootWithFileHeader = await AbstractFileHeaderCodeFixProvider.GetTransformedSyntaxRootAsync( + SyntaxGenerator.SyntaxFacts, + FileHeaderHelper, + newLineTrivia, + document, + fileHeaderTemplate, + cancellationToken).ConfigureAwait(false); + + return document.WithSyntaxRoot(rootWithFileHeader); } + else if (hintDocument is not null) + { + // If there is no file header preference, see if we can use the one in the hint document + var bannerService = hintDocument.GetRequiredLanguageService(); + var hintSyntaxRoot = await hintDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var fileBanner = bannerService.GetFileBanner(hintSyntaxRoot); + + var rootWithBanner = rootToFormat.WithPrependedLeadingTrivia(fileBanner); + return document.WithSyntaxRoot(rootWithBanner); + } + + return document; } } diff --git a/src/Features/Core/Portable/AddImport/AbstractAddImportCodeFixProvider.cs b/src/Features/Core/Portable/AddImport/AbstractAddImportCodeFixProvider.cs index 261fba430225f..d904622eeaf36 100644 --- a/src/Features/Core/Portable/AddImport/AbstractAddImportCodeFixProvider.cs +++ b/src/Features/Core/Portable/AddImport/AbstractAddImportCodeFixProvider.cs @@ -11,87 +11,86 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.SymbolSearch; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportCodeFixProvider : CodeFixProvider { - internal abstract partial class AbstractAddImportCodeFixProvider : CodeFixProvider - { - private const int MaxResults = 5; + private const int MaxResults = 5; - private readonly IPackageInstallerService? _packageInstallerService; - private readonly ISymbolSearchService? _symbolSearchService; + private readonly IPackageInstallerService? _packageInstallerService; + private readonly ISymbolSearchService? _symbolSearchService; - /// - /// Values for these parameters can be provided (during testing) for mocking purposes. - /// - protected AbstractAddImportCodeFixProvider( - IPackageInstallerService? packageInstallerService = null, - ISymbolSearchService? symbolSearchService = null) - { - _packageInstallerService = packageInstallerService; - _symbolSearchService = symbolSearchService; + /// + /// Values for these parameters can be provided (during testing) for mocking purposes. + /// + protected AbstractAddImportCodeFixProvider( + IPackageInstallerService? packageInstallerService = null, + ISymbolSearchService? symbolSearchService = null) + { + _packageInstallerService = packageInstallerService; + _symbolSearchService = symbolSearchService; - // Backdoor that allows this provider to use the high-priority bucket. - this.CustomTags = this.CustomTags.Add(CodeAction.CanBeHighPriorityTag); - } + // Backdoor that allows this provider to use the high-priority bucket. + this.CustomTags = this.CustomTags.Add(CodeAction.CanBeHighPriorityTag); + } - /// - /// Add-using gets special privileges as being the most used code-action, along with being a core - /// 'smart tag' feature in VS prior to us even having 'light bulbs'. We want them to be computed - /// first, ahead of everything else, and the main results should show up at the top of the list. - /// - protected override CodeActionRequestPriority ComputeRequestPriority() - => CodeActionRequestPriority.High; + /// + /// Add-using gets special privileges as being the most used code-action, along with being a core + /// 'smart tag' feature in VS prior to us even having 'light bulbs'. We want them to be computed + /// first, ahead of everything else, and the main results should show up at the top of the list. + /// + protected override CodeActionRequestPriority ComputeRequestPriority() + => CodeActionRequestPriority.High; - public sealed override FixAllProvider? GetFixAllProvider() - { - // Currently Fix All is not supported for this provider - // https://github.com/dotnet/roslyn/issues/34457 - return null; - } + public sealed override FixAllProvider? GetFixAllProvider() + { + // Currently Fix All is not supported for this provider + // https://github.com/dotnet/roslyn/issues/34457 + return null; + } - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var document = context.Document; - var span = context.Span; - var cancellationToken = context.CancellationToken; - var diagnostics = context.Diagnostics; + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var span = context.Span; + var cancellationToken = context.CancellationToken; + var diagnostics = context.Diagnostics; - var addImportService = document.GetRequiredLanguageService(); - var services = document.Project.Solution.Services; + var addImportService = document.GetRequiredLanguageService(); + var services = document.Project.Solution.Services; - var codeActionOptions = context.Options.GetOptions(document.Project.Services); - var searchOptions = codeActionOptions.SearchOptions; + var codeActionOptions = context.Options.GetOptions(document.Project.Services); + var searchOptions = codeActionOptions.SearchOptions; - var symbolSearchService = _symbolSearchService ?? services.GetRequiredService(); + var symbolSearchService = _symbolSearchService ?? services.GetRequiredService(); - var installerService = searchOptions.SearchNuGetPackages ? - _packageInstallerService ?? services.GetService() : null; + var installerService = searchOptions.SearchNuGetPackages ? + _packageInstallerService ?? services.GetService() : null; - var packageSources = installerService?.IsEnabled(document.Project.Id) == true - ? installerService.TryGetPackageSources() - : []; + var packageSources = installerService?.IsEnabled(document.Project.Id) == true + ? installerService.TryGetPackageSources() + : []; - if (packageSources.IsEmpty) - { - searchOptions = searchOptions with { SearchNuGetPackages = false }; - } + if (packageSources.IsEmpty) + { + searchOptions = searchOptions with { SearchNuGetPackages = false }; + } - var cleanupOptions = await document.GetCodeCleanupOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); + var cleanupOptions = await document.GetCodeCleanupOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); - var addImportOptions = new AddImportOptions( - searchOptions, - cleanupOptions, - codeActionOptions.HideAdvancedMembers); + var addImportOptions = new AddImportOptions( + searchOptions, + cleanupOptions, + codeActionOptions.HideAdvancedMembers); - var fixesForDiagnostic = await addImportService.GetFixesForDiagnosticsAsync( - document, span, diagnostics, MaxResults, symbolSearchService, addImportOptions, packageSources, cancellationToken).ConfigureAwait(false); + var fixesForDiagnostic = await addImportService.GetFixesForDiagnosticsAsync( + document, span, diagnostics, MaxResults, symbolSearchService, addImportOptions, packageSources, cancellationToken).ConfigureAwait(false); - foreach (var (diagnostic, fixes) in fixesForDiagnostic) - { - // Limit the results returned since this will be displayed to the user - var codeActions = addImportService.GetCodeActionsForFixes(document, fixes, installerService, MaxResults); - context.RegisterFixes(codeActions, diagnostic); - } + foreach (var (diagnostic, fixes) in fixesForDiagnostic) + { + // Limit the results returned since this will be displayed to the user + var codeActions = addImportService.GetCodeActionsForFixes(document, fixes, installerService, MaxResults); + context.RegisterFixes(codeActions, diagnostic); } } } diff --git a/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs b/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs index 62043c2700270..99094f7dcdd97 100644 --- a/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs +++ b/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs @@ -24,605 +24,604 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService + : IAddImportFeatureService, IEqualityComparer + where TSimpleNameSyntax : SyntaxNode { - internal abstract partial class AbstractAddImportFeatureService - : IAddImportFeatureService, IEqualityComparer - where TSimpleNameSyntax : SyntaxNode + /// + /// Cache of information about whether a is likely contained within a + /// NuGet packages directory. + /// + private static readonly ConditionalWeakTable> s_isInPackagesDirectory = new(); + + protected abstract bool CanAddImport(SyntaxNode node, bool allowInHiddenRegions, CancellationToken cancellationToken); + protected abstract bool CanAddImportForMethod(string diagnosticId, ISyntaxFacts syntaxFacts, SyntaxNode node, out TSimpleNameSyntax nameNode); + protected abstract bool CanAddImportForNamespace(string diagnosticId, SyntaxNode node, out TSimpleNameSyntax nameNode); + protected abstract bool CanAddImportForDeconstruct(string diagnosticId, SyntaxNode node); + protected abstract bool CanAddImportForGetAwaiter(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node); + protected abstract bool CanAddImportForGetEnumerator(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node); + protected abstract bool CanAddImportForGetAsyncEnumerator(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node); + protected abstract bool CanAddImportForQuery(string diagnosticId, SyntaxNode node); + protected abstract bool CanAddImportForType(string diagnosticId, SyntaxNode node, out TSimpleNameSyntax nameNode); + + protected abstract ISet GetImportNamespacesInScope(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); + protected abstract ITypeSymbol GetDeconstructInfo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); + protected abstract ITypeSymbol GetQueryClauseInfo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); + protected abstract bool IsViableExtensionMethod(IMethodSymbol method, SyntaxNode expression, SemanticModel semanticModel, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken); + + protected abstract Task AddImportAsync(SyntaxNode contextNode, INamespaceOrTypeSymbol symbol, Document document, AddImportPlacementOptions options, CancellationToken cancellationToken); + protected abstract Task AddImportAsync(SyntaxNode contextNode, IReadOnlyList nameSpaceParts, Document document, AddImportPlacementOptions options, CancellationToken cancellationToken); + + protected abstract bool IsAddMethodContext(SyntaxNode node, SemanticModel semanticModel); + + protected abstract string GetDescription(IReadOnlyList nameParts); + protected abstract (string description, bool hasExistingImport) GetDescription(Document document, AddImportPlacementOptions options, INamespaceOrTypeSymbol symbol, SemanticModel semanticModel, SyntaxNode root, CancellationToken cancellationToken); + + public async Task> GetFixesAsync( + Document document, TextSpan span, string diagnosticId, int maxResults, + ISymbolSearchService symbolSearchService, AddImportOptions options, + ImmutableArray packageSources, CancellationToken cancellationToken) { - /// - /// Cache of information about whether a is likely contained within a - /// NuGet packages directory. - /// - private static readonly ConditionalWeakTable> s_isInPackagesDirectory = new(); - - protected abstract bool CanAddImport(SyntaxNode node, bool allowInHiddenRegions, CancellationToken cancellationToken); - protected abstract bool CanAddImportForMethod(string diagnosticId, ISyntaxFacts syntaxFacts, SyntaxNode node, out TSimpleNameSyntax nameNode); - protected abstract bool CanAddImportForNamespace(string diagnosticId, SyntaxNode node, out TSimpleNameSyntax nameNode); - protected abstract bool CanAddImportForDeconstruct(string diagnosticId, SyntaxNode node); - protected abstract bool CanAddImportForGetAwaiter(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node); - protected abstract bool CanAddImportForGetEnumerator(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node); - protected abstract bool CanAddImportForGetAsyncEnumerator(string diagnosticId, ISyntaxFacts syntaxFactsService, SyntaxNode node); - protected abstract bool CanAddImportForQuery(string diagnosticId, SyntaxNode node); - protected abstract bool CanAddImportForType(string diagnosticId, SyntaxNode node, out TSimpleNameSyntax nameNode); - - protected abstract ISet GetImportNamespacesInScope(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); - protected abstract ITypeSymbol GetDeconstructInfo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); - protected abstract ITypeSymbol GetQueryClauseInfo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); - protected abstract bool IsViableExtensionMethod(IMethodSymbol method, SyntaxNode expression, SemanticModel semanticModel, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken); - - protected abstract Task AddImportAsync(SyntaxNode contextNode, INamespaceOrTypeSymbol symbol, Document document, AddImportPlacementOptions options, CancellationToken cancellationToken); - protected abstract Task AddImportAsync(SyntaxNode contextNode, IReadOnlyList nameSpaceParts, Document document, AddImportPlacementOptions options, CancellationToken cancellationToken); - - protected abstract bool IsAddMethodContext(SyntaxNode node, SemanticModel semanticModel); - - protected abstract string GetDescription(IReadOnlyList nameParts); - protected abstract (string description, bool hasExistingImport) GetDescription(Document document, AddImportPlacementOptions options, INamespaceOrTypeSymbol symbol, SemanticModel semanticModel, SyntaxNode root, CancellationToken cancellationToken); - - public async Task> GetFixesAsync( - Document document, TextSpan span, string diagnosticId, int maxResults, - ISymbolSearchService symbolSearchService, AddImportOptions options, - ImmutableArray packageSources, CancellationToken cancellationToken) + var client = await RemoteHostClient.TryGetClientAsync(document.Project, cancellationToken).ConfigureAwait(false); + if (client != null) { - var client = await RemoteHostClient.TryGetClientAsync(document.Project, cancellationToken).ConfigureAwait(false); - if (client != null) - { - var result = await client.TryInvokeAsync>( - document.Project.Solution, - (service, solutionInfo, callbackId, cancellationToken) => - service.GetFixesAsync(solutionInfo, callbackId, document.Id, span, diagnosticId, maxResults, options, packageSources, cancellationToken), - callbackTarget: symbolSearchService, - cancellationToken).ConfigureAwait(false); - - return result.HasValue ? result.Value : []; - } - - return await GetFixesInCurrentProcessAsync( - document, span, diagnosticId, maxResults, - symbolSearchService, options, - packageSources, cancellationToken).ConfigureAwait(false); + var result = await client.TryInvokeAsync>( + document.Project.Solution, + (service, solutionInfo, callbackId, cancellationToken) => + service.GetFixesAsync(solutionInfo, callbackId, document.Id, span, diagnosticId, maxResults, options, packageSources, cancellationToken), + callbackTarget: symbolSearchService, + cancellationToken).ConfigureAwait(false); + + return result.HasValue ? result.Value : []; } - private async Task> GetFixesInCurrentProcessAsync( - Document document, TextSpan span, string diagnosticId, int maxResults, - ISymbolSearchService symbolSearchService, AddImportOptions options, - ImmutableArray packageSources, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var node = root.FindToken(span.Start, findInsideTrivia: true) - .GetAncestor(n => n.Span.Contains(span) && n != root); + return await GetFixesInCurrentProcessAsync( + document, span, diagnosticId, maxResults, + symbolSearchService, options, + packageSources, cancellationToken).ConfigureAwait(false); + } + + private async Task> GetFixesInCurrentProcessAsync( + Document document, TextSpan span, string diagnosticId, int maxResults, + ISymbolSearchService symbolSearchService, AddImportOptions options, + ImmutableArray packageSources, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var node = root.FindToken(span.Start, findInsideTrivia: true) + .GetAncestor(n => n.Span.Contains(span) && n != root); - using var _ = ArrayBuilder.GetInstance(out var result); - if (node != null) + using var _ = ArrayBuilder.GetInstance(out var result); + if (node != null) + { + using (Logger.LogBlock(FunctionId.Refactoring_AddImport, cancellationToken)) { - using (Logger.LogBlock(FunctionId.Refactoring_AddImport, cancellationToken)) + if (!cancellationToken.IsCancellationRequested) { - if (!cancellationToken.IsCancellationRequested) + if (CanAddImport(node, options.CleanupOptions.AddImportOptions.AllowInHiddenRegions, cancellationToken)) { - if (CanAddImport(node, options.CleanupOptions.AddImportOptions.AllowInHiddenRegions, cancellationToken)) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var allSymbolReferences = await FindResultsAsync( - document, semanticModel, diagnosticId, node, maxResults, symbolSearchService, - options, packageSources, cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var allSymbolReferences = await FindResultsAsync( + document, semanticModel, diagnosticId, node, maxResults, symbolSearchService, + options, packageSources, cancellationToken).ConfigureAwait(false); - foreach (var reference in allSymbolReferences) - { - cancellationToken.ThrowIfCancellationRequested(); + foreach (var reference in allSymbolReferences) + { + cancellationToken.ThrowIfCancellationRequested(); - var fixData = await reference.TryGetFixDataAsync(document, node, options.CleanupOptions, cancellationToken).ConfigureAwait(false); - result.AddIfNotNull(fixData); + var fixData = await reference.TryGetFixDataAsync(document, node, options.CleanupOptions, cancellationToken).ConfigureAwait(false); + result.AddIfNotNull(fixData); - if (result.Count > maxResults) - break; - } + if (result.Count > maxResults) + break; } } } } - - return result.ToImmutable(); } - private async Task> FindResultsAsync( - Document document, SemanticModel semanticModel, string diagnosticId, SyntaxNode node, int maxResults, ISymbolSearchService symbolSearchService, - AddImportOptions options, ImmutableArray packageSources, CancellationToken cancellationToken) - { - // Caches so we don't produce the same data multiple times while searching - // all over the solution. - var project = document.Project; - var projectToAssembly = new ConcurrentDictionary>(concurrencyLevel: 2, capacity: project.Solution.ProjectIds.Count); - var referenceToCompilation = new ConcurrentDictionary(concurrencyLevel: 2, capacity: project.Solution.Projects.Sum(p => p.MetadataReferences.Count)); - - var finder = new SymbolReferenceFinder( - this, document, semanticModel, diagnosticId, node, symbolSearchService, - options, packageSources, cancellationToken); - - // Look for exact matches first: - var exactReferences = await FindResultsAsync(projectToAssembly, referenceToCompilation, project, maxResults, finder, exact: true, cancellationToken: cancellationToken).ConfigureAwait(false); - if (exactReferences.Length > 0) - return exactReferences; - - // No exact matches found. Fall back to fuzzy searching. - // Only bother doing this for host workspaces. We don't want this for - // things like the Interactive workspace as this will cause us to - // create expensive bk-trees which we won't even be able to save for - // future use. - if (!IsHostOrRemoteWorkspace(project)) - { - return []; - } + return result.ToImmutable(); + } - var fuzzyReferences = await FindResultsAsync(projectToAssembly, referenceToCompilation, project, maxResults, finder, exact: false, cancellationToken: cancellationToken).ConfigureAwait(false); - return fuzzyReferences; + private async Task> FindResultsAsync( + Document document, SemanticModel semanticModel, string diagnosticId, SyntaxNode node, int maxResults, ISymbolSearchService symbolSearchService, + AddImportOptions options, ImmutableArray packageSources, CancellationToken cancellationToken) + { + // Caches so we don't produce the same data multiple times while searching + // all over the solution. + var project = document.Project; + var projectToAssembly = new ConcurrentDictionary>(concurrencyLevel: 2, capacity: project.Solution.ProjectIds.Count); + var referenceToCompilation = new ConcurrentDictionary(concurrencyLevel: 2, capacity: project.Solution.Projects.Sum(p => p.MetadataReferences.Count)); + + var finder = new SymbolReferenceFinder( + this, document, semanticModel, diagnosticId, node, symbolSearchService, + options, packageSources, cancellationToken); + + // Look for exact matches first: + var exactReferences = await FindResultsAsync(projectToAssembly, referenceToCompilation, project, maxResults, finder, exact: true, cancellationToken: cancellationToken).ConfigureAwait(false); + if (exactReferences.Length > 0) + return exactReferences; + + // No exact matches found. Fall back to fuzzy searching. + // Only bother doing this for host workspaces. We don't want this for + // things like the Interactive workspace as this will cause us to + // create expensive bk-trees which we won't even be able to save for + // future use. + if (!IsHostOrRemoteWorkspace(project)) + { + return []; } - private static bool IsHostOrRemoteWorkspace(Project project) - => project.Solution.WorkspaceKind is WorkspaceKind.Host or WorkspaceKind.RemoteWorkspace; + var fuzzyReferences = await FindResultsAsync(projectToAssembly, referenceToCompilation, project, maxResults, finder, exact: false, cancellationToken: cancellationToken).ConfigureAwait(false); + return fuzzyReferences; + } + + private static bool IsHostOrRemoteWorkspace(Project project) + => project.Solution.WorkspaceKind is WorkspaceKind.Host or WorkspaceKind.RemoteWorkspace; - private async Task> FindResultsAsync( - ConcurrentDictionary> projectToAssembly, - ConcurrentDictionary referenceToCompilation, - Project project, int maxResults, SymbolReferenceFinder finder, bool exact, CancellationToken cancellationToken) + private async Task> FindResultsAsync( + ConcurrentDictionary> projectToAssembly, + ConcurrentDictionary referenceToCompilation, + Project project, int maxResults, SymbolReferenceFinder finder, bool exact, CancellationToken cancellationToken) + { + var allReferences = new ConcurrentQueue(); + + // First search the current project to see if any symbols (source or metadata) match the + // search string. + await FindResultsInAllSymbolsInStartingProjectAsync( + allReferences, finder, exact, cancellationToken).ConfigureAwait(false); + + // Only bother doing this for host workspaces. We don't want this for + // things like the Interactive workspace as we can't even add project + // references to the interactive window. We could consider adding metadata + // references with #r in the future. + if (IsHostOrRemoteWorkspace(project)) { - var allReferences = new ConcurrentQueue(); - - // First search the current project to see if any symbols (source or metadata) match the - // search string. - await FindResultsInAllSymbolsInStartingProjectAsync( - allReferences, finder, exact, cancellationToken).ConfigureAwait(false); - - // Only bother doing this for host workspaces. We don't want this for - // things like the Interactive workspace as we can't even add project - // references to the interactive window. We could consider adding metadata - // references with #r in the future. - if (IsHostOrRemoteWorkspace(project)) - { - // Now search unreferenced projects, and see if they have any source symbols that match - // the search string. - await FindResultsInUnreferencedProjectSourceSymbolsAsync(projectToAssembly, project, allReferences, maxResults, finder, exact, cancellationToken).ConfigureAwait(false); + // Now search unreferenced projects, and see if they have any source symbols that match + // the search string. + await FindResultsInUnreferencedProjectSourceSymbolsAsync(projectToAssembly, project, allReferences, maxResults, finder, exact, cancellationToken).ConfigureAwait(false); - // Finally, check and see if we have any metadata symbols that match the search string. - await FindResultsInUnreferencedMetadataSymbolsAsync(referenceToCompilation, project, allReferences, maxResults, finder, exact, cancellationToken).ConfigureAwait(false); + // Finally, check and see if we have any metadata symbols that match the search string. + await FindResultsInUnreferencedMetadataSymbolsAsync(referenceToCompilation, project, allReferences, maxResults, finder, exact, cancellationToken).ConfigureAwait(false); - // We only support searching NuGet in an exact manner currently. - if (exact) - { - await finder.FindNugetOrReferenceAssemblyReferencesAsync(allReferences, cancellationToken).ConfigureAwait(false); - } + // We only support searching NuGet in an exact manner currently. + if (exact) + { + await finder.FindNugetOrReferenceAssemblyReferencesAsync(allReferences, cancellationToken).ConfigureAwait(false); } - - return allReferences.ToImmutableArray(); } - private static async Task FindResultsInAllSymbolsInStartingProjectAsync( - ConcurrentQueue allSymbolReferences, SymbolReferenceFinder finder, bool exact, CancellationToken cancellationToken) - { - AddRange( - allSymbolReferences, - await finder.FindInAllSymbolsInStartingProjectAsync(exact, cancellationToken).ConfigureAwait(false)); - } - - private static async Task FindResultsInUnreferencedProjectSourceSymbolsAsync( - ConcurrentDictionary> projectToAssembly, - Project project, ConcurrentQueue allSymbolReferences, int maxResults, - SymbolReferenceFinder finder, bool exact, CancellationToken cancellationToken) - { - // If we didn't find enough hits searching just in the project, then check - // in any unreferenced projects. - if (allSymbolReferences.Count >= maxResults) - return; - - var viableUnreferencedProjects = GetViableUnreferencedProjects(project); + return allReferences.ToImmutableArray(); + } - // Search all unreferenced projects in parallel. - using var _ = ArrayBuilder.GetInstance(out var findTasks); + private static async Task FindResultsInAllSymbolsInStartingProjectAsync( + ConcurrentQueue allSymbolReferences, SymbolReferenceFinder finder, bool exact, CancellationToken cancellationToken) + { + AddRange( + allSymbolReferences, + await finder.FindInAllSymbolsInStartingProjectAsync(exact, cancellationToken).ConfigureAwait(false)); + } - // Create another cancellation token so we can both search all projects in parallel, - // but also stop any searches once we get enough results. - using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + private static async Task FindResultsInUnreferencedProjectSourceSymbolsAsync( + ConcurrentDictionary> projectToAssembly, + Project project, ConcurrentQueue allSymbolReferences, int maxResults, + SymbolReferenceFinder finder, bool exact, CancellationToken cancellationToken) + { + // If we didn't find enough hits searching just in the project, then check + // in any unreferenced projects. + if (allSymbolReferences.Count >= maxResults) + return; - foreach (var unreferencedProject in viableUnreferencedProjects) - { - if (!unreferencedProject.SupportsCompilation) - continue; + var viableUnreferencedProjects = GetViableUnreferencedProjects(project); - // Search in this unreferenced project. But don't search in any of its' - // direct references. i.e. we don't want to search in its metadata references - // or in the projects it references itself. We'll be searching those entities - // individually. - findTasks.Add(ProcessReferencesAsync( - allSymbolReferences, maxResults, linkedTokenSource, - finder.FindInSourceSymbolsInProjectAsync(projectToAssembly, unreferencedProject, exact, linkedTokenSource.Token))); - } + // Search all unreferenced projects in parallel. + using var _ = ArrayBuilder.GetInstance(out var findTasks); - await Task.WhenAll(findTasks).ConfigureAwait(false); - } + // Create another cancellation token so we can both search all projects in parallel, + // but also stop any searches once we get enough results. + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - private async Task FindResultsInUnreferencedMetadataSymbolsAsync( - ConcurrentDictionary referenceToCompilation, - Project project, ConcurrentQueue allSymbolReferences, int maxResults, SymbolReferenceFinder finder, - bool exact, CancellationToken cancellationToken) + foreach (var unreferencedProject in viableUnreferencedProjects) { - // Only do this if none of the project searches produced any results. We may have a - // lot of metadata to search through, and it would be good to avoid that if we can. - if (allSymbolReferences.Count > 0) - return; + if (!unreferencedProject.SupportsCompilation) + continue; + + // Search in this unreferenced project. But don't search in any of its' + // direct references. i.e. we don't want to search in its metadata references + // or in the projects it references itself. We'll be searching those entities + // individually. + findTasks.Add(ProcessReferencesAsync( + allSymbolReferences, maxResults, linkedTokenSource, + finder.FindInSourceSymbolsInProjectAsync(projectToAssembly, unreferencedProject, exact, linkedTokenSource.Token))); + } - // Keep track of the references we've seen (so that we don't process them multiple times - // across many sibling projects). Prepopulate it with our own metadata references since - // we know we don't need to search in that. - var seenReferences = new HashSet(comparer: this); - seenReferences.AddAll(project.MetadataReferences.OfType()); + await Task.WhenAll(findTasks).ConfigureAwait(false); + } - var newReferences = GetUnreferencedMetadataReferences(project, seenReferences); + private async Task FindResultsInUnreferencedMetadataSymbolsAsync( + ConcurrentDictionary referenceToCompilation, + Project project, ConcurrentQueue allSymbolReferences, int maxResults, SymbolReferenceFinder finder, + bool exact, CancellationToken cancellationToken) + { + // Only do this if none of the project searches produced any results. We may have a + // lot of metadata to search through, and it would be good to avoid that if we can. + if (allSymbolReferences.Count > 0) + return; - // Search all metadata references in parallel. - using var _ = ArrayBuilder.GetInstance(out var findTasks); + // Keep track of the references we've seen (so that we don't process them multiple times + // across many sibling projects). Prepopulate it with our own metadata references since + // we know we don't need to search in that. + var seenReferences = new HashSet(comparer: this); + seenReferences.AddAll(project.MetadataReferences.OfType()); - // Create another cancellation token so we can both search all projects in parallel, - // but also stop any searches once we get enough results. - using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var newReferences = GetUnreferencedMetadataReferences(project, seenReferences); - foreach (var (referenceProject, reference) in newReferences) - { - var compilation = referenceToCompilation.GetOrAdd( - reference, r => CreateCompilation(project, r)); + // Search all metadata references in parallel. + using var _ = ArrayBuilder.GetInstance(out var findTasks); - // Ignore netmodules. First, they're incredibly esoteric and barely used. - // Second, the SymbolFinder API doesn't even support searching them. - if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) - { - findTasks.Add(ProcessReferencesAsync( - allSymbolReferences, maxResults, linkedTokenSource, - finder.FindInMetadataSymbolsAsync(assembly, referenceProject, reference, exact, linkedTokenSource.Token))); - } - } + // Create another cancellation token so we can both search all projects in parallel, + // but also stop any searches once we get enough results. + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - await Task.WhenAll(findTasks).ConfigureAwait(false); - } - - /// - /// Returns the set of PEReferences in the solution that are not currently being referenced - /// by this project. The set returned will be tuples containing the PEReference, and the project-id - /// for the project we found the pe-reference in. - /// - private static ImmutableArray<(Project, PortableExecutableReference)> GetUnreferencedMetadataReferences( - Project project, HashSet seenReferences) + foreach (var (referenceProject, reference) in newReferences) { - var result = ArrayBuilder<(Project, PortableExecutableReference)>.GetInstance(); + var compilation = referenceToCompilation.GetOrAdd( + reference, r => CreateCompilation(project, r)); - var solution = project.Solution; - foreach (var p in solution.Projects) + // Ignore netmodules. First, they're incredibly esoteric and barely used. + // Second, the SymbolFinder API doesn't even support searching them. + if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) { - if (p == project) - { - continue; - } - - foreach (var reference in p.MetadataReferences) - { - if (reference is PortableExecutableReference peReference && - !IsInPackagesDirectory(peReference) && - seenReferences.Add(peReference)) - { - result.Add((p, peReference)); - } - } + findTasks.Add(ProcessReferencesAsync( + allSymbolReferences, maxResults, linkedTokenSource, + finder.FindInMetadataSymbolsAsync(assembly, referenceProject, reference, exact, linkedTokenSource.Token))); } - - return result.ToImmutableAndFree(); } - private static async Task ProcessReferencesAsync( - ConcurrentQueue allSymbolReferences, - int maxResults, - CancellationTokenSource linkedTokenSource, - Task> task) + await Task.WhenAll(findTasks).ConfigureAwait(false); + } + + /// + /// Returns the set of PEReferences in the solution that are not currently being referenced + /// by this project. The set returned will be tuples containing the PEReference, and the project-id + /// for the project we found the pe-reference in. + /// + private static ImmutableArray<(Project, PortableExecutableReference)> GetUnreferencedMetadataReferences( + Project project, HashSet seenReferences) + { + var result = ArrayBuilder<(Project, PortableExecutableReference)>.GetInstance(); + + var solution = project.Solution; + foreach (var p in solution.Projects) { - AddRange(allSymbolReferences, await task.ConfigureAwait(false)); + if (p == project) + { + continue; + } - // If we've gone over the max amount of items we're looking for, attempt to cancel all existing work that is - // still searching. - if (allSymbolReferences.Count >= maxResults) + foreach (var reference in p.MetadataReferences) { - try - { - linkedTokenSource.Cancel(); - } - catch (ObjectDisposedException) + if (reference is PortableExecutableReference peReference && + !IsInPackagesDirectory(peReference) && + seenReferences.Add(peReference)) { + result.Add((p, peReference)); } } } - /// - /// We ignore references that are in a directory that contains the names - /// "Packages", "packs", "NuGetFallbackFolder", or "NuGetPackages" - /// These directories are most likely the ones produced by NuGet, and we don't want - /// to offer to add .dll reference manually for dlls that are part of NuGet packages. - /// - /// Note that this is only a heuristic (though a good one), and we should remove this - /// when we can get an API from NuGet that tells us if a reference is actually provided - /// by a nuget packages. - /// Tracking issue: https://github.com/dotnet/project-system/issues/5275 - /// - /// This heuristic will do the right thing in practically all cases for all. It - /// prevents the very unpleasant experience of us offering to add a direct metadata - /// reference to something that should only be referenced as a nuget package. - /// - /// It does mean that if the following is true: - /// You have a project that has a non-nuget metadata reference to something in a "packages" - /// directory, and you are in another project that uses a type name that would have matched - /// an accessible type from that dll. then we will not offer to add that .dll reference to - /// that other project. - /// - /// However, that would be an exceedingly uncommon case that is degraded. Whereas we're - /// vastly improved in the common case. This is a totally acceptable and desirable outcome - /// for such a heuristic. - /// - private static bool IsInPackagesDirectory(PortableExecutableReference reference) - { - return s_isInPackagesDirectory.GetValue( - reference, - static reference => new StrongBox(ComputeIsInPackagesDirectory(reference))).Value; + return result.ToImmutableAndFree(); + } + + private static async Task ProcessReferencesAsync( + ConcurrentQueue allSymbolReferences, + int maxResults, + CancellationTokenSource linkedTokenSource, + Task> task) + { + AddRange(allSymbolReferences, await task.ConfigureAwait(false)); - static bool ComputeIsInPackagesDirectory(PortableExecutableReference reference) + // If we've gone over the max amount of items we're looking for, attempt to cancel all existing work that is + // still searching. + if (allSymbolReferences.Count >= maxResults) + { + try { - return ContainsPathComponent(reference, "packages") - || ContainsPathComponent(reference, "packs") - || ContainsPathComponent(reference, "NuGetFallbackFolder") - || ContainsPathComponent(reference, "NuGetPackages"); + linkedTokenSource.Cancel(); } - - static bool ContainsPathComponent(PortableExecutableReference reference, string pathComponent) + catch (ObjectDisposedException) { - return PathUtilities.ContainsPathComponent(reference.FilePath, pathComponent, ignoreCase: true); } } + } - /// - /// Called when we want to search a metadata reference. We create a dummy compilation - /// containing just that reference and we search that. That way we can get actual symbols - /// returned. - /// - /// We don't want to use the project that the reference is actually associated with as - /// getting the compilation for that project may be extremely expensive. For example, - /// in a large solution it may cause us to build an enormous amount of skeleton assemblies. - /// - private static Compilation CreateCompilation(Project project, PortableExecutableReference reference) + /// + /// We ignore references that are in a directory that contains the names + /// "Packages", "packs", "NuGetFallbackFolder", or "NuGetPackages" + /// These directories are most likely the ones produced by NuGet, and we don't want + /// to offer to add .dll reference manually for dlls that are part of NuGet packages. + /// + /// Note that this is only a heuristic (though a good one), and we should remove this + /// when we can get an API from NuGet that tells us if a reference is actually provided + /// by a nuget packages. + /// Tracking issue: https://github.com/dotnet/project-system/issues/5275 + /// + /// This heuristic will do the right thing in practically all cases for all. It + /// prevents the very unpleasant experience of us offering to add a direct metadata + /// reference to something that should only be referenced as a nuget package. + /// + /// It does mean that if the following is true: + /// You have a project that has a non-nuget metadata reference to something in a "packages" + /// directory, and you are in another project that uses a type name that would have matched + /// an accessible type from that dll. then we will not offer to add that .dll reference to + /// that other project. + /// + /// However, that would be an exceedingly uncommon case that is degraded. Whereas we're + /// vastly improved in the common case. This is a totally acceptable and desirable outcome + /// for such a heuristic. + /// + private static bool IsInPackagesDirectory(PortableExecutableReference reference) + { + return s_isInPackagesDirectory.GetValue( + reference, + static reference => new StrongBox(ComputeIsInPackagesDirectory(reference))).Value; + + static bool ComputeIsInPackagesDirectory(PortableExecutableReference reference) { - var compilationService = project.Services.GetRequiredService(); - var compilation = compilationService.CreateCompilation("TempAssembly", compilationService.GetDefaultCompilationOptions()); - return compilation.WithReferences(reference); + return ContainsPathComponent(reference, "packages") + || ContainsPathComponent(reference, "packs") + || ContainsPathComponent(reference, "NuGetFallbackFolder") + || ContainsPathComponent(reference, "NuGetPackages"); } - bool IEqualityComparer.Equals(PortableExecutableReference? x, PortableExecutableReference? y) + static bool ContainsPathComponent(PortableExecutableReference reference, string pathComponent) { - if (x == y) - return true; + return PathUtilities.ContainsPathComponent(reference.FilePath, pathComponent, ignoreCase: true); + } + } - var path1 = x?.FilePath ?? x?.Display; - var path2 = y?.FilePath ?? y?.Display; - if (path1 == null || path2 == null) - return false; + /// + /// Called when we want to search a metadata reference. We create a dummy compilation + /// containing just that reference and we search that. That way we can get actual symbols + /// returned. + /// + /// We don't want to use the project that the reference is actually associated with as + /// getting the compilation for that project may be extremely expensive. For example, + /// in a large solution it may cause us to build an enormous amount of skeleton assemblies. + /// + private static Compilation CreateCompilation(Project project, PortableExecutableReference reference) + { + var compilationService = project.Services.GetRequiredService(); + var compilation = compilationService.CreateCompilation("TempAssembly", compilationService.GetDefaultCompilationOptions()); + return compilation.WithReferences(reference); + } - return StringComparer.OrdinalIgnoreCase.Equals(path1, path2); - } + bool IEqualityComparer.Equals(PortableExecutableReference? x, PortableExecutableReference? y) + { + if (x == y) + return true; - int IEqualityComparer.GetHashCode(PortableExecutableReference obj) - { - var path = obj.FilePath ?? obj.Display; - return path == null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(path); - } + var path1 = x?.FilePath ?? x?.Display; + var path2 = y?.FilePath ?? y?.Display; + if (path1 == null || path2 == null) + return false; - private static HashSet GetViableUnreferencedProjects(Project project) - { - var solution = project.Solution; - var viableProjects = new HashSet(solution.Projects); + return StringComparer.OrdinalIgnoreCase.Equals(path1, path2); + } - // Clearly we can't reference ourselves. - viableProjects.Remove(project); + int IEqualityComparer.GetHashCode(PortableExecutableReference obj) + { + var path = obj.FilePath ?? obj.Display; + return path == null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(path); + } - // We can't reference any project that transitively depends on us. Doing so would - // cause a circular reference between projects. - var dependencyGraph = solution.GetProjectDependencyGraph(); - var projectsThatTransitivelyDependOnThisProject = dependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(project.Id); + private static HashSet GetViableUnreferencedProjects(Project project) + { + var solution = project.Solution; + var viableProjects = new HashSet(solution.Projects); - viableProjects.RemoveAll(projectsThatTransitivelyDependOnThisProject.Select(solution.GetRequiredProject)); + // Clearly we can't reference ourselves. + viableProjects.Remove(project); - // We also aren't interested in any projects we're already directly referencing. - viableProjects.RemoveAll(project.ProjectReferences.Select(r => solution.GetRequiredProject(r.ProjectId))); - return viableProjects; - } + // We can't reference any project that transitively depends on us. Doing so would + // cause a circular reference between projects. + var dependencyGraph = solution.GetProjectDependencyGraph(); + var projectsThatTransitivelyDependOnThisProject = dependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(project.Id); - private static void AddRange(ConcurrentQueue allSymbolReferences, ImmutableArray proposedReferences) - { - foreach (var reference in proposedReferences) - allSymbolReferences.Enqueue(reference); - } + viableProjects.RemoveAll(projectsThatTransitivelyDependOnThisProject.Select(solution.GetRequiredProject)); - protected static bool IsViableExtensionMethod(IMethodSymbol method, ITypeSymbol receiver) - { - if (receiver == null || method == null) - { - return false; - } + // We also aren't interested in any projects we're already directly referencing. + viableProjects.RemoveAll(project.ProjectReferences.Select(r => solution.GetRequiredProject(r.ProjectId))); + return viableProjects; + } - // It's possible that the 'method' we're looking at is from a different language than - // the language we're currently in. For example, we might find the extension method - // in an unreferenced VB project while we're in C#. However, in order to 'reduce' - // the extension method, the compiler requires both the method and receiver to be - // from the same language. - // - // So, if they're not from the same language, we simply can't proceed. Now in this - // case we decide that the method is not viable. But we could, in the future, decide - // to just always consider such methods viable. - - if (receiver.Language != method.Language) - { - return false; - } + private static void AddRange(ConcurrentQueue allSymbolReferences, ImmutableArray proposedReferences) + { + foreach (var reference in proposedReferences) + allSymbolReferences.Enqueue(reference); + } - return method.ReduceExtensionMethod(receiver) != null; + protected static bool IsViableExtensionMethod(IMethodSymbol method, ITypeSymbol receiver) + { + if (receiver == null || method == null) + { + return false; } - private static bool NotGlobalNamespace(SymbolReference reference) + // It's possible that the 'method' we're looking at is from a different language than + // the language we're currently in. For example, we might find the extension method + // in an unreferenced VB project while we're in C#. However, in order to 'reduce' + // the extension method, the compiler requires both the method and receiver to be + // from the same language. + // + // So, if they're not from the same language, we simply can't proceed. Now in this + // case we decide that the method is not viable. But we could, in the future, decide + // to just always consider such methods viable. + + if (receiver.Language != method.Language) { - var symbol = reference.SymbolResult.Symbol; - return symbol.IsNamespace ? !((INamespaceSymbol)symbol).IsGlobalNamespace : true; + return false; } - private static bool NotNull(SymbolReference reference) - => reference.SymbolResult.Symbol != null; - - public async Task Fixes)>> GetFixesForDiagnosticsAsync( - Document document, TextSpan span, ImmutableArray diagnostics, int maxResultsPerDiagnostic, - ISymbolSearchService symbolSearchService, AddImportOptions options, - ImmutableArray packageSources, CancellationToken cancellationToken) - { - // We might have multiple different diagnostics covering the same span. Have to - // process them all as we might produce different fixes for each diagnostic. + return method.ReduceExtensionMethod(receiver) != null; + } - using var _ = ArrayBuilder<(Diagnostic, ImmutableArray)>.GetInstance(out var result); + private static bool NotGlobalNamespace(SymbolReference reference) + { + var symbol = reference.SymbolResult.Symbol; + return symbol.IsNamespace ? !((INamespaceSymbol)symbol).IsGlobalNamespace : true; + } - foreach (var diagnostic in diagnostics) - { - var fixes = await GetFixesAsync( - document, span, diagnostic.Id, maxResultsPerDiagnostic, - symbolSearchService, options, - packageSources, cancellationToken).ConfigureAwait(false); + private static bool NotNull(SymbolReference reference) + => reference.SymbolResult.Symbol != null; - result.Add((diagnostic, fixes)); - } + public async Task Fixes)>> GetFixesForDiagnosticsAsync( + Document document, TextSpan span, ImmutableArray diagnostics, int maxResultsPerDiagnostic, + ISymbolSearchService symbolSearchService, AddImportOptions options, + ImmutableArray packageSources, CancellationToken cancellationToken) + { + // We might have multiple different diagnostics covering the same span. Have to + // process them all as we might produce different fixes for each diagnostic. - return result.ToImmutable(); - } + using var _ = ArrayBuilder<(Diagnostic, ImmutableArray)>.GetInstance(out var result); - public async Task> GetUniqueFixesAsync( - Document document, TextSpan span, ImmutableArray diagnosticIds, - ISymbolSearchService symbolSearchService, AddImportOptions options, - ImmutableArray packageSources, CancellationToken cancellationToken) + foreach (var diagnostic in diagnostics) { - var client = await RemoteHostClient.TryGetClientAsync(document.Project, cancellationToken).ConfigureAwait(false); - if (client != null) - { - var result = await client.TryInvokeAsync>( - document.Project.Solution, - (service, solutionInfo, callbackId, cancellationToken) => - service.GetUniqueFixesAsync(solutionInfo, callbackId, document.Id, span, diagnosticIds, options, packageSources, cancellationToken), - callbackTarget: symbolSearchService, - cancellationToken).ConfigureAwait(false); - - return result.HasValue ? result.Value : []; - } - - return await GetUniqueFixesAsyncInCurrentProcessAsync( - document, span, diagnosticIds, + var fixes = await GetFixesAsync( + document, span, diagnostic.Id, maxResultsPerDiagnostic, symbolSearchService, options, packageSources, cancellationToken).ConfigureAwait(false); + + result.Add((diagnostic, fixes)); } - private async Task> GetUniqueFixesAsyncInCurrentProcessAsync( - Document document, - TextSpan span, - ImmutableArray diagnosticIds, - ISymbolSearchService symbolSearchService, - AddImportOptions options, - ImmutableArray packageSources, - CancellationToken cancellationToken) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + return result.ToImmutable(); + } - // Get the diagnostics that indicate a missing import. - var diagnostics = semanticModel.GetDiagnostics(span, cancellationToken) - .Where(diagnostic => diagnosticIds.Contains(diagnostic.Id)) - .ToImmutableArray(); + public async Task> GetUniqueFixesAsync( + Document document, TextSpan span, ImmutableArray diagnosticIds, + ISymbolSearchService symbolSearchService, AddImportOptions options, + ImmutableArray packageSources, CancellationToken cancellationToken) + { + var client = await RemoteHostClient.TryGetClientAsync(document.Project, cancellationToken).ConfigureAwait(false); + if (client != null) + { + var result = await client.TryInvokeAsync>( + document.Project.Solution, + (service, solutionInfo, callbackId, cancellationToken) => + service.GetUniqueFixesAsync(solutionInfo, callbackId, document.Id, span, diagnosticIds, options, packageSources, cancellationToken), + callbackTarget: symbolSearchService, + cancellationToken).ConfigureAwait(false); + + return result.HasValue ? result.Value : []; + } - var getFixesForDiagnosticsTasks = diagnostics - .GroupBy(diagnostic => diagnostic.Location.SourceSpan) - .Select(diagnosticsForSourceSpan => GetFixesForDiagnosticsAsync( - document, diagnosticsForSourceSpan.Key, diagnosticsForSourceSpan.AsImmutable(), - maxResultsPerDiagnostic: 2, symbolSearchService, options, packageSources, cancellationToken)); + return await GetUniqueFixesAsyncInCurrentProcessAsync( + document, span, diagnosticIds, + symbolSearchService, options, + packageSources, cancellationToken).ConfigureAwait(false); + } - using var _ = ArrayBuilder.GetInstance(out var fixes); - foreach (var getFixesForDiagnosticsTask in getFixesForDiagnosticsTasks) - { - var fixesForDiagnostics = await getFixesForDiagnosticsTask.ConfigureAwait(false); + private async Task> GetUniqueFixesAsyncInCurrentProcessAsync( + Document document, + TextSpan span, + ImmutableArray diagnosticIds, + ISymbolSearchService symbolSearchService, + AddImportOptions options, + ImmutableArray packageSources, + CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - foreach (var fixesForDiagnostic in fixesForDiagnostics) - { - // When there is more than one potential fix for a missing import diagnostic, - // which is possible when the same class name is present in multiple namespaces, - // we do not want to choose for the user and be wrong. We will not attempt to - // fix this diagnostic and instead leave it for the user to resolve since they - // will have more context for determining the proper fix. - if (fixesForDiagnostic.Fixes.Length == 1) - fixes.Add(fixesForDiagnostic.Fixes[0]); - } - } + // Get the diagnostics that indicate a missing import. + var diagnostics = semanticModel.GetDiagnostics(span, cancellationToken) + .Where(diagnostic => diagnosticIds.Contains(diagnostic.Id)) + .ToImmutableArray(); - return fixes.ToImmutable(); - } + var getFixesForDiagnosticsTasks = diagnostics + .GroupBy(diagnostic => diagnostic.Location.SourceSpan) + .Select(diagnosticsForSourceSpan => GetFixesForDiagnosticsAsync( + document, diagnosticsForSourceSpan.Key, diagnosticsForSourceSpan.AsImmutable(), + maxResultsPerDiagnostic: 2, symbolSearchService, options, packageSources, cancellationToken)); - public ImmutableArray GetCodeActionsForFixes( - Document document, ImmutableArray fixes, - IPackageInstallerService? installerService, int maxResults) + using var _ = ArrayBuilder.GetInstance(out var fixes); + foreach (var getFixesForDiagnosticsTask in getFixesForDiagnosticsTasks) { - using var _ = ArrayBuilder.GetInstance(out var result); + var fixesForDiagnostics = await getFixesForDiagnosticsTask.ConfigureAwait(false); - foreach (var fix in fixes) + foreach (var fixesForDiagnostic in fixesForDiagnostics) { - result.AddIfNotNull(TryCreateCodeAction(document, fix, installerService)); - if (result.Count >= maxResults) - break; + // When there is more than one potential fix for a missing import diagnostic, + // which is possible when the same class name is present in multiple namespaces, + // we do not want to choose for the user and be wrong. We will not attempt to + // fix this diagnostic and instead leave it for the user to resolve since they + // will have more context for determining the proper fix. + if (fixesForDiagnostic.Fixes.Length == 1) + fixes.Add(fixesForDiagnostic.Fixes[0]); } - - return result.ToImmutable(); } - private static CodeAction? TryCreateCodeAction(Document document, AddImportFixData fixData, IPackageInstallerService? installerService) - => fixData.Kind switch - { - AddImportFixKind.ProjectSymbol => new ProjectSymbolReferenceCodeAction(document, fixData), - AddImportFixKind.MetadataSymbol => new MetadataSymbolReferenceCodeAction(document, fixData), - AddImportFixKind.ReferenceAssemblySymbol => new AssemblyReferenceCodeAction(document, fixData), - AddImportFixKind.PackageSymbol => ParentInstallPackageCodeAction.TryCreateCodeAction( - document, new InstallPackageData(fixData.PackageSource, fixData.PackageName, fixData.PackageVersionOpt, fixData.TextChanges), installerService), - _ => throw ExceptionUtilities.Unreachable(), - }; - - private static ITypeSymbol? GetAwaitInfo(SemanticModel semanticModel, ISyntaxFacts syntaxFactsService, SyntaxNode node) - { - var awaitExpression = FirstAwaitExpressionAncestor(syntaxFactsService, node); - if (awaitExpression is null) - return null; + return fixes.ToImmutable(); + } - Debug.Assert(syntaxFactsService.IsAwaitExpression(awaitExpression)); - var innerExpression = syntaxFactsService.GetExpressionOfAwaitExpression(awaitExpression); + public ImmutableArray GetCodeActionsForFixes( + Document document, ImmutableArray fixes, + IPackageInstallerService? installerService, int maxResults) + { + using var _ = ArrayBuilder.GetInstance(out var result); - return semanticModel.GetTypeInfo(innerExpression).Type; + foreach (var fix in fixes) + { + result.AddIfNotNull(TryCreateCodeAction(document, fix, installerService)); + if (result.Count >= maxResults) + break; } - private static ITypeSymbol? GetCollectionExpressionType(SemanticModel semanticModel, ISyntaxFacts syntaxFactsService, SyntaxNode node) + return result.ToImmutable(); + } + + private static CodeAction? TryCreateCodeAction(Document document, AddImportFixData fixData, IPackageInstallerService? installerService) + => fixData.Kind switch { - var collectionExpression = FirstForeachCollectionExpressionAncestor(syntaxFactsService, node); + AddImportFixKind.ProjectSymbol => new ProjectSymbolReferenceCodeAction(document, fixData), + AddImportFixKind.MetadataSymbol => new MetadataSymbolReferenceCodeAction(document, fixData), + AddImportFixKind.ReferenceAssemblySymbol => new AssemblyReferenceCodeAction(document, fixData), + AddImportFixKind.PackageSymbol => ParentInstallPackageCodeAction.TryCreateCodeAction( + document, new InstallPackageData(fixData.PackageSource, fixData.PackageName, fixData.PackageVersionOpt, fixData.TextChanges), installerService), + _ => throw ExceptionUtilities.Unreachable(), + }; + + private static ITypeSymbol? GetAwaitInfo(SemanticModel semanticModel, ISyntaxFacts syntaxFactsService, SyntaxNode node) + { + var awaitExpression = FirstAwaitExpressionAncestor(syntaxFactsService, node); + if (awaitExpression is null) + return null; - if (collectionExpression is null) - { - return null; - } + Debug.Assert(syntaxFactsService.IsAwaitExpression(awaitExpression)); + var innerExpression = syntaxFactsService.GetExpressionOfAwaitExpression(awaitExpression); - return semanticModel.GetTypeInfo(collectionExpression).Type; - } + return semanticModel.GetTypeInfo(innerExpression).Type; + } - protected static bool AncestorOrSelfIsAwaitExpression(ISyntaxFacts syntaxFactsService, SyntaxNode node) - => FirstAwaitExpressionAncestor(syntaxFactsService, node) != null; + private static ITypeSymbol? GetCollectionExpressionType(SemanticModel semanticModel, ISyntaxFacts syntaxFactsService, SyntaxNode node) + { + var collectionExpression = FirstForeachCollectionExpressionAncestor(syntaxFactsService, node); - private static SyntaxNode? FirstAwaitExpressionAncestor(ISyntaxFacts syntaxFactsService, SyntaxNode node) - => node.FirstAncestorOrSelf((n, syntaxFactsService) => syntaxFactsService.IsAwaitExpression(n), syntaxFactsService); + if (collectionExpression is null) + { + return null; + } - private static SyntaxNode? FirstForeachCollectionExpressionAncestor(ISyntaxFacts syntaxFactsService, SyntaxNode node) - => node.FirstAncestorOrSelf((n, syntaxFactsService) => syntaxFactsService.IsExpressionOfForeach(n), syntaxFactsService); + return semanticModel.GetTypeInfo(collectionExpression).Type; } + + protected static bool AncestorOrSelfIsAwaitExpression(ISyntaxFacts syntaxFactsService, SyntaxNode node) + => FirstAwaitExpressionAncestor(syntaxFactsService, node) != null; + + private static SyntaxNode? FirstAwaitExpressionAncestor(ISyntaxFacts syntaxFactsService, SyntaxNode node) + => node.FirstAncestorOrSelf((n, syntaxFactsService) => syntaxFactsService.IsAwaitExpression(n), syntaxFactsService); + + private static SyntaxNode? FirstForeachCollectionExpressionAncestor(ISyntaxFacts syntaxFactsService, SyntaxNode node) + => node.FirstAncestorOrSelf((n, syntaxFactsService) => syntaxFactsService.IsExpressionOfForeach(n), syntaxFactsService); } diff --git a/src/Features/Core/Portable/AddImport/AddImportFixData.cs b/src/Features/Core/Portable/AddImport/AddImportFixData.cs index 2e67b823f0219..bebc38f0f4a02 100644 --- a/src/Features/Core/Portable/AddImport/AddImportFixData.cs +++ b/src/Features/Core/Portable/AddImport/AddImportFixData.cs @@ -11,138 +11,137 @@ using Microsoft.CodeAnalysis.Tags; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +[DataContract] +internal sealed class AddImportFixData( + AddImportFixKind kind, + ImmutableArray textChanges, + string title = null, + ImmutableArray tags = default, + CodeActionPriority priority = default, + ProjectId projectReferenceToAdd = null, + ProjectId portableExecutableReferenceProjectId = null, + string portableExecutableReferenceFilePathToAdd = null, + string assemblyReferenceAssemblyName = null, + string assemblyReferenceFullyQualifiedTypeName = null, + string packageSource = null, + string packageName = null, + string packageVersionOpt = null) { - [DataContract] - internal sealed class AddImportFixData( - AddImportFixKind kind, - ImmutableArray textChanges, - string title = null, - ImmutableArray tags = default, - CodeActionPriority priority = default, - ProjectId projectReferenceToAdd = null, - ProjectId portableExecutableReferenceProjectId = null, - string portableExecutableReferenceFilePathToAdd = null, - string assemblyReferenceAssemblyName = null, - string assemblyReferenceFullyQualifiedTypeName = null, - string packageSource = null, - string packageName = null, - string packageVersionOpt = null) - { - [DataMember(Order = 0)] - public AddImportFixKind Kind { get; } = kind; - - /// - /// Text changes to make to the document. Usually just the import to add. May also - /// include a change to the name node the feature was invoked on to fix the casing of it. - /// May be empty for fixes that don't need to add an import and only do something like - /// add a project/metadata reference. - /// - [DataMember(Order = 1)] - public readonly ImmutableArray TextChanges = textChanges; - - /// - /// String to display in the lightbulb menu. - /// - [DataMember(Order = 2)] - public readonly string Title = title; - - /// - /// Tags that control what glyph is displayed in the lightbulb menu. - /// - [DataMember(Order = 3)] - public readonly ImmutableArray Tags = tags; - - /// - /// The priority this item should have in the lightbulb list. - /// - [DataMember(Order = 4)] - public readonly CodeActionPriority Priority = priority; - - #region When adding P2P references. - - /// - /// The optional id for a we'd like to add a reference to. - /// - [DataMember(Order = 5)] - public readonly ProjectId ProjectReferenceToAdd = projectReferenceToAdd; - - #endregion - - #region When adding a metadata reference - - /// - /// If we're adding then this - /// is the id for the we can find that - /// referenced from. - /// - [DataMember(Order = 6)] - public readonly ProjectId PortableExecutableReferenceProjectId = portableExecutableReferenceProjectId; - - /// - /// If we want to add a metadata reference, this - /// is the for it. - /// - [DataMember(Order = 7)] - public readonly string PortableExecutableReferenceFilePathToAdd = portableExecutableReferenceFilePathToAdd; - - #endregion - - #region When adding an assembly reference - - [DataMember(Order = 8)] - public readonly string AssemblyReferenceAssemblyName = assemblyReferenceAssemblyName; - - [DataMember(Order = 9)] - public readonly string AssemblyReferenceFullyQualifiedTypeName = assemblyReferenceFullyQualifiedTypeName; - - #endregion - - #region When adding a package reference - - [DataMember(Order = 10)] - public readonly string PackageSource = packageSource; - - [DataMember(Order = 11)] - public readonly string PackageName = packageName; - - [DataMember(Order = 12)] - public readonly string PackageVersionOpt = packageVersionOpt; - - #endregion - - public static AddImportFixData CreateForProjectSymbol(ImmutableArray textChanges, string title, ImmutableArray tags, CodeActionPriority priority, ProjectId projectReferenceToAdd) - => new(AddImportFixKind.ProjectSymbol, - textChanges, - title: title, - tags: tags, - priority: priority, - projectReferenceToAdd: projectReferenceToAdd); - - public static AddImportFixData CreateForMetadataSymbol(ImmutableArray textChanges, string title, ImmutableArray tags, CodeActionPriority priority, ProjectId portableExecutableReferenceProjectId, string portableExecutableReferenceFilePathToAdd) - => new(AddImportFixKind.MetadataSymbol, - textChanges, - title: title, - tags: tags, - priority: priority, - portableExecutableReferenceProjectId: portableExecutableReferenceProjectId, - portableExecutableReferenceFilePathToAdd: portableExecutableReferenceFilePathToAdd); - - public static AddImportFixData CreateForReferenceAssemblySymbol(ImmutableArray textChanges, string title, string assemblyReferenceAssemblyName, string assemblyReferenceFullyQualifiedTypeName) - => new(AddImportFixKind.ReferenceAssemblySymbol, - textChanges, - title: title, - tags: WellKnownTagArrays.AddReference, - priority: CodeActionPriority.Low, - assemblyReferenceAssemblyName: assemblyReferenceAssemblyName, - assemblyReferenceFullyQualifiedTypeName: assemblyReferenceFullyQualifiedTypeName); - - public static AddImportFixData CreateForPackageSymbol(ImmutableArray textChanges, string packageSource, string packageName, string packageVersionOpt) - => new(AddImportFixKind.PackageSymbol, - textChanges, - packageSource: packageSource, - priority: CodeActionPriority.Low, - packageName: packageName, - packageVersionOpt: packageVersionOpt); - } + [DataMember(Order = 0)] + public AddImportFixKind Kind { get; } = kind; + + /// + /// Text changes to make to the document. Usually just the import to add. May also + /// include a change to the name node the feature was invoked on to fix the casing of it. + /// May be empty for fixes that don't need to add an import and only do something like + /// add a project/metadata reference. + /// + [DataMember(Order = 1)] + public readonly ImmutableArray TextChanges = textChanges; + + /// + /// String to display in the lightbulb menu. + /// + [DataMember(Order = 2)] + public readonly string Title = title; + + /// + /// Tags that control what glyph is displayed in the lightbulb menu. + /// + [DataMember(Order = 3)] + public readonly ImmutableArray Tags = tags; + + /// + /// The priority this item should have in the lightbulb list. + /// + [DataMember(Order = 4)] + public readonly CodeActionPriority Priority = priority; + + #region When adding P2P references. + + /// + /// The optional id for a we'd like to add a reference to. + /// + [DataMember(Order = 5)] + public readonly ProjectId ProjectReferenceToAdd = projectReferenceToAdd; + + #endregion + + #region When adding a metadata reference + + /// + /// If we're adding then this + /// is the id for the we can find that + /// referenced from. + /// + [DataMember(Order = 6)] + public readonly ProjectId PortableExecutableReferenceProjectId = portableExecutableReferenceProjectId; + + /// + /// If we want to add a metadata reference, this + /// is the for it. + /// + [DataMember(Order = 7)] + public readonly string PortableExecutableReferenceFilePathToAdd = portableExecutableReferenceFilePathToAdd; + + #endregion + + #region When adding an assembly reference + + [DataMember(Order = 8)] + public readonly string AssemblyReferenceAssemblyName = assemblyReferenceAssemblyName; + + [DataMember(Order = 9)] + public readonly string AssemblyReferenceFullyQualifiedTypeName = assemblyReferenceFullyQualifiedTypeName; + + #endregion + + #region When adding a package reference + + [DataMember(Order = 10)] + public readonly string PackageSource = packageSource; + + [DataMember(Order = 11)] + public readonly string PackageName = packageName; + + [DataMember(Order = 12)] + public readonly string PackageVersionOpt = packageVersionOpt; + + #endregion + + public static AddImportFixData CreateForProjectSymbol(ImmutableArray textChanges, string title, ImmutableArray tags, CodeActionPriority priority, ProjectId projectReferenceToAdd) + => new(AddImportFixKind.ProjectSymbol, + textChanges, + title: title, + tags: tags, + priority: priority, + projectReferenceToAdd: projectReferenceToAdd); + + public static AddImportFixData CreateForMetadataSymbol(ImmutableArray textChanges, string title, ImmutableArray tags, CodeActionPriority priority, ProjectId portableExecutableReferenceProjectId, string portableExecutableReferenceFilePathToAdd) + => new(AddImportFixKind.MetadataSymbol, + textChanges, + title: title, + tags: tags, + priority: priority, + portableExecutableReferenceProjectId: portableExecutableReferenceProjectId, + portableExecutableReferenceFilePathToAdd: portableExecutableReferenceFilePathToAdd); + + public static AddImportFixData CreateForReferenceAssemblySymbol(ImmutableArray textChanges, string title, string assemblyReferenceAssemblyName, string assemblyReferenceFullyQualifiedTypeName) + => new(AddImportFixKind.ReferenceAssemblySymbol, + textChanges, + title: title, + tags: WellKnownTagArrays.AddReference, + priority: CodeActionPriority.Low, + assemblyReferenceAssemblyName: assemblyReferenceAssemblyName, + assemblyReferenceFullyQualifiedTypeName: assemblyReferenceFullyQualifiedTypeName); + + public static AddImportFixData CreateForPackageSymbol(ImmutableArray textChanges, string packageSource, string packageName, string packageVersionOpt) + => new(AddImportFixKind.PackageSymbol, + textChanges, + packageSource: packageSource, + priority: CodeActionPriority.Low, + packageName: packageName, + packageVersionOpt: packageVersionOpt); } diff --git a/src/Features/Core/Portable/AddImport/AddImportFixKind.cs b/src/Features/Core/Portable/AddImport/AddImportFixKind.cs index ad040a8f0ea16..dd70bc465d5a0 100644 --- a/src/Features/Core/Portable/AddImport/AddImportFixKind.cs +++ b/src/Features/Core/Portable/AddImport/AddImportFixKind.cs @@ -2,28 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal enum AddImportFixKind { - internal enum AddImportFixKind - { - /// - /// Adding a project reference. - /// - ProjectSymbol, + /// + /// Adding a project reference. + /// + ProjectSymbol, - /// - /// Adding an assembly reference. - /// - MetadataSymbol, + /// + /// Adding an assembly reference. + /// + MetadataSymbol, - /// - /// Adding a package reference. - /// - PackageSymbol, + /// + /// Adding a package reference. + /// + PackageSymbol, - /// - /// Adding a framework reference assembly reference. - /// - ReferenceAssemblySymbol, - } + /// + /// Adding a framework reference assembly reference. + /// + ReferenceAssemblySymbol, } diff --git a/src/Features/Core/Portable/AddImport/CodeActions/AssemblyReferenceCodeAction.cs b/src/Features/Core/Portable/AddImport/CodeActions/AssemblyReferenceCodeAction.cs index 7bf7b13c219fc..878c5030c5afa 100644 --- a/src/Features/Core/Portable/AddImport/CodeActions/AssemblyReferenceCodeAction.cs +++ b/src/Features/Core/Portable/AddImport/CodeActions/AssemblyReferenceCodeAction.cs @@ -11,105 +11,104 @@ using Microsoft.CodeAnalysis.Host; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private sealed class AssemblyReferenceCodeAction : AddImportCodeAction { - private sealed class AssemblyReferenceCodeAction : AddImportCodeAction + /// + /// This code action only works by adding a reference. As such, it requires a non document change (and is + /// thus restricted in which hosts it can run). + /// + public AssemblyReferenceCodeAction( + Document originalDocument, + AddImportFixData fixData) + : base(originalDocument, fixData, RequiresNonDocumentChangeTags) + { + Contract.ThrowIfFalse(fixData.Kind == AddImportFixKind.ReferenceAssemblySymbol); + } + + protected override async Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) + => await ComputeOperationsAsync(isPreview: true, cancellationToken).ConfigureAwait(false); + + protected override Task> ComputeOperationsAsync(IProgress progress, CancellationToken cancellationToken) + => ComputeOperationsAsync(isPreview: false, cancellationToken); + + private async Task> ComputeOperationsAsync(bool isPreview, CancellationToken cancellationToken) + { + var newDocument = await GetUpdatedDocumentAsync(cancellationToken).ConfigureAwait(false); + var newProject = newDocument.Project; + + if (isPreview) + { + // If this is a preview, just return an ApplyChangesOperation for the updated document + var operation = new ApplyChangesOperation(newProject.Solution); + return [operation]; + } + else + { + // Otherwise return an operation that can apply the text changes and add the reference + var operation = new AddAssemblyReferenceCodeActionOperation( + FixData.AssemblyReferenceAssemblyName, + FixData.AssemblyReferenceFullyQualifiedTypeName, + newProject); + return [operation]; + } + } + + private sealed class AddAssemblyReferenceCodeActionOperation( + string assemblyReferenceAssemblyName, + string assemblyReferenceFullyQualifiedTypeName, + Project newProject) : CodeActionOperation { - /// - /// This code action only works by adding a reference. As such, it requires a non document change (and is - /// thus restricted in which hosts it can run). - /// - public AssemblyReferenceCodeAction( - Document originalDocument, - AddImportFixData fixData) - : base(originalDocument, fixData, RequiresNonDocumentChangeTags) + private readonly string _assemblyReferenceAssemblyName = assemblyReferenceAssemblyName; + private readonly string _assemblyReferenceFullyQualifiedTypeName = assemblyReferenceFullyQualifiedTypeName; + private readonly Project _newProject = newProject; + + internal override bool ApplyDuringTests => true; + + public override void Apply(Workspace workspace, CancellationToken cancellationToken) { - Contract.ThrowIfFalse(fixData.Kind == AddImportFixKind.ReferenceAssemblySymbol); + var operation = GetApplyChangesOperation(workspace); + if (operation is null) + return; + + operation.Apply(workspace, cancellationToken); } - protected override async Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) - => await ComputeOperationsAsync(isPreview: true, cancellationToken).ConfigureAwait(false); + internal override Task TryApplyAsync( + Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) + { + var operation = GetApplyChangesOperation(workspace); + if (operation is null) + return SpecializedTasks.False; - protected override Task> ComputeOperationsAsync(IProgress progress, CancellationToken cancellationToken) - => ComputeOperationsAsync(isPreview: false, cancellationToken); + return operation.TryApplyAsync(workspace, originalSolution, progressTracker, cancellationToken); + } - private async Task> ComputeOperationsAsync(bool isPreview, CancellationToken cancellationToken) + private ApplyChangesOperation? GetApplyChangesOperation(Workspace workspace) { - var newDocument = await GetUpdatedDocumentAsync(cancellationToken).ConfigureAwait(false); - var newProject = newDocument.Project; - - if (isPreview) - { - // If this is a preview, just return an ApplyChangesOperation for the updated document - var operation = new ApplyChangesOperation(newProject.Solution); - return [operation]; - } - else - { - // Otherwise return an operation that can apply the text changes and add the reference - var operation = new AddAssemblyReferenceCodeActionOperation( - FixData.AssemblyReferenceAssemblyName, - FixData.AssemblyReferenceFullyQualifiedTypeName, - newProject); - return [operation]; - } + var resolvedPath = ResolvePath(workspace); + if (string.IsNullOrWhiteSpace(resolvedPath)) + return null; + + var service = workspace.Services.GetRequiredService(); + var reference = service.GetReference(resolvedPath, MetadataReferenceProperties.Assembly); + var newProject = _newProject.WithMetadataReferences( + _newProject.MetadataReferences.Concat(reference)); + + return new ApplyChangesOperation(newProject.Solution); } - private sealed class AddAssemblyReferenceCodeActionOperation( - string assemblyReferenceAssemblyName, - string assemblyReferenceFullyQualifiedTypeName, - Project newProject) : CodeActionOperation + private string? ResolvePath(Workspace workspace) { - private readonly string _assemblyReferenceAssemblyName = assemblyReferenceAssemblyName; - private readonly string _assemblyReferenceFullyQualifiedTypeName = assemblyReferenceFullyQualifiedTypeName; - private readonly Project _newProject = newProject; - - internal override bool ApplyDuringTests => true; - - public override void Apply(Workspace workspace, CancellationToken cancellationToken) - { - var operation = GetApplyChangesOperation(workspace); - if (operation is null) - return; - - operation.Apply(workspace, cancellationToken); - } - - internal override Task TryApplyAsync( - Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) - { - var operation = GetApplyChangesOperation(workspace); - if (operation is null) - return SpecializedTasks.False; - - return operation.TryApplyAsync(workspace, originalSolution, progressTracker, cancellationToken); - } - - private ApplyChangesOperation? GetApplyChangesOperation(Workspace workspace) - { - var resolvedPath = ResolvePath(workspace); - if (string.IsNullOrWhiteSpace(resolvedPath)) - return null; - - var service = workspace.Services.GetRequiredService(); - var reference = service.GetReference(resolvedPath, MetadataReferenceProperties.Assembly); - var newProject = _newProject.WithMetadataReferences( - _newProject.MetadataReferences.Concat(reference)); - - return new ApplyChangesOperation(newProject.Solution); - } - - private string? ResolvePath(Workspace workspace) - { - var assemblyResolverService = workspace.Services.GetRequiredService(); - - return assemblyResolverService.ResolveAssemblyPath( - _newProject.Id, - _assemblyReferenceAssemblyName, - _assemblyReferenceFullyQualifiedTypeName); - } + var assemblyResolverService = workspace.Services.GetRequiredService(); + + return assemblyResolverService.ResolveAssemblyPath( + _newProject.Id, + _assemblyReferenceAssemblyName, + _assemblyReferenceFullyQualifiedTypeName); } } } diff --git a/src/Features/Core/Portable/AddImport/CodeActions/InstallWithPackageManagerCodeAction.cs b/src/Features/Core/Portable/AddImport/CodeActions/InstallWithPackageManagerCodeAction.cs index dcfe20bf6c949..1556893fb5bec 100644 --- a/src/Features/Core/Portable/AddImport/CodeActions/InstallWithPackageManagerCodeAction.cs +++ b/src/Features/Core/Portable/AddImport/CodeActions/InstallWithPackageManagerCodeAction.cs @@ -11,38 +11,37 @@ using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Packaging; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private sealed class InstallWithPackageManagerCodeAction( + IPackageInstallerService installerService, + string packageName) : CodeAction { - private sealed class InstallWithPackageManagerCodeAction( + private readonly IPackageInstallerService _installerService = installerService; + private readonly string _packageName = packageName; + + public override string Title => FeaturesResources.Install_with_package_manager; + + protected override Task> ComputeOperationsAsync( + IProgress progress, CancellationToken cancellationToken) + { + return Task.FromResult(ImmutableArray.Create( + new InstallWithPackageManagerCodeActionOperation(_installerService, _packageName))); + } + + private class InstallWithPackageManagerCodeActionOperation( IPackageInstallerService installerService, - string packageName) : CodeAction + string packageName) : CodeActionOperation { private readonly IPackageInstallerService _installerService = installerService; private readonly string _packageName = packageName; public override string Title => FeaturesResources.Install_with_package_manager; - protected override Task> ComputeOperationsAsync( - IProgress progress, CancellationToken cancellationToken) - { - return Task.FromResult(ImmutableArray.Create( - new InstallWithPackageManagerCodeActionOperation(_installerService, _packageName))); - } - - private class InstallWithPackageManagerCodeActionOperation( - IPackageInstallerService installerService, - string packageName) : CodeActionOperation - { - private readonly IPackageInstallerService _installerService = installerService; - private readonly string _packageName = packageName; - - public override string Title => FeaturesResources.Install_with_package_manager; - - public override void Apply(Workspace workspace, CancellationToken cancellationToken) - => _installerService.ShowManagePackagesDialog(_packageName); - } + public override void Apply(Workspace workspace, CancellationToken cancellationToken) + => _installerService.ShowManagePackagesDialog(_packageName); } } } diff --git a/src/Features/Core/Portable/AddImport/CodeActions/MetadataSymbolReferenceCodeAction.cs b/src/Features/Core/Portable/AddImport/CodeActions/MetadataSymbolReferenceCodeAction.cs index 2b7848a5d5f39..03332599440dc 100644 --- a/src/Features/Core/Portable/AddImport/CodeActions/MetadataSymbolReferenceCodeAction.cs +++ b/src/Features/Core/Portable/AddImport/CodeActions/MetadataSymbolReferenceCodeAction.cs @@ -9,31 +9,30 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private class MetadataSymbolReferenceCodeAction : SymbolReferenceCodeAction { - private class MetadataSymbolReferenceCodeAction : SymbolReferenceCodeAction + /// + /// This code action only works by adding a reference to a metadata dll. As such, it requires a non + /// document change (and is thus restricted in which hosts it can run). + /// + public MetadataSymbolReferenceCodeAction(Document originalDocument, AddImportFixData fixData) + : base(originalDocument, fixData, RequiresNonDocumentChangeTags) { - /// - /// This code action only works by adding a reference to a metadata dll. As such, it requires a non - /// document change (and is thus restricted in which hosts it can run). - /// - public MetadataSymbolReferenceCodeAction(Document originalDocument, AddImportFixData fixData) - : base(originalDocument, fixData, RequiresNonDocumentChangeTags) - { - Contract.ThrowIfFalse(fixData.Kind == AddImportFixKind.MetadataSymbol); - } + Contract.ThrowIfFalse(fixData.Kind == AddImportFixKind.MetadataSymbol); + } - protected override Task UpdateProjectAsync(Project project, bool isPreview, CancellationToken cancellationToken) - { - var projectWithReference = project.Solution.GetRequiredProject(FixData.PortableExecutableReferenceProjectId); - var reference = projectWithReference.MetadataReferences - .OfType() - .First(pe => pe.FilePath == FixData.PortableExecutableReferenceFilePathToAdd); + protected override Task UpdateProjectAsync(Project project, bool isPreview, CancellationToken cancellationToken) + { + var projectWithReference = project.Solution.GetRequiredProject(FixData.PortableExecutableReferenceProjectId); + var reference = projectWithReference.MetadataReferences + .OfType() + .First(pe => pe.FilePath == FixData.PortableExecutableReferenceFilePathToAdd); - return Task.FromResult(new ApplyChangesOperation(project.AddMetadataReference(reference).Solution)); - } + return Task.FromResult(new ApplyChangesOperation(project.AddMetadataReference(reference).Solution)); } } } diff --git a/src/Features/Core/Portable/AddImport/CodeActions/ProjectSymbolReferenceCodeAction.cs b/src/Features/Core/Portable/AddImport/CodeActions/ProjectSymbolReferenceCodeAction.cs index 7bcb8cb9b2459..bd351d10002d7 100644 --- a/src/Features/Core/Portable/AddImport/CodeActions/ProjectSymbolReferenceCodeAction.cs +++ b/src/Features/Core/Portable/AddImport/CodeActions/ProjectSymbolReferenceCodeAction.cs @@ -10,79 +10,78 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + /// + /// Code action for adding an import when we find a symbol in source in either our + /// starting project, or some other unreferenced project in the solution. If we + /// find a source symbol in a different project, we'll also add a p2p reference when + /// we apply the code action. + /// + private class ProjectSymbolReferenceCodeAction : SymbolReferenceCodeAction { /// - /// Code action for adding an import when we find a symbol in source in either our - /// starting project, or some other unreferenced project in the solution. If we - /// find a source symbol in a different project, we'll also add a p2p reference when - /// we apply the code action. + /// This code action may or may not add a project reference. If it does, it requires a non document change + /// (and is thus restricted in which hosts it can run). If it doesn't, it can run anywhere. /// - private class ProjectSymbolReferenceCodeAction : SymbolReferenceCodeAction + public ProjectSymbolReferenceCodeAction( + Document originalDocument, + AddImportFixData fixData) + : base(originalDocument, + fixData, + additionalTags: ShouldAddProjectReference(originalDocument, fixData) ? RequiresNonDocumentChangeTags : []) { - /// - /// This code action may or may not add a project reference. If it does, it requires a non document change - /// (and is thus restricted in which hosts it can run). If it doesn't, it can run anywhere. - /// - public ProjectSymbolReferenceCodeAction( - Document originalDocument, - AddImportFixData fixData) - : base(originalDocument, - fixData, - additionalTags: ShouldAddProjectReference(originalDocument, fixData) ? RequiresNonDocumentChangeTags : []) - { - Contract.ThrowIfFalse(fixData.Kind == AddImportFixKind.ProjectSymbol); - } + Contract.ThrowIfFalse(fixData.Kind == AddImportFixKind.ProjectSymbol); + } - private static bool ShouldAddProjectReference(Document originalDocument, AddImportFixData fixData) - => fixData.ProjectReferenceToAdd != null && fixData.ProjectReferenceToAdd != originalDocument.Project.Id; + private static bool ShouldAddProjectReference(Document originalDocument, AddImportFixData fixData) + => fixData.ProjectReferenceToAdd != null && fixData.ProjectReferenceToAdd != originalDocument.Project.Id; - protected override Task UpdateProjectAsync(Project project, bool isPreview, CancellationToken cancellationToken) - { - if (!ShouldAddProjectReference(this.OriginalDocument, this.FixData)) - return SpecializedTasks.Null(); - - var projectWithAddedReference = project.AddProjectReference(new ProjectReference(FixData.ProjectReferenceToAdd)); - var applyOperation = new ApplyChangesOperation(projectWithAddedReference.Solution); - if (isPreview) - { - return Task.FromResult(applyOperation); - } + protected override Task UpdateProjectAsync(Project project, bool isPreview, CancellationToken cancellationToken) + { + if (!ShouldAddProjectReference(this.OriginalDocument, this.FixData)) + return SpecializedTasks.Null(); - return Task.FromResult(new AddProjectReferenceCodeActionOperation(OriginalDocument.Project.Id, FixData.ProjectReferenceToAdd, applyOperation)); + var projectWithAddedReference = project.AddProjectReference(new ProjectReference(FixData.ProjectReferenceToAdd)); + var applyOperation = new ApplyChangesOperation(projectWithAddedReference.Solution); + if (isPreview) + { + return Task.FromResult(applyOperation); } - private sealed class AddProjectReferenceCodeActionOperation(ProjectId referencingProject, ProjectId referencedProject, ApplyChangesOperation applyOperation) : CodeActionOperation - { - private readonly ProjectId _referencingProject = referencingProject; - private readonly ProjectId _referencedProject = referencedProject; - private readonly ApplyChangesOperation _applyOperation = applyOperation; + return Task.FromResult(new AddProjectReferenceCodeActionOperation(OriginalDocument.Project.Id, FixData.ProjectReferenceToAdd, applyOperation)); + } + + private sealed class AddProjectReferenceCodeActionOperation(ProjectId referencingProject, ProjectId referencedProject, ApplyChangesOperation applyOperation) : CodeActionOperation + { + private readonly ProjectId _referencingProject = referencingProject; + private readonly ProjectId _referencedProject = referencedProject; + private readonly ApplyChangesOperation _applyOperation = applyOperation; - internal override bool ApplyDuringTests => true; + internal override bool ApplyDuringTests => true; - public override void Apply(Workspace workspace, CancellationToken cancellationToken) - { - if (!CanApply(workspace)) - return; + public override void Apply(Workspace workspace, CancellationToken cancellationToken) + { + if (!CanApply(workspace)) + return; - _applyOperation.Apply(workspace, cancellationToken); - } + _applyOperation.Apply(workspace, cancellationToken); + } - internal override Task TryApplyAsync( - Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) - { - if (!CanApply(workspace)) - return SpecializedTasks.False; + internal override Task TryApplyAsync( + Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) + { + if (!CanApply(workspace)) + return SpecializedTasks.False; - return _applyOperation.TryApplyAsync(workspace, originalSolution, progressTracker, cancellationToken); - } + return _applyOperation.TryApplyAsync(workspace, originalSolution, progressTracker, cancellationToken); + } - private bool CanApply(Workspace workspace) - { - return workspace.CanAddProjectReference(_referencingProject, _referencedProject); - } + private bool CanApply(Workspace workspace) + { + return workspace.CanAddProjectReference(_referencingProject, _referencedProject); } } } diff --git a/src/Features/Core/Portable/AddImport/CodeActions/SymbolReference.SymbolReferenceCodeAction.cs b/src/Features/Core/Portable/AddImport/CodeActions/SymbolReference.SymbolReferenceCodeAction.cs index 5b1ac5e9e0d19..c9b9e26deef5d 100644 --- a/src/Features/Core/Portable/AddImport/CodeActions/SymbolReference.SymbolReferenceCodeAction.cs +++ b/src/Features/Core/Portable/AddImport/CodeActions/SymbolReference.SymbolReferenceCodeAction.cs @@ -10,61 +10,60 @@ using Microsoft.CodeAnalysis.CodeActions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + /// + /// Code action we use when just adding a using, possibly with a project or + /// metadata reference. We don't use the standard code action types because + /// we want to do things like show a glyph if this will do more than just add + /// an import. + /// + private abstract class SymbolReferenceCodeAction : AddImportCodeAction { - /// - /// Code action we use when just adding a using, possibly with a project or - /// metadata reference. We don't use the standard code action types because - /// we want to do things like show a glyph if this will do more than just add - /// an import. - /// - private abstract class SymbolReferenceCodeAction : AddImportCodeAction + protected SymbolReferenceCodeAction( + Document originalDocument, + AddImportFixData fixData, + ImmutableArray additionalTags) + : base(originalDocument, fixData, additionalTags) { - protected SymbolReferenceCodeAction( - Document originalDocument, - AddImportFixData fixData, - ImmutableArray additionalTags) - : base(originalDocument, fixData, additionalTags) - { - } + } - protected override async Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) + protected override async Task> ComputePreviewOperationsAsync(CancellationToken cancellationToken) + { + var operation = await GetChangeSolutionOperationAsync(isPreview: true, cancellationToken).ConfigureAwait(false); + if (operation is null) { - var operation = await GetChangeSolutionOperationAsync(isPreview: true, cancellationToken).ConfigureAwait(false); - if (operation is null) - { - return []; - } - - return SpecializedCollections.SingletonEnumerable(operation); + return []; } - protected override async Task> ComputeOperationsAsync( - IProgress progress, CancellationToken cancellationToken) - { - var operation = await GetChangeSolutionOperationAsync(isPreview: false, cancellationToken).ConfigureAwait(false); - if (operation is null) - { - return []; - } + return SpecializedCollections.SingletonEnumerable(operation); + } - return [operation]; + protected override async Task> ComputeOperationsAsync( + IProgress progress, CancellationToken cancellationToken) + { + var operation = await GetChangeSolutionOperationAsync(isPreview: false, cancellationToken).ConfigureAwait(false); + if (operation is null) + { + return []; } - private async Task GetChangeSolutionOperationAsync(bool isPreview, CancellationToken cancellationToken) - { - var updatedDocument = await GetUpdatedDocumentAsync(cancellationToken).ConfigureAwait(false); + return [operation]; + } - // Defer to subtype to add any p2p or metadata refs as appropriate. If no changes to project references - // are necessary, the call to 'UpdateProjectAsync' will return null, in which case we fall back to just - // returning the updated document with its text changes. - var updatedProject = await UpdateProjectAsync(updatedDocument.Project, isPreview, cancellationToken).ConfigureAwait(false); - return updatedProject ?? new ApplyChangesOperation(updatedDocument.Project.Solution); - } + private async Task GetChangeSolutionOperationAsync(bool isPreview, CancellationToken cancellationToken) + { + var updatedDocument = await GetUpdatedDocumentAsync(cancellationToken).ConfigureAwait(false); - protected abstract Task UpdateProjectAsync(Project project, bool isPreview, CancellationToken cancellationToken); + // Defer to subtype to add any p2p or metadata refs as appropriate. If no changes to project references + // are necessary, the call to 'UpdateProjectAsync' will return null, in which case we fall back to just + // returning the updated document with its text changes. + var updatedProject = await UpdateProjectAsync(updatedDocument.Project, isPreview, cancellationToken).ConfigureAwait(false); + return updatedProject ?? new ApplyChangesOperation(updatedDocument.Project.Solution); } + + protected abstract Task UpdateProjectAsync(Project project, bool isPreview, CancellationToken cancellationToken); } } diff --git a/src/Features/Core/Portable/AddImport/IAddImportFeatureService.cs b/src/Features/Core/Portable/AddImport/IAddImportFeatureService.cs index 2f856e4edc061..e9d4a60566d14 100644 --- a/src/Features/Core/Portable/AddImport/IAddImportFeatureService.cs +++ b/src/Features/Core/Portable/AddImport/IAddImportFeatureService.cs @@ -13,50 +13,49 @@ using Microsoft.CodeAnalysis.SymbolSearch; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.AddImport -{ - [DataContract] - internal readonly record struct AddImportOptions( - [property: DataMember(Order = 0)] SymbolSearchOptions SearchOptions, - [property: DataMember(Order = 1)] CodeCleanupOptions CleanupOptions, - [property: DataMember(Order = 2)] bool HideAdvancedMembers); +namespace Microsoft.CodeAnalysis.AddImport; + +[DataContract] +internal readonly record struct AddImportOptions( + [property: DataMember(Order = 0)] SymbolSearchOptions SearchOptions, + [property: DataMember(Order = 1)] CodeCleanupOptions CleanupOptions, + [property: DataMember(Order = 2)] bool HideAdvancedMembers); - internal interface IAddImportFeatureService : ILanguageService - { - /// - /// Gets data for how to fix a particular id within the specified Document. - /// Useful when you do not have an instance of the diagnostic, such as when invoked as a remote service. - /// - Task> GetFixesAsync( - Document document, TextSpan span, string diagnosticId, int maxResults, - ISymbolSearchService symbolSearchService, AddImportOptions options, - ImmutableArray packageSources, CancellationToken cancellationToken); +internal interface IAddImportFeatureService : ILanguageService +{ + /// + /// Gets data for how to fix a particular id within the specified Document. + /// Useful when you do not have an instance of the diagnostic, such as when invoked as a remote service. + /// + Task> GetFixesAsync( + Document document, TextSpan span, string diagnosticId, int maxResults, + ISymbolSearchService symbolSearchService, AddImportOptions options, + ImmutableArray packageSources, CancellationToken cancellationToken); - /// - /// Gets data for how to fix a set of s within the specified Document. - /// The fix data can be used to create code actions that apply the fixes. - /// - Task Fixes)>> GetFixesForDiagnosticsAsync( - Document document, TextSpan span, ImmutableArray diagnostics, int maxResultsPerDiagnostic, - ISymbolSearchService symbolSearchService, AddImportOptions options, - ImmutableArray packageSources, CancellationToken cancellationToken); + /// + /// Gets data for how to fix a set of s within the specified Document. + /// The fix data can be used to create code actions that apply the fixes. + /// + Task Fixes)>> GetFixesForDiagnosticsAsync( + Document document, TextSpan span, ImmutableArray diagnostics, int maxResultsPerDiagnostic, + ISymbolSearchService symbolSearchService, AddImportOptions options, + ImmutableArray packageSources, CancellationToken cancellationToken); - /// - /// Gets code actions that, when applied, will fix the missing imports for the document using - /// the information from the provided fixes. - /// - ImmutableArray GetCodeActionsForFixes( - Document document, ImmutableArray fixes, - IPackageInstallerService? installerService, int maxResults); + /// + /// Gets code actions that, when applied, will fix the missing imports for the document using + /// the information from the provided fixes. + /// + ImmutableArray GetCodeActionsForFixes( + Document document, ImmutableArray fixes, + IPackageInstallerService? installerService, int maxResults); - /// - /// Gets data for how to fix a particular id within the specified Document. - /// Similar to - /// except it only returns fix data when there is a single using fix for a given span - /// - Task> GetUniqueFixesAsync( - Document document, TextSpan span, ImmutableArray diagnosticIds, - ISymbolSearchService symbolSearchService, AddImportOptions options, - ImmutableArray packageSources, CancellationToken cancellationToken); - } + /// + /// Gets data for how to fix a particular id within the specified Document. + /// Similar to + /// except it only returns fix data when there is a single using fix for a given span + /// + Task> GetUniqueFixesAsync( + Document document, TextSpan span, ImmutableArray diagnosticIds, + ISymbolSearchService symbolSearchService, AddImportOptions options, + ImmutableArray packageSources, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/AddImport/PackageSourceHelper.cs b/src/Features/Core/Portable/AddImport/PackageSourceHelper.cs index 0da4c90925106..f2d2aa0a86e75 100644 --- a/src/Features/Core/Portable/AddImport/PackageSourceHelper.cs +++ b/src/Features/Core/Portable/AddImport/PackageSourceHelper.cs @@ -7,45 +7,44 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Packaging; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal static class PackageSourceHelper { - internal static class PackageSourceHelper + private const string NugetOrg = "nuget.org"; + public const string NugetOrgSourceName = "::nuget::"; + + public static IEnumerable<(string sourceName, string sourceUrl)> GetPackageSources(ImmutableArray packageSources) { - private const string NugetOrg = "nuget.org"; - public const string NugetOrgSourceName = "::nuget::"; + // Package source names are user configurable, but various operations and background tasks process + // only the nuget source, so we ignore the user defined name for nuget.org so we can identify it later. - public static IEnumerable<(string sourceName, string sourceUrl)> GetPackageSources(ImmutableArray packageSources) + var foundNugetOrg = false; + foreach (var packageSource in packageSources) { - // Package source names are user configurable, but various operations and background tasks process - // only the nuget source, so we ignore the user defined name for nuget.org so we can identify it later. - - var foundNugetOrg = false; - foreach (var packageSource in packageSources) + // If the user has multiple sources from nuget.org, we only need one of them to be special + if (!foundNugetOrg && IsNugetOrg(packageSource.Source)) { - // If the user has multiple sources from nuget.org, we only need one of them to be special - if (!foundNugetOrg && IsNugetOrg(packageSource.Source)) - { - foundNugetOrg = true; - yield return (NugetOrgSourceName, packageSource.Source); - } - else - { - yield return (packageSource.Name, packageSource.Source); - } + foundNugetOrg = true; + yield return (NugetOrgSourceName, packageSource.Source); } - } - - private static bool IsNugetOrg(string sourceUrl) - { - if (!Uri.TryCreate(sourceUrl, UriKind.Absolute, out var uri)) + else { - return false; + yield return (packageSource.Name, packageSource.Source); } + } + } - // The default source url for nuget.org is "api.nuget.org" so the first case catches everything - // but the check is a little more expansive just to avoid a maintenance burden. - return uri.Host.EndsWith($".{NugetOrg}", StringComparison.OrdinalIgnoreCase) - || uri.Host.Equals(NugetOrg, StringComparison.OrdinalIgnoreCase); + private static bool IsNugetOrg(string sourceUrl) + { + if (!Uri.TryCreate(sourceUrl, UriKind.Absolute, out var uri)) + { + return false; } + + // The default source url for nuget.org is "api.nuget.org" so the first case catches everything + // but the check is a little more expansive just to avoid a maintenance burden. + return uri.Host.EndsWith($".{NugetOrg}", StringComparison.OrdinalIgnoreCase) + || uri.Host.Equals(NugetOrg, StringComparison.OrdinalIgnoreCase); } } diff --git a/src/Features/Core/Portable/AddImport/References/AssemblyReference.cs b/src/Features/Core/Portable/AddImport/References/AssemblyReference.cs index 5d7abea083f77..9619fcf4eaaec 100644 --- a/src/Features/Core/Portable/AddImport/References/AssemblyReference.cs +++ b/src/Features/Core/Portable/AddImport/References/AssemblyReference.cs @@ -11,39 +11,38 @@ using Microsoft.CodeAnalysis.SymbolSearch; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private partial class AssemblyReference( + AbstractAddImportFeatureService provider, + SearchResult searchResult, + ReferenceAssemblyWithTypeResult referenceAssemblyWithType) : Reference(provider, searchResult) { - private partial class AssemblyReference( - AbstractAddImportFeatureService provider, - SearchResult searchResult, - ReferenceAssemblyWithTypeResult referenceAssemblyWithType) : Reference(provider, searchResult) + private readonly ReferenceAssemblyWithTypeResult _referenceAssemblyWithType = referenceAssemblyWithType; + + public override async Task TryGetFixDataAsync( + Document document, SyntaxNode node, CodeCleanupOptions options, CancellationToken cancellationToken) + { + var textChanges = await GetTextChangesAsync(document, node, options, cancellationToken).ConfigureAwait(false); + + var title = $"{provider.GetDescription(SearchResult.NameParts)} ({string.Format(FeaturesResources.from_0, _referenceAssemblyWithType.AssemblyName)})"; + var fullyQualifiedTypeName = string.Join( + ".", _referenceAssemblyWithType.ContainingNamespaceNames.Concat(_referenceAssemblyWithType.TypeName)); + + return AddImportFixData.CreateForReferenceAssemblySymbol( + textChanges, title, _referenceAssemblyWithType.AssemblyName, fullyQualifiedTypeName); + } + + public override bool Equals(object obj) { - private readonly ReferenceAssemblyWithTypeResult _referenceAssemblyWithType = referenceAssemblyWithType; - - public override async Task TryGetFixDataAsync( - Document document, SyntaxNode node, CodeCleanupOptions options, CancellationToken cancellationToken) - { - var textChanges = await GetTextChangesAsync(document, node, options, cancellationToken).ConfigureAwait(false); - - var title = $"{provider.GetDescription(SearchResult.NameParts)} ({string.Format(FeaturesResources.from_0, _referenceAssemblyWithType.AssemblyName)})"; - var fullyQualifiedTypeName = string.Join( - ".", _referenceAssemblyWithType.ContainingNamespaceNames.Concat(_referenceAssemblyWithType.TypeName)); - - return AddImportFixData.CreateForReferenceAssemblySymbol( - textChanges, title, _referenceAssemblyWithType.AssemblyName, fullyQualifiedTypeName); - } - - public override bool Equals(object obj) - { - var reference = obj as AssemblyReference; - return base.Equals(obj) && - _referenceAssemblyWithType.AssemblyName == reference._referenceAssemblyWithType.AssemblyName; - } - - public override int GetHashCode() - => Hash.Combine(_referenceAssemblyWithType.AssemblyName, base.GetHashCode()); + var reference = obj as AssemblyReference; + return base.Equals(obj) && + _referenceAssemblyWithType.AssemblyName == reference._referenceAssemblyWithType.AssemblyName; } + + public override int GetHashCode() + => Hash.Combine(_referenceAssemblyWithType.AssemblyName, base.GetHashCode()); } } diff --git a/src/Features/Core/Portable/AddImport/References/MetadataSymbolReference.cs b/src/Features/Core/Portable/AddImport/References/MetadataSymbolReference.cs index 3a8be02b56ec2..08c275264d53b 100644 --- a/src/Features/Core/Portable/AddImport/References/MetadataSymbolReference.cs +++ b/src/Features/Core/Portable/AddImport/References/MetadataSymbolReference.cs @@ -15,66 +15,65 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private partial class MetadataSymbolReference( + AbstractAddImportFeatureService provider, + SymbolResult symbolResult, + ProjectId referenceProjectId, + PortableExecutableReference reference) : SymbolReference(provider, symbolResult) { - private partial class MetadataSymbolReference( - AbstractAddImportFeatureService provider, - SymbolResult symbolResult, - ProjectId referenceProjectId, - PortableExecutableReference reference) : SymbolReference(provider, symbolResult) - { - private readonly ProjectId _referenceProjectId = referenceProjectId; - private readonly PortableExecutableReference _reference = reference; + private readonly ProjectId _referenceProjectId = referenceProjectId; + private readonly PortableExecutableReference _reference = reference; - /// - /// If we're adding a metadata-reference, then we always offer to do the add, - /// even if there's an existing source-import in the file. - /// - protected override bool ShouldAddWithExistingImport(Document document) => true; + /// + /// If we're adding a metadata-reference, then we always offer to do the add, + /// even if there's an existing source-import in the file. + /// + protected override bool ShouldAddWithExistingImport(Document document) => true; - protected override (string description, bool hasExistingImport) GetDescription( - Document document, CodeCleanupOptions options, SyntaxNode node, - SemanticModel semanticModel, CancellationToken cancellationToken) + protected override (string description, bool hasExistingImport) GetDescription( + Document document, CodeCleanupOptions options, SyntaxNode node, + SemanticModel semanticModel, CancellationToken cancellationToken) + { + var (description, hasExistingImport) = base.GetDescription(document, options, node, semanticModel, cancellationToken); + if (description == null) { - var (description, hasExistingImport) = base.GetDescription(document, options, node, semanticModel, cancellationToken); - if (description == null) - { - return (null, false); - } - - return (string.Format(FeaturesResources.Add_reference_to_0, Path.GetFileName(_reference.FilePath)), - hasExistingImport); + return (null, false); } - protected override AddImportFixData GetFixData( - Document document, ImmutableArray textChanges, string description, - ImmutableArray tags, CodeActionPriority priority) - { - return AddImportFixData.CreateForMetadataSymbol( - textChanges, description, tags, priority, - _referenceProjectId, _reference.FilePath); - } + return (string.Format(FeaturesResources.Add_reference_to_0, Path.GetFileName(_reference.FilePath)), + hasExistingImport); + } - // Adding metadata references should be considered lower pri than anything else. - protected override CodeActionPriority GetPriority(Document document) - => CodeActionPriority.Low; + protected override AddImportFixData GetFixData( + Document document, ImmutableArray textChanges, string description, + ImmutableArray tags, CodeActionPriority priority) + { + return AddImportFixData.CreateForMetadataSymbol( + textChanges, description, tags, priority, + _referenceProjectId, _reference.FilePath); + } - protected override ImmutableArray GetTags(Document document) - => WellKnownTagArrays.AddReference; + // Adding metadata references should be considered lower pri than anything else. + protected override CodeActionPriority GetPriority(Document document) + => CodeActionPriority.Low; - public override bool Equals(object obj) - { - var reference = obj as MetadataSymbolReference; - return base.Equals(reference) && - StringComparer.OrdinalIgnoreCase.Equals(_reference.FilePath, reference._reference.FilePath); - } + protected override ImmutableArray GetTags(Document document) + => WellKnownTagArrays.AddReference; - public override int GetHashCode() - => Hash.Combine( - base.GetHashCode(), - StringComparer.OrdinalIgnoreCase.GetHashCode(_reference.FilePath)); + public override bool Equals(object obj) + { + var reference = obj as MetadataSymbolReference; + return base.Equals(reference) && + StringComparer.OrdinalIgnoreCase.Equals(_reference.FilePath, reference._reference.FilePath); } + + public override int GetHashCode() + => Hash.Combine( + base.GetHashCode(), + StringComparer.OrdinalIgnoreCase.GetHashCode(_reference.FilePath)); } } diff --git a/src/Features/Core/Portable/AddImport/References/PackageReference.cs b/src/Features/Core/Portable/AddImport/References/PackageReference.cs index 95ebbf84d0407..1d830b1479c41 100644 --- a/src/Features/Core/Portable/AddImport/References/PackageReference.cs +++ b/src/Features/Core/Portable/AddImport/References/PackageReference.cs @@ -10,44 +10,43 @@ using Microsoft.CodeAnalysis.CodeGeneration; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private partial class PackageReference( + AbstractAddImportFeatureService provider, + SearchResult searchResult, + string source, + string packageName, + string versionOpt) : Reference(provider, searchResult) { - private partial class PackageReference( - AbstractAddImportFeatureService provider, - SearchResult searchResult, - string source, - string packageName, - string versionOpt) : Reference(provider, searchResult) + private readonly string _source = source; + private readonly string _packageName = packageName; + private readonly string _versionOpt = versionOpt; + + public override async Task TryGetFixDataAsync( + Document document, SyntaxNode node, CodeCleanupOptions options, CancellationToken cancellationToken) + { + var textChanges = await GetTextChangesAsync( + document, node, options, cancellationToken).ConfigureAwait(false); + + return AddImportFixData.CreateForPackageSymbol( + textChanges, _source, _packageName, _versionOpt); + } + + public override bool Equals(object obj) + { + var reference = obj as PackageReference; + return base.Equals(obj) && + _packageName == reference._packageName && + _versionOpt == reference._versionOpt; + } + + public override int GetHashCode() { - private readonly string _source = source; - private readonly string _packageName = packageName; - private readonly string _versionOpt = versionOpt; - - public override async Task TryGetFixDataAsync( - Document document, SyntaxNode node, CodeCleanupOptions options, CancellationToken cancellationToken) - { - var textChanges = await GetTextChangesAsync( - document, node, options, cancellationToken).ConfigureAwait(false); - - return AddImportFixData.CreateForPackageSymbol( - textChanges, _source, _packageName, _versionOpt); - } - - public override bool Equals(object obj) - { - var reference = obj as PackageReference; - return base.Equals(obj) && - _packageName == reference._packageName && - _versionOpt == reference._versionOpt; - } - - public override int GetHashCode() - { - return Hash.Combine(_versionOpt, - Hash.Combine(_packageName, base.GetHashCode())); - } + return Hash.Combine(_versionOpt, + Hash.Combine(_packageName, base.GetHashCode())); } } } diff --git a/src/Features/Core/Portable/AddImport/References/ProjectSymbolReference.cs b/src/Features/Core/Portable/AddImport/References/ProjectSymbolReference.cs index 0fcc41aa5002d..6ee839f7567bd 100644 --- a/src/Features/Core/Portable/AddImport/References/ProjectSymbolReference.cs +++ b/src/Features/Core/Portable/AddImport/References/ProjectSymbolReference.cs @@ -14,100 +14,99 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + /// + /// Handles references to source symbols both from the current project the user is invoking + /// 'add-import' from, as well as symbols from other viable projects. + /// + /// In the case where the reference is from another project we put a glyph in the add using + /// light bulb and we say "(from ProjectXXX)" to make it clear that this will do more than + /// just add a using/import. + /// + private partial class ProjectSymbolReference( + AbstractAddImportFeatureService provider, + SymbolResult symbolResult, + Project project) : SymbolReference(provider, symbolResult) { - /// - /// Handles references to source symbols both from the current project the user is invoking - /// 'add-import' from, as well as symbols from other viable projects. - /// - /// In the case where the reference is from another project we put a glyph in the add using - /// light bulb and we say "(from ProjectXXX)" to make it clear that this will do more than - /// just add a using/import. - /// - private partial class ProjectSymbolReference( - AbstractAddImportFeatureService provider, - SymbolResult symbolResult, - Project project) : SymbolReference(provider, symbolResult) + private readonly Project _project = project; + + protected override ImmutableArray GetTags(Document document) { - private readonly Project _project = project; + return document.Project.Id == _project.Id + ? [] + : _project.Language == LanguageNames.CSharp + ? WellKnownTagArrays.CSharpProject + : _project.Language == LanguageNames.VisualBasic + ? WellKnownTagArrays.VisualBasicProject + : WellKnownTagArrays.AddReference; + } - protected override ImmutableArray GetTags(Document document) - { - return document.Project.Id == _project.Id - ? [] - : _project.Language == LanguageNames.CSharp - ? WellKnownTagArrays.CSharpProject - : _project.Language == LanguageNames.VisualBasic - ? WellKnownTagArrays.VisualBasicProject - : WellKnownTagArrays.AddReference; - } + /// + /// If we're adding a reference to another project, it's ok to still add, even if there + /// is an existing source-import in the file. We won't add the import, but we'll still + /// add the project-reference. + /// + protected override bool ShouldAddWithExistingImport(Document document) + => document.Project.Id != _project.Id; - /// - /// If we're adding a reference to another project, it's ok to still add, even if there - /// is an existing source-import in the file. We won't add the import, but we'll still - /// add the project-reference. - /// - protected override bool ShouldAddWithExistingImport(Document document) - => document.Project.Id != _project.Id; + protected override CodeActionPriority GetPriority(Document document) + { + // The only high priority fix we have is when we find a hit in our + // own project and we don't need to do a rename. Anything else (i.e. + // we need to add a project reference, or we need to rename) is low + // priority. - protected override CodeActionPriority GetPriority(Document document) + if (document.Project.Id == _project.Id) { - // The only high priority fix we have is when we find a hit in our - // own project and we don't need to do a rename. Anything else (i.e. - // we need to add a project reference, or we need to rename) is low - // priority. - - if (document.Project.Id == _project.Id) + if (SearchResult.DesiredNameMatchesSourceName(document)) { - if (SearchResult.DesiredNameMatchesSourceName(document)) - { - // Set priority to high so Add Imports will appear above other suggested actions - // https://github.com/dotnet/roslyn/pull/33214 - return CodeActionPriority.High; - } + // Set priority to high so Add Imports will appear above other suggested actions + // https://github.com/dotnet/roslyn/pull/33214 + return CodeActionPriority.High; } - - // This is a weaker match. This should be lower than all other fixes. - return CodeActionPriority.Low; - } - - protected override AddImportFixData GetFixData( - Document document, ImmutableArray textChanges, string description, - ImmutableArray tags, CodeActionPriority priority) - { - return AddImportFixData.CreateForProjectSymbol( - textChanges, description, tags, priority, _project.Id); } - protected override (string description, bool hasExistingImport) GetDescription( - Document document, CodeCleanupOptions options, SyntaxNode node, - SemanticModel semanticModel, CancellationToken cancellationToken) - { - var (description, hasExistingImport) = base.GetDescription(document, options, node, semanticModel, cancellationToken); - if (description == null) - { - return (null, false); - } - - var project = document.Project; - description = project.Id == _project.Id - ? description - : string.Format(FeaturesResources.Add_reference_to_0, _project.Name); + // This is a weaker match. This should be lower than all other fixes. + return CodeActionPriority.Low; + } - return (description, hasExistingImport); - } + protected override AddImportFixData GetFixData( + Document document, ImmutableArray textChanges, string description, + ImmutableArray tags, CodeActionPriority priority) + { + return AddImportFixData.CreateForProjectSymbol( + textChanges, description, tags, priority, _project.Id); + } - public override bool Equals(object obj) + protected override (string description, bool hasExistingImport) GetDescription( + Document document, CodeCleanupOptions options, SyntaxNode node, + SemanticModel semanticModel, CancellationToken cancellationToken) + { + var (description, hasExistingImport) = base.GetDescription(document, options, node, semanticModel, cancellationToken); + if (description == null) { - var reference = obj as ProjectSymbolReference; - return base.Equals(reference) && - _project.Id == reference._project.Id; + return (null, false); } - public override int GetHashCode() - => Hash.Combine(_project.Id, base.GetHashCode()); + var project = document.Project; + description = project.Id == _project.Id + ? description + : string.Format(FeaturesResources.Add_reference_to_0, _project.Name); + + return (description, hasExistingImport); } + + public override bool Equals(object obj) + { + var reference = obj as ProjectSymbolReference; + return base.Equals(reference) && + _project.Id == reference._project.Id; + } + + public override int GetHashCode() + => Hash.Combine(_project.Id, base.GetHashCode()); } } diff --git a/src/Features/Core/Portable/AddImport/References/Reference.cs b/src/Features/Core/Portable/AddImport/References/Reference.cs index f0af8c254426e..1ff9001385035 100644 --- a/src/Features/Core/Portable/AddImport/References/Reference.cs +++ b/src/Features/Core/Portable/AddImport/References/Reference.cs @@ -16,112 +16,111 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private abstract class Reference : IEquatable { - private abstract class Reference : IEquatable + protected readonly AbstractAddImportFeatureService provider; + public readonly SearchResult SearchResult; + + protected Reference( + AbstractAddImportFeatureService provider, + SearchResult searchResult) { - protected readonly AbstractAddImportFeatureService provider; - public readonly SearchResult SearchResult; + this.provider = provider; + SearchResult = searchResult; + } - protected Reference( - AbstractAddImportFeatureService provider, - SearchResult searchResult) + public int CompareTo(Document document, Reference other) + { + var diff = ComparerWithState.CompareTo(this, other, document, s_comparers); + if (diff != 0) { - this.provider = provider; - SearchResult = searchResult; + return diff; } - public int CompareTo(Document document, Reference other) + // Both our names need to change. Sort by the name we're + // changing to. + diff = StringComparer.OrdinalIgnoreCase.Compare( + SearchResult.DesiredName, other.SearchResult.DesiredName); + if (diff != 0) { - var diff = ComparerWithState.CompareTo(this, other, document, s_comparers); - if (diff != 0) - { - return diff; - } - - // Both our names need to change. Sort by the name we're - // changing to. - diff = StringComparer.OrdinalIgnoreCase.Compare( - SearchResult.DesiredName, other.SearchResult.DesiredName); - if (diff != 0) - { - return diff; - } - - // If the weights are the same and no names changed, just order - // them based on the namespace we're adding an import for. - return INamespaceOrTypeSymbolExtensions.CompareNameParts( - SearchResult.NameParts, other.SearchResult.NameParts, - placeSystemNamespaceFirst: true); + return diff; } - private static readonly ImmutableArray> s_comparers - = - [ - (r, d) => r.SearchResult.Weight, - (r, d) => !r.SearchResult.DesiredNameMatchesSourceName(d), - ]; + // If the weights are the same and no names changed, just order + // them based on the namespace we're adding an import for. + return INamespaceOrTypeSymbolExtensions.CompareNameParts( + SearchResult.NameParts, other.SearchResult.NameParts, + placeSystemNamespaceFirst: true); + } - public override bool Equals(object obj) - => Equals(obj as Reference); + private static readonly ImmutableArray> s_comparers + = + [ + (r, d) => r.SearchResult.Weight, + (r, d) => !r.SearchResult.DesiredNameMatchesSourceName(d), + ]; - public bool Equals(Reference other) - { - return other != null && - other.SearchResult.NameParts != null && - SearchResult.NameParts.SequenceEqual(other.SearchResult.NameParts); - } + public override bool Equals(object obj) + => Equals(obj as Reference); - public override int GetHashCode() - => Hash.CombineValues(SearchResult.NameParts); + public bool Equals(Reference other) + { + return other != null && + other.SearchResult.NameParts != null && + SearchResult.NameParts.SequenceEqual(other.SearchResult.NameParts); + } - protected async Task<(SyntaxNode, Document)> ReplaceNameNodeAsync( - SyntaxNode contextNode, Document document, CancellationToken cancellationToken) + public override int GetHashCode() + => Hash.CombineValues(SearchResult.NameParts); + + protected async Task<(SyntaxNode, Document)> ReplaceNameNodeAsync( + SyntaxNode contextNode, Document document, CancellationToken cancellationToken) + { + if (!SearchResult.DesiredNameDiffersFromSourceName()) { - if (!SearchResult.DesiredNameDiffersFromSourceName()) - { - return (contextNode, document); - } + return (contextNode, document); + } - var identifier = SearchResult.NameNode.GetFirstToken(); - var generator = SyntaxGenerator.GetGenerator(document); - var newIdentifier = generator.IdentifierName(SearchResult.DesiredName).GetFirstToken().WithTriviaFrom(identifier); - var annotation = new SyntaxAnnotation(); + var identifier = SearchResult.NameNode.GetFirstToken(); + var generator = SyntaxGenerator.GetGenerator(document); + var newIdentifier = generator.IdentifierName(SearchResult.DesiredName).GetFirstToken().WithTriviaFrom(identifier); + var annotation = new SyntaxAnnotation(); - var root = contextNode.SyntaxTree.GetRoot(cancellationToken); - root = root.ReplaceToken(identifier, newIdentifier.WithAdditionalAnnotations(annotation)); + var root = contextNode.SyntaxTree.GetRoot(cancellationToken); + root = root.ReplaceToken(identifier, newIdentifier.WithAdditionalAnnotations(annotation)); - var newDocument = document.WithSyntaxRoot(root); - var newRoot = await newDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newContextNode = newRoot.GetAnnotatedTokens(annotation).First().Parent; + var newDocument = document.WithSyntaxRoot(root); + var newRoot = await newDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newContextNode = newRoot.GetAnnotatedTokens(annotation).First().Parent; - return (newContextNode, newDocument); - } + return (newContextNode, newDocument); + } - public abstract Task TryGetFixDataAsync( - Document document, SyntaxNode node, CodeCleanupOptions options, CancellationToken cancellationToken); + public abstract Task TryGetFixDataAsync( + Document document, SyntaxNode node, CodeCleanupOptions options, CancellationToken cancellationToken); - protected async Task> GetTextChangesAsync( - Document document, SyntaxNode node, CodeCleanupOptions options, CancellationToken cancellationToken) - { - var originalDocument = document; + protected async Task> GetTextChangesAsync( + Document document, SyntaxNode node, CodeCleanupOptions options, CancellationToken cancellationToken) + { + var originalDocument = document; - (node, document) = await ReplaceNameNodeAsync( - node, document, cancellationToken).ConfigureAwait(false); + (node, document) = await ReplaceNameNodeAsync( + node, document, cancellationToken).ConfigureAwait(false); - var newDocument = await provider.AddImportAsync( - node, SearchResult.NameParts, document, options.AddImportOptions, cancellationToken).ConfigureAwait(false); + var newDocument = await provider.AddImportAsync( + node, SearchResult.NameParts, document, options.AddImportOptions, cancellationToken).ConfigureAwait(false); - var cleanedDocument = await CodeAction.CleanupDocumentAsync( - newDocument, options, cancellationToken).ConfigureAwait(false); + var cleanedDocument = await CodeAction.CleanupDocumentAsync( + newDocument, options, cancellationToken).ConfigureAwait(false); - var textChanges = await cleanedDocument.GetTextChangesAsync( - originalDocument, cancellationToken).ConfigureAwait(false); + var textChanges = await cleanedDocument.GetTextChangesAsync( + originalDocument, cancellationToken).ConfigureAwait(false); - return textChanges.ToImmutableArray(); - } + return textChanges.ToImmutableArray(); } } } diff --git a/src/Features/Core/Portable/AddImport/References/SymbolReference.cs b/src/Features/Core/Portable/AddImport/References/SymbolReference.cs index ee597afe72232..eb0e050735325 100644 --- a/src/Features/Core/Portable/AddImport/References/SymbolReference.cs +++ b/src/Features/Core/Portable/AddImport/References/SymbolReference.cs @@ -15,119 +15,118 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private abstract partial class SymbolReference( + AbstractAddImportFeatureService provider, + SymbolResult symbolResult) : Reference(provider, new SearchResult(symbolResult)) { - private abstract partial class SymbolReference( - AbstractAddImportFeatureService provider, - SymbolResult symbolResult) : Reference(provider, new SearchResult(symbolResult)) - { - public readonly SymbolResult SymbolResult = symbolResult; + public readonly SymbolResult SymbolResult = symbolResult; - protected abstract bool ShouldAddWithExistingImport(Document document); + protected abstract bool ShouldAddWithExistingImport(Document document); - protected abstract ImmutableArray GetTags(Document document); + protected abstract ImmutableArray GetTags(Document document); - public override bool Equals(object obj) + public override bool Equals(object obj) + { + var equals = base.Equals(obj); + if (!equals) { - var equals = base.Equals(obj); - if (!equals) - { - return false; - } - - var name1 = SymbolResult.DesiredName; - var name2 = (obj as SymbolReference)?.SymbolResult.DesiredName; - return StringComparer.Ordinal.Equals(name1, name2); + return false; } - public override int GetHashCode() - => Hash.Combine(SymbolResult.DesiredName, base.GetHashCode()); + var name1 = SymbolResult.DesiredName; + var name2 = (obj as SymbolReference)?.SymbolResult.DesiredName; + return StringComparer.Ordinal.Equals(name1, name2); + } + + public override int GetHashCode() + => Hash.Combine(SymbolResult.DesiredName, base.GetHashCode()); - private async Task> GetTextChangesAsync( - Document document, SyntaxNode contextNode, - CodeCleanupOptions options, bool hasExistingImport, - CancellationToken cancellationToken) + private async Task> GetTextChangesAsync( + Document document, SyntaxNode contextNode, + CodeCleanupOptions options, bool hasExistingImport, + CancellationToken cancellationToken) + { + // Defer to the language to add the actual import/using. + if (hasExistingImport) { - // Defer to the language to add the actual import/using. - if (hasExistingImport) - { - return []; - } + return []; + } - (var newContextNode, var newDocument) = await ReplaceNameNodeAsync( - contextNode, document, cancellationToken).ConfigureAwait(false); + (var newContextNode, var newDocument) = await ReplaceNameNodeAsync( + contextNode, document, cancellationToken).ConfigureAwait(false); - var updatedDocument = await provider.AddImportAsync( - newContextNode, SymbolResult.Symbol, newDocument, - options.AddImportOptions, cancellationToken).ConfigureAwait(false); + var updatedDocument = await provider.AddImportAsync( + newContextNode, SymbolResult.Symbol, newDocument, + options.AddImportOptions, cancellationToken).ConfigureAwait(false); - var cleanedDocument = await CodeAction.CleanupDocumentAsync( - updatedDocument, options, cancellationToken).ConfigureAwait(false); + var cleanedDocument = await CodeAction.CleanupDocumentAsync( + updatedDocument, options, cancellationToken).ConfigureAwait(false); - var textChanges = await cleanedDocument.GetTextChangesAsync( - document, cancellationToken).ConfigureAwait(false); + var textChanges = await cleanedDocument.GetTextChangesAsync( + document, cancellationToken).ConfigureAwait(false); - return textChanges.ToImmutableArray(); - } + return textChanges.ToImmutableArray(); + } - public sealed override async Task TryGetFixDataAsync( - Document document, SyntaxNode node, - CodeCleanupOptions options, CancellationToken cancellationToken) + public sealed override async Task TryGetFixDataAsync( + Document document, SyntaxNode node, + CodeCleanupOptions options, CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var (description, hasExistingImport) = GetDescription(document, options, node, semanticModel, cancellationToken); + if (description == null) { - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var (description, hasExistingImport) = GetDescription(document, options, node, semanticModel, cancellationToken); - if (description == null) - { - return null; - } + return null; + } - if (hasExistingImport && !ShouldAddWithExistingImport(document)) - { - return null; - } + if (hasExistingImport && !ShouldAddWithExistingImport(document)) + { + return null; + } - var isFuzzy = !SearchResult.DesiredNameMatchesSourceName(document); - var tags = GetTags(document); - if (isFuzzy) + var isFuzzy = !SearchResult.DesiredNameMatchesSourceName(document); + var tags = GetTags(document); + if (isFuzzy) + { + // The name is going to change. Make it clear in the description that this is + // going to happen. + description = $"{SearchResult.DesiredName} - {description}"; + + // if we were a fuzzy match, and we didn't have any glyph to show, then add the + // namespace-glyph to this item. This helps indicate that not only are we fixing + // the spelling of this name we are *also* adding a namespace. This helps as we + // have gotten feedback in the past that the 'using/import' addition was + // unexpected. + if (tags.IsDefaultOrEmpty) { - // The name is going to change. Make it clear in the description that this is - // going to happen. - description = $"{SearchResult.DesiredName} - {description}"; - - // if we were a fuzzy match, and we didn't have any glyph to show, then add the - // namespace-glyph to this item. This helps indicate that not only are we fixing - // the spelling of this name we are *also* adding a namespace. This helps as we - // have gotten feedback in the past that the 'using/import' addition was - // unexpected. - if (tags.IsDefaultOrEmpty) - { - tags = WellKnownTagArrays.Namespace; - } + tags = WellKnownTagArrays.Namespace; } + } - var textChanges = await GetTextChangesAsync( - document, node, options, hasExistingImport, cancellationToken).ConfigureAwait(false); + var textChanges = await GetTextChangesAsync( + document, node, options, hasExistingImport, cancellationToken).ConfigureAwait(false); - return GetFixData( - document, textChanges, description, - tags, GetPriority(document)); - } + return GetFixData( + document, textChanges, description, + tags, GetPriority(document)); + } - protected abstract AddImportFixData GetFixData( - Document document, ImmutableArray textChanges, - string description, ImmutableArray tags, CodeActionPriority priority); + protected abstract AddImportFixData GetFixData( + Document document, ImmutableArray textChanges, + string description, ImmutableArray tags, CodeActionPriority priority); - protected abstract CodeActionPriority GetPriority(Document document); + protected abstract CodeActionPriority GetPriority(Document document); - protected virtual (string description, bool hasExistingImport) GetDescription( - Document document, CodeCleanupOptions options, SyntaxNode node, - SemanticModel semanticModel, CancellationToken cancellationToken) - { - return provider.GetDescription( - document, options.AddImportOptions, SymbolResult.Symbol, semanticModel, node, cancellationToken); - } + protected virtual (string description, bool hasExistingImport) GetDescription( + Document document, CodeCleanupOptions options, SyntaxNode node, + SemanticModel semanticModel, CancellationToken cancellationToken) + { + return provider.GetDescription( + document, options.AddImportOptions, SymbolResult.Symbol, semanticModel, node, cancellationToken); } } } diff --git a/src/Features/Core/Portable/AddImport/Remote/AbstractAddImportFeatureService_Remote.cs b/src/Features/Core/Portable/AddImport/Remote/AbstractAddImportFeatureService_Remote.cs index 86d7d84e27023..bc76a0baa2930 100644 --- a/src/Features/Core/Portable/AddImport/Remote/AbstractAddImportFeatureService_Remote.cs +++ b/src/Features/Core/Portable/AddImport/Remote/AbstractAddImportFeatureService_Remote.cs @@ -12,37 +12,36 @@ using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.SymbolSearch; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +/// +/// Used to supply the OOP server a callback that it can use to search for ReferenceAssemblies or +/// nuget packages. We can't necessarily do that search directly in the OOP server as our +/// 'SymbolSearchEngine' may actually be running in a *different* process (there is no guarantee +/// that all remote work happens in the same process). +/// +/// This does mean, currently, that when we call over to OOP to do a search, it will bounce +/// back to VS, which will then bounce back out to OOP to perform the Nuget/ReferenceAssembly +/// portion of the search. Ideally we could keep this all OOP. +/// +[ExportRemoteServiceCallbackDispatcher(typeof(IRemoteMissingImportDiscoveryService)), Shared] +internal sealed class RemoteMissingImportDiscoveryServiceCallbackDispatcher : RemoteServiceCallbackDispatcher, IRemoteMissingImportDiscoveryService.ICallback { - /// - /// Used to supply the OOP server a callback that it can use to search for ReferenceAssemblies or - /// nuget packages. We can't necessarily do that search directly in the OOP server as our - /// 'SymbolSearchEngine' may actually be running in a *different* process (there is no guarantee - /// that all remote work happens in the same process). - /// - /// This does mean, currently, that when we call over to OOP to do a search, it will bounce - /// back to VS, which will then bounce back out to OOP to perform the Nuget/ReferenceAssembly - /// portion of the search. Ideally we could keep this all OOP. - /// - [ExportRemoteServiceCallbackDispatcher(typeof(IRemoteMissingImportDiscoveryService)), Shared] - internal sealed class RemoteMissingImportDiscoveryServiceCallbackDispatcher : RemoteServiceCallbackDispatcher, IRemoteMissingImportDiscoveryService.ICallback + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RemoteMissingImportDiscoveryServiceCallbackDispatcher() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public RemoteMissingImportDiscoveryServiceCallbackDispatcher() - { - } + } - private ISymbolSearchService GetService(RemoteServiceCallbackId callbackId) - => (ISymbolSearchService)GetCallback(callbackId); + private ISymbolSearchService GetService(RemoteServiceCallbackId callbackId) + => (ISymbolSearchService)GetCallback(callbackId); - public ValueTask> FindPackagesWithTypeAsync(RemoteServiceCallbackId callbackId, string source, string name, int arity, CancellationToken cancellationToken) - => GetService(callbackId).FindPackagesWithTypeAsync(source, name, arity, cancellationToken); + public ValueTask> FindPackagesWithTypeAsync(RemoteServiceCallbackId callbackId, string source, string name, int arity, CancellationToken cancellationToken) + => GetService(callbackId).FindPackagesWithTypeAsync(source, name, arity, cancellationToken); - public ValueTask> FindPackagesWithAssemblyAsync(RemoteServiceCallbackId callbackId, string source, string name, CancellationToken cancellationToken) - => GetService(callbackId).FindPackagesWithAssemblyAsync(source, name, cancellationToken); + public ValueTask> FindPackagesWithAssemblyAsync(RemoteServiceCallbackId callbackId, string source, string name, CancellationToken cancellationToken) + => GetService(callbackId).FindPackagesWithAssemblyAsync(source, name, cancellationToken); - public ValueTask> FindReferenceAssembliesWithTypeAsync(RemoteServiceCallbackId callbackId, string name, int arity, CancellationToken cancellationToken) - => GetService(callbackId).FindReferenceAssembliesWithTypeAsync(name, arity, cancellationToken); - } + public ValueTask> FindReferenceAssembliesWithTypeAsync(RemoteServiceCallbackId callbackId, string name, int arity, CancellationToken cancellationToken) + => GetService(callbackId).FindReferenceAssembliesWithTypeAsync(name, arity, cancellationToken); } diff --git a/src/Features/Core/Portable/AddImport/Remote/IRemoteMissingImportDiscoveryService.cs b/src/Features/Core/Portable/AddImport/Remote/IRemoteMissingImportDiscoveryService.cs index 09c650230f4db..d4f97c2d00a12 100644 --- a/src/Features/Core/Portable/AddImport/Remote/IRemoteMissingImportDiscoveryService.cs +++ b/src/Features/Core/Portable/AddImport/Remote/IRemoteMissingImportDiscoveryService.cs @@ -12,23 +12,22 @@ using Microsoft.CodeAnalysis.SymbolSearch; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal interface IRemoteMissingImportDiscoveryService { - internal interface IRemoteMissingImportDiscoveryService + internal interface ICallback { - internal interface ICallback - { - ValueTask> FindPackagesWithTypeAsync(RemoteServiceCallbackId callbackId, string source, string name, int arity, CancellationToken cancellationToken); - ValueTask> FindPackagesWithAssemblyAsync(RemoteServiceCallbackId callbackId, string source, string name, CancellationToken cancellationToken); - ValueTask> FindReferenceAssembliesWithTypeAsync(RemoteServiceCallbackId callbackId, string name, int arity, CancellationToken cancellationToken); - } + ValueTask> FindPackagesWithTypeAsync(RemoteServiceCallbackId callbackId, string source, string name, int arity, CancellationToken cancellationToken); + ValueTask> FindPackagesWithAssemblyAsync(RemoteServiceCallbackId callbackId, string source, string name, CancellationToken cancellationToken); + ValueTask> FindReferenceAssembliesWithTypeAsync(RemoteServiceCallbackId callbackId, string name, int arity, CancellationToken cancellationToken); + } - ValueTask> GetFixesAsync( - Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DocumentId documentId, TextSpan span, string diagnosticId, int maxResults, - AddImportOptions options, ImmutableArray packageSources, CancellationToken cancellationToken); + ValueTask> GetFixesAsync( + Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DocumentId documentId, TextSpan span, string diagnosticId, int maxResults, + AddImportOptions options, ImmutableArray packageSources, CancellationToken cancellationToken); - ValueTask> GetUniqueFixesAsync( - Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DocumentId id, TextSpan span, ImmutableArray diagnosticIds, - AddImportOptions options, ImmutableArray packageSources, CancellationToken cancellationToken); - } + ValueTask> GetUniqueFixesAsync( + Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DocumentId id, TextSpan span, ImmutableArray diagnosticIds, + AddImportOptions options, ImmutableArray packageSources, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/AddImport/SearchScopes/AllSymbolsProjectSearchScope.cs b/src/Features/Core/Portable/AddImport/SearchScopes/AllSymbolsProjectSearchScope.cs index 42ad0b31d7207..7248a18c3153d 100644 --- a/src/Features/Core/Portable/AddImport/SearchScopes/AllSymbolsProjectSearchScope.cs +++ b/src/Features/Core/Portable/AddImport/SearchScopes/AllSymbolsProjectSearchScope.cs @@ -7,28 +7,27 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.FindSymbols; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + /// + /// SearchScope used for searching *all* the symbols contained within a project/compilation. + /// i.e. the symbols created from source *and* symbols from references (both project and + /// metadata). + /// + private class AllSymbolsProjectSearchScope( + AbstractAddImportFeatureService provider, + Project project, + bool exact) : ProjectSearchScope(provider, project, exact) { - /// - /// SearchScope used for searching *all* the symbols contained within a project/compilation. - /// i.e. the symbols created from source *and* symbols from references (both project and - /// metadata). - /// - private class AllSymbolsProjectSearchScope( - AbstractAddImportFeatureService provider, - Project project, - bool exact) : ProjectSearchScope(provider, project, exact) + protected override async Task> FindDeclarationsAsync( + SymbolFilter filter, SearchQuery searchQuery, CancellationToken cancellationToken) { - protected override async Task> FindDeclarationsAsync( - SymbolFilter filter, SearchQuery searchQuery, CancellationToken cancellationToken) - { - var declarations = await DeclarationFinder.FindAllDeclarationsWithNormalQueryAsync( - _project, searchQuery, filter, cancellationToken).ConfigureAwait(false); + var declarations = await DeclarationFinder.FindAllDeclarationsWithNormalQueryAsync( + _project, searchQuery, filter, cancellationToken).ConfigureAwait(false); - return declarations; - } + return declarations; } } } diff --git a/src/Features/Core/Portable/AddImport/SearchScopes/MetadataSymbolsSearchScope.cs b/src/Features/Core/Portable/AddImport/SearchScopes/MetadataSymbolsSearchScope.cs index a67693ca01db7..e0663ad885a0d 100644 --- a/src/Features/Core/Portable/AddImport/SearchScopes/MetadataSymbolsSearchScope.cs +++ b/src/Features/Core/Portable/AddImport/SearchScopes/MetadataSymbolsSearchScope.cs @@ -8,43 +8,42 @@ using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.FindSymbols.SymbolTree; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private class MetadataSymbolsSearchScope( + AbstractAddImportFeatureService provider, + Project assemblyProject, + IAssemblySymbol assembly, + PortableExecutableReference metadataReference, + bool exact) : SearchScope(provider, exact) { - private class MetadataSymbolsSearchScope( - AbstractAddImportFeatureService provider, - Project assemblyProject, - IAssemblySymbol assembly, - PortableExecutableReference metadataReference, - bool exact) : SearchScope(provider, exact) - { - private readonly Project _assemblyProject = assemblyProject; - private readonly IAssemblySymbol _assembly = assembly; - private readonly PortableExecutableReference _metadataReference = metadataReference; + private readonly Project _assemblyProject = assemblyProject; + private readonly IAssemblySymbol _assembly = assembly; + private readonly PortableExecutableReference _metadataReference = metadataReference; - public override SymbolReference CreateReference(SymbolResult searchResult) - { - return new MetadataSymbolReference( - provider, - searchResult.WithSymbol(searchResult.Symbol), - _assemblyProject.Id, - _metadataReference); - } + public override SymbolReference CreateReference(SymbolResult searchResult) + { + return new MetadataSymbolReference( + provider, + searchResult.WithSymbol(searchResult.Symbol), + _assemblyProject.Id, + _metadataReference); + } - protected override async Task> FindDeclarationsAsync( - SymbolFilter filter, SearchQuery searchQuery, CancellationToken cancellationToken) - { - var service = _assemblyProject.Solution.Services.GetRequiredService(); - var info = await service.TryGetPotentiallyStaleMetadataSymbolTreeInfoAsync(_assemblyProject, _metadataReference, cancellationToken).ConfigureAwait(false); - if (info == null) - return []; + protected override async Task> FindDeclarationsAsync( + SymbolFilter filter, SearchQuery searchQuery, CancellationToken cancellationToken) + { + var service = _assemblyProject.Solution.Services.GetRequiredService(); + var info = await service.TryGetPotentiallyStaleMetadataSymbolTreeInfoAsync(_assemblyProject, _metadataReference, cancellationToken).ConfigureAwait(false); + if (info == null) + return []; - var declarations = await info.FindAsync( - searchQuery, _assembly, filter, cancellationToken).ConfigureAwait(false); + var declarations = await info.FindAsync( + searchQuery, _assembly, filter, cancellationToken).ConfigureAwait(false); - return declarations; - } + return declarations; } } } diff --git a/src/Features/Core/Portable/AddImport/SearchScopes/ProjectSearchScope.cs b/src/Features/Core/Portable/AddImport/SearchScopes/ProjectSearchScope.cs index 1011a9a00652e..11d34bf3904c2 100644 --- a/src/Features/Core/Portable/AddImport/SearchScopes/ProjectSearchScope.cs +++ b/src/Features/Core/Portable/AddImport/SearchScopes/ProjectSearchScope.cs @@ -4,29 +4,28 @@ using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private abstract class ProjectSearchScope : SearchScope { - private abstract class ProjectSearchScope : SearchScope - { - protected readonly Project _project; + protected readonly Project _project; - public ProjectSearchScope( - AbstractAddImportFeatureService provider, - Project project, - bool exact) - : base(provider, exact) - { - Contract.ThrowIfFalse(project.SupportsCompilation); - _project = project; - } + public ProjectSearchScope( + AbstractAddImportFeatureService provider, + Project project, + bool exact) + : base(provider, exact) + { + Contract.ThrowIfFalse(project.SupportsCompilation); + _project = project; + } - public override SymbolReference CreateReference(SymbolResult symbol) - { - return new ProjectSymbolReference( - provider, symbol.WithSymbol(symbol.Symbol), _project); - } + public override SymbolReference CreateReference(SymbolResult symbol) + { + return new ProjectSymbolReference( + provider, symbol.WithSymbol(symbol.Symbol), _project); } } } diff --git a/src/Features/Core/Portable/AddImport/SearchScopes/SearchScope.cs b/src/Features/Core/Portable/AddImport/SearchScopes/SearchScope.cs index 95a7e956417f9..1cf0e82db5e23 100644 --- a/src/Features/Core/Portable/AddImport/SearchScopes/SearchScope.cs +++ b/src/Features/Core/Portable/AddImport/SearchScopes/SearchScope.cs @@ -11,67 +11,66 @@ using Microsoft.CodeAnalysis.FindSymbols; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + /// + /// SearchScope is used to control where the + /// searches. We search different scopes in different ways. For example we use + /// SymbolTreeInfos to search unreferenced projects and metadata dlls. However, + /// for the current project we're editing we defer to the compiler to do the + /// search. + /// + private abstract class SearchScope { - /// - /// SearchScope is used to control where the - /// searches. We search different scopes in different ways. For example we use - /// SymbolTreeInfos to search unreferenced projects and metadata dlls. However, - /// for the current project we're editing we defer to the compiler to do the - /// search. - /// - private abstract class SearchScope - { - public readonly bool Exact; - protected readonly AbstractAddImportFeatureService provider; + public readonly bool Exact; + protected readonly AbstractAddImportFeatureService provider; - protected SearchScope(AbstractAddImportFeatureService provider, bool exact) - { - this.provider = provider; - Exact = exact; - } + protected SearchScope(AbstractAddImportFeatureService provider, bool exact) + { + this.provider = provider; + Exact = exact; + } - protected abstract Task> FindDeclarationsAsync(SymbolFilter filter, SearchQuery query, CancellationToken cancellationToken); + protected abstract Task> FindDeclarationsAsync(SymbolFilter filter, SearchQuery query, CancellationToken cancellationToken); - public abstract SymbolReference CreateReference(SymbolResult symbol) where T : INamespaceOrTypeSymbol; + public abstract SymbolReference CreateReference(SymbolResult symbol) where T : INamespaceOrTypeSymbol; - public async Task>> FindDeclarationsAsync( - string name, TSimpleNameSyntax nameNode, SymbolFilter filter, CancellationToken cancellationToken) + public async Task>> FindDeclarationsAsync( + string name, TSimpleNameSyntax nameNode, SymbolFilter filter, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (name != null && string.IsNullOrWhiteSpace(name)) { - cancellationToken.ThrowIfCancellationRequested(); - if (name != null && string.IsNullOrWhiteSpace(name)) - { - return []; - } + return []; + } - using var query = Exact ? SearchQuery.Create(name, ignoreCase: true) : SearchQuery.CreateFuzzy(name); - var symbols = await FindDeclarationsAsync(filter, query, cancellationToken).ConfigureAwait(false); + using var query = Exact ? SearchQuery.Create(name, ignoreCase: true) : SearchQuery.CreateFuzzy(name); + var symbols = await FindDeclarationsAsync(filter, query, cancellationToken).ConfigureAwait(false); - if (Exact) - { - // We did an exact, case insensitive, search. Case sensitive matches should - // be preferred though over insensitive ones. - return symbols.SelectAsArray(s => - SymbolResult.Create(s.Name, nameNode, s, weight: s.Name == name ? 0 : 1)); - } + if (Exact) + { + // We did an exact, case insensitive, search. Case sensitive matches should + // be preferred though over insensitive ones. + return symbols.SelectAsArray(s => + SymbolResult.Create(s.Name, nameNode, s, weight: s.Name == name ? 0 : 1)); + } - // TODO(cyrusn): It's a shame we have to compute this twice. However, there's no - // great way to store the original value we compute because it happens deep in the - // compiler bowels when we call FindDeclarations. - using var similarityChecker = new WordSimilarityChecker(name, substringsAreSimilar: false); + // TODO(cyrusn): It's a shame we have to compute this twice. However, there's no + // great way to store the original value we compute because it happens deep in the + // compiler bowels when we call FindDeclarations. + using var similarityChecker = new WordSimilarityChecker(name, substringsAreSimilar: false); - var result = symbols.SelectAsArray(s => - { - var areSimilar = similarityChecker.AreSimilar(s.Name, out var matchCost); + var result = symbols.SelectAsArray(s => + { + var areSimilar = similarityChecker.AreSimilar(s.Name, out var matchCost); - Debug.Assert(areSimilar); - return SymbolResult.Create(s.Name, nameNode, s, matchCost); - }); + Debug.Assert(areSimilar); + return SymbolResult.Create(s.Name, nameNode, s, matchCost); + }); - return result; - } + return result; } } } diff --git a/src/Features/Core/Portable/AddImport/SearchScopes/SourceSymbolsProjectSearchScope.cs b/src/Features/Core/Portable/AddImport/SearchScopes/SourceSymbolsProjectSearchScope.cs index 228abe56095ea..ba04099888007 100644 --- a/src/Features/Core/Portable/AddImport/SearchScopes/SourceSymbolsProjectSearchScope.cs +++ b/src/Features/Core/Portable/AddImport/SearchScopes/SourceSymbolsProjectSearchScope.cs @@ -11,49 +11,48 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + /// + /// SearchScope used for searching *only* the source symbols contained within a project/compilation. + /// i.e. symbols from metadata will not be searched. + /// + private class SourceSymbolsProjectSearchScope( + AbstractAddImportFeatureService provider, + ConcurrentDictionary> projectToAssembly, + Project project, bool ignoreCase) : ProjectSearchScope(provider, project, ignoreCase) { - /// - /// SearchScope used for searching *only* the source symbols contained within a project/compilation. - /// i.e. symbols from metadata will not be searched. - /// - private class SourceSymbolsProjectSearchScope( - AbstractAddImportFeatureService provider, - ConcurrentDictionary> projectToAssembly, - Project project, bool ignoreCase) : ProjectSearchScope(provider, project, ignoreCase) - { - private readonly ConcurrentDictionary> _projectToAssembly = projectToAssembly; + private readonly ConcurrentDictionary> _projectToAssembly = projectToAssembly; - protected override async Task> FindDeclarationsAsync( - SymbolFilter filter, SearchQuery searchQuery, CancellationToken cancellationToken) + protected override async Task> FindDeclarationsAsync( + SymbolFilter filter, SearchQuery searchQuery, CancellationToken cancellationToken) + { + var service = _project.Solution.Services.GetRequiredService(); + var info = await service.TryGetPotentiallyStaleSourceSymbolTreeInfoAsync(_project, cancellationToken).ConfigureAwait(false); + if (info == null) { - var service = _project.Solution.Services.GetRequiredService(); - var info = await service.TryGetPotentiallyStaleSourceSymbolTreeInfoAsync(_project, cancellationToken).ConfigureAwait(false); - if (info == null) - { - // Looks like there was nothing in the cache. Return no results for now. - return []; - } - - // Don't create the assembly until it is actually needed by the SymbolTreeInfo.FindAsync - // code. Creating the assembly can be costly and we want to avoid it until it is actually - // needed. - var lazyAssembly = _projectToAssembly.GetOrAdd(_project, CreateLazyAssembly); - - var declarations = await info.FindAsync( - searchQuery, lazyAssembly, filter, cancellationToken).ConfigureAwait(false); - - return declarations; - - static AsyncLazy CreateLazyAssembly(Project project) - => new(async c => - { - var compilation = await project.GetRequiredCompilationAsync(c).ConfigureAwait(false); - return compilation.Assembly; - }); + // Looks like there was nothing in the cache. Return no results for now. + return []; } + + // Don't create the assembly until it is actually needed by the SymbolTreeInfo.FindAsync + // code. Creating the assembly can be costly and we want to avoid it until it is actually + // needed. + var lazyAssembly = _projectToAssembly.GetOrAdd(_project, CreateLazyAssembly); + + var declarations = await info.FindAsync( + searchQuery, lazyAssembly, filter, cancellationToken).ConfigureAwait(false); + + return declarations; + + static AsyncLazy CreateLazyAssembly(Project project) + => new(async c => + { + var compilation = await project.GetRequiredCompilationAsync(c).ConfigureAwait(false); + return compilation.Assembly; + }); } } } diff --git a/src/Features/Core/Portable/AddImport/SymbolReferenceFinder.cs b/src/Features/Core/Portable/AddImport/SymbolReferenceFinder.cs index b453ca699b1b6..57508f95172f4 100644 --- a/src/Features/Core/Portable/AddImport/SymbolReferenceFinder.cs +++ b/src/Features/Core/Portable/AddImport/SymbolReferenceFinder.cs @@ -21,595 +21,594 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.Shared.Utilities.EditorBrowsableHelpers; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private partial class SymbolReferenceFinder { - private partial class SymbolReferenceFinder + private const string AttributeSuffix = nameof(Attribute); + + private readonly string _diagnosticId; + private readonly Document _document; + private readonly SemanticModel _semanticModel; + + private readonly ISet _namespacesInScope; + private readonly ISyntaxFactsService _syntaxFacts; + private readonly AbstractAddImportFeatureService _owner; + + private readonly SyntaxNode _node; + private readonly ISymbolSearchService _symbolSearchService; + private readonly AddImportOptions _options; + private readonly ImmutableArray _packageSources; + + public SymbolReferenceFinder( + AbstractAddImportFeatureService owner, + Document document, + SemanticModel semanticModel, + string diagnosticId, + SyntaxNode node, + ISymbolSearchService symbolSearchService, + AddImportOptions options, + ImmutableArray packageSources, + CancellationToken cancellationToken) + { + _owner = owner; + _document = document; + _semanticModel = semanticModel; + _diagnosticId = diagnosticId; + _node = node; + + _symbolSearchService = symbolSearchService; + _options = options; + _packageSources = packageSources; + _syntaxFacts = document.GetLanguageService(); + + _namespacesInScope = GetNamespacesInScope(cancellationToken); + } + + private ISet GetNamespacesInScope(CancellationToken cancellationToken) { - private const string AttributeSuffix = nameof(Attribute); - - private readonly string _diagnosticId; - private readonly Document _document; - private readonly SemanticModel _semanticModel; - - private readonly ISet _namespacesInScope; - private readonly ISyntaxFactsService _syntaxFacts; - private readonly AbstractAddImportFeatureService _owner; - - private readonly SyntaxNode _node; - private readonly ISymbolSearchService _symbolSearchService; - private readonly AddImportOptions _options; - private readonly ImmutableArray _packageSources; - - public SymbolReferenceFinder( - AbstractAddImportFeatureService owner, - Document document, - SemanticModel semanticModel, - string diagnosticId, - SyntaxNode node, - ISymbolSearchService symbolSearchService, - AddImportOptions options, - ImmutableArray packageSources, - CancellationToken cancellationToken) + // Add all the namespaces brought in by imports/usings. + var set = _owner.GetImportNamespacesInScope(_semanticModel, _node, cancellationToken); + + // Also add all the namespaces we're contained in. We don't want + // to add imports for these namespaces either. + for (var containingNamespace = _semanticModel.GetEnclosingNamespace(_node.SpanStart, cancellationToken); + containingNamespace != null; + containingNamespace = containingNamespace.ContainingNamespace) { - _owner = owner; - _document = document; - _semanticModel = semanticModel; - _diagnosticId = diagnosticId; - _node = node; - - _symbolSearchService = symbolSearchService; - _options = options; - _packageSources = packageSources; - _syntaxFacts = document.GetLanguageService(); - - _namespacesInScope = GetNamespacesInScope(cancellationToken); + set.Add(MapToCompilationNamespaceIfPossible(containingNamespace)); } - private ISet GetNamespacesInScope(CancellationToken cancellationToken) - { - // Add all the namespaces brought in by imports/usings. - var set = _owner.GetImportNamespacesInScope(_semanticModel, _node, cancellationToken); - - // Also add all the namespaces we're contained in. We don't want - // to add imports for these namespaces either. - for (var containingNamespace = _semanticModel.GetEnclosingNamespace(_node.SpanStart, cancellationToken); - containingNamespace != null; - containingNamespace = containingNamespace.ContainingNamespace) - { - set.Add(MapToCompilationNamespaceIfPossible(containingNamespace)); - } + return set; + } - return set; - } + private INamespaceSymbol MapToCompilationNamespaceIfPossible(INamespaceSymbol containingNamespace) + => _semanticModel.Compilation.GetCompilationNamespace(containingNamespace) ?? containingNamespace; - private INamespaceSymbol MapToCompilationNamespaceIfPossible(INamespaceSymbol containingNamespace) - => _semanticModel.Compilation.GetCompilationNamespace(containingNamespace) ?? containingNamespace; + internal Task> FindInAllSymbolsInStartingProjectAsync(bool exact, CancellationToken cancellationToken) + => DoAsync(new AllSymbolsProjectSearchScope(_owner, _document.Project, exact), cancellationToken); - internal Task> FindInAllSymbolsInStartingProjectAsync(bool exact, CancellationToken cancellationToken) - => DoAsync(new AllSymbolsProjectSearchScope(_owner, _document.Project, exact), cancellationToken); + internal Task> FindInSourceSymbolsInProjectAsync(ConcurrentDictionary> projectToAssembly, Project project, bool exact, CancellationToken cancellationToken) + => DoAsync(new SourceSymbolsProjectSearchScope(_owner, projectToAssembly, project, exact), cancellationToken); - internal Task> FindInSourceSymbolsInProjectAsync(ConcurrentDictionary> projectToAssembly, Project project, bool exact, CancellationToken cancellationToken) - => DoAsync(new SourceSymbolsProjectSearchScope(_owner, projectToAssembly, project, exact), cancellationToken); + internal Task> FindInMetadataSymbolsAsync(IAssemblySymbol assembly, Project assemblyProject, PortableExecutableReference metadataReference, bool exact, CancellationToken cancellationToken) + => DoAsync(new MetadataSymbolsSearchScope(_owner, assemblyProject, assembly, metadataReference, exact), cancellationToken); + + private async Task> DoAsync(SearchScope searchScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Spin off tasks to do all our searching in parallel + using var _1 = ArrayBuilder>>.GetInstance(out var tasks); + tasks.Add(GetReferencesForMatchingTypesAsync(searchScope, cancellationToken)); + tasks.Add(GetReferencesForMatchingNamespacesAsync(searchScope, cancellationToken)); + tasks.Add(GetReferencesForMatchingFieldsAndPropertiesAsync(searchScope, cancellationToken)); + tasks.Add(GetReferencesForMatchingExtensionMethodsAsync(searchScope, cancellationToken)); + + // Searching for things like "Add" (for collection initializers) and "Select" + // (for extension methods) should only be done when doing an 'exact' search. + // We should not do fuzzy searches for these names. In this case it's not + // like the user was writing Add or Select, but instead we're looking for + // viable symbols with those names to make a collection initializer or + // query expression valid. + if (searchScope.Exact) + { + tasks.Add(GetReferencesForCollectionInitializerMethodsAsync(searchScope, cancellationToken)); + tasks.Add(GetReferencesForQueryPatternsAsync(searchScope, cancellationToken)); + tasks.Add(GetReferencesForDeconstructAsync(searchScope, cancellationToken)); + tasks.Add(GetReferencesForGetAwaiterAsync(searchScope, cancellationToken)); + tasks.Add(GetReferencesForGetEnumeratorAsync(searchScope, cancellationToken)); + tasks.Add(GetReferencesForGetAsyncEnumeratorAsync(searchScope, cancellationToken)); + } - internal Task> FindInMetadataSymbolsAsync(IAssemblySymbol assembly, Project assemblyProject, PortableExecutableReference metadataReference, bool exact, CancellationToken cancellationToken) - => DoAsync(new MetadataSymbolsSearchScope(_owner, assemblyProject, assembly, metadataReference, exact), cancellationToken); + await Task.WhenAll(tasks).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); - private async Task> DoAsync(SearchScope searchScope, CancellationToken cancellationToken) + using var _2 = ArrayBuilder.GetInstance(out var allReferences); + foreach (var task in tasks) { - cancellationToken.ThrowIfCancellationRequested(); + var taskResult = await task.ConfigureAwait(false); + allReferences.AddRange(taskResult); + } - // Spin off tasks to do all our searching in parallel - using var _1 = ArrayBuilder>>.GetInstance(out var tasks); - tasks.Add(GetReferencesForMatchingTypesAsync(searchScope, cancellationToken)); - tasks.Add(GetReferencesForMatchingNamespacesAsync(searchScope, cancellationToken)); - tasks.Add(GetReferencesForMatchingFieldsAndPropertiesAsync(searchScope, cancellationToken)); - tasks.Add(GetReferencesForMatchingExtensionMethodsAsync(searchScope, cancellationToken)); - - // Searching for things like "Add" (for collection initializers) and "Select" - // (for extension methods) should only be done when doing an 'exact' search. - // We should not do fuzzy searches for these names. In this case it's not - // like the user was writing Add or Select, but instead we're looking for - // viable symbols with those names to make a collection initializer or - // query expression valid. - if (searchScope.Exact) - { - tasks.Add(GetReferencesForCollectionInitializerMethodsAsync(searchScope, cancellationToken)); - tasks.Add(GetReferencesForQueryPatternsAsync(searchScope, cancellationToken)); - tasks.Add(GetReferencesForDeconstructAsync(searchScope, cancellationToken)); - tasks.Add(GetReferencesForGetAwaiterAsync(searchScope, cancellationToken)); - tasks.Add(GetReferencesForGetEnumeratorAsync(searchScope, cancellationToken)); - tasks.Add(GetReferencesForGetAsyncEnumeratorAsync(searchScope, cancellationToken)); - } + return DeDupeAndSortReferences(allReferences.ToImmutable()); + } - await Task.WhenAll(tasks).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); + private ImmutableArray DeDupeAndSortReferences(ImmutableArray allReferences) + { + return allReferences + .Distinct() + .Where(NotNull) + .Where(NotGlobalNamespace) + .OrderBy((r1, r2) => r1.CompareTo(_document, r2)) + .ToImmutableArray(); + } - using var _2 = ArrayBuilder.GetInstance(out var allReferences); - foreach (var task in tasks) - { - var taskResult = await task.ConfigureAwait(false); - allReferences.AddRange(taskResult); - } + private static void CalculateContext( + TSimpleNameSyntax nameNode, ISyntaxFactsService syntaxFacts, out string name, out int arity, + out bool inAttributeContext, out bool hasIncompleteParentMember, out bool looksGeneric) + { + // Has to be a simple identifier or generic name. + syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out name, out arity); - return DeDupeAndSortReferences(allReferences.ToImmutable()); - } + inAttributeContext = syntaxFacts.IsAttributeName(nameNode); + hasIncompleteParentMember = nameNode?.Parent?.RawKind == syntaxFacts.SyntaxKinds.IncompleteMember; + looksGeneric = syntaxFacts.LooksGeneric(nameNode); + } - private ImmutableArray DeDupeAndSortReferences(ImmutableArray allReferences) + /// + /// Searches for types that match the name the user has written. Returns s + /// to the s or s those types are + /// contained in. + /// + private async Task> GetReferencesForMatchingTypesAsync( + SearchScope searchScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!_owner.CanAddImportForType(_diagnosticId, _node, out var nameNode)) { - return allReferences - .Distinct() - .Where(NotNull) - .Where(NotGlobalNamespace) - .OrderBy((r1, r2) => r1.CompareTo(_document, r2)) - .ToImmutableArray(); + return []; } - private static void CalculateContext( - TSimpleNameSyntax nameNode, ISyntaxFactsService syntaxFacts, out string name, out int arity, - out bool inAttributeContext, out bool hasIncompleteParentMember, out bool looksGeneric) - { - // Has to be a simple identifier or generic name. - syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out name, out arity); - - inAttributeContext = syntaxFacts.IsAttributeName(nameNode); - hasIncompleteParentMember = nameNode?.Parent?.RawKind == syntaxFacts.SyntaxKinds.IncompleteMember; - looksGeneric = syntaxFacts.LooksGeneric(nameNode); - } + CalculateContext( + nameNode, _syntaxFacts, + out var name, out var arity, out var inAttributeContext, + out var hasIncompleteParentMember, out var looksGeneric); - /// - /// Searches for types that match the name the user has written. Returns s - /// to the s or s those types are - /// contained in. - /// - private async Task> GetReferencesForMatchingTypesAsync( - SearchScope searchScope, CancellationToken cancellationToken) + if (ExpressionBinds(nameNode, checkForExtensionMethods: false, cancellationToken: cancellationToken)) { - cancellationToken.ThrowIfCancellationRequested(); - if (!_owner.CanAddImportForType(_diagnosticId, _node, out var nameNode)) - { - return []; - } - - CalculateContext( - nameNode, _syntaxFacts, - out var name, out var arity, out var inAttributeContext, - out var hasIncompleteParentMember, out var looksGeneric); + // If the expression bound, there's nothing to do. + return []; + } - if (ExpressionBinds(nameNode, checkForExtensionMethods: false, cancellationToken: cancellationToken)) - { - // If the expression bound, there's nothing to do. - return []; - } + var symbols = await searchScope.FindDeclarationsAsync(name, nameNode, SymbolFilter.Type, cancellationToken).ConfigureAwait(false); - var symbols = await searchScope.FindDeclarationsAsync(name, nameNode, SymbolFilter.Type, cancellationToken).ConfigureAwait(false); + // also lookup type symbols with the "Attribute" suffix if necessary. + if (inAttributeContext) + { + var attributeSymbols = await searchScope.FindDeclarationsAsync( + name + AttributeSuffix, nameNode, SymbolFilter.Type, cancellationToken).ConfigureAwait(false); - // also lookup type symbols with the "Attribute" suffix if necessary. - if (inAttributeContext) - { - var attributeSymbols = await searchScope.FindDeclarationsAsync( - name + AttributeSuffix, nameNode, SymbolFilter.Type, cancellationToken).ConfigureAwait(false); + symbols = symbols.AddRange( + attributeSymbols.Select(r => r.WithDesiredName(r.DesiredName.GetWithoutAttributeSuffix(isCaseSensitive: false)))); + } - symbols = symbols.AddRange( - attributeSymbols.Select(r => r.WithDesiredName(r.DesiredName.GetWithoutAttributeSuffix(isCaseSensitive: false)))); - } + var typeSymbols = OfType(symbols); - var typeSymbols = OfType(symbols); + var editorBrowserInfo = new EditorBrowsableInfo(_semanticModel.Compilation); - var editorBrowserInfo = new EditorBrowsableInfo(_semanticModel.Compilation); + // Only keep symbols which are accessible from the current location and that are allowed by the current + // editor browsable rules. + var accessibleTypeSymbols = typeSymbols.WhereAsArray( + s => ArityAccessibilityAndAttributeContextAreCorrect(s.Symbol, arity, inAttributeContext, hasIncompleteParentMember, looksGeneric) && + s.Symbol.IsEditorBrowsable(_options.HideAdvancedMembers, _semanticModel.Compilation, editorBrowserInfo)); - // Only keep symbols which are accessible from the current location and that are allowed by the current - // editor browsable rules. - var accessibleTypeSymbols = typeSymbols.WhereAsArray( - s => ArityAccessibilityAndAttributeContextAreCorrect(s.Symbol, arity, inAttributeContext, hasIncompleteParentMember, looksGeneric) && - s.Symbol.IsEditorBrowsable(_options.HideAdvancedMembers, _semanticModel.Compilation, editorBrowserInfo)); + // These types may be contained within namespaces, or they may be nested + // inside generic types. Record these namespaces/types if it would be + // legal to add imports for them. - // These types may be contained within namespaces, or they may be nested - // inside generic types. Record these namespaces/types if it would be - // legal to add imports for them. + var typesContainedDirectlyInNamespaces = accessibleTypeSymbols.WhereAsArray(s => s.Symbol.ContainingSymbol is INamespaceSymbol); + var typesContainedDirectlyInTypes = accessibleTypeSymbols.WhereAsArray(s => s.Symbol.ContainingType != null); - var typesContainedDirectlyInNamespaces = accessibleTypeSymbols.WhereAsArray(s => s.Symbol.ContainingSymbol is INamespaceSymbol); - var typesContainedDirectlyInTypes = accessibleTypeSymbols.WhereAsArray(s => s.Symbol.ContainingType != null); + var namespaceReferences = GetNamespaceSymbolReferences(searchScope, + typesContainedDirectlyInNamespaces.SelectAsArray(r => r.WithSymbol(r.Symbol.ContainingNamespace))); - var namespaceReferences = GetNamespaceSymbolReferences(searchScope, - typesContainedDirectlyInNamespaces.SelectAsArray(r => r.WithSymbol(r.Symbol.ContainingNamespace))); + var typeReferences = typesContainedDirectlyInTypes.SelectAsArray( + r => searchScope.CreateReference(r.WithSymbol(r.Symbol.ContainingType))); - var typeReferences = typesContainedDirectlyInTypes.SelectAsArray( - r => searchScope.CreateReference(r.WithSymbol(r.Symbol.ContainingType))); + return namespaceReferences.Concat(typeReferences); + } - return namespaceReferences.Concat(typeReferences); + private bool ArityAccessibilityAndAttributeContextAreCorrect( + ITypeSymbol symbol, + int arity, + bool inAttributeContext, + bool hasIncompleteParentMember, + bool looksGeneric) + { + if (inAttributeContext && !symbol.IsAttribute()) + { + return false; } - private bool ArityAccessibilityAndAttributeContextAreCorrect( - ITypeSymbol symbol, - int arity, - bool inAttributeContext, - bool hasIncompleteParentMember, - bool looksGeneric) + if (!symbol.IsAccessibleWithin(_semanticModel.Compilation.Assembly)) { - if (inAttributeContext && !symbol.IsAttribute()) - { - return false; - } - - if (!symbol.IsAccessibleWithin(_semanticModel.Compilation.Assembly)) - { - return false; - } - - if (looksGeneric && symbol.GetTypeArguments().Length == 0) - { - return false; - } + return false; + } - return arity == 0 || symbol.GetArity() == arity || hasIncompleteParentMember; + if (looksGeneric && symbol.GetTypeArguments().Length == 0) + { + return false; } - /// - /// Searches for namespaces that match the name the user has written. Returns s - /// to the s those namespaces are contained in. - /// - private async Task> GetReferencesForMatchingNamespacesAsync( - SearchScope searchScope, CancellationToken cancellationToken) + return arity == 0 || symbol.GetArity() == arity || hasIncompleteParentMember; + } + + /// + /// Searches for namespaces that match the name the user has written. Returns s + /// to the s those namespaces are contained in. + /// + private async Task> GetReferencesForMatchingNamespacesAsync( + SearchScope searchScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_owner.CanAddImportForNamespace(_diagnosticId, _node, out var nameNode)) { - cancellationToken.ThrowIfCancellationRequested(); - if (_owner.CanAddImportForNamespace(_diagnosticId, _node, out var nameNode)) - { - _syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out var name, out var arity); + _syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out var name, out var arity); - if (arity == 0 && - !ExpressionBinds(nameNode, checkForExtensionMethods: false, cancellationToken)) - { - var symbols = await searchScope.FindDeclarationsAsync(name, nameNode, SymbolFilter.Namespace, cancellationToken).ConfigureAwait(false); - var namespaceSymbols = OfType(symbols); - var containingNamespaceSymbols = OfType(symbols).SelectAsArray(s => s.WithSymbol(s.Symbol.ContainingNamespace)); + if (arity == 0 && + !ExpressionBinds(nameNode, checkForExtensionMethods: false, cancellationToken)) + { + var symbols = await searchScope.FindDeclarationsAsync(name, nameNode, SymbolFilter.Namespace, cancellationToken).ConfigureAwait(false); + var namespaceSymbols = OfType(symbols); + var containingNamespaceSymbols = OfType(symbols).SelectAsArray(s => s.WithSymbol(s.Symbol.ContainingNamespace)); - return GetNamespaceSymbolReferences(searchScope, containingNamespaceSymbols); - } + return GetNamespaceSymbolReferences(searchScope, containingNamespaceSymbols); } - - return []; } - /// - /// Specialized finder for the "Color Color" case. Used when we have "Color.Black" and "Color" - /// bound to a Field/Property, but not a type. In this case, we want to look for namespaces - /// containing 'Color' as if we import them it can resolve this issue. - /// - private async Task> GetReferencesForMatchingFieldsAndPropertiesAsync( - SearchScope searchScope, CancellationToken cancellationToken) + return []; + } + + /// + /// Specialized finder for the "Color Color" case. Used when we have "Color.Black" and "Color" + /// bound to a Field/Property, but not a type. In this case, we want to look for namespaces + /// containing 'Color' as if we import them it can resolve this issue. + /// + private async Task> GetReferencesForMatchingFieldsAndPropertiesAsync( + SearchScope searchScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_owner.CanAddImportForMethod(_diagnosticId, _syntaxFacts, _node, out var nameNode) && + nameNode != null) { - cancellationToken.ThrowIfCancellationRequested(); - if (_owner.CanAddImportForMethod(_diagnosticId, _syntaxFacts, _node, out var nameNode) && - nameNode != null) + // We have code like "Color.Black". "Color" bound to a 'Color Color' property, and + // 'Black' did not bind. We want to find a type called 'Color' that will actually + // allow 'Black' to bind. + var syntaxFacts = _document.GetLanguageService(); + if (syntaxFacts.IsNameOfSimpleMemberAccessExpression(nameNode) || + syntaxFacts.IsNameOfMemberBindingExpression(nameNode)) { - // We have code like "Color.Black". "Color" bound to a 'Color Color' property, and - // 'Black' did not bind. We want to find a type called 'Color' that will actually - // allow 'Black' to bind. - var syntaxFacts = _document.GetLanguageService(); - if (syntaxFacts.IsNameOfSimpleMemberAccessExpression(nameNode) || - syntaxFacts.IsNameOfMemberBindingExpression(nameNode)) + var expression = syntaxFacts.IsNameOfSimpleMemberAccessExpression(nameNode) + ? syntaxFacts.GetExpressionOfMemberAccessExpression(nameNode.Parent, allowImplicitTarget: true) + : syntaxFacts.GetTargetOfMemberBinding(nameNode.Parent); + if (expression is TSimpleNameSyntax simpleName) { - var expression = syntaxFacts.IsNameOfSimpleMemberAccessExpression(nameNode) - ? syntaxFacts.GetExpressionOfMemberAccessExpression(nameNode.Parent, allowImplicitTarget: true) - : syntaxFacts.GetTargetOfMemberBinding(nameNode.Parent); - if (expression is TSimpleNameSyntax simpleName) + // Check if the expression before the dot binds to a property or field. + var symbol = _semanticModel.GetSymbolInfo(expression, cancellationToken).GetAnySymbol(); + if (symbol?.Kind is SymbolKind.Property or SymbolKind.Field) { - // Check if the expression before the dot binds to a property or field. - var symbol = _semanticModel.GetSymbolInfo(expression, cancellationToken).GetAnySymbol(); - if (symbol?.Kind is SymbolKind.Property or SymbolKind.Field) + // Check if we have the 'Color Color' case. + var propertyOrFieldType = symbol.GetSymbolType(); + if (propertyOrFieldType is INamedTypeSymbol propertyType && + Equals(propertyType.Name, symbol.Name)) { - // Check if we have the 'Color Color' case. - var propertyOrFieldType = symbol.GetSymbolType(); - if (propertyOrFieldType is INamedTypeSymbol propertyType && - Equals(propertyType.Name, symbol.Name)) - { - // Try to look up 'Color' as a type. - var symbolResults = await searchScope.FindDeclarationsAsync( - symbol.Name, simpleName, SymbolFilter.Type, cancellationToken).ConfigureAwait(false); - - // Return results that have accessible members. - var namedTypeSymbols = OfType(symbolResults); - var name = nameNode.GetFirstToken().ValueText; - var namespaceResults = - namedTypeSymbols.WhereAsArray(sr => HasAccessibleStaticFieldOrProperty(sr.Symbol, name)) - .SelectAsArray(sr => sr.WithSymbol(sr.Symbol.ContainingNamespace)); - - return GetNamespaceSymbolReferences(searchScope, namespaceResults); - } + // Try to look up 'Color' as a type. + var symbolResults = await searchScope.FindDeclarationsAsync( + symbol.Name, simpleName, SymbolFilter.Type, cancellationToken).ConfigureAwait(false); + + // Return results that have accessible members. + var namedTypeSymbols = OfType(symbolResults); + var name = nameNode.GetFirstToken().ValueText; + var namespaceResults = + namedTypeSymbols.WhereAsArray(sr => HasAccessibleStaticFieldOrProperty(sr.Symbol, name)) + .SelectAsArray(sr => sr.WithSymbol(sr.Symbol.ContainingNamespace)); + + return GetNamespaceSymbolReferences(searchScope, namespaceResults); } } } } - - return []; } - private bool HasAccessibleStaticFieldOrProperty(INamedTypeSymbol namedType, string fieldOrPropertyName) - { - return namedType.GetMembers(fieldOrPropertyName) - .Any(static (m, self) => (m is IFieldSymbol || m is IPropertySymbol) && - m.IsStatic && - m.IsAccessibleWithin(self._semanticModel.Compilation.Assembly), this); - } + return []; + } - /// - /// Searches for extension methods that match the name the user has written. Returns - /// s to the s that contain - /// the static classes that those extension methods are contained in. - /// - private async Task> GetReferencesForMatchingExtensionMethodsAsync( - SearchScope searchScope, CancellationToken cancellationToken) + private bool HasAccessibleStaticFieldOrProperty(INamedTypeSymbol namedType, string fieldOrPropertyName) + { + return namedType.GetMembers(fieldOrPropertyName) + .Any(static (m, self) => (m is IFieldSymbol || m is IPropertySymbol) && + m.IsStatic && + m.IsAccessibleWithin(self._semanticModel.Compilation.Assembly), this); + } + + /// + /// Searches for extension methods that match the name the user has written. Returns + /// s to the s that contain + /// the static classes that those extension methods are contained in. + /// + private async Task> GetReferencesForMatchingExtensionMethodsAsync( + SearchScope searchScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_owner.CanAddImportForMethod(_diagnosticId, _syntaxFacts, _node, out var nameNode) && + nameNode != null) { cancellationToken.ThrowIfCancellationRequested(); - if (_owner.CanAddImportForMethod(_diagnosticId, _syntaxFacts, _node, out var nameNode) && - nameNode != null) - { - cancellationToken.ThrowIfCancellationRequested(); - // See if the name binds. If it does, there's nothing further we need to do. - if (!ExpressionBinds(nameNode, checkForExtensionMethods: true, cancellationToken)) + // See if the name binds. If it does, there's nothing further we need to do. + if (!ExpressionBinds(nameNode, checkForExtensionMethods: true, cancellationToken)) + { + _syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out var name, out var arity); + if (name != null) { - _syntaxFacts.GetNameAndArityOfSimpleName(nameNode, out var name, out var arity); - if (name != null) - { - var symbols = await searchScope.FindDeclarationsAsync(name, nameNode, SymbolFilter.Member, cancellationToken).ConfigureAwait(false); + var symbols = await searchScope.FindDeclarationsAsync(name, nameNode, SymbolFilter.Member, cancellationToken).ConfigureAwait(false); - var methodSymbols = OfType(symbols); + var methodSymbols = OfType(symbols); - var extensionMethodSymbols = GetViableExtensionMethods( - methodSymbols, nameNode.Parent, cancellationToken); + var extensionMethodSymbols = GetViableExtensionMethods( + methodSymbols, nameNode.Parent, cancellationToken); - var namespaceSymbols = extensionMethodSymbols.SelectAsArray(s => s.WithSymbol(s.Symbol.ContainingNamespace)); - return GetNamespaceSymbolReferences(searchScope, namespaceSymbols); - } + var namespaceSymbols = extensionMethodSymbols.SelectAsArray(s => s.WithSymbol(s.Symbol.ContainingNamespace)); + return GetNamespaceSymbolReferences(searchScope, namespaceSymbols); } } - - return []; } - private ImmutableArray> GetViableExtensionMethods( - ImmutableArray> methodSymbols, - SyntaxNode expression, CancellationToken cancellationToken) - { - return GetViableExtensionMethodsWorker(methodSymbols).WhereAsArray( - s => _owner.IsViableExtensionMethod(s.Symbol, expression, _semanticModel, _syntaxFacts, cancellationToken)); - } + return []; + } - private ImmutableArray> GetViableExtensionMethods( - ImmutableArray> methodSymbols, ITypeSymbol typeSymbol) - { - return GetViableExtensionMethodsWorker(methodSymbols).WhereAsArray( - s => IsViableExtensionMethod(s.Symbol, typeSymbol)); - } + private ImmutableArray> GetViableExtensionMethods( + ImmutableArray> methodSymbols, + SyntaxNode expression, CancellationToken cancellationToken) + { + return GetViableExtensionMethodsWorker(methodSymbols).WhereAsArray( + s => _owner.IsViableExtensionMethod(s.Symbol, expression, _semanticModel, _syntaxFacts, cancellationToken)); + } - private ImmutableArray> GetViableExtensionMethodsWorker( - ImmutableArray> methodSymbols) - { - return methodSymbols.WhereAsArray( - s => s.Symbol.IsExtensionMethod && - s.Symbol.IsAccessibleWithin(_semanticModel.Compilation.Assembly)); - } + private ImmutableArray> GetViableExtensionMethods( + ImmutableArray> methodSymbols, ITypeSymbol typeSymbol) + { + return GetViableExtensionMethodsWorker(methodSymbols).WhereAsArray( + s => IsViableExtensionMethod(s.Symbol, typeSymbol)); + } - /// - /// Searches for extension methods exactly called 'Add'. Returns - /// s to the s that contain - /// the static classes that those extension methods are contained in. - /// - private async Task> GetReferencesForCollectionInitializerMethodsAsync( - SearchScope searchScope, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - if (_owner.CanAddImportForMethod(_diagnosticId, _syntaxFacts, _node, out _) && - !_syntaxFacts.IsSimpleName(_node) && - _owner.IsAddMethodContext(_node, _semanticModel)) - { - var symbols = await searchScope.FindDeclarationsAsync( - nameof(IList.Add), nameNode: null, filter: SymbolFilter.Member, cancellationToken).ConfigureAwait(false); + private ImmutableArray> GetViableExtensionMethodsWorker( + ImmutableArray> methodSymbols) + { + return methodSymbols.WhereAsArray( + s => s.Symbol.IsExtensionMethod && + s.Symbol.IsAccessibleWithin(_semanticModel.Compilation.Assembly)); + } - // Note: there is no desiredName for these search results. We're searching for - // extension methods called "Add", but we have no intention of renaming any - // of the existing user code to that name. - var methodSymbols = OfType(symbols).SelectAsArray(s => s.WithDesiredName(null)); + /// + /// Searches for extension methods exactly called 'Add'. Returns + /// s to the s that contain + /// the static classes that those extension methods are contained in. + /// + private async Task> GetReferencesForCollectionInitializerMethodsAsync( + SearchScope searchScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_owner.CanAddImportForMethod(_diagnosticId, _syntaxFacts, _node, out _) && + !_syntaxFacts.IsSimpleName(_node) && + _owner.IsAddMethodContext(_node, _semanticModel)) + { + var symbols = await searchScope.FindDeclarationsAsync( + nameof(IList.Add), nameNode: null, filter: SymbolFilter.Member, cancellationToken).ConfigureAwait(false); - var viableMethods = GetViableExtensionMethods( - methodSymbols, _node.Parent, cancellationToken); + // Note: there is no desiredName for these search results. We're searching for + // extension methods called "Add", but we have no intention of renaming any + // of the existing user code to that name. + var methodSymbols = OfType(symbols).SelectAsArray(s => s.WithDesiredName(null)); - return GetNamespaceSymbolReferences(searchScope, - viableMethods.SelectAsArray(m => m.WithSymbol(m.Symbol.ContainingNamespace))); - } + var viableMethods = GetViableExtensionMethods( + methodSymbols, _node.Parent, cancellationToken); - return []; + return GetNamespaceSymbolReferences(searchScope, + viableMethods.SelectAsArray(m => m.WithSymbol(m.Symbol.ContainingNamespace))); } - /// - /// Searches for extension methods exactly called 'Select'. Returns - /// s to the s that contain - /// the static classes that those extension methods are contained in. - /// - private async Task> GetReferencesForQueryPatternsAsync( - SearchScope searchScope, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (_owner.CanAddImportForQuery(_diagnosticId, _node)) - { - var type = _owner.GetQueryClauseInfo(_semanticModel, _node, cancellationToken); - if (type != null) - { - // find extension methods named "Select" - return await GetReferencesForExtensionMethodAsync( - searchScope, nameof(Enumerable.Select), type, predicate: null, cancellationToken).ConfigureAwait(false); - } - } + return []; + } - return []; - } + /// + /// Searches for extension methods exactly called 'Select'. Returns + /// s to the s that contain + /// the static classes that those extension methods are contained in. + /// + private async Task> GetReferencesForQueryPatternsAsync( + SearchScope searchScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - /// - /// Searches for extension methods exactly called 'GetAwaiter'. Returns - /// s to the s that contain - /// the static classes that those extension methods are contained in. - /// - private async Task> GetReferencesForGetAwaiterAsync( - SearchScope searchScope, CancellationToken cancellationToken) + if (_owner.CanAddImportForQuery(_diagnosticId, _node)) { - cancellationToken.ThrowIfCancellationRequested(); - - if (_owner.CanAddImportForGetAwaiter(_diagnosticId, _syntaxFacts, _node)) + var type = _owner.GetQueryClauseInfo(_semanticModel, _node, cancellationToken); + if (type != null) { - var type = GetAwaitInfo(_semanticModel, _syntaxFacts, _node); - if (type != null) - { - return await GetReferencesForExtensionMethodAsync( - searchScope, WellKnownMemberNames.GetAwaiter, type, - static m => m.IsValidGetAwaiter(), - cancellationToken).ConfigureAwait(false); - } + // find extension methods named "Select" + return await GetReferencesForExtensionMethodAsync( + searchScope, nameof(Enumerable.Select), type, predicate: null, cancellationToken).ConfigureAwait(false); } - - return []; } - /// - /// Searches for extension methods exactly called 'GetEnumerator'. Returns - /// s to the s that contain - /// the static classes that those extension methods are contained in. - /// - private async Task> GetReferencesForGetEnumeratorAsync( - SearchScope searchScope, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (_owner.CanAddImportForGetEnumerator(_diagnosticId, _syntaxFacts, _node)) - { - var type = GetCollectionExpressionType(_semanticModel, _syntaxFacts, _node); - if (type != null) - { - return await GetReferencesForExtensionMethodAsync( - searchScope, WellKnownMemberNames.GetEnumeratorMethodName, type, - static m => m.IsValidGetEnumerator(), - cancellationToken).ConfigureAwait(false); - } - } + return []; + } - return []; - } + /// + /// Searches for extension methods exactly called 'GetAwaiter'. Returns + /// s to the s that contain + /// the static classes that those extension methods are contained in. + /// + private async Task> GetReferencesForGetAwaiterAsync( + SearchScope searchScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - /// - /// Searches for extension methods exactly called 'GetAsyncEnumerator'. Returns - /// s to the s that contain - /// the static classes that those extension methods are contained in. - /// - private async Task> GetReferencesForGetAsyncEnumeratorAsync( - SearchScope searchScope, CancellationToken cancellationToken) + if (_owner.CanAddImportForGetAwaiter(_diagnosticId, _syntaxFacts, _node)) { - cancellationToken.ThrowIfCancellationRequested(); - - if (_owner.CanAddImportForGetAsyncEnumerator(_diagnosticId, _syntaxFacts, _node)) + var type = GetAwaitInfo(_semanticModel, _syntaxFacts, _node); + if (type != null) { - var type = GetCollectionExpressionType(_semanticModel, _syntaxFacts, _node); - if (type != null) - { - return await GetReferencesForExtensionMethodAsync( - searchScope, WellKnownMemberNames.GetAsyncEnumeratorMethodName, type, - static m => m.IsValidGetAsyncEnumerator(), - cancellationToken).ConfigureAwait(false); - } + return await GetReferencesForExtensionMethodAsync( + searchScope, WellKnownMemberNames.GetAwaiter, type, + static m => m.IsValidGetAwaiter(), + cancellationToken).ConfigureAwait(false); } - - return []; } - /// - /// Searches for extension methods exactly called 'Deconstruct'. Returns - /// s to the s that contain - /// the static classes that those extension methods are contained in. - /// - private async Task> GetReferencesForDeconstructAsync( - SearchScope searchScope, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + return []; + } + + /// + /// Searches for extension methods exactly called 'GetEnumerator'. Returns + /// s to the s that contain + /// the static classes that those extension methods are contained in. + /// + private async Task> GetReferencesForGetEnumeratorAsync( + SearchScope searchScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - if (_owner.CanAddImportForDeconstruct(_diagnosticId, _node)) + if (_owner.CanAddImportForGetEnumerator(_diagnosticId, _syntaxFacts, _node)) + { + var type = GetCollectionExpressionType(_semanticModel, _syntaxFacts, _node); + if (type != null) { - var type = _owner.GetDeconstructInfo(_semanticModel, _node, cancellationToken); - if (type != null) - { - // Note: we could check that the extension methods have the right number of out-params. - // But that would involve figuring out what we're trying to deconstruct into. For now - // we'll just be permissive, with the assumption that there won't be that many matching - // 'Deconstruct' extension methods for the type of node that we're on. - return await GetReferencesForExtensionMethodAsync( - searchScope, "Deconstruct", type, static m => m.ReturnsVoid, cancellationToken).ConfigureAwait(false); - } + return await GetReferencesForExtensionMethodAsync( + searchScope, WellKnownMemberNames.GetEnumeratorMethodName, type, + static m => m.IsValidGetEnumerator(), + cancellationToken).ConfigureAwait(false); } - - return []; } - private async Task> GetReferencesForExtensionMethodAsync( - SearchScope searchScope, string name, ITypeSymbol type, Func predicate, CancellationToken cancellationToken) - { - var symbols = await searchScope.FindDeclarationsAsync( - name, nameNode: null, filter: SymbolFilter.Member, cancellationToken).ConfigureAwait(false); + return []; + } - // Note: there is no "desiredName" when doing this. We're not going to do any - // renames of the user code. We're just looking for an extension method called - // "Select", but that name has no bearing on the code in question that we're - // trying to fix up. - var methodSymbols = OfType(symbols).SelectAsArray(s => s.WithDesiredName(null)); - var viableExtensionMethods = GetViableExtensionMethods(methodSymbols, type); + /// + /// Searches for extension methods exactly called 'GetAsyncEnumerator'. Returns + /// s to the s that contain + /// the static classes that those extension methods are contained in. + /// + private async Task> GetReferencesForGetAsyncEnumeratorAsync( + SearchScope searchScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - if (predicate != null) + if (_owner.CanAddImportForGetAsyncEnumerator(_diagnosticId, _syntaxFacts, _node)) + { + var type = GetCollectionExpressionType(_semanticModel, _syntaxFacts, _node); + if (type != null) { - viableExtensionMethods = viableExtensionMethods.WhereAsArray(s => predicate(s.Symbol)); + return await GetReferencesForExtensionMethodAsync( + searchScope, WellKnownMemberNames.GetAsyncEnumeratorMethodName, type, + static m => m.IsValidGetAsyncEnumerator(), + cancellationToken).ConfigureAwait(false); } + } - var namespaceSymbols = viableExtensionMethods.SelectAsArray(s => s.WithSymbol(s.Symbol.ContainingNamespace)); + return []; + } - return GetNamespaceSymbolReferences(searchScope, namespaceSymbols); - } + /// + /// Searches for extension methods exactly called 'Deconstruct'. Returns + /// s to the s that contain + /// the static classes that those extension methods are contained in. + /// + private async Task> GetReferencesForDeconstructAsync( + SearchScope searchScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - protected bool ExpressionBinds( - TSimpleNameSyntax nameNode, bool checkForExtensionMethods, CancellationToken cancellationToken) + if (_owner.CanAddImportForDeconstruct(_diagnosticId, _node)) { - // See if the name binds to something other then the error type. If it does, there's nothing further we need to do. - // For extension methods, however, we will continue to search if there exists any better matched method. - cancellationToken.ThrowIfCancellationRequested(); - var symbolInfo = _semanticModel.GetSymbolInfo(nameNode, cancellationToken); - if (symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure && !checkForExtensionMethods) + var type = _owner.GetDeconstructInfo(_semanticModel, _node, cancellationToken); + if (type != null) { - return true; + // Note: we could check that the extension methods have the right number of out-params. + // But that would involve figuring out what we're trying to deconstruct into. For now + // we'll just be permissive, with the assumption that there won't be that many matching + // 'Deconstruct' extension methods for the type of node that we're on. + return await GetReferencesForExtensionMethodAsync( + searchScope, "Deconstruct", type, static m => m.ReturnsVoid, cancellationToken).ConfigureAwait(false); } - - return symbolInfo.Symbol != null; } - private ImmutableArray GetNamespaceSymbolReferences( - SearchScope scope, ImmutableArray> namespaces) + return []; + } + + private async Task> GetReferencesForExtensionMethodAsync( + SearchScope searchScope, string name, ITypeSymbol type, Func predicate, CancellationToken cancellationToken) + { + var symbols = await searchScope.FindDeclarationsAsync( + name, nameNode: null, filter: SymbolFilter.Member, cancellationToken).ConfigureAwait(false); + + // Note: there is no "desiredName" when doing this. We're not going to do any + // renames of the user code. We're just looking for an extension method called + // "Select", but that name has no bearing on the code in question that we're + // trying to fix up. + var methodSymbols = OfType(symbols).SelectAsArray(s => s.WithDesiredName(null)); + var viableExtensionMethods = GetViableExtensionMethods(methodSymbols, type); + + if (predicate != null) { - using var _ = ArrayBuilder.GetInstance(out var references); + viableExtensionMethods = viableExtensionMethods.WhereAsArray(s => predicate(s.Symbol)); + } - foreach (var namespaceResult in namespaces) - { - var symbol = namespaceResult.Symbol; - var mappedResult = namespaceResult.WithSymbol(MapToCompilationNamespaceIfPossible(namespaceResult.Symbol)); - var namespaceIsInScope = _namespacesInScope.Contains(mappedResult.Symbol); - if (!symbol.IsGlobalNamespace && !namespaceIsInScope) - references.Add(scope.CreateReference(mappedResult)); - } + var namespaceSymbols = viableExtensionMethods.SelectAsArray(s => s.WithSymbol(s.Symbol.ContainingNamespace)); - return references.ToImmutable(); + return GetNamespaceSymbolReferences(searchScope, namespaceSymbols); + } + + protected bool ExpressionBinds( + TSimpleNameSyntax nameNode, bool checkForExtensionMethods, CancellationToken cancellationToken) + { + // See if the name binds to something other then the error type. If it does, there's nothing further we need to do. + // For extension methods, however, we will continue to search if there exists any better matched method. + cancellationToken.ThrowIfCancellationRequested(); + var symbolInfo = _semanticModel.GetSymbolInfo(nameNode, cancellationToken); + if (symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure && !checkForExtensionMethods) + { + return true; } - private static ImmutableArray> OfType(ImmutableArray> symbols) where T : ISymbol + return symbolInfo.Symbol != null; + } + + private ImmutableArray GetNamespaceSymbolReferences( + SearchScope scope, ImmutableArray> namespaces) + { + using var _ = ArrayBuilder.GetInstance(out var references); + + foreach (var namespaceResult in namespaces) { - return symbols.WhereAsArray(s => s.Symbol is T) - .SelectAsArray(s => s.WithSymbol((T)s.Symbol)); + var symbol = namespaceResult.Symbol; + var mappedResult = namespaceResult.WithSymbol(MapToCompilationNamespaceIfPossible(namespaceResult.Symbol)); + var namespaceIsInScope = _namespacesInScope.Contains(mappedResult.Symbol); + if (!symbol.IsGlobalNamespace && !namespaceIsInScope) + references.Add(scope.CreateReference(mappedResult)); } + + return references.ToImmutable(); + } + + private static ImmutableArray> OfType(ImmutableArray> symbols) where T : ISymbol + { + return symbols.WhereAsArray(s => s.Symbol is T) + .SelectAsArray(s => s.WithSymbol((T)s.Symbol)); } } } diff --git a/src/Features/Core/Portable/AddImport/SymbolReferenceFinder_PackageAssemblySearch.cs b/src/Features/Core/Portable/AddImport/SymbolReferenceFinder_PackageAssemblySearch.cs index a1284d2bf2ac5..a6233315ee6a8 100644 --- a/src/Features/Core/Portable/AddImport/SymbolReferenceFinder_PackageAssemblySearch.cs +++ b/src/Features/Core/Portable/AddImport/SymbolReferenceFinder_PackageAssemblySearch.cs @@ -11,168 +11,167 @@ using Microsoft.CodeAnalysis.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private partial class SymbolReferenceFinder { - private partial class SymbolReferenceFinder + internal async Task FindNugetOrReferenceAssemblyReferencesAsync( + ConcurrentQueue allReferences, CancellationToken cancellationToken) { - internal async Task FindNugetOrReferenceAssemblyReferencesAsync( - ConcurrentQueue allReferences, CancellationToken cancellationToken) - { - // Only do this if none of the project or metadata searches produced - // any results. We always consider source and local metadata to be - // better than any NuGet/assembly-reference results. - if (allReferences.Count > 0) - return; - - if (!_owner.CanAddImportForType(_diagnosticId, _node, out var nameNode)) - { - return; - } + // Only do this if none of the project or metadata searches produced + // any results. We always consider source and local metadata to be + // better than any NuGet/assembly-reference results. + if (allReferences.Count > 0) + return; - CalculateContext( - nameNode, _syntaxFacts, - out var name, out var arity, out var inAttributeContext, out _, out _); + if (!_owner.CanAddImportForType(_diagnosticId, _node, out var nameNode)) + { + return; + } - if (ExpressionBinds(nameNode, checkForExtensionMethods: false, cancellationToken: cancellationToken)) - { - return; - } + CalculateContext( + nameNode, _syntaxFacts, + out var name, out var arity, out var inAttributeContext, out _, out _); - await FindNugetOrReferenceAssemblyTypeReferencesAsync( - allReferences, nameNode, name, arity, inAttributeContext, cancellationToken).ConfigureAwait(false); + if (ExpressionBinds(nameNode, checkForExtensionMethods: false, cancellationToken: cancellationToken)) + { + return; } - private async Task FindNugetOrReferenceAssemblyTypeReferencesAsync( - ConcurrentQueue allReferences, TSimpleNameSyntax nameNode, - string name, int arity, bool inAttributeContext, - CancellationToken cancellationToken) - { - if (arity == 0 && inAttributeContext) - { - await FindNugetOrReferenceAssemblyTypeReferencesWorkerAsync( - allReferences, nameNode, name + AttributeSuffix, arity, - isAttributeSearch: true, cancellationToken: cancellationToken).ConfigureAwait(false); - } + await FindNugetOrReferenceAssemblyTypeReferencesAsync( + allReferences, nameNode, name, arity, inAttributeContext, cancellationToken).ConfigureAwait(false); + } + private async Task FindNugetOrReferenceAssemblyTypeReferencesAsync( + ConcurrentQueue allReferences, TSimpleNameSyntax nameNode, + string name, int arity, bool inAttributeContext, + CancellationToken cancellationToken) + { + if (arity == 0 && inAttributeContext) + { await FindNugetOrReferenceAssemblyTypeReferencesWorkerAsync( - allReferences, nameNode, name, arity, - isAttributeSearch: false, cancellationToken: cancellationToken).ConfigureAwait(false); + allReferences, nameNode, name + AttributeSuffix, arity, + isAttributeSearch: true, cancellationToken: cancellationToken).ConfigureAwait(false); } - private async Task FindNugetOrReferenceAssemblyTypeReferencesWorkerAsync( - ConcurrentQueue allReferences, TSimpleNameSyntax nameNode, - string name, int arity, bool isAttributeSearch, CancellationToken cancellationToken) - { - if (_options.SearchOptions.SearchReferenceAssemblies) - { - cancellationToken.ThrowIfCancellationRequested(); - await FindReferenceAssemblyTypeReferencesAsync( - allReferences, nameNode, name, arity, isAttributeSearch, cancellationToken).ConfigureAwait(false); - } + await FindNugetOrReferenceAssemblyTypeReferencesWorkerAsync( + allReferences, nameNode, name, arity, + isAttributeSearch: false, cancellationToken: cancellationToken).ConfigureAwait(false); + } - var packageSources = PackageSourceHelper.GetPackageSources(_packageSources); - foreach (var (sourceName, sourceUrl) in packageSources) - { - cancellationToken.ThrowIfCancellationRequested(); - await FindNugetTypeReferencesAsync( - sourceName, sourceUrl, allReferences, - nameNode, name, arity, isAttributeSearch, cancellationToken).ConfigureAwait(false); - } + private async Task FindNugetOrReferenceAssemblyTypeReferencesWorkerAsync( + ConcurrentQueue allReferences, TSimpleNameSyntax nameNode, + string name, int arity, bool isAttributeSearch, CancellationToken cancellationToken) + { + if (_options.SearchOptions.SearchReferenceAssemblies) + { + cancellationToken.ThrowIfCancellationRequested(); + await FindReferenceAssemblyTypeReferencesAsync( + allReferences, nameNode, name, arity, isAttributeSearch, cancellationToken).ConfigureAwait(false); } - private async Task FindReferenceAssemblyTypeReferencesAsync( - ConcurrentQueue allReferences, - TSimpleNameSyntax nameNode, - string name, - int arity, - bool isAttributeSearch, - CancellationToken cancellationToken) + var packageSources = PackageSourceHelper.GetPackageSources(_packageSources); + foreach (var (sourceName, sourceUrl) in packageSources) { cancellationToken.ThrowIfCancellationRequested(); - var results = await _symbolSearchService.FindReferenceAssembliesWithTypeAsync( - name, arity, cancellationToken).ConfigureAwait(false); + await FindNugetTypeReferencesAsync( + sourceName, sourceUrl, allReferences, + nameNode, name, arity, isAttributeSearch, cancellationToken).ConfigureAwait(false); + } + } - var project = _document.Project; + private async Task FindReferenceAssemblyTypeReferencesAsync( + ConcurrentQueue allReferences, + TSimpleNameSyntax nameNode, + string name, + int arity, + bool isAttributeSearch, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var results = await _symbolSearchService.FindReferenceAssembliesWithTypeAsync( + name, arity, cancellationToken).ConfigureAwait(false); - foreach (var result in results) - { - cancellationToken.ThrowIfCancellationRequested(); - await HandleReferenceAssemblyReferenceAsync( - allReferences, nameNode, project, - isAttributeSearch, result, weight: allReferences.Count, - cancellationToken: cancellationToken).ConfigureAwait(false); - } - } + var project = _document.Project; - private async Task FindNugetTypeReferencesAsync( - string sourceName, - string sourceUrl, - ConcurrentQueue allReferences, - TSimpleNameSyntax nameNode, - string name, - int arity, - bool isAttributeSearch, - CancellationToken cancellationToken) + foreach (var result in results) { cancellationToken.ThrowIfCancellationRequested(); - var results = await _symbolSearchService.FindPackagesWithTypeAsync( - sourceName, name, arity, cancellationToken).ConfigureAwait(false); + await HandleReferenceAssemblyReferenceAsync( + allReferences, nameNode, project, + isAttributeSearch, result, weight: allReferences.Count, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + } - foreach (var result in results) - { - cancellationToken.ThrowIfCancellationRequested(); - HandleNugetReference( - sourceUrl, allReferences, nameNode, - isAttributeSearch, result, - weight: allReferences.Count); - } + private async Task FindNugetTypeReferencesAsync( + string sourceName, + string sourceUrl, + ConcurrentQueue allReferences, + TSimpleNameSyntax nameNode, + string name, + int arity, + bool isAttributeSearch, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var results = await _symbolSearchService.FindPackagesWithTypeAsync( + sourceName, name, arity, cancellationToken).ConfigureAwait(false); + + foreach (var result in results) + { + cancellationToken.ThrowIfCancellationRequested(); + HandleNugetReference( + sourceUrl, allReferences, nameNode, + isAttributeSearch, result, + weight: allReferences.Count); } + } - private async Task HandleReferenceAssemblyReferenceAsync( - ConcurrentQueue allReferences, - TSimpleNameSyntax nameNode, - Project project, - bool isAttributeSearch, - ReferenceAssemblyWithTypeResult result, - int weight, - CancellationToken cancellationToken) + private async Task HandleReferenceAssemblyReferenceAsync( + ConcurrentQueue allReferences, + TSimpleNameSyntax nameNode, + Project project, + bool isAttributeSearch, + ReferenceAssemblyWithTypeResult result, + int weight, + CancellationToken cancellationToken) + { + foreach (var reference in project.MetadataReferences) { - foreach (var reference in project.MetadataReferences) + cancellationToken.ThrowIfCancellationRequested(); + var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + + var assemblySymbol = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; + if (assemblySymbol?.Name == result.AssemblyName) { - cancellationToken.ThrowIfCancellationRequested(); - var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - - var assemblySymbol = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; - if (assemblySymbol?.Name == result.AssemblyName) - { - // Project already has a reference to an assembly with this name. - return; - } + // Project already has a reference to an assembly with this name. + return; } - - var desiredName = GetDesiredName(isAttributeSearch, result.TypeName); - allReferences.Enqueue(new AssemblyReference( - _owner, new SearchResult(desiredName, nameNode, result.ContainingNamespaceNames.ToReadOnlyList(), weight), result)); } - private void HandleNugetReference( - string source, - ConcurrentQueue allReferences, - TSimpleNameSyntax nameNode, - bool isAttributeSearch, - PackageWithTypeResult result, - int weight) - { - var desiredName = GetDesiredName(isAttributeSearch, result.TypeName); - allReferences.Enqueue(new PackageReference(_owner, - new SearchResult(desiredName, nameNode, result.ContainingNamespaceNames.ToReadOnlyList(), weight), - source, result.PackageName, result.Version)); - } + var desiredName = GetDesiredName(isAttributeSearch, result.TypeName); + allReferences.Enqueue(new AssemblyReference( + _owner, new SearchResult(desiredName, nameNode, result.ContainingNamespaceNames.ToReadOnlyList(), weight), result)); + } - private static string? GetDesiredName(bool isAttributeSearch, string typeName) - => isAttributeSearch ? typeName.GetWithoutAttributeSuffix(isCaseSensitive: false) : typeName; + private void HandleNugetReference( + string source, + ConcurrentQueue allReferences, + TSimpleNameSyntax nameNode, + bool isAttributeSearch, + PackageWithTypeResult result, + int weight) + { + var desiredName = GetDesiredName(isAttributeSearch, result.TypeName); + allReferences.Enqueue(new PackageReference(_owner, + new SearchResult(desiredName, nameNode, result.ContainingNamespaceNames.ToReadOnlyList(), weight), + source, result.PackageName, result.Version)); } + + private static string? GetDesiredName(bool isAttributeSearch, string typeName) + => isAttributeSearch ? typeName.GetWithoutAttributeSuffix(isCaseSensitive: false) : typeName; } } diff --git a/src/Features/Core/Portable/AddImport/SymbolResult.cs b/src/Features/Core/Portable/AddImport/SymbolResult.cs index d81e63a6ae914..09f95fc809148 100644 --- a/src/Features/Core/Portable/AddImport/SymbolResult.cs +++ b/src/Features/Core/Portable/AddImport/SymbolResult.cs @@ -8,91 +8,90 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.AddImport +namespace Microsoft.CodeAnalysis.AddImport; + +internal abstract partial class AbstractAddImportFeatureService { - internal abstract partial class AbstractAddImportFeatureService + private readonly struct SearchResult(string? desiredName, TSimpleNameSyntax nameNode, IReadOnlyList nameParts, double weight) { - private readonly struct SearchResult(string? desiredName, TSimpleNameSyntax nameNode, IReadOnlyList nameParts, double weight) - { - public readonly IReadOnlyList NameParts = nameParts; + public readonly IReadOnlyList NameParts = nameParts; - // How good a match this was. 0 means it was a perfect match. Larger numbers are less - // and less good. - public readonly double Weight = weight; + // How good a match this was. 0 means it was a perfect match. Larger numbers are less + // and less good. + public readonly double Weight = weight; - // The desired name to change the user text to if this was a fuzzy (spell-checking) match. - public readonly string? DesiredName = desiredName; + // The desired name to change the user text to if this was a fuzzy (spell-checking) match. + public readonly string? DesiredName = desiredName; - // The node to convert to the desired name - public readonly TSimpleNameSyntax NameNode = nameNode; + // The node to convert to the desired name + public readonly TSimpleNameSyntax NameNode = nameNode; - public SearchResult(SymbolResult result) - : this(result.DesiredName, result.NameNode, INamespaceOrTypeSymbolExtensions.GetNameParts(result.Symbol), result.Weight) - { - } + public SearchResult(SymbolResult result) + : this(result.DesiredName, result.NameNode, INamespaceOrTypeSymbolExtensions.GetNameParts(result.Symbol), result.Weight) + { + } - public bool DesiredNameDiffersFromSourceName() - { - return !string.IsNullOrEmpty(DesiredName) && - NameNode != null && - NameNode.GetFirstToken().ValueText != DesiredName; - } + public bool DesiredNameDiffersFromSourceName() + { + return !string.IsNullOrEmpty(DesiredName) && + NameNode != null && + NameNode.GetFirstToken().ValueText != DesiredName; + } + + public bool DesiredNameDiffersFromSourceNameOnlyByCase() + { + Debug.Assert(DesiredNameDiffersFromSourceName()); + return StringComparer.OrdinalIgnoreCase.Equals( + NameNode.GetFirstToken().ValueText, DesiredName); + } - public bool DesiredNameDiffersFromSourceNameOnlyByCase() + public bool DesiredNameMatchesSourceName(Document document) + { + if (!DesiredNameDiffersFromSourceName()) { - Debug.Assert(DesiredNameDiffersFromSourceName()); - return StringComparer.OrdinalIgnoreCase.Equals( - NameNode.GetFirstToken().ValueText, DesiredName); + // Names match in any language. + return true; } - public bool DesiredNameMatchesSourceName(Document document) + var syntaxFacts = document.GetRequiredLanguageService(); + + // Names differ. But in a case insensitive language they may match. + if (!syntaxFacts.IsCaseSensitive && + DesiredNameDiffersFromSourceNameOnlyByCase()) { - if (!DesiredNameDiffersFromSourceName()) - { - // Names match in any language. - return true; - } - - var syntaxFacts = document.GetRequiredLanguageService(); - - // Names differ. But in a case insensitive language they may match. - if (!syntaxFacts.IsCaseSensitive && - DesiredNameDiffersFromSourceNameOnlyByCase()) - { - return true; - } - - // Name are totally different in any language. - return false; + return true; } + + // Name are totally different in any language. + return false; } + } - private readonly struct SymbolResult(string desiredName, TSimpleNameSyntax nameNode, T symbol, double weight) where T : ISymbol - { - // The symbol that matched the string being searched for. - public readonly T Symbol = symbol; + private readonly struct SymbolResult(string desiredName, TSimpleNameSyntax nameNode, T symbol, double weight) where T : ISymbol + { + // The symbol that matched the string being searched for. + public readonly T Symbol = symbol; - // How good a match this was. 0 means it was a perfect match. Larger numbers are less - // and less good. - public readonly double Weight = weight; + // How good a match this was. 0 means it was a perfect match. Larger numbers are less + // and less good. + public readonly double Weight = weight; - // The desired name to change the user text to if this was a fuzzy (spell-checking) match. - public readonly string DesiredName = desiredName; + // The desired name to change the user text to if this was a fuzzy (spell-checking) match. + public readonly string DesiredName = desiredName; - // The node to convert to the desired name - public readonly TSimpleNameSyntax NameNode = nameNode; + // The node to convert to the desired name + public readonly TSimpleNameSyntax NameNode = nameNode; - public SymbolResult WithSymbol(T2 symbol) where T2 : ISymbol - => new(DesiredName, NameNode, symbol, Weight); + public SymbolResult WithSymbol(T2 symbol) where T2 : ISymbol + => new(DesiredName, NameNode, symbol, Weight); - internal SymbolResult WithDesiredName(string desiredName) - => new(desiredName, NameNode, Symbol, Weight); - } + internal SymbolResult WithDesiredName(string desiredName) + => new(desiredName, NameNode, Symbol, Weight); + } - private struct SymbolResult - { - public static SymbolResult Create(string desiredName, TSimpleNameSyntax nameNode, T symbol, double weight) where T : ISymbol - => new(desiredName, nameNode, symbol, weight); - } + private struct SymbolResult + { + public static SymbolResult Create(string desiredName, TSimpleNameSyntax nameNode, T symbol, double weight) where T : ISymbol + => new(desiredName, nameNode, symbol, weight); } } diff --git a/src/Features/Core/Portable/AddMissingReference/AbstractAddMissingReferenceCodeFixProvider.cs b/src/Features/Core/Portable/AddMissingReference/AbstractAddMissingReferenceCodeFixProvider.cs index a02262aedf14e..549182e4ffa03 100644 --- a/src/Features/Core/Portable/AddMissingReference/AbstractAddMissingReferenceCodeFixProvider.cs +++ b/src/Features/Core/Portable/AddMissingReference/AbstractAddMissingReferenceCodeFixProvider.cs @@ -16,77 +16,76 @@ using Microsoft.CodeAnalysis.SymbolSearch; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddMissingReference +namespace Microsoft.CodeAnalysis.AddMissingReference; + +internal abstract partial class AbstractAddMissingReferenceCodeFixProvider : AbstractAddPackageCodeFixProvider { - internal abstract partial class AbstractAddMissingReferenceCodeFixProvider : AbstractAddPackageCodeFixProvider + /// + /// Values for these parameters can be provided (during testing) for mocking purposes. + /// + protected AbstractAddMissingReferenceCodeFixProvider( + IPackageInstallerService? packageInstallerService = null, + ISymbolSearchService? symbolSearchService = null) + : base(packageInstallerService, symbolSearchService) { - /// - /// Values for these parameters can be provided (during testing) for mocking purposes. - /// - protected AbstractAddMissingReferenceCodeFixProvider( - IPackageInstallerService? packageInstallerService = null, - ISymbolSearchService? symbolSearchService = null) - : base(packageInstallerService, symbolSearchService) - { - } + } - protected override bool IncludePrerelease => false; + protected override bool IncludePrerelease => false; - public override FixAllProvider? GetFixAllProvider() - { - // Fix All is not support for this code fix - // https://github.com/dotnet/roslyn/issues/34459 - return null; - } + public override FixAllProvider? GetFixAllProvider() + { + // Fix All is not support for this code fix + // https://github.com/dotnet/roslyn/issues/34459 + return null; + } - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var cancellationToken = context.CancellationToken; - var uniqueIdentities = await GetUniqueIdentitiesAsync(context).ConfigureAwait(false); + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var cancellationToken = context.CancellationToken; + var uniqueIdentities = await GetUniqueIdentitiesAsync(context).ConfigureAwait(false); - var assemblyNames = uniqueIdentities.Select(i => i.Name).ToSet(); - var addPackageCodeActions = await GetAddPackagesCodeActionsAsync(context, assemblyNames).ConfigureAwait(false); - var addReferenceCodeActions = await GetAddReferencesCodeActionsAsync(context, uniqueIdentities).ConfigureAwait(false); + var assemblyNames = uniqueIdentities.Select(i => i.Name).ToSet(); + var addPackageCodeActions = await GetAddPackagesCodeActionsAsync(context, assemblyNames).ConfigureAwait(false); + var addReferenceCodeActions = await GetAddReferencesCodeActionsAsync(context, uniqueIdentities).ConfigureAwait(false); - context.RegisterFixes(addPackageCodeActions, context.Diagnostics); - context.RegisterFixes(addReferenceCodeActions, context.Diagnostics); - } + context.RegisterFixes(addPackageCodeActions, context.Diagnostics); + context.RegisterFixes(addReferenceCodeActions, context.Diagnostics); + } - private static async Task> GetAddReferencesCodeActionsAsync(CodeFixContext context, ISet uniqueIdentities) + private static async Task> GetAddReferencesCodeActionsAsync(CodeFixContext context, ISet uniqueIdentities) + { + var result = ArrayBuilder.GetInstance(); + foreach (var identity in uniqueIdentities) { - var result = ArrayBuilder.GetInstance(); - foreach (var identity in uniqueIdentities) - { - var codeAction = await AddMissingReferenceCodeAction.CreateAsync( - context.Document.Project, identity, context.CancellationToken).ConfigureAwait(false); - result.Add(codeAction); - } - - return result.ToImmutableAndFree(); + var codeAction = await AddMissingReferenceCodeAction.CreateAsync( + context.Document.Project, identity, context.CancellationToken).ConfigureAwait(false); + result.Add(codeAction); } - private static async Task> GetUniqueIdentitiesAsync(CodeFixContext context) + return result.ToImmutableAndFree(); + } + + private static async Task> GetUniqueIdentitiesAsync(CodeFixContext context) + { + var cancellationToken = context.CancellationToken; + var compilation = await context.Document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + + var uniqueIdentities = new HashSet(); + foreach (var diagnostic in context.Diagnostics) { - var cancellationToken = context.CancellationToken; - var compilation = await context.Document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var assemblyIds = compilation.GetUnreferencedAssemblyIdentities(diagnostic); + uniqueIdentities.AddRange(assemblyIds); - var uniqueIdentities = new HashSet(); - foreach (var diagnostic in context.Diagnostics) + var properties = diagnostic.Properties; + if (properties.TryGetValue(DiagnosticPropertyConstants.UnreferencedAssemblyIdentity, out var displayName) && + displayName != null && + AssemblyIdentity.TryParseDisplayName(displayName, out var serializedIdentity)) { - var assemblyIds = compilation.GetUnreferencedAssemblyIdentities(diagnostic); - uniqueIdentities.AddRange(assemblyIds); - - var properties = diagnostic.Properties; - if (properties.TryGetValue(DiagnosticPropertyConstants.UnreferencedAssemblyIdentity, out var displayName) && - displayName != null && - AssemblyIdentity.TryParseDisplayName(displayName, out var serializedIdentity)) - { - uniqueIdentities.Add(serializedIdentity); - } + uniqueIdentities.Add(serializedIdentity); } - - uniqueIdentities.Remove(compilation.Assembly.Identity); - return uniqueIdentities; } + + uniqueIdentities.Remove(compilation.Assembly.Identity); + return uniqueIdentities; } } diff --git a/src/Features/Core/Portable/AddMissingReference/AddMissingReferenceCodeAction.cs b/src/Features/Core/Portable/AddMissingReference/AddMissingReferenceCodeAction.cs index ec291eac07dec..467086a39cca9 100644 --- a/src/Features/Core/Portable/AddMissingReference/AddMissingReferenceCodeAction.cs +++ b/src/Features/Core/Portable/AddMissingReference/AddMissingReferenceCodeAction.cs @@ -12,83 +12,82 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddMissingReference +namespace Microsoft.CodeAnalysis.AddMissingReference; + +internal sealed class AddMissingReferenceCodeAction(Project project, string title, ProjectReference? projectReferenceToAdd, AssemblyIdentity missingAssemblyIdentity) : CodeAction { - internal sealed class AddMissingReferenceCodeAction(Project project, string title, ProjectReference? projectReferenceToAdd, AssemblyIdentity missingAssemblyIdentity) : CodeAction - { - private readonly Project _project = project; - private readonly ProjectReference? _projectReferenceToAdd = projectReferenceToAdd; - private readonly AssemblyIdentity _missingAssemblyIdentity = missingAssemblyIdentity; + private readonly Project _project = project; + private readonly ProjectReference? _projectReferenceToAdd = projectReferenceToAdd; + private readonly AssemblyIdentity _missingAssemblyIdentity = missingAssemblyIdentity; - public override string Title { get; } = title; + public override string Title { get; } = title; - /// - /// This code action only works by adding references. As such, it requires a non document change (and is - /// thus restricted in which hosts it can run). - /// - public override ImmutableArray Tags => RequiresNonDocumentChangeTags; + /// + /// This code action only works by adding references. As such, it requires a non document change (and is + /// thus restricted in which hosts it can run). + /// + public override ImmutableArray Tags => RequiresNonDocumentChangeTags; - public static async Task CreateAsync(Project project, AssemblyIdentity missingAssemblyIdentity, CancellationToken cancellationToken) - { - var dependencyGraph = project.Solution.GetProjectDependencyGraph(); + public static async Task CreateAsync(Project project, AssemblyIdentity missingAssemblyIdentity, CancellationToken cancellationToken) + { + var dependencyGraph = project.Solution.GetProjectDependencyGraph(); - // We want to find a project that generates this assembly, if one so exists. We therefore - // search all projects that our project with an error depends on. We want to do this for - // complicated and evil scenarios like this one: - // - // C -> B -> A - // - // A' - // - // Where, for some insane reason, A and A' are two projects that both emit an assembly - // by the same name. So imagine we are using a type in B from C, and we are missing a - // reference to A.dll. Both A and A' are candidates, but we know we can throw out A' - // since whatever type from B we are using that's causing the error, we know that type - // isn't referencing A'. Put another way: this code action adds a reference, but should - // never change the transitive closure of project references that C has. - // - // Doing this filtering also means we get to check less projects (good), and ensures that - // whatever project reference we end up adding won't add a circularity (also good.) - foreach (var candidateProjectId in dependencyGraph.GetProjectsThatThisProjectTransitivelyDependsOn(project.Id)) + // We want to find a project that generates this assembly, if one so exists. We therefore + // search all projects that our project with an error depends on. We want to do this for + // complicated and evil scenarios like this one: + // + // C -> B -> A + // + // A' + // + // Where, for some insane reason, A and A' are two projects that both emit an assembly + // by the same name. So imagine we are using a type in B from C, and we are missing a + // reference to A.dll. Both A and A' are candidates, but we know we can throw out A' + // since whatever type from B we are using that's causing the error, we know that type + // isn't referencing A'. Put another way: this code action adds a reference, but should + // never change the transitive closure of project references that C has. + // + // Doing this filtering also means we get to check less projects (good), and ensures that + // whatever project reference we end up adding won't add a circularity (also good.) + foreach (var candidateProjectId in dependencyGraph.GetProjectsThatThisProjectTransitivelyDependsOn(project.Id)) + { + var candidateProject = project.Solution.GetRequiredProject(candidateProjectId); + if (candidateProject.SupportsCompilation && + string.Equals(missingAssemblyIdentity.Name, candidateProject.AssemblyName, StringComparison.OrdinalIgnoreCase)) { - var candidateProject = project.Solution.GetRequiredProject(candidateProjectId); - if (candidateProject.SupportsCompilation && - string.Equals(missingAssemblyIdentity.Name, candidateProject.AssemblyName, StringComparison.OrdinalIgnoreCase)) + // The name matches, so let's see if the full identities are equal. + var compilation = await candidateProject.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + if (missingAssemblyIdentity.Equals(compilation.Assembly.Identity)) { - // The name matches, so let's see if the full identities are equal. - var compilation = await candidateProject.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - if (missingAssemblyIdentity.Equals(compilation.Assembly.Identity)) - { - // It matches, so just add a reference to this - return new AddMissingReferenceCodeAction(project, - string.Format(FeaturesResources.Add_project_reference_to_0, candidateProject.Name), - new ProjectReference(candidateProjectId), missingAssemblyIdentity); - } + // It matches, so just add a reference to this + return new AddMissingReferenceCodeAction(project, + string.Format(FeaturesResources.Add_project_reference_to_0, candidateProject.Name), + new ProjectReference(candidateProjectId), missingAssemblyIdentity); } } - - // No matching project, so metadata reference - var description = string.Format(FeaturesResources.Add_reference_to_0, missingAssemblyIdentity.GetDisplayName()); - return new AddMissingReferenceCodeAction(project, description, null, missingAssemblyIdentity); } - protected override Task> ComputeOperationsAsync( - IProgress progress, CancellationToken cancellationToken) + // No matching project, so metadata reference + var description = string.Format(FeaturesResources.Add_reference_to_0, missingAssemblyIdentity.GetDisplayName()); + return new AddMissingReferenceCodeAction(project, description, null, missingAssemblyIdentity); + } + + protected override Task> ComputeOperationsAsync( + IProgress progress, CancellationToken cancellationToken) + { + // If we have a project reference to add, then add it + if (_projectReferenceToAdd != null) { - // If we have a project reference to add, then add it - if (_projectReferenceToAdd != null) - { - // note: no need to post process since we are just adding a project reference and not making any code changes. - return Task.FromResult(ImmutableArray.Create( - new ApplyChangesOperation(_project.AddProjectReference(_projectReferenceToAdd).Solution))); - } - else - { - // We didn't have any project, so we need to try adding a metadata reference - var factoryService = _project.Solution.Services.GetRequiredService(); - var operation = factoryService.CreateAddMetadataReferenceOperation(_project.Id, _missingAssemblyIdentity); - return Task.FromResult(ImmutableArray.Create(operation)); - } + // note: no need to post process since we are just adding a project reference and not making any code changes. + return Task.FromResult(ImmutableArray.Create( + new ApplyChangesOperation(_project.AddProjectReference(_projectReferenceToAdd).Solution))); + } + else + { + // We didn't have any project, so we need to try adding a metadata reference + var factoryService = _project.Solution.Services.GetRequiredService(); + var operation = factoryService.CreateAddMetadataReferenceOperation(_project.Id, _missingAssemblyIdentity); + return Task.FromResult(ImmutableArray.Create(operation)); } } } diff --git a/src/Features/Core/Portable/AddPackage/AbstractAddPackageCodeFixProvider.cs b/src/Features/Core/Portable/AddPackage/AbstractAddPackageCodeFixProvider.cs index ea89224f80892..c6dd016be4013 100644 --- a/src/Features/Core/Portable/AddPackage/AbstractAddPackageCodeFixProvider.cs +++ b/src/Features/Core/Portable/AddPackage/AbstractAddPackageCodeFixProvider.cs @@ -16,88 +16,87 @@ using Microsoft.CodeAnalysis.SymbolSearch; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddPackage +namespace Microsoft.CodeAnalysis.AddPackage; + +internal abstract partial class AbstractAddPackageCodeFixProvider : CodeFixProvider { - internal abstract partial class AbstractAddPackageCodeFixProvider : CodeFixProvider + private readonly IPackageInstallerService _packageInstallerService; + private readonly ISymbolSearchService _symbolSearchService; + + /// + /// Values for these parameters can be provided (during testing) for mocking purposes. + /// + protected AbstractAddPackageCodeFixProvider( + IPackageInstallerService packageInstallerService, + ISymbolSearchService symbolSearchService) { - private readonly IPackageInstallerService _packageInstallerService; - private readonly ISymbolSearchService _symbolSearchService; - - /// - /// Values for these parameters can be provided (during testing) for mocking purposes. - /// - protected AbstractAddPackageCodeFixProvider( - IPackageInstallerService packageInstallerService, - ISymbolSearchService symbolSearchService) - { - _packageInstallerService = packageInstallerService; - _symbolSearchService = symbolSearchService; - } + _packageInstallerService = packageInstallerService; + _symbolSearchService = symbolSearchService; + } - protected abstract bool IncludePrerelease { get; } + protected abstract bool IncludePrerelease { get; } - public abstract override FixAllProvider GetFixAllProvider(); + public abstract override FixAllProvider GetFixAllProvider(); - protected async Task> GetAddPackagesCodeActionsAsync( - CodeFixContext context, ISet assemblyNames) - { - var document = context.Document; - var cancellationToken = context.CancellationToken; + protected async Task> GetAddPackagesCodeActionsAsync( + CodeFixContext context, ISet assemblyNames) + { + var document = context.Document; + var cancellationToken = context.CancellationToken; + + var workspaceServices = document.Project.Solution.Services; - var workspaceServices = document.Project.Solution.Services; + var symbolSearchService = _symbolSearchService ?? workspaceServices.GetService(); + var installerService = _packageInstallerService ?? workspaceServices.GetService(); - var symbolSearchService = _symbolSearchService ?? workspaceServices.GetService(); - var installerService = _packageInstallerService ?? workspaceServices.GetService(); + var codeActions = ArrayBuilder.GetInstance(); + if (symbolSearchService != null && + installerService != null && + context.Options.GetOptions(document.Project.Services).SearchOptions.SearchNuGetPackages && + installerService.IsEnabled(document.Project.Id)) + { + var packageSources = PackageSourceHelper.GetPackageSources(installerService.TryGetPackageSources()); - var codeActions = ArrayBuilder.GetInstance(); - if (symbolSearchService != null && - installerService != null && - context.Options.GetOptions(document.Project.Services).SearchOptions.SearchNuGetPackages && - installerService.IsEnabled(document.Project.Id)) + foreach (var (name, source) in packageSources) { - var packageSources = PackageSourceHelper.GetPackageSources(installerService.TryGetPackageSources()); + cancellationToken.ThrowIfCancellationRequested(); - foreach (var (name, source) in packageSources) + var sortedPackages = await FindMatchingPackagesAsync( + name, symbolSearchService, + assemblyNames, cancellationToken).ConfigureAwait(false); + + foreach (var package in sortedPackages) { - cancellationToken.ThrowIfCancellationRequested(); - - var sortedPackages = await FindMatchingPackagesAsync( - name, symbolSearchService, - assemblyNames, cancellationToken).ConfigureAwait(false); - - foreach (var package in sortedPackages) - { - codeActions.Add(new InstallPackageParentCodeAction( - installerService, source, - package.PackageName, IncludePrerelease, document)); - } + codeActions.Add(new InstallPackageParentCodeAction( + installerService, source, + package.PackageName, IncludePrerelease, document)); } } - - return codeActions.ToImmutableAndFree(); } - private static async Task> FindMatchingPackagesAsync( - string sourceName, - ISymbolSearchService searchService, - ISet assemblyNames, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var result = new HashSet(); - - foreach (var assemblyName in assemblyNames) - { - var packagesWithAssembly = await searchService.FindPackagesWithAssemblyAsync( - sourceName, assemblyName, cancellationToken).ConfigureAwait(false); + return codeActions.ToImmutableAndFree(); + } - result.AddRange(packagesWithAssembly); - } + private static async Task> FindMatchingPackagesAsync( + string sourceName, + ISymbolSearchService searchService, + ISet assemblyNames, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var result = new HashSet(); - // Ensure the packages are sorted by rank. - var sortedPackages = result.ToImmutableArray().Sort(); + foreach (var assemblyName in assemblyNames) + { + var packagesWithAssembly = await searchService.FindPackagesWithAssemblyAsync( + sourceName, assemblyName, cancellationToken).ConfigureAwait(false); - return sortedPackages; + result.AddRange(packagesWithAssembly); } + + // Ensure the packages are sorted by rank. + var sortedPackages = result.ToImmutableArray().Sort(); + + return sortedPackages; } } diff --git a/src/Features/Core/Portable/AddPackage/AbstractAddSpecificPackageCodeFixProvider.cs b/src/Features/Core/Portable/AddPackage/AbstractAddSpecificPackageCodeFixProvider.cs index a3dfca8ddbca7..f3bc83dce9071 100644 --- a/src/Features/Core/Portable/AddPackage/AbstractAddSpecificPackageCodeFixProvider.cs +++ b/src/Features/Core/Portable/AddPackage/AbstractAddSpecificPackageCodeFixProvider.cs @@ -10,41 +10,40 @@ using Microsoft.CodeAnalysis.Packaging; using Microsoft.CodeAnalysis.SymbolSearch; -namespace Microsoft.CodeAnalysis.AddPackage +namespace Microsoft.CodeAnalysis.AddPackage; + +internal abstract partial class AbstractAddSpecificPackageCodeFixProvider : AbstractAddPackageCodeFixProvider { - internal abstract partial class AbstractAddSpecificPackageCodeFixProvider : AbstractAddPackageCodeFixProvider + /// + /// Values for these parameters can be provided (during testing) for mocking purposes. + /// + protected AbstractAddSpecificPackageCodeFixProvider( + IPackageInstallerService packageInstallerService = null, + ISymbolSearchService symbolSearchService = null) + : base(packageInstallerService, symbolSearchService) { - /// - /// Values for these parameters can be provided (during testing) for mocking purposes. - /// - protected AbstractAddSpecificPackageCodeFixProvider( - IPackageInstallerService packageInstallerService = null, - ISymbolSearchService symbolSearchService = null) - : base(packageInstallerService, symbolSearchService) - { - } + } - protected override bool IncludePrerelease => true; + protected override bool IncludePrerelease => true; - public override FixAllProvider GetFixAllProvider() - { - // Fix All is not supported by this code fix - // https://github.com/dotnet/roslyn/issues/34458 - return null; - } + public override FixAllProvider GetFixAllProvider() + { + // Fix All is not supported by this code fix + // https://github.com/dotnet/roslyn/issues/34458 + return null; + } + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var assemblyName = GetAssemblyName(context.Diagnostics[0].Id); - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + if (assemblyName != null) { - var assemblyName = GetAssemblyName(context.Diagnostics[0].Id); - - if (assemblyName != null) - { - var assemblyNames = new HashSet { assemblyName }; - var addPackageCodeActions = await GetAddPackagesCodeActionsAsync(context, assemblyNames).ConfigureAwait(false); - context.RegisterFixes(addPackageCodeActions, context.Diagnostics); - } + var assemblyNames = new HashSet { assemblyName }; + var addPackageCodeActions = await GetAddPackagesCodeActionsAsync(context, assemblyNames).ConfigureAwait(false); + context.RegisterFixes(addPackageCodeActions, context.Diagnostics); } - - protected abstract string GetAssemblyName(string id); } + + protected abstract string GetAssemblyName(string id); } diff --git a/src/Features/Core/Portable/AddPackage/InstallPackageDirectlyCodeAction.cs b/src/Features/Core/Portable/AddPackage/InstallPackageDirectlyCodeAction.cs index d049b04cca2cf..3d1994705418b 100644 --- a/src/Features/Core/Portable/AddPackage/InstallPackageDirectlyCodeAction.cs +++ b/src/Features/Core/Portable/AddPackage/InstallPackageDirectlyCodeAction.cs @@ -11,28 +11,27 @@ using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Packaging; -namespace Microsoft.CodeAnalysis.AddPackage +namespace Microsoft.CodeAnalysis.AddPackage; + +internal sealed class InstallPackageDirectlyCodeAction( + IPackageInstallerService installerService, + Document document, + string source, + string packageName, + string versionOpt, + bool includePrerelease, + bool isLocal) : CodeAction { - internal sealed class InstallPackageDirectlyCodeAction( - IPackageInstallerService installerService, - Document document, - string source, - string packageName, - string versionOpt, - bool includePrerelease, - bool isLocal) : CodeAction - { - private readonly CodeActionOperation _installPackageOperation = new InstallPackageDirectlyCodeActionOperation( - installerService, document, source, packageName, - versionOpt, includePrerelease, isLocal); + private readonly CodeActionOperation _installPackageOperation = new InstallPackageDirectlyCodeActionOperation( + installerService, document, source, packageName, + versionOpt, includePrerelease, isLocal); - public override string Title { get; } = versionOpt == null - ? FeaturesResources.Find_and_install_latest_version - : isLocal - ? string.Format(FeaturesResources.Use_local_version_0, versionOpt) - : string.Format(FeaturesResources.Install_version_0, versionOpt); + public override string Title { get; } = versionOpt == null + ? FeaturesResources.Find_and_install_latest_version + : isLocal + ? string.Format(FeaturesResources.Use_local_version_0, versionOpt) + : string.Format(FeaturesResources.Install_version_0, versionOpt); - protected override Task> ComputeOperationsAsync(IProgress progress, CancellationToken cancellationToken) - => Task.FromResult(ImmutableArray.Create(_installPackageOperation)); - } + protected override Task> ComputeOperationsAsync(IProgress progress, CancellationToken cancellationToken) + => Task.FromResult(ImmutableArray.Create(_installPackageOperation)); } diff --git a/src/Features/Core/Portable/AddPackage/InstallPackageDirectlyCodeActionOperation.cs b/src/Features/Core/Portable/AddPackage/InstallPackageDirectlyCodeActionOperation.cs index e23fcdf28dfd6..b248ccaf7f8ea 100644 --- a/src/Features/Core/Portable/AddPackage/InstallPackageDirectlyCodeActionOperation.cs +++ b/src/Features/Core/Portable/AddPackage/InstallPackageDirectlyCodeActionOperation.cs @@ -10,66 +10,65 @@ using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Packaging; -namespace Microsoft.CodeAnalysis.AddPackage +namespace Microsoft.CodeAnalysis.AddPackage; + +/// +/// Operation responsible purely for installing a nuget package with a specific +/// version, or a the latest version of a nuget package. Is not responsible +/// for adding an import to user code. +/// +internal sealed class InstallPackageDirectlyCodeActionOperation : CodeActionOperation { - /// - /// Operation responsible purely for installing a nuget package with a specific - /// version, or a the latest version of a nuget package. Is not responsible - /// for adding an import to user code. - /// - internal sealed class InstallPackageDirectlyCodeActionOperation : CodeActionOperation + private readonly Document _document; + private readonly IPackageInstallerService _installerService; + private readonly string? _source; + private readonly string _packageName; + private readonly string? _versionOpt; + private readonly bool _includePrerelease; + private readonly bool _isLocal; + private readonly List _projectsWithMatchingVersion = []; + + public InstallPackageDirectlyCodeActionOperation( + IPackageInstallerService installerService, + Document document, + string? source, + string packageName, + string? versionOpt, + bool includePrerelease, + bool isLocal) { - private readonly Document _document; - private readonly IPackageInstallerService _installerService; - private readonly string? _source; - private readonly string _packageName; - private readonly string? _versionOpt; - private readonly bool _includePrerelease; - private readonly bool _isLocal; - private readonly List _projectsWithMatchingVersion = []; + _installerService = installerService; + _document = document; + _source = source; + _packageName = packageName; + _versionOpt = versionOpt; + _includePrerelease = includePrerelease; + _isLocal = isLocal; - public InstallPackageDirectlyCodeActionOperation( - IPackageInstallerService installerService, - Document document, - string? source, - string packageName, - string? versionOpt, - bool includePrerelease, - bool isLocal) + if (versionOpt != null) { - _installerService = installerService; - _document = document; - _source = source; - _packageName = packageName; - _versionOpt = versionOpt; - _includePrerelease = includePrerelease; - _isLocal = isLocal; - - if (versionOpt != null) - { - const int projectsToShow = 5; - var otherProjects = installerService.GetProjectsWithInstalledPackage( - _document.Project.Solution, packageName, versionOpt).ToList(); - _projectsWithMatchingVersion.AddRange(otherProjects.Take(projectsToShow).Select(p => p.Name)); - if (otherProjects.Count > projectsToShow) - _projectsWithMatchingVersion.Add("..."); - } + const int projectsToShow = 5; + var otherProjects = installerService.GetProjectsWithInstalledPackage( + _document.Project.Solution, packageName, versionOpt).ToList(); + _projectsWithMatchingVersion.AddRange(otherProjects.Take(projectsToShow).Select(p => p.Name)); + if (otherProjects.Count > projectsToShow) + _projectsWithMatchingVersion.Add("..."); } + } - public override string Title => _versionOpt == null - ? string.Format(FeaturesResources.Find_and_install_latest_version_of_0, _packageName) - : _isLocal - ? string.Format(FeaturesResources.Use_locally_installed_0_version_1_This_version_used_in_colon_2, _packageName, _versionOpt, string.Join(", ", _projectsWithMatchingVersion)) - : string.Format(FeaturesResources.Install_0_1, _packageName, _versionOpt); + public override string Title => _versionOpt == null + ? string.Format(FeaturesResources.Find_and_install_latest_version_of_0, _packageName) + : _isLocal + ? string.Format(FeaturesResources.Use_locally_installed_0_version_1_This_version_used_in_colon_2, _packageName, _versionOpt, string.Join(", ", _projectsWithMatchingVersion)) + : string.Format(FeaturesResources.Install_0_1, _packageName, _versionOpt); - internal override bool ApplyDuringTests => true; + internal override bool ApplyDuringTests => true; - internal override Task TryApplyAsync( - Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) - { - return _installerService.TryInstallPackageAsync( - workspace, _document.Id, _source, _packageName, - _versionOpt, _includePrerelease, progressTracker, cancellationToken); - } + internal override Task TryApplyAsync( + Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) + { + return _installerService.TryInstallPackageAsync( + workspace, _document.Id, _source, _packageName, + _versionOpt, _includePrerelease, progressTracker, cancellationToken); } } diff --git a/src/Features/Core/Portable/AddPackage/InstallPackageParentCodeAction.cs b/src/Features/Core/Portable/AddPackage/InstallPackageParentCodeAction.cs index b2e32c946522f..38180d40490e3 100644 --- a/src/Features/Core/Portable/AddPackage/InstallPackageParentCodeAction.cs +++ b/src/Features/Core/Portable/AddPackage/InstallPackageParentCodeAction.cs @@ -11,70 +11,69 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Tags; -namespace Microsoft.CodeAnalysis.AddPackage +namespace Microsoft.CodeAnalysis.AddPackage; + +/// +/// This is the top level 'Install Nuget Package' code action we show in +/// the lightbulb. It will have children to 'Install Latest', +/// 'Install Version 'X' ..., and 'Install with package manager'. +/// +/// +/// Even though we have child actions, we mark ourselves as explicitly non-inlinable. +/// We want to the experience of having the top level item the user has to see and +/// navigate through, and we don't want our child items confusingly being added to the +/// top level light-bulb where it's not clear what effect they would have if invoked. +/// +internal class InstallPackageParentCodeAction( + IPackageInstallerService installerService, + string source, + string packageName, + bool includePrerelease, + Document document) : CodeAction.CodeActionWithNestedActions(string.Format(FeaturesResources.Install_package_0, packageName), + CreateNestedActions(installerService, source, packageName, includePrerelease, document), + isInlinable: false) { /// - /// This is the top level 'Install Nuget Package' code action we show in - /// the lightbulb. It will have children to 'Install Latest', - /// 'Install Version 'X' ..., and 'Install with package manager'. + /// This code action only works by installing a package. As such, it requires a non document change (and is + /// thus restricted in which hosts it can run). /// - /// - /// Even though we have child actions, we mark ourselves as explicitly non-inlinable. - /// We want to the experience of having the top level item the user has to see and - /// navigate through, and we don't want our child items confusingly being added to the - /// top level light-bulb where it's not clear what effect they would have if invoked. - /// - internal class InstallPackageParentCodeAction( + public override ImmutableArray Tags => RequiresNonDocumentChangeTags; + + private static ImmutableArray CreateNestedActions( + IPackageInstallerService installerService, + string source, string packageName, bool includePrerelease, + Document document) + { + // Determine what versions of this package are already installed in some project + // in this solution. We'll offer to add those specific versions to this project, + // followed by an option to "Find and install latest version." + var installedVersions = installerService.GetInstalledVersions(packageName); + return + [ + // First add the actions to install a specific version. + .. installedVersions.Select(v => CreateCodeAction( + installerService, source, packageName, document, + versionOpt: v, includePrerelease: includePrerelease, isLocal: true)), + // Now add the action to install the specific version. + CreateCodeAction( + installerService, source, packageName, document, + versionOpt: null, includePrerelease: includePrerelease, isLocal: false), + // And finally the action to show the package manager dialog. + new InstallWithPackageManagerCodeAction(installerService, packageName), + ]; + } + + private static CodeAction CreateCodeAction( IPackageInstallerService installerService, string source, string packageName, + Document document, + string versionOpt, bool includePrerelease, - Document document) : CodeAction.CodeActionWithNestedActions(string.Format(FeaturesResources.Install_package_0, packageName), - CreateNestedActions(installerService, source, packageName, includePrerelease, document), - isInlinable: false) + bool isLocal) { - /// - /// This code action only works by installing a package. As such, it requires a non document change (and is - /// thus restricted in which hosts it can run). - /// - public override ImmutableArray Tags => RequiresNonDocumentChangeTags; - - private static ImmutableArray CreateNestedActions( - IPackageInstallerService installerService, - string source, string packageName, bool includePrerelease, - Document document) - { - // Determine what versions of this package are already installed in some project - // in this solution. We'll offer to add those specific versions to this project, - // followed by an option to "Find and install latest version." - var installedVersions = installerService.GetInstalledVersions(packageName); - return - [ - // First add the actions to install a specific version. - .. installedVersions.Select(v => CreateCodeAction( - installerService, source, packageName, document, - versionOpt: v, includePrerelease: includePrerelease, isLocal: true)), - // Now add the action to install the specific version. - CreateCodeAction( - installerService, source, packageName, document, - versionOpt: null, includePrerelease: includePrerelease, isLocal: false), - // And finally the action to show the package manager dialog. - new InstallWithPackageManagerCodeAction(installerService, packageName), - ]; - } - - private static CodeAction CreateCodeAction( - IPackageInstallerService installerService, - string source, - string packageName, - Document document, - string versionOpt, - bool includePrerelease, - bool isLocal) - { - return new InstallPackageDirectlyCodeAction( - installerService, document, source, packageName, - versionOpt, includePrerelease, isLocal); - } + return new InstallPackageDirectlyCodeAction( + installerService, document, source, packageName, + versionOpt, includePrerelease, isLocal); } } diff --git a/src/Features/Core/Portable/AddPackage/InstallWithPackageManagerCodeAction.cs b/src/Features/Core/Portable/AddPackage/InstallWithPackageManagerCodeAction.cs index a0802920c8b8d..a60e4bcabe987 100644 --- a/src/Features/Core/Portable/AddPackage/InstallWithPackageManagerCodeAction.cs +++ b/src/Features/Core/Portable/AddPackage/InstallWithPackageManagerCodeAction.cs @@ -11,32 +11,31 @@ using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Packaging; -namespace Microsoft.CodeAnalysis.AddPackage +namespace Microsoft.CodeAnalysis.AddPackage; + +internal sealed class InstallWithPackageManagerCodeAction( + IPackageInstallerService installerService, string packageName) : CodeAction { - internal sealed class InstallWithPackageManagerCodeAction( - IPackageInstallerService installerService, string packageName) : CodeAction - { - private readonly IPackageInstallerService _installerService = installerService; - private readonly string _packageName = packageName; + private readonly IPackageInstallerService _installerService = installerService; + private readonly string _packageName = packageName; - public override string Title => FeaturesResources.Install_with_package_manager; + public override string Title => FeaturesResources.Install_with_package_manager; - protected override Task> ComputeOperationsAsync( - IProgress progress, CancellationToken cancellationToken) - { - return Task.FromResult(ImmutableArray.Create( - new InstallWithPackageManagerCodeActionOperation(this))); - } + protected override Task> ComputeOperationsAsync( + IProgress progress, CancellationToken cancellationToken) + { + return Task.FromResult(ImmutableArray.Create( + new InstallWithPackageManagerCodeActionOperation(this))); + } - private class InstallWithPackageManagerCodeActionOperation( - InstallWithPackageManagerCodeAction codeAction) : CodeActionOperation - { - private readonly InstallWithPackageManagerCodeAction _codeAction = codeAction; + private class InstallWithPackageManagerCodeActionOperation( + InstallWithPackageManagerCodeAction codeAction) : CodeActionOperation + { + private readonly InstallWithPackageManagerCodeAction _codeAction = codeAction; - public override string Title => FeaturesResources.Install_with_package_manager; + public override string Title => FeaturesResources.Install_with_package_manager; - public override void Apply(Workspace workspace, CancellationToken cancellationToken) - => _codeAction._installerService.ShowManagePackagesDialog(_codeAction._packageName); - } + public override void Apply(Workspace workspace, CancellationToken cancellationToken) + => _codeAction._installerService.ShowManagePackagesDialog(_codeAction._packageName); } } diff --git a/src/Features/Core/Portable/BraceCompletion/AbstractBraceCompletionService.cs b/src/Features/Core/Portable/BraceCompletion/AbstractBraceCompletionService.cs index afd30e0b060ee..9f900340cee7a 100644 --- a/src/Features/Core/Portable/BraceCompletion/AbstractBraceCompletionService.cs +++ b/src/Features/Core/Portable/BraceCompletion/AbstractBraceCompletionService.cs @@ -12,212 +12,211 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.BraceCompletion +namespace Microsoft.CodeAnalysis.BraceCompletion; + +internal abstract class AbstractBraceCompletionService : IBraceCompletionService { - internal abstract class AbstractBraceCompletionService : IBraceCompletionService - { - protected abstract ISyntaxFacts SyntaxFacts { get; } + protected abstract ISyntaxFacts SyntaxFacts { get; } - protected abstract char OpeningBrace { get; } - protected abstract char ClosingBrace { get; } + protected abstract char OpeningBrace { get; } + protected abstract char ClosingBrace { get; } - /// - /// Whether or not this brace completion session actually needs semantics to work (and thus should get a semantic model). - /// - protected virtual bool NeedsSemantics => false; + /// + /// Whether or not this brace completion session actually needs semantics to work (and thus should get a semantic model). + /// + protected virtual bool NeedsSemantics => false; - /// - /// Returns if the token is a valid opening token kind for this brace completion service. - /// - protected abstract bool IsValidOpeningBraceToken(SyntaxToken token); + /// + /// Returns if the token is a valid opening token kind for this brace completion service. + /// + protected abstract bool IsValidOpeningBraceToken(SyntaxToken token); - /// - /// Returns if the token is a valid closing token kind for this brace completion service. - /// - protected abstract bool IsValidClosingBraceToken(SyntaxToken token); + /// + /// Returns if the token is a valid closing token kind for this brace completion service. + /// + protected abstract bool IsValidClosingBraceToken(SyntaxToken token); - public abstract bool AllowOverType(BraceCompletionContext braceCompletionContext, CancellationToken cancellationToken); + public abstract bool AllowOverType(BraceCompletionContext braceCompletionContext, CancellationToken cancellationToken); - public Task HasBraceCompletionAsync(BraceCompletionContext context, Document document, CancellationToken cancellationToken) + public Task HasBraceCompletionAsync(BraceCompletionContext context, Document document, CancellationToken cancellationToken) + { + if (!context.HasCompletionForOpeningBrace(OpeningBrace)) { - if (!context.HasCompletionForOpeningBrace(OpeningBrace)) - { - return Task.FromResult(false); - } - - var openingToken = context.GetOpeningToken(); - if (!NeedsSemantics) - { - return Task.FromResult(IsValidOpenBraceTokenAtPosition(context.Document.Text, openingToken, context.OpeningPoint)); - } - - // Pass along a document with frozen partial semantics. Brace completion is a highly latency sensitive - // operation. We don't want to wait on things like source generators to figure things out. - return IsValidOpenBraceTokenAtPositionAsync(document.WithFrozenPartialSemantics(cancellationToken), openingToken, context.OpeningPoint, cancellationToken); + return Task.FromResult(false); } - public BraceCompletionResult GetBraceCompletion(BraceCompletionContext context) + var openingToken = context.GetOpeningToken(); + if (!NeedsSemantics) { - Debug.Assert(context.HasCompletionForOpeningBrace(OpeningBrace)); + return Task.FromResult(IsValidOpenBraceTokenAtPosition(context.Document.Text, openingToken, context.OpeningPoint)); + } - var closingPoint = context.ClosingPoint; - var braceTextEdit = new TextChange(TextSpan.FromBounds(closingPoint, closingPoint), ClosingBrace.ToString()); + // Pass along a document with frozen partial semantics. Brace completion is a highly latency sensitive + // operation. We don't want to wait on things like source generators to figure things out. + return IsValidOpenBraceTokenAtPositionAsync(document.WithFrozenPartialSemantics(cancellationToken), openingToken, context.OpeningPoint, cancellationToken); + } - // The caret location should be in between the braces. - var originalOpeningLinePosition = context.Document.Text.Lines.GetLinePosition(context.OpeningPoint); - var caretLocation = new LinePosition(originalOpeningLinePosition.Line, originalOpeningLinePosition.Character + 1); - return new BraceCompletionResult([braceTextEdit], caretLocation); - } + public BraceCompletionResult GetBraceCompletion(BraceCompletionContext context) + { + Debug.Assert(context.HasCompletionForOpeningBrace(OpeningBrace)); - public virtual BraceCompletionResult? GetTextChangesAfterCompletion(BraceCompletionContext braceCompletionContext, IndentationOptions options, CancellationToken cancellationToken) - => null; + var closingPoint = context.ClosingPoint; + var braceTextEdit = new TextChange(TextSpan.FromBounds(closingPoint, closingPoint), ClosingBrace.ToString()); - public virtual BraceCompletionResult? GetTextChangeAfterReturn(BraceCompletionContext braceCompletionContext, IndentationOptions options, CancellationToken cancellationToken) - => null; + // The caret location should be in between the braces. + var originalOpeningLinePosition = context.Document.Text.Lines.GetLinePosition(context.OpeningPoint); + var caretLocation = new LinePosition(originalOpeningLinePosition.Line, originalOpeningLinePosition.Character + 1); + return new BraceCompletionResult([braceTextEdit], caretLocation); + } - public virtual bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken) - { - if (OpeningBrace != brace) - { - return false; - } + public virtual BraceCompletionResult? GetTextChangesAfterCompletion(BraceCompletionContext braceCompletionContext, IndentationOptions options, CancellationToken cancellationToken) + => null; - // check that the user is not typing in a string literal or comment - var syntaxFactsService = document.LanguageServices.GetRequiredService(); + public virtual BraceCompletionResult? GetTextChangeAfterReturn(BraceCompletionContext braceCompletionContext, IndentationOptions options, CancellationToken cancellationToken) + => null; - return !syntaxFactsService.IsInNonUserCode(document.SyntaxTree, openingPosition, cancellationToken); + public virtual bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken) + { + if (OpeningBrace != brace) + { + return false; } - public BraceCompletionContext? GetCompletedBraceContext(ParsedDocument document, int caretLocation) - { - var leftToken = document.Root.FindTokenOnLeftOfPosition(caretLocation); - var rightToken = document.Root.FindTokenOnRightOfPosition(caretLocation); + // check that the user is not typing in a string literal or comment + var syntaxFactsService = document.LanguageServices.GetRequiredService(); - if (IsValidOpeningBraceToken(leftToken) && IsValidClosingBraceToken(rightToken)) - { - return new BraceCompletionContext(document, leftToken.GetLocation().SourceSpan.Start, rightToken.GetLocation().SourceSpan.End, caretLocation); - } + return !syntaxFactsService.IsInNonUserCode(document.SyntaxTree, openingPosition, cancellationToken); + } - return null; - } + public BraceCompletionContext? GetCompletedBraceContext(ParsedDocument document, int caretLocation) + { + var leftToken = document.Root.FindTokenOnLeftOfPosition(caretLocation); + var rightToken = document.Root.FindTokenOnRightOfPosition(caretLocation); - /// - /// Only called if returns true; - /// - protected virtual Task IsValidOpenBraceTokenAtPositionAsync(Document document, SyntaxToken token, int position, CancellationToken cancellationToken) + if (IsValidOpeningBraceToken(leftToken) && IsValidClosingBraceToken(rightToken)) { - // Subclass should have overridden this. - throw ExceptionUtilities.Unreachable(); + return new BraceCompletionContext(document, leftToken.GetLocation().SourceSpan.Start, rightToken.GetLocation().SourceSpan.End, caretLocation); } - /// - /// Checks if the already inserted token is a valid opening token at the position in the document. - /// By default checks that the opening token is a valid token at the position and not in skipped token trivia. - /// - protected virtual bool IsValidOpenBraceTokenAtPosition(SourceText text, SyntaxToken token, int position) - => token.SpanStart == position && IsValidOpeningBraceToken(token) && !ParentIsSkippedTokensTriviaOrNull(this.SyntaxFacts, token); - - /// - /// Returns true when the current position is inside user code (e.g. not strings) and the closing token - /// matches the expected closing token for this brace completion service. - /// Helper method used by implementations. - /// - protected bool AllowOverTypeInUserCodeWithValidClosingToken(BraceCompletionContext context, CancellationToken cancellationToken) - { - var tree = context.Document.SyntaxTree; - var syntaxFactsService = context.Document.LanguageServices.GetRequiredService(); + return null; + } - return !syntaxFactsService.IsInNonUserCode(tree, context.CaretLocation, cancellationToken) - && CheckClosingTokenKind(context.Document, context.ClosingPoint); - } + /// + /// Only called if returns true; + /// + protected virtual Task IsValidOpenBraceTokenAtPositionAsync(Document document, SyntaxToken token, int position, CancellationToken cancellationToken) + { + // Subclass should have overridden this. + throw ExceptionUtilities.Unreachable(); + } - /// - /// Returns true when the closing token matches the expected closing token for this brace completion service. - /// Used by implementations - /// when the over type could be triggered from outside of user code (e.g. overtyping end quotes in a string). - /// - protected bool AllowOverTypeWithValidClosingToken(BraceCompletionContext context) - { - return CheckClosingTokenKind(context.Document, context.ClosingPoint); - } + /// + /// Checks if the already inserted token is a valid opening token at the position in the document. + /// By default checks that the opening token is a valid token at the position and not in skipped token trivia. + /// + protected virtual bool IsValidOpenBraceTokenAtPosition(SourceText text, SyntaxToken token, int position) + => token.SpanStart == position && IsValidOpeningBraceToken(token) && !ParentIsSkippedTokensTriviaOrNull(this.SyntaxFacts, token); + + /// + /// Returns true when the current position is inside user code (e.g. not strings) and the closing token + /// matches the expected closing token for this brace completion service. + /// Helper method used by implementations. + /// + protected bool AllowOverTypeInUserCodeWithValidClosingToken(BraceCompletionContext context, CancellationToken cancellationToken) + { + var tree = context.Document.SyntaxTree; + var syntaxFactsService = context.Document.LanguageServices.GetRequiredService(); - protected static bool ParentIsSkippedTokensTriviaOrNull(ISyntaxFacts syntaxFacts, SyntaxToken token) - => token.Parent == null || syntaxFacts.IsSkippedTokensTrivia(token.Parent); + return !syntaxFactsService.IsInNonUserCode(tree, context.CaretLocation, cancellationToken) + && CheckClosingTokenKind(context.Document, context.ClosingPoint); + } - /// - /// Checks that the token at the closing position is a valid closing token. - /// - private bool CheckClosingTokenKind(ParsedDocument document, int closingPosition) - { - var closingToken = document.Root.FindTokenFromEnd(closingPosition, includeZeroWidth: false, findInsideTrivia: true); - return IsValidClosingBraceToken(closingToken); - } + /// + /// Returns true when the closing token matches the expected closing token for this brace completion service. + /// Used by implementations + /// when the over type could be triggered from outside of user code (e.g. overtyping end quotes in a string). + /// + protected bool AllowOverTypeWithValidClosingToken(BraceCompletionContext context) + { + return CheckClosingTokenKind(context.Document, context.ClosingPoint); + } - public static class CurlyBrace - { - public const char OpenCharacter = '{'; - public const char CloseCharacter = '}'; - } + protected static bool ParentIsSkippedTokensTriviaOrNull(ISyntaxFacts syntaxFacts, SyntaxToken token) + => token.Parent == null || syntaxFacts.IsSkippedTokensTrivia(token.Parent); - public static class Parenthesis - { - public const char OpenCharacter = '('; - public const char CloseCharacter = ')'; - } + /// + /// Checks that the token at the closing position is a valid closing token. + /// + private bool CheckClosingTokenKind(ParsedDocument document, int closingPosition) + { + var closingToken = document.Root.FindTokenFromEnd(closingPosition, includeZeroWidth: false, findInsideTrivia: true); + return IsValidClosingBraceToken(closingToken); + } - public static class Bracket - { - public const char OpenCharacter = '['; - public const char CloseCharacter = ']'; - } + public static class CurlyBrace + { + public const char OpenCharacter = '{'; + public const char CloseCharacter = '}'; + } - public static class LessAndGreaterThan - { - public const char OpenCharacter = '<'; - public const char CloseCharacter = '>'; - } + public static class Parenthesis + { + public const char OpenCharacter = '('; + public const char CloseCharacter = ')'; + } - public static class DoubleQuote - { - public const char OpenCharacter = '"'; - public const char CloseCharacter = '"'; - } + public static class Bracket + { + public const char OpenCharacter = '['; + public const char CloseCharacter = ']'; + } - public static class SingleQuote - { - public const char OpenCharacter = '\''; - public const char CloseCharacter = '\''; - } + public static class LessAndGreaterThan + { + public const char OpenCharacter = '<'; + public const char CloseCharacter = '>'; + } + + public static class DoubleQuote + { + public const char OpenCharacter = '"'; + public const char CloseCharacter = '"'; + } + + public static class SingleQuote + { + public const char OpenCharacter = '\''; + public const char CloseCharacter = '\''; + } - /// - /// Determines if inserting the opening brace at the location could be an attempt to - /// escape a previously inserted opening brace. - /// E.g. they are trying to type $"{{" - /// - protected static bool CouldEscapePreviousOpenBrace(char openingBrace, int position, SourceText text) + /// + /// Determines if inserting the opening brace at the location could be an attempt to + /// escape a previously inserted opening brace. + /// E.g. they are trying to type $"{{" + /// + protected static bool CouldEscapePreviousOpenBrace(char openingBrace, int position, SourceText text) + { + var index = position - 1; + var openBraceCount = 0; + while (index >= 0) { - var index = position - 1; - var openBraceCount = 0; - while (index >= 0) + if (text[index] == openingBrace) { - if (text[index] == openingBrace) - { - openBraceCount++; - } - else - { - break; - } - - index--; + openBraceCount++; } - - if (openBraceCount > 0 && openBraceCount % 2 == 1) + else { - return true; + break; } - return false; + index--; } + + if (openBraceCount > 0 && openBraceCount % 2 == 1) + { + return true; + } + + return false; } } diff --git a/src/Features/Core/Portable/BraceCompletion/IBraceCompletionService.cs b/src/Features/Core/Portable/BraceCompletion/IBraceCompletionService.cs index 0d2c51038e9c1..1930a30ceb821 100644 --- a/src/Features/Core/Portable/BraceCompletion/IBraceCompletionService.cs +++ b/src/Features/Core/Portable/BraceCompletion/IBraceCompletionService.cs @@ -10,101 +10,100 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.BraceCompletion +namespace Microsoft.CodeAnalysis.BraceCompletion; + +internal interface IBraceCompletionService +{ + /// + /// Checks if this brace completion service should be the service used to provide brace completions at + /// the specified position with the specified opening brace. + /// + /// Only one implementation of should return true + /// for a given brace, opening position, and document. Only that service will be asked + /// for brace completion results. + /// + /// + /// The opening brace character to be inserted at the opening position. + /// + /// The opening position to insert the brace. + /// Note that the brace is not yet inserted at this position in the document. + /// + /// The document to insert the brace at the position. + /// A cancellation token. + bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken); + + /// + /// True if is available in the given . + /// Completes synchronously unless the service needs Semantic Model to determine the brace completion result. + /// + Task HasBraceCompletionAsync(BraceCompletionContext context, Document document, CancellationToken cancellationToken); + + /// + /// Returns the text change to add the closing brace given the context. + /// + BraceCompletionResult GetBraceCompletion(BraceCompletionContext braceCompletionContext); + + /// + /// Returns any text changes that need to be made after adding the closing brace. + /// + /// + /// This cannot be merged with + /// as we need to swap the editor tracking mode of the closing point from positive to negative + /// in BraceCompletionSessionProvider.BraceCompletionSession.Start after completing the brace and before + /// doing any kind of formatting on it. So these must be two distinct steps until we fully move to LSP. + /// + BraceCompletionResult? GetTextChangesAfterCompletion(BraceCompletionContext braceCompletionContext, IndentationOptions options, CancellationToken cancellationToken); + + /// + /// Get any text changes that should be applied after the enter key is typed inside a brace completion context. + /// + BraceCompletionResult? GetTextChangeAfterReturn(BraceCompletionContext braceCompletionContext, IndentationOptions options, CancellationToken cancellationToken); + + /// + /// Returns the brace completion context if the caret is located between an already completed + /// set of braces with only whitespace in between. + /// + BraceCompletionContext? GetCompletedBraceContext(ParsedDocument document, int caretLocation); + + /// + /// Returns true if over typing should be allowed given the caret location and completed pair of braces. + /// For example some providers allow over typing in non-user code and others do not. + /// + bool AllowOverType(BraceCompletionContext braceCompletionContext, CancellationToken cancellationToken); +} + +internal readonly struct BraceCompletionResult(ImmutableArray textChanges, LinePosition caretLocation) +{ + /// + /// The set of text changes that should be applied to the input text to retrieve the + /// brace completion result. + /// + public ImmutableArray TextChanges { get; } = textChanges; + + /// + /// The caret location in the new text created by applying all + /// to the input text. Note the column in the line position can be virtual in that it points + /// to a location in the line which does not actually contain whitespace. + /// Hosts can determine how best to handle that virtual location. + /// For example, placing the character in virtual space (when suppported) + /// or inserting an appropriate number of spaces into the document". + /// + public LinePosition CaretLocation { get; } = caretLocation; +} + +internal readonly struct BraceCompletionContext(ParsedDocument document, int openingPoint, int closingPoint, int caretLocation) { - internal interface IBraceCompletionService - { - /// - /// Checks if this brace completion service should be the service used to provide brace completions at - /// the specified position with the specified opening brace. - /// - /// Only one implementation of should return true - /// for a given brace, opening position, and document. Only that service will be asked - /// for brace completion results. - /// - /// - /// The opening brace character to be inserted at the opening position. - /// - /// The opening position to insert the brace. - /// Note that the brace is not yet inserted at this position in the document. - /// - /// The document to insert the brace at the position. - /// A cancellation token. - bool CanProvideBraceCompletion(char brace, int openingPosition, ParsedDocument document, CancellationToken cancellationToken); - - /// - /// True if is available in the given . - /// Completes synchronously unless the service needs Semantic Model to determine the brace completion result. - /// - Task HasBraceCompletionAsync(BraceCompletionContext context, Document document, CancellationToken cancellationToken); - - /// - /// Returns the text change to add the closing brace given the context. - /// - BraceCompletionResult GetBraceCompletion(BraceCompletionContext braceCompletionContext); - - /// - /// Returns any text changes that need to be made after adding the closing brace. - /// - /// - /// This cannot be merged with - /// as we need to swap the editor tracking mode of the closing point from positive to negative - /// in BraceCompletionSessionProvider.BraceCompletionSession.Start after completing the brace and before - /// doing any kind of formatting on it. So these must be two distinct steps until we fully move to LSP. - /// - BraceCompletionResult? GetTextChangesAfterCompletion(BraceCompletionContext braceCompletionContext, IndentationOptions options, CancellationToken cancellationToken); - - /// - /// Get any text changes that should be applied after the enter key is typed inside a brace completion context. - /// - BraceCompletionResult? GetTextChangeAfterReturn(BraceCompletionContext braceCompletionContext, IndentationOptions options, CancellationToken cancellationToken); - - /// - /// Returns the brace completion context if the caret is located between an already completed - /// set of braces with only whitespace in between. - /// - BraceCompletionContext? GetCompletedBraceContext(ParsedDocument document, int caretLocation); - - /// - /// Returns true if over typing should be allowed given the caret location and completed pair of braces. - /// For example some providers allow over typing in non-user code and others do not. - /// - bool AllowOverType(BraceCompletionContext braceCompletionContext, CancellationToken cancellationToken); - } - - internal readonly struct BraceCompletionResult(ImmutableArray textChanges, LinePosition caretLocation) - { - /// - /// The set of text changes that should be applied to the input text to retrieve the - /// brace completion result. - /// - public ImmutableArray TextChanges { get; } = textChanges; - - /// - /// The caret location in the new text created by applying all - /// to the input text. Note the column in the line position can be virtual in that it points - /// to a location in the line which does not actually contain whitespace. - /// Hosts can determine how best to handle that virtual location. - /// For example, placing the character in virtual space (when suppported) - /// or inserting an appropriate number of spaces into the document". - /// - public LinePosition CaretLocation { get; } = caretLocation; - } - - internal readonly struct BraceCompletionContext(ParsedDocument document, int openingPoint, int closingPoint, int caretLocation) - { - public ParsedDocument Document { get; } = document; - - public int OpeningPoint { get; } = openingPoint; - - public int ClosingPoint { get; } = closingPoint; - - public int CaretLocation { get; } = caretLocation; - - public bool HasCompletionForOpeningBrace(char openingBrace) - => ClosingPoint >= 1 && Document.Text[OpeningPoint] == openingBrace; - - public SyntaxToken GetOpeningToken() - => Document.Root.FindToken(OpeningPoint, findInsideTrivia: true); - } + public ParsedDocument Document { get; } = document; + + public int OpeningPoint { get; } = openingPoint; + + public int ClosingPoint { get; } = closingPoint; + + public int CaretLocation { get; } = caretLocation; + + public bool HasCompletionForOpeningBrace(char openingBrace) + => ClosingPoint >= 1 && Document.Text[OpeningPoint] == openingBrace; + + public SyntaxToken GetOpeningToken() + => Document.Root.FindToken(OpeningPoint, findInsideTrivia: true); } diff --git a/src/Features/Core/Portable/BraceMatching/AbstractBraceMatcher.cs b/src/Features/Core/Portable/BraceMatching/AbstractBraceMatcher.cs index 792f1dda3ccf4..ac1537bc8417f 100644 --- a/src/Features/Core/Portable/BraceMatching/AbstractBraceMatcher.cs +++ b/src/Features/Core/Portable/BraceMatching/AbstractBraceMatcher.cs @@ -9,89 +9,88 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +internal abstract class AbstractBraceMatcher : IBraceMatcher { - internal abstract class AbstractBraceMatcher : IBraceMatcher - { - private readonly BraceCharacterAndKind _openBrace; - private readonly BraceCharacterAndKind _closeBrace; + private readonly BraceCharacterAndKind _openBrace; + private readonly BraceCharacterAndKind _closeBrace; - protected AbstractBraceMatcher( - BraceCharacterAndKind openBrace, - BraceCharacterAndKind closeBrace) - { - _openBrace = openBrace; - _closeBrace = closeBrace; - } + protected AbstractBraceMatcher( + BraceCharacterAndKind openBrace, + BraceCharacterAndKind closeBrace) + { + _openBrace = openBrace; + _closeBrace = closeBrace; + } - private bool TryFindMatchingToken(SyntaxToken token, out SyntaxToken match) - { - var parent = token.Parent; + private bool TryFindMatchingToken(SyntaxToken token, out SyntaxToken match) + { + var parent = token.Parent; - var braceTokens = (from child in parent.ChildNodesAndTokens() - where child.IsToken - let tok = child.AsToken() - where tok.RawKind == _openBrace.Kind || tok.RawKind == _closeBrace.Kind - where tok.Span.Length > 0 - select tok).ToList(); + var braceTokens = (from child in parent.ChildNodesAndTokens() + where child.IsToken + let tok = child.AsToken() + where tok.RawKind == _openBrace.Kind || tok.RawKind == _closeBrace.Kind + where tok.Span.Length > 0 + select tok).ToList(); - if (braceTokens.Count == 2 && - braceTokens[0].RawKind == _openBrace.Kind && - braceTokens[1].RawKind == _closeBrace.Kind) + if (braceTokens.Count == 2 && + braceTokens[0].RawKind == _openBrace.Kind && + braceTokens[1].RawKind == _closeBrace.Kind) + { + if (braceTokens[0] == token) { - if (braceTokens[0] == token) - { - match = braceTokens[1]; - return true; - } - else if (braceTokens[1] == token) - { - match = braceTokens[0]; - return true; - } + match = braceTokens[1]; + return true; + } + else if (braceTokens[1] == token) + { + match = braceTokens[0]; + return true; } - - match = default; - return false; } - public async Task FindBracesAsync( - Document document, - int position, - BraceMatchingOptions options, - CancellationToken cancellationToken) - { - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(position); + match = default; + return false; + } - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (position < text.Length && this.IsBrace(text[position])) + public async Task FindBracesAsync( + Document document, + int position, + BraceMatchingOptions options, + CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(position); + + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (position < text.Length && this.IsBrace(text[position])) + { + if (token.RawKind == _openBrace.Kind && AllowedForToken(token)) { - if (token.RawKind == _openBrace.Kind && AllowedForToken(token)) + var leftToken = token; + if (TryFindMatchingToken(leftToken, out var rightToken)) { - var leftToken = token; - if (TryFindMatchingToken(leftToken, out var rightToken)) - { - return new BraceMatchingResult(leftToken.Span, rightToken.Span); - } + return new BraceMatchingResult(leftToken.Span, rightToken.Span); } - else if (token.RawKind == _closeBrace.Kind && AllowedForToken(token)) + } + else if (token.RawKind == _closeBrace.Kind && AllowedForToken(token)) + { + var rightToken = token; + if (TryFindMatchingToken(rightToken, out var leftToken)) { - var rightToken = token; - if (TryFindMatchingToken(rightToken, out var leftToken)) - { - return new BraceMatchingResult(leftToken.Span, rightToken.Span); - } + return new BraceMatchingResult(leftToken.Span, rightToken.Span); } } - - return null; } - protected virtual bool AllowedForToken(SyntaxToken token) - => true; - - private bool IsBrace(char c) - => _openBrace.Character == c || _closeBrace.Character == c; + return null; } + + protected virtual bool AllowedForToken(SyntaxToken token) + => true; + + private bool IsBrace(char c) + => _openBrace.Character == c || _closeBrace.Character == c; } diff --git a/src/Features/Core/Portable/BraceMatching/AbstractDirectiveTriviaBraceMatcher.cs b/src/Features/Core/Portable/BraceMatching/AbstractDirectiveTriviaBraceMatcher.cs index f9780c6c31f8a..0abda852f63ea 100644 --- a/src/Features/Core/Portable/BraceMatching/AbstractDirectiveTriviaBraceMatcher.cs +++ b/src/Features/Core/Portable/BraceMatching/AbstractDirectiveTriviaBraceMatcher.cs @@ -8,63 +8,62 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +internal abstract class AbstractDirectiveTriviaBraceMatcher : IBraceMatcher + where TDirectiveTriviaSyntax : SyntaxNode + where TIfDirectiveTriviaSyntax : TDirectiveTriviaSyntax + where TElseIfDirectiveTriviaSyntax : TDirectiveTriviaSyntax + where TElseDirectiveTriviaSyntax : TDirectiveTriviaSyntax + where TEndIfDirectiveTriviaSyntax : TDirectiveTriviaSyntax + where TRegionDirectiveTriviaSyntax : TDirectiveTriviaSyntax + where TEndRegionDirectiveTriviaSyntax : TDirectiveTriviaSyntax { - internal abstract class AbstractDirectiveTriviaBraceMatcher : IBraceMatcher - where TDirectiveTriviaSyntax : SyntaxNode - where TIfDirectiveTriviaSyntax : TDirectiveTriviaSyntax - where TElseIfDirectiveTriviaSyntax : TDirectiveTriviaSyntax - where TElseDirectiveTriviaSyntax : TDirectiveTriviaSyntax - where TEndIfDirectiveTriviaSyntax : TDirectiveTriviaSyntax - where TRegionDirectiveTriviaSyntax : TDirectiveTriviaSyntax - where TEndRegionDirectiveTriviaSyntax : TDirectiveTriviaSyntax + protected abstract ImmutableArray GetMatchingConditionalDirectives(TDirectiveTriviaSyntax directive, CancellationToken cancellationToken); + protected abstract TDirectiveTriviaSyntax? GetMatchingDirective(TDirectiveTriviaSyntax directive, CancellationToken cancellationToken); + internal abstract TextSpan GetSpanForTagging(TDirectiveTriviaSyntax directive); + + public async Task FindBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken) { - protected abstract ImmutableArray GetMatchingConditionalDirectives(TDirectiveTriviaSyntax directive, CancellationToken cancellationToken); - protected abstract TDirectiveTriviaSyntax? GetMatchingDirective(TDirectiveTriviaSyntax directive, CancellationToken cancellationToken); - internal abstract TextSpan GetSpanForTagging(TDirectiveTriviaSyntax directive); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(position, findInsideTrivia: true); - public async Task FindBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken) + if (token.Parent is not TDirectiveTriviaSyntax directive) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(position, findInsideTrivia: true); - - if (token.Parent is not TDirectiveTriviaSyntax directive) - { - return null; - } - - TDirectiveTriviaSyntax? matchingDirective = null; - if (IsConditionalDirective(directive)) - { - // #if/#elif/#else/#endif directive cases. - var matchingDirectives = GetMatchingConditionalDirectives(directive, cancellationToken); - if (matchingDirectives.Length > 0) - matchingDirective = matchingDirectives[(matchingDirectives.IndexOf(directive) + 1) % matchingDirectives.Length]; - } - else - { - // #region/#endregion or other directive cases. - matchingDirective = GetMatchingDirective(directive, cancellationToken); - } + return null; + } - if (matchingDirective == null) - { - // one line directives, that do not have a matching begin/end directive pair. - return null; - } + TDirectiveTriviaSyntax? matchingDirective = null; + if (IsConditionalDirective(directive)) + { + // #if/#elif/#else/#endif directive cases. + var matchingDirectives = GetMatchingConditionalDirectives(directive, cancellationToken); + if (matchingDirectives.Length > 0) + matchingDirective = matchingDirectives[(matchingDirectives.IndexOf(directive) + 1) % matchingDirectives.Length]; + } + else + { + // #region/#endregion or other directive cases. + matchingDirective = GetMatchingDirective(directive, cancellationToken); + } - return new BraceMatchingResult( - LeftSpan: GetSpanForTagging(directive), - RightSpan: GetSpanForTagging(matchingDirective)); + if (matchingDirective == null) + { + // one line directives, that do not have a matching begin/end directive pair. + return null; } - private static bool IsConditionalDirective(TDirectiveTriviaSyntax directive) - => directive is TIfDirectiveTriviaSyntax or - TElseIfDirectiveTriviaSyntax or - TElseDirectiveTriviaSyntax or - TEndIfDirectiveTriviaSyntax; + return new BraceMatchingResult( + LeftSpan: GetSpanForTagging(directive), + RightSpan: GetSpanForTagging(matchingDirective)); } + + private static bool IsConditionalDirective(TDirectiveTriviaSyntax directive) + => directive is TIfDirectiveTriviaSyntax or + TElseIfDirectiveTriviaSyntax or + TElseDirectiveTriviaSyntax or + TEndIfDirectiveTriviaSyntax; } diff --git a/src/Features/Core/Portable/BraceMatching/AbstractEmbeddedLanguageBraceMatcher.cs b/src/Features/Core/Portable/BraceMatching/AbstractEmbeddedLanguageBraceMatcher.cs index 92f847a5d79c6..afffb4e6d87ce 100644 --- a/src/Features/Core/Portable/BraceMatching/AbstractEmbeddedLanguageBraceMatcher.cs +++ b/src/Features/Core/Portable/BraceMatching/AbstractEmbeddedLanguageBraceMatcher.cs @@ -10,45 +10,44 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +/// +/// Brace matcher that analyzes string literals (for C#/VB) and then dispatches out to embedded brace matchers for +/// particular embedded languages (like JSON/Regex). +/// +internal abstract class AbstractEmbeddedLanguageBraceMatcher : + AbstractEmbeddedLanguageFeatureService, IBraceMatcher { - /// - /// Brace matcher that analyzes string literals (for C#/VB) and then dispatches out to embedded brace matchers for - /// particular embedded languages (like JSON/Regex). - /// - internal abstract class AbstractEmbeddedLanguageBraceMatcher : - AbstractEmbeddedLanguageFeatureService, IBraceMatcher + protected AbstractEmbeddedLanguageBraceMatcher( + string languageName, + EmbeddedLanguageInfo info, + ISyntaxKinds syntaxKinds, + IEnumerable> allServices) + : base(languageName, info, syntaxKinds, allServices) { - protected AbstractEmbeddedLanguageBraceMatcher( - string languageName, - EmbeddedLanguageInfo info, - ISyntaxKinds syntaxKinds, - IEnumerable> allServices) - : base(languageName, info, syntaxKinds, allServices) - { - } - - public async Task FindBracesAsync( - Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(position); + } - if (!this.SyntaxTokenKinds.Contains(token.RawKind)) - return null; + public async Task FindBracesAsync( + Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(position); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (!this.SyntaxTokenKinds.Contains(token.RawKind)) + return null; - var braceMatchers = GetServices(semanticModel, token, cancellationToken); - foreach (var braceMatcher in braceMatchers) - { - // If this service added values then need to check the other ones. - var result = braceMatcher.Value.FindBraces(document.Project, semanticModel, token, position, options, cancellationToken); - if (result.HasValue) - return result; - } + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - return null; + var braceMatchers = GetServices(semanticModel, token, cancellationToken); + foreach (var braceMatcher in braceMatchers) + { + // If this service added values then need to check the other ones. + var result = braceMatcher.Value.FindBraces(document.Project, semanticModel, token, position, options, cancellationToken); + if (result.HasValue) + return result; } + + return null; } } diff --git a/src/Features/Core/Portable/BraceMatching/BraceCharacterAndKind.cs b/src/Features/Core/Portable/BraceMatching/BraceCharacterAndKind.cs index 288c8fcd53260..4de3252c593a3 100644 --- a/src/Features/Core/Portable/BraceMatching/BraceCharacterAndKind.cs +++ b/src/Features/Core/Portable/BraceMatching/BraceCharacterAndKind.cs @@ -4,11 +4,10 @@ using Microsoft; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +internal readonly struct BraceCharacterAndKind(char character, int kind) { - internal readonly struct BraceCharacterAndKind(char character, int kind) - { - public char Character { get; } = character; - public int Kind { get; } = kind; - } + public char Character { get; } = character; + public int Kind { get; } = kind; } diff --git a/src/Features/Core/Portable/BraceMatching/BraceMatchingOptions.cs b/src/Features/Core/Portable/BraceMatching/BraceMatchingOptions.cs index 5675782a8af41..ed0a0d717cc24 100644 --- a/src/Features/Core/Portable/BraceMatching/BraceMatchingOptions.cs +++ b/src/Features/Core/Portable/BraceMatching/BraceMatchingOptions.cs @@ -5,17 +5,16 @@ using System.Runtime.Serialization; using Microsoft.CodeAnalysis.DocumentHighlighting; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +[DataContract] +internal readonly record struct BraceMatchingOptions( + [property: DataMember(Order = 0)] HighlightingOptions HighlightingOptions) { - [DataContract] - internal readonly record struct BraceMatchingOptions( - [property: DataMember(Order = 0)] HighlightingOptions HighlightingOptions) + public BraceMatchingOptions() + : this(HighlightingOptions.Default) { - public BraceMatchingOptions() - : this(HighlightingOptions.Default) - { - } - - public static readonly BraceMatchingOptions Default = new(); } + + public static readonly BraceMatchingOptions Default = new(); } diff --git a/src/Features/Core/Portable/BraceMatching/BraceMatchingService.cs b/src/Features/Core/Portable/BraceMatching/BraceMatchingService.cs index 773cb1ac3720c..89f7cdcc321ee 100644 --- a/src/Features/Core/Portable/BraceMatching/BraceMatchingService.cs +++ b/src/Features/Core/Portable/BraceMatching/BraceMatchingService.cs @@ -13,36 +13,35 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +[Export(typeof(IBraceMatchingService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class BraceMatchingService( + [ImportMany] IEnumerable> braceMatchers) : IBraceMatchingService { - [Export(typeof(IBraceMatchingService)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal class BraceMatchingService( - [ImportMany] IEnumerable> braceMatchers) : IBraceMatchingService - { - private readonly ImmutableArray> _braceMatchers = braceMatchers.ToImmutableArray(); + private readonly ImmutableArray> _braceMatchers = braceMatchers.ToImmutableArray(); - public async Task GetMatchingBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken) + public async Task GetMatchingBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (position < 0 || position > text.Length) { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (position < 0 || position > text.Length) - { - throw new ArgumentException(nameof(position)); - } + throw new ArgumentException(nameof(position)); + } - var matchers = _braceMatchers.Where(b => b.Metadata.Language == document.Project.Language); - foreach (var matcher in matchers) + var matchers = _braceMatchers.Where(b => b.Metadata.Language == document.Project.Language); + foreach (var matcher in matchers) + { + cancellationToken.ThrowIfCancellationRequested(); + var braces = await matcher.Value.FindBracesAsync(document, position, options, cancellationToken).ConfigureAwait(false); + if (braces.HasValue) { - cancellationToken.ThrowIfCancellationRequested(); - var braces = await matcher.Value.FindBracesAsync(document, position, options, cancellationToken).ConfigureAwait(false); - if (braces.HasValue) - { - return braces; - } + return braces; } - - return null; } + + return null; } } diff --git a/src/Features/Core/Portable/BraceMatching/ExportBraceMatcherAttribute.cs b/src/Features/Core/Portable/BraceMatching/ExportBraceMatcherAttribute.cs index d00702f53cebf..a69f2265ff65c 100644 --- a/src/Features/Core/Portable/BraceMatching/ExportBraceMatcherAttribute.cs +++ b/src/Features/Core/Portable/BraceMatching/ExportBraceMatcherAttribute.cs @@ -7,12 +7,11 @@ using System; using System.Composition; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +[MetadataAttribute] +[AttributeUsage(AttributeTargets.Class)] +internal sealed class ExportBraceMatcherAttribute(string language) : ExportAttribute(typeof(IBraceMatcher)) { - [MetadataAttribute] - [AttributeUsage(AttributeTargets.Class)] - internal sealed class ExportBraceMatcherAttribute(string language) : ExportAttribute(typeof(IBraceMatcher)) - { - public string Language { get; } = language ?? throw new ArgumentNullException(nameof(language)); - } + public string Language { get; } = language ?? throw new ArgumentNullException(nameof(language)); } diff --git a/src/Features/Core/Portable/BraceMatching/ExportEmbeddedLanguageBraceMatcherAttribute.cs b/src/Features/Core/Portable/BraceMatching/ExportEmbeddedLanguageBraceMatcherAttribute.cs index befceb5cd9e09..1a9a1fa50a4e3 100644 --- a/src/Features/Core/Portable/BraceMatching/ExportEmbeddedLanguageBraceMatcherAttribute.cs +++ b/src/Features/Core/Portable/BraceMatching/ExportEmbeddedLanguageBraceMatcherAttribute.cs @@ -4,18 +4,17 @@ using Microsoft.CodeAnalysis.EmbeddedLanguages; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +/// +/// Use this attribute to export a . +/// +internal class ExportEmbeddedLanguageBraceMatcherAttribute( + string name, string[] languages, bool supportsUnannotatedAPIs, params string[] identifiers) : ExportEmbeddedLanguageFeatureServiceAttribute(typeof(IEmbeddedLanguageBraceMatcher), name, languages, supportsUnannotatedAPIs, identifiers) { - /// - /// Use this attribute to export a . - /// - internal class ExportEmbeddedLanguageBraceMatcherAttribute( - string name, string[] languages, bool supportsUnannotatedAPIs, params string[] identifiers) : ExportEmbeddedLanguageFeatureServiceAttribute(typeof(IEmbeddedLanguageBraceMatcher), name, languages, supportsUnannotatedAPIs, identifiers) + public ExportEmbeddedLanguageBraceMatcherAttribute( + string name, string[] languages, params string[] identifiers) + : this(name, languages, supportsUnannotatedAPIs: false, identifiers) { - public ExportEmbeddedLanguageBraceMatcherAttribute( - string name, string[] languages, params string[] identifiers) - : this(name, languages, supportsUnannotatedAPIs: false, identifiers) - { - } } } diff --git a/src/Features/Core/Portable/BraceMatching/IBraceMatcher.cs b/src/Features/Core/Portable/BraceMatching/IBraceMatcher.cs index 12342548bb04a..2bdb16a9cae2c 100644 --- a/src/Features/Core/Portable/BraceMatching/IBraceMatcher.cs +++ b/src/Features/Core/Portable/BraceMatching/IBraceMatcher.cs @@ -5,27 +5,26 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +internal interface IBraceMatcher { - internal interface IBraceMatcher - { - /// - /// Given a and a within that document, gets the if the position is at the start or end character of a matching pair of braces. - /// Importantly, the is the position of the actual character to examine. For - /// example, given: Goo()$$[1, 2, 3] (where $$ is the position), this would only be considering - /// the [ brace, not the ) brace that precedes it. Similarly, for Goo()[1, 2, 3$$] this - /// would be considering the ] brace. If Goo()[1, 2, 3]$$ were passed, no braces should be - /// reported, despite the position being at the end of a brace. - /// - /// It is the job of the calling feature ("Brace Matching") to actually make multiple calls into these matchers - /// to then determine what to do. For example with Goo(true)$$[1, 2, 3] (where $$ is now the caret - /// position of the user), the feature will make two calls in, one for Goo(true$$)[1, 2, 3] and one for - /// Goo(true)$$[1, 2, 3]. This will allow it to see that the caret is between two complete brace pairs, - /// and it can highlight both. The does not have to consider this, or try to pick - /// which set of braces to return. - /// - /// - Task FindBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken); - } + /// + /// Given a and a within that document, gets the if the position is at the start or end character of a matching pair of braces. + /// Importantly, the is the position of the actual character to examine. For + /// example, given: Goo()$$[1, 2, 3] (where $$ is the position), this would only be considering + /// the [ brace, not the ) brace that precedes it. Similarly, for Goo()[1, 2, 3$$] this + /// would be considering the ] brace. If Goo()[1, 2, 3]$$ were passed, no braces should be + /// reported, despite the position being at the end of a brace. + /// + /// It is the job of the calling feature ("Brace Matching") to actually make multiple calls into these matchers + /// to then determine what to do. For example with Goo(true)$$[1, 2, 3] (where $$ is now the caret + /// position of the user), the feature will make two calls in, one for Goo(true$$)[1, 2, 3] and one for + /// Goo(true)$$[1, 2, 3]. This will allow it to see that the caret is between two complete brace pairs, + /// and it can highlight both. The does not have to consider this, or try to pick + /// which set of braces to return. + /// + /// + Task FindBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/BraceMatching/IBraceMatchingService.cs b/src/Features/Core/Portable/BraceMatching/IBraceMatchingService.cs index 7008e97f1eade..57f60595d504a 100644 --- a/src/Features/Core/Portable/BraceMatching/IBraceMatchingService.cs +++ b/src/Features/Core/Portable/BraceMatching/IBraceMatchingService.cs @@ -7,15 +7,14 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.BraceMatching -{ - internal interface IBraceMatchingService - { - Task GetMatchingBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken); - } +namespace Microsoft.CodeAnalysis.BraceMatching; - [DataContract] - internal readonly record struct BraceMatchingResult( - [property: DataMember(Order = 0)] TextSpan LeftSpan, - [property: DataMember(Order = 1)] TextSpan RightSpan); +internal interface IBraceMatchingService +{ + Task GetMatchingBracesAsync(Document document, int position, BraceMatchingOptions options, CancellationToken cancellationToken); } + +[DataContract] +internal readonly record struct BraceMatchingResult( + [property: DataMember(Order = 0)] TextSpan LeftSpan, + [property: DataMember(Order = 1)] TextSpan RightSpan); diff --git a/src/Features/Core/Portable/BraceMatching/IBraceMatchingServiceExtensions.cs b/src/Features/Core/Portable/BraceMatching/IBraceMatchingServiceExtensions.cs index 12e991a789003..633e71d7bf657 100644 --- a/src/Features/Core/Portable/BraceMatching/IBraceMatchingServiceExtensions.cs +++ b/src/Features/Core/Portable/BraceMatching/IBraceMatchingServiceExtensions.cs @@ -6,55 +6,54 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +internal static class IBraceMatchingServiceExtensions { - internal static class IBraceMatchingServiceExtensions + public static async Task FindMatchingSpanAsync( + this IBraceMatchingService service, + Document document, + int position, + BraceMatchingOptions options, + CancellationToken cancellationToken) { - public static async Task FindMatchingSpanAsync( - this IBraceMatchingService service, - Document document, - int position, - BraceMatchingOptions options, - CancellationToken cancellationToken) - { - // These are the matching spans when checking the token to the right of the position. - var braces1 = await service.GetMatchingBracesAsync(document, position, options, cancellationToken).ConfigureAwait(false); + // These are the matching spans when checking the token to the right of the position. + var braces1 = await service.GetMatchingBracesAsync(document, position, options, cancellationToken).ConfigureAwait(false); - // These are the matching spans when checking the token to the left of the position. - BraceMatchingResult? braces2 = null; + // These are the matching spans when checking the token to the left of the position. + BraceMatchingResult? braces2 = null; - // Ensure caret is valid at left of position. - if (position > 0) - { - braces2 = await service.GetMatchingBracesAsync(document, position - 1, options, cancellationToken).ConfigureAwait(false); - } - - // Favor matches where the position is on the outside boundary of the braces. i.e. if we - // have: {^()} - // - // then this would return the () not the {} - if (braces1.HasValue && position >= braces1.Value.LeftSpan.Start && position < braces1.Value.LeftSpan.End) - { - // ^{ } -- return right span - return braces1.Value.RightSpan; - } - else if (braces2.HasValue && position > braces2.Value.RightSpan.Start && position <= braces2.Value.RightSpan.End) - { - // { }^ -- return left span - return braces2.Value.LeftSpan; - } - else if (braces2.HasValue && position > braces2.Value.LeftSpan.Start && position <= braces2.Value.LeftSpan.End) - { - // {^ } -- return right span - return braces2.Value.RightSpan; - } - else if (braces1.HasValue && position >= braces1.Value.RightSpan.Start && position < braces1.Value.RightSpan.End) - { - // { ^} - return left span - return braces1.Value.LeftSpan; - } + // Ensure caret is valid at left of position. + if (position > 0) + { + braces2 = await service.GetMatchingBracesAsync(document, position - 1, options, cancellationToken).ConfigureAwait(false); + } - return null; + // Favor matches where the position is on the outside boundary of the braces. i.e. if we + // have: {^()} + // + // then this would return the () not the {} + if (braces1.HasValue && position >= braces1.Value.LeftSpan.Start && position < braces1.Value.LeftSpan.End) + { + // ^{ } -- return right span + return braces1.Value.RightSpan; } + else if (braces2.HasValue && position > braces2.Value.RightSpan.Start && position <= braces2.Value.RightSpan.End) + { + // { }^ -- return left span + return braces2.Value.LeftSpan; + } + else if (braces2.HasValue && position > braces2.Value.LeftSpan.Start && position <= braces2.Value.LeftSpan.End) + { + // {^ } -- return right span + return braces2.Value.RightSpan; + } + else if (braces1.HasValue && position >= braces1.Value.RightSpan.Start && position < braces1.Value.RightSpan.End) + { + // { ^} - return left span + return braces1.Value.LeftSpan; + } + + return null; } } diff --git a/src/Features/Core/Portable/BraceMatching/IEmbeddedLanguageBraceMatcher.cs b/src/Features/Core/Portable/BraceMatching/IEmbeddedLanguageBraceMatcher.cs index af52cbaa0c080..b51fdbbc093db 100644 --- a/src/Features/Core/Portable/BraceMatching/IEmbeddedLanguageBraceMatcher.cs +++ b/src/Features/Core/Portable/BraceMatching/IEmbeddedLanguageBraceMatcher.cs @@ -5,16 +5,15 @@ using System.Threading; using Microsoft.CodeAnalysis.EmbeddedLanguages; -namespace Microsoft.CodeAnalysis.BraceMatching +namespace Microsoft.CodeAnalysis.BraceMatching; + +internal interface IEmbeddedLanguageBraceMatcher : IEmbeddedLanguageFeatureService { - internal interface IEmbeddedLanguageBraceMatcher : IEmbeddedLanguageFeatureService - { - BraceMatchingResult? FindBraces( - Project project, - SemanticModel semanticModel, - SyntaxToken token, - int position, - BraceMatchingOptions options, - CancellationToken cancellationToken); - } + BraceMatchingResult? FindBraces( + Project project, + SemanticModel semanticModel, + SyntaxToken token, + int position, + BraceMatchingOptions options, + CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/BracePairs/IBracePairsService.cs b/src/Features/Core/Portable/BracePairs/IBracePairsService.cs index 62098dca4182c..3fcf8b813061d 100644 --- a/src/Features/Core/Portable/BracePairs/IBracePairsService.cs +++ b/src/Features/Core/Portable/BracePairs/IBracePairsService.cs @@ -10,73 +10,72 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.BracePairs +namespace Microsoft.CodeAnalysis.BracePairs; + +internal readonly record struct BracePairData( + TextSpan Start, + TextSpan End); + +internal interface IBracePairsService : ILanguageService { - internal readonly record struct BracePairData( - TextSpan Start, - TextSpan End); + Task AddBracePairsAsync(Document document, TextSpan textSpan, ArrayBuilder bracePairs, CancellationToken cancellationToken); +} - internal interface IBracePairsService : ILanguageService - { - Task AddBracePairsAsync(Document document, TextSpan textSpan, ArrayBuilder bracePairs, CancellationToken cancellationToken); - } +internal abstract class AbstractBracePairsService : IBracePairsService +{ + private readonly Dictionary _bracePairKinds = []; - internal abstract class AbstractBracePairsService : IBracePairsService + protected AbstractBracePairsService( + ISyntaxKinds syntaxKinds) { - private readonly Dictionary _bracePairKinds = []; - - protected AbstractBracePairsService( - ISyntaxKinds syntaxKinds) - { - Add(syntaxKinds.OpenBraceToken, syntaxKinds.CloseBraceToken); - Add(syntaxKinds.OpenBracketToken, syntaxKinds.CloseBracketToken); - Add(syntaxKinds.OpenParenToken, syntaxKinds.CloseParenToken); - Add(syntaxKinds.LessThanToken, syntaxKinds.GreaterThanToken); + Add(syntaxKinds.OpenBraceToken, syntaxKinds.CloseBraceToken); + Add(syntaxKinds.OpenBracketToken, syntaxKinds.CloseBracketToken); + Add(syntaxKinds.OpenParenToken, syntaxKinds.CloseParenToken); + Add(syntaxKinds.LessThanToken, syntaxKinds.GreaterThanToken); - return; + return; - void Add(int? open, int? close) - { - if (open != null && close != null) - _bracePairKinds[open.Value] = close.Value; - } + void Add(int? open, int? close) + { + if (open != null && close != null) + _bracePairKinds[open.Value] = close.Value; } + } - public async Task AddBracePairsAsync( - Document document, TextSpan span, ArrayBuilder bracePairs, CancellationToken cancellationToken) - { - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var stack); + public async Task AddBracePairsAsync( + Document document, TextSpan span, ArrayBuilder bracePairs, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + using var _ = ArrayBuilder.GetInstance(out var stack); - stack.Add(root); + stack.Add(root); - while (stack.Count > 0) + while (stack.Count > 0) + { + var current = stack.Pop(); + if (current.IsNode) { - var current = stack.Pop(); - if (current.IsNode) - { - // Ignore nodes that have no intersection at all with the span we're being asked with. Note: if - // there is any node intersection, then we want to process it. We specifically don't check token - // intersection as the start token might not be in the span we're asked about, but the close token - // may be. - if (!current.Span.IntersectsWith(span)) - continue; + // Ignore nodes that have no intersection at all with the span we're being asked with. Note: if + // there is any node intersection, then we want to process it. We specifically don't check token + // intersection as the start token might not be in the span we're asked about, but the close token + // may be. + if (!current.Span.IntersectsWith(span)) + continue; - foreach (var child in current.ChildNodesAndTokens().Reverse()) - stack.Push(child); - } - else if (current.IsToken) + foreach (var child in current.ChildNodesAndTokens().Reverse()) + stack.Push(child); + } + else if (current.IsToken) + { + if (_bracePairKinds.TryGetValue(current.AsToken().RawKind, out var closeKind)) { - if (_bracePairKinds.TryGetValue(current.AsToken().RawKind, out var closeKind)) + // hit an open token. Try to find the corresponding close token in the parent. + if (current.Parent != null) { - // hit an open token. Try to find the corresponding close token in the parent. - if (current.Parent != null) + foreach (var sibling in current.Parent.ChildNodesAndTokens()) { - foreach (var sibling in current.Parent.ChildNodesAndTokens()) - { - if (sibling.IsToken && sibling.RawKind == closeKind) - bracePairs.Add(new BracePairData(current.Span, sibling.Span)); - } + if (sibling.IsToken && sibling.RawKind == closeKind) + bracePairs.Add(new BracePairData(current.Span, sibling.Span)); } } } diff --git a/src/Features/Core/Portable/ChangeSignature/AbstractChangeSignatureCodeRefactoringProvider.cs b/src/Features/Core/Portable/ChangeSignature/AbstractChangeSignatureCodeRefactoringProvider.cs index 18e50f30ef69e..8d20a15b7959a 100644 --- a/src/Features/Core/Portable/ChangeSignature/AbstractChangeSignatureCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ChangeSignature/AbstractChangeSignatureCodeRefactoringProvider.cs @@ -11,27 +11,26 @@ using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, + Name = PredefinedCodeRefactoringProviderNames.ChangeSignature), Shared] +internal class ChangeSignatureCodeRefactoringProvider : CodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, - Name = PredefinedCodeRefactoringProviderNames.ChangeSignature), Shared] - internal class ChangeSignatureCodeRefactoringProvider : CodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public ChangeSignatureCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public ChangeSignatureCodeRefactoringProvider() - { - } + } - public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, span, cancellationToken) = context; + if (span.IsEmpty) { - var (document, span, cancellationToken) = context; - if (span.IsEmpty) - { - var service = document.GetLanguageService(); - var actions = await service.GetChangeSignatureCodeActionAsync(document, span, context.Options, cancellationToken).ConfigureAwait(false); - context.RegisterRefactorings(actions); - } + var service = document.GetLanguageService(); + var actions = await service.GetChangeSignatureCodeActionAsync(document, span, context.Options, cancellationToken).ConfigureAwait(false); + context.RegisterRefactorings(actions); } } } diff --git a/src/Features/Core/Portable/ChangeSignature/AbstractChangeSignatureService.cs b/src/Features/Core/Portable/ChangeSignature/AbstractChangeSignatureService.cs index 003879f635a0a..87850319e07e0 100644 --- a/src/Features/Core/Portable/ChangeSignature/AbstractChangeSignatureService.cs +++ b/src/Features/Core/Portable/ChangeSignature/AbstractChangeSignatureService.cs @@ -28,1051 +28,1050 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ChangeSignature -{ - internal abstract class AbstractChangeSignatureService : ILanguageService - { - protected SyntaxAnnotation changeSignatureFormattingAnnotation = new("ChangeSignatureFormatting"); +namespace Microsoft.CodeAnalysis.ChangeSignature; - /// - /// Determines the symbol on which we are invoking ReorderParameters - /// - public abstract Task<(ISymbol? symbol, int selectedIndex)> GetInvocationSymbolAsync(Document document, int position, bool restrictToDeclarations, CancellationToken cancellationToken); +internal abstract class AbstractChangeSignatureService : ILanguageService +{ + protected SyntaxAnnotation changeSignatureFormattingAnnotation = new("ChangeSignatureFormatting"); - /// - /// Given a SyntaxNode for which we want to reorder parameters/arguments, find the - /// SyntaxNode of a kind where we know how to reorder parameters/arguments. - /// - public abstract SyntaxNode? FindNodeToUpdate(Document document, SyntaxNode node); + /// + /// Determines the symbol on which we are invoking ReorderParameters + /// + public abstract Task<(ISymbol? symbol, int selectedIndex)> GetInvocationSymbolAsync(Document document, int position, bool restrictToDeclarations, CancellationToken cancellationToken); - public abstract Task> DetermineCascadedSymbolsFromDelegateInvokeAsync( - IMethodSymbol symbol, Document document, CancellationToken cancellationToken); + /// + /// Given a SyntaxNode for which we want to reorder parameters/arguments, find the + /// SyntaxNode of a kind where we know how to reorder parameters/arguments. + /// + public abstract SyntaxNode? FindNodeToUpdate(Document document, SyntaxNode node); - public abstract Task ChangeSignatureAsync( - Document document, - ISymbol declarationSymbol, - SyntaxNode potentiallyUpdatedNode, - SyntaxNode originalNode, - SignatureChange signaturePermutation, - LineFormattingOptionsProvider fallbackOptions, - CancellationToken cancellationToken); + public abstract Task> DetermineCascadedSymbolsFromDelegateInvokeAsync( + IMethodSymbol symbol, Document document, CancellationToken cancellationToken); - protected abstract IEnumerable GetFormattingRules(Document document); + public abstract Task ChangeSignatureAsync( + Document document, + ISymbol declarationSymbol, + SyntaxNode potentiallyUpdatedNode, + SyntaxNode originalNode, + SignatureChange signaturePermutation, + LineFormattingOptionsProvider fallbackOptions, + CancellationToken cancellationToken); - protected abstract T TransferLeadingWhitespaceTrivia(T newArgument, SyntaxNode oldArgument) where T : SyntaxNode; + protected abstract IEnumerable GetFormattingRules(Document document); - protected abstract SyntaxToken CommaTokenWithElasticSpace(); + protected abstract T TransferLeadingWhitespaceTrivia(T newArgument, SyntaxNode oldArgument) where T : SyntaxNode; - /// - /// For some Foo(int x, params int[] p), this helps convert the "1, 2, 3" in Foo(0, 1, 2, 3) - /// to "new int[] { 1, 2, 3 }" in Foo(0, new int[] { 1, 2, 3 }); - /// - protected abstract TArgumentSyntax CreateExplicitParamsArrayFromIndividualArguments(SeparatedSyntaxList newArguments, int startingIndex, IParameterSymbol parameterSymbol) - where TArgumentSyntax : SyntaxNode; + protected abstract SyntaxToken CommaTokenWithElasticSpace(); - protected abstract TArgumentSyntax AddNameToArgument(TArgumentSyntax argument, string name) - where TArgumentSyntax : SyntaxNode; + /// + /// For some Foo(int x, params int[] p), this helps convert the "1, 2, 3" in Foo(0, 1, 2, 3) + /// to "new int[] { 1, 2, 3 }" in Foo(0, new int[] { 1, 2, 3 }); + /// + protected abstract TArgumentSyntax CreateExplicitParamsArrayFromIndividualArguments(SeparatedSyntaxList newArguments, int startingIndex, IParameterSymbol parameterSymbol) + where TArgumentSyntax : SyntaxNode; - /// - /// Only some languages support: - /// - Optional parameters and params arrays simultaneously in declarations - /// - Passing the params array as a named argument - /// - protected abstract bool SupportsOptionalAndParamsArrayParametersSimultaneously(); + protected abstract TArgumentSyntax AddNameToArgument(TArgumentSyntax argument, string name) + where TArgumentSyntax : SyntaxNode; - protected abstract bool TryGetRecordPrimaryConstructor(INamedTypeSymbol typeSymbol, [NotNullWhen(true)] out IMethodSymbol? primaryConstructor); + /// + /// Only some languages support: + /// - Optional parameters and params arrays simultaneously in declarations + /// - Passing the params array as a named argument + /// + protected abstract bool SupportsOptionalAndParamsArrayParametersSimultaneously(); - /// - /// A temporarily hack that should be removed once/if https://github.com/dotnet/roslyn/issues/53092 is fixed. - /// - protected abstract ImmutableArray GetParameters(ISymbol declarationSymbol); + protected abstract bool TryGetRecordPrimaryConstructor(INamedTypeSymbol typeSymbol, [NotNullWhen(true)] out IMethodSymbol? primaryConstructor); - protected abstract SyntaxGenerator Generator { get; } - protected abstract ISyntaxFacts SyntaxFacts { get; } + /// + /// A temporarily hack that should be removed once/if https://github.com/dotnet/roslyn/issues/53092 is fixed. + /// + protected abstract ImmutableArray GetParameters(ISymbol declarationSymbol); - public async Task> GetChangeSignatureCodeActionAsync(Document document, TextSpan span, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var context = await GetChangeSignatureContextAsync(document, span.Start, restrictToDeclarations: true, fallbackOptions, cancellationToken).ConfigureAwait(false); + protected abstract SyntaxGenerator Generator { get; } + protected abstract ISyntaxFacts SyntaxFacts { get; } - return context is ChangeSignatureAnalysisSucceededContext changeSignatureAnalyzedSucceedContext - ? [new ChangeSignatureCodeAction(this, changeSignatureAnalyzedSucceedContext)] - : ImmutableArray.Empty; - } + public async Task> GetChangeSignatureCodeActionAsync(Document document, TextSpan span, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var context = await GetChangeSignatureContextAsync(document, span.Start, restrictToDeclarations: true, fallbackOptions, cancellationToken).ConfigureAwait(false); - internal async Task GetChangeSignatureContextAsync( - Document document, int position, bool restrictToDeclarations, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var (symbol, selectedIndex) = await GetInvocationSymbolAsync( - document, position, restrictToDeclarations, cancellationToken).ConfigureAwait(false); + return context is ChangeSignatureAnalysisSucceededContext changeSignatureAnalyzedSucceedContext + ? [new ChangeSignatureCodeAction(this, changeSignatureAnalyzedSucceedContext)] + : ImmutableArray.Empty; + } - // Cross-language symbols will show as metadata, so map it to source if possible. - symbol = await SymbolFinder.FindSourceDefinitionAsync(symbol, document.Project.Solution, cancellationToken).ConfigureAwait(false) ?? symbol; + internal async Task GetChangeSignatureContextAsync( + Document document, int position, bool restrictToDeclarations, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var (symbol, selectedIndex) = await GetInvocationSymbolAsync( + document, position, restrictToDeclarations, cancellationToken).ConfigureAwait(false); - if (symbol == null) - { - return new CannotChangeSignatureAnalyzedContext(ChangeSignatureFailureKind.IncorrectKind); - } + // Cross-language symbols will show as metadata, so map it to source if possible. + symbol = await SymbolFinder.FindSourceDefinitionAsync(symbol, document.Project.Solution, cancellationToken).ConfigureAwait(false) ?? symbol; - if (symbol is IMethodSymbol method) - { - var containingType = method.ContainingType; + if (symbol == null) + { + return new CannotChangeSignatureAnalyzedContext(ChangeSignatureFailureKind.IncorrectKind); + } - if (method.Name == WellKnownMemberNames.DelegateBeginInvokeName && - containingType != null && - containingType.IsDelegateType() && - containingType.DelegateInvokeMethod != null) - { - symbol = containingType.DelegateInvokeMethod; - } - } + if (symbol is IMethodSymbol method) + { + var containingType = method.ContainingType; - if (symbol is IEventSymbol ev) + if (method.Name == WellKnownMemberNames.DelegateBeginInvokeName && + containingType != null && + containingType.IsDelegateType() && + containingType.DelegateInvokeMethod != null) { - symbol = ev.Type; + symbol = containingType.DelegateInvokeMethod; } + } - if (symbol is INamedTypeSymbol typeSymbol) - { - if (typeSymbol.IsDelegateType() && typeSymbol.DelegateInvokeMethod != null) - { - symbol = typeSymbol.DelegateInvokeMethod; - } - else if (TryGetRecordPrimaryConstructor(typeSymbol, out var primaryConstructor)) - { - symbol = primaryConstructor; - } - } + if (symbol is IEventSymbol ev) + { + symbol = ev.Type; + } - if (!symbol.MatchesKind(SymbolKind.Method, SymbolKind.Property)) + if (symbol is INamedTypeSymbol typeSymbol) + { + if (typeSymbol.IsDelegateType() && typeSymbol.DelegateInvokeMethod != null) { - return new CannotChangeSignatureAnalyzedContext(ChangeSignatureFailureKind.IncorrectKind); + symbol = typeSymbol.DelegateInvokeMethod; } - - if (symbol.Locations.Any(static loc => loc.IsInMetadata)) + else if (TryGetRecordPrimaryConstructor(typeSymbol, out var primaryConstructor)) { - return new CannotChangeSignatureAnalyzedContext(ChangeSignatureFailureKind.DefinedInMetadata); + symbol = primaryConstructor; } + } - // This should be called after the metadata check above to avoid looking for nodes in metadata. - var declarationLocation = symbol.Locations.FirstOrDefault(); - if (declarationLocation == null) - { - return new CannotChangeSignatureAnalyzedContext(ChangeSignatureFailureKind.DefinedInMetadata); - } + if (!symbol.MatchesKind(SymbolKind.Method, SymbolKind.Property)) + { + return new CannotChangeSignatureAnalyzedContext(ChangeSignatureFailureKind.IncorrectKind); + } - var solution = document.Project.Solution; - var declarationDocument = solution.GetRequiredDocument(declarationLocation.SourceTree!); - var declarationChangeSignatureService = declarationDocument.GetRequiredLanguageService(); + if (symbol.Locations.Any(static loc => loc.IsInMetadata)) + { + return new CannotChangeSignatureAnalyzedContext(ChangeSignatureFailureKind.DefinedInMetadata); + } - int positionForTypeBinding; - var reference = symbol.DeclaringSyntaxReferences.FirstOrDefault(); + // This should be called after the metadata check above to avoid looking for nodes in metadata. + var declarationLocation = symbol.Locations.FirstOrDefault(); + if (declarationLocation == null) + { + return new CannotChangeSignatureAnalyzedContext(ChangeSignatureFailureKind.DefinedInMetadata); + } - if (reference != null) - { - var syntax = await reference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); - positionForTypeBinding = syntax.SpanStart; - } - else - { - // There may be no declaring syntax reference, for example delegate Invoke methods. - // The user may need to fully-qualify type names, including the type(s) defined in - // this document. - positionForTypeBinding = 0; - } + var solution = document.Project.Solution; + var declarationDocument = solution.GetRequiredDocument(declarationLocation.SourceTree!); + var declarationChangeSignatureService = declarationDocument.GetRequiredLanguageService(); - var parameterConfiguration = ParameterConfiguration.Create( - GetParameters(symbol).Select(p => new ExistingParameter(p)).ToImmutableArray(), - symbol.IsExtensionMethod(), selectedIndex); + int positionForTypeBinding; + var reference = symbol.DeclaringSyntaxReferences.FirstOrDefault(); - return new ChangeSignatureAnalysisSucceededContext( - declarationDocument, positionForTypeBinding, symbol, parameterConfiguration, fallbackOptions); + if (reference != null) + { + var syntax = await reference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + positionForTypeBinding = syntax.SpanStart; } - - internal async Task ChangeSignatureWithContextAsync(ChangeSignatureAnalyzedContext context, ChangeSignatureOptionsResult? options, CancellationToken cancellationToken) + else { - return context switch - { - ChangeSignatureAnalysisSucceededContext changeSignatureAnalyzedSucceedContext => await GetChangeSignatureResultAsync(changeSignatureAnalyzedSucceedContext, options, cancellationToken).ConfigureAwait(false), - CannotChangeSignatureAnalyzedContext cannotChangeSignatureAnalyzedContext => new ChangeSignatureResult(succeeded: false, changeSignatureFailureKind: cannotChangeSignatureAnalyzedContext.CannotChangeSignatureReason), - _ => throw ExceptionUtilities.Unreachable(), - }; + // There may be no declaring syntax reference, for example delegate Invoke methods. + // The user may need to fully-qualify type names, including the type(s) defined in + // this document. + positionForTypeBinding = 0; + } - async Task GetChangeSignatureResultAsync(ChangeSignatureAnalysisSucceededContext context, ChangeSignatureOptionsResult? options, CancellationToken cancellationToken) - { - if (options == null) - { - return new ChangeSignatureResult(succeeded: false); - } + var parameterConfiguration = ParameterConfiguration.Create( + GetParameters(symbol).Select(p => new ExistingParameter(p)).ToImmutableArray(), + symbol.IsExtensionMethod(), selectedIndex); - var (updatedSolution, confirmationMessage) = await CreateUpdatedSolutionAsync(context, options, cancellationToken).ConfigureAwait(false); - return new ChangeSignatureResult(updatedSolution != null, updatedSolution, context.Symbol.ToDisplayString(), context.Symbol.GetGlyph(), options.PreviewChanges, confirmationMessage: confirmationMessage); - } - } + return new ChangeSignatureAnalysisSucceededContext( + declarationDocument, positionForTypeBinding, symbol, parameterConfiguration, fallbackOptions); + } - /// Returns null if the operation is cancelled. - internal static ChangeSignatureOptionsResult? GetChangeSignatureOptions(ChangeSignatureAnalyzedContext context) + internal async Task ChangeSignatureWithContextAsync(ChangeSignatureAnalyzedContext context, ChangeSignatureOptionsResult? options, CancellationToken cancellationToken) + { + return context switch { - if (context is not ChangeSignatureAnalysisSucceededContext succeededContext) + ChangeSignatureAnalysisSucceededContext changeSignatureAnalyzedSucceedContext => await GetChangeSignatureResultAsync(changeSignatureAnalyzedSucceedContext, options, cancellationToken).ConfigureAwait(false), + CannotChangeSignatureAnalyzedContext cannotChangeSignatureAnalyzedContext => new ChangeSignatureResult(succeeded: false, changeSignatureFailureKind: cannotChangeSignatureAnalyzedContext.CannotChangeSignatureReason), + _ => throw ExceptionUtilities.Unreachable(), + }; + + async Task GetChangeSignatureResultAsync(ChangeSignatureAnalysisSucceededContext context, ChangeSignatureOptionsResult? options, CancellationToken cancellationToken) + { + if (options == null) { - return null; + return new ChangeSignatureResult(succeeded: false); } - var changeSignatureOptionsService = succeededContext.Solution.Services.GetRequiredService(); + var (updatedSolution, confirmationMessage) = await CreateUpdatedSolutionAsync(context, options, cancellationToken).ConfigureAwait(false); + return new ChangeSignatureResult(updatedSolution != null, updatedSolution, context.Symbol.ToDisplayString(), context.Symbol.GetGlyph(), options.PreviewChanges, confirmationMessage: confirmationMessage); + } + } - return changeSignatureOptionsService.GetChangeSignatureOptions( - succeededContext.Document, succeededContext.PositionForTypeBinding, succeededContext.Symbol, succeededContext.ParameterConfiguration); + /// Returns null if the operation is cancelled. + internal static ChangeSignatureOptionsResult? GetChangeSignatureOptions(ChangeSignatureAnalyzedContext context) + { + if (context is not ChangeSignatureAnalysisSucceededContext succeededContext) + { + return null; } - private static async Task> FindChangeSignatureReferencesAsync( - ISymbol symbol, - Solution solution, - CancellationToken cancellationToken) + var changeSignatureOptionsService = succeededContext.Solution.Services.GetRequiredService(); + + return changeSignatureOptionsService.GetChangeSignatureOptions( + succeededContext.Document, succeededContext.PositionForTypeBinding, succeededContext.Symbol, succeededContext.ParameterConfiguration); + } + + private static async Task> FindChangeSignatureReferencesAsync( + ISymbol symbol, + Solution solution, + CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.FindReference_ChangeSignature, cancellationToken)) { - using (Logger.LogBlock(FunctionId.FindReference_ChangeSignature, cancellationToken)) - { - var streamingProgress = new StreamingProgressCollector(); + var streamingProgress = new StreamingProgressCollector(); - var engine = new FindReferencesSearchEngine( - solution, - documents: null, - ReferenceFinders.DefaultReferenceFinders.Add(DelegateInvokeMethodReferenceFinder.DelegateInvokeMethod), - streamingProgress, - FindReferencesSearchOptions.Default); + var engine = new FindReferencesSearchEngine( + solution, + documents: null, + ReferenceFinders.DefaultReferenceFinders.Add(DelegateInvokeMethodReferenceFinder.DelegateInvokeMethod), + streamingProgress, + FindReferencesSearchOptions.Default); - await engine.FindReferencesAsync(symbol, cancellationToken).ConfigureAwait(false); - return streamingProgress.GetReferencedSymbols(); - } + await engine.FindReferencesAsync(symbol, cancellationToken).ConfigureAwait(false); + return streamingProgress.GetReferencedSymbols(); } + } - private async Task<(Solution updatedSolution, string? confirmationMessage)> CreateUpdatedSolutionAsync( - ChangeSignatureAnalysisSucceededContext context, ChangeSignatureOptionsResult options, CancellationToken cancellationToken) - { - var telemetryTimer = Stopwatch.StartNew(); + private async Task<(Solution updatedSolution, string? confirmationMessage)> CreateUpdatedSolutionAsync( + ChangeSignatureAnalysisSucceededContext context, ChangeSignatureOptionsResult options, CancellationToken cancellationToken) + { + var telemetryTimer = Stopwatch.StartNew(); + + var currentSolution = context.Solution; + var declaredSymbol = context.Symbol; - var currentSolution = context.Solution; - var declaredSymbol = context.Symbol; + var nodesToUpdate = new Dictionary>(); + var definitionToUse = new Dictionary(); - var nodesToUpdate = new Dictionary>(); - var definitionToUse = new Dictionary(); + string? confirmationMessage = null; - string? confirmationMessage = null; + var symbols = await FindChangeSignatureReferencesAsync( + declaredSymbol, context.Solution, cancellationToken).ConfigureAwait(false); - var symbols = await FindChangeSignatureReferencesAsync( - declaredSymbol, context.Solution, cancellationToken).ConfigureAwait(false); + var declaredSymbolParametersCount = GetParameters(declaredSymbol).Length; - var declaredSymbolParametersCount = GetParameters(declaredSymbol).Length; + var telemetryNumberOfDeclarationsToUpdate = 0; + var telemetryNumberOfReferencesToUpdate = 0; - var telemetryNumberOfDeclarationsToUpdate = 0; - var telemetryNumberOfReferencesToUpdate = 0; + foreach (var symbol in symbols) + { + var methodSymbol = symbol.Definition as IMethodSymbol; - foreach (var symbol in symbols) + if (methodSymbol is { MethodKind: MethodKind.PropertyGet or MethodKind.PropertySet }) { - var methodSymbol = symbol.Definition as IMethodSymbol; + continue; + } - if (methodSymbol is { MethodKind: MethodKind.PropertyGet or MethodKind.PropertySet }) - { - continue; - } + if (symbol.Definition.Kind == SymbolKind.NamedType) + { + continue; + } - if (symbol.Definition.Kind == SymbolKind.NamedType) - { - continue; - } + if (symbol.Definition.Locations.Any(static loc => loc.IsInMetadata)) + { + confirmationMessage = FeaturesResources.This_symbol_has_related_definitions_or_references_in_metadata_Changing_its_signature_may_result_in_build_errors_Do_you_want_to_continue; + continue; + } - if (symbol.Definition.Locations.Any(static loc => loc.IsInMetadata)) - { - confirmationMessage = FeaturesResources.This_symbol_has_related_definitions_or_references_in_metadata_Changing_its_signature_may_result_in_build_errors_Do_you_want_to_continue; - continue; - } + var symbolWithSyntacticParameters = symbol.Definition; + var symbolWithSemanticParameters = symbol.Definition; - var symbolWithSyntacticParameters = symbol.Definition; - var symbolWithSemanticParameters = symbol.Definition; + var includeDefinitionLocations = true; - var includeDefinitionLocations = true; + if (symbol.Definition.Kind == SymbolKind.Field) + { + includeDefinitionLocations = false; + } - if (symbol.Definition.Kind == SymbolKind.Field) + if (symbolWithSyntacticParameters is IEventSymbol eventSymbol) + { + if (eventSymbol.Type is INamedTypeSymbol type && type.DelegateInvokeMethod != null) { - includeDefinitionLocations = false; + symbolWithSemanticParameters = type.DelegateInvokeMethod; } - - if (symbolWithSyntacticParameters is IEventSymbol eventSymbol) + else { - if (eventSymbol.Type is INamedTypeSymbol type && type.DelegateInvokeMethod != null) - { - symbolWithSemanticParameters = type.DelegateInvokeMethod; - } - else - { - continue; - } + continue; } + } - if (methodSymbol != null) + if (methodSymbol != null) + { + if (methodSymbol.MethodKind == MethodKind.DelegateInvoke) { - if (methodSymbol.MethodKind == MethodKind.DelegateInvoke) - { - symbolWithSyntacticParameters = methodSymbol.ContainingType; - } - - if (methodSymbol.Name == WellKnownMemberNames.DelegateBeginInvokeName && - methodSymbol.ContainingType != null && - methodSymbol.ContainingType.IsDelegateType()) - { - includeDefinitionLocations = false; - } - - // We update delegates which may have different signature. - // It seems it is enough for now to compare delegates by parameter count only. - if (methodSymbol.Parameters.Length != declaredSymbolParametersCount) - { - includeDefinitionLocations = false; - } + symbolWithSyntacticParameters = methodSymbol.ContainingType; } - // Find and annotate all the relevant definitions - if (includeDefinitionLocations) + if (methodSymbol.Name == WellKnownMemberNames.DelegateBeginInvokeName && + methodSymbol.ContainingType != null && + methodSymbol.ContainingType.IsDelegateType()) { - foreach (var def in symbolWithSyntacticParameters.Locations) - { - if (!TryGetNodeWithEditableSignatureOrAttributes(def, currentSolution, out var nodeToUpdate, out var documentId)) - { - continue; - } - - if (!nodesToUpdate.ContainsKey(documentId)) - { - nodesToUpdate.Add(documentId, []); - } - - telemetryNumberOfDeclarationsToUpdate++; - AddUpdatableNodeToDictionaries(nodesToUpdate, documentId, nodeToUpdate, definitionToUse, symbolWithSemanticParameters); - } + includeDefinitionLocations = false; } - // Find and annotate all the relevant references - foreach (var location in symbol.Locations) + // We update delegates which may have different signature. + // It seems it is enough for now to compare delegates by parameter count only. + if (methodSymbol.Parameters.Length != declaredSymbolParametersCount) { - if (location.Location.IsInMetadata) - { - confirmationMessage = FeaturesResources.This_symbol_has_related_definitions_or_references_in_metadata_Changing_its_signature_may_result_in_build_errors_Do_you_want_to_continue; - continue; - } + includeDefinitionLocations = false; + } + } - if (!TryGetNodeWithEditableSignatureOrAttributes(location.Location, currentSolution, out var nodeToUpdate2, out var documentId2)) + // Find and annotate all the relevant definitions + if (includeDefinitionLocations) + { + foreach (var def in symbolWithSyntacticParameters.Locations) + { + if (!TryGetNodeWithEditableSignatureOrAttributes(def, currentSolution, out var nodeToUpdate, out var documentId)) { continue; } - if (!nodesToUpdate.ContainsKey(documentId2)) + if (!nodesToUpdate.ContainsKey(documentId)) { - nodesToUpdate.Add(documentId2, []); + nodesToUpdate.Add(documentId, []); } - telemetryNumberOfReferencesToUpdate++; - AddUpdatableNodeToDictionaries(nodesToUpdate, documentId2, nodeToUpdate2, definitionToUse, symbolWithSemanticParameters); + telemetryNumberOfDeclarationsToUpdate++; + AddUpdatableNodeToDictionaries(nodesToUpdate, documentId, nodeToUpdate, definitionToUse, symbolWithSemanticParameters); } } - // Construct all the relevant syntax trees from the base solution - var updatedRoots = new Dictionary(); - foreach (var docId in nodesToUpdate.Keys) + // Find and annotate all the relevant references + foreach (var location in symbol.Locations) { - var doc = currentSolution.GetRequiredDocument(docId); - var updater = doc.Project.Services.GetRequiredService(); - var root = await doc.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (root is null) + if (location.Location.IsInMetadata) { - throw new NotSupportedException(WorkspacesResources.Document_does_not_support_syntax_trees); + confirmationMessage = FeaturesResources.This_symbol_has_related_definitions_or_references_in_metadata_Changing_its_signature_may_result_in_build_errors_Do_you_want_to_continue; + continue; } - var nodes = nodesToUpdate[docId]; - - var newRoot = root.ReplaceNodes(nodes, (originalNode, potentiallyUpdatedNode) => + if (!TryGetNodeWithEditableSignatureOrAttributes(location.Location, currentSolution, out var nodeToUpdate2, out var documentId2)) { - return updater.ChangeSignatureAsync( - doc, - definitionToUse[originalNode], - potentiallyUpdatedNode, - originalNode, - UpdateSignatureChangeToIncludeExtraParametersFromTheDeclarationSymbol(definitionToUse[originalNode], options.UpdatedSignature), - context.FallbackOptions, - cancellationToken).WaitAndGetResult_CanCallOnBackground(cancellationToken); - }); - - var annotatedNodes = newRoot.GetAnnotatedNodes(syntaxAnnotation: changeSignatureFormattingAnnotation); - var formattingOptions = await doc.GetSyntaxFormattingOptionsAsync(context.FallbackOptions, cancellationToken).ConfigureAwait(false); - - var formattedRoot = Formatter.Format( - newRoot, - changeSignatureFormattingAnnotation, - doc.Project.Solution.Services, - options: formattingOptions, - rules: GetFormattingRules(doc), - cancellationToken: CancellationToken.None); - - updatedRoots[docId] = formattedRoot; - } - - // Update the documents using the updated syntax trees - foreach (var docId in nodesToUpdate.Keys) - { - var updatedDoc = currentSolution.GetRequiredDocument(docId).WithSyntaxRoot(updatedRoots[docId]); - var cleanupOptions = await updatedDoc.GetCodeCleanupOptionsAsync(context.FallbackOptions, cancellationToken).ConfigureAwait(false); + continue; + } - var docWithImports = await ImportAdder.AddImportsFromSymbolAnnotationAsync(updatedDoc, cleanupOptions.AddImportOptions, cancellationToken).ConfigureAwait(false); - var reducedDoc = await Simplifier.ReduceAsync(docWithImports, Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken: cancellationToken).ConfigureAwait(false); - var formattedDoc = await Formatter.FormatAsync(reducedDoc, SyntaxAnnotation.ElasticAnnotation, cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false); + if (!nodesToUpdate.ContainsKey(documentId2)) + { + nodesToUpdate.Add(documentId2, []); + } - currentSolution = currentSolution.WithDocumentSyntaxRoot(docId, (await formattedDoc.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!); + telemetryNumberOfReferencesToUpdate++; + AddUpdatableNodeToDictionaries(nodesToUpdate, documentId2, nodeToUpdate2, definitionToUse, symbolWithSemanticParameters); } - - telemetryTimer.Stop(); - ChangeSignatureLogger.LogCommitInformation(telemetryNumberOfDeclarationsToUpdate, telemetryNumberOfReferencesToUpdate, telemetryTimer.Elapsed); - - return (currentSolution, confirmationMessage); } -#nullable disable - - private static void AddUpdatableNodeToDictionaries(Dictionary> nodesToUpdate, DocumentId documentId, SyntaxNode nodeToUpdate, Dictionary definitionToUse, ISymbol symbolWithSemanticParameters) + // Construct all the relevant syntax trees from the base solution + var updatedRoots = new Dictionary(); + foreach (var docId in nodesToUpdate.Keys) { - nodesToUpdate[documentId].Add(nodeToUpdate); - if (definitionToUse.TryGetValue(nodeToUpdate, out var sym) && sym != symbolWithSemanticParameters) + var doc = currentSolution.GetRequiredDocument(docId); + var updater = doc.Project.Services.GetRequiredService(); + var root = await doc.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) { - Debug.Assert(false, "Change Signature: Attempted to modify node twice with different semantic parameters."); + throw new NotSupportedException(WorkspacesResources.Document_does_not_support_syntax_trees); } - definitionToUse[nodeToUpdate] = symbolWithSemanticParameters; + var nodes = nodesToUpdate[docId]; + + var newRoot = root.ReplaceNodes(nodes, (originalNode, potentiallyUpdatedNode) => + { + return updater.ChangeSignatureAsync( + doc, + definitionToUse[originalNode], + potentiallyUpdatedNode, + originalNode, + UpdateSignatureChangeToIncludeExtraParametersFromTheDeclarationSymbol(definitionToUse[originalNode], options.UpdatedSignature), + context.FallbackOptions, + cancellationToken).WaitAndGetResult_CanCallOnBackground(cancellationToken); + }); + + var annotatedNodes = newRoot.GetAnnotatedNodes(syntaxAnnotation: changeSignatureFormattingAnnotation); + var formattingOptions = await doc.GetSyntaxFormattingOptionsAsync(context.FallbackOptions, cancellationToken).ConfigureAwait(false); + + var formattedRoot = Formatter.Format( + newRoot, + changeSignatureFormattingAnnotation, + doc.Project.Solution.Services, + options: formattingOptions, + rules: GetFormattingRules(doc), + cancellationToken: CancellationToken.None); + + updatedRoots[docId] = formattedRoot; } - private static bool TryGetNodeWithEditableSignatureOrAttributes(Location location, Solution solution, out SyntaxNode nodeToUpdate, out DocumentId documentId) + // Update the documents using the updated syntax trees + foreach (var docId in nodesToUpdate.Keys) { - var tree = location.SourceTree; - documentId = solution.GetDocumentId(tree); - var document = solution.GetDocument(documentId); + var updatedDoc = currentSolution.GetRequiredDocument(docId).WithSyntaxRoot(updatedRoots[docId]); + var cleanupOptions = await updatedDoc.GetCodeCleanupOptionsAsync(context.FallbackOptions, cancellationToken).ConfigureAwait(false); - var root = tree.GetRoot(); - var node = root.FindNode(location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true); - var updater = document.GetLanguageService(); - nodeToUpdate = updater.FindNodeToUpdate(document, node); + var docWithImports = await ImportAdder.AddImportsFromSymbolAnnotationAsync(updatedDoc, cleanupOptions.AddImportOptions, cancellationToken).ConfigureAwait(false); + var reducedDoc = await Simplifier.ReduceAsync(docWithImports, Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + var formattedDoc = await Formatter.FormatAsync(reducedDoc, SyntaxAnnotation.ElasticAnnotation, cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false); - return nodeToUpdate != null; + currentSolution = currentSolution.WithDocumentSyntaxRoot(docId, (await formattedDoc.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!); } - protected ImmutableArray PermuteArguments( - ISymbol declarationSymbol, - ImmutableArray arguments, - SignatureChange updatedSignature, - bool isReducedExtensionMethod = false) + telemetryTimer.Stop(); + ChangeSignatureLogger.LogCommitInformation(telemetryNumberOfDeclarationsToUpdate, telemetryNumberOfReferencesToUpdate, telemetryTimer.Elapsed); + + return (currentSolution, confirmationMessage); + } + +#nullable disable + + private static void AddUpdatableNodeToDictionaries(Dictionary> nodesToUpdate, DocumentId documentId, SyntaxNode nodeToUpdate, Dictionary definitionToUse, ISymbol symbolWithSemanticParameters) + { + nodesToUpdate[documentId].Add(nodeToUpdate); + if (definitionToUse.TryGetValue(nodeToUpdate, out var sym) && sym != symbolWithSemanticParameters) { - // 1. Determine which parameters are permutable - var declarationParameters = GetParameters(declarationSymbol); - var declarationParametersToPermute = GetParametersToPermute(arguments, declarationParameters, isReducedExtensionMethod); - var argumentsToPermute = arguments.Take(declarationParametersToPermute.Length).ToList(); + Debug.Assert(false, "Change Signature: Attempted to modify node twice with different semantic parameters."); + } - // 2. Create an argument to parameter map, and a parameter to index map for the sort. - var argumentToParameterMap = new Dictionary(); - var parameterToIndexMap = new Dictionary(); + definitionToUse[nodeToUpdate] = symbolWithSemanticParameters; + } - for (var i = 0; i < declarationParametersToPermute.Length; i++) - { - var decl = declarationParametersToPermute[i]; - var arg = argumentsToPermute[i]; + private static bool TryGetNodeWithEditableSignatureOrAttributes(Location location, Solution solution, out SyntaxNode nodeToUpdate, out DocumentId documentId) + { + var tree = location.SourceTree; + documentId = solution.GetDocumentId(tree); + var document = solution.GetDocument(documentId); - argumentToParameterMap[arg] = decl; - var originalIndex = declarationParameters.IndexOf(decl); + var root = tree.GetRoot(); + var node = root.FindNode(location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true); + var updater = document.GetLanguageService(); + nodeToUpdate = updater.FindNodeToUpdate(document, node); - var updatedIndex = updatedSignature.GetUpdatedIndex(originalIndex); + return nodeToUpdate != null; + } - // If there's no value, then we may be handling a method with more parameters than the original symbol (like BeginInvoke). - parameterToIndexMap[decl] = updatedIndex ?? -1; - } + protected ImmutableArray PermuteArguments( + ISymbol declarationSymbol, + ImmutableArray arguments, + SignatureChange updatedSignature, + bool isReducedExtensionMethod = false) + { + // 1. Determine which parameters are permutable + var declarationParameters = GetParameters(declarationSymbol); + var declarationParametersToPermute = GetParametersToPermute(arguments, declarationParameters, isReducedExtensionMethod); + var argumentsToPermute = arguments.Take(declarationParametersToPermute.Length).ToList(); - // 3. Sort the arguments that need to be reordered - argumentsToPermute.Sort((a1, a2) => { return parameterToIndexMap[argumentToParameterMap[a1]].CompareTo(parameterToIndexMap[argumentToParameterMap[a2]]); }); + // 2. Create an argument to parameter map, and a parameter to index map for the sort. + var argumentToParameterMap = new Dictionary(); + var parameterToIndexMap = new Dictionary(); - // 4. Add names to arguments where necessary. - var newArguments = ArrayBuilder.GetInstance(); - var expectedIndex = 0 + (isReducedExtensionMethod ? 1 : 0); - var seenNamedArgument = false; + for (var i = 0; i < declarationParametersToPermute.Length; i++) + { + var decl = declarationParametersToPermute[i]; + var arg = argumentsToPermute[i]; - // Holds the params array argument so it can be - // added at the end. - IUnifiedArgumentSyntax paramsArrayArgument = null; + argumentToParameterMap[arg] = decl; + var originalIndex = declarationParameters.IndexOf(decl); - foreach (var argument in argumentsToPermute) - { - var param = argumentToParameterMap[argument]; - var actualIndex = updatedSignature.GetUpdatedIndex(declarationParameters.IndexOf(param)); + var updatedIndex = updatedSignature.GetUpdatedIndex(originalIndex); - if (!actualIndex.HasValue) - { - continue; - } + // If there's no value, then we may be handling a method with more parameters than the original symbol (like BeginInvoke). + parameterToIndexMap[decl] = updatedIndex ?? -1; + } - if (!param.IsParams) - { - // If seen a named argument before, add names for subsequent ones. - if ((seenNamedArgument || actualIndex != expectedIndex) && !argument.IsNamed) - { - newArguments.Add(argument.WithName(param.Name).WithAdditionalAnnotations(Formatter.Annotation)); - seenNamedArgument = true; - } - else - { - newArguments.Add(argument); - } - } - else - { - paramsArrayArgument = argument; - } + // 3. Sort the arguments that need to be reordered + argumentsToPermute.Sort((a1, a2) => { return parameterToIndexMap[argumentToParameterMap[a1]].CompareTo(parameterToIndexMap[argumentToParameterMap[a2]]); }); - seenNamedArgument |= argument.IsNamed; - expectedIndex++; + // 4. Add names to arguments where necessary. + var newArguments = ArrayBuilder.GetInstance(); + var expectedIndex = 0 + (isReducedExtensionMethod ? 1 : 0); + var seenNamedArgument = false; + + // Holds the params array argument so it can be + // added at the end. + IUnifiedArgumentSyntax paramsArrayArgument = null; + + foreach (var argument in argumentsToPermute) + { + var param = argumentToParameterMap[argument]; + var actualIndex = updatedSignature.GetUpdatedIndex(declarationParameters.IndexOf(param)); + + if (!actualIndex.HasValue) + { + continue; } - // 5. Add the params argument with the first value: - if (paramsArrayArgument != null) + if (!param.IsParams) { - var param = argumentToParameterMap[paramsArrayArgument]; - if (seenNamedArgument && !paramsArrayArgument.IsNamed) + // If seen a named argument before, add names for subsequent ones. + if ((seenNamedArgument || actualIndex != expectedIndex) && !argument.IsNamed) { - newArguments.Add(paramsArrayArgument.WithName(param.Name).WithAdditionalAnnotations(Formatter.Annotation)); + newArguments.Add(argument.WithName(param.Name).WithAdditionalAnnotations(Formatter.Annotation)); seenNamedArgument = true; } else { - newArguments.Add(paramsArrayArgument); + newArguments.Add(argument); } } - - // 6. Add the remaining arguments. These will already have names or be params arguments, but may have been removed. - var removedParams = updatedSignature.OriginalConfiguration.ParamsParameter != null && updatedSignature.UpdatedConfiguration.ParamsParameter == null; - for (var i = declarationParametersToPermute.Length; i < arguments.Length; i++) + else { - if (!arguments[i].IsNamed && removedParams && i >= updatedSignature.UpdatedConfiguration.ToListOfParameters().Length) - { - break; - } - - if (!arguments[i].IsNamed || updatedSignature.UpdatedConfiguration.ToListOfParameters().Any(static (p, arg) => p.Name == arg.arguments[arg.i].GetName(), (arguments, i))) - { - newArguments.Add(arguments[i]); - } + paramsArrayArgument = argument; } - return newArguments.ToImmutableAndFree(); + seenNamedArgument |= argument.IsNamed; + expectedIndex++; } - /// - /// Sometimes signature changes can cascade from a declaration with m parameters to one with n > m parameters, such as - /// delegate Invoke methods (m) and delegate BeginInvoke methods (n = m + 2). This method adds on those extra parameters - /// to the base . - /// - private SignatureChange UpdateSignatureChangeToIncludeExtraParametersFromTheDeclarationSymbol(ISymbol declarationSymbol, SignatureChange updatedSignature) + // 5. Add the params argument with the first value: + if (paramsArrayArgument != null) { - var realParameters = GetParameters(declarationSymbol); - if (realParameters.Length > updatedSignature.OriginalConfiguration.ToListOfParameters().Length) + var param = argumentToParameterMap[paramsArrayArgument]; + if (seenNamedArgument && !paramsArrayArgument.IsNamed) { - var originalConfigurationParameters = updatedSignature.OriginalConfiguration.ToListOfParameters(); - var updatedConfigurationParameters = updatedSignature.UpdatedConfiguration.ToListOfParameters(); - - var bonusParameters = realParameters.Skip(originalConfigurationParameters.Length); - - var originalConfigurationParametersWithExtraParameters = originalConfigurationParameters.AddRange(bonusParameters.Select(p => new ExistingParameter(p))); - var updatedConfigurationParametersWithExtraParameters = updatedConfigurationParameters.AddRange(bonusParameters.Select(p => new ExistingParameter(p))); - - updatedSignature = new SignatureChange( - ParameterConfiguration.Create(originalConfigurationParametersWithExtraParameters, updatedSignature.OriginalConfiguration.ThisParameter != null, selectedIndex: 0), - ParameterConfiguration.Create(updatedConfigurationParametersWithExtraParameters, updatedSignature.OriginalConfiguration.ThisParameter != null, selectedIndex: 0)); + newArguments.Add(paramsArrayArgument.WithName(param.Name).WithAdditionalAnnotations(Formatter.Annotation)); + seenNamedArgument = true; + } + else + { + newArguments.Add(paramsArrayArgument); } - - return updatedSignature; } - private static ImmutableArray GetParametersToPermute( - ImmutableArray arguments, - ImmutableArray originalParameters, - bool isReducedExtensionMethod) + // 6. Add the remaining arguments. These will already have names or be params arguments, but may have been removed. + var removedParams = updatedSignature.OriginalConfiguration.ParamsParameter != null && updatedSignature.UpdatedConfiguration.ParamsParameter == null; + for (var i = declarationParametersToPermute.Length; i < arguments.Length; i++) { - var position = -1 + (isReducedExtensionMethod ? 1 : 0); - var parametersToPermute = ArrayBuilder.GetInstance(); + if (!arguments[i].IsNamed && removedParams && i >= updatedSignature.UpdatedConfiguration.ToListOfParameters().Length) + { + break; + } - foreach (var argument in arguments) + if (!arguments[i].IsNamed || updatedSignature.UpdatedConfiguration.ToListOfParameters().Any(static (p, arg) => p.Name == arg.arguments[arg.i].GetName(), (arguments, i))) { - if (argument.IsNamed) - { - var name = argument.GetName(); + newArguments.Add(arguments[i]); + } + } - // TODO: file bug for var match = originalParameters.FirstOrDefault(p => p.Name == ); - var match = originalParameters.FirstOrDefault(p => p.Name == name); - if (match == null || originalParameters.IndexOf(match) <= position) - { - break; - } - else - { - position = originalParameters.IndexOf(match); - parametersToPermute.Add(match); - } - } - else - { - position++; + return newArguments.ToImmutableAndFree(); + } - if (position >= originalParameters.Length) - { - break; - } + /// + /// Sometimes signature changes can cascade from a declaration with m parameters to one with n > m parameters, such as + /// delegate Invoke methods (m) and delegate BeginInvoke methods (n = m + 2). This method adds on those extra parameters + /// to the base . + /// + private SignatureChange UpdateSignatureChangeToIncludeExtraParametersFromTheDeclarationSymbol(ISymbol declarationSymbol, SignatureChange updatedSignature) + { + var realParameters = GetParameters(declarationSymbol); + if (realParameters.Length > updatedSignature.OriginalConfiguration.ToListOfParameters().Length) + { + var originalConfigurationParameters = updatedSignature.OriginalConfiguration.ToListOfParameters(); + var updatedConfigurationParameters = updatedSignature.UpdatedConfiguration.ToListOfParameters(); - parametersToPermute.Add(originalParameters[position]); - } - } + var bonusParameters = realParameters.Skip(originalConfigurationParameters.Length); + + var originalConfigurationParametersWithExtraParameters = originalConfigurationParameters.AddRange(bonusParameters.Select(p => new ExistingParameter(p))); + var updatedConfigurationParametersWithExtraParameters = updatedConfigurationParameters.AddRange(bonusParameters.Select(p => new ExistingParameter(p))); - return parametersToPermute.ToImmutableAndFree(); + updatedSignature = new SignatureChange( + ParameterConfiguration.Create(originalConfigurationParametersWithExtraParameters, updatedSignature.OriginalConfiguration.ThisParameter != null, selectedIndex: 0), + ParameterConfiguration.Create(updatedConfigurationParametersWithExtraParameters, updatedSignature.OriginalConfiguration.ThisParameter != null, selectedIndex: 0)); } - /// - /// Given the cursor position, find which parameter is selected. - /// Returns 0 as the default value. Note that the ChangeSignature dialog adjusts the selection for - /// the `this` parameter in extension methods (the selected index won't remain 0). - /// - protected static int GetParameterIndex(SeparatedSyntaxList parameters, int position) - where TNode : SyntaxNode + return updatedSignature; + } + + private static ImmutableArray GetParametersToPermute( + ImmutableArray arguments, + ImmutableArray originalParameters, + bool isReducedExtensionMethod) + { + var position = -1 + (isReducedExtensionMethod ? 1 : 0); + var parametersToPermute = ArrayBuilder.GetInstance(); + + foreach (var argument in arguments) { - if (parameters.Count == 0) + if (argument.IsNamed) { - return 0; - } + var name = argument.GetName(); - if (position < parameters.Span.Start) - { - return 0; + // TODO: file bug for var match = originalParameters.FirstOrDefault(p => p.Name == ); + var match = originalParameters.FirstOrDefault(p => p.Name == name); + if (match == null || originalParameters.IndexOf(match) <= position) + { + break; + } + else + { + position = originalParameters.IndexOf(match); + parametersToPermute.Add(match); + } } - - if (position > parameters.Span.End) + else { - return 0; - } + position++; - for (var i = 0; i < parameters.Count - 1; i++) - { - // `$$,` points to the argument before the separator - // but `,$$` points to the argument following the separator - if (position <= parameters.GetSeparator(i).Span.Start) + if (position >= originalParameters.Length) { - return i; + break; } + + parametersToPermute.Add(originalParameters[position]); } + } - return parameters.Count - 1; + return parametersToPermute.ToImmutableAndFree(); + } + + /// + /// Given the cursor position, find which parameter is selected. + /// Returns 0 as the default value. Note that the ChangeSignature dialog adjusts the selection for + /// the `this` parameter in extension methods (the selected index won't remain 0). + /// + protected static int GetParameterIndex(SeparatedSyntaxList parameters, int position) + where TNode : SyntaxNode + { + if (parameters.Count == 0) + { + return 0; } - protected (ImmutableArray parameters, ImmutableArray separators) UpdateDeclarationBase( - SeparatedSyntaxList list, - SignatureChange updatedSignature, - Func createNewParameterMethod) where T : SyntaxNode + if (position < parameters.Span.Start) { - var originalParameters = updatedSignature.OriginalConfiguration.ToListOfParameters(); - var reorderedParameters = updatedSignature.UpdatedConfiguration.ToListOfParameters(); + return 0; + } - var numAddedParameters = 0; + if (position > parameters.Span.End) + { + return 0; + } - // Iterate through the list of new parameters and combine any - // preexisting parameters with added parameters to construct - // the full updated list. - var newParameters = ImmutableArray.CreateBuilder(); - for (var index = 0; index < reorderedParameters.Length; index++) + for (var i = 0; i < parameters.Count - 1; i++) + { + // `$$,` points to the argument before the separator + // but `,$$` points to the argument following the separator + if (position <= parameters.GetSeparator(i).Span.Start) { - var newParam = reorderedParameters[index]; - if (newParam is ExistingParameter existingParameter) - { - var pos = originalParameters.IndexOf(p => p is ExistingParameter ep && ep.Symbol.Equals(existingParameter.Symbol)); - var param = list[pos]; + return i; + } + } - if (index < list.Count) - { - param = TransferLeadingWhitespaceTrivia(param, list[index]); - } - else - { - param = param.WithLeadingTrivia(); - } + return parameters.Count - 1; + } + + protected (ImmutableArray parameters, ImmutableArray separators) UpdateDeclarationBase( + SeparatedSyntaxList list, + SignatureChange updatedSignature, + Func createNewParameterMethod) where T : SyntaxNode + { + var originalParameters = updatedSignature.OriginalConfiguration.ToListOfParameters(); + var reorderedParameters = updatedSignature.UpdatedConfiguration.ToListOfParameters(); - newParameters.Add(param); + var numAddedParameters = 0; + + // Iterate through the list of new parameters and combine any + // preexisting parameters with added parameters to construct + // the full updated list. + var newParameters = ImmutableArray.CreateBuilder(); + for (var index = 0; index < reorderedParameters.Length; index++) + { + var newParam = reorderedParameters[index]; + if (newParam is ExistingParameter existingParameter) + { + var pos = originalParameters.IndexOf(p => p is ExistingParameter ep && ep.Symbol.Equals(existingParameter.Symbol)); + var param = list[pos]; + + if (index < list.Count) + { + param = TransferLeadingWhitespaceTrivia(param, list[index]); } else { - // Added parameter - var newParameter = createNewParameterMethod((AddedParameter)newParam); - - if (index < list.Count) - { - newParameter = TransferLeadingWhitespaceTrivia(newParameter, list[index]); - } - else - { - newParameter = newParameter.WithLeadingTrivia(); - } - - newParameters.Add(newParameter); - numAddedParameters++; + param = param.WithLeadingTrivia(); } + + newParameters.Add(param); } + else + { + // Added parameter + var newParameter = createNewParameterMethod((AddedParameter)newParam); - // (a,b,c) - // Adding X parameters, need to add X separators. - var numSeparatorsToSkip = originalParameters.Length - reorderedParameters.Length; + if (index < list.Count) + { + newParameter = TransferLeadingWhitespaceTrivia(newParameter, list[index]); + } + else + { + newParameter = newParameter.WithLeadingTrivia(); + } - if (originalParameters.Length == 0) - { - // () - // Adding X parameters, need to add X-1 separators. - numSeparatorsToSkip++; + newParameters.Add(newParameter); + numAddedParameters++; } - - return (newParameters.ToImmutable(), GetSeparators(list, numSeparatorsToSkip)); } - protected ImmutableArray GetSeparators(SeparatedSyntaxList arguments, int numSeparatorsToSkip) where T : SyntaxNode + // (a,b,c) + // Adding X parameters, need to add X separators. + var numSeparatorsToSkip = originalParameters.Length - reorderedParameters.Length; + + if (originalParameters.Length == 0) { - var separators = ImmutableArray.CreateBuilder(); + // () + // Adding X parameters, need to add X-1 separators. + numSeparatorsToSkip++; + } - for (var i = 0; i < arguments.SeparatorCount - numSeparatorsToSkip; i++) - { - separators.Add(i < arguments.SeparatorCount - ? arguments.GetSeparator(i) - : CommaTokenWithElasticSpace()); - } + return (newParameters.ToImmutable(), GetSeparators(list, numSeparatorsToSkip)); + } - return separators.ToImmutable(); - } + protected ImmutableArray GetSeparators(SeparatedSyntaxList arguments, int numSeparatorsToSkip) where T : SyntaxNode + { + var separators = ImmutableArray.CreateBuilder(); - protected virtual async Task> AddNewArgumentsToListAsync( - ISymbol declarationSymbol, - SeparatedSyntaxList newArguments, - SignatureChange signaturePermutation, - bool isReducedExtensionMethod, - bool isParamsArrayExpanded, - bool generateAttributeArguments, - Document document, - int position, - CancellationToken cancellationToken) - where TArgumentSyntax : SyntaxNode + for (var i = 0; i < arguments.SeparatorCount - numSeparatorsToSkip; i++) { - var fullList = ArrayBuilder.GetInstance(); - var separators = ArrayBuilder.GetInstance(); + separators.Add(i < arguments.SeparatorCount + ? arguments.GetSeparator(i) + : CommaTokenWithElasticSpace()); + } + + return separators.ToImmutable(); + } - var updatedParameters = signaturePermutation.UpdatedConfiguration.ToListOfParameters(); + protected virtual async Task> AddNewArgumentsToListAsync( + ISymbol declarationSymbol, + SeparatedSyntaxList newArguments, + SignatureChange signaturePermutation, + bool isReducedExtensionMethod, + bool isParamsArrayExpanded, + bool generateAttributeArguments, + Document document, + int position, + CancellationToken cancellationToken) + where TArgumentSyntax : SyntaxNode + { + var fullList = ArrayBuilder.GetInstance(); + var separators = ArrayBuilder.GetInstance(); + + var updatedParameters = signaturePermutation.UpdatedConfiguration.ToListOfParameters(); - var indexInListOfPreexistingArguments = 0; + var indexInListOfPreexistingArguments = 0; - var seenNamedArguments = false; - var seenOmitted = false; - var paramsHandled = false; + var seenNamedArguments = false; + var seenOmitted = false; + var paramsHandled = false; - for (var i = 0; i < updatedParameters.Length; i++) + for (var i = 0; i < updatedParameters.Length; i++) + { + // Skip this parameter in list of arguments for extension method calls but not for reduced ones. + if (updatedParameters[i] != signaturePermutation.UpdatedConfiguration.ThisParameter + || !isReducedExtensionMethod) { - // Skip this parameter in list of arguments for extension method calls but not for reduced ones. - if (updatedParameters[i] != signaturePermutation.UpdatedConfiguration.ThisParameter - || !isReducedExtensionMethod) + var parameters = GetParameters(declarationSymbol); + if (updatedParameters[i] is AddedParameter addedParameter) { - var parameters = GetParameters(declarationSymbol); - if (updatedParameters[i] is AddedParameter addedParameter) - { - // Omitting an argument only works in some languages, depending on whether - // there is a params array. We sometimes need to reinterpret an requested - // omitted parameter as one with a TODO requested. - var forcedCallsiteErrorDueToParamsArray = addedParameter.CallSiteKind == CallSiteKind.Omitted && - parameters.LastOrDefault()?.IsParams == true && - !SupportsOptionalAndParamsArrayParametersSimultaneously(); - - var isCallsiteActuallyOmitted = addedParameter.CallSiteKind == CallSiteKind.Omitted && !forcedCallsiteErrorDueToParamsArray; - var isCallsiteActuallyTODO = addedParameter.CallSiteKind == CallSiteKind.Todo || forcedCallsiteErrorDueToParamsArray; + // Omitting an argument only works in some languages, depending on whether + // there is a params array. We sometimes need to reinterpret an requested + // omitted parameter as one with a TODO requested. + var forcedCallsiteErrorDueToParamsArray = addedParameter.CallSiteKind == CallSiteKind.Omitted && + parameters.LastOrDefault()?.IsParams == true && + !SupportsOptionalAndParamsArrayParametersSimultaneously(); - if (isCallsiteActuallyOmitted) - { - seenOmitted = true; - seenNamedArguments = true; - continue; - } + var isCallsiteActuallyOmitted = addedParameter.CallSiteKind == CallSiteKind.Omitted && !forcedCallsiteErrorDueToParamsArray; + var isCallsiteActuallyTODO = addedParameter.CallSiteKind == CallSiteKind.Todo || forcedCallsiteErrorDueToParamsArray; - var expression = await GenerateInferredCallsiteExpressionAsync( - document, - position, - addedParameter, - cancellationToken).ConfigureAwait(false); + if (isCallsiteActuallyOmitted) + { + seenOmitted = true; + seenNamedArguments = true; + continue; + } - if (expression == null) - { - // If we tried to infer the expression but failed, use a TODO instead. - isCallsiteActuallyTODO |= addedParameter.CallSiteKind == CallSiteKind.Inferred; + var expression = await GenerateInferredCallsiteExpressionAsync( + document, + position, + addedParameter, + cancellationToken).ConfigureAwait(false); - expression = Generator.ParseExpression(isCallsiteActuallyTODO ? "TODO" : addedParameter.CallSiteValue); - } + if (expression == null) + { + // If we tried to infer the expression but failed, use a TODO instead. + isCallsiteActuallyTODO |= addedParameter.CallSiteKind == CallSiteKind.Inferred; - // TODO: Need to be able to specify which kind of attribute argument it is to the SyntaxGenerator. - // https://github.com/dotnet/roslyn/issues/43354 - var argument = generateAttributeArguments - ? (TArgumentSyntax)Generator.AttributeArgument( - name: seenNamedArguments || addedParameter.CallSiteKind == CallSiteKind.ValueWithName ? addedParameter.Name : null, - expression: expression) - : (TArgumentSyntax)Generator.Argument( - name: seenNamedArguments || addedParameter.CallSiteKind == CallSiteKind.ValueWithName ? addedParameter.Name : null, - refKind: RefKind.None, - expression: expression); - - fullList.Add(argument); - separators.Add(CommaTokenWithElasticSpace()); + expression = Generator.ParseExpression(isCallsiteActuallyTODO ? "TODO" : addedParameter.CallSiteValue); } - else + + // TODO: Need to be able to specify which kind of attribute argument it is to the SyntaxGenerator. + // https://github.com/dotnet/roslyn/issues/43354 + var argument = generateAttributeArguments + ? (TArgumentSyntax)Generator.AttributeArgument( + name: seenNamedArguments || addedParameter.CallSiteKind == CallSiteKind.ValueWithName ? addedParameter.Name : null, + expression: expression) + : (TArgumentSyntax)Generator.Argument( + name: seenNamedArguments || addedParameter.CallSiteKind == CallSiteKind.ValueWithName ? addedParameter.Name : null, + refKind: RefKind.None, + expression: expression); + + fullList.Add(argument); + separators.Add(CommaTokenWithElasticSpace()); + } + else + { + if (indexInListOfPreexistingArguments == parameters.Length - 1 && + parameters[indexInListOfPreexistingArguments].IsParams) { - if (indexInListOfPreexistingArguments == parameters.Length - 1 && - parameters[indexInListOfPreexistingArguments].IsParams) + // Handling params array + if (seenOmitted) { - // Handling params array - if (seenOmitted) + // Need to ensure the params array is an actual array, and that the argument is named. + if (isParamsArrayExpanded) { - // Need to ensure the params array is an actual array, and that the argument is named. - if (isParamsArrayExpanded) - { - var newArgument = CreateExplicitParamsArrayFromIndividualArguments(newArguments, indexInListOfPreexistingArguments, parameters[indexInListOfPreexistingArguments]); - newArgument = AddNameToArgument(newArgument, parameters[indexInListOfPreexistingArguments].Name); - fullList.Add(newArgument); - } - else if (indexInListOfPreexistingArguments < newArguments.Count) - { - var newArgument = newArguments[indexInListOfPreexistingArguments]; - newArgument = AddNameToArgument(newArgument, parameters[indexInListOfPreexistingArguments].Name); - fullList.Add(newArgument); - } - - paramsHandled = true; + var newArgument = CreateExplicitParamsArrayFromIndividualArguments(newArguments, indexInListOfPreexistingArguments, parameters[indexInListOfPreexistingArguments]); + newArgument = AddNameToArgument(newArgument, parameters[indexInListOfPreexistingArguments].Name); + fullList.Add(newArgument); } - else + else if (indexInListOfPreexistingArguments < newArguments.Count) { - // Normal case. Handled later. + var newArgument = newArguments[indexInListOfPreexistingArguments]; + newArgument = AddNameToArgument(newArgument, parameters[indexInListOfPreexistingArguments].Name); + fullList.Add(newArgument); } + + paramsHandled = true; } - else if (indexInListOfPreexistingArguments < newArguments.Count) + else { - if (SyntaxFacts.IsNamedArgument(newArguments[indexInListOfPreexistingArguments])) - { - seenNamedArguments = true; - } - - if (indexInListOfPreexistingArguments < newArguments.SeparatorCount) - { - separators.Add(newArguments.GetSeparator(indexInListOfPreexistingArguments)); - } + // Normal case. Handled later. + } + } + else if (indexInListOfPreexistingArguments < newArguments.Count) + { + if (SyntaxFacts.IsNamedArgument(newArguments[indexInListOfPreexistingArguments])) + { + seenNamedArguments = true; + } - var newArgument = newArguments[indexInListOfPreexistingArguments]; + if (indexInListOfPreexistingArguments < newArguments.SeparatorCount) + { + separators.Add(newArguments.GetSeparator(indexInListOfPreexistingArguments)); + } - if (seenNamedArguments && !SyntaxFacts.IsNamedArgument(newArgument)) - { - newArgument = AddNameToArgument(newArgument, parameters[indexInListOfPreexistingArguments].Name); - } + var newArgument = newArguments[indexInListOfPreexistingArguments]; - fullList.Add(newArgument); - indexInListOfPreexistingArguments++; + if (seenNamedArguments && !SyntaxFacts.IsNamedArgument(newArgument)) + { + newArgument = AddNameToArgument(newArgument, parameters[indexInListOfPreexistingArguments].Name); } + + fullList.Add(newArgument); + indexInListOfPreexistingArguments++; } } } + } - if (!paramsHandled) + if (!paramsHandled) + { + // Add the rest of existing parameters, e.g. from the params argument. + while (indexInListOfPreexistingArguments < newArguments.Count) { - // Add the rest of existing parameters, e.g. from the params argument. - while (indexInListOfPreexistingArguments < newArguments.Count) + if (indexInListOfPreexistingArguments < newArguments.SeparatorCount) { - if (indexInListOfPreexistingArguments < newArguments.SeparatorCount) - { - separators.Add(newArguments.GetSeparator(indexInListOfPreexistingArguments)); - } - - fullList.Add(newArguments[indexInListOfPreexistingArguments++]); + separators.Add(newArguments.GetSeparator(indexInListOfPreexistingArguments)); } - } - if (fullList.Count == separators.Count && separators.Count != 0) - { - separators.Remove(separators.Last()); + fullList.Add(newArguments[indexInListOfPreexistingArguments++]); } + } - return Generator.SeparatedList(fullList.ToImmutableAndFree(), separators.ToImmutableAndFree()); + if (fullList.Count == separators.Count && separators.Count != 0) + { + separators.Remove(separators.Last()); } - private async Task GenerateInferredCallsiteExpressionAsync( - Document document, - int position, - AddedParameter addedParameter, - CancellationToken cancellationToken) + return Generator.SeparatedList(fullList.ToImmutableAndFree(), separators.ToImmutableAndFree()); + } + + private async Task GenerateInferredCallsiteExpressionAsync( + Document document, + int position, + AddedParameter addedParameter, + CancellationToken cancellationToken) + { + if (addedParameter.CallSiteKind != CallSiteKind.Inferred || !addedParameter.TypeBinds) { - if (addedParameter.CallSiteKind != CallSiteKind.Inferred || !addedParameter.TypeBinds) - { - return null; - } + return null; + } - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var recommender = document.GetRequiredLanguageService(); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var recommender = document.GetRequiredLanguageService(); - var recommendationOptions = new RecommendationServiceOptions() - { - HideAdvancedMembers = false, - FilterOutOfScopeLocals = true, - }; + var recommendationOptions = new RecommendationServiceOptions() + { + HideAdvancedMembers = false, + FilterOutOfScopeLocals = true, + }; - var context = document.GetRequiredLanguageService().CreateContext(document, semanticModel, position, cancellationToken); - var recommendations = recommender.GetRecommendedSymbolsInContext(context, recommendationOptions, cancellationToken).NamedSymbols; + var context = document.GetRequiredLanguageService().CreateContext(document, semanticModel, position, cancellationToken); + var recommendations = recommender.GetRecommendedSymbolsInContext(context, recommendationOptions, cancellationToken).NamedSymbols; - var sourceSymbols = recommendations.Where(r => r.IsNonImplicitAndFromSource()); + var sourceSymbols = recommendations.Where(r => r.IsNonImplicitAndFromSource()); - // For locals, prefer the one with the closest declaration. Because we used the Recommender, - // we do not have to worry about filtering out inaccessible locals. - // TODO: Support range variables here as well: https://github.com/dotnet/roslyn/issues/44689 - var orderedLocalAndParameterSymbols = sourceSymbols - .Where(s => s.IsKind(SymbolKind.Local) || s.IsKind(SymbolKind.Parameter)) - .OrderByDescending(s => s.Locations.First().SourceSpan.Start); + // For locals, prefer the one with the closest declaration. Because we used the Recommender, + // we do not have to worry about filtering out inaccessible locals. + // TODO: Support range variables here as well: https://github.com/dotnet/roslyn/issues/44689 + var orderedLocalAndParameterSymbols = sourceSymbols + .Where(s => s.IsKind(SymbolKind.Local) || s.IsKind(SymbolKind.Parameter)) + .OrderByDescending(s => s.Locations.First().SourceSpan.Start); - // No particular ordering preference for properties/fields. - var orderedPropertiesAndFields = sourceSymbols - .Where(s => s.IsKind(SymbolKind.Property) || s.IsKind(SymbolKind.Field)); + // No particular ordering preference for properties/fields. + var orderedPropertiesAndFields = sourceSymbols + .Where(s => s.IsKind(SymbolKind.Property) || s.IsKind(SymbolKind.Field)); - var fullyOrderedSymbols = orderedLocalAndParameterSymbols.Concat(orderedPropertiesAndFields); + var fullyOrderedSymbols = orderedLocalAndParameterSymbols.Concat(orderedPropertiesAndFields); - foreach (var symbol in fullyOrderedSymbols) + foreach (var symbol in fullyOrderedSymbols) + { + var symbolType = symbol.GetSymbolType(); + if (symbolType == null) { - var symbolType = symbol.GetSymbolType(); - if (symbolType == null) - { - continue; - } - - if (semanticModel.Compilation.ClassifyCommonConversion(symbolType, addedParameter.Type).IsImplicit) - { - return Generator.IdentifierName(symbol.Name); - } + continue; } - return null; + if (semanticModel.Compilation.ClassifyCommonConversion(symbolType, addedParameter.Type).IsImplicit) + { + return Generator.IdentifierName(symbol.Name); + } } - protected ImmutableArray GetPermutedDocCommentTrivia(SyntaxNode node, ImmutableArray permutedParamNodes, LanguageServices services, LineFormattingOptions options) + return null; + } + + protected ImmutableArray GetPermutedDocCommentTrivia(SyntaxNode node, ImmutableArray permutedParamNodes, LanguageServices services, LineFormattingOptions options) + { + var updatedLeadingTrivia = ImmutableArray.CreateBuilder(); + var index = 0; + + var syntaxFacts = services.GetRequiredService(); + + foreach (var trivia in node.GetLeadingTrivia()) { - var updatedLeadingTrivia = ImmutableArray.CreateBuilder(); - var index = 0; + if (!trivia.HasStructure) + { + updatedLeadingTrivia.Add(trivia); + continue; + } - var syntaxFacts = services.GetRequiredService(); + var structuredTrivia = trivia.GetStructure(); + if (!syntaxFacts.IsDocumentationComment(structuredTrivia)) + { + updatedLeadingTrivia.Add(trivia); + continue; + } - foreach (var trivia in node.GetLeadingTrivia()) + var updatedNodeList = ArrayBuilder.GetInstance(); + var structuredContent = syntaxFacts.GetContentFromDocumentationCommentTriviaSyntax(trivia); + for (var i = 0; i < structuredContent.Count; i++) { - if (!trivia.HasStructure) + var content = structuredContent[i]; + if (!syntaxFacts.IsParameterNameXmlElementSyntax(content)) { - updatedLeadingTrivia.Add(trivia); + updatedNodeList.Add(content); continue; } - var structuredTrivia = trivia.GetStructure(); - if (!syntaxFacts.IsDocumentationComment(structuredTrivia)) + // Found a param tag, so insert the next one from the reordered list + if (index < permutedParamNodes.Length) { - updatedLeadingTrivia.Add(trivia); - continue; + updatedNodeList.Add(permutedParamNodes[index].WithLeadingTrivia(content.GetLeadingTrivia()).WithTrailingTrivia(content.GetTrailingTrivia())); + index++; } - - var updatedNodeList = ArrayBuilder.GetInstance(); - var structuredContent = syntaxFacts.GetContentFromDocumentationCommentTriviaSyntax(trivia); - for (var i = 0; i < structuredContent.Count; i++) + else { - var content = structuredContent[i]; - if (!syntaxFacts.IsParameterNameXmlElementSyntax(content)) - { - updatedNodeList.Add(content); - continue; - } - - // Found a param tag, so insert the next one from the reordered list - if (index < permutedParamNodes.Length) - { - updatedNodeList.Add(permutedParamNodes[index].WithLeadingTrivia(content.GetLeadingTrivia()).WithTrailingTrivia(content.GetTrailingTrivia())); - index++; - } - else - { - // Inspecting a param element that we are deleting but not replacing. - } + // Inspecting a param element that we are deleting but not replacing. } - - var newDocComments = Generator.DocumentationCommentTriviaWithUpdatedContent(trivia, updatedNodeList.ToImmutableAndFree()); - newDocComments = newDocComments.WithLeadingTrivia(structuredTrivia.GetLeadingTrivia()).WithTrailingTrivia(structuredTrivia.GetTrailingTrivia()); - var newTrivia = Generator.Trivia(newDocComments); - updatedLeadingTrivia.Add(newTrivia); - } - - var extraNodeList = ArrayBuilder.GetInstance(); - while (index < permutedParamNodes.Length) - { - extraNodeList.Add(permutedParamNodes[index]); - index++; } - if (extraNodeList.Any()) - { - var extraDocComments = Generator.DocumentationCommentTrivia( - extraNodeList, - node.GetTrailingTrivia(), - options.NewLine); - var newTrivia = Generator.Trivia(extraDocComments); + var newDocComments = Generator.DocumentationCommentTriviaWithUpdatedContent(trivia, updatedNodeList.ToImmutableAndFree()); + newDocComments = newDocComments.WithLeadingTrivia(structuredTrivia.GetLeadingTrivia()).WithTrailingTrivia(structuredTrivia.GetTrailingTrivia()); + var newTrivia = Generator.Trivia(newDocComments); + updatedLeadingTrivia.Add(newTrivia); + } - updatedLeadingTrivia.Add(newTrivia); - } + var extraNodeList = ArrayBuilder.GetInstance(); + while (index < permutedParamNodes.Length) + { + extraNodeList.Add(permutedParamNodes[index]); + index++; + } - extraNodeList.Free(); + if (extraNodeList.Any()) + { + var extraDocComments = Generator.DocumentationCommentTrivia( + extraNodeList, + node.GetTrailingTrivia(), + options.NewLine); + var newTrivia = Generator.Trivia(extraDocComments); - return updatedLeadingTrivia.ToImmutable(); + updatedLeadingTrivia.Add(newTrivia); } - protected static bool IsParamsArrayExpandedHelper(ISymbol symbol, int argumentCount, bool lastArgumentIsNamed, SemanticModel semanticModel, SyntaxNode lastArgumentExpression, CancellationToken cancellationToken) + extraNodeList.Free(); + + return updatedLeadingTrivia.ToImmutable(); + } + + protected static bool IsParamsArrayExpandedHelper(ISymbol symbol, int argumentCount, bool lastArgumentIsNamed, SemanticModel semanticModel, SyntaxNode lastArgumentExpression, CancellationToken cancellationToken) + { + if (symbol is IMethodSymbol methodSymbol && methodSymbol.Parameters.LastOrDefault()?.IsParams == true) { - if (symbol is IMethodSymbol methodSymbol && methodSymbol.Parameters.LastOrDefault()?.IsParams == true) + if (argumentCount > methodSymbol.Parameters.Length) + { + return true; + } + + if (argumentCount == methodSymbol.Parameters.Length) { - if (argumentCount > methodSymbol.Parameters.Length) + if (lastArgumentIsNamed) { - return true; + // If the last argument is named, then it cannot be part of an expanded params array. + return false; } - - if (argumentCount == methodSymbol.Parameters.Length) + else { - if (lastArgumentIsNamed) - { - // If the last argument is named, then it cannot be part of an expanded params array. - return false; - } - else - { - var fromType = semanticModel.GetTypeInfo(lastArgumentExpression, cancellationToken); - var toType = methodSymbol.Parameters.Last().Type; - return !semanticModel.Compilation.HasImplicitConversion(fromType.Type, toType); - } + var fromType = semanticModel.GetTypeInfo(lastArgumentExpression, cancellationToken); + var toType = methodSymbol.Parameters.Last().Type; + return !semanticModel.Compilation.HasImplicitConversion(fromType.Type, toType); } } - - return false; } - protected static int GetParameterIndexFromInvocationArgument(SyntaxNode argument, Document document, SemanticModel semanticModel, CancellationToken cancellationToken) - { - var semanticFacts = document.GetRequiredLanguageService(); - var parameter = semanticFacts.FindParameterForArgument(semanticModel, argument, cancellationToken); - if (parameter is null) - return 0; - - // If we're in the invocation of an extension method that is called via this.Method(params). The 'this' - // argument has an ordinal value of -1 but change signature is expecting all params to start at 0 (including - // the 'this' param). - return parameter.ContainingSymbol.IsReducedExtension() - ? parameter.Ordinal + 1 - : parameter.Ordinal; - } + return false; + } + + protected static int GetParameterIndexFromInvocationArgument(SyntaxNode argument, Document document, SemanticModel semanticModel, CancellationToken cancellationToken) + { + var semanticFacts = document.GetRequiredLanguageService(); + var parameter = semanticFacts.FindParameterForArgument(semanticModel, argument, cancellationToken); + if (parameter is null) + return 0; + + // If we're in the invocation of an extension method that is called via this.Method(params). The 'this' + // argument has an ordinal value of -1 but change signature is expecting all params to start at 0 (including + // the 'this' param). + return parameter.ContainingSymbol.IsReducedExtension() + ? parameter.Ordinal + 1 + : parameter.Ordinal; } } diff --git a/src/Features/Core/Portable/ChangeSignature/CallSiteKind.cs b/src/Features/Core/Portable/ChangeSignature/CallSiteKind.cs index 5740886a63e07..3ac890bc42548 100644 --- a/src/Features/Core/Portable/ChangeSignature/CallSiteKind.cs +++ b/src/Features/Core/Portable/ChangeSignature/CallSiteKind.cs @@ -2,42 +2,41 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +internal enum CallSiteKind { - internal enum CallSiteKind - { - /// - /// Use an explicit value to populate call sites, without forcing - /// the addition of a named argument. - /// - Value, + /// + /// Use an explicit value to populate call sites, without forcing + /// the addition of a named argument. + /// + Value, - /// - /// Use an explicit value to populate call sites, and convert - /// arguments to named arguments even if not required. Often - /// useful for literal callsite values like "true" or "null". - /// - ValueWithName, + /// + /// Use an explicit value to populate call sites, and convert + /// arguments to named arguments even if not required. Often + /// useful for literal callsite values like "true" or "null". + /// + ValueWithName, - /// - /// Indicates whether a "TODO" should be introduced at callsites - /// to cause errors that the user can then go visit and fix up. - /// - Todo, + /// + /// Indicates whether a "TODO" should be introduced at callsites + /// to cause errors that the user can then go visit and fix up. + /// + Todo, - /// - /// When an optional parameter is added, passing an argument for - /// it is not required. This indicates that the corresponding argument - /// should be omitted. This often results in subsequent arguments needing - /// to become named arguments - /// - Omitted, + /// + /// When an optional parameter is added, passing an argument for + /// it is not required. This indicates that the corresponding argument + /// should be omitted. This often results in subsequent arguments needing + /// to become named arguments + /// + Omitted, - /// - /// Populate each call site with an available variable of a matching types. - /// If no matching variable is found, this falls back to the - /// behavior. - /// - Inferred - } + /// + /// Populate each call site with an available variable of a matching types. + /// If no matching variable is found, this falls back to the + /// behavior. + /// + Inferred } diff --git a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureAnalyzedContext.cs b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureAnalyzedContext.cs index 4538b88151270..bfdc33e7cc6b8 100644 --- a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureAnalyzedContext.cs +++ b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureAnalyzedContext.cs @@ -4,26 +4,25 @@ using Microsoft.CodeAnalysis.CodeCleanup; -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +internal abstract class ChangeSignatureAnalyzedContext { - internal abstract class ChangeSignatureAnalyzedContext - { - } +} - internal sealed class ChangeSignatureAnalysisSucceededContext( - Document document, int positionForTypeBinding, ISymbol symbol, ParameterConfiguration parameterConfiguration, CodeCleanupOptionsProvider fallbackOptions) : ChangeSignatureAnalyzedContext - { - public readonly Document Document = document; - public readonly ISymbol Symbol = symbol; - public readonly ParameterConfiguration ParameterConfiguration = parameterConfiguration; - public readonly int PositionForTypeBinding = positionForTypeBinding; - public readonly CodeCleanupOptionsProvider FallbackOptions = fallbackOptions; +internal sealed class ChangeSignatureAnalysisSucceededContext( + Document document, int positionForTypeBinding, ISymbol symbol, ParameterConfiguration parameterConfiguration, CodeCleanupOptionsProvider fallbackOptions) : ChangeSignatureAnalyzedContext +{ + public readonly Document Document = document; + public readonly ISymbol Symbol = symbol; + public readonly ParameterConfiguration ParameterConfiguration = parameterConfiguration; + public readonly int PositionForTypeBinding = positionForTypeBinding; + public readonly CodeCleanupOptionsProvider FallbackOptions = fallbackOptions; - public Solution Solution => Document.Project.Solution; - } + public Solution Solution => Document.Project.Solution; +} - internal sealed class CannotChangeSignatureAnalyzedContext(ChangeSignatureFailureKind reason) : ChangeSignatureAnalyzedContext - { - public readonly ChangeSignatureFailureKind CannotChangeSignatureReason = reason; - } +internal sealed class CannotChangeSignatureAnalyzedContext(ChangeSignatureFailureKind reason) : ChangeSignatureAnalyzedContext +{ + public readonly ChangeSignatureFailureKind CannotChangeSignatureReason = reason; } diff --git a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureCodeAction.cs b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureCodeAction.cs index d9d384773b9ac..d6f7f10fac8c1 100644 --- a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureCodeAction.cs +++ b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureCodeAction.cs @@ -12,38 +12,37 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +internal class ChangeSignatureCodeAction(AbstractChangeSignatureService changeSignatureService, ChangeSignatureAnalysisSucceededContext context) : CodeActionWithOptions { - internal class ChangeSignatureCodeAction(AbstractChangeSignatureService changeSignatureService, ChangeSignatureAnalysisSucceededContext context) : CodeActionWithOptions - { - private readonly AbstractChangeSignatureService _changeSignatureService = changeSignatureService; - private readonly ChangeSignatureAnalysisSucceededContext _context = context; + private readonly AbstractChangeSignatureService _changeSignatureService = changeSignatureService; + private readonly ChangeSignatureAnalysisSucceededContext _context = context; - /// - /// This code action currently pops up a confirmation dialog to the user. As such, it does more than make - /// document changes (and is thus restricted in which hosts it can run). - /// - public override ImmutableArray Tags => RequiresNonDocumentChangeTags; + /// + /// This code action currently pops up a confirmation dialog to the user. As such, it does more than make + /// document changes (and is thus restricted in which hosts it can run). + /// + public override ImmutableArray Tags => RequiresNonDocumentChangeTags; - public override string Title => FeaturesResources.Change_signature; + public override string Title => FeaturesResources.Change_signature; - public override object? GetOptions(CancellationToken cancellationToken) - => AbstractChangeSignatureService.GetChangeSignatureOptions(_context); + public override object? GetOptions(CancellationToken cancellationToken) + => AbstractChangeSignatureService.GetChangeSignatureOptions(_context); - protected override async Task> ComputeOperationsAsync( - object options, IProgress progressTracker, CancellationToken cancellationToken) + protected override async Task> ComputeOperationsAsync( + object options, IProgress progressTracker, CancellationToken cancellationToken) + { + if (options is ChangeSignatureOptionsResult changeSignatureOptions && changeSignatureOptions != null) { - if (options is ChangeSignatureOptionsResult changeSignatureOptions && changeSignatureOptions != null) - { - var changeSignatureResult = await _changeSignatureService.ChangeSignatureWithContextAsync(_context, changeSignatureOptions, cancellationToken).ConfigureAwait(false); + var changeSignatureResult = await _changeSignatureService.ChangeSignatureWithContextAsync(_context, changeSignatureOptions, cancellationToken).ConfigureAwait(false); - if (changeSignatureResult.Succeeded) - { - return SpecializedCollections.SingletonEnumerable(new ChangeSignatureCodeActionOperation(changeSignatureResult.UpdatedSolution, changeSignatureResult.ConfirmationMessage)); - } + if (changeSignatureResult.Succeeded) + { + return SpecializedCollections.SingletonEnumerable(new ChangeSignatureCodeActionOperation(changeSignatureResult.UpdatedSolution, changeSignatureResult.ConfirmationMessage)); } - - return SpecializedCollections.EmptyEnumerable(); } + + return SpecializedCollections.EmptyEnumerable(); } } diff --git a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureCodeActionOperation.cs b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureCodeActionOperation.cs index ae1f7e3030cd0..796ba8d194469 100644 --- a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureCodeActionOperation.cs +++ b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureCodeActionOperation.cs @@ -10,42 +10,41 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +/// +/// Defines the for the +/// This is used instead of as we need to show a confirmation +/// dialog to the user before applying the change. +/// +internal sealed class ChangeSignatureCodeActionOperation(Solution changedSolution, string? confirmationMessage) : CodeActionOperation { - /// - /// Defines the for the - /// This is used instead of as we need to show a confirmation - /// dialog to the user before applying the change. - /// - internal sealed class ChangeSignatureCodeActionOperation(Solution changedSolution, string? confirmationMessage) : CodeActionOperation - { - public Solution ChangedSolution { get; } = changedSolution ?? throw new ArgumentNullException(nameof(changedSolution)); + public Solution ChangedSolution { get; } = changedSolution ?? throw new ArgumentNullException(nameof(changedSolution)); - public string? ConfirmationMessage { get; } = confirmationMessage; + public string? ConfirmationMessage { get; } = confirmationMessage; - internal override bool ApplyDuringTests => true; + internal override bool ApplyDuringTests => true; - /// - /// Show the confirmation message, if available, before attempting to apply the changes. - /// - internal sealed override Task TryApplyAsync( - Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) - { - return ApplyWorker(workspace, originalSolution, progressTracker, cancellationToken) ? SpecializedTasks.True : SpecializedTasks.False; - } + /// + /// Show the confirmation message, if available, before attempting to apply the changes. + /// + internal sealed override Task TryApplyAsync( + Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) + { + return ApplyWorker(workspace, originalSolution, progressTracker, cancellationToken) ? SpecializedTasks.True : SpecializedTasks.False; + } - private bool ApplyWorker(Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) + private bool ApplyWorker(Workspace workspace, Solution originalSolution, IProgress progressTracker, CancellationToken cancellationToken) + { + if (ConfirmationMessage != null) { - if (ConfirmationMessage != null) + var notificationService = workspace.Services.GetRequiredService(); + if (!notificationService.ConfirmMessageBox(ConfirmationMessage, severity: NotificationSeverity.Warning)) { - var notificationService = workspace.Services.GetRequiredService(); - if (!notificationService.ConfirmMessageBox(ConfirmationMessage, severity: NotificationSeverity.Warning)) - { - return false; - } + return false; } - - return ApplyChangesOperation.ApplyOrMergeChanges(workspace, originalSolution, ChangedSolution, progressTracker, cancellationToken); } + + return ApplyChangesOperation.ApplyOrMergeChanges(workspace, originalSolution, ChangedSolution, progressTracker, cancellationToken); } } diff --git a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureFailureKind.cs b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureFailureKind.cs index f077a4d4fc107..afe07a39bb5fd 100644 --- a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureFailureKind.cs +++ b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureFailureKind.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +internal enum ChangeSignatureFailureKind { - internal enum ChangeSignatureFailureKind - { - None, - DefinedInMetadata, - IncorrectKind, - } + None, + DefinedInMetadata, + IncorrectKind, } diff --git a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureOptionsResult.cs b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureOptionsResult.cs index 4fe3dfb74c894..f09876bcafc20 100644 --- a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureOptionsResult.cs +++ b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureOptionsResult.cs @@ -2,14 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +/// +/// A value of null indicates that the operation has been cancelled. +/// +internal sealed class ChangeSignatureOptionsResult(SignatureChange updatedSignature, bool previewChanges) { - /// - /// A value of null indicates that the operation has been cancelled. - /// - internal sealed class ChangeSignatureOptionsResult(SignatureChange updatedSignature, bool previewChanges) - { - public readonly bool PreviewChanges = previewChanges; - public readonly SignatureChange UpdatedSignature = updatedSignature; - } + public readonly bool PreviewChanges = previewChanges; + public readonly SignatureChange UpdatedSignature = updatedSignature; } diff --git a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureResult.cs b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureResult.cs index 82a939c27d4b4..9d1cc08509470 100644 --- a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureResult.cs +++ b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureResult.cs @@ -4,28 +4,27 @@ using System.Diagnostics.CodeAnalysis; -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +internal sealed class ChangeSignatureResult( + bool succeeded, + Solution? updatedSolution = null, + string? name = null, + Glyph? glyph = null, + bool previewChanges = false, + ChangeSignatureFailureKind? changeSignatureFailureKind = null, + string? confirmationMessage = null) { - internal sealed class ChangeSignatureResult( - bool succeeded, - Solution? updatedSolution = null, - string? name = null, - Glyph? glyph = null, - bool previewChanges = false, - ChangeSignatureFailureKind? changeSignatureFailureKind = null, - string? confirmationMessage = null) - { - [MemberNotNullWhen(true, nameof(UpdatedSolution))] - public bool Succeeded { get; } = succeeded; - public Solution? UpdatedSolution { get; } = updatedSolution; - public Glyph? Glyph { get; } = glyph; - public bool PreviewChanges { get; } = previewChanges; - public ChangeSignatureFailureKind? ChangeSignatureFailureKind { get; } = changeSignatureFailureKind; - public string? ConfirmationMessage { get; } = confirmationMessage; + [MemberNotNullWhen(true, nameof(UpdatedSolution))] + public bool Succeeded { get; } = succeeded; + public Solution? UpdatedSolution { get; } = updatedSolution; + public Glyph? Glyph { get; } = glyph; + public bool PreviewChanges { get; } = previewChanges; + public ChangeSignatureFailureKind? ChangeSignatureFailureKind { get; } = changeSignatureFailureKind; + public string? ConfirmationMessage { get; } = confirmationMessage; - /// - /// Name of the symbol. Needed here for the Preview Changes dialog. - /// - public string? Name { get; } = name; - } + /// + /// Name of the symbol. Needed here for the Preview Changes dialog. + /// + public string? Name { get; } = name; } diff --git a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureTelemetryLogger.cs b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureTelemetryLogger.cs index a0b4dd7dbe864..7a2eb58160c84 100644 --- a/src/Features/Core/Portable/ChangeSignature/ChangeSignatureTelemetryLogger.cs +++ b/src/Features/Core/Portable/ChangeSignature/ChangeSignatureTelemetryLogger.cs @@ -7,199 +7,198 @@ using System; using Microsoft.CodeAnalysis.Internal.Log; -namespace Microsoft.CodeAnalysis.ChangeSignature -{ - internal class ChangeSignatureLogger - { - private const string Maximum = nameof(Maximum); - private const string Minimum = nameof(Minimum); - private const string Mean = nameof(Mean); +namespace Microsoft.CodeAnalysis.ChangeSignature; - private static readonly CountLogAggregator s_countLogAggregator = new(); - private static readonly StatisticLogAggregator s_statisticLogAggregator = new(); - private static readonly HistogramLogAggregator s_histogramLogAggregator = new(bucketSize: 1000, maxBucketValue: 30000); +internal class ChangeSignatureLogger +{ + private const string Maximum = nameof(Maximum); + private const string Minimum = nameof(Minimum); + private const string Mean = nameof(Mean); - internal enum ActionInfo - { - // Calculate % of successful dialog launches - ChangeSignatureDialogLaunched, - ChangeSignatureDialogCommitted, - ChangeSignatureCommitCompleted, - - // Calculate % of successful dialog launches - AddParameterDialogLaunched, - AddParameterDialogCommitted, - - // Which transformations were done - CommittedSessionAddedRemovedReordered, - CommittedSessionAddedRemovedOnly, - CommittedSessionAddedReorderedOnly, - CommittedSessionRemovedReorderedOnly, - CommittedSessionAddedOnly, - CommittedSessionRemovedOnly, - CommittedSessionReorderedOnly, - - // Signature change specification details - CommittedSession_OriginalParameterCount, - CommittedSessionWithRemoved_NumberRemoved, - CommittedSessionWithAdded_NumberAdded, - - // Signature change commit information - CommittedSessionNumberOfDeclarationsUpdated, - CommittedSessionNumberOfCallSitesUpdated, - CommittedSessionCommitElapsedMS, - - // Added parameter binds or doesn't bind - AddedParameterTypeBinds, - - // Added parameter required or optional w/default - AddedParameterRequired, - - // Added parameter callsite value options - AddedParameterValueExplicit, - AddedParameterValueExplicitNamed, - AddedParameterValueTODO, - AddedParameterValueOmitted - } + private static readonly CountLogAggregator s_countLogAggregator = new(); + private static readonly StatisticLogAggregator s_statisticLogAggregator = new(); + private static readonly HistogramLogAggregator s_histogramLogAggregator = new(bucketSize: 1000, maxBucketValue: 30000); - internal static void LogChangeSignatureDialogLaunched() - => s_countLogAggregator.IncreaseCount(ActionInfo.ChangeSignatureDialogLaunched); + internal enum ActionInfo + { + // Calculate % of successful dialog launches + ChangeSignatureDialogLaunched, + ChangeSignatureDialogCommitted, + ChangeSignatureCommitCompleted, + + // Calculate % of successful dialog launches + AddParameterDialogLaunched, + AddParameterDialogCommitted, + + // Which transformations were done + CommittedSessionAddedRemovedReordered, + CommittedSessionAddedRemovedOnly, + CommittedSessionAddedReorderedOnly, + CommittedSessionRemovedReorderedOnly, + CommittedSessionAddedOnly, + CommittedSessionRemovedOnly, + CommittedSessionReorderedOnly, + + // Signature change specification details + CommittedSession_OriginalParameterCount, + CommittedSessionWithRemoved_NumberRemoved, + CommittedSessionWithAdded_NumberAdded, + + // Signature change commit information + CommittedSessionNumberOfDeclarationsUpdated, + CommittedSessionNumberOfCallSitesUpdated, + CommittedSessionCommitElapsedMS, + + // Added parameter binds or doesn't bind + AddedParameterTypeBinds, + + // Added parameter required or optional w/default + AddedParameterRequired, + + // Added parameter callsite value options + AddedParameterValueExplicit, + AddedParameterValueExplicitNamed, + AddedParameterValueTODO, + AddedParameterValueOmitted + } - internal static void LogChangeSignatureDialogCommitted() - => s_countLogAggregator.IncreaseCount(ActionInfo.ChangeSignatureDialogCommitted); + internal static void LogChangeSignatureDialogLaunched() + => s_countLogAggregator.IncreaseCount(ActionInfo.ChangeSignatureDialogLaunched); - internal static void LogAddParameterDialogLaunched() - => s_countLogAggregator.IncreaseCount(ActionInfo.AddParameterDialogLaunched); + internal static void LogChangeSignatureDialogCommitted() + => s_countLogAggregator.IncreaseCount(ActionInfo.ChangeSignatureDialogCommitted); - internal static void LogAddParameterDialogCommitted() - => s_countLogAggregator.IncreaseCount(ActionInfo.AddParameterDialogCommitted); + internal static void LogAddParameterDialogLaunched() + => s_countLogAggregator.IncreaseCount(ActionInfo.AddParameterDialogLaunched); - internal static void LogTransformationInformation(int numOriginalParameters, int numParametersAdded, int numParametersRemoved, bool anyParametersReordered) - { - LogTransformationCombination(numParametersAdded > 0, numParametersRemoved > 0, anyParametersReordered); + internal static void LogAddParameterDialogCommitted() + => s_countLogAggregator.IncreaseCount(ActionInfo.AddParameterDialogCommitted); - s_countLogAggregator.IncreaseCountBy(ActionInfo.CommittedSession_OriginalParameterCount, numOriginalParameters); + internal static void LogTransformationInformation(int numOriginalParameters, int numParametersAdded, int numParametersRemoved, bool anyParametersReordered) + { + LogTransformationCombination(numParametersAdded > 0, numParametersRemoved > 0, anyParametersReordered); - if (numParametersAdded > 0) - { - s_countLogAggregator.IncreaseCountBy(ActionInfo.CommittedSessionWithAdded_NumberAdded, numParametersAdded); - } + s_countLogAggregator.IncreaseCountBy(ActionInfo.CommittedSession_OriginalParameterCount, numOriginalParameters); - if (numParametersRemoved > 0) - { - s_countLogAggregator.IncreaseCountBy(ActionInfo.CommittedSessionWithRemoved_NumberRemoved, numParametersRemoved); - } + if (numParametersAdded > 0) + { + s_countLogAggregator.IncreaseCountBy(ActionInfo.CommittedSessionWithAdded_NumberAdded, numParametersAdded); } - private static void LogTransformationCombination(bool parametersAdded, bool parametersRemoved, bool parametersReordered) + if (numParametersRemoved > 0) { - // All three transformations - if (parametersAdded && parametersRemoved && parametersReordered) - { - s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionAddedRemovedReordered); - return; - } - - // Two transformations - if (parametersAdded && parametersRemoved) - { - s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionAddedRemovedOnly); - return; - } - - if (parametersAdded && parametersReordered) - { - s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionAddedReorderedOnly); - return; - } - - if (parametersRemoved && parametersReordered) - { - s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionRemovedReorderedOnly); - return; - } - - // One transformation - if (parametersAdded) - { - s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionAddedOnly); - return; - } - - if (parametersRemoved) - { - s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionRemovedOnly); - return; - } - - if (parametersReordered) - { - s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionReorderedOnly); - return; - } + s_countLogAggregator.IncreaseCountBy(ActionInfo.CommittedSessionWithRemoved_NumberRemoved, numParametersRemoved); } + } - internal static void LogCommitInformation(int numDeclarationsUpdated, int numCallSitesUpdated, TimeSpan elapsedTime) + private static void LogTransformationCombination(bool parametersAdded, bool parametersRemoved, bool parametersReordered) + { + // All three transformations + if (parametersAdded && parametersRemoved && parametersReordered) { - s_countLogAggregator.IncreaseCount(ActionInfo.ChangeSignatureCommitCompleted); - - s_countLogAggregator.IncreaseCountBy(ActionInfo.CommittedSessionNumberOfDeclarationsUpdated, numDeclarationsUpdated); - s_countLogAggregator.IncreaseCountBy(ActionInfo.CommittedSessionNumberOfCallSitesUpdated, numCallSitesUpdated); - - s_statisticLogAggregator.AddDataPoint(ActionInfo.CommittedSessionCommitElapsedMS, (int)elapsedTime.TotalMilliseconds); - s_histogramLogAggregator.LogTime(ActionInfo.CommittedSessionCommitElapsedMS, elapsedTime); + s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionAddedRemovedReordered); + return; } - internal static void LogAddedParameterTypeBinds() + // Two transformations + if (parametersAdded && parametersRemoved) { - s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterTypeBinds); + s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionAddedRemovedOnly); + return; } - internal static void LogAddedParameterRequired() + if (parametersAdded && parametersReordered) { - s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterRequired); + s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionAddedReorderedOnly); + return; } - internal static void LogAddedParameter_ValueExplicit() + if (parametersRemoved && parametersReordered) { - s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterValueExplicit); + s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionRemovedReorderedOnly); + return; } - internal static void LogAddedParameter_ValueExplicitNamed() + // One transformation + if (parametersAdded) { - s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterValueExplicitNamed); + s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionAddedOnly); + return; } - internal static void LogAddedParameter_ValueTODO() + if (parametersRemoved) { - s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterValueTODO); + s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionRemovedOnly); + return; } - internal static void LogAddedParameter_ValueOmitted() + if (parametersReordered) { - s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterValueOmitted); + s_countLogAggregator.IncreaseCount(ActionInfo.CommittedSessionReorderedOnly); + return; } + } + + internal static void LogCommitInformation(int numDeclarationsUpdated, int numCallSitesUpdated, TimeSpan elapsedTime) + { + s_countLogAggregator.IncreaseCount(ActionInfo.ChangeSignatureCommitCompleted); + + s_countLogAggregator.IncreaseCountBy(ActionInfo.CommittedSessionNumberOfDeclarationsUpdated, numDeclarationsUpdated); + s_countLogAggregator.IncreaseCountBy(ActionInfo.CommittedSessionNumberOfCallSitesUpdated, numCallSitesUpdated); + + s_statisticLogAggregator.AddDataPoint(ActionInfo.CommittedSessionCommitElapsedMS, (int)elapsedTime.TotalMilliseconds); + s_histogramLogAggregator.LogTime(ActionInfo.CommittedSessionCommitElapsedMS, elapsedTime); + } + + internal static void LogAddedParameterTypeBinds() + { + s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterTypeBinds); + } + + internal static void LogAddedParameterRequired() + { + s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterRequired); + } - internal static void ReportTelemetry() + internal static void LogAddedParameter_ValueExplicit() + { + s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterValueExplicit); + } + + internal static void LogAddedParameter_ValueExplicitNamed() + { + s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterValueExplicitNamed); + } + + internal static void LogAddedParameter_ValueTODO() + { + s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterValueTODO); + } + + internal static void LogAddedParameter_ValueOmitted() + { + s_countLogAggregator.IncreaseCount(ActionInfo.AddedParameterValueOmitted); + } + + internal static void ReportTelemetry() + { + Logger.Log(FunctionId.ChangeSignature_Data, KeyValueLogMessage.Create(m => { - Logger.Log(FunctionId.ChangeSignature_Data, KeyValueLogMessage.Create(m => + foreach (var kv in s_countLogAggregator) { - foreach (var kv in s_countLogAggregator) - { - m[kv.Key.ToString()] = kv.Value.GetCount(); - } - - foreach (var kv in s_statisticLogAggregator) - { - var statistics = kv.Value.GetStatisticResult(); - statistics.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); - } - - foreach (var kv in s_histogramLogAggregator) - { - kv.Value.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); - } - })); - } + m[kv.Key.ToString()] = kv.Value.GetCount(); + } + + foreach (var kv in s_statisticLogAggregator) + { + var statistics = kv.Value.GetStatisticResult(); + statistics.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); + } + + foreach (var kv in s_histogramLogAggregator) + { + kv.Value.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); + } + })); } } diff --git a/src/Features/Core/Portable/ChangeSignature/DelegateInvokeMethodReferenceFinder.cs b/src/Features/Core/Portable/ChangeSignature/DelegateInvokeMethodReferenceFinder.cs index 8260327e07d14..da3355a3344b3 100644 --- a/src/Features/Core/Portable/ChangeSignature/DelegateInvokeMethodReferenceFinder.cs +++ b/src/Features/Core/Portable/ChangeSignature/DelegateInvokeMethodReferenceFinder.cs @@ -14,106 +14,105 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +/// +/// For ChangeSignature, FAR on a delegate invoke method must cascade to BeginInvoke, +/// cascade through method group conversions, and discover implicit invocations that do not +/// mention the string "Invoke" or the delegate type itself. This implementation finds these +/// symbols by binding most identifiers and invocation expressions in the solution. +/// +/// +/// TODO: Rewrite this to track backward through references instead of binding everything +/// +internal class DelegateInvokeMethodReferenceFinder : AbstractReferenceFinder { - /// - /// For ChangeSignature, FAR on a delegate invoke method must cascade to BeginInvoke, - /// cascade through method group conversions, and discover implicit invocations that do not - /// mention the string "Invoke" or the delegate type itself. This implementation finds these - /// symbols by binding most identifiers and invocation expressions in the solution. - /// - /// - /// TODO: Rewrite this to track backward through references instead of binding everything - /// - internal class DelegateInvokeMethodReferenceFinder : AbstractReferenceFinder - { - public static readonly IReferenceFinder DelegateInvokeMethod = new DelegateInvokeMethodReferenceFinder(); + public static readonly IReferenceFinder DelegateInvokeMethod = new DelegateInvokeMethodReferenceFinder(); - protected override bool CanFind(IMethodSymbol symbol) - => symbol.MethodKind == MethodKind.DelegateInvoke; + protected override bool CanFind(IMethodSymbol symbol) + => symbol.MethodKind == MethodKind.DelegateInvoke; - protected override async ValueTask> DetermineCascadedSymbolsAsync( - IMethodSymbol symbol, - Solution solution, - FindReferencesSearchOptions options, - CancellationToken cancellationToken) - { - using var _ = ArrayBuilder.GetInstance(out var result); + protected override async ValueTask> DetermineCascadedSymbolsAsync( + IMethodSymbol symbol, + Solution solution, + FindReferencesSearchOptions options, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var result); - var beginInvoke = symbol.ContainingType.GetMembers(WellKnownMemberNames.DelegateBeginInvokeName).FirstOrDefault(); - if (beginInvoke != null) - result.Add(beginInvoke); + var beginInvoke = symbol.ContainingType.GetMembers(WellKnownMemberNames.DelegateBeginInvokeName).FirstOrDefault(); + if (beginInvoke != null) + result.Add(beginInvoke); - // All method group references - foreach (var project in solution.Projects) + // All method group references + foreach (var project in solution.Projects) + { + foreach (var document in project.Documents) { - foreach (var document in project.Documents) - { - var changeSignatureService = document.GetRequiredLanguageService(); - var cascaded = await changeSignatureService.DetermineCascadedSymbolsFromDelegateInvokeAsync( - symbol, document, cancellationToken).ConfigureAwait(false); - result.AddRange(cascaded); - } + var changeSignatureService = document.GetRequiredLanguageService(); + var cascaded = await changeSignatureService.DetermineCascadedSymbolsFromDelegateInvokeAsync( + symbol, document, cancellationToken).ConfigureAwait(false); + result.AddRange(cascaded); } - - return result.ToImmutable(); } - protected override Task> DetermineDocumentsToSearchAsync( - IMethodSymbol symbol, - HashSet? globalAliases, - Project project, - IImmutableSet? documents, - FindReferencesSearchOptions options, - CancellationToken cancellationToken) - { - return Task.FromResult(project.Documents.ToImmutableArray()); - } + return result.ToImmutable(); + } - protected override async ValueTask> FindReferencesInDocumentAsync( - IMethodSymbol methodSymbol, - FindReferencesDocumentState state, - FindReferencesSearchOptions options, - CancellationToken cancellationToken) - { - // FAR on the Delegate type and use those results to find Invoke calls + protected override Task> DetermineDocumentsToSearchAsync( + IMethodSymbol symbol, + HashSet? globalAliases, + Project project, + IImmutableSet? documents, + FindReferencesSearchOptions options, + CancellationToken cancellationToken) + { + return Task.FromResult(project.Documents.ToImmutableArray()); + } + + protected override async ValueTask> FindReferencesInDocumentAsync( + IMethodSymbol methodSymbol, + FindReferencesDocumentState state, + FindReferencesSearchOptions options, + CancellationToken cancellationToken) + { + // FAR on the Delegate type and use those results to find Invoke calls + + var syntaxFacts = state.SyntaxFacts; - var syntaxFacts = state.SyntaxFacts; + var root = state.Root; + var nodes = root.DescendantNodes(); - var root = state.Root; - var nodes = root.DescendantNodes(); + using var _ = ArrayBuilder.GetInstance(out var convertedAnonymousFunctions); + foreach (var node in nodes) + { + if (!syntaxFacts.IsAnonymousFunctionExpression(node)) + continue; - using var _ = ArrayBuilder.GetInstance(out var convertedAnonymousFunctions); - foreach (var node in nodes) + var convertedType = (ISymbol?)state.SemanticModel.GetTypeInfo(node, cancellationToken).ConvertedType; + if (convertedType != null) { - if (!syntaxFacts.IsAnonymousFunctionExpression(node)) - continue; - - var convertedType = (ISymbol?)state.SemanticModel.GetTypeInfo(node, cancellationToken).ConvertedType; - if (convertedType != null) - { - convertedType = await SymbolFinder.FindSourceDefinitionAsync(convertedType, state.Solution, cancellationToken).ConfigureAwait(false) - ?? convertedType; - } - - if (convertedType == methodSymbol.ContainingType) - convertedAnonymousFunctions.Add(node); + convertedType = await SymbolFinder.FindSourceDefinitionAsync(convertedType, state.Solution, cancellationToken).ConfigureAwait(false) + ?? convertedType; } - var invocations = nodes.Where(syntaxFacts.IsInvocationExpression) - .Where(e => state.SemanticModel.GetSymbolInfo(e, cancellationToken).Symbol?.OriginalDefinition == methodSymbol); - - return invocations.Concat(convertedAnonymousFunctions).SelectAsArray( - node => new FinderLocation( - node, - new ReferenceLocation( - state.Document, - alias: null, - node.GetLocation(), - isImplicit: false, - GetSymbolUsageInfo(node, state, cancellationToken), - GetAdditionalFindUsagesProperties(node, state), - CandidateReason.None))); + if (convertedType == methodSymbol.ContainingType) + convertedAnonymousFunctions.Add(node); } + + var invocations = nodes.Where(syntaxFacts.IsInvocationExpression) + .Where(e => state.SemanticModel.GetSymbolInfo(e, cancellationToken).Symbol?.OriginalDefinition == methodSymbol); + + return invocations.Concat(convertedAnonymousFunctions).SelectAsArray( + node => new FinderLocation( + node, + new ReferenceLocation( + state.Document, + alias: null, + node.GetLocation(), + isImplicit: false, + GetSymbolUsageInfo(node, state, cancellationToken), + GetAdditionalFindUsagesProperties(node, state), + CandidateReason.None))); } } diff --git a/src/Features/Core/Portable/ChangeSignature/IChangeSignatureOptionsService.cs b/src/Features/Core/Portable/ChangeSignature/IChangeSignatureOptionsService.cs index 3f170acb1daa6..c175c04fbf1fd 100644 --- a/src/Features/Core/Portable/ChangeSignature/IChangeSignatureOptionsService.cs +++ b/src/Features/Core/Portable/ChangeSignature/IChangeSignatureOptionsService.cs @@ -4,24 +4,23 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +internal interface IChangeSignatureOptionsService : IWorkspaceService { - internal interface IChangeSignatureOptionsService : IWorkspaceService - { - /// - /// Gets options and produces a if successful. - /// - /// the context document - /// the position in the document with - /// the signature of the method, used for binding types (e.g. for added - /// parameters) - /// the symbol for changing the signature - /// existing parameters of the symbol - /// - ChangeSignatureOptionsResult? GetChangeSignatureOptions( - Document document, - int positionForTypeBinding, - ISymbol symbol, - ParameterConfiguration parameters); - } + /// + /// Gets options and produces a if successful. + /// + /// the context document + /// the position in the document with + /// the signature of the method, used for binding types (e.g. for added + /// parameters) + /// the symbol for changing the signature + /// existing parameters of the symbol + /// + ChangeSignatureOptionsResult? GetChangeSignatureOptions( + Document document, + int positionForTypeBinding, + ISymbol symbol, + ParameterConfiguration parameters); } diff --git a/src/Features/Core/Portable/ChangeSignature/IUnifiedArgumentSyntax.cs b/src/Features/Core/Portable/ChangeSignature/IUnifiedArgumentSyntax.cs index 6ddd7a94c7784..98c9cde47b328 100644 --- a/src/Features/Core/Portable/ChangeSignature/IUnifiedArgumentSyntax.cs +++ b/src/Features/Core/Portable/ChangeSignature/IUnifiedArgumentSyntax.cs @@ -4,14 +4,13 @@ #nullable disable -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +internal interface IUnifiedArgumentSyntax { - internal interface IUnifiedArgumentSyntax - { - bool IsDefault { get; } - bool IsNamed { get; } - string GetName(); - IUnifiedArgumentSyntax WithName(string name); - IUnifiedArgumentSyntax WithAdditionalAnnotations(SyntaxAnnotation annotation); - } + bool IsDefault { get; } + bool IsNamed { get; } + string GetName(); + IUnifiedArgumentSyntax WithName(string name); + IUnifiedArgumentSyntax WithAdditionalAnnotations(SyntaxAnnotation annotation); } diff --git a/src/Features/Core/Portable/ChangeSignature/Parameter.cs b/src/Features/Core/Portable/ChangeSignature/Parameter.cs index 648f7533a36db..05356fa3d7917 100644 --- a/src/Features/Core/Portable/ChangeSignature/Parameter.cs +++ b/src/Features/Core/Portable/ChangeSignature/Parameter.cs @@ -4,95 +4,94 @@ using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +/// +/// Base type for Parameter information, whether the parameter +/// is preexisting or new. +/// +internal abstract class Parameter { - /// - /// Base type for Parameter information, whether the parameter - /// is preexisting or new. - /// - internal abstract class Parameter - { - public abstract bool HasDefaultValue { get; } - public abstract string Name { get; } - } + public abstract bool HasDefaultValue { get; } + public abstract string Name { get; } +} - internal sealed class ExistingParameter(IParameterSymbol param) : Parameter - { - public IParameterSymbol Symbol { get; } = param; +internal sealed class ExistingParameter(IParameterSymbol param) : Parameter +{ + public IParameterSymbol Symbol { get; } = param; - public override bool HasDefaultValue => Symbol.HasExplicitDefaultValue; - public override string Name => Symbol.Name; - } + public override bool HasDefaultValue => Symbol.HasExplicitDefaultValue; + public override string Name => Symbol.Name; +} - internal sealed class AddedParameter : Parameter +internal sealed class AddedParameter : Parameter +{ + public AddedParameter( + ITypeSymbol type, + string typeName, + string name, + CallSiteKind callSiteKind, + string callSiteValue = "", + bool isRequired = true, + string defaultValue = "", + bool typeBinds = true) { - public AddedParameter( - ITypeSymbol type, - string typeName, - string name, - CallSiteKind callSiteKind, - string callSiteValue = "", - bool isRequired = true, - string defaultValue = "", - bool typeBinds = true) - { - Type = type; - TypeBinds = typeBinds; - TypeName = typeName; - Name = name; - CallSiteValue = callSiteValue; + Type = type; + TypeBinds = typeBinds; + TypeName = typeName; + Name = name; + CallSiteValue = callSiteValue; - IsRequired = isRequired; - DefaultValue = defaultValue; - CallSiteKind = callSiteKind; + IsRequired = isRequired; + DefaultValue = defaultValue; + CallSiteKind = callSiteKind; - // Populate the call site text for the UI - switch (CallSiteKind) - { - case CallSiteKind.Value: - case CallSiteKind.ValueWithName: - CallSiteValue = callSiteValue; - break; - case CallSiteKind.Todo: - CallSiteValue = FeaturesResources.ChangeSignature_NewParameterIntroduceTODOVariable; - break; - case CallSiteKind.Omitted: - CallSiteValue = FeaturesResources.ChangeSignature_NewParameterOmitValue; - break; - case CallSiteKind.Inferred: - CallSiteValue = FeaturesResources.ChangeSignature_NewParameterInferValue; - break; - default: - throw ExceptionUtilities.Unreachable(); - } + // Populate the call site text for the UI + switch (CallSiteKind) + { + case CallSiteKind.Value: + case CallSiteKind.ValueWithName: + CallSiteValue = callSiteValue; + break; + case CallSiteKind.Todo: + CallSiteValue = FeaturesResources.ChangeSignature_NewParameterIntroduceTODOVariable; + break; + case CallSiteKind.Omitted: + CallSiteValue = FeaturesResources.ChangeSignature_NewParameterOmitValue; + break; + case CallSiteKind.Inferred: + CallSiteValue = FeaturesResources.ChangeSignature_NewParameterInferValue; + break; + default: + throw ExceptionUtilities.Unreachable(); } + } - public override string Name { get; } - public override bool HasDefaultValue => !string.IsNullOrWhiteSpace(DefaultValue); + public override string Name { get; } + public override bool HasDefaultValue => !string.IsNullOrWhiteSpace(DefaultValue); - public ITypeSymbol Type { get; } - public string TypeName { get; } - public bool TypeBinds { get; } + public ITypeSymbol Type { get; } + public string TypeName { get; } + public bool TypeBinds { get; } - public CallSiteKind CallSiteKind { get; } + public CallSiteKind CallSiteKind { get; } - /// - /// Display string for the Call Site column in the Change Signature dialog. - /// - public string CallSiteValue { get; } + /// + /// Display string for the Call Site column in the Change Signature dialog. + /// + public string CallSiteValue { get; } - /// - /// True if required, false if optional with a default value. - /// - public bool IsRequired { get; } + /// + /// True if required, false if optional with a default value. + /// + public bool IsRequired { get; } - /// - /// Value to use in the declaration of an optional parameter. - /// E.g. the "3" in M(int x = 3); - /// - public string DefaultValue { get; } + /// + /// Value to use in the declaration of an optional parameter. + /// E.g. the "3" in M(int x = 3); + /// + public string DefaultValue { get; } - // For test purposes: to display assert failure details in tests. - public override string ToString() => $"{Type.ToDisplayString(new SymbolDisplayFormat(genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters))} {Name} ({CallSiteValue})"; - } + // For test purposes: to display assert failure details in tests. + public override string ToString() => $"{Type.ToDisplayString(new SymbolDisplayFormat(genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters))} {Name} ({CallSiteValue})"; } diff --git a/src/Features/Core/Portable/ChangeSignature/ParameterConfiguration.cs b/src/Features/Core/Portable/ChangeSignature/ParameterConfiguration.cs index e394c69b25b3c..aee2d659ce644 100644 --- a/src/Features/Core/Portable/ChangeSignature/ParameterConfiguration.cs +++ b/src/Features/Core/Portable/ChangeSignature/ParameterConfiguration.cs @@ -6,79 +6,78 @@ using System.Linq; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +internal sealed class ParameterConfiguration( + ExistingParameter? thisParameter, + ImmutableArray parametersWithoutDefaultValues, + ImmutableArray remainingEditableParameters, + ExistingParameter? paramsParameter, + int selectedIndex) { - internal sealed class ParameterConfiguration( - ExistingParameter? thisParameter, - ImmutableArray parametersWithoutDefaultValues, - ImmutableArray remainingEditableParameters, - ExistingParameter? paramsParameter, - int selectedIndex) + public readonly ExistingParameter? ThisParameter = thisParameter; + public readonly ImmutableArray ParametersWithoutDefaultValues = parametersWithoutDefaultValues; + public readonly ImmutableArray RemainingEditableParameters = remainingEditableParameters; + public readonly ExistingParameter? ParamsParameter = paramsParameter; + public readonly int SelectedIndex = selectedIndex; + + public static ParameterConfiguration Create(ImmutableArray parameters, bool isExtensionMethod, int selectedIndex) { - public readonly ExistingParameter? ThisParameter = thisParameter; - public readonly ImmutableArray ParametersWithoutDefaultValues = parametersWithoutDefaultValues; - public readonly ImmutableArray RemainingEditableParameters = remainingEditableParameters; - public readonly ExistingParameter? ParamsParameter = paramsParameter; - public readonly int SelectedIndex = selectedIndex; + var parametersList = parameters.ToList(); + ExistingParameter? thisParameter = null; + var parametersWithoutDefaultValues = ArrayBuilder.GetInstance(); + var remainingReorderableParameters = ArrayBuilder.GetInstance(); + ExistingParameter? paramsParameter = null; - public static ParameterConfiguration Create(ImmutableArray parameters, bool isExtensionMethod, int selectedIndex) + if (parametersList.Count > 0 && isExtensionMethod) { - var parametersList = parameters.ToList(); - ExistingParameter? thisParameter = null; - var parametersWithoutDefaultValues = ArrayBuilder.GetInstance(); - var remainingReorderableParameters = ArrayBuilder.GetInstance(); - ExistingParameter? paramsParameter = null; - - if (parametersList.Count > 0 && isExtensionMethod) - { - // Extension method `this` parameters cannot be added, so must be pre-existing. - thisParameter = (ExistingParameter)parametersList[0]; - parametersList.RemoveAt(0); - } + // Extension method `this` parameters cannot be added, so must be pre-existing. + thisParameter = (ExistingParameter)parametersList[0]; + parametersList.RemoveAt(0); + } - if ((parametersList.LastOrDefault() as ExistingParameter)?.Symbol.IsParams == true) - { - // Params arrays cannot be added, so must be pre-existing. - paramsParameter = (ExistingParameter)parametersList[^1]; - parametersList.RemoveAt(parametersList.Count - 1); - } + if ((parametersList.LastOrDefault() as ExistingParameter)?.Symbol.IsParams == true) + { + // Params arrays cannot be added, so must be pre-existing. + paramsParameter = (ExistingParameter)parametersList[^1]; + parametersList.RemoveAt(parametersList.Count - 1); + } - var seenDefaultValues = false; - foreach (var param in parametersList) + var seenDefaultValues = false; + foreach (var param in parametersList) + { + if (param.HasDefaultValue) { - if (param.HasDefaultValue) - { - seenDefaultValues = true; - } - - (seenDefaultValues ? remainingReorderableParameters : parametersWithoutDefaultValues).Add(param); + seenDefaultValues = true; } - return new ParameterConfiguration(thisParameter, parametersWithoutDefaultValues.ToImmutableAndFree(), remainingReorderableParameters.ToImmutableAndFree(), paramsParameter, selectedIndex); + (seenDefaultValues ? remainingReorderableParameters : parametersWithoutDefaultValues).Add(param); } - internal ParameterConfiguration WithoutAddedParameters() - => Create(ToListOfParameters().OfType().ToImmutableArray(), ThisParameter != null, selectedIndex: 0); + return new ParameterConfiguration(thisParameter, parametersWithoutDefaultValues.ToImmutableAndFree(), remainingReorderableParameters.ToImmutableAndFree(), paramsParameter, selectedIndex); + } - public ImmutableArray ToListOfParameters() - { - var list = ArrayBuilder.GetInstance(); + internal ParameterConfiguration WithoutAddedParameters() + => Create(ToListOfParameters().OfType().ToImmutableArray(), ThisParameter != null, selectedIndex: 0); - if (ThisParameter != null) - { - list.Add(ThisParameter); - } + public ImmutableArray ToListOfParameters() + { + var list = ArrayBuilder.GetInstance(); - list.AddRange(ParametersWithoutDefaultValues); - list.AddRange(RemainingEditableParameters); + if (ThisParameter != null) + { + list.Add(ThisParameter); + } - if (ParamsParameter != null) - { - list.Add(ParamsParameter); - } + list.AddRange(ParametersWithoutDefaultValues); + list.AddRange(RemainingEditableParameters); - return list.ToImmutableAndFree(); + if (ParamsParameter != null) + { + list.Add(ParamsParameter); } + + return list.ToImmutableAndFree(); } } diff --git a/src/Features/Core/Portable/ChangeSignature/SignatureChange.cs b/src/Features/Core/Portable/ChangeSignature/SignatureChange.cs index 989b992a988b1..eb80c7a42a672 100644 --- a/src/Features/Core/Portable/ChangeSignature/SignatureChange.cs +++ b/src/Features/Core/Portable/ChangeSignature/SignatureChange.cs @@ -9,113 +9,112 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ChangeSignature +namespace Microsoft.CodeAnalysis.ChangeSignature; + +internal sealed class SignatureChange { - internal sealed class SignatureChange - { - public readonly ParameterConfiguration OriginalConfiguration; - public readonly ParameterConfiguration UpdatedConfiguration; + public readonly ParameterConfiguration OriginalConfiguration; + public readonly ParameterConfiguration UpdatedConfiguration; - private readonly Dictionary _originalIndexToUpdatedIndexMap = []; + private readonly Dictionary _originalIndexToUpdatedIndexMap = []; - public SignatureChange(ParameterConfiguration originalConfiguration, ParameterConfiguration updatedConfiguration) - { - OriginalConfiguration = originalConfiguration; - UpdatedConfiguration = updatedConfiguration; + public SignatureChange(ParameterConfiguration originalConfiguration, ParameterConfiguration updatedConfiguration) + { + OriginalConfiguration = originalConfiguration; + UpdatedConfiguration = updatedConfiguration; - // TODO: Could be better than O(n^2) - var originalParameterList = originalConfiguration.ToListOfParameters(); - var updatedParameterList = updatedConfiguration.ToListOfParameters(); + // TODO: Could be better than O(n^2) + var originalParameterList = originalConfiguration.ToListOfParameters(); + var updatedParameterList = updatedConfiguration.ToListOfParameters(); - for (var i = 0; i < originalParameterList.Length; i++) + for (var i = 0; i < originalParameterList.Length; i++) + { + int? index = null; + var parameter = originalParameterList[i]; + if (parameter is ExistingParameter existingParameter) { - int? index = null; - var parameter = originalParameterList[i]; - if (parameter is ExistingParameter existingParameter) + var updatedIndex = updatedParameterList.IndexOf(p => p is ExistingParameter ep && ep.Symbol.Equals(existingParameter.Symbol)); + if (updatedIndex >= 0) { - var updatedIndex = updatedParameterList.IndexOf(p => p is ExistingParameter ep && ep.Symbol.Equals(existingParameter.Symbol)); - if (updatedIndex >= 0) - { - index = updatedIndex; - } + index = updatedIndex; } - - _originalIndexToUpdatedIndexMap.Add(i, index); } + + _originalIndexToUpdatedIndexMap.Add(i, index); } + } - public int? GetUpdatedIndex(int parameterIndex) + public int? GetUpdatedIndex(int parameterIndex) + { + if (parameterIndex >= OriginalConfiguration.ToListOfParameters().Length) { - if (parameterIndex >= OriginalConfiguration.ToListOfParameters().Length) - { - return null; - } - - return _originalIndexToUpdatedIndexMap[parameterIndex]; + return null; } - internal SignatureChange WithoutAddedParameters() - => new(OriginalConfiguration, UpdatedConfiguration.WithoutAddedParameters()); + return _originalIndexToUpdatedIndexMap[parameterIndex]; + } - internal void LogTelemetry() - { - var originalListOfParameters = OriginalConfiguration.ToListOfParameters(); - var updatedListOfParameters = UpdatedConfiguration.ToListOfParameters(); + internal SignatureChange WithoutAddedParameters() + => new(OriginalConfiguration, UpdatedConfiguration.WithoutAddedParameters()); + + internal void LogTelemetry() + { + var originalListOfParameters = OriginalConfiguration.ToListOfParameters(); + var updatedListOfParameters = UpdatedConfiguration.ToListOfParameters(); - ChangeSignatureLogger.LogTransformationInformation( - numOriginalParameters: originalListOfParameters.Length, - numParametersAdded: updatedListOfParameters.Count(p => p is AddedParameter), - numParametersRemoved: originalListOfParameters.Count(p => !updatedListOfParameters.Contains(p)), - anyParametersReordered: AnyParametersReordered(originalListOfParameters, updatedListOfParameters)); + ChangeSignatureLogger.LogTransformationInformation( + numOriginalParameters: originalListOfParameters.Length, + numParametersAdded: updatedListOfParameters.Count(p => p is AddedParameter), + numParametersRemoved: originalListOfParameters.Count(p => !updatedListOfParameters.Contains(p)), + anyParametersReordered: AnyParametersReordered(originalListOfParameters, updatedListOfParameters)); - foreach (var addedParameter in updatedListOfParameters.OfType()) + foreach (var addedParameter in updatedListOfParameters.OfType()) + { + if (addedParameter.IsRequired) { - if (addedParameter.IsRequired) - { - ChangeSignatureLogger.LogAddedParameterRequired(); - } + ChangeSignatureLogger.LogAddedParameterRequired(); + } - if (addedParameter.TypeBinds) - { - ChangeSignatureLogger.LogAddedParameterTypeBinds(); - } + if (addedParameter.TypeBinds) + { + ChangeSignatureLogger.LogAddedParameterTypeBinds(); + } - if (addedParameter.CallSiteKind == CallSiteKind.Todo) - { - ChangeSignatureLogger.LogAddedParameter_ValueTODO(); - } - else if (addedParameter.CallSiteKind == CallSiteKind.Omitted) + if (addedParameter.CallSiteKind == CallSiteKind.Todo) + { + ChangeSignatureLogger.LogAddedParameter_ValueTODO(); + } + else if (addedParameter.CallSiteKind == CallSiteKind.Omitted) + { + ChangeSignatureLogger.LogAddedParameter_ValueOmitted(); + } + else + { + if (addedParameter.CallSiteKind == CallSiteKind.ValueWithName) { - ChangeSignatureLogger.LogAddedParameter_ValueOmitted(); + ChangeSignatureLogger.LogAddedParameter_ValueExplicitNamed(); } else { - if (addedParameter.CallSiteKind == CallSiteKind.ValueWithName) - { - ChangeSignatureLogger.LogAddedParameter_ValueExplicitNamed(); - } - else - { - ChangeSignatureLogger.LogAddedParameter_ValueExplicit(); - } + ChangeSignatureLogger.LogAddedParameter_ValueExplicit(); } } } + } - private static bool AnyParametersReordered(ImmutableArray originalListOfParameters, ImmutableArray updatedListOfParameters) - { - var originalListWithoutRemovedOrAdded = originalListOfParameters.Where(updatedListOfParameters.Contains).ToImmutableArray(); - var updatedListWithoutRemovedOrAdded = updatedListOfParameters.Where(originalListOfParameters.Contains).ToImmutableArray(); + private static bool AnyParametersReordered(ImmutableArray originalListOfParameters, ImmutableArray updatedListOfParameters) + { + var originalListWithoutRemovedOrAdded = originalListOfParameters.Where(updatedListOfParameters.Contains).ToImmutableArray(); + var updatedListWithoutRemovedOrAdded = updatedListOfParameters.Where(originalListOfParameters.Contains).ToImmutableArray(); - for (var i = 0; i < originalListWithoutRemovedOrAdded.Length; i++) + for (var i = 0; i < originalListWithoutRemovedOrAdded.Length; i++) + { + if (originalListWithoutRemovedOrAdded[i] != updatedListWithoutRemovedOrAdded[i]) { - if (originalListWithoutRemovedOrAdded[i] != updatedListWithoutRemovedOrAdded[i]) - { - return true; - } + return true; } - - return false; } + + return false; } } diff --git a/src/Features/Core/Portable/ClassifiedSpansAndHighlightSpan.cs b/src/Features/Core/Portable/ClassifiedSpansAndHighlightSpan.cs index 87c89858b8716..ee5f8cfffa159 100644 --- a/src/Features/Core/Portable/ClassifiedSpansAndHighlightSpan.cs +++ b/src/Features/Core/Portable/ClassifiedSpansAndHighlightSpan.cs @@ -7,15 +7,14 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal readonly struct ClassifiedSpansAndHighlightSpan( + ImmutableArray classifiedSpans, + TextSpan highlightSpan) { - internal readonly struct ClassifiedSpansAndHighlightSpan( - ImmutableArray classifiedSpans, - TextSpan highlightSpan) - { - public const string Key = nameof(ClassifiedSpansAndHighlightSpan); + public const string Key = nameof(ClassifiedSpansAndHighlightSpan); - public readonly ImmutableArray ClassifiedSpans = classifiedSpans; - public readonly TextSpan HighlightSpan = highlightSpan; - } + public readonly ImmutableArray ClassifiedSpans = classifiedSpans; + public readonly TextSpan HighlightSpan = highlightSpan; } diff --git a/src/Features/Core/Portable/ClassifiedSpansAndHighlightSpanFactory.cs b/src/Features/Core/Portable/ClassifiedSpansAndHighlightSpanFactory.cs index 9e7e4d8da3e9e..e063b99c42960 100644 --- a/src/Features/Core/Portable/ClassifiedSpansAndHighlightSpanFactory.cs +++ b/src/Features/Core/Portable/ClassifiedSpansAndHighlightSpanFactory.cs @@ -11,79 +11,78 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal static class ClassifiedSpansAndHighlightSpanFactory { - internal static class ClassifiedSpansAndHighlightSpanFactory + public static async Task ClassifyAsync( + DocumentSpan documentSpan, ClassifiedSpansAndHighlightSpan? classifiedSpans, ClassificationOptions options, CancellationToken cancellationToken) { - public static async Task ClassifyAsync( - DocumentSpan documentSpan, ClassifiedSpansAndHighlightSpan? classifiedSpans, ClassificationOptions options, CancellationToken cancellationToken) - { - // If the document span is providing us with the classified spans up front, then we - // can just use that. Otherwise, go back and actually classify the text for the line - // the document span is on. - if (classifiedSpans != null) - return classifiedSpans.Value; + // If the document span is providing us with the classified spans up front, then we + // can just use that. Otherwise, go back and actually classify the text for the line + // the document span is on. + if (classifiedSpans != null) + return classifiedSpans.Value; - return await ClassifyAsync( - documentSpan.Document, documentSpan.SourceSpan, options, cancellationToken).ConfigureAwait(false); - } + return await ClassifyAsync( + documentSpan.Document, documentSpan.SourceSpan, options, cancellationToken).ConfigureAwait(false); + } - private static async Task ClassifyAsync( - Document document, TextSpan sourceSpan, ClassificationOptions options, CancellationToken cancellationToken) - { - var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + private static async Task ClassifyAsync( + Document document, TextSpan sourceSpan, ClassificationOptions options, CancellationToken cancellationToken) + { + var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var narrowSpan = sourceSpan; - var lineSpan = GetLineSpanForReference(sourceText, narrowSpan); + var narrowSpan = sourceSpan; + var lineSpan = GetLineSpanForReference(sourceText, narrowSpan); - var taggedLineParts = await GetTaggedTextForDocumentRegionAsync( - document, narrowSpan, lineSpan, options, cancellationToken).ConfigureAwait(false); - return taggedLineParts; - } + var taggedLineParts = await GetTaggedTextForDocumentRegionAsync( + document, narrowSpan, lineSpan, options, cancellationToken).ConfigureAwait(false); + return taggedLineParts; + } - private static TextSpan GetLineSpanForReference(SourceText sourceText, TextSpan referenceSpan) - { - var sourceLine = sourceText.Lines.GetLineFromPosition(referenceSpan.Start); - var firstNonWhitespacePosition = sourceLine.GetFirstNonWhitespacePosition().Value; + private static TextSpan GetLineSpanForReference(SourceText sourceText, TextSpan referenceSpan) + { + var sourceLine = sourceText.Lines.GetLineFromPosition(referenceSpan.Start); + var firstNonWhitespacePosition = sourceLine.GetFirstNonWhitespacePosition().Value; - // Get the span of the line from the first non-whitespace character to the end of it. Note: the reference - // span might actually start in the leading whitespace of the line (nothing prevents any of our - // languages/providers from doing that), so ensure that the line snap we clip out at least starts at that - // position so that our span math will be correct. - return TextSpan.FromBounds(Math.Min(firstNonWhitespacePosition, referenceSpan.Start), sourceLine.End); - } + // Get the span of the line from the first non-whitespace character to the end of it. Note: the reference + // span might actually start in the leading whitespace of the line (nothing prevents any of our + // languages/providers from doing that), so ensure that the line snap we clip out at least starts at that + // position so that our span math will be correct. + return TextSpan.FromBounds(Math.Min(firstNonWhitespacePosition, referenceSpan.Start), sourceLine.End); + } - private static async Task GetTaggedTextForDocumentRegionAsync( - Document document, TextSpan narrowSpan, TextSpan widenedSpan, ClassificationOptions options, CancellationToken cancellationToken) - { - var highlightSpan = new TextSpan( - start: narrowSpan.Start - widenedSpan.Start, - length: narrowSpan.Length); + private static async Task GetTaggedTextForDocumentRegionAsync( + Document document, TextSpan narrowSpan, TextSpan widenedSpan, ClassificationOptions options, CancellationToken cancellationToken) + { + var highlightSpan = new TextSpan( + start: narrowSpan.Start - widenedSpan.Start, + length: narrowSpan.Length); - var classifiedSpans = await GetClassifiedSpansAsync( - document, narrowSpan, widenedSpan, options, cancellationToken).ConfigureAwait(false); - return new ClassifiedSpansAndHighlightSpan(classifiedSpans, highlightSpan); - } + var classifiedSpans = await GetClassifiedSpansAsync( + document, narrowSpan, widenedSpan, options, cancellationToken).ConfigureAwait(false); + return new ClassifiedSpansAndHighlightSpan(classifiedSpans, highlightSpan); + } - private static async Task> GetClassifiedSpansAsync( - Document document, TextSpan narrowSpan, TextSpan widenedSpan, ClassificationOptions options, CancellationToken cancellationToken) - { - // We don't present things like static/assigned variables differently. So pass `includeAdditiveSpans: - // false` as we don't need that data. - var result = await ClassifierHelper.GetClassifiedSpansAsync( - document, widenedSpan, options, includeAdditiveSpans: false, cancellationToken).ConfigureAwait(false); - if (!result.IsDefault) - return result; + private static async Task> GetClassifiedSpansAsync( + Document document, TextSpan narrowSpan, TextSpan widenedSpan, ClassificationOptions options, CancellationToken cancellationToken) + { + // We don't present things like static/assigned variables differently. So pass `includeAdditiveSpans: + // false` as we don't need that data. + var result = await ClassifierHelper.GetClassifiedSpansAsync( + document, widenedSpan, options, includeAdditiveSpans: false, cancellationToken).ConfigureAwait(false); + if (!result.IsDefault) + return result; - // For languages that don't expose a classification service, we show the entire - // item as plain text. Break the text into three spans so that we can properly - // highlight the 'narrow-span' later on when we display the item. - return - [ - new ClassifiedSpan(ClassificationTypeNames.Text, TextSpan.FromBounds(widenedSpan.Start, narrowSpan.Start)), - new ClassifiedSpan(ClassificationTypeNames.Text, narrowSpan), - new ClassifiedSpan(ClassificationTypeNames.Text, TextSpan.FromBounds(narrowSpan.End, widenedSpan.End)), - ]; - } + // For languages that don't expose a classification service, we show the entire + // item as plain text. Break the text into three spans so that we can properly + // highlight the 'narrow-span' later on when we display the item. + return + [ + new ClassifiedSpan(ClassificationTypeNames.Text, TextSpan.FromBounds(widenedSpan.Start, narrowSpan.Start)), + new ClassifiedSpan(ClassificationTypeNames.Text, narrowSpan), + new ClassifiedSpan(ClassificationTypeNames.Text, TextSpan.FromBounds(narrowSpan.End, widenedSpan.End)), + ]; } } diff --git a/src/Features/Core/Portable/CodeFixes/AbstractConfigurationActionWithNestedActions.cs b/src/Features/Core/Portable/CodeFixes/AbstractConfigurationActionWithNestedActions.cs index d959f393d9585..3a14e861c05f5 100644 --- a/src/Features/Core/Portable/CodeFixes/AbstractConfigurationActionWithNestedActions.cs +++ b/src/Features/Core/Portable/CodeFixes/AbstractConfigurationActionWithNestedActions.cs @@ -5,28 +5,27 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.CodeActions; -namespace Microsoft.CodeAnalysis.CodeFixes +namespace Microsoft.CodeAnalysis.CodeFixes; + +/// +/// Represents a configuration code action with nested actions registered by individual s. +/// Note that the code fix/light bulb engine groups all such from different providers +/// into another top level suggested action to avoid light bulb clutter. This topmost suggested action is *not* represented by this code action. +/// +internal abstract class AbstractConfigurationActionWithNestedActions : CodeAction.CodeActionWithNestedActions { - /// - /// Represents a configuration code action with nested actions registered by individual s. - /// Note that the code fix/light bulb engine groups all such from different providers - /// into another top level suggested action to avoid light bulb clutter. This topmost suggested action is *not* represented by this code action. - /// - internal abstract class AbstractConfigurationActionWithNestedActions : CodeAction.CodeActionWithNestedActions + protected AbstractConfigurationActionWithNestedActions(ImmutableArray nestedActions, string title) + : base(title, nestedActions, isInlinable: false, + priority: CodeActionPriority.Lowest) // Put configurations/suppressions at the end of everything. { - protected AbstractConfigurationActionWithNestedActions(ImmutableArray nestedActions, string title) - : base(title, nestedActions, isInlinable: false, - priority: CodeActionPriority.Lowest) // Put configurations/suppressions at the end of everything. - { - } + } - /// - /// Additional priority associated with all configuration and suppression code actions. - /// This allows special code actions such as Bulk configuration to to be at the end of - /// all suppression and configuration actions by having a lower additional priority. - /// - internal virtual CodeActionPriority AdditionalPriority => CodeActionPriority.Default; + /// + /// Additional priority associated with all configuration and suppression code actions. + /// This allows special code actions such as Bulk configuration to to be at the end of + /// all suppression and configuration actions by having a lower additional priority. + /// + internal virtual CodeActionPriority AdditionalPriority => CodeActionPriority.Default; - internal virtual bool IsBulkConfigurationAction => false; - } + internal virtual bool IsBulkConfigurationAction => false; } diff --git a/src/Features/Core/Portable/CodeFixes/CodeFixCollection.cs b/src/Features/Core/Portable/CodeFixes/CodeFixCollection.cs index 816ed555b9ba0..f7b987bdfa94d 100644 --- a/src/Features/Core/Portable/CodeFixes/CodeFixCollection.cs +++ b/src/Features/Core/Portable/CodeFixes/CodeFixCollection.cs @@ -7,29 +7,28 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CodeFixes +namespace Microsoft.CodeAnalysis.CodeFixes; + +/// +/// Represents a collection of es supplied by a given fix provider +/// (such as or ). +/// +internal class CodeFixCollection( + object provider, + TextSpan span, + ImmutableArray fixes, + FixAllState fixAllState, + ImmutableArray supportedScopes, + Diagnostic firstDiagnostic) { + public object Provider { get; } = provider; + public TextSpan TextSpan { get; } = span; + public ImmutableArray Fixes { get; } = fixes.NullToEmpty(); + /// - /// Represents a collection of es supplied by a given fix provider - /// (such as or ). + /// Optional fix all context, which is non-null if the given supports fix all occurrences code fix. /// - internal class CodeFixCollection( - object provider, - TextSpan span, - ImmutableArray fixes, - FixAllState fixAllState, - ImmutableArray supportedScopes, - Diagnostic firstDiagnostic) - { - public object Provider { get; } = provider; - public TextSpan TextSpan { get; } = span; - public ImmutableArray Fixes { get; } = fixes.NullToEmpty(); - - /// - /// Optional fix all context, which is non-null if the given supports fix all occurrences code fix. - /// - public FixAllState FixAllState { get; } = fixAllState; - public ImmutableArray SupportedScopes { get; } = supportedScopes.NullToEmpty(); - public Diagnostic FirstDiagnostic { get; } = firstDiagnostic; - } + public FixAllState FixAllState { get; } = fixAllState; + public ImmutableArray SupportedScopes { get; } = supportedScopes.NullToEmpty(); + public Diagnostic FirstDiagnostic { get; } = firstDiagnostic; } diff --git a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs index 5771f4032a1e9..3422e33b99dc9 100644 --- a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs +++ b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs @@ -19,765 +19,764 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.Configuration +namespace Microsoft.CodeAnalysis.CodeFixes.Configuration; + +/// +/// Helper class to configure diagnostic severity or code style option value based on .editorconfig file +/// +internal sealed partial class ConfigurationUpdater { + private enum ConfigurationKind + { + OptionValue, + Severity, + BulkConfigure + } + + private const string DiagnosticOptionPrefix = "dotnet_diagnostic."; + private const string SeveritySuffix = ".severity"; + private const string BulkConfigureAllAnalyzerDiagnosticsOptionKey = "dotnet_analyzer_diagnostic.severity"; + private const string BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix = "dotnet_analyzer_diagnostic.category-"; + private const string AllAnalyzerDiagnosticsCategory = ""; + + // Regular expression for .editorconfig header. + // For example: "[*.cs] # Optional comment" + // "[*.{vb,cs}]" + // "[*] ; Optional comment" + // "[ConsoleApp/Program.cs]" + private static readonly Regex s_headerPattern = new(@"\[(\*|[^ #;\[\]]+\.({[^ #;{}\.\[\]]+}|[^ #;{}\.\[\]]+))\]\s*([#;].*)?"); + + // Regular expression for .editorconfig code style option entry. + // For example: + // 1. "dotnet_style_object_initializer = true # Optional comment" + // 2. "dotnet_style_object_initializer = true:suggestion ; Optional comment" + // 3. "dotnet_diagnostic.CA2000.severity = suggestion # Optional comment" + // 4. "dotnet_analyzer_diagnostic.category-Security.severity = suggestion # Optional comment" + // 5. "dotnet_analyzer_diagnostic.severity = suggestion # Optional comment" + // + // Regex groups: + // 1. Option key + // 2. Option value + // 3. Optional severity suffix in option value, i.e. ':severity' suffix + // 4. Optional comment suffix + private static readonly Regex s_optionEntryPattern = new($@"(.*)=([\w, ]*)(:[\w]+)?([ ]*[;#].*)?"); + + private readonly string? _optionNameOpt; + private readonly string? _newOptionValueOpt; + private readonly string _newSeverity; + private readonly ConfigurationKind _configurationKind; + private readonly Diagnostic? _diagnostic; + private readonly string? _categoryToBulkConfigure; + private readonly bool _isPerLanguage; + private readonly Project _project; + private readonly bool _addNewEntryIfNoExistingEntryFound; + private readonly string _language; + + private ConfigurationUpdater( + string? optionNameOpt, + string? newOptionValueOpt, + string newSeverity, + ConfigurationKind configurationKind, + Diagnostic? diagnosticToConfigure, + string? categoryToBulkConfigure, + bool isPerLanguage, + Project project, + bool addNewEntryIfNoExistingEntryFound) + { + Debug.Assert(configurationKind != ConfigurationKind.OptionValue || !string.IsNullOrEmpty(newOptionValueOpt)); + Debug.Assert(!string.IsNullOrEmpty(newSeverity)); + Debug.Assert(diagnosticToConfigure != null ^ categoryToBulkConfigure != null); + Debug.Assert((categoryToBulkConfigure != null) == (configurationKind == ConfigurationKind.BulkConfigure)); + + _optionNameOpt = optionNameOpt; + _newOptionValueOpt = newOptionValueOpt; + _newSeverity = newSeverity; + _configurationKind = configurationKind; + _diagnostic = diagnosticToConfigure; + _categoryToBulkConfigure = categoryToBulkConfigure; + _isPerLanguage = isPerLanguage; + _project = project; + _addNewEntryIfNoExistingEntryFound = addNewEntryIfNoExistingEntryFound; + _language = project.Language; + } + /// - /// Helper class to configure diagnostic severity or code style option value based on .editorconfig file + /// Updates or adds an .editorconfig to the given + /// so that the severity of the given is configured to be the given + /// . /// - internal sealed partial class ConfigurationUpdater + public static Task ConfigureSeverityAsync( + ReportDiagnostic severity, + Diagnostic diagnostic, + Project project, + CancellationToken cancellationToken) { - private enum ConfigurationKind + if (severity == ReportDiagnostic.Default) { - OptionValue, - Severity, - BulkConfigure + severity = diagnostic.DefaultSeverity.ToReportDiagnostic(); } - private const string DiagnosticOptionPrefix = "dotnet_diagnostic."; - private const string SeveritySuffix = ".severity"; - private const string BulkConfigureAllAnalyzerDiagnosticsOptionKey = "dotnet_analyzer_diagnostic.severity"; - private const string BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix = "dotnet_analyzer_diagnostic.category-"; - private const string AllAnalyzerDiagnosticsCategory = ""; - - // Regular expression for .editorconfig header. - // For example: "[*.cs] # Optional comment" - // "[*.{vb,cs}]" - // "[*] ; Optional comment" - // "[ConsoleApp/Program.cs]" - private static readonly Regex s_headerPattern = new(@"\[(\*|[^ #;\[\]]+\.({[^ #;{}\.\[\]]+}|[^ #;{}\.\[\]]+))\]\s*([#;].*)?"); - - // Regular expression for .editorconfig code style option entry. - // For example: - // 1. "dotnet_style_object_initializer = true # Optional comment" - // 2. "dotnet_style_object_initializer = true:suggestion ; Optional comment" - // 3. "dotnet_diagnostic.CA2000.severity = suggestion # Optional comment" - // 4. "dotnet_analyzer_diagnostic.category-Security.severity = suggestion # Optional comment" - // 5. "dotnet_analyzer_diagnostic.severity = suggestion # Optional comment" - // - // Regex groups: - // 1. Option key - // 2. Option value - // 3. Optional severity suffix in option value, i.e. ':severity' suffix - // 4. Optional comment suffix - private static readonly Regex s_optionEntryPattern = new($@"(.*)=([\w, ]*)(:[\w]+)?([ ]*[;#].*)?"); - - private readonly string? _optionNameOpt; - private readonly string? _newOptionValueOpt; - private readonly string _newSeverity; - private readonly ConfigurationKind _configurationKind; - private readonly Diagnostic? _diagnostic; - private readonly string? _categoryToBulkConfigure; - private readonly bool _isPerLanguage; - private readonly Project _project; - private readonly bool _addNewEntryIfNoExistingEntryFound; - private readonly string _language; - - private ConfigurationUpdater( - string? optionNameOpt, - string? newOptionValueOpt, - string newSeverity, - ConfigurationKind configurationKind, - Diagnostic? diagnosticToConfigure, - string? categoryToBulkConfigure, - bool isPerLanguage, - Project project, - bool addNewEntryIfNoExistingEntryFound) + return ConfigureSeverityAsync(severity.ToEditorConfigString(), diagnostic, project, cancellationToken); + } + + /// + /// Updates or adds an .editorconfig to the given + /// so that the severity of the given is configured to be the given + /// . + /// + public static Task ConfigureSeverityAsync( + string editorConfigSeverity, + Diagnostic diagnostic, + Project project, + CancellationToken cancellationToken) + { + // For option based code style diagnostic, try to find the .editorconfig key-value pair for the + // option setting. + var codeStyleOptionValues = GetCodeStyleOptionValuesForDiagnostic(diagnostic, project); + + ConfigurationUpdater updater; + if (!codeStyleOptionValues.IsEmpty) { - Debug.Assert(configurationKind != ConfigurationKind.OptionValue || !string.IsNullOrEmpty(newOptionValueOpt)); - Debug.Assert(!string.IsNullOrEmpty(newSeverity)); - Debug.Assert(diagnosticToConfigure != null ^ categoryToBulkConfigure != null); - Debug.Assert((categoryToBulkConfigure != null) == (configurationKind == ConfigurationKind.BulkConfigure)); - - _optionNameOpt = optionNameOpt; - _newOptionValueOpt = newOptionValueOpt; - _newSeverity = newSeverity; - _configurationKind = configurationKind; - _diagnostic = diagnosticToConfigure; - _categoryToBulkConfigure = categoryToBulkConfigure; - _isPerLanguage = isPerLanguage; - _project = project; - _addNewEntryIfNoExistingEntryFound = addNewEntryIfNoExistingEntryFound; - _language = project.Language; + return ConfigureCodeStyleOptionsAsync( + codeStyleOptionValues.Select(t => (t.optionName, t.currentOptionValue, t.isPerLanguage)), + editorConfigSeverity, diagnostic, project, configurationKind: ConfigurationKind.Severity, cancellationToken); } - - /// - /// Updates or adds an .editorconfig to the given - /// so that the severity of the given is configured to be the given - /// . - /// - public static Task ConfigureSeverityAsync( - ReportDiagnostic severity, - Diagnostic diagnostic, - Project project, - CancellationToken cancellationToken) + else { - if (severity == ReportDiagnostic.Default) - { - severity = diagnostic.DefaultSeverity.ToReportDiagnostic(); - } - - return ConfigureSeverityAsync(severity.ToEditorConfigString(), diagnostic, project, cancellationToken); + updater = new ConfigurationUpdater(optionNameOpt: null, newOptionValueOpt: null, editorConfigSeverity, + configurationKind: ConfigurationKind.Severity, diagnostic, categoryToBulkConfigure: null, + isPerLanguage: false, project, addNewEntryIfNoExistingEntryFound: true); + return updater.ConfigureAsync(cancellationToken); } + } - /// - /// Updates or adds an .editorconfig to the given - /// so that the severity of the given is configured to be the given - /// . - /// - public static Task ConfigureSeverityAsync( - string editorConfigSeverity, - Diagnostic diagnostic, - Project project, - CancellationToken cancellationToken) - { - // For option based code style diagnostic, try to find the .editorconfig key-value pair for the - // option setting. - var codeStyleOptionValues = GetCodeStyleOptionValuesForDiagnostic(diagnostic, project); + /// + /// Updates or adds an .editorconfig to the given + /// so that the default severity of the diagnostics with the given is configured to be the given + /// . + /// + public static Task BulkConfigureSeverityAsync( + string editorConfigSeverity, + string category, + Project project, + CancellationToken cancellationToken) + { + Contract.ThrowIfFalse(!string.IsNullOrEmpty(category)); + return BulkConfigureSeverityCoreAsync(editorConfigSeverity, category, project, cancellationToken); + } - ConfigurationUpdater updater; - if (!codeStyleOptionValues.IsEmpty) - { - return ConfigureCodeStyleOptionsAsync( - codeStyleOptionValues.Select(t => (t.optionName, t.currentOptionValue, t.isPerLanguage)), - editorConfigSeverity, diagnostic, project, configurationKind: ConfigurationKind.Severity, cancellationToken); - } - else - { - updater = new ConfigurationUpdater(optionNameOpt: null, newOptionValueOpt: null, editorConfigSeverity, - configurationKind: ConfigurationKind.Severity, diagnostic, categoryToBulkConfigure: null, - isPerLanguage: false, project, addNewEntryIfNoExistingEntryFound: true); - return updater.ConfigureAsync(cancellationToken); - } - } + /// + /// Updates or adds an .editorconfig to the given + /// so that the default severity of all diagnostics is configured to be the given + /// . + /// + public static Task BulkConfigureSeverityAsync( + string editorConfigSeverity, + Project project, + CancellationToken cancellationToken) + { + return BulkConfigureSeverityCoreAsync(editorConfigSeverity, category: AllAnalyzerDiagnosticsCategory, project, cancellationToken); + } - /// - /// Updates or adds an .editorconfig to the given - /// so that the default severity of the diagnostics with the given is configured to be the given - /// . - /// - public static Task BulkConfigureSeverityAsync( - string editorConfigSeverity, - string category, - Project project, - CancellationToken cancellationToken) - { - Contract.ThrowIfFalse(!string.IsNullOrEmpty(category)); - return BulkConfigureSeverityCoreAsync(editorConfigSeverity, category, project, cancellationToken); - } + private static Task BulkConfigureSeverityCoreAsync( + string editorConfigSeverity, + string category, + Project project, + CancellationToken cancellationToken) + { + Contract.ThrowIfNull(category); + var updater = new ConfigurationUpdater(optionNameOpt: null, newOptionValueOpt: null, editorConfigSeverity, + configurationKind: ConfigurationKind.BulkConfigure, diagnosticToConfigure: null, category, + isPerLanguage: false, project, addNewEntryIfNoExistingEntryFound: true); + return updater.ConfigureAsync(cancellationToken); + } - /// - /// Updates or adds an .editorconfig to the given - /// so that the default severity of all diagnostics is configured to be the given - /// . - /// - public static Task BulkConfigureSeverityAsync( - string editorConfigSeverity, - Project project, - CancellationToken cancellationToken) + /// + /// Updates or adds an .editorconfig to the given + /// so that the given is configured to have the given . + /// + public static Task ConfigureCodeStyleOptionAsync( + string optionName, + string optionValue, + Diagnostic diagnostic, + bool isPerLanguage, + Project project, + CancellationToken cancellationToken) + => ConfigureCodeStyleOptionsAsync( + SpecializedCollections.SingletonEnumerable((optionName, optionValue, isPerLanguage)), + diagnostic.Severity.ToEditorConfigString(), + diagnostic, project, configurationKind: ConfigurationKind.OptionValue, cancellationToken); + + private static async Task ConfigureCodeStyleOptionsAsync( + IEnumerable<(string optionName, string optionValue, bool isPerLanguage)> codeStyleOptionValues, + string editorConfigSeverity, + Diagnostic diagnostic, + Project project, + ConfigurationKind configurationKind, + CancellationToken cancellationToken) + { + Debug.Assert(!codeStyleOptionValues.IsEmpty()); + + // For severity configuration for IDE code style diagnostics, we want to ensure the following: + // 1. For code style option based entries, i.e. "%option_name% = %option_value%:%severity%, + // we only update existing entries, but do not add a new entry. + // 2. For "dotnet_diagnostic.<%DiagnosticId%>.severity = %severity%" entries, we update existing entries, and if none found + // we add a single new severity configuration entry for all code style options that share the same diagnostic ID. + // This behavior is required to ensure that we always add the up-to-date dotnet_diagnostic based severity entry + // so the IDE code style diagnostics can be enforced in build, as the compiler only understands dotnet_diagnostic entries. + // See https://github.com/dotnet/roslyn/issues/44201 for more details. + + // First handle "%option_name% = %option_value%:%severity% entries. + // For option value configuration, we always want to add new entry if no existing value is found. + // For severity configuration, we only want to update existing value if found. + var currentProject = project; + var areAllOptionsPerLanguage = true; + var addNewEntryIfNoExistingEntryFound = configurationKind != ConfigurationKind.Severity; + foreach (var (optionName, optionValue, isPerLanguage) in codeStyleOptionValues) { - return BulkConfigureSeverityCoreAsync(editorConfigSeverity, category: AllAnalyzerDiagnosticsCategory, project, cancellationToken); + Debug.Assert(!string.IsNullOrEmpty(optionName)); + Debug.Assert(optionValue != null); + + var updater = new ConfigurationUpdater(optionName, optionValue, editorConfigSeverity, configurationKind, + diagnostic, categoryToBulkConfigure: null, isPerLanguage, currentProject, + addNewEntryIfNoExistingEntryFound); + var solution = await updater.ConfigureAsync(cancellationToken).ConfigureAwait(false); + currentProject = solution.GetProject(project.Id)!; + areAllOptionsPerLanguage = areAllOptionsPerLanguage && isPerLanguage; } - private static Task BulkConfigureSeverityCoreAsync( - string editorConfigSeverity, - string category, - Project project, - CancellationToken cancellationToken) + // For severity configuration, handle "dotnet_diagnostic.<%DiagnosticId%>.severity = %severity%" entry. + // We want to update existing entry + add new entry if no existing value is found. + if (configurationKind == ConfigurationKind.Severity) { - Contract.ThrowIfNull(category); var updater = new ConfigurationUpdater(optionNameOpt: null, newOptionValueOpt: null, editorConfigSeverity, - configurationKind: ConfigurationKind.BulkConfigure, diagnosticToConfigure: null, category, - isPerLanguage: false, project, addNewEntryIfNoExistingEntryFound: true); - return updater.ConfigureAsync(cancellationToken); + configurationKind: ConfigurationKind.Severity, diagnostic, categoryToBulkConfigure: null, + isPerLanguage: areAllOptionsPerLanguage, currentProject, addNewEntryIfNoExistingEntryFound: true); + var solution = await updater.ConfigureAsync(cancellationToken).ConfigureAwait(false); + currentProject = solution.GetProject(project.Id)!; } - /// - /// Updates or adds an .editorconfig to the given - /// so that the given is configured to have the given . - /// - public static Task ConfigureCodeStyleOptionAsync( - string optionName, - string optionValue, - Diagnostic diagnostic, - bool isPerLanguage, - Project project, - CancellationToken cancellationToken) - => ConfigureCodeStyleOptionsAsync( - SpecializedCollections.SingletonEnumerable((optionName, optionValue, isPerLanguage)), - diagnostic.Severity.ToEditorConfigString(), - diagnostic, project, configurationKind: ConfigurationKind.OptionValue, cancellationToken); - - private static async Task ConfigureCodeStyleOptionsAsync( - IEnumerable<(string optionName, string optionValue, bool isPerLanguage)> codeStyleOptionValues, - string editorConfigSeverity, - Diagnostic diagnostic, - Project project, - ConfigurationKind configurationKind, - CancellationToken cancellationToken) - { - Debug.Assert(!codeStyleOptionValues.IsEmpty()); - - // For severity configuration for IDE code style diagnostics, we want to ensure the following: - // 1. For code style option based entries, i.e. "%option_name% = %option_value%:%severity%, - // we only update existing entries, but do not add a new entry. - // 2. For "dotnet_diagnostic.<%DiagnosticId%>.severity = %severity%" entries, we update existing entries, and if none found - // we add a single new severity configuration entry for all code style options that share the same diagnostic ID. - // This behavior is required to ensure that we always add the up-to-date dotnet_diagnostic based severity entry - // so the IDE code style diagnostics can be enforced in build, as the compiler only understands dotnet_diagnostic entries. - // See https://github.com/dotnet/roslyn/issues/44201 for more details. - - // First handle "%option_name% = %option_value%:%severity% entries. - // For option value configuration, we always want to add new entry if no existing value is found. - // For severity configuration, we only want to update existing value if found. - var currentProject = project; - var areAllOptionsPerLanguage = true; - var addNewEntryIfNoExistingEntryFound = configurationKind != ConfigurationKind.Severity; - foreach (var (optionName, optionValue, isPerLanguage) in codeStyleOptionValues) - { - Debug.Assert(!string.IsNullOrEmpty(optionName)); - Debug.Assert(optionValue != null); - - var updater = new ConfigurationUpdater(optionName, optionValue, editorConfigSeverity, configurationKind, - diagnostic, categoryToBulkConfigure: null, isPerLanguage, currentProject, - addNewEntryIfNoExistingEntryFound); - var solution = await updater.ConfigureAsync(cancellationToken).ConfigureAwait(false); - currentProject = solution.GetProject(project.Id)!; - areAllOptionsPerLanguage = areAllOptionsPerLanguage && isPerLanguage; - } - - // For severity configuration, handle "dotnet_diagnostic.<%DiagnosticId%>.severity = %severity%" entry. - // We want to update existing entry + add new entry if no existing value is found. - if (configurationKind == ConfigurationKind.Severity) - { - var updater = new ConfigurationUpdater(optionNameOpt: null, newOptionValueOpt: null, editorConfigSeverity, - configurationKind: ConfigurationKind.Severity, diagnostic, categoryToBulkConfigure: null, - isPerLanguage: areAllOptionsPerLanguage, currentProject, addNewEntryIfNoExistingEntryFound: true); - var solution = await updater.ConfigureAsync(cancellationToken).ConfigureAwait(false); - currentProject = solution.GetProject(project.Id)!; - } + return currentProject.Solution; + } - return currentProject.Solution; + private async Task ConfigureAsync(CancellationToken cancellationToken) + { + // Find existing .editorconfig or generate a new one if none exists. + var editorConfigDocument = FindOrGenerateEditorConfig(); + if (editorConfigDocument == null) + { + return _project.Solution; } - private async Task ConfigureAsync(CancellationToken cancellationToken) - { - // Find existing .editorconfig or generate a new one if none exists. - var editorConfigDocument = FindOrGenerateEditorConfig(); - if (editorConfigDocument == null) - { - return _project.Solution; - } + var solution = editorConfigDocument.Project.Solution; + var originalText = await editorConfigDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var solution = editorConfigDocument.Project.Solution; - var originalText = await editorConfigDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + // Compute the updated text for analyzer config document. + var newText = GetNewAnalyzerConfigDocumentText(originalText, editorConfigDocument); - // Compute the updated text for analyzer config document. - var newText = GetNewAnalyzerConfigDocumentText(originalText, editorConfigDocument); + if (newText == null || newText.Equals(originalText)) + { + return solution; + } - if (newText == null || newText.Equals(originalText)) - { - return solution; - } + return solution.WithAnalyzerConfigDocumentText(editorConfigDocument.Id, newText); + } - return solution.WithAnalyzerConfigDocumentText(editorConfigDocument.Id, newText); + private AnalyzerConfigDocument? FindOrGenerateEditorConfig() + { + var analyzerConfigPath = _diagnostic != null + ? _project.TryGetAnalyzerConfigPathForDiagnosticConfiguration(_diagnostic) + : _project.TryGetAnalyzerConfigPathForProjectConfiguration(); + if (analyzerConfigPath == null) + { + return null; } - private AnalyzerConfigDocument? FindOrGenerateEditorConfig() + if (_project.Solution?.FilePath == null) { - var analyzerConfigPath = _diagnostic != null - ? _project.TryGetAnalyzerConfigPathForDiagnosticConfiguration(_diagnostic) - : _project.TryGetAnalyzerConfigPathForProjectConfiguration(); - if (analyzerConfigPath == null) - { - return null; - } - - if (_project.Solution?.FilePath == null) - { - // Project has no solution or solution without a file path. - // Add analyzer config to just the current project. - return GetOrCreateAnalyzerConfigDocument(_project, analyzerConfigPath); - } + // Project has no solution or solution without a file path. + // Add analyzer config to just the current project. + return GetOrCreateAnalyzerConfigDocument(_project, analyzerConfigPath); + } - // Otherwise, add analyzer config document to all applicable projects for the current project's solution. - AnalyzerConfigDocument? analyzerConfigDocument = null; - var analyzerConfigDirectory = PathUtilities.GetDirectoryName(analyzerConfigPath) ?? throw ExceptionUtilities.Unreachable(); - var currentSolution = _project.Solution; - foreach (var projectId in _project.Solution.ProjectIds) + // Otherwise, add analyzer config document to all applicable projects for the current project's solution. + AnalyzerConfigDocument? analyzerConfigDocument = null; + var analyzerConfigDirectory = PathUtilities.GetDirectoryName(analyzerConfigPath) ?? throw ExceptionUtilities.Unreachable(); + var currentSolution = _project.Solution; + foreach (var projectId in _project.Solution.ProjectIds) + { + var project = currentSolution.GetProject(projectId); + if (project?.FilePath?.StartsWith(analyzerConfigDirectory) == true) { - var project = currentSolution.GetProject(projectId); - if (project?.FilePath?.StartsWith(analyzerConfigDirectory) == true) + var addedAnalyzerConfigDocument = GetOrCreateAnalyzerConfigDocument(project, analyzerConfigPath); + if (addedAnalyzerConfigDocument != null) { - var addedAnalyzerConfigDocument = GetOrCreateAnalyzerConfigDocument(project, analyzerConfigPath); - if (addedAnalyzerConfigDocument != null) - { - analyzerConfigDocument ??= addedAnalyzerConfigDocument; - currentSolution = addedAnalyzerConfigDocument.Project.Solution; - } + analyzerConfigDocument ??= addedAnalyzerConfigDocument; + currentSolution = addedAnalyzerConfigDocument.Project.Solution; } } - - return analyzerConfigDocument; } - private static AnalyzerConfigDocument? GetOrCreateAnalyzerConfigDocument(Project project, string analyzerConfigPath) + return analyzerConfigDocument; + } + + private static AnalyzerConfigDocument? GetOrCreateAnalyzerConfigDocument(Project project, string analyzerConfigPath) + { + var existingAnalyzerConfigDocument = project.TryGetExistingAnalyzerConfigDocumentAtPath(analyzerConfigPath); + if (existingAnalyzerConfigDocument != null) { - var existingAnalyzerConfigDocument = project.TryGetExistingAnalyzerConfigDocumentAtPath(analyzerConfigPath); - if (existingAnalyzerConfigDocument != null) - { - return existingAnalyzerConfigDocument; - } + return existingAnalyzerConfigDocument; + } - var id = DocumentId.CreateNewId(project.Id); - var documentInfo = DocumentInfo.Create( - id, - name: ".editorconfig", - filePath: analyzerConfigPath); + var id = DocumentId.CreateNewId(project.Id); + var documentInfo = DocumentInfo.Create( + id, + name: ".editorconfig", + filePath: analyzerConfigPath); - var newSolution = project.Solution.AddAnalyzerConfigDocuments([documentInfo]); - return newSolution.GetProject(project.Id)?.GetAnalyzerConfigDocument(id); - } + var newSolution = project.Solution.AddAnalyzerConfigDocuments([documentInfo]); + return newSolution.GetProject(project.Id)?.GetAnalyzerConfigDocument(id); + } - private static ImmutableArray<(string optionName, string currentOptionValue, bool isPerLanguage)> GetCodeStyleOptionValuesForDiagnostic( - Diagnostic diagnostic, - Project project) + private static ImmutableArray<(string optionName, string currentOptionValue, bool isPerLanguage)> GetCodeStyleOptionValuesForDiagnostic( + Diagnostic diagnostic, + Project project) + { + // For option based code style diagnostic, try to find the .editorconfig key-value pair for the + // option setting. + // For example, IDE diagnostics which are configurable with following code style option based .editorconfig entry: + // "%option_name% = %option_value%:%severity% + // we return '(option_name, new_option_value, new_severity)' + var codeStyleOptions = GetCodeStyleOptionsForDiagnostic(diagnostic, project); + if (!codeStyleOptions.IsEmpty) { - // For option based code style diagnostic, try to find the .editorconfig key-value pair for the - // option setting. - // For example, IDE diagnostics which are configurable with following code style option based .editorconfig entry: - // "%option_name% = %option_value%:%severity% - // we return '(option_name, new_option_value, new_severity)' - var codeStyleOptions = GetCodeStyleOptionsForDiagnostic(diagnostic, project); - if (!codeStyleOptions.IsEmpty) - { - var builder = ArrayBuilder<(string optionName, string currentOptionValue, bool isPerLanguage)>.GetInstance(); - - try - { - foreach (var option in codeStyleOptions) - { - var optionValue = option.Definition.Serializer.Serialize(option.DefaultValue); - builder.Add((option.Definition.ConfigName, optionValue, option.IsPerLanguage)); - } + var builder = ArrayBuilder<(string optionName, string currentOptionValue, bool isPerLanguage)>.GetInstance(); - return builder.ToImmutable(); - } - finally + try + { + foreach (var option in codeStyleOptions) { - builder.Free(); + var optionValue = option.Definition.Serializer.Serialize(option.DefaultValue); + builder.Add((option.Definition.ConfigName, optionValue, option.IsPerLanguage)); } - } - return []; + return builder.ToImmutable(); + } + finally + { + builder.Free(); + } } - internal static bool TryGetEditorConfigStringParts(string editorConfigString, out (string optionName, string optionValue) parts) + return []; + } + + internal static bool TryGetEditorConfigStringParts(string editorConfigString, out (string optionName, string optionValue) parts) + { + if (!string.IsNullOrEmpty(editorConfigString)) { - if (!string.IsNullOrEmpty(editorConfigString)) + var match = s_optionEntryPattern.Match(editorConfigString); + if (match.Success) { - var match = s_optionEntryPattern.Match(editorConfigString); - if (match.Success) - { - parts = (optionName: match.Groups[1].Value.Trim(), - optionValue: match.Groups[2].Value.Trim()); - return true; - } + parts = (optionName: match.Groups[1].Value.Trim(), + optionValue: match.Groups[2].Value.Trim()); + return true; } - - parts = default; - return false; } - internal static ImmutableArray GetCodeStyleOptionsForDiagnostic(Diagnostic diagnostic, Project project) + parts = default; + return false; + } + + internal static ImmutableArray GetCodeStyleOptionsForDiagnostic(Diagnostic diagnostic, Project project) + { + if (IDEDiagnosticIdToOptionMappingHelper.TryGetMappedOptions(diagnostic.Id, project.Language, out var options)) { - if (IDEDiagnosticIdToOptionMappingHelper.TryGetMappedOptions(diagnostic.Id, project.Language, out var options)) - { - return (from option in options - where option.DefaultValue is ICodeStyleOption - orderby option.Definition.ConfigName - select option).ToImmutableArray(); - } + return (from option in options + where option.DefaultValue is ICodeStyleOption + orderby option.Definition.ConfigName + select option).ToImmutableArray(); + } - return []; + return []; + } + + private SourceText? GetNewAnalyzerConfigDocumentText(SourceText originalText, AnalyzerConfigDocument editorConfigDocument) + { + // Check if an entry to configure the rule severity already exists in the .editorconfig file. + // If it does, we update the existing entry with the new severity. + var (newText, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd) = CheckIfRuleExistsAndReplaceInFile(originalText, editorConfigDocument); + if (newText != null) + { + return newText; } - private SourceText? GetNewAnalyzerConfigDocumentText(SourceText originalText, AnalyzerConfigDocument editorConfigDocument) + if (!_addNewEntryIfNoExistingEntryFound) { - // Check if an entry to configure the rule severity already exists in the .editorconfig file. - // If it does, we update the existing entry with the new severity. - var (newText, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd) = CheckIfRuleExistsAndReplaceInFile(originalText, editorConfigDocument); - if (newText != null) - { - return newText; - } + return originalText; + } - if (!_addNewEntryIfNoExistingEntryFound) - { - return originalText; - } + // We did not find any existing entry in the in the .editorconfig file to configure rule severity. + // So we add a new configuration entry to the .editorconfig file. + return AddMissingRule(originalText, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + } - // We did not find any existing entry in the in the .editorconfig file to configure rule severity. - // So we add a new configuration entry to the .editorconfig file. - return AddMissingRule(originalText, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + private (SourceText? newText, TextLine? lastValidHeaderSpanEnd, TextLine? lastValidSpecificHeaderSpanEnd) CheckIfRuleExistsAndReplaceInFile( + SourceText result, + AnalyzerConfigDocument editorConfigDocument) + { + // If there's an error finding the editorconfig directory, bail out. + var editorConfigDirectory = PathUtilities.GetDirectoryName(editorConfigDocument.FilePath); + if (editorConfigDirectory == null) + { + return (null, null, null); } - private (SourceText? newText, TextLine? lastValidHeaderSpanEnd, TextLine? lastValidSpecificHeaderSpanEnd) CheckIfRuleExistsAndReplaceInFile( - SourceText result, - AnalyzerConfigDocument editorConfigDocument) - { - // If there's an error finding the editorconfig directory, bail out. - var editorConfigDirectory = PathUtilities.GetDirectoryName(editorConfigDocument.FilePath); - if (editorConfigDirectory == null) - { - return (null, null, null); - } + var relativePath = string.Empty; + var diagnosticFilePath = string.Empty; - var relativePath = string.Empty; - var diagnosticFilePath = string.Empty; + // If diagnostic SourceTree is null, it means either Location.None or Bulk configuration at root editorconfig, and thus no relative path. + var diagnosticSourceTree = _diagnostic?.Location.SourceTree; + if (diagnosticSourceTree != null) + { + // Finds the relative path between editorconfig directory and diagnostic filepath. + diagnosticFilePath = diagnosticSourceTree.FilePath.ToLowerInvariant(); + relativePath = PathUtilities.GetRelativePath(editorConfigDirectory.ToLowerInvariant(), diagnosticFilePath); + relativePath = PathUtilities.NormalizeWithForwardSlash(relativePath); + } - // If diagnostic SourceTree is null, it means either Location.None or Bulk configuration at root editorconfig, and thus no relative path. - var diagnosticSourceTree = _diagnostic?.Location.SourceTree; - if (diagnosticSourceTree != null) - { - // Finds the relative path between editorconfig directory and diagnostic filepath. - diagnosticFilePath = diagnosticSourceTree.FilePath.ToLowerInvariant(); - relativePath = PathUtilities.GetRelativePath(editorConfigDirectory.ToLowerInvariant(), diagnosticFilePath); - relativePath = PathUtilities.NormalizeWithForwardSlash(relativePath); - } + TextLine? mostRecentHeader = null; + TextLine? lastValidHeader = null; + TextLine? lastValidHeaderSpanEnd = null; - TextLine? mostRecentHeader = null; - TextLine? lastValidHeader = null; - TextLine? lastValidHeaderSpanEnd = null; + TextLine? lastValidSpecificHeader = null; + TextLine? lastValidSpecificHeaderSpanEnd = null; - TextLine? lastValidSpecificHeader = null; - TextLine? lastValidSpecificHeaderSpanEnd = null; + var textChange = new TextChange(); + var isGlobalConfig = false; - var textChange = new TextChange(); - var isGlobalConfig = false; + foreach (var curLine in result.Lines) + { + var curLineText = curLine.ToString(); - foreach (var curLine in result.Lines) + if (s_optionEntryPattern.IsMatch(curLineText)) { - var curLineText = curLine.ToString(); + var groups = s_optionEntryPattern.Match(curLineText).Groups; + + // Regex groups: + // 1. Option key + // 2. Option value + // 3. Optional severity suffix, i.e. ':severity' suffix + // 4. Optional comment suffix + var untrimmedKey = groups[1].Value.ToString(); + var key = untrimmedKey.Trim(); + var value = groups[2].Value.ToString(); + var severitySuffixInValue = groups[3].Value.ToString(); + var commentValue = groups[4].Value.ToString(); + + // Check for global config header: "is_global = true" + if (mostRecentHeader == null && + lastValidHeader == null && + key.Equals("is_global", StringComparison.OrdinalIgnoreCase) && + value.Trim().Equals("true", StringComparison.OrdinalIgnoreCase) && + severitySuffixInValue.Length == 0) + { + isGlobalConfig = true; + mostRecentHeader = curLine; + lastValidHeader = curLine; + lastValidHeaderSpanEnd = curLine; + continue; + } - if (s_optionEntryPattern.IsMatch(curLineText)) + // Verify the most recent header is a valid header + if (mostRecentHeader != null && + lastValidHeader != null && + mostRecentHeader.Equals(lastValidHeader)) { - var groups = s_optionEntryPattern.Match(curLineText).Groups; - - // Regex groups: - // 1. Option key - // 2. Option value - // 3. Optional severity suffix, i.e. ':severity' suffix - // 4. Optional comment suffix - var untrimmedKey = groups[1].Value.ToString(); - var key = untrimmedKey.Trim(); - var value = groups[2].Value.ToString(); - var severitySuffixInValue = groups[3].Value.ToString(); - var commentValue = groups[4].Value.ToString(); - - // Check for global config header: "is_global = true" - if (mostRecentHeader == null && - lastValidHeader == null && - key.Equals("is_global", StringComparison.OrdinalIgnoreCase) && - value.Trim().Equals("true", StringComparison.OrdinalIgnoreCase) && - severitySuffixInValue.Length == 0) + // We found the rule in the file -- replace it with updated option value/severity. + if (key.Equals(_optionNameOpt)) { - isGlobalConfig = true; - mostRecentHeader = curLine; - lastValidHeader = curLine; - lastValidHeaderSpanEnd = curLine; - continue; + // We found an option configuration entry of form: + // "%option_name% = %option_value% + // OR + // "%option_name% = %option_value%:%severity% + var newOptionValue = _configurationKind == ConfigurationKind.OptionValue + ? $"{value.GetLeadingWhitespace()}{_newOptionValueOpt}{value.GetTrailingWhitespace()}" + : value; + var newSeverityValue = _configurationKind == ConfigurationKind.Severity && severitySuffixInValue.Length > 0 ? $":{_newSeverity}" : severitySuffixInValue; + + textChange = new TextChange(curLine.Span, $"{untrimmedKey}={newOptionValue}{newSeverityValue}{commentValue}"); } - - // Verify the most recent header is a valid header - if (mostRecentHeader != null && - lastValidHeader != null && - mostRecentHeader.Equals(lastValidHeader)) + else { - // We found the rule in the file -- replace it with updated option value/severity. - if (key.Equals(_optionNameOpt)) + // We want to detect severity based entry only when we are configuring severity and have no option name specified. + if (_configurationKind != ConfigurationKind.OptionValue && + _optionNameOpt == null && + severitySuffixInValue.Length == 0 && + key.EndsWith(SeveritySuffix)) { - // We found an option configuration entry of form: - // "%option_name% = %option_value% - // OR - // "%option_name% = %option_value%:%severity% - var newOptionValue = _configurationKind == ConfigurationKind.OptionValue - ? $"{value.GetLeadingWhitespace()}{_newOptionValueOpt}{value.GetTrailingWhitespace()}" - : value; - var newSeverityValue = _configurationKind == ConfigurationKind.Severity && severitySuffixInValue.Length > 0 ? $":{_newSeverity}" : severitySuffixInValue; - - textChange = new TextChange(curLine.Span, $"{untrimmedKey}={newOptionValue}{newSeverityValue}{commentValue}"); - } - else - { - // We want to detect severity based entry only when we are configuring severity and have no option name specified. - if (_configurationKind != ConfigurationKind.OptionValue && - _optionNameOpt == null && - severitySuffixInValue.Length == 0 && - key.EndsWith(SeveritySuffix)) + // We found a rule configuration entry of severity based form: + // "dotnet_diagnostic.<%DiagnosticId%>.severity = %severity% + // OR + // "dotnet_analyzer_diagnostic.severity = %severity% + // OR + // "dotnet_analyzer_diagnostic.category-<%DiagnosticCategory%>.severity = %severity% + + var foundMatch = false; + switch (_configurationKind) { - // We found a rule configuration entry of severity based form: - // "dotnet_diagnostic.<%DiagnosticId%>.severity = %severity% - // OR - // "dotnet_analyzer_diagnostic.severity = %severity% - // OR - // "dotnet_analyzer_diagnostic.category-<%DiagnosticCategory%>.severity = %severity% - - var foundMatch = false; - switch (_configurationKind) - { - case ConfigurationKind.Severity: - RoslynDebug.Assert(_diagnostic != null); - if (key.StartsWith(DiagnosticOptionPrefix, StringComparison.Ordinal)) + case ConfigurationKind.Severity: + RoslynDebug.Assert(_diagnostic != null); + if (key.StartsWith(DiagnosticOptionPrefix, StringComparison.Ordinal)) + { + var diagIdLength = key.Length - (DiagnosticOptionPrefix.Length + SeveritySuffix.Length); + if (diagIdLength > 0) { - var diagIdLength = key.Length - (DiagnosticOptionPrefix.Length + SeveritySuffix.Length); - if (diagIdLength > 0) - { - var diagId = key.Substring(DiagnosticOptionPrefix.Length, diagIdLength); - foundMatch = string.Equals(diagId, _diagnostic.Id, StringComparison.OrdinalIgnoreCase); - } + var diagId = key.Substring(DiagnosticOptionPrefix.Length, diagIdLength); + foundMatch = string.Equals(diagId, _diagnostic.Id, StringComparison.OrdinalIgnoreCase); } + } - break; + break; - case ConfigurationKind.BulkConfigure: - RoslynDebug.Assert(_categoryToBulkConfigure != null); - if (_categoryToBulkConfigure == AllAnalyzerDiagnosticsCategory) - { - foundMatch = key == BulkConfigureAllAnalyzerDiagnosticsOptionKey; - } - else + case ConfigurationKind.BulkConfigure: + RoslynDebug.Assert(_categoryToBulkConfigure != null); + if (_categoryToBulkConfigure == AllAnalyzerDiagnosticsCategory) + { + foundMatch = key == BulkConfigureAllAnalyzerDiagnosticsOptionKey; + } + else + { + if (key.StartsWith(BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix, StringComparison.Ordinal)) { - if (key.StartsWith(BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix, StringComparison.Ordinal)) - { - var categoryLength = key.Length - (BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix.Length + SeveritySuffix.Length); - var category = key.Substring(BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix.Length, categoryLength); - foundMatch = string.Equals(category, _categoryToBulkConfigure, StringComparison.OrdinalIgnoreCase); - } + var categoryLength = key.Length - (BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix.Length + SeveritySuffix.Length); + var category = key.Substring(BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix.Length, categoryLength); + foundMatch = string.Equals(category, _categoryToBulkConfigure, StringComparison.OrdinalIgnoreCase); } + } - break; - } + break; + } - if (foundMatch) - { - var newSeverityValue = $"{value.GetLeadingWhitespace()}{_newSeverity}{value.GetTrailingWhitespace()}"; - textChange = new TextChange(curLine.Span, $"{untrimmedKey}={newSeverityValue}{commentValue}"); - } + if (foundMatch) + { + var newSeverityValue = $"{value.GetLeadingWhitespace()}{_newSeverity}{value.GetTrailingWhitespace()}"; + textChange = new TextChange(curLine.Span, $"{untrimmedKey}={newSeverityValue}{commentValue}"); } } } } - else if (!isGlobalConfig && s_headerPattern.IsMatch(curLineText.Trim())) - { - // We found a header entry such as '[*.cs]', '[*.vb]', etc. - // Verify that header is valid. - mostRecentHeader = curLine; - var groups = s_headerPattern.Match(curLineText.Trim()).Groups; - var mostRecentHeaderText = groups[1].Value.ToString().ToLowerInvariant(); + } + else if (!isGlobalConfig && s_headerPattern.IsMatch(curLineText.Trim())) + { + // We found a header entry such as '[*.cs]', '[*.vb]', etc. + // Verify that header is valid. + mostRecentHeader = curLine; + var groups = s_headerPattern.Match(curLineText.Trim()).Groups; + var mostRecentHeaderText = groups[1].Value.ToString().ToLowerInvariant(); - if (mostRecentHeaderText.Equals("*")) + if (mostRecentHeaderText.Equals("*")) + { + lastValidHeader = mostRecentHeader; + } + else + { + // We splice on the last occurrence of '.' to account for filenames containing periods. + var nameExtensionSplitIndex = mostRecentHeaderText.LastIndexOf('.'); + var fileName = mostRecentHeaderText[..nameExtensionSplitIndex]; + var splicedFileExtensions = mostRecentHeaderText[(nameExtensionSplitIndex + 1)..].Split(',', ' ', '{', '}'); + + // Replacing characters in the header with the regex equivalent. + fileName = fileName.Replace(".", @"\."); + fileName = fileName.Replace("*", ".*"); + fileName = fileName.Replace("/", @"\/"); + + // Creating the header regex string, ex. [*.{cs,vb}] => ((\.cs)|(\.vb)) + var headerRegexStr = fileName + @"((\." + splicedFileExtensions[0] + ")"; + for (var i = 1; i < splicedFileExtensions.Length; i++) { - lastValidHeader = mostRecentHeader; + headerRegexStr += @"|(\." + splicedFileExtensions[i] + ")"; } - else - { - // We splice on the last occurrence of '.' to account for filenames containing periods. - var nameExtensionSplitIndex = mostRecentHeaderText.LastIndexOf('.'); - var fileName = mostRecentHeaderText[..nameExtensionSplitIndex]; - var splicedFileExtensions = mostRecentHeaderText[(nameExtensionSplitIndex + 1)..].Split(',', ' ', '{', '}'); - - // Replacing characters in the header with the regex equivalent. - fileName = fileName.Replace(".", @"\."); - fileName = fileName.Replace("*", ".*"); - fileName = fileName.Replace("/", @"\/"); - - // Creating the header regex string, ex. [*.{cs,vb}] => ((\.cs)|(\.vb)) - var headerRegexStr = fileName + @"((\." + splicedFileExtensions[0] + ")"; - for (var i = 1; i < splicedFileExtensions.Length; i++) - { - headerRegexStr += @"|(\." + splicedFileExtensions[i] + ")"; - } - headerRegexStr += ")"; + headerRegexStr += ")"; - var headerRegex = new Regex(headerRegexStr); + var headerRegex = new Regex(headerRegexStr); - // We check that the relative path of the .editorconfig file to the diagnostic file - // matches the header regex pattern. - if (headerRegex.IsMatch(relativePath)) - { - var match = headerRegex.Match(relativePath).Value; - var matchWithoutExtension = match[..match.LastIndexOf('.')]; + // We check that the relative path of the .editorconfig file to the diagnostic file + // matches the header regex pattern. + if (headerRegex.IsMatch(relativePath)) + { + var match = headerRegex.Match(relativePath).Value; + var matchWithoutExtension = match[..match.LastIndexOf('.')]; - // Edge case: The below statement checks that we correctly handle cases such as a header of [m.cs] and - // a file name of Program.cs. - if (matchWithoutExtension.Contains(PathUtilities.GetFileName(diagnosticFilePath, false))) + // Edge case: The below statement checks that we correctly handle cases such as a header of [m.cs] and + // a file name of Program.cs. + if (matchWithoutExtension.Contains(PathUtilities.GetFileName(diagnosticFilePath, false))) + { + // If the diagnostic's isPerLanguage = true, the rule is valid for both C# and VB. + // For the purpose of adding missing rules later, we want to keep track of whether there is a + // valid header that contains both [*.cs] and [*.vb]. + // If isPerLanguage = false or a compiler diagnostic, the rule is only valid for one of the languages. + // Thus, we want to keep track of whether there is an existing header that only contains [*.cs] or only + // [*.vb], depending on the language. + // We also keep track of the last valid header for the language. + var isLanguageAgnosticEntry = (_diagnostic == null || !SuppressionHelpers.IsCompilerDiagnostic(_diagnostic)) && _isPerLanguage; + if (isLanguageAgnosticEntry) { - // If the diagnostic's isPerLanguage = true, the rule is valid for both C# and VB. - // For the purpose of adding missing rules later, we want to keep track of whether there is a - // valid header that contains both [*.cs] and [*.vb]. - // If isPerLanguage = false or a compiler diagnostic, the rule is only valid for one of the languages. - // Thus, we want to keep track of whether there is an existing header that only contains [*.cs] or only - // [*.vb], depending on the language. - // We also keep track of the last valid header for the language. - var isLanguageAgnosticEntry = (_diagnostic == null || !SuppressionHelpers.IsCompilerDiagnostic(_diagnostic)) && _isPerLanguage; - if (isLanguageAgnosticEntry) + if ((_language.Equals(LanguageNames.CSharp) || _language.Equals(LanguageNames.VisualBasic)) && + splicedFileExtensions.Contains("cs") && splicedFileExtensions.Contains("vb")) { - if ((_language.Equals(LanguageNames.CSharp) || _language.Equals(LanguageNames.VisualBasic)) && - splicedFileExtensions.Contains("cs") && splicedFileExtensions.Contains("vb")) - { - lastValidSpecificHeader = mostRecentHeader; - } + lastValidSpecificHeader = mostRecentHeader; } - else if (splicedFileExtensions.Length == 1) + } + else if (splicedFileExtensions.Length == 1) + { + if (_language.Equals(LanguageNames.CSharp) && splicedFileExtensions.Contains("cs")) { - if (_language.Equals(LanguageNames.CSharp) && splicedFileExtensions.Contains("cs")) - { - lastValidSpecificHeader = mostRecentHeader; - } - else if (_language.Equals(LanguageNames.VisualBasic) && splicedFileExtensions.Contains("vb")) - { - lastValidSpecificHeader = mostRecentHeader; - } + lastValidSpecificHeader = mostRecentHeader; + } + else if (_language.Equals(LanguageNames.VisualBasic) && splicedFileExtensions.Contains("vb")) + { + lastValidSpecificHeader = mostRecentHeader; } - - lastValidHeader = mostRecentHeader; } + + lastValidHeader = mostRecentHeader; } - // Location.None special case. - else if (relativePath.IsEmpty() && new Regex(fileName).IsMatch(relativePath)) + } + // Location.None special case. + else if (relativePath.IsEmpty() && new Regex(fileName).IsMatch(relativePath)) + { + if ((_language.Equals(LanguageNames.CSharp) && splicedFileExtensions.Contains("cs")) || + (_language.Equals(LanguageNames.VisualBasic) && splicedFileExtensions.Contains("vb"))) { - if ((_language.Equals(LanguageNames.CSharp) && splicedFileExtensions.Contains("cs")) || - (_language.Equals(LanguageNames.VisualBasic) && splicedFileExtensions.Contains("vb"))) - { - lastValidHeader = mostRecentHeader; - } + lastValidHeader = mostRecentHeader; } } } + } - // We want to keep track of how far this (valid) section spans. - if (mostRecentHeader != null && - lastValidHeader != null && - mostRecentHeader.Equals(lastValidHeader)) + // We want to keep track of how far this (valid) section spans. + if (mostRecentHeader != null && + lastValidHeader != null && + mostRecentHeader.Equals(lastValidHeader)) + { + lastValidHeaderSpanEnd = curLine; + if (lastValidSpecificHeader != null && mostRecentHeader.Equals(lastValidSpecificHeader)) { - lastValidHeaderSpanEnd = curLine; - if (lastValidSpecificHeader != null && mostRecentHeader.Equals(lastValidSpecificHeader)) - { - lastValidSpecificHeaderSpanEnd = curLine; - } + lastValidSpecificHeaderSpanEnd = curLine; } } + } + + // We return only the last text change in case of duplicate entries for the same rule. + if (textChange != default) + { + return (result.WithChanges(textChange), lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + } - // We return only the last text change in case of duplicate entries for the same rule. - if (textChange != default) + // Rule not found. + return (null, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + } + + private SourceText? AddMissingRule( + SourceText result, + TextLine? lastValidHeaderSpanEnd, + TextLine? lastValidSpecificHeaderSpanEnd) + { + // Create a new rule configuration entry for the given diagnostic ID or bulk configuration category. + // If optionNameOpt and optionValueOpt are non-null, it indicates an option based diagnostic ID + // which can be configured by a new entry such as: "%option_name% = %option_value%:%severity% + // Otherwise, if diagnostic is non-null, it indicates a non-option diagnostic ID, + // which can be configured by a new entry such as: "dotnet_diagnostic.<%DiagnosticId%>.severity = %severity% + // Otherwise, it indicates a bulk configuration entry for default severity of a specific diagnostic category or all analyzer diagnostics, + // which can be configured by a new entry such as: + // 1. All analyzer diagnostics: "dotnet_analyzer_diagnostic.severity = %severity% + // 2. Category configuration: "dotnet_analyzer_diagnostic.category-<%DiagnosticCategory%>.severity = %severity% + + var newEntry = !string.IsNullOrEmpty(_optionNameOpt) && !string.IsNullOrEmpty(_newOptionValueOpt) + ? $"{_optionNameOpt} = {_newOptionValueOpt}" + : _diagnostic != null + ? $"{DiagnosticOptionPrefix}{_diagnostic.Id}{SeveritySuffix} = {_newSeverity}" + : _categoryToBulkConfigure == AllAnalyzerDiagnosticsCategory + ? $"{BulkConfigureAllAnalyzerDiagnosticsOptionKey} = {_newSeverity}" + : $"{BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix}{_categoryToBulkConfigure}{SeveritySuffix} = {_newSeverity}"; + + // Insert a new line and comment text above the new entry + var commentPrefix = _diagnostic != null + ? $"{_diagnostic.Id}: {_diagnostic.Descriptor.Title}" + : _categoryToBulkConfigure == AllAnalyzerDiagnosticsCategory + ? "Default severity for all analyzer diagnostics" + : $"Default severity for analyzer diagnostics with category '{_categoryToBulkConfigure}'"; + + newEntry = $"\r\n# {commentPrefix}\r\n{newEntry}\r\n"; + + // Check if have a correct existing header for the new entry. + // - If the diagnostic's isPerLanguage = true, it means the rule is valid for both C# and VB. + // Thus, if there is a valid existing header containing both [*.cs] and [*.vb], then we prioritize it. + // - If isPerLanguage = false, it means the rule is only valid for one of the languages. Thus, we + // prioritize headers that contain only the file extension for the given language. + // - If neither of the above hold true, we choose the last existing valid header. + // - If no valid existing headers, we generate a new header. + if (lastValidSpecificHeaderSpanEnd.HasValue) + { + if (lastValidSpecificHeaderSpanEnd.Value.ToString().Trim().Length != 0) { - return (result.WithChanges(textChange), lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + newEntry = "\r\n" + newEntry; } - // Rule not found. - return (null, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); + var textChange = new TextChange(new TextSpan(lastValidSpecificHeaderSpanEnd.Value.Span.End, 0), newEntry); + return result.WithChanges(textChange); } - - private SourceText? AddMissingRule( - SourceText result, - TextLine? lastValidHeaderSpanEnd, - TextLine? lastValidSpecificHeaderSpanEnd) + else if (lastValidHeaderSpanEnd.HasValue) { - // Create a new rule configuration entry for the given diagnostic ID or bulk configuration category. - // If optionNameOpt and optionValueOpt are non-null, it indicates an option based diagnostic ID - // which can be configured by a new entry such as: "%option_name% = %option_value%:%severity% - // Otherwise, if diagnostic is non-null, it indicates a non-option diagnostic ID, - // which can be configured by a new entry such as: "dotnet_diagnostic.<%DiagnosticId%>.severity = %severity% - // Otherwise, it indicates a bulk configuration entry for default severity of a specific diagnostic category or all analyzer diagnostics, - // which can be configured by a new entry such as: - // 1. All analyzer diagnostics: "dotnet_analyzer_diagnostic.severity = %severity% - // 2. Category configuration: "dotnet_analyzer_diagnostic.category-<%DiagnosticCategory%>.severity = %severity% - - var newEntry = !string.IsNullOrEmpty(_optionNameOpt) && !string.IsNullOrEmpty(_newOptionValueOpt) - ? $"{_optionNameOpt} = {_newOptionValueOpt}" - : _diagnostic != null - ? $"{DiagnosticOptionPrefix}{_diagnostic.Id}{SeveritySuffix} = {_newSeverity}" - : _categoryToBulkConfigure == AllAnalyzerDiagnosticsCategory - ? $"{BulkConfigureAllAnalyzerDiagnosticsOptionKey} = {_newSeverity}" - : $"{BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix}{_categoryToBulkConfigure}{SeveritySuffix} = {_newSeverity}"; - - // Insert a new line and comment text above the new entry - var commentPrefix = _diagnostic != null - ? $"{_diagnostic.Id}: {_diagnostic.Descriptor.Title}" - : _categoryToBulkConfigure == AllAnalyzerDiagnosticsCategory - ? "Default severity for all analyzer diagnostics" - : $"Default severity for analyzer diagnostics with category '{_categoryToBulkConfigure}'"; - - newEntry = $"\r\n# {commentPrefix}\r\n{newEntry}\r\n"; - - // Check if have a correct existing header for the new entry. - // - If the diagnostic's isPerLanguage = true, it means the rule is valid for both C# and VB. - // Thus, if there is a valid existing header containing both [*.cs] and [*.vb], then we prioritize it. - // - If isPerLanguage = false, it means the rule is only valid for one of the languages. Thus, we - // prioritize headers that contain only the file extension for the given language. - // - If neither of the above hold true, we choose the last existing valid header. - // - If no valid existing headers, we generate a new header. - if (lastValidSpecificHeaderSpanEnd.HasValue) + if (lastValidHeaderSpanEnd.Value.ToString().Trim().Length != 0) { - if (lastValidSpecificHeaderSpanEnd.Value.ToString().Trim().Length != 0) - { - newEntry = "\r\n" + newEntry; - } - - var textChange = new TextChange(new TextSpan(lastValidSpecificHeaderSpanEnd.Value.Span.End, 0), newEntry); - return result.WithChanges(textChange); + newEntry = "\r\n" + newEntry; } - else if (lastValidHeaderSpanEnd.HasValue) - { - if (lastValidHeaderSpanEnd.Value.ToString().Trim().Length != 0) - { - newEntry = "\r\n" + newEntry; - } - var textChange = new TextChange(new TextSpan(lastValidHeaderSpanEnd.Value.Span.End, 0), newEntry); - return result.WithChanges(textChange); - } + var textChange = new TextChange(new TextSpan(lastValidHeaderSpanEnd.Value.Span.End, 0), newEntry); + return result.WithChanges(textChange); + } - // We need to generate a new header such as '[*.cs]' or '[*.vb]': - // - For compiler diagnostic entries and code style entries which have per-language option = false, generate only [*.cs] or [*.vb]. - // - For the remainder, generate [*.{cs,vb}] - if (_language is LanguageNames.CSharp or LanguageNames.VisualBasic) + // We need to generate a new header such as '[*.cs]' or '[*.vb]': + // - For compiler diagnostic entries and code style entries which have per-language option = false, generate only [*.cs] or [*.vb]. + // - For the remainder, generate [*.{cs,vb}] + if (_language is LanguageNames.CSharp or LanguageNames.VisualBasic) + { + // Insert a newline if not already present + var lines = result.Lines; + var lastLine = lines.Count > 0 ? lines[^1] : default; + var prefix = string.Empty; + if (lastLine.ToString().Trim().Length != 0) { - // Insert a newline if not already present - var lines = result.Lines; - var lastLine = lines.Count > 0 ? lines[^1] : default; - var prefix = string.Empty; - if (lastLine.ToString().Trim().Length != 0) - { - prefix = "\r\n"; - } - - // Insert newline if file is not empty - if (lines.Count > 1 && lastLine.ToString().Trim().Length == 0) - { - prefix += "\r\n"; - } + prefix = "\r\n"; + } - var compilerDiagOrNotPerLang = (_diagnostic != null && SuppressionHelpers.IsCompilerDiagnostic(_diagnostic)) || !_isPerLanguage; - if (_language.Equals(LanguageNames.CSharp) && compilerDiagOrNotPerLang) - { - prefix += "[*.cs]\r\n"; - } - else if (_language.Equals(LanguageNames.VisualBasic) && compilerDiagOrNotPerLang) - { - prefix += "[*.vb]\r\n"; - } - else - { - prefix += "[*.{cs,vb}]\r\n"; - } + // Insert newline if file is not empty + if (lines.Count > 1 && lastLine.ToString().Trim().Length == 0) + { + prefix += "\r\n"; + } - var textChange = new TextChange(new TextSpan(result.Length, 0), prefix + newEntry); - return result.WithChanges(textChange); + var compilerDiagOrNotPerLang = (_diagnostic != null && SuppressionHelpers.IsCompilerDiagnostic(_diagnostic)) || !_isPerLanguage; + if (_language.Equals(LanguageNames.CSharp) && compilerDiagOrNotPerLang) + { + prefix += "[*.cs]\r\n"; + } + else if (_language.Equals(LanguageNames.VisualBasic) && compilerDiagOrNotPerLang) + { + prefix += "[*.vb]\r\n"; + } + else + { + prefix += "[*.{cs,vb}]\r\n"; } - return null; + var textChange = new TextChange(new TextSpan(result.Length, 0), prefix + newEntry); + return result.WithChanges(textChange); } + + return null; } } diff --git a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureCodeStyle/ConfigureCodeStyleOptionCodeFixProvider.TopLevelConfigureCodeStyleOptionCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureCodeStyle/ConfigureCodeStyleOptionCodeFixProvider.TopLevelConfigureCodeStyleOptionCodeAction.cs index 2d5cf20da4ce3..0ccfe07a7fe4b 100644 --- a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureCodeStyle/ConfigureCodeStyleOptionCodeFixProvider.TopLevelConfigureCodeStyleOptionCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureCodeStyle/ConfigureCodeStyleOptionCodeFixProvider.TopLevelConfigureCodeStyleOptionCodeAction.cs @@ -5,21 +5,20 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.CodeActions; -namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureCodeStyle +namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureCodeStyle; + +internal sealed partial class ConfigureCodeStyleOptionCodeFixProvider : IConfigurationFixProvider { - internal sealed partial class ConfigureCodeStyleOptionCodeFixProvider : IConfigurationFixProvider + private sealed class TopLevelConfigureCodeStyleOptionCodeAction : AbstractConfigurationActionWithNestedActions { - private sealed class TopLevelConfigureCodeStyleOptionCodeAction : AbstractConfigurationActionWithNestedActions + public TopLevelConfigureCodeStyleOptionCodeAction(Diagnostic diagnostic, ImmutableArray nestedActions) + : base(nestedActions, string.Format(FeaturesResources.Configure_0_code_style, diagnostic.Id)) { - public TopLevelConfigureCodeStyleOptionCodeAction(Diagnostic diagnostic, ImmutableArray nestedActions) - : base(nestedActions, string.Format(FeaturesResources.Configure_0_code_style, diagnostic.Id)) - { - } + } - public TopLevelConfigureCodeStyleOptionCodeAction(string optionName, ImmutableArray nestedActions) - : base(nestedActions, optionName) - { - } + public TopLevelConfigureCodeStyleOptionCodeAction(string optionName, ImmutableArray nestedActions) + : base(nestedActions, optionName) + { } } } diff --git a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureCodeStyle/ConfigureCodeStyleOptionCodeFixProvider.cs b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureCodeStyle/ConfigureCodeStyleOptionCodeFixProvider.cs index b86ae1e33f9f6..483b48866729c 100644 --- a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureCodeStyle/ConfigureCodeStyleOptionCodeFixProvider.cs +++ b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureCodeStyle/ConfigureCodeStyleOptionCodeFixProvider.cs @@ -21,138 +21,137 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.CodeActions.CodeAction; -namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureCodeStyle +namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureCodeStyle; + +[ExportConfigurationFixProvider(PredefinedConfigurationFixProviderNames.ConfigureCodeStyleOption, LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] +[ExtensionOrder(Before = PredefinedConfigurationFixProviderNames.ConfigureSeverity)] +[ExtensionOrder(After = PredefinedConfigurationFixProviderNames.Suppression)] +internal sealed partial class ConfigureCodeStyleOptionCodeFixProvider : IConfigurationFixProvider { - [ExportConfigurationFixProvider(PredefinedConfigurationFixProviderNames.ConfigureCodeStyleOption, LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] - [ExtensionOrder(Before = PredefinedConfigurationFixProviderNames.ConfigureSeverity)] - [ExtensionOrder(After = PredefinedConfigurationFixProviderNames.Suppression)] - internal sealed partial class ConfigureCodeStyleOptionCodeFixProvider : IConfigurationFixProvider + private static readonly ImmutableArray s_boolValues = [true, false]; + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public ConfigureCodeStyleOptionCodeFixProvider() { - private static readonly ImmutableArray s_boolValues = [true, false]; + } - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public ConfigureCodeStyleOptionCodeFixProvider() + public bool IsFixableDiagnostic(Diagnostic diagnostic) + { + // We only offer fix for configurable code style diagnostics which have one of more editorconfig based storage locations. + // Also skip suppressed diagnostics defensively, though the code fix engine should ideally never call us for suppressed diagnostics. + if (diagnostic.IsSuppressed || + SuppressionHelpers.IsNotConfigurableDiagnostic(diagnostic) || + diagnostic.Location.SourceTree == null) { + return false; } - public bool IsFixableDiagnostic(Diagnostic diagnostic) - { - // We only offer fix for configurable code style diagnostics which have one of more editorconfig based storage locations. - // Also skip suppressed diagnostics defensively, though the code fix engine should ideally never call us for suppressed diagnostics. - if (diagnostic.IsSuppressed || - SuppressionHelpers.IsNotConfigurableDiagnostic(diagnostic) || - diagnostic.Location.SourceTree == null) - { - return false; - } - - var language = diagnostic.Location.SourceTree.Options.Language; - return IDEDiagnosticIdToOptionMappingHelper.TryGetMappedOptions(diagnostic.Id, language, out _); - } + var language = diagnostic.Location.SourceTree.Options.Language; + return IDEDiagnosticIdToOptionMappingHelper.TryGetMappedOptions(diagnostic.Id, language, out _); + } - public FixAllProvider? GetFixAllProvider() - => null; + public FixAllProvider? GetFixAllProvider() + => null; - public Task> GetFixesAsync(TextDocument document, TextSpan span, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - => Task.FromResult(GetConfigurations(document.Project, diagnostics)); + public Task> GetFixesAsync(TextDocument document, TextSpan span, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + => Task.FromResult(GetConfigurations(document.Project, diagnostics)); - public Task> GetFixesAsync(Project project, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - => Task.FromResult(GetConfigurations(project, diagnostics)); + public Task> GetFixesAsync(Project project, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + => Task.FromResult(GetConfigurations(project, diagnostics)); - private static ImmutableArray GetConfigurations(Project project, IEnumerable diagnostics) + private static ImmutableArray GetConfigurations(Project project, IEnumerable diagnostics) + { + var result = ArrayBuilder.GetInstance(); + foreach (var diagnostic in diagnostics) { - var result = ArrayBuilder.GetInstance(); - foreach (var diagnostic in diagnostics) + // First get all the relevant code style options for the diagnostic. + var codeStyleOptions = ConfigurationUpdater.GetCodeStyleOptionsForDiagnostic(diagnostic, project); + if (codeStyleOptions.IsEmpty) { - // First get all the relevant code style options for the diagnostic. - var codeStyleOptions = ConfigurationUpdater.GetCodeStyleOptionsForDiagnostic(diagnostic, project); - if (codeStyleOptions.IsEmpty) - { - continue; - } + continue; + } - // For each code style option, create a top level code action with nested code actions for every valid option value. - // For example, if the option value is CodeStyleOption, we will have two nested actions, one for 'true' setting and one - // for 'false' setting. If the option value is CodeStyleOption, we will have a nested action for each enum field. - using var _ = ArrayBuilder.GetInstance(out var nestedActions); - var hasMultipleOptions = codeStyleOptions.Length > 1; - foreach (var option in codeStyleOptions) + // For each code style option, create a top level code action with nested code actions for every valid option value. + // For example, if the option value is CodeStyleOption, we will have two nested actions, one for 'true' setting and one + // for 'false' setting. If the option value is CodeStyleOption, we will have a nested action for each enum field. + using var _ = ArrayBuilder.GetInstance(out var nestedActions); + var hasMultipleOptions = codeStyleOptions.Length > 1; + foreach (var option in codeStyleOptions) + { + var topLevelAction = GetCodeActionForCodeStyleOption(option, diagnostic, hasMultipleOptions); + if (topLevelAction != null) { - var topLevelAction = GetCodeActionForCodeStyleOption(option, diagnostic, hasMultipleOptions); - if (topLevelAction != null) - { - nestedActions.Add(topLevelAction); - } + nestedActions.Add(topLevelAction); } + } - if (nestedActions.Count != 0) - { - // Wrap actions by another level if the diagnostic ID has multiple associated code style options to reduce clutter. - var resultCodeAction = nestedActions.Count > 1 - ? new TopLevelConfigureCodeStyleOptionCodeAction(diagnostic, nestedActions.ToImmutable()) - : nestedActions.Single(); + if (nestedActions.Count != 0) + { + // Wrap actions by another level if the diagnostic ID has multiple associated code style options to reduce clutter. + var resultCodeAction = nestedActions.Count > 1 + ? new TopLevelConfigureCodeStyleOptionCodeAction(diagnostic, nestedActions.ToImmutable()) + : nestedActions.Single(); - result.Add(new CodeFix(project, resultCodeAction, diagnostic)); - } + result.Add(new CodeFix(project, resultCodeAction, diagnostic)); } + } - return result.ToImmutableAndFree(); + return result.ToImmutableAndFree(); - // Local functions - TopLevelConfigureCodeStyleOptionCodeAction? GetCodeActionForCodeStyleOption(IOption2 option, Diagnostic diagnostic, bool hasMultipleOptions) - { - // Add a code action for every valid value of the given code style option. - // We only support light-bulb configuration of code style options with boolean or enum values. + // Local functions + TopLevelConfigureCodeStyleOptionCodeAction? GetCodeActionForCodeStyleOption(IOption2 option, Diagnostic diagnostic, bool hasMultipleOptions) + { + // Add a code action for every valid value of the given code style option. + // We only support light-bulb configuration of code style options with boolean or enum values. - using var _ = ArrayBuilder.GetInstance(out var nestedActions); + using var _ = ArrayBuilder.GetInstance(out var nestedActions); - // Try to get the parsed editorconfig string representation of the new code style option value - var optionName = option.Definition.ConfigName; - var defaultValue = (ICodeStyleOption?)option.DefaultValue; - Contract.ThrowIfNull(defaultValue); + // Try to get the parsed editorconfig string representation of the new code style option value + var optionName = option.Definition.ConfigName; + var defaultValue = (ICodeStyleOption?)option.DefaultValue; + Contract.ThrowIfNull(defaultValue); - if (defaultValue.Value is bool) + if (defaultValue.Value is bool) + { + foreach (var boolValue in s_boolValues) { - foreach (var boolValue in s_boolValues) - { - AddCodeActionWithOptionValue(defaultValue, boolValue); - } + AddCodeActionWithOptionValue(defaultValue, boolValue); } - else if (defaultValue.Value?.GetType() is Type t && t.IsEnum) + } + else if (defaultValue.Value?.GetType() is Type t && t.IsEnum) + { + foreach (var enumValue in Enum.GetValues(t)) { - foreach (var enumValue in Enum.GetValues(t)) - { - AddCodeActionWithOptionValue(defaultValue, enumValue!); - } + AddCodeActionWithOptionValue(defaultValue, enumValue!); } + } - if (nestedActions.Count > 0) - { - // If this is not a unique code style option for the diagnostic, use the optionName as the code action title. - // In that case, we will already have a containing top level action for the diagnostic. - // Otherwise, use the diagnostic information in the title. - return hasMultipleOptions - ? new TopLevelConfigureCodeStyleOptionCodeAction(optionName, nestedActions.ToImmutable()) - : new TopLevelConfigureCodeStyleOptionCodeAction(diagnostic, nestedActions.ToImmutable()); - } + if (nestedActions.Count > 0) + { + // If this is not a unique code style option for the diagnostic, use the optionName as the code action title. + // In that case, we will already have a containing top level action for the diagnostic. + // Otherwise, use the diagnostic information in the title. + return hasMultipleOptions + ? new TopLevelConfigureCodeStyleOptionCodeAction(optionName, nestedActions.ToImmutable()) + : new TopLevelConfigureCodeStyleOptionCodeAction(diagnostic, nestedActions.ToImmutable()); + } - return null; + return null; - // Local functions - void AddCodeActionWithOptionValue(ICodeStyleOption codeStyleOption, object newValue) - { - // Create a new code style option value with the newValue - var configuredCodeStyleOption = codeStyleOption.WithValue(newValue); - var optionValue = option.Definition.Serializer.Serialize(configuredCodeStyleOption); - - // Add code action to configure the optionValue. - nestedActions.Add( - SolutionChangeAction.Create( - optionValue, - cancellationToken => ConfigurationUpdater.ConfigureCodeStyleOptionAsync(optionName, optionValue, diagnostic, option.IsPerLanguage, project, cancellationToken), - optionValue)); - } + // Local functions + void AddCodeActionWithOptionValue(ICodeStyleOption codeStyleOption, object newValue) + { + // Create a new code style option value with the newValue + var configuredCodeStyleOption = codeStyleOption.WithValue(newValue); + var optionValue = option.Definition.Serializer.Serialize(configuredCodeStyleOption); + + // Add code action to configure the optionValue. + nestedActions.Add( + SolutionChangeAction.Create( + optionValue, + cancellationToken => ConfigurationUpdater.ConfigureCodeStyleOptionAsync(optionName, optionValue, diagnostic, option.IsPerLanguage, project, cancellationToken), + optionValue)); } } } diff --git a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelBulkConfigureSeverityCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelBulkConfigureSeverityCodeAction.cs index 317e4a9527938..00bb92f75adbd 100644 --- a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelBulkConfigureSeverityCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelBulkConfigureSeverityCodeAction.cs @@ -5,18 +5,17 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.CodeActions; -namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureSeverity +namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureSeverity; + +internal sealed partial class ConfigureSeverityLevelCodeFixProvider : IConfigurationFixProvider { - internal sealed partial class ConfigureSeverityLevelCodeFixProvider : IConfigurationFixProvider + private sealed class TopLevelBulkConfigureSeverityCodeAction(ImmutableArray nestedActions, string? category) : AbstractConfigurationActionWithNestedActions(nestedActions, + category != null + ? string.Format(FeaturesResources.Configure_severity_for_all_0_analyzers, category) + : FeaturesResources.Configure_severity_for_all_analyzers) { - private sealed class TopLevelBulkConfigureSeverityCodeAction(ImmutableArray nestedActions, string? category) : AbstractConfigurationActionWithNestedActions(nestedActions, - category != null - ? string.Format(FeaturesResources.Configure_severity_for_all_0_analyzers, category) - : FeaturesResources.Configure_severity_for_all_analyzers) - { - internal override CodeActionPriority AdditionalPriority { get; } = category != null ? CodeActionPriority.Low : CodeActionPriority.Lowest; + internal override CodeActionPriority AdditionalPriority { get; } = category != null ? CodeActionPriority.Low : CodeActionPriority.Lowest; - internal override bool IsBulkConfigurationAction => true; - } + internal override bool IsBulkConfigurationAction => true; } } diff --git a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelConfigureSeverityCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelConfigureSeverityCodeAction.cs index c1fbc8e240eb8..41ce22e1b6c6a 100644 --- a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelConfigureSeverityCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelConfigureSeverityCodeAction.cs @@ -7,12 +7,11 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.CodeActions; -namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureSeverity +namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureSeverity; + +internal sealed partial class ConfigureSeverityLevelCodeFixProvider : IConfigurationFixProvider { - internal sealed partial class ConfigureSeverityLevelCodeFixProvider : IConfigurationFixProvider + private sealed class TopLevelConfigureSeverityCodeAction(Diagnostic diagnostic, ImmutableArray nestedActions) : AbstractConfigurationActionWithNestedActions(nestedActions, string.Format(FeaturesResources.Configure_0_severity, diagnostic.Id)) { - private sealed class TopLevelConfigureSeverityCodeAction(Diagnostic diagnostic, ImmutableArray nestedActions) : AbstractConfigurationActionWithNestedActions(nestedActions, string.Format(FeaturesResources.Configure_0_severity, diagnostic.Id)) - { - } } } diff --git a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.cs b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.cs index 184aa972bffd5..5d8e0d9cc85ee 100644 --- a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.cs +++ b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.cs @@ -15,104 +15,103 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.CodeActions.CodeAction; -namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureSeverity +namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureSeverity; + +[ExportConfigurationFixProvider(PredefinedConfigurationFixProviderNames.ConfigureSeverity, LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] +[ExtensionOrder(After = PredefinedConfigurationFixProviderNames.Suppression)] +internal sealed partial class ConfigureSeverityLevelCodeFixProvider : IConfigurationFixProvider { - [ExportConfigurationFixProvider(PredefinedConfigurationFixProviderNames.ConfigureSeverity, LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] - [ExtensionOrder(After = PredefinedConfigurationFixProviderNames.Suppression)] - internal sealed partial class ConfigureSeverityLevelCodeFixProvider : IConfigurationFixProvider + private static readonly ImmutableArray<(string value, string title)> s_editorConfigSeverityStrings = + [ + (EditorConfigSeverityStrings.None, WorkspacesResources.None), + (EditorConfigSeverityStrings.Silent, FeaturesResources.Silent), + (EditorConfigSeverityStrings.Suggestion, WorkspacesResources.Suggestion), + (EditorConfigSeverityStrings.Warning, WorkspacesResources.Warning), + (EditorConfigSeverityStrings.Error, WorkspacesResources.Error), + ]; + + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public ConfigureSeverityLevelCodeFixProvider() { - private static readonly ImmutableArray<(string value, string title)> s_editorConfigSeverityStrings = - [ - (EditorConfigSeverityStrings.None, WorkspacesResources.None), - (EditorConfigSeverityStrings.Silent, FeaturesResources.Silent), - (EditorConfigSeverityStrings.Suggestion, WorkspacesResources.Suggestion), - (EditorConfigSeverityStrings.Warning, WorkspacesResources.Warning), - (EditorConfigSeverityStrings.Error, WorkspacesResources.Error), - ]; - - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public ConfigureSeverityLevelCodeFixProvider() - { - } + } - // We only offer fix for configurable diagnostics. - // Also skip suppressed diagnostics defensively, though the code fix engine should ideally never call us for suppressed diagnostics. - public bool IsFixableDiagnostic(Diagnostic diagnostic) - => !diagnostic.IsSuppressed && !SuppressionHelpers.IsNotConfigurableDiagnostic(diagnostic); + // We only offer fix for configurable diagnostics. + // Also skip suppressed diagnostics defensively, though the code fix engine should ideally never call us for suppressed diagnostics. + public bool IsFixableDiagnostic(Diagnostic diagnostic) + => !diagnostic.IsSuppressed && !SuppressionHelpers.IsNotConfigurableDiagnostic(diagnostic); - public FixAllProvider? GetFixAllProvider() - => null; + public FixAllProvider? GetFixAllProvider() + => null; - public Task> GetFixesAsync(TextDocument document, TextSpan span, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - => Task.FromResult(GetConfigurations(document.Project, diagnostics, cancellationToken)); + public Task> GetFixesAsync(TextDocument document, TextSpan span, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + => Task.FromResult(GetConfigurations(document.Project, diagnostics, cancellationToken)); - public Task> GetFixesAsync(Project project, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) - => Task.FromResult(GetConfigurations(project, diagnostics, cancellationToken)); + public Task> GetFixesAsync(Project project, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + => Task.FromResult(GetConfigurations(project, diagnostics, cancellationToken)); - private static ImmutableArray GetConfigurations(Project project, IEnumerable diagnostics, CancellationToken cancellationToken) + private static ImmutableArray GetConfigurations(Project project, IEnumerable diagnostics, CancellationToken cancellationToken) + { + var result = ArrayBuilder.GetInstance(); + var analyzerDiagnosticsByCategory = new SortedDictionary>(); + using var disposer = ArrayBuilder.GetInstance(out var analyzerDiagnostics); + foreach (var diagnostic in diagnostics) { - var result = ArrayBuilder.GetInstance(); - var analyzerDiagnosticsByCategory = new SortedDictionary>(); - using var disposer = ArrayBuilder.GetInstance(out var analyzerDiagnostics); - foreach (var diagnostic in diagnostics) + var nestedActions = ArrayBuilder.GetInstance(); + foreach (var (value, title) in s_editorConfigSeverityStrings) { - var nestedActions = ArrayBuilder.GetInstance(); - foreach (var (value, title) in s_editorConfigSeverityStrings) - { - nestedActions.Add( - SolutionChangeAction.Create( - title, - cancellationToken => ConfigurationUpdater.ConfigureSeverityAsync(value, diagnostic, project, cancellationToken), - value)); - } + nestedActions.Add( + SolutionChangeAction.Create( + title, + cancellationToken => ConfigurationUpdater.ConfigureSeverityAsync(value, diagnostic, project, cancellationToken), + value)); + } - var codeAction = new TopLevelConfigureSeverityCodeAction(diagnostic, nestedActions.ToImmutableAndFree()); - result.Add(new CodeFix(project, codeAction, diagnostic)); + var codeAction = new TopLevelConfigureSeverityCodeAction(diagnostic, nestedActions.ToImmutableAndFree()); + result.Add(new CodeFix(project, codeAction, diagnostic)); - // Bulk configuration is only supported for analyzer diagnostics. - if (!SuppressionHelpers.IsCompilerDiagnostic(diagnostic)) + // Bulk configuration is only supported for analyzer diagnostics. + if (!SuppressionHelpers.IsCompilerDiagnostic(diagnostic)) + { + // Ensure diagnostic has a valid non-empty 'Category' for category based configuration. + if (!string.IsNullOrEmpty(diagnostic.Descriptor.Category)) { - // Ensure diagnostic has a valid non-empty 'Category' for category based configuration. - if (!string.IsNullOrEmpty(diagnostic.Descriptor.Category)) - { - var diagnosticsForCategory = analyzerDiagnosticsByCategory.GetOrAdd(diagnostic.Descriptor.Category, _ => ArrayBuilder.GetInstance()); - diagnosticsForCategory.Add(diagnostic); - } - - analyzerDiagnostics.Add(diagnostic); + var diagnosticsForCategory = analyzerDiagnosticsByCategory.GetOrAdd(diagnostic.Descriptor.Category, _ => ArrayBuilder.GetInstance()); + diagnosticsForCategory.Add(diagnostic); } - } - foreach (var (category, diagnosticsWithCategory) in analyzerDiagnosticsByCategory) - { - AddBulkConfigurationCodeFixes(diagnosticsWithCategory.ToImmutableAndFree(), category); + analyzerDiagnostics.Add(diagnostic); } + } - if (analyzerDiagnostics.Count > 0) - { - AddBulkConfigurationCodeFixes(analyzerDiagnostics.ToImmutable(), category: null); - } + foreach (var (category, diagnosticsWithCategory) in analyzerDiagnosticsByCategory) + { + AddBulkConfigurationCodeFixes(diagnosticsWithCategory.ToImmutableAndFree(), category); + } + + if (analyzerDiagnostics.Count > 0) + { + AddBulkConfigurationCodeFixes(analyzerDiagnostics.ToImmutable(), category: null); + } - return result.ToImmutableAndFree(); + return result.ToImmutableAndFree(); - void AddBulkConfigurationCodeFixes(ImmutableArray diagnostics, string? category) + void AddBulkConfigurationCodeFixes(ImmutableArray diagnostics, string? category) + { + var nestedActions = ArrayBuilder.GetInstance(); + foreach (var (value, title) in s_editorConfigSeverityStrings) { - var nestedActions = ArrayBuilder.GetInstance(); - foreach (var (value, title) in s_editorConfigSeverityStrings) - { - nestedActions.Add( - SolutionChangeAction.Create( - title, - solution => category != null - ? ConfigurationUpdater.BulkConfigureSeverityAsync(value, category, project, cancellationToken) - : ConfigurationUpdater.BulkConfigureSeverityAsync(value, project, cancellationToken), - value)); - } - - var codeAction = new TopLevelBulkConfigureSeverityCodeAction(nestedActions.ToImmutableAndFree(), category); - result.Add(new CodeFix(project, codeAction, diagnostics)); + nestedActions.Add( + SolutionChangeAction.Create( + title, + solution => category != null + ? ConfigurationUpdater.BulkConfigureSeverityAsync(value, category, project, cancellationToken) + : ConfigurationUpdater.BulkConfigureSeverityAsync(value, project, cancellationToken), + value)); } + + var codeAction = new TopLevelBulkConfigureSeverityCodeAction(nestedActions.ToImmutableAndFree(), category); + result.Add(new CodeFix(project, codeAction, diagnostics)); } } } diff --git a/src/Features/Core/Portable/CodeFixes/DiagnosticExtensions.cs b/src/Features/Core/Portable/CodeFixes/DiagnosticExtensions.cs index f01788ccef5d9..d9e2f0644eabf 100644 --- a/src/Features/Core/Portable/CodeFixes/DiagnosticExtensions.cs +++ b/src/Features/Core/Portable/CodeFixes/DiagnosticExtensions.cs @@ -5,15 +5,14 @@ using Microsoft.CodeAnalysis.CodeFixes.Suppression; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.CodeFixes +namespace Microsoft.CodeAnalysis.CodeFixes; + +internal static class DiagnosticExtensions { - internal static class DiagnosticExtensions + public static bool IsMoreSevereThanOrEqualTo(this DiagnosticSeverity left, DiagnosticSeverity right) { - public static bool IsMoreSevereThanOrEqualTo(this DiagnosticSeverity left, DiagnosticSeverity right) - { - var leftInt = (int)left; - var rightInt = (int)right; - return leftInt >= rightInt; - } + var leftInt = (int)left; + var rightInt = (int)right; + return leftInt >= rightInt; } } diff --git a/src/Features/Core/Portable/CodeFixes/FirstFixResult.cs b/src/Features/Core/Portable/CodeFixes/FirstFixResult.cs index a4e2c2d965e43..19f4fd158539f 100644 --- a/src/Features/Core/Portable/CodeFixes/FirstFixResult.cs +++ b/src/Features/Core/Portable/CodeFixes/FirstFixResult.cs @@ -4,14 +4,13 @@ using System.Diagnostics.CodeAnalysis; -namespace Microsoft.CodeAnalysis.CodeFixes +namespace Microsoft.CodeAnalysis.CodeFixes; + +internal readonly struct FirstFixResult(bool upToDate, CodeFixCollection? codeFixCollection) { - internal readonly struct FirstFixResult(bool upToDate, CodeFixCollection? codeFixCollection) - { - public readonly bool UpToDate = upToDate; - public readonly CodeFixCollection? CodeFixCollection = codeFixCollection; + public readonly bool UpToDate = upToDate; + public readonly CodeFixCollection? CodeFixCollection = codeFixCollection; - [MemberNotNullWhen(true, nameof(CodeFixCollection))] - public bool HasFix => CodeFixCollection != null; - } + [MemberNotNullWhen(true, nameof(CodeFixCollection))] + public bool HasFix => CodeFixCollection != null; } diff --git a/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/AbstractFixAllCodeFixCodeAction.cs b/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/AbstractFixAllCodeFixCodeAction.cs index 325d42c71271f..1f087c2d009d2 100644 --- a/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/AbstractFixAllCodeFixCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/AbstractFixAllCodeFixCodeAction.cs @@ -9,51 +9,50 @@ using System.Threading; using Microsoft.CodeAnalysis.CodeFixesAndRefactorings; -namespace Microsoft.CodeAnalysis.CodeFixes +namespace Microsoft.CodeAnalysis.CodeFixes; + +/// +/// Fix all code action for a code action registered by a . +/// +internal abstract class AbstractFixAllCodeFixCodeAction : AbstractFixAllCodeAction { - /// - /// Fix all code action for a code action registered by a . - /// - internal abstract class AbstractFixAllCodeFixCodeAction : AbstractFixAllCodeAction + private static readonly HashSet s_predefinedCodeFixProviderNames = GetPredefinedCodeFixProviderNames(); + + protected AbstractFixAllCodeFixCodeAction( + IFixAllState fixAllState, bool showPreviewChangesDialog) + : base(fixAllState, showPreviewChangesDialog) { - private static readonly HashSet s_predefinedCodeFixProviderNames = GetPredefinedCodeFixProviderNames(); + } + + protected sealed override IFixAllContext CreateFixAllContext(IFixAllState fixAllState, IProgress progressTracker, CancellationToken cancellationToken) + => new FixAllContext((FixAllState)fixAllState, progressTracker, cancellationToken); - protected AbstractFixAllCodeFixCodeAction( - IFixAllState fixAllState, bool showPreviewChangesDialog) - : base(fixAllState, showPreviewChangesDialog) + protected sealed override bool IsInternalProvider(IFixAllState fixAllState) + { + var exportAttributes = fixAllState.Provider.GetType().GetTypeInfo().GetCustomAttributes(typeof(ExportCodeFixProviderAttribute), false); + if (exportAttributes?.Any() == true) { + var exportAttribute = (ExportCodeFixProviderAttribute)exportAttributes.First(); + return !string.IsNullOrEmpty(exportAttribute.Name) + && s_predefinedCodeFixProviderNames.Contains(exportAttribute.Name); } - protected sealed override IFixAllContext CreateFixAllContext(IFixAllState fixAllState, IProgress progressTracker, CancellationToken cancellationToken) - => new FixAllContext((FixAllState)fixAllState, progressTracker, cancellationToken); - - protected sealed override bool IsInternalProvider(IFixAllState fixAllState) - { - var exportAttributes = fixAllState.Provider.GetType().GetTypeInfo().GetCustomAttributes(typeof(ExportCodeFixProviderAttribute), false); - if (exportAttributes?.Any() == true) - { - var exportAttribute = (ExportCodeFixProviderAttribute)exportAttributes.First(); - return !string.IsNullOrEmpty(exportAttribute.Name) - && s_predefinedCodeFixProviderNames.Contains(exportAttribute.Name); - } + return false; + } - return false; - } + private static HashSet GetPredefinedCodeFixProviderNames() + { + var names = new HashSet(); - private static HashSet GetPredefinedCodeFixProviderNames() + var fields = typeof(PredefinedCodeFixProviderNames).GetTypeInfo().DeclaredFields; + foreach (var field in fields) { - var names = new HashSet(); - - var fields = typeof(PredefinedCodeFixProviderNames).GetTypeInfo().DeclaredFields; - foreach (var field in fields) + if (field.IsStatic) { - if (field.IsStatic) - { - names.Add((string)field.GetValue(null)!); - } + names.Add((string)field.GetValue(null)!); } - - return names; } + + return names; } } diff --git a/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/FixMultipleCodeAction.cs b/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/FixMultipleCodeAction.cs index a9a5d5be3a9a2..50f7f1eec0767 100644 --- a/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/FixMultipleCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/FixMultipleCodeAction.cs @@ -6,18 +6,17 @@ using Microsoft.CodeAnalysis.CodeFixesAndRefactorings; -namespace Microsoft.CodeAnalysis.CodeFixes +namespace Microsoft.CodeAnalysis.CodeFixes; + +internal partial class FixMultipleCodeAction( + IFixAllState fixAllState, + string title, + string computingFixWaitDialogMessage) : AbstractFixAllCodeFixCodeAction(fixAllState, showPreviewChangesDialog: false) { - internal partial class FixMultipleCodeAction( - IFixAllState fixAllState, - string title, - string computingFixWaitDialogMessage) : AbstractFixAllCodeFixCodeAction(fixAllState, showPreviewChangesDialog: false) - { - private readonly string _title = title; - private readonly string _computingFixWaitDialogMessage = computingFixWaitDialogMessage; + private readonly string _title = title; + private readonly string _computingFixWaitDialogMessage = computingFixWaitDialogMessage; - public override string Title => _title; + public override string Title => _title; - internal override string Message => _computingFixWaitDialogMessage; - } + internal override string Message => _computingFixWaitDialogMessage; } diff --git a/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/IFixMultipleOccurrencesService.cs b/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/IFixMultipleOccurrencesService.cs index 2548e21154ba4..20fba9729c20a 100644 --- a/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/IFixMultipleOccurrencesService.cs +++ b/src/Features/Core/Portable/CodeFixes/FixAllOccurrences/IFixMultipleOccurrencesService.cs @@ -10,40 +10,39 @@ using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.CodeFixes +namespace Microsoft.CodeAnalysis.CodeFixes; + +internal interface IFixMultipleOccurrencesService : IWorkspaceService { - internal interface IFixMultipleOccurrencesService : IWorkspaceService - { - /// - /// Get the fix multiple occurrences code fix for the given diagnostics with source locations. - /// NOTE: This method does not apply the fix to the workspace. - /// - Solution GetFix( - ImmutableDictionary> diagnosticsToFix, - Workspace workspace, - CodeFixProvider fixProvider, - FixAllProvider fixAllProvider, - CodeActionOptionsProvider optionsProvider, - string equivalenceKey, - string waitDialogTitle, - string waitDialogMessage, - IProgress progressTracker, - CancellationToken cancellationToken); + /// + /// Get the fix multiple occurrences code fix for the given diagnostics with source locations. + /// NOTE: This method does not apply the fix to the workspace. + /// + Solution GetFix( + ImmutableDictionary> diagnosticsToFix, + Workspace workspace, + CodeFixProvider fixProvider, + FixAllProvider fixAllProvider, + CodeActionOptionsProvider optionsProvider, + string equivalenceKey, + string waitDialogTitle, + string waitDialogMessage, + IProgress progressTracker, + CancellationToken cancellationToken); - /// - /// Get the fix multiple occurrences code fix for the given diagnostics with source locations. - /// NOTE: This method does not apply the fix to the workspace. - /// - Solution GetFix( - ImmutableDictionary> diagnosticsToFix, - Workspace workspace, - CodeFixProvider fixProvider, - FixAllProvider fixAllProvider, - CodeActionOptionsProvider optionsProvider, - string equivalenceKey, - string waitDialogTitle, - string waitDialogMessage, - IProgress progressTracker, - CancellationToken cancellationToken); - } + /// + /// Get the fix multiple occurrences code fix for the given diagnostics with source locations. + /// NOTE: This method does not apply the fix to the workspace. + /// + Solution GetFix( + ImmutableDictionary> diagnosticsToFix, + Workspace workspace, + CodeFixProvider fixProvider, + FixAllProvider fixAllProvider, + CodeActionOptionsProvider optionsProvider, + string equivalenceKey, + string waitDialogTitle, + string waitDialogMessage, + IProgress progressTracker, + CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/CodeFixes/GenerateMember/AbstractGenerateMemberCodeFixProvider.cs b/src/Features/Core/Portable/CodeFixes/GenerateMember/AbstractGenerateMemberCodeFixProvider.cs index 0ffade9ee20b9..baac9918047b7 100644 --- a/src/Features/Core/Portable/CodeFixes/GenerateMember/AbstractGenerateMemberCodeFixProvider.cs +++ b/src/Features/Core/Portable/CodeFixes/GenerateMember/AbstractGenerateMemberCodeFixProvider.cs @@ -14,82 +14,81 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.GenerateMember +namespace Microsoft.CodeAnalysis.CodeFixes.GenerateMember; + +internal abstract class AbstractGenerateMemberCodeFixProvider : CodeFixProvider { - internal abstract class AbstractGenerateMemberCodeFixProvider : CodeFixProvider + public override FixAllProvider? GetFixAllProvider() + { + // Fix All is not supported by this code fix + return null; + } + + protected abstract Task> GetCodeActionsAsync(Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken); + protected abstract bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic); + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { - public override FixAllProvider? GetFixAllProvider() + // TODO: https://github.com/dotnet/roslyn/issues/5777 + // Not supported in REPL for now. + if (context.Document.Project.IsSubmission) { - // Fix All is not supported by this code fix - return null; + return; } - protected abstract Task> GetCodeActionsAsync(Document document, SyntaxNode node, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken); - protected abstract bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic); + var diagnostic = context.Diagnostics.First(); + var document = context.Document; + var syntaxFacts = document.GetRequiredLanguageService(); - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + var root = await document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var names = GetTargetNodes(syntaxFacts, root, context.Span, diagnostic); + foreach (var name in names) { - // TODO: https://github.com/dotnet/roslyn/issues/5777 - // Not supported in REPL for now. - if (context.Document.Project.IsSubmission) + var codeActions = await GetCodeActionsAsync(context.Document, name, context.Options, context.CancellationToken).ConfigureAwait(false); + if (codeActions.IsDefaultOrEmpty) { - return; + continue; } - var diagnostic = context.Diagnostics.First(); - var document = context.Document; - var syntaxFacts = document.GetRequiredLanguageService(); - - var root = await document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var names = GetTargetNodes(syntaxFacts, root, context.Span, diagnostic); - foreach (var name in names) - { - var codeActions = await GetCodeActionsAsync(context.Document, name, context.Options, context.CancellationToken).ConfigureAwait(false); - if (codeActions.IsDefaultOrEmpty) - { - continue; - } - - context.RegisterFixes(codeActions, context.Diagnostics); - return; - } + context.RegisterFixes(codeActions, context.Diagnostics); + return; } + } - protected virtual SyntaxNode? GetTargetNode(SyntaxNode node) - => node; + protected virtual SyntaxNode? GetTargetNode(SyntaxNode node) + => node; - private IEnumerable GetTargetNodes( - ISyntaxFactsService syntaxFacts, SyntaxNode root, - TextSpan span, Diagnostic diagnostic) + private IEnumerable GetTargetNodes( + ISyntaxFactsService syntaxFacts, SyntaxNode root, + TextSpan span, Diagnostic diagnostic) + { + var token = root.FindToken(span.Start); + if (token.Span.IntersectsWith(span)) { - var token = root.FindToken(span.Start); - if (token.Span.IntersectsWith(span)) + var first = true; + foreach (var ancestor in token.GetAncestors()) { - var first = true; - foreach (var ancestor in token.GetAncestors()) + // If we're crossing a local function/lambda point then stop looking higher. We've clearly gone past + // the point of the original diagnostic and should not consider this node as something to consider. + // + // Note: it's ok if we are on a lambda that was the direct node with the diagnostic (i.e. if the + // compiler was reporting a diagnostic on a lambda itself). However, once we start walking upwards, + // we don't want to cross a lambda. + if (!first && + syntaxFacts.IsAnonymousOrLocalFunction(ancestor) && + ancestor.SpanStart < token.SpanStart) { - // If we're crossing a local function/lambda point then stop looking higher. We've clearly gone past - // the point of the original diagnostic and should not consider this node as something to consider. - // - // Note: it's ok if we are on a lambda that was the direct node with the diagnostic (i.e. if the - // compiler was reporting a diagnostic on a lambda itself). However, once we start walking upwards, - // we don't want to cross a lambda. - if (!first && - syntaxFacts.IsAnonymousOrLocalFunction(ancestor) && - ancestor.SpanStart < token.SpanStart) - { - break; - } + break; + } - first = false; - if (!IsCandidate(ancestor, token, diagnostic)) - continue; + first = false; + if (!IsCandidate(ancestor, token, diagnostic)) + continue; - var name = GetTargetNode(ancestor); + var name = GetTargetNode(ancestor); - if (name != null) - yield return name; - } + if (name != null) + yield return name; } } } diff --git a/src/Features/Core/Portable/CodeFixes/ICodeFixProviderFactory.cs b/src/Features/Core/Portable/CodeFixes/ICodeFixProviderFactory.cs index b8023aebb36c1..d4f2b739a7742 100644 --- a/src/Features/Core/Portable/CodeFixes/ICodeFixProviderFactory.cs +++ b/src/Features/Core/Portable/CodeFixes/ICodeFixProviderFactory.cs @@ -6,13 +6,12 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.CodeFixes +namespace Microsoft.CodeAnalysis.CodeFixes; + +/// +/// CodeFixProvider factory. if an analyzer reference implements this, we call this to get CodeFixProviders +/// +internal interface ICodeFixProviderFactory { - /// - /// CodeFixProvider factory. if an analyzer reference implements this, we call this to get CodeFixProviders - /// - internal interface ICodeFixProviderFactory - { - ImmutableArray GetFixers(); - } + ImmutableArray GetFixers(); } diff --git a/src/Features/Core/Portable/CodeFixes/RoslynAssemblyHelper.cs b/src/Features/Core/Portable/CodeFixes/RoslynAssemblyHelper.cs index 8a269207cdb56..8d0ab847dfe83 100644 --- a/src/Features/Core/Portable/CodeFixes/RoslynAssemblyHelper.cs +++ b/src/Features/Core/Portable/CodeFixes/RoslynAssemblyHelper.cs @@ -7,12 +7,11 @@ using System.Linq; using System.Reflection; -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +internal static class RoslynAssemblyHelper { - internal static class RoslynAssemblyHelper - { - public static bool HasRoslynPublicKey(object source) - => source.GetType().GetTypeInfo().Assembly.GetName().GetPublicKey().SequenceEqual( - typeof(RoslynAssemblyHelper).GetTypeInfo().Assembly.GetName().GetPublicKey()); - } + public static bool HasRoslynPublicKey(object source) + => source.GetType().GetTypeInfo().Assembly.GetName().GetPublicKey().SequenceEqual( + typeof(RoslynAssemblyHelper).GetTypeInfo().Assembly.GetName().GetPublicKey()); } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionBatchFixAllProvider.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionBatchFixAllProvider.cs index 3d9642a07d3c2..9bb5419aa19c0 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionBatchFixAllProvider.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionBatchFixAllProvider.cs @@ -19,388 +19,387 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +/// +/// Helper class for "Fix all occurrences" code fix providers. +/// +internal abstract class AbstractSuppressionBatchFixAllProvider : FixAllProvider { - /// - /// Helper class for "Fix all occurrences" code fix providers. - /// - internal abstract class AbstractSuppressionBatchFixAllProvider : FixAllProvider + public override async Task GetFixAsync(FixAllContext fixAllContext) { - public override async Task GetFixAsync(FixAllContext fixAllContext) + if (fixAllContext.Document != null) { - if (fixAllContext.Document != null) - { - var documentsAndDiagnosticsToFixMap = await fixAllContext.GetDocumentDiagnosticsToFixAsync().ConfigureAwait(false); - return await GetFixAsync(documentsAndDiagnosticsToFixMap, fixAllContext).ConfigureAwait(false); - } - else - { - var projectsAndDiagnosticsToFixMap = await fixAllContext.GetProjectDiagnosticsToFixAsync().ConfigureAwait(false); - return await GetFixAsync(projectsAndDiagnosticsToFixMap, fixAllContext).ConfigureAwait(false); - } + var documentsAndDiagnosticsToFixMap = await fixAllContext.GetDocumentDiagnosticsToFixAsync().ConfigureAwait(false); + return await GetFixAsync(documentsAndDiagnosticsToFixMap, fixAllContext).ConfigureAwait(false); + } + else + { + var projectsAndDiagnosticsToFixMap = await fixAllContext.GetProjectDiagnosticsToFixAsync().ConfigureAwait(false); + return await GetFixAsync(projectsAndDiagnosticsToFixMap, fixAllContext).ConfigureAwait(false); } + } - private async Task GetFixAsync( - ImmutableDictionary> documentsAndDiagnosticsToFixMap, - FixAllContext fixAllContext) + private async Task GetFixAsync( + ImmutableDictionary> documentsAndDiagnosticsToFixMap, + FixAllContext fixAllContext) + { + var cancellationToken = fixAllContext.CancellationToken; + if (documentsAndDiagnosticsToFixMap?.Any() == true) { - var cancellationToken = fixAllContext.CancellationToken; - if (documentsAndDiagnosticsToFixMap?.Any() == true) - { - var progressTracker = fixAllContext.Progress; - progressTracker.Report(CodeAnalysisProgress.Description(fixAllContext.GetDefaultFixAllTitle())); + var progressTracker = fixAllContext.Progress; + progressTracker.Report(CodeAnalysisProgress.Description(fixAllContext.GetDefaultFixAllTitle())); - var fixAllState = fixAllContext.State; - FixAllLogger.LogDiagnosticsStats(fixAllState.CorrelationId, documentsAndDiagnosticsToFixMap); + var fixAllState = fixAllContext.State; + FixAllLogger.LogDiagnosticsStats(fixAllState.CorrelationId, documentsAndDiagnosticsToFixMap); - var diagnosticsAndCodeActions = await GetDiagnosticsAndCodeActionsAsync(documentsAndDiagnosticsToFixMap, fixAllContext).ConfigureAwait(false); + var diagnosticsAndCodeActions = await GetDiagnosticsAndCodeActionsAsync(documentsAndDiagnosticsToFixMap, fixAllContext).ConfigureAwait(false); - if (diagnosticsAndCodeActions.Length > 0) + if (diagnosticsAndCodeActions.Length > 0) + { + var functionId = FunctionId.CodeFixes_FixAllOccurrencesComputation_Document_Merge; + using (Logger.LogBlock(functionId, FixAllLogger.CreateCorrelationLogMessage(fixAllState.CorrelationId), cancellationToken)) { - var functionId = FunctionId.CodeFixes_FixAllOccurrencesComputation_Document_Merge; - using (Logger.LogBlock(functionId, FixAllLogger.CreateCorrelationLogMessage(fixAllState.CorrelationId), cancellationToken)) - { - FixAllLogger.LogFixesToMergeStats(functionId, fixAllState.CorrelationId, diagnosticsAndCodeActions.Length); - return await TryGetMergedFixAsync( - diagnosticsAndCodeActions, fixAllState, progressTracker, cancellationToken).ConfigureAwait(false); - } + FixAllLogger.LogFixesToMergeStats(functionId, fixAllState.CorrelationId, diagnosticsAndCodeActions.Length); + return await TryGetMergedFixAsync( + diagnosticsAndCodeActions, fixAllState, progressTracker, cancellationToken).ConfigureAwait(false); } } - - return null; } - private async Task> GetDiagnosticsAndCodeActionsAsync( - ImmutableDictionary> documentsAndDiagnosticsToFixMap, - FixAllContext fixAllContext) - { - var cancellationToken = fixAllContext.CancellationToken; - var fixAllState = fixAllContext.State; - var fixesBag = new ConcurrentBag<(Diagnostic diagnostic, CodeAction action)>(); - - using (Logger.LogBlock( - FunctionId.CodeFixes_FixAllOccurrencesComputation_Document_Fixes, - FixAllLogger.CreateCorrelationLogMessage(fixAllState.CorrelationId), - cancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - var progressTracker = fixAllContext.Progress; + return null; + } - using var _1 = ArrayBuilder.GetInstance(out var tasks); - using var _2 = ArrayBuilder.GetInstance(out var documentsToFix); + private async Task> GetDiagnosticsAndCodeActionsAsync( + ImmutableDictionary> documentsAndDiagnosticsToFixMap, + FixAllContext fixAllContext) + { + var cancellationToken = fixAllContext.CancellationToken; + var fixAllState = fixAllContext.State; + var fixesBag = new ConcurrentBag<(Diagnostic diagnostic, CodeAction action)>(); + + using (Logger.LogBlock( + FunctionId.CodeFixes_FixAllOccurrencesComputation_Document_Fixes, + FixAllLogger.CreateCorrelationLogMessage(fixAllState.CorrelationId), + cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + var progressTracker = fixAllContext.Progress; - // Determine the set of documents to actually fix. We can also use this to update the progress bar with - // the amount of remaining work to perform. We'll update the progress bar as we compute each fix in - // AddDocumentFixesAsync. - foreach (var (document, diagnosticsToFix) in documentsAndDiagnosticsToFixMap) - { - if (!diagnosticsToFix.IsDefaultOrEmpty) - documentsToFix.Add(document); - } + using var _1 = ArrayBuilder.GetInstance(out var tasks); + using var _2 = ArrayBuilder.GetInstance(out var documentsToFix); - progressTracker.AddItems(documentsToFix.Count); + // Determine the set of documents to actually fix. We can also use this to update the progress bar with + // the amount of remaining work to perform. We'll update the progress bar as we compute each fix in + // AddDocumentFixesAsync. + foreach (var (document, diagnosticsToFix) in documentsAndDiagnosticsToFixMap) + { + if (!diagnosticsToFix.IsDefaultOrEmpty) + documentsToFix.Add(document); + } - foreach (var document in documentsToFix) - { - var diagnosticsToFix = documentsAndDiagnosticsToFixMap[document]; - tasks.Add(AddDocumentFixesAsync( - document, diagnosticsToFix, fixesBag, fixAllState, progressTracker, cancellationToken)); - } + progressTracker.AddItems(documentsToFix.Count); - await Task.WhenAll(tasks).ConfigureAwait(false); + foreach (var document in documentsToFix) + { + var diagnosticsToFix = documentsAndDiagnosticsToFixMap[document]; + tasks.Add(AddDocumentFixesAsync( + document, diagnosticsToFix, fixesBag, fixAllState, progressTracker, cancellationToken)); } - return fixesBag.ToImmutableArray(); + await Task.WhenAll(tasks).ConfigureAwait(false); } - private async Task AddDocumentFixesAsync( - Document document, ImmutableArray diagnostics, - ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> fixes, - FixAllState fixAllState, IProgress progressTracker, CancellationToken cancellationToken) + return fixesBag.ToImmutableArray(); + } + + private async Task AddDocumentFixesAsync( + Document document, ImmutableArray diagnostics, + ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> fixes, + FixAllState fixAllState, IProgress progressTracker, CancellationToken cancellationToken) + { + try { - try - { - await this.AddDocumentFixesAsync(document, diagnostics, fixes, fixAllState, cancellationToken).ConfigureAwait(false); - } - finally - { - progressTracker.ItemCompleted(); - } + await this.AddDocumentFixesAsync(document, diagnostics, fixes, fixAllState, cancellationToken).ConfigureAwait(false); } - - protected virtual async Task AddDocumentFixesAsync( - Document document, ImmutableArray diagnostics, - ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> fixes, - FixAllState fixAllState, CancellationToken cancellationToken) + finally { - Debug.Assert(!diagnostics.IsDefault); - cancellationToken.ThrowIfCancellationRequested(); + progressTracker.ItemCompleted(); + } + } - var registerCodeFix = GetRegisterCodeFixAction(fixAllState, fixes); + protected virtual async Task AddDocumentFixesAsync( + Document document, ImmutableArray diagnostics, + ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> fixes, + FixAllState fixAllState, CancellationToken cancellationToken) + { + Debug.Assert(!diagnostics.IsDefault); + cancellationToken.ThrowIfCancellationRequested(); - var fixerTasks = new List(); - foreach (var diagnostic in diagnostics) - { - cancellationToken.ThrowIfCancellationRequested(); - fixerTasks.Add(Task.Run(() => - { - var context = new CodeFixContext(document, diagnostic, registerCodeFix, cancellationToken); + var registerCodeFix = GetRegisterCodeFixAction(fixAllState, fixes); - // TODO: Wrap call to ComputeFixesAsync() below in IExtensionManager.PerformFunctionAsync() so that - // a buggy extension that throws can't bring down the host? - return fixAllState.Provider.RegisterCodeFixesAsync(context) ?? Task.CompletedTask; - }, cancellationToken)); - } + var fixerTasks = new List(); + foreach (var diagnostic in diagnostics) + { + cancellationToken.ThrowIfCancellationRequested(); + fixerTasks.Add(Task.Run(() => + { + var context = new CodeFixContext(document, diagnostic, registerCodeFix, cancellationToken); - await Task.WhenAll(fixerTasks).ConfigureAwait(false); + // TODO: Wrap call to ComputeFixesAsync() below in IExtensionManager.PerformFunctionAsync() so that + // a buggy extension that throws can't bring down the host? + return fixAllState.Provider.RegisterCodeFixesAsync(context) ?? Task.CompletedTask; + }, cancellationToken)); } - private async Task GetFixAsync( - ImmutableDictionary> projectsAndDiagnosticsToFixMap, - FixAllContext fixAllContext) + await Task.WhenAll(fixerTasks).ConfigureAwait(false); + } + + private async Task GetFixAsync( + ImmutableDictionary> projectsAndDiagnosticsToFixMap, + FixAllContext fixAllContext) + { + var cancellationToken = fixAllContext.CancellationToken; + var fixAllState = fixAllContext.State; + var progressTracker = fixAllContext.Progress; + + if (projectsAndDiagnosticsToFixMap != null && projectsAndDiagnosticsToFixMap.Any()) { - var cancellationToken = fixAllContext.CancellationToken; - var fixAllState = fixAllContext.State; - var progressTracker = fixAllContext.Progress; + FixAllLogger.LogDiagnosticsStats(fixAllState.CorrelationId, projectsAndDiagnosticsToFixMap); - if (projectsAndDiagnosticsToFixMap != null && projectsAndDiagnosticsToFixMap.Any()) + var bag = new ConcurrentBag<(Diagnostic diagnostic, CodeAction action)>(); + using (Logger.LogBlock( + FunctionId.CodeFixes_FixAllOccurrencesComputation_Project_Fixes, + FixAllLogger.CreateCorrelationLogMessage(fixAllState.CorrelationId), + cancellationToken)) { - FixAllLogger.LogDiagnosticsStats(fixAllState.CorrelationId, projectsAndDiagnosticsToFixMap); - - var bag = new ConcurrentBag<(Diagnostic diagnostic, CodeAction action)>(); - using (Logger.LogBlock( - FunctionId.CodeFixes_FixAllOccurrencesComputation_Project_Fixes, - FixAllLogger.CreateCorrelationLogMessage(fixAllState.CorrelationId), - cancellationToken)) - { - var projects = projectsAndDiagnosticsToFixMap.Keys; - var tasks = projects.Select(p => AddProjectFixesAsync( - p, projectsAndDiagnosticsToFixMap[p], bag, fixAllState, cancellationToken)).ToArray(); + var projects = projectsAndDiagnosticsToFixMap.Keys; + var tasks = projects.Select(p => AddProjectFixesAsync( + p, projectsAndDiagnosticsToFixMap[p], bag, fixAllState, cancellationToken)).ToArray(); - await Task.WhenAll(tasks).ConfigureAwait(false); - } + await Task.WhenAll(tasks).ConfigureAwait(false); + } - var result = bag.ToImmutableArray(); - if (result.Length > 0) + var result = bag.ToImmutableArray(); + if (result.Length > 0) + { + var functionId = FunctionId.CodeFixes_FixAllOccurrencesComputation_Project_Merge; + using (Logger.LogBlock(functionId, cancellationToken)) { - var functionId = FunctionId.CodeFixes_FixAllOccurrencesComputation_Project_Merge; - using (Logger.LogBlock(functionId, cancellationToken)) - { - FixAllLogger.LogFixesToMergeStats(functionId, fixAllState.CorrelationId, result.Length); - return await TryGetMergedFixAsync( - result, fixAllState, progressTracker, cancellationToken).ConfigureAwait(false); - } + FixAllLogger.LogFixesToMergeStats(functionId, fixAllState.CorrelationId, result.Length); + return await TryGetMergedFixAsync( + result, fixAllState, progressTracker, cancellationToken).ConfigureAwait(false); } } - - return null; } - private static Action> GetRegisterCodeFixAction( - FixAllState fixAllState, - ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> result) + return null; + } + + private static Action> GetRegisterCodeFixAction( + FixAllState fixAllState, + ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> result) + { + return (action, diagnostics) => { - return (action, diagnostics) => + using var _ = ArrayBuilder.GetInstance(out var builder); + builder.Push(action); + while (builder.Count > 0) { - using var _ = ArrayBuilder.GetInstance(out var builder); - builder.Push(action); - while (builder.Count > 0) + var currentAction = builder.Pop(); + if (currentAction is { EquivalenceKey: var equivalenceKey } + && equivalenceKey == fixAllState.CodeActionEquivalenceKey) { - var currentAction = builder.Pop(); - if (currentAction is { EquivalenceKey: var equivalenceKey } - && equivalenceKey == fixAllState.CodeActionEquivalenceKey) - { - result.Add((diagnostics.First(), currentAction)); - } - - foreach (var nestedAction in currentAction.NestedActions) - { - builder.Push(nestedAction); - } + result.Add((diagnostics.First(), currentAction)); } - }; - } - protected virtual Task AddProjectFixesAsync( - Project project, ImmutableArray diagnostics, - ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> fixes, - FixAllState fixAllState, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + foreach (var nestedAction in currentAction.NestedActions) + { + builder.Push(nestedAction); + } + } + }; + } - public virtual async Task TryGetMergedFixAsync( - ImmutableArray<(Diagnostic diagnostic, CodeAction action)> batchOfFixes, - FixAllState fixAllState, IProgress progressTracker, CancellationToken cancellationToken) - { - Contract.ThrowIfFalse(batchOfFixes.Any()); + protected virtual Task AddProjectFixesAsync( + Project project, ImmutableArray diagnostics, + ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> fixes, + FixAllState fixAllState, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } - var solution = fixAllState.Solution; - var newSolution = await TryMergeFixesAsync( - solution, batchOfFixes, progressTracker, cancellationToken).ConfigureAwait(false); - if (newSolution != null && newSolution != solution) - { - var title = FixAllHelper.GetDefaultFixAllTitle(fixAllState.Scope, title: fixAllState.DiagnosticIds.First(), fixAllState.Document!, fixAllState.Project); - return CodeAction.SolutionChangeAction.Create(title, _ => Task.FromResult(newSolution), title); - } + public virtual async Task TryGetMergedFixAsync( + ImmutableArray<(Diagnostic diagnostic, CodeAction action)> batchOfFixes, + FixAllState fixAllState, IProgress progressTracker, CancellationToken cancellationToken) + { + Contract.ThrowIfFalse(batchOfFixes.Any()); - return null; + var solution = fixAllState.Solution; + var newSolution = await TryMergeFixesAsync( + solution, batchOfFixes, progressTracker, cancellationToken).ConfigureAwait(false); + if (newSolution != null && newSolution != solution) + { + var title = FixAllHelper.GetDefaultFixAllTitle(fixAllState.Scope, title: fixAllState.DiagnosticIds.First(), fixAllState.Document!, fixAllState.Project); + return CodeAction.SolutionChangeAction.Create(title, _ => Task.FromResult(newSolution), title); } - private static async Task TryMergeFixesAsync( - Solution oldSolution, - ImmutableArray<(Diagnostic diagnostic, CodeAction action)> diagnosticsAndCodeActions, - IProgress progressTracker, - CancellationToken cancellationToken) - { - var documentIdToChangedDocuments = await GetDocumentIdToChangedDocumentsAsync( - oldSolution, diagnosticsAndCodeActions, progressTracker, cancellationToken).ConfigureAwait(false); + return null; + } - cancellationToken.ThrowIfCancellationRequested(); + private static async Task TryMergeFixesAsync( + Solution oldSolution, + ImmutableArray<(Diagnostic diagnostic, CodeAction action)> diagnosticsAndCodeActions, + IProgress progressTracker, + CancellationToken cancellationToken) + { + var documentIdToChangedDocuments = await GetDocumentIdToChangedDocumentsAsync( + oldSolution, diagnosticsAndCodeActions, progressTracker, cancellationToken).ConfigureAwait(false); - // Now, in parallel, process all the changes to any individual document, producing - // the final source text for any given document. - var documentIdToFinalText = await GetDocumentIdToFinalTextAsync( - oldSolution, documentIdToChangedDocuments, - diagnosticsAndCodeActions, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); - // Finally, apply the changes to each document to the solution, producing the - // new solution. - var currentSolution = oldSolution; - foreach (var (documentId, finalText) in documentIdToFinalText) - currentSolution = currentSolution.WithDocumentText(documentId, finalText); + // Now, in parallel, process all the changes to any individual document, producing + // the final source text for any given document. + var documentIdToFinalText = await GetDocumentIdToFinalTextAsync( + oldSolution, documentIdToChangedDocuments, + diagnosticsAndCodeActions, cancellationToken).ConfigureAwait(false); - return currentSolution; - } + // Finally, apply the changes to each document to the solution, producing the + // new solution. + var currentSolution = oldSolution; + foreach (var (documentId, finalText) in documentIdToFinalText) + currentSolution = currentSolution.WithDocumentText(documentId, finalText); - private static async Task>> GetDocumentIdToChangedDocumentsAsync( - Solution oldSolution, - ImmutableArray<(Diagnostic diagnostic, CodeAction action)> diagnosticsAndCodeActions, - IProgress progressTracker, - CancellationToken cancellationToken) - { - var documentIdToChangedDocuments = new ConcurrentDictionary>(); + return currentSolution; + } - // Process all code actions in parallel to find all the documents that are changed. - // For each changed document, also keep track of the associated code action that - // produced it. - var getChangedDocumentsTasks = new List(); - foreach (var (_, action) in diagnosticsAndCodeActions) - { - getChangedDocumentsTasks.Add(GetChangedDocumentsAsync( - oldSolution, documentIdToChangedDocuments, action, progressTracker, cancellationToken)); - } + private static async Task>> GetDocumentIdToChangedDocumentsAsync( + Solution oldSolution, + ImmutableArray<(Diagnostic diagnostic, CodeAction action)> diagnosticsAndCodeActions, + IProgress progressTracker, + CancellationToken cancellationToken) + { + var documentIdToChangedDocuments = new ConcurrentDictionary>(); - await Task.WhenAll(getChangedDocumentsTasks).ConfigureAwait(false); - return documentIdToChangedDocuments; + // Process all code actions in parallel to find all the documents that are changed. + // For each changed document, also keep track of the associated code action that + // produced it. + var getChangedDocumentsTasks = new List(); + foreach (var (_, action) in diagnosticsAndCodeActions) + { + getChangedDocumentsTasks.Add(GetChangedDocumentsAsync( + oldSolution, documentIdToChangedDocuments, action, progressTracker, cancellationToken)); } - private static async Task> GetDocumentIdToFinalTextAsync( - Solution oldSolution, - IReadOnlyDictionary> documentIdToChangedDocuments, - ImmutableArray<(Diagnostic diagnostic, CodeAction action)> diagnosticsAndCodeActions, - CancellationToken cancellationToken) - { - // We process changes to a document in 'Diagnostic' order. i.e. we apply the change - // created for an earlier diagnostic before the change applied to a later diagnostic. - // It's as if we processed the diagnostics in the document, in order, finding the code - // action for it and applying it right then. - var codeActionToDiagnosticLocation = diagnosticsAndCodeActions.ToDictionary( - tuple => tuple.action, tuple => tuple.diagnostic?.Location.SourceSpan.Start ?? 0); - - var documentIdToFinalText = new ConcurrentDictionary(); - var getFinalDocumentTasks = new List(); - foreach (var (_, changedDocuments) in documentIdToChangedDocuments) - { - getFinalDocumentTasks.Add(GetFinalDocumentTextAsync( - oldSolution, codeActionToDiagnosticLocation, documentIdToFinalText, changedDocuments, cancellationToken)); - } + await Task.WhenAll(getChangedDocumentsTasks).ConfigureAwait(false); + return documentIdToChangedDocuments; + } - await Task.WhenAll(getFinalDocumentTasks).ConfigureAwait(false); - return documentIdToFinalText; + private static async Task> GetDocumentIdToFinalTextAsync( + Solution oldSolution, + IReadOnlyDictionary> documentIdToChangedDocuments, + ImmutableArray<(Diagnostic diagnostic, CodeAction action)> diagnosticsAndCodeActions, + CancellationToken cancellationToken) + { + // We process changes to a document in 'Diagnostic' order. i.e. we apply the change + // created for an earlier diagnostic before the change applied to a later diagnostic. + // It's as if we processed the diagnostics in the document, in order, finding the code + // action for it and applying it right then. + var codeActionToDiagnosticLocation = diagnosticsAndCodeActions.ToDictionary( + tuple => tuple.action, tuple => tuple.diagnostic?.Location.SourceSpan.Start ?? 0); + + var documentIdToFinalText = new ConcurrentDictionary(); + var getFinalDocumentTasks = new List(); + foreach (var (_, changedDocuments) in documentIdToChangedDocuments) + { + getFinalDocumentTasks.Add(GetFinalDocumentTextAsync( + oldSolution, codeActionToDiagnosticLocation, documentIdToFinalText, changedDocuments, cancellationToken)); } - private static async Task GetFinalDocumentTextAsync( - Solution oldSolution, - Dictionary codeActionToDiagnosticLocation, - ConcurrentDictionary documentIdToFinalText, - IEnumerable<(CodeAction action, Document document)> changedDocuments, - CancellationToken cancellationToken) - { - // Merges all the text changes made to a single document by many code actions - // into the final text for that document. + await Task.WhenAll(getFinalDocumentTasks).ConfigureAwait(false); + return documentIdToFinalText; + } - var orderedDocuments = changedDocuments.OrderBy(t => codeActionToDiagnosticLocation[t.action]) - .ThenBy(t => t.action.Title) - .ToImmutableArray(); + private static async Task GetFinalDocumentTextAsync( + Solution oldSolution, + Dictionary codeActionToDiagnosticLocation, + ConcurrentDictionary documentIdToFinalText, + IEnumerable<(CodeAction action, Document document)> changedDocuments, + CancellationToken cancellationToken) + { + // Merges all the text changes made to a single document by many code actions + // into the final text for that document. - if (orderedDocuments.Length == 1) - { - // Super simple case. Only one code action changed this document. Just use - // its final result. - var document = orderedDocuments[0].document; - var finalText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - documentIdToFinalText.TryAdd(document.Id, finalText); - return; - } + var orderedDocuments = changedDocuments.OrderBy(t => codeActionToDiagnosticLocation[t.action]) + .ThenBy(t => t.action.Title) + .ToImmutableArray(); - Debug.Assert(orderedDocuments.Length > 1); + if (orderedDocuments.Length == 1) + { + // Super simple case. Only one code action changed this document. Just use + // its final result. + var document = orderedDocuments[0].document; + var finalText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + documentIdToFinalText.TryAdd(document.Id, finalText); + return; + } - // More complex case. We have multiple changes to the document. Apply them in order - // to get the final document. + Debug.Assert(orderedDocuments.Length > 1); - var oldDocument = oldSolution.GetRequiredDocument(orderedDocuments[0].document.Id); - var merger = new TextChangeMerger(oldDocument); + // More complex case. We have multiple changes to the document. Apply them in order + // to get the final document. - foreach (var (_, currentDocument) in orderedDocuments) - { - cancellationToken.ThrowIfCancellationRequested(); - Debug.Assert(currentDocument.Id == oldDocument.Id); + var oldDocument = oldSolution.GetRequiredDocument(orderedDocuments[0].document.Id); + var merger = new TextChangeMerger(oldDocument); - await merger.TryMergeChangesAsync(currentDocument, cancellationToken).ConfigureAwait(false); - } + foreach (var (_, currentDocument) in orderedDocuments) + { + cancellationToken.ThrowIfCancellationRequested(); + Debug.Assert(currentDocument.Id == oldDocument.Id); - // WithChanges requires a ordered list of TextChanges without any overlap. - var newText = await merger.GetFinalMergedTextAsync(cancellationToken).ConfigureAwait(false); - documentIdToFinalText.TryAdd(oldDocument.Id, newText); + await merger.TryMergeChangesAsync(currentDocument, cancellationToken).ConfigureAwait(false); } - private static readonly Func> s_getValue = - _ => []; + // WithChanges requires a ordered list of TextChanges without any overlap. + var newText = await merger.GetFinalMergedTextAsync(cancellationToken).ConfigureAwait(false); + documentIdToFinalText.TryAdd(oldDocument.Id, newText); + } - private static async Task GetChangedDocumentsAsync( - Solution oldSolution, - ConcurrentDictionary> documentIdToChangedDocuments, - CodeAction codeAction, - IProgress progressTracker, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + private static readonly Func> s_getValue = + _ => []; - var changedSolution = await codeAction.GetChangedSolutionInternalAsync( - oldSolution, progressTracker, cancellationToken: cancellationToken).ConfigureAwait(false); - if (changedSolution is null) - { - // No changed documents - return; - } + private static async Task GetChangedDocumentsAsync( + Solution oldSolution, + ConcurrentDictionary> documentIdToChangedDocuments, + CodeAction codeAction, + IProgress progressTracker, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - var solutionChanges = new SolutionChanges(changedSolution, oldSolution); + var changedSolution = await codeAction.GetChangedSolutionInternalAsync( + oldSolution, progressTracker, cancellationToken: cancellationToken).ConfigureAwait(false); + if (changedSolution is null) + { + // No changed documents + return; + } - // TODO: Handle added/removed documents - // TODO: Handle changed/added/removed additional documents + var solutionChanges = new SolutionChanges(changedSolution, oldSolution); - var documentIdsWithChanges = solutionChanges - .GetProjectChanges() - .SelectMany(p => p.GetChangedDocuments()); + // TODO: Handle added/removed documents + // TODO: Handle changed/added/removed additional documents - foreach (var documentId in documentIdsWithChanges) - { - var changedDocument = changedSolution.GetRequiredDocument(documentId); + var documentIdsWithChanges = solutionChanges + .GetProjectChanges() + .SelectMany(p => p.GetChangedDocuments()); - documentIdToChangedDocuments.GetOrAdd(documentId, s_getValue).Add( - (codeAction, changedDocument)); - } + foreach (var documentId in documentIdsWithChanges) + { + var changedDocument = changedSolution.GetRequiredDocument(documentId); + + documentIdToChangedDocuments.GetOrAdd(documentId, s_getValue).Add( + (codeAction, changedDocument)); } } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.AbstractGlobalSuppressMessageCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.AbstractGlobalSuppressMessageCodeAction.cs index 18aaea47a3975..dce4b49dc2284 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.AbstractGlobalSuppressMessageCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.AbstractGlobalSuppressMessageCodeAction.cs @@ -14,103 +14,102 @@ using Microsoft.CodeAnalysis.LanguageService; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider + internal abstract class AbstractGlobalSuppressMessageCodeAction : AbstractSuppressionCodeAction { - internal abstract class AbstractGlobalSuppressMessageCodeAction : AbstractSuppressionCodeAction - { - private readonly Project _project; + private readonly Project _project; - protected AbstractGlobalSuppressMessageCodeAction(AbstractSuppressionCodeFixProvider fixer, Project project) - : base(fixer, title: FeaturesResources.in_Suppression_File) - { - _project = project; - } + protected AbstractGlobalSuppressMessageCodeAction(AbstractSuppressionCodeFixProvider fixer, Project project) + : base(fixer, title: FeaturesResources.in_Suppression_File) + { + _project = project; + } - protected sealed override async Task> ComputeOperationsAsync( - IProgress progress, CancellationToken cancellationToken) - { - var changedSuppressionDocument = await GetChangedSuppressionDocumentAsync(cancellationToken).ConfigureAwait(false); - return - [ - new ApplyChangesOperation(changedSuppressionDocument.Project.Solution), - new OpenDocumentOperation(changedSuppressionDocument.Id, activateIfAlreadyOpen: true), - new DocumentNavigationOperation(changedSuppressionDocument.Id, position: 0), - ]; - } + protected sealed override async Task> ComputeOperationsAsync( + IProgress progress, CancellationToken cancellationToken) + { + var changedSuppressionDocument = await GetChangedSuppressionDocumentAsync(cancellationToken).ConfigureAwait(false); + return + [ + new ApplyChangesOperation(changedSuppressionDocument.Project.Solution), + new OpenDocumentOperation(changedSuppressionDocument.Id, activateIfAlreadyOpen: true), + new DocumentNavigationOperation(changedSuppressionDocument.Id, position: 0), + ]; + } - protected abstract Task GetChangedSuppressionDocumentAsync(CancellationToken cancellationToken); + protected abstract Task GetChangedSuppressionDocumentAsync(CancellationToken cancellationToken); - private string GetSuppressionsFilePath(string suppressionsFileName) + private string GetSuppressionsFilePath(string suppressionsFileName) + { + if (!string.IsNullOrEmpty(_project.FilePath)) { - if (!string.IsNullOrEmpty(_project.FilePath)) + var fullPath = Path.GetFullPath(_project.FilePath); + var directory = PathUtilities.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(directory)) { - var fullPath = Path.GetFullPath(_project.FilePath); - var directory = PathUtilities.GetDirectoryName(fullPath); - if (!string.IsNullOrEmpty(directory)) + var suppressionsFilePath = PathUtilities.CombinePossiblyRelativeAndRelativePaths(directory, suppressionsFileName); + if (!string.IsNullOrEmpty(suppressionsFilePath)) { - var suppressionsFilePath = PathUtilities.CombinePossiblyRelativeAndRelativePaths(directory, suppressionsFileName); - if (!string.IsNullOrEmpty(suppressionsFilePath)) - { - return suppressionsFilePath; - } + return suppressionsFilePath; } } - - return suppressionsFileName; } - protected async Task GetOrCreateSuppressionsDocumentAsync(CancellationToken c) - { - var index = 1; - var suppressionsFileName = s_globalSuppressionsFileName + Fixer.DefaultFileExtension; - var suppressionsFilePath = GetSuppressionsFilePath(suppressionsFileName); + return suppressionsFileName; + } - Document suppressionsDoc = null; - while (suppressionsDoc == null) + protected async Task GetOrCreateSuppressionsDocumentAsync(CancellationToken c) + { + var index = 1; + var suppressionsFileName = s_globalSuppressionsFileName + Fixer.DefaultFileExtension; + var suppressionsFilePath = GetSuppressionsFilePath(suppressionsFileName); + + Document suppressionsDoc = null; + while (suppressionsDoc == null) + { + var hasDocWithSuppressionsName = false; + foreach (var document in _project.Documents) { - var hasDocWithSuppressionsName = false; - foreach (var document in _project.Documents) + var filePath = document.FilePath; + var fullPath = !string.IsNullOrEmpty(filePath) ? Path.GetFullPath(filePath) : filePath; + if (fullPath == suppressionsFilePath) { - var filePath = document.FilePath; - var fullPath = !string.IsNullOrEmpty(filePath) ? Path.GetFullPath(filePath) : filePath; - if (fullPath == suppressionsFilePath) - { - // Existing global suppressions file. See if this file only has imports and global assembly - // attributes. - hasDocWithSuppressionsName = true; + // Existing global suppressions file. See if this file only has imports and global assembly + // attributes. + hasDocWithSuppressionsName = true; - var t = await document.GetSyntaxTreeAsync(c).ConfigureAwait(false); - var r = await t.GetRootAsync(c).ConfigureAwait(false); - var syntaxFacts = _project.Services.GetRequiredService(); + var t = await document.GetSyntaxTreeAsync(c).ConfigureAwait(false); + var r = await t.GetRootAsync(c).ConfigureAwait(false); + var syntaxFacts = _project.Services.GetRequiredService(); - if (r.ChildNodes().All(n => syntaxFacts.IsUsingOrExternOrImport(n) || Fixer.IsAttributeListWithAssemblyAttributes(n))) - { - suppressionsDoc = document; - break; - } + if (r.ChildNodes().All(n => syntaxFacts.IsUsingOrExternOrImport(n) || Fixer.IsAttributeListWithAssemblyAttributes(n))) + { + suppressionsDoc = document; + break; } } + } - if (suppressionsDoc == null) + if (suppressionsDoc == null) + { + if (hasDocWithSuppressionsName || File.Exists(suppressionsFilePath)) { - if (hasDocWithSuppressionsName || File.Exists(suppressionsFilePath)) - { - index++; - suppressionsFileName = s_globalSuppressionsFileName + index.ToString() + Fixer.DefaultFileExtension; - suppressionsFilePath = GetSuppressionsFilePath(suppressionsFileName); - } - else - { - // Create an empty global suppressions file. - suppressionsDoc = _project.AddDocument(suppressionsFileName, string.Empty); - } + index++; + suppressionsFileName = s_globalSuppressionsFileName + index.ToString() + Fixer.DefaultFileExtension; + suppressionsFilePath = GetSuppressionsFilePath(suppressionsFileName); + } + else + { + // Create an empty global suppressions file. + suppressionsDoc = _project.AddDocument(suppressionsFileName, string.Empty); } } - - return suppressionsDoc; } + + return suppressionsDoc; } } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.AbstractSuppressionCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.AbstractSuppressionCodeAction.cs index eef96b491aac3..0b587c53739c0 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.AbstractSuppressionCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.AbstractSuppressionCodeAction.cs @@ -4,21 +4,20 @@ #nullable disable -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal partial class AbstractSuppressionCodeFixProvider { - internal partial class AbstractSuppressionCodeFixProvider + internal abstract class AbstractSuppressionCodeAction : NestedSuppressionCodeAction { - internal abstract class AbstractSuppressionCodeAction : NestedSuppressionCodeAction - { - private readonly AbstractSuppressionCodeFixProvider _fixer; - - protected AbstractSuppressionCodeAction(AbstractSuppressionCodeFixProvider fixer, string title) - : base(title) - { - _fixer = fixer; - } + private readonly AbstractSuppressionCodeFixProvider _fixer; - protected AbstractSuppressionCodeFixProvider Fixer => _fixer; + protected AbstractSuppressionCodeAction(AbstractSuppressionCodeFixProvider fixer, string title) + : base(title) + { + _fixer = fixer; } + + protected AbstractSuppressionCodeFixProvider Fixer => _fixer; } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.FixAllProvider.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.FixAllProvider.cs index cb08906a7f22f..7ffb555181f4d 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.FixAllProvider.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.FixAllProvider.cs @@ -10,63 +10,62 @@ using Microsoft.CodeAnalysis.CodeActions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider + private sealed class SuppressionFixAllProvider : FixAllProvider { - private sealed class SuppressionFixAllProvider : FixAllProvider + public static readonly SuppressionFixAllProvider Instance = new(); + + private SuppressionFixAllProvider() { - public static readonly SuppressionFixAllProvider Instance = new(); + } + + public override IEnumerable GetSupportedFixAllScopes() + => ImmutableArray.Create(FixAllScope.Document, FixAllScope.Project, + FixAllScope.Solution, FixAllScope.ContainingMember, FixAllScope.ContainingType); - private SuppressionFixAllProvider() + public override async Task GetFixAsync(FixAllContext fixAllContext) + { + // currently there's no FixAll support for local suppression, just bail out + if (NestedSuppressionCodeAction.IsEquivalenceKeyForLocalSuppression(fixAllContext.CodeActionEquivalenceKey)) { + return null; } - public override IEnumerable GetSupportedFixAllScopes() - => ImmutableArray.Create(FixAllScope.Document, FixAllScope.Project, - FixAllScope.Solution, FixAllScope.ContainingMember, FixAllScope.ContainingType); + var suppressionFixer = (AbstractSuppressionCodeFixProvider)((WrapperCodeFixProvider)fixAllContext.CodeFixProvider).SuppressionFixProvider; - public override async Task GetFixAsync(FixAllContext fixAllContext) + if (NestedSuppressionCodeAction.IsEquivalenceKeyForGlobalSuppression(fixAllContext.CodeActionEquivalenceKey)) { - // currently there's no FixAll support for local suppression, just bail out - if (NestedSuppressionCodeAction.IsEquivalenceKeyForLocalSuppression(fixAllContext.CodeActionEquivalenceKey)) - { - return null; - } - - var suppressionFixer = (AbstractSuppressionCodeFixProvider)((WrapperCodeFixProvider)fixAllContext.CodeFixProvider).SuppressionFixProvider; - - if (NestedSuppressionCodeAction.IsEquivalenceKeyForGlobalSuppression(fixAllContext.CodeActionEquivalenceKey)) - { - var fallbackOptions = fixAllContext.GetOptionsProvider(); + var fallbackOptions = fixAllContext.GetOptionsProvider(); - // For global suppressions, we defer to the global suppression system to handle directly. - var title = fixAllContext.CodeActionEquivalenceKey; - return fixAllContext.Document != null - ? GlobalSuppressMessageFixAllCodeAction.Create( - title, suppressionFixer, fixAllContext.Document, - await fixAllContext.GetDocumentDiagnosticsToFixAsync().ConfigureAwait(false), - fallbackOptions) - : GlobalSuppressMessageFixAllCodeAction.Create( - title, suppressionFixer, fixAllContext.Project, - await fixAllContext.GetProjectDiagnosticsToFixAsync().ConfigureAwait(false), - fallbackOptions); - } - - if (NestedSuppressionCodeAction.IsEquivalenceKeyForPragmaWarning(fixAllContext.CodeActionEquivalenceKey)) - { - var batchFixer = new PragmaWarningBatchFixAllProvider(suppressionFixer); - return await batchFixer.GetFixAsync(fixAllContext).ConfigureAwait(false); - } + // For global suppressions, we defer to the global suppression system to handle directly. + var title = fixAllContext.CodeActionEquivalenceKey; + return fixAllContext.Document != null + ? GlobalSuppressMessageFixAllCodeAction.Create( + title, suppressionFixer, fixAllContext.Document, + await fixAllContext.GetDocumentDiagnosticsToFixAsync().ConfigureAwait(false), + fallbackOptions) + : GlobalSuppressMessageFixAllCodeAction.Create( + title, suppressionFixer, fixAllContext.Project, + await fixAllContext.GetProjectDiagnosticsToFixAsync().ConfigureAwait(false), + fallbackOptions); + } - if (NestedSuppressionCodeAction.IsEquivalenceKeyForRemoveSuppression(fixAllContext.CodeActionEquivalenceKey)) - { - var batchFixer = RemoveSuppressionCodeAction.GetBatchFixer(suppressionFixer); - return await batchFixer.GetFixAsync(fixAllContext).ConfigureAwait(false); - } + if (NestedSuppressionCodeAction.IsEquivalenceKeyForPragmaWarning(fixAllContext.CodeActionEquivalenceKey)) + { + var batchFixer = new PragmaWarningBatchFixAllProvider(suppressionFixer); + return await batchFixer.GetFixAsync(fixAllContext).ConfigureAwait(false); + } - throw ExceptionUtilities.Unreachable(); + if (NestedSuppressionCodeAction.IsEquivalenceKeyForRemoveSuppression(fixAllContext.CodeActionEquivalenceKey)) + { + var batchFixer = RemoveSuppressionCodeAction.GetBatchFixer(suppressionFixer); + return await batchFixer.GetFixAsync(fixAllContext).ConfigureAwait(false); } + + throw ExceptionUtilities.Unreachable(); } } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.GlobalSuppressMessageCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.GlobalSuppressMessageCodeAction.cs index 76112d3066fa4..c9df6009c13d7 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.GlobalSuppressMessageCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.GlobalSuppressMessageCodeAction.cs @@ -11,37 +11,36 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider + internal sealed class GlobalSuppressMessageCodeAction( + ISymbol targetSymbol, INamedTypeSymbol suppressMessageAttribute, + Project project, Diagnostic diagnostic, + AbstractSuppressionCodeFixProvider fixer, + CodeActionOptionsProvider fallbackOptions) : AbstractGlobalSuppressMessageCodeAction(fixer, project) { - internal sealed class GlobalSuppressMessageCodeAction( - ISymbol targetSymbol, INamedTypeSymbol suppressMessageAttribute, - Project project, Diagnostic diagnostic, - AbstractSuppressionCodeFixProvider fixer, - CodeActionOptionsProvider fallbackOptions) : AbstractGlobalSuppressMessageCodeAction(fixer, project) + private readonly ISymbol _targetSymbol = targetSymbol; + private readonly INamedTypeSymbol _suppressMessageAttribute = suppressMessageAttribute; + private readonly Diagnostic _diagnostic = diagnostic; + private readonly CodeActionOptionsProvider _fallbackOptions = fallbackOptions; + + protected override async Task GetChangedSuppressionDocumentAsync(CancellationToken cancellationToken) { - private readonly ISymbol _targetSymbol = targetSymbol; - private readonly INamedTypeSymbol _suppressMessageAttribute = suppressMessageAttribute; - private readonly Diagnostic _diagnostic = diagnostic; - private readonly CodeActionOptionsProvider _fallbackOptions = fallbackOptions; - - protected override async Task GetChangedSuppressionDocumentAsync(CancellationToken cancellationToken) - { - var suppressionsDoc = await GetOrCreateSuppressionsDocumentAsync(cancellationToken).ConfigureAwait(false); - var services = suppressionsDoc.Project.Solution.Services; - var suppressionsRoot = await suppressionsDoc.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var addImportsService = suppressionsDoc.GetRequiredLanguageService(); - var options = await suppressionsDoc.GetSyntaxFormattingOptionsAsync(_fallbackOptions, cancellationToken).ConfigureAwait(false); - - suppressionsRoot = Fixer.AddGlobalSuppressMessageAttribute( - suppressionsRoot, _targetSymbol, _suppressMessageAttribute, _diagnostic, services, options, addImportsService, cancellationToken); - return suppressionsDoc.WithSyntaxRoot(suppressionsRoot); - } - - protected override string DiagnosticIdForEquivalenceKey => _diagnostic.Id; - - internal ISymbol TargetSymbol_TestOnly => _targetSymbol; + var suppressionsDoc = await GetOrCreateSuppressionsDocumentAsync(cancellationToken).ConfigureAwait(false); + var services = suppressionsDoc.Project.Solution.Services; + var suppressionsRoot = await suppressionsDoc.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var addImportsService = suppressionsDoc.GetRequiredLanguageService(); + var options = await suppressionsDoc.GetSyntaxFormattingOptionsAsync(_fallbackOptions, cancellationToken).ConfigureAwait(false); + + suppressionsRoot = Fixer.AddGlobalSuppressMessageAttribute( + suppressionsRoot, _targetSymbol, _suppressMessageAttribute, _diagnostic, services, options, addImportsService, cancellationToken); + return suppressionsDoc.WithSyntaxRoot(suppressionsRoot); } + + protected override string DiagnosticIdForEquivalenceKey => _diagnostic.Id; + + internal ISymbol TargetSymbol_TestOnly => _targetSymbol; } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.GlobalSuppressMessageFixAllCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.GlobalSuppressMessageFixAllCodeAction.cs index 591fbfb2d235f..5eb68373b0d5c 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.GlobalSuppressMessageFixAllCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.GlobalSuppressMessageFixAllCodeAction.cs @@ -18,224 +18,223 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider + internal sealed class GlobalSuppressMessageFixAllCodeAction : AbstractGlobalSuppressMessageCodeAction { - internal sealed class GlobalSuppressMessageFixAllCodeAction : AbstractGlobalSuppressMessageCodeAction + private readonly INamedTypeSymbol _suppressMessageAttribute; + private readonly IEnumerable>> _diagnosticsBySymbol; + private readonly CodeActionOptionsProvider _fallbackOptions; + + private GlobalSuppressMessageFixAllCodeAction( + AbstractSuppressionCodeFixProvider fixer, + INamedTypeSymbol suppressMessageAttribute, + IEnumerable>> diagnosticsBySymbol, + Project project, + CodeActionOptionsProvider fallbackOptions) + : base(fixer, project) { - private readonly INamedTypeSymbol _suppressMessageAttribute; - private readonly IEnumerable>> _diagnosticsBySymbol; - private readonly CodeActionOptionsProvider _fallbackOptions; - - private GlobalSuppressMessageFixAllCodeAction( - AbstractSuppressionCodeFixProvider fixer, - INamedTypeSymbol suppressMessageAttribute, - IEnumerable>> diagnosticsBySymbol, - Project project, - CodeActionOptionsProvider fallbackOptions) - : base(fixer, project) - { - _suppressMessageAttribute = suppressMessageAttribute; - _diagnosticsBySymbol = diagnosticsBySymbol; - _fallbackOptions = fallbackOptions; - } + _suppressMessageAttribute = suppressMessageAttribute; + _diagnosticsBySymbol = diagnosticsBySymbol; + _fallbackOptions = fallbackOptions; + } - internal static CodeAction Create(string title, AbstractSuppressionCodeFixProvider fixer, Document triggerDocument, ImmutableDictionary> diagnosticsByDocument, CodeActionOptionsProvider fallbackOptions) - { - return new GlobalSuppressionSolutionChangeAction(title, - (_, ct) => CreateChangedSolutionAsync(fixer, triggerDocument, diagnosticsByDocument, fallbackOptions, ct), - equivalenceKey: title); - } + internal static CodeAction Create(string title, AbstractSuppressionCodeFixProvider fixer, Document triggerDocument, ImmutableDictionary> diagnosticsByDocument, CodeActionOptionsProvider fallbackOptions) + { + return new GlobalSuppressionSolutionChangeAction(title, + (_, ct) => CreateChangedSolutionAsync(fixer, triggerDocument, diagnosticsByDocument, fallbackOptions, ct), + equivalenceKey: title); + } - internal static CodeAction Create(string title, AbstractSuppressionCodeFixProvider fixer, Project triggerProject, ImmutableDictionary> diagnosticsByProject, CodeActionOptionsProvider fallbackOptions) - { - return new GlobalSuppressionSolutionChangeAction(title, - (_, ct) => CreateChangedSolutionAsync(fixer, triggerProject, diagnosticsByProject, fallbackOptions, ct), - equivalenceKey: title); - } + internal static CodeAction Create(string title, AbstractSuppressionCodeFixProvider fixer, Project triggerProject, ImmutableDictionary> diagnosticsByProject, CodeActionOptionsProvider fallbackOptions) + { + return new GlobalSuppressionSolutionChangeAction(title, + (_, ct) => CreateChangedSolutionAsync(fixer, triggerProject, diagnosticsByProject, fallbackOptions, ct), + equivalenceKey: title); + } - private sealed class GlobalSuppressionSolutionChangeAction( - string title, - Func, CancellationToken, Task> createChangedSolution, - string equivalenceKey) : SolutionChangeAction(title, createChangedSolution, equivalenceKey) + private sealed class GlobalSuppressionSolutionChangeAction( + string title, + Func, CancellationToken, Task> createChangedSolution, + string equivalenceKey) : SolutionChangeAction(title, createChangedSolution, equivalenceKey) + { + protected override Task PostProcessChangesAsync(Document document, CancellationToken cancellationToken) { - protected override Task PostProcessChangesAsync(Document document, CancellationToken cancellationToken) - { - // PERF: We don't to formatting on the entire global suppressions document, but instead do it for each attribute individual in the fixer. - return Task.FromResult(document); - } + // PERF: We don't to formatting on the entire global suppressions document, but instead do it for each attribute individual in the fixer. + return Task.FromResult(document); } + } - private static async Task CreateChangedSolutionAsync( - AbstractSuppressionCodeFixProvider fixer, - Document triggerDocument, - ImmutableDictionary> diagnosticsByDocument, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) + private static async Task CreateChangedSolutionAsync( + AbstractSuppressionCodeFixProvider fixer, + Document triggerDocument, + ImmutableDictionary> diagnosticsByDocument, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var currentSolution = triggerDocument.Project.Solution; + foreach (var grouping in diagnosticsByDocument.GroupBy(d => d.Key.Project)) { - var currentSolution = triggerDocument.Project.Solution; - foreach (var grouping in diagnosticsByDocument.GroupBy(d => d.Key.Project)) - { - var oldProject = grouping.Key; - var currentProject = currentSolution.GetProject(oldProject.Id); - var compilation = await currentProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var supressMessageAttribute = compilation.SuppressMessageAttributeType(); + var oldProject = grouping.Key; + var currentProject = currentSolution.GetProject(oldProject.Id); + var compilation = await currentProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + var supressMessageAttribute = compilation.SuppressMessageAttributeType(); - if (supressMessageAttribute != null) - { - var diagnosticsBySymbol = await CreateDiagnosticsBySymbolAsync(fixer, grouping, cancellationToken).ConfigureAwait(false); - if (diagnosticsBySymbol.Any()) - { - var projectCodeAction = new GlobalSuppressMessageFixAllCodeAction(fixer, supressMessageAttribute, diagnosticsBySymbol, currentProject, fallbackOptions); - var newDocument = await projectCodeAction.GetChangedSuppressionDocumentAsync(cancellationToken).ConfigureAwait(false); - currentSolution = newDocument.Project.Solution; - } - } - } - - return currentSolution; - } - - private static async Task CreateChangedSolutionAsync( - AbstractSuppressionCodeFixProvider fixer, - Project triggerProject, - ImmutableDictionary> diagnosticsByProject, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - var currentSolution = triggerProject.Solution; - foreach (var (oldProject, diagnostics) in diagnosticsByProject) + if (supressMessageAttribute != null) { - var currentProject = currentSolution.GetProject(oldProject.Id); - var compilation = await currentProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var suppressMessageAttribute = compilation.SuppressMessageAttributeType(); - - if (suppressMessageAttribute != null) + var diagnosticsBySymbol = await CreateDiagnosticsBySymbolAsync(fixer, grouping, cancellationToken).ConfigureAwait(false); + if (diagnosticsBySymbol.Any()) { - var diagnosticsBySymbol = await CreateDiagnosticsBySymbolAsync(oldProject, diagnostics, cancellationToken).ConfigureAwait(false); - if (diagnosticsBySymbol.Any()) - { - var projectCodeAction = new GlobalSuppressMessageFixAllCodeAction( - fixer, suppressMessageAttribute, diagnosticsBySymbol, currentProject, fallbackOptions); - var newDocument = await projectCodeAction.GetChangedSuppressionDocumentAsync(cancellationToken).ConfigureAwait(false); - currentSolution = newDocument.Project.Solution; - } + var projectCodeAction = new GlobalSuppressMessageFixAllCodeAction(fixer, supressMessageAttribute, diagnosticsBySymbol, currentProject, fallbackOptions); + var newDocument = await projectCodeAction.GetChangedSuppressionDocumentAsync(cancellationToken).ConfigureAwait(false); + currentSolution = newDocument.Project.Solution; } } - - return currentSolution; } - // Equivalence key is not meaningful for FixAll code action. - protected override string DiagnosticIdForEquivalenceKey => string.Empty; + return currentSolution; + } - protected override async Task GetChangedSuppressionDocumentAsync(CancellationToken cancellationToken) + private static async Task CreateChangedSolutionAsync( + AbstractSuppressionCodeFixProvider fixer, + Project triggerProject, + ImmutableDictionary> diagnosticsByProject, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var currentSolution = triggerProject.Solution; + foreach (var (oldProject, diagnostics) in diagnosticsByProject) { - var suppressionsDoc = await GetOrCreateSuppressionsDocumentAsync(cancellationToken).ConfigureAwait(false); - var services = suppressionsDoc.Project.Solution.Services; - var suppressionsRoot = await suppressionsDoc.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var addImportsService = suppressionsDoc.GetRequiredLanguageService(); - var cleanupOptions = await suppressionsDoc.GetCodeCleanupOptionsAsync(_fallbackOptions, cancellationToken).ConfigureAwait(false); + var currentProject = currentSolution.GetProject(oldProject.Id); + var compilation = await currentProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + var suppressMessageAttribute = compilation.SuppressMessageAttributeType(); - foreach (var (targetSymbol, diagnostics) in _diagnosticsBySymbol) + if (suppressMessageAttribute != null) { - foreach (var diagnostic in diagnostics) + var diagnosticsBySymbol = await CreateDiagnosticsBySymbolAsync(oldProject, diagnostics, cancellationToken).ConfigureAwait(false); + if (diagnosticsBySymbol.Any()) { - Contract.ThrowIfFalse(!diagnostic.IsSuppressed); - suppressionsRoot = Fixer.AddGlobalSuppressMessageAttribute( - suppressionsRoot, targetSymbol, _suppressMessageAttribute, diagnostic, - services, cleanupOptions.FormattingOptions, addImportsService, cancellationToken); + var projectCodeAction = new GlobalSuppressMessageFixAllCodeAction( + fixer, suppressMessageAttribute, diagnosticsBySymbol, currentProject, fallbackOptions); + var newDocument = await projectCodeAction.GetChangedSuppressionDocumentAsync(cancellationToken).ConfigureAwait(false); + currentSolution = newDocument.Project.Solution; } } - - var result = suppressionsDoc.WithSyntaxRoot(suppressionsRoot); - var final = await CleanupDocumentAsync(result, cleanupOptions, cancellationToken).ConfigureAwait(false); - return final; } - private static async Task>>> CreateDiagnosticsBySymbolAsync(AbstractSuppressionCodeFixProvider fixer, IEnumerable>> diagnosticsByDocument, CancellationToken cancellationToken) + return currentSolution; + } + + // Equivalence key is not meaningful for FixAll code action. + protected override string DiagnosticIdForEquivalenceKey => string.Empty; + + protected override async Task GetChangedSuppressionDocumentAsync(CancellationToken cancellationToken) + { + var suppressionsDoc = await GetOrCreateSuppressionsDocumentAsync(cancellationToken).ConfigureAwait(false); + var services = suppressionsDoc.Project.Solution.Services; + var suppressionsRoot = await suppressionsDoc.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var addImportsService = suppressionsDoc.GetRequiredLanguageService(); + var cleanupOptions = await suppressionsDoc.GetCodeCleanupOptionsAsync(_fallbackOptions, cancellationToken).ConfigureAwait(false); + + foreach (var (targetSymbol, diagnostics) in _diagnosticsBySymbol) { - var diagnosticsMapBuilder = ImmutableDictionary.CreateBuilder>(); - foreach (var (document, diagnostics) in diagnosticsByDocument) + foreach (var diagnostic in diagnostics) { - foreach (var diagnostic in diagnostics) - { - Contract.ThrowIfFalse(diagnostic.Location.IsInSource); - var suppressionTargetInfo = await fixer.GetSuppressionTargetInfoAsync(document, diagnostic.Location.SourceSpan, cancellationToken).ConfigureAwait(false); - if (suppressionTargetInfo != null) - { - var targetSymbol = suppressionTargetInfo.TargetSymbol; - Contract.ThrowIfNull(targetSymbol); - AddDiagnosticForSymbolIfNeeded(targetSymbol, diagnostic, diagnosticsMapBuilder); - } - } + Contract.ThrowIfFalse(!diagnostic.IsSuppressed); + suppressionsRoot = Fixer.AddGlobalSuppressMessageAttribute( + suppressionsRoot, targetSymbol, _suppressMessageAttribute, diagnostic, + services, cleanupOptions.FormattingOptions, addImportsService, cancellationToken); } - - return CreateDiagnosticsBySymbol(diagnosticsMapBuilder); } - private static async Task>>> CreateDiagnosticsBySymbolAsync(Project project, ImmutableArray diagnostics, CancellationToken cancellationToken) + var result = suppressionsDoc.WithSyntaxRoot(suppressionsRoot); + var final = await CleanupDocumentAsync(result, cleanupOptions, cancellationToken).ConfigureAwait(false); + return final; + } + + private static async Task>>> CreateDiagnosticsBySymbolAsync(AbstractSuppressionCodeFixProvider fixer, IEnumerable>> diagnosticsByDocument, CancellationToken cancellationToken) + { + var diagnosticsMapBuilder = ImmutableDictionary.CreateBuilder>(); + foreach (var (document, diagnostics) in diagnosticsByDocument) { - var diagnosticsMapBuilder = ImmutableDictionary.CreateBuilder>(); - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - if (compilation != null) + foreach (var diagnostic in diagnostics) { - foreach (var diagnostic in diagnostics) + Contract.ThrowIfFalse(diagnostic.Location.IsInSource); + var suppressionTargetInfo = await fixer.GetSuppressionTargetInfoAsync(document, diagnostic.Location.SourceSpan, cancellationToken).ConfigureAwait(false); + if (suppressionTargetInfo != null) { - Contract.ThrowIfFalse(!diagnostic.Location.IsInSource); - var targetSymbol = compilation.Assembly; + var targetSymbol = suppressionTargetInfo.TargetSymbol; + Contract.ThrowIfNull(targetSymbol); AddDiagnosticForSymbolIfNeeded(targetSymbol, diagnostic, diagnosticsMapBuilder); } } - - return CreateDiagnosticsBySymbol(diagnosticsMapBuilder); } - private static void AddDiagnosticForSymbolIfNeeded(ISymbol targetSymbol, Diagnostic diagnostic, ImmutableDictionary>.Builder diagnosticsMapBuilder) + return CreateDiagnosticsBySymbol(diagnosticsMapBuilder); + } + + private static async Task>>> CreateDiagnosticsBySymbolAsync(Project project, ImmutableArray diagnostics, CancellationToken cancellationToken) + { + var diagnosticsMapBuilder = ImmutableDictionary.CreateBuilder>(); + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation != null) { - if (diagnostic.IsSuppressed) + foreach (var diagnostic in diagnostics) { - return; + Contract.ThrowIfFalse(!diagnostic.Location.IsInSource); + var targetSymbol = compilation.Assembly; + AddDiagnosticForSymbolIfNeeded(targetSymbol, diagnostic, diagnosticsMapBuilder); } + } - if (!diagnosticsMapBuilder.TryGetValue(targetSymbol, out var diagnosticsForSymbol)) - { - diagnosticsForSymbol = []; - diagnosticsMapBuilder.Add(targetSymbol, diagnosticsForSymbol); - } + return CreateDiagnosticsBySymbol(diagnosticsMapBuilder); + } - diagnosticsForSymbol.Add(diagnostic); + private static void AddDiagnosticForSymbolIfNeeded(ISymbol targetSymbol, Diagnostic diagnostic, ImmutableDictionary>.Builder diagnosticsMapBuilder) + { + if (diagnostic.IsSuppressed) + { + return; } - private static IEnumerable>> CreateDiagnosticsBySymbol(ImmutableDictionary>.Builder diagnosticsMapBuilder) + if (!diagnosticsMapBuilder.TryGetValue(targetSymbol, out var diagnosticsForSymbol)) { - if (diagnosticsMapBuilder.Count == 0) - { - return SpecializedCollections.EmptyEnumerable>>(); - } + diagnosticsForSymbol = []; + diagnosticsMapBuilder.Add(targetSymbol, diagnosticsForSymbol); + } - var builder = new List>>(); - foreach (var (symbol, diagnostics) in diagnosticsMapBuilder) - builder.Add(KeyValuePairUtil.Create(symbol, GetUniqueDiagnostics(diagnostics))); + diagnosticsForSymbol.Add(diagnostic); + } - return builder.OrderBy(kvp => kvp.Key.GetDocumentationCommentId() ?? string.Empty); + private static IEnumerable>> CreateDiagnosticsBySymbol(ImmutableDictionary>.Builder diagnosticsMapBuilder) + { + if (diagnosticsMapBuilder.Count == 0) + { + return SpecializedCollections.EmptyEnumerable>>(); } - private static ImmutableArray GetUniqueDiagnostics(List diagnostics) + var builder = new List>>(); + foreach (var (symbol, diagnostics) in diagnosticsMapBuilder) + builder.Add(KeyValuePairUtil.Create(symbol, GetUniqueDiagnostics(diagnostics))); + + return builder.OrderBy(kvp => kvp.Key.GetDocumentationCommentId() ?? string.Empty); + } + + private static ImmutableArray GetUniqueDiagnostics(List diagnostics) + { + var uniqueIds = new HashSet(); + var uniqueDiagnostics = ArrayBuilder.GetInstance(); + foreach (var diagnostic in diagnostics) { - var uniqueIds = new HashSet(); - var uniqueDiagnostics = ArrayBuilder.GetInstance(); - foreach (var diagnostic in diagnostics) + if (uniqueIds.Add(diagnostic.Id)) { - if (uniqueIds.Add(diagnostic.Id)) - { - uniqueDiagnostics.Add(diagnostic); - } + uniqueDiagnostics.Add(diagnostic); } - - return uniqueDiagnostics.ToImmutableAndFree(); } + + return uniqueDiagnostics.ToImmutableAndFree(); } } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.IPragmaBasedCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.IPragmaBasedCodeAction.cs index 40331e99d57f6..151e4c20645d2 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.IPragmaBasedCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.IPragmaBasedCodeAction.cs @@ -7,16 +7,15 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal partial class AbstractSuppressionCodeFixProvider { - internal partial class AbstractSuppressionCodeFixProvider + /// + /// Suppression code action based on pragma add/remove/edit. + /// + internal interface IPragmaBasedCodeAction { - /// - /// Suppression code action based on pragma add/remove/edit. - /// - internal interface IPragmaBasedCodeAction - { - Task GetChangedDocumentAsync(bool includeStartTokenChange, bool includeEndTokenChange, CancellationToken cancellationToken); - } + Task GetChangedDocumentAsync(bool includeStartTokenChange, bool includeEndTokenChange, CancellationToken cancellationToken); } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.LocalSuppressMessageCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.LocalSuppressMessageCodeAction.cs index 87a03f9f95667..b54fd2b163f23 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.LocalSuppressMessageCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.LocalSuppressMessageCodeAction.cs @@ -7,37 +7,36 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider + internal sealed class LocalSuppressMessageCodeAction( + AbstractSuppressionCodeFixProvider fixer, + ISymbol targetSymbol, + INamedTypeSymbol suppressMessageAttribute, + SyntaxNode targetNode, + Document document, + Diagnostic diagnostic) : AbstractSuppressionCodeAction(fixer, FeaturesResources.in_Source_attribute) { - internal sealed class LocalSuppressMessageCodeAction( - AbstractSuppressionCodeFixProvider fixer, - ISymbol targetSymbol, - INamedTypeSymbol suppressMessageAttribute, - SyntaxNode targetNode, - Document document, - Diagnostic diagnostic) : AbstractSuppressionCodeAction(fixer, FeaturesResources.in_Source_attribute) - { - private readonly AbstractSuppressionCodeFixProvider _fixer = fixer; - private readonly ISymbol _targetSymbol = targetSymbol; - private readonly INamedTypeSymbol _suppressMessageAttribute = suppressMessageAttribute; - private readonly SyntaxNode _targetNode = targetNode; - private readonly Document _document = document; - private readonly Diagnostic _diagnostic = diagnostic; + private readonly AbstractSuppressionCodeFixProvider _fixer = fixer; + private readonly ISymbol _targetSymbol = targetSymbol; + private readonly INamedTypeSymbol _suppressMessageAttribute = suppressMessageAttribute; + private readonly SyntaxNode _targetNode = targetNode; + private readonly Document _document = document; + private readonly Diagnostic _diagnostic = diagnostic; - protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) - { - var newTargetNode = _fixer.AddLocalSuppressMessageAttribute( - _targetNode, _targetSymbol, _suppressMessageAttribute, _diagnostic); - var root = await _document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newRoot = root.ReplaceNode(_targetNode, newTargetNode); - return _document.WithSyntaxRoot(newRoot); - } + protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) + { + var newTargetNode = _fixer.AddLocalSuppressMessageAttribute( + _targetNode, _targetSymbol, _suppressMessageAttribute, _diagnostic); + var root = await _document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = root.ReplaceNode(_targetNode, newTargetNode); + return _document.WithSyntaxRoot(newRoot); + } - protected override string DiagnosticIdForEquivalenceKey => _diagnostic.Id; + protected override string DiagnosticIdForEquivalenceKey => _diagnostic.Id; - internal SyntaxNode TargetNode_TestOnly => _targetNode; - } + internal SyntaxNode TargetNode_TestOnly => _targetNode; } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaBatchFixHelpers.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaBatchFixHelpers.cs index 64eb7fec5a455..25a8fcd24a3f3 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaBatchFixHelpers.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaBatchFixHelpers.cs @@ -14,168 +14,167 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal partial class AbstractSuppressionCodeFixProvider { - internal partial class AbstractSuppressionCodeFixProvider + /// + /// Helper methods for pragma suppression add/remove batch fixers. + /// + private static class PragmaBatchFixHelpers { - /// - /// Helper methods for pragma suppression add/remove batch fixers. - /// - private static class PragmaBatchFixHelpers + public static CodeAction CreateBatchPragmaFix( + AbstractSuppressionCodeFixProvider suppressionFixProvider, + Document document, + ImmutableArray pragmaActions, + ImmutableArray pragmaDiagnostics, + FixAllState fixAllState, + CancellationToken cancellationToken) + { + return CodeAction.Create( + ((CodeAction)pragmaActions[0]).Title, + createChangedDocument: ct => + BatchPragmaFixesAsync(suppressionFixProvider, document, pragmaActions, pragmaDiagnostics, fixAllState.CodeActionOptionsProvider, cancellationToken), + equivalenceKey: fixAllState.CodeActionEquivalenceKey); + } + + private static async Task BatchPragmaFixesAsync( + AbstractSuppressionCodeFixProvider suppressionFixProvider, + Document document, + ImmutableArray pragmaActions, + ImmutableArray diagnostics, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) { - public static CodeAction CreateBatchPragmaFix( - AbstractSuppressionCodeFixProvider suppressionFixProvider, - Document document, - ImmutableArray pragmaActions, - ImmutableArray pragmaDiagnostics, - FixAllState fixAllState, - CancellationToken cancellationToken) + // We apply all the pragma suppression fixes sequentially. + // At every application, we track the updated locations for remaining diagnostics in the document. + var currentDiagnosticSpans = new Dictionary(); + foreach (var diagnostic in diagnostics) { - return CodeAction.Create( - ((CodeAction)pragmaActions[0]).Title, - createChangedDocument: ct => - BatchPragmaFixesAsync(suppressionFixProvider, document, pragmaActions, pragmaDiagnostics, fixAllState.CodeActionOptionsProvider, cancellationToken), - equivalenceKey: fixAllState.CodeActionEquivalenceKey); + currentDiagnosticSpans.Add(diagnostic, diagnostic.Location.SourceSpan); } - private static async Task BatchPragmaFixesAsync( - AbstractSuppressionCodeFixProvider suppressionFixProvider, - Document document, - ImmutableArray pragmaActions, - ImmutableArray diagnostics, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) + var currentDocument = document; + for (var i = 0; i < pragmaActions.Length; i++) { - // We apply all the pragma suppression fixes sequentially. - // At every application, we track the updated locations for remaining diagnostics in the document. - var currentDiagnosticSpans = new Dictionary(); - foreach (var diagnostic in diagnostics) + var originalpragmaAction = pragmaActions[i]; + var diagnostic = diagnostics[i]; + // Get the diagnostic span for the diagnostic in latest document snapshot. + if (!currentDiagnosticSpans.TryGetValue(diagnostic, out var currentDiagnosticSpan)) { - currentDiagnosticSpans.Add(diagnostic, diagnostic.Location.SourceSpan); + // Diagnostic whose location conflicts with a prior fix. + continue; } - var currentDocument = document; - for (var i = 0; i < pragmaActions.Length; i++) + // Compute and apply pragma suppression fix. + var currentTree = await currentDocument.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var currentLocation = Location.Create(currentTree, currentDiagnosticSpan); + diagnostic = Diagnostic.Create( + id: diagnostic.Id, + category: diagnostic.Descriptor.Category, + message: diagnostic.GetMessage(), + severity: diagnostic.Severity, + defaultSeverity: diagnostic.DefaultSeverity, + isEnabledByDefault: diagnostic.Descriptor.IsEnabledByDefault, + warningLevel: diagnostic.WarningLevel, + title: diagnostic.Descriptor.Title, + description: diagnostic.Descriptor.Description, + helpLink: diagnostic.Descriptor.HelpLinkUri, + location: currentLocation, + additionalLocations: diagnostic.AdditionalLocations, + customTags: diagnostic.Descriptor.CustomTags, + properties: diagnostic.Properties, + isSuppressed: diagnostic.IsSuppressed); + + var newSuppressionFixes = await suppressionFixProvider.GetFixesAsync(currentDocument, currentDiagnosticSpan, SpecializedCollections.SingletonEnumerable(diagnostic), fallbackOptions, cancellationToken).ConfigureAwait(false); + var newSuppressionFix = newSuppressionFixes.SingleOrDefault(); + if (newSuppressionFix != null) { - var originalpragmaAction = pragmaActions[i]; - var diagnostic = diagnostics[i]; - // Get the diagnostic span for the diagnostic in latest document snapshot. - if (!currentDiagnosticSpans.TryGetValue(diagnostic, out var currentDiagnosticSpan)) - { - // Diagnostic whose location conflicts with a prior fix. - continue; - } - - // Compute and apply pragma suppression fix. - var currentTree = await currentDocument.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var currentLocation = Location.Create(currentTree, currentDiagnosticSpan); - diagnostic = Diagnostic.Create( - id: diagnostic.Id, - category: diagnostic.Descriptor.Category, - message: diagnostic.GetMessage(), - severity: diagnostic.Severity, - defaultSeverity: diagnostic.DefaultSeverity, - isEnabledByDefault: diagnostic.Descriptor.IsEnabledByDefault, - warningLevel: diagnostic.WarningLevel, - title: diagnostic.Descriptor.Title, - description: diagnostic.Descriptor.Description, - helpLink: diagnostic.Descriptor.HelpLinkUri, - location: currentLocation, - additionalLocations: diagnostic.AdditionalLocations, - customTags: diagnostic.Descriptor.CustomTags, - properties: diagnostic.Properties, - isSuppressed: diagnostic.IsSuppressed); - - var newSuppressionFixes = await suppressionFixProvider.GetFixesAsync(currentDocument, currentDiagnosticSpan, SpecializedCollections.SingletonEnumerable(diagnostic), fallbackOptions, cancellationToken).ConfigureAwait(false); - var newSuppressionFix = newSuppressionFixes.SingleOrDefault(); - if (newSuppressionFix != null) + var newPragmaAction = newSuppressionFix.Action as IPragmaBasedCodeAction ?? + newSuppressionFix.Action.NestedActions.OfType().SingleOrDefault(); + if (newPragmaAction != null) { - var newPragmaAction = newSuppressionFix.Action as IPragmaBasedCodeAction ?? - newSuppressionFix.Action.NestedActions.OfType().SingleOrDefault(); - if (newPragmaAction != null) - { - // Get the text changes with pragma suppression add/removals. - // Note: We do it one token at a time to ensure we get single text change in the new document, otherwise UpdateDiagnosticSpans won't function as expected. - // Update the diagnostics spans based on the text changes. - var startTokenChanges = await GetTextChangesAsync(newPragmaAction, currentDocument, - includeStartTokenChange: true, includeEndTokenChange: false, cancellationToken: cancellationToken).ConfigureAwait(false); - - var endTokenChanges = await GetTextChangesAsync(newPragmaAction, currentDocument, - includeStartTokenChange: false, includeEndTokenChange: true, cancellationToken: cancellationToken).ConfigureAwait(false); - - var currentText = await currentDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var orderedChanges = startTokenChanges.Concat(endTokenChanges).OrderBy(change => change.Span).Distinct(); - var newText = currentText.WithChanges(orderedChanges); - currentDocument = currentDocument.WithText(newText); - - // Update the diagnostics spans based on the text changes. - UpdateDiagnosticSpans(diagnostics, currentDiagnosticSpans, orderedChanges); - } + // Get the text changes with pragma suppression add/removals. + // Note: We do it one token at a time to ensure we get single text change in the new document, otherwise UpdateDiagnosticSpans won't function as expected. + // Update the diagnostics spans based on the text changes. + var startTokenChanges = await GetTextChangesAsync(newPragmaAction, currentDocument, + includeStartTokenChange: true, includeEndTokenChange: false, cancellationToken: cancellationToken).ConfigureAwait(false); + + var endTokenChanges = await GetTextChangesAsync(newPragmaAction, currentDocument, + includeStartTokenChange: false, includeEndTokenChange: true, cancellationToken: cancellationToken).ConfigureAwait(false); + + var currentText = await currentDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var orderedChanges = startTokenChanges.Concat(endTokenChanges).OrderBy(change => change.Span).Distinct(); + var newText = currentText.WithChanges(orderedChanges); + currentDocument = currentDocument.WithText(newText); + + // Update the diagnostics spans based on the text changes. + UpdateDiagnosticSpans(diagnostics, currentDiagnosticSpans, orderedChanges); } } - - return currentDocument; } - private static async Task> GetTextChangesAsync( - IPragmaBasedCodeAction pragmaAction, - Document currentDocument, - bool includeStartTokenChange, - bool includeEndTokenChange, - CancellationToken cancellationToken) - { - var newDocument = await pragmaAction.GetChangedDocumentAsync(includeStartTokenChange, includeEndTokenChange, cancellationToken).ConfigureAwait(false); - return await newDocument.GetTextChangesAsync(currentDocument, cancellationToken).ConfigureAwait(false); - } + return currentDocument; + } + + private static async Task> GetTextChangesAsync( + IPragmaBasedCodeAction pragmaAction, + Document currentDocument, + bool includeStartTokenChange, + bool includeEndTokenChange, + CancellationToken cancellationToken) + { + var newDocument = await pragmaAction.GetChangedDocumentAsync(includeStartTokenChange, includeEndTokenChange, cancellationToken).ConfigureAwait(false); + return await newDocument.GetTextChangesAsync(currentDocument, cancellationToken).ConfigureAwait(false); + } + + private static void UpdateDiagnosticSpans(ImmutableArray diagnostics, Dictionary currentDiagnosticSpans, IEnumerable textChanges) + { + static bool IsPriorSpan(TextSpan span, TextChange textChange) => span.End <= textChange.Span.Start; + static bool IsFollowingSpan(TextSpan span, TextChange textChange) => span.Start >= textChange.Span.End; + static bool IsEnclosingSpan(TextSpan span, TextChange textChange) => span.Contains(textChange.Span); - private static void UpdateDiagnosticSpans(ImmutableArray diagnostics, Dictionary currentDiagnosticSpans, IEnumerable textChanges) + foreach (var diagnostic in diagnostics) { - static bool IsPriorSpan(TextSpan span, TextChange textChange) => span.End <= textChange.Span.Start; - static bool IsFollowingSpan(TextSpan span, TextChange textChange) => span.Start >= textChange.Span.End; - static bool IsEnclosingSpan(TextSpan span, TextChange textChange) => span.Contains(textChange.Span); + // We use 'originalSpan' to identify if the diagnostic is prior/following/enclosing with respect to each text change. + // We use 'currentSpan' to track updates made to the originalSpan by each text change. + if (!currentDiagnosticSpans.TryGetValue(diagnostic, out var originalSpan)) + { + continue; + } - foreach (var diagnostic in diagnostics) + var currentSpan = originalSpan; + foreach (var textChange in textChanges) { - // We use 'originalSpan' to identify if the diagnostic is prior/following/enclosing with respect to each text change. - // We use 'currentSpan' to track updates made to the originalSpan by each text change. - if (!currentDiagnosticSpans.TryGetValue(diagnostic, out var originalSpan)) + if (IsPriorSpan(originalSpan, textChange)) { + // Prior span, needs no update. continue; } - var currentSpan = originalSpan; - foreach (var textChange in textChanges) + var delta = textChange.NewText.Length - textChange.Span.Length; + if (delta != 0) { - if (IsPriorSpan(originalSpan, textChange)) + if (IsFollowingSpan(originalSpan, textChange)) { - // Prior span, needs no update. - continue; + // Following span. + var newStart = currentSpan.Start + delta; + currentSpan = new TextSpan(newStart, currentSpan.Length); + currentDiagnosticSpans[diagnostic] = currentSpan; } - - var delta = textChange.NewText.Length - textChange.Span.Length; - if (delta != 0) + else if (IsEnclosingSpan(originalSpan, textChange)) + { + // Enclosing span. + var newLength = currentSpan.Length + delta; + currentSpan = new TextSpan(currentSpan.Start, newLength); + currentDiagnosticSpans[diagnostic] = currentSpan; + } + else { - if (IsFollowingSpan(originalSpan, textChange)) - { - // Following span. - var newStart = currentSpan.Start + delta; - currentSpan = new TextSpan(newStart, currentSpan.Length); - currentDiagnosticSpans[diagnostic] = currentSpan; - } - else if (IsEnclosingSpan(originalSpan, textChange)) - { - // Enclosing span. - var newLength = currentSpan.Length + delta; - currentSpan = new TextSpan(currentSpan.Start, newLength); - currentDiagnosticSpans[diagnostic] = currentSpan; - } - else - { - // Overlapping span. - // Drop conflicting diagnostics. - currentDiagnosticSpans.Remove(diagnostic); - break; - } + // Overlapping span. + // Drop conflicting diagnostics. + currentDiagnosticSpans.Remove(diagnostic); + break; } } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaHelpers.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaHelpers.cs index 79616a4837679..c748295d4eabb 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaHelpers.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaHelpers.cs @@ -12,263 +12,262 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal partial class AbstractSuppressionCodeFixProvider { - internal partial class AbstractSuppressionCodeFixProvider + /// + /// Helper methods for pragma based suppression code actions. + /// + private static class PragmaHelpers { - /// - /// Helper methods for pragma based suppression code actions. - /// - private static class PragmaHelpers + internal static async Task GetChangeDocumentWithPragmaAdjustedAsync( + Document document, + TextSpan diagnosticSpan, + SuppressionTargetInfo suppressionTargetInfo, + Func getNewStartToken, + Func getNewEndToken, + CancellationToken cancellationToken) { - internal static async Task GetChangeDocumentWithPragmaAdjustedAsync( - Document document, - TextSpan diagnosticSpan, - SuppressionTargetInfo suppressionTargetInfo, - Func getNewStartToken, - Func getNewEndToken, - CancellationToken cancellationToken) + var startToken = suppressionTargetInfo.StartToken; + var endToken = suppressionTargetInfo.EndToken; + var nodeWithTokens = suppressionTargetInfo.NodeWithTokens; + var root = await nodeWithTokens.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + + var startAndEndTokenAreTheSame = startToken == endToken; + var newStartToken = getNewStartToken(startToken, diagnosticSpan); + + var newEndToken = endToken; + if (startAndEndTokenAreTheSame) { - var startToken = suppressionTargetInfo.StartToken; - var endToken = suppressionTargetInfo.EndToken; - var nodeWithTokens = suppressionTargetInfo.NodeWithTokens; - var root = await nodeWithTokens.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var annotation = new SyntaxAnnotation(); + newEndToken = root.ReplaceToken(startToken, newStartToken.WithAdditionalAnnotations(annotation)).GetAnnotatedTokens(annotation).Single(); + var spanChange = newStartToken.LeadingTrivia.FullSpan.Length - startToken.LeadingTrivia.FullSpan.Length; + diagnosticSpan = new TextSpan(diagnosticSpan.Start + spanChange, diagnosticSpan.Length); + } - var startAndEndTokenAreTheSame = startToken == endToken; - var newStartToken = getNewStartToken(startToken, diagnosticSpan); + newEndToken = getNewEndToken(newEndToken, diagnosticSpan); - var newEndToken = endToken; - if (startAndEndTokenAreTheSame) - { - var annotation = new SyntaxAnnotation(); - newEndToken = root.ReplaceToken(startToken, newStartToken.WithAdditionalAnnotations(annotation)).GetAnnotatedTokens(annotation).Single(); - var spanChange = newStartToken.LeadingTrivia.FullSpan.Length - startToken.LeadingTrivia.FullSpan.Length; - diagnosticSpan = new TextSpan(diagnosticSpan.Start + spanChange, diagnosticSpan.Length); - } + SyntaxNode newNode; + if (startAndEndTokenAreTheSame) + { + newNode = nodeWithTokens.ReplaceToken(startToken, newEndToken); + } + else + { + newNode = nodeWithTokens.ReplaceTokens([startToken, endToken], (o, n) => o == startToken ? newStartToken : newEndToken); + } - newEndToken = getNewEndToken(newEndToken, diagnosticSpan); + var newRoot = root.ReplaceNode(nodeWithTokens, newNode); + return document.WithSyntaxRoot(newRoot); + } - SyntaxNode newNode; - if (startAndEndTokenAreTheSame) - { - newNode = nodeWithTokens.ReplaceToken(startToken, newEndToken); - } - else + private static int GetPositionForPragmaInsertion(ImmutableArray triviaList, TextSpan currentDiagnosticSpan, AbstractSuppressionCodeFixProvider fixer, bool isStartToken, out SyntaxTrivia triviaAtIndex) + { + // Start token: Insert the #pragma disable directive just **before** the first end of line trivia prior to diagnostic location. + // End token: Insert the #pragma disable directive just **after** the first end of line trivia after diagnostic location. + + int getNextIndex(int cur) => isStartToken ? cur - 1 : cur + 1; + bool shouldConsiderTrivia(SyntaxTrivia trivia) + => isStartToken + ? trivia.FullSpan.End <= currentDiagnosticSpan.Start + : trivia.FullSpan.Start >= currentDiagnosticSpan.End; + + var walkedPastDiagnosticSpan = false; + var seenEndOfLineTrivia = false; + var index = isStartToken ? triviaList.Length - 1 : 0; + while (index >= 0 && index < triviaList.Length) + { + var trivia = triviaList[index]; + + walkedPastDiagnosticSpan = walkedPastDiagnosticSpan || shouldConsiderTrivia(trivia); + seenEndOfLineTrivia = seenEndOfLineTrivia || + IsEndOfLineOrContainsEndOfLine(trivia, fixer); + + if (walkedPastDiagnosticSpan && seenEndOfLineTrivia) { - newNode = nodeWithTokens.ReplaceTokens([startToken, endToken], (o, n) => o == startToken ? newStartToken : newEndToken); + break; } - var newRoot = root.ReplaceNode(nodeWithTokens, newNode); - return document.WithSyntaxRoot(newRoot); + index = getNextIndex(index); } - private static int GetPositionForPragmaInsertion(ImmutableArray triviaList, TextSpan currentDiagnosticSpan, AbstractSuppressionCodeFixProvider fixer, bool isStartToken, out SyntaxTrivia triviaAtIndex) - { - // Start token: Insert the #pragma disable directive just **before** the first end of line trivia prior to diagnostic location. - // End token: Insert the #pragma disable directive just **after** the first end of line trivia after diagnostic location. - - int getNextIndex(int cur) => isStartToken ? cur - 1 : cur + 1; - bool shouldConsiderTrivia(SyntaxTrivia trivia) - => isStartToken - ? trivia.FullSpan.End <= currentDiagnosticSpan.Start - : trivia.FullSpan.Start >= currentDiagnosticSpan.End; - - var walkedPastDiagnosticSpan = false; - var seenEndOfLineTrivia = false; - var index = isStartToken ? triviaList.Length - 1 : 0; - while (index >= 0 && index < triviaList.Length) - { - var trivia = triviaList[index]; + triviaAtIndex = index >= 0 && index < triviaList.Length + ? triviaList[index] + : default; - walkedPastDiagnosticSpan = walkedPastDiagnosticSpan || shouldConsiderTrivia(trivia); - seenEndOfLineTrivia = seenEndOfLineTrivia || - IsEndOfLineOrContainsEndOfLine(trivia, fixer); + return index; + } - if (walkedPastDiagnosticSpan && seenEndOfLineTrivia) - { - break; - } + internal static SyntaxToken GetNewStartTokenWithAddedPragma( + SyntaxToken startToken, + TextSpan currentDiagnosticSpan, + Diagnostic diagnostic, + AbstractSuppressionCodeFixProvider fixer, + Func formatNode, + bool isRemoveSuppression, + CancellationToken cancellationToken) + { + var trivia = startToken.LeadingTrivia.ToImmutableArray(); + var index = GetPositionForPragmaInsertion(trivia, currentDiagnosticSpan, fixer, isStartToken: true, triviaAtIndex: out var insertAfterTrivia); + index++; - index = getNextIndex(index); - } + bool needsLeadingEOL; + if (index > 0) + { + needsLeadingEOL = !IsEndOfLineOrHasTrailingEndOfLine(insertAfterTrivia, fixer); + } + else if (startToken.FullSpan.Start == 0) + { + needsLeadingEOL = false; + } + else + { + needsLeadingEOL = true; + } - triviaAtIndex = index >= 0 && index < triviaList.Length - ? triviaList[index] - : default; + var pragmaTrivia = !isRemoveSuppression + ? fixer.CreatePragmaDisableDirectiveTrivia(diagnostic, formatNode, needsLeadingEOL, needsTrailingEndOfLine: true, cancellationToken) + : fixer.CreatePragmaRestoreDirectiveTrivia(diagnostic, formatNode, needsLeadingEOL, needsTrailingEndOfLine: true, cancellationToken); - return index; - } + return startToken.WithLeadingTrivia(trivia.InsertRange(index, pragmaTrivia)); + } - internal static SyntaxToken GetNewStartTokenWithAddedPragma( - SyntaxToken startToken, - TextSpan currentDiagnosticSpan, - Diagnostic diagnostic, - AbstractSuppressionCodeFixProvider fixer, - Func formatNode, - bool isRemoveSuppression, - CancellationToken cancellationToken) - { - var trivia = startToken.LeadingTrivia.ToImmutableArray(); - var index = GetPositionForPragmaInsertion(trivia, currentDiagnosticSpan, fixer, isStartToken: true, triviaAtIndex: out var insertAfterTrivia); - index++; + private static bool IsEndOfLineOrHasLeadingEndOfLine(SyntaxTrivia trivia, AbstractSuppressionCodeFixProvider fixer) + { + return fixer.IsEndOfLine(trivia) || + (trivia.HasStructure && fixer.IsEndOfLine(trivia.GetStructure().DescendantTrivia().FirstOrDefault())); + } - bool needsLeadingEOL; - if (index > 0) - { - needsLeadingEOL = !IsEndOfLineOrHasTrailingEndOfLine(insertAfterTrivia, fixer); - } - else if (startToken.FullSpan.Start == 0) - { - needsLeadingEOL = false; - } - else - { - needsLeadingEOL = true; - } + private static bool IsEndOfLineOrHasTrailingEndOfLine(SyntaxTrivia trivia, AbstractSuppressionCodeFixProvider fixer) + { + return fixer.IsEndOfLine(trivia) || + (trivia.HasStructure && fixer.IsEndOfLine(trivia.GetStructure().DescendantTrivia().LastOrDefault())); + } - var pragmaTrivia = !isRemoveSuppression - ? fixer.CreatePragmaDisableDirectiveTrivia(diagnostic, formatNode, needsLeadingEOL, needsTrailingEndOfLine: true, cancellationToken) - : fixer.CreatePragmaRestoreDirectiveTrivia(diagnostic, formatNode, needsLeadingEOL, needsTrailingEndOfLine: true, cancellationToken); + private static bool IsEndOfLineOrContainsEndOfLine(SyntaxTrivia trivia, AbstractSuppressionCodeFixProvider fixer) + { + return fixer.IsEndOfLine(trivia) || + (trivia.HasStructure && trivia.GetStructure().DescendantTrivia().Any(fixer.IsEndOfLine)); + } - return startToken.WithLeadingTrivia(trivia.InsertRange(index, pragmaTrivia)); + internal static SyntaxToken GetNewEndTokenWithAddedPragma( + SyntaxToken endToken, + TextSpan currentDiagnosticSpan, + Diagnostic diagnostic, + AbstractSuppressionCodeFixProvider fixer, + Func formatNode, + bool isRemoveSuppression, + CancellationToken cancellationToken) + { + ImmutableArray trivia; + var isEOF = fixer.IsEndOfFileToken(endToken); + if (isEOF) + { + trivia = endToken.LeadingTrivia.ToImmutableArray(); } - - private static bool IsEndOfLineOrHasLeadingEndOfLine(SyntaxTrivia trivia, AbstractSuppressionCodeFixProvider fixer) + else { - return fixer.IsEndOfLine(trivia) || - (trivia.HasStructure && fixer.IsEndOfLine(trivia.GetStructure().DescendantTrivia().FirstOrDefault())); + trivia = endToken.TrailingTrivia.ToImmutableArray(); } - private static bool IsEndOfLineOrHasTrailingEndOfLine(SyntaxTrivia trivia, AbstractSuppressionCodeFixProvider fixer) + var index = GetPositionForPragmaInsertion(trivia, currentDiagnosticSpan, fixer, isStartToken: false, triviaAtIndex: out var insertBeforeTrivia); + + bool needsTrailingEOL; + if (index < trivia.Length) { - return fixer.IsEndOfLine(trivia) || - (trivia.HasStructure && fixer.IsEndOfLine(trivia.GetStructure().DescendantTrivia().LastOrDefault())); + needsTrailingEOL = !IsEndOfLineOrHasLeadingEndOfLine(insertBeforeTrivia, fixer); } - - private static bool IsEndOfLineOrContainsEndOfLine(SyntaxTrivia trivia, AbstractSuppressionCodeFixProvider fixer) + else if (isEOF) { - return fixer.IsEndOfLine(trivia) || - (trivia.HasStructure && trivia.GetStructure().DescendantTrivia().Any(fixer.IsEndOfLine)); + needsTrailingEOL = false; } - - internal static SyntaxToken GetNewEndTokenWithAddedPragma( - SyntaxToken endToken, - TextSpan currentDiagnosticSpan, - Diagnostic diagnostic, - AbstractSuppressionCodeFixProvider fixer, - Func formatNode, - bool isRemoveSuppression, - CancellationToken cancellationToken) + else { - ImmutableArray trivia; - var isEOF = fixer.IsEndOfFileToken(endToken); - if (isEOF) - { - trivia = endToken.LeadingTrivia.ToImmutableArray(); - } - else - { - trivia = endToken.TrailingTrivia.ToImmutableArray(); - } - - var index = GetPositionForPragmaInsertion(trivia, currentDiagnosticSpan, fixer, isStartToken: false, triviaAtIndex: out var insertBeforeTrivia); - - bool needsTrailingEOL; - if (index < trivia.Length) - { - needsTrailingEOL = !IsEndOfLineOrHasLeadingEndOfLine(insertBeforeTrivia, fixer); - } - else if (isEOF) - { - needsTrailingEOL = false; - } - else - { - needsTrailingEOL = true; - } + needsTrailingEOL = true; + } - var pragmaTrivia = !isRemoveSuppression - ? fixer.CreatePragmaRestoreDirectiveTrivia(diagnostic, formatNode, needsLeadingEndOfLine: true, needsTrailingEndOfLine: needsTrailingEOL, cancellationToken) - : fixer.CreatePragmaDisableDirectiveTrivia(diagnostic, formatNode, needsLeadingEndOfLine: true, needsTrailingEndOfLine: needsTrailingEOL, cancellationToken); + var pragmaTrivia = !isRemoveSuppression + ? fixer.CreatePragmaRestoreDirectiveTrivia(diagnostic, formatNode, needsLeadingEndOfLine: true, needsTrailingEndOfLine: needsTrailingEOL, cancellationToken) + : fixer.CreatePragmaDisableDirectiveTrivia(diagnostic, formatNode, needsLeadingEndOfLine: true, needsTrailingEndOfLine: needsTrailingEOL, cancellationToken); - if (isEOF) - { - return endToken.WithLeadingTrivia(trivia.InsertRange(index, pragmaTrivia)); - } - else - { - return endToken.WithTrailingTrivia(trivia.InsertRange(index, pragmaTrivia)); - } + if (isEOF) + { + return endToken.WithLeadingTrivia(trivia.InsertRange(index, pragmaTrivia)); } + else + { + return endToken.WithTrailingTrivia(trivia.InsertRange(index, pragmaTrivia)); + } + } - internal static void NormalizeTriviaOnTokens(AbstractSuppressionCodeFixProvider fixer, ref Document document, ref SuppressionTargetInfo suppressionTargetInfo) + internal static void NormalizeTriviaOnTokens(AbstractSuppressionCodeFixProvider fixer, ref Document document, ref SuppressionTargetInfo suppressionTargetInfo) + { + // For pragma suppression fixes, we need to normalize the leading trivia on start token to account for + // the trailing trivia on its previous token (and similarly normalize trailing trivia for end token). + + var startToken = suppressionTargetInfo.StartToken; + var endToken = suppressionTargetInfo.EndToken; + var nodeWithTokens = suppressionTargetInfo.NodeWithTokens; + var startAndEndTokensAreSame = startToken == endToken; + var isEndTokenEOF = fixer.IsEndOfFileToken(endToken); + + var previousOfStart = startToken.GetPreviousToken(includeZeroWidth: true); + var nextOfEnd = !isEndTokenEOF ? endToken.GetNextToken(includeZeroWidth: true) : default; + if (!previousOfStart.HasTrailingTrivia && !nextOfEnd.HasLeadingTrivia) { - // For pragma suppression fixes, we need to normalize the leading trivia on start token to account for - // the trailing trivia on its previous token (and similarly normalize trailing trivia for end token). - - var startToken = suppressionTargetInfo.StartToken; - var endToken = suppressionTargetInfo.EndToken; - var nodeWithTokens = suppressionTargetInfo.NodeWithTokens; - var startAndEndTokensAreSame = startToken == endToken; - var isEndTokenEOF = fixer.IsEndOfFileToken(endToken); - - var previousOfStart = startToken.GetPreviousToken(includeZeroWidth: true); - var nextOfEnd = !isEndTokenEOF ? endToken.GetNextToken(includeZeroWidth: true) : default; - if (!previousOfStart.HasTrailingTrivia && !nextOfEnd.HasLeadingTrivia) - { - return; - } + return; + } - var root = nodeWithTokens.SyntaxTree.GetRoot(); - var spanEnd = !isEndTokenEOF ? nextOfEnd.FullSpan.End : endToken.FullSpan.End; - var subtreeRoot = root.FindNode(new TextSpan(previousOfStart.FullSpan.Start, spanEnd - previousOfStart.FullSpan.Start)); + var root = nodeWithTokens.SyntaxTree.GetRoot(); + var spanEnd = !isEndTokenEOF ? nextOfEnd.FullSpan.End : endToken.FullSpan.End; + var subtreeRoot = root.FindNode(new TextSpan(previousOfStart.FullSpan.Start, spanEnd - previousOfStart.FullSpan.Start)); - var currentStartToken = startToken; - var currentEndToken = endToken; - var newStartToken = startToken.WithLeadingTrivia(previousOfStart.TrailingTrivia.Concat(startToken.LeadingTrivia)); + var currentStartToken = startToken; + var currentEndToken = endToken; + var newStartToken = startToken.WithLeadingTrivia(previousOfStart.TrailingTrivia.Concat(startToken.LeadingTrivia)); - var newEndToken = currentEndToken; - if (startAndEndTokensAreSame) - { - newEndToken = newStartToken; - } + var newEndToken = currentEndToken; + if (startAndEndTokensAreSame) + { + newEndToken = newStartToken; + } - newEndToken = newEndToken.WithTrailingTrivia(endToken.TrailingTrivia.Concat(nextOfEnd.LeadingTrivia)); + newEndToken = newEndToken.WithTrailingTrivia(endToken.TrailingTrivia.Concat(nextOfEnd.LeadingTrivia)); - var newPreviousOfStart = previousOfStart.WithTrailingTrivia(); - var newNextOfEnd = nextOfEnd.WithLeadingTrivia(); + var newPreviousOfStart = previousOfStart.WithTrailingTrivia(); + var newNextOfEnd = nextOfEnd.WithLeadingTrivia(); - var newSubtreeRoot = subtreeRoot.ReplaceTokens([startToken, previousOfStart, endToken, nextOfEnd], - (o, n) => + var newSubtreeRoot = subtreeRoot.ReplaceTokens([startToken, previousOfStart, endToken, nextOfEnd], + (o, n) => + { + if (o == currentStartToken) { - if (o == currentStartToken) - { - return startAndEndTokensAreSame ? newEndToken : newStartToken; - } - else if (o == previousOfStart) - { - return newPreviousOfStart; - } - else if (o == currentEndToken) - { - return newEndToken; - } - else if (o == nextOfEnd) - { - return newNextOfEnd; - } - else - { - return n; - } - }); - - root = root.ReplaceNode(subtreeRoot, newSubtreeRoot); - document = document.WithSyntaxRoot(root); - suppressionTargetInfo.StartToken = root.FindToken(startToken.SpanStart); - suppressionTargetInfo.EndToken = root.FindToken(endToken.SpanStart); - suppressionTargetInfo.NodeWithTokens = fixer.GetNodeWithTokens(suppressionTargetInfo.StartToken, suppressionTargetInfo.EndToken, root); - } + return startAndEndTokensAreSame ? newEndToken : newStartToken; + } + else if (o == previousOfStart) + { + return newPreviousOfStart; + } + else if (o == currentEndToken) + { + return newEndToken; + } + else if (o == nextOfEnd) + { + return newNextOfEnd; + } + else + { + return n; + } + }); + + root = root.ReplaceNode(subtreeRoot, newSubtreeRoot); + document = document.WithSyntaxRoot(root); + suppressionTargetInfo.StartToken = root.FindToken(startToken.SpanStart); + suppressionTargetInfo.EndToken = root.FindToken(endToken.SpanStart); + suppressionTargetInfo.NodeWithTokens = fixer.GetNodeWithTokens(suppressionTargetInfo.StartToken, suppressionTargetInfo.EndToken, root); } } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaWarningBatchFixAllProvider.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaWarningBatchFixAllProvider.cs index 66021794e1465..33bf3f7e1b06e 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaWarningBatchFixAllProvider.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaWarningBatchFixAllProvider.cs @@ -13,54 +13,53 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider + /// + /// Batch fixer for pragma suppress code action. + /// + internal sealed class PragmaWarningBatchFixAllProvider(AbstractSuppressionCodeFixProvider suppressionFixProvider) : AbstractSuppressionBatchFixAllProvider { - /// - /// Batch fixer for pragma suppress code action. - /// - internal sealed class PragmaWarningBatchFixAllProvider(AbstractSuppressionCodeFixProvider suppressionFixProvider) : AbstractSuppressionBatchFixAllProvider + private readonly AbstractSuppressionCodeFixProvider _suppressionFixProvider = suppressionFixProvider; + + protected override async Task AddDocumentFixesAsync( + Document document, ImmutableArray diagnostics, + ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> fixes, + FixAllState fixAllState, CancellationToken cancellationToken) { - private readonly AbstractSuppressionCodeFixProvider _suppressionFixProvider = suppressionFixProvider; + var pragmaActionsBuilder = ArrayBuilder.GetInstance(); + var pragmaDiagnosticsBuilder = ArrayBuilder.GetInstance(); - protected override async Task AddDocumentFixesAsync( - Document document, ImmutableArray diagnostics, - ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> fixes, - FixAllState fixAllState, CancellationToken cancellationToken) + foreach (var diagnostic in diagnostics.Where(d => d.Location.IsInSource && !d.IsSuppressed)) { - var pragmaActionsBuilder = ArrayBuilder.GetInstance(); - var pragmaDiagnosticsBuilder = ArrayBuilder.GetInstance(); - - foreach (var diagnostic in diagnostics.Where(d => d.Location.IsInSource && !d.IsSuppressed)) + var span = diagnostic.Location.SourceSpan; + var pragmaSuppressions = await _suppressionFixProvider.GetPragmaSuppressionsAsync( + document, span, SpecializedCollections.SingletonEnumerable(diagnostic), fixAllState.CodeActionOptionsProvider, cancellationToken).ConfigureAwait(false); + var pragmaSuppression = pragmaSuppressions.SingleOrDefault(); + if (pragmaSuppression != null) { - var span = diagnostic.Location.SourceSpan; - var pragmaSuppressions = await _suppressionFixProvider.GetPragmaSuppressionsAsync( - document, span, SpecializedCollections.SingletonEnumerable(diagnostic), fixAllState.CodeActionOptionsProvider, cancellationToken).ConfigureAwait(false); - var pragmaSuppression = pragmaSuppressions.SingleOrDefault(); - if (pragmaSuppression != null) + if (fixAllState.IsFixMultiple) { - if (fixAllState.IsFixMultiple) - { - pragmaSuppression = pragmaSuppression.CloneForFixMultipleContext(); - } - - pragmaActionsBuilder.Add(pragmaSuppression); - pragmaDiagnosticsBuilder.Add(diagnostic); + pragmaSuppression = pragmaSuppression.CloneForFixMultipleContext(); } + + pragmaActionsBuilder.Add(pragmaSuppression); + pragmaDiagnosticsBuilder.Add(diagnostic); } + } - // Get the pragma batch fix. - if (pragmaActionsBuilder.Count > 0) - { - var pragmaBatchFix = PragmaBatchFixHelpers.CreateBatchPragmaFix( - _suppressionFixProvider, document, - pragmaActionsBuilder.ToImmutableAndFree(), - pragmaDiagnosticsBuilder.ToImmutableAndFree(), - fixAllState, cancellationToken); + // Get the pragma batch fix. + if (pragmaActionsBuilder.Count > 0) + { + var pragmaBatchFix = PragmaBatchFixHelpers.CreateBatchPragmaFix( + _suppressionFixProvider, document, + pragmaActionsBuilder.ToImmutableAndFree(), + pragmaDiagnosticsBuilder.ToImmutableAndFree(), + fixAllState, cancellationToken); - fixes.Add((diagnostic: null, pragmaBatchFix)); - } + fixes.Add((diagnostic: null, pragmaBatchFix)); } } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaWarningCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaWarningCodeAction.cs index 45e02a7c47efa..204d8296e4658 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaWarningCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaWarningCodeAction.cs @@ -8,83 +8,82 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Formatting; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider + internal sealed class PragmaWarningCodeAction : AbstractSuppressionCodeAction, IPragmaBasedCodeAction { - internal sealed class PragmaWarningCodeAction : AbstractSuppressionCodeAction, IPragmaBasedCodeAction - { - private readonly SuppressionTargetInfo _suppressionTargetInfo; - private readonly Document _document; - private readonly SyntaxFormattingOptions _options; - private readonly Diagnostic _diagnostic; - private readonly bool _forFixMultipleContext; + private readonly SuppressionTargetInfo _suppressionTargetInfo; + private readonly Document _document; + private readonly SyntaxFormattingOptions _options; + private readonly Diagnostic _diagnostic; + private readonly bool _forFixMultipleContext; - public static PragmaWarningCodeAction Create( - SuppressionTargetInfo suppressionTargetInfo, - Document document, - SyntaxFormattingOptions options, - Diagnostic diagnostic, - AbstractSuppressionCodeFixProvider fixer) - { - // We need to normalize the leading trivia on start token to account for - // the trailing trivia on its previous token (and similarly normalize trailing trivia for end token). - PragmaHelpers.NormalizeTriviaOnTokens(fixer, ref document, ref suppressionTargetInfo); + public static PragmaWarningCodeAction Create( + SuppressionTargetInfo suppressionTargetInfo, + Document document, + SyntaxFormattingOptions options, + Diagnostic diagnostic, + AbstractSuppressionCodeFixProvider fixer) + { + // We need to normalize the leading trivia on start token to account for + // the trailing trivia on its previous token (and similarly normalize trailing trivia for end token). + PragmaHelpers.NormalizeTriviaOnTokens(fixer, ref document, ref suppressionTargetInfo); - return new PragmaWarningCodeAction(suppressionTargetInfo, document, options, diagnostic, fixer); - } + return new PragmaWarningCodeAction(suppressionTargetInfo, document, options, diagnostic, fixer); + } - private PragmaWarningCodeAction( - SuppressionTargetInfo suppressionTargetInfo, - Document document, - SyntaxFormattingOptions options, - Diagnostic diagnostic, - AbstractSuppressionCodeFixProvider fixer, - bool forFixMultipleContext = false) - : base(fixer, title: FeaturesResources.in_Source) - { - _suppressionTargetInfo = suppressionTargetInfo; - _document = document; - _options = options; - _diagnostic = diagnostic; - _forFixMultipleContext = forFixMultipleContext; - } + private PragmaWarningCodeAction( + SuppressionTargetInfo suppressionTargetInfo, + Document document, + SyntaxFormattingOptions options, + Diagnostic diagnostic, + AbstractSuppressionCodeFixProvider fixer, + bool forFixMultipleContext = false) + : base(fixer, title: FeaturesResources.in_Source) + { + _suppressionTargetInfo = suppressionTargetInfo; + _document = document; + _options = options; + _diagnostic = diagnostic; + _forFixMultipleContext = forFixMultipleContext; + } - public PragmaWarningCodeAction CloneForFixMultipleContext() - => new(_suppressionTargetInfo, _document, _options, _diagnostic, Fixer, forFixMultipleContext: true); + public PragmaWarningCodeAction CloneForFixMultipleContext() + => new(_suppressionTargetInfo, _document, _options, _diagnostic, Fixer, forFixMultipleContext: true); - protected override string DiagnosticIdForEquivalenceKey - => _forFixMultipleContext ? string.Empty : _diagnostic.Id; + protected override string DiagnosticIdForEquivalenceKey + => _forFixMultipleContext ? string.Empty : _diagnostic.Id; - protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) - => await GetChangedDocumentAsync(includeStartTokenChange: true, includeEndTokenChange: true, cancellationToken: cancellationToken).ConfigureAwait(false); + protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) + => await GetChangedDocumentAsync(includeStartTokenChange: true, includeEndTokenChange: true, cancellationToken: cancellationToken).ConfigureAwait(false); - public async Task GetChangedDocumentAsync(bool includeStartTokenChange, bool includeEndTokenChange, CancellationToken cancellationToken) - { - return await PragmaHelpers.GetChangeDocumentWithPragmaAdjustedAsync( - _document, - _diagnostic.Location.SourceSpan, - _suppressionTargetInfo, - (startToken, currentDiagnosticSpan) => - { - return includeStartTokenChange - ? PragmaHelpers.GetNewStartTokenWithAddedPragma(startToken, currentDiagnosticSpan, _diagnostic, Fixer, FormatNode, isRemoveSuppression: false, cancellationToken) - : startToken; - }, - (endToken, currentDiagnosticSpan) => - { - return includeEndTokenChange - ? PragmaHelpers.GetNewEndTokenWithAddedPragma(endToken, currentDiagnosticSpan, _diagnostic, Fixer, FormatNode, isRemoveSuppression: false, cancellationToken) - : endToken; - }, - cancellationToken).ConfigureAwait(false); - } + public async Task GetChangedDocumentAsync(bool includeStartTokenChange, bool includeEndTokenChange, CancellationToken cancellationToken) + { + return await PragmaHelpers.GetChangeDocumentWithPragmaAdjustedAsync( + _document, + _diagnostic.Location.SourceSpan, + _suppressionTargetInfo, + (startToken, currentDiagnosticSpan) => + { + return includeStartTokenChange + ? PragmaHelpers.GetNewStartTokenWithAddedPragma(startToken, currentDiagnosticSpan, _diagnostic, Fixer, FormatNode, isRemoveSuppression: false, cancellationToken) + : startToken; + }, + (endToken, currentDiagnosticSpan) => + { + return includeEndTokenChange + ? PragmaHelpers.GetNewEndTokenWithAddedPragma(endToken, currentDiagnosticSpan, _diagnostic, Fixer, FormatNode, isRemoveSuppression: false, cancellationToken) + : endToken; + }, + cancellationToken).ConfigureAwait(false); + } - public SyntaxToken StartToken_TestOnly => _suppressionTargetInfo.StartToken; - public SyntaxToken EndToken_TestOnly => _suppressionTargetInfo.EndToken; + public SyntaxToken StartToken_TestOnly => _suppressionTargetInfo.StartToken; + public SyntaxToken EndToken_TestOnly => _suppressionTargetInfo.EndToken; - private SyntaxNode FormatNode(SyntaxNode node, CancellationToken cancellationToken) - => Formatter.Format(node, _document.Project.Solution.Services, _options, cancellationToken); - } + private SyntaxNode FormatNode(SyntaxNode node, CancellationToken cancellationToken) + => Formatter.Format(node, _document.Project.Solution.Services, _options, cancellationToken); } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction.BatchFixer.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction.BatchFixer.cs index a622b32b280e4..ec90e430ddf57 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction.BatchFixer.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction.BatchFixer.cs @@ -15,158 +15,157 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider + internal abstract partial class RemoveSuppressionCodeAction { - internal abstract partial class RemoveSuppressionCodeAction + public static FixAllProvider GetBatchFixer(AbstractSuppressionCodeFixProvider suppressionFixProvider) + => new RemoveSuppressionBatchFixAllProvider(suppressionFixProvider); + + /// + /// Batch fixer for pragma suppression removal code action. + /// + private sealed class RemoveSuppressionBatchFixAllProvider(AbstractSuppressionCodeFixProvider suppressionFixProvider) : AbstractSuppressionBatchFixAllProvider { - public static FixAllProvider GetBatchFixer(AbstractSuppressionCodeFixProvider suppressionFixProvider) - => new RemoveSuppressionBatchFixAllProvider(suppressionFixProvider); + private readonly AbstractSuppressionCodeFixProvider _suppressionFixProvider = suppressionFixProvider; - /// - /// Batch fixer for pragma suppression removal code action. - /// - private sealed class RemoveSuppressionBatchFixAllProvider(AbstractSuppressionCodeFixProvider suppressionFixProvider) : AbstractSuppressionBatchFixAllProvider + protected override async Task AddDocumentFixesAsync( + Document document, ImmutableArray diagnostics, + ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> fixes, + FixAllState fixAllState, CancellationToken cancellationToken) { - private readonly AbstractSuppressionCodeFixProvider _suppressionFixProvider = suppressionFixProvider; + // Batch all the pragma remove suppression fixes by executing them sequentially for the document. + var pragmaActionsBuilder = ArrayBuilder.GetInstance(); + var pragmaDiagnosticsBuilder = ArrayBuilder.GetInstance(); - protected override async Task AddDocumentFixesAsync( - Document document, ImmutableArray diagnostics, - ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> fixes, - FixAllState fixAllState, CancellationToken cancellationToken) + foreach (var diagnostic in diagnostics.Where(d => d.Location.IsInSource && d.IsSuppressed)) { - // Batch all the pragma remove suppression fixes by executing them sequentially for the document. - var pragmaActionsBuilder = ArrayBuilder.GetInstance(); - var pragmaDiagnosticsBuilder = ArrayBuilder.GetInstance(); - - foreach (var diagnostic in diagnostics.Where(d => d.Location.IsInSource && d.IsSuppressed)) + var span = diagnostic.Location.SourceSpan; + var removeSuppressionFixes = await _suppressionFixProvider.GetFixesAsync( + document, span, SpecializedCollections.SingletonEnumerable(diagnostic), fixAllState.CodeActionOptionsProvider, cancellationToken).ConfigureAwait(false); + var removeSuppressionFix = removeSuppressionFixes.SingleOrDefault(); + if (removeSuppressionFix != null) { - var span = diagnostic.Location.SourceSpan; - var removeSuppressionFixes = await _suppressionFixProvider.GetFixesAsync( - document, span, SpecializedCollections.SingletonEnumerable(diagnostic), fixAllState.CodeActionOptionsProvider, cancellationToken).ConfigureAwait(false); - var removeSuppressionFix = removeSuppressionFixes.SingleOrDefault(); - if (removeSuppressionFix != null) + if (removeSuppressionFix.Action is RemoveSuppressionCodeAction codeAction) { - if (removeSuppressionFix.Action is RemoveSuppressionCodeAction codeAction) + if (fixAllState.IsFixMultiple) { - if (fixAllState.IsFixMultiple) - { - codeAction = codeAction.CloneForFixMultipleContext(); - } - - if (codeAction is PragmaRemoveAction pragmaRemoveAction) - { - pragmaActionsBuilder.Add(pragmaRemoveAction); - pragmaDiagnosticsBuilder.Add(diagnostic); - } - else - { - fixes.Add((diagnostic, codeAction)); - } + codeAction = codeAction.CloneForFixMultipleContext(); + } + + if (codeAction is PragmaRemoveAction pragmaRemoveAction) + { + pragmaActionsBuilder.Add(pragmaRemoveAction); + pragmaDiagnosticsBuilder.Add(diagnostic); + } + else + { + fixes.Add((diagnostic, codeAction)); } } } + } - // Get the pragma batch fix. - if (pragmaActionsBuilder.Count > 0) - { - var pragmaBatchFix = PragmaBatchFixHelpers.CreateBatchPragmaFix( - _suppressionFixProvider, document, - pragmaActionsBuilder.ToImmutableAndFree(), - pragmaDiagnosticsBuilder.ToImmutableAndFree(), - fixAllState, cancellationToken); + // Get the pragma batch fix. + if (pragmaActionsBuilder.Count > 0) + { + var pragmaBatchFix = PragmaBatchFixHelpers.CreateBatchPragmaFix( + _suppressionFixProvider, document, + pragmaActionsBuilder.ToImmutableAndFree(), + pragmaDiagnosticsBuilder.ToImmutableAndFree(), + fixAllState, cancellationToken); - fixes.Add((diagnostic: null, pragmaBatchFix)); - } + fixes.Add((diagnostic: null, pragmaBatchFix)); } + } - protected override async Task AddProjectFixesAsync( - Project project, ImmutableArray diagnostics, - ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> bag, - FixAllState fixAllState, CancellationToken cancellationToken) + protected override async Task AddProjectFixesAsync( + Project project, ImmutableArray diagnostics, + ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> bag, + FixAllState fixAllState, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics.Where(d => !d.Location.IsInSource && d.IsSuppressed)) { - foreach (var diagnostic in diagnostics.Where(d => !d.Location.IsInSource && d.IsSuppressed)) + var removeSuppressionFixes = await _suppressionFixProvider.GetFixesAsync( + project, SpecializedCollections.SingletonEnumerable(diagnostic), fixAllState.CodeActionOptionsProvider, cancellationToken).ConfigureAwait(false); + if (removeSuppressionFixes.SingleOrDefault()?.Action is RemoveSuppressionCodeAction removeSuppressionCodeAction) { - var removeSuppressionFixes = await _suppressionFixProvider.GetFixesAsync( - project, SpecializedCollections.SingletonEnumerable(diagnostic), fixAllState.CodeActionOptionsProvider, cancellationToken).ConfigureAwait(false); - if (removeSuppressionFixes.SingleOrDefault()?.Action is RemoveSuppressionCodeAction removeSuppressionCodeAction) + if (fixAllState.IsFixMultiple) { - if (fixAllState.IsFixMultiple) - { - removeSuppressionCodeAction = removeSuppressionCodeAction.CloneForFixMultipleContext(); - } - - bag.Add((diagnostic, removeSuppressionCodeAction)); + removeSuppressionCodeAction = removeSuppressionCodeAction.CloneForFixMultipleContext(); } + + bag.Add((diagnostic, removeSuppressionCodeAction)); } } + } - public override async Task TryGetMergedFixAsync( - ImmutableArray<(Diagnostic diagnostic, CodeAction action)> batchOfFixes, - FixAllState fixAllState, - IProgress progressTracker, - CancellationToken cancellationToken) - { - // Batch all the attribute removal fixes into a single fix. - // Pragma removal fixes have already been batch for each document AddDocumentFixes method. - // This ensures no merge conflicts in merging all fixes by our base implementation. + public override async Task TryGetMergedFixAsync( + ImmutableArray<(Diagnostic diagnostic, CodeAction action)> batchOfFixes, + FixAllState fixAllState, + IProgress progressTracker, + CancellationToken cancellationToken) + { + // Batch all the attribute removal fixes into a single fix. + // Pragma removal fixes have already been batch for each document AddDocumentFixes method. + // This ensures no merge conflicts in merging all fixes by our base implementation. - var oldSolution = fixAllState.Project.Solution; - var currentSolution = oldSolution; + var oldSolution = fixAllState.Project.Solution; + var currentSolution = oldSolution; - var attributeRemoveFixes = new List(); - var newBatchOfFixes = new List<(Diagnostic diagnostic, CodeAction action)>(); - foreach (var codeAction in batchOfFixes) + var attributeRemoveFixes = new List(); + var newBatchOfFixes = new List<(Diagnostic diagnostic, CodeAction action)>(); + foreach (var codeAction in batchOfFixes) + { + if (codeAction.action is AttributeRemoveAction attributeRemoveFix) { - if (codeAction.action is AttributeRemoveAction attributeRemoveFix) - { - attributeRemoveFixes.Add(attributeRemoveFix); - } - else - { - newBatchOfFixes.Add(codeAction); - } + attributeRemoveFixes.Add(attributeRemoveFix); } - - if (attributeRemoveFixes.Count > 0) + else { - // Batch all of attribute removal fixes. - foreach (var removeSuppressionFixesForTree in attributeRemoveFixes.GroupBy(fix => fix.SyntaxTreeToModify)) - { - var tree = removeSuppressionFixesForTree.Key; - - var attributeRemoveFixesForTree = removeSuppressionFixesForTree.OfType().ToImmutableArray(); - var attributesToRemove = await GetAttributeNodesToFixAsync(attributeRemoveFixesForTree, cancellationToken).ConfigureAwait(false); - var document = oldSolution.GetDocument(tree); - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newRoot = root.RemoveNodes(attributesToRemove, SyntaxRemoveOptions.KeepLeadingTrivia | SyntaxRemoveOptions.AddElasticMarker); - currentSolution = currentSolution.WithDocumentSyntaxRoot(document.Id, newRoot); - } - - var batchAttributeRemoveFix = CodeAction.Create( - attributeRemoveFixes.First().Title, - createChangedSolution: ct => Task.FromResult(currentSolution), - equivalenceKey: fixAllState.CodeActionEquivalenceKey); - - newBatchOfFixes.Insert(0, (diagnostic: null, batchAttributeRemoveFix)); + newBatchOfFixes.Add(codeAction); } - - return await base.TryGetMergedFixAsync( - newBatchOfFixes.ToImmutableArray(), fixAllState, progressTracker, cancellationToken).ConfigureAwait(false); } - private static async Task> GetAttributeNodesToFixAsync(ImmutableArray attributeRemoveFixes, CancellationToken cancellationToken) + if (attributeRemoveFixes.Count > 0) { - using var _ = ArrayBuilder.GetInstance(attributeRemoveFixes.Length, out var builder); - foreach (var attributeRemoveFix in attributeRemoveFixes) + // Batch all of attribute removal fixes. + foreach (var removeSuppressionFixesForTree in attributeRemoveFixes.GroupBy(fix => fix.SyntaxTreeToModify)) { - var attributeToRemove = await attributeRemoveFix.GetAttributeToRemoveAsync(cancellationToken).ConfigureAwait(false); - builder.Add(attributeToRemove); + var tree = removeSuppressionFixesForTree.Key; + + var attributeRemoveFixesForTree = removeSuppressionFixesForTree.OfType().ToImmutableArray(); + var attributesToRemove = await GetAttributeNodesToFixAsync(attributeRemoveFixesForTree, cancellationToken).ConfigureAwait(false); + var document = oldSolution.GetDocument(tree); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = root.RemoveNodes(attributesToRemove, SyntaxRemoveOptions.KeepLeadingTrivia | SyntaxRemoveOptions.AddElasticMarker); + currentSolution = currentSolution.WithDocumentSyntaxRoot(document.Id, newRoot); } - return builder.ToImmutableAndClear(); + var batchAttributeRemoveFix = CodeAction.Create( + attributeRemoveFixes.First().Title, + createChangedSolution: ct => Task.FromResult(currentSolution), + equivalenceKey: fixAllState.CodeActionEquivalenceKey); + + newBatchOfFixes.Insert(0, (diagnostic: null, batchAttributeRemoveFix)); + } + + return await base.TryGetMergedFixAsync( + newBatchOfFixes.ToImmutableArray(), fixAllState, progressTracker, cancellationToken).ConfigureAwait(false); + } + + private static async Task> GetAttributeNodesToFixAsync(ImmutableArray attributeRemoveFixes, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(attributeRemoveFixes.Length, out var builder); + foreach (var attributeRemoveFix in attributeRemoveFixes) + { + var attributeToRemove = await attributeRemoveFix.GetAttributeToRemoveAsync(cancellationToken).ConfigureAwait(false); + builder.Add(attributeToRemove); } + + return builder.ToImmutableAndClear(); } } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction.cs index 2a23aa2a0cdbb..029be4d52cdea 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction.cs @@ -9,60 +9,59 @@ using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Formatting; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider + /// + /// Base type for remove suppression code actions. + /// + internal abstract partial class RemoveSuppressionCodeAction : AbstractSuppressionCodeAction { - /// - /// Base type for remove suppression code actions. - /// - internal abstract partial class RemoveSuppressionCodeAction : AbstractSuppressionCodeAction - { - private readonly Diagnostic _diagnostic; - private readonly bool _forFixMultipleContext; + private readonly Diagnostic _diagnostic; + private readonly bool _forFixMultipleContext; - public static async Task CreateAsync( - SuppressionTargetInfo suppressionTargetInfo, - Document documentOpt, - Project project, - Diagnostic diagnostic, - AbstractSuppressionCodeFixProvider fixer, - CodeActionOptionsProvider options, - CancellationToken cancellationToken) + public static async Task CreateAsync( + SuppressionTargetInfo suppressionTargetInfo, + Document documentOpt, + Project project, + Diagnostic diagnostic, + AbstractSuppressionCodeFixProvider fixer, + CodeActionOptionsProvider options, + CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + var attribute = diagnostic.GetSuppressionInfo(compilation).Attribute; + if (attribute != null) { - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var attribute = diagnostic.GetSuppressionInfo(compilation).Attribute; - if (attribute != null) - { - return AttributeRemoveAction.Create(attribute, project, diagnostic, fixer); - } - else if (documentOpt != null && !SuppressionHelpers.IsSynthesizedExternalSourceDiagnostic(diagnostic)) - { - var formattingOptions = await documentOpt.GetSyntaxFormattingOptionsAsync(options, cancellationToken).ConfigureAwait(false); - return PragmaRemoveAction.Create(suppressionTargetInfo, documentOpt, formattingOptions, diagnostic, fixer); - } - else - { - return null; - } + return AttributeRemoveAction.Create(attribute, project, diagnostic, fixer); } - - protected RemoveSuppressionCodeAction( - Diagnostic diagnostic, - AbstractSuppressionCodeFixProvider fixer, - bool forFixMultipleContext = false) - : base(fixer, title: string.Format(FeaturesResources.Remove_Suppression_0, diagnostic.Id)) + else if (documentOpt != null && !SuppressionHelpers.IsSynthesizedExternalSourceDiagnostic(diagnostic)) { - _diagnostic = diagnostic; - _forFixMultipleContext = forFixMultipleContext; + var formattingOptions = await documentOpt.GetSyntaxFormattingOptionsAsync(options, cancellationToken).ConfigureAwait(false); + return PragmaRemoveAction.Create(suppressionTargetInfo, documentOpt, formattingOptions, diagnostic, fixer); } + else + { + return null; + } + } - public abstract RemoveSuppressionCodeAction CloneForFixMultipleContext(); - public abstract SyntaxTree SyntaxTreeToModify { get; } - - public override string EquivalenceKey => FeaturesResources.Remove_Suppression + DiagnosticIdForEquivalenceKey; - protected override string DiagnosticIdForEquivalenceKey - => _forFixMultipleContext ? string.Empty : _diagnostic.Id; + protected RemoveSuppressionCodeAction( + Diagnostic diagnostic, + AbstractSuppressionCodeFixProvider fixer, + bool forFixMultipleContext = false) + : base(fixer, title: string.Format(FeaturesResources.Remove_Suppression_0, diagnostic.Id)) + { + _diagnostic = diagnostic; + _forFixMultipleContext = forFixMultipleContext; } + + public abstract RemoveSuppressionCodeAction CloneForFixMultipleContext(); + public abstract SyntaxTree SyntaxTreeToModify { get; } + + public override string EquivalenceKey => FeaturesResources.Remove_Suppression + DiagnosticIdForEquivalenceKey; + protected override string DiagnosticIdForEquivalenceKey + => _forFixMultipleContext ? string.Empty : _diagnostic.Id; } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction_Attribute.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction_Attribute.cs index e9850885cd222..b57ecccf77291 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction_Attribute.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction_Attribute.cs @@ -9,68 +9,67 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editing; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider + internal abstract partial class RemoveSuppressionCodeAction { - internal abstract partial class RemoveSuppressionCodeAction + /// + /// Code action to remove suppress message attributes for remove suppression. + /// + private sealed class AttributeRemoveAction : RemoveSuppressionCodeAction { - /// - /// Code action to remove suppress message attributes for remove suppression. - /// - private sealed class AttributeRemoveAction : RemoveSuppressionCodeAction + private readonly Project _project; + private readonly AttributeData _attribute; + + public static AttributeRemoveAction Create( + AttributeData attribute, + Project project, + Diagnostic diagnostic, + AbstractSuppressionCodeFixProvider fixer) { - private readonly Project _project; - private readonly AttributeData _attribute; + return new AttributeRemoveAction(attribute, project, diagnostic, fixer); + } - public static AttributeRemoveAction Create( - AttributeData attribute, - Project project, - Diagnostic diagnostic, - AbstractSuppressionCodeFixProvider fixer) - { - return new AttributeRemoveAction(attribute, project, diagnostic, fixer); - } + private AttributeRemoveAction( + AttributeData attribute, + Project project, + Diagnostic diagnostic, + AbstractSuppressionCodeFixProvider fixer, + bool forFixMultipleContext = false) + : base(diagnostic, fixer, forFixMultipleContext) + { + _project = project; + _attribute = attribute; + } - private AttributeRemoveAction( - AttributeData attribute, - Project project, - Diagnostic diagnostic, - AbstractSuppressionCodeFixProvider fixer, - bool forFixMultipleContext = false) - : base(diagnostic, fixer, forFixMultipleContext) - { - _project = project; - _attribute = attribute; - } + public override RemoveSuppressionCodeAction CloneForFixMultipleContext() + => new AttributeRemoveAction(_attribute, _project, _diagnostic, Fixer, forFixMultipleContext: true); - public override RemoveSuppressionCodeAction CloneForFixMultipleContext() - => new AttributeRemoveAction(_attribute, _project, _diagnostic, Fixer, forFixMultipleContext: true); + public override SyntaxTree SyntaxTreeToModify => _attribute.ApplicationSyntaxReference.SyntaxTree; - public override SyntaxTree SyntaxTreeToModify => _attribute.ApplicationSyntaxReference.SyntaxTree; + public async Task GetAttributeToRemoveAsync(CancellationToken cancellationToken) + { + var attributeNode = await _attribute.ApplicationSyntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + return Fixer.IsSingleAttributeInAttributeList(attributeNode) + ? attributeNode.Parent + : attributeNode; + } - public async Task GetAttributeToRemoveAsync(CancellationToken cancellationToken) + protected override async Task GetChangedSolutionAsync( + IProgress progress, CancellationToken cancellationToken) + { + var attributeNode = await GetAttributeToRemoveAsync(cancellationToken).ConfigureAwait(false); + var documentWithAttribute = _project.GetDocument(attributeNode.SyntaxTree); + if (documentWithAttribute == null) { - var attributeNode = await _attribute.ApplicationSyntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); - return Fixer.IsSingleAttributeInAttributeList(attributeNode) - ? attributeNode.Parent - : attributeNode; + return _project.Solution; } - protected override async Task GetChangedSolutionAsync( - IProgress progress, CancellationToken cancellationToken) - { - var attributeNode = await GetAttributeToRemoveAsync(cancellationToken).ConfigureAwait(false); - var documentWithAttribute = _project.GetDocument(attributeNode.SyntaxTree); - if (documentWithAttribute == null) - { - return _project.Solution; - } - - var editor = await DocumentEditor.CreateAsync(documentWithAttribute, cancellationToken).ConfigureAwait(false); - editor.RemoveNode(attributeNode); - return editor.GetChangedDocument().Project.Solution; - } + var editor = await DocumentEditor.CreateAsync(documentWithAttribute, cancellationToken).ConfigureAwait(false); + editor.RemoveNode(attributeNode); + return editor.GetChangedDocument().Project.Solution; } } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction_Pragma.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction_Pragma.cs index b374bbaf1ece5..8c519f3b4459a 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction_Pragma.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction_Pragma.cs @@ -13,213 +13,212 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider + internal abstract partial class RemoveSuppressionCodeAction { - internal abstract partial class RemoveSuppressionCodeAction + /// + /// Code action to edit/remove/add the pragma directives for removing diagnostic suppression. + /// + private class PragmaRemoveAction : RemoveSuppressionCodeAction, IPragmaBasedCodeAction { - /// - /// Code action to edit/remove/add the pragma directives for removing diagnostic suppression. - /// - private class PragmaRemoveAction : RemoveSuppressionCodeAction, IPragmaBasedCodeAction + private readonly Document _document; + private readonly SyntaxFormattingOptions _options; + private readonly SuppressionTargetInfo _suppressionTargetInfo; + + public static PragmaRemoveAction Create( + SuppressionTargetInfo suppressionTargetInfo, + Document document, + SyntaxFormattingOptions options, + Diagnostic diagnostic, + AbstractSuppressionCodeFixProvider fixer) { - private readonly Document _document; - private readonly SyntaxFormattingOptions _options; - private readonly SuppressionTargetInfo _suppressionTargetInfo; - - public static PragmaRemoveAction Create( - SuppressionTargetInfo suppressionTargetInfo, - Document document, - SyntaxFormattingOptions options, - Diagnostic diagnostic, - AbstractSuppressionCodeFixProvider fixer) - { - // We need to normalize the leading trivia on start token to account for - // the trailing trivia on its previous token (and similarly normalize trailing trivia for end token). - PragmaHelpers.NormalizeTriviaOnTokens(fixer, ref document, ref suppressionTargetInfo); - - return new PragmaRemoveAction(suppressionTargetInfo, document, options, diagnostic, fixer); - } + // We need to normalize the leading trivia on start token to account for + // the trailing trivia on its previous token (and similarly normalize trailing trivia for end token). + PragmaHelpers.NormalizeTriviaOnTokens(fixer, ref document, ref suppressionTargetInfo); - private PragmaRemoveAction( - SuppressionTargetInfo suppressionTargetInfo, - Document document, - SyntaxFormattingOptions options, - Diagnostic diagnostic, - AbstractSuppressionCodeFixProvider fixer, - bool forFixMultipleContext = false) - : base(diagnostic, fixer, forFixMultipleContext) - { - _document = document; - _options = options; - _suppressionTargetInfo = suppressionTargetInfo; - } - - public override RemoveSuppressionCodeAction CloneForFixMultipleContext() - => new PragmaRemoveAction(_suppressionTargetInfo, _document, _options, _diagnostic, Fixer, forFixMultipleContext: true); + return new PragmaRemoveAction(suppressionTargetInfo, document, options, diagnostic, fixer); + } - public override SyntaxTree SyntaxTreeToModify => _suppressionTargetInfo.StartToken.SyntaxTree; + private PragmaRemoveAction( + SuppressionTargetInfo suppressionTargetInfo, + Document document, + SyntaxFormattingOptions options, + Diagnostic diagnostic, + AbstractSuppressionCodeFixProvider fixer, + bool forFixMultipleContext = false) + : base(diagnostic, fixer, forFixMultipleContext) + { + _document = document; + _options = options; + _suppressionTargetInfo = suppressionTargetInfo; + } - protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) - => await GetChangedDocumentAsync(includeStartTokenChange: true, includeEndTokenChange: true, cancellationToken: cancellationToken).ConfigureAwait(false); + public override RemoveSuppressionCodeAction CloneForFixMultipleContext() + => new PragmaRemoveAction(_suppressionTargetInfo, _document, _options, _diagnostic, Fixer, forFixMultipleContext: true); - public async Task GetChangedDocumentAsync(bool includeStartTokenChange, bool includeEndTokenChange, CancellationToken cancellationToken) - { - var add = false; - var toggle = false; + public override SyntaxTree SyntaxTreeToModify => _suppressionTargetInfo.StartToken.SyntaxTree; - int indexOfLeadingPragmaDisableToRemove = -1, indexOfTrailingPragmaEnableToRemove = -1; - if (CanRemovePragmaTrivia(_suppressionTargetInfo.StartToken, _diagnostic, Fixer, isStartToken: true, indexOfTriviaToRemove: out indexOfLeadingPragmaDisableToRemove) && - CanRemovePragmaTrivia(_suppressionTargetInfo.EndToken, _diagnostic, Fixer, isStartToken: false, indexOfTriviaToRemove: out indexOfTrailingPragmaEnableToRemove)) - { - // Verify if there is no other trivia before the start token would again cause this diagnostic to be suppressed. - // If invalidated, then we just toggle existing pragma enable and disable directives before and start of the line. - // If not, then we just remove the existing pragma trivia surrounding the line. - toggle = await IsDiagnosticSuppressedBeforeLeadingPragmaAsync(indexOfLeadingPragmaDisableToRemove, cancellationToken).ConfigureAwait(false); - } - else - { - // Otherwise, just add a pragma enable before the start token and a pragma restore after it. - add = true; - } + protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) + => await GetChangedDocumentAsync(includeStartTokenChange: true, includeEndTokenChange: true, cancellationToken: cancellationToken).ConfigureAwait(false); - SyntaxToken getNewStartToken(SyntaxToken startToken, TextSpan currentDiagnosticSpan) => includeStartTokenChange - ? GetNewTokenWithModifiedPragma(startToken, currentDiagnosticSpan, add, toggle, indexOfLeadingPragmaDisableToRemove, isStartToken: true, cancellationToken) - : startToken; - - SyntaxToken getNewEndToken(SyntaxToken endToken, TextSpan currentDiagnosticSpan) => includeEndTokenChange - ? GetNewTokenWithModifiedPragma(endToken, currentDiagnosticSpan, add, toggle, indexOfTrailingPragmaEnableToRemove, isStartToken: false, cancellationToken) - : endToken; - - return await PragmaHelpers.GetChangeDocumentWithPragmaAdjustedAsync( - _document, - _diagnostic.Location.SourceSpan, - _suppressionTargetInfo, - getNewStartToken, - getNewEndToken, - cancellationToken).ConfigureAwait(false); - } + public async Task GetChangedDocumentAsync(bool includeStartTokenChange, bool includeEndTokenChange, CancellationToken cancellationToken) + { + var add = false; + var toggle = false; - private static SyntaxTriviaList GetTriviaListForSuppression(SyntaxToken token, bool isStartToken, AbstractSuppressionCodeFixProvider fixer) + int indexOfLeadingPragmaDisableToRemove = -1, indexOfTrailingPragmaEnableToRemove = -1; + if (CanRemovePragmaTrivia(_suppressionTargetInfo.StartToken, _diagnostic, Fixer, isStartToken: true, indexOfTriviaToRemove: out indexOfLeadingPragmaDisableToRemove) && + CanRemovePragmaTrivia(_suppressionTargetInfo.EndToken, _diagnostic, Fixer, isStartToken: false, indexOfTriviaToRemove: out indexOfTrailingPragmaEnableToRemove)) { - return isStartToken || fixer.IsEndOfFileToken(token) - ? token.LeadingTrivia - : token.TrailingTrivia; + // Verify if there is no other trivia before the start token would again cause this diagnostic to be suppressed. + // If invalidated, then we just toggle existing pragma enable and disable directives before and start of the line. + // If not, then we just remove the existing pragma trivia surrounding the line. + toggle = await IsDiagnosticSuppressedBeforeLeadingPragmaAsync(indexOfLeadingPragmaDisableToRemove, cancellationToken).ConfigureAwait(false); } - - private static SyntaxToken UpdateTriviaList(SyntaxToken token, bool isStartToken, SyntaxTriviaList triviaList, AbstractSuppressionCodeFixProvider fixer) + else { - return isStartToken || fixer.IsEndOfFileToken(token) - ? token.WithLeadingTrivia(triviaList) - : token.WithTrailingTrivia(triviaList); + // Otherwise, just add a pragma enable before the start token and a pragma restore after it. + add = true; } - private static bool CanRemovePragmaTrivia(SyntaxToken token, Diagnostic diagnostic, AbstractSuppressionCodeFixProvider fixer, bool isStartToken, out int indexOfTriviaToRemove) - { - indexOfTriviaToRemove = -1; - - var triviaList = GetTriviaListForSuppression(token, isStartToken, fixer); + SyntaxToken getNewStartToken(SyntaxToken startToken, TextSpan currentDiagnosticSpan) => includeStartTokenChange + ? GetNewTokenWithModifiedPragma(startToken, currentDiagnosticSpan, add, toggle, indexOfLeadingPragmaDisableToRemove, isStartToken: true, cancellationToken) + : startToken; + + SyntaxToken getNewEndToken(SyntaxToken endToken, TextSpan currentDiagnosticSpan) => includeEndTokenChange + ? GetNewTokenWithModifiedPragma(endToken, currentDiagnosticSpan, add, toggle, indexOfTrailingPragmaEnableToRemove, isStartToken: false, cancellationToken) + : endToken; + + return await PragmaHelpers.GetChangeDocumentWithPragmaAdjustedAsync( + _document, + _diagnostic.Location.SourceSpan, + _suppressionTargetInfo, + getNewStartToken, + getNewEndToken, + cancellationToken).ConfigureAwait(false); + } - var diagnosticSpan = diagnostic.Location.SourceSpan; - bool shouldIncludeTrivia(SyntaxTrivia t) => isStartToken ? t.FullSpan.End <= diagnosticSpan.Start : t.FullSpan.Start >= diagnosticSpan.End; - var filteredTriviaList = triviaList.Where(shouldIncludeTrivia); - if (isStartToken) - { - // Walk bottom up for leading trivia. - filteredTriviaList = filteredTriviaList.Reverse(); - } + private static SyntaxTriviaList GetTriviaListForSuppression(SyntaxToken token, bool isStartToken, AbstractSuppressionCodeFixProvider fixer) + { + return isStartToken || fixer.IsEndOfFileToken(token) + ? token.LeadingTrivia + : token.TrailingTrivia; + } - foreach (var trivia in filteredTriviaList) - { - if (fixer.IsAnyPragmaDirectiveForId(trivia, diagnostic.Id, out var isEnableDirective, out var hasMultipleIds)) - { - if (hasMultipleIds) - { - // Handle only simple cases where we have a single pragma directive with single ID matching ours in the trivia. - return false; - } - - // We want to look for leading disable directive and trailing enable directive. - if ((isStartToken && !isEnableDirective) || - (!isStartToken && isEnableDirective)) - { - indexOfTriviaToRemove = triviaList.IndexOf(trivia); - return true; - } + private static SyntaxToken UpdateTriviaList(SyntaxToken token, bool isStartToken, SyntaxTriviaList triviaList, AbstractSuppressionCodeFixProvider fixer) + { + return isStartToken || fixer.IsEndOfFileToken(token) + ? token.WithLeadingTrivia(triviaList) + : token.WithTrailingTrivia(triviaList); + } - return false; - } - } + private static bool CanRemovePragmaTrivia(SyntaxToken token, Diagnostic diagnostic, AbstractSuppressionCodeFixProvider fixer, bool isStartToken, out int indexOfTriviaToRemove) + { + indexOfTriviaToRemove = -1; - return false; - } + var triviaList = GetTriviaListForSuppression(token, isStartToken, fixer); - private SyntaxToken GetNewTokenWithModifiedPragma(SyntaxToken token, TextSpan currentDiagnosticSpan, bool add, bool toggle, int indexOfTriviaToRemoveOrToggle, bool isStartToken, CancellationToken cancellationToken) + var diagnosticSpan = diagnostic.Location.SourceSpan; + bool shouldIncludeTrivia(SyntaxTrivia t) => isStartToken ? t.FullSpan.End <= diagnosticSpan.Start : t.FullSpan.Start >= diagnosticSpan.End; + var filteredTriviaList = triviaList.Where(shouldIncludeTrivia); + if (isStartToken) { - return add - ? GetNewTokenWithAddedPragma(token, currentDiagnosticSpan, isStartToken, cancellationToken) - : GetNewTokenWithRemovedOrToggledPragma(token, indexOfTriviaToRemoveOrToggle, isStartToken, toggle); + // Walk bottom up for leading trivia. + filteredTriviaList = filteredTriviaList.Reverse(); } - private SyntaxToken GetNewTokenWithAddedPragma(SyntaxToken token, TextSpan currentDiagnosticSpan, bool isStartToken, CancellationToken cancellationToken) + foreach (var trivia in filteredTriviaList) { - if (isStartToken) - { - return PragmaHelpers.GetNewStartTokenWithAddedPragma(token, currentDiagnosticSpan, _diagnostic, Fixer, FormatNode, isRemoveSuppression: true, cancellationToken); - } - else + if (fixer.IsAnyPragmaDirectiveForId(trivia, diagnostic.Id, out var isEnableDirective, out var hasMultipleIds)) { - return PragmaHelpers.GetNewEndTokenWithAddedPragma(token, currentDiagnosticSpan, _diagnostic, Fixer, FormatNode, isRemoveSuppression: true, cancellationToken); + if (hasMultipleIds) + { + // Handle only simple cases where we have a single pragma directive with single ID matching ours in the trivia. + return false; + } + + // We want to look for leading disable directive and trailing enable directive. + if ((isStartToken && !isEnableDirective) || + (!isStartToken && isEnableDirective)) + { + indexOfTriviaToRemove = triviaList.IndexOf(trivia); + return true; + } + + return false; } } - private SyntaxToken GetNewTokenWithRemovedOrToggledPragma(SyntaxToken token, int indexOfTriviaToRemoveOrToggle, bool isStartToken, bool toggle) - => GetNewTokenWithPragmaUnsuppress(token, indexOfTriviaToRemoveOrToggle, Fixer, isStartToken, toggle); + return false; + } + + private SyntaxToken GetNewTokenWithModifiedPragma(SyntaxToken token, TextSpan currentDiagnosticSpan, bool add, bool toggle, int indexOfTriviaToRemoveOrToggle, bool isStartToken, CancellationToken cancellationToken) + { + return add + ? GetNewTokenWithAddedPragma(token, currentDiagnosticSpan, isStartToken, cancellationToken) + : GetNewTokenWithRemovedOrToggledPragma(token, indexOfTriviaToRemoveOrToggle, isStartToken, toggle); + } - private static SyntaxToken GetNewTokenWithPragmaUnsuppress(SyntaxToken token, int indexOfTriviaToRemoveOrToggle, AbstractSuppressionCodeFixProvider fixer, bool isStartToken, bool toggle) + private SyntaxToken GetNewTokenWithAddedPragma(SyntaxToken token, TextSpan currentDiagnosticSpan, bool isStartToken, CancellationToken cancellationToken) + { + if (isStartToken) { - Contract.ThrowIfFalse(indexOfTriviaToRemoveOrToggle >= 0); + return PragmaHelpers.GetNewStartTokenWithAddedPragma(token, currentDiagnosticSpan, _diagnostic, Fixer, FormatNode, isRemoveSuppression: true, cancellationToken); + } + else + { + return PragmaHelpers.GetNewEndTokenWithAddedPragma(token, currentDiagnosticSpan, _diagnostic, Fixer, FormatNode, isRemoveSuppression: true, cancellationToken); + } + } - var triviaList = GetTriviaListForSuppression(token, isStartToken, fixer); + private SyntaxToken GetNewTokenWithRemovedOrToggledPragma(SyntaxToken token, int indexOfTriviaToRemoveOrToggle, bool isStartToken, bool toggle) + => GetNewTokenWithPragmaUnsuppress(token, indexOfTriviaToRemoveOrToggle, Fixer, isStartToken, toggle); - if (toggle) - { - var triviaToToggle = triviaList.ElementAt(indexOfTriviaToRemoveOrToggle); - Contract.ThrowIfFalse(triviaToToggle != default); - var toggledTrivia = fixer.TogglePragmaDirective(triviaToToggle); - triviaList = triviaList.Replace(triviaToToggle, toggledTrivia); - } - else - { - triviaList = triviaList.RemoveAt(indexOfTriviaToRemoveOrToggle); - } + private static SyntaxToken GetNewTokenWithPragmaUnsuppress(SyntaxToken token, int indexOfTriviaToRemoveOrToggle, AbstractSuppressionCodeFixProvider fixer, bool isStartToken, bool toggle) + { + Contract.ThrowIfFalse(indexOfTriviaToRemoveOrToggle >= 0); - return UpdateTriviaList(token, isStartToken, triviaList, fixer); - } + var triviaList = GetTriviaListForSuppression(token, isStartToken, fixer); - private async Task IsDiagnosticSuppressedBeforeLeadingPragmaAsync(int indexOfPragma, CancellationToken cancellationToken) + if (toggle) + { + var triviaToToggle = triviaList.ElementAt(indexOfTriviaToRemoveOrToggle); + Contract.ThrowIfFalse(triviaToToggle != default); + var toggledTrivia = fixer.TogglePragmaDirective(triviaToToggle); + triviaList = triviaList.Replace(triviaToToggle, toggledTrivia); + } + else { - var model = await _document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var tree = model.SyntaxTree; - - // get the warning state of this diagnostic ID at the start of the pragma - var trivia = _suppressionTargetInfo.StartToken.LeadingTrivia.ElementAt(indexOfPragma); - var spanToCheck = new TextSpan( - start: Math.Max(0, trivia.Span.Start - 1), - length: 1); - var locationToCheck = Location.Create(tree, spanToCheck); - var dummyDiagnosticWithLocationToCheck = Diagnostic.Create(_diagnostic.Descriptor, locationToCheck); - var effectiveDiagnostic = CompilationWithAnalyzers.GetEffectiveDiagnostics(new[] { dummyDiagnosticWithLocationToCheck }, model.Compilation).FirstOrDefault(); - return effectiveDiagnostic == null || effectiveDiagnostic.IsSuppressed; + triviaList = triviaList.RemoveAt(indexOfTriviaToRemoveOrToggle); } - public SyntaxToken StartToken_TestOnly => _suppressionTargetInfo.StartToken; - public SyntaxToken EndToken_TestOnly => _suppressionTargetInfo.EndToken; + return UpdateTriviaList(token, isStartToken, triviaList, fixer); + } - private SyntaxNode FormatNode(SyntaxNode node, CancellationToken cancellationToken) - => Formatter.Format(node, _document.Project.Solution.Services, _options, cancellationToken); + private async Task IsDiagnosticSuppressedBeforeLeadingPragmaAsync(int indexOfPragma, CancellationToken cancellationToken) + { + var model = await _document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var tree = model.SyntaxTree; + + // get the warning state of this diagnostic ID at the start of the pragma + var trivia = _suppressionTargetInfo.StartToken.LeadingTrivia.ElementAt(indexOfPragma); + var spanToCheck = new TextSpan( + start: Math.Max(0, trivia.Span.Start - 1), + length: 1); + var locationToCheck = Location.Create(tree, spanToCheck); + var dummyDiagnosticWithLocationToCheck = Diagnostic.Create(_diagnostic.Descriptor, locationToCheck); + var effectiveDiagnostic = CompilationWithAnalyzers.GetEffectiveDiagnostics(new[] { dummyDiagnosticWithLocationToCheck }, model.Compilation).FirstOrDefault(); + return effectiveDiagnostic == null || effectiveDiagnostic.IsSuppressed; } + + public SyntaxToken StartToken_TestOnly => _suppressionTargetInfo.StartToken; + public SyntaxToken EndToken_TestOnly => _suppressionTargetInfo.EndToken; + + private SyntaxNode FormatNode(SyntaxNode node, CancellationToken cancellationToken) + => Formatter.Format(node, _document.Project.Solution.Services, _options, cancellationToken); } } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.cs index a7d88c7d46043..410c85da807df 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.cs @@ -21,360 +21,359 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { - internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider - { - public const string SuppressMessageAttributeName = "System.Diagnostics.CodeAnalysis.SuppressMessage"; - private const string s_globalSuppressionsFileName = "GlobalSuppressions"; - private const string s_suppressionsFileCommentTemplate = + public const string SuppressMessageAttributeName = "System.Diagnostics.CodeAnalysis.SuppressMessage"; + private const string s_globalSuppressionsFileName = "GlobalSuppressions"; + private const string s_suppressionsFileCommentTemplate = @"{0} This file is used by Code Analysis to maintain SuppressMessage {0} attributes that are applied to this project. {0} Project-level suppressions either have no target or are given {0} a specific target and scoped to a namespace, type, member, etc. "; - protected AbstractSuppressionCodeFixProvider() + protected AbstractSuppressionCodeFixProvider() + { + } + + public FixAllProvider GetFixAllProvider() + => SuppressionFixAllProvider.Instance; + + public bool IsFixableDiagnostic(Diagnostic diagnostic) + => SuppressionHelpers.CanBeSuppressed(diagnostic) || SuppressionHelpers.CanBeUnsuppressed(diagnostic); + + protected abstract SyntaxTriviaList CreatePragmaDisableDirectiveTrivia(Diagnostic diagnostic, Func formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken); + protected abstract SyntaxTriviaList CreatePragmaRestoreDirectiveTrivia(Diagnostic diagnostic, Func formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken); + + protected abstract SyntaxNode AddGlobalSuppressMessageAttribute( + SyntaxNode newRoot, + ISymbol targetSymbol, + INamedTypeSymbol suppressMessageAttribute, + Diagnostic diagnostic, + SolutionServices services, + SyntaxFormattingOptions options, + IAddImportsService addImportsService, + CancellationToken cancellationToken); + + protected abstract SyntaxNode AddLocalSuppressMessageAttribute( + SyntaxNode targetNode, ISymbol targetSymbol, INamedTypeSymbol suppressMessageAttribute, Diagnostic diagnostic); + + protected abstract string DefaultFileExtension { get; } + protected abstract string SingleLineCommentStart { get; } + protected abstract bool IsAttributeListWithAssemblyAttributes(SyntaxNode node); + protected abstract bool IsEndOfLine(SyntaxTrivia trivia); + protected abstract bool IsEndOfFileToken(SyntaxToken token); + protected abstract bool IsSingleAttributeInAttributeList(SyntaxNode attribute); + protected abstract bool IsAnyPragmaDirectiveForId(SyntaxTrivia trivia, string id, out bool enableDirective, out bool hasMultipleIds); + protected abstract SyntaxTrivia TogglePragmaDirective(SyntaxTrivia trivia); + + protected string GlobalSuppressionsFileHeaderComment + { + get { + return string.Format(s_suppressionsFileCommentTemplate, SingleLineCommentStart); } + } - public FixAllProvider GetFixAllProvider() - => SuppressionFixAllProvider.Instance; - - public bool IsFixableDiagnostic(Diagnostic diagnostic) - => SuppressionHelpers.CanBeSuppressed(diagnostic) || SuppressionHelpers.CanBeUnsuppressed(diagnostic); - - protected abstract SyntaxTriviaList CreatePragmaDisableDirectiveTrivia(Diagnostic diagnostic, Func formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken); - protected abstract SyntaxTriviaList CreatePragmaRestoreDirectiveTrivia(Diagnostic diagnostic, Func formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken); - - protected abstract SyntaxNode AddGlobalSuppressMessageAttribute( - SyntaxNode newRoot, - ISymbol targetSymbol, - INamedTypeSymbol suppressMessageAttribute, - Diagnostic diagnostic, - SolutionServices services, - SyntaxFormattingOptions options, - IAddImportsService addImportsService, - CancellationToken cancellationToken); - - protected abstract SyntaxNode AddLocalSuppressMessageAttribute( - SyntaxNode targetNode, ISymbol targetSymbol, INamedTypeSymbol suppressMessageAttribute, Diagnostic diagnostic); - - protected abstract string DefaultFileExtension { get; } - protected abstract string SingleLineCommentStart { get; } - protected abstract bool IsAttributeListWithAssemblyAttributes(SyntaxNode node); - protected abstract bool IsEndOfLine(SyntaxTrivia trivia); - protected abstract bool IsEndOfFileToken(SyntaxToken token); - protected abstract bool IsSingleAttributeInAttributeList(SyntaxNode attribute); - protected abstract bool IsAnyPragmaDirectiveForId(SyntaxTrivia trivia, string id, out bool enableDirective, out bool hasMultipleIds); - protected abstract SyntaxTrivia TogglePragmaDirective(SyntaxTrivia trivia); - - protected string GlobalSuppressionsFileHeaderComment + protected static string GetOrMapDiagnosticId(Diagnostic diagnostic, out bool includeTitle) + { + if (diagnostic.Id == IDEDiagnosticIds.FormattingDiagnosticId) { - get - { - return string.Format(s_suppressionsFileCommentTemplate, SingleLineCommentStart); - } + includeTitle = false; + return FormattingDiagnosticIds.FormatDocumentControlDiagnosticId; } - protected static string GetOrMapDiagnosticId(Diagnostic diagnostic, out bool includeTitle) - { - if (diagnostic.Id == IDEDiagnosticIds.FormattingDiagnosticId) - { - includeTitle = false; - return FormattingDiagnosticIds.FormatDocumentControlDiagnosticId; - } + includeTitle = true; + return diagnostic.Id; + } - includeTitle = true; - return diagnostic.Id; + protected abstract SyntaxNode GetContainingStatement(SyntaxToken token); + protected abstract bool TokenHasTrailingLineContinuationChar(SyntaxToken token); + + protected SyntaxToken GetAdjustedTokenForPragmaDisable(SyntaxToken token, SyntaxNode root, TextLineCollection lines) + { + var containingStatement = GetContainingStatement(token); + + // The containing statement might not start on the same line as the token, but we don't want to split + // a statement in the middle, so we actually want to use the first token on the line that has the first token + // of the statement. + // + // eg, given: public void M() { int x = 1; } + // + // When trying to suppress an "unused local" for x, token would be "x", the first token + // of the containing statement is "int", but we want the pragma before "public". + if (containingStatement is not null && containingStatement.GetFirstToken() != token) + { + var indexOfLine = lines.IndexOf(containingStatement.GetFirstToken().SpanStart); + var line = lines[indexOfLine]; + token = root.FindToken(line.Start); } - protected abstract SyntaxNode GetContainingStatement(SyntaxToken token); - protected abstract bool TokenHasTrailingLineContinuationChar(SyntaxToken token); + return token; + } - protected SyntaxToken GetAdjustedTokenForPragmaDisable(SyntaxToken token, SyntaxNode root, TextLineCollection lines) - { - var containingStatement = GetContainingStatement(token); - - // The containing statement might not start on the same line as the token, but we don't want to split - // a statement in the middle, so we actually want to use the first token on the line that has the first token - // of the statement. - // - // eg, given: public void M() { int x = 1; } - // - // When trying to suppress an "unused local" for x, token would be "x", the first token - // of the containing statement is "int", but we want the pragma before "public". - if (containingStatement is not null && containingStatement.GetFirstToken() != token) - { - var indexOfLine = lines.IndexOf(containingStatement.GetFirstToken().SpanStart); - var line = lines[indexOfLine]; - token = root.FindToken(line.Start); - } + private SyntaxToken GetAdjustedTokenForPragmaRestore(SyntaxToken token, SyntaxNode root, TextLineCollection lines, int indexOfLine) + { + var containingStatement = GetContainingStatement(token); - return token; + // As per above, the last token of the statement might not be the last token on the line + if (containingStatement is not null && containingStatement.GetLastToken() != token) + { + indexOfLine = lines.IndexOf(containingStatement.GetLastToken().SpanStart); } - private SyntaxToken GetAdjustedTokenForPragmaRestore(SyntaxToken token, SyntaxNode root, TextLineCollection lines, int indexOfLine) + var line = lines[indexOfLine]; + token = root.FindToken(line.End); + + // VB has line continuation characters that can explicitly extend the line beyond the last + // token, so allow for that by just skipping over them + while (TokenHasTrailingLineContinuationChar(token)) { - var containingStatement = GetContainingStatement(token); + indexOfLine = indexOfLine + 1; + token = root.FindToken(lines[indexOfLine].End, findInsideTrivia: true); + } - // As per above, the last token of the statement might not be the last token on the line - if (containingStatement is not null && containingStatement.GetLastToken() != token) - { - indexOfLine = lines.IndexOf(containingStatement.GetLastToken().SpanStart); - } + return token; + } - var line = lines[indexOfLine]; - token = root.FindToken(line.End); + public Task> GetFixesAsync( + TextDocument textDocument, TextSpan span, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + if (textDocument is not Document document) + return Task.FromResult(ImmutableArray.Empty); - // VB has line continuation characters that can explicitly extend the line beyond the last - // token, so allow for that by just skipping over them - while (TokenHasTrailingLineContinuationChar(token)) - { - indexOfLine = indexOfLine + 1; - token = root.FindToken(lines[indexOfLine].End, findInsideTrivia: true); - } + return GetSuppressionsAsync(document, span, diagnostics, fallbackOptions, skipSuppressMessage: false, skipUnsuppress: false, cancellationToken: cancellationToken); + } - return token; - } + internal async Task> GetPragmaSuppressionsAsync(Document document, TextSpan span, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var codeFixes = await GetSuppressionsAsync(document, span, diagnostics, fallbackOptions, skipSuppressMessage: true, skipUnsuppress: true, cancellationToken: cancellationToken).ConfigureAwait(false); + return codeFixes.SelectMany(fix => fix.Action.NestedActions) + .OfType() + .ToImmutableArray(); + } - public Task> GetFixesAsync( - TextDocument textDocument, TextSpan span, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + private async Task> GetSuppressionsAsync( + Document document, TextSpan span, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, bool skipSuppressMessage, bool skipUnsuppress, CancellationToken cancellationToken) + { + var suppressionTargetInfo = await GetSuppressionTargetInfoAsync(document, span, cancellationToken).ConfigureAwait(false); + if (suppressionTargetInfo == null) { - if (textDocument is not Document document) - return Task.FromResult(ImmutableArray.Empty); - - return GetSuppressionsAsync(document, span, diagnostics, fallbackOptions, skipSuppressMessage: false, skipUnsuppress: false, cancellationToken: cancellationToken); + return []; } - internal async Task> GetPragmaSuppressionsAsync(Document document, TextSpan span, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + return await GetSuppressionsAsync( + document, document.Project, diagnostics, suppressionTargetInfo, fallbackOptions, skipSuppressMessage, skipUnsuppress, cancellationToken).ConfigureAwait(false); + } + + public async Task> GetFixesAsync( + Project project, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + if (!project.SupportsCompilation) { - var codeFixes = await GetSuppressionsAsync(document, span, diagnostics, fallbackOptions, skipSuppressMessage: true, skipUnsuppress: true, cancellationToken: cancellationToken).ConfigureAwait(false); - return codeFixes.SelectMany(fix => fix.Action.NestedActions) - .OfType() - .ToImmutableArray(); + return []; } - private async Task> GetSuppressionsAsync( - Document document, TextSpan span, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, bool skipSuppressMessage, bool skipUnsuppress, CancellationToken cancellationToken) - { - var suppressionTargetInfo = await GetSuppressionTargetInfoAsync(document, span, cancellationToken).ConfigureAwait(false); - if (suppressionTargetInfo == null) - { - return []; - } + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + var suppressionTargetInfo = new SuppressionTargetInfo() { TargetSymbol = compilation.Assembly }; + return await GetSuppressionsAsync( + documentOpt: null, project, diagnostics, suppressionTargetInfo, + fallbackOptions, skipSuppressMessage: false, skipUnsuppress: false, + cancellationToken).ConfigureAwait(false); + } - return await GetSuppressionsAsync( - document, document.Project, diagnostics, suppressionTargetInfo, fallbackOptions, skipSuppressMessage, skipUnsuppress, cancellationToken).ConfigureAwait(false); + private async Task> GetSuppressionsAsync( + Document documentOpt, Project project, IEnumerable diagnostics, SuppressionTargetInfo suppressionTargetInfo, CodeActionOptionsProvider fallbackOptions, bool skipSuppressMessage, bool skipUnsuppress, CancellationToken cancellationToken) + { + // We only care about diagnostics that can be suppressed/unsuppressed. + diagnostics = diagnostics.Where(IsFixableDiagnostic); + if (diagnostics.IsEmpty()) + { + return []; } - public async Task> GetFixesAsync( - Project project, IEnumerable diagnostics, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + INamedTypeSymbol suppressMessageAttribute = null; + if (!skipSuppressMessage) { - if (!project.SupportsCompilation) - { - return []; - } - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var suppressionTargetInfo = new SuppressionTargetInfo() { TargetSymbol = compilation.Assembly }; - return await GetSuppressionsAsync( - documentOpt: null, project, diagnostics, suppressionTargetInfo, - fallbackOptions, skipSuppressMessage: false, skipUnsuppress: false, - cancellationToken).ConfigureAwait(false); + suppressMessageAttribute = compilation.SuppressMessageAttributeType(); + skipSuppressMessage = suppressMessageAttribute == null || !suppressMessageAttribute.IsAttribute(); } - private async Task> GetSuppressionsAsync( - Document documentOpt, Project project, IEnumerable diagnostics, SuppressionTargetInfo suppressionTargetInfo, CodeActionOptionsProvider fallbackOptions, bool skipSuppressMessage, bool skipUnsuppress, CancellationToken cancellationToken) + var lazyFormattingOptions = (SyntaxFormattingOptions)null; + var result = ArrayBuilder.GetInstance(); + foreach (var diagnostic in diagnostics) { - // We only care about diagnostics that can be suppressed/unsuppressed. - diagnostics = diagnostics.Where(IsFixableDiagnostic); - if (diagnostics.IsEmpty()) - { - return []; - } - - INamedTypeSymbol suppressMessageAttribute = null; - if (!skipSuppressMessage) + if (!diagnostic.IsSuppressed) { - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - suppressMessageAttribute = compilation.SuppressMessageAttributeType(); - skipSuppressMessage = suppressMessageAttribute == null || !suppressMessageAttribute.IsAttribute(); - } - - var lazyFormattingOptions = (SyntaxFormattingOptions)null; - var result = ArrayBuilder.GetInstance(); - foreach (var diagnostic in diagnostics) - { - if (!diagnostic.IsSuppressed) + var nestedActions = ArrayBuilder.GetInstance(); + if (diagnostic.Location.IsInSource && documentOpt != null) { - var nestedActions = ArrayBuilder.GetInstance(); - if (diagnostic.Location.IsInSource && documentOpt != null) - { - // pragma warning disable. - lazyFormattingOptions ??= await documentOpt.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - nestedActions.Add(PragmaWarningCodeAction.Create(suppressionTargetInfo, documentOpt, lazyFormattingOptions, diagnostic, this)); - } + // pragma warning disable. + lazyFormattingOptions ??= await documentOpt.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + nestedActions.Add(PragmaWarningCodeAction.Create(suppressionTargetInfo, documentOpt, lazyFormattingOptions, diagnostic, this)); + } - // SuppressMessageAttribute suppression is not supported for compiler diagnostics. - if (!skipSuppressMessage && SuppressionHelpers.CanBeSuppressedWithAttribute(diagnostic)) + // SuppressMessageAttribute suppression is not supported for compiler diagnostics. + if (!skipSuppressMessage && SuppressionHelpers.CanBeSuppressedWithAttribute(diagnostic)) + { + // global assembly-level suppress message attribute. + nestedActions.Add(new GlobalSuppressMessageCodeAction( + suppressionTargetInfo.TargetSymbol, suppressMessageAttribute, project, diagnostic, this, fallbackOptions)); + + // local suppress message attribute + // please note that in order to avoid issues with existing unit tests referencing the code fix + // by their index this needs to be the last added to nestedActions + if (suppressionTargetInfo.TargetMemberNode != null && suppressionTargetInfo.TargetSymbol.Kind != SymbolKind.Namespace) { - // global assembly-level suppress message attribute. - nestedActions.Add(new GlobalSuppressMessageCodeAction( - suppressionTargetInfo.TargetSymbol, suppressMessageAttribute, project, diagnostic, this, fallbackOptions)); - - // local suppress message attribute - // please note that in order to avoid issues with existing unit tests referencing the code fix - // by their index this needs to be the last added to nestedActions - if (suppressionTargetInfo.TargetMemberNode != null && suppressionTargetInfo.TargetSymbol.Kind != SymbolKind.Namespace) - { - nestedActions.Add(new LocalSuppressMessageCodeAction( - this, suppressionTargetInfo.TargetSymbol, suppressMessageAttribute, suppressionTargetInfo.TargetMemberNode, documentOpt, diagnostic)); - } + nestedActions.Add(new LocalSuppressMessageCodeAction( + this, suppressionTargetInfo.TargetSymbol, suppressMessageAttribute, suppressionTargetInfo.TargetMemberNode, documentOpt, diagnostic)); } + } - if (nestedActions.Count > 0) - { - var codeAction = new TopLevelSuppressionCodeAction( - diagnostic, nestedActions.ToImmutableAndFree()); - result.Add(new CodeFix(project, codeAction, diagnostic)); - } + if (nestedActions.Count > 0) + { + var codeAction = new TopLevelSuppressionCodeAction( + diagnostic, nestedActions.ToImmutableAndFree()); + result.Add(new CodeFix(project, codeAction, diagnostic)); } - else if (!skipUnsuppress) + } + else if (!skipUnsuppress) + { + var codeAction = await RemoveSuppressionCodeAction.CreateAsync(suppressionTargetInfo, documentOpt, project, diagnostic, this, fallbackOptions, cancellationToken).ConfigureAwait(false); + if (codeAction != null) { - var codeAction = await RemoveSuppressionCodeAction.CreateAsync(suppressionTargetInfo, documentOpt, project, diagnostic, this, fallbackOptions, cancellationToken).ConfigureAwait(false); - if (codeAction != null) - { - result.Add(new CodeFix(project, codeAction, diagnostic)); - } + result.Add(new CodeFix(project, codeAction, diagnostic)); } } - - return result.ToImmutableAndFree(); } - internal class SuppressionTargetInfo + return result.ToImmutableAndFree(); + } + + internal class SuppressionTargetInfo + { + public ISymbol TargetSymbol { get; set; } + public SyntaxToken StartToken { get; set; } + public SyntaxToken EndToken { get; set; } + public SyntaxNode NodeWithTokens { get; set; } + public SyntaxNode TargetMemberNode { get; set; } + } + + private async Task GetSuppressionTargetInfoAsync(Document document, TextSpan span, CancellationToken cancellationToken) + { + var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (syntaxTree.GetLineVisibility(span.Start, cancellationToken) == LineVisibility.Hidden) { - public ISymbol TargetSymbol { get; set; } - public SyntaxToken StartToken { get; set; } - public SyntaxToken EndToken { get; set; } - public SyntaxNode NodeWithTokens { get; set; } - public SyntaxNode TargetMemberNode { get; set; } + return null; } - private async Task GetSuppressionTargetInfoAsync(Document document, TextSpan span, CancellationToken cancellationToken) - { - var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (syntaxTree.GetLineVisibility(span.Start, cancellationToken) == LineVisibility.Hidden) - { - return null; - } + // Find the start token to attach leading pragma disable warning directive. + var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var lines = syntaxTree.GetText(cancellationToken).Lines; + var indexOfLine = lines.IndexOf(span.Start); + var lineAtPos = lines[indexOfLine]; + var startToken = root.FindToken(lineAtPos.Start); + startToken = GetAdjustedTokenForPragmaDisable(startToken, root, lines); - // Find the start token to attach leading pragma disable warning directive. - var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var lines = syntaxTree.GetText(cancellationToken).Lines; - var indexOfLine = lines.IndexOf(span.Start); - var lineAtPos = lines[indexOfLine]; - var startToken = root.FindToken(lineAtPos.Start); - startToken = GetAdjustedTokenForPragmaDisable(startToken, root, lines); + // Find the end token to attach pragma restore warning directive. + var spanEnd = Math.Max(startToken.Span.End, span.End); + indexOfLine = lines.IndexOf(spanEnd); + lineAtPos = lines[indexOfLine]; + var endToken = root.FindToken(lineAtPos.End); + endToken = GetAdjustedTokenForPragmaRestore(endToken, root, lines, indexOfLine); - // Find the end token to attach pragma restore warning directive. - var spanEnd = Math.Max(startToken.Span.End, span.End); - indexOfLine = lines.IndexOf(spanEnd); - lineAtPos = lines[indexOfLine]; - var endToken = root.FindToken(lineAtPos.End); - endToken = GetAdjustedTokenForPragmaRestore(endToken, root, lines, indexOfLine); + var nodeWithTokens = GetNodeWithTokens(startToken, endToken, root); - var nodeWithTokens = GetNodeWithTokens(startToken, endToken, root); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetLanguageService(); - var syntaxFacts = document.GetLanguageService(); + ISymbol targetSymbol = null; + var targetMemberNode = syntaxFacts.GetContainingMemberDeclaration(root, nodeWithTokens.SpanStart); + if (targetMemberNode != null) + { + targetSymbol = semanticModel.GetDeclaredSymbol(targetMemberNode, cancellationToken); - ISymbol targetSymbol = null; - var targetMemberNode = syntaxFacts.GetContainingMemberDeclaration(root, nodeWithTokens.SpanStart); - if (targetMemberNode != null) + if (targetSymbol == null) { - targetSymbol = semanticModel.GetDeclaredSymbol(targetMemberNode, cancellationToken); - - if (targetSymbol == null) - { - var analyzerDriverService = document.GetLanguageService(); + var analyzerDriverService = document.GetLanguageService(); - // targetMemberNode could be a declaration node with multiple decls (e.g. field declaration defining multiple variables). - // Let us compute all the declarations intersecting the span. - var declsBuilder = ArrayBuilder.GetInstance(); - analyzerDriverService.ComputeDeclarationsInSpan(semanticModel, span, true, declsBuilder, cancellationToken); - var decls = declsBuilder.ToImmutableAndFree(); + // targetMemberNode could be a declaration node with multiple decls (e.g. field declaration defining multiple variables). + // Let us compute all the declarations intersecting the span. + var declsBuilder = ArrayBuilder.GetInstance(); + analyzerDriverService.ComputeDeclarationsInSpan(semanticModel, span, true, declsBuilder, cancellationToken); + var decls = declsBuilder.ToImmutableAndFree(); - if (!decls.IsEmpty) + if (!decls.IsEmpty) + { + var containedDecls = decls.Where(d => span.Contains(d.DeclaredNode.Span)); + if (containedDecls.Count() == 1) { - var containedDecls = decls.Where(d => span.Contains(d.DeclaredNode.Span)); - if (containedDecls.Count() == 1) - { - // Single containing declaration, use this symbol. - var decl = containedDecls.Single(); - targetSymbol = decl.DeclaredSymbol; - } - else + // Single containing declaration, use this symbol. + var decl = containedDecls.Single(); + targetSymbol = decl.DeclaredSymbol; + } + else + { + // Otherwise, use the most enclosing declaration. + TextSpan? minContainingSpan = null; + foreach (var decl in decls) { - // Otherwise, use the most enclosing declaration. - TextSpan? minContainingSpan = null; - foreach (var decl in decls) + var declSpan = decl.DeclaredNode.Span; + if (declSpan.Contains(span) && + (!minContainingSpan.HasValue || minContainingSpan.Value.Contains(declSpan))) { - var declSpan = decl.DeclaredNode.Span; - if (declSpan.Contains(span) && - (!minContainingSpan.HasValue || minContainingSpan.Value.Contains(declSpan))) - { - minContainingSpan = declSpan; - targetSymbol = decl.DeclaredSymbol; - } + minContainingSpan = declSpan; + targetSymbol = decl.DeclaredSymbol; } } } } } + } - // Outside of a member declaration, suppress diagnostic for the entire assembly. - targetSymbol ??= semanticModel.Compilation.Assembly; + // Outside of a member declaration, suppress diagnostic for the entire assembly. + targetSymbol ??= semanticModel.Compilation.Assembly; - return new SuppressionTargetInfo() { TargetSymbol = targetSymbol, NodeWithTokens = nodeWithTokens, StartToken = startToken, EndToken = endToken, TargetMemberNode = targetMemberNode }; - } + return new SuppressionTargetInfo() { TargetSymbol = targetSymbol, NodeWithTokens = nodeWithTokens, StartToken = startToken, EndToken = endToken, TargetMemberNode = targetMemberNode }; + } - internal SyntaxNode GetNodeWithTokens(SyntaxToken startToken, SyntaxToken endToken, SyntaxNode root) + internal SyntaxNode GetNodeWithTokens(SyntaxToken startToken, SyntaxToken endToken, SyntaxNode root) + { + if (IsEndOfFileToken(endToken)) { - if (IsEndOfFileToken(endToken)) - { - return root; - } - else - { - return startToken.GetCommonRoot(endToken); - } + return root; } + else + { + return startToken.GetCommonRoot(endToken); + } + } - protected static string GetScopeString(SymbolKind targetSymbolKind) + protected static string GetScopeString(SymbolKind targetSymbolKind) + { + switch (targetSymbolKind) { - switch (targetSymbolKind) - { - case SymbolKind.Event: - case SymbolKind.Field: - case SymbolKind.Method: - case SymbolKind.Property: - return "member"; + case SymbolKind.Event: + case SymbolKind.Field: + case SymbolKind.Method: + case SymbolKind.Property: + return "member"; - case SymbolKind.NamedType: - return "type"; + case SymbolKind.NamedType: + return "type"; - case SymbolKind.Namespace: - return "namespace"; + case SymbolKind.Namespace: + return "namespace"; - default: - return null; - } + default: + return null; } - - protected static string GetTargetString(ISymbol targetSymbol) - => "~" + DocumentationCommentId.CreateDeclarationId(targetSymbol); } + + protected static string GetTargetString(ISymbol targetSymbol) + => "~" + DocumentationCommentId.CreateDeclarationId(targetSymbol); } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/ExportConfigurationFixProviderAttribute.cs b/src/Features/Core/Portable/CodeFixes/Suppression/ExportConfigurationFixProviderAttribute.cs index dd679f8152702..f48ab5e57cf7d 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/ExportConfigurationFixProviderAttribute.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/ExportConfigurationFixProviderAttribute.cs @@ -7,42 +7,41 @@ using System; using System.Composition; -namespace Microsoft.CodeAnalysis.CodeFixes +namespace Microsoft.CodeAnalysis.CodeFixes; + +/// +/// Use this attribute to declare a implementation so that it can be discovered by the host. +/// +[MetadataAttribute] +[AttributeUsage(AttributeTargets.Class)] +internal class ExportConfigurationFixProviderAttribute : ExportAttribute { /// - /// Use this attribute to declare a implementation so that it can be discovered by the host. + /// The name of the . /// - [MetadataAttribute] - [AttributeUsage(AttributeTargets.Class)] - internal class ExportConfigurationFixProviderAttribute : ExportAttribute + public string Name { get; } + + /// + /// The source languages this provider can provide fixes for. See . + /// + public string[] Languages { get; } + + public ExportConfigurationFixProviderAttribute( + string name, + params string[] languages) + : base(typeof(IConfigurationFixProvider)) { - /// - /// The name of the . - /// - public string Name { get; } - - /// - /// The source languages this provider can provide fixes for. See . - /// - public string[] Languages { get; } - - public ExportConfigurationFixProviderAttribute( - string name, - params string[] languages) - : base(typeof(IConfigurationFixProvider)) + if (languages == null) { - if (languages == null) - { - throw new ArgumentNullException(nameof(languages)); - } - - if (languages.Length == 0) - { - throw new ArgumentException(nameof(languages)); - } - - Languages = languages; - Name = name ?? throw new ArgumentNullException(nameof(name)); + throw new ArgumentNullException(nameof(languages)); } + + if (languages.Length == 0) + { + throw new ArgumentException(nameof(languages)); + } + + Languages = languages; + Name = name ?? throw new ArgumentNullException(nameof(name)); } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/NestedSuppressionCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Suppression/NestedSuppressionCodeAction.cs index cfda1f27c2672..c980a965211d0 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/NestedSuppressionCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/NestedSuppressionCodeAction.cs @@ -6,30 +6,29 @@ using Microsoft.CodeAnalysis.CodeActions; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal abstract class NestedSuppressionCodeAction : CodeAction { - internal abstract class NestedSuppressionCodeAction : CodeAction - { - protected NestedSuppressionCodeAction(string title) - => Title = title; - - public sealed override string Title { get; } - - protected abstract string DiagnosticIdForEquivalenceKey { get; } - - public override string EquivalenceKey => Title + DiagnosticIdForEquivalenceKey; - - // Put suppressions at the end of everything. - protected sealed override CodeActionPriority ComputePriority() - => CodeActionPriority.Lowest; - - public static bool IsEquivalenceKeyForGlobalSuppression(string equivalenceKey) - => equivalenceKey.StartsWith(FeaturesResources.in_Suppression_File); - public static bool IsEquivalenceKeyForPragmaWarning(string equivalenceKey) - => equivalenceKey.StartsWith(FeaturesResources.in_Source); - public static bool IsEquivalenceKeyForRemoveSuppression(string equivalenceKey) - => equivalenceKey.StartsWith(FeaturesResources.Remove_Suppression); - public static bool IsEquivalenceKeyForLocalSuppression(string equivalenceKey) - => equivalenceKey.StartsWith(FeaturesResources.in_Source_attribute); - } + protected NestedSuppressionCodeAction(string title) + => Title = title; + + public sealed override string Title { get; } + + protected abstract string DiagnosticIdForEquivalenceKey { get; } + + public override string EquivalenceKey => Title + DiagnosticIdForEquivalenceKey; + + // Put suppressions at the end of everything. + protected sealed override CodeActionPriority ComputePriority() + => CodeActionPriority.Lowest; + + public static bool IsEquivalenceKeyForGlobalSuppression(string equivalenceKey) + => equivalenceKey.StartsWith(FeaturesResources.in_Suppression_File); + public static bool IsEquivalenceKeyForPragmaWarning(string equivalenceKey) + => equivalenceKey.StartsWith(FeaturesResources.in_Source); + public static bool IsEquivalenceKeyForRemoveSuppression(string equivalenceKey) + => equivalenceKey.StartsWith(FeaturesResources.Remove_Suppression); + public static bool IsEquivalenceKeyForLocalSuppression(string equivalenceKey) + => equivalenceKey.StartsWith(FeaturesResources.in_Source_attribute); } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/SuppressionHelpers.cs b/src/Features/Core/Portable/CodeFixes/Suppression/SuppressionHelpers.cs index 860dcce47c70b..622f48f090beb 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/SuppressionHelpers.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/SuppressionHelpers.cs @@ -12,79 +12,78 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal static class SuppressionHelpers { - internal static class SuppressionHelpers - { - private const string SynthesizedExternalSourceDiagnosticTag = "SynthesizedExternalSourceDiagnostic"; - public static readonly ImmutableArray SynthesizedExternalSourceDiagnosticCustomTags = [SynthesizedExternalSourceDiagnosticTag]; + private const string SynthesizedExternalSourceDiagnosticTag = "SynthesizedExternalSourceDiagnostic"; + public static readonly ImmutableArray SynthesizedExternalSourceDiagnosticCustomTags = [SynthesizedExternalSourceDiagnosticTag]; - public static bool CanBeSuppressed(Diagnostic diagnostic) - => CanBeSuppressedOrUnsuppressed(diagnostic, checkCanBeSuppressed: true); + public static bool CanBeSuppressed(Diagnostic diagnostic) + => CanBeSuppressedOrUnsuppressed(diagnostic, checkCanBeSuppressed: true); - public static bool CanBeSuppressedWithAttribute(Diagnostic diagnostic) + public static bool CanBeSuppressedWithAttribute(Diagnostic diagnostic) + { + if (IsCompilerDiagnostic(diagnostic)) { - if (IsCompilerDiagnostic(diagnostic)) - { - return false; - } - - // IDE0055 cannot be suppressed with an attribute because the formatter implementation only adheres to - // pragma-based source suppressions. - return diagnostic.Id != IDEDiagnosticIds.FormattingDiagnosticId; + return false; } - public static bool CanBeUnsuppressed(Diagnostic diagnostic) - => CanBeSuppressedOrUnsuppressed(diagnostic, checkCanBeSuppressed: false); + // IDE0055 cannot be suppressed with an attribute because the formatter implementation only adheres to + // pragma-based source suppressions. + return diagnostic.Id != IDEDiagnosticIds.FormattingDiagnosticId; + } + + public static bool CanBeUnsuppressed(Diagnostic diagnostic) + => CanBeSuppressedOrUnsuppressed(diagnostic, checkCanBeSuppressed: false); - private static bool CanBeSuppressedOrUnsuppressed(Diagnostic diagnostic, bool checkCanBeSuppressed) + private static bool CanBeSuppressedOrUnsuppressed(Diagnostic diagnostic, bool checkCanBeSuppressed) + { + if (diagnostic.IsSuppressed == checkCanBeSuppressed || + IsNotConfigurableDiagnostic(diagnostic)) { - if (diagnostic.IsSuppressed == checkCanBeSuppressed || - IsNotConfigurableDiagnostic(diagnostic)) - { - // Don't offer suppression fixes for: - // 1. Diagnostics with a source suppression. - // 2. Non-configurable diagnostics (includes compiler errors). + // Don't offer suppression fixes for: + // 1. Diagnostics with a source suppression. + // 2. Non-configurable diagnostics (includes compiler errors). + return false; + } + + switch (diagnostic.Severity) + { + case DiagnosticSeverity.Hidden: + // Hidden diagnostics should never show up. return false; - } - - switch (diagnostic.Severity) - { - case DiagnosticSeverity.Hidden: - // Hidden diagnostics should never show up. - return false; - - case DiagnosticSeverity.Error: - case DiagnosticSeverity.Warning: - case DiagnosticSeverity.Info: - // We allow suppressions for all the remaining configurable, non-hidden diagnostics. - // Note that compiler errors are not configurable by default, so only analyzer errors are suppressable. - return true; - - default: - throw ExceptionUtilities.UnexpectedValue(diagnostic.Severity); - } + + case DiagnosticSeverity.Error: + case DiagnosticSeverity.Warning: + case DiagnosticSeverity.Info: + // We allow suppressions for all the remaining configurable, non-hidden diagnostics. + // Note that compiler errors are not configurable by default, so only analyzer errors are suppressable. + return true; + + default: + throw ExceptionUtilities.UnexpectedValue(diagnostic.Severity); } + } - public static bool IsNotConfigurableDiagnostic(DiagnosticData diagnostic) - => HasCustomTag(diagnostic.CustomTags, WellKnownDiagnosticTags.NotConfigurable); + public static bool IsNotConfigurableDiagnostic(DiagnosticData diagnostic) + => HasCustomTag(diagnostic.CustomTags, WellKnownDiagnosticTags.NotConfigurable); - public static bool IsNotConfigurableDiagnostic(Diagnostic diagnostic) - => HasCustomTag(diagnostic.Descriptor.ImmutableCustomTags(), WellKnownDiagnosticTags.NotConfigurable); + public static bool IsNotConfigurableDiagnostic(Diagnostic diagnostic) + => HasCustomTag(diagnostic.Descriptor.ImmutableCustomTags(), WellKnownDiagnosticTags.NotConfigurable); - public static bool IsCompilerDiagnostic(DiagnosticData diagnostic) - => HasCustomTag(diagnostic.CustomTags, WellKnownDiagnosticTags.Compiler); + public static bool IsCompilerDiagnostic(DiagnosticData diagnostic) + => HasCustomTag(diagnostic.CustomTags, WellKnownDiagnosticTags.Compiler); - public static bool IsCompilerDiagnostic(Diagnostic diagnostic) - => HasCustomTag(diagnostic.Descriptor.ImmutableCustomTags(), WellKnownDiagnosticTags.Compiler); + public static bool IsCompilerDiagnostic(Diagnostic diagnostic) + => HasCustomTag(diagnostic.Descriptor.ImmutableCustomTags(), WellKnownDiagnosticTags.Compiler); - public static bool IsSynthesizedExternalSourceDiagnostic(DiagnosticData diagnostic) - => HasCustomTag(diagnostic.CustomTags, SynthesizedExternalSourceDiagnosticTag); + public static bool IsSynthesizedExternalSourceDiagnostic(DiagnosticData diagnostic) + => HasCustomTag(diagnostic.CustomTags, SynthesizedExternalSourceDiagnosticTag); - public static bool IsSynthesizedExternalSourceDiagnostic(Diagnostic diagnostic) - => HasCustomTag(diagnostic.Descriptor.ImmutableCustomTags(), SynthesizedExternalSourceDiagnosticTag); + public static bool IsSynthesizedExternalSourceDiagnostic(Diagnostic diagnostic) + => HasCustomTag(diagnostic.Descriptor.ImmutableCustomTags(), SynthesizedExternalSourceDiagnosticTag); - public static bool HasCustomTag(ImmutableArray customTags, string tagToFind) - => customTags.Any(static (c, tagToFind) => CultureInfo.InvariantCulture.CompareInfo.Compare(c, tagToFind) == 0, tagToFind); - } + public static bool HasCustomTag(ImmutableArray customTags, string tagToFind) + => customTags.Any(static (c, tagToFind) => CultureInfo.InvariantCulture.CompareInfo.Compare(c, tagToFind) == 0, tagToFind); } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/TopLevelSuppressionCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Suppression/TopLevelSuppressionCodeAction.cs index 0925ad3d7f30f..248788d106b3c 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/TopLevelSuppressionCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/TopLevelSuppressionCodeAction.cs @@ -7,9 +7,8 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.CodeActions; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal sealed class TopLevelSuppressionCodeAction(Diagnostic diagnostic, ImmutableArray nestedActions) : AbstractConfigurationActionWithNestedActions(ImmutableArray.CastUp(nestedActions), string.Format(FeaturesResources.Suppress_0, diagnostic.Id)) { - internal sealed class TopLevelSuppressionCodeAction(Diagnostic diagnostic, ImmutableArray nestedActions) : AbstractConfigurationActionWithNestedActions(ImmutableArray.CastUp(nestedActions), string.Format(FeaturesResources.Suppress_0, diagnostic.Id)) - { - } } diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/WrapperCodeFixProvider.cs b/src/Features/Core/Portable/CodeFixes/Suppression/WrapperCodeFixProvider.cs index 9d98c6d0cc4ac..3c3884d3c2899 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/WrapperCodeFixProvider.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/WrapperCodeFixProvider.cs @@ -9,47 +9,46 @@ using System.Linq; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.CodeFixes.Suppression +namespace Microsoft.CodeAnalysis.CodeFixes.Suppression; + +internal sealed class WrapperCodeFixProvider(IConfigurationFixProvider suppressionFixProvider, IEnumerable diagnosticIds) : CodeFixProvider { - internal sealed class WrapperCodeFixProvider(IConfigurationFixProvider suppressionFixProvider, IEnumerable diagnosticIds) : CodeFixProvider - { - private readonly ImmutableArray _originalDiagnosticIds = diagnosticIds.Distinct().ToImmutableArray(); - private readonly IConfigurationFixProvider _suppressionFixProvider = suppressionFixProvider; + private readonly ImmutableArray _originalDiagnosticIds = diagnosticIds.Distinct().ToImmutableArray(); + private readonly IConfigurationFixProvider _suppressionFixProvider = suppressionFixProvider; - public IConfigurationFixProvider SuppressionFixProvider => _suppressionFixProvider; - public override ImmutableArray FixableDiagnosticIds => _originalDiagnosticIds; + public IConfigurationFixProvider SuppressionFixProvider => _suppressionFixProvider; + public override ImmutableArray FixableDiagnosticIds => _originalDiagnosticIds; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var diagnostics = context.Diagnostics.Where(_suppressionFixProvider.IsFixableDiagnostic); + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostics = context.Diagnostics.Where(_suppressionFixProvider.IsFixableDiagnostic); - var documentDiagnostics = diagnostics.Where(d => d.Location.IsInSource).ToImmutableArray(); - if (!documentDiagnostics.IsEmpty) - { - var suppressionFixes = await _suppressionFixProvider.GetFixesAsync(context.Document, context.Span, documentDiagnostics, context.Options, context.CancellationToken).ConfigureAwait(false); - RegisterSuppressionFixes(context, suppressionFixes); - } + var documentDiagnostics = diagnostics.Where(d => d.Location.IsInSource).ToImmutableArray(); + if (!documentDiagnostics.IsEmpty) + { + var suppressionFixes = await _suppressionFixProvider.GetFixesAsync(context.Document, context.Span, documentDiagnostics, context.Options, context.CancellationToken).ConfigureAwait(false); + RegisterSuppressionFixes(context, suppressionFixes); + } - var projectDiagnostics = diagnostics.Where(d => !d.Location.IsInSource).ToImmutableArray(); - if (!projectDiagnostics.IsEmpty) - { - var suppressionFixes = await _suppressionFixProvider.GetFixesAsync(context.Document.Project, projectDiagnostics, context.Options, context.CancellationToken).ConfigureAwait(false); - RegisterSuppressionFixes(context, suppressionFixes); - } + var projectDiagnostics = diagnostics.Where(d => !d.Location.IsInSource).ToImmutableArray(); + if (!projectDiagnostics.IsEmpty) + { + var suppressionFixes = await _suppressionFixProvider.GetFixesAsync(context.Document.Project, projectDiagnostics, context.Options, context.CancellationToken).ConfigureAwait(false); + RegisterSuppressionFixes(context, suppressionFixes); } + } - private static void RegisterSuppressionFixes(CodeFixContext context, ImmutableArray suppressionFixes) + private static void RegisterSuppressionFixes(CodeFixContext context, ImmutableArray suppressionFixes) + { + if (!suppressionFixes.IsDefault) { - if (!suppressionFixes.IsDefault) + foreach (var suppressionFix in suppressionFixes) { - foreach (var suppressionFix in suppressionFixes) - { - context.RegisterCodeFix(suppressionFix.Action, suppressionFix.Diagnostics); - } + context.RegisterCodeFix(suppressionFix.Action, suppressionFix.Diagnostics); } } - - public override FixAllProvider GetFixAllProvider() - => _suppressionFixProvider.GetFixAllProvider(); } + + public override FixAllProvider GetFixAllProvider() + => _suppressionFixProvider.GetFixAllProvider(); } diff --git a/src/Features/Core/Portable/CodeFixesAndRefactorings/AbstractFixAllCodeAction.cs b/src/Features/Core/Portable/CodeFixesAndRefactorings/AbstractFixAllCodeAction.cs index 6444d7e7cbbe9..bb5a8fa6592f4 100644 --- a/src/Features/Core/Portable/CodeFixesAndRefactorings/AbstractFixAllCodeAction.cs +++ b/src/Features/Core/Portable/CodeFixesAndRefactorings/AbstractFixAllCodeAction.cs @@ -12,93 +12,92 @@ using Roslyn.Utilities; using FixAllScope = Microsoft.CodeAnalysis.CodeFixes.FixAllScope; -namespace Microsoft.CodeAnalysis.CodeFixesAndRefactorings +namespace Microsoft.CodeAnalysis.CodeFixesAndRefactorings; + +/// +/// Fix all code action for a code action registered by +/// a or a . +/// +internal abstract class AbstractFixAllCodeAction : CodeAction { - /// - /// Fix all code action for a code action registered by - /// a or a . - /// - internal abstract class AbstractFixAllCodeAction : CodeAction - { - private bool _showPreviewChangesDialog; + private bool _showPreviewChangesDialog; - public IFixAllState FixAllState { get; } + public IFixAllState FixAllState { get; } - protected AbstractFixAllCodeAction( - IFixAllState fixAllState, bool showPreviewChangesDialog) - { - FixAllState = fixAllState; - _showPreviewChangesDialog = showPreviewChangesDialog; - } + protected AbstractFixAllCodeAction( + IFixAllState fixAllState, bool showPreviewChangesDialog) + { + FixAllState = fixAllState; + _showPreviewChangesDialog = showPreviewChangesDialog; + } - /// - /// Determine if the is an internal first-party provider or not. - /// - protected abstract bool IsInternalProvider(IFixAllState fixAllState); + /// + /// Determine if the is an internal first-party provider or not. + /// + protected abstract bool IsInternalProvider(IFixAllState fixAllState); - /// - /// Creates a new with the given parameters. - /// - protected abstract IFixAllContext CreateFixAllContext(IFixAllState fixAllState, IProgress progressTracker, CancellationToken cancellationToken); - - public override string Title - => this.FixAllState.Scope switch - { - FixAllScope.Document => FeaturesResources.Document, - FixAllScope.Project => FeaturesResources.Project, - FixAllScope.Solution => FeaturesResources.Solution, - FixAllScope.ContainingMember => FeaturesResources.Containing_Member, - FixAllScope.ContainingType => FeaturesResources.Containing_Type, - _ => throw ExceptionUtilities.UnexpectedValue(this.FixAllState.Scope), - }; - - internal override string Message => FeaturesResources.Computing_fix_all_occurrences_code_fix; - - protected sealed override Task> ComputeOperationsAsync( - IProgress progressTracker, CancellationToken cancellationToken) + /// + /// Creates a new with the given parameters. + /// + protected abstract IFixAllContext CreateFixAllContext(IFixAllState fixAllState, IProgress progressTracker, CancellationToken cancellationToken); + + public override string Title + => this.FixAllState.Scope switch { - cancellationToken.ThrowIfCancellationRequested(); - FixAllLogger.LogState(FixAllState, IsInternalProvider(FixAllState)); + FixAllScope.Document => FeaturesResources.Document, + FixAllScope.Project => FeaturesResources.Project, + FixAllScope.Solution => FeaturesResources.Solution, + FixAllScope.ContainingMember => FeaturesResources.Containing_Member, + FixAllScope.ContainingType => FeaturesResources.Containing_Type, + _ => throw ExceptionUtilities.UnexpectedValue(this.FixAllState.Scope), + }; + + internal override string Message => FeaturesResources.Computing_fix_all_occurrences_code_fix; + + protected sealed override Task> ComputeOperationsAsync( + IProgress progressTracker, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + FixAllLogger.LogState(FixAllState, IsInternalProvider(FixAllState)); - var service = FixAllState.Project.Solution.Services.GetRequiredService(); + var service = FixAllState.Project.Solution.Services.GetRequiredService(); - var fixAllContext = CreateFixAllContext(FixAllState, progressTracker, cancellationToken); - progressTracker.Report(CodeAnalysisProgress.Description(fixAllContext.GetDefaultFixAllTitle())); + var fixAllContext = CreateFixAllContext(FixAllState, progressTracker, cancellationToken); + progressTracker.Report(CodeAnalysisProgress.Description(fixAllContext.GetDefaultFixAllTitle())); - return service.GetFixAllOperationsAsync(fixAllContext, _showPreviewChangesDialog); - } + return service.GetFixAllOperationsAsync(fixAllContext, _showPreviewChangesDialog); + } - protected sealed override Task GetChangedSolutionAsync( - IProgress progressTracker, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - FixAllLogger.LogState(FixAllState, IsInternalProvider(FixAllState)); + protected sealed override Task GetChangedSolutionAsync( + IProgress progressTracker, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + FixAllLogger.LogState(FixAllState, IsInternalProvider(FixAllState)); - var service = FixAllState.Project.Solution.Services.GetRequiredService(); + var service = FixAllState.Project.Solution.Services.GetRequiredService(); - var fixAllContext = CreateFixAllContext(FixAllState, progressTracker, cancellationToken); - progressTracker.Report(CodeAnalysisProgress.Description(fixAllContext.GetDefaultFixAllTitle())); + var fixAllContext = CreateFixAllContext(FixAllState, progressTracker, cancellationToken); + progressTracker.Report(CodeAnalysisProgress.Description(fixAllContext.GetDefaultFixAllTitle())); - return service.GetFixAllChangedSolutionAsync(fixAllContext); - } + return service.GetFixAllChangedSolutionAsync(fixAllContext); + } - // internal for testing purposes. - internal TestAccessor GetTestAccessor() - => new(this); + // internal for testing purposes. + internal TestAccessor GetTestAccessor() + => new(this); - // internal for testing purposes. - internal readonly struct TestAccessor - { - private readonly AbstractFixAllCodeAction _fixAllCodeAction; + // internal for testing purposes. + internal readonly struct TestAccessor + { + private readonly AbstractFixAllCodeAction _fixAllCodeAction; - internal TestAccessor(AbstractFixAllCodeAction fixAllCodeAction) - => _fixAllCodeAction = fixAllCodeAction; + internal TestAccessor(AbstractFixAllCodeAction fixAllCodeAction) + => _fixAllCodeAction = fixAllCodeAction; - /// - /// Gets a reference to , which can be read or written by test code. - /// - public ref bool ShowPreviewChangesDialog - => ref _fixAllCodeAction._showPreviewChangesDialog; - } + /// + /// Gets a reference to , which can be read or written by test code. + /// + public ref bool ShowPreviewChangesDialog + => ref _fixAllCodeAction._showPreviewChangesDialog; } } diff --git a/src/Features/Core/Portable/CodeFixesAndRefactorings/CodeActionRequestPriorityProvider.cs b/src/Features/Core/Portable/CodeFixesAndRefactorings/CodeActionRequestPriorityProvider.cs index b0f7908dd1b91..78d0210124f1b 100644 --- a/src/Features/Core/Portable/CodeFixesAndRefactorings/CodeActionRequestPriorityProvider.cs +++ b/src/Features/Core/Portable/CodeFixesAndRefactorings/CodeActionRequestPriorityProvider.cs @@ -11,118 +11,117 @@ using Microsoft.CodeAnalysis.Diagnostics; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeActions +namespace Microsoft.CodeAnalysis.CodeActions; + +internal interface ICodeActionRequestPriorityProvider +{ + /// + /// represents no specified priority. i.e. any priority should match this. + /// + CodeActionRequestPriority? Priority { get; } + + /// + /// Tracks the given as a de-prioritized analyzer that should be moved to + /// bucket. + /// + void AddDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer); + + bool IsDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer); +} + +internal static class ICodeActionRequestPriorityProviderExtensions { - internal interface ICodeActionRequestPriorityProvider + /// + /// Returns true if the given can report diagnostics that can have fixes from a code + /// fix provider with matching . This method is useful for performing a performance + /// optimization for lightbulb diagnostic computation, wherein we can reduce the set of analyzers to be executed + /// when computing fixes for a specific . + /// + public static bool MatchesPriority(this ICodeActionRequestPriorityProvider provider, DiagnosticAnalyzer analyzer) { - /// - /// represents no specified priority. i.e. any priority should match this. - /// - CodeActionRequestPriority? Priority { get; } - - /// - /// Tracks the given as a de-prioritized analyzer that should be moved to - /// bucket. - /// - void AddDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer); - - bool IsDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer); + var priority = provider.Priority; + + // If caller isn't asking for prioritized result, then run all analyzers. + if (priority is null) + return true; + + // 'CodeActionRequestPriority.Lowest' is used for suppression/configuration fixes, + // which requires all analyzer diagnostics. + if (priority == CodeActionRequestPriority.Lowest) + return true; + + // The compiler analyzer always counts for any priority. It's diagnostics may be fixed + // by high pri or normal pri fixers. + if (analyzer.IsCompilerAnalyzer()) + return true; + + // Check if we are computing diagnostics for 'CodeActionRequestPriority.Low' and + // this analyzer was de-prioritized to low priority bucket. + if (priority == CodeActionRequestPriority.Low && + provider.IsDeprioritizedAnalyzerWithLowPriority(analyzer)) + { + return true; + } + + // Now compute this analyzer's priority and compare it with the provider's request 'Priority'. + // Our internal 'IBuiltInAnalyzer' can specify custom request priority, while all + // the third-party analyzers are assigned 'Medium' priority. + var analyzerPriority = analyzer is IBuiltInAnalyzer { IsHighPriority: true } + ? CodeActionRequestPriority.High + : CodeActionRequestPriority.Default; + + return priority == analyzerPriority; } - internal static class ICodeActionRequestPriorityProviderExtensions + /// + /// Returns true if the given should be considered a candidate when computing + /// fixes for the given . + /// + public static bool MatchesPriority(this ICodeActionRequestPriorityProvider provider, CodeFixProvider codeFixProvider) { - /// - /// Returns true if the given can report diagnostics that can have fixes from a code - /// fix provider with matching . This method is useful for performing a performance - /// optimization for lightbulb diagnostic computation, wherein we can reduce the set of analyzers to be executed - /// when computing fixes for a specific . - /// - public static bool MatchesPriority(this ICodeActionRequestPriorityProvider provider, DiagnosticAnalyzer analyzer) + if (provider.Priority == null) { - var priority = provider.Priority; - - // If caller isn't asking for prioritized result, then run all analyzers. - if (priority is null) - return true; - - // 'CodeActionRequestPriority.Lowest' is used for suppression/configuration fixes, - // which requires all analyzer diagnostics. - if (priority == CodeActionRequestPriority.Lowest) - return true; - - // The compiler analyzer always counts for any priority. It's diagnostics may be fixed - // by high pri or normal pri fixers. - if (analyzer.IsCompilerAnalyzer()) - return true; - - // Check if we are computing diagnostics for 'CodeActionRequestPriority.Low' and - // this analyzer was de-prioritized to low priority bucket. - if (priority == CodeActionRequestPriority.Low && - provider.IsDeprioritizedAnalyzerWithLowPriority(analyzer)) - { - return true; - } - - // Now compute this analyzer's priority and compare it with the provider's request 'Priority'. - // Our internal 'IBuiltInAnalyzer' can specify custom request priority, while all - // the third-party analyzers are assigned 'Medium' priority. - var analyzerPriority = analyzer is IBuiltInAnalyzer { IsHighPriority: true } - ? CodeActionRequestPriority.High - : CodeActionRequestPriority.Default; - - return priority == analyzerPriority; + // We are computing fixes for all priorities + return true; } - /// - /// Returns true if the given should be considered a candidate when computing - /// fixes for the given . - /// - public static bool MatchesPriority(this ICodeActionRequestPriorityProvider provider, CodeFixProvider codeFixProvider) + if (provider.Priority == CodeActionRequestPriority.Low) { - if (provider.Priority == null) - { - // We are computing fixes for all priorities - return true; - } - - if (provider.Priority == CodeActionRequestPriority.Low) - { - // 'Low' priority can be used for two types of code fixers: - // 1. Those which explicitly set their 'RequestPriority' to 'Low' and - // 2. Those which can fix diagnostics for expensive analyzers which were de-prioritized - // to 'Low' priority bucket to improve lightbulb population performance. - // Hence, when processing the 'Low' Priority bucket, we accept fixers with any RequestPriority, - // as long as they can fix a diagnostic from an analyzer that was executed in the 'Low' bucket. - return true; - } - - return provider.Priority == codeFixProvider.RequestPriority; + // 'Low' priority can be used for two types of code fixers: + // 1. Those which explicitly set their 'RequestPriority' to 'Low' and + // 2. Those which can fix diagnostics for expensive analyzers which were de-prioritized + // to 'Low' priority bucket to improve lightbulb population performance. + // Hence, when processing the 'Low' Priority bucket, we accept fixers with any RequestPriority, + // as long as they can fix a diagnostic from an analyzer that was executed in the 'Low' bucket. + return true; } + + return provider.Priority == codeFixProvider.RequestPriority; } +} - internal sealed class DefaultCodeActionRequestPriorityProvider(CodeActionRequestPriority? priority = null) : ICodeActionRequestPriorityProvider - { - private readonly object _gate = new(); - private HashSet? _lowPriorityAnalyzers; +internal sealed class DefaultCodeActionRequestPriorityProvider(CodeActionRequestPriority? priority = null) : ICodeActionRequestPriorityProvider +{ + private readonly object _gate = new(); + private HashSet? _lowPriorityAnalyzers; - public CodeActionRequestPriority? Priority { get; } = priority; + public CodeActionRequestPriority? Priority { get; } = priority; - public void AddDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer) + public void AddDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer) + { + lock (_gate) { - lock (_gate) - { - _lowPriorityAnalyzers ??= []; - _lowPriorityAnalyzers.Add(analyzer); - } + _lowPriorityAnalyzers ??= []; + _lowPriorityAnalyzers.Add(analyzer); } + } - public bool IsDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer) + public bool IsDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer) + { + lock (_gate) { - lock (_gate) - { - return _lowPriorityAnalyzers != null && _lowPriorityAnalyzers.Contains(analyzer); - } + return _lowPriorityAnalyzers != null && _lowPriorityAnalyzers.Contains(analyzer); } } } diff --git a/src/Features/Core/Portable/CodeFixesAndRefactorings/IFixAllGetFixesService.cs b/src/Features/Core/Portable/CodeFixesAndRefactorings/IFixAllGetFixesService.cs index 7625188e383cf..fd3a554bfa89b 100644 --- a/src/Features/Core/Portable/CodeFixesAndRefactorings/IFixAllGetFixesService.cs +++ b/src/Features/Core/Portable/CodeFixesAndRefactorings/IFixAllGetFixesService.cs @@ -9,33 +9,32 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.CodeFixesAndRefactorings +namespace Microsoft.CodeAnalysis.CodeFixesAndRefactorings; + +internal interface IFixAllGetFixesService : IWorkspaceService { - internal interface IFixAllGetFixesService : IWorkspaceService - { - /// - /// Computes the fix all occurrences code fix, brings up the preview changes dialog for the fix and - /// returns the code action operations corresponding to the fix. - /// - Task> GetFixAllOperationsAsync(IFixAllContext fixAllContext, bool showPreviewChangesDialog); + /// + /// Computes the fix all occurrences code fix, brings up the preview changes dialog for the fix and + /// returns the code action operations corresponding to the fix. + /// + Task> GetFixAllOperationsAsync(IFixAllContext fixAllContext, bool showPreviewChangesDialog); - /// - /// Computes the fix all occurrences code fix and returns the changed solution. - /// - Task GetFixAllChangedSolutionAsync(IFixAllContext fixAllContext); + /// + /// Computes the fix all occurrences code fix and returns the changed solution. + /// + Task GetFixAllChangedSolutionAsync(IFixAllContext fixAllContext); - /// - /// Previews the changes that would occur after a code fix and returns the updated solution with those changes. - /// - Solution? PreviewChanges( - Workspace workspace, - Solution currentSolution, - Solution newSolution, - FixAllKind fixAllKind, - string previewChangesTitle, - string topLevelHeader, - string? language, - int? correlationId, - CancellationToken cancellationToken); - } + /// + /// Previews the changes that would occur after a code fix and returns the updated solution with those changes. + /// + Solution? PreviewChanges( + Workspace workspace, + Solution currentSolution, + Solution newSolution, + FixAllKind fixAllKind, + string previewChangesTitle, + string topLevelHeader, + string? language, + int? correlationId, + CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/CodeLens/CodeLensFindReferenceProgress.cs b/src/Features/Core/Portable/CodeLens/CodeLensFindReferenceProgress.cs index 976b4482ce0b7..076e4ddaabbdb 100644 --- a/src/Features/Core/Portable/CodeLens/CodeLensFindReferenceProgress.cs +++ b/src/Features/Core/Portable/CodeLens/CodeLensFindReferenceProgress.cs @@ -13,140 +13,139 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeLens +namespace Microsoft.CodeAnalysis.CodeLens; + +/// +/// Tracks incremental progress of a find references search, we use this to +/// count the number of references up until a certain cap is reached and cancel the search +/// or until the search completes, if such a cap is not reached. +/// +/// +/// All public methods of this type could be called from multiple threads. +/// +internal sealed class CodeLensFindReferencesProgress( + ISymbol queriedDefinition, + SyntaxNode queriedNode, + int searchCap, + CancellationToken cancellationToken) : IFindReferencesProgress, IDisposable { - /// - /// Tracks incremental progress of a find references search, we use this to - /// count the number of references up until a certain cap is reached and cancel the search - /// or until the search completes, if such a cap is not reached. - /// + private readonly CancellationTokenSource _aggregateCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + private readonly SyntaxNode _queriedNode = queriedNode; + private readonly ISymbol _queriedSymbol = queriedDefinition; + private readonly ConcurrentSet _locations = new ConcurrentSet(LocationComparer.Instance); + /// - /// All public methods of this type could be called from multiple threads. + /// If the cap is 0, then there is no cap. /// - internal sealed class CodeLensFindReferencesProgress( - ISymbol queriedDefinition, - SyntaxNode queriedNode, - int searchCap, - CancellationToken cancellationToken) : IFindReferencesProgress, IDisposable - { - private readonly CancellationTokenSource _aggregateCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - private readonly SyntaxNode _queriedNode = queriedNode; - private readonly ISymbol _queriedSymbol = queriedDefinition; - private readonly ConcurrentSet _locations = new ConcurrentSet(LocationComparer.Instance); + public int SearchCap { get; } = searchCap; - /// - /// If the cap is 0, then there is no cap. - /// - public int SearchCap { get; } = searchCap; + /// + /// The cancellation token that aggregates the original cancellation token + this progress + /// + public CancellationToken CancellationToken => _aggregateCancellationTokenSource.Token; - /// - /// The cancellation token that aggregates the original cancellation token + this progress - /// - public CancellationToken CancellationToken => _aggregateCancellationTokenSource.Token; + public bool SearchCapReached => SearchCap != 0 && ReferencesCount > SearchCap; - public bool SearchCapReached => SearchCap != 0 && ReferencesCount > SearchCap; + public int ReferencesCount => _locations.Count; - public int ReferencesCount => _locations.Count; + public ImmutableArray Locations => _locations.ToImmutableArray(); - public ImmutableArray Locations => _locations.ToImmutableArray(); + public void OnStarted() + { + } - public void OnStarted() - { - } + public void OnCompleted() + { + } - public void OnCompleted() - { - } + public void OnFindInDocumentStarted(Document document) + { + } - public void OnFindInDocumentStarted(Document document) - { - } + public void OnFindInDocumentCompleted(Document document) + { + } - public void OnFindInDocumentCompleted(Document document) - { - } + private static bool FilterDefinition(ISymbol definition) + { + return definition.IsImplicitlyDeclared || + (definition as IMethodSymbol)?.AssociatedSymbol != null; + } - private static bool FilterDefinition(ISymbol definition) - { - return definition.IsImplicitlyDeclared || - (definition as IMethodSymbol)?.AssociatedSymbol != null; - } + // Returns partial symbol locations whose node does not match the queried syntaxNode + private IEnumerable GetPartialLocations(ISymbol symbol, CancellationToken cancellationToken) + { + // Returns nodes from source not equal to actual location + return from syntaxReference in symbol.DeclaringSyntaxReferences + let candidateSyntaxNode = syntaxReference.GetSyntax(cancellationToken) + where !(_queriedNode.Span == candidateSyntaxNode.Span && + _queriedNode.SyntaxTree.FilePath.Equals(candidateSyntaxNode.SyntaxTree.FilePath, + StringComparison.OrdinalIgnoreCase)) + select candidateSyntaxNode.GetLocation(); + } - // Returns partial symbol locations whose node does not match the queried syntaxNode - private IEnumerable GetPartialLocations(ISymbol symbol, CancellationToken cancellationToken) + public void OnDefinitionFound(ISymbol symbol) + { + if (FilterDefinition(symbol)) { - // Returns nodes from source not equal to actual location - return from syntaxReference in symbol.DeclaringSyntaxReferences - let candidateSyntaxNode = syntaxReference.GetSyntax(cancellationToken) - where !(_queriedNode.Span == candidateSyntaxNode.Span && - _queriedNode.SyntaxTree.FilePath.Equals(candidateSyntaxNode.SyntaxTree.FilePath, - StringComparison.OrdinalIgnoreCase)) - select candidateSyntaxNode.GetLocation(); + return; } - public void OnDefinitionFound(ISymbol symbol) - { - if (FilterDefinition(symbol)) - { - return; - } - - // Partial types can have more than one declaring syntax references. - // Add remote locations for all the syntax references except the queried syntax node. - // To query for the partial locations, filter definition locations that occur in source whose span is part of - // span of any syntax node from Definition.DeclaringSyntaxReferences except for the queried syntax node. - var locations = symbol.Locations.Intersect(_queriedSymbol.Locations, LocationComparer.Instance).Any() - ? GetPartialLocations(symbol, _aggregateCancellationTokenSource.Token) - : symbol.Locations; - - _locations.AddRange(locations.Where(location => location.IsInSource)); - - if (SearchCapReached) - { - _aggregateCancellationTokenSource.Cancel(); - } - } + // Partial types can have more than one declaring syntax references. + // Add remote locations for all the syntax references except the queried syntax node. + // To query for the partial locations, filter definition locations that occur in source whose span is part of + // span of any syntax node from Definition.DeclaringSyntaxReferences except for the queried syntax node. + var locations = symbol.Locations.Intersect(_queriedSymbol.Locations, LocationComparer.Instance).Any() + ? GetPartialLocations(symbol, _aggregateCancellationTokenSource.Token) + : symbol.Locations; - /// - /// Exclude the following kind of symbols: - /// 1. Implicitly declared symbols (such as implicit fields backing properties) - /// 2. Symbols that can't be referenced by name (such as property getters and setters). - /// 3. Metadata only symbols, i.e. symbols with no location in source. - /// - private bool FilterReference(ISymbol definition, ReferenceLocation reference) - { - var isImplicitlyDeclared = definition.IsImplicitlyDeclared || definition.IsAccessor(); - // FindRefs treats a constructor invocation as a reference to the constructor symbol and to the named type symbol that defines it and - // so should we. Otherwise named types may have a reference count of 0, even if there are calls to its constructors, which might cause - // people think the class is not in use (#49636). - // Invocations to implicit parameterless constructors need to be included too. - var isConstructorInvocation = _queriedSymbol.Kind == SymbolKind.NamedType && - (definition as IMethodSymbol)?.MethodKind == MethodKind.Constructor; - return (isImplicitlyDeclared && !isConstructorInvocation) || - !reference.Location.IsInSource || - !definition.Locations.Any(static loc => loc.IsInSource); - } + _locations.AddRange(locations.Where(location => location.IsInSource)); - public void OnReferenceFound(ISymbol symbol, ReferenceLocation location) + if (SearchCapReached) { - if (FilterReference(symbol, location)) - { - return; - } + _aggregateCancellationTokenSource.Cancel(); + } + } - _locations.Add(location.Location); + /// + /// Exclude the following kind of symbols: + /// 1. Implicitly declared symbols (such as implicit fields backing properties) + /// 2. Symbols that can't be referenced by name (such as property getters and setters). + /// 3. Metadata only symbols, i.e. symbols with no location in source. + /// + private bool FilterReference(ISymbol definition, ReferenceLocation reference) + { + var isImplicitlyDeclared = definition.IsImplicitlyDeclared || definition.IsAccessor(); + // FindRefs treats a constructor invocation as a reference to the constructor symbol and to the named type symbol that defines it and + // so should we. Otherwise named types may have a reference count of 0, even if there are calls to its constructors, which might cause + // people think the class is not in use (#49636). + // Invocations to implicit parameterless constructors need to be included too. + var isConstructorInvocation = _queriedSymbol.Kind == SymbolKind.NamedType && + (definition as IMethodSymbol)?.MethodKind == MethodKind.Constructor; + return (isImplicitlyDeclared && !isConstructorInvocation) || + !reference.Location.IsInSource || + !definition.Locations.Any(static loc => loc.IsInSource); + } - if (SearchCapReached) - { - _aggregateCancellationTokenSource.Cancel(); - } + public void OnReferenceFound(ISymbol symbol, ReferenceLocation location) + { + if (FilterReference(symbol, location)) + { + return; } - public void ReportProgress(int current, int maximum) + _locations.Add(location.Location); + + if (SearchCapReached) { + _aggregateCancellationTokenSource.Cancel(); } + } - public void Dispose() - => _aggregateCancellationTokenSource.Dispose(); + public void ReportProgress(int current, int maximum) + { } + + public void Dispose() + => _aggregateCancellationTokenSource.Dispose(); } diff --git a/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs b/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs index d55c0aef087ef..9452a2787dd1b 100644 --- a/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs +++ b/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs @@ -18,325 +18,324 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CodeLens +namespace Microsoft.CodeAnalysis.CodeLens; + +internal sealed class CodeLensReferencesService : ICodeLensReferencesService { - internal sealed class CodeLensReferencesService : ICodeLensReferencesService - { - private static readonly SymbolDisplayFormat MethodDisplayFormat = - new(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - memberOptions: SymbolDisplayMemberOptions.IncludeContainingType); - - /// - /// Set ourselves as an implicit invocation of FindReferences. This will cause the finding operation to operate - /// in serial, not parallel. We're running ephemerally in the BG and do not want to saturate the system with - /// work that then slows the user down. Also, only process the inheritance hierarchy unidirectionally. We want - /// to find references that could actually call into a particular, not references to other members that could - /// never actually call into this member. - /// - private static readonly FindReferencesSearchOptions s_nonParallelSearch = - FindReferencesSearchOptions.Default with - { - Explicit = false, - UnidirectionalHierarchyCascade = true - }; + private static readonly SymbolDisplayFormat MethodDisplayFormat = + new(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + memberOptions: SymbolDisplayMemberOptions.IncludeContainingType); + + /// + /// Set ourselves as an implicit invocation of FindReferences. This will cause the finding operation to operate + /// in serial, not parallel. We're running ephemerally in the BG and do not want to saturate the system with + /// work that then slows the user down. Also, only process the inheritance hierarchy unidirectionally. We want + /// to find references that could actually call into a particular, not references to other members that could + /// never actually call into this member. + /// + private static readonly FindReferencesSearchOptions s_nonParallelSearch = + FindReferencesSearchOptions.Default with + { + Explicit = false, + UnidirectionalHierarchyCascade = true + }; - private static async Task FindAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, - Func> onResults, Func> onCapped, - int searchCap, CancellationToken cancellationToken) where T : struct + private static async Task FindAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, + Func> onResults, Func> onCapped, + int searchCap, CancellationToken cancellationToken) where T : struct + { + var document = await solution.GetDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + if (document == null) { - var document = await solution.GetDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - if (document == null) - { - return null; - } + return null; + } - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - var symbol = semanticModel.GetDeclaredSymbol(syntaxNode, cancellationToken); - if (symbol == null) - { - return null; - } + var symbol = semanticModel.GetDeclaredSymbol(syntaxNode, cancellationToken); + if (symbol == null) + { + return null; + } - using var progress = new CodeLensFindReferencesProgress(symbol, syntaxNode, searchCap, cancellationToken); - try - { - await SymbolFinder.FindReferencesAsync( - symbol, solution, progress, documents: null, s_nonParallelSearch, progress.CancellationToken).ConfigureAwait(false); + using var progress = new CodeLensFindReferencesProgress(symbol, syntaxNode, searchCap, cancellationToken); + try + { + await SymbolFinder.FindReferencesAsync( + symbol, solution, progress, documents: null, s_nonParallelSearch, progress.CancellationToken).ConfigureAwait(false); - return await onResults(progress).ConfigureAwait(false); - } - catch (OperationCanceledException) + return await onResults(progress).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + if (onCapped != null && progress.SearchCapReached) { - if (onCapped != null && progress.SearchCapReached) - { - // search was cancelled, and it was cancelled by us because a cap was reached. - return await onCapped(progress).ConfigureAwait(false); - } - - // search was cancelled, but not because of cap. - // this always throws. - throw; + // search was cancelled, and it was cancelled by us because a cap was reached. + return await onCapped(progress).ConfigureAwait(false); } + + // search was cancelled, but not because of cap. + // this always throws. + throw; } + } - public async ValueTask GetProjectCodeLensVersionAsync(Solution solution, ProjectId projectId, CancellationToken cancellationToken) + public async ValueTask GetProjectCodeLensVersionAsync(Solution solution, ProjectId projectId, CancellationToken cancellationToken) + { + return await solution.GetRequiredProject(projectId).GetDependentVersionAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task GetReferenceCountAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, int maxSearchResults, CancellationToken cancellationToken) + { + var projectVersion = await GetProjectCodeLensVersionAsync(solution, documentId.ProjectId, cancellationToken).ConfigureAwait(false); + return await FindAsync(solution, documentId, syntaxNode, + progress => Task.FromResult(new ReferenceCount( + progress.SearchCap > 0 + ? Math.Min(progress.ReferencesCount, progress.SearchCap) + : progress.ReferencesCount, progress.SearchCapReached, projectVersion.ToString())), + progress => Task.FromResult(new ReferenceCount(progress.SearchCap, IsCapped: true, projectVersion.ToString())), + maxSearchResults, cancellationToken).ConfigureAwait(false); + } + + private static async Task GetDescriptorOfEnclosingSymbolAsync(Solution solution, Location location, CancellationToken cancellationToken) + { + var document = solution.GetDocument(location.SourceTree); + + if (document == null) { - return await solution.GetRequiredProject(projectId).GetDependentVersionAsync(cancellationToken).ConfigureAwait(false); + return null; } - public async Task GetReferenceCountAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, int maxSearchResults, CancellationToken cancellationToken) + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var langServices = document.GetLanguageService(); + if (langServices == null) { - var projectVersion = await GetProjectCodeLensVersionAsync(solution, documentId.ProjectId, cancellationToken).ConfigureAwait(false); - return await FindAsync(solution, documentId, syntaxNode, - progress => Task.FromResult(new ReferenceCount( - progress.SearchCap > 0 - ? Math.Min(progress.ReferencesCount, progress.SearchCap) - : progress.ReferencesCount, progress.SearchCapReached, projectVersion.ToString())), - progress => Task.FromResult(new ReferenceCount(progress.SearchCap, IsCapped: true, projectVersion.ToString())), - maxSearchResults, cancellationToken).ConfigureAwait(false); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "Unsupported language '{0}'", semanticModel.Language), nameof(semanticModel)); } - private static async Task GetDescriptorOfEnclosingSymbolAsync(Solution solution, Location location, CancellationToken cancellationToken) + var position = location.SourceSpan.Start; + var token = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false)).FindToken(position, true); + var node = GetEnclosingCodeElementNode(document, token, langServices, cancellationToken); + var longName = langServices.GetDisplayName(semanticModel, node); + + // get the full line of source text on the line that contains this position + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + + // get the actual span of text for the line containing reference + var textLine = text.Lines.GetLineFromPosition(position); + + // turn the span from document relative to line relative + var spanStart = token.Span.Start - textLine.Span.Start; + var line = textLine.ToString(); + + var beforeLine1 = textLine.LineNumber > 0 ? text.Lines[textLine.LineNumber - 1].ToString() : string.Empty; + var beforeLine2 = textLine.LineNumber - 1 > 0 + ? text.Lines[textLine.LineNumber - 2].ToString() + : string.Empty; + var afterLine1 = textLine.LineNumber < text.Lines.Count - 1 + ? text.Lines[textLine.LineNumber + 1].ToString() + : string.Empty; + var afterLine2 = textLine.LineNumber + 1 < text.Lines.Count - 1 + ? text.Lines[textLine.LineNumber + 2].ToString() + : string.Empty; + var referenceSpan = new TextSpan(spanStart, token.Span.Length); + + var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken); + var glyph = symbol?.GetGlyph(); + var startLinePosition = location.GetLineSpan().StartLinePosition; + var documentId = solution.GetDocument(location.SourceTree)?.Id; + + return new ReferenceLocationDescriptor( + longName, + semanticModel.Language, + glyph, + token.Span.Start, + token.Span.Length, + startLinePosition.Line, + startLinePosition.Character, + documentId.ProjectId.Id, + documentId.Id, + document.FilePath, + line.TrimEnd(), + referenceSpan.Start, + referenceSpan.Length, + beforeLine1.TrimEnd(), + beforeLine2.TrimEnd(), + afterLine1.TrimEnd(), + afterLine2.TrimEnd()); + } + + private static SyntaxNode GetEnclosingCodeElementNode(Document document, SyntaxToken token, ICodeLensDisplayInfoService langServices, CancellationToken cancellationToken) + { + var syntaxFactsService = document.GetLanguageService(); + + var node = token.Parent; + while (node != null) { - var document = solution.GetDocument(location.SourceTree); + cancellationToken.ThrowIfCancellationRequested(); - if (document == null) + if (syntaxFactsService.IsDocumentationComment(node)) { - return null; + var structuredTriviaSyntax = (IStructuredTriviaSyntax)node; + var parentTrivia = structuredTriviaSyntax.ParentTrivia; + node = parentTrivia.Token.Parent; } - - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var langServices = document.GetLanguageService(); - if (langServices == null) + else if (syntaxFactsService.IsDeclaration(node) || + syntaxFactsService.IsUsingOrExternOrImport(node) || + syntaxFactsService.IsGlobalAssemblyAttribute(node)) { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "Unsupported language '{0}'", semanticModel.Language), nameof(semanticModel)); + break; } - - var position = location.SourceSpan.Start; - var token = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false)).FindToken(position, true); - var node = GetEnclosingCodeElementNode(document, token, langServices, cancellationToken); - var longName = langServices.GetDisplayName(semanticModel, node); - - // get the full line of source text on the line that contains this position - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - - // get the actual span of text for the line containing reference - var textLine = text.Lines.GetLineFromPosition(position); - - // turn the span from document relative to line relative - var spanStart = token.Span.Start - textLine.Span.Start; - var line = textLine.ToString(); - - var beforeLine1 = textLine.LineNumber > 0 ? text.Lines[textLine.LineNumber - 1].ToString() : string.Empty; - var beforeLine2 = textLine.LineNumber - 1 > 0 - ? text.Lines[textLine.LineNumber - 2].ToString() - : string.Empty; - var afterLine1 = textLine.LineNumber < text.Lines.Count - 1 - ? text.Lines[textLine.LineNumber + 1].ToString() - : string.Empty; - var afterLine2 = textLine.LineNumber + 1 < text.Lines.Count - 1 - ? text.Lines[textLine.LineNumber + 2].ToString() - : string.Empty; - var referenceSpan = new TextSpan(spanStart, token.Span.Length); - - var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken); - var glyph = symbol?.GetGlyph(); - var startLinePosition = location.GetLineSpan().StartLinePosition; - var documentId = solution.GetDocument(location.SourceTree)?.Id; - - return new ReferenceLocationDescriptor( - longName, - semanticModel.Language, - glyph, - token.Span.Start, - token.Span.Length, - startLinePosition.Line, - startLinePosition.Character, - documentId.ProjectId.Id, - documentId.Id, - document.FilePath, - line.TrimEnd(), - referenceSpan.Start, - referenceSpan.Length, - beforeLine1.TrimEnd(), - beforeLine2.TrimEnd(), - afterLine1.TrimEnd(), - afterLine2.TrimEnd()); - } - - private static SyntaxNode GetEnclosingCodeElementNode(Document document, SyntaxToken token, ICodeLensDisplayInfoService langServices, CancellationToken cancellationToken) - { - var syntaxFactsService = document.GetLanguageService(); - - var node = token.Parent; - while (node != null) + else { - cancellationToken.ThrowIfCancellationRequested(); - - if (syntaxFactsService.IsDocumentationComment(node)) - { - var structuredTriviaSyntax = (IStructuredTriviaSyntax)node; - var parentTrivia = structuredTriviaSyntax.ParentTrivia; - node = parentTrivia.Token.Parent; - } - else if (syntaxFactsService.IsDeclaration(node) || - syntaxFactsService.IsUsingOrExternOrImport(node) || - syntaxFactsService.IsGlobalAssemblyAttribute(node)) - { - break; - } - else - { - node = node.Parent; - } + node = node.Parent; } + } - node ??= token.Parent; + node ??= token.Parent; - return langServices.GetDisplayNode(node); - } + return langServices.GetDisplayNode(node); + } - public async Task?> FindReferenceLocationsAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, CancellationToken cancellationToken) - { - return await FindAsync(solution, documentId, syntaxNode, - async progress => - { - var referenceTasks = progress.Locations - .Select(location => GetDescriptorOfEnclosingSymbolAsync(solution, location, cancellationToken)) - .ToArray(); + public async Task?> FindReferenceLocationsAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + return await FindAsync(solution, documentId, syntaxNode, + async progress => + { + var referenceTasks = progress.Locations + .Select(location => GetDescriptorOfEnclosingSymbolAsync(solution, location, cancellationToken)) + .ToArray(); - var result = await Task.WhenAll(referenceTasks).ConfigureAwait(false); + var result = await Task.WhenAll(referenceTasks).ConfigureAwait(false); - return result.ToImmutableArray(); - }, onCapped: null, searchCap: 0, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return result.ToImmutableArray(); + }, onCapped: null, searchCap: 0, cancellationToken: cancellationToken).ConfigureAwait(false); + } - private static ISymbol GetEnclosingMethod(SemanticModel semanticModel, Location location, CancellationToken cancellationToken) + private static ISymbol GetEnclosingMethod(SemanticModel semanticModel, Location location, CancellationToken cancellationToken) + { + var enclosingSymbol = semanticModel.GetEnclosingSymbol(location.SourceSpan.Start, cancellationToken); + + for (var current = enclosingSymbol; current != null; current = current.ContainingSymbol) { - var enclosingSymbol = semanticModel.GetEnclosingSymbol(location.SourceSpan.Start, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); - for (var current = enclosingSymbol; current != null; current = current.ContainingSymbol) + if (current.Kind != SymbolKind.Method) { - cancellationToken.ThrowIfCancellationRequested(); - - if (current.Kind != SymbolKind.Method) - { - continue; - } + continue; + } - var method = (IMethodSymbol)current; - if (method.IsAccessor()) - { - return method.AssociatedSymbol; - } + var method = (IMethodSymbol)current; + if (method.IsAccessor()) + { + return method.AssociatedSymbol; + } - if (method.MethodKind != MethodKind.AnonymousFunction) - { - return method; - } + if (method.MethodKind != MethodKind.AnonymousFunction) + { + return method; } + } + + return null; + } + private static async Task TryGetMethodDescriptorAsync(Location commonLocation, Solution solution, CancellationToken cancellationToken) + { + var document = solution.GetDocument(commonLocation.SourceTree); + if (document == null) + { return null; } - private static async Task TryGetMethodDescriptorAsync(Location commonLocation, Solution solution, CancellationToken cancellationToken) - { - var document = solution.GetDocument(commonLocation.SourceTree); - if (document == null) - { - return null; - } + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var fullName = GetEnclosingMethod(semanticModel, commonLocation, cancellationToken)?.ToDisplayString(MethodDisplayFormat); - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var fullName = GetEnclosingMethod(semanticModel, commonLocation, cancellationToken)?.ToDisplayString(MethodDisplayFormat); + return !string.IsNullOrEmpty(fullName) ? new ReferenceMethodDescriptor(fullName, document.FilePath, document.Project.OutputFilePath) : null; + } - return !string.IsNullOrEmpty(fullName) ? new ReferenceMethodDescriptor(fullName, document.FilePath, document.Project.OutputFilePath) : null; - } + public Task?> FindReferenceMethodsAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + return FindAsync(solution, documentId, syntaxNode, + async progress => + { + var descriptorTasks = + progress.Locations + .Select(location => TryGetMethodDescriptorAsync(location, solution, cancellationToken)) + .ToArray(); - public Task?> FindReferenceMethodsAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, CancellationToken cancellationToken) - { - return FindAsync(solution, documentId, syntaxNode, - async progress => - { - var descriptorTasks = - progress.Locations - .Select(location => TryGetMethodDescriptorAsync(location, solution, cancellationToken)) - .ToArray(); + var result = await Task.WhenAll(descriptorTasks).ConfigureAwait(false); - var result = await Task.WhenAll(descriptorTasks).ConfigureAwait(false); + return result.OfType().ToImmutableArray(); + }, onCapped: null, searchCap: 0, cancellationToken: cancellationToken); + } - return result.OfType().ToImmutableArray(); - }, onCapped: null, searchCap: 0, cancellationToken: cancellationToken); - } + public async Task GetFullyQualifiedNameAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, + CancellationToken cancellationToken) + { + var document = solution.GetDocument(syntaxNode.GetLocation().SourceTree); - public async Task GetFullyQualifiedNameAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, - CancellationToken cancellationToken) - { - var document = solution.GetDocument(syntaxNode.GetLocation().SourceTree); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var declaredSymbol = semanticModel.GetDeclaredSymbol(syntaxNode, cancellationToken); - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var declaredSymbol = semanticModel.GetDeclaredSymbol(syntaxNode, cancellationToken); + if (declaredSymbol == null) + { + return string.Empty; + } - if (declaredSymbol == null) - { - return string.Empty; - } + var parts = declaredSymbol.ToDisplayParts(MethodDisplayFormat); + var pool = PooledStringBuilder.GetInstance(); - var parts = declaredSymbol.ToDisplayParts(MethodDisplayFormat); - var pool = PooledStringBuilder.GetInstance(); + try + { + var actualBuilder = pool.Builder; + var previousWasClass = false; - try + for (var index = 0; index < parts.Length; index++) { - var actualBuilder = pool.Builder; - var previousWasClass = false; - - for (var index = 0; index < parts.Length; index++) + var part = parts[index]; + if (previousWasClass && + part.Kind == SymbolDisplayPartKind.Punctuation && + index < parts.Length - 1) { - var part = parts[index]; - if (previousWasClass && - part.Kind == SymbolDisplayPartKind.Punctuation && - index < parts.Length - 1) + switch (parts[index + 1].Kind) { - switch (parts[index + 1].Kind) - { - case SymbolDisplayPartKind.ClassName: - case SymbolDisplayPartKind.RecordClassName: - case SymbolDisplayPartKind.DelegateName: - case SymbolDisplayPartKind.EnumName: - case SymbolDisplayPartKind.ErrorTypeName: - case SymbolDisplayPartKind.InterfaceName: - case SymbolDisplayPartKind.StructName: - case SymbolDisplayPartKind.RecordStructName: - actualBuilder.Append('+'); - break; - - default: - actualBuilder.Append(part); - break; - } + case SymbolDisplayPartKind.ClassName: + case SymbolDisplayPartKind.RecordClassName: + case SymbolDisplayPartKind.DelegateName: + case SymbolDisplayPartKind.EnumName: + case SymbolDisplayPartKind.ErrorTypeName: + case SymbolDisplayPartKind.InterfaceName: + case SymbolDisplayPartKind.StructName: + case SymbolDisplayPartKind.RecordStructName: + actualBuilder.Append('+'); + break; + + default: + actualBuilder.Append(part); + break; } - else - { - actualBuilder.Append(part); - } - - previousWasClass = part.Kind is SymbolDisplayPartKind.ClassName or - SymbolDisplayPartKind.RecordClassName or - SymbolDisplayPartKind.InterfaceName or - SymbolDisplayPartKind.StructName or - SymbolDisplayPartKind.RecordStructName; + } + else + { + actualBuilder.Append(part); } - return actualBuilder.ToString(); - } - finally - { - pool.Free(); + previousWasClass = part.Kind is SymbolDisplayPartKind.ClassName or + SymbolDisplayPartKind.RecordClassName or + SymbolDisplayPartKind.InterfaceName or + SymbolDisplayPartKind.StructName or + SymbolDisplayPartKind.RecordStructName; } + + return actualBuilder.ToString(); + } + finally + { + pool.Free(); } } } diff --git a/src/Features/Core/Portable/CodeLens/CodeLensReferencesServiceFactory.cs b/src/Features/Core/Portable/CodeLens/CodeLensReferencesServiceFactory.cs index 40ada8882e2d4..ec245f4668dbc 100644 --- a/src/Features/Core/Portable/CodeLens/CodeLensReferencesServiceFactory.cs +++ b/src/Features/Core/Portable/CodeLens/CodeLensReferencesServiceFactory.cs @@ -9,20 +9,19 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CodeLens -{ - [ExportWorkspaceServiceFactory(typeof(ICodeLensReferencesService)), Shared] - internal sealed class CodeLensReferencesServiceFactory : IWorkspaceServiceFactory - { - public static readonly ICodeLensReferencesService Instance = new CodeLensReferencesService(); +namespace Microsoft.CodeAnalysis.CodeLens; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CodeLensReferencesServiceFactory() - { - } +[ExportWorkspaceServiceFactory(typeof(ICodeLensReferencesService)), Shared] +internal sealed class CodeLensReferencesServiceFactory : IWorkspaceServiceFactory +{ + public static readonly ICodeLensReferencesService Instance = new CodeLensReferencesService(); - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => Instance; + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CodeLensReferencesServiceFactory() + { } + + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => Instance; } diff --git a/src/Features/Core/Portable/CodeLens/ICodeLensDisplayInfoService.cs b/src/Features/Core/Portable/CodeLens/ICodeLensDisplayInfoService.cs index dda0a91324e6f..a269a5ae88e4c 100644 --- a/src/Features/Core/Portable/CodeLens/ICodeLensDisplayInfoService.cs +++ b/src/Features/Core/Portable/CodeLens/ICodeLensDisplayInfoService.cs @@ -6,18 +6,17 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.CodeLens +namespace Microsoft.CodeAnalysis.CodeLens; + +internal interface ICodeLensDisplayInfoService : ILanguageService { - internal interface ICodeLensDisplayInfoService : ILanguageService - { - /// - /// Gets the node used for display info - /// - SyntaxNode GetDisplayNode(SyntaxNode node); + /// + /// Gets the node used for display info + /// + SyntaxNode GetDisplayNode(SyntaxNode node); - /// - /// Gets the DisplayName for the given node - /// - string GetDisplayName(SemanticModel semanticModel, SyntaxNode node); - } + /// + /// Gets the DisplayName for the given node + /// + string GetDisplayName(SemanticModel semanticModel, SyntaxNode node); } diff --git a/src/Features/Core/Portable/CodeLens/ICodeLensReferencesService.cs b/src/Features/Core/Portable/CodeLens/ICodeLensReferencesService.cs index ab1cc44397bd1..9aaa5eec4d1f2 100644 --- a/src/Features/Core/Portable/CodeLens/ICodeLensReferencesService.cs +++ b/src/Features/Core/Portable/CodeLens/ICodeLensReferencesService.cs @@ -7,34 +7,33 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.CodeLens +namespace Microsoft.CodeAnalysis.CodeLens; + +internal interface ICodeLensReferencesService : IWorkspaceService { - internal interface ICodeLensReferencesService : IWorkspaceService - { - ValueTask GetProjectCodeLensVersionAsync(Solution solution, ProjectId projectId, CancellationToken cancellationToken); + ValueTask GetProjectCodeLensVersionAsync(Solution solution, ProjectId projectId, CancellationToken cancellationToken); - /// - /// Given a document and syntax node, returns the number of locations where the located node is referenced. - /// - /// Optionally, the service supports capping the reference count to a value specified by - /// if is greater than 0. - /// - /// - Task GetReferenceCountAsync(Solution solution, DocumentId documentId, SyntaxNode? syntaxNode, int maxSearchResults, CancellationToken cancellationToken); + /// + /// Given a document and syntax node, returns the number of locations where the located node is referenced. + /// + /// Optionally, the service supports capping the reference count to a value specified by + /// if is greater than 0. + /// + /// + Task GetReferenceCountAsync(Solution solution, DocumentId documentId, SyntaxNode? syntaxNode, int maxSearchResults, CancellationToken cancellationToken); - /// - /// Given a document and syntax node, returns a collection of locations where the located node is referenced. - /// - Task?> FindReferenceLocationsAsync(Solution solution, DocumentId documentId, SyntaxNode? syntaxNode, CancellationToken cancellationToken); + /// + /// Given a document and syntax node, returns a collection of locations where the located node is referenced. + /// + Task?> FindReferenceLocationsAsync(Solution solution, DocumentId documentId, SyntaxNode? syntaxNode, CancellationToken cancellationToken); - /// - /// Given a document and syntax node, returns a collection of locations of methods that refer to the located node. - /// - Task?> FindReferenceMethodsAsync(Solution solution, DocumentId documentId, SyntaxNode? syntaxNode, CancellationToken cancellationToken); + /// + /// Given a document and syntax node, returns a collection of locations of methods that refer to the located node. + /// + Task?> FindReferenceMethodsAsync(Solution solution, DocumentId documentId, SyntaxNode? syntaxNode, CancellationToken cancellationToken); - /// - /// Given a document and syntax node, returns the fully qualified name of the located node's declaration. - /// - Task GetFullyQualifiedNameAsync(Solution solution, DocumentId documentId, SyntaxNode? syntaxNode, CancellationToken cancellationToken); - } + /// + /// Given a document and syntax node, returns the fully qualified name of the located node's declaration. + /// + Task GetFullyQualifiedNameAsync(Solution solution, DocumentId documentId, SyntaxNode? syntaxNode, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/CodeLens/IRemoteCodeLensReferencesService.cs b/src/Features/Core/Portable/CodeLens/IRemoteCodeLensReferencesService.cs index 1ef3a640fc58e..cc0ccbe1bd488 100644 --- a/src/Features/Core/Portable/CodeLens/IRemoteCodeLensReferencesService.cs +++ b/src/Features/Core/Portable/CodeLens/IRemoteCodeLensReferencesService.cs @@ -10,13 +10,12 @@ using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CodeLens +namespace Microsoft.CodeAnalysis.CodeLens; + +internal interface IRemoteCodeLensReferencesService { - internal interface IRemoteCodeLensReferencesService - { - ValueTask GetReferenceCountAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, int maxResultCount, CancellationToken cancellationToken); - ValueTask?> FindReferenceLocationsAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken); - ValueTask?> FindReferenceMethodsAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken); - ValueTask GetFullyQualifiedNameAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken); - } + ValueTask GetReferenceCountAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, int maxResultCount, CancellationToken cancellationToken); + ValueTask?> FindReferenceLocationsAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken); + ValueTask?> FindReferenceMethodsAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken); + ValueTask GetFullyQualifiedNameAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/CodeLens/LocationComparer.cs b/src/Features/Core/Portable/CodeLens/LocationComparer.cs index 582ffdbb424a2..3cf10c485e91e 100644 --- a/src/Features/Core/Portable/CodeLens/LocationComparer.cs +++ b/src/Features/Core/Portable/CodeLens/LocationComparer.cs @@ -8,32 +8,31 @@ using System.Collections.Generic; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeLens +namespace Microsoft.CodeAnalysis.CodeLens; + +internal sealed class LocationComparer : IEqualityComparer { - internal sealed class LocationComparer : IEqualityComparer - { - public static LocationComparer Instance { get; } = new LocationComparer(); + public static LocationComparer Instance { get; } = new LocationComparer(); - public bool Equals(Location x, Location y) + public bool Equals(Location x, Location y) + { + if (x != null && x.IsInSource && y != null && y.IsInSource) { - if (x != null && x.IsInSource && y != null && y.IsInSource) - { - return x.SourceSpan.Equals(y.SourceSpan) && - x.SourceTree.FilePath.Equals(y.SourceTree.FilePath, StringComparison.OrdinalIgnoreCase); - } - - return object.Equals(x, y); + return x.SourceSpan.Equals(y.SourceSpan) && + x.SourceTree.FilePath.Equals(y.SourceTree.FilePath, StringComparison.OrdinalIgnoreCase); } - public int GetHashCode(Location obj) - { - if (obj != null && obj.IsInSource) - { - return Hash.Combine(obj.SourceSpan.GetHashCode(), - StringComparer.OrdinalIgnoreCase.GetHashCode(obj.SourceTree.FilePath)); - } + return object.Equals(x, y); + } - return obj?.GetHashCode() ?? 0; + public int GetHashCode(Location obj) + { + if (obj != null && obj.IsInSource) + { + return Hash.Combine(obj.SourceSpan.GetHashCode(), + StringComparer.OrdinalIgnoreCase.GetHashCode(obj.SourceTree.FilePath)); } + + return obj?.GetHashCode() ?? 0; } } diff --git a/src/Features/Core/Portable/CodeLens/ReferenceCount.cs b/src/Features/Core/Portable/CodeLens/ReferenceCount.cs index 1e480f036389b..3e2d586dcb5a2 100644 --- a/src/Features/Core/Portable/CodeLens/ReferenceCount.cs +++ b/src/Features/Core/Portable/CodeLens/ReferenceCount.cs @@ -4,32 +4,31 @@ using System.Runtime.Serialization; -namespace Microsoft.CodeAnalysis.CodeLens +namespace Microsoft.CodeAnalysis.CodeLens; + +/// +/// Represents the result of a FindReferences Count operation. +/// +/// Represents the number of references to a given symbol. +/// Represents if the count is capped by a certain maximum. +[DataContract] +internal readonly record struct ReferenceCount( + [property: DataMember(Order = 0)] int Count, + [property: DataMember(Order = 1)] bool IsCapped, + [property: DataMember(Order = 2)] string Version) { - /// - /// Represents the result of a FindReferences Count operation. - /// - /// Represents the number of references to a given symbol. - /// Represents if the count is capped by a certain maximum. - [DataContract] - internal readonly record struct ReferenceCount( - [property: DataMember(Order = 0)] int Count, - [property: DataMember(Order = 1)] bool IsCapped, - [property: DataMember(Order = 2)] string Version) + public string GetDescription() { - public string GetDescription() - { - var referenceWord = Count == 1 - ? FeaturesResources._0_reference_unquoted - : FeaturesResources._0_references_unquoted; + var referenceWord = Count == 1 + ? FeaturesResources._0_reference_unquoted + : FeaturesResources._0_references_unquoted; - var description = string.Format(referenceWord, GetCappedReferenceCountString()); - return description; - } + var description = string.Format(referenceWord, GetCappedReferenceCountString()); + return description; + } - public string GetToolTip(string? codeElementKind) - => string.Format(FeaturesResources.This_0_has_1_references, codeElementKind, GetCappedReferenceCountString()); + public string GetToolTip(string? codeElementKind) + => string.Format(FeaturesResources.This_0_has_1_references, codeElementKind, GetCappedReferenceCountString()); - private string GetCappedReferenceCountString() => $"{Count}{(IsCapped ? "+" : string.Empty)}"; - } + private string GetCappedReferenceCountString() => $"{Count}{(IsCapped ? "+" : string.Empty)}"; } diff --git a/src/Features/Core/Portable/CodeLens/ReferenceLocationDescriptor.cs b/src/Features/Core/Portable/CodeLens/ReferenceLocationDescriptor.cs index 3f7fb6cddfe6a..bb08943825cf2 100644 --- a/src/Features/Core/Portable/CodeLens/ReferenceLocationDescriptor.cs +++ b/src/Features/Core/Portable/CodeLens/ReferenceLocationDescriptor.cs @@ -7,125 +7,124 @@ using System; using System.Runtime.Serialization; -namespace Microsoft.CodeAnalysis.CodeLens +namespace Microsoft.CodeAnalysis.CodeLens; + +/// +/// Holds information required to display and navigate to individual references +/// +[DataContract] +internal sealed class ReferenceLocationDescriptor( + string longDescription, + string language, + Glyph? glyph, + int spanStart, + int spanLength, + int lineNumber, + int columnNumber, + Guid projectGuid, + Guid documentGuid, + string filePath, + string referenceLineText, + int referenceStart, + int referenceLength, + string beforeReferenceText1, + string beforeReferenceText2, + string afterReferenceText1, + string afterReferenceText2) { /// - /// Holds information required to display and navigate to individual references + /// Fully qualified name of the symbol containing the reference location /// - [DataContract] - internal sealed class ReferenceLocationDescriptor( - string longDescription, - string language, - Glyph? glyph, - int spanStart, - int spanLength, - int lineNumber, - int columnNumber, - Guid projectGuid, - Guid documentGuid, - string filePath, - string referenceLineText, - int referenceStart, - int referenceLength, - string beforeReferenceText1, - string beforeReferenceText2, - string afterReferenceText1, - string afterReferenceText2) - { - /// - /// Fully qualified name of the symbol containing the reference location - /// - [DataMember(Order = 0)] - public string LongDescription { get; } = longDescription; - - /// - /// Language of the reference location - /// - [DataMember(Order = 1)] - public string Language { get; } = language; - - /// - /// The kind of symbol containing the reference location (such as type, method, property, etc.) - /// - [DataMember(Order = 2)] - public Glyph? Glyph { get; } = glyph; - - /// - /// Reference's span start based on the document content - /// - [DataMember(Order = 3)] - public int SpanStart { get; } = spanStart; - - /// - /// Reference's span length based on the document content - /// - [DataMember(Order = 4)] - public int SpanLength { get; } = spanLength; - - /// - /// Reference's line based on the document content - /// - [DataMember(Order = 5)] - public int LineNumber { get; } = lineNumber; - - /// - /// Reference's character based on the document content - /// - [DataMember(Order = 6)] - public int ColumnNumber { get; } = columnNumber; - - [DataMember(Order = 7)] - public Guid ProjectGuid { get; } = projectGuid; - - [DataMember(Order = 8)] - public Guid DocumentGuid { get; } = documentGuid; - - /// - /// Document's file path - /// - [DataMember(Order = 9)] - public string FilePath { get; } = filePath; - - /// - /// the full line of source that contained the reference - /// - [DataMember(Order = 10)] - public string ReferenceLineText { get; } = referenceLineText; - - /// - /// the beginning of the span within reference text that was the use of the reference - /// - [DataMember(Order = 11)] - public int ReferenceStart { get; } = referenceStart; - - /// - /// the length of the span of the reference - /// - [DataMember(Order = 12)] - public int ReferenceLength { get; } = referenceLength; - - /// - /// Text above the line with the reference - /// - [DataMember(Order = 13)] - public string BeforeReferenceText1 { get; } = beforeReferenceText1; - - /// - /// Text above the line with the reference - /// - [DataMember(Order = 14)] - public string BeforeReferenceText2 { get; } = beforeReferenceText2; - - /// - /// Text below the line with the reference - /// - [DataMember(Order = 15)] - public string AfterReferenceText1 { get; } = afterReferenceText1; - - /// - /// Text below the line with the reference - /// - [DataMember(Order = 16)] - public string AfterReferenceText2 { get; } = afterReferenceText2; - } + [DataMember(Order = 0)] + public string LongDescription { get; } = longDescription; + + /// + /// Language of the reference location + /// + [DataMember(Order = 1)] + public string Language { get; } = language; + + /// + /// The kind of symbol containing the reference location (such as type, method, property, etc.) + /// + [DataMember(Order = 2)] + public Glyph? Glyph { get; } = glyph; + + /// + /// Reference's span start based on the document content + /// + [DataMember(Order = 3)] + public int SpanStart { get; } = spanStart; + + /// + /// Reference's span length based on the document content + /// + [DataMember(Order = 4)] + public int SpanLength { get; } = spanLength; + + /// + /// Reference's line based on the document content + /// + [DataMember(Order = 5)] + public int LineNumber { get; } = lineNumber; + + /// + /// Reference's character based on the document content + /// + [DataMember(Order = 6)] + public int ColumnNumber { get; } = columnNumber; + + [DataMember(Order = 7)] + public Guid ProjectGuid { get; } = projectGuid; + + [DataMember(Order = 8)] + public Guid DocumentGuid { get; } = documentGuid; + + /// + /// Document's file path + /// + [DataMember(Order = 9)] + public string FilePath { get; } = filePath; + + /// + /// the full line of source that contained the reference + /// + [DataMember(Order = 10)] + public string ReferenceLineText { get; } = referenceLineText; + + /// + /// the beginning of the span within reference text that was the use of the reference + /// + [DataMember(Order = 11)] + public int ReferenceStart { get; } = referenceStart; + + /// + /// the length of the span of the reference + /// + [DataMember(Order = 12)] + public int ReferenceLength { get; } = referenceLength; + + /// + /// Text above the line with the reference + /// + [DataMember(Order = 13)] + public string BeforeReferenceText1 { get; } = beforeReferenceText1; + + /// + /// Text above the line with the reference + /// + [DataMember(Order = 14)] + public string BeforeReferenceText2 { get; } = beforeReferenceText2; + + /// + /// Text below the line with the reference + /// + [DataMember(Order = 15)] + public string AfterReferenceText1 { get; } = afterReferenceText1; + + /// + /// Text below the line with the reference + /// + [DataMember(Order = 16)] + public string AfterReferenceText2 { get; } = afterReferenceText2; } diff --git a/src/Features/Core/Portable/CodeLens/ReferenceMethodDescriptor.cs b/src/Features/Core/Portable/CodeLens/ReferenceMethodDescriptor.cs index 38368b1cbbe7f..e666941894cf1 100644 --- a/src/Features/Core/Portable/CodeLens/ReferenceMethodDescriptor.cs +++ b/src/Features/Core/Portable/CodeLens/ReferenceMethodDescriptor.cs @@ -6,39 +6,38 @@ using System.Runtime.Serialization; -namespace Microsoft.CodeAnalysis.CodeLens +namespace Microsoft.CodeAnalysis.CodeLens; + +/// +/// A caller method of a callee +/// +/// +/// Describe a caller method of a callee +/// +/// Method's fully qualified name +/// Method full path +/// +/// Method full name is expected to be in the .NET full name type convention. That is, +/// namespace/type is delimited by '.' and nested type is delimited by '+' +/// +[DataContract] +internal sealed class ReferenceMethodDescriptor(string fullName, string filePath, string outputFilePath) { /// - /// A caller method of a callee + /// Returns method's fully quilified name without parameters /// - /// - /// Describe a caller method of a callee - /// - /// Method's fully qualified name - /// Method full path - /// - /// Method full name is expected to be in the .NET full name type convention. That is, - /// namespace/type is delimited by '.' and nested type is delimited by '+' - /// - [DataContract] - internal sealed class ReferenceMethodDescriptor(string fullName, string filePath, string outputFilePath) - { - /// - /// Returns method's fully quilified name without parameters - /// - [DataMember(Order = 0)] - public string FullName { get; private set; } = fullName; + [DataMember(Order = 0)] + public string FullName { get; private set; } = fullName; - /// - /// Returns method's file path. - /// - [DataMember(Order = 1)] - public string FilePath { get; private set; } = filePath; + /// + /// Returns method's file path. + /// + [DataMember(Order = 1)] + public string FilePath { get; private set; } = filePath; - /// - /// Returns output file path for the project containing the method. - /// - [DataMember(Order = 2)] - public string OutputFilePath { get; private set; } = outputFilePath; - } + /// + /// Returns output file path for the project containing the method. + /// + [DataMember(Order = 2)] + public string OutputFilePath { get; private set; } = outputFilePath; } diff --git a/src/Features/Core/Portable/CodeRefactoringHelpers.cs b/src/Features/Core/Portable/CodeRefactoringHelpers.cs index b8d7a10ed8183..02022661099bd 100644 --- a/src/Features/Core/Portable/CodeRefactoringHelpers.cs +++ b/src/Features/Core/Portable/CodeRefactoringHelpers.cs @@ -9,124 +9,123 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +internal static class CodeRefactoringHelpers { - internal static class CodeRefactoringHelpers + /// + /// + /// Determines if a is under-selected given . + /// + /// + /// Under-selection is defined as omitting whole nodes from either the beginning or the end. It can be used e.g. + /// to detect that following selection `1 + [|2 + 3|]` is under-selecting the whole expression node tree. + /// + /// + /// Returns false if only and precisely one is selected. In that case the is treated more as a caret location. + /// + /// + /// It's intended to be used in conjunction with that, for non-empty selections, returns the smallest encompassing node. A node that + /// can, for certain refactorings, be too large given user-selection even though it is the smallest that can be + /// retrieved. + /// + /// + /// When doesn't intersect the node in any way it's not considered to be + /// under-selected. + /// + /// + /// Null node is always considered under-selected. + /// + /// + public static bool IsNodeUnderselected(SyntaxNode? node, TextSpan selection) { - /// - /// - /// Determines if a is under-selected given . - /// - /// - /// Under-selection is defined as omitting whole nodes from either the beginning or the end. It can be used e.g. - /// to detect that following selection `1 + [|2 + 3|]` is under-selecting the whole expression node tree. - /// - /// - /// Returns false if only and precisely one is selected. In that case the is treated more as a caret location. - /// - /// - /// It's intended to be used in conjunction with that, for non-empty selections, returns the smallest encompassing node. A node that - /// can, for certain refactorings, be too large given user-selection even though it is the smallest that can be - /// retrieved. - /// - /// - /// When doesn't intersect the node in any way it's not considered to be - /// under-selected. - /// - /// - /// Null node is always considered under-selected. - /// - /// - public static bool IsNodeUnderselected(SyntaxNode? node, TextSpan selection) + // Selection is null -> it's always under-selected + // REASON: Easier API use -> under-selected node, don't work on it further + if (node == null) { - // Selection is null -> it's always under-selected - // REASON: Easier API use -> under-selected node, don't work on it further - if (node == null) - { - return true; - } + return true; + } - // Selection or node is empty -> can't be under-selected - if (selection.IsEmpty || node.Span.IsEmpty) - { - return false; - } + // Selection or node is empty -> can't be under-selected + if (selection.IsEmpty || node.Span.IsEmpty) + { + return false; + } - // Selection is larger than node.Span -> can't be under-selecting - if (selection.Contains(node.Span)) - { - return false; - } + // Selection is larger than node.Span -> can't be under-selecting + if (selection.Contains(node.Span)) + { + return false; + } - // Selection doesn't intersect node -> can't be under-selecting. - // RATIONALE: If there's no intersection then we got the node in some other way, e.g. - // extracting it after user selected `;` at the end of an expression statement - // `goo()[|;|]` for `goo()` node. - if (!node.FullSpan.OverlapsWith(selection)) - { - return false; - } + // Selection doesn't intersect node -> can't be under-selecting. + // RATIONALE: If there's no intersection then we got the node in some other way, e.g. + // extracting it after user selected `;` at the end of an expression statement + // `goo()[|;|]` for `goo()` node. + if (!node.FullSpan.OverlapsWith(selection)) + { + return false; + } - // Only precisely one token of the node is selected -> treat is as empty selection -> not - // under-selected. The rationale is that if only one Token is selected then the selection - // wasn't about precisely getting the one node and nothing else & therefore we should treat - // it as empty selection. - if (node.FullSpan.Contains(selection.Start)) + // Only precisely one token of the node is selected -> treat is as empty selection -> not + // under-selected. The rationale is that if only one Token is selected then the selection + // wasn't about precisely getting the one node and nothing else & therefore we should treat + // it as empty selection. + if (node.FullSpan.Contains(selection.Start)) + { + var selectionStartToken = node.FindToken(selection.Start); + if (selection.IsAround(selectionStartToken)) { - var selectionStartToken = node.FindToken(selection.Start); - if (selection.IsAround(selectionStartToken)) - { - return false; - } + return false; } + } - var beginningNode = node.FindToken(node.Span.Start).Parent; - var endNode = node.FindToken(node.Span.End - 1).Parent; - RoslynDebug.Assert(beginningNode is object); - RoslynDebug.Assert(endNode is object); + var beginningNode = node.FindToken(node.Span.Start).Parent; + var endNode = node.FindToken(node.Span.End - 1).Parent; + RoslynDebug.Assert(beginningNode is object); + RoslynDebug.Assert(endNode is object); - // Node is under-selected if either the first (lowest) child doesn't contain start of selection - // of the last child doesn't intersect with the end. + // Node is under-selected if either the first (lowest) child doesn't contain start of selection + // of the last child doesn't intersect with the end. - // Node is under-selected if either the first (lowest) child ends before the selection has started - // or the last child starts after the selection ends (i.e. one of them is completely on the outside of selection). - // It's a crude heuristic but it allows omitting parts of nodes or trivial tokens from the beginning/end - // but fires up e.g.: `1 + [|2 + 3|]`. - return beginningNode.Span.End <= selection.Start || endNode.Span.Start >= selection.End; - } + // Node is under-selected if either the first (lowest) child ends before the selection has started + // or the last child starts after the selection ends (i.e. one of them is completely on the outside of selection). + // It's a crude heuristic but it allows omitting parts of nodes or trivial tokens from the beginning/end + // but fires up e.g.: `1 + [|2 + 3|]`. + return beginningNode.Span.End <= selection.Start || endNode.Span.Start >= selection.End; + } - /// - /// Trims leading and trailing whitespace from . - /// - /// - /// Returns unchanged in case . - /// Returns empty Span with original in case it contains only whitespace. - /// - public static async Task GetTrimmedTextSpanAsync(Document document, TextSpan span, CancellationToken cancellationToken) + /// + /// Trims leading and trailing whitespace from . + /// + /// + /// Returns unchanged in case . + /// Returns empty Span with original in case it contains only whitespace. + /// + public static async Task GetTrimmedTextSpanAsync(Document document, TextSpan span, CancellationToken cancellationToken) + { + if (span.IsEmpty) { - if (span.IsEmpty) - { - return span; - } - - var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var start = span.Start; - var end = span.End; + return span; + } - while (start < end && char.IsWhiteSpace(sourceText[end - 1])) - { - end--; - } + var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var start = span.Start; + var end = span.End; - while (start < end && char.IsWhiteSpace(sourceText[start])) - { - start++; - } + while (start < end && char.IsWhiteSpace(sourceText[end - 1])) + { + end--; + } - return TextSpan.FromBounds(start, end); + while (start < end && char.IsWhiteSpace(sourceText[start])) + { + start++; } + + return TextSpan.FromBounds(start, end); } } diff --git a/src/Features/Core/Portable/CodeRefactorings/AbstractRefactoringHelpersService.cs b/src/Features/Core/Portable/CodeRefactorings/AbstractRefactoringHelpersService.cs index 45588381c6445..53ed67ddf0918 100644 --- a/src/Features/Core/Portable/CodeRefactorings/AbstractRefactoringHelpersService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/AbstractRefactoringHelpersService.cs @@ -14,430 +14,414 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +internal abstract class AbstractRefactoringHelpersService : IRefactoringHelpersService + where TExpressionSyntax : SyntaxNode + where TArgumentSyntax : SyntaxNode + where TExpressionStatementSyntax : SyntaxNode { - internal abstract class AbstractRefactoringHelpersService : IRefactoringHelpersService - where TExpressionSyntax : SyntaxNode - where TArgumentSyntax : SyntaxNode - where TExpressionStatementSyntax : SyntaxNode + protected abstract IHeaderFacts HeaderFacts { get; } + + public abstract bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration); + + public async Task> GetRelevantNodesAsync( + Document document, TextSpan selectionRaw, bool allowEmptyNodes, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode { - protected abstract IHeaderFacts HeaderFacts { get; } + using var _1 = ArrayBuilder.GetInstance(out var relevantNodesBuilder); + await AddRelevantNodesAsync(document, selectionRaw, relevantNodesBuilder, cancellationToken).ConfigureAwait(false); - public abstract bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration); + if (allowEmptyNodes) + return relevantNodesBuilder.ToImmutable(); - public async Task> GetRelevantNodesAsync( - Document document, TextSpan selectionRaw, bool allowEmptyNodes, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + using var _2 = ArrayBuilder.GetInstance(out var nonEmptyNodes); + foreach (var node in relevantNodesBuilder) { - using var _1 = ArrayBuilder.GetInstance(out var relevantNodesBuilder); - await AddRelevantNodesAsync(document, selectionRaw, relevantNodesBuilder, cancellationToken).ConfigureAwait(false); + if (node.Span.Length > 0) + nonEmptyNodes.Add(node); + } - if (allowEmptyNodes) - return relevantNodesBuilder.ToImmutable(); + nonEmptyNodes.RemoveDuplicates(); + return nonEmptyNodes.ToImmutableAndClear(); + } + + private async Task AddRelevantNodesAsync( + Document document, TextSpan selectionRaw, ArrayBuilder relevantNodes, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + // Given selection is trimmed first to enable over-selection that spans multiple lines. Since trailing whitespace ends + // at newline boundary over-selection to e.g. a line after LocalFunctionStatement would cause FindNode to find enclosing + // block's Node. That is because in addition to LocalFunctionStatement the selection would also contain trailing trivia + // (whitespace) of following statement. + + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var syntaxFacts = document.GetRequiredLanguageService(); + var headerFacts = document.GetRequiredLanguageService(); + var selectionTrimmed = await CodeRefactoringHelpers.GetTrimmedTextSpanAsync(document, selectionRaw, cancellationToken).ConfigureAwait(false); + + // If user selected only whitespace we don't want to return anything. We could do following: + // 1) Consider token that owns (as its trivia) the whitespace. + // 2) Consider start/beginning of whitespace as location (empty selection) + // Option 1) can't be used all the time and 2) can be confusing for users. Therefore bailing out is the + // most consistent option. + if (selectionTrimmed.IsEmpty && !selectionRaw.IsEmpty) + return; + + // Every time a Node is considered an extractNodes method is called to add all nodes around the original one + // that should also be considered. + // + // That enables us to e.g. return node `b` when Node `var a = b;` is being considered without a complex (and potentially + // lang. & situation dependent) into Children descending code here. We can't just try extracted Node because we might + // want the whole node `var a = b;` + + // Handle selections: + // - Most/the whole wanted Node is selected (e.g. `C [|Fun() {}|]` + // - The smallest node whose FullSpan includes the whole (trimmed) selection + // - Using FullSpan is important because it handles over-selection with comments + // - Travels upwards through same-sized (FullSpan) nodes, extracting + // - Token with wanted Node as direct parent is selected (e.g. IdentifierToken for LocalFunctionStatement: `C [|Fun|]() {}`) + // Note: Whether we have selection or location has to be checked against original selection because selecting just + // whitespace could collapse selectionTrimmed into and empty Location. But we don't want `[| |]token` + // registering as ` [||]token`. + if (!selectionTrimmed.IsEmpty) + { + AddRelevantNodesForSelection(syntaxFacts, root, selectionTrimmed, relevantNodes, cancellationToken); + } + else + { + var location = selectionTrimmed.Start; - using var _2 = ArrayBuilder.GetInstance(out var nonEmptyNodes); - foreach (var node in relevantNodesBuilder) + // No more selection -> Handle what current selection is touching: + // + // Consider touching only for empty selections. Otherwise `[|C|] methodName(){}` would be considered as + // touching the Method's Node (through the left edge, see below) which is something the user probably + // didn't want since they specifically selected only the return type. + // + // What the selection is touching is used in two ways. + // - Firstly, it is used to handle situation where it touches a Token whose direct ancestor is wanted + // Node. While having the (even empty) selection inside such token or to left of such Token is already + // handle by code above touching it from right `C methodName[||](){}` isn't (the FindNode for that + // returns Args node). + // + // - Secondly, it is used for left/right edge climbing. E.g. `[||]C methodName(){}` the touching token's + // direct ancestor is TypeNode for the return type but it is still reasonable to expect that the user + // might want to be given refactorings for the whole method (as he has caret on the edge of it). + // Therefore we travel the Node tree upwards and as long as we're on the left edge of a Node's span we + // consider such node & potentially continue traveling upwards. The situation for right edge (`C + // methodName(){}[||]`) is analogical. E.g. for right edge `C methodName(){}[||]`: CloseBraceToken -> + // BlockSyntax -> LocalFunctionStatement -> null (higher node doesn't end on position anymore) Note: + // left-edge climbing needs to handle AttributeLists explicitly, see below for more information. + // + // - Thirdly, if location isn't touching anything, we move the location to the token in whose trivia + // location is in. more about that below. + // + // - Fourthly, if we're in an expression / argument we consider touching a parent expression whenever + // we're within it as long as it is on the first line of such expression (arbitrary heuristic). + + // In addition to per-node extr also check if current location (if selection is empty) is in a header of + // higher level desired node once. We do that only for locations because otherwise `[|int|] A { get; + // set; }) would trigger all refactorings for Property Decl. We cannot check this any sooner because the + // above code could've changed current location. + AddNonHiddenCorrectTypeNodes(ExtractNodesInHeader(root, location, headerFacts), relevantNodes, cancellationToken); + + var (tokenToLeft, tokenToRight) = await GetTokensToLeftAndRightAsync( + document, root, location, cancellationToken).ConfigureAwait(false); + + // Add Nodes for touching tokens as described above. + AddNodesForTokenToRight(syntaxFacts, root, relevantNodes, tokenToRight, cancellationToken); + AddNodesForTokenToLeft(syntaxFacts, relevantNodes, tokenToLeft, cancellationToken); + + // If the wanted node is an expression syntax -> traverse upwards even if location is deep within a SyntaxNode. + // We want to treat more types like expressions, e.g.: ArgumentSyntax should still trigger even if deep-in. + if (IsWantedTypeExpressionLike()) { - if (node.Span.Length > 0) - nonEmptyNodes.Add(node); + // Reason to treat Arguments (and potentially others) as Expression-like: + // https://github.com/dotnet/roslyn/pull/37295#issuecomment-516145904 + await AddNodesDeepInAsync(document, location, relevantNodes, cancellationToken).ConfigureAwait(false); } + } + } + + private static bool IsWantedTypeExpressionLike() where TSyntaxNode : SyntaxNode + { + var wantedType = typeof(TSyntaxNode); + + var expressionType = typeof(TExpressionSyntax); + var argumentType = typeof(TArgumentSyntax); + var expressionStatementType = typeof(TExpressionStatementSyntax); - nonEmptyNodes.RemoveDuplicates(); - return nonEmptyNodes.ToImmutableAndClear(); + return IsAEqualOrSubclassOfB(wantedType, expressionType) || + IsAEqualOrSubclassOfB(wantedType, argumentType) || + IsAEqualOrSubclassOfB(wantedType, expressionStatementType); + + static bool IsAEqualOrSubclassOfB(Type a, Type b) + { + return a == b || a.IsSubclassOf(b); } + } + + private static async Task<(SyntaxToken tokenToLeft, SyntaxToken tokenToRight)> GetTokensToLeftAndRightAsync( + Document document, + SyntaxNode root, + int location, + CancellationToken cancellationToken) + { + // get Token for current location + var tokenOnLocation = root.FindToken(location); - private async Task AddRelevantNodesAsync( - Document document, TextSpan selectionRaw, ArrayBuilder relevantNodes, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + var syntaxKinds = document.GetRequiredLanguageService(); + if (tokenOnLocation.RawKind == syntaxKinds.CommaToken && location >= tokenOnLocation.Span.End) { - // Given selection is trimmed first to enable over-selection that spans multiple lines. Since trailing whitespace ends - // at newline boundary over-selection to e.g. a line after LocalFunctionStatement would cause FindNode to find enclosing - // block's Node. That is because in addition to LocalFunctionStatement the selection would also contain trailing trivia - // (whitespace) of following statement. - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - var syntaxFacts = document.GetRequiredLanguageService(); - var headerFacts = document.GetRequiredLanguageService(); - var selectionTrimmed = await CodeRefactoringHelpers.GetTrimmedTextSpanAsync(document, selectionRaw, cancellationToken).ConfigureAwait(false); - - // If user selected only whitespace we don't want to return anything. We could do following: - // 1) Consider token that owns (as its trivia) the whitespace. - // 2) Consider start/beginning of whitespace as location (empty selection) - // Option 1) can't be used all the time and 2) can be confusing for users. Therefore bailing out is the - // most consistent option. - if (selectionTrimmed.IsEmpty && !selectionRaw.IsEmpty) - return; - - // Every time a Node is considered an extractNodes method is called to add all nodes around the original one - // that should also be considered. + var commaToken = tokenOnLocation; + + // A couple of scenarios to care about: + // + // X,$$ Y + // + // In this case, consider the user on the Y node. + // + // X,$$ + // Y // - // That enables us to e.g. return node `b` when Node `var a = b;` is being considered without a complex (and potentially - // lang. & situation dependent) into Children descending code here. We can't just try extracted Node because we might - // want the whole node `var a = b;` - - // Handle selections: - // - Most/the whole wanted Node is selected (e.g. `C [|Fun() {}|]` - // - The smallest node whose FullSpan includes the whole (trimmed) selection - // - Using FullSpan is important because it handles over-selection with comments - // - Travels upwards through same-sized (FullSpan) nodes, extracting - // - Token with wanted Node as direct parent is selected (e.g. IdentifierToken for LocalFunctionStatement: `C [|Fun|]() {}`) - // Note: Whether we have selection or location has to be checked against original selection because selecting just - // whitespace could collapse selectionTrimmed into and empty Location. But we don't want `[| |]token` - // registering as ` [||]token`. - if (!selectionTrimmed.IsEmpty) + // In this case, consider the user on the X node. + var nextToken = commaToken.GetNextToken(); + var previousToken = commaToken.GetPreviousToken(); + if (nextToken != default && !commaToken.TrailingTrivia.Any(t => t.RawKind == syntaxKinds.EndOfLineTrivia)) { - AddRelevantNodesForSelection(syntaxFacts, root, selectionTrimmed, relevantNodes, cancellationToken); + return (tokenToLeft: default, tokenToRight: nextToken); } - else + else if (previousToken != default && previousToken.Span.End == commaToken.Span.Start) { - var location = selectionTrimmed.Start; - - // No more selection -> Handle what current selection is touching: - // - // Consider touching only for empty selections. Otherwise `[|C|] methodName(){}` would be considered as - // touching the Method's Node (through the left edge, see below) which is something the user probably - // didn't want since they specifically selected only the return type. - // - // What the selection is touching is used in two ways. - // - Firstly, it is used to handle situation where it touches a Token whose direct ancestor is wanted - // Node. While having the (even empty) selection inside such token or to left of such Token is already - // handle by code above touching it from right `C methodName[||](){}` isn't (the FindNode for that - // returns Args node). - // - // - Secondly, it is used for left/right edge climbing. E.g. `[||]C methodName(){}` the touching token's - // direct ancestor is TypeNode for the return type but it is still reasonable to expect that the user - // might want to be given refactorings for the whole method (as he has caret on the edge of it). - // Therefore we travel the Node tree upwards and as long as we're on the left edge of a Node's span we - // consider such node & potentially continue traveling upwards. The situation for right edge (`C - // methodName(){}[||]`) is analogical. E.g. for right edge `C methodName(){}[||]`: CloseBraceToken -> - // BlockSyntax -> LocalFunctionStatement -> null (higher node doesn't end on position anymore) Note: - // left-edge climbing needs to handle AttributeLists explicitly, see below for more information. - // - // - Thirdly, if location isn't touching anything, we move the location to the token in whose trivia - // location is in. more about that below. - // - // - Fourthly, if we're in an expression / argument we consider touching a parent expression whenever - // we're within it as long as it is on the first line of such expression (arbitrary heuristic). - - // In addition to per-node extr also check if current location (if selection is empty) is in a header of - // higher level desired node once. We do that only for locations because otherwise `[|int|] A { get; - // set; }) would trigger all refactorings for Property Decl. We cannot check this any sooner because the - // above code could've changed current location. - AddNonHiddenCorrectTypeNodes(ExtractNodesInHeader(root, location, headerFacts), relevantNodes, cancellationToken); - - var (tokenToLeft, tokenToRight) = await GetTokensToLeftAndRightAsync( - document, root, location, cancellationToken).ConfigureAwait(false); - - // Add Nodes for touching tokens as described above. - AddNodesForTokenToRight(syntaxFacts, root, relevantNodes, tokenToRight, cancellationToken); - AddNodesForTokenToLeft(syntaxFacts, relevantNodes, tokenToLeft, cancellationToken); - - // If the wanted node is an expression syntax -> traverse upwards even if location is deep within a SyntaxNode. - // We want to treat more types like expressions, e.g.: ArgumentSyntax should still trigger even if deep-in. - if (IsWantedTypeExpressionLike()) - { - // Reason to treat Arguments (and potentially others) as Expression-like: - // https://github.com/dotnet/roslyn/pull/37295#issuecomment-516145904 - await AddNodesDeepInAsync(document, location, relevantNodes, cancellationToken).ConfigureAwait(false); - } + return (tokenToLeft: previousToken, tokenToRight: default); } } - private static bool IsWantedTypeExpressionLike() where TSyntaxNode : SyntaxNode - { - var wantedType = typeof(TSyntaxNode); + // Gets a token that is directly to the right of current location or that encompasses current location (`[||]tokenToRightOrIn` or `tok[||]enToRightOrIn`) + var tokenToRight = tokenOnLocation.Span.Contains(location) + ? tokenOnLocation + : default; - var expressionType = typeof(TExpressionSyntax); - var argumentType = typeof(TArgumentSyntax); - var expressionStatementType = typeof(TExpressionStatementSyntax); - - return IsAEqualOrSubclassOfB(wantedType, expressionType) || - IsAEqualOrSubclassOfB(wantedType, argumentType) || - IsAEqualOrSubclassOfB(wantedType, expressionStatementType); + // A token can be to the left only when there's either no tokenDirectlyToRightOrIn or there's one directly starting at current location. + // Otherwise (otherwise tokenToRightOrIn is also left from location, e.g: `tok[||]enToRightOrIn`) + var tokenToLeft = default(SyntaxToken); + if (tokenToRight == default || tokenToRight.FullSpan.Start == location) + { + var previousToken = tokenOnLocation.Span.End == location + ? tokenOnLocation + : tokenOnLocation.GetPreviousToken(includeZeroWidth: true); - static bool IsAEqualOrSubclassOfB(Type a, Type b) - { - return a == b || a.IsSubclassOf(b); - } + tokenToLeft = previousToken.Span.End == location + ? previousToken + : default; } - private static async Task<(SyntaxToken tokenToLeft, SyntaxToken tokenToRight)> GetTokensToLeftAndRightAsync( - Document document, - SyntaxNode root, - int location, - CancellationToken cancellationToken) + // If both tokens directly to left & right are empty -> we're somewhere in the middle of whitespace. + // Since there wouldn't be (m)any other refactorings we can try to offer at least the ones for (semantically) + // closest token/Node. Thus, we move the location to the token in whose `.FullSpan` the original location was. + if (tokenToLeft == default && tokenToRight == default) { - // get Token for current location - var tokenOnLocation = root.FindToken(location); + var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var syntaxKinds = document.GetRequiredLanguageService(); - if (tokenOnLocation.RawKind == syntaxKinds.CommaToken && location >= tokenOnLocation.Span.End) + if (IsAcceptableLineDistanceAway(sourceText, tokenOnLocation, location)) { - var commaToken = tokenOnLocation; - - // A couple of scenarios to care about: - // - // X,$$ Y - // - // In this case, consider the user on the Y node. - // - // X,$$ - // Y - // - // In this case, consider the user on the X node. - var nextToken = commaToken.GetNextToken(); - var previousToken = commaToken.GetPreviousToken(); - if (nextToken != default && !commaToken.TrailingTrivia.Any(t => t.RawKind == syntaxKinds.EndOfLineTrivia)) + // tokenOnLocation: token in whose trivia location is at + if (tokenOnLocation.Span.Start >= location) { - return (tokenToLeft: default, tokenToRight: nextToken); + tokenToRight = tokenOnLocation; } - else if (previousToken != default && previousToken.Span.End == commaToken.Span.Start) + else { - return (tokenToLeft: previousToken, tokenToRight: default); + tokenToLeft = tokenOnLocation; } } + } - // Gets a token that is directly to the right of current location or that encompasses current location (`[||]tokenToRightOrIn` or `tok[||]enToRightOrIn`) - var tokenToRight = tokenOnLocation.Span.Contains(location) - ? tokenOnLocation - : default; - - // A token can be to the left only when there's either no tokenDirectlyToRightOrIn or there's one directly starting at current location. - // Otherwise (otherwise tokenToRightOrIn is also left from location, e.g: `tok[||]enToRightOrIn`) - var tokenToLeft = default(SyntaxToken); - if (tokenToRight == default || tokenToRight.FullSpan.Start == location) - { - var previousToken = tokenOnLocation.Span.End == location - ? tokenOnLocation - : tokenOnLocation.GetPreviousToken(includeZeroWidth: true); - - tokenToLeft = previousToken.Span.End == location - ? previousToken - : default; - } + return (tokenToLeft, tokenToRight); - // If both tokens directly to left & right are empty -> we're somewhere in the middle of whitespace. - // Since there wouldn't be (m)any other refactorings we can try to offer at least the ones for (semantically) - // closest token/Node. Thus, we move the location to the token in whose `.FullSpan` the original location was. - if (tokenToLeft == default && tokenToRight == default) - { - var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + static bool IsAcceptableLineDistanceAway( + SourceText sourceText, SyntaxToken tokenOnLocation, int location) + { + // assume non-trivia token can't span multiple lines + var tokenLine = sourceText.Lines.GetLineFromPosition(tokenOnLocation.Span.Start); + var locationLine = sourceText.Lines.GetLineFromPosition(location); - if (IsAcceptableLineDistanceAway(sourceText, tokenOnLocation, location)) - { - // tokenOnLocation: token in whose trivia location is at - if (tokenOnLocation.Span.Start >= location) - { - tokenToRight = tokenOnLocation; - } - else - { - tokenToLeft = tokenOnLocation; - } - } - } + // Change location to nearest token only if the token is off by one line or less + var lineDistance = tokenLine.LineNumber - locationLine.LineNumber; + if (lineDistance is not 0 and not 1) + return false; - return (tokenToLeft, tokenToRight); + // Note: being a line below a tokenOnLocation is impossible in current model as whitespace + // trailing trivia ends on new line. Which is fine because if you're a line _after_ some node + // you usually don't want refactorings for what's above you. - static bool IsAcceptableLineDistanceAway( - SourceText sourceText, SyntaxToken tokenOnLocation, int location) + if (lineDistance == 1) { - // assume non-trivia token can't span multiple lines - var tokenLine = sourceText.Lines.GetLineFromPosition(tokenOnLocation.Span.Start); - var locationLine = sourceText.Lines.GetLineFromPosition(location); - - // Change location to nearest token only if the token is off by one line or less - var lineDistance = tokenLine.LineNumber - locationLine.LineNumber; - if (lineDistance is not 0 and not 1) - return false; - - // Note: being a line below a tokenOnLocation is impossible in current model as whitespace - // trailing trivia ends on new line. Which is fine because if you're a line _after_ some node - // you usually don't want refactorings for what's above you. - - if (lineDistance == 1) - { - // position is one line above the node of interest. This is fine if that - // line is blank. Otherwise, if it isn't (i.e. it contains comments, - // directives, or other trivia), then it's not likely the user is selecting - // this entry. - return locationLine.IsEmptyOrWhitespace(); - } - - // On hte same line. This position is acceptable. - return true; + // position is one line above the node of interest. This is fine if that + // line is blank. Otherwise, if it isn't (i.e. it contains comments, + // directives, or other trivia), then it's not likely the user is selecting + // this entry. + return locationLine.IsEmptyOrWhitespace(); } + + // On hte same line. This position is acceptable. + return true; } + } - private void AddNodesForTokenToLeft( - ISyntaxFactsService syntaxFacts, - ArrayBuilder relevantNodesBuilder, - SyntaxToken tokenToLeft, - CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - var location = tokenToLeft.Span.End; + private void AddNodesForTokenToLeft( + ISyntaxFactsService syntaxFacts, + ArrayBuilder relevantNodesBuilder, + SyntaxToken tokenToLeft, + CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + var location = tokenToLeft.Span.End; - // there could be multiple (n) tokens to the left if first n-1 are Empty -> iterate over all of them - while (tokenToLeft != default) + // there could be multiple (n) tokens to the left if first n-1 are Empty -> iterate over all of them + while (tokenToLeft != default) + { + var leftNode = tokenToLeft.Parent; + do { - var leftNode = tokenToLeft.Parent; - do - { - // Consider either a Node that is: - // - Ancestor Node of such Token as long as their span ends on location (it's still on the edge) - AddNonHiddenCorrectTypeNodes(ExtractNodesSimple(leftNode, syntaxFacts), relevantNodesBuilder, cancellationToken); + // Consider either a Node that is: + // - Ancestor Node of such Token as long as their span ends on location (it's still on the edge) + AddNonHiddenCorrectTypeNodes(ExtractNodesSimple(leftNode, syntaxFacts), relevantNodesBuilder, cancellationToken); - leftNode = leftNode?.Parent; - if (leftNode is null) - break; + leftNode = leftNode?.Parent; + if (leftNode is null) + break; - if (leftNode.GetLastToken().Span.End != location && leftNode.Span.End != location) - break; - } - while (true); - - // as long as current tokenToLeft is empty -> its previous token is also tokenToLeft - tokenToLeft = tokenToLeft.Span.IsEmpty - ? tokenToLeft.GetPreviousToken(includeZeroWidth: true) - : default; + if (leftNode.GetLastToken().Span.End != location && leftNode.Span.End != location) + break; } + while (true); + + // as long as current tokenToLeft is empty -> its previous token is also tokenToLeft + tokenToLeft = tokenToLeft.Span.IsEmpty + ? tokenToLeft.GetPreviousToken(includeZeroWidth: true) + : default; } + } - private void AddNodesForTokenToRight( - ISyntaxFactsService syntaxFacts, - SyntaxNode root, - ArrayBuilder relevantNodesBuilder, - SyntaxToken tokenToRightOrIn, - CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - var location = tokenToRightOrIn.Span.Start; + private void AddNodesForTokenToRight( + ISyntaxFactsService syntaxFacts, + SyntaxNode root, + ArrayBuilder relevantNodesBuilder, + SyntaxToken tokenToRightOrIn, + CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + var location = tokenToRightOrIn.Span.Start; - if (tokenToRightOrIn != default) + if (tokenToRightOrIn != default) + { + var rightNode = tokenToRightOrIn.Parent; + do { - var rightNode = tokenToRightOrIn.Parent; - do + // Consider either a Node that is: + // - Parent of touched Token (location can be within) + // - Ancestor Node of such Token as long as their span starts on location (it's still on the edge) + AddNonHiddenCorrectTypeNodes(ExtractNodesSimple(rightNode, syntaxFacts), relevantNodesBuilder, cancellationToken); + + rightNode = rightNode?.Parent; + if (rightNode == null) + break; + + // The edge climbing for node to the right needs to handle Attributes e.g.: + // [Test1] + // //Comment1 + // [||]object Property1 { get; set; } + // In essence: + // - On the left edge of the node (-> left edge of first AttributeLists) + // - On the left edge of the node sans AttributeLists (& as everywhere comments) + if (rightNode.Span.Start != location) { - // Consider either a Node that is: - // - Parent of touched Token (location can be within) - // - Ancestor Node of such Token as long as their span starts on location (it's still on the edge) - AddNonHiddenCorrectTypeNodes(ExtractNodesSimple(rightNode, syntaxFacts), relevantNodesBuilder, cancellationToken); - - rightNode = rightNode?.Parent; - if (rightNode == null) + var rightNodeSpanWithoutAttributes = syntaxFacts.GetSpanWithoutAttributes(root, rightNode); + if (rightNodeSpanWithoutAttributes.Start != location) break; - - // The edge climbing for node to the right needs to handle Attributes e.g.: - // [Test1] - // //Comment1 - // [||]object Property1 { get; set; } - // In essence: - // - On the left edge of the node (-> left edge of first AttributeLists) - // - On the left edge of the node sans AttributeLists (& as everywhere comments) - if (rightNode.Span.Start != location) - { - var rightNodeSpanWithoutAttributes = syntaxFacts.GetSpanWithoutAttributes(root, rightNode); - if (rightNodeSpanWithoutAttributes.Start != location) - break; - } } - while (true); } + while (true); } + } - private void AddRelevantNodesForSelection( - ISyntaxFactsService syntaxFacts, - SyntaxNode root, - TextSpan selectionTrimmed, - ArrayBuilder relevantNodesBuilder, - CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + private void AddRelevantNodesForSelection( + ISyntaxFactsService syntaxFacts, + SyntaxNode root, + TextSpan selectionTrimmed, + ArrayBuilder relevantNodesBuilder, + CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + var selectionNode = root.FindNode(selectionTrimmed, getInnermostNodeForTie: true); + var prevNode = selectionNode; + do { - var selectionNode = root.FindNode(selectionTrimmed, getInnermostNodeForTie: true); - var prevNode = selectionNode; - do + var nonHiddenExtractedSelectedNodes = ExtractNodesSimple(selectionNode, syntaxFacts).OfType().Where(n => !n.OverlapsHiddenPosition(cancellationToken)); + foreach (var nonHiddenExtractedNode in nonHiddenExtractedSelectedNodes) { - var nonHiddenExtractedSelectedNodes = ExtractNodesSimple(selectionNode, syntaxFacts).OfType().Where(n => !n.OverlapsHiddenPosition(cancellationToken)); - foreach (var nonHiddenExtractedNode in nonHiddenExtractedSelectedNodes) - { - // For selections we need to handle an edge case where only AttributeLists are within selection (e.g. `Func([|[in][out]|] arg1);`). - // In that case the smallest encompassing node is still the whole argument node but it's hard to justify showing refactorings for it - // if user selected only its attributes. + // For selections we need to handle an edge case where only AttributeLists are within selection (e.g. `Func([|[in][out]|] arg1);`). + // In that case the smallest encompassing node is still the whole argument node but it's hard to justify showing refactorings for it + // if user selected only its attributes. - // Selection contains only AttributeLists -> don't consider current Node - var spanWithoutAttributes = syntaxFacts.GetSpanWithoutAttributes(root, nonHiddenExtractedNode); - if (!selectionTrimmed.IntersectsWith(spanWithoutAttributes)) - { - break; - } - - relevantNodesBuilder.Add(nonHiddenExtractedNode); + // Selection contains only AttributeLists -> don't consider current Node + var spanWithoutAttributes = syntaxFacts.GetSpanWithoutAttributes(root, nonHiddenExtractedNode); + if (!selectionTrimmed.IntersectsWith(spanWithoutAttributes)) + { + break; } - prevNode = selectionNode; - selectionNode = selectionNode.Parent; + relevantNodesBuilder.Add(nonHiddenExtractedNode); } - while (selectionNode != null && prevNode.FullWidth() == selectionNode.FullWidth()); + + prevNode = selectionNode; + selectionNode = selectionNode.Parent; } + while (selectionNode != null && prevNode.FullWidth() == selectionNode.FullWidth()); + } - /// - /// Extractor function that retrieves all nodes that should be considered for extraction of given current node. - /// - /// The rationale is that when user selects e.g. entire local declaration statement [|var a = b;|] it is reasonable - /// to provide refactoring for `b` node. Similarly for other types of refactorings. - /// - /// - /// - /// Should also return given node. - /// - protected virtual IEnumerable ExtractNodesSimple(SyntaxNode? node, ISyntaxFactsService syntaxFacts) + /// + /// Extractor function that retrieves all nodes that should be considered for extraction of given current node. + /// + /// The rationale is that when user selects e.g. entire local declaration statement [|var a = b;|] it is reasonable + /// to provide refactoring for `b` node. Similarly for other types of refactorings. + /// + /// + /// + /// Should also return given node. + /// + protected virtual IEnumerable ExtractNodesSimple(SyntaxNode? node, ISyntaxFactsService syntaxFacts) + { + if (node == null) { - if (node == null) - { - yield break; - } + yield break; + } - // First return the node itself so that it is considered - yield return node; + // First return the node itself so that it is considered + yield return node; - // REMARKS: - // The set of currently attempted extractions is in no way exhaustive and covers only cases - // that were found to be relevant for refactorings that were moved to `TryGetSelectedNodeAsync`. - // Feel free to extend it / refine current heuristics. + // REMARKS: + // The set of currently attempted extractions is in no way exhaustive and covers only cases + // that were found to be relevant for refactorings that were moved to `TryGetSelectedNodeAsync`. + // Feel free to extend it / refine current heuristics. - // `var a = b;` | `var a = b`; - if (syntaxFacts.IsLocalDeclarationStatement(node) || syntaxFacts.IsLocalDeclarationStatement(node.Parent)) + // `var a = b;` | `var a = b`; + if (syntaxFacts.IsLocalDeclarationStatement(node) || syntaxFacts.IsLocalDeclarationStatement(node.Parent)) + { + var localDeclarationStatement = syntaxFacts.IsLocalDeclarationStatement(node) ? node : node.Parent!; + + // Check if there's only one variable being declared, otherwise following transformation + // would go through which isn't reasonable since we can't say the first one specifically + // is wanted. + // `var a = 1, `c = 2, d = 3`; + // -> `var a = 1`, c = 2, d = 3; + var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); + if (variables.Count == 1) { - var localDeclarationStatement = syntaxFacts.IsLocalDeclarationStatement(node) ? node : node.Parent!; - - // Check if there's only one variable being declared, otherwise following transformation - // would go through which isn't reasonable since we can't say the first one specifically - // is wanted. - // `var a = 1, `c = 2, d = 3`; - // -> `var a = 1`, c = 2, d = 3; - var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); - if (variables.Count == 1) - { - var declaredVariable = variables.First(); - - // -> `a = b` - yield return declaredVariable; + var declaredVariable = variables.First(); - // -> `b` - var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(declaredVariable); - if (initializer != null) - { - var value = syntaxFacts.GetValueOfEqualsValueClause(initializer); - if (value != null) - { - yield return value; - } - } - } - } + // -> `a = b` + yield return declaredVariable; - // var `a = b`; - if (syntaxFacts.IsVariableDeclarator(node)) - { // -> `b` - var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(node); + var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(declaredVariable); if (initializer != null) { var value = syntaxFacts.GetValueOfEqualsValueClause(initializer); @@ -447,142 +431,157 @@ protected virtual IEnumerable ExtractNodesSimple(SyntaxNode? node, I } } } + } - // `a = b;` + // var `a = b`; + if (syntaxFacts.IsVariableDeclarator(node)) + { // -> `b` - if (syntaxFacts.IsSimpleAssignmentStatement(node)) + var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(node); + if (initializer != null) { - syntaxFacts.GetPartsOfAssignmentExpressionOrStatement(node, out _, out _, out var rightSide); - yield return rightSide; + var value = syntaxFacts.GetValueOfEqualsValueClause(initializer); + if (value != null) + { + yield return value; + } } + } - // `a();` - // -> a() - if (syntaxFacts.IsExpressionStatement(node)) - { - yield return syntaxFacts.GetExpressionOfExpressionStatement(node); - } + // `a = b;` + // -> `b` + if (syntaxFacts.IsSimpleAssignmentStatement(node)) + { + syntaxFacts.GetPartsOfAssignmentExpressionOrStatement(node, out _, out _, out var rightSide); + yield return rightSide; + } - // `a()`; - // -> `a();` - if (syntaxFacts.IsExpressionStatement(node.Parent)) - { - yield return node.Parent; - } + // `a();` + // -> a() + if (syntaxFacts.IsExpressionStatement(node)) + { + yield return syntaxFacts.GetExpressionOfExpressionStatement(node); } - /// - /// Extractor function that checks and retrieves all nodes current location is in a header. - /// - protected virtual IEnumerable ExtractNodesInHeader(SyntaxNode root, int location, IHeaderFactsService headerFacts) + // `a()`; + // -> `a();` + if (syntaxFacts.IsExpressionStatement(node.Parent)) { - // Header: [Test] `public int a` { get; set; } - if (headerFacts.IsOnPropertyDeclarationHeader(root, location, out var propertyDeclaration)) - yield return propertyDeclaration; + yield return node.Parent; + } + } - // Header: public C([Test]`int a = 42`) {} - if (headerFacts.IsOnParameterHeader(root, location, out var parameter)) - yield return parameter; + /// + /// Extractor function that checks and retrieves all nodes current location is in a header. + /// + protected virtual IEnumerable ExtractNodesInHeader(SyntaxNode root, int location, IHeaderFactsService headerFacts) + { + // Header: [Test] `public int a` { get; set; } + if (headerFacts.IsOnPropertyDeclarationHeader(root, location, out var propertyDeclaration)) + yield return propertyDeclaration; - // Header: `public I.C([Test]int a = 42)` {} - if (headerFacts.IsOnMethodHeader(root, location, out var method)) - yield return method; + // Header: public C([Test]`int a = 42`) {} + if (headerFacts.IsOnParameterHeader(root, location, out var parameter)) + yield return parameter; - // Header: `static C([Test]int a = 42)` {} - if (headerFacts.IsOnLocalFunctionHeader(root, location, out var localFunction)) - yield return localFunction; + // Header: `public I.C([Test]int a = 42)` {} + if (headerFacts.IsOnMethodHeader(root, location, out var method)) + yield return method; - // Header: `var a = `3,` b = `5,` c = `7 + 3``; - if (headerFacts.IsOnLocalDeclarationHeader(root, location, out var localDeclaration)) - yield return localDeclaration; + // Header: `static C([Test]int a = 42)` {} + if (headerFacts.IsOnLocalFunctionHeader(root, location, out var localFunction)) + yield return localFunction; - // Header: `if(...)`{ }; - if (headerFacts.IsOnIfStatementHeader(root, location, out var ifStatement)) - yield return ifStatement; + // Header: `var a = `3,` b = `5,` c = `7 + 3``; + if (headerFacts.IsOnLocalDeclarationHeader(root, location, out var localDeclaration)) + yield return localDeclaration; - // Header: `foreach (var a in b)` { } - if (headerFacts.IsOnForeachHeader(root, location, out var foreachStatement)) - yield return foreachStatement; + // Header: `if(...)`{ }; + if (headerFacts.IsOnIfStatementHeader(root, location, out var ifStatement)) + yield return ifStatement; - if (headerFacts.IsOnTypeHeader(root, location, out var typeDeclaration)) - yield return typeDeclaration; - } + // Header: `foreach (var a in b)` { } + if (headerFacts.IsOnForeachHeader(root, location, out var foreachStatement)) + yield return foreachStatement; + + if (headerFacts.IsOnTypeHeader(root, location, out var typeDeclaration)) + yield return typeDeclaration; + } - protected virtual async Task AddNodesDeepInAsync( - Document document, - int position, - ArrayBuilder relevantNodesBuilder, - CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + protected virtual async Task AddNodesDeepInAsync( + Document document, + int position, + ArrayBuilder relevantNodesBuilder, + CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + // If we're deep inside we don't have to deal with being on edges (that gets dealt by TryGetSelectedNodeAsync) + // -> can simply FindToken -> proceed testing its ancestors + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) { - // If we're deep inside we don't have to deal with being on edges (that gets dealt by TryGetSelectedNodeAsync) - // -> can simply FindToken -> proceed testing its ancestors - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (root is null) - { - throw new NotSupportedException(WorkspacesResources.Document_does_not_support_syntax_trees); - } + throw new NotSupportedException(WorkspacesResources.Document_does_not_support_syntax_trees); + } - var token = root.FindTokenOnRightOfPosition(position, true); + var token = root.FindTokenOnRightOfPosition(position, true); - // traverse upwards and add all parents if of correct type - var ancestor = token.Parent; - while (ancestor != null) + // traverse upwards and add all parents if of correct type + var ancestor = token.Parent; + while (ancestor != null) + { + if (ancestor is TSyntaxNode correctTypeNode) { - if (ancestor is TSyntaxNode correctTypeNode) - { - var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var argumentStartLine = sourceText.Lines.GetLineFromPosition(correctTypeNode.Span.Start).LineNumber; - var caretLine = sourceText.Lines.GetLineFromPosition(position).LineNumber; + var argumentStartLine = sourceText.Lines.GetLineFromPosition(correctTypeNode.Span.Start).LineNumber; + var caretLine = sourceText.Lines.GetLineFromPosition(position).LineNumber; - if (argumentStartLine == caretLine && !correctTypeNode.OverlapsHiddenPosition(cancellationToken)) - { - relevantNodesBuilder.Add(correctTypeNode); - } - else if (argumentStartLine < caretLine) - { - // higher level nodes will have Span starting at least on the same line -> can bail out - return; - } + if (argumentStartLine == caretLine && !correctTypeNode.OverlapsHiddenPosition(cancellationToken)) + { + relevantNodesBuilder.Add(correctTypeNode); + } + else if (argumentStartLine < caretLine) + { + // higher level nodes will have Span starting at least on the same line -> can bail out + return; } - - ancestor = ancestor.Parent; } - } - private static void AddNonHiddenCorrectTypeNodes( - IEnumerable nodes, ArrayBuilder resultBuilder, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - var correctTypeNonHiddenNodes = nodes.OfType().Where(n => !n.OverlapsHiddenPosition(cancellationToken)); - foreach (var nodeToBeAdded in correctTypeNonHiddenNodes) - resultBuilder.Add(nodeToBeAdded); + ancestor = ancestor.Parent; } + } - public bool IsOnTypeHeader(SyntaxNode root, int position, bool fullHeader, [NotNullWhen(true)] out SyntaxNode? typeDeclaration) - => HeaderFacts.IsOnTypeHeader(root, position, fullHeader, out typeDeclaration); + private static void AddNonHiddenCorrectTypeNodes( + IEnumerable nodes, ArrayBuilder resultBuilder, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + var correctTypeNonHiddenNodes = nodes.OfType().Where(n => !n.OverlapsHiddenPosition(cancellationToken)); + foreach (var nodeToBeAdded in correctTypeNonHiddenNodes) + resultBuilder.Add(nodeToBeAdded); + } - public bool IsOnPropertyDeclarationHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? propertyDeclaration) - => HeaderFacts.IsOnPropertyDeclarationHeader(root, position, out propertyDeclaration); + public bool IsOnTypeHeader(SyntaxNode root, int position, bool fullHeader, [NotNullWhen(true)] out SyntaxNode? typeDeclaration) + => HeaderFacts.IsOnTypeHeader(root, position, fullHeader, out typeDeclaration); - public bool IsOnParameterHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? parameter) - => HeaderFacts.IsOnParameterHeader(root, position, out parameter); + public bool IsOnPropertyDeclarationHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? propertyDeclaration) + => HeaderFacts.IsOnPropertyDeclarationHeader(root, position, out propertyDeclaration); - public bool IsOnMethodHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? method) - => HeaderFacts.IsOnMethodHeader(root, position, out method); + public bool IsOnParameterHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? parameter) + => HeaderFacts.IsOnParameterHeader(root, position, out parameter); - public bool IsOnLocalFunctionHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? localFunction) - => HeaderFacts.IsOnLocalFunctionHeader(root, position, out localFunction); + public bool IsOnMethodHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? method) + => HeaderFacts.IsOnMethodHeader(root, position, out method); - public bool IsOnLocalDeclarationHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? localDeclaration) - => HeaderFacts.IsOnLocalDeclarationHeader(root, position, out localDeclaration); + public bool IsOnLocalFunctionHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? localFunction) + => HeaderFacts.IsOnLocalFunctionHeader(root, position, out localFunction); - public bool IsOnIfStatementHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? ifStatement) - => HeaderFacts.IsOnIfStatementHeader(root, position, out ifStatement); + public bool IsOnLocalDeclarationHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? localDeclaration) + => HeaderFacts.IsOnLocalDeclarationHeader(root, position, out localDeclaration); - public bool IsOnWhileStatementHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? whileStatement) - => HeaderFacts.IsOnWhileStatementHeader(root, position, out whileStatement); + public bool IsOnIfStatementHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? ifStatement) + => HeaderFacts.IsOnIfStatementHeader(root, position, out ifStatement); - public bool IsOnForeachHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? foreachStatement) - => HeaderFacts.IsOnForeachHeader(root, position, out foreachStatement); - } + public bool IsOnWhileStatementHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? whileStatement) + => HeaderFacts.IsOnWhileStatementHeader(root, position, out whileStatement); + + public bool IsOnForeachHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? foreachStatement) + => HeaderFacts.IsOnForeachHeader(root, position, out foreachStatement); } diff --git a/src/Features/Core/Portable/CodeRefactorings/AddAwait/AbstractAddAwaitCodeRefactoringProvider.cs b/src/Features/Core/Portable/CodeRefactorings/AddAwait/AbstractAddAwaitCodeRefactoringProvider.cs index 488fb6dd8edf4..67646683d47b5 100644 --- a/src/Features/Core/Portable/CodeRefactorings/AddAwait/AbstractAddAwaitCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/CodeRefactorings/AddAwait/AbstractAddAwaitCodeRefactoringProvider.cs @@ -10,108 +10,107 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CodeRefactorings.AddAwait +namespace Microsoft.CodeAnalysis.CodeRefactorings.AddAwait; + +/// +/// Refactor: +/// var x = GetAsync(); +/// +/// Into: +/// var x = await GetAsync(); +/// +/// Or: +/// var x = await GetAsync().ConfigureAwait(false); +/// +internal abstract class AbstractAddAwaitCodeRefactoringProvider : CodeRefactoringProvider + where TExpressionSyntax : SyntaxNode { - /// - /// Refactor: - /// var x = GetAsync(); - /// - /// Into: - /// var x = await GetAsync(); - /// - /// Or: - /// var x = await GetAsync().ConfigureAwait(false); - /// - internal abstract class AbstractAddAwaitCodeRefactoringProvider : CodeRefactoringProvider - where TExpressionSyntax : SyntaxNode - { - protected abstract string GetTitle(); - protected abstract string GetTitleWithConfigureAwait(); + protected abstract string GetTitle(); + protected abstract string GetTitleWithConfigureAwait(); - protected abstract bool IsInAsyncContext(SyntaxNode node); + protected abstract bool IsInAsyncContext(SyntaxNode node); - public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, span, cancellationToken) = context; + public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, span, cancellationToken) = context; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var node = root.FindNode(span); - if (!IsInAsyncContext(node)) - return; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var node = root.FindNode(span); + if (!IsInAsyncContext(node)) + return; - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); - var expressions = await context.GetRelevantNodesAsync().ConfigureAwait(false); - for (var i = expressions.Length - 1; i >= 0; i--) + var expressions = await context.GetRelevantNodesAsync().ConfigureAwait(false); + for (var i = expressions.Length - 1; i >= 0; i--) + { + var expression = expressions[i]; + if (IsValidAwaitableExpression(model, syntaxFacts, expression, cancellationToken)) { - var expression = expressions[i]; - if (IsValidAwaitableExpression(model, syntaxFacts, expression, cancellationToken)) - { - var title = GetTitle(); - context.RegisterRefactoring( - CodeAction.Create( - title, - c => AddAwaitAsync(document, expression, withConfigureAwait: false, c), - title), - expression.Span); - - var titleWithConfigureAwait = GetTitleWithConfigureAwait(); - context.RegisterRefactoring( - CodeAction.Create( - titleWithConfigureAwait, - c => AddAwaitAsync(document, expression, withConfigureAwait: true, c), - titleWithConfigureAwait), - expression.Span); - } + var title = GetTitle(); + context.RegisterRefactoring( + CodeAction.Create( + title, + c => AddAwaitAsync(document, expression, withConfigureAwait: false, c), + title), + expression.Span); + + var titleWithConfigureAwait = GetTitleWithConfigureAwait(); + context.RegisterRefactoring( + CodeAction.Create( + titleWithConfigureAwait, + c => AddAwaitAsync(document, expression, withConfigureAwait: true, c), + titleWithConfigureAwait), + expression.Span); } } + } - private static bool IsValidAwaitableExpression( - SemanticModel model, ISyntaxFactsService syntaxFacts, SyntaxNode node, CancellationToken cancellationToken) + private static bool IsValidAwaitableExpression( + SemanticModel model, ISyntaxFactsService syntaxFacts, SyntaxNode node, CancellationToken cancellationToken) + { + if (syntaxFacts.IsExpressionOfInvocationExpression(node.Parent)) { - if (syntaxFacts.IsExpressionOfInvocationExpression(node.Parent)) - { - // Do not offer fix on `MethodAsync()$$.ConfigureAwait()` - // Do offer fix on `MethodAsync()$$.Invalid()` - if (!model.GetTypeInfo(node.GetRequiredParent().GetRequiredParent(), cancellationToken).Type.IsErrorType()) - return false; - } - - if (syntaxFacts.IsExpressionOfAwaitExpression(node)) + // Do not offer fix on `MethodAsync()$$.ConfigureAwait()` + // Do offer fix on `MethodAsync()$$.Invalid()` + if (!model.GetTypeInfo(node.GetRequiredParent().GetRequiredParent(), cancellationToken).Type.IsErrorType()) return false; + } - // if we're on an actual type symbol itself (like literally `Task`) we don't want to offer to add await. - // we only want to add for actual expressions whose type is awaitable, not on the awaitable type itself. - var symbol = model.GetSymbolInfo(node, cancellationToken).GetAnySymbol(); - if (symbol is ITypeSymbol) - return false; + if (syntaxFacts.IsExpressionOfAwaitExpression(node)) + return false; - var type = model.GetTypeInfo(node, cancellationToken).Type; - return type?.IsAwaitableNonDynamic(model, node.SpanStart) == true; - } + // if we're on an actual type symbol itself (like literally `Task`) we don't want to offer to add await. + // we only want to add for actual expressions whose type is awaitable, not on the awaitable type itself. + var symbol = model.GetSymbolInfo(node, cancellationToken).GetAnySymbol(); + if (symbol is ITypeSymbol) + return false; + + var type = model.GetTypeInfo(node, cancellationToken).Type; + return type?.IsAwaitableNonDynamic(model, node.SpanStart) == true; + } - private static Task AddAwaitAsync( - Document document, - TExpressionSyntax expression, - bool withConfigureAwait, - CancellationToken cancellationToken) + private static Task AddAwaitAsync( + Document document, + TExpressionSyntax expression, + bool withConfigureAwait, + CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + var withoutTrivia = expression.WithoutTrivia(); + withoutTrivia = (TExpressionSyntax)generator.AddParentheses(withoutTrivia); + if (withConfigureAwait) { - var generator = SyntaxGenerator.GetGenerator(document); - var withoutTrivia = expression.WithoutTrivia(); - withoutTrivia = (TExpressionSyntax)generator.AddParentheses(withoutTrivia); - if (withConfigureAwait) - { - withoutTrivia = (TExpressionSyntax)generator.InvocationExpression( - generator.MemberAccessExpression(withoutTrivia, nameof(Task.ConfigureAwait)), - generator.FalseLiteralExpression()); - } + withoutTrivia = (TExpressionSyntax)generator.InvocationExpression( + generator.MemberAccessExpression(withoutTrivia, nameof(Task.ConfigureAwait)), + generator.FalseLiteralExpression()); + } - var awaitExpression = generator - .AddParentheses(generator.AwaitExpression(withoutTrivia)) - .WithTriviaFrom(expression); + var awaitExpression = generator + .AddParentheses(generator.AwaitExpression(withoutTrivia)) + .WithTriviaFrom(expression); - return document.ReplaceNodeAsync(expression, awaitExpression, cancellationToken); - } + return document.ReplaceNodeAsync(expression, awaitExpression, cancellationToken); } } diff --git a/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AbstractAddMissingImportsFeatureService.cs b/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AbstractAddMissingImportsFeatureService.cs index 7969c1e510404..9abc95a30523a 100644 --- a/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AbstractAddMissingImportsFeatureService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AbstractAddMissingImportsFeatureService.cs @@ -21,253 +21,252 @@ using Microsoft.CodeAnalysis.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.AddMissingImports +namespace Microsoft.CodeAnalysis.AddMissingImports; + +internal abstract class AbstractAddMissingImportsFeatureService : IAddMissingImportsFeatureService { - internal abstract class AbstractAddMissingImportsFeatureService : IAddMissingImportsFeatureService - { - protected abstract ImmutableArray FixableDiagnosticIds { get; } + protected abstract ImmutableArray FixableDiagnosticIds { get; } - protected abstract ImmutableArray GetFormatRules(SourceText text); + protected abstract ImmutableArray GetFormatRules(SourceText text); + + /// + public async Task AddMissingImportsAsync(Document document, TextSpan textSpan, AddMissingImportsOptions options, IProgress progressTracker, CancellationToken cancellationToken) + { + var analysisResult = await AnalyzeAsync(document, textSpan, options, cancellationToken).ConfigureAwait(false); + return await AddMissingImportsAsync( + document, analysisResult, options.CleanupOptions.FormattingOptions, progressTracker, cancellationToken).ConfigureAwait(false); + } - /// - public async Task AddMissingImportsAsync(Document document, TextSpan textSpan, AddMissingImportsOptions options, IProgress progressTracker, CancellationToken cancellationToken) + /// + public async Task AddMissingImportsAsync( + Document document, + AddMissingImportsAnalysisResult analysisResult, + SyntaxFormattingOptions formattingOptions, + IProgress progressTracker, + CancellationToken cancellationToken) + { + if (analysisResult.CanAddMissingImports) { - var analysisResult = await AnalyzeAsync(document, textSpan, options, cancellationToken).ConfigureAwait(false); - return await AddMissingImportsAsync( - document, analysisResult, options.CleanupOptions.FormattingOptions, progressTracker, cancellationToken).ConfigureAwait(false); + // Apply those fixes to the document. + var newDocument = await ApplyFixesAsync(document, analysisResult.AddImportFixData, formattingOptions, progressTracker, cancellationToken).ConfigureAwait(false); + return newDocument; } - /// - public async Task AddMissingImportsAsync( - Document document, - AddMissingImportsAnalysisResult analysisResult, - SyntaxFormattingOptions formattingOptions, - IProgress progressTracker, - CancellationToken cancellationToken) - { - if (analysisResult.CanAddMissingImports) - { - // Apply those fixes to the document. - var newDocument = await ApplyFixesAsync(document, analysisResult.AddImportFixData, formattingOptions, progressTracker, cancellationToken).ConfigureAwait(false); - return newDocument; - } + return document; + } - return document; - } + /// + public async Task AnalyzeAsync(Document document, TextSpan textSpan, AddMissingImportsOptions options, CancellationToken cancellationToken) + { + // Get the diagnostics that indicate a missing import. + var addImportFeatureService = document.GetRequiredLanguageService(); - /// - public async Task AnalyzeAsync(Document document, TextSpan textSpan, AddMissingImportsOptions options, CancellationToken cancellationToken) - { - // Get the diagnostics that indicate a missing import. - var addImportFeatureService = document.GetRequiredLanguageService(); + var solution = document.Project.Solution; + var symbolSearchService = solution.Services.GetRequiredService(); - var solution = document.Project.Solution; - var symbolSearchService = solution.Services.GetRequiredService(); + // Since we are not currently considering NuGet packages, pass an empty array + var packageSources = ImmutableArray.Empty; - // Since we are not currently considering NuGet packages, pass an empty array - var packageSources = ImmutableArray.Empty; + var addImportOptions = new AddImportOptions( + SearchOptions: new() { SearchReferenceAssemblies = true, SearchNuGetPackages = false }, + CleanupOptions: options.CleanupOptions, + HideAdvancedMembers: options.HideAdvancedMembers); - var addImportOptions = new AddImportOptions( - SearchOptions: new() { SearchReferenceAssemblies = true, SearchNuGetPackages = false }, - CleanupOptions: options.CleanupOptions, - HideAdvancedMembers: options.HideAdvancedMembers); + var unambiguousFixes = await addImportFeatureService.GetUniqueFixesAsync( + document, textSpan, FixableDiagnosticIds, symbolSearchService, + addImportOptions, packageSources, cancellationToken).ConfigureAwait(false); - var unambiguousFixes = await addImportFeatureService.GetUniqueFixesAsync( - document, textSpan, FixableDiagnosticIds, symbolSearchService, - addImportOptions, packageSources, cancellationToken).ConfigureAwait(false); + // We do not want to add project or framework references without the user's input, so filter those out. + var usableFixes = unambiguousFixes.WhereAsArray(fixData => DoesNotAddReference(fixData, document.Project.Id)); - // We do not want to add project or framework references without the user's input, so filter those out. - var usableFixes = unambiguousFixes.WhereAsArray(fixData => DoesNotAddReference(fixData, document.Project.Id)); + return new AddMissingImportsAnalysisResult(usableFixes); + } - return new AddMissingImportsAnalysisResult(usableFixes); - } + private static bool DoesNotAddReference(AddImportFixData fixData, ProjectId currentProjectId) + { + return (fixData.ProjectReferenceToAdd is null || fixData.ProjectReferenceToAdd == currentProjectId) + && (fixData.PortableExecutableReferenceProjectId is null || fixData.PortableExecutableReferenceProjectId == currentProjectId) + && string.IsNullOrEmpty(fixData.AssemblyReferenceAssemblyName); + } - private static bool DoesNotAddReference(AddImportFixData fixData, ProjectId currentProjectId) + private async Task ApplyFixesAsync( + Document document, + ImmutableArray fixes, + SyntaxFormattingOptions formattingOptions, + IProgress progressTracker, + CancellationToken cancellationToken) + { + if (fixes.IsEmpty) { - return (fixData.ProjectReferenceToAdd is null || fixData.ProjectReferenceToAdd == currentProjectId) - && (fixData.PortableExecutableReferenceProjectId is null || fixData.PortableExecutableReferenceProjectId == currentProjectId) - && string.IsNullOrEmpty(fixData.AssemblyReferenceAssemblyName); + return document; } - private async Task ApplyFixesAsync( - Document document, - ImmutableArray fixes, - SyntaxFormattingOptions formattingOptions, - IProgress progressTracker, - CancellationToken cancellationToken) - { - if (fixes.IsEmpty) - { - return document; - } + var solution = document.Project.Solution; + var textDiffingService = solution.Services.GetRequiredService(); + var packageInstallerService = solution.Services.GetService(); + var addImportService = document.GetRequiredLanguageService(); - var solution = document.Project.Solution; - var textDiffingService = solution.Services.GetRequiredService(); - var packageInstallerService = solution.Services.GetService(); - var addImportService = document.GetRequiredLanguageService(); + // Do not limit the results since we plan to fix all the reported issues. + var codeActions = addImportService.GetCodeActionsForFixes(document, fixes, packageInstallerService, maxResults: int.MaxValue); + var getChangesTasks = codeActions.Select( + action => GetChangesForCodeActionAsync(document, action, textDiffingService, progressTracker, cancellationToken)); - // Do not limit the results since we plan to fix all the reported issues. - var codeActions = addImportService.GetCodeActionsForFixes(document, fixes, packageInstallerService, maxResults: int.MaxValue); - var getChangesTasks = codeActions.Select( - action => GetChangesForCodeActionAsync(document, action, textDiffingService, progressTracker, cancellationToken)); + // Using Sets allows us to accumulate only the distinct changes. + var allTextChanges = new HashSet(); + // Some fixes require adding missing references. + var allAddedProjectReferences = new HashSet(); + var allAddedMetaDataReferences = new HashSet(); - // Using Sets allows us to accumulate only the distinct changes. - var allTextChanges = new HashSet(); - // Some fixes require adding missing references. - var allAddedProjectReferences = new HashSet(); - var allAddedMetaDataReferences = new HashSet(); + foreach (var getChangesTask in getChangesTasks) + { + var (projectChanges, textChanges) = await getChangesTask.ConfigureAwait(false); - foreach (var getChangesTask in getChangesTasks) - { - var (projectChanges, textChanges) = await getChangesTask.ConfigureAwait(false); + allTextChanges.UnionWith(textChanges); + allAddedProjectReferences.UnionWith(projectChanges.GetAddedProjectReferences()); + allAddedMetaDataReferences.UnionWith(projectChanges.GetAddedMetadataReferences()); + } - allTextChanges.UnionWith(textChanges); - allAddedProjectReferences.UnionWith(projectChanges.GetAddedProjectReferences()); - allAddedMetaDataReferences.UnionWith(projectChanges.GetAddedMetadataReferences()); - } + // Apply changes to both the project and document. + var newProject = document.Project; + newProject = newProject.AddMetadataReferences(allAddedMetaDataReferences); + newProject = newProject.AddProjectReferences(allAddedProjectReferences); + + // Only consider insertion changes to reduce the chance of producing a + // badly merged final document. Alphabetize the new imports, this will not + // change the insertion point but will give a more correct result. The user + // may still need to use organize imports afterwards. + var orderedTextInserts = allTextChanges.Where(change => change.Span.IsEmpty) + .OrderBy(change => change.NewText); + + // Capture each location where we are inserting imports as well as the total + // length of the text we are inserting so that we can format the span afterwards. + var insertSpans = allTextChanges + .GroupBy(change => change.Span) + .Select(changes => new TextSpan(changes.Key.Start, changes.Sum(change => change.NewText!.Length))); + + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var newText = text.WithChanges(orderedTextInserts); + var newDocument = newProject.GetRequiredDocument(document.Id).WithText(newText); + + // When imports are added to a code file that has no previous imports, extra + // newlines are generated between each import because the fix is expecting to + // separate the imports from the rest of the code file. We need to format the + // imports to remove these extra newlines. + return await CleanUpNewLinesAsync(newDocument, insertSpans, formattingOptions, cancellationToken).ConfigureAwait(false); + } - // Apply changes to both the project and document. - var newProject = document.Project; - newProject = newProject.AddMetadataReferences(allAddedMetaDataReferences); - newProject = newProject.AddProjectReferences(allAddedProjectReferences); - - // Only consider insertion changes to reduce the chance of producing a - // badly merged final document. Alphabetize the new imports, this will not - // change the insertion point but will give a more correct result. The user - // may still need to use organize imports afterwards. - var orderedTextInserts = allTextChanges.Where(change => change.Span.IsEmpty) - .OrderBy(change => change.NewText); - - // Capture each location where we are inserting imports as well as the total - // length of the text we are inserting so that we can format the span afterwards. - var insertSpans = allTextChanges - .GroupBy(change => change.Span) - .Select(changes => new TextSpan(changes.Key.Start, changes.Sum(change => change.NewText!.Length))); - - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var newText = text.WithChanges(orderedTextInserts); - var newDocument = newProject.GetRequiredDocument(document.Id).WithText(newText); - - // When imports are added to a code file that has no previous imports, extra - // newlines are generated between each import because the fix is expecting to - // separate the imports from the rest of the code file. We need to format the - // imports to remove these extra newlines. - return await CleanUpNewLinesAsync(newDocument, insertSpans, formattingOptions, cancellationToken).ConfigureAwait(false); - } + private async Task CleanUpNewLinesAsync(Document document, IEnumerable insertSpans, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken) + { + var newDocument = document; - private async Task CleanUpNewLinesAsync(Document document, IEnumerable insertSpans, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken) + // Since imports can be added at both the CompilationUnit and the Namespace level, + // format each span individually so that we can retain each newline that was intended + // to separate the import section from the other content. + foreach (var insertSpan in insertSpans) { - var newDocument = document; + newDocument = await CleanUpNewLinesAsync(newDocument, insertSpan, formattingOptions, cancellationToken).ConfigureAwait(false); + } - // Since imports can be added at both the CompilationUnit and the Namespace level, - // format each span individually so that we can retain each newline that was intended - // to separate the import section from the other content. - foreach (var insertSpan in insertSpans) - { - newDocument = await CleanUpNewLinesAsync(newDocument, insertSpan, formattingOptions, cancellationToken).ConfigureAwait(false); - } + return newDocument; + } - return newDocument; + private async Task CleanUpNewLinesAsync(Document document, TextSpan insertSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var services = document.Project.Solution.Services; + + var textChanges = Formatter.GetFormattedTextChanges( + root, + [insertSpan], + services, + options: options, + rules: GetFormatRules(text), + cancellationToken); + + // If there are no changes then, do less work. + if (textChanges.Count == 0) + { + return document; } - private async Task CleanUpNewLinesAsync(Document document, TextSpan insertSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var services = document.Project.Solution.Services; - - var textChanges = Formatter.GetFormattedTextChanges( - root, - [insertSpan], - services, - options: options, - rules: GetFormatRules(text), - cancellationToken); - - // If there are no changes then, do less work. - if (textChanges.Count == 0) - { - return document; - } + // The last text change should include where the insert span ends + Debug.Assert(textChanges.Last().Span.IntersectsWith(insertSpan.End)); - // The last text change should include where the insert span ends - Debug.Assert(textChanges.Last().Span.IntersectsWith(insertSpan.End)); + // If there are changes then, this was a case where there were no + // previous imports statements. We need to retain the final extra + // newline because that separates the imports section from the rest + // of the code. + textChanges.RemoveAt(textChanges.Count - 1); - // If there are changes then, this was a case where there were no - // previous imports statements. We need to retain the final extra - // newline because that separates the imports section from the rest - // of the code. - textChanges.RemoveAt(textChanges.Count - 1); + var newText = text.WithChanges(textChanges); + return document.WithText(newText); + } - var newText = text.WithChanges(textChanges); - return document.WithText(newText); + private static async Task<(ProjectChanges, IEnumerable)> GetChangesForCodeActionAsync( + Document document, + CodeAction codeAction, + IDocumentTextDifferencingService textDiffingService, + IProgress progressTracker, + CancellationToken cancellationToken) + { + // CodeAction.GetChangedSolutionAsync is only implemented for code actions that can fully compute the new + // solution without deferred computation or taking a dependency on the main thread. In other cases, the + // implementation of GetChangedSolutionAsync will throw an exception and the code action application is + // expected to apply the changes by executing the operations in GetOperationsAsync (which may have other + // side effects). This code cannot assume the input CodeAction supports GetChangedSolutionAsync, so it first + // attempts to apply text changes obtained from GetOperationsAsync. Two forms are supported: + // + // 1. GetOperationsAsync returns an empty list of operations (i.e. no changes are required) + // 2. GetOperationsAsync returns a list of operations, where the first change is an ApplyChangesOperation to + // change the text in the solution, and any remaining changes are deferred computation changes. + // + // If GetOperationsAsync does not adhere to one of these patterns, the code falls back to calling + // GetChangedSolutionAsync since there is no clear way to apply the changes otherwise. + var operations = await codeAction.GetOperationsAsync( + document.Project.Solution, progressTracker, cancellationToken).ConfigureAwait(false); + Solution newSolution; + if (operations.Length == 0) + { + newSolution = document.Project.Solution; } - - private static async Task<(ProjectChanges, IEnumerable)> GetChangesForCodeActionAsync( - Document document, - CodeAction codeAction, - IDocumentTextDifferencingService textDiffingService, - IProgress progressTracker, - CancellationToken cancellationToken) + else if (operations is [ApplyChangesOperation applyChangesOperation]) { - // CodeAction.GetChangedSolutionAsync is only implemented for code actions that can fully compute the new - // solution without deferred computation or taking a dependency on the main thread. In other cases, the - // implementation of GetChangedSolutionAsync will throw an exception and the code action application is - // expected to apply the changes by executing the operations in GetOperationsAsync (which may have other - // side effects). This code cannot assume the input CodeAction supports GetChangedSolutionAsync, so it first - // attempts to apply text changes obtained from GetOperationsAsync. Two forms are supported: - // - // 1. GetOperationsAsync returns an empty list of operations (i.e. no changes are required) - // 2. GetOperationsAsync returns a list of operations, where the first change is an ApplyChangesOperation to - // change the text in the solution, and any remaining changes are deferred computation changes. - // - // If GetOperationsAsync does not adhere to one of these patterns, the code falls back to calling - // GetChangedSolutionAsync since there is no clear way to apply the changes otherwise. - var operations = await codeAction.GetOperationsAsync( - document.Project.Solution, progressTracker, cancellationToken).ConfigureAwait(false); - Solution newSolution; - if (operations.Length == 0) - { - newSolution = document.Project.Solution; - } - else if (operations is [ApplyChangesOperation applyChangesOperation]) - { - newSolution = applyChangesOperation.ChangedSolution; - } - else - { - newSolution = await codeAction.GetRequiredChangedSolutionAsync(progressTracker, cancellationToken).ConfigureAwait(false); - } - - var newDocument = newSolution.GetRequiredDocument(document.Id); + newSolution = applyChangesOperation.ChangedSolution; + } + else + { + newSolution = await codeAction.GetRequiredChangedSolutionAsync(progressTracker, cancellationToken).ConfigureAwait(false); + } - // Use Line differencing to reduce the possibility of changes that overwrite existing code. - var textChanges = await textDiffingService.GetTextChangesAsync( - document, newDocument, TextDifferenceTypes.Line, cancellationToken).ConfigureAwait(false); - var projectChanges = newDocument.Project.GetChanges(document.Project); + var newDocument = newSolution.GetRequiredDocument(document.Id); - return (projectChanges, textChanges); - } + // Use Line differencing to reduce the possibility of changes that overwrite existing code. + var textChanges = await textDiffingService.GetTextChangesAsync( + document, newDocument, TextDifferenceTypes.Line, cancellationToken).ConfigureAwait(false); + var projectChanges = newDocument.Project.GetChanges(document.Project); - protected sealed class CleanUpNewLinesFormatter(SourceText text) : AbstractFormattingRule - { - private readonly SourceText _text = text; + return (projectChanges, textChanges); + } - public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) - { - // Since we know the general shape of these new import statements, we simply look for where - // tokens are not on the same line and force them to only be separated by a single newline. + protected sealed class CleanUpNewLinesFormatter(SourceText text) : AbstractFormattingRule + { + private readonly SourceText _text = text; - _text.GetLineAndOffset(previousToken.Span.Start, out var previousLine, out _); - _text.GetLineAndOffset(currentToken.Span.Start, out var currentLine, out _); + public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) + { + // Since we know the general shape of these new import statements, we simply look for where + // tokens are not on the same line and force them to only be separated by a single newline. - if (previousLine != currentLine) - { - return FormattingOperations.CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.ForceLines); - } + _text.GetLineAndOffset(previousToken.Span.Start, out var previousLine, out _); + _text.GetLineAndOffset(currentToken.Span.Start, out var currentLine, out _); - return null; + if (previousLine != currentLine) + { + return FormattingOperations.CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.ForceLines); } + + return null; } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AbstractAddMissingImportsRefactoringProvider.cs b/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AbstractAddMissingImportsRefactoringProvider.cs index 070f21b307bff..9a9344144e2ad 100644 --- a/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AbstractAddMissingImportsRefactoringProvider.cs +++ b/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AbstractAddMissingImportsRefactoringProvider.cs @@ -14,65 +14,64 @@ using Microsoft.CodeAnalysis.PasteTracking; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.AddMissingImports +namespace Microsoft.CodeAnalysis.AddMissingImports; + +internal abstract class AbstractAddMissingImportsRefactoringProvider : CodeRefactoringProvider { - internal abstract class AbstractAddMissingImportsRefactoringProvider : CodeRefactoringProvider - { - protected abstract string CodeActionTitle { get; } + protected abstract string CodeActionTitle { get; } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, _, cancellationToken) = context; + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, _, cancellationToken) = context; - // If we aren't in a host that supports paste tracking (known by having exactly one export of type - // IPasteTrackingService), we can't do anything. This is just to avoid creating MEF part rejections for - // things composing the Features layer. - var services = document.Project.Solution.Workspace.Services.HostServices as IMefHostExportProvider; - var pasteTrackingService = services?.GetExports().SingleOrDefault()?.Value; - if (pasteTrackingService is null) - return; + // If we aren't in a host that supports paste tracking (known by having exactly one export of type + // IPasteTrackingService), we can't do anything. This is just to avoid creating MEF part rejections for + // things composing the Features layer. + var services = document.Project.Solution.Workspace.Services.HostServices as IMefHostExportProvider; + var pasteTrackingService = services?.GetExports().SingleOrDefault()?.Value; + if (pasteTrackingService is null) + return; - // Currently this refactoring requires the SourceTextContainer to have a pasted text span. - var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (!pasteTrackingService.TryGetPastedTextSpan(sourceText.Container, out var textSpan)) - { - return; - } + // Currently this refactoring requires the SourceTextContainer to have a pasted text span. + var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (!pasteTrackingService.TryGetPastedTextSpan(sourceText.Container, out var textSpan)) + { + return; + } - // Check pasted text span for missing imports - var addMissingImportsService = document.GetRequiredLanguageService(); + // Check pasted text span for missing imports + var addMissingImportsService = document.GetRequiredLanguageService(); - var cleanupOptions = await document.GetCodeCleanupOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); - var options = new AddMissingImportsOptions( - cleanupOptions, - context.Options.GetOptions(document.Project.Services).HideAdvancedMembers); + var cleanupOptions = await document.GetCodeCleanupOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); + var options = new AddMissingImportsOptions( + cleanupOptions, + context.Options.GetOptions(document.Project.Services).HideAdvancedMembers); - var analysis = await addMissingImportsService.AnalyzeAsync(document, textSpan, options, cancellationToken).ConfigureAwait(false); - if (!analysis.CanAddMissingImports) - { - return; - } + var analysis = await addMissingImportsService.AnalyzeAsync(document, textSpan, options, cancellationToken).ConfigureAwait(false); + if (!analysis.CanAddMissingImports) + { + return; + } - var addImportsCodeAction = CodeAction.Create( - CodeActionTitle, - (progressTracker, cancellationToken) => AddMissingImportsAsync( - document, addMissingImportsService, analysis, options.CleanupOptions.FormattingOptions, progressTracker, cancellationToken), - CodeActionTitle); + var addImportsCodeAction = CodeAction.Create( + CodeActionTitle, + (progressTracker, cancellationToken) => AddMissingImportsAsync( + document, addMissingImportsService, analysis, options.CleanupOptions.FormattingOptions, progressTracker, cancellationToken), + CodeActionTitle); - context.RegisterRefactoring(addImportsCodeAction, textSpan); - } + context.RegisterRefactoring(addImportsCodeAction, textSpan); + } - private static async Task AddMissingImportsAsync( - Document document, - IAddMissingImportsFeatureService addMissingImportsService, - AddMissingImportsAnalysisResult analysis, - SyntaxFormattingOptions formattingOptions, - IProgress progressTracker, - CancellationToken cancellationToken) - { - var modifiedDocument = await addMissingImportsService.AddMissingImportsAsync( - document, analysis, formattingOptions, progressTracker, cancellationToken).ConfigureAwait(false); - return modifiedDocument.Project.Solution; - } + private static async Task AddMissingImportsAsync( + Document document, + IAddMissingImportsFeatureService addMissingImportsService, + AddMissingImportsAnalysisResult analysis, + SyntaxFormattingOptions formattingOptions, + IProgress progressTracker, + CancellationToken cancellationToken) + { + var modifiedDocument = await addMissingImportsService.AddMissingImportsAsync( + document, analysis, formattingOptions, progressTracker, cancellationToken).ConfigureAwait(false); + return modifiedDocument.Project.Solution; } } diff --git a/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AddMissingImportsAnalysisResult.cs b/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AddMissingImportsAnalysisResult.cs index 72efe750ba9b4..cce889d4785d7 100644 --- a/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AddMissingImportsAnalysisResult.cs +++ b/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/AddMissingImportsAnalysisResult.cs @@ -5,12 +5,11 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.AddImport; -namespace Microsoft.CodeAnalysis.AddMissingImports +namespace Microsoft.CodeAnalysis.AddMissingImports; + +internal sealed class AddMissingImportsAnalysisResult( + ImmutableArray addImportFixData) { - internal sealed class AddMissingImportsAnalysisResult( - ImmutableArray addImportFixData) - { - public ImmutableArray AddImportFixData { get; } = addImportFixData; - public bool CanAddMissingImports => !AddImportFixData.IsEmpty; - } + public ImmutableArray AddImportFixData { get; } = addImportFixData; + public bool CanAddMissingImports => !AddImportFixData.IsEmpty; } diff --git a/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/IAddMissingImportsFeatureService.cs b/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/IAddMissingImportsFeatureService.cs index 0aa9268590188..99c6f34963218 100644 --- a/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/IAddMissingImportsFeatureService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/AddMissingImports/IAddMissingImportsFeatureService.cs @@ -11,33 +11,32 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.AddMissingImports -{ - [DataContract] - internal readonly record struct AddMissingImportsOptions( - [property: DataMember(Order = 0)] CodeCleanupOptions CleanupOptions, - [property: DataMember(Order = 1)] bool HideAdvancedMembers); +namespace Microsoft.CodeAnalysis.AddMissingImports; + +[DataContract] +internal readonly record struct AddMissingImportsOptions( + [property: DataMember(Order = 0)] CodeCleanupOptions CleanupOptions, + [property: DataMember(Order = 1)] bool HideAdvancedMembers); - internal interface IAddMissingImportsFeatureService : ILanguageService - { - /// - /// Attempts to add missing imports to the document within the textspan provided. The imports added will - /// not add assembly references to the project. In case of failure, null is returned. Failure can happen - /// if there are ambiguous imports, no known resolutions to import, or if no imports that would be provided - /// would be added without adding a reference for the project. - /// - Task AddMissingImportsAsync(Document document, TextSpan textSpan, AddMissingImportsOptions options, IProgress progressTracker, CancellationToken cancellationToken); +internal interface IAddMissingImportsFeatureService : ILanguageService +{ + /// + /// Attempts to add missing imports to the document within the textspan provided. The imports added will + /// not add assembly references to the project. In case of failure, null is returned. Failure can happen + /// if there are ambiguous imports, no known resolutions to import, or if no imports that would be provided + /// would be added without adding a reference for the project. + /// + Task AddMissingImportsAsync(Document document, TextSpan textSpan, AddMissingImportsOptions options, IProgress progressTracker, CancellationToken cancellationToken); - /// - /// Analyzes the document inside the texstpan to determine if imports can be added. - /// - Task AnalyzeAsync(Document document, TextSpan textSpan, AddMissingImportsOptions options, CancellationToken cancellationToken); + /// + /// Analyzes the document inside the texstpan to determine if imports can be added. + /// + Task AnalyzeAsync(Document document, TextSpan textSpan, AddMissingImportsOptions options, CancellationToken cancellationToken); - /// - /// Performs the same action as but with a predetermined analysis of the input - /// instead of recalculating it - /// - Task AddMissingImportsAsync(Document document, AddMissingImportsAnalysisResult analysisResult, SyntaxFormattingOptions formattingOptions, IProgress progressTracker, CancellationToken cancellationToken); - } + /// + /// Performs the same action as but with a predetermined analysis of the input + /// instead of recalculating it + /// + Task AddMissingImportsAsync(Document document, AddMissingImportsAnalysisResult analysisResult, SyntaxFormattingOptions formattingOptions, IProgress progressTracker, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/CodeRefactorings/CodeRefactoring.cs b/src/Features/Core/Portable/CodeRefactorings/CodeRefactoring.cs index 17216a5ff5d45..a35daaab4d96b 100644 --- a/src/Features/Core/Portable/CodeRefactorings/CodeRefactoring.cs +++ b/src/Features/Core/Portable/CodeRefactorings/CodeRefactoring.cs @@ -8,43 +8,42 @@ using Microsoft.CodeAnalysis.CodeFixesAndRefactorings; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +/// +/// Represents a set of transformations that can be applied to a piece of code. +/// +internal class CodeRefactoring { + public CodeRefactoringProvider Provider { get; } + /// - /// Represents a set of transformations that can be applied to a piece of code. + /// List of tuples of possible actions that can be used to transform the code the TextSpan within the original document they're applicable to. /// - internal class CodeRefactoring + /// + /// applicableToSpan should represent a logical section within the original document that the action is + /// applicable to. It doesn't have to precisely represent the exact that will get changed. + /// + public ImmutableArray<(CodeAction action, TextSpan? applicableToSpan)> CodeActions { get; } + + public FixAllProviderInfo? FixAllProviderInfo { get; } + + public CodeActionOptionsProvider CodeActionOptionsProvider { get; } + + public CodeRefactoring( + CodeRefactoringProvider provider, + ImmutableArray<(CodeAction, TextSpan?)> actions, + FixAllProviderInfo? fixAllProviderInfo, + CodeActionOptionsProvider codeActionOptionsProvider) { - public CodeRefactoringProvider Provider { get; } - - /// - /// List of tuples of possible actions that can be used to transform the code the TextSpan within the original document they're applicable to. - /// - /// - /// applicableToSpan should represent a logical section within the original document that the action is - /// applicable to. It doesn't have to precisely represent the exact that will get changed. - /// - public ImmutableArray<(CodeAction action, TextSpan? applicableToSpan)> CodeActions { get; } - - public FixAllProviderInfo? FixAllProviderInfo { get; } - - public CodeActionOptionsProvider CodeActionOptionsProvider { get; } - - public CodeRefactoring( - CodeRefactoringProvider provider, - ImmutableArray<(CodeAction, TextSpan?)> actions, - FixAllProviderInfo? fixAllProviderInfo, - CodeActionOptionsProvider codeActionOptionsProvider) + Provider = provider; + CodeActions = actions.NullToEmpty(); + FixAllProviderInfo = fixAllProviderInfo; + CodeActionOptionsProvider = codeActionOptionsProvider; + + if (CodeActions.IsEmpty) { - Provider = provider; - CodeActions = actions.NullToEmpty(); - FixAllProviderInfo = fixAllProviderInfo; - CodeActionOptionsProvider = codeActionOptionsProvider; - - if (CodeActions.IsEmpty) - { - throw new ArgumentException(FeaturesResources.Actions_can_not_be_empty, nameof(actions)); - } + throw new ArgumentException(FeaturesResources.Actions_can_not_be_empty, nameof(actions)); } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringContextExtensions.cs b/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringContextExtensions.cs index 5d9d1afd2bfa9..238212dd663f1 100644 --- a/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringContextExtensions.cs +++ b/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringContextExtensions.cs @@ -11,63 +11,62 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +internal static class CodeRefactoringContextExtensions { - internal static class CodeRefactoringContextExtensions + /// + /// Use this helper to register multiple refactorings (). + /// + public static void RegisterRefactorings( + this CodeRefactoringContext context, ImmutableArray actions, TextSpan? applicableToSpan = null) + where TCodeAction : CodeAction { - /// - /// Use this helper to register multiple refactorings (). - /// - public static void RegisterRefactorings( - this CodeRefactoringContext context, ImmutableArray actions, TextSpan? applicableToSpan = null) - where TCodeAction : CodeAction + if (!actions.IsDefault) { - if (!actions.IsDefault) + foreach (var action in actions) { - foreach (var action in actions) + if (applicableToSpan != null) + { + context.RegisterRefactoring(action, applicableToSpan.Value); + } + else { - if (applicableToSpan != null) - { - context.RegisterRefactoring(action, applicableToSpan.Value); - } - else - { - context.RegisterRefactoring(action); - } + context.RegisterRefactoring(action); } } } + } - public static Task TryGetRelevantNodeAsync(this CodeRefactoringContext context) where TSyntaxNode : SyntaxNode - => TryGetRelevantNodeAsync(context, allowEmptyNode: false); + public static Task TryGetRelevantNodeAsync(this CodeRefactoringContext context) where TSyntaxNode : SyntaxNode + => TryGetRelevantNodeAsync(context, allowEmptyNode: false); - public static Task TryGetRelevantNodeAsync(this CodeRefactoringContext context, bool allowEmptyNode) where TSyntaxNode : SyntaxNode - => TryGetRelevantNodeAsync(context.Document, context.Span, allowEmptyNode, context.CancellationToken); + public static Task TryGetRelevantNodeAsync(this CodeRefactoringContext context, bool allowEmptyNode) where TSyntaxNode : SyntaxNode + => TryGetRelevantNodeAsync(context.Document, context.Span, allowEmptyNode, context.CancellationToken); - public static Task> GetRelevantNodesAsync(this CodeRefactoringContext context) where TSyntaxNode : SyntaxNode - => GetRelevantNodesAsync(context, allowEmptyNodes: false); + public static Task> GetRelevantNodesAsync(this CodeRefactoringContext context) where TSyntaxNode : SyntaxNode + => GetRelevantNodesAsync(context, allowEmptyNodes: false); - public static Task> GetRelevantNodesAsync(this CodeRefactoringContext context, bool allowEmptyNodes) where TSyntaxNode : SyntaxNode - => GetRelevantNodesAsync(context.Document, context.Span, allowEmptyNodes, context.CancellationToken); + public static Task> GetRelevantNodesAsync(this CodeRefactoringContext context, bool allowEmptyNodes) where TSyntaxNode : SyntaxNode + => GetRelevantNodesAsync(context.Document, context.Span, allowEmptyNodes, context.CancellationToken); - public static Task TryGetRelevantNodeAsync(this Document document, TextSpan span, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - => TryGetRelevantNodeAsync(document, span, allowEmptyNode: false, cancellationToken); + public static Task TryGetRelevantNodeAsync(this Document document, TextSpan span, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + => TryGetRelevantNodeAsync(document, span, allowEmptyNode: false, cancellationToken); - public static async Task TryGetRelevantNodeAsync(this Document document, TextSpan span, bool allowEmptyNode, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - var potentialNodes = await GetRelevantNodesAsync(document, span, allowEmptyNode, cancellationToken).ConfigureAwait(false); - return potentialNodes.FirstOrDefault(); - } + public static async Task TryGetRelevantNodeAsync(this Document document, TextSpan span, bool allowEmptyNode, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + var potentialNodes = await GetRelevantNodesAsync(document, span, allowEmptyNode, cancellationToken).ConfigureAwait(false); + return potentialNodes.FirstOrDefault(); + } - public static Task> GetRelevantNodesAsync( - this Document document, TextSpan span, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - => GetRelevantNodesAsync(document, span, allowEmptyNodes: false, cancellationToken); + public static Task> GetRelevantNodesAsync( + this Document document, TextSpan span, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + => GetRelevantNodesAsync(document, span, allowEmptyNodes: false, cancellationToken); - public static Task> GetRelevantNodesAsync( - this Document document, TextSpan span, bool allowEmptyNodes, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - var helpers = document.GetRequiredLanguageService(); - return helpers.GetRelevantNodesAsync(document, span, allowEmptyNodes, cancellationToken); - } + public static Task> GetRelevantNodesAsync( + this Document document, TextSpan span, bool allowEmptyNodes, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + var helpers = document.GetRequiredLanguageService(); + return helpers.GetRelevantNodesAsync(document, span, allowEmptyNodes, cancellationToken); } } diff --git a/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringService.cs b/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringService.cs index ab173d9156aa7..ef78b92ebcde3 100644 --- a/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringService.cs @@ -21,211 +21,210 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +[Export(typeof(ICodeRefactoringService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CodeRefactoringService( + [ImportMany] IEnumerable> providers) : ICodeRefactoringService { - [Export(typeof(ICodeRefactoringService)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class CodeRefactoringService( - [ImportMany] IEnumerable> providers) : ICodeRefactoringService + private readonly Lazy>>> _lazyLanguageToProvidersMap = new Lazy>>>( + () => + ImmutableDictionary.CreateRange( + DistributeLanguages(providers) + .GroupBy(lz => lz.Metadata.Language) + .Select(grp => new KeyValuePair>>( + grp.Key, + new Lazy>(() => ExtensionOrderer.Order(grp).Select(lz => lz.Value).ToImmutableArray()))))); + private readonly Lazy> _lazyRefactoringToMetadataMap = new(() => providers.Where(provider => provider.IsValueCreated).ToImmutableDictionary(provider => provider.Value, provider => provider.Metadata)); + + private ImmutableDictionary _fixAllProviderMap = ImmutableDictionary.Empty; + + private static IEnumerable> DistributeLanguages(IEnumerable> providers) { - private readonly Lazy>>> _lazyLanguageToProvidersMap = new Lazy>>>( - () => - ImmutableDictionary.CreateRange( - DistributeLanguages(providers) - .GroupBy(lz => lz.Metadata.Language) - .Select(grp => new KeyValuePair>>( - grp.Key, - new Lazy>(() => ExtensionOrderer.Order(grp).Select(lz => lz.Value).ToImmutableArray()))))); - private readonly Lazy> _lazyRefactoringToMetadataMap = new(() => providers.Where(provider => provider.IsValueCreated).ToImmutableDictionary(provider => provider.Value, provider => provider.Metadata)); - - private ImmutableDictionary _fixAllProviderMap = ImmutableDictionary.Empty; - - private static IEnumerable> DistributeLanguages(IEnumerable> providers) + foreach (var provider in providers) { - foreach (var provider in providers) + foreach (var language in provider.Metadata.Languages) { - foreach (var language in provider.Metadata.Languages) - { - var orderable = new OrderableLanguageMetadata( - provider.Metadata.Name, language, provider.Metadata.AfterTyped, provider.Metadata.BeforeTyped); - yield return new Lazy(() => provider.Value, orderable); - } + var orderable = new OrderableLanguageMetadata( + provider.Metadata.Name, language, provider.Metadata.AfterTyped, provider.Metadata.BeforeTyped); + yield return new Lazy(() => provider.Value, orderable); } } + } - private ImmutableDictionary>> LanguageToProvidersMap - => _lazyLanguageToProvidersMap.Value; + private ImmutableDictionary>> LanguageToProvidersMap + => _lazyLanguageToProvidersMap.Value; - private ImmutableDictionary RefactoringToMetadataMap - => _lazyRefactoringToMetadataMap.Value; + private ImmutableDictionary RefactoringToMetadataMap + => _lazyRefactoringToMetadataMap.Value; - private ConcatImmutableArray GetProviders(TextDocument document) + private ConcatImmutableArray GetProviders(TextDocument document) + { + var allRefactorings = ImmutableArray.Empty; + if (LanguageToProvidersMap.TryGetValue(document.Project.Language, out var lazyProviders)) { - var allRefactorings = ImmutableArray.Empty; - if (LanguageToProvidersMap.TryGetValue(document.Project.Language, out var lazyProviders)) - { - allRefactorings = ProjectCodeRefactoringProvider.FilterExtensions(document, lazyProviders.Value, GetExtensionInfo); - } + allRefactorings = ProjectCodeRefactoringProvider.FilterExtensions(document, lazyProviders.Value, GetExtensionInfo); + } - return allRefactorings.ConcatFast(GetProjectRefactorings(document)); + return allRefactorings.ConcatFast(GetProjectRefactorings(document)); - static ImmutableArray GetProjectRefactorings(TextDocument document) - { - // TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict refactorings in Interactive - if (document.Project.Solution.WorkspaceKind == WorkspaceKind.Interactive) - return []; + static ImmutableArray GetProjectRefactorings(TextDocument document) + { + // TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict refactorings in Interactive + if (document.Project.Solution.WorkspaceKind == WorkspaceKind.Interactive) + return []; - return ProjectCodeRefactoringProvider.GetExtensions(document, GetExtensionInfo); - } + return ProjectCodeRefactoringProvider.GetExtensions(document, GetExtensionInfo); + } + + static ProjectCodeRefactoringProvider.ExtensionInfo GetExtensionInfo(ExportCodeRefactoringProviderAttribute attribute) + => new(attribute.DocumentKinds, attribute.DocumentExtensions); + } + + public async Task HasRefactoringsAsync( + TextDocument document, + TextSpan state, + CodeActionOptionsProvider options, + CancellationToken cancellationToken) + { + var extensionManager = document.Project.Solution.Services.GetRequiredService(); + + foreach (var provider in GetProviders(document)) + { + cancellationToken.ThrowIfCancellationRequested(); + RefactoringToMetadataMap.TryGetValue(provider, out var providerMetadata); + + var refactoring = await GetRefactoringFromProviderAsync( + document, state, provider, providerMetadata, extensionManager, options, cancellationToken).ConfigureAwait(false); - static ProjectCodeRefactoringProvider.ExtensionInfo GetExtensionInfo(ExportCodeRefactoringProviderAttribute attribute) - => new(attribute.DocumentKinds, attribute.DocumentExtensions); + if (refactoring != null) + { + return true; + } } - public async Task HasRefactoringsAsync( - TextDocument document, - TextSpan state, - CodeActionOptionsProvider options, - CancellationToken cancellationToken) + return false; + } + + public async Task> GetRefactoringsAsync( + TextDocument document, + TextSpan state, + CodeActionRequestPriority? priority, + CodeActionOptionsProvider options, + Func addOperationScope, + CancellationToken cancellationToken) + { + using (TelemetryLogging.LogBlockTimeAggregated(FunctionId.CodeRefactoring_Summary, $"Pri{priority.GetPriorityInt()}")) + using (Logger.LogBlock(FunctionId.Refactoring_CodeRefactoringService_GetRefactoringsAsync, cancellationToken)) { var extensionManager = document.Project.Solution.Services.GetRequiredService(); + using var _ = ArrayBuilder>.GetInstance(out var tasks); foreach (var provider in GetProviders(document)) { - cancellationToken.ThrowIfCancellationRequested(); - RefactoringToMetadataMap.TryGetValue(provider, out var providerMetadata); + if (priority != null && priority != provider.RequestPriority) + continue; - var refactoring = await GetRefactoringFromProviderAsync( - document, state, provider, providerMetadata, extensionManager, options, cancellationToken).ConfigureAwait(false); - - if (refactoring != null) + tasks.Add(Task.Run(async () => { - return true; - } + // Log an individual telemetry event for slow code refactoring computations to + // allow targeted trace notifications for further investigation. 500 ms seemed like + // a good value so as to not be too noisy, but if fired, indicates a potential + // area requiring investigation. + const int CodeRefactoringTelemetryDelay = 500; + + var providerName = provider.GetType().Name; + RefactoringToMetadataMap.TryGetValue(provider, out var providerMetadata); + + var logMessage = KeyValueLogMessage.Create(m => + { + m[TelemetryLogging.KeyName] = providerName; + m[TelemetryLogging.KeyLanguageName] = document.Project.Language; + }); + + using (addOperationScope(providerName)) + using (RoslynEventSource.LogInformationalBlock(FunctionId.Refactoring_CodeRefactoringService_GetRefactoringsAsync, providerName, cancellationToken)) + using (TelemetryLogging.LogBlockTime(FunctionId.CodeRefactoring_Delay, logMessage, CodeRefactoringTelemetryDelay)) + { + return await GetRefactoringFromProviderAsync(document, state, provider, providerMetadata, + extensionManager, options, cancellationToken).ConfigureAwait(false); + } + }, + cancellationToken)); } - return false; + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + return results.WhereNotNull().ToImmutableArray(); } + } - public async Task> GetRefactoringsAsync( - TextDocument document, - TextSpan state, - CodeActionRequestPriority? priority, - CodeActionOptionsProvider options, - Func addOperationScope, - CancellationToken cancellationToken) - { - using (TelemetryLogging.LogBlockTimeAggregated(FunctionId.CodeRefactoring_Summary, $"Pri{priority.GetPriorityInt()}")) - using (Logger.LogBlock(FunctionId.Refactoring_CodeRefactoringService_GetRefactoringsAsync, cancellationToken)) + private Task GetRefactoringFromProviderAsync( + TextDocument textDocument, + TextSpan state, + CodeRefactoringProvider provider, + CodeChangeProviderMetadata? providerMetadata, + IExtensionManager extensionManager, + CodeActionOptionsProvider options, + CancellationToken cancellationToken) + { + return extensionManager.PerformFunctionAsync( + provider, + async () => { - var extensionManager = document.Project.Solution.Services.GetRequiredService(); - using var _ = ArrayBuilder>.GetInstance(out var tasks); - - foreach (var provider in GetProviders(document)) - { - if (priority != null && priority != provider.RequestPriority) - continue; + cancellationToken.ThrowIfCancellationRequested(); + using var _ = ArrayBuilder<(CodeAction action, TextSpan? applicableToSpan)>.GetInstance(out var actions); + var context = new CodeRefactoringContext(textDocument, state, - tasks.Add(Task.Run(async () => + // TODO: Can we share code between similar lambdas that we pass to this API in BatchFixAllProvider.cs, CodeFixService.cs and CodeRefactoringService.cs? + (action, applicableToSpan) => { - // Log an individual telemetry event for slow code refactoring computations to - // allow targeted trace notifications for further investigation. 500 ms seemed like - // a good value so as to not be too noisy, but if fired, indicates a potential - // area requiring investigation. - const int CodeRefactoringTelemetryDelay = 500; - - var providerName = provider.GetType().Name; - RefactoringToMetadataMap.TryGetValue(provider, out var providerMetadata); - - var logMessage = KeyValueLogMessage.Create(m => + // Serialize access for thread safety - we don't know what thread the refactoring provider will call this delegate from. + lock (actions) { - m[TelemetryLogging.KeyName] = providerName; - m[TelemetryLogging.KeyLanguageName] = document.Project.Language; - }); + // Add the Refactoring Provider Name to the parent CodeAction's CustomTags. + // Always add a name even in cases of 3rd party refactorings that do not export + // name metadata. + action.AddCustomTagAndTelemetryInfo(providerMetadata, provider); - using (addOperationScope(providerName)) - using (RoslynEventSource.LogInformationalBlock(FunctionId.Refactoring_CodeRefactoringService_GetRefactoringsAsync, providerName, cancellationToken)) - using (TelemetryLogging.LogBlockTime(FunctionId.CodeRefactoring_Delay, logMessage, CodeRefactoringTelemetryDelay)) - { - return await GetRefactoringFromProviderAsync(document, state, provider, providerMetadata, - extensionManager, options, cancellationToken).ConfigureAwait(false); + actions.Add((action, applicableToSpan)); } }, - cancellationToken)); - } + options, + cancellationToken); - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - return results.WhereNotNull().ToImmutableArray(); - } - } + var task = provider.ComputeRefactoringsAsync(context) ?? Task.CompletedTask; + await task.ConfigureAwait(false); - private Task GetRefactoringFromProviderAsync( - TextDocument textDocument, - TextSpan state, - CodeRefactoringProvider provider, - CodeChangeProviderMetadata? providerMetadata, - IExtensionManager extensionManager, - CodeActionOptionsProvider options, - CancellationToken cancellationToken) - { - return extensionManager.PerformFunctionAsync( - provider, - async () => + if (actions.Count == 0) { - cancellationToken.ThrowIfCancellationRequested(); - using var _ = ArrayBuilder<(CodeAction action, TextSpan? applicableToSpan)>.GetInstance(out var actions); - var context = new CodeRefactoringContext(textDocument, state, + return null; + } - // TODO: Can we share code between similar lambdas that we pass to this API in BatchFixAllProvider.cs, CodeFixService.cs and CodeRefactoringService.cs? - (action, applicableToSpan) => - { - // Serialize access for thread safety - we don't know what thread the refactoring provider will call this delegate from. - lock (actions) - { - // Add the Refactoring Provider Name to the parent CodeAction's CustomTags. - // Always add a name even in cases of 3rd party refactorings that do not export - // name metadata. - action.AddCustomTagAndTelemetryInfo(providerMetadata, provider); - - actions.Add((action, applicableToSpan)); - } - }, - options, - cancellationToken); - - var task = provider.ComputeRefactoringsAsync(context) ?? Task.CompletedTask; - await task.ConfigureAwait(false); - - if (actions.Count == 0) - { - return null; - } + var fixAllProviderInfo = extensionManager.PerformFunction( + provider, () => ImmutableInterlocked.GetOrAdd(ref _fixAllProviderMap, provider, FixAllProviderInfo.Create), defaultValue: null); + return new CodeRefactoring(provider, actions.ToImmutable(), fixAllProviderInfo, options); + }, defaultValue: null); + } - var fixAllProviderInfo = extensionManager.PerformFunction( - provider, () => ImmutableInterlocked.GetOrAdd(ref _fixAllProviderMap, provider, FixAllProviderInfo.Create), defaultValue: null); - return new CodeRefactoring(provider, actions.ToImmutable(), fixAllProviderInfo, options); - }, defaultValue: null); - } + private class ProjectCodeRefactoringProvider + : AbstractProjectExtensionProvider + { + protected override ImmutableArray GetLanguages(ExportCodeRefactoringProviderAttribute exportAttribute) + => exportAttribute.Languages.ToImmutableArray(); - private class ProjectCodeRefactoringProvider - : AbstractProjectExtensionProvider + protected override bool TryGetExtensionsFromReference(AnalyzerReference reference, out ImmutableArray extensions) { - protected override ImmutableArray GetLanguages(ExportCodeRefactoringProviderAttribute exportAttribute) - => exportAttribute.Languages.ToImmutableArray(); - - protected override bool TryGetExtensionsFromReference(AnalyzerReference reference, out ImmutableArray extensions) + // check whether the analyzer reference knows how to return fixers directly. + if (reference is ICodeRefactoringProviderFactory codeRefactoringProviderFactory) { - // check whether the analyzer reference knows how to return fixers directly. - if (reference is ICodeRefactoringProviderFactory codeRefactoringProviderFactory) - { - extensions = codeRefactoringProviderFactory.GetRefactorings(); - return true; - } - - extensions = default; - return false; + extensions = codeRefactoringProviderFactory.GetRefactorings(); + return true; } + + extensions = default; + return false; } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/ExtractMethod/AbstractExtractMethodCodeRefactoringProvider.cs b/src/Features/Core/Portable/CodeRefactorings/ExtractMethod/AbstractExtractMethodCodeRefactoringProvider.cs index 03be86e697fad..0a9847d7caba7 100644 --- a/src/Features/Core/Portable/CodeRefactorings/ExtractMethod/AbstractExtractMethodCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/CodeRefactorings/ExtractMethod/AbstractExtractMethodCodeRefactoringProvider.cs @@ -17,127 +17,126 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeRefactorings.ExtractMethod +namespace Microsoft.CodeAnalysis.CodeRefactorings.ExtractMethod; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, + Name = PredefinedCodeRefactoringProviderNames.ExtractMethod), Shared] +internal class ExtractMethodCodeRefactoringProvider : CodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, - Name = PredefinedCodeRefactoringProviderNames.ExtractMethod), Shared] - internal class ExtractMethodCodeRefactoringProvider : CodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public ExtractMethodCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public ExtractMethodCodeRefactoringProvider() - { - } + } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - // Don't bother if there isn't a selection - var (document, textSpan, cancellationToken) = context; - if (textSpan.IsEmpty) - return; + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + // Don't bother if there isn't a selection + var (document, textSpan, cancellationToken) = context; + if (textSpan.IsEmpty) + return; - var solution = document.Project.Solution; - if (solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) - return; + var solution = document.Project.Solution; + if (solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) + return; - var activeInlineRenameSession = solution.Services.GetService().ActiveInlineRenameSession; - if (activeInlineRenameSession) - return; + var activeInlineRenameSession = solution.Services.GetService().ActiveInlineRenameSession; + if (activeInlineRenameSession) + return; - if (cancellationToken.IsCancellationRequested) - return; + if (cancellationToken.IsCancellationRequested) + return; - var extractOptions = await document.GetExtractMethodGenerationOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); + var extractOptions = await document.GetExtractMethodGenerationOptionsAsync(context.Options, cancellationToken).ConfigureAwait(false); - var actions = await GetCodeActionsAsync(document, textSpan, extractOptions, cancellationToken).ConfigureAwait(false); - context.RegisterRefactorings(actions); - } + var actions = await GetCodeActionsAsync(document, textSpan, extractOptions, cancellationToken).ConfigureAwait(false); + context.RegisterRefactorings(actions); + } - private static async Task> GetCodeActionsAsync( - Document document, - TextSpan textSpan, - ExtractMethodGenerationOptions extractOptions, - CancellationToken cancellationToken) - { - using var _ = ArrayBuilder.GetInstance(out var actions); - var methodAction = await ExtractMethodAsync(document, textSpan, extractOptions, cancellationToken).ConfigureAwait(false); - actions.AddIfNotNull(methodAction); + private static async Task> GetCodeActionsAsync( + Document document, + TextSpan textSpan, + ExtractMethodGenerationOptions extractOptions, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var actions); + var methodAction = await ExtractMethodAsync(document, textSpan, extractOptions, cancellationToken).ConfigureAwait(false); + actions.AddIfNotNull(methodAction); - var localFunctionAction = await ExtractLocalFunctionAsync(document, textSpan, extractOptions, cancellationToken).ConfigureAwait(false); - actions.AddIfNotNull(localFunctionAction); + var localFunctionAction = await ExtractLocalFunctionAsync(document, textSpan, extractOptions, cancellationToken).ConfigureAwait(false); + actions.AddIfNotNull(localFunctionAction); - return actions.ToImmutable(); - } + return actions.ToImmutable(); + } + + private static async Task ExtractMethodAsync( + Document document, TextSpan textSpan, ExtractMethodGenerationOptions extractOptions, CancellationToken cancellationToken) + { + var result = await ExtractMethodService.ExtractMethodAsync( + document, + textSpan, + localFunction: false, + extractOptions, + cancellationToken).ConfigureAwait(false); - private static async Task ExtractMethodAsync( - Document document, TextSpan textSpan, ExtractMethodGenerationOptions extractOptions, CancellationToken cancellationToken) + Contract.ThrowIfNull(result); + + if (!result.Succeeded) + return null; + + return CodeAction.Create( + FeaturesResources.Extract_method, + async cancellationToken => + { + var (document, invocationNameToken) = await result.GetDocumentAsync(cancellationToken).ConfigureAwait(false); + return await AddRenameAnnotationAsync(document, invocationNameToken, cancellationToken).ConfigureAwait(false); + }, + nameof(FeaturesResources.Extract_method)); + } + + private static async Task ExtractLocalFunctionAsync( + Document document, TextSpan textSpan, ExtractMethodGenerationOptions extractOptions, CancellationToken cancellationToken) + { + var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetLanguageService(); + if (!syntaxFacts.SupportsLocalFunctionDeclaration(syntaxTree.Options)) { - var result = await ExtractMethodService.ExtractMethodAsync( - document, - textSpan, - localFunction: false, - extractOptions, - cancellationToken).ConfigureAwait(false); - - Contract.ThrowIfNull(result); - - if (!result.Succeeded) - return null; - - return CodeAction.Create( - FeaturesResources.Extract_method, - async cancellationToken => - { - var (document, invocationNameToken) = await result.GetDocumentAsync(cancellationToken).ConfigureAwait(false); - return await AddRenameAnnotationAsync(document, invocationNameToken, cancellationToken).ConfigureAwait(false); - }, - nameof(FeaturesResources.Extract_method)); + return null; } - private static async Task ExtractLocalFunctionAsync( - Document document, TextSpan textSpan, ExtractMethodGenerationOptions extractOptions, CancellationToken cancellationToken) - { - var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetLanguageService(); - if (!syntaxFacts.SupportsLocalFunctionDeclaration(syntaxTree.Options)) + var localFunctionResult = await ExtractMethodService.ExtractMethodAsync( + document, + textSpan, + localFunction: true, + extractOptions, + cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(localFunctionResult); + + if (!localFunctionResult.Succeeded) + return null; + + var codeAction = CodeAction.Create( + FeaturesResources.Extract_local_function, + async cancellationToken => { - return null; - } - - var localFunctionResult = await ExtractMethodService.ExtractMethodAsync( - document, - textSpan, - localFunction: true, - extractOptions, - cancellationToken).ConfigureAwait(false); - Contract.ThrowIfNull(localFunctionResult); - - if (!localFunctionResult.Succeeded) - return null; - - var codeAction = CodeAction.Create( - FeaturesResources.Extract_local_function, - async cancellationToken => - { - var (document, invocationNameToken) = await localFunctionResult.GetDocumentAsync(cancellationToken).ConfigureAwait(false); - return await AddRenameAnnotationAsync(document, invocationNameToken, cancellationToken).ConfigureAwait(false); - }, - nameof(FeaturesResources.Extract_local_function)); - return codeAction; - } + var (document, invocationNameToken) = await localFunctionResult.GetDocumentAsync(cancellationToken).ConfigureAwait(false); + return await AddRenameAnnotationAsync(document, invocationNameToken, cancellationToken).ConfigureAwait(false); + }, + nameof(FeaturesResources.Extract_local_function)); + return codeAction; + } - private static async Task AddRenameAnnotationAsync(Document document, SyntaxToken? invocationNameToken, CancellationToken cancellationToken) - { - if (invocationNameToken == null) - return document; + private static async Task AddRenameAnnotationAsync(Document document, SyntaxToken? invocationNameToken, CancellationToken cancellationToken) + { + if (invocationNameToken == null) + return document; - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var finalRoot = root.ReplaceToken( - invocationNameToken.Value, - invocationNameToken.Value.WithAdditionalAnnotations(RenameAnnotation.Create())); + var finalRoot = root.ReplaceToken( + invocationNameToken.Value, + invocationNameToken.Value.WithAdditionalAnnotations(RenameAnnotation.Create())); - return document.WithSyntaxRoot(finalRoot); - } + return document.WithSyntaxRoot(finalRoot); } } diff --git a/src/Features/Core/Portable/CodeRefactorings/FixAllOccurences/FixAllCodeRefactoringCodeAction.cs b/src/Features/Core/Portable/CodeRefactorings/FixAllOccurences/FixAllCodeRefactoringCodeAction.cs index 1c8751a6b3396..0dfddc83c1fcf 100644 --- a/src/Features/Core/Portable/CodeRefactorings/FixAllOccurences/FixAllCodeRefactoringCodeAction.cs +++ b/src/Features/Core/Portable/CodeRefactorings/FixAllOccurences/FixAllCodeRefactoringCodeAction.cs @@ -7,17 +7,16 @@ using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixesAndRefactorings; -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +/// +/// Fix all code action for a code action registered by a . +/// +internal sealed class FixAllCodeRefactoringCodeAction(IFixAllState fixAllState) : AbstractFixAllCodeAction(fixAllState, showPreviewChangesDialog: true) { - /// - /// Fix all code action for a code action registered by a . - /// - internal sealed class FixAllCodeRefactoringCodeAction(IFixAllState fixAllState) : AbstractFixAllCodeAction(fixAllState, showPreviewChangesDialog: true) - { - protected override IFixAllContext CreateFixAllContext(IFixAllState fixAllState, IProgress progressTracker, CancellationToken cancellationToken) - => new FixAllContext((FixAllState)fixAllState, progressTracker, cancellationToken); + protected override IFixAllContext CreateFixAllContext(IFixAllState fixAllState, IProgress progressTracker, CancellationToken cancellationToken) + => new FixAllContext((FixAllState)fixAllState, progressTracker, cancellationToken); - protected override bool IsInternalProvider(IFixAllState fixAllState) - => true; // FixAll for refactorings is currently only supported for internal code refactoring providers. - } + protected override bool IsInternalProvider(IFixAllState fixAllState) + => true; // FixAll for refactorings is currently only supported for internal code refactoring providers. } diff --git a/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringHelpersService.cs b/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringHelpersService.cs index 55113ddbc0a61..25e2b32922a3c 100644 --- a/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringHelpersService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringHelpersService.cs @@ -4,10 +4,9 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +internal interface ICodeRefactoringHelpersService : IWorkspaceService { - internal interface ICodeRefactoringHelpersService : IWorkspaceService - { - bool ActiveInlineRenameSession { get; } - } + bool ActiveInlineRenameSession { get; } } diff --git a/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringProviderFactory.cs b/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringProviderFactory.cs index 6c08e8d7d8017..05c5af6265fe3 100644 --- a/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringProviderFactory.cs +++ b/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringProviderFactory.cs @@ -6,10 +6,9 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +internal interface ICodeRefactoringProviderFactory { - internal interface ICodeRefactoringProviderFactory - { - ImmutableArray GetRefactorings(); - } + ImmutableArray GetRefactorings(); } diff --git a/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringService.cs b/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringService.cs index aa425370bf7fb..702b02c0cbe45 100644 --- a/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/ICodeRefactoringService.cs @@ -9,18 +9,17 @@ using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +internal interface ICodeRefactoringService { - internal interface ICodeRefactoringService - { - Task HasRefactoringsAsync(TextDocument document, TextSpan textSpan, CodeActionOptionsProvider options, CancellationToken cancellationToken); + Task HasRefactoringsAsync(TextDocument document, TextSpan textSpan, CodeActionOptionsProvider options, CancellationToken cancellationToken); - Task> GetRefactoringsAsync(TextDocument document, TextSpan textSpan, CodeActionRequestPriority? priority, CodeActionOptionsProvider options, Func addOperationScope, CancellationToken cancellationToken); - } + Task> GetRefactoringsAsync(TextDocument document, TextSpan textSpan, CodeActionRequestPriority? priority, CodeActionOptionsProvider options, Func addOperationScope, CancellationToken cancellationToken); +} - internal static class ICodeRefactoringServiceExtensions - { - public static Task> GetRefactoringsAsync(this ICodeRefactoringService service, TextDocument document, TextSpan state, CodeActionOptionsProvider options, CancellationToken cancellationToken) - => service.GetRefactoringsAsync(document, state, priority: null, options, addOperationScope: _ => null, cancellationToken); - } +internal static class ICodeRefactoringServiceExtensions +{ + public static Task> GetRefactoringsAsync(this ICodeRefactoringService service, TextDocument document, TextSpan state, CodeActionOptionsProvider options, CancellationToken cancellationToken) + => service.GetRefactoringsAsync(document, state, priority: null, options, addOperationScope: _ => null, cancellationToken); } diff --git a/src/Features/Core/Portable/CodeRefactorings/IRefactoringHelpersService.cs b/src/Features/Core/Portable/CodeRefactorings/IRefactoringHelpersService.cs index 5c56249dc1ba0..2e44fb25d4737 100644 --- a/src/Features/Core/Portable/CodeRefactorings/IRefactoringHelpersService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/IRefactoringHelpersService.cs @@ -10,58 +10,57 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +/// +/// Contains helpers related to asking intuitive semantic questions about a users intent +/// based on the position of their caret or span of their selection. +/// +internal interface IRefactoringHelpersService : IHeaderFactsService, ILanguageService { /// - /// Contains helpers related to asking intuitive semantic questions about a users intent - /// based on the position of their caret or span of their selection. + /// True if the user is on a blank line where a member could go inside a type declaration. + /// This will be between members and not ever inside a member. /// - internal interface IRefactoringHelpersService : IHeaderFactsService, ILanguageService - { - /// - /// True if the user is on a blank line where a member could go inside a type declaration. - /// This will be between members and not ever inside a member. - /// - bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration); + bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration); - /// - /// - /// Returns an array of instances for refactoring given specified selection - /// in document. determines if the returned nodes will can have empty spans - /// or not. - /// - /// - /// A instance is returned if: - Selection is zero-width and inside/touching - /// a Token with direct parent of type . - Selection is zero-width and - /// touching a Token whose ancestor of type ends/starts precisely on current - /// selection. - Selection is zero-width and in whitespace that corresponds to a Token whose direct ancestor is - /// of type of type . - Selection is zero-width and in a header (defined by - /// ISyntaxFacts helpers) of an node of type of type . - Token whose direct - /// parent of type is selected. - Selection is zero-width and wanted node is - /// an expression / argument with selection within such syntax node (arbitrarily deep) on its first line. - - /// Whole node of a type is selected. - /// - /// - /// Attempts extracting a Node of type for each Node it considers (see - /// above). E.g. extracts initializer expressions from declarations and assignments, Property declaration from - /// any header node, etc. - /// - /// - /// Note: this function trims all whitespace from both the beginning and the end of given . The trimmed version is then used to determine relevant . It also - /// handles incomplete selections of tokens gracefully. Over-selection containing leading comments is also - /// handled correctly. - /// - /// - Task> GetRelevantNodesAsync(Document document, TextSpan selection, bool allowEmptyNodes, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode; - } + /// + /// + /// Returns an array of instances for refactoring given specified selection + /// in document. determines if the returned nodes will can have empty spans + /// or not. + /// + /// + /// A instance is returned if: - Selection is zero-width and inside/touching + /// a Token with direct parent of type . - Selection is zero-width and + /// touching a Token whose ancestor of type ends/starts precisely on current + /// selection. - Selection is zero-width and in whitespace that corresponds to a Token whose direct ancestor is + /// of type of type . - Selection is zero-width and in a header (defined by + /// ISyntaxFacts helpers) of an node of type of type . - Token whose direct + /// parent of type is selected. - Selection is zero-width and wanted node is + /// an expression / argument with selection within such syntax node (arbitrarily deep) on its first line. - + /// Whole node of a type is selected. + /// + /// + /// Attempts extracting a Node of type for each Node it considers (see + /// above). E.g. extracts initializer expressions from declarations and assignments, Property declaration from + /// any header node, etc. + /// + /// + /// Note: this function trims all whitespace from both the beginning and the end of given . The trimmed version is then used to determine relevant . It also + /// handles incomplete selections of tokens gracefully. Over-selection containing leading comments is also + /// handled correctly. + /// + /// + Task> GetRelevantNodesAsync(Document document, TextSpan selection, bool allowEmptyNodes, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode; +} - internal static class IRefactoringHelpersServiceExtensions +internal static class IRefactoringHelpersServiceExtensions +{ + public static Task> GetRelevantNodesAsync( + this IRefactoringHelpersService service, Document document, TextSpan selection, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode { - public static Task> GetRelevantNodesAsync( - this IRefactoringHelpersService service, Document document, TextSpan selection, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - return service.GetRelevantNodesAsync(document, selection, allowEmptyNodes: false, cancellationToken); - } + return service.GetRelevantNodesAsync(document, selection, allowEmptyNodes: false, cancellationToken); } } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.Editor.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.Editor.cs index 239a2dd3ebdeb..0d2c915fd1e18 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.Editor.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.Editor.cs @@ -11,54 +11,53 @@ using Microsoft.CodeAnalysis.Formatting; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType +namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType; + +internal abstract partial class AbstractMoveTypeService { - internal abstract partial class AbstractMoveTypeService + /// + /// An abstract class for different edits performed by the Move Type Code Action. + /// + private abstract class Editor( + TService service, + State state, + string fileName, + CancellationToken cancellationToken) { + protected State State { get; } = state; + protected TService Service { get; } = service; + protected string FileName { get; } = fileName; + protected CancellationToken CancellationToken { get; } = cancellationToken; + protected SemanticDocument SemanticDocument => State.SemanticDocument; + /// - /// An abstract class for different edits performed by the Move Type Code Action. + /// Operations performed by CodeAction. /// - private abstract class Editor( - TService service, - State state, - string fileName, - CancellationToken cancellationToken) + public virtual async Task> GetOperationsAsync() { - protected State State { get; } = state; - protected TService Service { get; } = service; - protected string FileName { get; } = fileName; - protected CancellationToken CancellationToken { get; } = cancellationToken; - protected SemanticDocument SemanticDocument => State.SemanticDocument; + var solution = await GetModifiedSolutionAsync().ConfigureAwait(false); - /// - /// Operations performed by CodeAction. - /// - public virtual async Task> GetOperationsAsync() + if (solution == null) { - var solution = await GetModifiedSolutionAsync().ConfigureAwait(false); - - if (solution == null) - { - return []; - } - - return [new ApplyChangesOperation(solution)]; + return []; } - /// - /// Incremental solution edits that correlate to code operations - /// - public abstract Task GetModifiedSolutionAsync(); - - public static Editor GetEditor(MoveTypeOperationKind operationKind, TService service, State state, string fileName, CancellationToken cancellationToken) - => operationKind switch - { - MoveTypeOperationKind.MoveType => new MoveTypeEditor(service, state, fileName, cancellationToken), - MoveTypeOperationKind.RenameType => new RenameTypeEditor(service, state, fileName, cancellationToken), - MoveTypeOperationKind.RenameFile => new RenameFileEditor(service, state, fileName, cancellationToken), - MoveTypeOperationKind.MoveTypeNamespaceScope => new MoveTypeNamespaceScopeEditor(service, state, fileName, cancellationToken), - _ => throw ExceptionUtilities.UnexpectedValue(operationKind), - }; + return [new ApplyChangesOperation(solution)]; } + + /// + /// Incremental solution edits that correlate to code operations + /// + public abstract Task GetModifiedSolutionAsync(); + + public static Editor GetEditor(MoveTypeOperationKind operationKind, TService service, State state, string fileName, CancellationToken cancellationToken) + => operationKind switch + { + MoveTypeOperationKind.MoveType => new MoveTypeEditor(service, state, fileName, cancellationToken), + MoveTypeOperationKind.RenameType => new RenameTypeEditor(service, state, fileName, cancellationToken), + MoveTypeOperationKind.RenameFile => new RenameFileEditor(service, state, fileName, cancellationToken), + MoveTypeOperationKind.MoveTypeNamespaceScope => new MoveTypeNamespaceScopeEditor(service, state, fileName, cancellationToken), + _ => throw ExceptionUtilities.UnexpectedValue(operationKind), + }; } } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeCodeAction.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeCodeAction.cs index 4446494137401..d7115a4c4e75b 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeCodeAction.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeCodeAction.cs @@ -11,49 +11,48 @@ using Microsoft.CodeAnalysis.CodeActions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType +namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType; + +internal abstract partial class AbstractMoveTypeService { - internal abstract partial class AbstractMoveTypeService + private sealed class MoveTypeCodeAction : CodeAction { - private sealed class MoveTypeCodeAction : CodeAction + private readonly State _state; + private readonly TService _service; + private readonly MoveTypeOperationKind _operationKind; + private readonly string _title; + private readonly string _fileName; + + public MoveTypeCodeAction( + TService service, + State state, + MoveTypeOperationKind operationKind, + string fileName) { - private readonly State _state; - private readonly TService _service; - private readonly MoveTypeOperationKind _operationKind; - private readonly string _title; - private readonly string _fileName; - - public MoveTypeCodeAction( - TService service, - State state, - MoveTypeOperationKind operationKind, - string fileName) - { - _state = state; - _service = service; - _operationKind = operationKind; - _fileName = fileName; - _title = CreateDisplayText(); - } - - private string CreateDisplayText() - => _operationKind switch - { - MoveTypeOperationKind.MoveType => string.Format(FeaturesResources.Move_type_to_0, _fileName), - MoveTypeOperationKind.RenameType => string.Format(FeaturesResources.Rename_type_to_0, _state.DocumentNameWithoutExtension), - MoveTypeOperationKind.RenameFile => string.Format(FeaturesResources.Rename_file_to_0, _fileName), - MoveTypeOperationKind.MoveTypeNamespaceScope => string.Empty, - _ => throw ExceptionUtilities.UnexpectedValue(_operationKind), - }; - - public override string Title => _title; - - protected override async Task> ComputeOperationsAsync( - IProgress progress, CancellationToken cancellationToken) + _state = state; + _service = service; + _operationKind = operationKind; + _fileName = fileName; + _title = CreateDisplayText(); + } + + private string CreateDisplayText() + => _operationKind switch { - var editor = Editor.GetEditor(_operationKind, _service, _state, _fileName, cancellationToken); - return await editor.GetOperationsAsync().ConfigureAwait(false); - } + MoveTypeOperationKind.MoveType => string.Format(FeaturesResources.Move_type_to_0, _fileName), + MoveTypeOperationKind.RenameType => string.Format(FeaturesResources.Rename_type_to_0, _state.DocumentNameWithoutExtension), + MoveTypeOperationKind.RenameFile => string.Format(FeaturesResources.Rename_file_to_0, _fileName), + MoveTypeOperationKind.MoveTypeNamespaceScope => string.Empty, + _ => throw ExceptionUtilities.UnexpectedValue(_operationKind), + }; + + public override string Title => _title; + + protected override async Task> ComputeOperationsAsync( + IProgress progress, CancellationToken cancellationToken) + { + var editor = Editor.GetEditor(_operationKind, _service, _state, _fileName, cancellationToken); + return await editor.GetOperationsAsync().ConfigureAwait(false); } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeEditor.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeEditor.cs index 1356e16159010..1cf7f7b7d46e5 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeEditor.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeEditor.cs @@ -17,297 +17,296 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType +namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType; + +internal abstract partial class AbstractMoveTypeService { - internal abstract partial class AbstractMoveTypeService + private sealed class MoveTypeEditor( + TService service, + State state, + string fileName, + CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken) { - private sealed class MoveTypeEditor( - TService service, - State state, - string fileName, - CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken) + + /// + /// Given a document and a type contained in it, moves the type + /// out to its own document. The new document's name typically + /// is the type name, or is at least based on the type name. + /// + /// + /// The algorithm for this, is as follows: + /// 1. Fork the original document that contains the type to be moved. + /// 2. Keep the type, required namespace containers and using statements. + /// remove everything else from the forked document. + /// 3. Add this forked document to the solution. + /// 4. Finally, update the original document and remove the type from it. + /// + public override async Task GetModifiedSolutionAsync() { + // Fork, update and add as new document. + var projectToBeUpdated = SemanticDocument.Document.Project; + var newDocumentId = DocumentId.CreateNewId(projectToBeUpdated.Id, FileName); - /// - /// Given a document and a type contained in it, moves the type - /// out to its own document. The new document's name typically - /// is the type name, or is at least based on the type name. - /// - /// - /// The algorithm for this, is as follows: - /// 1. Fork the original document that contains the type to be moved. - /// 2. Keep the type, required namespace containers and using statements. - /// remove everything else from the forked document. - /// 3. Add this forked document to the solution. - /// 4. Finally, update the original document and remove the type from it. - /// - public override async Task GetModifiedSolutionAsync() - { - // Fork, update and add as new document. - var projectToBeUpdated = SemanticDocument.Document.Project; - var newDocumentId = DocumentId.CreateNewId(projectToBeUpdated.Id, FileName); + // We do this process in the following steps: + // + // 1. Produce the new document, with the moved type, with all the same imports as the original file. + // 2. Remove the original type from the first document, not touching the imports in it. This is + // necessary to prevent duplicate symbol errors (and other compiler issues) as we process imports. + // 3. Now that the type has been moved to the new file, remove the unnecessary imports from the new + // file. This will also tell us which imports are necessary in the new file. + // 4. Now go back to the original file and remove any unnecessary imports *if* they are in the new file. + // these imports only were needed for the moved type, and so they shouldn't stay in the original + // file. - // We do this process in the following steps: - // - // 1. Produce the new document, with the moved type, with all the same imports as the original file. - // 2. Remove the original type from the first document, not touching the imports in it. This is - // necessary to prevent duplicate symbol errors (and other compiler issues) as we process imports. - // 3. Now that the type has been moved to the new file, remove the unnecessary imports from the new - // file. This will also tell us which imports are necessary in the new file. - // 4. Now go back to the original file and remove any unnecessary imports *if* they are in the new file. - // these imports only were needed for the moved type, and so they shouldn't stay in the original - // file. + var documentWithMovedType = await AddNewDocumentWithSingleTypeDeclarationAsync(newDocumentId).ConfigureAwait(false); - var documentWithMovedType = await AddNewDocumentWithSingleTypeDeclarationAsync(newDocumentId).ConfigureAwait(false); + var solutionWithNewDocument = documentWithMovedType.Project.Solution; - var solutionWithNewDocument = documentWithMovedType.Project.Solution; + // Get the original source document again, from the latest forked solution. + var sourceDocument = solutionWithNewDocument.GetRequiredDocument(SemanticDocument.Document.Id); - // Get the original source document again, from the latest forked solution. - var sourceDocument = solutionWithNewDocument.GetRequiredDocument(SemanticDocument.Document.Id); + // update source document to add partial modifiers to type chain + // and/or remove type declaration from original source document. + var solutionWithBothDocumentsUpdated = await RemoveTypeFromSourceDocumentAsync(sourceDocument).ConfigureAwait(false); - // update source document to add partial modifiers to type chain - // and/or remove type declaration from original source document. - var solutionWithBothDocumentsUpdated = await RemoveTypeFromSourceDocumentAsync(sourceDocument).ConfigureAwait(false); + return await RemoveUnnecessaryImportsAsync(solutionWithBothDocumentsUpdated, sourceDocument.Id, documentWithMovedType.Id).ConfigureAwait(false); + } - return await RemoveUnnecessaryImportsAsync(solutionWithBothDocumentsUpdated, sourceDocument.Id, documentWithMovedType.Id).ConfigureAwait(false); - } + private async Task RemoveUnnecessaryImportsAsync( + Solution solution, DocumentId sourceDocumentId, DocumentId documentWithMovedTypeId) + { + var documentWithMovedType = solution.GetRequiredDocument(documentWithMovedTypeId); + var documentWithMovedTypeFormattingOptions = await documentWithMovedType.GetSyntaxFormattingOptionsAsync(State.FallbackOptions, CancellationToken).ConfigureAwait(false); + + var syntaxFacts = documentWithMovedType.GetRequiredLanguageService(); + var removeUnnecessaryImports = documentWithMovedType.GetRequiredLanguageService(); + + // Remove all unnecessary imports from the new document we've created. + documentWithMovedType = await removeUnnecessaryImports.RemoveUnnecessaryImportsAsync(documentWithMovedType, documentWithMovedTypeFormattingOptions, CancellationToken).ConfigureAwait(false); + + solution = solution.WithDocumentSyntaxRoot( + documentWithMovedTypeId, await documentWithMovedType.GetRequiredSyntaxRootAsync(CancellationToken).ConfigureAwait(false)); + + // See which imports we kept around. + var rootWithMovedType = await documentWithMovedType.GetRequiredSyntaxRootAsync(CancellationToken).ConfigureAwait(false); + var movedImports = rootWithMovedType.DescendantNodes() + .Where(syntaxFacts.IsUsingOrExternOrImport) + .ToImmutableArray(); + + // Now remove any unnecessary imports from the original doc that moved to the new doc. + var sourceDocument = solution.GetRequiredDocument(sourceDocumentId); + var sourceDocumentFormattingOptions = await sourceDocument.GetSyntaxFormattingOptionsAsync(State.FallbackOptions, CancellationToken).ConfigureAwait(false); + sourceDocument = await removeUnnecessaryImports.RemoveUnnecessaryImportsAsync( + sourceDocument, + n => movedImports.Contains(i => syntaxFacts.AreEquivalent(i, n)), + sourceDocumentFormattingOptions, + CancellationToken).ConfigureAwait(false); + + return solution.WithDocumentSyntaxRoot( + sourceDocumentId, await sourceDocument.GetRequiredSyntaxRootAsync(CancellationToken).ConfigureAwait(false)); + } - private async Task RemoveUnnecessaryImportsAsync( - Solution solution, DocumentId sourceDocumentId, DocumentId documentWithMovedTypeId) - { - var documentWithMovedType = solution.GetRequiredDocument(documentWithMovedTypeId); - var documentWithMovedTypeFormattingOptions = await documentWithMovedType.GetSyntaxFormattingOptionsAsync(State.FallbackOptions, CancellationToken).ConfigureAwait(false); - - var syntaxFacts = documentWithMovedType.GetRequiredLanguageService(); - var removeUnnecessaryImports = documentWithMovedType.GetRequiredLanguageService(); - - // Remove all unnecessary imports from the new document we've created. - documentWithMovedType = await removeUnnecessaryImports.RemoveUnnecessaryImportsAsync(documentWithMovedType, documentWithMovedTypeFormattingOptions, CancellationToken).ConfigureAwait(false); - - solution = solution.WithDocumentSyntaxRoot( - documentWithMovedTypeId, await documentWithMovedType.GetRequiredSyntaxRootAsync(CancellationToken).ConfigureAwait(false)); - - // See which imports we kept around. - var rootWithMovedType = await documentWithMovedType.GetRequiredSyntaxRootAsync(CancellationToken).ConfigureAwait(false); - var movedImports = rootWithMovedType.DescendantNodes() - .Where(syntaxFacts.IsUsingOrExternOrImport) - .ToImmutableArray(); - - // Now remove any unnecessary imports from the original doc that moved to the new doc. - var sourceDocument = solution.GetRequiredDocument(sourceDocumentId); - var sourceDocumentFormattingOptions = await sourceDocument.GetSyntaxFormattingOptionsAsync(State.FallbackOptions, CancellationToken).ConfigureAwait(false); - sourceDocument = await removeUnnecessaryImports.RemoveUnnecessaryImportsAsync( - sourceDocument, - n => movedImports.Contains(i => syntaxFacts.AreEquivalent(i, n)), - sourceDocumentFormattingOptions, - CancellationToken).ConfigureAwait(false); - - return solution.WithDocumentSyntaxRoot( - sourceDocumentId, await sourceDocument.GetRequiredSyntaxRootAsync(CancellationToken).ConfigureAwait(false)); - } + /// + /// Forks the source document, keeps required type, namespace containers + /// and adds it the solution. + /// + /// id for the new document to be added + private async Task AddNewDocumentWithSingleTypeDeclarationAsync(DocumentId newDocumentId) + { + var document = SemanticDocument.Document; + Debug.Assert(document.Name != FileName, + $"New document name is same as old document name:{FileName}"); + + var root = SemanticDocument.Root; + var projectToBeUpdated = document.Project; + var documentEditor = await DocumentEditor.CreateAsync(document, CancellationToken).ConfigureAwait(false); + + // Make the type chain above this new type partial. Also, remove any + // attributes from the containing partial types. We don't want to create + // duplicate attributes on things. + AddPartialModifiersToTypeChain( + documentEditor, removeAttributesAndComments: true, removeTypeInheritance: true); + + // remove things that are not being moved, from the forked document. + var membersToRemove = GetMembersToRemove(root); + foreach (var member in membersToRemove) + documentEditor.RemoveNode(member, SyntaxRemoveOptions.KeepNoTrivia); + + // Remove attributes from the root node as well, since those will apply as AttributeTarget.Assembly and + // don't need to be specified multiple times + documentEditor.RemoveAllAttributes(root); + + var modifiedRoot = documentEditor.GetChangedRoot(); + modifiedRoot = await AddFinalNewLineIfDesiredAsync(document, modifiedRoot).ConfigureAwait(false); + + // add an empty document to solution, so that we'll have options from the right context. + var solutionWithNewDocument = projectToBeUpdated.Solution.AddDocument( + newDocumentId, FileName, text: string.Empty, folders: document.Folders); + + // update the text for the new document + solutionWithNewDocument = solutionWithNewDocument.WithDocumentSyntaxRoot(newDocumentId, modifiedRoot, PreservationMode.PreserveIdentity); + + // get the updated document, give it the minimal set of imports that the type + // inside it needs. + var newDocument = solutionWithNewDocument.GetRequiredDocument(newDocumentId); + return newDocument; + } - /// - /// Forks the source document, keeps required type, namespace containers - /// and adds it the solution. - /// - /// id for the new document to be added - private async Task AddNewDocumentWithSingleTypeDeclarationAsync(DocumentId newDocumentId) + /// + /// Add a trailing newline if we don't already have one if that's what the user's + /// preference is. + /// + private async Task AddFinalNewLineIfDesiredAsync(Document document, SyntaxNode modifiedRoot) + { + var documentFormattingOptions = await document.GetDocumentFormattingOptionsAsync(State.FallbackOptions, CancellationToken).ConfigureAwait(false); + var insertFinalNewLine = documentFormattingOptions.InsertFinalNewLine; + if (insertFinalNewLine) { - var document = SemanticDocument.Document; - Debug.Assert(document.Name != FileName, - $"New document name is same as old document name:{FileName}"); - - var root = SemanticDocument.Root; - var projectToBeUpdated = document.Project; - var documentEditor = await DocumentEditor.CreateAsync(document, CancellationToken).ConfigureAwait(false); - - // Make the type chain above this new type partial. Also, remove any - // attributes from the containing partial types. We don't want to create - // duplicate attributes on things. - AddPartialModifiersToTypeChain( - documentEditor, removeAttributesAndComments: true, removeTypeInheritance: true); - - // remove things that are not being moved, from the forked document. - var membersToRemove = GetMembersToRemove(root); - foreach (var member in membersToRemove) - documentEditor.RemoveNode(member, SyntaxRemoveOptions.KeepNoTrivia); - - // Remove attributes from the root node as well, since those will apply as AttributeTarget.Assembly and - // don't need to be specified multiple times - documentEditor.RemoveAllAttributes(root); - - var modifiedRoot = documentEditor.GetChangedRoot(); - modifiedRoot = await AddFinalNewLineIfDesiredAsync(document, modifiedRoot).ConfigureAwait(false); - - // add an empty document to solution, so that we'll have options from the right context. - var solutionWithNewDocument = projectToBeUpdated.Solution.AddDocument( - newDocumentId, FileName, text: string.Empty, folders: document.Folders); - - // update the text for the new document - solutionWithNewDocument = solutionWithNewDocument.WithDocumentSyntaxRoot(newDocumentId, modifiedRoot, PreservationMode.PreserveIdentity); - - // get the updated document, give it the minimal set of imports that the type - // inside it needs. - var newDocument = solutionWithNewDocument.GetRequiredDocument(newDocumentId); - return newDocument; - } + var endOfFileToken = ((ICompilationUnitSyntax)modifiedRoot).EndOfFileToken; + var previousToken = endOfFileToken.GetPreviousToken(includeZeroWidth: true, includeSkipped: true); - /// - /// Add a trailing newline if we don't already have one if that's what the user's - /// preference is. - /// - private async Task AddFinalNewLineIfDesiredAsync(Document document, SyntaxNode modifiedRoot) - { - var documentFormattingOptions = await document.GetDocumentFormattingOptionsAsync(State.FallbackOptions, CancellationToken).ConfigureAwait(false); - var insertFinalNewLine = documentFormattingOptions.InsertFinalNewLine; - if (insertFinalNewLine) + var syntaxFacts = document.GetRequiredLanguageService(); + if (endOfFileToken.LeadingTrivia.IsEmpty() && + !previousToken.TrailingTrivia.Any(syntaxFacts.IsEndOfLineTrivia)) { - var endOfFileToken = ((ICompilationUnitSyntax)modifiedRoot).EndOfFileToken; - var previousToken = endOfFileToken.GetPreviousToken(includeZeroWidth: true, includeSkipped: true); - - var syntaxFacts = document.GetRequiredLanguageService(); - if (endOfFileToken.LeadingTrivia.IsEmpty() && - !previousToken.TrailingTrivia.Any(syntaxFacts.IsEndOfLineTrivia)) - { - var lineFormattingOptions = await document.GetLineFormattingOptionsAsync(State.FallbackOptions, CancellationToken).ConfigureAwait(false); - var generator = document.GetRequiredLanguageService(); - var endOfLine = generator.EndOfLine(lineFormattingOptions.NewLine); - return modifiedRoot.ReplaceToken( - previousToken, previousToken.WithAppendedTrailingTrivia(endOfLine)); - } + var lineFormattingOptions = await document.GetLineFormattingOptionsAsync(State.FallbackOptions, CancellationToken).ConfigureAwait(false); + var generator = document.GetRequiredLanguageService(); + var endOfLine = generator.EndOfLine(lineFormattingOptions.NewLine); + return modifiedRoot.ReplaceToken( + previousToken, previousToken.WithAppendedTrailingTrivia(endOfLine)); } - - return modifiedRoot; } - /// - /// update the original document and remove the type that was moved. - /// perform other fix ups as necessary. - /// - /// an updated solution with the original document fixed up as appropriate. - private async Task RemoveTypeFromSourceDocumentAsync(Document sourceDocument) - { - var documentEditor = await DocumentEditor.CreateAsync(sourceDocument, CancellationToken).ConfigureAwait(false); + return modifiedRoot; + } - // Make the type chain above the type we're moving 'partial'. - // However, keep all the attributes on these types as theses are the - // original attributes and we don't want to mess with them. - AddPartialModifiersToTypeChain(documentEditor, - removeAttributesAndComments: false, removeTypeInheritance: false); - documentEditor.RemoveNode(State.TypeNode, SyntaxRemoveOptions.KeepUnbalancedDirectives); + /// + /// update the original document and remove the type that was moved. + /// perform other fix ups as necessary. + /// + /// an updated solution with the original document fixed up as appropriate. + private async Task RemoveTypeFromSourceDocumentAsync(Document sourceDocument) + { + var documentEditor = await DocumentEditor.CreateAsync(sourceDocument, CancellationToken).ConfigureAwait(false); - var updatedDocument = documentEditor.GetChangedDocument(); + // Make the type chain above the type we're moving 'partial'. + // However, keep all the attributes on these types as theses are the + // original attributes and we don't want to mess with them. + AddPartialModifiersToTypeChain(documentEditor, + removeAttributesAndComments: false, removeTypeInheritance: false); + documentEditor.RemoveNode(State.TypeNode, SyntaxRemoveOptions.KeepUnbalancedDirectives); - return updatedDocument.Project.Solution; - } + var updatedDocument = documentEditor.GetChangedDocument(); - /// - /// Traverses the syntax tree of the forked document and - /// collects a list of nodes that are not being moved. - /// This list of nodes are then removed from the forked copy. - /// - /// root, of the syntax tree of forked document - /// list of syntax nodes, to be removed from the forked copy. - private ISet GetMembersToRemove(SyntaxNode root) - { - var spine = new HashSet(); + return updatedDocument.Project.Solution; + } + + /// + /// Traverses the syntax tree of the forked document and + /// collects a list of nodes that are not being moved. + /// This list of nodes are then removed from the forked copy. + /// + /// root, of the syntax tree of forked document + /// list of syntax nodes, to be removed from the forked copy. + private ISet GetMembersToRemove(SyntaxNode root) + { + var spine = new HashSet(); - // collect the parent chain of declarations to keep. - spine.AddRange(State.TypeNode.GetAncestors()); + // collect the parent chain of declarations to keep. + spine.AddRange(State.TypeNode.GetAncestors()); - // get potential namespace, types and members to remove. - var removableCandidates = root - .DescendantNodes(spine.Contains) - .Where(n => FilterToTopLevelMembers(n, State.TypeNode)).ToSet(); + // get potential namespace, types and members to remove. + var removableCandidates = root + .DescendantNodes(spine.Contains) + .Where(n => FilterToTopLevelMembers(n, State.TypeNode)).ToSet(); - // diff candidates with items we want to keep. - removableCandidates.ExceptWith(spine); + // diff candidates with items we want to keep. + removableCandidates.ExceptWith(spine); #if DEBUG - // None of the nodes we're removing should also have any of their parent - // nodes removed. If that happened we could get a crash by first trying to remove - // the parent, then trying to remove the child. - foreach (var node in removableCandidates) + // None of the nodes we're removing should also have any of their parent + // nodes removed. If that happened we could get a crash by first trying to remove + // the parent, then trying to remove the child. + foreach (var node in removableCandidates) + { + foreach (var ancestor in node.GetAncestors()) { - foreach (var ancestor in node.GetAncestors()) - { - Debug.Assert(!removableCandidates.Contains(ancestor)); - } + Debug.Assert(!removableCandidates.Contains(ancestor)); } + } #endif - return removableCandidates; + return removableCandidates; + } + + private static bool FilterToTopLevelMembers(SyntaxNode node, SyntaxNode typeNode) + { + // We never filter out the actual node we're trying to keep around. + if (node == typeNode) + { + return false; } - private static bool FilterToTopLevelMembers(SyntaxNode node, SyntaxNode typeNode) + return node is TTypeDeclarationSyntax or + TMemberDeclarationSyntax or + TNamespaceDeclarationSyntax; + } + + /// + /// if a nested type is being moved, this ensures its containing type is partial. + /// + private void AddPartialModifiersToTypeChain( + DocumentEditor documentEditor, + bool removeAttributesAndComments, + bool removeTypeInheritance) + { + var semanticFacts = State.SemanticDocument.Document.GetRequiredLanguageService(); + var typeChain = State.TypeNode.Ancestors().OfType(); + + foreach (var node in typeChain) { - // We never filter out the actual node we're trying to keep around. - if (node == typeNode) + var symbol = (INamedTypeSymbol?)State.SemanticDocument.SemanticModel.GetDeclaredSymbol(node, CancellationToken); + Contract.ThrowIfNull(symbol); + if (!semanticFacts.IsPartial(symbol, CancellationToken)) { - return false; + documentEditor.SetModifiers(node, + documentEditor.Generator.GetModifiers(node) | DeclarationModifiers.Partial); } - return node is TTypeDeclarationSyntax or - TMemberDeclarationSyntax or - TNamespaceDeclarationSyntax; - } - - /// - /// if a nested type is being moved, this ensures its containing type is partial. - /// - private void AddPartialModifiersToTypeChain( - DocumentEditor documentEditor, - bool removeAttributesAndComments, - bool removeTypeInheritance) - { - var semanticFacts = State.SemanticDocument.Document.GetRequiredLanguageService(); - var typeChain = State.TypeNode.Ancestors().OfType(); + if (removeAttributesAndComments) + { + documentEditor.RemoveAllAttributes(node); + documentEditor.RemoveAllComments(node); + } - foreach (var node in typeChain) + if (removeTypeInheritance) { - var symbol = (INamedTypeSymbol?)State.SemanticDocument.SemanticModel.GetDeclaredSymbol(node, CancellationToken); - Contract.ThrowIfNull(symbol); - if (!semanticFacts.IsPartial(symbol, CancellationToken)) - { - documentEditor.SetModifiers(node, - documentEditor.Generator.GetModifiers(node) | DeclarationModifiers.Partial); - } - - if (removeAttributesAndComments) - { - documentEditor.RemoveAllAttributes(node); - documentEditor.RemoveAllComments(node); - } - - if (removeTypeInheritance) - { - documentEditor.RemoveAllTypeInheritance(node); - } + documentEditor.RemoveAllTypeInheritance(node); } + } - documentEditor.ReplaceNode(State.TypeNode, - (currentNode, generator) => - { - var currentTypeNode = (TTypeDeclarationSyntax)currentNode; + documentEditor.ReplaceNode(State.TypeNode, + (currentNode, generator) => + { + var currentTypeNode = (TTypeDeclarationSyntax)currentNode; - // Trim leading blank lines from the type so we don't have an - // excessive number of them. - return RemoveLeadingBlankLines(currentTypeNode); - }); - } + // Trim leading blank lines from the type so we don't have an + // excessive number of them. + return RemoveLeadingBlankLines(currentTypeNode); + }); + } - private TTypeDeclarationSyntax RemoveLeadingBlankLines( - TTypeDeclarationSyntax currentTypeNode) - { - var syntaxFacts = State.SemanticDocument.Document.GetRequiredLanguageService(); - var bannerService = State.SemanticDocument.Document.GetRequiredLanguageService(); + private TTypeDeclarationSyntax RemoveLeadingBlankLines( + TTypeDeclarationSyntax currentTypeNode) + { + var syntaxFacts = State.SemanticDocument.Document.GetRequiredLanguageService(); + var bannerService = State.SemanticDocument.Document.GetRequiredLanguageService(); - var withoutBlankLines = bannerService.GetNodeWithoutLeadingBlankLines(currentTypeNode); + var withoutBlankLines = bannerService.GetNodeWithoutLeadingBlankLines(currentTypeNode); - // Add an elastic marker so the formatter can add any blank lines it thinks are - // important to have (i.e. after a block of usings/imports). - return withoutBlankLines.WithPrependedLeadingTrivia(syntaxFacts.ElasticMarker); - } + // Add an elastic marker so the formatter can add any blank lines it thinks are + // important to have (i.e. after a block of usings/imports). + return withoutBlankLines.WithPrependedLeadingTrivia(syntaxFacts.ElasticMarker); } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeNamespaceScopeEditor.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeNamespaceScopeEditor.cs index e4f2fff578a7d..e7ee173d5ecab 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeNamespaceScopeEditor.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.MoveTypeNamespaceScopeEditor.cs @@ -12,126 +12,124 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType -{ +namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType; - internal abstract partial class AbstractMoveTypeService +internal abstract partial class AbstractMoveTypeService +{ + /// + /// Editor that takes a type in a scope and creates a scope beside it. For example, if the type is contained within a namespace + /// it will evaluate if the namespace scope needs to be closed and reopened to create a new scope. + /// + private class MoveTypeNamespaceScopeEditor(TService service, State state, string fileName, CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken) { - /// - /// Editor that takes a type in a scope and creates a scope beside it. For example, if the type is contained within a namespace - /// it will evaluate if the namespace scope needs to be closed and reopened to create a new scope. - /// - private class MoveTypeNamespaceScopeEditor(TService service, State state, string fileName, CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken) + public override async Task GetModifiedSolutionAsync() { - public override async Task GetModifiedSolutionAsync() + var node = State.TypeNode; + var documentToEdit = State.SemanticDocument.Document; + + if (node.Parent is TNamespaceDeclarationSyntax namespaceDeclaration) { - var node = State.TypeNode; - var documentToEdit = State.SemanticDocument.Document; + return await GetNamespaceScopeChangedSolutionAsync(namespaceDeclaration, node, documentToEdit, CancellationToken).ConfigureAwait(false); + } - if (node.Parent is TNamespaceDeclarationSyntax namespaceDeclaration) - { - return await GetNamespaceScopeChangedSolutionAsync(namespaceDeclaration, node, documentToEdit, CancellationToken).ConfigureAwait(false); - } + return null; + } - return null; - } + private static async Task GetNamespaceScopeChangedSolutionAsync( + TNamespaceDeclarationSyntax namespaceDeclaration, + TTypeDeclarationSyntax typeToMove, + Document documentToEdit, + CancellationToken cancellationToken) + { + var syntaxFactsService = documentToEdit.GetLanguageService(); + var childNodes = syntaxFactsService.GetMembersOfBaseNamespaceDeclaration(namespaceDeclaration); - private static async Task GetNamespaceScopeChangedSolutionAsync( - TNamespaceDeclarationSyntax namespaceDeclaration, - TTypeDeclarationSyntax typeToMove, - Document documentToEdit, - CancellationToken cancellationToken) + if (childNodes.Count <= 1) { - var syntaxFactsService = documentToEdit.GetLanguageService(); - var childNodes = syntaxFactsService.GetMembersOfBaseNamespaceDeclaration(namespaceDeclaration); + return null; + } - if (childNodes.Count <= 1) - { - return null; - } + var editor = await DocumentEditor.CreateAsync(documentToEdit, cancellationToken).ConfigureAwait(false); + editor.RemoveNode(typeToMove, SyntaxRemoveOptions.KeepNoTrivia); - var editor = await DocumentEditor.CreateAsync(documentToEdit, cancellationToken).ConfigureAwait(false); - editor.RemoveNode(typeToMove, SyntaxRemoveOptions.KeepNoTrivia); + var syntaxGenerator = editor.Generator; + var index = childNodes.IndexOf(typeToMove); - var syntaxGenerator = editor.Generator; - var index = childNodes.IndexOf(typeToMove); + var itemsBefore = index > 0 ? childNodes.Take(index) : []; + var itemsAfter = index < childNodes.Count - 1 ? childNodes.Skip(index + 1) : []; - var itemsBefore = index > 0 ? childNodes.Take(index) : []; - var itemsAfter = index < childNodes.Count - 1 ? childNodes.Skip(index + 1) : []; + var name = syntaxFactsService.GetDisplayName(namespaceDeclaration, DisplayNameOptions.IncludeNamespaces); + var newNamespaceDeclaration = syntaxGenerator.NamespaceDeclaration(name, WithElasticTrivia(typeToMove)).WithAdditionalAnnotations(NamespaceScopeMovedAnnotation); - var name = syntaxFactsService.GetDisplayName(namespaceDeclaration, DisplayNameOptions.IncludeNamespaces); - var newNamespaceDeclaration = syntaxGenerator.NamespaceDeclaration(name, WithElasticTrivia(typeToMove)).WithAdditionalAnnotations(NamespaceScopeMovedAnnotation); + if (itemsBefore.Any() && itemsAfter.Any()) + { + var itemsAfterNamespaceDeclaration = syntaxGenerator.NamespaceDeclaration(name, WithElasticTrivia(itemsAfter)); - if (itemsBefore.Any() && itemsAfter.Any()) + foreach (var nodeToRemove in itemsAfter) { - var itemsAfterNamespaceDeclaration = syntaxGenerator.NamespaceDeclaration(name, WithElasticTrivia(itemsAfter)); - - foreach (var nodeToRemove in itemsAfter) - { - editor.RemoveNode(nodeToRemove, SyntaxRemoveOptions.KeepNoTrivia); - } - - editor.InsertAfter(namespaceDeclaration, [newNamespaceDeclaration, itemsAfterNamespaceDeclaration]); + editor.RemoveNode(nodeToRemove, SyntaxRemoveOptions.KeepNoTrivia); } - else if (itemsBefore.Any()) - { - editor.InsertAfter(namespaceDeclaration, newNamespaceDeclaration); - var nodeToCleanup = itemsBefore.Last(); - editor.ReplaceNode(nodeToCleanup, WithElasticTrivia(nodeToCleanup, leading: false)); - } - else if (itemsAfter.Any()) - { - editor.InsertBefore(namespaceDeclaration, newNamespaceDeclaration); + editor.InsertAfter(namespaceDeclaration, [newNamespaceDeclaration, itemsAfterNamespaceDeclaration]); + } + else if (itemsBefore.Any()) + { + editor.InsertAfter(namespaceDeclaration, newNamespaceDeclaration); - var nodeToCleanup = itemsAfter.First(); - editor.ReplaceNode(nodeToCleanup, WithElasticTrivia(nodeToCleanup, trailing: false)); - } + var nodeToCleanup = itemsBefore.Last(); + editor.ReplaceNode(nodeToCleanup, WithElasticTrivia(nodeToCleanup, leading: false)); + } + else if (itemsAfter.Any()) + { + editor.InsertBefore(namespaceDeclaration, newNamespaceDeclaration); - var changedDocument = editor.GetChangedDocument(); - return changedDocument.Project.Solution; + var nodeToCleanup = itemsAfter.First(); + editor.ReplaceNode(nodeToCleanup, WithElasticTrivia(nodeToCleanup, trailing: false)); } - private static SyntaxNode WithElasticTrivia(SyntaxNode syntaxNode, bool leading = true, bool trailing = true) - { - if (leading && syntaxNode.HasLeadingTrivia) - { - syntaxNode = syntaxNode.WithLeadingTrivia(syntaxNode.GetLeadingTrivia().Select(SyntaxTriviaExtensions.AsElastic)); - } + var changedDocument = editor.GetChangedDocument(); + return changedDocument.Project.Solution; + } - if (trailing && syntaxNode.HasTrailingTrivia) - { - syntaxNode = syntaxNode.WithTrailingTrivia(syntaxNode.GetTrailingTrivia().Select(SyntaxTriviaExtensions.AsElastic)); - } + private static SyntaxNode WithElasticTrivia(SyntaxNode syntaxNode, bool leading = true, bool trailing = true) + { + if (leading && syntaxNode.HasLeadingTrivia) + { + syntaxNode = syntaxNode.WithLeadingTrivia(syntaxNode.GetLeadingTrivia().Select(SyntaxTriviaExtensions.AsElastic)); + } - return syntaxNode; + if (trailing && syntaxNode.HasTrailingTrivia) + { + syntaxNode = syntaxNode.WithTrailingTrivia(syntaxNode.GetTrailingTrivia().Select(SyntaxTriviaExtensions.AsElastic)); } - private static IEnumerable WithElasticTrivia(IEnumerable syntaxNodes) + return syntaxNode; + } + + private static IEnumerable WithElasticTrivia(IEnumerable syntaxNodes) + { + if (syntaxNodes.Any()) { - if (syntaxNodes.Any()) + var firstNode = syntaxNodes.First(); + var lastNode = syntaxNodes.Last(); + + if (firstNode == lastNode) + { + yield return WithElasticTrivia(firstNode); + } + else { - var firstNode = syntaxNodes.First(); - var lastNode = syntaxNodes.Last(); + yield return WithElasticTrivia(firstNode, trailing: false); - if (firstNode == lastNode) - { - yield return WithElasticTrivia(firstNode); - } - else + foreach (var node in syntaxNodes.Skip(1)) { - yield return WithElasticTrivia(firstNode, trailing: false); - - foreach (var node in syntaxNodes.Skip(1)) + if (node == lastNode) + { + yield return WithElasticTrivia(node, leading: false); + } + else { - if (node == lastNode) - { - yield return WithElasticTrivia(node, leading: false); - } - else - { - yield return node; - } + yield return node; } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.RenameFileEditor.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.RenameFileEditor.cs index 9335d717de916..ca50d8147f37f 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.RenameFileEditor.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.RenameFileEditor.cs @@ -9,34 +9,33 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; -namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType +namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType; + +internal abstract partial class AbstractMoveTypeService { - internal abstract partial class AbstractMoveTypeService + private class RenameFileEditor(TService service, State state, string fileName, CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken) { - private class RenameFileEditor(TService service, State state, string fileName, CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken) + public override Task> GetOperationsAsync() + => Task.FromResult(RenameFileToMatchTypeName()); + + public override Task GetModifiedSolutionAsync() { - public override Task> GetOperationsAsync() - => Task.FromResult(RenameFileToMatchTypeName()); - - public override Task GetModifiedSolutionAsync() - { - var modifiedSolution = SemanticDocument.Project.Solution - .WithDocumentName(SemanticDocument.Document.Id, FileName); - - return Task.FromResult(modifiedSolution); - } - - /// - /// Renames the file to match the type contained in it. - /// - private ImmutableArray RenameFileToMatchTypeName() - { - var documentId = SemanticDocument.Document.Id; - var oldSolution = SemanticDocument.Document.Project.Solution; - var newSolution = oldSolution.WithDocumentName(documentId, FileName); - - return [new ApplyChangesOperation(newSolution)]; - } + var modifiedSolution = SemanticDocument.Project.Solution + .WithDocumentName(SemanticDocument.Document.Id, FileName); + + return Task.FromResult(modifiedSolution); + } + + /// + /// Renames the file to match the type contained in it. + /// + private ImmutableArray RenameFileToMatchTypeName() + { + var documentId = SemanticDocument.Document.Id; + var oldSolution = SemanticDocument.Document.Project.Solution; + var newSolution = oldSolution.WithDocumentName(documentId, FileName); + + return [new ApplyChangesOperation(newSolution)]; } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.RenameTypeEditor.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.RenameTypeEditor.cs index 613c705a63f1c..fdb34911480dc 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.RenameTypeEditor.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.RenameTypeEditor.cs @@ -8,25 +8,24 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Rename; -namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType +namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType; + +internal abstract partial class AbstractMoveTypeService { - internal abstract partial class AbstractMoveTypeService + private class RenameTypeEditor(TService service, State state, string fileName, CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken) { - private class RenameTypeEditor(TService service, State state, string fileName, CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken) - { - /// - /// Renames a type to match its containing file name. - /// - public override async Task GetModifiedSolutionAsync() - { - // TODO: detect conflicts ahead of time and open an inline rename session if any exists. - // this will bring up dashboard with conflicts and will allow the user to resolve them. - // if no such conflicts exist, proceed with RenameSymbolAsync. - var solution = SemanticDocument.Document.Project.Solution; - var symbol = State.SemanticDocument.SemanticModel.GetDeclaredSymbol(State.TypeNode, CancellationToken); - return await Renamer.RenameSymbolAsync(solution, symbol, new SymbolRenameOptions(), FileName, CancellationToken).ConfigureAwait(false); - } + /// + /// Renames a type to match its containing file name. + /// + public override async Task GetModifiedSolutionAsync() + { + // TODO: detect conflicts ahead of time and open an inline rename session if any exists. + // this will bring up dashboard with conflicts and will allow the user to resolve them. + // if no such conflicts exist, proceed with RenameSymbolAsync. + var solution = SemanticDocument.Document.Project.Solution; + var symbol = State.SemanticDocument.SemanticModel.GetDeclaredSymbol(State.TypeNode, CancellationToken); + return await Renamer.RenameSymbolAsync(solution, symbol, new SymbolRenameOptions(), FileName, CancellationToken).ConfigureAwait(false); } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.State.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.State.cs index 2cec51e4d8aee..3b3b2e3737b86 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.State.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.State.cs @@ -12,69 +12,68 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType +namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType; + +internal abstract partial class AbstractMoveTypeService { - internal abstract partial class AbstractMoveTypeService + private sealed class State { - private sealed class State - { - public SemanticDocument SemanticDocument { get; } - public CodeCleanupOptionsProvider FallbackOptions { get; } + public SemanticDocument SemanticDocument { get; } + public CodeCleanupOptionsProvider FallbackOptions { get; } - public TTypeDeclarationSyntax TypeNode { get; set; } - public string TypeName { get; set; } - public string DocumentNameWithoutExtension { get; set; } - public bool IsDocumentNameAValidIdentifier { get; set; } + public TTypeDeclarationSyntax TypeNode { get; set; } + public string TypeName { get; set; } + public string DocumentNameWithoutExtension { get; set; } + public bool IsDocumentNameAValidIdentifier { get; set; } - private State(SemanticDocument document, CodeCleanupOptionsProvider fallbackOptions) - { - SemanticDocument = document; - FallbackOptions = fallbackOptions; - } + private State(SemanticDocument document, CodeCleanupOptionsProvider fallbackOptions) + { + SemanticDocument = document; + FallbackOptions = fallbackOptions; + } - internal static State Generate( - SemanticDocument document, TTypeDeclarationSyntax typeDeclaration, CodeCleanupOptionsProvider fallbackOptions, - CancellationToken cancellationToken) + internal static State Generate( + SemanticDocument document, TTypeDeclarationSyntax typeDeclaration, CodeCleanupOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var state = new State(document, fallbackOptions); + if (!state.TryInitialize(typeDeclaration, cancellationToken)) { - var state = new State(document, fallbackOptions); - if (!state.TryInitialize(typeDeclaration, cancellationToken)) - { - return null; - } - - return state; + return null; } - private bool TryInitialize( - TTypeDeclarationSyntax typeDeclaration, - CancellationToken cancellationToken) + return state; + } + + private bool TryInitialize( + TTypeDeclarationSyntax typeDeclaration, + CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) { - if (cancellationToken.IsCancellationRequested) - { - return false; - } + return false; + } - var tree = SemanticDocument.SyntaxTree; - var root = SemanticDocument.Root; - var syntaxFacts = SemanticDocument.Document.GetLanguageService(); + var tree = SemanticDocument.SyntaxTree; + var root = SemanticDocument.Root; + var syntaxFacts = SemanticDocument.Document.GetLanguageService(); - // compiler declared types, anonymous types, types defined in metadata should be filtered out. - if (SemanticDocument.SemanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken) is not INamedTypeSymbol typeSymbol || - typeSymbol.Locations.Any(static loc => loc.IsInMetadata) || - typeSymbol.IsAnonymousType || - typeSymbol.IsImplicitlyDeclared || - typeSymbol.Name == string.Empty) - { - return false; - } + // compiler declared types, anonymous types, types defined in metadata should be filtered out. + if (SemanticDocument.SemanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken) is not INamedTypeSymbol typeSymbol || + typeSymbol.Locations.Any(static loc => loc.IsInMetadata) || + typeSymbol.IsAnonymousType || + typeSymbol.IsImplicitlyDeclared || + typeSymbol.Name == string.Empty) + { + return false; + } - TypeNode = typeDeclaration; - TypeName = typeSymbol.Name; - DocumentNameWithoutExtension = Path.GetFileNameWithoutExtension(SemanticDocument.Document.Name); - IsDocumentNameAValidIdentifier = syntaxFacts.IsValidIdentifier(DocumentNameWithoutExtension); + TypeNode = typeDeclaration; + TypeName = typeSymbol.Name; + DocumentNameWithoutExtension = Path.GetFileNameWithoutExtension(SemanticDocument.Document.Name); + IsDocumentNameAValidIdentifier = syntaxFacts.IsValidIdentifier(DocumentNameWithoutExtension); - return true; - } + return true; } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.cs index ae7442234edc3..a60133133f9ea 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/AbstractMoveTypeService.cs @@ -21,245 +21,244 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType +namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType; + +internal abstract class AbstractMoveTypeService : IMoveTypeService { - internal abstract class AbstractMoveTypeService : IMoveTypeService - { - /// - /// Annotation to mark the namespace encapsulating the type that has been moved - /// - public static SyntaxAnnotation NamespaceScopeMovedAnnotation = new(nameof(MoveTypeOperationKind.MoveTypeNamespaceScope)); + /// + /// Annotation to mark the namespace encapsulating the type that has been moved + /// + public static SyntaxAnnotation NamespaceScopeMovedAnnotation = new(nameof(MoveTypeOperationKind.MoveTypeNamespaceScope)); - public abstract Task GetModifiedSolutionAsync(Document document, TextSpan textSpan, MoveTypeOperationKind operationKind, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); - public abstract Task> GetRefactoringAsync(Document document, TextSpan textSpan, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); - } + public abstract Task GetModifiedSolutionAsync(Document document, TextSpan textSpan, MoveTypeOperationKind operationKind, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); + public abstract Task> GetRefactoringAsync(Document document, TextSpan textSpan, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); +} - internal abstract partial class AbstractMoveTypeService : - AbstractMoveTypeService - where TService : AbstractMoveTypeService - where TTypeDeclarationSyntax : SyntaxNode - where TNamespaceDeclarationSyntax : SyntaxNode - where TMemberDeclarationSyntax : SyntaxNode - where TCompilationUnitSyntax : SyntaxNode +internal abstract partial class AbstractMoveTypeService : + AbstractMoveTypeService + where TService : AbstractMoveTypeService + where TTypeDeclarationSyntax : SyntaxNode + where TNamespaceDeclarationSyntax : SyntaxNode + where TMemberDeclarationSyntax : SyntaxNode + where TCompilationUnitSyntax : SyntaxNode +{ + public override async Task> GetRefactoringAsync( + Document document, TextSpan textSpan, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) { - public override async Task> GetRefactoringAsync( - Document document, TextSpan textSpan, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) + var state = await CreateStateAsync(document, textSpan, fallbackOptions, cancellationToken).ConfigureAwait(false); + + if (state == null) { - var state = await CreateStateAsync(document, textSpan, fallbackOptions, cancellationToken).ConfigureAwait(false); + return []; + } - if (state == null) - { - return []; - } + var actions = CreateActions(state, cancellationToken); + return actions; + } - var actions = CreateActions(state, cancellationToken); - return actions; - } + public override async Task GetModifiedSolutionAsync(Document document, TextSpan textSpan, MoveTypeOperationKind operationKind, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var state = await CreateStateAsync(document, textSpan, fallbackOptions, cancellationToken).ConfigureAwait(false); - public override async Task GetModifiedSolutionAsync(Document document, TextSpan textSpan, MoveTypeOperationKind operationKind, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) + if (state == null) { - var state = await CreateStateAsync(document, textSpan, fallbackOptions, cancellationToken).ConfigureAwait(false); + return document.Project.Solution; + } - if (state == null) - { - return document.Project.Solution; - } + var suggestedFileNames = GetSuggestedFileNames( + state.TypeNode, + IsNestedType(state.TypeNode), + state.TypeName, + state.SemanticDocument.Document.Name, + state.SemanticDocument.SemanticModel, + cancellationToken); + + var editor = Editor.GetEditor(operationKind, (TService)this, state, suggestedFileNames.FirstOrDefault(), cancellationToken); + var modifiedSolution = await editor.GetModifiedSolutionAsync().ConfigureAwait(false); + return modifiedSolution ?? document.Project.Solution; + } - var suggestedFileNames = GetSuggestedFileNames( - state.TypeNode, - IsNestedType(state.TypeNode), - state.TypeName, - state.SemanticDocument.Document.Name, - state.SemanticDocument.SemanticModel, - cancellationToken); - - var editor = Editor.GetEditor(operationKind, (TService)this, state, suggestedFileNames.FirstOrDefault(), cancellationToken); - var modifiedSolution = await editor.GetModifiedSolutionAsync().ConfigureAwait(false); - return modifiedSolution ?? document.Project.Solution; + protected abstract Task GetRelevantNodeAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken); + + private async Task CreateStateAsync(Document document, TextSpan textSpan, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var nodeToAnalyze = await GetRelevantNodeAsync(document, textSpan, cancellationToken).ConfigureAwait(false); + if (nodeToAnalyze == null) + { + return null; } - protected abstract Task GetRelevantNodeAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken); + var semanticDocument = await SemanticDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false); + return State.Generate(semanticDocument, nodeToAnalyze, fallbackOptions, cancellationToken); + } - private async Task CreateStateAsync(Document document, TextSpan textSpan, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) + private ImmutableArray CreateActions(State state, CancellationToken cancellationToken) + { + var typeMatchesDocumentName = TypeMatchesDocumentName( + state.TypeNode, + state.TypeName, + state.DocumentNameWithoutExtension, + state.SemanticDocument.SemanticModel, + cancellationToken); + + if (typeMatchesDocumentName) { - var nodeToAnalyze = await GetRelevantNodeAsync(document, textSpan, cancellationToken).ConfigureAwait(false); - if (nodeToAnalyze == null) - { - return null; - } - - var semanticDocument = await SemanticDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false); - return State.Generate(semanticDocument, nodeToAnalyze, fallbackOptions, cancellationToken); + // if type name matches document name, per style conventions, we have nothing to do. + return []; } - private ImmutableArray CreateActions(State state, CancellationToken cancellationToken) + using var _ = ArrayBuilder.GetInstance(out var actions); + var manyTypes = MultipleTopLevelTypeDeclarationInSourceDocument(state.SemanticDocument.Root); + var isNestedType = IsNestedType(state.TypeNode); + + var syntaxFacts = state.SemanticDocument.Document.GetRequiredLanguageService(); + var isClassNextToGlobalStatements = manyTypes + ? false + : ClassNextToGlobalStatements(state.SemanticDocument.Root, syntaxFacts); + + var suggestedFileNames = GetSuggestedFileNames( + state.TypeNode, + isNestedType, + state.TypeName, + state.SemanticDocument.Document.Name, + state.SemanticDocument.SemanticModel, + cancellationToken); + + // (1) Add Move type to new file code action: + // case 1: There are multiple type declarations in current document. offer, move to new file. + // case 2: This is a nested type, offer to move to new file. + // case 3: If there is a single type decl in current file, *do not* offer move to new file, + // rename actions are sufficient in this case. + // case 4: If there are top level statements(Global statements) offer to move even + // in cases where there are only one class in the file. + if (manyTypes || isNestedType || isClassNextToGlobalStatements) { - var typeMatchesDocumentName = TypeMatchesDocumentName( - state.TypeNode, - state.TypeName, - state.DocumentNameWithoutExtension, - state.SemanticDocument.SemanticModel, - cancellationToken); - - if (typeMatchesDocumentName) + foreach (var fileName in suggestedFileNames) { - // if type name matches document name, per style conventions, we have nothing to do. - return []; + actions.Add(GetCodeAction(state, fileName, operationKind: MoveTypeOperationKind.MoveType)); } + } - using var _ = ArrayBuilder.GetInstance(out var actions); - var manyTypes = MultipleTopLevelTypeDeclarationInSourceDocument(state.SemanticDocument.Root); - var isNestedType = IsNestedType(state.TypeNode); - - var syntaxFacts = state.SemanticDocument.Document.GetRequiredLanguageService(); - var isClassNextToGlobalStatements = manyTypes - ? false - : ClassNextToGlobalStatements(state.SemanticDocument.Root, syntaxFacts); - - var suggestedFileNames = GetSuggestedFileNames( - state.TypeNode, - isNestedType, - state.TypeName, - state.SemanticDocument.Document.Name, - state.SemanticDocument.SemanticModel, - cancellationToken); - - // (1) Add Move type to new file code action: - // case 1: There are multiple type declarations in current document. offer, move to new file. - // case 2: This is a nested type, offer to move to new file. - // case 3: If there is a single type decl in current file, *do not* offer move to new file, - // rename actions are sufficient in this case. - // case 4: If there are top level statements(Global statements) offer to move even - // in cases where there are only one class in the file. - if (manyTypes || isNestedType || isClassNextToGlobalStatements) + // (2) Add rename file and rename type code actions: + // Case: No type declaration in file matches the file name. + if (!AnyTopLevelTypeMatchesDocumentName(state, cancellationToken)) + { + foreach (var fileName in suggestedFileNames) { - foreach (var fileName in suggestedFileNames) - { - actions.Add(GetCodeAction(state, fileName, operationKind: MoveTypeOperationKind.MoveType)); - } + actions.Add(GetCodeAction(state, fileName, operationKind: MoveTypeOperationKind.RenameFile)); } - // (2) Add rename file and rename type code actions: - // Case: No type declaration in file matches the file name. - if (!AnyTopLevelTypeMatchesDocumentName(state, cancellationToken)) + // only if the document name can be legal identifier in the language, + // offer to rename type with document name + if (state.IsDocumentNameAValidIdentifier) { - foreach (var fileName in suggestedFileNames) - { - actions.Add(GetCodeAction(state, fileName, operationKind: MoveTypeOperationKind.RenameFile)); - } - - // only if the document name can be legal identifier in the language, - // offer to rename type with document name - if (state.IsDocumentNameAValidIdentifier) - { - actions.Add(GetCodeAction( - state, fileName: state.DocumentNameWithoutExtension, - operationKind: MoveTypeOperationKind.RenameType)); - } + actions.Add(GetCodeAction( + state, fileName: state.DocumentNameWithoutExtension, + operationKind: MoveTypeOperationKind.RenameType)); } + } - Debug.Assert(actions.Count != 0, "No code actions found for MoveType Refactoring"); + Debug.Assert(actions.Count != 0, "No code actions found for MoveType Refactoring"); - return actions.ToImmutable(); - } + return actions.ToImmutable(); + } - private static bool ClassNextToGlobalStatements(SyntaxNode root, ISyntaxFactsService syntaxFacts) - => syntaxFacts.ContainsGlobalStatement(root); + private static bool ClassNextToGlobalStatements(SyntaxNode root, ISyntaxFactsService syntaxFacts) + => syntaxFacts.ContainsGlobalStatement(root); - private CodeAction GetCodeAction(State state, string fileName, MoveTypeOperationKind operationKind) - => new MoveTypeCodeAction((TService)this, state, operationKind, fileName); + private CodeAction GetCodeAction(State state, string fileName, MoveTypeOperationKind operationKind) + => new MoveTypeCodeAction((TService)this, state, operationKind, fileName); - private static bool IsNestedType(TTypeDeclarationSyntax typeNode) - => typeNode.Parent is TTypeDeclarationSyntax; + private static bool IsNestedType(TTypeDeclarationSyntax typeNode) + => typeNode.Parent is TTypeDeclarationSyntax; - /// - /// checks if there is a single top level type declaration in a document - /// - /// - /// optimized for perf, uses Skip(1).Any() instead of Count() > 1 - /// - private static bool MultipleTopLevelTypeDeclarationInSourceDocument(SyntaxNode root) - => TopLevelTypeDeclarations(root).Skip(1).Any(); + /// + /// checks if there is a single top level type declaration in a document + /// + /// + /// optimized for perf, uses Skip(1).Any() instead of Count() > 1 + /// + private static bool MultipleTopLevelTypeDeclarationInSourceDocument(SyntaxNode root) + => TopLevelTypeDeclarations(root).Skip(1).Any(); - private static IEnumerable TopLevelTypeDeclarations(SyntaxNode root) - => root.DescendantNodes(n => n is TCompilationUnitSyntax or TNamespaceDeclarationSyntax) - .OfType(); + private static IEnumerable TopLevelTypeDeclarations(SyntaxNode root) + => root.DescendantNodes(n => n is TCompilationUnitSyntax or TNamespaceDeclarationSyntax) + .OfType(); - private static bool AnyTopLevelTypeMatchesDocumentName(State state, CancellationToken cancellationToken) - { - var root = state.SemanticDocument.Root; - var semanticModel = state.SemanticDocument.SemanticModel; - - return TopLevelTypeDeclarations(root).Any( - typeDeclaration => - { - var typeName = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken).Name; - return TypeMatchesDocumentName( - typeDeclaration, typeName, state.DocumentNameWithoutExtension, - semanticModel, cancellationToken); - }); - } + private static bool AnyTopLevelTypeMatchesDocumentName(State state, CancellationToken cancellationToken) + { + var root = state.SemanticDocument.Root; + var semanticModel = state.SemanticDocument.SemanticModel; - /// - /// checks if type name matches its parent document name, per style rules. - /// - /// - /// Note: For a nested type, a matching document name could be just the type name or a - /// dotted qualified name of its type hierarchy. - /// - protected static bool TypeMatchesDocumentName( - TTypeDeclarationSyntax typeNode, - string typeName, - string documentNameWithoutExtension, - SemanticModel semanticModel, - CancellationToken cancellationToken) - { - // If it is not a nested type, we compare the unqualified type name with the document name. - // If it is a nested type, the type name `Outer.Inner` matches file names `Inner.cs` and `Outer.Inner.cs` - var namesMatch = typeName.Equals(documentNameWithoutExtension, StringComparison.CurrentCulture); - if (!namesMatch) + return TopLevelTypeDeclarations(root).Any( + typeDeclaration => { - var typeNameParts = GetTypeNamePartsForNestedTypeNode(typeNode, semanticModel, cancellationToken); - var fileNameParts = documentNameWithoutExtension.Split('.'); + var typeName = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken).Name; + return TypeMatchesDocumentName( + typeDeclaration, typeName, state.DocumentNameWithoutExtension, + semanticModel, cancellationToken); + }); + } - // qualified type name `Outer.Inner` matches file names `Inner.cs` and `Outer.Inner.cs` - return typeNameParts.SequenceEqual(fileNameParts, StringComparer.CurrentCulture); - } + /// + /// checks if type name matches its parent document name, per style rules. + /// + /// + /// Note: For a nested type, a matching document name could be just the type name or a + /// dotted qualified name of its type hierarchy. + /// + protected static bool TypeMatchesDocumentName( + TTypeDeclarationSyntax typeNode, + string typeName, + string documentNameWithoutExtension, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + // If it is not a nested type, we compare the unqualified type name with the document name. + // If it is a nested type, the type name `Outer.Inner` matches file names `Inner.cs` and `Outer.Inner.cs` + var namesMatch = typeName.Equals(documentNameWithoutExtension, StringComparison.CurrentCulture); + if (!namesMatch) + { + var typeNameParts = GetTypeNamePartsForNestedTypeNode(typeNode, semanticModel, cancellationToken); + var fileNameParts = documentNameWithoutExtension.Split('.'); - return namesMatch; + // qualified type name `Outer.Inner` matches file names `Inner.cs` and `Outer.Inner.cs` + return typeNameParts.SequenceEqual(fileNameParts, StringComparer.CurrentCulture); } - private static ImmutableArray GetSuggestedFileNames( - TTypeDeclarationSyntax typeNode, - bool isNestedType, - string typeName, - string documentNameWithExtension, - SemanticModel semanticModel, - CancellationToken cancellationToken) - { - var fileExtension = Path.GetExtension(documentNameWithExtension); + return namesMatch; + } - var standaloneName = typeName + fileExtension; + private static ImmutableArray GetSuggestedFileNames( + TTypeDeclarationSyntax typeNode, + bool isNestedType, + string typeName, + string documentNameWithExtension, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var fileExtension = Path.GetExtension(documentNameWithExtension); - // If it is a nested type, we should match type hierarchy's name parts with the file name. - if (isNestedType) - { - var typeNameParts = GetTypeNamePartsForNestedTypeNode(typeNode, semanticModel, cancellationToken); - var dottedName = typeNameParts.Join(".") + fileExtension; + var standaloneName = typeName + fileExtension; - return [standaloneName, dottedName]; - } - else - { - return [standaloneName]; - } - } + // If it is a nested type, we should match type hierarchy's name parts with the file name. + if (isNestedType) + { + var typeNameParts = GetTypeNamePartsForNestedTypeNode(typeNode, semanticModel, cancellationToken); + var dottedName = typeNameParts.Join(".") + fileExtension; - private static IEnumerable GetTypeNamePartsForNestedTypeNode( - TTypeDeclarationSyntax typeNode, SemanticModel semanticModel, CancellationToken cancellationToken) - => typeNode.AncestorsAndSelf() - .OfType() - .Select(n => semanticModel.GetDeclaredSymbol(n, cancellationToken).Name) - .Reverse(); + return [standaloneName, dottedName]; + } + else + { + return [standaloneName]; + } } + + private static IEnumerable GetTypeNamePartsForNestedTypeNode( + TTypeDeclarationSyntax typeNode, SemanticModel semanticModel, CancellationToken cancellationToken) + => typeNode.AncestorsAndSelf() + .OfType() + .Select(n => semanticModel.GetDeclaredSymbol(n, cancellationToken).Name) + .Reverse(); } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/IMoveTypeService.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/IMoveTypeService.cs index a30bb92fe86c5..1c5f73b482166 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/IMoveTypeService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/IMoveTypeService.cs @@ -13,12 +13,11 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType +namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType; + +internal interface IMoveTypeService : ILanguageService { - internal interface IMoveTypeService : ILanguageService - { - Task> GetRefactoringAsync(Document document, TextSpan textSpan, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); + Task> GetRefactoringAsync(Document document, TextSpan textSpan, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); - Task GetModifiedSolutionAsync(Document document, TextSpan textSpan, MoveTypeOperationKind operationKind, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); - } + Task GetModifiedSolutionAsync(Document document, TextSpan textSpan, MoveTypeOperationKind operationKind, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/MoveTypeCodeRefactoringProvider.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/MoveTypeCodeRefactoringProvider.cs index a981dac43a3df..c33cb8c371203 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/MoveTypeCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/MoveTypeCodeRefactoringProvider.cs @@ -7,30 +7,29 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType +namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType; + +[ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, + Name = PredefinedCodeRefactoringProviderNames.MoveTypeToFile), Shared] +internal class MoveTypeCodeRefactoringProvider : CodeRefactoringProvider { - [ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, - Name = PredefinedCodeRefactoringProviderNames.MoveTypeToFile), Shared] - internal class MoveTypeCodeRefactoringProvider : CodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public MoveTypeCodeRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public MoveTypeCodeRefactoringProvider() - { - } + } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, textSpan, cancellationToken) = context; - if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) - return; + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, textSpan, cancellationToken) = context; + if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) + return; - if (document.IsGeneratedCode(cancellationToken)) - return; + if (document.IsGeneratedCode(cancellationToken)) + return; - var service = document.GetRequiredLanguageService(); - var actions = await service.GetRefactoringAsync(document, textSpan, context.Options, cancellationToken).ConfigureAwait(false); - context.RegisterRefactorings(actions); - } + var service = document.GetRequiredLanguageService(); + var actions = await service.GetRefactoringAsync(document, textSpan, context.Options, cancellationToken).ConfigureAwait(false); + context.RegisterRefactorings(actions); } } diff --git a/src/Features/Core/Portable/CodeRefactorings/MoveType/MoveTypeOperationKind.cs b/src/Features/Core/Portable/CodeRefactorings/MoveType/MoveTypeOperationKind.cs index 58c99aef37ff6..df7b6a2c5be90 100644 --- a/src/Features/Core/Portable/CodeRefactorings/MoveType/MoveTypeOperationKind.cs +++ b/src/Features/Core/Portable/CodeRefactorings/MoveType/MoveTypeOperationKind.cs @@ -2,29 +2,28 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType +namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType; + +internal enum MoveTypeOperationKind { - internal enum MoveTypeOperationKind - { - /// - /// Moves a type to it's own file - /// - MoveType, + /// + /// Moves a type to it's own file + /// + MoveType, - /// - /// Functionally doesn't change the type symbol, but moves it to it's own - /// namespace declaration scope. - /// - MoveTypeNamespaceScope, + /// + /// Functionally doesn't change the type symbol, but moves it to it's own + /// namespace declaration scope. + /// + MoveTypeNamespaceScope, - /// - /// Renames the target type - /// - RenameType, + /// + /// Renames the target type + /// + RenameType, - /// - /// Renames the file containing the target type - /// - RenameFile - } + /// + /// Renames the file containing the target type + /// + RenameFile } diff --git a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs index 3bfd0a41856c8..57de1c323c407 100644 --- a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs +++ b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs @@ -2,89 +2,88 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +internal static class PredefinedCodeRefactoringProviderNames { - internal static class PredefinedCodeRefactoringProviderNames - { - public const string AddAwait = "Add Await Code Action Provider"; - public const string AddConstructorParametersFromMembers = "Add Parameters From Members Code Action Provider"; - public const string AddDebuggerDisplay = nameof(AddDebuggerDisplay); - public const string AddFileBanner = "Add Banner To File Code Action Provider"; - public const string AddMissingImports = "Add Missing Imports On Paste Code Action Provider"; - public const string AddParameterCheck = nameof(AddParameterCheck); - public const string ChangeSignature = "Change Signature Code Action Provider"; - public const string ConvertAnonymousTypeToClass = "Convert Anonymous Type to Class Code Action Provider"; - public const string ConvertAnonymousTypeToTuple = "Convert Anonymous Type to Tuple Code Action Provider"; - public const string ConvertAutoPropertyToFullProperty = nameof(ConvertAutoPropertyToFullProperty); - public const string ConvertBetweenRegularAndVerbatimInterpolatedString = nameof(ConvertBetweenRegularAndVerbatimInterpolatedString); - public const string ConvertBetweenRegularAndVerbatimString = nameof(ConvertBetweenRegularAndVerbatimString); - public const string ConvertConcatenationToInterpolatedString = nameof(ConvertConcatenationToInterpolatedString); - public const string ConvertDirectCastToTryCast = "Convert Direct Cast to Try Cast"; - public const string ConvertForEachToFor = nameof(ConvertForEachToFor); - public const string ConvertForEachToLinqQuery = nameof(ConvertForEachToLinqQuery); - public const string ConvertForToForEach = nameof(ConvertForToForEach); - public const string ConvertIfToSwitch = nameof(ConvertIfToSwitch); - public const string ConvertLinqQueryToForEach = nameof(ConvertLinqQueryToForEach); - public const string ConvertLocalFunctionToMethod = nameof(ConvertLocalFunctionToMethod); - public const string ConvertNamespace = "Convert Namespace"; - public const string ConvertNumericLiteral = nameof(ConvertNumericLiteral); - public const string ConvertPlaceholderToInterpolatedString = nameof(ConvertPlaceholderToInterpolatedString); - public const string ConvertPrimaryToRegularConstructor = nameof(ConvertPrimaryToRegularConstructor); - public const string ConvertToInterpolatedString = "Convert To Interpolated String Code Action Provider"; - public const string ConvertToProgramMain = "Convert To Program.Main"; - public const string ConvertToRawString = nameof(ConvertToRawString); - public const string ConvertToRecord = nameof(ConvertToRecord); - public const string ConvertToTopLevelStatements = "Convert To Top Level Statements"; - public const string ConvertTryCastToDirectCast = "Convert Try Cast to Direct Cast"; - public const string ConvertTupleToStruct = "Convert Tuple to Struct Code Action Provider"; - public const string EnableNullable = "Enable Nullable Reference Types"; - public const string EncapsulateField = "Encapsulate Field"; - public const string ExtractClass = "Extract Class Code Action Provider"; - public const string ExtractInterface = "Extract Interface Code Action Provider"; - public const string ExtractMethod = "Extract Method Code Action Provider"; - public const string GenerateComparisonOperators = nameof(GenerateComparisonOperators); - public const string GenerateConstructorFromMembers = "Generate Constructor From Members Code Action Provider"; - public const string GenerateDefaultConstructors = "Generate Default Constructors Code Action Provider"; - public const string GenerateEqualsAndGetHashCodeFromMembers = "Generate Equals and GetHashCode Code Action Provider"; - public const string GenerateOverrides = "Generate Overrides Code Action Provider"; - public const string ImplementInterfaceExplicitly = nameof(ImplementInterfaceExplicitly); - public const string ImplementInterfaceImplicitly = nameof(ImplementInterfaceImplicitly); - public const string InitializeMemberFromParameter = nameof(InitializeMemberFromParameter); - public const string InitializeMemberFromPrimaryConstructorParameter = nameof(InitializeMemberFromPrimaryConstructorParameter); - public const string InlineMethod = "Inline Method Code Action Provider"; - public const string InlineTemporary = "Inline Temporary Code Action Provider"; - public const string IntroduceLocalForExpression = nameof(IntroduceLocalForExpression); - public const string IntroduceParameter = nameof(IntroduceParameter); - public const string IntroduceUsingStatement = "Introduce Using Statement Code Action Provider"; - public const string IntroduceVariable = "Introduce Variable Code Action Provider"; - public const string InvertConditional = "Invert Conditional Code Action Provider"; - public const string InvertIf = "Invert If Code Action Provider"; - public const string InvertLogical = "Invert Logical Code Action Provider"; - public const string InvertMultiLineIf = nameof(InvertMultiLineIf); - public const string MakeLocalFunctionStatic = nameof(MakeLocalFunctionStatic); - public const string MergeConsecutiveIfStatements = "Merge Consecutive If Statements Code Action Provider"; - public const string MergeNestedIfStatements = "Merge Nested If Statements Code Action Provider"; - public const string MoveDeclarationNearReference = "Move Declaration Near Reference Code Action Provider"; - public const string MoveStaticMembers = "Move Static Members to Another Type Code Action Provider"; - public const string MoveToNamespace = "Move To Namespace Code Action Provider"; - public const string MoveTypeToFile = "Move Type To File Code Action Provider"; - public const string NameTupleElement = nameof(NameTupleElement); - public const string PullMemberUp = "Pull Member Up Code Action Provider"; - public const string RenameTracking = nameof(RenameTracking); - public const string ReplaceConditionalWithStatements = nameof(ReplaceConditionalWithStatements); - public const string ReplaceDocCommentTextWithTag = "Replace Documentation Comment Text With Tag Code Action Provider"; - public const string ReplaceMethodWithProperty = nameof(ReplaceMethodWithProperty); - public const string ReplacePropertyWithMethods = nameof(ReplacePropertyWithMethods); - public const string ReverseForStatement = nameof(ReverseForStatement); - public const string SplitIntoConsecutiveIfStatements = "Split Into Consecutive If Statements Code Action Provider"; - public const string SplitIntoNestedIfStatements = "Split Into Nested If Statements Code Action Provider"; - public const string SyncNamespace = "Sync Namespace and Folder Name Code Action Provider"; - public const string UseExplicitType = "Use Explicit Type Code Action Provider"; - public const string UseExpressionBody = "Use Expression Body Code Action Provider"; - public const string UseExpressionBodyForLambda = nameof(UseExpressionBodyForLambda); - public const string UseImplicitType = "Use Implicit Type Code Action Provider"; - public const string UseNamedArguments = nameof(UseNamedArguments); - public const string UseRecursivePatterns = nameof(UseRecursivePatterns); - public const string Wrapping = "Wrapping Code Action Provider"; - } + public const string AddAwait = "Add Await Code Action Provider"; + public const string AddConstructorParametersFromMembers = "Add Parameters From Members Code Action Provider"; + public const string AddDebuggerDisplay = nameof(AddDebuggerDisplay); + public const string AddFileBanner = "Add Banner To File Code Action Provider"; + public const string AddMissingImports = "Add Missing Imports On Paste Code Action Provider"; + public const string AddParameterCheck = nameof(AddParameterCheck); + public const string ChangeSignature = "Change Signature Code Action Provider"; + public const string ConvertAnonymousTypeToClass = "Convert Anonymous Type to Class Code Action Provider"; + public const string ConvertAnonymousTypeToTuple = "Convert Anonymous Type to Tuple Code Action Provider"; + public const string ConvertAutoPropertyToFullProperty = nameof(ConvertAutoPropertyToFullProperty); + public const string ConvertBetweenRegularAndVerbatimInterpolatedString = nameof(ConvertBetweenRegularAndVerbatimInterpolatedString); + public const string ConvertBetweenRegularAndVerbatimString = nameof(ConvertBetweenRegularAndVerbatimString); + public const string ConvertConcatenationToInterpolatedString = nameof(ConvertConcatenationToInterpolatedString); + public const string ConvertDirectCastToTryCast = "Convert Direct Cast to Try Cast"; + public const string ConvertForEachToFor = nameof(ConvertForEachToFor); + public const string ConvertForEachToLinqQuery = nameof(ConvertForEachToLinqQuery); + public const string ConvertForToForEach = nameof(ConvertForToForEach); + public const string ConvertIfToSwitch = nameof(ConvertIfToSwitch); + public const string ConvertLinqQueryToForEach = nameof(ConvertLinqQueryToForEach); + public const string ConvertLocalFunctionToMethod = nameof(ConvertLocalFunctionToMethod); + public const string ConvertNamespace = "Convert Namespace"; + public const string ConvertNumericLiteral = nameof(ConvertNumericLiteral); + public const string ConvertPlaceholderToInterpolatedString = nameof(ConvertPlaceholderToInterpolatedString); + public const string ConvertPrimaryToRegularConstructor = nameof(ConvertPrimaryToRegularConstructor); + public const string ConvertToInterpolatedString = "Convert To Interpolated String Code Action Provider"; + public const string ConvertToProgramMain = "Convert To Program.Main"; + public const string ConvertToRawString = nameof(ConvertToRawString); + public const string ConvertToRecord = nameof(ConvertToRecord); + public const string ConvertToTopLevelStatements = "Convert To Top Level Statements"; + public const string ConvertTryCastToDirectCast = "Convert Try Cast to Direct Cast"; + public const string ConvertTupleToStruct = "Convert Tuple to Struct Code Action Provider"; + public const string EnableNullable = "Enable Nullable Reference Types"; + public const string EncapsulateField = "Encapsulate Field"; + public const string ExtractClass = "Extract Class Code Action Provider"; + public const string ExtractInterface = "Extract Interface Code Action Provider"; + public const string ExtractMethod = "Extract Method Code Action Provider"; + public const string GenerateComparisonOperators = nameof(GenerateComparisonOperators); + public const string GenerateConstructorFromMembers = "Generate Constructor From Members Code Action Provider"; + public const string GenerateDefaultConstructors = "Generate Default Constructors Code Action Provider"; + public const string GenerateEqualsAndGetHashCodeFromMembers = "Generate Equals and GetHashCode Code Action Provider"; + public const string GenerateOverrides = "Generate Overrides Code Action Provider"; + public const string ImplementInterfaceExplicitly = nameof(ImplementInterfaceExplicitly); + public const string ImplementInterfaceImplicitly = nameof(ImplementInterfaceImplicitly); + public const string InitializeMemberFromParameter = nameof(InitializeMemberFromParameter); + public const string InitializeMemberFromPrimaryConstructorParameter = nameof(InitializeMemberFromPrimaryConstructorParameter); + public const string InlineMethod = "Inline Method Code Action Provider"; + public const string InlineTemporary = "Inline Temporary Code Action Provider"; + public const string IntroduceLocalForExpression = nameof(IntroduceLocalForExpression); + public const string IntroduceParameter = nameof(IntroduceParameter); + public const string IntroduceUsingStatement = "Introduce Using Statement Code Action Provider"; + public const string IntroduceVariable = "Introduce Variable Code Action Provider"; + public const string InvertConditional = "Invert Conditional Code Action Provider"; + public const string InvertIf = "Invert If Code Action Provider"; + public const string InvertLogical = "Invert Logical Code Action Provider"; + public const string InvertMultiLineIf = nameof(InvertMultiLineIf); + public const string MakeLocalFunctionStatic = nameof(MakeLocalFunctionStatic); + public const string MergeConsecutiveIfStatements = "Merge Consecutive If Statements Code Action Provider"; + public const string MergeNestedIfStatements = "Merge Nested If Statements Code Action Provider"; + public const string MoveDeclarationNearReference = "Move Declaration Near Reference Code Action Provider"; + public const string MoveStaticMembers = "Move Static Members to Another Type Code Action Provider"; + public const string MoveToNamespace = "Move To Namespace Code Action Provider"; + public const string MoveTypeToFile = "Move Type To File Code Action Provider"; + public const string NameTupleElement = nameof(NameTupleElement); + public const string PullMemberUp = "Pull Member Up Code Action Provider"; + public const string RenameTracking = nameof(RenameTracking); + public const string ReplaceConditionalWithStatements = nameof(ReplaceConditionalWithStatements); + public const string ReplaceDocCommentTextWithTag = "Replace Documentation Comment Text With Tag Code Action Provider"; + public const string ReplaceMethodWithProperty = nameof(ReplaceMethodWithProperty); + public const string ReplacePropertyWithMethods = nameof(ReplacePropertyWithMethods); + public const string ReverseForStatement = nameof(ReverseForStatement); + public const string SplitIntoConsecutiveIfStatements = "Split Into Consecutive If Statements Code Action Provider"; + public const string SplitIntoNestedIfStatements = "Split Into Nested If Statements Code Action Provider"; + public const string SyncNamespace = "Sync Namespace and Folder Name Code Action Provider"; + public const string UseExplicitType = "Use Explicit Type Code Action Provider"; + public const string UseExpressionBody = "Use Expression Body Code Action Provider"; + public const string UseExpressionBodyForLambda = nameof(UseExpressionBodyForLambda); + public const string UseImplicitType = "Use Implicit Type Code Action Provider"; + public const string UseNamedArguments = nameof(UseNamedArguments); + public const string UseRecursivePatterns = nameof(UseRecursivePatterns); + public const string Wrapping = "Wrapping Code Action Provider"; } diff --git a/src/Features/Core/Portable/CodeRefactorings/ServicesLayerCodeActionHelpersService.cs b/src/Features/Core/Portable/CodeRefactorings/ServicesLayerCodeActionHelpersService.cs index 05ce42057948f..869e3e468557a 100644 --- a/src/Features/Core/Portable/CodeRefactorings/ServicesLayerCodeActionHelpersService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/ServicesLayerCodeActionHelpersService.cs @@ -9,23 +9,22 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.CodeRefactorings +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +[ExportWorkspaceServiceFactory(typeof(ICodeRefactoringHelpersService), ServiceLayer.Default), Shared] +internal class ServicesLayerCodeActionHelpersService : IWorkspaceServiceFactory { - [ExportWorkspaceServiceFactory(typeof(ICodeRefactoringHelpersService), ServiceLayer.Default), Shared] - internal class ServicesLayerCodeActionHelpersService : IWorkspaceServiceFactory + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ServicesLayerCodeActionHelpersService() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ServicesLayerCodeActionHelpersService() - { - } + } - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new CodeActionHelpersService(); + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => new CodeActionHelpersService(); - private class CodeActionHelpersService : ICodeRefactoringHelpersService - { - public bool ActiveInlineRenameSession => false; - } + private class CodeActionHelpersService : ICodeRefactoringHelpersService + { + public bool ActiveInlineRenameSession => false; } } diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractChangeNamespaceService.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractChangeNamespaceService.cs index 7c875315872a2..6a945ab96565f 100644 --- a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractChangeNamespaceService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractChangeNamespaceService.cs @@ -28,861 +28,860 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ChangeNamespace +namespace Microsoft.CodeAnalysis.ChangeNamespace; + +/// +/// This intermediate class is used to hide method `TryGetReplacementReferenceSyntax` from . +/// +internal abstract class AbstractChangeNamespaceService : IChangeNamespaceService { + public abstract Task CanChangeNamespaceAsync(Document document, SyntaxNode container, CancellationToken cancellationToken); + + public abstract Task ChangeNamespaceAsync(Document document, SyntaxNode container, string targetNamespace, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); + + public abstract Task TryChangeTopLevelNamespacesAsync(Document document, string targetNamespace, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); + /// - /// This intermediate class is used to hide method `TryGetReplacementReferenceSyntax` from . + /// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the + /// namespace to be changed. If this reference is the right side of a qualified name, the new node returned would + /// be the entire qualified name. Depends on whether is provided, the name + /// in the new node might be qualified with this new namespace instead. /// - internal abstract class AbstractChangeNamespaceService : IChangeNamespaceService - { - public abstract Task CanChangeNamespaceAsync(Document document, SyntaxNode container, CancellationToken cancellationToken); - - public abstract Task ChangeNamespaceAsync(Document document, SyntaxNode container, string targetNamespace, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); - - public abstract Task TryChangeTopLevelNamespacesAsync(Document document, string targetNamespace, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken); - - /// - /// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the - /// namespace to be changed. If this reference is the right side of a qualified name, the new node returned would - /// be the entire qualified name. Depends on whether is provided, the name - /// in the new node might be qualified with this new namespace instead. - /// - /// A reference to a type declared inside the namespace to be changed, which is calculated - /// based on results from `SymbolFinder.FindReferencesAsync`. - /// If specified, the namespace of original reference will be replaced with given - /// namespace in the replacement node. - /// The node to be replaced. This might be an ancestor of original - /// The replacement node. - public abstract bool TryGetReplacementReferenceSyntax(SyntaxNode reference, ImmutableArray newNamespaceParts, ISyntaxFactsService syntaxFacts, [NotNullWhen(returnValue: true)] out SyntaxNode? old, [NotNullWhen(returnValue: true)] out SyntaxNode? @new); - } + /// A reference to a type declared inside the namespace to be changed, which is calculated + /// based on results from `SymbolFinder.FindReferencesAsync`. + /// If specified, the namespace of original reference will be replaced with given + /// namespace in the replacement node. + /// The node to be replaced. This might be an ancestor of original + /// The replacement node. + public abstract bool TryGetReplacementReferenceSyntax(SyntaxNode reference, ImmutableArray newNamespaceParts, ISyntaxFactsService syntaxFacts, [NotNullWhen(returnValue: true)] out SyntaxNode? old, [NotNullWhen(returnValue: true)] out SyntaxNode? @new); +} - internal abstract class AbstractChangeNamespaceService - : AbstractChangeNamespaceService - where TNamespaceDeclarationSyntax : SyntaxNode - where TCompilationUnitSyntax : SyntaxNode - where TMemberDeclarationSyntax : SyntaxNode - { - private static readonly char[] s_dotSeparator = ['.']; +internal abstract class AbstractChangeNamespaceService + : AbstractChangeNamespaceService + where TNamespaceDeclarationSyntax : SyntaxNode + where TCompilationUnitSyntax : SyntaxNode + where TMemberDeclarationSyntax : SyntaxNode +{ + private static readonly char[] s_dotSeparator = ['.']; - /// - /// The annotation used to track applicable container in each document to be fixed. - /// - protected static SyntaxAnnotation ContainerAnnotation { get; } = new SyntaxAnnotation(); + /// + /// The annotation used to track applicable container in each document to be fixed. + /// + protected static SyntaxAnnotation ContainerAnnotation { get; } = new SyntaxAnnotation(); - protected static SyntaxAnnotation WarningAnnotation { get; } - = CodeActions.WarningAnnotation.Create( - FeaturesResources.Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning); + protected static SyntaxAnnotation WarningAnnotation { get; } + = CodeActions.WarningAnnotation.Create( + FeaturesResources.Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning); - protected abstract TCompilationUnitSyntax ChangeNamespaceDeclaration( - TCompilationUnitSyntax root, ImmutableArray declaredNamespaceParts, ImmutableArray targetNamespaceParts); + protected abstract TCompilationUnitSyntax ChangeNamespaceDeclaration( + TCompilationUnitSyntax root, ImmutableArray declaredNamespaceParts, ImmutableArray targetNamespaceParts); - protected abstract SyntaxList GetMemberDeclarationsInContainer(SyntaxNode compilationUnitOrNamespaceDecl); + protected abstract SyntaxList GetMemberDeclarationsInContainer(SyntaxNode compilationUnitOrNamespaceDecl); - protected abstract Task TryGetApplicableContainerFromSpanAsync(Document document, TextSpan span, CancellationToken cancellationToken); + protected abstract Task TryGetApplicableContainerFromSpanAsync(Document document, TextSpan span, CancellationToken cancellationToken); - protected abstract string GetDeclaredNamespace(SyntaxNode container); + protected abstract string GetDeclaredNamespace(SyntaxNode container); - /// - /// Decide if we can change the namespace for provided based on the criteria listed for - /// - /// - /// - /// If namespace can be changed, returns a list of documents that linked to the provided document (including itself) - /// and the corresponding container nodes in each document, which will later be used for annotation. Otherwise, a - /// default ImmutableArray is returned. Currently we only support linked document in multi-targeting project scenario. - /// - protected abstract Task> GetValidContainersFromAllLinkedDocumentsAsync(Document document, SyntaxNode container, CancellationToken cancellationToken); + /// + /// Decide if we can change the namespace for provided based on the criteria listed for + /// + /// + /// + /// If namespace can be changed, returns a list of documents that linked to the provided document (including itself) + /// and the corresponding container nodes in each document, which will later be used for annotation. Otherwise, a + /// default ImmutableArray is returned. Currently we only support linked document in multi-targeting project scenario. + /// + protected abstract Task> GetValidContainersFromAllLinkedDocumentsAsync(Document document, SyntaxNode container, CancellationToken cancellationToken); - private static bool IsValidContainer(SyntaxNode container) - => container is TCompilationUnitSyntax or TNamespaceDeclarationSyntax; + private static bool IsValidContainer(SyntaxNode container) + => container is TCompilationUnitSyntax or TNamespaceDeclarationSyntax; - protected static bool IsGlobalNamespace(ImmutableArray parts) - => parts is [""]; + protected static bool IsGlobalNamespace(ImmutableArray parts) + => parts is [""]; - public override async Task CanChangeNamespaceAsync(Document document, SyntaxNode container, CancellationToken cancellationToken) + public override async Task CanChangeNamespaceAsync(Document document, SyntaxNode container, CancellationToken cancellationToken) + { + if (!IsValidContainer(container)) { - if (!IsValidContainer(container)) - { - throw new ArgumentException(nameof(container)); - } - - var applicableContainers = await GetValidContainersFromAllLinkedDocumentsAsync(document, container, cancellationToken).ConfigureAwait(false); - return !applicableContainers.IsDefault; + throw new ArgumentException(nameof(container)); } - public override async Task TryChangeTopLevelNamespacesAsync( - Document document, - string targetNamespace, - CodeCleanupOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - var syntaxFacts = document.GetRequiredLanguageService(); - var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - // Don't descend into anything other than top level declarations from the root. - // ChangeNamespaceService only controls top level declarations right now. - // Don't use namespaces that already match the target namespace - var originalNamespaceDeclarations = await GetTopLevelNamespacesAsync(document, cancellationToken).ConfigureAwait(false); + var applicableContainers = await GetValidContainersFromAllLinkedDocumentsAsync(document, container, cancellationToken).ConfigureAwait(false); + return !applicableContainers.IsDefault; + } - if (originalNamespaceDeclarations.Length == 0) - { - return null; - } + public override async Task TryChangeTopLevelNamespacesAsync( + Document document, + string targetNamespace, + CodeCleanupOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var originalNamespaceName = semanticModel.GetRequiredDeclaredSymbol(originalNamespaceDeclarations.First(), cancellationToken).ToDisplayString(); - var solution = document.Project.Solution; + // Don't descend into anything other than top level declarations from the root. + // ChangeNamespaceService only controls top level declarations right now. + // Don't use namespaces that already match the target namespace + var originalNamespaceDeclarations = await GetTopLevelNamespacesAsync(document, cancellationToken).ConfigureAwait(false); - // Only loop as many top level namespace declarations as we originally had. - // Change namespace doesn't change this number, so this helps limit us and - // rule out namespaces that didn't need to be changed - for (var i = 0; i < originalNamespaceDeclarations.Length; i++) - { - var namespaceName = semanticModel.GetRequiredDeclaredSymbol(originalNamespaceDeclarations[i], cancellationToken).ToDisplayString(); - if (namespaceName != originalNamespaceName) - { - // Skip all namespaces that didn't match the original namespace name that - // we were syncing. - continue; - } - - syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (originalNamespaceDeclarations.Length == 0) + { + return null; + } - // Since the original namespaces were retrieved before the document was modified - // get the current top level namespaces. Since we're only renaming namespaces, the - // number and index of each is the same. - var namespaces = await GetTopLevelNamespacesAsync(document, cancellationToken).ConfigureAwait(false); - Debug.Assert(namespaces.Length == originalNamespaceDeclarations.Length); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var originalNamespaceName = semanticModel.GetRequiredDeclaredSymbol(originalNamespaceDeclarations.First(), cancellationToken).ToDisplayString(); + var solution = document.Project.Solution; - var namespaceToRename = namespaces[i]; - solution = await ChangeNamespaceAsync(document, namespaceToRename, targetNamespace, fallbackOptions, cancellationToken).ConfigureAwait(false); - document = solution.GetRequiredDocument(document.Id); + // Only loop as many top level namespace declarations as we originally had. + // Change namespace doesn't change this number, so this helps limit us and + // rule out namespaces that didn't need to be changed + for (var i = 0; i < originalNamespaceDeclarations.Length; i++) + { + var namespaceName = semanticModel.GetRequiredDeclaredSymbol(originalNamespaceDeclarations[i], cancellationToken).ToDisplayString(); + if (namespaceName != originalNamespaceName) + { + // Skip all namespaces that didn't match the original namespace name that + // we were syncing. + continue; } - return solution; - static async Task> GetTopLevelNamespacesAsync(Document document, CancellationToken cancellationToken) - { - var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); + syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - return syntaxRoot - .DescendantNodes(n => !syntaxFacts.IsDeclaration(n)) - .Where(syntaxFacts.IsBaseNamespaceDeclaration) - .ToImmutableArray(); - } + // Since the original namespaces were retrieved before the document was modified + // get the current top level namespaces. Since we're only renaming namespaces, the + // number and index of each is the same. + var namespaces = await GetTopLevelNamespacesAsync(document, cancellationToken).ConfigureAwait(false); + Debug.Assert(namespaces.Length == originalNamespaceDeclarations.Length); + + var namespaceToRename = namespaces[i]; + solution = await ChangeNamespaceAsync(document, namespaceToRename, targetNamespace, fallbackOptions, cancellationToken).ConfigureAwait(false); + document = solution.GetRequiredDocument(document.Id); } - public override async Task ChangeNamespaceAsync( - Document document, - SyntaxNode container, - string targetNamespace, - CodeCleanupOptionsProvider fallbackOptions, - CancellationToken cancellationToken) + return solution; + static async Task> GetTopLevelNamespacesAsync(Document document, CancellationToken cancellationToken) { - // Make sure given namespace name is valid, "" means global namespace. + var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var syntaxFacts = document.GetRequiredLanguageService(); - if (targetNamespace == null - || (targetNamespace.Length > 0 && !targetNamespace.Split(s_dotSeparator).All(syntaxFacts.IsValidIdentifier))) - { - throw new ArgumentException(nameof(targetNamespace)); - } - - if (!IsValidContainer(container)) - { - throw new ArgumentException(nameof(container)); - } - var solution = document.Project.Solution; + return syntaxRoot + .DescendantNodes(n => !syntaxFacts.IsDeclaration(n)) + .Where(syntaxFacts.IsBaseNamespaceDeclaration) + .ToImmutableArray(); + } + } - var containersFromAllDocuments = await GetValidContainersFromAllLinkedDocumentsAsync(document, container, cancellationToken).ConfigureAwait(false); - if (containersFromAllDocuments.IsDefault) - { - return solution; - } + public override async Task ChangeNamespaceAsync( + Document document, + SyntaxNode container, + string targetNamespace, + CodeCleanupOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + // Make sure given namespace name is valid, "" means global namespace. + var syntaxFacts = document.GetRequiredLanguageService(); + if (targetNamespace == null + || (targetNamespace.Length > 0 && !targetNamespace.Split(s_dotSeparator).All(syntaxFacts.IsValidIdentifier))) + { + throw new ArgumentException(nameof(targetNamespace)); + } - // No action required if declared namespace already matches target. - var declaredNamespace = GetDeclaredNamespace(container); - if (syntaxFacts.StringComparer.Equals(targetNamespace, declaredNamespace)) - { - return solution; - } + if (!IsValidContainer(container)) + { + throw new ArgumentException(nameof(container)); + } - // Annotate the container nodes so we can still find and modify them after syntax tree has changed. - var annotatedSolution = await AnnotateContainersAsync(solution, containersFromAllDocuments, cancellationToken).ConfigureAwait(false); + var solution = document.Project.Solution; - // Here's the entire process for changing namespace: - // 1. Change the namespace declaration, fix references and add imports that might be necessary. - // 2. Explicitly merge the diff to get a new solution. - // 3. Remove added imports that are unnecessary. - // 4. Do another explicit diff merge based on last merged solution. - // - // The reason for doing explicit diff merge twice is so merging after remove unnecessary imports can be correctly handled. + var containersFromAllDocuments = await GetValidContainersFromAllLinkedDocumentsAsync(document, container, cancellationToken).ConfigureAwait(false); + if (containersFromAllDocuments.IsDefault) + { + return solution; + } - var documentIds = containersFromAllDocuments.SelectAsArray(pair => pair.id); - var solutionAfterNamespaceChange = annotatedSolution; - using var _ = PooledHashSet.GetInstance(out var referenceDocuments); + // No action required if declared namespace already matches target. + var declaredNamespace = GetDeclaredNamespace(container); + if (syntaxFacts.StringComparer.Equals(targetNamespace, declaredNamespace)) + { + return solution; + } - foreach (var documentId in documentIds) - { - var (newSolution, refDocumentIds) = - await ChangeNamespaceInSingleDocumentAsync(solutionAfterNamespaceChange, documentId, declaredNamespace, targetNamespace, fallbackOptions, cancellationToken) - .ConfigureAwait(false); - solutionAfterNamespaceChange = newSolution; - referenceDocuments.AddRange(refDocumentIds); - } + // Annotate the container nodes so we can still find and modify them after syntax tree has changed. + var annotatedSolution = await AnnotateContainersAsync(solution, containersFromAllDocuments, cancellationToken).ConfigureAwait(false); - var solutionAfterFirstMerge = await MergeDiffAsync(solution, solutionAfterNamespaceChange, cancellationToken).ConfigureAwait(false); + // Here's the entire process for changing namespace: + // 1. Change the namespace declaration, fix references and add imports that might be necessary. + // 2. Explicitly merge the diff to get a new solution. + // 3. Remove added imports that are unnecessary. + // 4. Do another explicit diff merge based on last merged solution. + // + // The reason for doing explicit diff merge twice is so merging after remove unnecessary imports can be correctly handled. - // After changing documents, we still need to remove unnecessary imports related to our change. - // We don't try to remove all imports that might become unnecessary/invalid after the namespace change, - // just ones that fully match the old/new namespace. Because it's hard to get it right and will almost - // certainly cause perf issue. - // For example, if we are changing namespace `Foo.Bar` (which is the only namespace declaration with such name) - // to `A.B`, the using of name `Bar` in a different file below would remain untouched, even it's no longer valid: - // - // namespace Foo - // { - // using Bar; - // ~~~~~~~~~ - // } - // - // Also, because we may have added different imports to document that triggered the refactoring - // and the documents that reference affected types declared in changed namespace, we try to remove - // unnecessary imports separately. - - var solutionAfterImportsRemoved = await RemoveUnnecessaryImportsAsync( - solutionAfterFirstMerge, - documentIds, - GetAllNamespaceImportsForDeclaringDocument(declaredNamespace, targetNamespace), - fallbackOptions, - cancellationToken).ConfigureAwait(false); + var documentIds = containersFromAllDocuments.SelectAsArray(pair => pair.id); + var solutionAfterNamespaceChange = annotatedSolution; + using var _ = PooledHashSet.GetInstance(out var referenceDocuments); - solutionAfterImportsRemoved = await RemoveUnnecessaryImportsAsync( - solutionAfterImportsRemoved, - referenceDocuments.ToImmutableArray(), - [declaredNamespace, targetNamespace], - fallbackOptions, - cancellationToken).ConfigureAwait(false); - - return await MergeDiffAsync(solutionAfterFirstMerge, solutionAfterImportsRemoved, cancellationToken).ConfigureAwait(false); + foreach (var documentId in documentIds) + { + var (newSolution, refDocumentIds) = + await ChangeNamespaceInSingleDocumentAsync(solutionAfterNamespaceChange, documentId, declaredNamespace, targetNamespace, fallbackOptions, cancellationToken) + .ConfigureAwait(false); + solutionAfterNamespaceChange = newSolution; + referenceDocuments.AddRange(refDocumentIds); } - protected async Task> TryGetApplicableContainersFromAllDocumentsAsync( - Solution solution, - ImmutableArray ids, - TextSpan span, - CancellationToken cancellationToken) - { - // If the node specified by span doesn't meet the requirement to be an applicable container in any of the documents - // (See `TryGetApplicableContainerFromSpanAsync`), or we are getting different namespace declarations among - // those documents, then we know we can't make a proper code change. We will return null and the check - // will return false. We use span of namespace declaration found in each document to decide if they are identical. + var solutionAfterFirstMerge = await MergeDiffAsync(solution, solutionAfterNamespaceChange, cancellationToken).ConfigureAwait(false); + + // After changing documents, we still need to remove unnecessary imports related to our change. + // We don't try to remove all imports that might become unnecessary/invalid after the namespace change, + // just ones that fully match the old/new namespace. Because it's hard to get it right and will almost + // certainly cause perf issue. + // For example, if we are changing namespace `Foo.Bar` (which is the only namespace declaration with such name) + // to `A.B`, the using of name `Bar` in a different file below would remain untouched, even it's no longer valid: + // + // namespace Foo + // { + // using Bar; + // ~~~~~~~~~ + // } + // + // Also, because we may have added different imports to document that triggered the refactoring + // and the documents that reference affected types declared in changed namespace, we try to remove + // unnecessary imports separately. + + var solutionAfterImportsRemoved = await RemoveUnnecessaryImportsAsync( + solutionAfterFirstMerge, + documentIds, + GetAllNamespaceImportsForDeclaringDocument(declaredNamespace, targetNamespace), + fallbackOptions, + cancellationToken).ConfigureAwait(false); + + solutionAfterImportsRemoved = await RemoveUnnecessaryImportsAsync( + solutionAfterImportsRemoved, + referenceDocuments.ToImmutableArray(), + [declaredNamespace, targetNamespace], + fallbackOptions, + cancellationToken).ConfigureAwait(false); + + return await MergeDiffAsync(solutionAfterFirstMerge, solutionAfterImportsRemoved, cancellationToken).ConfigureAwait(false); + } - var documents = ids.SelectAsArray(solution.GetRequiredDocument); - using var _1 = ArrayBuilder<(DocumentId, SyntaxNode)>.GetInstance(ids.Length, out var containers); - using var _2 = PooledHashSet.GetInstance(out var spanForContainers); + protected async Task> TryGetApplicableContainersFromAllDocumentsAsync( + Solution solution, + ImmutableArray ids, + TextSpan span, + CancellationToken cancellationToken) + { + // If the node specified by span doesn't meet the requirement to be an applicable container in any of the documents + // (See `TryGetApplicableContainerFromSpanAsync`), or we are getting different namespace declarations among + // those documents, then we know we can't make a proper code change. We will return null and the check + // will return false. We use span of namespace declaration found in each document to decide if they are identical. - foreach (var document in documents) - { - var container = await TryGetApplicableContainerFromSpanAsync(document, span, cancellationToken).ConfigureAwait(false); + var documents = ids.SelectAsArray(solution.GetRequiredDocument); + using var _1 = ArrayBuilder<(DocumentId, SyntaxNode)>.GetInstance(ids.Length, out var containers); + using var _2 = PooledHashSet.GetInstance(out var spanForContainers); - if (container is TNamespaceDeclarationSyntax) - { - spanForContainers.Add(container.Span); - } - else if (container is TCompilationUnitSyntax) - { - // In case there's no namespace declaration in the document, we used an empty span as key, - // since a valid namespace declaration node can't have zero length. - spanForContainers.Add(default); - } - else - { - return default; - } + foreach (var document in documents) + { + var container = await TryGetApplicableContainerFromSpanAsync(document, span, cancellationToken).ConfigureAwait(false); - containers.Add((document.Id, container)); + if (container is TNamespaceDeclarationSyntax) + { + spanForContainers.Add(container.Span); + } + else if (container is TCompilationUnitSyntax) + { + // In case there's no namespace declaration in the document, we used an empty span as key, + // since a valid namespace declaration node can't have zero length. + spanForContainers.Add(default); + } + else + { + return default; } - return spanForContainers.Count == 1 ? containers.ToImmutable() : default; + containers.Add((document.Id, container)); } - /// - /// Mark container nodes with our annotation so we can keep track of them across syntax modifications. - /// - protected static async Task AnnotateContainersAsync(Solution solution, ImmutableArray<(DocumentId, SyntaxNode)> containers, CancellationToken cancellationToken) - { - var solutionEditor = new SolutionEditor(solution); - foreach (var (id, container) in containers) - { - var documentEditor = await solutionEditor.GetDocumentEditorAsync(id, cancellationToken).ConfigureAwait(false); - documentEditor.ReplaceNode(container, container.WithAdditionalAnnotations(ContainerAnnotation)); - } + return spanForContainers.Count == 1 ? containers.ToImmutable() : default; + } - return solutionEditor.GetChangedSolution(); + /// + /// Mark container nodes with our annotation so we can keep track of them across syntax modifications. + /// + protected static async Task AnnotateContainersAsync(Solution solution, ImmutableArray<(DocumentId, SyntaxNode)> containers, CancellationToken cancellationToken) + { + var solutionEditor = new SolutionEditor(solution); + foreach (var (id, container) in containers) + { + var documentEditor = await solutionEditor.GetDocumentEditorAsync(id, cancellationToken).ConfigureAwait(false); + documentEditor.ReplaceNode(container, container.WithAdditionalAnnotations(ContainerAnnotation)); } - protected async Task ContainsPartialTypeWithMultipleDeclarationsAsync( - Document document, SyntaxNode container, CancellationToken cancellationToken) + return solutionEditor.GetChangedSolution(); + } + + protected async Task ContainsPartialTypeWithMultipleDeclarationsAsync( + Document document, SyntaxNode container, CancellationToken cancellationToken) + { + var memberDecls = GetMemberDeclarationsInContainer(container); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var semanticFacts = document.GetRequiredLanguageService(); + + foreach (var memberDecl in memberDecls) { - var memberDecls = GetMemberDeclarationsInContainer(container); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var semanticFacts = document.GetRequiredLanguageService(); + var memberSymbol = semanticModel.GetDeclaredSymbol(memberDecl, cancellationToken); - foreach (var memberDecl in memberDecls) + // Simplify the check by assuming no multiple partial declarations in one document + if (memberSymbol is INamedTypeSymbol typeSymbol + && typeSymbol.DeclaringSyntaxReferences.Length > 1 + && semanticFacts.IsPartial(typeSymbol, cancellationToken)) { - var memberSymbol = semanticModel.GetDeclaredSymbol(memberDecl, cancellationToken); - - // Simplify the check by assuming no multiple partial declarations in one document - if (memberSymbol is INamedTypeSymbol typeSymbol - && typeSymbol.DeclaringSyntaxReferences.Length > 1 - && semanticFacts.IsPartial(typeSymbol, cancellationToken)) - { - return true; - } + return true; } - - return false; } - protected static bool IsSupportedLinkedDocument(Document document, out ImmutableArray allDocumentIds) - { - var solution = document.Project.Solution; - var linkedDocumentIds = document.GetLinkedDocumentIds(); + return false; + } - // TODO: figure out how to properly determine if and how a document is linked using project system. + protected static bool IsSupportedLinkedDocument(Document document, out ImmutableArray allDocumentIds) + { + var solution = document.Project.Solution; + var linkedDocumentIds = document.GetLinkedDocumentIds(); - // If we found a linked document which is part of a project with different project file, - // then it's an actual linked file (i.e. not a multi-targeting project). We don't support that for now. - if (linkedDocumentIds.Any(static (id, arg) => - !PathUtilities.PathsEqual(arg.solution.GetRequiredDocument(id).Project.FilePath!, arg.document.Project.FilePath!), (solution, document))) - { - allDocumentIds = default; - return false; - } + // TODO: figure out how to properly determine if and how a document is linked using project system. - allDocumentIds = linkedDocumentIds.Add(document.Id); - return true; + // If we found a linked document which is part of a project with different project file, + // then it's an actual linked file (i.e. not a multi-targeting project). We don't support that for now. + if (linkedDocumentIds.Any(static (id, arg) => + !PathUtilities.PathsEqual(arg.solution.GetRequiredDocument(id).Project.FilePath!, arg.document.Project.FilePath!), (solution, document))) + { + allDocumentIds = default; + return false; } - private async Task> GetDeclaredSymbolsInContainerAsync( - Document document, - SyntaxNode container, - CancellationToken cancellationToken) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var declarations = GetMemberDeclarationsInContainer(container); - var builder = ArrayBuilder.GetInstance(); + allDocumentIds = linkedDocumentIds.Add(document.Id); + return true; + } - foreach (var declaration in declarations) - { - var symbol = semanticModel.GetDeclaredSymbol(declaration, cancellationToken); - builder.AddIfNotNull(symbol); - } + private async Task> GetDeclaredSymbolsInContainerAsync( + Document document, + SyntaxNode container, + CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var declarations = GetMemberDeclarationsInContainer(container); + var builder = ArrayBuilder.GetInstance(); - return builder.ToImmutableAndFree(); + foreach (var declaration in declarations) + { + var symbol = semanticModel.GetDeclaredSymbol(declaration, cancellationToken); + builder.AddIfNotNull(symbol); } - private static ImmutableArray GetNamespaceParts(string @namespace) - => @namespace?.Split(s_dotSeparator).ToImmutableArray() ?? default; + return builder.ToImmutableAndFree(); + } + + private static ImmutableArray GetNamespaceParts(string @namespace) + => @namespace?.Split(s_dotSeparator).ToImmutableArray() ?? default; - private static ImmutableArray GetAllNamespaceImportsForDeclaringDocument(string oldNamespace, string newNamespace) + private static ImmutableArray GetAllNamespaceImportsForDeclaringDocument(string oldNamespace, string newNamespace) + { + var parts = GetNamespaceParts(oldNamespace); + var builder = ArrayBuilder.GetInstance(); + for (var i = 1; i <= parts.Length; ++i) { - var parts = GetNamespaceParts(oldNamespace); - var builder = ArrayBuilder.GetInstance(); - for (var i = 1; i <= parts.Length; ++i) - { - builder.Add(string.Join(".", parts.Take(i))); - } + builder.Add(string.Join(".", parts.Take(i))); + } - builder.Add(newNamespace); + builder.Add(newNamespace); - return builder.ToImmutableAndFree(); - } + return builder.ToImmutableAndFree(); + } - private static ImmutableArray CreateImports(Document document, ImmutableArray names, bool withFormatterAnnotation) - { - var generator = SyntaxGenerator.GetGenerator(document); - using var _ = ArrayBuilder.GetInstance(names.Length, out var builder); - for (var i = 0; i < names.Length; ++i) - builder.Add(CreateImport(generator, names[i], withFormatterAnnotation)); + private static ImmutableArray CreateImports(Document document, ImmutableArray names, bool withFormatterAnnotation) + { + var generator = SyntaxGenerator.GetGenerator(document); + using var _ = ArrayBuilder.GetInstance(names.Length, out var builder); + for (var i = 0; i < names.Length; ++i) + builder.Add(CreateImport(generator, names[i], withFormatterAnnotation)); - return builder.ToImmutableAndClear(); - } + return builder.ToImmutableAndClear(); + } - private static SyntaxNode CreateImport(SyntaxGenerator syntaxGenerator, string name, bool withFormatterAnnotation) + private static SyntaxNode CreateImport(SyntaxGenerator syntaxGenerator, string name, bool withFormatterAnnotation) + { + var import = syntaxGenerator.NamespaceImportDeclaration(name); + if (withFormatterAnnotation) { - var import = syntaxGenerator.NamespaceImportDeclaration(name); - if (withFormatterAnnotation) - { - import = import.WithAdditionalAnnotations(Formatter.Annotation); - } - - return import; + import = import.WithAdditionalAnnotations(Formatter.Annotation); } - /// - /// Try to change the namespace declaration in the document (specified by in ). - /// Returns a new solution after changing namespace, and a list of IDs for documents that also changed because they reference - /// the types declared in the changed namespace (not include the document contains the declaration itself). - /// - private async Task<(Solution, ImmutableArray)> ChangeNamespaceInSingleDocumentAsync( - Solution solution, - DocumentId id, - string oldNamespace, - string newNamespace, - CodeCleanupOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - var document = solution.GetRequiredDocument(id); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var container = root.GetAnnotatedNodes(ContainerAnnotation).Single(); + return import; + } + + /// + /// Try to change the namespace declaration in the document (specified by in ). + /// Returns a new solution after changing namespace, and a list of IDs for documents that also changed because they reference + /// the types declared in the changed namespace (not include the document contains the declaration itself). + /// + private async Task<(Solution, ImmutableArray)> ChangeNamespaceInSingleDocumentAsync( + Solution solution, + DocumentId id, + string oldNamespace, + string newNamespace, + CodeCleanupOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var document = solution.GetRequiredDocument(id); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var container = root.GetAnnotatedNodes(ContainerAnnotation).Single(); - // Get types declared in the changing namespace, because we need to fix all references to them, - // e.g. change the namespace for qualified name, add imports to proper containers, etc. - var declaredSymbols = await GetDeclaredSymbolsInContainerAsync(document, container, cancellationToken).ConfigureAwait(false); + // Get types declared in the changing namespace, because we need to fix all references to them, + // e.g. change the namespace for qualified name, add imports to proper containers, etc. + var declaredSymbols = await GetDeclaredSymbolsInContainerAsync(document, container, cancellationToken).ConfigureAwait(false); - var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - // Separating references to declaredSymbols into two groups based on whether it's located in the same - // document as the namespace declaration. This is because code change required for them are different. - var refLocationsInCurrentDocument = new List(); - var refLocationsInOtherDocuments = new List(); + // Separating references to declaredSymbols into two groups based on whether it's located in the same + // document as the namespace declaration. This is because code change required for them are different. + var refLocationsInCurrentDocument = new List(); + var refLocationsInOtherDocuments = new List(); - var refLocations = await Task.WhenAll( - declaredSymbols.Select(declaredSymbol - => FindReferenceLocationsForSymbolAsync(document, declaredSymbol, cancellationToken))).ConfigureAwait(false); + var refLocations = await Task.WhenAll( + declaredSymbols.Select(declaredSymbol + => FindReferenceLocationsForSymbolAsync(document, declaredSymbol, cancellationToken))).ConfigureAwait(false); - foreach (var refLocation in refLocations.SelectMany(locs => locs)) + foreach (var refLocation in refLocations.SelectMany(locs => locs)) + { + if (refLocation.Document.Id == document.Id) { - if (refLocation.Document.Id == document.Id) - { - refLocationsInCurrentDocument.Add(refLocation); - } - else - { - RoslynDebug.AssertNotNull(refLocation.Document.FilePath); - RoslynDebug.AssertNotNull(document.FilePath); - Debug.Assert(!PathUtilities.PathsEqual(refLocation.Document.FilePath, document.FilePath)); - refLocationsInOtherDocuments.Add(refLocation); - } + refLocationsInCurrentDocument.Add(refLocation); + } + else + { + RoslynDebug.AssertNotNull(refLocation.Document.FilePath); + RoslynDebug.AssertNotNull(document.FilePath); + Debug.Assert(!PathUtilities.PathsEqual(refLocation.Document.FilePath, document.FilePath)); + refLocationsInOtherDocuments.Add(refLocation); } + } - var documentWithNewNamespace = await FixDeclarationDocumentAsync(document, refLocationsInCurrentDocument, oldNamespace, newNamespace, fallbackOptions, cancellationToken) - .ConfigureAwait(false); - var solutionWithChangedNamespace = documentWithNewNamespace.Project.Solution; + var documentWithNewNamespace = await FixDeclarationDocumentAsync(document, refLocationsInCurrentDocument, oldNamespace, newNamespace, fallbackOptions, cancellationToken) + .ConfigureAwait(false); + var solutionWithChangedNamespace = documentWithNewNamespace.Project.Solution; - var refLocationsInSolution = refLocationsInOtherDocuments - .Where(loc => solutionWithChangedNamespace.ContainsDocument(loc.Document.Id)) - .ToImmutableArray(); + var refLocationsInSolution = refLocationsInOtherDocuments + .Where(loc => solutionWithChangedNamespace.ContainsDocument(loc.Document.Id)) + .ToImmutableArray(); - if (refLocationsInSolution.Length != refLocationsInOtherDocuments.Count) - { - // We have received feedback indicate some documents are not in the solution. - // Report this as non-fatal error if this happens. - FatalError.ReportNonFatalError( - new SyncNamespaceDocumentsNotInSolutionException(refLocationsInOtherDocuments - .Where(loc => !solutionWithChangedNamespace.ContainsDocument(loc.Document.Id)).Distinct().SelectAsArray(loc => loc.Document.Id))); - } + if (refLocationsInSolution.Length != refLocationsInOtherDocuments.Count) + { + // We have received feedback indicate some documents are not in the solution. + // Report this as non-fatal error if this happens. + FatalError.ReportNonFatalError( + new SyncNamespaceDocumentsNotInSolutionException(refLocationsInOtherDocuments + .Where(loc => !solutionWithChangedNamespace.ContainsDocument(loc.Document.Id)).Distinct().SelectAsArray(loc => loc.Document.Id))); + } - var refLocationGroups = refLocationsInSolution.GroupBy(loc => loc.Document.Id); + var refLocationGroups = refLocationsInSolution.GroupBy(loc => loc.Document.Id); - var fixedDocuments = await Task.WhenAll( - refLocationGroups.Select(refInOneDocument => - FixReferencingDocumentAsync( - solutionWithChangedNamespace.GetRequiredDocument(refInOneDocument.Key), - refInOneDocument, - newNamespace, - fallbackOptions, - cancellationToken))).ConfigureAwait(false); + var fixedDocuments = await Task.WhenAll( + refLocationGroups.Select(refInOneDocument => + FixReferencingDocumentAsync( + solutionWithChangedNamespace.GetRequiredDocument(refInOneDocument.Key), + refInOneDocument, + newNamespace, + fallbackOptions, + cancellationToken))).ConfigureAwait(false); - var solutionWithFixedReferences = await MergeDocumentChangesAsync(solutionWithChangedNamespace, fixedDocuments, cancellationToken).ConfigureAwait(false); + var solutionWithFixedReferences = await MergeDocumentChangesAsync(solutionWithChangedNamespace, fixedDocuments, cancellationToken).ConfigureAwait(false); - return (solutionWithFixedReferences, refLocationGroups.SelectAsArray(g => g.Key)); - } + return (solutionWithFixedReferences, refLocationGroups.SelectAsArray(g => g.Key)); + } - private static async Task MergeDocumentChangesAsync(Solution originalSolution, Document[] changedDocuments, CancellationToken cancellationToken) + private static async Task MergeDocumentChangesAsync(Solution originalSolution, Document[] changedDocuments, CancellationToken cancellationToken) + { + foreach (var document in changedDocuments) { - foreach (var document in changedDocuments) - { - originalSolution = originalSolution.WithDocumentSyntaxRoot( - document.Id, - await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false)); - } - - return originalSolution; + originalSolution = originalSolution.WithDocumentSyntaxRoot( + document.Id, + await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false)); } - private readonly struct LocationForAffectedSymbol(ReferenceLocation location, bool isReferenceToExtensionMethod) - { - public ReferenceLocation ReferenceLocation { get; } = location; + return originalSolution; + } - public bool IsReferenceToExtensionMethod { get; } = isReferenceToExtensionMethod; + private readonly struct LocationForAffectedSymbol(ReferenceLocation location, bool isReferenceToExtensionMethod) + { + public ReferenceLocation ReferenceLocation { get; } = location; - public Document Document => ReferenceLocation.Document; - } + public bool IsReferenceToExtensionMethod { get; } = isReferenceToExtensionMethod; - private static async Task> FindReferenceLocationsForSymbolAsync( - Document document, ISymbol symbol, CancellationToken cancellationToken) + public Document Document => ReferenceLocation.Document; + } + + private static async Task> FindReferenceLocationsForSymbolAsync( + Document document, ISymbol symbol, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var builder); + + var referencedSymbols = await FindReferencesAsync(symbol, document, cancellationToken).ConfigureAwait(false); + builder.AddRange(referencedSymbols + .Where(refSymbol => refSymbol.Definition.Equals(symbol)) + .SelectMany(refSymbol => refSymbol.Locations) + .Select(location => new LocationForAffectedSymbol(location, isReferenceToExtensionMethod: false))); + + // So far we only have references to types declared in affected namespace. We also need to + // handle invocation of extension methods (in reduced form) that are declared in those types. + // Therefore additional calls to find references are needed for those extension methods. + // This will returns all the references, not just in the reduced form. But we will + // not further distinguish the usage. In the worst case, those references are redundant because + // they are already covered by the type references found above. + if (symbol is INamedTypeSymbol typeSymbol && typeSymbol.MightContainExtensionMethods) { - using var _ = ArrayBuilder.GetInstance(out var builder); - - var referencedSymbols = await FindReferencesAsync(symbol, document, cancellationToken).ConfigureAwait(false); - builder.AddRange(referencedSymbols - .Where(refSymbol => refSymbol.Definition.Equals(symbol)) - .SelectMany(refSymbol => refSymbol.Locations) - .Select(location => new LocationForAffectedSymbol(location, isReferenceToExtensionMethod: false))); - - // So far we only have references to types declared in affected namespace. We also need to - // handle invocation of extension methods (in reduced form) that are declared in those types. - // Therefore additional calls to find references are needed for those extension methods. - // This will returns all the references, not just in the reduced form. But we will - // not further distinguish the usage. In the worst case, those references are redundant because - // they are already covered by the type references found above. - if (symbol is INamedTypeSymbol typeSymbol && typeSymbol.MightContainExtensionMethods) + foreach (var methodSymbol in typeSymbol.GetMembers().OfType()) { - foreach (var methodSymbol in typeSymbol.GetMembers().OfType()) + if (methodSymbol.IsExtensionMethod) { - if (methodSymbol.IsExtensionMethod) - { - var referencedMethodSymbols = await FindReferencesAsync(methodSymbol, document, cancellationToken).ConfigureAwait(false); - builder.AddRange(referencedMethodSymbols - .SelectMany(refSymbol => refSymbol.Locations) - .Select(location => new LocationForAffectedSymbol(location, isReferenceToExtensionMethod: true))); - } + var referencedMethodSymbols = await FindReferencesAsync(methodSymbol, document, cancellationToken).ConfigureAwait(false); + builder.AddRange(referencedMethodSymbols + .SelectMany(refSymbol => refSymbol.Locations) + .Select(location => new LocationForAffectedSymbol(location, isReferenceToExtensionMethod: true))); } } - - return builder.ToImmutable(); } - private static async Task> FindReferencesAsync(ISymbol symbol, Document document, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var progress = new StreamingProgressCollector(); - await SymbolFinder.FindReferencesAsync( - symbol, document.Project.Solution, progress, documents: null, - FindReferencesSearchOptions.Default, cancellationToken).ConfigureAwait(false); + return builder.ToImmutable(); + } - return progress.GetReferencedSymbols(); - } + private static async Task> FindReferencesAsync(ISymbol symbol, Document document, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var progress = new StreamingProgressCollector(); + await SymbolFinder.FindReferencesAsync( + symbol, document.Project.Solution, progress, documents: null, + FindReferencesSearchOptions.Default, cancellationToken).ConfigureAwait(false); - private async Task FixDeclarationDocumentAsync( - Document document, - IReadOnlyList refLocations, - string oldNamespace, - string newNamespace, - CodeCleanupOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - Debug.Assert(newNamespace != null); + return progress.GetReferencedSymbols(); + } - // 1. Fix references to the affected types in this document if necessary. - // 2. Add usings for containing namespaces, in case we have references - // relying on old namespace declaration for resolution. - // - // For example, in the code below, after we change namespace to - // "A.B.C", we will need to add "using Foo.Bar;". - // - // namespace Foo.Bar.Baz - // { - // class C1 - // { - // C2 _c2; // C2 is define in namespace "Foo.Bar" in another document. - // } - // } - // - // 3. Change namespace declaration to target namespace. - // 4. Simplify away unnecessary qualifications. + private async Task FixDeclarationDocumentAsync( + Document document, + IReadOnlyList refLocations, + string oldNamespace, + string newNamespace, + CodeCleanupOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + Debug.Assert(newNamespace != null); + + // 1. Fix references to the affected types in this document if necessary. + // 2. Add usings for containing namespaces, in case we have references + // relying on old namespace declaration for resolution. + // + // For example, in the code below, after we change namespace to + // "A.B.C", we will need to add "using Foo.Bar;". + // + // namespace Foo.Bar.Baz + // { + // class C1 + // { + // C2 _c2; // C2 is define in namespace "Foo.Bar" in another document. + // } + // } + // + // 3. Change namespace declaration to target namespace. + // 4. Simplify away unnecessary qualifications. + + var addImportService = document.GetRequiredLanguageService(); + ImmutableArray containersToAddImports; + + var oldNamespaceParts = GetNamespaceParts(oldNamespace); + var newNamespaceParts = GetNamespaceParts(newNamespace); + + if (refLocations.Count > 0) + { + (document, containersToAddImports) = await FixReferencesAsync(document, this, addImportService, refLocations, newNamespaceParts, fallbackOptions, cancellationToken) + .ConfigureAwait(false); + } + else + { + // If there's no reference to types declared in this document, + // we will use root node as import container. + containersToAddImports = [await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false)]; + } - var addImportService = document.GetRequiredLanguageService(); - ImmutableArray containersToAddImports; + Debug.Assert(containersToAddImports.Length > 0); - var oldNamespaceParts = GetNamespaceParts(oldNamespace); - var newNamespaceParts = GetNamespaceParts(newNamespace); + // Need to import all containing namespaces of old namespace and add them to the document (if it's not global namespace) + // Include the new namespace in case there are multiple namespace declarations in + // the declaring document. They may need a using statement added to correctly keep + // references to the type inside it's new namespace + var namesToImport = GetAllNamespaceImportsForDeclaringDocument(oldNamespace, newNamespace); - if (refLocations.Count > 0) - { - (document, containersToAddImports) = await FixReferencesAsync(document, this, addImportService, refLocations, newNamespaceParts, fallbackOptions, cancellationToken) - .ConfigureAwait(false); - } - else - { - // If there's no reference to types declared in this document, - // we will use root node as import container. - containersToAddImports = [await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false)]; - } + var documentOptions = await document.GetCodeCleanupOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - Debug.Assert(containersToAddImports.Length > 0); + var documentWithAddedImports = await AddImportsInContainersAsync( + document, + addImportService, + containersToAddImports, + namesToImport, + documentOptions.AddImportOptions, + cancellationToken).ConfigureAwait(false); - // Need to import all containing namespaces of old namespace and add them to the document (if it's not global namespace) - // Include the new namespace in case there are multiple namespace declarations in - // the declaring document. They may need a using statement added to correctly keep - // references to the type inside it's new namespace - var namesToImport = GetAllNamespaceImportsForDeclaringDocument(oldNamespace, newNamespace); + var root = await documentWithAddedImports.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var documentOptions = await document.GetCodeCleanupOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + root = ChangeNamespaceDeclaration((TCompilationUnitSyntax)root, oldNamespaceParts, newNamespaceParts) + .WithAdditionalAnnotations(Formatter.Annotation); - var documentWithAddedImports = await AddImportsInContainersAsync( - document, - addImportService, - containersToAddImports, - namesToImport, - documentOptions.AddImportOptions, - cancellationToken).ConfigureAwait(false); + // Need to invoke formatter explicitly since we are doing the diff merge ourselves. + var services = documentWithAddedImports.Project.Solution.Services; + root = Formatter.Format(root, Formatter.Annotation, services, documentOptions.FormattingOptions, cancellationToken); - var root = await documentWithAddedImports.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + root = root.WithAdditionalAnnotations(Simplifier.Annotation); + var formattedDocument = documentWithAddedImports.WithSyntaxRoot(root); + return await Simplifier.ReduceAsync(formattedDocument, documentOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false); + } - root = ChangeNamespaceDeclaration((TCompilationUnitSyntax)root, oldNamespaceParts, newNamespaceParts) - .WithAdditionalAnnotations(Formatter.Annotation); + private static async Task FixReferencingDocumentAsync( + Document document, + IEnumerable refLocations, + string newNamespace, + CodeCleanupOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + // 1. Fully qualify all simple references (i.e. not via an alias) with new namespace. + // 2. Add using of new namespace (for each reference's container). + // 3. Try to simplify qualified names introduced from step(1). - // Need to invoke formatter explicitly since we are doing the diff merge ourselves. - var services = documentWithAddedImports.Project.Solution.Services; - root = Formatter.Format(root, Formatter.Annotation, services, documentOptions.FormattingOptions, cancellationToken); + var addImportService = document.GetRequiredLanguageService(); + var changeNamespaceService = document.GetRequiredLanguageService(); - root = root.WithAdditionalAnnotations(Simplifier.Annotation); - var formattedDocument = documentWithAddedImports.WithSyntaxRoot(root); - return await Simplifier.ReduceAsync(formattedDocument, documentOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false); - } + var newNamespaceParts = GetNamespaceParts(newNamespace); - private static async Task FixReferencingDocumentAsync( - Document document, - IEnumerable refLocations, - string newNamespace, - CodeCleanupOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - // 1. Fully qualify all simple references (i.e. not via an alias) with new namespace. - // 2. Add using of new namespace (for each reference's container). - // 3. Try to simplify qualified names introduced from step(1). + var (documentWithRefFixed, containers) = + await FixReferencesAsync(document, changeNamespaceService, addImportService, refLocations, newNamespaceParts, fallbackOptions, cancellationToken) + .ConfigureAwait(false); - var addImportService = document.GetRequiredLanguageService(); - var changeNamespaceService = document.GetRequiredLanguageService(); + var documentOptions = await document.GetCodeCleanupOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var newNamespaceParts = GetNamespaceParts(newNamespace); + var documentWithAdditionalImports = await AddImportsInContainersAsync( + documentWithRefFixed, + addImportService, + containers, + [newNamespace], + documentOptions.AddImportOptions, + cancellationToken).ConfigureAwait(false); - var (documentWithRefFixed, containers) = - await FixReferencesAsync(document, changeNamespaceService, addImportService, refLocations, newNamespaceParts, fallbackOptions, cancellationToken) - .ConfigureAwait(false); + // Need to invoke formatter explicitly since we are doing the diff merge ourselves. + var formattedDocument = await Formatter.FormatAsync(documentWithAdditionalImports, Formatter.Annotation, documentOptions.FormattingOptions, cancellationToken) + .ConfigureAwait(false); - var documentOptions = await document.GetCodeCleanupOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + return await Simplifier.ReduceAsync(formattedDocument, documentOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false); + } - var documentWithAdditionalImports = await AddImportsInContainersAsync( - documentWithRefFixed, - addImportService, - containers, - [newNamespace], - documentOptions.AddImportOptions, - cancellationToken).ConfigureAwait(false); + /// + /// Fix each reference and return a collection of proper containers (innermost container + /// with imports) that new import should be added to based on reference locations. + /// If is specified (not default), the fix would be: + /// 1. qualify the reference with new namespace and mark it for simplification, or + /// 2. find and mark the qualified reference for simplification. + /// Otherwise, there would be no namespace replacement. + /// + private static async Task<(Document, ImmutableArray)> FixReferencesAsync( + Document document, + IChangeNamespaceService changeNamespaceService, + IAddImportsService addImportService, + IEnumerable refLocations, + ImmutableArray newNamespaceParts, + CodeCleanupOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var root = editor.OriginalRoot; + using var _ = PooledHashSet.GetInstance(out var containers); - // Need to invoke formatter explicitly since we are doing the diff merge ourselves. - var formattedDocument = await Formatter.FormatAsync(documentWithAdditionalImports, Formatter.Annotation, documentOptions.FormattingOptions, cancellationToken) - .ConfigureAwait(false); + var generator = SyntaxGenerator.GetGenerator(document); + var syntaxFacts = document.GetRequiredLanguageService(); + var codeGenerator = document.GetRequiredLanguageService(); - return await Simplifier.ReduceAsync(formattedDocument, documentOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false); - } + // We need a dummy import to figure out the container for given reference. + var dummyImport = CreateImport(generator, "Dummy", withFormatterAnnotation: false); + var abstractChangeNamespaceService = (AbstractChangeNamespaceService)changeNamespaceService; - /// - /// Fix each reference and return a collection of proper containers (innermost container - /// with imports) that new import should be added to based on reference locations. - /// If is specified (not default), the fix would be: - /// 1. qualify the reference with new namespace and mark it for simplification, or - /// 2. find and mark the qualified reference for simplification. - /// Otherwise, there would be no namespace replacement. - /// - private static async Task<(Document, ImmutableArray)> FixReferencesAsync( - Document document, - IChangeNamespaceService changeNamespaceService, - IAddImportsService addImportService, - IEnumerable refLocations, - ImmutableArray newNamespaceParts, - CodeCleanupOptionsProvider fallbackOptions, - CancellationToken cancellationToken) + foreach (var refLoc in refLocations) { - var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - var root = editor.OriginalRoot; - using var _ = PooledHashSet.GetInstance(out var containers); - - var generator = SyntaxGenerator.GetGenerator(document); - var syntaxFacts = document.GetRequiredLanguageService(); - var codeGenerator = document.GetRequiredLanguageService(); + Debug.Assert(document.Id == refLoc.Document.Id); - // We need a dummy import to figure out the container for given reference. - var dummyImport = CreateImport(generator, "Dummy", withFormatterAnnotation: false); - var abstractChangeNamespaceService = (AbstractChangeNamespaceService)changeNamespaceService; - - foreach (var refLoc in refLocations) + // Ignore references via alias. For simple cases where the alias is defined as the type we are interested, + // it will be handled properly because it is one of the reference to the type symbol. Otherwise, we don't + // attempt to make a potential fix, and user might end up with errors as a result. + if (refLoc.ReferenceLocation.Alias != null) { - Debug.Assert(document.Id == refLoc.Document.Id); - - // Ignore references via alias. For simple cases where the alias is defined as the type we are interested, - // it will be handled properly because it is one of the reference to the type symbol. Otherwise, we don't - // attempt to make a potential fix, and user might end up with errors as a result. - if (refLoc.ReferenceLocation.Alias != null) - { - continue; - } + continue; + } - // Other documents in the solution might have changed after we calculated those ReferenceLocation, - // so we can't trust anything to be still up-to-date except their spans. + // Other documents in the solution might have changed after we calculated those ReferenceLocation, + // so we can't trust anything to be still up-to-date except their spans. - // Get inner most node in case of type used as a base type. e.g. - // - // public class Foo {} - // public class Bar : Foo {} - // - // For the reference to Foo where it is used as a base class, the BaseTypeSyntax and the TypeSyntax - // have exact same span. + // Get inner most node in case of type used as a base type. e.g. + // + // public class Foo {} + // public class Bar : Foo {} + // + // For the reference to Foo where it is used as a base class, the BaseTypeSyntax and the TypeSyntax + // have exact same span. - var refNode = root.FindNode(refLoc.ReferenceLocation.Location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true); + var refNode = root.FindNode(refLoc.ReferenceLocation.Location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true); - // For invocation of extension method, we only need to add missing import. - if (!refLoc.IsReferenceToExtensionMethod) + // For invocation of extension method, we only need to add missing import. + if (!refLoc.IsReferenceToExtensionMethod) + { + if (abstractChangeNamespaceService.TryGetReplacementReferenceSyntax( + refNode, newNamespaceParts, syntaxFacts, out var oldNode, out var newNode)) { - if (abstractChangeNamespaceService.TryGetReplacementReferenceSyntax( - refNode, newNamespaceParts, syntaxFacts, out var oldNode, out var newNode)) - { - editor.ReplaceNode(oldNode, newNode.WithAdditionalAnnotations(Simplifier.Annotation)); - } + editor.ReplaceNode(oldNode, newNode.WithAdditionalAnnotations(Simplifier.Annotation)); } - - var addImportsOptions = await document.GetAddImportPlacementOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - - // Use a dummy import node to figure out which container the new import will be added to. - var container = addImportService.GetImportContainer(root, refNode, dummyImport, addImportsOptions); - containers.Add(container); - } - - foreach (var container in containers) - { - editor.TrackNode(container); } - var fixedDocument = editor.GetChangedDocument(); - root = await fixedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var result = (fixedDocument, containers.SelectAsArray(c => root.GetCurrentNode(c) - ?? throw new InvalidOperationException("Can't get SyntaxNode from GetCurrentNode."))); + var addImportsOptions = await document.GetAddImportPlacementOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - return result; + // Use a dummy import node to figure out which container the new import will be added to. + var container = addImportService.GetImportContainer(root, refNode, dummyImport, addImportsOptions); + containers.Add(container); } - private static async Task RemoveUnnecessaryImportsAsync( - Solution solution, - ImmutableArray ids, - ImmutableArray names, - CodeCleanupOptionsProvider fallbackOptions, - CancellationToken cancellationToken) + foreach (var container in containers) { - using var _ = PooledHashSet.GetInstance(out var linkedDocumentsToSkip); - var documentsToProcessBuilder = ArrayBuilder.GetInstance(); + editor.TrackNode(container); + } - foreach (var id in ids) - { - if (linkedDocumentsToSkip.Contains(id)) - { - continue; - } + var fixedDocument = editor.GetChangedDocument(); + root = await fixedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var result = (fixedDocument, containers.SelectAsArray(c => root.GetCurrentNode(c) + ?? throw new InvalidOperationException("Can't get SyntaxNode from GetCurrentNode."))); - var document = solution.GetRequiredDocument(id); - linkedDocumentsToSkip.AddRange(document.GetLinkedDocumentIds()); - documentsToProcessBuilder.Add(document); + return result; + } - document = await RemoveUnnecessaryImportsWorkerAsync( - document, - CreateImports(document, names, withFormatterAnnotation: false), - cancellationToken).ConfigureAwait(false); - solution = document.Project.Solution; - } + private static async Task RemoveUnnecessaryImportsAsync( + Solution solution, + ImmutableArray ids, + ImmutableArray names, + CodeCleanupOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + using var _ = PooledHashSet.GetInstance(out var linkedDocumentsToSkip); + var documentsToProcessBuilder = ArrayBuilder.GetInstance(); - var documentsToProcess = documentsToProcessBuilder.ToImmutableAndFree(); + foreach (var id in ids) + { + if (linkedDocumentsToSkip.Contains(id)) + { + continue; + } - var changeDocuments = await Task.WhenAll(documentsToProcess.Select( - doc => RemoveUnnecessaryImportsWorkerAsync( - doc, - CreateImports(doc, names, withFormatterAnnotation: false), - cancellationToken))).ConfigureAwait(false); + var document = solution.GetRequiredDocument(id); + linkedDocumentsToSkip.AddRange(document.GetLinkedDocumentIds()); + documentsToProcessBuilder.Add(document); - return await MergeDocumentChangesAsync(solution, changeDocuments, cancellationToken).ConfigureAwait(false); + document = await RemoveUnnecessaryImportsWorkerAsync( + document, + CreateImports(document, names, withFormatterAnnotation: false), + cancellationToken).ConfigureAwait(false); + solution = document.Project.Solution; + } - async Task RemoveUnnecessaryImportsWorkerAsync( - Document doc, - IEnumerable importsToRemove, - CancellationToken token) - { - var removeImportService = doc.GetRequiredLanguageService(); - var syntaxFacts = doc.GetRequiredLanguageService(); - var formattingOptions = await doc.GetSyntaxFormattingOptionsAsync(fallbackOptions, token).ConfigureAwait(false); + var documentsToProcess = documentsToProcessBuilder.ToImmutableAndFree(); - return await removeImportService.RemoveUnnecessaryImportsAsync( + var changeDocuments = await Task.WhenAll(documentsToProcess.Select( + doc => RemoveUnnecessaryImportsWorkerAsync( doc, - import => importsToRemove.Any(importToRemove => syntaxFacts.AreEquivalent(importToRemove, import)), - formattingOptions, - token).ConfigureAwait(false); - } - } + CreateImports(doc, names, withFormatterAnnotation: false), + cancellationToken))).ConfigureAwait(false); - /// - /// Add imports for the namespace specified by - /// to the provided - /// - private static async Task AddImportsInContainersAsync( - Document document, - IAddImportsService addImportService, - ImmutableArray containers, - ImmutableArray names, - AddImportPlacementOptions options, - CancellationToken cancellationToken) - { - // Sort containers based on their span start, to make the result of - // adding imports deterministic. - if (containers.Length > 1) - { - containers = containers.Sort(SyntaxNodeSpanStartComparer.Instance); - } + return await MergeDocumentChangesAsync(solution, changeDocuments, cancellationToken).ConfigureAwait(false); - var generator = document.GetRequiredLanguageService(); - - var imports = CreateImports(document, names, withFormatterAnnotation: true); - foreach (var container in containers) - { - // If the container is a namespace declaration, the context we pass to - // AddImportService must be a child of the declaration, otherwise the - // import will be added to root node instead. - var contextLocation = container is TNamespaceDeclarationSyntax - ? container.DescendantNodes().First() - : container; - - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - root = addImportService.AddImports(compilation, root, contextLocation, imports, generator, options, cancellationToken); - document = document.WithSyntaxRoot(root); - } + async Task RemoveUnnecessaryImportsWorkerAsync( + Document doc, + IEnumerable importsToRemove, + CancellationToken token) + { + var removeImportService = doc.GetRequiredLanguageService(); + var syntaxFacts = doc.GetRequiredLanguageService(); + var formattingOptions = await doc.GetSyntaxFormattingOptionsAsync(fallbackOptions, token).ConfigureAwait(false); + + return await removeImportService.RemoveUnnecessaryImportsAsync( + doc, + import => importsToRemove.Any(importToRemove => syntaxFacts.AreEquivalent(importToRemove, import)), + formattingOptions, + token).ConfigureAwait(false); + } + } - return document; + /// + /// Add imports for the namespace specified by + /// to the provided + /// + private static async Task AddImportsInContainersAsync( + Document document, + IAddImportsService addImportService, + ImmutableArray containers, + ImmutableArray names, + AddImportPlacementOptions options, + CancellationToken cancellationToken) + { + // Sort containers based on their span start, to make the result of + // adding imports deterministic. + if (containers.Length > 1) + { + containers = containers.Sort(SyntaxNodeSpanStartComparer.Instance); } - private static async Task MergeDiffAsync(Solution oldSolution, Solution newSolution, CancellationToken cancellationToken) + var generator = document.GetRequiredLanguageService(); + + var imports = CreateImports(document, names, withFormatterAnnotation: true); + foreach (var container in containers) { - var diffMergingSession = new LinkedFileDiffMergingSession(oldSolution, newSolution, newSolution.GetChanges(oldSolution)); - var mergeResult = await diffMergingSession.MergeDiffsAsync(mergeConflictHandler: null, cancellationToken: cancellationToken).ConfigureAwait(false); - return mergeResult.MergedSolution; + // If the container is a namespace declaration, the context we pass to + // AddImportService must be a child of the declaration, otherwise the + // import will be added to root node instead. + var contextLocation = container is TNamespaceDeclarationSyntax + ? container.DescendantNodes().First() + : container; + + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + root = addImportService.AddImports(compilation, root, contextLocation, imports, generator, options, cancellationToken); + document = document.WithSyntaxRoot(root); } - private class SyntaxNodeSpanStartComparer : IComparer + return document; + } + + private static async Task MergeDiffAsync(Solution oldSolution, Solution newSolution, CancellationToken cancellationToken) + { + var diffMergingSession = new LinkedFileDiffMergingSession(oldSolution, newSolution, newSolution.GetChanges(oldSolution)); + var mergeResult = await diffMergingSession.MergeDiffsAsync(mergeConflictHandler: null, cancellationToken: cancellationToken).ConfigureAwait(false); + return mergeResult.MergedSolution; + } + + private class SyntaxNodeSpanStartComparer : IComparer + { + private SyntaxNodeSpanStartComparer() { - private SyntaxNodeSpanStartComparer() - { - } + } - public static SyntaxNodeSpanStartComparer Instance { get; } = new SyntaxNodeSpanStartComparer(); + public static SyntaxNodeSpanStartComparer Instance { get; } = new SyntaxNodeSpanStartComparer(); - public int Compare(SyntaxNode? x, SyntaxNode? y) - { - Contract.ThrowIfNull(x); - Contract.ThrowIfNull(y); + public int Compare(SyntaxNode? x, SyntaxNode? y) + { + Contract.ThrowIfNull(x); + Contract.ThrowIfNull(y); - return x.Span.Start - y.Span.Start; - } + return x.Span.Start - y.Span.Start; } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.MoveFileCodeAction.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.MoveFileCodeAction.cs index 5258d656d9ce8..bed1fb9e5d44f 100644 --- a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.MoveFileCodeAction.cs +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.MoveFileCodeAction.cs @@ -15,163 +15,162 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace +namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace; + +internal abstract partial class AbstractSyncNamespaceCodeRefactoringProvider + : CodeRefactoringProvider + where TNamespaceDeclarationSyntax : SyntaxNode + where TCompilationUnitSyntax : SyntaxNode + where TMemberDeclarationSyntax : SyntaxNode { - internal abstract partial class AbstractSyncNamespaceCodeRefactoringProvider - : CodeRefactoringProvider - where TNamespaceDeclarationSyntax : SyntaxNode - where TCompilationUnitSyntax : SyntaxNode - where TMemberDeclarationSyntax : SyntaxNode + private sealed class MoveFileCodeAction(State state, ImmutableArray newFolders) : CodeAction { - private sealed class MoveFileCodeAction(State state, ImmutableArray newFolders) : CodeAction - { - private readonly State _state = state; - private readonly ImmutableArray _newfolders = newFolders; + private readonly State _state = state; + private readonly ImmutableArray _newfolders = newFolders; - public override string Title - => _newfolders.Length > 0 - ? string.Format(FeaturesResources.Move_file_to_0, string.Join(PathUtilities.DirectorySeparatorStr, _newfolders)) - : FeaturesResources.Move_file_to_project_root_folder; + public override string Title + => _newfolders.Length > 0 + ? string.Format(FeaturesResources.Move_file_to_0, string.Join(PathUtilities.DirectorySeparatorStr, _newfolders)) + : FeaturesResources.Move_file_to_project_root_folder; - protected override async Task> ComputeOperationsAsync( - IProgress progress, CancellationToken cancellationToken) - { - var document = _state.Document; - var solution = _state.Document.Project.Solution; - var newDocumentId = DocumentId.CreateNewId(document.Project.Id, document.Name); + protected override async Task> ComputeOperationsAsync( + IProgress progress, CancellationToken cancellationToken) + { + var document = _state.Document; + var solution = _state.Document.Project.Solution; + var newDocumentId = DocumentId.CreateNewId(document.Project.Id, document.Name); - solution = solution.RemoveDocument(document.Id); + solution = solution.RemoveDocument(document.Id); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - solution = solution.AddDocument(newDocumentId, document.Name, text, folders: _newfolders); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + solution = solution.AddDocument(newDocumentId, document.Name, text, folders: _newfolders); - return [new ApplyChangesOperation(solution), new OpenDocumentOperation(newDocumentId, activateIfAlreadyOpen: true)]; - } + return [new ApplyChangesOperation(solution), new OpenDocumentOperation(newDocumentId, activateIfAlreadyOpen: true)]; + } - public static ImmutableArray Create(State state) + public static ImmutableArray Create(State state) + { + Debug.Assert(state.RelativeDeclaredNamespace != null); + + // Since all documents have identical folder structure, we can do the computation on any of them. + var document = state.Document; + // In case the relative namespace is "", the file should be moved to project root, + // set `parts` to empty to indicate that. + var parts = state.RelativeDeclaredNamespace.Length == 0 + ? [] + : state.RelativeDeclaredNamespace.Split(['.']).ToImmutableArray(); + + // Invalid char can only appear in namespace name when there's error, + // which we have checked before creating any code actions. + Debug.Assert(parts.IsEmpty || parts.Any(static s => s.IndexOfAny(Path.GetInvalidPathChars()) < 0)); + + var projectRootFolder = FolderInfo.CreateFolderHierarchyForProject(document.Project); + var candidateFolders = FindCandidateFolders(projectRootFolder, parts, []); + return candidateFolders.SelectAsArray(folders => new MoveFileCodeAction(state, folders)); + } + + /// + /// We try to provide additional "move file" options if we can find existing folders that matches target namespace. + /// For example, if the target namespace is 'DefaultNamesapce.A.B.C', and there's a folder 'ProjectRoot\A.B\' already + /// exists, then will provide two actions, "move file to ProjectRoot\A.B\C\" and "move file to ProjectRoot\A\B\C\". + /// + private static ImmutableArray> FindCandidateFolders( + FolderInfo currentFolderInfo, + ImmutableArray parts, + ImmutableArray currentFolder) + { + if (parts.IsEmpty) { - Debug.Assert(state.RelativeDeclaredNamespace != null); - - // Since all documents have identical folder structure, we can do the computation on any of them. - var document = state.Document; - // In case the relative namespace is "", the file should be moved to project root, - // set `parts` to empty to indicate that. - var parts = state.RelativeDeclaredNamespace.Length == 0 - ? [] - : state.RelativeDeclaredNamespace.Split(['.']).ToImmutableArray(); - - // Invalid char can only appear in namespace name when there's error, - // which we have checked before creating any code actions. - Debug.Assert(parts.IsEmpty || parts.Any(static s => s.IndexOfAny(Path.GetInvalidPathChars()) < 0)); - - var projectRootFolder = FolderInfo.CreateFolderHierarchyForProject(document.Project); - var candidateFolders = FindCandidateFolders(projectRootFolder, parts, []); - return candidateFolders.SelectAsArray(folders => new MoveFileCodeAction(state, folders)); + return [currentFolder]; } - /// - /// We try to provide additional "move file" options if we can find existing folders that matches target namespace. - /// For example, if the target namespace is 'DefaultNamesapce.A.B.C', and there's a folder 'ProjectRoot\A.B\' already - /// exists, then will provide two actions, "move file to ProjectRoot\A.B\C\" and "move file to ProjectRoot\A\B\C\". - /// - private static ImmutableArray> FindCandidateFolders( - FolderInfo currentFolderInfo, - ImmutableArray parts, - ImmutableArray currentFolder) + // Try to figure out all possible folder names that can match the target namespace. + // For example, if the target is "A.B.C", then the matching folder names include + // "A", "A.B" and "A.B.C". The item "index" in the result tuple is the number + // of items in namespace parts used to construct iten "foldername". + var candidates = Enumerable.Range(1, parts.Length) + .Select(i => (foldername: string.Join(".", parts.Take(i)), index: i)) + .ToImmutableDictionary(t => t.foldername, t => t.index, PathUtilities.Comparer); + + var subFolders = currentFolderInfo.ChildFolders; + + var builder = ArrayBuilder>.GetInstance(); + foreach (var (folderName, index) in candidates) { - if (parts.IsEmpty) + if (subFolders.TryGetValue(folderName, out var matchingFolderInfo)) { - return [currentFolder]; + var newParts = index >= parts.Length + ? [] + : ImmutableArray.Create(parts, index, parts.Length - index); + var newCurrentFolder = currentFolder.Add(matchingFolderInfo.Name); + builder.AddRange(FindCandidateFolders(matchingFolderInfo, newParts, newCurrentFolder)); } + } - // Try to figure out all possible folder names that can match the target namespace. - // For example, if the target is "A.B.C", then the matching folder names include - // "A", "A.B" and "A.B.C". The item "index" in the result tuple is the number - // of items in namespace parts used to construct iten "foldername". - var candidates = Enumerable.Range(1, parts.Length) - .Select(i => (foldername: string.Join(".", parts.Take(i)), index: i)) - .ToImmutableDictionary(t => t.foldername, t => t.index, PathUtilities.Comparer); + // Make sure we always have the default path as an available option to the user + // (which might have been found by the search above, therefore the check here) + // For example, if the target namespace is "A.B.C.D", and there's folder \A.B\, + // the search above would only return "\A.B\C\D". We'd want to provide + // "\A\B\C\D" as the default path. + var defaultPathBasedOnCurrentFolder = currentFolder.AddRange(parts); + if (builder.All(folders => !folders.SequenceEqual(defaultPathBasedOnCurrentFolder, PathUtilities.Comparer))) + { + builder.Add(defaultPathBasedOnCurrentFolder); + } - var subFolders = currentFolderInfo.ChildFolders; + return builder.ToImmutableAndFree(); + } - var builder = ArrayBuilder>.GetInstance(); - foreach (var (folderName, index) in candidates) - { - if (subFolders.TryGetValue(folderName, out var matchingFolderInfo)) - { - var newParts = index >= parts.Length - ? [] - : ImmutableArray.Create(parts, index, parts.Length - index); - var newCurrentFolder = currentFolder.Add(matchingFolderInfo.Name); - builder.AddRange(FindCandidateFolders(matchingFolderInfo, newParts, newCurrentFolder)); - } - } + private class FolderInfo + { + private readonly Dictionary _childFolders; - // Make sure we always have the default path as an available option to the user - // (which might have been found by the search above, therefore the check here) - // For example, if the target namespace is "A.B.C.D", and there's folder \A.B\, - // the search above would only return "\A.B\C\D". We'd want to provide - // "\A\B\C\D" as the default path. - var defaultPathBasedOnCurrentFolder = currentFolder.AddRange(parts); - if (builder.All(folders => !folders.SequenceEqual(defaultPathBasedOnCurrentFolder, PathUtilities.Comparer))) - { - builder.Add(defaultPathBasedOnCurrentFolder); - } + public string Name { get; } - return builder.ToImmutableAndFree(); - } + public IReadOnlyDictionary ChildFolders => _childFolders; - private class FolderInfo + private FolderInfo(string name) { - private readonly Dictionary _childFolders; - - public string Name { get; } - - public IReadOnlyDictionary ChildFolders => _childFolders; + Name = name; + _childFolders = new Dictionary(StringComparer.Ordinal); + } - private FolderInfo(string name) + private void AddFolder(IEnumerable folder) + { + if (!folder.Any()) { - Name = name; - _childFolders = new Dictionary(StringComparer.Ordinal); + return; } - private void AddFolder(IEnumerable folder) + var firstFolder = folder.First(); + if (!_childFolders.TryGetValue(firstFolder, out var firstFolderInfo)) { - if (!folder.Any()) - { - return; - } + firstFolderInfo = new FolderInfo(firstFolder); + _childFolders[firstFolder] = firstFolderInfo; + } - var firstFolder = folder.First(); - if (!_childFolders.TryGetValue(firstFolder, out var firstFolderInfo)) - { - firstFolderInfo = new FolderInfo(firstFolder); - _childFolders[firstFolder] = firstFolderInfo; - } + firstFolderInfo.AddFolder(folder.Skip(1)); + } - firstFolderInfo.AddFolder(folder.Skip(1)); - } + // TODO: + // Since we are getting folder data from documents, only non-empty folders + // in the project are discovered. It's possible to get complete folder structure + // from VS but it requires UI thread to do so. We might want to revisit this later. + public static FolderInfo CreateFolderHierarchyForProject(Project project) + { + var handledFolders = new HashSet(StringComparer.Ordinal); - // TODO: - // Since we are getting folder data from documents, only non-empty folders - // in the project are discovered. It's possible to get complete folder structure - // from VS but it requires UI thread to do so. We might want to revisit this later. - public static FolderInfo CreateFolderHierarchyForProject(Project project) + var rootFolderInfo = new FolderInfo(""); + foreach (var document in project.Documents) { - var handledFolders = new HashSet(StringComparer.Ordinal); - - var rootFolderInfo = new FolderInfo(""); - foreach (var document in project.Documents) + var folders = document.Folders; + if (handledFolders.Add(string.Join(PathUtilities.DirectorySeparatorStr, folders))) { - var folders = document.Folders; - if (handledFolders.Add(string.Join(PathUtilities.DirectorySeparatorStr, folders))) - { - rootFolderInfo.AddFolder(folders); - } + rootFolderInfo.AddFolder(folders); } - - return rootFolderInfo; } + + return rootFolderInfo; } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.State.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.State.cs index 3c6a25a4978ec..9f9afa686ebeb 100644 --- a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.State.cs +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.State.cs @@ -16,211 +16,210 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace +namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace; + +internal abstract partial class AbstractSyncNamespaceCodeRefactoringProvider + : CodeRefactoringProvider + where TNamespaceDeclarationSyntax : SyntaxNode + where TCompilationUnitSyntax : SyntaxNode + where TMemberDeclarationSyntax : SyntaxNode { - internal abstract partial class AbstractSyncNamespaceCodeRefactoringProvider - : CodeRefactoringProvider - where TNamespaceDeclarationSyntax : SyntaxNode - where TCompilationUnitSyntax : SyntaxNode - where TMemberDeclarationSyntax : SyntaxNode + internal sealed class State { - internal sealed class State + /// + /// The document in which the refactoring is triggered. + /// + public Document Document { get; } + + /// + /// The applicable container node based on cursor location, + /// which will be used to change namespace. + /// + public SyntaxNode Container { get; } + + /// + /// This is the new name we want to change the namespace to. + /// Empty string means global namespace, whereas null means change namespace action is not available. + /// + public string? TargetNamespace { get; } + + /// + /// This is the part of the declared namespace that is contained in default namespace. + /// We will use this to construct target folder to move the file to. + /// For example, if default namespace is `A` and declared namespace is `A.B.C`, + /// this would be `B.C`. + /// + public string? RelativeDeclaredNamespace { get; } + + private State( + Document document, + SyntaxNode container, + string? targetNamespace, + string? relativeDeclaredNamespace) { - /// - /// The document in which the refactoring is triggered. - /// - public Document Document { get; } - - /// - /// The applicable container node based on cursor location, - /// which will be used to change namespace. - /// - public SyntaxNode Container { get; } - - /// - /// This is the new name we want to change the namespace to. - /// Empty string means global namespace, whereas null means change namespace action is not available. - /// - public string? TargetNamespace { get; } - - /// - /// This is the part of the declared namespace that is contained in default namespace. - /// We will use this to construct target folder to move the file to. - /// For example, if default namespace is `A` and declared namespace is `A.B.C`, - /// this would be `B.C`. - /// - public string? RelativeDeclaredNamespace { get; } - - private State( - Document document, - SyntaxNode container, - string? targetNamespace, - string? relativeDeclaredNamespace) + Document = document; + Container = container; + TargetNamespace = targetNamespace; + RelativeDeclaredNamespace = relativeDeclaredNamespace; + } + + public static async Task CreateAsync( + AbstractSyncNamespaceCodeRefactoringProvider provider, + Document document, + TextSpan textSpan, + CancellationToken cancellationToken) + { + // User must put cursor on one of the nodes described below to trigger the refactoring. + // For each scenario, all requirements must be met. Some of them are checked by `TryGetApplicableInvocationNodeAsync`, + // rest by `IChangeNamespaceService.CanChangeNamespaceAsync`. + // + // - A namespace declaration node that is the only namespace declaration in the document and all types are declared in it: + // 1. No nested namespace declarations (even it's empty). + // 2. The cursor is on the name of the namespace declaration. + // 3. The name of the namespace is valid (i.e. no errors). + // 4. No partial type declared in the namespace. Otherwise its multiple declaration will + // end up in different namespace. + // + // - A compilation unit node that contains no namespace declaration: + // 1. The cursor is on the name of first declared type. + // 2. No partial type declared in the document. Otherwise its multiple declaration will + // end up in different namespace. + + var applicableNode = await provider.TryGetApplicableInvocationNodeAsync(document, textSpan, cancellationToken).ConfigureAwait(false); + if (applicableNode == null) { - Document = document; - Container = container; - TargetNamespace = targetNamespace; - RelativeDeclaredNamespace = relativeDeclaredNamespace; + return null; } - public static async Task CreateAsync( - AbstractSyncNamespaceCodeRefactoringProvider provider, - Document document, - TextSpan textSpan, - CancellationToken cancellationToken) + var changeNamespaceService = document.GetRequiredLanguageService(); + var canChange = await changeNamespaceService.CanChangeNamespaceAsync(document, applicableNode, cancellationToken).ConfigureAwait(false); + + if (!canChange || !IsDocumentPathRootedInProjectFolder(document)) { - // User must put cursor on one of the nodes described below to trigger the refactoring. - // For each scenario, all requirements must be met. Some of them are checked by `TryGetApplicableInvocationNodeAsync`, - // rest by `IChangeNamespaceService.CanChangeNamespaceAsync`. - // - // - A namespace declaration node that is the only namespace declaration in the document and all types are declared in it: - // 1. No nested namespace declarations (even it's empty). - // 2. The cursor is on the name of the namespace declaration. - // 3. The name of the namespace is valid (i.e. no errors). - // 4. No partial type declared in the namespace. Otherwise its multiple declaration will - // end up in different namespace. - // - // - A compilation unit node that contains no namespace declaration: - // 1. The cursor is on the name of first declared type. - // 2. No partial type declared in the document. Otherwise its multiple declaration will - // end up in different namespace. - - var applicableNode = await provider.TryGetApplicableInvocationNodeAsync(document, textSpan, cancellationToken).ConfigureAwait(false); - if (applicableNode == null) - { - return null; - } - - var changeNamespaceService = document.GetRequiredLanguageService(); - var canChange = await changeNamespaceService.CanChangeNamespaceAsync(document, applicableNode, cancellationToken).ConfigureAwait(false); - - if (!canChange || !IsDocumentPathRootedInProjectFolder(document)) - { - return null; - } - - var syntaxFacts = document.GetRequiredLanguageService(); - - // We can't determine what the expected namespace would be without knowing the default namespace. - var defaultNamespace = GetDefaultNamespace(document, syntaxFacts); - if (defaultNamespace == null) - { - return null; - } - - string declaredNamespace; - if (applicableNode is TCompilationUnitSyntax) - { - declaredNamespace = string.Empty; - } - else if (applicableNode is TNamespaceDeclarationSyntax) - { - var syntaxGenerator = SyntaxGenerator.GetGenerator(document); - declaredNamespace = syntaxGenerator.GetName(applicableNode); - } - else - { - throw ExceptionUtilities.Unreachable(); - } - - // Namespace can't be changed if we can't construct a valid qualified identifier from folder names. - // In this case, we might still be able to provide refactoring to move file to new location. - var targetNamespace = PathMetadataUtilities.TryBuildNamespaceFromFolders(document.Folders, syntaxFacts, defaultNamespace); - - // No action required if namespace already matches folders. - if (syntaxFacts.StringComparer.Equals(targetNamespace, declaredNamespace)) - { - return null; - } - - // Only provide "move file" action if default namespace contains declared namespace. - // For example, if the default namespace is `Microsoft.CodeAnalysis`, and declared - // namespace is `System.Diagnostics`, it's very likely this document is an outlier - // in the project and user probably has some special rule for it. - var relativeNamespace = GetRelativeNamespace(defaultNamespace, declaredNamespace, syntaxFacts); - - return new State(document, applicableNode, targetNamespace, relativeNamespace); + return null; } - /// - /// Determines if the actual file path matches its logical path in project - /// which is constructed as [project_root_path]\Logical\Folders\. The refactoring - /// is triggered only when the two match. The reason of doing this is we don't really know - /// the user's intention of keeping the file path out-of-sync with its logical path. - /// - private static bool IsDocumentPathRootedInProjectFolder(Document document) + var syntaxFacts = document.GetRequiredLanguageService(); + + // We can't determine what the expected namespace would be without knowing the default namespace. + var defaultNamespace = GetDefaultNamespace(document, syntaxFacts); + if (defaultNamespace == null) { - var absoluteDirectoryPath = PathUtilities.GetDirectoryName(document.FilePath); - if (absoluteDirectoryPath is null) - return false; + return null; + } - var projectRoot = PathUtilities.GetDirectoryName(document.Project.FilePath); - if (projectRoot is null) - return false; + string declaredNamespace; + if (applicableNode is TCompilationUnitSyntax) + { + declaredNamespace = string.Empty; + } + else if (applicableNode is TNamespaceDeclarationSyntax) + { + var syntaxGenerator = SyntaxGenerator.GetGenerator(document); + declaredNamespace = syntaxGenerator.GetName(applicableNode); + } + else + { + throw ExceptionUtilities.Unreachable(); + } - var folderPath = Path.Combine(document.Folders.ToArray()); - var logicalDirectoryPath = PathUtilities.CombineAbsoluteAndRelativePaths(projectRoot, folderPath); - if (logicalDirectoryPath is null) - return false; + // Namespace can't be changed if we can't construct a valid qualified identifier from folder names. + // In this case, we might still be able to provide refactoring to move file to new location. + var targetNamespace = PathMetadataUtilities.TryBuildNamespaceFromFolders(document.Folders, syntaxFacts, defaultNamespace); - return PathUtilities.PathsEqual(absoluteDirectoryPath, logicalDirectoryPath); + // No action required if namespace already matches folders. + if (syntaxFacts.StringComparer.Equals(targetNamespace, declaredNamespace)) + { + return null; } - private static string? GetDefaultNamespace(Document document, ISyntaxFactsService syntaxFacts) + // Only provide "move file" action if default namespace contains declared namespace. + // For example, if the default namespace is `Microsoft.CodeAnalysis`, and declared + // namespace is `System.Diagnostics`, it's very likely this document is an outlier + // in the project and user probably has some special rule for it. + var relativeNamespace = GetRelativeNamespace(defaultNamespace, declaredNamespace, syntaxFacts); + + return new State(document, applicableNode, targetNamespace, relativeNamespace); + } + + /// + /// Determines if the actual file path matches its logical path in project + /// which is constructed as [project_root_path]\Logical\Folders\. The refactoring + /// is triggered only when the two match. The reason of doing this is we don't really know + /// the user's intention of keeping the file path out-of-sync with its logical path. + /// + private static bool IsDocumentPathRootedInProjectFolder(Document document) + { + var absoluteDirectoryPath = PathUtilities.GetDirectoryName(document.FilePath); + if (absoluteDirectoryPath is null) + return false; + + var projectRoot = PathUtilities.GetDirectoryName(document.Project.FilePath); + if (projectRoot is null) + return false; + + var folderPath = Path.Combine(document.Folders.ToArray()); + var logicalDirectoryPath = PathUtilities.CombineAbsoluteAndRelativePaths(projectRoot, folderPath); + if (logicalDirectoryPath is null) + return false; + + return PathUtilities.PathsEqual(absoluteDirectoryPath, logicalDirectoryPath); + } + + private static string? GetDefaultNamespace(Document document, ISyntaxFactsService syntaxFacts) + { + var solution = document.Project.Solution; + var linkedIds = document.GetLinkedDocumentIds(); + var documents = linkedIds.SelectAsArray(solution.GetRequiredDocument).Add(document); + + // For all projects containing all the linked documents, bail if + // 1. Any of them doesn't have default namespace, or + // 2. Multiple default namespace are found. (this might be possible by tweaking project file). + // The refactoring depends on a single default namespace to operate. + var defaultNamespaceFromProjects = new HashSet( + documents.Select(d => d.Project.DefaultNamespace), + syntaxFacts.StringComparer); + + if (defaultNamespaceFromProjects.Count > 1) + return null; + + return defaultNamespaceFromProjects.SingleOrDefault(); + } + + /// + /// Try get the relative namespace for based on , + /// if is the containing namespace of . Otherwise, + /// Returns null. + /// For example: + /// - If is "A.B" and is "A.B.C.D", then + /// the relative namespace is "C.D". + /// - If is "A.B" and is also "A.B", then + /// the relative namespace is "". + /// - If is "" then the relative namespace us . + /// + private static string? GetRelativeNamespace(string relativeTo, string @namespace, ISyntaxFactsService syntaxFacts) + { + Debug.Assert(relativeTo != null && @namespace != null); + + if (syntaxFacts.StringComparer.Equals(@namespace, relativeTo)) { - var solution = document.Project.Solution; - var linkedIds = document.GetLinkedDocumentIds(); - var documents = linkedIds.SelectAsArray(solution.GetRequiredDocument).Add(document); - - // For all projects containing all the linked documents, bail if - // 1. Any of them doesn't have default namespace, or - // 2. Multiple default namespace are found. (this might be possible by tweaking project file). - // The refactoring depends on a single default namespace to operate. - var defaultNamespaceFromProjects = new HashSet( - documents.Select(d => d.Project.DefaultNamespace), - syntaxFacts.StringComparer); - - if (defaultNamespaceFromProjects.Count > 1) - return null; - - return defaultNamespaceFromProjects.SingleOrDefault(); + return string.Empty; } - - /// - /// Try get the relative namespace for based on , - /// if is the containing namespace of . Otherwise, - /// Returns null. - /// For example: - /// - If is "A.B" and is "A.B.C.D", then - /// the relative namespace is "C.D". - /// - If is "A.B" and is also "A.B", then - /// the relative namespace is "". - /// - If is "" then the relative namespace us . - /// - private static string? GetRelativeNamespace(string relativeTo, string @namespace, ISyntaxFactsService syntaxFacts) + else if (relativeTo.Length == 0) { - Debug.Assert(relativeTo != null && @namespace != null); - - if (syntaxFacts.StringComparer.Equals(@namespace, relativeTo)) - { - return string.Empty; - } - else if (relativeTo.Length == 0) - { - return @namespace; - } - else if (relativeTo.Length >= @namespace.Length) - { - return null; - } - - var containingText = relativeTo + "."; - var namespacePrefix = @namespace[..containingText.Length]; - - return syntaxFacts.StringComparer.Equals(containingText, namespacePrefix) - ? @namespace[(relativeTo.Length + 1)..] - : null; + return @namespace; } + else if (relativeTo.Length >= @namespace.Length) + { + return null; + } + + var containingText = relativeTo + "."; + var namespacePrefix = @namespace[..containingText.Length]; + + return syntaxFacts.StringComparer.Equals(containingText, namespacePrefix) + ? @namespace[(relativeTo.Length + 1)..] + : null; } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.cs index f0dc9eac7ccf2..f5588ba0fdd09 100644 --- a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.cs @@ -12,89 +12,88 @@ using Microsoft.CodeAnalysis.Text; using static Microsoft.CodeAnalysis.CodeActions.CodeAction; -namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace +namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace; + +internal abstract partial class AbstractSyncNamespaceCodeRefactoringProvider + : CodeRefactoringProvider + where TNamespaceDeclarationSyntax : SyntaxNode + where TCompilationUnitSyntax : SyntaxNode + where TMemberDeclarationSyntax : SyntaxNode { - internal abstract partial class AbstractSyncNamespaceCodeRefactoringProvider - : CodeRefactoringProvider - where TNamespaceDeclarationSyntax : SyntaxNode - where TCompilationUnitSyntax : SyntaxNode - where TMemberDeclarationSyntax : SyntaxNode + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + var (document, textSpan, cancellationToken) = context; + if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles || + document.IsGeneratedCode(cancellationToken)) { - var (document, textSpan, cancellationToken) = context; - if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles || - document.IsGeneratedCode(cancellationToken)) - { - return; - } + return; + } - var state = await State.CreateAsync(this, document, textSpan, cancellationToken).ConfigureAwait(false); - if (state == null) - { - return; - } + var state = await State.CreateAsync(this, document, textSpan, cancellationToken).ConfigureAwait(false); + if (state == null) + { + return; + } - // No move file action if rootnamespace isn't a prefix of current declared namespace - if (state.RelativeDeclaredNamespace != null) - { - // These code actions try to move file to a new location based on declared namespace - // and the default namespace of the project. The new location is a list of folders - // determined by the relative part of the declared namespace compare to the default namespace. - // - // For example, if he default namespace is `A.B.C`, file path is - // "[project root dir]\Class1.cs" and declared namespace in the file is - // `A.B.C.D.E`, then this action will move the file to [project root dir]\D\E\Class1.cs". . - // - // We also try to use existing folders as target if possible, using the same example above, - // if folder "[project root dir]\D.E\" already exist, we will also offer to move file to - // "[project root dir]\D.E\Class1.cs". - context.RegisterRefactorings(MoveFileCodeAction.Create(state)); - } + // No move file action if rootnamespace isn't a prefix of current declared namespace + if (state.RelativeDeclaredNamespace != null) + { + // These code actions try to move file to a new location based on declared namespace + // and the default namespace of the project. The new location is a list of folders + // determined by the relative part of the declared namespace compare to the default namespace. + // + // For example, if he default namespace is `A.B.C`, file path is + // "[project root dir]\Class1.cs" and declared namespace in the file is + // `A.B.C.D.E`, then this action will move the file to [project root dir]\D\E\Class1.cs". . + // + // We also try to use existing folders as target if possible, using the same example above, + // if folder "[project root dir]\D.E\" already exist, we will also offer to move file to + // "[project root dir]\D.E\Class1.cs". + context.RegisterRefactorings(MoveFileCodeAction.Create(state)); + } - // No change namespace action if we can't construct a valid namespace from rootnamespace and folder names. - if (state.TargetNamespace != null) - { - // This code action tries to change the name of the namespace declaration to - // match the folder hierarchy of the document. The new namespace is constructed - // by concatenating the default namespace of the project and all the folders in - // the file path up to the project root. - // - // For example, if he default namespace is `A.B.C`, file path is - // "[project root dir]\D\E\F\Class1.cs" and declared namespace in the file is - // `Foo.Bar.Baz`, then this action will change the namespace declaration - // to `A.B.C.D.E.F`. - // - // Note that it also handles the case where the target namespace or declared namespace - // is global namespace, i.e. default namespace is "" and the file is located at project - // root directory, and no namespace declaration in the document, respectively. + // No change namespace action if we can't construct a valid namespace from rootnamespace and folder names. + if (state.TargetNamespace != null) + { + // This code action tries to change the name of the namespace declaration to + // match the folder hierarchy of the document. The new namespace is constructed + // by concatenating the default namespace of the project and all the folders in + // the file path up to the project root. + // + // For example, if he default namespace is `A.B.C`, file path is + // "[project root dir]\D\E\F\Class1.cs" and declared namespace in the file is + // `Foo.Bar.Baz`, then this action will change the namespace declaration + // to `A.B.C.D.E.F`. + // + // Note that it also handles the case where the target namespace or declared namespace + // is global namespace, i.e. default namespace is "" and the file is located at project + // root directory, and no namespace declaration in the document, respectively. - var service = document.GetRequiredLanguageService(); + var service = document.GetRequiredLanguageService(); - var title = state.TargetNamespace.Length == 0 - ? FeaturesResources.Change_to_global_namespace - : string.Format(FeaturesResources.Change_namespace_to_0, state.TargetNamespace); - var solutionChangeAction = CodeAction.Create( - title, - token => service.ChangeNamespaceAsync(document, state.Container, state.TargetNamespace, context.Options, token), - title); + var title = state.TargetNamespace.Length == 0 + ? FeaturesResources.Change_to_global_namespace + : string.Format(FeaturesResources.Change_namespace_to_0, state.TargetNamespace); + var solutionChangeAction = CodeAction.Create( + title, + token => service.ChangeNamespaceAsync(document, state.Container, state.TargetNamespace, context.Options, token), + title); - context.RegisterRefactoring(solutionChangeAction, textSpan); - } + context.RegisterRefactoring(solutionChangeAction, textSpan); } + } - /// - /// Try to get the node that can be used to trigger the refactoring based on current cursor position. - /// - /// - /// (1) a node of type node, if cursor in the name and it's the - /// only namespace declaration in the document. - /// (2) a node of type node, if the cursor is in the name of first - /// declaration in global namespace and there's no namespace declaration in this document. - /// (3) otherwise, null. - /// - protected abstract Task TryGetApplicableInvocationNodeAsync(Document document, TextSpan span, CancellationToken cancellationToken); + /// + /// Try to get the node that can be used to trigger the refactoring based on current cursor position. + /// + /// + /// (1) a node of type node, if cursor in the name and it's the + /// only namespace declaration in the document. + /// (2) a node of type node, if the cursor is in the name of first + /// declaration in global namespace and there's no namespace declaration in this document. + /// (3) otherwise, null. + /// + protected abstract Task TryGetApplicableInvocationNodeAsync(Document document, TextSpan span, CancellationToken cancellationToken); - protected abstract string EscapeIdentifier(string identifier); - } + protected abstract string EscapeIdentifier(string identifier); } diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/SyncNamespaceDocumentsNotInSolutionException.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/SyncNamespaceDocumentsNotInSolutionException.cs index 37b80c4281a6e..0179fe923c91e 100644 --- a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/SyncNamespaceDocumentsNotInSolutionException.cs +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/SyncNamespaceDocumentsNotInSolutionException.cs @@ -6,21 +6,20 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace +namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace; + +internal class SyncNamespaceDocumentsNotInSolutionException(ImmutableArray documentIds) : Exception { - internal class SyncNamespaceDocumentsNotInSolutionException(ImmutableArray documentIds) : Exception - { - private readonly ImmutableArray _documentIds = documentIds; + private readonly ImmutableArray _documentIds = documentIds; - public override string ToString() + public override string ToString() + { + using var _ = PooledStringBuilder.GetInstance(out var builder); + foreach (var documentId in _documentIds) { - using var _ = PooledStringBuilder.GetInstance(out var builder); - foreach (var documentId in _documentIds) - { - builder.AppendLine($"{documentId.GetDebuggerDisplay()}, IsSourceGeneratedDocument: {documentId.IsSourceGenerated}"); - } - - return builder.ToString(); + builder.AppendLine($"{documentId.GetDebuggerDisplay()}, IsSourceGeneratedDocument: {documentId.IsSourceGenerated}"); } + + return builder.ToString(); } } diff --git a/src/Features/Core/Portable/CodeRefactorings/WorkspaceServices/IAddMetadataReferenceCodeActionOperationFactoryWorkspaceService.cs b/src/Features/Core/Portable/CodeRefactorings/WorkspaceServices/IAddMetadataReferenceCodeActionOperationFactoryWorkspaceService.cs index 8ad6d9bae0c13..059a65cac955c 100644 --- a/src/Features/Core/Portable/CodeRefactorings/WorkspaceServices/IAddMetadataReferenceCodeActionOperationFactoryWorkspaceService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/WorkspaceServices/IAddMetadataReferenceCodeActionOperationFactoryWorkspaceService.cs @@ -6,10 +6,9 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.CodeActions.WorkspaceServices +namespace Microsoft.CodeAnalysis.CodeActions.WorkspaceServices; + +internal interface IAddMetadataReferenceCodeActionOperationFactoryWorkspaceService : IWorkspaceService { - internal interface IAddMetadataReferenceCodeActionOperationFactoryWorkspaceService : IWorkspaceService - { - CodeActionOperation CreateAddMetadataReferenceOperation(ProjectId projectId, AssemblyIdentity assemblyIdentity); - } + CodeActionOperation CreateAddMetadataReferenceOperation(ProjectId projectId, AssemblyIdentity assemblyIdentity); } diff --git a/src/Features/Core/Portable/CodeRefactorings/WorkspaceServices/ISymbolRenamedCodeActionOperationFactoryWorkspaceService.cs b/src/Features/Core/Portable/CodeRefactorings/WorkspaceServices/ISymbolRenamedCodeActionOperationFactoryWorkspaceService.cs index 4932c9771d20d..48de894b7db0f 100644 --- a/src/Features/Core/Portable/CodeRefactorings/WorkspaceServices/ISymbolRenamedCodeActionOperationFactoryWorkspaceService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/WorkspaceServices/ISymbolRenamedCodeActionOperationFactoryWorkspaceService.cs @@ -6,10 +6,9 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.CodeActions.WorkspaceServices +namespace Microsoft.CodeAnalysis.CodeActions.WorkspaceServices; + +internal interface ISymbolRenamedCodeActionOperationFactoryWorkspaceService : IWorkspaceService { - internal interface ISymbolRenamedCodeActionOperationFactoryWorkspaceService : IWorkspaceService - { - CodeActionOperation CreateSymbolRenamedOperation(ISymbol symbol, string newName, Solution startingSolution, Solution updatedSolution); - } + CodeActionOperation CreateSymbolRenamedOperation(ISymbol symbol, string newName, Solution startingSolution, Solution updatedSolution); } diff --git a/src/Features/Core/Portable/CommentSelection/AbstractCommentSelectionService.cs b/src/Features/Core/Portable/CommentSelection/AbstractCommentSelectionService.cs index 644ae654ab0fd..7aca4b4f90673 100644 --- a/src/Features/Core/Portable/CommentSelection/AbstractCommentSelectionService.cs +++ b/src/Features/Core/Portable/CommentSelection/AbstractCommentSelectionService.cs @@ -11,18 +11,17 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CommentSelection +namespace Microsoft.CodeAnalysis.CommentSelection; + +internal abstract class AbstractCommentSelectionService : ICommentSelectionService { - internal abstract class AbstractCommentSelectionService : ICommentSelectionService - { - public abstract string BlockCommentEndString { get; } - public abstract string BlockCommentStartString { get; } - public abstract string SingleLineCommentString { get; } - public abstract bool SupportsBlockComment { get; } + public abstract string BlockCommentEndString { get; } + public abstract string BlockCommentStartString { get; } + public abstract string SingleLineCommentString { get; } + public abstract bool SupportsBlockComment { get; } - public CommentSelectionInfo GetInfo() - => SupportsBlockComment - ? new(supportsSingleLineComment: true, SupportsBlockComment, SingleLineCommentString, BlockCommentStartString, BlockCommentEndString) - : new(supportsSingleLineComment: true, SupportsBlockComment, SingleLineCommentString, blockCommentStartString: "", blockCommentEndString: ""); - } + public CommentSelectionInfo GetInfo() + => SupportsBlockComment + ? new(supportsSingleLineComment: true, SupportsBlockComment, SingleLineCommentString, BlockCommentStartString, BlockCommentEndString) + : new(supportsSingleLineComment: true, SupportsBlockComment, SingleLineCommentString, blockCommentStartString: "", blockCommentEndString: ""); } diff --git a/src/Features/Core/Portable/CommentSelection/CommentSelectionInfo.cs b/src/Features/Core/Portable/CommentSelection/CommentSelectionInfo.cs index d25b9074a82a0..5d406d161a8c4 100644 --- a/src/Features/Core/Portable/CommentSelection/CommentSelectionInfo.cs +++ b/src/Features/Core/Portable/CommentSelection/CommentSelectionInfo.cs @@ -2,25 +2,24 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.CommentSelection +namespace Microsoft.CodeAnalysis.CommentSelection; + +internal readonly struct CommentSelectionInfo { - internal readonly struct CommentSelectionInfo + public CommentSelectionInfo(bool supportsSingleLineComment, bool supportsBlockComment, string singleLineCommentString, string blockCommentStartString, string blockCommentEndString) : this() { - public CommentSelectionInfo(bool supportsSingleLineComment, bool supportsBlockComment, string singleLineCommentString, string blockCommentStartString, string blockCommentEndString) : this() - { - SupportsSingleLineComment = supportsSingleLineComment; - SupportsBlockComment = supportsBlockComment; - SingleLineCommentString = singleLineCommentString; - BlockCommentStartString = blockCommentStartString; - BlockCommentEndString = blockCommentEndString; - } + SupportsSingleLineComment = supportsSingleLineComment; + SupportsBlockComment = supportsBlockComment; + SingleLineCommentString = singleLineCommentString; + BlockCommentStartString = blockCommentStartString; + BlockCommentEndString = blockCommentEndString; + } - public bool SupportsSingleLineComment { get; } - public bool SupportsBlockComment { get; } + public bool SupportsSingleLineComment { get; } + public bool SupportsBlockComment { get; } - public string SingleLineCommentString { get; } + public string SingleLineCommentString { get; } - public string BlockCommentStartString { get; } - public string BlockCommentEndString { get; } - } + public string BlockCommentStartString { get; } + public string BlockCommentEndString { get; } } diff --git a/src/Features/Core/Portable/CommentSelection/ICommentSelectionService.cs b/src/Features/Core/Portable/CommentSelection/ICommentSelectionService.cs index 8e259cc0b4ff0..e2cb75840e743 100644 --- a/src/Features/Core/Portable/CommentSelection/ICommentSelectionService.cs +++ b/src/Features/Core/Portable/CommentSelection/ICommentSelectionService.cs @@ -9,10 +9,9 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.CommentSelection +namespace Microsoft.CodeAnalysis.CommentSelection; + +internal interface ICommentSelectionService : ILanguageService { - internal interface ICommentSelectionService : ILanguageService - { - CommentSelectionInfo GetInfo(); - } + CommentSelectionInfo GetInfo(); } diff --git a/src/Features/Core/Portable/Common/AbstractProjectExtensionProvider.cs b/src/Features/Core/Portable/Common/AbstractProjectExtensionProvider.cs index 4fdc443a79e94..69d3d9d57adf2 100644 --- a/src/Features/Core/Portable/Common/AbstractProjectExtensionProvider.cs +++ b/src/Features/Core/Portable/Common/AbstractProjectExtensionProvider.cs @@ -11,174 +11,173 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +internal abstract class AbstractProjectExtensionProvider + where TProvider : AbstractProjectExtensionProvider, new() + where TExportAttribute : Attribute + where TExtension : class { - internal abstract class AbstractProjectExtensionProvider - where TProvider : AbstractProjectExtensionProvider, new() - where TExportAttribute : Attribute - where TExtension : class + public record class ExtensionInfo(string[] DocumentKinds, string[]? DocumentExtensions); + + // Following CWTs are used to cache completion providers from projects' references, + // so we can avoid the slow path unless there's any change to the references. + private static readonly ConditionalWeakTable, StrongBox>> s_referencesToExtensionsMap = new(); + private static readonly ConditionalWeakTable s_referenceToProviderMap = new(); + private static readonly ConditionalWeakTable s_extensionInfoMap = new(); + + private AnalyzerReference Reference { get; init; } = null!; + private ImmutableDictionary> _extensionsPerLanguage = ImmutableDictionary>.Empty; + + protected abstract ImmutableArray GetLanguages(TExportAttribute exportAttribute); + protected abstract bool TryGetExtensionsFromReference(AnalyzerReference reference, out ImmutableArray extensions); + + public static bool TryGetCachedExtensions(IReadOnlyList analyzerReferences, out ImmutableArray extensions) + { + if (s_referencesToExtensionsMap.TryGetValue(analyzerReferences, out var providers)) + { + extensions = providers.Value; + return true; + } + + extensions = []; + return false; + } + + public static ImmutableArray GetExtensions(Project? project) { - public record class ExtensionInfo(string[] DocumentKinds, string[]? DocumentExtensions); + if (project is null) + return []; - // Following CWTs are used to cache completion providers from projects' references, - // so we can avoid the slow path unless there's any change to the references. - private static readonly ConditionalWeakTable, StrongBox>> s_referencesToExtensionsMap = new(); - private static readonly ConditionalWeakTable s_referenceToProviderMap = new(); - private static readonly ConditionalWeakTable s_extensionInfoMap = new(); + return GetExtensions(project.Language, project.AnalyzerReferences); + } + + public static ImmutableArray GetExtensions(string language, IReadOnlyList analyzerReferences) + { + if (TryGetCachedExtensions(analyzerReferences, out var providers)) + return providers; - private AnalyzerReference Reference { get; init; } = null!; - private ImmutableDictionary> _extensionsPerLanguage = ImmutableDictionary>.Empty; + return GetExtensionsSlow(language, analyzerReferences); - protected abstract ImmutableArray GetLanguages(TExportAttribute exportAttribute); - protected abstract bool TryGetExtensionsFromReference(AnalyzerReference reference, out ImmutableArray extensions); + static ImmutableArray GetExtensionsSlow(string language, IReadOnlyList analyzerReferences) + => s_referencesToExtensionsMap.GetValue(analyzerReferences, _ => new(ComputeExtensions(language, analyzerReferences))).Value; - public static bool TryGetCachedExtensions(IReadOnlyList analyzerReferences, out ImmutableArray extensions) + static ImmutableArray ComputeExtensions(string language, IReadOnlyList analyzerReferences) { - if (s_referencesToExtensionsMap.TryGetValue(analyzerReferences, out var providers)) + using var _ = ArrayBuilder.GetInstance(out var builder); + foreach (var reference in analyzerReferences) { - extensions = providers.Value; - return true; + var provider = s_referenceToProviderMap.GetValue( + reference, static reference => new TProvider() { Reference = reference }); + foreach (var extension in provider.GetExtensions(language)) + builder.Add(extension); } - extensions = []; - return false; + return builder.ToImmutable(); } + } - public static ImmutableArray GetExtensions(Project? project) - { - if (project is null) - return []; + public static ImmutableArray GetExtensions(TextDocument document, Func? getExtensionInfoForFiltering) + { + var extensions = GetExtensions(document.Project); + return getExtensionInfoForFiltering != null + ? FilterExtensions(document, extensions, getExtensionInfoForFiltering) + : extensions; + } - return GetExtensions(project.Language, project.AnalyzerReferences); - } + public static ImmutableArray FilterExtensions(TextDocument document, ImmutableArray extensions, Func getExtensionInfoForFiltering) + { + return extensions.WhereAsArray(ShouldIncludeExtension); - public static ImmutableArray GetExtensions(string language, IReadOnlyList analyzerReferences) + bool ShouldIncludeExtension(TExtension extension) { - if (TryGetCachedExtensions(analyzerReferences, out var providers)) - return providers; + if (!s_extensionInfoMap.TryGetValue(extension, out var extensionInfo)) + { + extensionInfo = s_extensionInfoMap.GetValue(extension, + new ConditionalWeakTable.CreateValueCallback(ComputeExtensionInfo)); + } - return GetExtensionsSlow(language, analyzerReferences); + if (extensionInfo == null) + return true; - static ImmutableArray GetExtensionsSlow(string language, IReadOnlyList analyzerReferences) - => s_referencesToExtensionsMap.GetValue(analyzerReferences, _ => new(ComputeExtensions(language, analyzerReferences))).Value; + if (!extensionInfo.DocumentKinds.Contains(document.Kind.ToString())) + return false; - static ImmutableArray ComputeExtensions(string language, IReadOnlyList analyzerReferences) + if (document.FilePath != null && + extensionInfo.DocumentExtensions != null && + !extensionInfo.DocumentExtensions.Contains(PathUtilities.GetExtension(document.FilePath))) { - using var _ = ArrayBuilder.GetInstance(out var builder); - foreach (var reference in analyzerReferences) - { - var provider = s_referenceToProviderMap.GetValue( - reference, static reference => new TProvider() { Reference = reference }); - foreach (var extension in provider.GetExtensions(language)) - builder.Add(extension); - } - - return builder.ToImmutable(); + return false; } - } - public static ImmutableArray GetExtensions(TextDocument document, Func? getExtensionInfoForFiltering) - { - var extensions = GetExtensions(document.Project); - return getExtensionInfoForFiltering != null - ? FilterExtensions(document, extensions, getExtensionInfoForFiltering) - : extensions; + return true; } - public static ImmutableArray FilterExtensions(TextDocument document, ImmutableArray extensions, Func getExtensionInfoForFiltering) + ExtensionInfo? ComputeExtensionInfo(TExtension extension) { - return extensions.WhereAsArray(ShouldIncludeExtension); - - bool ShouldIncludeExtension(TExtension extension) + TExportAttribute? attribute; + try { - if (!s_extensionInfoMap.TryGetValue(extension, out var extensionInfo)) - { - extensionInfo = s_extensionInfoMap.GetValue(extension, - new ConditionalWeakTable.CreateValueCallback(ComputeExtensionInfo)); - } - - if (extensionInfo == null) - return true; - - if (!extensionInfo.DocumentKinds.Contains(document.Kind.ToString())) - return false; - - if (document.FilePath != null && - extensionInfo.DocumentExtensions != null && - !extensionInfo.DocumentExtensions.Contains(PathUtilities.GetExtension(document.FilePath))) - { - return false; - } - - return true; + var typeInfo = extension.GetType().GetTypeInfo(); + attribute = typeInfo.GetCustomAttribute(); } - - ExtensionInfo? ComputeExtensionInfo(TExtension extension) + catch { - TExportAttribute? attribute; - try - { - var typeInfo = extension.GetType().GetTypeInfo(); - attribute = typeInfo.GetCustomAttribute(); - } - catch - { - attribute = null; - } - - if (attribute == null) - return null; - return getExtensionInfoForFiltering(attribute); + attribute = null; } + + if (attribute == null) + return null; + return getExtensionInfoForFiltering(attribute); } + } - private ImmutableArray GetExtensions(string language) - => ImmutableInterlocked.GetOrAdd(ref _extensionsPerLanguage, language, (language, provider) => provider.CreateExtensions(language), this); + private ImmutableArray GetExtensions(string language) + => ImmutableInterlocked.GetOrAdd(ref _extensionsPerLanguage, language, (language, provider) => provider.CreateExtensions(language), this); - private ImmutableArray CreateExtensions(string language) - { - // check whether the analyzer reference knows how to return extensions directly. - if (TryGetExtensionsFromReference(this.Reference, out var extensions)) - return extensions; + private ImmutableArray CreateExtensions(string language) + { + // check whether the analyzer reference knows how to return extensions directly. + if (TryGetExtensionsFromReference(this.Reference, out var extensions)) + return extensions; - // otherwise, see whether we can pick it up from reference itself - if (this.Reference is not AnalyzerFileReference analyzerFileReference) - return []; + // otherwise, see whether we can pick it up from reference itself + if (this.Reference is not AnalyzerFileReference analyzerFileReference) + return []; - using var _ = ArrayBuilder.GetInstance(out var builder); + using var _ = ArrayBuilder.GetInstance(out var builder); - try - { - var analyzerAssembly = analyzerFileReference.GetAssembly(); - var typeInfos = analyzerAssembly.DefinedTypes; + try + { + var analyzerAssembly = analyzerFileReference.GetAssembly(); + var typeInfos = analyzerAssembly.DefinedTypes; - foreach (var typeInfo in typeInfos) + foreach (var typeInfo in typeInfos) + { + if (typeof(TExtension).IsAssignableFrom(typeInfo)) { - if (typeof(TExtension).IsAssignableFrom(typeInfo)) + try { - try - { - var attribute = typeInfo.GetCustomAttribute(); - if (attribute is not null) - { - var languages = GetLanguages(attribute); - if (languages.Contains(language)) - builder.AddIfNotNull((TExtension?)Activator.CreateInstance(typeInfo.AsType())); - } - } - catch + var attribute = typeInfo.GetCustomAttribute(); + if (attribute is not null) { + var languages = GetLanguages(attribute); + if (languages.Contains(language)) + builder.AddIfNotNull((TExtension?)Activator.CreateInstance(typeInfo.AsType())); } } + catch + { + } } } - catch - { - // REVIEW: is the below message right? - // NOTE: We could report "unable to load analyzer" exception here but it should have been already reported by DiagnosticService. - } - - return builder.ToImmutable(); } + catch + { + // REVIEW: is the below message right? + // NOTE: We could report "unable to load analyzer" exception here but it should have been already reported by DiagnosticService. + } + + return builder.ToImmutable(); } } diff --git a/src/Features/Core/Portable/Common/DelayTimeSpan.cs b/src/Features/Core/Portable/Common/DelayTimeSpan.cs index 6a255f7fab847..2597b8536666a 100644 --- a/src/Features/Core/Portable/Common/DelayTimeSpan.cs +++ b/src/Features/Core/Portable/Common/DelayTimeSpan.cs @@ -4,33 +4,32 @@ using System; -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +internal static class DelayTimeSpan { - internal static class DelayTimeSpan - { - /// - /// 50 milliseconds. - /// - public static readonly TimeSpan NearImmediate = TimeSpan.FromMilliseconds(50); + /// + /// 50 milliseconds. + /// + public static readonly TimeSpan NearImmediate = TimeSpan.FromMilliseconds(50); - /// - /// 250 milliseconds. - /// - public static readonly TimeSpan Short = TimeSpan.FromMilliseconds(250); + /// + /// 250 milliseconds. + /// + public static readonly TimeSpan Short = TimeSpan.FromMilliseconds(250); - /// - /// 500 milliseconds. - /// - public static readonly TimeSpan Medium = TimeSpan.FromMilliseconds(500); + /// + /// 500 milliseconds. + /// + public static readonly TimeSpan Medium = TimeSpan.FromMilliseconds(500); - /// - /// 1.5 seconds. - /// - public static readonly TimeSpan Idle = TimeSpan.FromMilliseconds(1500); + /// + /// 1.5 seconds. + /// + public static readonly TimeSpan Idle = TimeSpan.FromMilliseconds(1500); - /// - /// 3 seconds. - /// - public static readonly TimeSpan NonFocus = TimeSpan.FromMilliseconds(3000); - } + /// + /// 3 seconds. + /// + public static readonly TimeSpan NonFocus = TimeSpan.FromMilliseconds(3000); } diff --git a/src/Features/Core/Portable/Common/DocumentNavigationOperation.cs b/src/Features/Core/Portable/Common/DocumentNavigationOperation.cs index 3ddd6ba0d9a75..83a378df597bc 100644 --- a/src/Features/Core/Portable/Common/DocumentNavigationOperation.cs +++ b/src/Features/Core/Portable/Common/DocumentNavigationOperation.cs @@ -5,27 +5,25 @@ using System; using System.Threading; -namespace Microsoft.CodeAnalysis.CodeActions -{ +namespace Microsoft.CodeAnalysis.CodeActions; #pragma warning disable RS0030 // Do not used banned APIs - /// - /// A for navigating to a specific position in a document. - /// When is called an implementation - /// of can return an instance of this operation along with the other - /// operations they want to apply. For example, an implementation could generate a new - /// in one and then have the host editor navigate to that - /// using this operation. - /// +/// +/// A for navigating to a specific position in a document. +/// When is called an implementation +/// of can return an instance of this operation along with the other +/// operations they want to apply. For example, an implementation could generate a new +/// in one and then have the host editor navigate to that +/// using this operation. +/// #pragma warning restore RS0030 // Do not used banned APIs - public class DocumentNavigationOperation(DocumentId documentId, int position = 0) : CodeActionOperation - { - internal DocumentId DocumentId { get; } = documentId ?? throw new ArgumentNullException(nameof(documentId)); - internal int Position { get; } = position; +public class DocumentNavigationOperation(DocumentId documentId, int position = 0) : CodeActionOperation +{ + internal DocumentId DocumentId { get; } = documentId ?? throw new ArgumentNullException(nameof(documentId)); + internal int Position { get; } = position; - public override void Apply(Workspace workspace, CancellationToken cancellationToken) - { - // Intentionally empty. Handling of this operation is special cased in CodeActionEditHandlerService.cs - } + public override void Apply(Workspace workspace, CancellationToken cancellationToken) + { + // Intentionally empty. Handling of this operation is special cased in CodeActionEditHandlerService.cs } } diff --git a/src/Features/Core/Portable/Common/FeaturesSessionTelemetry.cs b/src/Features/Core/Portable/Common/FeaturesSessionTelemetry.cs index d434041ae9c6d..42a89c0a898cc 100644 --- a/src/Features/Core/Portable/Common/FeaturesSessionTelemetry.cs +++ b/src/Features/Core/Portable/Common/FeaturesSessionTelemetry.cs @@ -5,14 +5,13 @@ using Microsoft.CodeAnalysis.ChangeSignature; using Microsoft.CodeAnalysis.Completion.Log; -namespace Microsoft.CodeAnalysis.Common +namespace Microsoft.CodeAnalysis.Common; + +internal static class FeaturesSessionTelemetry { - internal static class FeaturesSessionTelemetry + public static void Report() { - public static void Report() - { - CompletionProvidersLogger.ReportTelemetry(); - ChangeSignatureLogger.ReportTelemetry(); - } + CompletionProvidersLogger.ReportTelemetry(); + ChangeSignatureLogger.ReportTelemetry(); } } diff --git a/src/Features/Core/Portable/Common/Glyph.cs b/src/Features/Core/Portable/Common/Glyph.cs index 75f68785bfe6e..f719b15d405b4 100644 --- a/src/Features/Core/Portable/Common/Glyph.cs +++ b/src/Features/Core/Portable/Common/Glyph.cs @@ -2,116 +2,115 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +internal enum Glyph { - internal enum Glyph - { - None, + None, - Assembly, + Assembly, - BasicFile, - BasicProject, + BasicFile, + BasicProject, - ClassPublic, - ClassProtected, - ClassPrivate, - ClassInternal, + ClassPublic, + ClassProtected, + ClassPrivate, + ClassInternal, - CSharpFile, - CSharpProject, + CSharpFile, + CSharpProject, - ConstantPublic, - ConstantProtected, - ConstantPrivate, - ConstantInternal, + ConstantPublic, + ConstantProtected, + ConstantPrivate, + ConstantInternal, - DelegatePublic, - DelegateProtected, - DelegatePrivate, - DelegateInternal, + DelegatePublic, + DelegateProtected, + DelegatePrivate, + DelegateInternal, - EnumPublic, - EnumProtected, - EnumPrivate, - EnumInternal, + EnumPublic, + EnumProtected, + EnumPrivate, + EnumInternal, - EnumMemberPublic, - EnumMemberProtected, - EnumMemberPrivate, - EnumMemberInternal, + EnumMemberPublic, + EnumMemberProtected, + EnumMemberPrivate, + EnumMemberInternal, - Error, - StatusInformation, + Error, + StatusInformation, - EventPublic, - EventProtected, - EventPrivate, - EventInternal, + EventPublic, + EventProtected, + EventPrivate, + EventInternal, - ExtensionMethodPublic, - ExtensionMethodProtected, - ExtensionMethodPrivate, - ExtensionMethodInternal, + ExtensionMethodPublic, + ExtensionMethodProtected, + ExtensionMethodPrivate, + ExtensionMethodInternal, - FieldPublic, - FieldProtected, - FieldPrivate, - FieldInternal, + FieldPublic, + FieldProtected, + FieldPrivate, + FieldInternal, - InterfacePublic, - InterfaceProtected, - InterfacePrivate, - InterfaceInternal, + InterfacePublic, + InterfaceProtected, + InterfacePrivate, + InterfaceInternal, - Intrinsic, + Intrinsic, - Keyword, + Keyword, - Label, + Label, - Local, + Local, - Namespace, + Namespace, - MethodPublic, - MethodProtected, - MethodPrivate, - MethodInternal, + MethodPublic, + MethodProtected, + MethodPrivate, + MethodInternal, - ModulePublic, - ModuleProtected, - ModulePrivate, - ModuleInternal, + ModulePublic, + ModuleProtected, + ModulePrivate, + ModuleInternal, - OpenFolder, + OpenFolder, - Operator, + Operator, - Parameter, + Parameter, - PropertyPublic, - PropertyProtected, - PropertyPrivate, - PropertyInternal, + PropertyPublic, + PropertyProtected, + PropertyPrivate, + PropertyInternal, - RangeVariable, + RangeVariable, - Reference, + Reference, - StructurePublic, - StructureProtected, - StructurePrivate, - StructureInternal, + StructurePublic, + StructureProtected, + StructurePrivate, + StructureInternal, - TypeParameter, + TypeParameter, - Snippet, + Snippet, - CompletionWarning, + CompletionWarning, - AddReference, - NuGet, - TargetTypeMatch - } + AddReference, + NuGet, + TargetTypeMatch } diff --git a/src/Features/Core/Portable/Common/GlyphExtensions.cs b/src/Features/Core/Portable/Common/GlyphExtensions.cs index c011f551c16b8..0bfa0ad9a9ea3 100644 --- a/src/Features/Core/Portable/Common/GlyphExtensions.cs +++ b/src/Features/Core/Portable/Common/GlyphExtensions.cs @@ -7,225 +7,224 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Tags; -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +internal static class GlyphExtensions { - internal static class GlyphExtensions + public static ImmutableArray GetGlyphs(this ImmutableArray tags) { - public static ImmutableArray GetGlyphs(this ImmutableArray tags) - { - var builder = ImmutableArray.CreateBuilder(initialCapacity: tags.Length); + var builder = ImmutableArray.CreateBuilder(initialCapacity: tags.Length); - foreach (var tag in tags) + foreach (var tag in tags) + { + var glyph = GetGlyph(tag, tags); + if (glyph != Glyph.None) { - var glyph = GetGlyph(tag, tags); - if (glyph != Glyph.None) - { - builder.Add(glyph); - } + builder.Add(glyph); } - - return builder.ToImmutable(); } - public static Glyph GetFirstGlyph(this ImmutableArray tags) - { - var glyphs = GetGlyphs(tags); + return builder.ToImmutable(); + } - return !glyphs.IsEmpty - ? glyphs[0] - : Glyph.None; - } + public static Glyph GetFirstGlyph(this ImmutableArray tags) + { + var glyphs = GetGlyphs(tags); + + return !glyphs.IsEmpty + ? glyphs[0] + : Glyph.None; + } - private static Glyph GetGlyph(string tag, ImmutableArray allTags) + private static Glyph GetGlyph(string tag, ImmutableArray allTags) + { + switch (tag) { - switch (tag) - { - case WellKnownTags.Assembly: - return Glyph.Assembly; - - case WellKnownTags.File: - return allTags.Contains(LanguageNames.VisualBasic) ? Glyph.BasicFile : Glyph.CSharpFile; - - case WellKnownTags.Project: - return allTags.Contains(LanguageNames.VisualBasic) ? Glyph.BasicProject : Glyph.CSharpProject; - - case WellKnownTags.Class: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.ClassProtected, - Accessibility.Private => Glyph.ClassPrivate, - Accessibility.Internal => Glyph.ClassInternal, - _ => Glyph.ClassPublic, - }; - case WellKnownTags.Constant: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.ConstantProtected, - Accessibility.Private => Glyph.ConstantPrivate, - Accessibility.Internal => Glyph.ConstantInternal, - _ => Glyph.ConstantPublic, - }; - case WellKnownTags.Delegate: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.DelegateProtected, - Accessibility.Private => Glyph.DelegatePrivate, - Accessibility.Internal => Glyph.DelegateInternal, - _ => Glyph.DelegatePublic, - }; - case WellKnownTags.Enum: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.EnumProtected, - Accessibility.Private => Glyph.EnumPrivate, - Accessibility.Internal => Glyph.EnumInternal, - _ => Glyph.EnumPublic, - }; - case WellKnownTags.EnumMember: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.EnumMemberProtected, - Accessibility.Private => Glyph.EnumMemberPrivate, - Accessibility.Internal => Glyph.EnumMemberInternal, - _ => Glyph.EnumMemberPublic, - }; - case WellKnownTags.Error: - return Glyph.Error; - - case WellKnownTags.Event: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.EventProtected, - Accessibility.Private => Glyph.EventPrivate, - Accessibility.Internal => Glyph.EventInternal, - _ => Glyph.EventPublic, - }; - case WellKnownTags.ExtensionMethod: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.ExtensionMethodProtected, - Accessibility.Private => Glyph.ExtensionMethodPrivate, - Accessibility.Internal => Glyph.ExtensionMethodInternal, - _ => Glyph.ExtensionMethodPublic, - }; - case WellKnownTags.Field: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.FieldProtected, - Accessibility.Private => Glyph.FieldPrivate, - Accessibility.Internal => Glyph.FieldInternal, - _ => Glyph.FieldPublic, - }; - case WellKnownTags.Interface: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.InterfaceProtected, - Accessibility.Private => Glyph.InterfacePrivate, - Accessibility.Internal => Glyph.InterfaceInternal, - _ => Glyph.InterfacePublic, - }; - case WellKnownTags.TargetTypeMatch: - return Glyph.TargetTypeMatch; - - case WellKnownTags.Intrinsic: - return Glyph.Intrinsic; - - case WellKnownTags.Keyword: - return Glyph.Keyword; - - case WellKnownTags.Label: - return Glyph.Label; - - case WellKnownTags.Local: - return Glyph.Local; - - case WellKnownTags.Namespace: - return Glyph.Namespace; - - case WellKnownTags.Method: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.MethodProtected, - Accessibility.Private => Glyph.MethodPrivate, - Accessibility.Internal => Glyph.MethodInternal, - _ => Glyph.MethodPublic, - }; - case WellKnownTags.Module: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.ModulePublic, - Accessibility.Private => Glyph.ModulePrivate, - Accessibility.Internal => Glyph.ModuleInternal, - _ => Glyph.ModulePublic, - }; - case WellKnownTags.Folder: - return Glyph.OpenFolder; - - case WellKnownTags.Operator: - return Glyph.Operator; - - case WellKnownTags.Parameter: - return Glyph.Parameter; - - case WellKnownTags.Property: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.PropertyProtected, - Accessibility.Private => Glyph.PropertyPrivate, - Accessibility.Internal => Glyph.PropertyInternal, - _ => Glyph.PropertyPublic, - }; - case WellKnownTags.RangeVariable: - return Glyph.RangeVariable; - - case WellKnownTags.Reference: - return Glyph.Reference; - - case WellKnownTags.NuGet: - return Glyph.NuGet; - - case WellKnownTags.Structure: - return (GetAccessibility(allTags)) switch - { - Accessibility.Protected => Glyph.StructureProtected, - Accessibility.Private => Glyph.StructurePrivate, - Accessibility.Internal => Glyph.StructureInternal, - _ => Glyph.StructurePublic, - }; - case WellKnownTags.TypeParameter: - return Glyph.TypeParameter; - - case WellKnownTags.Snippet: - return Glyph.Snippet; - - case WellKnownTags.Warning: - return Glyph.CompletionWarning; - - case WellKnownTags.StatusInformation: - return Glyph.StatusInformation; - } + case WellKnownTags.Assembly: + return Glyph.Assembly; + + case WellKnownTags.File: + return allTags.Contains(LanguageNames.VisualBasic) ? Glyph.BasicFile : Glyph.CSharpFile; + + case WellKnownTags.Project: + return allTags.Contains(LanguageNames.VisualBasic) ? Glyph.BasicProject : Glyph.CSharpProject; - return Glyph.None; + case WellKnownTags.Class: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.ClassProtected, + Accessibility.Private => Glyph.ClassPrivate, + Accessibility.Internal => Glyph.ClassInternal, + _ => Glyph.ClassPublic, + }; + case WellKnownTags.Constant: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.ConstantProtected, + Accessibility.Private => Glyph.ConstantPrivate, + Accessibility.Internal => Glyph.ConstantInternal, + _ => Glyph.ConstantPublic, + }; + case WellKnownTags.Delegate: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.DelegateProtected, + Accessibility.Private => Glyph.DelegatePrivate, + Accessibility.Internal => Glyph.DelegateInternal, + _ => Glyph.DelegatePublic, + }; + case WellKnownTags.Enum: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.EnumProtected, + Accessibility.Private => Glyph.EnumPrivate, + Accessibility.Internal => Glyph.EnumInternal, + _ => Glyph.EnumPublic, + }; + case WellKnownTags.EnumMember: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.EnumMemberProtected, + Accessibility.Private => Glyph.EnumMemberPrivate, + Accessibility.Internal => Glyph.EnumMemberInternal, + _ => Glyph.EnumMemberPublic, + }; + case WellKnownTags.Error: + return Glyph.Error; + + case WellKnownTags.Event: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.EventProtected, + Accessibility.Private => Glyph.EventPrivate, + Accessibility.Internal => Glyph.EventInternal, + _ => Glyph.EventPublic, + }; + case WellKnownTags.ExtensionMethod: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.ExtensionMethodProtected, + Accessibility.Private => Glyph.ExtensionMethodPrivate, + Accessibility.Internal => Glyph.ExtensionMethodInternal, + _ => Glyph.ExtensionMethodPublic, + }; + case WellKnownTags.Field: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.FieldProtected, + Accessibility.Private => Glyph.FieldPrivate, + Accessibility.Internal => Glyph.FieldInternal, + _ => Glyph.FieldPublic, + }; + case WellKnownTags.Interface: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.InterfaceProtected, + Accessibility.Private => Glyph.InterfacePrivate, + Accessibility.Internal => Glyph.InterfaceInternal, + _ => Glyph.InterfacePublic, + }; + case WellKnownTags.TargetTypeMatch: + return Glyph.TargetTypeMatch; + + case WellKnownTags.Intrinsic: + return Glyph.Intrinsic; + + case WellKnownTags.Keyword: + return Glyph.Keyword; + + case WellKnownTags.Label: + return Glyph.Label; + + case WellKnownTags.Local: + return Glyph.Local; + + case WellKnownTags.Namespace: + return Glyph.Namespace; + + case WellKnownTags.Method: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.MethodProtected, + Accessibility.Private => Glyph.MethodPrivate, + Accessibility.Internal => Glyph.MethodInternal, + _ => Glyph.MethodPublic, + }; + case WellKnownTags.Module: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.ModulePublic, + Accessibility.Private => Glyph.ModulePrivate, + Accessibility.Internal => Glyph.ModuleInternal, + _ => Glyph.ModulePublic, + }; + case WellKnownTags.Folder: + return Glyph.OpenFolder; + + case WellKnownTags.Operator: + return Glyph.Operator; + + case WellKnownTags.Parameter: + return Glyph.Parameter; + + case WellKnownTags.Property: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.PropertyProtected, + Accessibility.Private => Glyph.PropertyPrivate, + Accessibility.Internal => Glyph.PropertyInternal, + _ => Glyph.PropertyPublic, + }; + case WellKnownTags.RangeVariable: + return Glyph.RangeVariable; + + case WellKnownTags.Reference: + return Glyph.Reference; + + case WellKnownTags.NuGet: + return Glyph.NuGet; + + case WellKnownTags.Structure: + return (GetAccessibility(allTags)) switch + { + Accessibility.Protected => Glyph.StructureProtected, + Accessibility.Private => Glyph.StructurePrivate, + Accessibility.Internal => Glyph.StructureInternal, + _ => Glyph.StructurePublic, + }; + case WellKnownTags.TypeParameter: + return Glyph.TypeParameter; + + case WellKnownTags.Snippet: + return Glyph.Snippet; + + case WellKnownTags.Warning: + return Glyph.CompletionWarning; + + case WellKnownTags.StatusInformation: + return Glyph.StatusInformation; } - public static Accessibility GetAccessibility(ImmutableArray tags) + return Glyph.None; + } + + public static Accessibility GetAccessibility(ImmutableArray tags) + { + foreach (var tag in tags) { - foreach (var tag in tags) + switch (tag) { - switch (tag) - { - case WellKnownTags.Public: - return Accessibility.Public; - case WellKnownTags.Protected: - return Accessibility.Protected; - case WellKnownTags.Internal: - return Accessibility.Internal; - case WellKnownTags.Private: - return Accessibility.Private; - } + case WellKnownTags.Public: + return Accessibility.Public; + case WellKnownTags.Protected: + return Accessibility.Protected; + case WellKnownTags.Internal: + return Accessibility.Internal; + case WellKnownTags.Private: + return Accessibility.Private; } - - return Accessibility.NotApplicable; } + + return Accessibility.NotApplicable; } } diff --git a/src/Features/Core/Portable/Common/GlyphTags.cs b/src/Features/Core/Portable/Common/GlyphTags.cs index 0ba5e3372ffad..d84cc4d3b6aaf 100644 --- a/src/Features/Core/Portable/Common/GlyphTags.cs +++ b/src/Features/Core/Portable/Common/GlyphTags.cs @@ -7,86 +7,85 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Tags; -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +internal static class GlyphTags { - internal static class GlyphTags - { - public static ImmutableArray GetTags(Glyph glyph) - => glyph switch - { - Glyph.Assembly => WellKnownTagArrays.Assembly, - Glyph.BasicFile => WellKnownTagArrays.VisualBasicFile, - Glyph.BasicProject => WellKnownTagArrays.VisualBasicProject, - Glyph.ClassPublic => WellKnownTagArrays.ClassPublic, - Glyph.ClassProtected => WellKnownTagArrays.ClassProtected, - Glyph.ClassPrivate => WellKnownTagArrays.ClassPrivate, - Glyph.ClassInternal => WellKnownTagArrays.ClassInternal, - Glyph.CSharpFile => WellKnownTagArrays.CSharpFile, - Glyph.CSharpProject => WellKnownTagArrays.CSharpProject, - Glyph.ConstantPublic => WellKnownTagArrays.ConstantPublic, - Glyph.ConstantProtected => WellKnownTagArrays.ConstantProtected, - Glyph.ConstantPrivate => WellKnownTagArrays.ConstantPrivate, - Glyph.ConstantInternal => WellKnownTagArrays.ConstantInternal, - Glyph.DelegatePublic => WellKnownTagArrays.DelegatePublic, - Glyph.DelegateProtected => WellKnownTagArrays.DelegateProtected, - Glyph.DelegatePrivate => WellKnownTagArrays.DelegatePrivate, - Glyph.DelegateInternal => WellKnownTagArrays.DelegateInternal, - Glyph.EnumPublic => WellKnownTagArrays.EnumPublic, - Glyph.EnumProtected => WellKnownTagArrays.EnumProtected, - Glyph.EnumPrivate => WellKnownTagArrays.EnumPrivate, - Glyph.EnumInternal => WellKnownTagArrays.EnumInternal, - Glyph.EnumMemberPublic => WellKnownTagArrays.EnumMemberPublic, - Glyph.EnumMemberProtected => WellKnownTagArrays.EnumMemberProtected, - Glyph.EnumMemberPrivate => WellKnownTagArrays.EnumMemberPrivate, - Glyph.EnumMemberInternal => WellKnownTagArrays.EnumMemberInternal, - Glyph.Error => WellKnownTagArrays.Error, - Glyph.EventPublic => WellKnownTagArrays.EventPublic, - Glyph.EventProtected => WellKnownTagArrays.EventProtected, - Glyph.EventPrivate => WellKnownTagArrays.EventPrivate, - Glyph.EventInternal => WellKnownTagArrays.EventInternal, - Glyph.ExtensionMethodPublic => WellKnownTagArrays.ExtensionMethodPublic, - Glyph.ExtensionMethodProtected => WellKnownTagArrays.ExtensionMethodProtected, - Glyph.ExtensionMethodPrivate => WellKnownTagArrays.ExtensionMethodPrivate, - Glyph.ExtensionMethodInternal => WellKnownTagArrays.ExtensionMethodInternal, - Glyph.FieldPublic => WellKnownTagArrays.FieldPublic, - Glyph.FieldProtected => WellKnownTagArrays.FieldProtected, - Glyph.FieldPrivate => WellKnownTagArrays.FieldPrivate, - Glyph.FieldInternal => WellKnownTagArrays.FieldInternal, - Glyph.InterfacePublic => WellKnownTagArrays.InterfacePublic, - Glyph.InterfaceProtected => WellKnownTagArrays.InterfaceProtected, - Glyph.InterfacePrivate => WellKnownTagArrays.InterfacePrivate, - Glyph.InterfaceInternal => WellKnownTagArrays.InterfaceInternal, - Glyph.Intrinsic => WellKnownTagArrays.Intrinsic, - Glyph.Keyword => WellKnownTagArrays.Keyword, - Glyph.Label => WellKnownTagArrays.Label, - Glyph.Local => WellKnownTagArrays.Local, - Glyph.Namespace => WellKnownTagArrays.Namespace, - Glyph.MethodPublic => WellKnownTagArrays.MethodPublic, - Glyph.MethodProtected => WellKnownTagArrays.MethodProtected, - Glyph.MethodPrivate => WellKnownTagArrays.MethodPrivate, - Glyph.MethodInternal => WellKnownTagArrays.MethodInternal, - Glyph.ModulePublic => WellKnownTagArrays.ModulePublic, - Glyph.ModuleProtected => WellKnownTagArrays.ModuleProtected, - Glyph.ModulePrivate => WellKnownTagArrays.ModulePrivate, - Glyph.ModuleInternal => WellKnownTagArrays.ModuleInternal, - Glyph.OpenFolder => WellKnownTagArrays.Folder, - Glyph.Operator => WellKnownTagArrays.Operator, - Glyph.Parameter => WellKnownTagArrays.Parameter, - Glyph.PropertyPublic => WellKnownTagArrays.PropertyPublic, - Glyph.PropertyProtected => WellKnownTagArrays.PropertyProtected, - Glyph.PropertyPrivate => WellKnownTagArrays.PropertyPrivate, - Glyph.PropertyInternal => WellKnownTagArrays.PropertyInternal, - Glyph.RangeVariable => WellKnownTagArrays.RangeVariable, - Glyph.Reference => WellKnownTagArrays.Reference, - Glyph.StructurePublic => WellKnownTagArrays.StructurePublic, - Glyph.StructureProtected => WellKnownTagArrays.StructureProtected, - Glyph.StructurePrivate => WellKnownTagArrays.StructurePrivate, - Glyph.StructureInternal => WellKnownTagArrays.StructureInternal, - Glyph.TypeParameter => WellKnownTagArrays.TypeParameter, - Glyph.Snippet => WellKnownTagArrays.Snippet, - Glyph.CompletionWarning => WellKnownTagArrays.Warning, - Glyph.StatusInformation => WellKnownTagArrays.StatusInformation, - _ => [], - }; - } + public static ImmutableArray GetTags(Glyph glyph) + => glyph switch + { + Glyph.Assembly => WellKnownTagArrays.Assembly, + Glyph.BasicFile => WellKnownTagArrays.VisualBasicFile, + Glyph.BasicProject => WellKnownTagArrays.VisualBasicProject, + Glyph.ClassPublic => WellKnownTagArrays.ClassPublic, + Glyph.ClassProtected => WellKnownTagArrays.ClassProtected, + Glyph.ClassPrivate => WellKnownTagArrays.ClassPrivate, + Glyph.ClassInternal => WellKnownTagArrays.ClassInternal, + Glyph.CSharpFile => WellKnownTagArrays.CSharpFile, + Glyph.CSharpProject => WellKnownTagArrays.CSharpProject, + Glyph.ConstantPublic => WellKnownTagArrays.ConstantPublic, + Glyph.ConstantProtected => WellKnownTagArrays.ConstantProtected, + Glyph.ConstantPrivate => WellKnownTagArrays.ConstantPrivate, + Glyph.ConstantInternal => WellKnownTagArrays.ConstantInternal, + Glyph.DelegatePublic => WellKnownTagArrays.DelegatePublic, + Glyph.DelegateProtected => WellKnownTagArrays.DelegateProtected, + Glyph.DelegatePrivate => WellKnownTagArrays.DelegatePrivate, + Glyph.DelegateInternal => WellKnownTagArrays.DelegateInternal, + Glyph.EnumPublic => WellKnownTagArrays.EnumPublic, + Glyph.EnumProtected => WellKnownTagArrays.EnumProtected, + Glyph.EnumPrivate => WellKnownTagArrays.EnumPrivate, + Glyph.EnumInternal => WellKnownTagArrays.EnumInternal, + Glyph.EnumMemberPublic => WellKnownTagArrays.EnumMemberPublic, + Glyph.EnumMemberProtected => WellKnownTagArrays.EnumMemberProtected, + Glyph.EnumMemberPrivate => WellKnownTagArrays.EnumMemberPrivate, + Glyph.EnumMemberInternal => WellKnownTagArrays.EnumMemberInternal, + Glyph.Error => WellKnownTagArrays.Error, + Glyph.EventPublic => WellKnownTagArrays.EventPublic, + Glyph.EventProtected => WellKnownTagArrays.EventProtected, + Glyph.EventPrivate => WellKnownTagArrays.EventPrivate, + Glyph.EventInternal => WellKnownTagArrays.EventInternal, + Glyph.ExtensionMethodPublic => WellKnownTagArrays.ExtensionMethodPublic, + Glyph.ExtensionMethodProtected => WellKnownTagArrays.ExtensionMethodProtected, + Glyph.ExtensionMethodPrivate => WellKnownTagArrays.ExtensionMethodPrivate, + Glyph.ExtensionMethodInternal => WellKnownTagArrays.ExtensionMethodInternal, + Glyph.FieldPublic => WellKnownTagArrays.FieldPublic, + Glyph.FieldProtected => WellKnownTagArrays.FieldProtected, + Glyph.FieldPrivate => WellKnownTagArrays.FieldPrivate, + Glyph.FieldInternal => WellKnownTagArrays.FieldInternal, + Glyph.InterfacePublic => WellKnownTagArrays.InterfacePublic, + Glyph.InterfaceProtected => WellKnownTagArrays.InterfaceProtected, + Glyph.InterfacePrivate => WellKnownTagArrays.InterfacePrivate, + Glyph.InterfaceInternal => WellKnownTagArrays.InterfaceInternal, + Glyph.Intrinsic => WellKnownTagArrays.Intrinsic, + Glyph.Keyword => WellKnownTagArrays.Keyword, + Glyph.Label => WellKnownTagArrays.Label, + Glyph.Local => WellKnownTagArrays.Local, + Glyph.Namespace => WellKnownTagArrays.Namespace, + Glyph.MethodPublic => WellKnownTagArrays.MethodPublic, + Glyph.MethodProtected => WellKnownTagArrays.MethodProtected, + Glyph.MethodPrivate => WellKnownTagArrays.MethodPrivate, + Glyph.MethodInternal => WellKnownTagArrays.MethodInternal, + Glyph.ModulePublic => WellKnownTagArrays.ModulePublic, + Glyph.ModuleProtected => WellKnownTagArrays.ModuleProtected, + Glyph.ModulePrivate => WellKnownTagArrays.ModulePrivate, + Glyph.ModuleInternal => WellKnownTagArrays.ModuleInternal, + Glyph.OpenFolder => WellKnownTagArrays.Folder, + Glyph.Operator => WellKnownTagArrays.Operator, + Glyph.Parameter => WellKnownTagArrays.Parameter, + Glyph.PropertyPublic => WellKnownTagArrays.PropertyPublic, + Glyph.PropertyProtected => WellKnownTagArrays.PropertyProtected, + Glyph.PropertyPrivate => WellKnownTagArrays.PropertyPrivate, + Glyph.PropertyInternal => WellKnownTagArrays.PropertyInternal, + Glyph.RangeVariable => WellKnownTagArrays.RangeVariable, + Glyph.Reference => WellKnownTagArrays.Reference, + Glyph.StructurePublic => WellKnownTagArrays.StructurePublic, + Glyph.StructureProtected => WellKnownTagArrays.StructureProtected, + Glyph.StructurePrivate => WellKnownTagArrays.StructurePrivate, + Glyph.StructureInternal => WellKnownTagArrays.StructureInternal, + Glyph.TypeParameter => WellKnownTagArrays.TypeParameter, + Glyph.Snippet => WellKnownTagArrays.Snippet, + Glyph.CompletionWarning => WellKnownTagArrays.Warning, + Glyph.StatusInformation => WellKnownTagArrays.StatusInformation, + _ => [], + }; } diff --git a/src/Features/Core/Portable/Common/StartInlineRenameSessionOperation.cs b/src/Features/Core/Portable/Common/StartInlineRenameSessionOperation.cs index a1cd8c697c50e..8500fbb6b0d48 100644 --- a/src/Features/Core/Portable/Common/StartInlineRenameSessionOperation.cs +++ b/src/Features/Core/Portable/Common/StartInlineRenameSessionOperation.cs @@ -5,26 +5,25 @@ using System; using System.Threading; -namespace Microsoft.CodeAnalysis.CodeActions -{ +namespace Microsoft.CodeAnalysis.CodeActions; + #pragma warning disable RS0030 // Do not used banned APIs - /// - /// A for navigating to a specific position in a document and invoking inline rename. - /// When is called an implementation - /// of can return an instance of this operation along with the other - /// operations they want to apply. For example, an implementation could generate a new - /// in one and then have the host editor navigate to that - /// and invoke rename at a given position using this operation. - /// +/// +/// A for navigating to a specific position in a document and invoking inline rename. +/// When is called an implementation +/// of can return an instance of this operation along with the other +/// operations they want to apply. For example, an implementation could generate a new +/// in one and then have the host editor navigate to that +/// and invoke rename at a given position using this operation. +/// #pragma warning restore RS0030 // Do not used banned APIs - internal sealed class StartInlineRenameSessionOperation(DocumentId documentId, int position) : CodeActionOperation - { - public DocumentId DocumentId { get; } = documentId ?? throw new ArgumentNullException(nameof(documentId)); - public int Position { get; } = position; +internal sealed class StartInlineRenameSessionOperation(DocumentId documentId, int position) : CodeActionOperation +{ + public DocumentId DocumentId { get; } = documentId ?? throw new ArgumentNullException(nameof(documentId)); + public int Position { get; } = position; - public override void Apply(Workspace workspace, CancellationToken cancellationToken) - { - // Intentionally empty. Handling of this operation is special cased in CodeActionEditHandlerService.cs - } + public override void Apply(Workspace workspace, CancellationToken cancellationToken) + { + // Intentionally empty. Handling of this operation is special cased in CodeActionEditHandlerService.cs } } diff --git a/src/Features/Core/Portable/Common/SymbolDisplayPartKindTags.cs b/src/Features/Core/Portable/Common/SymbolDisplayPartKindTags.cs index 58eaf80d1db8d..366119e55369a 100644 --- a/src/Features/Core/Portable/Common/SymbolDisplayPartKindTags.cs +++ b/src/Features/Core/Portable/Common/SymbolDisplayPartKindTags.cs @@ -4,73 +4,72 @@ using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +internal static class SymbolDisplayPartKindTags { - internal static class SymbolDisplayPartKindTags + public static SymbolDisplayPartKind GetSymbolDisplayPartKind(this INamedTypeSymbol namedType) { - public static SymbolDisplayPartKind GetSymbolDisplayPartKind(this INamedTypeSymbol namedType) - { - if (namedType.IsEnumType()) - return SymbolDisplayPartKind.EnumName; - - if (namedType.IsDelegateType()) - return SymbolDisplayPartKind.DelegateName; + if (namedType.IsEnumType()) + return SymbolDisplayPartKind.EnumName; - if (namedType.IsInterfaceType()) - return SymbolDisplayPartKind.InterfaceName; + if (namedType.IsDelegateType()) + return SymbolDisplayPartKind.DelegateName; - if (namedType.IsRecord) - return namedType.IsValueType ? SymbolDisplayPartKind.RecordStructName : SymbolDisplayPartKind.RecordClassName; + if (namedType.IsInterfaceType()) + return SymbolDisplayPartKind.InterfaceName; - if (namedType.IsStructType()) - return SymbolDisplayPartKind.StructName; + if (namedType.IsRecord) + return namedType.IsValueType ? SymbolDisplayPartKind.RecordStructName : SymbolDisplayPartKind.RecordClassName; - if (namedType.IsModuleType()) - return SymbolDisplayPartKind.ModuleName; + if (namedType.IsStructType()) + return SymbolDisplayPartKind.StructName; - if (namedType.IsErrorType()) - return SymbolDisplayPartKind.ErrorTypeName; + if (namedType.IsModuleType()) + return SymbolDisplayPartKind.ModuleName; - return SymbolDisplayPartKind.ClassName; - } + if (namedType.IsErrorType()) + return SymbolDisplayPartKind.ErrorTypeName; - public static string GetTag(SymbolDisplayPartKind kind) - => kind switch - { - SymbolDisplayPartKind.AliasName => TextTags.Alias, - SymbolDisplayPartKind.AssemblyName => TextTags.Assembly, - SymbolDisplayPartKind.ClassName => TextTags.Class, - SymbolDisplayPartKind.DelegateName => TextTags.Delegate, - SymbolDisplayPartKind.EnumName => TextTags.Enum, - SymbolDisplayPartKind.ErrorTypeName => TextTags.ErrorType, - SymbolDisplayPartKind.EventName => TextTags.Event, - SymbolDisplayPartKind.FieldName => TextTags.Field, - SymbolDisplayPartKind.InterfaceName => TextTags.Interface, - SymbolDisplayPartKind.Keyword => TextTags.Keyword, - SymbolDisplayPartKind.LabelName => TextTags.Label, - SymbolDisplayPartKind.LineBreak => TextTags.LineBreak, - SymbolDisplayPartKind.NumericLiteral => TextTags.NumericLiteral, - SymbolDisplayPartKind.StringLiteral => TextTags.StringLiteral, - SymbolDisplayPartKind.LocalName => TextTags.Local, - SymbolDisplayPartKind.MethodName => TextTags.Method, - SymbolDisplayPartKind.ModuleName => TextTags.Module, - SymbolDisplayPartKind.NamespaceName => TextTags.Namespace, - SymbolDisplayPartKind.Operator => TextTags.Operator, - SymbolDisplayPartKind.ParameterName => TextTags.Parameter, - SymbolDisplayPartKind.PropertyName => TextTags.Property, - SymbolDisplayPartKind.Punctuation => TextTags.Punctuation, - SymbolDisplayPartKind.Space => TextTags.Space, - SymbolDisplayPartKind.StructName => TextTags.Struct, - SymbolDisplayPartKind.AnonymousTypeIndicator => TextTags.AnonymousTypeIndicator, - SymbolDisplayPartKind.Text => TextTags.Text, - SymbolDisplayPartKind.TypeParameterName => TextTags.TypeParameter, - SymbolDisplayPartKind.RangeVariableName => TextTags.RangeVariable, - SymbolDisplayPartKind.EnumMemberName => TextTags.EnumMember, - SymbolDisplayPartKind.ExtensionMethodName => TextTags.ExtensionMethod, - SymbolDisplayPartKind.ConstantName => TextTags.Constant, - SymbolDisplayPartKind.RecordClassName => TextTags.Record, - SymbolDisplayPartKind.RecordStructName => TextTags.RecordStruct, - _ => string.Empty, - }; + return SymbolDisplayPartKind.ClassName; } + + public static string GetTag(SymbolDisplayPartKind kind) + => kind switch + { + SymbolDisplayPartKind.AliasName => TextTags.Alias, + SymbolDisplayPartKind.AssemblyName => TextTags.Assembly, + SymbolDisplayPartKind.ClassName => TextTags.Class, + SymbolDisplayPartKind.DelegateName => TextTags.Delegate, + SymbolDisplayPartKind.EnumName => TextTags.Enum, + SymbolDisplayPartKind.ErrorTypeName => TextTags.ErrorType, + SymbolDisplayPartKind.EventName => TextTags.Event, + SymbolDisplayPartKind.FieldName => TextTags.Field, + SymbolDisplayPartKind.InterfaceName => TextTags.Interface, + SymbolDisplayPartKind.Keyword => TextTags.Keyword, + SymbolDisplayPartKind.LabelName => TextTags.Label, + SymbolDisplayPartKind.LineBreak => TextTags.LineBreak, + SymbolDisplayPartKind.NumericLiteral => TextTags.NumericLiteral, + SymbolDisplayPartKind.StringLiteral => TextTags.StringLiteral, + SymbolDisplayPartKind.LocalName => TextTags.Local, + SymbolDisplayPartKind.MethodName => TextTags.Method, + SymbolDisplayPartKind.ModuleName => TextTags.Module, + SymbolDisplayPartKind.NamespaceName => TextTags.Namespace, + SymbolDisplayPartKind.Operator => TextTags.Operator, + SymbolDisplayPartKind.ParameterName => TextTags.Parameter, + SymbolDisplayPartKind.PropertyName => TextTags.Property, + SymbolDisplayPartKind.Punctuation => TextTags.Punctuation, + SymbolDisplayPartKind.Space => TextTags.Space, + SymbolDisplayPartKind.StructName => TextTags.Struct, + SymbolDisplayPartKind.AnonymousTypeIndicator => TextTags.AnonymousTypeIndicator, + SymbolDisplayPartKind.Text => TextTags.Text, + SymbolDisplayPartKind.TypeParameterName => TextTags.TypeParameter, + SymbolDisplayPartKind.RangeVariableName => TextTags.RangeVariable, + SymbolDisplayPartKind.EnumMemberName => TextTags.EnumMember, + SymbolDisplayPartKind.ExtensionMethodName => TextTags.ExtensionMethod, + SymbolDisplayPartKind.ConstantName => TextTags.Constant, + SymbolDisplayPartKind.RecordClassName => TextTags.Record, + SymbolDisplayPartKind.RecordStructName => TextTags.RecordStruct, + _ => string.Empty, + }; } diff --git a/src/Features/Core/Portable/Common/TaggedText.cs b/src/Features/Core/Portable/Common/TaggedText.cs index 7619559bb5d2a..6812018ebadde 100644 --- a/src/Features/Core/Portable/Common/TaggedText.cs +++ b/src/Features/Core/Portable/Common/TaggedText.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -11,367 +9,296 @@ using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +/// +/// A piece of text with a descriptive tag. +/// +[DataContract] +public readonly record struct TaggedText { /// - /// A piece of text with a descriptive tag. + /// A descriptive tag from . /// - [DataContract] - public readonly record struct TaggedText - { - /// - /// A descriptive tag from . - /// - [DataMember(Order = 0)] - public string Tag { get; } - - /// - /// The actual text to be displayed. - /// - [DataMember(Order = 1)] - public string Text { get; } - - /// - /// Gets the style(s) to apply to the text. - /// - [DataMember(Order = 2)] - internal TaggedTextStyle Style { get; } - - /// - /// Gets the navigation target for the text, or if the text does not have a navigation - /// target. - /// - [DataMember(Order = 3)] - internal string NavigationTarget { get; } - - /// - /// Gets the navigation hint for the text, or if the text does not have a navigation - /// hint. - /// - [DataMember(Order = 4)] - internal string NavigationHint { get; } - - /// - /// Creates a new instance of - /// - /// A descriptive tag from . - /// The actual text to be displayed. - public TaggedText(string tag, string text) - : this(tag, text, TaggedTextStyle.None, navigationTarget: null, navigationHint: null) - { - } + [DataMember(Order = 0)] + public string Tag { get; } - /// - /// Creates a new instance of - /// - /// A descriptive tag from . - /// The actual text to be displayed. - /// The style(s) to apply to the text. - /// The navigation target for the text, or if the text does not have a navigation target. - /// The navigation hint for the text, or if the text does not have a navigation hint. - internal TaggedText(string tag, string text, TaggedTextStyle style, string navigationTarget, string navigationHint) - { - Tag = tag ?? throw new ArgumentNullException(nameof(tag)); - Text = text ?? throw new ArgumentNullException(nameof(text)); - Style = style; - NavigationTarget = navigationTarget; - NavigationHint = navigationHint; - } + /// + /// The actual text to be displayed. + /// + [DataMember(Order = 1)] + public string Text { get; } + + /// + /// Gets the style(s) to apply to the text. + /// + [DataMember(Order = 2)] + internal TaggedTextStyle Style { get; } - public override string ToString() - => Text; + /// + /// Gets the navigation target for the text, or if the text does not have a navigation + /// target. + /// + [DataMember(Order = 3)] + internal string? NavigationTarget { get; } + + /// + /// Gets the navigation hint for the text, or if the text does not have a navigation + /// hint. + /// + [DataMember(Order = 4)] + internal string? NavigationHint { get; } + + /// + /// Creates a new instance of + /// + /// A descriptive tag from . + /// The actual text to be displayed. + public TaggedText(string tag, string text) + : this(tag, text, TaggedTextStyle.None, navigationTarget: null, navigationHint: null) + { } - internal static class TaggedTextExtensions + /// + /// Creates a new instance of + /// + /// A descriptive tag from . + /// The actual text to be displayed. + /// The style(s) to apply to the text. + /// The navigation target for the text, or if the text does not have a navigation target. + /// The navigation hint for the text, or if the text does not have a navigation hint. + internal TaggedText(string tag, string text, TaggedTextStyle style, string? navigationTarget, string? navigationHint) { - public static ImmutableArray ToTaggedText(this IEnumerable displayParts, Func getNavigationHint = null, bool includeNavigationHints = true) - => displayParts.ToTaggedText(TaggedTextStyle.None, getNavigationHint, includeNavigationHints); + Tag = tag ?? throw new ArgumentNullException(nameof(tag)); + Text = text ?? throw new ArgumentNullException(nameof(text)); + Style = style; + NavigationTarget = navigationTarget; + NavigationHint = navigationHint; + } - public static ImmutableArray ToTaggedText( - this IEnumerable displayParts, TaggedTextStyle style, Func getNavigationHint = null, bool includeNavigationHints = true) - { - if (displayParts == null) - return []; - - getNavigationHint ??= static symbol => symbol?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); - - return displayParts.SelectAsArray(d => - new TaggedText( - GetTag(d), - d.ToString(), - style, - includeNavigationHints && d.Kind != SymbolDisplayPartKind.NamespaceName ? GetNavigationTarget(d.Symbol) : null, - includeNavigationHints && d.Kind != SymbolDisplayPartKind.NamespaceName ? getNavigationHint(d.Symbol) : null)); - } + public override string ToString() + => Text; +} - private static string GetTag(SymbolDisplayPart part) - { - // We don't actually have any specific classifications for aliases. So if the compiler passed us that kind, - // attempt to map to the corresponding namespace/named-type kind that matches the underlying alias target. - if (part is { Symbol: IAliasSymbol alias, Kind: SymbolDisplayPartKind.AliasName }) - { - if (alias.Target is INamespaceSymbol) - return SymbolDisplayPartKindTags.GetTag(SymbolDisplayPartKind.NamespaceName); - else if (alias.Target is INamedTypeSymbol namedType) - return SymbolDisplayPartKindTags.GetTag(namedType.GetSymbolDisplayPartKind()); - } +internal static class TaggedTextExtensions +{ + public static ImmutableArray ToTaggedText( + this IEnumerable? displayParts, TaggedTextStyle style = TaggedTextStyle.None, Func? getNavigationHint = null, bool includeNavigationHints = true) + { + if (displayParts == null) + return []; + + // To support CodeGeneration symbols, which do not support ToDisplayString we need to be able to override it. + getNavigationHint ??= static symbol => symbol?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + + return displayParts.SelectAsArray(d => + new TaggedText( + GetTag(d), + d.ToString(), + style, + includeNavigationHints && d.Kind != SymbolDisplayPartKind.NamespaceName ? GetNavigationTarget(d.Symbol) : null, + includeNavigationHints && d.Kind != SymbolDisplayPartKind.NamespaceName ? getNavigationHint(d.Symbol) : null)); + } - return SymbolDisplayPartKindTags.GetTag(part.Kind); + private static string GetTag(SymbolDisplayPart part) + { + // We don't actually have any specific classifications for aliases. So if the compiler passed us that kind, + // attempt to map to the corresponding namespace/named-type kind that matches the underlying alias target. + if (part is { Symbol: IAliasSymbol alias, Kind: SymbolDisplayPartKind.AliasName }) + { + if (alias.Target is INamespaceSymbol) + return SymbolDisplayPartKindTags.GetTag(SymbolDisplayPartKind.NamespaceName); + else if (alias.Target is INamedTypeSymbol namedType) + return SymbolDisplayPartKindTags.GetTag(namedType.GetSymbolDisplayPartKind()); } - private static string GetNavigationTarget(ISymbol symbol) - => symbol is null ? null : SymbolKey.CreateString(symbol); + return SymbolDisplayPartKindTags.GetTag(part.Kind); + } - public static string JoinText(this ImmutableArray values) - { + private static string? GetNavigationTarget(ISymbol? symbol) + => symbol is null ? null : SymbolKey.CreateString(symbol); - return values.IsDefault - ? null - : Join(values); + public static string JoinText(this ImmutableArray values) + { + if (values.IsDefaultOrEmpty) + { + return ""; } - private static string Join(ImmutableArray values) + if (values is [var value]) { - var pooled = PooledStringBuilder.GetInstance(); - var builder = pooled.Builder; - foreach (var val in values) - { - builder.Append(val.Text); - } - - return pooled.ToStringAndFree(); + return value.Text; } - public static string ToClassificationTypeName(this string taggedTextTag) + using var _ = PooledStringBuilder.GetInstance(out var builder); + builder.EnsureCapacity(values.Sum(static value => value.Text.Length)); + foreach (var val in values) { - switch (taggedTextTag) - { - case TextTags.Keyword: - return ClassificationTypeNames.Keyword; - - case TextTags.Class: - return ClassificationTypeNames.ClassName; - - case TextTags.Delegate: - return ClassificationTypeNames.DelegateName; - - case TextTags.Enum: - return ClassificationTypeNames.EnumName; - - case TextTags.Interface: - return ClassificationTypeNames.InterfaceName; - - case TextTags.Module: - return ClassificationTypeNames.ModuleName; - - case TextTags.Struct: - return ClassificationTypeNames.StructName; - - case TextTags.TypeParameter: - return ClassificationTypeNames.TypeParameterName; - - case TextTags.Field: - return ClassificationTypeNames.FieldName; - - case TextTags.Event: - return ClassificationTypeNames.EventName; - - case TextTags.Label: - return ClassificationTypeNames.LabelName; - - case TextTags.Local: - return ClassificationTypeNames.LocalName; - - case TextTags.Method: - return ClassificationTypeNames.MethodName; - - case TextTags.Namespace: - return ClassificationTypeNames.NamespaceName; - - case TextTags.Parameter: - return ClassificationTypeNames.ParameterName; - - case TextTags.Property: - return ClassificationTypeNames.PropertyName; - - case TextTags.ExtensionMethod: - return ClassificationTypeNames.ExtensionMethodName; - - case TextTags.EnumMember: - return ClassificationTypeNames.EnumMemberName; - - case TextTags.Constant: - return ClassificationTypeNames.ConstantName; - - case TextTags.Alias: - case TextTags.Assembly: - case TextTags.ErrorType: - case TextTags.RangeVariable: - return ClassificationTypeNames.Identifier; - - case TextTags.NumericLiteral: - return ClassificationTypeNames.NumericLiteral; - - case TextTags.StringLiteral: - return ClassificationTypeNames.StringLiteral; - - case TextTags.Space: - case TextTags.LineBreak: - return ClassificationTypeNames.WhiteSpace; - - case TextTags.Operator: - return ClassificationTypeNames.Operator; - - case TextTags.Punctuation: - return ClassificationTypeNames.Punctuation; - - case TextTags.AnonymousTypeIndicator: - case TextTags.Text: - return ClassificationTypeNames.Text; - - case TextTags.Record: - return ClassificationTypeNames.RecordClassName; - - case TextTags.RecordStruct: - return ClassificationTypeNames.RecordStructName; - - case TextTags.ContainerStart: - case TextTags.ContainerEnd: - case TextTags.CodeBlockStart: - case TextTags.CodeBlockEnd: - // These tags are not visible so classify them as whitespace - return ClassificationTypeNames.WhiteSpace; - - default: - throw ExceptionUtilities.UnexpectedValue(taggedTextTag); - } + builder.Append(val.Text); } - public static IEnumerable ToClassifiedSpans( - this IEnumerable parts) + return builder.ToString(); + } + + public static string ToClassificationTypeName(this string taggedTextTag) + => taggedTextTag switch { - var index = 0; - foreach (var part in parts) - { - var text = part.ToString(); - var classificationTypeName = part.Tag.ToClassificationTypeName(); + TextTags.Keyword => ClassificationTypeNames.Keyword, + TextTags.Class => ClassificationTypeNames.ClassName, + TextTags.Delegate => ClassificationTypeNames.DelegateName, + TextTags.Enum => ClassificationTypeNames.EnumName, + TextTags.Interface => ClassificationTypeNames.InterfaceName, + TextTags.Module => ClassificationTypeNames.ModuleName, + TextTags.Struct => ClassificationTypeNames.StructName, + TextTags.TypeParameter => ClassificationTypeNames.TypeParameterName, + TextTags.Field => ClassificationTypeNames.FieldName, + TextTags.Event => ClassificationTypeNames.EventName, + TextTags.Label => ClassificationTypeNames.LabelName, + TextTags.Local => ClassificationTypeNames.LocalName, + TextTags.Method => ClassificationTypeNames.MethodName, + TextTags.Namespace => ClassificationTypeNames.NamespaceName, + TextTags.Parameter => ClassificationTypeNames.ParameterName, + TextTags.Property => ClassificationTypeNames.PropertyName, + TextTags.ExtensionMethod => ClassificationTypeNames.ExtensionMethodName, + TextTags.EnumMember => ClassificationTypeNames.EnumMemberName, + TextTags.Constant => ClassificationTypeNames.ConstantName, + TextTags.Alias or TextTags.Assembly or TextTags.ErrorType or TextTags.RangeVariable => ClassificationTypeNames.Identifier, + TextTags.NumericLiteral => ClassificationTypeNames.NumericLiteral, + TextTags.StringLiteral => ClassificationTypeNames.StringLiteral, + TextTags.Space or TextTags.LineBreak => ClassificationTypeNames.WhiteSpace, + TextTags.Operator => ClassificationTypeNames.Operator, + TextTags.Punctuation => ClassificationTypeNames.Punctuation, + TextTags.AnonymousTypeIndicator or TextTags.Text => ClassificationTypeNames.Text, + TextTags.Record => ClassificationTypeNames.RecordClassName, + TextTags.RecordStruct => ClassificationTypeNames.RecordStructName, + // These tags are not visible so classify them as whitespace + TextTags.ContainerStart or TextTags.ContainerEnd or TextTags.CodeBlockStart or TextTags.CodeBlockEnd => ClassificationTypeNames.WhiteSpace, + _ => throw ExceptionUtilities.UnexpectedValue(taggedTextTag), + }; + + public static IEnumerable ToClassifiedSpans( + this IEnumerable parts) + { + var index = 0; + foreach (var part in parts) + { + var text = part.ToString(); + var classificationTypeName = part.Tag.ToClassificationTypeName(); - yield return new ClassifiedSpan(new TextSpan(index, text.Length), classificationTypeName); - index += text.Length; - } + yield return new ClassifiedSpan(new TextSpan(index, text.Length), classificationTypeName); + index += text.Length; } + } - private const string LeftToRightMarkerPrefix = "\u200e"; + private const string LeftToRightMarkerPrefix = "\u200e"; - public static string ToVisibleDisplayString(this TaggedText part, bool includeLeftToRightMarker) - { - var text = part.ToString(); + public static string ToVisibleDisplayString(this TaggedText part, bool includeLeftToRightMarker) + { + var text = part.ToString(); - if (includeLeftToRightMarker) + if (includeLeftToRightMarker) + { + var classificationTypeName = part.Tag.ToClassificationTypeName(); + if (classificationTypeName is ClassificationTypeNames.Punctuation or + ClassificationTypeNames.WhiteSpace) { - var classificationTypeName = part.Tag.ToClassificationTypeName(); - if (classificationTypeName is ClassificationTypeNames.Punctuation or - ClassificationTypeNames.WhiteSpace) - { - text = LeftToRightMarkerPrefix + text; - } + text = LeftToRightMarkerPrefix + text; } - - return text; } - public static string ToVisibleDisplayString(this IEnumerable parts, bool includeLeftToRightMarker) - { - return string.Join(string.Empty, parts.Select( - p => p.ToVisibleDisplayString(includeLeftToRightMarker))); - } + return text; + } - public static string GetFullText(this IEnumerable parts) - => string.Join(string.Empty, parts.Select(p => p.ToString())); + public static string ToVisibleDisplayString(this IEnumerable parts, bool includeLeftToRightMarker) + { + return string.Join(string.Empty, parts.Select( + p => p.ToVisibleDisplayString(includeLeftToRightMarker))); + } - public static void AddAliasName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Alias, text)); + public static string GetFullText(this IEnumerable parts) + => string.Join(string.Empty, parts.Select(p => p.ToString())); - public static void AddAssemblyName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Assembly, text)); + public static void AddAliasName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Alias, text)); - public static void AddClassName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Class, text)); + public static void AddAssemblyName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Assembly, text)); - public static void AddDelegateName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Delegate, text)); + public static void AddClassName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Class, text)); - public static void AddEnumName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Enum, text)); + public static void AddDelegateName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Delegate, text)); - public static void AddErrorTypeName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.ErrorType, text)); + public static void AddEnumName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Enum, text)); - public static void AddEventName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Event, text)); + public static void AddErrorTypeName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.ErrorType, text)); - public static void AddFieldName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Field, text)); + public static void AddEventName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Event, text)); - public static void AddInterfaceName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Interface, text)); + public static void AddFieldName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Field, text)); - public static void AddKeyword(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Keyword, text)); + public static void AddInterfaceName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Interface, text)); - public static void AddLabelName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Label, text)); + public static void AddKeyword(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Keyword, text)); - public static void AddLineBreak(this IList parts, string text = "\r\n") - => parts.Add(new TaggedText(TextTags.LineBreak, text)); + public static void AddLabelName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Label, text)); - public static void AddNumericLiteral(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.NumericLiteral, text)); + public static void AddLineBreak(this IList parts, string text = "\r\n") + => parts.Add(new TaggedText(TextTags.LineBreak, text)); - public static void AddStringLiteral(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.StringLiteral, text)); + public static void AddNumericLiteral(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.NumericLiteral, text)); - public static void AddLocalName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Local, text)); + public static void AddStringLiteral(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.StringLiteral, text)); - public static void AddMethodName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Method, text)); + public static void AddLocalName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Local, text)); - public static void AddModuleName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Module, text)); + public static void AddMethodName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Method, text)); - public static void AddNamespaceName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Namespace, text)); + public static void AddModuleName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Module, text)); - public static void AddOperator(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Operator, text)); + public static void AddNamespaceName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Namespace, text)); - public static void AddParameterName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Parameter, text)); + public static void AddOperator(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Operator, text)); - public static void AddPropertyName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Property, text)); + public static void AddParameterName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Parameter, text)); - public static void AddPunctuation(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Punctuation, text)); + public static void AddPropertyName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Property, text)); - public static void AddRangeVariableName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.RangeVariable, text)); + public static void AddPunctuation(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Punctuation, text)); - public static void AddStructName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Struct, text)); + public static void AddRangeVariableName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.RangeVariable, text)); - public static void AddSpace(this IList parts, string text = " ") - => parts.Add(new TaggedText(TextTags.Space, text)); + public static void AddStructName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Struct, text)); - public static void AddText(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.Text, text)); + public static void AddSpace(this IList parts, string text = " ") + => parts.Add(new TaggedText(TextTags.Space, text)); - public static void AddTypeParameterName(this IList parts, string text) - => parts.Add(new TaggedText(TextTags.TypeParameter, text)); - } + public static void AddText(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.Text, text)); + + public static void AddTypeParameterName(this IList parts, string text) + => parts.Add(new TaggedText(TextTags.TypeParameter, text)); } diff --git a/src/Features/Core/Portable/Common/TaggedTextStyle.cs b/src/Features/Core/Portable/Common/TaggedTextStyle.cs index 4440b13bb3434..fde6ec50538b7 100644 --- a/src/Features/Core/Portable/Common/TaggedTextStyle.cs +++ b/src/Features/Core/Portable/Common/TaggedTextStyle.cs @@ -4,21 +4,20 @@ using System; -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +[Flags] +internal enum TaggedTextStyle { - [Flags] - internal enum TaggedTextStyle - { - None = 0, + None = 0, - Strong = 1 << 0, + Strong = 1 << 0, - Emphasis = 1 << 1, + Emphasis = 1 << 1, - Underline = 1 << 2, + Underline = 1 << 2, - Code = 1 << 3, + Code = 1 << 3, - PreserveWhitespace = 1 << 4, - } + PreserveWhitespace = 1 << 4, } diff --git a/src/Features/Core/Portable/Common/TextTags.cs b/src/Features/Core/Portable/Common/TextTags.cs index ca2f1517116dc..105ee48bcf446 100644 --- a/src/Features/Core/Portable/Common/TextTags.cs +++ b/src/Features/Core/Portable/Common/TextTags.cs @@ -4,68 +4,67 @@ #nullable disable -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +/// +/// The set of well known text tags used for the property. +/// These tags influence the presentation of text. +/// +public static class TextTags { + public const string Alias = nameof(Alias); + public const string Assembly = nameof(Assembly); + public const string Class = nameof(Class); + public const string Delegate = nameof(Delegate); + public const string Enum = nameof(Enum); + public const string ErrorType = nameof(ErrorType); + public const string Event = nameof(Event); + public const string Field = nameof(Field); + public const string Interface = nameof(Interface); + public const string Keyword = nameof(Keyword); + public const string Label = nameof(Label); + public const string LineBreak = nameof(LineBreak); + public const string NumericLiteral = nameof(NumericLiteral); + public const string StringLiteral = nameof(StringLiteral); + public const string Local = nameof(Local); + public const string Method = nameof(Method); + public const string Module = nameof(Module); + public const string Namespace = nameof(Namespace); + public const string Operator = nameof(Operator); + public const string Parameter = nameof(Parameter); + public const string Property = nameof(Property); + public const string Punctuation = nameof(Punctuation); + public const string Space = nameof(Space); + public const string Struct = nameof(Struct); + public const string AnonymousTypeIndicator = nameof(AnonymousTypeIndicator); + public const string Text = nameof(Text); + public const string TypeParameter = nameof(TypeParameter); + public const string RangeVariable = nameof(RangeVariable); + public const string EnumMember = nameof(EnumMember); + public const string ExtensionMethod = nameof(ExtensionMethod); + public const string Constant = nameof(Constant); + public const string Record = nameof(Record); + public const string RecordStruct = nameof(RecordStruct); + /// - /// The set of well known text tags used for the property. - /// These tags influence the presentation of text. + /// Indicates the start of a text container. The elements after through (but not + /// including) the matching are rendered in a rectangular block which is positioned + /// as an inline element relative to surrounding elements. The text of the element + /// itself precedes the content of the container, and is typically a bullet or number header for an item in a + /// list. /// - public static class TextTags - { - public const string Alias = nameof(Alias); - public const string Assembly = nameof(Assembly); - public const string Class = nameof(Class); - public const string Delegate = nameof(Delegate); - public const string Enum = nameof(Enum); - public const string ErrorType = nameof(ErrorType); - public const string Event = nameof(Event); - public const string Field = nameof(Field); - public const string Interface = nameof(Interface); - public const string Keyword = nameof(Keyword); - public const string Label = nameof(Label); - public const string LineBreak = nameof(LineBreak); - public const string NumericLiteral = nameof(NumericLiteral); - public const string StringLiteral = nameof(StringLiteral); - public const string Local = nameof(Local); - public const string Method = nameof(Method); - public const string Module = nameof(Module); - public const string Namespace = nameof(Namespace); - public const string Operator = nameof(Operator); - public const string Parameter = nameof(Parameter); - public const string Property = nameof(Property); - public const string Punctuation = nameof(Punctuation); - public const string Space = nameof(Space); - public const string Struct = nameof(Struct); - public const string AnonymousTypeIndicator = nameof(AnonymousTypeIndicator); - public const string Text = nameof(Text); - public const string TypeParameter = nameof(TypeParameter); - public const string RangeVariable = nameof(RangeVariable); - public const string EnumMember = nameof(EnumMember); - public const string ExtensionMethod = nameof(ExtensionMethod); - public const string Constant = nameof(Constant); - public const string Record = nameof(Record); - public const string RecordStruct = nameof(RecordStruct); + internal const string ContainerStart = nameof(ContainerStart); - /// - /// Indicates the start of a text container. The elements after through (but not - /// including) the matching are rendered in a rectangular block which is positioned - /// as an inline element relative to surrounding elements. The text of the element - /// itself precedes the content of the container, and is typically a bullet or number header for an item in a - /// list. - /// - internal const string ContainerStart = nameof(ContainerStart); - - /// - /// Indicates the end of a text container. See . - /// - internal const string ContainerEnd = nameof(ContainerEnd); + /// + /// Indicates the end of a text container. See . + /// + internal const string ContainerEnd = nameof(ContainerEnd); - /// - /// Indicates the start of a code block. The elements after - /// through (but not including) the matching are rendered as - /// a codeblock in LSP markup. - /// - internal const string CodeBlockStart = nameof(CodeBlockStart); - internal const string CodeBlockEnd = nameof(CodeBlockEnd); - } + /// + /// Indicates the start of a code block. The elements after + /// through (but not including) the matching are rendered as + /// a codeblock in LSP markup. + /// + internal const string CodeBlockStart = nameof(CodeBlockStart); + internal const string CodeBlockEnd = nameof(CodeBlockEnd); } diff --git a/src/Features/Core/Portable/Common/UpdatedEventArgs.cs b/src/Features/Core/Portable/Common/UpdatedEventArgs.cs index 256e42fc34d7a..a3bd810e1cffa 100644 --- a/src/Features/Core/Portable/Common/UpdatedEventArgs.cs +++ b/src/Features/Core/Portable/Common/UpdatedEventArgs.cs @@ -4,28 +4,27 @@ using System; -namespace Microsoft.CodeAnalysis.Common +namespace Microsoft.CodeAnalysis.Common; + +internal class UpdatedEventArgs(object id, Workspace workspace, ProjectId? projectId, DocumentId? documentId) : EventArgs { - internal class UpdatedEventArgs(object id, Workspace workspace, ProjectId? projectId, DocumentId? documentId) : EventArgs - { - /// - /// The identity of update group. - /// - public object Id { get; } = id; + /// + /// The identity of update group. + /// + public object Id { get; } = id; - /// - /// this update is associated with. - /// - public Workspace Workspace { get; } = workspace; + /// + /// this update is associated with. + /// + public Workspace Workspace { get; } = workspace; - /// - /// this update is associated with, or . - /// - public ProjectId? ProjectId { get; } = projectId; + /// + /// this update is associated with, or . + /// + public ProjectId? ProjectId { get; } = projectId; - /// - /// this update is associated with, or . - /// - public DocumentId? DocumentId { get; } = documentId; - } + /// + /// this update is associated with, or . + /// + public DocumentId? DocumentId { get; } = documentId; } diff --git a/src/Features/Core/Portable/Completion/ArgumentContext.cs b/src/Features/Core/Portable/Completion/ArgumentContext.cs index 54c51f2e510ea..afd150ae304d6 100644 --- a/src/Features/Core/Portable/Completion/ArgumentContext.cs +++ b/src/Features/Core/Portable/Completion/ArgumentContext.cs @@ -6,57 +6,56 @@ using System.Threading; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// Provides context information for argument completion. +/// +internal sealed class ArgumentContext( + ArgumentProvider provider, + SemanticModel semanticModel, + int position, + IParameterSymbol parameter, + string? previousValue, + CancellationToken cancellationToken) { + internal ArgumentProvider Provider { get; } = provider ?? throw new ArgumentNullException(nameof(provider)); + + /// + /// Gets the semantic model where argument completion is requested. + /// + public SemanticModel SemanticModel { get; } = semanticModel ?? throw new ArgumentNullException(nameof(semanticModel)); + + /// + /// Gets the position within where argument completion is requested. + /// + public int Position { get; } = position; + + /// + /// Gets the symbol for the parameter for which an argument value is requested. + /// + public IParameterSymbol Parameter { get; } = parameter ?? throw new ArgumentNullException(nameof(parameter)); + + /// + /// Gets the previously-provided argument value for this parameter. + /// + /// + /// The existing text of the argument value, if the argument is already in code; otherwise, + /// when requesting a new argument value. + /// + public string? PreviousValue { get; } = previousValue; + + /// + /// Gets a cancellation token that argument providers may observe. + /// + public CancellationToken CancellationToken { get; } = cancellationToken; + /// - /// Provides context information for argument completion. + /// Gets or sets the default argument value. /// - internal sealed class ArgumentContext( - ArgumentProvider provider, - SemanticModel semanticModel, - int position, - IParameterSymbol parameter, - string? previousValue, - CancellationToken cancellationToken) - { - internal ArgumentProvider Provider { get; } = provider ?? throw new ArgumentNullException(nameof(provider)); - - /// - /// Gets the semantic model where argument completion is requested. - /// - public SemanticModel SemanticModel { get; } = semanticModel ?? throw new ArgumentNullException(nameof(semanticModel)); - - /// - /// Gets the position within where argument completion is requested. - /// - public int Position { get; } = position; - - /// - /// Gets the symbol for the parameter for which an argument value is requested. - /// - public IParameterSymbol Parameter { get; } = parameter ?? throw new ArgumentNullException(nameof(parameter)); - - /// - /// Gets the previously-provided argument value for this parameter. - /// - /// - /// The existing text of the argument value, if the argument is already in code; otherwise, - /// when requesting a new argument value. - /// - public string? PreviousValue { get; } = previousValue; - - /// - /// Gets a cancellation token that argument providers may observe. - /// - public CancellationToken CancellationToken { get; } = cancellationToken; - - /// - /// Gets or sets the default argument value. - /// - /// - /// If this value is not set, the argument completion session will insert a language-specific default value for - /// the argument. - /// - public string? DefaultValue { get; set; } - } + /// + /// If this value is not set, the argument completion session will insert a language-specific default value for + /// the argument. + /// + public string? DefaultValue { get; set; } } diff --git a/src/Features/Core/Portable/Completion/ArgumentProvider.cs b/src/Features/Core/Portable/Completion/ArgumentProvider.cs index a53911fe48283..4451c27d93829 100644 --- a/src/Features/Core/Portable/Completion/ArgumentProvider.cs +++ b/src/Features/Core/Portable/Completion/ArgumentProvider.cs @@ -7,23 +7,22 @@ using System.Text; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal abstract class ArgumentProvider { - internal abstract class ArgumentProvider - { - public string Name { get; } + public string Name { get; } - protected ArgumentProvider() - => Name = GetType().FullName!; + protected ArgumentProvider() + => Name = GetType().FullName!; - /// - /// Supports providing argument values for an argument completion session. - /// - /// - /// See for more information about argument values. - /// - /// The argument context. - /// A representing the asynchronous operation. - public abstract Task ProvideArgumentAsync(ArgumentContext context); - } + /// + /// Supports providing argument values for an argument completion session. + /// + /// + /// See for more information about argument values. + /// + /// The argument context. + /// A representing the asynchronous operation. + public abstract Task ProvideArgumentAsync(ArgumentContext context); } diff --git a/src/Features/Core/Portable/Completion/CharacterSetModificationKind.cs b/src/Features/Core/Portable/Completion/CharacterSetModificationKind.cs index a9e1538762cd9..aac00a3a32a05 100644 --- a/src/Features/Core/Portable/Completion/CharacterSetModificationKind.cs +++ b/src/Features/Core/Portable/Completion/CharacterSetModificationKind.cs @@ -2,26 +2,25 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// The kind of character set modification. +/// +public enum CharacterSetModificationKind { /// - /// The kind of character set modification. + /// The rule adds new characters onto the existing set of characters. /// - public enum CharacterSetModificationKind - { - /// - /// The rule adds new characters onto the existing set of characters. - /// - Add, + Add, - /// - /// The rule removes characters from the existing set of characters. - /// - Remove, + /// + /// The rule removes characters from the existing set of characters. + /// + Remove, - /// - /// The rule replaces the existing set of characters. - /// - Replace - } + /// + /// The rule replaces the existing set of characters. + /// + Replace } diff --git a/src/Features/Core/Portable/Completion/CharacterSetModificationRule.cs b/src/Features/Core/Portable/Completion/CharacterSetModificationRule.cs index bed17e217aff6..9115fa8976a73 100644 --- a/src/Features/Core/Portable/Completion/CharacterSetModificationRule.cs +++ b/src/Features/Core/Portable/Completion/CharacterSetModificationRule.cs @@ -4,45 +4,44 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// A rule that modifies a set of characters. +/// +public readonly struct CharacterSetModificationRule { /// - /// A rule that modifies a set of characters. + /// The kind of modification. /// - public readonly struct CharacterSetModificationRule - { - /// - /// The kind of modification. - /// - public CharacterSetModificationKind Kind { get; } + public CharacterSetModificationKind Kind { get; } - /// - /// One or more characters. - /// - public ImmutableArray Characters { get; } + /// + /// One or more characters. + /// + public ImmutableArray Characters { get; } - private CharacterSetModificationRule(CharacterSetModificationKind kind, ImmutableArray characters) - { - Kind = kind; - Characters = characters; - } + private CharacterSetModificationRule(CharacterSetModificationKind kind, ImmutableArray characters) + { + Kind = kind; + Characters = characters; + } - /// - /// Creates a new instance. - /// - /// The kind of rule. - /// One or more characters. These are typically punctuation characters. - /// - public static CharacterSetModificationRule Create(CharacterSetModificationKind kind, ImmutableArray characters) - => new(kind, characters); + /// + /// Creates a new instance. + /// + /// The kind of rule. + /// One or more characters. These are typically punctuation characters. + /// + public static CharacterSetModificationRule Create(CharacterSetModificationKind kind, ImmutableArray characters) + => new(kind, characters); - /// - /// Creates a new instance. - /// - /// The kind of rule. - /// One or more characters. These are typically punctuation characters. - /// - public static CharacterSetModificationRule Create(CharacterSetModificationKind kind, params char[] characters) - => new(kind, characters.ToImmutableArray()); - } + /// + /// Creates a new instance. + /// + /// The kind of rule. + /// One or more characters. These are typically punctuation characters. + /// + public static CharacterSetModificationRule Create(CharacterSetModificationKind kind, params char[] characters) + => new(kind, characters.ToImmutableArray()); } diff --git a/src/Features/Core/Portable/Completion/CommonCompletionItem.cs b/src/Features/Core/Portable/Completion/CommonCompletionItem.cs index 62119b609390e..5e7b8827ea033 100644 --- a/src/Features/Core/Portable/Completion/CommonCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/CommonCompletionItem.cs @@ -8,89 +8,88 @@ using Microsoft.CodeAnalysis.Tags; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal static class CommonCompletionItem { - internal static class CommonCompletionItem + public const string DescriptionProperty = nameof(DescriptionProperty); + + public static CompletionItem Create( + string displayText, + string? displayTextSuffix, + CompletionItemRules rules, + Glyph? glyph = null, + ImmutableArray description = default, + string? sortText = null, + string? filterText = null, + bool showsWarningIcon = false, + ImmutableArray> properties = default, + ImmutableArray tags = default, + string? inlineDescription = null, + string? displayTextPrefix = null, + bool isComplexTextEdit = false) { - public const string DescriptionProperty = nameof(DescriptionProperty); + tags = tags.NullToEmpty(); - public static CompletionItem Create( - string displayText, - string? displayTextSuffix, - CompletionItemRules rules, - Glyph? glyph = null, - ImmutableArray description = default, - string? sortText = null, - string? filterText = null, - bool showsWarningIcon = false, - ImmutableArray> properties = default, - ImmutableArray tags = default, - string? inlineDescription = null, - string? displayTextPrefix = null, - bool isComplexTextEdit = false) + if (glyph != null) { - tags = tags.NullToEmpty(); - - if (glyph != null) - { - // put glyph tags first - tags = GlyphTags.GetTags(glyph.Value).AddRange(tags); - } - - if (showsWarningIcon) - { - tags = tags.Add(WellKnownTags.Warning); - } - - if (!description.IsDefault && description.Length > 0) - { - properties = properties.NullToEmpty().Add(new KeyValuePair(DescriptionProperty, EncodeDescription(description.ToTaggedText()))); - } - - return CompletionItem.CreateInternal( - displayText: displayText, - displayTextSuffix: displayTextSuffix, - displayTextPrefix: displayTextPrefix, - filterText: filterText, - sortText: sortText, - properties: properties, - tags: tags, - rules: rules, - inlineDescription: inlineDescription, - isComplexTextEdit: isComplexTextEdit); + // put glyph tags first + tags = GlyphTags.GetTags(glyph.Value).AddRange(tags); } - public static bool HasDescription(CompletionItem item) - => item.TryGetProperty(DescriptionProperty, out var _); + if (showsWarningIcon) + { + tags = tags.Add(WellKnownTags.Warning); + } - public static CompletionDescription GetDescription(CompletionItem item) + if (!description.IsDefault && description.Length > 0) { - if (item.TryGetProperty(DescriptionProperty, out var encodedDescription)) - { - return DecodeDescription(encodedDescription); - } - else - { - return CompletionDescription.Empty; - } + properties = properties.NullToEmpty().Add(new KeyValuePair(DescriptionProperty, EncodeDescription(description.ToTaggedText()))); } - private static readonly char[] s_descriptionSeparators = ['|']; + return CompletionItem.CreateInternal( + displayText: displayText, + displayTextSuffix: displayTextSuffix, + displayTextPrefix: displayTextPrefix, + filterText: filterText, + sortText: sortText, + properties: properties, + tags: tags, + rules: rules, + inlineDescription: inlineDescription, + isComplexTextEdit: isComplexTextEdit); + } - private static string EncodeDescription(ImmutableArray description) - => string.Join("|", description.SelectMany(d => new[] { d.Tag, d.Text }).Select(t => t.Escape('\\', s_descriptionSeparators))); + public static bool HasDescription(CompletionItem item) + => item.TryGetProperty(DescriptionProperty, out var _); - private static CompletionDescription DecodeDescription(string encoded) + public static CompletionDescription GetDescription(CompletionItem item) + { + if (item.TryGetProperty(DescriptionProperty, out var encodedDescription)) { - var parts = encoded.Split(s_descriptionSeparators).Select(t => t.Unescape('\\')).ToArray(); + return DecodeDescription(encodedDescription); + } + else + { + return CompletionDescription.Empty; + } + } + + private static readonly char[] s_descriptionSeparators = ['|']; - var builder = ImmutableArray.Empty.ToBuilder(); - for (var i = 0; i < parts.Length; i += 2) - { - builder.Add(new TaggedText(parts[i], parts[i + 1])); - } + private static string EncodeDescription(ImmutableArray description) + => string.Join("|", description.SelectMany(d => new[] { d.Tag, d.Text }).Select(t => t.Escape('\\', s_descriptionSeparators))); - return CompletionDescription.Create(builder.ToImmutable()); + private static CompletionDescription DecodeDescription(string encoded) + { + var parts = encoded.Split(s_descriptionSeparators).Select(t => t.Unescape('\\')).ToArray(); + + var builder = ImmutableArray.Empty.ToBuilder(); + for (var i = 0; i < parts.Length; i += 2) + { + builder.Add(new TaggedText(parts[i], parts[i + 1])); } + + return CompletionDescription.Create(builder.ToImmutable()); } } diff --git a/src/Features/Core/Portable/Completion/CommonCompletionProvider.cs b/src/Features/Core/Portable/Completion/CommonCompletionProvider.cs index 63d44ce375327..be2eabb3e952e 100644 --- a/src/Features/Core/Portable/Completion/CommonCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/CommonCompletionProvider.cs @@ -16,117 +16,116 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal abstract class CommonCompletionProvider : CompletionProvider { - internal abstract class CommonCompletionProvider : CompletionProvider + private static readonly CompletionItemRules s_suggestionItemRules = CompletionItemRules.Create(enterKeyRule: EnterKeyRule.Never); + + /// + /// Language used to retrieve from . + /// Null for language agnostic values. + /// + internal abstract string Language { get; } + + /// + /// For backwards API compat only, should not be called. + /// + public sealed override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options) { - private static readonly CompletionItemRules s_suggestionItemRules = CompletionItemRules.Create(enterKeyRule: EnterKeyRule.Never); - - /// - /// Language used to retrieve from . - /// Null for language agnostic values. - /// - internal abstract string Language { get; } - - /// - /// For backwards API compat only, should not be called. - /// - public sealed override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options) - { - Debug.Fail("For backwards API compat only, should not be called"); + Debug.Fail("For backwards API compat only, should not be called"); - // Publicly available options do not affect this API. - return ShouldTriggerCompletionImpl(text, caretPosition, trigger, CompletionOptions.Default); - } + // Publicly available options do not affect this API. + return ShouldTriggerCompletionImpl(text, caretPosition, trigger, CompletionOptions.Default); + } - internal override bool ShouldTriggerCompletion(LanguageServices languageServices, SourceText text, int caretPosition, CompletionTrigger trigger, CompletionOptions options, OptionSet passThroughOptions) - => ShouldTriggerCompletionImpl(text, caretPosition, trigger, options); + internal override bool ShouldTriggerCompletion(LanguageServices languageServices, SourceText text, int caretPosition, CompletionTrigger trigger, CompletionOptions options, OptionSet passThroughOptions) + => ShouldTriggerCompletionImpl(text, caretPosition, trigger, options); - private bool ShouldTriggerCompletionImpl(SourceText text, int caretPosition, CompletionTrigger trigger, in CompletionOptions options) - => trigger.Kind == CompletionTriggerKind.Insertion && - caretPosition > 0 && - IsInsertionTrigger(text, insertedCharacterPosition: caretPosition - 1, options); + private bool ShouldTriggerCompletionImpl(SourceText text, int caretPosition, CompletionTrigger trigger, in CompletionOptions options) + => trigger.Kind == CompletionTriggerKind.Insertion && + caretPosition > 0 && + IsInsertionTrigger(text, insertedCharacterPosition: caretPosition - 1, options); - public virtual bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options) - => false; + public virtual bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options) + => false; - /// - /// For backwards API compat only, should not be called. - /// - public sealed override Task GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken) - { - Debug.Fail("For backwards API compat only, should not be called"); + /// + /// For backwards API compat only, should not be called. + /// + public sealed override Task GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken) + { + Debug.Fail("For backwards API compat only, should not be called"); - // Publicly available options do not affect this API. - return GetDescriptionAsync(document, item, CompletionOptions.Default, SymbolDescriptionOptions.Default, cancellationToken); - } + // Publicly available options do not affect this API. + return GetDescriptionAsync(document, item, CompletionOptions.Default, SymbolDescriptionOptions.Default, cancellationToken); + } - internal override async Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - { - // Get the actual description provided by whatever subclass we are. - // Then, if we would commit text that could be expanded as a snippet, - // put that information in the description so that the user knows. - var description = await GetDescriptionWorkerAsync(document, item, options, displayOptions, cancellationToken).ConfigureAwait(false); - var parts = await TryAddSnippetInvocationPartAsync(document, item, description.TaggedParts, cancellationToken).ConfigureAwait(false); + internal override async Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + { + // Get the actual description provided by whatever subclass we are. + // Then, if we would commit text that could be expanded as a snippet, + // put that information in the description so that the user knows. + var description = await GetDescriptionWorkerAsync(document, item, options, displayOptions, cancellationToken).ConfigureAwait(false); + var parts = await TryAddSnippetInvocationPartAsync(document, item, description.TaggedParts, cancellationToken).ConfigureAwait(false); - return description.WithTaggedParts(parts); - } + return description.WithTaggedParts(parts); + } - private async Task> TryAddSnippetInvocationPartAsync( - Document document, CompletionItem item, - ImmutableArray parts, CancellationToken cancellationToken) + private async Task> TryAddSnippetInvocationPartAsync( + Document document, CompletionItem item, + ImmutableArray parts, CancellationToken cancellationToken) + { + var snippetService = document.Project.Services.GetService(); + if (snippetService != null) { - var snippetService = document.Project.Services.GetService(); - if (snippetService != null) + var change = await GetTextChangeAsync(document, item, ch: '\t', cancellationToken: cancellationToken).ConfigureAwait(false) ?? + new TextChange(item.Span, item.DisplayText); + var insertionText = change.NewText; + + if (snippetService != null && snippetService.SnippetShortcutExists_NonBlocking(insertionText)) { - var change = await GetTextChangeAsync(document, item, ch: '\t', cancellationToken: cancellationToken).ConfigureAwait(false) ?? - new TextChange(item.Span, item.DisplayText); - var insertionText = change.NewText; + var note = string.Format(FeaturesResources.Note_colon_Tab_twice_to_insert_the_0_snippet, insertionText); - if (snippetService != null && snippetService.SnippetShortcutExists_NonBlocking(insertionText)) + if (parts.Any()) { - var note = string.Format(FeaturesResources.Note_colon_Tab_twice_to_insert_the_0_snippet, insertionText); - - if (parts.Any()) - { - parts = parts.Add(new TaggedText(TextTags.LineBreak, Environment.NewLine)); - } - - parts = parts.Add(new TaggedText(TextTags.Text, note)); + parts = parts.Add(new TaggedText(TextTags.LineBreak, Environment.NewLine)); } - } - return parts; + parts = parts.Add(new TaggedText(TextTags.Text, note)); + } } - internal virtual Task GetDescriptionWorkerAsync( - Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - { - return CommonCompletionItem.HasDescription(item) - ? Task.FromResult(CommonCompletionItem.GetDescription(item)) - : Task.FromResult(CompletionDescription.Empty); - } + return parts; + } - public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) - { - var change = (await GetTextChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false)) - ?? new TextChange(item.Span, item.DisplayText); - return CompletionChange.Create(change); - } + internal virtual Task GetDescriptionWorkerAsync( + Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + { + return CommonCompletionItem.HasDescription(item) + ? Task.FromResult(CommonCompletionItem.GetDescription(item)) + : Task.FromResult(CompletionDescription.Empty); + } - public virtual Task GetTextChangeAsync(Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) - => GetTextChangeAsync(selectedItem, ch, cancellationToken); + public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) + { + var change = (await GetTextChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false)) + ?? new TextChange(item.Span, item.DisplayText); + return CompletionChange.Create(change); + } - protected virtual Task GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) - => SpecializedTasks.Default(); + public virtual Task GetTextChangeAsync(Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + => GetTextChangeAsync(selectedItem, ch, cancellationToken); - protected static CompletionItem CreateSuggestionModeItem(string? displayText, string? description) - { - return CommonCompletionItem.Create( - displayText: displayText ?? string.Empty, - displayTextSuffix: "", - description: description == null ? default : description.ToSymbolDisplayParts(), - rules: s_suggestionItemRules); - } + protected virtual Task GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + => SpecializedTasks.Default(); + + protected static CompletionItem CreateSuggestionModeItem(string? displayText, string? description) + { + return CommonCompletionItem.Create( + displayText: displayText ?? string.Empty, + displayTextSuffix: "", + description: description == null ? default : description.ToSymbolDisplayParts(), + rules: s_suggestionItemRules); } } diff --git a/src/Features/Core/Portable/Completion/CommonCompletionService.cs b/src/Features/Core/Portable/Completion/CommonCompletionService.cs index b30f3c2ff5a78..83842b17529c6 100644 --- a/src/Features/Core/Portable/Completion/CommonCompletionService.cs +++ b/src/Features/Core/Portable/Completion/CommonCompletionService.cs @@ -9,46 +9,45 @@ using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Tags; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal abstract partial class CommonCompletionService : CompletionService { - internal abstract partial class CommonCompletionService : CompletionService + protected CommonCompletionService(SolutionServices services, IAsynchronousOperationListenerProvider listenerProvider) + : base(services, listenerProvider) { - protected CommonCompletionService(SolutionServices services, IAsynchronousOperationListenerProvider listenerProvider) - : base(services, listenerProvider) - { - } + } - protected override CompletionItem GetBetterItem(CompletionItem item, CompletionItem existingItem) + protected override CompletionItem GetBetterItem(CompletionItem item, CompletionItem existingItem) + { + // We've constructed the export order of completion providers so + // that snippets are exported after everything else. That way, + // when we choose a single item per display text, snippet + // glyphs appear by snippets. This breaks pre-selection of items + // whose display text is also a snippet (workitem 852578), + // the snippet item doesn't have its preselect bit set. + // We'll special case this by not preferring later items + // if they are snippets and the other candidate is preselected. + if (existingItem.Rules.MatchPriority != MatchPriority.Default && IsSnippetItem(item)) { - // We've constructed the export order of completion providers so - // that snippets are exported after everything else. That way, - // when we choose a single item per display text, snippet - // glyphs appear by snippets. This breaks pre-selection of items - // whose display text is also a snippet (workitem 852578), - // the snippet item doesn't have its preselect bit set. - // We'll special case this by not preferring later items - // if they are snippets and the other candidate is preselected. - if (existingItem.Rules.MatchPriority != MatchPriority.Default && IsSnippetItem(item)) - { - return existingItem; - } - - return base.GetBetterItem(item, existingItem); + return existingItem; } - protected static bool IsKeywordItem(CompletionItem item) - => item.Tags.Contains(WellKnownTags.Keyword); + return base.GetBetterItem(item, existingItem); + } - protected static bool IsSnippetItem(CompletionItem item) - => item.Tags.Contains(WellKnownTags.Snippet); + protected static bool IsKeywordItem(CompletionItem item) + => item.Tags.Contains(WellKnownTags.Keyword); - internal override void FilterItems( - Document document, - IReadOnlyList matchResults, - string filterText, - IList builder) - { - CompletionService.FilterItems(CompletionHelper.GetHelper(document), matchResults, filterText, builder); - } + protected static bool IsSnippetItem(CompletionItem item) + => item.Tags.Contains(WellKnownTags.Snippet); + + internal override void FilterItems( + Document document, + IReadOnlyList matchResults, + string filterText, + IList builder) + { + CompletionService.FilterItems(CompletionHelper.GetHelper(document), matchResults, filterText, builder); } } diff --git a/src/Features/Core/Portable/Completion/CommonCompletionUtilities.cs b/src/Features/Core/Portable/Completion/CommonCompletionUtilities.cs index 5f5fec8585994..544a13614bcb9 100644 --- a/src/Features/Core/Portable/Completion/CommonCompletionUtilities.cs +++ b/src/Features/Core/Portable/Completion/CommonCompletionUtilities.cs @@ -20,244 +20,243 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal static class CommonCompletionUtilities { - internal static class CommonCompletionUtilities + private const string NonBreakingSpaceString = "\x00A0"; + + public static TextSpan GetWordSpan(SourceText text, int position, + Func isWordStartCharacter, Func isWordCharacter) { - private const string NonBreakingSpaceString = "\x00A0"; + return GetWordSpan(text, position, isWordStartCharacter, isWordCharacter, alwaysExtendEndSpan: false); + } - public static TextSpan GetWordSpan(SourceText text, int position, - Func isWordStartCharacter, Func isWordCharacter) + public static TextSpan GetWordSpan(SourceText text, int position, + Func isWordStartCharacter, Func isWordCharacter, bool alwaysExtendEndSpan = false) + { + var start = position; + while (start > 0 && isWordStartCharacter(text[start - 1])) { - return GetWordSpan(text, position, isWordStartCharacter, isWordCharacter, alwaysExtendEndSpan: false); + start--; } - public static TextSpan GetWordSpan(SourceText text, int position, - Func isWordStartCharacter, Func isWordCharacter, bool alwaysExtendEndSpan = false) + // If we're brought up in the middle of a word, extend to the end of the word as well. + // This means that if a user brings up the completion list at the start of the word they + // will "insert" the text before what's already there (useful for qualifying existing + // text). However, if they bring up completion in the "middle" of a word, then they will + // "overwrite" the text. Useful for correcting misspellings or just replacing unwanted + // code with new code. + var end = position; + if (start != position || alwaysExtendEndSpan) { - var start = position; - while (start > 0 && isWordStartCharacter(text[start - 1])) - { - start--; - } - - // If we're brought up in the middle of a word, extend to the end of the word as well. - // This means that if a user brings up the completion list at the start of the word they - // will "insert" the text before what's already there (useful for qualifying existing - // text). However, if they bring up completion in the "middle" of a word, then they will - // "overwrite" the text. Useful for correcting misspellings or just replacing unwanted - // code with new code. - var end = position; - if (start != position || alwaysExtendEndSpan) + while (end < text.Length && isWordCharacter(text[end])) { - while (end < text.Length && isWordCharacter(text[end])) - { - end++; - } + end++; } - - return TextSpan.FromBounds(start, end); } - public static bool IsStartingNewWord(SourceText text, int characterPosition, Func isWordStartCharacter, Func isWordCharacter) - { - var ch = text[characterPosition]; - if (!isWordStartCharacter(ch)) - { - return false; - } - - // Only want to trigger if we're the first character in an identifier. If there's a - // character before or after us, then we don't want to trigger. - if (characterPosition > 0 && - isWordCharacter(text[characterPosition - 1])) - { - return false; - } - - if (characterPosition < text.Length - 1 && - isWordCharacter(text[characterPosition + 1])) - { - return false; - } - - return true; - } + return TextSpan.FromBounds(start, end); + } - public static Func> CreateDescriptionFactory( - SolutionServices workspaceServices, - SemanticModel semanticModel, - int position, - ISymbol symbol, - SymbolDescriptionOptions options) + public static bool IsStartingNewWord(SourceText text, int characterPosition, Func isWordStartCharacter, Func isWordCharacter) + { + var ch = text[characterPosition]; + if (!isWordStartCharacter(ch)) { - return CreateDescriptionFactory(workspaceServices, semanticModel, position, options, [symbol]); + return false; } - public static Func> CreateDescriptionFactory( - SolutionServices workspaceServices, SemanticModel semanticModel, int position, SymbolDescriptionOptions options, IReadOnlyList symbols) + // Only want to trigger if we're the first character in an identifier. If there's a + // character before or after us, then we don't want to trigger. + if (characterPosition > 0 && + isWordCharacter(text[characterPosition - 1])) { - return c => CreateDescriptionAsync(workspaceServices, semanticModel, position, symbols, options, supportedPlatforms: null, cancellationToken: c); + return false; } - public static Func> CreateDescriptionFactory( - SolutionServices workspaceServices, SemanticModel semanticModel, int position, IReadOnlyList symbols, SymbolDescriptionOptions options, SupportedPlatformData supportedPlatforms) + if (characterPosition < text.Length - 1 && + isWordCharacter(text[characterPosition + 1])) { - return c => CreateDescriptionAsync(workspaceServices, semanticModel, position, symbols, options, supportedPlatforms: supportedPlatforms, cancellationToken: c); + return false; } - public static async Task CreateDescriptionAsync( - SolutionServices workspaceServices, SemanticModel semanticModel, int position, ISymbol symbol, int overloadCount, SymbolDescriptionOptions options, SupportedPlatformData? supportedPlatforms, CancellationToken cancellationToken) - { - var symbolDisplayService = workspaceServices.GetRequiredLanguageService(semanticModel.Language); - var formatter = workspaceServices.GetRequiredLanguageService(semanticModel.Language); + return true; + } - // TODO(cyrusn): Figure out a way to cancel this. - var sections = await symbolDisplayService.ToDescriptionGroupsAsync(semanticModel, position, [symbol], options, cancellationToken).ConfigureAwait(false); + public static Func> CreateDescriptionFactory( + SolutionServices workspaceServices, + SemanticModel semanticModel, + int position, + ISymbol symbol, + SymbolDescriptionOptions options) + { + return CreateDescriptionFactory(workspaceServices, semanticModel, position, options, [symbol]); + } - if (!sections.TryGetValue(SymbolDescriptionGroups.MainDescription, out var mainDescriptionTexts)) - { - return CompletionDescription.Empty; - } + public static Func> CreateDescriptionFactory( + SolutionServices workspaceServices, SemanticModel semanticModel, int position, SymbolDescriptionOptions options, IReadOnlyList symbols) + { + return c => CreateDescriptionAsync(workspaceServices, semanticModel, position, symbols, options, supportedPlatforms: null, cancellationToken: c); + } - var textContentBuilder = new List(); - textContentBuilder.AddRange(mainDescriptionTexts); + public static Func> CreateDescriptionFactory( + SolutionServices workspaceServices, SemanticModel semanticModel, int position, IReadOnlyList symbols, SymbolDescriptionOptions options, SupportedPlatformData supportedPlatforms) + { + return c => CreateDescriptionAsync(workspaceServices, semanticModel, position, symbols, options, supportedPlatforms: supportedPlatforms, cancellationToken: c); + } - switch (symbol.Kind) - { - case SymbolKind.Method: - case SymbolKind.Property: - case SymbolKind.NamedType: - if (overloadCount > 0) - { - var isGeneric = symbol.GetArity() > 0; + public static async Task CreateDescriptionAsync( + SolutionServices workspaceServices, SemanticModel semanticModel, int position, ISymbol symbol, int overloadCount, SymbolDescriptionOptions options, SupportedPlatformData? supportedPlatforms, CancellationToken cancellationToken) + { + var symbolDisplayService = workspaceServices.GetRequiredLanguageService(semanticModel.Language); + var formatter = workspaceServices.GetRequiredLanguageService(semanticModel.Language); - textContentBuilder.AddSpace(); - textContentBuilder.AddPunctuation("("); - textContentBuilder.AddPunctuation("+"); - textContentBuilder.AddText(NonBreakingSpaceString + overloadCount.ToString()); + // TODO(cyrusn): Figure out a way to cancel this. + var sections = await symbolDisplayService.ToDescriptionGroupsAsync(semanticModel, position, [symbol], options, cancellationToken).ConfigureAwait(false); - AddOverloadPart(textContentBuilder, overloadCount, isGeneric); + if (!sections.TryGetValue(SymbolDescriptionGroups.MainDescription, out var mainDescriptionTexts)) + { + return CompletionDescription.Empty; + } - textContentBuilder.AddPunctuation(")"); - } + var textContentBuilder = new List(); + textContentBuilder.AddRange(mainDescriptionTexts); - break; - } + switch (symbol.Kind) + { + case SymbolKind.Method: + case SymbolKind.Property: + case SymbolKind.NamedType: + if (overloadCount > 0) + { + var isGeneric = symbol.GetArity() > 0; - AddDocumentationPart(textContentBuilder, symbol, semanticModel, position, formatter, cancellationToken); + textContentBuilder.AddSpace(); + textContentBuilder.AddPunctuation("("); + textContentBuilder.AddPunctuation("+"); + textContentBuilder.AddText(NonBreakingSpaceString + overloadCount.ToString()); - if (sections.TryGetValue(SymbolDescriptionGroups.AwaitableUsageText, out var parts)) - { - textContentBuilder.AddRange(parts); - } + AddOverloadPart(textContentBuilder, overloadCount, isGeneric); - if (sections.TryGetValue(SymbolDescriptionGroups.StructuralTypes, out parts)) - { - if (!parts.IsDefaultOrEmpty) - { - textContentBuilder.AddLineBreak(); - textContentBuilder.AddLineBreak(); - textContentBuilder.AddRange(parts); + textContentBuilder.AddPunctuation(")"); } - } - - if (supportedPlatforms != null) - { - textContentBuilder.AddLineBreak(); - textContentBuilder.AddRange(supportedPlatforms.ToDisplayParts().ToTaggedText()); - } - return CompletionDescription.Create(textContentBuilder.AsImmutable()); + break; } - public static Task CreateDescriptionAsync( - SolutionServices workspaceServices, SemanticModel semanticModel, int position, IReadOnlyList symbols, SymbolDescriptionOptions options, SupportedPlatformData? supportedPlatforms, CancellationToken cancellationToken) - { - // Lets try to find the first non-obsolete symbol (overload) and fall-back - // to the first symbol if all are obsolete. - var symbol = symbols.FirstOrDefault(s => !s.IsObsolete()) ?? symbols[0]; - - return CreateDescriptionAsync(workspaceServices, semanticModel, position, symbol, overloadCount: symbols.Count - 1, options, supportedPlatforms, cancellationToken); - } + AddDocumentationPart(textContentBuilder, symbol, semanticModel, position, formatter, cancellationToken); - private static void AddOverloadPart(List textContentBuilder, int overloadCount, bool isGeneric) + if (sections.TryGetValue(SymbolDescriptionGroups.AwaitableUsageText, out var parts)) { - var text = isGeneric - ? overloadCount == 1 - ? FeaturesResources.generic_overload - : FeaturesResources.generic_overloads - : overloadCount == 1 - ? FeaturesResources.overload - : FeaturesResources.overloads_; - - textContentBuilder.AddText(NonBreakingSpaceString + text); + textContentBuilder.AddRange(parts); } - private static void AddDocumentationPart( - List textContentBuilder, ISymbol symbol, SemanticModel semanticModel, int position, IDocumentationCommentFormattingService formatter, CancellationToken cancellationToken) + if (sections.TryGetValue(SymbolDescriptionGroups.StructuralTypes, out parts)) { - var documentation = symbol.GetDocumentationParts(semanticModel, position, formatter, cancellationToken); - - if (documentation.Any()) + if (!parts.IsDefaultOrEmpty) { textContentBuilder.AddLineBreak(); - textContentBuilder.AddRange(documentation); + textContentBuilder.AddLineBreak(); + textContentBuilder.AddRange(parts); } } - internal static bool IsTextualTriggerString(SourceText text, int characterPosition, string value) + if (supportedPlatforms != null) { - // The character position starts at the last character of 'value'. So if 'value' has - // length 1, then we don't want to move, if it has length 2 we want to move back one, - // etc. - characterPosition = characterPosition - value.Length + 1; + textContentBuilder.AddLineBreak(); + textContentBuilder.AddRange(supportedPlatforms.ToDisplayParts().ToTaggedText()); + } - for (var i = 0; i < value.Length; i++, characterPosition++) - { - if (characterPosition < 0 || characterPosition >= text.Length) - { - return false; - } + return CompletionDescription.Create(textContentBuilder.AsImmutable()); + } - if (text[characterPosition] != value[i]) - { - return false; - } - } + public static Task CreateDescriptionAsync( + SolutionServices workspaceServices, SemanticModel semanticModel, int position, IReadOnlyList symbols, SymbolDescriptionOptions options, SupportedPlatformData? supportedPlatforms, CancellationToken cancellationToken) + { + // Lets try to find the first non-obsolete symbol (overload) and fall-back + // to the first symbol if all are obsolete. + var symbol = symbols.FirstOrDefault(s => !s.IsObsolete()) ?? symbols[0]; - return true; - } + return CreateDescriptionAsync(workspaceServices, semanticModel, position, symbol, overloadCount: symbols.Count - 1, options, supportedPlatforms, cancellationToken); + } - public static bool TryRemoveAttributeSuffix(ISymbol symbol, SyntaxContext context, [NotNullWhen(true)] out string? name) + private static void AddOverloadPart(List textContentBuilder, int overloadCount, bool isGeneric) + { + var text = isGeneric + ? overloadCount == 1 + ? FeaturesResources.generic_overload + : FeaturesResources.generic_overloads + : overloadCount == 1 + ? FeaturesResources.overload + : FeaturesResources.overloads_; + + textContentBuilder.AddText(NonBreakingSpaceString + text); + } + + private static void AddDocumentationPart( + List textContentBuilder, ISymbol symbol, SemanticModel semanticModel, int position, IDocumentationCommentFormattingService formatter, CancellationToken cancellationToken) + { + var documentation = symbol.GetDocumentationParts(semanticModel, position, formatter, cancellationToken); + + if (documentation.Any()) { - var isAttributeNameContext = context.IsAttributeNameContext; - var syntaxFacts = context.GetRequiredLanguageService(); + textContentBuilder.AddLineBreak(); + textContentBuilder.AddRange(documentation); + } + } - if (!isAttributeNameContext) + internal static bool IsTextualTriggerString(SourceText text, int characterPosition, string value) + { + // The character position starts at the last character of 'value'. So if 'value' has + // length 1, then we don't want to move, if it has length 2 we want to move back one, + // etc. + characterPosition = characterPosition - value.Length + 1; + + for (var i = 0; i < value.Length; i++, characterPosition++) + { + if (characterPosition < 0 || characterPosition >= text.Length) { - name = null; return false; } - // Do the symbol textual check first. Then the more expensive symbolic check. - if (!symbol.Name.TryGetWithoutAttributeSuffix(syntaxFacts.IsCaseSensitive, out name) || - !symbol.IsAttribute()) + if (text[characterPosition] != value[i]) { return false; } + } + + return true; + } - return true; + public static bool TryRemoveAttributeSuffix(ISymbol symbol, SyntaxContext context, [NotNullWhen(true)] out string? name) + { + var isAttributeNameContext = context.IsAttributeNameContext; + var syntaxFacts = context.GetRequiredLanguageService(); + + if (!isAttributeNameContext) + { + name = null; + return false; } - internal static ImmutableHashSet GetTriggerCharacters(CompletionProvider provider) + // Do the symbol textual check first. Then the more expensive symbolic check. + if (!symbol.Name.TryGetWithoutAttributeSuffix(syntaxFacts.IsCaseSensitive, out name) || + !symbol.IsAttribute()) { - if (provider is LSPCompletionProvider lspProvider) - { - return lspProvider.TriggerCharacters; - } + return false; + } - return []; + return true; + } + + internal static ImmutableHashSet GetTriggerCharacters(CompletionProvider provider) + { + if (provider is LSPCompletionProvider lspProvider) + { + return lspProvider.TriggerCharacters; } + + return []; } } diff --git a/src/Features/Core/Portable/Completion/CompletionChange.cs b/src/Features/Core/Portable/Completion/CompletionChange.cs index f88f5bce1a779..3525f04dddecc 100644 --- a/src/Features/Core/Portable/Completion/CompletionChange.cs +++ b/src/Features/Core/Portable/Completion/CompletionChange.cs @@ -8,137 +8,136 @@ using System.Linq; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// The change to be applied to the document when a is committed. +/// +public sealed class CompletionChange { /// - /// The change to be applied to the document when a is committed. + /// The text change to be applied to the document. This must always be supplied and is useful for hosts that + /// can apply a large text change efficiently while only making minimal edits to a file. + /// + public TextChange TextChange { get; } + + /// + /// Individual smaller text changes that are more fine grained than the total value. + /// This can be useful for host that do not support diffing changes to find minimal edits. Even if this is + /// provided, must still be provided as well. + /// + public ImmutableArray TextChanges { get; } + + /// + /// The new caret position after the change has been applied. + /// If null then the new caret position will be determined by the completion host. + /// + public int? NewPosition { get; } + + /// + /// True if the changes include the typed character that caused the + /// to be committed. If false the completion host will determine if and where the commit + /// character is inserted into the document. /// - public sealed class CompletionChange + public bool IncludesCommitCharacter { get; } + + internal ImmutableDictionary Properties { get; } + + private CompletionChange( + TextChange textChange, ImmutableArray textChanges, int? newPosition, bool includesCommitCharacter) + : this(textChange, textChanges, newPosition, includesCommitCharacter, ImmutableDictionary.Empty) + { + } + + private CompletionChange( + TextChange textChange, ImmutableArray textChanges, int? newPosition, bool includesCommitCharacter, ImmutableDictionary properties) { - /// - /// The text change to be applied to the document. This must always be supplied and is useful for hosts that - /// can apply a large text change efficiently while only making minimal edits to a file. - /// - public TextChange TextChange { get; } - - /// - /// Individual smaller text changes that are more fine grained than the total value. - /// This can be useful for host that do not support diffing changes to find minimal edits. Even if this is - /// provided, must still be provided as well. - /// - public ImmutableArray TextChanges { get; } - - /// - /// The new caret position after the change has been applied. - /// If null then the new caret position will be determined by the completion host. - /// - public int? NewPosition { get; } - - /// - /// True if the changes include the typed character that caused the - /// to be committed. If false the completion host will determine if and where the commit - /// character is inserted into the document. - /// - public bool IncludesCommitCharacter { get; } - - internal ImmutableDictionary Properties { get; } - - private CompletionChange( - TextChange textChange, ImmutableArray textChanges, int? newPosition, bool includesCommitCharacter) - : this(textChange, textChanges, newPosition, includesCommitCharacter, ImmutableDictionary.Empty) - { - } - - private CompletionChange( - TextChange textChange, ImmutableArray textChanges, int? newPosition, bool includesCommitCharacter, ImmutableDictionary properties) - { - TextChange = textChange; - NewPosition = newPosition; - IncludesCommitCharacter = includesCommitCharacter; - TextChanges = textChanges.NullToEmpty(); - if (TextChanges.IsEmpty) - TextChanges = [textChange]; - Properties = properties; - } - - /// - /// Creates a new instance. - /// - /// The text changes to be applied to the document. - /// The new caret position after the change has been applied. If null then the caret - /// position is not specified and will be determined by the completion host. - /// True if the changes include the typed character that caused the to be committed. If false, the completion host will determine if and where the - /// commit character is inserted into the document. - /// - /// This factory method is only valid when has a single entry in it. If there - /// are multiple entries, must be called instead, - /// with both the individual text changes, and an aggregated text change for hosts that only support that. - /// - [Obsolete("Use Create overload that takes a single TextChange and multiple TextChanges instead", error: true)] - public static CompletionChange Create( - ImmutableArray textChanges, - int? newPosition = null, - bool includesCommitCharacter = false) - { - return new CompletionChange(textChanges.Single(), textChanges, newPosition, includesCommitCharacter); - } + TextChange = textChange; + NewPosition = newPosition; + IncludesCommitCharacter = includesCommitCharacter; + TextChanges = textChanges.NullToEmpty(); + if (TextChanges.IsEmpty) + TextChanges = [textChange]; + Properties = properties; + } + + /// + /// Creates a new instance. + /// + /// The text changes to be applied to the document. + /// The new caret position after the change has been applied. If null then the caret + /// position is not specified and will be determined by the completion host. + /// True if the changes include the typed character that caused the to be committed. If false, the completion host will determine if and where the + /// commit character is inserted into the document. + /// + /// This factory method is only valid when has a single entry in it. If there + /// are multiple entries, must be called instead, + /// with both the individual text changes, and an aggregated text change for hosts that only support that. + /// + [Obsolete("Use Create overload that takes a single TextChange and multiple TextChanges instead", error: true)] + public static CompletionChange Create( + ImmutableArray textChanges, + int? newPosition = null, + bool includesCommitCharacter = false) + { + return new CompletionChange(textChanges.Single(), textChanges, newPosition, includesCommitCharacter); + } #pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads - public static CompletionChange Create( + public static CompletionChange Create( #pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads - TextChange textChange, - int? newPosition = null, - bool includesCommitCharacter = false) - { - return new CompletionChange(textChange, textChanges: default, newPosition, includesCommitCharacter); - } + TextChange textChange, + int? newPosition = null, + bool includesCommitCharacter = false) + { + return new CompletionChange(textChange, textChanges: default, newPosition, includesCommitCharacter); + } #pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads. #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static CompletionChange Create( + public static CompletionChange Create( #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters #pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads. - TextChange textChange, - ImmutableArray textChanges, - int? newPosition = null, - bool includesCommitCharacter = false) - { - return new CompletionChange(textChange, textChanges, newPosition, includesCommitCharacter); - } - - internal static CompletionChange Create( - TextChange textChange, - ImmutableArray textChanges, - ImmutableDictionary properties, - int? newPosition, - bool includesCommitCharacter) - { - return new CompletionChange(textChange, textChanges, newPosition, includesCommitCharacter, properties); - } - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionChange WithTextChange(TextChange textChange) - => new(textChange, TextChanges, NewPosition, IncludesCommitCharacter); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionChange WithTextChanges(ImmutableArray textChanges) - => new(TextChange, textChanges, NewPosition, IncludesCommitCharacter); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionChange WithNewPosition(int? newPostion) - => new(TextChange, TextChanges, newPostion, IncludesCommitCharacter); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionChange WithIncludesCommitCharacter(bool includesCommitCharacter) - => new(TextChange, TextChanges, NewPosition, includesCommitCharacter); + TextChange textChange, + ImmutableArray textChanges, + int? newPosition = null, + bool includesCommitCharacter = false) + { + return new CompletionChange(textChange, textChanges, newPosition, includesCommitCharacter); + } + + internal static CompletionChange Create( + TextChange textChange, + ImmutableArray textChanges, + ImmutableDictionary properties, + int? newPosition, + bool includesCommitCharacter) + { + return new CompletionChange(textChange, textChanges, newPosition, includesCommitCharacter, properties); } + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionChange WithTextChange(TextChange textChange) + => new(textChange, TextChanges, NewPosition, IncludesCommitCharacter); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionChange WithTextChanges(ImmutableArray textChanges) + => new(TextChange, textChanges, NewPosition, IncludesCommitCharacter); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionChange WithNewPosition(int? newPostion) + => new(TextChange, TextChanges, newPostion, IncludesCommitCharacter); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionChange WithIncludesCommitCharacter(bool includesCommitCharacter) + => new(TextChange, TextChanges, NewPosition, includesCommitCharacter); } diff --git a/src/Features/Core/Portable/Completion/CompletionContext.cs b/src/Features/Core/Portable/Completion/CompletionContext.cs index 3714451076a13..2dce69ee8231f 100644 --- a/src/Features/Core/Portable/Completion/CompletionContext.cs +++ b/src/Features/Core/Portable/Completion/CompletionContext.cs @@ -14,223 +14,222 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// The context presented to a when providing completions. +/// +public sealed class CompletionContext { + private readonly SegmentedList _items = []; + + private CompletionItem? _suggestionModeItem; + private bool _isExclusive; + + internal CompletionProvider Provider { get; } + /// - /// The context presented to a when providing completions. + /// The document that completion was invoked within. /// - public sealed class CompletionContext - { - private readonly SegmentedList _items = []; - - private CompletionItem? _suggestionModeItem; - private bool _isExclusive; - - internal CompletionProvider Provider { get; } - - /// - /// The document that completion was invoked within. - /// - public Document Document { get; } - - /// - /// The caret position when completion was triggered. - /// - public int Position { get; } - - /// - /// By providing this object, we have an opportunity to share requested SyntaxContext among all CompletionProviders - /// during a completion session to reduce repeat computation. - /// - private SharedSyntaxContextsWithSpeculativeModel? SharedSyntaxContextsWithSpeculativeModel { get; } - - /// - /// The span of the syntax element at the caret position. - /// - /// This is the most common value used for and will - /// be automatically assigned to any that has no specified. - /// - [Obsolete("Not used anymore. Use CompletionListSpan instead.", error: true)] - public TextSpan DefaultItemSpan { get; } + public Document Document { get; } + + /// + /// The caret position when completion was triggered. + /// + public int Position { get; } + + /// + /// By providing this object, we have an opportunity to share requested SyntaxContext among all CompletionProviders + /// during a completion session to reduce repeat computation. + /// + private SharedSyntaxContextsWithSpeculativeModel? SharedSyntaxContextsWithSpeculativeModel { get; } + + /// + /// The span of the syntax element at the caret position. + /// + /// This is the most common value used for and will + /// be automatically assigned to any that has no specified. + /// + [Obsolete("Not used anymore. Use CompletionListSpan instead.", error: true)] + public TextSpan DefaultItemSpan { get; } #pragma warning disable RS0030 // Do not used banned APIs - /// - /// The span of the document the completion list corresponds to. It will be set initially to - /// the result of , but it can - /// be overwritten during . - /// The purpose of the span is to: - /// 1. Signify where the completions should be presented. - /// 2. Designate any existing text in the document that should be used for filtering. - /// 3. Specify, by default, what portion of the text should be replaced when a completion - /// item is committed. - /// - public TextSpan CompletionListSpan { get; set; } + /// + /// The span of the document the completion list corresponds to. It will be set initially to + /// the result of , but it can + /// be overwritten during . + /// The purpose of the span is to: + /// 1. Signify where the completions should be presented. + /// 2. Designate any existing text in the document that should be used for filtering. + /// 3. Specify, by default, what portion of the text should be replaced when a completion + /// item is committed. + /// + public TextSpan CompletionListSpan { get; set; } #pragma warning restore - /// - /// The triggering action that caused completion to be started. - /// - public CompletionTrigger Trigger { get; } - - /// - /// The options that completion was started with. - /// - internal CompletionOptions CompletionOptions { get; } - - /// - /// The cancellation token to use for this operation. - /// - public CancellationToken CancellationToken { get; } - - /// - /// Set to true if the items added here should be the only items presented to the user. - /// Expand items should never be exclusive. - /// - public bool IsExclusive - { - get - { - return _isExclusive && !Provider.IsExpandItemProvider; - } + /// + /// The triggering action that caused completion to be started. + /// + public CompletionTrigger Trigger { get; } - set - { - if (value) - Debug.Assert(!Provider.IsExpandItemProvider); + /// + /// The options that completion was started with. + /// + internal CompletionOptions CompletionOptions { get; } - _isExclusive = value; - } + /// + /// The cancellation token to use for this operation. + /// + public CancellationToken CancellationToken { get; } + + /// + /// Set to true if the items added here should be the only items presented to the user. + /// Expand items should never be exclusive. + /// + public bool IsExclusive + { + get + { + return _isExclusive && !Provider.IsExpandItemProvider; } - /// - /// The options that completion was started with. - /// - public OptionSet Options { get; } - - /// - /// Creates a instance. - /// - public CompletionContext( - CompletionProvider provider, - Document document, - int position, - TextSpan defaultSpan, - CompletionTrigger trigger, - OptionSet? options, - CancellationToken cancellationToken) - : this(provider ?? throw new ArgumentNullException(nameof(provider)), - document ?? throw new ArgumentNullException(nameof(document)), - position, - sharedSyntaxContextsWithSpeculativeModel: null, - defaultSpan, - trigger, - // Publicly available options do not affect this API. - CompletionOptions.Default, - cancellationToken) + set { + if (value) + Debug.Assert(!Provider.IsExpandItemProvider); + + _isExclusive = value; + } + } + + /// + /// The options that completion was started with. + /// + public OptionSet Options { get; } + + /// + /// Creates a instance. + /// + public CompletionContext( + CompletionProvider provider, + Document document, + int position, + TextSpan defaultSpan, + CompletionTrigger trigger, + OptionSet? options, + CancellationToken cancellationToken) + : this(provider ?? throw new ArgumentNullException(nameof(provider)), + document ?? throw new ArgumentNullException(nameof(document)), + position, + sharedSyntaxContextsWithSpeculativeModel: null, + defaultSpan, + trigger, + // Publicly available options do not affect this API. + CompletionOptions.Default, + cancellationToken) + { #pragma warning disable RS0030 // Do not used banned APIs - Options = options ?? OptionSet.Empty; + Options = options ?? OptionSet.Empty; #pragma warning restore - } + } - /// - /// Creates a instance. - /// - internal CompletionContext( - CompletionProvider provider, - Document document, - int position, - SharedSyntaxContextsWithSpeculativeModel? sharedSyntaxContextsWithSpeculativeModel, - TextSpan defaultSpan, - CompletionTrigger trigger, - in CompletionOptions options, - CancellationToken cancellationToken) - { - Provider = provider; - Document = document; - Position = position; - CompletionListSpan = defaultSpan; - Trigger = trigger; - CompletionOptions = options; - CancellationToken = cancellationToken; + /// + /// Creates a instance. + /// + internal CompletionContext( + CompletionProvider provider, + Document document, + int position, + SharedSyntaxContextsWithSpeculativeModel? sharedSyntaxContextsWithSpeculativeModel, + TextSpan defaultSpan, + CompletionTrigger trigger, + in CompletionOptions options, + CancellationToken cancellationToken) + { + Provider = provider; + Document = document; + Position = position; + CompletionListSpan = defaultSpan; + Trigger = trigger; + CompletionOptions = options; + CancellationToken = cancellationToken; - SharedSyntaxContextsWithSpeculativeModel = sharedSyntaxContextsWithSpeculativeModel; + SharedSyntaxContextsWithSpeculativeModel = sharedSyntaxContextsWithSpeculativeModel; #pragma warning disable RS0030 // Do not used banned APIs - Options = OptionSet.Empty; + Options = OptionSet.Empty; #pragma warning restore - } + } - internal IReadOnlyList Items => _items; + internal IReadOnlyList Items => _items; - public void AddItem(CompletionItem item) + public void AddItem(CompletionItem item) + { + if (item == null) { - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } + throw new ArgumentNullException(nameof(item)); + } + + item = FixItem(item); + _items.Add(item); + } - item = FixItem(item); - _items.Add(item); + public void AddItems(IEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); } - public void AddItems(IEnumerable items) + foreach (var item in items) { - if (items == null) - { - throw new ArgumentNullException(nameof(items)); - } + AddItem(item); + } + } - foreach (var item in items) - { - AddItem(item); - } + /// + /// An optional that appears selected in the list presented to the user during suggestion mode. + /// + /// Suggestion mode disables auto-selection of items in the list, giving preference to the text typed by the user unless a specific item is selected manually. + /// + /// Specifying a is a request that the completion host operate in suggestion mode. + /// The item specified determines the text displayed and the description associated with it unless a different item is manually selected. + /// + /// No text is ever inserted when this item is completed, leaving the text the user typed instead. + /// + public CompletionItem? SuggestionModeItem + { + get + { + return _suggestionModeItem; } - /// - /// An optional that appears selected in the list presented to the user during suggestion mode. - /// - /// Suggestion mode disables auto-selection of items in the list, giving preference to the text typed by the user unless a specific item is selected manually. - /// - /// Specifying a is a request that the completion host operate in suggestion mode. - /// The item specified determines the text displayed and the description associated with it unless a different item is manually selected. - /// - /// No text is ever inserted when this item is completed, leaving the text the user typed instead. - /// - public CompletionItem? SuggestionModeItem + set { - get + if (value != null) { - return _suggestionModeItem; + value = FixItem(value); } - set - { - if (value != null) - { - value = FixItem(value); - } - - _suggestionModeItem = value; - } + _suggestionModeItem = value; } + } - internal Task GetSyntaxContextWithExistingSpeculativeModelAsync(Document document, CancellationToken cancellationToken) - { - if (SharedSyntaxContextsWithSpeculativeModel is null) - return Utilities.CreateSyntaxContextWithExistingSpeculativeModelAsync(document, Position, cancellationToken); + internal Task GetSyntaxContextWithExistingSpeculativeModelAsync(Document document, CancellationToken cancellationToken) + { + if (SharedSyntaxContextsWithSpeculativeModel is null) + return Utilities.CreateSyntaxContextWithExistingSpeculativeModelAsync(document, Position, cancellationToken); - return SharedSyntaxContextsWithSpeculativeModel.GetSyntaxContextAsync(document, cancellationToken); - } + return SharedSyntaxContextsWithSpeculativeModel.GetSyntaxContextAsync(document, cancellationToken); + } - private CompletionItem FixItem(CompletionItem item) - { - // remember provider so we can find it again later - item.ProviderName = Provider.Name; + private CompletionItem FixItem(CompletionItem item) + { + // remember provider so we can find it again later + item.ProviderName = Provider.Name; - item.Span = CompletionListSpan; + item.Span = CompletionListSpan; - return item; - } + return item; } } diff --git a/src/Features/Core/Portable/Completion/CompletionDescription.cs b/src/Features/Core/Portable/Completion/CompletionDescription.cs index 8d05a1c3fe5fe..3ea4c0833efda 100644 --- a/src/Features/Core/Portable/Completion/CompletionDescription.cs +++ b/src/Features/Core/Portable/Completion/CompletionDescription.cs @@ -6,70 +6,69 @@ using System.Linq; using System.Threading; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// The description of a . +/// +public sealed class CompletionDescription { + private string? _text; + /// - /// The description of a . + /// The used when there is no description. /// - public sealed class CompletionDescription - { - private string? _text; - - /// - /// The used when there is no description. - /// - public static readonly CompletionDescription Empty = new([]); + public static readonly CompletionDescription Empty = new([]); - /// - /// The individual tagged parts of the description. - /// - public ImmutableArray TaggedParts { get; } + /// + /// The individual tagged parts of the description. + /// + public ImmutableArray TaggedParts { get; } - private CompletionDescription(ImmutableArray taggedParts) - => TaggedParts = taggedParts.NullToEmpty(); + private CompletionDescription(ImmutableArray taggedParts) + => TaggedParts = taggedParts.NullToEmpty(); - /// - /// Creates a new instance of with the specified parts. - /// - /// The individual tagged parts of the description. - public static CompletionDescription Create(ImmutableArray taggedParts) - => new(taggedParts); + /// + /// Creates a new instance of with the specified parts. + /// + /// The individual tagged parts of the description. + public static CompletionDescription Create(ImmutableArray taggedParts) + => new(taggedParts); - /// - /// Creates a new instance of from untagged text. - /// - public static CompletionDescription FromText(string text) - => new([new TaggedText(TextTags.Text, text)]); + /// + /// Creates a new instance of from untagged text. + /// + public static CompletionDescription FromText(string text) + => new([new TaggedText(TextTags.Text, text)]); - /// - /// Creates a copy of this with the property changed. - /// - public CompletionDescription WithTaggedParts(ImmutableArray taggedParts) + /// + /// Creates a copy of this with the property changed. + /// + public CompletionDescription WithTaggedParts(ImmutableArray taggedParts) + { + if (taggedParts != TaggedParts) { - if (taggedParts != TaggedParts) - { - return new CompletionDescription(taggedParts); - } - else - { - return this; - } + return new CompletionDescription(taggedParts); + } + else + { + return this; } + } - /// - /// The text of the description without tags. - /// - public string Text + /// + /// The text of the description without tags. + /// + public string Text + { + get { - get + if (_text == null) { - if (_text == null) - { - Interlocked.CompareExchange(ref _text, string.Concat(TaggedParts.Select(p => p.Text)), null); - } - - return _text; + Interlocked.CompareExchange(ref _text, string.Concat(TaggedParts.Select(p => p.Text)), null); } + + return _text; } } } diff --git a/src/Features/Core/Portable/Completion/CompletionFilterReason.cs b/src/Features/Core/Portable/Completion/CompletionFilterReason.cs index dac35538d4a74..7861731d8b5b1 100644 --- a/src/Features/Core/Portable/Completion/CompletionFilterReason.cs +++ b/src/Features/Core/Portable/Completion/CompletionFilterReason.cs @@ -2,24 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal enum CompletionFilterReason { - internal enum CompletionFilterReason - { - Insertion, - Deletion, - CaretPositionChanged, - Other, + Insertion, + Deletion, + CaretPositionChanged, + Other, #if false - // If necessary, we could add additional filter reasons. For example, for the below items. - // However, we have no need for them currently. That somewhat makes sense. We only want - // to really customize our filtering behavior depending on if a user was typing/deleting - // in the buffer. - Snippets, - ItemFiltersChanged, - Invoke, - InvokeAndCommitIfUnique + // If necessary, we could add additional filter reasons. For example, for the below items. + // However, we have no need for them currently. That somewhat makes sense. We only want + // to really customize our filtering behavior depending on if a user was typing/deleting + // in the buffer. + Snippets, + ItemFiltersChanged, + Invoke, + InvokeAndCommitIfUnique #endif - } } diff --git a/src/Features/Core/Portable/Completion/CompletionHelper.cs b/src/Features/Core/Portable/Completion/CompletionHelper.cs index 47c8f8439bfce..82cf6c6c95b04 100644 --- a/src/Features/Core/Portable/Completion/CompletionHelper.cs +++ b/src/Features/Core/Portable/Completion/CompletionHelper.cs @@ -9,269 +9,268 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Tags; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal sealed class CompletionHelper(bool isCaseSensitive) { - internal sealed class CompletionHelper(bool isCaseSensitive) - { - private static CompletionHelper CaseSensitiveInstance { get; } = new CompletionHelper(isCaseSensitive: true); - private static CompletionHelper CaseInsensitiveInstance { get; } = new CompletionHelper(isCaseSensitive: false); + private static CompletionHelper CaseSensitiveInstance { get; } = new CompletionHelper(isCaseSensitive: true); + private static CompletionHelper CaseInsensitiveInstance { get; } = new CompletionHelper(isCaseSensitive: false); - private readonly bool _isCaseSensitive = isCaseSensitive; + private readonly bool _isCaseSensitive = isCaseSensitive; - public static CompletionHelper GetHelper(Document document) - { - var syntaxFacts = document.GetLanguageService(); - var caseSensitive = syntaxFacts?.IsCaseSensitive ?? true; + public static CompletionHelper GetHelper(Document document) + { + var syntaxFacts = document.GetLanguageService(); + var caseSensitive = syntaxFacts?.IsCaseSensitive ?? true; - return caseSensitive - ? CaseSensitiveInstance - : CaseInsensitiveInstance; - } + return caseSensitive + ? CaseSensitiveInstance + : CaseInsensitiveInstance; + } - public int CompareMatchResults(MatchResult matchResult1, MatchResult matchResult2, bool filterTextHasNoUpperCase) - { - var item1 = matchResult1.CompletionItem; - var match1 = matchResult1.PatternMatch; + public int CompareMatchResults(MatchResult matchResult1, MatchResult matchResult2, bool filterTextHasNoUpperCase) + { + var item1 = matchResult1.CompletionItem; + var match1 = matchResult1.PatternMatch; - var item2 = matchResult2.CompletionItem; - var match2 = matchResult2.PatternMatch; + var item2 = matchResult2.CompletionItem; + var match2 = matchResult2.PatternMatch; - if (match1 != null && match2 != null) - { - var result = CompareItems(match1.Value, match2.Value, item1, item2, _isCaseSensitive, filterTextHasNoUpperCase); - if (result != 0) - { - return result; - } - } - else if (match1 != null) - { - return -1; - } - else if (match2 != null) + if (match1 != null && match2 != null) + { + var result = CompareItems(match1.Value, match2.Value, item1, item2, _isCaseSensitive, filterTextHasNoUpperCase); + if (result != 0) { - return 1; + return result; } + } + else if (match1 != null) + { + return -1; + } + else if (match2 != null) + { + return 1; + } - var matchPriorityDiff = CompareSpecialMatchPriorityValues(item1, item2); - if (matchPriorityDiff != 0) - { - return matchPriorityDiff; - } + var matchPriorityDiff = CompareSpecialMatchPriorityValues(item1, item2); + if (matchPriorityDiff != 0) + { + return matchPriorityDiff; + } - // Prefer things with a keyword tag, if the filter texts are the same. - if (!TagsEqual(item1, item2) && item1.FilterText == item2.FilterText) - { - return (!IsKeywordItem(item1)).CompareTo(!IsKeywordItem(item2)); - } + // Prefer things with a keyword tag, if the filter texts are the same. + if (!TagsEqual(item1, item2) && item1.FilterText == item2.FilterText) + { + return (!IsKeywordItem(item1)).CompareTo(!IsKeywordItem(item2)); + } - return 0; + return 0; - static bool IsKeywordItem(CompletionItem item) - => item.Tags.Contains(WellKnownTags.Keyword); + static bool IsKeywordItem(CompletionItem item) + => item.Tags.Contains(WellKnownTags.Keyword); - static bool TagsEqual(CompletionItem item1, CompletionItem item2) - => System.Linq.ImmutableArrayExtensions.SequenceEqual(item1.Tags, item2.Tags); + static bool TagsEqual(CompletionItem item1, CompletionItem item2) + => System.Linq.ImmutableArrayExtensions.SequenceEqual(item1.Tags, item2.Tags); + } + + private static int CompareItems( + PatternMatch match1, + PatternMatch match2, + CompletionItem item1, + CompletionItem item2, + bool isCaseSensitive, + bool filterTextHasNoUpperCase) + { + // *Almost* always prefer non-expanded item regardless of the pattern matching result. + // Except when all non-expanded items are worse than prefix matching and there's + // a complete match from expanded ones. + // + // For example, In the scenarios below, `NS2.Designer` would be selected over `System.Security.Cryptography.DES` + // + // namespace System.Security.Cryptography + // { + // class DES {} + // } + // namespace NS2 + // { + // class Designer {} + // class C + // { + // des$$ + // } + // } + // + // But in this case, `System.Security.Cryptography.DES` would be selected over `NS2.MyDesigner` + // + // namespace System.Security.Cryptography + // { + // class DES {} + // } + // namespace NS2 + // { + // class MyDesigner {} + // class C + // { + // des$$ + // } + // } + // + // This currently means items from unimported namespaces (those are the only expanded items now) + // are treated as "2nd tier" results, which forces users to be more explicit about selecting them. + var expandedDiff = CompareExpandedItem(item1, match1, item2, match2); + if (expandedDiff != 0) + { + return expandedDiff; } - private static int CompareItems( - PatternMatch match1, - PatternMatch match2, - CompletionItem item1, - CompletionItem item2, - bool isCaseSensitive, - bool filterTextHasNoUpperCase) + // Then see how the two items compare in a case insensitive fashion. Matches that + // are strictly better (ignoring case) should prioritize the item. i.e. if we have + // a prefix match, that should always be better than a substring match. + // + // The reason we ignore case is that it's very common for people to type expecting + // completion to fix up their casing. i.e. 'false' will be written with the + // expectation that it will get fixed by the completion list to 'False'. + var caseInsensitiveComparison = match1.CompareTo(match2, ignoreCase: true); + if (caseInsensitiveComparison != 0) { - // *Almost* always prefer non-expanded item regardless of the pattern matching result. - // Except when all non-expanded items are worse than prefix matching and there's - // a complete match from expanded ones. - // - // For example, In the scenarios below, `NS2.Designer` would be selected over `System.Security.Cryptography.DES` - // - // namespace System.Security.Cryptography - // { - // class DES {} - // } - // namespace NS2 - // { - // class Designer {} - // class C - // { - // des$$ - // } - // } - // - // But in this case, `System.Security.Cryptography.DES` would be selected over `NS2.MyDesigner` - // - // namespace System.Security.Cryptography - // { - // class DES {} - // } - // namespace NS2 - // { - // class MyDesigner {} - // class C - // { - // des$$ - // } - // } - // - // This currently means items from unimported namespaces (those are the only expanded items now) - // are treated as "2nd tier" results, which forces users to be more explicit about selecting them. - var expandedDiff = CompareExpandedItem(item1, match1, item2, match2); - if (expandedDiff != 0) - { - return expandedDiff; - } + return caseInsensitiveComparison; + } - // Then see how the two items compare in a case insensitive fashion. Matches that - // are strictly better (ignoring case) should prioritize the item. i.e. if we have - // a prefix match, that should always be better than a substring match. - // - // The reason we ignore case is that it's very common for people to type expecting - // completion to fix up their casing. i.e. 'false' will be written with the - // expectation that it will get fixed by the completion list to 'False'. - var caseInsensitiveComparison = match1.CompareTo(match2, ignoreCase: true); - if (caseInsensitiveComparison != 0) - { - return caseInsensitiveComparison; - } + // Now we have two items match in case-insensitive manner, + // + // 1. if we are in a case-insensitive language, we'd first check if either item has the MatchPriority set to one of + // the two special values ("Preselect" and "Deprioritize"). If so and these two items have different MatchPriority, + // then we'd select the one of "Preselect", or the one that's not of "Deprioritize". Otherwise we will prefer the one + // matches case-sensitively. This is to make sure common items in VB like "True" and "False" are prioritized for selection + // when user types "t" and "f" (see https://github.com/dotnet/roslyn/issues/4892) + // + // 2. or similarly, if the filter text contains only lowercase letters, we want to relax our filtering standard a tiny + // bit to account for the sceanrio that users expect completion to fix the casing. This only happens if one of the item's + // MatchPriority is "Deprioritize". Otherwise we will always prefer the one matches case-sensitively. + // This is to make sure uncommon items like conversion "(short)" are not selected over `Should` when user types `sho` + // (see https://github.com/dotnet/roslyn/issues/55546) + + var specialMatchPriorityValuesDiff = 0; + if (!isCaseSensitive) + { + specialMatchPriorityValuesDiff = CompareSpecialMatchPriorityValues(item1, item2); + } + else if (filterTextHasNoUpperCase) + { + specialMatchPriorityValuesDiff = CompareDeprioritization(item1, item2); + } - // Now we have two items match in case-insensitive manner, - // - // 1. if we are in a case-insensitive language, we'd first check if either item has the MatchPriority set to one of - // the two special values ("Preselect" and "Deprioritize"). If so and these two items have different MatchPriority, - // then we'd select the one of "Preselect", or the one that's not of "Deprioritize". Otherwise we will prefer the one - // matches case-sensitively. This is to make sure common items in VB like "True" and "False" are prioritized for selection - // when user types "t" and "f" (see https://github.com/dotnet/roslyn/issues/4892) - // - // 2. or similarly, if the filter text contains only lowercase letters, we want to relax our filtering standard a tiny - // bit to account for the sceanrio that users expect completion to fix the casing. This only happens if one of the item's - // MatchPriority is "Deprioritize". Otherwise we will always prefer the one matches case-sensitively. - // This is to make sure uncommon items like conversion "(short)" are not selected over `Should` when user types `sho` - // (see https://github.com/dotnet/roslyn/issues/55546) + if (specialMatchPriorityValuesDiff != 0) + return specialMatchPriorityValuesDiff; - var specialMatchPriorityValuesDiff = 0; - if (!isCaseSensitive) - { - specialMatchPriorityValuesDiff = CompareSpecialMatchPriorityValues(item1, item2); - } - else if (filterTextHasNoUpperCase) + // At this point we have two items which we're matching in a rather similar fashion. + // If one is a prefix of the other, prefer the prefix. i.e. if we have + // "Table" and "table:=" and the user types 't' and we are in a case insensitive + // language, then we prefer the former. + if (item1.GetEntireDisplayText().Length != item2.GetEntireDisplayText().Length) + { + var comparison = isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + if (item2.GetEntireDisplayText().StartsWith(item1.GetEntireDisplayText(), comparison)) { - specialMatchPriorityValuesDiff = CompareDeprioritization(item1, item2); + return -1; } - - if (specialMatchPriorityValuesDiff != 0) - return specialMatchPriorityValuesDiff; - - // At this point we have two items which we're matching in a rather similar fashion. - // If one is a prefix of the other, prefer the prefix. i.e. if we have - // "Table" and "table:=" and the user types 't' and we are in a case insensitive - // language, then we prefer the former. - if (item1.GetEntireDisplayText().Length != item2.GetEntireDisplayText().Length) + else if (item1.GetEntireDisplayText().StartsWith(item2.GetEntireDisplayText(), comparison)) { - var comparison = isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - if (item2.GetEntireDisplayText().StartsWith(item1.GetEntireDisplayText(), comparison)) - { - return -1; - } - else if (item1.GetEntireDisplayText().StartsWith(item2.GetEntireDisplayText(), comparison)) - { - return 1; - } + return 1; } - - // Now compare the matches again in a case sensitive manner. If everything was - // equal up to this point, we prefer the item that better matches based on case. - return match1.CompareTo(match2, ignoreCase: false); } - private static int CompareSpecialMatchPriorityValues(CompletionItem item1, CompletionItem item2) - { - if (item1.Rules.MatchPriority == item2.Rules.MatchPriority) - return 0; + // Now compare the matches again in a case sensitive manner. If everything was + // equal up to this point, we prefer the item that better matches based on case. + return match1.CompareTo(match2, ignoreCase: false); + } - var deprioritizationCompare = CompareDeprioritization(item1, item2); - return deprioritizationCompare == 0 - ? ComparePreselection(item1, item2) - : deprioritizationCompare; - } + private static int CompareSpecialMatchPriorityValues(CompletionItem item1, CompletionItem item2) + { + if (item1.Rules.MatchPriority == item2.Rules.MatchPriority) + return 0; - /// - /// If 2 items differ on preselection, then item1 is better if it is preselected, otherwise it is worse. - /// - private static int ComparePreselection(CompletionItem item1, CompletionItem item2) - => (item1.Rules.MatchPriority != MatchPriority.Preselect).CompareTo(item2.Rules.MatchPriority != MatchPriority.Preselect); + var deprioritizationCompare = CompareDeprioritization(item1, item2); + return deprioritizationCompare == 0 + ? ComparePreselection(item1, item2) + : deprioritizationCompare; + } - /// - /// If 2 items differ on depriorization, then item1 is worse if it is depriozritized, otherwise it is better. - /// - private static int CompareDeprioritization(CompletionItem item1, CompletionItem item2) - => (item1.Rules.MatchPriority == MatchPriority.Deprioritize).CompareTo(item2.Rules.MatchPriority == MatchPriority.Deprioritize); + /// + /// If 2 items differ on preselection, then item1 is better if it is preselected, otherwise it is worse. + /// + private static int ComparePreselection(CompletionItem item1, CompletionItem item2) + => (item1.Rules.MatchPriority != MatchPriority.Preselect).CompareTo(item2.Rules.MatchPriority != MatchPriority.Preselect); - private static int CompareExpandedItem(CompletionItem item1, PatternMatch match1, CompletionItem item2, PatternMatch match2) - { - var isItem1Expanded = item1.Flags.IsExpanded(); - var isItem2Expanded = item2.Flags.IsExpanded(); + /// + /// If 2 items differ on depriorization, then item1 is worse if it is depriozritized, otherwise it is better. + /// + private static int CompareDeprioritization(CompletionItem item1, CompletionItem item2) + => (item1.Rules.MatchPriority == MatchPriority.Deprioritize).CompareTo(item2.Rules.MatchPriority == MatchPriority.Deprioritize); - // Consider them equal if both items are of the same kind (i.e. both expanded or non-expanded) - if (isItem1Expanded == isItem2Expanded) - { - return 0; - } + private static int CompareExpandedItem(CompletionItem item1, PatternMatch match1, CompletionItem item2, PatternMatch match2) + { + var isItem1Expanded = item1.Flags.IsExpanded(); + var isItem2Expanded = item2.Flags.IsExpanded(); - // Now we have two items of different kind. - // If neither item is exact match, we always prefer non-expanded one. - // For example, `NS2.MyTask` would be selected over `NS1.Tasks` - // - // namespace NS1 - // { - // class Tasks {} - // } - // namespace NS2 - // { - // class MyTask {} - // class C - // { - // task$$ - // } - // } - if (match1.Kind != PatternMatchKind.Exact && match2.Kind != PatternMatchKind.Exact) - { - return isItem1Expanded ? 1 : -1; - } + // Consider them equal if both items are of the same kind (i.e. both expanded or non-expanded) + if (isItem1Expanded == isItem2Expanded) + { + return 0; + } - // Now we have two items of different kind and at least one is exact match. - // Prefer non-expanded item if it is prefix match or better. - // In the scenarios below, `NS2.Designer` would be selected over `System.Security.Cryptography.DES` - // - // namespace System.Security.Cryptography - // { - // class DES {} - // } - // namespace NS2 - // { - // class Designer {} - // class C - // { - // des$$ - // } - // } - if (!isItem1Expanded && match1.Kind <= PatternMatchKind.Prefix) - { - return -1; - } + // Now we have two items of different kind. + // If neither item is exact match, we always prefer non-expanded one. + // For example, `NS2.MyTask` would be selected over `NS1.Tasks` + // + // namespace NS1 + // { + // class Tasks {} + // } + // namespace NS2 + // { + // class MyTask {} + // class C + // { + // task$$ + // } + // } + if (match1.Kind != PatternMatchKind.Exact && match2.Kind != PatternMatchKind.Exact) + { + return isItem1Expanded ? 1 : -1; + } - if (!isItem2Expanded && match2.Kind <= PatternMatchKind.Prefix) - { - return 1; - } + // Now we have two items of different kind and at least one is exact match. + // Prefer non-expanded item if it is prefix match or better. + // In the scenarios below, `NS2.Designer` would be selected over `System.Security.Cryptography.DES` + // + // namespace System.Security.Cryptography + // { + // class DES {} + // } + // namespace NS2 + // { + // class Designer {} + // class C + // { + // des$$ + // } + // } + if (!isItem1Expanded && match1.Kind <= PatternMatchKind.Prefix) + { + return -1; + } - // Now we are left with an expanded item with exact match and a non-expanded item with worse than prefix match. - // Prefer non-expanded item with exact match. - Debug.Assert(isItem1Expanded && match1.Kind == PatternMatchKind.Exact && !isItem2Expanded && match2.Kind > PatternMatchKind.Prefix || - isItem2Expanded && match2.Kind == PatternMatchKind.Exact && !isItem1Expanded && match1.Kind > PatternMatchKind.Prefix); - return isItem1Expanded ? -1 : 1; + if (!isItem2Expanded && match2.Kind <= PatternMatchKind.Prefix) + { + return 1; } + + // Now we are left with an expanded item with exact match and a non-expanded item with worse than prefix match. + // Prefer non-expanded item with exact match. + Debug.Assert(isItem1Expanded && match1.Kind == PatternMatchKind.Exact && !isItem2Expanded && match2.Kind > PatternMatchKind.Prefix || + isItem2Expanded && match2.Kind == PatternMatchKind.Exact && !isItem1Expanded && match1.Kind > PatternMatchKind.Prefix); + return isItem1Expanded ? -1 : 1; } } diff --git a/src/Features/Core/Portable/Completion/CompletionItem.cs b/src/Features/Core/Portable/Completion/CompletionItem.cs index 2ace992ef1e23..1ff6fe6482976 100644 --- a/src/Features/Core/Portable/Completion/CompletionItem.cs +++ b/src/Features/Core/Portable/Completion/CompletionItem.cs @@ -11,540 +11,539 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// One of many possible completions used to form the completion list presented to the user. +/// +[DebuggerDisplay("{DisplayText}")] +public sealed class CompletionItem : IComparable { + private readonly string? _filterText; + private string? _lazyEntireDisplayText; + private ImmutableDictionary? _lazyPropertiesAsImmutableDictionary; + private readonly ImmutableArray> _properties; + /// - /// One of many possible completions used to form the completion list presented to the user. + /// The text that is displayed to the user. /// - [DebuggerDisplay("{DisplayText}")] - public sealed class CompletionItem : IComparable - { - private readonly string? _filterText; - private string? _lazyEntireDisplayText; - private ImmutableDictionary? _lazyPropertiesAsImmutableDictionary; - private readonly ImmutableArray> _properties; - - /// - /// The text that is displayed to the user. - /// - public string DisplayText { get; } - - /// - /// An optional prefix to be displayed prepended to . Can be null. - /// Pattern-matching of user input will not be performed against this, but only against . - /// - public string DisplayTextPrefix { get; } - - /// - /// An optional suffix to be displayed appended to . Can be null. - /// Pattern-matching of user input will not be performed against this, but only against . - /// - public string DisplayTextSuffix { get; } - - /// - /// The text used to determine if the item matches the filter and is show in the list. - /// This is often the same as but may be different in certain circumstances. - /// - public string FilterText => _filterText ?? DisplayText; - - /// - /// If provided, each additional string would be used in the same way as for item matching. - /// However, there's a key difference: matches of is considered inferior than matches - /// of when they have identical pattern matching result. - /// - internal ImmutableArray AdditionalFilterTexts { get; init; } = []; - - /// - /// Returns if is identical to . - /// Otherwise returns . - /// Be aware that this value is independent from and could return - /// even if is . - /// - internal bool HasDifferentFilterText => _filterText != null; - - internal bool HasAdditionalFilterTexts => !AdditionalFilterTexts.IsEmpty; - - /// - /// The text used to determine the order that the item appears in the list. - /// This is often the same as the but may be different in certain circumstances. - /// - public string SortText { get; } - - /// - /// Descriptive text to place after in the display layer. Should - /// be short as it will show up in the UI. Display will present this in a way to distinguish - /// this from the normal text (for example, by fading out and right-aligning). - /// - public string InlineDescription { get; } - - /// - /// The span of the syntax element associated with this item. - /// - /// The span identifies the text in the document that is used to filter the initial list presented to the user, - /// and typically represents the region of the document that will be changed if this item is committed. - /// The latter is not always true because individual provider is free to make more complex changes to the document. - /// If this is the case, the provider should set to true. - /// - public TextSpan Span { get; internal set; } - - /// - /// Additional information attached to a completion item by it creator. - /// - public ImmutableDictionary Properties - { - get - { - if (_lazyPropertiesAsImmutableDictionary is null) - _lazyPropertiesAsImmutableDictionary = _properties.ToImmutableDictionary(); + public string DisplayText { get; } - return _lazyPropertiesAsImmutableDictionary; - } - } + /// + /// An optional prefix to be displayed prepended to . Can be null. + /// Pattern-matching of user input will not be performed against this, but only against . + /// + public string DisplayTextPrefix { get; } - internal ImmutableArray> GetProperties() - { - return _properties; - } + /// + /// An optional suffix to be displayed appended to . Can be null. + /// Pattern-matching of user input will not be performed against this, but only against . + /// + public string DisplayTextSuffix { get; } - internal bool TryGetProperty(string name, [NotNullWhen(true)] out string? value) - { - if (_lazyPropertiesAsImmutableDictionary is not null) - return _lazyPropertiesAsImmutableDictionary.TryGetValue(name, out value); + /// + /// The text used to determine if the item matches the filter and is show in the list. + /// This is often the same as but may be different in certain circumstances. + /// + public string FilterText => _filterText ?? DisplayText; - foreach ((var propName, var propValue) in _properties) - { - if (name == propName) - { - value = propValue; - return true; - } - } + /// + /// If provided, each additional string would be used in the same way as for item matching. + /// However, there's a key difference: matches of is considered inferior than matches + /// of when they have identical pattern matching result. + /// + internal ImmutableArray AdditionalFilterTexts { get; init; } = []; - value = null; - return false; - } + /// + /// Returns if is identical to . + /// Otherwise returns . + /// Be aware that this value is independent from and could return + /// even if is . + /// + internal bool HasDifferentFilterText => _filterText != null; - internal string GetProperty(string name) - { - if (TryGetProperty(name, out var value)) - return value; + internal bool HasAdditionalFilterTexts => !AdditionalFilterTexts.IsEmpty; - // Let ImmutableDictionary handle throwing - if (_lazyPropertiesAsImmutableDictionary is not null) - return _lazyPropertiesAsImmutableDictionary[name]; + /// + /// The text used to determine the order that the item appears in the list. + /// This is often the same as the but may be different in certain circumstances. + /// + public string SortText { get; } - throw new KeyNotFoundException($"Property {name} not found"); - } + /// + /// Descriptive text to place after in the display layer. Should + /// be short as it will show up in the UI. Display will present this in a way to distinguish + /// this from the normal text (for example, by fading out and right-aligning). + /// + public string InlineDescription { get; } - /// - /// Descriptive tags from . - /// These tags may influence how the item is displayed. - /// - public ImmutableArray Tags { get; } - - /// - /// Rules that declare how this item should behave. - /// - public CompletionItemRules Rules { get; } - - /// - /// Returns true if this item's text edit requires complex resolution. - /// An edit is considered complex if the span of the change is different from - /// specified by . - /// - /// Example of an item type requiring complex resolution is C#/VB override completion. - /// - public bool IsComplexTextEdit { get; } - - /// - /// The name of the that created this - /// . Not available to clients. Only used by - /// the Completion subsystem itself for things like getting description text - /// and making additional change during commit. - /// - internal string? ProviderName { get; set; } - - /// - /// The automation text to use when narrating the completion item. If set to - /// null, narration will use the instead. - /// - internal string? AutomationText { get; set; } - - internal CompletionItemFlags Flags { get; set; } - - private CompletionItem( - string? displayText, - string? filterText, - string? sortText, - TextSpan span, - ImmutableArray> properties, - ImmutableArray tags, - CompletionItemRules? rules, - string? displayTextPrefix, - string? displayTextSuffix, - string? inlineDescription, - bool isComplexTextEdit) + /// + /// The span of the syntax element associated with this item. + /// + /// The span identifies the text in the document that is used to filter the initial list presented to the user, + /// and typically represents the region of the document that will be changed if this item is committed. + /// The latter is not always true because individual provider is free to make more complex changes to the document. + /// If this is the case, the provider should set to true. + /// + public TextSpan Span { get; internal set; } + + /// + /// Additional information attached to a completion item by it creator. + /// + public ImmutableDictionary Properties + { + get { - DisplayText = displayText ?? ""; - DisplayTextPrefix = displayTextPrefix ?? ""; - DisplayTextSuffix = displayTextSuffix ?? ""; - SortText = sortText ?? DisplayText; - InlineDescription = inlineDescription ?? ""; - Span = span; - Tags = tags.NullToEmpty(); - Rules = rules ?? CompletionItemRules.Default; - IsComplexTextEdit = isComplexTextEdit; - - _properties = properties.NullToEmpty(); - if (_properties.Length > 10) - { - // Prefer to just keep an ImmutableArray, but performance on large property collections - // (quite uncommon) dictate falling back to a non-linear lookup data structure. + if (_lazyPropertiesAsImmutableDictionary is null) _lazyPropertiesAsImmutableDictionary = _properties.ToImmutableDictionary(); - } - if (!DisplayText.Equals(filterText ?? "", StringComparison.Ordinal)) + return _lazyPropertiesAsImmutableDictionary; + } + } + + internal ImmutableArray> GetProperties() + { + return _properties; + } + + internal bool TryGetProperty(string name, [NotNullWhen(true)] out string? value) + { + if (_lazyPropertiesAsImmutableDictionary is not null) + return _lazyPropertiesAsImmutableDictionary.TryGetValue(name, out value); + + foreach ((var propName, var propValue) in _properties) + { + if (name == propName) { - _filterText = filterText; + value = propValue; + return true; } } - // binary back compat overload - public static CompletionItem Create( - string? displayText, - string? filterText, - string? sortText, - ImmutableDictionary? properties, - ImmutableArray tags, - CompletionItemRules? rules) + value = null; + return false; + } + + internal string GetProperty(string name) + { + if (TryGetProperty(name, out var value)) + return value; + + // Let ImmutableDictionary handle throwing + if (_lazyPropertiesAsImmutableDictionary is not null) + return _lazyPropertiesAsImmutableDictionary[name]; + + throw new KeyNotFoundException($"Property {name} not found"); + } + + /// + /// Descriptive tags from . + /// These tags may influence how the item is displayed. + /// + public ImmutableArray Tags { get; } + + /// + /// Rules that declare how this item should behave. + /// + public CompletionItemRules Rules { get; } + + /// + /// Returns true if this item's text edit requires complex resolution. + /// An edit is considered complex if the span of the change is different from + /// specified by . + /// + /// Example of an item type requiring complex resolution is C#/VB override completion. + /// + public bool IsComplexTextEdit { get; } + + /// + /// The name of the that created this + /// . Not available to clients. Only used by + /// the Completion subsystem itself for things like getting description text + /// and making additional change during commit. + /// + internal string? ProviderName { get; set; } + + /// + /// The automation text to use when narrating the completion item. If set to + /// null, narration will use the instead. + /// + internal string? AutomationText { get; set; } + + internal CompletionItemFlags Flags { get; set; } + + private CompletionItem( + string? displayText, + string? filterText, + string? sortText, + TextSpan span, + ImmutableArray> properties, + ImmutableArray tags, + CompletionItemRules? rules, + string? displayTextPrefix, + string? displayTextSuffix, + string? inlineDescription, + bool isComplexTextEdit) + { + DisplayText = displayText ?? ""; + DisplayTextPrefix = displayTextPrefix ?? ""; + DisplayTextSuffix = displayTextSuffix ?? ""; + SortText = sortText ?? DisplayText; + InlineDescription = inlineDescription ?? ""; + Span = span; + Tags = tags.NullToEmpty(); + Rules = rules ?? CompletionItemRules.Default; + IsComplexTextEdit = isComplexTextEdit; + + _properties = properties.NullToEmpty(); + if (_properties.Length > 10) { - return Create(displayText, filterText, sortText, properties, tags, rules, displayTextPrefix: null, displayTextSuffix: null); + // Prefer to just keep an ImmutableArray, but performance on large property collections + // (quite uncommon) dictate falling back to a non-linear lookup data structure. + _lazyPropertiesAsImmutableDictionary = _properties.ToImmutableDictionary(); } - // binary back compat overload - public static CompletionItem Create( - string? displayText, - string? filterText, - string? sortText, - ImmutableDictionary? properties, - ImmutableArray tags, - CompletionItemRules? rules, - string? displayTextPrefix, - string? displayTextSuffix) + if (!DisplayText.Equals(filterText ?? "", StringComparison.Ordinal)) { - return Create(displayText, filterText, sortText, properties, tags, rules, displayTextPrefix, displayTextSuffix, inlineDescription: null); + _filterText = filterText; } + } + + // binary back compat overload + public static CompletionItem Create( + string? displayText, + string? filterText, + string? sortText, + ImmutableDictionary? properties, + ImmutableArray tags, + CompletionItemRules? rules) + { + return Create(displayText, filterText, sortText, properties, tags, rules, displayTextPrefix: null, displayTextSuffix: null); + } + + // binary back compat overload + public static CompletionItem Create( + string? displayText, + string? filterText, + string? sortText, + ImmutableDictionary? properties, + ImmutableArray tags, + CompletionItemRules? rules, + string? displayTextPrefix, + string? displayTextSuffix) + { + return Create(displayText, filterText, sortText, properties, tags, rules, displayTextPrefix, displayTextSuffix, inlineDescription: null); + } - // binary back compat overload - public static CompletionItem Create( - string? displayText, - string? filterText, - string? sortText, - ImmutableDictionary? properties, - ImmutableArray tags, - CompletionItemRules? rules, - string? displayTextPrefix, - string? displayTextSuffix, - string? inlineDescription) + // binary back compat overload + public static CompletionItem Create( + string? displayText, + string? filterText, + string? sortText, + ImmutableDictionary? properties, + ImmutableArray tags, + CompletionItemRules? rules, + string? displayTextPrefix, + string? displayTextSuffix, + string? inlineDescription) + { + return Create( + displayText, filterText, sortText, properties, tags, rules, displayTextPrefix, + displayTextSuffix, inlineDescription, isComplexTextEdit: false); + } + + public static CompletionItem Create( + string? displayText, + string? filterText = null, + string? sortText = null, + ImmutableDictionary? properties = null, + ImmutableArray tags = default, + CompletionItemRules? rules = null, + string? displayTextPrefix = null, + string? displayTextSuffix = null, + string? inlineDescription = null, + bool isComplexTextEdit = false) + { + var result = CreateInternal( + displayText, filterText, sortText, properties.AsImmutableOrNull(), tags, rules, displayTextPrefix, + displayTextSuffix, inlineDescription, isComplexTextEdit); + + result._lazyPropertiesAsImmutableDictionary = properties; + + return result; + } + + internal static CompletionItem CreateInternal( + string? displayText, + string? filterText = null, + string? sortText = null, + ImmutableArray> properties = default, + ImmutableArray tags = default, + CompletionItemRules? rules = null, + string? displayTextPrefix = null, + string? displayTextSuffix = null, + string? inlineDescription = null, + bool isComplexTextEdit = false) + { + return new CompletionItem( + span: default, + displayText: displayText, + filterText: filterText, + sortText: sortText, + properties: properties, + tags: tags, + rules: rules, + displayTextPrefix: displayTextPrefix, + displayTextSuffix: displayTextSuffix, + inlineDescription: inlineDescription, + isComplexTextEdit: isComplexTextEdit); + } + + /// + /// Creates a new + /// + /// The text that is displayed to the user. + /// The text used to determine if the item matches the filter and is show in the list. + /// The text used to determine the order that the item appears in the list. + /// The span of the syntax element in the document associated with this item. + /// Additional information. + /// Descriptive tags that may influence how the item is displayed. + /// The rules that declare how this item should behave. + /// + [Obsolete("Use the Create overload that does not take a span", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static CompletionItem Create( + string displayText, + string filterText, + string sortText, + TextSpan span, + ImmutableDictionary? properties, + ImmutableArray tags, + CompletionItemRules rules) + { + var result = new CompletionItem( + span: span, + displayText: displayText, + filterText: filterText, + sortText: sortText, + properties: properties.AsImmutableOrNull(), + tags: tags, + rules: rules, + displayTextPrefix: null, + displayTextSuffix: null, + inlineDescription: null, + isComplexTextEdit: false); + + result._lazyPropertiesAsImmutableDictionary = properties; + + return result; + } + + private CompletionItem With( + Optional span = default, + Optional displayText = default, + Optional filterText = default, + Optional sortText = default, + Optional>> properties = default, + Optional> tags = default, + Optional rules = default, + Optional displayTextPrefix = default, + Optional displayTextSuffix = default, + Optional inlineDescription = default, + Optional isComplexTextEdit = default, + Optional> additionalFilterTexts = default) + { + var newSpan = span.HasValue ? span.Value : Span; + var newDisplayText = displayText.HasValue ? displayText.Value : DisplayText; + var newFilterText = filterText.HasValue ? filterText.Value : FilterText; + var newSortText = sortText.HasValue ? sortText.Value : SortText; + var newInlineDescription = inlineDescription.HasValue ? inlineDescription.Value : InlineDescription; + var newProperties = properties.HasValue ? properties.Value : _properties; + var newTags = tags.HasValue ? tags.Value : Tags; + var newRules = rules.HasValue ? rules.Value : Rules; + var newDisplayTextPrefix = displayTextPrefix.HasValue ? displayTextPrefix.Value : DisplayTextPrefix; + var newDisplayTextSuffix = displayTextSuffix.HasValue ? displayTextSuffix.Value : DisplayTextSuffix; + var newIsComplexTextEdit = isComplexTextEdit.HasValue ? isComplexTextEdit.Value : IsComplexTextEdit; + var newAdditionalFilterTexts = additionalFilterTexts.HasValue ? additionalFilterTexts.Value.NullToEmpty() : AdditionalFilterTexts; + + if (newSpan == Span && + newDisplayText == DisplayText && + newFilterText == FilterText && + newSortText == SortText && + newProperties == _properties && + newTags == Tags && + newRules == Rules && + newDisplayTextPrefix == DisplayTextPrefix && + newDisplayTextSuffix == DisplayTextSuffix && + newInlineDescription == InlineDescription && + newIsComplexTextEdit == IsComplexTextEdit && + newAdditionalFilterTexts == AdditionalFilterTexts) { - return Create( - displayText, filterText, sortText, properties, tags, rules, displayTextPrefix, - displayTextSuffix, inlineDescription, isComplexTextEdit: false); + return this; } - public static CompletionItem Create( - string? displayText, - string? filterText = null, - string? sortText = null, - ImmutableDictionary? properties = null, - ImmutableArray tags = default, - CompletionItemRules? rules = null, - string? displayTextPrefix = null, - string? displayTextSuffix = null, - string? inlineDescription = null, - bool isComplexTextEdit = false) + return new CompletionItem( + displayText: newDisplayText, + filterText: newFilterText, + span: newSpan, + sortText: newSortText, + properties: newProperties, + tags: newTags, + rules: newRules, + displayTextPrefix: newDisplayTextPrefix, + displayTextSuffix: newDisplayTextSuffix, + inlineDescription: newInlineDescription, + isComplexTextEdit: newIsComplexTextEdit) { - var result = CreateInternal( - displayText, filterText, sortText, properties.AsImmutableOrNull(), tags, rules, displayTextPrefix, - displayTextSuffix, inlineDescription, isComplexTextEdit); + AutomationText = AutomationText, + ProviderName = ProviderName, + Flags = Flags, + AdditionalFilterTexts = newAdditionalFilterTexts + }; + } - result._lazyPropertiesAsImmutableDictionary = properties; + /// + /// Creates a copy of this with the property changed. + /// + [Obsolete("Not used anymore. CompletionList.Span is used to control the span used for filtering.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public CompletionItem WithSpan(TextSpan span) + => this; - return result; - } + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItem WithDisplayText(string text) + => With(displayText: text); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItem WithDisplayTextPrefix(string displayTextPrefix) + => With(displayTextPrefix: displayTextPrefix); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItem WithDisplayTextSuffix(string displayTextSuffix) + => With(displayTextSuffix: displayTextSuffix); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItem WithFilterText(string text) + => With(filterText: text); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItem WithSortText(string text) + => With(sortText: text); - internal static CompletionItem CreateInternal( - string? displayText, - string? filterText = null, - string? sortText = null, - ImmutableArray> properties = default, - ImmutableArray tags = default, - CompletionItemRules? rules = null, - string? displayTextPrefix = null, - string? displayTextSuffix = null, - string? inlineDescription = null, - bool isComplexTextEdit = false) + /// + /// Creates a copy of this with the specified property changed. + /// + public CompletionItem WithProperties(ImmutableDictionary properties) + { + var result = With(properties: properties.AsImmutableOrNull()); + + result._lazyPropertiesAsImmutableDictionary = properties; + + return result; + } + + internal CompletionItem WithProperties(ImmutableArray> properties) + => With(properties: properties); + + /// + /// Creates a copy of this with the specified property. + /// + public CompletionItem AddProperty(string name, string value) + => With(properties: GetProperties().Add(new KeyValuePair(name, value))); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItem WithTags(ImmutableArray tags) + => With(tags: tags); + + /// + /// Creates a copy of this with a tag added to the collection. + /// + public CompletionItem AddTag(string tag) + { + if (tag == null) { - return new CompletionItem( - span: default, - displayText: displayText, - filterText: filterText, - sortText: sortText, - properties: properties, - tags: tags, - rules: rules, - displayTextPrefix: displayTextPrefix, - displayTextSuffix: displayTextSuffix, - inlineDescription: inlineDescription, - isComplexTextEdit: isComplexTextEdit); + throw new ArgumentNullException(nameof(tag)); } - /// - /// Creates a new - /// - /// The text that is displayed to the user. - /// The text used to determine if the item matches the filter and is show in the list. - /// The text used to determine the order that the item appears in the list. - /// The span of the syntax element in the document associated with this item. - /// Additional information. - /// Descriptive tags that may influence how the item is displayed. - /// The rules that declare how this item should behave. - /// - [Obsolete("Use the Create overload that does not take a span", error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] - public static CompletionItem Create( - string displayText, - string filterText, - string sortText, - TextSpan span, - ImmutableDictionary? properties, - ImmutableArray tags, - CompletionItemRules rules) + if (Tags.Contains(tag)) { - var result = new CompletionItem( - span: span, - displayText: displayText, - filterText: filterText, - sortText: sortText, - properties: properties.AsImmutableOrNull(), - tags: tags, - rules: rules, - displayTextPrefix: null, - displayTextSuffix: null, - inlineDescription: null, - isComplexTextEdit: false); - - result._lazyPropertiesAsImmutableDictionary = properties; - - return result; + return this; } - - private CompletionItem With( - Optional span = default, - Optional displayText = default, - Optional filterText = default, - Optional sortText = default, - Optional>> properties = default, - Optional> tags = default, - Optional rules = default, - Optional displayTextPrefix = default, - Optional displayTextSuffix = default, - Optional inlineDescription = default, - Optional isComplexTextEdit = default, - Optional> additionalFilterTexts = default) + else { - var newSpan = span.HasValue ? span.Value : Span; - var newDisplayText = displayText.HasValue ? displayText.Value : DisplayText; - var newFilterText = filterText.HasValue ? filterText.Value : FilterText; - var newSortText = sortText.HasValue ? sortText.Value : SortText; - var newInlineDescription = inlineDescription.HasValue ? inlineDescription.Value : InlineDescription; - var newProperties = properties.HasValue ? properties.Value : _properties; - var newTags = tags.HasValue ? tags.Value : Tags; - var newRules = rules.HasValue ? rules.Value : Rules; - var newDisplayTextPrefix = displayTextPrefix.HasValue ? displayTextPrefix.Value : DisplayTextPrefix; - var newDisplayTextSuffix = displayTextSuffix.HasValue ? displayTextSuffix.Value : DisplayTextSuffix; - var newIsComplexTextEdit = isComplexTextEdit.HasValue ? isComplexTextEdit.Value : IsComplexTextEdit; - var newAdditionalFilterTexts = additionalFilterTexts.HasValue ? additionalFilterTexts.Value.NullToEmpty() : AdditionalFilterTexts; - - if (newSpan == Span && - newDisplayText == DisplayText && - newFilterText == FilterText && - newSortText == SortText && - newProperties == _properties && - newTags == Tags && - newRules == Rules && - newDisplayTextPrefix == DisplayTextPrefix && - newDisplayTextSuffix == DisplayTextSuffix && - newInlineDescription == InlineDescription && - newIsComplexTextEdit == IsComplexTextEdit && - newAdditionalFilterTexts == AdditionalFilterTexts) - { - return this; - } - - return new CompletionItem( - displayText: newDisplayText, - filterText: newFilterText, - span: newSpan, - sortText: newSortText, - properties: newProperties, - tags: newTags, - rules: newRules, - displayTextPrefix: newDisplayTextPrefix, - displayTextSuffix: newDisplayTextSuffix, - inlineDescription: newInlineDescription, - isComplexTextEdit: newIsComplexTextEdit) - { - AutomationText = AutomationText, - ProviderName = ProviderName, - Flags = Flags, - AdditionalFilterTexts = newAdditionalFilterTexts - }; + return With(tags: Tags.Add(tag)); } + } - /// - /// Creates a copy of this with the property changed. - /// - [Obsolete("Not used anymore. CompletionList.Span is used to control the span used for filtering.", error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] - public CompletionItem WithSpan(TextSpan span) - => this; - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItem WithDisplayText(string text) - => With(displayText: text); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItem WithDisplayTextPrefix(string displayTextPrefix) - => With(displayTextPrefix: displayTextPrefix); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItem WithDisplayTextSuffix(string displayTextSuffix) - => With(displayTextSuffix: displayTextSuffix); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItem WithFilterText(string text) - => With(filterText: text); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItem WithSortText(string text) - => With(sortText: text); - - /// - /// Creates a copy of this with the specified property changed. - /// - public CompletionItem WithProperties(ImmutableDictionary properties) - { - var result = With(properties: properties.AsImmutableOrNull()); + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItem WithRules(CompletionItemRules rules) + => With(rules: rules); - result._lazyPropertiesAsImmutableDictionary = properties; + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItem WithIsComplexTextEdit(bool isComplexTextEdit) + => With(isComplexTextEdit: isComplexTextEdit); - return result; - } + /// + /// Creates a copy of this with the property changed. + /// + internal CompletionItem WithAdditionalFilterTexts(ImmutableArray additionalFilterTexts) + => With(additionalFilterTexts: additionalFilterTexts); - internal CompletionItem WithProperties(ImmutableArray> properties) - => With(properties: properties); - - /// - /// Creates a copy of this with the specified property. - /// - public CompletionItem AddProperty(string name, string value) - => With(properties: GetProperties().Add(new KeyValuePair(name, value))); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItem WithTags(ImmutableArray tags) - => With(tags: tags); - - /// - /// Creates a copy of this with a tag added to the collection. - /// - public CompletionItem AddTag(string tag) + int IComparable.CompareTo([AllowNull] CompletionItem other) + { + if (other == null) { - if (tag == null) - { - throw new ArgumentNullException(nameof(tag)); - } - - if (Tags.Contains(tag)) - { - return this; - } - else - { - return With(tags: Tags.Add(tag)); - } + return 1; } - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItem WithRules(CompletionItemRules rules) - => With(rules: rules); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItem WithIsComplexTextEdit(bool isComplexTextEdit) - => With(isComplexTextEdit: isComplexTextEdit); - - /// - /// Creates a copy of this with the property changed. - /// - internal CompletionItem WithAdditionalFilterTexts(ImmutableArray additionalFilterTexts) - => With(additionalFilterTexts: additionalFilterTexts); - - int IComparable.CompareTo([AllowNull] CompletionItem other) + // Make sure expanded items are listed after non-expanded ones. + // Note that in our ItemManager at async-completion layer, we implicitly + // rely on this behavior when combining delayed expanded items list with + // non-expanded items (to avoid sorting again). if this changed, we need + // to make sure to sort items properly in ItemManager. + var thisIsExpandItem = Flags.IsExpanded(); + var otherIsExpandItem = other.Flags.IsExpanded(); + + if (thisIsExpandItem == otherIsExpandItem) { - if (other == null) + var result = StringComparer.OrdinalIgnoreCase.Compare(SortText, other.SortText); + if (result == 0) { - return 1; + result = StringComparer.OrdinalIgnoreCase.Compare(GetEntireDisplayText(), other.GetEntireDisplayText()); } - // Make sure expanded items are listed after non-expanded ones. - // Note that in our ItemManager at async-completion layer, we implicitly - // rely on this behavior when combining delayed expanded items list with - // non-expanded items (to avoid sorting again). if this changed, we need - // to make sure to sort items properly in ItemManager. - var thisIsExpandItem = Flags.IsExpanded(); - var otherIsExpandItem = other.Flags.IsExpanded(); - - if (thisIsExpandItem == otherIsExpandItem) - { - var result = StringComparer.OrdinalIgnoreCase.Compare(SortText, other.SortText); - if (result == 0) - { - result = StringComparer.OrdinalIgnoreCase.Compare(GetEntireDisplayText(), other.GetEntireDisplayText()); - } - - return result; - } - else if (thisIsExpandItem) - { - return 1; - } - else - { - return -1; - } + return result; + } + else if (thisIsExpandItem) + { + return 1; + } + else + { + return -1; } + } - internal string GetEntireDisplayText() - => _lazyEntireDisplayText ??= DisplayTextPrefix + DisplayText + DisplayTextSuffix; + internal string GetEntireDisplayText() + => _lazyEntireDisplayText ??= DisplayTextPrefix + DisplayText + DisplayTextSuffix; - public override string ToString() => GetEntireDisplayText(); - } + public override string ToString() => GetEntireDisplayText(); } diff --git a/src/Features/Core/Portable/Completion/CompletionItemFlags.cs b/src/Features/Core/Portable/Completion/CompletionItemFlags.cs index 4aff82533f5b7..8468ecaa518f5 100644 --- a/src/Features/Core/Portable/Completion/CompletionItemFlags.cs +++ b/src/Features/Core/Portable/Completion/CompletionItemFlags.cs @@ -4,37 +4,36 @@ using System; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +[Flags] +internal enum CompletionItemFlags +{ + None = 0x0, + + /// + /// Indicates this is cached and reused across completion sessions. + /// This might be used by completion system for things like deciding whether it can safely cache and reuse + /// other data corresponding to this item. + /// + /// TODO: Revisit the approach we used for caching VS items. + /// https://github.com/dotnet/roslyn/issues/35160 + /// + Cached = 0x1, + + /// + /// Indicates this should be shown only when expanded items is requested. + /// + Expanded = 0x2, + + CachedAndExpanded = Cached | Expanded, +} + +internal static class CompletionItemFlagsExtensions { - [Flags] - internal enum CompletionItemFlags - { - None = 0x0, - - /// - /// Indicates this is cached and reused across completion sessions. - /// This might be used by completion system for things like deciding whether it can safely cache and reuse - /// other data corresponding to this item. - /// - /// TODO: Revisit the approach we used for caching VS items. - /// https://github.com/dotnet/roslyn/issues/35160 - /// - Cached = 0x1, - - /// - /// Indicates this should be shown only when expanded items is requested. - /// - Expanded = 0x2, - - CachedAndExpanded = Cached | Expanded, - } - - internal static class CompletionItemFlagsExtensions - { - public static bool IsCached(this CompletionItemFlags flags) - => (flags & CompletionItemFlags.Cached) != 0; - - public static bool IsExpanded(this CompletionItemFlags flags) - => (flags & CompletionItemFlags.Expanded) != 0; - } + public static bool IsCached(this CompletionItemFlags flags) + => (flags & CompletionItemFlags.Cached) != 0; + + public static bool IsExpanded(this CompletionItemFlags flags) + => (flags & CompletionItemFlags.Expanded) != 0; } diff --git a/src/Features/Core/Portable/Completion/CompletionItemRules.cs b/src/Features/Core/Portable/Completion/CompletionItemRules.cs index 108a4e617c034..4bbe80dec65e2 100644 --- a/src/Features/Core/Portable/Completion/CompletionItemRules.cs +++ b/src/Features/Core/Portable/Completion/CompletionItemRules.cs @@ -4,244 +4,243 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +public enum CompletionItemSelectionBehavior { - public enum CompletionItemSelectionBehavior + Default, + + /// + /// If no text has been typed, the item should be soft selected. This is appropriate for + /// completion providers that want to provide suggestions that shouldn't interfere with + /// typing. For example a provider that comes up on space might offer items that are soft + /// selected so that an additional space (or other puntuation character) will not then + /// commit that item. + /// + SoftSelection, + + /// + /// If no text has been typed, the item should be hard selected. This is appropriate for + /// completion providers that are providing suggestions the user is nearly certain to + /// select. Because the item is hard selected, any commit characters typed after it will + /// cause it to be committed. + /// + HardSelection, +} + +/// +/// Rules for how the individual items are handled. +/// +public sealed class CompletionItemRules +{ + /// + /// The rule used when no rule is specified when constructing a . + /// + public static CompletionItemRules Default = + new( + filterCharacterRules: default, + commitCharacterRules: default, + enterKeyRule: EnterKeyRule.Default, + formatOnCommit: false, + matchPriority: Completion.MatchPriority.Default, + selectionBehavior: CompletionItemSelectionBehavior.Default); + + /// + /// Rules that modify the set of characters that can be typed to filter the list of completion items. + /// + public ImmutableArray FilterCharacterRules { get; } + + /// + /// Rules that modify the set of characters that can be typed to cause the selected item to be committed. + /// + public ImmutableArray CommitCharacterRules { get; } + + /// + /// A rule about whether the enter key is passed through to the editor after the selected item has been committed. + /// + public EnterKeyRule EnterKeyRule { get; } + + /// + /// True if the modified text should be formatted automatically. + /// + public bool FormatOnCommit { get; } + + /// + /// True if the related completion item should be initially selected. + /// + public int MatchPriority { get; } + + /// + /// How this item should be selected when the completion list first appears and + /// before the user has typed any characters. + /// + public CompletionItemSelectionBehavior SelectionBehavior { get; } + + private CompletionItemRules( + ImmutableArray filterCharacterRules, + ImmutableArray commitCharacterRules, + EnterKeyRule enterKeyRule, + bool formatOnCommit, + int matchPriority, + CompletionItemSelectionBehavior selectionBehavior) { - Default, - - /// - /// If no text has been typed, the item should be soft selected. This is appropriate for - /// completion providers that want to provide suggestions that shouldn't interfere with - /// typing. For example a provider that comes up on space might offer items that are soft - /// selected so that an additional space (or other puntuation character) will not then - /// commit that item. - /// - SoftSelection, - - /// - /// If no text has been typed, the item should be hard selected. This is appropriate for - /// completion providers that are providing suggestions the user is nearly certain to - /// select. Because the item is hard selected, any commit characters typed after it will - /// cause it to be committed. - /// - HardSelection, + FilterCharacterRules = filterCharacterRules.NullToEmpty(); + CommitCharacterRules = commitCharacterRules.NullToEmpty(); + EnterKeyRule = enterKeyRule; + FormatOnCommit = formatOnCommit; + MatchPriority = matchPriority; + SelectionBehavior = selectionBehavior; } /// - /// Rules for how the individual items are handled. + /// Creates a new instance. /// - public sealed class CompletionItemRules + /// Rules about which keys typed are used to filter the list of completion items. + /// Rules about which keys typed caused the completion item to be committed. + /// Rule about whether the enter key is passed through to the editor after the selected item has been committed. + /// True if the modified text should be formatted automatically. + /// True if the related completion item should be initially selected. + /// + public static CompletionItemRules Create( + ImmutableArray filterCharacterRules, + ImmutableArray commitCharacterRules, + EnterKeyRule enterKeyRule, + bool formatOnCommit, + int? matchPriority) { - /// - /// The rule used when no rule is specified when constructing a . - /// - public static CompletionItemRules Default = - new( - filterCharacterRules: default, - commitCharacterRules: default, - enterKeyRule: EnterKeyRule.Default, - formatOnCommit: false, - matchPriority: Completion.MatchPriority.Default, - selectionBehavior: CompletionItemSelectionBehavior.Default); - - /// - /// Rules that modify the set of characters that can be typed to filter the list of completion items. - /// - public ImmutableArray FilterCharacterRules { get; } - - /// - /// Rules that modify the set of characters that can be typed to cause the selected item to be committed. - /// - public ImmutableArray CommitCharacterRules { get; } - - /// - /// A rule about whether the enter key is passed through to the editor after the selected item has been committed. - /// - public EnterKeyRule EnterKeyRule { get; } - - /// - /// True if the modified text should be formatted automatically. - /// - public bool FormatOnCommit { get; } - - /// - /// True if the related completion item should be initially selected. - /// - public int MatchPriority { get; } - - /// - /// How this item should be selected when the completion list first appears and - /// before the user has typed any characters. - /// - public CompletionItemSelectionBehavior SelectionBehavior { get; } - - private CompletionItemRules( - ImmutableArray filterCharacterRules, - ImmutableArray commitCharacterRules, - EnterKeyRule enterKeyRule, - bool formatOnCommit, - int matchPriority, - CompletionItemSelectionBehavior selectionBehavior) - { - FilterCharacterRules = filterCharacterRules.NullToEmpty(); - CommitCharacterRules = commitCharacterRules.NullToEmpty(); - EnterKeyRule = enterKeyRule; - FormatOnCommit = formatOnCommit; - MatchPriority = matchPriority; - SelectionBehavior = selectionBehavior; - } + return Create( + filterCharacterRules, commitCharacterRules, + enterKeyRule, formatOnCommit, matchPriority, + selectionBehavior: CompletionItemSelectionBehavior.Default); + } - /// - /// Creates a new instance. - /// - /// Rules about which keys typed are used to filter the list of completion items. - /// Rules about which keys typed caused the completion item to be committed. - /// Rule about whether the enter key is passed through to the editor after the selected item has been committed. - /// True if the modified text should be formatted automatically. - /// True if the related completion item should be initially selected. - /// - public static CompletionItemRules Create( - ImmutableArray filterCharacterRules, - ImmutableArray commitCharacterRules, - EnterKeyRule enterKeyRule, - bool formatOnCommit, - int? matchPriority) + /// + /// Creates a new instance. + /// + /// Rules about which keys typed are used to filter the list of completion items. + /// Rules about which keys typed caused the completion item to be committed. + /// Rule about whether the enter key is passed through to the editor after the selected item has been committed. + /// True if the modified text should be formatted automatically. + /// True if the related completion item should be initially selected. + /// How this item should be selected if no text has been typed after the completion list is brought up. + /// + public static CompletionItemRules Create( + ImmutableArray filterCharacterRules = default, + ImmutableArray commitCharacterRules = default, + EnterKeyRule enterKeyRule = EnterKeyRule.Default, + bool formatOnCommit = false, + int? matchPriority = null, + CompletionItemSelectionBehavior selectionBehavior = CompletionItemSelectionBehavior.Default) + { + if (filterCharacterRules.IsDefaultOrEmpty && + commitCharacterRules.IsDefaultOrEmpty && + enterKeyRule == Default.EnterKeyRule && + formatOnCommit == Default.FormatOnCommit && + matchPriority.GetValueOrDefault() == Default.MatchPriority && + selectionBehavior == Default.SelectionBehavior) { - return Create( - filterCharacterRules, commitCharacterRules, - enterKeyRule, formatOnCommit, matchPriority, - selectionBehavior: CompletionItemSelectionBehavior.Default); + return Default; } - - /// - /// Creates a new instance. - /// - /// Rules about which keys typed are used to filter the list of completion items. - /// Rules about which keys typed caused the completion item to be committed. - /// Rule about whether the enter key is passed through to the editor after the selected item has been committed. - /// True if the modified text should be formatted automatically. - /// True if the related completion item should be initially selected. - /// How this item should be selected if no text has been typed after the completion list is brought up. - /// - public static CompletionItemRules Create( - ImmutableArray filterCharacterRules = default, - ImmutableArray commitCharacterRules = default, - EnterKeyRule enterKeyRule = EnterKeyRule.Default, - bool formatOnCommit = false, - int? matchPriority = null, - CompletionItemSelectionBehavior selectionBehavior = CompletionItemSelectionBehavior.Default) + else { - if (filterCharacterRules.IsDefaultOrEmpty && - commitCharacterRules.IsDefaultOrEmpty && - enterKeyRule == Default.EnterKeyRule && - formatOnCommit == Default.FormatOnCommit && - matchPriority.GetValueOrDefault() == Default.MatchPriority && - selectionBehavior == Default.SelectionBehavior) - { - return Default; - } - else - { - return new CompletionItemRules( - filterCharacterRules, commitCharacterRules, enterKeyRule, formatOnCommit, - matchPriority.GetValueOrDefault(), selectionBehavior); - } + return new CompletionItemRules( + filterCharacterRules, commitCharacterRules, enterKeyRule, formatOnCommit, + matchPriority.GetValueOrDefault(), selectionBehavior); } + } - /// - /// Creates a new instance--internal for TypeScript. - /// - /// Rules about which keys typed are used to filter the list of completion items. - /// Rules about which keys typed caused the completion item to be committed. - /// Rule about whether the enter key is passed through to the editor after the selected item has been committed. - /// True if the modified text should be formatted automatically. - /// True if the related completion item should be initially selected. - /// - internal static CompletionItemRules Create( - ImmutableArray filterCharacterRules, - ImmutableArray commitCharacterRules, - EnterKeyRule enterKeyRule, - bool formatOnCommit, - bool preselect) + /// + /// Creates a new instance--internal for TypeScript. + /// + /// Rules about which keys typed are used to filter the list of completion items. + /// Rules about which keys typed caused the completion item to be committed. + /// Rule about whether the enter key is passed through to the editor after the selected item has been committed. + /// True if the modified text should be formatted automatically. + /// True if the related completion item should be initially selected. + /// + internal static CompletionItemRules Create( + ImmutableArray filterCharacterRules, + ImmutableArray commitCharacterRules, + EnterKeyRule enterKeyRule, + bool formatOnCommit, + bool preselect) + { + var matchPriority = preselect ? Completion.MatchPriority.Preselect : Completion.MatchPriority.Default; + return CompletionItemRules.Create(filterCharacterRules, commitCharacterRules, enterKeyRule, formatOnCommit, matchPriority); + } + + private CompletionItemRules With( + Optional> filterRules = default, + Optional> commitRules = default, + Optional enterKeyRule = default, + Optional formatOnCommit = default, + Optional matchPriority = default, + Optional selectionBehavior = default) + { + var newFilterRules = filterRules.HasValue ? filterRules.Value : FilterCharacterRules; + var newCommitRules = commitRules.HasValue ? commitRules.Value : CommitCharacterRules; + var newEnterKeyRule = enterKeyRule.HasValue ? enterKeyRule.Value : EnterKeyRule; + var newFormatOnCommit = formatOnCommit.HasValue ? formatOnCommit.Value : FormatOnCommit; + var newMatchPriority = matchPriority.HasValue ? matchPriority.Value : MatchPriority; + var newSelectionBehavior = selectionBehavior.HasValue ? selectionBehavior.Value : SelectionBehavior; + + if (newFilterRules == FilterCharacterRules && + newCommitRules == CommitCharacterRules && + newEnterKeyRule == EnterKeyRule && + newFormatOnCommit == FormatOnCommit && + newMatchPriority == MatchPriority && + newSelectionBehavior == SelectionBehavior) { - var matchPriority = preselect ? Completion.MatchPriority.Preselect : Completion.MatchPriority.Default; - return CompletionItemRules.Create(filterCharacterRules, commitCharacterRules, enterKeyRule, formatOnCommit, matchPriority); + return this; } - - private CompletionItemRules With( - Optional> filterRules = default, - Optional> commitRules = default, - Optional enterKeyRule = default, - Optional formatOnCommit = default, - Optional matchPriority = default, - Optional selectionBehavior = default) + else { - var newFilterRules = filterRules.HasValue ? filterRules.Value : FilterCharacterRules; - var newCommitRules = commitRules.HasValue ? commitRules.Value : CommitCharacterRules; - var newEnterKeyRule = enterKeyRule.HasValue ? enterKeyRule.Value : EnterKeyRule; - var newFormatOnCommit = formatOnCommit.HasValue ? formatOnCommit.Value : FormatOnCommit; - var newMatchPriority = matchPriority.HasValue ? matchPriority.Value : MatchPriority; - var newSelectionBehavior = selectionBehavior.HasValue ? selectionBehavior.Value : SelectionBehavior; - - if (newFilterRules == FilterCharacterRules && - newCommitRules == CommitCharacterRules && - newEnterKeyRule == EnterKeyRule && - newFormatOnCommit == FormatOnCommit && - newMatchPriority == MatchPriority && - newSelectionBehavior == SelectionBehavior) - { - return this; - } - else - { - return Create( - newFilterRules, newCommitRules, - newEnterKeyRule, newFormatOnCommit, - newMatchPriority, newSelectionBehavior); - } + return Create( + newFilterRules, newCommitRules, + newEnterKeyRule, newFormatOnCommit, + newMatchPriority, newSelectionBehavior); } - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItemRules WithFilterCharacterRules(ImmutableArray filterCharacterRules) - => With(filterRules: filterCharacterRules); - - internal CompletionItemRules WithFilterCharacterRule(CharacterSetModificationRule rule) - => With(filterRules: ImmutableArray.Create(rule)); - - internal CompletionItemRules WithCommitCharacterRule(CharacterSetModificationRule rule) - => With(commitRules: ImmutableArray.Create(rule)); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItemRules WithCommitCharacterRules(ImmutableArray commitCharacterRules) - => With(commitRules: commitCharacterRules); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItemRules WithEnterKeyRule(EnterKeyRule enterKeyRule) - => With(enterKeyRule: enterKeyRule); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItemRules WithFormatOnCommit(bool formatOnCommit) - => With(formatOnCommit: formatOnCommit); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItemRules WithMatchPriority(int matchPriority) - => With(matchPriority: matchPriority); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionItemRules WithSelectionBehavior(CompletionItemSelectionBehavior selectionBehavior) - => With(selectionBehavior: selectionBehavior); } + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItemRules WithFilterCharacterRules(ImmutableArray filterCharacterRules) + => With(filterRules: filterCharacterRules); + + internal CompletionItemRules WithFilterCharacterRule(CharacterSetModificationRule rule) + => With(filterRules: ImmutableArray.Create(rule)); + + internal CompletionItemRules WithCommitCharacterRule(CharacterSetModificationRule rule) + => With(commitRules: ImmutableArray.Create(rule)); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItemRules WithCommitCharacterRules(ImmutableArray commitCharacterRules) + => With(commitRules: commitCharacterRules); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItemRules WithEnterKeyRule(EnterKeyRule enterKeyRule) + => With(enterKeyRule: enterKeyRule); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItemRules WithFormatOnCommit(bool formatOnCommit) + => With(formatOnCommit: formatOnCommit); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItemRules WithMatchPriority(int matchPriority) + => With(matchPriority: matchPriority); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionItemRules WithSelectionBehavior(CompletionItemSelectionBehavior selectionBehavior) + => With(selectionBehavior: selectionBehavior); } diff --git a/src/Features/Core/Portable/Completion/CompletionList.cs b/src/Features/Core/Portable/Completion/CompletionList.cs index f256bd3501697..d1f98f7c7c3e5 100644 --- a/src/Features/Core/Portable/Completion/CompletionList.cs +++ b/src/Features/Core/Portable/Completion/CompletionList.cs @@ -9,183 +9,182 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// The set of completions to present to the user. +/// +public sealed class CompletionList { + private readonly Lazy> _lazyItems; + + /// + /// The completion items to present to the user. + /// + [Obsolete($"This property is obsolete. Use {nameof(ItemsList)} instead", error: false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public ImmutableArray Items => _lazyItems.Value; + + /// + /// The completion items to present to the user. + /// This property is preferred over `Items` because of the flexibility it provides. + /// For example, the list can be backed by types like SegmentedList to avoid LOH allocations. + /// + public IReadOnlyList ItemsList { get; } + + /// + /// The span of the syntax element at the caret position when the was created. + /// Individual spans may vary. + /// + [Obsolete("Not used anymore. CompletionList.Span is used instead.", error: true)] + public TextSpan DefaultSpan { get; } + + /// + /// The span of the syntax element at the caret position when the + /// was created. + /// + /// The span identifies the text in the document that is used to filter the initial list + /// presented to the user, and typically represents the region of the document that will + /// be changed if this item is committed. + /// The latter is not always the case because each provider is free to make more complex changes + /// to the document. If this is the case, must be + /// set to . + /// + public TextSpan Span { get; } + /// - /// The set of completions to present to the user. + /// The rules used to control behavior of the completion list shown to the user during typing. /// - public sealed class CompletionList + public CompletionRules Rules { get; } + + /// + /// An optional that appears selected in the list presented to the user during suggestion mode. + /// Suggestion mode disables auto-selection of items in the list, giving preference to the text typed by the user unless a specific item is selected manually. + /// Specifying a is a request that the completion host operate in suggestion mode. + /// The item specified determines the text displayed and the description associated with it unless a different item is manually selected. + /// No text is ever inserted when this item is completed, leaving the text the user typed instead. + /// + public CompletionItem? SuggestionModeItem { get; } + + /// + /// Whether the items in this list should be the only items presented to the user. + /// + internal bool IsExclusive { get; } + + private CompletionList( + TextSpan defaultSpan, + IReadOnlyList itemsList, + CompletionRules? rules, + CompletionItem? suggestionModeItem, + bool isExclusive) { - private readonly Lazy> _lazyItems; - - /// - /// The completion items to present to the user. - /// - [Obsolete($"This property is obsolete. Use {nameof(ItemsList)} instead", error: false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public ImmutableArray Items => _lazyItems.Value; - - /// - /// The completion items to present to the user. - /// This property is preferred over `Items` because of the flexibility it provides. - /// For example, the list can be backed by types like SegmentedList to avoid LOH allocations. - /// - public IReadOnlyList ItemsList { get; } - - /// - /// The span of the syntax element at the caret position when the was created. - /// Individual spans may vary. - /// - [Obsolete("Not used anymore. CompletionList.Span is used instead.", error: true)] - public TextSpan DefaultSpan { get; } - - /// - /// The span of the syntax element at the caret position when the - /// was created. - /// - /// The span identifies the text in the document that is used to filter the initial list - /// presented to the user, and typically represents the region of the document that will - /// be changed if this item is committed. - /// The latter is not always the case because each provider is free to make more complex changes - /// to the document. If this is the case, must be - /// set to . - /// - public TextSpan Span { get; } - - /// - /// The rules used to control behavior of the completion list shown to the user during typing. - /// - public CompletionRules Rules { get; } - - /// - /// An optional that appears selected in the list presented to the user during suggestion mode. - /// Suggestion mode disables auto-selection of items in the list, giving preference to the text typed by the user unless a specific item is selected manually. - /// Specifying a is a request that the completion host operate in suggestion mode. - /// The item specified determines the text displayed and the description associated with it unless a different item is manually selected. - /// No text is ever inserted when this item is completed, leaving the text the user typed instead. - /// - public CompletionItem? SuggestionModeItem { get; } - - /// - /// Whether the items in this list should be the only items presented to the user. - /// - internal bool IsExclusive { get; } - - private CompletionList( - TextSpan defaultSpan, - IReadOnlyList itemsList, - CompletionRules? rules, - CompletionItem? suggestionModeItem, - bool isExclusive) - { - Span = defaultSpan; - ItemsList = itemsList; - _lazyItems = new(() => ItemsList.ToImmutableArrayOrEmpty(), System.Threading.LazyThreadSafetyMode.PublicationOnly); - - Rules = rules ?? CompletionRules.Default; - SuggestionModeItem = suggestionModeItem; - IsExclusive = isExclusive; - - foreach (var item in ItemsList) - { - item.Span = defaultSpan; - } - } + Span = defaultSpan; + ItemsList = itemsList; + _lazyItems = new(() => ItemsList.ToImmutableArrayOrEmpty(), System.Threading.LazyThreadSafetyMode.PublicationOnly); + + Rules = rules ?? CompletionRules.Default; + SuggestionModeItem = suggestionModeItem; + IsExclusive = isExclusive; - /// - /// Creates a new instance. - /// - /// The span of the syntax element at the caret position when the was created. - /// The completion items to present to the user. - /// The rules used to control behavior of the completion list shown to the user during typing. - /// An optional that appears selected in the list presented to the user during suggestion mode. - /// - public static CompletionList Create( - TextSpan defaultSpan, - ImmutableArray items, - CompletionRules? rules = null, - CompletionItem? suggestionModeItem = null) + foreach (var item in ItemsList) { - return Create(defaultSpan, items, rules, suggestionModeItem, isExclusive: false); + item.Span = defaultSpan; } + } - internal static CompletionList Create( - TextSpan defaultSpan, - IReadOnlyList itemsList, - CompletionRules? rules, - CompletionItem? suggestionModeItem, - bool isExclusive) + /// + /// Creates a new instance. + /// + /// The span of the syntax element at the caret position when the was created. + /// The completion items to present to the user. + /// The rules used to control behavior of the completion list shown to the user during typing. + /// An optional that appears selected in the list presented to the user during suggestion mode. + /// + public static CompletionList Create( + TextSpan defaultSpan, + ImmutableArray items, + CompletionRules? rules = null, + CompletionItem? suggestionModeItem = null) + { + return Create(defaultSpan, items, rules, suggestionModeItem, isExclusive: false); + } + + internal static CompletionList Create( + TextSpan defaultSpan, + IReadOnlyList itemsList, + CompletionRules? rules, + CompletionItem? suggestionModeItem, + bool isExclusive) + { + return new CompletionList(defaultSpan, itemsList, rules, suggestionModeItem, isExclusive); + } + + private CompletionList With( + Optional span = default, + Optional> itemsList = default, + Optional rules = default, + Optional suggestionModeItem = default) + { + var newSpan = span.HasValue ? span.Value : Span; + var newItemsList = itemsList.HasValue ? itemsList.Value : ItemsList; + var newRules = rules.HasValue ? rules.Value : Rules; + var newSuggestionModeItem = suggestionModeItem.HasValue ? suggestionModeItem.Value : SuggestionModeItem; + + if (newSpan == Span && + newItemsList == ItemsList && + newRules == Rules && + newSuggestionModeItem == SuggestionModeItem) { - return new CompletionList(defaultSpan, itemsList, rules, suggestionModeItem, isExclusive); + return this; } - - private CompletionList With( - Optional span = default, - Optional> itemsList = default, - Optional rules = default, - Optional suggestionModeItem = default) + else { - var newSpan = span.HasValue ? span.Value : Span; - var newItemsList = itemsList.HasValue ? itemsList.Value : ItemsList; - var newRules = rules.HasValue ? rules.Value : Rules; - var newSuggestionModeItem = suggestionModeItem.HasValue ? suggestionModeItem.Value : SuggestionModeItem; - - if (newSpan == Span && - newItemsList == ItemsList && - newRules == Rules && - newSuggestionModeItem == SuggestionModeItem) - { - return this; - } - else - { - return Create(newSpan, newItemsList, newRules, newSuggestionModeItem, IsExclusive); - } + return Create(newSpan, newItemsList, newRules, newSuggestionModeItem, IsExclusive); } + } - /// - /// Creates a copy of this with the property changed. - /// - [Obsolete("Not used anymore. Use WithSpan instead.", error: true)] - public CompletionList WithDefaultSpan(TextSpan span) - => With(span: span); + /// + /// Creates a copy of this with the property changed. + /// + [Obsolete("Not used anymore. Use WithSpan instead.", error: true)] + public CompletionList WithDefaultSpan(TextSpan span) + => With(span: span); - public CompletionList WithSpan(TextSpan span) - => With(span: span); + public CompletionList WithSpan(TextSpan span) + => With(span: span); #pragma warning disable RS0030 // Do not used banned APIs - /// - /// Creates a copy of this with the property changed. - /// - public CompletionList WithItems(ImmutableArray items) + /// + /// Creates a copy of this with the property changed. + /// + public CompletionList WithItems(ImmutableArray items) #pragma warning restore RS0030 // Do not used banned APIs - => With(itemsList: items); - - /// - /// Creates a copy of this with the property changed. - /// - internal CompletionList WithItemsList(IReadOnlyList itemsList) - => With(itemsList: new(itemsList)); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionList WithRules(CompletionRules rules) - => With(rules: rules); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionList WithSuggestionModeItem(CompletionItem suggestionModeItem) - => With(suggestionModeItem: suggestionModeItem); - - /// - /// The default returned when no items are found to populate the list. - /// - public static readonly CompletionList Empty = new( - default, [], CompletionRules.Default, - suggestionModeItem: null, isExclusive: false); - - internal bool IsEmpty => ItemsList.Count == 0 && SuggestionModeItem is null; - } + => With(itemsList: items); + + /// + /// Creates a copy of this with the property changed. + /// + internal CompletionList WithItemsList(IReadOnlyList itemsList) + => With(itemsList: new(itemsList)); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionList WithRules(CompletionRules rules) + => With(rules: rules); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionList WithSuggestionModeItem(CompletionItem suggestionModeItem) + => With(suggestionModeItem: suggestionModeItem); + + /// + /// The default returned when no items are found to populate the list. + /// + public static readonly CompletionList Empty = new( + default, [], CompletionRules.Default, + suggestionModeItem: null, isExclusive: false); + + internal bool IsEmpty => ItemsList.Count == 0 && SuggestionModeItem is null; } diff --git a/src/Features/Core/Portable/Completion/CompletionOptions.cs b/src/Features/Core/Portable/Completion/CompletionOptions.cs index 51246b8034c5a..1ee9389f0e01a 100644 --- a/src/Features/Core/Portable/Completion/CompletionOptions.cs +++ b/src/Features/Core/Portable/Completion/CompletionOptions.cs @@ -7,89 +7,88 @@ using Microsoft.CodeAnalysis.Recommendations; using Microsoft.CodeAnalysis.Shared; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal sealed record class CompletionOptions { - internal sealed record class CompletionOptions - { - public bool TriggerOnTyping { get; init; } = true; - public bool TriggerOnTypingLetters { get; init; } = true; - public bool? TriggerOnDeletion { get; init; } = null; - public bool TriggerInArgumentLists { get; init; } = true; - public EnterKeyRule EnterKeyBehavior { get; init; } = EnterKeyRule.Default; - public SnippetsRule SnippetsBehavior { get; init; } = SnippetsRule.Default; - public bool HideAdvancedMembers { get; init; } = false; - public bool ShowNameSuggestions { get; init; } = true; - public bool? ShowItemsFromUnimportedNamespaces { get; init; } = true; - public bool UnnamedSymbolCompletionDisabled { get; init; } = false; - public bool TargetTypedCompletionFilter { get; init; } = false; - public bool ProvideDateAndTimeCompletions { get; init; } = true; - public bool ProvideRegexCompletions { get; init; } = true; - public bool PerformSort { get; init; } = true; + public bool TriggerOnTyping { get; init; } = true; + public bool TriggerOnTypingLetters { get; init; } = true; + public bool? TriggerOnDeletion { get; init; } = null; + public bool TriggerInArgumentLists { get; init; } = true; + public EnterKeyRule EnterKeyBehavior { get; init; } = EnterKeyRule.Default; + public SnippetsRule SnippetsBehavior { get; init; } = SnippetsRule.Default; + public bool HideAdvancedMembers { get; init; } = false; + public bool ShowNameSuggestions { get; init; } = true; + public bool? ShowItemsFromUnimportedNamespaces { get; init; } = true; + public bool UnnamedSymbolCompletionDisabled { get; init; } = false; + public bool TargetTypedCompletionFilter { get; init; } = false; + public bool ProvideDateAndTimeCompletions { get; init; } = true; + public bool ProvideRegexCompletions { get; init; } = true; + public bool PerformSort { get; init; } = true; - /// - /// Force completion APIs to produce complete results, even in cases where caches have not been pre-populated. - /// This is typically used for testing scenarios, and by public APIs where consumers do not have access to - /// other internal APIs used to control cache creation and/or wait for caches to be populated before examining - /// completion results. - /// - public bool ForceExpandedCompletionIndexCreation { get; init; } = false; + /// + /// Force completion APIs to produce complete results, even in cases where caches have not been pre-populated. + /// This is typically used for testing scenarios, and by public APIs where consumers do not have access to + /// other internal APIs used to control cache creation and/or wait for caches to be populated before examining + /// completion results. + /// + public bool ForceExpandedCompletionIndexCreation { get; init; } = false; - /// - /// Set to true to update import completion cache in background if the provider isn't supposed to be triggered in the context. - /// (cache will always be refreshed when provider is triggered) - /// - public bool UpdateImportCompletionCacheInBackground { get; init; } = false; + /// + /// Set to true to update import completion cache in background if the provider isn't supposed to be triggered in the context. + /// (cache will always be refreshed when provider is triggered) + /// + public bool UpdateImportCompletionCacheInBackground { get; init; } = false; - /// - /// Whether completion can add import statement as part of committed change. - /// For example, adding import is not allowed in debugger view. - /// - public bool CanAddImportStatement { get; init; } = true; + /// + /// Whether completion can add import statement as part of committed change. + /// For example, adding import is not allowed in debugger view. + /// + public bool CanAddImportStatement { get; init; } = true; - public bool FilterOutOfScopeLocals { get; init; } = true; - public bool ShowXmlDocCommentCompletion { get; init; } = true; - public bool? ShowNewSnippetExperienceUserOption { get; init; } = null; - public bool ShowNewSnippetExperienceFeatureFlag { get; init; } = true; - public ExpandedCompletionMode ExpandedCompletionBehavior { get; init; } = ExpandedCompletionMode.AllItems; - public NamingStylePreferences? NamingStyleFallbackOptions { get; init; } = null; + public bool FilterOutOfScopeLocals { get; init; } = true; + public bool ShowXmlDocCommentCompletion { get; init; } = true; + public bool? ShowNewSnippetExperienceUserOption { get; init; } = null; + public bool ShowNewSnippetExperienceFeatureFlag { get; init; } = true; + public ExpandedCompletionMode ExpandedCompletionBehavior { get; init; } = ExpandedCompletionMode.AllItems; + public NamingStylePreferences? NamingStyleFallbackOptions { get; init; } = null; - public static readonly CompletionOptions Default = new(); + public static readonly CompletionOptions Default = new(); - public RecommendationServiceOptions ToRecommendationServiceOptions() - => new() - { - FilterOutOfScopeLocals = FilterOutOfScopeLocals, - HideAdvancedMembers = HideAdvancedMembers - }; + public RecommendationServiceOptions ToRecommendationServiceOptions() + => new() + { + FilterOutOfScopeLocals = FilterOutOfScopeLocals, + HideAdvancedMembers = HideAdvancedMembers + }; - /// - /// Whether items from unimported namespaces should be included in the completion list. - /// - public bool ShouldShowItemsFromUnimportedNamespaces - => !ShowItemsFromUnimportedNamespaces.HasValue || ShowItemsFromUnimportedNamespaces.Value; + /// + /// Whether items from unimported namespaces should be included in the completion list. + /// + public bool ShouldShowItemsFromUnimportedNamespaces + => !ShowItemsFromUnimportedNamespaces.HasValue || ShowItemsFromUnimportedNamespaces.Value; - /// - /// Whether items from new snippet experience should be included in the completion list. - /// This takes into consideration the experiment we are running in addition to the value - /// from user facing options. - /// - public bool ShouldShowNewSnippetExperience(Document document) + /// + /// Whether items from new snippet experience should be included in the completion list. + /// This takes into consideration the experiment we are running in addition to the value + /// from user facing options. + /// + public bool ShouldShowNewSnippetExperience(Document document) + { + // Will be removed once semantic snippets will be added to razor. + var solution = document.Project.Solution; + var documentSupportsFeatureService = solution.Services.GetRequiredService(); + if (!documentSupportsFeatureService.SupportsSemanticSnippets(document)) { - // Will be removed once semantic snippets will be added to razor. - var solution = document.Project.Solution; - var documentSupportsFeatureService = solution.Services.GetRequiredService(); - if (!documentSupportsFeatureService.SupportsSemanticSnippets(document)) - { - return false; - } - - if (document.IsRazorDocument()) - { - return false; - } + return false; + } - // Don't trigger snippet completion if the option value is "default" and the experiment is disabled for the user. - return ShowNewSnippetExperienceUserOption ?? ShowNewSnippetExperienceFeatureFlag; + if (document.IsRazorDocument()) + { + return false; } + + // Don't trigger snippet completion if the option value is "default" and the experiment is disabled for the user. + return ShowNewSnippetExperienceUserOption ?? ShowNewSnippetExperienceFeatureFlag; } } diff --git a/src/Features/Core/Portable/Completion/CompletionProvider.cs b/src/Features/Core/Portable/Completion/CompletionProvider.cs index 27ea7b7c9b336..1df048ca92ab8 100644 --- a/src/Features/Core/Portable/Completion/CompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/CompletionProvider.cs @@ -9,83 +9,82 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// Implement a subtype of this class and export it to provide completions during typing in an editor. +/// +public abstract class CompletionProvider { - /// - /// Implement a subtype of this class and export it to provide completions during typing in an editor. - /// - public abstract class CompletionProvider - { - internal string Name { get; } + internal string Name { get; } - protected CompletionProvider() - => Name = GetType().FullName!; + protected CompletionProvider() + => Name = GetType().FullName!; - /// - /// Implement to contribute 's and other details to a - /// - public abstract Task ProvideCompletionsAsync(CompletionContext context); + /// + /// Implement to contribute 's and other details to a + /// + public abstract Task ProvideCompletionsAsync(CompletionContext context); - /// - /// Returns true if the character recently inserted or deleted in the text should trigger completion. - /// - /// The text that completion is occurring within. - /// The position of the caret after the triggering action. - /// The triggering action. - /// The set of options in effect. - public virtual bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options) - => false; + /// + /// Returns true if the character recently inserted or deleted in the text should trigger completion. + /// + /// The text that completion is occurring within. + /// The position of the caret after the triggering action. + /// The triggering action. + /// The set of options in effect. + public virtual bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options) + => false; - /// - /// Returns true if the character recently inserted or deleted in the text should trigger completion. - /// - /// The language services available on the text document. - /// The text that completion is occurring within. - /// The position of the caret after the triggering action. - /// The triggering action. - /// The set of options in effect. - internal virtual bool ShouldTriggerCompletion(LanguageServices languageServices, SourceText text, int caretPosition, CompletionTrigger trigger, CompletionOptions options, OptionSet passThroughOptions) + /// + /// Returns true if the character recently inserted or deleted in the text should trigger completion. + /// + /// The language services available on the text document. + /// The text that completion is occurring within. + /// The position of the caret after the triggering action. + /// The triggering action. + /// The set of options in effect. + internal virtual bool ShouldTriggerCompletion(LanguageServices languageServices, SourceText text, int caretPosition, CompletionTrigger trigger, CompletionOptions options, OptionSet passThroughOptions) #pragma warning disable RS0030, CS0618 // Do not used banned/obsolete APIs - => ShouldTriggerCompletion(text, caretPosition, trigger, passThroughOptions); + => ShouldTriggerCompletion(text, caretPosition, trigger, passThroughOptions); #pragma warning restore - /// - /// This allows Completion Providers that indicated they were triggered textually to use syntax to - /// confirm they are really triggered, or decide they are not actually triggered and should become - /// an augmenting provider instead. - /// - internal virtual async Task IsSyntacticTriggerCharacterAsync(Document document, int caretPosition, CompletionTrigger trigger, CompletionOptions options, CancellationToken cancellationToken) - => ShouldTriggerCompletion(document.Project.Services, await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false), caretPosition, trigger, options, document.Project.Solution.Options); + /// + /// This allows Completion Providers that indicated they were triggered textually to use syntax to + /// confirm they are really triggered, or decide they are not actually triggered and should become + /// an augmenting provider instead. + /// + internal virtual async Task IsSyntacticTriggerCharacterAsync(Document document, int caretPosition, CompletionTrigger trigger, CompletionOptions options, CancellationToken cancellationToken) + => ShouldTriggerCompletion(document.Project.Services, await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false), caretPosition, trigger, options, document.Project.Solution.Options); - /// - /// Gets the description of the specified item. - /// - public virtual Task GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken) - => Task.FromResult(CompletionDescription.Empty); + /// + /// Gets the description of the specified item. + /// + public virtual Task GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken) + => Task.FromResult(CompletionDescription.Empty); - internal virtual Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + internal virtual Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) #pragma warning disable RS0030 // Do not used banned APIs - => GetDescriptionAsync(document, item, cancellationToken); + => GetDescriptionAsync(document, item, cancellationToken); #pragma warning restore - /// - /// Gets the change to be applied when the specified item is committed. - /// - /// The current document. - /// The item to be committed. - /// The optional key character that caused the commit. - /// - public virtual Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) - => Task.FromResult(CompletionChange.Create(new TextChange(item.Span, item.DisplayText))); + /// + /// Gets the change to be applied when the specified item is committed. + /// + /// The current document. + /// The item to be committed. + /// The optional key character that caused the commit. + /// + public virtual Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) + => Task.FromResult(CompletionChange.Create(new TextChange(item.Span, item.DisplayText))); - /// - /// True if the provider produces snippet items. - /// - internal virtual bool IsSnippetProvider => false; + /// + /// True if the provider produces snippet items. + /// + internal virtual bool IsSnippetProvider => false; - /// - /// True if the provider produces items show be shown in expanded list only. - /// - internal virtual bool IsExpandItemProvider => false; - } + /// + /// True if the provider produces items show be shown in expanded list only. + /// + internal virtual bool IsExpandItemProvider => false; } diff --git a/src/Features/Core/Portable/Completion/CompletionProviderMetadata.cs b/src/Features/Core/Portable/Completion/CompletionProviderMetadata.cs index d5d2f136ac5f0..5eed74c425489 100644 --- a/src/Features/Core/Portable/Completion/CompletionProviderMetadata.cs +++ b/src/Features/Core/Portable/Completion/CompletionProviderMetadata.cs @@ -6,11 +6,10 @@ using Microsoft.CodeAnalysis.Host.Mef; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal sealed class CompletionProviderMetadata(IDictionary data) : OrderableLanguageMetadata(data) { - internal sealed class CompletionProviderMetadata(IDictionary data) : OrderableLanguageMetadata(data) - { - public string[]? Roles { get; } = (string[]?)data.GetValueOrDefault("Roles") - ?? (string[]?)data.GetValueOrDefault("TextViewRoles"); - } + public string[]? Roles { get; } = (string[]?)data.GetValueOrDefault("Roles") + ?? (string[]?)data.GetValueOrDefault("TextViewRoles"); } diff --git a/src/Features/Core/Portable/Completion/CompletionRules.cs b/src/Features/Core/Portable/Completion/CompletionRules.cs index a86bcbe1eb20a..bdfe9b947cdcb 100644 --- a/src/Features/Core/Portable/Completion/CompletionRules.cs +++ b/src/Features/Core/Portable/Completion/CompletionRules.cs @@ -4,167 +4,166 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// Presentation and behavior rules for completion. +/// +public sealed class CompletionRules { /// - /// Presentation and behavior rules for completion. + /// True if the completion list should be dismissed if the user's typing causes it to filter + /// and display no items. + /// + public bool DismissIfEmpty { get; } + + /// + /// True if the list should be dismissed when the user deletes the last character in the span. + /// + public bool DismissIfLastCharacterDeleted { get; } + + /// + /// The default set of typed characters that cause the selected item to be committed. + /// Individual s can override this. + /// + public ImmutableArray DefaultCommitCharacters { get; } + + /// + /// The default rule that determines if the enter key is passed through to the editor after the selected item has been committed. + /// Individual s can override this. + /// + public EnterKeyRule DefaultEnterKeyRule { get; } + + /// + /// The rule determining how snippets work. /// - public sealed class CompletionRules + public SnippetsRule SnippetsRule { get; } + + private CompletionRules( + bool dismissIfEmpty, + bool dismissIfLastCharacterDeleted, + ImmutableArray defaultCommitCharacters, + EnterKeyRule defaultEnterKeyRule, + SnippetsRule snippetsRule) { - /// - /// True if the completion list should be dismissed if the user's typing causes it to filter - /// and display no items. - /// - public bool DismissIfEmpty { get; } - - /// - /// True if the list should be dismissed when the user deletes the last character in the span. - /// - public bool DismissIfLastCharacterDeleted { get; } - - /// - /// The default set of typed characters that cause the selected item to be committed. - /// Individual s can override this. - /// - public ImmutableArray DefaultCommitCharacters { get; } - - /// - /// The default rule that determines if the enter key is passed through to the editor after the selected item has been committed. - /// Individual s can override this. - /// - public EnterKeyRule DefaultEnterKeyRule { get; } - - /// - /// The rule determining how snippets work. - /// - public SnippetsRule SnippetsRule { get; } - - private CompletionRules( - bool dismissIfEmpty, - bool dismissIfLastCharacterDeleted, - ImmutableArray defaultCommitCharacters, - EnterKeyRule defaultEnterKeyRule, - SnippetsRule snippetsRule) - { - DismissIfEmpty = dismissIfEmpty; - DismissIfLastCharacterDeleted = dismissIfLastCharacterDeleted; - DefaultCommitCharacters = defaultCommitCharacters.NullToEmpty(); - DefaultEnterKeyRule = defaultEnterKeyRule; - SnippetsRule = snippetsRule; - } + DismissIfEmpty = dismissIfEmpty; + DismissIfLastCharacterDeleted = dismissIfLastCharacterDeleted; + DefaultCommitCharacters = defaultCommitCharacters.NullToEmpty(); + DefaultEnterKeyRule = defaultEnterKeyRule; + SnippetsRule = snippetsRule; + } - /// - /// Creates a new instance. - /// - /// True if the completion list should be dismissed if the user's typing causes it to filter and display no items. - /// True if the list should be dismissed when the user deletes the last character in the span. - /// The default set of typed characters that cause the selected item to be committed. - /// The default rule that determines if the enter key is passed through to the editor after the selected item has been committed. - public static CompletionRules Create( - bool dismissIfEmpty, - bool dismissIfLastCharacterDeleted, - ImmutableArray defaultCommitCharacters, - EnterKeyRule defaultEnterKeyRule) - { - return Create(dismissIfEmpty, dismissIfLastCharacterDeleted, defaultCommitCharacters, - defaultEnterKeyRule, SnippetsRule.Default); - } + /// + /// Creates a new instance. + /// + /// True if the completion list should be dismissed if the user's typing causes it to filter and display no items. + /// True if the list should be dismissed when the user deletes the last character in the span. + /// The default set of typed characters that cause the selected item to be committed. + /// The default rule that determines if the enter key is passed through to the editor after the selected item has been committed. + public static CompletionRules Create( + bool dismissIfEmpty, + bool dismissIfLastCharacterDeleted, + ImmutableArray defaultCommitCharacters, + EnterKeyRule defaultEnterKeyRule) + { + return Create(dismissIfEmpty, dismissIfLastCharacterDeleted, defaultCommitCharacters, + defaultEnterKeyRule, SnippetsRule.Default); + } - /// - /// Creates a new instance. - /// - /// True if the completion list should be dismissed if the user's typing causes it to filter and display no items. - /// True if the list should be dismissed when the user deletes the last character in the span. - /// The default set of typed characters that cause the selected item to be committed. - /// The default rule that determines if the enter key is passed through to the editor after the selected item has been committed. - /// The rule that controls snippets behavior. - public static CompletionRules Create( - bool dismissIfEmpty = false, - bool dismissIfLastCharacterDeleted = false, - ImmutableArray defaultCommitCharacters = default, - EnterKeyRule defaultEnterKeyRule = EnterKeyRule.Default, - SnippetsRule snippetsRule = SnippetsRule.Default) + /// + /// Creates a new instance. + /// + /// True if the completion list should be dismissed if the user's typing causes it to filter and display no items. + /// True if the list should be dismissed when the user deletes the last character in the span. + /// The default set of typed characters that cause the selected item to be committed. + /// The default rule that determines if the enter key is passed through to the editor after the selected item has been committed. + /// The rule that controls snippets behavior. + public static CompletionRules Create( + bool dismissIfEmpty = false, + bool dismissIfLastCharacterDeleted = false, + ImmutableArray defaultCommitCharacters = default, + EnterKeyRule defaultEnterKeyRule = EnterKeyRule.Default, + SnippetsRule snippetsRule = SnippetsRule.Default) + { + return new CompletionRules( + dismissIfEmpty: dismissIfEmpty, + dismissIfLastCharacterDeleted: dismissIfLastCharacterDeleted, + defaultCommitCharacters: defaultCommitCharacters, + defaultEnterKeyRule: defaultEnterKeyRule, + snippetsRule: snippetsRule); + } + + private CompletionRules With( + Optional dismissIfEmpty = default, + Optional dismissIfLastCharacterDeleted = default, + Optional> defaultCommitCharacters = default, + Optional defaultEnterKeyRule = default, + Optional snippetsRule = default) + { + var newDismissIfEmpty = dismissIfEmpty.HasValue ? dismissIfEmpty.Value : DismissIfEmpty; + var newDismissIfLastCharacterDeleted = dismissIfLastCharacterDeleted.HasValue ? dismissIfLastCharacterDeleted.Value : DismissIfLastCharacterDeleted; + var newDefaultCommitCharacters = defaultCommitCharacters.HasValue ? defaultCommitCharacters.Value : DefaultCommitCharacters; + var newDefaultEnterKeyRule = defaultEnterKeyRule.HasValue ? defaultEnterKeyRule.Value : DefaultEnterKeyRule; + var newSnippetsRule = snippetsRule.HasValue ? snippetsRule.Value : SnippetsRule; + + if (newDismissIfEmpty == DismissIfEmpty && + newDismissIfLastCharacterDeleted == DismissIfLastCharacterDeleted && + newDefaultCommitCharacters == DefaultCommitCharacters && + newDefaultEnterKeyRule == DefaultEnterKeyRule && + newSnippetsRule == SnippetsRule) { - return new CompletionRules( - dismissIfEmpty: dismissIfEmpty, - dismissIfLastCharacterDeleted: dismissIfLastCharacterDeleted, - defaultCommitCharacters: defaultCommitCharacters, - defaultEnterKeyRule: defaultEnterKeyRule, - snippetsRule: snippetsRule); + return this; } - - private CompletionRules With( - Optional dismissIfEmpty = default, - Optional dismissIfLastCharacterDeleted = default, - Optional> defaultCommitCharacters = default, - Optional defaultEnterKeyRule = default, - Optional snippetsRule = default) + else { - var newDismissIfEmpty = dismissIfEmpty.HasValue ? dismissIfEmpty.Value : DismissIfEmpty; - var newDismissIfLastCharacterDeleted = dismissIfLastCharacterDeleted.HasValue ? dismissIfLastCharacterDeleted.Value : DismissIfLastCharacterDeleted; - var newDefaultCommitCharacters = defaultCommitCharacters.HasValue ? defaultCommitCharacters.Value : DefaultCommitCharacters; - var newDefaultEnterKeyRule = defaultEnterKeyRule.HasValue ? defaultEnterKeyRule.Value : DefaultEnterKeyRule; - var newSnippetsRule = snippetsRule.HasValue ? snippetsRule.Value : SnippetsRule; - - if (newDismissIfEmpty == DismissIfEmpty && - newDismissIfLastCharacterDeleted == DismissIfLastCharacterDeleted && - newDefaultCommitCharacters == DefaultCommitCharacters && - newDefaultEnterKeyRule == DefaultEnterKeyRule && - newSnippetsRule == SnippetsRule) - { - return this; - } - else - { - return Create( - newDismissIfEmpty, - newDismissIfLastCharacterDeleted, - newDefaultCommitCharacters, - newDefaultEnterKeyRule, - newSnippetsRule); - } + return Create( + newDismissIfEmpty, + newDismissIfLastCharacterDeleted, + newDefaultCommitCharacters, + newDefaultEnterKeyRule, + newSnippetsRule); } - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionRules WithDismissIfEmpty(bool dismissIfEmpty) - => With(dismissIfEmpty: dismissIfEmpty); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionRules WithDismissIfLastCharacterDeleted(bool dismissIfLastCharacterDeleted) - => With(dismissIfLastCharacterDeleted: dismissIfLastCharacterDeleted); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionRules WithDefaultCommitCharacters(ImmutableArray defaultCommitCharacters) - => With(defaultCommitCharacters: defaultCommitCharacters); - - /// - /// Creates a copy of this with the property changed. - /// - public CompletionRules WithDefaultEnterKeyRule(EnterKeyRule defaultEnterKeyRule) - => With(defaultEnterKeyRule: defaultEnterKeyRule); - - /// - /// Creates a copy of the this with the property changed. - /// - public CompletionRules WithSnippetsRule(SnippetsRule snippetsRule) - => With(snippetsRule: snippetsRule); - - private static readonly ImmutableArray s_defaultCommitKeys = [' ', '{', '}', '[', ']', '(', ')', '.', ',', ':', ';', '+', '-', '*', '/', '%', '&', '|', '^', '!', '~', '=', '<', '>', '?', '@', '#', '\'', '\"', '\\']; - - /// - /// The default if none is otherwise specified. - /// - public static readonly CompletionRules Default = new( - dismissIfEmpty: false, - dismissIfLastCharacterDeleted: false, - defaultCommitCharacters: s_defaultCommitKeys, - defaultEnterKeyRule: EnterKeyRule.Default, - snippetsRule: SnippetsRule.Default); } + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionRules WithDismissIfEmpty(bool dismissIfEmpty) + => With(dismissIfEmpty: dismissIfEmpty); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionRules WithDismissIfLastCharacterDeleted(bool dismissIfLastCharacterDeleted) + => With(dismissIfLastCharacterDeleted: dismissIfLastCharacterDeleted); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionRules WithDefaultCommitCharacters(ImmutableArray defaultCommitCharacters) + => With(defaultCommitCharacters: defaultCommitCharacters); + + /// + /// Creates a copy of this with the property changed. + /// + public CompletionRules WithDefaultEnterKeyRule(EnterKeyRule defaultEnterKeyRule) + => With(defaultEnterKeyRule: defaultEnterKeyRule); + + /// + /// Creates a copy of the this with the property changed. + /// + public CompletionRules WithSnippetsRule(SnippetsRule snippetsRule) + => With(snippetsRule: snippetsRule); + + private static readonly ImmutableArray s_defaultCommitKeys = [' ', '{', '}', '[', ']', '(', ')', '.', ',', ':', ';', '+', '-', '*', '/', '%', '&', '|', '^', '!', '~', '=', '<', '>', '?', '@', '#', '\'', '\"', '\\']; + + /// + /// The default if none is otherwise specified. + /// + public static readonly CompletionRules Default = new( + dismissIfEmpty: false, + dismissIfLastCharacterDeleted: false, + defaultCommitCharacters: s_defaultCommitKeys, + defaultEnterKeyRule: EnterKeyRule.Default, + snippetsRule: SnippetsRule.Default); } diff --git a/src/Features/Core/Portable/Completion/CompletionService.ProviderManager.cs b/src/Features/Core/Portable/Completion/CompletionService.ProviderManager.cs index f450afb5e8c06..9f1075f052dd7 100644 --- a/src/Features/Core/Portable/Completion/CompletionService.ProviderManager.cs +++ b/src/Features/Core/Portable/Completion/CompletionService.ProviderManager.cs @@ -18,280 +18,279 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +public abstract partial class CompletionService { - public abstract partial class CompletionService + private sealed class ProviderManager : IEqualityComparer> { - private sealed class ProviderManager : IEqualityComparer> - { - private readonly object _gate = new(); - private readonly Lazy> _nameToProvider; - private readonly Dictionary, ImmutableArray> _rolesToProviders; - private IReadOnlyList>? _lazyImportedProviders; - private readonly CompletionService _service; + private readonly object _gate = new(); + private readonly Lazy> _nameToProvider; + private readonly Dictionary, ImmutableArray> _rolesToProviders; + private IReadOnlyList>? _lazyImportedProviders; + private readonly CompletionService _service; - private readonly AsyncBatchingWorkQueue> _projectProvidersWorkQueue; + private readonly AsyncBatchingWorkQueue> _projectProvidersWorkQueue; - public ProviderManager(CompletionService service, IAsynchronousOperationListenerProvider listenerProvider) - { - _service = service; - _rolesToProviders = new Dictionary, ImmutableArray>(this); - _nameToProvider = new Lazy>(LoadImportedProvidersAndCreateNameMap, LazyThreadSafetyMode.PublicationOnly); - - _projectProvidersWorkQueue = new AsyncBatchingWorkQueue>( - TimeSpan.FromSeconds(1), - ProcessBatchAsync, - EqualityComparer>.Default, - listenerProvider.GetListener(FeatureAttribute.CompletionSet), - CancellationToken.None); - } + public ProviderManager(CompletionService service, IAsynchronousOperationListenerProvider listenerProvider) + { + _service = service; + _rolesToProviders = new Dictionary, ImmutableArray>(this); + _nameToProvider = new Lazy>(LoadImportedProvidersAndCreateNameMap, LazyThreadSafetyMode.PublicationOnly); + + _projectProvidersWorkQueue = new AsyncBatchingWorkQueue>( + TimeSpan.FromSeconds(1), + ProcessBatchAsync, + EqualityComparer>.Default, + listenerProvider.GetListener(FeatureAttribute.CompletionSet), + CancellationToken.None); + } - private ImmutableDictionary LoadImportedProvidersAndCreateNameMap() - { - var builder = ImmutableDictionary.CreateBuilder(); + private ImmutableDictionary LoadImportedProvidersAndCreateNameMap() + { + var builder = ImmutableDictionary.CreateBuilder(); - foreach (var lazyImportedProvider in GetLazyImportedProviders()) - builder[lazyImportedProvider.Value.Name] = lazyImportedProvider.Value; + foreach (var lazyImportedProvider in GetLazyImportedProviders()) + builder[lazyImportedProvider.Value.Name] = lazyImportedProvider.Value; #pragma warning disable CS0618 - // We need to keep supporting built-in providers for a while longer since this is a public API. - foreach (var builtinProvider in _service.GetBuiltInProviders()) - builder[builtinProvider.Name] = builtinProvider; + // We need to keep supporting built-in providers for a while longer since this is a public API. + foreach (var builtinProvider in _service.GetBuiltInProviders()) + builder[builtinProvider.Name] = builtinProvider; #pragma warning restore CS0618 - return builder.ToImmutable(); - } + return builder.ToImmutable(); + } - private IReadOnlyList> GetLazyImportedProviders() + private IReadOnlyList> GetLazyImportedProviders() + { + if (_lazyImportedProviders == null) { - if (_lazyImportedProviders == null) - { - var language = _service.Language; - var mefExporter = _service._services.ExportProvider; + var language = _service.Language; + var mefExporter = _service._services.ExportProvider; - var providers = ExtensionOrderer.Order( - mefExporter.GetExports() - .Where(lz => lz.Metadata.Language == language) - ).ToList(); + var providers = ExtensionOrderer.Order( + mefExporter.GetExports() + .Where(lz => lz.Metadata.Language == language) + ).ToList(); - Interlocked.CompareExchange(ref _lazyImportedProviders, providers, null); - } - - return _lazyImportedProviders; + Interlocked.CompareExchange(ref _lazyImportedProviders, providers, null); } - private ValueTask ProcessBatchAsync(ImmutableSegmentedList> referencesList, CancellationToken cancellationToken) + return _lazyImportedProviders; + } + + private ValueTask ProcessBatchAsync(ImmutableSegmentedList> referencesList, CancellationToken cancellationToken) + { + foreach (var references in referencesList) { - foreach (var references in referencesList) - { - cancellationToken.ThrowIfCancellationRequested(); - // Go through the potentially slow path to ensure project providers are loaded. - // We only do this in background here to avoid UI delays. - _ = ProjectCompletionProvider.GetExtensions(_service.Language, references); - } + cancellationToken.ThrowIfCancellationRequested(); + // Go through the potentially slow path to ensure project providers are loaded. + // We only do this in background here to avoid UI delays. + _ = ProjectCompletionProvider.GetExtensions(_service.Language, references); + } + + return ValueTaskFactory.CompletedTask; + } - return ValueTaskFactory.CompletedTask; + public ImmutableArray GetCachedProjectCompletionProvidersOrQueueLoadInBackground(Project? project, CompletionOptions options) + { + if (project is null || project.Solution.WorkspaceKind == WorkspaceKind.Interactive) + { + // TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict completions in Interactive + return []; } - public ImmutableArray GetCachedProjectCompletionProvidersOrQueueLoadInBackground(Project? project, CompletionOptions options) + // On primary completion paths, don't load providers if they are not already cached, + // return immediately and load them in background instead. If the test hook + // 'ForceExpandedCompletionIndexCreation' is set, calculate the values immediately. + // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1620947 + if (options.ForceExpandedCompletionIndexCreation) { - if (project is null || project.Solution.WorkspaceKind == WorkspaceKind.Interactive) - { - // TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict completions in Interactive - return []; - } + return ProjectCompletionProvider.GetExtensions(_service.Language, project.AnalyzerReferences); + } - // On primary completion paths, don't load providers if they are not already cached, - // return immediately and load them in background instead. If the test hook - // 'ForceExpandedCompletionIndexCreation' is set, calculate the values immediately. - // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1620947 - if (options.ForceExpandedCompletionIndexCreation) - { - return ProjectCompletionProvider.GetExtensions(_service.Language, project.AnalyzerReferences); - } + if (ProjectCompletionProvider.TryGetCachedExtensions(project.AnalyzerReferences, out var providers)) + return providers; - if (ProjectCompletionProvider.TryGetCachedExtensions(project.AnalyzerReferences, out var providers)) - return providers; + _projectProvidersWorkQueue.AddWork(project.AnalyzerReferences); + return []; + } - _projectProvidersWorkQueue.AddWork(project.AnalyzerReferences); - return []; - } + private ImmutableArray GetImportedAndBuiltInProviders(ImmutableHashSet? roles) + { + roles ??= []; - private ImmutableArray GetImportedAndBuiltInProviders(ImmutableHashSet? roles) + lock (_gate) { - roles ??= []; - - lock (_gate) + if (!_rolesToProviders.TryGetValue(roles, out var providers)) { - if (!_rolesToProviders.TryGetValue(roles, out var providers)) - { - providers = GetImportedAndBuiltInProvidersWorker(roles); - _rolesToProviders.Add(roles, providers); - } - - return providers; + providers = GetImportedAndBuiltInProvidersWorker(roles); + _rolesToProviders.Add(roles, providers); } - ImmutableArray GetImportedAndBuiltInProvidersWorker(ImmutableHashSet roles) - { - return - [ - .. GetLazyImportedProviders() - .Where(lz => lz.Metadata.Roles == null || lz.Metadata.Roles.Length == 0 || roles.Overlaps(lz.Metadata.Roles)) - .Select(lz => lz.Value), - // We need to keep supporting built-in providers for a while longer since this is a public API. - // https://github.com/dotnet/roslyn/issues/42367 + return providers; + } + + ImmutableArray GetImportedAndBuiltInProvidersWorker(ImmutableHashSet roles) + { + return + [ + .. GetLazyImportedProviders() + .Where(lz => lz.Metadata.Roles == null || lz.Metadata.Roles.Length == 0 || roles.Overlaps(lz.Metadata.Roles)) + .Select(lz => lz.Value), + // We need to keep supporting built-in providers for a while longer since this is a public API. + // https://github.com/dotnet/roslyn/issues/42367 #pragma warning disable 0618 - .. _service.GetBuiltInProviders(), + .. _service.GetBuiltInProviders(), #pragma warning restore 0618 - ]; - } + ]; } + } - public CompletionProvider? GetProvider(CompletionItem item, Project? project) - { - if (item.ProviderName == null) - return null; + public CompletionProvider? GetProvider(CompletionItem item, Project? project) + { + if (item.ProviderName == null) + return null; - if (_nameToProvider.Value.TryGetValue(item.ProviderName, out var provider)) - return provider; + if (_nameToProvider.Value.TryGetValue(item.ProviderName, out var provider)) + return provider; - using var _ = PooledDelegates.GetPooledFunction(static (p, n) => p.Name == n, item.ProviderName, out Func isNameMatchingProviderPredicate); + using var _ = PooledDelegates.GetPooledFunction(static (p, n) => p.Name == n, item.ProviderName, out Func isNameMatchingProviderPredicate); - // Publicly available options will not impact this call, since the completion item must have already - // existed if it produced the input completion item. - return GetCachedProjectCompletionProvidersOrQueueLoadInBackground(project, CompletionOptions.Default).FirstOrDefault(isNameMatchingProviderPredicate); - } + // Publicly available options will not impact this call, since the completion item must have already + // existed if it produced the input completion item. + return GetCachedProjectCompletionProvidersOrQueueLoadInBackground(project, CompletionOptions.Default).FirstOrDefault(isNameMatchingProviderPredicate); + } + + public ConcatImmutableArray GetFilteredProviders( + Project? project, ImmutableHashSet? roles, CompletionTrigger trigger, in CompletionOptions options) + { + var allCompletionProviders = FilterProviders(GetImportedAndBuiltInProviders(roles), trigger, options); + var projectCompletionProviders = FilterProviders(GetCachedProjectCompletionProvidersOrQueueLoadInBackground(project, options), trigger, options); + return allCompletionProviders.ConcatFast(projectCompletionProviders); + } - public ConcatImmutableArray GetFilteredProviders( - Project? project, ImmutableHashSet? roles, CompletionTrigger trigger, in CompletionOptions options) + private ImmutableArray FilterProviders( + ImmutableArray providers, + CompletionTrigger trigger, + in CompletionOptions options) + { + providers = options.ExpandedCompletionBehavior switch { - var allCompletionProviders = FilterProviders(GetImportedAndBuiltInProviders(roles), trigger, options); - var projectCompletionProviders = FilterProviders(GetCachedProjectCompletionProvidersOrQueueLoadInBackground(project, options), trigger, options); - return allCompletionProviders.ConcatFast(projectCompletionProviders); + ExpandedCompletionMode.NonExpandedItemsOnly => providers.WhereAsArray(p => !p.IsExpandItemProvider), + ExpandedCompletionMode.ExpandedItemsOnly => providers.WhereAsArray(p => p.IsExpandItemProvider), + _ => providers, + }; + + // If the caller passed along specific options that affect snippets, + // then defer to those. Otherwise if the caller just wants the default + // behavior, then get the snippets behavior from our own rules. + var snippetsRule = options.SnippetsBehavior != SnippetsRule.Default + ? options.SnippetsBehavior + : _service.GetRules(options).SnippetsRule; + + if (snippetsRule is SnippetsRule.Default or + SnippetsRule.NeverInclude) + { + return providers.Where(p => !p.IsSnippetProvider).ToImmutableArray(); } - - private ImmutableArray FilterProviders( - ImmutableArray providers, - CompletionTrigger trigger, - in CompletionOptions options) + else if (snippetsRule == SnippetsRule.AlwaysInclude) { - providers = options.ExpandedCompletionBehavior switch - { - ExpandedCompletionMode.NonExpandedItemsOnly => providers.WhereAsArray(p => !p.IsExpandItemProvider), - ExpandedCompletionMode.ExpandedItemsOnly => providers.WhereAsArray(p => p.IsExpandItemProvider), - _ => providers, - }; - - // If the caller passed along specific options that affect snippets, - // then defer to those. Otherwise if the caller just wants the default - // behavior, then get the snippets behavior from our own rules. - var snippetsRule = options.SnippetsBehavior != SnippetsRule.Default - ? options.SnippetsBehavior - : _service.GetRules(options).SnippetsRule; - - if (snippetsRule is SnippetsRule.Default or - SnippetsRule.NeverInclude) - { - return providers.Where(p => !p.IsSnippetProvider).ToImmutableArray(); - } - else if (snippetsRule == SnippetsRule.AlwaysInclude) + return providers; + } + else if (snippetsRule == SnippetsRule.IncludeAfterTypingIdentifierQuestionTab) + { + if (trigger.Kind == CompletionTriggerKind.Snippets) { - return providers; + return providers.Where(p => p.IsSnippetProvider).ToImmutableArray(); } - else if (snippetsRule == SnippetsRule.IncludeAfterTypingIdentifierQuestionTab) + else { - if (trigger.Kind == CompletionTriggerKind.Snippets) - { - return providers.Where(p => p.IsSnippetProvider).ToImmutableArray(); - } - else - { - return providers.Where(p => !p.IsSnippetProvider).ToImmutableArray(); - } + return providers.Where(p => !p.IsSnippetProvider).ToImmutableArray(); } - - return []; } - public void LoadProviders() + return []; + } + + public void LoadProviders() + { + _ = _nameToProvider.Value; + } + + bool IEqualityComparer>.Equals([AllowNull] ImmutableHashSet x, [AllowNull] ImmutableHashSet y) + { + if (x == y) { - _ = _nameToProvider.Value; + return true; } - bool IEqualityComparer>.Equals([AllowNull] ImmutableHashSet x, [AllowNull] ImmutableHashSet y) + if (x == null || y == null || x.Count != y.Count) { - if (x == y) - { - return true; - } + return false; + } - if (x == null || y == null || x.Count != y.Count) + foreach (var v in x) + { + if (!y.Contains(v)) { return false; } + } - foreach (var v in x) - { - if (!y.Contains(v)) - { - return false; - } - } + return true; + } - return true; + int IEqualityComparer>.GetHashCode([DisallowNull] ImmutableHashSet obj) + { + var hash = 0; + foreach (var o in obj) + { + hash += o.GetHashCode(); } - int IEqualityComparer>.GetHashCode([DisallowNull] ImmutableHashSet obj) - { - var hash = 0; - foreach (var o in obj) - { - hash += o.GetHashCode(); - } + return hash; + } - return hash; - } + private sealed class ProjectCompletionProvider + : AbstractProjectExtensionProvider + { + protected override ImmutableArray GetLanguages(ExportCompletionProviderAttribute exportAttribute) + => [exportAttribute.Language]; - private sealed class ProjectCompletionProvider - : AbstractProjectExtensionProvider + protected override bool TryGetExtensionsFromReference(AnalyzerReference reference, out ImmutableArray extensions) { - protected override ImmutableArray GetLanguages(ExportCompletionProviderAttribute exportAttribute) - => [exportAttribute.Language]; - - protected override bool TryGetExtensionsFromReference(AnalyzerReference reference, out ImmutableArray extensions) + // check whether the analyzer reference knows how to return completion providers directly. + if (reference is ICompletionProviderFactory completionProviderFactory) { - // check whether the analyzer reference knows how to return completion providers directly. - if (reference is ICompletionProviderFactory completionProviderFactory) - { - extensions = completionProviderFactory.GetCompletionProviders(); - return true; - } - - extensions = default; - return false; + extensions = completionProviderFactory.GetCompletionProviders(); + return true; } + + extensions = default; + return false; } + } - internal TestAccessor GetTestAccessor() - => new(this); + internal TestAccessor GetTestAccessor() + => new(this); - internal readonly struct TestAccessor(ProviderManager providerManager) - { - private readonly ProviderManager _providerManager = providerManager; + internal readonly struct TestAccessor(ProviderManager providerManager) + { + private readonly ProviderManager _providerManager = providerManager; - public ImmutableArray GetImportedAndBuiltInProviders(ImmutableHashSet roles) - { - return _providerManager.GetImportedAndBuiltInProviders(roles); - } + public ImmutableArray GetImportedAndBuiltInProviders(ImmutableHashSet roles) + { + return _providerManager.GetImportedAndBuiltInProviders(roles); + } - public ImmutableArray GetProjectProviders(Project project) - { - // Force-load the extension completion providers - return _providerManager.GetCachedProjectCompletionProvidersOrQueueLoadInBackground( - project, - CompletionOptions.Default with { ForceExpandedCompletionIndexCreation = true }); - } + public ImmutableArray GetProjectProviders(Project project) + { + // Force-load the extension completion providers + return _providerManager.GetCachedProjectCompletionProvidersOrQueueLoadInBackground( + project, + CompletionOptions.Default with { ForceExpandedCompletionIndexCreation = true }); } } } diff --git a/src/Features/Core/Portable/Completion/CompletionService.cs b/src/Features/Core/Portable/Completion/CompletionService.cs index ab9e9acc9d319..bcbc67a6dd6b9 100644 --- a/src/Features/Core/Portable/Completion/CompletionService.cs +++ b/src/Features/Core/Portable/Completion/CompletionService.cs @@ -21,398 +21,397 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// A per language service for constructing context dependent list of completions that +/// can be presented to a user during typing in an editor. It aggregates completions from +/// one or more s. +/// +public abstract partial class CompletionService : ILanguageService { + private readonly SolutionServices _services; + private readonly ProviderManager _providerManager; + /// - /// A per language service for constructing context dependent list of completions that - /// can be presented to a user during typing in an editor. It aggregates completions from - /// one or more s. + /// Test-only switch. /// - public abstract partial class CompletionService : ILanguageService + private bool _suppressPartialSemantics; + + // Prevent inheritance outside of Roslyn. + internal CompletionService(SolutionServices services, IAsynchronousOperationListenerProvider listenerProvider) { - private readonly SolutionServices _services; - private readonly ProviderManager _providerManager; + _services = services; + _providerManager = new(this, listenerProvider); + } - /// - /// Test-only switch. - /// - private bool _suppressPartialSemantics; + /// + /// Gets the service corresponding to the specified document. + /// + public static CompletionService? GetService(Document? document) + => document?.GetLanguageService(); - // Prevent inheritance outside of Roslyn. - internal CompletionService(SolutionServices services, IAsynchronousOperationListenerProvider listenerProvider) - { - _services = services; - _providerManager = new(this, listenerProvider); - } + /// + /// Returns the providers always available to the service. + /// This does not included providers imported via MEF composition. + /// + [Obsolete("Built-in providers will be ignored in a future release, please make them MEF exports instead.")] + protected virtual ImmutableArray GetBuiltInProviders() + => []; - /// - /// Gets the service corresponding to the specified document. - /// - public static CompletionService? GetService(Document? document) - => document?.GetLanguageService(); - - /// - /// Returns the providers always available to the service. - /// This does not included providers imported via MEF composition. - /// - [Obsolete("Built-in providers will be ignored in a future release, please make them MEF exports instead.")] - protected virtual ImmutableArray GetBuiltInProviders() - => []; - - /// - /// The language from this service corresponds to. - /// - public abstract string Language { get; } - - /// - /// Gets the current presentation and behavior rules. - /// - /// - /// Backward compatibility only. - /// - public CompletionRules GetRules() - { - Debug.Fail("For backwards API compat only, should not be called"); + /// + /// The language from this service corresponds to. + /// + public abstract string Language { get; } - // Publicly available options do not affect this API. - return GetRules(CompletionOptions.Default); - } + /// + /// Gets the current presentation and behavior rules. + /// + /// + /// Backward compatibility only. + /// + public CompletionRules GetRules() + { + Debug.Fail("For backwards API compat only, should not be called"); - internal abstract CompletionRules GetRules(CompletionOptions options); - - /// - /// Returns true if the character recently inserted or deleted in the text should trigger completion. - /// - /// The document text to trigger completion within - /// The position of the caret after the triggering action. - /// The potential triggering action. - /// Optional set of roles associated with the editor state. - /// Optional options that override the default options. - /// - /// This API uses SourceText instead of Document so implementations can only be based on text, not syntax or semantics. - /// - public bool ShouldTriggerCompletion( - SourceText text, - int caretPosition, - CompletionTrigger trigger, - ImmutableHashSet? roles = null, - OptionSet? options = null) - { - var document = text.GetOpenDocumentInCurrentContextWithChanges(); - var languageServices = document?.Project.Services ?? _services.GetLanguageServices(Language); + // Publicly available options do not affect this API. + return GetRules(CompletionOptions.Default); + } - // Publicly available options do not affect this API. Force complete results from this public API since - // external consumers do not have access to Roslyn's waiters. - var completionOptions = CompletionOptions.Default with { ForceExpandedCompletionIndexCreation = true }; - var passThroughOptions = options ?? document?.Project.Solution.Options ?? OptionSet.Empty; + internal abstract CompletionRules GetRules(CompletionOptions options); - return ShouldTriggerCompletion(document?.Project, languageServices, text, caretPosition, trigger, completionOptions, passThroughOptions, roles); - } + /// + /// Returns true if the character recently inserted or deleted in the text should trigger completion. + /// + /// The document text to trigger completion within + /// The position of the caret after the triggering action. + /// The potential triggering action. + /// Optional set of roles associated with the editor state. + /// Optional options that override the default options. + /// + /// This API uses SourceText instead of Document so implementations can only be based on text, not syntax or semantics. + /// + public bool ShouldTriggerCompletion( + SourceText text, + int caretPosition, + CompletionTrigger trigger, + ImmutableHashSet? roles = null, + OptionSet? options = null) + { + var document = text.GetOpenDocumentInCurrentContextWithChanges(); + var languageServices = document?.Project.Services ?? _services.GetLanguageServices(Language); - internal virtual bool SupportsTriggerOnDeletion(CompletionOptions options) - => options.TriggerOnDeletion == true; - - /// - /// Returns true if the character recently inserted or deleted in the text should trigger completion. - /// - /// The project containing the document and text - /// Language services - /// The document text to trigger completion within - /// The position of the caret after the triggering action. - /// The potential triggering action. - /// Options. - /// Options originating either from external caller of the or set externally to . - /// Optional set of roles associated with the editor state. - /// - /// We pass the project here to retrieve information about the , - /// and which are fast operations. - /// It should not be used for syntactic or semantic operations. - /// - internal virtual bool ShouldTriggerCompletion( - Project? project, - LanguageServices languageServices, - SourceText text, - int caretPosition, - CompletionTrigger trigger, - CompletionOptions options, - OptionSet passThroughOptions, - ImmutableHashSet? roles = null) - { - // The trigger kind guarantees that user wants a completion. - if (trigger.Kind is CompletionTriggerKind.Invoke or CompletionTriggerKind.InvokeAndCommitIfUnique) - return true; + // Publicly available options do not affect this API. Force complete results from this public API since + // external consumers do not have access to Roslyn's waiters. + var completionOptions = CompletionOptions.Default with { ForceExpandedCompletionIndexCreation = true }; + var passThroughOptions = options ?? document?.Project.Solution.Options ?? OptionSet.Empty; - if (!options.TriggerOnTyping) - return false; + return ShouldTriggerCompletion(document?.Project, languageServices, text, caretPosition, trigger, completionOptions, passThroughOptions, roles); + } - // Enter does not trigger completion. - if (trigger.Kind == CompletionTriggerKind.Insertion && trigger.Character == '\n') - { - return false; - } + internal virtual bool SupportsTriggerOnDeletion(CompletionOptions options) + => options.TriggerOnDeletion == true; - if (trigger.Kind == CompletionTriggerKind.Deletion && SupportsTriggerOnDeletion(options)) - { - return char.IsLetterOrDigit(trigger.Character) || trigger.Character == '.'; - } - - var extensionManager = languageServices.SolutionServices.GetRequiredService(); + /// + /// Returns true if the character recently inserted or deleted in the text should trigger completion. + /// + /// The project containing the document and text + /// Language services + /// The document text to trigger completion within + /// The position of the caret after the triggering action. + /// The potential triggering action. + /// Options. + /// Options originating either from external caller of the or set externally to . + /// Optional set of roles associated with the editor state. + /// + /// We pass the project here to retrieve information about the , + /// and which are fast operations. + /// It should not be used for syntactic or semantic operations. + /// + internal virtual bool ShouldTriggerCompletion( + Project? project, + LanguageServices languageServices, + SourceText text, + int caretPosition, + CompletionTrigger trigger, + CompletionOptions options, + OptionSet passThroughOptions, + ImmutableHashSet? roles = null) + { + // The trigger kind guarantees that user wants a completion. + if (trigger.Kind is CompletionTriggerKind.Invoke or CompletionTriggerKind.InvokeAndCommitIfUnique) + return true; - var providers = _providerManager.GetFilteredProviders(project, roles, trigger, options); - return providers.Any(p => - extensionManager.PerformFunction(p, - () => p.ShouldTriggerCompletion(languageServices, text, caretPosition, trigger, options, passThroughOptions), - defaultValue: false)); - } + if (!options.TriggerOnTyping) + return false; - /// - /// Gets the span of the syntax element at the caret position. - /// This is the most common value used for . - /// - /// The document text that completion is occurring within. - /// The position of the caret within the text. - [Obsolete("Not used anymore. CompletionService.GetDefaultCompletionListSpan is used instead.", error: true)] - public virtual TextSpan GetDefaultItemSpan(SourceText text, int caretPosition) - => GetDefaultCompletionListSpan(text, caretPosition); - - public virtual TextSpan GetDefaultCompletionListSpan(SourceText text, int caretPosition) + // Enter does not trigger completion. + if (trigger.Kind == CompletionTriggerKind.Insertion && trigger.Character == '\n') { - return CommonCompletionUtilities.GetWordSpan( - text, caretPosition, char.IsLetter, char.IsLetterOrDigit); + return false; } - /// - /// Gets the description of the item. - /// - /// This will be the original document that - /// was created against. - /// The item to get the description for. - /// - public Task GetDescriptionAsync( - Document document, - CompletionItem item, - CancellationToken cancellationToken = default) + if (trigger.Kind == CompletionTriggerKind.Deletion && SupportsTriggerOnDeletion(options)) { - // Publicly available options do not affect this API. - return GetDescriptionAsync(document, item, CompletionOptions.Default, SymbolDescriptionOptions.Default, cancellationToken); + return char.IsLetterOrDigit(trigger.Character) || trigger.Character == '.'; } - /// - /// Gets the description of the item. - /// - /// This will be the original document that - /// was created against. - /// The item to get the description for. - /// Completion options - /// Display options - /// - internal virtual async Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken = default) - { - var provider = GetProvider(item, document.Project); - if (provider is null) - return CompletionDescription.Empty; + var extensionManager = languageServices.SolutionServices.GetRequiredService(); + + var providers = _providerManager.GetFilteredProviders(project, roles, trigger, options); + return providers.Any(p => + extensionManager.PerformFunction(p, + () => p.ShouldTriggerCompletion(languageServices, text, caretPosition, trigger, options, passThroughOptions), + defaultValue: false)); + } + /// + /// Gets the span of the syntax element at the caret position. + /// This is the most common value used for . + /// + /// The document text that completion is occurring within. + /// The position of the caret within the text. + [Obsolete("Not used anymore. CompletionService.GetDefaultCompletionListSpan is used instead.", error: true)] + public virtual TextSpan GetDefaultItemSpan(SourceText text, int caretPosition) + => GetDefaultCompletionListSpan(text, caretPosition); + + public virtual TextSpan GetDefaultCompletionListSpan(SourceText text, int caretPosition) + { + return CommonCompletionUtilities.GetWordSpan( + text, caretPosition, char.IsLetter, char.IsLetterOrDigit); + } + + /// + /// Gets the description of the item. + /// + /// This will be the original document that + /// was created against. + /// The item to get the description for. + /// + public Task GetDescriptionAsync( + Document document, + CompletionItem item, + CancellationToken cancellationToken = default) + { + // Publicly available options do not affect this API. + return GetDescriptionAsync(document, item, CompletionOptions.Default, SymbolDescriptionOptions.Default, cancellationToken); + } + + /// + /// Gets the description of the item. + /// + /// This will be the original document that + /// was created against. + /// The item to get the description for. + /// Completion options + /// Display options + /// + internal virtual async Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken = default) + { + var provider = GetProvider(item, document.Project); + if (provider is null) + return CompletionDescription.Empty; + + var extensionManager = document.Project.Solution.Workspace.Services.GetRequiredService(); + + // We don't need SemanticModel here, just want to make sure it won't get GC'd before CompletionProviders are able to get it. + (document, var semanticModel) = await GetDocumentWithFrozenPartialSemanticsAsync(document, cancellationToken).ConfigureAwait(false); + var description = await extensionManager.PerformFunctionAsync( + provider, + () => provider.GetDescriptionAsync(document, item, options, displayOptions, cancellationToken), + defaultValue: null).ConfigureAwait(false); + GC.KeepAlive(semanticModel); + return description; + } + + /// + /// Gets the change to be applied when the item is committed. + /// + /// The document that completion is occurring within. + /// The item to get the change for. + /// The typed character that caused the item to be committed. + /// This character may be used as part of the change. + /// This value is null when the commit was caused by the [TAB] or [ENTER] keys. + /// + public virtual async Task GetChangeAsync( + Document document, + CompletionItem item, + char? commitCharacter = null, + CancellationToken cancellationToken = default) + { + var provider = GetProvider(item, document.Project); + if (provider != null) + { var extensionManager = document.Project.Solution.Workspace.Services.GetRequiredService(); // We don't need SemanticModel here, just want to make sure it won't get GC'd before CompletionProviders are able to get it. (document, var semanticModel) = await GetDocumentWithFrozenPartialSemanticsAsync(document, cancellationToken).ConfigureAwait(false); - var description = await extensionManager.PerformFunctionAsync( + + var change = await extensionManager.PerformFunctionAsync( provider, - () => provider.GetDescriptionAsync(document, item, options, displayOptions, cancellationToken), - defaultValue: null).ConfigureAwait(false); + () => provider.GetChangeAsync(document, item, commitCharacter, cancellationToken), + defaultValue: null!).ConfigureAwait(false); + if (change == null) + return CompletionChange.Create(new TextChange(new TextSpan(), "")); + GC.KeepAlive(semanticModel); - return description; + Debug.Assert(item.Span == change.TextChange.Span || item.IsComplexTextEdit); + return change; } - - /// - /// Gets the change to be applied when the item is committed. - /// - /// The document that completion is occurring within. - /// The item to get the change for. - /// The typed character that caused the item to be committed. - /// This character may be used as part of the change. - /// This value is null when the commit was caused by the [TAB] or [ENTER] keys. - /// - public virtual async Task GetChangeAsync( - Document document, - CompletionItem item, - char? commitCharacter = null, - CancellationToken cancellationToken = default) + else { - var provider = GetProvider(item, document.Project); - if (provider != null) - { - var extensionManager = document.Project.Solution.Workspace.Services.GetRequiredService(); + return CompletionChange.Create(new TextChange(item.Span, item.DisplayText)); + } + } - // We don't need SemanticModel here, just want to make sure it won't get GC'd before CompletionProviders are able to get it. - (document, var semanticModel) = await GetDocumentWithFrozenPartialSemanticsAsync(document, cancellationToken).ConfigureAwait(false); + // The FilterItems method might need to handle a large list of items when import completion is enabled and filter text is + // very short, i.e. <= 1. Therefore, use pooled list to avoid repeated (potentially LOH) allocations. + private static readonly ObjectPool> s_listOfMatchResultPool = new(factory: () => []); - var change = await extensionManager.PerformFunctionAsync( - provider, - () => provider.GetChangeAsync(document, item, commitCharacter, cancellationToken), - defaultValue: null!).ConfigureAwait(false); - if (change == null) - return CompletionChange.Create(new TextChange(new TextSpan(), "")); + /// + /// Given a list of completion items that match the current code typed by the user, + /// returns the item that is considered the best match, and whether or not that + /// item should be selected or not. + /// + /// itemToFilterText provides the values that each individual completion item should + /// be filtered against. + /// + public virtual ImmutableArray FilterItems( + Document document, + ImmutableArray items, + string filterText) + { + using var helper = new PatternMatchHelper(filterText); + var filterDataList = new SegmentedList( + items.Select(item => helper.GetMatchResult(item, includeMatchSpans: false, CultureInfo.CurrentCulture))); - GC.KeepAlive(semanticModel); - Debug.Assert(item.Span == change.TextChange.Span || item.IsComplexTextEdit); - return change; - } - else - { - return CompletionChange.Create(new TextChange(item.Span, item.DisplayText)); - } + var builder = s_listOfMatchResultPool.Allocate(); + try + { + FilterItems(CompletionHelper.GetHelper(document), filterDataList, filterText, builder); + return builder.SelectAsArray(result => result.CompletionItem); } - - // The FilterItems method might need to handle a large list of items when import completion is enabled and filter text is - // very short, i.e. <= 1. Therefore, use pooled list to avoid repeated (potentially LOH) allocations. - private static readonly ObjectPool> s_listOfMatchResultPool = new(factory: () => []); - - /// - /// Given a list of completion items that match the current code typed by the user, - /// returns the item that is considered the best match, and whether or not that - /// item should be selected or not. - /// - /// itemToFilterText provides the values that each individual completion item should - /// be filtered against. - /// - public virtual ImmutableArray FilterItems( - Document document, - ImmutableArray items, - string filterText) + finally { - using var helper = new PatternMatchHelper(filterText); - var filterDataList = new SegmentedList( - items.Select(item => helper.GetMatchResult(item, includeMatchSpans: false, CultureInfo.CurrentCulture))); - - var builder = s_listOfMatchResultPool.Allocate(); - try - { - FilterItems(CompletionHelper.GetHelper(document), filterDataList, filterText, builder); - return builder.SelectAsArray(result => result.CompletionItem); - } - finally - { - // Don't call ClearAndFree, which resets the capacity to a default value. - builder.Clear(); - s_listOfMatchResultPool.Free(builder); - } + // Don't call ClearAndFree, which resets the capacity to a default value. + builder.Clear(); + s_listOfMatchResultPool.Free(builder); } + } - internal virtual void FilterItems( - Document document, - IReadOnlyList matchResults, - string filterText, - IList builder) - { + internal virtual void FilterItems( + Document document, + IReadOnlyList matchResults, + string filterText, + IList builder) + { #pragma warning disable RS0030 // Do not used banned APIs - // Default implementation just drops the pattern matches and builder, and calls the public overload of FilterItems instead for compatibility. - var filteredItems = FilterItems(document, matchResults.SelectAsArray(item => item.CompletionItem), filterText); + // Default implementation just drops the pattern matches and builder, and calls the public overload of FilterItems instead for compatibility. + var filteredItems = FilterItems(document, matchResults.SelectAsArray(item => item.CompletionItem), filterText); #pragma warning restore RS0030 // Do not used banned APIs - using var completionPatternMatchers = new PatternMatchHelper(filterText); - builder.AddRange(filteredItems.Select(item => completionPatternMatchers.GetMatchResult(item, includeMatchSpans: false, CultureInfo.CurrentCulture))); - } + using var completionPatternMatchers = new PatternMatchHelper(filterText); + builder.AddRange(filteredItems.Select(item => completionPatternMatchers.GetMatchResult(item, includeMatchSpans: false, CultureInfo.CurrentCulture))); + } - /// - /// Determine among the provided items the best match w.r.t. the given filter text, - /// those returned would be considered equally good candidates for selection by controller. - /// - internal static void FilterItems( - CompletionHelper completionHelper, - IReadOnlyList matchResults, - string filterText, - IList builder) + /// + /// Determine among the provided items the best match w.r.t. the given filter text, + /// those returned would be considered equally good candidates for selection by controller. + /// + internal static void FilterItems( + CompletionHelper completionHelper, + IReadOnlyList matchResults, + string filterText, + IList builder) + { + // It's very common for people to type expecting completion to fix up their casing, + // so if no uppercase characters were typed so far, we'd loosen our standard on comparing items + // in terms of case-sensitivity and take into consideration the MatchPriority in certain scenarios. + // i.e. when everything else is equal, if item1 is a better case-sensitive match but has + // MatchPriority.Deprioritize, and item2 is not MatchPriority.Deprioritize, then we consider + // item2 a better match. + var filterTextHasNoUpperCase = !filterText.Any(char.IsUpper); + + foreach (var matchResult in matchResults) { - // It's very common for people to type expecting completion to fix up their casing, - // so if no uppercase characters were typed so far, we'd loosen our standard on comparing items - // in terms of case-sensitivity and take into consideration the MatchPriority in certain scenarios. - // i.e. when everything else is equal, if item1 is a better case-sensitive match but has - // MatchPriority.Deprioritize, and item2 is not MatchPriority.Deprioritize, then we consider - // item2 a better match. - var filterTextHasNoUpperCase = !filterText.Any(char.IsUpper); - - foreach (var matchResult in matchResults) + if (!matchResult.ShouldBeConsideredMatchingFilterText) + continue; + + if (builder.Count == 0) + { + // We've found no good items yet. So this is the best item currently. + builder.Add(matchResult); + continue; + } + + var comparison = completionHelper.CompareMatchResults(matchResult, builder[0], filterTextHasNoUpperCase); + + if (comparison == 0) { - if (!matchResult.ShouldBeConsideredMatchingFilterText) - continue; - - if (builder.Count == 0) - { - // We've found no good items yet. So this is the best item currently. - builder.Add(matchResult); - continue; - } - - var comparison = completionHelper.CompareMatchResults(matchResult, builder[0], filterTextHasNoUpperCase); - - if (comparison == 0) - { - // This item is as good as the items we've been collecting. We'll return it and let the controller - // decide what to do. (For example, it will pick the one that has the best MRU index). - builder.Add(matchResult); - } - else if (comparison < 0) - { - // This item is strictly better than the best items we've found so far. - builder.Clear(); - builder.Add(matchResult); - } + // This item is as good as the items we've been collecting. We'll return it and let the controller + // decide what to do. (For example, it will pick the one that has the best MRU index). + builder.Add(matchResult); + } + else if (comparison < 0) + { + // This item is strictly better than the best items we've found so far. + builder.Clear(); + builder.Add(matchResult); } } + } - /// - /// Don't call. Used for pre-populating MEF providers only. - /// - internal void LoadImportedProviders() - => _providerManager.LoadProviders(); + /// + /// Don't call. Used for pre-populating MEF providers only. + /// + internal void LoadImportedProviders() + => _providerManager.LoadProviders(); - /// - /// Don't call. Used for pre-load project providers only. - /// - internal void TriggerLoadProjectProviders(Project project, CompletionOptions options) - => _providerManager.GetCachedProjectCompletionProvidersOrQueueLoadInBackground(project, options); + /// + /// Don't call. Used for pre-load project providers only. + /// + internal void TriggerLoadProjectProviders(Project project, CompletionOptions options) + => _providerManager.GetCachedProjectCompletionProvidersOrQueueLoadInBackground(project, options); - internal CompletionProvider? GetProvider(CompletionItem item, Project? project) - => _providerManager.GetProvider(item, project); + internal CompletionProvider? GetProvider(CompletionItem item, Project? project) + => _providerManager.GetProvider(item, project); - internal TestAccessor GetTestAccessor() - => new(this); + internal TestAccessor GetTestAccessor() + => new(this); - internal readonly struct TestAccessor(CompletionService completionServiceWithProviders) - { - private readonly CompletionService _completionServiceWithProviders = completionServiceWithProviders; + internal readonly struct TestAccessor(CompletionService completionServiceWithProviders) + { + private readonly CompletionService _completionServiceWithProviders = completionServiceWithProviders; - public ImmutableArray GetImportedAndBuiltInProviders(ImmutableHashSet roles) - => _completionServiceWithProviders._providerManager.GetTestAccessor().GetImportedAndBuiltInProviders(roles); + public ImmutableArray GetImportedAndBuiltInProviders(ImmutableHashSet roles) + => _completionServiceWithProviders._providerManager.GetTestAccessor().GetImportedAndBuiltInProviders(roles); - public ImmutableArray GetProjectProviders(Project project) - => _completionServiceWithProviders._providerManager.GetTestAccessor().GetProjectProviders(project); + public ImmutableArray GetProjectProviders(Project project) + => _completionServiceWithProviders._providerManager.GetTestAccessor().GetProjectProviders(project); - public async Task GetContextAsync( - CompletionProvider provider, - Document document, - int position, - CompletionTrigger triggerInfo, - CompletionOptions options, - CancellationToken cancellationToken) - { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var defaultItemSpan = _completionServiceWithProviders.GetDefaultCompletionListSpan(text, position); - - return await CompletionService.GetContextAsync( - provider, - document, - position, - triggerInfo, - options, - defaultItemSpan, - sharedContext: null, - cancellationToken).ConfigureAwait(false); - } + public async Task GetContextAsync( + CompletionProvider provider, + Document document, + int position, + CompletionTrigger triggerInfo, + CompletionOptions options, + CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var defaultItemSpan = _completionServiceWithProviders.GetDefaultCompletionListSpan(text, position); - public void SuppressPartialSemantics() - => _completionServiceWithProviders._suppressPartialSemantics = true; + return await CompletionService.GetContextAsync( + provider, + document, + position, + triggerInfo, + options, + defaultItemSpan, + sharedContext: null, + cancellationToken).ConfigureAwait(false); } + + public void SuppressPartialSemantics() + => _completionServiceWithProviders._suppressPartialSemantics = true; } } diff --git a/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs b/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs index 3388f0cfc56fb..3545f832ab7f8 100644 --- a/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs +++ b/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs @@ -20,429 +20,428 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +public abstract partial class CompletionService { - public abstract partial class CompletionService + /// + /// Gets the completions available at the caret position. + /// + /// The document that completion is occurring within. + /// The position of the caret after the triggering action. + /// The triggering action. + /// Optional set of roles associated with the editor state. + /// Optional options that override the default options. + /// + public Task GetCompletionsAsync( + Document document, + int caretPosition, + CompletionTrigger trigger = default, + ImmutableHashSet? roles = null, + OptionSet? options = null, + CancellationToken cancellationToken = default) { - /// - /// Gets the completions available at the caret position. - /// - /// The document that completion is occurring within. - /// The position of the caret after the triggering action. - /// The triggering action. - /// Optional set of roles associated with the editor state. - /// Optional options that override the default options. - /// - public Task GetCompletionsAsync( - Document document, - int caretPosition, - CompletionTrigger trigger = default, - ImmutableHashSet? roles = null, - OptionSet? options = null, - CancellationToken cancellationToken = default) - { - // Publicly available options do not affect this API. Force complete results from this public API since - // external consumers do not have access to Roslyn's waiters. - var completionOptions = CompletionOptions.Default with { ForceExpandedCompletionIndexCreation = true }; - var passThroughOptions = options ?? document.Project.Solution.Options; + // Publicly available options do not affect this API. Force complete results from this public API since + // external consumers do not have access to Roslyn's waiters. + var completionOptions = CompletionOptions.Default with { ForceExpandedCompletionIndexCreation = true }; + var passThroughOptions = options ?? document.Project.Solution.Options; - return GetCompletionsAsync(document, caretPosition, completionOptions, passThroughOptions, trigger, roles, cancellationToken); - } + return GetCompletionsAsync(document, caretPosition, completionOptions, passThroughOptions, trigger, roles, cancellationToken); + } - /// - /// Gets the completions available at the caret position. - /// - /// The document that completion is occurring within. - /// The position of the caret after the triggering action. - /// The CompletionOptions that override the default options. - /// The triggering action. - /// Optional set of roles associated with the editor state. - /// - internal virtual async Task GetCompletionsAsync( - Document document, - int caretPosition, - CompletionOptions options, - OptionSet passThroughOptions, - CompletionTrigger trigger = default, - ImmutableHashSet? roles = null, - CancellationToken cancellationToken = default) - { - // We don't need SemanticModel here, just want to make sure it won't get GC'd before CompletionProviders are able to get it. - (document, var semanticModel) = await GetDocumentWithFrozenPartialSemanticsAsync(document, cancellationToken).ConfigureAwait(false); + /// + /// Gets the completions available at the caret position. + /// + /// The document that completion is occurring within. + /// The position of the caret after the triggering action. + /// The CompletionOptions that override the default options. + /// The triggering action. + /// Optional set of roles associated with the editor state. + /// + internal virtual async Task GetCompletionsAsync( + Document document, + int caretPosition, + CompletionOptions options, + OptionSet passThroughOptions, + CompletionTrigger trigger = default, + ImmutableHashSet? roles = null, + CancellationToken cancellationToken = default) + { + // We don't need SemanticModel here, just want to make sure it won't get GC'd before CompletionProviders are able to get it. + (document, var semanticModel) = await GetDocumentWithFrozenPartialSemanticsAsync(document, cancellationToken).ConfigureAwait(false); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var completionListSpan = GetDefaultCompletionListSpan(text, caretPosition); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var completionListSpan = GetDefaultCompletionListSpan(text, caretPosition); - var providers = _providerManager.GetFilteredProviders(document.Project, roles, trigger, options); + var providers = _providerManager.GetFilteredProviders(document.Project, roles, trigger, options); - // Phase 1: Completion Providers decide if they are triggered based on textual analysis - // Phase 2: Completion Providers use syntax to confirm they are triggered, or decide they are not actually triggered and should become an augmenting provider - // Phase 3: Triggered Providers are asked for items - // Phase 4: If any items were provided, all augmenting providers are asked for items - // This allows a provider to be textually triggered but later decide to be an augmenting provider based on deeper syntactic analysis. + // Phase 1: Completion Providers decide if they are triggered based on textual analysis + // Phase 2: Completion Providers use syntax to confirm they are triggered, or decide they are not actually triggered and should become an augmenting provider + // Phase 3: Triggered Providers are asked for items + // Phase 4: If any items were provided, all augmenting providers are asked for items + // This allows a provider to be textually triggered but later decide to be an augmenting provider based on deeper syntactic analysis. - var triggeredProviders = GetTriggeredProviders(document, providers, caretPosition, options, trigger, roles, text); + var triggeredProviders = GetTriggeredProviders(document, providers, caretPosition, options, trigger, roles, text); - var additionalAugmentingProviders = await GetAugmentingProvidersAsync(document, triggeredProviders, caretPosition, trigger, options, cancellationToken).ConfigureAwait(false); - triggeredProviders = triggeredProviders.Except(additionalAugmentingProviders).ToImmutableArray(); + var additionalAugmentingProviders = await GetAugmentingProvidersAsync(document, triggeredProviders, caretPosition, trigger, options, cancellationToken).ConfigureAwait(false); + triggeredProviders = triggeredProviders.Except(additionalAugmentingProviders).ToImmutableArray(); - // PERF: Many CompletionProviders compute identical contexts. This actually shows up on the 2-core typing test. - // so we try to share a single SyntaxContext based on document/caretPosition among all providers to reduce repeat computation. - var sharedContext = new SharedSyntaxContextsWithSpeculativeModel(document, caretPosition); + // PERF: Many CompletionProviders compute identical contexts. This actually shows up on the 2-core typing test. + // so we try to share a single SyntaxContext based on document/caretPosition among all providers to reduce repeat computation. + var sharedContext = new SharedSyntaxContextsWithSpeculativeModel(document, caretPosition); - // Now, ask all the triggered providers, in parallel, to populate a completion context. - // Note: we keep any context with items *or* with a suggested item. - var triggeredContexts = await ComputeNonEmptyCompletionContextsAsync( - document, caretPosition, trigger, options, completionListSpan, triggeredProviders, sharedContext, cancellationToken).ConfigureAwait(false); + // Now, ask all the triggered providers, in parallel, to populate a completion context. + // Note: we keep any context with items *or* with a suggested item. + var triggeredContexts = await ComputeNonEmptyCompletionContextsAsync( + document, caretPosition, trigger, options, completionListSpan, triggeredProviders, sharedContext, cancellationToken).ConfigureAwait(false); - // Nothing to do if we didn't even get any regular items back (i.e. 0 items or suggestion item only.) - if (!triggeredContexts.Any(static cc => cc.Items.Count > 0)) - return CompletionList.Empty; + // Nothing to do if we didn't even get any regular items back (i.e. 0 items or suggestion item only.) + if (!triggeredContexts.Any(static cc => cc.Items.Count > 0)) + return CompletionList.Empty; - // See if there were completion contexts provided that were exclusive. If so, then - // that's all we'll return. - var exclusiveContexts = triggeredContexts.Where(t => t.IsExclusive).ToImmutableArray(); - if (!exclusiveContexts.IsEmpty) - return MergeAndPruneCompletionLists(exclusiveContexts, options, isExclusive: true); + // See if there were completion contexts provided that were exclusive. If so, then + // that's all we'll return. + var exclusiveContexts = triggeredContexts.Where(t => t.IsExclusive).ToImmutableArray(); + if (!exclusiveContexts.IsEmpty) + return MergeAndPruneCompletionLists(exclusiveContexts, options, isExclusive: true); - // Great! We had some items. Now we want to see if any of the other providers - // would like to augment the completion list. For example, we might trigger - // enum-completion on space. If enum completion results in any items, then - // we'll want to augment the list with all the regular symbol completion items. - var augmentingProviders = providers.Except(triggeredProviders).ToImmutableArray(); + // Great! We had some items. Now we want to see if any of the other providers + // would like to augment the completion list. For example, we might trigger + // enum-completion on space. If enum completion results in any items, then + // we'll want to augment the list with all the regular symbol completion items. + var augmentingProviders = providers.Except(triggeredProviders).ToImmutableArray(); - var augmentingContexts = await ComputeNonEmptyCompletionContextsAsync( - document, caretPosition, trigger, options, completionListSpan, augmentingProviders, sharedContext, cancellationToken).ConfigureAwait(false); + var augmentingContexts = await ComputeNonEmptyCompletionContextsAsync( + document, caretPosition, trigger, options, completionListSpan, augmentingProviders, sharedContext, cancellationToken).ConfigureAwait(false); - GC.KeepAlive(semanticModel); + GC.KeepAlive(semanticModel); - // Providers are ordered, but we processed them in our own order. Ensure that the - // groups are properly ordered based on the original providers. - var completionProviderToIndex = GetCompletionProviderToIndex(providers); - var allContexts = triggeredContexts.Concat(augmentingContexts) - .Sort((p1, p2) => completionProviderToIndex[p1.Provider] - completionProviderToIndex[p2.Provider]); + // Providers are ordered, but we processed them in our own order. Ensure that the + // groups are properly ordered based on the original providers. + var completionProviderToIndex = GetCompletionProviderToIndex(providers); + var allContexts = triggeredContexts.Concat(augmentingContexts) + .Sort((p1, p2) => completionProviderToIndex[p1.Provider] - completionProviderToIndex[p2.Provider]); - return MergeAndPruneCompletionLists(allContexts, options, isExclusive: false); + return MergeAndPruneCompletionLists(allContexts, options, isExclusive: false); - ImmutableArray GetTriggeredProviders( - Document document, ConcatImmutableArray providers, int caretPosition, CompletionOptions options, CompletionTrigger trigger, ImmutableHashSet? roles, SourceText text) + ImmutableArray GetTriggeredProviders( + Document document, ConcatImmutableArray providers, int caretPosition, CompletionOptions options, CompletionTrigger trigger, ImmutableHashSet? roles, SourceText text) + { + switch (trigger.Kind) { - switch (trigger.Kind) - { - case CompletionTriggerKind.Insertion: - case CompletionTriggerKind.Deletion: + case CompletionTriggerKind.Insertion: + case CompletionTriggerKind.Deletion: - if (ShouldTriggerCompletion(document.Project, document.Project.Services, text, caretPosition, trigger, options, passThroughOptions, roles)) - { - var triggeredProviders = providers.Where(p => p.ShouldTriggerCompletion(document.Project.Services, text, caretPosition, trigger, options, passThroughOptions)).ToImmutableArrayOrEmpty(); + if (ShouldTriggerCompletion(document.Project, document.Project.Services, text, caretPosition, trigger, options, passThroughOptions, roles)) + { + var triggeredProviders = providers.Where(p => p.ShouldTriggerCompletion(document.Project.Services, text, caretPosition, trigger, options, passThroughOptions)).ToImmutableArrayOrEmpty(); - Debug.Assert(ValidatePossibleTriggerCharacterSet(trigger.Kind, triggeredProviders, document, text, caretPosition, options)); - return triggeredProviders.IsEmpty ? providers.ToImmutableArray() : triggeredProviders; - } + Debug.Assert(ValidatePossibleTriggerCharacterSet(trigger.Kind, triggeredProviders, document, text, caretPosition, options)); + return triggeredProviders.IsEmpty ? providers.ToImmutableArray() : triggeredProviders; + } - return []; + return []; - default: - return providers.ToImmutableArray(); - } + default: + return providers.ToImmutableArray(); } + } - static async Task> GetAugmentingProvidersAsync( - Document document, ImmutableArray triggeredProviders, int caretPosition, CompletionTrigger trigger, CompletionOptions options, CancellationToken cancellationToken) + static async Task> GetAugmentingProvidersAsync( + Document document, ImmutableArray triggeredProviders, int caretPosition, CompletionTrigger trigger, CompletionOptions options, CancellationToken cancellationToken) + { + var extensionManager = document.Project.Solution.Workspace.Services.GetRequiredService(); + var additionalAugmentingProviders = ArrayBuilder.GetInstance(triggeredProviders.Length); + if (trigger.Kind == CompletionTriggerKind.Insertion) { - var extensionManager = document.Project.Solution.Workspace.Services.GetRequiredService(); - var additionalAugmentingProviders = ArrayBuilder.GetInstance(triggeredProviders.Length); - if (trigger.Kind == CompletionTriggerKind.Insertion) + foreach (var provider in triggeredProviders) { - foreach (var provider in triggeredProviders) - { - var isSyntacticTrigger = await extensionManager.PerformFunctionAsync( - provider, - () => provider.IsSyntacticTriggerCharacterAsync(document, caretPosition, trigger, options, cancellationToken), - defaultValue: false).ConfigureAwait(false); - if (!isSyntacticTrigger) - additionalAugmentingProviders.Add(provider); - } + var isSyntacticTrigger = await extensionManager.PerformFunctionAsync( + provider, + () => provider.IsSyntacticTriggerCharacterAsync(document, caretPosition, trigger, options, cancellationToken), + defaultValue: false).ConfigureAwait(false); + if (!isSyntacticTrigger) + additionalAugmentingProviders.Add(provider); } - - return additionalAugmentingProviders.ToImmutableAndFree(); } + + return additionalAugmentingProviders.ToImmutableAndFree(); } + } - /// - /// Returns a document with frozen partial semantic unless we already have a complete compilation available. - /// Getting full semantic could be costly in certain scenarios and would cause significant delay in completion. - /// In most cases we'd still end up with complete document, but we'd consider it an acceptable trade-off even when - /// we get into this transient state. - /// - private async Task<(Document document, SemanticModel? semanticModel)> GetDocumentWithFrozenPartialSemanticsAsync(Document document, CancellationToken cancellationToken) + /// + /// Returns a document with frozen partial semantic unless we already have a complete compilation available. + /// Getting full semantic could be costly in certain scenarios and would cause significant delay in completion. + /// In most cases we'd still end up with complete document, but we'd consider it an acceptable trade-off even when + /// we get into this transient state. + /// + private async Task<(Document document, SemanticModel? semanticModel)> GetDocumentWithFrozenPartialSemanticsAsync(Document document, CancellationToken cancellationToken) + { + if (_suppressPartialSemantics) { - if (_suppressPartialSemantics) - { - return (document, await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false)); - } + return (document, await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false)); + } + + return await document.GetFullOrPartialSemanticModelAsync(cancellationToken).ConfigureAwait(false); + } - return await document.GetFullOrPartialSemanticModelAsync(cancellationToken).ConfigureAwait(false); + private static bool ValidatePossibleTriggerCharacterSet(CompletionTriggerKind completionTriggerKind, IEnumerable triggeredProviders, + Document document, SourceText text, int caretPosition, in CompletionOptions options) + { + // Only validate on insertion triggers. + if (completionTriggerKind != CompletionTriggerKind.Insertion) + { + return true; } - private static bool ValidatePossibleTriggerCharacterSet(CompletionTriggerKind completionTriggerKind, IEnumerable triggeredProviders, - Document document, SourceText text, int caretPosition, in CompletionOptions options) + var syntaxFactsService = document.GetLanguageService(); + if (caretPosition > 0 && syntaxFactsService != null) { - // Only validate on insertion triggers. - if (completionTriggerKind != CompletionTriggerKind.Insertion) + // The trigger character has already been inserted before the current caret position. + var character = text[caretPosition - 1]; + + // Identifier characters are not part of the possible trigger character set, so don't validate them. + var isIdentifierCharacter = syntaxFactsService.IsIdentifierStartCharacter(character) || syntaxFactsService.IsIdentifierEscapeCharacter(character); + if (isIdentifierCharacter) { return true; } - var syntaxFactsService = document.GetLanguageService(); - if (caretPosition > 0 && syntaxFactsService != null) + // Only verify against built in providers. 3rd party ones do not necessarily implement the possible trigger characters API. + foreach (var provider in triggeredProviders) { - // The trigger character has already been inserted before the current caret position. - var character = text[caretPosition - 1]; - - // Identifier characters are not part of the possible trigger character set, so don't validate them. - var isIdentifierCharacter = syntaxFactsService.IsIdentifierStartCharacter(character) || syntaxFactsService.IsIdentifierEscapeCharacter(character); - if (isIdentifierCharacter) + if (provider is LSPCompletionProvider lspProvider && lspProvider.IsInsertionTrigger(text, caretPosition - 1, options)) { - return true; - } - - // Only verify against built in providers. 3rd party ones do not necessarily implement the possible trigger characters API. - foreach (var provider in triggeredProviders) - { - if (provider is LSPCompletionProvider lspProvider && lspProvider.IsInsertionTrigger(text, caretPosition - 1, options)) + if (!lspProvider.TriggerCharacters.Contains(character)) { - if (!lspProvider.TriggerCharacters.Contains(character)) - { - Debug.Assert(lspProvider.TriggerCharacters.Contains(character), - $"the character {character} is not a valid trigger character for {lspProvider.Name}"); - } + Debug.Assert(lspProvider.TriggerCharacters.Contains(character), + $"the character {character} is not a valid trigger character for {lspProvider.Name}"); } } } - - return true; } - private static bool HasAnyItems(CompletionContext cc) - => cc.Items.Count > 0 || cc.SuggestionModeItem != null; - - private static async Task> ComputeNonEmptyCompletionContextsAsync( - Document document, int caretPosition, CompletionTrigger trigger, - CompletionOptions options, TextSpan completionListSpan, - ImmutableArray providers, - SharedSyntaxContextsWithSpeculativeModel sharedContext, - CancellationToken cancellationToken) - { - var completionContextTasks = new List>(); - foreach (var provider in providers) - { - completionContextTasks.Add(GetContextAsync( - provider, document, caretPosition, trigger, - options, completionListSpan, sharedContext, cancellationToken)); - } + return true; + } - var completionContexts = await Task.WhenAll(completionContextTasks).ConfigureAwait(false); - return completionContexts.Where(HasAnyItems).ToImmutableArray(); - } + private static bool HasAnyItems(CompletionContext cc) + => cc.Items.Count > 0 || cc.SuggestionModeItem != null; - private CompletionList MergeAndPruneCompletionLists( - ImmutableArray completionContexts, - in CompletionOptions options, - bool isExclusive) + private static async Task> ComputeNonEmptyCompletionContextsAsync( + Document document, int caretPosition, CompletionTrigger trigger, + CompletionOptions options, TextSpan completionListSpan, + ImmutableArray providers, + SharedSyntaxContextsWithSpeculativeModel sharedContext, + CancellationToken cancellationToken) + { + var completionContextTasks = new List>(); + foreach (var provider in providers) { - Debug.Assert(!completionContexts.IsDefaultOrEmpty); + completionContextTasks.Add(GetContextAsync( + provider, document, caretPosition, trigger, + options, completionListSpan, sharedContext, cancellationToken)); + } - using var displayNameToItemsMap = new DisplayNameToItemsMap(this); - CompletionItem? suggestionModeItem = null; + var completionContexts = await Task.WhenAll(completionContextTasks).ConfigureAwait(false); + return completionContexts.Where(HasAnyItems).ToImmutableArray(); + } - foreach (var context in completionContexts) - { - foreach (var item in context.Items) - { - Debug.Assert(item != null); - displayNameToItemsMap.Add(item); - } + private CompletionList MergeAndPruneCompletionLists( + ImmutableArray completionContexts, + in CompletionOptions options, + bool isExclusive) + { + Debug.Assert(!completionContexts.IsDefaultOrEmpty); - // first one wins - suggestionModeItem ??= context.SuggestionModeItem; - } + using var displayNameToItemsMap = new DisplayNameToItemsMap(this); + CompletionItem? suggestionModeItem = null; - if (displayNameToItemsMap.IsEmpty) + foreach (var context in completionContexts) + { + foreach (var item in context.Items) { - return CompletionList.Empty; + Debug.Assert(item != null); + displayNameToItemsMap.Add(item); } - return CompletionList.Create( - completionContexts[0].CompletionListSpan, // All contexts have the same completion list span. - displayNameToItemsMap.ToSegmentedList(options), - GetRules(options), - suggestionModeItem, - isExclusive); + // first one wins + suggestionModeItem ??= context.SuggestionModeItem; } - /// - /// Determines if the items are similar enough they should be represented by a single item in the list. - /// - protected virtual bool ItemsMatch(CompletionItem item, CompletionItem existingItem) + if (displayNameToItemsMap.IsEmpty) { - return item.Span == existingItem.Span - && item.SortText == existingItem.SortText && item.InlineDescription == existingItem.InlineDescription; + return CompletionList.Empty; } - /// - /// Determines which of two items should represent the matching pair. - /// - protected virtual CompletionItem GetBetterItem(CompletionItem item, CompletionItem existingItem) - { - // the item later in the sort order (determined by provider order) wins? - return item; - } + return CompletionList.Create( + completionContexts[0].CompletionListSpan, // All contexts have the same completion list span. + displayNameToItemsMap.ToSegmentedList(options), + GetRules(options), + suggestionModeItem, + isExclusive); + } - private static Dictionary GetCompletionProviderToIndex(ConcatImmutableArray completionProviders) - { - var result = new Dictionary(completionProviders.Length); + /// + /// Determines if the items are similar enough they should be represented by a single item in the list. + /// + protected virtual bool ItemsMatch(CompletionItem item, CompletionItem existingItem) + { + return item.Span == existingItem.Span + && item.SortText == existingItem.SortText && item.InlineDescription == existingItem.InlineDescription; + } - var i = 0; - foreach (var completionProvider in completionProviders) - { - result[completionProvider] = i; - i++; - } + /// + /// Determines which of two items should represent the matching pair. + /// + protected virtual CompletionItem GetBetterItem(CompletionItem item, CompletionItem existingItem) + { + // the item later in the sort order (determined by provider order) wins? + return item; + } - return result; - } + private static Dictionary GetCompletionProviderToIndex(ConcatImmutableArray completionProviders) + { + var result = new Dictionary(completionProviders.Length); - private static async Task GetContextAsync( - CompletionProvider provider, - Document document, - int position, - CompletionTrigger triggerInfo, - CompletionOptions options, - TextSpan defaultSpan, - SharedSyntaxContextsWithSpeculativeModel? sharedContext, - CancellationToken cancellationToken) + var i = 0; + foreach (var completionProvider in completionProviders) { - var extensionManager = document.Project.Solution.Workspace.Services.GetRequiredService(); + result[completionProvider] = i; + i++; + } - var context = new CompletionContext(provider, document, position, sharedContext, defaultSpan, triggerInfo, options, cancellationToken); + return result; + } - // Wrap with extension manager call. This will ensure this provider is not disabled. If not, it will ask - // it for completions. If that throws, then the provider will be moved to the disabled state. - await extensionManager.PerformActionAsync( - provider, - () => provider.ProvideCompletionsAsync(context)).ConfigureAwait(false); + private static async Task GetContextAsync( + CompletionProvider provider, + Document document, + int position, + CompletionTrigger triggerInfo, + CompletionOptions options, + TextSpan defaultSpan, + SharedSyntaxContextsWithSpeculativeModel? sharedContext, + CancellationToken cancellationToken) + { + var extensionManager = document.Project.Solution.Workspace.Services.GetRequiredService(); - return context; - } + var context = new CompletionContext(provider, document, position, sharedContext, defaultSpan, triggerInfo, options, cancellationToken); - private class DisplayNameToItemsMap(CompletionService service) : IEnumerable, IDisposable - { - // We might need to handle large amount of items with import completion enabled, - // so use a dedicated pool to minimize array allocations. Set the size of pool to a small - // number 5 because we don't expect more than a couple of callers at the same time. - private static readonly ObjectPool> s_uniqueSourcesPool = new(factory: () => [], size: 5); - private static readonly ObjectPool> s_sortListPool = new(factory: () => [], size: 5); + // Wrap with extension manager call. This will ensure this provider is not disabled. If not, it will ask + // it for completions. If that throws, then the provider will be moved to the disabled state. + await extensionManager.PerformActionAsync( + provider, + () => provider.ProvideCompletionsAsync(context)).ConfigureAwait(false); + + return context; + } - private readonly Dictionary _displayNameToItemsMap = s_uniqueSourcesPool.Allocate(); - private readonly CompletionService _service = service; + private class DisplayNameToItemsMap(CompletionService service) : IEnumerable, IDisposable + { + // We might need to handle large amount of items with import completion enabled, + // so use a dedicated pool to minimize array allocations. Set the size of pool to a small + // number 5 because we don't expect more than a couple of callers at the same time. + private static readonly ObjectPool> s_uniqueSourcesPool = new(factory: () => [], size: 5); + private static readonly ObjectPool> s_sortListPool = new(factory: () => [], size: 5); - public int Count { get; private set; } + private readonly Dictionary _displayNameToItemsMap = s_uniqueSourcesPool.Allocate(); + private readonly CompletionService _service = service; - public SegmentedList ToSegmentedList(in CompletionOptions options) - { - if (!options.PerformSort) - { - return new(this); - } + public int Count { get; private set; } - // Use a list to do the sorting as it's significantly faster than doing so on a SegmentedList. - var list = s_sortListPool.Allocate(); - try - { - list.AddRange(this); - list.Sort(); - return new(list); - } - finally - { - list.Clear(); - s_sortListPool.Free(list); - } + public SegmentedList ToSegmentedList(in CompletionOptions options) + { + if (!options.PerformSort) + { + return new(this); } - public void Dispose() + // Use a list to do the sorting as it's significantly faster than doing so on a SegmentedList. + var list = s_sortListPool.Allocate(); + try { - _displayNameToItemsMap.Clear(); - s_uniqueSourcesPool.Free(_displayNameToItemsMap); + list.AddRange(this); + list.Sort(); + return new(list); } + finally + { + list.Clear(); + s_sortListPool.Free(list); + } + } - public bool IsEmpty => _displayNameToItemsMap.Count == 0; + public void Dispose() + { + _displayNameToItemsMap.Clear(); + s_uniqueSourcesPool.Free(_displayNameToItemsMap); + } - public void Add(CompletionItem item) + public bool IsEmpty => _displayNameToItemsMap.Count == 0; + + public void Add(CompletionItem item) + { + var entireDisplayText = item.GetEntireDisplayText(); + + if (!_displayNameToItemsMap.TryGetValue(entireDisplayText, out var value)) { - var entireDisplayText = item.GetEntireDisplayText(); + Count++; + _displayNameToItemsMap.Add(entireDisplayText, item); + return; + } - if (!_displayNameToItemsMap.TryGetValue(entireDisplayText, out var value)) + // If two items have the same display text choose which one to keep. + // If they don't actually match keep both. + if (value is CompletionItem sameNamedItem) + { + if (_service.ItemsMatch(item, sameNamedItem)) { - Count++; - _displayNameToItemsMap.Add(entireDisplayText, item); + _displayNameToItemsMap[entireDisplayText] = _service.GetBetterItem(item, sameNamedItem); return; } - // If two items have the same display text choose which one to keep. - // If they don't actually match keep both. - if (value is CompletionItem sameNamedItem) + Count++; + // Matching items should be rare, no need to use object pool for this. + _displayNameToItemsMap[entireDisplayText] = new List() { sameNamedItem, item }; + } + else if (value is List sameNamedItems) + { + for (var i = 0; i < sameNamedItems.Count; i++) { - if (_service.ItemsMatch(item, sameNamedItem)) + var existingItem = sameNamedItems[i]; + if (_service.ItemsMatch(item, existingItem)) { - _displayNameToItemsMap[entireDisplayText] = _service.GetBetterItem(item, sameNamedItem); + sameNamedItems[i] = _service.GetBetterItem(item, existingItem); return; } - - Count++; - // Matching items should be rare, no need to use object pool for this. - _displayNameToItemsMap[entireDisplayText] = new List() { sameNamedItem, item }; } - else if (value is List sameNamedItems) - { - for (var i = 0; i < sameNamedItems.Count; i++) - { - var existingItem = sameNamedItems[i]; - if (_service.ItemsMatch(item, existingItem)) - { - sameNamedItems[i] = _service.GetBetterItem(item, existingItem); - return; - } - } - Count++; - sameNamedItems.Add(item); - } + Count++; + sameNamedItems.Add(item); } + } - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() + { + foreach (var value in _displayNameToItemsMap.Values) { - foreach (var value in _displayNameToItemsMap.Values) + if (value is CompletionItem sameNamedItem) { - if (value is CompletionItem sameNamedItem) - { - yield return sameNamedItem; - } - else if (value is List sameNamedItems) + yield return sameNamedItem; + } + else if (value is List sameNamedItems) + { + foreach (var item in sameNamedItems) { - foreach (var item in sameNamedItems) - { - yield return item; - } + yield return item; } } } + } - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); } } } diff --git a/src/Features/Core/Portable/Completion/CompletionTags.cs b/src/Features/Core/Portable/Completion/CompletionTags.cs index 028edb5798780..946c0d735fee0 100644 --- a/src/Features/Core/Portable/Completion/CompletionTags.cs +++ b/src/Features/Core/Portable/Completion/CompletionTags.cs @@ -6,57 +6,56 @@ using System.ComponentModel; using Microsoft.CodeAnalysis.Tags; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// The set of well known tags used for the property. +/// These tags influence the presentation of items in the list. +/// +[Obsolete("Use Microsoft.CodeAnalysis.Tags.WellKnownTags instead.")] +[EditorBrowsable(EditorBrowsableState.Never)] +public static class CompletionTags { - /// - /// The set of well known tags used for the property. - /// These tags influence the presentation of items in the list. - /// - [Obsolete("Use Microsoft.CodeAnalysis.Tags.WellKnownTags instead.")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static class CompletionTags - { - // accessibility - public const string Public = WellKnownTags.Public; - public const string Protected = WellKnownTags.Protected; - public const string Private = WellKnownTags.Private; - public const string Internal = WellKnownTags.Internal; + // accessibility + public const string Public = WellKnownTags.Public; + public const string Protected = WellKnownTags.Protected; + public const string Private = WellKnownTags.Private; + public const string Internal = WellKnownTags.Internal; - // project elements - public const string File = WellKnownTags.File; - public const string Project = WellKnownTags.Project; - public const string Folder = WellKnownTags.Folder; - public const string Assembly = WellKnownTags.Assembly; + // project elements + public const string File = WellKnownTags.File; + public const string Project = WellKnownTags.Project; + public const string Folder = WellKnownTags.Folder; + public const string Assembly = WellKnownTags.Assembly; - // language elements - public const string Class = WellKnownTags.Class; - public const string Constant = WellKnownTags.Constant; - public const string Delegate = WellKnownTags.Delegate; - public const string Enum = WellKnownTags.Enum; - public const string EnumMember = WellKnownTags.EnumMember; - public const string Event = WellKnownTags.Event; - public const string ExtensionMethod = WellKnownTags.ExtensionMethod; - public const string Field = WellKnownTags.Field; - public const string Interface = WellKnownTags.Interface; - public const string Intrinsic = WellKnownTags.Intrinsic; - public const string Keyword = WellKnownTags.Keyword; - public const string Label = WellKnownTags.Label; - public const string Local = WellKnownTags.Local; - public const string Namespace = WellKnownTags.Namespace; - public const string Method = WellKnownTags.Method; - public const string Module = WellKnownTags.Module; - public const string Operator = WellKnownTags.Operator; - public const string Parameter = WellKnownTags.Parameter; - public const string Property = WellKnownTags.Property; - public const string RangeVariable = WellKnownTags.RangeVariable; - public const string Reference = WellKnownTags.Reference; - public const string Structure = WellKnownTags.Structure; - public const string TypeParameter = WellKnownTags.TypeParameter; + // language elements + public const string Class = WellKnownTags.Class; + public const string Constant = WellKnownTags.Constant; + public const string Delegate = WellKnownTags.Delegate; + public const string Enum = WellKnownTags.Enum; + public const string EnumMember = WellKnownTags.EnumMember; + public const string Event = WellKnownTags.Event; + public const string ExtensionMethod = WellKnownTags.ExtensionMethod; + public const string Field = WellKnownTags.Field; + public const string Interface = WellKnownTags.Interface; + public const string Intrinsic = WellKnownTags.Intrinsic; + public const string Keyword = WellKnownTags.Keyword; + public const string Label = WellKnownTags.Label; + public const string Local = WellKnownTags.Local; + public const string Namespace = WellKnownTags.Namespace; + public const string Method = WellKnownTags.Method; + public const string Module = WellKnownTags.Module; + public const string Operator = WellKnownTags.Operator; + public const string Parameter = WellKnownTags.Parameter; + public const string Property = WellKnownTags.Property; + public const string RangeVariable = WellKnownTags.RangeVariable; + public const string Reference = WellKnownTags.Reference; + public const string Structure = WellKnownTags.Structure; + public const string TypeParameter = WellKnownTags.TypeParameter; - // other - public const string Snippet = WellKnownTags.Snippet; - public const string Error = WellKnownTags.Error; - public const string Warning = WellKnownTags.Warning; - internal const string StatusInformation = WellKnownTags.StatusInformation; - } + // other + public const string Snippet = WellKnownTags.Snippet; + public const string Error = WellKnownTags.Error; + public const string Warning = WellKnownTags.Warning; + internal const string StatusInformation = WellKnownTags.StatusInformation; } diff --git a/src/Features/Core/Portable/Completion/CompletionTrigger.cs b/src/Features/Core/Portable/Completion/CompletionTrigger.cs index 5648b3641cf44..879c480fc0f10 100644 --- a/src/Features/Core/Portable/Completion/CompletionTrigger.cs +++ b/src/Features/Core/Portable/Completion/CompletionTrigger.cs @@ -4,60 +4,59 @@ using System; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// The action that triggered completion to start. +/// +/// +/// NOTE: Roslyn's LSP completion implementation uses this struct. If a new property is added, either: +/// 1: The property's type must be serializable +/// OR +/// 2. LSP will need to be updated to not use CompletionTrigger - see +/// Features\LanguageServer\Protocol\Handler\Completion\CompletionResolveData.cs +/// +public readonly struct CompletionTrigger { /// - /// The action that triggered completion to start. + /// The reason that completion was started. /// - /// - /// NOTE: Roslyn's LSP completion implementation uses this struct. If a new property is added, either: - /// 1: The property's type must be serializable - /// OR - /// 2. LSP will need to be updated to not use CompletionTrigger - see - /// Features\LanguageServer\Protocol\Handler\Completion\CompletionResolveData.cs - /// - public readonly struct CompletionTrigger + public CompletionTriggerKind Kind { get; } + + /// + /// The character associated with the triggering action. + /// + public char Character { get; } + + internal CompletionTrigger(CompletionTriggerKind kind, char character = (char)0) + : this() { - /// - /// The reason that completion was started. - /// - public CompletionTriggerKind Kind { get; } - - /// - /// The character associated with the triggering action. - /// - public char Character { get; } - - internal CompletionTrigger(CompletionTriggerKind kind, char character = (char)0) - : this() - { - Kind = kind; - Character = character; - } - - /// - /// Do not use. Use instead. - /// - [Obsolete("Use 'Invoke' instead.")] - public static readonly CompletionTrigger Default = - new(CompletionTriggerKind.Other); - - /// - /// The default when none is specified. - /// - public static readonly CompletionTrigger Invoke = - new(CompletionTriggerKind.Invoke); - - /// - /// Creates a new instance of a association with the insertion of a typed character into the document. - /// - public static CompletionTrigger CreateInsertionTrigger(char insertedCharacter) - => new(CompletionTriggerKind.Insertion, insertedCharacter); - - /// - /// Creates a new instance of a association with the deletion of a character from the document. - /// - public static CompletionTrigger CreateDeletionTrigger(char deletedCharacter) - => new(CompletionTriggerKind.Deletion, deletedCharacter); + Kind = kind; + Character = character; } + + /// + /// Do not use. Use instead. + /// + [Obsolete("Use 'Invoke' instead.")] + public static readonly CompletionTrigger Default = + new(CompletionTriggerKind.Other); + + /// + /// The default when none is specified. + /// + public static readonly CompletionTrigger Invoke = + new(CompletionTriggerKind.Invoke); + + /// + /// Creates a new instance of a association with the insertion of a typed character into the document. + /// + public static CompletionTrigger CreateInsertionTrigger(char insertedCharacter) + => new(CompletionTriggerKind.Insertion, insertedCharacter); + + /// + /// Creates a new instance of a association with the deletion of a character from the document. + /// + public static CompletionTrigger CreateDeletionTrigger(char deletedCharacter) + => new(CompletionTriggerKind.Deletion, deletedCharacter); } diff --git a/src/Features/Core/Portable/Completion/CompletionTriggerKind.cs b/src/Features/Core/Portable/Completion/CompletionTriggerKind.cs index 75e62ac07b496..663fc46432978 100644 --- a/src/Features/Core/Portable/Completion/CompletionTriggerKind.cs +++ b/src/Features/Core/Portable/Completion/CompletionTriggerKind.cs @@ -4,44 +4,43 @@ using System; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// The kind of action that triggered completion to start. +/// +public enum CompletionTriggerKind { /// - /// The kind of action that triggered completion to start. + /// Completion was triggered via some other mechanism. + /// + [Obsolete("Use 'Invoke' instead.")] + Other = 0, + + /// + /// Completion was trigger by a direct invocation of the completion feature + /// (ctrl-j in Visual Studio). + /// + Invoke = 0, + + /// + /// Completion was triggered via an action inserting a character into the document. + /// + Insertion = 1, + + /// + /// Completion was triggered via an action deleting a character from the document. + /// + Deletion = 2, + + /// + /// Completion was triggered for snippets only. + /// + Snippets = 3, + + /// + /// Completion was triggered with a request to commit if a unique item would be selected + /// (ctrl-space in Visual Studio). /// - public enum CompletionTriggerKind - { - /// - /// Completion was triggered via some other mechanism. - /// - [Obsolete("Use 'Invoke' instead.")] - Other = 0, - - /// - /// Completion was trigger by a direct invocation of the completion feature - /// (ctrl-j in Visual Studio). - /// - Invoke = 0, - - /// - /// Completion was triggered via an action inserting a character into the document. - /// - Insertion = 1, - - /// - /// Completion was triggered via an action deleting a character from the document. - /// - Deletion = 2, - - /// - /// Completion was triggered for snippets only. - /// - Snippets = 3, - - /// - /// Completion was triggered with a request to commit if a unique item would be selected - /// (ctrl-space in Visual Studio). - /// - InvokeAndCommitIfUnique = 4 - } + InvokeAndCommitIfUnique = 4 } diff --git a/src/Features/Core/Portable/Completion/EnterKeyRule.cs b/src/Features/Core/Portable/Completion/EnterKeyRule.cs index 9ce237025fdd7..332f9751e5422 100644 --- a/src/Features/Core/Portable/Completion/EnterKeyRule.cs +++ b/src/Features/Core/Portable/Completion/EnterKeyRule.cs @@ -2,28 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// Determines whether the enter key is passed through to the editor after it has been used to commit a completion item. +/// +public enum EnterKeyRule { + Default = 0, + /// - /// Determines whether the enter key is passed through to the editor after it has been used to commit a completion item. + /// The enter key is never passed through to the editor after it has been used to commit the completion item. /// - public enum EnterKeyRule - { - Default = 0, + Never, - /// - /// The enter key is never passed through to the editor after it has been used to commit the completion item. - /// - Never, - - /// - /// The enter key is always passed through to the editor after it has been used to commit the completion item. - /// - Always, + /// + /// The enter key is always passed through to the editor after it has been used to commit the completion item. + /// + Always, - /// - /// The enter is key only passed through to the editor if the completion item has been fully typed out. - /// - AfterFullyTypedWord - } + /// + /// The enter is key only passed through to the editor if the completion item has been fully typed out. + /// + AfterFullyTypedWord } diff --git a/src/Features/Core/Portable/Completion/ExpandedCompletionMode.cs b/src/Features/Core/Portable/Completion/ExpandedCompletionMode.cs index 56a7abbd701a0..6b5e97510547f 100644 --- a/src/Features/Core/Portable/Completion/ExpandedCompletionMode.cs +++ b/src/Features/Core/Portable/Completion/ExpandedCompletionMode.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal enum ExpandedCompletionMode { - internal enum ExpandedCompletionMode - { - NonExpandedItemsOnly, - ExpandedItemsOnly, - AllItems - } + NonExpandedItemsOnly, + ExpandedItemsOnly, + AllItems } diff --git a/src/Features/Core/Portable/Completion/ExportArgumentProviderAttribute.cs b/src/Features/Core/Portable/Completion/ExportArgumentProviderAttribute.cs index 7041e0bae4aa6..311ba7dbb85a4 100644 --- a/src/Features/Core/Portable/Completion/ExportArgumentProviderAttribute.cs +++ b/src/Features/Core/Portable/Completion/ExportArgumentProviderAttribute.cs @@ -5,14 +5,13 @@ using System; using System.Composition; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +[MetadataAttribute] +[AttributeUsage(AttributeTargets.Class)] +internal sealed class ExportArgumentProviderAttribute(string name, string language) : ExportAttribute(typeof(ArgumentProvider)) { - [MetadataAttribute] - [AttributeUsage(AttributeTargets.Class)] - internal sealed class ExportArgumentProviderAttribute(string name, string language) : ExportAttribute(typeof(ArgumentProvider)) - { - public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); - public string Language { get; } = language ?? throw new ArgumentNullException(nameof(language)); - public string[] Roles { get; set; } = []; - } + public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); + public string Language { get; } = language ?? throw new ArgumentNullException(nameof(language)); + public string[] Roles { get; set; } = []; } diff --git a/src/Features/Core/Portable/Completion/ExportCompletionProviderAttribute.cs b/src/Features/Core/Portable/Completion/ExportCompletionProviderAttribute.cs index 0defcaee6d004..1c13d565f91ca 100644 --- a/src/Features/Core/Portable/Completion/ExportCompletionProviderAttribute.cs +++ b/src/Features/Core/Portable/Completion/ExportCompletionProviderAttribute.cs @@ -5,18 +5,17 @@ using System; using System.Composition; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// Use this attribute to export a so that it will +/// be found and used by the per language associated . +/// +[MetadataAttribute] +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class ExportCompletionProviderAttribute(string name, string language) : ExportAttribute(typeof(CompletionProvider)) { - /// - /// Use this attribute to export a so that it will - /// be found and used by the per language associated . - /// - [MetadataAttribute] - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class ExportCompletionProviderAttribute(string name, string language) : ExportAttribute(typeof(CompletionProvider)) - { - public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); - public string Language { get; } = language ?? throw new ArgumentNullException(nameof(language)); - public string[]? Roles { get; set; } - } + public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); + public string Language { get; } = language ?? throw new ArgumentNullException(nameof(language)); + public string[]? Roles { get; set; } } diff --git a/src/Features/Core/Portable/Completion/FileSystemCompletionHelper.cs b/src/Features/Core/Portable/Completion/FileSystemCompletionHelper.cs index 21f882dc0c467..5b757b529b076 100644 --- a/src/Features/Core/Portable/Completion/FileSystemCompletionHelper.cs +++ b/src/Features/Core/Portable/Completion/FileSystemCompletionHelper.cs @@ -16,252 +16,251 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal class FileSystemCompletionHelper { - internal class FileSystemCompletionHelper - { - private static readonly char[] s_windowsDirectorySeparator = ['\\']; + private static readonly char[] s_windowsDirectorySeparator = ['\\']; - private readonly Glyph _folderGlyph; - private readonly Glyph _fileGlyph; + private readonly Glyph _folderGlyph; + private readonly Glyph _fileGlyph; - // absolute paths - private readonly ImmutableArray _searchPaths; - private readonly string? _baseDirectory; + // absolute paths + private readonly ImmutableArray _searchPaths; + private readonly string? _baseDirectory; - private readonly ImmutableArray _allowableExtensions; - private readonly CompletionItemRules _itemRules; + private readonly ImmutableArray _allowableExtensions; + private readonly CompletionItemRules _itemRules; - public FileSystemCompletionHelper( - Glyph folderGlyph, - Glyph fileGlyph, - ImmutableArray searchPaths, - string? baseDirectory, - ImmutableArray allowableExtensions, - CompletionItemRules itemRules) - { - Debug.Assert(searchPaths.All(PathUtilities.IsAbsolute)); - Debug.Assert(baseDirectory == null || PathUtilities.IsAbsolute(baseDirectory)); - - _searchPaths = searchPaths; - _baseDirectory = baseDirectory; - _allowableExtensions = allowableExtensions; - _folderGlyph = folderGlyph; - _fileGlyph = fileGlyph; - _itemRules = itemRules; - } + public FileSystemCompletionHelper( + Glyph folderGlyph, + Glyph fileGlyph, + ImmutableArray searchPaths, + string? baseDirectory, + ImmutableArray allowableExtensions, + CompletionItemRules itemRules) + { + Debug.Assert(searchPaths.All(PathUtilities.IsAbsolute)); + Debug.Assert(baseDirectory == null || PathUtilities.IsAbsolute(baseDirectory)); + + _searchPaths = searchPaths; + _baseDirectory = baseDirectory; + _allowableExtensions = allowableExtensions; + _folderGlyph = folderGlyph; + _fileGlyph = fileGlyph; + _itemRules = itemRules; + } - // virtual for testing - protected virtual string[] GetLogicalDrives() - => IOUtilities.PerformIO(Directory.GetLogicalDrives, []); + // virtual for testing + protected virtual string[] GetLogicalDrives() + => IOUtilities.PerformIO(Directory.GetLogicalDrives, []); - // virtual for testing - protected virtual bool DirectoryExists(string fullPath) - { - Debug.Assert(PathUtilities.IsAbsolute(fullPath)); - return Directory.Exists(fullPath); - } + // virtual for testing + protected virtual bool DirectoryExists(string fullPath) + { + Debug.Assert(PathUtilities.IsAbsolute(fullPath)); + return Directory.Exists(fullPath); + } - // virtual for testing - protected virtual IEnumerable EnumerateDirectories(string fullDirectoryPath) - { - Debug.Assert(PathUtilities.IsAbsolute(fullDirectoryPath)); - return IOUtilities.PerformIO(() => Directory.EnumerateDirectories(fullDirectoryPath), []); - } + // virtual for testing + protected virtual IEnumerable EnumerateDirectories(string fullDirectoryPath) + { + Debug.Assert(PathUtilities.IsAbsolute(fullDirectoryPath)); + return IOUtilities.PerformIO(() => Directory.EnumerateDirectories(fullDirectoryPath), []); + } - // virtual for testing - protected virtual IEnumerable EnumerateFiles(string fullDirectoryPath) - { - Debug.Assert(PathUtilities.IsAbsolute(fullDirectoryPath)); - return IOUtilities.PerformIO(() => Directory.EnumerateFiles(fullDirectoryPath), []); - } + // virtual for testing + protected virtual IEnumerable EnumerateFiles(string fullDirectoryPath) + { + Debug.Assert(PathUtilities.IsAbsolute(fullDirectoryPath)); + return IOUtilities.PerformIO(() => Directory.EnumerateFiles(fullDirectoryPath), []); + } - // virtual for testing - protected virtual bool IsVisibleFileSystemEntry(string fullPath) + // virtual for testing + protected virtual bool IsVisibleFileSystemEntry(string fullPath) + { + Debug.Assert(PathUtilities.IsAbsolute(fullPath)); + return IOUtilities.PerformIO(() => (File.GetAttributes(fullPath) & (FileAttributes.Hidden | FileAttributes.System)) == 0, false); + } + + private CompletionItem CreateNetworkRoot() + => CommonCompletionItem.Create( + "\\\\", + displayTextSuffix: "", + glyph: null, + description: "\\\\".ToSymbolDisplayParts(), + rules: _itemRules); + + private CompletionItem CreateUnixRoot() + => CommonCompletionItem.Create( + "/", + displayTextSuffix: "", + glyph: _folderGlyph, + description: "/".ToSymbolDisplayParts(), + rules: _itemRules); + + private CompletionItem CreateFileSystemEntryItem(string fullPath, bool isDirectory) + => CommonCompletionItem.Create( + PathUtilities.GetFileName(fullPath), + displayTextSuffix: "", + glyph: isDirectory ? _folderGlyph : _fileGlyph, + description: fullPath.ToSymbolDisplayParts(), + rules: _itemRules); + + private CompletionItem CreateLogicalDriveItem(string drive) + => CommonCompletionItem.Create( + drive, + displayTextSuffix: "", + glyph: _folderGlyph, + description: drive.ToSymbolDisplayParts(), + rules: _itemRules); + + public Task> GetItemsAsync(string directoryPath, CancellationToken cancellationToken) + => Task.Run(() => GetItems(directoryPath, cancellationToken), cancellationToken); + + private ImmutableArray GetItems(string directoryPath, CancellationToken cancellationToken) + { + if (!PathUtilities.IsUnixLikePlatform && directoryPath == "\\") { - Debug.Assert(PathUtilities.IsAbsolute(fullPath)); - return IOUtilities.PerformIO(() => (File.GetAttributes(fullPath) & (FileAttributes.Hidden | FileAttributes.System)) == 0, false); + // The user has typed only "\". In this case, we want to add "\\" to the list. + return [CreateNetworkRoot()]; } - private CompletionItem CreateNetworkRoot() - => CommonCompletionItem.Create( - "\\\\", - displayTextSuffix: "", - glyph: null, - description: "\\\\".ToSymbolDisplayParts(), - rules: _itemRules); - - private CompletionItem CreateUnixRoot() - => CommonCompletionItem.Create( - "/", - displayTextSuffix: "", - glyph: _folderGlyph, - description: "/".ToSymbolDisplayParts(), - rules: _itemRules); - - private CompletionItem CreateFileSystemEntryItem(string fullPath, bool isDirectory) - => CommonCompletionItem.Create( - PathUtilities.GetFileName(fullPath), - displayTextSuffix: "", - glyph: isDirectory ? _folderGlyph : _fileGlyph, - description: fullPath.ToSymbolDisplayParts(), - rules: _itemRules); - - private CompletionItem CreateLogicalDriveItem(string drive) - => CommonCompletionItem.Create( - drive, - displayTextSuffix: "", - glyph: _folderGlyph, - description: drive.ToSymbolDisplayParts(), - rules: _itemRules); - - public Task> GetItemsAsync(string directoryPath, CancellationToken cancellationToken) - => Task.Run(() => GetItems(directoryPath, cancellationToken), cancellationToken); - - private ImmutableArray GetItems(string directoryPath, CancellationToken cancellationToken) - { - if (!PathUtilities.IsUnixLikePlatform && directoryPath == "\\") - { - // The user has typed only "\". In this case, we want to add "\\" to the list. - return [CreateNetworkRoot()]; - } + var result = ArrayBuilder.GetInstance(); - var result = ArrayBuilder.GetInstance(); + var pathKind = PathUtilities.GetPathKind(directoryPath); + switch (pathKind) + { + case PathKind.Empty: + // base directory + if (_baseDirectory != null) + { + result.AddRange(GetItemsInDirectory(_baseDirectory, cancellationToken)); + } - var pathKind = PathUtilities.GetPathKind(directoryPath); - switch (pathKind) - { - case PathKind.Empty: - // base directory - if (_baseDirectory != null) + // roots + if (PathUtilities.IsUnixLikePlatform) + { + result.AddRange(CreateUnixRoot()); + } + else + { + foreach (var drive in GetLogicalDrives()) { - result.AddRange(GetItemsInDirectory(_baseDirectory, cancellationToken)); + result.Add(CreateLogicalDriveItem(drive.TrimEnd(s_windowsDirectorySeparator))); } - // roots - if (PathUtilities.IsUnixLikePlatform) - { - result.AddRange(CreateUnixRoot()); - } - else - { - foreach (var drive in GetLogicalDrives()) - { - result.Add(CreateLogicalDriveItem(drive.TrimEnd(s_windowsDirectorySeparator))); - } + result.Add(CreateNetworkRoot()); + } - result.Add(CreateNetworkRoot()); - } + // entries on search paths + foreach (var searchPath in _searchPaths) + { + result.AddRange(GetItemsInDirectory(searchPath, cancellationToken)); + } - // entries on search paths - foreach (var searchPath in _searchPaths) - { - result.AddRange(GetItemsInDirectory(searchPath, cancellationToken)); - } + break; - break; + case PathKind.Absolute: + case PathKind.RelativeToCurrentDirectory: + case PathKind.RelativeToCurrentParent: + case PathKind.RelativeToCurrentRoot: + var fullDirectoryPath = FileUtilities.ResolveRelativePath(directoryPath, basePath: null, baseDirectory: _baseDirectory); + if (fullDirectoryPath != null) + { + result.AddRange(GetItemsInDirectory(fullDirectoryPath, cancellationToken)); + } + else + { + // invalid path + result.Clear(); + } - case PathKind.Absolute: - case PathKind.RelativeToCurrentDirectory: - case PathKind.RelativeToCurrentParent: - case PathKind.RelativeToCurrentRoot: - var fullDirectoryPath = FileUtilities.ResolveRelativePath(directoryPath, basePath: null, baseDirectory: _baseDirectory); - if (fullDirectoryPath != null) - { - result.AddRange(GetItemsInDirectory(fullDirectoryPath, cancellationToken)); - } - else - { - // invalid path - result.Clear(); - } + break; - break; + case PathKind.Relative: - case PathKind.Relative: + // base directory: + if (_baseDirectory != null) + { + result.AddRange(GetItemsInDirectory(PathUtilities.CombineAbsoluteAndRelativePaths(_baseDirectory, directoryPath)!, cancellationToken)); + } - // base directory: - if (_baseDirectory != null) - { - result.AddRange(GetItemsInDirectory(PathUtilities.CombineAbsoluteAndRelativePaths(_baseDirectory, directoryPath)!, cancellationToken)); - } + // search paths: + foreach (var searchPath in _searchPaths) + { + result.AddRange(GetItemsInDirectory(PathUtilities.CombineAbsoluteAndRelativePaths(searchPath, directoryPath)!, cancellationToken)); + } - // search paths: - foreach (var searchPath in _searchPaths) - { - result.AddRange(GetItemsInDirectory(PathUtilities.CombineAbsoluteAndRelativePaths(searchPath, directoryPath)!, cancellationToken)); - } + break; - break; + case PathKind.RelativeToDriveDirectory: + // Paths "C:dir" are not supported, but when the path doesn't include any directory, i.e. "C:", + // we return the drive itself. + if (directoryPath.Length == 2) + { + result.Add(CreateLogicalDriveItem(directoryPath)); + } - case PathKind.RelativeToDriveDirectory: - // Paths "C:dir" are not supported, but when the path doesn't include any directory, i.e. "C:", - // we return the drive itself. - if (directoryPath.Length == 2) - { - result.Add(CreateLogicalDriveItem(directoryPath)); - } + break; - break; + default: + throw ExceptionUtilities.UnexpectedValue(pathKind); + } - default: - throw ExceptionUtilities.UnexpectedValue(pathKind); - } + return result.ToImmutableAndFree(); + } - return result.ToImmutableAndFree(); - } + private IEnumerable GetItemsInDirectory(string fullDirectoryPath, CancellationToken cancellationToken) + { + Debug.Assert(PathUtilities.IsAbsolute(fullDirectoryPath)); + + cancellationToken.ThrowIfCancellationRequested(); - private IEnumerable GetItemsInDirectory(string fullDirectoryPath, CancellationToken cancellationToken) + if (!DirectoryExists(fullDirectoryPath)) { - Debug.Assert(PathUtilities.IsAbsolute(fullDirectoryPath)); + yield break; + } - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - if (!DirectoryExists(fullDirectoryPath)) + foreach (var directory in EnumerateDirectories(fullDirectoryPath)) + { + if (IsVisibleFileSystemEntry(directory)) { - yield break; + yield return CreateFileSystemEntryItem(directory, isDirectory: true); } + } - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - foreach (var directory in EnumerateDirectories(fullDirectoryPath)) + foreach (var file in EnumerateFiles(fullDirectoryPath)) + { + if (_allowableExtensions.Length != 0 && + !_allowableExtensions.Contains( + PathUtilities.GetExtension(file), + PathUtilities.IsUnixLikePlatform ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase)) { - if (IsVisibleFileSystemEntry(directory)) - { - yield return CreateFileSystemEntryItem(directory, isDirectory: true); - } + continue; } cancellationToken.ThrowIfCancellationRequested(); - foreach (var file in EnumerateFiles(fullDirectoryPath)) + if (IsVisibleFileSystemEntry(file)) { - if (_allowableExtensions.Length != 0 && - !_allowableExtensions.Contains( - PathUtilities.GetExtension(file), - PathUtilities.IsUnixLikePlatform ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - cancellationToken.ThrowIfCancellationRequested(); - - if (IsVisibleFileSystemEntry(file)) - { - yield return CreateFileSystemEntryItem(file, isDirectory: false); - } + yield return CreateFileSystemEntryItem(file, isDirectory: false); } } + } - internal TestAccessor GetTestAccessor() - => new(this); + internal TestAccessor GetTestAccessor() + => new(this); - internal readonly struct TestAccessor(FileSystemCompletionHelper fileSystemCompletionHelper) - { - private readonly FileSystemCompletionHelper _fileSystemCompletionHelper = fileSystemCompletionHelper; + internal readonly struct TestAccessor(FileSystemCompletionHelper fileSystemCompletionHelper) + { + private readonly FileSystemCompletionHelper _fileSystemCompletionHelper = fileSystemCompletionHelper; - internal ImmutableArray GetItems(string directoryPath, CancellationToken cancellationToken) - => _fileSystemCompletionHelper.GetItems(directoryPath, cancellationToken); - } + internal ImmutableArray GetItems(string directoryPath, CancellationToken cancellationToken) + => _fileSystemCompletionHelper.GetItems(directoryPath, cancellationToken); } } diff --git a/src/Features/Core/Portable/Completion/ICompletionProviderFactory.cs b/src/Features/Core/Portable/Completion/ICompletionProviderFactory.cs index bc994119aa5bb..622266c2610e5 100644 --- a/src/Features/Core/Portable/Completion/ICompletionProviderFactory.cs +++ b/src/Features/Core/Portable/Completion/ICompletionProviderFactory.cs @@ -4,10 +4,9 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal interface ICompletionProviderFactory { - internal interface ICompletionProviderFactory - { - ImmutableArray GetCompletionProviders(); - } + ImmutableArray GetCompletionProviders(); } diff --git a/src/Features/Core/Portable/Completion/INotifyCommittingItemCompletionProvider.cs b/src/Features/Core/Portable/Completion/INotifyCommittingItemCompletionProvider.cs index af3342e4068d3..1616fe9bf170a 100644 --- a/src/Features/Core/Portable/Completion/INotifyCommittingItemCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/INotifyCommittingItemCompletionProvider.cs @@ -5,22 +5,21 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// Interface to implement if the provider want to sign up for notification when one of the items it provided +/// is being committed by the host, since calling doesn't necessarily +/// lead to commission. +/// +internal interface INotifyCommittingItemCompletionProvider { /// - /// Interface to implement if the provider want to sign up for notification when one of the items it provided - /// is being committed by the host, since calling doesn't necessarily - /// lead to commission. + /// This is invoked when one of the items provided by this provider is being committed. /// - internal interface INotifyCommittingItemCompletionProvider - { - /// - /// This is invoked when one of the items provided by this provider is being committed. - /// - /// - /// Host provides no guarantee when will this be called (i.e. pre or post document change), nor the text - /// change will actually happen at all (e.g. the commit operation might be cancelled due to cancellation/exception/etc.) - /// - Task NotifyCommittingItemAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken); - } + /// + /// Host provides no guarantee when will this be called (i.e. pre or post document change), nor the text + /// change will actually happen at all (e.g. the commit operation might be cancelled due to cancellation/exception/etc.) + /// + Task NotifyCommittingItemAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/Completion/LSPCompletionProvider.cs b/src/Features/Core/Portable/Completion/LSPCompletionProvider.cs index 0d59b4f454bae..9fe888d34e334 100644 --- a/src/Features/Core/Portable/Completion/LSPCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/LSPCompletionProvider.cs @@ -4,14 +4,13 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal abstract class LSPCompletionProvider : CommonCompletionProvider { - internal abstract class LSPCompletionProvider : CommonCompletionProvider - { - /// - /// Defines the set of possible non-identifier trigger characters for this completion provider. - /// Used by the LSP server to determine the trigger character set for completion. - /// - public abstract ImmutableHashSet TriggerCharacters { get; } - } + /// + /// Defines the set of possible non-identifier trigger characters for this completion provider. + /// Used by the LSP server to determine the trigger character set for completion. + /// + public abstract ImmutableHashSet TriggerCharacters { get; } } diff --git a/src/Features/Core/Portable/Completion/Log/CompletionProvidersLogger.cs b/src/Features/Core/Portable/Completion/Log/CompletionProvidersLogger.cs index e87e70de92477..3db5b7efca218 100644 --- a/src/Features/Core/Portable/Completion/Log/CompletionProvidersLogger.cs +++ b/src/Features/Core/Portable/Completion/Log/CompletionProvidersLogger.cs @@ -5,117 +5,116 @@ using System; using Microsoft.CodeAnalysis.Internal.Log; -namespace Microsoft.CodeAnalysis.Completion.Log +namespace Microsoft.CodeAnalysis.Completion.Log; + +internal static class CompletionProvidersLogger { - internal static class CompletionProvidersLogger - { - private static readonly StatisticLogAggregator s_statisticLogAggregator = new(); - private static readonly CountLogAggregator s_countLogAggregator = new(); + private static readonly StatisticLogAggregator s_statisticLogAggregator = new(); + private static readonly CountLogAggregator s_countLogAggregator = new(); - private static readonly HistogramLogAggregator s_histogramLogAggregator = new(bucketSize: 50, maxBucketValue: 1000); + private static readonly HistogramLogAggregator s_histogramLogAggregator = new(bucketSize: 50, maxBucketValue: 1000); - internal enum ActionInfo - { - TypeImportCompletionTicks, - TypeImportCompletionItemCount, - TypeImportCompletionReferenceCount, - TypeImportCompletionCacheMissCount, - CommitsOfTypeImportCompletionItem, - - ExtensionMethodCompletionTicks, - ExtensionMethodCompletionMethodsProvided, - ExtensionMethodCompletionGetSymbolsTicks, - ExtensionMethodCompletionCreateItemsTicks, - ExtensionMethodCompletionRemoteAssetSyncTicks, - ExtensionMethodCompletionRemoteTicks, - CommitsOfExtensionMethodImportCompletionItem, - ExtensionMethodCompletionPartialResultCount, - - CommitUsingSemicolonToAddParenthesis, - CommitUsingDotToAddParenthesis - } + internal enum ActionInfo + { + TypeImportCompletionTicks, + TypeImportCompletionItemCount, + TypeImportCompletionReferenceCount, + TypeImportCompletionCacheMissCount, + CommitsOfTypeImportCompletionItem, + + ExtensionMethodCompletionTicks, + ExtensionMethodCompletionMethodsProvided, + ExtensionMethodCompletionGetSymbolsTicks, + ExtensionMethodCompletionCreateItemsTicks, + ExtensionMethodCompletionRemoteAssetSyncTicks, + ExtensionMethodCompletionRemoteTicks, + CommitsOfExtensionMethodImportCompletionItem, + ExtensionMethodCompletionPartialResultCount, + + CommitUsingSemicolonToAddParenthesis, + CommitUsingDotToAddParenthesis + } - internal static void LogTypeImportCompletionTicksDataPoint(TimeSpan elapsed) - { - s_histogramLogAggregator.LogTime(ActionInfo.TypeImportCompletionTicks, elapsed); - s_statisticLogAggregator.AddDataPoint(ActionInfo.TypeImportCompletionTicks, elapsed); - } + internal static void LogTypeImportCompletionTicksDataPoint(TimeSpan elapsed) + { + s_histogramLogAggregator.LogTime(ActionInfo.TypeImportCompletionTicks, elapsed); + s_statisticLogAggregator.AddDataPoint(ActionInfo.TypeImportCompletionTicks, elapsed); + } - internal static void LogTypeImportCompletionItemCountDataPoint(int count) - => s_statisticLogAggregator.AddDataPoint(ActionInfo.TypeImportCompletionItemCount, count); + internal static void LogTypeImportCompletionItemCountDataPoint(int count) + => s_statisticLogAggregator.AddDataPoint(ActionInfo.TypeImportCompletionItemCount, count); - internal static void LogTypeImportCompletionReferenceCountDataPoint(int count) - => s_statisticLogAggregator.AddDataPoint(ActionInfo.TypeImportCompletionReferenceCount, count); + internal static void LogTypeImportCompletionReferenceCountDataPoint(int count) + => s_statisticLogAggregator.AddDataPoint(ActionInfo.TypeImportCompletionReferenceCount, count); - internal static void LogTypeImportCompletionCacheMiss() - => s_countLogAggregator.IncreaseCount(ActionInfo.TypeImportCompletionCacheMissCount); + internal static void LogTypeImportCompletionCacheMiss() + => s_countLogAggregator.IncreaseCount(ActionInfo.TypeImportCompletionCacheMissCount); - internal static void LogCommitOfTypeImportCompletionItem() - => s_countLogAggregator.IncreaseCount(ActionInfo.CommitsOfTypeImportCompletionItem); + internal static void LogCommitOfTypeImportCompletionItem() + => s_countLogAggregator.IncreaseCount(ActionInfo.CommitsOfTypeImportCompletionItem); + + internal static void LogExtensionMethodCompletionTicksDataPoint(TimeSpan total, TimeSpan getSymbols, TimeSpan createItems, TimeSpan? remoteAssetSync) + { + s_histogramLogAggregator.LogTime(ActionInfo.ExtensionMethodCompletionTicks, total); + s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionTicks, total); - internal static void LogExtensionMethodCompletionTicksDataPoint(TimeSpan total, TimeSpan getSymbols, TimeSpan createItems, TimeSpan? remoteAssetSync) + if (remoteAssetSync.HasValue) { - s_histogramLogAggregator.LogTime(ActionInfo.ExtensionMethodCompletionTicks, total); - s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionTicks, total); + s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionRemoteAssetSyncTicks, remoteAssetSync.Value); + s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionRemoteTicks, total - remoteAssetSync.Value - getSymbols - createItems); + } - if (remoteAssetSync.HasValue) - { - s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionRemoteAssetSyncTicks, remoteAssetSync.Value); - s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionRemoteTicks, total - remoteAssetSync.Value - getSymbols - createItems); - } + s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionGetSymbolsTicks, getSymbols); + s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionCreateItemsTicks, createItems); + } - s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionGetSymbolsTicks, getSymbols); - s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionCreateItemsTicks, createItems); - } + internal static void LogExtensionMethodCompletionMethodsProvidedDataPoint(int count) + => s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionMethodsProvided, count); - internal static void LogExtensionMethodCompletionMethodsProvidedDataPoint(int count) - => s_statisticLogAggregator.AddDataPoint(ActionInfo.ExtensionMethodCompletionMethodsProvided, count); + internal static void LogCommitOfExtensionMethodImportCompletionItem() + => s_countLogAggregator.IncreaseCount(ActionInfo.CommitsOfExtensionMethodImportCompletionItem); - internal static void LogCommitOfExtensionMethodImportCompletionItem() - => s_countLogAggregator.IncreaseCount(ActionInfo.CommitsOfExtensionMethodImportCompletionItem); + internal static void LogExtensionMethodCompletionPartialResultCount() + => s_countLogAggregator.IncreaseCount(ActionInfo.ExtensionMethodCompletionPartialResultCount); - internal static void LogExtensionMethodCompletionPartialResultCount() - => s_countLogAggregator.IncreaseCount(ActionInfo.ExtensionMethodCompletionPartialResultCount); + internal static void LogCommitUsingSemicolonToAddParenthesis() + => s_countLogAggregator.IncreaseCount(ActionInfo.CommitUsingSemicolonToAddParenthesis); - internal static void LogCommitUsingSemicolonToAddParenthesis() - => s_countLogAggregator.IncreaseCount(ActionInfo.CommitUsingSemicolonToAddParenthesis); + internal static void LogCommitUsingDotToAddParenthesis() + => s_countLogAggregator.IncreaseCount(ActionInfo.CommitUsingDotToAddParenthesis); - internal static void LogCommitUsingDotToAddParenthesis() - => s_countLogAggregator.IncreaseCount(ActionInfo.CommitUsingDotToAddParenthesis); + internal static void LogCustomizedCommitToAddParenthesis(char? commitChar) + { + switch (commitChar) + { + case '.': + LogCommitUsingDotToAddParenthesis(); + break; + case ';': + LogCommitUsingSemicolonToAddParenthesis(); + break; + } + } - internal static void LogCustomizedCommitToAddParenthesis(char? commitChar) + internal static void ReportTelemetry() + { + Logger.Log(FunctionId.Intellisense_CompletionProviders_Data, KeyValueLogMessage.Create(m => { - switch (commitChar) + foreach (var kv in s_statisticLogAggregator) { - case '.': - LogCommitUsingDotToAddParenthesis(); - break; - case ';': - LogCommitUsingSemicolonToAddParenthesis(); - break; + var statistics = kv.Value.GetStatisticResult(); + statistics.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); } - } - internal static void ReportTelemetry() - { - Logger.Log(FunctionId.Intellisense_CompletionProviders_Data, KeyValueLogMessage.Create(m => + foreach (var kv in s_countLogAggregator) { - foreach (var kv in s_statisticLogAggregator) - { - var statistics = kv.Value.GetStatisticResult(); - statistics.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); - } - - foreach (var kv in s_countLogAggregator) - { - m[kv.Key.ToString()] = kv.Value.GetCount(); - } - - foreach (var kv in s_histogramLogAggregator) - { - kv.Value.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); - } - })); - } + m[kv.Key.ToString()] = kv.Value.GetCount(); + } + + foreach (var kv in s_histogramLogAggregator) + { + kv.Value.WriteTelemetryPropertiesTo(m, prefix: kv.Key.ToString()); + } + })); } } diff --git a/src/Features/Core/Portable/Completion/MatchPriority.cs b/src/Features/Core/Portable/Completion/MatchPriority.cs index 4a4eeb50b2916..587dafa0d84da 100644 --- a/src/Features/Core/Portable/Completion/MatchPriority.cs +++ b/src/Features/Core/Portable/Completion/MatchPriority.cs @@ -2,37 +2,36 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// An additional hint to the matching algorithm that can +/// augment or override the existing text-based matching. +/// +public static class MatchPriority { /// - /// An additional hint to the matching algorithm that can - /// augment or override the existing text-based matching. + /// The matching algorithm should not select this item unless it's a dramatically + /// better text-based match than other items. /// - public static class MatchPriority - { - /// - /// The matching algorithm should not select this item unless it's a dramatically - /// better text-based match than other items. - /// - internal const int Deprioritize = int.MinValue / 2; + internal const int Deprioritize = int.MinValue / 2; - /// - /// The matching algorithm should give this item no special treatment. - /// - /// Ordinary s typically specify this. - /// - public static readonly int Default = 0; + /// + /// The matching algorithm should give this item no special treatment. + /// + /// Ordinary s typically specify this. + /// + public static readonly int Default = 0; - /// - /// The matching algorithm will tend to prefer this item unless - /// a dramatically better text-based match is available. - /// - /// With no filter text, this item (or the first item alphabetically - /// with this priority) should always be selected. - /// - /// This is used for specific IDE scenarios like "Object creation preselection" - /// or "Enum preselection" or "Completion list tag preselection". - /// - public static readonly int Preselect = int.MaxValue / 2; - } + /// + /// The matching algorithm will tend to prefer this item unless + /// a dramatically better text-based match is available. + /// + /// With no filter text, this item (or the first item alphabetically + /// with this priority) should always be selected. + /// + /// This is used for specific IDE scenarios like "Object creation preselection" + /// or "Enum preselection" or "Completion list tag preselection". + /// + public static readonly int Preselect = int.MaxValue / 2; } diff --git a/src/Features/Core/Portable/Completion/MatchResult.cs b/src/Features/Core/Portable/Completion/MatchResult.cs index 4542b61702006..a4278adba9e0c 100644 --- a/src/Features/Core/Portable/Completion/MatchResult.cs +++ b/src/Features/Core/Portable/Completion/MatchResult.cs @@ -5,80 +5,79 @@ using System.Collections.Generic; using Microsoft.CodeAnalysis.PatternMatching; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal readonly struct MatchResult( + CompletionItem completionItem, + bool shouldBeConsideredMatchingFilterText, + PatternMatch? patternMatch, + int index, + string? matchedAdditionalFilterText, + int recentItemIndex = -1) { - internal readonly struct MatchResult( - CompletionItem completionItem, - bool shouldBeConsideredMatchingFilterText, - PatternMatch? patternMatch, - int index, - string? matchedAdditionalFilterText, - int recentItemIndex = -1) - { - /// - /// The CompletionItem used to create this MatchResult. - /// - public readonly CompletionItem CompletionItem = completionItem; + /// + /// The CompletionItem used to create this MatchResult. + /// + public readonly CompletionItem CompletionItem = completionItem; + + public readonly PatternMatch? PatternMatch = patternMatch; - public readonly PatternMatch? PatternMatch = patternMatch; + // The value of `ShouldBeConsideredMatchingFilterText` doesn't 100% reflect the actual PatternMatch result. + // In certain cases, there'd be no match but we'd still want to consider it a match (e.g. when the item is in MRU list,) + // and this is why PatternMatch can be null. There's also cases it's a match but we want to consider it a non-match + // (e.g. when not a prefix match in deletion scenario). + public readonly bool ShouldBeConsideredMatchingFilterText = shouldBeConsideredMatchingFilterText; - // The value of `ShouldBeConsideredMatchingFilterText` doesn't 100% reflect the actual PatternMatch result. - // In certain cases, there'd be no match but we'd still want to consider it a match (e.g. when the item is in MRU list,) - // and this is why PatternMatch can be null. There's also cases it's a match but we want to consider it a non-match - // (e.g. when not a prefix match in deletion scenario). - public readonly bool ShouldBeConsideredMatchingFilterText = shouldBeConsideredMatchingFilterText; + public string FilterTextUsed => MatchedAdditionalFilterText ?? CompletionItem.FilterText; - public string FilterTextUsed => MatchedAdditionalFilterText ?? CompletionItem.FilterText; + // We want to preserve the original alphabetical order for items with same pattern match score, + // but `ArrayBuilder.Sort` we currently use isn't stable. So we have to add a monotonically increasing + // integer to achieve this. + public readonly int IndexInOriginalSortedOrder = index; + public readonly int RecentItemIndex = recentItemIndex; - // We want to preserve the original alphabetical order for items with same pattern match score, - // but `ArrayBuilder.Sort` we currently use isn't stable. So we have to add a monotonically increasing - // integer to achieve this. - public readonly int IndexInOriginalSortedOrder = index; - public readonly int RecentItemIndex = recentItemIndex; + /// + /// If `CompletionItem.AdditionalFilterTexts` was used to create this MatchResult, then this is set to the one that was used. + /// Otherwise this is set to null. + /// + public readonly string? MatchedAdditionalFilterText = matchedAdditionalFilterText; - /// - /// If `CompletionItem.AdditionalFilterTexts` was used to create this MatchResult, then this is set to the one that was used. - /// Otherwise this is set to null. - /// - public readonly string? MatchedAdditionalFilterText = matchedAdditionalFilterText; + public bool MatchedWithAdditionalFilterTexts => MatchedAdditionalFilterText is not null; - public bool MatchedWithAdditionalFilterTexts => MatchedAdditionalFilterText is not null; + public static IComparer SortingComparer { get; } = new Comparer(); - public static IComparer SortingComparer { get; } = new Comparer(); + private class Comparer : IComparer + { + // This comparison is used for sorting items in the completion list for the original sorting. - private class Comparer : IComparer + public int Compare(MatchResult x, MatchResult y) { - // This comparison is used for sorting items in the completion list for the original sorting. + var matchX = x.PatternMatch; + var matchY = y.PatternMatch; - public int Compare(MatchResult x, MatchResult y) + if (matchX.HasValue) { - var matchX = x.PatternMatch; - var matchY = y.PatternMatch; - - if (matchX.HasValue) + if (matchY.HasValue) { - if (matchY.HasValue) - { - var ret = matchX.Value.CompareTo(matchY.Value); - - // We'd rank match of FilterText over match of any of AdditionalFilterTexts if they has same pattern match score - if (ret == 0) - ret = x.MatchedWithAdditionalFilterTexts.CompareTo(y.MatchedWithAdditionalFilterTexts); + var ret = matchX.Value.CompareTo(matchY.Value); - // We want to preserve the original order for items with same pattern match score. - return ret == 0 ? x.IndexInOriginalSortedOrder - y.IndexInOriginalSortedOrder : ret; - } + // We'd rank match of FilterText over match of any of AdditionalFilterTexts if they has same pattern match score + if (ret == 0) + ret = x.MatchedWithAdditionalFilterTexts.CompareTo(y.MatchedWithAdditionalFilterTexts); - return -1; + // We want to preserve the original order for items with same pattern match score. + return ret == 0 ? x.IndexInOriginalSortedOrder - y.IndexInOriginalSortedOrder : ret; } - if (matchY.HasValue) - { - return 1; - } + return -1; + } - return x.IndexInOriginalSortedOrder - y.IndexInOriginalSortedOrder; + if (matchY.HasValue) + { + return 1; } + + return x.IndexInOriginalSortedOrder - y.IndexInOriginalSortedOrder; } } } diff --git a/src/Features/Core/Portable/Completion/PatternMatchHelper.cs b/src/Features/Core/Portable/Completion/PatternMatchHelper.cs index 1742bfa80bdea..12b6ad2a4189d 100644 --- a/src/Features/Core/Portable/Completion/PatternMatchHelper.cs +++ b/src/Features/Core/Portable/Completion/PatternMatchHelper.cs @@ -10,249 +10,248 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// This type is not thread safe due to the restriction of underlying PatternMatcher. +/// Must be disposed after use. +/// +internal sealed class PatternMatchHelper(string pattern) : IDisposable { - /// - /// This type is not thread safe due to the restriction of underlying PatternMatcher. - /// Must be disposed after use. - /// - internal sealed class PatternMatchHelper(string pattern) : IDisposable - { - private static readonly CultureInfo EnUSCultureInfo; + private static readonly CultureInfo EnUSCultureInfo; - static PatternMatchHelper() + static PatternMatchHelper() + { + try { - try - { - EnUSCultureInfo = new("en-US"); - } - catch (CultureNotFoundException) - { - // See https://github.com/microsoft/vscode-dotnettools/issues/386 - // This can happen when running on a runtime that is setup for culture invariant mode only. - EnUSCultureInfo = CultureInfo.InvariantCulture; - } + EnUSCultureInfo = new("en-US"); + } + catch (CultureNotFoundException) + { + // See https://github.com/microsoft/vscode-dotnettools/issues/386 + // This can happen when running on a runtime that is setup for culture invariant mode only. + EnUSCultureInfo = CultureInfo.InvariantCulture; } + } - private readonly object _gate = new(); - private readonly Dictionary<(CultureInfo, bool includeMatchedSpans), PatternMatcher> _patternMatcherMap = []; + private readonly object _gate = new(); + private readonly Dictionary<(CultureInfo, bool includeMatchedSpans), PatternMatcher> _patternMatcherMap = []; - public string Pattern { get; } = pattern; + public string Pattern { get; } = pattern; + + public ImmutableArray GetHighlightedSpans(string text, CultureInfo culture) + { + var match = GetMatch(text, includeMatchSpans: true, culture: culture); + return match == null ? [] : match.Value.MatchedSpans; + } - public ImmutableArray GetHighlightedSpans(string text, CultureInfo culture) + public PatternMatch? GetMatch(string text, bool includeMatchSpans, CultureInfo culture) + { + var patternMatcher = GetPatternMatcher(culture, includeMatchSpans); + var match = patternMatcher.GetFirstMatch(text); + + // We still have making checks for language having different to English capitalization, + // for example, for Turkish with dotted and dotless i capitalization totally diferent from English. + // Now we escaping from the second check for English languages. + // Maybe we can escape as well for more similar languages in case if we meet performance issues. + if (culture.ThreeLetterWindowsLanguageName.Equals(EnUSCultureInfo.ThreeLetterWindowsLanguageName)) { - var match = GetMatch(text, includeMatchSpans: true, culture: culture); - return match == null ? [] : match.Value.MatchedSpans; + return match; } - public PatternMatch? GetMatch(string text, bool includeMatchSpans, CultureInfo culture) + // Keywords in .NET are always in En-US. + // Identifiers can be in user language. + // Try to get matches for both and return the best of them. + patternMatcher = GetPatternMatcher(EnUSCultureInfo, includeMatchSpans); + var enUSCultureMatch = patternMatcher.GetFirstMatch(text); + + if (match == null) { - var patternMatcher = GetPatternMatcher(culture, includeMatchSpans); - var match = patternMatcher.GetFirstMatch(text); - - // We still have making checks for language having different to English capitalization, - // for example, for Turkish with dotted and dotless i capitalization totally diferent from English. - // Now we escaping from the second check for English languages. - // Maybe we can escape as well for more similar languages in case if we meet performance issues. - if (culture.ThreeLetterWindowsLanguageName.Equals(EnUSCultureInfo.ThreeLetterWindowsLanguageName)) - { - return match; - } + return enUSCultureMatch; + } - // Keywords in .NET are always in En-US. - // Identifiers can be in user language. - // Try to get matches for both and return the best of them. - patternMatcher = GetPatternMatcher(EnUSCultureInfo, includeMatchSpans); - var enUSCultureMatch = patternMatcher.GetFirstMatch(text); + if (enUSCultureMatch == null) + { + return match; + } - if (match == null) - { - return enUSCultureMatch; - } + return match.Value.CompareTo(enUSCultureMatch.Value) < 0 ? match.Value : enUSCultureMatch.Value; + } - if (enUSCultureMatch == null) + private PatternMatcher GetPatternMatcher(CultureInfo culture, bool includeMatchedSpans) + { + lock (_gate) + { + var key = (culture, includeMatchedSpans); + if (!_patternMatcherMap.TryGetValue(key, out var patternMatcher)) { - return match; + patternMatcher = PatternMatcher.CreatePatternMatcher( + Pattern, culture, includeMatchedSpans, + allowFuzzyMatching: false); + _patternMatcherMap.Add(key, patternMatcher); } - return match.Value.CompareTo(enUSCultureMatch.Value) < 0 ? match.Value : enUSCultureMatch.Value; + return patternMatcher; } + } + + public MatchResult GetMatchResult( + CompletionItem item, + bool includeMatchSpans, + CultureInfo culture) + { + var match = GetMatch(item.FilterText, includeMatchSpans, culture); + string? matchedAdditionalFilterText = null; - private PatternMatcher GetPatternMatcher(CultureInfo culture, bool includeMatchedSpans) + if (item.HasAdditionalFilterTexts) { - lock (_gate) + foreach (var additionalFilterText in item.AdditionalFilterTexts) { - var key = (culture, includeMatchedSpans); - if (!_patternMatcherMap.TryGetValue(key, out var patternMatcher)) + var additionalMatch = GetMatch(additionalFilterText, includeMatchSpans, culture); + if (additionalMatch.HasValue && additionalMatch.Value.CompareTo(match, ignoreCase: false) < 0) { - patternMatcher = PatternMatcher.CreatePatternMatcher( - Pattern, culture, includeMatchedSpans, - allowFuzzyMatching: false); - _patternMatcherMap.Add(key, patternMatcher); + match = additionalMatch; + matchedAdditionalFilterText = additionalFilterText; } - - return patternMatcher; } } - public MatchResult GetMatchResult( - CompletionItem item, - bool includeMatchSpans, - CultureInfo culture) - { - var match = GetMatch(item.FilterText, includeMatchSpans, culture); - string? matchedAdditionalFilterText = null; + return new MatchResult( + item, + shouldBeConsideredMatchingFilterText: match is not null, + match, + index: -1, + matchedAdditionalFilterText); + } - if (item.HasAdditionalFilterTexts) + /// + /// Returns true if the completion item matches the pattern so far. Returns 'true' + /// if and only if the completion item matches and should be included in the filtered completion + /// results, or false if it should not be. + /// + public bool MatchesPattern(CompletionItem item, CultureInfo culture) + => GetMatchResult(item, includeMatchSpans: false, culture).ShouldBeConsideredMatchingFilterText; + + public bool TryCreateMatchResult( + CompletionItem item, + CompletionTriggerKind initialTriggerKind, + CompletionFilterReason filterReason, + int recentItemIndex, + bool includeMatchSpans, + int currentIndex, + out MatchResult matchResult) + { + // Get the match of the given completion item for the pattern provided so far. + // A completion item is checked against the pattern by see if it's + // CompletionItem.FilterText matches the item. That way, the pattern it checked + // against terms like "IList" and not IList<>. + // Note that the check on filter text length is purely for efficiency, we should + // get the same result with or without it. + var patternMatch = Pattern.Length > 0 + ? GetMatch(item.FilterText, includeMatchSpans, CultureInfo.CurrentCulture) + : null; + + string? matchedAdditionalFilterText = null; + var shouldBeConsideredMatchingFilterText = ShouldBeConsideredMatchingFilterText( + item.FilterText, + item.Rules.MatchPriority, + initialTriggerKind, + filterReason, + recentItemIndex, + patternMatch); + + if (Pattern.Length > 0 && item.HasAdditionalFilterTexts) + { + foreach (var additionalFilterText in item.AdditionalFilterTexts) { - foreach (var additionalFilterText in item.AdditionalFilterTexts) + var additionalMatch = GetMatch(additionalFilterText, includeMatchSpans, CultureInfo.CurrentCulture); + var additionalFlag = ShouldBeConsideredMatchingFilterText( + additionalFilterText, + item.Rules.MatchPriority, + initialTriggerKind, + filterReason, + recentItemIndex, + additionalMatch); + + if (!shouldBeConsideredMatchingFilterText || + additionalFlag && additionalMatch.HasValue && additionalMatch.Value.CompareTo(patternMatch, ignoreCase: false) < 0) { - var additionalMatch = GetMatch(additionalFilterText, includeMatchSpans, culture); - if (additionalMatch.HasValue && additionalMatch.Value.CompareTo(match, ignoreCase: false) < 0) - { - match = additionalMatch; - matchedAdditionalFilterText = additionalFilterText; - } + matchedAdditionalFilterText = additionalFilterText; + shouldBeConsideredMatchingFilterText = additionalFlag; + patternMatch = additionalMatch; } } + } - return new MatchResult( - item, - shouldBeConsideredMatchingFilterText: match is not null, - match, - index: -1, - matchedAdditionalFilterText); + if (shouldBeConsideredMatchingFilterText || KeepAllItemsInTheList(initialTriggerKind, Pattern)) + { + matchResult = new MatchResult( + item, shouldBeConsideredMatchingFilterText, + patternMatch, currentIndex, matchedAdditionalFilterText, recentItemIndex); + + return true; } - /// - /// Returns true if the completion item matches the pattern so far. Returns 'true' - /// if and only if the completion item matches and should be included in the filtered completion - /// results, or false if it should not be. - /// - public bool MatchesPattern(CompletionItem item, CultureInfo culture) - => GetMatchResult(item, includeMatchSpans: false, culture).ShouldBeConsideredMatchingFilterText; + matchResult = default; + return false; - public bool TryCreateMatchResult( - CompletionItem item, + bool ShouldBeConsideredMatchingFilterText( + string filterText, + int matchPriority, CompletionTriggerKind initialTriggerKind, CompletionFilterReason filterReason, int recentItemIndex, - bool includeMatchSpans, - int currentIndex, - out MatchResult matchResult) + PatternMatch? patternMatch) { - // Get the match of the given completion item for the pattern provided so far. - // A completion item is checked against the pattern by see if it's - // CompletionItem.FilterText matches the item. That way, the pattern it checked - // against terms like "IList" and not IList<>. - // Note that the check on filter text length is purely for efficiency, we should - // get the same result with or without it. - var patternMatch = Pattern.Length > 0 - ? GetMatch(item.FilterText, includeMatchSpans, CultureInfo.CurrentCulture) - : null; - - string? matchedAdditionalFilterText = null; - var shouldBeConsideredMatchingFilterText = ShouldBeConsideredMatchingFilterText( - item.FilterText, - item.Rules.MatchPriority, - initialTriggerKind, - filterReason, - recentItemIndex, - patternMatch); - - if (Pattern.Length > 0 && item.HasAdditionalFilterTexts) + // For the deletion we bake in the core logic for how matching should work. + // This way deletion feels the same across all languages that opt into deletion + // as a completion trigger. + + // Specifically, to avoid being too aggressive when matching an item during + // completion, we require that the current filter text be a prefix of the + // item in the list. + if (filterReason == CompletionFilterReason.Deletion && + initialTriggerKind == CompletionTriggerKind.Deletion) { - foreach (var additionalFilterText in item.AdditionalFilterTexts) - { - var additionalMatch = GetMatch(additionalFilterText, includeMatchSpans, CultureInfo.CurrentCulture); - var additionalFlag = ShouldBeConsideredMatchingFilterText( - additionalFilterText, - item.Rules.MatchPriority, - initialTriggerKind, - filterReason, - recentItemIndex, - additionalMatch); - - if (!shouldBeConsideredMatchingFilterText || - additionalFlag && additionalMatch.HasValue && additionalMatch.Value.CompareTo(patternMatch, ignoreCase: false) < 0) - { - matchedAdditionalFilterText = additionalFilterText; - shouldBeConsideredMatchingFilterText = additionalFlag; - patternMatch = additionalMatch; - } - } + return filterText.GetCaseInsensitivePrefixLength(Pattern) > 0; } - if (shouldBeConsideredMatchingFilterText || KeepAllItemsInTheList(initialTriggerKind, Pattern)) + // If the user hasn't typed anything, and this item was preselected, or was in the + // MRU list, then we definitely want to include it. + if (Pattern.Length == 0) { - matchResult = new MatchResult( - item, shouldBeConsideredMatchingFilterText, - patternMatch, currentIndex, matchedAdditionalFilterText, recentItemIndex); - - return true; + if (recentItemIndex >= 0 || matchPriority > MatchPriority.Default) + return true; } - matchResult = default; - return false; - - bool ShouldBeConsideredMatchingFilterText( - string filterText, - int matchPriority, - CompletionTriggerKind initialTriggerKind, - CompletionFilterReason filterReason, - int recentItemIndex, - PatternMatch? patternMatch) - { - // For the deletion we bake in the core logic for how matching should work. - // This way deletion feels the same across all languages that opt into deletion - // as a completion trigger. - - // Specifically, to avoid being too aggressive when matching an item during - // completion, we require that the current filter text be a prefix of the - // item in the list. - if (filterReason == CompletionFilterReason.Deletion && - initialTriggerKind == CompletionTriggerKind.Deletion) - { - return filterText.GetCaseInsensitivePrefixLength(Pattern) > 0; - } - - // If the user hasn't typed anything, and this item was preselected, or was in the - // MRU list, then we definitely want to include it. - if (Pattern.Length == 0) - { - if (recentItemIndex >= 0 || matchPriority > MatchPriority.Default) - return true; - } - - // Otherwise, the item matches filter text if a pattern match is returned. - return patternMatch != null; - } + // Otherwise, the item matches filter text if a pattern match is returned. + return patternMatch != null; + } - // If the item didn't match the filter text, we still keep it in the list - // if one of two things is true: - // 1. The user has typed nothing or only typed a single character. In this case they might - // have just typed the character to get completion. Filtering out items - // here is not desirable. - // - // 2. They brought up completion with ctrl-j or through deletion. In these - // cases we just always keep all the items in the list. - static bool KeepAllItemsInTheList(CompletionTriggerKind initialTriggerKind, string filterText) - { - return filterText.Length <= 1 || - initialTriggerKind == CompletionTriggerKind.Invoke || - initialTriggerKind == CompletionTriggerKind.Deletion; - } + // If the item didn't match the filter text, we still keep it in the list + // if one of two things is true: + // 1. The user has typed nothing or only typed a single character. In this case they might + // have just typed the character to get completion. Filtering out items + // here is not desirable. + // + // 2. They brought up completion with ctrl-j or through deletion. In these + // cases we just always keep all the items in the list. + static bool KeepAllItemsInTheList(CompletionTriggerKind initialTriggerKind, string filterText) + { + return filterText.Length <= 1 || + initialTriggerKind == CompletionTriggerKind.Invoke || + initialTriggerKind == CompletionTriggerKind.Deletion; } + } - public void Dispose() + public void Dispose() + { + lock (_gate) { - lock (_gate) - { - foreach (var matcher in _patternMatcherMap.Values) - matcher.Dispose(); + foreach (var matcher in _patternMatcherMap.Values) + matcher.Dispose(); - _patternMatcherMap.Clear(); - } + _patternMatcherMap.Clear(); } } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractAggregateEmbeddedLanguageCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractAggregateEmbeddedLanguageCompletionProvider.cs index e86f42dc67dcc..b6e5d449571dd 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractAggregateEmbeddedLanguageCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractAggregateEmbeddedLanguageCompletionProvider.cs @@ -16,107 +16,106 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +/// +/// The singular completion provider that will hook into completion and will +/// provide all completions across all embedded languages. +/// +/// Completions for an individual language are provided by +/// . +/// +internal abstract class AbstractAggregateEmbeddedLanguageCompletionProvider : LSPCompletionProvider { - /// - /// The singular completion provider that will hook into completion and will - /// provide all completions across all embedded languages. - /// - /// Completions for an individual language are provided by - /// . - /// - internal abstract class AbstractAggregateEmbeddedLanguageCompletionProvider : LSPCompletionProvider - { - public const string EmbeddedProviderName = "EmbeddedProvider"; + public const string EmbeddedProviderName = "EmbeddedProvider"; - private ImmutableArray _languageProviders; + private ImmutableArray _languageProviders; - protected AbstractAggregateEmbeddedLanguageCompletionProvider(IEnumerable> languageServices, string languageName) + protected AbstractAggregateEmbeddedLanguageCompletionProvider(IEnumerable> languageServices, string languageName) + { + var embeddedLanguageServiceType = typeof(IEmbeddedLanguagesProvider).AssemblyQualifiedName; + TriggerCharacters = languageServices + .Where(lazyLanguageService => IsEmbeddedLanguageProvider(lazyLanguageService, languageName, embeddedLanguageServiceType)) + .SelectMany(lazyLanguageService => ((IEmbeddedLanguagesProvider)lazyLanguageService.Value).Languages) + .SelectMany(GetTriggerCharactersForEmbeddedLanguage) + .ToImmutableHashSet(); + } + + private static ImmutableHashSet GetTriggerCharactersForEmbeddedLanguage(IEmbeddedLanguage language) + { + var completionProvider = language.CompletionProvider; + if (completionProvider != null) { - var embeddedLanguageServiceType = typeof(IEmbeddedLanguagesProvider).AssemblyQualifiedName; - TriggerCharacters = languageServices - .Where(lazyLanguageService => IsEmbeddedLanguageProvider(lazyLanguageService, languageName, embeddedLanguageServiceType)) - .SelectMany(lazyLanguageService => ((IEmbeddedLanguagesProvider)lazyLanguageService.Value).Languages) - .SelectMany(GetTriggerCharactersForEmbeddedLanguage) - .ToImmutableHashSet(); + return completionProvider.TriggerCharacters; } - private static ImmutableHashSet GetTriggerCharactersForEmbeddedLanguage(IEmbeddedLanguage language) - { - var completionProvider = language.CompletionProvider; - if (completionProvider != null) - { - return completionProvider.TriggerCharacters; - } + return []; + } - return []; - } + private static bool IsEmbeddedLanguageProvider(Lazy lazyLanguageService, string languageName, string? embeddedLanguageServiceType) + { + return lazyLanguageService.Metadata.Language == languageName && lazyLanguageService.Metadata.ServiceType == embeddedLanguageServiceType; + } - private static bool IsEmbeddedLanguageProvider(Lazy lazyLanguageService, string languageName, string? embeddedLanguageServiceType) + protected ImmutableArray GetLanguageProviders(Host.LanguageServices? languageServices) + { + if (_languageProviders.IsDefault) { - return lazyLanguageService.Metadata.Language == languageName && lazyLanguageService.Metadata.ServiceType == embeddedLanguageServiceType; + var languagesProvider = languageServices?.GetService(); + ImmutableInterlocked.InterlockedInitialize(ref _languageProviders, languagesProvider?.Languages ?? []); } - protected ImmutableArray GetLanguageProviders(Host.LanguageServices? languageServices) - { - if (_languageProviders.IsDefault) - { - var languagesProvider = languageServices?.GetService(); - ImmutableInterlocked.InterlockedInitialize(ref _languageProviders, languagesProvider?.Languages ?? []); - } - - return _languageProviders; - } + return _languageProviders; + } - public override ImmutableHashSet TriggerCharacters { get; } + public override ImmutableHashSet TriggerCharacters { get; } - internal sealed override bool ShouldTriggerCompletion(LanguageServices languageServices, SourceText text, int caretPosition, CompletionTrigger trigger, CompletionOptions options, OptionSet passThroughOptions) + internal sealed override bool ShouldTriggerCompletion(LanguageServices languageServices, SourceText text, int caretPosition, CompletionTrigger trigger, CompletionOptions options, OptionSet passThroughOptions) + { + foreach (var language in GetLanguageProviders(languageServices)) { - foreach (var language in GetLanguageProviders(languageServices)) + var completionProvider = language.CompletionProvider; + if (completionProvider != null) { - var completionProvider = language.CompletionProvider; - if (completionProvider != null) + if (completionProvider.ShouldTriggerCompletion(text, caretPosition, trigger)) { - if (completionProvider.ShouldTriggerCompletion(text, caretPosition, trigger)) - { - return true; - } + return true; } } - - return false; } - public override async Task ProvideCompletionsAsync(CompletionContext context) + return false; + } + + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + foreach (var language in GetLanguageProviders(context.Document.Project.Services)) { - foreach (var language in GetLanguageProviders(context.Document.Project.Services)) + var completionProvider = language.CompletionProvider; + if (completionProvider != null) { - var completionProvider = language.CompletionProvider; - if (completionProvider != null) - { - var count = context.Items.Count; - await completionProvider.ProvideCompletionsAsync(context).ConfigureAwait(false); + var count = context.Items.Count; + await completionProvider.ProvideCompletionsAsync(context).ConfigureAwait(false); - if (context.Items.Count > count) - { - return; - } + if (context.Items.Count > count) + { + return; } } } + } - public override Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) - => GetLanguage(item).CompletionProvider!.GetChangeAsync(document, item, commitKey, cancellationToken); + public override Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) + => GetLanguage(item).CompletionProvider!.GetChangeAsync(document, item, commitKey, cancellationToken); - internal override Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => GetLanguage(item).CompletionProvider!.GetDescriptionAsync(document, item, cancellationToken); + internal override Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => GetLanguage(item).CompletionProvider!.GetDescriptionAsync(document, item, cancellationToken); - private IEmbeddedLanguage GetLanguage(CompletionItem item) - { - if (_languageProviders.IsDefault) - throw ExceptionUtilities.Unreachable(); + private IEmbeddedLanguage GetLanguage(CompletionItem item) + { + if (_languageProviders.IsDefault) + throw ExceptionUtilities.Unreachable(); - return _languageProviders.Single(lang => lang.CompletionProvider?.Name == item.GetProperty(EmbeddedProviderName)); - } + return _languageProviders.Single(lang => lang.CompletionProvider?.Name == item.GetProperty(EmbeddedProviderName)); } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractAwaitCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractAwaitCompletionProvider.cs index 0b049f6643238..1ae291e7c7054 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractAwaitCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractAwaitCompletionProvider.cs @@ -17,257 +17,256 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +/// +/// A completion provider for offering keyword. +/// This is implemented separately, not as a keyword recommender as it contains extra logic for making container method async. +/// +internal abstract class AbstractAwaitCompletionProvider : LSPCompletionProvider { + private const string AwaitCompletionTargetTokenPosition = nameof(AwaitCompletionTargetTokenPosition); + private const string AppendConfigureAwait = nameof(AppendConfigureAwait); + private const string MakeContainerAsync = nameof(MakeContainerAsync); + /// - /// A completion provider for offering keyword. - /// This is implemented separately, not as a keyword recommender as it contains extra logic for making container method async. + /// If 'await' should be placed at the current position. If not present, it means to add 'await' prior + /// to the preceding expression. /// - internal abstract class AbstractAwaitCompletionProvider : LSPCompletionProvider + private const string AddAwaitAtCurrentPosition = nameof(AddAwaitAtCurrentPosition); + + protected enum DotAwaitContext { - private const string AwaitCompletionTargetTokenPosition = nameof(AwaitCompletionTargetTokenPosition); - private const string AppendConfigureAwait = nameof(AppendConfigureAwait); - private const string MakeContainerAsync = nameof(MakeContainerAsync); + None, + AwaitOnly, + AwaitAndConfigureAwait, + } - /// - /// If 'await' should be placed at the current position. If not present, it means to add 'await' prior - /// to the preceding expression. - /// - private const string AddAwaitAtCurrentPosition = nameof(AddAwaitAtCurrentPosition); + private readonly string _awaitKeyword; + private readonly string _awaitfDisplayText; + private readonly string _awaitfFilterText; + private readonly string _falseKeyword; - protected enum DotAwaitContext - { - None, - AwaitOnly, - AwaitAndConfigureAwait, - } + protected AbstractAwaitCompletionProvider(ISyntaxFacts syntaxFacts) + { + _falseKeyword = syntaxFacts.GetText(syntaxFacts.SyntaxKinds.FalseKeyword); + _awaitKeyword = syntaxFacts.GetText(syntaxFacts.SyntaxKinds.AwaitKeyword); + _awaitfDisplayText = $"{_awaitKeyword}f"; + _awaitfFilterText = $"{_awaitKeyword}F"; // Uppercase F to select "awaitf" if "af" is written. + } - private readonly string _awaitKeyword; - private readonly string _awaitfDisplayText; - private readonly string _awaitfFilterText; - private readonly string _falseKeyword; + /// + /// Gets the span start where async keyword should go. + /// + protected abstract int GetSpanStart(SyntaxNode declaration); - protected AbstractAwaitCompletionProvider(ISyntaxFacts syntaxFacts) - { - _falseKeyword = syntaxFacts.GetText(syntaxFacts.SyntaxKinds.FalseKeyword); - _awaitKeyword = syntaxFacts.GetText(syntaxFacts.SyntaxKinds.AwaitKeyword); - _awaitfDisplayText = $"{_awaitKeyword}f"; - _awaitfFilterText = $"{_awaitKeyword}F"; // Uppercase F to select "awaitf" if "af" is written. - } + protected abstract SyntaxNode? GetAsyncSupportingDeclaration(SyntaxToken token); - /// - /// Gets the span start where async keyword should go. - /// - protected abstract int GetSpanStart(SyntaxNode declaration); + protected abstract ITypeSymbol? GetTypeSymbolOfExpression(SemanticModel semanticModel, SyntaxNode potentialAwaitableExpression, CancellationToken cancellationToken); + protected abstract SyntaxNode? GetExpressionToPlaceAwaitInFrontOf(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); + protected abstract SyntaxToken? GetDotTokenLeftOfPosition(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); - protected abstract SyntaxNode? GetAsyncSupportingDeclaration(SyntaxToken token); + protected virtual bool IsAwaitKeywordContext(SyntaxContext syntaxContext) + => syntaxContext.IsAwaitKeywordContext; - protected abstract ITypeSymbol? GetTypeSymbolOfExpression(SemanticModel semanticModel, SyntaxNode potentialAwaitableExpression, CancellationToken cancellationToken); - protected abstract SyntaxNode? GetExpressionToPlaceAwaitInFrontOf(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); - protected abstract SyntaxToken? GetDotTokenLeftOfPosition(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); + private static bool IsConfigureAwaitable(Compilation compilation, ITypeSymbol symbol) + { + var originalDefinition = symbol.OriginalDefinition; + return + originalDefinition.Equals(compilation.TaskOfTType()) || + originalDefinition.Equals(compilation.TaskType()) || + originalDefinition.Equals(compilation.ValueTaskOfTType()) || + originalDefinition.Equals(compilation.ValueTaskType()); + } - protected virtual bool IsAwaitKeywordContext(SyntaxContext syntaxContext) - => syntaxContext.IsAwaitKeywordContext; + public sealed override async Task ProvideCompletionsAsync(CompletionContext context) + { + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; + var syntaxFacts = document.GetRequiredLanguageService(); + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (syntaxFacts.IsInNonUserCode(syntaxTree, position, cancellationToken)) + return; - private static bool IsConfigureAwaitable(Compilation compilation, ITypeSymbol symbol) - { - var originalDefinition = symbol.OriginalDefinition; - return - originalDefinition.Equals(compilation.TaskOfTType()) || - originalDefinition.Equals(compilation.TaskType()) || - originalDefinition.Equals(compilation.ValueTaskOfTType()) || - originalDefinition.Equals(compilation.ValueTaskType()); - } + var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); - public sealed override async Task ProvideCompletionsAsync(CompletionContext context) - { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; - var syntaxFacts = document.GetRequiredLanguageService(); - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (syntaxFacts.IsInNonUserCode(syntaxTree, position, cancellationToken)) - return; + var isAwaitKeywordContext = IsAwaitKeywordContext(syntaxContext); + var dotAwaitContext = GetDotAwaitKeywordContext(syntaxContext, cancellationToken); + if (!isAwaitKeywordContext && dotAwaitContext == DotAwaitContext.None) + return; - var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); + var token = syntaxContext.TargetToken; + var declaration = GetAsyncSupportingDeclaration(token); - var isAwaitKeywordContext = IsAwaitKeywordContext(syntaxContext); - var dotAwaitContext = GetDotAwaitKeywordContext(syntaxContext, cancellationToken); - if (!isAwaitKeywordContext && dotAwaitContext == DotAwaitContext.None) - return; + using var builder = TemporaryArray>.Empty; - var token = syntaxContext.TargetToken; - var declaration = GetAsyncSupportingDeclaration(token); + builder.Add(new KeyValuePair(AwaitCompletionTargetTokenPosition, token.SpanStart.ToString())); - using var builder = TemporaryArray>.Empty; + var makeContainerAsync = declaration is not null && !SyntaxGenerator.GetGenerator(document).GetModifiers(declaration).IsAsync; + if (makeContainerAsync) + builder.Add(new KeyValuePair(MakeContainerAsync, string.Empty)); - builder.Add(new KeyValuePair(AwaitCompletionTargetTokenPosition, token.SpanStart.ToString())); + if (isAwaitKeywordContext) + { + builder.Add(new KeyValuePair(AddAwaitAtCurrentPosition, string.Empty)); + var properties = builder.ToImmutableAndClear(); + + context.AddItem(CreateCompletionItem( + properties, _awaitKeyword, _awaitKeyword, + FeaturesResources.Asynchronously_waits_for_the_task_to_finish, + isComplexTextEdit: makeContainerAsync, + appendConfigureAwait: false)); + } + else + { + Contract.ThrowIfTrue(dotAwaitContext == DotAwaitContext.None); - var makeContainerAsync = declaration is not null && !SyntaxGenerator.GetGenerator(document).GetModifiers(declaration).IsAsync; - if (makeContainerAsync) - builder.Add(new KeyValuePair(MakeContainerAsync, string.Empty)); + var properties = builder.ToImmutableAndClear(); - if (isAwaitKeywordContext) - { - builder.Add(new KeyValuePair(AddAwaitAtCurrentPosition, string.Empty)); - var properties = builder.ToImmutableAndClear(); + // add the `await` option that will remove the dot and add `await` to the start of the expression. + context.AddItem(CreateCompletionItem( + properties, _awaitKeyword, _awaitKeyword, + FeaturesResources.Await_the_preceding_expression, + isComplexTextEdit: true, + appendConfigureAwait: false)); - context.AddItem(CreateCompletionItem( - properties, _awaitKeyword, _awaitKeyword, - FeaturesResources.Asynchronously_waits_for_the_task_to_finish, - isComplexTextEdit: makeContainerAsync, - appendConfigureAwait: false)); - } - else + if (dotAwaitContext == DotAwaitContext.AwaitAndConfigureAwait) { - Contract.ThrowIfTrue(dotAwaitContext == DotAwaitContext.None); - - var properties = builder.ToImmutableAndClear(); - - // add the `await` option that will remove the dot and add `await` to the start of the expression. + // add the `awaitf` option to do the same, but also add .ConfigureAwait(false); + properties = properties.Add(new KeyValuePair(AppendConfigureAwait, string.Empty)); context.AddItem(CreateCompletionItem( - properties, _awaitKeyword, _awaitKeyword, - FeaturesResources.Await_the_preceding_expression, + properties, _awaitfDisplayText, _awaitfFilterText, + string.Format(FeaturesResources.Await_the_preceding_expression_and_add_ConfigureAwait_0, _falseKeyword), isComplexTextEdit: true, - appendConfigureAwait: false)); - - if (dotAwaitContext == DotAwaitContext.AwaitAndConfigureAwait) - { - // add the `awaitf` option to do the same, but also add .ConfigureAwait(false); - properties = properties.Add(new KeyValuePair(AppendConfigureAwait, string.Empty)); - context.AddItem(CreateCompletionItem( - properties, _awaitfDisplayText, _awaitfFilterText, - string.Format(FeaturesResources.Await_the_preceding_expression_and_add_ConfigureAwait_0, _falseKeyword), - isComplexTextEdit: true, - appendConfigureAwait: true)); - } - } - - return; - - static CompletionItem CreateCompletionItem( - ImmutableArray> completionProperties, string displayText, string filterText, string tooltip, bool isComplexTextEdit, bool appendConfigureAwait) - { - var description = appendConfigureAwait - ? [new SymbolDisplayPart(SymbolDisplayPartKind.Text, null, tooltip)] - : RecommendedKeyword.CreateDisplayParts(displayText, tooltip); - - return CommonCompletionItem.Create( - displayText: displayText, - displayTextSuffix: "", - filterText: filterText, - rules: CompletionItemRules.Default, - glyph: Glyph.Keyword, - description: description, - isComplexTextEdit: isComplexTextEdit, - properties: completionProperties); + appendConfigureAwait: true)); } } - public sealed override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) - { - // IsComplexTextEdit is true when we want to add async to the container or place await in front of the expression. - if (!item.IsComplexTextEdit) - return await base.GetChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false); + return; - using var _ = ArrayBuilder.GetInstance(out var builder); + static CompletionItem CreateCompletionItem( + ImmutableArray> completionProperties, string displayText, string filterText, string tooltip, bool isComplexTextEdit, bool appendConfigureAwait) + { + var description = appendConfigureAwait + ? [new SymbolDisplayPart(SymbolDisplayPartKind.Text, null, tooltip)] + : RecommendedKeyword.CreateDisplayParts(displayText, tooltip); + + return CommonCompletionItem.Create( + displayText: displayText, + displayTextSuffix: "", + filterText: filterText, + rules: CompletionItemRules.Default, + glyph: Glyph.Keyword, + description: description, + isComplexTextEdit: isComplexTextEdit, + properties: completionProperties); + } + } - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); - var syntaxKinds = syntaxFacts.SyntaxKinds; + public sealed override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) + { + // IsComplexTextEdit is true when we want to add async to the container or place await in front of the expression. + if (!item.IsComplexTextEdit) + return await base.GetChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false); - if (item.TryGetProperty(MakeContainerAsync, out var _)) - { - var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var tokenPosition = int.Parse(item.GetProperty(AwaitCompletionTargetTokenPosition)); - var declaration = GetAsyncSupportingDeclaration(root.FindToken(tokenPosition)); - if (declaration is null) - { - // IsComplexTextEdit should only be true when GetAsyncSupportingDeclaration returns non-null. - // This is ensured by the ShouldMakeContainerAsync overrides. - Debug.Fail("Expected non-null value for declaration."); - return await base.GetChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false); - } + using var _ = ArrayBuilder.GetInstance(out var builder); - builder.Add(new TextChange(new TextSpan(GetSpanStart(declaration), 0), syntaxFacts.GetText(syntaxKinds.AsyncKeyword) + " ")); - } + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + var syntaxKinds = syntaxFacts.SyntaxKinds; - if (item.TryGetProperty(AddAwaitAtCurrentPosition, out var _)) + if (item.TryGetProperty(MakeContainerAsync, out var _)) + { + var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var tokenPosition = int.Parse(item.GetProperty(AwaitCompletionTargetTokenPosition)); + var declaration = GetAsyncSupportingDeclaration(root.FindToken(tokenPosition)); + if (declaration is null) { - builder.Add(new TextChange(item.Span, _awaitKeyword)); + // IsComplexTextEdit should only be true when GetAsyncSupportingDeclaration returns non-null. + // This is ensured by the ShouldMakeContainerAsync overrides. + Debug.Fail("Expected non-null value for declaration."); + return await base.GetChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false); } - else - { - var position = item.Span.Start; - var dotToken = GetDotTokenLeftOfPosition(syntaxTree, position, cancellationToken); - var expr = GetExpressionToPlaceAwaitInFrontOf(syntaxTree, position, cancellationToken); - Contract.ThrowIfFalse(dotToken.HasValue); - Contract.ThrowIfNull(expr); + builder.Add(new TextChange(new TextSpan(GetSpanStart(declaration), 0), syntaxFacts.GetText(syntaxKinds.AsyncKeyword) + " ")); + } - // place "await" in front of expr - builder.Add(new TextChange(new TextSpan(expr.SpanStart, 0), _awaitKeyword + " ")); + if (item.TryGetProperty(AddAwaitAtCurrentPosition, out var _)) + { + builder.Add(new TextChange(item.Span, _awaitKeyword)); + } + else + { + var position = item.Span.Start; + var dotToken = GetDotTokenLeftOfPosition(syntaxTree, position, cancellationToken); + var expr = GetExpressionToPlaceAwaitInFrontOf(syntaxTree, position, cancellationToken); - // remove any text after dot, including the dot token and optionally append .ConfigureAwait(false) - var replacementText = item.TryGetProperty(AppendConfigureAwait, out var _) - ? $".{nameof(Task.ConfigureAwait)}({_falseKeyword})" - : ""; + Contract.ThrowIfFalse(dotToken.HasValue); + Contract.ThrowIfNull(expr); - builder.Add(new TextChange(TextSpan.FromBounds(dotToken.Value.SpanStart, item.Span.End), replacementText)); - } + // place "await" in front of expr + builder.Add(new TextChange(new TextSpan(expr.SpanStart, 0), _awaitKeyword + " ")); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var newText = text.WithChanges(builder); - var allChanges = builder.ToImmutable(); + // remove any text after dot, including the dot token and optionally append .ConfigureAwait(false) + var replacementText = item.TryGetProperty(AppendConfigureAwait, out var _) + ? $".{nameof(Task.ConfigureAwait)}({_falseKeyword})" + : ""; - // Collapse all text changes down to a single change (for clients that only care about that), but also keep - // all the individual changes around for clients that prefer the fine-grained information. - return CompletionChange.Create(Utilities.Collapse(newText, allChanges), allChanges); + builder.Add(new TextChange(TextSpan.FromBounds(dotToken.Value.SpanStart, item.Span.End), replacementText)); } - /// - /// Should be offered, if left of the dot at position is an awaitable expression? - /// - /// someTask.$$ // Suggest await completion - /// await someTask.$$ // Don't suggest await completion - /// - /// - /// - /// , if await can not be suggested for the expression left of the dot. - /// , if await should be suggested for the expression left of the dot, but ConfigureAwait(false) not. - /// , if await should be suggested for the expression left of the dot and ConfigureAwait(false). - /// - private DotAwaitContext GetDotAwaitKeywordContext(SyntaxContext syntaxContext, CancellationToken cancellationToken) + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var newText = text.WithChanges(builder); + var allChanges = builder.ToImmutable(); + + // Collapse all text changes down to a single change (for clients that only care about that), but also keep + // all the individual changes around for clients that prefer the fine-grained information. + return CompletionChange.Create(Utilities.Collapse(newText, allChanges), allChanges); + } + + /// + /// Should be offered, if left of the dot at position is an awaitable expression? + /// + /// someTask.$$ // Suggest await completion + /// await someTask.$$ // Don't suggest await completion + /// + /// + /// + /// , if await can not be suggested for the expression left of the dot. + /// , if await should be suggested for the expression left of the dot, but ConfigureAwait(false) not. + /// , if await should be suggested for the expression left of the dot and ConfigureAwait(false). + /// + private DotAwaitContext GetDotAwaitKeywordContext(SyntaxContext syntaxContext, CancellationToken cancellationToken) + { + var position = syntaxContext.Position; + var syntaxTree = syntaxContext.SyntaxTree; + var potentialAwaitableExpression = GetExpressionToPlaceAwaitInFrontOf(syntaxTree, position, cancellationToken); + if (potentialAwaitableExpression is not null) { - var position = syntaxContext.Position; - var syntaxTree = syntaxContext.SyntaxTree; - var potentialAwaitableExpression = GetExpressionToPlaceAwaitInFrontOf(syntaxTree, position, cancellationToken); - if (potentialAwaitableExpression is not null) + var parentOfAwaitable = potentialAwaitableExpression.Parent; + var document = syntaxContext.Document; + var syntaxFacts = document.GetRequiredLanguageService(); + if (!syntaxFacts.IsAwaitExpression(parentOfAwaitable)) { - var parentOfAwaitable = potentialAwaitableExpression.Parent; - var document = syntaxContext.Document; - var syntaxFacts = document.GetRequiredLanguageService(); - if (!syntaxFacts.IsAwaitExpression(parentOfAwaitable)) + var semanticModel = syntaxContext.SemanticModel; + var symbol = GetTypeSymbolOfExpression(semanticModel, potentialAwaitableExpression, cancellationToken); + if (symbol.IsAwaitableNonDynamic(semanticModel, position)) { - var semanticModel = syntaxContext.SemanticModel; - var symbol = GetTypeSymbolOfExpression(semanticModel, potentialAwaitableExpression, cancellationToken); - if (symbol.IsAwaitableNonDynamic(semanticModel, position)) + // We have a awaitable type left of the dot, that is not yet awaited. + // We need to check if await is valid at the insertion position. + var syntaxContextAtInsertationPosition = syntaxContext.GetRequiredLanguageService().CreateContext( + document, syntaxContext.SemanticModel, potentialAwaitableExpression.SpanStart, cancellationToken); + if (syntaxContextAtInsertationPosition.IsAwaitKeywordContext) { - // We have a awaitable type left of the dot, that is not yet awaited. - // We need to check if await is valid at the insertion position. - var syntaxContextAtInsertationPosition = syntaxContext.GetRequiredLanguageService().CreateContext( - document, syntaxContext.SemanticModel, potentialAwaitableExpression.SpanStart, cancellationToken); - if (syntaxContextAtInsertationPosition.IsAwaitKeywordContext) - { - return IsConfigureAwaitable(syntaxContext.SemanticModel.Compilation, symbol) - ? DotAwaitContext.AwaitAndConfigureAwait - : DotAwaitContext.AwaitOnly; - } + return IsConfigureAwaitable(syntaxContext.SemanticModel.Compilation, symbol) + ? DotAwaitContext.AwaitAndConfigureAwait + : DotAwaitContext.AwaitOnly; } } } - - return DotAwaitContext.None; } + + return DotAwaitContext.None; } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractContextVariableArgumentProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractContextVariableArgumentProvider.cs index 8b095dc0cd4cc..843781641a9dc 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractContextVariableArgumentProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractContextVariableArgumentProvider.cs @@ -8,158 +8,157 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +/// +/// This attempts to locate a matching value in the context of a method invocation. +/// +internal abstract class AbstractContextVariableArgumentProvider : ArgumentProvider { - /// - /// This attempts to locate a matching value in the context of a method invocation. - /// - internal abstract class AbstractContextVariableArgumentProvider : ArgumentProvider + protected abstract string ThisOrMeKeyword { get; } + + protected abstract bool IsInstanceContext(SyntaxTree syntaxTree, SyntaxToken targetToken, SemanticModel semanticModel, CancellationToken cancellationToken); + + public override async Task ProvideArgumentAsync(ArgumentContext context) { - protected abstract string ThisOrMeKeyword { get; } + if (context.PreviousValue is not null) + { + // This argument provider does not attempt to replace arguments already in code. + return; + } - protected abstract bool IsInstanceContext(SyntaxTree syntaxTree, SyntaxToken targetToken, SemanticModel semanticModel, CancellationToken cancellationToken); + var requireExactType = context.Parameter.Type.IsSpecialType() + || context.Parameter.RefKind != RefKind.None; + var symbols = context.SemanticModel.LookupSymbols(context.Position); - public override async Task ProvideArgumentAsync(ArgumentContext context) + // First try to find a local variable + ISymbol? bestSymbol = null; + string? bestSymbolName = null; + CommonConversion bestConversion = default; + foreach (var symbol in symbols) { - if (context.PreviousValue is not null) - { - // This argument provider does not attempt to replace arguments already in code. - return; - } + ISymbol candidate; + if (symbol.IsKind(SymbolKind.Parameter, out IParameterSymbol? parameter)) + candidate = parameter; + else if (symbol.IsKind(SymbolKind.Local, out ILocalSymbol? local)) + candidate = local; + else + continue; + + CheckCandidate(candidate); + } - var requireExactType = context.Parameter.Type.IsSpecialType() - || context.Parameter.RefKind != RefKind.None; - var symbols = context.SemanticModel.LookupSymbols(context.Position); + if (bestSymbol is not null) + { + context.DefaultValue = bestSymbolName; + return; + } - // First try to find a local variable - ISymbol? bestSymbol = null; - string? bestSymbolName = null; - CommonConversion bestConversion = default; - foreach (var symbol in symbols) + // Next try fields and properties of the current type + foreach (var symbol in symbols) + { + ISymbol candidate; + if (symbol.IsKind(SymbolKind.Field, out IFieldSymbol? field)) + candidate = field; + else if (symbol.IsKind(SymbolKind.Property, out IPropertySymbol? property)) + candidate = property; + else + continue; + + // Require a name match for primitive types + if (candidate.GetSymbolType().IsSpecialType() + && !string.Equals(candidate.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase)) { - ISymbol candidate; - if (symbol.IsKind(SymbolKind.Parameter, out IParameterSymbol? parameter)) - candidate = parameter; - else if (symbol.IsKind(SymbolKind.Local, out ILocalSymbol? local)) - candidate = local; - else - continue; - - CheckCandidate(candidate); + continue; } - if (bestSymbol is not null) + CheckCandidate(candidate); + } + + if (bestSymbol is not null) + { + context.DefaultValue = bestSymbolName; + return; + } + + // Finally, if the invocation occurs in an instance context, check the current type ('this' or 'Me') + var tree = context.SemanticModel.SyntaxTree; + var targetToken = await tree.GetTouchingTokenAsync(context.Position, context.CancellationToken).ConfigureAwait(false); + if (IsInstanceContext(tree, targetToken, context.SemanticModel, context.CancellationToken)) + { + var enclosingSymbol = context.SemanticModel.GetEnclosingSymbol(targetToken.SpanStart, context.CancellationToken); + while (enclosingSymbol is IMethodSymbol { MethodKind: MethodKind.LocalFunction or MethodKind.AnonymousFunction }) { - context.DefaultValue = bestSymbolName; - return; + // It is allowed to reference the instance (`this`) within a local function or anonymous function, + // as long as the containing method allows it + enclosingSymbol = enclosingSymbol.ContainingSymbol; } - // Next try fields and properties of the current type - foreach (var symbol in symbols) + if (enclosingSymbol is IMethodSymbol { ContainingType: { } containingType }) { - ISymbol candidate; - if (symbol.IsKind(SymbolKind.Field, out IFieldSymbol? field)) - candidate = field; - else if (symbol.IsKind(SymbolKind.Property, out IPropertySymbol? property)) - candidate = property; - else - continue; - - // Require a name match for primitive types - if (candidate.GetSymbolType().IsSpecialType() - && !string.Equals(candidate.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - CheckCandidate(candidate); + CheckCandidate(containingType, ThisOrMeKeyword); } + } - if (bestSymbol is not null) + if (bestSymbol is not null) + { + context.DefaultValue = bestSymbolName; + return; + } + + // Local functions + void CheckCandidate(ISymbol candidate, string? overridingName = null) + { + if (candidate.GetSymbolType() is not { } symbolType) { - context.DefaultValue = bestSymbolName; return; } - // Finally, if the invocation occurs in an instance context, check the current type ('this' or 'Me') - var tree = context.SemanticModel.SyntaxTree; - var targetToken = await tree.GetTouchingTokenAsync(context.Position, context.CancellationToken).ConfigureAwait(false); - if (IsInstanceContext(tree, targetToken, context.SemanticModel, context.CancellationToken)) + if (requireExactType && !SymbolEqualityComparer.Default.Equals(context.Parameter.Type, symbolType)) { - var enclosingSymbol = context.SemanticModel.GetEnclosingSymbol(targetToken.SpanStart, context.CancellationToken); - while (enclosingSymbol is IMethodSymbol { MethodKind: MethodKind.LocalFunction or MethodKind.AnonymousFunction }) - { - // It is allowed to reference the instance (`this`) within a local function or anonymous function, - // as long as the containing method allows it - enclosingSymbol = enclosingSymbol.ContainingSymbol; - } - - if (enclosingSymbol is IMethodSymbol { ContainingType: { } containingType }) - { - CheckCandidate(containingType, ThisOrMeKeyword); - } + return; } - if (bestSymbol is not null) + var conversion = context.SemanticModel.Compilation.ClassifyCommonConversion(symbolType, context.Parameter.Type); + if (!conversion.IsImplicit) { - context.DefaultValue = bestSymbolName; return; } - // Local functions - void CheckCandidate(ISymbol candidate, string? overridingName = null) + if (bestSymbol is not null && !IsNewConversionSameOrBetter(conversion)) { - if (candidate.GetSymbolType() is not { } symbolType) - { + if (!IsNewConversionSameOrBetter(conversion)) return; - } - if (requireExactType && !SymbolEqualityComparer.Default.Equals(context.Parameter.Type, symbolType)) - { + if (!IsNewNameSameOrBetter(candidate)) return; - } - - var conversion = context.SemanticModel.Compilation.ClassifyCommonConversion(symbolType, context.Parameter.Type); - if (!conversion.IsImplicit) - { - return; - } - - if (bestSymbol is not null && !IsNewConversionSameOrBetter(conversion)) - { - if (!IsNewConversionSameOrBetter(conversion)) - return; - - if (!IsNewNameSameOrBetter(candidate)) - return; - } - - bestSymbol = candidate; - bestSymbolName = overridingName ?? bestSymbol.Name; - bestConversion = conversion; } - bool IsNewConversionSameOrBetter(CommonConversion conversion) - { - if (bestConversion.IsIdentity && !conversion.IsIdentity) - return false; + bestSymbol = candidate; + bestSymbolName = overridingName ?? bestSymbol.Name; + bestConversion = conversion; + } - if (bestConversion.IsImplicit && !conversion.IsImplicit) - return false; + bool IsNewConversionSameOrBetter(CommonConversion conversion) + { + if (bestConversion.IsIdentity && !conversion.IsIdentity) + return false; - return true; - } + if (bestConversion.IsImplicit && !conversion.IsImplicit) + return false; - bool IsNewNameSameOrBetter(ISymbol symbol) - { - if (string.Equals(bestSymbol.Name, context.Parameter.Name)) - return string.Equals(symbol.Name, context.Parameter.Name); + return true; + } + + bool IsNewNameSameOrBetter(ISymbol symbol) + { + if (string.Equals(bestSymbol.Name, context.Parameter.Name)) + return string.Equals(symbol.Name, context.Parameter.Name); - if (string.Equals(bestSymbol.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase)) - return string.Equals(symbol.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase); + if (string.Equals(bestSymbol.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase)) + return string.Equals(symbol.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase); - return true; - } + return true; } } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractCrefCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractCrefCompletionProvider.cs index 0b809fac4e63a..a9b7ff58ab5f1 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractCrefCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractCrefCompletionProvider.cs @@ -9,39 +9,38 @@ using Microsoft.CodeAnalysis.Options; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractCrefCompletionProvider : LSPCompletionProvider { - internal abstract class AbstractCrefCompletionProvider : LSPCompletionProvider + protected const string HideAdvancedMembers = nameof(HideAdvancedMembers); + + internal override async Task GetDescriptionWorkerAsync( + Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) { - protected const string HideAdvancedMembers = nameof(HideAdvancedMembers); + var position = SymbolCompletionItem.GetContextPosition(item); - internal override async Task GetDescriptionWorkerAsync( - Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + // What EditorBrowsable settings were we previously passed in (if it mattered)? + if (item.TryGetProperty(HideAdvancedMembers, out var hideAdvancedMembersString) && + bool.TryParse(hideAdvancedMembersString, out var hideAdvancedMembers)) { - var position = SymbolCompletionItem.GetContextPosition(item); - - // What EditorBrowsable settings were we previously passed in (if it mattered)? - if (item.TryGetProperty(HideAdvancedMembers, out var hideAdvancedMembersString) && - bool.TryParse(hideAdvancedMembersString, out var hideAdvancedMembers)) - { - options = options with { HideAdvancedMembers = hideAdvancedMembers }; - } - - var (token, semanticModel, symbols) = await GetSymbolsAsync(document, position, options, cancellationToken).ConfigureAwait(false); - if (symbols.Length == 0) - { - return CompletionDescription.Empty; - } - - Contract.ThrowIfNull(semanticModel); - - var name = SymbolCompletionItem.GetSymbolName(item); - var kind = SymbolCompletionItem.GetKind(item); - var bestSymbols = symbols.WhereAsArray(s => s.Kind == kind && s.Name == name); - return await SymbolCompletionItem.GetDescriptionAsync(item, bestSymbols, document, semanticModel, displayOptions, cancellationToken).ConfigureAwait(false); + options = options with { HideAdvancedMembers = hideAdvancedMembers }; } - protected abstract Task<(SyntaxToken, SemanticModel?, ImmutableArray)> GetSymbolsAsync( - Document document, int position, CompletionOptions options, CancellationToken cancellationToken); + var (token, semanticModel, symbols) = await GetSymbolsAsync(document, position, options, cancellationToken).ConfigureAwait(false); + if (symbols.Length == 0) + { + return CompletionDescription.Empty; + } + + Contract.ThrowIfNull(semanticModel); + + var name = SymbolCompletionItem.GetSymbolName(item); + var kind = SymbolCompletionItem.GetKind(item); + var bestSymbols = symbols.WhereAsArray(s => s.Kind == kind && s.Name == name); + return await SymbolCompletionItem.GetDescriptionAsync(item, bestSymbols, document, semanticModel, displayOptions, cancellationToken).ConfigureAwait(false); } + + protected abstract Task<(SyntaxToken, SemanticModel?, ImmutableArray)> GetSymbolsAsync( + Document document, int position, CompletionOptions options, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractDefaultArgumentProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractDefaultArgumentProvider.cs index 3c8dd2b499967..59e18862907b6 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractDefaultArgumentProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractDefaultArgumentProvider.cs @@ -2,9 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal abstract class AbstractDefaultArgumentProvider : ArgumentProvider { - internal abstract class AbstractDefaultArgumentProvider : ArgumentProvider - { - } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractDocCommentCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractDocCommentCompletionProvider.cs index c4eb2d0e37d92..4f2066f4253d4 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractDocCommentCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractDocCommentCompletionProvider.cs @@ -13,327 +13,326 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +using static DocumentationCommentXmlNames; + +internal abstract class AbstractDocCommentCompletionProvider : LSPCompletionProvider + where TSyntax : SyntaxNode { - using static DocumentationCommentXmlNames; + // Tag names + private static readonly ImmutableArray s_listTagNames = [ListHeaderElementName, TermElementName, ItemElementName, DescriptionElementName]; + private static readonly ImmutableArray s_listHeaderTagNames = [TermElementName, DescriptionElementName]; + private static readonly ImmutableArray s_nestedTagNames = [CElementName, CodeElementName, ParaElementName, ListElementName]; + private static readonly ImmutableArray s_topLevelRepeatableTagNames = [ExceptionElementName, IncludeElementName, PermissionElementName]; + private static readonly ImmutableArray s_topLevelSingleUseTagNames = [SummaryElementName, RemarksElementName, ExampleElementName, CompletionListElementName]; + + private static readonly Dictionary s_tagMap = + new Dictionary() + { + // tagOpen textBeforeCaret $$ textAfterCaret tagClose + { ExceptionElementName, ($"<{ExceptionElementName}", $" {CrefAttributeName}=\"", "\"", null) }, + { IncludeElementName, ($"<{IncludeElementName}", $" {FileAttributeName}=\'", $"\' {PathAttributeName}=\'[@name=\"\"]\'", "/>") }, + { InheritdocElementName, ($"<{InheritdocElementName}", $"", "", "/>") }, + { PermissionElementName, ($"<{PermissionElementName}", $" {CrefAttributeName}=\"", "\"", null) }, + { SeeElementName, ($"<{SeeElementName}", $" {CrefAttributeName}=\"", "\"", "/>") }, + { SeeAlsoElementName, ($"<{SeeAlsoElementName}", $" {CrefAttributeName}=\"", "\"", "/>") }, + { ListElementName, ($"<{ListElementName}", $" {TypeAttributeName}=\"", "\"", null) }, + { ParameterReferenceElementName, ($"<{ParameterReferenceElementName}", $" {NameAttributeName}=\"", "\"", "/>") }, + { TypeParameterReferenceElementName, ($"<{TypeParameterReferenceElementName}", $" {NameAttributeName}=\"", "\"", "/>") }, + { CompletionListElementName, ($"<{CompletionListElementName}", $" {CrefAttributeName}=\"", "\"", "/>") }, + }; + + private static readonly ImmutableArray<(string elementName, string attributeName, string text)> s_attributeMap = + [ + (ExceptionElementName, CrefAttributeName, $"{CrefAttributeName}=\""), + (PermissionElementName, CrefAttributeName, $"{CrefAttributeName}=\""), + (SeeElementName, CrefAttributeName, $"{CrefAttributeName}=\""), + (SeeElementName, LangwordAttributeName, $"{LangwordAttributeName}=\""), + (SeeElementName, HrefAttributeName, $"{HrefAttributeName}=\""), + (SeeAlsoElementName, CrefAttributeName, $"{CrefAttributeName}=\""), + (SeeAlsoElementName, HrefAttributeName, $"{HrefAttributeName}=\""), + (ListElementName, TypeAttributeName, $"{TypeAttributeName}=\""), + (ParameterElementName, NameAttributeName, $"{NameAttributeName}=\""), + (ParameterReferenceElementName, NameAttributeName, $"{NameAttributeName}=\""), + (TypeParameterElementName, NameAttributeName, $"{NameAttributeName}=\""), + (TypeParameterReferenceElementName, NameAttributeName, $"{NameAttributeName}=\""), + (IncludeElementName, FileAttributeName, $"{FileAttributeName}=\""), + (IncludeElementName, PathAttributeName, $"{PathAttributeName}=\""), + (InheritdocElementName, CrefAttributeName, $"{CrefAttributeName}=\""), + (InheritdocElementName, PathAttributeName, $"{PathAttributeName}=\""), + ]; + + private static readonly ImmutableArray s_listTypeValues = ["bullet", "number", "table"]; + + private readonly CompletionItemRules defaultRules; + + protected AbstractDocCommentCompletionProvider(CompletionItemRules defaultRules) + { + this.defaultRules = defaultRules ?? throw new ArgumentNullException(nameof(defaultRules)); + } - internal abstract class AbstractDocCommentCompletionProvider : LSPCompletionProvider - where TSyntax : SyntaxNode + public override async Task ProvideCompletionsAsync(CompletionContext context) { - // Tag names - private static readonly ImmutableArray s_listTagNames = [ListHeaderElementName, TermElementName, ItemElementName, DescriptionElementName]; - private static readonly ImmutableArray s_listHeaderTagNames = [TermElementName, DescriptionElementName]; - private static readonly ImmutableArray s_nestedTagNames = [CElementName, CodeElementName, ParaElementName, ListElementName]; - private static readonly ImmutableArray s_topLevelRepeatableTagNames = [ExceptionElementName, IncludeElementName, PermissionElementName]; - private static readonly ImmutableArray s_topLevelSingleUseTagNames = [SummaryElementName, RemarksElementName, ExampleElementName, CompletionListElementName]; - - private static readonly Dictionary s_tagMap = - new Dictionary() - { - // tagOpen textBeforeCaret $$ textAfterCaret tagClose - { ExceptionElementName, ($"<{ExceptionElementName}", $" {CrefAttributeName}=\"", "\"", null) }, - { IncludeElementName, ($"<{IncludeElementName}", $" {FileAttributeName}=\'", $"\' {PathAttributeName}=\'[@name=\"\"]\'", "/>") }, - { InheritdocElementName, ($"<{InheritdocElementName}", $"", "", "/>") }, - { PermissionElementName, ($"<{PermissionElementName}", $" {CrefAttributeName}=\"", "\"", null) }, - { SeeElementName, ($"<{SeeElementName}", $" {CrefAttributeName}=\"", "\"", "/>") }, - { SeeAlsoElementName, ($"<{SeeAlsoElementName}", $" {CrefAttributeName}=\"", "\"", "/>") }, - { ListElementName, ($"<{ListElementName}", $" {TypeAttributeName}=\"", "\"", null) }, - { ParameterReferenceElementName, ($"<{ParameterReferenceElementName}", $" {NameAttributeName}=\"", "\"", "/>") }, - { TypeParameterReferenceElementName, ($"<{TypeParameterReferenceElementName}", $" {NameAttributeName}=\"", "\"", "/>") }, - { CompletionListElementName, ($"<{CompletionListElementName}", $" {CrefAttributeName}=\"", "\"", "/>") }, - }; - - private static readonly ImmutableArray<(string elementName, string attributeName, string text)> s_attributeMap = - [ - (ExceptionElementName, CrefAttributeName, $"{CrefAttributeName}=\""), - (PermissionElementName, CrefAttributeName, $"{CrefAttributeName}=\""), - (SeeElementName, CrefAttributeName, $"{CrefAttributeName}=\""), - (SeeElementName, LangwordAttributeName, $"{LangwordAttributeName}=\""), - (SeeElementName, HrefAttributeName, $"{HrefAttributeName}=\""), - (SeeAlsoElementName, CrefAttributeName, $"{CrefAttributeName}=\""), - (SeeAlsoElementName, HrefAttributeName, $"{HrefAttributeName}=\""), - (ListElementName, TypeAttributeName, $"{TypeAttributeName}=\""), - (ParameterElementName, NameAttributeName, $"{NameAttributeName}=\""), - (ParameterReferenceElementName, NameAttributeName, $"{NameAttributeName}=\""), - (TypeParameterElementName, NameAttributeName, $"{NameAttributeName}=\""), - (TypeParameterReferenceElementName, NameAttributeName, $"{NameAttributeName}=\""), - (IncludeElementName, FileAttributeName, $"{FileAttributeName}=\""), - (IncludeElementName, PathAttributeName, $"{PathAttributeName}=\""), - (InheritdocElementName, CrefAttributeName, $"{CrefAttributeName}=\""), - (InheritdocElementName, PathAttributeName, $"{PathAttributeName}=\""), - ]; - - private static readonly ImmutableArray s_listTypeValues = ["bullet", "number", "table"]; - - private readonly CompletionItemRules defaultRules; - - protected AbstractDocCommentCompletionProvider(CompletionItemRules defaultRules) + if (!context.CompletionOptions.ShowXmlDocCommentCompletion) { - this.defaultRules = defaultRules ?? throw new ArgumentNullException(nameof(defaultRules)); + return; } - public override async Task ProvideCompletionsAsync(CompletionContext context) - { - if (!context.CompletionOptions.ShowXmlDocCommentCompletion) - { - return; - } - - var items = await GetItemsWorkerAsync( - context.Document, context.Position, context.Trigger, context.CancellationToken).ConfigureAwait(false); + var items = await GetItemsWorkerAsync( + context.Document, context.Position, context.Trigger, context.CancellationToken).ConfigureAwait(false); - if (items != null) - { - context.AddItems(items); - } + if (items != null) + { + context.AddItems(items); } + } - protected abstract Task?> GetItemsWorkerAsync(Document document, int position, CompletionTrigger trigger, CancellationToken cancellationToken); - - protected abstract IEnumerable GetExistingTopLevelElementNames(TSyntax syntax); + protected abstract Task?> GetItemsWorkerAsync(Document document, int position, CompletionTrigger trigger, CancellationToken cancellationToken); - protected abstract IEnumerable GetExistingTopLevelAttributeValues(TSyntax syntax, string tagName, string attributeName); + protected abstract IEnumerable GetExistingTopLevelElementNames(TSyntax syntax); - protected abstract IEnumerable GetKeywordNames(); + protected abstract IEnumerable GetExistingTopLevelAttributeValues(TSyntax syntax, string tagName, string attributeName); - /// - /// A temporarily hack that should be removed once/if https://github.com/dotnet/roslyn/issues/53092 is fixed. - /// - protected abstract ImmutableArray GetParameters(ISymbol symbol); + protected abstract IEnumerable GetKeywordNames(); - private CompletionItem GetItem(string name) - { - if (s_tagMap.TryGetValue(name, out var values)) - { - return CreateCompletionItem(name, - beforeCaretText: values.tagOpen + values.textBeforeCaret, - afterCaretText: values.textAfterCaret + values.tagClose); - } + /// + /// A temporarily hack that should be removed once/if https://github.com/dotnet/roslyn/issues/53092 is fixed. + /// + protected abstract ImmutableArray GetParameters(ISymbol symbol); - return CreateCompletionItem(name); - } - - protected IEnumerable GetAttributeItems(string tagName, ISet existingAttributes) + private CompletionItem GetItem(string name) + { + if (s_tagMap.TryGetValue(name, out var values)) { - return s_attributeMap.Where(x => x.elementName == tagName && !existingAttributes.Contains(x.attributeName)) - .Select(x => CreateCompletionItem(x.attributeName, beforeCaretText: x.text, afterCaretText: "\"")); + return CreateCompletionItem(name, + beforeCaretText: values.tagOpen + values.textBeforeCaret, + afterCaretText: values.textAfterCaret + values.tagClose); } - protected IEnumerable GetAlwaysVisibleItems() - => [GetCDataItem(), GetCommentItem(), GetItem(InheritdocElementName), GetItem(SeeElementName), GetItem(SeeAlsoElementName)]; + return CreateCompletionItem(name); + } - private CompletionItem GetCommentItem() - { - const string prefix = "!--"; - const string suffix = "-->"; - return CreateCompletionItem(prefix, beforeCaretText: "<" + prefix, afterCaretText: suffix); - } + protected IEnumerable GetAttributeItems(string tagName, ISet existingAttributes) + { + return s_attributeMap.Where(x => x.elementName == tagName && !existingAttributes.Contains(x.attributeName)) + .Select(x => CreateCompletionItem(x.attributeName, beforeCaretText: x.text, afterCaretText: "\"")); + } - private CompletionItem GetCDataItem() - { - const string prefix = "![CDATA["; - const string suffix = "]]>"; - return CreateCompletionItem(prefix, beforeCaretText: "<" + prefix, afterCaretText: suffix); - } + protected IEnumerable GetAlwaysVisibleItems() + => [GetCDataItem(), GetCommentItem(), GetItem(InheritdocElementName), GetItem(SeeElementName), GetItem(SeeAlsoElementName)]; - protected IEnumerable GetNestedItems(ISymbol? symbol, bool includeKeywords) - { - var items = s_nestedTagNames.Select(GetItem); + private CompletionItem GetCommentItem() + { + const string prefix = "!--"; + const string suffix = "-->"; + return CreateCompletionItem(prefix, beforeCaretText: "<" + prefix, afterCaretText: suffix); + } - if (symbol != null) - { - items = items.Concat(GetParamRefItems(symbol)) - .Concat(GetTypeParamRefItems(symbol)); - } + private CompletionItem GetCDataItem() + { + const string prefix = "![CDATA["; + const string suffix = "]]>"; + return CreateCompletionItem(prefix, beforeCaretText: "<" + prefix, afterCaretText: suffix); + } - if (includeKeywords) - { - items = items.Concat(GetKeywordNames().Select(CreateLangwordCompletionItem)); - } + protected IEnumerable GetNestedItems(ISymbol? symbol, bool includeKeywords) + { + var items = s_nestedTagNames.Select(GetItem); - return items; + if (symbol != null) + { + items = items.Concat(GetParamRefItems(symbol)) + .Concat(GetTypeParamRefItems(symbol)); } - private IEnumerable GetParamRefItems(ISymbol symbol) + if (includeKeywords) { - var names = GetParameters(symbol).Select(p => p.Name); - - return names.Select(p => CreateCompletionItem( - displayText: FormatParameter(ParameterReferenceElementName, p), - beforeCaretText: FormatParameterRefTag(ParameterReferenceElementName, p), - afterCaretText: string.Empty)); + items = items.Concat(GetKeywordNames().Select(CreateLangwordCompletionItem)); } - private IEnumerable GetTypeParamRefItems(ISymbol symbol) - { - var names = symbol.GetAllTypeParameters().Select(t => t.Name); + return items; + } - return names.Select(t => CreateCompletionItem( - displayText: FormatParameter(TypeParameterReferenceElementName, t), - beforeCaretText: FormatParameterRefTag(TypeParameterReferenceElementName, t), - afterCaretText: string.Empty)); - } + private IEnumerable GetParamRefItems(ISymbol symbol) + { + var names = GetParameters(symbol).Select(p => p.Name); + + return names.Select(p => CreateCompletionItem( + displayText: FormatParameter(ParameterReferenceElementName, p), + beforeCaretText: FormatParameterRefTag(ParameterReferenceElementName, p), + afterCaretText: string.Empty)); + } - protected IEnumerable GetAttributeValueItems(ISymbol? symbol, string tagName, string attributeName) + private IEnumerable GetTypeParamRefItems(ISymbol symbol) + { + var names = symbol.GetAllTypeParameters().Select(t => t.Name); + + return names.Select(t => CreateCompletionItem( + displayText: FormatParameter(TypeParameterReferenceElementName, t), + beforeCaretText: FormatParameterRefTag(TypeParameterReferenceElementName, t), + afterCaretText: string.Empty)); + } + + protected IEnumerable GetAttributeValueItems(ISymbol? symbol, string tagName, string attributeName) + { + if (attributeName == NameAttributeName && symbol != null) { - if (attributeName == NameAttributeName && symbol != null) + if (tagName is ParameterElementName or ParameterReferenceElementName) { - if (tagName is ParameterElementName or ParameterReferenceElementName) - { - return GetParameters(symbol) - .Select(parameter => CreateCompletionItem(parameter.Name)); - } - else if (tagName == TypeParameterElementName) - { - return symbol.GetTypeParameters() - .Select(typeParameter => CreateCompletionItem(typeParameter.Name)); - } - else if (tagName == TypeParameterReferenceElementName) - { - return symbol.GetAllTypeParameters() - .Select(typeParameter => CreateCompletionItem(typeParameter.Name)); - } + return GetParameters(symbol) + .Select(parameter => CreateCompletionItem(parameter.Name)); } - else if (attributeName == LangwordAttributeName && tagName == SeeElementName) + else if (tagName == TypeParameterElementName) { - return GetKeywordNames().Select(CreateCompletionItem); + return symbol.GetTypeParameters() + .Select(typeParameter => CreateCompletionItem(typeParameter.Name)); } - else if (attributeName == TypeAttributeName && tagName == ListElementName) + else if (tagName == TypeParameterReferenceElementName) { - return s_listTypeValues.Select(CreateCompletionItem); + return symbol.GetAllTypeParameters() + .Select(typeParameter => CreateCompletionItem(typeParameter.Name)); } - - return SpecializedCollections.EmptyEnumerable(); } - - protected ImmutableArray GetTopLevelItems(ISymbol? symbol, TSyntax syntax) + else if (attributeName == LangwordAttributeName && tagName == SeeElementName) { - using var _1 = ArrayBuilder.GetInstance(out var items); - using var _2 = PooledHashSet.GetInstance(out var existingTopLevelTags); + return GetKeywordNames().Select(CreateCompletionItem); + } + else if (attributeName == TypeAttributeName && tagName == ListElementName) + { + return s_listTypeValues.Select(CreateCompletionItem); + } - existingTopLevelTags.AddAll(GetExistingTopLevelElementNames(syntax)); + return SpecializedCollections.EmptyEnumerable(); + } - items.AddRange(s_topLevelSingleUseTagNames.Except(existingTopLevelTags).Select(GetItem)); - items.AddRange(s_topLevelRepeatableTagNames.Select(GetItem)); + protected ImmutableArray GetTopLevelItems(ISymbol? symbol, TSyntax syntax) + { + using var _1 = ArrayBuilder.GetInstance(out var items); + using var _2 = PooledHashSet.GetInstance(out var existingTopLevelTags); - if (symbol != null) - { - items.AddRange(GetParameterItems(GetParameters(symbol), syntax, ParameterElementName)); - items.AddRange(GetParameterItems(symbol.GetTypeParameters(), syntax, TypeParameterElementName)); + existingTopLevelTags.AddAll(GetExistingTopLevelElementNames(syntax)); - if (symbol is IPropertySymbol && !existingTopLevelTags.Contains(ValueElementName)) - { - items.Add(GetItem(ValueElementName)); - } + items.AddRange(s_topLevelSingleUseTagNames.Except(existingTopLevelTags).Select(GetItem)); + items.AddRange(s_topLevelRepeatableTagNames.Select(GetItem)); - var returns = symbol is IMethodSymbol method && !method.ReturnsVoid; - if (returns && !existingTopLevelTags.Contains(ReturnsElementName)) - { - items.Add(GetItem(ReturnsElementName)); - } + if (symbol != null) + { + items.AddRange(GetParameterItems(GetParameters(symbol), syntax, ParameterElementName)); + items.AddRange(GetParameterItems(symbol.GetTypeParameters(), syntax, TypeParameterElementName)); + + if (symbol is IPropertySymbol && !existingTopLevelTags.Contains(ValueElementName)) + { + items.Add(GetItem(ValueElementName)); + } + + var returns = symbol is IMethodSymbol method && !method.ReturnsVoid; + if (returns && !existingTopLevelTags.Contains(ReturnsElementName)) + { + items.Add(GetItem(ReturnsElementName)); + } - if (symbol is INamedTypeSymbol namedType && namedType.IsDelegateType()) + if (symbol is INamedTypeSymbol namedType && namedType.IsDelegateType()) + { + var delegateInvokeMethod = namedType.DelegateInvokeMethod; + if (delegateInvokeMethod != null) { - var delegateInvokeMethod = namedType.DelegateInvokeMethod; - if (delegateInvokeMethod != null) - { - items.AddRange(GetParameterItems(delegateInvokeMethod.GetParameters(), syntax, ParameterElementName)); - } + items.AddRange(GetParameterItems(delegateInvokeMethod.GetParameters(), syntax, ParameterElementName)); } } - - return items.ToImmutable(); } - protected IEnumerable GetItemTagItems() - => new[] { TermElementName, DescriptionElementName }.Select(GetItem); - - protected IEnumerable GetListItems() - => s_listTagNames.Select(GetItem); + return items.ToImmutable(); + } - protected IEnumerable GetListHeaderItems() - => s_listHeaderTagNames.Select(GetItem); + protected IEnumerable GetItemTagItems() + => new[] { TermElementName, DescriptionElementName }.Select(GetItem); - private IEnumerable GetParameterItems(ImmutableArray symbols, TSyntax syntax, string tagName) where TSymbol : ISymbol - { - var names = symbols.Select(p => p.Name).ToSet(); - names.RemoveAll(GetExistingTopLevelAttributeValues(syntax, tagName, NameAttributeName).WhereNotNull()); - return names.Select(name => CreateCompletionItem(FormatParameter(tagName, name))); - } + protected IEnumerable GetListItems() + => s_listTagNames.Select(GetItem); - private static string FormatParameter(string kind, string name) - => $"{kind} {NameAttributeName}=\"{name}\""; + protected IEnumerable GetListHeaderItems() + => s_listHeaderTagNames.Select(GetItem); - private static string FormatParameterRefTag(string kind, string name) - => $"<{kind} {NameAttributeName}=\"{name}\"/>"; + private IEnumerable GetParameterItems(ImmutableArray symbols, TSyntax syntax, string tagName) where TSymbol : ISymbol + { + var names = symbols.Select(p => p.Name).ToSet(); + names.RemoveAll(GetExistingTopLevelAttributeValues(syntax, tagName, NameAttributeName).WhereNotNull()); + return names.Select(name => CreateCompletionItem(FormatParameter(tagName, name))); + } - public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitChar = null, CancellationToken cancellationToken = default) - { - var beforeCaretText = XmlDocCommentCompletionItem.GetBeforeCaretText(item); - var afterCaretText = XmlDocCommentCompletionItem.GetAfterCaretText(item); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + private static string FormatParameter(string kind, string name) + => $"{kind} {NameAttributeName}=\"{name}\""; - var itemSpan = item.Span; - var replacementSpan = TextSpan.FromBounds(text[itemSpan.Start - 1] == '<' && beforeCaretText[0] == '<' ? itemSpan.Start - 1 : itemSpan.Start, itemSpan.End); + private static string FormatParameterRefTag(string kind, string name) + => $"<{kind} {NameAttributeName}=\"{name}\"/>"; - var replacementText = beforeCaretText; - var newPosition = replacementSpan.Start + beforeCaretText.Length; + public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitChar = null, CancellationToken cancellationToken = default) + { + var beforeCaretText = XmlDocCommentCompletionItem.GetBeforeCaretText(item); + var afterCaretText = XmlDocCommentCompletionItem.GetAfterCaretText(item); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (commitChar.HasValue && !char.IsWhiteSpace(commitChar.Value) && commitChar.Value != replacementText[^1]) - { - // include the commit character - replacementText += commitChar.Value; + var itemSpan = item.Span; + var replacementSpan = TextSpan.FromBounds(text[itemSpan.Start - 1] == '<' && beforeCaretText[0] == '<' ? itemSpan.Start - 1 : itemSpan.Start, itemSpan.End); - // The caret goes after whatever commit character we spit. - newPosition++; - } + var replacementText = beforeCaretText; + var newPosition = replacementSpan.Start + beforeCaretText.Length; - replacementText += afterCaretText; + if (commitChar.HasValue && !char.IsWhiteSpace(commitChar.Value) && commitChar.Value != replacementText[^1]) + { + // include the commit character + replacementText += commitChar.Value; - return CompletionChange.Create( - new TextChange(replacementSpan, replacementText), - newPosition, includesCommitCharacter: true); + // The caret goes after whatever commit character we spit. + newPosition++; } - private CompletionItem CreateCompletionItem(string displayText) - { - return CreateCompletionItem( - displayText: displayText, - beforeCaretText: displayText, - afterCaretText: string.Empty); - } + replacementText += afterCaretText; - private CompletionItem CreateLangwordCompletionItem(string displayText) - { - return CreateCompletionItem( - displayText: displayText, - beforeCaretText: "", - afterCaretText: string.Empty); - } + return CompletionChange.Create( + new TextChange(replacementSpan, replacementText), + newPosition, includesCommitCharacter: true); + } - protected CompletionItem CreateCompletionItem(string displayText, string beforeCaretText, string afterCaretText) - => XmlDocCommentCompletionItem.Create(displayText, beforeCaretText, afterCaretText, rules: GetCompletionItemRules(displayText)); + private CompletionItem CreateCompletionItem(string displayText) + { + return CreateCompletionItem( + displayText: displayText, + beforeCaretText: displayText, + afterCaretText: string.Empty); + } - private static readonly CharacterSetModificationRule WithoutQuoteRule = CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '"'); - private static readonly CharacterSetModificationRule WithoutSpaceRule = CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ' '); + private CompletionItem CreateLangwordCompletionItem(string displayText) + { + return CreateCompletionItem( + displayText: displayText, + beforeCaretText: "", + afterCaretText: string.Empty); + } - protected static readonly ImmutableArray FilterRules = [CharacterSetModificationRule.Create(CharacterSetModificationKind.Add, '!', '-', '[')]; + protected CompletionItem CreateCompletionItem(string displayText, string beforeCaretText, string afterCaretText) + => XmlDocCommentCompletionItem.Create(displayText, beforeCaretText, afterCaretText, rules: GetCompletionItemRules(displayText)); - private CompletionItemRules GetCompletionItemRules(string displayText) - { - var commitRules = defaultRules.CommitCharacterRules; + private static readonly CharacterSetModificationRule WithoutQuoteRule = CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '"'); + private static readonly CharacterSetModificationRule WithoutSpaceRule = CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ' '); - if (displayText.Contains("\"")) - { - commitRules = commitRules.Add(WithoutQuoteRule); - } + protected static readonly ImmutableArray FilterRules = [CharacterSetModificationRule.Create(CharacterSetModificationKind.Add, '!', '-', '[')]; - if (displayText.Contains(" ")) - { - commitRules = commitRules.Add(WithoutSpaceRule); - } + private CompletionItemRules GetCompletionItemRules(string displayText) + { + var commitRules = defaultRules.CommitCharacterRules; + + if (displayText.Contains("\"")) + { + commitRules = commitRules.Add(WithoutQuoteRule); + } - return defaultRules.WithCommitCharacterRules(commitRules); + if (displayText.Contains(" ")) + { + commitRules = commitRules.Add(WithoutSpaceRule); } + + return defaultRules.WithCommitCharacterRules(commitRules); } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractInternalsVisibleToCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractInternalsVisibleToCompletionProvider.cs index df2a9123afa7e..2441c844fa572 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractInternalsVisibleToCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractInternalsVisibleToCompletionProvider.cs @@ -14,294 +14,293 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractInternalsVisibleToCompletionProvider : LSPCompletionProvider { - internal abstract class AbstractInternalsVisibleToCompletionProvider : LSPCompletionProvider - { - private const string ProjectGuidKey = nameof(ProjectGuidKey); + private const string ProjectGuidKey = nameof(ProjectGuidKey); - protected abstract IImmutableList GetAssemblyScopedAttributeSyntaxNodesOfDocument(SyntaxNode documentRoot); - protected abstract SyntaxNode? GetConstructorArgumentOfInternalsVisibleToAttribute(SyntaxNode internalsVisibleToAttribute); + protected abstract IImmutableList GetAssemblyScopedAttributeSyntaxNodesOfDocument(SyntaxNode documentRoot); + protected abstract SyntaxNode? GetConstructorArgumentOfInternalsVisibleToAttribute(SyntaxNode internalsVisibleToAttribute); - public sealed override bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options) + public sealed override bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options) + { + // Should trigger in these cases ($$ is the cursor position) + // [InternalsVisibleTo($$ -> user enters " + // [InternalsVisibleTo("$$")] -> user enters any character + var ch = text[insertedCharacterPosition]; + if (ch == '\"') { - // Should trigger in these cases ($$ is the cursor position) - // [InternalsVisibleTo($$ -> user enters " - // [InternalsVisibleTo("$$")] -> user enters any character - var ch = text[insertedCharacterPosition]; - if (ch == '\"') - { - return true; - } - else + return true; + } + else + { + if (insertedCharacterPosition > 0) { - if (insertedCharacterPosition > 0) + ch = text[insertedCharacterPosition - 1]; + if (ch == '\"') { - ch = text[insertedCharacterPosition - 1]; - if (ch == '\"') - { - return ShouldTriggerAfterQuotes(text, insertedCharacterPosition); - } + return ShouldTriggerAfterQuotes(text, insertedCharacterPosition); } } - - return false; } - protected abstract bool ShouldTriggerAfterQuotes(SourceText text, int insertedCharacterPosition); + return false; + } - public override ImmutableHashSet TriggerCharacters { get; } = ['\"']; + protected abstract bool ShouldTriggerAfterQuotes(SourceText text, int insertedCharacterPosition); - public override async Task ProvideCompletionsAsync(CompletionContext context) - { - try - { - var cancellationToken = context.CancellationToken; - var syntaxTree = await context.Document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var syntaxFactsService = context.Document.GetRequiredLanguageService(); - if (syntaxFactsService.IsEntirelyWithinStringOrCharOrNumericLiteral(syntaxTree, context.Position, cancellationToken)) - { - var token = syntaxTree.FindTokenOnLeftOfPosition(context.Position, cancellationToken); - var attributeSyntaxNode = GetAttributeSyntaxNodeOfToken(syntaxFactsService, token); - if (attributeSyntaxNode == null) - { - return; - } - - if (await CheckTypeInfoOfAttributeAsync(context.Document, attributeSyntaxNode, context.CancellationToken).ConfigureAwait(false)) - { - await AddAssemblyCompletionItemsAsync(context, cancellationToken).ConfigureAwait(false); - } - } - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) - { - // nop - } - } + public override ImmutableHashSet TriggerCharacters { get; } = ['\"']; - private static SyntaxNode? GetAttributeSyntaxNodeOfToken(ISyntaxFactsService syntaxFactsService, SyntaxToken token) + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - //Supported cases: - //[Attribute("| - //[Attribute(parameterName:"Text|") - //Also supported but excluded by IsPositionEntirelyWithinStringLiteral in ProvideCompletionsAsync - //[Attribute(""| - //[Attribute("Text"|) - var node = token.Parent; - if (node != null && syntaxFactsService.IsStringLiteralExpression(node)) + var cancellationToken = context.CancellationToken; + var syntaxTree = await context.Document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var syntaxFactsService = context.Document.GetRequiredLanguageService(); + if (syntaxFactsService.IsEntirelyWithinStringOrCharOrNumericLiteral(syntaxTree, context.Position, cancellationToken)) { - // Edge cases: - // ElementAccessExpressionSyntax is present if the following statement is another attribute: - // [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("| - // [assembly: System.Reflection.AssemblyVersion("1.0.0.0")] - // [assembly: System.Reflection.AssemblyCompany("Test")] - // BinaryExpression is present if the string literal is concatenated: - // From: https://msdn.microsoft.com/de-de/library/system.runtime.compilerservices.internalsvisibletoattribute(v=vs.110).aspx - // [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Friend1, PublicKey=002400000480000094" + - // "0000000602000000240000525341310004000" + .. - while (syntaxFactsService.IsElementAccessExpression(node.Parent) || syntaxFactsService.IsBinaryExpression(node.Parent)) + var token = syntaxTree.FindTokenOnLeftOfPosition(context.Position, cancellationToken); + var attributeSyntaxNode = GetAttributeSyntaxNodeOfToken(syntaxFactsService, token); + if (attributeSyntaxNode == null) { - node = node.Parent; + return; } - // node -> AttributeArgumentSyntax -> AttributeArgumentListSyntax -> AttributeSyntax - var attributeSyntaxNodeCandidate = node.Parent?.Parent?.Parent; - if (syntaxFactsService.IsAttribute(attributeSyntaxNodeCandidate)) + if (await CheckTypeInfoOfAttributeAsync(context.Document, attributeSyntaxNode, context.CancellationToken).ConfigureAwait(false)) { - return attributeSyntaxNodeCandidate; + await AddAssemblyCompletionItemsAsync(context, cancellationToken).ConfigureAwait(false); } } - - return null; } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) + { + // nop + } + } - private static async Task CheckTypeInfoOfAttributeAsync(Document document, SyntaxNode attributeNode, CancellationToken cancellationToken) + private static SyntaxNode? GetAttributeSyntaxNodeOfToken(ISyntaxFactsService syntaxFactsService, SyntaxToken token) + { + //Supported cases: + //[Attribute("| + //[Attribute(parameterName:"Text|") + //Also supported but excluded by IsPositionEntirelyWithinStringLiteral in ProvideCompletionsAsync + //[Attribute(""| + //[Attribute("Text"|) + var node = token.Parent; + if (node != null && syntaxFactsService.IsStringLiteralExpression(node)) { - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(attributeNode, cancellationToken).ConfigureAwait(false); - var typeInfo = semanticModel.GetTypeInfo(attributeNode, cancellationToken); - var type = typeInfo.Type; - if (type == null) + // Edge cases: + // ElementAccessExpressionSyntax is present if the following statement is another attribute: + // [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("| + // [assembly: System.Reflection.AssemblyVersion("1.0.0.0")] + // [assembly: System.Reflection.AssemblyCompany("Test")] + // BinaryExpression is present if the string literal is concatenated: + // From: https://msdn.microsoft.com/de-de/library/system.runtime.compilerservices.internalsvisibletoattribute(v=vs.110).aspx + // [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Friend1, PublicKey=002400000480000094" + + // "0000000602000000240000525341310004000" + .. + while (syntaxFactsService.IsElementAccessExpression(node.Parent) || syntaxFactsService.IsBinaryExpression(node.Parent)) { - return false; + node = node.Parent; } - var internalsVisibleToAttributeSymbol = semanticModel.Compilation.GetTypeByMetadataName(typeof(InternalsVisibleToAttribute).FullName!); - return type.Equals(internalsVisibleToAttributeSymbol); + // node -> AttributeArgumentSyntax -> AttributeArgumentListSyntax -> AttributeSyntax + var attributeSyntaxNodeCandidate = node.Parent?.Parent?.Parent; + if (syntaxFactsService.IsAttribute(attributeSyntaxNodeCandidate)) + { + return attributeSyntaxNodeCandidate; + } } - private async Task AddAssemblyCompletionItemsAsync(CompletionContext context, CancellationToken cancellationToken) + return null; + } + + private static async Task CheckTypeInfoOfAttributeAsync(Document document, SyntaxNode attributeNode, CancellationToken cancellationToken) + { + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(attributeNode, cancellationToken).ConfigureAwait(false); + var typeInfo = semanticModel.GetTypeInfo(attributeNode, cancellationToken); + var type = typeInfo.Type; + if (type == null) { - var currentProject = context.Document.Project; - var allInternalsVisibleToAttributesOfProject = await GetAllInternalsVisibleToAssemblyNamesOfProjectAsync(context, cancellationToken).ConfigureAwait(false); - foreach (var project in context.Document.Project.Solution.Projects) - { - if (project == currentProject) - { - continue; - } + return false; + } - if (IsProjectTypeUnsupported(project)) - { - continue; - } + var internalsVisibleToAttributeSymbol = semanticModel.Compilation.GetTypeByMetadataName(typeof(InternalsVisibleToAttribute).FullName!); + return type.Equals(internalsVisibleToAttributeSymbol); + } - if (allInternalsVisibleToAttributesOfProject.Contains(project.AssemblyName)) - { - continue; - } + private async Task AddAssemblyCompletionItemsAsync(CompletionContext context, CancellationToken cancellationToken) + { + var currentProject = context.Document.Project; + var allInternalsVisibleToAttributesOfProject = await GetAllInternalsVisibleToAssemblyNamesOfProjectAsync(context, cancellationToken).ConfigureAwait(false); + foreach (var project in context.Document.Project.Solution.Projects) + { + if (project == currentProject) + { + continue; + } - var projectGuid = project.Id.Id.ToString(); - var completionItem = CommonCompletionItem.Create( - displayText: project.AssemblyName, - displayTextSuffix: "", - rules: CompletionItemRules.Default, - glyph: project.GetGlyph(), - properties: [new KeyValuePair(ProjectGuidKey, projectGuid)]); - context.AddItem(completionItem); + if (IsProjectTypeUnsupported(project)) + { + continue; } - if (context.Items.Count > 0) + if (allInternalsVisibleToAttributesOfProject.Contains(project.AssemblyName)) { - context.CompletionListSpan = await GetTextChangeSpanAsync( - context.Document, context.CompletionListSpan, cancellationToken).ConfigureAwait(false); + continue; } + + var projectGuid = project.Id.Id.ToString(); + var completionItem = CommonCompletionItem.Create( + displayText: project.AssemblyName, + displayTextSuffix: "", + rules: CompletionItemRules.Default, + glyph: project.GetGlyph(), + properties: [new KeyValuePair(ProjectGuidKey, projectGuid)]); + context.AddItem(completionItem); } - private static bool IsProjectTypeUnsupported(Project project) - => !project.SupportsCompilation; + if (context.Items.Count > 0) + { + context.CompletionListSpan = await GetTextChangeSpanAsync( + context.Document, context.CompletionListSpan, cancellationToken).ConfigureAwait(false); + } + } + + private static bool IsProjectTypeUnsupported(Project project) + => !project.SupportsCompilation; - private async Task> GetAllInternalsVisibleToAssemblyNamesOfProjectAsync(CompletionContext completionContext, CancellationToken cancellationToken) + private async Task> GetAllInternalsVisibleToAssemblyNamesOfProjectAsync(CompletionContext completionContext, CancellationToken cancellationToken) + { + // Looking up other InternalsVisibleTo attributes of this project. This is faster than compiling all projects of the solution and checking access via + // sourceAssembly.GivesAccessTo(compilation.Assembly) + // at the cost of being not so precise (can't check the validity of the PublicKey). + var project = completionContext.Document.Project; + var resultBuilder = (ImmutableHashSet.Builder?)null; + foreach (var document in project.Documents) { - // Looking up other InternalsVisibleTo attributes of this project. This is faster than compiling all projects of the solution and checking access via - // sourceAssembly.GivesAccessTo(compilation.Assembly) - // at the cost of being not so precise (can't check the validity of the PublicKey). - var project = completionContext.Document.Project; - var resultBuilder = (ImmutableHashSet.Builder?)null; - foreach (var document in project.Documents) + var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var assemblyScopedAttributes = GetAssemblyScopedAttributeSyntaxNodesOfDocument(syntaxRoot); + foreach (var attribute in assemblyScopedAttributes) { - var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var assemblyScopedAttributes = GetAssemblyScopedAttributeSyntaxNodesOfDocument(syntaxRoot); - foreach (var attribute in assemblyScopedAttributes) + // Skip attributes with errors. This skips the attribute that is currently edited, until it is complete: + // [assembly: InternalsVisibleTo("$$ + // CS1003: Syntax error, ']' expected; CS1010: A string was not properly delimited; CS1026: An incomplete statement was found + // see also SyntaxNode.HasErrors + if (attribute.ContainsDiagnostics) { - // Skip attributes with errors. This skips the attribute that is currently edited, until it is complete: - // [assembly: InternalsVisibleTo("$$ - // CS1003: Syntax error, ']' expected; CS1010: A string was not properly delimited; CS1026: An incomplete statement was found - // see also SyntaxNode.HasErrors - if (attribute.ContainsDiagnostics) + foreach (var diagnostic in attribute.GetDiagnostics()) { - foreach (var diagnostic in attribute.GetDiagnostics()) + if (diagnostic.Severity == DiagnosticSeverity.Error) { - if (diagnostic.Severity == DiagnosticSeverity.Error) - { - continue; - } + continue; } } + } - if (await CheckTypeInfoOfAttributeAsync(document, attribute, completionContext.CancellationToken).ConfigureAwait(false)) + if (await CheckTypeInfoOfAttributeAsync(document, attribute, completionContext.CancellationToken).ConfigureAwait(false)) + { + // See Microsoft.CodeAnalysis.PEAssembly.BuildInternalsVisibleToMap for reference on how + // the 'real' InternalsVisibleTo logic extracts and compares the assemblyName: + // * Extract the assemblyName by AssemblyIdentity.TryParseDisplayName + // * Compare with StringComparer.OrdinalIgnoreCase + // We take the same approach, but we do only a limited check of the PublicKey. + // The PublicKey is checked by AssemblyIdentity.TryParseDisplayName to be + // parseable (length, can be converted to bytes, etc.), but it is not tested whether + // the public key actually fits to the assembly. + var assemblyName = await GetAssemblyNameFromInternalsVisibleToAttributeAsync(document, attribute, completionContext.CancellationToken).ConfigureAwait(false); + if (!string.IsNullOrWhiteSpace(assemblyName)) { - // See Microsoft.CodeAnalysis.PEAssembly.BuildInternalsVisibleToMap for reference on how - // the 'real' InternalsVisibleTo logic extracts and compares the assemblyName: - // * Extract the assemblyName by AssemblyIdentity.TryParseDisplayName - // * Compare with StringComparer.OrdinalIgnoreCase - // We take the same approach, but we do only a limited check of the PublicKey. - // The PublicKey is checked by AssemblyIdentity.TryParseDisplayName to be - // parseable (length, can be converted to bytes, etc.), but it is not tested whether - // the public key actually fits to the assembly. - var assemblyName = await GetAssemblyNameFromInternalsVisibleToAttributeAsync(document, attribute, completionContext.CancellationToken).ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(assemblyName)) - { - resultBuilder ??= ImmutableHashSet.CreateBuilder(StringComparer.OrdinalIgnoreCase); - resultBuilder.Add(assemblyName); - } + resultBuilder ??= ImmutableHashSet.CreateBuilder(StringComparer.OrdinalIgnoreCase); + resultBuilder.Add(assemblyName); } } } - - return resultBuilder == null - ? [] - : resultBuilder.ToImmutable(); } - private async Task GetAssemblyNameFromInternalsVisibleToAttributeAsync(Document document, SyntaxNode node, CancellationToken cancellationToken) - { - var constructorArgument = GetConstructorArgumentOfInternalsVisibleToAttribute(node); - if (constructorArgument == null) - { - return string.Empty; - } - - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(constructorArgument, cancellationToken).ConfigureAwait(false); - var constantCandidate = semanticModel.GetConstantValue(constructorArgument, cancellationToken); - if (constantCandidate.HasValue && constantCandidate.Value is string argument) - { - if (AssemblyIdentity.TryParseDisplayName(argument, out var assemblyIdentity)) - { - return assemblyIdentity.Name; - } - } + return resultBuilder == null + ? [] + : resultBuilder.ToImmutable(); + } + private async Task GetAssemblyNameFromInternalsVisibleToAttributeAsync(Document document, SyntaxNode node, CancellationToken cancellationToken) + { + var constructorArgument = GetConstructorArgumentOfInternalsVisibleToAttribute(node); + if (constructorArgument == null) + { return string.Empty; } - private static async Task GetTextChangeSpanAsync(Document document, TextSpan startSpan, CancellationToken cancellationToken) + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(constructorArgument, cancellationToken).ConfigureAwait(false); + var constantCandidate = semanticModel.GetConstantValue(constructorArgument, cancellationToken); + if (constantCandidate.HasValue && constantCandidate.Value is string argument) { - var result = startSpan; - var syntaxFacts = document.GetRequiredLanguageService(); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(result.Start); - if (syntaxFacts.IsStringLiteral(token) || syntaxFacts.IsVerbatimStringLiteral(token)) + if (AssemblyIdentity.TryParseDisplayName(argument, out var assemblyIdentity)) { - var text = root.GetText(); - - // Expand selection in both directions until a double quote or any line break character is reached - static bool IsWordCharacter(char ch) => !(ch == '"' || TextUtilities.IsAnyLineBreakCharacter(ch)); - - result = CommonCompletionUtilities.GetWordSpan( - text, startSpan.Start, IsWordCharacter, IsWordCharacter, alwaysExtendEndSpan: true); + return assemblyIdentity.Name; } - - return result; } - public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) + return string.Empty; + } + + private static async Task GetTextChangeSpanAsync(Document document, TextSpan startSpan, CancellationToken cancellationToken) + { + var result = startSpan; + var syntaxFacts = document.GetRequiredLanguageService(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(result.Start); + if (syntaxFacts.IsStringLiteral(token) || syntaxFacts.IsVerbatimStringLiteral(token)) { - var projectIdGuid = item.GetProperty(ProjectGuidKey); - var projectId = ProjectId.CreateFromSerialized(new Guid(projectIdGuid)); - var project = document.Project.Solution.GetRequiredProject(projectId); - var assemblyName = item.DisplayText; - var publicKey = await GetPublicKeyOfProjectAsync(project, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrEmpty(publicKey)) - { - assemblyName += $", PublicKey={publicKey}"; - } + var text = root.GetText(); + + // Expand selection in both directions until a double quote or any line break character is reached + static bool IsWordCharacter(char ch) => !(ch == '"' || TextUtilities.IsAnyLineBreakCharacter(ch)); - var textChange = new TextChange(item.Span, assemblyName); - return CompletionChange.Create(textChange); + result = CommonCompletionUtilities.GetWordSpan( + text, startSpan.Start, IsWordCharacter, IsWordCharacter, alwaysExtendEndSpan: true); } - private static async Task GetPublicKeyOfProjectAsync(Project project, CancellationToken cancellationToken) - { - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - if (compilation?.Assembly?.Identity?.IsStrongName == true) - { - return GetPublicKeyAsHexString(compilation.Assembly.Identity.PublicKey); - } + return result; + } - return string.Empty; + public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) + { + var projectIdGuid = item.GetProperty(ProjectGuidKey); + var projectId = ProjectId.CreateFromSerialized(new Guid(projectIdGuid)); + var project = document.Project.Solution.GetRequiredProject(projectId); + var assemblyName = item.DisplayText; + var publicKey = await GetPublicKeyOfProjectAsync(project, cancellationToken).ConfigureAwait(false); + if (!string.IsNullOrEmpty(publicKey)) + { + assemblyName += $", PublicKey={publicKey}"; } - private static string GetPublicKeyAsHexString(ImmutableArray publicKey) + var textChange = new TextChange(item.Span, assemblyName); + return CompletionChange.Create(textChange); + } + + private static async Task GetPublicKeyOfProjectAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation?.Assembly?.Identity?.IsStrongName == true) { - var pooledStrBuilder = PooledStringBuilder.GetInstance(); - var builder = pooledStrBuilder.Builder; - foreach (var b in publicKey) - { - builder.Append(b.ToString("x2")); - } + return GetPublicKeyAsHexString(compilation.Assembly.Identity.PublicKey); + } + + return string.Empty; + } - return pooledStrBuilder.ToStringAndFree(); + private static string GetPublicKeyAsHexString(ImmutableArray publicKey) + { + var pooledStrBuilder = PooledStringBuilder.GetInstance(); + var builder = pooledStrBuilder.Builder; + foreach (var b in publicKey) + { + builder.Append(b.ToString("x2")); } + + return pooledStrBuilder.ToStringAndFree(); } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractKeywordCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractKeywordCompletionProvider.cs index 3fe38fd087426..21398936c8f3e 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractKeywordCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractKeywordCompletionProvider.cs @@ -14,72 +14,71 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract partial class AbstractKeywordCompletionProvider : LSPCompletionProvider + where TContext : SyntaxContext { - internal abstract partial class AbstractKeywordCompletionProvider : LSPCompletionProvider - where TContext : SyntaxContext - { - private static readonly Comparer s_comparer = new(); + private static readonly Comparer s_comparer = new(); - private readonly ImmutableArray> _keywordRecommenders; + private readonly ImmutableArray> _keywordRecommenders; - protected AbstractKeywordCompletionProvider(ImmutableArray> keywordRecommenders) - => _keywordRecommenders = keywordRecommenders; + protected AbstractKeywordCompletionProvider(ImmutableArray> keywordRecommenders) + => _keywordRecommenders = keywordRecommenders; - protected abstract CompletionItem CreateItem(RecommendedKeyword keyword, TContext context, CancellationToken cancellationToken); + protected abstract CompletionItem CreateItem(RecommendedKeyword keyword, TContext context, CancellationToken cancellationToken); - public override async Task ProvideCompletionsAsync(CompletionContext context) - { - var cancellationToken = context.CancellationToken; - - using (Logger.LogBlock(FunctionId.Completion_KeywordCompletionProvider_GetItemsWorker, cancellationToken)) - { - context.AddItems(await context.Document.GetUnionItemsFromDocumentAndLinkedDocumentsAsync( - s_comparer, - d => RecommendCompletionItemsAsync(d, context, cancellationToken)).ConfigureAwait(false)); - } - } + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + var cancellationToken = context.CancellationToken; - private async Task> RecommendCompletionItemsAsync(Document document, CompletionContext context, CancellationToken cancellationToken) + using (Logger.LogBlock(FunctionId.Completion_KeywordCompletionProvider_GetItemsWorker, cancellationToken)) { - var position = context.Position; - var syntaxContext = (TContext)await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); - var keywords = await RecommendKeywordsAsync(document, position, syntaxContext, cancellationToken).ConfigureAwait(false); - return keywords.SelectAsArray(k => CreateItem(k, syntaxContext, cancellationToken)); + context.AddItems(await context.Document.GetUnionItemsFromDocumentAndLinkedDocumentsAsync( + s_comparer, + d => RecommendCompletionItemsAsync(d, context, cancellationToken)).ConfigureAwait(false)); } + } - private async Task> RecommendKeywordsAsync( - Document document, - int position, - TContext context, - CancellationToken cancellationToken) - { - var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); - if (syntaxFacts.IsInNonUserCode(syntaxTree, position, cancellationToken)) - return []; + private async Task> RecommendCompletionItemsAsync(Document document, CompletionContext context, CancellationToken cancellationToken) + { + var position = context.Position; + var syntaxContext = (TContext)await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); + var keywords = await RecommendKeywordsAsync(document, position, syntaxContext, cancellationToken).ConfigureAwait(false); + return keywords.SelectAsArray(k => CreateItem(k, syntaxContext, cancellationToken)); + } - using var _ = ArrayBuilder.GetInstance(out var result); - foreach (var recommender in _keywordRecommenders) - { - var keywords = recommender.RecommendKeywords(position, context, cancellationToken); - result.AddRange(keywords.NullToEmpty()); - } + private async Task> RecommendKeywordsAsync( + Document document, + int position, + TContext context, + CancellationToken cancellationToken) + { + var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + if (syntaxFacts.IsInNonUserCode(syntaxTree, position, cancellationToken)) + return []; - result.RemoveDuplicates(); - return result.ToImmutable(); + using var _ = ArrayBuilder.GetInstance(out var result); + foreach (var recommender in _keywordRecommenders) + { + var keywords = recommender.RecommendKeywords(position, context, cancellationToken); + result.AddRange(keywords.NullToEmpty()); } - public sealed override Task GetTextChangeAsync(Document document, CompletionItem item, char? ch, CancellationToken cancellationToken) - => Task.FromResult((TextChange?)new TextChange(item.Span, item.DisplayText)); + result.RemoveDuplicates(); + return result.ToImmutable(); + } + + public sealed override Task GetTextChangeAsync(Document document, CompletionItem item, char? ch, CancellationToken cancellationToken) + => Task.FromResult((TextChange?)new TextChange(item.Span, item.DisplayText)); - private class Comparer : IEqualityComparer - { - public bool Equals(CompletionItem? x, CompletionItem? y) - => x?.DisplayText == y?.DisplayText; + private class Comparer : IEqualityComparer + { + public bool Equals(CompletionItem? x, CompletionItem? y) + => x?.DisplayText == y?.DisplayText; - public int GetHashCode(CompletionItem obj) - => Hash.Combine(obj.DisplayText.GetHashCode(), obj.DisplayText.GetHashCode()); - } + public int GetHashCode(CompletionItem obj) + => Hash.Combine(obj.DisplayText.GetHashCode(), obj.DisplayText.GetHashCode()); } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractMemberInsertingCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractMemberInsertingCompletionProvider.cs index 0d7db8af30f41..c94218309c384 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractMemberInsertingCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractMemberInsertingCompletionProvider.cs @@ -18,227 +18,226 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract partial class AbstractMemberInsertingCompletionProvider : LSPCompletionProvider { - internal abstract partial class AbstractMemberInsertingCompletionProvider : LSPCompletionProvider - { - private readonly SyntaxAnnotation _annotation = new(); - private readonly SyntaxAnnotation _otherAnnotation = new(); + private readonly SyntaxAnnotation _annotation = new(); + private readonly SyntaxAnnotation _otherAnnotation = new(); - protected abstract SyntaxToken GetToken(CompletionItem completionItem, SyntaxTree tree, CancellationToken cancellationToken); + protected abstract SyntaxToken GetToken(CompletionItem completionItem, SyntaxTree tree, CancellationToken cancellationToken); - protected abstract Task GenerateMemberAsync(ISymbol member, INamedTypeSymbol containingType, Document document, CompletionItem item, CancellationToken cancellationToken); - protected abstract int GetTargetCaretPosition(SyntaxNode caretTarget); - protected abstract SyntaxNode GetSyntax(SyntaxToken commonSyntaxToken); + protected abstract Task GenerateMemberAsync(ISymbol member, INamedTypeSymbol containingType, Document document, CompletionItem item, CancellationToken cancellationToken); + protected abstract int GetTargetCaretPosition(SyntaxNode caretTarget); + protected abstract SyntaxNode GetSyntax(SyntaxToken commonSyntaxToken); - public AbstractMemberInsertingCompletionProvider() - { - } + public AbstractMemberInsertingCompletionProvider() + { + } - public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) - { - // TODO: pass fallback options: https://github.com/dotnet/roslyn/issues/60786 - var globalOptions = document.Project.Solution.Services.GetService(); - var fallbackOptions = globalOptions?.Provider ?? CodeActionOptions.DefaultProvider; + public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) + { + // TODO: pass fallback options: https://github.com/dotnet/roslyn/issues/60786 + var globalOptions = document.Project.Solution.Services.GetService(); + var fallbackOptions = globalOptions?.Provider ?? CodeActionOptions.DefaultProvider; - var newDocument = await DetermineNewDocumentAsync(document, item, fallbackOptions, cancellationToken).ConfigureAwait(false); - var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var newRoot = await newDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newDocument = await DetermineNewDocumentAsync(document, item, fallbackOptions, cancellationToken).ConfigureAwait(false); + var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var newRoot = await newDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - int? newPosition = null; + int? newPosition = null; - // Attempt to find the inserted node and move the caret appropriately - if (newRoot != null) + // Attempt to find the inserted node and move the caret appropriately + if (newRoot != null) + { + var caretTarget = newRoot.GetAnnotatedNodes(_annotation).FirstOrDefault(); + if (caretTarget != null) { - var caretTarget = newRoot.GetAnnotatedNodes(_annotation).FirstOrDefault(); - if (caretTarget != null) + var targetPosition = GetTargetCaretPosition(caretTarget); + + // Something weird happened and we failed to get a valid position. + // Bail on moving the caret. + if (targetPosition > 0 && targetPosition <= newText.Length) { - var targetPosition = GetTargetCaretPosition(caretTarget); - - // Something weird happened and we failed to get a valid position. - // Bail on moving the caret. - if (targetPosition > 0 && targetPosition <= newText.Length) - { - newPosition = targetPosition; - } + newPosition = targetPosition; } } - - var changes = await newDocument.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false); - var changesArray = changes.ToImmutableArray(); - var change = Utilities.Collapse(newText, changesArray); - - return CompletionChange.Create(change, changesArray, newPosition, includesCommitCharacter: true); } - private async Task DetermineNewDocumentAsync( - Document document, - CompletionItem completionItem, - CleanCodeGenerationOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var changes = await newDocument.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false); + var changesArray = changes.ToImmutableArray(); + var change = Utilities.Collapse(newText, changesArray); - // The span we're going to replace - var line = text.Lines[MemberInsertionCompletionItem.GetLine(completionItem)]; + return CompletionChange.Create(change, changesArray, newPosition, includesCommitCharacter: true); + } - // Annotate the line we care about so we can find it after adding usings - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var token = GetToken(completionItem, tree, cancellationToken); - var annotatedRoot = tree.GetRoot(cancellationToken).ReplaceToken(token, token.WithAdditionalAnnotations(_otherAnnotation)); - // Make sure the new document is frozen before we try to get the semantic model. This is to - // avoid trigger source generator, which is expensive and not needed for calculating the change. - document = document.WithSyntaxRoot(annotatedRoot).WithFrozenPartialSemantics(cancellationToken); + private async Task DetermineNewDocumentAsync( + Document document, + CompletionItem completionItem, + CleanCodeGenerationOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var memberContainingDocument = await GenerateMemberAndUsingsAsync(document, completionItem, line, fallbackOptions, cancellationToken).ConfigureAwait(false); - if (memberContainingDocument == null) - { - // Generating the new document failed because we somehow couldn't resolve - // the underlying symbol's SymbolKey. At this point, we won't be able to - // make any changes, so just return the document we started with. - return document; - } + // The span we're going to replace + var line = text.Lines[MemberInsertionCompletionItem.GetLine(completionItem)]; - var memberContainingDocumentCleanupOptions = await document.GetCodeCleanupOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var insertionRoot = await GetTreeWithAddedSyntaxNodeRemovedAsync(memberContainingDocument, memberContainingDocumentCleanupOptions, cancellationToken).ConfigureAwait(false); - var insertionText = await GenerateInsertionTextAsync(memberContainingDocument, memberContainingDocumentCleanupOptions, cancellationToken).ConfigureAwait(false); + // Annotate the line we care about so we can find it after adding usings + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var token = GetToken(completionItem, tree, cancellationToken); + var annotatedRoot = tree.GetRoot(cancellationToken).ReplaceToken(token, token.WithAdditionalAnnotations(_otherAnnotation)); + // Make sure the new document is frozen before we try to get the semantic model. This is to + // avoid trigger source generator, which is expensive and not needed for calculating the change. + document = document.WithSyntaxRoot(annotatedRoot).WithFrozenPartialSemantics(cancellationToken); - var destinationSpan = ComputeDestinationSpan(insertionRoot); + var memberContainingDocument = await GenerateMemberAndUsingsAsync(document, completionItem, line, fallbackOptions, cancellationToken).ConfigureAwait(false); + if (memberContainingDocument == null) + { + // Generating the new document failed because we somehow couldn't resolve + // the underlying symbol's SymbolKey. At this point, we won't be able to + // make any changes, so just return the document we started with. + return document; + } - var finalText = insertionRoot.GetText(text.Encoding) - .Replace(destinationSpan, insertionText.Trim()); + var memberContainingDocumentCleanupOptions = await document.GetCodeCleanupOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var insertionRoot = await GetTreeWithAddedSyntaxNodeRemovedAsync(memberContainingDocument, memberContainingDocumentCleanupOptions, cancellationToken).ConfigureAwait(false); + var insertionText = await GenerateInsertionTextAsync(memberContainingDocument, memberContainingDocumentCleanupOptions, cancellationToken).ConfigureAwait(false); - document = document.WithText(finalText); - var newRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var declaration = GetSyntax(newRoot.FindToken(destinationSpan.End)); + var destinationSpan = ComputeDestinationSpan(insertionRoot); - document = document.WithSyntaxRoot(newRoot.ReplaceNode(declaration, declaration.WithAdditionalAnnotations(_annotation))); - var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - return await Formatter.FormatAsync(document, _annotation, formattingOptions, cancellationToken).ConfigureAwait(false); - } + var finalText = insertionRoot.GetText(text.Encoding) + .Replace(destinationSpan, insertionText.Trim()); - private async Task GenerateMemberAndUsingsAsync( - Document document, - CompletionItem completionItem, - TextLine line, - CodeAndImportGenerationOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - var codeGenService = document.GetRequiredLanguageService(); + document = document.WithText(finalText); + var newRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var declaration = GetSyntax(newRoot.FindToken(destinationSpan.End)); - // Resolve member and type in our new, forked, solution - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + document = document.WithSyntaxRoot(newRoot.ReplaceNode(declaration, declaration.WithAdditionalAnnotations(_annotation))); + var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + return await Formatter.FormatAsync(document, _annotation, formattingOptions, cancellationToken).ConfigureAwait(false); + } - var containingType = semanticModel.GetEnclosingSymbol(line.Start, cancellationToken); - Contract.ThrowIfNull(containingType); + private async Task GenerateMemberAndUsingsAsync( + Document document, + CompletionItem completionItem, + TextLine line, + CodeAndImportGenerationOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var codeGenService = document.GetRequiredLanguageService(); - var symbols = await SymbolCompletionItem.GetSymbolsAsync(completionItem, document, cancellationToken).ConfigureAwait(false); - var overriddenMember = symbols.FirstOrDefault(); + // Resolve member and type in our new, forked, solution + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (overriddenMember == null) - { - // Unfortunately, SymbolKey resolution failed. Bail. - return null; - } + var containingType = semanticModel.GetEnclosingSymbol(line.Start, cancellationToken); + Contract.ThrowIfNull(containingType); - // CodeGenerationOptions containing before and after - var context = new CodeGenerationSolutionContext( - document.Project.Solution, - new CodeGenerationContext( - contextLocation: semanticModel.SyntaxTree.GetLocation(TextSpan.FromBounds(line.Start, line.Start))), - fallbackOptions); + var symbols = await SymbolCompletionItem.GetSymbolsAsync(completionItem, document, cancellationToken).ConfigureAwait(false); + var overriddenMember = symbols.FirstOrDefault(); - var generatedMember = await GenerateMemberAsync(overriddenMember, containingType, document, completionItem, cancellationToken).ConfigureAwait(false); - generatedMember = _annotation.AddAnnotationToSymbol(generatedMember); + if (overriddenMember == null) + { + // Unfortunately, SymbolKey resolution failed. Bail. + return null; + } - Document? memberContainingDocument = null; - if (generatedMember.Kind == SymbolKind.Method) - { - memberContainingDocument = await codeGenService.AddMethodAsync(context, containingType, (IMethodSymbol)generatedMember, cancellationToken).ConfigureAwait(false); - } - else if (generatedMember.Kind == SymbolKind.Property) - { - memberContainingDocument = await codeGenService.AddPropertyAsync(context, containingType, (IPropertySymbol)generatedMember, cancellationToken).ConfigureAwait(false); - } - else if (generatedMember.Kind == SymbolKind.Event) - { - memberContainingDocument = await codeGenService.AddEventAsync(context, containingType, (IEventSymbol)generatedMember, cancellationToken).ConfigureAwait(false); - } + // CodeGenerationOptions containing before and after + var context = new CodeGenerationSolutionContext( + document.Project.Solution, + new CodeGenerationContext( + contextLocation: semanticModel.SyntaxTree.GetLocation(TextSpan.FromBounds(line.Start, line.Start))), + fallbackOptions); - return memberContainingDocument; - } + var generatedMember = await GenerateMemberAsync(overriddenMember, containingType, document, completionItem, cancellationToken).ConfigureAwait(false); + generatedMember = _annotation.AddAnnotationToSymbol(generatedMember); - private TextSpan ComputeDestinationSpan(SyntaxNode insertionRoot) + Document? memberContainingDocument = null; + if (generatedMember.Kind == SymbolKind.Method) { - var targetToken = insertionRoot.GetAnnotatedTokens(_otherAnnotation).FirstOrNull(); - Contract.ThrowIfNull(targetToken); - - var text = insertionRoot.GetText(); - var line = text.Lines.GetLineFromPosition(targetToken.Value.Span.End); - - // DevDiv 958235: - // - // void goo() - // { - // } - // override $$ - // - // If our text edit includes the trailing trivia of the close brace of goo(), - // that token will be reconstructed. The ensuing tree diff will then count - // the { } as replaced even though we didn't want it to. If the user - // has collapsed the outline for goo, that means we'll edit the outlined - // region and weird stuff will happen. Therefore, we'll start with the first - // token on the line in order to leave the token and its trivia alone. - var position = line.GetFirstNonWhitespacePosition(); - Contract.ThrowIfNull(position); - - var firstToken = insertionRoot.FindToken(position.Value); - return TextSpan.FromBounds(firstToken.SpanStart, line.End); + memberContainingDocument = await codeGenService.AddMethodAsync(context, containingType, (IMethodSymbol)generatedMember, cancellationToken).ConfigureAwait(false); } - - private async Task GenerateInsertionTextAsync( - Document memberContainingDocument, CodeCleanupOptions cleanupOptions, CancellationToken cancellationToken) + else if (generatedMember.Kind == SymbolKind.Property) { - memberContainingDocument = await Simplifier.ReduceAsync(memberContainingDocument, Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false); - memberContainingDocument = await Formatter.FormatAsync(memberContainingDocument, Formatter.Annotation, cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false); - - var root = await memberContainingDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - return root.GetAnnotatedNodes(_annotation).Single().ToString().Trim(); + memberContainingDocument = await codeGenService.AddPropertyAsync(context, containingType, (IPropertySymbol)generatedMember, cancellationToken).ConfigureAwait(false); } - - private async Task GetTreeWithAddedSyntaxNodeRemovedAsync( - Document document, CodeCleanupOptions cleanupOptions, CancellationToken cancellationToken) + else if (generatedMember.Kind == SymbolKind.Event) { - // Added imports are annotated for simplification too. Therefore, we simplify the document - // before removing added member node to preserve those imports in the document. - document = await Simplifier.ReduceAsync(document, Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false); + memberContainingDocument = await codeGenService.AddEventAsync(context, containingType, (IEventSymbol)generatedMember, cancellationToken).ConfigureAwait(false); + } - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var members = root.GetAnnotatedNodes(_annotation).AsImmutable(); + return memberContainingDocument; + } - root = root.RemoveNodes(members, SyntaxRemoveOptions.KeepUnbalancedDirectives); - Contract.ThrowIfNull(root); + private TextSpan ComputeDestinationSpan(SyntaxNode insertionRoot) + { + var targetToken = insertionRoot.GetAnnotatedTokens(_otherAnnotation).FirstOrNull(); + Contract.ThrowIfNull(targetToken); + + var text = insertionRoot.GetText(); + var line = text.Lines.GetLineFromPosition(targetToken.Value.Span.End); + + // DevDiv 958235: + // + // void goo() + // { + // } + // override $$ + // + // If our text edit includes the trailing trivia of the close brace of goo(), + // that token will be reconstructed. The ensuing tree diff will then count + // the { } as replaced even though we didn't want it to. If the user + // has collapsed the outline for goo, that means we'll edit the outlined + // region and weird stuff will happen. Therefore, we'll start with the first + // token on the line in order to leave the token and its trivia alone. + var position = line.GetFirstNonWhitespacePosition(); + Contract.ThrowIfNull(position); + + var firstToken = insertionRoot.FindToken(position.Value); + return TextSpan.FromBounds(firstToken.SpanStart, line.End); + } - var dismemberedDocument = document.WithSyntaxRoot(root); + private async Task GenerateInsertionTextAsync( + Document memberContainingDocument, CodeCleanupOptions cleanupOptions, CancellationToken cancellationToken) + { + memberContainingDocument = await Simplifier.ReduceAsync(memberContainingDocument, Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false); + memberContainingDocument = await Formatter.FormatAsync(memberContainingDocument, Formatter.Annotation, cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false); - dismemberedDocument = await Formatter.FormatAsync(dismemberedDocument, Formatter.Annotation, cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false); - return await dismemberedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - } + var root = await memberContainingDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + return root.GetAnnotatedNodes(_annotation).Single().ToString().Trim(); + } - private static readonly ImmutableArray s_commitRules = [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, '(')]; + private async Task GetTreeWithAddedSyntaxNodeRemovedAsync( + Document document, CodeCleanupOptions cleanupOptions, CancellationToken cancellationToken) + { + // Added imports are annotated for simplification too. Therefore, we simplify the document + // before removing added member node to preserve those imports in the document. + document = await Simplifier.ReduceAsync(document, Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false); - private static readonly ImmutableArray s_filterRules = [CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '(')]; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var members = root.GetAnnotatedNodes(_annotation).AsImmutable(); - private static readonly CompletionItemRules s_defaultRules = - CompletionItemRules.Create( - commitCharacterRules: s_commitRules, - filterCharacterRules: s_filterRules, - enterKeyRule: EnterKeyRule.Never); + root = root.RemoveNodes(members, SyntaxRemoveOptions.KeepUnbalancedDirectives); + Contract.ThrowIfNull(root); - protected static CompletionItemRules GetRules() - => s_defaultRules; + var dismemberedDocument = document.WithSyntaxRoot(root); - internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => MemberInsertionCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + dismemberedDocument = await Formatter.FormatAsync(dismemberedDocument, Formatter.Annotation, cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false); + return await dismemberedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); } + + private static readonly ImmutableArray s_commitRules = [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, '(')]; + + private static readonly ImmutableArray s_filterRules = [CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '(')]; + + private static readonly CompletionItemRules s_defaultRules = + CompletionItemRules.Create( + commitCharacterRules: s_commitRules, + filterCharacterRules: s_filterRules, + enterKeyRule: EnterKeyRule.Never); + + protected static CompletionItemRules GetRules() + => s_defaultRules; + + internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => MemberInsertionCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractObjectCreationCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractObjectCreationCompletionProvider.cs index e46c3612c2c3d..8e10ec8999830 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractObjectCreationCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractObjectCreationCompletionProvider.cs @@ -12,92 +12,91 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractObjectCreationCompletionProvider : AbstractSymbolCompletionProvider + where TSyntaxContext : SyntaxContext { - internal abstract class AbstractObjectCreationCompletionProvider : AbstractSymbolCompletionProvider - where TSyntaxContext : SyntaxContext + /// + /// Return null if not in object creation type context. + /// + protected abstract SyntaxNode? GetObjectCreationNewExpression(SyntaxTree tree, int position, CancellationToken cancellationToken); + protected abstract CompletionItemRules GetCompletionItemRules(ImmutableArray symbols); + + protected override CompletionItem CreateItem( + CompletionContext completionContext, + string displayText, + string displayTextSuffix, + string insertionText, + ImmutableArray symbols, + TSyntaxContext context, + SupportedPlatformData? supportedPlatformData) { - /// - /// Return null if not in object creation type context. - /// - protected abstract SyntaxNode? GetObjectCreationNewExpression(SyntaxTree tree, int position, CancellationToken cancellationToken); - protected abstract CompletionItemRules GetCompletionItemRules(ImmutableArray symbols); - - protected override CompletionItem CreateItem( - CompletionContext completionContext, - string displayText, - string displayTextSuffix, - string insertionText, - ImmutableArray symbols, - TSyntaxContext context, - SupportedPlatformData? supportedPlatformData) - { - return SymbolCompletionItem.CreateWithSymbolId( - displayText: displayText, - displayTextSuffix: displayTextSuffix, - symbols: symbols.SelectAsArray(t => t.Symbol), - // Always preselect - rules: GetCompletionItemRules(symbols).WithMatchPriority(MatchPriority.Preselect), - contextPosition: context.Position, - insertionText: insertionText, - filterText: GetFilterTextDefault(symbols[0].Symbol, displayText, context), - supportedPlatforms: supportedPlatformData); - } + return SymbolCompletionItem.CreateWithSymbolId( + displayText: displayText, + displayTextSuffix: displayTextSuffix, + symbols: symbols.SelectAsArray(t => t.Symbol), + // Always preselect + rules: GetCompletionItemRules(symbols).WithMatchPriority(MatchPriority.Preselect), + contextPosition: context.Position, + insertionText: insertionText, + filterText: GetFilterTextDefault(symbols[0].Symbol, displayText, context), + supportedPlatforms: supportedPlatformData); + } - protected override Task> GetSymbolsAsync( - CompletionContext? completionContext, TSyntaxContext context, int position, CompletionOptions options, CancellationToken cancellationToken) - { - var newExpression = GetObjectCreationNewExpression(context.SyntaxTree, position, cancellationToken); - if (newExpression == null) - return SpecializedTasks.EmptyImmutableArray(); + protected override Task> GetSymbolsAsync( + CompletionContext? completionContext, TSyntaxContext context, int position, CompletionOptions options, CancellationToken cancellationToken) + { + var newExpression = GetObjectCreationNewExpression(context.SyntaxTree, position, cancellationToken); + if (newExpression == null) + return SpecializedTasks.EmptyImmutableArray(); - var typeInferenceService = context.GetRequiredLanguageService(); - var type = typeInferenceService.InferType( - context.SemanticModel, position, objectAsDefault: false, cancellationToken: cancellationToken); + var typeInferenceService = context.GetRequiredLanguageService(); + var type = typeInferenceService.InferType( + context.SemanticModel, position, objectAsDefault: false, cancellationToken: cancellationToken); - // Unwrap an array type fully. We only want to offer the underlying element type in the - // list of completion items. - var isArray = type is IArrayTypeSymbol; - while (type is IArrayTypeSymbol arrayType) - type = arrayType.ElementType; + // Unwrap an array type fully. We only want to offer the underlying element type in the + // list of completion items. + var isArray = type is IArrayTypeSymbol; + while (type is IArrayTypeSymbol arrayType) + type = arrayType.ElementType; - if (type == null) - return SpecializedTasks.EmptyImmutableArray(); + if (type == null) + return SpecializedTasks.EmptyImmutableArray(); - // Unwrap nullable - if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - type = type.GetTypeArguments().Single(); + // Unwrap nullable + if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + type = type.GetTypeArguments().Single(); - if (type.SpecialType == SpecialType.System_Void) - return SpecializedTasks.EmptyImmutableArray(); + if (type.SpecialType == SpecialType.System_Void) + return SpecializedTasks.EmptyImmutableArray(); - if (type.ContainsAnonymousType()) - return SpecializedTasks.EmptyImmutableArray(); + if (type.ContainsAnonymousType()) + return SpecializedTasks.EmptyImmutableArray(); - if (!type.CanBeReferencedByName) - return SpecializedTasks.EmptyImmutableArray(); + if (!type.CanBeReferencedByName) + return SpecializedTasks.EmptyImmutableArray(); - // Normally the user can't say things like "new IList". Except for "IList[] x = new |". - // In this case we do want to allow them to preselect certain types in the completion - // list even if they can't new them directly. - if (!isArray) + // Normally the user can't say things like "new IList". Except for "IList[] x = new |". + // In this case we do want to allow them to preselect certain types in the completion + // list even if they can't new them directly. + if (!isArray) + { + if (type.TypeKind is TypeKind.Interface or TypeKind.Pointer or TypeKind.Dynamic || + type.IsAbstract) { - if (type.TypeKind is TypeKind.Interface or TypeKind.Pointer or TypeKind.Dynamic || - type.IsAbstract) - { - return SpecializedTasks.EmptyImmutableArray(); - } - - if (type is ITypeParameterSymbol typeParameter && !typeParameter.HasConstructorConstraint) - return SpecializedTasks.EmptyImmutableArray(); + return SpecializedTasks.EmptyImmutableArray(); } - if (!type.IsEditorBrowsable(options.HideAdvancedMembers, context.SemanticModel.Compilation)) + if (type is ITypeParameterSymbol typeParameter && !typeParameter.HasConstructorConstraint) return SpecializedTasks.EmptyImmutableArray(); - - // In the case of array creation, we don't offer a preselected/hard-selected item because - // the user may want an implicitly-typed array creation - return Task.FromResult(ImmutableArray.Create(new SymbolAndSelectionInfo(Symbol: type, Preselect: !isArray))); } + + if (!type.IsEditorBrowsable(options.HideAdvancedMembers, context.SemanticModel.Compilation)) + return SpecializedTasks.EmptyImmutableArray(); + + // In the case of array creation, we don't offer a preselected/hard-selected item because + // the user may want an implicitly-typed array creation + return Task.FromResult(ImmutableArray.Create(new SymbolAndSelectionInfo(Symbol: type, Preselect: !isArray))); } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractObjectInitializerCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractObjectInitializerCompletionProvider.cs index 64b6fe0dd2a11..e772caac1da80 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractObjectInitializerCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractObjectInitializerCompletionProvider.cs @@ -13,120 +13,119 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractObjectInitializerCompletionProvider : LSPCompletionProvider { - internal abstract class AbstractObjectInitializerCompletionProvider : LSPCompletionProvider + protected abstract Tuple? GetInitializedType(Document document, SemanticModel semanticModel, int position, CancellationToken cancellationToken); + protected abstract HashSet GetInitializedMembers(SyntaxTree tree, int position, CancellationToken cancellationToken); + protected abstract string EscapeIdentifier(ISymbol symbol); + + public override async Task ProvideCompletionsAsync(CompletionContext context) { - protected abstract Tuple? GetInitializedType(Document document, SemanticModel semanticModel, int position, CancellationToken cancellationToken); - protected abstract HashSet GetInitializedMembers(SyntaxTree tree, int position, CancellationToken cancellationToken); - protected abstract string EscapeIdentifier(ISymbol symbol); + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; - public override async Task ProvideCompletionsAsync(CompletionContext context) + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); + if (GetInitializedType(document, semanticModel, position, cancellationToken) is not var (type, initializerLocation)) { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; + return; + } - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); - if (GetInitializedType(document, semanticModel, position, cancellationToken) is not var (type, initializerLocation)) - { - return; - } + if (type is ITypeParameterSymbol typeParameterSymbol) + { + type = typeParameterSymbol.GetNamedTypeSymbolConstraint(); + } - if (type is ITypeParameterSymbol typeParameterSymbol) - { - type = typeParameterSymbol.GetNamedTypeSymbolConstraint(); - } + if (type is not INamedTypeSymbol initializedType) + { + return; + } - if (type is not INamedTypeSymbol initializedType) - { - return; - } + if (await IsExclusiveAsync(document, position, cancellationToken).ConfigureAwait(false)) + { + context.IsExclusive = true; + } - if (await IsExclusiveAsync(document, position, cancellationToken).ConfigureAwait(false)) - { - context.IsExclusive = true; - } + var enclosing = semanticModel.GetEnclosingNamedType(position, cancellationToken); + Contract.ThrowIfNull(enclosing); - var enclosing = semanticModel.GetEnclosingNamedType(position, cancellationToken); - Contract.ThrowIfNull(enclosing); + // Find the members that can be initialized. If we have a NamedTypeSymbol, also get the overridden members. + IEnumerable members = semanticModel.LookupSymbols(position, initializedType); + members = members.Where(m => IsInitializable(m, enclosing) && + m.CanBeReferencedByName && + IsLegalFieldOrProperty(m) && + !m.IsImplicitlyDeclared); - // Find the members that can be initialized. If we have a NamedTypeSymbol, also get the overridden members. - IEnumerable members = semanticModel.LookupSymbols(position, initializedType); - members = members.Where(m => IsInitializable(m, enclosing) && - m.CanBeReferencedByName && - IsLegalFieldOrProperty(m) && - !m.IsImplicitlyDeclared); + // Filter out those members that have already been typed + var alreadyTypedMembers = GetInitializedMembers(semanticModel.SyntaxTree, position, cancellationToken); + var uninitializedMembers = members.Where(m => !alreadyTypedMembers.Contains(m.Name)); - // Filter out those members that have already been typed - var alreadyTypedMembers = GetInitializedMembers(semanticModel.SyntaxTree, position, cancellationToken); - var uninitializedMembers = members.Where(m => !alreadyTypedMembers.Contains(m.Name)); + // Sort the members by name so if we preselect one, it'll be stable + uninitializedMembers = uninitializedMembers.Where(m => m.IsEditorBrowsable(context.CompletionOptions.HideAdvancedMembers, semanticModel.Compilation)) + .OrderBy(m => m.Name); - // Sort the members by name so if we preselect one, it'll be stable - uninitializedMembers = uninitializedMembers.Where(m => m.IsEditorBrowsable(context.CompletionOptions.HideAdvancedMembers, semanticModel.Compilation)) - .OrderBy(m => m.Name); + var firstUnitializedRequiredMember = true; - var firstUnitializedRequiredMember = true; + foreach (var uninitializedMember in uninitializedMembers) + { + var rules = s_rules; - foreach (var uninitializedMember in uninitializedMembers) + // We'll hard select the first required member to make it a bit easier to type out an object initializer + // with a bunch of members. + if (firstUnitializedRequiredMember && uninitializedMember.IsRequired()) { - var rules = s_rules; - - // We'll hard select the first required member to make it a bit easier to type out an object initializer - // with a bunch of members. - if (firstUnitializedRequiredMember && uninitializedMember.IsRequired()) - { - rules = rules.WithSelectionBehavior(CompletionItemSelectionBehavior.HardSelection).WithMatchPriority(MatchPriority.Preselect); - firstUnitializedRequiredMember = false; - } - - context.AddItem(SymbolCompletionItem.CreateWithSymbolId( - displayText: EscapeIdentifier(uninitializedMember), - displayTextSuffix: "", - insertionText: null, - symbols: ImmutableArray.Create(uninitializedMember), - contextPosition: initializerLocation.SourceSpan.Start, - inlineDescription: uninitializedMember.IsRequired() ? FeaturesResources.Required : null, - rules: rules)); + rules = rules.WithSelectionBehavior(CompletionItemSelectionBehavior.HardSelection).WithMatchPriority(MatchPriority.Preselect); + firstUnitializedRequiredMember = false; } + + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( + displayText: EscapeIdentifier(uninitializedMember), + displayTextSuffix: "", + insertionText: null, + symbols: ImmutableArray.Create(uninitializedMember), + contextPosition: initializerLocation.SourceSpan.Start, + inlineDescription: uninitializedMember.IsRequired() ? FeaturesResources.Required : null, + rules: rules)); } + } - internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); - protected abstract Task IsExclusiveAsync(Document document, int position, CancellationToken cancellationToken); + protected abstract Task IsExclusiveAsync(Document document, int position, CancellationToken cancellationToken); - private static bool IsLegalFieldOrProperty(ISymbol symbol) - { - return symbol.IsWriteableFieldOrProperty() - || symbol.ContainingType.IsAnonymousType - || CanSupportObjectInitializer(symbol); - } + private static bool IsLegalFieldOrProperty(ISymbol symbol) + { + return symbol.IsWriteableFieldOrProperty() + || symbol.ContainingType.IsAnonymousType + || CanSupportObjectInitializer(symbol); + } + + private static readonly CompletionItemRules s_rules = CompletionItemRules.Create(enterKeyRule: EnterKeyRule.Never); + + protected virtual bool IsInitializable(ISymbol member, INamedTypeSymbol containingType) + { + return + !member.IsStatic && + member.MatchesKind(SymbolKind.Field, SymbolKind.Property) && + member.IsAccessibleWithin(containingType); + } - private static readonly CompletionItemRules s_rules = CompletionItemRules.Create(enterKeyRule: EnterKeyRule.Never); + private static bool CanSupportObjectInitializer(ISymbol symbol) + { + Debug.Assert(!symbol.IsWriteableFieldOrProperty(), "Assertion failed - expected writable field/property check before calling this method."); - protected virtual bool IsInitializable(ISymbol member, INamedTypeSymbol containingType) + if (symbol is IFieldSymbol fieldSymbol) { - return - !member.IsStatic && - member.MatchesKind(SymbolKind.Field, SymbolKind.Property) && - member.IsAccessibleWithin(containingType); + return !fieldSymbol.Type.IsStructType(); } - - private static bool CanSupportObjectInitializer(ISymbol symbol) + else if (symbol is IPropertySymbol propertySymbol) { - Debug.Assert(!symbol.IsWriteableFieldOrProperty(), "Assertion failed - expected writable field/property check before calling this method."); - - if (symbol is IFieldSymbol fieldSymbol) - { - return !fieldSymbol.Type.IsStructType(); - } - else if (symbol is IPropertySymbol propertySymbol) - { - return !propertySymbol.Type.IsStructType(); - } - - throw ExceptionUtilities.Unreachable(); + return !propertySymbol.Type.IsStructType(); } + + throw ExceptionUtilities.Unreachable(); } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractOverrideCompletionProvider.ItemGetter.cs b/src/Features/Core/Portable/Completion/Providers/AbstractOverrideCompletionProvider.ItemGetter.cs index 6c28fd01dc641..9e07d92fef95c 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractOverrideCompletionProvider.ItemGetter.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractOverrideCompletionProvider.ItemGetter.cs @@ -9,166 +9,165 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract partial class AbstractOverrideCompletionProvider { - internal abstract partial class AbstractOverrideCompletionProvider + private partial class ItemGetter { - private partial class ItemGetter + private readonly CancellationToken _cancellationToken; + private readonly int _position; + private readonly AbstractOverrideCompletionProvider _provider; + private readonly SymbolDisplayFormat _overrideNameFormat = SymbolDisplayFormats.NameFormat.WithParameterOptions( + SymbolDisplayParameterOptions.IncludeDefaultValue | + SymbolDisplayParameterOptions.IncludeExtensionThis | + SymbolDisplayParameterOptions.IncludeType | + SymbolDisplayParameterOptions.IncludeName | + SymbolDisplayParameterOptions.IncludeParamsRefOut) + .AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + + private readonly Document _document; + private readonly SourceText _text; + private readonly SyntaxTree _syntaxTree; + private readonly int _startLineNumber; + + private ItemGetter( + AbstractOverrideCompletionProvider overrideCompletionProvider, + Document document, + int position, + SourceText text, + SyntaxTree syntaxTree, + int startLineNumber, + CancellationToken cancellationToken) { - private readonly CancellationToken _cancellationToken; - private readonly int _position; - private readonly AbstractOverrideCompletionProvider _provider; - private readonly SymbolDisplayFormat _overrideNameFormat = SymbolDisplayFormats.NameFormat.WithParameterOptions( - SymbolDisplayParameterOptions.IncludeDefaultValue | - SymbolDisplayParameterOptions.IncludeExtensionThis | - SymbolDisplayParameterOptions.IncludeType | - SymbolDisplayParameterOptions.IncludeName | - SymbolDisplayParameterOptions.IncludeParamsRefOut) - .AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); - - private readonly Document _document; - private readonly SourceText _text; - private readonly SyntaxTree _syntaxTree; - private readonly int _startLineNumber; - - private ItemGetter( - AbstractOverrideCompletionProvider overrideCompletionProvider, - Document document, - int position, - SourceText text, - SyntaxTree syntaxTree, - int startLineNumber, - CancellationToken cancellationToken) - { - _provider = overrideCompletionProvider; - _document = document; - _position = position; - _text = text; - _syntaxTree = syntaxTree; - _startLineNumber = startLineNumber; - _cancellationToken = cancellationToken; - } + _provider = overrideCompletionProvider; + _document = document; + _position = position; + _text = text; + _syntaxTree = syntaxTree; + _startLineNumber = startLineNumber; + _cancellationToken = cancellationToken; + } - public static async Task CreateAsync( - AbstractOverrideCompletionProvider overrideCompletionProvider, - Document document, - int position, - CancellationToken cancellationToken) - { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + public static async Task CreateAsync( + AbstractOverrideCompletionProvider overrideCompletionProvider, + Document document, + int position, + CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var startLineNumber = text.Lines.IndexOf(position); - return new ItemGetter(overrideCompletionProvider, document, position, text, syntaxTree, startLineNumber, cancellationToken); - } + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var startLineNumber = text.Lines.IndexOf(position); + return new ItemGetter(overrideCompletionProvider, document, position, text, syntaxTree, startLineNumber, cancellationToken); + } - public async Task> GetItemsAsync() + public async Task> GetItemsAsync() + { + // modifiers* override modifiers* type? | + if (!TryCheckForTrailingTokens(_position)) + return default; + + var startToken = _provider.FindStartingToken(_syntaxTree, _position, _cancellationToken); + if (startToken.Parent == null) + return default; + + var semanticModel = await _document.ReuseExistingSpeculativeModelAsync(startToken.Parent, _cancellationToken).ConfigureAwait(false); + if (!_provider.TryDetermineReturnType(startToken, semanticModel, _cancellationToken, out var returnType, out var tokenAfterReturnType) || + !_provider.TryDetermineModifiers(tokenAfterReturnType, _text, _startLineNumber, out var seenAccessibility, out var modifiers) || + !TryDetermineOverridableMembers(semanticModel, startToken, seenAccessibility, out var overridableMembers)) { - // modifiers* override modifiers* type? | - if (!TryCheckForTrailingTokens(_position)) - return default; - - var startToken = _provider.FindStartingToken(_syntaxTree, _position, _cancellationToken); - if (startToken.Parent == null) - return default; - - var semanticModel = await _document.ReuseExistingSpeculativeModelAsync(startToken.Parent, _cancellationToken).ConfigureAwait(false); - if (!_provider.TryDetermineReturnType(startToken, semanticModel, _cancellationToken, out var returnType, out var tokenAfterReturnType) || - !_provider.TryDetermineModifiers(tokenAfterReturnType, _text, _startLineNumber, out var seenAccessibility, out var modifiers) || - !TryDetermineOverridableMembers(semanticModel, startToken, seenAccessibility, out var overridableMembers)) - { - return default; - } - - return _provider - .FilterOverrides(overridableMembers, returnType) - .SelectAsArray(m => CreateItem(m, semanticModel, startToken, modifiers)); + return default; } - private CompletionItem CreateItem( - ISymbol symbol, SemanticModel semanticModel, - SyntaxToken startToken, DeclarationModifiers modifiers) - { - var position = startToken.SpanStart; - - var displayString = symbol.ToMinimalDisplayString(semanticModel, position, _overrideNameFormat); - - return MemberInsertionCompletionItem.Create( - displayString, - displayTextSuffix: "", - modifiers, - _startLineNumber, - symbol, - startToken, - position, - rules: GetRules()); - } + return _provider + .FilterOverrides(overridableMembers, returnType) + .SelectAsArray(m => CreateItem(m, semanticModel, startToken, modifiers)); + } - private bool TryDetermineOverridableMembers( - SemanticModel semanticModel, - SyntaxToken startToken, - Accessibility seenAccessibility, - out ImmutableArray overridableMembers) + private CompletionItem CreateItem( + ISymbol symbol, SemanticModel semanticModel, + SyntaxToken startToken, DeclarationModifiers modifiers) + { + var position = startToken.SpanStart; + + var displayString = symbol.ToMinimalDisplayString(semanticModel, position, _overrideNameFormat); + + return MemberInsertionCompletionItem.Create( + displayString, + displayTextSuffix: "", + modifiers, + _startLineNumber, + symbol, + startToken, + position, + rules: GetRules()); + } + + private bool TryDetermineOverridableMembers( + SemanticModel semanticModel, + SyntaxToken startToken, + Accessibility seenAccessibility, + out ImmutableArray overridableMembers) + { + var containingType = semanticModel.GetEnclosingSymbol(startToken.SpanStart, _cancellationToken); + if (containingType is null) { - var containingType = semanticModel.GetEnclosingSymbol(startToken.SpanStart, _cancellationToken); - if (containingType is null) - { - overridableMembers = default; - return false; - } + overridableMembers = default; + return false; + } - var result = containingType.GetOverridableMembers(_cancellationToken); + var result = containingType.GetOverridableMembers(_cancellationToken); - // Filter based on accessibility - if (seenAccessibility != Accessibility.NotApplicable) - result = result.WhereAsArray(m => MatchesAccessibility(m.DeclaredAccessibility, seenAccessibility)); + // Filter based on accessibility + if (seenAccessibility != Accessibility.NotApplicable) + result = result.WhereAsArray(m => MatchesAccessibility(m.DeclaredAccessibility, seenAccessibility)); - overridableMembers = result; - return overridableMembers.Length > 0; + overridableMembers = result; + return overridableMembers.Length > 0; - static bool MatchesAccessibility(Accessibility declaredAccessibility, Accessibility seenAccessibility) + static bool MatchesAccessibility(Accessibility declaredAccessibility, Accessibility seenAccessibility) + { + // since some accessibility modifiers take two keywords, allow filtering to those if the user has + // only typed one of the keywords. This makes it less onerous than having to determine the exact + // right modifier set to specify, and follows the intuition of writing less filtering less and + // writing more filtering out more. + return seenAccessibility switch { - // since some accessibility modifiers take two keywords, allow filtering to those if the user has - // only typed one of the keywords. This makes it less onerous than having to determine the exact - // right modifier set to specify, and follows the intuition of writing less filtering less and - // writing more filtering out more. - return seenAccessibility switch - { - // `private`, `private protected` - Accessibility.Private => declaredAccessibility is Accessibility.Private or Accessibility.ProtectedAndInternal, - // `protected`, `private protected`, `protected internal` - Accessibility.Protected => declaredAccessibility is Accessibility.Protected or Accessibility.ProtectedAndInternal or Accessibility.ProtectedOrInternal, - // `internal`, `protected internal` - Accessibility.Internal => declaredAccessibility is Accessibility.Internal or Accessibility.ProtectedOrInternal, - // For anything else, require an exact match. - _ => declaredAccessibility == seenAccessibility, - }; - } + // `private`, `private protected` + Accessibility.Private => declaredAccessibility is Accessibility.Private or Accessibility.ProtectedAndInternal, + // `protected`, `private protected`, `protected internal` + Accessibility.Protected => declaredAccessibility is Accessibility.Protected or Accessibility.ProtectedAndInternal or Accessibility.ProtectedOrInternal, + // `internal`, `protected internal` + Accessibility.Internal => declaredAccessibility is Accessibility.Internal or Accessibility.ProtectedOrInternal, + // For anything else, require an exact match. + _ => declaredAccessibility == seenAccessibility, + }; } + } - private bool TryCheckForTrailingTokens(int position) - { - var root = _syntaxTree.GetRoot(_cancellationToken); - var token = root.FindToken(position); - - // Don't want to offer Override completion if there's a token after the current - // position. - if (token.SpanStart > position) - { - return false; - } + private bool TryCheckForTrailingTokens(int position) + { + var root = _syntaxTree.GetRoot(_cancellationToken); + var token = root.FindToken(position); - // If the next token is also on our line then we don't want to offer completion. - if (IsOnStartLine(token.GetNextToken().SpanStart)) - { - return false; - } + // Don't want to offer Override completion if there's a token after the current + // position. + if (token.SpanStart > position) + { + return false; + } - return true; + // If the next token is also on our line then we don't want to offer completion. + if (IsOnStartLine(token.GetNextToken().SpanStart)) + { + return false; } - private bool IsOnStartLine(int position) - => _text.Lines.IndexOf(position) == _startLineNumber; + return true; } + + private bool IsOnStartLine(int position) + => _text.Lines.IndexOf(position) == _startLineNumber; } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractOverrideCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractOverrideCompletionProvider.cs index 60f4fbf04d4a5..a686af662ddba 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractOverrideCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractOverrideCompletionProvider.cs @@ -13,69 +13,68 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract partial class AbstractOverrideCompletionProvider() : AbstractMemberInsertingCompletionProvider { - internal abstract partial class AbstractOverrideCompletionProvider() : AbstractMemberInsertingCompletionProvider + public abstract SyntaxToken FindStartingToken(SyntaxTree tree, int position, CancellationToken cancellationToken); + public abstract ImmutableArray FilterOverrides(ImmutableArray members, ITypeSymbol? returnType); + public abstract bool TryDetermineModifiers(SyntaxToken startToken, SourceText text, int startLine, out Accessibility seenAccessibility, out DeclarationModifiers modifiers); + + public override async Task ProvideCompletionsAsync(CompletionContext context) { - public abstract SyntaxToken FindStartingToken(SyntaxTree tree, int position, CancellationToken cancellationToken); - public abstract ImmutableArray FilterOverrides(ImmutableArray members, ITypeSymbol? returnType); - public abstract bool TryDetermineModifiers(SyntaxToken startToken, SourceText text, int startLine, out Accessibility seenAccessibility, out DeclarationModifiers modifiers); + var state = await ItemGetter.CreateAsync(this, context.Document, context.Position, context.CancellationToken).ConfigureAwait(false); + var items = await state.GetItemsAsync().ConfigureAwait(false); - public override async Task ProvideCompletionsAsync(CompletionContext context) + if (!items.IsDefaultOrEmpty) { - var state = await ItemGetter.CreateAsync(this, context.Document, context.Position, context.CancellationToken).ConfigureAwait(false); - var items = await state.GetItemsAsync().ConfigureAwait(false); - - if (!items.IsDefaultOrEmpty) - { - context.IsExclusive = true; - context.AddItems(items); - } + context.IsExclusive = true; + context.AddItems(items); } + } - protected override Task GenerateMemberAsync(ISymbol newOverriddenMember, INamedTypeSymbol newContainingType, Document newDocument, CompletionItem completionItem, CancellationToken cancellationToken) + protected override Task GenerateMemberAsync(ISymbol newOverriddenMember, INamedTypeSymbol newContainingType, Document newDocument, CompletionItem completionItem, CancellationToken cancellationToken) + { + // Special case: if you are overriding object.ToString(), we will make the return value as non-nullable. The return was made nullable because + // are implementations out there that will return null, but that's not something we really want new implementations doing. We may need to consider + // expanding this behavior to other methods in the future; if that is the case then we would want there to be an attribute on the return type + // rather than updating this list, but for now there is no such attribute until we find more cases for it. See + // https://github.com/dotnet/roslyn/issues/30317 for some additional conversation about this design decision. + // + // We don't check if methodSymbol.ContainingType is object, in case you're overriding something that is itself an override + if (newOverriddenMember is IMethodSymbol methodSymbol && + methodSymbol.Name == "ToString" && + methodSymbol.Parameters.Length == 0) { - // Special case: if you are overriding object.ToString(), we will make the return value as non-nullable. The return was made nullable because - // are implementations out there that will return null, but that's not something we really want new implementations doing. We may need to consider - // expanding this behavior to other methods in the future; if that is the case then we would want there to be an attribute on the return type - // rather than updating this list, but for now there is no such attribute until we find more cases for it. See - // https://github.com/dotnet/roslyn/issues/30317 for some additional conversation about this design decision. - // - // We don't check if methodSymbol.ContainingType is object, in case you're overriding something that is itself an override - if (newOverriddenMember is IMethodSymbol methodSymbol && - methodSymbol.Name == "ToString" && - methodSymbol.Parameters.Length == 0) - { - newOverriddenMember = CodeGenerationSymbolFactory.CreateMethodSymbol(methodSymbol, returnType: methodSymbol.ReturnType.WithNullableAnnotation(NullableAnnotation.NotAnnotated)); - } + newOverriddenMember = CodeGenerationSymbolFactory.CreateMethodSymbol(methodSymbol, returnType: methodSymbol.ReturnType.WithNullableAnnotation(NullableAnnotation.NotAnnotated)); + } - // Figure out what to insert, and do it. Throw if we've somehow managed to get this far and can't. - var syntaxFactory = newDocument.GetRequiredLanguageService(); + // Figure out what to insert, and do it. Throw if we've somehow managed to get this far and can't. + var syntaxFactory = newDocument.GetRequiredLanguageService(); - var itemModifiers = MemberInsertionCompletionItem.GetModifiers(completionItem); - var modifiers = itemModifiers.WithIsUnsafe(itemModifiers.IsUnsafe | newOverriddenMember.RequiresUnsafeModifier()); + var itemModifiers = MemberInsertionCompletionItem.GetModifiers(completionItem); + var modifiers = itemModifiers.WithIsUnsafe(itemModifiers.IsUnsafe | newOverriddenMember.RequiresUnsafeModifier()); - return syntaxFactory.OverrideAsync( - newOverriddenMember, newContainingType, newDocument, modifiers, cancellationToken); - } + return syntaxFactory.OverrideAsync( + newOverriddenMember, newContainingType, newDocument, modifiers, cancellationToken); + } - public abstract bool TryDetermineReturnType( - SyntaxToken startToken, - SemanticModel semanticModel, - CancellationToken cancellationToken, - out ITypeSymbol? returnType, - out SyntaxToken nextToken); + public abstract bool TryDetermineReturnType( + SyntaxToken startToken, + SemanticModel semanticModel, + CancellationToken cancellationToken, + out ITypeSymbol? returnType, + out SyntaxToken nextToken); - protected static bool IsOnStartLine(int position, SourceText text, int startLine) - => text.Lines.IndexOf(position) == startLine; + protected static bool IsOnStartLine(int position, SourceText text, int startLine) + => text.Lines.IndexOf(position) == startLine; - protected static ITypeSymbol GetReturnType(ISymbol symbol) - => symbol.Kind switch - { - SymbolKind.Event => ((IEventSymbol)symbol).Type, - SymbolKind.Method => ((IMethodSymbol)symbol).ReturnType, - SymbolKind.Property => ((IPropertySymbol)symbol).Type, - _ => throw ExceptionUtilities.UnexpectedValue(symbol.Kind), - }; - } + protected static ITypeSymbol GetReturnType(ISymbol symbol) + => symbol.Kind switch + { + SymbolKind.Event => ((IEventSymbol)symbol).Type, + SymbolKind.Method => ((IMethodSymbol)symbol).ReturnType, + SymbolKind.Property => ((IPropertySymbol)symbol).Type, + _ => throw ExceptionUtilities.UnexpectedValue(symbol.Kind), + }; } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractPartialMethodCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractPartialMethodCompletionProvider.cs index 6914f9af82dd2..3ee0a6af8faff 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractPartialMethodCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractPartialMethodCompletionProvider.cs @@ -12,109 +12,108 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract partial class AbstractPartialMethodCompletionProvider : AbstractMemberInsertingCompletionProvider { - internal abstract partial class AbstractPartialMethodCompletionProvider : AbstractMemberInsertingCompletionProvider + protected static readonly SymbolDisplayFormat SignatureDisplayFormat = + new( + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + memberOptions: + SymbolDisplayMemberOptions.IncludeParameters, + parameterOptions: + SymbolDisplayParameterOptions.IncludeName | + SymbolDisplayParameterOptions.IncludeType | + SymbolDisplayParameterOptions.IncludeParamsRefOut, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | + SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + protected AbstractPartialMethodCompletionProvider() { - protected static readonly SymbolDisplayFormat SignatureDisplayFormat = - new( - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - memberOptions: - SymbolDisplayMemberOptions.IncludeParameters, - parameterOptions: - SymbolDisplayParameterOptions.IncludeName | - SymbolDisplayParameterOptions.IncludeType | - SymbolDisplayParameterOptions.IncludeParamsRefOut, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - protected AbstractPartialMethodCompletionProvider() - { - } + } - protected abstract bool IncludeAccessibility(IMethodSymbol method, CancellationToken cancellationToken); - protected abstract bool IsPartialMethodCompletionContext(SyntaxTree tree, int position, CancellationToken cancellationToken, out DeclarationModifiers modifiers, out SyntaxToken token); - protected abstract string GetDisplayText(IMethodSymbol method, SemanticModel semanticModel, int position); - protected abstract bool IsPartial(IMethodSymbol method); + protected abstract bool IncludeAccessibility(IMethodSymbol method, CancellationToken cancellationToken); + protected abstract bool IsPartialMethodCompletionContext(SyntaxTree tree, int position, CancellationToken cancellationToken, out DeclarationModifiers modifiers, out SyntaxToken token); + protected abstract string GetDisplayText(IMethodSymbol method, SemanticModel semanticModel, int position); + protected abstract bool IsPartial(IMethodSymbol method); - public override async Task ProvideCompletionsAsync(CompletionContext context) + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; + + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (!IsPartialMethodCompletionContext(tree, position, cancellationToken, out var modifiers, out var token)) { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; - - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (!IsPartialMethodCompletionContext(tree, position, cancellationToken, out var modifiers, out var token)) - { - return; - } - - var items = await CreatePartialItemsAsync( - document, position, context.CompletionListSpan, modifiers, token, cancellationToken).ConfigureAwait(false); - - if (items?.Any() == true) - { - context.IsExclusive = true; - context.AddItems(items); - } + return; } - protected override async Task GenerateMemberAsync(ISymbol member, INamedTypeSymbol containingType, Document document, CompletionItem item, CancellationToken cancellationToken) + var items = await CreatePartialItemsAsync( + document, position, context.CompletionListSpan, modifiers, token, cancellationToken).ConfigureAwait(false); + + if (items?.Any() == true) { - var syntaxFactory = document.GetLanguageService(); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var method = (IMethodSymbol)member; - return CodeGenerationSymbolFactory.CreateMethodSymbol( - attributes: [], - accessibility: IncludeAccessibility(method, cancellationToken) ? method.DeclaredAccessibility : Accessibility.NotApplicable, - modifiers: MemberInsertionCompletionItem.GetModifiers(item), - returnType: method.ReturnType, - refKind: method.RefKind, - explicitInterfaceImplementations: default, - name: member.Name, - typeParameters: method.TypeParameters, - parameters: method.Parameters.SelectAsArray(p => CodeGenerationSymbolFactory.CreateParameterSymbol(p.GetAttributes(), p.RefKind, p.IsParams, p.Type, p.Name)), - statements: syntaxFactory.CreateThrowNotImplementedStatementBlock(semanticModel.Compilation)); + context.IsExclusive = true; + context.AddItems(items); } + } - protected async Task?> CreatePartialItemsAsync( - Document document, int position, TextSpan span, DeclarationModifiers modifiers, SyntaxToken token, CancellationToken cancellationToken) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + protected override async Task GenerateMemberAsync(ISymbol member, INamedTypeSymbol containingType, Document document, CompletionItem item, CancellationToken cancellationToken) + { + var syntaxFactory = document.GetLanguageService(); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var method = (IMethodSymbol)member; + return CodeGenerationSymbolFactory.CreateMethodSymbol( + attributes: [], + accessibility: IncludeAccessibility(method, cancellationToken) ? method.DeclaredAccessibility : Accessibility.NotApplicable, + modifiers: MemberInsertionCompletionItem.GetModifiers(item), + returnType: method.ReturnType, + refKind: method.RefKind, + explicitInterfaceImplementations: default, + name: member.Name, + typeParameters: method.TypeParameters, + parameters: method.Parameters.SelectAsArray(p => CodeGenerationSymbolFactory.CreateParameterSymbol(p.GetAttributes(), p.RefKind, p.IsParams, p.Type, p.Name)), + statements: syntaxFactory.CreateThrowNotImplementedStatementBlock(semanticModel.Compilation)); + } - // Only inside classes and structs - if (semanticModel.GetEnclosingSymbol(position, cancellationToken) is not INamedTypeSymbol enclosingSymbol) - return null; + protected async Task?> CreatePartialItemsAsync( + Document document, int position, TextSpan span, DeclarationModifiers modifiers, SyntaxToken token, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (enclosingSymbol.TypeKind is not (TypeKind.Struct or TypeKind.Class)) - return null; + // Only inside classes and structs + if (semanticModel.GetEnclosingSymbol(position, cancellationToken) is not INamedTypeSymbol enclosingSymbol) + return null; - var symbols = semanticModel.LookupSymbols(position, container: enclosingSymbol) - .OfType() - .Where(m => IsPartial(m) && m.PartialImplementationPart == null); + if (enclosingSymbol.TypeKind is not (TypeKind.Struct or TypeKind.Class)) + return null; - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var line = text.Lines.IndexOf(position); - var lineSpan = text.Lines.GetLineFromPosition(position).Span; - return symbols.Select(s => CreateItem(s, line, span, semanticModel, modifiers, token)); - } + var symbols = semanticModel.LookupSymbols(position, container: enclosingSymbol) + .OfType() + .Where(m => IsPartial(m) && m.PartialImplementationPart == null); - private CompletionItem CreateItem(IMethodSymbol method, int line, TextSpan span, SemanticModel semanticModel, DeclarationModifiers modifiers, SyntaxToken token) - { - modifiers = new DeclarationModifiers(method.IsStatic, isUnsafe: method.RequiresUnsafeModifier(), isPartial: true, isAsync: modifiers.IsAsync); - var displayText = GetDisplayText(method, semanticModel, span.Start); - - return MemberInsertionCompletionItem.Create( - displayText, - displayTextSuffix: "", - modifiers, - line, - method, - token, - span.Start, - rules: GetRules()); - } + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var line = text.Lines.IndexOf(position); + var lineSpan = text.Lines.GetLineFromPosition(position).Span; + return symbols.Select(s => CreateItem(s, line, span, semanticModel, modifiers, token)); + } + + private CompletionItem CreateItem(IMethodSymbol method, int line, TextSpan span, SemanticModel semanticModel, DeclarationModifiers modifiers, SyntaxToken token) + { + modifiers = new DeclarationModifiers(method.IsStatic, isUnsafe: method.RequiresUnsafeModifier(), isPartial: true, isAsync: modifiers.IsAsync); + var displayText = GetDisplayText(method, semanticModel, span.Start); + + return MemberInsertionCompletionItem.Create( + displayText, + displayTextSuffix: "", + modifiers, + line, + method, + token, + span.Start, + rules: GetRules()); } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractPartialTypeCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractPartialTypeCompletionProvider.cs index 4f94ce4560ba8..65b0957c36f00 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractPartialTypeCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractPartialTypeCompletionProvider.cs @@ -15,107 +15,106 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract partial class AbstractPartialTypeCompletionProvider : LSPCompletionProvider + where TSyntaxContext : SyntaxContext { - internal abstract partial class AbstractPartialTypeCompletionProvider : LSPCompletionProvider - where TSyntaxContext : SyntaxContext + protected AbstractPartialTypeCompletionProvider() { - protected AbstractPartialTypeCompletionProvider() - { - } + } - public sealed override async Task ProvideCompletionsAsync(CompletionContext completionContext) + public sealed override async Task ProvideCompletionsAsync(CompletionContext completionContext) + { + try { - try - { - var document = completionContext.Document; - var position = completionContext.Position; - var cancellationToken = completionContext.CancellationToken; + var document = completionContext.Document; + var position = completionContext.Position; + var cancellationToken = completionContext.CancellationToken; - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var node = GetPartialTypeSyntaxNode(tree, position, cancellationToken); + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var node = GetPartialTypeSyntaxNode(tree, position, cancellationToken); - if (node != null) + if (node != null) + { + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(node, cancellationToken).ConfigureAwait(false); + if (semanticModel.GetDeclaredSymbol(node, cancellationToken) is INamedTypeSymbol declaredSymbol) { - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(node, cancellationToken).ConfigureAwait(false); - if (semanticModel.GetDeclaredSymbol(node, cancellationToken) is INamedTypeSymbol declaredSymbol) + var syntaxContextService = document.GetRequiredLanguageService(); + var syntaxContext = (TSyntaxContext)syntaxContextService.CreateContext(document, semanticModel, position, cancellationToken); + var symbols = LookupCandidateSymbols(syntaxContext, declaredSymbol, cancellationToken); + var items = symbols?.Select(s => CreateCompletionItem(s, syntaxContext)); + + if (items != null) { - var syntaxContextService = document.GetRequiredLanguageService(); - var syntaxContext = (TSyntaxContext)syntaxContextService.CreateContext(document, semanticModel, position, cancellationToken); - var symbols = LookupCandidateSymbols(syntaxContext, declaredSymbol, cancellationToken); - var items = symbols?.Select(s => CreateCompletionItem(s, syntaxContext)); - - if (items != null) - { - completionContext.AddItems(items); - } + completionContext.AddItems(items); } } } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) - { - // nop - } } - - private CompletionItem CreateCompletionItem(INamedTypeSymbol symbol, TSyntaxContext context) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) { - var (displayText, suffix, insertionText) = GetDisplayAndSuffixAndInsertionText(symbol, context); - - return SymbolCompletionItem.CreateWithSymbolId( - displayText: displayText, - displayTextSuffix: suffix, - insertionText: insertionText, - symbols: ImmutableArray.Create(symbol), - contextPosition: context.Position, - properties: GetProperties(symbol, context), - rules: CompletionItemRules.Default); + // nop } + } - protected abstract ImmutableArray> GetProperties(INamedTypeSymbol symbol, TSyntaxContext context); - - protected abstract SyntaxNode? GetPartialTypeSyntaxNode(SyntaxTree tree, int position, CancellationToken cancellationToken); - - protected abstract (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(INamedTypeSymbol symbol, TSyntaxContext context); + private CompletionItem CreateCompletionItem(INamedTypeSymbol symbol, TSyntaxContext context) + { + var (displayText, suffix, insertionText) = GetDisplayAndSuffixAndInsertionText(symbol, context); + + return SymbolCompletionItem.CreateWithSymbolId( + displayText: displayText, + displayTextSuffix: suffix, + insertionText: insertionText, + symbols: ImmutableArray.Create(symbol), + contextPosition: context.Position, + properties: GetProperties(symbol, context), + rules: CompletionItemRules.Default); + } - protected virtual IEnumerable? LookupCandidateSymbols(TSyntaxContext context, INamedTypeSymbol declaredSymbol, CancellationToken cancellationToken) - { - if (declaredSymbol == null) - { - throw new ArgumentNullException(nameof(declaredSymbol)); - } + protected abstract ImmutableArray> GetProperties(INamedTypeSymbol symbol, TSyntaxContext context); - var semanticModel = context.SemanticModel; + protected abstract SyntaxNode? GetPartialTypeSyntaxNode(SyntaxTree tree, int position, CancellationToken cancellationToken); - if (declaredSymbol.ContainingSymbol is not INamespaceOrTypeSymbol containingSymbol) - { - return SpecializedCollections.EmptyEnumerable(); - } + protected abstract (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(INamedTypeSymbol symbol, TSyntaxContext context); - return semanticModel.LookupNamespacesAndTypes(context.Position, containingSymbol) - .OfType() - .Where(symbol => declaredSymbol.TypeKind == symbol.TypeKind && - NotNewDeclaredMember(symbol, context) && - InSameProject(symbol, semanticModel.Compilation)); + protected virtual IEnumerable? LookupCandidateSymbols(TSyntaxContext context, INamedTypeSymbol declaredSymbol, CancellationToken cancellationToken) + { + if (declaredSymbol == null) + { + throw new ArgumentNullException(nameof(declaredSymbol)); } - private static bool InSameProject(INamedTypeSymbol symbol, Compilation compilation) - => symbol.DeclaringSyntaxReferences.Any(static (r, compilation) => compilation.SyntaxTrees.Contains(r.SyntaxTree), compilation); + var semanticModel = context.SemanticModel; - private static bool NotNewDeclaredMember(INamedTypeSymbol symbol, TSyntaxContext context) + if (declaredSymbol.ContainingSymbol is not INamespaceOrTypeSymbol containingSymbol) { - return symbol.DeclaringSyntaxReferences - .Select(reference => reference.GetSyntax()) - .Any(node => !(node.SyntaxTree == context.SyntaxTree && node.Span.IntersectsWith(context.Position))); + return SpecializedCollections.EmptyEnumerable(); } - internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + return semanticModel.LookupNamespacesAndTypes(context.Position, containingSymbol) + .OfType() + .Where(symbol => declaredSymbol.TypeKind == symbol.TypeKind && + NotNewDeclaredMember(symbol, context) && + InSameProject(symbol, semanticModel.Compilation)); + } + + private static bool InSameProject(INamedTypeSymbol symbol, Compilation compilation) + => symbol.DeclaringSyntaxReferences.Any(static (r, compilation) => compilation.SyntaxTrees.Contains(r.SyntaxTree), compilation); - public override Task GetTextChangeAsync(Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) - { - var insertionText = SymbolCompletionItem.GetInsertionText(selectedItem); - return Task.FromResult(new TextChange(selectedItem.Span, insertionText)); - } + private static bool NotNewDeclaredMember(INamedTypeSymbol symbol, TSyntaxContext context) + { + return symbol.DeclaringSyntaxReferences + .Select(reference => reference.GetSyntax()) + .Any(node => !(node.SyntaxTree == context.SyntaxTree && node.Span.IntersectsWith(context.Position))); + } + + internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + + public override Task GetTextChangeAsync(Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + { + var insertionText = SymbolCompletionItem.GetInsertionText(selectedItem); + return Task.FromResult(new TextChange(selectedItem.Span, insertionText)); } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractPreprocessorCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractPreprocessorCompletionProvider.cs index 219444c585205..592264a15bdc9 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractPreprocessorCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractPreprocessorCompletionProvider.cs @@ -11,44 +11,43 @@ using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractPreprocessorCompletionProvider : LSPCompletionProvider { - internal abstract class AbstractPreprocessorCompletionProvider : LSPCompletionProvider + public sealed override async Task ProvideCompletionsAsync(CompletionContext context) { - public sealed override async Task ProvideCompletionsAsync(CompletionContext context) - { - var cancellationToken = context.CancellationToken; - var originatingDocument = context.Document; - var position = context.Position; - var solution = originatingDocument.Project.Solution; - var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(originatingDocument, cancellationToken).ConfigureAwait(false); - if (!syntaxContext.IsPreProcessorExpressionContext) - return; + var cancellationToken = context.CancellationToken; + var originatingDocument = context.Document; + var position = context.Position; + var solution = originatingDocument.Project.Solution; + var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(originatingDocument, cancellationToken).ConfigureAwait(false); + if (!syntaxContext.IsPreProcessorExpressionContext) + return; - // Walk all the projects this document is linked in so that we get the full set of preprocessor symbols - // defined across all of them. - var syntaxFacts = originatingDocument.GetRequiredLanguageService(); - var preprocessorNames = new HashSet(syntaxFacts.StringComparer); + // Walk all the projects this document is linked in so that we get the full set of preprocessor symbols + // defined across all of them. + var syntaxFacts = originatingDocument.GetRequiredLanguageService(); + var preprocessorNames = new HashSet(syntaxFacts.StringComparer); - foreach (var documentId in solution.GetRelatedDocumentIds(originatingDocument.Id)) - { - var document = solution.GetRequiredDocument(documentId); - var currentSyntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + foreach (var documentId in solution.GetRelatedDocumentIds(originatingDocument.Id)) + { + var document = solution.GetRequiredDocument(documentId); + var currentSyntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - preprocessorNames.AddRange(currentSyntaxTree.Options.PreprocessorSymbolNames); - } + preprocessorNames.AddRange(currentSyntaxTree.Options.PreprocessorSymbolNames); + } - // Keep all the preprocessor symbol names together. We don't want to intermingle them with any keywords we - // include (like `true/false`) - foreach (var name in preprocessorNames.OrderBy(a => a)) - { - context.AddItem(CommonCompletionItem.Create( - name, - displayTextSuffix: "", - CompletionItemRules.Default, - glyph: Glyph.Keyword, - sortText: "_0_" + name)); - } + // Keep all the preprocessor symbol names together. We don't want to intermingle them with any keywords we + // include (like `true/false`) + foreach (var name in preprocessorNames.OrderBy(a => a)) + { + context.AddItem(CommonCompletionItem.Create( + name, + displayTextSuffix: "", + CompletionItemRules.Default, + glyph: Glyph.Keyword, + sortText: "_0_" + name)); } } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractRecommendationServiceBasedCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractRecommendationServiceBasedCompletionProvider.cs index f91eb6f8c2d0e..6a3062fe92f1b 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractRecommendationServiceBasedCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractRecommendationServiceBasedCompletionProvider.cs @@ -9,262 +9,258 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Recommendations; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers -{ - internal abstract class AbstractRecommendationServiceBasedCompletionProvider : AbstractSymbolCompletionProvider - where TSyntaxContext : SyntaxContext - { - protected abstract Task ShouldPreselectInferredTypesAsync(CompletionContext? completionContext, int position, CompletionOptions options, CancellationToken cancellationToken); - protected abstract Task ShouldProvideAvailableSymbolsInCurrentContextAsync(CompletionContext? completionContext, TSyntaxContext context, int position, CompletionOptions options, CancellationToken cancellationToken); +namespace Microsoft.CodeAnalysis.Completion.Providers; - protected abstract CompletionItemRules GetCompletionItemRules(ImmutableArray symbols, TSyntaxContext context); - protected abstract CompletionItemSelectionBehavior PreselectedItemSelectionBehavior { get; } - protected abstract bool IsInstrinsic(ISymbol symbol); - protected abstract bool IsTriggerOnDot(SyntaxToken token, int characterPosition); +internal abstract class AbstractRecommendationServiceBasedCompletionProvider : AbstractSymbolCompletionProvider + where TSyntaxContext : SyntaxContext +{ + protected abstract Task ShouldPreselectInferredTypesAsync(CompletionContext? completionContext, int position, CompletionOptions options, CancellationToken cancellationToken); + protected abstract Task ShouldProvideAvailableSymbolsInCurrentContextAsync(CompletionContext? completionContext, TSyntaxContext context, int position, CompletionOptions options, CancellationToken cancellationToken); - protected abstract string GetFilterText(ISymbol symbol, string displayText, TSyntaxContext context); + protected abstract CompletionItemRules GetCompletionItemRules(ImmutableArray symbols, TSyntaxContext context); + protected abstract CompletionItemSelectionBehavior PreselectedItemSelectionBehavior { get; } + protected abstract bool IsInstrinsic(ISymbol symbol); + protected abstract bool IsTriggerOnDot(SyntaxToken token, int characterPosition); - protected sealed override async Task> GetSymbolsAsync( - CompletionContext? completionContext, TSyntaxContext context, int position, CompletionOptions options, CancellationToken cancellationToken) - { - var shouldProvideSymbols = await ShouldProvideAvailableSymbolsInCurrentContextAsync(completionContext, context, position, options, cancellationToken).ConfigureAwait(false); - if (!shouldProvideSymbols) - return []; + protected abstract string GetFilterText(ISymbol symbol, string displayText, TSyntaxContext context); - var recommendationOptions = options.ToRecommendationServiceOptions(); - var recommender = context.GetRequiredLanguageService(); - var recommendedSymbols = recommender.GetRecommendedSymbolsInContext(context, recommendationOptions, cancellationToken); + protected sealed override async Task> GetSymbolsAsync( + CompletionContext? completionContext, TSyntaxContext context, int position, CompletionOptions options, CancellationToken cancellationToken) + { + var shouldProvideSymbols = await ShouldProvideAvailableSymbolsInCurrentContextAsync(completionContext, context, position, options, cancellationToken).ConfigureAwait(false); + if (!shouldProvideSymbols) + return []; - if (context.IsTaskLikeTypeContext) - { - // If we get 'Task' back, attempt to preselect that as the most likely result. - var taskType = context.SemanticModel.Compilation.TaskType(); - return recommendedSymbols.NamedSymbols.SelectAsArray( - s => IsValidForTaskLikeTypeOnlyContext(s, context), - s => new SymbolAndSelectionInfo(Symbol: s, Preselect: s.OriginalDefinition.Equals(taskType))); - } - else if (context.IsGenericConstraintContext) - { - // Just filter valid symbols. Nothing to preselect - return recommendedSymbols.NamedSymbols.SelectAsArray(IsValidForGenericConstraintContext, s => new SymbolAndSelectionInfo(Symbol: s, Preselect: false)); - } - else - { - var shouldPreselectInferredTypes = await ShouldPreselectInferredTypesAsync(completionContext, position, options, cancellationToken).ConfigureAwait(false); - if (!shouldPreselectInferredTypes) - return recommendedSymbols.NamedSymbols.SelectAsArray(s => new SymbolAndSelectionInfo(Symbol: s, Preselect: false)); + var recommendationOptions = options.ToRecommendationServiceOptions(); + var recommender = context.GetRequiredLanguageService(); + var recommendedSymbols = recommender.GetRecommendedSymbolsInContext(context, recommendationOptions, cancellationToken); - var inferredTypes = context.InferredTypes.Where(t => t.SpecialType != SpecialType.System_Void).ToSet(); + if (context.IsTaskLikeTypeContext) + { + // If we get 'Task' back, attempt to preselect that as the most likely result. + var taskType = context.SemanticModel.Compilation.TaskType(); + return recommendedSymbols.NamedSymbols.SelectAsArray( + s => IsValidForTaskLikeTypeOnlyContext(s, context), + s => new SymbolAndSelectionInfo(Symbol: s, Preselect: s.OriginalDefinition.Equals(taskType))); + } + else if (context.IsGenericConstraintContext) + { + // Just filter valid symbols. Nothing to preselect + return recommendedSymbols.NamedSymbols.SelectAsArray(IsValidForGenericConstraintContext, s => new SymbolAndSelectionInfo(Symbol: s, Preselect: false)); + } + else + { + var shouldPreselectInferredTypes = await ShouldPreselectInferredTypesAsync(completionContext, position, options, cancellationToken).ConfigureAwait(false); + if (!shouldPreselectInferredTypes) + return recommendedSymbols.NamedSymbols.SelectAsArray(s => new SymbolAndSelectionInfo(Symbol: s, Preselect: false)); - using var _ = ArrayBuilder.GetInstance(out var result); + var inferredTypes = context.InferredTypes.Where(t => t.SpecialType != SpecialType.System_Void).ToSet(); - foreach (var symbol in recommendedSymbols.NamedSymbols) + return recommendedSymbols.NamedSymbols.SelectAsArray( + static (symbol, args) => { // Don't preselect intrinsic type symbols so we can preselect their keywords instead. We will also // ignore nullability for purposes of preselection -- if a method is returning a string? but we've // inferred we're assigning to a string or vice versa we'll still count those as the same. - var preselect = inferredTypes.Contains(GetSymbolType(symbol), SymbolEqualityComparer.Default) && !IsInstrinsic(symbol); - result.Add(new SymbolAndSelectionInfo(symbol, preselect)); - } + var preselect = args.inferredTypes.Contains(GetSymbolType(symbol), SymbolEqualityComparer.Default) && !args.self.IsInstrinsic(symbol); + return new SymbolAndSelectionInfo(symbol, preselect); + }, + (inferredTypes, self: this)); + } + } - return result.ToImmutable(); - } + private static bool IsValidForTaskLikeTypeOnlyContext(ISymbol symbol, TSyntaxContext context) + { + // We want to allow all namespaces as the user may be typing a namespace name to get to a task-like type. + if (symbol.IsNamespace()) + return true; + + if (symbol is not INamedTypeSymbol namedType || + symbol.IsDelegateType() || + namedType.IsEnumType()) + { + return false; } - private static bool IsValidForTaskLikeTypeOnlyContext(ISymbol symbol, TSyntaxContext context) + if (namedType.TypeKind == TypeKind.Interface) { - // We want to allow all namespaces as the user may be typing a namespace name to get to a task-like type. - if (symbol.IsNamespace()) - return true; + // The only interfaces, that are valid in async context are IAsyncEnumerable and IAsyncEnumerator. + // So if we are validating an interface, then we can just check for 2 of this possible variants + var compilation = context.SemanticModel.Compilation; + return namedType.Equals(compilation.IAsyncEnumerableOfTType()) || + namedType.Equals(compilation.IAsyncEnumeratorOfTType()); + } - if (symbol is not INamedTypeSymbol namedType || - symbol.IsDelegateType() || - namedType.IsEnumType()) - { - return false; - } + return namedType.IsAwaitableNonDynamic(context.SemanticModel, context.Position) || + namedType.GetTypeMembers().Any(static (m, context) => IsValidForTaskLikeTypeOnlyContext(m, context), context); + } - if (namedType.TypeKind == TypeKind.Interface) - { - // The only interfaces, that are valid in async context are IAsyncEnumerable and IAsyncEnumerator. - // So if we are validating an interface, then we can just check for 2 of this possible variants - var compilation = context.SemanticModel.Compilation; - return namedType.Equals(compilation.IAsyncEnumerableOfTType()) || - namedType.Equals(compilation.IAsyncEnumeratorOfTType()); - } + private static bool IsValidForGenericConstraintContext(ISymbol symbol) + { + if (symbol.IsNamespace() || + symbol.IsKind(SymbolKind.TypeParameter)) + { + return true; + } - return namedType.IsAwaitableNonDynamic(context.SemanticModel, context.Position) || - namedType.GetTypeMembers().Any(static (m, context) => IsValidForTaskLikeTypeOnlyContext(m, context), context); + if (symbol is not INamedTypeSymbol namedType || + symbol.IsDelegateType() || + namedType.IsEnumType()) + { + return false; } - private static bool IsValidForGenericConstraintContext(ISymbol symbol) + // If current symbol is a struct or static or sealed class then it cannot be used as a generic constraint. + // However it can contain other valid constraint types and if this is true we should show it + if (namedType.IsStructType() || namedType.IsStatic || namedType.IsSealed) { - if (symbol.IsNamespace() || - symbol.IsKind(SymbolKind.TypeParameter)) - { - return true; - } + return namedType.GetTypeMembers().Any(IsValidForGenericConstraintContext); + } - if (symbol is not INamedTypeSymbol namedType || - symbol.IsDelegateType() || - namedType.IsEnumType()) - { - return false; - } + return true; + } - // If current symbol is a struct or static or sealed class then it cannot be used as a generic constraint. - // However it can contain other valid constraint types and if this is true we should show it - if (namedType.IsStructType() || namedType.IsStatic || namedType.IsSealed) - { - return namedType.GetTypeMembers().Any(IsValidForGenericConstraintContext); - } + private static ITypeSymbol? GetSymbolType(ISymbol symbol) + => symbol is IMethodSymbol method ? method.ReturnType : symbol.GetSymbolType(); + + protected override CompletionItem CreateItem( + CompletionContext completionContext, + string displayText, + string displayTextSuffix, + string insertionText, + ImmutableArray symbols, + TSyntaxContext context, + SupportedPlatformData? supportedPlatformData) + { + var rules = GetCompletionItemRules(symbols, context); - return true; - } + var preselect = symbols.Any(static t => t.Preselect); + var matchPriority = preselect ? ComputeSymbolMatchPriority(symbols[0].Symbol) : MatchPriority.Default; + rules = rules.WithMatchPriority(matchPriority); - private static ITypeSymbol? GetSymbolType(ISymbol symbol) - => symbol is IMethodSymbol method ? method.ReturnType : symbol.GetSymbolType(); - - protected override CompletionItem CreateItem( - CompletionContext completionContext, - string displayText, - string displayTextSuffix, - string insertionText, - ImmutableArray symbols, - TSyntaxContext context, - SupportedPlatformData? supportedPlatformData) + if (ShouldSoftSelectInArgumentList(completionContext, context, preselect)) { - var rules = GetCompletionItemRules(symbols, context); + rules = rules.WithSelectionBehavior(CompletionItemSelectionBehavior.SoftSelection); + } + else if (context.IsRightSideOfNumericType) + { + rules = rules.WithSelectionBehavior(CompletionItemSelectionBehavior.SoftSelection); + } + else if (preselect) + { + rules = rules.WithSelectionBehavior(PreselectedItemSelectionBehavior); + } - var preselect = symbols.Any(static t => t.Preselect); - var matchPriority = preselect ? ComputeSymbolMatchPriority(symbols[0].Symbol) : MatchPriority.Default; - rules = rules.WithMatchPriority(matchPriority); + return SymbolCompletionItem.CreateWithNameAndKind( + displayText: displayText, + displayTextSuffix: displayTextSuffix, + symbols: symbols.SelectAsArray(t => t.Symbol), + rules: rules, + contextPosition: context.Position, + insertionText: insertionText, + filterText: GetFilterText(symbols[0].Symbol, displayText, context), + supportedPlatforms: supportedPlatformData); + } - if (ShouldSoftSelectInArgumentList(completionContext, context, preselect)) - { - rules = rules.WithSelectionBehavior(CompletionItemSelectionBehavior.SoftSelection); - } - else if (context.IsRightSideOfNumericType) - { - rules = rules.WithSelectionBehavior(CompletionItemSelectionBehavior.SoftSelection); - } - else if (preselect) - { - rules = rules.WithSelectionBehavior(PreselectedItemSelectionBehavior); - } + private static bool ShouldSoftSelectInArgumentList(CompletionContext completionContext, TSyntaxContext context, bool preselect) + { + return !preselect && + completionContext.Trigger.Kind == CompletionTriggerKind.Insertion && + context.IsOnArgumentListBracketOrComma && + IsArgumentListTriggerCharacter(completionContext.Trigger.Character); + } - return SymbolCompletionItem.CreateWithNameAndKind( - displayText: displayText, - displayTextSuffix: displayTextSuffix, - symbols: symbols.SelectAsArray(t => t.Symbol), - rules: rules, - contextPosition: context.Position, - insertionText: insertionText, - filterText: GetFilterText(symbols[0].Symbol, displayText, context), - supportedPlatforms: supportedPlatformData); - } + private static bool IsArgumentListTriggerCharacter(char character) + => character is ' ' or '(' or '['; - private static bool ShouldSoftSelectInArgumentList(CompletionContext completionContext, TSyntaxContext context, bool preselect) + private static int ComputeSymbolMatchPriority(ISymbol symbol) + { + if (symbol.MatchesKind(SymbolKind.Local, SymbolKind.Parameter, SymbolKind.RangeVariable)) { - return !preselect && - completionContext.Trigger.Kind == CompletionTriggerKind.Insertion && - context.IsOnArgumentListBracketOrComma && - IsArgumentListTriggerCharacter(completionContext.Trigger.Character); + return SymbolMatchPriority.PreferLocalOrParameterOrRangeVariable; } - private static bool IsArgumentListTriggerCharacter(char character) - => character is ' ' or '(' or '['; - - private static int ComputeSymbolMatchPriority(ISymbol symbol) + if (symbol.MatchesKind(SymbolKind.Field, SymbolKind.Property)) { - if (symbol.MatchesKind(SymbolKind.Local, SymbolKind.Parameter, SymbolKind.RangeVariable)) - { - return SymbolMatchPriority.PreferLocalOrParameterOrRangeVariable; - } - - if (symbol.MatchesKind(SymbolKind.Field, SymbolKind.Property)) - { - return SymbolMatchPriority.PreferFieldOrProperty; - } - - if (symbol.MatchesKind(SymbolKind.Event, SymbolKind.Method)) - { - return SymbolMatchPriority.PreferEventOrMethod; - } + return SymbolMatchPriority.PreferFieldOrProperty; + } - return SymbolMatchPriority.PreferType; + if (symbol.MatchesKind(SymbolKind.Event, SymbolKind.Method)) + { + return SymbolMatchPriority.PreferEventOrMethod; } - internal sealed override async Task GetDescriptionWorkerAsync( - Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + return SymbolMatchPriority.PreferType; + } + + internal sealed override async Task GetDescriptionWorkerAsync( + Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + { + var position = SymbolCompletionItem.GetContextPosition(item); + var name = SymbolCompletionItem.GetSymbolName(item); + var kind = SymbolCompletionItem.GetKind(item); + var isGeneric = SymbolCompletionItem.GetSymbolIsGeneric(item); + var relatedDocumentIds = document.Project.Solution.GetRelatedDocumentIds(document.Id); + var typeConvertibilityCache = new Dictionary(SymbolEqualityComparer.Default); + + foreach (var relatedId in relatedDocumentIds) { - var position = SymbolCompletionItem.GetContextPosition(item); - var name = SymbolCompletionItem.GetSymbolName(item); - var kind = SymbolCompletionItem.GetKind(item); - var isGeneric = SymbolCompletionItem.GetSymbolIsGeneric(item); - var relatedDocumentIds = document.Project.Solution.GetRelatedDocumentIds(document.Id); - var typeConvertibilityCache = new Dictionary(SymbolEqualityComparer.Default); - - foreach (var relatedId in relatedDocumentIds) + var relatedDocument = document.Project.Solution.GetRequiredDocument(relatedId); + var context = await Utilities.CreateSyntaxContextWithExistingSpeculativeModelAsync(relatedDocument, position, cancellationToken).ConfigureAwait(false) as TSyntaxContext; + Contract.ThrowIfNull(context); + var symbols = await TryGetSymbolsForContextAsync(completionContext: null, context, options, cancellationToken).ConfigureAwait(false); + + if (!symbols.IsDefault) { - var relatedDocument = document.Project.Solution.GetRequiredDocument(relatedId); - var context = await Utilities.CreateSyntaxContextWithExistingSpeculativeModelAsync(relatedDocument, position, cancellationToken).ConfigureAwait(false) as TSyntaxContext; - Contract.ThrowIfNull(context); - var symbols = await TryGetSymbolsForContextAsync(completionContext: null, context, options, cancellationToken).ConfigureAwait(false); + var bestSymbols = symbols.WhereAsArray(s => SymbolMatches(s, name, kind, isGeneric)); - if (!symbols.IsDefault) + if (bestSymbols.Any()) { - var bestSymbols = symbols.WhereAsArray(s => SymbolMatches(s, name, kind, isGeneric)); - - if (bestSymbols.Any()) + if (options.TargetTypedCompletionFilter && + TryFindFirstSymbolMatchesTargetTypes(_ => context, bestSymbols, typeConvertibilityCache, out var index) && index > 0) { - if (options.TargetTypedCompletionFilter && - TryFindFirstSymbolMatchesTargetTypes(_ => context, bestSymbols, typeConvertibilityCache, out var index) && index > 0) - { - // Since the first symbol is used to get the item description by default, - // this would ensure the displayed one matches target types (if there's any). - var firstMatch = bestSymbols[index]; - bestSymbols = bestSymbols.RemoveAt(index); - bestSymbols = bestSymbols.Insert(0, firstMatch); - } - - return await SymbolCompletionItem.GetDescriptionAsync(item, bestSymbols.SelectAsArray(t => t.Symbol), document, context.SemanticModel, displayOptions, cancellationToken).ConfigureAwait(false); + // Since the first symbol is used to get the item description by default, + // this would ensure the displayed one matches target types (if there's any). + var firstMatch = bestSymbols[index]; + bestSymbols = bestSymbols.RemoveAt(index); + bestSymbols = bestSymbols.Insert(0, firstMatch); } - } - } - return CompletionDescription.Empty; - - static bool SymbolMatches(SymbolAndSelectionInfo info, string? name, SymbolKind? kind, bool isGeneric) - { - return kind != null && info.Symbol.Kind == kind && info.Symbol.Name == name && isGeneric == info.Symbol.GetArity() > 0; + return await SymbolCompletionItem.GetDescriptionAsync(item, bestSymbols.SelectAsArray(t => t.Symbol), document, context.SemanticModel, displayOptions, cancellationToken).ConfigureAwait(false); + } } } - protected sealed override async Task IsSemanticTriggerCharacterAsync(Document document, int characterPosition, CancellationToken cancellationToken) + return CompletionDescription.Empty; + + static bool SymbolMatches(SymbolAndSelectionInfo info, string? name, SymbolKind? kind, bool isGeneric) { - var result = await IsTriggerOnDotAsync(document, characterPosition, cancellationToken).ConfigureAwait(false); - return result ?? true; + return kind != null && info.Symbol.Kind == kind && info.Symbol.Name == name && isGeneric == info.Symbol.GetArity() > 0; } + } - protected async Task IsTriggerOnDotAsync(Document document, int characterPosition, CancellationToken cancellationToken) - { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (text[characterPosition] != '.') - return null; + protected sealed override async Task IsSemanticTriggerCharacterAsync(Document document, int characterPosition, CancellationToken cancellationToken) + { + var result = await IsTriggerOnDotAsync(document, characterPosition, cancellationToken).ConfigureAwait(false); + return result ?? true; + } - // don't want to trigger after a number. All other cases after dot are ok. - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(characterPosition); + protected async Task IsTriggerOnDotAsync(Document document, int characterPosition, CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (text[characterPosition] != '.') + return null; - return IsTriggerOnDot(token, characterPosition); - } + // don't want to trigger after a number. All other cases after dot are ok. + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(characterPosition); + + return IsTriggerOnDot(token, characterPosition); } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractSuggestionModeCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractSuggestionModeCompletionProvider.cs index 0853a8bb756d8..eb66e6d6775d5 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractSuggestionModeCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractSuggestionModeCompletionProvider.cs @@ -8,21 +8,20 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractSuggestionModeCompletionProvider : LSPCompletionProvider { - internal abstract class AbstractSuggestionModeCompletionProvider : LSPCompletionProvider - { - protected abstract Task GetSuggestionModeItemAsync(Document document, int position, TextSpan span, CompletionTrigger triggerInfo, CancellationToken cancellationToken); + protected abstract Task GetSuggestionModeItemAsync(Document document, int position, TextSpan span, CompletionTrigger triggerInfo, CancellationToken cancellationToken); - public override async Task ProvideCompletionsAsync(CompletionContext context) - { - context.SuggestionModeItem = await GetSuggestionModeItemAsync( - context.Document, context.Position, context.CompletionListSpan, context.Trigger, context.CancellationToken).ConfigureAwait(false); - } + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + context.SuggestionModeItem = await GetSuggestionModeItemAsync( + context.Document, context.Position, context.CompletionListSpan, context.Trigger, context.CancellationToken).ConfigureAwait(false); + } - protected static CompletionItem CreateEmptySuggestionModeItem() - => CreateSuggestionModeItem(displayText: null, description: null); + protected static CompletionItem CreateEmptySuggestionModeItem() + => CreateSuggestionModeItem(displayText: null, description: null); - public override ImmutableHashSet TriggerCharacters => []; - } + public override ImmutableHashSet TriggerCharacters => []; } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.SymbolAndSelectionInfo.cs b/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.SymbolAndSelectionInfo.cs index f5d8321b18a84..116332b1a6d8d 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.SymbolAndSelectionInfo.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.SymbolAndSelectionInfo.cs @@ -5,19 +5,18 @@ using System; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract partial class AbstractSymbolCompletionProvider { - internal abstract partial class AbstractSymbolCompletionProvider + // The equality of this type is only used when we try to figure out missing symbols + // among linked files, therefore delegate to CompletionLinkedFilesSymbolEquivalenceComparer + protected readonly record struct SymbolAndSelectionInfo(ISymbol Symbol, bool Preselect) { - // The equality of this type is only used when we try to figure out missing symbols - // among linked files, therefore delegate to CompletionLinkedFilesSymbolEquivalenceComparer - protected readonly record struct SymbolAndSelectionInfo(ISymbol Symbol, bool Preselect) - { - public bool Equals(SymbolAndSelectionInfo other) - => LinkedFilesSymbolEquivalenceComparer.Instance.Equals(Symbol, other.Symbol); + public bool Equals(SymbolAndSelectionInfo other) + => LinkedFilesSymbolEquivalenceComparer.Instance.Equals(Symbol, other.Symbol); - public override int GetHashCode() - => LinkedFilesSymbolEquivalenceComparer.Instance.GetHashCode(Symbol); - } + public override int GetHashCode() + => LinkedFilesSymbolEquivalenceComparer.Instance.GetHashCode(Symbol); } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs index b034bdfb91a19..f3a276a0b9718 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs @@ -19,375 +19,374 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract partial class AbstractSymbolCompletionProvider : LSPCompletionProvider + where TSyntaxContext : SyntaxContext { - internal abstract partial class AbstractSymbolCompletionProvider : LSPCompletionProvider - where TSyntaxContext : SyntaxContext + protected AbstractSymbolCompletionProvider() + { + } + + protected abstract (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(ISymbol symbol, TSyntaxContext context); + + protected abstract Task> GetSymbolsAsync( + CompletionContext? completionContext, + TSyntaxContext syntaxContext, + int position, + CompletionOptions options, + CancellationToken cancellationToken); + + protected abstract CompletionItem CreateItem( + CompletionContext completionContext, + string displayText, + string displayTextSuffix, + string insertionText, + ImmutableArray symbols, + TSyntaxContext context, + SupportedPlatformData? supportedPlatformData); + + /// A cache to use for repeated lookups. This should be created with + /// because we ignore nullability. + private static bool ShouldIncludeInTargetTypedCompletionList( + ISymbol symbol, + ImmutableArray inferredTypes, + SemanticModel semanticModel, + int position, + Dictionary typeConvertibilityCache) { - protected AbstractSymbolCompletionProvider() + // When searching for identifiers of type C, exclude the symbol for the `C` type itself. + if (symbol.Kind == SymbolKind.NamedType) { + return false; } - protected abstract (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(ISymbol symbol, TSyntaxContext context); - - protected abstract Task> GetSymbolsAsync( - CompletionContext? completionContext, - TSyntaxContext syntaxContext, - int position, - CompletionOptions options, - CancellationToken cancellationToken); - - protected abstract CompletionItem CreateItem( - CompletionContext completionContext, - string displayText, - string displayTextSuffix, - string insertionText, - ImmutableArray symbols, - TSyntaxContext context, - SupportedPlatformData? supportedPlatformData); - - /// A cache to use for repeated lookups. This should be created with - /// because we ignore nullability. - private static bool ShouldIncludeInTargetTypedCompletionList( - ISymbol symbol, - ImmutableArray inferredTypes, - SemanticModel semanticModel, - int position, - Dictionary typeConvertibilityCache) + // Avoid offering members of object since they too commonly show up and are infrequently desired. + if (symbol.ContainingType?.SpecialType == SpecialType.System_Object) { - // When searching for identifiers of type C, exclude the symbol for the `C` type itself. - if (symbol.Kind == SymbolKind.NamedType) - { - return false; - } - - // Avoid offering members of object since they too commonly show up and are infrequently desired. - if (symbol.ContainingType?.SpecialType == SpecialType.System_Object) - { - return false; - } - - // Don't offer locals on the right-hand-side of their declaration: `int x = x` - if (symbol.Kind == SymbolKind.Local) - { - var local = (ILocalSymbol)symbol; - var declarationSyntax = symbol.DeclaringSyntaxReferences.Select(r => r.GetSyntax()).SingleOrDefault(); - if (declarationSyntax != null && position < declarationSyntax.FullSpan.End) - { - return false; - } - } + return false; + } - var type = symbol.GetMemberType() ?? symbol.GetSymbolType(); - if (type == null) + // Don't offer locals on the right-hand-side of their declaration: `int x = x` + if (symbol.Kind == SymbolKind.Local) + { + var local = (ILocalSymbol)symbol; + var declarationSyntax = symbol.DeclaringSyntaxReferences.Select(r => r.GetSyntax()).SingleOrDefault(); + if (declarationSyntax != null && position < declarationSyntax.FullSpan.End) { return false; } + } - if (typeConvertibilityCache.TryGetValue(type, out var isConvertible)) - { - return isConvertible; - } - - typeConvertibilityCache[type] = CompletionUtilities.IsTypeImplicitlyConvertible(semanticModel.Compilation, type, inferredTypes); - return typeConvertibilityCache[type]; + var type = symbol.GetMemberType() ?? symbol.GetSymbolType(); + if (type == null) + { + return false; } - /// - /// Given a list of symbols, and a mapping from each symbol to its original SemanticModel, - /// creates the list of completion items for them. - /// - private ImmutableArray CreateItems( - CompletionContext completionContext, - ImmutableArray symbols, - Func contextLookup, - Dictionary>? invalidProjectMap, - List? totalProjects) + if (typeConvertibilityCache.TryGetValue(type, out var isConvertible)) { - // We might get symbol w/o name but CanBeReferencedByName is still set to true, - // need to filter them out. - // https://github.com/dotnet/roslyn/issues/47690 - var symbolGroups = from symbol in symbols - let texts = GetDisplayAndSuffixAndInsertionText(symbol.Symbol, contextLookup(symbol)) - where !string.IsNullOrWhiteSpace(texts.displayText) - group symbol by texts into g - select g; - - using var _ = ArrayBuilder.GetInstance(out var itemListBuilder); - var typeConvertibilityCache = new Dictionary(SymbolEqualityComparer.Default); - - foreach (var symbolGroup in symbolGroups) - { - var includeItemInTargetTypedCompletion = false; - var arbitraryFirstContext = contextLookup(symbolGroup.First()); - var symbolList = symbolGroup.ToImmutableArray(); + return isConvertible; + } - if (completionContext.CompletionOptions.TargetTypedCompletionFilter) - { - includeItemInTargetTypedCompletion = TryFindFirstSymbolMatchesTargetTypes(contextLookup, symbolList, typeConvertibilityCache, out var index); - if (includeItemInTargetTypedCompletion && index > 0) - { - // This would ensure a symbol matches target types to be used for description if there's any, - // assuming the default implementation of GetDescriptionWorkerAsync is used. - var firstMatch = symbolList[index]; - symbolList = symbolList.RemoveAt(index); - symbolList = symbolList.Insert(0, firstMatch); - } - } + typeConvertibilityCache[type] = CompletionUtilities.IsTypeImplicitlyConvertible(semanticModel.Compilation, type, inferredTypes); + return typeConvertibilityCache[type]; + } - var supportedPlatformData = ComputeSupportedPlatformData(completionContext, symbolList, invalidProjectMap, totalProjects); - var item = CreateItem( - completionContext, symbolGroup.Key.displayText, symbolGroup.Key.suffix, symbolGroup.Key.insertionText, symbolList, arbitraryFirstContext, supportedPlatformData); + /// + /// Given a list of symbols, and a mapping from each symbol to its original SemanticModel, + /// creates the list of completion items for them. + /// + private ImmutableArray CreateItems( + CompletionContext completionContext, + ImmutableArray symbols, + Func contextLookup, + Dictionary>? invalidProjectMap, + List? totalProjects) + { + // We might get symbol w/o name but CanBeReferencedByName is still set to true, + // need to filter them out. + // https://github.com/dotnet/roslyn/issues/47690 + var symbolGroups = from symbol in symbols + let texts = GetDisplayAndSuffixAndInsertionText(symbol.Symbol, contextLookup(symbol)) + where !string.IsNullOrWhiteSpace(texts.displayText) + group symbol by texts into g + select g; + + using var _ = ArrayBuilder.GetInstance(out var itemListBuilder); + var typeConvertibilityCache = new Dictionary(SymbolEqualityComparer.Default); + + foreach (var symbolGroup in symbolGroups) + { + var includeItemInTargetTypedCompletion = false; + var arbitraryFirstContext = contextLookup(symbolGroup.First()); + var symbolList = symbolGroup.ToImmutableArray(); - if (includeItemInTargetTypedCompletion) + if (completionContext.CompletionOptions.TargetTypedCompletionFilter) + { + includeItemInTargetTypedCompletion = TryFindFirstSymbolMatchesTargetTypes(contextLookup, symbolList, typeConvertibilityCache, out var index); + if (includeItemInTargetTypedCompletion && index > 0) { - item = item.AddTag(WellKnownTags.TargetTypeMatch); + // This would ensure a symbol matches target types to be used for description if there's any, + // assuming the default implementation of GetDescriptionWorkerAsync is used. + var firstMatch = symbolList[index]; + symbolList = symbolList.RemoveAt(index); + symbolList = symbolList.Insert(0, firstMatch); } - - itemListBuilder.Add(item); } - return itemListBuilder.ToImmutable(); - } + var supportedPlatformData = ComputeSupportedPlatformData(completionContext, symbolList, invalidProjectMap, totalProjects); + var item = CreateItem( + completionContext, symbolGroup.Key.displayText, symbolGroup.Key.suffix, symbolGroup.Key.insertionText, symbolList, arbitraryFirstContext, supportedPlatformData); - protected static bool TryFindFirstSymbolMatchesTargetTypes( - Func contextLookup, - ImmutableArray symbolList, - Dictionary typeConvertibilityCache, - out int index) - { - for (index = 0; index < symbolList.Length; ++index) + if (includeItemInTargetTypedCompletion) { - var symbol = symbolList[index]; - var syntaxContext = contextLookup(symbol); - if (ShouldIncludeInTargetTypedCompletionList(symbol.Symbol, syntaxContext.InferredTypes, syntaxContext.SemanticModel, syntaxContext.Position, typeConvertibilityCache)) - break; + item = item.AddTag(WellKnownTags.TargetTypeMatch); } - return index < symbolList.Length; + itemListBuilder.Add(item); } - private static SupportedPlatformData? ComputeSupportedPlatformData(CompletionContext completionContext, ImmutableArray symbols, Dictionary>? invalidProjectMap, List? totalProjects) + return itemListBuilder.ToImmutable(); + } + + protected static bool TryFindFirstSymbolMatchesTargetTypes( + Func contextLookup, + ImmutableArray symbolList, + Dictionary typeConvertibilityCache, + out int index) + { + for (index = 0; index < symbolList.Length; ++index) { - SupportedPlatformData? supportedPlatformData = null; - if (invalidProjectMap != null) - { - List? invalidProjects = null; - foreach (var symbol in symbols) - { - if (invalidProjectMap.TryGetValue(symbol.Symbol, out invalidProjects)) - break; - } + var symbol = symbolList[index]; + var syntaxContext = contextLookup(symbol); + if (ShouldIncludeInTargetTypedCompletionList(symbol.Symbol, syntaxContext.InferredTypes, syntaxContext.SemanticModel, syntaxContext.Position, typeConvertibilityCache)) + break; + } + + return index < symbolList.Length; + } - if (invalidProjects != null) - supportedPlatformData = new SupportedPlatformData(completionContext.Document.Project.Solution, invalidProjects, totalProjects); + private static SupportedPlatformData? ComputeSupportedPlatformData(CompletionContext completionContext, ImmutableArray symbols, Dictionary>? invalidProjectMap, List? totalProjects) + { + SupportedPlatformData? supportedPlatformData = null; + if (invalidProjectMap != null) + { + List? invalidProjects = null; + foreach (var symbol in symbols) + { + if (invalidProjectMap.TryGetValue(symbol.Symbol, out invalidProjects)) + break; } - return supportedPlatformData; + if (invalidProjects != null) + supportedPlatformData = new SupportedPlatformData(completionContext.Document.Project.Solution, invalidProjects, totalProjects); } - protected static CompletionItem CreateItemDefault( - string displayText, - string displayTextSuffix, - string insertionText, - ImmutableArray symbols, - TSyntaxContext context, - SupportedPlatformData? supportedPlatformData) - { - var preselect = symbols.Any(static t => t.Preselect); - return SymbolCompletionItem.CreateWithSymbolId( - displayText: displayText, - displayTextSuffix: displayTextSuffix, - insertionText: insertionText, - filterText: GetFilterTextDefault(symbols[0].Symbol, displayText, context), - contextPosition: context.Position, - symbols: symbols.SelectAsArray(t => t.Symbol), - supportedPlatforms: supportedPlatformData, - rules: CompletionItemRules.Default - .WithMatchPriority(preselect ? MatchPriority.Preselect : MatchPriority.Default) - .WithSelectionBehavior(context.IsRightSideOfNumericType ? CompletionItemSelectionBehavior.SoftSelection : CompletionItemSelectionBehavior.Default)); - } + return supportedPlatformData; + } - protected static string GetFilterTextDefault(ISymbol symbol, string displayText, TSyntaxContext context) - { - return displayText == symbol.Name || displayText is ['@', ..] || (context.IsAttributeNameContext && symbol.IsAttribute()) - ? displayText - : symbol.Name; - } + protected static CompletionItem CreateItemDefault( + string displayText, + string displayTextSuffix, + string insertionText, + ImmutableArray symbols, + TSyntaxContext context, + SupportedPlatformData? supportedPlatformData) + { + var preselect = symbols.Any(static t => t.Preselect); + return SymbolCompletionItem.CreateWithSymbolId( + displayText: displayText, + displayTextSuffix: displayTextSuffix, + insertionText: insertionText, + filterText: GetFilterTextDefault(symbols[0].Symbol, displayText, context), + contextPosition: context.Position, + symbols: symbols.SelectAsArray(t => t.Symbol), + supportedPlatforms: supportedPlatformData, + rules: CompletionItemRules.Default + .WithMatchPriority(preselect ? MatchPriority.Preselect : MatchPriority.Default) + .WithSelectionBehavior(context.IsRightSideOfNumericType ? CompletionItemSelectionBehavior.SoftSelection : CompletionItemSelectionBehavior.Default)); + } + + protected static string GetFilterTextDefault(ISymbol symbol, string displayText, TSyntaxContext context) + { + return displayText == symbol.Name || displayText is ['@', ..] || (context.IsAttributeNameContext && symbol.IsAttribute()) + ? displayText + : symbol.Name; + } - internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); + internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken); - public override async Task ProvideCompletionsAsync(CompletionContext completionContext) + public override async Task ProvideCompletionsAsync(CompletionContext completionContext) + { + try { - try + var document = completionContext.Document; + var position = completionContext.Position; + var options = completionContext.CompletionOptions; + var cancellationToken = completionContext.CancellationToken; + + // If we were triggered by typing a character, then do a semantic check to make sure + // we're still applicable. If not, then return immediately. + if (completionContext.Trigger.Kind == CompletionTriggerKind.Insertion) { - var document = completionContext.Document; - var position = completionContext.Position; - var options = completionContext.CompletionOptions; - var cancellationToken = completionContext.CancellationToken; - - // If we were triggered by typing a character, then do a semantic check to make sure - // we're still applicable. If not, then return immediately. - if (completionContext.Trigger.Kind == CompletionTriggerKind.Insertion) + var isSemanticTriggerCharacter = await IsSemanticTriggerCharacterAsync(document, position - 1, cancellationToken).ConfigureAwait(false); + if (!isSemanticTriggerCharacter) { - var isSemanticTriggerCharacter = await IsSemanticTriggerCharacterAsync(document, position - 1, cancellationToken).ConfigureAwait(false); - if (!isSemanticTriggerCharacter) - { - return; - } + return; } + } - completionContext.IsExclusive = IsExclusive(); - - using (Logger.LogBlock(FunctionId.Completion_SymbolCompletionProvider_GetItemsWorker, cancellationToken)) - { - var syntaxContext = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false) as TSyntaxContext; - Contract.ThrowIfNull(syntaxContext); + completionContext.IsExclusive = IsExclusive(); - var regularItems = await GetItemsAsync(completionContext, syntaxContext, document, position, options, cancellationToken).ConfigureAwait(false); - completionContext.AddItems(regularItems); - } - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) + using (Logger.LogBlock(FunctionId.Completion_SymbolCompletionProvider_GetItemsWorker, cancellationToken)) { - // nop + var syntaxContext = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false) as TSyntaxContext; + Contract.ThrowIfNull(syntaxContext); + + var regularItems = await GetItemsAsync(completionContext, syntaxContext, document, position, options, cancellationToken).ConfigureAwait(false); + completionContext.AddItems(regularItems); } } - - private async Task> GetItemsAsync( - CompletionContext completionContext, - TSyntaxContext syntaxContext, - Document document, - int position, - CompletionOptions options, - CancellationToken cancellationToken) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) { - var relatedDocumentIds = document.GetLinkedDocumentIds(); - - if (relatedDocumentIds.IsEmpty) - { - var itemsForCurrentDocument = await GetSymbolsAsync(completionContext, syntaxContext, position, options, cancellationToken).ConfigureAwait(false); - return CreateItems(completionContext, itemsForCurrentDocument, _ => syntaxContext, invalidProjectMap: null, totalProjects: null); - } + // nop + } + } - var contextAndSymbolLists = await GetPerContextSymbolsAsync(completionContext, document, options, new[] { document.Id }.Concat(relatedDocumentIds), cancellationToken).ConfigureAwait(false); - var symbolToContextMap = UnionSymbols(contextAndSymbolLists); - var missingSymbolsMap = FindSymbolsMissingInLinkedContexts(symbolToContextMap, contextAndSymbolLists); - var totalProjects = contextAndSymbolLists.Select(t => t.documentId.ProjectId).ToList(); + private async Task> GetItemsAsync( + CompletionContext completionContext, + TSyntaxContext syntaxContext, + Document document, + int position, + CompletionOptions options, + CancellationToken cancellationToken) + { + var relatedDocumentIds = document.GetLinkedDocumentIds(); - return CreateItems( - completionContext, symbolToContextMap.Keys.ToImmutableArray(), symbol => symbolToContextMap[symbol], missingSymbolsMap, totalProjects); + if (relatedDocumentIds.IsEmpty) + { + var itemsForCurrentDocument = await GetSymbolsAsync(completionContext, syntaxContext, position, options, cancellationToken).ConfigureAwait(false); + return CreateItems(completionContext, itemsForCurrentDocument, _ => syntaxContext, invalidProjectMap: null, totalProjects: null); } - protected virtual bool IsExclusive() - => false; + var contextAndSymbolLists = await GetPerContextSymbolsAsync(completionContext, document, options, new[] { document.Id }.Concat(relatedDocumentIds), cancellationToken).ConfigureAwait(false); + var symbolToContextMap = UnionSymbols(contextAndSymbolLists); + var missingSymbolsMap = FindSymbolsMissingInLinkedContexts(symbolToContextMap, contextAndSymbolLists); + var totalProjects = contextAndSymbolLists.Select(t => t.documentId.ProjectId).ToList(); - protected virtual Task IsSemanticTriggerCharacterAsync(Document document, int characterPosition, CancellationToken cancellationToken) - => SpecializedTasks.True; + return CreateItems( + completionContext, symbolToContextMap.Keys.ToImmutableArray(), symbol => symbolToContextMap[symbol], missingSymbolsMap, totalProjects); + } - private static Dictionary UnionSymbols( - ImmutableArray<(DocumentId documentId, TSyntaxContext syntaxContext, ImmutableArray symbols)> linkedContextSymbolLists) - { - // To correctly map symbols back to their SyntaxContext, we do care about assembly identity. - // We don't care about assembly identity when creating the union. - var result = new Dictionary(); - foreach (var (documentId, syntaxContext, symbols) in linkedContextSymbolLists) - { - // We need to use the SemanticModel any particular symbol came from in order to generate its description correctly. - // Therefore, when we add a symbol to set of union symbols, add a mapping from it to its SyntaxContext. - foreach (var symbol in symbols.GroupBy(s => new { s.Symbol.Name, s.Symbol.Kind }).Select(g => g.First())) - { - if (!result.ContainsKey(symbol)) - result.Add(symbol, syntaxContext); - } - } + protected virtual bool IsExclusive() + => false; - return result; - } + protected virtual Task IsSemanticTriggerCharacterAsync(Document document, int characterPosition, CancellationToken cancellationToken) + => SpecializedTasks.True; - private async Task symbols)>> GetPerContextSymbolsAsync( - CompletionContext completionContext, Document document, CompletionOptions options, IEnumerable relatedDocuments, CancellationToken cancellationToken) + private static Dictionary UnionSymbols( + ImmutableArray<(DocumentId documentId, TSyntaxContext syntaxContext, ImmutableArray symbols)> linkedContextSymbolLists) + { + // To correctly map symbols back to their SyntaxContext, we do care about assembly identity. + // We don't care about assembly identity when creating the union. + var result = new Dictionary(); + foreach (var (documentId, syntaxContext, symbols) in linkedContextSymbolLists) { - var solution = document.Project.Solution; - - using var _1 = ArrayBuilder symbols)>>.GetInstance(out var tasks); - using var _2 = ArrayBuilder<(DocumentId documentId, TSyntaxContext syntaxContext, ImmutableArray symbols)>.GetInstance(out var perContextSymbols); - - foreach (var relatedDocumentId in relatedDocuments) + // We need to use the SemanticModel any particular symbol came from in order to generate its description correctly. + // Therefore, when we add a symbol to set of union symbols, add a mapping from it to its SyntaxContext. + foreach (var symbol in symbols.GroupBy(s => new { s.Symbol.Name, s.Symbol.Kind }).Select(g => g.First())) { - tasks.Add(Task.Run(async () => - { - var relatedDocument = solution.GetRequiredDocument(relatedDocumentId); - var syntaxContext = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(relatedDocument, cancellationToken).ConfigureAwait(false) as TSyntaxContext; + if (!result.ContainsKey(symbol)) + result.Add(symbol, syntaxContext); + } + } - Contract.ThrowIfNull(syntaxContext); - var symbols = await TryGetSymbolsForContextAsync(completionContext, syntaxContext, options, cancellationToken).ConfigureAwait(false); + return result; + } - return (relatedDocument.Id, syntaxContext, symbols); - }, cancellationToken)); - } + private async Task symbols)>> GetPerContextSymbolsAsync( + CompletionContext completionContext, Document document, CompletionOptions options, IEnumerable relatedDocuments, CancellationToken cancellationToken) + { + var solution = document.Project.Solution; - await Task.WhenAll(tasks).ConfigureAwait(false); + using var _1 = ArrayBuilder symbols)>>.GetInstance(out var tasks); + using var _2 = ArrayBuilder<(DocumentId documentId, TSyntaxContext syntaxContext, ImmutableArray symbols)>.GetInstance(out var perContextSymbols); - foreach (var task in tasks) + foreach (var relatedDocumentId in relatedDocuments) + { + tasks.Add(Task.Run(async () => { - var (relatedDocumentId, syntaxContext, symbols) = await task.ConfigureAwait(false); - if (!symbols.IsDefault) - perContextSymbols.Add((relatedDocumentId, syntaxContext, symbols)); - } + var relatedDocument = solution.GetRequiredDocument(relatedDocumentId); + var syntaxContext = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(relatedDocument, cancellationToken).ConfigureAwait(false) as TSyntaxContext; - return perContextSymbols.ToImmutable(); - } + Contract.ThrowIfNull(syntaxContext); + var symbols = await TryGetSymbolsForContextAsync(completionContext, syntaxContext, options, cancellationToken).ConfigureAwait(false); - /// - /// If current context is in active region, returns available symbols. Otherwise, returns null. - /// - protected async Task> TryGetSymbolsForContextAsync( - CompletionContext? completionContext, TSyntaxContext syntaxContext, CompletionOptions options, CancellationToken cancellationToken) - { - var syntaxFacts = syntaxContext.GetRequiredLanguageService(); - return syntaxFacts.IsInInactiveRegion(syntaxContext.SyntaxTree, syntaxContext.Position, cancellationToken) - ? default - : await GetSymbolsAsync(completionContext, syntaxContext, syntaxContext.Position, options, cancellationToken).ConfigureAwait(false); + return (relatedDocument.Id, syntaxContext, symbols); + }, cancellationToken)); } - /// - /// Given a list of symbols, determine which are not recommended at the same position in linked documents. - /// - /// The symbols recommended in the active context. - /// The symbols recommended in linked documents - /// The list of projects each recommended symbol did NOT appear in. - private static Dictionary> FindSymbolsMissingInLinkedContexts( - Dictionary symbolToContext, - ImmutableArray<(DocumentId documentId, TSyntaxContext syntaxContext, ImmutableArray symbols)> linkedContextSymbolLists) + await Task.WhenAll(tasks).ConfigureAwait(false); + + foreach (var task in tasks) { - var missingSymbols = new Dictionary>(LinkedFilesSymbolEquivalenceComparer.Instance); + var (relatedDocumentId, syntaxContext, symbols) = await task.ConfigureAwait(false); + if (!symbols.IsDefault) + perContextSymbols.Add((relatedDocumentId, syntaxContext, symbols)); + } - foreach (var (documentId, syntaxContext, symbols) in linkedContextSymbolLists) - { - var symbolsMissingInLinkedContext = symbolToContext.Keys.Except(symbols); - foreach (var (symbol, _) in symbolsMissingInLinkedContext) - missingSymbols.GetOrAdd(symbol, m => []).Add(documentId.ProjectId); - } + return perContextSymbols.ToImmutable(); + } - return missingSymbols; - } + /// + /// If current context is in active region, returns available symbols. Otherwise, returns null. + /// + protected async Task> TryGetSymbolsForContextAsync( + CompletionContext? completionContext, TSyntaxContext syntaxContext, CompletionOptions options, CancellationToken cancellationToken) + { + var syntaxFacts = syntaxContext.GetRequiredLanguageService(); + return syntaxFacts.IsInInactiveRegion(syntaxContext.SyntaxTree, syntaxContext.Position, cancellationToken) + ? default + : await GetSymbolsAsync(completionContext, syntaxContext, syntaxContext.Position, options, cancellationToken).ConfigureAwait(false); + } - public sealed override Task GetTextChangeAsync(Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) - => Task.FromResult(new TextChange(selectedItem.Span, GetInsertionText(selectedItem, ch))); + /// + /// Given a list of symbols, determine which are not recommended at the same position in linked documents. + /// + /// The symbols recommended in the active context. + /// The symbols recommended in linked documents + /// The list of projects each recommended symbol did NOT appear in. + private static Dictionary> FindSymbolsMissingInLinkedContexts( + Dictionary symbolToContext, + ImmutableArray<(DocumentId documentId, TSyntaxContext syntaxContext, ImmutableArray symbols)> linkedContextSymbolLists) + { + var missingSymbols = new Dictionary>(LinkedFilesSymbolEquivalenceComparer.Instance); - private string GetInsertionText(CompletionItem item, char? ch) + foreach (var (documentId, syntaxContext, symbols) in linkedContextSymbolLists) { - return ch == null - ? SymbolCompletionItem.GetInsertionText(item) - : GetInsertionText(item, ch.Value); + var symbolsMissingInLinkedContext = symbolToContext.Keys.Except(symbols); + foreach (var (symbol, _) in symbolsMissingInLinkedContext) + missingSymbols.GetOrAdd(symbol, m => []).Add(documentId.ProjectId); } - /// - /// Override this if you want to provide customized insertion based on the character typed. - /// - protected virtual string GetInsertionText(CompletionItem item, char ch) - => SymbolCompletionItem.GetInsertionText(item); + return missingSymbols; + } + + public sealed override Task GetTextChangeAsync(Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) + => Task.FromResult(new TextChange(selectedItem.Span, GetInsertionText(selectedItem, ch))); + + private string GetInsertionText(CompletionItem item, char? ch) + { + return ch == null + ? SymbolCompletionItem.GetInsertionText(item) + : GetInsertionText(item, ch.Value); } + + /// + /// Override this if you want to provide customized insertion based on the character typed. + /// + protected virtual string GetInsertionText(CompletionItem item, char ch) + => SymbolCompletionItem.GetInsertionText(item); } diff --git a/src/Features/Core/Portable/Completion/Providers/CompletionUtilities.cs b/src/Features/Core/Portable/Completion/Providers/CompletionUtilities.cs index 6581517ea7911..af9194e781f6f 100644 --- a/src/Features/Core/Portable/Completion/Providers/CompletionUtilities.cs +++ b/src/Features/Core/Portable/Completion/Providers/CompletionUtilities.cs @@ -8,44 +8,43 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal static class CompletionUtilities { - internal static class CompletionUtilities + public static bool IsTypeImplicitlyConvertible(Compilation compilation, ITypeSymbol sourceType, ImmutableArray targetTypes) { - public static bool IsTypeImplicitlyConvertible(Compilation compilation, ITypeSymbol sourceType, ImmutableArray targetTypes) + foreach (var targetType in targetTypes) { - foreach (var targetType in targetTypes) + if (compilation.ClassifyCommonConversion(sourceType, targetType).IsImplicit) { - if (compilation.ClassifyCommonConversion(sourceType, targetType).IsImplicit) - { - return true; - } + return true; } - - return false; } - public static ImmutableArray GetDistinctProjectsFromLatestSolutionSnapshot(ImmutableSegmentedList projects) - { - if (projects.IsEmpty) - return []; + return false; + } - Solution? solution = null; - using var _ = PooledHashSet.GetInstance(out var projectIds); + public static ImmutableArray GetDistinctProjectsFromLatestSolutionSnapshot(ImmutableSegmentedList projects) + { + if (projects.IsEmpty) + return []; - // Use WorkspaceVersion to decide which solution snapshot is latest among projects in list. - // Dedupe and return corresponding projects from this snapshot. - foreach (var project in projects) + Solution? solution = null; + using var _ = PooledHashSet.GetInstance(out var projectIds); + + // Use WorkspaceVersion to decide which solution snapshot is latest among projects in list. + // Dedupe and return corresponding projects from this snapshot. + foreach (var project in projects) + { + projectIds.Add(project.Id); + if (solution is null || project.Solution.WorkspaceVersion > solution.WorkspaceVersion) { - projectIds.Add(project.Id); - if (solution is null || project.Solution.WorkspaceVersion > solution.WorkspaceVersion) - { - solution = project.Solution; - } + solution = project.Solution; } - - Contract.ThrowIfNull(solution); - return projectIds.Select(solution.GetProject).WhereNotNull().ToImmutableArray(); } + + Contract.ThrowIfNull(solution); + return projectIds.Select(solution.GetProject).WhereNotNull().ToImmutableArray(); } } diff --git a/src/Features/Core/Portable/Completion/Providers/EmbeddedLanguageCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/EmbeddedLanguageCompletionProvider.cs index 612efdc2b906c..cb565cec92f72 100644 --- a/src/Features/Core/Portable/Completion/Providers/EmbeddedLanguageCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/EmbeddedLanguageCompletionProvider.cs @@ -8,21 +8,20 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Completion.Providers -{ - internal abstract class EmbeddedLanguageCompletionProvider - { - public string Name { get; } +namespace Microsoft.CodeAnalysis.Completion.Providers; - internal EmbeddedLanguageCompletionProvider() - { - Name = GetType().FullName!; - } +internal abstract class EmbeddedLanguageCompletionProvider +{ + public string Name { get; } - public abstract ImmutableHashSet TriggerCharacters { get; } - public abstract bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger); - public abstract Task ProvideCompletionsAsync(CompletionContext context); - public abstract Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken); - public abstract Task GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken); + internal EmbeddedLanguageCompletionProvider() + { + Name = GetType().FullName!; } + + public abstract ImmutableHashSet TriggerCharacters { get; } + public abstract bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger); + public abstract Task ProvideCompletionsAsync(CompletionContext context); + public abstract Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken); + public abstract Task GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/Completion/Providers/IKeywordRecommender.cs b/src/Features/Core/Portable/Completion/Providers/IKeywordRecommender.cs index dec8955c35379..c64763082b8b7 100644 --- a/src/Features/Core/Portable/Completion/Providers/IKeywordRecommender.cs +++ b/src/Features/Core/Portable/Completion/Providers/IKeywordRecommender.cs @@ -6,11 +6,10 @@ using System.Threading; using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal interface IKeywordRecommender + where TContext : SyntaxContext { - internal interface IKeywordRecommender - where TContext : SyntaxContext - { - ImmutableArray RecommendKeywords(int position, TContext context, CancellationToken cancellationToken); - } + ImmutableArray RecommendKeywords(int position, TContext context, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractExtensionMethodImportCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractExtensionMethodImportCompletionProvider.cs index 0e89e146de30a..153654404b600 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractExtensionMethodImportCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractExtensionMethodImportCompletionProvider.cs @@ -14,113 +14,112 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractExtensionMethodImportCompletionProvider : AbstractImportCompletionProvider { - internal abstract class AbstractExtensionMethodImportCompletionProvider : AbstractImportCompletionProvider - { - protected abstract string GenericSuffix { get; } + protected abstract string GenericSuffix { get; } - // Don't provide unimported extension methods if adding import is not supported, - // since we are current incapable of making a change using its fully qualify form. - protected override bool ShouldProvideCompletion(CompletionContext completionContext, SyntaxContext syntaxContext) - => syntaxContext.IsRightOfNameSeparator && IsAddingImportsSupported(completionContext.Document, completionContext.CompletionOptions); + // Don't provide unimported extension methods if adding import is not supported, + // since we are current incapable of making a change using its fully qualify form. + protected override bool ShouldProvideCompletion(CompletionContext completionContext, SyntaxContext syntaxContext) + => syntaxContext.IsRightOfNameSeparator && IsAddingImportsSupported(completionContext.Document, completionContext.CompletionOptions); - protected override void LogCommit() - => CompletionProvidersLogger.LogCommitOfExtensionMethodImportCompletionItem(); + protected override void LogCommit() + => CompletionProvidersLogger.LogCommitOfExtensionMethodImportCompletionItem(); - protected override void WarmUpCacheInBackground(Document document) - { - _ = ExtensionMethodImportCompletionHelper.WarmUpCacheAsync(document.Project, CancellationToken.None); - } + protected override void WarmUpCacheInBackground(Document document) + { + _ = ExtensionMethodImportCompletionHelper.WarmUpCacheAsync(document.Project, CancellationToken.None); + } - protected override async Task AddCompletionItemsAsync( - CompletionContext completionContext, - SyntaxContext syntaxContext, - HashSet namespaceInScope, - CancellationToken cancellationToken) + protected override async Task AddCompletionItemsAsync( + CompletionContext completionContext, + SyntaxContext syntaxContext, + HashSet namespaceInScope, + CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.Completion_ExtensionMethodImportCompletionProvider_GetCompletionItemsAsync, cancellationToken)) { - using (Logger.LogBlock(FunctionId.Completion_ExtensionMethodImportCompletionProvider_GetCompletionItemsAsync, cancellationToken)) + var syntaxFacts = completionContext.Document.GetRequiredLanguageService(); + if (TryGetReceiverTypeSymbol(syntaxContext, syntaxFacts, cancellationToken, out var receiverTypeSymbol)) { - var syntaxFacts = completionContext.Document.GetRequiredLanguageService(); - if (TryGetReceiverTypeSymbol(syntaxContext, syntaxFacts, cancellationToken, out var receiverTypeSymbol)) - { - var inferredTypes = completionContext.CompletionOptions.TargetTypedCompletionFilter - ? syntaxContext.InferredTypes - : []; - - var result = await ExtensionMethodImportCompletionHelper.GetUnimportedExtensionMethodsAsync( - syntaxContext, - receiverTypeSymbol, - namespaceInScope, - inferredTypes, - forceCacheCreation: completionContext.CompletionOptions.ForceExpandedCompletionIndexCreation, - hideAdvancedMembers: completionContext.CompletionOptions.HideAdvancedMembers, - cancellationToken).ConfigureAwait(false); - - if (result is not null) - { - var receiverTypeKey = SymbolKey.CreateString(receiverTypeSymbol, cancellationToken); - completionContext.AddItems(result.CompletionItems.Select(i => Convert(i, receiverTypeKey))); - } + var inferredTypes = completionContext.CompletionOptions.TargetTypedCompletionFilter + ? syntaxContext.InferredTypes + : []; + + var result = await ExtensionMethodImportCompletionHelper.GetUnimportedExtensionMethodsAsync( + syntaxContext, + receiverTypeSymbol, + namespaceInScope, + inferredTypes, + forceCacheCreation: completionContext.CompletionOptions.ForceExpandedCompletionIndexCreation, + hideAdvancedMembers: completionContext.CompletionOptions.HideAdvancedMembers, + cancellationToken).ConfigureAwait(false); + + if (result is not null) + { + var receiverTypeKey = SymbolKey.CreateString(receiverTypeSymbol, cancellationToken); + completionContext.AddItems(result.CompletionItems.Select(i => Convert(i, receiverTypeKey))); } } } + } - private static bool TryGetReceiverTypeSymbol( - SyntaxContext syntaxContext, - ISyntaxFactsService syntaxFacts, - CancellationToken cancellationToken, - [NotNullWhen(true)] out ITypeSymbol? receiverTypeSymbol) - { - var parentNode = syntaxContext.TargetToken.Parent; + private static bool TryGetReceiverTypeSymbol( + SyntaxContext syntaxContext, + ISyntaxFactsService syntaxFacts, + CancellationToken cancellationToken, + [NotNullWhen(true)] out ITypeSymbol? receiverTypeSymbol) + { + var parentNode = syntaxContext.TargetToken.Parent; - // Even though implicit access to extension method is allowed, we decide not support it for simplicity - // e.g. we will not provide completion for unimported extension method in this case - // New Bar() {.X = .$$ } - var expressionNode = syntaxFacts.GetLeftSideOfDot(parentNode, allowImplicitTarget: false); + // Even though implicit access to extension method is allowed, we decide not support it for simplicity + // e.g. we will not provide completion for unimported extension method in this case + // New Bar() {.X = .$$ } + var expressionNode = syntaxFacts.GetLeftSideOfDot(parentNode, allowImplicitTarget: false); - if (expressionNode != null) + if (expressionNode != null) + { + // Check if we are accessing members of a type, no extension methods are exposed off of types. + if (syntaxContext.SemanticModel.GetSymbolInfo(expressionNode, cancellationToken).GetAnySymbol() is not ITypeSymbol) { - // Check if we are accessing members of a type, no extension methods are exposed off of types. - if (syntaxContext.SemanticModel.GetSymbolInfo(expressionNode, cancellationToken).GetAnySymbol() is not ITypeSymbol) + // The expression we're calling off of needs to have an actual instance type. + // We try to be more tolerant to errors here so completion would still be available in certain case of partially typed code. + receiverTypeSymbol = syntaxContext.SemanticModel.GetTypeInfo(expressionNode, cancellationToken).Type; + if (receiverTypeSymbol is IErrorTypeSymbol errorTypeSymbol) { - // The expression we're calling off of needs to have an actual instance type. - // We try to be more tolerant to errors here so completion would still be available in certain case of partially typed code. - receiverTypeSymbol = syntaxContext.SemanticModel.GetTypeInfo(expressionNode, cancellationToken).Type; - if (receiverTypeSymbol is IErrorTypeSymbol errorTypeSymbol) - { - receiverTypeSymbol = errorTypeSymbol.CandidateSymbols.Select(GetSymbolType).FirstOrDefault(s => s != null); - } - - return receiverTypeSymbol != null; + receiverTypeSymbol = errorTypeSymbol.CandidateSymbols.Select(GetSymbolType).FirstOrDefault(s => s != null); } - } - receiverTypeSymbol = null; - return false; + return receiverTypeSymbol != null; + } } - private static ITypeSymbol? GetSymbolType(ISymbol symbol) - => symbol switch - { - ILocalSymbol localSymbol => localSymbol.Type, - IFieldSymbol fieldSymbol => fieldSymbol.Type, - IPropertySymbol propertySymbol => propertySymbol.Type, - IParameterSymbol parameterSymbol => parameterSymbol.Type, - IAliasSymbol aliasSymbol => aliasSymbol.Target as ITypeSymbol, - _ => symbol as ITypeSymbol, - }; - - private CompletionItem Convert(SerializableImportCompletionItem serializableItem, string receiverTypeSymbolKey) - => ImportCompletionItem.Create( - serializableItem.Name, - serializableItem.Arity, - serializableItem.ContainingNamespace, - serializableItem.Glyph, - GenericSuffix, - CompletionItemFlags.Expanded, - (serializableItem.SymbolKeyData, receiverTypeSymbolKey, serializableItem.AdditionalOverloadCount), - serializableItem.IncludedInTargetTypeCompletion); + receiverTypeSymbol = null; + return false; } + + private static ITypeSymbol? GetSymbolType(ISymbol symbol) + => symbol switch + { + ILocalSymbol localSymbol => localSymbol.Type, + IFieldSymbol fieldSymbol => fieldSymbol.Type, + IPropertySymbol propertySymbol => propertySymbol.Type, + IParameterSymbol parameterSymbol => parameterSymbol.Type, + IAliasSymbol aliasSymbol => aliasSymbol.Target as ITypeSymbol, + _ => symbol as ITypeSymbol, + }; + + private CompletionItem Convert(SerializableImportCompletionItem serializableItem, string receiverTypeSymbolKey) + => ImportCompletionItem.Create( + serializableItem.Name, + serializableItem.Arity, + serializableItem.ContainingNamespace, + serializableItem.Glyph, + GenericSuffix, + CompletionItemFlags.Expanded, + (serializableItem.SymbolKeyData, receiverTypeSymbolKey, serializableItem.AdditionalOverloadCount), + serializableItem.IncludedInTargetTypeCompletion); } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionCacheServiceFactory.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionCacheServiceFactory.cs index f3a9aa51e4fa5..580e9a849c32a 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionCacheServiceFactory.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionCacheServiceFactory.cs @@ -13,66 +13,65 @@ using Microsoft.CodeAnalysis.Shared.TestHooks; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractImportCompletionCacheServiceFactory : IWorkspaceServiceFactory { - internal abstract class AbstractImportCompletionCacheServiceFactory : IWorkspaceServiceFactory - { - private readonly ConcurrentDictionary _peItemsCache = []; + private readonly ConcurrentDictionary _peItemsCache = []; - private readonly ConcurrentDictionary _projectItemsCache = []; + private readonly ConcurrentDictionary _projectItemsCache = []; - private readonly IAsynchronousOperationListenerProvider _listenerProvider; - private readonly Func, CancellationToken, ValueTask> _processBatchAsync; - private readonly CancellationToken _disposalToken; + private readonly IAsynchronousOperationListenerProvider _listenerProvider; + private readonly Func, CancellationToken, ValueTask> _processBatchAsync; + private readonly CancellationToken _disposalToken; - protected AbstractImportCompletionCacheServiceFactory( - IAsynchronousOperationListenerProvider listenerProvider, - Func, CancellationToken, ValueTask> processBatchAsync - , CancellationToken disposalToken) - { - _listenerProvider = listenerProvider; - _processBatchAsync = processBatchAsync; - _disposalToken = disposalToken; - } + protected AbstractImportCompletionCacheServiceFactory( + IAsynchronousOperationListenerProvider listenerProvider, + Func, CancellationToken, ValueTask> processBatchAsync + , CancellationToken disposalToken) + { + _listenerProvider = listenerProvider; + _processBatchAsync = processBatchAsync; + _disposalToken = disposalToken; + } - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + var workspace = workspaceServices.Workspace; + if (workspace.Kind == WorkspaceKind.Host) { - var workspace = workspaceServices.Workspace; - if (workspace.Kind == WorkspaceKind.Host) + var cacheService = workspaceServices.GetService(); + if (cacheService != null) { - var cacheService = workspaceServices.GetService(); - if (cacheService != null) - { - cacheService.CacheFlushRequested += OnCacheFlushRequested; - } + cacheService.CacheFlushRequested += OnCacheFlushRequested; } + } - var workQueue = new AsyncBatchingWorkQueue( - TimeSpan.FromSeconds(1), - _processBatchAsync, - _listenerProvider.GetListener(FeatureAttribute.CompletionSet), - _disposalToken); + var workQueue = new AsyncBatchingWorkQueue( + TimeSpan.FromSeconds(1), + _processBatchAsync, + _listenerProvider.GetListener(FeatureAttribute.CompletionSet), + _disposalToken); - return new ImportCompletionCacheService( - _peItemsCache, _projectItemsCache, workQueue); - } + return new ImportCompletionCacheService( + _peItemsCache, _projectItemsCache, workQueue); + } - private void OnCacheFlushRequested(object? sender, EventArgs e) - { - _peItemsCache.Clear(); - _projectItemsCache.Clear(); - } + private void OnCacheFlushRequested(object? sender, EventArgs e) + { + _peItemsCache.Clear(); + _projectItemsCache.Clear(); + } - private class ImportCompletionCacheService( - ConcurrentDictionary peCache, - ConcurrentDictionary projectCache, - AsyncBatchingWorkQueue workQueue) : IImportCompletionCacheService - { - public IDictionary PEItemsCache { get; } = peCache; + private class ImportCompletionCacheService( + ConcurrentDictionary peCache, + ConcurrentDictionary projectCache, + AsyncBatchingWorkQueue workQueue) : IImportCompletionCacheService + { + public IDictionary PEItemsCache { get; } = peCache; - public IDictionary ProjectItemsCache { get; } = projectCache; + public IDictionary ProjectItemsCache { get; } = projectCache; - public AsyncBatchingWorkQueue WorkQueue { get; } = workQueue; - } + public AsyncBatchingWorkQueue WorkQueue { get; } = workQueue; } } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs index 4e010531ec50b..9aac29d32a390 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs @@ -19,235 +19,234 @@ using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractImportCompletionProvider : LSPCompletionProvider, INotifyCommittingItemCompletionProvider { - internal abstract class AbstractImportCompletionProvider : LSPCompletionProvider, INotifyCommittingItemCompletionProvider + protected abstract bool ShouldProvideCompletion(CompletionContext completionContext, SyntaxContext syntaxContext); + protected abstract void WarmUpCacheInBackground(Document document); + protected abstract Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, HashSet namespacesInScope, CancellationToken cancellationToken); + protected abstract bool IsFinalSemicolonOfUsingOrExtern(SyntaxNode directive, SyntaxToken token); + protected abstract Task ShouldProvideParenthesisCompletionAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken); + protected abstract void LogCommit(); + + public Task NotifyCommittingItemAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) { - protected abstract bool ShouldProvideCompletion(CompletionContext completionContext, SyntaxContext syntaxContext); - protected abstract void WarmUpCacheInBackground(Document document); - protected abstract Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, HashSet namespacesInScope, CancellationToken cancellationToken); - protected abstract bool IsFinalSemicolonOfUsingOrExtern(SyntaxNode directive, SyntaxToken token); - protected abstract Task ShouldProvideParenthesisCompletionAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken); - protected abstract void LogCommit(); - - public Task NotifyCommittingItemAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) - { - LogCommit(); - return Task.CompletedTask; - } + LogCommit(); + return Task.CompletedTask; + } - internal override bool IsExpandItemProvider => true; + internal override bool IsExpandItemProvider => true; - public override async Task ProvideCompletionsAsync(CompletionContext completionContext) - { - if (!completionContext.CompletionOptions.ShouldShowItemsFromUnimportedNamespaces) - return; + public override async Task ProvideCompletionsAsync(CompletionContext completionContext) + { + if (!completionContext.CompletionOptions.ShouldShowItemsFromUnimportedNamespaces) + return; - var cancellationToken = completionContext.CancellationToken; - var document = completionContext.Document; + var cancellationToken = completionContext.CancellationToken; + var document = completionContext.Document; - var syntaxContext = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); + var syntaxContext = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); - if (!ShouldProvideCompletion(completionContext, syntaxContext)) - { - // Queue a background task to warm up cache and return immediately if this is not the context to trigger this provider. - // `ForceExpandedCompletionIndexCreation` and `UpdateImportCompletionCacheInBackground` are both test only options to - // make test behavior deterministic. - var options = completionContext.CompletionOptions; - if (options.UpdateImportCompletionCacheInBackground && !options.ForceExpandedCompletionIndexCreation) - WarmUpCacheInBackground(document); - - return; - } - - // Find all namespaces in scope at current cursor location, - // which will be used to filter so the provider only returns out-of-scope types. - var namespacesInScope = GetNamespacesInScope(syntaxContext, cancellationToken); - await AddCompletionItemsAsync(completionContext, syntaxContext, namespacesInScope, cancellationToken).ConfigureAwait(false); + if (!ShouldProvideCompletion(completionContext, syntaxContext)) + { + // Queue a background task to warm up cache and return immediately if this is not the context to trigger this provider. + // `ForceExpandedCompletionIndexCreation` and `UpdateImportCompletionCacheInBackground` are both test only options to + // make test behavior deterministic. + var options = completionContext.CompletionOptions; + if (options.UpdateImportCompletionCacheInBackground && !options.ForceExpandedCompletionIndexCreation) + WarmUpCacheInBackground(document); + + return; } - private static HashSet GetNamespacesInScope(SyntaxContext syntaxContext, CancellationToken cancellationToken) - { - var semanticModel = syntaxContext.SemanticModel; - var document = syntaxContext.Document; + // Find all namespaces in scope at current cursor location, + // which will be used to filter so the provider only returns out-of-scope types. + var namespacesInScope = GetNamespacesInScope(syntaxContext, cancellationToken); + await AddCompletionItemsAsync(completionContext, syntaxContext, namespacesInScope, cancellationToken).ConfigureAwait(false); + } - var importedNamespaces = GetImportedNamespaces(syntaxContext, cancellationToken); + private static HashSet GetNamespacesInScope(SyntaxContext syntaxContext, CancellationToken cancellationToken) + { + var semanticModel = syntaxContext.SemanticModel; + var document = syntaxContext.Document; - // This hashset will be used to match namespace names, so it must have the same case-sensitivity as the source language. - var syntaxFacts = document.GetRequiredLanguageService(); - var namespacesInScope = new HashSet(importedNamespaces, syntaxFacts.StringComparer); + var importedNamespaces = GetImportedNamespaces(syntaxContext, cancellationToken); - // Get containing namespaces. - var namespaceSymbol = semanticModel.GetEnclosingNamespace(syntaxContext.Position, cancellationToken); - while (namespaceSymbol != null) - { - namespacesInScope.Add(namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat)); - namespaceSymbol = namespaceSymbol.ContainingNamespace; - } + // This hashset will be used to match namespace names, so it must have the same case-sensitivity as the source language. + var syntaxFacts = document.GetRequiredLanguageService(); + var namespacesInScope = new HashSet(importedNamespaces, syntaxFacts.StringComparer); - return namespacesInScope; + // Get containing namespaces. + var namespaceSymbol = semanticModel.GetEnclosingNamespace(syntaxContext.Position, cancellationToken); + while (namespaceSymbol != null) + { + namespacesInScope.Add(namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat)); + namespaceSymbol = namespaceSymbol.ContainingNamespace; } - private static ImmutableArray GetImportedNamespaces(SyntaxContext context, CancellationToken cancellationToken) - { - var position = context.Position; - var targetToken = context.TargetToken; + return namespacesInScope; + } + + private static ImmutableArray GetImportedNamespaces(SyntaxContext context, CancellationToken cancellationToken) + { + var position = context.Position; + var targetToken = context.TargetToken; - // If we are immediately after `using` directive adjust position to the start of the next token. - // This is a workaround for an issue, when immediately after a `using` directive it is not included into the import scope. - // See https://github.com/dotnet/roslyn/issues/67447 for more info. - if (context.IsRightAfterUsingOrImportDirective) - position = targetToken.GetNextToken(includeZeroWidth: true).SpanStart; + // If we are immediately after `using` directive adjust position to the start of the next token. + // This is a workaround for an issue, when immediately after a `using` directive it is not included into the import scope. + // See https://github.com/dotnet/roslyn/issues/67447 for more info. + if (context.IsRightAfterUsingOrImportDirective) + position = targetToken.GetNextToken(includeZeroWidth: true).SpanStart; - var scopes = context.SemanticModel.GetImportScopes(position, cancellationToken); + var scopes = context.SemanticModel.GetImportScopes(position, cancellationToken); - using var _ = ArrayBuilder.GetInstance(out var usingsBuilder); + using var _ = ArrayBuilder.GetInstance(out var usingsBuilder); - foreach (var scope in scopes) + foreach (var scope in scopes) + { + foreach (var import in scope.Imports) { - foreach (var import in scope.Imports) + if (import.NamespaceOrType is INamespaceSymbol @namespace) { - if (import.NamespaceOrType is INamespaceSymbol @namespace) - { - usingsBuilder.Add(@namespace.ToDisplayString(SymbolDisplayFormats.NameFormat)); - } + usingsBuilder.Add(@namespace.ToDisplayString(SymbolDisplayFormats.NameFormat)); } } - - return usingsBuilder.ToImmutable(); } - public override async Task GetChangeAsync( - Document document, CompletionItem completionItem, char? commitKey, CancellationToken cancellationToken) - { - var containingNamespace = ImportCompletionItem.GetContainingNamespace(completionItem); - var provideParenthesisCompletion = await ShouldProvideParenthesisCompletionAsync( - document, - completionItem, - commitKey, - cancellationToken).ConfigureAwait(false); - - var insertText = completionItem.DisplayText; - if (provideParenthesisCompletion) - { - insertText += "()"; - CompletionProvidersLogger.LogCustomizedCommitToAddParenthesis(commitKey); - } - - if (await ShouldCompleteWithFullyQualifyTypeNameAsync().ConfigureAwait(false)) - { - var completionText = $"{containingNamespace}.{insertText}"; - return CompletionChange.Create(new TextChange(completionItem.Span, completionText)); - } - - // Find context node so we can use it to decide where to insert using/imports. - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var addImportContextNode = root.FindToken(completionItem.Span.Start, findInsideTrivia: true).Parent; - - // Add required using/imports directive. - var addImportService = document.GetRequiredLanguageService(); - var generator = document.GetRequiredLanguageService(); - - // TODO: fallback options https://github.com/dotnet/roslyn/issues/60786 - var globalOptions = document.Project.Solution.Services.GetService(); - var fallbackOptions = globalOptions?.Provider ?? CodeActionOptions.DefaultProvider; - - var addImportsOptions = await document.GetAddImportPlacementOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + return usingsBuilder.ToImmutable(); + } - var importNode = CreateImport(document, containingNamespace); + public override async Task GetChangeAsync( + Document document, CompletionItem completionItem, char? commitKey, CancellationToken cancellationToken) + { + var containingNamespace = ImportCompletionItem.GetContainingNamespace(completionItem); + var provideParenthesisCompletion = await ShouldProvideParenthesisCompletionAsync( + document, + completionItem, + commitKey, + cancellationToken).ConfigureAwait(false); + + var insertText = completionItem.DisplayText; + if (provideParenthesisCompletion) + { + insertText += "()"; + CompletionProvidersLogger.LogCustomizedCommitToAddParenthesis(commitKey); + } - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var rootWithImport = addImportService.AddImport(compilation, root, addImportContextNode!, importNode, generator, addImportsOptions, cancellationToken); - var documentWithImport = document.WithSyntaxRoot(rootWithImport); - // This only formats the annotated import we just added, not the entire document. - var formattedDocumentWithImport = await Formatter.FormatAsync(documentWithImport, Formatter.Annotation, formattingOptions, cancellationToken).ConfigureAwait(false); + if (await ShouldCompleteWithFullyQualifyTypeNameAsync().ConfigureAwait(false)) + { + var completionText = $"{containingNamespace}.{insertText}"; + return CompletionChange.Create(new TextChange(completionItem.Span, completionText)); + } - using var _ = ArrayBuilder.GetInstance(out var builder); + // Find context node so we can use it to decide where to insert using/imports. + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var addImportContextNode = root.FindToken(completionItem.Span.Start, findInsideTrivia: true).Parent; + + // Add required using/imports directive. + var addImportService = document.GetRequiredLanguageService(); + var generator = document.GetRequiredLanguageService(); + + // TODO: fallback options https://github.com/dotnet/roslyn/issues/60786 + var globalOptions = document.Project.Solution.Services.GetService(); + var fallbackOptions = globalOptions?.Provider ?? CodeActionOptions.DefaultProvider; + + var addImportsOptions = await document.GetAddImportPlacementOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + + var importNode = CreateImport(document, containingNamespace); + + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var rootWithImport = addImportService.AddImport(compilation, root, addImportContextNode!, importNode, generator, addImportsOptions, cancellationToken); + var documentWithImport = document.WithSyntaxRoot(rootWithImport); + // This only formats the annotated import we just added, not the entire document. + var formattedDocumentWithImport = await Formatter.FormatAsync(documentWithImport, Formatter.Annotation, formattingOptions, cancellationToken).ConfigureAwait(false); + + using var _ = ArrayBuilder.GetInstance(out var builder); + + // Get text change for add import + var importChanges = await formattedDocumentWithImport.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false); + builder.AddRange(importChanges); + + // Create text change for complete type name. + // + // Note: Don't try to obtain TextChange for completed type name by replacing the text directly, + // then use Document.GetTextChangesAsync on document created from the changed text. This is + // because it will do a diff and return TextChanges with minimum span instead of actual + // replacement span. + // + // For example: If I'm typing "asd", the completion provider could be triggered after "a" + // is typed. Then if I selected type "AsnEncodedData" to commit, by using the approach described + // above, we will get a TextChange of "AsnEncodedDat" with 0 length span, instead of a change of + // the full display text with a span of length 1. This will later mess up span-tracking and end up + // with "AsnEncodedDatasd" in the code. + builder.Add(new TextChange(completionItem.Span, insertText)); + + // Then get the combined change + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var newText = text.WithChanges(builder); + + var changes = builder.ToImmutable(); + var change = Utilities.Collapse(newText, changes); + return CompletionChange.Create(change, changes); + + async Task ShouldCompleteWithFullyQualifyTypeNameAsync() + { + if (ImportCompletionItem.ShouldAlwaysFullyQualify(completionItem)) + return true; - // Get text change for add import - var importChanges = await formattedDocumentWithImport.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false); - builder.AddRange(importChanges); + if (!IsAddingImportsSupported(document, completionOptions: null)) + return true; - // Create text change for complete type name. + // We might need to qualify unimported types to use them in an import directive, because they only affect members of the containing + // import container (e.g. namespace/class/etc. declarations). // - // Note: Don't try to obtain TextChange for completed type name by replacing the text directly, - // then use Document.GetTextChangesAsync on document created from the changed text. This is - // because it will do a diff and return TextChanges with minimum span instead of actual - // replacement span. + // For example, `List` and `StringBuilder` both need to be fully qualified below: + // + // using CollectionOfStringBuilders = System.Collections.Generic.List; // - // For example: If I'm typing "asd", the completion provider could be triggered after "a" - // is typed. Then if I selected type "AsnEncodedData" to commit, by using the approach described - // above, we will get a TextChange of "AsnEncodedDat" with 0 length span, instead of a change of - // the full display text with a span of length 1. This will later mess up span-tracking and end up - // with "AsnEncodedDatasd" in the code. - builder.Add(new TextChange(completionItem.Span, insertText)); - - // Then get the combined change - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var newText = text.WithChanges(builder); - - var changes = builder.ToImmutable(); - var change = Utilities.Collapse(newText, changes); - return CompletionChange.Create(change, changes); - - async Task ShouldCompleteWithFullyQualifyTypeNameAsync() - { - if (ImportCompletionItem.ShouldAlwaysFullyQualify(completionItem)) - return true; - - if (!IsAddingImportsSupported(document, completionOptions: null)) - return true; - - // We might need to qualify unimported types to use them in an import directive, because they only affect members of the containing - // import container (e.g. namespace/class/etc. declarations). - // - // For example, `List` and `StringBuilder` both need to be fully qualified below: - // - // using CollectionOfStringBuilders = System.Collections.Generic.List; - // - // However, if we are typing in an C# using directive that is inside a nested import container (i.e. inside a namespace declaration block), - // then we can add an using in the outer import container instead (this is not allowed in VB). - // - // For example: - // - // using System.Collections.Generic; - // using System.Text; - // - // namespace Foo - // { - // using CollectionOfStringBuilders = List; - // } - // - // Here we will always choose to qualify the unimported type, just to be consistent and keeps things simple. - return await IsInImportsDirectiveAsync(document, completionItem.Span.Start, cancellationToken).ConfigureAwait(false); - } - } - - private async Task IsInImportsDirectiveAsync(Document document, int position, CancellationToken cancellationToken) - { - var syntaxFacts = document.GetRequiredLanguageService(); - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDirectives: true); - return leftToken.GetAncestor(syntaxFacts.IsUsingOrExternOrImport) is { } node - && !IsFinalSemicolonOfUsingOrExtern(node, leftToken); + // However, if we are typing in an C# using directive that is inside a nested import container (i.e. inside a namespace declaration block), + // then we can add an using in the outer import container instead (this is not allowed in VB). + // + // For example: + // + // using System.Collections.Generic; + // using System.Text; + // + // namespace Foo + // { + // using CollectionOfStringBuilders = List; + // } + // + // Here we will always choose to qualify the unimported type, just to be consistent and keeps things simple. + return await IsInImportsDirectiveAsync(document, completionItem.Span.Start, cancellationToken).ConfigureAwait(false); } + } - protected static bool IsAddingImportsSupported(Document document, CompletionOptions? completionOptions) - { - // Certain documents, e.g. Razor document, don't support adding imports - return completionOptions?.CanAddImportStatement != false && - document.Project.Solution.Services.GetRequiredService().SupportsRefactorings(document); - } + private async Task IsInImportsDirectiveAsync(Document document, int position, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDirectives: true); + return leftToken.GetAncestor(syntaxFacts.IsUsingOrExternOrImport) is { } node + && !IsFinalSemicolonOfUsingOrExtern(node, leftToken); + } - private static SyntaxNode CreateImport(Document document, string namespaceName) - { - var syntaxGenerator = SyntaxGenerator.GetGenerator(document); - return syntaxGenerator.NamespaceImportDeclaration(namespaceName).WithAdditionalAnnotations(Formatter.Annotation); - } + protected static bool IsAddingImportsSupported(Document document, CompletionOptions? completionOptions) + { + // Certain documents, e.g. Razor document, don't support adding imports + return completionOptions?.CanAddImportStatement != false && + document.Project.Solution.Services.GetRequiredService().SupportsRefactorings(document); + } - internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - => ImportCompletionItem.GetCompletionDescriptionAsync(document, item, displayOptions, cancellationToken); + private static SyntaxNode CreateImport(Document document, string namespaceName) + { + var syntaxGenerator = SyntaxGenerator.GetGenerator(document); + return syntaxGenerator.NamespaceImportDeclaration(namespaceName).WithAdditionalAnnotations(Formatter.Annotation); } + + internal override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + => ImportCompletionItem.GetCompletionDescriptionAsync(document, item, displayOptions, cancellationToken); } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionProvider.cs index 9f46880aed9b4..b2c99eacba2df 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionProvider.cs @@ -13,169 +13,168 @@ using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractTypeImportCompletionProvider : AbstractImportCompletionProvider + where AliasDeclarationTypeNode : SyntaxNode { - internal abstract class AbstractTypeImportCompletionProvider : AbstractImportCompletionProvider - where AliasDeclarationTypeNode : SyntaxNode - { - protected override bool ShouldProvideCompletion(CompletionContext completionContext, SyntaxContext syntaxContext) - => syntaxContext.IsTypeContext || syntaxContext.IsEnumBaseListContext; + protected override bool ShouldProvideCompletion(CompletionContext completionContext, SyntaxContext syntaxContext) + => syntaxContext.IsTypeContext || syntaxContext.IsEnumBaseListContext; - protected override void LogCommit() - => CompletionProvidersLogger.LogCommitOfTypeImportCompletionItem(); + protected override void LogCommit() + => CompletionProvidersLogger.LogCommitOfTypeImportCompletionItem(); - protected override void WarmUpCacheInBackground(Document document) - { - var typeImportCompletionService = document.GetRequiredLanguageService(); - typeImportCompletionService.QueueCacheWarmUpTask(document.Project); - } + protected override void WarmUpCacheInBackground(Document document) + { + var typeImportCompletionService = document.GetRequiredLanguageService(); + typeImportCompletionService.QueueCacheWarmUpTask(document.Project); + } - protected override async Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, HashSet namespacesInScope, CancellationToken cancellationToken) + protected override async Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, HashSet namespacesInScope, CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.Completion_TypeImportCompletionProvider_GetCompletionItemsAsync, cancellationToken)) { - using (Logger.LogBlock(FunctionId.Completion_TypeImportCompletionProvider_GetCompletionItemsAsync, cancellationToken)) - { - var telemetryCounter = new TelemetryCounter(); - var typeImportCompletionService = completionContext.Document.GetRequiredLanguageService(); + var telemetryCounter = new TelemetryCounter(); + var typeImportCompletionService = completionContext.Document.GetRequiredLanguageService(); - var (itemsFromAllAssemblies, isPartialResult) = await typeImportCompletionService.GetAllTopLevelTypesAsync( - syntaxContext, - forceCacheCreation: completionContext.CompletionOptions.ForceExpandedCompletionIndexCreation, - completionContext.CompletionOptions, - cancellationToken).ConfigureAwait(false); + var (itemsFromAllAssemblies, isPartialResult) = await typeImportCompletionService.GetAllTopLevelTypesAsync( + syntaxContext, + forceCacheCreation: completionContext.CompletionOptions.ForceExpandedCompletionIndexCreation, + completionContext.CompletionOptions, + cancellationToken).ConfigureAwait(false); - var aliasTargetNamespaceToTypeNameMap = GetAliasTypeDictionary(completionContext.Document, syntaxContext, cancellationToken); - foreach (var items in itemsFromAllAssemblies) - AddItems(items, completionContext, namespacesInScope, aliasTargetNamespaceToTypeNameMap, telemetryCounter); + var aliasTargetNamespaceToTypeNameMap = GetAliasTypeDictionary(completionContext.Document, syntaxContext, cancellationToken); + foreach (var items in itemsFromAllAssemblies) + AddItems(items, completionContext, namespacesInScope, aliasTargetNamespaceToTypeNameMap, telemetryCounter); - if (isPartialResult) - telemetryCounter.CacheMiss = true; + if (isPartialResult) + telemetryCounter.CacheMiss = true; - telemetryCounter.Report(); - } + telemetryCounter.Report(); } + } - /// - /// Get a multi-Dictionary stores the information about the target of all alias Symbol in the syntax tree. - /// Multiple aliases might live under same namespace. - /// Key is the namespace of the symbol, value is the name of the symbol. - /// - private static MultiDictionary GetAliasTypeDictionary( - Document document, - SyntaxContext syntaxContext, - CancellationToken cancellationToken) - { - var syntaxFactsService = document.GetRequiredLanguageService(); - var dictionary = new MultiDictionary(syntaxFactsService.StringComparer); + /// + /// Get a multi-Dictionary stores the information about the target of all alias Symbol in the syntax tree. + /// Multiple aliases might live under same namespace. + /// Key is the namespace of the symbol, value is the name of the symbol. + /// + private static MultiDictionary GetAliasTypeDictionary( + Document document, + SyntaxContext syntaxContext, + CancellationToken cancellationToken) + { + var syntaxFactsService = document.GetRequiredLanguageService(); + var dictionary = new MultiDictionary(syntaxFactsService.StringComparer); - foreach (var scope in syntaxContext.SemanticModel.GetImportScopes(syntaxContext.Position, cancellationToken)) + foreach (var scope in syntaxContext.SemanticModel.GetImportScopes(syntaxContext.Position, cancellationToken)) + { + foreach (var symbol in scope.Aliases) { - foreach (var symbol in scope.Aliases) + if (symbol is { Target: ITypeSymbol { TypeKind: not TypeKind.Error } target }) { - if (symbol is { Target: ITypeSymbol { TypeKind: not TypeKind.Error } target }) + // If the target type is a type constructs from generics type, e.g. + // using AliasBar = Bar + // namespace Foo + // { + // public class Bar + // { + // } + // } + // namespace Foo2 + // { + // public class Main + // { + // $$ + // } + // } + // In such case, user might want to type Bar and still want 'using Foo'. + // We shouldn't try to filter the CompletionItem for Bar later. + // so just ignore the Bar here. + var typeParameter = target.GetTypeParameters(); + if (typeParameter.IsEmpty) { - // If the target type is a type constructs from generics type, e.g. - // using AliasBar = Bar - // namespace Foo - // { - // public class Bar - // { - // } - // } - // namespace Foo2 - // { - // public class Main - // { - // $$ - // } - // } - // In such case, user might want to type Bar and still want 'using Foo'. - // We shouldn't try to filter the CompletionItem for Bar later. - // so just ignore the Bar here. - var typeParameter = target.GetTypeParameters(); - if (typeParameter.IsEmpty) - { - var namespaceOfTarget = target.ContainingNamespace.ToDisplayString(SymbolDisplayFormats.NameFormat); - var typeNameOfTarget = target.Name; - dictionary.Add(namespaceOfTarget, typeNameOfTarget); - } + var namespaceOfTarget = target.ContainingNamespace.ToDisplayString(SymbolDisplayFormats.NameFormat); + var typeNameOfTarget = target.Name; + dictionary.Add(namespaceOfTarget, typeNameOfTarget); } } } + } + + return dictionary; + } - return dictionary; + private static void AddItems( + ImmutableArray items, + CompletionContext completionContext, + HashSet namespacesInScope, + MultiDictionary aliasTargetNamespaceToTypeNameMap, + TelemetryCounter counter) + { + counter.ReferenceCount++; + foreach (var item in items) + { + if (ShouldAddItem(item, namespacesInScope, aliasTargetNamespaceToTypeNameMap)) + { + // We can return cached item directly, item's span will be fixed by completion service. + // On the other hand, because of this (i.e. mutating the span of cached item for each run), + // the provider can not be used as a service by components that might be run in parallel + // with completion, which would be a race. + completionContext.AddItem(item); + counter.ItemsCount++; + } } - private static void AddItems( - ImmutableArray items, - CompletionContext completionContext, + static bool ShouldAddItem( + CompletionItem item, HashSet namespacesInScope, - MultiDictionary aliasTargetNamespaceToTypeNameMap, - TelemetryCounter counter) + MultiDictionary aliasTargetNamespaceToTypeNameMap) { - counter.ReferenceCount++; - foreach (var item in items) + var containingNamespace = ImportCompletionItem.GetContainingNamespace(item); + // 1. if the namespace of the item is in scoop. Don't add the item + if (namespacesInScope.Contains(containingNamespace)) { - if (ShouldAddItem(item, namespacesInScope, aliasTargetNamespaceToTypeNameMap)) - { - // We can return cached item directly, item's span will be fixed by completion service. - // On the other hand, because of this (i.e. mutating the span of cached item for each run), - // the provider can not be used as a service by components that might be run in parallel - // with completion, which would be a race. - completionContext.AddItem(item); - counter.ItemsCount++; - } + return false; } - static bool ShouldAddItem( - CompletionItem item, - HashSet namespacesInScope, - MultiDictionary aliasTargetNamespaceToTypeNameMap) + // 2. If the item might be an alias target. First check if the target alias map has any value then + // check if the type name is in the dictionary. + // It is done in this way to avoid calling ImportCompletionItem.GetTypeName for all the CompletionItems + if (!aliasTargetNamespaceToTypeNameMap.IsEmpty + && aliasTargetNamespaceToTypeNameMap[containingNamespace].Contains(ImportCompletionItem.GetTypeName(item))) { - var containingNamespace = ImportCompletionItem.GetContainingNamespace(item); - // 1. if the namespace of the item is in scoop. Don't add the item - if (namespacesInScope.Contains(containingNamespace)) - { - return false; - } - - // 2. If the item might be an alias target. First check if the target alias map has any value then - // check if the type name is in the dictionary. - // It is done in this way to avoid calling ImportCompletionItem.GetTypeName for all the CompletionItems - if (!aliasTargetNamespaceToTypeNameMap.IsEmpty - && aliasTargetNamespaceToTypeNameMap[containingNamespace].Contains(ImportCompletionItem.GetTypeName(item))) - { - return false; - } - - return true; + return false; } + + return true; } + } - private class TelemetryCounter - { - private readonly SharedStopwatch _elapsedTime; + private class TelemetryCounter + { + private readonly SharedStopwatch _elapsedTime; - public int ItemsCount { get; set; } - public int ReferenceCount { get; set; } - public bool CacheMiss { get; set; } + public int ItemsCount { get; set; } + public int ReferenceCount { get; set; } + public bool CacheMiss { get; set; } - public TelemetryCounter() - { - _elapsedTime = SharedStopwatch.StartNew(); - } + public TelemetryCounter() + { + _elapsedTime = SharedStopwatch.StartNew(); + } - public void Report() + public void Report() + { + if (CacheMiss) { - if (CacheMiss) - { - CompletionProvidersLogger.LogTypeImportCompletionCacheMiss(); - } - - // cache miss still count towards the cost of completion, so we need to log regardless of it. - CompletionProvidersLogger.LogTypeImportCompletionTicksDataPoint(_elapsedTime.Elapsed); - CompletionProvidersLogger.LogTypeImportCompletionItemCountDataPoint(ItemsCount); - CompletionProvidersLogger.LogTypeImportCompletionReferenceCountDataPoint(ReferenceCount); + CompletionProvidersLogger.LogTypeImportCompletionCacheMiss(); } + + // cache miss still count towards the cost of completion, so we need to log regardless of it. + CompletionProvidersLogger.LogTypeImportCompletionTicksDataPoint(_elapsedTime.Elapsed); + CompletionProvidersLogger.LogTypeImportCompletionItemCountDataPoint(ItemsCount); + CompletionProvidersLogger.LogTypeImportCompletionReferenceCountDataPoint(ReferenceCount); } } } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs index 4100ec49218b8..6067e48e693ae 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs @@ -19,306 +19,305 @@ using static Microsoft.CodeAnalysis.Shared.Utilities.EditorBrowsableHelpers; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract partial class AbstractTypeImportCompletionService : ITypeImportCompletionService { - internal abstract partial class AbstractTypeImportCompletionService : ITypeImportCompletionService - { - private IImportCompletionCacheService CacheService { get; } + private IImportCompletionCacheService CacheService { get; } - protected abstract string GenericTypeSuffix { get; } + protected abstract string GenericTypeSuffix { get; } - protected abstract bool IsCaseSensitive { get; } + protected abstract bool IsCaseSensitive { get; } - protected abstract string Language { get; } + protected abstract string Language { get; } - internal AbstractTypeImportCompletionService(SolutionServices services) - { - CacheService = services.GetRequiredService>(); - } + internal AbstractTypeImportCompletionService(SolutionServices services) + { + CacheService = services.GetRequiredService>(); + } - public void QueueCacheWarmUpTask(Project project) - { - CacheService.WorkQueue.AddWork(project); - } + public void QueueCacheWarmUpTask(Project project) + { + CacheService.WorkQueue.AddWork(project); + } - public async Task<(ImmutableArray>, bool)> GetAllTopLevelTypesAsync( - SyntaxContext syntaxContext, - bool forceCacheCreation, - CompletionOptions options, - CancellationToken cancellationToken) - { - var currentProject = syntaxContext.Document.Project; - var (getCacheResults, isPartialResult) = await GetCacheEntriesAsync(currentProject, syntaxContext.SemanticModel.Compilation, forceCacheCreation, cancellationToken).ConfigureAwait(false); - - var currentCompilation = syntaxContext.SemanticModel.Compilation; - return (getCacheResults.SelectAsArray(GetItemsFromCacheResult), isPartialResult); - - ImmutableArray GetItemsFromCacheResult(TypeImportCompletionCacheEntry cacheEntry) - => cacheEntry.GetItemsForContext( - currentCompilation, - Language, - GenericTypeSuffix, - syntaxContext.IsAttributeNameContext, - syntaxContext.IsEnumBaseListContext, - IsCaseSensitive, - options.HideAdvancedMembers); - } + public async Task<(ImmutableArray>, bool)> GetAllTopLevelTypesAsync( + SyntaxContext syntaxContext, + bool forceCacheCreation, + CompletionOptions options, + CancellationToken cancellationToken) + { + var currentProject = syntaxContext.Document.Project; + var (getCacheResults, isPartialResult) = await GetCacheEntriesAsync(currentProject, syntaxContext.SemanticModel.Compilation, forceCacheCreation, cancellationToken).ConfigureAwait(false); + + var currentCompilation = syntaxContext.SemanticModel.Compilation; + return (getCacheResults.SelectAsArray(GetItemsFromCacheResult), isPartialResult); + + ImmutableArray GetItemsFromCacheResult(TypeImportCompletionCacheEntry cacheEntry) + => cacheEntry.GetItemsForContext( + currentCompilation, + Language, + GenericTypeSuffix, + syntaxContext.IsAttributeNameContext, + syntaxContext.IsEnumBaseListContext, + IsCaseSensitive, + options.HideAdvancedMembers); + } - private async Task<(ImmutableArray results, bool isPartial)> GetCacheEntriesAsync(Project currentProject, Compilation originCompilation, bool forceCacheCreation, CancellationToken cancellationToken) + private async Task<(ImmutableArray results, bool isPartial)> GetCacheEntriesAsync(Project currentProject, Compilation originCompilation, bool forceCacheCreation, CancellationToken cancellationToken) + { + try { - try - { - var isPartialResult = false; - using var _1 = ArrayBuilder.GetInstance(out var resultBuilder); - using var _2 = ArrayBuilder.GetInstance(out var projectsBuilder); - using var _3 = PooledHashSet.GetInstance(out var nonGlobalAliasedProjectReferencesSet); + var isPartialResult = false; + using var _1 = ArrayBuilder.GetInstance(out var resultBuilder); + using var _2 = ArrayBuilder.GetInstance(out var projectsBuilder); + using var _3 = PooledHashSet.GetInstance(out var nonGlobalAliasedProjectReferencesSet); + + var solution = currentProject.Solution; + var graph = solution.GetProjectDependencyGraph(); + var referencedProjects = graph.GetProjectsThatThisProjectTransitivelyDependsOn(currentProject.Id).Select(solution.GetRequiredProject).Where(p => p.SupportsCompilation); - var solution = currentProject.Solution; - var graph = solution.GetProjectDependencyGraph(); - var referencedProjects = graph.GetProjectsThatThisProjectTransitivelyDependsOn(currentProject.Id).Select(solution.GetRequiredProject).Where(p => p.SupportsCompilation); + projectsBuilder.Add(currentProject); + projectsBuilder.AddRange(referencedProjects); + nonGlobalAliasedProjectReferencesSet.AddRange(currentProject.ProjectReferences.Where(pr => !HasGlobalAlias(pr.Aliases)).Select(pr => pr.ProjectId)); - projectsBuilder.Add(currentProject); - projectsBuilder.AddRange(referencedProjects); - nonGlobalAliasedProjectReferencesSet.AddRange(currentProject.ProjectReferences.Where(pr => !HasGlobalAlias(pr.Aliases)).Select(pr => pr.ProjectId)); + foreach (var project in projectsBuilder) + { + var projectId = project.Id; + if (nonGlobalAliasedProjectReferencesSet.Contains(projectId)) + continue; - foreach (var project in projectsBuilder) + if (forceCacheCreation) { - var projectId = project.Id; - if (nonGlobalAliasedProjectReferencesSet.Contains(projectId)) - continue; + var upToDateCacheEntry = await GetUpToDateCacheForProjectAsync(project, cancellationToken).ConfigureAwait(false); + resultBuilder.Add(upToDateCacheEntry); + } + else if (CacheService.ProjectItemsCache.TryGetValue(projectId, out var cacheEntry)) + { + resultBuilder.Add(cacheEntry); + } + else + { + isPartialResult = true; + } + } - if (forceCacheCreation) + var editorBrowsableInfo = new Lazy(() => new EditorBrowsableInfo(originCompilation)); + foreach (var peReference in currentProject.MetadataReferences.OfType()) + { + // Can't cache items for reference with null key. We don't want risk potential perf regression by + // making those items repeatedly, so simply not returning anything from this assembly, until + // we have a better understanding on this scenario. + var peReferenceKey = GetPEReferenceCacheKey(peReference); + if (peReferenceKey is null || !HasGlobalAlias(peReference.Properties.Aliases)) + continue; + + if (forceCacheCreation) + { + if (TryGetUpToDateCacheForPEReference(originCompilation, solution, editorBrowsableInfo.Value, peReference, cancellationToken, out var upToDateCacheEntry)) { - var upToDateCacheEntry = await GetUpToDateCacheForProjectAsync(project, cancellationToken).ConfigureAwait(false); resultBuilder.Add(upToDateCacheEntry); } - else if (CacheService.ProjectItemsCache.TryGetValue(projectId, out var cacheEntry)) - { - resultBuilder.Add(cacheEntry); - } - else - { - isPartialResult = true; - } } - - var editorBrowsableInfo = new Lazy(() => new EditorBrowsableInfo(originCompilation)); - foreach (var peReference in currentProject.MetadataReferences.OfType()) + else if (CacheService.PEItemsCache.TryGetValue(peReferenceKey, out var cacheEntry)) { - // Can't cache items for reference with null key. We don't want risk potential perf regression by - // making those items repeatedly, so simply not returning anything from this assembly, until - // we have a better understanding on this scenario. - var peReferenceKey = GetPEReferenceCacheKey(peReference); - if (peReferenceKey is null || !HasGlobalAlias(peReference.Properties.Aliases)) - continue; - - if (forceCacheCreation) - { - if (TryGetUpToDateCacheForPEReference(originCompilation, solution, editorBrowsableInfo.Value, peReference, cancellationToken, out var upToDateCacheEntry)) - { - resultBuilder.Add(upToDateCacheEntry); - } - } - else if (CacheService.PEItemsCache.TryGetValue(peReferenceKey, out var cacheEntry)) - { - resultBuilder.Add(cacheEntry); - } - else - { - isPartialResult = true; - } + resultBuilder.Add(cacheEntry); + } + else + { + isPartialResult = true; } - - return (resultBuilder.ToImmutable(), isPartialResult); - } - finally - { - if (!forceCacheCreation) - CacheService.WorkQueue.AddWork(currentProject); } + + return (resultBuilder.ToImmutable(), isPartialResult); + } + finally + { + if (!forceCacheCreation) + CacheService.WorkQueue.AddWork(currentProject); } + } - public static async ValueTask BatchUpdateCacheAsync(ImmutableSegmentedList projects, CancellationToken cancellationToken) + public static async ValueTask BatchUpdateCacheAsync(ImmutableSegmentedList projects, CancellationToken cancellationToken) + { + var latestProjects = CompletionUtilities.GetDistinctProjectsFromLatestSolutionSnapshot(projects); + foreach (var project in latestProjects) { - var latestProjects = CompletionUtilities.GetDistinctProjectsFromLatestSolutionSnapshot(projects); - foreach (var project in latestProjects) - { - cancellationToken.ThrowIfCancellationRequested(); - var service = (AbstractTypeImportCompletionService)project.GetRequiredLanguageService(); - var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - _ = await service.GetCacheEntriesAsync(project, compilation, forceCacheCreation: true, cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); + var service = (AbstractTypeImportCompletionService)project.GetRequiredLanguageService(); + var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + _ = await service.GetCacheEntriesAsync(project, compilation, forceCacheCreation: true, cancellationToken).ConfigureAwait(false); } + } - private static bool HasGlobalAlias(ImmutableArray aliases) - => aliases.IsEmpty || aliases.Any(static alias => alias == MetadataReferenceProperties.GlobalAlias); + private static bool HasGlobalAlias(ImmutableArray aliases) + => aliases.IsEmpty || aliases.Any(static alias => alias == MetadataReferenceProperties.GlobalAlias); - private static string? GetPEReferenceCacheKey(PortableExecutableReference peReference) - => peReference.FilePath ?? peReference.Display; + private static string? GetPEReferenceCacheKey(PortableExecutableReference peReference) + => peReference.FilePath ?? peReference.Display; - /// - /// Get appropriate completion items for all the visible top level types from given project. - /// This method is intended to be used for getting types from source only, so the project must support compilation. - /// For getting types from PE, use . - /// - private async Task GetUpToDateCacheForProjectAsync(Project project, CancellationToken cancellationToken) - { - // Since we only need top level types from source, therefore we only care if source symbol checksum changes. - var checksum = await SymbolTreeInfo.GetSourceSymbolsChecksumAsync(project, cancellationToken).ConfigureAwait(false); - var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + /// + /// Get appropriate completion items for all the visible top level types from given project. + /// This method is intended to be used for getting types from source only, so the project must support compilation. + /// For getting types from PE, use . + /// + private async Task GetUpToDateCacheForProjectAsync(Project project, CancellationToken cancellationToken) + { + // Since we only need top level types from source, therefore we only care if source symbol checksum changes. + var checksum = await SymbolTreeInfo.GetSourceSymbolsChecksumAsync(project, cancellationToken).ConfigureAwait(false); + var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + + return CreateCacheWorker( + project.Id, + compilation.Assembly, + checksum, + CacheService.ProjectItemsCache, + new EditorBrowsableInfo(compilation), + cancellationToken); + } - return CreateCacheWorker( - project.Id, - compilation.Assembly, - checksum, - CacheService.ProjectItemsCache, - new EditorBrowsableInfo(compilation), + /// + /// Get appropriate completion items for all the visible top level types from given PE reference. + /// + private bool TryGetUpToDateCacheForPEReference( + Compilation originCompilation, + Solution solution, + EditorBrowsableInfo editorBrowsableInfo, + PortableExecutableReference peReference, + CancellationToken cancellationToken, + out TypeImportCompletionCacheEntry cacheEntry) + { + if (originCompilation.GetAssemblyOrModuleSymbol(peReference) is not IAssemblySymbol assemblySymbol) + { + cacheEntry = default; + return false; + } + else + { + cacheEntry = CreateCacheWorker( + GetPEReferenceCacheKey(peReference)!, + assemblySymbol, + checksum: SymbolTreeInfo.GetMetadataChecksum(solution.Services, peReference, cancellationToken), + CacheService.PEItemsCache, + editorBrowsableInfo, cancellationToken); + return true; } + } - /// - /// Get appropriate completion items for all the visible top level types from given PE reference. - /// - private bool TryGetUpToDateCacheForPEReference( - Compilation originCompilation, - Solution solution, - EditorBrowsableInfo editorBrowsableInfo, - PortableExecutableReference peReference, - CancellationToken cancellationToken, - out TypeImportCompletionCacheEntry cacheEntry) + private TypeImportCompletionCacheEntry CreateCacheWorker( + TKey key, + IAssemblySymbol assembly, + Checksum checksum, + IDictionary cache, + EditorBrowsableInfo editorBrowsableInfo, + CancellationToken cancellationToken) + where TKey : notnull + { + // Cache hit + if (cache.TryGetValue(key, out var cacheEntry) && cacheEntry.Checksum == checksum) { - if (originCompilation.GetAssemblyOrModuleSymbol(peReference) is not IAssemblySymbol assemblySymbol) - { - cacheEntry = default; - return false; - } - else - { - cacheEntry = CreateCacheWorker( - GetPEReferenceCacheKey(peReference)!, - assemblySymbol, - checksum: SymbolTreeInfo.GetMetadataChecksum(solution.Services, peReference, cancellationToken), - CacheService.PEItemsCache, - editorBrowsableInfo, - cancellationToken); - return true; - } + return cacheEntry; } - private TypeImportCompletionCacheEntry CreateCacheWorker( - TKey key, - IAssemblySymbol assembly, - Checksum checksum, - IDictionary cache, - EditorBrowsableInfo editorBrowsableInfo, - CancellationToken cancellationToken) - where TKey : notnull - { - // Cache hit - if (cache.TryGetValue(key, out var cacheEntry) && cacheEntry.Checksum == checksum) - { - return cacheEntry; - } + using var builder = new TypeImportCompletionCacheEntry.Builder(SymbolKey.Create(assembly, cancellationToken), checksum, Language, GenericTypeSuffix, editorBrowsableInfo); + GetCompletionItemsForTopLevelTypeDeclarations(assembly.GlobalNamespace, builder, cancellationToken); + cacheEntry = builder.ToReferenceCacheEntry(); + cache[key] = cacheEntry; - using var builder = new TypeImportCompletionCacheEntry.Builder(SymbolKey.Create(assembly, cancellationToken), checksum, Language, GenericTypeSuffix, editorBrowsableInfo); - GetCompletionItemsForTopLevelTypeDeclarations(assembly.GlobalNamespace, builder, cancellationToken); - cacheEntry = builder.ToReferenceCacheEntry(); - cache[key] = cacheEntry; + return cacheEntry; + } + private static string ConcatNamespace(string? containingNamespace, string name) + => string.IsNullOrEmpty(containingNamespace) ? name : containingNamespace + "." + name; - return cacheEntry; - } - private static string ConcatNamespace(string? containingNamespace, string name) - => string.IsNullOrEmpty(containingNamespace) ? name : containingNamespace + "." + name; + private static void GetCompletionItemsForTopLevelTypeDeclarations( + INamespaceSymbol rootNamespaceSymbol, + TypeImportCompletionCacheEntry.Builder builder, + CancellationToken cancellationToken) + { + VisitNamespace(rootNamespaceSymbol, containingNamespace: null, builder, cancellationToken); + return; - private static void GetCompletionItemsForTopLevelTypeDeclarations( - INamespaceSymbol rootNamespaceSymbol, + static void VisitNamespace( + INamespaceSymbol symbol, + string? containingNamespace, TypeImportCompletionCacheEntry.Builder builder, CancellationToken cancellationToken) { - VisitNamespace(rootNamespaceSymbol, containingNamespace: null, builder, cancellationToken); - return; - - static void VisitNamespace( - INamespaceSymbol symbol, - string? containingNamespace, - TypeImportCompletionCacheEntry.Builder builder, - CancellationToken cancellationToken) + cancellationToken.ThrowIfCancellationRequested(); + containingNamespace = ConcatNamespace(containingNamespace, symbol.Name); + + foreach (var memberNamespace in symbol.GetNamespaceMembers()) { - cancellationToken.ThrowIfCancellationRequested(); - containingNamespace = ConcatNamespace(containingNamespace, symbol.Name); + VisitNamespace(memberNamespace, containingNamespace, builder, cancellationToken); + } + + using var _ = PooledDictionary.GetInstance(out var overloads); + var types = symbol.GetTypeMembers(); - foreach (var memberNamespace in symbol.GetNamespaceMembers()) + // Iterate over all top level internal and public types, keep track of "type overloads". + foreach (var type in types) + { + // Include all top level types except those declared as `file` (i.e. all internal or public) + if (type.CanBeReferencedByName && !type.IsFileLocal) { - VisitNamespace(memberNamespace, containingNamespace, builder, cancellationToken); + overloads.TryGetValue(type.Name, out var overloadInfo); + overloads[type.Name] = overloadInfo.Aggregate(type); } + } - using var _ = PooledDictionary.GetInstance(out var overloads); - var types = symbol.GetTypeMembers(); + foreach (var pair in overloads) + { + var overloadInfo = pair.Value; - // Iterate over all top level internal and public types, keep track of "type overloads". - foreach (var type in types) + // Create CompletionItem for non-generic type overload, if exists. + if (overloadInfo.NonGenericOverload != null) { - // Include all top level types except those declared as `file` (i.e. all internal or public) - if (type.CanBeReferencedByName && !type.IsFileLocal) - { - overloads.TryGetValue(type.Name, out var overloadInfo); - overloads[type.Name] = overloadInfo.Aggregate(type); - } + builder.AddItem( + overloadInfo.NonGenericOverload, + containingNamespace, + overloadInfo.NonGenericOverload.DeclaredAccessibility == Accessibility.Public); } - foreach (var pair in overloads) + // Create one CompletionItem for all generic type overloads, if there's any. + // For simplicity, we always show the type symbol with lowest arity in CompletionDescription + // and without displaying the total number of overloads. + if (overloadInfo.BestGenericOverload != null) { - var overloadInfo = pair.Value; - - // Create CompletionItem for non-generic type overload, if exists. - if (overloadInfo.NonGenericOverload != null) - { - builder.AddItem( - overloadInfo.NonGenericOverload, - containingNamespace, - overloadInfo.NonGenericOverload.DeclaredAccessibility == Accessibility.Public); - } - - // Create one CompletionItem for all generic type overloads, if there's any. - // For simplicity, we always show the type symbol with lowest arity in CompletionDescription - // and without displaying the total number of overloads. - if (overloadInfo.BestGenericOverload != null) - { - // If any of the generic overloads is public, then the completion item is considered public. - builder.AddItem( - overloadInfo.BestGenericOverload, - containingNamespace, - overloadInfo.ContainsPublicGenericOverload); - } + // If any of the generic overloads is public, then the completion item is considered public. + builder.AddItem( + overloadInfo.BestGenericOverload, + containingNamespace, + overloadInfo.ContainsPublicGenericOverload); } } } + } - private readonly struct TypeOverloadInfo(INamedTypeSymbol nonGenericOverload, INamedTypeSymbol bestGenericOverload, bool containsPublicGenericOverload) - { - public INamedTypeSymbol NonGenericOverload { get; } = nonGenericOverload; + private readonly struct TypeOverloadInfo(INamedTypeSymbol nonGenericOverload, INamedTypeSymbol bestGenericOverload, bool containsPublicGenericOverload) + { + public INamedTypeSymbol NonGenericOverload { get; } = nonGenericOverload; - // Generic with fewest type parameters is considered best symbol to show in description. - public INamedTypeSymbol BestGenericOverload { get; } = bestGenericOverload; + // Generic with fewest type parameters is considered best symbol to show in description. + public INamedTypeSymbol BestGenericOverload { get; } = bestGenericOverload; - public bool ContainsPublicGenericOverload { get; } = containsPublicGenericOverload; + public bool ContainsPublicGenericOverload { get; } = containsPublicGenericOverload; - public TypeOverloadInfo Aggregate(INamedTypeSymbol type) + public TypeOverloadInfo Aggregate(INamedTypeSymbol type) + { + if (type.Arity == 0) { - if (type.Arity == 0) - { - return new TypeOverloadInfo(nonGenericOverload: type, BestGenericOverload, ContainsPublicGenericOverload); - } + return new TypeOverloadInfo(nonGenericOverload: type, BestGenericOverload, ContainsPublicGenericOverload); + } - // We consider generic with fewer type parameters better symbol to show in description - var newBestGenericOverload = BestGenericOverload == null || type.Arity < BestGenericOverload.Arity - ? type - : BestGenericOverload; + // We consider generic with fewer type parameters better symbol to show in description + var newBestGenericOverload = BestGenericOverload == null || type.Arity < BestGenericOverload.Arity + ? type + : BestGenericOverload; - var newContainsPublicGenericOverload = type.DeclaredAccessibility >= Accessibility.Public || ContainsPublicGenericOverload; + var newContainsPublicGenericOverload = type.DeclaredAccessibility >= Accessibility.Public || ContainsPublicGenericOverload; - return new TypeOverloadInfo(NonGenericOverload, newBestGenericOverload, newContainsPublicGenericOverload); - } + return new TypeOverloadInfo(NonGenericOverload, newBestGenericOverload, newContainsPublicGenericOverload); } } } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/DefaultExtensionMethodImportCompletionCacheServiceFactory.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/DefaultExtensionMethodImportCompletionCacheServiceFactory.cs index 24e73e726182c..eeb06b8ae680e 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/DefaultExtensionMethodImportCompletionCacheServiceFactory.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/DefaultExtensionMethodImportCompletionCacheServiceFactory.cs @@ -8,16 +8,15 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.TestHooks; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +/// +/// We don't use PE cache from the service, so just pass in type `object` for PE entries. +/// +[ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Default), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class DefaultExtensionMethodImportCompletionCacheServiceFactory(IAsynchronousOperationListenerProvider listenerProvider) + : AbstractImportCompletionCacheServiceFactory(listenerProvider, ExtensionMethodImportCompletionHelper.BatchUpdateCacheAsync, CancellationToken.None) { - /// - /// We don't use PE cache from the service, so just pass in type `object` for PE entries. - /// - [ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Default), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class DefaultExtensionMethodImportCompletionCacheServiceFactory(IAsynchronousOperationListenerProvider listenerProvider) - : AbstractImportCompletionCacheServiceFactory(listenerProvider, ExtensionMethodImportCompletionHelper.BatchUpdateCacheAsync, CancellationToken.None) - { - } } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/DefaultTypeImportCompletionCacheServiceFactory.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/DefaultTypeImportCompletionCacheServiceFactory.cs index d02382f02dc16..6fc678e011184 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/DefaultTypeImportCompletionCacheServiceFactory.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/DefaultTypeImportCompletionCacheServiceFactory.cs @@ -8,13 +8,12 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.TestHooks; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +[ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Default), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class DefaultTypeImportCompletionCacheServiceFactory(IAsynchronousOperationListenerProvider listenerProvider) + : AbstractImportCompletionCacheServiceFactory(listenerProvider, AbstractTypeImportCompletionService.BatchUpdateCacheAsync, CancellationToken.None) { - [ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Default), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class DefaultTypeImportCompletionCacheServiceFactory(IAsynchronousOperationListenerProvider listenerProvider) - : AbstractImportCompletionCacheServiceFactory(listenerProvider, AbstractTypeImportCompletionService.BatchUpdateCacheAsync, CancellationToken.None) - { - } } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionCacheEntry.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionCacheEntry.cs index 4afea26df40d1..39391a053439b 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionCacheEntry.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionCacheEntry.cs @@ -6,53 +6,52 @@ using Microsoft.CodeAnalysis.FindSymbols; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal readonly struct ExtensionMethodImportCompletionCacheEntry { - internal readonly struct ExtensionMethodImportCompletionCacheEntry + public Checksum Checksum { get; } + public string Language { get; } + + /// + /// Mapping from the name of receiver type to extension method symbol infos. + /// + public readonly MultiDictionary ReceiverTypeNameToExtensionMethodMap { get; } + + public bool ContainsExtensionMethod => !ReceiverTypeNameToExtensionMethodMap.IsEmpty; + + private ExtensionMethodImportCompletionCacheEntry( + Checksum checksum, + string language, + MultiDictionary receiverTypeNameToExtensionMethodMap) { - public Checksum Checksum { get; } - public string Language { get; } + Checksum = checksum; + Language = language; + ReceiverTypeNameToExtensionMethodMap = receiverTypeNameToExtensionMethodMap; + } - /// - /// Mapping from the name of receiver type to extension method symbol infos. - /// - public readonly MultiDictionary ReceiverTypeNameToExtensionMethodMap { get; } + public class Builder(Checksum checksum, string langauge, IEqualityComparer comparer) + { + private readonly Checksum _checksum = checksum; + private readonly string _language = langauge; - public bool ContainsExtensionMethod => !ReceiverTypeNameToExtensionMethodMap.IsEmpty; + private readonly MultiDictionary _mapBuilder = new MultiDictionary(comparer); - private ExtensionMethodImportCompletionCacheEntry( - Checksum checksum, - string language, - MultiDictionary receiverTypeNameToExtensionMethodMap) + public ExtensionMethodImportCompletionCacheEntry ToCacheEntry() { - Checksum = checksum; - Language = language; - ReceiverTypeNameToExtensionMethodMap = receiverTypeNameToExtensionMethodMap; + return new ExtensionMethodImportCompletionCacheEntry( + _checksum, + _language, + _mapBuilder); } - public class Builder(Checksum checksum, string langauge, IEqualityComparer comparer) + public void AddItem(TopLevelSyntaxTreeIndex syntaxIndex) { - private readonly Checksum _checksum = checksum; - private readonly string _language = langauge; - - private readonly MultiDictionary _mapBuilder = new MultiDictionary(comparer); - - public ExtensionMethodImportCompletionCacheEntry ToCacheEntry() - { - return new ExtensionMethodImportCompletionCacheEntry( - _checksum, - _language, - _mapBuilder); - } - - public void AddItem(TopLevelSyntaxTreeIndex syntaxIndex) + foreach (var (receiverType, symbolInfoIndices) in syntaxIndex.ReceiverTypeNameToExtensionMethodMap) { - foreach (var (receiverType, symbolInfoIndices) in syntaxIndex.ReceiverTypeNameToExtensionMethodMap) + foreach (var index in symbolInfoIndices) { - foreach (var index in symbolInfoIndices) - { - _mapBuilder.Add(receiverType, syntaxIndex.DeclaredSymbolInfos[index]); - } + _mapBuilder.Add(receiverType, syntaxIndex.DeclaredSymbolInfos[index]); } } } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.SymbolComputer.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.SymbolComputer.cs index c81482b5044ad..28b81bce83aa4 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.SymbolComputer.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.SymbolComputer.cs @@ -14,538 +14,537 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal static partial class ExtensionMethodImportCompletionHelper { - internal static partial class ExtensionMethodImportCompletionHelper + private class SymbolComputer { - private class SymbolComputer + private readonly int _position; + private readonly Document _originatingDocument; + + /// + /// The semantic model provided for . This may be a speculative semantic + /// model with limited validity based on the context surrounding . + /// + private readonly SemanticModel _originatingSemanticModel; + private readonly ITypeSymbol _receiverTypeSymbol; + private readonly ImmutableArray _receiverTypeNames; + private readonly ISet _namespaceInScope; + private readonly IImportCompletionCacheService _cacheService; + + // This dictionary is used as cache among all projects and PE references. + // The key is the receiver type as in the extension method declaration (symbol retrived from originating compilation). + // The value indicates if we can reduce an extension method with this receiver type given receiver type. + private readonly ConcurrentDictionary _checkedReceiverTypes = []; + + public SymbolComputer( + Document document, + SemanticModel semanticModel, + ITypeSymbol receiverTypeSymbol, + int position, + ISet namespaceInScope) { - private readonly int _position; - private readonly Document _originatingDocument; - - /// - /// The semantic model provided for . This may be a speculative semantic - /// model with limited validity based on the context surrounding . - /// - private readonly SemanticModel _originatingSemanticModel; - private readonly ITypeSymbol _receiverTypeSymbol; - private readonly ImmutableArray _receiverTypeNames; - private readonly ISet _namespaceInScope; - private readonly IImportCompletionCacheService _cacheService; - - // This dictionary is used as cache among all projects and PE references. - // The key is the receiver type as in the extension method declaration (symbol retrived from originating compilation). - // The value indicates if we can reduce an extension method with this receiver type given receiver type. - private readonly ConcurrentDictionary _checkedReceiverTypes = []; - - public SymbolComputer( - Document document, - SemanticModel semanticModel, - ITypeSymbol receiverTypeSymbol, - int position, - ISet namespaceInScope) - { - _originatingDocument = document; - _originatingSemanticModel = semanticModel; - _receiverTypeSymbol = receiverTypeSymbol; - _position = position; - _namespaceInScope = namespaceInScope; - - var receiverTypeNames = GetReceiverTypeNames(receiverTypeSymbol); - _receiverTypeNames = AddComplexTypes(receiverTypeNames); - _cacheService = GetCacheService(document.Project); - } + _originatingDocument = document; + _originatingSemanticModel = semanticModel; + _receiverTypeSymbol = receiverTypeSymbol; + _position = position; + _namespaceInScope = namespaceInScope; + + var receiverTypeNames = GetReceiverTypeNames(receiverTypeSymbol); + _receiverTypeNames = AddComplexTypes(receiverTypeNames); + _cacheService = GetCacheService(document.Project); + } - private static IImportCompletionCacheService GetCacheService(Project project) - => project.Solution.Services.GetRequiredService>(); + private static IImportCompletionCacheService GetCacheService(Project project) + => project.Solution.Services.GetRequiredService>(); - private static string? GetPEReferenceCacheKey(PortableExecutableReference peReference) - => peReference.FilePath ?? peReference.Display; + private static string? GetPEReferenceCacheKey(PortableExecutableReference peReference) + => peReference.FilePath ?? peReference.Display; - /// - /// Force create/update all relevant indices - /// - public static void QueueCacheWarmUpTask(Project project) - { - GetCacheService(project).WorkQueue.AddWork(project); - } + /// + /// Force create/update all relevant indices + /// + public static void QueueCacheWarmUpTask(Project project) + { + GetCacheService(project).WorkQueue.AddWork(project); + } - public static async ValueTask UpdateCacheAsync(Project project, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var cacheService = GetCacheService(project); + public static async ValueTask UpdateCacheAsync(Project project, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var cacheService = GetCacheService(project); - foreach (var relevantProject in GetAllRelevantProjects(project)) - await GetUpToDateCacheEntryAsync(relevantProject, cacheService, cancellationToken).ConfigureAwait(false); + foreach (var relevantProject in GetAllRelevantProjects(project)) + await GetUpToDateCacheEntryAsync(relevantProject, cacheService, cancellationToken).ConfigureAwait(false); - foreach (var peReference in GetAllRelevantPeReferences(project)) - await SymbolTreeInfo.GetInfoForMetadataReferenceAsync(project.Solution, peReference, checksum: null, cancellationToken).ConfigureAwait(false); - } + foreach (var peReference in GetAllRelevantPeReferences(project)) + await SymbolTreeInfo.GetInfoForMetadataReferenceAsync(project.Solution, peReference, checksum: null, cancellationToken).ConfigureAwait(false); + } - public async Task<(ImmutableArray symbols, bool isPartialResult)> GetExtensionMethodSymbolsAsync(bool forceCacheCreation, bool hideAdvancedMembers, CancellationToken cancellationToken) + public async Task<(ImmutableArray symbols, bool isPartialResult)> GetExtensionMethodSymbolsAsync(bool forceCacheCreation, bool hideAdvancedMembers, CancellationToken cancellationToken) + { + try { - try + // Find applicable symbols in parallel + using var _1 = ArrayBuilder?>>.GetInstance(out var tasks); + + foreach (var peReference in GetAllRelevantPeReferences(_originatingDocument.Project)) { - // Find applicable symbols in parallel - using var _1 = ArrayBuilder?>>.GetInstance(out var tasks); + tasks.Add(Task.Run(() => GetExtensionMethodSymbolsFromPeReferenceAsync( + peReference, + forceCacheCreation, + cancellationToken).AsTask(), cancellationToken)); + } - foreach (var peReference in GetAllRelevantPeReferences(_originatingDocument.Project)) - { - tasks.Add(Task.Run(() => GetExtensionMethodSymbolsFromPeReferenceAsync( - peReference, - forceCacheCreation, - cancellationToken).AsTask(), cancellationToken)); - } + foreach (var project in GetAllRelevantProjects(_originatingDocument.Project)) + { + tasks.Add(Task.Run(() => GetExtensionMethodSymbolsFromProjectAsync( + project, + forceCacheCreation, + cancellationToken), cancellationToken)); + } + + using var _2 = ArrayBuilder.GetInstance(out var symbols); + var isPartialResult = false; - foreach (var project in GetAllRelevantProjects(_originatingDocument.Project)) + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + + foreach (var result in results) + { + // `null` indicates we don't have the index ready for the corresponding project/PE. + // returns what we have even it means we only show partial results. + if (result == null) { - tasks.Add(Task.Run(() => GetExtensionMethodSymbolsFromProjectAsync( - project, - forceCacheCreation, - cancellationToken), cancellationToken)); + isPartialResult = true; + continue; } - using var _2 = ArrayBuilder.GetInstance(out var symbols); - var isPartialResult = false; + symbols.AddRange(result); + } - var results = await Task.WhenAll(tasks).ConfigureAwait(false); + var browsableSymbols = symbols.ToImmutable() + .FilterToVisibleAndBrowsableSymbols(hideAdvancedMembers, _originatingSemanticModel.Compilation); - foreach (var result in results) - { - // `null` indicates we don't have the index ready for the corresponding project/PE. - // returns what we have even it means we only show partial results. - if (result == null) - { - isPartialResult = true; - continue; - } + return (browsableSymbols, isPartialResult); + } + finally + { + // If we are not force creating/updating the cache, an update task needs to be queued in background. + if (!forceCacheCreation) + GetCacheService(_originatingDocument.Project).WorkQueue.AddWork(_originatingDocument.Project); + } + } - symbols.AddRange(result); - } + // Returns all referenced projects and originating project itself. + private static ImmutableArray GetAllRelevantProjects(Project project) + { + var graph = project.Solution.GetProjectDependencyGraph(); + var relevantProjectIds = graph.GetProjectsThatThisProjectTransitivelyDependsOn(project.Id).Concat(project.Id); + return relevantProjectIds.Select(project.Solution.GetRequiredProject).Where(p => p.SupportsCompilation).ToImmutableArray(); + } - var browsableSymbols = symbols.ToImmutable() - .FilterToVisibleAndBrowsableSymbols(hideAdvancedMembers, _originatingSemanticModel.Compilation); + // Returns all PEs referenced by originating project. + private static ImmutableArray GetAllRelevantPeReferences(Project project) + => project.MetadataReferences.OfType().ToImmutableArray(); - return (browsableSymbols, isPartialResult); - } - finally - { - // If we are not force creating/updating the cache, an update task needs to be queued in background. - if (!forceCacheCreation) - GetCacheService(_originatingDocument.Project).WorkQueue.AddWork(_originatingDocument.Project); - } + private async Task?> GetExtensionMethodSymbolsFromProjectAsync( + Project project, + bool forceCacheCreation, + CancellationToken cancellationToken) + { + ExtensionMethodImportCompletionCacheEntry cacheEntry; + if (forceCacheCreation) + { + cacheEntry = await GetUpToDateCacheEntryAsync(project, _cacheService, cancellationToken).ConfigureAwait(false); } - - // Returns all referenced projects and originating project itself. - private static ImmutableArray GetAllRelevantProjects(Project project) + else if (!_cacheService.ProjectItemsCache.TryGetValue(project.Id, out cacheEntry)) { - var graph = project.Solution.GetProjectDependencyGraph(); - var relevantProjectIds = graph.GetProjectsThatThisProjectTransitivelyDependsOn(project.Id).Concat(project.Id); - return relevantProjectIds.Select(project.Solution.GetRequiredProject).Where(p => p.SupportsCompilation).ToImmutableArray(); + // Use cached data if available, even checksum doesn't match. otherwise, returns null indicating cache not ready. + return null; } - // Returns all PEs referenced by originating project. - private static ImmutableArray GetAllRelevantPeReferences(Project project) - => project.MetadataReferences.OfType().ToImmutableArray(); - - private async Task?> GetExtensionMethodSymbolsFromProjectAsync( - Project project, - bool forceCacheCreation, - CancellationToken cancellationToken) + if (!cacheEntry.ContainsExtensionMethod) { - ExtensionMethodImportCompletionCacheEntry cacheEntry; - if (forceCacheCreation) - { - cacheEntry = await GetUpToDateCacheEntryAsync(project, _cacheService, cancellationToken).ConfigureAwait(false); - } - else if (!_cacheService.ProjectItemsCache.TryGetValue(project.Id, out cacheEntry)) - { - // Use cached data if available, even checksum doesn't match. otherwise, returns null indicating cache not ready. - return null; - } + return ImmutableArray.Empty; + } - if (!cacheEntry.ContainsExtensionMethod) - { - return ImmutableArray.Empty; - } + var originatingAssembly = _originatingSemanticModel.Compilation.Assembly; + var filter = CreateAggregatedFilter(cacheEntry); - var originatingAssembly = _originatingSemanticModel.Compilation.Assembly; - var filter = CreateAggregatedFilter(cacheEntry); + // Avoid recalculating a compilation for the originating document, particularly for the case where the + // provided semantic model is a speculative semantic model. + var compilation = project == _originatingDocument.Project + ? _originatingSemanticModel.Compilation + : await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var assembly = compilation.Assembly; + var internalsVisible = originatingAssembly.IsSameAssemblyOrHasFriendAccessTo(assembly); - // Avoid recalculating a compilation for the originating document, particularly for the case where the - // provided semantic model is a speculative semantic model. - var compilation = project == _originatingDocument.Project - ? _originatingSemanticModel.Compilation - : await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var assembly = compilation.Assembly; - var internalsVisible = originatingAssembly.IsSameAssemblyOrHasFriendAccessTo(assembly); + var matchingMethodSymbols = GetPotentialMatchingSymbolsFromAssembly( + compilation.Assembly, filter, internalsVisible, cancellationToken); - var matchingMethodSymbols = GetPotentialMatchingSymbolsFromAssembly( - compilation.Assembly, filter, internalsVisible, cancellationToken); + return project == _originatingDocument.Project + ? GetExtensionMethodsForSymbolsFromSameCompilation(matchingMethodSymbols, cancellationToken) + : GetExtensionMethodsForSymbolsFromDifferentCompilation(matchingMethodSymbols, cancellationToken); + } - return project == _originatingDocument.Project - ? GetExtensionMethodsForSymbolsFromSameCompilation(matchingMethodSymbols, cancellationToken) - : GetExtensionMethodsForSymbolsFromDifferentCompilation(matchingMethodSymbols, cancellationToken); + private async ValueTask?> GetExtensionMethodSymbolsFromPeReferenceAsync( + PortableExecutableReference peReference, + bool forceCacheCreation, + CancellationToken cancellationToken) + { + SymbolTreeInfo? symbolInfo; + if (forceCacheCreation) + { + symbolInfo = await SymbolTreeInfo.GetInfoForMetadataReferenceAsync( + _originatingDocument.Project.Solution, peReference, checksum: null, cancellationToken).ConfigureAwait(false); } - - private async ValueTask?> GetExtensionMethodSymbolsFromPeReferenceAsync( - PortableExecutableReference peReference, - bool forceCacheCreation, - CancellationToken cancellationToken) + else { - SymbolTreeInfo? symbolInfo; - if (forceCacheCreation) + var cachedInfoTask = SymbolTreeInfo.TryGetCachedInfoForMetadataReferenceIgnoreChecksumAsync(peReference, cancellationToken); + if (cachedInfoTask.IsCompleted) { - symbolInfo = await SymbolTreeInfo.GetInfoForMetadataReferenceAsync( - _originatingDocument.Project.Solution, peReference, checksum: null, cancellationToken).ConfigureAwait(false); + // Use cached data if available, even checksum doesn't match. We will update the cache in the background. + symbolInfo = await cachedInfoTask.ConfigureAwait(false); } else { - var cachedInfoTask = SymbolTreeInfo.TryGetCachedInfoForMetadataReferenceIgnoreChecksumAsync(peReference, cancellationToken); - if (cachedInfoTask.IsCompleted) - { - // Use cached data if available, even checksum doesn't match. We will update the cache in the background. - symbolInfo = await cachedInfoTask.ConfigureAwait(false); - } - else - { - // No cached data immediately available, returns null to indicate index not ready - return null; - } + // No cached data immediately available, returns null to indicate index not ready + return null; } + } - if (symbolInfo is null || - !symbolInfo.ContainsExtensionMethod || - _originatingSemanticModel.Compilation.GetAssemblyOrModuleSymbol(peReference) is not IAssemblySymbol assembly) - { - return ImmutableArray.Empty; - } + if (symbolInfo is null || + !symbolInfo.ContainsExtensionMethod || + _originatingSemanticModel.Compilation.GetAssemblyOrModuleSymbol(peReference) is not IAssemblySymbol assembly) + { + return ImmutableArray.Empty; + } - var filter = CreateAggregatedFilter(symbolInfo); - var internalsVisible = _originatingSemanticModel.Compilation.Assembly.IsSameAssemblyOrHasFriendAccessTo(assembly); + var filter = CreateAggregatedFilter(symbolInfo); + var internalsVisible = _originatingSemanticModel.Compilation.Assembly.IsSameAssemblyOrHasFriendAccessTo(assembly); - var matchingMethodSymbols = GetPotentialMatchingSymbolsFromAssembly(assembly, filter, internalsVisible, cancellationToken); + var matchingMethodSymbols = GetPotentialMatchingSymbolsFromAssembly(assembly, filter, internalsVisible, cancellationToken); - return GetExtensionMethodsForSymbolsFromSameCompilation(matchingMethodSymbols, cancellationToken); - } + return GetExtensionMethodsForSymbolsFromSameCompilation(matchingMethodSymbols, cancellationToken); + } + + private ImmutableArray GetExtensionMethodsForSymbolsFromDifferentCompilation( + MultiDictionary matchingMethodSymbols, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var builder); - private ImmutableArray GetExtensionMethodsForSymbolsFromDifferentCompilation( - MultiDictionary matchingMethodSymbols, - CancellationToken cancellationToken) + // Matching extension method symbols are grouped based on their receiver type. + foreach (var (declaredReceiverType, methodSymbols) in matchingMethodSymbols) { - using var _ = ArrayBuilder.GetInstance(out var builder); + cancellationToken.ThrowIfCancellationRequested(); - // Matching extension method symbols are grouped based on their receiver type. - foreach (var (declaredReceiverType, methodSymbols) in matchingMethodSymbols) + var declaredReceiverTypeInOriginatingCompilation = SymbolFinder.FindSimilarSymbols(declaredReceiverType, _originatingSemanticModel.Compilation, cancellationToken).FirstOrDefault(); + if (declaredReceiverTypeInOriginatingCompilation == null) { - cancellationToken.ThrowIfCancellationRequested(); + // Bug: https://github.com/dotnet/roslyn/issues/45404 + // SymbolFinder.FindSimilarSymbols would fail if originating and referenced compilation targeting different frameworks say net472 and netstandard respectively. + // Here's SymbolKey for System.String from those two framework as an example: + // + // {1 (D "String" (N "System" 0 (N "" 0 (U (S "netstandard" 4) 3) 2) 1) 0 0 (% 0) 0)} + // {1 (D "String" (N "System" 0 (N "" 0 (U (S "mscorlib" 4) 3) 2) 1) 0 0 (% 0) 0)} + // + // Also we don't use the "ignoreAssemblyKey" option for SymbolKey resolution because its perfermance doesn't meet our requirement. + continue; + } - var declaredReceiverTypeInOriginatingCompilation = SymbolFinder.FindSimilarSymbols(declaredReceiverType, _originatingSemanticModel.Compilation, cancellationToken).FirstOrDefault(); - if (declaredReceiverTypeInOriginatingCompilation == null) - { - // Bug: https://github.com/dotnet/roslyn/issues/45404 - // SymbolFinder.FindSimilarSymbols would fail if originating and referenced compilation targeting different frameworks say net472 and netstandard respectively. - // Here's SymbolKey for System.String from those two framework as an example: - // - // {1 (D "String" (N "System" 0 (N "" 0 (U (S "netstandard" 4) 3) 2) 1) 0 0 (% 0) 0)} - // {1 (D "String" (N "System" 0 (N "" 0 (U (S "mscorlib" 4) 3) 2) 1) 0 0 (% 0) 0)} - // - // Also we don't use the "ignoreAssemblyKey" option for SymbolKey resolution because its perfermance doesn't meet our requirement. - continue; - } + if (_checkedReceiverTypes.TryGetValue(declaredReceiverTypeInOriginatingCompilation, out var cachedResult) && !cachedResult) + { + // If we already checked an extension method with same receiver type before, and we know it can't be applied + // to the receiverTypeSymbol, then no need to proceed methods from this group.. + continue; + } - if (_checkedReceiverTypes.TryGetValue(declaredReceiverTypeInOriginatingCompilation, out var cachedResult) && !cachedResult) + // This is also affected by the symbol resolving issue mentioned above, which means in case referenced projects + // are targeting different framework, we will miss extension methods with any framework type in their signature from those projects. + var isFirstMethod = true; + foreach (var methodInOriginatingCompilation in methodSymbols.Select(s => SymbolFinder.FindSimilarSymbols(s, _originatingSemanticModel.Compilation).FirstOrDefault()).WhereNotNull()) + { + if (isFirstMethod) { - // If we already checked an extension method with same receiver type before, and we know it can't be applied - // to the receiverTypeSymbol, then no need to proceed methods from this group.. - continue; - } + isFirstMethod = false; - // This is also affected by the symbol resolving issue mentioned above, which means in case referenced projects - // are targeting different framework, we will miss extension methods with any framework type in their signature from those projects. - var isFirstMethod = true; - foreach (var methodInOriginatingCompilation in methodSymbols.Select(s => SymbolFinder.FindSimilarSymbols(s, _originatingSemanticModel.Compilation).FirstOrDefault()).WhereNotNull()) - { - if (isFirstMethod) + // We haven't seen this receiver type yet. Try to check by reducing one extension method + // to the given receiver type and save the result. + if (!cachedResult) { - isFirstMethod = false; - - // We haven't seen this receiver type yet. Try to check by reducing one extension method - // to the given receiver type and save the result. + // If this is the first symbol we retrived from originating compilation, + // try to check if we can apply it to given receiver type, and save result to our cache. + // Since method symbols are grouped by their declared receiver type, they are either all matches to the receiver type + // or all mismatches. So we only need to call ReduceExtensionMethod on one of them. + var reducedMethodSymbol = methodInOriginatingCompilation.ReduceExtensionMethod(_receiverTypeSymbol); + cachedResult = reducedMethodSymbol != null; + _checkedReceiverTypes[declaredReceiverTypeInOriginatingCompilation] = cachedResult; + + // Now, cachedResult being false means method doesn't match the receiver type, + // stop processing methods from this group. if (!cachedResult) { - // If this is the first symbol we retrived from originating compilation, - // try to check if we can apply it to given receiver type, and save result to our cache. - // Since method symbols are grouped by their declared receiver type, they are either all matches to the receiver type - // or all mismatches. So we only need to call ReduceExtensionMethod on one of them. - var reducedMethodSymbol = methodInOriginatingCompilation.ReduceExtensionMethod(_receiverTypeSymbol); - cachedResult = reducedMethodSymbol != null; - _checkedReceiverTypes[declaredReceiverTypeInOriginatingCompilation] = cachedResult; - - // Now, cachedResult being false means method doesn't match the receiver type, - // stop processing methods from this group. - if (!cachedResult) - { - break; - } + break; } } + } - if (_originatingSemanticModel.IsAccessible(_position, methodInOriginatingCompilation)) - { - builder.Add(methodInOriginatingCompilation); - } + if (_originatingSemanticModel.IsAccessible(_position, methodInOriginatingCompilation)) + { + builder.Add(methodInOriginatingCompilation); } } - - return builder.ToImmutable(); } - private ImmutableArray GetExtensionMethodsForSymbolsFromSameCompilation( - MultiDictionary matchingMethodSymbols, - CancellationToken cancellationToken) + return builder.ToImmutable(); + } + + private ImmutableArray GetExtensionMethodsForSymbolsFromSameCompilation( + MultiDictionary matchingMethodSymbols, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var builder); + + // Matching extension method symbols are grouped based on their receiver type. + foreach (var (receiverType, methodSymbols) in matchingMethodSymbols) { - using var _ = ArrayBuilder.GetInstance(out var builder); + cancellationToken.ThrowIfCancellationRequested(); - // Matching extension method symbols are grouped based on their receiver type. - foreach (var (receiverType, methodSymbols) in matchingMethodSymbols) + // If we already checked an extension method with same receiver type before, and we know it can't be applied + // to the receiverTypeSymbol, then no need to proceed further. + if (_checkedReceiverTypes.TryGetValue(receiverType, out var cachedResult) && !cachedResult) { - cancellationToken.ThrowIfCancellationRequested(); - - // If we already checked an extension method with same receiver type before, and we know it can't be applied - // to the receiverTypeSymbol, then no need to proceed further. - if (_checkedReceiverTypes.TryGetValue(receiverType, out var cachedResult) && !cachedResult) - { - continue; - } + continue; + } - // We haven't seen this type yet. Try to check by reducing one extension method - // to the given receiver type and save the result. - if (!cachedResult) - { - var reducedMethodSymbol = methodSymbols.First().ReduceExtensionMethod(_receiverTypeSymbol); - cachedResult = reducedMethodSymbol != null; - _checkedReceiverTypes[receiverType] = cachedResult; - } + // We haven't seen this type yet. Try to check by reducing one extension method + // to the given receiver type and save the result. + if (!cachedResult) + { + var reducedMethodSymbol = methodSymbols.First().ReduceExtensionMethod(_receiverTypeSymbol); + cachedResult = reducedMethodSymbol != null; + _checkedReceiverTypes[receiverType] = cachedResult; + } - // Receiver type matches the receiver type of the extension method declaration. - // We can add accessible ones to the item builder. - if (cachedResult) + // Receiver type matches the receiver type of the extension method declaration. + // We can add accessible ones to the item builder. + if (cachedResult) + { + foreach (var methodSymbol in methodSymbols) { - foreach (var methodSymbol in methodSymbols) + if (_originatingSemanticModel.IsAccessible(_position, methodSymbol)) { - if (_originatingSemanticModel.IsAccessible(_position, methodSymbol)) - { - builder.Add(methodSymbol); - } + builder.Add(methodSymbol); } } } - - return builder.ToImmutable(); } - private MultiDictionary GetPotentialMatchingSymbolsFromAssembly( - IAssemblySymbol assembly, - MultiDictionary extensionMethodFilter, - bool internalsVisible, - CancellationToken cancellationToken) + return builder.ToImmutable(); + } + + private MultiDictionary GetPotentialMatchingSymbolsFromAssembly( + IAssemblySymbol assembly, + MultiDictionary extensionMethodFilter, + bool internalsVisible, + CancellationToken cancellationToken) + { + var builder = new MultiDictionary(); + + // The filter contains all the extension methods that potentially match the receiver type. + // We use it as a guide to selectively retrive container and member symbols from the assembly. + foreach (var (fullyQualifiedContainerName, methodInfo) in extensionMethodFilter) { - var builder = new MultiDictionary(); + // First try to filter out types from already imported namespaces + var indexOfLastDot = fullyQualifiedContainerName.LastIndexOf('.'); + var qualifiedNamespaceName = indexOfLastDot > 0 ? fullyQualifiedContainerName[..indexOfLastDot] : string.Empty; - // The filter contains all the extension methods that potentially match the receiver type. - // We use it as a guide to selectively retrive container and member symbols from the assembly. - foreach (var (fullyQualifiedContainerName, methodInfo) in extensionMethodFilter) + if (_namespaceInScope.Contains(qualifiedNamespaceName)) { - // First try to filter out types from already imported namespaces - var indexOfLastDot = fullyQualifiedContainerName.LastIndexOf('.'); - var qualifiedNamespaceName = indexOfLastDot > 0 ? fullyQualifiedContainerName[..indexOfLastDot] : string.Empty; - - if (_namespaceInScope.Contains(qualifiedNamespaceName)) - { - continue; - } + continue; + } - // Container of extension method (static class in C# and Module in VB) can't be generic or nested. - var containerSymbol = assembly.GetTypeByMetadataName(fullyQualifiedContainerName); + // Container of extension method (static class in C# and Module in VB) can't be generic or nested. + var containerSymbol = assembly.GetTypeByMetadataName(fullyQualifiedContainerName); - if (containerSymbol == null - || !containerSymbol.MightContainExtensionMethods - || !IsAccessible(containerSymbol, internalsVisible)) - { - continue; - } + if (containerSymbol == null + || !containerSymbol.MightContainExtensionMethods + || !IsAccessible(containerSymbol, internalsVisible)) + { + continue; + } - // Now we have the container symbol, first try to get member extension method symbols that matches our syntactic filter, - // then further check if those symbols matches semantically. - foreach (var (methodName, receiverTypeName) in methodInfo) - { - cancellationToken.ThrowIfCancellationRequested(); + // Now we have the container symbol, first try to get member extension method symbols that matches our syntactic filter, + // then further check if those symbols matches semantically. + foreach (var (methodName, receiverTypeName) in methodInfo) + { + cancellationToken.ThrowIfCancellationRequested(); - var methodSymbols = containerSymbol.GetMembers(methodName).OfType(); + var methodSymbols = containerSymbol.GetMembers(methodName).OfType(); - foreach (var methodSymbol in methodSymbols) + foreach (var methodSymbol in methodSymbols) + { + if (MatchExtensionMethod(methodSymbol, receiverTypeName, internalsVisible, out var receiverType)) { - if (MatchExtensionMethod(methodSymbol, receiverTypeName, internalsVisible, out var receiverType)) - { - // Find a potential match. - builder.Add(receiverType!, methodSymbol); - } + // Find a potential match. + builder.Add(receiverType!, methodSymbol); } } } + } - return builder; + return builder; - static bool MatchExtensionMethod(IMethodSymbol method, string filterReceiverTypeName, bool internalsVisible, out ITypeSymbol? receiverType) + static bool MatchExtensionMethod(IMethodSymbol method, string filterReceiverTypeName, bool internalsVisible, out ITypeSymbol? receiverType) + { + receiverType = null; + if (!method.IsExtensionMethod || method.Parameters.IsEmpty || !IsAccessible(method, internalsVisible)) { - receiverType = null; - if (!method.IsExtensionMethod || method.Parameters.IsEmpty || !IsAccessible(method, internalsVisible)) - { - return false; - } - - // We get a match if the receiver type name match. - // For complex type, we would check if it matches with filter on whether it's an array. - if (filterReceiverTypeName.Length > 0 && !string.Equals(filterReceiverTypeName, GetReceiverTypeName(method.Parameters[0].Type), StringComparison.Ordinal)) - { - return false; - } + return false; + } - receiverType = method.Parameters[0].Type; - return true; + // We get a match if the receiver type name match. + // For complex type, we would check if it matches with filter on whether it's an array. + if (filterReceiverTypeName.Length > 0 && !string.Equals(filterReceiverTypeName, GetReceiverTypeName(method.Parameters[0].Type), StringComparison.Ordinal)) + { + return false; } - // An quick accessibility check based on declared accessibility only, a semantic based check is still required later. - // Since we are dealing with extension methods and their container (top level static class and modules), only public, - // internal and private modifiers are in play here. - // Also, this check is called for a method symbol only when the container was checked and is accessible. - static bool IsAccessible(ISymbol symbol, bool internalsVisible) - => symbol.DeclaredAccessibility == Accessibility.Public || - (symbol.DeclaredAccessibility == Accessibility.Internal && internalsVisible); + receiverType = method.Parameters[0].Type; + return true; } - /// - /// Create a filter for extension methods from source. - /// The filter is a map from fully qualified type name to info of extension methods it contains. - /// - private MultiDictionary CreateAggregatedFilter(ExtensionMethodImportCompletionCacheEntry syntaxIndex) - { - var results = new MultiDictionary(); + // An quick accessibility check based on declared accessibility only, a semantic based check is still required later. + // Since we are dealing with extension methods and their container (top level static class and modules), only public, + // internal and private modifiers are in play here. + // Also, this check is called for a method symbol only when the container was checked and is accessible. + static bool IsAccessible(ISymbol symbol, bool internalsVisible) + => symbol.DeclaredAccessibility == Accessibility.Public || + (symbol.DeclaredAccessibility == Accessibility.Internal && internalsVisible); + } - foreach (var receiverTypeName in _receiverTypeNames) - { - var methodInfos = syntaxIndex.ReceiverTypeNameToExtensionMethodMap[receiverTypeName]; - if (methodInfos.Count == 0) - { - continue; - } + /// + /// Create a filter for extension methods from source. + /// The filter is a map from fully qualified type name to info of extension methods it contains. + /// + private MultiDictionary CreateAggregatedFilter(ExtensionMethodImportCompletionCacheEntry syntaxIndex) + { + var results = new MultiDictionary(); - foreach (var methodInfo in methodInfos) - { - results.Add(methodInfo.FullyQualifiedContainerName, (methodInfo.Name, receiverTypeName)); - } + foreach (var receiverTypeName in _receiverTypeNames) + { + var methodInfos = syntaxIndex.ReceiverTypeNameToExtensionMethodMap[receiverTypeName]; + if (methodInfos.Count == 0) + { + continue; } - return results; + foreach (var methodInfo in methodInfos) + { + results.Add(methodInfo.FullyQualifiedContainerName, (methodInfo.Name, receiverTypeName)); + } } - /// - /// Create filter for extension methods from metadata - /// The filter is a map from fully qualified type name to info of extension methods it contains. - /// - private MultiDictionary CreateAggregatedFilter(SymbolTreeInfo symbolInfo) - { - var results = new MultiDictionary(); + return results; + } - foreach (var receiverTypeName in _receiverTypeNames) - { - var methodInfos = symbolInfo.GetExtensionMethodInfoForReceiverType(receiverTypeName); - if (methodInfos.Count == 0) - { - continue; - } + /// + /// Create filter for extension methods from metadata + /// The filter is a map from fully qualified type name to info of extension methods it contains. + /// + private MultiDictionary CreateAggregatedFilter(SymbolTreeInfo symbolInfo) + { + var results = new MultiDictionary(); - foreach (var methodInfo in methodInfos) - { - results.Add(methodInfo.FullyQualifiedContainerName, (methodInfo.Name, receiverTypeName)); - } + foreach (var receiverTypeName in _receiverTypeNames) + { + var methodInfos = symbolInfo.GetExtensionMethodInfoForReceiverType(receiverTypeName); + if (methodInfos.Count == 0) + { + continue; } - return results; + foreach (var methodInfo in methodInfos) + { + results.Add(methodInfo.FullyQualifiedContainerName, (methodInfo.Name, receiverTypeName)); + } } - /// - /// Get the metadata name of all the base types and interfaces this type derived from. - /// - private static ImmutableArray GetReceiverTypeNames(ITypeSymbol receiverTypeSymbol) - { - using var _ = PooledHashSet.GetInstance(out var allTypeNamesBuilder); - AddNamesForTypeWorker(receiverTypeSymbol, allTypeNamesBuilder); - return allTypeNamesBuilder.ToImmutableArray(); + return results; + } - static void AddNamesForTypeWorker(ITypeSymbol receiverTypeSymbol, PooledHashSet builder) + /// + /// Get the metadata name of all the base types and interfaces this type derived from. + /// + private static ImmutableArray GetReceiverTypeNames(ITypeSymbol receiverTypeSymbol) + { + using var _ = PooledHashSet.GetInstance(out var allTypeNamesBuilder); + AddNamesForTypeWorker(receiverTypeSymbol, allTypeNamesBuilder); + return allTypeNamesBuilder.ToImmutableArray(); + + static void AddNamesForTypeWorker(ITypeSymbol receiverTypeSymbol, PooledHashSet builder) + { + if (receiverTypeSymbol is ITypeParameterSymbol typeParameter) { - if (receiverTypeSymbol is ITypeParameterSymbol typeParameter) + foreach (var constraintType in typeParameter.ConstraintTypes) { - foreach (var constraintType in typeParameter.ConstraintTypes) - { - AddNamesForTypeWorker(constraintType, builder); - } + AddNamesForTypeWorker(constraintType, builder); } - else - { - builder.Add(GetReceiverTypeName(receiverTypeSymbol)); - builder.AddRange(receiverTypeSymbol.GetBaseTypes().Select(t => t.MetadataName)); - builder.AddRange(receiverTypeSymbol.GetAllInterfacesIncludingThis().Select(t => t.MetadataName)); + } + else + { + builder.Add(GetReceiverTypeName(receiverTypeSymbol)); + builder.AddRange(receiverTypeSymbol.GetBaseTypes().Select(t => t.MetadataName)); + builder.AddRange(receiverTypeSymbol.GetAllInterfacesIncludingThis().Select(t => t.MetadataName)); - // interface doesn't inherit from object, but is implicitly convertible to object type. - if (receiverTypeSymbol.IsInterfaceType()) - { - builder.Add(nameof(Object)); - } + // interface doesn't inherit from object, but is implicitly convertible to object type. + if (receiverTypeSymbol.IsInterfaceType()) + { + builder.Add(nameof(Object)); } } } + } - /// - /// Add strings represent complex types (i.e. "" for non-array types and "[]" for array types) to the receiver type, - /// so we would include in the filter info about extension methods with complex receiver type. - /// - private static ImmutableArray AddComplexTypes(ImmutableArray receiverTypeNames) - { - return - [ - .. receiverTypeNames, - FindSymbols.Extensions.ComplexReceiverTypeName, - FindSymbols.Extensions.ComplexArrayReceiverTypeName, - ]; - } + /// + /// Add strings represent complex types (i.e. "" for non-array types and "[]" for array types) to the receiver type, + /// so we would include in the filter info about extension methods with complex receiver type. + /// + private static ImmutableArray AddComplexTypes(ImmutableArray receiverTypeNames) + { + return + [ + .. receiverTypeNames, + FindSymbols.Extensions.ComplexReceiverTypeName, + FindSymbols.Extensions.ComplexArrayReceiverTypeName, + ]; + } - private static string GetReceiverTypeName(ITypeSymbol typeSymbol) + private static string GetReceiverTypeName(ITypeSymbol typeSymbol) + { + switch (typeSymbol) { - switch (typeSymbol) - { - case INamedTypeSymbol namedType: - return namedType.MetadataName; + case INamedTypeSymbol namedType: + return namedType.MetadataName; - case IArrayTypeSymbol arrayType: - var elementType = arrayType.ElementType; - while (elementType is IArrayTypeSymbol symbol) - { - elementType = symbol.ElementType; - } + case IArrayTypeSymbol arrayType: + var elementType = arrayType.ElementType; + while (elementType is IArrayTypeSymbol symbol) + { + elementType = symbol.ElementType; + } - var elementTypeName = GetReceiverTypeName(elementType); + var elementTypeName = GetReceiverTypeName(elementType); - // We do not differentiate array of different kinds sicne they are all represented in the indices as "NonArrayElementTypeName[]" - // e.g. int[], int[][], int[,], etc. are all represented as "int[]", whereas array of complex type such as T[] is "[]". - return elementTypeName + FindSymbols.Extensions.ArrayReceiverTypeNameSuffix; + // We do not differentiate array of different kinds sicne they are all represented in the indices as "NonArrayElementTypeName[]" + // e.g. int[], int[][], int[,], etc. are all represented as "int[]", whereas array of complex type such as T[] is "[]". + return elementTypeName + FindSymbols.Extensions.ArrayReceiverTypeNameSuffix; - default: - // Complex types are represented by ""; - return FindSymbols.Extensions.ComplexReceiverTypeName; - } + default: + // Complex types are represented by ""; + return FindSymbols.Extensions.ComplexReceiverTypeName; } } } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.cs index 07bd18ece311e..58af5842ea690 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.cs @@ -17,271 +17,270 @@ using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +/// +/// Provides completion items for extension methods from unimported namespace. +/// +/// It runs out-of-proc if it's enabled +internal static partial class ExtensionMethodImportCompletionHelper { - /// - /// Provides completion items for extension methods from unimported namespace. - /// - /// It runs out-of-proc if it's enabled - internal static partial class ExtensionMethodImportCompletionHelper + public static async Task WarmUpCacheAsync(Project project, CancellationToken cancellationToken) { - public static async Task WarmUpCacheAsync(Project project, CancellationToken cancellationToken) + var client = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false); + if (client != null) { - var client = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false); - if (client != null) - { - var result = await client.TryInvokeAsync( - project, - (service, solutionInfo, cancellationToken) => service.WarmUpCacheAsync( - solutionInfo, project.Id, cancellationToken), - cancellationToken).ConfigureAwait(false); - } - else - { - WarmUpCacheInCurrentProcess(project); - } + var result = await client.TryInvokeAsync( + project, + (service, solutionInfo, cancellationToken) => service.WarmUpCacheAsync( + solutionInfo, project.Id, cancellationToken), + cancellationToken).ConfigureAwait(false); } - - public static void WarmUpCacheInCurrentProcess(Project project) - => SymbolComputer.QueueCacheWarmUpTask(project); - - public static async Task GetUnimportedExtensionMethodsAsync( - SyntaxContext syntaxContext, - ITypeSymbol receiverTypeSymbol, - ISet namespaceInScope, - ImmutableArray targetTypesSymbols, - bool forceCacheCreation, - bool hideAdvancedMembers, - CancellationToken cancellationToken) + else { - SerializableUnimportedExtensionMethods? result = null; - var document = syntaxContext.Document; - var position = syntaxContext.Position; - var project = document.Project; + WarmUpCacheInCurrentProcess(project); + } + } - var totalTime = SharedStopwatch.StartNew(); + public static void WarmUpCacheInCurrentProcess(Project project) + => SymbolComputer.QueueCacheWarmUpTask(project); + + public static async Task GetUnimportedExtensionMethodsAsync( + SyntaxContext syntaxContext, + ITypeSymbol receiverTypeSymbol, + ISet namespaceInScope, + ImmutableArray targetTypesSymbols, + bool forceCacheCreation, + bool hideAdvancedMembers, + CancellationToken cancellationToken) + { + SerializableUnimportedExtensionMethods? result = null; + var document = syntaxContext.Document; + var position = syntaxContext.Position; + var project = document.Project; - var client = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false); - if (client != null) - { - var receiverTypeSymbolKeyData = SymbolKey.CreateString(receiverTypeSymbol, cancellationToken); - var targetTypesSymbolKeyData = targetTypesSymbols.SelectAsArray(s => SymbolKey.CreateString(s, cancellationToken)); - - // Call the project overload. Add-import-for-extension-method doesn't search outside of the current - // project cone. - var remoteResult = await client.TryInvokeAsync( - project, - (service, solutionInfo, cancellationToken) => service.GetUnimportedExtensionMethodsAsync( - solutionInfo, document.Id, position, receiverTypeSymbolKeyData, namespaceInScope.ToImmutableArray(), - targetTypesSymbolKeyData, forceCacheCreation, hideAdvancedMembers, cancellationToken), - cancellationToken).ConfigureAwait(false); - - result = remoteResult.HasValue ? remoteResult.Value : null; - } - else - { - result = await GetUnimportedExtensionMethodsInCurrentProcessAsync( - document, syntaxContext.SemanticModel, position, receiverTypeSymbol, namespaceInScope, targetTypesSymbols, forceCacheCreation, hideAdvancedMembers, remoteAssetSyncTime: null, cancellationToken) - .ConfigureAwait(false); - } + var totalTime = SharedStopwatch.StartNew(); - if (result is not null) - { - // report telemetry: - CompletionProvidersLogger.LogExtensionMethodCompletionTicksDataPoint( - totalTime.Elapsed, result.GetSymbolsTime, result.CreateItemsTime, result.RemoteAssetSyncTime); + var client = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false); + if (client != null) + { + var receiverTypeSymbolKeyData = SymbolKey.CreateString(receiverTypeSymbol, cancellationToken); + var targetTypesSymbolKeyData = targetTypesSymbols.SelectAsArray(s => SymbolKey.CreateString(s, cancellationToken)); + + // Call the project overload. Add-import-for-extension-method doesn't search outside of the current + // project cone. + var remoteResult = await client.TryInvokeAsync( + project, + (service, solutionInfo, cancellationToken) => service.GetUnimportedExtensionMethodsAsync( + solutionInfo, document.Id, position, receiverTypeSymbolKeyData, namespaceInScope.ToImmutableArray(), + targetTypesSymbolKeyData, forceCacheCreation, hideAdvancedMembers, cancellationToken), + cancellationToken).ConfigureAwait(false); + + result = remoteResult.HasValue ? remoteResult.Value : null; + } + else + { + result = await GetUnimportedExtensionMethodsInCurrentProcessAsync( + document, syntaxContext.SemanticModel, position, receiverTypeSymbol, namespaceInScope, targetTypesSymbols, forceCacheCreation, hideAdvancedMembers, remoteAssetSyncTime: null, cancellationToken) + .ConfigureAwait(false); + } - if (result.IsPartialResult) - CompletionProvidersLogger.LogExtensionMethodCompletionPartialResultCount(); - } + if (result is not null) + { + // report telemetry: + CompletionProvidersLogger.LogExtensionMethodCompletionTicksDataPoint( + totalTime.Elapsed, result.GetSymbolsTime, result.CreateItemsTime, result.RemoteAssetSyncTime); - return result; + if (result.IsPartialResult) + CompletionProvidersLogger.LogExtensionMethodCompletionPartialResultCount(); } - public static async Task GetUnimportedExtensionMethodsInCurrentProcessAsync( - Document document, - SemanticModel? semanticModel, - int position, - ITypeSymbol receiverTypeSymbol, - ISet namespaceInScope, - ImmutableArray targetTypes, - bool forceCacheCreation, - bool hideAdvancedMembers, - TimeSpan? remoteAssetSyncTime, - CancellationToken cancellationToken) - { - var stopwatch = SharedStopwatch.StartNew(); + return result; + } + + public static async Task GetUnimportedExtensionMethodsInCurrentProcessAsync( + Document document, + SemanticModel? semanticModel, + int position, + ITypeSymbol receiverTypeSymbol, + ISet namespaceInScope, + ImmutableArray targetTypes, + bool forceCacheCreation, + bool hideAdvancedMembers, + TimeSpan? remoteAssetSyncTime, + CancellationToken cancellationToken) + { + var stopwatch = SharedStopwatch.StartNew(); - // First find symbols of all applicable extension methods. - // Workspace's syntax/symbol index is used to avoid iterating every method symbols in the solution. - semanticModel ??= await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var symbolComputer = new SymbolComputer( - document, semanticModel, receiverTypeSymbol, position, namespaceInScope); - var (extensionMethodSymbols, isPartialResult) = await symbolComputer.GetExtensionMethodSymbolsAsync(forceCacheCreation, hideAdvancedMembers, cancellationToken).ConfigureAwait(false); + // First find symbols of all applicable extension methods. + // Workspace's syntax/symbol index is used to avoid iterating every method symbols in the solution. + semanticModel ??= await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var symbolComputer = new SymbolComputer( + document, semanticModel, receiverTypeSymbol, position, namespaceInScope); + var (extensionMethodSymbols, isPartialResult) = await symbolComputer.GetExtensionMethodSymbolsAsync(forceCacheCreation, hideAdvancedMembers, cancellationToken).ConfigureAwait(false); - var getSymbolsTime = stopwatch.Elapsed; - stopwatch = SharedStopwatch.StartNew(); + var getSymbolsTime = stopwatch.Elapsed; + stopwatch = SharedStopwatch.StartNew(); - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var items = ConvertSymbolsToCompletionItems(compilation, extensionMethodSymbols, targetTypes, cancellationToken); + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var items = ConvertSymbolsToCompletionItems(compilation, extensionMethodSymbols, targetTypes, cancellationToken); - var createItemsTime = stopwatch.Elapsed; + var createItemsTime = stopwatch.Elapsed; - return new SerializableUnimportedExtensionMethods(items, isPartialResult, getSymbolsTime, createItemsTime, remoteAssetSyncTime); - } + return new SerializableUnimportedExtensionMethods(items, isPartialResult, getSymbolsTime, createItemsTime, remoteAssetSyncTime); + } - public static async ValueTask BatchUpdateCacheAsync(ImmutableSegmentedList projects, CancellationToken cancellationToken) + public static async ValueTask BatchUpdateCacheAsync(ImmutableSegmentedList projects, CancellationToken cancellationToken) + { + var latestProjects = CompletionUtilities.GetDistinctProjectsFromLatestSolutionSnapshot(projects); + foreach (var project in latestProjects) { - var latestProjects = CompletionUtilities.GetDistinctProjectsFromLatestSolutionSnapshot(projects); - foreach (var project in latestProjects) - { - await SymbolComputer.UpdateCacheAsync(project, cancellationToken).ConfigureAwait(false); - } + await SymbolComputer.UpdateCacheAsync(project, cancellationToken).ConfigureAwait(false); } + } - private static ImmutableArray ConvertSymbolsToCompletionItems( - Compilation compilation, ImmutableArray extentsionMethodSymbols, ImmutableArray targetTypeSymbols, CancellationToken cancellationToken) - { - Dictionary typeConvertibilityCache = []; - using var _1 = PooledDictionary.GetInstance(out var namespaceNameCache); - using var _2 = PooledDictionary<(string containingNamespace, string methodName, bool isGeneric), (IMethodSymbol bestSymbol, int overloadCount, bool includeInTargetTypedCompletion)> - .GetInstance(out var overloadMap); + private static ImmutableArray ConvertSymbolsToCompletionItems( + Compilation compilation, ImmutableArray extentsionMethodSymbols, ImmutableArray targetTypeSymbols, CancellationToken cancellationToken) + { + Dictionary typeConvertibilityCache = []; + using var _1 = PooledDictionary.GetInstance(out var namespaceNameCache); + using var _2 = PooledDictionary<(string containingNamespace, string methodName, bool isGeneric), (IMethodSymbol bestSymbol, int overloadCount, bool includeInTargetTypedCompletion)> + .GetInstance(out var overloadMap); - // Aggregate overloads - foreach (var symbol in extentsionMethodSymbols) - { - cancellationToken.ThrowIfCancellationRequested(); + // Aggregate overloads + foreach (var symbol in extentsionMethodSymbols) + { + cancellationToken.ThrowIfCancellationRequested(); - IMethodSymbol bestSymbol; - int overloadCount; - var includeInTargetTypedCompletion = ShouldIncludeInTargetTypedCompletion(compilation, symbol, targetTypeSymbols, typeConvertibilityCache); + IMethodSymbol bestSymbol; + int overloadCount; + var includeInTargetTypedCompletion = ShouldIncludeInTargetTypedCompletion(compilation, symbol, targetTypeSymbols, typeConvertibilityCache); - var containingNamespacename = GetFullyQualifiedNamespaceName(symbol.ContainingNamespace, namespaceNameCache); - var overloadKey = (containingNamespacename, symbol.Name, isGeneric: symbol.Arity > 0); + var containingNamespacename = GetFullyQualifiedNamespaceName(symbol.ContainingNamespace, namespaceNameCache); + var overloadKey = (containingNamespacename, symbol.Name, isGeneric: symbol.Arity > 0); - // Select the overload convertible to any targeted type (if any) and with minimum number of parameters to display - if (overloadMap.TryGetValue(overloadKey, out var currentValue)) + // Select the overload convertible to any targeted type (if any) and with minimum number of parameters to display + if (overloadMap.TryGetValue(overloadKey, out var currentValue)) + { + if (currentValue.includeInTargetTypedCompletion == includeInTargetTypedCompletion) + { + bestSymbol = currentValue.bestSymbol.Parameters.Length > symbol.Parameters.Length ? symbol : currentValue.bestSymbol; + } + else if (currentValue.includeInTargetTypedCompletion) { - if (currentValue.includeInTargetTypedCompletion == includeInTargetTypedCompletion) - { - bestSymbol = currentValue.bestSymbol.Parameters.Length > symbol.Parameters.Length ? symbol : currentValue.bestSymbol; - } - else if (currentValue.includeInTargetTypedCompletion) - { - bestSymbol = currentValue.bestSymbol; - } - else - { - bestSymbol = symbol; - } - - overloadCount = currentValue.overloadCount + 1; - includeInTargetTypedCompletion = includeInTargetTypedCompletion || currentValue.includeInTargetTypedCompletion; + bestSymbol = currentValue.bestSymbol; } else { bestSymbol = symbol; - overloadCount = 1; } - overloadMap[overloadKey] = (bestSymbol, overloadCount, includeInTargetTypedCompletion); + overloadCount = currentValue.overloadCount + 1; + includeInTargetTypedCompletion = includeInTargetTypedCompletion || currentValue.includeInTargetTypedCompletion; } - - // Then convert symbols into completion items - using var _3 = ArrayBuilder.GetInstance(out var itemsBuilder); - - foreach (var ((containingNamespace, _, _), (bestSymbol, overloadCount, includeInTargetTypedCompletion)) in overloadMap) + else { - // To display the count of additional overloads, we need to subtract total by 1. - var item = new SerializableImportCompletionItem( - SymbolKey.CreateString(bestSymbol, cancellationToken), - bestSymbol.Name, - bestSymbol.Arity, - bestSymbol.GetGlyph(), - containingNamespace, - additionalOverloadCount: overloadCount - 1, - includeInTargetTypedCompletion); - - itemsBuilder.Add(item); + bestSymbol = symbol; + overloadCount = 1; } - return itemsBuilder.ToImmutable(); + overloadMap[overloadKey] = (bestSymbol, overloadCount, includeInTargetTypedCompletion); } - private static bool ShouldIncludeInTargetTypedCompletion( - Compilation compilation, IMethodSymbol methodSymbol, ImmutableArray targetTypeSymbols, - Dictionary typeConvertibilityCache) + // Then convert symbols into completion items + using var _3 = ArrayBuilder.GetInstance(out var itemsBuilder); + + foreach (var ((containingNamespace, _, _), (bestSymbol, overloadCount, includeInTargetTypedCompletion)) in overloadMap) { - if (methodSymbol.ReturnsVoid || methodSymbol.ReturnType == null || targetTypeSymbols.IsEmpty) - { - return false; - } + // To display the count of additional overloads, we need to subtract total by 1. + var item = new SerializableImportCompletionItem( + SymbolKey.CreateString(bestSymbol, cancellationToken), + bestSymbol.Name, + bestSymbol.Arity, + bestSymbol.GetGlyph(), + containingNamespace, + additionalOverloadCount: overloadCount - 1, + includeInTargetTypedCompletion); + + itemsBuilder.Add(item); + } - if (typeConvertibilityCache.TryGetValue(methodSymbol.ReturnType, out var isConvertible)) - { - return isConvertible; - } + return itemsBuilder.ToImmutable(); + } - isConvertible = CompletionUtilities.IsTypeImplicitlyConvertible(compilation, methodSymbol.ReturnType, targetTypeSymbols); - typeConvertibilityCache[methodSymbol.ReturnType] = isConvertible; + private static bool ShouldIncludeInTargetTypedCompletion( + Compilation compilation, IMethodSymbol methodSymbol, ImmutableArray targetTypeSymbols, + Dictionary typeConvertibilityCache) + { + if (methodSymbol.ReturnsVoid || methodSymbol.ReturnType == null || targetTypeSymbols.IsEmpty) + { + return false; + } + if (typeConvertibilityCache.TryGetValue(methodSymbol.ReturnType, out var isConvertible)) + { return isConvertible; } - private static string GetFullyQualifiedNamespaceName(INamespaceSymbol symbol, Dictionary stringCache) - { - if (symbol.ContainingNamespace == null || symbol.ContainingNamespace.IsGlobalNamespace) - { - return symbol.Name; - } + isConvertible = CompletionUtilities.IsTypeImplicitlyConvertible(compilation, methodSymbol.ReturnType, targetTypeSymbols); + typeConvertibilityCache[methodSymbol.ReturnType] = isConvertible; - if (stringCache.TryGetValue(symbol, out var name)) - { - return name; - } + return isConvertible; + } + + private static string GetFullyQualifiedNamespaceName(INamespaceSymbol symbol, Dictionary stringCache) + { + if (symbol.ContainingNamespace == null || symbol.ContainingNamespace.IsGlobalNamespace) + { + return symbol.Name; + } - name = GetFullyQualifiedNamespaceName(symbol.ContainingNamespace, stringCache) + "." + symbol.Name; - stringCache[symbol] = name; + if (stringCache.TryGetValue(symbol, out var name)) + { return name; } - private static async Task GetUpToDateCacheEntryAsync( - Project project, - IImportCompletionCacheService cacheService, - CancellationToken cancellationToken) + name = GetFullyQualifiedNamespaceName(symbol.ContainingNamespace, stringCache) + "." + symbol.Name; + stringCache[symbol] = name; + return name; + } + + private static async Task GetUpToDateCacheEntryAsync( + Project project, + IImportCompletionCacheService cacheService, + CancellationToken cancellationToken) + { + // While we are caching data from SyntaxTreeInfo, all the things we cared about here are actually based on sources symbols. + // So using source symbol checksum would suffice. + var checksum = await SymbolTreeInfo.GetSourceSymbolsChecksumAsync(project, cancellationToken).ConfigureAwait(false); + + // Cache miss, create all requested items. + if (!cacheService.ProjectItemsCache.TryGetValue(project.Id, out var cacheEntry) || + cacheEntry.Checksum != checksum || + cacheEntry.Language != project.Language) { - // While we are caching data from SyntaxTreeInfo, all the things we cared about here are actually based on sources symbols. - // So using source symbol checksum would suffice. - var checksum = await SymbolTreeInfo.GetSourceSymbolsChecksumAsync(project, cancellationToken).ConfigureAwait(false); - - // Cache miss, create all requested items. - if (!cacheService.ProjectItemsCache.TryGetValue(project.Id, out var cacheEntry) || - cacheEntry.Checksum != checksum || - cacheEntry.Language != project.Language) - { - var syntaxFacts = project.Services.GetRequiredService(); - var builder = new ExtensionMethodImportCompletionCacheEntry.Builder(checksum, project.Language, syntaxFacts.StringComparer); + var syntaxFacts = project.Services.GetRequiredService(); + var builder = new ExtensionMethodImportCompletionCacheEntry.Builder(checksum, project.Language, syntaxFacts.StringComparer); - foreach (var document in project.Documents) + foreach (var document in project.Documents) + { + // Don't look for extension methods in generated code. + if (document.State.Attributes.IsGenerated) { - // Don't look for extension methods in generated code. - if (document.State.Attributes.IsGenerated) - { - continue; - } - - var info = await TopLevelSyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false); - if (info.ContainsExtensionMethod) - { - builder.AddItem(info); - } + continue; } - cacheEntry = builder.ToCacheEntry(); - cacheService.ProjectItemsCache[project.Id] = cacheEntry; + var info = await TopLevelSyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false); + if (info.ContainsExtensionMethod) + { + builder.AddItem(info); + } } - return cacheEntry; + cacheEntry = builder.ToCacheEntry(); + cacheService.ProjectItemsCache[project.Id] = cacheEntry; } + + return cacheEntry; } } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IImportCompletionCacheService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IImportCompletionCacheService.cs index 571b7f6ab5f31..c2621629fdf4c 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IImportCompletionCacheService.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IImportCompletionCacheService.cs @@ -10,15 +10,14 @@ using Microsoft.CodeAnalysis.Host; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal interface IImportCompletionCacheService : IWorkspaceService { - internal interface IImportCompletionCacheService : IWorkspaceService - { - // PE references are keyed on assembly path. - IDictionary PEItemsCache { get; } + // PE references are keyed on assembly path. + IDictionary PEItemsCache { get; } - IDictionary ProjectItemsCache { get; } + IDictionary ProjectItemsCache { get; } - AsyncBatchingWorkQueue WorkQueue { get; } - } + AsyncBatchingWorkQueue WorkQueue { get; } } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IRemoteExtensionMethodImportCompletionService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IRemoteExtensionMethodImportCompletionService.cs index dff57caf6d130..6af33283e8a54 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IRemoteExtensionMethodImportCompletionService.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IRemoteExtensionMethodImportCompletionService.cs @@ -7,21 +7,20 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Remote; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal interface IRemoteExtensionMethodImportCompletionService { - internal interface IRemoteExtensionMethodImportCompletionService - { - ValueTask GetUnimportedExtensionMethodsAsync( - Checksum solutionChecksum, - DocumentId documentId, - int position, - string receiverTypeSymbolKeyData, - ImmutableArray namespaceInScope, - ImmutableArray targetTypesSymbolKeyData, - bool forceCacheCreation, - bool hideAdvancedMembers, - CancellationToken cancellationToken); + ValueTask GetUnimportedExtensionMethodsAsync( + Checksum solutionChecksum, + DocumentId documentId, + int position, + string receiverTypeSymbolKeyData, + ImmutableArray namespaceInScope, + ImmutableArray targetTypesSymbolKeyData, + bool forceCacheCreation, + bool hideAdvancedMembers, + CancellationToken cancellationToken); - ValueTask WarmUpCacheAsync(Checksum solutionChecksum, ProjectId projectId, CancellationToken cancellationToken); - } + ValueTask WarmUpCacheAsync(Checksum solutionChecksum, ProjectId projectId, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ITypeImportCompletionService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ITypeImportCompletionService.cs index 4f4ce9c66fca8..1c3d547efe1e9 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ITypeImportCompletionService.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ITypeImportCompletionService.cs @@ -8,25 +8,24 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal interface ITypeImportCompletionService : ILanguageService { - internal interface ITypeImportCompletionService : ILanguageService - { - /// - /// Get completion items for all the accessible top level types from the given project and all its references. - /// Each array returned contains all items from one of the reachable entities (i.e. projects and PE references.) - /// Returns null if we don't have all the items cached and is false. - /// - /// - /// Because items from each entity are cached as a separate array, we simply return them as is instead of an - /// aggregated array to avoid unnecessary allocations. - /// - Task<(ImmutableArray>, bool)> GetAllTopLevelTypesAsync( - SyntaxContext syntaxContext, - bool forceCacheCreation, - CompletionOptions options, - CancellationToken cancellationToken); + /// + /// Get completion items for all the accessible top level types from the given project and all its references. + /// Each array returned contains all items from one of the reachable entities (i.e. projects and PE references.) + /// Returns null if we don't have all the items cached and is false. + /// + /// + /// Because items from each entity are cached as a separate array, we simply return them as is instead of an + /// aggregated array to avoid unnecessary allocations. + /// + Task<(ImmutableArray>, bool)> GetAllTopLevelTypesAsync( + SyntaxContext syntaxContext, + bool forceCacheCreation, + CompletionOptions options, + CancellationToken cancellationToken); - void QueueCacheWarmUpTask(Project project); - } + void QueueCacheWarmUpTask(Project project); } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ImportCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ImportCompletionItem.cs index 1261492688c56..a1c2a4b673d50 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ImportCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ImportCompletionItem.cs @@ -12,221 +12,220 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Tags; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal static class ImportCompletionItem { - internal static class ImportCompletionItem + // Note the additional space as prefix to the System namespace, + // to make sure items from System.* get sorted ahead. + private const string OtherNamespaceSortTextFormat = "~{0} {1}"; + private const string SystemNamespaceSortTextFormat = "~{0} {1}"; + + private const string TypeAritySuffixName = nameof(TypeAritySuffixName); + private const string AttributeFullName = nameof(AttributeFullName); + private const string MethodKey = nameof(MethodKey); + private const string ReceiverKey = nameof(ReceiverKey); + private const string OverloadCountKey = nameof(OverloadCountKey); + private const string AlwaysFullyQualifyKey = nameof(AlwaysFullyQualifyKey); + + public static CompletionItem Create( + string name, + int arity, + string containingNamespace, + Glyph glyph, + string genericTypeSuffix, + CompletionItemFlags flags, + (string methodSymbolKey, string receiverTypeSymbolKey, int overloadCount)? extensionMethodData, + bool includedInTargetTypeCompletion = false) { - // Note the additional space as prefix to the System namespace, - // to make sure items from System.* get sorted ahead. - private const string OtherNamespaceSortTextFormat = "~{0} {1}"; - private const string SystemNamespaceSortTextFormat = "~{0} {1}"; - - private const string TypeAritySuffixName = nameof(TypeAritySuffixName); - private const string AttributeFullName = nameof(AttributeFullName); - private const string MethodKey = nameof(MethodKey); - private const string ReceiverKey = nameof(ReceiverKey); - private const string OverloadCountKey = nameof(OverloadCountKey); - private const string AlwaysFullyQualifyKey = nameof(AlwaysFullyQualifyKey); - - public static CompletionItem Create( - string name, - int arity, - string containingNamespace, - Glyph glyph, - string genericTypeSuffix, - CompletionItemFlags flags, - (string methodSymbolKey, string receiverTypeSymbolKey, int overloadCount)? extensionMethodData, - bool includedInTargetTypeCompletion = false) + ImmutableArray> properties = default; + + if (extensionMethodData != null || arity > 0) { - ImmutableArray> properties = default; + using var _ = ArrayBuilder>.GetInstance(out var builder); - if (extensionMethodData != null || arity > 0) + if (extensionMethodData.HasValue) { - using var _ = ArrayBuilder>.GetInstance(out var builder); + builder.Add(new KeyValuePair(MethodKey, extensionMethodData.Value.methodSymbolKey)); + builder.Add(new KeyValuePair(ReceiverKey, extensionMethodData.Value.receiverTypeSymbolKey)); - if (extensionMethodData.HasValue) + if (extensionMethodData.Value.overloadCount > 0) { - builder.Add(new KeyValuePair(MethodKey, extensionMethodData.Value.methodSymbolKey)); - builder.Add(new KeyValuePair(ReceiverKey, extensionMethodData.Value.receiverTypeSymbolKey)); - - if (extensionMethodData.Value.overloadCount > 0) - { - builder.Add(new KeyValuePair(OverloadCountKey, extensionMethodData.Value.overloadCount.ToString())); - } + builder.Add(new KeyValuePair(OverloadCountKey, extensionMethodData.Value.overloadCount.ToString())); } - else - { - // We don't need arity to recover symbol if we already have SymbolKeyData or it's 0. - // (but it still needed below to decide whether to show generic suffix) - builder.Add(new KeyValuePair(TypeAritySuffixName, ArityUtilities.GetMetadataAritySuffix(arity))); - } - - properties = builder.ToImmutable(); } - - // Use " " as sort text. The space before namespace makes items with identical display name - // but from different namespace all show up in the list, it also makes sure item with shorter name shows first, - // e.g. 'SomeType` before 'SomeTypeWithLongerName'. - var sortTextBuilder = PooledStringBuilder.GetInstance(); - sortTextBuilder.Builder.AppendFormat(GetSortTextFormatString(containingNamespace), name, containingNamespace); - - var item = CompletionItem.CreateInternal( - displayText: name, - sortText: sortTextBuilder.ToStringAndFree(), - properties: properties, - tags: GlyphTags.GetTags(glyph), - rules: CompletionItemRules.Default, - displayTextPrefix: null, - displayTextSuffix: arity == 0 ? string.Empty : genericTypeSuffix, - inlineDescription: containingNamespace, - isComplexTextEdit: true); - - if (includedInTargetTypeCompletion) + else { - item = item.AddTag(WellKnownTags.TargetTypeMatch); + // We don't need arity to recover symbol if we already have SymbolKeyData or it's 0. + // (but it still needed below to decide whether to show generic suffix) + builder.Add(new KeyValuePair(TypeAritySuffixName, ArityUtilities.GetMetadataAritySuffix(arity))); } - item.Flags = flags; - return item; + properties = builder.ToImmutable(); } - public static CompletionItem CreateAttributeItemWithoutSuffix(CompletionItem attributeItem, string attributeNameWithoutSuffix, CompletionItemFlags flags) + // Use " " as sort text. The space before namespace makes items with identical display name + // but from different namespace all show up in the list, it also makes sure item with shorter name shows first, + // e.g. 'SomeType` before 'SomeTypeWithLongerName'. + var sortTextBuilder = PooledStringBuilder.GetInstance(); + sortTextBuilder.Builder.AppendFormat(GetSortTextFormatString(containingNamespace), name, containingNamespace); + + var item = CompletionItem.CreateInternal( + displayText: name, + sortText: sortTextBuilder.ToStringAndFree(), + properties: properties, + tags: GlyphTags.GetTags(glyph), + rules: CompletionItemRules.Default, + displayTextPrefix: null, + displayTextSuffix: arity == 0 ? string.Empty : genericTypeSuffix, + inlineDescription: containingNamespace, + isComplexTextEdit: true); + + if (includedInTargetTypeCompletion) { - Debug.Assert(!attributeItem.TryGetProperty(AttributeFullName, out var _)); - - var attributeItems = attributeItem.GetProperties(); - - // Remember the full type name so we can get the symbol when description is displayed. - using var _ = ArrayBuilder>.GetInstance(attributeItems.Length + 1, out var builder); - builder.AddRange(attributeItems); - builder.Add(new KeyValuePair(AttributeFullName, attributeItem.DisplayText)); - - var sortTextBuilder = PooledStringBuilder.GetInstance(); - sortTextBuilder.Builder.AppendFormat(GetSortTextFormatString(attributeItem.InlineDescription), attributeNameWithoutSuffix, attributeItem.InlineDescription); - - var item = CompletionItem.CreateInternal( - displayText: attributeNameWithoutSuffix, - sortText: sortTextBuilder.ToStringAndFree(), - properties: builder.ToImmutable(), - tags: attributeItem.Tags, - rules: attributeItem.Rules, - displayTextPrefix: attributeItem.DisplayTextPrefix, - displayTextSuffix: attributeItem.DisplayTextSuffix, - inlineDescription: attributeItem.InlineDescription, - isComplexTextEdit: true); - - item.Flags = flags; - return item; + item = item.AddTag(WellKnownTags.TargetTypeMatch); } - private static string GetSortTextFormatString(string containingNamespace) - { - if (containingNamespace == "System" || containingNamespace.StartsWith("System.")) - return SystemNamespaceSortTextFormat; + item.Flags = flags; + return item; + } - return OtherNamespaceSortTextFormat; - } + public static CompletionItem CreateAttributeItemWithoutSuffix(CompletionItem attributeItem, string attributeNameWithoutSuffix, CompletionItemFlags flags) + { + Debug.Assert(!attributeItem.TryGetProperty(AttributeFullName, out var _)); + + var attributeItems = attributeItem.GetProperties(); + + // Remember the full type name so we can get the symbol when description is displayed. + using var _ = ArrayBuilder>.GetInstance(attributeItems.Length + 1, out var builder); + builder.AddRange(attributeItems); + builder.Add(new KeyValuePair(AttributeFullName, attributeItem.DisplayText)); + + var sortTextBuilder = PooledStringBuilder.GetInstance(); + sortTextBuilder.Builder.AppendFormat(GetSortTextFormatString(attributeItem.InlineDescription), attributeNameWithoutSuffix, attributeItem.InlineDescription); + + var item = CompletionItem.CreateInternal( + displayText: attributeNameWithoutSuffix, + sortText: sortTextBuilder.ToStringAndFree(), + properties: builder.ToImmutable(), + tags: attributeItem.Tags, + rules: attributeItem.Rules, + displayTextPrefix: attributeItem.DisplayTextPrefix, + displayTextSuffix: attributeItem.DisplayTextSuffix, + inlineDescription: attributeItem.InlineDescription, + isComplexTextEdit: true); + + item.Flags = flags; + return item; + } - public static CompletionItem CreateItemWithGenericDisplaySuffix(CompletionItem item, string genericTypeSuffix) - => item.WithDisplayTextSuffix(genericTypeSuffix); + private static string GetSortTextFormatString(string containingNamespace) + { + if (containingNamespace == "System" || containingNamespace.StartsWith("System.")) + return SystemNamespaceSortTextFormat; - public static string GetContainingNamespace(CompletionItem item) - => item.InlineDescription; + return OtherNamespaceSortTextFormat; + } - public static async Task GetCompletionDescriptionAsync(Document document, CompletionItem item, SymbolDescriptionOptions options, CancellationToken cancellationToken) - { - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var (symbol, overloadCount) = GetSymbolAndOverloadCount(item, compilation); + public static CompletionItem CreateItemWithGenericDisplaySuffix(CompletionItem item, string genericTypeSuffix) + => item.WithDisplayTextSuffix(genericTypeSuffix); - if (symbol != null) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - return await CommonCompletionUtilities.CreateDescriptionAsync( - document.Project.Solution.Services, - semanticModel, - position: 0, - symbol, - overloadCount, - options, - supportedPlatforms: null, - cancellationToken).ConfigureAwait(false); - } + public static string GetContainingNamespace(CompletionItem item) + => item.InlineDescription; - return CompletionDescription.Empty; - } + public static async Task GetCompletionDescriptionAsync(Document document, CompletionItem item, SymbolDescriptionOptions options, CancellationToken cancellationToken) + { + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var (symbol, overloadCount) = GetSymbolAndOverloadCount(item, compilation); - public static string GetTypeName(CompletionItem item) + if (symbol != null) { - var typeName = item.TryGetProperty(AttributeFullName, out var attributeFullName) - ? attributeFullName - : item.DisplayText; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + return await CommonCompletionUtilities.CreateDescriptionAsync( + document.Project.Solution.Services, + semanticModel, + position: 0, + symbol, + overloadCount, + options, + supportedPlatforms: null, + cancellationToken).ConfigureAwait(false); + } - if (item.TryGetProperty(TypeAritySuffixName, out var aritySuffix)) - { - return typeName + aritySuffix; - } + return CompletionDescription.Empty; + } - return typeName; + public static string GetTypeName(CompletionItem item) + { + var typeName = item.TryGetProperty(AttributeFullName, out var attributeFullName) + ? attributeFullName + : item.DisplayText; + + if (item.TryGetProperty(TypeAritySuffixName, out var aritySuffix)) + { + return typeName + aritySuffix; } - private static string GetFullyQualifiedName(string namespaceName, string typeName) - => namespaceName.Length == 0 ? typeName : namespaceName + "." + typeName; + return typeName; + } + + private static string GetFullyQualifiedName(string namespaceName, string typeName) + => namespaceName.Length == 0 ? typeName : namespaceName + "." + typeName; - private static (ISymbol? symbol, int overloadCount) GetSymbolAndOverloadCount(CompletionItem item, Compilation compilation) + private static (ISymbol? symbol, int overloadCount) GetSymbolAndOverloadCount(CompletionItem item, Compilation compilation) + { + // If we have SymbolKey data (i.e. this is an extension method item), use it to recover symbol + if (item.TryGetProperty(MethodKey, out var methodSymbolKey)) { - // If we have SymbolKey data (i.e. this is an extension method item), use it to recover symbol - if (item.TryGetProperty(MethodKey, out var methodSymbolKey)) + var methodSymbol = SymbolKey.ResolveString(methodSymbolKey, compilation).GetAnySymbol() as IMethodSymbol; + + if (methodSymbol != null) { - var methodSymbol = SymbolKey.ResolveString(methodSymbolKey, compilation).GetAnySymbol() as IMethodSymbol; + var overloadCount = item.TryGetProperty(OverloadCountKey, out var overloadCountString) && int.TryParse(overloadCountString, out var count) ? count : 0; - if (methodSymbol != null) + // Get reduced extension method symbol for the given receiver type. + if (item.TryGetProperty(ReceiverKey, out var receiverTypeKey)) { - var overloadCount = item.TryGetProperty(OverloadCountKey, out var overloadCountString) && int.TryParse(overloadCountString, out var count) ? count : 0; - - // Get reduced extension method symbol for the given receiver type. - if (item.TryGetProperty(ReceiverKey, out var receiverTypeKey)) + if (SymbolKey.ResolveString(receiverTypeKey, compilation).GetAnySymbol() is ITypeSymbol receiverTypeSymbol) { - if (SymbolKey.ResolveString(receiverTypeKey, compilation).GetAnySymbol() is ITypeSymbol receiverTypeSymbol) - { - return (methodSymbol.ReduceExtensionMethod(receiverTypeSymbol) ?? methodSymbol, overloadCount); - } + return (methodSymbol.ReduceExtensionMethod(receiverTypeSymbol) ?? methodSymbol, overloadCount); } - - return (methodSymbol, overloadCount); } - return default; + return (methodSymbol, overloadCount); } - // Otherwise, this is a type item, so we don't have SymbolKey data. But we should still have all - // the data to construct its full metadata name - var containingNamespace = GetContainingNamespace(item); - var typeName = item.TryGetProperty(AttributeFullName, out var attributeFullName) ? attributeFullName : item.DisplayText; - var fullyQualifiedName = GetFullyQualifiedName(containingNamespace, typeName); + return default; + } - // We choose not to display the number of "type overloads" for simplicity. - // Otherwise, we need additional logic to track internal and public visible - // types separately, and cache both completion items. - if (item.TryGetProperty(TypeAritySuffixName, out var aritySuffix)) - { - return (compilation.GetTypeByMetadataName(fullyQualifiedName + aritySuffix), 0); - } + // Otherwise, this is a type item, so we don't have SymbolKey data. But we should still have all + // the data to construct its full metadata name + var containingNamespace = GetContainingNamespace(item); + var typeName = item.TryGetProperty(AttributeFullName, out var attributeFullName) ? attributeFullName : item.DisplayText; + var fullyQualifiedName = GetFullyQualifiedName(containingNamespace, typeName); - return (compilation.GetTypeByMetadataName(fullyQualifiedName), 0); + // We choose not to display the number of "type overloads" for simplicity. + // Otherwise, we need additional logic to track internal and public visible + // types separately, and cache both completion items. + if (item.TryGetProperty(TypeAritySuffixName, out var aritySuffix)) + { + return (compilation.GetTypeByMetadataName(fullyQualifiedName + aritySuffix), 0); } - public static CompletionItem MarkItemToAlwaysFullyQualify(CompletionItem item) - { - var itemProperties = item.GetProperties(); + return (compilation.GetTypeByMetadataName(fullyQualifiedName), 0); + } - using var _ = ArrayBuilder>.GetInstance(itemProperties.Length + 1, out var builder); - builder.AddRange(itemProperties); - builder.Add(new KeyValuePair(AlwaysFullyQualifyKey, AlwaysFullyQualifyKey)); + public static CompletionItem MarkItemToAlwaysFullyQualify(CompletionItem item) + { + var itemProperties = item.GetProperties(); - return item.WithProperties(builder.ToImmutable()); - } + using var _ = ArrayBuilder>.GetInstance(itemProperties.Length + 1, out var builder); + builder.AddRange(itemProperties); + builder.Add(new KeyValuePair(AlwaysFullyQualifyKey, AlwaysFullyQualifyKey)); - public static bool ShouldAlwaysFullyQualify(CompletionItem item) => item.TryGetProperty(AlwaysFullyQualifyKey, out var _); + return item.WithProperties(builder.ToImmutable()); } + + public static bool ShouldAlwaysFullyQualify(CompletionItem item) => item.TryGetProperty(AlwaysFullyQualifyKey, out var _); } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/SerializableImportCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/SerializableImportCompletionItem.cs index f8948f722f4ff..7d9444aa3794b 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/SerializableImportCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/SerializableImportCompletionItem.cs @@ -4,30 +4,29 @@ using System.Runtime.Serialization; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +[DataContract] +internal readonly struct SerializableImportCompletionItem(string symbolKeyData, string name, int arity, Glyph glyph, string containingNamespace, int additionalOverloadCount, bool includedInTargetTypeCompletion) { - [DataContract] - internal readonly struct SerializableImportCompletionItem(string symbolKeyData, string name, int arity, Glyph glyph, string containingNamespace, int additionalOverloadCount, bool includedInTargetTypeCompletion) - { - [DataMember(Order = 0)] - public readonly string SymbolKeyData = symbolKeyData; + [DataMember(Order = 0)] + public readonly string SymbolKeyData = symbolKeyData; - [DataMember(Order = 1)] - public readonly string Name = name; + [DataMember(Order = 1)] + public readonly string Name = name; - [DataMember(Order = 2)] - public readonly int Arity = arity; + [DataMember(Order = 2)] + public readonly int Arity = arity; - [DataMember(Order = 3)] - public readonly Glyph Glyph = glyph; + [DataMember(Order = 3)] + public readonly Glyph Glyph = glyph; - [DataMember(Order = 4)] - public readonly string ContainingNamespace = containingNamespace; + [DataMember(Order = 4)] + public readonly string ContainingNamespace = containingNamespace; - [DataMember(Order = 5)] - public readonly int AdditionalOverloadCount = additionalOverloadCount; + [DataMember(Order = 5)] + public readonly int AdditionalOverloadCount = additionalOverloadCount; - [DataMember(Order = 6)] - public readonly bool IncludedInTargetTypeCompletion = includedInTargetTypeCompletion; - } + [DataMember(Order = 6)] + public readonly bool IncludedInTargetTypeCompletion = includedInTargetTypeCompletion; } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/SerializableUnimportedExtensionMethods.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/SerializableUnimportedExtensionMethods.cs index 6dc0d2a639390..95ecce5db9342 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/SerializableUnimportedExtensionMethods.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/SerializableUnimportedExtensionMethods.cs @@ -6,29 +6,28 @@ using System.Collections.Immutable; using System.Runtime.Serialization; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +[DataContract] +internal sealed class SerializableUnimportedExtensionMethods( + ImmutableArray completionItems, + bool isPartialResult, + TimeSpan getSymbolsTime, + TimeSpan createItemsTime, + TimeSpan? remoteAssetSyncTime) { - [DataContract] - internal sealed class SerializableUnimportedExtensionMethods( - ImmutableArray completionItems, - bool isPartialResult, - TimeSpan getSymbolsTime, - TimeSpan createItemsTime, - TimeSpan? remoteAssetSyncTime) - { - [DataMember(Order = 0)] - public readonly ImmutableArray CompletionItems = completionItems; + [DataMember(Order = 0)] + public readonly ImmutableArray CompletionItems = completionItems; - [DataMember(Order = 1)] - public readonly bool IsPartialResult = isPartialResult; + [DataMember(Order = 1)] + public readonly bool IsPartialResult = isPartialResult; - [DataMember(Order = 2)] - public TimeSpan GetSymbolsTime { get; set; } = getSymbolsTime; + [DataMember(Order = 2)] + public TimeSpan GetSymbolsTime { get; set; } = getSymbolsTime; - [DataMember(Order = 3)] - public readonly TimeSpan CreateItemsTime = createItemsTime; + [DataMember(Order = 3)] + public readonly TimeSpan CreateItemsTime = createItemsTime; - [DataMember(Order = 4)] - public readonly TimeSpan? RemoteAssetSyncTime = remoteAssetSyncTime; - } + [DataMember(Order = 4)] + public readonly TimeSpan? RemoteAssetSyncTime = remoteAssetSyncTime; } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/TypeImportCompletionCacheEntry.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/TypeImportCompletionCacheEntry.cs index 067f6093637b5..c414b0124aaa9 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/TypeImportCompletionCacheEntry.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/TypeImportCompletionCacheEntry.cs @@ -10,243 +10,242 @@ using static Microsoft.CodeAnalysis.Shared.Utilities.EditorBrowsableHelpers; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal readonly struct TypeImportCompletionCacheEntry { - internal readonly struct TypeImportCompletionCacheEntry + public SymbolKey AssemblySymbolKey { get; } + + public string Language { get; } + + public Checksum Checksum { get; } + + private ImmutableArray ItemInfos { get; } + + /// + /// The number of items in this entry for types declared as public. + /// This is used to minimize memory allocation in case non-public items aren't needed. + /// + private int PublicItemCount { get; } + + /// + /// Only 1 entry (which corresponds to `System` namespace) can have items, + /// suitable for enum's base list. This flag allows to fast-skip other entries + /// without need to enumerate their items + /// + private bool HasEnumBaseTypes { get; } + + private TypeImportCompletionCacheEntry( + SymbolKey assemblySymbolKey, + Checksum checksum, + string language, + ImmutableArray items, + int publicItemCount, + bool hasEnumBaseTypes) { - public SymbolKey AssemblySymbolKey { get; } - - public string Language { get; } - - public Checksum Checksum { get; } - - private ImmutableArray ItemInfos { get; } - - /// - /// The number of items in this entry for types declared as public. - /// This is used to minimize memory allocation in case non-public items aren't needed. - /// - private int PublicItemCount { get; } - - /// - /// Only 1 entry (which corresponds to `System` namespace) can have items, - /// suitable for enum's base list. This flag allows to fast-skip other entries - /// without need to enumerate their items - /// - private bool HasEnumBaseTypes { get; } - - private TypeImportCompletionCacheEntry( - SymbolKey assemblySymbolKey, - Checksum checksum, - string language, - ImmutableArray items, - int publicItemCount, - bool hasEnumBaseTypes) - { - AssemblySymbolKey = assemblySymbolKey; - Checksum = checksum; - Language = language; + AssemblySymbolKey = assemblySymbolKey; + Checksum = checksum; + Language = language; - ItemInfos = items; - PublicItemCount = publicItemCount; - HasEnumBaseTypes = hasEnumBaseTypes; - } + ItemInfos = items; + PublicItemCount = publicItemCount; + HasEnumBaseTypes = hasEnumBaseTypes; + } - public ImmutableArray GetItemsForContext( - Compilation originCompilation, - string language, - string genericTypeSuffix, - bool isAttributeContext, - bool isEnumBaseListContext, - bool isCaseSensitive, - bool hideAdvancedMembers) - { - if (AssemblySymbolKey.Resolve(originCompilation).Symbol is not IAssemblySymbol assemblySymbol) - return []; + public ImmutableArray GetItemsForContext( + Compilation originCompilation, + string language, + string genericTypeSuffix, + bool isAttributeContext, + bool isEnumBaseListContext, + bool isCaseSensitive, + bool hideAdvancedMembers) + { + if (AssemblySymbolKey.Resolve(originCompilation).Symbol is not IAssemblySymbol assemblySymbol) + return []; - if (isEnumBaseListContext && !HasEnumBaseTypes) - return []; + if (isEnumBaseListContext && !HasEnumBaseTypes) + return []; - var isSameLanguage = Language == language; - var isInternalsVisible = originCompilation.Assembly.IsSameAssemblyOrHasFriendAccessTo(assemblySymbol); - using var _ = ArrayBuilder.GetInstance(out var builder); + var isSameLanguage = Language == language; + var isInternalsVisible = originCompilation.Assembly.IsSameAssemblyOrHasFriendAccessTo(assemblySymbol); + using var _ = ArrayBuilder.GetInstance(out var builder); - // PERF: try set the capacity upfront to avoid allocation from Resize - if (!isAttributeContext && !isEnumBaseListContext) + // PERF: try set the capacity upfront to avoid allocation from Resize + if (!isAttributeContext && !isEnumBaseListContext) + { + if (isInternalsVisible) { - if (isInternalsVisible) - { - builder.EnsureCapacity(ItemInfos.Length); - } - else - { - builder.EnsureCapacity(PublicItemCount); - } + builder.EnsureCapacity(ItemInfos.Length); } - - foreach (var info in ItemInfos) + else { - if (!info.IsPublic && !isInternalsVisible) - { - continue; - } + builder.EnsureCapacity(PublicItemCount); + } + } - // Option to show advanced members is false so we need to exclude them. - if (hideAdvancedMembers && info.IsEditorBrowsableStateAdvanced) - { - continue; - } + foreach (var info in ItemInfos) + { + if (!info.IsPublic && !isInternalsVisible) + { + continue; + } - var item = info.Item; + // Option to show advanced members is false so we need to exclude them. + if (hideAdvancedMembers && info.IsEditorBrowsableStateAdvanced) + { + continue; + } - if (isAttributeContext) - { - // Don't show non attribute item in attribute context - if (!info.IsAttribute) - { - continue; - } - - // We are in attribute context, will not show or complete with "Attribute" suffix. - item = GetAppropriateAttributeItem(info.Item, isCaseSensitive); - } + var item = info.Item; - // Skip item if not suitable for enum base list - if (isEnumBaseListContext && !info.IsEnumBaseType) + if (isAttributeContext) + { + // Don't show non attribute item in attribute context + if (!info.IsAttribute) { continue; } - // C# and VB the display text is different for generics, i.e. and (Of T). For simplicity, we only cache for one language. - // But when we trigger in a project with different language than when the cache entry was created for, we will need to - // change the generic suffix accordingly. - if (!isSameLanguage && info.IsGeneric) - { - // We don't want to cache this item. - item = ImportCompletionItem.CreateItemWithGenericDisplaySuffix(item, genericTypeSuffix); - } - - builder.Add(item); + // We are in attribute context, will not show or complete with "Attribute" suffix. + item = GetAppropriateAttributeItem(info.Item, isCaseSensitive); } - return builder.ToImmutable(); - - static CompletionItem GetAppropriateAttributeItem(CompletionItem attributeItem, bool isCaseSensitive) + // Skip item if not suitable for enum base list + if (isEnumBaseListContext && !info.IsEnumBaseType) { - if (attributeItem.DisplayText.TryGetWithoutAttributeSuffix(isCaseSensitive: isCaseSensitive, out var attributeNameWithoutSuffix)) - { - // We don't want to cache this item. - return ImportCompletionItem.CreateAttributeItemWithoutSuffix(attributeItem, attributeNameWithoutSuffix, CompletionItemFlags.Expanded); - } + continue; + } - return attributeItem; + // C# and VB the display text is different for generics, i.e. and (Of T). For simplicity, we only cache for one language. + // But when we trigger in a project with different language than when the cache entry was created for, we will need to + // change the generic suffix accordingly. + if (!isSameLanguage && info.IsGeneric) + { + // We don't want to cache this item. + item = ImportCompletionItem.CreateItemWithGenericDisplaySuffix(item, genericTypeSuffix); } + + builder.Add(item); } - public class Builder(SymbolKey assemblySymbolKey, Checksum checksum, string language, string genericTypeSuffix, EditorBrowsableInfo editorBrowsableInfo) : IDisposable + return builder.ToImmutable(); + + static CompletionItem GetAppropriateAttributeItem(CompletionItem attributeItem, bool isCaseSensitive) { - private readonly SymbolKey _assemblySymbolKey = assemblySymbolKey; - private readonly string _language = language; - private readonly string _genericTypeSuffix = genericTypeSuffix; - private readonly Checksum _checksum = checksum; - private readonly EditorBrowsableInfo _editorBrowsableInfo = editorBrowsableInfo; + if (attributeItem.DisplayText.TryGetWithoutAttributeSuffix(isCaseSensitive: isCaseSensitive, out var attributeNameWithoutSuffix)) + { + // We don't want to cache this item. + return ImportCompletionItem.CreateAttributeItemWithoutSuffix(attributeItem, attributeNameWithoutSuffix, CompletionItemFlags.Expanded); + } - private int _publicItemCount; - private bool _hasEnumBaseTypes; + return attributeItem; + } + } - private readonly ArrayBuilder _itemsBuilder = ArrayBuilder.GetInstance(); + public class Builder(SymbolKey assemblySymbolKey, Checksum checksum, string language, string genericTypeSuffix, EditorBrowsableInfo editorBrowsableInfo) : IDisposable + { + private readonly SymbolKey _assemblySymbolKey = assemblySymbolKey; + private readonly string _language = language; + private readonly string _genericTypeSuffix = genericTypeSuffix; + private readonly Checksum _checksum = checksum; + private readonly EditorBrowsableInfo _editorBrowsableInfo = editorBrowsableInfo; - public TypeImportCompletionCacheEntry ToReferenceCacheEntry() - { - return new TypeImportCompletionCacheEntry( - _assemblySymbolKey, - _checksum, - _language, - _itemsBuilder.ToImmutable(), - _publicItemCount, - _hasEnumBaseTypes); - } + private int _publicItemCount; + private bool _hasEnumBaseTypes; - public void AddItem(INamedTypeSymbol symbol, string containingNamespace, bool isPublic) - { - // We want to cache items with EditorBrowsableState == Advanced regardless of current "hide adv members" option value - var (isBrowsable, isEditorBrowsableStateAdvanced) = symbol.IsEditorBrowsableWithState( - hideAdvancedMembers: false, - _editorBrowsableInfo.Compilation, - _editorBrowsableInfo); + private readonly ArrayBuilder _itemsBuilder = ArrayBuilder.GetInstance(); - if (!isBrowsable) - { - // Hide this item from completion - return; - } + public TypeImportCompletionCacheEntry ToReferenceCacheEntry() + { + return new TypeImportCompletionCacheEntry( + _assemblySymbolKey, + _checksum, + _language, + _itemsBuilder.ToImmutable(), + _publicItemCount, + _hasEnumBaseTypes); + } + + public void AddItem(INamedTypeSymbol symbol, string containingNamespace, bool isPublic) + { + // We want to cache items with EditorBrowsableState == Advanced regardless of current "hide adv members" option value + var (isBrowsable, isEditorBrowsableStateAdvanced) = symbol.IsEditorBrowsableWithState( + hideAdvancedMembers: false, + _editorBrowsableInfo.Compilation, + _editorBrowsableInfo); - var isGeneric = symbol.Arity > 0; + if (!isBrowsable) + { + // Hide this item from completion + return; + } - // Need to determine if a type is an attribute up front since we want to filter out - // non-attribute types when in attribute context. We can't do this lazily since we don't hold - // on to symbols. However, the cost of calling `IsAttribute` on every top-level type symbols - // is prohibitively high, so we opt for the heuristic that would do the simple textual "Attribute" - // suffix check first, then the more expensive symbolic check. As a result, all unimported - // attribute types that don't have "Attribute" suffix would be filtered out when in attribute context. - var isAttribute = symbol.Name.HasAttributeSuffix(isCaseSensitive: false) && symbol.IsAttribute(); + var isGeneric = symbol.Arity > 0; - var isEnumBaseType = symbol.SpecialType is >= SpecialType.System_SByte and <= SpecialType.System_UInt64; - _hasEnumBaseTypes |= isEnumBaseType; + // Need to determine if a type is an attribute up front since we want to filter out + // non-attribute types when in attribute context. We can't do this lazily since we don't hold + // on to symbols. However, the cost of calling `IsAttribute` on every top-level type symbols + // is prohibitively high, so we opt for the heuristic that would do the simple textual "Attribute" + // suffix check first, then the more expensive symbolic check. As a result, all unimported + // attribute types that don't have "Attribute" suffix would be filtered out when in attribute context. + var isAttribute = symbol.Name.HasAttributeSuffix(isCaseSensitive: false) && symbol.IsAttribute(); - var item = ImportCompletionItem.Create( - symbol.Name, - symbol.Arity, - containingNamespace, - symbol.GetGlyph(), - _genericTypeSuffix, - CompletionItemFlags.CachedAndExpanded, - extensionMethodData: null); + var isEnumBaseType = symbol.SpecialType is >= SpecialType.System_SByte and <= SpecialType.System_UInt64; + _hasEnumBaseTypes |= isEnumBaseType; - if (isPublic) - _publicItemCount++; + var item = ImportCompletionItem.Create( + symbol.Name, + symbol.Arity, + containingNamespace, + symbol.GetGlyph(), + _genericTypeSuffix, + CompletionItemFlags.CachedAndExpanded, + extensionMethodData: null); - _itemsBuilder.Add(new TypeImportCompletionItemInfo(item, isPublic, isGeneric, isAttribute, isEditorBrowsableStateAdvanced, isEnumBaseType)); - } + if (isPublic) + _publicItemCount++; - public void Dispose() - => _itemsBuilder.Free(); + _itemsBuilder.Add(new TypeImportCompletionItemInfo(item, isPublic, isGeneric, isAttribute, isEditorBrowsableStateAdvanced, isEnumBaseType)); } - private readonly struct TypeImportCompletionItemInfo(CompletionItem item, bool isPublic, bool isGeneric, bool isAttribute, bool isEditorBrowsableStateAdvanced, bool isEnumBaseType) - { - private readonly ItemPropertyKind _properties = (isPublic ? ItemPropertyKind.IsPublic : 0) - | (isGeneric ? ItemPropertyKind.IsGeneric : 0) - | (isAttribute ? ItemPropertyKind.IsAttribute : 0) - | (isEnumBaseType ? ItemPropertyKind.IsEnumBaseType : 0) - | (isEditorBrowsableStateAdvanced ? ItemPropertyKind.IsEditorBrowsableStateAdvanced : 0); + public void Dispose() + => _itemsBuilder.Free(); + } - public CompletionItem Item { get; } = item; + private readonly struct TypeImportCompletionItemInfo(CompletionItem item, bool isPublic, bool isGeneric, bool isAttribute, bool isEditorBrowsableStateAdvanced, bool isEnumBaseType) + { + private readonly ItemPropertyKind _properties = (isPublic ? ItemPropertyKind.IsPublic : 0) + | (isGeneric ? ItemPropertyKind.IsGeneric : 0) + | (isAttribute ? ItemPropertyKind.IsAttribute : 0) + | (isEnumBaseType ? ItemPropertyKind.IsEnumBaseType : 0) + | (isEditorBrowsableStateAdvanced ? ItemPropertyKind.IsEditorBrowsableStateAdvanced : 0); - public bool IsPublic - => (_properties & ItemPropertyKind.IsPublic) != 0; + public CompletionItem Item { get; } = item; - public bool IsGeneric - => (_properties & ItemPropertyKind.IsGeneric) != 0; + public bool IsPublic + => (_properties & ItemPropertyKind.IsPublic) != 0; - public bool IsAttribute - => (_properties & ItemPropertyKind.IsAttribute) != 0; + public bool IsGeneric + => (_properties & ItemPropertyKind.IsGeneric) != 0; - public bool IsEnumBaseType - => (_properties & ItemPropertyKind.IsEnumBaseType) != 0; + public bool IsAttribute + => (_properties & ItemPropertyKind.IsAttribute) != 0; - public bool IsEditorBrowsableStateAdvanced - => (_properties & ItemPropertyKind.IsEditorBrowsableStateAdvanced) != 0; + public bool IsEnumBaseType + => (_properties & ItemPropertyKind.IsEnumBaseType) != 0; - [Flags] - private enum ItemPropertyKind : byte - { - IsPublic = 1, - IsGeneric = 2, - IsAttribute = 4, - IsEnumBaseType = 8, - IsEditorBrowsableStateAdvanced = 16 - } + public bool IsEditorBrowsableStateAdvanced + => (_properties & ItemPropertyKind.IsEditorBrowsableStateAdvanced) != 0; + + [Flags] + private enum ItemPropertyKind : byte + { + IsPublic = 1, + IsGeneric = 2, + IsAttribute = 4, + IsEnumBaseType = 8, + IsEditorBrowsableStateAdvanced = 16 } } } diff --git a/src/Features/Core/Portable/Completion/Providers/MemberInsertingCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/MemberInsertingCompletionItem.cs index 840c90b3fb7b4..026b44ed4591e 100644 --- a/src/Features/Core/Portable/Completion/Providers/MemberInsertingCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/MemberInsertingCompletionItem.cs @@ -9,69 +9,68 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.LanguageService; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal class MemberInsertionCompletionItem { - internal class MemberInsertionCompletionItem + public static CompletionItem Create( + string displayText, + string displayTextSuffix, + DeclarationModifiers modifiers, + int line, + ISymbol symbol, + SyntaxToken token, + int descriptionPosition, + CompletionItemRules rules) { - public static CompletionItem Create( - string displayText, - string displayTextSuffix, - DeclarationModifiers modifiers, - int line, - ISymbol symbol, - SyntaxToken token, - int descriptionPosition, - CompletionItemRules rules) - { - var props = ImmutableArray.Create( - new KeyValuePair("Line", line.ToString()), - new KeyValuePair("Modifiers", modifiers.ToString()), - new KeyValuePair("TokenSpanEnd", token.Span.End.ToString())); + var props = ImmutableArray.Create( + new KeyValuePair("Line", line.ToString()), + new KeyValuePair("Modifiers", modifiers.ToString()), + new KeyValuePair("TokenSpanEnd", token.Span.End.ToString())); - return SymbolCompletionItem.CreateWithSymbolId( - displayText: displayText, - displayTextSuffix: displayTextSuffix, - symbols: ImmutableArray.Create(symbol), - contextPosition: descriptionPosition, - properties: props, - rules: rules, - isComplexTextEdit: true); - } + return SymbolCompletionItem.CreateWithSymbolId( + displayText: displayText, + displayTextSuffix: displayTextSuffix, + symbols: ImmutableArray.Create(symbol), + contextPosition: descriptionPosition, + properties: props, + rules: rules, + isComplexTextEdit: true); + } - public static Task GetDescriptionAsync(CompletionItem item, Document document, SymbolDescriptionOptions options, CancellationToken cancellationToken) - => SymbolCompletionItem.GetDescriptionAsync(item, document, options, cancellationToken); + public static Task GetDescriptionAsync(CompletionItem item, Document document, SymbolDescriptionOptions options, CancellationToken cancellationToken) + => SymbolCompletionItem.GetDescriptionAsync(item, document, options, cancellationToken); - public static DeclarationModifiers GetModifiers(CompletionItem item) + public static DeclarationModifiers GetModifiers(CompletionItem item) + { + if (item.TryGetProperty("Modifiers", out var text) && + DeclarationModifiers.TryParse(text, out var modifiers)) { - if (item.TryGetProperty("Modifiers", out var text) && - DeclarationModifiers.TryParse(text, out var modifiers)) - { - return modifiers; - } - - return default; + return modifiers; } - public static int GetLine(CompletionItem item) - { - if (item.TryGetProperty("Line", out var text) - && int.TryParse(text, out var number)) - { - return number; - } + return default; + } - return 0; + public static int GetLine(CompletionItem item) + { + if (item.TryGetProperty("Line", out var text) + && int.TryParse(text, out var number)) + { + return number; } - public static int GetTokenSpanEnd(CompletionItem item) - { - if (item.TryGetProperty("TokenSpanEnd", out var text) - && int.TryParse(text, out var number)) - { - return number; - } + return 0; + } - return 0; + public static int GetTokenSpanEnd(CompletionItem item) + { + if (item.TryGetProperty("TokenSpanEnd", out var text) + && int.TryParse(text, out var number)) + { + return number; } + + return 0; } } diff --git a/src/Features/Core/Portable/Completion/Providers/RecommendedKeyword.cs b/src/Features/Core/Portable/Completion/Providers/RecommendedKeyword.cs index 87de9755520a9..869ffdd646947 100644 --- a/src/Features/Core/Portable/Completion/Providers/RecommendedKeyword.cs +++ b/src/Features/Core/Portable/Completion/Providers/RecommendedKeyword.cs @@ -7,40 +7,39 @@ using System.Threading; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal sealed class RecommendedKeyword( + string keyword, + Glyph glyph, + Func> descriptionFactory, + bool isIntrinsic = false, + bool shouldFormatOnCommit = false, + int? matchPriority = null) { - internal sealed class RecommendedKeyword( - string keyword, - Glyph glyph, - Func> descriptionFactory, - bool isIntrinsic = false, - bool shouldFormatOnCommit = false, - int? matchPriority = null) + public Glyph Glyph { get; } = glyph; + public string Keyword { get; } = keyword; + public Func> DescriptionFactory { get; } = descriptionFactory; + public bool IsIntrinsic { get; } = isIntrinsic; + public bool ShouldFormatOnCommit { get; } = shouldFormatOnCommit; + public int MatchPriority { get; } = matchPriority ?? Completion.MatchPriority.Default; + + public RecommendedKeyword(string keyword, string toolTip = "", Glyph glyph = Glyph.Keyword, bool isIntrinsic = false, bool shouldFormatOnCommit = false, int? matchPriority = null) + : this(keyword, glyph, _ => CreateDisplayParts(keyword, toolTip), isIntrinsic, shouldFormatOnCommit, matchPriority) { - public Glyph Glyph { get; } = glyph; - public string Keyword { get; } = keyword; - public Func> DescriptionFactory { get; } = descriptionFactory; - public bool IsIntrinsic { get; } = isIntrinsic; - public bool ShouldFormatOnCommit { get; } = shouldFormatOnCommit; - public int MatchPriority { get; } = matchPriority ?? Completion.MatchPriority.Default; + } - public RecommendedKeyword(string keyword, string toolTip = "", Glyph glyph = Glyph.Keyword, bool isIntrinsic = false, bool shouldFormatOnCommit = false, int? matchPriority = null) - : this(keyword, glyph, _ => CreateDisplayParts(keyword, toolTip), isIntrinsic, shouldFormatOnCommit, matchPriority) - { - } + internal static ImmutableArray CreateDisplayParts(string keyword, string toolTip) + { + var textContentBuilder = new System.Collections.Generic.List(); + textContentBuilder.AddText(string.Format(FeaturesResources._0_Keyword, keyword)); - internal static ImmutableArray CreateDisplayParts(string keyword, string toolTip) + if (!string.IsNullOrEmpty(toolTip)) { - var textContentBuilder = new System.Collections.Generic.List(); - textContentBuilder.AddText(string.Format(FeaturesResources._0_Keyword, keyword)); - - if (!string.IsNullOrEmpty(toolTip)) - { - textContentBuilder.AddLineBreak(); - textContentBuilder.AddText(toolTip); - } - - return textContentBuilder.ToImmutableArray(); + textContentBuilder.AddLineBreak(); + textContentBuilder.AddText(toolTip); } + + return textContentBuilder.ToImmutableArray(); } } diff --git a/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractDirectivePathCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractDirectivePathCompletionProvider.cs index 9777722814862..7dc9d44f55432 100644 --- a/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractDirectivePathCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractDirectivePathCompletionProvider.cs @@ -16,186 +16,185 @@ using Microsoft.CodeAnalysis.Scripting.Hosting; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractDirectivePathCompletionProvider : CompletionProvider { - internal abstract class AbstractDirectivePathCompletionProvider : CompletionProvider - { - protected static bool IsDirectorySeparator(char ch) - => ch == '/' || (ch == '\\' && !PathUtilities.IsUnixLikePlatform); + protected static bool IsDirectorySeparator(char ch) + => ch == '/' || (ch == '\\' && !PathUtilities.IsUnixLikePlatform); - protected abstract bool TryGetStringLiteralToken(SyntaxTree tree, int position, out SyntaxToken stringLiteral, CancellationToken cancellationToken); + protected abstract bool TryGetStringLiteralToken(SyntaxTree tree, int position, out SyntaxToken stringLiteral, CancellationToken cancellationToken); - /// - /// r for metadata reference directive, load for source file directive. - /// - protected abstract string DirectiveName { get; } + /// + /// r for metadata reference directive, load for source file directive. + /// + protected abstract string DirectiveName { get; } - public sealed override async Task ProvideCompletionsAsync(CompletionContext context) + public sealed override async Task ProvideCompletionsAsync(CompletionContext context) + { + try { - try - { - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (!TryGetStringLiteralToken(tree, position, out var stringLiteral, cancellationToken)) - { - return; - } + if (!TryGetStringLiteralToken(tree, position, out var stringLiteral, cancellationToken)) + { + return; + } - var literalValue = stringLiteral.ToString(); + var literalValue = stringLiteral.ToString(); - context.CompletionListSpan = GetTextChangeSpan( - quotedPath: literalValue, - quotedPathStart: stringLiteral.SpanStart, - position: position); + context.CompletionListSpan = GetTextChangeSpan( + quotedPath: literalValue, + quotedPathStart: stringLiteral.SpanStart, + position: position); - var pathThroughLastSlash = GetPathThroughLastSlash( - quotedPath: literalValue, - quotedPathStart: stringLiteral.SpanStart, - position: position); + var pathThroughLastSlash = GetPathThroughLastSlash( + quotedPath: literalValue, + quotedPathStart: stringLiteral.SpanStart, + position: position); - await ProvideCompletionsAsync(context, pathThroughLastSlash).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) - { - // nop - } + await ProvideCompletionsAsync(context, pathThroughLastSlash).ConfigureAwait(false); } - - public sealed override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General)) { - var lineStart = text.Lines.GetLineFromPosition(caretPosition).Start; - - // check if the line starts with {whitespace}#{whitespace}{DirectiveName}{whitespace}" + // nop + } + } - var poundIndex = text.IndexOfNonWhiteSpace(lineStart, caretPosition - lineStart); - if (poundIndex == -1 || text[poundIndex] != '#') - { - return false; - } + public sealed override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options) + { + var lineStart = text.Lines.GetLineFromPosition(caretPosition).Start; - var directiveNameStartIndex = text.IndexOfNonWhiteSpace(poundIndex + 1, caretPosition - poundIndex - 1); - if (directiveNameStartIndex == -1 || !text.ContentEquals(directiveNameStartIndex, DirectiveName)) - { - return false; - } + // check if the line starts with {whitespace}#{whitespace}{DirectiveName}{whitespace}" - var directiveNameEndIndex = directiveNameStartIndex + DirectiveName.Length; - var quoteIndex = text.IndexOfNonWhiteSpace(directiveNameEndIndex, caretPosition - directiveNameEndIndex); - if (quoteIndex == -1 || text[quoteIndex] != '"') - { - return false; - } + var poundIndex = text.IndexOfNonWhiteSpace(lineStart, caretPosition - lineStart); + if (poundIndex == -1 || text[poundIndex] != '#') + { + return false; + } - return true; + var directiveNameStartIndex = text.IndexOfNonWhiteSpace(poundIndex + 1, caretPosition - poundIndex - 1); + if (directiveNameStartIndex == -1 || !text.ContentEquals(directiveNameStartIndex, DirectiveName)) + { + return false; } - private static string GetPathThroughLastSlash(string quotedPath, int quotedPathStart, int position) + var directiveNameEndIndex = directiveNameStartIndex + DirectiveName.Length; + var quoteIndex = text.IndexOfNonWhiteSpace(directiveNameEndIndex, caretPosition - directiveNameEndIndex); + if (quoteIndex == -1 || text[quoteIndex] != '"') { - Contract.ThrowIfTrue(quotedPath[0] != '"'); + return false; + } - const int QuoteLength = 1; + return true; + } - var positionInQuotedPath = position - quotedPathStart; - var path = quotedPath[QuoteLength..positionInQuotedPath].Trim(); - var afterLastSlashIndex = AfterLastSlashIndex(path, path.Length); + private static string GetPathThroughLastSlash(string quotedPath, int quotedPathStart, int position) + { + Contract.ThrowIfTrue(quotedPath[0] != '"'); - // We want the portion up to, and including the last slash if there is one. That way if - // the user pops up completion in the middle of a path (i.e. "C:\Win") then we'll - // consider the path to be "C:\" and we will show appropriate completions. - return afterLastSlashIndex >= 0 ? path[..afterLastSlashIndex] : path; - } + const int QuoteLength = 1; - private static TextSpan GetTextChangeSpan(string quotedPath, int quotedPathStart, int position) - { - // We want the text change to be from after the last slash to the end of the quoted - // path. If there is no last slash, then we want it from right after the start quote - // character. - var positionInQuotedPath = position - quotedPathStart; + var positionInQuotedPath = position - quotedPathStart; + var path = quotedPath[QuoteLength..positionInQuotedPath].Trim(); + var afterLastSlashIndex = AfterLastSlashIndex(path, path.Length); - // Where we want to start tracking is right after the slash (if we have one), or else - // right after the string starts. - var afterLastSlashIndex = AfterLastSlashIndex(quotedPath, positionInQuotedPath); - var afterFirstQuote = 1; + // We want the portion up to, and including the last slash if there is one. That way if + // the user pops up completion in the middle of a path (i.e. "C:\Win") then we'll + // consider the path to be "C:\" and we will show appropriate completions. + return afterLastSlashIndex >= 0 ? path[..afterLastSlashIndex] : path; + } - var startIndex = Math.Max(afterLastSlashIndex, afterFirstQuote); - var endIndex = quotedPath.Length; + private static TextSpan GetTextChangeSpan(string quotedPath, int quotedPathStart, int position) + { + // We want the text change to be from after the last slash to the end of the quoted + // path. If there is no last slash, then we want it from right after the start quote + // character. + var positionInQuotedPath = position - quotedPathStart; - // If the string ends with a quote, the we do not want to consume that. - if (EndsWithQuote(quotedPath)) - { - endIndex--; - } + // Where we want to start tracking is right after the slash (if we have one), or else + // right after the string starts. + var afterLastSlashIndex = AfterLastSlashIndex(quotedPath, positionInQuotedPath); + var afterFirstQuote = 1; + + var startIndex = Math.Max(afterLastSlashIndex, afterFirstQuote); + var endIndex = quotedPath.Length; - return TextSpan.FromBounds(startIndex + quotedPathStart, endIndex + quotedPathStart); + // If the string ends with a quote, the we do not want to consume that. + if (EndsWithQuote(quotedPath)) + { + endIndex--; } - private static bool EndsWithQuote(string quotedPath) - => quotedPath is [.., _, '"']; + return TextSpan.FromBounds(startIndex + quotedPathStart, endIndex + quotedPathStart); + } - /// - /// Returns the index right after the last slash that precedes 'position'. If there is no - /// slash in the string, -1 is returned. - /// - private static int AfterLastSlashIndex(string text, int position) - { - // Position might be out of bounds of the string (if the string is unterminated. Make - // sure it's within bounds. - position = Math.Min(position, text.Length - 1); + private static bool EndsWithQuote(string quotedPath) + => quotedPath is [.., _, '"']; - int index; - if ((index = text.LastIndexOf('/', position)) >= 0 || - !PathUtilities.IsUnixLikePlatform && (index = text.LastIndexOf('\\', position)) >= 0) - { - return index + 1; - } + /// + /// Returns the index right after the last slash that precedes 'position'. If there is no + /// slash in the string, -1 is returned. + /// + private static int AfterLastSlashIndex(string text, int position) + { + // Position might be out of bounds of the string (if the string is unterminated. Make + // sure it's within bounds. + position = Math.Min(position, text.Length - 1); - return -1; + int index; + if ((index = text.LastIndexOf('/', position)) >= 0 || + !PathUtilities.IsUnixLikePlatform && (index = text.LastIndexOf('\\', position)) >= 0) + { + return index + 1; } - protected abstract Task ProvideCompletionsAsync(CompletionContext context, string pathThroughLastSlash); + return -1; + } - protected static FileSystemCompletionHelper GetFileSystemCompletionHelper( - Document document, - Glyph itemGlyph, - ImmutableArray extensions, - CompletionItemRules completionRules) - { - ImmutableArray referenceSearchPaths; - string? baseDirectory; - if (document.Project.CompilationOptions?.MetadataReferenceResolver is RuntimeMetadataReferenceResolver resolver) - { - referenceSearchPaths = resolver.PathResolver.SearchPaths; - baseDirectory = resolver.PathResolver.BaseDirectory; - } - else - { - referenceSearchPaths = []; - baseDirectory = null; - } + protected abstract Task ProvideCompletionsAsync(CompletionContext context, string pathThroughLastSlash); - return new FileSystemCompletionHelper( - Glyph.OpenFolder, - itemGlyph, - referenceSearchPaths, - GetBaseDirectory(document, baseDirectory), - extensions, - completionRules); + protected static FileSystemCompletionHelper GetFileSystemCompletionHelper( + Document document, + Glyph itemGlyph, + ImmutableArray extensions, + CompletionItemRules completionRules) + { + ImmutableArray referenceSearchPaths; + string? baseDirectory; + if (document.Project.CompilationOptions?.MetadataReferenceResolver is RuntimeMetadataReferenceResolver resolver) + { + referenceSearchPaths = resolver.PathResolver.SearchPaths; + baseDirectory = resolver.PathResolver.BaseDirectory; } - - private static string? GetBaseDirectory(Document document, string? baseDirectory) + else { - var result = PathUtilities.GetDirectoryName(document.FilePath); - if (!PathUtilities.IsAbsolute(result)) - { - result = baseDirectory; - Debug.Assert(result == null || PathUtilities.IsAbsolute(result)); - } + referenceSearchPaths = []; + baseDirectory = null; + } + + return new FileSystemCompletionHelper( + Glyph.OpenFolder, + itemGlyph, + referenceSearchPaths, + GetBaseDirectory(document, baseDirectory), + extensions, + completionRules); + } - return result; + private static string? GetBaseDirectory(Document document, string? baseDirectory) + { + var result = PathUtilities.GetDirectoryName(document.FilePath); + if (!PathUtilities.IsAbsolute(result)) + { + result = baseDirectory; + Debug.Assert(result == null || PathUtilities.IsAbsolute(result)); } + + return result; } } diff --git a/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractLoadDirectiveCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractLoadDirectiveCompletionProvider.cs index 45ab4e001d666..21b7f3f91b7d1 100644 --- a/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractLoadDirectiveCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractLoadDirectiveCompletionProvider.cs @@ -10,37 +10,36 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractLoadDirectiveCompletionProvider : AbstractDirectivePathCompletionProvider { - internal abstract class AbstractLoadDirectiveCompletionProvider : AbstractDirectivePathCompletionProvider - { - private static readonly CompletionItemRules s_rules = CompletionItemRules.Create( - filterCharacterRules: [], - commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, GetCommitCharacters())], - enterKeyRule: EnterKeyRule.Never, - selectionBehavior: CompletionItemSelectionBehavior.HardSelection); + private static readonly CompletionItemRules s_rules = CompletionItemRules.Create( + filterCharacterRules: [], + commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, GetCommitCharacters())], + enterKeyRule: EnterKeyRule.Never, + selectionBehavior: CompletionItemSelectionBehavior.HardSelection); - private static ImmutableArray GetCommitCharacters() + private static ImmutableArray GetCommitCharacters() + { + using var builderDisposer = ArrayBuilder.GetInstance(out var builder); + builder.Add('"'); + if (PathUtilities.IsUnixLikePlatform) { - using var builderDisposer = ArrayBuilder.GetInstance(out var builder); - builder.Add('"'); - if (PathUtilities.IsUnixLikePlatform) - { - builder.Add('/'); - } - else - { - builder.Add('/'); - builder.Add('\\'); - } - - return builder.ToImmutable(); + builder.Add('/'); } - - protected override async Task ProvideCompletionsAsync(CompletionContext context, string pathThroughLastSlash) + else { - var helper = GetFileSystemCompletionHelper(context.Document, Glyph.CSharpFile, [".csx"], s_rules); - context.AddItems(await helper.GetItemsAsync(pathThroughLastSlash, context.CancellationToken).ConfigureAwait(false)); + builder.Add('/'); + builder.Add('\\'); } + + return builder.ToImmutable(); + } + + protected override async Task ProvideCompletionsAsync(CompletionContext context, string pathThroughLastSlash) + { + var helper = GetFileSystemCompletionHelper(context.Document, Glyph.CSharpFile, [".csx"], s_rules); + context.AddItems(await helper.GetItemsAsync(pathThroughLastSlash, context.CancellationToken).ConfigureAwait(false)); } } diff --git a/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractReferenceDirectiveCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractReferenceDirectiveCompletionProvider.cs index bf80564eebe85..2776564e7fc6a 100644 --- a/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractReferenceDirectiveCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/Scripting/AbstractReferenceDirectiveCompletionProvider.cs @@ -11,65 +11,64 @@ using Microsoft.CodeAnalysis.Scripting.Hosting; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal abstract class AbstractReferenceDirectiveCompletionProvider : AbstractDirectivePathCompletionProvider { - internal abstract class AbstractReferenceDirectiveCompletionProvider : AbstractDirectivePathCompletionProvider + private static readonly CompletionItemRules s_rules = CompletionItemRules.Create( + filterCharacterRules: [], + commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, GetCommitCharacters())], + enterKeyRule: EnterKeyRule.Never, + selectionBehavior: CompletionItemSelectionBehavior.HardSelection); + + private static readonly char[] s_pathIndicators = ['/', '\\', ':']; + + private static ImmutableArray GetCommitCharacters() { - private static readonly CompletionItemRules s_rules = CompletionItemRules.Create( - filterCharacterRules: [], - commitCharacterRules: [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, GetCommitCharacters())], - enterKeyRule: EnterKeyRule.Never, - selectionBehavior: CompletionItemSelectionBehavior.HardSelection); + using var builderDisposer = ArrayBuilder.GetInstance(out var builder); - private static readonly char[] s_pathIndicators = ['/', '\\', ':']; + builder.Add('"'); - private static ImmutableArray GetCommitCharacters() + if (PathUtilities.IsUnixLikePlatform) { - using var builderDisposer = ArrayBuilder.GetInstance(out var builder); + builder.Add('/'); + } + else + { + builder.Add('/'); + builder.Add('\\'); + } - builder.Add('"'); + if (GacFileResolver.IsAvailable) + { + builder.Add(','); + } - if (PathUtilities.IsUnixLikePlatform) - { - builder.Add('/'); - } - else + return builder.ToImmutable(); + } + + protected override async Task ProvideCompletionsAsync(CompletionContext context, string pathThroughLastSlash) + { + var resolver = context.Document.Project.CompilationOptions.MetadataReferenceResolver as RuntimeMetadataReferenceResolver; + if (resolver != null && pathThroughLastSlash.IndexOfAny(s_pathIndicators) < 0) + { + foreach (var (name, path) in resolver.TrustedPlatformAssemblies) { - builder.Add('/'); - builder.Add('\\'); + context.AddItem(CommonCompletionItem.Create(name, displayTextSuffix: "", glyph: Glyph.Assembly, rules: s_rules)); + context.AddItem(CommonCompletionItem.Create(PathUtilities.GetFileName(path, includeExtension: true), displayTextSuffix: "", glyph: Glyph.Assembly, rules: s_rules)); } - if (GacFileResolver.IsAvailable) + if (resolver.GacFileResolver is object) { - builder.Add(','); + var gacHelper = new GlobalAssemblyCacheCompletionHelper(s_rules); + context.AddItems(await gacHelper.GetItemsAsync(pathThroughLastSlash, context.CancellationToken).ConfigureAwait(false)); } - - return builder.ToImmutable(); } - protected override async Task ProvideCompletionsAsync(CompletionContext context, string pathThroughLastSlash) + if (pathThroughLastSlash.IndexOf(',') < 0) { - var resolver = context.Document.Project.CompilationOptions.MetadataReferenceResolver as RuntimeMetadataReferenceResolver; - if (resolver != null && pathThroughLastSlash.IndexOfAny(s_pathIndicators) < 0) - { - foreach (var (name, path) in resolver.TrustedPlatformAssemblies) - { - context.AddItem(CommonCompletionItem.Create(name, displayTextSuffix: "", glyph: Glyph.Assembly, rules: s_rules)); - context.AddItem(CommonCompletionItem.Create(PathUtilities.GetFileName(path, includeExtension: true), displayTextSuffix: "", glyph: Glyph.Assembly, rules: s_rules)); - } - - if (resolver.GacFileResolver is object) - { - var gacHelper = new GlobalAssemblyCacheCompletionHelper(s_rules); - context.AddItems(await gacHelper.GetItemsAsync(pathThroughLastSlash, context.CancellationToken).ConfigureAwait(false)); - } - } - - if (pathThroughLastSlash.IndexOf(',') < 0) - { - var helper = GetFileSystemCompletionHelper(context.Document, Glyph.Assembly, RuntimeMetadataReferenceResolver.AssemblyExtensions, s_rules); - context.AddItems(await helper.GetItemsAsync(pathThroughLastSlash, context.CancellationToken).ConfigureAwait(false)); - } + var helper = GetFileSystemCompletionHelper(context.Document, Glyph.Assembly, RuntimeMetadataReferenceResolver.AssemblyExtensions, s_rules); + context.AddItems(await helper.GetItemsAsync(pathThroughLastSlash, context.CancellationToken).ConfigureAwait(false)); } } } diff --git a/src/Features/Core/Portable/Completion/Providers/Scripting/GlobalAssemblyCacheCompletionHelper.cs b/src/Features/Core/Portable/Completion/Providers/Scripting/GlobalAssemblyCacheCompletionHelper.cs index 4c2142a195dc6..103ffcbb2032b 100644 --- a/src/Features/Core/Portable/Completion/Providers/Scripting/GlobalAssemblyCacheCompletionHelper.cs +++ b/src/Features/Core/Portable/Completion/Providers/Scripting/GlobalAssemblyCacheCompletionHelper.cs @@ -16,56 +16,55 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal sealed class GlobalAssemblyCacheCompletionHelper { - internal sealed class GlobalAssemblyCacheCompletionHelper - { - private static readonly Lazy> s_lazyAssemblySimpleNames = - new(() => GlobalAssemblyCache.Instance.GetAssemblySimpleNames().ToList()); + private static readonly Lazy> s_lazyAssemblySimpleNames = + new(() => GlobalAssemblyCache.Instance.GetAssemblySimpleNames().ToList()); - private readonly CompletionItemRules _itemRules; + private readonly CompletionItemRules _itemRules; - public GlobalAssemblyCacheCompletionHelper(CompletionItemRules itemRules) - { - Debug.Assert(itemRules != null); - _itemRules = itemRules; - } + public GlobalAssemblyCacheCompletionHelper(CompletionItemRules itemRules) + { + Debug.Assert(itemRules != null); + _itemRules = itemRules; + } - public Task> GetItemsAsync(string directoryPath, CancellationToken cancellationToken) - => Task.Run(() => GetItems(directoryPath, cancellationToken)); + public Task> GetItemsAsync(string directoryPath, CancellationToken cancellationToken) + => Task.Run(() => GetItems(directoryPath, cancellationToken)); - // internal for testing - internal ImmutableArray GetItems(string directoryPath, CancellationToken cancellationToken) - { - using var resultDisposer = ArrayBuilder.GetInstance(out var result); + // internal for testing + internal ImmutableArray GetItems(string directoryPath, CancellationToken cancellationToken) + { + using var resultDisposer = ArrayBuilder.GetInstance(out var result); - var comma = directoryPath.IndexOf(','); - if (comma >= 0) + var comma = directoryPath.IndexOf(','); + if (comma >= 0) + { + var partialName = directoryPath[..comma]; + foreach (var identity in GetAssemblyIdentities(partialName)) { - var partialName = directoryPath[..comma]; - foreach (var identity in GetAssemblyIdentities(partialName)) - { - result.Add(CommonCompletionItem.Create( - identity.GetDisplayName(), displayTextSuffix: "", glyph: Glyph.Assembly, rules: _itemRules)); - } + result.Add(CommonCompletionItem.Create( + identity.GetDisplayName(), displayTextSuffix: "", glyph: Glyph.Assembly, rules: _itemRules)); } - else + } + else + { + foreach (var displayName in s_lazyAssemblySimpleNames.Value) { - foreach (var displayName in s_lazyAssemblySimpleNames.Value) - { - cancellationToken.ThrowIfCancellationRequested(); - result.Add(CommonCompletionItem.Create( - displayName, displayTextSuffix: "", glyph: Glyph.Assembly, rules: _itemRules)); - } + cancellationToken.ThrowIfCancellationRequested(); + result.Add(CommonCompletionItem.Create( + displayName, displayTextSuffix: "", glyph: Glyph.Assembly, rules: _itemRules)); } - - return result.ToImmutable(); } - private static IEnumerable GetAssemblyIdentities(string partialName) - { - return IOUtilities.PerformIO(() => GlobalAssemblyCache.Instance.GetAssemblyIdentities(partialName), - SpecializedCollections.EmptyEnumerable()); - } + return result.ToImmutable(); + } + + private static IEnumerable GetAssemblyIdentities(string partialName) + { + return IOUtilities.PerformIO(() => GlobalAssemblyCache.Instance.GetAssemblyIdentities(partialName), + SpecializedCollections.EmptyEnumerable()); } } diff --git a/src/Features/Core/Portable/Completion/Providers/Snippets/AbstractSnippetCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/Snippets/AbstractSnippetCompletionProvider.cs index 2b8d475150b64..2262c164dfb1b 100644 --- a/src/Features/Core/Portable/Completion/Providers/Snippets/AbstractSnippetCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/Snippets/AbstractSnippetCompletionProvider.cs @@ -13,125 +13,124 @@ using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Completion.Providers.Snippets +namespace Microsoft.CodeAnalysis.Completion.Providers.Snippets; + +internal abstract class AbstractSnippetCompletionProvider : CompletionProvider { - internal abstract class AbstractSnippetCompletionProvider : CompletionProvider + internal override bool IsSnippetProvider => true; + + public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) { - internal override bool IsSnippetProvider => true; + // This retrieves the document without the text used to invoke completion + // as well as the new cursor position after that has been removed. + var (strippedDocument, position) = await GetDocumentWithoutInvokingTextAsync(document, SnippetCompletionItem.GetInvocationPosition(item), cancellationToken).ConfigureAwait(false); + var service = strippedDocument.GetRequiredLanguageService(); + var snippetIdentifier = SnippetCompletionItem.GetSnippetIdentifier(item); + var snippetProvider = service.GetSnippetProvider(snippetIdentifier); + + // Logging for telemetry. + Logger.Log(FunctionId.Completion_SemanticSnippets, $"Name: {snippetIdentifier}", LogLevel.Information); + + // This retrieves the generated Snippet + var snippet = await snippetProvider.GetSnippetAsync(strippedDocument, position, cancellationToken).ConfigureAwait(false); + var strippedText = await strippedDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + + // This introduces the text changes of the snippet into the document with the completion invoking text + var allChangesText = strippedText.WithChanges(snippet.TextChanges); + + // This retrieves ALL text changes from the original document which includes the TextChanges from the snippet + // as well as the clean up. + var allChangesDocument = document.WithText(allChangesText); + var allTextChanges = await allChangesDocument.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false); - public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) + var change = Utilities.Collapse(allChangesText, allTextChanges.AsImmutable()); + + // Converts the snippet to an LSP formatted snippet string. + var lspSnippet = await RoslynLSPSnippetConverter.GenerateLSPSnippetAsync(allChangesDocument, snippet.CursorPosition, snippet.Placeholders, change, item.Span.Start, cancellationToken).ConfigureAwait(false); + + // If the TextChanges retrieved starts after the trigger point of the CompletionItem, + // then we need to move the bounds backwards and encapsulate the trigger point and adjust the changed text. + if (change.Span.Start > item.Span.Start) { - // This retrieves the document without the text used to invoke completion - // as well as the new cursor position after that has been removed. - var (strippedDocument, position) = await GetDocumentWithoutInvokingTextAsync(document, SnippetCompletionItem.GetInvocationPosition(item), cancellationToken).ConfigureAwait(false); - var service = strippedDocument.GetRequiredLanguageService(); - var snippetIdentifier = SnippetCompletionItem.GetSnippetIdentifier(item); - var snippetProvider = service.GetSnippetProvider(snippetIdentifier); - - // Logging for telemetry. - Logger.Log(FunctionId.Completion_SemanticSnippets, $"Name: {snippetIdentifier}", LogLevel.Information); - - // This retrieves the generated Snippet - var snippet = await snippetProvider.GetSnippetAsync(strippedDocument, position, cancellationToken).ConfigureAwait(false); - var strippedText = await strippedDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - - // This introduces the text changes of the snippet into the document with the completion invoking text - var allChangesText = strippedText.WithChanges(snippet.TextChanges); - - // This retrieves ALL text changes from the original document which includes the TextChanges from the snippet - // as well as the clean up. - var allChangesDocument = document.WithText(allChangesText); - var allTextChanges = await allChangesDocument.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false); - - var change = Utilities.Collapse(allChangesText, allTextChanges.AsImmutable()); - - // Converts the snippet to an LSP formatted snippet string. - var lspSnippet = await RoslynLSPSnippetConverter.GenerateLSPSnippetAsync(allChangesDocument, snippet.CursorPosition, snippet.Placeholders, change, item.Span.Start, cancellationToken).ConfigureAwait(false); - - // If the TextChanges retrieved starts after the trigger point of the CompletionItem, - // then we need to move the bounds backwards and encapsulate the trigger point and adjust the changed text. - if (change.Span.Start > item.Span.Start) - { - var textSpan = TextSpan.FromBounds(item.Span.Start, change.Span.End); - var snippetText = allChangesText.GetSubText(textSpan).ToString(); - change = new TextChange(textSpan, snippetText); - } - - var props = ImmutableDictionary.Empty - .Add(SnippetCompletionItem.LSPSnippetKey, lspSnippet); - - return CompletionChange.Create(change, allTextChanges.AsImmutable(), properties: props, snippet.CursorPosition, includesCommitCharacter: true); + var textSpan = TextSpan.FromBounds(item.Span.Start, change.Span.End); + var snippetText = allChangesText.GetSubText(textSpan).ToString(); + change = new TextChange(textSpan, snippetText); } - public override async Task ProvideCompletionsAsync(CompletionContext context) + var props = ImmutableDictionary.Empty + .Add(SnippetCompletionItem.LSPSnippetKey, lspSnippet); + + return CompletionChange.Create(change, allTextChanges.AsImmutable(), properties: props, snippet.CursorPosition, includesCommitCharacter: true); + } + + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + if (!context.CompletionOptions.ShouldShowNewSnippetExperience(context.Document)) { - if (!context.CompletionOptions.ShouldShowNewSnippetExperience(context.Document)) - { - return; - } - - var document = context.Document; - var cancellationToken = context.CancellationToken; - var position = context.Position; - var service = document.GetLanguageService(); - - if (service == null) - { - return; - } - - var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); - var snippetContext = new SnippetContext(syntaxContext); - var snippets = await service.GetSnippetsAsync(snippetContext, cancellationToken).ConfigureAwait(false); - - foreach (var snippetData in snippets) - { - var completionItem = SnippetCompletionItem.Create( - displayText: snippetData.Identifier, - displayTextSuffix: "", - position: position, - snippetIdentifier: snippetData.Identifier, - glyph: Glyph.Snippet, - description: (snippetData.Description + Environment.NewLine + string.Format(FeaturesResources.Code_snippet_for_0, snippetData.Description)).ToSymbolDisplayParts(), - inlineDescription: snippetData.Description, - additionalFilterTexts: snippetData.AdditionalFilterTexts); - context.AddItem(completionItem); - } + return; } - internal override async Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + var document = context.Document; + var cancellationToken = context.CancellationToken; + var position = context.Position; + var service = document.GetLanguageService(); + + if (service == null) { - return await Task.FromResult(CommonCompletionItem.GetDescription(item)).ConfigureAwait(false); + return; } - /// Gets the document without whatever text was used to invoke the completion. - /// Also gets the new position the cursor will be on. - /// Returns the original document and position if completion was invoked using Ctrl-Space. - /// - /// public void Method() - /// { - /// $$ //invoked by typing Ctrl-Space - /// } - /// Example invoking when span is not empty: - /// public void Method() - /// { - /// Wr$$ //invoked by typing out the completion - /// } - private static async Task<(Document, int)> GetDocumentWithoutInvokingTextAsync(Document document, int position, CancellationToken cancellationToken) + var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false); + var snippetContext = new SnippetContext(syntaxContext); + var snippets = await service.GetSnippetsAsync(snippetContext, cancellationToken).ConfigureAwait(false); + + foreach (var snippetData in snippets) { - var originalText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var completionItem = SnippetCompletionItem.Create( + displayText: snippetData.Identifier, + displayTextSuffix: "", + position: position, + snippetIdentifier: snippetData.Identifier, + glyph: Glyph.Snippet, + description: (snippetData.Description + Environment.NewLine + string.Format(FeaturesResources.Code_snippet_for_0, snippetData.Description)).ToSymbolDisplayParts(), + inlineDescription: snippetData.Description, + additionalFilterTexts: snippetData.AdditionalFilterTexts); + context.AddItem(completionItem); + } + } - // Uses the existing CompletionService logic to find the TextSpan we want to use for the document sans invoking text - var completionService = document.GetRequiredLanguageService(); - var span = completionService.GetDefaultCompletionListSpan(originalText, position); + internal override async Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + { + return await Task.FromResult(CommonCompletionItem.GetDescription(item)).ConfigureAwait(false); + } - var textChange = new TextChange(span, string.Empty); - originalText = originalText.WithChanges(textChange); + /// Gets the document without whatever text was used to invoke the completion. + /// Also gets the new position the cursor will be on. + /// Returns the original document and position if completion was invoked using Ctrl-Space. + /// + /// public void Method() + /// { + /// $$ //invoked by typing Ctrl-Space + /// } + /// Example invoking when span is not empty: + /// public void Method() + /// { + /// Wr$$ //invoked by typing out the completion + /// } + private static async Task<(Document, int)> GetDocumentWithoutInvokingTextAsync(Document document, int position, CancellationToken cancellationToken) + { + var originalText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - // The document might not be frozen, so make sure we freeze it here to avoid triggering source generator - // which is not needed for snippet completion and will cause perf issue. - var newDocument = document.WithText(originalText).WithFrozenPartialSemantics(cancellationToken); - return (newDocument, span.Start); - } + // Uses the existing CompletionService logic to find the TextSpan we want to use for the document sans invoking text + var completionService = document.GetRequiredLanguageService(); + var span = completionService.GetDefaultCompletionListSpan(originalText, position); + + var textChange = new TextChange(span, string.Empty); + originalText = originalText.WithChanges(textChange); + + // The document might not be frozen, so make sure we freeze it here to avoid triggering source generator + // which is not needed for snippet completion and will cause perf issue. + var newDocument = document.WithText(originalText).WithFrozenPartialSemantics(cancellationToken); + return (newDocument, span.Start); } } diff --git a/src/Features/Core/Portable/Completion/Providers/Snippets/SnippetCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/Snippets/SnippetCompletionItem.cs index 98a41cff924df..63e7315b70148 100644 --- a/src/Features/Core/Portable/Completion/Providers/Snippets/SnippetCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/Snippets/SnippetCompletionItem.cs @@ -6,58 +6,57 @@ using System.Collections.Immutable; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers.Snippets +namespace Microsoft.CodeAnalysis.Completion.Providers.Snippets; + +internal class SnippetCompletionItem { - internal class SnippetCompletionItem + public static string LSPSnippetKey = "LSPSnippet"; + public static string SnippetIdentifierKey = "SnippetIdentifier"; + + public static CompletionItem Create( + string displayText, + string displayTextSuffix, + int position, + string snippetIdentifier, + Glyph glyph, + ImmutableArray description, + string inlineDescription, + ImmutableArray additionalFilterTexts) + { + var props = ImmutableArray.Create( + new KeyValuePair("Position", position.ToString()), + new KeyValuePair(SnippetIdentifierKey, snippetIdentifier)); + + return CommonCompletionItem.Create( + displayText: displayText, + displayTextSuffix: displayTextSuffix, + glyph: glyph, + description: description, + // Adding a space after the identifier string that way it will always be sorted after a keyword. + sortText: snippetIdentifier + " ", + filterText: snippetIdentifier, + properties: props, + isComplexTextEdit: true, + inlineDescription: inlineDescription, + rules: CompletionItemRules.Default) + .WithAdditionalFilterTexts(additionalFilterTexts); + } + + public static string GetSnippetIdentifier(CompletionItem item) + { + Contract.ThrowIfFalse(item.TryGetProperty(SnippetIdentifierKey, out var text)); + return text; + } + + public static int GetInvocationPosition(CompletionItem item) + { + Contract.ThrowIfFalse(item.TryGetProperty("Position", out var text)); + Contract.ThrowIfFalse(int.TryParse(text, out var num)); + return num; + } + + public static bool IsSnippet(CompletionItem item) { - public static string LSPSnippetKey = "LSPSnippet"; - public static string SnippetIdentifierKey = "SnippetIdentifier"; - - public static CompletionItem Create( - string displayText, - string displayTextSuffix, - int position, - string snippetIdentifier, - Glyph glyph, - ImmutableArray description, - string inlineDescription, - ImmutableArray additionalFilterTexts) - { - var props = ImmutableArray.Create( - new KeyValuePair("Position", position.ToString()), - new KeyValuePair(SnippetIdentifierKey, snippetIdentifier)); - - return CommonCompletionItem.Create( - displayText: displayText, - displayTextSuffix: displayTextSuffix, - glyph: glyph, - description: description, - // Adding a space after the identifier string that way it will always be sorted after a keyword. - sortText: snippetIdentifier + " ", - filterText: snippetIdentifier, - properties: props, - isComplexTextEdit: true, - inlineDescription: inlineDescription, - rules: CompletionItemRules.Default) - .WithAdditionalFilterTexts(additionalFilterTexts); - } - - public static string GetSnippetIdentifier(CompletionItem item) - { - Contract.ThrowIfFalse(item.TryGetProperty(SnippetIdentifierKey, out var text)); - return text; - } - - public static int GetInvocationPosition(CompletionItem item) - { - Contract.ThrowIfFalse(item.TryGetProperty("Position", out var text)); - Contract.ThrowIfFalse(int.TryParse(text, out var num)); - return num; - } - - public static bool IsSnippet(CompletionItem item) - { - return item.TryGetProperty(SnippetIdentifierKey, out var _); - } + return item.TryGetProperty(SnippetIdentifierKey, out var _); } } diff --git a/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs index b31ece09c12f7..1f3c651245e43 100644 --- a/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs @@ -14,356 +14,355 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal static class SymbolCompletionItem { - internal static class SymbolCompletionItem + private const string InsertionTextProperty = "InsertionText"; + + private static readonly Action, ArrayBuilder>> s_addSymbolEncoding = AddSymbolEncoding; + private static readonly Action, ArrayBuilder>> s_addSymbolInfo = AddSymbolInfo; + private static readonly char[] s_projectSeperators = [';']; + + private static CompletionItem CreateWorker( + string displayText, + string? displayTextSuffix, + IReadOnlyList symbols, + CompletionItemRules rules, + int contextPosition, + Action, ArrayBuilder>> symbolEncoder, + string? sortText = null, + string? insertionText = null, + string? filterText = null, + SupportedPlatformData? supportedPlatforms = null, + ImmutableArray> properties = default, + ImmutableArray tags = default, + string? displayTextPrefix = null, + string? inlineDescription = null, + Glyph? glyph = null, + bool isComplexTextEdit = false) { - private const string InsertionTextProperty = "InsertionText"; - - private static readonly Action, ArrayBuilder>> s_addSymbolEncoding = AddSymbolEncoding; - private static readonly Action, ArrayBuilder>> s_addSymbolInfo = AddSymbolInfo; - private static readonly char[] s_projectSeperators = [';']; - - private static CompletionItem CreateWorker( - string displayText, - string? displayTextSuffix, - IReadOnlyList symbols, - CompletionItemRules rules, - int contextPosition, - Action, ArrayBuilder>> symbolEncoder, - string? sortText = null, - string? insertionText = null, - string? filterText = null, - SupportedPlatformData? supportedPlatforms = null, - ImmutableArray> properties = default, - ImmutableArray tags = default, - string? displayTextPrefix = null, - string? inlineDescription = null, - Glyph? glyph = null, - bool isComplexTextEdit = false) + using var _ = ArrayBuilder>.GetInstance(out var builder); + + if (!properties.IsDefault) + builder.AddRange(properties); + + if (insertionText != null) { - using var _ = ArrayBuilder>.GetInstance(out var builder); + builder.Add(new KeyValuePair(InsertionTextProperty, insertionText)); + } - if (!properties.IsDefault) - builder.AddRange(properties); + builder.Add(new KeyValuePair("ContextPosition", contextPosition.ToString())); + AddSupportedPlatforms(builder, supportedPlatforms); + symbolEncoder(symbols, builder); + + var firstSymbol = symbols[0]; + var item = CommonCompletionItem.Create( + displayText: displayText, + displayTextSuffix: displayTextSuffix, + displayTextPrefix: displayTextPrefix, + inlineDescription: inlineDescription, + rules: rules, + filterText: filterText ?? (displayText is ['@', ..] ? displayText : firstSymbol.Name), + sortText: sortText ?? firstSymbol.Name, + glyph: glyph ?? firstSymbol.GetGlyph(), + showsWarningIcon: supportedPlatforms != null, + properties: builder.ToImmutable(), + tags: tags, + isComplexTextEdit: isComplexTextEdit); + + return item; + } - if (insertionText != null) - { - builder.Add(new KeyValuePair(InsertionTextProperty, insertionText)); - } + private static void AddSymbolEncoding(IReadOnlyList symbols, ArrayBuilder> properties) + => properties.Add(new KeyValuePair("Symbols", EncodeSymbols(symbols))); - builder.Add(new KeyValuePair("ContextPosition", contextPosition.ToString())); - AddSupportedPlatforms(builder, supportedPlatforms); - symbolEncoder(symbols, builder); - - var firstSymbol = symbols[0]; - var item = CommonCompletionItem.Create( - displayText: displayText, - displayTextSuffix: displayTextSuffix, - displayTextPrefix: displayTextPrefix, - inlineDescription: inlineDescription, - rules: rules, - filterText: filterText ?? (displayText is ['@', ..] ? displayText : firstSymbol.Name), - sortText: sortText ?? firstSymbol.Name, - glyph: glyph ?? firstSymbol.GetGlyph(), - showsWarningIcon: supportedPlatforms != null, - properties: builder.ToImmutable(), - tags: tags, - isComplexTextEdit: isComplexTextEdit); - - return item; - } + private static void AddSymbolInfo(IReadOnlyList symbols, ArrayBuilder> properties) + { + var symbol = symbols[0]; + var isGeneric = symbol.GetArity() > 0; + properties.Add(new KeyValuePair("SymbolKind", ((int)symbol.Kind).ToString())); + properties.Add(new KeyValuePair("SymbolName", symbol.Name)); + + if (isGeneric) + properties.Add(new KeyValuePair("IsGeneric", isGeneric.ToString())); + } - private static void AddSymbolEncoding(IReadOnlyList symbols, ArrayBuilder> properties) - => properties.Add(new KeyValuePair("Symbols", EncodeSymbols(symbols))); + public static CompletionItem AddShouldProvideParenthesisCompletion(CompletionItem item) + => item.AddProperty("ShouldProvideParenthesisCompletion", true.ToString()); - private static void AddSymbolInfo(IReadOnlyList symbols, ArrayBuilder> properties) + public static bool GetShouldProvideParenthesisCompletion(CompletionItem item) + { + if (item.TryGetProperty("ShouldProvideParenthesisCompletion", out _)) { - var symbol = symbols[0]; - var isGeneric = symbol.GetArity() > 0; - properties.Add(new KeyValuePair("SymbolKind", ((int)symbol.Kind).ToString())); - properties.Add(new KeyValuePair("SymbolName", symbol.Name)); - - if (isGeneric) - properties.Add(new KeyValuePair("IsGeneric", isGeneric.ToString())); + return true; } - public static CompletionItem AddShouldProvideParenthesisCompletion(CompletionItem item) - => item.AddProperty("ShouldProvideParenthesisCompletion", true.ToString()); + return false; + } - public static bool GetShouldProvideParenthesisCompletion(CompletionItem item) + public static string EncodeSymbols(IReadOnlyList symbols) + { + if (symbols.Count > 1) { - if (item.TryGetProperty("ShouldProvideParenthesisCompletion", out _)) - { - return true; - } - - return false; + return string.Join("|", symbols.Select(EncodeSymbol)); } - - public static string EncodeSymbols(IReadOnlyList symbols) + else if (symbols.Count == 1) { - if (symbols.Count > 1) - { - return string.Join("|", symbols.Select(EncodeSymbol)); - } - else if (symbols.Count == 1) - { - return EncodeSymbol(symbols[0]); - } - else - { - return string.Empty; - } + return EncodeSymbol(symbols[0]); + } + else + { + return string.Empty; } + } - public static string EncodeSymbol(ISymbol symbol) - => SymbolKey.CreateString(symbol); + public static string EncodeSymbol(ISymbol symbol) + => SymbolKey.CreateString(symbol); - public static bool HasSymbols(CompletionItem item) - => item.TryGetProperty("Symbols", out var _); + public static bool HasSymbols(CompletionItem item) + => item.TryGetProperty("Symbols", out var _); - private static readonly char[] s_symbolSplitters = ['|']; + private static readonly char[] s_symbolSplitters = ['|']; - public static async Task> GetSymbolsAsync(CompletionItem item, Document document, CancellationToken cancellationToken) + public static async Task> GetSymbolsAsync(CompletionItem item, Document document, CancellationToken cancellationToken) + { + if (item.TryGetProperty("Symbols", out var symbolIds)) { - if (item.TryGetProperty("Symbols", out var symbolIds)) - { - var idList = symbolIds.Split(s_symbolSplitters, StringSplitOptions.RemoveEmptyEntries).ToList(); - using var _ = ArrayBuilder.GetInstance(out var symbols); + var idList = symbolIds.Split(s_symbolSplitters, StringSplitOptions.RemoveEmptyEntries).ToList(); + using var _ = ArrayBuilder.GetInstance(out var symbols); - var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - DecodeSymbols(idList, compilation, symbols); + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + DecodeSymbols(idList, compilation, symbols); - // merge in symbols from other linked documents - if (idList.Count > 0) + // merge in symbols from other linked documents + if (idList.Count > 0) + { + var linkedIds = document.GetLinkedDocumentIds(); + if (linkedIds.Length > 0) { - var linkedIds = document.GetLinkedDocumentIds(); - if (linkedIds.Length > 0) + foreach (var id in linkedIds) { - foreach (var id in linkedIds) - { - var linkedDoc = document.Project.Solution.GetRequiredDocument(id); - var linkedCompilation = await linkedDoc.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - DecodeSymbols(idList, linkedCompilation, symbols); - } + var linkedDoc = document.Project.Solution.GetRequiredDocument(id); + var linkedCompilation = await linkedDoc.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + DecodeSymbols(idList, linkedCompilation, symbols); } } - - return symbols.ToImmutable(); } - return []; + return symbols.ToImmutable(); } - private static void DecodeSymbols(List ids, Compilation compilation, ArrayBuilder symbols) + return []; + } + + private static void DecodeSymbols(List ids, Compilation compilation, ArrayBuilder symbols) + { + for (var i = 0; i < ids.Count;) { - for (var i = 0; i < ids.Count;) + var id = ids[i]; + var symbol = DecodeSymbol(id, compilation); + if (symbol != null) { - var id = ids[i]; - var symbol = DecodeSymbol(id, compilation); - if (symbol != null) - { - ids.RemoveAt(i); // consume id from the list - symbols.Add(symbol); // add symbol to the results - } - else - { - i++; - } + ids.RemoveAt(i); // consume id from the list + symbols.Add(symbol); // add symbol to the results + } + else + { + i++; } } + } - private static ISymbol? DecodeSymbol(string id, Compilation compilation) - => SymbolKey.ResolveString(id, compilation).GetAnySymbol(); + private static ISymbol? DecodeSymbol(string id, Compilation compilation) + => SymbolKey.ResolveString(id, compilation).GetAnySymbol(); - public static async Task GetDescriptionAsync( - CompletionItem item, Document document, SymbolDescriptionOptions options, CancellationToken cancellationToken) - { - var symbols = await GetSymbolsAsync(item, document, cancellationToken).ConfigureAwait(false); - return await GetDescriptionForSymbolsAsync(item, document, symbols, options, cancellationToken).ConfigureAwait(false); - } + public static async Task GetDescriptionAsync( + CompletionItem item, Document document, SymbolDescriptionOptions options, CancellationToken cancellationToken) + { + var symbols = await GetSymbolsAsync(item, document, cancellationToken).ConfigureAwait(false); + return await GetDescriptionForSymbolsAsync(item, document, symbols, options, cancellationToken).ConfigureAwait(false); + } - public static async Task GetDescriptionForSymbolsAsync( - CompletionItem item, Document document, ImmutableArray symbols, SymbolDescriptionOptions options, CancellationToken cancellationToken) - { - if (symbols.Length == 0) - return CompletionDescription.Empty; + public static async Task GetDescriptionForSymbolsAsync( + CompletionItem item, Document document, ImmutableArray symbols, SymbolDescriptionOptions options, CancellationToken cancellationToken) + { + if (symbols.Length == 0) + return CompletionDescription.Empty; - var position = GetDescriptionPosition(item); - if (position == -1) - position = item.Span.Start; + var position = GetDescriptionPosition(item); + if (position == -1) + position = item.Span.Start; - var supportedPlatforms = GetSupportedPlatforms(item, document.Project.Solution); - var contextDocument = FindAppropriateDocumentForDescriptionContext(document, supportedPlatforms); - var semanticModel = await contextDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var supportedPlatforms = GetSupportedPlatforms(item, document.Project.Solution); + var contextDocument = FindAppropriateDocumentForDescriptionContext(document, supportedPlatforms); + var semanticModel = await contextDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var services = document.Project.Solution.Services; - return await CommonCompletionUtilities.CreateDescriptionAsync(services, semanticModel, position, symbols, options, supportedPlatforms, cancellationToken).ConfigureAwait(false); - } + var services = document.Project.Solution.Services; + return await CommonCompletionUtilities.CreateDescriptionAsync(services, semanticModel, position, symbols, options, supportedPlatforms, cancellationToken).ConfigureAwait(false); + } - private static Document FindAppropriateDocumentForDescriptionContext(Document document, SupportedPlatformData? supportedPlatforms) + private static Document FindAppropriateDocumentForDescriptionContext(Document document, SupportedPlatformData? supportedPlatforms) + { + if (supportedPlatforms != null && supportedPlatforms.InvalidProjects.Contains(document.Id.ProjectId)) { - if (supportedPlatforms != null && supportedPlatforms.InvalidProjects.Contains(document.Id.ProjectId)) + var contextId = document.GetLinkedDocumentIds().FirstOrDefault(id => !supportedPlatforms.InvalidProjects.Contains(id.ProjectId)); + if (contextId != null) { - var contextId = document.GetLinkedDocumentIds().FirstOrDefault(id => !supportedPlatforms.InvalidProjects.Contains(id.ProjectId)); - if (contextId != null) - { - return document.Project.Solution.GetRequiredDocument(contextId); - } + return document.Project.Solution.GetRequiredDocument(contextId); } - - return document; } - private static void AddSupportedPlatforms(ArrayBuilder> properties, SupportedPlatformData? supportedPlatforms) - { - if (supportedPlatforms != null) - { - properties.Add(new KeyValuePair("InvalidProjects", string.Join(";", supportedPlatforms.InvalidProjects.Select(id => id.Id)))); - properties.Add(new KeyValuePair("CandidateProjects", string.Join(";", supportedPlatforms.CandidateProjects.Select(id => id.Id)))); - } - } + return document; + } - public static SupportedPlatformData? GetSupportedPlatforms(CompletionItem item, Solution solution) + private static void AddSupportedPlatforms(ArrayBuilder> properties, SupportedPlatformData? supportedPlatforms) + { + if (supportedPlatforms != null) { - if (item.TryGetProperty("InvalidProjects", out var invalidProjects) - && item.TryGetProperty("CandidateProjects", out var candidateProjects)) - { - return new SupportedPlatformData( - solution, - invalidProjects.Split(s_projectSeperators).Select(s => ProjectId.CreateFromSerialized(Guid.Parse(s))).ToList(), - candidateProjects.Split(s_projectSeperators).Select(s => ProjectId.CreateFromSerialized(Guid.Parse(s))).ToList()); - } - - return null; + properties.Add(new KeyValuePair("InvalidProjects", string.Join(";", supportedPlatforms.InvalidProjects.Select(id => id.Id)))); + properties.Add(new KeyValuePair("CandidateProjects", string.Join(";", supportedPlatforms.CandidateProjects.Select(id => id.Id)))); } + } - public static int GetContextPosition(CompletionItem item) + public static SupportedPlatformData? GetSupportedPlatforms(CompletionItem item, Solution solution) + { + if (item.TryGetProperty("InvalidProjects", out var invalidProjects) + && item.TryGetProperty("CandidateProjects", out var candidateProjects)) { - if (item.TryGetProperty("ContextPosition", out var text) && - int.TryParse(text, out var number)) - { - return number; - } - else - { - return -1; - } + return new SupportedPlatformData( + solution, + invalidProjects.Split(s_projectSeperators).Select(s => ProjectId.CreateFromSerialized(Guid.Parse(s))).ToList(), + candidateProjects.Split(s_projectSeperators).Select(s => ProjectId.CreateFromSerialized(Guid.Parse(s))).ToList()); } - public static int GetDescriptionPosition(CompletionItem item) - => GetContextPosition(item); - - public static string GetInsertionText(CompletionItem item) - => item.GetProperty(InsertionTextProperty); - - public static bool TryGetInsertionText(CompletionItem item, [NotNullWhen(true)] out string? insertionText) - => item.TryGetProperty(InsertionTextProperty, out insertionText); - - // COMPAT OVERLOAD: This is used by IntelliCode. - public static CompletionItem CreateWithSymbolId( - string displayText, - IReadOnlyList symbols, - CompletionItemRules rules, - int contextPosition, - string? sortText = null, - string? insertionText = null, - string? filterText = null, - SupportedPlatformData? supportedPlatforms = null, - ImmutableDictionary? properties = null, - ImmutableArray tags = default, - bool isComplexTextEdit = false) - { - return CreateWithSymbolId( - displayText, - displayTextSuffix: null, - symbols, - rules, - contextPosition, - sortText, - insertionText, - filterText, - displayTextPrefix: null, - inlineDescription: null, - glyph: null, - supportedPlatforms, - properties.AsImmutableOrNull(), - tags, - isComplexTextEdit); - } + return null; + } - public static CompletionItem CreateWithSymbolId( - string displayText, - string? displayTextSuffix, - IReadOnlyList symbols, - CompletionItemRules rules, - int contextPosition, - string? sortText = null, - string? insertionText = null, - string? filterText = null, - string? displayTextPrefix = null, - string? inlineDescription = null, - Glyph? glyph = null, - SupportedPlatformData? supportedPlatforms = null, - ImmutableArray> properties = default, - ImmutableArray tags = default, - bool isComplexTextEdit = false) + public static int GetContextPosition(CompletionItem item) + { + if (item.TryGetProperty("ContextPosition", out var text) && + int.TryParse(text, out var number)) { - return CreateWorker( - displayText, displayTextSuffix, symbols, rules, contextPosition, - s_addSymbolEncoding, sortText, insertionText, - filterText, supportedPlatforms, properties, tags, displayTextPrefix, - inlineDescription, glyph, isComplexTextEdit); + return number; } - - public static CompletionItem CreateWithNameAndKind( - string displayText, - string displayTextSuffix, - IReadOnlyList symbols, - CompletionItemRules rules, - int contextPosition, - string? sortText = null, - string? insertionText = null, - string? filterText = null, - string? displayTextPrefix = null, - string? inlineDescription = null, - Glyph? glyph = null, - SupportedPlatformData? supportedPlatforms = null, - ImmutableArray> properties = default, - ImmutableArray tags = default, - bool isComplexTextEdit = false) + else { - return CreateWorker( - displayText, displayTextSuffix, symbols, rules, contextPosition, - s_addSymbolInfo, sortText, insertionText, - filterText, supportedPlatforms, properties, tags, - displayTextPrefix, inlineDescription, glyph, isComplexTextEdit); + return -1; } + } - internal static string? GetSymbolName(CompletionItem item) - => item.TryGetProperty("SymbolName", out var name) ? name : null; + public static int GetDescriptionPosition(CompletionItem item) + => GetContextPosition(item); + + public static string GetInsertionText(CompletionItem item) + => item.GetProperty(InsertionTextProperty); + + public static bool TryGetInsertionText(CompletionItem item, [NotNullWhen(true)] out string? insertionText) + => item.TryGetProperty(InsertionTextProperty, out insertionText); + + // COMPAT OVERLOAD: This is used by IntelliCode. + public static CompletionItem CreateWithSymbolId( + string displayText, + IReadOnlyList symbols, + CompletionItemRules rules, + int contextPosition, + string? sortText = null, + string? insertionText = null, + string? filterText = null, + SupportedPlatformData? supportedPlatforms = null, + ImmutableDictionary? properties = null, + ImmutableArray tags = default, + bool isComplexTextEdit = false) + { + return CreateWithSymbolId( + displayText, + displayTextSuffix: null, + symbols, + rules, + contextPosition, + sortText, + insertionText, + filterText, + displayTextPrefix: null, + inlineDescription: null, + glyph: null, + supportedPlatforms, + properties.AsImmutableOrNull(), + tags, + isComplexTextEdit); + } - internal static SymbolKind? GetKind(CompletionItem item) - => item.TryGetProperty("SymbolKind", out var kind) ? (SymbolKind?)int.Parse(kind) : null; + public static CompletionItem CreateWithSymbolId( + string displayText, + string? displayTextSuffix, + IReadOnlyList symbols, + CompletionItemRules rules, + int contextPosition, + string? sortText = null, + string? insertionText = null, + string? filterText = null, + string? displayTextPrefix = null, + string? inlineDescription = null, + Glyph? glyph = null, + SupportedPlatformData? supportedPlatforms = null, + ImmutableArray> properties = default, + ImmutableArray tags = default, + bool isComplexTextEdit = false) + { + return CreateWorker( + displayText, displayTextSuffix, symbols, rules, contextPosition, + s_addSymbolEncoding, sortText, insertionText, + filterText, supportedPlatforms, properties, tags, displayTextPrefix, + inlineDescription, glyph, isComplexTextEdit); + } - internal static bool GetSymbolIsGeneric(CompletionItem item) - => item.TryGetProperty("IsGeneric", out var v) && bool.TryParse(v, out var isGeneric) && isGeneric; + public static CompletionItem CreateWithNameAndKind( + string displayText, + string displayTextSuffix, + IReadOnlyList symbols, + CompletionItemRules rules, + int contextPosition, + string? sortText = null, + string? insertionText = null, + string? filterText = null, + string? displayTextPrefix = null, + string? inlineDescription = null, + Glyph? glyph = null, + SupportedPlatformData? supportedPlatforms = null, + ImmutableArray> properties = default, + ImmutableArray tags = default, + bool isComplexTextEdit = false) + { + return CreateWorker( + displayText, displayTextSuffix, symbols, rules, contextPosition, + s_addSymbolInfo, sortText, insertionText, + filterText, supportedPlatforms, properties, tags, + displayTextPrefix, inlineDescription, glyph, isComplexTextEdit); + } - public static async Task GetDescriptionAsync( - CompletionItem item, IReadOnlyList symbols, Document document, SemanticModel semanticModel, SymbolDescriptionOptions options, CancellationToken cancellationToken) - { - var position = GetDescriptionPosition(item); - var supportedPlatforms = GetSupportedPlatforms(item, document.Project.Solution); + internal static string? GetSymbolName(CompletionItem item) + => item.TryGetProperty("SymbolName", out var name) ? name : null; - if (symbols.Count != 0) - { - return await CommonCompletionUtilities.CreateDescriptionAsync(document.Project.Solution.Services, semanticModel, position, symbols, options, supportedPlatforms, cancellationToken).ConfigureAwait(false); - } - else - { - return CompletionDescription.Empty; - } + internal static SymbolKind? GetKind(CompletionItem item) + => item.TryGetProperty("SymbolKind", out var kind) ? (SymbolKind?)int.Parse(kind) : null; + + internal static bool GetSymbolIsGeneric(CompletionItem item) + => item.TryGetProperty("IsGeneric", out var v) && bool.TryParse(v, out var isGeneric) && isGeneric; + + public static async Task GetDescriptionAsync( + CompletionItem item, IReadOnlyList symbols, Document document, SemanticModel semanticModel, SymbolDescriptionOptions options, CancellationToken cancellationToken) + { + var position = GetDescriptionPosition(item); + var supportedPlatforms = GetSupportedPlatforms(item, document.Project.Solution); + + if (symbols.Count != 0) + { + return await CommonCompletionUtilities.CreateDescriptionAsync(document.Project.Solution.Services, semanticModel, position, symbols, options, supportedPlatforms, cancellationToken).ConfigureAwait(false); + } + else + { + return CompletionDescription.Empty; } } } diff --git a/src/Features/Core/Portable/Completion/Providers/SymbolMatchPriority.cs b/src/Features/Core/Portable/Completion/Providers/SymbolMatchPriority.cs index 33f5d5e17031d..206b744d1a9fe 100644 --- a/src/Features/Core/Portable/Completion/Providers/SymbolMatchPriority.cs +++ b/src/Features/Core/Portable/Completion/Providers/SymbolMatchPriority.cs @@ -4,15 +4,14 @@ #pragma warning disable CA1802 // Use literals where appropriate - if any of these are used by an assembly that has IVT it would be breaking to change to constant -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal static class SymbolMatchPriority { - internal static class SymbolMatchPriority - { - internal static readonly int Keyword = 100; - internal static readonly int PreferType = 200; - internal static readonly int PreferNamedArgument = 300; - internal static readonly int PreferEventOrMethod = 400; - internal static readonly int PreferFieldOrProperty = 500; - internal static readonly int PreferLocalOrParameterOrRangeVariable = 600; - } + internal static readonly int Keyword = 100; + internal static readonly int PreferType = 200; + internal static readonly int PreferNamedArgument = 300; + internal static readonly int PreferEventOrMethod = 400; + internal static readonly int PreferFieldOrProperty = 500; + internal static readonly int PreferLocalOrParameterOrRangeVariable = 600; } diff --git a/src/Features/Core/Portable/Completion/Providers/UnionCompletionItemComparer.cs b/src/Features/Core/Portable/Completion/Providers/UnionCompletionItemComparer.cs index d33d9bd75cad6..e2941d0c1fc77 100644 --- a/src/Features/Core/Portable/Completion/Providers/UnionCompletionItemComparer.cs +++ b/src/Features/Core/Portable/Completion/Providers/UnionCompletionItemComparer.cs @@ -6,21 +6,20 @@ using System.Linq; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal sealed class UnionCompletionItemComparer : IEqualityComparer { - internal sealed class UnionCompletionItemComparer : IEqualityComparer - { - public static readonly UnionCompletionItemComparer Instance = new(); + public static readonly UnionCompletionItemComparer Instance = new(); - private UnionCompletionItemComparer() - { - } + private UnionCompletionItemComparer() + { + } - public bool Equals(CompletionItem? x, CompletionItem? y) - => ReferenceEquals(x, y) || - x is not null && y is not null && x.DisplayText == y.DisplayText && x.Tags.SequenceEqual(y.Tags); + public bool Equals(CompletionItem? x, CompletionItem? y) + => ReferenceEquals(x, y) || + x is not null && y is not null && x.DisplayText == y.DisplayText && x.Tags.SequenceEqual(y.Tags); - public int GetHashCode(CompletionItem obj) - => Hash.Combine(obj.DisplayText.GetHashCode(), obj.Tags.Length); - } + public int GetHashCode(CompletionItem obj) + => Hash.Combine(obj.DisplayText.GetHashCode(), obj.Tags.Length); } diff --git a/src/Features/Core/Portable/Completion/Providers/XmlDocCommentCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/XmlDocCommentCompletionItem.cs index 7b9c5326a026e..85c33ff03a87b 100644 --- a/src/Features/Core/Portable/Completion/Providers/XmlDocCommentCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/XmlDocCommentCompletionItem.cs @@ -5,37 +5,36 @@ using System.Collections.Generic; using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.Completion.Providers +namespace Microsoft.CodeAnalysis.Completion.Providers; + +internal static class XmlDocCommentCompletionItem { - internal static class XmlDocCommentCompletionItem - { - private const string BeforeCaretText = nameof(BeforeCaretText); - private const string AfterCaretText = nameof(AfterCaretText); + private const string BeforeCaretText = nameof(BeforeCaretText); + private const string AfterCaretText = nameof(AfterCaretText); - public static CompletionItem Create(string displayText, string beforeCaretText, string afterCaretText, CompletionItemRules rules) - { - var props = ImmutableArray.Create( - new KeyValuePair(BeforeCaretText, beforeCaretText), - new KeyValuePair(AfterCaretText, afterCaretText)); + public static CompletionItem Create(string displayText, string beforeCaretText, string afterCaretText, CompletionItemRules rules) + { + var props = ImmutableArray.Create( + new KeyValuePair(BeforeCaretText, beforeCaretText), + new KeyValuePair(AfterCaretText, afterCaretText)); - // Set isComplexTextEdit to be always true for simplicity, even - // though we don't always need to make change outside the default - // completion list Span. - // See AbstractDocCommentCompletionProvider.GetChangeAsync for how - // the actual Span is calculated. - return CommonCompletionItem.Create( - displayText: displayText, - displayTextSuffix: "", - glyph: Glyph.Keyword, - properties: props, - rules: rules, - isComplexTextEdit: true); - } + // Set isComplexTextEdit to be always true for simplicity, even + // though we don't always need to make change outside the default + // completion list Span. + // See AbstractDocCommentCompletionProvider.GetChangeAsync for how + // the actual Span is calculated. + return CommonCompletionItem.Create( + displayText: displayText, + displayTextSuffix: "", + glyph: Glyph.Keyword, + properties: props, + rules: rules, + isComplexTextEdit: true); + } - public static string GetBeforeCaretText(CompletionItem item) - => item.GetProperty(BeforeCaretText); + public static string GetBeforeCaretText(CompletionItem item) + => item.GetProperty(BeforeCaretText); - public static string? GetAfterCaretText(CompletionItem item) - => item.GetProperty(AfterCaretText); - } + public static string? GetAfterCaretText(CompletionItem item) + => item.GetProperty(AfterCaretText); } diff --git a/src/Features/Core/Portable/Completion/SharedSyntaxContextsWithSpeculativeModel.cs b/src/Features/Core/Portable/Completion/SharedSyntaxContextsWithSpeculativeModel.cs index 56d5017ceb5e8..ac417ecd79574 100644 --- a/src/Features/Core/Portable/Completion/SharedSyntaxContextsWithSpeculativeModel.cs +++ b/src/Features/Core/Portable/Completion/SharedSyntaxContextsWithSpeculativeModel.cs @@ -10,42 +10,41 @@ using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal sealed class SharedSyntaxContextsWithSpeculativeModel { - internal sealed class SharedSyntaxContextsWithSpeculativeModel - { - private readonly Document _document; - private readonly int _position; + private readonly Document _document; + private readonly int _position; - private readonly ConcurrentDictionary> _cache; - private readonly Lazy> _lazyRelatedDocumentIds; + private readonly ConcurrentDictionary> _cache; + private readonly Lazy> _lazyRelatedDocumentIds; + + public SharedSyntaxContextsWithSpeculativeModel(Document document, int position) + { + _document = document; + _position = position; + _cache = []; + _lazyRelatedDocumentIds = new(_document.GetLinkedDocumentIds, isThreadSafe: true); + } - public SharedSyntaxContextsWithSpeculativeModel(Document document, int position) + public Task GetSyntaxContextAsync(Document document, CancellationToken cancellationToken) + { + if (!_cache.TryGetValue(document, out var lazyContext)) { - _document = document; - _position = position; - _cache = []; - _lazyRelatedDocumentIds = new(_document.GetLinkedDocumentIds, isThreadSafe: true); + if (_document.Id != document.Id && !_lazyRelatedDocumentIds.Value.Contains(document.Id)) + throw new ArgumentException("Don't support getting SyntaxContext for document unrelated to the original document"); + + lazyContext = GetLazySyntaxContextWithSpeculativeModel(document, this); } - public Task GetSyntaxContextAsync(Document document, CancellationToken cancellationToken) + return lazyContext.GetValueAsync(cancellationToken); + + // Extract a local function to avoid creating a closure for code path of cache hit. + static AsyncLazy GetLazySyntaxContextWithSpeculativeModel(Document document, SharedSyntaxContextsWithSpeculativeModel self) { - if (!_cache.TryGetValue(document, out var lazyContext)) - { - if (_document.Id != document.Id && !_lazyRelatedDocumentIds.Value.Contains(document.Id)) - throw new ArgumentException("Don't support getting SyntaxContext for document unrelated to the original document"); - - lazyContext = GetLazySyntaxContextWithSpeculativeModel(document, this); - } - - return lazyContext.GetValueAsync(cancellationToken); - - // Extract a local function to avoid creating a closure for code path of cache hit. - static AsyncLazy GetLazySyntaxContextWithSpeculativeModel(Document document, SharedSyntaxContextsWithSpeculativeModel self) - { - return self._cache.GetOrAdd(document, d => AsyncLazy.Create(cancellationToken - => Utilities.CreateSyntaxContextWithExistingSpeculativeModelAsync(d, self._position, cancellationToken))); - } + return self._cache.GetOrAdd(document, d => AsyncLazy.Create(cancellationToken + => Utilities.CreateSyntaxContextWithExistingSpeculativeModelAsync(d, self._position, cancellationToken))); } } } diff --git a/src/Features/Core/Portable/Completion/SnippetsRule.cs b/src/Features/Core/Portable/Completion/SnippetsRule.cs index f9a6eb423c15a..1094216188413 100644 --- a/src/Features/Core/Portable/Completion/SnippetsRule.cs +++ b/src/Features/Core/Portable/Completion/SnippetsRule.cs @@ -2,28 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +public enum SnippetsRule { - public enum SnippetsRule - { - /// - /// Snippet triggering follows the default rules of the language. - /// - Default = 0, + /// + /// Snippet triggering follows the default rules of the language. + /// + Default = 0, - /// - /// Snippets are never included in the completion list - /// - NeverInclude = 1, + /// + /// Snippets are never included in the completion list + /// + NeverInclude = 1, - /// - /// Snippets are always included in the completion list. - /// - AlwaysInclude = 2, + /// + /// Snippets are always included in the completion list. + /// + AlwaysInclude = 2, - /// - /// Snippets are included if the user types: id?<tab> - /// - IncludeAfterTypingIdentifierQuestionTab = 3, - } + /// + /// Snippets are included if the user types: id?<tab> + /// + IncludeAfterTypingIdentifierQuestionTab = 3, } diff --git a/src/Features/Core/Portable/Completion/Utilities.cs b/src/Features/Core/Portable/Completion/Utilities.cs index 2847e55d9f8aa..ca694d75aac45 100644 --- a/src/Features/Core/Portable/Completion/Utilities.cs +++ b/src/Features/Core/Portable/Completion/Utilities.cs @@ -11,51 +11,50 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Completion +namespace Microsoft.CodeAnalysis.Completion; + +internal static class Utilities { - internal static class Utilities + public static TextChange Collapse(SourceText newText, ImmutableArray changes) { - public static TextChange Collapse(SourceText newText, ImmutableArray changes) + if (changes.Length == 0) + { + return new TextChange(new TextSpan(0, 0), ""); + } + else if (changes.Length == 1) { - if (changes.Length == 0) - { - return new TextChange(new TextSpan(0, 0), ""); - } - else if (changes.Length == 1) - { - return changes[0]; - } - - // The span we want to replace goes from the start of the first span to the end of - // the last span. - var totalOldSpan = TextSpan.FromBounds(changes.First().Span.Start, changes.Last().Span.End); - - // We figure out the text we're replacing with by actually just figuring out the - // new span in the newText and grabbing the text out of that. The newSpan will - // start from the same position as the oldSpan, but it's length will be the old - // span's length + all the deltas we accumulate through each text change. i.e. - // if the first change adds 2 characters and the second change adds 4, then - // the newSpan will be 2+4=6 characters longer than the old span. - var sumOfDeltas = changes.Sum(c => c.NewText!.Length - c.Span.Length); - var totalNewSpan = new TextSpan(totalOldSpan.Start, totalOldSpan.Length + sumOfDeltas); - - return new TextChange(totalOldSpan, newText.ToString(totalNewSpan)); + return changes[0]; } - // This is a temporarily method to support preference of IntelliCode items comparing to non-IntelliCode items. - // We expect that Editor will introduce this support and we will get rid of relying on the "★" then. - public static bool IsPreferredItem(this CompletionItem completionItem) - => completionItem.DisplayText.StartsWith(UnicodeStarAndSpace); + // The span we want to replace goes from the start of the first span to the end of + // the last span. + var totalOldSpan = TextSpan.FromBounds(changes.First().Span.Start, changes.Last().Span.End); - public const string UnicodeStarAndSpace = "\u2605 "; + // We figure out the text we're replacing with by actually just figuring out the + // new span in the newText and grabbing the text out of that. The newSpan will + // start from the same position as the oldSpan, but it's length will be the old + // span's length + all the deltas we accumulate through each text change. i.e. + // if the first change adds 2 characters and the second change adds 4, then + // the newSpan will be 2+4=6 characters longer than the old span. + var sumOfDeltas = changes.Sum(c => c.NewText!.Length - c.Span.Length); + var totalNewSpan = new TextSpan(totalOldSpan.Start, totalOldSpan.Length + sumOfDeltas); - public static async Task CreateSyntaxContextWithExistingSpeculativeModelAsync(Document document, int position, CancellationToken cancellationToken) - { - Contract.ThrowIfFalse(document.SupportsSemanticModel, "Should only be called from C#/VB providers."); - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); + return new TextChange(totalOldSpan, newText.ToString(totalNewSpan)); + } - var service = document.GetRequiredLanguageService(); - return service.CreateContext(document, semanticModel, position, cancellationToken); - } + // This is a temporarily method to support preference of IntelliCode items comparing to non-IntelliCode items. + // We expect that Editor will introduce this support and we will get rid of relying on the "★" then. + public static bool IsPreferredItem(this CompletionItem completionItem) + => completionItem.DisplayText.StartsWith(UnicodeStarAndSpace); + + public const string UnicodeStarAndSpace = "\u2605 "; + + public static async Task CreateSyntaxContextWithExistingSpeculativeModelAsync(Document document, int position, CancellationToken cancellationToken) + { + Contract.ThrowIfFalse(document.SupportsSemanticModel, "Should only be called from C#/VB providers."); + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); + + var service = document.GetRequiredLanguageService(); + return service.CreateContext(document, semanticModel, position, cancellationToken); } } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ActiveStatementFlags.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ActiveStatementFlags.cs index 6ef34b39e2891..0cdc818b6de73 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ActiveStatementFlags.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ActiveStatementFlags.cs @@ -4,59 +4,58 @@ using System; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Flags regarding active statements information. +/// +[Flags] +internal enum ActiveStatementFlags { + None = 0, + + /// + /// At least one of the threads whom this active statement belongs to is in a leaf frame. + /// + LeafFrame = 1, + + /// + /// The statement is partially executed. + /// + /// + /// An active statement is partially executed if the thread is stopped in between two sequence points. + /// This may happen when the users steps through the code in disassembly window (stepping over machine instructions), + /// when the compiler emits a call to Debugger.Break (VB Stop statement), etc. + /// + /// Partially executed active statement can't be edited. + /// + PartiallyExecuted = 2, + + /// + /// The statement IL is not in user code. + /// + NonUserCode = 4, + + /// + /// Indicates that the active statement instruction belongs to the latest version of the containing method. + /// If not set, the containing method was updated but the active statement was not remapped yet because the thread + /// has not returned to that instruction yet and was not remapped to the new version. + /// + /// + /// When the debugger asks the CLR for the active statement information it compares ICorDebugFunction.GetVersionNumber() + /// and ICorDebugFunction.GetCurrentVersionNumber() to determine the value of this flag. + /// + MethodUpToDate = 8, + + /// + /// At least one of the threads whom this active statement belongs to is in a non-leaf frame. + /// + NonLeafFrame = 16, + /// - /// Flags regarding active statements information. + /// When applying updates while the code is executing, we will not attempt any remap for methods which are on the + /// executing stack. This is done so we can avoid blocking an edit due an executing active statement. + /// Language services needs to acknowledge such active statements when emitting further remap information. /// - [Flags] - internal enum ActiveStatementFlags - { - None = 0, - - /// - /// At least one of the threads whom this active statement belongs to is in a leaf frame. - /// - LeafFrame = 1, - - /// - /// The statement is partially executed. - /// - /// - /// An active statement is partially executed if the thread is stopped in between two sequence points. - /// This may happen when the users steps through the code in disassembly window (stepping over machine instructions), - /// when the compiler emits a call to Debugger.Break (VB Stop statement), etc. - /// - /// Partially executed active statement can't be edited. - /// - PartiallyExecuted = 2, - - /// - /// The statement IL is not in user code. - /// - NonUserCode = 4, - - /// - /// Indicates that the active statement instruction belongs to the latest version of the containing method. - /// If not set, the containing method was updated but the active statement was not remapped yet because the thread - /// has not returned to that instruction yet and was not remapped to the new version. - /// - /// - /// When the debugger asks the CLR for the active statement information it compares ICorDebugFunction.GetVersionNumber() - /// and ICorDebugFunction.GetCurrentVersionNumber() to determine the value of this flag. - /// - MethodUpToDate = 8, - - /// - /// At least one of the threads whom this active statement belongs to is in a non-leaf frame. - /// - NonLeafFrame = 16, - - /// - /// When applying updates while the code is executing, we will not attempt any remap for methods which are on the - /// executing stack. This is done so we can avoid blocking an edit due an executing active statement. - /// Language services needs to acknowledge such active statements when emitting further remap information. - /// - Stale = 32 - } + Stale = 32 } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/DebugSessionFlags.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/DebugSessionFlags.cs index bee5d83cbdad7..968c138ff32b6 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/DebugSessionFlags.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/DebugSessionFlags.cs @@ -3,19 +3,18 @@ // See the LICENSE file in the project root for more information. using System; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +[Flags] +internal enum DebugSessionFlags { - [Flags] - internal enum DebugSessionFlags - { - /// - /// No flags. - /// - None = 0, + /// + /// No flags. + /// + None = 0, - /// - /// Edit and Continue has been disabled by the client. - /// - EditAndContinueDisabled = 0x1, - } + /// + /// Edit and Continue has been disabled by the client. + /// + EditAndContinueDisabled = 0x1, } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/HotReloadResult.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/HotReloadResult.cs index 74dad2948435a..e90540cba7ee0 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/HotReloadResult.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/HotReloadResult.cs @@ -2,38 +2,37 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Result for a hot reload apply operation. +/// +internal enum HotReloadResult { /// - /// Result for a hot reload apply operation. + /// Successfully applied the changes. /// - internal enum HotReloadResult - { - /// - /// Successfully applied the changes. - /// - Applied = 0, + Applied = 0, - /// - /// No changes were found. - /// - NoChanges = 1, + /// + /// No changes were found. + /// + NoChanges = 1, - /// - /// Rude edits were found. - /// Changes can be applied by restarting the session. - /// - RestartRequired = 2, + /// + /// Rude edits were found. + /// Changes can be applied by restarting the session. + /// + RestartRequired = 2, - /// - /// Edits with a compiler error were found. - /// This assumes that the agents do not support restart and any rude edits were treated as errors. - /// - ErrorEdits = 3, + /// + /// Edits with a compiler error were found. + /// This assumes that the agents do not support restart and any rude edits were treated as errors. + /// + ErrorEdits = 3, - /// - /// An internal error was found while applying code updates. This will generally be propagated through an exception. - /// - ApplyUpdateFailure = 4 - } + /// + /// An internal error was found while applying code updates. This will generally be propagated through an exception. + /// + ApplyUpdateFailure = 4 } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/IManagedHotReloadService.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/IManagedHotReloadService.cs index 8c919d7aa5383..967ada3d96643 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/IManagedHotReloadService.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/IManagedHotReloadService.cs @@ -7,58 +7,57 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Service for providing helper functionality to a language service regarding hot reload and Edit and Continue operations. +/// This is currently exported through MEF. +/// +internal interface IManagedHotReloadService { /// - /// Service for providing helper functionality to a language service regarding hot reload and Edit and Continue operations. - /// This is currently exported through MEF. + /// Retrieves a list of active statements for the debugging session. + /// Shall only be called while the debugger is stopped (break mode). + /// Returns empty array if no debugger is present. /// - internal interface IManagedHotReloadService - { - /// - /// Retrieves a list of active statements for the debugging session. - /// Shall only be called while the debugger is stopped (break mode). - /// Returns empty array if no debugger is present. - /// - /// Cancellation token. - /// - /// Returns all the active statements in the session. Each has an unique . - /// For example, if an instruction is active in two different threads, only one active statement will be reported for it. - /// - ValueTask> GetActiveStatementsAsync(CancellationToken cancellation); + /// Cancellation token. + /// + /// Returns all the active statements in the session. Each has an unique . + /// For example, if an instruction is active in two different threads, only one active statement will be reported for it. + /// + ValueTask> GetActiveStatementsAsync(CancellationToken cancellation); - /// - /// Check for Edit and Continue availability on all instances with specified . - /// If no debugger is present, this will check if there are any agents available for hot reload or whether - /// the user has disabled Edit and Continue or hot reload. - /// - /// Target module version identifier. This is only used when under a debugging session. - /// Cancellation token. - /// - /// Returns first status that's not , if any. - /// Otherwise, if there is at least one instance of the module loaded in a debugging session or there are active hot reload agents, returns . - /// Otherwise, returns . - /// - ValueTask GetAvailabilityAsync(Guid module, CancellationToken cancellation); + /// + /// Check for Edit and Continue availability on all instances with specified . + /// If no debugger is present, this will check if there are any agents available for hot reload or whether + /// the user has disabled Edit and Continue or hot reload. + /// + /// Target module version identifier. This is only used when under a debugging session. + /// Cancellation token. + /// + /// Returns first status that's not , if any. + /// Otherwise, if there is at least one instance of the module loaded in a debugging session or there are active hot reload agents, returns . + /// Otherwise, returns . + /// + ValueTask GetAvailabilityAsync(Guid module, CancellationToken cancellation); - /// - /// Notifies the debugger that a document has changed, which may affect the given module when that change is applied. - /// Calls ISymUnmanagedEncUpdate.InitializeForEnc on SymReader for the given module. - /// No-op if no debugger is present. - /// - /// Module version identifier. - /// Cancellation token. - ValueTask PrepareModuleForUpdateAsync(Guid module, CancellationToken cancellation); + /// + /// Notifies the debugger that a document has changed, which may affect the given module when that change is applied. + /// Calls ISymUnmanagedEncUpdate.InitializeForEnc on SymReader for the given module. + /// No-op if no debugger is present. + /// + /// Module version identifier. + /// Cancellation token. + ValueTask PrepareModuleForUpdateAsync(Guid module, CancellationToken cancellation); - /// - /// Get capabilities string for the set of hot reload edits supported by the runtime. - /// - /// Cancellation token. - /// - /// Returns an array of identifiers. If different agents have different capabilities, it's up to the manager - /// to merge them and present unified set of capabilities to the language service. - /// The merging policy is entirely dependent on how the manager applies changes to multiple runtimes. - /// - ValueTask> GetCapabilitiesAsync(CancellationToken cancellation); - } + /// + /// Get capabilities string for the set of hot reload edits supported by the runtime. + /// + /// Cancellation token. + /// + /// Returns an array of identifiers. If different agents have different capabilities, it's up to the manager + /// to merge them and present unified set of capabilities to the language service. + /// The merging policy is entirely dependent on how the manager applies changes to multiple runtimes. + /// + ValueTask> GetCapabilitiesAsync(CancellationToken cancellation); } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedActiveStatementDebugInfo.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedActiveStatementDebugInfo.cs index 0ef8a4815c757..3f666802d6221 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedActiveStatementDebugInfo.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedActiveStatementDebugInfo.cs @@ -3,51 +3,50 @@ // See the LICENSE file in the project root for more information. using System.Runtime.Serialization; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Active statement debug information retrieved from the runtime and the PDB. +/// +/// +/// Creates a ManagedActiveStatementDebugInfo. +/// +/// Instruction of the active statement that is being executed. +/// Document name as found in the PDB, if the active statement location was determined. +/// Location of the closest non-hidden sequence point from the active statement. +/// Active statement flags shared across all threads that own the active statement. +[DataContract] +internal readonly struct ManagedActiveStatementDebugInfo( + ManagedInstructionId activeInstruction, + string? documentName, + SourceSpan sourceSpan, + ActiveStatementFlags flags) { + /// - /// Active statement debug information retrieved from the runtime and the PDB. + /// The instruction of the active statement that is being executed. /// - /// - /// Creates a ManagedActiveStatementDebugInfo. - /// - /// Instruction of the active statement that is being executed. - /// Document name as found in the PDB, if the active statement location was determined. - /// Location of the closest non-hidden sequence point from the active statement. - /// Active statement flags shared across all threads that own the active statement. - [DataContract] - internal readonly struct ManagedActiveStatementDebugInfo( - ManagedInstructionId activeInstruction, - string? documentName, - SourceSpan sourceSpan, - ActiveStatementFlags flags) - { - - /// - /// The instruction of the active statement that is being executed. - /// - [DataMember(Name = "activeInstruction")] - public ManagedInstructionId ActiveInstruction { get; } = activeInstruction; + [DataMember(Name = "activeInstruction")] + public ManagedInstructionId ActiveInstruction { get; } = activeInstruction; - /// - /// Document name as found in the PDB, or null if the debugger can't determine the location of the active statement. - /// - [DataMember(Name = "documentName")] - public string? DocumentName { get; } = documentName; + /// + /// Document name as found in the PDB, or null if the debugger can't determine the location of the active statement. + /// + [DataMember(Name = "documentName")] + public string? DocumentName { get; } = documentName; - /// - /// Location of the closest non-hidden sequence point retrieved from the PDB, - /// or default() if the debugger can't determine the location of the active statement. - /// - [DataMember(Name = "sourceSpan")] - public SourceSpan SourceSpan { get; } = sourceSpan; + /// + /// Location of the closest non-hidden sequence point retrieved from the PDB, + /// or default() if the debugger can't determine the location of the active statement. + /// + [DataMember(Name = "sourceSpan")] + public SourceSpan SourceSpan { get; } = sourceSpan; - /// - /// Aggregated across any threads that own the active instruction. - /// - [DataMember(Name = "flags")] - public ActiveStatementFlags Flags { get; } = flags; + /// + /// Aggregated across any threads that own the active instruction. + /// + [DataMember(Name = "flags")] + public ActiveStatementFlags Flags { get; } = flags; - public bool HasSourceLocation => DocumentName != null; - } + public bool HasSourceLocation => DocumentName != null; } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedActiveStatementUpdate.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedActiveStatementUpdate.cs index 4758a673bbba9..7e49d221cbb55 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedActiveStatementUpdate.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedActiveStatementUpdate.cs @@ -3,42 +3,41 @@ // See the LICENSE file in the project root for more information. using System.Runtime.Serialization; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Active statement affected by a managed update. +/// This is used when remapping the instruction pointer to the appropriate location. +/// +/// +/// Creates a ManagedActiveStatementUpdate. +/// +/// Method information before the change was made. +/// Old IL offset of the active statement. +/// Updated text span for the active statement. +[DataContract] +internal readonly struct ManagedActiveStatementUpdate( + ManagedModuleMethodId method, + int ilOffset, + SourceSpan newSpan) { + /// - /// Active statement affected by a managed update. - /// This is used when remapping the instruction pointer to the appropriate location. + /// Method ID. It has the token for the method that contains the active statement + /// and the version when the change was made. /// - /// - /// Creates a ManagedActiveStatementUpdate. - /// - /// Method information before the change was made. - /// Old IL offset of the active statement. - /// Updated text span for the active statement. - [DataContract] - internal readonly struct ManagedActiveStatementUpdate( - ManagedModuleMethodId method, - int ilOffset, - SourceSpan newSpan) - { + [DataMember(Name = "method")] + public ManagedModuleMethodId Method { get; } = method; - /// - /// Method ID. It has the token for the method that contains the active statement - /// and the version when the change was made. - /// - [DataMember(Name = "method")] - public ManagedModuleMethodId Method { get; } = method; - - /// - /// Old IL offset for the active statement. - /// - [DataMember(Name = "ilOffset")] - public int ILOffset { get; } = ilOffset; + /// + /// Old IL offset for the active statement. + /// + [DataMember(Name = "ilOffset")] + public int ILOffset { get; } = ilOffset; - /// - /// Updated text span for the active statement after the edit was made. - /// - [DataMember(Name = "newSpan")] - public SourceSpan NewSpan { get; } = newSpan; - } + /// + /// Updated text span for the active statement after the edit was made. + /// + [DataMember(Name = "newSpan")] + public SourceSpan NewSpan { get; } = newSpan; } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedEditAndContinueEngineCapabilities.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedEditAndContinueEngineCapabilities.cs index 38a44fdd10cc9..71a410fe3b2dc 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedEditAndContinueEngineCapabilities.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedEditAndContinueEngineCapabilities.cs @@ -3,32 +3,31 @@ // See the LICENSE file in the project root for more information. using System; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Flags regarding Edit and Continue engine capabilities. +/// +[Flags] +internal enum ManagedEditAndContinueEngineCapabilities { /// - /// Flags regarding Edit and Continue engine capabilities. + /// No flags. /// - [Flags] - internal enum ManagedEditAndContinueEngineCapabilities - { - /// - /// No flags. - /// - None = 0, + None = 0, - /// - /// Whether we can replace methods while stopped. - /// - CanReplaceMethodsWhileStopped = 0x1, + /// + /// Whether we can replace methods while stopped. + /// + CanReplaceMethodsWhileStopped = 0x1, - /// - /// Whether the engine supports changes made in the current method. - /// - SupportsInMethodReplacements = 0x2, + /// + /// Whether the engine supports changes made in the current method. + /// + SupportsInMethodReplacements = 0x2, - /// - /// Whether it supports applying changes once a module has been loaded. - /// - SupportsEditAndContinueOnModuleLoad = 0x4 - } + /// + /// Whether it supports applying changes once a module has been loaded. + /// + SupportsEditAndContinueOnModuleLoad = 0x4 } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedExceptionRegionUpdate.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedExceptionRegionUpdate.cs index fb68ab6ccd386..e93316d0d7bda 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedExceptionRegionUpdate.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedExceptionRegionUpdate.cs @@ -3,52 +3,51 @@ // See the LICENSE file in the project root for more information. using System.Runtime.Serialization; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Exception region affected by a managed update. +/// +/// +/// Creates an ExceptionRegionUpdate. +/// +/// Method information before the change was made. +/// Total of lines modified after the update. +/// Updated text span for the active statement. +[DataContract] +internal readonly struct ManagedExceptionRegionUpdate( + ManagedModuleMethodId method, + int delta, + SourceSpan newSpan) { + /// - /// Exception region affected by a managed update. + /// Method ID. It has the token for the method that contains the exception region + /// and the version when the change was made. /// - /// - /// Creates an ExceptionRegionUpdate. - /// - /// Method information before the change was made. - /// Total of lines modified after the update. - /// Updated text span for the active statement. - [DataContract] - internal readonly struct ManagedExceptionRegionUpdate( - ManagedModuleMethodId method, - int delta, - SourceSpan newSpan) - { - - /// - /// Method ID. It has the token for the method that contains the exception region - /// and the version when the change was made. - /// - [DataMember(Name = "method")] - public ManagedModuleMethodId Method { get; } = method; + [DataMember(Name = "method")] + public ManagedModuleMethodId Method { get; } = method; - /// - /// The delta is the total of lines modified after the update. This value is inverse: - /// - /// OldSpan = NewSpan + Delta - /// NewSpan = OldSpan - Delta - /// - /// For example, if 2 new lines were added preceding the exception region, this value will be -2. - /// - [DataMember(Name = "delta")] - public int Delta { get; } = delta; + /// + /// The delta is the total of lines modified after the update. This value is inverse: + /// + /// OldSpan = NewSpan + Delta + /// NewSpan = OldSpan - Delta + /// + /// For example, if 2 new lines were added preceding the exception region, this value will be -2. + /// + [DataMember(Name = "delta")] + public int Delta { get; } = delta; - /// - /// Specifies where the exception region starts and ends after the update. This value is 0-based. - /// An exception region value generally corresponds to a catch { } block source span before any update is made. - /// - /// - /// This value will take into account any lines affected by the update, so we can correctly track the new exception regions - /// when remapping the instruction pointer. - /// The new span is expected to be: [PreviousExceptionRegionSpan] + [Delta of updated lines]. - /// - [DataMember(Name = "newSpan")] - public SourceSpan NewSpan { get; } = newSpan; - } + /// + /// Specifies where the exception region starts and ends after the update. This value is 0-based. + /// An exception region value generally corresponds to a catch { } block source span before any update is made. + /// + /// + /// This value will take into account any lines affected by the update, so we can correctly track the new exception regions + /// when remapping the instruction pointer. + /// The new span is expected to be: [PreviousExceptionRegionSpan] + [Delta of updated lines]. + /// + [DataMember(Name = "newSpan")] + public SourceSpan NewSpan { get; } = newSpan; } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadAvailability.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadAvailability.cs index 3ad7c9a2a3731..1314c27db5903 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadAvailability.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadAvailability.cs @@ -4,27 +4,26 @@ using System.Runtime.Serialization; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Managed hot reload availability information. +/// +[DataContract] +internal readonly struct ManagedHotReloadAvailability( + ManagedHotReloadAvailabilityStatus status, + string? localizedMessage = null) { + /// - /// Managed hot reload availability information. + /// Status for the managed hot reload session. /// - [DataContract] - internal readonly struct ManagedHotReloadAvailability( - ManagedHotReloadAvailabilityStatus status, - string? localizedMessage = null) - { - - /// - /// Status for the managed hot reload session. - /// - [DataMember(Name = "status")] - public ManagedHotReloadAvailabilityStatus Status { get; } = status; + [DataMember(Name = "status")] + public ManagedHotReloadAvailabilityStatus Status { get; } = status; - /// - /// [Optional] Localized message for . - /// - [DataMember(Name = "localizedMessage")] - public string? LocalizedMessage { get; } = localizedMessage; - } + /// + /// [Optional] Localized message for . + /// + [DataMember(Name = "localizedMessage")] + public string? LocalizedMessage { get; } = localizedMessage; } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadAvailabilityStatus.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadAvailabilityStatus.cs index 68cb0c7c32e3e..2745506b723c2 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadAvailabilityStatus.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadAvailabilityStatus.cs @@ -2,119 +2,118 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Availability status for applying changes under a session. +/// +/// +/// Do not change the value for any of the preexisting status, as this is the value +/// used when reporting telemetry. +/// +internal enum ManagedHotReloadAvailabilityStatus { /// - /// Availability status for applying changes under a session. - /// - /// - /// Do not change the value for any of the preexisting status, as this is the value - /// used when reporting telemetry. - /// - internal enum ManagedHotReloadAvailabilityStatus - { - /// - /// Applying changes is available to the current session. - /// - Available = 0, - - /// - /// Edit and Continue not supported due to interop debugging. - /// - Interop = 1, - - /// - /// Unable to edit code running in SQL server. - /// - SqlClr = 2, - - /// - /// Edit and Continue not supported in minidump debugging. - /// - Minidump = 3, - - /// - /// Edit and Continue not supported since debugger was attached to a process that - /// does not support EnC on attach. - /// - Attach = 4, - - /// - /// Edit and Continue not supported if the assembly has not been loaded. - /// - ModuleNotLoaded = 5, - - /// - /// Edit and Continue not supported if the assembly that has been modified during - /// debugging is reloaded. - /// - ModuleReloaded = 6, - - /// - /// Edit and Continue not supported if the source code on disk does not match the - /// code running in the process. - /// - NotBuilt = 8, - - /// - /// Edit and Continue not supported for the current engine. - /// - UnsupportedEngine = 9, - - /// - /// Edit and Continue in a 64-bit process requires .NET Framework version 4.5.1 or - /// higher. - /// - NotSupportedForClr64Version = 10, - - /// - /// Edit and Continue not supported on the current module. This is a fallback - /// scenario in case we fail to determine the exact reason the module does not - /// support EnC. - /// - NotAllowedForModule = 11, - - /// - /// Edit and Continue not supported if code was optimized. - /// - Optimized = 12, - - /// - /// Edit and Continue not supported if assembly was loaded as domain-neutral. - /// - DomainNeutralAssembly = 13, - - /// - /// Edit and Continue not supported if assembly was loaded through reflection. - /// - ReflectionAssembly = 14, - - /// - /// Edit and Continue not supported if IntelliTrace events and call information is - /// enabled. - /// - IntelliTrace = 15, - - /// - /// Edit and Continue not supported on the .NET Runtime the program is running. - /// - NotAllowedForRuntime = 16, - - /// - /// Edit and Continue not supported due to an internal error in the debugger. - /// - InternalError = 17, - - /// - /// Edit and Continue is unavailable, e.g. no suitable engine providers were found. - /// - Unavailable = 18, - - /// - /// Applying changes has been disabled by the client. - /// If debugging, this means Edit and Continue has been disabled. - /// If not debugging, this means hot reload has been disabled. - /// - Disabled = 19 - }; -} + /// Applying changes is available to the current session. + /// + Available = 0, + + /// + /// Edit and Continue not supported due to interop debugging. + /// + Interop = 1, + + /// + /// Unable to edit code running in SQL server. + /// + SqlClr = 2, + + /// + /// Edit and Continue not supported in minidump debugging. + /// + Minidump = 3, + + /// + /// Edit and Continue not supported since debugger was attached to a process that + /// does not support EnC on attach. + /// + Attach = 4, + + /// + /// Edit and Continue not supported if the assembly has not been loaded. + /// + ModuleNotLoaded = 5, + + /// + /// Edit and Continue not supported if the assembly that has been modified during + /// debugging is reloaded. + /// + ModuleReloaded = 6, + + /// + /// Edit and Continue not supported if the source code on disk does not match the + /// code running in the process. + /// + NotBuilt = 8, + + /// + /// Edit and Continue not supported for the current engine. + /// + UnsupportedEngine = 9, + + /// + /// Edit and Continue in a 64-bit process requires .NET Framework version 4.5.1 or + /// higher. + /// + NotSupportedForClr64Version = 10, + + /// + /// Edit and Continue not supported on the current module. This is a fallback + /// scenario in case we fail to determine the exact reason the module does not + /// support EnC. + /// + NotAllowedForModule = 11, + + /// + /// Edit and Continue not supported if code was optimized. + /// + Optimized = 12, + + /// + /// Edit and Continue not supported if assembly was loaded as domain-neutral. + /// + DomainNeutralAssembly = 13, + + /// + /// Edit and Continue not supported if assembly was loaded through reflection. + /// + ReflectionAssembly = 14, + + /// + /// Edit and Continue not supported if IntelliTrace events and call information is + /// enabled. + /// + IntelliTrace = 15, + + /// + /// Edit and Continue not supported on the .NET Runtime the program is running. + /// + NotAllowedForRuntime = 16, + + /// + /// Edit and Continue not supported due to an internal error in the debugger. + /// + InternalError = 17, + + /// + /// Edit and Continue is unavailable, e.g. no suitable engine providers were found. + /// + Unavailable = 18, + + /// + /// Applying changes has been disabled by the client. + /// If debugging, this means Edit and Continue has been disabled. + /// If not debugging, this means hot reload has been disabled. + /// + Disabled = 19 +}; diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadDiagnostic.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadDiagnostic.cs index 42430f953662d..f6a3931ebd9f1 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadDiagnostic.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadDiagnostic.cs @@ -4,56 +4,55 @@ using System.Runtime.Serialization; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Diagnostic information about a particular edit made through hot reload. +/// +/// +/// Creates a new for an edit made by the user. +/// +/// Diagnostic information identifier. +/// User message. +/// Severity of the edit, whether it's an error or a warning. +/// File path for the target edit. +/// Source span of the edit. +[DataContract] +internal readonly struct ManagedHotReloadDiagnostic( + string id, + string message, + ManagedHotReloadDiagnosticSeverity severity, + string filePath, + SourceSpan span) { + + /// + /// Diagnostic information identifier. + /// + [DataMember(Name = "id")] + public string Id { get; } = id; + + /// + /// User message which will be displayed for the edit. + /// + [DataMember(Name = "message")] + public string Message { get; } = message; + + /// + /// Severity of the diagnostic information. + /// + [DataMember(Name = "severity")] + public ManagedHotReloadDiagnosticSeverity Severity { get; } = severity; + + /// + /// File path where the edit was made. + /// + [DataMember(Name = "filePath")] + public string FilePath { get; } = filePath; + /// - /// Diagnostic information about a particular edit made through hot reload. + /// Source span for the edit. /// - /// - /// Creates a new for an edit made by the user. - /// - /// Diagnostic information identifier. - /// User message. - /// Severity of the edit, whether it's an error or a warning. - /// File path for the target edit. - /// Source span of the edit. - [DataContract] - internal readonly struct ManagedHotReloadDiagnostic( - string id, - string message, - ManagedHotReloadDiagnosticSeverity severity, - string filePath, - SourceSpan span) - { - - /// - /// Diagnostic information identifier. - /// - [DataMember(Name = "id")] - public string Id { get; } = id; - - /// - /// User message which will be displayed for the edit. - /// - [DataMember(Name = "message")] - public string Message { get; } = message; - - /// - /// Severity of the diagnostic information. - /// - [DataMember(Name = "severity")] - public ManagedHotReloadDiagnosticSeverity Severity { get; } = severity; - - /// - /// File path where the edit was made. - /// - [DataMember(Name = "filePath")] - public string FilePath { get; } = filePath; - - /// - /// Source span for the edit. - /// - [DataMember(Name = "span")] - public SourceSpan Span { get; } = span; - } + [DataMember(Name = "span")] + public SourceSpan Span { get; } = span; } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadDiagnosticSeverity.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadDiagnosticSeverity.cs index 398f1894252fc..c5398977becb4 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadDiagnosticSeverity.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadDiagnosticSeverity.cs @@ -2,28 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Severity of a rude edit made by the user. +/// +internal enum ManagedHotReloadDiagnosticSeverity { /// - /// Severity of a rude edit made by the user. + /// Diagnostic for a warning. /// - internal enum ManagedHotReloadDiagnosticSeverity - { - /// - /// Diagnostic for a warning. - /// - Warning = 1, + Warning = 1, - /// - /// Diagnostic for a rude edit. - /// This is a less severe diagnostic and can be generally addressed by restarting the application. - /// - RestartRequired = 2, + /// + /// Diagnostic for a rude edit. + /// This is a less severe diagnostic and can be generally addressed by restarting the application. + /// + RestartRequired = 2, - /// - /// Diagnostic for a compiler error. - /// This means we can't do anything until the error is fixed. - /// - Error = 3 - } + /// + /// Diagnostic for a compiler error. + /// This means we can't do anything until the error is fixed. + /// + Error = 3 } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedInstructionId.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedInstructionId.cs index 53d731cf8dd89..26400e854be13 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedInstructionId.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedInstructionId.cs @@ -7,52 +7,51 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Active instruction identifier. +/// It has the information necessary to track an active instruction within the debug session. +/// +/// +/// Creates an ActiveInstructionId. +/// +/// Method which the instruction is scoped to. +/// IL offset for the instruction. +[DataContract] +[DebuggerDisplay("{GetDebuggerDisplay(),nq}")] +internal readonly struct ManagedInstructionId( + ManagedMethodId method, + int ilOffset) : IEquatable { + /// - /// Active instruction identifier. - /// It has the information necessary to track an active instruction within the debug session. + /// Method which the instruction is scoped to. /// - /// - /// Creates an ActiveInstructionId. - /// - /// Method which the instruction is scoped to. - /// IL offset for the instruction. - [DataContract] - [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] - internal readonly struct ManagedInstructionId( - ManagedMethodId method, - int ilOffset) : IEquatable - { - - /// - /// Method which the instruction is scoped to. - /// - [DataMember(Name = "method")] - public ManagedMethodId Method { get; } = method; + [DataMember(Name = "method")] + public ManagedMethodId Method { get; } = method; - /// - /// The IL offset for the instruction. - /// - [DataMember(Name = "ilOffset")] - public int ILOffset { get; } = ilOffset; + /// + /// The IL offset for the instruction. + /// + [DataMember(Name = "ilOffset")] + public int ILOffset { get; } = ilOffset; - public bool Equals(ManagedInstructionId other) - { - return Method.Equals(other.Method) && ILOffset == other.ILOffset; - } + public bool Equals(ManagedInstructionId other) + { + return Method.Equals(other.Method) && ILOffset == other.ILOffset; + } - public override bool Equals(object? obj) => obj is ManagedInstructionId instr && Equals(instr); + public override bool Equals(object? obj) => obj is ManagedInstructionId instr && Equals(instr); - public override int GetHashCode() - { - return Method.GetHashCode() ^ ILOffset; - } + public override int GetHashCode() + { + return Method.GetHashCode() ^ ILOffset; + } - public static bool operator ==(ManagedInstructionId left, ManagedInstructionId right) => left.Equals(right); + public static bool operator ==(ManagedInstructionId left, ManagedInstructionId right) => left.Equals(right); - public static bool operator !=(ManagedInstructionId left, ManagedInstructionId right) => !(left == right); + public static bool operator !=(ManagedInstructionId left, ManagedInstructionId right) => !(left == right); - internal string GetDebuggerDisplay() => $"{Method.GetDebuggerDisplay()} IL_{ILOffset:X4}"; - } + internal string GetDebuggerDisplay() => $"{Method.GetDebuggerDisplay()} IL_{ILOffset:X4}"; } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedMethodId.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedMethodId.cs index 2d5bae49154e2..7d454cb51cb9f 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedMethodId.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedMethodId.cs @@ -7,60 +7,59 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// ManagedMethodId is a module/method pair which is used to uniquely identify the +/// symbol store's understanding of a particular CLR method. +/// +/// +/// Creates a ManagedMethodId. +/// +/// Module version ID in which the method exists. +/// Method ID. +[DataContract] +[DebuggerDisplay("{GetDebuggerDisplay(),nq}")] +internal readonly struct ManagedMethodId( + Guid module, + ManagedModuleMethodId method) : IEquatable { - /// - /// ManagedMethodId is a module/method pair which is used to uniquely identify the - /// symbol store's understanding of a particular CLR method. - /// - /// - /// Creates a ManagedMethodId. - /// - /// Module version ID in which the method exists. - /// Method ID. - [DataContract] - [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] - internal readonly struct ManagedMethodId( - Guid module, - ManagedModuleMethodId method) : IEquatable + public ManagedMethodId(Guid module, int token, int version) + : this(module, new(token, version)) { - public ManagedMethodId(Guid module, int token, int version) - : this(module, new(token, version)) - { - } + } - /// - /// The module version ID in which the method exists. - /// - [DataMember(Name = "module")] - public Guid Module { get; } = module; + /// + /// The module version ID in which the method exists. + /// + [DataMember(Name = "module")] + public Guid Module { get; } = module; - /// - /// The unique identifier for the method within . - /// - [DataMember(Name = "method")] - public ManagedModuleMethodId Method { get; } = method; + /// + /// The unique identifier for the method within . + /// + [DataMember(Name = "method")] + public ManagedModuleMethodId Method { get; } = method; - public int Token => Method.Token; + public int Token => Method.Token; - public int Version => Method.Version; + public int Version => Method.Version; - public bool Equals(ManagedMethodId other) - { - return Module == other.Module && Method.Equals(other.Method); - } + public bool Equals(ManagedMethodId other) + { + return Module == other.Module && Method.Equals(other.Method); + } - public override bool Equals(object? obj) => obj is ManagedMethodId method && Equals(method); + public override bool Equals(object? obj) => obj is ManagedMethodId method && Equals(method); - public override int GetHashCode() - { - return Module.GetHashCode() ^ Method.GetHashCode(); - } + public override int GetHashCode() + { + return Module.GetHashCode() ^ Method.GetHashCode(); + } - public static bool operator ==(ManagedMethodId left, ManagedMethodId right) => left.Equals(right); + public static bool operator ==(ManagedMethodId left, ManagedMethodId right) => left.Equals(right); - public static bool operator !=(ManagedMethodId left, ManagedMethodId right) => !(left == right); + public static bool operator !=(ManagedMethodId left, ManagedMethodId right) => !(left == right); - internal string GetDebuggerDisplay() => $"mvid={Module} {Method.GetDebuggerDisplay()}"; - } + internal string GetDebuggerDisplay() => $"mvid={Module} {Method.GetDebuggerDisplay()}"; } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedModuleMethodId.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedModuleMethodId.cs index 26ed6488ce94c..99e32491d27a5 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedModuleMethodId.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedModuleMethodId.cs @@ -7,79 +7,78 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// ManagedModuleMethodId is a token/version pair which is used to uniquely identify the +/// symbol store's understanding of a particular CLR method within a module context. +/// See for more details. +/// +[DataContract] +[DebuggerDisplay("{GetDebuggerDisplay(),nq}")] +internal readonly struct ManagedModuleMethodId : IEquatable { /// - /// ManagedModuleMethodId is a token/version pair which is used to uniquely identify the - /// symbol store's understanding of a particular CLR method within a module context. - /// See for more details. + /// Creates a ManagedModuleMethodId. /// - [DataContract] - [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] - internal readonly struct ManagedModuleMethodId : IEquatable + /// Method token. + /// Method version. + /// + /// If is less or equals 0x06000000 or is less or equals zero. + /// + public ManagedModuleMethodId( + int token, + int version) { - /// - /// Creates a ManagedModuleMethodId. - /// - /// Method token. - /// Method version. - /// - /// If is less or equals 0x06000000 or is less or equals zero. - /// - public ManagedModuleMethodId( - int token, - int version) - { - // 0x06 means that the token is for a MethodDef. - // Valid method tokens are expected to be greather than 0x06000000. - if (token <= 0x06000000) - throw new ArgumentOutOfRangeException(nameof(token)); - if (version <= 0) - throw new ArgumentOutOfRangeException(nameof(version)); + // 0x06 means that the token is for a MethodDef. + // Valid method tokens are expected to be greather than 0x06000000. + if (token <= 0x06000000) + throw new ArgumentOutOfRangeException(nameof(token)); + if (version <= 0) + throw new ArgumentOutOfRangeException(nameof(version)); - Token = token; - Version = version; - } + Token = token; + Version = version; + } - /// - /// The method definition metadata token of the method that contains this symbol. - /// - [DataMember(Name = "token")] - public int Token { get; } + /// + /// The method definition metadata token of the method that contains this symbol. + /// + [DataMember(Name = "token")] + public int Token { get; } - /// - /// MethodVersion is a 1-based index. This will be '1' for methods that have not - /// been edited through Edit-and-continue. For edited methods, the version indicates - /// the EnC apply of this method. - /// Thus, if the user does 5 EnC applies and a particular method is only edited in the 5th apply, - /// then there are two method ids for this method, and they have Version = 1 and Version = 5. - /// - /// The debugger needs to deal with old versions of the method because they will - /// continue to be on the call stack until control is unwound.The debugger can also hit - /// breakpoints or stop for exceptions within exception handling regions of old - /// methods. In other words, if the user sets a breakpoint within the catch block of a - /// non-leaf method, the debugger needs to set that breakpoint within the old version - /// of the method. - /// - /// In scenarios such as function breakpoint binding, the value '0' may used to - /// indicate the current version of the method. - /// - [DataMember(Name = "version")] - public int Version { get; } + /// + /// MethodVersion is a 1-based index. This will be '1' for methods that have not + /// been edited through Edit-and-continue. For edited methods, the version indicates + /// the EnC apply of this method. + /// Thus, if the user does 5 EnC applies and a particular method is only edited in the 5th apply, + /// then there are two method ids for this method, and they have Version = 1 and Version = 5. + /// + /// The debugger needs to deal with old versions of the method because they will + /// continue to be on the call stack until control is unwound.The debugger can also hit + /// breakpoints or stop for exceptions within exception handling regions of old + /// methods. In other words, if the user sets a breakpoint within the catch block of a + /// non-leaf method, the debugger needs to set that breakpoint within the old version + /// of the method. + /// + /// In scenarios such as function breakpoint binding, the value '0' may used to + /// indicate the current version of the method. + /// + [DataMember(Name = "version")] + public int Version { get; } - public bool Equals(ManagedModuleMethodId other) - { - return Token == other.Token && Version == other.Version; - } + public bool Equals(ManagedModuleMethodId other) + { + return Token == other.Token && Version == other.Version; + } - public override bool Equals(object? obj) => obj is ManagedModuleMethodId method && Equals(method); + public override bool Equals(object? obj) => obj is ManagedModuleMethodId method && Equals(method); - public override int GetHashCode() => Token ^ Version; + public override int GetHashCode() => Token ^ Version; - public static bool operator ==(ManagedModuleMethodId left, ManagedModuleMethodId right) => left.Equals(right); + public static bool operator ==(ManagedModuleMethodId left, ManagedModuleMethodId right) => left.Equals(right); - public static bool operator !=(ManagedModuleMethodId left, ManagedModuleMethodId right) => !(left == right); + public static bool operator !=(ManagedModuleMethodId left, ManagedModuleMethodId right) => !(left == right); - internal string GetDebuggerDisplay() => $"0x{Token:X8} v{Version}"; - } + internal string GetDebuggerDisplay() => $"0x{Token:X8} v{Version}"; } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/SequencePointUpdates.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/SequencePointUpdates.cs index d91848b761e9e..b4f96d8db5b61 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/SequencePointUpdates.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/SequencePointUpdates.cs @@ -5,32 +5,31 @@ using System.Runtime.Serialization; using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Sequence points affected by an update on a specified file. +/// +/// +/// Creates a SequencePointsUpdate. +/// +/// Name of the file which was modified. +/// Collection of the file lines affected by the update. +[DataContract] +internal readonly struct SequencePointUpdates( + string fileName, + ImmutableArray lineUpdates) { + /// - /// Sequence points affected by an update on a specified file. + /// Name of the modified file as stored in PDB. /// - /// - /// Creates a SequencePointsUpdate. - /// - /// Name of the file which was modified. - /// Collection of the file lines affected by the update. - [DataContract] - internal readonly struct SequencePointUpdates( - string fileName, - ImmutableArray lineUpdates) - { - - /// - /// Name of the modified file as stored in PDB. - /// - [DataMember(Name = "fileName")] - public string FileName { get; } = fileName; + [DataMember(Name = "fileName")] + public string FileName { get; } = fileName; - /// - /// Collection of the file lines affected by the update. - /// - [DataMember(Name = "lineUpdates")] - public ImmutableArray LineUpdates { get; } = lineUpdates; - } + /// + /// Collection of the file lines affected by the update. + /// + [DataMember(Name = "lineUpdates")] + public ImmutableArray LineUpdates { get; } = lineUpdates; } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/SourceLineUpdate.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/SourceLineUpdate.cs index fbc678f481f41..18e140c7e3078 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/SourceLineUpdate.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/SourceLineUpdate.cs @@ -5,50 +5,49 @@ using System.Runtime.Serialization; using System; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// Maps source lines affected by an update. +/// Zero-based line number. +/// +[DataContract] +internal readonly struct SourceLineUpdate { /// - /// Maps source lines affected by an update. - /// Zero-based line number. + /// Creates a SourceLineUpdate. /// - [DataContract] - internal readonly struct SourceLineUpdate + /// Line number before the update was made. + /// Line number after the update was made. + /// + /// If or is less than 0. + /// + /// + /// We expect that and have the same value + /// when the line delta is zero. + /// + public SourceLineUpdate( + int oldLine, + int newLine) { - /// - /// Creates a SourceLineUpdate. - /// - /// Line number before the update was made. - /// Line number after the update was made. - /// - /// If or is less than 0. - /// - /// - /// We expect that and have the same value - /// when the line delta is zero. - /// - public SourceLineUpdate( - int oldLine, - int newLine) - { - if (oldLine < 0) - throw new ArgumentOutOfRangeException(nameof(oldLine)); - if (newLine < 0) - throw new ArgumentOutOfRangeException(nameof(newLine)); + if (oldLine < 0) + throw new ArgumentOutOfRangeException(nameof(oldLine)); + if (newLine < 0) + throw new ArgumentOutOfRangeException(nameof(newLine)); - OldLine = oldLine; - NewLine = newLine; - } + OldLine = oldLine; + NewLine = newLine; + } - /// - /// Line number before the update was made, must be zero-based. - /// - [DataMember(Name = "oldLine")] - public int OldLine { get; } + /// + /// Line number before the update was made, must be zero-based. + /// + [DataMember(Name = "oldLine")] + public int OldLine { get; } - /// - /// Line number after the update was made, must be zero-based. - /// - [DataMember(Name = "newLine")] - public int NewLine { get; } - } + /// + /// Line number after the update was made, must be zero-based. + /// + [DataMember(Name = "newLine")] + public int NewLine { get; } } diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/SourceSpan.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/SourceSpan.cs index 4f72ea6000d12..0ad17a5582421 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/SourceSpan.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/SourceSpan.cs @@ -7,103 +7,102 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue +namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +/// +/// The start/end line/column ranges for a contiguous span of text. These should be all zero-indexed. +/// This is an alias for TextSpan structures. +/// +[DataContract] +[DebuggerDisplay("{GetDebuggerDisplay(),nq}")] +internal readonly struct SourceSpan : IEquatable { /// - /// The start/end line/column ranges for a contiguous span of text. These should be all zero-indexed. - /// This is an alias for TextSpan structures. + /// Creates a TextSpan. /// - [DataContract] - [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] - internal readonly struct SourceSpan : IEquatable + /// Start line. + /// Start column. -1 if column information is missing. + /// End line. + /// End column. -1 if column information is missing. + /// + /// If or is less than zero. + /// If or is less than -1. + /// If only or is -1. + /// + public SourceSpan( + int startLine, + int startColumn, + int endLine, + int endColumn) { - /// - /// Creates a TextSpan. - /// - /// Start line. - /// Start column. -1 if column information is missing. - /// End line. - /// End column. -1 if column information is missing. - /// - /// If or is less than zero. - /// If or is less than -1. - /// If only or is -1. - /// - public SourceSpan( - int startLine, - int startColumn, - int endLine, - int endColumn) - { - if (startLine < 0) - throw new ArgumentOutOfRangeException(nameof(startLine)); - if (startColumn < -1) - throw new ArgumentOutOfRangeException(nameof(startColumn)); - if (endLine < 0) - throw new ArgumentOutOfRangeException(nameof(endLine)); - if (endColumn < -1) - throw new ArgumentOutOfRangeException(nameof(endColumn)); + if (startLine < 0) + throw new ArgumentOutOfRangeException(nameof(startLine)); + if (startColumn < -1) + throw new ArgumentOutOfRangeException(nameof(startColumn)); + if (endLine < 0) + throw new ArgumentOutOfRangeException(nameof(endLine)); + if (endColumn < -1) + throw new ArgumentOutOfRangeException(nameof(endColumn)); - if ((startColumn == -1 || endColumn == -1) && startColumn != endColumn) - throw new ArgumentOutOfRangeException(startColumn == -1 ? nameof(endColumn) : nameof(startColumn)); + if ((startColumn == -1 || endColumn == -1) && startColumn != endColumn) + throw new ArgumentOutOfRangeException(startColumn == -1 ? nameof(endColumn) : nameof(startColumn)); - StartLine = startLine; - StartColumn = startColumn; - EndLine = endLine; - EndColumn = endColumn; - } + StartLine = startLine; + StartColumn = startColumn; + EndLine = endLine; + EndColumn = endColumn; + } - /// - /// Zero-based integer for the starting source line. - /// - [DataMember(Name = "startLine")] - public int StartLine { get; } + /// + /// Zero-based integer for the starting source line. + /// + [DataMember(Name = "startLine")] + public int StartLine { get; } - /// - /// Zero-based integer for the starting source column. If column information is missing (e.g. language service doesn't support it), - /// this value should be treated as -1. - /// - [DataMember(Name = "startColumn")] - public int StartColumn { get; } + /// + /// Zero-based integer for the starting source column. If column information is missing (e.g. language service doesn't support it), + /// this value should be treated as -1. + /// + [DataMember(Name = "startColumn")] + public int StartColumn { get; } - /// - /// Zero-based integer for the ending source line. - /// - [DataMember(Name = "endLine")] - public int EndLine { get; } + /// + /// Zero-based integer for the ending source line. + /// + [DataMember(Name = "endLine")] + public int EndLine { get; } - /// - /// Zero-based integer for the ending source column. If column information is missing (e.g. language service doesn't support it), - /// this value should be treated as -1. - /// - [DataMember(Name = "endColumn")] - public int EndColumn { get; } + /// + /// Zero-based integer for the ending source column. If column information is missing (e.g. language service doesn't support it), + /// this value should be treated as -1. + /// + [DataMember(Name = "endColumn")] + public int EndColumn { get; } - public bool Equals(SourceSpan other) - { - return StartLine == other.StartLine && - StartColumn == other.StartColumn && - EndLine == other.EndLine && - EndColumn == other.EndColumn; - } + public bool Equals(SourceSpan other) + { + return StartLine == other.StartLine && + StartColumn == other.StartColumn && + EndLine == other.EndLine && + EndColumn == other.EndColumn; + } - public override bool Equals(object? obj) => obj is SourceSpan span && Equals(span); + public override bool Equals(object? obj) => obj is SourceSpan span && Equals(span); - public override int GetHashCode() - { - return - ((StartLine & 0xffff) << 16) | // bytes 3, 2 are a hash for start line - ((StartColumn & 0xff) << 8) | // byte 1 is a hash for start column - ((EndLine ^ EndColumn) % 255); // byte 0 is a hash for end line and column - } + public override int GetHashCode() + { + return + ((StartLine & 0xffff) << 16) | // bytes 3, 2 are a hash for start line + ((StartColumn & 0xff) << 8) | // byte 1 is a hash for start column + ((EndLine ^ EndColumn) % 255); // byte 0 is a hash for end line and column + } - public static bool operator ==(SourceSpan left, SourceSpan right) => left.Equals(right); + public static bool operator ==(SourceSpan left, SourceSpan right) => left.Equals(right); - public static bool operator !=(SourceSpan left, SourceSpan right) => !(left == right); + public static bool operator !=(SourceSpan left, SourceSpan right) => !(left == right); - internal string GetDebuggerDisplay() - => (StartColumn >= 0) - ? $"({StartLine},{StartColumn})-({EndLine},{EndColumn})" - : $"{StartLine}-{EndLine}"; - } + internal string GetDebuggerDisplay() + => (StartColumn >= 0) + ? $"({StartLine},{StartColumn})-({EndLine},{EndColumn})" + : $"{StartLine}-{EndLine}"; } diff --git a/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeCodeRefactoringProvider.cs b/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeCodeRefactoringProvider.cs index 394f220b8b355..1c5946446f481 100644 --- a/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeCodeRefactoringProvider.cs @@ -8,28 +8,27 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ConvertAnonymousType +namespace Microsoft.CodeAnalysis.ConvertAnonymousType; + +internal abstract class AbstractConvertAnonymousTypeCodeRefactoringProvider + : CodeRefactoringProvider + where TAnonymousObjectCreationExpressionSyntax : SyntaxNode { - internal abstract class AbstractConvertAnonymousTypeCodeRefactoringProvider - : CodeRefactoringProvider - where TAnonymousObjectCreationExpressionSyntax : SyntaxNode + protected static async Task<(TAnonymousObjectCreationExpressionSyntax?, INamedTypeSymbol?)> TryGetAnonymousObjectAsync( + Document document, TextSpan span, CancellationToken cancellationToken) { - protected static async Task<(TAnonymousObjectCreationExpressionSyntax?, INamedTypeSymbol?)> TryGetAnonymousObjectAsync( - Document document, TextSpan span, CancellationToken cancellationToken) - { - // Gets a `TAnonymousObjectCreationExpressionSyntax` for current selection. - // Due to the way `TryGetSelectedNodeAsync` works and how `TAnonymousObjectCreationExpressionSyntax` is e.g. for C# constructed - // it matches even when caret is next to some tokens within the anonymous object creation node. - // E.g.: `var a = new [||]{ b=1,[||] c=2 };` both match due to the caret being next to `,` and `{`. - var anonymousObject = await document.TryGetRelevantNodeAsync( - span, cancellationToken).ConfigureAwait(false); - if (anonymousObject == null) - return default; + // Gets a `TAnonymousObjectCreationExpressionSyntax` for current selection. + // Due to the way `TryGetSelectedNodeAsync` works and how `TAnonymousObjectCreationExpressionSyntax` is e.g. for C# constructed + // it matches even when caret is next to some tokens within the anonymous object creation node. + // E.g.: `var a = new [||]{ b=1,[||] c=2 };` both match due to the caret being next to `,` and `{`. + var anonymousObject = await document.TryGetRelevantNodeAsync( + span, cancellationToken).ConfigureAwait(false); + if (anonymousObject == null) + return default; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var anonymousType = semanticModel.GetTypeInfo(anonymousObject, cancellationToken).Type as INamedTypeSymbol; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var anonymousType = semanticModel.GetTypeInfo(anonymousObject, cancellationToken).Type as INamedTypeSymbol; - return (anonymousObject, anonymousType); - } + return (anonymousObject, anonymousType); } } diff --git a/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeToClassCodeRefactoringProvider.cs b/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeToClassCodeRefactoringProvider.cs index 8368c67a33f02..038a136fa2e6e 100644 --- a/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeToClassCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeToClassCodeRefactoringProvider.cs @@ -23,384 +23,383 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ConvertAnonymousType +namespace Microsoft.CodeAnalysis.ConvertAnonymousType; + +internal abstract class AbstractConvertAnonymousTypeToClassCodeRefactoringProvider< + TExpressionSyntax, + TNameSyntax, + TIdentifierNameSyntax, + TObjectCreationExpressionSyntax, + TAnonymousObjectCreationExpressionSyntax, + TNamespaceDeclarationSyntax> + : AbstractConvertAnonymousTypeCodeRefactoringProvider + where TExpressionSyntax : SyntaxNode + where TNameSyntax : TExpressionSyntax + where TIdentifierNameSyntax : TNameSyntax + where TObjectCreationExpressionSyntax : TExpressionSyntax + where TAnonymousObjectCreationExpressionSyntax : TExpressionSyntax + where TNamespaceDeclarationSyntax : SyntaxNode { - internal abstract class AbstractConvertAnonymousTypeToClassCodeRefactoringProvider< - TExpressionSyntax, - TNameSyntax, - TIdentifierNameSyntax, - TObjectCreationExpressionSyntax, - TAnonymousObjectCreationExpressionSyntax, - TNamespaceDeclarationSyntax> - : AbstractConvertAnonymousTypeCodeRefactoringProvider - where TExpressionSyntax : SyntaxNode - where TNameSyntax : TExpressionSyntax - where TIdentifierNameSyntax : TNameSyntax - where TObjectCreationExpressionSyntax : TExpressionSyntax - where TAnonymousObjectCreationExpressionSyntax : TExpressionSyntax - where TNamespaceDeclarationSyntax : SyntaxNode - { - protected abstract TObjectCreationExpressionSyntax CreateObjectCreationExpression(TNameSyntax nameNode, TAnonymousObjectCreationExpressionSyntax currentAnonymousObject); + protected abstract TObjectCreationExpressionSyntax CreateObjectCreationExpression(TNameSyntax nameNode, TAnonymousObjectCreationExpressionSyntax currentAnonymousObject); - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, textSpan, cancellationToken) = context; + var (anonymousObject, anonymousType) = await TryGetAnonymousObjectAsync(document, textSpan, cancellationToken).ConfigureAwait(false); + + if (anonymousObject == null || anonymousType == null) + return; + + // Check if the anonymous type actually references another anonymous type inside of it. + // If it does, we can't convert this. There is no way to describe this anonymous type + // in the concrete type we create. + var containsAnonymousType = anonymousType.GetMembers() + .OfType() + .Any(p => p.Type.ContainsAnonymousType()); + if (containsAnonymousType) + return; + + var syntaxFacts = document.GetRequiredLanguageService(); + if (syntaxFacts.SupportsRecord(anonymousObject.SyntaxTree.Options)) { - var (document, textSpan, cancellationToken) = context; - var (anonymousObject, anonymousType) = await TryGetAnonymousObjectAsync(document, textSpan, cancellationToken).ConfigureAwait(false); - - if (anonymousObject == null || anonymousType == null) - return; - - // Check if the anonymous type actually references another anonymous type inside of it. - // If it does, we can't convert this. There is no way to describe this anonymous type - // in the concrete type we create. - var containsAnonymousType = anonymousType.GetMembers() - .OfType() - .Any(p => p.Type.ContainsAnonymousType()); - if (containsAnonymousType) - return; - - var syntaxFacts = document.GetRequiredLanguageService(); - if (syntaxFacts.SupportsRecord(anonymousObject.SyntaxTree.Options)) - { - context.RegisterRefactoring( - CodeAction.Create( - FeaturesResources.Convert_to_record, - c => ConvertAsync(document, textSpan, context.Options, isRecord: true, c), - nameof(FeaturesResources.Convert_to_record)), - anonymousObject.Span); - } - context.RegisterRefactoring( CodeAction.Create( - FeaturesResources.Convert_to_class, - c => ConvertAsync(document, textSpan, context.Options, isRecord: false, c), - nameof(FeaturesResources.Convert_to_class)), + FeaturesResources.Convert_to_record, + c => ConvertAsync(document, textSpan, context.Options, isRecord: true, c), + nameof(FeaturesResources.Convert_to_record)), anonymousObject.Span); } - private async Task ConvertAsync(Document document, TextSpan span, CodeActionOptionsProvider fallbackOptions, bool isRecord, CancellationToken cancellationToken) - { - var (anonymousObject, anonymousType) = await TryGetAnonymousObjectAsync(document, span, cancellationToken).ConfigureAwait(false); - - Debug.Assert(anonymousObject != null); - Debug.Assert(anonymousType != null); - - var position = span.Start; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - // Generate a unique name for the class we're creating. We'll also add a rename - // annotation so the user can pick the right name for the type afterwards. - var className = NameGenerator.GenerateUniqueName( - isRecord ? "NewRecord" : "NewClass", - n => semanticModel.LookupSymbols(position, name: n).IsEmpty); - - // First, create the set of properties this class will have based on the properties the - // anonymous type has itself. Also, get a mapping of the original anonymous type's - // properties to the new name we generated for it (if we converted camelCase to - // PascalCase). - var (properties, propertyMap) = GenerateProperties(document, anonymousType); - - // Next, generate the full class that will be used to replace all instances of this - // anonymous type. - var namedTypeSymbol = await GenerateFinalNamedTypeAsync( - document, className, isRecord, properties, cancellationToken).ConfigureAwait(false); - - var generator = SyntaxGenerator.GetGenerator(document); - var editor = new SyntaxEditor(root, generator); - - var syntaxFacts = document.GetRequiredLanguageService(); - var containingMember = anonymousObject.FirstAncestorOrSelf((node, syntaxFacts) => syntaxFacts.IsMethodLevelMember(node), syntaxFacts) ?? anonymousObject; - - // Next, go and update any references to these anonymous type properties to match - // the new PascalCased name we've picked for the new properties that will go in - // the named type. - await ReplacePropertyReferencesAsync( - document, editor, containingMember, - propertyMap, cancellationToken).ConfigureAwait(false); - - // Next, go through and replace all matching anonymous types in this method with a call - // to construct the new named type we've generated. - await ReplaceMatchingAnonymousTypesAsync( - document, editor, namedTypeSymbol, - containingMember, anonymousObject, - anonymousType, cancellationToken).ConfigureAwait(false); - - var context = new CodeGenerationContext( - generateMembers: true, - sortMembers: false, - autoInsertionLocation: false); - - var info = await document.GetCodeGenerationInfoAsync(context, fallbackOptions, cancellationToken).ConfigureAwait(false); - var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - - // Then, actually insert the new class in the appropriate container. - var container = anonymousObject.GetAncestor() ?? root; - editor.ReplaceNode(container, (currentContainer, _) => - info.Service.AddNamedType(currentContainer, namedTypeSymbol, info, cancellationToken)); - - var updatedDocument = document.WithSyntaxRoot(editor.GetChangedRoot()); - - // Finally, format using the equals+getHashCode service so that our generated methods - // follow any special formatting rules specific to them. - var equalsAndGetHashCodeService = document.GetRequiredLanguageService(); - return await equalsAndGetHashCodeService.FormatDocumentAsync( - updatedDocument, formattingOptions, cancellationToken).ConfigureAwait(false); - } + context.RegisterRefactoring( + CodeAction.Create( + FeaturesResources.Convert_to_class, + c => ConvertAsync(document, textSpan, context.Options, isRecord: false, c), + nameof(FeaturesResources.Convert_to_class)), + anonymousObject.Span); + } + + private async Task ConvertAsync(Document document, TextSpan span, CodeActionOptionsProvider fallbackOptions, bool isRecord, CancellationToken cancellationToken) + { + var (anonymousObject, anonymousType) = await TryGetAnonymousObjectAsync(document, span, cancellationToken).ConfigureAwait(false); + + Debug.Assert(anonymousObject != null); + Debug.Assert(anonymousType != null); + + var position = span.Start; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + // Generate a unique name for the class we're creating. We'll also add a rename + // annotation so the user can pick the right name for the type afterwards. + var className = NameGenerator.GenerateUniqueName( + isRecord ? "NewRecord" : "NewClass", + n => semanticModel.LookupSymbols(position, name: n).IsEmpty); + + // First, create the set of properties this class will have based on the properties the + // anonymous type has itself. Also, get a mapping of the original anonymous type's + // properties to the new name we generated for it (if we converted camelCase to + // PascalCase). + var (properties, propertyMap) = GenerateProperties(document, anonymousType); + + // Next, generate the full class that will be used to replace all instances of this + // anonymous type. + var namedTypeSymbol = await GenerateFinalNamedTypeAsync( + document, className, isRecord, properties, cancellationToken).ConfigureAwait(false); + + var generator = SyntaxGenerator.GetGenerator(document); + var editor = new SyntaxEditor(root, generator); + + var syntaxFacts = document.GetRequiredLanguageService(); + var containingMember = anonymousObject.FirstAncestorOrSelf((node, syntaxFacts) => syntaxFacts.IsMethodLevelMember(node), syntaxFacts) ?? anonymousObject; + + // Next, go and update any references to these anonymous type properties to match + // the new PascalCased name we've picked for the new properties that will go in + // the named type. + await ReplacePropertyReferencesAsync( + document, editor, containingMember, + propertyMap, cancellationToken).ConfigureAwait(false); + + // Next, go through and replace all matching anonymous types in this method with a call + // to construct the new named type we've generated. + await ReplaceMatchingAnonymousTypesAsync( + document, editor, namedTypeSymbol, + containingMember, anonymousObject, + anonymousType, cancellationToken).ConfigureAwait(false); + + var context = new CodeGenerationContext( + generateMembers: true, + sortMembers: false, + autoInsertionLocation: false); + + var info = await document.GetCodeGenerationInfoAsync(context, fallbackOptions, cancellationToken).ConfigureAwait(false); + var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + + // Then, actually insert the new class in the appropriate container. + var container = anonymousObject.GetAncestor() ?? root; + editor.ReplaceNode(container, (currentContainer, _) => + info.Service.AddNamedType(currentContainer, namedTypeSymbol, info, cancellationToken)); + + var updatedDocument = document.WithSyntaxRoot(editor.GetChangedRoot()); + + // Finally, format using the equals+getHashCode service so that our generated methods + // follow any special formatting rules specific to them. + var equalsAndGetHashCodeService = document.GetRequiredLanguageService(); + return await equalsAndGetHashCodeService.FormatDocumentAsync( + updatedDocument, formattingOptions, cancellationToken).ConfigureAwait(false); + } - private static async Task ReplacePropertyReferencesAsync( - Document document, SyntaxEditor editor, SyntaxNode containingMember, - ImmutableDictionary propertyMap, CancellationToken cancellationToken) + private static async Task ReplacePropertyReferencesAsync( + Document document, SyntaxEditor editor, SyntaxNode containingMember, + ImmutableDictionary propertyMap, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var identifiers = containingMember.DescendantNodes().OfType(); + + foreach (var identifier in identifiers) { - var syntaxFacts = document.GetRequiredLanguageService(); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var identifiers = containingMember.DescendantNodes().OfType(); + if (!syntaxFacts.IsNameOfSimpleMemberAccessExpression(identifier) && + !syntaxFacts.IsNameOfMemberBindingExpression(identifier)) + { + continue; + } - foreach (var identifier in identifiers) + if (semanticModel.GetSymbolInfo(identifier, cancellationToken).GetAnySymbol() is not IPropertySymbol symbol) + continue; + + if (propertyMap.TryGetValue(symbol, out var newName)) { - if (!syntaxFacts.IsNameOfSimpleMemberAccessExpression(identifier) && - !syntaxFacts.IsNameOfMemberBindingExpression(identifier)) - { - continue; - } - - if (semanticModel.GetSymbolInfo(identifier, cancellationToken).GetAnySymbol() is not IPropertySymbol symbol) - continue; - - if (propertyMap.TryGetValue(symbol, out var newName)) - { - editor.ReplaceNode( - identifier, - (currentId, g) => g.IdentifierName(newName).WithTriviaFrom(currentId)); - } + editor.ReplaceNode( + identifier, + (currentId, g) => g.IdentifierName(newName).WithTriviaFrom(currentId)); } } + } - private async Task ReplaceMatchingAnonymousTypesAsync( - Document document, SyntaxEditor editor, INamedTypeSymbol classSymbol, - SyntaxNode containingMember, TAnonymousObjectCreationExpressionSyntax creationNode, - INamedTypeSymbol anonymousType, CancellationToken cancellationToken) + private async Task ReplaceMatchingAnonymousTypesAsync( + Document document, SyntaxEditor editor, INamedTypeSymbol classSymbol, + SyntaxNode containingMember, TAnonymousObjectCreationExpressionSyntax creationNode, + INamedTypeSymbol anonymousType, CancellationToken cancellationToken) + { + // When invoked we want to fixup all creations of the "same" anonymous type within the + // containing method. We define same-ness as meaning "they have the type symbol". this + // means both have the same member names, in the same order, with the same member types. + // We fix all these up in the method because the user may be creating several instances + // of this anonymous type in that method and then combining them in interesting ways + // (i.e. checking them for equality, using them in collections, etc.). The language + // guarantees within a method boundary that these will be the same type and can be used + // together in this fashion. + // + // Note: we could consider expanding this in the future (potentially with another + // lightbulb action). Specifically, we could look in the containing type and replace + // any matches in any methods. + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var childCreationNodes = containingMember.DescendantNodesAndSelf() + .OfType(); + + foreach (var childCreation in childCreationNodes) { - // When invoked we want to fixup all creations of the "same" anonymous type within the - // containing method. We define same-ness as meaning "they have the type symbol". this - // means both have the same member names, in the same order, with the same member types. - // We fix all these up in the method because the user may be creating several instances - // of this anonymous type in that method and then combining them in interesting ways - // (i.e. checking them for equality, using them in collections, etc.). The language - // guarantees within a method boundary that these will be the same type and can be used - // together in this fashion. - // - // Note: we could consider expanding this in the future (potentially with another - // lightbulb action). Specifically, we could look in the containing type and replace - // any matches in any methods. - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var childCreationNodes = containingMember.DescendantNodesAndSelf() - .OfType(); - - foreach (var childCreation in childCreationNodes) + var childType = semanticModel.GetTypeInfo(childCreation, cancellationToken).Type; + if (childType == null) { - var childType = semanticModel.GetTypeInfo(childCreation, cancellationToken).Type; - if (childType == null) - { - Debug.Fail("We should always be able to get an anonymous type for any anonymous creation node."); - continue; - } - - if (anonymousType.Equals(childType)) - ReplaceWithObjectCreation(editor, classSymbol, creationNode, childCreation); + Debug.Fail("We should always be able to get an anonymous type for any anonymous creation node."); + continue; } - } - private void ReplaceWithObjectCreation( - SyntaxEditor editor, INamedTypeSymbol classSymbol, - TAnonymousObjectCreationExpressionSyntax startingCreationNode, - TAnonymousObjectCreationExpressionSyntax childCreation) - { - // Use the callback form as anonymous types may be nested, and we want to - // properly replace them even in that case. - editor.ReplaceNode( - childCreation, - (currentNode, g) => - { - var currentAnonymousObject = (TAnonymousObjectCreationExpressionSyntax)currentNode; - - // If we hit the node the user started on, then add the rename annotation here. - var className = classSymbol.Name; - var classNameToken = startingCreationNode == childCreation - ? g.Identifier(className).WithAdditionalAnnotations(RenameAnnotation.Create()) - : g.Identifier(className); - - var classNameNode = classSymbol.TypeParameters.Length == 0 - ? (TNameSyntax)g.IdentifierName(classNameToken) - : (TNameSyntax)g.GenericName(classNameToken, - classSymbol.TypeParameters.Select(tp => g.IdentifierName(tp.Name))); - - return CreateObjectCreationExpression(classNameNode, currentAnonymousObject) - .WithAdditionalAnnotations(Formatter.Annotation); - }); + if (anonymousType.Equals(childType)) + ReplaceWithObjectCreation(editor, classSymbol, creationNode, childCreation); } + } - private static async Task GenerateFinalNamedTypeAsync( - Document document, string typeName, bool isRecord, - ImmutableArray properties, - CancellationToken cancellationToken) - { - var generator = SyntaxGenerator.GetGenerator(document); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + private void ReplaceWithObjectCreation( + SyntaxEditor editor, INamedTypeSymbol classSymbol, + TAnonymousObjectCreationExpressionSyntax startingCreationNode, + TAnonymousObjectCreationExpressionSyntax childCreation) + { + // Use the callback form as anonymous types may be nested, and we want to + // properly replace them even in that case. + editor.ReplaceNode( + childCreation, + (currentNode, g) => + { + var currentAnonymousObject = (TAnonymousObjectCreationExpressionSyntax)currentNode; - // Next, see if any of the properties ended up using any type parameters from the - // containing method/named-type. If so, we'll need to generate a generic type so we can - // properly pass these along. - var capturedTypeParameters = - properties.Select(p => p.Type) - .SelectMany(t => t.GetReferencedTypeParameters()) - .Distinct() - .ToImmutableArray(); + // If we hit the node the user started on, then add the rename annotation here. + var className = classSymbol.Name; + var classNameToken = startingCreationNode == childCreation + ? g.Identifier(className).WithAdditionalAnnotations(RenameAnnotation.Create()) + : g.Identifier(className); - using var _ = ArrayBuilder.GetInstance(out var members); + var classNameNode = classSymbol.TypeParameters.Length == 0 + ? (TNameSyntax)g.IdentifierName(classNameToken) + : (TNameSyntax)g.GenericName(classNameToken, + classSymbol.TypeParameters.Select(tp => g.IdentifierName(tp.Name))); - if (isRecord) - { - // Create a record with a single primary constructor, containing a parameter for all the properties - // we're generating. - var constructor = CodeGenerationSymbolFactory.CreateConstructorSymbol( - attributes: default, - Accessibility.Public, - modifiers: default, - typeName, - properties.SelectAsArray(prop => CodeGenerationSymbolFactory.CreateParameterSymbol(prop.Type, prop.Name)), - isPrimaryConstructor: true); - - members.Add(constructor); - } - else - { - // Now try to generate all the members that will go in the new class. This is a bit - // circular. In order to generate some of the members, we need to know about the type. - // But in order to create the type, we need the members. To address this we do two - // passes. First, we create an empty version of the class. This can then be used to - // help create members like Equals/GetHashCode. Then, once we have all the members we - // create the final type. - var namedTypeWithoutMembers = CreateNamedType(typeName, isRecord: false, capturedTypeParameters, members: default); - - var constructor = CreateClassConstructor(semanticModel, typeName, properties, generator); - - // Generate Equals/GetHashCode. Only readonly properties are suitable for these - // methods. We can defer to our existing language service for this so that we - // generate the same Equals/GetHashCode that our other IDE features generate. - var readonlyProperties = ImmutableArray.CastUp( - properties.WhereAsArray(p => p.SetMethod == null)); - - var equalsAndGetHashCodeService = document.GetRequiredLanguageService(); - - var equalsMethod = await equalsAndGetHashCodeService.GenerateEqualsMethodAsync( - document, namedTypeWithoutMembers, readonlyProperties, - localNameOpt: SyntaxGeneratorExtensions.OtherName, cancellationToken).ConfigureAwait(false); - var getHashCodeMethod = await equalsAndGetHashCodeService.GenerateGetHashCodeMethodAsync( - document, namedTypeWithoutMembers, - readonlyProperties, cancellationToken).ConfigureAwait(false); - - members.AddRange(properties); - members.Add(constructor); - members.Add(equalsMethod); - members.Add(getHashCodeMethod); + return CreateObjectCreationExpression(classNameNode, currentAnonymousObject) + .WithAdditionalAnnotations(Formatter.Annotation); + }); + } - } + private static async Task GenerateFinalNamedTypeAsync( + Document document, string typeName, bool isRecord, + ImmutableArray properties, + CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - return CreateNamedType(typeName, isRecord, capturedTypeParameters, members.ToImmutable()); - } + // Next, see if any of the properties ended up using any type parameters from the + // containing method/named-type. If so, we'll need to generate a generic type so we can + // properly pass these along. + var capturedTypeParameters = + properties.Select(p => p.Type) + .SelectMany(t => t.GetReferencedTypeParameters()) + .Distinct() + .ToImmutableArray(); - private static INamedTypeSymbol CreateNamedType( - string className, bool isRecord, ImmutableArray capturedTypeParameters, ImmutableArray members) + using var _ = ArrayBuilder.GetInstance(out var members); + + if (isRecord) { - return CodeGenerationSymbolFactory.CreateNamedTypeSymbol( - attributes: default, Accessibility.Internal, modifiers: default, - isRecord, TypeKind.Class, className, capturedTypeParameters, members: members); + // Create a record with a single primary constructor, containing a parameter for all the properties + // we're generating. + var constructor = CodeGenerationSymbolFactory.CreateConstructorSymbol( + attributes: default, + Accessibility.Public, + modifiers: default, + typeName, + properties.SelectAsArray(prop => CodeGenerationSymbolFactory.CreateParameterSymbol(prop.Type, prop.Name)), + isPrimaryConstructor: true); + + members.Add(constructor); } - - private static (ImmutableArray properties, ImmutableDictionary propertyMap) GenerateProperties( - Document document, INamedTypeSymbol anonymousType) + else { - var originalProperties = anonymousType.GetMembers().OfType().ToImmutableArray(); - var newProperties = originalProperties.SelectAsArray(p => GenerateProperty(document, p)); - - // If we changed the names of any properties, record that name mapping. We'll - // use this to update reference to the old anonymous-type properties to the new - // names. - var builder = ImmutableDictionary.CreateBuilder(); - for (var i = 0; i < originalProperties.Length; i++) - { - var originalProperty = originalProperties[i]; - var newProperty = newProperties[i]; + // Now try to generate all the members that will go in the new class. This is a bit + // circular. In order to generate some of the members, we need to know about the type. + // But in order to create the type, we need the members. To address this we do two + // passes. First, we create an empty version of the class. This can then be used to + // help create members like Equals/GetHashCode. Then, once we have all the members we + // create the final type. + var namedTypeWithoutMembers = CreateNamedType(typeName, isRecord: false, capturedTypeParameters, members: default); + + var constructor = CreateClassConstructor(semanticModel, typeName, properties, generator); + + // Generate Equals/GetHashCode. Only readonly properties are suitable for these + // methods. We can defer to our existing language service for this so that we + // generate the same Equals/GetHashCode that our other IDE features generate. + var readonlyProperties = ImmutableArray.CastUp( + properties.WhereAsArray(p => p.SetMethod == null)); - if (originalProperty.Name != newProperty.Name) - builder[originalProperty] = newProperty.Name; - } + var equalsAndGetHashCodeService = document.GetRequiredLanguageService(); - return (newProperties, builder.ToImmutable()); - } + var equalsMethod = await equalsAndGetHashCodeService.GenerateEqualsMethodAsync( + document, namedTypeWithoutMembers, readonlyProperties, + localNameOpt: SyntaxGeneratorExtensions.OtherName, cancellationToken).ConfigureAwait(false); + var getHashCodeMethod = await equalsAndGetHashCodeService.GenerateGetHashCodeMethodAsync( + document, namedTypeWithoutMembers, + readonlyProperties, cancellationToken).ConfigureAwait(false); + + members.AddRange(properties); + members.Add(constructor); + members.Add(equalsMethod); + members.Add(getHashCodeMethod); - private static IPropertySymbol GenerateProperty(Document document, IPropertySymbol prop) - { - // The actual properties generated by C#/VB are not what we want. For example, they - // think of themselves as having real accessors that will read/write into a real field - // in the type. Instead, we just want to generate auto-props. So we effectively clone - // the property, just throwing aways anything we don't need for that purpose. - // - // We also want to follow general .NET naming. So that means converting to pascal - // case from camel-case. - - var getMethod = prop.GetMethod != null ? CreateAccessorSymbol(prop, MethodKind.PropertyGet) : null; - var setMethod = prop.SetMethod != null ? CreateAccessorSymbol(prop, MethodKind.PropertySet) : null; - - return CodeGenerationSymbolFactory.CreatePropertySymbol( - attributes: default, Accessibility.Public, modifiers: default, - prop.Type, refKind: default, explicitInterfaceImplementations: default, - GetLegalName(prop.Name.ToPascalCase(trimLeadingTypePrefix: false), document), - parameters: default, getMethod: getMethod, setMethod: setMethod); } - private static string GetLegalName(string name, Document document) + return CreateNamedType(typeName, isRecord, capturedTypeParameters, members.ToImmutable()); + } + + private static INamedTypeSymbol CreateNamedType( + string className, bool isRecord, ImmutableArray capturedTypeParameters, ImmutableArray members) + { + return CodeGenerationSymbolFactory.CreateNamedTypeSymbol( + attributes: default, Accessibility.Internal, modifiers: default, + isRecord, TypeKind.Class, className, capturedTypeParameters, members: members); + } + + private static (ImmutableArray properties, ImmutableDictionary propertyMap) GenerateProperties( + Document document, INamedTypeSymbol anonymousType) + { + var originalProperties = anonymousType.GetMembers().OfType().ToImmutableArray(); + var newProperties = originalProperties.SelectAsArray(p => GenerateProperty(document, p)); + + // If we changed the names of any properties, record that name mapping. We'll + // use this to update reference to the old anonymous-type properties to the new + // names. + var builder = ImmutableDictionary.CreateBuilder(); + for (var i = 0; i < originalProperties.Length; i++) { - var syntaxFacts = document.GetRequiredLanguageService(); - return syntaxFacts.IsLegalIdentifier(name) - ? name - : "Item"; // Just a dummy name for the property. Does not need to be localized. + var originalProperty = originalProperties[i]; + var newProperty = newProperties[i]; + + if (originalProperty.Name != newProperty.Name) + builder[originalProperty] = newProperty.Name; } - private static IMethodSymbol CreateAccessorSymbol(IPropertySymbol prop, MethodKind kind) - => CodeGenerationSymbolFactory.CreateMethodSymbol( - attributes: default, Accessibility.Public, DeclarationModifiers.Abstract, - prop.Type, refKind: default, explicitInterfaceImplementations: default, - name: "", typeParameters: default, parameters: default, methodKind: kind); + return (newProperties, builder.ToImmutable()); + } + + private static IPropertySymbol GenerateProperty(Document document, IPropertySymbol prop) + { + // The actual properties generated by C#/VB are not what we want. For example, they + // think of themselves as having real accessors that will read/write into a real field + // in the type. Instead, we just want to generate auto-props. So we effectively clone + // the property, just throwing aways anything we don't need for that purpose. + // + // We also want to follow general .NET naming. So that means converting to pascal + // case from camel-case. + + var getMethod = prop.GetMethod != null ? CreateAccessorSymbol(prop, MethodKind.PropertyGet) : null; + var setMethod = prop.SetMethod != null ? CreateAccessorSymbol(prop, MethodKind.PropertySet) : null; + + return CodeGenerationSymbolFactory.CreatePropertySymbol( + attributes: default, Accessibility.Public, modifiers: default, + prop.Type, refKind: default, explicitInterfaceImplementations: default, + GetLegalName(prop.Name.ToPascalCase(trimLeadingTypePrefix: false), document), + parameters: default, getMethod: getMethod, setMethod: setMethod); + } - private static IMethodSymbol CreateClassConstructor( - SemanticModel semanticModel, string className, - ImmutableArray properties, SyntaxGenerator generator) + private static string GetLegalName(string name, Document document) + { + var syntaxFacts = document.GetRequiredLanguageService(); + return syntaxFacts.IsLegalIdentifier(name) + ? name + : "Item"; // Just a dummy name for the property. Does not need to be localized. + } + + private static IMethodSymbol CreateAccessorSymbol(IPropertySymbol prop, MethodKind kind) + => CodeGenerationSymbolFactory.CreateMethodSymbol( + attributes: default, Accessibility.Public, DeclarationModifiers.Abstract, + prop.Type, refKind: default, explicitInterfaceImplementations: default, + name: "", typeParameters: default, parameters: default, methodKind: kind); + + private static IMethodSymbol CreateClassConstructor( + SemanticModel semanticModel, string className, + ImmutableArray properties, SyntaxGenerator generator) + { + // For every property, create a corresponding parameter, as well as an assignment + // statement from that parameter to the property. + var parameterToPropMap = new Dictionary(); + var parameters = properties.SelectAsArray(prop => { - // For every property, create a corresponding parameter, as well as an assignment - // statement from that parameter to the property. - var parameterToPropMap = new Dictionary(); - var parameters = properties.SelectAsArray(prop => - { - var parameter = CodeGenerationSymbolFactory.CreateParameterSymbol( - prop.Type, prop.Name.ToCamelCase(trimLeadingTypePrefix: false)); + var parameter = CodeGenerationSymbolFactory.CreateParameterSymbol( + prop.Type, prop.Name.ToCamelCase(trimLeadingTypePrefix: false)); - parameterToPropMap[parameter.Name] = prop; + parameterToPropMap[parameter.Name] = prop; - return parameter; - }); + return parameter; + }); - var assignmentStatements = generator.CreateAssignmentStatements( - semanticModel, parameters, parameterToPropMap, ImmutableDictionary.Empty, - addNullChecks: false, preferThrowExpression: false); + var assignmentStatements = generator.CreateAssignmentStatements( + semanticModel, parameters, parameterToPropMap, ImmutableDictionary.Empty, + addNullChecks: false, preferThrowExpression: false); - var constructor = CodeGenerationSymbolFactory.CreateConstructorSymbol( - attributes: default, Accessibility.Public, modifiers: default, - className, parameters, assignmentStatements); + var constructor = CodeGenerationSymbolFactory.CreateConstructorSymbol( + attributes: default, Accessibility.Public, modifiers: default, + className, parameters, assignmentStatements); - return constructor; - } + return constructor; } } diff --git a/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeToTupleCodeRefactoringProvider.cs b/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeToTupleCodeRefactoringProvider.cs index edff9e5d50e5f..5e82cabdad3d8 100644 --- a/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeToTupleCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertAnonymousType/AbstractConvertAnonymousTypeToTupleCodeRefactoringProvider.cs @@ -16,119 +16,118 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.ConvertAnonymousType +namespace Microsoft.CodeAnalysis.ConvertAnonymousType; + +internal abstract class AbstractConvertAnonymousTypeToTupleCodeRefactoringProvider< + TExpressionSyntax, + TTupleExpressionSyntax, + TAnonymousObjectCreationExpressionSyntax> + : AbstractConvertAnonymousTypeCodeRefactoringProvider + where TExpressionSyntax : SyntaxNode + where TTupleExpressionSyntax : TExpressionSyntax + where TAnonymousObjectCreationExpressionSyntax : TExpressionSyntax { - internal abstract class AbstractConvertAnonymousTypeToTupleCodeRefactoringProvider< - TExpressionSyntax, - TTupleExpressionSyntax, - TAnonymousObjectCreationExpressionSyntax> - : AbstractConvertAnonymousTypeCodeRefactoringProvider - where TExpressionSyntax : SyntaxNode - where TTupleExpressionSyntax : TExpressionSyntax - where TAnonymousObjectCreationExpressionSyntax : TExpressionSyntax - { - protected abstract int GetInitializerCount(TAnonymousObjectCreationExpressionSyntax anonymousType); - protected abstract TTupleExpressionSyntax ConvertToTuple(TAnonymousObjectCreationExpressionSyntax anonCreation); + protected abstract int GetInitializerCount(TAnonymousObjectCreationExpressionSyntax anonymousType); + protected abstract TTupleExpressionSyntax ConvertToTuple(TAnonymousObjectCreationExpressionSyntax anonCreation); - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, span, cancellationToken) = context; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, span, cancellationToken) = context; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var (anonymousNode, anonymousType) = await TryGetAnonymousObjectAsync(document, span, cancellationToken).ConfigureAwait(false); - if (anonymousNode == null || anonymousType == null) - return; + var (anonymousNode, anonymousType) = await TryGetAnonymousObjectAsync(document, span, cancellationToken).ConfigureAwait(false); + if (anonymousNode == null || anonymousType == null) + return; - // Analysis is trivial. All anonymous types with more than two fields are marked as being - // convertible to a tuple. - if (GetInitializerCount(anonymousNode) < 2) - return; + // Analysis is trivial. All anonymous types with more than two fields are marked as being + // convertible to a tuple. + if (GetInitializerCount(anonymousNode) < 2) + return; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var allAnonymousNodes = GetAllAnonymousTypesInContainer(document, semanticModel, anonymousNode, cancellationToken); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var allAnonymousNodes = GetAllAnonymousTypesInContainer(document, semanticModel, anonymousNode, cancellationToken); - // If we have multiple different anonymous types in this member, then offer two fixes, one to just fixup this - // anonymous type, and one to fixup all anonymous types. - if (allAnonymousNodes.Any(t => !anonymousType.Equals(t.symbol, SymbolEqualityComparer.Default))) - { - context.RegisterRefactoring( - CodeAction.Create( - FeaturesResources.Convert_to_tuple, - [ - CodeAction.Create(FeaturesResources.just_this_anonymous_type, c => FixInCurrentMemberAsync(document, anonymousNode, anonymousType, allAnonymousTypes: false, c), nameof(FeaturesResources.just_this_anonymous_type)), - CodeAction.Create(FeaturesResources.all_anonymous_types_in_container, c => FixInCurrentMemberAsync(document, anonymousNode, anonymousType, allAnonymousTypes: true, c), nameof(FeaturesResources.all_anonymous_types_in_container)), - ], - isInlinable: false), - span); - } - else - { - // otherwise, just offer the change to the single tuple type. - context.RegisterRefactoring( - CodeAction.Create(FeaturesResources.Convert_to_tuple, c => FixInCurrentMemberAsync(document, anonymousNode, anonymousType, allAnonymousTypes: false, c), nameof(FeaturesResources.Convert_to_tuple)), - span); - } + // If we have multiple different anonymous types in this member, then offer two fixes, one to just fixup this + // anonymous type, and one to fixup all anonymous types. + if (allAnonymousNodes.Any(t => !anonymousType.Equals(t.symbol, SymbolEqualityComparer.Default))) + { + context.RegisterRefactoring( + CodeAction.Create( + FeaturesResources.Convert_to_tuple, + [ + CodeAction.Create(FeaturesResources.just_this_anonymous_type, c => FixInCurrentMemberAsync(document, anonymousNode, anonymousType, allAnonymousTypes: false, c), nameof(FeaturesResources.just_this_anonymous_type)), + CodeAction.Create(FeaturesResources.all_anonymous_types_in_container, c => FixInCurrentMemberAsync(document, anonymousNode, anonymousType, allAnonymousTypes: true, c), nameof(FeaturesResources.all_anonymous_types_in_container)), + ], + isInlinable: false), + span); } - - private IEnumerable<(TAnonymousObjectCreationExpressionSyntax node, INamedTypeSymbol symbol)> GetAllAnonymousTypesInContainer( - Document document, - SemanticModel semanticModel, - TAnonymousObjectCreationExpressionSyntax anonymousNode, - CancellationToken cancellationToken) + else { - // Now see if we have any other anonymous types (with a different shape) in the containing member. - // If so, offer to fix those up as well. - var syntaxFacts = document.GetRequiredLanguageService(); - var containingMember = anonymousNode.FirstAncestorOrSelf((node, syntaxFacts) => syntaxFacts.IsMethodLevelMember(node), syntaxFacts) ?? anonymousNode; + // otherwise, just offer the change to the single tuple type. + context.RegisterRefactoring( + CodeAction.Create(FeaturesResources.Convert_to_tuple, c => FixInCurrentMemberAsync(document, anonymousNode, anonymousType, allAnonymousTypes: false, c), nameof(FeaturesResources.Convert_to_tuple)), + span); + } + } + + private IEnumerable<(TAnonymousObjectCreationExpressionSyntax node, INamedTypeSymbol symbol)> GetAllAnonymousTypesInContainer( + Document document, + SemanticModel semanticModel, + TAnonymousObjectCreationExpressionSyntax anonymousNode, + CancellationToken cancellationToken) + { + // Now see if we have any other anonymous types (with a different shape) in the containing member. + // If so, offer to fix those up as well. + var syntaxFacts = document.GetRequiredLanguageService(); + var containingMember = anonymousNode.FirstAncestorOrSelf((node, syntaxFacts) => syntaxFacts.IsMethodLevelMember(node), syntaxFacts) ?? anonymousNode; - var childCreationNodes = containingMember.DescendantNodesAndSelf() - .OfType() - .Where(s => this.GetInitializerCount(s) >= 2); + var childCreationNodes = containingMember.DescendantNodesAndSelf() + .OfType() + .Where(s => this.GetInitializerCount(s) >= 2); - foreach (var childNode in childCreationNodes) - { - if (semanticModel.GetTypeInfo(childNode, cancellationToken).Type is INamedTypeSymbol childType) - yield return (childNode, childType); - } + foreach (var childNode in childCreationNodes) + { + if (semanticModel.GetTypeInfo(childNode, cancellationToken).Type is INamedTypeSymbol childType) + yield return (childNode, childType); } + } - private async Task FixInCurrentMemberAsync( - Document document, TAnonymousObjectCreationExpressionSyntax creationNode, INamedTypeSymbol anonymousType, bool allAnonymousTypes, CancellationToken cancellationToken) + private async Task FixInCurrentMemberAsync( + Document document, TAnonymousObjectCreationExpressionSyntax creationNode, INamedTypeSymbol anonymousType, bool allAnonymousTypes, CancellationToken cancellationToken) + { + // For the standard invocation of the code-fix, we want to fixup all creations of the + // "same" anonymous type within the containing method. We define same-ness as meaning + // "they have the type symbol". This means both have the same member names, in the same + // order, with the same member types. We fix all these up in the method because the + // user may be creating several instances of this anonymous type in that method and + // then combining them in interesting ways (i.e. checking them for equality, using them + // in collections, etc.). The language guarantees within a method boundary that these + // will be the same type and can be used together in this fashion. + + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var editor = new SyntaxEditor(root, SyntaxGenerator.GetGenerator(document)); + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var otherAnonymousNodes = GetAllAnonymousTypesInContainer(document, semanticModel, creationNode, cancellationToken); + + foreach (var (node, symbol) in otherAnonymousNodes) { - // For the standard invocation of the code-fix, we want to fixup all creations of the - // "same" anonymous type within the containing method. We define same-ness as meaning - // "they have the type symbol". This means both have the same member names, in the same - // order, with the same member types. We fix all these up in the method because the - // user may be creating several instances of this anonymous type in that method and - // then combining them in interesting ways (i.e. checking them for equality, using them - // in collections, etc.). The language guarantees within a method boundary that these - // will be the same type and can be used together in this fashion. - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var editor = new SyntaxEditor(root, SyntaxGenerator.GetGenerator(document)); - - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var otherAnonymousNodes = GetAllAnonymousTypesInContainer(document, semanticModel, creationNode, cancellationToken); - - foreach (var (node, symbol) in otherAnonymousNodes) - { - if (allAnonymousTypes || anonymousType.Equals(symbol, SymbolEqualityComparer.Default)) - ReplaceWithTuple(editor, node); - } - - return document.WithSyntaxRoot(editor.GetChangedRoot()); + if (allAnonymousTypes || anonymousType.Equals(symbol, SymbolEqualityComparer.Default)) + ReplaceWithTuple(editor, node); } - private void ReplaceWithTuple(SyntaxEditor editor, TAnonymousObjectCreationExpressionSyntax node) - => editor.ReplaceNode( - node, (current, _) => - { - // Use the callback form as anonymous types may be nested, and we want to - // properly replace them even in that case. - if (current is not TAnonymousObjectCreationExpressionSyntax anonCreation) - return current; - - return ConvertToTuple(anonCreation).WithAdditionalAnnotations(Formatter.Annotation); - }); + return document.WithSyntaxRoot(editor.GetChangedRoot()); } + + private void ReplaceWithTuple(SyntaxEditor editor, TAnonymousObjectCreationExpressionSyntax node) + => editor.ReplaceNode( + node, (current, _) => + { + // Use the callback form as anonymous types may be nested, and we want to + // properly replace them even in that case. + if (current is not TAnonymousObjectCreationExpressionSyntax anonCreation) + return current; + + return ConvertToTuple(anonCreation).WithAdditionalAnnotations(Formatter.Annotation); + }); } diff --git a/src/Features/Core/Portable/ConvertAutoPropertyToFullProperty/AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs b/src/Features/Core/Portable/ConvertAutoPropertyToFullProperty/AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs index a50fead8df87a..751f1711f1482 100644 --- a/src/Features/Core/Portable/ConvertAutoPropertyToFullProperty/AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertAutoPropertyToFullProperty/AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs @@ -15,111 +15,110 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ConvertAutoPropertyToFullProperty +namespace Microsoft.CodeAnalysis.ConvertAutoPropertyToFullProperty; + +internal abstract class AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider : CodeRefactoringProvider + where TPropertyDeclarationNode : SyntaxNode + where TTypeDeclarationNode : SyntaxNode + where TCodeGenerationContextInfo : CodeGenerationContextInfo { - internal abstract class AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider : CodeRefactoringProvider - where TPropertyDeclarationNode : SyntaxNode - where TTypeDeclarationNode : SyntaxNode - where TCodeGenerationContextInfo : CodeGenerationContextInfo + protected abstract Task GetFieldNameAsync(Document document, IPropertySymbol propertySymbol, NamingStylePreferencesProvider fallbackOptions, CancellationToken cancellationToken); + protected abstract (SyntaxNode newGetAccessor, SyntaxNode newSetAccessor) GetNewAccessors( + TCodeGenerationContextInfo info, SyntaxNode property, string fieldName, SyntaxGenerator generator, CancellationToken cancellationToken); + protected abstract SyntaxNode GetPropertyWithoutInitializer(SyntaxNode property); + protected abstract SyntaxNode GetInitializerValue(SyntaxNode property); + protected abstract SyntaxNode ConvertPropertyToExpressionBodyIfDesired(TCodeGenerationContextInfo info, SyntaxNode fullProperty); + protected abstract SyntaxNode GetTypeBlock(SyntaxNode syntaxNode); + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - protected abstract Task GetFieldNameAsync(Document document, IPropertySymbol propertySymbol, NamingStylePreferencesProvider fallbackOptions, CancellationToken cancellationToken); - protected abstract (SyntaxNode newGetAccessor, SyntaxNode newSetAccessor) GetNewAccessors( - TCodeGenerationContextInfo info, SyntaxNode property, string fieldName, SyntaxGenerator generator, CancellationToken cancellationToken); - protected abstract SyntaxNode GetPropertyWithoutInitializer(SyntaxNode property); - protected abstract SyntaxNode GetInitializerValue(SyntaxNode property); - protected abstract SyntaxNode ConvertPropertyToExpressionBodyIfDesired(TCodeGenerationContextInfo info, SyntaxNode fullProperty); - protected abstract SyntaxNode GetTypeBlock(SyntaxNode syntaxNode); - - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, _, cancellationToken) = context; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - var property = await GetPropertyAsync(context).ConfigureAwait(false); - if (property == null) - return; - - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (semanticModel.GetDeclaredSymbol(property) is not IPropertySymbol propertySymbol) - return; - - if (!IsValidAutoProperty(propertySymbol)) - return; - - context.RegisterRefactoring( - CodeAction.Create( - FeaturesResources.Convert_to_full_property, - c => ExpandToFullPropertyAsync(document, property, propertySymbol, root, context.Options, c), - nameof(FeaturesResources.Convert_to_full_property)), - property.Span); - } + var (document, _, cancellationToken) = context; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var property = await GetPropertyAsync(context).ConfigureAwait(false); + if (property == null) + return; + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (semanticModel.GetDeclaredSymbol(property) is not IPropertySymbol propertySymbol) + return; + + if (!IsValidAutoProperty(propertySymbol)) + return; + + context.RegisterRefactoring( + CodeAction.Create( + FeaturesResources.Convert_to_full_property, + c => ExpandToFullPropertyAsync(document, property, propertySymbol, root, context.Options, c), + nameof(FeaturesResources.Convert_to_full_property)), + property.Span); + } - internal static bool IsValidAutoProperty(IPropertySymbol propertySymbol) - { - var fields = propertySymbol.ContainingType.GetMembers().OfType(); - var field = fields.FirstOrDefault(f => propertySymbol.Equals(f.AssociatedSymbol)); - return field != null; - } + internal static bool IsValidAutoProperty(IPropertySymbol propertySymbol) + { + var fields = propertySymbol.ContainingType.GetMembers().OfType(); + var field = fields.FirstOrDefault(f => propertySymbol.Equals(f.AssociatedSymbol)); + return field != null; + } - private static async Task GetPropertyAsync(CodeRefactoringContext context) - { - var containingProperty = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (containingProperty?.Parent is not TTypeDeclarationNode) - return null; + private static async Task GetPropertyAsync(CodeRefactoringContext context) + { + var containingProperty = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (containingProperty?.Parent is not TTypeDeclarationNode) + return null; - return containingProperty; - } + return containingProperty; + } - private async Task ExpandToFullPropertyAsync( - Document document, - SyntaxNode property, - IPropertySymbol propertySymbol, - SyntaxNode root, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) + private async Task ExpandToFullPropertyAsync( + Document document, + SyntaxNode property, + IPropertySymbol propertySymbol, + SyntaxNode root, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + Contract.ThrowIfNull(document.DocumentState.ParseOptions); + + var editor = new SyntaxEditor(root, document.Project.Solution.Services); + var generator = editor.Generator; + var info = (TCodeGenerationContextInfo)await document.GetCodeGenerationInfoAsync(CodeGenerationContext.Default, fallbackOptions, cancellationToken).ConfigureAwait(false); + + // Create full property. If the auto property had an initial value + // we need to remove it and later add it to the backing field + var fieldName = await GetFieldNameAsync(document, propertySymbol, fallbackOptions, cancellationToken).ConfigureAwait(false); + var (newGetAccessor, newSetAccessor) = GetNewAccessors(info, property, fieldName, generator, cancellationToken); + var fullProperty = generator + .WithAccessorDeclarations( + GetPropertyWithoutInitializer(property), + newSetAccessor == null + ? [newGetAccessor] + : [newGetAccessor, newSetAccessor]) + .WithLeadingTrivia(property.GetLeadingTrivia()); + fullProperty = ConvertPropertyToExpressionBodyIfDesired(info, fullProperty); + + editor.ReplaceNode(property, fullProperty.WithAdditionalAnnotations(Formatter.Annotation)); + + // add backing field, plus initializer if it exists + var newField = CodeGenerationSymbolFactory.CreateFieldSymbol( + default, Accessibility.Private, + DeclarationModifiers.From(propertySymbol), + propertySymbol.Type, fieldName, + initializer: GetInitializerValue(property)); + + var typeDeclaration = propertySymbol.ContainingType.DeclaringSyntaxReferences; + foreach (var td in typeDeclaration) { - Contract.ThrowIfNull(document.DocumentState.ParseOptions); - - var editor = new SyntaxEditor(root, document.Project.Solution.Services); - var generator = editor.Generator; - var info = (TCodeGenerationContextInfo)await document.GetCodeGenerationInfoAsync(CodeGenerationContext.Default, fallbackOptions, cancellationToken).ConfigureAwait(false); - - // Create full property. If the auto property had an initial value - // we need to remove it and later add it to the backing field - var fieldName = await GetFieldNameAsync(document, propertySymbol, fallbackOptions, cancellationToken).ConfigureAwait(false); - var (newGetAccessor, newSetAccessor) = GetNewAccessors(info, property, fieldName, generator, cancellationToken); - var fullProperty = generator - .WithAccessorDeclarations( - GetPropertyWithoutInitializer(property), - newSetAccessor == null - ? [newGetAccessor] - : [newGetAccessor, newSetAccessor]) - .WithLeadingTrivia(property.GetLeadingTrivia()); - fullProperty = ConvertPropertyToExpressionBodyIfDesired(info, fullProperty); - - editor.ReplaceNode(property, fullProperty.WithAdditionalAnnotations(Formatter.Annotation)); - - // add backing field, plus initializer if it exists - var newField = CodeGenerationSymbolFactory.CreateFieldSymbol( - default, Accessibility.Private, - DeclarationModifiers.From(propertySymbol), - propertySymbol.Type, fieldName, - initializer: GetInitializerValue(property)); - - var typeDeclaration = propertySymbol.ContainingType.DeclaringSyntaxReferences; - foreach (var td in typeDeclaration) + var typeBlock = GetTypeBlock(await td.GetSyntaxAsync(cancellationToken).ConfigureAwait(false)); + if (property.Ancestors().Contains(typeBlock)) { - var typeBlock = GetTypeBlock(await td.GetSyntaxAsync(cancellationToken).ConfigureAwait(false)); - if (property.Ancestors().Contains(typeBlock)) - { - editor.ReplaceNode( - typeBlock, - (currentTypeDeclaration, _) => info.Service.AddField(currentTypeDeclaration, newField, info, cancellationToken)); - } + editor.ReplaceNode( + typeBlock, + (currentTypeDeclaration, _) => info.Service.AddField(currentTypeDeclaration, newField, info, cancellationToken)); } - - var newRoot = editor.GetChangedRoot(); - return document.WithSyntaxRoot(newRoot); } + + var newRoot = editor.GetChangedRoot(); + return document.WithSyntaxRoot(newRoot); } } diff --git a/src/Features/Core/Portable/ConvertCast/AbstractConvertCastCodeRefactoringProvider.cs b/src/Features/Core/Portable/ConvertCast/AbstractConvertCastCodeRefactoringProvider.cs index bf213bc843342..7dd64c5e45b3b 100644 --- a/src/Features/Core/Portable/ConvertCast/AbstractConvertCastCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertCast/AbstractConvertCastCodeRefactoringProvider.cs @@ -9,72 +9,71 @@ using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.ConvertCast +namespace Microsoft.CodeAnalysis.ConvertCast; + +/// +/// Refactor: +/// var o = (object)1; +/// +/// Into: +/// var o = 1 as object; +/// +/// Or vice versa. +/// +internal abstract class AbstractConvertCastCodeRefactoringProvider + : CodeRefactoringProvider + where TTypeNode : SyntaxNode + where TFromExpression : SyntaxNode + where TToExpression : SyntaxNode { - /// - /// Refactor: - /// var o = (object)1; - /// - /// Into: - /// var o = 1 as object; - /// - /// Or vice versa. - /// - internal abstract class AbstractConvertCastCodeRefactoringProvider - : CodeRefactoringProvider - where TTypeNode : SyntaxNode - where TFromExpression : SyntaxNode - where TToExpression : SyntaxNode - { - protected abstract string GetTitle(); + protected abstract string GetTitle(); - protected abstract int FromKind { get; } - protected abstract TToExpression ConvertExpression(TFromExpression from, NullableContext nullableContext, bool isReferenceType); - protected abstract TTypeNode GetTypeNode(TFromExpression from); + protected abstract int FromKind { get; } + protected abstract TToExpression ConvertExpression(TFromExpression from, NullableContext nullableContext, bool isReferenceType); + protected abstract TTypeNode GetTypeNode(TFromExpression from); - public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var fromNodes = await context.GetRelevantNodesAsync().ConfigureAwait(false); - var from = fromNodes.FirstOrDefault(n => n.RawKind == FromKind); - if (from == null) - return; + public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var fromNodes = await context.GetRelevantNodesAsync().ConfigureAwait(false); + var from = fromNodes.FirstOrDefault(n => n.RawKind == FromKind); + if (from == null) + return; - if (from.GetDiagnostics().Any(d => d.DefaultSeverity == DiagnosticSeverity.Error)) - return; + if (from.GetDiagnostics().Any(d => d.DefaultSeverity == DiagnosticSeverity.Error)) + return; - var (document, _, cancellationToken) = context; + var (document, _, cancellationToken) = context; - var typeNode = GetTypeNode(from); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var type = semanticModel.GetTypeInfo(typeNode, cancellationToken).Type; - var nullableContext = semanticModel.GetNullableContext(from.SpanStart); + var typeNode = GetTypeNode(from); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var type = semanticModel.GetTypeInfo(typeNode, cancellationToken).Type; + var nullableContext = semanticModel.GetNullableContext(from.SpanStart); - if (type is { TypeKind: TypeKind.Error }) - return; + if (type is { TypeKind: TypeKind.Error }) + return; - if (type is { IsReferenceType: true } or { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T }) - { - var title = GetTitle(); - var isReferenceType = type.IsReferenceType; - context.RegisterRefactoring( - CodeAction.Create( - title, - c => ConvertAsync(document, from, nullableContext, isReferenceType, cancellationToken), - title), - from.Span); - } - } - - private async Task ConvertAsync( - Document document, - TFromExpression from, - NullableContext nullableContext, - bool isReferenceType, - CancellationToken cancellationToken) + if (type is { IsReferenceType: true } or { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T }) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newRoot = root.ReplaceNode(from, ConvertExpression(from, nullableContext, isReferenceType)); - return document.WithSyntaxRoot(newRoot); + var title = GetTitle(); + var isReferenceType = type.IsReferenceType; + context.RegisterRefactoring( + CodeAction.Create( + title, + c => ConvertAsync(document, from, nullableContext, isReferenceType, cancellationToken), + title), + from.Span); } } + + private async Task ConvertAsync( + Document document, + TFromExpression from, + NullableContext nullableContext, + bool isReferenceType, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = root.ReplaceNode(from, ConvertExpression(from, nullableContext, isReferenceType)); + return document.WithSyntaxRoot(newRoot); + } } diff --git a/src/Features/Core/Portable/ConvertForEachToFor/AbstractConvertForEachToForCodeRefactoringProvider.cs b/src/Features/Core/Portable/ConvertForEachToFor/AbstractConvertForEachToForCodeRefactoringProvider.cs index 59110bee0a072..cb135a84b964a 100644 --- a/src/Features/Core/Portable/ConvertForEachToFor/AbstractConvertForEachToForCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertForEachToFor/AbstractConvertForEachToForCodeRefactoringProvider.cs @@ -20,211 +20,243 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ConvertForEachToFor +namespace Microsoft.CodeAnalysis.ConvertForEachToFor; + +internal abstract class AbstractConvertForEachToForCodeRefactoringProvider< + TStatementSyntax, + TForEachStatement> : CodeRefactoringProvider + where TStatementSyntax : SyntaxNode + where TForEachStatement : TStatementSyntax { - internal abstract class AbstractConvertForEachToForCodeRefactoringProvider< - TStatementSyntax, - TForEachStatement> : CodeRefactoringProvider - where TStatementSyntax : SyntaxNode - where TForEachStatement : TStatementSyntax + private const string get_Count = nameof(get_Count); + private const string get_Item = nameof(get_Item); + + private const string Length = nameof(Array.Length); + private const string Count = nameof(IList.Count); + + private static readonly ImmutableArray s_KnownInterfaceNames = + [typeof(IList<>).FullName!, typeof(IReadOnlyList<>).FullName!, typeof(IList).FullName!]; + + protected bool IsForEachVariableWrittenInside { get; private set; } + protected abstract string Title { get; } + protected abstract bool ValidLocation(ForEachInfo foreachInfo); + protected abstract (SyntaxNode start, SyntaxNode end) GetForEachBody(TForEachStatement foreachStatement); + protected abstract void ConvertToForStatement( + SemanticModel model, ForEachInfo info, SyntaxEditor editor, CancellationToken cancellationToken); + protected abstract bool IsValid(TForEachStatement foreachNode); + + /// + /// Perform language specific checks if the conversion is supported. + /// C#: Currently nothing blocking a conversion + /// VB: Nested foreach loops sharing a single Next statement, Next statements with multiple variables and next statements + /// not using the loop variable are not supported. + /// + protected abstract bool IsSupported(ILocalSymbol foreachVariable, IForEachLoopOperation forEachOperation, TForEachStatement foreachStatement); + + protected static SyntaxAnnotation CreateWarningAnnotation() + => WarningAnnotation.Create(FeaturesResources.Warning_colon_semantics_may_change_when_converting_statement); + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - private const string get_Count = nameof(get_Count); - private const string get_Item = nameof(get_Item); - - private const string Length = nameof(Array.Length); - private const string Count = nameof(IList.Count); - - private static readonly ImmutableArray s_KnownInterfaceNames = - [typeof(IList<>).FullName!, typeof(IReadOnlyList<>).FullName!, typeof(IList).FullName!]; - - protected bool IsForEachVariableWrittenInside { get; private set; } - protected abstract string Title { get; } - protected abstract bool ValidLocation(ForEachInfo foreachInfo); - protected abstract (SyntaxNode start, SyntaxNode end) GetForEachBody(TForEachStatement foreachStatement); - protected abstract void ConvertToForStatement( - SemanticModel model, ForEachInfo info, SyntaxEditor editor, CancellationToken cancellationToken); - protected abstract bool IsValid(TForEachStatement foreachNode); - - /// - /// Perform language specific checks if the conversion is supported. - /// C#: Currently nothing blocking a conversion - /// VB: Nested foreach loops sharing a single Next statement, Next statements with multiple variables and next statements - /// not using the loop variable are not supported. - /// - protected abstract bool IsSupported(ILocalSymbol foreachVariable, IForEachLoopOperation forEachOperation, TForEachStatement foreachStatement); - - protected static SyntaxAnnotation CreateWarningAnnotation() - => WarningAnnotation.Create(FeaturesResources.Warning_colon_semantics_may_change_when_converting_statement); - - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + var (document, _, cancellationToken) = context; + var foreachStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (foreachStatement == null || !IsValid(foreachStatement)) { - var (document, _, cancellationToken) = context; - var foreachStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (foreachStatement == null || !IsValid(foreachStatement)) - { - return; - } - - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + return; + } - var semanticFact = document.GetRequiredLanguageService(); - var foreachInfo = GetForeachInfo(semanticFact, model, foreachStatement, cancellationToken); - if (foreachInfo == null || !ValidLocation(foreachInfo)) - { - return; - } + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - context.RegisterRefactoring( - CodeAction.Create( - Title, - c => ConvertForeachToForAsync(document, foreachInfo, c), - Title), - foreachStatement.Span); + var semanticFact = document.GetRequiredLanguageService(); + var foreachInfo = GetForeachInfo(semanticFact, model, foreachStatement, cancellationToken); + if (foreachInfo == null || !ValidLocation(foreachInfo)) + { + return; } - protected static SyntaxToken CreateUniqueName( - ISemanticFactsService semanticFacts, SemanticModel model, SyntaxNode location, string baseName, CancellationToken cancellationToken) - => semanticFacts.GenerateUniqueLocalName(model, location, container: null, baseName, cancellationToken); + context.RegisterRefactoring( + CodeAction.Create( + Title, + c => ConvertForeachToForAsync(document, foreachInfo, c), + Title), + foreachStatement.Span); + } - protected static SyntaxNode GetCollectionVariableName( - SemanticModel model, SyntaxGenerator generator, - ForEachInfo foreachInfo, SyntaxNode foreachCollectionExpression, CancellationToken cancellationToken) - { - if (foreachInfo.RequireCollectionStatement) - { - return generator.IdentifierName( - CreateUniqueName(foreachInfo.SemanticFacts, - model, foreachInfo.ForEachStatement, foreachInfo.CollectionNameSuggestion, cancellationToken)); - } + protected static SyntaxToken CreateUniqueName( + ISemanticFactsService semanticFacts, SemanticModel model, SyntaxNode location, string baseName, CancellationToken cancellationToken) + => semanticFacts.GenerateUniqueLocalName(model, location, container: null, baseName, cancellationToken); - return foreachCollectionExpression.WithoutTrivia().WithAdditionalAnnotations(Formatter.Annotation); + protected static SyntaxNode GetCollectionVariableName( + SemanticModel model, SyntaxGenerator generator, + ForEachInfo foreachInfo, SyntaxNode foreachCollectionExpression, CancellationToken cancellationToken) + { + if (foreachInfo.RequireCollectionStatement) + { + return generator.IdentifierName( + CreateUniqueName(foreachInfo.SemanticFacts, + model, foreachInfo.ForEachStatement, foreachInfo.CollectionNameSuggestion, cancellationToken)); } - protected static void IntroduceCollectionStatement( - ForEachInfo foreachInfo, SyntaxEditor editor, - SyntaxNode type, SyntaxNode foreachCollectionExpression, SyntaxNode collectionVariable) + return foreachCollectionExpression.WithoutTrivia().WithAdditionalAnnotations(Formatter.Annotation); + } + + protected static void IntroduceCollectionStatement( + ForEachInfo foreachInfo, SyntaxEditor editor, + SyntaxNode type, SyntaxNode foreachCollectionExpression, SyntaxNode collectionVariable) + { + if (!foreachInfo.RequireCollectionStatement) { - if (!foreachInfo.RequireCollectionStatement) - { - return; - } + return; + } - // TODO: refactor introduce variable refactoring to real service and use that service here to introduce local variable - var generator = editor.Generator; + // TODO: refactor introduce variable refactoring to real service and use that service here to introduce local variable + var generator = editor.Generator; - // attach rename annotation to control variable - var collectionVariableToken = generator.Identifier(collectionVariable.ToString()).WithAdditionalAnnotations(RenameAnnotation.Create()); + // attach rename annotation to control variable + var collectionVariableToken = generator.Identifier(collectionVariable.ToString()).WithAdditionalAnnotations(RenameAnnotation.Create()); - // this expression is from user code. don't simplify this. - var expression = foreachCollectionExpression.WithoutAnnotations(SimplificationHelpers.DoNotSimplifyAnnotation); - var collectionStatement = generator.LocalDeclarationStatement( - type, - collectionVariableToken, - (foreachInfo.ExplicitCastInterface != null) ? generator.CastExpression(foreachInfo.ExplicitCastInterface, expression) : expression); + // this expression is from user code. don't simplify this. + var expression = foreachCollectionExpression.WithoutAnnotations(SimplificationHelpers.DoNotSimplifyAnnotation); + var collectionStatement = generator.LocalDeclarationStatement( + type, + collectionVariableToken, + (foreachInfo.ExplicitCastInterface != null) ? generator.CastExpression(foreachInfo.ExplicitCastInterface, expression) : expression); - // attach trivia to right place - collectionStatement = collectionStatement.WithLeadingTrivia(foreachInfo.ForEachStatement.GetFirstToken().LeadingTrivia); + // attach trivia to right place + collectionStatement = collectionStatement.WithLeadingTrivia(foreachInfo.ForEachStatement.GetFirstToken().LeadingTrivia); - editor.InsertBefore(foreachInfo.ForEachStatement, collectionStatement); - } + editor.InsertBefore(foreachInfo.ForEachStatement, collectionStatement); + } - protected static TStatementSyntax AddItemVariableDeclaration( - SyntaxGenerator generator, SyntaxNode type, SyntaxToken foreachVariable, - ITypeSymbol castType, SyntaxNode collectionVariable, SyntaxToken indexVariable) + protected static TStatementSyntax AddItemVariableDeclaration( + SyntaxGenerator generator, SyntaxNode type, SyntaxToken foreachVariable, + ITypeSymbol castType, SyntaxNode collectionVariable, SyntaxToken indexVariable) + { + var memberAccess = generator.ElementAccessExpression( + collectionVariable, generator.IdentifierName(indexVariable)); + + if (castType != null) { - var memberAccess = generator.ElementAccessExpression( - collectionVariable, generator.IdentifierName(indexVariable)); + memberAccess = generator.CastExpression(castType, memberAccess); + } - if (castType != null) - { - memberAccess = generator.CastExpression(castType, memberAccess); - } + var localDecl = generator.LocalDeclarationStatement( + type, foreachVariable, memberAccess); - var localDecl = generator.LocalDeclarationStatement( - type, foreachVariable, memberAccess); + return (TStatementSyntax)localDecl.WithAdditionalAnnotations(Formatter.Annotation); + } - return (TStatementSyntax)localDecl.WithAdditionalAnnotations(Formatter.Annotation); + private ForEachInfo? GetForeachInfo( + ISemanticFactsService semanticFact, SemanticModel model, + TForEachStatement foreachStatement, CancellationToken cancellationToken) + { + if (model.GetOperation(foreachStatement, cancellationToken) is not IForEachLoopOperation operation || operation.Locals.Length != 1) + { + return null; } - private ForEachInfo? GetForeachInfo( - ISemanticFactsService semanticFact, SemanticModel model, - TForEachStatement foreachStatement, CancellationToken cancellationToken) + var foreachVariable = operation.Locals[0]; + if (foreachVariable == null) { - if (model.GetOperation(foreachStatement, cancellationToken) is not IForEachLoopOperation operation || operation.Locals.Length != 1) - { - return null; - } + return null; + } - var foreachVariable = operation.Locals[0]; - if (foreachVariable == null) - { - return null; - } + // Perform language specific checks if the foreachStatement + // is using unsupported features + if (!IsSupported(foreachVariable, operation, foreachStatement)) + { + return null; + } - // Perform language specific checks if the foreachStatement - // is using unsupported features - if (!IsSupported(foreachVariable, operation, foreachStatement)) - { - return null; - } + IsForEachVariableWrittenInside = CheckIfForEachVariableIsWrittenInside(model, foreachVariable, foreachStatement); - IsForEachVariableWrittenInside = CheckIfForEachVariableIsWrittenInside(model, foreachVariable, foreachStatement); + var foreachCollection = RemoveImplicitConversion(operation.Collection); + if (foreachCollection == null) + { + return null; + } - var foreachCollection = RemoveImplicitConversion(operation.Collection); - if (foreachCollection == null) - { - return null; - } + GetInterfaceInfo(model, foreachVariable, foreachCollection, + out var explicitCastInterface, out var collectionNameSuggestion, out var countName); + if (collectionNameSuggestion == null || countName == null) + { + return null; + } - GetInterfaceInfo(model, foreachVariable, foreachCollection, - out var explicitCastInterface, out var collectionNameSuggestion, out var countName); - if (collectionNameSuggestion == null || countName == null) - { - return null; - } + var requireCollectionStatement = CheckRequireCollectionStatement(foreachCollection); + return new ForEachInfo( + semanticFact, collectionNameSuggestion, countName, explicitCastInterface, + foreachVariable.Type, requireCollectionStatement, foreachStatement); + } - var requireCollectionStatement = CheckRequireCollectionStatement(foreachCollection); - return new ForEachInfo( - semanticFact, collectionNameSuggestion, countName, explicitCastInterface, - foreachVariable.Type, requireCollectionStatement, foreachStatement); + private static void GetInterfaceInfo( + SemanticModel model, ILocalSymbol foreachVariable, IOperation foreachCollection, + out ITypeSymbol? explicitCastInterface, out string? collectionNameSuggestion, out string? countName) + { + explicitCastInterface = null; + collectionNameSuggestion = null; + countName = null; + + // go through list of types and interfaces to find out right set; + var foreachType = foreachVariable.Type; + if (IsNullOrErrorType(foreachType)) + { + return; } - private static void GetInterfaceInfo( - SemanticModel model, ILocalSymbol foreachVariable, IOperation foreachCollection, - out ITypeSymbol? explicitCastInterface, out string? collectionNameSuggestion, out string? countName) + var collectionType = foreachCollection.Type; + if (IsNullOrErrorType(collectionType)) { - explicitCastInterface = null; - collectionNameSuggestion = null; - countName = null; + return; + } + + // go through explicit types first. - // go through list of types and interfaces to find out right set; - var foreachType = foreachVariable.Type; - if (IsNullOrErrorType(foreachType)) + // check array case + if (collectionType is IArrayTypeSymbol array) + { + if (array.Rank != 1) { + // array type supports IList and other interfaces, but implementation + // only supports Rank == 1 case. other case, it will throw on runtime + // even if there is no error on compile time. + // we explicitly mark that we only support Rank == 1 case return; } - var collectionType = foreachCollection.Type; - if (IsNullOrErrorType(collectionType)) + if (!IsExchangable(array.ElementType, foreachType, model.Compilation)) { return; } - // go through explicit types first. + collectionNameSuggestion = "array"; + explicitCastInterface = null; + countName = Length; + return; + } - // check array case - if (collectionType is IArrayTypeSymbol array) + // check string case + if (collectionType.SpecialType == SpecialType.System_String) + { + var charType = model.Compilation.GetSpecialType(SpecialType.System_Char); + if (!IsExchangable(charType, foreachType, model.Compilation)) { - if (array.Rank != 1) - { - // array type supports IList and other interfaces, but implementation - // only supports Rank == 1 case. other case, it will throw on runtime - // even if there is no error on compile time. - // we explicitly mark that we only support Rank == 1 case - return; - } + return; + } + + collectionNameSuggestion = "str"; + explicitCastInterface = null; + countName = Length; + return; + } - if (!IsExchangable(array.ElementType, foreachType, model.Compilation)) + // check ImmutableArray case + if (collectionType.OriginalDefinition.Equals(model.Compilation.GetTypeByMetadataName(typeof(ImmutableArray<>).FullName!))) + { + var indexer = GetInterfaceMember(collectionType, get_Item); + if (indexer != null) + { + if (!IsExchangable(indexer.ReturnType, foreachType, model.Compilation)) { return; } @@ -234,210 +266,177 @@ private static void GetInterfaceInfo( countName = Length; return; } + } - // check string case - if (collectionType.SpecialType == SpecialType.System_String) - { - var charType = model.Compilation.GetSpecialType(SpecialType.System_Char); - if (!IsExchangable(charType, foreachType, model.Compilation)) - { - return; - } + // go through all known interfaces we support next. + var knownCollectionInterfaces = s_KnownInterfaceNames.Select( + model.Compilation.GetTypeByMetadataName).Where(t => !IsNullOrErrorType(t)); - collectionNameSuggestion = "str"; + // for all interfaces, we suggest collection name as "list" + collectionNameSuggestion = "list"; + + // check type itself is interface case + if (collectionType.TypeKind == TypeKind.Interface && knownCollectionInterfaces.Contains(collectionType.OriginalDefinition)) + { + var indexer = GetInterfaceMember(collectionType, get_Item); + if (indexer != null && + IsExchangable(indexer.ReturnType, foreachType, model.Compilation)) + { explicitCastInterface = null; - countName = Length; + countName = Count; return; } + } - // check ImmutableArray case - if (collectionType.OriginalDefinition.Equals(model.Compilation.GetTypeByMetadataName(typeof(ImmutableArray<>).FullName!))) + // check regular cases (implicitly implemented) + ITypeSymbol? explicitInterface = null; + foreach (var current in collectionType.AllInterfaces) + { + if (!knownCollectionInterfaces.Contains(current.OriginalDefinition)) { - var indexer = GetInterfaceMember(collectionType, get_Item); - if (indexer != null) - { - if (!IsExchangable(indexer.ReturnType, foreachType, model.Compilation)) - { - return; - } - - collectionNameSuggestion = "array"; - explicitCastInterface = null; - countName = Length; - return; - } + continue; } - // go through all known interfaces we support next. - var knownCollectionInterfaces = s_KnownInterfaceNames.Select( - model.Compilation.GetTypeByMetadataName).Where(t => !IsNullOrErrorType(t)); - - // for all interfaces, we suggest collection name as "list" - collectionNameSuggestion = "list"; - - // check type itself is interface case - if (collectionType.TypeKind == TypeKind.Interface && knownCollectionInterfaces.Contains(collectionType.OriginalDefinition)) + // see how the type implements the interface + var countSymbol = GetInterfaceMember(current, get_Count); + var indexerSymbol = GetInterfaceMember(current, get_Item); + if (countSymbol == null || indexerSymbol == null) { - var indexer = GetInterfaceMember(collectionType, get_Item); - if (indexer != null && - IsExchangable(indexer.ReturnType, foreachType, model.Compilation)) - { - explicitCastInterface = null; - countName = Count; - return; - } + continue; } - // check regular cases (implicitly implemented) - ITypeSymbol? explicitInterface = null; - foreach (var current in collectionType.AllInterfaces) + if (collectionType.FindImplementationForInterfaceMember(countSymbol) is not IMethodSymbol countImpl || + collectionType.FindImplementationForInterfaceMember(indexerSymbol) is not IMethodSymbol indexerImpl) { - if (!knownCollectionInterfaces.Contains(current.OriginalDefinition)) - { - continue; - } - - // see how the type implements the interface - var countSymbol = GetInterfaceMember(current, get_Count); - var indexerSymbol = GetInterfaceMember(current, get_Item); - if (countSymbol == null || indexerSymbol == null) - { - continue; - } - - if (collectionType.FindImplementationForInterfaceMember(countSymbol) is not IMethodSymbol countImpl || - collectionType.FindImplementationForInterfaceMember(indexerSymbol) is not IMethodSymbol indexerImpl) - { - continue; - } - - if (!IsExchangable(indexerImpl.ReturnType, foreachType, model.Compilation)) - { - continue; - } - - // implicitly implemented! - if (countImpl.ExplicitInterfaceImplementations.IsEmpty && - indexerImpl.ExplicitInterfaceImplementations.IsEmpty) - { - explicitCastInterface = null; - countName = Count; - return; - } + continue; + } - explicitInterface ??= current; + if (!IsExchangable(indexerImpl.ReturnType, foreachType, model.Compilation)) + { + continue; } - // okay, we don't have implicitly implemented one, but we do have explicitly implemented one - if (explicitInterface != null) + // implicitly implemented! + if (countImpl.ExplicitInterfaceImplementations.IsEmpty && + indexerImpl.ExplicitInterfaceImplementations.IsEmpty) { - explicitCastInterface = explicitInterface; + explicitCastInterface = null; countName = Count; + return; } + + explicitInterface ??= current; } - private static bool IsExchangable( - ITypeSymbol type1, ITypeSymbol type2, Compilation compilation) + // okay, we don't have implicitly implemented one, but we do have explicitly implemented one + if (explicitInterface != null) { - return compilation.HasImplicitConversion(type1, type2) || - compilation.HasImplicitConversion(type2, type1); + explicitCastInterface = explicitInterface; + countName = Count; } + } - private static bool IsNullOrErrorType([NotNullWhen(false)] ITypeSymbol? type) - => type is null or IErrorTypeSymbol; - - private static IMethodSymbol? GetInterfaceMember(ITypeSymbol interfaceType, string memberName) - { - foreach (var current in interfaceType.GetAllInterfacesIncludingThis()) - { - var members = current.GetMembers(memberName); - if (members is [IMethodSymbol method, ..]) - return method; - } + private static bool IsExchangable( + ITypeSymbol type1, ITypeSymbol type2, Compilation compilation) + { + return compilation.HasImplicitConversion(type1, type2) || + compilation.HasImplicitConversion(type2, type1); + } - return null; - } + private static bool IsNullOrErrorType([NotNullWhen(false)] ITypeSymbol? type) + => type is null or IErrorTypeSymbol; - private static bool CheckRequireCollectionStatement(IOperation operation) + private static IMethodSymbol? GetInterfaceMember(ITypeSymbol interfaceType, string memberName) + { + foreach (var current in interfaceType.GetAllInterfacesIncludingThis()) { - // this lists type of references in collection part of foreach we will use - // as it is in - // var element = reference[indexer]; - // - // otherwise, we will introduce local variable for the expression first and then - // do "foreach to for" refactoring - // - // foreach(var a in new int[] {....}) - // to - // var array = new int[] { ... } - // foreach(var a in array) - switch (operation.Kind) - { - case OperationKind.LocalReference: - case OperationKind.FieldReference: - case OperationKind.ParameterReference: - case OperationKind.PropertyReference: - case OperationKind.ArrayElementReference: - return false; - default: - return true; - } + var members = current.GetMembers(memberName); + if (members is [IMethodSymbol method, ..]) + return method; } - private static IOperation RemoveImplicitConversion(IOperation collection) - { - return (collection is IConversionOperation conversion && conversion.IsImplicit) - ? RemoveImplicitConversion(conversion.Operand) : collection; - } + return null; + } - private bool CheckIfForEachVariableIsWrittenInside(SemanticModel semanticModel, ISymbol foreachVariable, TForEachStatement foreachStatement) + private static bool CheckRequireCollectionStatement(IOperation operation) + { + // this lists type of references in collection part of foreach we will use + // as it is in + // var element = reference[indexer]; + // + // otherwise, we will introduce local variable for the expression first and then + // do "foreach to for" refactoring + // + // foreach(var a in new int[] {....}) + // to + // var array = new int[] { ... } + // foreach(var a in array) + switch (operation.Kind) { - var (start, end) = GetForEachBody(foreachStatement); - if (start == null || end == null) - { - // empty body. this can happen in VB + case OperationKind.LocalReference: + case OperationKind.FieldReference: + case OperationKind.ParameterReference: + case OperationKind.PropertyReference: + case OperationKind.ArrayElementReference: return false; - } - - var dataFlow = semanticModel.AnalyzeDataFlow(start, end); - - if (!dataFlow.Succeeded) - { - // if we can't get good analysis, assume it is written + default: return true; - } - - return dataFlow.WrittenInside.Contains(foreachVariable); } + } - private async Task ConvertForeachToForAsync( - Document document, - ForEachInfo foreachInfo, - CancellationToken cancellationToken) - { - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var services = document.Project.Solution.Services; - var editor = new SyntaxEditor(model.SyntaxTree.GetRoot(cancellationToken), services); - - ConvertToForStatement(model, foreachInfo, editor, cancellationToken); + private static IOperation RemoveImplicitConversion(IOperation collection) + { + return (collection is IConversionOperation conversion && conversion.IsImplicit) + ? RemoveImplicitConversion(conversion.Operand) : collection; + } - var newRoot = editor.GetChangedRoot(); - return document.WithSyntaxRoot(newRoot); + private bool CheckIfForEachVariableIsWrittenInside(SemanticModel semanticModel, ISymbol foreachVariable, TForEachStatement foreachStatement) + { + var (start, end) = GetForEachBody(foreachStatement); + if (start == null || end == null) + { + // empty body. this can happen in VB + return false; } - protected sealed class ForEachInfo( - ISemanticFactsService semanticFacts, string collectionNameSuggestion, string countName, - ITypeSymbol? explicitCastInterface, ITypeSymbol forEachElementType, - bool requireCollectionStatement, TForEachStatement forEachStatement) + var dataFlow = semanticModel.AnalyzeDataFlow(start, end); + + if (!dataFlow.Succeeded) { - public ISemanticFactsService SemanticFacts { get; } = semanticFacts; - - public string CollectionNameSuggestion { get; } = collectionNameSuggestion; - public string CountName { get; } = countName; - public ITypeSymbol? ExplicitCastInterface { get; } = explicitCastInterface; - public ITypeSymbol ForEachElementType { get; } = forEachElementType; - public bool RequireCollectionStatement { get; } = requireCollectionStatement || (explicitCastInterface != null); - public TForEachStatement ForEachStatement { get; } = forEachStatement; + // if we can't get good analysis, assume it is written + return true; } + + return dataFlow.WrittenInside.Contains(foreachVariable); + } + + private async Task ConvertForeachToForAsync( + Document document, + ForEachInfo foreachInfo, + CancellationToken cancellationToken) + { + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var services = document.Project.Solution.Services; + var editor = new SyntaxEditor(model.SyntaxTree.GetRoot(cancellationToken), services); + + ConvertToForStatement(model, foreachInfo, editor, cancellationToken); + + var newRoot = editor.GetChangedRoot(); + return document.WithSyntaxRoot(newRoot); + } + + protected sealed class ForEachInfo( + ISemanticFactsService semanticFacts, string collectionNameSuggestion, string countName, + ITypeSymbol? explicitCastInterface, ITypeSymbol forEachElementType, + bool requireCollectionStatement, TForEachStatement forEachStatement) + { + public ISemanticFactsService SemanticFacts { get; } = semanticFacts; + + public string CollectionNameSuggestion { get; } = collectionNameSuggestion; + public string CountName { get; } = countName; + public ITypeSymbol? ExplicitCastInterface { get; } = explicitCastInterface; + public ITypeSymbol ForEachElementType { get; } = forEachElementType; + public bool RequireCollectionStatement { get; } = requireCollectionStatement || (explicitCastInterface != null); + public TForEachStatement ForEachStatement { get; } = forEachStatement; } } diff --git a/src/Features/Core/Portable/ConvertForToForEach/AbstractConvertForToForEachCodeRefactoringProvider.cs b/src/Features/Core/Portable/ConvertForToForEach/AbstractConvertForToForEachCodeRefactoringProvider.cs index 21b90fba9bc79..2f01d31dcacb6 100644 --- a/src/Features/Core/Portable/ConvertForToForEach/AbstractConvertForToForEachCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertForToForEach/AbstractConvertForToForEachCodeRefactoringProvider.cs @@ -17,520 +17,519 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ConvertForToForEach +namespace Microsoft.CodeAnalysis.ConvertForToForEach; + +internal abstract class AbstractConvertForToForEachCodeRefactoringProvider< + TStatementSyntax, + TForStatementSyntax, + TExpressionSyntax, + TMemberAccessExpressionSyntax, + TTypeNode, + TVariableDeclaratorSyntax> : CodeRefactoringProvider + where TStatementSyntax : SyntaxNode + where TForStatementSyntax : TStatementSyntax + where TExpressionSyntax : SyntaxNode + where TMemberAccessExpressionSyntax : SyntaxNode + where TTypeNode : SyntaxNode + where TVariableDeclaratorSyntax : SyntaxNode { - internal abstract class AbstractConvertForToForEachCodeRefactoringProvider< - TStatementSyntax, - TForStatementSyntax, - TExpressionSyntax, - TMemberAccessExpressionSyntax, - TTypeNode, - TVariableDeclaratorSyntax> : CodeRefactoringProvider - where TStatementSyntax : SyntaxNode - where TForStatementSyntax : TStatementSyntax - where TExpressionSyntax : SyntaxNode - where TMemberAccessExpressionSyntax : SyntaxNode - where TTypeNode : SyntaxNode - where TVariableDeclaratorSyntax : SyntaxNode - { - protected abstract string GetTitle(); + protected abstract string GetTitle(); + + protected abstract SyntaxList GetBodyStatements(TForStatementSyntax forStatement); + protected abstract bool IsValidVariableDeclarator(TVariableDeclaratorSyntax firstVariable); - protected abstract SyntaxList GetBodyStatements(TForStatementSyntax forStatement); - protected abstract bool IsValidVariableDeclarator(TVariableDeclaratorSyntax firstVariable); + protected abstract bool TryGetForStatementComponents( + TForStatementSyntax forStatement, + out SyntaxToken iterationVariable, + [NotNullWhen(true)] out TExpressionSyntax? initializer, + [NotNullWhen(true)] out TMemberAccessExpressionSyntax? memberAccess, + out TExpressionSyntax? stepValueExpressionOpt, + CancellationToken cancellationToken); - protected abstract bool TryGetForStatementComponents( - TForStatementSyntax forStatement, - out SyntaxToken iterationVariable, - [NotNullWhen(true)] out TExpressionSyntax? initializer, - [NotNullWhen(true)] out TMemberAccessExpressionSyntax? memberAccess, - out TExpressionSyntax? stepValueExpressionOpt, - CancellationToken cancellationToken); + protected abstract SyntaxNode ConvertForNode( + TForStatementSyntax currentFor, TTypeNode? typeNode, SyntaxToken foreachIdentifier, + TExpressionSyntax collectionExpression, ITypeSymbol iterationVariableType); - protected abstract SyntaxNode ConvertForNode( - TForStatementSyntax currentFor, TTypeNode? typeNode, SyntaxToken foreachIdentifier, - TExpressionSyntax collectionExpression, ITypeSymbol iterationVariableType); + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, textSpan, cancellationToken) = context; + var forStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (forStatement == null) + return; - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + if (!TryGetForStatementComponents(forStatement, + out var iterationVariable, out var initializer, out var memberAccess, out var stepValueExpressionOpt, cancellationToken)) { - var (document, textSpan, cancellationToken) = context; - var forStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (forStatement == null) - return; + return; + } - if (!TryGetForStatementComponents(forStatement, - out var iterationVariable, out var initializer, out var memberAccess, out var stepValueExpressionOpt, cancellationToken)) - { - return; - } + var syntaxFacts = document.GetRequiredLanguageService(); + syntaxFacts.GetPartsOfMemberAccessExpression(memberAccess, + out var collectionExpressionNode, out var memberAccessNameNode); - var syntaxFacts = document.GetRequiredLanguageService(); - syntaxFacts.GetPartsOfMemberAccessExpression(memberAccess, - out var collectionExpressionNode, out var memberAccessNameNode); + var collectionExpression = (TExpressionSyntax)collectionExpressionNode; + syntaxFacts.GetNameAndArityOfSimpleName(memberAccessNameNode, out var memberAccessName, out _); + if (memberAccessName is not nameof(Array.Length) and not nameof(IList.Count)) + return; - var collectionExpression = (TExpressionSyntax)collectionExpressionNode; - syntaxFacts.GetNameAndArityOfSimpleName(memberAccessNameNode, out var memberAccessName, out _); - if (memberAccessName is not nameof(Array.Length) and not nameof(IList.Count)) - return; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + // Make sure it's a single-variable for loop and that we're not a loop where we're + // referencing some previously declared symbol. i.e + // VB allows: + // + // dim i as integer + // for i = 0 to ... + // + // We can't convert this as it would change important semantics. + // NOTE: we could potentially update this if we saw that the variable was not used + // after the for-loop. But, for now, we'll just be conservative and assume this means + // the user wanted the 'i' for some other purpose and we should keep things as is. + if (semanticModel.GetOperation(forStatement, cancellationToken) is not ILoopOperation { Locals.Length: 1 }) + return; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - // Make sure it's a single-variable for loop and that we're not a loop where we're - // referencing some previously declared symbol. i.e - // VB allows: - // - // dim i as integer - // for i = 0 to ... - // - // We can't convert this as it would change important semantics. - // NOTE: we could potentially update this if we saw that the variable was not used - // after the for-loop. But, for now, we'll just be conservative and assume this means - // the user wanted the 'i' for some other purpose and we should keep things as is. - if (semanticModel.GetOperation(forStatement, cancellationToken) is not ILoopOperation { Locals.Length: 1 }) - return; + // Make sure we're starting at 0. + var initializerValue = semanticModel.GetConstantValue(initializer, cancellationToken); + if (initializerValue is not { HasValue: true, Value: 0 }) + return; - // Make sure we're starting at 0. - var initializerValue = semanticModel.GetConstantValue(initializer, cancellationToken); - if (initializerValue is not { HasValue: true, Value: 0 }) + // Make sure we're incrementing by 1. + if (stepValueExpressionOpt != null) + { + var stepValue = semanticModel.GetConstantValue(stepValueExpressionOpt); + if (stepValue is not { HasValue: true, Value: 1 }) return; + } - // Make sure we're incrementing by 1. - if (stepValueExpressionOpt != null) - { - var stepValue = semanticModel.GetConstantValue(stepValueExpressionOpt); - if (stepValue is not { HasValue: true, Value: 1 }) - return; - } + var collectionType = semanticModel.GetTypeInfo(collectionExpression, cancellationToken); + if (collectionType.Type is null or IErrorTypeSymbol) + return; - var collectionType = semanticModel.GetTypeInfo(collectionExpression, cancellationToken); - if (collectionType.Type is null or IErrorTypeSymbol) - return; + var containingType = semanticModel.GetEnclosingNamedType(textSpan.Start, cancellationToken); + if (containingType == null) + return; - var containingType = semanticModel.GetEnclosingNamedType(textSpan.Start, cancellationToken); - if (containingType == null) - return; + var ienumerableType = semanticModel.Compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T); + var ienumeratorType = semanticModel.Compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerator_T); - var ienumerableType = semanticModel.Compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T); - var ienumeratorType = semanticModel.Compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerator_T); + // make sure the collection can be iterated. + if (!TryGetIterationElementType( + containingType, collectionType.Type, + ienumerableType, ienumeratorType, + out var iterationType)) + { + return; + } - // make sure the collection can be iterated. - if (!TryGetIterationElementType( - containingType, collectionType.Type, - ienumerableType, ienumeratorType, - out var iterationType)) - { + // If the user uses the iteration variable for any other reason, we can't convert this. + var bodyStatements = GetBodyStatements(forStatement); + foreach (var statement in bodyStatements) + { + if (IterationVariableIsUsedForMoreThanCollectionIndex(statement)) return; - } - - // If the user uses the iteration variable for any other reason, we can't convert this. - var bodyStatements = GetBodyStatements(forStatement); - foreach (var statement in bodyStatements) - { - if (IterationVariableIsUsedForMoreThanCollectionIndex(statement)) - return; - } - - // Looks good. We can convert this. - var title = GetTitle(); - context.RegisterRefactoring( - CodeAction.Create( - title, - cancellationToken => ConvertForToForEachAsync( - document, forStatement, iterationVariable, collectionExpression, - containingType, collectionType.Type, iterationType, cancellationToken), - title), - forStatement.Span); - - return; + } - // local functions - bool IterationVariableIsUsedForMoreThanCollectionIndex(SyntaxNode current) + // Looks good. We can convert this. + var title = GetTitle(); + context.RegisterRefactoring( + CodeAction.Create( + title, + cancellationToken => ConvertForToForEachAsync( + document, forStatement, iterationVariable, collectionExpression, + containingType, collectionType.Type, iterationType, cancellationToken), + title), + forStatement.Span); + + return; + + // local functions + bool IterationVariableIsUsedForMoreThanCollectionIndex(SyntaxNode current) + { + if (syntaxFacts.IsIdentifierName(current)) { - if (syntaxFacts.IsIdentifierName(current)) + syntaxFacts.GetNameAndArityOfSimpleName(current, out var name, out _); + if (name == iterationVariable.ValueText) { - syntaxFacts.GetNameAndArityOfSimpleName(current, out var name, out _); - if (name == iterationVariable.ValueText) - { - // found a reference. make sure it's only used inside something like - // list[i] + // found a reference. make sure it's only used inside something like + // list[i] - var argument = current.Parent; - if (!syntaxFacts.IsSimpleArgument(argument)) - return true; - - // we support `list[i]` or `list.ElementAt(i)` - var argumentList = argument?.Parent; - if (argumentList is null) - return true; - - var arguments = syntaxFacts.GetArgumentsOfArgumentList(argumentList); - // was used in a multi-dimensional indexing, or multiple argument method call. Can't convert this. - if (arguments.Count != 1) - return true; + var argument = current.Parent; + if (!syntaxFacts.IsSimpleArgument(argument)) + return true; - if (!IsGoodElementAccessExpression(argumentList) && - !IsGoodInvocationExpression(argumentList)) - { - // used in something other than accessing into a collection. - // can't convert this for-loop. - return true; - } - } + // we support `list[i]` or `list.ElementAt(i)` + var argumentList = argument?.Parent; + if (argumentList is null) + return true; - // this usage of the for-variable is fine. - } + var arguments = syntaxFacts.GetArgumentsOfArgumentList(argumentList); + // was used in a multi-dimensional indexing, or multiple argument method call. Can't convert this. + if (arguments.Count != 1) + return true; - foreach (var child in current.ChildNodesAndTokens()) - { - if (child.IsNode) + if (!IsGoodElementAccessExpression(argumentList) && + !IsGoodInvocationExpression(argumentList)) { - if (IterationVariableIsUsedForMoreThanCollectionIndex(child.AsNode()!)) - return true; + // used in something other than accessing into a collection. + // can't convert this for-loop. + return true; } } - return false; + // this usage of the for-variable is fine. } - bool IsGoodElementAccessExpression(SyntaxNode argumentList) + foreach (var child in current.ChildNodesAndTokens()) { - if (syntaxFacts.IsElementAccessExpression(argumentList.Parent)) + if (child.IsNode) { - var expr = syntaxFacts.GetExpressionOfElementAccessExpression(argumentList.Parent); - - // Have to be indexing into the collection. - if (syntaxFacts.AreEquivalent(expr, collectionExpression)) + if (IterationVariableIsUsedForMoreThanCollectionIndex(child.AsNode()!)) return true; } + } - return false; + return false; + } + + bool IsGoodElementAccessExpression(SyntaxNode argumentList) + { + if (syntaxFacts.IsElementAccessExpression(argumentList.Parent)) + { + var expr = syntaxFacts.GetExpressionOfElementAccessExpression(argumentList.Parent); + + // Have to be indexing into the collection. + if (syntaxFacts.AreEquivalent(expr, collectionExpression)) + return true; } - bool IsGoodInvocationExpression(SyntaxNode argumentList) + return false; + } + + bool IsGoodInvocationExpression(SyntaxNode argumentList) + { + if (syntaxFacts.IsInvocationExpression(argumentList.Parent)) { - if (syntaxFacts.IsInvocationExpression(argumentList.Parent)) + var invokedExpression = syntaxFacts.GetExpressionOfInvocationExpression(argumentList.Parent); + if (syntaxFacts.IsMemberAccessExpression(invokedExpression)) { - var invokedExpression = syntaxFacts.GetExpressionOfInvocationExpression(argumentList.Parent); - if (syntaxFacts.IsMemberAccessExpression(invokedExpression)) - { - syntaxFacts.GetPartsOfMemberAccessExpression(invokedExpression, out var accessedExpression, out var accessedName); - syntaxFacts.GetNameAndArityOfSimpleName(accessedName, out var memberName, out _); + syntaxFacts.GetPartsOfMemberAccessExpression(invokedExpression, out var accessedExpression, out var accessedName); + syntaxFacts.GetNameAndArityOfSimpleName(accessedName, out var memberName, out _); - // Have to be indexing into the collection. - if (memberName == nameof(Enumerable.ElementAt) && - syntaxFacts.AreEquivalent(accessedExpression, collectionExpression)) - { - return true; - } + // Have to be indexing into the collection. + if (memberName == nameof(Enumerable.ElementAt) && + syntaxFacts.AreEquivalent(accessedExpression, collectionExpression)) + { + return true; } } - - return false; } - } - private static IEnumerable TryFindMembersInThisOrBaseTypes( - INamedTypeSymbol containingType, ITypeSymbol type, string memberName) where TSymbol : class, ISymbol - { - var methods = type.GetAccessibleMembersInThisAndBaseTypes(containingType); - return methods.Where(m => m.Name == memberName); + return false; } + } - private static TSymbol? TryFindMemberInThisOrBaseTypes( - INamedTypeSymbol containingType, ITypeSymbol type, string memberName) where TSymbol : class, ISymbol + private static IEnumerable TryFindMembersInThisOrBaseTypes( + INamedTypeSymbol containingType, ITypeSymbol type, string memberName) where TSymbol : class, ISymbol + { + var methods = type.GetAccessibleMembersInThisAndBaseTypes(containingType); + return methods.Where(m => m.Name == memberName); + } + + private static TSymbol? TryFindMemberInThisOrBaseTypes( + INamedTypeSymbol containingType, ITypeSymbol type, string memberName) where TSymbol : class, ISymbol + { + return TryFindMembersInThisOrBaseTypes(containingType, type, memberName).FirstOrDefault(); + } + + private static bool TryGetIterationElementType( + INamedTypeSymbol containingType, ITypeSymbol collectionType, + INamedTypeSymbol ienumerableType, INamedTypeSymbol ienumeratorType, + [NotNullWhen(true)] out ITypeSymbol? iterationType) + { + if (collectionType is IArrayTypeSymbol arrayType) { - return TryFindMembersInThisOrBaseTypes(containingType, type, memberName).FirstOrDefault(); + iterationType = arrayType.ElementType; + + // We only support single-dimensional array iteration. + return arrayType.Rank == 1; } - private static bool TryGetIterationElementType( - INamedTypeSymbol containingType, ITypeSymbol collectionType, - INamedTypeSymbol ienumerableType, INamedTypeSymbol ienumeratorType, - [NotNullWhen(true)] out ITypeSymbol? iterationType) + // Check in the class/struct hierarchy first. + var getEnumeratorMethod = TryFindMemberInThisOrBaseTypes( + containingType, collectionType, WellKnownMemberNames.GetEnumeratorMethodName); + if (getEnumeratorMethod != null) { - if (collectionType is IArrayTypeSymbol arrayType) - { - iterationType = arrayType.ElementType; + return TryGetIterationElementTypeFromGetEnumerator( + containingType, getEnumeratorMethod, ienumeratorType, out iterationType); + } - // We only support single-dimensional array iteration. - return arrayType.Rank == 1; - } + // couldn't find .GetEnumerator on the class/struct. Check the interface hierarchy. + var instantiatedIEnumerableType = collectionType.GetAllInterfacesIncludingThis().FirstOrDefault( + t => Equals(t.OriginalDefinition, ienumerableType)); - // Check in the class/struct hierarchy first. - var getEnumeratorMethod = TryFindMemberInThisOrBaseTypes( - containingType, collectionType, WellKnownMemberNames.GetEnumeratorMethodName); - if (getEnumeratorMethod != null) - { - return TryGetIterationElementTypeFromGetEnumerator( - containingType, getEnumeratorMethod, ienumeratorType, out iterationType); - } + if (instantiatedIEnumerableType != null) + { + iterationType = instantiatedIEnumerableType.TypeArguments[0]; + return true; + } - // couldn't find .GetEnumerator on the class/struct. Check the interface hierarchy. - var instantiatedIEnumerableType = collectionType.GetAllInterfacesIncludingThis().FirstOrDefault( - t => Equals(t.OriginalDefinition, ienumerableType)); + iterationType = null; + return false; + } - if (instantiatedIEnumerableType != null) - { - iterationType = instantiatedIEnumerableType.TypeArguments[0]; - return true; - } + private static bool TryGetIterationElementTypeFromGetEnumerator( + INamedTypeSymbol containingType, IMethodSymbol getEnumeratorMethod, + INamedTypeSymbol ienumeratorType, [NotNullWhen(true)] out ITypeSymbol? iterationType) + { + var getEnumeratorReturnType = getEnumeratorMethod.ReturnType; - iterationType = null; - return false; + // Check in the class/struct hierarchy first. + var currentProperty = TryFindMemberInThisOrBaseTypes( + containingType, getEnumeratorReturnType, WellKnownMemberNames.CurrentPropertyName); + if (currentProperty != null) + { + iterationType = currentProperty.Type; + return true; } - private static bool TryGetIterationElementTypeFromGetEnumerator( - INamedTypeSymbol containingType, IMethodSymbol getEnumeratorMethod, - INamedTypeSymbol ienumeratorType, [NotNullWhen(true)] out ITypeSymbol? iterationType) - { - var getEnumeratorReturnType = getEnumeratorMethod.ReturnType; + // couldn't find .Current on the class/struct. Check the interface hierarchy. + var instantiatedIEnumeratorType = getEnumeratorReturnType.GetAllInterfacesIncludingThis().FirstOrDefault( + t => Equals(t.OriginalDefinition, ienumeratorType)); - // Check in the class/struct hierarchy first. - var currentProperty = TryFindMemberInThisOrBaseTypes( - containingType, getEnumeratorReturnType, WellKnownMemberNames.CurrentPropertyName); - if (currentProperty != null) - { - iterationType = currentProperty.Type; - return true; - } + if (instantiatedIEnumeratorType != null) + { + iterationType = instantiatedIEnumeratorType.TypeArguments[0]; + return true; + } - // couldn't find .Current on the class/struct. Check the interface hierarchy. - var instantiatedIEnumeratorType = getEnumeratorReturnType.GetAllInterfacesIncludingThis().FirstOrDefault( - t => Equals(t.OriginalDefinition, ienumeratorType)); + iterationType = null; + return false; + } - if (instantiatedIEnumeratorType != null) + private async Task ConvertForToForEachAsync( + Document document, + TForStatementSyntax forStatement, + SyntaxToken iterationVariable, + TExpressionSyntax collectionExpression, + INamedTypeSymbol containingType, + ITypeSymbol collectionType, + ITypeSymbol iterationType, + CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var semanticFacts = document.GetRequiredLanguageService(); + var generator = SyntaxGenerator.GetGenerator(document); + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var editor = new SyntaxEditor(root, generator); + + // create dummy "list[i]" and "list.ElementAt(i)" expressions. We'll use this to find all places to replace + // in the current for statement. + var indexExpression = generator.ElementAccessExpression(collectionExpression, generator.IdentifierName(iterationVariable)); + var elementAtExpression = generator.InvocationExpression( + generator.MemberAccessExpression(collectionExpression, generator.IdentifierName(nameof(Enumerable.ElementAt))), + generator.IdentifierName(iterationVariable)); + + // See if the first statement in the for loop is of the form: + // var x = list[i] or + // + // If so, we'll use those as the iteration variables for the new foreach statement. + var (typeNode, foreachIdentifier, declarationStatement) = TryDeconstructInitialDeclaration(); + + if (typeNode == null) + { + // user didn't provide an explicit type. Check if the index-type of the collection + // is different from than .Current type of the enumerator. If so, add an explicit + // type so that the foreach will coerce the types accordingly. + var indexerType = GetIndexerType(containingType, collectionType, semanticModel.Compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T)); + if (!Equals(indexerType, iterationType)) { - iterationType = instantiatedIEnumeratorType.TypeArguments[0]; - return true; + typeNode = (TTypeNode)generator.TypeExpression( + indexerType ?? semanticModel.Compilation.GetSpecialType(SpecialType.System_Object)); } - - iterationType = null; - return false; } - private async Task ConvertForToForEachAsync( - Document document, - TForStatementSyntax forStatement, - SyntaxToken iterationVariable, - TExpressionSyntax collectionExpression, - INamedTypeSymbol containingType, - ITypeSymbol collectionType, - ITypeSymbol iterationType, - CancellationToken cancellationToken) + // If we couldn't find an appropriate existing variable to use as the foreach + // variable, then generate one automatically. + if (foreachIdentifier.RawKind == 0) { - var syntaxFacts = document.GetRequiredLanguageService(); - var semanticFacts = document.GetRequiredLanguageService(); - var generator = SyntaxGenerator.GetGenerator(document); - - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var editor = new SyntaxEditor(root, generator); - - // create dummy "list[i]" and "list.ElementAt(i)" expressions. We'll use this to find all places to replace - // in the current for statement. - var indexExpression = generator.ElementAccessExpression(collectionExpression, generator.IdentifierName(iterationVariable)); - var elementAtExpression = generator.InvocationExpression( - generator.MemberAccessExpression(collectionExpression, generator.IdentifierName(nameof(Enumerable.ElementAt))), - generator.IdentifierName(iterationVariable)); - - // See if the first statement in the for loop is of the form: - // var x = list[i] or - // - // If so, we'll use those as the iteration variables for the new foreach statement. - var (typeNode, foreachIdentifier, declarationStatement) = TryDeconstructInitialDeclaration(); - - if (typeNode == null) - { - // user didn't provide an explicit type. Check if the index-type of the collection - // is different from than .Current type of the enumerator. If so, add an explicit - // type so that the foreach will coerce the types accordingly. - var indexerType = GetIndexerType(containingType, collectionType, semanticModel.Compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T)); - if (!Equals(indexerType, iterationType)) - { - typeNode = (TTypeNode)generator.TypeExpression( - indexerType ?? semanticModel.Compilation.GetSpecialType(SpecialType.System_Object)); - } - } + foreachIdentifier = semanticFacts.GenerateUniqueName( + semanticModel, forStatement, container: null, baseName: "v", usedNames: [], cancellationToken); + foreachIdentifier = foreachIdentifier.WithAdditionalAnnotations(RenameAnnotation.Create()); + } - // If we couldn't find an appropriate existing variable to use as the foreach - // variable, then generate one automatically. - if (foreachIdentifier.RawKind == 0) - { - foreachIdentifier = semanticFacts.GenerateUniqueName( - semanticModel, forStatement, container: null, baseName: "v", usedNames: [], cancellationToken); - foreachIdentifier = foreachIdentifier.WithAdditionalAnnotations(RenameAnnotation.Create()); - } + // Create the expression we'll use to replace all matches in the for-body. + var foreachIdentifierReference = foreachIdentifier.WithoutAnnotations(RenameAnnotation.Kind).WithoutTrivia(); - // Create the expression we'll use to replace all matches in the for-body. - var foreachIdentifierReference = foreachIdentifier.WithoutAnnotations(RenameAnnotation.Kind).WithoutTrivia(); + // Walk the for statement, replacing any matches we find. + FindAndReplaceMatches(forStatement); - // Walk the for statement, replacing any matches we find. - FindAndReplaceMatches(forStatement); + // Finally, remove the declaration statement if we found one. Move all its leading + // trivia to the next statement. + if (declarationStatement != null) + { + editor.RemoveNode(declarationStatement, + SyntaxGenerator.DefaultRemoveOptions | SyntaxRemoveOptions.KeepLeadingTrivia); + } - // Finally, remove the declaration statement if we found one. Move all its leading - // trivia to the next statement. - if (declarationStatement != null) - { - editor.RemoveNode(declarationStatement, - SyntaxGenerator.DefaultRemoveOptions | SyntaxRemoveOptions.KeepLeadingTrivia); - } + editor.ReplaceNode( + forStatement, + (currentFor, _) => ConvertForNode( + (TForStatementSyntax)currentFor, typeNode, foreachIdentifier, + collectionExpression, iterationType)); - editor.ReplaceNode( - forStatement, - (currentFor, _) => ConvertForNode( - (TForStatementSyntax)currentFor, typeNode, foreachIdentifier, - collectionExpression, iterationType)); + return document.WithSyntaxRoot(editor.GetChangedRoot()); - return document.WithSyntaxRoot(editor.GetChangedRoot()); + // local functions + (TTypeNode?, SyntaxToken, TStatementSyntax) TryDeconstructInitialDeclaration() + { + var bodyStatements = GetBodyStatements(forStatement); - // local functions - (TTypeNode?, SyntaxToken, TStatementSyntax) TryDeconstructInitialDeclaration() + if (bodyStatements.Count >= 1) { - var bodyStatements = GetBodyStatements(forStatement); - - if (bodyStatements.Count >= 1) + var firstStatement = bodyStatements[0]; + if (syntaxFacts.IsLocalDeclarationStatement(firstStatement)) { - var firstStatement = bodyStatements[0]; - if (syntaxFacts.IsLocalDeclarationStatement(firstStatement)) + var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(firstStatement); + if (variables.Count == 1) { - var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(firstStatement); - if (variables.Count == 1) + var firstVariable = (TVariableDeclaratorSyntax)variables[0]; + if (IsValidVariableDeclarator(firstVariable)) { - var firstVariable = (TVariableDeclaratorSyntax)variables[0]; - if (IsValidVariableDeclarator(firstVariable)) + var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(firstVariable); + if (initializer != null) { - var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(firstVariable); - if (initializer != null) + var firstVariableInitializer = syntaxFacts.GetValueOfEqualsValueClause(initializer); + if (syntaxFacts.AreEquivalent(firstVariableInitializer, indexExpression)) { - var firstVariableInitializer = syntaxFacts.GetValueOfEqualsValueClause(initializer); - if (syntaxFacts.AreEquivalent(firstVariableInitializer, indexExpression)) - { - var type = (TTypeNode?)syntaxFacts.GetTypeOfVariableDeclarator(firstVariable)?.WithoutLeadingTrivia(); - var identifier = syntaxFacts.GetIdentifierOfVariableDeclarator(firstVariable); - var statement = firstStatement; - return (type, identifier, statement); - } + var type = (TTypeNode?)syntaxFacts.GetTypeOfVariableDeclarator(firstVariable)?.WithoutLeadingTrivia(); + var identifier = syntaxFacts.GetIdentifierOfVariableDeclarator(firstVariable); + var statement = firstStatement; + return (type, identifier, statement); } } } } } - - return default; } - void FindAndReplaceMatches(SyntaxNode current) + return default; + } + + void FindAndReplaceMatches(SyntaxNode current) + { + if (SemanticEquivalence.AreEquivalent(semanticModel, current, collectionExpression)) { - if (SemanticEquivalence.AreEquivalent(semanticModel, current, collectionExpression)) + if (syntaxFacts.AreEquivalent(current.Parent, indexExpression)) { - if (syntaxFacts.AreEquivalent(current.Parent, indexExpression)) - { - // Found a match. replace with iteration variable. - var indexMatch = current.GetRequiredParent(); - Replace(indexMatch); - } - else if (syntaxFacts.AreEquivalent(current.Parent?.Parent, elementAtExpression)) - { - // Found a match. replace with iteration variable. - var indexMatch = current.GetRequiredParent().GetRequiredParent(); - Replace(indexMatch); - } - else + // Found a match. replace with iteration variable. + var indexMatch = current.GetRequiredParent(); + Replace(indexMatch); + } + else if (syntaxFacts.AreEquivalent(current.Parent?.Parent, elementAtExpression)) + { + // Found a match. replace with iteration variable. + var indexMatch = current.GetRequiredParent().GetRequiredParent(); + Replace(indexMatch); + } + else + { + // Collection was used for some other purpose. If it's passed as an argument + // to something, or is written to, or has a method invoked on it, we'll warn + // that it's potentially changing and may break if you switch to a foreach loop. + var shouldWarn = syntaxFacts.IsArgument(current.Parent); + shouldWarn |= semanticFacts.IsWrittenTo(semanticModel, current, cancellationToken); + shouldWarn |= + syntaxFacts.IsMemberAccessExpression(current.Parent) && + syntaxFacts.IsInvocationExpression(current.Parent.Parent); + + if (shouldWarn) { - // Collection was used for some other purpose. If it's passed as an argument - // to something, or is written to, or has a method invoked on it, we'll warn - // that it's potentially changing and may break if you switch to a foreach loop. - var shouldWarn = syntaxFacts.IsArgument(current.Parent); - shouldWarn |= semanticFacts.IsWrittenTo(semanticModel, current, cancellationToken); - shouldWarn |= - syntaxFacts.IsMemberAccessExpression(current.Parent) && - syntaxFacts.IsInvocationExpression(current.Parent.Parent); - - if (shouldWarn) - { - editor.ReplaceNode( - current, - (node, _) => node.WithAdditionalAnnotations( - WarningAnnotation.Create(FeaturesResources.Warning_colon_Iteration_variable_crossed_function_boundary))); - } + editor.ReplaceNode( + current, + (node, _) => node.WithAdditionalAnnotations( + WarningAnnotation.Create(FeaturesResources.Warning_colon_Iteration_variable_crossed_function_boundary))); } - - return; } - foreach (var child in current.ChildNodesAndTokens()) - { - if (child.IsNode) - FindAndReplaceMatches(child.AsNode()!); - } + return; } - bool CrossesFunctionBoundary(SyntaxNode node) + foreach (var child in current.ChildNodesAndTokens()) { - var containingFunction = node.AncestorsAndSelf().FirstOrDefault( - n => syntaxFacts.IsLocalFunctionStatement(n) || syntaxFacts.IsAnonymousFunctionExpression(n)); - - if (containingFunction == null) - return false; - - return containingFunction.AncestorsAndSelf().Contains(forStatement); + if (child.IsNode) + FindAndReplaceMatches(child.AsNode()!); } + } - void Replace(SyntaxNode indexMatch) - { - var replacementToken = foreachIdentifierReference; - - if (semanticFacts.IsWrittenTo(semanticModel, indexMatch, cancellationToken)) - { - replacementToken = replacementToken.WithAdditionalAnnotations( - WarningAnnotation.Create(FeaturesResources.Warning_colon_Collection_was_modified_during_iteration)); - } + bool CrossesFunctionBoundary(SyntaxNode node) + { + var containingFunction = node.AncestorsAndSelf().FirstOrDefault( + n => syntaxFacts.IsLocalFunctionStatement(n) || syntaxFacts.IsAnonymousFunctionExpression(n)); - if (CrossesFunctionBoundary(indexMatch)) - { - replacementToken = replacementToken.WithAdditionalAnnotations( - WarningAnnotation.Create(FeaturesResources.Warning_colon_Iteration_variable_crossed_function_boundary)); - } + if (containingFunction == null) + return false; - editor.ReplaceNode( - indexMatch, - generator.IdentifierName(replacementToken).WithTriviaFrom(indexMatch)); - } + return containingFunction.AncestorsAndSelf().Contains(forStatement); } - private static ITypeSymbol? GetIndexerType( - INamedTypeSymbol containingType, - ITypeSymbol collectionType, - INamedTypeSymbol ienumerableType) + void Replace(SyntaxNode indexMatch) { - if (collectionType is IArrayTypeSymbol arrayType) - return arrayType.Rank == 1 ? arrayType.ElementType : null; + var replacementToken = foreachIdentifierReference; - var indexer = collectionType - .GetAccessibleMembersInThisAndBaseTypes(containingType) - .Where(IsViableIndexer) - .FirstOrDefault(); - - if (indexer?.Type != null) - return indexer.Type; - - if (collectionType.IsInterfaceType()) + if (semanticFacts.IsWrittenTo(semanticModel, indexMatch, cancellationToken)) { - var interfaces = collectionType.GetAllInterfacesIncludingThis(); - indexer = interfaces.SelectMany(i => i.GetMembers().OfType().Where(IsViableIndexer)).FirstOrDefault(); - - if (indexer?.Type != null) - return indexer.Type; + replacementToken = replacementToken.WithAdditionalAnnotations( + WarningAnnotation.Create(FeaturesResources.Warning_colon_Collection_was_modified_during_iteration)); } - foreach (var interfaceType in collectionType.GetAllInterfacesIncludingThis()) + if (CrossesFunctionBoundary(indexMatch)) { - if (Equals(interfaceType.OriginalDefinition, ienumerableType)) - return interfaceType.TypeArguments[0]; + replacementToken = replacementToken.WithAdditionalAnnotations( + WarningAnnotation.Create(FeaturesResources.Warning_colon_Iteration_variable_crossed_function_boundary)); } - return null; + editor.ReplaceNode( + indexMatch, + generator.IdentifierName(replacementToken).WithTriviaFrom(indexMatch)); } + } + + private static ITypeSymbol? GetIndexerType( + INamedTypeSymbol containingType, + ITypeSymbol collectionType, + INamedTypeSymbol ienumerableType) + { + if (collectionType is IArrayTypeSymbol arrayType) + return arrayType.Rank == 1 ? arrayType.ElementType : null; + + var indexer = collectionType + .GetAccessibleMembersInThisAndBaseTypes(containingType) + .Where(IsViableIndexer) + .FirstOrDefault(); + + if (indexer?.Type != null) + return indexer.Type; - private static bool IsViableIndexer(IPropertySymbol property) - => property is { IsIndexer: true, Parameters: [{ Type.SpecialType: SpecialType.System_Int32 }] }; + if (collectionType.IsInterfaceType()) + { + var interfaces = collectionType.GetAllInterfacesIncludingThis(); + indexer = interfaces.SelectMany(i => i.GetMembers().OfType().Where(IsViableIndexer)).FirstOrDefault(); + + if (indexer?.Type != null) + return indexer.Type; + } + + foreach (var interfaceType in collectionType.GetAllInterfacesIncludingThis()) + { + if (Equals(interfaceType.OriginalDefinition, ienumerableType)) + return interfaceType.TypeArguments[0]; + } + + return null; } + + private static bool IsViableIndexer(IPropertySymbol property) + => property is { IsIndexer: true, Parameters: [{ Type.SpecialType: SpecialType.System_Int32 }] }; } diff --git a/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.AnalyzedNodes.cs b/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.AnalyzedNodes.cs index baf7adc6a8899..81865fc5ae625 100644 --- a/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.AnalyzedNodes.cs +++ b/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.AnalyzedNodes.cs @@ -5,98 +5,97 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.CodeAnalysis.ConvertIfToSwitch +namespace Microsoft.CodeAnalysis.ConvertIfToSwitch; + +internal abstract partial class AbstractConvertIfToSwitchCodeRefactoringProvider< + TIfStatementSyntax, + TExpressionSyntax, + TIsExpressionSyntax, + TPatternSyntax> + where TIfStatementSyntax : SyntaxNode + where TExpressionSyntax : SyntaxNode + where TIsExpressionSyntax : SyntaxNode + where TPatternSyntax : SyntaxNode { - internal abstract partial class AbstractConvertIfToSwitchCodeRefactoringProvider< - TIfStatementSyntax, - TExpressionSyntax, - TIsExpressionSyntax, - TPatternSyntax> - where TIfStatementSyntax : SyntaxNode - where TExpressionSyntax : SyntaxNode - where TIsExpressionSyntax : SyntaxNode - where TPatternSyntax : SyntaxNode + /// + /// Represents a switch-section constructed from a series of + /// if-conditions, possibly combined with logical-or operator + /// + internal sealed class AnalyzedSwitchSection(ImmutableArray labels, IOperation body, SyntaxNode syntaxToRemove) + { + public readonly ImmutableArray Labels = labels; + public readonly IOperation Body = body; + public readonly SyntaxNode SyntaxToRemove = syntaxToRemove; + } + + /// + /// Represents a switch-label constructed from a series of + /// if-conditions, possibly combined by logical-and operator + /// + internal sealed class AnalyzedSwitchLabel(AnalyzedPattern pattern, ImmutableArray guards) + { + public readonly AnalyzedPattern Pattern = pattern; + public readonly ImmutableArray Guards = guards; + } + + /// + /// Base class to represents a case clause (pattern) constructed from various checks + /// + internal abstract class AnalyzedPattern { + private AnalyzedPattern() + { + } + /// - /// Represents a switch-section constructed from a series of - /// if-conditions, possibly combined with logical-or operator + /// Represents a type-pattern, constructed from is-expression /// - internal sealed class AnalyzedSwitchSection(ImmutableArray labels, IOperation body, SyntaxNode syntaxToRemove) + internal sealed class Type(TIsExpressionSyntax expression) : AnalyzedPattern { - public readonly ImmutableArray Labels = labels; - public readonly IOperation Body = body; - public readonly SyntaxNode SyntaxToRemove = syntaxToRemove; + public readonly TIsExpressionSyntax IsExpressionSyntax = expression; } /// - /// Represents a switch-label constructed from a series of - /// if-conditions, possibly combined by logical-and operator + /// Represents a source-pattern constructed from C# patterns /// - internal sealed class AnalyzedSwitchLabel(AnalyzedPattern pattern, ImmutableArray guards) + internal sealed class Source(TPatternSyntax patternSyntax) : AnalyzedPattern { - public readonly AnalyzedPattern Pattern = pattern; - public readonly ImmutableArray Guards = guards; + public readonly TPatternSyntax PatternSyntax = patternSyntax; } /// - /// Base class to represents a case clause (pattern) constructed from various checks + /// Represents a constant-pattern constructed from an equality check /// - internal abstract class AnalyzedPattern + internal sealed class Constant(TExpressionSyntax expression) : AnalyzedPattern { - private AnalyzedPattern() - { - } - - /// - /// Represents a type-pattern, constructed from is-expression - /// - internal sealed class Type(TIsExpressionSyntax expression) : AnalyzedPattern - { - public readonly TIsExpressionSyntax IsExpressionSyntax = expression; - } - - /// - /// Represents a source-pattern constructed from C# patterns - /// - internal sealed class Source(TPatternSyntax patternSyntax) : AnalyzedPattern - { - public readonly TPatternSyntax PatternSyntax = patternSyntax; - } - - /// - /// Represents a constant-pattern constructed from an equality check - /// - internal sealed class Constant(TExpressionSyntax expression) : AnalyzedPattern - { - public readonly TExpressionSyntax ExpressionSyntax = expression; - } + public readonly TExpressionSyntax ExpressionSyntax = expression; + } - /// - /// Represents a relational-pattern constructed from comparison operators - /// - internal sealed class Relational(BinaryOperatorKind operatorKind, TExpressionSyntax value) : AnalyzedPattern - { - public readonly BinaryOperatorKind OperatorKind = operatorKind; - public readonly TExpressionSyntax Value = value; - } + /// + /// Represents a relational-pattern constructed from comparison operators + /// + internal sealed class Relational(BinaryOperatorKind operatorKind, TExpressionSyntax value) : AnalyzedPattern + { + public readonly BinaryOperatorKind OperatorKind = operatorKind; + public readonly TExpressionSyntax Value = value; + } - /// - /// Represents a range-pattern constructed from a couple of comparison operators - /// - internal sealed class Range(TExpressionSyntax lowerBound, TExpressionSyntax higherBound) : AnalyzedPattern - { - public readonly TExpressionSyntax LowerBound = lowerBound; - public readonly TExpressionSyntax HigherBound = higherBound; - } + /// + /// Represents a range-pattern constructed from a couple of comparison operators + /// + internal sealed class Range(TExpressionSyntax lowerBound, TExpressionSyntax higherBound) : AnalyzedPattern + { + public readonly TExpressionSyntax LowerBound = lowerBound; + public readonly TExpressionSyntax HigherBound = higherBound; + } - /// - /// Represents an and-pattern, constructed from two other patterns. - /// - internal sealed class And(AnalyzedPattern leftPattern, AnalyzedPattern rightPattern) : AnalyzedPattern - { - public readonly AnalyzedPattern LeftPattern = leftPattern; - public readonly AnalyzedPattern RightPattern = rightPattern; - } + /// + /// Represents an and-pattern, constructed from two other patterns. + /// + internal sealed class And(AnalyzedPattern leftPattern, AnalyzedPattern rightPattern) : AnalyzedPattern + { + public readonly AnalyzedPattern LeftPattern = leftPattern; + public readonly AnalyzedPattern RightPattern = rightPattern; } } } diff --git a/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.Analyzer.cs b/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.Analyzer.cs index 2f738b5682ccd..6c4b292e94ae6 100644 --- a/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.Analyzer.cs +++ b/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.Analyzer.cs @@ -10,482 +10,481 @@ using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ConvertIfToSwitch -{ - using static BinaryOperatorKind; +namespace Microsoft.CodeAnalysis.ConvertIfToSwitch; + +using static BinaryOperatorKind; - internal abstract partial class AbstractConvertIfToSwitchCodeRefactoringProvider< - TIfStatementSyntax, TExpressionSyntax, TIsExpressionSyntax, TPatternSyntax> +internal abstract partial class AbstractConvertIfToSwitchCodeRefactoringProvider< + TIfStatementSyntax, TExpressionSyntax, TIsExpressionSyntax, TPatternSyntax> +{ + // Match the following pattern which can be safely converted to switch statement + // + // + // : if () { }, + // : if () { }, ( return | throw ) + // | + // + // + // : if () { _ } else + // | if () { _ } else { } + // | if () { _ } else { _ } + // | if () { _ } + // + // + // : || + // | + // + // + // : && // C# + // | is // C# + // | is // C# + // | == // C#, VB + // | // C#, VB + // | ( >= | <= ) + // && ( <= | >= ) // C#, VB + // | ( <= | >= ) + // && ( >= | <= ) // C#, VB + // + internal abstract class Analyzer { - // Match the following pattern which can be safely converted to switch statement + public abstract bool CanConvert(IConditionalOperation operation); + public abstract bool HasUnreachableEndPoint(IOperation operation); + public abstract bool CanImplicitlyConvert(SemanticModel semanticModel, SyntaxNode syntax, ITypeSymbol targetType); + + /// + /// Holds the expression determined to be used as the target expression of the switch + /// + /// + /// Note that this is initially unset until we find a non-constant expression. + /// + private SyntaxNode _switchTargetExpression = null!; + /// + /// Holds the type of the + /// + private ITypeSymbol? _switchTargetType = null!; + private readonly ISyntaxFacts _syntaxFacts; + + protected Analyzer(ISyntaxFacts syntaxFacts, Feature features) + { + _syntaxFacts = syntaxFacts; + Features = features; + } + + public Feature Features { get; } + + public bool Supports(Feature feature) + => (Features & feature) != 0; + + public (ImmutableArray, SyntaxNode TargetExpression) AnalyzeIfStatementSequence(ReadOnlySpan operations) + { + using var _ = ArrayBuilder.GetInstance(out var sections); + if (!ParseIfStatementSequence(operations, sections, out var defaultBodyOpt)) + { + return default; + } + + if (defaultBodyOpt is object) + { + sections.Add(new AnalyzedSwitchSection(labels: default, defaultBodyOpt, defaultBodyOpt.Syntax)); + } + + RoslynDebug.Assert(_switchTargetExpression is object); + return (sections.ToImmutable(), _switchTargetExpression); + } + + // Tree to parse: // // // : if () { }, // : if () { }, ( return | throw ) // | // - // - // : if () { _ } else - // | if () { _ } else { } - // | if () { _ } else { _ } - // | if () { _ } - // - // - // : || - // | - // - // - // : && // C# - // | is // C# - // | is // C# - // | == // C#, VB - // | // C#, VB - // | ( >= | <= ) - // && ( <= | >= ) // C#, VB - // | ( <= | >= ) - // && ( >= | <= ) // C#, VB - // - internal abstract class Analyzer + private bool ParseIfStatementSequence(ReadOnlySpan operations, ArrayBuilder sections, out IOperation? defaultBodyOpt) { - public abstract bool CanConvert(IConditionalOperation operation); - public abstract bool HasUnreachableEndPoint(IOperation operation); - public abstract bool CanImplicitlyConvert(SemanticModel semanticModel, SyntaxNode syntax, ITypeSymbol targetType); - - /// - /// Holds the expression determined to be used as the target expression of the switch - /// - /// - /// Note that this is initially unset until we find a non-constant expression. - /// - private SyntaxNode _switchTargetExpression = null!; - /// - /// Holds the type of the - /// - private ITypeSymbol? _switchTargetType = null!; - private readonly ISyntaxFacts _syntaxFacts; - - protected Analyzer(ISyntaxFacts syntaxFacts, Feature features) + var current = 0; + while (current < operations.Length && + operations[current] is IConditionalOperation { WhenFalse: null } op && + HasUnreachableEndPoint(op.WhenTrue) && + ParseIfStatement(op, sections, out _)) { - _syntaxFacts = syntaxFacts; - Features = features; + current++; } - public Feature Features { get; } - - public bool Supports(Feature feature) - => (Features & feature) != 0; - - public (ImmutableArray, SyntaxNode TargetExpression) AnalyzeIfStatementSequence(ReadOnlySpan operations) + defaultBodyOpt = null; + if (current == 0) { - using var _ = ArrayBuilder.GetInstance(out var sections); - if (!ParseIfStatementSequence(operations, sections, out var defaultBodyOpt)) - { - return default; - } - - if (defaultBodyOpt is object) + // didn't consume a sequence of if-statements with unreachable ends. Check for the last case. + return operations.Length > 0 && ParseIfStatement(operations[0], sections, out defaultBodyOpt); + } + else + { + if (current < operations.Length) { - sections.Add(new AnalyzedSwitchSection(labels: default, defaultBodyOpt, defaultBodyOpt.Syntax)); + // consumed a sequence of if-statements with unreachable-ends. If we end with a normal + // if-statement, we're done. Otherwise, we end with whatever last return/throw we see. + var nextStatement = operations[current]; + if (!ParseIfStatement(nextStatement, sections, out defaultBodyOpt) && + nextStatement is IReturnOperation { ReturnedValue: not null } or IThrowOperation { Exception: not null }) + { + defaultBodyOpt = nextStatement; + } } - RoslynDebug.Assert(_switchTargetExpression is object); - return (sections.ToImmutable(), _switchTargetExpression); + return true; } + } - // Tree to parse: - // - // - // : if () { }, - // : if () { }, ( return | throw ) - // | - // - private bool ParseIfStatementSequence(ReadOnlySpan operations, ArrayBuilder sections, out IOperation? defaultBodyOpt) + // Tree to parse: + // + // + // : if () { _ } else + // | if () { _ } else { } + // | if () { _ } else { _ } + // | if () { _ } + // + private bool ParseIfStatement(IOperation operation, ArrayBuilder sections, out IOperation? defaultBodyOpt) + { + switch (operation) { - var current = 0; - while (current < operations.Length && - operations[current] is IConditionalOperation { WhenFalse: null } op && - HasUnreachableEndPoint(op.WhenTrue) && - ParseIfStatement(op, sections, out _)) - { - current++; - } + case IConditionalOperation op when CanConvert(op): + var section = ParseSwitchSection(op); + if (section is null) + { + break; + } - defaultBodyOpt = null; - if (current == 0) - { - // didn't consume a sequence of if-statements with unreachable ends. Check for the last case. - return operations.Length > 0 && ParseIfStatement(operations[0], sections, out defaultBodyOpt); - } - else - { - if (current < operations.Length) + sections.Add(section); + + if (op.WhenFalse is null) { - // consumed a sequence of if-statements with unreachable-ends. If we end with a normal - // if-statement, we're done. Otherwise, we end with whatever last return/throw we see. - var nextStatement = operations[current]; - if (!ParseIfStatement(nextStatement, sections, out defaultBodyOpt) && - nextStatement is IReturnOperation { ReturnedValue: not null } or IThrowOperation { Exception: not null }) - { - defaultBodyOpt = nextStatement; - } + defaultBodyOpt = null; + } + else if (!ParseIfStatementOrBlock(op.WhenFalse, sections, out defaultBodyOpt)) + { + defaultBodyOpt = op.WhenFalse; } return true; - } } - // Tree to parse: - // - // - // : if () { _ } else - // | if () { _ } else { } - // | if () { _ } else { _ } - // | if () { _ } - // - private bool ParseIfStatement(IOperation operation, ArrayBuilder sections, out IOperation? defaultBodyOpt) - { - switch (operation) - { - case IConditionalOperation op when CanConvert(op): - var section = ParseSwitchSection(op); - if (section is null) - { - break; - } - - sections.Add(section); - - if (op.WhenFalse is null) - { - defaultBodyOpt = null; - } - else if (!ParseIfStatementOrBlock(op.WhenFalse, sections, out defaultBodyOpt)) - { - defaultBodyOpt = op.WhenFalse; - } - - return true; - } + defaultBodyOpt = null; + return false; + } - defaultBodyOpt = null; - return false; - } + private bool ParseIfStatementOrBlock(IOperation op, ArrayBuilder sections, out IOperation? defaultBodyOpt) + { + return op is IBlockOperation block + ? ParseIfStatementSequence(block.Operations.AsSpan(), sections, out defaultBodyOpt) + : ParseIfStatement(op, sections, out defaultBodyOpt); + } - private bool ParseIfStatementOrBlock(IOperation op, ArrayBuilder sections, out IOperation? defaultBodyOpt) + private AnalyzedSwitchSection? ParseSwitchSection(IConditionalOperation operation) + { + using var _ = ArrayBuilder.GetInstance(out var labels); + if (!ParseSwitchLabels(operation.Condition, labels)) { - return op is IBlockOperation block - ? ParseIfStatementSequence(block.Operations.AsSpan(), sections, out defaultBodyOpt) - : ParseIfStatement(op, sections, out defaultBodyOpt); + return null; } - private AnalyzedSwitchSection? ParseSwitchSection(IConditionalOperation operation) + return new AnalyzedSwitchSection(labels.ToImmutable(), operation.WhenTrue, operation.Syntax); + } + + // Tree to parse: + // + // + // : || + // | + // + private bool ParseSwitchLabels(IOperation operation, ArrayBuilder labels) + { + if (operation is IBinaryOperation { OperatorKind: ConditionalOr } op) { - using var _ = ArrayBuilder.GetInstance(out var labels); - if (!ParseSwitchLabels(operation.Condition, labels)) + if (!ParseSwitchLabels(op.LeftOperand, labels)) { - return null; + return false; } - return new AnalyzedSwitchSection(labels.ToImmutable(), operation.WhenTrue, operation.Syntax); + operation = op.RightOperand; } - // Tree to parse: - // - // - // : || - // | - // - private bool ParseSwitchLabels(IOperation operation, ArrayBuilder labels) + var label = ParseSwitchLabel(operation); + if (label is null) { - if (operation is IBinaryOperation { OperatorKind: ConditionalOr } op) - { - if (!ParseSwitchLabels(op.LeftOperand, labels)) - { - return false; - } - - operation = op.RightOperand; - } + return false; + } - var label = ParseSwitchLabel(operation); - if (label is null) - { - return false; - } + labels.Add(label); + return true; + } - labels.Add(label); - return true; - } + private AnalyzedSwitchLabel? ParseSwitchLabel(IOperation operation) + { + using var _ = ArrayBuilder.GetInstance(out var guards); + var pattern = ParsePattern(operation, guards); + if (pattern is null) + return null; - private AnalyzedSwitchLabel? ParseSwitchLabel(IOperation operation) - { - using var _ = ArrayBuilder.GetInstance(out var guards); - var pattern = ParsePattern(operation, guards); - if (pattern is null) - return null; + return new AnalyzedSwitchLabel(pattern, guards.ToImmutable()); + } - return new AnalyzedSwitchLabel(pattern, guards.ToImmutable()); - } + private enum ConstantResult + { + /// + /// None of operands were constant. + /// + None, + /// + /// Signifies that the left operand is the constant. + /// + Left, + /// + /// Signifies that the right operand is the constant. + /// + Right, + } - private enum ConstantResult + private ConstantResult DetermineConstant(IBinaryOperation op) + { + return (op.LeftOperand, op.RightOperand) switch { - /// - /// None of operands were constant. - /// - None, - /// - /// Signifies that the left operand is the constant. - /// - Left, - /// - /// Signifies that the right operand is the constant. - /// - Right, - } + var (e, v) when IsConstant(v) && CheckTargetExpression(e) && CheckConstantType(v) => ConstantResult.Right, + var (v, e) when IsConstant(v) && CheckTargetExpression(e) && CheckConstantType(v) => ConstantResult.Left, + _ => ConstantResult.None, + }; + } - private ConstantResult DetermineConstant(IBinaryOperation op) + // Tree to parse: + // + // + // : && // C# + // | is // C# + // | is // C# + // | == // C#, VB + // | // VB + // | ( >= | <= ) + // && ( <= | >= ) // VB + // | ( <= | >= ) + // && ( >= | <= ) // VB + // + private AnalyzedPattern? ParsePattern(IOperation operation, ArrayBuilder guards) + { + switch (operation) { - return (op.LeftOperand, op.RightOperand) switch - { - var (e, v) when IsConstant(v) && CheckTargetExpression(e) && CheckConstantType(v) => ConstantResult.Right, - var (v, e) when IsConstant(v) && CheckTargetExpression(e) && CheckConstantType(v) => ConstantResult.Left, - _ => ConstantResult.None, - }; - } + case IBinaryOperation { OperatorKind: ConditionalAnd } op + when Supports(Feature.RangePattern) && GetRangeBounds(op) is (TExpressionSyntax lower, TExpressionSyntax higher): + return new AnalyzedPattern.Range(lower, higher); - // Tree to parse: - // - // - // : && // C# - // | is // C# - // | is // C# - // | == // C#, VB - // | // VB - // | ( >= | <= ) - // && ( <= | >= ) // VB - // | ( <= | >= ) - // && ( >= | <= ) // VB - // - private AnalyzedPattern? ParsePattern(IOperation operation, ArrayBuilder guards) - { - switch (operation) - { - case IBinaryOperation { OperatorKind: ConditionalAnd } op - when Supports(Feature.RangePattern) && GetRangeBounds(op) is (TExpressionSyntax lower, TExpressionSyntax higher): - return new AnalyzedPattern.Range(lower, higher); + case IBinaryOperation { OperatorKind: BinaryOperatorKind.Equals } op: + return DetermineConstant(op) switch + { + ConstantResult.Left when op.LeftOperand.Syntax is TExpressionSyntax left + => new AnalyzedPattern.Constant(left), + ConstantResult.Right when op.RightOperand.Syntax is TExpressionSyntax right + => new AnalyzedPattern.Constant(right), + _ => null + }; + + case IBinaryOperation { OperatorKind: NotEquals } op + when Supports(Feature.InequalityPattern): + return ParseRelationalPattern(op); + + case IBinaryOperation op + when Supports(Feature.RelationalPattern) && IsRelationalOperator(op.OperatorKind): + return ParseRelationalPattern(op); + + // Check this below the cases that produce Relational/Ranges. We would prefer to use those if + // available before utilizing a CaseGuard. + case IBinaryOperation { OperatorKind: ConditionalAnd } op + when Supports(Feature.AndPattern | Feature.CaseGuard): + { + var leftPattern = ParsePattern(op.LeftOperand, guards); + if (leftPattern == null) + return null; - case IBinaryOperation { OperatorKind: BinaryOperatorKind.Equals } op: - return DetermineConstant(op) switch + if (Supports(Feature.AndPattern)) { - ConstantResult.Left when op.LeftOperand.Syntax is TExpressionSyntax left - => new AnalyzedPattern.Constant(left), - ConstantResult.Right when op.RightOperand.Syntax is TExpressionSyntax right - => new AnalyzedPattern.Constant(right), - _ => null - }; - - case IBinaryOperation { OperatorKind: NotEquals } op - when Supports(Feature.InequalityPattern): - return ParseRelationalPattern(op); - - case IBinaryOperation op - when Supports(Feature.RelationalPattern) && IsRelationalOperator(op.OperatorKind): - return ParseRelationalPattern(op); - - // Check this below the cases that produce Relational/Ranges. We would prefer to use those if - // available before utilizing a CaseGuard. - case IBinaryOperation { OperatorKind: ConditionalAnd } op - when Supports(Feature.AndPattern | Feature.CaseGuard): - { - var leftPattern = ParsePattern(op.LeftOperand, guards); - if (leftPattern == null) - return null; - - if (Supports(Feature.AndPattern)) - { - var guardCount = guards.Count; - var rightPattern = ParsePattern(op.RightOperand, guards); - if (rightPattern != null) - return new AnalyzedPattern.And(leftPattern, rightPattern); - - // Making a pattern out of the RHS didn't work. Reset the guards back to where we started. - guards.Count = guardCount; - } - - if (Supports(Feature.CaseGuard) && op.RightOperand.Syntax is TExpressionSyntax node) - { - guards.Add(node); - return leftPattern; - } + var guardCount = guards.Count; + var rightPattern = ParsePattern(op.RightOperand, guards); + if (rightPattern != null) + return new AnalyzedPattern.And(leftPattern, rightPattern); - return null; + // Making a pattern out of the RHS didn't work. Reset the guards back to where we started. + guards.Count = guardCount; } - case IIsTypeOperation op - when Supports(Feature.IsTypePattern) && CheckTargetExpression(op.ValueOperand) && op.Syntax is TIsExpressionSyntax node: - return new AnalyzedPattern.Type(node); - - case IIsPatternOperation op - when Supports(Feature.SourcePattern) && CheckTargetExpression(op.Value) && op.Pattern.Syntax is TPatternSyntax pattern: - return new AnalyzedPattern.Source(pattern); + if (Supports(Feature.CaseGuard) && op.RightOperand.Syntax is TExpressionSyntax node) + { + guards.Add(node); + return leftPattern; + } - case IParenthesizedOperation op: - return ParsePattern(op.Operand, guards); - } + return null; + } - return null; - } + case IIsTypeOperation op + when Supports(Feature.IsTypePattern) && CheckTargetExpression(op.ValueOperand) && op.Syntax is TIsExpressionSyntax node: + return new AnalyzedPattern.Type(node); - private AnalyzedPattern? ParseRelationalPattern(IBinaryOperation op) - { - return DetermineConstant(op) switch - { - ConstantResult.Left when op.LeftOperand.Syntax is TExpressionSyntax left - => new AnalyzedPattern.Relational(Flip(op.OperatorKind), left), - ConstantResult.Right when op.RightOperand.Syntax is TExpressionSyntax right - => new AnalyzedPattern.Relational(op.OperatorKind, right), - _ => null - }; - } + case IIsPatternOperation op + when Supports(Feature.SourcePattern) && CheckTargetExpression(op.Value) && op.Pattern.Syntax is TPatternSyntax pattern: + return new AnalyzedPattern.Source(pattern); - private enum BoundKind - { - /// - /// Not a range bound. - /// - None, - /// - /// Signifies that the lower-bound of a range pattern - /// - Lower, - /// - /// Signifies that the higher-bound of a range pattern - /// - Higher, + case IParenthesizedOperation op: + return ParsePattern(op.Operand, guards); } - private (SyntaxNode Lower, SyntaxNode Higher) GetRangeBounds(IBinaryOperation op) - { - if (op is not - { LeftOperand: IBinaryOperation left, RightOperand: IBinaryOperation right }) - { - return default; - } - - return (GetRangeBound(left), GetRangeBound(right)) switch - { - ({ Kind: BoundKind.Lower } low, { Kind: BoundKind.Higher } high) - when CheckTargetExpression(low.Expression, high.Expression) => (low.Value.Syntax, high.Value.Syntax), - ({ Kind: BoundKind.Higher } high, { Kind: BoundKind.Lower } low) - when CheckTargetExpression(low.Expression, high.Expression) => (low.Value.Syntax, high.Value.Syntax), - _ => default - }; - - bool CheckTargetExpression(IOperation left, IOperation right) - => _syntaxFacts.AreEquivalent(left.Syntax, right.Syntax) && this.CheckTargetExpression(left); - } + return null; + } - private static (BoundKind Kind, IOperation Expression, IOperation Value) GetRangeBound(IBinaryOperation op) + private AnalyzedPattern? ParseRelationalPattern(IBinaryOperation op) + { + return DetermineConstant(op) switch { - return op.OperatorKind switch - { - // 5 <= i - LessThanOrEqual when IsConstant(op.LeftOperand) => (BoundKind.Lower, op.RightOperand, op.LeftOperand), - // i <= 5 - LessThanOrEqual when IsConstant(op.RightOperand) => (BoundKind.Higher, op.LeftOperand, op.RightOperand), - // 5 >= i - GreaterThanOrEqual when IsConstant(op.LeftOperand) => (BoundKind.Higher, op.RightOperand, op.LeftOperand), - // i >= 5 - GreaterThanOrEqual when IsConstant(op.RightOperand) => (BoundKind.Lower, op.LeftOperand, op.RightOperand), - _ => default - }; - } + ConstantResult.Left when op.LeftOperand.Syntax is TExpressionSyntax left + => new AnalyzedPattern.Relational(Flip(op.OperatorKind), left), + ConstantResult.Right when op.RightOperand.Syntax is TExpressionSyntax right + => new AnalyzedPattern.Relational(op.OperatorKind, right), + _ => null + }; + } + private enum BoundKind + { /// - /// Changes the direction the operator is pointing + /// Not a range bound. /// - private static BinaryOperatorKind Flip(BinaryOperatorKind operatorKind) - { - return operatorKind switch - { - LessThan => GreaterThan, - LessThanOrEqual => GreaterThanOrEqual, - GreaterThanOrEqual => LessThanOrEqual, - GreaterThan => LessThan, - NotEquals => NotEquals, - var v => throw ExceptionUtilities.UnexpectedValue(v) - }; - } + None, + /// + /// Signifies that the lower-bound of a range pattern + /// + Lower, + /// + /// Signifies that the higher-bound of a range pattern + /// + Higher, + } - private static bool IsRelationalOperator(BinaryOperatorKind operatorKind) + private (SyntaxNode Lower, SyntaxNode Higher) GetRangeBounds(IBinaryOperation op) + { + if (op is not + { LeftOperand: IBinaryOperation left, RightOperand: IBinaryOperation right }) { - switch (operatorKind) - { - case LessThan: - case LessThanOrEqual: - case GreaterThanOrEqual: - case GreaterThan: - return true; - default: - return false; - } + return default; } - private static bool IsConstant(IOperation operation) + return (GetRangeBound(left), GetRangeBound(right)) switch { - // Constants do not propagate to conversions - return operation is IConversionOperation { Conversion.IsUserDefined: false } op - ? IsConstant(op.Operand) - : operation.ConstantValue.HasValue; - } + ({ Kind: BoundKind.Lower } low, { Kind: BoundKind.Higher } high) + when CheckTargetExpression(low.Expression, high.Expression) => (low.Value.Syntax, high.Value.Syntax), + ({ Kind: BoundKind.Higher } high, { Kind: BoundKind.Lower } low) + when CheckTargetExpression(low.Expression, high.Expression) => (low.Value.Syntax, high.Value.Syntax), + _ => default + }; + + bool CheckTargetExpression(IOperation left, IOperation right) + => _syntaxFacts.AreEquivalent(left.Syntax, right.Syntax) && this.CheckTargetExpression(left); + } - private bool CheckTargetExpression(IOperation operation) + private static (BoundKind Kind, IOperation Expression, IOperation Value) GetRangeBound(IBinaryOperation op) + { + return op.OperatorKind switch { - operation = operation.WalkDownConversion(); + // 5 <= i + LessThanOrEqual when IsConstant(op.LeftOperand) => (BoundKind.Lower, op.RightOperand, op.LeftOperand), + // i <= 5 + LessThanOrEqual when IsConstant(op.RightOperand) => (BoundKind.Higher, op.LeftOperand, op.RightOperand), + // 5 >= i + GreaterThanOrEqual when IsConstant(op.LeftOperand) => (BoundKind.Higher, op.RightOperand, op.LeftOperand), + // i >= 5 + GreaterThanOrEqual when IsConstant(op.RightOperand) => (BoundKind.Lower, op.LeftOperand, op.RightOperand), + _ => default + }; + } - var expression = operation.Syntax; - // If we have not figured the switch expression yet, - // we will assume that the first expression is the one. - if (_switchTargetExpression is null) - { - RoslynDebug.Assert(_switchTargetType is null); + /// + /// Changes the direction the operator is pointing + /// + private static BinaryOperatorKind Flip(BinaryOperatorKind operatorKind) + { + return operatorKind switch + { + LessThan => GreaterThan, + LessThanOrEqual => GreaterThanOrEqual, + GreaterThanOrEqual => LessThanOrEqual, + GreaterThan => LessThan, + NotEquals => NotEquals, + var v => throw ExceptionUtilities.UnexpectedValue(v) + }; + } - _switchTargetExpression = expression; - _switchTargetType = operation.Type; + private static bool IsRelationalOperator(BinaryOperatorKind operatorKind) + { + switch (operatorKind) + { + case LessThan: + case LessThanOrEqual: + case GreaterThanOrEqual: + case GreaterThan: return true; - } - - return _syntaxFacts.AreEquivalent(expression, _switchTargetExpression); + default: + return false; } + } + + private static bool IsConstant(IOperation operation) + { + // Constants do not propagate to conversions + return operation is IConversionOperation { Conversion.IsUserDefined: false } op + ? IsConstant(op.Operand) + : operation.ConstantValue.HasValue; + } + + private bool CheckTargetExpression(IOperation operation) + { + operation = operation.WalkDownConversion(); - private bool CheckConstantType(IOperation operation) + var expression = operation.Syntax; + // If we have not figured the switch expression yet, + // we will assume that the first expression is the one. + if (_switchTargetExpression is null) { - RoslynDebug.AssertNotNull(operation.SemanticModel); - RoslynDebug.AssertNotNull(_switchTargetType); + RoslynDebug.Assert(_switchTargetType is null); - return CanImplicitlyConvert(operation.SemanticModel, operation.Syntax, _switchTargetType); + _switchTargetExpression = expression; + _switchTargetType = operation.Type; + return true; } + + return _syntaxFacts.AreEquivalent(expression, _switchTargetExpression); } - [Flags] - internal enum Feature + private bool CheckConstantType(IOperation operation) { - None = 0, - // VB/C# 9.0 features - RelationalPattern = 1, - // VB features - InequalityPattern = 1 << 1, - RangePattern = 1 << 2, - // C# 7.0 features - SourcePattern = 1 << 3, - IsTypePattern = 1 << 4, - CaseGuard = 1 << 5, - // C# 8.0 features - SwitchExpression = 1 << 6, - // C# 9.0 features - OrPattern = 1 << 7, - AndPattern = 1 << 8, - TypePattern = 1 << 9, + RoslynDebug.AssertNotNull(operation.SemanticModel); + RoslynDebug.AssertNotNull(_switchTargetType); + + return CanImplicitlyConvert(operation.SemanticModel, operation.Syntax, _switchTargetType); } } + + [Flags] + internal enum Feature + { + None = 0, + // VB/C# 9.0 features + RelationalPattern = 1, + // VB features + InequalityPattern = 1 << 1, + RangePattern = 1 << 2, + // C# 7.0 features + SourcePattern = 1 << 3, + IsTypePattern = 1 << 4, + CaseGuard = 1 << 5, + // C# 8.0 features + SwitchExpression = 1 << 6, + // C# 9.0 features + OrPattern = 1 << 7, + AndPattern = 1 << 8, + TypePattern = 1 << 9, + } } diff --git a/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.Rewriting.cs b/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.Rewriting.cs index 4f7335e9d9272..458ef2e30fbe1 100644 --- a/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.Rewriting.cs +++ b/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.Rewriting.cs @@ -14,59 +14,58 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ConvertIfToSwitch +namespace Microsoft.CodeAnalysis.ConvertIfToSwitch; + +internal abstract partial class AbstractConvertIfToSwitchCodeRefactoringProvider< + TIfStatementSyntax, TExpressionSyntax, TIsExpressionSyntax, TPatternSyntax> { - internal abstract partial class AbstractConvertIfToSwitchCodeRefactoringProvider< - TIfStatementSyntax, TExpressionSyntax, TIsExpressionSyntax, TPatternSyntax> - { - public abstract SyntaxNode CreateSwitchExpressionStatement(SyntaxNode target, ImmutableArray sections, Feature feature); - public abstract SyntaxNode CreateSwitchStatement(TIfStatementSyntax ifStatement, SyntaxNode target, IEnumerable sectionList); - public abstract IEnumerable AsSwitchSectionStatements(IOperation operation); - public abstract SyntaxNode AsSwitchLabelSyntax(AnalyzedSwitchLabel label, Feature feature); - protected abstract SyntaxTriviaList GetLeadingTriviaToTransfer(SyntaxNode syntaxToRemove); + public abstract SyntaxNode CreateSwitchExpressionStatement(SyntaxNode target, ImmutableArray sections, Feature feature); + public abstract SyntaxNode CreateSwitchStatement(TIfStatementSyntax ifStatement, SyntaxNode target, IEnumerable sectionList); + public abstract IEnumerable AsSwitchSectionStatements(IOperation operation); + public abstract SyntaxNode AsSwitchLabelSyntax(AnalyzedSwitchLabel label, Feature feature); + protected abstract SyntaxTriviaList GetLeadingTriviaToTransfer(SyntaxNode syntaxToRemove); - private async Task UpdateDocumentAsync( - Document document, - SyntaxNode target, - TIfStatementSyntax ifStatement, - ImmutableArray sections, - Feature feature, - bool convertToSwitchExpression, - CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var generator = SyntaxGenerator.GetGenerator(document); - var ifSpan = ifStatement.Span; - var options = root.SyntaxTree.Options; + private async Task UpdateDocumentAsync( + Document document, + SyntaxNode target, + TIfStatementSyntax ifStatement, + ImmutableArray sections, + Feature feature, + bool convertToSwitchExpression, + CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var generator = SyntaxGenerator.GetGenerator(document); + var ifSpan = ifStatement.Span; + var options = root.SyntaxTree.Options; - var @switch = convertToSwitchExpression - ? CreateSwitchExpressionStatement(target, sections, feature) - : CreateSwitchStatement(ifStatement, target, sections.Select(section => AsSwitchSectionSyntax(section, generator, feature))); + var @switch = convertToSwitchExpression + ? CreateSwitchExpressionStatement(target, sections, feature) + : CreateSwitchStatement(ifStatement, target, sections.Select(section => AsSwitchSectionSyntax(section, generator, feature))); - var lastNode = sections.Last().SyntaxToRemove; - @switch = @switch - .WithLeadingTrivia(ifStatement.GetLeadingTrivia()) - .WithTrailingTrivia(lastNode.GetTrailingTrivia()) - .WithAdditionalAnnotations(Formatter.Annotation) - .WithAdditionalAnnotations(Simplifier.Annotation); + var lastNode = sections.Last().SyntaxToRemove; + @switch = @switch + .WithLeadingTrivia(ifStatement.GetLeadingTrivia()) + .WithTrailingTrivia(lastNode.GetTrailingTrivia()) + .WithAdditionalAnnotations(Formatter.Annotation) + .WithAdditionalAnnotations(Simplifier.Annotation); - var nodesToRemove = sections.Skip(1).Select(s => s.SyntaxToRemove).Where(s => s.Parent == ifStatement.Parent); - root = root.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia); - Debug.Assert(root is object); // we didn't remove the root - root = root.ReplaceNode(root.FindNode(ifSpan, getInnermostNodeForTie: true), @switch); - return document.WithSyntaxRoot(root); - } + var nodesToRemove = sections.Skip(1).Select(s => s.SyntaxToRemove).Where(s => s.Parent == ifStatement.Parent); + root = root.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia); + Debug.Assert(root is object); // we didn't remove the root + root = root.ReplaceNode(root.FindNode(ifSpan, getInnermostNodeForTie: true), @switch); + return document.WithSyntaxRoot(root); + } - private SyntaxNode AsSwitchSectionSyntax(AnalyzedSwitchSection section, SyntaxGenerator generator, Feature feature) - { - var statements = AsSwitchSectionStatements(section.Body); - var sectionNode = section.Labels.IsDefault - ? generator.DefaultSwitchSection(statements) - : generator.SwitchSectionFromLabels(section.Labels.Select(label => AsSwitchLabelSyntax(label, feature)), statements); + private SyntaxNode AsSwitchSectionSyntax(AnalyzedSwitchSection section, SyntaxGenerator generator, Feature feature) + { + var statements = AsSwitchSectionStatements(section.Body); + var sectionNode = section.Labels.IsDefault + ? generator.DefaultSwitchSection(statements) + : generator.SwitchSectionFromLabels(section.Labels.Select(label => AsSwitchLabelSyntax(label, feature)), statements); - sectionNode = sectionNode.WithPrependedLeadingTrivia(GetLeadingTriviaToTransfer(section.SyntaxToRemove)); + sectionNode = sectionNode.WithPrependedLeadingTrivia(GetLeadingTriviaToTransfer(section.SyntaxToRemove)); - return sectionNode; - } + return sectionNode; } } diff --git a/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.cs b/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.cs index 6e3653d0a5d80..e2745ada66629 100644 --- a/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.cs @@ -19,247 +19,246 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ConvertIfToSwitch +namespace Microsoft.CodeAnalysis.ConvertIfToSwitch; + +internal abstract partial class AbstractConvertIfToSwitchCodeRefactoringProvider< + TIfStatementSyntax, TExpressionSyntax, TIsExpressionSyntax, TPatternSyntax> : SyntaxEditorBasedCodeRefactoringProvider { - internal abstract partial class AbstractConvertIfToSwitchCodeRefactoringProvider< - TIfStatementSyntax, TExpressionSyntax, TIsExpressionSyntax, TPatternSyntax> : SyntaxEditorBasedCodeRefactoringProvider - { - private const string SwitchStatementEquivalenceKey = "SwitchStatement"; - private const string SwitchExpressionEquivalenceKey = "SwitchExpression"; + private const string SwitchStatementEquivalenceKey = "SwitchStatement"; + private const string SwitchExpressionEquivalenceKey = "SwitchExpression"; - public abstract string GetTitle(bool forSwitchExpression); - public abstract Analyzer CreateAnalyzer(ISyntaxFacts syntaxFacts, ParseOptions options); + public abstract string GetTitle(bool forSwitchExpression); + public abstract Analyzer CreateAnalyzer(ISyntaxFacts syntaxFacts, ParseOptions options); - protected sealed override ImmutableArray SupportedFixAllScopes => AllFixAllScopes; + protected sealed override ImmutableArray SupportedFixAllScopes => AllFixAllScopes; - public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, _, cancellationToken) = context; + if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) { - var (document, _, cancellationToken) = context; - if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles) - { - return; - } + return; + } - var ifStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var syntaxFactsService = document.GetRequiredLanguageService(); - if (!ShouldOfferRefactoring(ifStatement, semanticModel, syntaxFactsService, out var analyzer, out var sections, out var target)) - { - return; - } + var ifStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var syntaxFactsService = document.GetRequiredLanguageService(); + if (!ShouldOfferRefactoring(ifStatement, semanticModel, syntaxFactsService, out var analyzer, out var sections, out var target)) + { + return; + } + context.RegisterRefactoring( + CodeAction.Create( + GetTitle(forSwitchExpression: false), + c => UpdateDocumentAsync(document, target, ifStatement, sections, analyzer.Features, convertToSwitchExpression: false, c), + SwitchStatementEquivalenceKey), + ifStatement.Span); + + if (analyzer.Supports(Feature.SwitchExpression) && + CanConvertToSwitchExpression(analyzer.Supports(Feature.OrPattern), sections)) + { context.RegisterRefactoring( CodeAction.Create( - GetTitle(forSwitchExpression: false), - c => UpdateDocumentAsync(document, target, ifStatement, sections, analyzer.Features, convertToSwitchExpression: false, c), - SwitchStatementEquivalenceKey), + GetTitle(forSwitchExpression: true), + c => UpdateDocumentAsync(document, target, ifStatement, sections, analyzer.Features, convertToSwitchExpression: true, c), + SwitchExpressionEquivalenceKey), ifStatement.Span); + } + } - if (analyzer.Supports(Feature.SwitchExpression) && - CanConvertToSwitchExpression(analyzer.Supports(Feature.OrPattern), sections)) - { - context.RegisterRefactoring( - CodeAction.Create( - GetTitle(forSwitchExpression: true), - c => UpdateDocumentAsync(document, target, ifStatement, sections, analyzer.Features, convertToSwitchExpression: true, c), - SwitchExpressionEquivalenceKey), - ifStatement.Span); - } + private bool ShouldOfferRefactoring( + [NotNullWhen(true)] TIfStatementSyntax? ifStatement, + SemanticModel semanticModel, + ISyntaxFactsService syntaxFactsService, + [NotNullWhen(true)] out Analyzer? analyzer, + [NotNullWhen(true)] out ImmutableArray sections, + [NotNullWhen(true)] out SyntaxNode? target) + { + analyzer = null; + sections = default; + target = null; + + if (ifStatement == null || ifStatement.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + { + return false; } - private bool ShouldOfferRefactoring( - [NotNullWhen(true)] TIfStatementSyntax? ifStatement, - SemanticModel semanticModel, - ISyntaxFactsService syntaxFactsService, - [NotNullWhen(true)] out Analyzer? analyzer, - [NotNullWhen(true)] out ImmutableArray sections, - [NotNullWhen(true)] out SyntaxNode? target) + var ifOperation = semanticModel.GetOperation(ifStatement); + if (ifOperation is not IConditionalOperation { Parent: IBlockOperation parentBlock }) { - analyzer = null; - sections = default; - target = null; + return false; + } - if (ifStatement == null || ifStatement.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) - { - return false; - } + var operations = parentBlock.Operations; + var index = operations.IndexOf(ifOperation); + analyzer = CreateAnalyzer(syntaxFactsService, ifStatement.SyntaxTree.Options); + (sections, target) = analyzer.AnalyzeIfStatementSequence(operations.AsSpan()[index..]); + if (sections.IsDefaultOrEmpty) + { + return false; + } - var ifOperation = semanticModel.GetOperation(ifStatement); - if (ifOperation is not IConditionalOperation { Parent: IBlockOperation parentBlock }) - { - return false; - } + // To prevent noisiness we don't offer this unless we're going to generate at least + // two switch labels. It can be quite annoying to basically have this offered + // on pretty much any simple 'if' like "if (a == 0)" or "if (x == null)". In these + // cases, the converted code just looks and feels worse, and it ends up causing the + // lightbulb to appear too much. + // + // This does mean that if someone has a simple if, and is about to add a lot more + // cases, and says to themselves "let me convert this to a switch first!", then they'll + // be out of luck. However, I believe the core value here is in taking existing large + // if-chains/checks and easily converting them over to a switch. So not offering the + // feature on simple if-statements seems like an acceptable compromise to take to ensure + // the overall user experience isn't degraded. + var labelCount = sections.Sum(section => section.Labels.IsDefault ? 1 : section.Labels.Length); + if (labelCount < 2) + { + return false; + } - var operations = parentBlock.Operations; - var index = operations.IndexOf(ifOperation); - analyzer = CreateAnalyzer(syntaxFactsService, ifStatement.SyntaxTree.Options); - (sections, target) = analyzer.AnalyzeIfStatementSequence(operations.AsSpan()[index..]); - if (sections.IsDefaultOrEmpty) - { - return false; - } + return true; + } + + private static bool CanConvertToSwitchExpression( + bool supportsOrPattern, ImmutableArray sections) + { + // There must be a default case for an exhaustive switch expression + if (!sections.Any(static section => section.Labels.IsDefault)) + return false; - // To prevent noisiness we don't offer this unless we're going to generate at least - // two switch labels. It can be quite annoying to basically have this offered - // on pretty much any simple 'if' like "if (a == 0)" or "if (x == null)". In these - // cases, the converted code just looks and feels worse, and it ends up causing the - // lightbulb to appear too much. - // - // This does mean that if someone has a simple if, and is about to add a lot more - // cases, and says to themselves "let me convert this to a switch first!", then they'll - // be out of luck. However, I believe the core value here is in taking existing large - // if-chains/checks and easily converting them over to a switch. So not offering the - // feature on simple if-statements seems like an acceptable compromise to take to ensure - // the overall user experience isn't degraded. - var labelCount = sections.Sum(section => section.Labels.IsDefault ? 1 : section.Labels.Length); - if (labelCount < 2) + // There must be at least one return statement + if (!sections.Any(static section => GetSwitchArmKind(section.Body) == OperationKind.Return)) + return false; + + if (!sections.All(section => CanConvertSectionForSwitchExpression(supportsOrPattern, section))) + return false; + + return true; + + static OperationKind GetSwitchArmKind(IOperation op) + { + switch (op) { - return false; + case IReturnOperation { ReturnedValue: { } }: + case IThrowOperation { Exception: { } }: + return op.Kind; + + case IBlockOperation { Operations: { Length: 1 } statements }: + return GetSwitchArmKind(statements[0]); } - return true; + return default; } - private static bool CanConvertToSwitchExpression( - bool supportsOrPattern, ImmutableArray sections) + static bool CanConvertSectionForSwitchExpression(bool supportsOrPattern, AnalyzedSwitchSection section) { - // There must be a default case for an exhaustive switch expression - if (!sections.Any(static section => section.Labels.IsDefault)) - return false; - - // There must be at least one return statement - if (!sections.Any(static section => GetSwitchArmKind(section.Body) == OperationKind.Return)) + // All arms must be convertible to a switch arm + if (GetSwitchArmKind(section.Body) == default) return false; - if (!sections.All(section => CanConvertSectionForSwitchExpression(supportsOrPattern, section))) - return false; + // Default label can trivially be converted to a switch arm. + if (section.Labels.IsDefault) + return true; - return true; + // Single label case can trivially be converted to a switch arm. + if (section.Labels.Length == 1) + return true; - static OperationKind GetSwitchArmKind(IOperation op) + if (section.Labels.Length == 0) { - switch (op) - { - case IReturnOperation { ReturnedValue: { } }: - case IThrowOperation { Exception: { } }: - return op.Kind; - - case IBlockOperation { Operations: { Length: 1 } statements }: - return GetSwitchArmKind(statements[0]); - } - - return default; + Debug.Fail("How did we not get any labels?"); + return false; } - static bool CanConvertSectionForSwitchExpression(bool supportsOrPattern, AnalyzedSwitchSection section) - { - // All arms must be convertible to a switch arm - if (GetSwitchArmKind(section.Body) == default) - return false; - - // Default label can trivially be converted to a switch arm. - if (section.Labels.IsDefault) - return true; - - // Single label case can trivially be converted to a switch arm. - if (section.Labels.Length == 1) - return true; - - if (section.Labels.Length == 0) - { - Debug.Fail("How did we not get any labels?"); - return false; - } - - // If there are two or more labels, we can support this as long as the language supports 'or' patterns - // and as long as no label has any guards. - return supportsOrPattern && section.Labels.All(label => label.Guards.IsDefaultOrEmpty); - } + // If there are two or more labels, we can support this as long as the language supports 'or' patterns + // and as long as no label has any guards. + return supportsOrPattern && section.Labels.All(label => label.Guards.IsDefaultOrEmpty); } + } - protected sealed override async Task FixAllAsync( - Document document, - ImmutableArray fixAllSpans, - SyntaxEditor editor, - CodeActionOptionsProvider optionsProvider, - string? equivalenceKey, - CancellationToken cancellationToken) + protected sealed override async Task FixAllAsync( + Document document, + ImmutableArray fixAllSpans, + SyntaxEditor editor, + CodeActionOptionsProvider optionsProvider, + string? equivalenceKey, + CancellationToken cancellationToken) + { + var convertToSwitchExpression = equivalenceKey == SwitchExpressionEquivalenceKey; + var syntaxFactsService = document.GetRequiredLanguageService(); + + // Get all the descendant if statements to process. + // NOTE: We need to realize the nodes with 'ToArray' call here + // to ensure we strongly hold onto the nodes so that 'TrackNodes' + // invoked below, which does tracking based off a ConditionalWeakTable, + // tracks the nodes for the entire duration of this method. + var ifStatements = editor.OriginalRoot.DescendantNodes().OfType().ToArray(); + + // We're going to be continually editing this tree. Track all the nodes we + // care about so we can find them across each edit. + document = document.WithSyntaxRoot(editor.OriginalRoot.TrackNodes(ifStatements)); + + // Process the if statements from outermost if to innermost if. + // Note that we need to process in this order because this refactoring + // can fold the inner if statements into the switch statement/expression + // generated for the outer if. + // For example, consider the following input: + // if (i == 3) + // { + // return 0; + // } + // else + // { + // if (i == 6) return 1; + // if (i == 7) return 1; + // return 0; + // } + // Processing the outer if leads to converting even the inner + // if statements into a single switch statement/expression: + // return i switch + // { + // 3 => 0, + // 6 => 1, + // 7 => 1, + // _ => 0 + // }; + // We would get an undesirable outcome if we processed the inner + // if statement before the outer if and converted the inner if + // statement into a separate switch statement/expression. + // + // Additionally, because of the fact that processing outer if + // can lead to inner if statements also getting removed from the + // converted code, we need to process all the if statements one + // at a time, recomputing the current root, document and semantic + // model after each if to switch conversion in the below loop. + // This is slightly slower compared to processing all if statements + // in parallel, but unavoidable given the prior example and requirements. + + foreach (var originalIfStatement in ifStatements) { - var convertToSwitchExpression = equivalenceKey == SwitchExpressionEquivalenceKey; - var syntaxFactsService = document.GetRequiredLanguageService(); - - // Get all the descendant if statements to process. - // NOTE: We need to realize the nodes with 'ToArray' call here - // to ensure we strongly hold onto the nodes so that 'TrackNodes' - // invoked below, which does tracking based off a ConditionalWeakTable, - // tracks the nodes for the entire duration of this method. - var ifStatements = editor.OriginalRoot.DescendantNodes().OfType().ToArray(); - - // We're going to be continually editing this tree. Track all the nodes we - // care about so we can find them across each edit. - document = document.WithSyntaxRoot(editor.OriginalRoot.TrackNodes(ifStatements)); - - // Process the if statements from outermost if to innermost if. - // Note that we need to process in this order because this refactoring - // can fold the inner if statements into the switch statement/expression - // generated for the outer if. - // For example, consider the following input: - // if (i == 3) - // { - // return 0; - // } - // else - // { - // if (i == 6) return 1; - // if (i == 7) return 1; - // return 0; - // } - // Processing the outer if leads to converting even the inner - // if statements into a single switch statement/expression: - // return i switch - // { - // 3 => 0, - // 6 => 1, - // 7 => 1, - // _ => 0 - // }; - // We would get an undesirable outcome if we processed the inner - // if statement before the outer if and converted the inner if - // statement into a separate switch statement/expression. - // - // Additionally, because of the fact that processing outer if - // can lead to inner if statements also getting removed from the - // converted code, we need to process all the if statements one - // at a time, recomputing the current root, document and semantic - // model after each if to switch conversion in the below loop. - // This is slightly slower compared to processing all if statements - // in parallel, but unavoidable given the prior example and requirements. - - foreach (var originalIfStatement in ifStatements) - { - // Only process if statements fully within a fixAllSpan - if (!fixAllSpans.Any(fixAllSpan => fixAllSpan.Contains(originalIfStatement.Span))) - continue; + // Only process if statements fully within a fixAllSpan + if (!fixAllSpans.Any(fixAllSpan => fixAllSpan.Contains(originalIfStatement.Span))) + continue; - // Get current root, if statement and semantic model. - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var ifStatement = root.GetCurrentNodes(originalIfStatement).SingleOrDefault(); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - // Check if the refactoring is applicable for this if statement. - if (!ShouldOfferRefactoring(ifStatement, semanticModel, syntaxFactsService, out var analyzer, out var sections, out var target)) - continue; + // Get current root, if statement and semantic model. + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var ifStatement = root.GetCurrentNodes(originalIfStatement).SingleOrDefault(); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - // When converting to switch expression, we need to perform an additional check to ensure this conversion is possible. - if (convertToSwitchExpression && !CanConvertToSwitchExpression(analyzer.Supports(Feature.OrPattern), sections)) - continue; + // Check if the refactoring is applicable for this if statement. + if (!ShouldOfferRefactoring(ifStatement, semanticModel, syntaxFactsService, out var analyzer, out var sections, out var target)) + continue; - document = await UpdateDocumentAsync(document, target, ifStatement, sections, - analyzer.Features, convertToSwitchExpression, cancellationToken).ConfigureAwait(false); - } + // When converting to switch expression, we need to perform an additional check to ensure this conversion is possible. + if (convertToSwitchExpression && !CanConvertToSwitchExpression(analyzer.Supports(Feature.OrPattern), sections)) + continue; - var updatedRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(editor.OriginalRoot, updatedRoot); + document = await UpdateDocumentAsync(document, target, ifStatement, sections, + analyzer.Features, convertToSwitchExpression, cancellationToken).ConfigureAwait(false); } + + var updatedRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(editor.OriginalRoot, updatedRoot); } } diff --git a/src/Features/Core/Portable/ConvertLinq/AbstractConvertLinqQueryToForEachProvider.cs b/src/Features/Core/Portable/ConvertLinq/AbstractConvertLinqQueryToForEachProvider.cs index 28935e16793de..c6fb9a5dbdef6 100644 --- a/src/Features/Core/Portable/ConvertLinq/AbstractConvertLinqQueryToForEachProvider.cs +++ b/src/Features/Core/Portable/ConvertLinq/AbstractConvertLinqQueryToForEachProvider.cs @@ -12,75 +12,74 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.ConvertLinq +namespace Microsoft.CodeAnalysis.ConvertLinq; + +internal abstract class AbstractConvertLinqQueryToForEachProvider : CodeRefactoringProvider + where TQueryExpression : SyntaxNode + where TStatement : SyntaxNode { - internal abstract class AbstractConvertLinqQueryToForEachProvider : CodeRefactoringProvider - where TQueryExpression : SyntaxNode - where TStatement : SyntaxNode - { - protected abstract string Title { get; } + protected abstract string Title { get; } - protected abstract bool TryConvert( - TQueryExpression queryExpression, - SemanticModel semanticModel, - ISemanticFactsService semanticFacts, - CancellationToken cancellationToken, - out DocumentUpdateInfo documentUpdate); + protected abstract bool TryConvert( + TQueryExpression queryExpression, + SemanticModel semanticModel, + ISemanticFactsService semanticFacts, + CancellationToken cancellationToken, + out DocumentUpdateInfo documentUpdate); - protected abstract Task FindNodeToRefactorAsync(CodeRefactoringContext context); + protected abstract Task FindNodeToRefactorAsync(CodeRefactoringContext context); - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, _, cancellationToken) = context; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var queryExpression = await FindNodeToRefactorAsync(context).ConfigureAwait(false); + if (queryExpression == null) { - var (document, _, cancellationToken) = context; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + return; + } - var queryExpression = await FindNodeToRefactorAsync(context).ConfigureAwait(false); - if (queryExpression == null) - { - return; - } + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var semanticFacts = document.GetRequiredLanguageService(); + if (TryConvert(queryExpression, semanticModel, semanticFacts, cancellationToken, out var documentUpdateInfo)) + { + context.RegisterRefactoring( + CodeAction.Create( + Title, + c => Task.FromResult(document.WithSyntaxRoot(documentUpdateInfo.UpdateRoot(root))), + Title), + queryExpression.Span); + } + } - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var semanticFacts = document.GetRequiredLanguageService(); - if (TryConvert(queryExpression, semanticModel, semanticFacts, cancellationToken, out var documentUpdateInfo)) - { - context.RegisterRefactoring( - CodeAction.Create( - Title, - c => Task.FromResult(document.WithSyntaxRoot(documentUpdateInfo.UpdateRoot(root))), - Title), - queryExpression.Span); - } + /// + /// Handles information about updating the document with the refactoring. + /// + internal sealed class DocumentUpdateInfo(TStatement source, IEnumerable destinations) + { + public readonly TStatement Source = source; + public readonly ImmutableArray Destinations = ImmutableArray.CreateRange(destinations); + + public DocumentUpdateInfo(TStatement source, TStatement destination) : this(source, [destination]) + { } /// - /// Handles information about updating the document with the refactoring. + /// Updates the root of the document with the document update. /// - internal sealed class DocumentUpdateInfo(TStatement source, IEnumerable destinations) + public SyntaxNode UpdateRoot(SyntaxNode root) { - public readonly TStatement Source = source; - public readonly ImmutableArray Destinations = ImmutableArray.CreateRange(destinations); - - public DocumentUpdateInfo(TStatement source, TStatement destination) : this(source, [destination]) + // There are two overloads of ReplaceNode: one accepts a collection of nodes and another a single node. + // If we replace a node, e.g. in statement of an if-statement(without block), + // it cannot replace it with collection even if there is a just 1 element in it. + if (Destinations.Length == 1) { + return root.ReplaceNode(Source, Destinations[0]); } - - /// - /// Updates the root of the document with the document update. - /// - public SyntaxNode UpdateRoot(SyntaxNode root) + else { - // There are two overloads of ReplaceNode: one accepts a collection of nodes and another a single node. - // If we replace a node, e.g. in statement of an if-statement(without block), - // it cannot replace it with collection even if there is a just 1 element in it. - if (Destinations.Length == 1) - { - return root.ReplaceNode(Source, Destinations[0]); - } - else - { - return root.ReplaceNode(Source, Destinations); - } + return root.ReplaceNode(Source, Destinations); } } } diff --git a/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractConvertForEachToLinqQueryProvider.cs b/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractConvertForEachToLinqQueryProvider.cs index f3ffe0198f334..7f297eb082dc7 100644 --- a/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractConvertForEachToLinqQueryProvider.cs +++ b/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractConvertForEachToLinqQueryProvider.cs @@ -11,177 +11,176 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.ConvertLinq.ConvertForEachToLinqQuery +namespace Microsoft.CodeAnalysis.ConvertLinq.ConvertForEachToLinqQuery; + +internal abstract class AbstractConvertForEachToLinqQueryProvider : CodeRefactoringProvider + where TForEachStatement : TStatement + where TStatement : SyntaxNode { - internal abstract class AbstractConvertForEachToLinqQueryProvider : CodeRefactoringProvider - where TForEachStatement : TStatement - where TStatement : SyntaxNode + /// + /// Parses the forEachStatement until a child node cannot be converted into a query clause. + /// + /// + /// + /// 1) extended nodes that can be converted into clauses + /// 2) identifiers introduced in nodes that can be converted + /// 3) statements that cannot be converted into clauses + /// 4) trailing comments to be added to the end + /// + protected abstract ForEachInfo CreateForEachInfo( + TForEachStatement forEachStatement, SemanticModel semanticModel, bool convertLocalDeclarations); + + /// + /// Tries to build a specific converter that covers e.g. Count, ToList, yield return or other similar cases. + /// + protected abstract bool TryBuildSpecificConverter( + ForEachInfo forEachInfo, + SemanticModel semanticModel, + TStatement statementCannotBeConverted, + CancellationToken cancellationToken, + [NotNullWhen(true)] out IConverter? converter); + + /// + /// Creates a default converter where foreach is joined with some children statements but other children statements are kept unmodified. + /// Example: + /// foreach(...) + /// { + /// if (condition) + /// { + /// doSomething(); + /// } + /// } + /// is converted to + /// foreach(... where condition) + /// { + /// doSomething(); + /// } + /// + protected abstract IConverter CreateDefaultConverter( + ForEachInfo forEachInfo); + + protected abstract SyntaxNode AddLinqUsing( + IConverter converter, SemanticModel semanticModel, SyntaxNode root); + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - /// - /// Parses the forEachStatement until a child node cannot be converted into a query clause. - /// - /// - /// - /// 1) extended nodes that can be converted into clauses - /// 2) identifiers introduced in nodes that can be converted - /// 3) statements that cannot be converted into clauses - /// 4) trailing comments to be added to the end - /// - protected abstract ForEachInfo CreateForEachInfo( - TForEachStatement forEachStatement, SemanticModel semanticModel, bool convertLocalDeclarations); - - /// - /// Tries to build a specific converter that covers e.g. Count, ToList, yield return or other similar cases. - /// - protected abstract bool TryBuildSpecificConverter( - ForEachInfo forEachInfo, - SemanticModel semanticModel, - TStatement statementCannotBeConverted, - CancellationToken cancellationToken, - [NotNullWhen(true)] out IConverter? converter); - - /// - /// Creates a default converter where foreach is joined with some children statements but other children statements are kept unmodified. - /// Example: - /// foreach(...) - /// { - /// if (condition) - /// { - /// doSomething(); - /// } - /// } - /// is converted to - /// foreach(... where condition) - /// { - /// doSomething(); - /// } - /// - protected abstract IConverter CreateDefaultConverter( - ForEachInfo forEachInfo); - - protected abstract SyntaxNode AddLinqUsing( - IConverter converter, SemanticModel semanticModel, SyntaxNode root); - - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + var (document, _, cancellationToken) = context; + + var forEachStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (forEachStatement == null) + { + return; + } + + if (forEachStatement.ContainsDiagnostics) + { + return; + } + + // Do not try to refactor queries with conditional compilation in them. + if (forEachStatement.ContainsDirectives) + { + return; + } + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + if (!TryBuildConverter(forEachStatement, semanticModel, convertLocalDeclarations: true, cancellationToken, out var queryConverter)) + { + return; + } + + if (semanticModel.GetDiagnostics(forEachStatement.Span, cancellationToken) + .Any(static diagnostic => diagnostic.DefaultSeverity == DiagnosticSeverity.Error)) + { + return; + } + + // Offer refactoring to convert foreach to LINQ query expression. For example: + // + // INPUT: + // foreach (var n1 in c1) + // foreach (var n2 in c2) + // if (n1 > n2) + // yield return n1 + n2; + // + // OUTPUT: + // from n1 in c1 + // from n2 in c2 + // where n1 > n2 + // select n1 + n2 + // + context.RegisterRefactoring( + CodeAction.Create( + FeaturesResources.Convert_to_linq, + c => ApplyConversionAsync(queryConverter, document, convertToQuery: true, c), + nameof(FeaturesResources.Convert_to_linq)), + forEachStatement.Span); + + // Offer refactoring to convert foreach to LINQ invocation expression. For example: + // + // INPUT: + // foreach (var n1 in c1) + // foreach (var n2 in c2) + // if (n1 > n2) + // yield return n1 + n2; + // + // OUTPUT: + // c1.SelectMany(n1 => c2.Where(n2 => n1 > n2).Select(n2 => n1 + n2)) + // + // CONSIDER: Currently, we do not handle foreach statements with a local declaration statement for Convert_to_linq refactoring, + // though we do handle them for the Convert_to_query refactoring above. + // In future, we can consider supporting them by creating anonymous objects/tuples wrapping the local declarations. + if (TryBuildConverter(forEachStatement, semanticModel, convertLocalDeclarations: false, cancellationToken, out var linqConverter)) { - var (document, _, cancellationToken) = context; - - var forEachStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); - if (forEachStatement == null) - { - return; - } - - if (forEachStatement.ContainsDiagnostics) - { - return; - } - - // Do not try to refactor queries with conditional compilation in them. - if (forEachStatement.ContainsDirectives) - { - return; - } - - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - if (!TryBuildConverter(forEachStatement, semanticModel, convertLocalDeclarations: true, cancellationToken, out var queryConverter)) - { - return; - } - - if (semanticModel.GetDiagnostics(forEachStatement.Span, cancellationToken) - .Any(static diagnostic => diagnostic.DefaultSeverity == DiagnosticSeverity.Error)) - { - return; - } - - // Offer refactoring to convert foreach to LINQ query expression. For example: - // - // INPUT: - // foreach (var n1 in c1) - // foreach (var n2 in c2) - // if (n1 > n2) - // yield return n1 + n2; - // - // OUTPUT: - // from n1 in c1 - // from n2 in c2 - // where n1 > n2 - // select n1 + n2 - // context.RegisterRefactoring( CodeAction.Create( - FeaturesResources.Convert_to_linq, - c => ApplyConversionAsync(queryConverter, document, convertToQuery: true, c), - nameof(FeaturesResources.Convert_to_linq)), + FeaturesResources.Convert_to_linq_call_form, + c => ApplyConversionAsync(linqConverter, document, convertToQuery: false, c), + nameof(FeaturesResources.Convert_to_linq_call_form)), forEachStatement.Span); - - // Offer refactoring to convert foreach to LINQ invocation expression. For example: - // - // INPUT: - // foreach (var n1 in c1) - // foreach (var n2 in c2) - // if (n1 > n2) - // yield return n1 + n2; - // - // OUTPUT: - // c1.SelectMany(n1 => c2.Where(n2 => n1 > n2).Select(n2 => n1 + n2)) - // - // CONSIDER: Currently, we do not handle foreach statements with a local declaration statement for Convert_to_linq refactoring, - // though we do handle them for the Convert_to_query refactoring above. - // In future, we can consider supporting them by creating anonymous objects/tuples wrapping the local declarations. - if (TryBuildConverter(forEachStatement, semanticModel, convertLocalDeclarations: false, cancellationToken, out var linqConverter)) - { - context.RegisterRefactoring( - CodeAction.Create( - FeaturesResources.Convert_to_linq_call_form, - c => ApplyConversionAsync(linqConverter, document, convertToQuery: false, c), - nameof(FeaturesResources.Convert_to_linq_call_form)), - forEachStatement.Span); - } } + } - private Task ApplyConversionAsync( - IConverter converter, - Document document, - bool convertToQuery, - CancellationToken cancellationToken) + private Task ApplyConversionAsync( + IConverter converter, + Document document, + bool convertToQuery, + CancellationToken cancellationToken) + { + var editor = new SyntaxEditor(converter.ForEachInfo.SemanticModel.SyntaxTree.GetRoot(cancellationToken), document.Project.Solution.Services); + converter.Convert(editor, convertToQuery, cancellationToken); + var newRoot = editor.GetChangedRoot(); + var rootWithLinqUsing = AddLinqUsing(converter, converter.ForEachInfo.SemanticModel, newRoot); + return Task.FromResult(document.WithSyntaxRoot(rootWithLinqUsing)); + } + + /// + /// Tries to build either a specific or a default converter. + /// + private bool TryBuildConverter( + TForEachStatement forEachStatement, + SemanticModel semanticModel, + bool convertLocalDeclarations, + CancellationToken cancellationToken, + [NotNullWhen(true)] out IConverter? converter) + { + var forEachInfo = CreateForEachInfo(forEachStatement, semanticModel, convertLocalDeclarations); + + if (forEachInfo.Statements.Length == 1 && + TryBuildSpecificConverter(forEachInfo, semanticModel, forEachInfo.Statements.Single(), cancellationToken, out converter)) { - var editor = new SyntaxEditor(converter.ForEachInfo.SemanticModel.SyntaxTree.GetRoot(cancellationToken), document.Project.Solution.Services); - converter.Convert(editor, convertToQuery, cancellationToken); - var newRoot = editor.GetChangedRoot(); - var rootWithLinqUsing = AddLinqUsing(converter, converter.ForEachInfo.SemanticModel, newRoot); - return Task.FromResult(document.WithSyntaxRoot(rootWithLinqUsing)); + return true; } - /// - /// Tries to build either a specific or a default converter. - /// - private bool TryBuildConverter( - TForEachStatement forEachStatement, - SemanticModel semanticModel, - bool convertLocalDeclarations, - CancellationToken cancellationToken, - [NotNullWhen(true)] out IConverter? converter) + // No sense to convert a single foreach to foreach over the same collection. + if (forEachInfo.ConvertingExtendedNodes.Length >= 1) { - var forEachInfo = CreateForEachInfo(forEachStatement, semanticModel, convertLocalDeclarations); - - if (forEachInfo.Statements.Length == 1 && - TryBuildSpecificConverter(forEachInfo, semanticModel, forEachInfo.Statements.Single(), cancellationToken, out converter)) - { - return true; - } - - // No sense to convert a single foreach to foreach over the same collection. - if (forEachInfo.ConvertingExtendedNodes.Length >= 1) - { - converter = CreateDefaultConverter(forEachInfo); - return true; - } - - converter = null; - return false; + converter = CreateDefaultConverter(forEachInfo); + return true; } + + converter = null; + return false; } } diff --git a/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/ExtendedSyntaxNode.cs b/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/ExtendedSyntaxNode.cs index a0d7548ee2a59..b410f7c939fdb 100644 --- a/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/ExtendedSyntaxNode.cs +++ b/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/ExtendedSyntaxNode.cs @@ -6,25 +6,24 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.ConvertLinq.ConvertForEachToLinqQuery +namespace Microsoft.CodeAnalysis.ConvertLinq.ConvertForEachToLinqQuery; + +internal readonly struct ExtendedSyntaxNode( + SyntaxNode node, + IEnumerable extraLeadingComments, + IEnumerable extraTrailingComments) { - internal readonly struct ExtendedSyntaxNode( - SyntaxNode node, - IEnumerable extraLeadingComments, - IEnumerable extraTrailingComments) - { - public SyntaxNode Node { get; } = node; + public SyntaxNode Node { get; } = node; - public ImmutableArray ExtraLeadingComments { get; } = extraLeadingComments.ToImmutableArray(); + public ImmutableArray ExtraLeadingComments { get; } = extraLeadingComments.ToImmutableArray(); - public ImmutableArray ExtraTrailingComments { get; } = extraTrailingComments.ToImmutableArray(); + public ImmutableArray ExtraTrailingComments { get; } = extraTrailingComments.ToImmutableArray(); - public ExtendedSyntaxNode( - SyntaxNode node, - IEnumerable extraLeadingTokens, - IEnumerable extraTrailingTokens) - : this(node, extraLeadingTokens.GetTrivia(), extraTrailingTokens.GetTrivia()) - { - } + public ExtendedSyntaxNode( + SyntaxNode node, + IEnumerable extraLeadingTokens, + IEnumerable extraTrailingTokens) + : this(node, extraLeadingTokens.GetTrivia(), extraTrailingTokens.GetTrivia()) + { } } diff --git a/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/ForEachInfo.cs b/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/ForEachInfo.cs index d4b37af5b6994..492de0fb3fcf0 100644 --- a/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/ForEachInfo.cs +++ b/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/ForEachInfo.cs @@ -6,29 +6,28 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.ConvertLinq.ConvertForEachToLinqQuery +namespace Microsoft.CodeAnalysis.ConvertLinq.ConvertForEachToLinqQuery; + +internal readonly struct ForEachInfo( + TForEachStatement forEachStatement, + SemanticModel semanticModel, + ImmutableArray convertingExtendedNodes, + ImmutableArray identifiers, + ImmutableArray statements, + ImmutableArray leadingTokens, + ImmutableArray trailingTokens) { - internal readonly struct ForEachInfo( - TForEachStatement forEachStatement, - SemanticModel semanticModel, - ImmutableArray convertingExtendedNodes, - ImmutableArray identifiers, - ImmutableArray statements, - ImmutableArray leadingTokens, - ImmutableArray trailingTokens) - { - public TForEachStatement ForEachStatement { get; } = forEachStatement; + public TForEachStatement ForEachStatement { get; } = forEachStatement; - public SemanticModel SemanticModel { get; } = semanticModel; + public SemanticModel SemanticModel { get; } = semanticModel; - public ImmutableArray ConvertingExtendedNodes { get; } = convertingExtendedNodes; + public ImmutableArray ConvertingExtendedNodes { get; } = convertingExtendedNodes; - public ImmutableArray Identifiers { get; } = identifiers; + public ImmutableArray Identifiers { get; } = identifiers; - public ImmutableArray Statements { get; } = statements; + public ImmutableArray Statements { get; } = statements; - public ImmutableArray LeadingTokens { get; } = leadingTokens; + public ImmutableArray LeadingTokens { get; } = leadingTokens; - public ImmutableArray TrailingTokens { get; } = trailingTokens; - } + public ImmutableArray TrailingTokens { get; } = trailingTokens; } diff --git a/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/IConverter.cs b/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/IConverter.cs index 3dcf31255bad7..d05e6aca201c0 100644 --- a/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/IConverter.cs +++ b/src/Features/Core/Portable/ConvertLinq/ConvertForEachToLinqQuery/IConverter.cs @@ -5,12 +5,11 @@ using System.Threading; using Microsoft.CodeAnalysis.Editing; -namespace Microsoft.CodeAnalysis.ConvertLinq.ConvertForEachToLinqQuery +namespace Microsoft.CodeAnalysis.ConvertLinq.ConvertForEachToLinqQuery; + +internal interface IConverter { - internal interface IConverter - { - ForEachInfo ForEachInfo { get; } + ForEachInfo ForEachInfo { get; } - void Convert(SyntaxEditor editor, bool convertToQuery, CancellationToken cancellationToken); - } + void Convert(SyntaxEditor editor, bool convertToQuery, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/ConvertToInterpolatedString/AbstractConvertPlaceholderToInterpolatedStringRefactoringProvider.cs b/src/Features/Core/Portable/ConvertToInterpolatedString/AbstractConvertPlaceholderToInterpolatedStringRefactoringProvider.cs index a55a0ed7fe36e..6f6906bec80e7 100644 --- a/src/Features/Core/Portable/ConvertToInterpolatedString/AbstractConvertPlaceholderToInterpolatedStringRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertToInterpolatedString/AbstractConvertPlaceholderToInterpolatedStringRefactoringProvider.cs @@ -17,310 +17,309 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ConvertToInterpolatedString +namespace Microsoft.CodeAnalysis.ConvertToInterpolatedString; + +internal abstract class AbstractConvertPlaceholderToInterpolatedStringRefactoringProvider< + TExpressionSyntax, + TLiteralExpressionSyntax, + TInvocationExpressionSyntax, + TInterpolatedStringExpressionSyntax, + TArgumentSyntax, + TArgumentListExpressionSyntax, + TInterpolationSyntax> : CodeRefactoringProvider + where TExpressionSyntax : SyntaxNode + where TLiteralExpressionSyntax : TExpressionSyntax + where TInvocationExpressionSyntax : TExpressionSyntax + where TInterpolatedStringExpressionSyntax : TExpressionSyntax + where TArgumentSyntax : SyntaxNode + where TArgumentListExpressionSyntax : SyntaxNode + where TInterpolationSyntax : SyntaxNode { - internal abstract class AbstractConvertPlaceholderToInterpolatedStringRefactoringProvider< - TExpressionSyntax, - TLiteralExpressionSyntax, - TInvocationExpressionSyntax, - TInterpolatedStringExpressionSyntax, - TArgumentSyntax, - TArgumentListExpressionSyntax, - TInterpolationSyntax> : CodeRefactoringProvider - where TExpressionSyntax : SyntaxNode - where TLiteralExpressionSyntax : TExpressionSyntax - where TInvocationExpressionSyntax : TExpressionSyntax - where TInterpolatedStringExpressionSyntax : TExpressionSyntax - where TArgumentSyntax : SyntaxNode - where TArgumentListExpressionSyntax : SyntaxNode - where TInterpolationSyntax : SyntaxNode + protected abstract TExpressionSyntax ParseExpression(string text); + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - protected abstract TExpressionSyntax ParseExpression(string text); + var (document, span, cancellationToken) = context; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, span, cancellationToken) = context; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var stringType = semanticModel.Compilation.GetSpecialType(SpecialType.System_String); + if (stringType.IsErrorType()) + return; - var stringType = semanticModel.Compilation.GetSpecialType(SpecialType.System_String); - if (stringType.IsErrorType()) - return; + var syntaxFacts = document.GetRequiredLanguageService(); - var syntaxFacts = document.GetRequiredLanguageService(); + var (invocation, placeholderArgument) = await TryFindInvocationAsync().ConfigureAwait(false); + if (invocation is null || placeholderArgument is null) + return; - var (invocation, placeholderArgument) = await TryFindInvocationAsync().ConfigureAwait(false); - if (invocation is null || placeholderArgument is null) - return; + var placeholderExpression = syntaxFacts.GetExpressionOfArgument(placeholderArgument); + var stringToken = placeholderExpression.GetFirstToken(); - var placeholderExpression = syntaxFacts.GetExpressionOfArgument(placeholderArgument); - var stringToken = placeholderExpression.GetFirstToken(); + // don't offer if the string argument has errors in it, or if converting it to an interpolated string creates errors. + if (stringToken.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + return; - // don't offer if the string argument has errors in it, or if converting it to an interpolated string creates errors. - if (stringToken.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + // Not supported if there are any omitted arguments following the placeholder. + var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(invocation); + var placeholderIndex = arguments.IndexOf(placeholderArgument); + Contract.ThrowIfTrue(placeholderIndex < 0); + for (var i = placeholderIndex + 1; i < arguments.Count; i++) + { + var argument = arguments[i]; + if (syntaxFacts.GetExpressionOfArgument(argument) is null) return; - // Not supported if there are any omitted arguments following the placeholder. - var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(invocation); - var placeholderIndex = arguments.IndexOf(placeholderArgument); - Contract.ThrowIfTrue(placeholderIndex < 0); - for (var i = placeholderIndex + 1; i < arguments.Count; i++) - { - var argument = arguments[i]; - if (syntaxFacts.GetExpressionOfArgument(argument) is null) - return; - - if (syntaxFacts.GetRefKindOfArgument(argument) != RefKind.None) - return; - } - - if (semanticModel.GetSymbolInfo(invocation, cancellationToken).GetAnySymbol() is not IMethodSymbol invocationSymbol) + if (syntaxFacts.GetRefKindOfArgument(argument) != RefKind.None) return; + } - // If the user is actually passing an array to a params argument, we can't change this to be an interpolated string. - if (invocationSymbol.IsParams()) - { - var lastArgument = syntaxFacts.GetArgumentsOfInvocationExpression(invocation).Last(); - var lastArgumentType = semanticModel.GetTypeInfo(syntaxFacts.GetExpressionOfArgument(lastArgument), cancellationToken).Type; - if (lastArgumentType is IArrayTypeSymbol) - return; - } - - // if the user is explicitly passing in a CultureInfo, don't offer as it's likely they want specialized - // formatting for the values. - foreach (var argument in arguments) - { - var type = semanticModel.GetTypeInfo(syntaxFacts.GetExpressionOfArgument(argument)).Type; - if (type is { Name: nameof(CultureInfo), ContainingNamespace.Name: nameof(System.Globalization), ContainingNamespace.ContainingNamespace.Name: nameof(System) }) - return; - } + if (semanticModel.GetSymbolInfo(invocation, cancellationToken).GetAnySymbol() is not IMethodSymbol invocationSymbol) + return; - if (ParseExpression("$" + stringToken.Text) is not TInterpolatedStringExpressionSyntax interpolatedString) + // If the user is actually passing an array to a params argument, we can't change this to be an interpolated string. + if (invocationSymbol.IsParams()) + { + var lastArgument = syntaxFacts.GetArgumentsOfInvocationExpression(invocation).Last(); + var lastArgumentType = semanticModel.GetTypeInfo(syntaxFacts.GetExpressionOfArgument(lastArgument), cancellationToken).Type; + if (lastArgumentType is IArrayTypeSymbol) return; + } - if (interpolatedString.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + // if the user is explicitly passing in a CultureInfo, don't offer as it's likely they want specialized + // formatting for the values. + foreach (var argument in arguments) + { + var type = semanticModel.GetTypeInfo(syntaxFacts.GetExpressionOfArgument(argument)).Type; + if (type is { Name: nameof(CultureInfo), ContainingNamespace.Name: nameof(System.Globalization), ContainingNamespace.ContainingNamespace.Name: nameof(System) }) return; + } - var shouldReplaceInvocation = invocationSymbol is { ContainingType.SpecialType: SpecialType.System_String, Name: nameof(string.Format) }; - - context.RegisterRefactoring( - CodeAction.Create( - FeaturesResources.Convert_to_interpolated_string, - cancellationToken => CreateInterpolatedStringAsync( - document, invocation, placeholderArgument, invocationSymbol, interpolatedString, shouldReplaceInvocation, cancellationToken), - nameof(FeaturesResources.Convert_to_interpolated_string)), - invocation.Span); + if (ParseExpression("$" + stringToken.Text) is not TInterpolatedStringExpressionSyntax interpolatedString) + return; + if (interpolatedString.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) return; - async Task<(TInvocationExpressionSyntax? invocation, TArgumentSyntax? placeholderArgument)> TryFindInvocationAsync() - { - // If selection is empty there can be multiple matching invocations (we can be deep in), need to go through all of them - var invocations = await document.GetRelevantNodesAsync(span, cancellationToken).ConfigureAwait(false); - foreach (var invocation in invocations) - { - var placeholderArgument = FindValidPlaceholderArgument(invocation); - if (placeholderArgument != null) - return (invocation, placeholderArgument); - } + var shouldReplaceInvocation = invocationSymbol is { ContainingType.SpecialType: SpecialType.System_String, Name: nameof(string.Format) }; - { - // User selected a single argument of the invocation (expression / format string) instead of the whole invocation. - var selectedArgument = await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); - var invocation = selectedArgument?.Parent?.Parent as TInvocationExpressionSyntax; - var placeholderArgument = FindValidPlaceholderArgument(invocation); - if (placeholderArgument != null) - return (invocation, placeholderArgument); - } + context.RegisterRefactoring( + CodeAction.Create( + FeaturesResources.Convert_to_interpolated_string, + cancellationToken => CreateInterpolatedStringAsync( + document, invocation, placeholderArgument, invocationSymbol, interpolatedString, shouldReplaceInvocation, cancellationToken), + nameof(FeaturesResources.Convert_to_interpolated_string)), + invocation.Span); - { - // User selected the whole argument list: string format with placeholders plus all expressions - var argumentList = await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); - var invocation = argumentList?.Parent as TInvocationExpressionSyntax; - var placeholderArgument = FindValidPlaceholderArgument(invocation); - if (placeholderArgument != null) - return (invocation, placeholderArgument); - } + return; - return default; + async Task<(TInvocationExpressionSyntax? invocation, TArgumentSyntax? placeholderArgument)> TryFindInvocationAsync() + { + // If selection is empty there can be multiple matching invocations (we can be deep in), need to go through all of them + var invocations = await document.GetRelevantNodesAsync(span, cancellationToken).ConfigureAwait(false); + foreach (var invocation in invocations) + { + var placeholderArgument = FindValidPlaceholderArgument(invocation); + if (placeholderArgument != null) + return (invocation, placeholderArgument); + } + + { + // User selected a single argument of the invocation (expression / format string) instead of the whole invocation. + var selectedArgument = await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); + var invocation = selectedArgument?.Parent?.Parent as TInvocationExpressionSyntax; + var placeholderArgument = FindValidPlaceholderArgument(invocation); + if (placeholderArgument != null) + return (invocation, placeholderArgument); } - TArgumentSyntax? FindValidPlaceholderArgument(TInvocationExpressionSyntax? invocation) { - if (invocation != null) + // User selected the whole argument list: string format with placeholders plus all expressions + var argumentList = await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); + var invocation = argumentList?.Parent as TInvocationExpressionSyntax; + var placeholderArgument = FindValidPlaceholderArgument(invocation); + if (placeholderArgument != null) + return (invocation, placeholderArgument); + } + + return default; + } + + TArgumentSyntax? FindValidPlaceholderArgument(TInvocationExpressionSyntax? invocation) + { + if (invocation != null) + { + // look for a string argument containing `"...{0}..."`, followed by more arguments. + var arguments = (SeparatedSyntaxList)syntaxFacts.GetArgumentsOfInvocationExpression(invocation); + for (int i = 0, n = arguments.Count - 1; i < n; i++) { - // look for a string argument containing `"...{0}..."`, followed by more arguments. - var arguments = (SeparatedSyntaxList)syntaxFacts.GetArgumentsOfInvocationExpression(invocation); - for (int i = 0, n = arguments.Count - 1; i < n; i++) + var argument = arguments[i]; + var expression = syntaxFacts.GetExpressionOfArgument(argument); + if (syntaxFacts.IsStringLiteralExpression(expression)) { - var argument = arguments[i]; - var expression = syntaxFacts.GetExpressionOfArgument(argument); - if (syntaxFacts.IsStringLiteralExpression(expression)) + var remainingArgCount = arguments.Count - i - 1; + Debug.Assert(remainingArgCount > 0); + var stringLiteralText = expression.GetFirstToken().Text; + if (stringLiteralText.Contains('{') && stringLiteralText.Contains('}')) { - var remainingArgCount = arguments.Count - i - 1; - Debug.Assert(remainingArgCount > 0); - var stringLiteralText = expression.GetFirstToken().Text; - if (stringLiteralText.Contains('{') && stringLiteralText.Contains('}')) - { - if (IsValidPlaceholderArgument(stringLiteralText, remainingArgCount)) - return (TArgumentSyntax)argument; - } + if (IsValidPlaceholderArgument(stringLiteralText, remainingArgCount)) + return (TArgumentSyntax)argument; } } } - - return null; } - bool IsValidPlaceholderArgument(string stringLiteralText, int remainingArgCount) - { - // See how many arguments follow the `"...{0}..."`. We have to have a {0}, {1}, ... {N} part in the - // string for each of them. Note, those could be in any order. - for (var i = 0; i < remainingArgCount; i++) - { - var indexString = i.ToString(CultureInfo.InvariantCulture); - if (!ContainsIndex(stringLiteralText, indexString)) - return false; - } + return null; + } - return true; + bool IsValidPlaceholderArgument(string stringLiteralText, int remainingArgCount) + { + // See how many arguments follow the `"...{0}..."`. We have to have a {0}, {1}, ... {N} part in the + // string for each of them. Note, those could be in any order. + for (var i = 0; i < remainingArgCount; i++) + { + var indexString = i.ToString(CultureInfo.InvariantCulture); + if (!ContainsIndex(stringLiteralText, indexString)) + return false; } - bool ContainsIndex(string stringLiteralText, string indexString) - { - var currentLocation = -1; - while (true) - { - currentLocation = stringLiteralText.IndexOf(indexString, currentLocation + 1); - if (currentLocation < 0) - return false; + return true; + } - if (currentLocation + 1 >= stringLiteralText.Length) - return false; + bool ContainsIndex(string stringLiteralText, string indexString) + { + var currentLocation = -1; + while (true) + { + currentLocation = stringLiteralText.IndexOf(indexString, currentLocation + 1); + if (currentLocation < 0) + return false; - // while we found the number, it was followed by another number. so this isn't a valid match. Keep looking. - if (stringLiteralText[currentLocation + 1] is >= '0' and <= '9') - continue; + if (currentLocation + 1 >= stringLiteralText.Length) + return false; - // now check if the number is preceeded by whitespace and a '{' + // while we found the number, it was followed by another number. so this isn't a valid match. Keep looking. + if (stringLiteralText[currentLocation + 1] is >= '0' and <= '9') + continue; - var lookbackLocation = currentLocation - 1; - while (lookbackLocation > 0 && char.IsWhiteSpace(stringLiteralText[lookbackLocation])) - lookbackLocation--; + // now check if the number is preceeded by whitespace and a '{' - if (stringLiteralText[lookbackLocation] != '{') - { - // not a match, keep looking. - continue; - } + var lookbackLocation = currentLocation - 1; + while (lookbackLocation > 0 && char.IsWhiteSpace(stringLiteralText[lookbackLocation])) + lookbackLocation--; - // success. we found `{ N` - return true; + if (stringLiteralText[lookbackLocation] != '{') + { + // not a match, keep looking. + continue; } + + // success. we found `{ N` + return true; } } + } - private static async Task CreateInterpolatedStringAsync( - Document document, - TInvocationExpressionSyntax invocation, - TArgumentSyntax placeholderArgument, - IMethodSymbol invocationSymbol, - TInterpolatedStringExpressionSyntax interpolatedString, - bool shouldReplaceInvocation, - CancellationToken cancellationToken) - { - var syntaxFacts = document.GetRequiredLanguageService(); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + private static async Task CreateInterpolatedStringAsync( + Document document, + TInvocationExpressionSyntax invocation, + TArgumentSyntax placeholderArgument, + IMethodSymbol invocationSymbol, + TInterpolatedStringExpressionSyntax interpolatedString, + bool shouldReplaceInvocation, + CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var arguments = (SeparatedSyntaxList)syntaxFacts.GetArgumentsOfInvocationExpression(invocation); - var literalExpression = (TLiteralExpressionSyntax?)syntaxFacts.GetExpressionOfArgument(placeholderArgument); - Contract.ThrowIfNull(literalExpression); + var arguments = (SeparatedSyntaxList)syntaxFacts.GetArgumentsOfInvocationExpression(invocation); + var literalExpression = (TLiteralExpressionSyntax?)syntaxFacts.GetExpressionOfArgument(placeholderArgument); + Contract.ThrowIfNull(literalExpression); - var syntaxGenerator = document.GetRequiredLanguageService(); + var syntaxGenerator = document.GetRequiredLanguageService(); - var newInterpolatedString = - InsertArgumentsIntoInterpolatedString( - ExpandArgumentExpressions( - GetReorderedArgumentsAfterPlaceholderArgument())); + var newInterpolatedString = + InsertArgumentsIntoInterpolatedString( + ExpandArgumentExpressions( + GetReorderedArgumentsAfterPlaceholderArgument())); - var replacementNode = shouldReplaceInvocation - ? newInterpolatedString - : syntaxGenerator.InvocationExpression( - syntaxFacts.GetExpressionOfInvocationExpression(invocation), - newInterpolatedString); + var replacementNode = shouldReplaceInvocation + ? newInterpolatedString + : syntaxGenerator.InvocationExpression( + syntaxFacts.GetExpressionOfInvocationExpression(invocation), + newInterpolatedString); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var newRoot = root.ReplaceNode(invocation, replacementNode.WithTriviaFrom(invocation)); - return document.WithSyntaxRoot(newRoot); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = root.ReplaceNode(invocation, replacementNode.WithTriviaFrom(invocation)); + return document.WithSyntaxRoot(newRoot); - ImmutableArray GetReorderedArgumentsAfterPlaceholderArgument() - { - var placeholderIndex = arguments.IndexOf(placeholderArgument); - Contract.ThrowIfTrue(placeholderIndex < 0); + ImmutableArray GetReorderedArgumentsAfterPlaceholderArgument() + { + var placeholderIndex = arguments.IndexOf(placeholderArgument); + Contract.ThrowIfTrue(placeholderIndex < 0); - var afterPlaceholderArguments = arguments.Skip(placeholderIndex + 1).ToImmutableArray(); - var unnamedArguments = afterPlaceholderArguments.TakeWhile(a => !syntaxFacts.IsNamedArgument(a)).ToImmutableArray(); - var namedAndUnnamedArguments = afterPlaceholderArguments.Skip(unnamedArguments.Length).ToImmutableArray(); + var afterPlaceholderArguments = arguments.Skip(placeholderIndex + 1).ToImmutableArray(); + var unnamedArguments = afterPlaceholderArguments.TakeWhile(a => !syntaxFacts.IsNamedArgument(a)).ToImmutableArray(); + var namedAndUnnamedArguments = afterPlaceholderArguments.Skip(unnamedArguments.Length).ToImmutableArray(); - // if all the remaining arguments are named, then sort by name in the original member so that the index - // finds the right one. If not all the arguments are named, then they must be in the right order - // already (the language requires this), so no need to sort. - if (namedAndUnnamedArguments.All(syntaxFacts.IsNamedArgument)) + // if all the remaining arguments are named, then sort by name in the original member so that the index + // finds the right one. If not all the arguments are named, then they must be in the right order + // already (the language requires this), so no need to sort. + if (namedAndUnnamedArguments.All(syntaxFacts.IsNamedArgument)) + { + namedAndUnnamedArguments = namedAndUnnamedArguments.Sort((arg1, arg2) => { - namedAndUnnamedArguments = namedAndUnnamedArguments.Sort((arg1, arg2) => - { - var arg1Name = syntaxFacts.GetNameForArgument(arg1); - var arg2Name = syntaxFacts.GetNameForArgument(arg2); + var arg1Name = syntaxFacts.GetNameForArgument(arg1); + var arg2Name = syntaxFacts.GetNameForArgument(arg2); - var param1 = invocationSymbol.Parameters.FirstOrDefault(p => syntaxFacts.StringComparer.Equals(p.Name, arg1Name)); - var param2 = invocationSymbol.Parameters.FirstOrDefault(p => syntaxFacts.StringComparer.Equals(p.Name, arg2Name)); + var param1 = invocationSymbol.Parameters.FirstOrDefault(p => syntaxFacts.StringComparer.Equals(p.Name, arg1Name)); + var param2 = invocationSymbol.Parameters.FirstOrDefault(p => syntaxFacts.StringComparer.Equals(p.Name, arg2Name)); - // Couldn't find the corresponding parameter. No way to know how to order these. Keep in original order - if (param1 is null || param2 is null) - return namedAndUnnamedArguments.IndexOf(arg1) - namedAndUnnamedArguments.IndexOf(arg2); + // Couldn't find the corresponding parameter. No way to know how to order these. Keep in original order + if (param1 is null || param2 is null) + return namedAndUnnamedArguments.IndexOf(arg1) - namedAndUnnamedArguments.IndexOf(arg2); - return param1.Ordinal - param2.Ordinal; - }); - } - - return unnamedArguments.Concat(namedAndUnnamedArguments); + return param1.Ordinal - param2.Ordinal; + }); } - ImmutableArray ExpandArgumentExpressions(ImmutableArray argumentsAfterPlaceholder) - { - using var _ = ArrayBuilder.GetInstance(out var builder); - foreach (var argument in argumentsAfterPlaceholder) - { - var argumentExpression = syntaxFacts.GetExpressionOfArgument(argument); - var convertedType = semanticModel.GetTypeInfo(argumentExpression, cancellationToken).ConvertedType; + return unnamedArguments.Concat(namedAndUnnamedArguments); + } - builder.Add(convertedType is null - ? (TExpressionSyntax)syntaxGenerator.AddParentheses(argumentExpression) - : (TExpressionSyntax)syntaxGenerator.CastExpression(convertedType, syntaxGenerator.AddParentheses(argumentExpression))); - } + ImmutableArray ExpandArgumentExpressions(ImmutableArray argumentsAfterPlaceholder) + { + using var _ = ArrayBuilder.GetInstance(out var builder); + foreach (var argument in argumentsAfterPlaceholder) + { + var argumentExpression = syntaxFacts.GetExpressionOfArgument(argument); + var convertedType = semanticModel.GetTypeInfo(argumentExpression, cancellationToken).ConvertedType; - return builder.ToImmutableAndClear(); + builder.Add(convertedType is null + ? (TExpressionSyntax)syntaxGenerator.AddParentheses(argumentExpression) + : (TExpressionSyntax)syntaxGenerator.CastExpression(convertedType, syntaxGenerator.AddParentheses(argumentExpression))); } - TExpressionSyntax InsertArgumentsIntoInterpolatedString(ImmutableArray expressions) + return builder.ToImmutableAndClear(); + } + + TExpressionSyntax InsertArgumentsIntoInterpolatedString(ImmutableArray expressions) + { + return interpolatedString.ReplaceNodes(syntaxFacts.GetContentsOfInterpolatedString(interpolatedString), (oldNode, newNode) => { - return interpolatedString.ReplaceNodes(syntaxFacts.GetContentsOfInterpolatedString(interpolatedString), (oldNode, newNode) => + if (newNode is TInterpolationSyntax interpolation) { - if (newNode is TInterpolationSyntax interpolation) + if (syntaxFacts.GetExpressionOfInterpolation(interpolation) is TLiteralExpressionSyntax literalExpression && + syntaxFacts.IsNumericLiteralExpression(literalExpression) && + int.TryParse(literalExpression.GetFirstToken().ValueText, out var index) && + index >= 0 && index < expressions.Length) { - if (syntaxFacts.GetExpressionOfInterpolation(interpolation) is TLiteralExpressionSyntax literalExpression && - syntaxFacts.IsNumericLiteralExpression(literalExpression) && - int.TryParse(literalExpression.GetFirstToken().ValueText, out var index) && - index >= 0 && index < expressions.Length) - { - return interpolation.ReplaceNode( - literalExpression, - syntaxFacts.ConvertToSingleLine(expressions[index], useElasticTrivia: true).WithAdditionalAnnotations(Formatter.Annotation)); - } + return interpolation.ReplaceNode( + literalExpression, + syntaxFacts.ConvertToSingleLine(expressions[index], useElasticTrivia: true).WithAdditionalAnnotations(Formatter.Annotation)); } + } - return newNode; - }); - } + return newNode; + }); } } } diff --git a/src/Features/Core/Portable/ConvertToInterpolatedString/ConvertRegularStringToInterpolatedStringRefactoringProvider.cs b/src/Features/Core/Portable/ConvertToInterpolatedString/ConvertRegularStringToInterpolatedStringRefactoringProvider.cs index 0af5304808548..5e4b62bef33ca 100644 --- a/src/Features/Core/Portable/ConvertToInterpolatedString/ConvertRegularStringToInterpolatedStringRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertToInterpolatedString/ConvertRegularStringToInterpolatedStringRefactoringProvider.cs @@ -14,104 +14,103 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.ConvertToInterpolatedString +namespace Microsoft.CodeAnalysis.ConvertToInterpolatedString; + +/// +/// Code refactoring that converts a regular string containing braces to an interpolated string +/// +[ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString), Shared] +internal sealed class ConvertRegularStringToInterpolatedStringRefactoringProvider : CodeRefactoringProvider { - /// - /// Code refactoring that converts a regular string containing braces to an interpolated string - /// - [ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString), Shared] - internal sealed class ConvertRegularStringToInterpolatedStringRefactoringProvider : CodeRefactoringProvider + [ImportingConstructor] + [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] + public ConvertRegularStringToInterpolatedStringRefactoringProvider() { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public ConvertRegularStringToInterpolatedStringRefactoringProvider() - { - } + } - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) - { - var (document, _, cancellationToken) = context; + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var (document, _, cancellationToken) = context; - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (root is null) - return; + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) + return; - var token = root.FindToken(context.Span.Start); - if (!context.Span.IntersectsWith(token.Span)) - return; + var token = root.FindToken(context.Span.Start); + if (!context.Span.IntersectsWith(token.Span)) + return; - var syntaxKinds = document.GetRequiredLanguageService(); - if (token.RawKind != syntaxKinds.StringLiteralToken) - return; + var syntaxKinds = document.GetRequiredLanguageService(); + if (token.RawKind != syntaxKinds.StringLiteralToken) + return; - var literalExpression = token.GetRequiredParent(); + var literalExpression = token.GetRequiredParent(); - // Check the string literal for errors. This will ensure that we do not try to fixup an incomplete string. - if (literalExpression.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) - return; + // Check the string literal for errors. This will ensure that we do not try to fixup an incomplete string. + if (literalExpression.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + return; - if (!token.Text.Contains('{') && !token.Text.Contains('}')) - return; + if (!token.Text.Contains('{') && !token.Text.Contains('}')) + return; - var syntaxFacts = document.GetRequiredLanguageService(); + var syntaxFacts = document.GetRequiredLanguageService(); - if (!syntaxFacts.SupportsConstantInterpolatedStrings(document.Project.ParseOptions!)) + if (!syntaxFacts.SupportsConstantInterpolatedStrings(document.Project.ParseOptions!)) + { + // If there is a const keyword, do not offer the refactoring (an interpolated string is not const) + var declarator = literalExpression.FirstAncestorOrSelf(syntaxFacts.IsVariableDeclarator); + if (declarator != null) { - // If there is a const keyword, do not offer the refactoring (an interpolated string is not const) - var declarator = literalExpression.FirstAncestorOrSelf(syntaxFacts.IsVariableDeclarator); - if (declarator != null) - { - var generator = SyntaxGenerator.GetGenerator(document); - if (generator.GetModifiers(declarator).IsConst) - return; - } - - // Attributes also only allow constant values. - var attribute = literalExpression.FirstAncestorOrSelf(syntaxFacts.IsAttribute); - if (attribute != null) + var generator = SyntaxGenerator.GetGenerator(document); + if (generator.GetModifiers(declarator).IsConst) return; } - context.RegisterRefactoring( - CodeAction.Create( - FeaturesResources.Convert_to_interpolated_string, - _ => UpdateDocumentAsync(document, root, token), - nameof(FeaturesResources.Convert_to_interpolated_string), - CodeActionPriority.Low), - literalExpression.Span); + // Attributes also only allow constant values. + var attribute = literalExpression.FirstAncestorOrSelf(syntaxFacts.IsAttribute); + if (attribute != null) + return; } - private static string GetTextWithoutQuotes(string text, bool isVerbatim) - { - // Trim off an extra character (@ symbol) for verbatim strings - var startIndex = isVerbatim ? 2 : 1; - return text[startIndex..^1]; - } + context.RegisterRefactoring( + CodeAction.Create( + FeaturesResources.Convert_to_interpolated_string, + _ => UpdateDocumentAsync(document, root, token), + nameof(FeaturesResources.Convert_to_interpolated_string), + CodeActionPriority.Low), + literalExpression.Span); + } - private static SyntaxNode CreateInterpolatedString(Document document, SyntaxNode literalExpression, bool isVerbatim) - { - var generator = SyntaxGenerator.GetGenerator(document); - var text = literalExpression.GetFirstToken().Text; - var valueText = literalExpression.GetFirstToken().ValueText; - var newNode = generator.InterpolatedStringText( - generator.InterpolatedStringTextToken( - GetTextWithoutQuotes(text.Replace("{", "{{").Replace("}", "}}"), isVerbatim), - valueText)); - - return generator.InterpolatedStringExpression( - generator.CreateInterpolatedStringStartToken(isVerbatim), - [newNode], - generator.CreateInterpolatedStringEndToken()).WithTriviaFrom(literalExpression); - } + private static string GetTextWithoutQuotes(string text, bool isVerbatim) + { + // Trim off an extra character (@ symbol) for verbatim strings + var startIndex = isVerbatim ? 2 : 1; + return text[startIndex..^1]; + } - private static Task UpdateDocumentAsync(Document document, SyntaxNode root, SyntaxToken token) - { - var syntaxFacts = document.GetRequiredLanguageService(); - var literalExpression = token.GetRequiredParent(); - return Task.FromResult(document.WithSyntaxRoot( - root.ReplaceNode( - literalExpression, - CreateInterpolatedString(document, literalExpression, syntaxFacts.IsVerbatimStringLiteral(token))))); - } + private static SyntaxNode CreateInterpolatedString(Document document, SyntaxNode literalExpression, bool isVerbatim) + { + var generator = SyntaxGenerator.GetGenerator(document); + var text = literalExpression.GetFirstToken().Text; + var valueText = literalExpression.GetFirstToken().ValueText; + var newNode = generator.InterpolatedStringText( + generator.InterpolatedStringTextToken( + GetTextWithoutQuotes(text.Replace("{", "{{").Replace("}", "}}"), isVerbatim), + valueText)); + + return generator.InterpolatedStringExpression( + generator.CreateInterpolatedStringStartToken(isVerbatim), + [newNode], + generator.CreateInterpolatedStringEndToken()).WithTriviaFrom(literalExpression); + } + + private static Task UpdateDocumentAsync(Document document, SyntaxNode root, SyntaxToken token) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var literalExpression = token.GetRequiredParent(); + return Task.FromResult(document.WithSyntaxRoot( + root.ReplaceNode( + literalExpression, + CreateInterpolatedString(document, literalExpression, syntaxFacts.IsVerbatimStringLiteral(token))))); } } diff --git a/src/Features/Core/Portable/ConvertTupleToStruct/AbstractConvertTupleToStructCodeRefactoringProvider.cs b/src/Features/Core/Portable/ConvertTupleToStruct/AbstractConvertTupleToStructCodeRefactoringProvider.cs index 43a434c3325f9..24b6101ffcff7 100644 --- a/src/Features/Core/Portable/ConvertTupleToStruct/AbstractConvertTupleToStructCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertTupleToStruct/AbstractConvertTupleToStructCodeRefactoringProvider.cs @@ -32,912 +32,911 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.ConvertTupleToStruct +namespace Microsoft.CodeAnalysis.ConvertTupleToStruct; + +internal abstract partial class AbstractConvertTupleToStructCodeRefactoringProvider< + TExpressionSyntax, + TNameSyntax, + TIdentifierNameSyntax, + TLiteralExpressionSyntax, + TObjectCreationExpressionSyntax, + TTupleExpressionSyntax, + TArgumentSyntax, + TTupleTypeSyntax, + TTypeBlockSyntax, + TNamespaceDeclarationSyntax> + : CodeRefactoringProvider, IConvertTupleToStructCodeRefactoringProvider + where TExpressionSyntax : SyntaxNode + where TNameSyntax : TExpressionSyntax + where TIdentifierNameSyntax : TNameSyntax + where TLiteralExpressionSyntax : TExpressionSyntax + where TObjectCreationExpressionSyntax : TExpressionSyntax + where TTupleExpressionSyntax : TExpressionSyntax + where TArgumentSyntax : SyntaxNode + where TTupleTypeSyntax : SyntaxNode + where TTypeBlockSyntax : SyntaxNode + where TNamespaceDeclarationSyntax : SyntaxNode { - internal abstract partial class AbstractConvertTupleToStructCodeRefactoringProvider< - TExpressionSyntax, - TNameSyntax, - TIdentifierNameSyntax, - TLiteralExpressionSyntax, - TObjectCreationExpressionSyntax, - TTupleExpressionSyntax, - TArgumentSyntax, - TTupleTypeSyntax, - TTypeBlockSyntax, - TNamespaceDeclarationSyntax> - : CodeRefactoringProvider, IConvertTupleToStructCodeRefactoringProvider - where TExpressionSyntax : SyntaxNode - where TNameSyntax : TExpressionSyntax - where TIdentifierNameSyntax : TNameSyntax - where TLiteralExpressionSyntax : TExpressionSyntax - where TObjectCreationExpressionSyntax : TExpressionSyntax - where TTupleExpressionSyntax : TExpressionSyntax - where TArgumentSyntax : SyntaxNode - where TTupleTypeSyntax : SyntaxNode - where TTypeBlockSyntax : SyntaxNode - where TNamespaceDeclarationSyntax : SyntaxNode + protected abstract TArgumentSyntax GetArgumentWithChangedName(TArgumentSyntax argument, string name); + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - protected abstract TArgumentSyntax GetArgumentWithChangedName(TArgumentSyntax argument, string name); + var (document, textSpan, cancellationToken) = context; + var (tupleExprOrTypeNode, tupleType) = await TryGetTupleInfoAsync( + document, textSpan, cancellationToken).ConfigureAwait(false); - public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + if (tupleExprOrTypeNode == null || tupleType == null) { - var (document, textSpan, cancellationToken) = context; - var (tupleExprOrTypeNode, tupleType) = await TryGetTupleInfoAsync( - document, textSpan, cancellationToken).ConfigureAwait(false); - - if (tupleExprOrTypeNode == null || tupleType == null) - { - return; - } - - // Check if the tuple type actually references another anonymous type inside of it. - // If it does, we can't convert this. There is no way to describe this anonymous type - // in the concrete type we create. - var fields = tupleType.TupleElements; - var containsAnonymousType = fields.Any(static p => p.Type.ContainsAnonymousType()); - if (containsAnonymousType) - { - return; - } + return; + } - var capturedTypeParameters = - fields.Select(p => p.Type) - .SelectMany(t => t.GetReferencedTypeParameters()) - .Distinct() - .ToImmutableArray(); + // Check if the tuple type actually references another anonymous type inside of it. + // If it does, we can't convert this. There is no way to describe this anonymous type + // in the concrete type we create. + var fields = tupleType.TupleElements; + var containsAnonymousType = fields.Any(static p => p.Type.ContainsAnonymousType()); + if (containsAnonymousType) + { + return; + } - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var capturedTypeParameters = + fields.Select(p => p.Type) + .SelectMany(t => t.GetReferencedTypeParameters()) + .Distinct() + .ToImmutableArray(); - var syntaxFacts = document.GetRequiredLanguageService(); - if (syntaxFacts.SupportsRecordStruct(syntaxTree.Options)) - { - var recordChildActions = CreateChildActions(document, textSpan, tupleExprOrTypeNode, fields, capturedTypeParameters, context.Options, isRecord: true); - if (recordChildActions.Length > 0) - { - context.RegisterRefactoring( - CodeAction.Create( - FeaturesResources.Convert_to_record_struct, - recordChildActions, - isInlinable: false), - tupleExprOrTypeNode.Span); - } - } + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var childActions = CreateChildActions(document, textSpan, tupleExprOrTypeNode, fields, capturedTypeParameters, context.Options, isRecord: false); - if (childActions.Length > 0) + var syntaxFacts = document.GetRequiredLanguageService(); + if (syntaxFacts.SupportsRecordStruct(syntaxTree.Options)) + { + var recordChildActions = CreateChildActions(document, textSpan, tupleExprOrTypeNode, fields, capturedTypeParameters, context.Options, isRecord: true); + if (recordChildActions.Length > 0) { context.RegisterRefactoring( CodeAction.Create( - FeaturesResources.Convert_to_struct, - childActions, + FeaturesResources.Convert_to_record_struct, + recordChildActions, isInlinable: false), tupleExprOrTypeNode.Span); } + } - return; + var childActions = CreateChildActions(document, textSpan, tupleExprOrTypeNode, fields, capturedTypeParameters, context.Options, isRecord: false); + if (childActions.Length > 0) + { + context.RegisterRefactoring( + CodeAction.Create( + FeaturesResources.Convert_to_struct, + childActions, + isInlinable: false), + tupleExprOrTypeNode.Span); + } - ImmutableArray CreateChildActions( - Document document, - TextSpan span, - SyntaxNode tupleExprOrTypeNode, - ImmutableArray fields, - ImmutableArray capturedTypeParameters, - CleanCodeGenerationOptionsProvider fallbackOptions, - bool isRecord) - { - using var scopes = TemporaryArray.Empty; - var containingMember = GetContainingMember(context.Document, tupleExprOrTypeNode); + return; + + ImmutableArray CreateChildActions( + Document document, + TextSpan span, + SyntaxNode tupleExprOrTypeNode, + ImmutableArray fields, + ImmutableArray capturedTypeParameters, + CleanCodeGenerationOptionsProvider fallbackOptions, + bool isRecord) + { + using var scopes = TemporaryArray.Empty; + var containingMember = GetContainingMember(context.Document, tupleExprOrTypeNode); - if (containingMember != null) - scopes.Add(CreateAction(document, span, Scope.ContainingMember, fallbackOptions, isRecord)); + if (containingMember != null) + scopes.Add(CreateAction(document, span, Scope.ContainingMember, fallbackOptions, isRecord)); - // If we captured any Method type-parameters, we can only replace the tuple types we - // find in the containing method. No other tuple types in other members would be able - // to reference this type parameter. - if (!capturedTypeParameters.Any(static tp => tp.TypeParameterKind == TypeParameterKind.Method)) + // If we captured any Method type-parameters, we can only replace the tuple types we + // find in the containing method. No other tuple types in other members would be able + // to reference this type parameter. + if (!capturedTypeParameters.Any(static tp => tp.TypeParameterKind == TypeParameterKind.Method)) + { + var containingType = tupleExprOrTypeNode.GetAncestor(); + if (containingType != null) + scopes.Add(CreateAction(document, span, Scope.ContainingType, fallbackOptions, isRecord)); + + // If we captured any Type type-parameters, we can only replace the tuple + // types we find in the containing type. No other tuple types in other + // types would be able to reference this type parameter. + if (!capturedTypeParameters.Any(static tp => tp.TypeParameterKind == TypeParameterKind.Type)) { - var containingType = tupleExprOrTypeNode.GetAncestor(); - if (containingType != null) - scopes.Add(CreateAction(document, span, Scope.ContainingType, fallbackOptions, isRecord)); - - // If we captured any Type type-parameters, we can only replace the tuple - // types we find in the containing type. No other tuple types in other - // types would be able to reference this type parameter. - if (!capturedTypeParameters.Any(static tp => tp.TypeParameterKind == TypeParameterKind.Type)) + // To do a global find/replace of matching tuples, we need to search for documents + // containing tuples *and* which have the names of the tuple fields in them. That means + // the tuple field name must exist in the document. + // + // this means we can only find tuples like ```(x: 1, ...)``` but not ```(1, 2)```. The + // latter has members called Item1 and Item2, but those names don't show up in source. + if (fields.All(f => f.CorrespondingTupleField != f)) { - // To do a global find/replace of matching tuples, we need to search for documents - // containing tuples *and* which have the names of the tuple fields in them. That means - // the tuple field name must exist in the document. - // - // this means we can only find tuples like ```(x: 1, ...)``` but not ```(1, 2)```. The - // latter has members called Item1 and Item2, but those names don't show up in source. - if (fields.All(f => f.CorrespondingTupleField != f)) - { - scopes.Add(CreateAction(document, span, Scope.ContainingProject, fallbackOptions, isRecord)); - scopes.Add(CreateAction(document, span, Scope.DependentProjects, fallbackOptions, isRecord)); - } + scopes.Add(CreateAction(document, span, Scope.ContainingProject, fallbackOptions, isRecord)); + scopes.Add(CreateAction(document, span, Scope.DependentProjects, fallbackOptions, isRecord)); } } - - return scopes.ToImmutableAndClear(); } + + return scopes.ToImmutableAndClear(); } + } - private static SyntaxNode? GetContainingMember(Document document, SyntaxNode tupleExprOrTypeNode) + private static SyntaxNode? GetContainingMember(Document document, SyntaxNode tupleExprOrTypeNode) + { + var syntaxFacts = document.GetRequiredLanguageService(); + return tupleExprOrTypeNode.FirstAncestorOrSelf((node, syntaxFacts) => syntaxFacts.IsMethodLevelMember(node), syntaxFacts); + } + + private CodeAction CreateAction(Document document, TextSpan span, Scope scope, CleanCodeGenerationOptionsProvider fallbackOptions, bool isRecord) + => CodeAction.Create(GetTitle(scope), c => ConvertToStructAsync(document, span, scope, fallbackOptions, isRecord, c), scope.ToString()); + + private static string GetTitle(Scope scope) + => scope switch { - var syntaxFacts = document.GetRequiredLanguageService(); - return tupleExprOrTypeNode.FirstAncestorOrSelf((node, syntaxFacts) => syntaxFacts.IsMethodLevelMember(node), syntaxFacts); + Scope.ContainingMember => FeaturesResources.updating_usages_in_containing_member, + Scope.ContainingType => FeaturesResources.updating_usages_in_containing_type, + Scope.ContainingProject => FeaturesResources.updating_usages_in_containing_project, + Scope.DependentProjects => FeaturesResources.updating_usages_in_dependent_projects, + _ => throw ExceptionUtilities.UnexpectedValue(scope), + }; + + private static async Task<(SyntaxNode, INamedTypeSymbol)> TryGetTupleInfoAsync( + Document document, TextSpan span, CancellationToken cancellationToken) + { + // Enable refactoring either for TupleExpression or TupleType + var expressionOrType = + await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false) as SyntaxNode ?? + await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); + if (expressionOrType == null) + { + return default; } - private CodeAction CreateAction(Document document, TextSpan span, Scope scope, CleanCodeGenerationOptionsProvider fallbackOptions, bool isRecord) - => CodeAction.Create(GetTitle(scope), c => ConvertToStructAsync(document, span, scope, fallbackOptions, isRecord, c), scope.ToString()); - - private static string GetTitle(Scope scope) - => scope switch - { - Scope.ContainingMember => FeaturesResources.updating_usages_in_containing_member, - Scope.ContainingType => FeaturesResources.updating_usages_in_containing_type, - Scope.ContainingProject => FeaturesResources.updating_usages_in_containing_project, - Scope.DependentProjects => FeaturesResources.updating_usages_in_dependent_projects, - _ => throw ExceptionUtilities.UnexpectedValue(scope), - }; - - private static async Task<(SyntaxNode, INamedTypeSymbol)> TryGetTupleInfoAsync( - Document document, TextSpan span, CancellationToken cancellationToken) + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var tupleType = semanticModel.GetTypeInfo(expressionOrType, cancellationToken).Type as INamedTypeSymbol; + if (tupleType?.IsTupleType != true) { - // Enable refactoring either for TupleExpression or TupleType - var expressionOrType = - await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false) as SyntaxNode ?? - await document.TryGetRelevantNodeAsync(span, cancellationToken).ConfigureAwait(false); - if (expressionOrType == null) - { - return default; - } + return default; + } - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var tupleType = semanticModel.GetTypeInfo(expressionOrType, cancellationToken).Type as INamedTypeSymbol; - if (tupleType?.IsTupleType != true) - { - return default; - } + return (expressionOrType, tupleType); + } - return (expressionOrType, tupleType); - } + public async Task ConvertToStructAsync( + Document document, TextSpan span, Scope scope, CleanCodeGenerationOptionsProvider fallbackOptions, bool isRecord, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - public async Task ConvertToStructAsync( - Document document, TextSpan span, Scope scope, CleanCodeGenerationOptionsProvider fallbackOptions, bool isRecord, CancellationToken cancellationToken) + using (Logger.LogBlock(FunctionId.AbstractConvertTupleToStructCodeRefactoringProvider_ConvertToStructAsync, cancellationToken)) { - cancellationToken.ThrowIfCancellationRequested(); - - using (Logger.LogBlock(FunctionId.AbstractConvertTupleToStructCodeRefactoringProvider_ConvertToStructAsync, cancellationToken)) + var solution = document.Project.Solution; + var client = await RemoteHostClient.TryGetClientAsync(solution.Services, cancellationToken).ConfigureAwait(false); + if (client != null) { - var solution = document.Project.Solution; - var client = await RemoteHostClient.TryGetClientAsync(solution.Services, cancellationToken).ConfigureAwait(false); - if (client != null) - { - var result = await client.TryInvokeAsync( - solution, - (service, solutionInfo, callbackId, cancellationToken) => service.ConvertToStructAsync(solutionInfo, callbackId, document.Id, span, scope, isRecord, cancellationToken), - callbackTarget: new RemoteOptionsProvider(solution.Services, fallbackOptions), - cancellationToken).ConfigureAwait(false); + var result = await client.TryInvokeAsync( + solution, + (service, solutionInfo, callbackId, cancellationToken) => service.ConvertToStructAsync(solutionInfo, callbackId, document.Id, span, scope, isRecord, cancellationToken), + callbackTarget: new RemoteOptionsProvider(solution.Services, fallbackOptions), + cancellationToken).ConfigureAwait(false); - if (!result.HasValue) - { - return solution; - } + if (!result.HasValue) + { + return solution; + } - var resultSolution = await RemoteUtilities.UpdateSolutionAsync( - solution, result.Value.DocumentTextChanges, cancellationToken).ConfigureAwait(false); + var resultSolution = await RemoteUtilities.UpdateSolutionAsync( + solution, result.Value.DocumentTextChanges, cancellationToken).ConfigureAwait(false); - return await AddRenameTokenAsync( - resultSolution, result.Value.RenamedToken, cancellationToken).ConfigureAwait(false); - } + return await AddRenameTokenAsync( + resultSolution, result.Value.RenamedToken, cancellationToken).ConfigureAwait(false); } - - return await ConvertToStructInCurrentProcessAsync( - document, span, scope, fallbackOptions, isRecord, cancellationToken).ConfigureAwait(false); } - private static async Task AddRenameTokenAsync( - Solution solution, - (DocumentId documentId, TextSpan span) renamedToken, - CancellationToken cancellationToken) - { - var document = solution.GetRequiredDocument(renamedToken.documentId); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(renamedToken.span.Start); - var newRoot = root.ReplaceToken(token, token.WithAdditionalAnnotations(RenameAnnotation.Create())); + return await ConvertToStructInCurrentProcessAsync( + document, span, scope, fallbackOptions, isRecord, cancellationToken).ConfigureAwait(false); + } - return document.WithSyntaxRoot(newRoot).Project.Solution; - } + private static async Task AddRenameTokenAsync( + Solution solution, + (DocumentId documentId, TextSpan span) renamedToken, + CancellationToken cancellationToken) + { + var document = solution.GetRequiredDocument(renamedToken.documentId); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(renamedToken.span.Start); + var newRoot = root.ReplaceToken(token, token.WithAdditionalAnnotations(RenameAnnotation.Create())); - private async Task ConvertToStructInCurrentProcessAsync( - Document document, TextSpan span, Scope scope, CleanCodeGenerationOptionsProvider fallbackOptions, bool isRecord, CancellationToken cancellationToken) - { - var (tupleExprOrTypeNode, tupleType) = await TryGetTupleInfoAsync( - document, span, cancellationToken).ConfigureAwait(false); - - Debug.Assert(tupleExprOrTypeNode != null); - Debug.Assert(tupleType != null); - - var position = span.Start; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var container = tupleExprOrTypeNode.GetAncestor() ?? root; - var containingNamespace = container is TNamespaceDeclarationSyntax namespaceDecl - ? (INamespaceSymbol)semanticModel.GetRequiredDeclaredSymbol(namespaceDecl, cancellationToken) - : semanticModel.Compilation.GlobalNamespace; - - // Generate a unique name for the struct we're creating. We'll also add a rename - // annotation so the user can pick the right name for the type afterwards. - var structName = NameGenerator.GenerateUniqueName( - "NewStruct", n => semanticModel.LookupSymbols(position, name: n).IsEmpty); - - var capturedTypeParameters = - tupleType.TupleElements.Select(p => p.Type) - .SelectMany(t => t.GetReferencedTypeParameters()) - .Distinct() - .ToImmutableArray(); - - // Get the rule that will name the parameter according to the users preferences for this document - // (and importantly not any of the documents where we change the call sites, below) - // For records we don't use this however, but rather leave the parameters exactly as the tuple elements - // were defined, since they function as both the parameters and the property names. - var parameterNamingRule = await document.GetApplicableNamingRuleAsync(SymbolKind.Parameter, Accessibility.NotApplicable, fallbackOptions, cancellationToken).ConfigureAwait(false); - - // Next, generate the full struct that will be used to replace all instances of this - // tuple type. - var namedTypeSymbol = await GenerateFinalNamedTypeAsync( - document, scope, isRecord, structName, capturedTypeParameters, tupleType, parameterNamingRule, cancellationToken).ConfigureAwait(false); - - var documentToEditorMap = new Dictionary(); - var documentsToUpdate = await GetDocumentsToUpdateAsync( - document, tupleExprOrTypeNode, tupleType, scope, cancellationToken).ConfigureAwait(false); - - // Next, go through and replace all matching tuple expressions and types in the appropriate - // scope with the new named type we've generated. - await ReplaceExpressionAndTypesInScopeAsync( - documentToEditorMap, documentsToUpdate, - tupleExprOrTypeNode, tupleType, - structName, capturedTypeParameters, - containingNamespace, parameterNamingRule, isRecord, cancellationToken).ConfigureAwait(false); - - await GenerateStructIntoContainingNamespaceAsync( - document, tupleExprOrTypeNode, namedTypeSymbol, - documentToEditorMap, fallbackOptions, cancellationToken).ConfigureAwait(false); - - var updatedSolution = await ApplyChangesAsync( - document, documentToEditorMap, fallbackOptions, cancellationToken).ConfigureAwait(false); - - return updatedSolution; - } + return document.WithSyntaxRoot(newRoot).Project.Solution; + } + + private async Task ConvertToStructInCurrentProcessAsync( + Document document, TextSpan span, Scope scope, CleanCodeGenerationOptionsProvider fallbackOptions, bool isRecord, CancellationToken cancellationToken) + { + var (tupleExprOrTypeNode, tupleType) = await TryGetTupleInfoAsync( + document, span, cancellationToken).ConfigureAwait(false); + + Debug.Assert(tupleExprOrTypeNode != null); + Debug.Assert(tupleType != null); + + var position = span.Start; + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var container = tupleExprOrTypeNode.GetAncestor() ?? root; + var containingNamespace = container is TNamespaceDeclarationSyntax namespaceDecl + ? (INamespaceSymbol)semanticModel.GetRequiredDeclaredSymbol(namespaceDecl, cancellationToken) + : semanticModel.Compilation.GlobalNamespace; + + // Generate a unique name for the struct we're creating. We'll also add a rename + // annotation so the user can pick the right name for the type afterwards. + var structName = NameGenerator.GenerateUniqueName( + "NewStruct", n => semanticModel.LookupSymbols(position, name: n).IsEmpty); + + var capturedTypeParameters = + tupleType.TupleElements.Select(p => p.Type) + .SelectMany(t => t.GetReferencedTypeParameters()) + .Distinct() + .ToImmutableArray(); + + // Get the rule that will name the parameter according to the users preferences for this document + // (and importantly not any of the documents where we change the call sites, below) + // For records we don't use this however, but rather leave the parameters exactly as the tuple elements + // were defined, since they function as both the parameters and the property names. + var parameterNamingRule = await document.GetApplicableNamingRuleAsync(SymbolKind.Parameter, Accessibility.NotApplicable, fallbackOptions, cancellationToken).ConfigureAwait(false); + + // Next, generate the full struct that will be used to replace all instances of this + // tuple type. + var namedTypeSymbol = await GenerateFinalNamedTypeAsync( + document, scope, isRecord, structName, capturedTypeParameters, tupleType, parameterNamingRule, cancellationToken).ConfigureAwait(false); + + var documentToEditorMap = new Dictionary(); + var documentsToUpdate = await GetDocumentsToUpdateAsync( + document, tupleExprOrTypeNode, tupleType, scope, cancellationToken).ConfigureAwait(false); + + // Next, go through and replace all matching tuple expressions and types in the appropriate + // scope with the new named type we've generated. + await ReplaceExpressionAndTypesInScopeAsync( + documentToEditorMap, documentsToUpdate, + tupleExprOrTypeNode, tupleType, + structName, capturedTypeParameters, + containingNamespace, parameterNamingRule, isRecord, cancellationToken).ConfigureAwait(false); + + await GenerateStructIntoContainingNamespaceAsync( + document, tupleExprOrTypeNode, namedTypeSymbol, + documentToEditorMap, fallbackOptions, cancellationToken).ConfigureAwait(false); + + var updatedSolution = await ApplyChangesAsync( + document, documentToEditorMap, fallbackOptions, cancellationToken).ConfigureAwait(false); + + return updatedSolution; + } - private async Task ReplaceExpressionAndTypesInScopeAsync( - Dictionary documentToEditorMap, - ImmutableArray documentsToUpdate, - SyntaxNode tupleExprOrTypeNode, INamedTypeSymbol tupleType, - string structName, ImmutableArray typeParameters, - INamespaceSymbol containingNamespace, NamingRule parameterNamingRule, - bool isRecord, CancellationToken cancellationToken) + private async Task ReplaceExpressionAndTypesInScopeAsync( + Dictionary documentToEditorMap, + ImmutableArray documentsToUpdate, + SyntaxNode tupleExprOrTypeNode, INamedTypeSymbol tupleType, + string structName, ImmutableArray typeParameters, + INamespaceSymbol containingNamespace, NamingRule parameterNamingRule, + bool isRecord, CancellationToken cancellationToken) + { + // Process the documents one project at a time. + foreach (var group in documentsToUpdate.GroupBy(d => d.Document.Project)) { - // Process the documents one project at a time. - foreach (var group in documentsToUpdate.GroupBy(d => d.Document.Project)) + // grab the compilation and keep it around as long as we're processing + // the project so we don't clean things up in the middle. To do this + // we use a GC.KeepAlive below so that we can mark that this compilation + // should stay around (even though we don't reference is directly in + // any other way here). + var project = group.Key; + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + + var generator = project.Services.GetRequiredService(); + + // Get the fully qualified name for the new type we're creating. We'll use this + // at replacement points so that we can find the right type even if we're in a + // different namespace. + + // If the struct is being injected into the global namespace, then reference it with + // "global::NewStruct", Otherwise, get the full name to the namespace, and append + // the NewStruct name to it. + var structNameNode = CreateStructNameNode( + generator, structName, typeParameters, addRenameAnnotation: false); + + var fullTypeName = containingNamespace.IsGlobalNamespace + ? (TNameSyntax)generator.GlobalAliasedName(structNameNode) + : (TNameSyntax)generator.QualifiedName(generator.NameExpression(containingNamespace), structNameNode); + + fullTypeName = fullTypeName.WithAdditionalAnnotations(Simplifier.Annotation) + .WithAdditionalAnnotations(DoNotAllowVarAnnotation.Annotation); + + foreach (var documentToUpdate in group) { - // grab the compilation and keep it around as long as we're processing - // the project so we don't clean things up in the middle. To do this - // we use a GC.KeepAlive below so that we can mark that this compilation - // should stay around (even though we don't reference is directly in - // any other way here). - var project = group.Key; - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - - var generator = project.Services.GetRequiredService(); - - // Get the fully qualified name for the new type we're creating. We'll use this - // at replacement points so that we can find the right type even if we're in a - // different namespace. - - // If the struct is being injected into the global namespace, then reference it with - // "global::NewStruct", Otherwise, get the full name to the namespace, and append - // the NewStruct name to it. - var structNameNode = CreateStructNameNode( - generator, structName, typeParameters, addRenameAnnotation: false); - - var fullTypeName = containingNamespace.IsGlobalNamespace - ? (TNameSyntax)generator.GlobalAliasedName(structNameNode) - : (TNameSyntax)generator.QualifiedName(generator.NameExpression(containingNamespace), structNameNode); - - fullTypeName = fullTypeName.WithAdditionalAnnotations(Simplifier.Annotation) - .WithAdditionalAnnotations(DoNotAllowVarAnnotation.Annotation); - - foreach (var documentToUpdate in group) - { - var document = documentToUpdate.Document; - var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var document = documentToUpdate.Document; + var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - // We should only ever get a default array (meaning, update the root), or a - // non-empty array. We should never be asked to update exactly '0' nodes. - Debug.Assert(documentToUpdate.NodesToUpdate.IsDefault || - !documentToUpdate.NodesToUpdate.IsEmpty); + // We should only ever get a default array (meaning, update the root), or a + // non-empty array. We should never be asked to update exactly '0' nodes. + Debug.Assert(documentToUpdate.NodesToUpdate.IsDefault || + !documentToUpdate.NodesToUpdate.IsEmpty); - // If we were given specific nodes to update, only update those. Otherwise - // updated everything from the root down. - var nodesToUpdate = documentToUpdate.NodesToUpdate.IsDefault - ? [syntaxRoot] - : documentToUpdate.NodesToUpdate; + // If we were given specific nodes to update, only update those. Otherwise + // updated everything from the root down. + var nodesToUpdate = documentToUpdate.NodesToUpdate.IsDefault + ? [syntaxRoot] + : documentToUpdate.NodesToUpdate; - var editor = new SyntaxEditor(syntaxRoot, generator); + var editor = new SyntaxEditor(syntaxRoot, generator); - var replaced = false; + var replaced = false; - foreach (var container in nodesToUpdate) - { - replaced |= await ReplaceTupleExpressionsAndTypesInDocumentAsync( - document, parameterNamingRule, isRecord, editor, tupleExprOrTypeNode, tupleType, - fullTypeName, structName, typeParameters, - container, cancellationToken).ConfigureAwait(false); - } - - if (replaced) - { - // We made a replacement. Keep track of this so we can update our solution - // later. - documentToEditorMap.Add(document, editor); - } + foreach (var container in nodesToUpdate) + { + replaced |= await ReplaceTupleExpressionsAndTypesInDocumentAsync( + document, parameterNamingRule, isRecord, editor, tupleExprOrTypeNode, tupleType, + fullTypeName, structName, typeParameters, + container, cancellationToken).ConfigureAwait(false); } - GC.KeepAlive(compilation); - } - } - - private static TNameSyntax CreateStructNameNode( - SyntaxGenerator generator, string structName, - ImmutableArray typeParameters, bool addRenameAnnotation) - { - var structNameToken = generator.Identifier(structName); - if (addRenameAnnotation) - { - structNameToken = structNameToken.WithAdditionalAnnotations(RenameAnnotation.Create()); + if (replaced) + { + // We made a replacement. Keep track of this so we can update our solution + // later. + documentToEditorMap.Add(document, editor); + } } - return typeParameters.IsEmpty - ? (TNameSyntax)generator.IdentifierName(structNameToken) - : (TNameSyntax)generator.GenericName(structNameToken, typeParameters.Select(tp => generator.IdentifierName(tp.Name))); + GC.KeepAlive(compilation); } + } - private static async Task> GetDocumentsToUpdateAsync( - Document document, SyntaxNode tupleExprOrTypeNode, - INamedTypeSymbol tupleType, Scope scope, CancellationToken cancellationToken) + private static TNameSyntax CreateStructNameNode( + SyntaxGenerator generator, string structName, + ImmutableArray typeParameters, bool addRenameAnnotation) + { + var structNameToken = generator.Identifier(structName); + if (addRenameAnnotation) { - return scope switch - { - Scope.ContainingMember => GetDocumentsToUpdateForContainingMember(document, tupleExprOrTypeNode), - Scope.ContainingType => await GetDocumentsToUpdateForContainingTypeAsync( - document, tupleExprOrTypeNode, cancellationToken).ConfigureAwait(false), - Scope.ContainingProject => await GetDocumentsToUpdateForContainingProjectAsync( - document.Project, tupleType, cancellationToken).ConfigureAwait(false), - Scope.DependentProjects => await GetDocumentsToUpdateForDependentProjectAsync( - document.Project, tupleType, cancellationToken).ConfigureAwait(false), - _ => throw ExceptionUtilities.UnexpectedValue(scope), - }; + structNameToken = structNameToken.WithAdditionalAnnotations(RenameAnnotation.Create()); } - private static async Task> GetDocumentsToUpdateForDependentProjectAsync( - Project startingProject, INamedTypeSymbol tupleType, CancellationToken cancellationToken) - { - var solution = startingProject.Solution; - var graph = solution.GetProjectDependencyGraph(); - - // Note: there are a couple of approaches we can take here. Processing 'direct' - // dependencies, or processing 'transitive' dependencies. Both have pros/cons: - // - // Direct Dependencies: - // Pros: - // All updated projects are able to see the newly added type. - // Transitive deps won't be updated to use a type they can't actually use. - // Cons: - // If that project then exports that new type, then transitive deps will - // break if they use those exported APIs since they won't know about the - // type. - // - // Transitive Dependencies: - // Pros: - // All affected code is updated. - // Cons: - // Non-direct deps will not compile unless the take a reference on the - // starting project. - - var dependentProjects = graph.GetProjectsThatDirectlyDependOnThisProject(startingProject.Id); - var allProjects = dependentProjects.Select(solution.GetRequiredProject) - .Where(p => p.SupportsCompilation) - .Concat(startingProject).ToSet(); - - using var _ = ArrayBuilder.GetInstance(out var result); - var tupleFieldNames = tupleType.TupleElements.SelectAsArray(f => f.Name); - - foreach (var project in allProjects) - { - await AddDocumentsToUpdateForProjectAsync( - project, result, tupleFieldNames, cancellationToken).ConfigureAwait(false); - } - - return result.ToImmutable(); - } + return typeParameters.IsEmpty + ? (TNameSyntax)generator.IdentifierName(structNameToken) + : (TNameSyntax)generator.GenericName(structNameToken, typeParameters.Select(tp => generator.IdentifierName(tp.Name))); + } - private static async Task> GetDocumentsToUpdateForContainingProjectAsync( - Project project, INamedTypeSymbol tupleType, CancellationToken cancellationToken) + private static async Task> GetDocumentsToUpdateAsync( + Document document, SyntaxNode tupleExprOrTypeNode, + INamedTypeSymbol tupleType, Scope scope, CancellationToken cancellationToken) + { + return scope switch { - using var _ = ArrayBuilder.GetInstance(out var result); - var tupleFieldNames = tupleType.TupleElements.SelectAsArray(f => f.Name); + Scope.ContainingMember => GetDocumentsToUpdateForContainingMember(document, tupleExprOrTypeNode), + Scope.ContainingType => await GetDocumentsToUpdateForContainingTypeAsync( + document, tupleExprOrTypeNode, cancellationToken).ConfigureAwait(false), + Scope.ContainingProject => await GetDocumentsToUpdateForContainingProjectAsync( + document.Project, tupleType, cancellationToken).ConfigureAwait(false), + Scope.DependentProjects => await GetDocumentsToUpdateForDependentProjectAsync( + document.Project, tupleType, cancellationToken).ConfigureAwait(false), + _ => throw ExceptionUtilities.UnexpectedValue(scope), + }; + } + private static async Task> GetDocumentsToUpdateForDependentProjectAsync( + Project startingProject, INamedTypeSymbol tupleType, CancellationToken cancellationToken) + { + var solution = startingProject.Solution; + var graph = solution.GetProjectDependencyGraph(); + + // Note: there are a couple of approaches we can take here. Processing 'direct' + // dependencies, or processing 'transitive' dependencies. Both have pros/cons: + // + // Direct Dependencies: + // Pros: + // All updated projects are able to see the newly added type. + // Transitive deps won't be updated to use a type they can't actually use. + // Cons: + // If that project then exports that new type, then transitive deps will + // break if they use those exported APIs since they won't know about the + // type. + // + // Transitive Dependencies: + // Pros: + // All affected code is updated. + // Cons: + // Non-direct deps will not compile unless the take a reference on the + // starting project. + + var dependentProjects = graph.GetProjectsThatDirectlyDependOnThisProject(startingProject.Id); + var allProjects = dependentProjects.Select(solution.GetRequiredProject) + .Where(p => p.SupportsCompilation) + .Concat(startingProject).ToSet(); + + using var _ = ArrayBuilder.GetInstance(out var result); + var tupleFieldNames = tupleType.TupleElements.SelectAsArray(f => f.Name); + + foreach (var project in allProjects) + { await AddDocumentsToUpdateForProjectAsync( project, result, tupleFieldNames, cancellationToken).ConfigureAwait(false); - - return result.ToImmutable(); } - private static async Task AddDocumentsToUpdateForProjectAsync(Project project, ArrayBuilder result, ImmutableArray tupleFieldNames, CancellationToken cancellationToken) + return result.ToImmutable(); + } + + private static async Task> GetDocumentsToUpdateForContainingProjectAsync( + Project project, INamedTypeSymbol tupleType, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var result); + var tupleFieldNames = tupleType.TupleElements.SelectAsArray(f => f.Name); + + await AddDocumentsToUpdateForProjectAsync( + project, result, tupleFieldNames, cancellationToken).ConfigureAwait(false); + + return result.ToImmutable(); + } + + private static async Task AddDocumentsToUpdateForProjectAsync(Project project, ArrayBuilder result, ImmutableArray tupleFieldNames, CancellationToken cancellationToken) + { + foreach (var document in project.Documents) { - foreach (var document in project.Documents) + var info = await document.GetSyntaxTreeIndexAsync(cancellationToken).ConfigureAwait(false); + if (info != null && + info.ContainsTupleExpressionOrTupleType && + InfoProbablyContainsTupleFieldNames(info, tupleFieldNames)) { - var info = await document.GetSyntaxTreeIndexAsync(cancellationToken).ConfigureAwait(false); - if (info != null && - info.ContainsTupleExpressionOrTupleType && - InfoProbablyContainsTupleFieldNames(info, tupleFieldNames)) - { - // Use 'default' for nodesToUpdate so we walk the entire document - result.Add(new DocumentToUpdate(document, nodesToUpdate: default)); - } + // Use 'default' for nodesToUpdate so we walk the entire document + result.Add(new DocumentToUpdate(document, nodesToUpdate: default)); } } + } - private static bool InfoProbablyContainsTupleFieldNames(SyntaxTreeIndex info, ImmutableArray tupleFieldNames) + private static bool InfoProbablyContainsTupleFieldNames(SyntaxTreeIndex info, ImmutableArray tupleFieldNames) + { + foreach (var name in tupleFieldNames) { - foreach (var name in tupleFieldNames) + if (!info.ProbablyContainsIdentifier(name)) { - if (!info.ProbablyContainsIdentifier(name)) - { - return false; - } + return false; } - - return true; } - private static async Task> GetDocumentsToUpdateForContainingTypeAsync( - Document startingDocument, SyntaxNode tupleExprOrTypeNode, CancellationToken cancellationToken) - { - var containingType = tupleExprOrTypeNode.GetAncestor(); - Debug.Assert(containingType != null, - "We should always get a containing scope since we already checked for that to support Scope.ContainingType."); - - var solution = startingDocument.Project.Solution; - var semanticModel = await startingDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var typeSymbol = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(containingType, cancellationToken); - - using var _ = ArrayBuilder.GetInstance(out var result); + return true; + } - var declarationService = startingDocument.GetRequiredLanguageService(); - foreach (var group in declarationService.GetDeclarations(typeSymbol).GroupBy(r => r.SyntaxTree)) - { - var document = solution.GetRequiredDocument(group.Key); - var nodes = group.SelectAsArray(r => r.GetSyntax(cancellationToken)); + private static async Task> GetDocumentsToUpdateForContainingTypeAsync( + Document startingDocument, SyntaxNode tupleExprOrTypeNode, CancellationToken cancellationToken) + { + var containingType = tupleExprOrTypeNode.GetAncestor(); + Debug.Assert(containingType != null, + "We should always get a containing scope since we already checked for that to support Scope.ContainingType."); - result.Add(new DocumentToUpdate(document, nodes)); - } + var solution = startingDocument.Project.Solution; + var semanticModel = await startingDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var typeSymbol = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(containingType, cancellationToken); - return result.ToImmutable(); - } + using var _ = ArrayBuilder.GetInstance(out var result); - private static ImmutableArray GetDocumentsToUpdateForContainingMember( - Document document, SyntaxNode tupleExprOrTypeNode) + var declarationService = startingDocument.GetRequiredLanguageService(); + foreach (var group in declarationService.GetDeclarations(typeSymbol).GroupBy(r => r.SyntaxTree)) { - var containingMember = GetContainingMember(document, tupleExprOrTypeNode); - Contract.ThrowIfNull(containingMember, - "We should always get a containing member since we already checked for that to support Scope.ContainingMember."); + var document = solution.GetRequiredDocument(group.Key); + var nodes = group.SelectAsArray(r => r.GetSyntax(cancellationToken)); - return [new DocumentToUpdate(document, [containingMember])]; + result.Add(new DocumentToUpdate(document, nodes)); } - private static async Task GenerateStructIntoContainingNamespaceAsync( - Document document, SyntaxNode tupleExprOrTypeNode, INamedTypeSymbol namedTypeSymbol, - Dictionary documentToEditorMap, - CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - // If we don't already have an editor for the containing document, then make one. - if (!documentToEditorMap.TryGetValue(document, out var editor)) - { - var generator = SyntaxGenerator.GetGenerator(document); - editor = new SyntaxEditor(root, generator); + return result.ToImmutable(); + } - documentToEditorMap.Add(document, editor); - } + private static ImmutableArray GetDocumentsToUpdateForContainingMember( + Document document, SyntaxNode tupleExprOrTypeNode) + { + var containingMember = GetContainingMember(document, tupleExprOrTypeNode); + Contract.ThrowIfNull(containingMember, + "We should always get a containing member since we already checked for that to support Scope.ContainingMember."); - var syntaxFacts = document.GetLanguageService(); - var container = tupleExprOrTypeNode.GetAncestor() ?? root; + return [new DocumentToUpdate(document, [containingMember])]; + } - var context = new CodeGenerationContext( - generateMembers: true, - sortMembers: false, - autoInsertionLocation: false); + private static async Task GenerateStructIntoContainingNamespaceAsync( + Document document, SyntaxNode tupleExprOrTypeNode, INamedTypeSymbol namedTypeSymbol, + Dictionary documentToEditorMap, + CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var info = await document.GetCodeGenerationInfoAsync(context, fallbackOptions, cancellationToken).ConfigureAwait(false); + // If we don't already have an editor for the containing document, then make one. + if (!documentToEditorMap.TryGetValue(document, out var editor)) + { + var generator = SyntaxGenerator.GetGenerator(document); + editor = new SyntaxEditor(root, generator); - // Then, actually insert the new class in the appropriate container. - editor.ReplaceNode(container, (currentContainer, _) => - info.Service.AddNamedType(currentContainer, namedTypeSymbol, info, cancellationToken)); + documentToEditorMap.Add(document, editor); } - private static async Task ApplyChangesAsync( - Document startingDocument, Dictionary documentToEditorMap, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) - { - var currentSolution = startingDocument.Project.Solution; + var syntaxFacts = document.GetLanguageService(); + var container = tupleExprOrTypeNode.GetAncestor() ?? root; - foreach (var (currentDoc, editor) in documentToEditorMap) - { - var docId = currentDoc.Id; - var newRoot = editor.GetChangedRoot(); - var updatedDocument = currentSolution.WithDocumentSyntaxRoot(docId, newRoot, PreservationMode.PreserveIdentity) - .GetRequiredDocument(docId); + var context = new CodeGenerationContext( + generateMembers: true, + sortMembers: false, + autoInsertionLocation: false); - if (currentDoc == startingDocument) - { - // If this is the starting document, format using the equals+getHashCode service - // so that our generated methods follow any special formatting rules specific to - // them. - var equalsAndGetHashCodeService = startingDocument.GetRequiredLanguageService(); - var formattingOptions = await updatedDocument.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); - - updatedDocument = await equalsAndGetHashCodeService.FormatDocumentAsync( - updatedDocument, formattingOptions, cancellationToken).ConfigureAwait(false); - } - - currentSolution = updatedDocument.Project.Solution; - } + var info = await document.GetCodeGenerationInfoAsync(context, fallbackOptions, cancellationToken).ConfigureAwait(false); - return currentSolution; - } + // Then, actually insert the new class in the appropriate container. + editor.ReplaceNode(container, (currentContainer, _) => + info.Service.AddNamedType(currentContainer, namedTypeSymbol, info, cancellationToken)); + } - private async Task ReplaceTupleExpressionsAndTypesInDocumentAsync( - Document document, NamingRule parameterNamingRule, bool isRecord, SyntaxEditor editor, - SyntaxNode startingNode, INamedTypeSymbol tupleType, TNameSyntax fullyQualifiedStructName, - string structName, ImmutableArray typeParameters, - SyntaxNode containerToUpdate, CancellationToken cancellationToken) - { - var changed = false; - changed |= await ReplaceMatchingTupleExpressionsAsync( - document, parameterNamingRule, isRecord, editor, startingNode, tupleType, - fullyQualifiedStructName, structName, typeParameters, - containerToUpdate, cancellationToken).ConfigureAwait(false); - - changed |= await ReplaceMatchingTupleTypesAsync( - document, editor, startingNode, tupleType, - fullyQualifiedStructName, structName, typeParameters, - containerToUpdate, cancellationToken).ConfigureAwait(false); - - return changed; - } + private static async Task ApplyChangesAsync( + Document startingDocument, Dictionary documentToEditorMap, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken) + { + var currentSolution = startingDocument.Project.Solution; - private async Task ReplaceMatchingTupleExpressionsAsync( - Document document, NamingRule parameterNamingRule, bool isRecord, SyntaxEditor editor, - SyntaxNode startingNode, INamedTypeSymbol tupleType, TNameSyntax qualifiedTypeName, - string typeName, ImmutableArray typeParameters, - SyntaxNode containingMember, CancellationToken cancellationToken) + foreach (var (currentDoc, editor) in documentToEditorMap) { - var syntaxFacts = document.GetRequiredLanguageService(); - var comparer = syntaxFacts.StringComparer; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - var childCreationNodes = containingMember.DescendantNodesAndSelf() - .OfType(); + var docId = currentDoc.Id; + var newRoot = editor.GetChangedRoot(); + var updatedDocument = currentSolution.WithDocumentSyntaxRoot(docId, newRoot, PreservationMode.PreserveIdentity) + .GetRequiredDocument(docId); - var changed = false; - foreach (var childCreation in childCreationNodes) + if (currentDoc == startingDocument) { - if (semanticModel.GetTypeInfo(childCreation, cancellationToken).Type is not INamedTypeSymbol childType) - { - Debug.Fail("We should always be able to get an tuple type for any tuple expression node."); - continue; - } - - if (AreEquivalent(comparer, tupleType, childType)) - { - changed = true; - ReplaceWithObjectCreation( - editor, typeName, typeParameters, qualifiedTypeName, startingNode, childCreation, parameterNamingRule, isRecord); - } + // If this is the starting document, format using the equals+getHashCode service + // so that our generated methods follow any special formatting rules specific to + // them. + var equalsAndGetHashCodeService = startingDocument.GetRequiredLanguageService(); + var formattingOptions = await updatedDocument.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false); + + updatedDocument = await equalsAndGetHashCodeService.FormatDocumentAsync( + updatedDocument, formattingOptions, cancellationToken).ConfigureAwait(false); } - return changed; + currentSolution = updatedDocument.Project.Solution; } - private static bool AreEquivalent(StringComparer comparer, INamedTypeSymbol tupleType, INamedTypeSymbol childType) - => SymbolEquivalenceComparer.Instance.Equals(tupleType, childType) && - NamesMatch(comparer, tupleType.TupleElements, childType.TupleElements); + return currentSolution; + } + + private async Task ReplaceTupleExpressionsAndTypesInDocumentAsync( + Document document, NamingRule parameterNamingRule, bool isRecord, SyntaxEditor editor, + SyntaxNode startingNode, INamedTypeSymbol tupleType, TNameSyntax fullyQualifiedStructName, + string structName, ImmutableArray typeParameters, + SyntaxNode containerToUpdate, CancellationToken cancellationToken) + { + var changed = false; + changed |= await ReplaceMatchingTupleExpressionsAsync( + document, parameterNamingRule, isRecord, editor, startingNode, tupleType, + fullyQualifiedStructName, structName, typeParameters, + containerToUpdate, cancellationToken).ConfigureAwait(false); + + changed |= await ReplaceMatchingTupleTypesAsync( + document, editor, startingNode, tupleType, + fullyQualifiedStructName, structName, typeParameters, + containerToUpdate, cancellationToken).ConfigureAwait(false); + + return changed; + } + + private async Task ReplaceMatchingTupleExpressionsAsync( + Document document, NamingRule parameterNamingRule, bool isRecord, SyntaxEditor editor, + SyntaxNode startingNode, INamedTypeSymbol tupleType, TNameSyntax qualifiedTypeName, + string typeName, ImmutableArray typeParameters, + SyntaxNode containingMember, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var comparer = syntaxFacts.StringComparer; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var childCreationNodes = containingMember.DescendantNodesAndSelf() + .OfType(); - private static bool NamesMatch( - StringComparer comparer, ImmutableArray fields1, ImmutableArray fields2) + var changed = false; + foreach (var childCreation in childCreationNodes) { - if (fields1.Length != fields2.Length) + if (semanticModel.GetTypeInfo(childCreation, cancellationToken).Type is not INamedTypeSymbol childType) { - return false; + Debug.Fail("We should always be able to get an tuple type for any tuple expression node."); + continue; } - for (var i = 0; i < fields1.Length; i++) + if (AreEquivalent(comparer, tupleType, childType)) { - if (!comparer.Equals(fields1[i].Name, fields2[i].Name)) - { - return false; - } + changed = true; + ReplaceWithObjectCreation( + editor, typeName, typeParameters, qualifiedTypeName, startingNode, childCreation, parameterNamingRule, isRecord); } - - return true; } - private void ReplaceWithObjectCreation( - SyntaxEditor editor, string typeName, ImmutableArray typeParameters, - TNameSyntax qualifiedTypeName, SyntaxNode startingCreationNode, TTupleExpressionSyntax childCreation, - NamingRule parameterNamingRule, bool isRecord) - { - // Use the callback form as tuples types may be nested, and we want to - // properly replace them even in that case. - editor.ReplaceNode( - childCreation, - (currentNode, g) => - { - var currentTupleExpr = (TTupleExpressionSyntax)currentNode; + return changed; + } - // If we hit the node the user started on, then add the rename annotation here. - var typeNameNode = startingCreationNode == childCreation - ? CreateStructNameNode(g, typeName, typeParameters, addRenameAnnotation: true) - : qualifiedTypeName; + private static bool AreEquivalent(StringComparer comparer, INamedTypeSymbol tupleType, INamedTypeSymbol childType) + => SymbolEquivalenceComparer.Instance.Equals(tupleType, childType) && + NamesMatch(comparer, tupleType.TupleElements, childType.TupleElements); - var syntaxFacts = g.SyntaxFacts; - syntaxFacts.GetPartsOfTupleExpression( - currentTupleExpr, out var openParen, out var arguments, out var closeParen); - arguments = ConvertArguments(g, parameterNamingRule, isRecord, arguments); + private static bool NamesMatch( + StringComparer comparer, ImmutableArray fields1, ImmutableArray fields2) + { + if (fields1.Length != fields2.Length) + { + return false; + } - return g.ObjectCreationExpression(typeNameNode, openParen, arguments, closeParen) - .WithAdditionalAnnotations(Formatter.Annotation); - }); + for (var i = 0; i < fields1.Length; i++) + { + if (!comparer.Equals(fields1[i].Name, fields2[i].Name)) + { + return false; + } } - private SeparatedSyntaxList ConvertArguments(SyntaxGenerator generator, NamingRule parameterNamingRule, bool isRecord, SeparatedSyntaxList arguments) - => generator.SeparatedList(ConvertArguments(generator, parameterNamingRule, isRecord, arguments.GetWithSeparators())); + return true; + } - private SyntaxNodeOrTokenList ConvertArguments(SyntaxGenerator generator, NamingRule parameterNamingRule, bool isRecord, SyntaxNodeOrTokenList list) - => new(list.Select(v => ConvertArgumentOrToken(generator, parameterNamingRule, isRecord, v))); + private void ReplaceWithObjectCreation( + SyntaxEditor editor, string typeName, ImmutableArray typeParameters, + TNameSyntax qualifiedTypeName, SyntaxNode startingCreationNode, TTupleExpressionSyntax childCreation, + NamingRule parameterNamingRule, bool isRecord) + { + // Use the callback form as tuples types may be nested, and we want to + // properly replace them even in that case. + editor.ReplaceNode( + childCreation, + (currentNode, g) => + { + var currentTupleExpr = (TTupleExpressionSyntax)currentNode; - private SyntaxNodeOrToken ConvertArgumentOrToken(SyntaxGenerator generator, NamingRule parameterNamingRule, bool isRecord, SyntaxNodeOrToken arg) - => arg.IsToken - ? arg - : ConvertArgument(generator, parameterNamingRule, isRecord, (TArgumentSyntax)arg.AsNode()!); + // If we hit the node the user started on, then add the rename annotation here. + var typeNameNode = startingCreationNode == childCreation + ? CreateStructNameNode(g, typeName, typeParameters, addRenameAnnotation: true) + : qualifiedTypeName; - private TArgumentSyntax ConvertArgument( - SyntaxGenerator generator, NamingRule parameterNamingRule, bool isRecord, TArgumentSyntax argument) - { - // If the original arguments had names then we keep them, but convert the case to match the - // the constructor parameters they now refer to. It helps keep the code self-documenting. - // Remove for complex args as it's most likely just clutter a person doesn't need - // when instantiating their new type. - var expr = generator.SyntaxFacts.GetExpressionOfArgument(argument); - if (expr is TLiteralExpressionSyntax) - { - var argumentName = generator.SyntaxFacts.GetNameForArgument(argument); - var newArgumentName = isRecord ? argumentName : parameterNamingRule.NamingStyle.MakeCompliant(argumentName).First(); + var syntaxFacts = g.SyntaxFacts; + syntaxFacts.GetPartsOfTupleExpression( + currentTupleExpr, out var openParen, out var arguments, out var closeParen); + arguments = ConvertArguments(g, parameterNamingRule, isRecord, arguments); - return GetArgumentWithChangedName(argument, newArgumentName); - } + return g.ObjectCreationExpression(typeNameNode, openParen, arguments, closeParen) + .WithAdditionalAnnotations(Formatter.Annotation); + }); + } - return (TArgumentSyntax)generator.Argument(expr).WithTriviaFrom(argument); - } + private SeparatedSyntaxList ConvertArguments(SyntaxGenerator generator, NamingRule parameterNamingRule, bool isRecord, SeparatedSyntaxList arguments) + => generator.SeparatedList(ConvertArguments(generator, parameterNamingRule, isRecord, arguments.GetWithSeparators())); - private static async Task ReplaceMatchingTupleTypesAsync( - Document document, SyntaxEditor editor, SyntaxNode startingNode, - INamedTypeSymbol tupleType, TNameSyntax qualifiedTypeName, - string typeName, ImmutableArray typeParameters, - SyntaxNode containingMember, CancellationToken cancellationToken) + private SyntaxNodeOrTokenList ConvertArguments(SyntaxGenerator generator, NamingRule parameterNamingRule, bool isRecord, SyntaxNodeOrTokenList list) + => new(list.Select(v => ConvertArgumentOrToken(generator, parameterNamingRule, isRecord, v))); + + private SyntaxNodeOrToken ConvertArgumentOrToken(SyntaxGenerator generator, NamingRule parameterNamingRule, bool isRecord, SyntaxNodeOrToken arg) + => arg.IsToken + ? arg + : ConvertArgument(generator, parameterNamingRule, isRecord, (TArgumentSyntax)arg.AsNode()!); + + private TArgumentSyntax ConvertArgument( + SyntaxGenerator generator, NamingRule parameterNamingRule, bool isRecord, TArgumentSyntax argument) + { + // If the original arguments had names then we keep them, but convert the case to match the + // the constructor parameters they now refer to. It helps keep the code self-documenting. + // Remove for complex args as it's most likely just clutter a person doesn't need + // when instantiating their new type. + var expr = generator.SyntaxFacts.GetExpressionOfArgument(argument); + if (expr is TLiteralExpressionSyntax) { - var comparer = document.GetRequiredLanguageService().StringComparer; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var argumentName = generator.SyntaxFacts.GetNameForArgument(argument); + var newArgumentName = isRecord ? argumentName : parameterNamingRule.NamingStyle.MakeCompliant(argumentName).First(); - var childTupleNodes = containingMember.DescendantNodesAndSelf() - .OfType(); + return GetArgumentWithChangedName(argument, newArgumentName); + } - var changed = false; - foreach (var childTupleType in childTupleNodes) - { - if (semanticModel.GetTypeInfo(childTupleType, cancellationToken).Type is not INamedTypeSymbol childType) - { - Debug.Fail("We should always be able to get an tuple type for any tuple type syntax node."); - continue; - } + return (TArgumentSyntax)generator.Argument(expr).WithTriviaFrom(argument); + } - if (AreEquivalent(comparer, tupleType, childType)) - { - changed = true; - ReplaceWithTypeNode( - editor, typeName, typeParameters, qualifiedTypeName, startingNode, childTupleType); - } - } + private static async Task ReplaceMatchingTupleTypesAsync( + Document document, SyntaxEditor editor, SyntaxNode startingNode, + INamedTypeSymbol tupleType, TNameSyntax qualifiedTypeName, + string typeName, ImmutableArray typeParameters, + SyntaxNode containingMember, CancellationToken cancellationToken) + { + var comparer = document.GetRequiredLanguageService().StringComparer; + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - return changed; - } + var childTupleNodes = containingMember.DescendantNodesAndSelf() + .OfType(); - private static void ReplaceWithTypeNode( - SyntaxEditor editor, string typeName, ImmutableArray typeParameters, - TNameSyntax qualifiedTypeName, SyntaxNode startingNode, TTupleTypeSyntax childTupleType) + var changed = false; + foreach (var childTupleType in childTupleNodes) { - // Use the callback form as tuple types may be nested, and we want to - // properly replace them even in that case. - editor.ReplaceNode( - childTupleType, - (currentNode, g) => - { - // If we hit the node the user started on, then add the rename annotation here. - var typeNameNode = startingNode == childTupleType - ? CreateStructNameNode(g, typeName, typeParameters, addRenameAnnotation: true) - : qualifiedTypeName; + if (semanticModel.GetTypeInfo(childTupleType, cancellationToken).Type is not INamedTypeSymbol childType) + { + Debug.Fail("We should always be able to get an tuple type for any tuple type syntax node."); + continue; + } - return typeNameNode.WithTriviaFrom(currentNode); - }); + if (AreEquivalent(comparer, tupleType, childType)) + { + changed = true; + ReplaceWithTypeNode( + editor, typeName, typeParameters, qualifiedTypeName, startingNode, childTupleType); + } } - private static async Task GenerateFinalNamedTypeAsync( - Document document, Scope scope, bool isRecord, string structName, ImmutableArray typeParameters, - INamedTypeSymbol tupleType, NamingRule parameterNamingRule, CancellationToken cancellationToken) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + return changed; + } - var fields = tupleType.TupleElements; + private static void ReplaceWithTypeNode( + SyntaxEditor editor, string typeName, ImmutableArray typeParameters, + TNameSyntax qualifiedTypeName, SyntaxNode startingNode, TTupleTypeSyntax childTupleType) + { + // Use the callback form as tuple types may be nested, and we want to + // properly replace them even in that case. + editor.ReplaceNode( + childTupleType, + (currentNode, g) => + { + // If we hit the node the user started on, then add the rename annotation here. + var typeNameNode = startingNode == childTupleType + ? CreateStructNameNode(g, typeName, typeParameters, addRenameAnnotation: true) + : qualifiedTypeName; - // Now try to generate all the members that will go in the new class. This is a bit - // circular. In order to generate some of the members, we need to know about the type. - // But in order to create the type, we need the members. To address this we do two - // passes. First, we create an empty version of the class. This can then be used to - // help create members like Equals/GetHashCode. Then, once we have all the members we - // create the final type. - var namedTypeWithoutMembers = CreateNamedType( - semanticModel.Compilation.Assembly, scope, isRecord, structName, typeParameters, members: default); + return typeNameNode.WithTriviaFrom(currentNode); + }); + } - var generator = SyntaxGenerator.GetGenerator(document); + private static async Task GenerateFinalNamedTypeAsync( + Document document, Scope scope, bool isRecord, string structName, ImmutableArray typeParameters, + INamedTypeSymbol tupleType, NamingRule parameterNamingRule, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var constructor = CreateConstructor(semanticModel, isRecord, structName, fields, generator, parameterNamingRule); + var fields = tupleType.TupleElements; - // Generate Equals/GetHashCode. We can defer to our existing language service for this - // so that we generate the same Equals/GetHashCode that our other IDE features generate. - var equalsAndGetHashCodeService = document.GetRequiredLanguageService(); + // Now try to generate all the members that will go in the new class. This is a bit + // circular. In order to generate some of the members, we need to know about the type. + // But in order to create the type, we need the members. To address this we do two + // passes. First, we create an empty version of the class. This can then be used to + // help create members like Equals/GetHashCode. Then, once we have all the members we + // create the final type. + var namedTypeWithoutMembers = CreateNamedType( + semanticModel.Compilation.Assembly, scope, isRecord, structName, typeParameters, members: default); - var equalsMethod = await equalsAndGetHashCodeService.GenerateEqualsMethodAsync( - document, namedTypeWithoutMembers, ImmutableArray.CastUp(fields), - localNameOpt: SyntaxGeneratorExtensions.OtherName, cancellationToken).ConfigureAwait(false); - var getHashCodeMethod = await equalsAndGetHashCodeService.GenerateGetHashCodeMethodAsync( - document, namedTypeWithoutMembers, - ImmutableArray.CastUp(fields), cancellationToken).ConfigureAwait(false); + var generator = SyntaxGenerator.GetGenerator(document); - using var _ = ArrayBuilder.GetInstance(out var members); + var constructor = CreateConstructor(semanticModel, isRecord, structName, fields, generator, parameterNamingRule); - // A record doesn't need fields because we always use a primary constructor - if (!isRecord) - members.AddRange(fields); + // Generate Equals/GetHashCode. We can defer to our existing language service for this + // so that we generate the same Equals/GetHashCode that our other IDE features generate. + var equalsAndGetHashCodeService = document.GetRequiredLanguageService(); - members.Add(constructor); + var equalsMethod = await equalsAndGetHashCodeService.GenerateEqualsMethodAsync( + document, namedTypeWithoutMembers, ImmutableArray.CastUp(fields), + localNameOpt: SyntaxGeneratorExtensions.OtherName, cancellationToken).ConfigureAwait(false); + var getHashCodeMethod = await equalsAndGetHashCodeService.GenerateGetHashCodeMethodAsync( + document, namedTypeWithoutMembers, + ImmutableArray.CastUp(fields), cancellationToken).ConfigureAwait(false); - // No need to generate Equals/GetHashCode/Deconstruct in a record. The compiler already synthesizes those for us. - if (!isRecord) - { - members.Add(equalsMethod); - members.Add(getHashCodeMethod); - members.Add(GenerateDeconstructMethod(semanticModel, generator, tupleType, constructor)); - } + using var _ = ArrayBuilder.GetInstance(out var members); - AddConversions(generator, members, tupleType, namedTypeWithoutMembers); + // A record doesn't need fields because we always use a primary constructor + if (!isRecord) + members.AddRange(fields); - var namedTypeSymbol = CreateNamedType( - semanticModel.Compilation.Assembly, scope, isRecord, structName, typeParameters, members.ToImmutable()); - return namedTypeSymbol; - } + members.Add(constructor); - private static IMethodSymbol GenerateDeconstructMethod( - SemanticModel model, SyntaxGenerator generator, - INamedTypeSymbol tupleType, IMethodSymbol constructor) + // No need to generate Equals/GetHashCode/Deconstruct in a record. The compiler already synthesizes those for us. + if (!isRecord) { - var assignments = tupleType.TupleElements.Select( - (field, index) => generator.ExpressionStatement( - generator.AssignmentStatement( - generator.IdentifierName(constructor.Parameters[index].Name), - generator.MemberAccessExpression( - generator.ThisExpression(), - field.Name)))).ToImmutableArray(); - - return CodeGenerationSymbolFactory.CreateMethodSymbol( - attributes: default, - Accessibility.Public, - modifiers: default, - model.Compilation.GetSpecialType(SpecialType.System_Void), - RefKind.None, - explicitInterfaceImplementations: default, - WellKnownMemberNames.DeconstructMethodName, - typeParameters: default, - constructor.Parameters.SelectAsArray(p => - CodeGenerationSymbolFactory.CreateParameterSymbol(RefKind.Out, p.Type, p.Name)), - assignments); + members.Add(equalsMethod); + members.Add(getHashCodeMethod); + members.Add(GenerateDeconstructMethod(semanticModel, generator, tupleType, constructor)); } - private static void AddConversions( - SyntaxGenerator generator, ArrayBuilder members, - INamedTypeSymbol tupleType, INamedTypeSymbol structType) - { - const string ValueName = "value"; - - var valueNode = generator.IdentifierName(ValueName); - var arguments = tupleType.TupleElements.SelectAsArray( - field => generator.Argument( - generator.MemberAccessExpression(valueNode, field.Name))); - - var convertToTupleStatement = generator.ReturnStatement( - generator.TupleExpression(arguments)); - - var convertToStructStatement = generator.ReturnStatement( - generator.ObjectCreationExpression(structType, arguments)); - - members.Add(CodeGenerationSymbolFactory.CreateConversionSymbol( - tupleType, - CodeGenerationSymbolFactory.CreateParameterSymbol(structType, ValueName), - isImplicit: true, - statements: [convertToTupleStatement])); - members.Add(CodeGenerationSymbolFactory.CreateConversionSymbol( - structType, - CodeGenerationSymbolFactory.CreateParameterSymbol(tupleType, ValueName), - isImplicit: true, - statements: [convertToStructStatement])); - } + AddConversions(generator, members, tupleType, namedTypeWithoutMembers); - private static INamedTypeSymbol CreateNamedType( - IAssemblySymbol containingAssembly, - Scope scope, bool isRecord, string structName, - ImmutableArray typeParameters, ImmutableArray members) - { - var accessibility = scope == Scope.DependentProjects - ? Accessibility.Public - : Accessibility.Internal; - return CodeGenerationSymbolFactory.CreateNamedTypeSymbol( - attributes: default, accessibility, modifiers: default, isRecord, - TypeKind.Struct, structName, typeParameters, members: members, containingAssembly: containingAssembly); - } + var namedTypeSymbol = CreateNamedType( + semanticModel.Compilation.Assembly, scope, isRecord, structName, typeParameters, members.ToImmutable()); + return namedTypeSymbol; + } - private static IMethodSymbol CreateConstructor( - SemanticModel semanticModel, bool isRecord, string className, - ImmutableArray fields, SyntaxGenerator generator, - NamingRule parameterNamingRule) + private static IMethodSymbol GenerateDeconstructMethod( + SemanticModel model, SyntaxGenerator generator, + INamedTypeSymbol tupleType, IMethodSymbol constructor) + { + var assignments = tupleType.TupleElements.Select( + (field, index) => generator.ExpressionStatement( + generator.AssignmentStatement( + generator.IdentifierName(constructor.Parameters[index].Name), + generator.MemberAccessExpression( + generator.ThisExpression(), + field.Name)))).ToImmutableArray(); + + return CodeGenerationSymbolFactory.CreateMethodSymbol( + attributes: default, + Accessibility.Public, + modifiers: default, + model.Compilation.GetSpecialType(SpecialType.System_Void), + RefKind.None, + explicitInterfaceImplementations: default, + WellKnownMemberNames.DeconstructMethodName, + typeParameters: default, + constructor.Parameters.SelectAsArray(p => + CodeGenerationSymbolFactory.CreateParameterSymbol(RefKind.Out, p.Type, p.Name)), + assignments); + } + + private static void AddConversions( + SyntaxGenerator generator, ArrayBuilder members, + INamedTypeSymbol tupleType, INamedTypeSymbol structType) + { + const string ValueName = "value"; + + var valueNode = generator.IdentifierName(ValueName); + var arguments = tupleType.TupleElements.SelectAsArray( + field => generator.Argument( + generator.MemberAccessExpression(valueNode, field.Name))); + + var convertToTupleStatement = generator.ReturnStatement( + generator.TupleExpression(arguments)); + + var convertToStructStatement = generator.ReturnStatement( + generator.ObjectCreationExpression(structType, arguments)); + + members.Add(CodeGenerationSymbolFactory.CreateConversionSymbol( + tupleType, + CodeGenerationSymbolFactory.CreateParameterSymbol(structType, ValueName), + isImplicit: true, + statements: [convertToTupleStatement])); + members.Add(CodeGenerationSymbolFactory.CreateConversionSymbol( + structType, + CodeGenerationSymbolFactory.CreateParameterSymbol(tupleType, ValueName), + isImplicit: true, + statements: [convertToStructStatement])); + } + + private static INamedTypeSymbol CreateNamedType( + IAssemblySymbol containingAssembly, + Scope scope, bool isRecord, string structName, + ImmutableArray typeParameters, ImmutableArray members) + { + var accessibility = scope == Scope.DependentProjects + ? Accessibility.Public + : Accessibility.Internal; + return CodeGenerationSymbolFactory.CreateNamedTypeSymbol( + attributes: default, accessibility, modifiers: default, isRecord, + TypeKind.Struct, structName, typeParameters, members: members, containingAssembly: containingAssembly); + } + + private static IMethodSymbol CreateConstructor( + SemanticModel semanticModel, bool isRecord, string className, + ImmutableArray fields, SyntaxGenerator generator, + NamingRule parameterNamingRule) + { + // For every property, create a corresponding parameter, as well as an assignment + // statement from that parameter to the property. + using var _ = PooledDictionary.GetInstance(out var parameterToPropMap); + var parameters = fields.SelectAsArray(field => { - // For every property, create a corresponding parameter, as well as an assignment - // statement from that parameter to the property. - using var _ = PooledDictionary.GetInstance(out var parameterToPropMap); - var parameters = fields.SelectAsArray(field => - { - var parameterName = isRecord ? field.Name : parameterNamingRule.NamingStyle.MakeCompliant(field.Name).First(); - var parameter = CodeGenerationSymbolFactory.CreateParameterSymbol( - field.Type, parameterName); + var parameterName = isRecord ? field.Name : parameterNamingRule.NamingStyle.MakeCompliant(field.Name).First(); + var parameter = CodeGenerationSymbolFactory.CreateParameterSymbol( + field.Type, parameterName); - parameterToPropMap[parameter.Name] = field; + parameterToPropMap[parameter.Name] = field; - return parameter; - }); + return parameter; + }); - var assignmentStatements = generator.CreateAssignmentStatements( - semanticModel, parameters, parameterToPropMap, ImmutableDictionary.Empty, - addNullChecks: false, preferThrowExpression: false); + var assignmentStatements = generator.CreateAssignmentStatements( + semanticModel, parameters, parameterToPropMap, ImmutableDictionary.Empty, + addNullChecks: false, preferThrowExpression: false); - var constructor = CodeGenerationSymbolFactory.CreateConstructorSymbol( - attributes: default, Accessibility.Public, modifiers: default, - className, parameters, assignmentStatements, isPrimaryConstructor: isRecord); + var constructor = CodeGenerationSymbolFactory.CreateConstructorSymbol( + attributes: default, Accessibility.Public, modifiers: default, + className, parameters, assignmentStatements, isPrimaryConstructor: isRecord); - return constructor; - } + return constructor; } } diff --git a/src/Features/Core/Portable/ConvertTupleToStruct/DocumentToUpdate.cs b/src/Features/Core/Portable/ConvertTupleToStruct/DocumentToUpdate.cs index fa49cfc69bc00..e800dc1df989b 100644 --- a/src/Features/Core/Portable/ConvertTupleToStruct/DocumentToUpdate.cs +++ b/src/Features/Core/Portable/ConvertTupleToStruct/DocumentToUpdate.cs @@ -4,19 +4,18 @@ using System.Collections.Immutable; -namespace Microsoft.CodeAnalysis.ConvertTupleToStruct +namespace Microsoft.CodeAnalysis.ConvertTupleToStruct; + +internal readonly struct DocumentToUpdate(Document document, ImmutableArray nodesToUpdate) { - internal readonly struct DocumentToUpdate(Document document, ImmutableArray nodesToUpdate) - { - /// - /// The document to update. - /// - public readonly Document Document = document; + /// + /// The document to update. + /// + public readonly Document Document = document; - /// - /// The subnodes in this document to walk and update. If empty, the entire document - /// should be walked. - /// - public readonly ImmutableArray NodesToUpdate = nodesToUpdate; - } + /// + /// The subnodes in this document to walk and update. If empty, the entire document + /// should be walked. + /// + public readonly ImmutableArray NodesToUpdate = nodesToUpdate; } diff --git a/src/Features/Core/Portable/ConvertTupleToStruct/IConvertTupleToStructCodeRefactoringProvider.cs b/src/Features/Core/Portable/ConvertTupleToStruct/IConvertTupleToStructCodeRefactoringProvider.cs index 9b1b77056b8d3..1d87468cfd10c 100644 --- a/src/Features/Core/Portable/ConvertTupleToStruct/IConvertTupleToStructCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertTupleToStruct/IConvertTupleToStructCodeRefactoringProvider.cs @@ -10,11 +10,10 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ConvertTupleToStruct +namespace Microsoft.CodeAnalysis.ConvertTupleToStruct; + +internal interface IConvertTupleToStructCodeRefactoringProvider : ILanguageService { - internal interface IConvertTupleToStructCodeRefactoringProvider : ILanguageService - { - Task ConvertToStructAsync( - Document document, TextSpan span, Scope scope, CleanCodeGenerationOptionsProvider fallbackOptions, bool isRecord, CancellationToken cancellationToken); - } + Task ConvertToStructAsync( + Document document, TextSpan span, Scope scope, CleanCodeGenerationOptionsProvider fallbackOptions, bool isRecord, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/ConvertTupleToStruct/IRemoteConvertTupleToStructCodeRefactoringService.cs b/src/Features/Core/Portable/ConvertTupleToStruct/IRemoteConvertTupleToStructCodeRefactoringService.cs index ac63f41aabd3a..2a953a2df0e3e 100644 --- a/src/Features/Core/Portable/ConvertTupleToStruct/IRemoteConvertTupleToStructCodeRefactoringService.cs +++ b/src/Features/Core/Portable/ConvertTupleToStruct/IRemoteConvertTupleToStructCodeRefactoringService.cs @@ -15,46 +15,45 @@ using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.ConvertTupleToStruct +namespace Microsoft.CodeAnalysis.ConvertTupleToStruct; + +internal interface IRemoteConvertTupleToStructCodeRefactoringService { - internal interface IRemoteConvertTupleToStructCodeRefactoringService + internal interface ICallback : IRemoteOptionsCallback { - internal interface ICallback : IRemoteOptionsCallback - { - } - - ValueTask ConvertToStructAsync( - Checksum solutionChecksum, - RemoteServiceCallbackId callbackId, - DocumentId documentId, - TextSpan span, - Scope scope, - bool isRecord, - CancellationToken cancellationToken); } - [ExportRemoteServiceCallbackDispatcher(typeof(IRemoteConvertTupleToStructCodeRefactoringService)), Shared] - internal sealed class RemoteConvertTupleToStructCodeRefactoringServiceCallbackDispatcher : RemoteServiceCallbackDispatcher, IRemoteConvertTupleToStructCodeRefactoringService.ICallback + ValueTask ConvertToStructAsync( + Checksum solutionChecksum, + RemoteServiceCallbackId callbackId, + DocumentId documentId, + TextSpan span, + Scope scope, + bool isRecord, + CancellationToken cancellationToken); +} + +[ExportRemoteServiceCallbackDispatcher(typeof(IRemoteConvertTupleToStructCodeRefactoringService)), Shared] +internal sealed class RemoteConvertTupleToStructCodeRefactoringServiceCallbackDispatcher : RemoteServiceCallbackDispatcher, IRemoteConvertTupleToStructCodeRefactoringService.ICallback +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RemoteConvertTupleToStructCodeRefactoringServiceCallbackDispatcher() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public RemoteConvertTupleToStructCodeRefactoringServiceCallbackDispatcher() - { - } - - public ValueTask GetOptionsAsync(RemoteServiceCallbackId callbackId, string language, CancellationToken cancellationToken) - => ((RemoteOptionsProvider)GetCallback(callbackId)).GetOptionsAsync(language, cancellationToken); } - [DataContract] - internal readonly struct SerializableConvertTupleToStructResult( - ImmutableArray<(DocumentId, ImmutableArray)> documentTextChanges, - (DocumentId, TextSpan) renamedToken) - { - [DataMember(Order = 0)] - public readonly ImmutableArray<(DocumentId, ImmutableArray)> DocumentTextChanges = documentTextChanges; + public ValueTask GetOptionsAsync(RemoteServiceCallbackId callbackId, string language, CancellationToken cancellationToken) + => ((RemoteOptionsProvider)GetCallback(callbackId)).GetOptionsAsync(language, cancellationToken); +} - [DataMember(Order = 1)] - public readonly (DocumentId, TextSpan) RenamedToken = renamedToken; - } +[DataContract] +internal readonly struct SerializableConvertTupleToStructResult( + ImmutableArray<(DocumentId, ImmutableArray)> documentTextChanges, + (DocumentId, TextSpan) renamedToken) +{ + [DataMember(Order = 0)] + public readonly ImmutableArray<(DocumentId, ImmutableArray)> DocumentTextChanges = documentTextChanges; + + [DataMember(Order = 1)] + public readonly (DocumentId, TextSpan) RenamedToken = renamedToken; } diff --git a/src/Features/Core/Portable/ConvertTupleToStruct/Scope.cs b/src/Features/Core/Portable/ConvertTupleToStruct/Scope.cs index c5c991caf0c60..3d648a1f1cc9a 100644 --- a/src/Features/Core/Portable/ConvertTupleToStruct/Scope.cs +++ b/src/Features/Core/Portable/ConvertTupleToStruct/Scope.cs @@ -2,13 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.ConvertTupleToStruct +namespace Microsoft.CodeAnalysis.ConvertTupleToStruct; + +internal enum Scope { - internal enum Scope - { - ContainingMember, - ContainingType, - ContainingProject, - DependentProjects - } + ContainingMember, + ContainingType, + ContainingProject, + DependentProjects } diff --git a/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.NameAndArity.cs b/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.NameAndArity.cs index 715739bce9428..2ef9e72dc358a 100644 --- a/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.NameAndArity.cs +++ b/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.NameAndArity.cs @@ -4,14 +4,13 @@ #nullable disable -namespace Microsoft.CodeAnalysis.Debugging +namespace Microsoft.CodeAnalysis.Debugging; + +internal partial class AbstractBreakpointResolver { - internal partial class AbstractBreakpointResolver + protected struct NameAndArity(string name, int arity) { - protected struct NameAndArity(string name, int arity) - { - public string Name = name; - public int Arity = arity; - } + public string Name = name; + public int Arity = arity; } } diff --git a/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.cs b/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.cs index d1e900bad7f9d..96af9c567e86b 100644 --- a/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.cs +++ b/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.cs @@ -17,281 +17,280 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Debugging +namespace Microsoft.CodeAnalysis.Debugging; + +internal abstract partial class AbstractBreakpointResolver { - internal abstract partial class AbstractBreakpointResolver + // I believe this is a close approximation of the IVsDebugName string format produced + // by the native language service implementations: + // + // C#: csharp\radmanaged\DebuggerInteraction\BreakpointNameResolver.cs + // VB: vb\Language\VsEditor\Debugging\VsLanguageDebugInfo.vb + // + // The one clear deviation from the native implementation is VB properties. Resolving + // the name of a property in VB used to return all the accessor methods (using their + // metadata names) because setting a breakpoint directly on a property isn't supported + // in VB. In Roslyn, we'll keep things consistent and just return the property name. + // This means that VB users won't be able to set breakpoints on property accessors using + // Ctrl+B, but it would seem that a better solution to this problem would be to simply + // enable setting breakpoints on all accessors by setting a breakpoint on the property + // declaration (same as C# behavior). + private static readonly SymbolDisplayFormat s_vsDebugNameFormat = + new( + globalNamespaceStyle: + SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: + SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: + SymbolDisplayGenericsOptions.IncludeTypeParameters, + memberOptions: + SymbolDisplayMemberOptions.IncludeContainingType | + SymbolDisplayMemberOptions.IncludeParameters, + parameterOptions: + SymbolDisplayParameterOptions.IncludeOptionalBrackets | + SymbolDisplayParameterOptions.IncludeType, + propertyStyle: + SymbolDisplayPropertyStyle.NameOnly, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | + SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + protected readonly string Text; + private readonly string _language; + private readonly Solution _solution; + private readonly IEqualityComparer _identifierComparer; + + protected AbstractBreakpointResolver( + Solution solution, + string text, + string language, + IEqualityComparer identifierComparer) { - // I believe this is a close approximation of the IVsDebugName string format produced - // by the native language service implementations: - // - // C#: csharp\radmanaged\DebuggerInteraction\BreakpointNameResolver.cs - // VB: vb\Language\VsEditor\Debugging\VsLanguageDebugInfo.vb - // - // The one clear deviation from the native implementation is VB properties. Resolving - // the name of a property in VB used to return all the accessor methods (using their - // metadata names) because setting a breakpoint directly on a property isn't supported - // in VB. In Roslyn, we'll keep things consistent and just return the property name. - // This means that VB users won't be able to set breakpoints on property accessors using - // Ctrl+B, but it would seem that a better solution to this problem would be to simply - // enable setting breakpoints on all accessors by setting a breakpoint on the property - // declaration (same as C# behavior). - private static readonly SymbolDisplayFormat s_vsDebugNameFormat = - new( - globalNamespaceStyle: - SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: - SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - genericsOptions: - SymbolDisplayGenericsOptions.IncludeTypeParameters, - memberOptions: - SymbolDisplayMemberOptions.IncludeContainingType | - SymbolDisplayMemberOptions.IncludeParameters, - parameterOptions: - SymbolDisplayParameterOptions.IncludeOptionalBrackets | - SymbolDisplayParameterOptions.IncludeType, - propertyStyle: - SymbolDisplayPropertyStyle.NameOnly, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - protected readonly string Text; - private readonly string _language; - private readonly Solution _solution; - private readonly IEqualityComparer _identifierComparer; - - protected AbstractBreakpointResolver( - Solution solution, - string text, - string language, - IEqualityComparer identifierComparer) - { - _solution = solution; - Text = text; - _language = language; - _identifierComparer = identifierComparer; - } + _solution = solution; + Text = text; + _language = language; + _identifierComparer = identifierComparer; + } - protected abstract void ParseText(out IList nameParts, out int? parameterCount); - protected abstract IEnumerable GetMembers(INamedTypeSymbol type, string name); - protected abstract bool HasMethodBody(IMethodSymbol method, CancellationToken cancellationToken); + protected abstract void ParseText(out IList nameParts, out int? parameterCount); + protected abstract IEnumerable GetMembers(INamedTypeSymbol type, string name); + protected abstract bool HasMethodBody(IMethodSymbol method, CancellationToken cancellationToken); - private BreakpointResolutionResult CreateBreakpoint(ISymbol methodSymbol) - { - var location = methodSymbol.Locations.First(loc => loc.IsInSource); + private BreakpointResolutionResult CreateBreakpoint(ISymbol methodSymbol) + { + var location = methodSymbol.Locations.First(loc => loc.IsInSource); - var document = _solution.GetDocument(location.SourceTree); - var textSpan = new TextSpan(location.SourceSpan.Start, 0); - var vsDebugName = methodSymbol.ToDisplayString(s_vsDebugNameFormat); + var document = _solution.GetDocument(location.SourceTree); + var textSpan = new TextSpan(location.SourceSpan.Start, 0); + var vsDebugName = methodSymbol.ToDisplayString(s_vsDebugNameFormat); - return BreakpointResolutionResult.CreateSpanResult(document, textSpan, vsDebugName); - } + return BreakpointResolutionResult.CreateSpanResult(document, textSpan, vsDebugName); + } - public async Task> DoAsync(CancellationToken cancellationToken) + public async Task> DoAsync(CancellationToken cancellationToken) + { + try { - try - { - ParseText(out var nameParts, out var parameterCount); - - // Notes: In C#, indexers can't be resolved by any name. This is acceptable, because the old language - // service wasn't able to resolve them either. In VB, parameterized properties will work in - // the same way as any other property. - // Destructors in C# can be resolved using the method name "Finalize". The resulting string - // representation will use C# language format ("C.~C()"). I verified that this works with - // "Break at Function" (breakpoint is correctly set and can be hit), so I don't see a reason - // to prohibit this (even though the old language service didn't support it). - var members = await FindMembersAsync(nameParts, cancellationToken).ConfigureAwait(false); - - // Filter down the list of symbols to "applicable methods", specifically: - // - "regular" methods - // - constructors - // - destructors - // - properties - // - operators? - // - conversions? - // where "applicable" means that the method or property represents a valid place to set a breakpoint - // and that it has the expected number of parameters - return members.Where(m => IsApplicable(m, parameterCount, cancellationToken)). - Select(CreateBreakpoint).ToImmutableArrayOrEmpty(); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return []; - } + ParseText(out var nameParts, out var parameterCount); + + // Notes: In C#, indexers can't be resolved by any name. This is acceptable, because the old language + // service wasn't able to resolve them either. In VB, parameterized properties will work in + // the same way as any other property. + // Destructors in C# can be resolved using the method name "Finalize". The resulting string + // representation will use C# language format ("C.~C()"). I verified that this works with + // "Break at Function" (breakpoint is correctly set and can be hit), so I don't see a reason + // to prohibit this (even though the old language service didn't support it). + var members = await FindMembersAsync(nameParts, cancellationToken).ConfigureAwait(false); + + // Filter down the list of symbols to "applicable methods", specifically: + // - "regular" methods + // - constructors + // - destructors + // - properties + // - operators? + // - conversions? + // where "applicable" means that the method or property represents a valid place to set a breakpoint + // and that it has the expected number of parameters + return members.Where(m => IsApplicable(m, parameterCount, cancellationToken)). + Select(CreateBreakpoint).ToImmutableArrayOrEmpty(); } - - private async Task> FindMembersAsync( - IList nameParts, CancellationToken cancellationToken) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - try - { - switch (nameParts.Count) - { - case 0: - // If there were no name parts, then we don't have any members to return. - // We only expect to hit this condition when the name provided does not parse. - return SpecializedCollections.EmptyList(); - - case 1: - // They're just searching for a method name. Have to look through every type to find - // it. - return FindMembers(await GetAllTypesAsync(cancellationToken).ConfigureAwait(false), nameParts[0]); - - case 2: - // They have a type name and a method name. Find a type with a matching name and a - // method in that type. - var types = await GetAllTypesAsync(cancellationToken).ConfigureAwait(false); - types = types.Where(t => MatchesName(t, nameParts[0], _identifierComparer)); - return FindMembers(types, nameParts[1]); - - default: - // They have a namespace or nested type qualified name. Walk up to the root namespace trying to match. - var containers = await _solution.GetGlobalNamespacesAsync(cancellationToken).ConfigureAwait(false); - return FindMembers(containers, nameParts.ToArray()); - } - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return []; - } + return []; } + } - private static bool MatchesName(INamespaceOrTypeSymbol typeOrNamespace, NameAndArity nameAndArity, IEqualityComparer comparer) + private async Task> FindMembersAsync( + IList nameParts, CancellationToken cancellationToken) + { + try { - switch (typeOrNamespace) + switch (nameParts.Count) { - case INamespaceSymbol namespaceSymbol: - return comparer.Equals(namespaceSymbol.Name, nameAndArity.Name) && nameAndArity.Arity == 0; - case INamedTypeSymbol typeSymbol: - return comparer.Equals(typeSymbol.Name, nameAndArity.Name) && - (nameAndArity.Arity == 0 || nameAndArity.Arity == typeSymbol.TypeArguments.Length); + case 0: + // If there were no name parts, then we don't have any members to return. + // We only expect to hit this condition when the name provided does not parse. + return SpecializedCollections.EmptyList(); + + case 1: + // They're just searching for a method name. Have to look through every type to find + // it. + return FindMembers(await GetAllTypesAsync(cancellationToken).ConfigureAwait(false), nameParts[0]); + + case 2: + // They have a type name and a method name. Find a type with a matching name and a + // method in that type. + var types = await GetAllTypesAsync(cancellationToken).ConfigureAwait(false); + types = types.Where(t => MatchesName(t, nameParts[0], _identifierComparer)); + return FindMembers(types, nameParts[1]); + default: - return false; + // They have a namespace or nested type qualified name. Walk up to the root namespace trying to match. + var containers = await _solution.GetGlobalNamespacesAsync(cancellationToken).ConfigureAwait(false); + return FindMembers(containers, nameParts.ToArray()); } } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + return []; + } + } - private static bool MatchesNames(INamedTypeSymbol type, NameAndArity[] names, IEqualityComparer comparer) + private static bool MatchesName(INamespaceOrTypeSymbol typeOrNamespace, NameAndArity nameAndArity, IEqualityComparer comparer) + { + switch (typeOrNamespace) { - Debug.Assert(type != null); - Debug.Assert(names.Length >= 2); + case INamespaceSymbol namespaceSymbol: + return comparer.Equals(namespaceSymbol.Name, nameAndArity.Name) && nameAndArity.Arity == 0; + case INamedTypeSymbol typeSymbol: + return comparer.Equals(typeSymbol.Name, nameAndArity.Name) && + (nameAndArity.Arity == 0 || nameAndArity.Arity == typeSymbol.TypeArguments.Length); + default: + return false; + } + } + + private static bool MatchesNames(INamedTypeSymbol type, NameAndArity[] names, IEqualityComparer comparer) + { + Debug.Assert(type != null); + Debug.Assert(names.Length >= 2); - INamespaceOrTypeSymbol container = type; + INamespaceOrTypeSymbol container = type; - // The last element in "names" is the method/property name, but we're only matching against types here, - // so we'll skip the last one. - for (var i = names.Length - 2; i >= 0; i--) + // The last element in "names" is the method/property name, but we're only matching against types here, + // so we'll skip the last one. + for (var i = names.Length - 2; i >= 0; i--) + { + if (!MatchesName(container, names[i], comparer)) { - if (!MatchesName(container, names[i], comparer)) - { - return false; - } - - container = ((INamespaceOrTypeSymbol)container.ContainingType) ?? container.ContainingNamespace; - - // We ran out of containers to match against before we matched all the names, so this type isn't a match. - if (container == null && i > 0) - { - return false; - } + return false; } - return true; + container = ((INamespaceOrTypeSymbol)container.ContainingType) ?? container.ContainingNamespace; + + // We ran out of containers to match against before we matched all the names, so this type isn't a match. + if (container == null && i > 0) + { + return false; + } } - private IEnumerable FindMembers(IEnumerable containers, params NameAndArity[] names) - { - // Recursively expand the list of containers to include all types in all nested containers, then filter down to a - // set of candidate types by walking the up the enclosing containers matching by simple name. - var types = containers.SelectMany(GetTypeMembersRecursive).Where(t => MatchesNames(t, names, _identifierComparer)); + return true; + } - var lastName = names.Last(); + private IEnumerable FindMembers(IEnumerable containers, params NameAndArity[] names) + { + // Recursively expand the list of containers to include all types in all nested containers, then filter down to a + // set of candidate types by walking the up the enclosing containers matching by simple name. + var types = containers.SelectMany(GetTypeMembersRecursive).Where(t => MatchesNames(t, names, _identifierComparer)); - return FindMembers(types, lastName); - } + var lastName = names.Last(); + + return FindMembers(types, lastName); + } + + private IEnumerable FindMembers(IEnumerable types, NameAndArity nameAndArity) + { + // Get the matching members from all types (including constructors and explicit interface + // implementations). If there is a partial method, prefer returning the implementation over + // the definition (since the definition will not be a candidate for setting a breakpoint). + var members = types.SelectMany(t => GetMembers(t, nameAndArity.Name)) + .Select(s => GetPartialImplementationPartOrNull(s) ?? s); + + return nameAndArity.Arity == 0 + ? members + : members.OfType().Where(m => m.TypeParameters.Length == nameAndArity.Arity); + } + + private async Task> GetAllTypesAsync(CancellationToken cancellationToken) + { + var namespaces = await _solution.GetGlobalNamespacesAsync(cancellationToken).ConfigureAwait(false); + return namespaces.GetAllTypes(cancellationToken); + } + + private static IMethodSymbol GetPartialImplementationPartOrNull(ISymbol symbol) + => (symbol.Kind == SymbolKind.Method) ? ((IMethodSymbol)symbol).PartialImplementationPart : null; - private IEnumerable FindMembers(IEnumerable types, NameAndArity nameAndArity) + /// + /// Is this method or property a valid place to set a breakpoint and does it match the expected parameter count? + /// + private bool IsApplicable(ISymbol methodOrProperty, int? parameterCount, CancellationToken cancellationToken) + { + // You can only set a breakpoint on methods (including constructors/destructors) and properties. + var kind = methodOrProperty.Kind; + if (kind is not (SymbolKind.Method or SymbolKind.Property)) { - // Get the matching members from all types (including constructors and explicit interface - // implementations). If there is a partial method, prefer returning the implementation over - // the definition (since the definition will not be a candidate for setting a breakpoint). - var members = types.SelectMany(t => GetMembers(t, nameAndArity.Name)) - .Select(s => GetPartialImplementationPartOrNull(s) ?? s); - - return nameAndArity.Arity == 0 - ? members - : members.OfType().Where(m => m.TypeParameters.Length == nameAndArity.Arity); + return false; } - private async Task> GetAllTypesAsync(CancellationToken cancellationToken) + // You can't set a breakpoint on an abstract method or property. + if (methodOrProperty.IsAbstract) { - var namespaces = await _solution.GetGlobalNamespacesAsync(cancellationToken).ConfigureAwait(false); - return namespaces.GetAllTypes(cancellationToken); + return false; } - private static IMethodSymbol GetPartialImplementationPartOrNull(ISymbol symbol) - => (symbol.Kind == SymbolKind.Method) ? ((IMethodSymbol)symbol).PartialImplementationPart : null; - - /// - /// Is this method or property a valid place to set a breakpoint and does it match the expected parameter count? - /// - private bool IsApplicable(ISymbol methodOrProperty, int? parameterCount, CancellationToken cancellationToken) + // If parameters were provided, check to make sure the method or property has the expected number + // of parameters (but we don't actually validate the type or name of the supplied parameters). + if (parameterCount != null) { - // You can only set a breakpoint on methods (including constructors/destructors) and properties. - var kind = methodOrProperty.Kind; - if (kind is not (SymbolKind.Method or SymbolKind.Property)) - { - return false; - } + var mismatch = IsMismatch(methodOrProperty, parameterCount); - // You can't set a breakpoint on an abstract method or property. - if (methodOrProperty.IsAbstract) + if (mismatch) { return false; } + } - // If parameters were provided, check to make sure the method or property has the expected number - // of parameters (but we don't actually validate the type or name of the supplied parameters). - if (parameterCount != null) - { - var mismatch = IsMismatch(methodOrProperty, parameterCount); - - if (mismatch) - { - return false; - } - } - - // Finally, check to make sure we have source, and if we've got a method symbol, make sure it - // has a body to set a breakpoint on. - if ((methodOrProperty.Language == _language) && methodOrProperty.Locations.Any(static location => location.IsInSource)) + // Finally, check to make sure we have source, and if we've got a method symbol, make sure it + // has a body to set a breakpoint on. + if ((methodOrProperty.Language == _language) && methodOrProperty.Locations.Any(static location => location.IsInSource)) + { + if (methodOrProperty.IsKind(SymbolKind.Method)) { - if (methodOrProperty.IsKind(SymbolKind.Method)) - { - return HasMethodBody((IMethodSymbol)methodOrProperty, cancellationToken); - } - - // Non-abstract properties are always applicable, because you can set a breakpoint on the - // accessor methods (get and/or set). - return true; + return HasMethodBody((IMethodSymbol)methodOrProperty, cancellationToken); } - return false; + // Non-abstract properties are always applicable, because you can set a breakpoint on the + // accessor methods (get and/or set). + return true; } - private static bool IsMismatch(ISymbol methodOrProperty, int? parameterCount) - => methodOrProperty switch - { - IMethodSymbol method => method.Parameters.Length != parameterCount, - IPropertySymbol property => property.Parameters.Length != parameterCount, - _ => false, - }; - - private static IEnumerable GetTypeMembersRecursive(INamespaceOrTypeSymbol container) - => container switch - { - INamespaceSymbol namespaceSymbol => namespaceSymbol.GetMembers().SelectMany(GetTypeMembersRecursive), - INamedTypeSymbol typeSymbol => typeSymbol.GetTypeMembers().SelectMany(GetTypeMembersRecursive).Concat(typeSymbol), - _ => null, - }; + return false; } + + private static bool IsMismatch(ISymbol methodOrProperty, int? parameterCount) + => methodOrProperty switch + { + IMethodSymbol method => method.Parameters.Length != parameterCount, + IPropertySymbol property => property.Parameters.Length != parameterCount, + _ => false, + }; + + private static IEnumerable GetTypeMembersRecursive(INamespaceOrTypeSymbol container) + => container switch + { + INamespaceSymbol namespaceSymbol => namespaceSymbol.GetMembers().SelectMany(GetTypeMembersRecursive), + INamedTypeSymbol typeSymbol => typeSymbol.GetTypeMembers().SelectMany(GetTypeMembersRecursive).Concat(typeSymbol), + _ => null, + }; } diff --git a/src/Features/Core/Portable/Debugging/BreakpointResolutionResult.cs b/src/Features/Core/Portable/Debugging/BreakpointResolutionResult.cs index 89d16befc4f0d..f7a3642ded202 100644 --- a/src/Features/Core/Portable/Debugging/BreakpointResolutionResult.cs +++ b/src/Features/Core/Portable/Debugging/BreakpointResolutionResult.cs @@ -4,27 +4,26 @@ using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Debugging +namespace Microsoft.CodeAnalysis.Debugging; + +internal sealed class BreakpointResolutionResult { - internal sealed class BreakpointResolutionResult - { - public Document Document { get; } - public TextSpan TextSpan { get; } - public string? LocationNameOpt { get; } - public bool IsLineBreakpoint { get; } + public Document Document { get; } + public TextSpan TextSpan { get; } + public string? LocationNameOpt { get; } + public bool IsLineBreakpoint { get; } - private BreakpointResolutionResult(Document document, TextSpan textSpan, string? locationNameOpt, bool isLineBreakpoint) - { - Document = document; - TextSpan = textSpan; - LocationNameOpt = locationNameOpt; - IsLineBreakpoint = isLineBreakpoint; - } + private BreakpointResolutionResult(Document document, TextSpan textSpan, string? locationNameOpt, bool isLineBreakpoint) + { + Document = document; + TextSpan = textSpan; + LocationNameOpt = locationNameOpt; + IsLineBreakpoint = isLineBreakpoint; + } - internal static BreakpointResolutionResult CreateSpanResult(Document document, TextSpan textSpan, string? locationNameOpt = null) - => new(document, textSpan, locationNameOpt, isLineBreakpoint: false); + internal static BreakpointResolutionResult CreateSpanResult(Document document, TextSpan textSpan, string? locationNameOpt = null) + => new(document, textSpan, locationNameOpt, isLineBreakpoint: false); - internal static BreakpointResolutionResult CreateLineResult(Document document, string? locationNameOpt = null) - => new(document, new TextSpan(), locationNameOpt, isLineBreakpoint: true); - } + internal static BreakpointResolutionResult CreateLineResult(Document document, string? locationNameOpt = null) + => new(document, new TextSpan(), locationNameOpt, isLineBreakpoint: true); } diff --git a/src/Features/Core/Portable/Debugging/DebugDataTipInfo.cs b/src/Features/Core/Portable/Debugging/DebugDataTipInfo.cs index 1de7cbe107d70..80776607c9364 100644 --- a/src/Features/Core/Portable/Debugging/DebugDataTipInfo.cs +++ b/src/Features/Core/Portable/Debugging/DebugDataTipInfo.cs @@ -4,14 +4,13 @@ using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Debugging +namespace Microsoft.CodeAnalysis.Debugging; + +internal readonly struct DebugDataTipInfo(TextSpan span, string text) { - internal readonly struct DebugDataTipInfo(TextSpan span, string text) - { - public readonly TextSpan Span = span; - public readonly string Text = text; + public readonly TextSpan Span = span; + public readonly string Text = text; - public bool IsDefault - => Span.Length == 0 && Span.Start == 0 && Text == null; - } + public bool IsDefault + => Span.Length == 0 && Span.Start == 0 && Text == null; } diff --git a/src/Features/Core/Portable/Debugging/DebugInformationReaderProvider.cs b/src/Features/Core/Portable/Debugging/DebugInformationReaderProvider.cs index ff758fc0fa3c8..380169eff4f5a 100644 --- a/src/Features/Core/Portable/Debugging/DebugInformationReaderProvider.cs +++ b/src/Features/Core/Portable/Debugging/DebugInformationReaderProvider.cs @@ -17,151 +17,150 @@ using Microsoft.DiaSymReader; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Debugging +namespace Microsoft.CodeAnalysis.Debugging; + +/// +/// An abstraction of a symbol reader that provides a reader of Edit and Continue debug information. +/// Owns the underlying PDB reader. +/// +internal abstract class DebugInformationReaderProvider : IDisposable { - /// - /// An abstraction of a symbol reader that provides a reader of Edit and Continue debug information. - /// Owns the underlying PDB reader. - /// - internal abstract class DebugInformationReaderProvider : IDisposable + private sealed class DummySymReaderMetadataProvider : ISymReaderMetadataProvider { - private sealed class DummySymReaderMetadataProvider : ISymReaderMetadataProvider - { - public static readonly DummySymReaderMetadataProvider Instance = new(); + public static readonly DummySymReaderMetadataProvider Instance = new(); - public unsafe bool TryGetStandaloneSignature(int standaloneSignatureToken, out byte* signature, out int length) - => throw ExceptionUtilities.Unreachable(); + public unsafe bool TryGetStandaloneSignature(int standaloneSignatureToken, out byte* signature, out int length) + => throw ExceptionUtilities.Unreachable(); - public bool TryGetTypeDefinitionInfo(int typeDefinitionToken, out string namespaceName, out string typeName, out TypeAttributes attributes) - => throw ExceptionUtilities.Unreachable(); + public bool TryGetTypeDefinitionInfo(int typeDefinitionToken, out string namespaceName, out string typeName, out TypeAttributes attributes) + => throw ExceptionUtilities.Unreachable(); - public bool TryGetTypeReferenceInfo(int typeReferenceToken, out string namespaceName, out string typeName) - => throw ExceptionUtilities.Unreachable(); - } + public bool TryGetTypeReferenceInfo(int typeReferenceToken, out string namespaceName, out string typeName) + => throw ExceptionUtilities.Unreachable(); + } - private sealed class Portable(MetadataReaderProvider pdbReaderProvider) : DebugInformationReaderProvider - { - private readonly MetadataReaderProvider _pdbReaderProvider = pdbReaderProvider; + private sealed class Portable(MetadataReaderProvider pdbReaderProvider) : DebugInformationReaderProvider + { + private readonly MetadataReaderProvider _pdbReaderProvider = pdbReaderProvider; - public override EditAndContinueMethodDebugInfoReader CreateEditAndContinueMethodDebugInfoReader() - => EditAndContinueMethodDebugInfoReader.Create(_pdbReaderProvider.GetMetadataReader()); + public override EditAndContinueMethodDebugInfoReader CreateEditAndContinueMethodDebugInfoReader() + => EditAndContinueMethodDebugInfoReader.Create(_pdbReaderProvider.GetMetadataReader()); - public override ValueTask CopyContentToAsync(Stream stream, CancellationToken cancellationToken) + public override ValueTask CopyContentToAsync(Stream stream, CancellationToken cancellationToken) + { + var reader = _pdbReaderProvider.GetMetadataReader(); + unsafe { - var reader = _pdbReaderProvider.GetMetadataReader(); - unsafe - { - using var metadataStream = new UnmanagedMemoryStream(reader.MetadataPointer, reader.MetadataLength); - metadataStream.CopyTo(stream); - } - - return ValueTaskFactory.CompletedTask; + using var metadataStream = new UnmanagedMemoryStream(reader.MetadataPointer, reader.MetadataLength); + metadataStream.CopyTo(stream); } - public override void Dispose() - => _pdbReaderProvider.Dispose(); + return ValueTaskFactory.CompletedTask; } - private sealed class Native(Stream stream, ISymUnmanagedReader5 symReader, int version) : DebugInformationReaderProvider - { - private readonly Stream _stream = stream; - private readonly int _version = version; - private ISymUnmanagedReader5 _symReader = symReader; + public override void Dispose() + => _pdbReaderProvider.Dispose(); + } - public override EditAndContinueMethodDebugInfoReader CreateEditAndContinueMethodDebugInfoReader() - => EditAndContinueMethodDebugInfoReader.Create(_symReader, _version); + private sealed class Native(Stream stream, ISymUnmanagedReader5 symReader, int version) : DebugInformationReaderProvider + { + private readonly Stream _stream = stream; + private readonly int _version = version; + private ISymUnmanagedReader5 _symReader = symReader; + + public override EditAndContinueMethodDebugInfoReader CreateEditAndContinueMethodDebugInfoReader() + => EditAndContinueMethodDebugInfoReader.Create(_symReader, _version); - public override async ValueTask CopyContentToAsync(Stream stream, CancellationToken cancellationToken) + public override async ValueTask CopyContentToAsync(Stream stream, CancellationToken cancellationToken) + { + var position = _stream.Position; + try { - var position = _stream.Position; - try - { - _stream.Position = 0; - await _stream.CopyToAsync(stream, bufferSize: 4 * 1024, cancellationToken).ConfigureAwait(false); - } - finally - { - _stream.Position = position; - } + _stream.Position = 0; + await _stream.CopyToAsync(stream, bufferSize: 4 * 1024, cancellationToken).ConfigureAwait(false); } - - public override void Dispose() + finally { - _stream.Dispose(); - - var symReader = Interlocked.Exchange(ref _symReader, null); - if (symReader != null && Marshal.IsComObject(symReader)) - { -#if NETCOREAPP - Debug.Assert(OperatingSystem.IsWindows()); -#endif - Marshal.ReleaseComObject(symReader); - } + _stream.Position = position; } } - public abstract void Dispose(); - - /// - /// Creates EnC debug information reader. - /// - public abstract EditAndContinueMethodDebugInfoReader CreateEditAndContinueMethodDebugInfoReader(); - - public abstract ValueTask CopyContentToAsync(Stream stream, CancellationToken cancellationToken); - - /// - /// Creates from a stream of Portable or Windows PDB. - /// - /// - /// Provider instance, which keeps the open until disposed. - /// - /// - /// Requires Microsoft.DiaSymReader.Native.{platform}.dll to be available for reading Windows PDB. - /// - /// is null. - /// does not support read and seek operations. - /// Error reading debug information from . - public static DebugInformationReaderProvider CreateFromStream(Stream stream) + public override void Dispose() { - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } + _stream.Dispose(); - if (!stream.CanRead || !stream.CanSeek) + var symReader = Interlocked.Exchange(ref _symReader, null); + if (symReader != null && Marshal.IsComObject(symReader)) { - throw new ArgumentException(FeaturesResources.StreamMustSupportReadAndSeek, nameof(stream)); +#if NETCOREAPP + Debug.Assert(OperatingSystem.IsWindows()); +#endif + Marshal.ReleaseComObject(symReader); } + } + } - var isPortable = stream.ReadByte() == 'B' && stream.ReadByte() == 'S' && stream.ReadByte() == 'J' && stream.ReadByte() == 'B'; - stream.Position = 0; + public abstract void Dispose(); - if (isPortable) - { - return new Portable(MetadataReaderProvider.FromPortablePdbStream(stream)); - } + /// + /// Creates EnC debug information reader. + /// + public abstract EditAndContinueMethodDebugInfoReader CreateEditAndContinueMethodDebugInfoReader(); - return CreateNative(stream); + public abstract ValueTask CopyContentToAsync(Stream stream, CancellationToken cancellationToken); + + /// + /// Creates from a stream of Portable or Windows PDB. + /// + /// + /// Provider instance, which keeps the open until disposed. + /// + /// + /// Requires Microsoft.DiaSymReader.Native.{platform}.dll to be available for reading Windows PDB. + /// + /// is null. + /// does not support read and seek operations. + /// Error reading debug information from . + public static DebugInformationReaderProvider CreateFromStream(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); } - // Do not inline to avoid loading Microsoft.DiaSymReader until it's actually needed. - [MethodImpl(MethodImplOptions.NoInlining)] - private static DebugInformationReaderProvider CreateNative(Stream stream) + if (!stream.CanRead || !stream.CanSeek) { - // We can use DummySymReaderMetadataProvider since we do not need to decode signatures, - // which is the only operation SymReader needs the provider for. - return new Native(stream, SymUnmanagedReaderFactory.CreateReader( - stream, DummySymReaderMetadataProvider.Instance, SymUnmanagedReaderCreationOptions.UseAlternativeLoadPath), version: 1); + throw new ArgumentException(FeaturesResources.StreamMustSupportReadAndSeek, nameof(stream)); } - /// - /// Creates from a Portable PDB metadata reader provider. - /// - /// - /// Provider instance, which takes ownership of the until disposed. - /// - /// is null. - public static DebugInformationReaderProvider CreateFromMetadataReader(MetadataReaderProvider metadataProvider) - => new Portable(metadataProvider ?? throw new ArgumentNullException(nameof(metadataProvider))); + var isPortable = stream.ReadByte() == 'B' && stream.ReadByte() == 'S' && stream.ReadByte() == 'J' && stream.ReadByte() == 'B'; + stream.Position = 0; + + if (isPortable) + { + return new Portable(MetadataReaderProvider.FromPortablePdbStream(stream)); + } + + return CreateNative(stream); + } + + // Do not inline to avoid loading Microsoft.DiaSymReader until it's actually needed. + [MethodImpl(MethodImplOptions.NoInlining)] + private static DebugInformationReaderProvider CreateNative(Stream stream) + { + // We can use DummySymReaderMetadataProvider since we do not need to decode signatures, + // which is the only operation SymReader needs the provider for. + return new Native(stream, SymUnmanagedReaderFactory.CreateReader( + stream, DummySymReaderMetadataProvider.Instance, SymUnmanagedReaderCreationOptions.UseAlternativeLoadPath), version: 1); } + + /// + /// Creates from a Portable PDB metadata reader provider. + /// + /// + /// Provider instance, which takes ownership of the until disposed. + /// + /// is null. + public static DebugInformationReaderProvider CreateFromMetadataReader(MetadataReaderProvider metadataProvider) + => new Portable(metadataProvider ?? throw new ArgumentNullException(nameof(metadataProvider))); } diff --git a/src/Features/Core/Portable/Debugging/DebugLocationInfo.cs b/src/Features/Core/Portable/Debugging/DebugLocationInfo.cs index 40879e583560a..f956127cb64c0 100644 --- a/src/Features/Core/Portable/Debugging/DebugLocationInfo.cs +++ b/src/Features/Core/Portable/Debugging/DebugLocationInfo.cs @@ -4,21 +4,20 @@ using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Debugging -{ - internal readonly struct DebugLocationInfo - { - public readonly string Name; - public readonly int LineOffset; +namespace Microsoft.CodeAnalysis.Debugging; - public DebugLocationInfo(string name, int lineOffset) - { - RoslynDebug.Assert(name != null); - Name = name; - LineOffset = lineOffset; - } +internal readonly struct DebugLocationInfo +{ + public readonly string Name; + public readonly int LineOffset; - public bool IsDefault - => Name == null; + public DebugLocationInfo(string name, int lineOffset) + { + RoslynDebug.Assert(name != null); + Name = name; + LineOffset = lineOffset; } + + public bool IsDefault + => Name == null; } diff --git a/src/Features/Core/Portable/Debugging/DebugMode.cs b/src/Features/Core/Portable/Debugging/DebugMode.cs index f6ffb0e521535..4e324aa14006f 100644 --- a/src/Features/Core/Portable/Debugging/DebugMode.cs +++ b/src/Features/Core/Portable/Debugging/DebugMode.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Debugging +namespace Microsoft.CodeAnalysis.Debugging; + +internal enum DebugMode { - internal enum DebugMode - { - Design, - Break, - Run - } + Design, + Break, + Run } diff --git a/src/Features/Core/Portable/Debugging/IBreakpointResolutionService.cs b/src/Features/Core/Portable/Debugging/IBreakpointResolutionService.cs index b33506b59f4bb..8c559453c5c19 100644 --- a/src/Features/Core/Portable/Debugging/IBreakpointResolutionService.cs +++ b/src/Features/Core/Portable/Debugging/IBreakpointResolutionService.cs @@ -8,12 +8,11 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Debugging +namespace Microsoft.CodeAnalysis.Debugging; + +internal interface IBreakpointResolutionService : ILanguageService { - internal interface IBreakpointResolutionService : ILanguageService - { - Task ResolveBreakpointAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken = default); + Task ResolveBreakpointAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken = default); - Task> ResolveBreakpointsAsync(Solution solution, string name, CancellationToken cancellationToken = default); - } + Task> ResolveBreakpointsAsync(Solution solution, string name, CancellationToken cancellationToken = default); } diff --git a/src/Features/Core/Portable/Debugging/ILanguageDebugInfoService.cs b/src/Features/Core/Portable/Debugging/ILanguageDebugInfoService.cs index d70f70bb1d80e..97928bf400953 100644 --- a/src/Features/Core/Portable/Debugging/ILanguageDebugInfoService.cs +++ b/src/Features/Core/Portable/Debugging/ILanguageDebugInfoService.cs @@ -6,18 +6,17 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Debugging +namespace Microsoft.CodeAnalysis.Debugging; + +internal interface ILanguageDebugInfoService : ILanguageService { - internal interface ILanguageDebugInfoService : ILanguageService - { - Task GetLocationInfoAsync(Document document, int position, CancellationToken cancellationToken); + Task GetLocationInfoAsync(Document document, int position, CancellationToken cancellationToken); - /// - /// Find an appropriate span to pass the debugger given a point in a snapshot. Optionally - /// pass back a string to pass to the debugger instead if no good span can be found. For - /// example, if the user hovers on "var" then we actually want to pass the fully qualified - /// name of the type that 'var' binds to, to the debugger. - /// - Task GetDataTipInfoAsync(Document document, int position, CancellationToken cancellationToken); - } + /// + /// Find an appropriate span to pass the debugger given a point in a snapshot. Optionally + /// pass back a string to pass to the debugger instead if no good span can be found. For + /// example, if the user hovers on "var" then we actually want to pass the fully qualified + /// name of the type that 'var' binds to, to the debugger. + /// + Task GetDataTipInfoAsync(Document document, int position, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/Debugging/IProximityExpressionsService.cs b/src/Features/Core/Portable/Debugging/IProximityExpressionsService.cs index 44f2d5907acdb..a578e78e14098 100644 --- a/src/Features/Core/Portable/Debugging/IProximityExpressionsService.cs +++ b/src/Features/Core/Portable/Debugging/IProximityExpressionsService.cs @@ -9,11 +9,10 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Debugging +namespace Microsoft.CodeAnalysis.Debugging; + +internal interface IProximityExpressionsService : ILanguageService { - internal interface IProximityExpressionsService : ILanguageService - { - Task> GetProximityExpressionsAsync(Document document, int position, CancellationToken cancellationToken); - Task IsValidAsync(Document document, int position, string expressionValue, CancellationToken cancellationToken); - } + Task> GetProximityExpressionsAsync(Document document, int position, CancellationToken cancellationToken); + Task IsValidAsync(Document document, int position, string expressionValue, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/DecompiledSource/IDecompiledSourceService.cs b/src/Features/Core/Portable/DecompiledSource/IDecompiledSourceService.cs index f93bf43260d7f..5fc23abcef22c 100644 --- a/src/Features/Core/Portable/DecompiledSource/IDecompiledSourceService.cs +++ b/src/Features/Core/Portable/DecompiledSource/IDecompiledSourceService.cs @@ -7,22 +7,21 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.DecompiledSource +namespace Microsoft.CodeAnalysis.DecompiledSource; + +internal interface IDecompiledSourceService : ILanguageService { - internal interface IDecompiledSourceService : ILanguageService - { - /// - /// Generates formatted source code containing general information about the symbol's - /// containing assembly and the decompiled source code which the given ISymbol is or is a part of - /// into the given document - /// - /// The document to generate source into - /// The in which symbol is resolved. - /// The symbol to generate source for - /// The reference that contains the symbol - /// The location of the implementation assembly to decompile - /// To cancel document operations - /// The updated document, or null if the decompilation could not be performed - Task AddSourceToAsync(Document document, Compilation symbolCompilation, ISymbol symbol, MetadataReference? metadataReference, string? assemblyLocation, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken); - } + /// + /// Generates formatted source code containing general information about the symbol's + /// containing assembly and the decompiled source code which the given ISymbol is or is a part of + /// into the given document + /// + /// The document to generate source into + /// The in which symbol is resolved. + /// The symbol to generate source for + /// The reference that contains the symbol + /// The location of the implementation assembly to decompile + /// To cancel document operations + /// The updated document, or null if the decompilation could not be performed + Task AddSourceToAsync(Document document, Compilation symbolCompilation, ISymbol symbol, MetadataReference? metadataReference, string? assemblyLocation, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs index 150e957379fb3..95444e39d250f 100644 --- a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs +++ b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs @@ -4,30 +4,29 @@ using System.Runtime.Serialization; -namespace Microsoft.CodeAnalysis.DesignerAttribute +namespace Microsoft.CodeAnalysis.DesignerAttribute; + +/// +/// Serialization typed used to pass information to/from OOP and VS. +/// +[DataContract] +internal struct DesignerAttributeData { /// - /// Serialization typed used to pass information to/from OOP and VS. + /// The category specified in a [DesignerCategory("...")] attribute. /// - [DataContract] - internal struct DesignerAttributeData - { - /// - /// The category specified in a [DesignerCategory("...")] attribute. - /// - [DataMember(Order = 0)] - public string? Category; + [DataMember(Order = 0)] + public string? Category; - /// - /// The document this applies to. - /// - [DataMember(Order = 1)] - public DocumentId DocumentId; + /// + /// The document this applies to. + /// + [DataMember(Order = 1)] + public DocumentId DocumentId; - /// - /// Path for this . - /// - [DataMember(Order = 2)] - public string FilePath; - } + /// + /// Path for this . + /// + [DataMember(Order = 2)] + public string FilePath; } diff --git a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs index 5548829920d1d..38d837127a930 100644 --- a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs +++ b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs @@ -17,315 +17,349 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Storage; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.DesignerAttribute +namespace Microsoft.CodeAnalysis.DesignerAttribute; + +[ExportWorkspaceService(typeof(IDesignerAttributeDiscoveryService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed partial class DesignerAttributeDiscoveryService() : IDesignerAttributeDiscoveryService { - [ExportWorkspaceService(typeof(IDesignerAttributeDiscoveryService)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed partial class DesignerAttributeDiscoveryService() : IDesignerAttributeDiscoveryService + /// + /// Cache from the individual references a project has, to a boolean specifying if reference knows about the + /// System.ComponentModel.DesignerCategoryAttribute attribute. + /// + private static readonly ConditionalWeakTable> s_metadataIdToDesignerAttributeInfo = new(); + + /// + /// Protects mutable state in this type. + /// + private readonly SemaphoreSlim _gate = new(initialCount: 1); + + /// + /// Keep track of the last information we reported. We will avoid notifying the host if we recompute and these + /// don't change. + /// + private readonly ConcurrentDictionary _documentToLastReportedInformation = []; + + private static async ValueTask HasDesignerCategoryTypeAsync(Project project, CancellationToken cancellationToken) { - /// - /// Cache from the individual references a project has, to a boolean specifying if reference knows about the - /// System.ComponentModel.DesignerCategoryAttribute attribute. - /// - private static readonly ConditionalWeakTable> s_metadataIdToDesignerAttributeInfo = new(); - - /// - /// Protects mutable state in this type. - /// - private readonly SemaphoreSlim _gate = new(initialCount: 1); - - /// - /// Keep track of the last information we reported. We will avoid notifying the host if we recompute and these - /// don't change. - /// - private readonly ConcurrentDictionary _documentToLastReportedInformation = []; - - private static async ValueTask HasDesignerCategoryTypeAsync(Project project, CancellationToken cancellationToken) + var solutionServices = project.Solution.Services; + var solutionKey = SolutionKey.ToSolutionKey(project.Solution); + foreach (var reference in project.MetadataReferences) { - var solutionServices = project.Solution.Services; - var solutionKey = SolutionKey.ToSolutionKey(project.Solution); - foreach (var reference in project.MetadataReferences) + if (reference is PortableExecutableReference peReference) { - if (reference is PortableExecutableReference peReference) + if (await HasDesignerCategoryTypeAsync( + solutionServices, solutionKey, peReference, cancellationToken).ConfigureAwait(false)) { - if (await HasDesignerCategoryTypeAsync( - solutionServices, solutionKey, peReference, cancellationToken).ConfigureAwait(false)) - { - return true; - } + return true; } } + } - return false; + return false; - static async Task HasDesignerCategoryTypeAsync( - SolutionServices solutionServices, - SolutionKey solutionKey, - PortableExecutableReference peReference, - CancellationToken cancellationToken) + static async Task HasDesignerCategoryTypeAsync( + SolutionServices solutionServices, + SolutionKey solutionKey, + PortableExecutableReference peReference, + CancellationToken cancellationToken) + { + MetadataId metadataId; + try { - MetadataId metadataId; - try - { - metadataId = peReference.GetMetadataId(); - } - catch (Exception ex) when (ex is BadImageFormatException or IOException) - { - return false; - } - - var asyncLazy = s_metadataIdToDesignerAttributeInfo.GetValue( - metadataId, _ => AsyncLazy.Create(cancellationToken => - ComputeHasDesignerCategoryTypeAsync(solutionServices, solutionKey, peReference, cancellationToken))); - return await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false); + metadataId = peReference.GetMetadataId(); } - - static async Task ComputeHasDesignerCategoryTypeAsync( - SolutionServices solutionServices, - SolutionKey solutionKey, - PortableExecutableReference peReference, - CancellationToken cancellationToken) + catch (Exception ex) when (ex is BadImageFormatException or IOException) { - var info = await SymbolTreeInfo.GetInfoForMetadataReferenceAsync( - solutionServices, solutionKey, peReference, checksum: null, cancellationToken).ConfigureAwait(false); - var result = - info.ContainsSymbolWithName(nameof(System)) && - info.ContainsSymbolWithName(nameof(System.ComponentModel)) && - info.ContainsSymbolWithName(nameof(System.ComponentModel.DesignerCategoryAttribute)); - return result; + return false; } + + var asyncLazy = s_metadataIdToDesignerAttributeInfo.GetValue( + metadataId, _ => AsyncLazy.Create(cancellationToken => + ComputeHasDesignerCategoryTypeAsync(solutionServices, solutionKey, peReference, cancellationToken))); + return await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false); } - public async ValueTask ProcessPriorityDocumentAsync( - Solution solution, - DocumentId priorityDocumentId, - IDesignerAttributeDiscoveryService.ICallback callback, + static async Task ComputeHasDesignerCategoryTypeAsync( + SolutionServices solutionServices, + SolutionKey solutionKey, + PortableExecutableReference peReference, CancellationToken cancellationToken) { - if (!solution.GetRequiredProject(priorityDocumentId.ProjectId).SupportsCompilation) - return; + var info = await SymbolTreeInfo.GetInfoForMetadataReferenceAsync( + solutionServices, solutionKey, peReference, checksum: null, cancellationToken).ConfigureAwait(false); + var result = + info.ContainsSymbolWithName(nameof(System)) && + info.ContainsSymbolWithName(nameof(System.ComponentModel)) && + info.ContainsSymbolWithName(nameof(System.ComponentModel.DesignerCategoryAttribute)); + return result; + } + } - // Create a frozen snapshot guaranteed to have this document in it. Note: it's important that we do - // this, and not just depend on the solution.WithFrozenPartialCompilationsAsync below. Very - // importantly, that solution may not contain this document yet. This does mean we'll process two - // separate solutions. - var frozenDocument = solution - .GetRequiredDocument(priorityDocumentId) - .WithFrozenPartialSemantics(cancellationToken); + public async ValueTask ProcessPriorityDocumentAsync( + Solution solution, + DocumentId priorityDocumentId, + IDesignerAttributeDiscoveryService.ICallback callback, + CancellationToken cancellationToken) + { + if (!solution.GetRequiredProject(priorityDocumentId.ProjectId).SupportsCompilation) + return; - var frozenProject = frozenDocument.Project; + // Create a frozen snapshot guaranteed to have this document in it. Note: it's important that we do + // this, and not just depend on the solution.WithFrozenPartialCompilationsAsync below. Very + // importantly, that solution may not contain this document yet. This does mean we'll process two + // separate solutions. + var frozenDocument = solution + .GetRequiredDocument(priorityDocumentId) + .WithFrozenPartialSemantics(cancellationToken); - using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) - { - var lazyProjectVersion = AsyncLazy.Create(frozenProject.GetSemanticVersionAsync); + var frozenProject = frozenDocument.Project; - await ScanForDesignerCategoryUsageAsync( - frozenProject, frozenDocument, callback, lazyProjectVersion, cancellationToken).ConfigureAwait(false); - } + using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) + { + var lazyProjectVersion = AsyncLazy.Create(frozenProject.GetSemanticVersionAsync); + + await ScanForDesignerCategoryUsageAsync( + frozenProject, frozenDocument, callback, lazyProjectVersion, cancellationToken).ConfigureAwait(false); } + } - public async ValueTask ProcessSolutionAsync( - Solution solution, - IDesignerAttributeDiscoveryService.ICallback callback, - CancellationToken cancellationToken) - { - // Freeze the entire solution at this point. We don't want to run generators (as they are very unlikely - // to contribute any changes that would affect which types we think are designable), and we want to be - // very fast to update the ui as a user types. - var frozenSolution = await solution.WithFrozenPartialCompilationsAsync(cancellationToken).ConfigureAwait(false); + public async ValueTask ProcessSolutionAsync( + Solution solution, + IDesignerAttributeDiscoveryService.ICallback callback, + CancellationToken cancellationToken) + { + // Freeze the entire solution at this point. We don't want to run generators (as they are very unlikely + // to contribute any changes that would affect which types we think are designable), and we want to be + // very fast to update the ui as a user types. + var frozenSolution = await solution.WithFrozenPartialCompilationsAsync(cancellationToken).ConfigureAwait(false); - using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) + using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) + { + // Remove any documents that are now gone. + foreach (var docId in _documentToLastReportedInformation.Keys) { - // Remove any documents that are now gone. - foreach (var docId in _documentToLastReportedInformation.Keys) - { - if (!solution.ContainsDocument(docId)) - _documentToLastReportedInformation.TryRemove(docId, out _); - } - - // Process the rest of the projects in dependency order so that their data is ready when we hit the - // projects that depend on them. - var dependencyGraph = frozenSolution.GetProjectDependencyGraph(); - foreach (var projectId in dependencyGraph.GetTopologicallySortedProjects(cancellationToken)) - await ProcessProjectAsync(frozenSolution.GetRequiredProject(projectId), callback, cancellationToken).ConfigureAwait(false); + if (!solution.ContainsDocument(docId)) + _documentToLastReportedInformation.TryRemove(docId, out _); } + + // Process the rest of the projects in dependency order so that their data is ready when we hit the + // projects that depend on them. + var dependencyGraph = frozenSolution.GetProjectDependencyGraph(); + foreach (var projectId in dependencyGraph.GetTopologicallySortedProjects(cancellationToken)) + await ProcessProjectAsync(frozenSolution.GetRequiredProject(projectId), callback, cancellationToken).ConfigureAwait(false); } + } - private async Task ProcessProjectAsync( - Project project, - IDesignerAttributeDiscoveryService.ICallback callback, - CancellationToken cancellationToken) - { - if (!project.SupportsCompilation) - return; + private async Task ProcessProjectAsync( + Project project, + IDesignerAttributeDiscoveryService.ICallback callback, + CancellationToken cancellationToken) + { + if (!project.SupportsCompilation) + return; - // Defer expensive work until it's actually needed. - // The top level project version for this project. We only care if anything top level changes here. - // Downstream impact will already happen due to us keying off of the references a project has (which will - // change if anything it depends on changes). - var lazyProjectVersion = AsyncLazy.Create(project.GetSemanticVersionAsync); + // Defer expensive work until it's actually needed. + // The top level project version for this project. We only care if anything top level changes here. + // Downstream impact will already happen due to us keying off of the references a project has (which will + // change if anything it depends on changes). + var lazyProjectVersion = AsyncLazy.Create(project.GetSemanticVersionAsync); - await ScanForDesignerCategoryUsageAsync( - project, specificDocument: null, callback, lazyProjectVersion, cancellationToken).ConfigureAwait(false); - } + await ScanForDesignerCategoryUsageAsync( + project, specificDocument: null, callback, lazyProjectVersion, cancellationToken).ConfigureAwait(false); + } - private async Task ScanForDesignerCategoryUsageAsync( - Project project, - Document? specificDocument, - IDesignerAttributeDiscoveryService.ICallback callback, - AsyncLazy lazyProjectVersion, - CancellationToken cancellationToken) - { - // Now get all the values that actually changed and notify VS about them. We don't need - // to tell it about the ones that didn't change since that will have no effect on the - // user experience. - var changedData = await ComputeChangedDataAsync( - project, specificDocument, lazyProjectVersion, cancellationToken).ConfigureAwait(false); - - // Only bother reporting non-empty information to save an unnecessary RPC. - if (!changedData.IsEmpty) - await callback.ReportDesignerAttributeDataAsync(changedData.SelectAsArray(d => d.data), cancellationToken).ConfigureAwait(false); - - // Now, keep track of what we've reported to the host so we won't report unchanged files in the future. We - // do this after the report has gone through as we want to make sure that if it cancels for any reason we - // don't hold onto values that may not have made it all the way to the project system. - foreach (var (data, projectVersion) in changedData) - _documentToLastReportedInformation[data.DocumentId] = (data.Category, projectVersion); - } + private async Task ScanForDesignerCategoryUsageAsync( + Project project, + Document? specificDocument, + IDesignerAttributeDiscoveryService.ICallback callback, + AsyncLazy lazyProjectVersion, + CancellationToken cancellationToken) + { + // Now get all the values that actually changed and notify VS about them. We don't need + // to tell it about the ones that didn't change since that will have no effect on the + // user experience. + var changedData = await ComputeChangedDataAsync( + project, specificDocument, lazyProjectVersion, cancellationToken).ConfigureAwait(false); + + // Only bother reporting non-empty information to save an unnecessary RPC. + if (!changedData.IsEmpty) + await callback.ReportDesignerAttributeDataAsync(changedData.SelectAsArray(d => d.data), cancellationToken).ConfigureAwait(false); + + // Now, keep track of what we've reported to the host so we won't report unchanged files in the future. We + // do this after the report has gone through as we want to make sure that if it cancels for any reason we + // don't hold onto values that may not have made it all the way to the project system. + foreach (var (data, projectVersion) in changedData) + _documentToLastReportedInformation[data.DocumentId] = (data.Category, projectVersion); + } - private async Task> ComputeChangedDataAsync( - Project project, - Document? specificDocument, - AsyncLazy lazyProjectVersion, - CancellationToken cancellationToken) - { - // NOTE: While we could potentially process the documents in a project in parallel, we intentionally do not. - // That's because this runs automatically in the BG in response to *any* change in the workspace. So it's - // very often going to be running, and it will be potentially competing against explicitly invoked actions - // by the user. Processing only one doc at a time, means we're not saturating the TPL with this work at the - // expense of other features. + private async Task> ComputeChangedDataAsync( + Project project, + Document? specificDocument, + AsyncLazy lazyProjectVersion, + CancellationToken cancellationToken) + { + // NOTE: While we could potentially process the documents in a project in parallel, we intentionally do not. + // That's because this runs automatically in the BG in response to *any* change in the workspace. So it's + // very often going to be running, and it will be potentially competing against explicitly invoked actions + // by the user. Processing only one doc at a time, means we're not saturating the TPL with this work at the + // expense of other features. - bool? hasDesignerCategoryType = null; + bool? hasDesignerCategoryType = null; - using var _ = ArrayBuilder<(DesignerAttributeData data, VersionStamp version)>.GetInstance(out var results); + using var _ = ArrayBuilder<(DesignerAttributeData data, VersionStamp version)>.GetInstance(out var results); - // Avoid realizing document instances until needed. - foreach (var documentId in project.DocumentIds) + // Avoid realizing document instances until needed. + foreach (var documentId in project.DocumentIds) + { + // If we're only analyzing a specific document, then skip the rest. + if (specificDocument != null && documentId != specificDocument.Id) + continue; + + // If we don't have a path for this document, we cant proceed with it. + // We need that path to inform the project system which file we're referring to. + var filePath = project.State.DocumentStates.GetRequiredState(documentId).FilePath; + if (filePath is null) + continue; + + // If nothing has changed at the top level between the last time we analyzed this document and now, then + // no need to analyze again. + var projectVersion = await lazyProjectVersion.GetValueAsync(cancellationToken).ConfigureAwait(false); + if (_documentToLastReportedInformation.TryGetValue(documentId, out var existingInfo) && + existingInfo.projectVersion == projectVersion) { - // If we're only analyzing a specific document, then skip the rest. - if (specificDocument != null && documentId != specificDocument.Id) - continue; - - // If we don't have a path for this document, we cant proceed with it. - // We need that path to inform the project system which file we're referring to. - var filePath = project.State.DocumentStates.GetRequiredState(documentId).FilePath; - if (filePath is null) - continue; - - // If nothing has changed at the top level between the last time we analyzed this document and now, then - // no need to analyze again. - var projectVersion = await lazyProjectVersion.GetValueAsync(cancellationToken).ConfigureAwait(false); - if (_documentToLastReportedInformation.TryGetValue(documentId, out var existingInfo) && - existingInfo.projectVersion == projectVersion) - { - continue; - } - - hasDesignerCategoryType ??= await HasDesignerCategoryTypeAsync(project, cancellationToken).ConfigureAwait(false); - var data = await ComputeDesignerAttributeDataAsync(project, documentId, filePath, hasDesignerCategoryType.Value).ConfigureAwait(false); - if (data.Category != existingInfo.category) - results.Add((data, projectVersion)); + continue; } - return results.ToImmutable(); + hasDesignerCategoryType ??= await HasDesignerCategoryTypeAsync(project, cancellationToken).ConfigureAwait(false); + var data = await ComputeDesignerAttributeDataAsync(project, documentId, filePath, hasDesignerCategoryType.Value).ConfigureAwait(false); + if (data.Category != existingInfo.category) + results.Add((data, projectVersion)); + } - async Task ComputeDesignerAttributeDataAsync( - Project project, DocumentId documentId, string filePath, bool hasDesignerCategoryType) - { - // We either haven't computed the designer info, or our data was out of date. We need - // So recompute here. Figure out what the current category is, and if that's different - // from what we previously stored. - var category = await ComputeDesignerAttributeCategoryAsync( - hasDesignerCategoryType, project, documentId, cancellationToken).ConfigureAwait(false); + return results.ToImmutable(); - return new DesignerAttributeData - { - Category = category, - DocumentId = documentId, - FilePath = filePath, - }; - } + async Task ComputeDesignerAttributeDataAsync( + Project project, DocumentId documentId, string filePath, bool hasDesignerCategoryType) + { + // We either haven't computed the designer info, or our data was out of date. We need + // So recompute here. Figure out what the current category is, and if that's different + // from what we previously stored. + var category = await ComputeDesignerAttributeCategoryAsync( + hasDesignerCategoryType, project, documentId, cancellationToken).ConfigureAwait(false); + + return new DesignerAttributeData + { + Category = category, + DocumentId = documentId, + FilePath = filePath, + }; } + } - public static async Task ComputeDesignerAttributeCategoryAsync( - bool hasDesignerCategoryType, Project project, DocumentId documentId, CancellationToken cancellationToken) - { - // simple case. If there's no DesignerCategory type in this compilation, then there's definitely no - // designable types. - if (!hasDesignerCategoryType) - return null; + public static async Task ComputeDesignerAttributeCategoryAsync( + bool hasDesignerCategoryType, Project project, DocumentId documentId, CancellationToken cancellationToken) + { + // simple case. If there's no DesignerCategory type in this compilation, then there's definitely no + // designable types. + if (!hasDesignerCategoryType) + return null; - // Wait to realize the document to avoid unnecessary allocations when indexing documents. - var document = project.GetRequiredDocument(documentId); + // Wait to realize the document to avoid unnecessary allocations when indexing documents. + var document = project.GetRequiredDocument(documentId); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var syntaxFacts = document.GetRequiredLanguageService(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); - // Legacy behavior. We only register the designer info for the first non-nested class - // in the file. - var firstClass = FindFirstNonNestedClass(syntaxFacts.GetMembersOfCompilationUnit(root)); - if (firstClass == null) - return null; + // Legacy behavior. We only register the designer info for the first non-nested class + // in the file. + var firstClass = FindFirstNonNestedClass(syntaxFacts.GetMembersOfCompilationUnit(root)); + if (firstClass == null) + return null; - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var firstClassType = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(firstClass, cancellationToken); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var firstClassType = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(firstClass, cancellationToken); - foreach (var type in firstClassType.GetBaseTypesAndThis()) - { - cancellationToken.ThrowIfCancellationRequested(); + foreach (var type in firstClassType.GetBaseTypesAndThis()) + { + cancellationToken.ThrowIfCancellationRequested(); - // See if it has the designer attribute on it. Use symbol-equivalence instead of direct equality - // as the symbol we have - var attribute = type.GetAttributes().FirstOrDefault(d => IsDesignerAttribute(d.AttributeClass)); - if (attribute is { ConstructorArguments: [{ Type.SpecialType: SpecialType.System_String, Value: string stringValue }] }) - return stringValue.Trim(); - } + // See if it has the designer attribute on it. Use symbol-equivalence instead of direct equality + // as the symbol we have + var attribute = type.GetAttributes().FirstOrDefault(d => IsDesignerAttribute(d.AttributeClass)); + if (attribute is { ConstructorArguments: [{ Type.SpecialType: SpecialType.System_String, Value: string stringValue }] }) + return stringValue.Trim(); + } - return null; + return null; - static bool IsDesignerAttribute(INamedTypeSymbol? attributeClass) - => attributeClass is - { - Name: nameof(DesignerCategoryAttribute), - ContainingNamespace.Name: nameof(System.ComponentModel), - ContainingNamespace.ContainingNamespace.Name: nameof(System), - ContainingNamespace.ContainingNamespace.ContainingNamespace.IsGlobalNamespace: true, - }; + static bool IsDesignerAttribute(INamedTypeSymbol? attributeClass) + => attributeClass is + { + Name: nameof(DesignerCategoryAttribute), + ContainingNamespace.Name: nameof(System.ComponentModel), + ContainingNamespace.ContainingNamespace.Name: nameof(System), + ContainingNamespace.ContainingNamespace.ContainingNamespace.IsGlobalNamespace: true, + }; - SyntaxNode? FindFirstNonNestedClass(SyntaxList members) + SyntaxNode? FindFirstNonNestedClass(SyntaxList members) + { + foreach (var member in members) { - foreach (var member in members) + cancellationToken.ThrowIfCancellationRequested(); + if (syntaxFacts.IsBaseNamespaceDeclaration(member)) { - cancellationToken.ThrowIfCancellationRequested(); - if (syntaxFacts.IsBaseNamespaceDeclaration(member)) - { - var firstClass = FindFirstNonNestedClass(syntaxFacts.GetMembersOfBaseNamespaceDeclaration(member)); - if (firstClass != null) - return firstClass; - } - else if (syntaxFacts.IsClassDeclaration(member)) - { - return member; - } + var firstClass = FindFirstNonNestedClass(syntaxFacts.GetMembersOfBaseNamespaceDeclaration(member)); + if (firstClass != null) + return firstClass; + } + else if (syntaxFacts.IsClassDeclaration(member)) + { + return member; } - - return null; } + + return null; + } + } + + public static async Task DiscoverDesignerAttributesAsync( + Solution solution, + Document? activeDocument, + RemoteHostClient client, + IAsynchronousOperationListener listener, + IDesignerAttributeDiscoveryService.ICallback target, + CancellationToken cancellationToken) + { + using var connection = client.CreateConnection(callbackTarget: target); + + // If there is an active document, then process changes to it right away, so that the UI updates quickly + // when the user adds/removes a form from a particular document. + if (RemoteSupportedLanguages.IsSupported(activeDocument?.Project.Language)) + { + // We only need to do a project sync to compute the up to date data for this particular file. + var priorityDocumentId = activeDocument.Id; + await connection.TryInvokeAsync( + activeDocument.Project, + (service, checksum, callbackId, cancellationToken) => service.DiscoverDesignerAttributesAsync( + callbackId, checksum, priorityDocumentId, cancellationToken), + cancellationToken).ConfigureAwait(false); } + + // Wait a little after the priority document and process the rest of the solution at a lower priority. + await listener.Delay(DelayTimeSpan.NonFocus, cancellationToken).ConfigureAwait(false); + + await connection.TryInvokeAsync( + solution, + (service, checksum, callbackId, cancellationToken) => service.DiscoverDesignerAttributesAsync( + callbackId, checksum, cancellationToken), + cancellationToken).ConfigureAwait(false); } } diff --git a/src/Features/Core/Portable/Diagnostics/AbstractHostDiagnosticUpdateSource.cs b/src/Features/Core/Portable/Diagnostics/AbstractHostDiagnosticUpdateSource.cs index f7c15dd607100..aad8ee4d6f6b5 100644 --- a/src/Features/Core/Portable/Diagnostics/AbstractHostDiagnosticUpdateSource.cs +++ b/src/Features/Core/Portable/Diagnostics/AbstractHostDiagnosticUpdateSource.cs @@ -10,183 +10,182 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +/// +/// Diagnostic update source for reporting workspace host specific diagnostics, +/// which may not be related to any given project/document in the solution. +/// For example, these include diagnostics generated for exceptions from third party analyzers. +/// +internal abstract class AbstractHostDiagnosticUpdateSource : IDiagnosticUpdateSource { - /// - /// Diagnostic update source for reporting workspace host specific diagnostics, - /// which may not be related to any given project/document in the solution. - /// For example, these include diagnostics generated for exceptions from third party analyzers. - /// - internal abstract class AbstractHostDiagnosticUpdateSource : IDiagnosticUpdateSource - { - private ImmutableDictionary> _analyzerHostDiagnosticsMap = - ImmutableDictionary>.Empty; + private ImmutableDictionary> _analyzerHostDiagnosticsMap = + ImmutableDictionary>.Empty; + + public abstract Workspace Workspace { get; } - public abstract Workspace Workspace { get; } + public bool SupportGetDiagnostics => false; - public bool SupportGetDiagnostics => false; + public ValueTask> GetDiagnosticsAsync(Workspace workspace, ProjectId? projectId, DocumentId? documentId, object? id, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) + => new([]); - public ValueTask> GetDiagnosticsAsync(Workspace workspace, ProjectId? projectId, DocumentId? documentId, object? id, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) - => new([]); + public event EventHandler>? DiagnosticsUpdated; + public event EventHandler DiagnosticsCleared { add { } remove { } } - public event EventHandler>? DiagnosticsUpdated; - public event EventHandler DiagnosticsCleared { add { } remove { } } + public void RaiseDiagnosticsUpdated(ImmutableArray args) + { + if (!args.IsEmpty) + DiagnosticsUpdated?.Invoke(this, args); + } - public void RaiseDiagnosticsUpdated(ImmutableArray args) + public void ReportAnalyzerDiagnostic(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, ProjectId? projectId) + { + // check whether we are reporting project specific diagnostic or workspace wide diagnostic + var solution = Workspace.CurrentSolution; + var project = projectId != null ? solution.GetProject(projectId) : null; + + // check whether project the diagnostic belong to still exist + if (projectId != null && project == null) { - if (!args.IsEmpty) - DiagnosticsUpdated?.Invoke(this, args); + // project the diagnostic belong to already removed from the solution. + // ignore the diagnostic + return; } - public void ReportAnalyzerDiagnostic(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, ProjectId? projectId) - { - // check whether we are reporting project specific diagnostic or workspace wide diagnostic - var solution = Workspace.CurrentSolution; - var project = projectId != null ? solution.GetProject(projectId) : null; + ReportAnalyzerDiagnostic(analyzer, DiagnosticData.Create(solution, diagnostic, project), project); + } + + public void ReportAnalyzerDiagnostic(DiagnosticAnalyzer analyzer, DiagnosticData diagnosticData, Project? project) + { + var raiseDiagnosticsUpdated = true; - // check whether project the diagnostic belong to still exist - if (projectId != null && project == null) + var dxs = ImmutableInterlocked.AddOrUpdate(ref _analyzerHostDiagnosticsMap, + analyzer, + [diagnosticData], + (a, existing) => { - // project the diagnostic belong to already removed from the solution. - // ignore the diagnostic - return; - } + var newDiags = existing.Add(diagnosticData); + raiseDiagnosticsUpdated = newDiags.Count > existing.Count; + return newDiags; + }); - ReportAnalyzerDiagnostic(analyzer, DiagnosticData.Create(solution, diagnostic, project), project); + if (raiseDiagnosticsUpdated) + { + RaiseDiagnosticsUpdated([MakeCreatedArgs(analyzer, dxs, project)]); } + } + + public void ClearAnalyzerReferenceDiagnostics(AnalyzerFileReference analyzerReference, string language, ProjectId projectId) + { + // Perf: if we don't have any diagnostics at all, just return right away; this avoids loading the analyzers + // which may have not been loaded if you didn't do too much in your session. + if (_analyzerHostDiagnosticsMap.Count == 0) + return; + + using var argsBuilder = TemporaryArray.Empty; + var analyzers = analyzerReference.GetAnalyzers(language); + AddArgsToClearAnalyzerDiagnostics(ref argsBuilder.AsRef(), analyzers, projectId); + RaiseDiagnosticsUpdated(argsBuilder.ToImmutableAndClear()); + } - public void ReportAnalyzerDiagnostic(DiagnosticAnalyzer analyzer, DiagnosticData diagnosticData, Project? project) + public void AddArgsToClearAnalyzerDiagnostics(ref TemporaryArray builder, ImmutableArray analyzers, ProjectId projectId) + { + foreach (var analyzer in analyzers) { - var raiseDiagnosticsUpdated = true; - - var dxs = ImmutableInterlocked.AddOrUpdate(ref _analyzerHostDiagnosticsMap, - analyzer, - [diagnosticData], - (a, existing) => - { - var newDiags = existing.Add(diagnosticData); - raiseDiagnosticsUpdated = newDiags.Count > existing.Count; - return newDiags; - }); - - if (raiseDiagnosticsUpdated) - { - RaiseDiagnosticsUpdated([MakeCreatedArgs(analyzer, dxs, project)]); - } + AddArgsToClearAnalyzerDiagnostics(ref builder, analyzer, projectId); } + } - public void ClearAnalyzerReferenceDiagnostics(AnalyzerFileReference analyzerReference, string language, ProjectId projectId) + public void AddArgsToClearAnalyzerDiagnostics(ref TemporaryArray builder, ProjectId projectId) + { + foreach (var (analyzer, _) in _analyzerHostDiagnosticsMap) { - // Perf: if we don't have any diagnostics at all, just return right away; this avoids loading the analyzers - // which may have not been loaded if you didn't do too much in your session. - if (_analyzerHostDiagnosticsMap.Count == 0) - return; - - using var argsBuilder = TemporaryArray.Empty; - var analyzers = analyzerReference.GetAnalyzers(language); - AddArgsToClearAnalyzerDiagnostics(ref argsBuilder.AsRef(), analyzers, projectId); - RaiseDiagnosticsUpdated(argsBuilder.ToImmutableAndClear()); + AddArgsToClearAnalyzerDiagnostics(ref builder, analyzer, projectId); } + } - public void AddArgsToClearAnalyzerDiagnostics(ref TemporaryArray builder, ImmutableArray analyzers, ProjectId projectId) + private void AddArgsToClearAnalyzerDiagnostics(ref TemporaryArray builder, DiagnosticAnalyzer analyzer, ProjectId projectId) + { + if (!_analyzerHostDiagnosticsMap.TryGetValue(analyzer, out var existing)) { - foreach (var analyzer in analyzers) - { - AddArgsToClearAnalyzerDiagnostics(ref builder, analyzer, projectId); - } + return; } - public void AddArgsToClearAnalyzerDiagnostics(ref TemporaryArray builder, ProjectId projectId) + // Check if analyzer is shared by analyzer references from different projects. + var sharedAnalyzer = existing.Contains(d => d.ProjectId != null && d.ProjectId != projectId); + if (sharedAnalyzer) { - foreach (var (analyzer, _) in _analyzerHostDiagnosticsMap) + var newDiags = existing.Where(d => d.ProjectId != projectId).ToImmutableHashSet(); + if (newDiags.Count < existing.Count && + ImmutableInterlocked.TryUpdate(ref _analyzerHostDiagnosticsMap, analyzer, newDiags, existing)) { - AddArgsToClearAnalyzerDiagnostics(ref builder, analyzer, projectId); + var project = Workspace.CurrentSolution.GetProject(projectId); + builder.Add(MakeRemovedArgs(analyzer, project)); } } - - private void AddArgsToClearAnalyzerDiagnostics(ref TemporaryArray builder, DiagnosticAnalyzer analyzer, ProjectId projectId) + else if (ImmutableInterlocked.TryRemove(ref _analyzerHostDiagnosticsMap, analyzer, out existing)) { - if (!_analyzerHostDiagnosticsMap.TryGetValue(analyzer, out var existing)) - { - return; - } + var project = Workspace.CurrentSolution.GetProject(projectId); + builder.Add(MakeRemovedArgs(analyzer, project)); - // Check if analyzer is shared by analyzer references from different projects. - var sharedAnalyzer = existing.Contains(d => d.ProjectId != null && d.ProjectId != projectId); - if (sharedAnalyzer) - { - var newDiags = existing.Where(d => d.ProjectId != projectId).ToImmutableHashSet(); - if (newDiags.Count < existing.Count && - ImmutableInterlocked.TryUpdate(ref _analyzerHostDiagnosticsMap, analyzer, newDiags, existing)) - { - var project = Workspace.CurrentSolution.GetProject(projectId); - builder.Add(MakeRemovedArgs(analyzer, project)); - } - } - else if (ImmutableInterlocked.TryRemove(ref _analyzerHostDiagnosticsMap, analyzer, out existing)) + if (existing.Any(d => d.ProjectId == null)) { - var project = Workspace.CurrentSolution.GetProject(projectId); - builder.Add(MakeRemovedArgs(analyzer, project)); - - if (existing.Any(d => d.ProjectId == null)) - { - builder.Add(MakeRemovedArgs(analyzer, project: null)); - } + builder.Add(MakeRemovedArgs(analyzer, project: null)); } } + } - private DiagnosticsUpdatedArgs MakeCreatedArgs(DiagnosticAnalyzer analyzer, ImmutableHashSet items, Project? project) - { - return DiagnosticsUpdatedArgs.DiagnosticsCreated( - CreateId(analyzer, project), Workspace, project?.Solution, project?.Id, documentId: null, diagnostics: items.ToImmutableArray()); - } + private DiagnosticsUpdatedArgs MakeCreatedArgs(DiagnosticAnalyzer analyzer, ImmutableHashSet items, Project? project) + { + return DiagnosticsUpdatedArgs.DiagnosticsCreated( + CreateId(analyzer, project), Workspace, project?.Solution, project?.Id, documentId: null, diagnostics: items.ToImmutableArray()); + } - private DiagnosticsUpdatedArgs MakeRemovedArgs(DiagnosticAnalyzer analyzer, Project? project) - { - return DiagnosticsUpdatedArgs.DiagnosticsRemoved( - CreateId(analyzer, project), Workspace, project?.Solution, project?.Id, documentId: null); - } + private DiagnosticsUpdatedArgs MakeRemovedArgs(DiagnosticAnalyzer analyzer, Project? project) + { + return DiagnosticsUpdatedArgs.DiagnosticsRemoved( + CreateId(analyzer, project), Workspace, project?.Solution, project?.Id, documentId: null); + } - private HostArgsId CreateId(DiagnosticAnalyzer analyzer, Project? project) => new(this, analyzer, project?.Id); + private HostArgsId CreateId(DiagnosticAnalyzer analyzer, Project? project) => new(this, analyzer, project?.Id); - internal TestAccessor GetTestAccessor() - => new(this); + internal TestAccessor GetTestAccessor() + => new(this); - internal readonly struct TestAccessor(AbstractHostDiagnosticUpdateSource abstractHostDiagnosticUpdateSource) - { - private readonly AbstractHostDiagnosticUpdateSource _abstractHostDiagnosticUpdateSource = abstractHostDiagnosticUpdateSource; + internal readonly struct TestAccessor(AbstractHostDiagnosticUpdateSource abstractHostDiagnosticUpdateSource) + { + private readonly AbstractHostDiagnosticUpdateSource _abstractHostDiagnosticUpdateSource = abstractHostDiagnosticUpdateSource; - internal ImmutableArray GetReportedDiagnostics() - => _abstractHostDiagnosticUpdateSource._analyzerHostDiagnosticsMap.Values.Flatten().ToImmutableArray(); + internal ImmutableArray GetReportedDiagnostics() + => _abstractHostDiagnosticUpdateSource._analyzerHostDiagnosticsMap.Values.Flatten().ToImmutableArray(); - internal ImmutableHashSet GetReportedDiagnostics(DiagnosticAnalyzer analyzer) + internal ImmutableHashSet GetReportedDiagnostics(DiagnosticAnalyzer analyzer) + { + if (!_abstractHostDiagnosticUpdateSource._analyzerHostDiagnosticsMap.TryGetValue(analyzer, out var diagnostics)) { - if (!_abstractHostDiagnosticUpdateSource._analyzerHostDiagnosticsMap.TryGetValue(analyzer, out var diagnostics)) - { - diagnostics = []; - } - - return diagnostics; + diagnostics = []; } + + return diagnostics; } + } - private sealed class HostArgsId(AbstractHostDiagnosticUpdateSource source, DiagnosticAnalyzer analyzer, ProjectId? projectId) : AnalyzerUpdateArgsId(analyzer) - { - private readonly AbstractHostDiagnosticUpdateSource _source = source; - private readonly ProjectId? _projectId = projectId; + private sealed class HostArgsId(AbstractHostDiagnosticUpdateSource source, DiagnosticAnalyzer analyzer, ProjectId? projectId) : AnalyzerUpdateArgsId(analyzer) + { + private readonly AbstractHostDiagnosticUpdateSource _source = source; + private readonly ProjectId? _projectId = projectId; - public override bool Equals(object? obj) + public override bool Equals(object? obj) + { + if (obj is not HostArgsId other) { - if (obj is not HostArgsId other) - { - return false; - } - - return _source == other._source && _projectId == other._projectId && base.Equals(obj); + return false; } - public override int GetHashCode() - => Hash.Combine(_source.GetHashCode(), Hash.Combine(_projectId == null ? 1 : _projectId.GetHashCode(), base.GetHashCode())); + return _source == other._source && _projectId == other._projectId && base.Equals(obj); } + + public override int GetHashCode() + => Hash.Combine(_source.GetHashCode(), Hash.Combine(_projectId == null ? 1 : _projectId.GetHashCode(), base.GetHashCode())); } } diff --git a/src/Features/Core/Portable/Diagnostics/AnalyzerUpdateArgsId.cs b/src/Features/Core/Portable/Diagnostics/AnalyzerUpdateArgsId.cs index 95e2a57fe41e1..bcc22fc1e1730 100644 --- a/src/Features/Core/Portable/Diagnostics/AnalyzerUpdateArgsId.cs +++ b/src/Features/Core/Portable/Diagnostics/AnalyzerUpdateArgsId.cs @@ -4,20 +4,19 @@ using Microsoft.CodeAnalysis.Common; -namespace Microsoft.CodeAnalysis.Diagnostics -{ - /// - /// Base type of a type that is used as for live diagnostic - /// - internal class AnalyzerUpdateArgsId : BuildToolId.Base, ISupportLiveUpdate - { - public DiagnosticAnalyzer Analyzer => _Field1!; +namespace Microsoft.CodeAnalysis.Diagnostics; - protected AnalyzerUpdateArgsId(DiagnosticAnalyzer analyzer) - : base(analyzer) - { - } +/// +/// Base type of a type that is used as for live diagnostic +/// +internal class AnalyzerUpdateArgsId : BuildToolId.Base, ISupportLiveUpdate +{ + public DiagnosticAnalyzer Analyzer => _Field1!; - public override string BuildTool => Analyzer.GetAnalyzerAssemblyName(); + protected AnalyzerUpdateArgsId(DiagnosticAnalyzer analyzer) + : base(analyzer) + { } + + public override string BuildTool => Analyzer.GetAnalyzerAssemblyName(); } diff --git a/src/Features/Core/Portable/Diagnostics/Analyzers/UnboundIdentifiersDiagnosticAnalyzerBase.cs b/src/Features/Core/Portable/Diagnostics/Analyzers/UnboundIdentifiersDiagnosticAnalyzerBase.cs index 677757d11ed08..6c81df982860f 100644 --- a/src/Features/Core/Portable/Diagnostics/Analyzers/UnboundIdentifiersDiagnosticAnalyzerBase.cs +++ b/src/Features/Core/Portable/Diagnostics/Analyzers/UnboundIdentifiersDiagnosticAnalyzerBase.cs @@ -9,89 +9,88 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics.AddImport +namespace Microsoft.CodeAnalysis.Diagnostics.AddImport; + +/// +/// See https://github.com/dotnet/roslyn/issues/7536. IDE should not be analyzing and reporting +/// compiler diagnostics for normal constructs. However, the compiler does not report issues +/// for incomplete members. That means that if you just have `public DateTime` that that is counted +/// as an incomplete member where no binding happens at all. This means that features like 'add import' +/// won't work here to offer to add `using System;` if that is all that is written. +/// +/// This definitely needs to be fixed at the compiler layer. However, until that happens, this is +/// only alternative at our disposal. +/// +/// +internal abstract class UnboundIdentifiersDiagnosticAnalyzerBase : DiagnosticAnalyzer, IBuiltInAnalyzer + where TLanguageKindEnum : struct + where TSimpleNameSyntax : SyntaxNode + where TQualifiedNameSyntax : SyntaxNode + where TIncompleteMemberSyntax : SyntaxNode { - /// - /// See https://github.com/dotnet/roslyn/issues/7536. IDE should not be analyzing and reporting - /// compiler diagnostics for normal constructs. However, the compiler does not report issues - /// for incomplete members. That means that if you just have `public DateTime` that that is counted - /// as an incomplete member where no binding happens at all. This means that features like 'add import' - /// won't work here to offer to add `using System;` if that is all that is written. - /// - /// This definitely needs to be fixed at the compiler layer. However, until that happens, this is - /// only alternative at our disposal. - /// - /// - internal abstract class UnboundIdentifiersDiagnosticAnalyzerBase : DiagnosticAnalyzer, IBuiltInAnalyzer - where TLanguageKindEnum : struct - where TSimpleNameSyntax : SyntaxNode - where TQualifiedNameSyntax : SyntaxNode - where TIncompleteMemberSyntax : SyntaxNode - { - protected abstract DiagnosticDescriptor DiagnosticDescriptor { get; } - protected abstract ImmutableArray SyntaxKindsOfInterest { get; } - protected abstract bool IsNameOf(SyntaxNode node); + protected abstract DiagnosticDescriptor DiagnosticDescriptor { get; } + protected abstract ImmutableArray SyntaxKindsOfInterest { get; } + protected abstract bool IsNameOf(SyntaxNode node); - // High priority as we need to know about unbound identifiers so that we can run add-using to fix them. - public bool IsHighPriority - => true; + // High priority as we need to know about unbound identifiers so that we can run add-using to fix them. + public bool IsHighPriority + => true; - public override ImmutableArray SupportedDiagnostics - => [DiagnosticDescriptor]; + public override ImmutableArray SupportedDiagnostics + => [DiagnosticDescriptor]; - public bool OpenFileOnly(SimplifierOptions? options) - => false; + public bool OpenFileOnly(SimplifierOptions? options) + => false; - 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.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKindsOfInterest.ToArray()); - } + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKindsOfInterest.ToArray()); + } - protected static DiagnosticDescriptor GetDiagnosticDescriptor(string id, LocalizableString messageFormat) - { - // it is not configurable diagnostic, title doesn't matter - return new DiagnosticDescriptor( - id, string.Empty, messageFormat, - DiagnosticCategory.Compiler, - DiagnosticSeverity.Error, - isEnabledByDefault: true, - customTags: DiagnosticCustomTags.Microsoft.Append(WellKnownDiagnosticTags.NotConfigurable)); - } + protected static DiagnosticDescriptor GetDiagnosticDescriptor(string id, LocalizableString messageFormat) + { + // it is not configurable diagnostic, title doesn't matter + return new DiagnosticDescriptor( + id, string.Empty, messageFormat, + DiagnosticCategory.Compiler, + DiagnosticSeverity.Error, + isEnabledByDefault: true, + customTags: DiagnosticCustomTags.Microsoft.Append(WellKnownDiagnosticTags.NotConfigurable)); + } - private void AnalyzeNode(SyntaxNodeAnalysisContext context) + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + if (context.Node is TIncompleteMemberSyntax) { - if (context.Node is TIncompleteMemberSyntax) - { - ReportUnboundIdentifierNames(context, context.Node); - } + ReportUnboundIdentifierNames(context, context.Node); } + } - private void ReportUnboundIdentifierNames(SyntaxNodeAnalysisContext context, SyntaxNode member) + private void ReportUnboundIdentifierNames(SyntaxNodeAnalysisContext context, SyntaxNode member) + { + var typeNames = member.DescendantNodes().Where(n => IsQualifiedOrSimpleName(n) && !n.Span.IsEmpty); + foreach (var typeName in typeNames) { - var typeNames = member.DescendantNodes().Where(n => IsQualifiedOrSimpleName(n) && !n.Span.IsEmpty); - foreach (var typeName in typeNames) + var info = context.SemanticModel.GetSymbolInfo(typeName); + if (info.Symbol == null && info.CandidateSymbols.Length == 0) { - var info = context.SemanticModel.GetSymbolInfo(typeName); - if (info.Symbol == null && info.CandidateSymbols.Length == 0) + // GetSymbolInfo returns no symbols for "nameof" expression, so handle it specially. + if (IsNameOf(typeName)) { - // GetSymbolInfo returns no symbols for "nameof" expression, so handle it specially. - if (IsNameOf(typeName)) - { - continue; - } - - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptor, typeName.GetLocation(), typeName.ToString())); + continue; } + + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptor, typeName.GetLocation(), typeName.ToString())); } } + } - private static bool IsQualifiedOrSimpleName(SyntaxNode n) - => n is TQualifiedNameSyntax or TSimpleNameSyntax; + private static bool IsQualifiedOrSimpleName(SyntaxNode n) + => n is TQualifiedNameSyntax or TSimpleNameSyntax; - public DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; - } + public DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; } diff --git a/src/Features/Core/Portable/Diagnostics/BuildOnlyDiagnosticsService.cs b/src/Features/Core/Portable/Diagnostics/BuildOnlyDiagnosticsService.cs index c497151cd0308..acc6905019187 100644 --- a/src/Features/Core/Portable/Diagnostics/BuildOnlyDiagnosticsService.cs +++ b/src/Features/Core/Portable/Diagnostics/BuildOnlyDiagnosticsService.cs @@ -9,138 +9,137 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +[ExportWorkspaceServiceFactory(typeof(IBuildOnlyDiagnosticsService), ServiceLayer.Default), Shared] +internal sealed class BuildOnlyDiagnosticsServiceFactory : IWorkspaceServiceFactory { - [ExportWorkspaceServiceFactory(typeof(IBuildOnlyDiagnosticsService), ServiceLayer.Default), Shared] - internal sealed class BuildOnlyDiagnosticsServiceFactory : IWorkspaceServiceFactory + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public BuildOnlyDiagnosticsServiceFactory() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public BuildOnlyDiagnosticsServiceFactory() - { - } + } + + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => new BuildOnlyDiagnosticsService(workspaceServices.Workspace); - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new BuildOnlyDiagnosticsService(workspaceServices.Workspace); + private sealed class BuildOnlyDiagnosticsService : IBuildOnlyDiagnosticsService + { + private readonly object _gate = new(); + private readonly Dictionary> _documentDiagnostics = []; + private readonly Dictionary> _projectDiagnostics = []; - private sealed class BuildOnlyDiagnosticsService : IBuildOnlyDiagnosticsService + public BuildOnlyDiagnosticsService(Workspace workspace) { - private readonly object _gate = new(); - private readonly Dictionary> _documentDiagnostics = []; - private readonly Dictionary> _projectDiagnostics = []; + workspace.WorkspaceChanged += OnWorkspaceChanged; + } - public BuildOnlyDiagnosticsService(Workspace workspace) + private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) + { + switch (e.Kind) { - workspace.WorkspaceChanged += OnWorkspaceChanged; + case WorkspaceChangeKind.SolutionAdded: + case WorkspaceChangeKind.SolutionCleared: + case WorkspaceChangeKind.SolutionReloaded: + case WorkspaceChangeKind.SolutionRemoved: + ClearAllDiagnostics(); + break; + + case WorkspaceChangeKind.ProjectReloaded: + case WorkspaceChangeKind.ProjectRemoved: + ClearDiagnostics(e.OldSolution.GetProject(e.ProjectId)); + break; + + case WorkspaceChangeKind.DocumentRemoved: + case WorkspaceChangeKind.DocumentReloaded: + case WorkspaceChangeKind.AdditionalDocumentRemoved: + case WorkspaceChangeKind.AdditionalDocumentReloaded: + case WorkspaceChangeKind.AnalyzerConfigDocumentRemoved: + case WorkspaceChangeKind.AnalyzerConfigDocumentReloaded: + ClearDiagnostics(e.DocumentId); + break; } + } - private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) + public void AddBuildOnlyDiagnostics(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableArray diagnostics) + { + lock (_gate) { - switch (e.Kind) + if (documentId != null) { - case WorkspaceChangeKind.SolutionAdded: - case WorkspaceChangeKind.SolutionCleared: - case WorkspaceChangeKind.SolutionReloaded: - case WorkspaceChangeKind.SolutionRemoved: - ClearAllDiagnostics(); - break; - - case WorkspaceChangeKind.ProjectReloaded: - case WorkspaceChangeKind.ProjectRemoved: - ClearDiagnostics(e.OldSolution.GetProject(e.ProjectId)); - break; - - case WorkspaceChangeKind.DocumentRemoved: - case WorkspaceChangeKind.DocumentReloaded: - case WorkspaceChangeKind.AdditionalDocumentRemoved: - case WorkspaceChangeKind.AdditionalDocumentReloaded: - case WorkspaceChangeKind.AnalyzerConfigDocumentRemoved: - case WorkspaceChangeKind.AnalyzerConfigDocumentReloaded: - ClearDiagnostics(e.DocumentId); - break; + _documentDiagnostics[documentId] = diagnostics; } - } - - public void AddBuildOnlyDiagnostics(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableArray diagnostics) - { - lock (_gate) + else if (projectId != null) { - if (documentId != null) - { - _documentDiagnostics[documentId] = diagnostics; - } - else if (projectId != null) - { - _projectDiagnostics[projectId] = diagnostics; - } + _projectDiagnostics[projectId] = diagnostics; } } + } - private void ClearAllDiagnostics() + private void ClearAllDiagnostics() + { + lock (_gate) { - lock (_gate) - { - _documentDiagnostics.Clear(); - _projectDiagnostics.Clear(); - } + _documentDiagnostics.Clear(); + _projectDiagnostics.Clear(); } + } - private void ClearDiagnostics(DocumentId? documentId) - { - if (documentId == null) - return; - - lock (_gate) - { - _documentDiagnostics.Remove(documentId); - } - } + private void ClearDiagnostics(DocumentId? documentId) + { + if (documentId == null) + return; - private void ClearDiagnostics(Project? project) + lock (_gate) { - if (project == null) - return; - - lock (_gate) - { - _projectDiagnostics.Remove(project.Id); - foreach (var documentId in project.DocumentIds) - _documentDiagnostics.Remove(documentId); - } + _documentDiagnostics.Remove(documentId); } + } + + private void ClearDiagnostics(Project? project) + { + if (project == null) + return; - public void ClearBuildOnlyDiagnostics(Solution solution, ProjectId? projectId, DocumentId? documentId) + lock (_gate) { - if (documentId != null) - ClearDiagnostics(documentId); - else - ClearDiagnostics(solution.GetProject(projectId)); + _projectDiagnostics.Remove(project.Id); + foreach (var documentId in project.DocumentIds) + _documentDiagnostics.Remove(documentId); } + } + + public void ClearBuildOnlyDiagnostics(Solution solution, ProjectId? projectId, DocumentId? documentId) + { + if (documentId != null) + ClearDiagnostics(documentId); + else + ClearDiagnostics(solution.GetProject(projectId)); + } - public ImmutableArray GetBuildOnlyDiagnostics(DocumentId documentId) + public ImmutableArray GetBuildOnlyDiagnostics(DocumentId documentId) + { + lock (_gate) { - lock (_gate) + if (_documentDiagnostics.TryGetValue(documentId, out var diagnostics)) { - if (_documentDiagnostics.TryGetValue(documentId, out var diagnostics)) - { - return diagnostics; - } - - return []; + return diagnostics; } + + return []; } + } - public ImmutableArray GetBuildOnlyDiagnostics(ProjectId projectId) + public ImmutableArray GetBuildOnlyDiagnostics(ProjectId projectId) + { + lock (_gate) { - lock (_gate) + if (_projectDiagnostics.TryGetValue(projectId, out var diagnostics)) { - if (_projectDiagnostics.TryGetValue(projectId, out var diagnostics)) - { - return diagnostics; - } - - return []; + return diagnostics; } + + return []; } } } diff --git a/src/Features/Core/Portable/Diagnostics/BuildToolId.cs b/src/Features/Core/Portable/Diagnostics/BuildToolId.cs index e515b7829a5e4..2b282e4382a9a 100644 --- a/src/Features/Core/Portable/Diagnostics/BuildToolId.cs +++ b/src/Features/Core/Portable/Diagnostics/BuildToolId.cs @@ -4,49 +4,48 @@ using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +/// +/// Support ErrorSource information. +/// +internal abstract class BuildToolId { - /// - /// Support ErrorSource information. - /// - internal abstract class BuildToolId + public abstract string BuildTool { get; } + + internal abstract class Base(T? field) : BuildToolId { - public abstract string BuildTool { get; } + protected readonly T? _Field1 = field; - internal abstract class Base(T? field) : BuildToolId + public override bool Equals(object? obj) { - protected readonly T? _Field1 = field; - - public override bool Equals(object? obj) + if (obj is not Base other) { - if (obj is not Base other) - { - return false; - } - - return object.Equals(_Field1, other._Field1); + return false; } - public override int GetHashCode() - => _Field1?.GetHashCode() ?? 0; + return object.Equals(_Field1, other._Field1); } - internal abstract class Base(T1? field1, T2? field2) : Base(field2) - { - private readonly T1? _Field2 = field1; + public override int GetHashCode() + => _Field1?.GetHashCode() ?? 0; + } - public override bool Equals(object? obj) - { - if (obj is not Base other) - { - return false; - } + internal abstract class Base(T1? field1, T2? field2) : Base(field2) + { + private readonly T1? _Field2 = field1; - return object.Equals(_Field2, other._Field2) && base.Equals(other); + public override bool Equals(object? obj) + { + if (obj is not Base other) + { + return false; } - public override int GetHashCode() - => Hash.Combine(_Field2?.GetHashCode() ?? 0, base.GetHashCode()); + return object.Equals(_Field2, other._Field2) && base.Equals(other); } + + public override int GetHashCode() + => Hash.Combine(_Field2?.GetHashCode() ?? 0, base.GetHashCode()); } } diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerExtensions.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerExtensions.cs index 1681701174106..28c39a91e766a 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerExtensions.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerExtensions.cs @@ -11,60 +11,59 @@ using Microsoft.CodeAnalysis.Simplification; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal static class DiagnosticAnalyzerExtensions { - internal static class DiagnosticAnalyzerExtensions - { - public static bool IsWorkspaceDiagnosticAnalyzer(this DiagnosticAnalyzer analyzer) - => analyzer is DocumentDiagnosticAnalyzer - || analyzer is ProjectDiagnosticAnalyzer - || analyzer == FileContentLoadAnalyzer.Instance - || analyzer == GeneratorDiagnosticsPlaceholderAnalyzer.Instance; + public static bool IsWorkspaceDiagnosticAnalyzer(this DiagnosticAnalyzer analyzer) + => analyzer is DocumentDiagnosticAnalyzer + || analyzer is ProjectDiagnosticAnalyzer + || analyzer == FileContentLoadAnalyzer.Instance + || analyzer == GeneratorDiagnosticsPlaceholderAnalyzer.Instance; - public static bool IsBuiltInAnalyzer(this DiagnosticAnalyzer analyzer) - => analyzer is IBuiltInAnalyzer || analyzer.IsWorkspaceDiagnosticAnalyzer() || analyzer.IsCompilerAnalyzer(); + public static bool IsBuiltInAnalyzer(this DiagnosticAnalyzer analyzer) + => analyzer is IBuiltInAnalyzer || analyzer.IsWorkspaceDiagnosticAnalyzer() || analyzer.IsCompilerAnalyzer(); - public static bool IsOpenFileOnly(this DiagnosticAnalyzer analyzer, SimplifierOptions? options) - => analyzer is IBuiltInAnalyzer builtInAnalyzer && builtInAnalyzer.OpenFileOnly(options); + public static bool IsOpenFileOnly(this DiagnosticAnalyzer analyzer, SimplifierOptions? options) + => analyzer is IBuiltInAnalyzer builtInAnalyzer && builtInAnalyzer.OpenFileOnly(options); - public static ReportDiagnostic GetEffectiveSeverity(this DiagnosticDescriptor descriptor, CompilationOptions options) - { - return options == null - ? descriptor.DefaultSeverity.ToReportDiagnostic() - : descriptor.GetEffectiveSeverity(options); - } + public static ReportDiagnostic GetEffectiveSeverity(this DiagnosticDescriptor descriptor, CompilationOptions options) + { + return options == null + ? descriptor.DefaultSeverity.ToReportDiagnostic() + : descriptor.GetEffectiveSeverity(options); + } - public static (string analyzerId, VersionStamp version) GetAnalyzerIdAndVersion(this DiagnosticAnalyzer analyzer) - { - // Get the unique ID for given diagnostic analyzer. - // note that we also put version stamp so that we can detect changed analyzer. - var typeInfo = analyzer.GetType().GetTypeInfo(); - return (analyzer.GetAnalyzerId(), GetAnalyzerVersion(typeInfo.Assembly.Location)); - } + public static (string analyzerId, VersionStamp version) GetAnalyzerIdAndVersion(this DiagnosticAnalyzer analyzer) + { + // Get the unique ID for given diagnostic analyzer. + // note that we also put version stamp so that we can detect changed analyzer. + var typeInfo = analyzer.GetType().GetTypeInfo(); + return (analyzer.GetAnalyzerId(), GetAnalyzerVersion(typeInfo.Assembly.Location)); + } - private static VersionStamp GetAnalyzerVersion(string path) + private static VersionStamp GetAnalyzerVersion(string path) + { + if (path == null || !File.Exists(path)) { - if (path == null || !File.Exists(path)) - { - return VersionStamp.Default; - } - - return VersionStamp.Create(File.GetLastWriteTimeUtc(path)); + return VersionStamp.Default; } - public static string GetAnalyzerAssemblyName(this DiagnosticAnalyzer analyzer) - => analyzer.GetType().Assembly.GetName().Name ?? throw ExceptionUtilities.Unreachable(); + return VersionStamp.Create(File.GetLastWriteTimeUtc(path)); + } + + public static string GetAnalyzerAssemblyName(this DiagnosticAnalyzer analyzer) + => analyzer.GetType().Assembly.GetName().Name ?? throw ExceptionUtilities.Unreachable(); - public static void AppendAnalyzerMap(this Dictionary analyzerMap, IEnumerable analyzers) + public static void AppendAnalyzerMap(this Dictionary analyzerMap, IEnumerable analyzers) + { + foreach (var analyzer in analyzers) { - foreach (var analyzer in analyzers) - { - // user might have included exact same analyzer twice as project analyzers explicitly. we consider them as one - analyzerMap[analyzer.GetAnalyzerId()] = analyzer; - } + // user might have included exact same analyzer twice as project analyzers explicitly. we consider them as one + analyzerMap[analyzer.GetAnalyzerId()] = analyzer; } - - public static IEnumerable ToAnalyzerPerformanceInfo(this IDictionary analysisResult, DiagnosticAnalyzerInfoCache analyzerInfo) - => analysisResult.Select(kv => new AnalyzerPerformanceInfo(kv.Key.GetAnalyzerId(), analyzerInfo.IsTelemetryCollectionAllowed(kv.Key), kv.Value.ExecutionTime)); } + + public static IEnumerable ToAnalyzerPerformanceInfo(this IDictionary analysisResult, DiagnosticAnalyzerInfoCache analyzerInfo) + => analysisResult.Select(kv => new AnalyzerPerformanceInfo(kv.Key.GetAnalyzerId(), analyzerInfo.IsTelemetryCollectionAllowed(kv.Key), kv.Value.ExecutionTime)); } diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerTelemetry.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerTelemetry.cs index 8d73b8c7b3dcf..27af0e9047ae0 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerTelemetry.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerTelemetry.cs @@ -15,96 +15,95 @@ using Roslyn.Utilities; #endif -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal sealed class DiagnosticAnalyzerTelemetry { - internal sealed class DiagnosticAnalyzerTelemetry + private readonly struct Data(AnalyzerTelemetryInfo analyzerTelemetryInfo, bool isTelemetryCollectionAllowed) { - private readonly struct Data(AnalyzerTelemetryInfo analyzerTelemetryInfo, bool isTelemetryCollectionAllowed) - { - public readonly int CompilationStartActionsCount = analyzerTelemetryInfo.CompilationStartActionsCount; - public readonly int CompilationEndActionsCount = analyzerTelemetryInfo.CompilationEndActionsCount; - public readonly int CompilationActionsCount = analyzerTelemetryInfo.CompilationActionsCount; - public readonly int SyntaxTreeActionsCount = analyzerTelemetryInfo.SyntaxTreeActionsCount; - public readonly int AdditionalFileActionsCount = analyzerTelemetryInfo.AdditionalFileActionsCount; - public readonly int SemanticModelActionsCount = analyzerTelemetryInfo.SemanticModelActionsCount; - public readonly int SymbolActionsCount = analyzerTelemetryInfo.SymbolActionsCount; - public readonly int SymbolStartActionsCount = analyzerTelemetryInfo.SymbolStartActionsCount; - public readonly int SymbolEndActionsCount = analyzerTelemetryInfo.SymbolEndActionsCount; - public readonly int SyntaxNodeActionsCount = analyzerTelemetryInfo.SyntaxNodeActionsCount; - public readonly int CodeBlockStartActionsCount = analyzerTelemetryInfo.CodeBlockStartActionsCount; - public readonly int CodeBlockEndActionsCount = analyzerTelemetryInfo.CodeBlockEndActionsCount; - public readonly int CodeBlockActionsCount = analyzerTelemetryInfo.CodeBlockActionsCount; - public readonly int OperationActionsCount = analyzerTelemetryInfo.OperationActionsCount; - public readonly int OperationBlockStartActionsCount = analyzerTelemetryInfo.OperationBlockStartActionsCount; - public readonly int OperationBlockEndActionsCount = analyzerTelemetryInfo.OperationBlockEndActionsCount; - public readonly int OperationBlockActionsCount = analyzerTelemetryInfo.OperationBlockActionsCount; - public readonly int SuppressionActionsCount = analyzerTelemetryInfo.SuppressionActionsCount; + public readonly int CompilationStartActionsCount = analyzerTelemetryInfo.CompilationStartActionsCount; + public readonly int CompilationEndActionsCount = analyzerTelemetryInfo.CompilationEndActionsCount; + public readonly int CompilationActionsCount = analyzerTelemetryInfo.CompilationActionsCount; + public readonly int SyntaxTreeActionsCount = analyzerTelemetryInfo.SyntaxTreeActionsCount; + public readonly int AdditionalFileActionsCount = analyzerTelemetryInfo.AdditionalFileActionsCount; + public readonly int SemanticModelActionsCount = analyzerTelemetryInfo.SemanticModelActionsCount; + public readonly int SymbolActionsCount = analyzerTelemetryInfo.SymbolActionsCount; + public readonly int SymbolStartActionsCount = analyzerTelemetryInfo.SymbolStartActionsCount; + public readonly int SymbolEndActionsCount = analyzerTelemetryInfo.SymbolEndActionsCount; + public readonly int SyntaxNodeActionsCount = analyzerTelemetryInfo.SyntaxNodeActionsCount; + public readonly int CodeBlockStartActionsCount = analyzerTelemetryInfo.CodeBlockStartActionsCount; + public readonly int CodeBlockEndActionsCount = analyzerTelemetryInfo.CodeBlockEndActionsCount; + public readonly int CodeBlockActionsCount = analyzerTelemetryInfo.CodeBlockActionsCount; + public readonly int OperationActionsCount = analyzerTelemetryInfo.OperationActionsCount; + public readonly int OperationBlockStartActionsCount = analyzerTelemetryInfo.OperationBlockStartActionsCount; + public readonly int OperationBlockEndActionsCount = analyzerTelemetryInfo.OperationBlockEndActionsCount; + public readonly int OperationBlockActionsCount = analyzerTelemetryInfo.OperationBlockActionsCount; + public readonly int SuppressionActionsCount = analyzerTelemetryInfo.SuppressionActionsCount; - public readonly bool IsTelemetryCollectionAllowed = isTelemetryCollectionAllowed; - } + public readonly bool IsTelemetryCollectionAllowed = isTelemetryCollectionAllowed; + } - private readonly object _guard = new(); - private ImmutableDictionary _analyzerInfoMap; + private readonly object _guard = new(); + private ImmutableDictionary _analyzerInfoMap; - public DiagnosticAnalyzerTelemetry() - => _analyzerInfoMap = ImmutableDictionary.Empty; + public DiagnosticAnalyzerTelemetry() + => _analyzerInfoMap = ImmutableDictionary.Empty; - public void UpdateAnalyzerActionsTelemetry(DiagnosticAnalyzer analyzer, AnalyzerTelemetryInfo analyzerTelemetryInfo, bool isTelemetryCollectionAllowed) + public void UpdateAnalyzerActionsTelemetry(DiagnosticAnalyzer analyzer, AnalyzerTelemetryInfo analyzerTelemetryInfo, bool isTelemetryCollectionAllowed) + { + lock (_guard) { - lock (_guard) - { - _analyzerInfoMap = _analyzerInfoMap.SetItem(analyzer.GetType(), new Data(analyzerTelemetryInfo, isTelemetryCollectionAllowed)); - } + _analyzerInfoMap = _analyzerInfoMap.SetItem(analyzer.GetType(), new Data(analyzerTelemetryInfo, isTelemetryCollectionAllowed)); } + } - public void ReportAndClear(int correlationId) + public void ReportAndClear(int correlationId) + { + ImmutableDictionary map; + lock (_guard) { - ImmutableDictionary map; - lock (_guard) - { - map = _analyzerInfoMap; - _analyzerInfoMap = ImmutableDictionary.Empty; - } + map = _analyzerInfoMap; + _analyzerInfoMap = ImmutableDictionary.Empty; + } - foreach (var (analyzerType, analyzerInfo) in map) + foreach (var (analyzerType, analyzerInfo) in map) + { + Logger.Log(FunctionId.DiagnosticAnalyzerDriver_AnalyzerTypeCount, KeyValueLogMessage.Create(m => { - Logger.Log(FunctionId.DiagnosticAnalyzerDriver_AnalyzerTypeCount, KeyValueLogMessage.Create(m => - { - m["Id"] = correlationId; + m["Id"] = correlationId; - var analyzerName = analyzerType.FullName; + var analyzerName = analyzerType.FullName; - if (analyzerInfo.IsTelemetryCollectionAllowed) - { - // log analyzer name and exception as is: - m["Analyzer.Name"] = analyzerName; - } - else - { - // annonymize analyzer and exception names: - m["Analyzer.NameHashCode"] = AnalyzerNameForTelemetry.ComputeSha256Hash(analyzerName); - } + if (analyzerInfo.IsTelemetryCollectionAllowed) + { + // log analyzer name and exception as is: + m["Analyzer.Name"] = analyzerName; + } + else + { + // annonymize analyzer and exception names: + m["Analyzer.NameHashCode"] = AnalyzerNameForTelemetry.ComputeSha256Hash(analyzerName); + } - m["Analyzer.CodeBlock"] = analyzerInfo.CodeBlockActionsCount; - m["Analyzer.CodeBlockStart"] = analyzerInfo.CodeBlockStartActionsCount; - m["Analyzer.CodeBlockEnd"] = analyzerInfo.CodeBlockEndActionsCount; - m["Analyzer.Compilation"] = analyzerInfo.CompilationActionsCount; - m["Analyzer.CompilationStart"] = analyzerInfo.CompilationStartActionsCount; - m["Analyzer.CompilationEnd"] = analyzerInfo.CompilationEndActionsCount; - m["Analyzer.SemanticModel"] = analyzerInfo.SemanticModelActionsCount; - m["Analyzer.SyntaxNode"] = analyzerInfo.SyntaxNodeActionsCount; - m["Analyzer.SyntaxTree"] = analyzerInfo.SyntaxTreeActionsCount; - m["Analyzer.AdditionalFile"] = analyzerInfo.AdditionalFileActionsCount; - m["Analyzer.Operation"] = analyzerInfo.OperationActionsCount; - m["Analyzer.OperationBlock"] = analyzerInfo.OperationBlockActionsCount; - m["Analyzer.OperationBlockStart"] = analyzerInfo.OperationBlockStartActionsCount; - m["Analyzer.OperationBlockEnd"] = analyzerInfo.OperationBlockEndActionsCount; - m["Analyzer.Symbol"] = analyzerInfo.SymbolActionsCount; - m["Analyzer.SymbolStart"] = analyzerInfo.SymbolStartActionsCount; - m["Analyzer.SymbolEnd"] = analyzerInfo.SymbolEndActionsCount; - m["Analyzer.Suppression"] = analyzerInfo.SuppressionActionsCount; - })); - } + m["Analyzer.CodeBlock"] = analyzerInfo.CodeBlockActionsCount; + m["Analyzer.CodeBlockStart"] = analyzerInfo.CodeBlockStartActionsCount; + m["Analyzer.CodeBlockEnd"] = analyzerInfo.CodeBlockEndActionsCount; + m["Analyzer.Compilation"] = analyzerInfo.CompilationActionsCount; + m["Analyzer.CompilationStart"] = analyzerInfo.CompilationStartActionsCount; + m["Analyzer.CompilationEnd"] = analyzerInfo.CompilationEndActionsCount; + m["Analyzer.SemanticModel"] = analyzerInfo.SemanticModelActionsCount; + m["Analyzer.SyntaxNode"] = analyzerInfo.SyntaxNodeActionsCount; + m["Analyzer.SyntaxTree"] = analyzerInfo.SyntaxTreeActionsCount; + m["Analyzer.AdditionalFile"] = analyzerInfo.AdditionalFileActionsCount; + m["Analyzer.Operation"] = analyzerInfo.OperationActionsCount; + m["Analyzer.OperationBlock"] = analyzerInfo.OperationBlockActionsCount; + m["Analyzer.OperationBlockStart"] = analyzerInfo.OperationBlockStartActionsCount; + m["Analyzer.OperationBlockEnd"] = analyzerInfo.OperationBlockEndActionsCount; + m["Analyzer.Symbol"] = analyzerInfo.SymbolActionsCount; + m["Analyzer.SymbolStart"] = analyzerInfo.SymbolStartActionsCount; + m["Analyzer.SymbolEnd"] = analyzerInfo.SymbolEndActionsCount; + m["Analyzer.Suppression"] = analyzerInfo.SuppressionActionsCount; + })); } } } diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticArguments.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticArguments.cs index d74ffcab35e34..78e6664f02d70 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticArguments.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticArguments.cs @@ -6,106 +6,105 @@ using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +/// +/// helper type to package diagnostic arguments to pass around between remote hosts +/// +[DataContract] +internal class DiagnosticArguments { /// - /// helper type to package diagnostic arguments to pass around between remote hosts + /// Flag indicating if suppressed diagnostics should be returned. /// - [DataContract] - internal class DiagnosticArguments - { - /// - /// Flag indicating if suppressed diagnostics should be returned. - /// - [DataMember(Order = 0)] - public bool ReportSuppressedDiagnostics; + [DataMember(Order = 0)] + public bool ReportSuppressedDiagnostics; - /// - /// Flag indicating if analyzer performance info, such as analyzer execution times, - /// should be logged as performance telemetry. - /// - [DataMember(Order = 1)] - public bool LogPerformanceInfo; + /// + /// Flag indicating if analyzer performance info, such as analyzer execution times, + /// should be logged as performance telemetry. + /// + [DataMember(Order = 1)] + public bool LogPerformanceInfo; - /// - /// Flag indicating if the analyzer telemety info, such as registered analyzer action counts - /// and analyzer execution times, should be included in the computed result. - /// - [DataMember(Order = 2)] - public bool GetTelemetryInfo; + /// + /// Flag indicating if the analyzer telemety info, such as registered analyzer action counts + /// and analyzer execution times, should be included in the computed result. + /// + [DataMember(Order = 2)] + public bool GetTelemetryInfo; - /// - /// Optional document ID, if computing diagnostics for a specific document. - /// For example, diagnostic computation for open file analysis. - /// - [DataMember(Order = 3)] - public DocumentId? DocumentId; + /// + /// Optional document ID, if computing diagnostics for a specific document. + /// For example, diagnostic computation for open file analysis. + /// + [DataMember(Order = 3)] + public DocumentId? DocumentId; - /// - /// Optional document text span, if computing diagnostics for a specific span for a document. - /// For example, diagnostic computation for light bulb invocation for a specific line in active document. - /// - [DataMember(Order = 4)] - public TextSpan? DocumentSpan; + /// + /// Optional document text span, if computing diagnostics for a specific span for a document. + /// For example, diagnostic computation for light bulb invocation for a specific line in active document. + /// + [DataMember(Order = 4)] + public TextSpan? DocumentSpan; - /// - /// Optional , if computing specific kind of diagnostics for a document request, - /// i.e. must be non-null for a non-null analysis kind. - /// Only supported non-null values are and . - /// - [DataMember(Order = 5)] - public AnalysisKind? DocumentAnalysisKind; + /// + /// Optional , if computing specific kind of diagnostics for a document request, + /// i.e. must be non-null for a non-null analysis kind. + /// Only supported non-null values are and . + /// + [DataMember(Order = 5)] + public AnalysisKind? DocumentAnalysisKind; - /// - /// Project ID for the document or project for which diagnostics need to be computed. - /// - [DataMember(Order = 6)] - public ProjectId ProjectId; + /// + /// Project ID for the document or project for which diagnostics need to be computed. + /// + [DataMember(Order = 6)] + public ProjectId ProjectId; - /// - /// Array of analyzer IDs for analyzers that need to be executed for computing diagnostics. - /// - [DataMember(Order = 7)] - public string[] AnalyzerIds; + /// + /// Array of analyzer IDs for analyzers that need to be executed for computing diagnostics. + /// + [DataMember(Order = 7)] + public string[] AnalyzerIds; - [DataMember(Order = 8)] - public IdeAnalyzerOptions IdeOptions; + [DataMember(Order = 8)] + public IdeAnalyzerOptions IdeOptions; - /// - /// Indicates diagnostic computation for an explicit user-invoked request, - /// such as a user-invoked Ctrl + Dot operation to bring up the light bulb. - /// - [DataMember(Order = 9)] - public bool IsExplicit; + /// + /// Indicates diagnostic computation for an explicit user-invoked request, + /// such as a user-invoked Ctrl + Dot operation to bring up the light bulb. + /// + [DataMember(Order = 9)] + public bool IsExplicit; - public DiagnosticArguments( - bool reportSuppressedDiagnostics, - bool logPerformanceInfo, - bool getTelemetryInfo, - DocumentId? documentId, - TextSpan? documentSpan, - AnalysisKind? documentAnalysisKind, - ProjectId projectId, - string[] analyzerIds, - IdeAnalyzerOptions ideOptions, - bool isExplicit) - { - Debug.Assert(documentId != null || documentSpan == null); - Debug.Assert(documentId != null || documentAnalysisKind == null); - Debug.Assert(documentAnalysisKind is null or - (AnalysisKind?)AnalysisKind.Syntax or (AnalysisKind?)AnalysisKind.Semantic); - Debug.Assert(analyzerIds.Length > 0); + public DiagnosticArguments( + bool reportSuppressedDiagnostics, + bool logPerformanceInfo, + bool getTelemetryInfo, + DocumentId? documentId, + TextSpan? documentSpan, + AnalysisKind? documentAnalysisKind, + ProjectId projectId, + string[] analyzerIds, + IdeAnalyzerOptions ideOptions, + bool isExplicit) + { + Debug.Assert(documentId != null || documentSpan == null); + Debug.Assert(documentId != null || documentAnalysisKind == null); + Debug.Assert(documentAnalysisKind is null or + (AnalysisKind?)AnalysisKind.Syntax or (AnalysisKind?)AnalysisKind.Semantic); + Debug.Assert(analyzerIds.Length > 0); - ReportSuppressedDiagnostics = reportSuppressedDiagnostics; - LogPerformanceInfo = logPerformanceInfo; - GetTelemetryInfo = getTelemetryInfo; - DocumentId = documentId; - DocumentSpan = documentSpan; - DocumentAnalysisKind = documentAnalysisKind; - ProjectId = projectId; - AnalyzerIds = analyzerIds; - IdeOptions = ideOptions; - IsExplicit = isExplicit; - } + ReportSuppressedDiagnostics = reportSuppressedDiagnostics; + LogPerformanceInfo = logPerformanceInfo; + GetTelemetryInfo = getTelemetryInfo; + DocumentId = documentId; + DocumentSpan = documentSpan; + DocumentAnalysisKind = documentAnalysisKind; + ProjectId = projectId; + AnalyzerIds = analyzerIds; + IdeOptions = ideOptions; + IsExplicit = isExplicit; } } diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticBucket.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticBucket.cs index 015b5d174057a..64d9a1ea57e5f 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticBucket.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticBucket.cs @@ -2,28 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal readonly struct DiagnosticBucket(object id, Workspace workspace, ProjectId? projectId, DocumentId? documentId) { - internal readonly struct DiagnosticBucket(object id, Workspace workspace, ProjectId? projectId, DocumentId? documentId) - { - /// - /// The identity of bucket group. - /// - public readonly object Id = id; + /// + /// The identity of bucket group. + /// + public readonly object Id = id; - /// - /// this bucket is associated with. - /// - public readonly Workspace Workspace = workspace; + /// + /// this bucket is associated with. + /// + public readonly Workspace Workspace = workspace; - /// - /// this bucket is associated with, or . - /// - public readonly ProjectId? ProjectId = projectId; + /// + /// this bucket is associated with, or . + /// + public readonly ProjectId? ProjectId = projectId; - /// - /// this bucket is associated with, or . - /// - public readonly DocumentId? DocumentId = documentId; - } + /// + /// this bucket is associated with, or . + /// + public readonly DocumentId? DocumentId = documentId; } diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticProviderMetadata.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticProviderMetadata.cs index a4f15f22941be..15d57c3ed94c6 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticProviderMetadata.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticProviderMetadata.cs @@ -8,11 +8,10 @@ using Microsoft.CodeAnalysis.Host.Mef; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal class DiagnosticProviderMetadata(IDictionary data) : ILanguageMetadata { - internal class DiagnosticProviderMetadata(IDictionary data) : ILanguageMetadata - { - public string Name { get; } = (string)data.GetValueOrDefault("Name"); - public string Language { get; } = (string)data.GetValueOrDefault("Language"); - } + public string Name { get; } = (string)data.GetValueOrDefault("Name"); + public string Language { get; } = (string)data.GetValueOrDefault("Language"); } diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticsUpdatedArgs.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticsUpdatedArgs.cs index 3d511c2e56eee..fbc559a3f0ace 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticsUpdatedArgs.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticsUpdatedArgs.cs @@ -7,54 +7,53 @@ using System.Linq; using Microsoft.CodeAnalysis.Common; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal sealed class DiagnosticsUpdatedArgs : UpdatedEventArgs { - internal sealed class DiagnosticsUpdatedArgs : UpdatedEventArgs + public DiagnosticsUpdatedKind Kind { get; } + public Solution? Solution { get; } + + private readonly ImmutableArray _diagnostics; + + private DiagnosticsUpdatedArgs( + object id, + Workspace workspace, + Solution? solution, + ProjectId? projectId, + DocumentId? documentId, + ImmutableArray diagnostics, + DiagnosticsUpdatedKind kind) + : base(id, workspace, projectId, documentId) + { + Debug.Assert(diagnostics.All(d => d.ProjectId == projectId && d.DocumentId == documentId)); + Debug.Assert(kind != DiagnosticsUpdatedKind.DiagnosticsRemoved || diagnostics.IsEmpty); + + Solution = solution; + Kind = kind; + _diagnostics = diagnostics; + } + + public ImmutableArray Diagnostics => _diagnostics; + + public static DiagnosticsUpdatedArgs DiagnosticsCreated( + object id, + Workspace workspace, + Solution? solution, + ProjectId? projectId, + DocumentId? documentId, + ImmutableArray diagnostics) + { + return new DiagnosticsUpdatedArgs(id, workspace, solution, projectId, documentId, diagnostics, DiagnosticsUpdatedKind.DiagnosticsCreated); + } + + public static DiagnosticsUpdatedArgs DiagnosticsRemoved( + object id, + Workspace workspace, + Solution? solution, + ProjectId? projectId, + DocumentId? documentId) { - public DiagnosticsUpdatedKind Kind { get; } - public Solution? Solution { get; } - - private readonly ImmutableArray _diagnostics; - - private DiagnosticsUpdatedArgs( - object id, - Workspace workspace, - Solution? solution, - ProjectId? projectId, - DocumentId? documentId, - ImmutableArray diagnostics, - DiagnosticsUpdatedKind kind) - : base(id, workspace, projectId, documentId) - { - Debug.Assert(diagnostics.All(d => d.ProjectId == projectId && d.DocumentId == documentId)); - Debug.Assert(kind != DiagnosticsUpdatedKind.DiagnosticsRemoved || diagnostics.IsEmpty); - - Solution = solution; - Kind = kind; - _diagnostics = diagnostics; - } - - public ImmutableArray Diagnostics => _diagnostics; - - public static DiagnosticsUpdatedArgs DiagnosticsCreated( - object id, - Workspace workspace, - Solution? solution, - ProjectId? projectId, - DocumentId? documentId, - ImmutableArray diagnostics) - { - return new DiagnosticsUpdatedArgs(id, workspace, solution, projectId, documentId, diagnostics, DiagnosticsUpdatedKind.DiagnosticsCreated); - } - - public static DiagnosticsUpdatedArgs DiagnosticsRemoved( - object id, - Workspace workspace, - Solution? solution, - ProjectId? projectId, - DocumentId? documentId) - { - return new DiagnosticsUpdatedArgs(id, workspace, solution, projectId, documentId, [], DiagnosticsUpdatedKind.DiagnosticsRemoved); - } + return new DiagnosticsUpdatedArgs(id, workspace, solution, projectId, documentId, [], DiagnosticsUpdatedKind.DiagnosticsRemoved); } } diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticsUpdatedKind.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticsUpdatedKind.cs index e6cd6e70c5870..36768de810188 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticsUpdatedKind.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticsUpdatedKind.cs @@ -2,24 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal enum DiagnosticsUpdatedKind { - internal enum DiagnosticsUpdatedKind - { - /// - /// Called when the diagnostic analyzer engine decides to remove existing diagnostics. - /// For example, this can happen when a document is removed from a solution. In that - /// case the analyzer engine will delete all diagnostics associated with that document. - /// Any layers caching diagnostics should listen for these events to know when to - /// delete their cached items entirely. - /// - DiagnosticsRemoved, + /// + /// Called when the diagnostic analyzer engine decides to remove existing diagnostics. + /// For example, this can happen when a document is removed from a solution. In that + /// case the analyzer engine will delete all diagnostics associated with that document. + /// Any layers caching diagnostics should listen for these events to know when to + /// delete their cached items entirely. + /// + DiagnosticsRemoved, - /// - /// Called when a new set of (possibly empty) diagnostics have been produced. This - /// happens through normal editing and processing of files as diagnostic analyzers - /// produce new diagnostics for documents and projects. - /// - DiagnosticsCreated, - } + /// + /// Called when a new set of (possibly empty) diagnostics have been produced. This + /// happens through normal editing and processing of files as diagnostic analyzers + /// produce new diagnostics for documents and projects. + /// + DiagnosticsCreated, } diff --git a/src/Features/Core/Portable/Diagnostics/IAnalyzerDriverService.cs b/src/Features/Core/Portable/Diagnostics/IAnalyzerDriverService.cs index 7b4363f194de1..c8b3bb1119f07 100644 --- a/src/Features/Core/Portable/Diagnostics/IAnalyzerDriverService.cs +++ b/src/Features/Core/Portable/Diagnostics/IAnalyzerDriverService.cs @@ -9,19 +9,18 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal interface IAnalyzerDriverService : ILanguageService { - internal interface IAnalyzerDriverService : ILanguageService - { - /// - /// Computes the for all the declarations whose span overlaps with the given . - /// - /// The semantic model for the document. - /// Span to get declarations. - /// Flag indicating whether should be computed for the returned declaration infos. - /// If false, then is always null. - /// Builder to add computed declarations. - /// Cancellation token. - void ComputeDeclarationsInSpan(SemanticModel model, TextSpan span, bool getSymbol, ArrayBuilder builder, CancellationToken cancellationToken); - } + /// + /// Computes the for all the declarations whose span overlaps with the given . + /// + /// The semantic model for the document. + /// Span to get declarations. + /// Flag indicating whether should be computed for the returned declaration infos. + /// If false, then is always null. + /// Builder to add computed declarations. + /// Cancellation token. + void ComputeDeclarationsInSpan(SemanticModel model, TextSpan span, bool getSymbol, ArrayBuilder builder, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/Diagnostics/IBuildOnlyDiagnosticsService.cs b/src/Features/Core/Portable/Diagnostics/IBuildOnlyDiagnosticsService.cs index 52a34b01326db..4230d17fb9539 100644 --- a/src/Features/Core/Portable/Diagnostics/IBuildOnlyDiagnosticsService.cs +++ b/src/Features/Core/Portable/Diagnostics/IBuildOnlyDiagnosticsService.cs @@ -5,20 +5,19 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +/// +/// Service to keep track of build-only diagnostics reported from explicit Build/Rebuild commands. +/// Note that this service only keeps track of those diagnostics that can never be reported from live analysis. +/// +internal interface IBuildOnlyDiagnosticsService : IWorkspaceService { - /// - /// Service to keep track of build-only diagnostics reported from explicit Build/Rebuild commands. - /// Note that this service only keeps track of those diagnostics that can never be reported from live analysis. - /// - internal interface IBuildOnlyDiagnosticsService : IWorkspaceService - { - void AddBuildOnlyDiagnostics(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableArray diagnostics); + void AddBuildOnlyDiagnostics(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableArray diagnostics); - void ClearBuildOnlyDiagnostics(Solution solution, ProjectId? projectId, DocumentId? documentId); + void ClearBuildOnlyDiagnostics(Solution solution, ProjectId? projectId, DocumentId? documentId); - ImmutableArray GetBuildOnlyDiagnostics(DocumentId documentId); + ImmutableArray GetBuildOnlyDiagnostics(DocumentId documentId); - ImmutableArray GetBuildOnlyDiagnostics(ProjectId projectId); - } + ImmutableArray GetBuildOnlyDiagnostics(ProjectId projectId); } diff --git a/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs index b489e95fa9408..25f722a88a18b 100644 --- a/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs +++ b/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs @@ -11,213 +11,212 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +// TODO: Remove all optional parameters from IDiagnosticAnalyzerService +// Tracked with https://github.com/dotnet/roslyn/issues/67434 +internal interface IDiagnosticAnalyzerService { - // TODO: Remove all optional parameters from IDiagnosticAnalyzerService - // Tracked with https://github.com/dotnet/roslyn/issues/67434 - internal interface IDiagnosticAnalyzerService - { - public IGlobalOptionService GlobalOptions { get; } - - /// - /// Provides and caches analyzer information. - /// - DiagnosticAnalyzerInfoCache AnalyzerInfoCache { get; } - - /// - /// Re-analyze given projects and documents. If both and are null, - /// then re-analyzes the entire for the given . - /// - void Reanalyze(Workspace workspace, IEnumerable? projectIds, IEnumerable? documentIds, bool highPriority); - - /// - /// Get specific diagnostics currently stored in the source. returned diagnostic might be out-of-date if solution has changed but analyzer hasn't run for the new solution. - /// - /// Workspace to fetch the diagnostics for. - /// - /// Specific id to scope the returned diagnostics. This id must correspond to the - /// associated with event. - /// - /// Indicates if diagnostics suppressed in source via pragmas and SuppressMessageAttributes should be returned. - /// - /// Indicates if non-local document diagnostics must be returned. - /// Non-local diagnostics are the ones reported by analyzers either at compilation end callback OR - /// in a different file from which the callback was made. Entire project must be analyzed to get the - /// complete set of non-local document diagnostics. - /// - /// Cancellation token. - Task> GetSpecificCachedDiagnosticsAsync(Workspace workspace, object id, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken); - - /// - /// Get diagnostics currently stored in the source. returned diagnostic might be out-of-date if solution has changed but analyzer hasn't run for the new solution. - /// - /// Workspace for the document/project/solution to compute diagnostics for. - /// Optional project to scope the returned diagnostics. - /// Optional document to scope the returned diagnostics. - /// Indicates if diagnostics suppressed in source via pragmas and SuppressMessageAttributes should be returned. - /// - /// Indicates if local document diagnostics must be returned. - /// Local diagnostics are the ones that are reported by analyzers on the same file for which the callback was received - /// and hence can be computed by analyzing a single file in isolation. - /// - /// - /// Indicates if non-local document diagnostics must be returned. - /// Non-local diagnostics are the ones reported by analyzers either at compilation end callback OR - /// in a different file from which the callback was made. Entire project must be analyzed to get the - /// complete set of non-local document diagnostics. - /// - /// Cancellation token. - Task> GetCachedDiagnosticsAsync(Workspace workspace, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken); - - /// - /// Get diagnostics for the given solution. all diagnostics returned should be up-to-date with respect to the given solution. - /// - /// Solution to fetch diagnostics for. - /// Optional project to scope the returned diagnostics. - /// Optional document to scope the returned diagnostics. - /// Indicates if diagnostics suppressed in source via pragmas and SuppressMessageAttributes should be returned. - /// - /// Indicates if non-local document diagnostics must be returned. - /// Non-local diagnostics are the ones reported by analyzers either at compilation end callback OR - /// in a different file from which the callback was made. Entire project must be analyzed to get the - /// complete set of non-local document diagnostics. - /// - /// Cancellation token. - Task> GetDiagnosticsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken); - - /// - /// Force analyzes the given project by running all applicable analyzers on the project and caching the reported analyzer diagnostics. - /// - Task ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken); - - /// - /// Get diagnostics of the given diagnostic ids and/or analyzers from the given solution. all diagnostics returned should be up-to-date with respect to the given solution. - /// Note that for project case, this method returns diagnostics from all project documents as well. Use - /// if you want to fetch only project diagnostics without source locations. - /// - /// Solution to fetch the diagnostics for. - /// Optional project to scope the returned diagnostics. - /// Optional document to scope the returned diagnostics. - /// Optional set of diagnostic IDs to scope the returned diagnostics. - /// Option callback to filter out analyzers to execute for computing diagnostics. - /// Indicates if diagnostics suppressed in source via pragmas and SuppressMessageAttributes should be returned. - /// - /// Indicates if local document diagnostics must be returned. - /// Local diagnostics are the ones that are reported by analyzers on the same file for which the callback was received - /// and hence can be computed by analyzing a single file in isolation. - /// - /// - /// Indicates if non-local document diagnostics must be returned. - /// Non-local diagnostics are the ones reported by analyzers either at compilation end callback OR - /// in a different file from which the callback was made. Entire project must be analyzed to get the - /// complete set of non-local document diagnostics. - /// - /// Cancellation token. - Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken); - - /// - /// Get project diagnostics (diagnostics with no source location) of the given diagnostic ids and/or analyzers from the given solution. all diagnostics returned should be up-to-date with respect to the given solution. - /// Note that this method doesn't return any document diagnostics. Use to also fetch those. - /// - /// Solution to fetch the diagnostics for. - /// Optional project to scope the returned diagnostics. - /// Optional set of diagnostic IDs to scope the returned diagnostics. - /// Option callback to filter out analyzers to execute for computing diagnostics. - /// Indicates if diagnostics suppressed in source via SuppressMessageAttributes should be returned. - /// - /// Indicates if non-local document diagnostics must be returned. - /// Non-local diagnostics are the ones reported by analyzers either at compilation end callback. - /// Entire project must be analyzed to get the complete set of non-local diagnostics. - /// - /// Cancellation token. - Task> GetProjectDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken); - - /// - /// Try to return up to date diagnostics for the given span for the document. - /// - /// It will return true if it was able to return all up-to-date diagnostics. - /// otherwise, false indicating there are some missing diagnostics in the diagnostic list - /// - /// This API will only force complete analyzers that support span based analysis, i.e. compiler analyzer and - /// s that support . - /// For the rest of the analyzers, it will only return diagnostics if the analyzer has already been executed. - /// Use - /// if you want to force complete all analyzers and get up-to-date diagnostics for all analyzers for the given span. - /// - Task<(ImmutableArray diagnostics, bool upToDate)> TryGetDiagnosticsForSpanAsync( - TextDocument document, TextSpan range, Func? shouldIncludeDiagnostic, - bool includeSuppressedDiagnostics, - ICodeActionRequestPriorityProvider priorityProvider, - DiagnosticKind diagnosticKind, - bool isExplicit, - CancellationToken cancellationToken); - - /// - /// Return up to date diagnostics for the given span for the document - /// - /// This can be expensive since it is force analyzing diagnostics if it doesn't have up-to-date one yet. - /// Predicate filters out analyzers from execution if - /// none of its reported diagnostics should be included in the result. - /// - /// - Task> GetDiagnosticsForSpanAsync( - TextDocument document, TextSpan? range, Func? shouldIncludeDiagnostic, - bool includeCompilerDiagnostics, - bool includeSuppressedDiagnostics, - ICodeActionRequestPriorityProvider priorityProvider, - Func? addOperationScope, - DiagnosticKind diagnosticKind, - bool isExplicit, - CancellationToken cancellationToken); - } + public IGlobalOptionService GlobalOptions { get; } + + /// + /// Provides and caches analyzer information. + /// + DiagnosticAnalyzerInfoCache AnalyzerInfoCache { get; } + + /// + /// Re-analyze given projects and documents. If both and are null, + /// then re-analyzes the entire for the given . + /// + void Reanalyze(Workspace workspace, IEnumerable? projectIds, IEnumerable? documentIds, bool highPriority); + + /// + /// Get specific diagnostics currently stored in the source. returned diagnostic might be out-of-date if solution has changed but analyzer hasn't run for the new solution. + /// + /// Workspace to fetch the diagnostics for. + /// + /// Specific id to scope the returned diagnostics. This id must correspond to the + /// associated with event. + /// + /// Indicates if diagnostics suppressed in source via pragmas and SuppressMessageAttributes should be returned. + /// + /// Indicates if non-local document diagnostics must be returned. + /// Non-local diagnostics are the ones reported by analyzers either at compilation end callback OR + /// in a different file from which the callback was made. Entire project must be analyzed to get the + /// complete set of non-local document diagnostics. + /// + /// Cancellation token. + Task> GetSpecificCachedDiagnosticsAsync(Workspace workspace, object id, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken); + + /// + /// Get diagnostics currently stored in the source. returned diagnostic might be out-of-date if solution has changed but analyzer hasn't run for the new solution. + /// + /// Workspace for the document/project/solution to compute diagnostics for. + /// Optional project to scope the returned diagnostics. + /// Optional document to scope the returned diagnostics. + /// Indicates if diagnostics suppressed in source via pragmas and SuppressMessageAttributes should be returned. + /// + /// Indicates if local document diagnostics must be returned. + /// Local diagnostics are the ones that are reported by analyzers on the same file for which the callback was received + /// and hence can be computed by analyzing a single file in isolation. + /// + /// + /// Indicates if non-local document diagnostics must be returned. + /// Non-local diagnostics are the ones reported by analyzers either at compilation end callback OR + /// in a different file from which the callback was made. Entire project must be analyzed to get the + /// complete set of non-local document diagnostics. + /// + /// Cancellation token. + Task> GetCachedDiagnosticsAsync(Workspace workspace, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken); + + /// + /// Get diagnostics for the given solution. all diagnostics returned should be up-to-date with respect to the given solution. + /// + /// Solution to fetch diagnostics for. + /// Optional project to scope the returned diagnostics. + /// Optional document to scope the returned diagnostics. + /// Indicates if diagnostics suppressed in source via pragmas and SuppressMessageAttributes should be returned. + /// + /// Indicates if non-local document diagnostics must be returned. + /// Non-local diagnostics are the ones reported by analyzers either at compilation end callback OR + /// in a different file from which the callback was made. Entire project must be analyzed to get the + /// complete set of non-local document diagnostics. + /// + /// Cancellation token. + Task> GetDiagnosticsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken); + + /// + /// Force analyzes the given project by running all applicable analyzers on the project and caching the reported analyzer diagnostics. + /// + Task ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken); + + /// + /// Get diagnostics of the given diagnostic ids and/or analyzers from the given solution. all diagnostics returned should be up-to-date with respect to the given solution. + /// Note that for project case, this method returns diagnostics from all project documents as well. Use + /// if you want to fetch only project diagnostics without source locations. + /// + /// Solution to fetch the diagnostics for. + /// Optional project to scope the returned diagnostics. + /// Optional document to scope the returned diagnostics. + /// Optional set of diagnostic IDs to scope the returned diagnostics. + /// Option callback to filter out analyzers to execute for computing diagnostics. + /// Indicates if diagnostics suppressed in source via pragmas and SuppressMessageAttributes should be returned. + /// + /// Indicates if local document diagnostics must be returned. + /// Local diagnostics are the ones that are reported by analyzers on the same file for which the callback was received + /// and hence can be computed by analyzing a single file in isolation. + /// + /// + /// Indicates if non-local document diagnostics must be returned. + /// Non-local diagnostics are the ones reported by analyzers either at compilation end callback OR + /// in a different file from which the callback was made. Entire project must be analyzed to get the + /// complete set of non-local document diagnostics. + /// + /// Cancellation token. + Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken); + + /// + /// Get project diagnostics (diagnostics with no source location) of the given diagnostic ids and/or analyzers from the given solution. all diagnostics returned should be up-to-date with respect to the given solution. + /// Note that this method doesn't return any document diagnostics. Use to also fetch those. + /// + /// Solution to fetch the diagnostics for. + /// Optional project to scope the returned diagnostics. + /// Optional set of diagnostic IDs to scope the returned diagnostics. + /// Option callback to filter out analyzers to execute for computing diagnostics. + /// Indicates if diagnostics suppressed in source via SuppressMessageAttributes should be returned. + /// + /// Indicates if non-local document diagnostics must be returned. + /// Non-local diagnostics are the ones reported by analyzers either at compilation end callback. + /// Entire project must be analyzed to get the complete set of non-local diagnostics. + /// + /// Cancellation token. + Task> GetProjectDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken); + + /// + /// Try to return up to date diagnostics for the given span for the document. + /// + /// It will return true if it was able to return all up-to-date diagnostics. + /// otherwise, false indicating there are some missing diagnostics in the diagnostic list + /// + /// This API will only force complete analyzers that support span based analysis, i.e. compiler analyzer and + /// s that support . + /// For the rest of the analyzers, it will only return diagnostics if the analyzer has already been executed. + /// Use + /// if you want to force complete all analyzers and get up-to-date diagnostics for all analyzers for the given span. + /// + Task<(ImmutableArray diagnostics, bool upToDate)> TryGetDiagnosticsForSpanAsync( + TextDocument document, TextSpan range, Func? shouldIncludeDiagnostic, + bool includeSuppressedDiagnostics, + ICodeActionRequestPriorityProvider priorityProvider, + DiagnosticKind diagnosticKind, + bool isExplicit, + CancellationToken cancellationToken); + + /// + /// Return up to date diagnostics for the given span for the document + /// + /// This can be expensive since it is force analyzing diagnostics if it doesn't have up-to-date one yet. + /// Predicate filters out analyzers from execution if + /// none of its reported diagnostics should be included in the result. + /// + /// + Task> GetDiagnosticsForSpanAsync( + TextDocument document, TextSpan? range, Func? shouldIncludeDiagnostic, + bool includeCompilerDiagnostics, + bool includeSuppressedDiagnostics, + ICodeActionRequestPriorityProvider priorityProvider, + Func? addOperationScope, + DiagnosticKind diagnosticKind, + bool isExplicit, + CancellationToken cancellationToken); +} + +internal static class IDiagnosticAnalyzerServiceExtensions +{ + /// + /// Return up to date diagnostics for the given for the given . + /// + /// This can be expensive since it is force analyzing diagnostics if it doesn't have up-to-date one yet. + /// + /// + public static Task> GetDiagnosticsForSpanAsync(this IDiagnosticAnalyzerService service, + TextDocument document, TextSpan? range, CancellationToken cancellationToken) + => service.GetDiagnosticsForSpanAsync(document, range, DiagnosticKind.All, includeSuppressedDiagnostics: false, cancellationToken); + + /// + /// Return up to date diagnostics of the given for the given + /// for the given . + /// + /// This can be expensive since it is force analyzing diagnostics if it doesn't have up-to-date one yet. + /// + /// + public static Task> GetDiagnosticsForSpanAsync(this IDiagnosticAnalyzerService service, + TextDocument document, TextSpan? range, DiagnosticKind diagnosticKind, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) + => service.GetDiagnosticsForSpanAsync(document, range, + diagnosticId: null, includeSuppressedDiagnostics, + priorityProvider: new DefaultCodeActionRequestPriorityProvider(), + addOperationScope: null, diagnosticKind, isExplicit: false, cancellationToken); - internal static class IDiagnosticAnalyzerServiceExtensions + /// + /// Return up to date diagnostics for the given and parameters for the given . + /// + /// This can be expensive since it is force analyzing diagnostics if it doesn't have up-to-date one yet. If + /// is not null, it gets diagnostics only for this given value. + /// + /// + public static Task> GetDiagnosticsForSpanAsync(this IDiagnosticAnalyzerService service, + TextDocument document, TextSpan? range, string? diagnosticId, + bool includeSuppressedDiagnostics, + ICodeActionRequestPriorityProvider priorityProvider, + Func? addOperationScope, + DiagnosticKind diagnosticKind, + bool isExplicit, + CancellationToken cancellationToken) { - /// - /// Return up to date diagnostics for the given for the given . - /// - /// This can be expensive since it is force analyzing diagnostics if it doesn't have up-to-date one yet. - /// - /// - public static Task> GetDiagnosticsForSpanAsync(this IDiagnosticAnalyzerService service, - TextDocument document, TextSpan? range, CancellationToken cancellationToken) - => service.GetDiagnosticsForSpanAsync(document, range, DiagnosticKind.All, includeSuppressedDiagnostics: false, cancellationToken); - - /// - /// Return up to date diagnostics of the given for the given - /// for the given . - /// - /// This can be expensive since it is force analyzing diagnostics if it doesn't have up-to-date one yet. - /// - /// - public static Task> GetDiagnosticsForSpanAsync(this IDiagnosticAnalyzerService service, - TextDocument document, TextSpan? range, DiagnosticKind diagnosticKind, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) - => service.GetDiagnosticsForSpanAsync(document, range, - diagnosticId: null, includeSuppressedDiagnostics, - priorityProvider: new DefaultCodeActionRequestPriorityProvider(), - addOperationScope: null, diagnosticKind, isExplicit: false, cancellationToken); - - /// - /// Return up to date diagnostics for the given and parameters for the given . - /// - /// This can be expensive since it is force analyzing diagnostics if it doesn't have up-to-date one yet. If - /// is not null, it gets diagnostics only for this given value. - /// - /// - public static Task> GetDiagnosticsForSpanAsync(this IDiagnosticAnalyzerService service, - TextDocument document, TextSpan? range, string? diagnosticId, - bool includeSuppressedDiagnostics, - ICodeActionRequestPriorityProvider priorityProvider, - Func? addOperationScope, - DiagnosticKind diagnosticKind, - bool isExplicit, - CancellationToken cancellationToken) - { - Func? shouldIncludeDiagnostic = diagnosticId != null ? id => id == diagnosticId : null; - return service.GetDiagnosticsForSpanAsync(document, range, shouldIncludeDiagnostic, - includeCompilerDiagnostics: true, includeSuppressedDiagnostics, priorityProvider, - addOperationScope, diagnosticKind, isExplicit, cancellationToken); - } + Func? shouldIncludeDiagnostic = diagnosticId != null ? id => id == diagnosticId : null; + return service.GetDiagnosticsForSpanAsync(document, range, shouldIncludeDiagnostic, + includeCompilerDiagnostics: true, includeSuppressedDiagnostics, priorityProvider, + addOperationScope, diagnosticKind, isExplicit, cancellationToken); } } diff --git a/src/Features/Core/Portable/Diagnostics/IDiagnosticUpdateSource.cs b/src/Features/Core/Portable/Diagnostics/IDiagnosticUpdateSource.cs index f2478f52dad85..9fa64d6182530 100644 --- a/src/Features/Core/Portable/Diagnostics/IDiagnosticUpdateSource.cs +++ b/src/Features/Core/Portable/Diagnostics/IDiagnosticUpdateSource.cs @@ -7,32 +7,31 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +/// +/// Implement this to participate in diagnostic service framework as one of diagnostic update source +/// +internal interface IDiagnosticUpdateSource { /// - /// Implement this to participate in diagnostic service framework as one of diagnostic update source + /// Raise this when new diagnostics are found /// - internal interface IDiagnosticUpdateSource - { - /// - /// Raise this when new diagnostics are found - /// - event EventHandler> DiagnosticsUpdated; + event EventHandler> DiagnosticsUpdated; - /// - /// Raise this when all diagnostics reported from this update source has cleared - /// - event EventHandler DiagnosticsCleared; + /// + /// Raise this when all diagnostics reported from this update source has cleared + /// + event EventHandler DiagnosticsCleared; - /// - /// Return if the source supports API otherwise, return - /// so that the engine can cache data from in memory. - /// - bool SupportGetDiagnostics { get; } + /// + /// Return if the source supports API otherwise, return + /// so that the engine can cache data from in memory. + /// + bool SupportGetDiagnostics { get; } - /// - /// Get diagnostics stored in the source. - /// - ValueTask> GetDiagnosticsAsync(Workspace workspace, ProjectId? projectId, DocumentId? documentId, object? id, bool includeSuppressedDiagnostics, CancellationToken cancellationToken); - } + /// + /// Get diagnostics stored in the source. + /// + ValueTask> GetDiagnosticsAsync(Workspace workspace, ProjectId? projectId, DocumentId? documentId, object? id, bool includeSuppressedDiagnostics, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/Diagnostics/IDiagnosticUpdateSourceRegistrationService.cs b/src/Features/Core/Portable/Diagnostics/IDiagnosticUpdateSourceRegistrationService.cs index ba3327bfc9369..8d8d929ed5d82 100644 --- a/src/Features/Core/Portable/Diagnostics/IDiagnosticUpdateSourceRegistrationService.cs +++ b/src/Features/Core/Portable/Diagnostics/IDiagnosticUpdateSourceRegistrationService.cs @@ -4,18 +4,17 @@ #nullable disable -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +/// +/// A service that let people to register new IDiagnosticUpdateSource +/// +internal interface IDiagnosticUpdateSourceRegistrationService { /// - /// A service that let people to register new IDiagnosticUpdateSource + /// Register new IDiagnosticUpdateSource + /// + /// Currently, it doesn't support unregister since our event is asynchronous and unregistering source that deal with asynchronous event is not straight forward. /// - internal interface IDiagnosticUpdateSourceRegistrationService - { - /// - /// Register new IDiagnosticUpdateSource - /// - /// Currently, it doesn't support unregister since our event is asynchronous and unregistering source that deal with asynchronous event is not straight forward. - /// - void Register(IDiagnosticUpdateSource source); - } + void Register(IDiagnosticUpdateSource source); } diff --git a/src/Features/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs index 80d108d92e7b3..b7846566646a3 100644 --- a/src/Features/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs +++ b/src/Features/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs @@ -13,26 +13,25 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Remote; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal interface IRemoteDiagnosticAnalyzerService { - internal interface IRemoteDiagnosticAnalyzerService - { - ValueTask CalculateDiagnosticsAsync(Checksum solutionChecksum, DiagnosticArguments arguments, CancellationToken cancellationToken); - ValueTask> GetSourceGeneratorDiagnosticsAsync(Checksum solutionChecksum, ProjectId projectId, CancellationToken cancellationToken); - ValueTask ReportAnalyzerPerformanceAsync(ImmutableArray snapshot, int unitCount, bool forSpanAnalysis, CancellationToken cancellationToken); - ValueTask StartSolutionCrawlerAsync(CancellationToken cancellationToken); - } + ValueTask CalculateDiagnosticsAsync(Checksum solutionChecksum, DiagnosticArguments arguments, CancellationToken cancellationToken); + ValueTask> GetSourceGeneratorDiagnosticsAsync(Checksum solutionChecksum, ProjectId projectId, CancellationToken cancellationToken); + ValueTask ReportAnalyzerPerformanceAsync(ImmutableArray snapshot, int unitCount, bool forSpanAnalysis, CancellationToken cancellationToken); + ValueTask StartSolutionCrawlerAsync(CancellationToken cancellationToken); +} - [DataContract] - internal readonly struct AnalyzerPerformanceInfo(string analyzerId, bool builtIn, TimeSpan timeSpan) - { - [DataMember(Order = 0)] - public readonly string AnalyzerId = analyzerId; +[DataContract] +internal readonly struct AnalyzerPerformanceInfo(string analyzerId, bool builtIn, TimeSpan timeSpan) +{ + [DataMember(Order = 0)] + public readonly string AnalyzerId = analyzerId; - [DataMember(Order = 1)] - public readonly bool BuiltIn = builtIn; + [DataMember(Order = 1)] + public readonly bool BuiltIn = builtIn; - [DataMember(Order = 2)] - public readonly TimeSpan TimeSpan = timeSpan; - } + [DataMember(Order = 2)] + public readonly TimeSpan TimeSpan = timeSpan; } diff --git a/src/Features/Core/Portable/Diagnostics/ISupportLiveUpdate.cs b/src/Features/Core/Portable/Diagnostics/ISupportLiveUpdate.cs index ae3229506f2fd..3f10d2e011a39 100644 --- a/src/Features/Core/Portable/Diagnostics/ISupportLiveUpdate.cs +++ b/src/Features/Core/Portable/Diagnostics/ISupportLiveUpdate.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +/// +/// Marker interface to indicate whether given diagnostic args are from live analysis. +/// +internal interface ISupportLiveUpdate { - /// - /// Marker interface to indicate whether given diagnostic args are from live analysis. - /// - internal interface ISupportLiveUpdate - { - } } diff --git a/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnostics.cs b/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnostics.cs index a138b8b294353..90d06de90dd45 100644 --- a/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnostics.cs +++ b/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnostics.cs @@ -2,14 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.LanguageServer +namespace Microsoft.CodeAnalysis.LanguageServer; + +/// +/// Marker interface for individual Roslyn languages to expose what diagnostics IDs they have are 'build only'. This +/// affects how the LSP client will handle and dedupe related diagnostics produced by Roslyn for live diagnostics +/// against the diagnostics produced by CPS when a build is performed. +/// +internal interface ILspBuildOnlyDiagnostics { - /// - /// Marker interface for individual Roslyn languages to expose what diagnostics IDs they have are 'build only'. This - /// affects how the LSP client will handle and dedupe related diagnostics produced by Roslyn for live diagnostics - /// against the diagnostics produced by CPS when a build is performed. - /// - internal interface ILspBuildOnlyDiagnostics - { - } } diff --git a/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnosticsMetadata.cs b/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnosticsMetadata.cs index d9e5226e3886b..9b19550b43baa 100644 --- a/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnosticsMetadata.cs +++ b/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnosticsMetadata.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.LanguageServer +namespace Microsoft.CodeAnalysis.LanguageServer; + +/// +internal interface ILspBuildOnlyDiagnosticsMetadata { - /// - internal interface ILspBuildOnlyDiagnosticsMetadata - { - string LanguageName { get; } - string[] BuildOnlyDiagnostics { get; } - } + string LanguageName { get; } + string[] BuildOnlyDiagnostics { get; } } diff --git a/src/Features/Core/Portable/Diagnostics/LanguageServer/LspBuildOnlyDiagnosticsAttribute.cs b/src/Features/Core/Portable/Diagnostics/LanguageServer/LspBuildOnlyDiagnosticsAttribute.cs index ddbd266652e6b..4f0960c49617b 100644 --- a/src/Features/Core/Portable/Diagnostics/LanguageServer/LspBuildOnlyDiagnosticsAttribute.cs +++ b/src/Features/Core/Portable/Diagnostics/LanguageServer/LspBuildOnlyDiagnosticsAttribute.cs @@ -5,14 +5,13 @@ using System; using System.Composition; -namespace Microsoft.CodeAnalysis.LanguageServer +namespace Microsoft.CodeAnalysis.LanguageServer; + +/// +[MetadataAttribute] +[AttributeUsage(AttributeTargets.Class)] +internal class LspBuildOnlyDiagnosticsAttribute(string languageName, params string[] buildOnlyDiagnostics) : ExportAttribute(typeof(ILspBuildOnlyDiagnostics)), ILspBuildOnlyDiagnosticsMetadata { - /// - [MetadataAttribute] - [AttributeUsage(AttributeTargets.Class)] - internal class LspBuildOnlyDiagnosticsAttribute(string languageName, params string[] buildOnlyDiagnostics) : ExportAttribute(typeof(ILspBuildOnlyDiagnostics)), ILspBuildOnlyDiagnosticsMetadata - { - public string LanguageName { get; } = languageName; - public string[] BuildOnlyDiagnostics { get; } = buildOnlyDiagnostics; - } + public string LanguageName { get; } = languageName; + public string[] BuildOnlyDiagnostics { get; } = buildOnlyDiagnostics; } diff --git a/src/Features/Core/Portable/Diagnostics/LiveDiagnosticUpdateArgsId.cs b/src/Features/Core/Portable/Diagnostics/LiveDiagnosticUpdateArgsId.cs index f54cb752850da..3458e3bcf33c0 100644 --- a/src/Features/Core/Portable/Diagnostics/LiveDiagnosticUpdateArgsId.cs +++ b/src/Features/Core/Portable/Diagnostics/LiveDiagnosticUpdateArgsId.cs @@ -5,40 +5,39 @@ using System; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal class LiveDiagnosticUpdateArgsId : AnalyzerUpdateArgsId { - internal class LiveDiagnosticUpdateArgsId : AnalyzerUpdateArgsId - { - private string? _buildTool; + private string? _buildTool; - public readonly object ProjectOrDocumentId; - public readonly AnalysisKind Kind; + public readonly object ProjectOrDocumentId; + public readonly AnalysisKind Kind; - public LiveDiagnosticUpdateArgsId(DiagnosticAnalyzer analyzer, object projectOrDocumentId, AnalysisKind kind) - : base(analyzer) - { - Contract.ThrowIfNull(projectOrDocumentId); + public LiveDiagnosticUpdateArgsId(DiagnosticAnalyzer analyzer, object projectOrDocumentId, AnalysisKind kind) + : base(analyzer) + { + Contract.ThrowIfNull(projectOrDocumentId); - ProjectOrDocumentId = projectOrDocumentId; - Kind = kind; - } + ProjectOrDocumentId = projectOrDocumentId; + Kind = kind; + } - public override string BuildTool => _buildTool ??= ComputeBuildTool(); + public override string BuildTool => _buildTool ??= ComputeBuildTool(); - private string ComputeBuildTool() - => Analyzer.IsBuiltInAnalyzer() ? PredefinedBuildTools.Live : Analyzer.GetAnalyzerAssemblyName(); + private string ComputeBuildTool() + => Analyzer.IsBuiltInAnalyzer() ? PredefinedBuildTools.Live : Analyzer.GetAnalyzerAssemblyName(); - public override bool Equals(object? obj) + public override bool Equals(object? obj) + { + if (obj is not LiveDiagnosticUpdateArgsId other) { - if (obj is not LiveDiagnosticUpdateArgsId other) - { - return false; - } - - return Kind == other.Kind && Equals(ProjectOrDocumentId, other.ProjectOrDocumentId) && base.Equals(obj); + return false; } - public override int GetHashCode() - => Hash.Combine(ProjectOrDocumentId, Hash.Combine((int)Kind, base.GetHashCode())); + return Kind == other.Kind && Equals(ProjectOrDocumentId, other.ProjectOrDocumentId) && base.Equals(obj); } + + public override int GetHashCode() + => Hash.Combine(ProjectOrDocumentId, Hash.Combine((int)Kind, base.GetHashCode())); } diff --git a/src/Features/Core/Portable/Diagnostics/Log/DiagnosticLogger.cs b/src/Features/Core/Portable/Diagnostics/Log/DiagnosticLogger.cs index 6a376fff8592b..31e0854b54f5e 100644 --- a/src/Features/Core/Portable/Diagnostics/Log/DiagnosticLogger.cs +++ b/src/Features/Core/Portable/Diagnostics/Log/DiagnosticLogger.cs @@ -6,29 +6,28 @@ using Microsoft.CodeAnalysis.Internal.Log; -namespace Microsoft.CodeAnalysis.Diagnostics.Log +namespace Microsoft.CodeAnalysis.Diagnostics.Log; + +internal static class DiagnosticLogger { - internal static class DiagnosticLogger - { - private const string From = nameof(From); - private const string Id = nameof(Id); - private const string HasDescription = nameof(HasDescription); - private const string Uri = nameof(Uri); + private const string From = nameof(From); + private const string Id = nameof(Id); + private const string HasDescription = nameof(HasDescription); + private const string Uri = nameof(Uri); - public static void LogHyperlink( - string from, - string id, - bool description, - bool telemetry, - string uri) + public static void LogHyperlink( + string from, + string id, + bool description, + bool telemetry, + string uri) + { + Logger.Log(FunctionId.Diagnostics_HyperLink, KeyValueLogMessage.Create(m => { - Logger.Log(FunctionId.Diagnostics_HyperLink, KeyValueLogMessage.Create(m => - { - m[From] = from; - m[Id] = telemetry ? id : id.GetHashCode().ToString(); - m[HasDescription] = description; - m[Uri] = telemetry ? uri : uri.GetHashCode().ToString(); - })); - } + m[From] = from; + m[Id] = telemetry ? id : id.GetHashCode().ToString(); + m[HasDescription] = description; + m[Uri] = telemetry ? uri : uri.GetHashCode().ToString(); + })); } } diff --git a/src/Features/Core/Portable/Diagnostics/PredefinedBuildTools.cs b/src/Features/Core/Portable/Diagnostics/PredefinedBuildTools.cs index 3f0b6d7a26132..93de528e384e6 100644 --- a/src/Features/Core/Portable/Diagnostics/PredefinedBuildTools.cs +++ b/src/Features/Core/Portable/Diagnostics/PredefinedBuildTools.cs @@ -4,11 +4,10 @@ #nullable disable -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal static class PredefinedBuildTools { - internal static class PredefinedBuildTools - { - public static readonly string Build = FeaturesResources.Compiler2; - public static readonly string Live = FeaturesResources.Live; - } + public static readonly string Build = FeaturesResources.Compiler2; + public static readonly string Live = FeaturesResources.Live; } diff --git a/src/Features/Core/Portable/Diagnostics/PredefinedDiagnosticProviderNames.cs b/src/Features/Core/Portable/Diagnostics/PredefinedDiagnosticProviderNames.cs index e4321e349ef7b..52f17c50bec1e 100644 --- a/src/Features/Core/Portable/Diagnostics/PredefinedDiagnosticProviderNames.cs +++ b/src/Features/Core/Portable/Diagnostics/PredefinedDiagnosticProviderNames.cs @@ -4,31 +4,30 @@ #nullable disable -namespace Microsoft.CodeAnalysis.Diagnostics +namespace Microsoft.CodeAnalysis.Diagnostics; + +internal static class PredefinedDiagnosticProviderNames { - internal static class PredefinedDiagnosticProviderNames - { #if false - public const string AddMissingReference = "Add Missing Reference"; - public const string AddUsingOrImport = "Add Using or Import"; - public const string FullyQualify = "Fully Qualify"; - public const string FixIncorrectExitContinue = "Fix Incorrect Exit Continue"; - public const string GenerateConstructor = "Generate Constructor"; - public const string GenerateEndConstruct = "Generate End Construct"; - public const string GenerateEnumMember = "Generate Enum Member"; - public const string GenerateEvent = "Generate Event"; - public const string GenerateVariable = "Generate Variable"; - public const string GenerateMethod = "Generate Method"; - public const string GenerateType = "Generate Type"; - public const string ImplementAbstractClass = "Implement Abstract Class"; - public const string MoveToTopOfFile = "Move To Top Of File"; + public const string AddMissingReference = "Add Missing Reference"; + public const string AddUsingOrImport = "Add Using or Import"; + public const string FullyQualify = "Fully Qualify"; + public const string FixIncorrectExitContinue = "Fix Incorrect Exit Continue"; + public const string GenerateConstructor = "Generate Constructor"; + public const string GenerateEndConstruct = "Generate End Construct"; + public const string GenerateEnumMember = "Generate Enum Member"; + public const string GenerateEvent = "Generate Event"; + public const string GenerateVariable = "Generate Variable"; + public const string GenerateMethod = "Generate Method"; + public const string GenerateType = "Generate Type"; + public const string ImplementAbstractClass = "Implement Abstract Class"; + public const string MoveToTopOfFile = "Move To Top Of File"; #endif - public const string AddUsingOrImport = "Add Using or Import"; - public const string ImplementInterface = "Implement Interface"; - public const string RemoveUnnecessaryCast = "Remove Unnecessary Casts"; - public const string RemoveUnnecessaryImports = "Remove Unnecessary Usings or Imports"; - public const string RenameTracking = "Rename Tracking"; - public const string SimplifyNames = "Simplify Names"; - public const string SpellCheck = "Spell Check"; - } + public const string AddUsingOrImport = "Add Using or Import"; + public const string ImplementInterface = "Implement Interface"; + public const string RemoveUnnecessaryCast = "Remove Unnecessary Casts"; + public const string RemoveUnnecessaryImports = "Remove Unnecessary Usings or Imports"; + public const string RenameTracking = "Rename Tracking"; + public const string SimplifyNames = "Simplify Names"; + public const string SpellCheck = "Spell Check"; } diff --git a/src/Features/Core/Portable/DocumentHighlighting/AbstractDocumentHighlightsService.cs b/src/Features/Core/Portable/DocumentHighlighting/AbstractDocumentHighlightsService.cs index 72dbf8668957a..f25de00436c7c 100644 --- a/src/Features/Core/Portable/DocumentHighlighting/AbstractDocumentHighlightsService.cs +++ b/src/Features/Core/Portable/DocumentHighlighting/AbstractDocumentHighlightsService.cs @@ -18,320 +18,319 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.DocumentHighlighting +namespace Microsoft.CodeAnalysis.DocumentHighlighting; + +internal abstract partial class AbstractDocumentHighlightsService : + AbstractEmbeddedLanguageFeatureService, + IDocumentHighlightsService { - internal abstract partial class AbstractDocumentHighlightsService : - AbstractEmbeddedLanguageFeatureService, - IDocumentHighlightsService + protected AbstractDocumentHighlightsService( + string languageName, + EmbeddedLanguageInfo info, + ISyntaxKinds syntaxKinds, + IEnumerable> allServices) + : base(languageName, info, syntaxKinds, allServices) { - protected AbstractDocumentHighlightsService( - string languageName, - EmbeddedLanguageInfo info, - ISyntaxKinds syntaxKinds, - IEnumerable> allServices) - : base(languageName, info, syntaxKinds, allServices) - { - } + } - public async Task> GetDocumentHighlightsAsync( - Document document, int position, IImmutableSet documentsToSearch, HighlightingOptions options, CancellationToken cancellationToken) - { - var solution = document.Project.Solution; + public async Task> GetDocumentHighlightsAsync( + Document document, int position, IImmutableSet documentsToSearch, HighlightingOptions options, CancellationToken cancellationToken) + { + var solution = document.Project.Solution; - var client = await RemoteHostClient.TryGetClientAsync(document.Project, cancellationToken).ConfigureAwait(false); - if (client != null) + var client = await RemoteHostClient.TryGetClientAsync(document.Project, cancellationToken).ConfigureAwait(false); + if (client != null) + { + // Call the project overload. We don't need the full solution synchronized over to the OOP + // in order to highlight values in this document. + var result = await client.TryInvokeAsync>( + document.Project, + (service, solutionInfo, cancellationToken) => service.GetDocumentHighlightsAsync(solutionInfo, document.Id, position, documentsToSearch.SelectAsArray(d => d.Id), options, cancellationToken), + cancellationToken).ConfigureAwait(false); + + if (!result.HasValue) { - // Call the project overload. We don't need the full solution synchronized over to the OOP - // in order to highlight values in this document. - var result = await client.TryInvokeAsync>( - document.Project, - (service, solutionInfo, cancellationToken) => service.GetDocumentHighlightsAsync(solutionInfo, document.Id, position, documentsToSearch.SelectAsArray(d => d.Id), options, cancellationToken), - cancellationToken).ConfigureAwait(false); - - if (!result.HasValue) - { - return []; - } - - return await result.Value.SelectAsArrayAsync(h => h.RehydrateAsync(solution)).ConfigureAwait(false); + return []; } - return await GetDocumentHighlightsInCurrentProcessAsync( - document, position, documentsToSearch, options, cancellationToken).ConfigureAwait(false); + return await result.Value.SelectAsArrayAsync(h => h.RehydrateAsync(solution)).ConfigureAwait(false); } - private async Task> GetDocumentHighlightsInCurrentProcessAsync( - Document document, int position, IImmutableSet documentsToSearch, HighlightingOptions options, CancellationToken cancellationToken) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var result = TryGetEmbeddedLanguageHighlights(document, semanticModel, position, options, cancellationToken); - if (!result.IsDefaultOrEmpty) - return result; + return await GetDocumentHighlightsInCurrentProcessAsync( + document, position, documentsToSearch, options, cancellationToken).ConfigureAwait(false); + } - var solution = document.Project.Solution; + private async Task> GetDocumentHighlightsInCurrentProcessAsync( + Document document, int position, IImmutableSet documentsToSearch, HighlightingOptions options, CancellationToken cancellationToken) + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var result = TryGetEmbeddedLanguageHighlights(document, semanticModel, position, options, cancellationToken); + if (!result.IsDefaultOrEmpty) + return result; - var symbol = await SymbolFinder.FindSymbolAtPositionAsync( - semanticModel, position, solution.Services, cancellationToken).ConfigureAwait(false); - if (symbol == null) - return []; + var solution = document.Project.Solution; - // Get unique tags for referenced symbols - var tags = await GetTagsForReferencedSymbolAsync( - symbol, document, documentsToSearch, cancellationToken).ConfigureAwait(false); + var symbol = await SymbolFinder.FindSymbolAtPositionAsync( + semanticModel, position, solution.Services, cancellationToken).ConfigureAwait(false); + if (symbol == null) + return []; - // Only accept these highlights if at least one of them actually intersected with the - // position the caller was asking for. For example, if the user had `$$new X();` then - // SymbolFinder will consider that the symbol `X`. However, the doc highlights won't include - // the `new` part, so it's not appropriate for us to highlight `X` in that case. - if (!tags.Any(static (t, position) => t.HighlightSpans.Any(static (hs, position) => hs.TextSpan.IntersectsWith(position), position), position)) - return []; + // Get unique tags for referenced symbols + var tags = await GetTagsForReferencedSymbolAsync( + symbol, document, documentsToSearch, cancellationToken).ConfigureAwait(false); - return tags; - } + // Only accept these highlights if at least one of them actually intersected with the + // position the caller was asking for. For example, if the user had `$$new X();` then + // SymbolFinder will consider that the symbol `X`. However, the doc highlights won't include + // the `new` part, so it's not appropriate for us to highlight `X` in that case. + if (!tags.Any(static (t, position) => t.HighlightSpans.Any(static (hs, position) => hs.TextSpan.IntersectsWith(position), position), position)) + return []; - private ImmutableArray TryGetEmbeddedLanguageHighlights( - Document document, SemanticModel semanticModel, int position, HighlightingOptions options, CancellationToken cancellationToken) - { - var root = semanticModel.SyntaxTree.GetRoot(cancellationToken); - var token = root.FindToken(position); - var embeddedHighlightsServices = this.GetServices(semanticModel, token, cancellationToken); - foreach (var service in embeddedHighlightsServices) - { - var result = service.Value.GetDocumentHighlights( - document, semanticModel, token, position, options, cancellationToken); - if (!result.IsDefaultOrEmpty) - return result; - } + return tags; + } - return default; + private ImmutableArray TryGetEmbeddedLanguageHighlights( + Document document, SemanticModel semanticModel, int position, HighlightingOptions options, CancellationToken cancellationToken) + { + var root = semanticModel.SyntaxTree.GetRoot(cancellationToken); + var token = root.FindToken(position); + var embeddedHighlightsServices = this.GetServices(semanticModel, token, cancellationToken); + foreach (var service in embeddedHighlightsServices) + { + var result = service.Value.GetDocumentHighlights( + document, semanticModel, token, position, options, cancellationToken); + if (!result.IsDefaultOrEmpty) + return result; } - private async Task> GetTagsForReferencedSymbolAsync( - ISymbol symbol, - Document document, - IImmutableSet documentsToSearch, - CancellationToken cancellationToken) - { - Contract.ThrowIfNull(symbol); - if (ShouldConsiderSymbol(symbol)) - { - var progress = new StreamingProgressCollector(); - - // We're running in the background. So set us as 'Explicit = false' to avoid running in parallel and - // using too many resources. - var options = FindReferencesSearchOptions.GetFeatureOptionsForStartingSymbol(symbol) with { Explicit = false }; - await SymbolFinder.FindReferencesInDocumentsInCurrentProcessAsync( - symbol, document.Project.Solution, progress, - documentsToSearch, options, cancellationToken).ConfigureAwait(false); - - return await FilterAndCreateSpansAsync( - progress.GetReferencedSymbols(), document, documentsToSearch, - symbol, options, cancellationToken).ConfigureAwait(false); - } + return default; + } - return []; + private async Task> GetTagsForReferencedSymbolAsync( + ISymbol symbol, + Document document, + IImmutableSet documentsToSearch, + CancellationToken cancellationToken) + { + Contract.ThrowIfNull(symbol); + if (ShouldConsiderSymbol(symbol)) + { + var progress = new StreamingProgressCollector(); + + // We're running in the background. So set us as 'Explicit = false' to avoid running in parallel and + // using too many resources. + var options = FindReferencesSearchOptions.GetFeatureOptionsForStartingSymbol(symbol) with { Explicit = false }; + await SymbolFinder.FindReferencesInDocumentsInCurrentProcessAsync( + symbol, document.Project.Solution, progress, + documentsToSearch, options, cancellationToken).ConfigureAwait(false); + + return await FilterAndCreateSpansAsync( + progress.GetReferencedSymbols(), document, documentsToSearch, + symbol, options, cancellationToken).ConfigureAwait(false); } - private static bool ShouldConsiderSymbol(ISymbol symbol) + return []; + } + + private static bool ShouldConsiderSymbol(ISymbol symbol) + { + switch (symbol.Kind) { - switch (symbol.Kind) - { - case SymbolKind.Method: - switch (((IMethodSymbol)symbol).MethodKind) - { - case MethodKind.AnonymousFunction: - case MethodKind.PropertyGet: - case MethodKind.PropertySet: - case MethodKind.EventAdd: - case MethodKind.EventRaise: - case MethodKind.EventRemove: - return false; - - default: - return true; - } + case SymbolKind.Method: + switch (((IMethodSymbol)symbol).MethodKind) + { + case MethodKind.AnonymousFunction: + case MethodKind.PropertyGet: + case MethodKind.PropertySet: + case MethodKind.EventAdd: + case MethodKind.EventRaise: + case MethodKind.EventRemove: + return false; + + default: + return true; + } - default: - return true; - } + default: + return true; } + } - private async Task> FilterAndCreateSpansAsync( - ImmutableArray references, Document startingDocument, - IImmutableSet documentsToSearch, ISymbol symbol, - FindReferencesSearchOptions options, CancellationToken cancellationToken) - { - var solution = startingDocument.Project.Solution; - references = references.FilterToItemsToShow(options); - references = references.FilterNonMatchingMethodNames(solution, symbol); - references = references.FilterToAliasMatches(symbol as IAliasSymbol); + private async Task> FilterAndCreateSpansAsync( + ImmutableArray references, Document startingDocument, + IImmutableSet documentsToSearch, ISymbol symbol, + FindReferencesSearchOptions options, CancellationToken cancellationToken) + { + var solution = startingDocument.Project.Solution; + references = references.FilterToItemsToShow(options); + references = references.FilterNonMatchingMethodNames(solution, symbol); + references = references.FilterToAliasMatches(symbol as IAliasSymbol); - if (symbol.IsConstructor()) - { - references = references.WhereAsArray(r => r.Definition.OriginalDefinition.Equals(symbol.OriginalDefinition)); - } + if (symbol.IsConstructor()) + { + references = references.WhereAsArray(r => r.Definition.OriginalDefinition.Equals(symbol.OriginalDefinition)); + } - using var _ = ArrayBuilder.GetInstance(out var additionalReferences); + using var _ = ArrayBuilder.GetInstance(out var additionalReferences); - foreach (var currentDocument in documentsToSearch) + foreach (var currentDocument in documentsToSearch) + { + // 'documentsToSearch' may contain documents from languages other than our own + // (for example cshtml files when we're searching the cs document). Since we're + // delegating to a virtual method for this language type, we have to make sure + // we only process the document if it's also our language. + if (currentDocument.Project.Language == startingDocument.Project.Language) { - // 'documentsToSearch' may contain documents from languages other than our own - // (for example cshtml files when we're searching the cs document). Since we're - // delegating to a virtual method for this language type, we have to make sure - // we only process the document if it's also our language. - if (currentDocument.Project.Language == startingDocument.Project.Language) - { - additionalReferences.AddRange(await GetAdditionalReferencesAsync(currentDocument, symbol, cancellationToken).ConfigureAwait(false)); - } + additionalReferences.AddRange(await GetAdditionalReferencesAsync(currentDocument, symbol, cancellationToken).ConfigureAwait(false)); } - - return await CreateSpansAsync( - solution, symbol, references, additionalReferences, - documentsToSearch, cancellationToken).ConfigureAwait(false); } - protected virtual Task> GetAdditionalReferencesAsync( - Document document, ISymbol symbol, CancellationToken cancellationToken) - { - return SpecializedTasks.EmptyImmutableArray(); - } + return await CreateSpansAsync( + solution, symbol, references, additionalReferences, + documentsToSearch, cancellationToken).ConfigureAwait(false); + } - private static async Task> CreateSpansAsync( - Solution solution, - ISymbol symbol, - IEnumerable references, - ArrayBuilder additionalReferences, - IImmutableSet documentToSearch, - CancellationToken cancellationToken) + protected virtual Task> GetAdditionalReferencesAsync( + Document document, ISymbol symbol, CancellationToken cancellationToken) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + private static async Task> CreateSpansAsync( + Solution solution, + ISymbol symbol, + IEnumerable references, + ArrayBuilder additionalReferences, + IImmutableSet documentToSearch, + CancellationToken cancellationToken) + { + var spanSet = new HashSet(); + var tagMap = new MultiDictionary(); + var addAllDefinitions = true; + + // Add definitions + // Filter out definitions that cannot be highlighted. e.g: alias symbols defined via project property pages. + if (symbol.Kind == SymbolKind.Alias && + symbol.Locations.Length > 0) { - var spanSet = new HashSet(); - var tagMap = new MultiDictionary(); - var addAllDefinitions = true; - - // Add definitions - // Filter out definitions that cannot be highlighted. e.g: alias symbols defined via project property pages. - if (symbol.Kind == SymbolKind.Alias && - symbol.Locations.Length > 0) - { - addAllDefinitions = false; + addAllDefinitions = false; - if (symbol.Locations.First().IsInSource) - { - // For alias symbol we want to get the tag only for the alias definition, not the target symbol's definition. - await AddLocationSpanAsync(symbol.Locations.First(), solution, spanSet, tagMap, HighlightSpanKind.Definition, cancellationToken).ConfigureAwait(false); - } + if (symbol.Locations.First().IsInSource) + { + // For alias symbol we want to get the tag only for the alias definition, not the target symbol's definition. + await AddLocationSpanAsync(symbol.Locations.First(), solution, spanSet, tagMap, HighlightSpanKind.Definition, cancellationToken).ConfigureAwait(false); } + } - // Add references and definitions - foreach (var reference in references) + // Add references and definitions + foreach (var reference in references) + { + if (addAllDefinitions && ShouldIncludeDefinition(reference.Definition)) { - if (addAllDefinitions && ShouldIncludeDefinition(reference.Definition)) + foreach (var location in reference.Definition.Locations) { - foreach (var location in reference.Definition.Locations) + if (location.IsInSource) { - if (location.IsInSource) + var document = solution.GetDocument(location.SourceTree); + + // GetDocument will return null for locations in #load'ed trees. + // TODO: Remove this check and add logic to fetch the #load'ed tree's + // Document once https://github.com/dotnet/roslyn/issues/5260 is fixed. + if (document == null) { - var document = solution.GetDocument(location.SourceTree); - - // GetDocument will return null for locations in #load'ed trees. - // TODO: Remove this check and add logic to fetch the #load'ed tree's - // Document once https://github.com/dotnet/roslyn/issues/5260 is fixed. - if (document == null) - { - Debug.Assert(solution.WorkspaceKind is WorkspaceKind.Interactive or WorkspaceKind.MiscellaneousFiles); - continue; - } - - if (documentToSearch.Contains(document)) - { - await AddLocationSpanAsync(location, solution, spanSet, tagMap, HighlightSpanKind.Definition, cancellationToken).ConfigureAwait(false); - } + Debug.Assert(solution.WorkspaceKind is WorkspaceKind.Interactive or WorkspaceKind.MiscellaneousFiles); + continue; } - } - } - foreach (var referenceLocation in reference.Locations) - { - var referenceKind = referenceLocation.IsWrittenTo ? HighlightSpanKind.WrittenReference : HighlightSpanKind.Reference; - await AddLocationSpanAsync(referenceLocation.Location, solution, spanSet, tagMap, referenceKind, cancellationToken).ConfigureAwait(false); + if (documentToSearch.Contains(document)) + { + await AddLocationSpanAsync(location, solution, spanSet, tagMap, HighlightSpanKind.Definition, cancellationToken).ConfigureAwait(false); + } + } } } - // Add additional references - foreach (var location in additionalReferences) - { - await AddLocationSpanAsync(location, solution, spanSet, tagMap, HighlightSpanKind.Reference, cancellationToken).ConfigureAwait(false); - } - - using var _1 = ArrayBuilder.GetInstance(tagMap.Count, out var list); - foreach (var kvp in tagMap) + foreach (var referenceLocation in reference.Locations) { - list.Add(new DocumentHighlights(kvp.Key, [.. kvp.Value])); + var referenceKind = referenceLocation.IsWrittenTo ? HighlightSpanKind.WrittenReference : HighlightSpanKind.Reference; + await AddLocationSpanAsync(referenceLocation.Location, solution, spanSet, tagMap, referenceKind, cancellationToken).ConfigureAwait(false); } + } - return list.ToImmutableAndClear(); + // Add additional references + foreach (var location in additionalReferences) + { + await AddLocationSpanAsync(location, solution, spanSet, tagMap, HighlightSpanKind.Reference, cancellationToken).ConfigureAwait(false); } - private static bool ShouldIncludeDefinition(ISymbol symbol) + using var _1 = ArrayBuilder.GetInstance(tagMap.Count, out var list); + foreach (var kvp in tagMap) { - switch (symbol.Kind) - { - case SymbolKind.Namespace: - return false; + list.Add(new DocumentHighlights(kvp.Key, [.. kvp.Value])); + } - case SymbolKind.NamedType: - return !((INamedTypeSymbol)symbol).IsScriptClass; - } + return list.ToImmutableAndClear(); + } - return true; + private static bool ShouldIncludeDefinition(ISymbol symbol) + { + switch (symbol.Kind) + { + case SymbolKind.Namespace: + return false; + + case SymbolKind.NamedType: + return !((INamedTypeSymbol)symbol).IsScriptClass; } - private static async Task AddLocationSpanAsync(Location location, Solution solution, HashSet spanSet, MultiDictionary tagList, HighlightSpanKind kind, CancellationToken cancellationToken) + return true; + } + + private static async Task AddLocationSpanAsync(Location location, Solution solution, HashSet spanSet, MultiDictionary tagList, HighlightSpanKind kind, CancellationToken cancellationToken) + { + var span = await GetLocationSpanAsync(solution, location, cancellationToken).ConfigureAwait(false); + if (span != null && !spanSet.Contains(span.Value)) { - var span = await GetLocationSpanAsync(solution, location, cancellationToken).ConfigureAwait(false); - if (span != null && !spanSet.Contains(span.Value)) - { - spanSet.Add(span.Value); - tagList.Add(span.Value.Document, new HighlightSpan(span.Value.SourceSpan, kind)); - } + spanSet.Add(span.Value); + tagList.Add(span.Value.Document, new HighlightSpan(span.Value.SourceSpan, kind)); } + } - private static async Task GetLocationSpanAsync( - Solution solution, Location location, CancellationToken cancellationToken) + private static async Task GetLocationSpanAsync( + Solution solution, Location location, CancellationToken cancellationToken) + { + try { - try + if (location != null && location.IsInSource) { - if (location != null && location.IsInSource) - { - var tree = location.SourceTree; + var tree = location.SourceTree; - var document = solution.GetRequiredDocument(tree); - var syntaxFacts = document.GetLanguageService(); + var document = solution.GetRequiredDocument(tree); + var syntaxFacts = document.GetLanguageService(); - if (syntaxFacts != null) - { - // Specify findInsideTrivia: true to ensure that we search within XML doc comments. - var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(location.SourceSpan.Start, findInsideTrivia: true); + if (syntaxFacts != null) + { + // Specify findInsideTrivia: true to ensure that we search within XML doc comments. + var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(location.SourceSpan.Start, findInsideTrivia: true); - return syntaxFacts.IsGenericName(token.Parent) || syntaxFacts.IsIndexerMemberCref(token.Parent) - ? new DocumentSpan(document, token.Span) - : new DocumentSpan(document, location.SourceSpan); - } + return syntaxFacts.IsGenericName(token.Parent) || syntaxFacts.IsIndexerMemberCref(token.Parent) + ? new DocumentSpan(document, token.Span) + : new DocumentSpan(document, location.SourceSpan); } } - catch (NullReferenceException e) when (FatalError.ReportAndCatch(e)) - { - // We currently are seeing a strange null references crash in this code. We have - // a strong belief that this is recoverable, but we'd like to know why it is - // happening. This exception filter allows us to report the issue and continue - // without damaging the user experience. Once we get more crash reports, we - // can figure out the root cause and address appropriately. This is preferable - // to just using conditionl access operators to be resilient (as we won't actually - // know why this is happening). - } - - return null; } + catch (NullReferenceException e) when (FatalError.ReportAndCatch(e)) + { + // We currently are seeing a strange null references crash in this code. We have + // a strong belief that this is recoverable, but we'd like to know why it is + // happening. This exception filter allows us to report the issue and continue + // without damaging the user experience. Once we get more crash reports, we + // can figure out the root cause and address appropriately. This is preferable + // to just using conditionl access operators to be resilient (as we won't actually + // know why this is happening). + } + + return null; } } diff --git a/src/Features/Core/Portable/DocumentHighlighting/ExportEmbeddedLanguageDocumentHighlighterAttribute.cs b/src/Features/Core/Portable/DocumentHighlighting/ExportEmbeddedLanguageDocumentHighlighterAttribute.cs index 1b913f1e972f5..df279130d13e2 100644 --- a/src/Features/Core/Portable/DocumentHighlighting/ExportEmbeddedLanguageDocumentHighlighterAttribute.cs +++ b/src/Features/Core/Portable/DocumentHighlighting/ExportEmbeddedLanguageDocumentHighlighterAttribute.cs @@ -4,18 +4,17 @@ using Microsoft.CodeAnalysis.EmbeddedLanguages; -namespace Microsoft.CodeAnalysis.DocumentHighlighting +namespace Microsoft.CodeAnalysis.DocumentHighlighting; + +/// +/// Use this attribute to export a . +/// +internal class ExportEmbeddedLanguageDocumentHighlighterAttribute( + string name, string[] languages, bool supportsUnannotatedAPIs, params string[] identifiers) : ExportEmbeddedLanguageFeatureServiceAttribute(typeof(IEmbeddedLanguageDocumentHighlighter), name, languages, supportsUnannotatedAPIs, identifiers) { - /// - /// Use this attribute to export a . - /// - internal class ExportEmbeddedLanguageDocumentHighlighterAttribute( - string name, string[] languages, bool supportsUnannotatedAPIs, params string[] identifiers) : ExportEmbeddedLanguageFeatureServiceAttribute(typeof(IEmbeddedLanguageDocumentHighlighter), name, languages, supportsUnannotatedAPIs, identifiers) + public ExportEmbeddedLanguageDocumentHighlighterAttribute( + string name, string[] languages, params string[] identifiers) + : this(name, languages, supportsUnannotatedAPIs: false, identifiers) { - public ExportEmbeddedLanguageDocumentHighlighterAttribute( - string name, string[] languages, params string[] identifiers) - : this(name, languages, supportsUnannotatedAPIs: false, identifiers) - { - } } } diff --git a/src/Features/Core/Portable/DocumentHighlighting/IDocumentHighlightsService.cs b/src/Features/Core/Portable/DocumentHighlighting/IDocumentHighlightsService.cs index cd752c003174f..1c65216b2136f 100644 --- a/src/Features/Core/Portable/DocumentHighlighting/IDocumentHighlightsService.cs +++ b/src/Features/Core/Portable/DocumentHighlighting/IDocumentHighlightsService.cs @@ -9,45 +9,44 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.DocumentHighlighting -{ - internal enum HighlightSpanKind - { - None, - Definition, - Reference, - WrittenReference, - } +namespace Microsoft.CodeAnalysis.DocumentHighlighting; - [DataContract] - internal readonly record struct HighlightSpan - { - [DataMember(Order = 0)] - public TextSpan TextSpan { get; } +internal enum HighlightSpanKind +{ + None, + Definition, + Reference, + WrittenReference, +} - [DataMember(Order = 1)] - public HighlightSpanKind Kind { get; } +[DataContract] +internal readonly record struct HighlightSpan +{ + [DataMember(Order = 0)] + public TextSpan TextSpan { get; } - public HighlightSpan(TextSpan textSpan, HighlightSpanKind kind) : this() - { - TextSpan = textSpan; - Kind = kind; - } - } + [DataMember(Order = 1)] + public HighlightSpanKind Kind { get; } - internal readonly struct DocumentHighlights(Document document, ImmutableArray highlightSpans) + public HighlightSpan(TextSpan textSpan, HighlightSpanKind kind) : this() { - public Document Document { get; } = document; - public ImmutableArray HighlightSpans { get; } = highlightSpans; + TextSpan = textSpan; + Kind = kind; } +} - /// - /// Note: This is the new version of the language service and superseded the same named type - /// in the EditorFeatures layer. - /// - internal interface IDocumentHighlightsService : ILanguageService - { - Task> GetDocumentHighlightsAsync( - Document document, int position, IImmutableSet documentsToSearch, HighlightingOptions options, CancellationToken cancellationToken); - } +internal readonly struct DocumentHighlights(Document document, ImmutableArray highlightSpans) +{ + public Document Document { get; } = document; + public ImmutableArray HighlightSpans { get; } = highlightSpans; +} + +/// +/// Note: This is the new version of the language service and superseded the same named type +/// in the EditorFeatures layer. +/// +internal interface IDocumentHighlightsService : ILanguageService +{ + Task> GetDocumentHighlightsAsync( + Document document, int position, IImmutableSet documentsToSearch, HighlightingOptions options, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/DocumentHighlighting/IEmbeddedLanguageDocumentHighlighter.cs b/src/Features/Core/Portable/DocumentHighlighting/IEmbeddedLanguageDocumentHighlighter.cs index 5d7e6fba9253b..7c64a2e55edbc 100644 --- a/src/Features/Core/Portable/DocumentHighlighting/IEmbeddedLanguageDocumentHighlighter.cs +++ b/src/Features/Core/Portable/DocumentHighlighting/IEmbeddedLanguageDocumentHighlighter.cs @@ -6,18 +6,17 @@ using System.Threading; using Microsoft.CodeAnalysis.EmbeddedLanguages; -namespace Microsoft.CodeAnalysis.DocumentHighlighting +namespace Microsoft.CodeAnalysis.DocumentHighlighting; + +/// +internal interface IEmbeddedLanguageDocumentHighlighter : IEmbeddedLanguageFeatureService { - /// - internal interface IEmbeddedLanguageDocumentHighlighter : IEmbeddedLanguageFeatureService - { - /// - ImmutableArray GetDocumentHighlights( - Document document, - SemanticModel semanticModel, - SyntaxToken token, - int position, - HighlightingOptions options, - CancellationToken cancellationToken); - } + /// + ImmutableArray GetDocumentHighlights( + Document document, + SemanticModel semanticModel, + SyntaxToken token, + int position, + HighlightingOptions options, + CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/DocumentHighlighting/IRemoteDocumentHighlightsService.cs b/src/Features/Core/Portable/DocumentHighlighting/IRemoteDocumentHighlightsService.cs index 86faaf8691b90..5570bbf6403af 100644 --- a/src/Features/Core/Portable/DocumentHighlighting/IRemoteDocumentHighlightsService.cs +++ b/src/Features/Core/Portable/DocumentHighlighting/IRemoteDocumentHighlightsService.cs @@ -8,27 +8,26 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.DocumentHighlighting +namespace Microsoft.CodeAnalysis.DocumentHighlighting; + +internal interface IRemoteDocumentHighlightsService { - internal interface IRemoteDocumentHighlightsService - { - ValueTask> GetDocumentHighlightsAsync( - Checksum solutionChecksum, DocumentId documentId, int position, ImmutableArray documentIdsToSearch, HighlightingOptions options, CancellationToken cancellationToken); - } + ValueTask> GetDocumentHighlightsAsync( + Checksum solutionChecksum, DocumentId documentId, int position, ImmutableArray documentIdsToSearch, HighlightingOptions options, CancellationToken cancellationToken); +} - [DataContract] - internal readonly struct SerializableDocumentHighlights(DocumentId documentId, ImmutableArray highlightSpans) - { - [DataMember(Order = 0)] - public readonly DocumentId DocumentId = documentId; +[DataContract] +internal readonly struct SerializableDocumentHighlights(DocumentId documentId, ImmutableArray highlightSpans) +{ + [DataMember(Order = 0)] + public readonly DocumentId DocumentId = documentId; - [DataMember(Order = 1)] - public readonly ImmutableArray HighlightSpans = highlightSpans; + [DataMember(Order = 1)] + public readonly ImmutableArray HighlightSpans = highlightSpans; - public async ValueTask RehydrateAsync(Solution solution) - => new(await solution.GetRequiredDocumentAsync(DocumentId, includeSourceGenerated: true).ConfigureAwait(false), HighlightSpans); + public async ValueTask RehydrateAsync(Solution solution) + => new(await solution.GetRequiredDocumentAsync(DocumentId, includeSourceGenerated: true).ConfigureAwait(false), HighlightSpans); - public static SerializableDocumentHighlights Dehydrate(DocumentHighlights highlights) - => new(highlights.Document.Id, highlights.HighlightSpans); - } + public static SerializableDocumentHighlights Dehydrate(DocumentHighlights highlights) + => new(highlights.Document.Id, highlights.HighlightSpans); } diff --git a/src/Features/Core/Portable/DocumentSpanExtensions.cs b/src/Features/Core/Portable/DocumentSpanExtensions.cs index e5303b3fa9387..be2edfaaae424 100644 --- a/src/Features/Core/Portable/DocumentSpanExtensions.cs +++ b/src/Features/Core/Portable/DocumentSpanExtensions.cs @@ -7,35 +7,34 @@ using Microsoft.CodeAnalysis.Navigation; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis +namespace Microsoft.CodeAnalysis; + +internal static class DocumentSpanExtensions { - internal static class DocumentSpanExtensions + private static (Workspace workspace, IDocumentNavigationService service) GetNavigationParts(DocumentSpan documentSpan) { - private static (Workspace workspace, IDocumentNavigationService service) GetNavigationParts(DocumentSpan documentSpan) - { - var solution = documentSpan.Document.Project.Solution; - var workspace = solution.Workspace; - var service = workspace.Services.GetRequiredService(); - return (workspace, service); - } + var solution = documentSpan.Document.Project.Solution; + var workspace = solution.Workspace; + var service = workspace.Services.GetRequiredService(); + return (workspace, service); + } - public static Task GetNavigableLocationAsync(this DocumentSpan documentSpan, CancellationToken cancellationToken) - { - var (workspace, service) = GetNavigationParts(documentSpan); - return service.GetLocationForSpanAsync(workspace, documentSpan.Document.Id, documentSpan.SourceSpan, allowInvalidSpan: false, cancellationToken); - } + public static Task GetNavigableLocationAsync(this DocumentSpan documentSpan, CancellationToken cancellationToken) + { + var (workspace, service) = GetNavigationParts(documentSpan); + return service.GetLocationForSpanAsync(workspace, documentSpan.Document.Id, documentSpan.SourceSpan, allowInvalidSpan: false, cancellationToken); + } - public static async Task IsHiddenAsync( - this DocumentSpan documentSpan, CancellationToken cancellationToken) + public static async Task IsHiddenAsync( + this DocumentSpan documentSpan, CancellationToken cancellationToken) + { + var document = documentSpan.Document; + if (document.SupportsSyntaxTree) { - var document = documentSpan.Document; - if (document.SupportsSyntaxTree) - { - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - return tree.IsHiddenPosition(documentSpan.SourceSpan.Start, cancellationToken); - } - - return false; + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + return tree.IsHiddenPosition(documentSpan.SourceSpan.Start, cancellationToken); } + + return false; } } diff --git a/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentFormattingService.cs b/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentFormattingService.cs index f631919dd9329..3b64e398908ca 100644 --- a/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentFormattingService.cs +++ b/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentFormattingService.cs @@ -14,596 +14,595 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.DocumentationComments +namespace Microsoft.CodeAnalysis.DocumentationComments; + +internal abstract class AbstractDocumentationCommentFormattingService : IDocumentationCommentFormattingService { - internal abstract class AbstractDocumentationCommentFormattingService : IDocumentationCommentFormattingService + private enum DocumentationCommentListType { - private enum DocumentationCommentListType - { - None, - Bullet, - Number, - Table, - } - - private class FormatterState - { - private bool _anyNonWhitespaceSinceLastPara; - private bool _pendingParagraphBreak; - private bool _pendingLineBreak; - private bool _pendingSingleSpace; - - private static readonly TaggedText s_spacePart = new(TextTags.Space, " "); - private static readonly TaggedText s_newlinePart = new(TextTags.LineBreak, "\r\n"); - - internal readonly ImmutableArray.Builder Builder = ImmutableArray.CreateBuilder(); - - /// - /// Defines the containing lists for the current formatting state. The last item in the list is the - /// innermost list. - /// - /// - /// - /// type - /// The type of list. - /// - /// - /// index - /// The index of the current item in the list. - /// - /// - /// renderedItem - /// if the label (a bullet or number) for the current list item has already been rendered; otherwise . - /// - /// - /// - private readonly List<(DocumentationCommentListType type, int index, bool renderedItem)> _listStack = []; - - /// - /// The top item of the stack indicates the hyperlink to apply to text rendered at the current location. It - /// consists of a navigation target (the destination to navigate to when clicked) and a hint - /// (typically shown as a tooltip for the link). This stack is never empty; when no hyperlink applies to the - /// current scope, the top item of the stack will be a default tuple instance. - /// - private readonly Stack<(string target, string hint)> _navigationTargetStack = new(); - - /// - /// Tracks the style for text. The top item of the stack is the current style to apply (the merged result of - /// all containing styles). This stack is never empty; when no style applies to the current scope, the top - /// item of the stack will be . - /// - private readonly Stack _styleStack = new(); - - public FormatterState() - { - _navigationTargetStack.Push(default); - _styleStack.Push(TaggedTextStyle.None); - } + None, + Bullet, + Number, + Table, + } + + private class FormatterState + { + private bool _anyNonWhitespaceSinceLastPara; + private bool _pendingParagraphBreak; + private bool _pendingLineBreak; + private bool _pendingSingleSpace; + + private static readonly TaggedText s_spacePart = new(TextTags.Space, " "); + private static readonly TaggedText s_newlinePart = new(TextTags.LineBreak, "\r\n"); + + internal readonly ImmutableArray.Builder Builder = ImmutableArray.CreateBuilder(); + + /// + /// Defines the containing lists for the current formatting state. The last item in the list is the + /// innermost list. + /// + /// + /// + /// type + /// The type of list. + /// + /// + /// index + /// The index of the current item in the list. + /// + /// + /// renderedItem + /// if the label (a bullet or number) for the current list item has already been rendered; otherwise . + /// + /// + /// + private readonly List<(DocumentationCommentListType type, int index, bool renderedItem)> _listStack = []; + + /// + /// The top item of the stack indicates the hyperlink to apply to text rendered at the current location. It + /// consists of a navigation target (the destination to navigate to when clicked) and a hint + /// (typically shown as a tooltip for the link). This stack is never empty; when no hyperlink applies to the + /// current scope, the top item of the stack will be a default tuple instance. + /// + private readonly Stack<(string target, string hint)> _navigationTargetStack = new(); + + /// + /// Tracks the style for text. The top item of the stack is the current style to apply (the merged result of + /// all containing styles). This stack is never empty; when no style applies to the current scope, the top + /// item of the stack will be . + /// + private readonly Stack _styleStack = new(); + + public FormatterState() + { + _navigationTargetStack.Push(default); + _styleStack.Push(TaggedTextStyle.None); + } - internal SemanticModel SemanticModel { get; set; } - internal ISymbol TypeResolutionSymbol { get; set; } - internal int Position { get; set; } + internal SemanticModel SemanticModel { get; set; } + internal ISymbol TypeResolutionSymbol { get; set; } + internal int Position { get; set; } - public bool AtBeginning + public bool AtBeginning + { + get { - get - { - return Builder.Count == 0; - } + return Builder.Count == 0; } + } - public SymbolDisplayFormat Format { get; internal set; } + public SymbolDisplayFormat Format { get; internal set; } - internal (string target, string hint) NavigationTarget => _navigationTargetStack.Peek(); - internal TaggedTextStyle Style => _styleStack.Peek(); + internal (string target, string hint) NavigationTarget => _navigationTargetStack.Peek(); + internal TaggedTextStyle Style => _styleStack.Peek(); - public void AppendSingleSpace() - => _pendingSingleSpace = true; + public void AppendSingleSpace() + => _pendingSingleSpace = true; - public void AppendString(string s) - { - EmitPendingChars(); - Builder.Add(new TaggedText(TextTags.Text, NormalizeLineEndings(s), Style, NavigationTarget.target, NavigationTarget.hint)); + public void AppendString(string s) + { + EmitPendingChars(); + Builder.Add(new TaggedText(TextTags.Text, NormalizeLineEndings(s), Style, NavigationTarget.target, NavigationTarget.hint)); - _anyNonWhitespaceSinceLastPara = true; + _anyNonWhitespaceSinceLastPara = true; - // XText.Value returns a string with `\n` as the line endings, causing - // the end result to have mixed line-endings. So normalize everything to `\r\n`. - // https://www.w3.org/TR/xml/#sec-line-ends - static string NormalizeLineEndings(string input) => input.Replace("\n", "\r\n"); - } + // XText.Value returns a string with `\n` as the line endings, causing + // the end result to have mixed line-endings. So normalize everything to `\r\n`. + // https://www.w3.org/TR/xml/#sec-line-ends + static string NormalizeLineEndings(string input) => input.Replace("\n", "\r\n"); + } - public void AppendParts(IEnumerable parts) - { - EmitPendingChars(); + public void AppendParts(IEnumerable parts) + { + EmitPendingChars(); - Builder.AddRange(parts); + Builder.AddRange(parts); - _anyNonWhitespaceSinceLastPara = true; - } + _anyNonWhitespaceSinceLastPara = true; + } + + public void PushList(DocumentationCommentListType listType) + { + _listStack.Add((listType, index: 0, renderedItem: false)); + MarkBeginOrEndPara(); + } - public void PushList(DocumentationCommentListType listType) + /// + /// Marks the start of an item in a list; called before each item. + /// + public void NextListItem() + { + if (_listStack.Count == 0) { - _listStack.Add((listType, index: 0, renderedItem: false)); - MarkBeginOrEndPara(); + return; } - /// - /// Marks the start of an item in a list; called before each item. - /// - public void NextListItem() + var (type, index, renderedItem) = _listStack[^1]; + if (renderedItem) { - if (_listStack.Count == 0) - { - return; - } + // Mark the end of the previous list item + Builder.Add(new TaggedText(TextTags.ContainerEnd, string.Empty)); + } - var (type, index, renderedItem) = _listStack[^1]; - if (renderedItem) - { - // Mark the end of the previous list item - Builder.Add(new TaggedText(TextTags.ContainerEnd, string.Empty)); - } + // The next list item has an incremented index, and has not yet been rendered to Builder. + _listStack[^1] = (type, index + 1, renderedItem: false); + MarkLineBreak(); + } - // The next list item has an incremented index, and has not yet been rendered to Builder. - _listStack[^1] = (type, index + 1, renderedItem: false); - MarkLineBreak(); + public void PopList() + { + if (_listStack.Count == 0) + { + return; } - public void PopList() + if (_listStack[^1].renderedItem) { - if (_listStack.Count == 0) - { - return; - } - - if (_listStack[^1].renderedItem) - { - Builder.Add(new TaggedText(TextTags.ContainerEnd, string.Empty)); - } - - _listStack.RemoveAt(_listStack.Count - 1); - MarkBeginOrEndPara(); + Builder.Add(new TaggedText(TextTags.ContainerEnd, string.Empty)); } - public void PushNavigationTarget(string target, string hint) - => _navigationTargetStack.Push((target, hint)); + _listStack.RemoveAt(_listStack.Count - 1); + MarkBeginOrEndPara(); + } + + public void PushNavigationTarget(string target, string hint) + => _navigationTargetStack.Push((target, hint)); - public void PopNavigationTarget() - => _navigationTargetStack.Pop(); + public void PopNavigationTarget() + => _navigationTargetStack.Pop(); - public void PushStyle(TaggedTextStyle style) - => _styleStack.Push(_styleStack.Peek() | style); + public void PushStyle(TaggedTextStyle style) + => _styleStack.Push(_styleStack.Peek() | style); - public void PopStyle() - => _styleStack.Pop(); + public void PopStyle() + => _styleStack.Pop(); - public void MarkBeginOrEndPara() + public void MarkBeginOrEndPara() + { + // If this is a with nothing before it, then skip it. + if (_anyNonWhitespaceSinceLastPara == false) { - // If this is a with nothing before it, then skip it. - if (_anyNonWhitespaceSinceLastPara == false) - { - return; - } + return; + } - _pendingParagraphBreak = true; + _pendingParagraphBreak = true; - // Reset flag. - _anyNonWhitespaceSinceLastPara = false; + // Reset flag. + _anyNonWhitespaceSinceLastPara = false; + } + + public void MarkLineBreak() + { + // If this is a
with nothing before it, then skip it. + if (_anyNonWhitespaceSinceLastPara == false && !_pendingLineBreak) + { + return; } - public void MarkLineBreak() + if (_pendingLineBreak || _pendingParagraphBreak) { - // If this is a
with nothing before it, then skip it. - if (_anyNonWhitespaceSinceLastPara == false && !_pendingLineBreak) - { - return; - } + // Multiple line breaks in sequence become a single paragraph break. + _pendingParagraphBreak = true; + _pendingLineBreak = false; + } + else + { + _pendingLineBreak = true; + } - if (_pendingLineBreak || _pendingParagraphBreak) - { - // Multiple line breaks in sequence become a single paragraph break. - _pendingParagraphBreak = true; - _pendingLineBreak = false; - } - else - { - _pendingLineBreak = true; - } + // Reset flag. + _anyNonWhitespaceSinceLastPara = false; + } + + public string GetText() + => Builder.GetFullText(); - // Reset flag. - _anyNonWhitespaceSinceLastPara = false; + private void EmitPendingChars() + { + if (_pendingParagraphBreak) + { + Builder.Add(s_newlinePart); + Builder.Add(s_newlinePart); + } + else if (_pendingLineBreak) + { + Builder.Add(s_newlinePart); + } + else if (_pendingSingleSpace) + { + Builder.Add(s_spacePart); } - public string GetText() - => Builder.GetFullText(); + _pendingParagraphBreak = false; + _pendingLineBreak = false; + _pendingSingleSpace = false; - private void EmitPendingChars() + for (var i = 0; i < _listStack.Count; i++) { - if (_pendingParagraphBreak) - { - Builder.Add(s_newlinePart); - Builder.Add(s_newlinePart); - } - else if (_pendingLineBreak) - { - Builder.Add(s_newlinePart); - } - else if (_pendingSingleSpace) + if (_listStack[i].renderedItem) { - Builder.Add(s_spacePart); + continue; } - _pendingParagraphBreak = false; - _pendingLineBreak = false; - _pendingSingleSpace = false; - - for (var i = 0; i < _listStack.Count; i++) + switch (_listStack[i].type) { - if (_listStack[i].renderedItem) - { - continue; - } - - switch (_listStack[i].type) - { - case DocumentationCommentListType.Bullet: - Builder.Add(new TaggedText(TextTags.ContainerStart, "• ")); - break; - - case DocumentationCommentListType.Number: - Builder.Add(new TaggedText(TextTags.ContainerStart, $"{_listStack[i].index}. ")); - break; - - case DocumentationCommentListType.Table: - case DocumentationCommentListType.None: - default: - Builder.Add(new TaggedText(TextTags.ContainerStart, string.Empty)); - break; - } - - _listStack[i] = (_listStack[i].type, _listStack[i].index, renderedItem: true); + case DocumentationCommentListType.Bullet: + Builder.Add(new TaggedText(TextTags.ContainerStart, "• ")); + break; + + case DocumentationCommentListType.Number: + Builder.Add(new TaggedText(TextTags.ContainerStart, $"{_listStack[i].index}. ")); + break; + + case DocumentationCommentListType.Table: + case DocumentationCommentListType.None: + default: + Builder.Add(new TaggedText(TextTags.ContainerStart, string.Empty)); + break; } + + _listStack[i] = (_listStack[i].type, _listStack[i].index, renderedItem: true); } } + } - public string Format(string rawXmlText, Compilation compilation = null) + public string Format(string rawXmlText, Compilation compilation = null) + { + if (rawXmlText == null) { - if (rawXmlText == null) - { - return null; - } + return null; + } - var state = new FormatterState(); + var state = new FormatterState(); - // In case the XML is a fragment (that is, a series of elements without a parent) - // wrap it up in a single tag. This makes parsing it much, much easier. - var inputString = "" + rawXmlText + ""; + // In case the XML is a fragment (that is, a series of elements without a parent) + // wrap it up in a single tag. This makes parsing it much, much easier. + var inputString = "" + rawXmlText + ""; - var summaryElement = XElement.Parse(inputString, LoadOptions.PreserveWhitespace); + var summaryElement = XElement.Parse(inputString, LoadOptions.PreserveWhitespace); - AppendTextFromNode(state, summaryElement, compilation); + AppendTextFromNode(state, summaryElement, compilation); - return state.GetText(); - } + return state.GetText(); + } - public ImmutableArray Format(string rawXmlText, ISymbol symbol, SemanticModel semanticModel, int position, SymbolDisplayFormat format, CancellationToken cancellationToken) + public ImmutableArray Format(string rawXmlText, ISymbol symbol, SemanticModel semanticModel, int position, SymbolDisplayFormat format, CancellationToken cancellationToken) + { + if (rawXmlText is null) { - if (rawXmlText is null) - { - return []; - } - //symbol = symbol.OriginalDefinition; + return []; + } + //symbol = symbol.OriginalDefinition; - var state = new FormatterState() { SemanticModel = semanticModel, Position = position, Format = format, TypeResolutionSymbol = symbol }; + var state = new FormatterState() { SemanticModel = semanticModel, Position = position, Format = format, TypeResolutionSymbol = symbol }; - // In case the XML is a fragment (that is, a series of elements without a parent) - // wrap it up in a single tag. This makes parsing it much, much easier. - var inputString = "" + rawXmlText + ""; + // In case the XML is a fragment (that is, a series of elements without a parent) + // wrap it up in a single tag. This makes parsing it much, much easier. + var inputString = "" + rawXmlText + ""; - var summaryElement = XElement.Parse(inputString, LoadOptions.PreserveWhitespace); + var summaryElement = XElement.Parse(inputString, LoadOptions.PreserveWhitespace); - AppendTextFromNode(state, summaryElement, state.SemanticModel.Compilation); + AppendTextFromNode(state, summaryElement, state.SemanticModel.Compilation); - return state.Builder.ToImmutable(); - } + return state.Builder.ToImmutable(); + } - private static void AppendTextFromNode(FormatterState state, XNode node, Compilation compilation) + private static void AppendTextFromNode(FormatterState state, XNode node, Compilation compilation) + { + if (node.NodeType is XmlNodeType.Text or XmlNodeType.CDATA) { - if (node.NodeType is XmlNodeType.Text or XmlNodeType.CDATA) - { - // cast is safe since XCData inherits XText - AppendTextFromTextNode(state, (XText)node); - } - - if (node.NodeType != XmlNodeType.Element) - { - return; - } + // cast is safe since XCData inherits XText + AppendTextFromTextNode(state, (XText)node); + } - var element = (XElement)node; + if (node.NodeType != XmlNodeType.Element) + { + return; + } - var name = element.Name.LocalName; - var needPopStyle = false; - (string target, string hint)? navigationTarget = null; + var element = (XElement)node; - if (name is DocumentationCommentXmlNames.SeeElementName or - DocumentationCommentXmlNames.SeeAlsoElementName or - "a") - { - if (element.IsEmpty || element.FirstNode == null) - { - foreach (var attribute in element.Attributes()) - { - AppendTextFromAttribute(state, attribute, attributeNameToParse: DocumentationCommentXmlNames.CrefAttributeName, SymbolDisplayPartKind.Text); - } + var name = element.Name.LocalName; + var needPopStyle = false; + (string target, string hint)? navigationTarget = null; - return; - } - else - { - navigationTarget = GetNavigationTarget(element, state.SemanticModel, state.Position, state.Format); - if (navigationTarget is object) - { - state.PushNavigationTarget(navigationTarget.Value.target, navigationTarget.Value.hint); - } - } - } - else if (name is DocumentationCommentXmlNames.ParameterReferenceElementName or - DocumentationCommentXmlNames.TypeParameterReferenceElementName) + if (name is DocumentationCommentXmlNames.SeeElementName or + DocumentationCommentXmlNames.SeeAlsoElementName or + "a") + { + if (element.IsEmpty || element.FirstNode == null) { - var kind = name == DocumentationCommentXmlNames.ParameterReferenceElementName ? SymbolDisplayPartKind.ParameterName : SymbolDisplayPartKind.TypeParameterName; foreach (var attribute in element.Attributes()) { - AppendTextFromAttribute(state, attribute, attributeNameToParse: DocumentationCommentXmlNames.NameAttributeName, kind); + AppendTextFromAttribute(state, attribute, attributeNameToParse: DocumentationCommentXmlNames.CrefAttributeName, SymbolDisplayPartKind.Text); } return; } - else if (name is DocumentationCommentXmlNames.CElementName or "tt") - { - needPopStyle = true; - state.PushStyle(TaggedTextStyle.Code); - } - else if (name is DocumentationCommentXmlNames.CodeElementName) - { - needPopStyle = true; - state.PushStyle(TaggedTextStyle.Code | TaggedTextStyle.PreserveWhitespace); - } - else if (name is "em" or "i") - { - needPopStyle = true; - state.PushStyle(TaggedTextStyle.Emphasis); - } - else if (name is "strong" or "b" or DocumentationCommentXmlNames.TermElementName) - { - needPopStyle = true; - state.PushStyle(TaggedTextStyle.Strong); - } - else if (name == "u") - { - needPopStyle = true; - state.PushStyle(TaggedTextStyle.Underline); - } - - if (name == DocumentationCommentXmlNames.ListElementName) + else { - var rawListType = element.Attribute(DocumentationCommentXmlNames.TypeAttributeName)?.Value; - var listType = rawListType switch + navigationTarget = GetNavigationTarget(element, state.SemanticModel, state.Position, state.Format); + if (navigationTarget is object) { - "table" => DocumentationCommentListType.Table, - "number" => DocumentationCommentListType.Number, - "bullet" => DocumentationCommentListType.Bullet, - _ => DocumentationCommentListType.None, - }; - state.PushList(listType); + state.PushNavigationTarget(navigationTarget.Value.target, navigationTarget.Value.hint); + } } - else if (name == DocumentationCommentXmlNames.ItemElementName) + } + else if (name is DocumentationCommentXmlNames.ParameterReferenceElementName or + DocumentationCommentXmlNames.TypeParameterReferenceElementName) + { + var kind = name == DocumentationCommentXmlNames.ParameterReferenceElementName ? SymbolDisplayPartKind.ParameterName : SymbolDisplayPartKind.TypeParameterName; + foreach (var attribute in element.Attributes()) { - state.NextListItem(); + AppendTextFromAttribute(state, attribute, attributeNameToParse: DocumentationCommentXmlNames.NameAttributeName, kind); } - if (name is DocumentationCommentXmlNames.ParaElementName - or DocumentationCommentXmlNames.CodeElementName) - { - state.MarkBeginOrEndPara(); - } - else if (name == "br") - { - state.MarkLineBreak(); - } + return; + } + else if (name is DocumentationCommentXmlNames.CElementName or "tt") + { + needPopStyle = true; + state.PushStyle(TaggedTextStyle.Code); + } + else if (name is DocumentationCommentXmlNames.CodeElementName) + { + needPopStyle = true; + state.PushStyle(TaggedTextStyle.Code | TaggedTextStyle.PreserveWhitespace); + } + else if (name is "em" or "i") + { + needPopStyle = true; + state.PushStyle(TaggedTextStyle.Emphasis); + } + else if (name is "strong" or "b" or DocumentationCommentXmlNames.TermElementName) + { + needPopStyle = true; + state.PushStyle(TaggedTextStyle.Strong); + } + else if (name == "u") + { + needPopStyle = true; + state.PushStyle(TaggedTextStyle.Underline); + } - foreach (var childNode in element.Nodes()) - { - AppendTextFromNode(state, childNode, compilation); - } + if (name == DocumentationCommentXmlNames.ListElementName) + { + var rawListType = element.Attribute(DocumentationCommentXmlNames.TypeAttributeName)?.Value; + var listType = rawListType switch + { + "table" => DocumentationCommentListType.Table, + "number" => DocumentationCommentListType.Number, + "bullet" => DocumentationCommentListType.Bullet, + _ => DocumentationCommentListType.None, + }; + state.PushList(listType); + } + else if (name == DocumentationCommentXmlNames.ItemElementName) + { + state.NextListItem(); + } - if (name is DocumentationCommentXmlNames.ParaElementName - or DocumentationCommentXmlNames.CodeElementName) - { - state.MarkBeginOrEndPara(); - } + if (name is DocumentationCommentXmlNames.ParaElementName + or DocumentationCommentXmlNames.CodeElementName) + { + state.MarkBeginOrEndPara(); + } + else if (name == "br") + { + state.MarkLineBreak(); + } - if (name == DocumentationCommentXmlNames.ListElementName) - { - state.PopList(); - } + foreach (var childNode in element.Nodes()) + { + AppendTextFromNode(state, childNode, compilation); + } - if (needPopStyle) - { - state.PopStyle(); - } + if (name is DocumentationCommentXmlNames.ParaElementName + or DocumentationCommentXmlNames.CodeElementName) + { + state.MarkBeginOrEndPara(); + } - if (navigationTarget is object) - { - state.PopNavigationTarget(); - } + if (name == DocumentationCommentXmlNames.ListElementName) + { + state.PopList(); + } - if (name == DocumentationCommentXmlNames.TermElementName) - { - state.AppendSingleSpace(); - state.AppendString("–"); - } + if (needPopStyle) + { + state.PopStyle(); } - private static (string target, string hint)? GetNavigationTarget(XElement element, SemanticModel semanticModel, int position, SymbolDisplayFormat format) + if (navigationTarget is object) { - var crefAttribute = element.Attribute(DocumentationCommentXmlNames.CrefAttributeName); - if (crefAttribute is not null && semanticModel is not null) - { - var symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(crefAttribute.Value, semanticModel.Compilation); - if (symbol is not null) - return (target: SymbolKey.CreateString(symbol), hint: symbol.ToMinimalDisplayString(semanticModel, position, format ?? SymbolDisplayFormat.MinimallyQualifiedFormat)); - } + state.PopNavigationTarget(); + } - var hrefAttribute = element.Attribute(DocumentationCommentXmlNames.HrefAttributeName); - if (hrefAttribute is not null) - return (target: hrefAttribute.Value, hint: hrefAttribute.Value); + if (name == DocumentationCommentXmlNames.TermElementName) + { + state.AppendSingleSpace(); + state.AppendString("–"); + } + } - return null; + private static (string target, string hint)? GetNavigationTarget(XElement element, SemanticModel semanticModel, int position, SymbolDisplayFormat format) + { + var crefAttribute = element.Attribute(DocumentationCommentXmlNames.CrefAttributeName); + if (crefAttribute is not null && semanticModel is not null) + { + var symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(crefAttribute.Value, semanticModel.Compilation); + if (symbol is not null) + return (target: SymbolKey.CreateString(symbol), hint: symbol.ToMinimalDisplayString(semanticModel, position, format ?? SymbolDisplayFormat.MinimallyQualifiedFormat)); } - private static void AppendTextFromAttribute(FormatterState state, XAttribute attribute, string attributeNameToParse, SymbolDisplayPartKind kind) + var hrefAttribute = element.Attribute(DocumentationCommentXmlNames.HrefAttributeName); + if (hrefAttribute is not null) + return (target: hrefAttribute.Value, hint: hrefAttribute.Value); + + return null; + } + + private static void AppendTextFromAttribute(FormatterState state, XAttribute attribute, string attributeNameToParse, SymbolDisplayPartKind kind) + { + var attributeName = attribute.Name.LocalName; + if (attributeNameToParse == attributeName) { - var attributeName = attribute.Name.LocalName; - if (attributeNameToParse == attributeName) + if (kind == SymbolDisplayPartKind.TypeParameterName) { - if (kind == SymbolDisplayPartKind.TypeParameterName) - { - state.AppendParts( - TypeParameterRefToSymbolDisplayParts(attribute.Value, state.TypeResolutionSymbol, state.Position, state.SemanticModel, state.Format).ToTaggedText(state.Style)); - } - else - { - state.AppendParts( - CrefToSymbolDisplayParts(attribute.Value, state.Position, state.SemanticModel, state.Format, kind).ToTaggedText(state.Style)); - } + state.AppendParts( + TypeParameterRefToSymbolDisplayParts(attribute.Value, state.TypeResolutionSymbol, state.Position, state.SemanticModel, state.Format).ToTaggedText(state.Style)); } else { - var displayKind = attributeName == DocumentationCommentXmlNames.LangwordAttributeName - ? TextTags.Keyword - : TextTags.Text; - var text = attribute.Value; - var style = state.Style; - var navigationTarget = attributeName == DocumentationCommentXmlNames.HrefAttributeName - ? attribute.Value - : null; - var navigationHint = navigationTarget; - state.AppendParts(SpecializedCollections.SingletonEnumerable(new TaggedText(displayKind, text, style, navigationTarget, navigationHint))); + state.AppendParts( + CrefToSymbolDisplayParts(attribute.Value, state.Position, state.SemanticModel, state.Format, kind).ToTaggedText(state.Style)); } } + else + { + var displayKind = attributeName == DocumentationCommentXmlNames.LangwordAttributeName + ? TextTags.Keyword + : TextTags.Text; + var text = attribute.Value; + var style = state.Style; + var navigationTarget = attributeName == DocumentationCommentXmlNames.HrefAttributeName + ? attribute.Value + : null; + var navigationHint = navigationTarget; + state.AppendParts(SpecializedCollections.SingletonEnumerable(new TaggedText(displayKind, text, style, navigationTarget, navigationHint))); + } + } - internal static IEnumerable CrefToSymbolDisplayParts( - string crefValue, int position, SemanticModel semanticModel, SymbolDisplayFormat format = null, SymbolDisplayPartKind kind = SymbolDisplayPartKind.Text) + internal static IEnumerable CrefToSymbolDisplayParts( + string crefValue, int position, SemanticModel semanticModel, SymbolDisplayFormat format = null, SymbolDisplayPartKind kind = SymbolDisplayPartKind.Text) + { + // first try to parse the symbol + if (crefValue != null && semanticModel != null) { - // first try to parse the symbol - if (crefValue != null && semanticModel != null) + var symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(crefValue, semanticModel.Compilation); + if (symbol != null) { - var symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(crefValue, semanticModel.Compilation); - if (symbol != null) + format ??= SymbolDisplayFormat.MinimallyQualifiedFormat; + if (symbol.IsConstructor()) { - format ??= SymbolDisplayFormat.MinimallyQualifiedFormat; - if (symbol.IsConstructor()) - { - format = format.WithMemberOptions(SymbolDisplayMemberOptions.IncludeParameters | SymbolDisplayMemberOptions.IncludeExplicitInterface); - } - - return symbol.ToMinimalDisplayParts(semanticModel, position, format); + format = format.WithMemberOptions(SymbolDisplayMemberOptions.IncludeParameters | SymbolDisplayMemberOptions.IncludeExplicitInterface); } - } - // if any of that fails fall back to just displaying the raw text - return SpecializedCollections.SingletonEnumerable( - new SymbolDisplayPart(kind, symbol: null, text: TrimCrefPrefix(crefValue))); + return symbol.ToMinimalDisplayParts(semanticModel, position, format); + } } - internal static IEnumerable TypeParameterRefToSymbolDisplayParts( - string crefValue, ISymbol typeResolutionSymbol, int position, SemanticModel semanticModel, SymbolDisplayFormat format) + // if any of that fails fall back to just displaying the raw text + return SpecializedCollections.SingletonEnumerable( + new SymbolDisplayPart(kind, symbol: null, text: TrimCrefPrefix(crefValue))); + } + + internal static IEnumerable TypeParameterRefToSymbolDisplayParts( + string crefValue, ISymbol typeResolutionSymbol, int position, SemanticModel semanticModel, SymbolDisplayFormat format) + { + if (semanticModel != null) { - if (semanticModel != null) + var typeParameterIndex = typeResolutionSymbol.OriginalDefinition.GetAllTypeParameters().IndexOf(tp => tp.Name == crefValue); + if (typeParameterIndex >= 0) { - var typeParameterIndex = typeResolutionSymbol.OriginalDefinition.GetAllTypeParameters().IndexOf(tp => tp.Name == crefValue); - if (typeParameterIndex >= 0) + var typeArgs = typeResolutionSymbol.GetAllTypeArguments(); + if (typeArgs.Length > typeParameterIndex) { - var typeArgs = typeResolutionSymbol.GetAllTypeArguments(); - if (typeArgs.Length > typeParameterIndex) - { - return typeArgs[typeParameterIndex].ToMinimalDisplayParts(semanticModel, position, format); - } + return typeArgs[typeParameterIndex].ToMinimalDisplayParts(semanticModel, position, format); } } - - // if any of that fails fall back to just displaying the raw text - return SpecializedCollections.SingletonEnumerable( - new SymbolDisplayPart(SymbolDisplayPartKind.TypeParameterName, symbol: null, text: TrimCrefPrefix(crefValue))); } - private static string TrimCrefPrefix(string value) - { - if (value is [_, ':', ..]) - value = value[2..]; + // if any of that fails fall back to just displaying the raw text + return SpecializedCollections.SingletonEnumerable( + new SymbolDisplayPart(SymbolDisplayPartKind.TypeParameterName, symbol: null, text: TrimCrefPrefix(crefValue))); + } + + private static string TrimCrefPrefix(string value) + { + if (value is [_, ':', ..]) + value = value[2..]; - return value; + return value; + } + + private static void AppendTextFromTextNode(FormatterState state, XText element) + { + var rawText = element.Value; + if ((state.Style & TaggedTextStyle.PreserveWhitespace) == TaggedTextStyle.PreserveWhitespace) + { + // Don't normalize code from middle. Only trim leading/trailing new lines. + state.AppendString(rawText.Trim('\n')); + return; } - private static void AppendTextFromTextNode(FormatterState state, XText element) + var builder = new StringBuilder(rawText.Length); + + // Normalize the whitespace. + var pendingWhitespace = false; + var hadAnyNonWhitespace = false; + for (var i = 0; i < rawText.Length; i++) { - var rawText = element.Value; - if ((state.Style & TaggedTextStyle.PreserveWhitespace) == TaggedTextStyle.PreserveWhitespace) + if (char.IsWhiteSpace(rawText[i])) { - // Don't normalize code from middle. Only trim leading/trailing new lines. - state.AppendString(rawText.Trim('\n')); - return; + // Whitespace. If it occurs at the beginning of the text we don't append it + // at all; otherwise, we reduce it to a single space. + if (!state.AtBeginning || hadAnyNonWhitespace) + { + pendingWhitespace = true; + } } - - var builder = new StringBuilder(rawText.Length); - - // Normalize the whitespace. - var pendingWhitespace = false; - var hadAnyNonWhitespace = false; - for (var i = 0; i < rawText.Length; i++) + else { - if (char.IsWhiteSpace(rawText[i])) + // Some other character... + if (pendingWhitespace) { - // Whitespace. If it occurs at the beginning of the text we don't append it - // at all; otherwise, we reduce it to a single space. - if (!state.AtBeginning || hadAnyNonWhitespace) + if (builder.Length == 0) { - pendingWhitespace = true; + state.AppendSingleSpace(); } - } - else - { - // Some other character... - if (pendingWhitespace) + else { - if (builder.Length == 0) - { - state.AppendSingleSpace(); - } - else - { - builder.Append(' '); - } - - pendingWhitespace = false; + builder.Append(' '); } - builder.Append(rawText[i]); - hadAnyNonWhitespace = true; + pendingWhitespace = false; } - } - if (builder.Length > 0) - { - state.AppendString(builder.ToString()); + builder.Append(rawText[i]); + hadAnyNonWhitespace = true; } + } - if (pendingWhitespace) - { - state.AppendSingleSpace(); - } + if (builder.Length > 0) + { + state.AppendString(builder.ToString()); + } + + if (pendingWhitespace) + { + state.AppendSingleSpace(); } } } diff --git a/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentSnippetService.cs b/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentSnippetService.cs index 199920bab8338..c02d964a5992d 100644 --- a/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentSnippetService.cs +++ b/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentSnippetService.cs @@ -11,405 +11,404 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.DocumentationComments +namespace Microsoft.CodeAnalysis.DocumentationComments; + +internal abstract class AbstractDocumentationCommentSnippetService : IDocumentationCommentSnippetService + where TDocumentationComment : SyntaxNode, IStructuredTriviaSyntax + where TMemberNode : SyntaxNode { - internal abstract class AbstractDocumentationCommentSnippetService : IDocumentationCommentSnippetService - where TDocumentationComment : SyntaxNode, IStructuredTriviaSyntax - where TMemberNode : SyntaxNode + protected abstract TMemberNode? GetContainingMember(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); + protected abstract bool SupportsDocumentationComments(TMemberNode member); + protected abstract bool HasDocumentationComment(TMemberNode member); + protected abstract int GetPrecedingDocumentationCommentCount(TMemberNode member); + protected abstract List GetDocumentationCommentStubLines(TMemberNode member, string existingCommentText); + + protected abstract SyntaxToken GetTokenToRight(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); + protected abstract SyntaxToken GetTokenToLeft(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); + protected abstract bool IsDocCommentNewLine(SyntaxToken token); + protected abstract bool IsEndOfLineTrivia(SyntaxTrivia trivia); + + protected abstract bool IsSingleExteriorTrivia(TDocumentationComment documentationComment, [NotNullWhen(true)] out string? existingCommentText); + protected abstract bool EndsWithSingleExteriorTrivia(TDocumentationComment? documentationComment); + protected abstract bool IsMultilineDocComment(TDocumentationComment? documentationComment); + protected abstract bool HasSkippedTrailingTrivia(SyntaxToken token); + + public abstract string DocumentationCommentCharacter { get; } + + protected abstract string ExteriorTriviaText { get; } + protected abstract bool AddIndent { get; } + + public DocumentationCommentSnippet? GetDocumentationCommentSnippetOnCharacterTyped( + SyntaxTree syntaxTree, + SourceText text, + int position, + in DocumentationCommentOptions options, + CancellationToken cancellationToken, + bool addIndentation = true) { - protected abstract TMemberNode? GetContainingMember(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); - protected abstract bool SupportsDocumentationComments(TMemberNode member); - protected abstract bool HasDocumentationComment(TMemberNode member); - protected abstract int GetPrecedingDocumentationCommentCount(TMemberNode member); - protected abstract List GetDocumentationCommentStubLines(TMemberNode member, string existingCommentText); - - protected abstract SyntaxToken GetTokenToRight(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); - protected abstract SyntaxToken GetTokenToLeft(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); - protected abstract bool IsDocCommentNewLine(SyntaxToken token); - protected abstract bool IsEndOfLineTrivia(SyntaxTrivia trivia); - - protected abstract bool IsSingleExteriorTrivia(TDocumentationComment documentationComment, [NotNullWhen(true)] out string? existingCommentText); - protected abstract bool EndsWithSingleExteriorTrivia(TDocumentationComment? documentationComment); - protected abstract bool IsMultilineDocComment(TDocumentationComment? documentationComment); - protected abstract bool HasSkippedTrailingTrivia(SyntaxToken token); - - public abstract string DocumentationCommentCharacter { get; } - - protected abstract string ExteriorTriviaText { get; } - protected abstract bool AddIndent { get; } - - public DocumentationCommentSnippet? GetDocumentationCommentSnippetOnCharacterTyped( - SyntaxTree syntaxTree, - SourceText text, - int position, - in DocumentationCommentOptions options, - CancellationToken cancellationToken, - bool addIndentation = true) + if (!options.AutoXmlDocCommentGeneration) { - if (!options.AutoXmlDocCommentGeneration) - { - return null; - } + return null; + } - // Only generate if the position is immediately after '///', - // and that is the only documentation comment on the target member. + // Only generate if the position is immediately after '///', + // and that is the only documentation comment on the target member. - var token = syntaxTree.GetRoot(cancellationToken).FindToken(position, findInsideTrivia: true); - if (position != token.SpanStart) - { - return null; - } + var token = syntaxTree.GetRoot(cancellationToken).FindToken(position, findInsideTrivia: true); + if (position != token.SpanStart) + { + return null; + } - var lines = addIndentation - ? GetDocumentationCommentLines(token, text, options, out _, out var caretOffset, out var spanToReplaceLength) - : GetDocumentationCommentLinesNoIndentation(token, text, options, out caretOffset, out spanToReplaceLength); + var lines = addIndentation + ? GetDocumentationCommentLines(token, text, options, out _, out var caretOffset, out var spanToReplaceLength) + : GetDocumentationCommentLinesNoIndentation(token, text, options, out caretOffset, out spanToReplaceLength); - if (lines == null) - { - return null; - } + if (lines == null) + { + return null; + } - var newLine = options.NewLine; + var newLine = options.NewLine; - var lastLine = lines[^1]; - lines[^1] = lastLine[..^newLine.Length]; + var lastLine = lines[^1]; + lines[^1] = lastLine[..^newLine.Length]; - var comments = string.Join(string.Empty, lines); + var comments = string.Join(string.Empty, lines); - var replaceSpan = new TextSpan(token.Span.Start, spanToReplaceLength); + var replaceSpan = new TextSpan(token.Span.Start, spanToReplaceLength); - return new DocumentationCommentSnippet(replaceSpan, comments, caretOffset); - } + return new DocumentationCommentSnippet(replaceSpan, comments, caretOffset); + } + + private List? GetDocumentationCommentLines(SyntaxToken token, SourceText text, in DocumentationCommentOptions options, out string? indentText, out int caretOffset, out int spanToReplaceLength) + { + indentText = null; - private List? GetDocumentationCommentLines(SyntaxToken token, SourceText text, in DocumentationCommentOptions options, out string? indentText, out int caretOffset, out int spanToReplaceLength) + var lines = GetDocumentationStubLines(token, text, options, out caretOffset, out spanToReplaceLength, out var existingCommentText); + if (lines is null) { - indentText = null; + return lines; + } - var lines = GetDocumentationStubLines(token, text, options, out caretOffset, out spanToReplaceLength, out var existingCommentText); - if (lines is null) - { - return lines; - } + var documentationComment = token.GetAncestor(); + var line = text.Lines.GetLineFromPosition(documentationComment!.FullSpan.Start); + if (line.IsEmptyOrWhitespace()) + { + return null; + } - var documentationComment = token.GetAncestor(); - var line = text.Lines.GetLineFromPosition(documentationComment!.FullSpan.Start); - if (line.IsEmptyOrWhitespace()) - { - return null; - } + // Add indents + var lineOffset = line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(options.TabSize); + indentText = lineOffset.CreateIndentationString(options.UseTabs, options.TabSize); - // Add indents - var lineOffset = line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(options.TabSize); - indentText = lineOffset.CreateIndentationString(options.UseTabs, options.TabSize); + IndentLines(lines, indentText); - IndentLines(lines, indentText); + // We always want the caret text to be on the second line, with one space after the doc comment XML + // GetDocumentationCommentStubLines ensures that space is always there + caretOffset = lines[0].Length + indentText.Length + ExteriorTriviaText.Length + 1; + spanToReplaceLength = existingCommentText!.Length; - // We always want the caret text to be on the second line, with one space after the doc comment XML - // GetDocumentationCommentStubLines ensures that space is always there - caretOffset = lines[0].Length + indentText.Length + ExteriorTriviaText.Length + 1; - spanToReplaceLength = existingCommentText!.Length; + return lines; + } + private List? GetDocumentationCommentLinesNoIndentation(SyntaxToken token, SourceText text, in DocumentationCommentOptions options, out int caretOffset, out int spanToReplaceLength) + { + var lines = GetDocumentationStubLines(token, text, options, out caretOffset, out spanToReplaceLength, out var existingCommentText); + if (lines is null) + { return lines; } - private List? GetDocumentationCommentLinesNoIndentation(SyntaxToken token, SourceText text, in DocumentationCommentOptions options, out int caretOffset, out int spanToReplaceLength) - { - var lines = GetDocumentationStubLines(token, text, options, out caretOffset, out spanToReplaceLength, out var existingCommentText); - if (lines is null) - { - return lines; - } + // We always want the caret text to be on the second line, with one space after the doc comment XML + // GetDocumentationCommentStubLines ensures that space is always there + caretOffset = lines[0].Length + ExteriorTriviaText.Length + 1; + spanToReplaceLength = existingCommentText!.Length; - // We always want the caret text to be on the second line, with one space after the doc comment XML - // GetDocumentationCommentStubLines ensures that space is always there - caretOffset = lines[0].Length + ExteriorTriviaText.Length + 1; - spanToReplaceLength = existingCommentText!.Length; + return lines; + } - return lines; - } + private List? GetDocumentationStubLines(SyntaxToken token, SourceText text, in DocumentationCommentOptions options, out int caretOffset, out int spanToReplaceLength, out string? existingCommentText) + { + caretOffset = 0; + spanToReplaceLength = 0; + existingCommentText = null; + + var documentationComment = token.GetAncestor(); - private List? GetDocumentationStubLines(SyntaxToken token, SourceText text, in DocumentationCommentOptions options, out int caretOffset, out int spanToReplaceLength, out string? existingCommentText) + if (documentationComment == null || !IsSingleExteriorTrivia(documentationComment, out existingCommentText)) { - caretOffset = 0; - spanToReplaceLength = 0; - existingCommentText = null; + return null; + } - var documentationComment = token.GetAncestor(); + var targetMember = GetTargetMember(documentationComment); - if (documentationComment == null || !IsSingleExteriorTrivia(documentationComment, out existingCommentText)) - { - return null; - } + // Ensure that the target member is only preceded by a single documentation comment (i.e. our ///). + if (targetMember == null || GetPrecedingDocumentationCommentCount(targetMember) != 1) + { + return null; + } - var targetMember = GetTargetMember(documentationComment); + var line = text.Lines.GetLineFromPosition(documentationComment.FullSpan.Start); + if (line.IsEmptyOrWhitespace()) + { + return null; + } - // Ensure that the target member is only preceded by a single documentation comment (i.e. our ///). - if (targetMember == null || GetPrecedingDocumentationCommentCount(targetMember) != 1) - { - return null; - } + var lines = GetDocumentationCommentStubLines(targetMember, existingCommentText); + Debug.Assert(lines.Count > 2); - var line = text.Lines.GetLineFromPosition(documentationComment.FullSpan.Start); - if (line.IsEmptyOrWhitespace()) - { - return null; - } + AddLineBreaks(lines, options.NewLine); - var lines = GetDocumentationCommentStubLines(targetMember, existingCommentText); - Debug.Assert(lines.Count > 2); + // Shave off initial three slashes + lines[0] = lines[0][3..]; - AddLineBreaks(lines, options.NewLine); + return lines; + } - // Shave off initial three slashes - lines[0] = lines[0][3..]; + public bool IsValidTargetMember(SyntaxTree syntaxTree, SourceText text, int position, CancellationToken cancellationToken) + => GetTargetMember(syntaxTree, text, position, cancellationToken) != null; - return lines; + private TMemberNode? GetTargetMember(SyntaxTree syntaxTree, SourceText text, int position, CancellationToken cancellationToken) + { + var member = GetContainingMember(syntaxTree, position, cancellationToken); + if (member == null) + { + return null; } - public bool IsValidTargetMember(SyntaxTree syntaxTree, SourceText text, int position, CancellationToken cancellationToken) - => GetTargetMember(syntaxTree, text, position, cancellationToken) != null; + if (!SupportsDocumentationComments(member) || HasDocumentationComment(member)) + { + return null; + } - private TMemberNode? GetTargetMember(SyntaxTree syntaxTree, SourceText text, int position, CancellationToken cancellationToken) + var startPosition = member.GetFirstToken().SpanStart; + var line = text.Lines.GetLineFromPosition(startPosition); + var lineOffset = line.GetFirstNonWhitespaceOffset(); + if (!lineOffset.HasValue || line.Start + lineOffset.Value < startPosition) { - var member = GetContainingMember(syntaxTree, position, cancellationToken); - if (member == null) - { - return null; - } + return null; + } - if (!SupportsDocumentationComments(member) || HasDocumentationComment(member)) - { - return null; - } + return member; + } - var startPosition = member.GetFirstToken().SpanStart; - var line = text.Lines.GetLineFromPosition(startPosition); - var lineOffset = line.GetFirstNonWhitespaceOffset(); - if (!lineOffset.HasValue || line.Start + lineOffset.Value < startPosition) - { - return null; - } + private TMemberNode? GetTargetMember(TDocumentationComment documentationComment) + { + var targetMember = documentationComment.ParentTrivia.Token.GetAncestor(); - return member; + if (targetMember == null || !SupportsDocumentationComments(targetMember)) + { + return null; } - private TMemberNode? GetTargetMember(TDocumentationComment documentationComment) + if (targetMember.SpanStart < documentationComment.SpanStart) { - var targetMember = documentationComment.ParentTrivia.Token.GetAncestor(); - - if (targetMember == null || !SupportsDocumentationComments(targetMember)) - { - return null; - } + return null; + } - if (targetMember.SpanStart < documentationComment.SpanStart) - { - return null; - } + return targetMember; + } - return targetMember; + private static void AddLineBreaks(IList lines, string newLine) + { + for (var i = 0; i < lines.Count; i++) + { + lines[i] = lines[i] + newLine; } + } - private static void AddLineBreaks(IList lines, string newLine) + private static void IndentLines(List lines, string? indentText) + { + for (var i = 1; i < lines.Count; i++) { - for (var i = 0; i < lines.Count; i++) - { - lines[i] = lines[i] + newLine; - } + lines[i] = indentText + lines[i]; } + } + + public DocumentationCommentSnippet? GetDocumentationCommentSnippetOnEnterTyped(SyntaxTree syntaxTree, SourceText text, int position, in DocumentationCommentOptions options, CancellationToken cancellationToken) + { + // Don't attempt to generate a new XML doc comment on ENTER if the option to auto-generate + // them isn't set. Regardless of the option, we should generate exterior trivia (i.e. /// or ''') + // on ENTER inside an existing XML doc comment. - private static void IndentLines(List lines, string? indentText) + if (options.AutoXmlDocCommentGeneration) { - for (var i = 1; i < lines.Count; i++) + var result = GenerateDocumentationCommentAfterEnter(syntaxTree, text, position, options, cancellationToken); + if (result != null) { - lines[i] = indentText + lines[i]; + return result; } } - public DocumentationCommentSnippet? GetDocumentationCommentSnippetOnEnterTyped(SyntaxTree syntaxTree, SourceText text, int position, in DocumentationCommentOptions options, CancellationToken cancellationToken) - { - // Don't attempt to generate a new XML doc comment on ENTER if the option to auto-generate - // them isn't set. Regardless of the option, we should generate exterior trivia (i.e. /// or ''') - // on ENTER inside an existing XML doc comment. - - if (options.AutoXmlDocCommentGeneration) - { - var result = GenerateDocumentationCommentAfterEnter(syntaxTree, text, position, options, cancellationToken); - if (result != null) - { - return result; - } - } + return GenerateExteriorTriviaAfterEnter(syntaxTree, text, position, options, cancellationToken); + } - return GenerateExteriorTriviaAfterEnter(syntaxTree, text, position, options, cancellationToken); + private DocumentationCommentSnippet? GenerateDocumentationCommentAfterEnter(SyntaxTree syntaxTree, SourceText text, int position, in DocumentationCommentOptions options, CancellationToken cancellationToken) + { + // Find the documentation comment before the new line that was just pressed + var token = GetTokenToLeft(syntaxTree, position, cancellationToken); + if (!IsDocCommentNewLine(token)) + { + return null; } - private DocumentationCommentSnippet? GenerateDocumentationCommentAfterEnter(SyntaxTree syntaxTree, SourceText text, int position, in DocumentationCommentOptions options, CancellationToken cancellationToken) + var newLine = options.NewLine; + var lines = GetDocumentationCommentLines(token, text, options, out var indentText, out _, out _); + if (lines == null) { - // Find the documentation comment before the new line that was just pressed - var token = GetTokenToLeft(syntaxTree, position, cancellationToken); - if (!IsDocCommentNewLine(token)) - { - return null; - } - - var newLine = options.NewLine; - var lines = GetDocumentationCommentLines(token, text, options, out var indentText, out _, out _); - if (lines == null) - { - return null; - } - - var newText = string.Join(string.Empty, lines); - var offset = lines[0].Length + lines[1].Length - newLine.Length; + return null; + } - // Shave off final line break or add trailing indent if necessary - var trivia = syntaxTree.GetRoot(cancellationToken).FindTrivia(position, findInsideTrivia: false); - if (IsEndOfLineTrivia(trivia)) - { - newText = newText[..^newLine.Length]; - } - else - { - newText += indentText; - } + var newText = string.Join(string.Empty, lines); + var offset = lines[0].Length + lines[1].Length - newLine.Length; - var replaceSpan = token.Span; - var currentLine = text.Lines.GetLineFromPosition(position); - var currentLinePosition = currentLine.GetFirstNonWhitespacePosition(); - if (currentLinePosition.HasValue) - { - var start = token.Span.Start; - replaceSpan = new TextSpan(start, currentLinePosition.Value - start); - } + // Shave off final line break or add trailing indent if necessary + var trivia = syntaxTree.GetRoot(cancellationToken).FindTrivia(position, findInsideTrivia: false); + if (IsEndOfLineTrivia(trivia)) + { + newText = newText[..^newLine.Length]; + } + else + { + newText += indentText; + } - return new DocumentationCommentSnippet(replaceSpan, newText, offset); + var replaceSpan = token.Span; + var currentLine = text.Lines.GetLineFromPosition(position); + var currentLinePosition = currentLine.GetFirstNonWhitespacePosition(); + if (currentLinePosition.HasValue) + { + var start = token.Span.Start; + replaceSpan = new TextSpan(start, currentLinePosition.Value - start); } - public DocumentationCommentSnippet? GetDocumentationCommentSnippetOnCommandInvoke(SyntaxTree syntaxTree, SourceText text, int position, in DocumentationCommentOptions options, CancellationToken cancellationToken) + return new DocumentationCommentSnippet(replaceSpan, newText, offset); + } + + public DocumentationCommentSnippet? GetDocumentationCommentSnippetOnCommandInvoke(SyntaxTree syntaxTree, SourceText text, int position, in DocumentationCommentOptions options, CancellationToken cancellationToken) + { + var targetMember = GetTargetMember(syntaxTree, text, position, cancellationToken); + if (targetMember == null) { - var targetMember = GetTargetMember(syntaxTree, text, position, cancellationToken); - if (targetMember == null) - { - return null; - } + return null; + } - var token = targetMember.GetFirstToken(); - var startPosition = token.SpanStart; - var line = text.Lines.GetLineFromPosition(startPosition); - Debug.Assert(!line.IsEmptyOrWhitespace()); + var token = targetMember.GetFirstToken(); + var startPosition = token.SpanStart; + var line = text.Lines.GetLineFromPosition(startPosition); + Debug.Assert(!line.IsEmptyOrWhitespace()); - var lines = GetDocumentationCommentStubLines(targetMember, string.Empty); - Debug.Assert(lines.Count > 2); + var lines = GetDocumentationCommentStubLines(targetMember, string.Empty); + Debug.Assert(lines.Count > 2); - var newLine = options.NewLine; - AddLineBreaks(lines, newLine); + var newLine = options.NewLine; + AddLineBreaks(lines, newLine); - // Add indents - var lineOffset = line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(options.TabSize); - Debug.Assert(line.Start + lineOffset == startPosition); + // Add indents + var lineOffset = line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(options.TabSize); + Debug.Assert(line.Start + lineOffset == startPosition); - var indentText = lineOffset.CreateIndentationString(options.UseTabs, options.TabSize); - IndentLines(lines, indentText); + var indentText = lineOffset.CreateIndentationString(options.UseTabs, options.TabSize); + IndentLines(lines, indentText); - lines[^1] = lines[^1] + indentText; + lines[^1] = lines[^1] + indentText; - var comments = string.Join(string.Empty, lines); - var offset = lines[0].Length + lines[1].Length - newLine.Length; + var comments = string.Join(string.Empty, lines); + var offset = lines[0].Length + lines[1].Length - newLine.Length; - // For a command we don't replace a token, but insert before it - var replaceSpan = new TextSpan(token.Span.Start, 0); + // For a command we don't replace a token, but insert before it + var replaceSpan = new TextSpan(token.Span.Start, 0); - return new DocumentationCommentSnippet(replaceSpan, comments, offset); - } + return new DocumentationCommentSnippet(replaceSpan, comments, offset); + } - private DocumentationCommentSnippet? GenerateExteriorTriviaAfterEnter(SyntaxTree syntaxTree, SourceText text, int position, in DocumentationCommentOptions options, CancellationToken cancellationToken) + private DocumentationCommentSnippet? GenerateExteriorTriviaAfterEnter(SyntaxTree syntaxTree, SourceText text, int position, in DocumentationCommentOptions options, CancellationToken cancellationToken) + { + // Find the documentation comment before the new line that was just pressed + var token = GetTokenToLeft(syntaxTree, position, cancellationToken); + if (!IsDocCommentNewLine(token) && HasSkippedTrailingTrivia(token)) { - // Find the documentation comment before the new line that was just pressed - var token = GetTokenToLeft(syntaxTree, position, cancellationToken); - if (!IsDocCommentNewLine(token) && HasSkippedTrailingTrivia(token)) - { - // See PressingEnter_InsertSlashes11 for an example of - // a case where multiple skipped tokens trivia appear at the same position. - // In that case, we need to ask for the token from the next position over. - token = GetTokenToLeft(syntaxTree, position + 1, cancellationToken); - - if (!IsDocCommentNewLine(token)) - { - return null; - } - } - - var currentLine = text.Lines.GetLineFromPosition(position); - if (currentLine.LineNumber == 0) - { - return null; - } + // See PressingEnter_InsertSlashes11 for an example of + // a case where multiple skipped tokens trivia appear at the same position. + // In that case, we need to ask for the token from the next position over. + token = GetTokenToLeft(syntaxTree, position + 1, cancellationToken); - // Previous line must begin with a doc comment - var previousLine = text.Lines[currentLine.LineNumber - 1]; - var previousLineText = previousLine.ToString().Trim(); - if (!previousLineText.StartsWith(ExteriorTriviaText, StringComparison.Ordinal)) + if (!IsDocCommentNewLine(token)) { return null; } + } - var nextLineStartsWithDocComment = text.Lines.Count > currentLine.LineNumber + 1 && - text.Lines[currentLine.LineNumber + 1].ToString().Trim().StartsWith(ExteriorTriviaText, StringComparison.Ordinal); + var currentLine = text.Lines.GetLineFromPosition(position); + if (currentLine.LineNumber == 0) + { + return null; + } - // if previous line has only exterior trivia, current line is empty and next line doesn't begin - // with exterior trivia then stop inserting auto generated xml doc string - if (previousLineText.Equals(ExteriorTriviaText) && - string.IsNullOrWhiteSpace(currentLine.ToString()) && - !nextLineStartsWithDocComment) - { - return null; - } + // Previous line must begin with a doc comment + var previousLine = text.Lines[currentLine.LineNumber - 1]; + var previousLineText = previousLine.ToString().Trim(); + if (!previousLineText.StartsWith(ExteriorTriviaText, StringComparison.Ordinal)) + { + return null; + } - var documentationComment = token.GetAncestor(); - if (IsMultilineDocComment(documentationComment)) - { - return null; - } + var nextLineStartsWithDocComment = text.Lines.Count > currentLine.LineNumber + 1 && + text.Lines[currentLine.LineNumber + 1].ToString().Trim().StartsWith(ExteriorTriviaText, StringComparison.Ordinal); - if (EndsWithSingleExteriorTrivia(documentationComment) && currentLine.IsEmptyOrWhitespace() && !nextLineStartsWithDocComment) - { - return null; - } + // if previous line has only exterior trivia, current line is empty and next line doesn't begin + // with exterior trivia then stop inserting auto generated xml doc string + if (previousLineText.Equals(ExteriorTriviaText) && + string.IsNullOrWhiteSpace(currentLine.ToString()) && + !nextLineStartsWithDocComment) + { + return null; + } - return GetDocumentationCommentSnippetFromPreviousLine(options, currentLine, previousLine); + var documentationComment = token.GetAncestor(); + if (IsMultilineDocComment(documentationComment)) + { + return null; } - public DocumentationCommentSnippet GetDocumentationCommentSnippetFromPreviousLine(in DocumentationCommentOptions options, TextLine currentLine, TextLine previousLine) + if (EndsWithSingleExteriorTrivia(documentationComment) && currentLine.IsEmptyOrWhitespace() && !nextLineStartsWithDocComment) { - var insertionText = CreateInsertionTextFromPreviousLine(previousLine, options); + return null; + } - var firstNonWhitespaceOffset = currentLine.GetFirstNonWhitespaceOffset(); - var replaceSpan = firstNonWhitespaceOffset != null - ? TextSpan.FromBounds(currentLine.Start, currentLine.Start + firstNonWhitespaceOffset.Value) - : currentLine.Span; + return GetDocumentationCommentSnippetFromPreviousLine(options, currentLine, previousLine); + } - return new DocumentationCommentSnippet(replaceSpan, insertionText, insertionText.Length); - } + public DocumentationCommentSnippet GetDocumentationCommentSnippetFromPreviousLine(in DocumentationCommentOptions options, TextLine currentLine, TextLine previousLine) + { + var insertionText = CreateInsertionTextFromPreviousLine(previousLine, options); - private string CreateInsertionTextFromPreviousLine(TextLine previousLine, in DocumentationCommentOptions options) - { - var previousLineText = previousLine.ToString(); - var firstNonWhitespaceColumn = previousLineText.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(options.TabSize); + var firstNonWhitespaceOffset = currentLine.GetFirstNonWhitespaceOffset(); + var replaceSpan = firstNonWhitespaceOffset != null + ? TextSpan.FromBounds(currentLine.Start, currentLine.Start + firstNonWhitespaceOffset.Value) + : currentLine.Span; - var trimmedPreviousLine = previousLineText.Trim(); - Debug.Assert(trimmedPreviousLine.StartsWith(ExteriorTriviaText), "Unexpected: previous line does not begin with doc comment exterior trivia."); + return new DocumentationCommentSnippet(replaceSpan, insertionText, insertionText.Length); + } - // skip exterior trivia. - trimmedPreviousLine = trimmedPreviousLine[3..]; + private string CreateInsertionTextFromPreviousLine(TextLine previousLine, in DocumentationCommentOptions options) + { + var previousLineText = previousLine.ToString(); + var firstNonWhitespaceColumn = previousLineText.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(options.TabSize); - var firstNonWhitespaceOffsetInPreviousXmlText = trimmedPreviousLine.GetFirstNonWhitespaceOffset(); + var trimmedPreviousLine = previousLineText.Trim(); + Debug.Assert(trimmedPreviousLine.StartsWith(ExteriorTriviaText), "Unexpected: previous line does not begin with doc comment exterior trivia."); - var extraIndent = firstNonWhitespaceOffsetInPreviousXmlText != null - ? trimmedPreviousLine[..firstNonWhitespaceOffsetInPreviousXmlText.Value] - : " "; + // skip exterior trivia. + trimmedPreviousLine = trimmedPreviousLine[3..]; - return firstNonWhitespaceColumn.CreateIndentationString(options.UseTabs, options.TabSize) + ExteriorTriviaText + extraIndent; - } + var firstNonWhitespaceOffsetInPreviousXmlText = trimmedPreviousLine.GetFirstNonWhitespaceOffset(); + + var extraIndent = firstNonWhitespaceOffsetInPreviousXmlText != null + ? trimmedPreviousLine[..firstNonWhitespaceOffsetInPreviousXmlText.Value] + : " "; + + return firstNonWhitespaceColumn.CreateIndentationString(options.UseTabs, options.TabSize) + ExteriorTriviaText + extraIndent; } } diff --git a/src/Features/Core/Portable/DocumentationComments/DocumentationCommentSnippet.cs b/src/Features/Core/Portable/DocumentationComments/DocumentationCommentSnippet.cs index 02408c5b3ecd6..9d27a3927db17 100644 --- a/src/Features/Core/Portable/DocumentationComments/DocumentationCommentSnippet.cs +++ b/src/Features/Core/Portable/DocumentationComments/DocumentationCommentSnippet.cs @@ -4,30 +4,29 @@ using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.DocumentationComments +namespace Microsoft.CodeAnalysis.DocumentationComments; + +internal class DocumentationCommentSnippet { - internal class DocumentationCommentSnippet - { - /// - /// The span in the original text that should be replaced with the documentation comment. - /// - public TextSpan SpanToReplace { get; } + /// + /// The span in the original text that should be replaced with the documentation comment. + /// + public TextSpan SpanToReplace { get; } - /// - /// The documentation comment text to replace the span with - /// - public string SnippetText { get; } + /// + /// The documentation comment text to replace the span with + /// + public string SnippetText { get; } - /// - /// The offset within where the caret should be positioned after replacement - /// - public int CaretOffset { get; } + /// + /// The offset within where the caret should be positioned after replacement + /// + public int CaretOffset { get; } - internal DocumentationCommentSnippet(TextSpan spanToReplace, string snippetText, int caretOffset) - { - SpanToReplace = spanToReplace; - SnippetText = snippetText; - CaretOffset = caretOffset; - } + internal DocumentationCommentSnippet(TextSpan spanToReplace, string snippetText, int caretOffset) + { + SpanToReplace = spanToReplace; + SnippetText = snippetText; + CaretOffset = caretOffset; } } diff --git a/src/Features/Core/Portable/DocumentationComments/IDocumentationCommentFormattingService.cs b/src/Features/Core/Portable/DocumentationComments/IDocumentationCommentFormattingService.cs index e0fa6d0a7bdb7..6a24d4c9dedaf 100644 --- a/src/Features/Core/Portable/DocumentationComments/IDocumentationCommentFormattingService.cs +++ b/src/Features/Core/Portable/DocumentationComments/IDocumentationCommentFormattingService.cs @@ -6,11 +6,10 @@ using System.Threading; using Microsoft.CodeAnalysis.Host; -namespace Microsoft.CodeAnalysis.DocumentationComments +namespace Microsoft.CodeAnalysis.DocumentationComments; + +internal interface IDocumentationCommentFormattingService : ILanguageService { - internal interface IDocumentationCommentFormattingService : ILanguageService - { - string? Format(string? rawXmlText, Compilation? compilation = null); - ImmutableArray Format(string? rawXmlText, ISymbol symbol, SemanticModel semanticModel, int position, SymbolDisplayFormat format, CancellationToken cancellationToken); - } + string? Format(string? rawXmlText, Compilation? compilation = null); + ImmutableArray Format(string? rawXmlText, ISymbol symbol, SemanticModel semanticModel, int position, SymbolDisplayFormat format, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/DocumentationComments/IDocumentationCommentSnippetService.cs b/src/Features/Core/Portable/DocumentationComments/IDocumentationCommentSnippetService.cs index a38e9f57b7374..a5adede4653ff 100644 --- a/src/Features/Core/Portable/DocumentationComments/IDocumentationCommentSnippetService.cs +++ b/src/Features/Core/Portable/DocumentationComments/IDocumentationCommentSnippetService.cs @@ -6,42 +6,41 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.DocumentationComments +namespace Microsoft.CodeAnalysis.DocumentationComments; + +internal interface IDocumentationCommentSnippetService : ILanguageService { - internal interface IDocumentationCommentSnippetService : ILanguageService - { - /// - /// A single character string indicating what the comment character is for the documentation comments - /// - string DocumentationCommentCharacter { get; } - - DocumentationCommentSnippet? GetDocumentationCommentSnippetOnCharacterTyped( - SyntaxTree syntaxTree, - SourceText text, - int position, - in DocumentationCommentOptions options, - CancellationToken cancellationToken, - bool addIndentation = true); - - DocumentationCommentSnippet? GetDocumentationCommentSnippetOnCommandInvoke( - SyntaxTree syntaxTree, - SourceText text, - int position, - in DocumentationCommentOptions options, - CancellationToken cancellationToken); - - DocumentationCommentSnippet? GetDocumentationCommentSnippetOnEnterTyped( - SyntaxTree syntaxTree, - SourceText text, - int position, - in DocumentationCommentOptions options, - CancellationToken cancellationToken); - - DocumentationCommentSnippet? GetDocumentationCommentSnippetFromPreviousLine( - in DocumentationCommentOptions options, - TextLine currentLine, - TextLine previousLine); - - bool IsValidTargetMember(SyntaxTree syntaxTree, SourceText text, int caretPosition, CancellationToken cancellationToken); - } + /// + /// A single character string indicating what the comment character is for the documentation comments + /// + string DocumentationCommentCharacter { get; } + + DocumentationCommentSnippet? GetDocumentationCommentSnippetOnCharacterTyped( + SyntaxTree syntaxTree, + SourceText text, + int position, + in DocumentationCommentOptions options, + CancellationToken cancellationToken, + bool addIndentation = true); + + DocumentationCommentSnippet? GetDocumentationCommentSnippetOnCommandInvoke( + SyntaxTree syntaxTree, + SourceText text, + int position, + in DocumentationCommentOptions options, + CancellationToken cancellationToken); + + DocumentationCommentSnippet? GetDocumentationCommentSnippetOnEnterTyped( + SyntaxTree syntaxTree, + SourceText text, + int position, + in DocumentationCommentOptions options, + CancellationToken cancellationToken); + + DocumentationCommentSnippet? GetDocumentationCommentSnippetFromPreviousLine( + in DocumentationCommentOptions options, + TextLine currentLine, + TextLine previousLine); + + bool IsValidTargetMember(SyntaxTree syntaxTree, SourceText text, int caretPosition, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs index a3cf6aacac834..a903b17faaeb8 100644 --- a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs +++ b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs @@ -24,2598 +24,2564 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal abstract class AbstractEditAndContinueAnalyzer : IEditAndContinueAnalyzer { - internal abstract class AbstractEditAndContinueAnalyzer : IEditAndContinueAnalyzer + internal const int DefaultStatementPart = 0; + private const string CreateNewOnMetadataUpdateAttributeName = "CreateNewOnMetadataUpdateAttribute"; + + /// + /// Contains enough information to determine whether two symbols have the same signature. + /// + private static readonly SymbolDisplayFormat s_unqualifiedMemberDisplayFormat = + new( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, + propertyStyle: SymbolDisplayPropertyStyle.NameOnly, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeVariance, + memberOptions: + SymbolDisplayMemberOptions.IncludeParameters | + SymbolDisplayMemberOptions.IncludeExplicitInterface, + parameterOptions: + SymbolDisplayParameterOptions.IncludeParamsRefOut | + SymbolDisplayParameterOptions.IncludeExtensionThis | + SymbolDisplayParameterOptions.IncludeType | + SymbolDisplayParameterOptions.IncludeName, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + private static readonly SymbolDisplayFormat s_fullyQualifiedMemberDisplayFormat = + new( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + propertyStyle: SymbolDisplayPropertyStyle.NameOnly, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeVariance, + memberOptions: + SymbolDisplayMemberOptions.IncludeParameters | + SymbolDisplayMemberOptions.IncludeExplicitInterface | + SymbolDisplayMemberOptions.IncludeContainingType, + parameterOptions: + SymbolDisplayParameterOptions.IncludeParamsRefOut | + SymbolDisplayParameterOptions.IncludeExtensionThis | + SymbolDisplayParameterOptions.IncludeType, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + // used by tests to validate correct handlign of unexpected exceptions + private readonly Action? _testFaultInjector; + + protected AbstractEditAndContinueAnalyzer(Action? testFaultInjector) { - internal const int DefaultStatementPart = 0; - private const string CreateNewOnMetadataUpdateAttributeName = "CreateNewOnMetadataUpdateAttribute"; - - /// - /// Contains enough information to determine whether two symbols have the same signature. - /// - private static readonly SymbolDisplayFormat s_unqualifiedMemberDisplayFormat = - new( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, - propertyStyle: SymbolDisplayPropertyStyle.NameOnly, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeVariance, - memberOptions: - SymbolDisplayMemberOptions.IncludeParameters | - SymbolDisplayMemberOptions.IncludeExplicitInterface, - parameterOptions: - SymbolDisplayParameterOptions.IncludeParamsRefOut | - SymbolDisplayParameterOptions.IncludeExtensionThis | - SymbolDisplayParameterOptions.IncludeType | - SymbolDisplayParameterOptions.IncludeName, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - private static readonly SymbolDisplayFormat s_fullyQualifiedMemberDisplayFormat = - new( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - propertyStyle: SymbolDisplayPropertyStyle.NameOnly, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeVariance, - memberOptions: - SymbolDisplayMemberOptions.IncludeParameters | - SymbolDisplayMemberOptions.IncludeExplicitInterface | - SymbolDisplayMemberOptions.IncludeContainingType, - parameterOptions: - SymbolDisplayParameterOptions.IncludeParamsRefOut | - SymbolDisplayParameterOptions.IncludeExtensionThis | - SymbolDisplayParameterOptions.IncludeType, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - // used by tests to validate correct handlign of unexpected exceptions - private readonly Action? _testFaultInjector; - - protected AbstractEditAndContinueAnalyzer(Action? testFaultInjector) - { - _testFaultInjector = testFaultInjector; - } - - private static TraceLog Log - => EditAndContinueService.AnalysisLog; - - internal abstract bool ExperimentalFeaturesEnabled(SyntaxTree tree); + _testFaultInjector = testFaultInjector; + } - /// - /// Finds member declaration node(s) containing given , if any. - /// - /// Span used to disambiguate member declarations if there are multiple applicable ones based on . - /// - /// The implementation has to decide what kinds of nodes in top-level match relationship represent a declaration. - /// Every member declaration must be represented by exactly one node, but not all nodes have to represent a declaration. - /// - /// May return multiple declarations if the specified belongs to bodies of multiple declarations, - /// such as in VB Dim a, b As New T case when is e.g. T. - /// - internal abstract bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode node, TextSpan activeSpan, out OneOrMany declarations); + private static TraceLog Log + => EditAndContinueService.AnalysisLog; + + internal abstract bool ExperimentalFeaturesEnabled(SyntaxTree tree); + + /// + /// Finds member declaration node(s) containing given , if any. + /// + /// Span used to disambiguate member declarations if there are multiple applicable ones based on . + /// + /// The implementation has to decide what kinds of nodes in top-level match relationship represent a declaration. + /// Every member declaration must be represented by exactly one node, but not all nodes have to represent a declaration. + /// + /// May return multiple declarations if the specified belongs to bodies of multiple declarations, + /// such as in VB Dim a, b As New T case when is e.g. T. + /// + internal abstract bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode node, TextSpan activeSpan, out OneOrMany declarations); + + /// + /// If the specified represents a member declaration returns an object that represents its body. + /// + /// + /// If specified then the returned body must belong to this symbol. + /// + /// node itself may represent a that doesn't belong to the . + /// E.g. a record copy-constructor declaration is represented by the record type declaration node, + /// but this node also represents the record symbol itself. + /// + /// + /// Null for nodes that don't represent declarations. + /// + internal abstract MemberBody? TryGetDeclarationBody(SyntaxNode node, ISymbol? symbol); + + /// + /// True if the specified node shares body with another declaration. + /// + internal abstract bool IsDeclarationWithSharedBody(SyntaxNode declaration, ISymbol member); + + /// + /// Returns a node that represents a body of a lambda containing specified , + /// or null if the node isn't contained in a lambda. If a node is returned it must uniquely represent the lambda, + /// i.e. be no two distinct nodes may represent the same lambda. + /// + protected abstract LambdaBody? FindEnclosingLambdaBody(SyntaxNode encompassingAncestor, SyntaxNode node); + + protected abstract Match ComputeTopLevelMatch(SyntaxNode oldCompilationUnit, SyntaxNode newCompilationUnit); + protected abstract BidirectionalMap? ComputeParameterMap(SyntaxNode oldDeclaration, SyntaxNode newDeclaration); + protected abstract IEnumerable GetSyntaxSequenceEdits(ImmutableArray oldNodes, ImmutableArray newNodes); + + protected abstract bool TryGetEnclosingBreakpointSpan(SyntaxToken token, out TextSpan span); + + /// + /// Get the active span that corresponds to specified node (or its part). + /// + /// + /// In case there are multiple breakpoint spans starting at the of the , + /// can be used to disambiguate between them. + /// The inner-most available span whose length is at least is returned. + /// + /// + /// might have multiple active statement span. is used to identify the + /// specific part. + /// + /// + /// True if the node has an active span associated with it, false otherwise. + /// + protected abstract bool TryGetActiveSpan(SyntaxNode node, int statementPart, int minLength, out TextSpan span); + + /// + /// Yields potential active statements around the specified active statement + /// starting with siblings following the statement, then preceding the statement, follows with its parent, its following siblings, etc. + /// + /// + /// Pairs of (node, statement part), or (node, -1) indicating there is no logical following statement. + /// The enumeration continues until the root is reached. + /// + protected abstract IEnumerable<(SyntaxNode statement, int statementPart)> EnumerateNearStatements(SyntaxNode statement); + + protected abstract bool StatementLabelEquals(SyntaxNode node1, SyntaxNode node2); + + /// + /// Returns true if the code emitted for the old active statement part ( of ) + /// is the same as the code emitted for the corresponding new active statement part ( of ). + /// + /// + /// A rude edit is reported if an active statement is changed and this method returns true. + /// + protected abstract bool AreEquivalentActiveStatements(SyntaxNode oldStatement, SyntaxNode newStatement, int statementPart); + + protected abstract bool AreEquivalentImpl(SyntaxToken oldToken, SyntaxToken newToken); + + /// + /// Determines if two syntax tokens are the same, disregarding trivia differences. + /// + private bool AreEquivalent(SyntaxToken oldToken, SyntaxToken newToken) + => oldToken.RawKind == newToken.RawKind && oldToken.Span.Length == newToken.Span.Length && AreEquivalentImpl(oldToken, newToken); + + protected abstract bool IsNamespaceDeclaration(SyntaxNode node); + protected abstract bool IsCompilationUnitWithGlobalStatements(SyntaxNode node); + protected abstract bool IsGlobalStatement(SyntaxNode node); + + /// + /// Returns all top-level type declarations (non-nested) for a given compilation unit node. + /// + protected abstract IEnumerable GetTopLevelTypeDeclarations(SyntaxNode compilationUnit); + + /// + /// Returns all symbols with declaring syntax ( must return a syntax node) + /// associated with an edit and an actual edit kind, which may be different then the specified one. + /// Returns an empty set if the edit is not associated with any symbols. + /// + protected abstract void AddSymbolEdits( + ref TemporaryArray<(ISymbol?, ISymbol?, EditKind)> result, + EditKind editKind, + SyntaxNode? oldNode, + ISymbol? oldSymbol, + SyntaxNode? newNode, + ISymbol? newSymbol, + SemanticModel? oldModel, + SemanticModel newModel, + Match topMatch, + IReadOnlyDictionary editMap, + SymbolInfoCache symbolCache, + CancellationToken cancellationToken); + + /// + /// Returns pairs of old and new symbols associated with a given syntactic edit. + /// + protected abstract OneOrMany<(ISymbol? oldSymbol, ISymbol? newSymbol)> GetEditedSymbols( + EditKind editKind, + SyntaxNode? oldNode, + SyntaxNode? newNode, + SemanticModel? oldModel, + SemanticModel newModel, + CancellationToken cancellationToken); + + private OneOrMany<(ISymbol? oldSymbol, ISymbol? newSymbol, EditKind editKind)> GetSymbolEdits( + EditKind editKind, + SyntaxNode? oldNode, + SyntaxNode? newNode, + SemanticModel? oldModel, + SemanticModel newModel, + Match topMatch, + IReadOnlyDictionary editMap, + SymbolInfoCache symbolCache, + CancellationToken cancellationToken) + { + var result = new TemporaryArray<(ISymbol?, ISymbol?, EditKind)>(); - /// - /// If the specified represents a member declaration returns an object that represents its body. - /// - /// - /// If specified then the returned body must belong to this symbol. - /// - /// node itself may represent a that doesn't belong to the . - /// E.g. a record copy-constructor declaration is represented by the record type declaration node, - /// but this node also represents the record symbol itself. - /// - /// - /// Null for nodes that don't represent declarations. - /// - internal abstract MemberBody? TryGetDeclarationBody(SyntaxNode node, ISymbol? symbol); + var symbols = GetEditedSymbols(editKind, oldNode, newNode, oldModel, newModel, cancellationToken); + foreach (var (oldSymbol, newSymbol) in symbols) + { + Debug.Assert(oldSymbol != null || newSymbol != null); - /// - /// True if the specified node shares body with another declaration. - /// - internal abstract bool IsDeclarationWithSharedBody(SyntaxNode declaration, ISymbol member); + // Top-level members may be matched by syntax even when their name and signature are different. + // An update edit is created for these matches that usually produces an insert + delete semantic edits. + // + // If however the members were just moved to another partial type declaration and now are "accidentally" syntax-matched, + // we shouldn't treat such update as insert and delete. + // + // Instead, a simple semantic update (or no edit at all if the body has not changed, other then trivia that can be expressed as a line delta) should be created. + // When we detect this case we break the original update edit into two edits: delete and insert. + // The logic in the analyzer then consolidates these edits into updates across partial type declarations if applicable. + if (editKind == EditKind.Update && GetSemanticallyMatchingNewSymbol(oldSymbol, newSymbol, newModel, symbolCache, cancellationToken) != null) + { + AddSymbolEdits(ref result, EditKind.Delete, oldNode, oldSymbol, newNode: null, newSymbol: null, oldModel, newModel, topMatch, editMap, symbolCache, cancellationToken); + AddSymbolEdits(ref result, EditKind.Insert, oldNode: null, oldSymbol: null, newNode, newSymbol, oldModel, newModel, topMatch, editMap, symbolCache, cancellationToken); + } + else + { + AddSymbolEdits(ref result, editKind, oldNode, oldSymbol, newNode, newSymbol, oldModel, newModel, topMatch, editMap, symbolCache, cancellationToken); + } + } - /// - /// Returns a node that represents a body of a lambda containing specified , - /// or null if the node isn't contained in a lambda. If a node is returned it must uniquely represent the lambda, - /// i.e. be no two distinct nodes may represent the same lambda. - /// - protected abstract LambdaBody? FindEnclosingLambdaBody(SyntaxNode encompassingAncestor, SyntaxNode node); + return result.Count switch + { + 0 => OneOrMany<(ISymbol?, ISymbol?, EditKind)>.Empty, + 1 => OneOrMany.Create(result[0]), + _ => OneOrMany.Create(result.ToImmutableAndClear()) + }; + } - protected abstract Match ComputeTopLevelMatch(SyntaxNode oldCompilationUnit, SyntaxNode newCompilationUnit); - protected abstract BidirectionalMap? ComputeParameterMap(SyntaxNode oldDeclaration, SyntaxNode newDeclaration); - protected abstract IEnumerable GetSyntaxSequenceEdits(ImmutableArray oldNodes, ImmutableArray newNodes); + /// + /// Enumerates all use sites of a specified variable within the specified syntax subtrees. + /// + protected abstract IEnumerable GetVariableUseSites(IEnumerable roots, ISymbol localOrParameter, SemanticModel model, CancellationToken cancellationToken); - protected abstract bool TryGetEnclosingBreakpointSpan(SyntaxToken token, out TextSpan span); + protected abstract bool AreHandledEventsEqual(IMethodSymbol oldMethod, IMethodSymbol newMethod); - /// - /// Get the active span that corresponds to specified node (or its part). - /// - /// - /// In case there are multiple breakpoint spans starting at the of the , - /// can be used to disambiguate between them. - /// The inner-most available span whose length is at least is returned. - /// - /// - /// might have multiple active statement span. is used to identify the - /// specific part. - /// - /// - /// True if the node has an active span associated with it, false otherwise. - /// - protected abstract bool TryGetActiveSpan(SyntaxNode node, int statementPart, int minLength, out TextSpan span); + // diagnostic spans: + protected abstract TextSpan? TryGetDiagnosticSpan(SyntaxNode node, EditKind editKind); - /// - /// Yields potential active statements around the specified active statement - /// starting with siblings following the statement, then preceding the statement, follows with its parent, its following siblings, etc. - /// - /// - /// Pairs of (node, statement part), or (node, -1) indicating there is no logical following statement. - /// The enumeration continues until the root is reached. - /// - protected abstract IEnumerable<(SyntaxNode statement, int statementPart)> EnumerateNearStatements(SyntaxNode statement); + internal TextSpan GetDiagnosticSpan(SyntaxNode node, EditKind editKind) + => TryGetDiagnosticSpan(node, editKind) ?? node.Span; - protected abstract bool StatementLabelEquals(SyntaxNode node1, SyntaxNode node2); + protected virtual TextSpan GetBodyDiagnosticSpan(SyntaxNode node, EditKind editKind) + { + var current = node.Parent; + while (true) + { + if (current == null) + { + return node.Span; + } - /// - /// Returns true if the code emitted for the old active statement part ( of ) - /// is the same as the code emitted for the corresponding new active statement part ( of ). - /// - /// - /// A rude edit is reported if an active statement is changed and this method returns true. - /// - protected abstract bool AreEquivalentActiveStatements(SyntaxNode oldStatement, SyntaxNode newStatement, int statementPart); + var span = TryGetDiagnosticSpan(current, editKind); + if (span != null) + { + return span.Value; + } - protected abstract bool AreEquivalentImpl(SyntaxToken oldToken, SyntaxToken newToken); + current = current.Parent; + } + } - /// - /// Determines if two syntax tokens are the same, disregarding trivia differences. - /// - private bool AreEquivalent(SyntaxToken oldToken, SyntaxToken newToken) - => oldToken.RawKind == newToken.RawKind && oldToken.Span.Length == newToken.Span.Length && AreEquivalentImpl(oldToken, newToken); + internal abstract TextSpan GetLambdaParameterDiagnosticSpan(SyntaxNode lambda, int ordinal); - protected abstract bool IsNamespaceDeclaration(SyntaxNode node); - protected abstract bool IsCompilationUnitWithGlobalStatements(SyntaxNode node); - protected abstract bool IsGlobalStatement(SyntaxNode node); + // display names: - /// - /// Returns all top-level type declarations (non-nested) for a given compilation unit node. - /// - protected abstract IEnumerable GetTopLevelTypeDeclarations(SyntaxNode compilationUnit); + internal string GetDisplayKindAndName(ISymbol symbol, string? displayKind = null, bool fullyQualify = false) + { + displayKind ??= GetDisplayKind(symbol); + var format = fullyQualify ? s_fullyQualifiedMemberDisplayFormat : s_unqualifiedMemberDisplayFormat; + + return (symbol is IParameterSymbol { ContainingType: not { TypeKind: TypeKind.Delegate } }) + ? string.Format( + FeaturesResources.symbol_kind_and_name_of_member_kind_and_name, + displayKind, + symbol.Name, + GetDisplayKind(symbol.ContainingSymbol), + symbol.ContainingSymbol.ToDisplayString(format)) + : string.Format( + FeaturesResources.member_kind_and_name, + displayKind, + symbol.ToDisplayString(format)); + } - /// - /// Returns all symbols with declaring syntax ( must return a syntax node) - /// associated with an edit and an actual edit kind, which may be different then the specified one. - /// Returns an empty set if the edit is not associated with any symbols. - /// - protected abstract void AddSymbolEdits( - ref TemporaryArray<(ISymbol?, ISymbol?, EditKind)> result, - EditKind editKind, - SyntaxNode? oldNode, - ISymbol? oldSymbol, - SyntaxNode? newNode, - ISymbol? newSymbol, - SemanticModel? oldModel, - SemanticModel newModel, - Match topMatch, - IReadOnlyDictionary editMap, - SymbolInfoCache symbolCache, - CancellationToken cancellationToken); + internal string GetDisplayName(SyntaxNode node, EditKind editKind = EditKind.Update) + => TryGetDisplayName(node, editKind) ?? throw ExceptionUtilities.UnexpectedValue(node.GetType().Name); - /// - /// Returns pairs of old and new symbols associated with a given syntactic edit. - /// - protected abstract OneOrMany<(ISymbol? oldSymbol, ISymbol? newSymbol)> GetEditedSymbols( - EditKind editKind, - SyntaxNode? oldNode, - SyntaxNode? newNode, - SemanticModel? oldModel, - SemanticModel newModel, - CancellationToken cancellationToken); - - private OneOrMany<(ISymbol? oldSymbol, ISymbol? newSymbol, EditKind editKind)> GetSymbolEdits( - EditKind editKind, - SyntaxNode? oldNode, - SyntaxNode? newNode, - SemanticModel? oldModel, - SemanticModel newModel, - Match topMatch, - IReadOnlyDictionary editMap, - SymbolInfoCache symbolCache, - CancellationToken cancellationToken) - { - var result = new TemporaryArray<(ISymbol?, ISymbol?, EditKind)>(); - - var symbols = GetEditedSymbols(editKind, oldNode, newNode, oldModel, newModel, cancellationToken); - foreach (var (oldSymbol, newSymbol) in symbols) - { - Debug.Assert(oldSymbol != null || newSymbol != null); - - // Top-level members may be matched by syntax even when their name and signature are different. - // An update edit is created for these matches that usually produces an insert + delete semantic edits. - // - // If however the members were just moved to another partial type declaration and now are "accidentally" syntax-matched, - // we shouldn't treat such update as insert and delete. - // - // Instead, a simple semantic update (or no edit at all if the body has not changed, other then trivia that can be expressed as a line delta) should be created. - // When we detect this case we break the original update edit into two edits: delete and insert. - // The logic in the analyzer then consolidates these edits into updates across partial type declarations if applicable. - if (editKind == EditKind.Update && GetSemanticallyMatchingNewSymbol(oldSymbol, newSymbol, newModel, symbolCache, cancellationToken) != null) - { - AddSymbolEdits(ref result, EditKind.Delete, oldNode, oldSymbol, newNode: null, newSymbol: null, oldModel, newModel, topMatch, editMap, symbolCache, cancellationToken); - AddSymbolEdits(ref result, EditKind.Insert, oldNode: null, oldSymbol: null, newNode, newSymbol, oldModel, newModel, topMatch, editMap, symbolCache, cancellationToken); - } - else - { - AddSymbolEdits(ref result, editKind, oldNode, oldSymbol, newNode, newSymbol, oldModel, newModel, topMatch, editMap, symbolCache, cancellationToken); - } - } + internal string GetDisplayKind(ISymbol symbol) + => symbol.Kind switch + { + SymbolKind.Event => GetDisplayName((IEventSymbol)symbol), + SymbolKind.Field => GetDisplayName((IFieldSymbol)symbol), + SymbolKind.Method => GetDisplayName((IMethodSymbol)symbol), + SymbolKind.NamedType => GetDisplayName((INamedTypeSymbol)symbol), + SymbolKind.Parameter => FeaturesResources.parameter, + SymbolKind.Local => FeaturesResources.local_variable, + SymbolKind.Property => GetDisplayName((IPropertySymbol)symbol), + SymbolKind.TypeParameter => FeaturesResources.type_parameter, + _ => throw ExceptionUtilities.UnexpectedValue(symbol.Kind) + }; + + internal virtual string GetDisplayName(IEventSymbol symbol) + => FeaturesResources.event_; + + internal virtual string GetDisplayName(IPropertySymbol symbol) + => symbol.IsAutoProperty() ? FeaturesResources.auto_property : FeaturesResources.property_; + + internal virtual string GetDisplayName(INamedTypeSymbol symbol) + => symbol.TypeKind switch + { + TypeKind.Class => FeaturesResources.class_, + TypeKind.Interface => FeaturesResources.interface_, + TypeKind.Delegate => FeaturesResources.delegate_, + TypeKind.Enum => FeaturesResources.enum_, + TypeKind.TypeParameter => FeaturesResources.type_parameter, + _ => FeaturesResources.type, + }; + + internal virtual string GetDisplayName(IFieldSymbol symbol) + => symbol.IsConst ? ((symbol.ContainingType.TypeKind == TypeKind.Enum) ? FeaturesResources.enum_value : FeaturesResources.const_field) : + FeaturesResources.field; + + internal virtual string GetDisplayName(IMethodSymbol symbol) + => symbol.MethodKind switch + { + MethodKind.Constructor => FeaturesResources.constructor, + MethodKind.PropertyGet or MethodKind.PropertySet => FeaturesResources.property_accessor, + MethodKind.EventAdd or MethodKind.EventRaise or MethodKind.EventRemove => FeaturesResources.event_accessor, + MethodKind.BuiltinOperator or MethodKind.UserDefinedOperator or MethodKind.Conversion => FeaturesResources.operator_, + _ => FeaturesResources.method, + }; + + /// + /// Returns the display name of an ancestor node that contains the specified node and has a display name. + /// + protected virtual string GetBodyDisplayName(SyntaxNode node, EditKind editKind = EditKind.Update) + { + var current = node.Parent; - return result.Count switch + if (current == null) + { + var displayName = TryGetDisplayName(node, editKind); + if (displayName != null) { - 0 => OneOrMany<(ISymbol?, ISymbol?, EditKind)>.Empty, - 1 => OneOrMany.Create(result[0]), - _ => OneOrMany.Create(result.ToImmutableAndClear()) - }; + return displayName; + } } - /// - /// Enumerates all use sites of a specified variable within the specified syntax subtrees. - /// - protected abstract IEnumerable GetVariableUseSites(IEnumerable roots, ISymbol localOrParameter, SemanticModel model, CancellationToken cancellationToken); - - protected abstract bool AreHandledEventsEqual(IMethodSymbol oldMethod, IMethodSymbol newMethod); - - // diagnostic spans: - protected abstract TextSpan? TryGetDiagnosticSpan(SyntaxNode node, EditKind editKind); - - internal TextSpan GetDiagnosticSpan(SyntaxNode node, EditKind editKind) - => TryGetDiagnosticSpan(node, editKind) ?? node.Span; - - protected virtual TextSpan GetBodyDiagnosticSpan(SyntaxNode node, EditKind editKind) + while (true) { - var current = node.Parent; - while (true) + if (current == null) { - if (current == null) - { - return node.Span; - } - - var span = TryGetDiagnosticSpan(current, editKind); - if (span != null) - { - return span.Value; - } + throw ExceptionUtilities.UnexpectedValue(node.GetType().Name); + } - current = current.Parent; + var displayName = TryGetDisplayName(current, editKind); + if (displayName != null) + { + return displayName; } + + current = current.Parent; } + } - internal abstract TextSpan GetLambdaParameterDiagnosticSpan(SyntaxNode lambda, int ordinal); + protected abstract string? TryGetDisplayName(SyntaxNode node, EditKind editKind); - // display names: + protected virtual string GetSuspensionPointDisplayName(SyntaxNode node, EditKind editKind) + => GetDisplayName(node, editKind); - internal string GetDisplayKindAndName(ISymbol symbol, string? displayKind = null, bool fullyQualify = false) - { - displayKind ??= GetDisplayKind(symbol); - var format = fullyQualify ? s_fullyQualifiedMemberDisplayFormat : s_unqualifiedMemberDisplayFormat; + protected abstract string LineDirectiveKeyword { get; } + protected abstract ushort LineDirectiveSyntaxKind { get; } + protected abstract SymbolDisplayFormat ErrorDisplayFormat { get; } + protected abstract List GetExceptionHandlingAncestors(SyntaxNode node, SyntaxNode root, bool isNonLeaf); + protected abstract TextSpan GetExceptionHandlingRegion(SyntaxNode node, out bool coversAllChildren); - return (symbol is IParameterSymbol { ContainingType: not { TypeKind: TypeKind.Delegate } }) - ? string.Format( - FeaturesResources.symbol_kind_and_name_of_member_kind_and_name, - displayKind, - symbol.Name, - GetDisplayKind(symbol.ContainingSymbol), - symbol.ContainingSymbol.ToDisplayString(format)) - : string.Format( - FeaturesResources.member_kind_and_name, - displayKind, - symbol.ToDisplayString(format)); - } + internal abstract void ReportTopLevelSyntacticRudeEdits(ArrayBuilder diagnostics, Match match, Edit edit, Dictionary editMap); + internal abstract void ReportEnclosingExceptionHandlingRudeEdits(ArrayBuilder diagnostics, IEnumerable> exceptionHandlingEdits, SyntaxNode oldStatement, TextSpan newStatementSpan); - internal string GetDisplayName(SyntaxNode node, EditKind editKind = EditKind.Update) - => TryGetDisplayName(node, editKind) ?? throw ExceptionUtilities.UnexpectedValue(node.GetType().Name); + internal abstract bool HasUnsupportedOperation(IEnumerable newNodes, [NotNullWhen(true)] out SyntaxNode? unsupportedNode, out RudeEditKind rudeEdit); - internal string GetDisplayKind(ISymbol symbol) - => symbol.Kind switch - { - SymbolKind.Event => GetDisplayName((IEventSymbol)symbol), - SymbolKind.Field => GetDisplayName((IFieldSymbol)symbol), - SymbolKind.Method => GetDisplayName((IMethodSymbol)symbol), - SymbolKind.NamedType => GetDisplayName((INamedTypeSymbol)symbol), - SymbolKind.Parameter => FeaturesResources.parameter, - SymbolKind.Local => FeaturesResources.local_variable, - SymbolKind.Property => GetDisplayName((IPropertySymbol)symbol), - SymbolKind.TypeParameter => FeaturesResources.type_parameter, - _ => throw ExceptionUtilities.UnexpectedValue(symbol.Kind) - }; + private bool ReportUnsupportedOperations(in DiagnosticContext diagnosticContext, DeclarationBody body, CancellationToken cancellationToken) + { + if (HasUnsupportedOperation(body.GetDescendantNodes(IsNotLambda), out var unsupportedNode, out var rudeEdit)) + { + diagnosticContext.Report(rudeEdit, unsupportedNode, cancellationToken); + return true; + } - internal virtual string GetDisplayName(IEventSymbol symbol) - => FeaturesResources.event_; + return false; + } - internal virtual string GetDisplayName(IPropertySymbol symbol) - => symbol.IsAutoProperty() ? FeaturesResources.auto_property : FeaturesResources.property_; + internal abstract void ReportOtherRudeEditsAroundActiveStatement( + ArrayBuilder diagnostics, + IReadOnlyDictionary forwardMap, + SyntaxNode oldActiveStatement, + DeclarationBody oldBody, + SyntaxNode newActiveStatement, + DeclarationBody newBody, + bool isNonLeaf); + + internal abstract void ReportInsertedMemberSymbolRudeEdits(ArrayBuilder diagnostics, ISymbol newSymbol, SyntaxNode newNode, bool insertingIntoExistingContainingType); + internal abstract void ReportStateMachineSuspensionPointRudeEdits(DiagnosticContext diagnosticContext, SyntaxNode oldNode, SyntaxNode newNode); + + internal abstract Func IsLambda { get; } + internal abstract Func IsNotLambda { get; } + + internal abstract bool IsInterfaceDeclaration(SyntaxNode node); + internal abstract bool IsRecordDeclaration(SyntaxNode node); + + /// + /// True if the node represents any form of a function definition nested in another function body (i.e. anonymous function, lambda, local function). + /// + internal abstract bool IsNestedFunction(SyntaxNode node); + + internal abstract bool IsLocalFunction(SyntaxNode node); + internal abstract bool IsGenericLocalFunction(SyntaxNode node); + internal abstract bool IsClosureScope(SyntaxNode node); + internal abstract SyntaxNode GetCapturedParameterScope(SyntaxNode declaringMethodOrLambda); + internal abstract IMethodSymbol GetLambdaExpressionSymbol(SemanticModel model, SyntaxNode lambdaExpression, CancellationToken cancellationToken); + internal abstract SyntaxNode? GetContainingQueryExpression(SyntaxNode node); + internal abstract bool QueryClauseLambdasTypeEquivalent(SemanticModel oldModel, SyntaxNode oldNode, SemanticModel newModel, SyntaxNode newNode, CancellationToken cancellationToken); + + internal bool ContainsLambda(MemberBody body) + { + var isLambda = IsLambda; + return body.RootNodes.Any(static (root, isLambda) => root.DescendantNodesAndSelf().Any(isLambda), isLambda); + } - internal virtual string GetDisplayName(INamedTypeSymbol symbol) - => symbol.TypeKind switch - { - TypeKind.Class => FeaturesResources.class_, - TypeKind.Interface => FeaturesResources.interface_, - TypeKind.Delegate => FeaturesResources.delegate_, - TypeKind.Enum => FeaturesResources.enum_, - TypeKind.TypeParameter => FeaturesResources.type_parameter, - _ => FeaturesResources.type, - }; + /// + /// Returns all lambda bodies of a node representing a lambda, + /// or false if the node doesn't represent a lambda. + /// + /// + /// C# anonymous function expression and VB lambda expression both have a single body + /// (in VB the body is the header of the lambda expression). + /// + /// Some lambda queries (group by, join by) have two bodies. + /// + internal abstract bool TryGetLambdaBodies(SyntaxNode node, [NotNullWhen(true)] out LambdaBody? body1, out LambdaBody? body2); + + internal abstract bool IsStateMachineMethod(SyntaxNode declaration); + + /// + /// Returns the type declaration that contains a specified . + /// This can be class, struct, interface, record or enum declaration. + /// + internal abstract SyntaxNode? TryGetContainingTypeDeclaration(SyntaxNode node); + + /// + /// Return true if the declaration is a field/property declaration with an initializer. + /// Shall return false for enum members. + /// + internal abstract bool IsDeclarationWithInitializer(SyntaxNode declaration); + + /// + /// True if is a declaration node of a primary constructor (i.e. parameter list of a type declaration). + /// + /// + /// of a primary constructor returns the type declaration syntax. + /// This is inconvenient for EnC analysis since it doesn't allow us to distinguish declaration of the type from the constructor. + /// E.g. delete/insert of a primary constructor is not the same as delete/insert of the entire type declaration. + /// + internal abstract bool IsPrimaryConstructorDeclaration(SyntaxNode declaration); + + /// + /// Return true if is a constructor to which field/property initializers are emitted. + /// + internal abstract bool IsConstructorWithMemberInitializers(ISymbol symbol, CancellationToken cancellationToken); + + internal abstract bool IsPartial(INamedTypeSymbol type); + + internal abstract SyntaxNode EmptyCompilationUnit { get; } + + private static readonly SourceText s_emptySource = SourceText.From(""); + + #region Document Analysis + + public async Task AnalyzeDocumentAsync( + Project oldProject, + AsyncLazy lazyOldActiveStatementMap, + Document newDocument, + ImmutableArray newActiveStatementSpans, + AsyncLazy lazyCapabilities, + CancellationToken cancellationToken) + { + var filePath = newDocument.FilePath; - internal virtual string GetDisplayName(IFieldSymbol symbol) - => symbol.IsConst ? ((symbol.ContainingType.TypeKind == TypeKind.Enum) ? FeaturesResources.enum_value : FeaturesResources.const_field) : - FeaturesResources.field; + Debug.Assert(newDocument.State.SupportsEditAndContinue()); + Debug.Assert(!newActiveStatementSpans.IsDefault); + Debug.Assert(newDocument.SupportsSyntaxTree); + Debug.Assert(newDocument.SupportsSemanticModel); + Debug.Assert(filePath != null); - internal virtual string GetDisplayName(IMethodSymbol symbol) - => symbol.MethodKind switch - { - MethodKind.Constructor => FeaturesResources.constructor, - MethodKind.PropertyGet or MethodKind.PropertySet => FeaturesResources.property_accessor, - MethodKind.EventAdd or MethodKind.EventRaise or MethodKind.EventRemove => FeaturesResources.event_accessor, - MethodKind.BuiltinOperator or MethodKind.UserDefinedOperator or MethodKind.Conversion => FeaturesResources.operator_, - _ => FeaturesResources.method, - }; + // assume changes until we determine there are none so that EnC is blocked on unexpected exception: + var hasChanges = true; + var analysisStopwatch = SharedStopwatch.StartNew(); - /// - /// Returns the display name of an ancestor node that contains the specified node and has a display name. - /// - protected virtual string GetBodyDisplayName(SyntaxNode node, EditKind editKind = EditKind.Update) + try { - var current = node.Parent; + cancellationToken.ThrowIfCancellationRequested(); - if (current == null) + SyntaxTree? oldTree; + SyntaxNode oldRoot; + SourceText oldText; + + var oldDocument = await oldProject.GetDocumentAsync(newDocument.Id, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + if (oldDocument != null) { - var displayName = TryGetDisplayName(node, editKind); - if (displayName != null) - { - return displayName; - } + oldTree = await oldDocument.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + oldRoot = await oldTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); } - - while (true) + else { - if (current == null) - { - throw ExceptionUtilities.UnexpectedValue(node.GetType().Name); - } - - var displayName = TryGetDisplayName(current, editKind); - if (displayName != null) - { - return displayName; - } - - current = current.Parent; + oldTree = null; + oldRoot = EmptyCompilationUnit; + oldText = s_emptySource; } - } - - protected abstract string? TryGetDisplayName(SyntaxNode node, EditKind editKind); - protected virtual string GetSuspensionPointDisplayName(SyntaxNode node, EditKind editKind) - => GetDisplayName(node, editKind); + var newTree = await newDocument.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(newTree); - protected abstract string LineDirectiveKeyword { get; } - protected abstract ushort LineDirectiveSyntaxKind { get; } - protected abstract SymbolDisplayFormat ErrorDisplayFormat { get; } - protected abstract List GetExceptionHandlingAncestors(SyntaxNode node, SyntaxNode root, bool isNonLeaf); - protected abstract TextSpan GetExceptionHandlingRegion(SyntaxNode node, out bool coversAllChildren); + // Changes in parse options might change the meaning of the code even if nothing else changed. + // The IDE should disallow changing the options during debugging session. + Debug.Assert(oldTree == null || oldTree.Options.Equals(newTree.Options)); - internal abstract void ReportTopLevelSyntacticRudeEdits(ArrayBuilder diagnostics, Match match, Edit edit, Dictionary editMap); - internal abstract void ReportEnclosingExceptionHandlingRudeEdits(ArrayBuilder diagnostics, IEnumerable> exceptionHandlingEdits, SyntaxNode oldStatement, TextSpan newStatementSpan); + var newRoot = await newTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + hasChanges = !oldText.ContentEquals(newText); - internal abstract bool HasUnsupportedOperation(IEnumerable newNodes, [NotNullWhen(true)] out SyntaxNode? unsupportedNode, out RudeEditKind rudeEdit); + _testFaultInjector?.Invoke(newRoot); + cancellationToken.ThrowIfCancellationRequested(); - private bool ReportUnsupportedOperations(in DiagnosticContext diagnosticContext, DeclarationBody body, CancellationToken cancellationToken) - { - if (HasUnsupportedOperation(body.GetDescendantNodes(IsNotLambda), out var unsupportedNode, out var rudeEdit)) + // TODO: newTree.HasErrors? + var syntaxDiagnostics = newRoot.GetDiagnostics(); + var syntaxError = syntaxDiagnostics.FirstOrDefault(d => d.Severity == DiagnosticSeverity.Error); + if (syntaxError != null) { - diagnosticContext.Report(rudeEdit, unsupportedNode, cancellationToken); - return true; + // Bail, since we can't do syntax diffing on broken trees (it would not produce useful results anyways). + // If we needed to do so for some reason, we'd need to harden the syntax tree comparers. + Log.Write("Syntax errors found in '{0}'", filePath); + return DocumentAnalysisResults.SyntaxErrors(newDocument.Id, filePath, [], syntaxError, analysisStopwatch.Elapsed, hasChanges); } - return false; - } + if (!hasChanges) + { + // The document might have been closed and reopened, which might have triggered analysis. + // If the document is unchanged don't continue the analysis since + // a) comparing texts is cheaper than diffing trees + // b) we need to ignore errors in unchanged documents - internal abstract void ReportOtherRudeEditsAroundActiveStatement( - ArrayBuilder diagnostics, - IReadOnlyDictionary forwardMap, - SyntaxNode oldActiveStatement, - DeclarationBody oldBody, - SyntaxNode newActiveStatement, - DeclarationBody newBody, - bool isNonLeaf); + Log.Write("Document unchanged: '{0}'", filePath); + return DocumentAnalysisResults.Unchanged(newDocument.Id, filePath, analysisStopwatch.Elapsed); + } - internal abstract void ReportInsertedMemberSymbolRudeEdits(ArrayBuilder diagnostics, ISymbol newSymbol, SyntaxNode newNode, bool insertingIntoExistingContainingType); - internal abstract void ReportStateMachineSuspensionPointRudeEdits(DiagnosticContext diagnosticContext, SyntaxNode oldNode, SyntaxNode newNode); + // Disallow modification of a file with experimental features enabled. + // These features may not be handled well by the analysis below. + if (ExperimentalFeaturesEnabled(newTree)) + { + Log.Write("Experimental features enabled in '{0}'", filePath); - internal abstract Func IsLambda { get; } - internal abstract Func IsNotLambda { get; } + return DocumentAnalysisResults.SyntaxErrors(newDocument.Id, filePath, [new RudeEditDiagnostic(RudeEditKind.ExperimentalFeaturesEnabled, default)], syntaxError: null, analysisStopwatch.Elapsed, hasChanges); + } - internal abstract bool IsInterfaceDeclaration(SyntaxNode node); - internal abstract bool IsRecordDeclaration(SyntaxNode node); + var capabilities = new EditAndContinueCapabilitiesGrantor(await lazyCapabilities.GetValueAsync(cancellationToken).ConfigureAwait(false)); + var oldActiveStatementMap = await lazyOldActiveStatementMap.GetValueAsync(cancellationToken).ConfigureAwait(false); - /// - /// True if the node represents any form of a function definition nested in another function body (i.e. anonymous function, lambda, local function). - /// - internal abstract bool IsNestedFunction(SyntaxNode node); + // If the document has changed at all, lets make sure Edit and Continue is supported + if (!capabilities.Grant(EditAndContinueCapabilities.Baseline)) + { + return DocumentAnalysisResults.SyntaxErrors(newDocument.Id, filePath, [new RudeEditDiagnostic(RudeEditKind.NotSupportedByRuntime, default)], syntaxError: null, analysisStopwatch.Elapsed, hasChanges); + } - internal abstract bool IsLocalFunction(SyntaxNode node); - internal abstract bool IsGenericLocalFunction(SyntaxNode node); - internal abstract bool IsClosureScope(SyntaxNode node); - internal abstract SyntaxNode GetCapturedParameterScope(SyntaxNode declaringMethodOrLambda); - internal abstract IMethodSymbol GetLambdaExpressionSymbol(SemanticModel model, SyntaxNode lambdaExpression, CancellationToken cancellationToken); - internal abstract SyntaxNode? GetContainingQueryExpression(SyntaxNode node); - internal abstract bool QueryClauseLambdasTypeEquivalent(SemanticModel oldModel, SyntaxNode oldNode, SemanticModel newModel, SyntaxNode newNode, CancellationToken cancellationToken); + // We are in break state when there are no active statements. + var inBreakState = !oldActiveStatementMap.IsEmpty; - internal bool ContainsLambda(MemberBody body) - { - var isLambda = IsLambda; - return body.RootNodes.Any(static (root, isLambda) => root.DescendantNodesAndSelf().Any(isLambda), isLambda); - } + // We do calculate diffs even if there are semantic errors for the following reasons: + // 1) We need to be able to find active spans in the new document. + // If we didn't calculate them we would only rely on tracking spans (might be ok). + // 2) If there are syntactic rude edits we'll report them faster without waiting for semantic analysis. + // The user may fix them before they address all the semantic errors. - /// - /// Returns all lambda bodies of a node representing a lambda, - /// or false if the node doesn't represent a lambda. - /// - /// - /// C# anonymous function expression and VB lambda expression both have a single body - /// (in VB the body is the header of the lambda expression). - /// - /// Some lambda queries (group by, join by) have two bodies. - /// - internal abstract bool TryGetLambdaBodies(SyntaxNode node, [NotNullWhen(true)] out LambdaBody? body1, out LambdaBody? body2); + using var _2 = ArrayBuilder.GetInstance(out var diagnostics); - internal abstract bool IsStateMachineMethod(SyntaxNode declaration); + cancellationToken.ThrowIfCancellationRequested(); - /// - /// Returns the type declaration that contains a specified . - /// This can be class, struct, interface, record or enum declaration. - /// - internal abstract SyntaxNode? TryGetContainingTypeDeclaration(SyntaxNode node); + var topMatch = ComputeTopLevelMatch(oldRoot, newRoot); + var syntacticEdits = topMatch.GetTreeEdits(); + var editMap = BuildEditMap(syntacticEdits); - /// - /// Return true if the declaration is a field/property declaration with an initializer. - /// Shall return false for enum members. - /// - internal abstract bool IsDeclarationWithInitializer(SyntaxNode declaration); + ReportTopLevelSyntacticRudeEdits(diagnostics, syntacticEdits, editMap); - /// - /// True if is a declaration node of a primary constructor (i.e. parameter list of a type declaration). - /// - /// - /// of a primary constructor returns the type declaration syntax. - /// This is inconvenient for EnC analysis since it doesn't allow us to distinguish declaration of the type from the constructor. - /// E.g. delete/insert of a primary constructor is not the same as delete/insert of the entire type declaration. - /// - internal abstract bool IsPrimaryConstructorDeclaration(SyntaxNode declaration); + cancellationToken.ThrowIfCancellationRequested(); - /// - /// Return true if is a constructor to which field/property initializers are emitted. - /// - internal abstract bool IsConstructorWithMemberInitializers(ISymbol symbol, CancellationToken cancellationToken); + using var _3 = ArrayBuilder<(SyntaxNode OldNode, SyntaxNode NewNode, TextSpan DiagnosticSpan)>.GetInstance(out var triviaEdits); + using var _4 = ArrayBuilder.GetInstance(out var lineEdits); + + AnalyzeTrivia( + topMatch, + editMap, + triviaEdits, + lineEdits, + cancellationToken); - internal abstract bool IsPartial(INamedTypeSymbol type); + cancellationToken.ThrowIfCancellationRequested(); - internal abstract SyntaxNode EmptyCompilationUnit { get; } + var oldActiveStatements = (oldTree == null) ? [] : + oldActiveStatementMap.GetOldActiveStatements(this, oldTree, oldText, oldRoot, cancellationToken); - private static readonly SourceText s_emptySource = SourceText.From(""); + var newActiveStatements = ImmutableArray.CreateBuilder(oldActiveStatements.Length); + newActiveStatements.Count = oldActiveStatements.Length; - #region Document Analysis + var newExceptionRegions = ImmutableArray.CreateBuilder>(oldActiveStatements.Length); + newExceptionRegions.Count = oldActiveStatements.Length; - public async Task AnalyzeDocumentAsync( - Project oldProject, - AsyncLazy lazyOldActiveStatementMap, - Document newDocument, - ImmutableArray newActiveStatementSpans, - AsyncLazy lazyCapabilities, - CancellationToken cancellationToken) - { - var filePath = newDocument.FilePath; + var semanticEdits = await AnalyzeSemanticsAsync( + syntacticEdits, + editMap, + oldActiveStatements, + newActiveStatementSpans, + triviaEdits, + oldProject, + oldDocument, + newDocument, + newText, + diagnostics, + newActiveStatements, + newExceptionRegions, + capabilities, + inBreakState, + cancellationToken).ConfigureAwait(false); - Debug.Assert(newDocument.State.SupportsEditAndContinue()); - Debug.Assert(!newActiveStatementSpans.IsDefault); - Debug.Assert(newDocument.SupportsSyntaxTree); - Debug.Assert(newDocument.SupportsSemanticModel); - Debug.Assert(filePath != null); + cancellationToken.ThrowIfCancellationRequested(); - // assume changes until we determine there are none so that EnC is blocked on unexpected exception: - var hasChanges = true; - var analysisStopwatch = SharedStopwatch.StartNew(); + AnalyzeUnchangedActiveMemberBodies(diagnostics, syntacticEdits.Match, newText, oldActiveStatements, newActiveStatementSpans, newActiveStatements, newExceptionRegions, cancellationToken); + Debug.Assert(newActiveStatements.All(a => a != null)); - try + var hasRudeEdits = diagnostics.Count > 0; + if (hasRudeEdits) { - cancellationToken.ThrowIfCancellationRequested(); - - SyntaxTree? oldTree; - SyntaxNode oldRoot; - SourceText oldText; + LogRudeEdits(diagnostics, newText, filePath); + } + else + { + Log.Write("Capabilities required by '{0}': {1}", filePath, capabilities.GrantedCapabilities); + } + + return new DocumentAnalysisResults( + newDocument.Id, + filePath, + newActiveStatements.MoveToImmutable(), + diagnostics.ToImmutable(), + syntaxError: null, + hasRudeEdits ? default : semanticEdits, + hasRudeEdits ? default : newExceptionRegions.MoveToImmutable(), + hasRudeEdits ? default : lineEdits.ToImmutable(), + hasRudeEdits ? default : capabilities.GrantedCapabilities, + analysisStopwatch.Elapsed, + hasChanges: true, + hasSyntaxErrors: false); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + // The same behavior as if there was a syntax error - we are unable to analyze the document. + // We expect OOM to be thrown during the analysis if the number of top-level entities is too large. + // In such case we report a rude edit for the document. If the host is actually running out of memory, + // it might throw another OOM here or later on. + var diagnostic = (e is OutOfMemoryException) + ? new RudeEditDiagnostic(RudeEditKind.SourceFileTooBig, span: default, arguments: [newDocument.FilePath]) + : new RudeEditDiagnostic(RudeEditKind.InternalError, span: default, arguments: [newDocument.FilePath, e.ToString()]); + + // Report as "syntax error" - we can't analyze the document + return DocumentAnalysisResults.SyntaxErrors(newDocument.Id, filePath, [diagnostic], syntaxError: null, analysisStopwatch.Elapsed, hasChanges); + } - var oldDocument = await oldProject.GetDocumentAsync(newDocument.Id, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - if (oldDocument != null) + static void LogRudeEdits(ArrayBuilder diagnostics, SourceText text, string filePath) + { + foreach (var diagnostic in diagnostics) + { + int lineNumber; + string? lineText; + try { - oldTree = await oldDocument.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - oldRoot = await oldTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var line = text.Lines.GetLineFromPosition(diagnostic.Span.Start); + lineNumber = line.LineNumber; + lineText = text.ToString(TextSpan.FromBounds(diagnostic.Span.Start, Math.Min(diagnostic.Span.Start + 120, line.End))); } - else + catch { - oldTree = null; - oldRoot = EmptyCompilationUnit; - oldText = s_emptySource; + lineNumber = -1; + lineText = null; } - var newTree = await newDocument.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - Contract.ThrowIfNull(newTree); + Log.Write("Rude edit {0}:{1} '{2}' line {3}: '{4}'", diagnostic.Kind, diagnostic.SyntaxKind, filePath, lineNumber, lineText); + } + } + } - // Changes in parse options might change the meaning of the code even if nothing else changed. - // The IDE should disallow changing the options during debugging session. - Debug.Assert(oldTree == null || oldTree.Options.Equals(newTree.Options)); + private void ReportTopLevelSyntacticRudeEdits(ArrayBuilder diagnostics, EditScript syntacticEdits, Dictionary editMap) + { + foreach (var edit in syntacticEdits.Edits) + { + ReportTopLevelSyntacticRudeEdits(diagnostics, syntacticEdits.Match, edit, editMap); + } + } - var newRoot = await newTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - hasChanges = !oldText.ContentEquals(newText); + internal Dictionary BuildEditMap(EditScript editScript) + { + var map = new Dictionary(editScript.Edits.Length); - _testFaultInjector?.Invoke(newRoot); - cancellationToken.ThrowIfCancellationRequested(); + foreach (var edit in editScript.Edits) + { + // do not include reorder and move edits - // TODO: newTree.HasErrors? - var syntaxDiagnostics = newRoot.GetDiagnostics(); - var syntaxError = syntaxDiagnostics.FirstOrDefault(d => d.Severity == DiagnosticSeverity.Error); - if (syntaxError != null) - { - // Bail, since we can't do syntax diffing on broken trees (it would not produce useful results anyways). - // If we needed to do so for some reason, we'd need to harden the syntax tree comparers. - Log.Write("Syntax errors found in '{0}'", filePath); - return DocumentAnalysisResults.SyntaxErrors(newDocument.Id, filePath, [], syntaxError, analysisStopwatch.Elapsed, hasChanges); - } + if (edit.Kind is EditKind.Delete or EditKind.Update) + { + map.Add(edit.OldNode, edit.Kind); + } - if (!hasChanges) - { - // The document might have been closed and reopened, which might have triggered analysis. - // If the document is unchanged don't continue the analysis since - // a) comparing texts is cheaper than diffing trees - // b) we need to ignore errors in unchanged documents + if (edit.Kind is EditKind.Insert or EditKind.Update) + { + map.Add(edit.NewNode, edit.Kind); + } + } - Log.Write("Document unchanged: '{0}'", filePath); - return DocumentAnalysisResults.Unchanged(newDocument.Id, filePath, analysisStopwatch.Elapsed); - } + // When a global statement is updated, inserted or deleted it means that the containing + // compilation unit has been updated. This update is not recorded in the edit script + // since compilation unit contains other items then global statements as well and + // we only want it to be updated in presence of changed global statements. + if ((IsCompilationUnitWithGlobalStatements(editScript.Match.OldRoot) || IsCompilationUnitWithGlobalStatements(editScript.Match.NewRoot)) && + map.Any(entry => IsGlobalStatement(entry.Key))) + { + map.Add(editScript.Match.OldRoot, EditKind.Update); + map.Add(editScript.Match.NewRoot, EditKind.Update); + } - // Disallow modification of a file with experimental features enabled. - // These features may not be handled well by the analysis below. - if (ExperimentalFeaturesEnabled(newTree)) - { - Log.Write("Experimental features enabled in '{0}'", filePath); + return map; + } - return DocumentAnalysisResults.SyntaxErrors(newDocument.Id, filePath, [new RudeEditDiagnostic(RudeEditKind.ExperimentalFeaturesEnabled, default)], syntaxError: null, analysisStopwatch.Elapsed, hasChanges); - } + #endregion - var capabilities = new EditAndContinueCapabilitiesGrantor(await lazyCapabilities.GetValueAsync(cancellationToken).ConfigureAwait(false)); - var oldActiveStatementMap = await lazyOldActiveStatementMap.GetValueAsync(cancellationToken).ConfigureAwait(false); + #region Syntax Analysis - // If the document has changed at all, lets make sure Edit and Continue is supported - if (!capabilities.Grant(EditAndContinueCapabilities.Baseline)) - { - return DocumentAnalysisResults.SyntaxErrors(newDocument.Id, filePath, [new RudeEditDiagnostic(RudeEditKind.NotSupportedByRuntime, default)], syntaxError: null, analysisStopwatch.Elapsed, hasChanges); - } + private void AnalyzeUnchangedActiveMemberBodies( + ArrayBuilder diagnostics, + Match topMatch, + SourceText newText, + ImmutableArray oldActiveStatements, + ImmutableArray newActiveStatementSpans, + [In, Out] ImmutableArray.Builder newActiveStatements, + [In, Out] ImmutableArray>.Builder newExceptionRegions, + CancellationToken cancellationToken) + { + Debug.Assert(!newActiveStatementSpans.IsDefault); + Debug.Assert(newActiveStatementSpans.IsEmpty || oldActiveStatements.Length == newActiveStatementSpans.Length); + Debug.Assert(oldActiveStatements.Length == newActiveStatements.Count); + Debug.Assert(oldActiveStatements.Length == newExceptionRegions.Count); - // We are in break state when there are no active statements. - var inBreakState = !oldActiveStatementMap.IsEmpty; + // Active statements in methods that were not updated + // are not changed but their spans might have been. - // We do calculate diffs even if there are semantic errors for the following reasons: - // 1) We need to be able to find active spans in the new document. - // If we didn't calculate them we would only rely on tracking spans (might be ok). - // 2) If there are syntactic rude edits we'll report them faster without waiting for semantic analysis. - // The user may fix them before they address all the semantic errors. + for (var i = 0; i < newActiveStatements.Count; i++) + { + if (newActiveStatements[i] == null) + { + Contract.ThrowIfFalse(newExceptionRegions[i].IsDefault); - using var _2 = ArrayBuilder.GetInstance(out var diagnostics); + var oldStatementSpan = oldActiveStatements[i].UnmappedSpan; - cancellationToken.ThrowIfCancellationRequested(); + var node = TryGetNode(topMatch.OldRoot, oldStatementSpan.Start); - var topMatch = ComputeTopLevelMatch(oldRoot, newRoot); - var syntacticEdits = topMatch.GetTreeEdits(); - var editMap = BuildEditMap(syntacticEdits); + // Guard against invalid active statement spans (in case PDB was somehow out of sync with the source). + if (node != null && TryFindMemberDeclaration(topMatch.OldRoot, node, oldStatementSpan, out var oldMemberDeclarations)) + { + foreach (var oldMember in oldMemberDeclarations) + { + var hasPartner = topMatch.TryGetNewNode(oldMember, out var newMember); + Contract.ThrowIfFalse(hasPartner); - ReportTopLevelSyntacticRudeEdits(diagnostics, syntacticEdits, editMap); + var oldBody = TryGetDeclarationBody(oldMember, symbol: null); + var newBody = TryGetDeclarationBody(newMember, symbol: null); - cancellationToken.ThrowIfCancellationRequested(); + // Guard against invalid active statement spans (in case PDB was somehow out of sync with the source). + if (oldBody == null || newBody == null) + { + Log.Write("Invalid active statement span: [{0}..{1})", oldStatementSpan.Start, oldStatementSpan.End); + continue; + } - using var _3 = ArrayBuilder<(SyntaxNode OldNode, SyntaxNode NewNode, TextSpan DiagnosticSpan)>.GetInstance(out var triviaEdits); - using var _4 = ArrayBuilder.GetInstance(out var lineEdits); + var statementPart = -1; + SyntaxNode? newStatement = null; - AnalyzeTrivia( - topMatch, - editMap, - triviaEdits, - lineEdits, - cancellationToken); + // We seed the method body matching algorithm with tracking spans (unless they were deleted) + // to get precise matching. + if (TryGetTrackedStatement(newActiveStatementSpans, i, newText, newBody, out var trackedStatement, out var trackedStatementPart)) + { + // Adjust for active statements that cover more than the old member span. + // For example, C# variable declarators that represent field initializers: + // [|public int <>;|] + var adjustedOldStatementStart = oldBody.ContainsActiveStatementSpan(oldStatementSpan) ? oldStatementSpan.Start : oldBody.Envelope.Start; - cancellationToken.ThrowIfCancellationRequested(); + // The tracking span might have been moved outside of lambda. + // It is not an error to move the statement - we just ignore it. + var oldEnclosingLambdaBody = FindEnclosingLambdaBody(oldBody.EncompassingAncestor, oldMember.FindToken(adjustedOldStatementStart).Parent!); + var newEnclosingLambdaBody = FindEnclosingLambdaBody(newBody.EncompassingAncestor, trackedStatement); + if (oldEnclosingLambdaBody == newEnclosingLambdaBody) + { + newStatement = trackedStatement; + statementPart = trackedStatementPart; + } + } - var oldActiveStatements = (oldTree == null) ? [] : - oldActiveStatementMap.GetOldActiveStatements(this, oldTree, oldText, oldRoot, cancellationToken); - - var newActiveStatements = ImmutableArray.CreateBuilder(oldActiveStatements.Length); - newActiveStatements.Count = oldActiveStatements.Length; - - var newExceptionRegions = ImmutableArray.CreateBuilder>(oldActiveStatements.Length); - newExceptionRegions.Count = oldActiveStatements.Length; - - var semanticEdits = await AnalyzeSemanticsAsync( - syntacticEdits, - editMap, - oldActiveStatements, - newActiveStatementSpans, - triviaEdits, - oldProject, - oldDocument, - newDocument, - newText, - diagnostics, - newActiveStatements, - newExceptionRegions, - capabilities, - inBreakState, - cancellationToken).ConfigureAwait(false); + if (newStatement == null) + { + Contract.ThrowIfFalse(statementPart == -1); + oldBody.FindStatementAndPartner(oldStatementSpan, newBody, out newStatement, out statementPart); + Contract.ThrowIfNull(newStatement); + } - cancellationToken.ThrowIfCancellationRequested(); + if (diagnostics.Count == 0) + { + var ancestors = GetExceptionHandlingAncestors(newStatement, newBody.EncompassingAncestor, oldActiveStatements[i].Statement.IsNonLeaf); + newExceptionRegions[i] = GetExceptionRegions(ancestors, newStatement.SyntaxTree, cancellationToken).Spans; + } - AnalyzeUnchangedActiveMemberBodies(diagnostics, syntacticEdits.Match, newText, oldActiveStatements, newActiveStatementSpans, newActiveStatements, newExceptionRegions, cancellationToken); - Debug.Assert(newActiveStatements.All(a => a != null)); + // Even though the body of the declaration haven't changed, + // changes to its header might have caused the active span to become unavailable. + // (e.g. In C# "const" was added to modifiers of a field with an initializer). + var newStatementSpan = FindClosestActiveSpan(newStatement, statementPart); - var hasRudeEdits = diagnostics.Count > 0; - if (hasRudeEdits) - { - LogRudeEdits(diagnostics, newText, filePath); + newActiveStatements[i] = GetActiveStatementWithSpan(oldActiveStatements[i], newBody.SyntaxTree, newStatementSpan, diagnostics, cancellationToken); + } } else { - Log.Write("Capabilities required by '{0}': {1}", filePath, capabilities.GrantedCapabilities); + Log.Write("Invalid active statement span: [{0}..{1})", oldStatementSpan.Start, oldStatementSpan.End); } - return new DocumentAnalysisResults( - newDocument.Id, - filePath, - newActiveStatements.MoveToImmutable(), - diagnostics.ToImmutable(), - syntaxError: null, - hasRudeEdits ? default : semanticEdits, - hasRudeEdits ? default : newExceptionRegions.MoveToImmutable(), - hasRudeEdits ? default : lineEdits.ToImmutable(), - hasRudeEdits ? default : capabilities.GrantedCapabilities, - analysisStopwatch.Elapsed, - hasChanges: true, - hasSyntaxErrors: false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - // The same behavior as if there was a syntax error - we are unable to analyze the document. - // We expect OOM to be thrown during the analysis if the number of top-level entities is too large. - // In such case we report a rude edit for the document. If the host is actually running out of memory, - // it might throw another OOM here or later on. - var diagnostic = (e is OutOfMemoryException) - ? new RudeEditDiagnostic(RudeEditKind.SourceFileTooBig, span: default, arguments: [newDocument.FilePath]) - : new RudeEditDiagnostic(RudeEditKind.InternalError, span: default, arguments: [newDocument.FilePath, e.ToString()]); - - // Report as "syntax error" - we can't analyze the document - return DocumentAnalysisResults.SyntaxErrors(newDocument.Id, filePath, [diagnostic], syntaxError: null, analysisStopwatch.Elapsed, hasChanges); - } - - static void LogRudeEdits(ArrayBuilder diagnostics, SourceText text, string filePath) - { - foreach (var diagnostic in diagnostics) + // we were not able to determine the active statement location (PDB data might be invalid) + if (newActiveStatements[i] == null) { - int lineNumber; - string? lineText; - try - { - var line = text.Lines.GetLineFromPosition(diagnostic.Span.Start); - lineNumber = line.LineNumber; - lineText = text.ToString(TextSpan.FromBounds(diagnostic.Span.Start, Math.Min(diagnostic.Span.Start + 120, line.End))); - } - catch - { - lineNumber = -1; - lineText = null; - } - - Log.Write("Rude edit {0}:{1} '{2}' line {3}: '{4}'", diagnostic.Kind, diagnostic.SyntaxKind, filePath, lineNumber, lineText); + newActiveStatements[i] = oldActiveStatements[i].Statement.WithSpan(default); + newExceptionRegions[i] = []; } } } + } + + internal readonly struct ActiveNode(int activeStatementIndex, SyntaxNode oldNode, LambdaBody? enclosingLambdaBody, int statementPart, SyntaxNode? newTrackedNode) + { + public readonly int ActiveStatementIndex = activeStatementIndex; + public readonly SyntaxNode OldNode = oldNode; + public readonly SyntaxNode? NewTrackedNode = newTrackedNode; + public readonly LambdaBody? EnclosingLambdaBody = enclosingLambdaBody; + public readonly int StatementPart = statementPart; + } + + /// + /// Information about an active and/or a matched lambda. + /// + internal readonly struct LambdaInfo + { + // non-null for an active lambda (lambda containing an active statement) + public readonly List? ActiveNodeIndices; - private void ReportTopLevelSyntacticRudeEdits(ArrayBuilder diagnostics, EditScript syntacticEdits, Dictionary editMap) + // both fields are non-null for a matching lambda (lambda that exists in both old and new document): + public readonly DeclarationBodyMap BodyMap; + public readonly LambdaBody? NewBody; + + public LambdaInfo(List activeNodeIndices) + : this(activeNodeIndices, DeclarationBodyMap.Empty, null) { - foreach (var edit in syntacticEdits.Edits) - { - ReportTopLevelSyntacticRudeEdits(diagnostics, syntacticEdits.Match, edit, editMap); - } } - internal Dictionary BuildEditMap(EditScript editScript) + private LambdaInfo(List? activeNodeIndices, DeclarationBodyMap bodyMap, LambdaBody? newLambdaBody) { - var map = new Dictionary(editScript.Edits.Length); - - foreach (var edit in editScript.Edits) - { - // do not include reorder and move edits + ActiveNodeIndices = activeNodeIndices; + BodyMap = bodyMap; + NewBody = newLambdaBody; + } - if (edit.Kind is EditKind.Delete or EditKind.Update) - { - map.Add(edit.OldNode, edit.Kind); - } + public bool HasActiveStatement + => ActiveNodeIndices != null; - if (edit.Kind is EditKind.Insert or EditKind.Update) - { - map.Add(edit.NewNode, edit.Kind); - } - } + public LambdaInfo WithMatch(DeclarationBodyMap match, LambdaBody newLambdaBody) + => new(ActiveNodeIndices, match, newLambdaBody); + } - // When a global statement is updated, inserted or deleted it means that the containing - // compilation unit has been updated. This update is not recorded in the edit script - // since compilation unit contains other items then global statements as well and - // we only want it to be updated in presence of changed global statements. - if ((IsCompilationUnitWithGlobalStatements(editScript.Match.OldRoot) || IsCompilationUnitWithGlobalStatements(editScript.Match.NewRoot)) && - map.Any(entry => IsGlobalStatement(entry.Key))) - { - map.Add(editScript.Match.OldRoot, EditKind.Update); - map.Add(editScript.Match.NewRoot, EditKind.Update); - } + private void AnalyzeChangedMemberBody( + SyntaxNode? oldDeclaration, + SyntaxNode? newDeclaration, + MemberBody? oldMemberBody, + MemberBody? newMemberBody, + SemanticModel? oldModel, + SemanticModel newModel, + ISymbol oldMember, + ISymbol newMember, + Compilation oldCompilation, + SourceText newText, + bool isMemberReplaced, + Match topMatch, + ImmutableArray oldActiveStatements, + ImmutableArray newActiveStatementSpans, + EditAndContinueCapabilitiesGrantor capabilities, + [Out] ImmutableArray.Builder newActiveStatements, + [Out] ImmutableArray>.Builder newExceptionRegions, + [Out] ArrayBuilder diagnostics, + out SyntaxMaps syntaxMaps, + CancellationToken cancellationToken) + { + Debug.Assert(!newActiveStatementSpans.IsDefault); + Debug.Assert(newActiveStatementSpans.IsEmpty || oldActiveStatements.Length == newActiveStatementSpans.Length); + Debug.Assert(oldActiveStatements.IsEmpty || oldActiveStatements.Length == newActiveStatements.Count); + Debug.Assert(newActiveStatements.Count == newExceptionRegions.Count); + Debug.Assert(oldMemberBody != null || newMemberBody != null); - return map; - } + var diagnosticContext = CreateDiagnosticContext(diagnostics, oldMember, newMember, newDeclaration, newModel, topMatch); - #endregion + var activeStatementIndices = oldMemberBody?.GetOverlappingActiveStatements(oldActiveStatements)?.ToArray() ?? []; - #region Syntax Analysis + if (isMemberReplaced && !activeStatementIndices.IsEmpty()) + { + diagnosticContext.Report(RudeEditKind.ChangingNameOrSignatureOfActiveMember, cancellationToken); + } - private void AnalyzeUnchangedActiveMemberBodies( - ArrayBuilder diagnostics, - Match topMatch, - SourceText newText, - ImmutableArray oldActiveStatements, - ImmutableArray newActiveStatementSpans, - [In, Out] ImmutableArray.Builder newActiveStatements, - [In, Out] ImmutableArray>.Builder newExceptionRegions, - CancellationToken cancellationToken) + try { - Debug.Assert(!newActiveStatementSpans.IsDefault); - Debug.Assert(newActiveStatementSpans.IsEmpty || oldActiveStatements.Length == newActiveStatementSpans.Length); - Debug.Assert(oldActiveStatements.Length == newActiveStatements.Count); - Debug.Assert(oldActiveStatements.Length == newExceptionRegions.Count); + if (newMemberBody != null) + { + _testFaultInjector?.Invoke(newMemberBody.RootNodes.First()); + } - // Active statements in methods that were not updated - // are not changed but their spans might have been. + // Populated with active lambdas and matched lambdas. + // Unmatched non-active lambdas are not included. + // { old-lambda-body -> info } + Dictionary? lazyActiveOrMatchedLambdas = null; - for (var i = 0; i < newActiveStatements.Count; i++) - { - if (newActiveStatements[i] == null) - { - Contract.ThrowIfFalse(newExceptionRegions[i].IsDefault); + // finds leaf nodes that correspond to the old active statements: + using var _ = ArrayBuilder.GetInstance(out var activeNodes); - var oldStatementSpan = oldActiveStatements[i].UnmappedSpan; + if (newMemberBody == null) + { + // The name or signature has been changed and the member needs to be deleted and a new one emitted. + // The debugger does not support active statement remapping between two different methods, so report rude edits. + // + // The body has been deleted. Two cases: + // 1) The declaration is available + // Example: Deleting field initializer. + // We try to find another active statement that's in the user code (e.g. initializer that logically follows the deleted one or a constructor body statement). + // If such active statement does not exist it is still possible to remap the deleted statement into the synthesized body of the method that replaces the current body. + // (e.g. default constructor). The debugger supports active statement remapping to a method without sequence point. It will remap to the first instruction of the method. + // + // 2) The declaration is also deleted, but a synthesized one is generated in its place and thus an update edit is issued. + // Will remap active statements to the first instruction of the synthesized body (same as above). - var node = TryGetNode(topMatch.OldRoot, oldStatementSpan.Start); + foreach (var activeStatementIndex in activeStatementIndices) + { + // the declaration must exist, otherwise we wouldn't find any active statements overlapping the old body + Debug.Assert(oldDeclaration != null); - // Guard against invalid active statement spans (in case PDB was somehow out of sync with the source). - if (node != null && TryFindMemberDeclaration(topMatch.OldRoot, node, oldStatementSpan, out var oldMemberDeclarations)) + // We have already calculated the new location of this active statement when analyzing another member declaration. + // This may only happen when two or more member declarations share the same body (VB AsNew clause). + if (newActiveStatements[activeStatementIndex] != null) { - foreach (var oldMember in oldMemberDeclarations) - { - var hasPartner = topMatch.TryGetNewNode(oldMember, out var newMember); - Contract.ThrowIfFalse(hasPartner); - - var oldBody = TryGetDeclarationBody(oldMember, symbol: null); - var newBody = TryGetDeclarationBody(newMember, symbol: null); + Debug.Assert(IsDeclarationWithSharedBody(oldDeclaration, oldMember)); + continue; + } - // Guard against invalid active statement spans (in case PDB was somehow out of sync with the source). - if (oldBody == null || newBody == null) - { - Log.Write("Invalid active statement span: [{0}..{1})", oldStatementSpan.Start, oldStatementSpan.End); - continue; - } - - var statementPart = -1; - SyntaxNode? newStatement = null; - - // We seed the method body matching algorithm with tracking spans (unless they were deleted) - // to get precise matching. - if (TryGetTrackedStatement(newActiveStatementSpans, i, newText, newBody, out var trackedStatement, out var trackedStatementPart)) - { - // Adjust for active statements that cover more than the old member span. - // For example, C# variable declarators that represent field initializers: - // [|public int <>;|] - var adjustedOldStatementStart = oldBody.ContainsActiveStatementSpan(oldStatementSpan) ? oldStatementSpan.Start : oldBody.Envelope.Start; - - // The tracking span might have been moved outside of lambda. - // It is not an error to move the statement - we just ignore it. - var oldEnclosingLambdaBody = FindEnclosingLambdaBody(oldBody.EncompassingAncestor, oldMember.FindToken(adjustedOldStatementStart).Parent!); - var newEnclosingLambdaBody = FindEnclosingLambdaBody(newBody.EncompassingAncestor, trackedStatement); - if (oldEnclosingLambdaBody == newEnclosingLambdaBody) - { - newStatement = trackedStatement; - statementPart = trackedStatementPart; - } - } - - if (newStatement == null) - { - Contract.ThrowIfFalse(statementPart == -1); - oldBody.FindStatementAndPartner(oldStatementSpan, newBody, out newStatement, out statementPart); - Contract.ThrowIfNull(newStatement); - } - - if (diagnostics.Count == 0) - { - var ancestors = GetExceptionHandlingAncestors(newStatement, newBody.EncompassingAncestor, oldActiveStatements[i].Statement.IsNonLeaf); - newExceptionRegions[i] = GetExceptionRegions(ancestors, newStatement.SyntaxTree, cancellationToken).Spans; - } - - // Even though the body of the declaration haven't changed, - // changes to its header might have caused the active span to become unavailable. - // (e.g. In C# "const" was added to modifiers of a field with an initializer). - var newStatementSpan = FindClosestActiveSpan(newStatement, statementPart); - - newActiveStatements[i] = GetActiveStatementWithSpan(oldActiveStatements[i], newBody.SyntaxTree, newStatementSpan, diagnostics, cancellationToken); - } - } - else - { - Log.Write("Invalid active statement span: [{0}..{1})", oldStatementSpan.Start, oldStatementSpan.End); - } - - // we were not able to determine the active statement location (PDB data might be invalid) - if (newActiveStatements[i] == null) - { - newActiveStatements[i] = oldActiveStatements[i].Statement.WithSpan(default); - newExceptionRegions[i] = []; - } + var newSpan = GetDeletedDeclarationActiveSpan(topMatch.Matches, oldDeclaration); + newActiveStatements[activeStatementIndex] = GetActiveStatementWithSpan(oldActiveStatements[activeStatementIndex], topMatch.NewRoot.SyntaxTree, newSpan, diagnostics, cancellationToken); + newExceptionRegions[activeStatementIndex] = []; } } - } - - internal readonly struct ActiveNode(int activeStatementIndex, SyntaxNode oldNode, LambdaBody? enclosingLambdaBody, int statementPart, SyntaxNode? newTrackedNode) - { - public readonly int ActiveStatementIndex = activeStatementIndex; - public readonly SyntaxNode OldNode = oldNode; - public readonly SyntaxNode? NewTrackedNode = newTrackedNode; - public readonly LambdaBody? EnclosingLambdaBody = enclosingLambdaBody; - public readonly int StatementPart = statementPart; - } - - /// - /// Information about an active and/or a matched lambda. - /// - internal readonly struct LambdaInfo - { - // non-null for an active lambda (lambda containing an active statement) - public readonly List? ActiveNodeIndices; - - // both fields are non-null for a matching lambda (lambda that exists in both old and new document): - public readonly DeclarationBodyMap BodyMap; - public readonly LambdaBody? NewBody; - - public LambdaInfo(List activeNodeIndices) - : this(activeNodeIndices, DeclarationBodyMap.Empty, null) - { - } - - private LambdaInfo(List? activeNodeIndices, DeclarationBodyMap bodyMap, LambdaBody? newLambdaBody) - { - ActiveNodeIndices = activeNodeIndices; - BodyMap = bodyMap; - NewBody = newLambdaBody; - } - - public bool HasActiveStatement - => ActiveNodeIndices != null; - - public LambdaInfo WithMatch(DeclarationBodyMap match, LambdaBody newLambdaBody) - => new(ActiveNodeIndices, match, newLambdaBody); - } - - private void AnalyzeChangedMemberBody( - SyntaxNode? oldDeclaration, - SyntaxNode? newDeclaration, - MemberBody? oldMemberBody, - MemberBody? newMemberBody, - SemanticModel? oldModel, - SemanticModel newModel, - ISymbol oldMember, - ISymbol newMember, - Compilation oldCompilation, - SourceText newText, - bool isMemberReplaced, - Match topMatch, - ImmutableArray oldActiveStatements, - ImmutableArray newActiveStatementSpans, - EditAndContinueCapabilitiesGrantor capabilities, - [Out] ImmutableArray.Builder newActiveStatements, - [Out] ImmutableArray>.Builder newExceptionRegions, - [Out] ArrayBuilder diagnostics, - out SyntaxMaps syntaxMaps, - CancellationToken cancellationToken) - { - Debug.Assert(!newActiveStatementSpans.IsDefault); - Debug.Assert(newActiveStatementSpans.IsEmpty || oldActiveStatements.Length == newActiveStatementSpans.Length); - Debug.Assert(oldActiveStatements.IsEmpty || oldActiveStatements.Length == newActiveStatements.Count); - Debug.Assert(newActiveStatements.Count == newExceptionRegions.Count); - Debug.Assert(oldMemberBody != null || newMemberBody != null); - - var diagnosticContext = CreateDiagnosticContext(diagnostics, oldMember, newMember, newDeclaration, newModel, topMatch); - - var activeStatementIndices = oldMemberBody?.GetOverlappingActiveStatements(oldActiveStatements)?.ToArray() ?? []; - - if (isMemberReplaced && !activeStatementIndices.IsEmpty()) - { - diagnosticContext.Report(RudeEditKind.ChangingNameOrSignatureOfActiveMember, cancellationToken); - } - - try + else { - if (newMemberBody != null) + foreach (var activeStatementIndex in activeStatementIndices) { - _testFaultInjector?.Invoke(newMemberBody.RootNodes.First()); - } + Debug.Assert(oldMemberBody != null); - // Populated with active lambdas and matched lambdas. - // Unmatched non-active lambdas are not included. - // { old-lambda-body -> info } - Dictionary? lazyActiveOrMatchedLambdas = null; + var oldStatementSpan = oldActiveStatements[activeStatementIndex].UnmappedSpan; - // finds leaf nodes that correspond to the old active statements: - using var _ = ArrayBuilder.GetInstance(out var activeNodes); + var oldStatementSyntax = oldMemberBody.FindStatement(oldStatementSpan, out var statementPart); + Contract.ThrowIfNull(oldStatementSyntax); - if (newMemberBody == null) - { - // The name or signature has been changed and the member needs to be deleted and a new one emitted. - // The debugger does not support active statement remapping between two different methods, so report rude edits. - // - // The body has been deleted. Two cases: - // 1) The declaration is available - // Example: Deleting field initializer. - // We try to find another active statement that's in the user code (e.g. initializer that logically follows the deleted one or a constructor body statement). - // If such active statement does not exist it is still possible to remap the deleted statement into the synthesized body of the method that replaces the current body. - // (e.g. default constructor). The debugger supports active statement remapping to a method without sequence point. It will remap to the first instruction of the method. - // - // 2) The declaration is also deleted, but a synthesized one is generated in its place and thus an update edit is issued. - // Will remap active statements to the first instruction of the synthesized body (same as above). - - foreach (var activeStatementIndex in activeStatementIndices) + var oldEnclosingLambdaBody = FindEnclosingLambdaBody(oldMemberBody.EncompassingAncestor, oldStatementSyntax); + if (oldEnclosingLambdaBody != null) { - // the declaration must exist, otherwise we wouldn't find any active statements overlapping the old body - Debug.Assert(oldDeclaration != null); + lazyActiveOrMatchedLambdas ??= []; - // We have already calculated the new location of this active statement when analyzing another member declaration. - // This may only happen when two or more member declarations share the same body (VB AsNew clause). - if (newActiveStatements[activeStatementIndex] != null) + if (!lazyActiveOrMatchedLambdas.TryGetValue(oldEnclosingLambdaBody, out var lambda)) { - Debug.Assert(IsDeclarationWithSharedBody(oldDeclaration, oldMember)); - continue; + lambda = new LambdaInfo([]); + lazyActiveOrMatchedLambdas.Add(oldEnclosingLambdaBody, lambda); } - var newSpan = GetDeletedDeclarationActiveSpan(topMatch.Matches, oldDeclaration); - newActiveStatements[activeStatementIndex] = GetActiveStatementWithSpan(oldActiveStatements[activeStatementIndex], topMatch.NewRoot.SyntaxTree, newSpan, diagnostics, cancellationToken); - newExceptionRegions[activeStatementIndex] = []; + lambda.ActiveNodeIndices!.Add(activeNodes.Count); } - } - else - { - foreach (var activeStatementIndex in activeStatementIndices) - { - Debug.Assert(oldMemberBody != null); - var oldStatementSpan = oldActiveStatements[activeStatementIndex].UnmappedSpan; - - var oldStatementSyntax = oldMemberBody.FindStatement(oldStatementSpan, out var statementPart); - Contract.ThrowIfNull(oldStatementSyntax); - - var oldEnclosingLambdaBody = FindEnclosingLambdaBody(oldMemberBody.EncompassingAncestor, oldStatementSyntax); - if (oldEnclosingLambdaBody != null) - { - lazyActiveOrMatchedLambdas ??= []; - - if (!lazyActiveOrMatchedLambdas.TryGetValue(oldEnclosingLambdaBody, out var lambda)) - { - lambda = new LambdaInfo([]); - lazyActiveOrMatchedLambdas.Add(oldEnclosingLambdaBody, lambda); - } - - lambda.ActiveNodeIndices!.Add(activeNodes.Count); - } + SyntaxNode? trackedNode = null; - SyntaxNode? trackedNode = null; + if (TryGetTrackedStatement(newActiveStatementSpans, activeStatementIndex, newText, newMemberBody, out var newStatementSyntax, out var _)) + { + var newEnclosingLambdaBody = FindEnclosingLambdaBody(newMemberBody.EncompassingAncestor, newStatementSyntax); - if (TryGetTrackedStatement(newActiveStatementSpans, activeStatementIndex, newText, newMemberBody, out var newStatementSyntax, out var _)) + // The tracking span might have been moved outside of the lambda span. + // It is not an error to move the statement - we just ignore it. + if (oldEnclosingLambdaBody == newEnclosingLambdaBody && + StatementLabelEquals(oldStatementSyntax, newStatementSyntax)) { - var newEnclosingLambdaBody = FindEnclosingLambdaBody(newMemberBody.EncompassingAncestor, newStatementSyntax); - - // The tracking span might have been moved outside of the lambda span. - // It is not an error to move the statement - we just ignore it. - if (oldEnclosingLambdaBody == newEnclosingLambdaBody && - StatementLabelEquals(oldStatementSyntax, newStatementSyntax)) - { - trackedNode = newStatementSyntax; - } + trackedNode = newStatementSyntax; } - - activeNodes.Add(new ActiveNode(activeStatementIndex, oldStatementSyntax, oldEnclosingLambdaBody, statementPart, trackedNode)); } + + activeNodes.Add(new ActiveNode(activeStatementIndex, oldStatementSyntax, oldEnclosingLambdaBody, statementPart, trackedNode)); } + } - var activeNodesInBody = activeNodes.Where(n => n.EnclosingLambdaBody == null).ToArray(); + var activeNodesInBody = activeNodes.Where(n => n.EnclosingLambdaBody == null).ToArray(); - var memberBodyMap = ComputeDeclarationBodyMap(oldMemberBody, newMemberBody, activeNodesInBody); - var aggregateBodyMap = IncludeLambdaBodyMaps(memberBodyMap, activeNodes, ref lazyActiveOrMatchedLambdas); + var memberBodyMap = ComputeDeclarationBodyMap(oldMemberBody, newMemberBody, activeNodesInBody); + var aggregateBodyMap = IncludeLambdaBodyMaps(memberBodyMap, activeNodes, ref lazyActiveOrMatchedLambdas); - var oldStateMachineInfo = oldMemberBody?.GetStateMachineInfo() ?? StateMachineInfo.None; - var newStateMachineInfo = newMemberBody?.GetStateMachineInfo() ?? StateMachineInfo.None; + var oldStateMachineInfo = oldMemberBody?.GetStateMachineInfo() ?? StateMachineInfo.None; + var newStateMachineInfo = newMemberBody?.GetStateMachineInfo() ?? StateMachineInfo.None; - ReportStateMachineBodyUpdateRudeEdits(diagnosticContext, memberBodyMap, oldStateMachineInfo, newStateMachineInfo, hasActiveStatement: activeNodesInBody.Length != 0, cancellationToken); + ReportStateMachineBodyUpdateRudeEdits(diagnosticContext, memberBodyMap, oldStateMachineInfo, newStateMachineInfo, hasActiveStatement: activeNodesInBody.Length != 0, cancellationToken); - ReportMemberOrLambdaBodyUpdateRudeEdits( - diagnosticContext, - oldCompilation, - oldDeclaration, - oldMember, - oldMemberBody, - oldMemberBody, - newDeclaration, - newMember, - newMemberBody, - newMemberBody, - capabilities, - oldStateMachineInfo, - newStateMachineInfo, - cancellationToken); + ReportMemberOrLambdaBodyUpdateRudeEdits( + diagnosticContext, + oldCompilation, + oldDeclaration, + oldMember, + oldMemberBody, + oldMemberBody, + newDeclaration, + newMember, + newMemberBody, + newMemberBody, + capabilities, + oldStateMachineInfo, + newStateMachineInfo, + cancellationToken); - ReportLambdaAndClosureRudeEdits( - oldModel, - oldMember, - oldMemberBody, - oldDeclaration, - newModel, - newMember, - newMemberBody, - newDeclaration, - lazyActiveOrMatchedLambdas, - aggregateBodyMap, - capabilities, - diagnostics, - out var newBodyHasLambdas, - out var runtimeRudeEdits, - cancellationToken); + ReportLambdaAndClosureRudeEdits( + oldModel, + oldMember, + oldMemberBody, + oldDeclaration, + newModel, + newMember, + newMemberBody, + newDeclaration, + lazyActiveOrMatchedLambdas, + aggregateBodyMap, + capabilities, + diagnostics, + out var newBodyHasLambdas, + out var runtimeRudeEdits, + cancellationToken); - // We need to provide syntax map to the compiler if - // 1) The new member has a active statement - // The values of local variables declared or synthesized in the method have to be preserved. - // 2) The new member generates a state machine - // In case the state machine is suspended we need to preserve variables. - // 3) The new member contains lambdas - // We need to map new lambdas in the method to the matching old ones. - // If the old method has lambdas but the new one doesn't there is nothing to preserve. - // 4) Constructor that emits initializers is updated. - // We create syntax map even if it's not necessary: if any data member initializers are active/contain lambdas. - // Since initializers are usually simple the map should not be large enough to make it worth optimizing it away. - var matchingNodes = - (!activeNodes.IsEmpty() || - newStateMachineInfo.HasSuspensionPoints || - newBodyHasLambdas || - IsConstructorWithMemberInitializers(newMember, cancellationToken) || - oldDeclaration != null && IsDeclarationWithInitializer(oldDeclaration) || - newDeclaration != null && IsDeclarationWithInitializer(newDeclaration)) - ? CreateSyntaxMap(aggregateBodyMap) - : null; - - syntaxMaps = new SyntaxMaps(newModel.SyntaxTree, matchingNodes, runtimeRudeEdits); - - foreach (var activeNode in activeNodes) - { - Debug.Assert(oldMemberBody != null); - Debug.Assert(oldDeclaration != null); + // We need to provide syntax map to the compiler if + // 1) The new member has a active statement + // The values of local variables declared or synthesized in the method have to be preserved. + // 2) The new member generates a state machine + // In case the state machine is suspended we need to preserve variables. + // 3) The new member contains lambdas + // We need to map new lambdas in the method to the matching old ones. + // If the old method has lambdas but the new one doesn't there is nothing to preserve. + // 4) Constructor that emits initializers is updated. + // We create syntax map even if it's not necessary: if any data member initializers are active/contain lambdas. + // Since initializers are usually simple the map should not be large enough to make it worth optimizing it away. + var matchingNodes = + (!activeNodes.IsEmpty() || + newStateMachineInfo.HasSuspensionPoints || + newBodyHasLambdas || + IsConstructorWithMemberInitializers(newMember, cancellationToken) || + oldDeclaration != null && IsDeclarationWithInitializer(oldDeclaration) || + newDeclaration != null && IsDeclarationWithInitializer(newDeclaration)) + ? CreateSyntaxMap(aggregateBodyMap) + : null; + + syntaxMaps = new SyntaxMaps(newModel.SyntaxTree, matchingNodes, runtimeRudeEdits); - var activeStatementIndex = activeNode.ActiveStatementIndex; - var isNonLeaf = oldActiveStatements[activeStatementIndex].Statement.IsNonLeaf; - var isPartiallyExecuted = (oldActiveStatements[activeStatementIndex].Statement.Flags & ActiveStatementFlags.PartiallyExecuted) != 0; - var statementPart = activeNode.StatementPart; - var oldStatementSyntax = activeNode.OldNode; - var oldEnclosingLambdaBody = activeNode.EnclosingLambdaBody; + foreach (var activeNode in activeNodes) + { + Debug.Assert(oldMemberBody != null); + Debug.Assert(oldDeclaration != null); - newExceptionRegions[activeStatementIndex] = []; + var activeStatementIndex = activeNode.ActiveStatementIndex; + var isNonLeaf = oldActiveStatements[activeStatementIndex].Statement.IsNonLeaf; + var isPartiallyExecuted = (oldActiveStatements[activeStatementIndex].Statement.Flags & ActiveStatementFlags.PartiallyExecuted) != 0; + var statementPart = activeNode.StatementPart; + var oldStatementSyntax = activeNode.OldNode; + var oldEnclosingLambdaBody = activeNode.EnclosingLambdaBody; - DeclarationBodyMap enclosingBodyMap; - DeclarationBody oldBody; - DeclarationBody? newBody; - if (oldEnclosingLambdaBody == null) - { - enclosingBodyMap = memberBodyMap; - oldBody = oldMemberBody; - newBody = newMemberBody; - } - else - { - Debug.Assert(lazyActiveOrMatchedLambdas != null); + newExceptionRegions[activeStatementIndex] = []; - var matchingLambdaInfo = lazyActiveOrMatchedLambdas[oldEnclosingLambdaBody]; - enclosingBodyMap = matchingLambdaInfo.BodyMap; - oldBody = oldEnclosingLambdaBody; - newBody = matchingLambdaInfo.NewBody; - } + DeclarationBodyMap enclosingBodyMap; + DeclarationBody oldBody; + DeclarationBody? newBody; + if (oldEnclosingLambdaBody == null) + { + enclosingBodyMap = memberBodyMap; + oldBody = oldMemberBody; + newBody = newMemberBody; + } + else + { + Debug.Assert(lazyActiveOrMatchedLambdas != null); - bool hasMatching; - SyntaxNode? newStatementSyntax; - if (newBody != null) - { - hasMatching = oldBody.TryMatchActiveStatement(newBody, oldStatementSyntax, ref statementPart, out newStatementSyntax); - if (!hasMatching) - { - // If the body has an empty mapping then all active statements in the body must be mapped by TryMatchActiveStatement. - Debug.Assert(!enclosingBodyMap.Forward.IsEmpty()); + var matchingLambdaInfo = lazyActiveOrMatchedLambdas[oldEnclosingLambdaBody]; + enclosingBodyMap = matchingLambdaInfo.BodyMap; + oldBody = oldEnclosingLambdaBody; + newBody = matchingLambdaInfo.NewBody; + } - hasMatching = enclosingBodyMap.Forward.TryGetValue(oldStatementSyntax, out newStatementSyntax); - } - } - else + bool hasMatching; + SyntaxNode? newStatementSyntax; + if (newBody != null) + { + hasMatching = oldBody.TryMatchActiveStatement(newBody, oldStatementSyntax, ref statementPart, out newStatementSyntax); + if (!hasMatching) { - // Lambda match is null if lambdas can't be matched, - // in such case we won't have active statement matched either. - hasMatching = false; - newStatementSyntax = null; - } + // If the body has an empty mapping then all active statements in the body must be mapped by TryMatchActiveStatement. + Debug.Assert(!enclosingBodyMap.Forward.IsEmpty()); - TextSpan newSpan; - if (hasMatching) - { - Debug.Assert(newStatementSyntax != null); - Debug.Assert(newBody != null); + hasMatching = enclosingBodyMap.Forward.TryGetValue(oldStatementSyntax, out newStatementSyntax); + } + } + else + { + // Lambda match is null if lambdas can't be matched, + // in such case we won't have active statement matched either. + hasMatching = false; + newStatementSyntax = null; + } - // The matching node doesn't produce sequence points. - // E.g. "const" keyword is inserted into a local variable declaration with an initializer. - newSpan = FindClosestActiveSpan(newStatementSyntax, statementPart); + TextSpan newSpan; + if (hasMatching) + { + Debug.Assert(newStatementSyntax != null); + Debug.Assert(newBody != null); - if ((isNonLeaf || isPartiallyExecuted) && !AreEquivalentActiveStatements(oldStatementSyntax, newStatementSyntax, statementPart)) - { - // rude edit: non-leaf active statement changed - diagnostics.Add(new RudeEditDiagnostic(isNonLeaf ? RudeEditKind.ActiveStatementUpdate : RudeEditKind.PartiallyExecutedActiveStatementUpdate, newSpan)); - } + // The matching node doesn't produce sequence points. + // E.g. "const" keyword is inserted into a local variable declaration with an initializer. + newSpan = FindClosestActiveSpan(newStatementSyntax, statementPart); - // other statements around active statement: - ReportOtherRudeEditsAroundActiveStatement( - diagnostics, - enclosingBodyMap.Reverse, - oldStatementSyntax, - oldBody, - newStatementSyntax, - newBody, - isNonLeaf); - } - else if (enclosingBodyMap.Forward.IsEmpty()) + if ((isNonLeaf || isPartiallyExecuted) && !AreEquivalentActiveStatements(oldStatementSyntax, newStatementSyntax, statementPart)) { - Debug.Assert(oldEnclosingLambdaBody != null); - Debug.Assert(lazyActiveOrMatchedLambdas != null); - - newSpan = GetDeletedNodeDiagnosticSpan(oldEnclosingLambdaBody, oldMemberBody.EncompassingAncestor, memberBodyMap.Forward, lazyActiveOrMatchedLambdas); - - // Lambda containing the active statement can't be found in the new source. - var oldLambda = oldEnclosingLambdaBody.GetLambda(); - diagnostics.Add(new RudeEditDiagnostic(RudeEditKind.ActiveStatementLambdaRemoved, newSpan, oldLambda, - [GetDisplayName(oldLambda)])); + // rude edit: non-leaf active statement changed + diagnostics.Add(new RudeEditDiagnostic(isNonLeaf ? RudeEditKind.ActiveStatementUpdate : RudeEditKind.PartiallyExecutedActiveStatementUpdate, newSpan)); } - else - { - newSpan = GetDeletedNodeActiveSpan(enclosingBodyMap.Forward, oldStatementSyntax); - if (isNonLeaf || isPartiallyExecuted) - { - // rude edit: internal active statement deleted - diagnostics.Add( - new RudeEditDiagnostic(isNonLeaf ? RudeEditKind.DeleteActiveStatement : RudeEditKind.PartiallyExecutedActiveStatementDelete, - GetDeletedNodeDiagnosticSpan(enclosingBodyMap.Forward, oldStatementSyntax), - arguments: [FeaturesResources.code])); - } - } + // other statements around active statement: + ReportOtherRudeEditsAroundActiveStatement( + diagnostics, + enclosingBodyMap.Reverse, + oldStatementSyntax, + oldBody, + newStatementSyntax, + newBody, + isNonLeaf); + } + else if (enclosingBodyMap.Forward.IsEmpty()) + { + Debug.Assert(oldEnclosingLambdaBody != null); + Debug.Assert(lazyActiveOrMatchedLambdas != null); - // If there was a lambda, but we couldn't match its body to the new tree, then the lambda was - // removed, so we don't need to check it for active statements. If there wasn't a lambda then - // match here will be the same as bodyMatch. - if (newBody != null) - { - // exception handling around the statement: - CalculateExceptionRegionsAroundActiveStatement( - enclosingBodyMap.Forward, - oldStatementSyntax, - oldBody.EncompassingAncestor, - newStatementSyntax, - newBody.EncompassingAncestor, - newSpan, - activeStatementIndex, - isNonLeaf, - newExceptionRegions, - diagnostics, - cancellationToken); - } + newSpan = GetDeletedNodeDiagnosticSpan(oldEnclosingLambdaBody, oldMemberBody.EncompassingAncestor, memberBodyMap.Forward, lazyActiveOrMatchedLambdas); - // We have already calculated the new location of this active statement when analyzing another member declaration. - // This may only happen when two or more member declarations share the same body (VB AsNew clause). - Debug.Assert(newSpan != default); + // Lambda containing the active statement can't be found in the new source. + var oldLambda = oldEnclosingLambdaBody.GetLambda(); + diagnostics.Add(new RudeEditDiagnostic(RudeEditKind.ActiveStatementLambdaRemoved, newSpan, oldLambda, + [GetDisplayName(oldLambda)])); + } + else + { + newSpan = GetDeletedNodeActiveSpan(enclosingBodyMap.Forward, oldStatementSyntax); - if (newActiveStatements[activeStatementIndex] == null) + if (isNonLeaf || isPartiallyExecuted) { - // Currently, the debugger does not support remapping to a different source file, - // so the new active statement must be in the syntax tree being analyzed. - newActiveStatements[activeStatementIndex] = GetActiveStatementWithSpan(oldActiveStatements[activeStatementIndex], newModel.SyntaxTree, newSpan, diagnostics, cancellationToken); - } - else - { - Debug.Assert(IsDeclarationWithSharedBody(oldDeclaration, oldMember)); + // rude edit: internal active statement deleted + diagnostics.Add( + new RudeEditDiagnostic(isNonLeaf ? RudeEditKind.DeleteActiveStatement : RudeEditKind.PartiallyExecutedActiveStatementDelete, + GetDeletedNodeDiagnosticSpan(enclosingBodyMap.Forward, oldStatementSyntax), + arguments: [FeaturesResources.code])); } } - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - // Set the new spans of active statements overlapping the method body to match the old spans. - // Even though these might be now outside of the method body it's ok since we report a rude edit and don't allow to continue. - foreach (var i in activeStatementIndices) - { - newActiveStatements[i] = oldActiveStatements[i].Statement; - newExceptionRegions[i] = []; + // If there was a lambda, but we couldn't match its body to the new tree, then the lambda was + // removed, so we don't need to check it for active statements. If there wasn't a lambda then + // match here will be the same as bodyMatch. + if (newBody != null) + { + // exception handling around the statement: + CalculateExceptionRegionsAroundActiveStatement( + enclosingBodyMap.Forward, + oldStatementSyntax, + oldBody.EncompassingAncestor, + newStatementSyntax, + newBody.EncompassingAncestor, + newSpan, + activeStatementIndex, + isNonLeaf, + newExceptionRegions, + diagnostics, + cancellationToken); } - // We expect OOM to be thrown during the analysis if the number of statements is too large. - // In such case we report a rude edit for the document. If the host is actually running out of memory, - // it might throw another OOM here or later on. - if (e is OutOfMemoryException) + // We have already calculated the new location of this active statement when analyzing another member declaration. + // This may only happen when two or more member declarations share the same body (VB AsNew clause). + Debug.Assert(newSpan != default); + + if (newActiveStatements[activeStatementIndex] == null) { - diagnosticContext.Report(RudeEditKind.MemberBodyTooBig, cancellationToken, arguments: [newMember.Name]); + // Currently, the debugger does not support remapping to a different source file, + // so the new active statement must be in the syntax tree being analyzed. + newActiveStatements[activeStatementIndex] = GetActiveStatementWithSpan(oldActiveStatements[activeStatementIndex], newModel.SyntaxTree, newSpan, diagnostics, cancellationToken); } else { - diagnosticContext.Report(RudeEditKind.MemberBodyInternalError, cancellationToken, arguments: [newMember.Name, e.ToString()]); + Debug.Assert(IsDeclarationWithSharedBody(oldDeclaration, oldMember)); } - - syntaxMaps = new SyntaxMaps(newModel.SyntaxTree); } } - - private static bool TryGetTrackedStatement(ImmutableArray activeStatementSpans, int index, SourceText text, MemberBody body, [NotNullWhen(true)] out SyntaxNode? trackedStatement, out int trackedStatementPart) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - trackedStatement = null; - trackedStatementPart = -1; + // Set the new spans of active statements overlapping the method body to match the old spans. + // Even though these might be now outside of the method body it's ok since we report a rude edit and don't allow to continue. - // Active statements are not tracked in this document (e.g. the file is closed). - if (activeStatementSpans.IsEmpty) + foreach (var i in activeStatementIndices) { - return false; + newActiveStatements[i] = oldActiveStatements[i].Statement; + newExceptionRegions[i] = []; } - var trackedLineSpan = activeStatementSpans[index]; - if (trackedLineSpan == default) + // We expect OOM to be thrown during the analysis if the number of statements is too large. + // In such case we report a rude edit for the document. If the host is actually running out of memory, + // it might throw another OOM here or later on. + if (e is OutOfMemoryException) { - return false; + diagnosticContext.Report(RudeEditKind.MemberBodyTooBig, cancellationToken, arguments: [newMember.Name]); } - - var trackedSpan = text.Lines.GetTextSpan(trackedLineSpan); - - // The tracking span might have been deleted or moved outside of the member span. - // It is not an error to move the statement - we just ignore it. - // Consider: Instead of checking here, explicitly handle all cases when active statements can be outside of the body in FindStatement and - // return false if the requested span is outside of the active envelope. - if (!body.Envelope.Contains(trackedSpan)) + else { - return false; + diagnosticContext.Report(RudeEditKind.MemberBodyInternalError, cancellationToken, arguments: [newMember.Name, e.ToString()]); } - trackedStatement = body.FindStatement(trackedSpan, out trackedStatementPart); - return true; + syntaxMaps = new SyntaxMaps(newModel.SyntaxTree); } + } + + private static bool TryGetTrackedStatement(ImmutableArray activeStatementSpans, int index, SourceText text, MemberBody body, [NotNullWhen(true)] out SyntaxNode? trackedStatement, out int trackedStatementPart) + { + trackedStatement = null; + trackedStatementPart = -1; - private ActiveStatement GetActiveStatementWithSpan(UnmappedActiveStatement oldStatement, SyntaxTree newTree, TextSpan newSpan, ArrayBuilder diagnostics, CancellationToken cancellationToken) + // Active statements are not tracked in this document (e.g. the file is closed). + if (activeStatementSpans.IsEmpty) { - var mappedLineSpan = newTree.GetMappedLineSpan(newSpan, cancellationToken); - if (mappedLineSpan.HasMappedPath && mappedLineSpan.Path != oldStatement.Statement.FileSpan.Path) - { - // TODO: consider supporting moving AS to a different file -- https://github.com/dotnet/roslyn/issues/51177. - // changing the source file of an active statement - diagnostics.Add(new RudeEditDiagnostic( - RudeEditKind.UpdateAroundActiveStatement, - newSpan, - LineDirectiveSyntaxKind, - arguments: [string.Format(FeaturesResources._0_directive, LineDirectiveKeyword)])); - } + return false; + } - return oldStatement.Statement.WithFileSpan(mappedLineSpan); + var trackedLineSpan = activeStatementSpans[index]; + if (trackedLineSpan == default) + { + return false; } - private void CalculateExceptionRegionsAroundActiveStatement( - IReadOnlyDictionary forwardMap, - SyntaxNode oldStatementSyntax, - SyntaxNode oldEncompassingAncestor, - SyntaxNode? newStatementSyntax, - SyntaxNode newEncompassingAncestor, - TextSpan newStatementSyntaxSpan, - int ordinal, - bool isNonLeaf, - ImmutableArray>.Builder newExceptionRegions, - ArrayBuilder diagnostics, - CancellationToken cancellationToken) + var trackedSpan = text.Lines.GetTextSpan(trackedLineSpan); + + // The tracking span might have been deleted or moved outside of the member span. + // It is not an error to move the statement - we just ignore it. + // Consider: Instead of checking here, explicitly handle all cases when active statements can be outside of the body in FindStatement and + // return false if the requested span is outside of the active envelope. + if (!body.Envelope.Contains(trackedSpan)) { - if (newStatementSyntax == null) - { - if (!newEncompassingAncestor.Span.Contains(newStatementSyntaxSpan.Start)) - { - return; - } + return false; + } - newStatementSyntax = newEncompassingAncestor.FindToken(newStatementSyntaxSpan.Start).Parent; + trackedStatement = body.FindStatement(trackedSpan, out trackedStatementPart); + return true; + } - Contract.ThrowIfNull(newStatementSyntax); - } + private ActiveStatement GetActiveStatementWithSpan(UnmappedActiveStatement oldStatement, SyntaxTree newTree, TextSpan newSpan, ArrayBuilder diagnostics, CancellationToken cancellationToken) + { + var mappedLineSpan = newTree.GetMappedLineSpan(newSpan, cancellationToken); + if (mappedLineSpan.HasMappedPath && mappedLineSpan.Path != oldStatement.Statement.FileSpan.Path) + { + // TODO: consider supporting moving AS to a different file -- https://github.com/dotnet/roslyn/issues/51177. + // changing the source file of an active statement + diagnostics.Add(new RudeEditDiagnostic( + RudeEditKind.UpdateAroundActiveStatement, + newSpan, + LineDirectiveSyntaxKind, + arguments: [string.Format(FeaturesResources._0_directive, LineDirectiveKeyword)])); + } - var oldAncestors = GetExceptionHandlingAncestors(oldStatementSyntax, oldEncompassingAncestor, isNonLeaf); - var newAncestors = GetExceptionHandlingAncestors(newStatementSyntax, newEncompassingAncestor, isNonLeaf); + return oldStatement.Statement.WithFileSpan(mappedLineSpan); + } - if (oldAncestors.Count > 0 || newAncestors.Count > 0) + private void CalculateExceptionRegionsAroundActiveStatement( + IReadOnlyDictionary forwardMap, + SyntaxNode oldStatementSyntax, + SyntaxNode oldEncompassingAncestor, + SyntaxNode? newStatementSyntax, + SyntaxNode newEncompassingAncestor, + TextSpan newStatementSyntaxSpan, + int ordinal, + bool isNonLeaf, + ImmutableArray>.Builder newExceptionRegions, + ArrayBuilder diagnostics, + CancellationToken cancellationToken) + { + if (newStatementSyntax == null) + { + if (!newEncompassingAncestor.Span.Contains(newStatementSyntaxSpan.Start)) { - var edits = new MapBasedLongestCommonSubsequence(forwardMap).GetEdits(oldAncestors, newAncestors); - ReportEnclosingExceptionHandlingRudeEdits(diagnostics, edits, oldStatementSyntax, newStatementSyntaxSpan); - - // Exception regions are not needed in presence of errors. - if (diagnostics.Count == 0) - { - Debug.Assert(oldAncestors.Count == newAncestors.Count); - newExceptionRegions[ordinal] = GetExceptionRegions(newAncestors, newStatementSyntax.SyntaxTree, cancellationToken).Spans; - } + return; } + + newStatementSyntax = newEncompassingAncestor.FindToken(newStatementSyntaxSpan.Start).Parent; + + Contract.ThrowIfNull(newStatementSyntax); } - /// - /// Calculates a syntax map of the entire method body including all lambda bodies it contains. - /// - private DeclarationBodyMap IncludeLambdaBodyMaps( - DeclarationBodyMap memberBodyMap, - ArrayBuilder memberBodyActiveNodes, - ref Dictionary? lazyActiveOrMatchedLambdas) + var oldAncestors = GetExceptionHandlingAncestors(oldStatementSyntax, oldEncompassingAncestor, isNonLeaf); + var newAncestors = GetExceptionHandlingAncestors(newStatementSyntax, newEncompassingAncestor, isNonLeaf); + + if (oldAncestors.Count > 0 || newAncestors.Count > 0) { - ArrayBuilder<(DeclarationBodyMap map, SyntaxNode? oldLambda)>? lambdaBodyMaps = null; - SyntaxNode? currentOldLambda = null; - var currentLambdaBodyMatch = -1; - var currentBodyMap = memberBodyMap; + var edits = new MapBasedLongestCommonSubsequence(forwardMap).GetEdits(oldAncestors, newAncestors); + ReportEnclosingExceptionHandlingRudeEdits(diagnostics, edits, oldStatementSyntax, newStatementSyntaxSpan); - while (true) + // Exception regions are not needed in presence of errors. + if (diagnostics.Count == 0) { - foreach (var (oldNode, newNode) in currentBodyMap.Forward) + Debug.Assert(oldAncestors.Count == newAncestors.Count); + newExceptionRegions[ordinal] = GetExceptionRegions(newAncestors, newStatementSyntax.SyntaxTree, cancellationToken).Spans; + } + } + } + + /// + /// Calculates a syntax map of the entire method body including all lambda bodies it contains. + /// + private DeclarationBodyMap IncludeLambdaBodyMaps( + DeclarationBodyMap memberBodyMap, + ArrayBuilder memberBodyActiveNodes, + ref Dictionary? lazyActiveOrMatchedLambdas) + { + ArrayBuilder<(DeclarationBodyMap map, SyntaxNode? oldLambda)>? lambdaBodyMaps = null; + SyntaxNode? currentOldLambda = null; + var currentLambdaBodyMatch = -1; + var currentBodyMap = memberBodyMap; + + while (true) + { + foreach (var (oldNode, newNode) in currentBodyMap.Forward) + { + // the node is a declaration of the current lambda (we already processed it): + if (oldNode == currentOldLambda) + { + continue; + } + + if (TryGetLambdaBodies(oldNode, out var oldLambdaBody1, out var oldLambdaBody2)) { - // the node is a declaration of the current lambda (we already processed it): - if (oldNode == currentOldLambda) + lambdaBodyMaps ??= ArrayBuilder<(DeclarationBodyMap, SyntaxNode?)>.GetInstance(); + lazyActiveOrMatchedLambdas ??= []; + + var newLambdaBody1 = oldLambdaBody1.TryGetPartnerLambdaBody(newNode); + if (newLambdaBody1 != null) { - continue; + lambdaBodyMaps.Add((ComputeLambdaBodyMap(oldLambdaBody1, newLambdaBody1, memberBodyActiveNodes, lazyActiveOrMatchedLambdas), oldNode)); } - if (TryGetLambdaBodies(oldNode, out var oldLambdaBody1, out var oldLambdaBody2)) + if (oldLambdaBody2 != null) { - lambdaBodyMaps ??= ArrayBuilder<(DeclarationBodyMap, SyntaxNode?)>.GetInstance(); - lazyActiveOrMatchedLambdas ??= []; - - var newLambdaBody1 = oldLambdaBody1.TryGetPartnerLambdaBody(newNode); - if (newLambdaBody1 != null) + var newLambdaBody2 = oldLambdaBody2.TryGetPartnerLambdaBody(newNode); + if (newLambdaBody2 != null) { - lambdaBodyMaps.Add((ComputeLambdaBodyMap(oldLambdaBody1, newLambdaBody1, memberBodyActiveNodes, lazyActiveOrMatchedLambdas), oldNode)); - } - - if (oldLambdaBody2 != null) - { - var newLambdaBody2 = oldLambdaBody2.TryGetPartnerLambdaBody(newNode); - if (newLambdaBody2 != null) - { - lambdaBodyMaps.Add((ComputeLambdaBodyMap(oldLambdaBody2, newLambdaBody2, memberBodyActiveNodes, lazyActiveOrMatchedLambdas), oldNode)); - } + lambdaBodyMaps.Add((ComputeLambdaBodyMap(oldLambdaBody2, newLambdaBody2, memberBodyActiveNodes, lazyActiveOrMatchedLambdas), oldNode)); } } } - - currentLambdaBodyMatch++; - if (lambdaBodyMaps == null || currentLambdaBodyMatch == lambdaBodyMaps.Count) - { - break; - } - - (currentBodyMap, currentOldLambda) = lambdaBodyMaps[currentLambdaBodyMatch]; } - if (lambdaBodyMaps == null) + currentLambdaBodyMatch++; + if (lambdaBodyMaps == null || currentLambdaBodyMatch == lambdaBodyMaps.Count) { - return memberBodyMap; + break; } - var map = new Dictionary(); - var additionalReverseMap = ImmutableDictionary.CreateBuilder(); + (currentBodyMap, currentOldLambda) = lambdaBodyMaps[currentLambdaBodyMatch]; + } + + if (lambdaBodyMaps == null) + { + return memberBodyMap; + } + + var map = new Dictionary(); + var additionalReverseMap = ImmutableDictionary.CreateBuilder(); - // include all matches and additional mappings, including the root: - map.AddRange(memberBodyMap.Forward); - additionalReverseMap.AddRange(memberBodyMap.AdditionalReverseMapping); + // include all matches and additional mappings, including the root: + map.AddRange(memberBodyMap.Forward); + additionalReverseMap.AddRange(memberBodyMap.AdditionalReverseMapping); - foreach (var (lambdaBodyMap, _) in lambdaBodyMaps) + foreach (var (lambdaBodyMap, _) in lambdaBodyMaps) + { + foreach (var (oldNode, newNode) in lambdaBodyMap.Forward) { - foreach (var (oldNode, newNode) in lambdaBodyMap.Forward) + if (!map.ContainsKey(oldNode)) { - if (!map.ContainsKey(oldNode)) - { - map[oldNode] = newNode; - } + map[oldNode] = newNode; } - - additionalReverseMap.AddRange(lambdaBodyMap.AdditionalReverseMapping); } - lambdaBodyMaps?.Free(); - - return new DeclarationBodyMap( - map, - map.ToDictionary(keySelector: entry => entry.Value, elementSelector: entry => entry.Key), - additionalReverseMap.ToImmutable()); + additionalReverseMap.AddRange(lambdaBodyMap.AdditionalReverseMapping); } - private static DeclarationBodyMap ComputeLambdaBodyMap( - LambdaBody oldLambdaBody, - LambdaBody newLambdaBody, - IReadOnlyList memberBodyActiveNodes, - [Out] Dictionary activeOrMatchedLambdas) - { - IEnumerable activeNodesInLambdaBody; - if (activeOrMatchedLambdas.TryGetValue(oldLambdaBody, out var info)) - { - // Lambda may be matched but not be active. - activeNodesInLambdaBody = info.ActiveNodeIndices?.Select(i => memberBodyActiveNodes[i]) ?? []; - } - else - { - // If the lambda body isn't in the map it doesn't have any active/tracked statements. - activeNodesInLambdaBody = []; - info = new LambdaInfo(); - } - - var lambdaBodyMatch = ComputeDeclarationBodyMap(oldLambdaBody, newLambdaBody, activeNodesInLambdaBody); + lambdaBodyMaps?.Free(); - activeOrMatchedLambdas[oldLambdaBody] = info.WithMatch(lambdaBodyMatch, newLambdaBody); + return new DeclarationBodyMap( + map, + map.ToDictionary(keySelector: entry => entry.Value, elementSelector: entry => entry.Key), + additionalReverseMap.ToImmutable()); + } - return lambdaBodyMatch; + private static DeclarationBodyMap ComputeLambdaBodyMap( + LambdaBody oldLambdaBody, + LambdaBody newLambdaBody, + IReadOnlyList memberBodyActiveNodes, + [Out] Dictionary activeOrMatchedLambdas) + { + IEnumerable activeNodesInLambdaBody; + if (activeOrMatchedLambdas.TryGetValue(oldLambdaBody, out var info)) + { + // Lambda may be matched but not be active. + activeNodesInLambdaBody = info.ActiveNodeIndices?.Select(i => memberBodyActiveNodes[i]) ?? []; } - - /// - /// Called for a member body and for bodies of all lambdas and local functions (recursively) found in the member body. - /// - private static DeclarationBodyMap ComputeDeclarationBodyMap(DeclarationBody? oldBody, DeclarationBody? newBody, IEnumerable activeNodes) - => (oldBody != null && newBody != null) - ? oldBody.ComputeMap(newBody, knownMatches: GetMatchingActiveNodes(activeNodes)) - : DeclarationBodyMap.Empty; - - private void ReportStateMachineBodyUpdateRudeEdits( - in DiagnosticContext diagnosticContext, - DeclarationBodyMap bodyMap, - StateMachineInfo oldStateMachineInfo, - StateMachineInfo newStateMachineInfo, - bool hasActiveStatement, - CancellationToken cancellationToken) + else { - // Consider following cases: - // 1) The new method contains yields/awaits but the old doesn't. - // If the method has active statements report rude edits for each inserted yield/await (insert "around" an active statement). - // 2) The old method is async/iterator, the new method is not and it contains an active statement. - // Report rude edit since we can't remap IP from MoveNext to the kickoff method. - // Note that iterators in VB don't need to contain yield, so this case is not covered by change in number of yields. + // If the lambda body isn't in the map it doesn't have any active/tracked statements. + activeNodesInLambdaBody = []; + info = new LambdaInfo(); + } - if (oldStateMachineInfo.HasSuspensionPoints) - { - foreach (var (oldNode, newNode) in bodyMap.Forward) - { - ReportStateMachineSuspensionPointRudeEdits(diagnosticContext, oldNode, newNode); - } - } + var lambdaBodyMatch = ComputeDeclarationBodyMap(oldLambdaBody, newLambdaBody, activeNodesInLambdaBody); - // It is allowed to update a regular method to an async method or an iterator. - // The only restriction is a presence of an active statement in the method body - // since the debugger does not support remapping active statements to a different method. - if (hasActiveStatement && oldStateMachineInfo.IsStateMachine != newStateMachineInfo.IsStateMachine) - { - diagnosticContext.Report(RudeEditKind.UpdatingStateMachineMethodAroundActiveStatement, cancellationToken, arguments: []); - } + activeOrMatchedLambdas[oldLambdaBody] = info.WithMatch(lambdaBodyMatch, newLambdaBody); - // report removing async as rude: - if (oldStateMachineInfo.IsAsync && !newStateMachineInfo.IsAsync) - { - diagnosticContext.Report(RudeEditKind.ChangingFromAsynchronousToSynchronous, cancellationToken); - } + return lambdaBodyMatch; + } - // VB supports iterator lambdas/methods without yields - if (oldStateMachineInfo.IsIterator && !newStateMachineInfo.IsIterator) + /// + /// Called for a member body and for bodies of all lambdas and local functions (recursively) found in the member body. + /// + private static DeclarationBodyMap ComputeDeclarationBodyMap(DeclarationBody? oldBody, DeclarationBody? newBody, IEnumerable activeNodes) + => (oldBody != null && newBody != null) + ? oldBody.ComputeMap(newBody, knownMatches: GetMatchingActiveNodes(activeNodes)) + : DeclarationBodyMap.Empty; + + private void ReportStateMachineBodyUpdateRudeEdits( + in DiagnosticContext diagnosticContext, + DeclarationBodyMap bodyMap, + StateMachineInfo oldStateMachineInfo, + StateMachineInfo newStateMachineInfo, + bool hasActiveStatement, + CancellationToken cancellationToken) + { + // Consider following cases: + // 1) The new method contains yields/awaits but the old doesn't. + // If the method has active statements report rude edits for each inserted yield/await (insert "around" an active statement). + // 2) The old method is async/iterator, the new method is not and it contains an active statement. + // Report rude edit since we can't remap IP from MoveNext to the kickoff method. + // Note that iterators in VB don't need to contain yield, so this case is not covered by change in number of yields. + + if (oldStateMachineInfo.HasSuspensionPoints) + { + foreach (var (oldNode, newNode) in bodyMap.Forward) { - diagnosticContext.Report(RudeEditKind.ModifiersUpdate, cancellationToken); + ReportStateMachineSuspensionPointRudeEdits(diagnosticContext, oldNode, newNode); } } - private static List>? GetMatchingActiveNodes(IEnumerable activeNodes) + // It is allowed to update a regular method to an async method or an iterator. + // The only restriction is a presence of an active statement in the method body + // since the debugger does not support remapping active statements to a different method. + if (hasActiveStatement && oldStateMachineInfo.IsStateMachine != newStateMachineInfo.IsStateMachine) { - // add nodes that are tracked by the editor buffer to known matches: - List>? lazyKnownMatches = null; - - foreach (var activeNode in activeNodes) - { - if (activeNode.NewTrackedNode != null) - { - lazyKnownMatches ??= []; - lazyKnownMatches.Add(KeyValuePairUtil.Create(activeNode.OldNode, activeNode.NewTrackedNode)); - } - } - - return lazyKnownMatches; + diagnosticContext.Report(RudeEditKind.UpdatingStateMachineMethodAroundActiveStatement, cancellationToken, arguments: []); } - public ActiveStatementExceptionRegions GetExceptionRegions(SyntaxNode root, TextSpan unmappedActiveStatementSpan, bool isNonLeaf, CancellationToken cancellationToken) + // report removing async as rude: + if (oldStateMachineInfo.IsAsync && !newStateMachineInfo.IsAsync) { - var node = root.FindToken(unmappedActiveStatementSpan.Start).Parent; - Debug.Assert(node != null); + diagnosticContext.Report(RudeEditKind.ChangingFromAsynchronousToSynchronous, cancellationToken); + } - var ancestors = GetExceptionHandlingAncestors(node, root, isNonLeaf); - return GetExceptionRegions(ancestors, root.SyntaxTree, cancellationToken); + // VB supports iterator lambdas/methods without yields + if (oldStateMachineInfo.IsIterator && !newStateMachineInfo.IsIterator) + { + diagnosticContext.Report(RudeEditKind.ModifiersUpdate, cancellationToken); } + } + + private static List>? GetMatchingActiveNodes(IEnumerable activeNodes) + { + // add nodes that are tracked by the editor buffer to known matches: + List>? lazyKnownMatches = null; - private ActiveStatementExceptionRegions GetExceptionRegions(List exceptionHandlingAncestors, SyntaxTree tree, CancellationToken cancellationToken) + foreach (var activeNode in activeNodes) { - if (exceptionHandlingAncestors.Count == 0) + if (activeNode.NewTrackedNode != null) { - return new ActiveStatementExceptionRegions([], isActiveStatementCovered: false); + lazyKnownMatches ??= []; + lazyKnownMatches.Add(KeyValuePairUtil.Create(activeNode.OldNode, activeNode.NewTrackedNode)); } + } - var isCovered = false; - using var _ = ArrayBuilder.GetInstance(out var result); - - for (var i = exceptionHandlingAncestors.Count - 1; i >= 0; i--) - { - var span = GetExceptionHandlingRegion(exceptionHandlingAncestors[i], out var coversAllChildren); + return lazyKnownMatches; + } - // TODO: https://github.com/dotnet/roslyn/issues/52971 - // 1) Check that the span doesn't cross #line pragmas with different file mappings. - // 2) Check that the mapped path does not change and report rude edits if it does. - result.Add(tree.GetMappedLineSpan(span, cancellationToken)); + public ActiveStatementExceptionRegions GetExceptionRegions(SyntaxNode root, TextSpan unmappedActiveStatementSpan, bool isNonLeaf, CancellationToken cancellationToken) + { + var node = root.FindToken(unmappedActiveStatementSpan.Start).Parent; + Debug.Assert(node != null); - // Exception regions describe regions of code that can't be edited. - // If the span covers all the children nodes we don't need to descend further. - if (coversAllChildren) - { - isCovered = true; - break; - } - } + var ancestors = GetExceptionHandlingAncestors(node, root, isNonLeaf); + return GetExceptionRegions(ancestors, root.SyntaxTree, cancellationToken); + } - return new ActiveStatementExceptionRegions(result.ToImmutable(), isCovered); + private ActiveStatementExceptionRegions GetExceptionRegions(List exceptionHandlingAncestors, SyntaxTree tree, CancellationToken cancellationToken) + { + if (exceptionHandlingAncestors.Count == 0) + { + return new ActiveStatementExceptionRegions([], isActiveStatementCovered: false); } - private TextSpan GetDeletedNodeDiagnosticSpan( - LambdaBody deletedLambdaBody, - SyntaxNode oldEncompassingAncestor, - IReadOnlyDictionary forwardMap, - Dictionary lambdaInfos) + var isCovered = false; + using var _ = ArrayBuilder.GetInstance(out var result); + + for (var i = exceptionHandlingAncestors.Count - 1; i >= 0; i--) { - var oldLambdaBody = deletedLambdaBody; - while (true) - { - var oldLambda = oldLambdaBody.GetLambda(); - var oldParentLambdaBody = FindEnclosingLambdaBody(oldEncompassingAncestor, oldLambda); - if (oldParentLambdaBody == null) - { - return GetDeletedNodeDiagnosticSpan(forwardMap, oldLambda); - } + var span = GetExceptionHandlingRegion(exceptionHandlingAncestors[i], out var coversAllChildren); - if (lambdaInfos.TryGetValue(oldParentLambdaBody, out var lambdaInfo) && !lambdaInfo.BodyMap.Forward.IsEmpty()) - { - return GetDeletedNodeDiagnosticSpan(lambdaInfo.BodyMap.Forward, oldLambda); - } + // TODO: https://github.com/dotnet/roslyn/issues/52971 + // 1) Check that the span doesn't cross #line pragmas with different file mappings. + // 2) Check that the mapped path does not change and report rude edits if it does. + result.Add(tree.GetMappedLineSpan(span, cancellationToken)); - oldLambdaBody = oldParentLambdaBody; + // Exception regions describe regions of code that can't be edited. + // If the span covers all the children nodes we don't need to descend further. + if (coversAllChildren) + { + isCovered = true; + break; } } - private TextSpan FindClosestActiveSpan(SyntaxNode statement, int statementPart) + return new ActiveStatementExceptionRegions(result.ToImmutable(), isCovered); + } + + private TextSpan GetDeletedNodeDiagnosticSpan( + LambdaBody deletedLambdaBody, + SyntaxNode oldEncompassingAncestor, + IReadOnlyDictionary forwardMap, + Dictionary lambdaInfos) + { + var oldLambdaBody = deletedLambdaBody; + while (true) { - if (TryGetActiveSpan(statement, statementPart, minLength: statement.Span.Length, out var span)) + var oldLambda = oldLambdaBody.GetLambda(); + var oldParentLambdaBody = FindEnclosingLambdaBody(oldEncompassingAncestor, oldLambda); + if (oldParentLambdaBody == null) { - return span; + return GetDeletedNodeDiagnosticSpan(forwardMap, oldLambda); } - // The node doesn't have sequence points. - // E.g. "const" keyword is inserted into a local variable declaration with an initializer. - foreach (var (node, part) in EnumerateNearStatements(statement)) + if (lambdaInfos.TryGetValue(oldParentLambdaBody, out var lambdaInfo) && !lambdaInfo.BodyMap.Forward.IsEmpty()) { - if (part == -1) - { - return node.Span; - } - - if (TryGetActiveSpan(node, part, minLength: 0, out span)) - { - return span; - } + return GetDeletedNodeDiagnosticSpan(lambdaInfo.BodyMap.Forward, oldLambda); } - // This might occur in cases where we report rude edit, so the exact location of the active span doesn't matter. - // For example, when a method expression body is removed in C#. - return statement.Span; + oldLambdaBody = oldParentLambdaBody; } + } - internal TextSpan GetDeletedNodeActiveSpan(IReadOnlyDictionary forwardMap, SyntaxNode deletedNode) + private TextSpan FindClosestActiveSpan(SyntaxNode statement, int statementPart) + { + if (TryGetActiveSpan(statement, statementPart, minLength: statement.Span.Length, out var span)) { - foreach (var (oldNode, part) in EnumerateNearStatements(deletedNode)) - { - if (part == -1) - { - break; - } + return span; + } - if (forwardMap.TryGetValue(oldNode, out var newNode)) - { - return FindClosestActiveSpan(newNode, part); - } + // The node doesn't have sequence points. + // E.g. "const" keyword is inserted into a local variable declaration with an initializer. + foreach (var (node, part) in EnumerateNearStatements(statement)) + { + if (part == -1) + { + return node.Span; } - return GetDeletedNodeDiagnosticSpan(forwardMap, deletedNode); + if (TryGetActiveSpan(node, part, minLength: 0, out span)) + { + return span; + } } - internal TextSpan GetDeletedDeclarationActiveSpan(IReadOnlyDictionary forwardMap, SyntaxNode deletedDeclaration) + // This might occur in cases where we report rude edit, so the exact location of the active span doesn't matter. + // For example, when a method expression body is removed in C#. + return statement.Span; + } + + internal TextSpan GetDeletedNodeActiveSpan(IReadOnlyDictionary forwardMap, SyntaxNode deletedNode) + { + foreach (var (oldNode, part) in EnumerateNearStatements(deletedNode)) { - if (IsDeclarationWithInitializer(deletedDeclaration)) + if (part == -1) { - return GetDeletedNodeActiveSpan(forwardMap, deletedDeclaration); + break; } - // TODO: if the member isn't a field/property we should return empty span. - // We need to adjust the tracking span design and UpdateUneditedSpans to account for such empty spans. - - var hasAncestor = TryGetMatchingAncestor(forwardMap, deletedDeclaration, out var newAncestor); - Debug.Assert(hasAncestor && newAncestor != null); - - // the only matching ancestor is teh compilation unit: - if (newAncestor.Parent == null) + if (forwardMap.TryGetValue(oldNode, out var newNode)) { - return default; + return FindClosestActiveSpan(newNode, part); } + } + + return GetDeletedNodeDiagnosticSpan(forwardMap, deletedNode); + } - return GetDiagnosticSpan(newAncestor, EditKind.Delete); + internal TextSpan GetDeletedDeclarationActiveSpan(IReadOnlyDictionary forwardMap, SyntaxNode deletedDeclaration) + { + if (IsDeclarationWithInitializer(deletedDeclaration)) + { + return GetDeletedNodeActiveSpan(forwardMap, deletedDeclaration); } - internal TextSpan GetDeletedNodeDiagnosticSpan(IReadOnlyDictionary forwardMap, SyntaxNode deletedNode) + // TODO: if the member isn't a field/property we should return empty span. + // We need to adjust the tracking span design and UpdateUneditedSpans to account for such empty spans. + + var hasAncestor = TryGetMatchingAncestor(forwardMap, deletedDeclaration, out var newAncestor); + Debug.Assert(hasAncestor && newAncestor != null); + + // the only matching ancestor is teh compilation unit: + if (newAncestor.Parent == null) { - var hasAncestor = TryGetMatchingAncestor(forwardMap, deletedNode, out var newAncestor); - Debug.Assert(hasAncestor && newAncestor != null); - return GetDiagnosticSpan(newAncestor, EditKind.Delete); + return default; } - /// - /// Finds the inner-most ancestor of the specified node that has a matching node in the new tree. - /// - private static bool TryGetMatchingAncestor(IReadOnlyDictionary forwardMap, SyntaxNode? oldNode, [NotNullWhen(true)] out SyntaxNode? newAncestor) + return GetDiagnosticSpan(newAncestor, EditKind.Delete); + } + + internal TextSpan GetDeletedNodeDiagnosticSpan(IReadOnlyDictionary forwardMap, SyntaxNode deletedNode) + { + var hasAncestor = TryGetMatchingAncestor(forwardMap, deletedNode, out var newAncestor); + Debug.Assert(hasAncestor && newAncestor != null); + return GetDiagnosticSpan(newAncestor, EditKind.Delete); + } + + /// + /// Finds the inner-most ancestor of the specified node that has a matching node in the new tree. + /// + private static bool TryGetMatchingAncestor(IReadOnlyDictionary forwardMap, SyntaxNode? oldNode, [NotNullWhen(true)] out SyntaxNode? newAncestor) + { + while (oldNode != null) { - while (oldNode != null) + if (forwardMap.TryGetValue(oldNode, out newAncestor)) { - if (forwardMap.TryGetValue(oldNode, out newAncestor)) - { - return true; - } - - oldNode = oldNode.Parent; + return true; } - // only happens if original oldNode is a root, - // otherwise we always find a matching ancestor pair (roots). - newAncestor = null; - return false; + oldNode = oldNode.Parent; } - protected static bool HasParentEdit(IReadOnlyDictionary editMap, Edit edit) + // only happens if original oldNode is a root, + // otherwise we always find a matching ancestor pair (roots). + newAncestor = null; + return false; + } + + protected static bool HasParentEdit(IReadOnlyDictionary editMap, Edit edit) + { + SyntaxNode node; + switch (edit.Kind) + { + case EditKind.Insert: + node = edit.NewNode; + break; + + case EditKind.Delete: + node = edit.OldNode; + break; + + default: + return false; + } + + return HasEdit(editMap, node.Parent, edit.Kind); + } + + protected static bool HasEdit(IReadOnlyDictionary editMap, SyntaxNode? node, EditKind editKind) + { + return + node is object && + editMap.TryGetValue(node, out var parentEdit) && + parentEdit == editKind; + } + + #endregion + + #region Rude Edits around Active Statement + + protected void AddAroundActiveStatementRudeDiagnostic(ArrayBuilder diagnostics, SyntaxNode? oldNode, SyntaxNode? newNode, TextSpan newActiveStatementSpan) + { + if (oldNode == null) { - SyntaxNode node; - switch (edit.Kind) - { - case EditKind.Insert: - node = edit.NewNode; - break; + RoslynDebug.Assert(newNode != null); + AddRudeInsertAroundActiveStatement(diagnostics, newNode); + } + else if (newNode == null) + { + RoslynDebug.Assert(oldNode != null); + AddRudeDeleteAroundActiveStatement(diagnostics, oldNode, newActiveStatementSpan); + } + else + { + AddRudeUpdateAroundActiveStatement(diagnostics, newNode); + } + } - case EditKind.Delete: - node = edit.OldNode; - break; + protected void AddRudeUpdateAroundActiveStatement(ArrayBuilder diagnostics, SyntaxNode newNode) + { + diagnostics.Add(new RudeEditDiagnostic( + RudeEditKind.UpdateAroundActiveStatement, + GetDiagnosticSpan(newNode, EditKind.Update), + newNode, + [GetDisplayName(newNode, EditKind.Update)])); + } - default: - return false; - } + protected void AddRudeInsertAroundActiveStatement(ArrayBuilder diagnostics, SyntaxNode newNode) + { + diagnostics.Add(new RudeEditDiagnostic( + RudeEditKind.InsertAroundActiveStatement, + GetDiagnosticSpan(newNode, EditKind.Insert), + newNode, + [GetDisplayName(newNode, EditKind.Insert)])); + } - return HasEdit(editMap, node.Parent, edit.Kind); - } + protected void AddRudeDeleteAroundActiveStatement(ArrayBuilder diagnostics, SyntaxNode oldNode, TextSpan newActiveStatementSpan) + { + diagnostics.Add(new RudeEditDiagnostic( + RudeEditKind.DeleteAroundActiveStatement, + newActiveStatementSpan, + oldNode, + [GetDisplayName(oldNode, EditKind.Delete)])); + } - protected static bool HasEdit(IReadOnlyDictionary editMap, SyntaxNode? node, EditKind editKind) + protected void ReportUnmatchedStatements( + ArrayBuilder diagnostics, + IReadOnlyDictionary reverseMap, + Func nodeSelector, + SyntaxNode oldActiveStatement, + SyntaxNode oldEncompassingAncestor, + SyntaxNode newActiveStatement, + SyntaxNode newEncompassingAncestor, + Func areEquivalent, + Func? areSimilar) + where TSyntaxNode : SyntaxNode + { + var newNodes = GetAncestors(newEncompassingAncestor, newActiveStatement, nodeSelector); + if (newNodes == null) { - return - node is object && - editMap.TryGetValue(node, out var parentEdit) && - parentEdit == editKind; + return; } - #endregion - - #region Rude Edits around Active Statement + var oldNodes = GetAncestors(oldEncompassingAncestor, oldActiveStatement, nodeSelector); - protected void AddAroundActiveStatementRudeDiagnostic(ArrayBuilder diagnostics, SyntaxNode? oldNode, SyntaxNode? newNode, TextSpan newActiveStatementSpan) + int matchCount; + if (oldNodes != null) { - if (oldNode == null) - { - RoslynDebug.Assert(newNode != null); - AddRudeInsertAroundActiveStatement(diagnostics, newNode); - } - else if (newNode == null) - { - RoslynDebug.Assert(oldNode != null); - AddRudeDeleteAroundActiveStatement(diagnostics, oldNode, newActiveStatementSpan); - } - else + matchCount = MatchNodes(oldNodes, newNodes, diagnostics: null, reverseMap, comparer: areEquivalent); + + // Do another pass over the nodes to improve error messages. + if (areSimilar != null && matchCount < Math.Min(oldNodes.Count, newNodes.Count)) { - AddRudeUpdateAroundActiveStatement(diagnostics, newNode); + matchCount += MatchNodes(oldNodes, newNodes, diagnostics, reverseMap: null, comparer: areSimilar); } } - - protected void AddRudeUpdateAroundActiveStatement(ArrayBuilder diagnostics, SyntaxNode newNode) + else { - diagnostics.Add(new RudeEditDiagnostic( - RudeEditKind.UpdateAroundActiveStatement, - GetDiagnosticSpan(newNode, EditKind.Update), - newNode, - [GetDisplayName(newNode, EditKind.Update)])); + matchCount = 0; } - protected void AddRudeInsertAroundActiveStatement(ArrayBuilder diagnostics, SyntaxNode newNode) + if (matchCount < newNodes.Count) { - diagnostics.Add(new RudeEditDiagnostic( - RudeEditKind.InsertAroundActiveStatement, - GetDiagnosticSpan(newNode, EditKind.Insert), - newNode, - [GetDisplayName(newNode, EditKind.Insert)])); + ReportRudeEditsAndInserts(oldNodes, newNodes, diagnostics); } + } + + private void ReportRudeEditsAndInserts(List? oldNodes, List newNodes, ArrayBuilder diagnostics) + { + var oldNodeCount = (oldNodes != null) ? oldNodes.Count : 0; - protected void AddRudeDeleteAroundActiveStatement(ArrayBuilder diagnostics, SyntaxNode oldNode, TextSpan newActiveStatementSpan) + for (var i = 0; i < newNodes.Count; i++) { - diagnostics.Add(new RudeEditDiagnostic( - RudeEditKind.DeleteAroundActiveStatement, - newActiveStatementSpan, - oldNode, - [GetDisplayName(oldNode, EditKind.Delete)])); - } - - protected void ReportUnmatchedStatements( - ArrayBuilder diagnostics, - IReadOnlyDictionary reverseMap, - Func nodeSelector, - SyntaxNode oldActiveStatement, - SyntaxNode oldEncompassingAncestor, - SyntaxNode newActiveStatement, - SyntaxNode newEncompassingAncestor, - Func areEquivalent, - Func? areSimilar) - where TSyntaxNode : SyntaxNode - { - var newNodes = GetAncestors(newEncompassingAncestor, newActiveStatement, nodeSelector); - if (newNodes == null) + var newNode = newNodes[i]; + + if (newNode != null) { - return; + // Any difference can be expressed as insert, delete & insert, edit, or move & edit. + // Heuristic: If the nesting levels of the old and new nodes are the same we report an edit. + // Otherwise we report an insert. + if (i < oldNodeCount && oldNodes![i] != null) + { + AddRudeUpdateAroundActiveStatement(diagnostics, newNode); + } + else + { + AddRudeInsertAroundActiveStatement(diagnostics, newNode); + } } + } + } - var oldNodes = GetAncestors(oldEncompassingAncestor, oldActiveStatement, nodeSelector); + private int MatchNodes( + List oldNodes, + List newNodes, + ArrayBuilder? diagnostics, + IReadOnlyDictionary? reverseMap, + Func comparer) + where TSyntaxNode : SyntaxNode + { + var matchCount = 0; + var oldIndex = 0; + for (var newIndex = 0; newIndex < newNodes.Count; newIndex++) + { + var newNode = newNodes[newIndex]; + if (newNode == null) + { + continue; + } - int matchCount; - if (oldNodes != null) + SyntaxNode? oldNode; + while (oldIndex < oldNodes.Count) { - matchCount = MatchNodes(oldNodes, newNodes, diagnostics: null, reverseMap, comparer: areEquivalent); + oldNode = oldNodes[oldIndex]; - // Do another pass over the nodes to improve error messages. - if (areSimilar != null && matchCount < Math.Min(oldNodes.Count, newNodes.Count)) + if (oldNode != null) { - matchCount += MatchNodes(oldNodes, newNodes, diagnostics, reverseMap: null, comparer: areSimilar); + break; } + + // node has already been matched with a previous new node: + oldIndex++; } - else + + if (oldIndex == oldNodes.Count) { - matchCount = 0; + break; } - if (matchCount < newNodes.Count) + var i = -1; + if (reverseMap == null) { - ReportRudeEditsAndInserts(oldNodes, newNodes, diagnostics); + i = IndexOfEquivalent(newNode, oldNodes, oldIndex, comparer); + } + else if (reverseMap.TryGetValue(newNode, out var oldPartner) && comparer((TSyntaxNode)oldPartner, (TSyntaxNode)newNode)) + { + i = oldNodes.IndexOf(oldPartner, oldIndex); } - } - - private void ReportRudeEditsAndInserts(List? oldNodes, List newNodes, ArrayBuilder diagnostics) - { - var oldNodeCount = (oldNodes != null) ? oldNodes.Count : 0; - for (var i = 0; i < newNodes.Count; i++) + if (i >= 0) { - var newNode = newNodes[i]; + // we have an update or an exact match: + oldNodes[i] = null; + newNodes[newIndex] = null; + matchCount++; - if (newNode != null) + if (diagnostics != null) { - // Any difference can be expressed as insert, delete & insert, edit, or move & edit. - // Heuristic: If the nesting levels of the old and new nodes are the same we report an edit. - // Otherwise we report an insert. - if (i < oldNodeCount && oldNodes![i] != null) - { - AddRudeUpdateAroundActiveStatement(diagnostics, newNode); - } - else - { - AddRudeInsertAroundActiveStatement(diagnostics, newNode); - } + AddRudeUpdateAroundActiveStatement(diagnostics, newNode); } } } - private int MatchNodes( - List oldNodes, - List newNodes, - ArrayBuilder? diagnostics, - IReadOnlyDictionary? reverseMap, - Func comparer) - where TSyntaxNode : SyntaxNode + return matchCount; + } + + private static int IndexOfEquivalent(SyntaxNode newNode, List oldNodes, int startIndex, Func comparer) + where TSyntaxNode : SyntaxNode + { + for (var i = startIndex; i < oldNodes.Count; i++) { - var matchCount = 0; - var oldIndex = 0; - for (var newIndex = 0; newIndex < newNodes.Count; newIndex++) + var oldNode = oldNodes[i]; + if (oldNode != null && comparer((TSyntaxNode)oldNode, (TSyntaxNode)newNode)) { - var newNode = newNodes[newIndex]; - if (newNode == null) - { - continue; - } - - SyntaxNode? oldNode; - while (oldIndex < oldNodes.Count) - { - oldNode = oldNodes[oldIndex]; - - if (oldNode != null) - { - break; - } - - // node has already been matched with a previous new node: - oldIndex++; - } - - if (oldIndex == oldNodes.Count) - { - break; - } - - var i = -1; - if (reverseMap == null) - { - i = IndexOfEquivalent(newNode, oldNodes, oldIndex, comparer); - } - else if (reverseMap.TryGetValue(newNode, out var oldPartner) && comparer((TSyntaxNode)oldPartner, (TSyntaxNode)newNode)) - { - i = oldNodes.IndexOf(oldPartner, oldIndex); - } - - if (i >= 0) - { - // we have an update or an exact match: - oldNodes[i] = null; - newNodes[newIndex] = null; - matchCount++; - - if (diagnostics != null) - { - AddRudeUpdateAroundActiveStatement(diagnostics, newNode); - } - } + return i; } - - return matchCount; } - private static int IndexOfEquivalent(SyntaxNode newNode, List oldNodes, int startIndex, Func comparer) - where TSyntaxNode : SyntaxNode + return -1; + } + + private static List? GetAncestors(SyntaxNode? root, SyntaxNode node, Func nodeSelector) + { + List? list = null; + var current = node; + + while (current is object && current != root) { - for (var i = startIndex; i < oldNodes.Count; i++) + if (nodeSelector(current)) { - var oldNode = oldNodes[i]; - if (oldNode != null && comparer((TSyntaxNode)oldNode, (TSyntaxNode)newNode)) - { - return i; - } + list ??= []; + list.Add(current); } - return -1; + current = current.Parent; } - private static List? GetAncestors(SyntaxNode? root, SyntaxNode node, Func nodeSelector) + list?.Reverse(); + + return list; + } + + #endregion + + #region Trivia Analysis + + /// + /// Top-level edit script does not contain edits for a member if only trivia changed in its body. + /// It also does not reflect changes in line mapping directives. + /// Members that are unchanged but their location in the file changes are not considered updated. + /// This method calculates line and trivia edits for all these cases. + /// + /// The resulting line edits are grouped by mapped document path and sorted by in each group. + /// + private void AnalyzeTrivia( + Match topMatch, + IReadOnlyDictionary editMap, + [Out] ArrayBuilder<(SyntaxNode OldNode, SyntaxNode NewNode, TextSpan DiagnosticSpan)> triviaEdits, + [Out] ArrayBuilder lineEdits, + CancellationToken cancellationToken) + { + var oldTree = topMatch.OldRoot.SyntaxTree; + var newTree = topMatch.NewRoot.SyntaxTree; + + // We enumerate tokens of the body and split them into segments. Every matched member body will have at least one segment. + // Each segment has sequence points mapped to the same file and also all lines the segment covers map to the same line delta. + // The first token of a segment must be the first token that starts on the line. If the first segment token was in the middle line + // the previous token on the same line would have different line delta and we wouldn't be able to map both of them at the same time. + // All segments are included in the segments list regardless of their line delta (even when it's 0 - i.e. the lines did not change). + // This is necessary as we need to detect collisions of multiple segments with different deltas later on. + // + // note: range [oldStartLine, oldEndLine] is end-inclusive + using var _ = ArrayBuilder<(string filePath, int oldStartLine, int oldEndLine, int delta, SyntaxNode oldNode, SyntaxNode newNode)>.GetInstance(out var segments); + + foreach (var (oldNode, newNode) in topMatch.Matches) { - List? list = null; - var current = node; + cancellationToken.ThrowIfCancellationRequested(); - while (current is object && current != root) + if (editMap.ContainsKey(newNode)) { - if (nodeSelector(current)) - { - list ??= []; - list.Add(current); - } - - current = current.Parent; + // Updated or inserted members will be (re)generated and don't need line edits. + Debug.Assert(editMap[newNode] is EditKind.Update or EditKind.Insert); + continue; } - list?.Reverse(); + var newTokens = TryGetDeclarationBody(newNode, symbol: null)?.GetActiveTokens(); + if (newTokens == null) + { + continue; + } - return list; - } + // A (rude) edit could have been made that changes whether the node may contain active statements, + // so although the nodes match they might not have the same active tokens. + // E.g. field declaration changed to const field declaration. + var oldTokens = TryGetDeclarationBody(oldNode, symbol: null)?.GetActiveTokens(); + if (oldTokens == null) + { + continue; + } - #endregion + var newTokensEnum = newTokens.GetEnumerator(); + var oldTokensEnum = oldTokens.GetEnumerator(); - #region Trivia Analysis + var lastNewToken = default(SyntaxToken); + var lastOldStartLine = -1; + var lastOldFilePath = (string?)null; + var requiresUpdate = false; - /// - /// Top-level edit script does not contain edits for a member if only trivia changed in its body. - /// It also does not reflect changes in line mapping directives. - /// Members that are unchanged but their location in the file changes are not considered updated. - /// This method calculates line and trivia edits for all these cases. - /// - /// The resulting line edits are grouped by mapped document path and sorted by in each group. - /// - private void AnalyzeTrivia( - Match topMatch, - IReadOnlyDictionary editMap, - [Out] ArrayBuilder<(SyntaxNode OldNode, SyntaxNode NewNode, TextSpan DiagnosticSpan)> triviaEdits, - [Out] ArrayBuilder lineEdits, - CancellationToken cancellationToken) - { - var oldTree = topMatch.OldRoot.SyntaxTree; - var newTree = topMatch.NewRoot.SyntaxTree; - - // We enumerate tokens of the body and split them into segments. Every matched member body will have at least one segment. - // Each segment has sequence points mapped to the same file and also all lines the segment covers map to the same line delta. - // The first token of a segment must be the first token that starts on the line. If the first segment token was in the middle line - // the previous token on the same line would have different line delta and we wouldn't be able to map both of them at the same time. - // All segments are included in the segments list regardless of their line delta (even when it's 0 - i.e. the lines did not change). - // This is necessary as we need to detect collisions of multiple segments with different deltas later on. - // - // note: range [oldStartLine, oldEndLine] is end-inclusive - using var _ = ArrayBuilder<(string filePath, int oldStartLine, int oldEndLine, int delta, SyntaxNode oldNode, SyntaxNode newNode)>.GetInstance(out var segments); + var firstSegmentIndex = segments.Count; + var currentSegment = (path: (string?)null, oldStartLine: 0, delta: 0, firstOldToken: default(SyntaxToken), firstNewToken: default(SyntaxToken)); + var rudeEditSpan = default(TextSpan); - foreach (var (oldNode, newNode) in topMatch.Matches) + // Check if the breakpoint span that covers the first node of the segment can be translated from the old to the new by adding a line delta. + // If not we need to recompile the containing member since we are not able to produce line update for it. + // The first node of the segment can be the first node on its line but the breakpoint span might start on the previous line. + bool IsCurrentSegmentBreakpointSpanMappable() { - cancellationToken.ThrowIfCancellationRequested(); + var oldToken = currentSegment.firstOldToken; + var newToken = currentSegment.firstNewToken; + Contract.ThrowIfNull(oldToken.Parent); + Contract.ThrowIfNull(newToken.Parent); - if (editMap.ContainsKey(newNode)) + // Some nodes (e.g. const local declaration) may not be covered by a breakpoint span. + if (!TryGetEnclosingBreakpointSpan(oldToken, out var oldBreakpointSpan) || + !TryGetEnclosingBreakpointSpan(newToken, out var newBreakpointSpan)) { - // Updated or inserted members will be (re)generated and don't need line edits. - Debug.Assert(editMap[newNode] is EditKind.Update or EditKind.Insert); - continue; + return true; } - var newTokens = TryGetDeclarationBody(newNode, symbol: null)?.GetActiveTokens(); - if (newTokens == null) - { - continue; - } + var oldMappedBreakpointSpan = (SourceFileSpan)oldTree.GetMappedLineSpan(oldBreakpointSpan, cancellationToken); + var newMappedBreakpointSpan = (SourceFileSpan)newTree.GetMappedLineSpan(newBreakpointSpan, cancellationToken); - // A (rude) edit could have been made that changes whether the node may contain active statements, - // so although the nodes match they might not have the same active tokens. - // E.g. field declaration changed to const field declaration. - var oldTokens = TryGetDeclarationBody(oldNode, symbol: null)?.GetActiveTokens(); - if (oldTokens == null) + if (oldMappedBreakpointSpan.AddLineDelta(currentSegment.delta) == newMappedBreakpointSpan) { - continue; + return true; } - var newTokensEnum = newTokens.GetEnumerator(); - var oldTokensEnum = oldTokens.GetEnumerator(); - - var lastNewToken = default(SyntaxToken); - var lastOldStartLine = -1; - var lastOldFilePath = (string?)null; - var requiresUpdate = false; - - var firstSegmentIndex = segments.Count; - var currentSegment = (path: (string?)null, oldStartLine: 0, delta: 0, firstOldToken: default(SyntaxToken), firstNewToken: default(SyntaxToken)); - var rudeEditSpan = default(TextSpan); + rudeEditSpan = newBreakpointSpan; + return false; + } - // Check if the breakpoint span that covers the first node of the segment can be translated from the old to the new by adding a line delta. - // If not we need to recompile the containing member since we are not able to produce line update for it. - // The first node of the segment can be the first node on its line but the breakpoint span might start on the previous line. - bool IsCurrentSegmentBreakpointSpanMappable() - { - var oldToken = currentSegment.firstOldToken; - var newToken = currentSegment.firstNewToken; - Contract.ThrowIfNull(oldToken.Parent); - Contract.ThrowIfNull(newToken.Parent); - - // Some nodes (e.g. const local declaration) may not be covered by a breakpoint span. - if (!TryGetEnclosingBreakpointSpan(oldToken, out var oldBreakpointSpan) || - !TryGetEnclosingBreakpointSpan(newToken, out var newBreakpointSpan)) - { - return true; - } + void AddCurrentSegment() + { + Debug.Assert(currentSegment.path != null); + Debug.Assert(lastOldStartLine >= 0); - var oldMappedBreakpointSpan = (SourceFileSpan)oldTree.GetMappedLineSpan(oldBreakpointSpan, cancellationToken); - var newMappedBreakpointSpan = (SourceFileSpan)newTree.GetMappedLineSpan(newBreakpointSpan, cancellationToken); + // segment it ends on the line where the previous token starts (lastOldStartLine) + segments.Add((currentSegment.path, currentSegment.oldStartLine, lastOldStartLine, currentSegment.delta, oldNode, newNode)); + } - if (oldMappedBreakpointSpan.AddLineDelta(currentSegment.delta) == newMappedBreakpointSpan) - { - return true; - } + bool oldHasToken; + bool newHasToken; - rudeEditSpan = newBreakpointSpan; - return false; - } + while (true) + { + oldHasToken = oldTokensEnum.MoveNext(); + newHasToken = newTokensEnum.MoveNext(); - void AddCurrentSegment() + // If tokens differ in other parts then in trivia, skip the current matching node pair since we are only looking for trivia changes. + // This may happen when active tokens of member bodies overlap with nodes that do not represent a body. + // For example, in VB an initializer of a variable declarator node is included in the bodies of each modified identifier listed in the declarator, + // the declarator itself doesn't represent a body. + if (oldHasToken != newHasToken || oldHasToken && !AreEquivalent(oldTokensEnum.Current, newTokensEnum.Current)) { - Debug.Assert(currentSegment.path != null); - Debug.Assert(lastOldStartLine >= 0); - - // segment it ends on the line where the previous token starts (lastOldStartLine) - segments.Add((currentSegment.path, currentSegment.oldStartLine, lastOldStartLine, currentSegment.delta, oldNode, newNode)); + requiresUpdate = false; + break; } - bool oldHasToken; - bool newHasToken; - - while (true) + if (!oldHasToken) { - oldHasToken = oldTokensEnum.MoveNext(); - newHasToken = newTokensEnum.MoveNext(); - - // If tokens differ in other parts then in trivia, skip the current matching node pair since we are only looking for trivia changes. - // This may happen when active tokens of member bodies overlap with nodes that do not represent a body. - // For example, in VB an initializer of a variable declarator node is included in the bodies of each modified identifier listed in the declarator, - // the declarator itself doesn't represent a body. - if (oldHasToken != newHasToken || oldHasToken && !AreEquivalent(oldTokensEnum.Current, newTokensEnum.Current)) - { - requiresUpdate = false; - break; - } - - if (!oldHasToken) + if (!IsCurrentSegmentBreakpointSpanMappable()) { - if (!IsCurrentSegmentBreakpointSpanMappable()) - { - requiresUpdate = true; - } - else - { - // add last segment of the method body: - AddCurrentSegment(); - } - - break; + requiresUpdate = true; } - - var oldSpan = oldTokensEnum.Current.Span; - var newSpan = newTokensEnum.Current.Span; - - var oldMappedSpan = oldTree.GetMappedLineSpan(oldSpan, cancellationToken); - var newMappedSpan = newTree.GetMappedLineSpan(newSpan, cancellationToken); - - var oldStartLine = oldMappedSpan.Span.Start.Line; - var newStartLine = newMappedSpan.Span.Start.Line; - var lineDelta = newStartLine - oldStartLine; - - // If any tokens in the method change their mapped column or mapped path the method must be recompiled - // since the Debugger/SymReader does not support these updates. - if (oldMappedSpan.Span.Start.Character != newMappedSpan.Span.Start.Character) + else { - requiresUpdate = true; - break; + // add last segment of the method body: + AddCurrentSegment(); } - if (currentSegment.path != oldMappedSpan.Path || currentSegment.delta != lineDelta) - { - // end of segment: - if (currentSegment.path != null) - { - // Previous token start line is the same as this token start line, but the previous token line delta is not the same. - // We can't therefore map the old start line to a new one using line delta since that would affect both tokens the same. - if (lastOldStartLine == oldStartLine && string.Equals(lastOldFilePath, oldMappedSpan.Path)) - { - requiresUpdate = true; - break; - } + break; + } - if (!IsCurrentSegmentBreakpointSpanMappable()) - { - requiresUpdate = true; - break; - } + var oldSpan = oldTokensEnum.Current.Span; + var newSpan = newTokensEnum.Current.Span; - // add current segment: - AddCurrentSegment(); - } + var oldMappedSpan = oldTree.GetMappedLineSpan(oldSpan, cancellationToken); + var newMappedSpan = newTree.GetMappedLineSpan(newSpan, cancellationToken); - // start new segment: - currentSegment = (oldMappedSpan.Path, oldStartLine, lineDelta, oldTokensEnum.Current, newTokensEnum.Current); - } + var oldStartLine = oldMappedSpan.Span.Start.Line; + var newStartLine = newMappedSpan.Span.Start.Line; + var lineDelta = newStartLine - oldStartLine; - lastNewToken = newTokensEnum.Current; - lastOldStartLine = oldStartLine; - lastOldFilePath = oldMappedSpan.Path; + // If any tokens in the method change their mapped column or mapped path the method must be recompiled + // since the Debugger/SymReader does not support these updates. + if (oldMappedSpan.Span.Start.Character != newMappedSpan.Span.Start.Character) + { + requiresUpdate = true; + break; } - // All tokens of a member body have been processed now. - if (requiresUpdate) + if (currentSegment.path != oldMappedSpan.Path || currentSegment.delta != lineDelta) { - // report the rude edit for the span of tokens that forced recompilation: - if (rudeEditSpan.IsEmpty) + // end of segment: + if (currentSegment.path != null) { - if (newTokensEnum.Current.HasLeadingTrivia) - { - // [token1](trailing-trivia1)(leading-trivia2)[token2] - // ~~~~~~~~~~~~~~~~~ - rudeEditSpan = TextSpan.FromBounds(newTokensEnum.Current.FullSpan.Start, newTokensEnum.Current.SpanStart); - } - else if (lastNewToken.HasTrailingTrivia) + // Previous token start line is the same as this token start line, but the previous token line delta is not the same. + // We can't therefore map the old start line to a new one using line delta since that would affect both tokens the same. + if (lastOldStartLine == oldStartLine && string.Equals(lastOldFilePath, oldMappedSpan.Path)) { - // [token1](trailing-trivia1)[token2] - // ~~~~~~~~~~~~~~~~~~ - rudeEditSpan = TextSpan.FromBounds(lastNewToken.Span.End, newTokensEnum.Current.SpanStart); + requiresUpdate = true; + break; } - else + + if (!IsCurrentSegmentBreakpointSpanMappable()) { - // The current token is the first token of the body and has no leading trivia. - // [token1] - // ~~~~~~~~ - rudeEditSpan = newTokensEnum.Current.Span; + requiresUpdate = true; + break; } - } - triviaEdits.Add((oldNode, newNode, rudeEditSpan)); + // add current segment: + AddCurrentSegment(); + } - // remove all segments added for the current member body: - segments.Count = firstSegmentIndex; + // start new segment: + currentSegment = (oldMappedSpan.Path, oldStartLine, lineDelta, oldTokensEnum.Current, newTokensEnum.Current); } - } - if (segments.Count == 0) - { - return; + lastNewToken = newTokensEnum.Current; + lastOldStartLine = oldStartLine; + lastOldFilePath = oldMappedSpan.Path; } - // sort segments by file and then by start line: - segments.Sort((x, y) => - { - var result = string.CompareOrdinal(x.filePath, y.filePath); - return (result != 0) ? result : x.oldStartLine.CompareTo(y.oldStartLine); - }); - - // Calculate line updates based on segments. - // If two segments with different line deltas overlap we need to recompile all overlapping members except for the first one. - // The debugger does not apply line deltas to recompiled methods and hence we can chose to recompile either of the overlapping segments - // and apply line delta to the others. - // - // The line delta is applied to the start line of a sequence point. If start lines of two sequence points mapped to the same location - // before the delta is applied then they will point to the same location after the delta is applied. But that wouldn't be correct - // if two different mappings required applying different deltas and thus different locations. - // This also applies when two methods are on the same line in the old version and they move by different deltas. - - using var _1 = ArrayBuilder.GetInstance(out var documentLineEdits); - - var currentDocumentPath = segments[0].filePath; - var previousOldEndLine = -1; - var previousLineDelta = 0; - foreach (var segment in segments) + // All tokens of a member body have been processed now. + if (requiresUpdate) { - if (segment.filePath != currentDocumentPath) + // report the rude edit for the span of tokens that forced recompilation: + if (rudeEditSpan.IsEmpty) { - // store results for the previous document: - if (documentLineEdits.Count > 0) + if (newTokensEnum.Current.HasLeadingTrivia) { - lineEdits.Add(new SequencePointUpdates(currentDocumentPath, documentLineEdits.ToImmutableAndClear())); + // [token1](trailing-trivia1)(leading-trivia2)[token2] + // ~~~~~~~~~~~~~~~~~ + rudeEditSpan = TextSpan.FromBounds(newTokensEnum.Current.FullSpan.Start, newTokensEnum.Current.SpanStart); + } + else if (lastNewToken.HasTrailingTrivia) + { + // [token1](trailing-trivia1)[token2] + // ~~~~~~~~~~~~~~~~~~ + rudeEditSpan = TextSpan.FromBounds(lastNewToken.Span.End, newTokensEnum.Current.SpanStart); + } + else + { + // The current token is the first token of the body and has no leading trivia. + // [token1] + // ~~~~~~~~ + rudeEditSpan = newTokensEnum.Current.Span; } - - // switch to the next document: - currentDocumentPath = segment.filePath; - previousOldEndLine = -1; - previousLineDelta = 0; - } - else if (segment.oldStartLine <= previousOldEndLine && segment.delta != previousLineDelta) - { - // The segment overlaps the previous one that has a different line delta. We need to recompile the method. - // The debugger filters out line deltas that correspond to recompiled methods so we don't need to. - triviaEdits.Add((segment.oldNode, segment.newNode, segment.newNode.Span)); - continue; } - // If the segment being added does not start on the line immediately following the previous segment end line - // we need to insert another line update that resets the delta to 0 for the lines following the end line. - if (documentLineEdits.Count > 0 && segment.oldStartLine > previousOldEndLine + 1) - { - Debug.Assert(previousOldEndLine >= 0); - documentLineEdits.Add(new SourceLineUpdate(previousOldEndLine + 1, previousOldEndLine + 1)); - previousLineDelta = 0; - } + triviaEdits.Add((oldNode, newNode, rudeEditSpan)); + + // remove all segments added for the current member body: + segments.Count = firstSegmentIndex; + } + } + + if (segments.Count == 0) + { + return; + } - // Skip segment that doesn't change line numbers - the line edit would have no effect. - // It was only added to facilitate detection of overlap with other segments. - // Also skip the segment if the last line update has the same line delta as - // consecutive same line deltas has the same effect as a single one. - if (segment.delta != 0 && segment.delta != previousLineDelta) + // sort segments by file and then by start line: + segments.Sort((x, y) => + { + var result = string.CompareOrdinal(x.filePath, y.filePath); + return (result != 0) ? result : x.oldStartLine.CompareTo(y.oldStartLine); + }); + + // Calculate line updates based on segments. + // If two segments with different line deltas overlap we need to recompile all overlapping members except for the first one. + // The debugger does not apply line deltas to recompiled methods and hence we can chose to recompile either of the overlapping segments + // and apply line delta to the others. + // + // The line delta is applied to the start line of a sequence point. If start lines of two sequence points mapped to the same location + // before the delta is applied then they will point to the same location after the delta is applied. But that wouldn't be correct + // if two different mappings required applying different deltas and thus different locations. + // This also applies when two methods are on the same line in the old version and they move by different deltas. + + using var _1 = ArrayBuilder.GetInstance(out var documentLineEdits); + + var currentDocumentPath = segments[0].filePath; + var previousOldEndLine = -1; + var previousLineDelta = 0; + foreach (var segment in segments) + { + if (segment.filePath != currentDocumentPath) + { + // store results for the previous document: + if (documentLineEdits.Count > 0) { - documentLineEdits.Add(new SourceLineUpdate(segment.oldStartLine, segment.oldStartLine + segment.delta)); + lineEdits.Add(new SequencePointUpdates(currentDocumentPath, documentLineEdits.ToImmutableAndClear())); } - previousOldEndLine = segment.oldEndLine; - previousLineDelta = segment.delta; + // switch to the next document: + currentDocumentPath = segment.filePath; + previousOldEndLine = -1; + previousLineDelta = 0; + } + else if (segment.oldStartLine <= previousOldEndLine && segment.delta != previousLineDelta) + { + // The segment overlaps the previous one that has a different line delta. We need to recompile the method. + // The debugger filters out line deltas that correspond to recompiled methods so we don't need to. + triviaEdits.Add((segment.oldNode, segment.newNode, segment.newNode.Span)); + continue; } - if (currentDocumentPath != null && documentLineEdits.Count > 0) + // If the segment being added does not start on the line immediately following the previous segment end line + // we need to insert another line update that resets the delta to 0 for the lines following the end line. + if (documentLineEdits.Count > 0 && segment.oldStartLine > previousOldEndLine + 1) { - lineEdits.Add(new SequencePointUpdates(currentDocumentPath, documentLineEdits.ToImmutable())); + Debug.Assert(previousOldEndLine >= 0); + documentLineEdits.Add(new SourceLineUpdate(previousOldEndLine + 1, previousOldEndLine + 1)); + previousLineDelta = 0; } - } - #endregion + // Skip segment that doesn't change line numbers - the line edit would have no effect. + // It was only added to facilitate detection of overlap with other segments. + // Also skip the segment if the last line update has the same line delta as + // consecutive same line deltas has the same effect as a single one. + if (segment.delta != 0 && segment.delta != previousLineDelta) + { + documentLineEdits.Add(new SourceLineUpdate(segment.oldStartLine, segment.oldStartLine + segment.delta)); + } - #region Semantic Analysis + previousOldEndLine = segment.oldEndLine; + previousLineDelta = segment.delta; + } - private sealed class AssemblyEqualityComparer : IEqualityComparer + if (currentDocumentPath != null && documentLineEdits.Count > 0) { - public static readonly IEqualityComparer Instance = new AssemblyEqualityComparer(); + lineEdits.Add(new SequencePointUpdates(currentDocumentPath, documentLineEdits.ToImmutable())); + } + } - public bool Equals(IAssemblySymbol? x, IAssemblySymbol? y) - { - // Types defined in old source assembly need to be treated as equivalent to types in the new source assembly, - // provided that they only differ in their containing assemblies. - // - // The old source symbol has the same identity as the new one. - // Two distinct assembly symbols that are referenced by the compilations have to have distinct identities. - // If the compilation has two metadata references whose identities unify the compiler de-dups them and only creates - // a single PE symbol. Thus comparing assemblies by identity partitions them so that each partition - // contains assemblies that originated from the same Gen0 assembly. + #endregion - return Equals(x?.Identity, y?.Identity); - } + #region Semantic Analysis - public int GetHashCode(IAssemblySymbol? obj) - => obj?.Identity.GetHashCode() ?? 0; - } + private sealed class AssemblyEqualityComparer : IEqualityComparer + { + public static readonly IEqualityComparer Instance = new AssemblyEqualityComparer(); - // Ignore tuple element changes, nullability, dynamic and parameter refkinds. These type changes do not affect runtime type. - // They only affect custom attributes or metadata flags emitted on the members - all runtimes are expected to accept - // these updates in metadata deltas, even if they do not have any observable effect. - private static readonly SymbolEquivalenceComparer s_runtimeSymbolEqualityComparer = new( - AssemblyEqualityComparer.Instance, distinguishRefFromOut: false, tupleNamesMustMatch: false, ignoreNullableAnnotations: true, objectAndDynamicCompareEqually: true); + public bool Equals(IAssemblySymbol? x, IAssemblySymbol? y) + { + // Types defined in old source assembly need to be treated as equivalent to types in the new source assembly, + // provided that they only differ in their containing assemblies. + // + // The old source symbol has the same identity as the new one. + // Two distinct assembly symbols that are referenced by the compilations have to have distinct identities. + // If the compilation has two metadata references whose identities unify the compiler de-dups them and only creates + // a single PE symbol. Thus comparing assemblies by identity partitions them so that each partition + // contains assemblies that originated from the same Gen0 assembly. - private static readonly SymbolEquivalenceComparer s_exactSymbolEqualityComparer = new( - AssemblyEqualityComparer.Instance, distinguishRefFromOut: true, tupleNamesMustMatch: true, ignoreNullableAnnotations: false, objectAndDynamicCompareEqually: false); + return Equals(x?.Identity, y?.Identity); + } - protected static bool SymbolsEquivalent(ISymbol oldSymbol, ISymbol newSymbol) - => s_exactSymbolEqualityComparer.Equals(oldSymbol, newSymbol); + public int GetHashCode(IAssemblySymbol? obj) + => obj?.Identity.GetHashCode() ?? 0; + } - protected static bool ParameterTypesEquivalent(ImmutableArray oldParameters, ImmutableArray newParameters, bool exact) - => oldParameters.SequenceEqual(newParameters, exact, ParameterTypesEquivalent); + // Ignore tuple element changes, nullability, dynamic and parameter refkinds. These type changes do not affect runtime type. + // They only affect custom attributes or metadata flags emitted on the members - all runtimes are expected to accept + // these updates in metadata deltas, even if they do not have any observable effect. + private static readonly SymbolEquivalenceComparer s_runtimeSymbolEqualityComparer = new( + AssemblyEqualityComparer.Instance, distinguishRefFromOut: false, tupleNamesMustMatch: false, ignoreNullableAnnotations: true, objectAndDynamicCompareEqually: true); - protected static bool CustomModifiersEquivalent(CustomModifier oldModifier, CustomModifier newModifier, bool exact) - => oldModifier.IsOptional == newModifier.IsOptional && - TypesEquivalent(oldModifier.Modifier, newModifier.Modifier, exact); + private static readonly SymbolEquivalenceComparer s_exactSymbolEqualityComparer = new( + AssemblyEqualityComparer.Instance, distinguishRefFromOut: true, tupleNamesMustMatch: true, ignoreNullableAnnotations: false, objectAndDynamicCompareEqually: false); - protected static bool CustomModifiersEquivalent(ImmutableArray oldModifiers, ImmutableArray newModifiers, bool exact) - => oldModifiers.SequenceEqual(newModifiers, exact, CustomModifiersEquivalent); + protected static bool SymbolsEquivalent(ISymbol oldSymbol, ISymbol newSymbol) + => s_exactSymbolEqualityComparer.Equals(oldSymbol, newSymbol); - protected static bool ReturnTypesEquivalent(IMethodSymbol oldMethod, IMethodSymbol newMethod, bool exact) - => oldMethod.ReturnsByRef == newMethod.ReturnsByRef && - oldMethod.ReturnsByRefReadonly == newMethod.ReturnsByRefReadonly && // modreq emitted on the return type - CustomModifiersEquivalent(oldMethod.ReturnTypeCustomModifiers, newMethod.ReturnTypeCustomModifiers, exact) && - CustomModifiersEquivalent(oldMethod.RefCustomModifiers, newMethod.RefCustomModifiers, exact) && - TypesEquivalent(oldMethod.ReturnType, newMethod.ReturnType, exact); + protected static bool ParameterTypesEquivalent(ImmutableArray oldParameters, ImmutableArray newParameters, bool exact) + => oldParameters.SequenceEqual(newParameters, exact, ParameterTypesEquivalent); - protected static bool ReturnTypesEquivalent(IPropertySymbol oldProperty, IPropertySymbol newProperty, bool exact) - => oldProperty.ReturnsByRef == newProperty.ReturnsByRef && - oldProperty.ReturnsByRefReadonly == newProperty.ReturnsByRefReadonly && - CustomModifiersEquivalent(oldProperty.TypeCustomModifiers, newProperty.TypeCustomModifiers, exact) && - CustomModifiersEquivalent(oldProperty.RefCustomModifiers, newProperty.RefCustomModifiers, exact) && - TypesEquivalent(oldProperty.Type, newProperty.Type, exact); + protected static bool CustomModifiersEquivalent(CustomModifier oldModifier, CustomModifier newModifier, bool exact) + => oldModifier.IsOptional == newModifier.IsOptional && + TypesEquivalent(oldModifier.Modifier, newModifier.Modifier, exact); - protected static bool ReturnTypesEquivalent(IEventSymbol oldEvent, IEventSymbol newEvent, bool exact) - => TypesEquivalent(oldEvent.Type, newEvent.Type, exact); + protected static bool CustomModifiersEquivalent(ImmutableArray oldModifiers, ImmutableArray newModifiers, bool exact) + => oldModifiers.SequenceEqual(newModifiers, exact, CustomModifiersEquivalent); - protected static bool ReturnTypesEquivalent(IFieldSymbol oldField, IFieldSymbol newField, bool exact) - => CustomModifiersEquivalent(oldField.RefCustomModifiers, newField.RefCustomModifiers, exact) && - TypesEquivalent(oldField.Type, newField.Type, exact); + protected static bool ReturnTypesEquivalent(IMethodSymbol oldMethod, IMethodSymbol newMethod, bool exact) + => oldMethod.ReturnsByRef == newMethod.ReturnsByRef && + oldMethod.ReturnsByRefReadonly == newMethod.ReturnsByRefReadonly && // modreq emitted on the return type + CustomModifiersEquivalent(oldMethod.ReturnTypeCustomModifiers, newMethod.ReturnTypeCustomModifiers, exact) && + CustomModifiersEquivalent(oldMethod.RefCustomModifiers, newMethod.RefCustomModifiers, exact) && + TypesEquivalent(oldMethod.ReturnType, newMethod.ReturnType, exact); - // Note: SignatureTypeEquivalenceComparer compares dynamic and object the same. - protected static bool TypesEquivalent(ITypeSymbol? oldType, ITypeSymbol? newType, bool exact) - => (exact ? s_exactSymbolEqualityComparer : (IEqualityComparer)s_runtimeSymbolEqualityComparer.SignatureTypeEquivalenceComparer).Equals(oldType, newType); + protected static bool ReturnTypesEquivalent(IPropertySymbol oldProperty, IPropertySymbol newProperty, bool exact) + => oldProperty.ReturnsByRef == newProperty.ReturnsByRef && + oldProperty.ReturnsByRefReadonly == newProperty.ReturnsByRefReadonly && + CustomModifiersEquivalent(oldProperty.TypeCustomModifiers, newProperty.TypeCustomModifiers, exact) && + CustomModifiersEquivalent(oldProperty.RefCustomModifiers, newProperty.RefCustomModifiers, exact) && + TypesEquivalent(oldProperty.Type, newProperty.Type, exact); - protected static bool TypesEquivalent(ImmutableArray oldTypes, ImmutableArray newTypes, bool exact) where T : ITypeSymbol - => oldTypes.SequenceEqual(newTypes, exact, (x, y, exact) => TypesEquivalent(x, y, exact)); + protected static bool ReturnTypesEquivalent(IEventSymbol oldEvent, IEventSymbol newEvent, bool exact) + => TypesEquivalent(oldEvent.Type, newEvent.Type, exact); - protected static bool ParameterTypesEquivalent(IParameterSymbol oldParameter, IParameterSymbol newParameter, bool exact) - => (exact ? s_exactSymbolEqualityComparer : s_runtimeSymbolEqualityComparer).ParameterEquivalenceComparer.Equals(oldParameter, newParameter); + protected static bool ReturnTypesEquivalent(IFieldSymbol oldField, IFieldSymbol newField, bool exact) + => CustomModifiersEquivalent(oldField.RefCustomModifiers, newField.RefCustomModifiers, exact) && + TypesEquivalent(oldField.Type, newField.Type, exact); - protected static bool TypeParameterConstraintsEquivalent(ITypeParameterSymbol oldParameter, ITypeParameterSymbol newParameter, bool exact) - => TypesEquivalent(oldParameter.ConstraintTypes, newParameter.ConstraintTypes, exact) && - oldParameter.HasReferenceTypeConstraint == newParameter.HasReferenceTypeConstraint && - oldParameter.HasValueTypeConstraint == newParameter.HasValueTypeConstraint && - oldParameter.HasConstructorConstraint == newParameter.HasConstructorConstraint && - oldParameter.HasNotNullConstraint == newParameter.HasNotNullConstraint && - oldParameter.HasUnmanagedTypeConstraint == newParameter.HasUnmanagedTypeConstraint && - oldParameter.Variance == newParameter.Variance; + // Note: SignatureTypeEquivalenceComparer compares dynamic and object the same. + protected static bool TypesEquivalent(ITypeSymbol? oldType, ITypeSymbol? newType, bool exact) + => (exact ? s_exactSymbolEqualityComparer : (IEqualityComparer)s_runtimeSymbolEqualityComparer.SignatureTypeEquivalenceComparer).Equals(oldType, newType); - protected static bool TypeParametersEquivalent(ImmutableArray oldParameters, ImmutableArray newParameters, bool exact) - => oldParameters.SequenceEqual(newParameters, exact, TypeParameterConstraintsEquivalent); + protected static bool TypesEquivalent(ImmutableArray oldTypes, ImmutableArray newTypes, bool exact) where T : ITypeSymbol + => oldTypes.SequenceEqual(newTypes, exact, (x, y, exact) => TypesEquivalent(x, y, exact)); - protected static bool BaseTypesEquivalent(INamedTypeSymbol oldType, INamedTypeSymbol newType, bool exact) - => TypesEquivalent(oldType.BaseType, newType.BaseType, exact) && - TypesEquivalent(oldType.AllInterfaces, newType.AllInterfaces, exact); + protected static bool ParameterTypesEquivalent(IParameterSymbol oldParameter, IParameterSymbol newParameter, bool exact) + => (exact ? s_exactSymbolEqualityComparer : s_runtimeSymbolEqualityComparer).ParameterEquivalenceComparer.Equals(oldParameter, newParameter); - protected static bool MemberOrDelegateSignaturesEquivalent(ISymbol? oldMember, ISymbol? newMember, bool exact = false) - { - if (oldMember == newMember) - { - return true; - } + protected static bool TypeParameterConstraintsEquivalent(ITypeParameterSymbol oldParameter, ITypeParameterSymbol newParameter, bool exact) + => TypesEquivalent(oldParameter.ConstraintTypes, newParameter.ConstraintTypes, exact) && + oldParameter.HasReferenceTypeConstraint == newParameter.HasReferenceTypeConstraint && + oldParameter.HasValueTypeConstraint == newParameter.HasValueTypeConstraint && + oldParameter.HasConstructorConstraint == newParameter.HasConstructorConstraint && + oldParameter.HasNotNullConstraint == newParameter.HasNotNullConstraint && + oldParameter.HasUnmanagedTypeConstraint == newParameter.HasUnmanagedTypeConstraint && + oldParameter.Variance == newParameter.Variance; - if (oldMember == null || newMember == null || oldMember.Kind != newMember.Kind) - { - return false; - } + protected static bool TypeParametersEquivalent(ImmutableArray oldParameters, ImmutableArray newParameters, bool exact) + => oldParameters.SequenceEqual(newParameters, exact, TypeParameterConstraintsEquivalent); - switch (oldMember.Kind) - { - case SymbolKind.Field: - return ReturnTypesEquivalent((IFieldSymbol)oldMember, (IFieldSymbol)newMember, exact); + protected static bool BaseTypesEquivalent(INamedTypeSymbol oldType, INamedTypeSymbol newType, bool exact) + => TypesEquivalent(oldType.BaseType, newType.BaseType, exact) && + TypesEquivalent(oldType.AllInterfaces, newType.AllInterfaces, exact); - case SymbolKind.Event: - return ReturnTypesEquivalent((IEventSymbol)oldMember, (IEventSymbol)newMember, exact); + protected static bool MemberOrDelegateSignaturesEquivalent(ISymbol? oldMember, ISymbol? newMember, bool exact = false) + { + if (oldMember == newMember) + { + return true; + } - case SymbolKind.Property: - var oldProperty = (IPropertySymbol)oldMember; - var newProperty = (IPropertySymbol)newMember; - return ParameterTypesEquivalent(oldProperty.Parameters, newProperty.Parameters, exact) && - ReturnTypesEquivalent(oldProperty, newProperty, exact); + if (oldMember == null || newMember == null || oldMember.Kind != newMember.Kind) + { + return false; + } - case SymbolKind.Method: - var oldMethod = (IMethodSymbol)oldMember; - var newMethod = (IMethodSymbol)newMember; - return ParameterTypesEquivalent(oldMethod.Parameters, newMethod.Parameters, exact) && - oldMethod.TypeParameters.Length == newMethod.TypeParameters.Length && - ReturnTypesEquivalent(oldMethod, newMethod, exact); + switch (oldMember.Kind) + { + case SymbolKind.Field: + return ReturnTypesEquivalent((IFieldSymbol)oldMember, (IFieldSymbol)newMember, exact); + + case SymbolKind.Event: + return ReturnTypesEquivalent((IEventSymbol)oldMember, (IEventSymbol)newMember, exact); + + case SymbolKind.Property: + var oldProperty = (IPropertySymbol)oldMember; + var newProperty = (IPropertySymbol)newMember; + return ParameterTypesEquivalent(oldProperty.Parameters, newProperty.Parameters, exact) && + ReturnTypesEquivalent(oldProperty, newProperty, exact); + + case SymbolKind.Method: + var oldMethod = (IMethodSymbol)oldMember; + var newMethod = (IMethodSymbol)newMember; + return ParameterTypesEquivalent(oldMethod.Parameters, newMethod.Parameters, exact) && + oldMethod.TypeParameters.Length == newMethod.TypeParameters.Length && + ReturnTypesEquivalent(oldMethod, newMethod, exact); + + case SymbolKind.NamedType when oldMember is INamedTypeSymbol { DelegateInvokeMethod: { } oldInvokeMethod }: + var newInvokeMethod = ((INamedTypeSymbol)newMember).DelegateInvokeMethod; + return newInvokeMethod != null && + ParameterTypesEquivalent(oldInvokeMethod.Parameters, newInvokeMethod.Parameters, exact) && + ReturnTypesEquivalent(oldInvokeMethod, newInvokeMethod, exact); + + default: + throw ExceptionUtilities.UnexpectedValue(oldMember.Kind); + } + } - case SymbolKind.NamedType when oldMember is INamedTypeSymbol { DelegateInvokeMethod: { } oldInvokeMethod }: - var newInvokeMethod = ((INamedTypeSymbol)newMember).DelegateInvokeMethod; - return newInvokeMethod != null && - ParameterTypesEquivalent(oldInvokeMethod.Parameters, newInvokeMethod.Parameters, exact) && - ReturnTypesEquivalent(oldInvokeMethod, newInvokeMethod, exact); + /// + /// Aggregates information needed to emit updates of constructors that contain member initialization. + /// + private sealed class MemberInitializationUpdates(INamedTypeSymbol oldType) + { + public readonly INamedTypeSymbol OldType = oldType; - default: - throw ExceptionUtilities.UnexpectedValue(oldMember.Kind); - } - } + /// + /// Contains syntax maps for all changed data member initializers or constructor declarations (of constructors emitting initializers) + /// in the currently analyzed document. The key is the new declaration of the member. + /// + public readonly Dictionary ChangedDeclarations = []; /// - /// Aggregates information needed to emit updates of constructors that contain member initialization. + /// True if a member initializer has been deleted + /// ( only contains syntax nodes of new declarations, which are not available for deleted members). /// - private sealed class MemberInitializationUpdates(INamedTypeSymbol oldType) - { - public readonly INamedTypeSymbol OldType = oldType; + public bool HasDeletedMemberInitializer; + } - /// - /// Contains syntax maps for all changed data member initializers or constructor declarations (of constructors emitting initializers) - /// in the currently analyzed document. The key is the new declaration of the member. - /// - public readonly Dictionary ChangedDeclarations = []; + protected sealed class SymbolInfoCache( + PooledDictionary symbolKeyCache) + { + public SymbolKey GetKey(ISymbol symbol, CancellationToken cancellationToken) + => symbolKeyCache.GetOrAdd(symbol, static (symbol, cancellationToken) => SymbolKey.Create(symbol, cancellationToken), cancellationToken); + } - /// - /// True if a member initializer has been deleted - /// ( only contains syntax nodes of new declarations, which are not available for deleted members). - /// - public bool HasDeletedMemberInitializer; - } + private async Task> AnalyzeSemanticsAsync( + EditScript editScript, + IReadOnlyDictionary editMap, + ImmutableArray oldActiveStatements, + ImmutableArray newActiveStatementSpans, + IReadOnlyList<(SyntaxNode OldNode, SyntaxNode NewNode, TextSpan DiagnosticSpan)> triviaEdits, + Project oldProject, + Document? oldDocument, + Document newDocument, + SourceText newText, + ArrayBuilder diagnostics, + ImmutableArray.Builder newActiveStatements, + ImmutableArray>.Builder newExceptionRegions, + EditAndContinueCapabilitiesGrantor capabilities, + bool inBreakState, + CancellationToken cancellationToken) + { + Debug.Assert(inBreakState || newActiveStatementSpans.IsEmpty); - protected sealed class SymbolInfoCache( - PooledDictionary symbolKeyCache) + if (editScript.Edits.Length == 0 && triviaEdits.Count == 0) { - public SymbolKey GetKey(ISymbol symbol, CancellationToken cancellationToken) - => symbolKeyCache.GetOrAdd(symbol, static (symbol, cancellationToken) => SymbolKey.Create(symbol, cancellationToken), cancellationToken); + return []; } - private async Task> AnalyzeSemanticsAsync( - EditScript editScript, - IReadOnlyDictionary editMap, - ImmutableArray oldActiveStatements, - ImmutableArray newActiveStatementSpans, - IReadOnlyList<(SyntaxNode OldNode, SyntaxNode NewNode, TextSpan DiagnosticSpan)> triviaEdits, - Project oldProject, - Document? oldDocument, - Document newDocument, - SourceText newText, - ArrayBuilder diagnostics, - ImmutableArray.Builder newActiveStatements, - ImmutableArray>.Builder newExceptionRegions, - EditAndContinueCapabilitiesGrantor capabilities, - bool inBreakState, - CancellationToken cancellationToken) - { - Debug.Assert(inBreakState || newActiveStatementSpans.IsEmpty); - - if (editScript.Edits.Length == 0 && triviaEdits.Count == 0) - { - return []; - } + // { new type -> constructor update } + PooledDictionary? instanceConstructorEdits = null; + PooledDictionary? staticConstructorEdits = null; - // { new type -> constructor update } - PooledDictionary? instanceConstructorEdits = null; - PooledDictionary? staticConstructorEdits = null; + using var _1 = PooledHashSet.GetInstance(out var processedSymbols); + using var _2 = ArrayBuilder.GetInstance(out var semanticEdits); + using var _3 = PooledDictionary.GetInstance(out var symbolKeyCache); - using var _1 = PooledHashSet.GetInstance(out var processedSymbols); - using var _2 = ArrayBuilder.GetInstance(out var semanticEdits); - using var _3 = PooledDictionary.GetInstance(out var symbolKeyCache); + var symbolCache = new SymbolInfoCache(symbolKeyCache); - var symbolCache = new SymbolInfoCache(symbolKeyCache); + try + { + var oldModel = (oldDocument != null) ? await oldDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false) : null; + var newModel = await newDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var oldCompilation = oldModel?.Compilation ?? await oldProject.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var newCompilation = newModel.Compilation; + var oldTree = editScript.Match.OldRoot.SyntaxTree; + var newTree = editScript.Match.NewRoot.SyntaxTree; - try + foreach (var edit in editScript.Edits) { - var oldModel = (oldDocument != null) ? await oldDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false) : null; - var newModel = await newDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var oldCompilation = oldModel?.Compilation ?? await oldProject.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); - var newCompilation = newModel.Compilation; - var oldTree = editScript.Match.OldRoot.SyntaxTree; - var newTree = editScript.Match.NewRoot.SyntaxTree; + cancellationToken.ThrowIfCancellationRequested(); - foreach (var edit in editScript.Edits) - { - cancellationToken.ThrowIfCancellationRequested(); + Debug.Assert(edit.OldNode is null || edit.NewNode is null || IsNamespaceDeclaration(edit.OldNode) == IsNamespaceDeclaration(edit.NewNode)); - Debug.Assert(edit.OldNode is null || edit.NewNode is null || IsNamespaceDeclaration(edit.OldNode) == IsNamespaceDeclaration(edit.NewNode)); + // We can ignore namespace nodes in newly added documents (old model is not available) since + // all newly added types in these namespaces will have their own syntax edit. + var symbolEdits = oldModel != null && IsNamespaceDeclaration(edit.OldNode ?? edit.NewNode!) + ? OneOrMany.Create(GetNamespaceSymbolEdits(oldModel, newModel, cancellationToken)) + : GetSymbolEdits(edit.Kind, edit.OldNode, edit.NewNode, oldModel, newModel, editScript.Match, editMap, symbolCache, cancellationToken); - // We can ignore namespace nodes in newly added documents (old model is not available) since - // all newly added types in these namespaces will have their own syntax edit. - var symbolEdits = oldModel != null && IsNamespaceDeclaration(edit.OldNode ?? edit.NewNode!) - ? OneOrMany.Create(GetNamespaceSymbolEdits(oldModel, newModel, cancellationToken)) - : GetSymbolEdits(edit.Kind, edit.OldNode, edit.NewNode, oldModel, newModel, editScript.Match, editMap, symbolCache, cancellationToken); + foreach (var symbolEdit in symbolEdits) + { + var (oldSymbol, newSymbol, syntacticEditKind) = symbolEdit; - foreach (var symbolEdit in symbolEdits) + if (syntacticEditKind == EditKind.Move) { - var (oldSymbol, newSymbol, syntacticEditKind) = symbolEdit; + Debug.Assert(oldSymbol is INamedTypeSymbol); + Debug.Assert(newSymbol is INamedTypeSymbol); - if (syntacticEditKind == EditKind.Move) + if (!processedSymbols.Add(newSymbol)) { - Debug.Assert(oldSymbol is INamedTypeSymbol); - Debug.Assert(newSymbol is INamedTypeSymbol); + continue; + } - if (!processedSymbols.Add(newSymbol)) + var oldSymbolInNewCompilation = symbolCache.GetKey(oldSymbol, cancellationToken).Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + var newSymbolInOldCompilation = symbolCache.GetKey(newSymbol, cancellationToken).Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + + if (oldSymbolInNewCompilation == null || newSymbolInOldCompilation == null) + { + if (TypesEquivalent(oldSymbol.ContainingType, newSymbol.ContainingType, exact: false) && + !SymbolsEquivalent(oldSymbol.ContainingNamespace, newSymbol.ContainingNamespace)) { - continue; + // pick the first declaration in the new file that contains the namespace change: + var newTypeDeclaration = GetSymbolDeclarationSyntax(newSymbol, refs => refs.First(r => r.SyntaxTree == edit.NewNode!.SyntaxTree), cancellationToken); + Debug.Assert(newTypeDeclaration != null); + + diagnostics.Add(new RudeEditDiagnostic( + RudeEditKind.ChangingNamespace, + GetDiagnosticSpan(newTypeDeclaration, EditKind.Update), + newTypeDeclaration, + [GetDisplayName(newTypeDeclaration), oldSymbol.ContainingNamespace.ToDisplayString(), newSymbol.ContainingNamespace.ToDisplayString()])); } - - var oldSymbolInNewCompilation = symbolCache.GetKey(oldSymbol, cancellationToken).Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; - var newSymbolInOldCompilation = symbolCache.GetKey(newSymbol, cancellationToken).Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; - - if (oldSymbolInNewCompilation == null || newSymbolInOldCompilation == null) + else { - if (TypesEquivalent(oldSymbol.ContainingType, newSymbol.ContainingType, exact: false) && - !SymbolsEquivalent(oldSymbol.ContainingNamespace, newSymbol.ContainingNamespace)) - { - // pick the first declaration in the new file that contains the namespace change: - var newTypeDeclaration = GetSymbolDeclarationSyntax(newSymbol, refs => refs.First(r => r.SyntaxTree == edit.NewNode!.SyntaxTree), cancellationToken); - Debug.Assert(newTypeDeclaration != null); - - diagnostics.Add(new RudeEditDiagnostic( - RudeEditKind.ChangingNamespace, - GetDiagnosticSpan(newTypeDeclaration, EditKind.Update), - newTypeDeclaration, - [GetDisplayName(newTypeDeclaration), oldSymbol.ContainingNamespace.ToDisplayString(), newSymbol.ContainingNamespace.ToDisplayString()])); - } - else - { - CreateDiagnosticContext(diagnostics, oldSymbol, newSymbol, edit.NewNode, newModel, editScript.Match). - Report(RudeEditKind.Move, cancellationToken); - } + CreateDiagnosticContext(diagnostics, oldSymbol, newSymbol, edit.NewNode, newModel, editScript.Match). + Report(RudeEditKind.Move, cancellationToken); } - - continue; } - if (!PreprocessSymbolEdit(ref oldSymbol, ref newSymbol)) - { - continue; - } + continue; + } + + if (!PreprocessSymbolEdit(ref oldSymbol, ref newSymbol)) + { + continue; + } - var symbol = newSymbol ?? oldSymbol; - Contract.ThrowIfNull(symbol); + var symbol = newSymbol ?? oldSymbol; + Contract.ThrowIfNull(symbol); - SemanticEditKind editKind; + SemanticEditKind editKind; - var (oldDeclaration, newDeclaration) = GetSymbolDeclarationNodes(oldSymbol, newSymbol, edit.OldNode, edit.NewNode); + var (oldDeclaration, newDeclaration) = GetSymbolDeclarationNodes(oldSymbol, newSymbol, edit.OldNode, edit.NewNode); - var diagnosticContext = CreateDiagnosticContext(diagnostics, oldSymbol, newSymbol, edit.NewNode, newModel, editScript.Match); + var diagnosticContext = CreateDiagnosticContext(diagnostics, oldSymbol, newSymbol, edit.NewNode, newModel, editScript.Match); - // The syntax change implies an update of the associated symbol but the old/new symbol does not actually exist. - // Treat the edit as Insert/Delete. This may happen e.g. when all C# global statements are removed, the first one is added or they are moved to another file. - if (syntacticEditKind == EditKind.Update) + // The syntax change implies an update of the associated symbol but the old/new symbol does not actually exist. + // Treat the edit as Insert/Delete. This may happen e.g. when all C# global statements are removed, the first one is added or they are moved to another file. + if (syntacticEditKind == EditKind.Update) + { + if (oldSymbol == null || oldDeclaration == null || oldDeclaration != null && oldDeclaration.SyntaxTree != oldModel?.SyntaxTree) { - if (oldSymbol == null || oldDeclaration == null || oldDeclaration != null && oldDeclaration.SyntaxTree != oldModel?.SyntaxTree) - { - syntacticEditKind = EditKind.Insert; - } - else if (newSymbol == null || newDeclaration == null || newDeclaration != null && newDeclaration.SyntaxTree != newModel.SyntaxTree) - { - syntacticEditKind = EditKind.Delete; - } + syntacticEditKind = EditKind.Insert; } - - if (!inBreakState) + else if (newSymbol == null || newDeclaration == null || newDeclaration != null && newDeclaration.SyntaxTree != newModel.SyntaxTree) { - // Delete/insert/update edit of a member of a reloadable type (including nested types) results in Replace edit of the containing type. - // If a Delete edit is part of delete-insert operation (member moved to a different partial type declaration or to a different file) - // skip producing Replace semantic edit for this Delete edit as one will be reported by the corresponding Insert edit. - - var oldContainingType = oldSymbol?.ContainingType; - var newContainingType = newSymbol?.ContainingType; - var containingType = newContainingType ?? oldContainingType; + syntacticEditKind = EditKind.Delete; + } + } - if (containingType != null && (syntacticEditKind != EditKind.Delete || newSymbol == null)) - { - var containingTypeSymbolKey = symbolCache.GetKey(containingType, cancellationToken); - oldContainingType ??= (INamedTypeSymbol?)containingTypeSymbolKey.Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; - newContainingType ??= (INamedTypeSymbol?)containingTypeSymbolKey.Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + if (!inBreakState) + { + // Delete/insert/update edit of a member of a reloadable type (including nested types) results in Replace edit of the containing type. + // If a Delete edit is part of delete-insert operation (member moved to a different partial type declaration or to a different file) + // skip producing Replace semantic edit for this Delete edit as one will be reported by the corresponding Insert edit. - if (oldContainingType != null && newContainingType != null && IsReloadable(oldContainingType)) - { - if (processedSymbols.Add(newContainingType)) - { - if (capabilities.Grant(EditAndContinueCapabilities.NewTypeDefinition)) - { - semanticEdits.Add(SemanticEditInfo.CreateReplace(containingTypeSymbolKey, - IsPartialTypeEdit(oldContainingType, newContainingType, oldTree, newTree) ? containingTypeSymbolKey : null)); - } - else - { - CreateDiagnosticContext(diagnostics, oldContainingType, newContainingType, newDeclaration, newModel, editScript.Match). - Report(RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime, cancellationToken); - } - } + var oldContainingType = oldSymbol?.ContainingType; + var newContainingType = newSymbol?.ContainingType; + var containingType = newContainingType ?? oldContainingType; - continue; - } - } + if (containingType != null && (syntacticEditKind != EditKind.Delete || newSymbol == null)) + { + var containingTypeSymbolKey = symbolCache.GetKey(containingType, cancellationToken); + oldContainingType ??= (INamedTypeSymbol?)containingTypeSymbolKey.Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + newContainingType ??= (INamedTypeSymbol?)containingTypeSymbolKey.Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; - // Deleting a reloadable type is a rude edit, reported the same as for non-reloadable. - // Adding a reloadable type is a standard type addition (TODO: unless added to a reloadable type?). - // Making reloadable attribute non-reloadable results in a new version of the type that is - // not reloadable but does not update the old version in-place. - if (syntacticEditKind != EditKind.Delete && oldSymbol is INamedTypeSymbol oldType && newSymbol is INamedTypeSymbol newType && IsReloadable(oldType)) + if (oldContainingType != null && newContainingType != null && IsReloadable(oldContainingType)) { - if (symbol == newType || processedSymbols.Add(newType)) + if (processedSymbols.Add(newContainingType)) { - if (oldType.Name != newType.Name) - { - // https://github.com/dotnet/roslyn/issues/54886 - diagnosticContext.Report(RudeEditKind.Renamed, cancellationToken); - } - else if (oldType.Arity != newType.Arity) - { - // https://github.com/dotnet/roslyn/issues/54881 - diagnosticContext.Report(RudeEditKind.ChangingTypeParameters, cancellationToken); - } - else if (!capabilities.Grant(EditAndContinueCapabilities.NewTypeDefinition)) + if (capabilities.Grant(EditAndContinueCapabilities.NewTypeDefinition)) { - diagnosticContext.Report(RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime, cancellationToken); + semanticEdits.Add(SemanticEditInfo.CreateReplace(containingTypeSymbolKey, + IsPartialTypeEdit(oldContainingType, newContainingType, oldTree, newTree) ? containingTypeSymbolKey : null)); } else { - var typeKey = symbolCache.GetKey(newType, cancellationToken); - semanticEdits.Add(SemanticEditInfo.CreateReplace(typeKey, - IsPartialTypeEdit(oldType, newType, oldTree, newTree) ? typeKey : null)); + CreateDiagnosticContext(diagnostics, oldContainingType, newContainingType, newDeclaration, newModel, editScript.Match). + Report(RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime, cancellationToken); } } @@ -2623,4094 +2589,4127 @@ private async Task> AnalyzeSemanticsAsync( } } - var skipBodyAnalysis = false; - - switch (syntacticEditKind) + // Deleting a reloadable type is a rude edit, reported the same as for non-reloadable. + // Adding a reloadable type is a standard type addition (TODO: unless added to a reloadable type?). + // Making reloadable attribute non-reloadable results in a new version of the type that is + // not reloadable but does not update the old version in-place. + if (syntacticEditKind != EditKind.Delete && oldSymbol is INamedTypeSymbol oldType && newSymbol is INamedTypeSymbol newType && IsReloadable(oldType)) { - case EditKind.Delete: + if (symbol == newType || processedSymbols.Add(newType)) + { + if (oldType.Name != newType.Name) + { + // https://github.com/dotnet/roslyn/issues/54886 + diagnosticContext.Report(RudeEditKind.Renamed, cancellationToken); + } + else if (oldType.Arity != newType.Arity) + { + // https://github.com/dotnet/roslyn/issues/54881 + diagnosticContext.Report(RudeEditKind.ChangingTypeParameters, cancellationToken); + } + else if (!capabilities.Grant(EditAndContinueCapabilities.NewTypeDefinition)) + { + diagnosticContext.Report(RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime, cancellationToken); + } + else { - Contract.ThrowIfNull(oldModel); - Contract.ThrowIfNull(oldSymbol); - Contract.ThrowIfNull(oldDeclaration); + var typeKey = symbolCache.GetKey(newType, cancellationToken); + semanticEdits.Add(SemanticEditInfo.CreateReplace(typeKey, + IsPartialTypeEdit(oldType, newType, oldTree, newTree) ? typeKey : null)); + } + } - // Check if the declaration has been moved from one document to another. - if (newSymbol != null) - { - // Symbol has actually not been deleted but rather moved to another document, another partial type declaration - // or replaced with an implicitly generated one (e.g. parameterless constructor, auto-generated record methods, etc.) - editKind = SemanticEditKind.Update; + continue; + } + } - // Ignore the delete if there is going to be an insert corresponding to the new symbol that will create an update edit. - if (DeleteEditImpliesInsertEdit(oldSymbol, newSymbol, oldCompilation, cancellationToken)) - { - // We need to update any active statements that are in the deleted member body. - ReportDeletedMemberActiveStatementsRudeEdits(); - continue; - } + var skipBodyAnalysis = false; - if (oldSymbol.ContainingType.IsRecord && - newSymbol.ContainingType.IsRecord && - newSymbol is IPropertySymbol newProperty && - IsPrimaryConstructorParameterMatchingSymbol(newSymbol, cancellationToken)) - { - AnalyzeRecordPropertyReplacement((IPropertySymbol)oldSymbol, newProperty, isDeleteEdit: true); - skipBodyAnalysis = true; - break; - } + switch (syntacticEditKind) + { + case EditKind.Delete: + { + Contract.ThrowIfNull(oldModel); + Contract.ThrowIfNull(oldSymbol); + Contract.ThrowIfNull(oldDeclaration); - // there is no insert edit for an implicit declaration, therefore we need to issue an update: - break; - } + // Check if the declaration has been moved from one document to another. + if (newSymbol != null) + { + // Symbol has actually not been deleted but rather moved to another document, another partial type declaration + // or replaced with an implicitly generated one (e.g. parameterless constructor, auto-generated record methods, etc.) + editKind = SemanticEditKind.Update; - // If a partial method definition is deleted (and not moved to another partial type declaration, which is handled above) - // so must be the implementation. An edit will be issued for the implementation change. - if (newSymbol is IMethodSymbol { IsPartialDefinition: true }) + // Ignore the delete if there is going to be an insert corresponding to the new symbol that will create an update edit. + if (DeleteEditImpliesInsertEdit(oldSymbol, newSymbol, oldCompilation, cancellationToken)) { + // We need to update any active statements that are in the deleted member body. + ReportDeletedMemberActiveStatementsRudeEdits(); continue; } - var diagnosticSpan = GetDeletedNodeDiagnosticSpan(editScript.Match.Matches, oldDeclaration); - - // If we got here for a global statement then the actual edit is a delete of the synthesized Main method - if (IsGlobalMain(oldSymbol)) + if (oldSymbol.ContainingType.IsRecord && + newSymbol.ContainingType.IsRecord && + newSymbol is IPropertySymbol newProperty && + IsPrimaryConstructorParameterMatchingSymbol(newSymbol, cancellationToken)) { - diagnostics.Add(new RudeEditDiagnostic(RudeEditKind.Delete, diagnosticSpan, edit.OldNode, [GetDisplayName(edit.OldNode!, EditKind.Delete)])); - continue; + AnalyzeRecordPropertyReplacement((IPropertySymbol)oldSymbol, newProperty, isDeleteEdit: true); + skipBodyAnalysis = true; + break; } - ReportDeletedMemberActiveStatementsRudeEdits(); - - var rudeEditKind = RudeEditKind.Delete; + // there is no insert edit for an implicit declaration, therefore we need to issue an update: + break; + } - if (oldSymbol.ContainingType == null) - { - // deleting type is not allowed - Debug.Assert(oldSymbol is INamedTypeSymbol); + // If a partial method definition is deleted (and not moved to another partial type declaration, which is handled above) + // so must be the implementation. An edit will be issued for the implementation change. + if (newSymbol is IMethodSymbol { IsPartialDefinition: true }) + { + continue; + } - diagnostics.Add(new RudeEditDiagnostic( - rudeEditKind, - diagnosticSpan, - oldDeclaration, - [GetDisplayKindAndName(oldSymbol, GetDisplayName(oldDeclaration, EditKind.Delete), fullyQualify: diagnosticSpan.IsEmpty)])); + var diagnosticSpan = GetDeletedNodeDiagnosticSpan(editScript.Match.Matches, oldDeclaration); - continue; - } + // If we got here for a global statement then the actual edit is a delete of the synthesized Main method + if (IsGlobalMain(oldSymbol)) + { + diagnostics.Add(new RudeEditDiagnostic(RudeEditKind.Delete, diagnosticSpan, edit.OldNode, [GetDisplayName(edit.OldNode!, EditKind.Delete)])); + continue; + } - // Check if the symbol being deleted is a member of a type that's also being deleted. - // If so, skip the member deletion and only report the containing symbol deletion. - var oldContainingType = oldSymbol.ContainingType; - var containingTypeKey = symbolCache.GetKey(oldContainingType, cancellationToken); - var newContainingType = (INamedTypeSymbol?)containingTypeKey.Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; - if (newContainingType == null) - { - // If a type parameter is deleted from the parameter list of a type declaration, the symbol key won't be resolved (because the arities do not match). - if (oldSymbol is ITypeParameterSymbol) - { - diagnosticContext.Report(RudeEditKind.Delete, cancellationToken); - } + ReportDeletedMemberActiveStatementsRudeEdits(); - continue; - } + var rudeEditKind = RudeEditKind.Delete; - if (!AllowsDeletion(oldSymbol)) - { - diagnostics.Add(new RudeEditDiagnostic( - rudeEditKind, - diagnosticSpan, - oldDeclaration, - [GetDisplayKindAndName(oldSymbol, GetDisplayName(oldDeclaration, EditKind.Delete), fullyQualify: diagnosticSpan.IsEmpty)])); + if (oldSymbol.ContainingType == null) + { + // deleting type is not allowed + Debug.Assert(oldSymbol is INamedTypeSymbol); - continue; - } + diagnostics.Add(new RudeEditDiagnostic( + rudeEditKind, + diagnosticSpan, + oldDeclaration, + [GetDisplayKindAndName(oldSymbol, GetDisplayName(oldDeclaration, EditKind.Delete), fullyQualify: diagnosticSpan.IsEmpty)])); - // Note: We do not report rude edits when deleting auto-properties/events of a type with a sequential or explicit layout. - // The properties are updated to throw and the backing field remains in the type. - // The deleted field will remain unused since adding the property/event back is a rude edit. + continue; + } - if (IsDeclarationWithInitializer(oldDeclaration)) + // Check if the symbol being deleted is a member of a type that's also being deleted. + // If so, skip the member deletion and only report the containing symbol deletion. + var oldContainingType = oldSymbol.ContainingType; + var containingTypeKey = symbolCache.GetKey(oldContainingType, cancellationToken); + var newContainingType = (INamedTypeSymbol?)containingTypeKey.Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + if (newContainingType == null) + { + // If a type parameter is deleted from the parameter list of a type declaration, the symbol key won't be resolved (because the arities do not match). + if (oldSymbol is ITypeParameterSymbol) { - DeferConstructorEdit(oldContainingType, newContainingType, oldDeclaration, syntaxMaps: default, oldSymbol.IsStatic, isMemberWithDeletedInitializer: true); + diagnosticContext.Report(RudeEditKind.Delete, cancellationToken); } - // If a property or field is deleted from a record the synthesized members may change - // (PrintMembers print all properties and fields, Equals and GHC compare all data members, etc.) - if (SymbolPresenceAffectsSynthesizedRecordMembers(oldSymbol)) - { - // If the deleted member has been replaced by another member (of a different kind, otherwise newSymbol would be non-null) of the same name - // we should not update record members as they will be updated by an insertion edit of the other member. - // An insert edit must exist for the other member, otherwise we would have two members in the old type of the same name but different kind (field/property). - var newMatchingSymbol = newContainingType.GetMembers(oldSymbol.Name).FirstOrDefault(m => m is IPropertySymbol or IFieldSymbol); - if (newMatchingSymbol is null) - { - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newContainingType, cancellationToken); - } - } + continue; + } - // Note: Delete of a constructor does not need to be deferred since it does not affect other constructors. - // We do need to handle deletion of a primary record constructor though. - if (oldContainingType.IsRecord) - { - if (IsPrimaryConstructor(oldSymbol, cancellationToken)) - { - var oldPrimaryConstructor = (IMethodSymbol)oldSymbol; + if (!AllowsDeletion(oldSymbol)) + { + diagnostics.Add(new RudeEditDiagnostic( + rudeEditKind, + diagnosticSpan, + oldDeclaration, + [GetDisplayKindAndName(oldSymbol, GetDisplayName(oldDeclaration, EditKind.Delete), fullyQualify: diagnosticSpan.IsEmpty)])); - // Deconstructor delete: - AddDeconstructorEdits(semanticEdits, oldPrimaryConstructor, otherConstructor: null, containingTypeKey, oldCompilation, newCompilation, isParameterDelete: true, cancellationToken); + continue; + } - // Synthesized method updates: - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newContainingType, cancellationToken); - } - else if (oldSymbol is IParameterSymbol oldParameter && IsPrimaryConstructor(oldParameter.ContainingSymbol, cancellationToken)) - { - AddSynthesizedMemberEditsForRecordParameterChange(semanticEdits, oldParameter, newContainingType, containingTypeKey, isParameterDelete: true, cancellationToken); - } - } + // Note: We do not report rude edits when deleting auto-properties/events of a type with a sequential or explicit layout. + // The properties are updated to throw and the backing field remains in the type. + // The deleted field will remain unused since adding the property/event back is a rude edit. + + if (IsDeclarationWithInitializer(oldDeclaration)) + { + DeferConstructorEdit(oldContainingType, newContainingType, oldDeclaration, syntaxMaps: default, oldSymbol.IsStatic, isMemberWithDeletedInitializer: true); + } - // do not add delete edits for parameters: - if (oldSymbol is IParameterSymbol or ITypeParameterSymbol) + // If a property or field is deleted from a record the synthesized members may change + // (PrintMembers print all properties and fields, Equals and GHC compare all data members, etc.) + if (SymbolPresenceAffectsSynthesizedRecordMembers(oldSymbol)) + { + // If the deleted member has been replaced by another member (of a different kind, otherwise newSymbol would be non-null) of the same name + // we should not update record members as they will be updated by an insertion edit of the other member. + // An insert edit must exist for the other member, otherwise we would have two members in the old type of the same name but different kind (field/property). + var newMatchingSymbol = newContainingType.GetMembers(oldSymbol.Name).FirstOrDefault(m => m is IPropertySymbol or IFieldSymbol); + if (newMatchingSymbol is null) { - continue; + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newContainingType, cancellationToken); } - - AddDeleteEditsForMemberAndAccessors(semanticEdits, oldSymbol.PartialAsImplementation(), containingTypeKey, cancellationToken); - continue; } - case EditKind.Insert: + // Note: Delete of a constructor does not need to be deferred since it does not affect other constructors. + // We do need to handle deletion of a primary record constructor though. + if (oldContainingType.IsRecord) { - Contract.ThrowIfNull(newModel); - Contract.ThrowIfNull(newSymbol); - Contract.ThrowIfNull(newDeclaration); - - editKind = SemanticEditKind.Insert; - INamedTypeSymbol? oldContainingType; - var newContainingType = newSymbol.ContainingType; - - if (oldSymbol != null) + if (IsPrimaryConstructor(oldSymbol, cancellationToken)) { - // Symbol has actually not been inserted but rather moved between documents or partial type declarations, - // or is replacing an implicitly generated one (e.g. parameterless constructor, auto-generated record methods, etc.) - editKind = SemanticEditKind.Update; - - oldContainingType = oldSymbol.ContainingType; - - if (oldSymbol is IPropertySymbol { ContainingType.IsRecord: true, GetMethod.IsImplicitlyDeclared: true, SetMethod.IsImplicitlyDeclared: true } oldRecordProperty && - IsPrimaryConstructorParameterMatchingSymbol(oldSymbol, cancellationToken)) - { - AnalyzeRecordPropertyReplacement(oldRecordProperty, (IPropertySymbol)newSymbol, isDeleteEdit: false); - skipBodyAnalysis = true; - break; - } + var oldPrimaryConstructor = (IMethodSymbol)oldSymbol; - // Handles cases when a data member explicit declaration is moved, which may change the type layout. - // As of C# 12, replacing implicitly declared member with explicitly declared does not introduce a field, - // but future language features might. This type layout update covers that case as well. - ReportTypeLayoutUpdateRudeEdits(diagnosticContext, newSymbol, cancellationToken); + // Deconstructor delete: + AddDeconstructorEdits(semanticEdits, oldPrimaryConstructor, otherConstructor: null, containingTypeKey, oldCompilation, newCompilation, isParameterDelete: true, cancellationToken); - break; + // Synthesized method updates: + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newContainingType, cancellationToken); } - - // If a partial method definition is inserted (and not moved to another partial type declaration, which is handled above) - // so must be the implementation. An edit will be issued for the implementation change. - if (newSymbol is IMethodSymbol { IsPartialDefinition: true }) + else if (oldSymbol is IParameterSymbol oldParameter && IsPrimaryConstructor(oldParameter.ContainingSymbol, cancellationToken)) { - continue; + AddSynthesizedMemberEditsForRecordParameterChange(semanticEdits, oldParameter, newContainingType, containingTypeKey, isParameterDelete: true, cancellationToken); } + } - if (newContainingType != null && !IsGlobalMain(newSymbol)) - { - // The edit actually adds a new symbol into an existing or a new type. + // do not add delete edits for parameters: + if (oldSymbol is IParameterSymbol or ITypeParameterSymbol) + { + continue; + } - var hasAssociatedSymbolInsert = - GetAssociatedMember(newSymbol) is { } newAssociatedMember && - HasEdit(editMap, GetSymbolDeclarationSyntax(newAssociatedMember, cancellationToken), EditKind.Insert); + AddDeleteEditsForMemberAndAccessors(semanticEdits, oldSymbol.PartialAsImplementation(), containingTypeKey, cancellationToken); + continue; + } - var containingTypeKey = symbolCache.GetKey(newContainingType, cancellationToken); - oldContainingType = containingTypeKey.Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol as INamedTypeSymbol; + case EditKind.Insert: + { + Contract.ThrowIfNull(newModel); + Contract.ThrowIfNull(newSymbol); + Contract.ThrowIfNull(newDeclaration); - // Check rude edits for each member even if it is inserted into a new type. - if (!hasAssociatedSymbolInsert && IsMember(newSymbol)) - { - ReportInsertedMemberSymbolRudeEdits(diagnostics, newSymbol, newDeclaration, insertingIntoExistingContainingType: oldContainingType != null); - } + editKind = SemanticEditKind.Insert; + INamedTypeSymbol? oldContainingType; + var newContainingType = newSymbol.ContainingType; - if (oldContainingType == null) - { - // If a type parameter is inserted into the parameter list of a type declaration, the symbol key won't be resolved (because the arities do not match). - if (!hasAssociatedSymbolInsert && newSymbol is ITypeParameterSymbol) - { - diagnosticContext.Report(RudeEditKind.Insert, cancellationToken); - } - - // Insertion of a new symbol into a new type. - // We'll produce a single insert edit for the entire type. - continue; - } + if (oldSymbol != null) + { + // Symbol has actually not been inserted but rather moved between documents or partial type declarations, + // or is replacing an implicitly generated one (e.g. parameterless constructor, auto-generated record methods, etc.) + editKind = SemanticEditKind.Update; - if (!hasAssociatedSymbolInsert && !CanAddNewMemberToExistingType(newSymbol, capabilities)) - { - diagnostics.Add(new RudeEditDiagnostic( - RudeEditKind.InsertNotSupportedByRuntime, - GetDiagnosticSpan(newDeclaration, EditKind.Insert), - newDeclaration, - arguments: [GetDisplayName(newDeclaration, EditKind.Insert)])); + oldContainingType = oldSymbol.ContainingType; - continue; - } + if (oldSymbol is IPropertySymbol { ContainingType.IsRecord: true, GetMethod.IsImplicitlyDeclared: true, SetMethod.IsImplicitlyDeclared: true } oldRecordProperty && + IsPrimaryConstructorParameterMatchingSymbol(oldSymbol, cancellationToken)) + { + AnalyzeRecordPropertyReplacement(oldRecordProperty, (IPropertySymbol)newSymbol, isDeleteEdit: false); + skipBodyAnalysis = true; + break; + } - // Report rude edits for changes to data member changes of a type with an explicit layout. - // We disallow moving a data member of a partial type with explicit layout even when it actually does not change the layout. - // We could compare the exact order of the members but the scenario is unlikely to occur. - ReportTypeLayoutUpdateRudeEdits(diagnosticContext, newSymbol, cancellationToken); + // Handles cases when a data member explicit declaration is moved, which may change the type layout. + // As of C# 12, replacing implicitly declared member with explicitly declared does not introduce a field, + // but future language features might. This type layout update covers that case as well. + ReportTypeLayoutUpdateRudeEdits(diagnosticContext, newSymbol, cancellationToken); - // If a property or field is inserted into a record the synthesized members may change - // (PrintMembers print all properties and fields, Equals and GHC compare all data members, etc.) - if (SymbolPresenceAffectsSynthesizedRecordMembers(newSymbol)) - { - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newContainingType, cancellationToken); - } + break; + } - if (newSymbol is IParameterSymbol newParameter && - newContainingType.IsRecord && - IsPrimaryConstructor(newParameter.ContainingSymbol, cancellationToken)) - { - AddSynthesizedMemberEditsForRecordParameterChange(semanticEdits, newParameter, oldContainingType, containingTypeKey, isParameterDelete: false, cancellationToken); - } + // If a partial method definition is inserted (and not moved to another partial type declaration, which is handled above) + // so must be the implementation. An edit will be issued for the implementation change. + if (newSymbol is IMethodSymbol { IsPartialDefinition: true }) + { + continue; + } - // do not create semantic edit for parameter insert or symbols whose associated symbol is also being inserted: - if (hasAssociatedSymbolInsert || newSymbol is IParameterSymbol or ITypeParameterSymbol) - { - continue; - } - } - else - { - // adds a new top-level type, or a global statement where none existed before, which is - // therefore inserting the $ type - Contract.ThrowIfFalse(newSymbol is INamedTypeSymbol || IsGlobalMain(newSymbol)); + if (newContainingType != null && !IsGlobalMain(newSymbol)) + { + // The edit actually adds a new symbol into an existing or a new type. - if (!capabilities.Grant(EditAndContinueCapabilities.NewTypeDefinition)) - { - diagnostics.Add(new RudeEditDiagnostic( - RudeEditKind.InsertNotSupportedByRuntime, - GetDiagnosticSpan(newDeclaration, EditKind.Insert), - newDeclaration, - arguments: [GetDisplayName(newDeclaration, EditKind.Insert)])); - } + var hasAssociatedSymbolInsert = + GetAssociatedMember(newSymbol) is { } newAssociatedMember && + HasEdit(editMap, GetSymbolDeclarationSyntax(newAssociatedMember, cancellationToken), EditKind.Insert); - oldContainingType = null; + var containingTypeKey = symbolCache.GetKey(newContainingType, cancellationToken); + oldContainingType = containingTypeKey.Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol as INamedTypeSymbol; - if (IsMember(newSymbol)) - { - ReportInsertedMemberSymbolRudeEdits(diagnostics, newSymbol, newDeclaration, insertingIntoExistingContainingType: false); - } + // Check rude edits for each member even if it is inserted into a new type. + if (!hasAssociatedSymbolInsert && IsMember(newSymbol)) + { + ReportInsertedMemberSymbolRudeEdits(diagnostics, newSymbol, newDeclaration, insertingIntoExistingContainingType: oldContainingType != null); } - Contract.ThrowIfFalse(editKind == SemanticEditKind.Insert); - - var isConstructorWithMemberInitializers = IsConstructorWithMemberInitializers(newSymbol, cancellationToken); - if (isConstructorWithMemberInitializers || IsDeclarationWithInitializer(newDeclaration)) + if (oldContainingType == null) { - Contract.ThrowIfNull(newContainingType); - Contract.ThrowIfNull(oldContainingType); - - DeferConstructorEdit(oldContainingType, newContainingType, newDeclaration, syntaxMaps: default, newSymbol.IsStatic, isMemberWithDeletedInitializer: false); - - if (isConstructorWithMemberInitializers) + // If a type parameter is inserted into the parameter list of a type declaration, the symbol key won't be resolved (because the arities do not match). + if (!hasAssociatedSymbolInsert && newSymbol is ITypeParameterSymbol) { - // Don't add a separate semantic edit. - // Edits of data members with initializers and constructors that emit initializers will be aggregated and added later. - continue; + diagnosticContext.Report(RudeEditKind.Insert, cancellationToken); } - } - } - - break; - - case EditKind.Update: - Contract.ThrowIfNull(oldModel); - Contract.ThrowIfNull(newModel); - Contract.ThrowIfNull(oldSymbol); - Contract.ThrowIfNull(newSymbol); - - editKind = SemanticEditKind.Update; - break; - - case EditKind.Reorder: - Contract.ThrowIfNull(oldSymbol); - Contract.ThrowIfNull(newSymbol); - ReportTypeLayoutUpdateRudeEdits(diagnosticContext, oldSymbol, cancellationToken); - - if (oldSymbol is IParameterSymbol && - !IsMemberOrDelegateReplaced(oldSymbol.ContainingSymbol, newSymbol.ContainingSymbol) && - !capabilities.Grant(EditAndContinueCapabilities.UpdateParameters)) - { - diagnosticContext.Report(RudeEditKind.RenamingNotSupportedByRuntime, cancellationToken); - } - - continue; - - default: - throw ExceptionUtilities.UnexpectedValue(edit.Kind); - } + // Insertion of a new symbol into a new type. + // We'll produce a single insert edit for the entire type. + continue; + } - void AnalyzeRecordPropertyReplacement(IPropertySymbol oldProperty, IPropertySymbol newProperty, bool isDeleteEdit) - { - Debug.Assert(newDeclaration != null); - Debug.Assert(oldProperty.ContainingType.IsRecord); - Debug.Assert(newProperty.ContainingType.IsRecord); + if (!hasAssociatedSymbolInsert && !CanAddNewMemberToExistingType(newSymbol, capabilities)) + { + diagnostics.Add(new RudeEditDiagnostic( + RudeEditKind.InsertNotSupportedByRuntime, + GetDiagnosticSpan(newDeclaration, EditKind.Insert), + newDeclaration, + arguments: [GetDisplayName(newDeclaration, EditKind.Insert)])); - var (customProperty, synthesizedProperty) = isDeleteEdit ? (oldProperty, newProperty) : (newProperty, oldProperty); + continue; + } - Debug.Assert(synthesizedProperty.IsSynthesizedAutoProperty()); - Debug.Assert(synthesizedProperty.SetMethod != null); + // Report rude edits for changes to data member changes of a type with an explicit layout. + // We disallow moving a data member of a partial type with explicit layout even when it actually does not change the layout. + // We could compare the exact order of the members but the scenario is unlikely to occur. + ReportTypeLayoutUpdateRudeEdits(diagnosticContext, newSymbol, cancellationToken); - // No update is needed if both properties are synthesized. The property has been updated indirectly, - // e.g. when a primary constructor parameter is deleted from one partial declaration and inserted into another one. - if (customProperty.IsSynthesizedAutoProperty()) - { - return; - } + // If a property or field is inserted into a record the synthesized members may change + // (PrintMembers print all properties and fields, Equals and GHC compare all data members, etc.) + if (SymbolPresenceAffectsSynthesizedRecordMembers(newSymbol)) + { + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newContainingType, cancellationToken); + } - // The synthesized auto-property is `T P { get; init; } = P`. - // If the initializer is different from `P` the primary constructor needs to be updated. - // Note: we update the constructor regardless of the initializer exact shape, but we could check for it. - DeferConstructorEdit(oldProperty.ContainingType, newProperty.ContainingType, newDeclaration: null, syntaxMaps: default, oldProperty.IsStatic, isMemberWithDeletedInitializer: true); + if (newSymbol is IParameterSymbol newParameter && + newContainingType.IsRecord && + IsPrimaryConstructor(newParameter.ContainingSymbol, cancellationToken)) + { + AddSynthesizedMemberEditsForRecordParameterChange(semanticEdits, newParameter, oldContainingType, containingTypeKey, isParameterDelete: false, cancellationToken); + } - if (customProperty.SetMethod == null) - { - // Custom read-only property replaced with synthesized auto-property - if (isDeleteEdit) - { - AddInsertEditsForMemberAndAccessors(semanticEdits, synthesizedProperty.SetMethod, cancellationToken); + // do not create semantic edit for parameter insert or symbols whose associated symbol is also being inserted: + if (hasAssociatedSymbolInsert || newSymbol is IParameterSymbol or ITypeParameterSymbol) + { + continue; + } } else { - AddDeleteEditsForMemberAndAccessors(semanticEdits, synthesizedProperty.SetMethod, symbolCache.GetKey(oldProperty.ContainingType, cancellationToken), cancellationToken); - } - } - - // The synthesized property replacing the deleted one will be an auto-property. - // If the accessor had body or the property changed accessibility then synthesized record members might be affected. - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newProperty.ContainingType, cancellationToken); - - // When a custom property w/o a backing field is replaced with synthesized in a type with explicit layout, - // the synthesized one adds a backing field, which changes the layout of the type. - // Note: we only report edits that add a field, not the one that remove one. - // The removed field remains in the type (so its layout is unchanged). - if (isDeleteEdit && !customProperty.IsAutoProperty()) - { - ReportTypeLayoutUpdateRudeEdits(diagnosticContext, newProperty, cancellationToken); - } - } - - void ReportDeletedMemberActiveStatementsRudeEdits() - { - Contract.ThrowIfNull(oldDeclaration); - Contract.ThrowIfNull(oldSymbol); + // adds a new top-level type, or a global statement where none existed before, which is + // therefore inserting the $ type + Contract.ThrowIfFalse(newSymbol is INamedTypeSymbol || IsGlobalMain(newSymbol)); - var oldBody = TryGetDeclarationBody(oldDeclaration, oldSymbol); - if (oldBody == null) - { - return; - } + if (!capabilities.Grant(EditAndContinueCapabilities.NewTypeDefinition)) + { + diagnostics.Add(new RudeEditDiagnostic( + RudeEditKind.InsertNotSupportedByRuntime, + GetDiagnosticSpan(newDeclaration, EditKind.Insert), + newDeclaration, + arguments: [GetDisplayName(newDeclaration, EditKind.Insert)])); + } - var activeStatementIndices = oldBody.GetOverlappingActiveStatements(oldActiveStatements); - if (!activeStatementIndices.Any()) - { - return; - } + oldContainingType = null; - TextSpan? newActiveStatementSpan = null; - foreach (var index in activeStatementIndices) - { - if (newActiveStatements[index] == null) - { - newActiveStatementSpan ??= GetDeletedDeclarationActiveSpan(editScript.Match.Matches, oldDeclaration); - newActiveStatements[index] = GetActiveStatementWithSpan(oldActiveStatements[index], newTree, newActiveStatementSpan.Value, diagnostics, cancellationToken); - newExceptionRegions[index] = []; - } - else - { - // active statements were mapped from a deleted declaration to another one: - Debug.Assert(newSymbol != null); + if (IsMember(newSymbol)) + { + ReportInsertedMemberSymbolRudeEdits(diagnostics, newSymbol, newDeclaration, insertingIntoExistingContainingType: false); + } } - } - - if (newActiveStatementSpan.HasValue) - { - diagnosticContext.Report(RudeEditKind.DeleteActiveStatement, cancellationToken); - } - } - Contract.ThrowIfFalse(editKind is SemanticEditKind.Update or SemanticEditKind.Insert); - SyntaxMaps syntaxMaps = default; + Contract.ThrowIfFalse(editKind == SemanticEditKind.Insert); - if (editKind == SemanticEditKind.Update) - { - Contract.ThrowIfNull(oldSymbol); + var isConstructorWithMemberInitializers = IsConstructorWithMemberInitializers(newSymbol, cancellationToken); + if (isConstructorWithMemberInitializers || IsDeclarationWithInitializer(newDeclaration)) + { + Contract.ThrowIfNull(newContainingType); + Contract.ThrowIfNull(oldContainingType); - var replaceMember = IsMemberOrDelegate(oldSymbol) && IsMemberOrDelegateReplaced(oldSymbol, newSymbol); + DeferConstructorEdit(oldContainingType, newContainingType, newDeclaration, syntaxMaps: default, newSymbol.IsStatic, isMemberWithDeletedInitializer: false); - if (replaceMember && oldSymbol.Name == newSymbol.Name) - { - var signatureRudeEdit = GetSignatureChangeRudeEdit(oldSymbol, newSymbol, capabilities); - if (signatureRudeEdit != RudeEditKind.None) - { - diagnosticContext.Report(signatureRudeEdit, cancellationToken); - continue; + if (isConstructorWithMemberInitializers) + { + // Don't add a separate semantic edit. + // Edits of data members with initializers and constructors that emit initializers will be aggregated and added later. + continue; + } } } - var oldBody = (oldDeclaration != null) ? TryGetDeclarationBody(oldDeclaration, oldSymbol) : null; - if (!skipBodyAnalysis) - { - var newBody = (newDeclaration != null) ? TryGetDeclarationBody(newDeclaration, newSymbol) : null; - if (oldBody != null || newBody != null) - { - // The old symbol's declaration syntax may be located in a different document than the old version of the current document. - var oldSyntaxModel = (oldDeclaration != null) - ? await oldProject.Solution.GetRequiredDocument(oldDeclaration.SyntaxTree).GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false) - : oldModel; + break; - AnalyzeChangedMemberBody( - oldDeclaration, - newDeclaration, - oldBody, - newBody, - oldSyntaxModel, - newModel, - oldSymbol, - newSymbol, - oldCompilation, - newText, - replaceMember, - editScript.Match, - oldActiveStatements, - newActiveStatementSpans, - capabilities, - newActiveStatements, - newExceptionRegions, - diagnostics, - out syntaxMaps, - cancellationToken); - } - } + case EditKind.Update: + Contract.ThrowIfNull(oldModel); + Contract.ThrowIfNull(newModel); + Contract.ThrowIfNull(oldSymbol); + Contract.ThrowIfNull(newSymbol); - AnalyzeSymbolUpdate(diagnosticContext, capabilities, semanticEdits, out var hasAttributeChange, cancellationToken); + editKind = SemanticEditKind.Update; + break; - if (newSymbol is IParameterSymbol or ITypeParameterSymbol) + case EditKind.Reorder: + Contract.ThrowIfNull(oldSymbol); + Contract.ThrowIfNull(newSymbol); + + ReportTypeLayoutUpdateRudeEdits(diagnosticContext, oldSymbol, cancellationToken); + + if (oldSymbol is IParameterSymbol && + !IsMemberOrDelegateReplaced(oldSymbol.ContainingSymbol, newSymbol.ContainingSymbol) && + !capabilities.Grant(EditAndContinueCapabilities.UpdateParameters)) { - // All (type) parameter changes are applied by an update created for the containing symbol. - continue; + diagnosticContext.Report(RudeEditKind.RenamingNotSupportedByRuntime, cancellationToken); } - // If a constructor changes from including initializers to not including initializers - // we don't need to aggregate syntax map from all initializers for the constructor update semantic edit. - var isConstructorWithMemberInitializers = IsConstructorWithMemberInitializers(newSymbol, cancellationToken); - var isOldDeclarationWithInitializer = oldDeclaration != null && IsDeclarationWithInitializer(oldDeclaration); - var isNewDeclarationWithInitializer = newDeclaration != null && IsDeclarationWithInitializer(newDeclaration); + continue; - if (isConstructorWithMemberInitializers || isOldDeclarationWithInitializer || isNewDeclarationWithInitializer) - { - DeferConstructorEdit(oldSymbol.ContainingType, newSymbol.ContainingType, newDeclaration, syntaxMaps, newSymbol.IsStatic, - isMemberWithDeletedInitializer: isOldDeclarationWithInitializer && !isNewDeclarationWithInitializer); + default: + throw ExceptionUtilities.UnexpectedValue(edit.Kind); + } - // Syntax maps will be aggregated into ones created for the constructor edit. - // It should not be set on the edit of the member with an initializer. - syntaxMaps = default; - } + void AnalyzeRecordPropertyReplacement(IPropertySymbol oldProperty, IPropertySymbol newProperty, bool isDeleteEdit) + { + Debug.Assert(newDeclaration != null); + Debug.Assert(oldProperty.ContainingType.IsRecord); + Debug.Assert(newProperty.ContainingType.IsRecord); - if (isConstructorWithMemberInitializers) - { - // all updates to constructors with initializers will be created later - continue; - } + var (customProperty, synthesizedProperty) = isDeleteEdit ? (oldProperty, newProperty) : (newProperty, oldProperty); - if (replaceMember) - { - // skip for delegates, rude edits have already been reported - if (oldSymbol is not INamedTypeSymbol { TypeKind: TypeKind.Delegate }) - { - // symbol insertion might change type layout: - ReportTypeLayoutUpdateRudeEdits(diagnosticContext, newSymbol, cancellationToken); + Debug.Assert(synthesizedProperty.IsSynthesizedAutoProperty()); + Debug.Assert(synthesizedProperty.SetMethod != null); - var containingSymbolKey = symbolCache.GetKey(oldSymbol.ContainingType, cancellationToken); - AddMemberSignatureOrNameChangeEdits(semanticEdits, oldSymbol.PartialAsImplementation(), newSymbol.PartialAsImplementation(), containingSymbolKey, cancellationToken); - } + // No update is needed if both properties are synthesized. The property has been updated indirectly, + // e.g. when a primary constructor parameter is deleted from one partial declaration and inserted into another one. + if (customProperty.IsSynthesizedAutoProperty()) + { + return; + } - // do not emit update - continue; - } + // The synthesized auto-property is `T P { get; init; } = P`. + // If the initializer is different from `P` the primary constructor needs to be updated. + // Note: we update the constructor regardless of the initializer exact shape, but we could check for it. + DeferConstructorEdit(oldProperty.ContainingType, newProperty.ContainingType, newDeclaration: null, syntaxMaps: default, oldProperty.IsStatic, isMemberWithDeletedInitializer: true); - // Avoid creating unnecessary updates that are easy to determine. - if (!hasAttributeChange && newSymbol is - INamedTypeSymbol { IsGenericType: false } or // changes in type parameter attributes and constraints need type update - IPropertySymbol { IsIndexer: false } or // changes in parameter attributes need indexer update - IFieldSymbol or - IEventSymbol) + if (customProperty.SetMethod == null) + { + // Custom read-only property replaced with synthesized auto-property + if (isDeleteEdit) { - continue; + AddInsertEditsForMemberAndAccessors(semanticEdits, synthesizedProperty.SetMethod, cancellationToken); + } + else + { + AddDeleteEditsForMemberAndAccessors(semanticEdits, synthesizedProperty.SetMethod, symbolCache.GetKey(oldProperty.ContainingType, cancellationToken), cancellationToken); } - - // While the above analysis operates on a partial definition or implementation, - // semantic edits must only be issued for the implementation. - symbol = symbol.PartialAsImplementation(); } - var symbolKey = symbolCache.GetKey(symbol, cancellationToken); - - // Specify partial type so that all edits of the same symbol located in multiple documents can be merged later on. - // The partial type needs to be specified in the following cases: - // 1) partial method is updated (in case both implementation and definition are updated) - // 2) partial type is updated - var partialType = editKind == SemanticEditKind.Update && symbol is IMethodSymbol { PartialDefinitionPart: not null } - ? symbolCache.GetKey(symbol.ContainingType, cancellationToken) - : IsPartialTypeEdit(oldSymbol, newSymbol, oldTree, newTree) - ? symbolKey - : (SymbolKey?)null; + // The synthesized property replacing the deleted one will be an auto-property. + // If the accessor had body or the property changed accessibility then synthesized record members might be affected. + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newProperty.ContainingType, cancellationToken); - semanticEdits.Add(editKind switch + // When a custom property w/o a backing field is replaced with synthesized in a type with explicit layout, + // the synthesized one adds a backing field, which changes the layout of the type. + // Note: we only report edits that add a field, not the one that remove one. + // The removed field remains in the type (so its layout is unchanged). + if (isDeleteEdit && !customProperty.IsAutoProperty()) { - SemanticEditKind.Update => SemanticEditInfo.CreateUpdate(symbolKey, syntaxMaps, partialType), - SemanticEditKind.Insert => SemanticEditInfo.CreateInsert(symbolKey, partialType), - SemanticEditKind.Replace => SemanticEditInfo.CreateReplace(symbolKey, partialType), - _ => throw ExceptionUtilities.UnexpectedValue(editKind) - }); + ReportTypeLayoutUpdateRudeEdits(diagnosticContext, newProperty, cancellationToken); + } } - } - // Trivia edits are generated for trivia that affect active statement positions. - foreach (var (oldEditNode, newEditNode, diagnosticSpan) in triviaEdits) - { - Contract.ThrowIfNull(oldModel); - Contract.ThrowIfNull(newModel); - - var triviaSymbolEdits = GetSymbolEdits(EditKind.Update, oldEditNode, newEditNode, oldModel, newModel, editScript.Match, editMap, symbolCache, cancellationToken); - foreach (var edit in triviaSymbolEdits) + void ReportDeletedMemberActiveStatementsRudeEdits() { - var (oldSymbol, newSymbol, _) = edit; + Contract.ThrowIfNull(oldDeclaration); + Contract.ThrowIfNull(oldSymbol); - if (!PreprocessSymbolEdit(ref oldSymbol, ref newSymbol)) + var oldBody = TryGetDeclarationBody(oldDeclaration, oldSymbol); + if (oldBody == null) { - // symbol already processed - continue; + return; } - Contract.ThrowIfNull(oldSymbol); - Contract.ThrowIfNull(newSymbol); - - var (oldDeclaration, newDeclaration) = GetSymbolDeclarationNodes(oldSymbol, newSymbol, oldEditNode, newEditNode); - Contract.ThrowIfNull(oldDeclaration); - Contract.ThrowIfNull(newDeclaration); + var activeStatementIndices = oldBody.GetOverlappingActiveStatements(oldActiveStatements); + if (!activeStatementIndices.Any()) + { + return; + } - var oldContainingType = oldSymbol.ContainingType; - var newContainingType = newSymbol.ContainingType; - if (oldContainingType != null && newContainingType != null && IsReloadable(oldContainingType)) + TextSpan? newActiveStatementSpan = null; + foreach (var index in activeStatementIndices) { - if (processedSymbols.Add(newContainingType)) + if (newActiveStatements[index] == null) { - if (capabilities.Grant(EditAndContinueCapabilities.NewTypeDefinition)) - { - var oldContainingTypeKey = SymbolKey.Create(oldContainingType, cancellationToken); - semanticEdits.Add(SemanticEditInfo.CreateReplace(oldContainingTypeKey, - IsPartialTypeEdit(oldContainingType, newContainingType, oldTree, newTree) ? oldContainingTypeKey : null)); - } - else - { - CreateDiagnosticContext(diagnostics, oldContainingType, newContainingType, newDeclaration, newModel, editScript.Match) - .Report(RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime, cancellationToken); - } + newActiveStatementSpan ??= GetDeletedDeclarationActiveSpan(editScript.Match.Matches, oldDeclaration); + newActiveStatements[index] = GetActiveStatementWithSpan(oldActiveStatements[index], newTree, newActiveStatementSpan.Value, diagnostics, cancellationToken); + newExceptionRegions[index] = []; + } + else + { + // active statements were mapped from a deleted declaration to another one: + Debug.Assert(newSymbol != null); } + } - continue; + if (newActiveStatementSpan.HasValue) + { + diagnosticContext.Report(RudeEditKind.DeleteActiveStatement, cancellationToken); } + } + + Contract.ThrowIfFalse(editKind is SemanticEditKind.Update or SemanticEditKind.Insert); + SyntaxMaps syntaxMaps = default; - var diagnosticContext = CreateDiagnosticContext(diagnostics, oldSymbol, newSymbol, newDeclaration, newModel, editScript.Match, diagnosticSpan); + if (editKind == SemanticEditKind.Update) + { + Contract.ThrowIfNull(oldSymbol); - AnalyzeSymbolUpdate(diagnosticContext, capabilities, semanticEdits, out var _, cancellationToken); + var replaceMember = IsMemberOrDelegate(oldSymbol) && IsMemberOrDelegateReplaced(oldSymbol, newSymbol); - // if the member doesn't have a body triva changes have no effect: - var oldBody = TryGetDeclarationBody(oldDeclaration, oldSymbol); - if (oldBody == null) + if (replaceMember && oldSymbol.Name == newSymbol.Name) { - continue; + var signatureRudeEdit = GetSignatureChangeRudeEdit(oldSymbol, newSymbol, capabilities); + if (signatureRudeEdit != RudeEditKind.None) + { + diagnosticContext.Report(signatureRudeEdit, cancellationToken); + continue; + } + } + + var oldBody = (oldDeclaration != null) ? TryGetDeclarationBody(oldDeclaration, oldSymbol) : null; + if (!skipBodyAnalysis) + { + var newBody = (newDeclaration != null) ? TryGetDeclarationBody(newDeclaration, newSymbol) : null; + if (oldBody != null || newBody != null) + { + // The old symbol's declaration syntax may be located in a different document than the old version of the current document. + var oldSyntaxModel = (oldDeclaration != null) + ? await oldProject.Solution.GetRequiredDocument(oldDeclaration.SyntaxTree).GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false) + : oldModel; + + AnalyzeChangedMemberBody( + oldDeclaration, + newDeclaration, + oldBody, + newBody, + oldSyntaxModel, + newModel, + oldSymbol, + newSymbol, + oldCompilation, + newText, + replaceMember, + editScript.Match, + oldActiveStatements, + newActiveStatementSpans, + capabilities, + newActiveStatements, + newExceptionRegions, + diagnostics, + out syntaxMaps, + cancellationToken); + } } - var newBody = TryGetDeclarationBody(newDeclaration, newSymbol); - Contract.ThrowIfNull(newBody); + AnalyzeSymbolUpdate(diagnosticContext, capabilities, semanticEdits, out var hasAttributeChange, cancellationToken); - if (ReportUnsupportedOperations(diagnosticContext, newBody, cancellationToken)) + if (newSymbol is IParameterSymbol or ITypeParameterSymbol) { + // All (type) parameter changes are applied by an update created for the containing symbol. continue; } - // only trivia changed: - Contract.ThrowIfNull(newBody); - Debug.Assert(IsConstructorWithMemberInitializers(oldSymbol, cancellationToken) == IsConstructorWithMemberInitializers(newSymbol, cancellationToken)); - Debug.Assert(IsDeclarationWithInitializer(oldDeclaration) == IsDeclarationWithInitializer(newDeclaration)); + // If a constructor changes from including initializers to not including initializers + // we don't need to aggregate syntax map from all initializers for the constructor update semantic edit. + var isConstructorWithMemberInitializers = IsConstructorWithMemberInitializers(newSymbol, cancellationToken); + var isOldDeclarationWithInitializer = oldDeclaration != null && IsDeclarationWithInitializer(oldDeclaration); + var isNewDeclarationWithInitializer = newDeclaration != null && IsDeclarationWithInitializer(newDeclaration); - // We need to provide syntax map to the compiler if the member is active (see member update above): - var isActiveMember = - oldBody.GetOverlappingActiveStatements(oldActiveStatements).Any() || - IsStateMachineMethod(oldDeclaration) || - ContainsLambda(oldBody); + if (isConstructorWithMemberInitializers || isOldDeclarationWithInitializer || isNewDeclarationWithInitializer) + { + DeferConstructorEdit(oldSymbol.ContainingType, newSymbol.ContainingType, newDeclaration, syntaxMaps, newSymbol.IsStatic, + isMemberWithDeletedInitializer: isOldDeclarationWithInitializer && !isNewDeclarationWithInitializer); - var isConstructorWithMemberInitializers = IsConstructorWithMemberInitializers(newSymbol, cancellationToken); - var isDeclarationWithInitializer = IsDeclarationWithInitializer(newDeclaration); + // Syntax maps will be aggregated into ones created for the constructor edit. + // It should not be set on the edit of the member with an initializer. + syntaxMaps = default; + } - // TODO: only create syntax map if any field initializers are active/contain lambdas or this is a partial type - var syntaxMaps = isActiveMember || isConstructorWithMemberInitializers || isDeclarationWithInitializer - ? new SyntaxMaps(newTree, CreateSyntaxMapForEquivalentNodes(oldBody, newBody), runtimeRudeEdits: null) - : default; + if (isConstructorWithMemberInitializers) + { + // all updates to constructors with initializers will be created later + continue; + } - if (isConstructorWithMemberInitializers || isDeclarationWithInitializer) + if (replaceMember) { - Contract.ThrowIfNull(oldContainingType); - Contract.ThrowIfNull(newContainingType); + // skip for delegates, rude edits have already been reported + if (oldSymbol is not INamedTypeSymbol { TypeKind: TypeKind.Delegate }) + { + // symbol insertion might change type layout: + ReportTypeLayoutUpdateRudeEdits(diagnosticContext, newSymbol, cancellationToken); - DeferConstructorEdit(oldContainingType, newContainingType, newDeclaration, syntaxMaps, newSymbol.IsStatic, isMemberWithDeletedInitializer: false); + var containingSymbolKey = symbolCache.GetKey(oldSymbol.ContainingType, cancellationToken); + AddMemberSignatureOrNameChangeEdits(semanticEdits, oldSymbol.PartialAsImplementation(), newSymbol.PartialAsImplementation(), containingSymbolKey, cancellationToken); + } - // Don't add a separate semantic edit. - // Updates of data members with initializers and constructors that emit initializers will be aggregated and added later. + // do not emit update continue; } - // If the member changed signature or name no additional updates are needed. - // E.g. property accessor might have trivia changes while the property type/name is being changed. - if (IsMember(oldSymbol) && IsMemberOrDelegateReplaced(oldSymbol, newSymbol)) + // Avoid creating unnecessary updates that are easy to determine. + if (!hasAttributeChange && newSymbol is + INamedTypeSymbol { IsGenericType: false } or // changes in type parameter attributes and constraints need type update + IPropertySymbol { IsIndexer: false } or // changes in parameter attributes need indexer update + IFieldSymbol or + IEventSymbol) { continue; } - var symbolKey = symbolCache.GetKey(newSymbol, cancellationToken); - - semanticEdits.Add(SemanticEditInfo.CreateUpdate( - symbolKey, - syntaxMaps, - partialType: IsPartialTypeEdit(oldSymbol, newSymbol, oldTree, newTree) ? symbolKey : null)); + // While the above analysis operates on a partial definition or implementation, + // semantic edits must only be issued for the implementation. + symbol = symbol.PartialAsImplementation(); } - } - if (instanceConstructorEdits != null) - { - AddConstructorEdits( - instanceConstructorEdits, - editScript.Match, - oldModel, - oldCompilation, - newModel, - isStatic: false, - semanticEdits, - diagnostics, - cancellationToken); - } + var symbolKey = symbolCache.GetKey(symbol, cancellationToken); - if (staticConstructorEdits != null) - { - AddConstructorEdits( - staticConstructorEdits, - editScript.Match, - oldModel, - oldCompilation, - newModel, - isStatic: true, - semanticEdits, - diagnostics, - cancellationToken); + // Specify partial type so that all edits of the same symbol located in multiple documents can be merged later on. + // The partial type needs to be specified in the following cases: + // 1) partial method is updated (in case both implementation and definition are updated) + // 2) partial type is updated + var partialType = editKind == SemanticEditKind.Update && symbol is IMethodSymbol { PartialDefinitionPart: not null } + ? symbolCache.GetKey(symbol.ContainingType, cancellationToken) + : IsPartialTypeEdit(oldSymbol, newSymbol, oldTree, newTree) + ? symbolKey + : (SymbolKey?)null; + + semanticEdits.Add(editKind switch + { + SemanticEditKind.Update => SemanticEditInfo.CreateUpdate(symbolKey, syntaxMaps, partialType), + SemanticEditKind.Insert => SemanticEditInfo.CreateInsert(symbolKey, partialType), + SemanticEditKind.Replace => SemanticEditInfo.CreateReplace(symbolKey, partialType), + _ => throw ExceptionUtilities.UnexpectedValue(editKind) + }); } + } + + // Trivia edits are generated for trivia that affect active statement positions. + foreach (var (oldEditNode, newEditNode, diagnosticSpan) in triviaEdits) + { + Contract.ThrowIfNull(oldModel); + Contract.ThrowIfNull(newModel); - bool PreprocessSymbolEdit(ref ISymbol? oldSymbol, ref ISymbol? newSymbol) + var triviaSymbolEdits = GetSymbolEdits(EditKind.Update, oldEditNode, newEditNode, oldModel, newModel, editScript.Match, editMap, symbolCache, cancellationToken); + foreach (var edit in triviaSymbolEdits) { - Contract.ThrowIfFalse(oldSymbol != null || newSymbol != null); + var (oldSymbol, newSymbol, _) = edit; - oldSymbol ??= Resolve(newSymbol!, symbolCache.GetKey(newSymbol!, cancellationToken), oldCompilation, cancellationToken); - newSymbol ??= Resolve(oldSymbol!, symbolCache.GetKey(oldSymbol!, cancellationToken), newCompilation, cancellationToken); + if (!PreprocessSymbolEdit(ref oldSymbol, ref newSymbol)) + { + // symbol already processed + continue; + } + + Contract.ThrowIfNull(oldSymbol); + Contract.ThrowIfNull(newSymbol); - static ISymbol? Resolve(ISymbol symbol, SymbolKey symbolKey, Compilation compilation, CancellationToken cancellationToken) + var (oldDeclaration, newDeclaration) = GetSymbolDeclarationNodes(oldSymbol, newSymbol, oldEditNode, newEditNode); + Contract.ThrowIfNull(oldDeclaration); + Contract.ThrowIfNull(newDeclaration); + + var oldContainingType = oldSymbol.ContainingType; + var newContainingType = newSymbol.ContainingType; + if (oldContainingType != null && newContainingType != null && IsReloadable(oldContainingType)) { - // Ignore ambiguous resolution result - it may happen if there are semantic errors in the compilation. - var result = symbolKey.Resolve(compilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + if (processedSymbols.Add(newContainingType)) + { + if (capabilities.Grant(EditAndContinueCapabilities.NewTypeDefinition)) + { + var oldContainingTypeKey = SymbolKey.Create(oldContainingType, cancellationToken); + semanticEdits.Add(SemanticEditInfo.CreateReplace(oldContainingTypeKey, + IsPartialTypeEdit(oldContainingType, newContainingType, oldTree, newTree) ? oldContainingTypeKey : null)); + } + else + { + CreateDiagnosticContext(diagnostics, oldContainingType, newContainingType, newDeclaration, newModel, editScript.Match) + .Report(RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime, cancellationToken); + } + } - // If we were looking for a definition and an implementation is returned the definition does not exist. - return symbol is IMethodSymbol { PartialDefinitionPart: not null } && result is IMethodSymbol { IsPartialDefinition: true } ? null : result; + continue; } - var symbol = newSymbol ?? oldSymbol; - Contract.ThrowIfNull(symbol); + var diagnosticContext = CreateDiagnosticContext(diagnostics, oldSymbol, newSymbol, newDeclaration, newModel, editScript.Match, diagnosticSpan); - return processedSymbols.Add(symbol); - } + AnalyzeSymbolUpdate(diagnosticContext, capabilities, semanticEdits, out var _, cancellationToken); - // Called when a body of a constructor or an initializer of a member is updated or inserted. - // newDeclaration is the declaration node of an updated/inserted constructor or a member with an initializer, - // or null if the constructor or member has been deleted. - void DeferConstructorEdit( - INamedTypeSymbol oldType, - INamedTypeSymbol newType, - SyntaxNode? newDeclaration, - SyntaxMaps syntaxMaps, - bool isStatic, - bool isMemberWithDeletedInitializer) - { - Dictionary constructorEdits; - if (isStatic) + // if the member doesn't have a body triva changes have no effect: + var oldBody = TryGetDeclarationBody(oldDeclaration, oldSymbol); + if (oldBody == null) { - constructorEdits = staticConstructorEdits ??= PooledDictionary.GetInstance(); + continue; } - else + + var newBody = TryGetDeclarationBody(newDeclaration, newSymbol); + Contract.ThrowIfNull(newBody); + + if (ReportUnsupportedOperations(diagnosticContext, newBody, cancellationToken)) { - constructorEdits = instanceConstructorEdits ??= PooledDictionary.GetInstance(); + continue; } - if (!constructorEdits.TryGetValue(newType, out var constructorEdit)) + // only trivia changed: + Contract.ThrowIfNull(newBody); + Debug.Assert(IsConstructorWithMemberInitializers(oldSymbol, cancellationToken) == IsConstructorWithMemberInitializers(newSymbol, cancellationToken)); + Debug.Assert(IsDeclarationWithInitializer(oldDeclaration) == IsDeclarationWithInitializer(newDeclaration)); + + // We need to provide syntax map to the compiler if the member is active (see member update above): + var isActiveMember = + oldBody.GetOverlappingActiveStatements(oldActiveStatements).Any() || + IsStateMachineMethod(oldDeclaration) || + ContainsLambda(oldBody); + + var isConstructorWithMemberInitializers = IsConstructorWithMemberInitializers(newSymbol, cancellationToken); + var isDeclarationWithInitializer = IsDeclarationWithInitializer(newDeclaration); + + // TODO: only create syntax map if any field initializers are active/contain lambdas or this is a partial type + var syntaxMaps = isActiveMember || isConstructorWithMemberInitializers || isDeclarationWithInitializer + ? new SyntaxMaps(newTree, CreateSyntaxMapForEquivalentNodes(oldBody, newBody), runtimeRudeEdits: null) + : default; + + if (isConstructorWithMemberInitializers || isDeclarationWithInitializer) { - constructorEdits.Add(newType, constructorEdit = new MemberInitializationUpdates(oldType)); + Contract.ThrowIfNull(oldContainingType); + Contract.ThrowIfNull(newContainingType); + + DeferConstructorEdit(oldContainingType, newContainingType, newDeclaration, syntaxMaps, newSymbol.IsStatic, isMemberWithDeletedInitializer: false); + + // Don't add a separate semantic edit. + // Updates of data members with initializers and constructors that emit initializers will be aggregated and added later. + continue; } - if (newDeclaration != null && !constructorEdit.ChangedDeclarations.ContainsKey(newDeclaration)) + // If the member changed signature or name no additional updates are needed. + // E.g. property accessor might have trivia changes while the property type/name is being changed. + if (IsMember(oldSymbol) && IsMemberOrDelegateReplaced(oldSymbol, newSymbol)) { - constructorEdit.ChangedDeclarations.Add(newDeclaration, syntaxMaps); + continue; } - constructorEdit.HasDeletedMemberInitializer |= isMemberWithDeletedInitializer; + var symbolKey = symbolCache.GetKey(newSymbol, cancellationToken); + + semanticEdits.Add(SemanticEditInfo.CreateUpdate( + symbolKey, + syntaxMaps, + partialType: IsPartialTypeEdit(oldSymbol, newSymbol, oldTree, newTree) ? symbolKey : null)); } } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) + + if (instanceConstructorEdits != null) + { + AddConstructorEdits( + instanceConstructorEdits, + editScript.Match, + oldModel, + oldCompilation, + newModel, + isStatic: false, + semanticEdits, + diagnostics, + cancellationToken); + } + + if (staticConstructorEdits != null) { - throw ExceptionUtilities.Unreachable(); + AddConstructorEdits( + staticConstructorEdits, + editScript.Match, + oldModel, + oldCompilation, + newModel, + isStatic: true, + semanticEdits, + diagnostics, + cancellationToken); } - finally + + bool PreprocessSymbolEdit(ref ISymbol? oldSymbol, ref ISymbol? newSymbol) { - instanceConstructorEdits?.Free(); - staticConstructorEdits?.Free(); + Contract.ThrowIfFalse(oldSymbol != null || newSymbol != null); + + oldSymbol ??= Resolve(newSymbol!, symbolCache.GetKey(newSymbol!, cancellationToken), oldCompilation, cancellationToken); + newSymbol ??= Resolve(oldSymbol!, symbolCache.GetKey(oldSymbol!, cancellationToken), newCompilation, cancellationToken); + + static ISymbol? Resolve(ISymbol symbol, SymbolKey symbolKey, Compilation compilation, CancellationToken cancellationToken) + { + // Ignore ambiguous resolution result - it may happen if there are semantic errors in the compilation. + var result = symbolKey.Resolve(compilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + + // If we were looking for a definition and an implementation is returned the definition does not exist. + return symbol is IMethodSymbol { PartialDefinitionPart: not null } && result is IMethodSymbol { IsPartialDefinition: true } ? null : result; + } + + var symbol = newSymbol ?? oldSymbol; + Contract.ThrowIfNull(symbol); + + return processedSymbols.Add(symbol); } - return semanticEdits.Distinct(SemanticEditInfoComparer.Instance).ToImmutableArray(); + // Called when a body of a constructor or an initializer of a member is updated or inserted. + // newDeclaration is the declaration node of an updated/inserted constructor or a member with an initializer, + // or null if the constructor or member has been deleted. + void DeferConstructorEdit( + INamedTypeSymbol oldType, + INamedTypeSymbol newType, + SyntaxNode? newDeclaration, + SyntaxMaps syntaxMaps, + bool isStatic, + bool isMemberWithDeletedInitializer) + { + Dictionary constructorEdits; + if (isStatic) + { + constructorEdits = staticConstructorEdits ??= PooledDictionary.GetInstance(); + } + else + { + constructorEdits = instanceConstructorEdits ??= PooledDictionary.GetInstance(); + } + + if (!constructorEdits.TryGetValue(newType, out var constructorEdit)) + { + constructorEdits.Add(newType, constructorEdit = new MemberInitializationUpdates(oldType)); + } + + if (newDeclaration != null && !constructorEdit.ChangedDeclarations.ContainsKey(newDeclaration)) + { + constructorEdit.ChangedDeclarations.Add(newDeclaration, syntaxMaps); + } - // If the symbol has a single declaring reference use its syntax node for further analysis. - // Some syntax edits may not be directly associated with the declarations. - // For example, in VB an update to AsNew clause of a multi-variable field declaration results in update to multiple symbols associated - // with the variable declaration. But we need to analyse each symbol's modified identifier separately. - (SyntaxNode? oldDeclaration, SyntaxNode? newDeclaration) GetSymbolDeclarationNodes(ISymbol? oldSymbol, ISymbol? newSymbol, SyntaxNode? oldNode, SyntaxNode? newNode) - => (oldDeclaration: (oldSymbol != null && GetSingleSymbolDeclarationSyntax(oldSymbol, cancellationToken) is { } oldDeclaration) ? oldDeclaration : oldNode, - newDeclaration: (newSymbol != null && GetSingleSymbolDeclarationSyntax(newSymbol, cancellationToken) is { } newDeclaration) ? newDeclaration : newNode); + constructorEdit.HasDeletedMemberInitializer |= isMemberWithDeletedInitializer; + } + } + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) + { + throw ExceptionUtilities.Unreachable(); + } + finally + { + instanceConstructorEdits?.Free(); + staticConstructorEdits?.Free(); } - protected static bool IsMemberOrDelegateReplaced(ISymbol oldMember, ISymbol newMember) - => oldMember.Name != newMember.Name || - !MemberOrDelegateSignaturesEquivalent(oldMember, newMember, exact: false); + return semanticEdits.Distinct(SemanticEditInfoComparer.Instance).ToImmutableArray(); + + // If the symbol has a single declaring reference use its syntax node for further analysis. + // Some syntax edits may not be directly associated with the declarations. + // For example, in VB an update to AsNew clause of a multi-variable field declaration results in update to multiple symbols associated + // with the variable declaration. But we need to analyse each symbol's modified identifier separately. + (SyntaxNode? oldDeclaration, SyntaxNode? newDeclaration) GetSymbolDeclarationNodes(ISymbol? oldSymbol, ISymbol? newSymbol, SyntaxNode? oldNode, SyntaxNode? newNode) + => (oldDeclaration: (oldSymbol != null && GetSingleSymbolDeclarationSyntax(oldSymbol, cancellationToken) is { } oldDeclaration) ? oldDeclaration : oldNode, + newDeclaration: (newSymbol != null && GetSingleSymbolDeclarationSyntax(newSymbol, cancellationToken) is { } newDeclaration) ? newDeclaration : newNode); + } + + protected static bool IsMemberOrDelegateReplaced(ISymbol oldMember, ISymbol newMember) + => oldMember.Name != newMember.Name || + !MemberOrDelegateSignaturesEquivalent(oldMember, newMember, exact: false); - protected static bool IsMember(ISymbol symbol) - => symbol.Kind is SymbolKind.Method or SymbolKind.Property or SymbolKind.Field or SymbolKind.Event; + protected static bool IsMember(ISymbol symbol) + => symbol.Kind is SymbolKind.Method or SymbolKind.Property or SymbolKind.Field or SymbolKind.Event; - protected static bool IsMemberOrDelegate(ISymbol symbol) - => IsMember(symbol) || symbol is INamedTypeSymbol { TypeKind: TypeKind.Delegate }; + protected static bool IsMemberOrDelegate(ISymbol symbol) + => IsMember(symbol) || symbol is INamedTypeSymbol { TypeKind: TypeKind.Delegate }; - protected static ISymbol? GetSemanticallyMatchingNewSymbol(ISymbol? oldSymbol, ISymbol? newSymbol, SemanticModel newModel, SymbolInfoCache symbolCache, CancellationToken cancellationToken) - => oldSymbol != null && IsMember(oldSymbol) && - newSymbol != null && IsMember(newSymbol) && - symbolCache.GetKey(oldSymbol, cancellationToken).Resolve(newModel.Compilation, ignoreAssemblyKey: true, cancellationToken).Symbol is { } matchingNewSymbol && - !matchingNewSymbol.IsSynthesized() && - matchingNewSymbol != newSymbol - ? matchingNewSymbol - : null; + protected static ISymbol? GetSemanticallyMatchingNewSymbol(ISymbol? oldSymbol, ISymbol? newSymbol, SemanticModel newModel, SymbolInfoCache symbolCache, CancellationToken cancellationToken) + => oldSymbol != null && IsMember(oldSymbol) && + newSymbol != null && IsMember(newSymbol) && + symbolCache.GetKey(oldSymbol, cancellationToken).Resolve(newModel.Compilation, ignoreAssemblyKey: true, cancellationToken).Symbol is { } matchingNewSymbol && + !matchingNewSymbol.IsSynthesized() && + matchingNewSymbol != newSymbol + ? matchingNewSymbol + : null; - protected static void AddMemberUpdate(ref TemporaryArray<(ISymbol?, ISymbol?, EditKind)> result, ISymbol? oldSymbol, ISymbol? newSymbol, ISymbol? newSemanticallyMatchingSymbol) + protected static void AddMemberUpdate(ref TemporaryArray<(ISymbol?, ISymbol?, EditKind)> result, ISymbol? oldSymbol, ISymbol? newSymbol, ISymbol? newSemanticallyMatchingSymbol) + { + if (newSemanticallyMatchingSymbol != null) { - if (newSemanticallyMatchingSymbol != null) - { - Debug.Assert(oldSymbol != null); - Debug.Assert(newSymbol != null); + Debug.Assert(oldSymbol != null); + Debug.Assert(newSymbol != null); + + result.Add((oldSymbol, null, EditKind.Delete)); + result.Add((null, newSymbol, EditKind.Insert)); + } + else if (oldSymbol != null || newSymbol != null) + { + result.Add((oldSymbol, newSymbol, EditKind.Update)); + } + } - result.Add((oldSymbol, null, EditKind.Delete)); - result.Add((null, newSymbol, EditKind.Insert)); + /// + /// Adds edits of synthesized members that may be affected by a change. + /// + private static void AddSynthesizedMemberEditsForRecordParameterChange( + ArrayBuilder semanticEdits, + IParameterSymbol parameterSymbol, + INamedTypeSymbol otherContainingType, + SymbolKey containingTypeKey, + bool isParameterDelete, + CancellationToken cancellationToken) + { + var member = parameterSymbol.ContainingSymbol; + Debug.Assert(member is IPropertySymbol or IMethodSymbol); + + // Parameter deleted from or inserted into a primary constructor of a record type. + // + // Note that although the compiler emits auto-properties and deconstructor automatically + // given just an insert or udpate edit of the primary constructor, it will not emit the necessary deletes. + // We could only add delete edits and avoid adding updates and inserts. It would make the code somewhat simpler + // but also asymetric (delete vs insert). Adding inserts and updates to the edits explicitly also avoids + // dependency on the compiler implementation details. + var primaryConstructor = (IMethodSymbol)member; + + // Delete/insert/update synthesized properties and their accessors. + + // If deleting a parameter from or inserting a parameter to primary constructor of a record + // that does not have a corresponding synthesized property (has a custom property of field) + // has no effect on the property. + + var synthesizedProperty = GetPropertySynthesizedForRecordPrimaryConstructorParameter(parameterSymbol); + if (synthesizedProperty != null) + { + var otherMembersOfParameterName = otherContainingType.GetMembers(parameterSymbol.Name); + if (otherMembersOfParameterName.Any(static m => m is IPropertySymbol)) + { + // Replace a synthesized auto-property with a custom implementation: + AddUpdateEditsForMemberAndAccessors(semanticEdits, synthesizedProperty, cancellationToken); + } + else if (isParameterDelete) + { + // Delete synthesized property: + AddDeleteEditsForMemberAndAccessors(semanticEdits, synthesizedProperty, deletedSymbolContainer: containingTypeKey, cancellationToken); } - else if (oldSymbol != null || newSymbol != null) + else { - result.Add((oldSymbol, newSymbol, EditKind.Update)); + // Insert synthesized property: + AddInsertEditsForMemberAndAccessors(semanticEdits, synthesizedProperty, cancellationToken); } } + } - /// - /// Adds edits of synthesized members that may be affected by a change. - /// - private static void AddSynthesizedMemberEditsForRecordParameterChange( - ArrayBuilder semanticEdits, - IParameterSymbol parameterSymbol, - INamedTypeSymbol otherContainingType, - SymbolKey containingTypeKey, - bool isParameterDelete, - CancellationToken cancellationToken) - { - var member = parameterSymbol.ContainingSymbol; - Debug.Assert(member is IPropertySymbol or IMethodSymbol); - - // Parameter deleted from or inserted into a primary constructor of a record type. - // - // Note that although the compiler emits auto-properties and deconstructor automatically - // given just an insert or udpate edit of the primary constructor, it will not emit the necessary deletes. - // We could only add delete edits and avoid adding updates and inserts. It would make the code somewhat simpler - // but also asymetric (delete vs insert). Adding inserts and updates to the edits explicitly also avoids - // dependency on the compiler implementation details. - var primaryConstructor = (IMethodSymbol)member; - - // Delete/insert/update synthesized properties and their accessors. - - // If deleting a parameter from or inserting a parameter to primary constructor of a record - // that does not have a corresponding synthesized property (has a custom property of field) - // has no effect on the property. + /// + /// Adds edits deleting/inserting deconstructor no longer matching of a record + /// and inserting/deleting deconstructor the one that maches its signature. + /// + private void AddDeconstructorEdits( + ArrayBuilder semanticEdits, + IMethodSymbol? constructor, + IMethodSymbol? otherConstructor, + SymbolKey containingTypeKey, + Compilation compilation, + Compilation otherCompilation, + bool isParameterDelete, + CancellationToken cancellationToken) + { + AddEdits(constructor, otherCompilation, isParameterDelete); + AddEdits(otherConstructor, compilation, !isParameterDelete); - var synthesizedProperty = GetPropertySynthesizedForRecordPrimaryConstructorParameter(parameterSymbol); - if (synthesizedProperty != null) + void AddEdits(IMethodSymbol? constructor, Compilation otherCompilation, bool isDelete) + { + if (constructor != null && + IsPrimaryConstructor(constructor, cancellationToken) && + constructor.GetMatchingDeconstructor() is { IsImplicitlyDeclared: true } deconstructor) { - var otherMembersOfParameterName = otherContainingType.GetMembers(parameterSymbol.Name); - if (otherMembersOfParameterName.Any(static m => m is IPropertySymbol)) + if (SymbolKey.Create(deconstructor, cancellationToken).Resolve(otherCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol != null) { - // Replace a synthesized auto-property with a custom implementation: - AddUpdateEditsForMemberAndAccessors(semanticEdits, synthesizedProperty, cancellationToken); + // Update for transition from synthesized to declared deconstructor + AddUpdateEditsForMemberAndAccessors(semanticEdits, deconstructor, cancellationToken); } - else if (isParameterDelete) + else if (isDelete) { - // Delete synthesized property: - AddDeleteEditsForMemberAndAccessors(semanticEdits, synthesizedProperty, deletedSymbolContainer: containingTypeKey, cancellationToken); + // Delete synthesized deconstructor: + AddDeleteEditsForMemberAndAccessors(semanticEdits, deconstructor, deletedSymbolContainer: containingTypeKey, cancellationToken); } else { - // Insert synthesized property: - AddInsertEditsForMemberAndAccessors(semanticEdits, synthesizedProperty, cancellationToken); + // Insert synthesized deconstructor: + AddInsertEditsForMemberAndAccessors(semanticEdits, deconstructor, cancellationToken); } } } + } - /// - /// Adds edits deleting/inserting deconstructor no longer matching of a record - /// and inserting/deleting deconstructor the one that maches its signature. - /// - private void AddDeconstructorEdits( - ArrayBuilder semanticEdits, - IMethodSymbol? constructor, - IMethodSymbol? otherConstructor, - SymbolKey containingTypeKey, - Compilation compilation, - Compilation otherCompilation, - bool isParameterDelete, - CancellationToken cancellationToken) - { - AddEdits(constructor, otherCompilation, isParameterDelete); - AddEdits(otherConstructor, compilation, !isParameterDelete); - - void AddEdits(IMethodSymbol? constructor, Compilation otherCompilation, bool isDelete) - { - if (constructor != null && - IsPrimaryConstructor(constructor, cancellationToken) && - constructor.GetMatchingDeconstructor() is { IsImplicitlyDeclared: true } deconstructor) - { - if (SymbolKey.Create(deconstructor, cancellationToken).Resolve(otherCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol != null) - { - // Update for transition from synthesized to declared deconstructor - AddUpdateEditsForMemberAndAccessors(semanticEdits, deconstructor, cancellationToken); - } - else if (isDelete) - { - // Delete synthesized deconstructor: - AddDeleteEditsForMemberAndAccessors(semanticEdits, deconstructor, deletedSymbolContainer: containingTypeKey, cancellationToken); - } - else - { - // Insert synthesized deconstructor: - AddInsertEditsForMemberAndAccessors(semanticEdits, deconstructor, cancellationToken); - } - } - } - } + /// + /// Returns whether or not the specified symbol can be deleted by the user. Normally deletes are a rude edit + /// but for some kinds of symbols we allow deletes, and synthesize an update to an empty method body during + /// emit. + /// + private static bool AllowsDeletion(ISymbol symbol) + { + // We don't currently allow deleting virtual or abstract methods, because if those are in the middle of + // an inheritance chain then throwing a missing method exception is not expected + if (symbol.GetSymbolModifiers() is not { IsVirtual: false, IsAbstract: false, IsOverride: false }) + return false; - /// - /// Returns whether or not the specified symbol can be deleted by the user. Normally deletes are a rude edit - /// but for some kinds of symbols we allow deletes, and synthesize an update to an empty method body during - /// emit. - /// - private static bool AllowsDeletion(ISymbol symbol) - { - // We don't currently allow deleting virtual or abstract methods, because if those are in the middle of - // an inheritance chain then throwing a missing method exception is not expected - if (symbol.GetSymbolModifiers() is not { IsVirtual: false, IsAbstract: false, IsOverride: false }) - return false; + // Extern methods can't be deleted + if (symbol.IsExtern) + return false; - // Extern methods can't be deleted - if (symbol.IsExtern) - return false; + // We don't allow deleting members from interfaces + if (symbol.ContainingType is { TypeKind: TypeKind.Interface }) + return false; - // We don't allow deleting members from interfaces - if (symbol.ContainingType is { TypeKind: TypeKind.Interface }) - return false; + // We store the containing symbol in NewSymbol of the edit for later use. + if (symbol is IMethodSymbol + { + MethodKind: + MethodKind.Ordinary or + MethodKind.Constructor or + MethodKind.EventAdd or + MethodKind.EventRemove or + MethodKind.EventRaise or + MethodKind.Conversion or + MethodKind.UserDefinedOperator or + MethodKind.PropertyGet or + MethodKind.PropertySet + }) + { + return true; + } - // We store the containing symbol in NewSymbol of the edit for later use. - if (symbol is IMethodSymbol - { - MethodKind: - MethodKind.Ordinary or - MethodKind.Constructor or - MethodKind.EventAdd or - MethodKind.EventRemove or - MethodKind.EventRaise or - MethodKind.Conversion or - MethodKind.UserDefinedOperator or - MethodKind.PropertyGet or - MethodKind.PropertySet - }) - { - return true; - } + // Can only delete event with explicitly declared accessors (otherwise a private field is generated that can't be deleted) + return symbol is + IParameterSymbol or + ITypeParameterSymbol or + IPropertySymbol or + IEventSymbol { AddMethod.IsImplicitlyDeclared: false, RemoveMethod.IsImplicitlyDeclared: false }; + } - // Can only delete event with explicitly declared accessors (otherwise a private field is generated that can't be deleted) - return symbol is - IParameterSymbol or - ITypeParameterSymbol or - IPropertySymbol or - IEventSymbol { AddMethod.IsImplicitlyDeclared: false, RemoveMethod.IsImplicitlyDeclared: false }; + /// + /// Add edit for the specified symbol and its accessors. + /// + private static void AddUpdateEditsForMemberAndAccessors(ArrayBuilder semanticEdits, ISymbol symbol, CancellationToken cancellationToken) + { + switch (symbol) + { + case IMethodSymbol or IFieldSymbol: + AddUpdate(symbol); + break; + + case IPropertySymbol propertySymbol: + AddUpdate(propertySymbol); + AddUpdate(propertySymbol.GetMethod); + AddUpdate(propertySymbol.SetMethod); + break; + + case IEventSymbol eventSymbol: + AddUpdate(eventSymbol); + AddUpdate(eventSymbol.AddMethod); + AddUpdate(eventSymbol.RemoveMethod); + AddUpdate(eventSymbol.RaiseMethod); + break; + + default: + throw ExceptionUtilities.UnexpectedValue(symbol.Kind); } - /// - /// Add edit for the specified symbol and its accessors. - /// - private static void AddUpdateEditsForMemberAndAccessors(ArrayBuilder semanticEdits, ISymbol symbol, CancellationToken cancellationToken) + void AddUpdate(ISymbol? symbol) { - switch (symbol) - { - case IMethodSymbol or IFieldSymbol: - AddUpdate(symbol); - break; + if (symbol is null) + return; - case IPropertySymbol propertySymbol: - AddUpdate(propertySymbol); - AddUpdate(propertySymbol.GetMethod); - AddUpdate(propertySymbol.SetMethod); - break; + Debug.Assert(symbol is not IMethodSymbol { IsPartialDefinition: true }); - case IEventSymbol eventSymbol: - AddUpdate(eventSymbol); - AddUpdate(eventSymbol.AddMethod); - AddUpdate(eventSymbol.RemoveMethod); - AddUpdate(eventSymbol.RaiseMethod); - break; + semanticEdits.Add(SemanticEditInfo.CreateUpdate(SymbolKey.Create(symbol, cancellationToken), syntaxMaps: default, partialType: null)); + } + } - default: - throw ExceptionUtilities.UnexpectedValue(symbol.Kind); - } + /// + /// Add edit for the specified symbol and its accessors. + /// + private static void AddDeleteEditsForMemberAndAccessors(ArrayBuilder semanticEdits, ISymbol oldSymbol, SymbolKey deletedSymbolContainer, CancellationToken cancellationToken) + { + switch (oldSymbol) + { + case IMethodSymbol or IFieldSymbol: + AddDelete(oldSymbol); + break; + + case IPropertySymbol propertySymbol: + // Delete accessors individually, because we actually just update them to be throwing. + AddDelete(propertySymbol); + AddDelete(propertySymbol.GetMethod); + AddDelete(propertySymbol.SetMethod); + break; + + case IEventSymbol eventSymbol: + // Delete accessors individually, because we actually just update them to be throwing. + AddDelete(eventSymbol); + AddDelete(eventSymbol.AddMethod); + AddDelete(eventSymbol.RemoveMethod); + AddDelete(eventSymbol.RaiseMethod); + break; + + default: + throw ExceptionUtilities.UnexpectedValue(oldSymbol.Kind); + } - void AddUpdate(ISymbol? symbol) - { - if (symbol is null) - return; + void AddDelete(ISymbol? symbol) + { + if (symbol is null) + return; + + Debug.Assert(symbol is not IMethodSymbol { IsPartialDefinition: true }); - Debug.Assert(symbol is not IMethodSymbol { IsPartialDefinition: true }); + var partialType = symbol is IMethodSymbol { PartialDefinitionPart: not null } ? SymbolKey.Create(symbol.ContainingType, cancellationToken) : (SymbolKey?)null; + semanticEdits.Add(SemanticEditInfo.CreateDelete(SymbolKey.Create(symbol, cancellationToken), deletedSymbolContainer, partialType)); + } + } + + /// + /// Add edit for the specified symbol and its accessors. + /// + private static void AddInsertEditsForMemberAndAccessors(ArrayBuilder semanticEdits, ISymbol newSymbol, CancellationToken cancellationToken) + { + // When inserting a new property, we need to insert the entire property, so + // that the backing field (if any), property and method semantics metadata tables can all be updated if/as necessary. + // + // When inserting a new event we need to insert the entire event, so + // pevent and method semantics metadata tables can all be updated if/as necessary. + + var partialType = newSymbol is IMethodSymbol { PartialDefinitionPart: not null } ? SymbolKey.Create(newSymbol.ContainingType, cancellationToken) : (SymbolKey?)null; + semanticEdits.Add(SemanticEditInfo.CreateInsert(SymbolKey.Create(newSymbol, cancellationToken), partialType)); + } - semanticEdits.Add(SemanticEditInfo.CreateUpdate(SymbolKey.Create(symbol, cancellationToken), syntaxMaps: default, partialType: null)); - } + private static void AddMemberSignatureOrNameChangeEdits( + ArrayBuilder semanticEdits, + ISymbol oldSymbol, + ISymbol newSymbol, + SymbolKey containingSymbolKey, + CancellationToken cancellationToken) + { + if (oldSymbol.Name != newSymbol.Name || oldSymbol is IMethodSymbol or IFieldSymbol) + { + AddDeleteEditsForMemberAndAccessors(semanticEdits, oldSymbol, containingSymbolKey, cancellationToken); + AddInsertEditsForMemberAndAccessors(semanticEdits, newSymbol, cancellationToken); + return; } - /// - /// Add edit for the specified symbol and its accessors. - /// - private static void AddDeleteEditsForMemberAndAccessors(ArrayBuilder semanticEdits, ISymbol oldSymbol, SymbolKey deletedSymbolContainer, CancellationToken cancellationToken) + switch (oldSymbol) { - switch (oldSymbol) - { - case IMethodSymbol or IFieldSymbol: - AddDelete(oldSymbol); - break; + case IPropertySymbol oldPropertySymbol: + // Properties may be overloaded on signature. - case IPropertySymbol propertySymbol: - // Delete accessors individually, because we actually just update them to be throwing. - AddDelete(propertySymbol); - AddDelete(propertySymbol.GetMethod); - AddDelete(propertySymbol.SetMethod); - break; + // delete the property and its accessors + AddDelete(oldPropertySymbol); + AddDelete(oldPropertySymbol.GetMethod); + AddDelete(oldPropertySymbol.SetMethod); - case IEventSymbol eventSymbol: - // Delete accessors individually, because we actually just update them to be throwing. - AddDelete(eventSymbol); - AddDelete(eventSymbol.AddMethod); - AddDelete(eventSymbol.RemoveMethod); - AddDelete(eventSymbol.RaiseMethod); - break; + // insert new property: + AddInsert(newSymbol); + break; - default: - throw ExceptionUtilities.UnexpectedValue(oldSymbol.Kind); - } + case IEventSymbol oldEventSymbol: + // Events can't be overloaded on their type. - void AddDelete(ISymbol? symbol) - { - if (symbol is null) - return; + // Update the event to associate it with the new accessors + semanticEdits.Add(SemanticEditInfo.CreateUpdate(SymbolKey.Create(oldSymbol, cancellationToken), syntaxMaps: default, partialType: null)); - Debug.Assert(symbol is not IMethodSymbol { IsPartialDefinition: true }); + // Do not change raise since its signature is not impacted by the event type change. - var partialType = symbol is IMethodSymbol { PartialDefinitionPart: not null } ? SymbolKey.Create(symbol.ContainingType, cancellationToken) : (SymbolKey?)null; - semanticEdits.Add(SemanticEditInfo.CreateDelete(SymbolKey.Create(symbol, cancellationToken), deletedSymbolContainer, partialType)); - } - } + // Update old bodies of add and remove to throw. + AddDelete(oldEventSymbol.AddMethod); + AddDelete(oldEventSymbol.RemoveMethod); - /// - /// Add edit for the specified symbol and its accessors. - /// - private static void AddInsertEditsForMemberAndAccessors(ArrayBuilder semanticEdits, ISymbol newSymbol, CancellationToken cancellationToken) - { - // When inserting a new property, we need to insert the entire property, so - // that the backing field (if any), property and method semantics metadata tables can all be updated if/as necessary. - // - // When inserting a new event we need to insert the entire event, so - // pevent and method semantics metadata tables can all be updated if/as necessary. + // Insert new add and remove: + var newEventSymbol = (IEventSymbol)newSymbol; + AddInsert(newEventSymbol.AddMethod); + AddInsert(newEventSymbol.RemoveMethod); + break; - var partialType = newSymbol is IMethodSymbol { PartialDefinitionPart: not null } ? SymbolKey.Create(newSymbol.ContainingType, cancellationToken) : (SymbolKey?)null; - semanticEdits.Add(SemanticEditInfo.CreateInsert(SymbolKey.Create(newSymbol, cancellationToken), partialType)); + default: + throw ExceptionUtilities.UnexpectedValue(oldSymbol.Kind); } - private static void AddMemberSignatureOrNameChangeEdits( - ArrayBuilder semanticEdits, - ISymbol oldSymbol, - ISymbol newSymbol, - SymbolKey containingSymbolKey, - CancellationToken cancellationToken) + void AddInsert(ISymbol? symbol) { - if (oldSymbol.Name != newSymbol.Name || oldSymbol is IMethodSymbol or IFieldSymbol) - { - AddDeleteEditsForMemberAndAccessors(semanticEdits, oldSymbol, containingSymbolKey, cancellationToken); - AddInsertEditsForMemberAndAccessors(semanticEdits, newSymbol, cancellationToken); + if (symbol is null) return; - } - switch (oldSymbol) - { - case IPropertySymbol oldPropertySymbol: - // Properties may be overloaded on signature. - - // delete the property and its accessors - AddDelete(oldPropertySymbol); - AddDelete(oldPropertySymbol.GetMethod); - AddDelete(oldPropertySymbol.SetMethod); - - // insert new property: - AddInsert(newSymbol); - break; + semanticEdits.Add(SemanticEditInfo.CreateInsert(SymbolKey.Create(symbol, cancellationToken), partialType: null)); + } - case IEventSymbol oldEventSymbol: - // Events can't be overloaded on their type. + void AddDelete(ISymbol? symbol) + { + if (symbol is null) + return; - // Update the event to associate it with the new accessors - semanticEdits.Add(SemanticEditInfo.CreateUpdate(SymbolKey.Create(oldSymbol, cancellationToken), syntaxMaps: default, partialType: null)); + semanticEdits.Add(SemanticEditInfo.CreateDelete(SymbolKey.Create(symbol, cancellationToken), containingSymbolKey, partialType: null)); + } + } - // Do not change raise since its signature is not impacted by the event type change. + private ImmutableArray<(ISymbol? oldSymbol, ISymbol? newSymbol, EditKind editKind)> GetNamespaceSymbolEdits( + SemanticModel oldModel, + SemanticModel newModel, + CancellationToken cancellationToken) + { + using var _1 = ArrayBuilder<(ISymbol? oldSymbol, ISymbol? newSymbol, EditKind editKind)>.GetInstance(out var builder); - // Update old bodies of add and remove to throw. - AddDelete(oldEventSymbol.AddMethod); - AddDelete(oldEventSymbol.RemoveMethod); + // Maps type name and arity to indices in builder array. Used to convert delete & insert edits to a move edit. + // If multiple types with the same name and arity are deleted we match the inserted types in the order they were declared + // and remove the index from the array, until no mathcing deleted type is found in the array of indices. + using var _2 = PooledDictionary<(string name, int arity), ArrayBuilder>.GetInstance(out var deletedTypes); - // Insert new add and remove: - var newEventSymbol = (IEventSymbol)newSymbol; - AddInsert(newEventSymbol.AddMethod); - AddInsert(newEventSymbol.RemoveMethod); - break; + // used to avoid duplicates due to partial declarations + using var _3 = PooledHashSet.GetInstance(out var processedTypes); - default: - throw ExceptionUtilities.UnexpectedValue(oldSymbol.Kind); - } + // Check that all top-level types declared in the old document are also declared in the new one. + // Those that are not were either deleted or renamed. - void AddInsert(ISymbol? symbol) + var oldRoot = oldModel.SyntaxTree.GetRoot(cancellationToken); + foreach (var oldTypeDeclaration in GetTopLevelTypeDeclarations(oldRoot)) + { + var oldType = (INamedTypeSymbol)GetRequiredDeclaredSymbol(oldModel, oldTypeDeclaration, cancellationToken); + if (!processedTypes.Add(oldType)) { - if (symbol is null) - return; - - semanticEdits.Add(SemanticEditInfo.CreateInsert(SymbolKey.Create(symbol, cancellationToken), partialType: null)); + continue; } - void AddDelete(ISymbol? symbol) + var newType = SymbolKey.Create(oldType, cancellationToken).Resolve(newModel.Compilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + if (newType == null) { - if (symbol is null) - return; + var key = (oldType.Name, oldType.Arity); - semanticEdits.Add(SemanticEditInfo.CreateDelete(SymbolKey.Create(symbol, cancellationToken), containingSymbolKey, partialType: null)); + if (!deletedTypes.TryGetValue(key, out var indices)) + deletedTypes.Add(key, indices = ArrayBuilder.GetInstance()); + + indices.Add(builder.Count); + builder.Add((oldSymbol: oldType, newSymbol: null, EditKind.Delete)); } } - private ImmutableArray<(ISymbol? oldSymbol, ISymbol? newSymbol, EditKind editKind)> GetNamespaceSymbolEdits( - SemanticModel oldModel, - SemanticModel newModel, - CancellationToken cancellationToken) - { - using var _1 = ArrayBuilder<(ISymbol? oldSymbol, ISymbol? newSymbol, EditKind editKind)>.GetInstance(out var builder); + // reverse all indices: + foreach (var (_, indices) in deletedTypes) + indices.ReverseContents(); - // Maps type name and arity to indices in builder array. Used to convert delete & insert edits to a move edit. - // If multiple types with the same name and arity are deleted we match the inserted types in the order they were declared - // and remove the index from the array, until no mathcing deleted type is found in the array of indices. - using var _2 = PooledDictionary<(string name, int arity), ArrayBuilder>.GetInstance(out var deletedTypes); + processedTypes.Clear(); - // used to avoid duplicates due to partial declarations - using var _3 = PooledHashSet.GetInstance(out var processedTypes); + // Check that all top-level types declared in the new document are also declared in the old one. + // Those that are not were added. - // Check that all top-level types declared in the old document are also declared in the new one. - // Those that are not were either deleted or renamed. + var newRoot = newModel.SyntaxTree.GetRoot(cancellationToken); + foreach (var newTypeDeclaration in GetTopLevelTypeDeclarations(newRoot)) + { + var newType = (INamedTypeSymbol)GetRequiredDeclaredSymbol(newModel, newTypeDeclaration, cancellationToken); + if (!processedTypes.Add(newType)) + { + continue; + } - var oldRoot = oldModel.SyntaxTree.GetRoot(cancellationToken); - foreach (var oldTypeDeclaration in GetTopLevelTypeDeclarations(oldRoot)) + var oldType = SymbolKey.Create(newType, cancellationToken).Resolve(oldModel.Compilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + if (oldType == null) { - var oldType = (INamedTypeSymbol)GetRequiredDeclaredSymbol(oldModel, oldTypeDeclaration, cancellationToken); - if (!processedTypes.Add(oldType)) + // Check if a type with the same name and arity was also removed. If so treat it as a move. + if (deletedTypes.TryGetValue((newType.Name, newType.Arity), out var deletedTypeIndices) && deletedTypeIndices.Count > 0) { - continue; - } + var deletedTypeIndex = deletedTypeIndices.Last(); + deletedTypeIndices.RemoveLast(); - var newType = SymbolKey.Create(oldType, cancellationToken).Resolve(newModel.Compilation, ignoreAssemblyKey: true, cancellationToken).Symbol; - if (newType == null) + builder[deletedTypeIndex] = (builder[deletedTypeIndex].oldSymbol, newType, EditKind.Move); + } + else { - var key = (oldType.Name, oldType.Arity); - - if (!deletedTypes.TryGetValue(key, out var indices)) - deletedTypes.Add(key, indices = ArrayBuilder.GetInstance()); - - indices.Add(builder.Count); - builder.Add((oldSymbol: oldType, newSymbol: null, EditKind.Delete)); + builder.Add((oldSymbol: null, newSymbol: newType, EditKind.Insert)); } } + } - // reverse all indices: - foreach (var (_, indices) in deletedTypes) - indices.ReverseContents(); - - processedTypes.Clear(); + // free all index array builders: + foreach (var (_, indices) in deletedTypes) + indices.Free(); - // Check that all top-level types declared in the new document are also declared in the old one. - // Those that are not were added. + return builder.ToImmutable(); + } - var newRoot = newModel.SyntaxTree.GetRoot(cancellationToken); - foreach (var newTypeDeclaration in GetTopLevelTypeDeclarations(newRoot)) + private static bool IsReloadable(INamedTypeSymbol type) + { + var current = type; + while (current != null) + { + foreach (var attributeData in current.GetAttributes()) { - var newType = (INamedTypeSymbol)GetRequiredDeclaredSymbol(newModel, newTypeDeclaration, cancellationToken); - if (!processedTypes.Add(newType)) + // We assume that the attribute System.Runtime.CompilerServices.CreateNewOnMetadataUpdateAttribute, if it exists, is well formed. + // If not an error will be reported during EnC delta emit. + if (attributeData.AttributeClass is { Name: CreateNewOnMetadataUpdateAttributeName, ContainingNamespace: { Name: "CompilerServices", ContainingNamespace: { Name: "Runtime", ContainingNamespace.Name: "System" } } }) { - continue; + return true; } + } - var oldType = SymbolKey.Create(newType, cancellationToken).Resolve(oldModel.Compilation, ignoreAssemblyKey: true, cancellationToken).Symbol; - if (oldType == null) - { - // Check if a type with the same name and arity was also removed. If so treat it as a move. - if (deletedTypes.TryGetValue((newType.Name, newType.Arity), out var deletedTypeIndices) && deletedTypeIndices.Count > 0) - { - var deletedTypeIndex = deletedTypeIndices.Last(); - deletedTypeIndices.RemoveLast(); + current = current.BaseType; + } - builder[deletedTypeIndex] = (builder[deletedTypeIndex].oldSymbol, newType, EditKind.Move); - } - else - { - builder.Add((oldSymbol: null, newSymbol: newType, EditKind.Insert)); - } - } - } + return false; + } - // free all index array builders: - foreach (var (_, indices) in deletedTypes) - indices.Free(); + private sealed class SemanticEditInfoComparer : IEqualityComparer + { + public static SemanticEditInfoComparer Instance = new(); - return builder.ToImmutable(); - } + private static readonly IEqualityComparer s_symbolKeyComparer = SymbolKey.GetComparer(); - private static bool IsReloadable(INamedTypeSymbol type) + public bool Equals([AllowNull] SemanticEditInfo x, [AllowNull] SemanticEditInfo y) { - var current = type; - while (current != null) + // When we delete a symbol, it might have the same symbol key as the matching insert + // edit that corresponds to it, for example if only the return type has changed, because + // symbol key does not consider return types. To ensure that this doesn't break us + // by incorrectly de-duping our two edits, we treat edits as equal only if their + // deleted symbol containers are both null, or both not null. + if (x.DeletedSymbolContainer is null != y.DeletedSymbolContainer is null) { - foreach (var attributeData in current.GetAttributes()) - { - // We assume that the attribute System.Runtime.CompilerServices.CreateNewOnMetadataUpdateAttribute, if it exists, is well formed. - // If not an error will be reported during EnC delta emit. - if (attributeData.AttributeClass is { Name: CreateNewOnMetadataUpdateAttributeName, ContainingNamespace: { Name: "CompilerServices", ContainingNamespace: { Name: "Runtime", ContainingNamespace.Name: "System" } } }) - { - return true; - } - } - - current = current.BaseType; + return false; } - return false; + return s_symbolKeyComparer.Equals(x.Symbol, y.Symbol); } - private sealed class SemanticEditInfoComparer : IEqualityComparer - { - public static SemanticEditInfoComparer Instance = new(); - - private static readonly IEqualityComparer s_symbolKeyComparer = SymbolKey.GetComparer(); + public int GetHashCode([DisallowNull] SemanticEditInfo obj) + => obj.Symbol.GetHashCode(); + } - public bool Equals([AllowNull] SemanticEditInfo x, [AllowNull] SemanticEditInfo y) - { - // When we delete a symbol, it might have the same symbol key as the matching insert - // edit that corresponds to it, for example if only the return type has changed, because - // symbol key does not consider return types. To ensure that this doesn't break us - // by incorrectly de-duping our two edits, we treat edits as equal only if their - // deleted symbol containers are both null, or both not null. - if (x.DeletedSymbolContainer is null != y.DeletedSymbolContainer is null) - { - return false; - } + private void ReportMemberOrLambdaBodyUpdateRudeEdits( + in DiagnosticContext diagnosticContext, + Compilation oldCompilation, + SyntaxNode? oldDeclaration, + ISymbol oldMember, + MemberBody? oldMemberBody, + DeclarationBody? oldBody, + SyntaxNode? newDeclaration, + ISymbol newMember, + MemberBody? newMemberBody, + DeclarationBody? newBody, + EditAndContinueCapabilitiesGrantor capabilities, + StateMachineInfo oldStateMachineInfo, + StateMachineInfo newStateMachineInfo, + CancellationToken cancellationToken) + { + Debug.Assert(oldBody == null || oldDeclaration != null && oldMemberBody != null); + Debug.Assert(newBody == null || newDeclaration != null && newMemberBody != null); - return s_symbolKeyComparer.Equals(x.Symbol, y.Symbol); - } + // Report rude edit if an unsupported operations is found in the new or old body. + // Only report for the new body if both bodies have unsupported operations. + _ = newBody != null && ReportUnsupportedOperations(diagnosticContext, newBody, cancellationToken) || + oldBody != null && ReportUnsupportedOperations(diagnosticContext, oldBody, cancellationToken); - public int GetHashCode([DisallowNull] SemanticEditInfo obj) - => obj.Symbol.GetHashCode(); + if (oldStateMachineInfo.IsStateMachine) + { + ReportMissingStateMachineAttribute(diagnosticContext, oldCompilation, oldStateMachineInfo, cancellationToken); } - private void ReportMemberOrLambdaBodyUpdateRudeEdits( - in DiagnosticContext diagnosticContext, - Compilation oldCompilation, - SyntaxNode? oldDeclaration, - ISymbol oldMember, - MemberBody? oldMemberBody, - DeclarationBody? oldBody, - SyntaxNode? newDeclaration, - ISymbol newMember, - MemberBody? newMemberBody, - DeclarationBody? newBody, - EditAndContinueCapabilitiesGrantor capabilities, - StateMachineInfo oldStateMachineInfo, - StateMachineInfo newStateMachineInfo, - CancellationToken cancellationToken) + if (!oldStateMachineInfo.IsStateMachine && + newStateMachineInfo.IsStateMachine && + !capabilities.Grant(EditAndContinueCapabilities.NewTypeDefinition)) { - Debug.Assert(oldBody == null || oldDeclaration != null && oldMemberBody != null); - Debug.Assert(newBody == null || newDeclaration != null && newMemberBody != null); - - // Report rude edit if an unsupported operations is found in the new or old body. - // Only report for the new body if both bodies have unsupported operations. - _ = newBody != null && ReportUnsupportedOperations(diagnosticContext, newBody, cancellationToken) || - oldBody != null && ReportUnsupportedOperations(diagnosticContext, oldBody, cancellationToken); - - if (oldStateMachineInfo.IsStateMachine) - { - ReportMissingStateMachineAttribute(diagnosticContext, oldCompilation, oldStateMachineInfo, cancellationToken); - } + // Adding a state machine, either for async or iterator, will require creating a new helper class + // so is a rude edit if the runtime doesn't support it + var rudeEdit = newStateMachineInfo.IsAsync ? RudeEditKind.MakeMethodAsyncNotSupportedByRuntime : RudeEditKind.MakeMethodIteratorNotSupportedByRuntime; + diagnosticContext.Report(rudeEdit, cancellationToken, arguments: []); + } - if (!oldStateMachineInfo.IsStateMachine && - newStateMachineInfo.IsStateMachine && - !capabilities.Grant(EditAndContinueCapabilities.NewTypeDefinition)) + if (oldStateMachineInfo.IsStateMachine && newStateMachineInfo.IsStateMachine) + { + if (!capabilities.Grant(EditAndContinueCapabilities.AddInstanceFieldToExistingType)) { - // Adding a state machine, either for async or iterator, will require creating a new helper class - // so is a rude edit if the runtime doesn't support it - var rudeEdit = newStateMachineInfo.IsAsync ? RudeEditKind.MakeMethodAsyncNotSupportedByRuntime : RudeEditKind.MakeMethodIteratorNotSupportedByRuntime; - diagnosticContext.Report(rudeEdit, cancellationToken, arguments: []); + diagnosticContext.Report(RudeEditKind.UpdatingStateMachineMethodNotSupportedByRuntime, cancellationToken, arguments: []); } - if (oldStateMachineInfo.IsStateMachine && newStateMachineInfo.IsStateMachine) + if ((InGenericContext(oldMember) || + InGenericContext(newMember) || + oldBody is LambdaBody && InGenericLocalContext(oldDeclaration!, oldMemberBody!.RootNodes) || + newBody is LambdaBody && InGenericLocalContext(newDeclaration!, newMemberBody!.RootNodes)) && + !capabilities.Grant(EditAndContinueCapabilities.GenericAddFieldToExistingType)) { - if (!capabilities.Grant(EditAndContinueCapabilities.AddInstanceFieldToExistingType)) - { - diagnosticContext.Report(RudeEditKind.UpdatingStateMachineMethodNotSupportedByRuntime, cancellationToken, arguments: []); - } - - if ((InGenericContext(oldMember) || - InGenericContext(newMember) || - oldBody is LambdaBody && InGenericLocalContext(oldDeclaration!, oldMemberBody!.RootNodes) || - newBody is LambdaBody && InGenericLocalContext(newDeclaration!, newMemberBody!.RootNodes)) && - !capabilities.Grant(EditAndContinueCapabilities.GenericAddFieldToExistingType)) - { - diagnosticContext.Report(RudeEditKind.UpdatingGenericNotSupportedByRuntime, cancellationToken); - } + diagnosticContext.Report(RudeEditKind.UpdatingGenericNotSupportedByRuntime, cancellationToken); } } + } - private void ReportUpdatedSymbolDeclarationRudeEdits( - in DiagnosticContext diagnosticContext, - EditAndContinueCapabilitiesGrantor capabilities, - out bool hasGeneratedAttributeChange, - out bool hasGeneratedReturnTypeAttributeChange, - CancellationToken cancellationToken) - { - var rudeEdit = RudeEditKind.None; - var oldSymbol = diagnosticContext.RequiredOldSymbol; - var newSymbol = diagnosticContext.RequiredNewSymbol; + private void ReportUpdatedSymbolDeclarationRudeEdits( + in DiagnosticContext diagnosticContext, + EditAndContinueCapabilitiesGrantor capabilities, + out bool hasGeneratedAttributeChange, + out bool hasGeneratedReturnTypeAttributeChange, + CancellationToken cancellationToken) + { + var rudeEdit = RudeEditKind.None; + var oldSymbol = diagnosticContext.RequiredOldSymbol; + var newSymbol = diagnosticContext.RequiredNewSymbol; - hasGeneratedAttributeChange = false; - hasGeneratedReturnTypeAttributeChange = false; + hasGeneratedAttributeChange = false; + hasGeneratedReturnTypeAttributeChange = false; - if (oldSymbol.Kind != newSymbol.Kind) + if (oldSymbol.Kind != newSymbol.Kind) + { + rudeEdit = (oldSymbol.Kind == SymbolKind.Field || newSymbol.Kind == SymbolKind.Field) ? RudeEditKind.FieldKindUpdate : RudeEditKind.Update; + } + else if (oldSymbol.Name != newSymbol.Name) + { + if (oldSymbol is IParameterSymbol && newSymbol is IParameterSymbol) { - rudeEdit = (oldSymbol.Kind == SymbolKind.Field || newSymbol.Kind == SymbolKind.Field) ? RudeEditKind.FieldKindUpdate : RudeEditKind.Update; + // We defer checking parameter renames until later, because if their types have also changed + // then we'll be emitting a new method, so it won't be a rename any more } - else if (oldSymbol.Name != newSymbol.Name) + else if (oldSymbol is IMethodSymbol oldMethod && newSymbol is IMethodSymbol newMethod) { - if (oldSymbol is IParameterSymbol && newSymbol is IParameterSymbol) - { - // We defer checking parameter renames until later, because if their types have also changed - // then we'll be emitting a new method, so it won't be a rename any more - } - else if (oldSymbol is IMethodSymbol oldMethod && newSymbol is IMethodSymbol newMethod) + if (oldMethod.AssociatedSymbol != null && newMethod.AssociatedSymbol != null) { - if (oldMethod.AssociatedSymbol != null && newMethod.AssociatedSymbol != null) - { - if (oldMethod.MethodKind != newMethod.MethodKind) - { - rudeEdit = RudeEditKind.AccessorKindUpdate; - } - else - { - // rude edit will be reported by the associated symbol - rudeEdit = RudeEditKind.None; - } - } - else if (oldMethod.MethodKind == MethodKind.Conversion) - { - rudeEdit = RudeEditKind.ModifiersUpdate; - } - else if (oldMethod.MethodKind == MethodKind.ExplicitInterfaceImplementation || newMethod.MethodKind == MethodKind.ExplicitInterfaceImplementation) - { - // Can't change from explicit to implicit interface implementation, or one interface to another - rudeEdit = RudeEditKind.Renamed; - } - else if (!AllowsDeletion(oldSymbol)) + if (oldMethod.MethodKind != newMethod.MethodKind) { - rudeEdit = RudeEditKind.Renamed; + rudeEdit = RudeEditKind.AccessorKindUpdate; } - else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities)) + else { - rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; + // rude edit will be reported by the associated symbol + rudeEdit = RudeEditKind.None; } } - else if (oldSymbol is IPropertySymbol oldProperty && newSymbol is IPropertySymbol newProperty) + else if (oldMethod.MethodKind == MethodKind.Conversion) { - if (!oldProperty.ExplicitInterfaceImplementations.IsEmpty || !newProperty.ExplicitInterfaceImplementations.IsEmpty) - { - // Can't change from explicit to implicit interface implementation, or one interface to another - rudeEdit = RudeEditKind.Renamed; - } - else if (!AllowsDeletion(oldSymbol)) - { - rudeEdit = RudeEditKind.Renamed; - } - else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities)) - { - rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; - } + rudeEdit = RudeEditKind.ModifiersUpdate; } - else if (oldSymbol is IEventSymbol oldEvent && newSymbol is IEventSymbol newEvent) + else if (oldMethod.MethodKind == MethodKind.ExplicitInterfaceImplementation || newMethod.MethodKind == MethodKind.ExplicitInterfaceImplementation) { - if (!oldEvent.ExplicitInterfaceImplementations.IsEmpty || !newEvent.ExplicitInterfaceImplementations.IsEmpty) - { - // Can't change from explicit to implicit interface implementation, or one interface to another - rudeEdit = RudeEditKind.Renamed; - } - else if (!AllowsDeletion(oldSymbol)) - { - rudeEdit = RudeEditKind.Renamed; - } - else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities)) - { - rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; - } + // Can't change from explicit to implicit interface implementation, or one interface to another + rudeEdit = RudeEditKind.Renamed; } - else + else if (!AllowsDeletion(oldSymbol)) { rudeEdit = RudeEditKind.Renamed; } - } - - if (oldSymbol.DeclaredAccessibility != newSymbol.DeclaredAccessibility) - { - rudeEdit = RudeEditKind.ChangingAccessibility; - } - - if (oldSymbol.IsStatic != newSymbol.IsStatic || - oldSymbol.IsVirtual != newSymbol.IsVirtual || - oldSymbol.IsAbstract != newSymbol.IsAbstract || - oldSymbol.IsOverride != newSymbol.IsOverride || - oldSymbol.IsExtern != newSymbol.IsExtern) - { - // Do not report for accessors as the error will be reported on their associated symbol. - if (oldSymbol is not IMethodSymbol { AssociatedSymbol: not null }) + else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities)) { - rudeEdit = RudeEditKind.ModifiersUpdate; + rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; } } - - if (oldSymbol is IFieldSymbol oldField && newSymbol is IFieldSymbol newField) + else if (oldSymbol is IPropertySymbol oldProperty && newSymbol is IPropertySymbol newProperty) { - if (oldField.IsConst != newField.IsConst || - oldField.IsReadOnly != newField.IsReadOnly || - oldField.IsVolatile != newField.IsVolatile) - { - rudeEdit = RudeEditKind.ModifiersUpdate; - } - - // Report rude edit for updating const fields and values of enums. - // The latter is only reported whne the enum underlying type does not change to avoid cascading rude edits. - if (oldField.IsConst && newField.IsConst && !Equals(oldField.ConstantValue, newField.ConstantValue) && - TypesEquivalent(oldField.ContainingType.EnumUnderlyingType, newField.ContainingType.EnumUnderlyingType, exact: false)) + if (!oldProperty.ExplicitInterfaceImplementations.IsEmpty || !newProperty.ExplicitInterfaceImplementations.IsEmpty) { - rudeEdit = RudeEditKind.InitializerUpdate; + // Can't change from explicit to implicit interface implementation, or one interface to another + rudeEdit = RudeEditKind.Renamed; } - - if (oldField.FixedSize != newField.FixedSize) + else if (!AllowsDeletion(oldSymbol)) { - rudeEdit = RudeEditKind.FixedSizeFieldUpdate; + rudeEdit = RudeEditKind.Renamed; } - - if (!IsMemberOrDelegateReplaced(oldField, newField)) + else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities)) { - Debug.Assert(ReturnTypesEquivalent(oldField, newField, exact: false)); - hasGeneratedAttributeChange |= !ReturnTypesEquivalent(oldField, newField, exact: true); + rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; } } - else if (oldSymbol is IMethodSymbol oldMethod && newSymbol is IMethodSymbol newMethod) + else if (oldSymbol is IEventSymbol oldEvent && newSymbol is IEventSymbol newEvent) { - // Changing property accessor to auto-property accessor adds a field: - if (oldMethod is { MethodKind: MethodKind.PropertyGet, AssociatedSymbol: IPropertySymbol oldProperty } && !oldProperty.IsAutoProperty() && - newMethod is { MethodKind: MethodKind.PropertyGet, AssociatedSymbol: IPropertySymbol newProperty } && newProperty.IsAutoProperty() && - !capabilities.Grant(GetRequiredAddFieldCapabilities(newMethod))) - { - rudeEdit = RudeEditKind.InsertNotSupportedByRuntime; - } - - // Consider: Generalize to compare P/Invokes regardless of how they are defined (using attribute or Declare) - if (oldMethod.MethodKind == MethodKind.DeclareMethod || newMethod.MethodKind == MethodKind.DeclareMethod) - { - var oldImportData = oldMethod.GetDllImportData(); - var newImportData = newMethod.GetDllImportData(); - if (oldImportData != null && newImportData != null) - { - // Declare method syntax can't change these. - Debug.Assert(oldImportData.BestFitMapping == newImportData.BestFitMapping || - oldImportData.CallingConvention == newImportData.CallingConvention || - oldImportData.ExactSpelling == newImportData.ExactSpelling || - oldImportData.SetLastError == newImportData.SetLastError || - oldImportData.ThrowOnUnmappableCharacter == newImportData.ThrowOnUnmappableCharacter); - - if (oldImportData.ModuleName != newImportData.ModuleName) - { - rudeEdit = RudeEditKind.DeclareLibraryUpdate; - } - else if (oldImportData.EntryPointName != newImportData.EntryPointName) - { - rudeEdit = RudeEditKind.DeclareAliasUpdate; - } - else if (oldImportData.CharacterSet != newImportData.CharacterSet) - { - rudeEdit = RudeEditKind.ModifiersUpdate; - } - } - else if (oldImportData is null != newImportData is null) - { - rudeEdit = RudeEditKind.ModifiersUpdate; - } - } - - // VB implements clause (the method name is the same, but interface implementations differ) - if (oldMethod.Name == newMethod.Name && - !oldMethod.ExplicitInterfaceImplementations.SequenceEqual(newMethod.ExplicitInterfaceImplementations, SymbolsEquivalent)) + if (!oldEvent.ExplicitInterfaceImplementations.IsEmpty || !newEvent.ExplicitInterfaceImplementations.IsEmpty) { - rudeEdit = RudeEditKind.ImplementsClauseUpdate; + // Can't change from explicit to implicit interface implementation, or one interface to another + rudeEdit = RudeEditKind.Renamed; } - - // VB handles clause - if (!AreHandledEventsEqual(oldMethod, newMethod)) + else if (!AllowsDeletion(oldSymbol)) { - rudeEdit = RudeEditKind.HandlesClauseUpdate; + rudeEdit = RudeEditKind.Renamed; } - - if (oldMethod.IsReadOnly != newMethod.IsReadOnly) + else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities)) { - hasGeneratedAttributeChange = true; + rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; } + } + else + { + rudeEdit = RudeEditKind.Renamed; + } + } - if (oldMethod.IsInitOnly != newMethod.IsInitOnly) - { - // modreq(IsExternalInit) on the return type - rudeEdit = RudeEditKind.AccessorKindUpdate; - } + if (oldSymbol.DeclaredAccessibility != newSymbol.DeclaredAccessibility) + { + rudeEdit = RudeEditKind.ChangingAccessibility; + } - // Check return type - do not report for accessors, their containing symbol will report the rude edits and attribute updates. - if (rudeEdit == RudeEditKind.None && - oldMethod.AssociatedSymbol == null && - newMethod.AssociatedSymbol == null && - !IsMemberOrDelegateReplaced(oldMethod, newMethod)) - { - Debug.Assert(ReturnTypesEquivalent(oldMethod, newMethod, exact: false)); - hasGeneratedReturnTypeAttributeChange |= !ReturnTypesEquivalent(oldMethod, newMethod, exact: true); - } - } - else if (oldSymbol is INamedTypeSymbol oldType && newSymbol is INamedTypeSymbol newType) + if (oldSymbol.IsStatic != newSymbol.IsStatic || + oldSymbol.IsVirtual != newSymbol.IsVirtual || + oldSymbol.IsAbstract != newSymbol.IsAbstract || + oldSymbol.IsOverride != newSymbol.IsOverride || + oldSymbol.IsExtern != newSymbol.IsExtern) + { + // Do not report for accessors as the error will be reported on their associated symbol. + if (oldSymbol is not IMethodSymbol { AssociatedSymbol: not null }) { - if (oldType.TypeKind != newType.TypeKind || - oldType.IsRecord != newType.IsRecord) // TODO: https://github.com/dotnet/roslyn/issues/51874 - { - rudeEdit = RudeEditKind.TypeKindUpdate; - } - else if (oldType.IsRefLikeType != newType.IsRefLikeType || - oldType.IsReadOnly != newType.IsReadOnly) - { - rudeEdit = RudeEditKind.ModifiersUpdate; - } + rudeEdit = RudeEditKind.ModifiersUpdate; + } + } - if (rudeEdit == RudeEditKind.None) - { - AnalyzeBaseTypes(oldType, newType, ref rudeEdit, ref hasGeneratedAttributeChange); + if (oldSymbol is IFieldSymbol oldField && newSymbol is IFieldSymbol newField) + { + if (oldField.IsConst != newField.IsConst || + oldField.IsReadOnly != newField.IsReadOnly || + oldField.IsVolatile != newField.IsVolatile) + { + rudeEdit = RudeEditKind.ModifiersUpdate; + } - if (oldType.DelegateInvokeMethod != null) - { - Contract.ThrowIfNull(newType.DelegateInvokeMethod); - Debug.Assert(ReturnTypesEquivalent(oldType.DelegateInvokeMethod, newType.DelegateInvokeMethod, exact: false)); + // Report rude edit for updating const fields and values of enums. + // The latter is only reported whne the enum underlying type does not change to avoid cascading rude edits. + if (oldField.IsConst && newField.IsConst && !Equals(oldField.ConstantValue, newField.ConstantValue) && + TypesEquivalent(oldField.ContainingType.EnumUnderlyingType, newField.ContainingType.EnumUnderlyingType, exact: false)) + { + rudeEdit = RudeEditKind.InitializerUpdate; + } - hasGeneratedReturnTypeAttributeChange |= !ReturnTypesEquivalent(oldType.DelegateInvokeMethod, newType.DelegateInvokeMethod, exact: true); - } - } + if (oldField.FixedSize != newField.FixedSize) + { + rudeEdit = RudeEditKind.FixedSizeFieldUpdate; } - else if (oldSymbol is IPropertySymbol oldProperty && newSymbol is IPropertySymbol newProperty) + + if (!IsMemberOrDelegateReplaced(oldField, newField)) { - if (!IsMemberOrDelegateReplaced(oldProperty, newProperty)) - { - Debug.Assert(ReturnTypesEquivalent(oldProperty, newProperty, exact: false)); - hasGeneratedReturnTypeAttributeChange |= !ReturnTypesEquivalent(oldProperty, newProperty, exact: true); - } + Debug.Assert(ReturnTypesEquivalent(oldField, newField, exact: false)); + hasGeneratedAttributeChange |= !ReturnTypesEquivalent(oldField, newField, exact: true); } - else if (oldSymbol is IEventSymbol oldEvent && newSymbol is IEventSymbol newEvent) + } + else if (oldSymbol is IMethodSymbol oldMethod && newSymbol is IMethodSymbol newMethod) + { + // Changing property accessor to auto-property accessor adds a field: + if (oldMethod is { MethodKind: MethodKind.PropertyGet, AssociatedSymbol: IPropertySymbol oldProperty } && !oldProperty.IsAutoProperty() && + newMethod is { MethodKind: MethodKind.PropertyGet, AssociatedSymbol: IPropertySymbol newProperty } && newProperty.IsAutoProperty() && + !capabilities.Grant(GetRequiredAddFieldCapabilities(newMethod))) { - // "readonly" modifier can only be applied on the event itself, not on its accessors. - if (oldEvent.AddMethod != null && newEvent.AddMethod != null && oldEvent.AddMethod.IsReadOnly != newEvent.AddMethod.IsReadOnly || - oldEvent.RemoveMethod != null && newEvent.RemoveMethod != null && oldEvent.RemoveMethod.IsReadOnly != newEvent.RemoveMethod.IsReadOnly) - { - hasGeneratedAttributeChange = true; - } - else if (!IsMemberOrDelegateReplaced(oldEvent, newEvent)) - { - Debug.Assert(ReturnTypesEquivalent(oldEvent, newEvent, exact: false)); - hasGeneratedReturnTypeAttributeChange |= !ReturnTypesEquivalent(oldEvent, newEvent, exact: true); - } + rudeEdit = RudeEditKind.InsertNotSupportedByRuntime; } - else if (oldSymbol is IParameterSymbol oldParameter && newSymbol is IParameterSymbol newParameter) + + // Consider: Generalize to compare P/Invokes regardless of how they are defined (using attribute or Declare) + if (oldMethod.MethodKind == MethodKind.DeclareMethod || newMethod.MethodKind == MethodKind.DeclareMethod) { - // If the containing member is being replaced then parameters are not being updated. - if (!IsMemberOrDelegateReplaced(oldParameter.ContainingSymbol, newParameter.ContainingSymbol)) + var oldImportData = oldMethod.GetDllImportData(); + var newImportData = newMethod.GetDllImportData(); + if (oldImportData != null && newImportData != null) { - if (IsExtensionMethodThisParameter(oldParameter) != IsExtensionMethodThisParameter(newParameter) || - GeneratesParameterAttribute(oldParameter.RefKind) != GeneratesParameterAttribute(newParameter.RefKind) || - oldParameter.IsParams != newParameter.IsParams || - !ParameterTypesEquivalent(oldParameter, newParameter, exact: true)) + // Declare method syntax can't change these. + Debug.Assert(oldImportData.BestFitMapping == newImportData.BestFitMapping || + oldImportData.CallingConvention == newImportData.CallingConvention || + oldImportData.ExactSpelling == newImportData.ExactSpelling || + oldImportData.SetLastError == newImportData.SetLastError || + oldImportData.ThrowOnUnmappableCharacter == newImportData.ThrowOnUnmappableCharacter); + + if (oldImportData.ModuleName != newImportData.ModuleName) { - hasGeneratedAttributeChange = true; + rudeEdit = RudeEditKind.DeclareLibraryUpdate; } - - if (oldParameter.HasExplicitDefaultValue != newParameter.HasExplicitDefaultValue || - oldParameter.HasExplicitDefaultValue && !Equals(oldParameter.ExplicitDefaultValue, newParameter.ExplicitDefaultValue)) + else if (oldImportData.EntryPointName != newImportData.EntryPointName) { - rudeEdit = RudeEditKind.InitializerUpdate; + rudeEdit = RudeEditKind.DeclareAliasUpdate; } - else if (oldParameter.Name != newParameter.Name && !capabilities.Grant(EditAndContinueCapabilities.UpdateParameters)) + else if (oldImportData.CharacterSet != newImportData.CharacterSet) { - rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; + rudeEdit = RudeEditKind.ModifiersUpdate; } } - } - else if (oldSymbol is ITypeParameterSymbol oldTypeParameter && newSymbol is ITypeParameterSymbol newTypeParameter) - { - AnalyzeTypeParameter(oldTypeParameter, newTypeParameter, ref rudeEdit, ref hasGeneratedAttributeChange); - } - - // Do not report modifier update if type kind changed. - if (rudeEdit == RudeEditKind.None && oldSymbol.IsSealed != newSymbol.IsSealed) - { - // Do not report for accessors as the error will be reported on their associated symbol. - if (oldSymbol is not IMethodSymbol { AssociatedSymbol: not null }) + else if (oldImportData is null != newImportData is null) { rudeEdit = RudeEditKind.ModifiersUpdate; } } - // updating within generic context - if (rudeEdit == RudeEditKind.None && - oldSymbol is not INamedTypeSymbol and not ITypeParameterSymbol and not IParameterSymbol && - InGenericContext(oldSymbol) && - !capabilities.Grant(EditAndContinueCapabilities.GenericUpdateMethod)) + // VB implements clause (the method name is the same, but interface implementations differ) + if (oldMethod.Name == newMethod.Name && + !oldMethod.ExplicitInterfaceImplementations.SequenceEqual(newMethod.ExplicitInterfaceImplementations, SymbolsEquivalent)) { - rudeEdit = RudeEditKind.UpdatingGenericNotSupportedByRuntime; + rudeEdit = RudeEditKind.ImplementsClauseUpdate; } - if (rudeEdit != RudeEditKind.None) + // VB handles clause + if (!AreHandledEventsEqual(oldMethod, newMethod)) { - diagnosticContext.Report(rudeEdit, cancellationToken); + rudeEdit = RudeEditKind.HandlesClauseUpdate; } - } - private static bool GeneratesParameterAttribute(RefKind kind) - => kind is RefKind.In or RefKind.RefReadOnlyParameter; + if (oldMethod.IsReadOnly != newMethod.IsReadOnly) + { + hasGeneratedAttributeChange = true; + } - private static void AnalyzeBaseTypes(INamedTypeSymbol oldType, INamedTypeSymbol newType, ref RudeEditKind rudeEdit, ref bool hasGeneratedAttributeChange) - { - if (oldType.EnumUnderlyingType != null && newType.EnumUnderlyingType != null) + if (oldMethod.IsInitOnly != newMethod.IsInitOnly) { - if (!TypesEquivalent(oldType.EnumUnderlyingType, newType.EnumUnderlyingType, exact: true)) - { - if (TypesEquivalent(oldType.EnumUnderlyingType, newType.EnumUnderlyingType, exact: false)) - { - hasGeneratedAttributeChange = true; - } - else - { - rudeEdit = RudeEditKind.EnumUnderlyingTypeUpdate; - } - } + // modreq(IsExternalInit) on the return type + rudeEdit = RudeEditKind.AccessorKindUpdate; } - else if (!BaseTypesEquivalent(oldType, newType, exact: true)) + + // Check return type - do not report for accessors, their containing symbol will report the rude edits and attribute updates. + if (rudeEdit == RudeEditKind.None && + oldMethod.AssociatedSymbol == null && + newMethod.AssociatedSymbol == null && + !IsMemberOrDelegateReplaced(oldMethod, newMethod)) { - if (BaseTypesEquivalent(oldType, newType, exact: false)) - { - hasGeneratedAttributeChange = true; - } - else - { - rudeEdit = RudeEditKind.BaseTypeOrInterfaceUpdate; - } + Debug.Assert(ReturnTypesEquivalent(oldMethod, newMethod, exact: false)); + hasGeneratedReturnTypeAttributeChange |= !ReturnTypesEquivalent(oldMethod, newMethod, exact: true); } } - - private static RudeEditKind GetSignatureChangeRudeEdit(ISymbol oldMember, ISymbol newMember, EditAndContinueCapabilitiesGrantor capabilities) + else if (oldSymbol is INamedTypeSymbol oldType && newSymbol is INamedTypeSymbol newType) { - if (oldMember.Kind != newMember.Kind) + if (oldType.TypeKind != newType.TypeKind || + oldType.IsRecord != newType.IsRecord) // TODO: https://github.com/dotnet/roslyn/issues/51874 + { + rudeEdit = RudeEditKind.TypeKindUpdate; + } + else if (oldType.IsRefLikeType != newType.IsRefLikeType || + oldType.IsReadOnly != newType.IsReadOnly) { - // rude edit will be reported later - return RudeEditKind.None; + rudeEdit = RudeEditKind.ModifiersUpdate; } - if (IsGlobalMain(oldMember)) + if (rudeEdit == RudeEditKind.None) { - // Only return type can be changed: - Debug.Assert(ParameterTypesEquivalent(oldMember.GetParameters(), newMember.GetParameters(), exact: true)); + AnalyzeBaseTypes(oldType, newType, ref rudeEdit, ref hasGeneratedAttributeChange); - return RudeEditKind.ChangeImplicitMainReturnType; - } + if (oldType.DelegateInvokeMethod != null) + { + Contract.ThrowIfNull(newType.DelegateInvokeMethod); + Debug.Assert(ReturnTypesEquivalent(oldType.DelegateInvokeMethod, newType.DelegateInvokeMethod, exact: false)); - if (!AllowsDeletion(newMember)) + hasGeneratedReturnTypeAttributeChange |= !ReturnTypesEquivalent(oldType.DelegateInvokeMethod, newType.DelegateInvokeMethod, exact: true); + } + } + } + else if (oldSymbol is IPropertySymbol oldProperty && newSymbol is IPropertySymbol newProperty) + { + if (!IsMemberOrDelegateReplaced(oldProperty, newProperty)) { - return RudeEditKind.TypeUpdate; + Debug.Assert(ReturnTypesEquivalent(oldProperty, newProperty, exact: false)); + hasGeneratedReturnTypeAttributeChange |= !ReturnTypesEquivalent(oldProperty, newProperty, exact: true); } - - // Note: do not report a rude edit for property/event accessors as it will already be reported for the property/event itself. - if (!CanRenameOrChangeSignature(oldMember, newMember, capabilities) && - oldMember is not IMethodSymbol { AssociatedSymbol.Kind: SymbolKind.Property or SymbolKind.Event }) + } + else if (oldSymbol is IEventSymbol oldEvent && newSymbol is IEventSymbol newEvent) + { + // "readonly" modifier can only be applied on the event itself, not on its accessors. + if (oldEvent.AddMethod != null && newEvent.AddMethod != null && oldEvent.AddMethod.IsReadOnly != newEvent.AddMethod.IsReadOnly || + oldEvent.RemoveMethod != null && newEvent.RemoveMethod != null && oldEvent.RemoveMethod.IsReadOnly != newEvent.RemoveMethod.IsReadOnly) { - return RudeEditKind.ChangingSignatureNotSupportedByRuntime; + hasGeneratedAttributeChange = true; + } + else if (!IsMemberOrDelegateReplaced(oldEvent, newEvent)) + { + Debug.Assert(ReturnTypesEquivalent(oldEvent, newEvent, exact: false)); + hasGeneratedReturnTypeAttributeChange |= !ReturnTypesEquivalent(oldEvent, newEvent, exact: true); } - - return RudeEditKind.None; } - - private static void AnalyzeTypeParameter(ITypeParameterSymbol oldParameter, ITypeParameterSymbol newParameter, ref RudeEditKind rudeEdit, ref bool hasGeneratedAttributeChange) + else if (oldSymbol is IParameterSymbol oldParameter && newSymbol is IParameterSymbol newParameter) { - if (!TypeParameterConstraintsEquivalent(oldParameter, newParameter, exact: true)) + // If the containing member is being replaced then parameters are not being updated. + if (!IsMemberOrDelegateReplaced(oldParameter.ContainingSymbol, newParameter.ContainingSymbol)) { - if (TypeParameterConstraintsEquivalent(oldParameter, newParameter, exact: false)) + if (IsExtensionMethodThisParameter(oldParameter) != IsExtensionMethodThisParameter(newParameter) || + GeneratesParameterAttribute(oldParameter.RefKind) != GeneratesParameterAttribute(newParameter.RefKind) || + oldParameter.IsParams != newParameter.IsParams || + !ParameterTypesEquivalent(oldParameter, newParameter, exact: true)) { hasGeneratedAttributeChange = true; } - else + + if (oldParameter.HasExplicitDefaultValue != newParameter.HasExplicitDefaultValue || + oldParameter.HasExplicitDefaultValue && !Equals(oldParameter.ExplicitDefaultValue, newParameter.ExplicitDefaultValue)) { - rudeEdit = (oldParameter.Variance != newParameter.Variance) ? RudeEditKind.VarianceUpdate : RudeEditKind.ChangingConstraints; + rudeEdit = RudeEditKind.InitializerUpdate; + } + else if (oldParameter.Name != newParameter.Name && !capabilities.Grant(EditAndContinueCapabilities.UpdateParameters)) + { + rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; } } } + else if (oldSymbol is ITypeParameterSymbol oldTypeParameter && newSymbol is ITypeParameterSymbol newTypeParameter) + { + AnalyzeTypeParameter(oldTypeParameter, newTypeParameter, ref rudeEdit, ref hasGeneratedAttributeChange); + } - private static bool IsExtensionMethodThisParameter(IParameterSymbol parameter) - => parameter is { Ordinal: 0, ContainingSymbol: IMethodSymbol { IsExtensionMethod: true } }; - - private void AnalyzeSymbolUpdate( - in DiagnosticContext diagnosticContext, - EditAndContinueCapabilitiesGrantor capabilities, - ArrayBuilder semanticEdits, - out bool hasAttributeChange, - CancellationToken cancellationToken) + // Do not report modifier update if type kind changed. + if (rudeEdit == RudeEditKind.None && oldSymbol.IsSealed != newSymbol.IsSealed) { - // TODO: fails in VB on delegate parameter https://github.com/dotnet/roslyn/issues/53337 - // Contract.ThrowIfFalse(newSymbol.IsImplicitlyDeclared == newDeclaration is null); + // Do not report for accessors as the error will be reported on their associated symbol. + if (oldSymbol is not IMethodSymbol { AssociatedSymbol: not null }) + { + rudeEdit = RudeEditKind.ModifiersUpdate; + } + } - ReportUpdatedSymbolDeclarationRudeEdits( - diagnosticContext, capabilities, out var hasGeneratedAttributeChange, out var hasGeneratedReturnTypeAttributeChange, cancellationToken); + // updating within generic context + if (rudeEdit == RudeEditKind.None && + oldSymbol is not INamedTypeSymbol and not ITypeParameterSymbol and not IParameterSymbol && + InGenericContext(oldSymbol) && + !capabilities.Grant(EditAndContinueCapabilities.GenericUpdateMethod)) + { + rudeEdit = RudeEditKind.UpdatingGenericNotSupportedByRuntime; + } - // We don't check capabilities of the runtime to update compiler generated attributes. - // All runtimes support changing the attributes in metadata, some just don't reflect the changes in the Reflection model. - // Having compiler-generated attributes visible via Reflaction API is not that important. - ReportCustomAttributeRudeEdits(diagnosticContext, capabilities, out var hasSymbolAttributeChange, out var hasReturnTypeAttributeChange, cancellationToken); - hasSymbolAttributeChange |= hasGeneratedAttributeChange; - hasReturnTypeAttributeChange |= hasGeneratedReturnTypeAttributeChange; + if (rudeEdit != RudeEditKind.None) + { + diagnosticContext.Report(rudeEdit, cancellationToken); + } + } - var oldSymbol = diagnosticContext.RequiredOldSymbol; - var newSymbol = diagnosticContext.RequiredNewSymbol; + private static bool GeneratesParameterAttribute(RefKind kind) + => kind is RefKind.In or RefKind.RefReadOnlyParameter; - if (oldSymbol is IParameterSymbol oldParameter && newSymbol is IParameterSymbol newParameter) + private static void AnalyzeBaseTypes(INamedTypeSymbol oldType, INamedTypeSymbol newType, ref RudeEditKind rudeEdit, ref bool hasGeneratedAttributeChange) + { + if (oldType.EnumUnderlyingType != null && newType.EnumUnderlyingType != null) + { + if (!TypesEquivalent(oldType.EnumUnderlyingType, newType.EnumUnderlyingType, exact: true)) { - AddSemanticEditsOriginatingFromParameterUpdate(semanticEdits, oldParameter, newParameter, diagnosticContext.NewModel.Compilation, cancellationToken); - - // Attributes applied on parameters of a delegate are applied to both Invoke and BeginInvoke methods. So are the parameter names. - if ((hasSymbolAttributeChange || oldParameter.Name != newParameter.Name) && - newParameter.ContainingType is INamedTypeSymbol { TypeKind: TypeKind.Delegate } newContainingDelegateType) + if (TypesEquivalent(oldType.EnumUnderlyingType, newType.EnumUnderlyingType, exact: false)) { - AddDelegateMethodEdit(semanticEdits, newContainingDelegateType, "Invoke", cancellationToken); - AddDelegateMethodEdit(semanticEdits, newContainingDelegateType, "BeginInvoke", cancellationToken); + hasGeneratedAttributeChange = true; } - } - - // Most symbol types will automatically have an edit added, so we just need to handle a few - if (hasReturnTypeAttributeChange && newSymbol is INamedTypeSymbol { TypeKind: TypeKind.Delegate } newDelegateType) - { - // attributes applied on return type of a delegate are applied to both Invoke and EndInvoke methods - AddDelegateMethodEdit(semanticEdits, newDelegateType, "Invoke", cancellationToken); - AddDelegateMethodEdit(semanticEdits, newDelegateType, "EndInvoke", cancellationToken); - } - - hasAttributeChange = hasSymbolAttributeChange || hasReturnTypeAttributeChange; - } - - /// - /// Semantic edits of members synthesized based on parameters that have no declaring syntax ( returns null) - /// and therefore not produced by - /// - private void AddSemanticEditsOriginatingFromParameterUpdate( - ArrayBuilder semanticEdits, - IParameterSymbol oldParameterSymbol, - IParameterSymbol newParameterSymbol, - Compilation newCompilation, - CancellationToken cancellationToken) - { - var oldContainingMember = oldParameterSymbol.ContainingSymbol; - var newContainingMember = newParameterSymbol.ContainingSymbol; - - if (oldContainingMember.ContainingType.IsRecord && - newContainingMember.ContainingType.IsRecord && - IsPrimaryConstructor(oldContainingMember, cancellationToken) is var oldIsPrimary && - IsPrimaryConstructor(newContainingMember, cancellationToken) is var newIsPrimary) - { - // both parameters are primary and differ in name or type - if (oldIsPrimary && newIsPrimary && (oldParameterSymbol.Name != newParameterSymbol.Name || !ParameterTypesEquivalent(oldParameterSymbol, newParameterSymbol, exact: false))) + else { - var oldPrimaryConstructor = (IMethodSymbol)oldContainingMember; - var newPrimaryConstructor = (IMethodSymbol)newContainingMember; - var containingSymbolKey = SymbolKey.Create(oldContainingMember.ContainingSymbol, cancellationToken); - - // Note: Edits for synthesized properties were already created by GetSymbolEdits. - - // add delete and insert edits of synthesized deconstructor: - var oldSynthesizedDeconstructor = oldPrimaryConstructor.GetMatchingDeconstructor(); - var newSynthesizedDeconstructor = newPrimaryConstructor.GetMatchingDeconstructor(); - Contract.ThrowIfNull(oldSynthesizedDeconstructor); - Contract.ThrowIfNull(newSynthesizedDeconstructor); - - AddMemberSignatureOrNameChangeEdits(semanticEdits, oldSynthesizedDeconstructor, newSynthesizedDeconstructor, containingSymbolKey, cancellationToken); - - // add updates of synthesized methods: - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newContainingMember.ContainingType, cancellationToken); + rudeEdit = RudeEditKind.EnumUnderlyingTypeUpdate; } } } - - private static void AddDelegateMethodEdit(ArrayBuilder semanticEdits, INamedTypeSymbol delegateType, string methodName, CancellationToken cancellationToken) + else if (!BaseTypesEquivalent(oldType, newType, exact: true)) { - var beginInvokeMethod = delegateType.GetMembers(methodName).FirstOrDefault(); - if (beginInvokeMethod != null) + if (BaseTypesEquivalent(oldType, newType, exact: false)) + { + hasGeneratedAttributeChange = true; + } + else { - semanticEdits.Add(SemanticEditInfo.CreateUpdate(SymbolKey.Create(beginInvokeMethod, cancellationToken), syntaxMaps: default, partialType: null)); + rudeEdit = RudeEditKind.BaseTypeOrInterfaceUpdate; } } + } - private void ReportCustomAttributeRudeEdits( - in DiagnosticContext diagnosticContext, - EditAndContinueCapabilitiesGrantor capabilities, - out bool hasAttributeChange, - out bool hasReturnTypeAttributeChange, - CancellationToken cancellationToken) + private static RudeEditKind GetSignatureChangeRudeEdit(ISymbol oldMember, ISymbol newMember, EditAndContinueCapabilitiesGrantor capabilities) + { + if (oldMember.Kind != newMember.Kind) { - var oldSymbol = diagnosticContext.RequiredOldSymbol; - var newSymbol = diagnosticContext.RequiredNewSymbol; + // rude edit will be reported later + return RudeEditKind.None; + } - // This is the only case we care about whether to issue an edit or not, because this is the only case where types have their attributes checked - // and types are the only things that would otherwise not have edits reported. - hasAttributeChange = ReportCustomAttributeRudeEdits(diagnosticContext, oldSymbol.GetAttributes(), newSymbol.GetAttributes(), capabilities, cancellationToken); + if (IsGlobalMain(oldMember)) + { + // Only return type can be changed: + Debug.Assert(ParameterTypesEquivalent(oldMember.GetParameters(), newMember.GetParameters(), exact: true)); - hasReturnTypeAttributeChange = false; + return RudeEditKind.ChangeImplicitMainReturnType; + } - if (oldSymbol is IMethodSymbol oldMethod && - newSymbol is IMethodSymbol newMethod) - { - hasReturnTypeAttributeChange |= ReportCustomAttributeRudeEdits(diagnosticContext, oldMethod.GetReturnTypeAttributes(), newMethod.GetReturnTypeAttributes(), capabilities, cancellationToken); - } - else if (oldSymbol is INamedTypeSymbol { DelegateInvokeMethod: not null and var oldInvokeMethod } && - newSymbol is INamedTypeSymbol { DelegateInvokeMethod: not null and var newInvokeMethod }) - { - hasReturnTypeAttributeChange |= ReportCustomAttributeRudeEdits(diagnosticContext, oldInvokeMethod.GetReturnTypeAttributes(), newInvokeMethod.GetReturnTypeAttributes(), capabilities, cancellationToken); - } + if (!AllowsDeletion(newMember)) + { + return RudeEditKind.TypeUpdate; } - private bool ReportCustomAttributeRudeEdits( - in DiagnosticContext diagnosticContext, - ImmutableArray? oldAttributes, - ImmutableArray newAttributes, - EditAndContinueCapabilitiesGrantor capabilities, - CancellationToken cancellationToken) + // Note: do not report a rude edit for property/event accessors as it will already be reported for the property/event itself. + if (!CanRenameOrChangeSignature(oldMember, newMember, capabilities) && + oldMember is not IMethodSymbol { AssociatedSymbol.Kind: SymbolKind.Property or SymbolKind.Event }) { - using var _ = ArrayBuilder.GetInstance(out var changedAttributes); + return RudeEditKind.ChangingSignatureNotSupportedByRuntime; + } - FindChangedAttributes(oldAttributes, newAttributes, changedAttributes); - if (oldAttributes.HasValue) - { - FindChangedAttributes(newAttributes, oldAttributes.Value, changedAttributes); - } + return RudeEditKind.None; + } - if (changedAttributes.Count == 0) + private static void AnalyzeTypeParameter(ITypeParameterSymbol oldParameter, ITypeParameterSymbol newParameter, ref RudeEditKind rudeEdit, ref bool hasGeneratedAttributeChange) + { + if (!TypeParameterConstraintsEquivalent(oldParameter, newParameter, exact: true)) + { + if (TypeParameterConstraintsEquivalent(oldParameter, newParameter, exact: false)) { - return false; + hasGeneratedAttributeChange = true; } - - // If the runtime doesn't support changing attributes we don't need to check anything else - if (!capabilities.Grant(EditAndContinueCapabilities.ChangeCustomAttributes)) + else { - diagnosticContext.Report(RudeEditKind.ChangingAttributesNotSupportedByRuntime, cancellationToken); - return false; + rudeEdit = (oldParameter.Variance != newParameter.Variance) ? RudeEditKind.VarianceUpdate : RudeEditKind.ChangingConstraints; } + } + } - var oldSymbol = diagnosticContext.RequiredOldSymbol; - - // Updating type parameter attributes is currently not supported. - if (oldSymbol is ITypeParameterSymbol) - { - var rudeEdit = oldSymbol.ContainingSymbol.Kind == SymbolKind.Method ? RudeEditKind.GenericMethodUpdate : RudeEditKind.GenericTypeUpdate; - diagnosticContext.Report(rudeEdit, cancellationToken); - return false; - } + private static bool IsExtensionMethodThisParameter(IParameterSymbol parameter) + => parameter is { Ordinal: 0, ContainingSymbol: IMethodSymbol { IsExtensionMethod: true } }; - // Even if the runtime supports attribute changes, only attributes stored in the CustomAttributes table are editable - foreach (var attributeData in changedAttributes) - { - if (IsNonCustomAttribute(attributeData)) - { - diagnosticContext.Report(RudeEditKind.ChangingNonCustomAttribute, cancellationToken, arguments: - [ - attributeData.AttributeClass!.Name, - GetDisplayKind(diagnosticContext.RequiredNewSymbol) - ]); + private void AnalyzeSymbolUpdate( + in DiagnosticContext diagnosticContext, + EditAndContinueCapabilitiesGrantor capabilities, + ArrayBuilder semanticEdits, + out bool hasAttributeChange, + CancellationToken cancellationToken) + { + // TODO: fails in VB on delegate parameter https://github.com/dotnet/roslyn/issues/53337 + // Contract.ThrowIfFalse(newSymbol.IsImplicitlyDeclared == newDeclaration is null); - return false; - } + ReportUpdatedSymbolDeclarationRudeEdits( + diagnosticContext, capabilities, out var hasGeneratedAttributeChange, out var hasGeneratedReturnTypeAttributeChange, cancellationToken); - if (attributeData.AttributeClass is - { - Name: "InlineArrayAttribute", - ContainingNamespace.Name: "CompilerServices", - ContainingNamespace.ContainingNamespace.Name: "Runtime", - ContainingNamespace.ContainingNamespace.ContainingNamespace.Name: "System", - ContainingNamespace.ContainingNamespace.ContainingNamespace.ContainingNamespace.IsGlobalNamespace: true - }) - { - diagnosticContext.Report(RudeEditKind.ChangingAttribute, cancellationToken, arguments: - [ - attributeData.AttributeClass.Name, - ]); + // We don't check capabilities of the runtime to update compiler generated attributes. + // All runtimes support changing the attributes in metadata, some just don't reflect the changes in the Reflection model. + // Having compiler-generated attributes visible via Reflaction API is not that important. + ReportCustomAttributeRudeEdits(diagnosticContext, capabilities, out var hasSymbolAttributeChange, out var hasReturnTypeAttributeChange, cancellationToken); + hasSymbolAttributeChange |= hasGeneratedAttributeChange; + hasReturnTypeAttributeChange |= hasGeneratedReturnTypeAttributeChange; - return false; - } - } + var oldSymbol = diagnosticContext.RequiredOldSymbol; + var newSymbol = diagnosticContext.RequiredNewSymbol; - return true; + if (oldSymbol is IParameterSymbol oldParameter && newSymbol is IParameterSymbol newParameter) + { + AddSemanticEditsOriginatingFromParameterUpdate(semanticEdits, oldParameter, newParameter, diagnosticContext.NewModel.Compilation, cancellationToken); - static void FindChangedAttributes(ImmutableArray? oldAttributes, ImmutableArray newAttributes, ArrayBuilder changedAttributes) + // Attributes applied on parameters of a delegate are applied to both Invoke and BeginInvoke methods. So are the parameter names. + if ((hasSymbolAttributeChange || oldParameter.Name != newParameter.Name) && + newParameter.ContainingType is INamedTypeSymbol { TypeKind: TypeKind.Delegate } newContainingDelegateType) { - for (var i = 0; i < newAttributes.Length; i++) - { - var newAttribute = newAttributes[i]; - var oldAttribute = FindMatch(newAttribute, oldAttributes); - - if (oldAttribute is null) - { - changedAttributes.Add(newAttribute); - } - } + AddDelegateMethodEdit(semanticEdits, newContainingDelegateType, "Invoke", cancellationToken); + AddDelegateMethodEdit(semanticEdits, newContainingDelegateType, "BeginInvoke", cancellationToken); } + } - static AttributeData? FindMatch(AttributeData attribute, ImmutableArray? oldAttributes) - { - if (!oldAttributes.HasValue) - { - return null; - } + // Most symbol types will automatically have an edit added, so we just need to handle a few + if (hasReturnTypeAttributeChange && newSymbol is INamedTypeSymbol { TypeKind: TypeKind.Delegate } newDelegateType) + { + // attributes applied on return type of a delegate are applied to both Invoke and EndInvoke methods + AddDelegateMethodEdit(semanticEdits, newDelegateType, "Invoke", cancellationToken); + AddDelegateMethodEdit(semanticEdits, newDelegateType, "EndInvoke", cancellationToken); + } - foreach (var match in oldAttributes.Value) - { - if (SymbolEquivalenceComparer.Instance.Equals(match.AttributeClass, attribute.AttributeClass)) - { - if (SymbolEquivalenceComparer.Instance.Equals(match.AttributeConstructor, attribute.AttributeConstructor) && - match.ConstructorArguments.SequenceEqual(attribute.ConstructorArguments, TypedConstantComparer.Instance) && - match.NamedArguments.SequenceEqual(attribute.NamedArguments, NamedArgumentComparer.Instance)) - { - return match; - } - } - } + hasAttributeChange = hasSymbolAttributeChange || hasReturnTypeAttributeChange; + } - return null; - } + /// + /// Semantic edits of members synthesized based on parameters that have no declaring syntax ( returns null) + /// and therefore not produced by + /// + private void AddSemanticEditsOriginatingFromParameterUpdate( + ArrayBuilder semanticEdits, + IParameterSymbol oldParameterSymbol, + IParameterSymbol newParameterSymbol, + Compilation newCompilation, + CancellationToken cancellationToken) + { + var oldContainingMember = oldParameterSymbol.ContainingSymbol; + var newContainingMember = newParameterSymbol.ContainingSymbol; - static bool IsNonCustomAttribute(AttributeData attribute) + if (oldContainingMember.ContainingType.IsRecord && + newContainingMember.ContainingType.IsRecord && + IsPrimaryConstructor(oldContainingMember, cancellationToken) is var oldIsPrimary && + IsPrimaryConstructor(newContainingMember, cancellationToken) is var newIsPrimary) + { + // both parameters are primary and differ in name or type + if (oldIsPrimary && newIsPrimary && (oldParameterSymbol.Name != newParameterSymbol.Name || !ParameterTypesEquivalent(oldParameterSymbol, newParameterSymbol, exact: false))) { - return attribute.AttributeClass?.ToNameDisplayString() switch - { - // - // This list comes from ShouldEmitAttribute in src\Compilers\CSharp\Portable\Symbols\Attributes\AttributeData.cs - // and src\Compilers\VisualBasic\Portable\Symbols\Attributes\AttributeData.vb - // TODO: Use a compiler API to get this information rather than hard coding a list: https://github.com/dotnet/roslyn/issues/53410 - // - "System.CLSCompliantAttribute" => true, - "System.Diagnostics.CodeAnalysis.AllowNullAttribute" => true, - "System.Diagnostics.CodeAnalysis.DisallowNullAttribute" => true, - "System.Diagnostics.CodeAnalysis.MaybeNullAttribute" => true, - "System.Diagnostics.CodeAnalysis.NotNullAttribute" => true, - "System.NonSerializedAttribute" => true, - "System.Reflection.AssemblyAlgorithmIdAttribute" => true, - "System.Reflection.AssemblyCultureAttribute" => true, - "System.Reflection.AssemblyFlagsAttribute" => true, - "System.Reflection.AssemblyVersionAttribute" => true, - "System.Runtime.CompilerServices.DllImportAttribute" => true, // Already covered by other rude edits, but included for completeness - "System.Runtime.CompilerServices.IndexerNameAttribute" => true, - "System.Runtime.CompilerServices.MethodImplAttribute" => true, - "System.Runtime.CompilerServices.SpecialNameAttribute" => true, - "System.Runtime.CompilerServices.TypeForwardedToAttribute" => true, - "System.Runtime.InteropServices.ComImportAttribute" => true, - "System.Runtime.InteropServices.DefaultParameterValueAttribute" => true, - "System.Runtime.InteropServices.FieldOffsetAttribute" => true, - "System.Runtime.InteropServices.InAttribute" => true, - "System.Runtime.InteropServices.MarshalAsAttribute" => true, - "System.Runtime.InteropServices.OptionalAttribute" => true, - "System.Runtime.InteropServices.OutAttribute" => true, - "System.Runtime.InteropServices.PreserveSigAttribute" => true, - "System.Runtime.InteropServices.StructLayoutAttribute" => true, - "System.Runtime.InteropServices.WindowsRuntime.WindowsRuntimeImportAttribute" => true, - "System.Security.DynamicSecurityMethodAttribute" => true, - "System.SerializableAttribute" => true, - - // - // This list is not from the compiler, but included explicitly for Edit and Continue purposes - // - - // Applying [AsyncMethodBuilder] changes the code that is emitted: - // * When the target is a method, for any await call to the method - // * When the target is a type, for any await call to a method that returns that type - // - // Therefore applying this attribute can cause unbounded changes to emitted code anywhere in a project - // which EnC wouldn't pick up, so we block it with a rude edit - "System.Runtime.CompilerServices.AsyncMethodBuilderAttribute" => true, - - // Also security attributes - not null => IsSecurityAttribute(attribute.AttributeClass), - _ => false - }; - } + var oldPrimaryConstructor = (IMethodSymbol)oldContainingMember; + var newPrimaryConstructor = (IMethodSymbol)newContainingMember; + var containingSymbolKey = SymbolKey.Create(oldContainingMember.ContainingSymbol, cancellationToken); - static bool IsSecurityAttribute(INamedTypeSymbol namedTypeSymbol) - { - // Security attributes are any attribute derived from System.Security.Permissions.SecurityAttribute, directly or indirectly + // Note: Edits for synthesized properties were already created by GetSymbolEdits. - var symbol = namedTypeSymbol; - while (symbol is not null) - { - if (symbol.ToNameDisplayString() == "System.Security.Permissions.SecurityAttribute") - { - return true; - } + // add delete and insert edits of synthesized deconstructor: + var oldSynthesizedDeconstructor = oldPrimaryConstructor.GetMatchingDeconstructor(); + var newSynthesizedDeconstructor = newPrimaryConstructor.GetMatchingDeconstructor(); + Contract.ThrowIfNull(oldSynthesizedDeconstructor); + Contract.ThrowIfNull(newSynthesizedDeconstructor); - symbol = symbol.BaseType; - } + AddMemberSignatureOrNameChangeEdits(semanticEdits, oldSynthesizedDeconstructor, newSynthesizedDeconstructor, containingSymbolKey, cancellationToken); - return false; + // add updates of synthesized methods: + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newContainingMember.ContainingType, cancellationToken); } } + } - /// - /// Check if the allow us to rename or change signature of a member. - /// Such edit translates to an addition of a new member, an update of any method bodies associated with the old one and marking the member as "deleted". - /// - private static bool CanRenameOrChangeSignature(ISymbol oldSymbol, ISymbol newSymbol, EditAndContinueCapabilitiesGrantor capabilities) - => CanAddNewMemberToExistingType(newSymbol, capabilities) && - CanUpdateMemberBody(oldSymbol, capabilities); - - private static bool CanAddNewMemberToExistingType(ISymbol newSymbol, EditAndContinueCapabilitiesGrantor capabilities) + private static void AddDelegateMethodEdit(ArrayBuilder semanticEdits, INamedTypeSymbol delegateType, string methodName, CancellationToken cancellationToken) + { + var beginInvokeMethod = delegateType.GetMembers(methodName).FirstOrDefault(); + if (beginInvokeMethod != null) { - var requiredCapabilities = EditAndContinueCapabilities.None; - - if (newSymbol is IMethodSymbol or IEventSymbol or IPropertySymbol) - { - requiredCapabilities |= GetRequiredAddMethodCapabilities(newSymbol); - } - - if (newSymbol is IFieldSymbol || newSymbol.IsAutoProperty()) - { - requiredCapabilities |= GetRequiredAddFieldCapabilities(newSymbol); - } - - return capabilities.Grant(requiredCapabilities); + semanticEdits.Add(SemanticEditInfo.CreateUpdate(SymbolKey.Create(beginInvokeMethod, cancellationToken), syntaxMaps: default, partialType: null)); } + } - private static EditAndContinueCapabilities GetRequiredAddMethodCapabilities(ISymbol symbol) - => EditAndContinueCapabilities.AddMethodToExistingType | - (InGenericContext(symbol) ? EditAndContinueCapabilities.GenericAddMethodToExistingType : 0); + private void ReportCustomAttributeRudeEdits( + in DiagnosticContext diagnosticContext, + EditAndContinueCapabilitiesGrantor capabilities, + out bool hasAttributeChange, + out bool hasReturnTypeAttributeChange, + CancellationToken cancellationToken) + { + var oldSymbol = diagnosticContext.RequiredOldSymbol; + var newSymbol = diagnosticContext.RequiredNewSymbol; - private static EditAndContinueCapabilities GetRequiredAddFieldCapabilities(ISymbol symbol) - => (symbol.IsStatic ? EditAndContinueCapabilities.AddStaticFieldToExistingType : EditAndContinueCapabilities.AddInstanceFieldToExistingType) | - (InGenericContext(symbol) ? EditAndContinueCapabilities.GenericAddFieldToExistingType : 0); + // This is the only case we care about whether to issue an edit or not, because this is the only case where types have their attributes checked + // and types are the only things that would otherwise not have edits reported. + hasAttributeChange = ReportCustomAttributeRudeEdits(diagnosticContext, oldSymbol.GetAttributes(), newSymbol.GetAttributes(), capabilities, cancellationToken); - private static bool CanUpdateMemberBody(ISymbol oldSymbol, EditAndContinueCapabilitiesGrantor capabilities) - { - // If the new member is generic and the old one isn't then the old one will be updated (via delete edit) and the new one inserted. - if (InGenericContext(oldSymbol)) - { - return capabilities.Grant(EditAndContinueCapabilities.GenericUpdateMethod); - } + hasReturnTypeAttributeChange = false; - return true; + if (oldSymbol is IMethodSymbol oldMethod && + newSymbol is IMethodSymbol newMethod) + { + hasReturnTypeAttributeChange |= ReportCustomAttributeRudeEdits(diagnosticContext, oldMethod.GetReturnTypeAttributes(), newMethod.GetReturnTypeAttributes(), capabilities, cancellationToken); } - - /// - /// Adds edits for synthesized record members. - /// - private static void AddSynthesizedRecordMethodUpdatesForPropertyChange( - ArrayBuilder semanticEdits, - Compilation compilation, - INamedTypeSymbol recordType, - CancellationToken cancellationToken) + else if (oldSymbol is INamedTypeSymbol { DelegateInvokeMethod: not null and var oldInvokeMethod } && + newSymbol is INamedTypeSymbol { DelegateInvokeMethod: not null and var newInvokeMethod }) { - Debug.Assert(recordType.IsRecord); + hasReturnTypeAttributeChange |= ReportCustomAttributeRudeEdits(diagnosticContext, oldInvokeMethod.GetReturnTypeAttributes(), newInvokeMethod.GetReturnTypeAttributes(), capabilities, cancellationToken); + } + } - foreach (var member in GetRecordUpdatedSynthesizedMethods(compilation, recordType)) - { - // We update all synthesized members regardless of whether the original change in the property actually changed them. - // We could avoid these updates if we check the details (e.g. name & type matching, etc.) + private bool ReportCustomAttributeRudeEdits( + in DiagnosticContext diagnosticContext, + ImmutableArray? oldAttributes, + ImmutableArray newAttributes, + EditAndContinueCapabilitiesGrantor capabilities, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var changedAttributes); - var symbolKey = SymbolKey.Create(member, cancellationToken); - semanticEdits.Add(SemanticEditInfo.CreateUpdate(symbolKey, syntaxMaps: default, partialType: null)); - } + FindChangedAttributes(oldAttributes, newAttributes, changedAttributes); + if (oldAttributes.HasValue) + { + FindChangedAttributes(newAttributes, oldAttributes.Value, changedAttributes); } - private static IEnumerable GetRecordUpdatedSynthesizedMethods(Compilation compilation, INamedTypeSymbol record) + if (changedAttributes.Count == 0) { - Debug.Assert(record.IsRecord); + return false; + } - // All methods that are updated have well known names, and calling GetMembers(string) is - // faster than enumerating. + // If the runtime doesn't support changing attributes we don't need to check anything else + if (!capabilities.Grant(EditAndContinueCapabilities.ChangeCustomAttributes)) + { + diagnosticContext.Report(RudeEditKind.ChangingAttributesNotSupportedByRuntime, cancellationToken); + return false; + } - // When a new field or property is added methods PrintMembers, Equals, GetHashCode and the copy-constructor may be updated. - // Some updates might not be strictly necessary but for simplicity we update all that are not explicitly declared. + var oldSymbol = diagnosticContext.RequiredOldSymbol; - // The primary constructor is deferred since it needs to be aggregated with initializer updates. + // Updating type parameter attributes is currently not supported. + if (oldSymbol is ITypeParameterSymbol) + { + var rudeEdit = oldSymbol.ContainingSymbol.Kind == SymbolKind.Method ? RudeEditKind.GenericMethodUpdate : RudeEditKind.GenericTypeUpdate; + diagnosticContext.Report(rudeEdit, cancellationToken); + return false; + } - var result = record.GetMembers(WellKnownMemberNames.PrintMembersMethodName) - .FirstOrDefault(static (m, compilation) => m is IMethodSymbol { IsImplicitlyDeclared: true } method && HasPrintMembersSignature(method, compilation), compilation); - if (result is not null) + // Even if the runtime supports attribute changes, only attributes stored in the CustomAttributes table are editable + foreach (var attributeData in changedAttributes) + { + if (IsNonCustomAttribute(attributeData)) { - yield return result; - } + diagnosticContext.Report(RudeEditKind.ChangingNonCustomAttribute, cancellationToken, arguments: + [ + attributeData.AttributeClass!.Name, + GetDisplayKind(diagnosticContext.RequiredNewSymbol) + ]); - result = record.GetMembers(WellKnownMemberNames.ObjectEquals) - .FirstOrDefault(static m => m is IMethodSymbol { IsImplicitlyDeclared: true } method && HasIEquatableEqualsSignature(method)); - if (result is not null) - { - yield return result; + return false; } - result = record.GetMembers(WellKnownMemberNames.ObjectGetHashCode) - .FirstOrDefault(static m => m is IMethodSymbol { IsImplicitlyDeclared: true } method && HasGetHashCodeSignature(method)); - if (result is not null) + if (attributeData.AttributeClass is + { + Name: "InlineArrayAttribute", + ContainingNamespace.Name: "CompilerServices", + ContainingNamespace.ContainingNamespace.Name: "Runtime", + ContainingNamespace.ContainingNamespace.ContainingNamespace.Name: "System", + ContainingNamespace.ContainingNamespace.ContainingNamespace.ContainingNamespace.IsGlobalNamespace: true + }) { - yield return result; - } + diagnosticContext.Report(RudeEditKind.ChangingAttribute, cancellationToken, arguments: + [ + attributeData.AttributeClass.Name, + ]); - // copy constructor - if (record.TypeKind == TypeKind.Class) - { - result = record.InstanceConstructors.SingleOrDefault(m => m.IsImplicitlyDeclared && m.IsCopyConstructor()); - if (result is not null) - { - yield return result; - } + return false; } } - internal readonly struct DiagnosticContext( - AbstractEditAndContinueAnalyzer analyzer, - ArrayBuilder diagnostics, - ISymbol? oldSymbol, - ISymbol? newSymbol, - SyntaxNode? newNode, - SemanticModel newModel, - Match? topMatch, - TextSpan diagnosticSpan) - { - public SemanticModel NewModel => newModel; - - public ISymbol? OldSymbol - => oldSymbol; + return true; - public ISymbol RequiredOldSymbol + static void FindChangedAttributes(ImmutableArray? oldAttributes, ImmutableArray newAttributes, ArrayBuilder changedAttributes) + { + for (var i = 0; i < newAttributes.Length; i++) { - get + var newAttribute = newAttributes[i]; + var oldAttribute = FindMatch(newAttribute, oldAttributes); + + if (oldAttribute is null) { - Contract.ThrowIfNull(oldSymbol); - return oldSymbol; + changedAttributes.Add(newAttribute); } } + } - public ISymbol RequiredNewSymbol + static AttributeData? FindMatch(AttributeData attribute, ImmutableArray? oldAttributes) + { + if (!oldAttributes.HasValue) { - get - { - Contract.ThrowIfNull(newSymbol); - return newSymbol; - } + return null; } - private SyntaxNode GetDiagnosticNode(out int distance, CancellationToken cancellationToken) + foreach (var match in oldAttributes.Value) { - distance = 0; - - if (newNode != null) - { - return newNode; - } - - var newDiagnosticSymbol = newSymbol; - if (newDiagnosticSymbol == null) + if (SymbolEquivalenceComparer.Instance.Equals(match.AttributeClass, attribute.AttributeClass)) { - Debug.Assert(oldSymbol != null); - - // try to resolve containing symbol: - newDiagnosticSymbol = TryGetNewContainer(oldSymbol, ref distance, cancellationToken); - - // try to map container syntax: - if (newDiagnosticSymbol == null && topMatch != null) + if (SymbolEquivalenceComparer.Instance.Equals(match.AttributeConstructor, attribute.AttributeConstructor) && + match.ConstructorArguments.SequenceEqual(attribute.ConstructorArguments, TypedConstantComparer.Instance) && + match.NamedArguments.SequenceEqual(attribute.NamedArguments, NamedArgumentComparer.Instance)) { - var oldContainerDeclaration = analyzer.GetSymbolDeclarationSyntax(oldSymbol.ContainingSymbol, topMatch.OldRoot.SyntaxTree, cancellationToken); - if (oldContainerDeclaration != null && - topMatch.TryGetNewNode(oldContainerDeclaration, out var newContainerDeclaration)) - { - return newContainerDeclaration; - } + return match; } } + } - while (newDiagnosticSymbol != null) - { - // TODO: condition !newDiagnosticSymbol.IsImplicitlyDeclared should not be needed https://github.com/dotnet/roslyn/issues/68510 - if (newDiagnosticSymbol.DeclaringSyntaxReferences.Length > 0 && !newDiagnosticSymbol.IsImplicitlyDeclared) - { - var node = analyzer.GetSymbolDeclarationSyntax(newDiagnosticSymbol, newModel.SyntaxTree, cancellationToken); - if (node != null) - { - return node; - } - } + return null; + } - if (newDiagnosticSymbol.Kind is not (SymbolKind.Parameter or SymbolKind.TypeParameter)) - { - distance++; - } + static bool IsNonCustomAttribute(AttributeData attribute) + { + return attribute.AttributeClass?.ToNameDisplayString() switch + { + // + // This list comes from ShouldEmitAttribute in src\Compilers\CSharp\Portable\Symbols\Attributes\AttributeData.cs + // and src\Compilers\VisualBasic\Portable\Symbols\Attributes\AttributeData.vb + // TODO: Use a compiler API to get this information rather than hard coding a list: https://github.com/dotnet/roslyn/issues/53410 + // + "System.CLSCompliantAttribute" => true, + "System.Diagnostics.CodeAnalysis.AllowNullAttribute" => true, + "System.Diagnostics.CodeAnalysis.DisallowNullAttribute" => true, + "System.Diagnostics.CodeAnalysis.MaybeNullAttribute" => true, + "System.Diagnostics.CodeAnalysis.NotNullAttribute" => true, + "System.NonSerializedAttribute" => true, + "System.Reflection.AssemblyAlgorithmIdAttribute" => true, + "System.Reflection.AssemblyCultureAttribute" => true, + "System.Reflection.AssemblyFlagsAttribute" => true, + "System.Reflection.AssemblyVersionAttribute" => true, + "System.Runtime.CompilerServices.DllImportAttribute" => true, // Already covered by other rude edits, but included for completeness + "System.Runtime.CompilerServices.IndexerNameAttribute" => true, + "System.Runtime.CompilerServices.MethodImplAttribute" => true, + "System.Runtime.CompilerServices.SpecialNameAttribute" => true, + "System.Runtime.CompilerServices.TypeForwardedToAttribute" => true, + "System.Runtime.InteropServices.ComImportAttribute" => true, + "System.Runtime.InteropServices.DefaultParameterValueAttribute" => true, + "System.Runtime.InteropServices.FieldOffsetAttribute" => true, + "System.Runtime.InteropServices.InAttribute" => true, + "System.Runtime.InteropServices.MarshalAsAttribute" => true, + "System.Runtime.InteropServices.OptionalAttribute" => true, + "System.Runtime.InteropServices.OutAttribute" => true, + "System.Runtime.InteropServices.PreserveSigAttribute" => true, + "System.Runtime.InteropServices.StructLayoutAttribute" => true, + "System.Runtime.InteropServices.WindowsRuntime.WindowsRuntimeImportAttribute" => true, + "System.Security.DynamicSecurityMethodAttribute" => true, + "System.SerializableAttribute" => true, - newDiagnosticSymbol = newDiagnosticSymbol.ContainingSymbol; - } + // + // This list is not from the compiler, but included explicitly for Edit and Continue purposes + // - return newModel.SyntaxTree.GetRoot(cancellationToken); - } + // Applying [AsyncMethodBuilder] changes the code that is emitted: + // * When the target is a method, for any await call to the method + // * When the target is a type, for any await call to a method that returns that type + // + // Therefore applying this attribute can cause unbounded changes to emitted code anywhere in a project + // which EnC wouldn't pick up, so we block it with a rude edit + "System.Runtime.CompilerServices.AsyncMethodBuilderAttribute" => true, - private ISymbol? TryGetNewContainer(ISymbol oldSymbol, ref int distance, CancellationToken cancellationToken) - { - var oldContainer = oldSymbol.ContainingSymbol; + // Also security attributes + not null => IsSecurityAttribute(attribute.AttributeClass), + _ => false + }; + } - if (oldSymbol.Kind is not (SymbolKind.Parameter or SymbolKind.TypeParameter)) - { - distance++; - } + static bool IsSecurityAttribute(INamedTypeSymbol namedTypeSymbol) + { + // Security attributes are any attribute derived from System.Security.Permissions.SecurityAttribute, directly or indirectly - while (oldContainer is not null and not INamespaceSymbol { IsGlobalNamespace: true }) + var symbol = namedTypeSymbol; + while (symbol is not null) + { + if (symbol.ToNameDisplayString() == "System.Security.Permissions.SecurityAttribute") { - var symbolKey = SymbolKey.Create(oldSymbol, cancellationToken); - if (symbolKey.Resolve(newModel.Compilation, ignoreAssemblyKey: true, cancellationToken).Symbol is { } newSymbol) - { - return newSymbol; - } - - oldContainer = oldContainer.ContainingSymbol; - distance++; + return true; } - return null; + symbol = symbol.BaseType; } - public void Report(RudeEditKind kind, TextSpan span) - => diagnostics.Add(new RudeEditDiagnostic(kind, span)); - - /// - /// Reports rude edit in the context of newDeclaration. - /// - /// If is in the same syntax tree as newDeclaration its span will be used for the location of the diagnostic, otherwise the diagnostic will be reported on the newDeclaration. - /// If is given it is used for the diagnostic arguments, otherwise the display name of the newDeclaration is passed as the single argument. - /// - /// The rude edit will be associated with the syntax kind of newDeclaration in telemetry. - /// - public void Report(RudeEditKind kind, SyntaxNode locationNode, CancellationToken cancellationToken, string?[]? arguments = null) - => Report( - kind, - cancellationToken, - span: (locationNode.SyntaxTree == newModel.SyntaxTree) ? - locationNode.Span : analyzer.GetDiagnosticSpan(GetDiagnosticNode(out var distance, cancellationToken), distance > 0 ? EditKind.Delete : EditKind.Update), - arguments); - - /// - /// Reports rude edit in the context of newDeclaration. - /// - /// If is given it will be used for the location of the diagnostic, otherwise the diagnostic will be reported on the newDeclaration. - /// If is given it is used for the diagnostic arguments, otherwise the display name of the newDeclaration is passed as the single argument. - /// - /// The rude edit will be associated with the syntax kind of newDeclaration in telemetry. - /// - public void Report(RudeEditKind kind, CancellationToken cancellationToken, TextSpan? span = null, string?[]? arguments = null) - { - var node = GetDiagnosticNode(out var distance, cancellationToken); - - span ??= diagnosticSpan.IsEmpty - ? analyzer.GetDiagnosticSpan(node, (distance > 0 || kind == RudeEditKind.ChangeImplicitMainReturnType) ? EditKind.Delete : EditKind.Update) - : diagnosticSpan; + return false; + } + } - diagnostics.Add(new RudeEditDiagnostic( - kind, - span.Value, - node, - arguments ?? kind switch - { - RudeEditKind.TypeKindUpdate or - RudeEditKind.ChangeImplicitMainReturnType or - RudeEditKind.GenericMethodUpdate or - RudeEditKind.GenericTypeUpdate or - RudeEditKind.SwitchBetweenLambdaAndLocalFunction or - RudeEditKind.AccessorKindUpdate or - RudeEditKind.InsertConstructorToTypeWithInitializersWithLambdas - => Array.Empty(), - - RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime - => new[] { CreateNewOnMetadataUpdateAttributeName }, - - RudeEditKind.Renamed - => new[] { analyzer.GetDisplayKindAndName(oldSymbol!, fullyQualify: false) }, - - _ => new[] - { - // Use name of oldSymbol, in case the symbol we are refering to has been renamed: - ((oldSymbol ?? newSymbol) is not { } symbol) - ? analyzer.GetDisplayName(node) - // Include member name if it is deleted or implicitly declared, otherwise it might not be obvious which member is being referred to. - : distance > 0 - ? analyzer.GetDisplayKindAndName(symbol, fullyQualify: distance > 1) - : analyzer.GetDisplayKind(symbol) - } - })); - } + /// + /// Check if the allow us to rename or change signature of a member. + /// Such edit translates to an addition of a new member, an update of any method bodies associated with the old one and marking the member as "deleted". + /// + private static bool CanRenameOrChangeSignature(ISymbol oldSymbol, ISymbol newSymbol, EditAndContinueCapabilitiesGrantor capabilities) + => CanAddNewMemberToExistingType(newSymbol, capabilities) && + CanUpdateMemberBody(oldSymbol, capabilities); - public void ReportTypeLayoutUpdateRudeEdits(CancellationToken cancellationToken) - { - Debug.Assert(newSymbol != null); + private static bool CanAddNewMemberToExistingType(ISymbol newSymbol, EditAndContinueCapabilitiesGrantor capabilities) + { + var requiredCapabilities = EditAndContinueCapabilities.None; - Report( - (newSymbol.ContainingType.TypeKind == TypeKind.Struct) ? RudeEditKind.InsertIntoStruct : RudeEditKind.InsertIntoClassWithLayout, - cancellationToken, - arguments: - [ - analyzer.GetDisplayKind(newSymbol), - analyzer.GetDisplayKind(newSymbol.ContainingType) - ]); - } + if (newSymbol is IMethodSymbol or IEventSymbol or IPropertySymbol) + { + requiredCapabilities |= GetRequiredAddMethodCapabilities(newSymbol); + } - public DiagnosticContext WithSymbols(ISymbol oldSymbol, ISymbol newSymbol) - => new(analyzer, diagnostics, oldSymbol, newSymbol, newNode, newModel, topMatch, diagnosticSpan); + if (newSymbol is IFieldSymbol || newSymbol.IsAutoProperty()) + { + requiredCapabilities |= GetRequiredAddFieldCapabilities(newSymbol); } - private DiagnosticContext CreateDiagnosticContext(ArrayBuilder diagnostics, ISymbol? oldSymbol, ISymbol? newSymbol, SyntaxNode? newNode, SemanticModel newModel, Match? topMatch, TextSpan diagnosticSpan = default) - => new(this, diagnostics, oldSymbol, newSymbol, newNode, newModel, topMatch, diagnosticSpan); + return capabilities.Grant(requiredCapabilities); + } + + private static EditAndContinueCapabilities GetRequiredAddMethodCapabilities(ISymbol symbol) + => EditAndContinueCapabilities.AddMethodToExistingType | + (InGenericContext(symbol) ? EditAndContinueCapabilities.GenericAddMethodToExistingType : 0); - #region Type Layout Update Validation + private static EditAndContinueCapabilities GetRequiredAddFieldCapabilities(ISymbol symbol) + => (symbol.IsStatic ? EditAndContinueCapabilities.AddStaticFieldToExistingType : EditAndContinueCapabilities.AddInstanceFieldToExistingType) | + (InGenericContext(symbol) ? EditAndContinueCapabilities.GenericAddFieldToExistingType : 0); - internal void ReportTypeLayoutUpdateRudeEdits(in DiagnosticContext diagnosticContext, ISymbol newSymbol, CancellationToken cancellationToken) + private static bool CanUpdateMemberBody(ISymbol oldSymbol, EditAndContinueCapabilitiesGrantor capabilities) + { + // If the new member is generic and the old one isn't then the old one will be updated (via delete edit) and the new one inserted. + if (InGenericContext(oldSymbol)) { - switch (newSymbol.Kind) - { - case SymbolKind.Field: - if (HasExplicitOrSequentialLayout(newSymbol.ContainingType, diagnosticContext.NewModel)) - { - diagnosticContext.ReportTypeLayoutUpdateRudeEdits(cancellationToken); - } + return capabilities.Grant(EditAndContinueCapabilities.GenericUpdateMethod); + } - break; + return true; + } - case SymbolKind.Property: - if (newSymbol.IsAutoProperty() && - HasExplicitOrSequentialLayout(newSymbol.ContainingType, diagnosticContext.NewModel)) - { - diagnosticContext.ReportTypeLayoutUpdateRudeEdits(cancellationToken); - } + /// + /// Adds edits for synthesized record members. + /// + private static void AddSynthesizedRecordMethodUpdatesForPropertyChange( + ArrayBuilder semanticEdits, + Compilation compilation, + INamedTypeSymbol recordType, + CancellationToken cancellationToken) + { + Debug.Assert(recordType.IsRecord); - break; + foreach (var member in GetRecordUpdatedSynthesizedMethods(compilation, recordType)) + { + // We update all synthesized members regardless of whether the original change in the property actually changed them. + // We could avoid these updates if we check the details (e.g. name & type matching, etc.) - case SymbolKind.Event: - if (HasBackingField((IEventSymbol)newSymbol) && - HasExplicitOrSequentialLayout(newSymbol.ContainingType, diagnosticContext.NewModel)) - { - diagnosticContext.ReportTypeLayoutUpdateRudeEdits(cancellationToken); - } + var symbolKey = SymbolKey.Create(member, cancellationToken); + semanticEdits.Add(SemanticEditInfo.CreateUpdate(symbolKey, syntaxMaps: default, partialType: null)); + } + } - break; + private static IEnumerable GetRecordUpdatedSynthesizedMethods(Compilation compilation, INamedTypeSymbol record) + { + Debug.Assert(record.IsRecord); - case SymbolKind.Parameter: - // parameter of a primary constructor that's lifted to a field - if (HasBackingField((IParameterSymbol)newSymbol, cancellationToken) && - HasExplicitOrSequentialLayout(newSymbol.ContainingType, diagnosticContext.NewModel)) - { - diagnosticContext.ReportTypeLayoutUpdateRudeEdits(cancellationToken); - } + // All methods that are updated have well known names, and calling GetMembers(string) is + // faster than enumerating. - break; - } + // When a new field or property is added methods PrintMembers, Equals, GetHashCode and the copy-constructor may be updated. + // Some updates might not be strictly necessary but for simplicity we update all that are not explicitly declared. + + // The primary constructor is deferred since it needs to be aggregated with initializer updates. + + var result = record.GetMembers(WellKnownMemberNames.PrintMembersMethodName) + .FirstOrDefault(static (m, compilation) => m is IMethodSymbol { IsImplicitlyDeclared: true } method && HasPrintMembersSignature(method, compilation), compilation); + if (result is not null) + { + yield return result; } - private bool HasBackingField(IParameterSymbol parameter, CancellationToken cancellationToken) - => IsPrimaryConstructor(parameter.ContainingSymbol, cancellationToken) && - parameter.ContainingType.GetMembers($"<{parameter.Name}>P").Any(m => m.Kind == SymbolKind.Field); + result = record.GetMembers(WellKnownMemberNames.ObjectEquals) + .FirstOrDefault(static m => m is IMethodSymbol { IsImplicitlyDeclared: true } method && HasIEquatableEqualsSignature(method)); + if (result is not null) + { + yield return result; + } - private static bool HasBackingField(IEventSymbol @event) + result = record.GetMembers(WellKnownMemberNames.ObjectGetHashCode) + .FirstOrDefault(static m => m is IMethodSymbol { IsImplicitlyDeclared: true } method && HasGetHashCodeSignature(method)); + if (result is not null) { -#nullable disable // https://github.com/dotnet/roslyn/issues/39288 - return @event.AddMethod.IsImplicitlyDeclared -#nullable enable - && !@event.IsAbstract; + yield return result; } - private static bool HasExplicitOrSequentialLayout(INamedTypeSymbol type, SemanticModel model) + // copy constructor + if (record.TypeKind == TypeKind.Class) { - if (type.TypeKind == TypeKind.Struct) + result = record.InstanceConstructors.SingleOrDefault(m => m.IsImplicitlyDeclared && m.IsCopyConstructor()); + if (result is not null) { - return true; + yield return result; } + } + } + + internal readonly struct DiagnosticContext( + AbstractEditAndContinueAnalyzer analyzer, + ArrayBuilder diagnostics, + ISymbol? oldSymbol, + ISymbol? newSymbol, + SyntaxNode? newNode, + SemanticModel newModel, + Match? topMatch, + TextSpan diagnosticSpan) + { + public SemanticModel NewModel => newModel; + + public ISymbol? OldSymbol + => oldSymbol; - if (type.TypeKind != TypeKind.Class) + public ISymbol RequiredOldSymbol + { + get { - return false; + Contract.ThrowIfNull(oldSymbol); + return oldSymbol; } + } - // Fields can't be inserted into a class with explicit or sequential layout - var attributes = type.GetAttributes(); - if (attributes.Length == 0) + public ISymbol RequiredNewSymbol + { + get { - return false; + Contract.ThrowIfNull(newSymbol); + return newSymbol; } + } - var layoutAttribute = model.Compilation.GetTypeByMetadataName(typeof(StructLayoutAttribute).FullName!); - if (layoutAttribute == null) + private SyntaxNode GetDiagnosticNode(out int distance, CancellationToken cancellationToken) + { + distance = 0; + + if (newNode != null) { - return false; + return newNode; } - foreach (var attribute in attributes) + var newDiagnosticSymbol = newSymbol; + if (newDiagnosticSymbol == null) { - RoslynDebug.Assert(attribute.AttributeClass is object); - if (attribute.AttributeClass.Equals(layoutAttribute) && attribute.ConstructorArguments.Length == 1) + Debug.Assert(oldSymbol != null); + + // try to resolve containing symbol: + newDiagnosticSymbol = TryGetNewContainer(oldSymbol, ref distance, cancellationToken); + + // try to map container syntax: + if (newDiagnosticSymbol == null && topMatch != null) { - return attribute.ConstructorArguments.Single().Value switch + var oldContainerDeclaration = analyzer.GetSymbolDeclarationSyntax(oldSymbol.ContainingSymbol, topMatch.OldRoot.SyntaxTree, cancellationToken); + if (oldContainerDeclaration != null && + topMatch.TryGetNewNode(oldContainerDeclaration, out var newContainerDeclaration)) { - int value => value != (int)LayoutKind.Auto, - short value => value != (short)LayoutKind.Auto, - _ => false - }; + return newContainerDeclaration; + } } } - return false; - } + while (newDiagnosticSymbol != null) + { + // TODO: condition !newDiagnosticSymbol.IsImplicitlyDeclared should not be needed https://github.com/dotnet/roslyn/issues/68510 + if (newDiagnosticSymbol.DeclaringSyntaxReferences.Length > 0 && !newDiagnosticSymbol.IsImplicitlyDeclared) + { + var node = analyzer.GetSymbolDeclarationSyntax(newDiagnosticSymbol, newModel.SyntaxTree, cancellationToken); + if (node != null) + { + return node; + } + } - #endregion + if (newDiagnosticSymbol.Kind is not (SymbolKind.Parameter or SymbolKind.TypeParameter)) + { + distance++; + } - private static Func CreateSyntaxMapForEquivalentNodes(MemberBody oldBody, MemberBody newBody) - { - var oldRootNodes = oldBody.RootNodes; - var newRootNodes = newBody.RootNodes; + newDiagnosticSymbol = newDiagnosticSymbol.ContainingSymbol; + } - return newNode => FindPartner(oldRootNodes, newRootNodes, newNode); + return newModel.SyntaxTree.GetRoot(cancellationToken); } - private static Func CreateSyntaxMap(DeclarationBodyMap bodyMap) + private ISymbol? TryGetNewContainer(ISymbol oldSymbol, ref int distance, CancellationToken cancellationToken) { - var reverseMatch = bodyMap.Reverse; - var additionalReverse = bodyMap.AdditionalReverseMapping; + var oldContainer = oldSymbol.ContainingSymbol; - return newNode => reverseMatch.TryGetValue(newNode, out var oldNode) || additionalReverse.TryGetValue(newNode, out oldNode) ? oldNode : null; - } + if (oldSymbol.Kind is not (SymbolKind.Parameter or SymbolKind.TypeParameter)) + { + distance++; + } - private SyntaxMaps CreateAggregateSyntaxMaps( - SyntaxTree newTree, - IReadOnlyDictionary reverseTopMatches, - IReadOnlyDictionary changedDeclarations) - { - return new( - newTree, - matchingNodes: newNode => + while (oldContainer is not null and not INamespaceSymbol { IsGlobalNamespace: true }) + { + var symbolKey = SymbolKey.Create(oldSymbol, cancellationToken); + if (symbolKey.Resolve(newModel.Compilation, ignoreAssemblyKey: true, cancellationToken).Symbol is { } newSymbol) { - // containing declaration - if (!TryFindMemberDeclaration(root: null, newNode, newNode.Span, out var newDeclarations)) - { - return null; - } + return newSymbol; + } - foreach (var newDeclaration in newDeclarations) - { - // The node is in a field, property or constructor declaration that has been changed: - if (changedDeclarations.TryGetValue(newDeclaration, out var nodeMaps)) - { - // If syntax map is not available the declaration was either - // 1) updated but is not active - // 2) inserted - return nodeMaps.MatchingNodes?.Invoke(newNode); - } + oldContainer = oldContainer.ContainingSymbol; + distance++; + } - // The node is in a declaration that hasn't been changed: - if (reverseTopMatches.TryGetValue(newDeclaration, out var oldDeclaration)) - { - var oldBody = TryGetDeclarationBody(oldDeclaration, symbol: null); - var newBody = TryGetDeclarationBody(newDeclaration, symbol: null); + return null; + } - // The declarations must have bodies since we found newNode in the newDeclaration's body - // and the new body can only differ from the old one in trivia. - Debug.Assert(oldBody != null); - Debug.Assert(newBody != null); + public void Report(RudeEditKind kind, TextSpan span) + => diagnostics.Add(new RudeEditDiagnostic(kind, span)); - return FindPartner(oldBody.RootNodes, newBody.RootNodes, newNode); - } - } + /// + /// Reports rude edit in the context of newDeclaration. + /// + /// If is in the same syntax tree as newDeclaration its span will be used for the location of the diagnostic, otherwise the diagnostic will be reported on the newDeclaration. + /// If is given it is used for the diagnostic arguments, otherwise the display name of the newDeclaration is passed as the single argument. + /// + /// The rude edit will be associated with the syntax kind of newDeclaration in telemetry. + /// + public void Report(RudeEditKind kind, SyntaxNode locationNode, CancellationToken cancellationToken, string?[]? arguments = null) + => Report( + kind, + cancellationToken, + span: (locationNode.SyntaxTree == newModel.SyntaxTree) ? + locationNode.Span : analyzer.GetDiagnosticSpan(GetDiagnosticNode(out var distance, cancellationToken), distance > 0 ? EditKind.Delete : EditKind.Update), + arguments); - return null; - }, - runtimeRudeEdits: newNode => - { - // containing declaration - if (!TryFindMemberDeclaration(root: null, newNode, newNode.Span, out var newDeclarations)) - { - return null; - } + /// + /// Reports rude edit in the context of newDeclaration. + /// + /// If is given it will be used for the location of the diagnostic, otherwise the diagnostic will be reported on the newDeclaration. + /// If is given it is used for the diagnostic arguments, otherwise the display name of the newDeclaration is passed as the single argument. + /// + /// The rude edit will be associated with the syntax kind of newDeclaration in telemetry. + /// + public void Report(RudeEditKind kind, CancellationToken cancellationToken, TextSpan? span = null, string?[]? arguments = null) + { + var node = GetDiagnosticNode(out var distance, cancellationToken); - foreach (var newDeclaration in newDeclarations) + span ??= diagnosticSpan.IsEmpty + ? analyzer.GetDiagnosticSpan(node, (distance > 0 || kind == RudeEditKind.ChangeImplicitMainReturnType) ? EditKind.Delete : EditKind.Update) + : diagnosticSpan; + + diagnostics.Add(new RudeEditDiagnostic( + kind, + span.Value, + node, + arguments ?? kind switch + { + RudeEditKind.TypeKindUpdate or + RudeEditKind.ChangeImplicitMainReturnType or + RudeEditKind.GenericMethodUpdate or + RudeEditKind.GenericTypeUpdate or + RudeEditKind.SwitchBetweenLambdaAndLocalFunction or + RudeEditKind.AccessorKindUpdate or + RudeEditKind.InsertConstructorToTypeWithInitializersWithLambdas + => Array.Empty(), + + RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime + => new[] { CreateNewOnMetadataUpdateAttributeName }, + + RudeEditKind.Renamed + => new[] { analyzer.GetDisplayKindAndName(oldSymbol!, fullyQualify: false) }, + + _ => new[] { - // The node is in a field, property or constructor declaration that has been changed. - // Lambdas in unchanged initializers (or the constructor) are also unchanged and hence won't have rude edits. - // They can't be affected by changes in other initializers (or the constructor). - if (changedDeclarations.TryGetValue(newDeclaration, out var nodeMaps)) - { - return nodeMaps.RuntimeRudeEdits?.Invoke(newNode); - } + // Use name of oldSymbol, in case the symbol we are refering to has been renamed: + ((oldSymbol ?? newSymbol) is not { } symbol) + ? analyzer.GetDisplayName(node) + // Include member name if it is deleted or implicitly declared, otherwise it might not be obvious which member is being referred to. + : distance > 0 + ? analyzer.GetDisplayKindAndName(symbol, fullyQualify: distance > 1) + : analyzer.GetDisplayKind(symbol) } + })); + } - return null; - }); + public void ReportTypeLayoutUpdateRudeEdits(CancellationToken cancellationToken) + { + Debug.Assert(newSymbol != null); + + Report( + (newSymbol.ContainingType.TypeKind == TypeKind.Struct) ? RudeEditKind.InsertIntoStruct : RudeEditKind.InsertIntoClassWithLayout, + cancellationToken, + arguments: + [ + analyzer.GetDisplayKind(newSymbol), + analyzer.GetDisplayKind(newSymbol.ContainingType) + ]); } - #region Constructors and Initializers + public DiagnosticContext WithSymbols(ISymbol oldSymbol, ISymbol newSymbol) + => new(analyzer, diagnostics, oldSymbol, newSymbol, newNode, newModel, topMatch, diagnosticSpan); + } - private void AddConstructorEdits( - IReadOnlyDictionary updatedTypes, - Match topMatch, - SemanticModel? oldModel, - Compilation oldCompilation, - SemanticModel newModel, - bool isStatic, - [Out] ArrayBuilder semanticEdits, - [Out] ArrayBuilder diagnostics, - CancellationToken cancellationToken) - { - var oldSyntaxTree = topMatch.OldRoot.SyntaxTree; - var newSyntaxTree = topMatch.NewRoot.SyntaxTree; + private DiagnosticContext CreateDiagnosticContext(ArrayBuilder diagnostics, ISymbol? oldSymbol, ISymbol? newSymbol, SyntaxNode? newNode, SemanticModel newModel, Match? topMatch, TextSpan diagnosticSpan = default) + => new(this, diagnostics, oldSymbol, newSymbol, newNode, newModel, topMatch, diagnosticSpan); - foreach (var (newType, updatesInCurrentDocument) in updatedTypes) - { - var oldType = updatesInCurrentDocument.OldType; + #region Type Layout Update Validation - var anyInitializerUpdatesInCurrentDocument = updatesInCurrentDocument.ChangedDeclarations.Keys.Any(IsDeclarationWithInitializer) || updatesInCurrentDocument.HasDeletedMemberInitializer; - var isPartialEdit = IsPartialTypeEdit(oldType, newType, oldSyntaxTree, newSyntaxTree); - var typeKey = SymbolKey.Create(newType, cancellationToken); - var partialType = isPartialEdit ? typeKey : (SymbolKey?)null; + internal void ReportTypeLayoutUpdateRudeEdits(in DiagnosticContext diagnosticContext, ISymbol newSymbol, CancellationToken cancellationToken) + { + switch (newSymbol.Kind) + { + case SymbolKind.Field: + if (HasExplicitOrSequentialLayout(newSymbol.ContainingType, diagnosticContext.NewModel)) + { + diagnosticContext.ReportTypeLayoutUpdateRudeEdits(cancellationToken); + } - // Create a syntax map that aggregates syntax maps of the constructor body and all initializers in this document. - // Use syntax maps stored in update.ChangedDeclarations and fallback to 1:1 map for unchanged members. - // - // This aggregated map will be combined with similar maps capturing members declared in partial type declarations - // located in other documents when the semantic edits are merged across all changed documents of the project. - // - // We will create an aggregate syntax map even in cases when we don't necessarily need it, - // for example if none of the edited declarations are active. It's ok to have a map that we don't need. - // This is simpler than detecting whether or not some of the initializers/constructors contain active statements. - var syntaxMaps = CreateAggregateSyntaxMaps(newSyntaxTree, topMatch.ReverseMatches, updatesInCurrentDocument.ChangedDeclarations); + break; - var memberInitializerContainingLambdaReported = false; + case SymbolKind.Property: + if (newSymbol.IsAutoProperty() && + HasExplicitOrSequentialLayout(newSymbol.ContainingType, diagnosticContext.NewModel)) + { + diagnosticContext.ReportTypeLayoutUpdateRudeEdits(cancellationToken); + } - // We might have already reported rude edits for initializers that have been updated. - // It would be possible to track those as well but not worth the added complexity. - var unsupportedOperationReported = false; + break; - foreach (var newCtor in isStatic ? newType.StaticConstructors : newType.InstanceConstructors) + case SymbolKind.Event: + if (HasBackingField((IEventSymbol)newSymbol) && + HasExplicitOrSequentialLayout(newSymbol.ContainingType, diagnosticContext.NewModel)) { - if (newType.TypeKind != oldType.TypeKind || oldType.IsRecord != newType.IsRecord) - { - // rude edit has been reported when changing type kinds - continue; - } + diagnosticContext.ReportTypeLayoutUpdateRudeEdits(cancellationToken); + } - // Constructor that doesn't contain initializers had a corresponding semantic edit produced previously - // or was not edited. In either case we should not produce a semantic edit for it. - if (!IsConstructorWithMemberInitializers(newCtor, cancellationToken)) - { - continue; - } + break; + + case SymbolKind.Parameter: + // parameter of a primary constructor that's lifted to a field + if (HasBackingField((IParameterSymbol)newSymbol, cancellationToken) && + HasExplicitOrSequentialLayout(newSymbol.ContainingType, diagnosticContext.NewModel)) + { + diagnosticContext.ReportTypeLayoutUpdateRudeEdits(cancellationToken); + } - var newCtorKey = SymbolKey.Create(newCtor, cancellationToken); + break; + } + } - SyntaxNode? oldDeclaration = null; - SyntaxNode? newDeclaration = null; - IMethodSymbol? oldCtor; - bool hasSignatureChanges; + private bool HasBackingField(IParameterSymbol parameter, CancellationToken cancellationToken) + => IsPrimaryConstructor(parameter.ContainingSymbol, cancellationToken) && + parameter.ContainingType.GetMembers($"<{parameter.Name}>P").Any(m => m.Kind == SymbolKind.Field); - if (!newCtor.IsImplicitlyDeclared) - { - // Constructors have to have a single declaration syntax, they can't be partial - newDeclaration = GetSymbolDeclarationSyntax(newCtor, cancellationToken); - - // If no initializer updates were made in the type we only need to produce semantic edits for constructors - // whose body has been updated, otherwise we need to produce edits for all constructors that include initializers. - // If changes were made to initializers or constructors of a partial type in another document they will be merged - // when aggregating semantic edits from all changed documents. Rude edits resulting from those changes, if any, will - // be reported in the document they were made in. - if (!anyInitializerUpdatesInCurrentDocument && !updatesInCurrentDocument.ChangedDeclarations.ContainsKey(newDeclaration)) - { - continue; - } + private static bool HasBackingField(IEventSymbol @event) + { +#nullable disable // https://github.com/dotnet/roslyn/issues/39288 + return @event.AddMethod.IsImplicitlyDeclared +#nullable enable + && !@event.IsAbstract; + } - // To avoid costly SymbolKey resolution we first try to match the constructor in the current document - // and special case parameter-less constructor. + private static bool HasExplicitOrSequentialLayout(INamedTypeSymbol type, SemanticModel model) + { + if (type.TypeKind == TypeKind.Struct) + { + return true; + } - if (topMatch.TryGetOldNode(newDeclaration, out oldDeclaration)) - { - Contract.ThrowIfNull(oldModel); + if (type.TypeKind != TypeKind.Class) + { + return false; + } - oldCtor = (IMethodSymbol)GetRequiredDeclaredSymbol(oldModel, oldDeclaration, cancellationToken); - Contract.ThrowIfFalse(oldCtor is { MethodKind: MethodKind.Constructor or MethodKind.StaticConstructor }); + // Fields can't be inserted into a class with explicit or sequential layout + var attributes = type.GetAttributes(); + if (attributes.Length == 0) + { + return false; + } - hasSignatureChanges = !MemberOrDelegateSignaturesEquivalent(oldCtor, newCtor, exact: false); - } - else if (newCtor.Parameters.Length == 0) - { - oldCtor = TryGetParameterlessConstructor(oldType, isStatic); - hasSignatureChanges = false; - } - else - { - var resolution = newCtorKey.Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken); + var layoutAttribute = model.Compilation.GetTypeByMetadataName(typeof(StructLayoutAttribute).FullName!); + if (layoutAttribute == null) + { + return false; + } - // There may be semantic errors in the compilation that result in multiple candidates. - // Pick the first candidate. + foreach (var attribute in attributes) + { + RoslynDebug.Assert(attribute.AttributeClass is object); + if (attribute.AttributeClass.Equals(layoutAttribute) && attribute.ConstructorArguments.Length == 1) + { + return attribute.ConstructorArguments.Single().Value switch + { + int value => value != (int)LayoutKind.Auto, + short value => value != (short)LayoutKind.Auto, + _ => false + }; + } + } - oldCtor = (IMethodSymbol?)resolution.Symbol; + return false; + } - // SymbolKey-resolved constructors have the same signatures. - Debug.Assert(oldCtor == null || MemberOrDelegateSignaturesEquivalent(oldCtor, newCtor, exact: false)); - hasSignatureChanges = false; - } - } - else - { - // New constructor contains initializers and is implicitly declared so it must be parameterless. - Debug.Assert(newCtor.Parameters.IsEmpty); + #endregion - oldCtor = TryGetParameterlessConstructor(oldType, isStatic); + private static Func CreateSyntaxMapForEquivalentNodes(MemberBody oldBody, MemberBody newBody) + { + var oldRootNodes = oldBody.RootNodes; + var newRootNodes = newBody.RootNodes; - // Skip update if both old and new are implicitly declared and no initializer updates were made in current document. - // If changes were made to initializers or constructors of a partial type in another document they will be merged - // when aggregating semantic edits from all changed documents. - if (!anyInitializerUpdatesInCurrentDocument && oldCtor is { IsImplicitlyDeclared: true }) - { - continue; - } + return newNode => FindPartner(oldRootNodes, newRootNodes, newNode); + } - hasSignatureChanges = false; - } + private static Func CreateSyntaxMap(DeclarationBodyMap bodyMap) + { + var reverseMatch = bodyMap.Reverse; + var additionalReverse = bodyMap.AdditionalReverseMapping; - var diagnosticContext = CreateDiagnosticContext(diagnostics, oldCtor, newCtor, newDeclaration, newModel, topMatch); + return newNode => reverseMatch.TryGetValue(newNode, out var oldNode) || additionalReverse.TryGetValue(newNode, out oldNode) ? oldNode : null; + } - // Report an error if the updated constructor's declaration is in the current document - // and its body edit is disallowed (e.g. the body itself or any member initializer contains stackalloc). - // If the declaration represents a primary constructor the body will be null. - if (newDeclaration?.SyntaxTree == newSyntaxTree) - { - unsupportedOperationReported |= - TryGetDeclarationBody(newDeclaration, newCtor) is { } newBody && ReportUnsupportedOperations(diagnosticContext, newBody, cancellationToken) || - oldDeclaration != null && TryGetDeclarationBody(oldDeclaration, oldCtor) is { } oldBody && ReportUnsupportedOperations(diagnosticContext, oldBody, cancellationToken); - } + private SyntaxMaps CreateAggregateSyntaxMaps( + SyntaxTree newTree, + IReadOnlyDictionary reverseTopMatches, + IReadOnlyDictionary changedDeclarations) + { + return new( + newTree, + matchingNodes: newNode => + { + // containing declaration + if (!TryFindMemberDeclaration(root: null, newNode, newNode.Span, out var newDeclarations)) + { + return null; + } - if (oldCtor != null) - { - // We don't need to check initializers of the new type since any change that would - // add stackalloc or other disallowed syntax would already be reported as rude edit. - unsupportedOperationReported |= AnyMemberInitializerBody( - oldType, - body => ReportUnsupportedOperations(diagnosticContext, body, cancellationToken), - isStatic, - cancellationToken); - - if (hasSignatureChanges) - { - // Even though we can't remap active statements between the deleted and inserted methods, - // we still need syntax map to map lambdas. - AddMemberSignatureOrNameChangeEdits(semanticEdits, oldCtor, newCtor, typeKey, cancellationToken); - } - else - { - semanticEdits.Add(SemanticEditInfo.CreateUpdate(newCtorKey, syntaxMaps, partialType)); - } - } - else + foreach (var newDeclaration in newDeclarations) + { + // The node is in a field, property or constructor declaration that has been changed: + if (changedDeclarations.TryGetValue(newDeclaration, out var nodeMaps)) { - if (!memberInitializerContainingLambdaReported && - AnyMemberInitializerBody(oldType, ContainsLambda, isStatic, cancellationToken)) - { - // TODO (bug https://github.com/dotnet/roslyn/issues/2504) - // rude edit: Adding a constructor to a type with a field or property initializer that contains an anonymous function - diagnosticContext.Report(RudeEditKind.InsertConstructorToTypeWithInitializersWithLambdas, cancellationToken); - memberInitializerContainingLambdaReported = true; - continue; - } - - semanticEdits.Add(SemanticEditInfo.CreateInsert(newCtorKey, partialType)); + // If syntax map is not available the declaration was either + // 1) updated but is not active + // 2) inserted + return nodeMaps.MatchingNodes?.Invoke(newNode); } - // primary record constructor updated to non-primary and vice versa: - if (newType.IsRecord) + // The node is in a declaration that hasn't been changed: + if (reverseTopMatches.TryGetValue(newDeclaration, out var oldDeclaration)) { - var oldCtorIsPrimary = oldCtor != null && IsPrimaryConstructor(oldCtor, cancellationToken); - var newCtorIsPrimary = IsPrimaryConstructor(newCtor, cancellationToken); + var oldBody = TryGetDeclarationBody(oldDeclaration, symbol: null); + var newBody = TryGetDeclarationBody(newDeclaration, symbol: null); - if (hasSignatureChanges && oldCtorIsPrimary && newCtorIsPrimary || - oldCtorIsPrimary != newCtorIsPrimary) - { - // Deconstructor: - AddDeconstructorEdits(semanticEdits, oldCtor, newCtor, typeKey, oldCompilation, newModel.Compilation, isParameterDelete: newCtorIsPrimary, cancellationToken); + // The declarations must have bodies since we found newNode in the newDeclaration's body + // and the new body can only differ from the old one in trivia. + Debug.Assert(oldBody != null); + Debug.Assert(newBody != null); - // Synthesized method updates: - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newCtor.ContainingType, cancellationToken); - } + return FindPartner(oldBody.RootNodes, newBody.RootNodes, newNode); } } - if (!isStatic && oldType.TypeKind == TypeKind.Class && newType.TypeKind == TypeKind.Class) + return null; + }, + runtimeRudeEdits: newNode => + { + // containing declaration + if (!TryFindMemberDeclaration(root: null, newNode, newNode.Span, out var newDeclarations)) { - // Adding the first instance constructor with parameters suppresses synthesized default constructor. - if (oldType.HasSynthesizedDefaultConstructor() && !newType.HasSynthesizedDefaultConstructor()) - { - semanticEdits.Add(SemanticEditInfo.CreateDelete( - SymbolKey.Create(oldType.InstanceConstructors.Single(c => c.Parameters is []), cancellationToken), - deletedSymbolContainer: typeKey, - partialType)); - } - - // Removing the last instance constructor with parameters inserts synthesized default constructor. - // We don't need to add an explicit semantic edit for synthesized members though since the compiler - // emits them automatically. + return null; } - } - } - - /// - /// Return true if is true for a body of any instance/static member of that has an initializer. - /// - private bool AnyMemberInitializerBody(INamedTypeSymbol type, Func predicate, bool isStatic, CancellationToken cancellationToken) - { - // checking the old type for existing lambdas (it's ok for the new initializers to contain lambdas) - foreach (var member in type.GetMembers()) - { - if (member.IsStatic == isStatic && - member.Kind is SymbolKind.Field or SymbolKind.Property && - member.DeclaringSyntaxReferences.Length > 0) // skip generated fields (e.g. VB auto-property backing fields) + foreach (var newDeclaration in newDeclarations) { - var syntax = GetSymbolDeclarationSyntax(member, cancellationToken); - if (IsDeclarationWithInitializer(syntax) && TryGetDeclarationBody(syntax, member) is { } body && predicate(body)) + // The node is in a field, property or constructor declaration that has been changed. + // Lambdas in unchanged initializers (or the constructor) are also unchanged and hence won't have rude edits. + // They can't be affected by changes in other initializers (or the constructor). + if (changedDeclarations.TryGetValue(newDeclaration, out var nodeMaps)) { - return true; + return nodeMaps.RuntimeRudeEdits?.Invoke(newNode); } } - } - return false; - } + return null; + }); + } - private static IMethodSymbol? TryGetParameterlessConstructor(INamedTypeSymbol type, bool isStatic) - { - var oldCtors = isStatic ? type.StaticConstructors : type.InstanceConstructors; - if (isStatic) - { - return type.StaticConstructors.FirstOrDefault(); - } - else - { - return type.InstanceConstructors.FirstOrDefault(m => m.Parameters.Length == 0); - } - } + #region Constructors and Initializers + + private void AddConstructorEdits( + IReadOnlyDictionary updatedTypes, + Match topMatch, + SemanticModel? oldModel, + Compilation oldCompilation, + SemanticModel newModel, + bool isStatic, + [Out] ArrayBuilder semanticEdits, + [Out] ArrayBuilder diagnostics, + CancellationToken cancellationToken) + { + var oldSyntaxTree = topMatch.OldRoot.SyntaxTree; + var newSyntaxTree = topMatch.NewRoot.SyntaxTree; - private static bool IsPartialTypeEdit(ISymbol? oldSymbol, ISymbol? newSymbol, SyntaxTree oldSyntaxTree, SyntaxTree newSyntaxTree) + foreach (var (newType, updatesInCurrentDocument) in updatedTypes) { - // If any of the partial declarations of the new or the old type are in another document - // the edit will need to be merged with other partial edits with matching partial type - static bool IsNotInDocument(SyntaxReference reference, SyntaxTree syntaxTree) - => reference.SyntaxTree != syntaxTree; + var oldType = updatesInCurrentDocument.OldType; - static bool IsPartialTypeEdit(ISymbol? symbol, SyntaxTree tree) - => symbol is INamedTypeSymbol && - symbol.DeclaringSyntaxReferences.Length > 1 && symbol.DeclaringSyntaxReferences.Any(IsNotInDocument, tree); - - return IsPartialTypeEdit(oldSymbol, oldSyntaxTree) || - IsPartialTypeEdit(newSymbol, newSyntaxTree); - } + var anyInitializerUpdatesInCurrentDocument = updatesInCurrentDocument.ChangedDeclarations.Keys.Any(IsDeclarationWithInitializer) || updatesInCurrentDocument.HasDeletedMemberInitializer; + var isPartialEdit = IsPartialTypeEdit(oldType, newType, oldSyntaxTree, newSyntaxTree); + var typeKey = SymbolKey.Create(newType, cancellationToken); + var partialType = isPartialEdit ? typeKey : (SymbolKey?)null; - #endregion + // Create a syntax map that aggregates syntax maps of the constructor body and all initializers in this document. + // Use syntax maps stored in update.ChangedDeclarations and fallback to 1:1 map for unchanged members. + // + // This aggregated map will be combined with similar maps capturing members declared in partial type declarations + // located in other documents when the semantic edits are merged across all changed documents of the project. + // + // We will create an aggregate syntax map even in cases when we don't necessarily need it, + // for example if none of the edited declarations are active. It's ok to have a map that we don't need. + // This is simpler than detecting whether or not some of the initializers/constructors contain active statements. + var syntaxMaps = CreateAggregateSyntaxMaps(newSyntaxTree, topMatch.ReverseMatches, updatesInCurrentDocument.ChangedDeclarations); - #region Lambdas and Closures + var memberInitializerContainingLambdaReported = false; - private void ReportLambdaAndClosureRudeEdits( - SemanticModel? oldModel, - ISymbol oldMember, - MemberBody? oldMemberBody, - SyntaxNode? oldDeclaration, - SemanticModel newModel, - ISymbol newMember, - MemberBody? newMemberBody, - SyntaxNode? newDeclaration, - IReadOnlyDictionary? activeOrMatchedLambdas, - DeclarationBodyMap bodyMap, - EditAndContinueCapabilitiesGrantor capabilities, - ArrayBuilder diagnostics, - out bool syntaxMapRequired, - out Func? runtimeRudeEdits, - CancellationToken cancellationToken) - { - syntaxMapRequired = false; - runtimeRudeEdits = null; + // We might have already reported rude edits for initializers that have been updated. + // It would be possible to track those as well but not worth the added complexity. + var unsupportedOperationReported = false; - if (activeOrMatchedLambdas != null) + foreach (var newCtor in isStatic ? newType.StaticConstructors : newType.InstanceConstructors) { - var anySignatureErrors = false; - var hasUnmatchedLambdas = false; - foreach (var (oldLambdaBody, newLambdaInfo) in activeOrMatchedLambdas) + if (newType.TypeKind != oldType.TypeKind || oldType.IsRecord != newType.IsRecord) { - var newLambdaBody = newLambdaInfo.NewBody; - if (newLambdaBody == null) - { - hasUnmatchedLambdas = true; - continue; - } - - var lambdaBodyMap = newLambdaInfo.BodyMap; + // rude edit has been reported when changing type kinds + continue; + } - Debug.Assert(oldModel != null); + // Constructor that doesn't contain initializers had a corresponding semantic edit produced previously + // or was not edited. In either case we should not produce a semantic edit for it. + if (!IsConstructorWithMemberInitializers(newCtor, cancellationToken)) + { + continue; + } - var oldLambda = oldLambdaBody.GetLambda(); - var newLambda = newLambdaBody.GetLambda(); + var newCtorKey = SymbolKey.Create(newCtor, cancellationToken); - Debug.Assert(IsNestedFunction(newLambda) == IsNestedFunction(oldLambda)); - var isNestedFunction = IsNestedFunction(newLambda); + SyntaxNode? oldDeclaration = null; + SyntaxNode? newDeclaration = null; + IMethodSymbol? oldCtor; + bool hasSignatureChanges; - var oldLambdaSymbol = isNestedFunction ? GetLambdaExpressionSymbol(oldModel, oldLambda, cancellationToken) : null; - var newLambdaSymbol = isNestedFunction ? GetLambdaExpressionSymbol(newModel, newLambda, cancellationToken) : null; + if (!newCtor.IsImplicitlyDeclared) + { + // Constructors have to have a single declaration syntax, they can't be partial + newDeclaration = GetSymbolDeclarationSyntax(newCtor, cancellationToken); - var diagnosticContext = CreateDiagnosticContext(diagnostics, oldLambdaSymbol, newLambdaSymbol, newLambda, newModel, topMatch: null); + // If no initializer updates were made in the type we only need to produce semantic edits for constructors + // whose body has been updated, otherwise we need to produce edits for all constructors that include initializers. + // If changes were made to initializers or constructors of a partial type in another document they will be merged + // when aggregating semantic edits from all changed documents. Rude edits resulting from those changes, if any, will + // be reported in the document they were made in. + if (!anyInitializerUpdatesInCurrentDocument && !updatesInCurrentDocument.ChangedDeclarations.ContainsKey(newDeclaration)) + { + continue; + } - var oldStateMachineInfo = oldLambdaBody.GetStateMachineInfo(); - var newStateMachineInfo = newLambdaBody.GetStateMachineInfo(); + // To avoid costly SymbolKey resolution we first try to match the constructor in the current document + // and special case parameter-less constructor. - ReportStateMachineBodyUpdateRudeEdits(diagnosticContext, lambdaBodyMap, oldStateMachineInfo, newStateMachineInfo, newLambdaInfo.HasActiveStatement, cancellationToken); + if (topMatch.TryGetOldNode(newDeclaration, out oldDeclaration)) + { + Contract.ThrowIfNull(oldModel); - // When the delta IL of the containing method is emitted lambdas declared in it are also emitted. - // If the runtime does not support changing IL of the method (e.g. method containing stackalloc) - // we need to report a rude edit. - // If only trivia change the IL is going to be unchanged and only sequence points in the PDB change, - // so we do not report rude edits. + oldCtor = (IMethodSymbol)GetRequiredDeclaredSymbol(oldModel, oldDeclaration, cancellationToken); + Contract.ThrowIfFalse(oldCtor is { MethodKind: MethodKind.Constructor or MethodKind.StaticConstructor }); - if (!oldLambdaBody.IsSyntaxEquivalentTo(newLambdaBody)) - { - ReportMemberOrLambdaBodyUpdateRudeEdits( - diagnosticContext, - oldModel.Compilation, - oldLambda, - oldMember, - oldMemberBody, - oldLambdaBody, - newLambda, - newMember, - newMemberBody, - newLambdaBody, - capabilities, - oldStateMachineInfo, - newStateMachineInfo, - cancellationToken); - - if ((IsGenericLocalFunction(oldLambda) || IsGenericLocalFunction(newLambda)) && - !capabilities.Grant(EditAndContinueCapabilities.GenericUpdateMethod)) - { - diagnosticContext.Report(RudeEditKind.UpdatingGenericNotSupportedByRuntime, cancellationToken); - } + hasSignatureChanges = !MemberOrDelegateSignaturesEquivalent(oldCtor, newCtor, exact: false); } - - // query signatures are analyzed separately: - if (isNestedFunction) + else if (newCtor.Parameters.Length == 0) { - ReportLambdaSignatureRudeEdits(diagnosticContext, oldLambda, newLambda, capabilities, out var hasErrors, cancellationToken); - anySignatureErrors |= hasErrors; + oldCtor = TryGetParameterlessConstructor(oldType, isStatic); + hasSignatureChanges = false; } - } - - // Any unmatched lambdas would have contained an active statement and a rude edit would be reported in syntax analysis phase. - // Skip the rest of lambda and closure analysis if such lambdas are present. - if (hasUnmatchedLambdas) - { - return; - } + else + { + var resolution = newCtorKey.Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken); - ArrayBuilder? lazyNewErroneousClauses = null; - foreach (var (oldQueryClause, newQueryClause) in bodyMap.Forward) - { - Debug.Assert(oldModel != null); + // There may be semantic errors in the compilation that result in multiple candidates. + // Pick the first candidate. - if (!QueryClauseLambdasTypeEquivalent(oldModel, oldQueryClause, newModel, newQueryClause, cancellationToken)) - { - lazyNewErroneousClauses ??= ArrayBuilder.GetInstance(); - lazyNewErroneousClauses.Add(newQueryClause); - } - } + oldCtor = (IMethodSymbol?)resolution.Symbol; - if (lazyNewErroneousClauses != null) - { - foreach (var newQueryClause in from clause in lazyNewErroneousClauses - orderby clause.SpanStart - group clause by GetContainingQueryExpression(clause) into clausesByQuery - select clausesByQuery.First()) - { - var diagnosticContext = CreateDiagnosticContext(diagnostics, oldSymbol: null, newSymbol: null, newQueryClause, newModel, topMatch: null); - diagnosticContext.Report(RudeEditKind.ChangingQueryLambdaType, cancellationToken); + // SymbolKey-resolved constructors have the same signatures. + Debug.Assert(oldCtor == null || MemberOrDelegateSignaturesEquivalent(oldCtor, newCtor, exact: false)); + hasSignatureChanges = false; } - - lazyNewErroneousClauses.Free(); - anySignatureErrors = true; } - - // only dig into captures if lambda signatures match - if (anySignatureErrors) + else { - return; - } - } - - var oldPrimaryConstructor = oldDeclaration != null && IsPrimaryConstructorDeclaration(oldDeclaration) - ? (IMethodSymbol)oldMember - : GetPrimaryConstructor(oldMember.ContainingType, cancellationToken); - - var newPrimaryConstructor = newDeclaration != null && IsPrimaryConstructorDeclaration(newDeclaration) - ? (IMethodSymbol)newMember - : GetPrimaryConstructor(newMember.ContainingType, cancellationToken); - - // If type layout is changed another rude edit is reported, so we can assume the layouts match. - // We don't need to analyze primary parameter captures unless type layout disallows captures. - var typeLayoutDisallowsNewCaptures = - (newPrimaryConstructor != null || oldPrimaryConstructor != null) && HasExplicitOrSequentialLayout(newMember.ContainingType, newModel); - - // The primary constructor if its parameters are lifted into fields when accessed from this member, otherwise null. - var oldLiftingPrimaryConstructor = oldMember != oldPrimaryConstructor && oldDeclaration != null && !IsDeclarationWithInitializer(oldDeclaration) ? oldPrimaryConstructor : null; - var newLiftingPrimaryConstructor = newMember != newPrimaryConstructor && newDeclaration != null && !IsDeclarationWithInitializer(newDeclaration) ? newPrimaryConstructor : null; - - GetCapturedVariables( - oldMemberBody, - oldModel, - oldLiftingPrimaryConstructor, - ignorePrimaryParameterCaptures: !typeLayoutDisallowsNewCaptures, - out var oldHasLambdasOrLocalFunctions, - out var oldInLambdaCaptures, - out var oldPrimaryCaptures); - - GetCapturedVariables( - newMemberBody, - newModel, - newLiftingPrimaryConstructor, - ignorePrimaryParameterCaptures: !typeLayoutDisallowsNewCaptures, - out var newHasLambdasOrLocalFunctions, - out var newInLambdaCaptures, - out var newPrimaryCaptures); + // New constructor contains initializers and is implicitly declared so it must be parameterless. + Debug.Assert(newCtor.Parameters.IsEmpty); - // Analyze primary parameter captures: - - ReportPrimaryParameterCaptureRudeEdits( - diagnostics, - oldLiftingPrimaryConstructor, - oldPrimaryCaptures, - newLiftingPrimaryConstructor, - newPrimaryCaptures, - newMember, - cancellationToken); + oldCtor = TryGetParameterlessConstructor(oldType, isStatic); - // Analyze captures in lambda bodies: + // Skip update if both old and new are implicitly declared and no initializer updates were made in current document. + // If changes were made to initializers or constructors of a partial type in another document they will be merged + // when aggregating semantic edits from all changed documents. + if (!anyInitializerUpdatesInCurrentDocument && oldCtor is { IsImplicitlyDeclared: true }) + { + continue; + } - if (!oldHasLambdasOrLocalFunctions && !newHasLambdasOrLocalFunctions) - { - return; - } + hasSignatureChanges = false; + } - syntaxMapRequired = newHasLambdasOrLocalFunctions; + var diagnosticContext = CreateDiagnosticContext(diagnostics, oldCtor, newCtor, newDeclaration, newModel, topMatch); - // { new capture index -> old capture index } - using var _1 = ArrayBuilder.GetInstance(newInLambdaCaptures.Length, fillWithValue: 0, out var reverseCapturesMap); + // Report an error if the updated constructor's declaration is in the current document + // and its body edit is disallowed (e.g. the body itself or any member initializer contains stackalloc). + // If the declaration represents a primary constructor the body will be null. + if (newDeclaration?.SyntaxTree == newSyntaxTree) + { + unsupportedOperationReported |= + TryGetDeclarationBody(newDeclaration, newCtor) is { } newBody && ReportUnsupportedOperations(diagnosticContext, newBody, cancellationToken) || + oldDeclaration != null && TryGetDeclarationBody(oldDeclaration, oldCtor) is { } oldBody && ReportUnsupportedOperations(diagnosticContext, oldBody, cancellationToken); + } - // { new capture index -> new closure scope or null for "this" } - using var _2 = ArrayBuilder.GetInstance(newInLambdaCaptures.Length, fillWithValue: null, out var newCapturesToClosureScopes); + if (oldCtor != null) + { + // We don't need to check initializers of the new type since any change that would + // add stackalloc or other disallowed syntax would already be reported as rude edit. + unsupportedOperationReported |= AnyMemberInitializerBody( + oldType, + body => ReportUnsupportedOperations(diagnosticContext, body, cancellationToken), + isStatic, + cancellationToken); - // Can be calculated from other maps but it's simpler to just calculate it upfront. - // { old capture index -> old closure scope or null for "this" } - using var _3 = ArrayBuilder.GetInstance(oldInLambdaCaptures.Length, fillWithValue: null, out var oldCapturesToClosureScopes); + if (hasSignatureChanges) + { + // Even though we can't remap active statements between the deleted and inserted methods, + // we still need syntax map to map lambdas. + AddMemberSignatureOrNameChangeEdits(semanticEdits, oldCtor, newCtor, typeKey, cancellationToken); + } + else + { + semanticEdits.Add(SemanticEditInfo.CreateUpdate(newCtorKey, syntaxMaps, partialType)); + } + } + else + { + if (!memberInitializerContainingLambdaReported && + AnyMemberInitializerBody(oldType, ContainsLambda, isStatic, cancellationToken)) + { + // TODO (bug https://github.com/dotnet/roslyn/issues/2504) + // rude edit: Adding a constructor to a type with a field or property initializer that contains an anonymous function + diagnosticContext.Report(RudeEditKind.InsertConstructorToTypeWithInitializersWithLambdas, cancellationToken); + memberInitializerContainingLambdaReported = true; + continue; + } - using var _4 = PooledDictionary.GetInstance(out var closureRudeEdits); + semanticEdits.Add(SemanticEditInfo.CreateInsert(newCtorKey, partialType)); + } - CalculateCapturedVariablesMaps( - oldInLambdaCaptures, - oldDeclaration, - oldPrimaryConstructor, - newInLambdaCaptures, - newDeclaration, - newPrimaryConstructor, - bodyMap, - reverseCapturesMap, - newCapturesToClosureScopes, - oldCapturesToClosureScopes, - closureRudeEdits, - cancellationToken); + // primary record constructor updated to non-primary and vice versa: + if (newType.IsRecord) + { + var oldCtorIsPrimary = oldCtor != null && IsPrimaryConstructor(oldCtor, cancellationToken); + var newCtorIsPrimary = IsPrimaryConstructor(newCtor, cancellationToken); - if (closureRudeEdits.Any()) - { - var rudeEdits = closureRudeEdits.ToImmutableSegmentedDictionary( - static item => item.Key, - static item => new RuntimeRudeEdit(item.Value.ToDiagnostic(item.Key.SyntaxTree).ToString())); + if (hasSignatureChanges && oldCtorIsPrimary && newCtorIsPrimary || + oldCtorIsPrimary != newCtorIsPrimary) + { + // Deconstructor: + AddDeconstructorEdits(semanticEdits, oldCtor, newCtor, typeKey, oldCompilation, newModel.Compilation, isParameterDelete: newCtorIsPrimary, cancellationToken); - runtimeRudeEdits = node => rudeEdits.TryGetValue(node, out var message) ? message : null; - return; + // Synthesized method updates: + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newCtor.ContainingType, cancellationToken); + } + } } - using var _5 = PooledDictionary.GetInstance(out var newCapturesIndex); - BuildIndex(newCapturesIndex, newInLambdaCaptures); - - var oldHasLambdas = false; - foreach (var (oldLambda, oldLambdaBody1, oldLambdaBody2) in GetLambdaBodies(oldMemberBody)) + if (!isStatic && oldType.TypeKind == TypeKind.Class && newType.TypeKind == TypeKind.Class) { - oldHasLambdas |= !IsLocalFunction(oldLambda); + // Adding the first instance constructor with parameters suppresses synthesized default constructor. + if (oldType.HasSynthesizedDefaultConstructor() && !newType.HasSynthesizedDefaultConstructor()) + { + semanticEdits.Add(SemanticEditInfo.CreateDelete( + SymbolKey.Create(oldType.InstanceConstructors.Single(c => c.Parameters is []), cancellationToken), + deletedSymbolContainer: typeKey, + partialType)); + } + + // Removing the last instance constructor with parameters inserts synthesized default constructor. + // We don't need to add an explicit semantic edit for synthesized members though since the compiler + // emits them automatically. } + } + } - var isInInterface = newMember.ContainingType.TypeKind == TypeKind.Interface; - var isNewMemberInGenericContext = InGenericContext(newMember); + /// + /// Return true if is true for a body of any instance/static member of that has an initializer. + /// + private bool AnyMemberInitializerBody(INamedTypeSymbol type, Func predicate, bool isStatic, CancellationToken cancellationToken) + { + // checking the old type for existing lambdas (it's ok for the new initializers to contain lambdas) - foreach (var (newLambda, newLambdaBody1, newLambdaBody2) in GetLambdaBodies(newMemberBody)) + foreach (var member in type.GetMembers()) + { + if (member.IsStatic == isStatic && + member.Kind is SymbolKind.Field or SymbolKind.Property && + member.DeclaringSyntaxReferences.Length > 0) // skip generated fields (e.g. VB auto-property backing fields) { - if (!bodyMap.Reverse.ContainsKey(newLambda)) + var syntax = GetSymbolDeclarationSyntax(member, cancellationToken); + if (IsDeclarationWithInitializer(syntax) && TryGetDeclarationBody(syntax, member) is { } body && predicate(body)) { - if (!CanAddNewLambda(newLambda, newLambdaBody1, newLambdaBody2)) - { - diagnostics.Add(new RudeEditDiagnostic(RudeEditKind.InsertNotSupportedByRuntime, GetDiagnosticSpan(newLambda, EditKind.Insert), newLambda, [GetDisplayName(newLambda, EditKind.Insert)])); - } - - // TODO: https://github.com/dotnet/roslyn/issues/37128 - // Local functions are emitted directly to the type containing the containing method. - // Although local functions are non-virtual the Core CLR currently does not support adding any method to an interface. - if (isInInterface && IsLocalFunction(newLambda)) - { - diagnostics.Add(new RudeEditDiagnostic(RudeEditKind.InsertLocalFunctionIntoInterfaceMethod, GetDiagnosticSpan(newLambda, EditKind.Insert), newLambda, [GetDisplayName(newLambda, EditKind.Insert)])); - } + return true; } } + } - bool CanAddNewLambda(SyntaxNode newLambda, LambdaBody newLambdaBody1, LambdaBody? newLambdaBody2) - { - // Adding a lambda/local function might result in - // 1) emitting a new closure type - // 2) adding method and a static field to an existing closure type - // - // We currently do not precisely determine whether or not a suitable closure type already exists - // as static closure types might be shared with lambdas defined in a different member of the containing type. - // See: https://github.com/dotnet/roslyn/issues/52759 - // - // Furthermore, if a new lambda captures a variable that is alredy captured by a local function then - // the closure type is converted from struct local function closure to a lambda display class. - // Similarly, when a new conversion from local function group to a delegate is added the closure type also changes. - // Both should be reported as rude edits during capture analysis. - // See https://github.com/dotnet/roslyn/issues/67323 + return false; + } + + private static IMethodSymbol? TryGetParameterlessConstructor(INamedTypeSymbol type, bool isStatic) + { + var oldCtors = isStatic ? type.StaticConstructors : type.InstanceConstructors; + if (isStatic) + { + return type.StaticConstructors.FirstOrDefault(); + } + else + { + return type.InstanceConstructors.FirstOrDefault(m => m.Parameters.Length == 0); + } + } + + private static bool IsPartialTypeEdit(ISymbol? oldSymbol, ISymbol? newSymbol, SyntaxTree oldSyntaxTree, SyntaxTree newSyntaxTree) + { + // If any of the partial declarations of the new or the old type are in another document + // the edit will need to be merged with other partial edits with matching partial type + static bool IsNotInDocument(SyntaxReference reference, SyntaxTree syntaxTree) + => reference.SyntaxTree != syntaxTree; - var isLocalFunction = IsLocalFunction(newLambda); + static bool IsPartialTypeEdit(ISymbol? symbol, SyntaxTree tree) + => symbol is INamedTypeSymbol && + symbol.DeclaringSyntaxReferences.Length > 1 && symbol.DeclaringSyntaxReferences.Any(IsNotInDocument, tree); - // We assume that [2] is always required since the closure type might already exist. - var requiredCapabilities = EditAndContinueCapabilities.AddMethodToExistingType; + return IsPartialTypeEdit(oldSymbol, oldSyntaxTree) || + IsPartialTypeEdit(newSymbol, newSyntaxTree); + } - var inGenericLocalContext = newMemberBody != null && InGenericLocalContext(newLambda, newMemberBody.RootNodes); + #endregion + + #region Lambdas and Closures + + private void ReportLambdaAndClosureRudeEdits( + SemanticModel? oldModel, + ISymbol oldMember, + MemberBody? oldMemberBody, + SyntaxNode? oldDeclaration, + SemanticModel newModel, + ISymbol newMember, + MemberBody? newMemberBody, + SyntaxNode? newDeclaration, + IReadOnlyDictionary? activeOrMatchedLambdas, + DeclarationBodyMap bodyMap, + EditAndContinueCapabilitiesGrantor capabilities, + ArrayBuilder diagnostics, + out bool syntaxMapRequired, + out Func? runtimeRudeEdits, + CancellationToken cancellationToken) + { + syntaxMapRequired = false; + runtimeRudeEdits = null; - if (isNewMemberInGenericContext || inGenericLocalContext) + if (activeOrMatchedLambdas != null) + { + var anySignatureErrors = false; + var hasUnmatchedLambdas = false; + foreach (var (oldLambdaBody, newLambdaInfo) in activeOrMatchedLambdas) + { + var newLambdaBody = newLambdaInfo.NewBody; + if (newLambdaBody == null) { - requiredCapabilities |= EditAndContinueCapabilities.GenericAddMethodToExistingType; + hasUnmatchedLambdas = true; + continue; } - // Static lambdas are cached in static fields, unless in generic local functions. - // If either body is static we need to require the capabilities. - var isLambdaCachedInField = - !inGenericLocalContext && - !isLocalFunction && - (GetAccessedCaptures(newLambdaBody1, newModel, newInLambdaCaptures, newCapturesIndex, newLiftingPrimaryConstructor).Equals(BitVector.Empty) || - newLambdaBody2 != null && GetAccessedCaptures(newLambdaBody2, newModel, newInLambdaCaptures, newCapturesIndex, newLiftingPrimaryConstructor).Equals(BitVector.Empty)); + var lambdaBodyMap = newLambdaInfo.BodyMap; - if (isLambdaCachedInField) - { - requiredCapabilities |= EditAndContinueCapabilities.AddStaticFieldToExistingType; + Debug.Assert(oldModel != null); + + var oldLambda = oldLambdaBody.GetLambda(); + var newLambda = newLambdaBody.GetLambda(); + + Debug.Assert(IsNestedFunction(newLambda) == IsNestedFunction(oldLambda)); + var isNestedFunction = IsNestedFunction(newLambda); + + var oldLambdaSymbol = isNestedFunction ? GetLambdaExpressionSymbol(oldModel, oldLambda, cancellationToken) : null; + var newLambdaSymbol = isNestedFunction ? GetLambdaExpressionSymbol(newModel, newLambda, cancellationToken) : null; + + var diagnosticContext = CreateDiagnosticContext(diagnostics, oldLambdaSymbol, newLambdaSymbol, newLambda, newModel, topMatch: null); + + var oldStateMachineInfo = oldLambdaBody.GetStateMachineInfo(); + var newStateMachineInfo = newLambdaBody.GetStateMachineInfo(); + + ReportStateMachineBodyUpdateRudeEdits(diagnosticContext, lambdaBodyMap, oldStateMachineInfo, newStateMachineInfo, newLambdaInfo.HasActiveStatement, cancellationToken); + + // When the delta IL of the containing method is emitted lambdas declared in it are also emitted. + // If the runtime does not support changing IL of the method (e.g. method containing stackalloc) + // we need to report a rude edit. + // If only trivia change the IL is going to be unchanged and only sequence points in the PDB change, + // so we do not report rude edits. + + if (!oldLambdaBody.IsSyntaxEquivalentTo(newLambdaBody)) + { + ReportMemberOrLambdaBodyUpdateRudeEdits( + diagnosticContext, + oldModel.Compilation, + oldLambda, + oldMember, + oldMemberBody, + oldLambdaBody, + newLambda, + newMember, + newMemberBody, + newLambdaBody, + capabilities, + oldStateMachineInfo, + newStateMachineInfo, + cancellationToken); - // If we are in a generic type or a member then the closure type is generic and we are adding a static field to a generic type. - if (isNewMemberInGenericContext) + if ((IsGenericLocalFunction(oldLambda) || IsGenericLocalFunction(newLambda)) && + !capabilities.Grant(EditAndContinueCapabilities.GenericUpdateMethod)) { - requiredCapabilities |= EditAndContinueCapabilities.GenericAddFieldToExistingType; + diagnosticContext.Report(RudeEditKind.UpdatingGenericNotSupportedByRuntime, cancellationToken); } } - // If the old verison of the method had any lambdas the nwe know a closure type exists and a new one isn't needed. - // We also know that adding a local function won't create a new closure type. - // Otherwise, we assume a new type is needed. - - if (!oldHasLambdas && !isLocalFunction) + // query signatures are analyzed separately: + if (isNestedFunction) { - requiredCapabilities |= EditAndContinueCapabilities.NewTypeDefinition; + ReportLambdaSignatureRudeEdits(diagnosticContext, oldLambda, newLambda, capabilities, out var hasErrors, cancellationToken); + anySignatureErrors |= hasErrors; } + } - return capabilities.Grant(requiredCapabilities); + // Any unmatched lambdas would have contained an active statement and a rude edit would be reported in syntax analysis phase. + // Skip the rest of lambda and closure analysis if such lambdas are present. + if (hasUnmatchedLambdas) + { + return; } - } - private IEnumerable<(SyntaxNode lambda, LambdaBody lambdaBody1, LambdaBody? lambdaBody2)> GetLambdaBodies(MemberBody? body) - { - if (body == null) + ArrayBuilder? lazyNewErroneousClauses = null; + foreach (var (oldQueryClause, newQueryClause) in bodyMap.Forward) { - yield break; + Debug.Assert(oldModel != null); + + if (!QueryClauseLambdasTypeEquivalent(oldModel, oldQueryClause, newModel, newQueryClause, cancellationToken)) + { + lazyNewErroneousClauses ??= ArrayBuilder.GetInstance(); + lazyNewErroneousClauses.Add(newQueryClause); + } } - foreach (var root in body.RootNodes) + if (lazyNewErroneousClauses != null) { - foreach (var node in root.DescendantNodesAndSelf()) + foreach (var newQueryClause in from clause in lazyNewErroneousClauses + orderby clause.SpanStart + group clause by GetContainingQueryExpression(clause) into clausesByQuery + select clausesByQuery.First()) { - if (TryGetLambdaBodies(node, out var body1, out var body2)) - { - yield return (node, body1, body2); - } + var diagnosticContext = CreateDiagnosticContext(diagnostics, oldSymbol: null, newSymbol: null, newQueryClause, newModel, topMatch: null); + diagnosticContext.Report(RudeEditKind.ChangingQueryLambdaType, cancellationToken); } + + lazyNewErroneousClauses.Free(); + anySignatureErrors = true; + } + + // only dig into captures if lambda signatures match + if (anySignatureErrors) + { + return; } } - private enum VariableCaptureKind + var oldPrimaryConstructor = oldDeclaration != null && IsPrimaryConstructorDeclaration(oldDeclaration) + ? (IMethodSymbol)oldMember + : GetPrimaryConstructor(oldMember.ContainingType, cancellationToken); + + var newPrimaryConstructor = newDeclaration != null && IsPrimaryConstructorDeclaration(newDeclaration) + ? (IMethodSymbol)newMember + : GetPrimaryConstructor(newMember.ContainingType, cancellationToken); + + // If type layout is changed another rude edit is reported, so we can assume the layouts match. + // We don't need to analyze primary parameter captures unless type layout disallows captures. + var typeLayoutDisallowsNewCaptures = + (newPrimaryConstructor != null || oldPrimaryConstructor != null) && HasExplicitOrSequentialLayout(newMember.ContainingType, newModel); + + // The primary constructor if its parameters are lifted into fields when accessed from this member, otherwise null. + var oldLiftingPrimaryConstructor = oldMember != oldPrimaryConstructor && oldDeclaration != null && !IsDeclarationWithInitializer(oldDeclaration) ? oldPrimaryConstructor : null; + var newLiftingPrimaryConstructor = newMember != newPrimaryConstructor && newDeclaration != null && !IsDeclarationWithInitializer(newDeclaration) ? newPrimaryConstructor : null; + + GetCapturedVariables( + oldMemberBody, + oldModel, + oldLiftingPrimaryConstructor, + ignorePrimaryParameterCaptures: !typeLayoutDisallowsNewCaptures, + out var oldHasLambdasOrLocalFunctions, + out var oldInLambdaCaptures, + out var oldPrimaryCaptures); + + GetCapturedVariables( + newMemberBody, + newModel, + newLiftingPrimaryConstructor, + ignorePrimaryParameterCaptures: !typeLayoutDisallowsNewCaptures, + out var newHasLambdasOrLocalFunctions, + out var newInLambdaCaptures, + out var newPrimaryCaptures); + + // Analyze primary parameter captures: + + ReportPrimaryParameterCaptureRudeEdits( + diagnostics, + oldLiftingPrimaryConstructor, + oldPrimaryCaptures, + newLiftingPrimaryConstructor, + newPrimaryCaptures, + newMember, + cancellationToken); + + // Analyze captures in lambda bodies: + + if (!oldHasLambdasOrLocalFunctions && !newHasLambdasOrLocalFunctions) { - This, - LocalOrParameter, + return; } - /// - /// Represents a captured local variable or a parameter of the current member. - /// Primary constructor parameters that are accessed via "this" are represented as - /// . - /// - private readonly struct VariableCapture(VariableCaptureKind kind, ISymbol symbol) - { - public readonly VariableCaptureKind Kind = kind; - public readonly ISymbol Symbol = symbol; + syntaxMapRequired = newHasLambdasOrLocalFunctions; - public bool IsThis => Kind == VariableCaptureKind.This; - public string Name => Symbol.Name; + // { new capture index -> old capture index } + using var _1 = ArrayBuilder.GetInstance(newInLambdaCaptures.Length, fillWithValue: 0, out var reverseCapturesMap); - public VariableCaptureKey Key - => VariableCaptureKey.Create(Kind, Symbol); - } + // { new capture index -> new closure scope or null for "this" } + using var _2 = ArrayBuilder.GetInstance(newInLambdaCaptures.Length, fillWithValue: null, out var newCapturesToClosureScopes); - /// - /// Use to look up captures by their symbol identity. - /// Captures of kind are represented by null . - /// - private readonly record struct VariableCaptureKey(VariableCaptureKind Kind, ISymbol? CapturedVariable) - { - public static VariableCaptureKey Create(VariableCaptureKind kind, ISymbol symbol) - => new(kind, kind == VariableCaptureKind.This ? null : symbol); + // Can be calculated from other maps but it's simpler to just calculate it upfront. + // { old capture index -> old closure scope or null for "this" } + using var _3 = ArrayBuilder.GetInstance(oldInLambdaCaptures.Length, fillWithValue: null, out var oldCapturesToClosureScopes); - public static VariableCaptureKey Create(ISymbol variable, IMethodSymbol? liftingPrimaryConstructor) - => Create(GetCaptureKind(variable, liftingPrimaryConstructor), variable); - } + using var _4 = PooledDictionary.GetInstance(out var closureRudeEdits); - private static VariableCaptureKind GetCaptureKind(ISymbol variable, IMethodSymbol? liftingPrimaryConstructor) - => variable is IParameterSymbol parameter && (parameter.IsThis || parameter.ContainingSymbol == liftingPrimaryConstructor) - ? VariableCaptureKind.This : VariableCaptureKind.LocalOrParameter; + CalculateCapturedVariablesMaps( + oldInLambdaCaptures, + oldDeclaration, + oldPrimaryConstructor, + newInLambdaCaptures, + newDeclaration, + newPrimaryConstructor, + bodyMap, + reverseCapturesMap, + newCapturesToClosureScopes, + oldCapturesToClosureScopes, + closureRudeEdits, + cancellationToken); - private void GetCapturedVariables( - MemberBody? memberBody, - SemanticModel? model, - IMethodSymbol? liftingPrimaryConstructor, - bool ignorePrimaryParameterCaptures, - out bool hasLambdaBodies, - out ImmutableArray variablesCapturedInLambdas, - out ImmutableArray primaryParametersCapturedViaThis) + if (closureRudeEdits.Any()) { - hasLambdaBodies = false; + var rudeEdits = closureRudeEdits.ToImmutableSegmentedDictionary( + static item => item.Key, + static item => new RuntimeRudeEdit(item.Value.ToDiagnostic(item.Key.SyntaxTree).ToString())); - if (memberBody == null) - { - variablesCapturedInLambdas = []; - primaryParametersCapturedViaThis = []; - return; - } + runtimeRudeEdits = node => rudeEdits.TryGetValue(node, out var message) ? message : null; + return; + } - Debug.Assert(model != null); + using var _5 = PooledDictionary.GetInstance(out var newCapturesIndex); + BuildIndex(newCapturesIndex, newInLambdaCaptures); - PooledDictionary? inLambdaCapturesIndex = null; - ArrayBuilder<(VariableCaptureKind kind, ISymbol symbol, ArrayBuilder capturingLambdas)>? inLambdaCaptures = null; + var oldHasLambdas = false; + foreach (var (oldLambda, oldLambdaBody1, oldLambdaBody2) in GetLambdaBodies(oldMemberBody)) + { + oldHasLambdas |= !IsLocalFunction(oldLambda); + } - foreach (var (lambda, lambdaBody1, lambdaBody2) in GetLambdaBodies(memberBody)) - { - hasLambdaBodies = true; + var isInInterface = newMember.ContainingType.TypeKind == TypeKind.Interface; + var isNewMemberInGenericContext = InGenericContext(newMember); - AddCaptures(lambdaBody1); - if (lambdaBody2 != null) + foreach (var (newLambda, newLambdaBody1, newLambdaBody2) in GetLambdaBodies(newMemberBody)) + { + if (!bodyMap.Reverse.ContainsKey(newLambda)) + { + if (!CanAddNewLambda(newLambda, newLambdaBody1, newLambdaBody2)) { - AddCaptures(lambdaBody2); + diagnostics.Add(new RudeEditDiagnostic(RudeEditKind.InsertNotSupportedByRuntime, GetDiagnosticSpan(newLambda, EditKind.Insert), newLambda, [GetDisplayName(newLambda, EditKind.Insert)])); } - void AddCaptures(LambdaBody lambdaBody) + // TODO: https://github.com/dotnet/roslyn/issues/37128 + // Local functions are emitted directly to the type containing the containing method. + // Although local functions are non-virtual the Core CLR currently does not support adding any method to an interface. + if (isInInterface && IsLocalFunction(newLambda)) { - var captures = lambdaBody.GetCapturedVariables(model); - if (!captures.IsEmpty) - { - inLambdaCapturesIndex ??= PooledDictionary.GetInstance(); - inLambdaCaptures ??= ArrayBuilder<(VariableCaptureKind, ISymbol, ArrayBuilder)>.GetInstance(); - - foreach (var capture in captures) - { - var key = VariableCaptureKey.Create(capture, liftingPrimaryConstructor); - var index = inLambdaCapturesIndex.GetOrAdd(key, inLambdaCaptures.Count); - if (index == inLambdaCaptures.Count) - { - // When capturing this parameter via primary constructor parameter capture - // the capture key might be the same for multiple captured symbols. - // We need any of the captured primary parameters, use the first one. - inLambdaCaptures.Add((key.Kind, capture, ArrayBuilder.GetInstance())); - } - - inLambdaCaptures[index].capturingLambdas.Add(lambdaBody); - } - } + diagnostics.Add(new RudeEditDiagnostic(RudeEditKind.InsertLocalFunctionIntoInterfaceMethod, GetDiagnosticSpan(newLambda, EditKind.Insert), newLambda, [GetDisplayName(newLambda, EditKind.Insert)])); } } + } - variablesCapturedInLambdas = inLambdaCaptures?.SelectAsArray( - static item => new VariableCapture(item.kind, item.symbol)) ?? []; + bool CanAddNewLambda(SyntaxNode newLambda, LambdaBody newLambdaBody1, LambdaBody? newLambdaBody2) + { + // Adding a lambda/local function might result in + // 1) emitting a new closure type + // 2) adding method and a static field to an existing closure type + // + // We currently do not precisely determine whether or not a suitable closure type already exists + // as static closure types might be shared with lambdas defined in a different member of the containing type. + // See: https://github.com/dotnet/roslyn/issues/52759 + // + // Furthermore, if a new lambda captures a variable that is alredy captured by a local function then + // the closure type is converted from struct local function closure to a lambda display class. + // Similarly, when a new conversion from local function group to a delegate is added the closure type also changes. + // Both should be reported as rude edits during capture analysis. + // See https://github.com/dotnet/roslyn/issues/67323 - inLambdaCaptures?.Free(); + var isLocalFunction = IsLocalFunction(newLambda); - // only primary constructor parameters can be captured outside of lambda bodies: - if (liftingPrimaryConstructor != null && !ignorePrimaryParameterCaptures) - { - primaryParametersCapturedViaThis = memberBody.GetCapturedVariables(model).SelectAsArray( - predicate: (capture, liftingPrimaryConstructor) => capture.ContainingSymbol == liftingPrimaryConstructor, - selector: (capture, _) => (IParameterSymbol)capture, - liftingPrimaryConstructor); - } - else - { - primaryParametersCapturedViaThis = []; - } + // We assume that [2] is always required since the closure type might already exist. + var requiredCapabilities = EditAndContinueCapabilities.AddMethodToExistingType; - inLambdaCapturesIndex?.Free(); - } + var inGenericLocalContext = newMemberBody != null && InGenericLocalContext(newLambda, newMemberBody.RootNodes); - private void ReportPrimaryParameterCaptureRudeEdits( - ArrayBuilder diagnostics, - IMethodSymbol? oldLiftingPrimaryConstructor, - ImmutableArray oldPrimaryCaptures, - IMethodSymbol? newLiftingPrimaryConstructor, - ImmutableArray newPrimaryCaptures, - ISymbol newMember, - CancellationToken cancellationToken) - { - foreach (var newCapture in newPrimaryCaptures) + if (isNewMemberInGenericContext || inGenericLocalContext) { - if (oldLiftingPrimaryConstructor == null || !IsCapturedPrimaryParameterCapturedInType(newCapture, oldLiftingPrimaryConstructor.ContainingType)) - { - diagnostics.Add(new RudeEditDiagnostic( - RudeEditKind.CapturingPrimaryConstructorParameter, - GetSymbolLocationSpan(newCapture, cancellationToken), - node: null, - [GetLayoutKindDisplay(newCapture), newCapture.Name])); - } + requiredCapabilities |= EditAndContinueCapabilities.GenericAddMethodToExistingType; } - // Disallow uncapturing primary parameters. We could allow it but would need to be sure the compiler is going to reuse - // the backing field if the parameter is later captured again, rather then emitting a new one of the same name. + // Static lambdas are cached in static fields, unless in generic local functions. + // If either body is static we need to require the capabilities. + var isLambdaCachedInField = + !inGenericLocalContext && + !isLocalFunction && + (GetAccessedCaptures(newLambdaBody1, newModel, newInLambdaCaptures, newCapturesIndex, newLiftingPrimaryConstructor).Equals(BitVector.Empty) || + newLambdaBody2 != null && GetAccessedCaptures(newLambdaBody2, newModel, newInLambdaCaptures, newCapturesIndex, newLiftingPrimaryConstructor).Equals(BitVector.Empty)); - foreach (var oldCapture in oldPrimaryCaptures) + if (isLambdaCachedInField) { - if (newLiftingPrimaryConstructor == null || !IsCapturedPrimaryParameterCapturedInType(oldCapture, newLiftingPrimaryConstructor.ContainingType)) + requiredCapabilities |= EditAndContinueCapabilities.AddStaticFieldToExistingType; + + // If we are in a generic type or a member then the closure type is generic and we are adding a static field to a generic type. + if (isNewMemberInGenericContext) { - diagnostics.Add(new RudeEditDiagnostic( - RudeEditKind.NotCapturingPrimaryConstructorParameter, - GetSymbolLocationSpan(newMember, cancellationToken), - node: null, - [GetLayoutKindDisplay(oldCapture), oldCapture.Name])); + requiredCapabilities |= EditAndContinueCapabilities.GenericAddFieldToExistingType; } } - static bool IsCapturedPrimaryParameterCapturedInType(IParameterSymbol capture, INamedTypeSymbol otherType) - { - var oldBackingField = capture.GetPrimaryParameterBackingField(); - - // captured parameter must have a backing field: - Contract.ThrowIfNull(oldBackingField); + // If the old verison of the method had any lambdas the nwe know a closure type exists and a new one isn't needed. + // We also know that adding a local function won't create a new closure type. + // Otherwise, we assume a new type is needed. - // The backing field still exists in the new type: - return otherType.GetMembers(oldBackingField.Name).Any(); + if (!oldHasLambdas && !isLocalFunction) + { + requiredCapabilities |= EditAndContinueCapabilities.NewTypeDefinition; } - static string GetLayoutKindDisplay(IParameterSymbol parameter) - => (parameter.ContainingType.TypeKind == TypeKind.Struct) ? FeaturesResources.struct_ : FeaturesResources.class_with_explicit_or_sequential_layout; + return capabilities.Grant(requiredCapabilities); } + } - private static BitVector GetAccessedCaptures( - LambdaBody lambdaBody, - SemanticModel model, - ImmutableArray captures, - PooledDictionary capturesIndex, - IMethodSymbol? liftingPrimaryConstructor) + private IEnumerable<(SyntaxNode lambda, LambdaBody lambdaBody1, LambdaBody? lambdaBody2)> GetLambdaBodies(MemberBody? body) + { + if (body == null) { - var result = BitVector.Create(captures.Length); + yield break; + } - foreach (var expressionOrStatement in lambdaBody.GetExpressionsAndStatements()) + foreach (var root in body.RootNodes) + { + foreach (var node in root.DescendantNodesAndSelf()) { - var dataFlow = model.AnalyzeDataFlow(expressionOrStatement); - MarkVariables(dataFlow.ReadInside); - MarkVariables(dataFlow.WrittenInside); - - void MarkVariables(ImmutableArray variables) + if (TryGetLambdaBodies(node, out var body1, out var body2)) { - foreach (var variable in variables) - { - if (capturesIndex.TryGetValue(VariableCaptureKey.Create(variable, liftingPrimaryConstructor), out var newCaptureIndex)) - { - result[newCaptureIndex] = true; - } - } + yield return (node, body1, body2); } } - - return result; } + } - private static void BuildIndex(Dictionary index, ImmutableArray array) - { - for (var i = 0; i < array.Length; i++) - { - index.Add(array[i].Key, i); - } - } + private enum VariableCaptureKind + { + This, + LocalOrParameter, + } - internal static ISymbol? GetAssociatedMember(ISymbol symbol) - => symbol switch - { - IMethodSymbol method => method.AssociatedSymbol, - ITypeParameterSymbol or IParameterSymbol => symbol.ContainingSymbol, - _ => null - }; + /// + /// Represents a captured local variable or a parameter of the current member. + /// Primary constructor parameters that are accessed via "this" are represented as + /// . + /// + private readonly struct VariableCapture(VariableCaptureKind kind, ISymbol symbol) + { + public readonly VariableCaptureKind Kind = kind; + public readonly ISymbol Symbol = symbol; - /// - /// Returns node that represents a declaration of the symbol. - /// - protected abstract SyntaxNode? GetSymbolDeclarationSyntax(ISymbol symbol, Func, SyntaxReference?> selector, CancellationToken cancellationToken); + public bool IsThis => Kind == VariableCaptureKind.This; + public string Name => Symbol.Name; - protected SyntaxNode GetSymbolDeclarationSyntax(ISymbol symbol, CancellationToken cancellationToken) - => GetSymbolDeclarationSyntax(symbol, selector: System.Linq.ImmutableArrayExtensions.First, cancellationToken)!; + public VariableCaptureKey Key + => VariableCaptureKey.Create(Kind, Symbol); + } - protected SyntaxNode? GetSingleSymbolDeclarationSyntax(ISymbol symbol, CancellationToken cancellationToken) - => GetSymbolDeclarationSyntax(symbol, selector: refs => refs is [var single] ? single : null, cancellationToken)!; + /// + /// Use to look up captures by their symbol identity. + /// Captures of kind are represented by null . + /// + private readonly record struct VariableCaptureKey(VariableCaptureKind Kind, ISymbol? CapturedVariable) + { + public static VariableCaptureKey Create(VariableCaptureKind kind, ISymbol symbol) + => new(kind, kind == VariableCaptureKind.This ? null : symbol); - protected SyntaxNode? GetSymbolDeclarationSyntax(ISymbol symbol, SyntaxTree tree, CancellationToken cancellationToken) - => GetSymbolDeclarationSyntax(symbol, syntaxRefs => syntaxRefs.FirstOrDefault(r => r.SyntaxTree == tree), cancellationToken); + public static VariableCaptureKey Create(ISymbol variable, IMethodSymbol? liftingPrimaryConstructor) + => Create(GetCaptureKind(variable, liftingPrimaryConstructor), variable); + } - protected abstract ISymbol? GetDeclaredSymbol(SemanticModel model, SyntaxNode declaration, CancellationToken cancellationToken); + private static VariableCaptureKind GetCaptureKind(ISymbol variable, IMethodSymbol? liftingPrimaryConstructor) + => variable is IParameterSymbol parameter && (parameter.IsThis || parameter.ContainingSymbol == liftingPrimaryConstructor) + ? VariableCaptureKind.This : VariableCaptureKind.LocalOrParameter; + + private void GetCapturedVariables( + MemberBody? memberBody, + SemanticModel? model, + IMethodSymbol? liftingPrimaryConstructor, + bool ignorePrimaryParameterCaptures, + out bool hasLambdaBodies, + out ImmutableArray variablesCapturedInLambdas, + out ImmutableArray primaryParametersCapturedViaThis) + { + hasLambdaBodies = false; - protected ISymbol GetRequiredDeclaredSymbol(SemanticModel model, SyntaxNode declaration, CancellationToken cancellationToken) + if (memberBody == null) { - var symbol = GetDeclaredSymbol(model, declaration, cancellationToken); - Contract.ThrowIfNull(symbol); - return symbol; + variablesCapturedInLambdas = []; + primaryParametersCapturedViaThis = []; + return; } - private TextSpan GetSymbolLocationSpan(ISymbol symbol, CancellationToken cancellationToken) - { - // Note that in VB implicit value parameter in property setter doesn't have a location. - // In C# its location is the location of the setter. - // See https://github.com/dotnet/roslyn/issues/14273 - return IsGlobalMain(symbol) ? GetDiagnosticSpan(GetSymbolDeclarationSyntax(symbol, cancellationToken), EditKind.Update) : - symbol is IParameterSymbol && IsGlobalMain(symbol.ContainingSymbol) ? GetDiagnosticSpan(GetSymbolDeclarationSyntax(symbol.ContainingSymbol, cancellationToken), EditKind.Update) : - symbol.Locations.FirstOrDefault()?.SourceSpan ?? symbol.ContainingSymbol.Locations.First().SourceSpan; - } + Debug.Assert(model != null); - private CapturedParameterKey GetParameterKey(IParameterSymbol parameter, CancellationToken cancellationToken) + PooledDictionary? inLambdaCapturesIndex = null; + ArrayBuilder<(VariableCaptureKind kind, ISymbol symbol, ArrayBuilder capturingLambdas)>? inLambdaCaptures = null; + + foreach (var (lambda, lambdaBody1, lambdaBody2) in GetLambdaBodies(memberBody)) { - Debug.Assert(!parameter.IsThis); + hasLambdaBodies = true; - if (parameter.IsImplicitValueParameter()) + AddCaptures(lambdaBody1); + if (lambdaBody2 != null) { - return new CapturedParameterKey(ParameterKind.Value); + AddCaptures(lambdaBody2); } - if (IsGlobalMain(parameter.ContainingSymbol)) + void AddCaptures(LambdaBody lambdaBody) { - return new CapturedParameterKey(ParameterKind.TopLevelMainArgs); - } + var captures = lambdaBody.GetCapturedVariables(model); + if (!captures.IsEmpty) + { + inLambdaCapturesIndex ??= PooledDictionary.GetInstance(); + inLambdaCaptures ??= ArrayBuilder<(VariableCaptureKind, ISymbol, ArrayBuilder)>.GetInstance(); - var lambda = parameter.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.LambdaMethod or MethodKind.LocalFunction } containingLambda ? - GetSymbolDeclarationSyntax(containingLambda, cancellationToken) : null; + foreach (var capture in captures) + { + var key = VariableCaptureKey.Create(capture, liftingPrimaryConstructor); + var index = inLambdaCapturesIndex.GetOrAdd(key, inLambdaCaptures.Count); + if (index == inLambdaCaptures.Count) + { + // When capturing this parameter via primary constructor parameter capture + // the capture key might be the same for multiple captured symbols. + // We need any of the captured primary parameters, use the first one. + inLambdaCaptures.Add((key.Kind, capture, ArrayBuilder.GetInstance())); + } - // Indexer parameters in the getter/setter are implicitly declared. - // We need to find the corresponding parameter of the indexer itself. - if (parameter is { IsImplicitlyDeclared: true, ContainingSymbol: IMethodSymbol { AssociatedSymbol: { } associatedSymbol } }) - { - parameter = associatedSymbol.GetParameters().Single(p => p.Name == parameter.Name); + inLambdaCaptures[index].capturingLambdas.Add(lambdaBody); + } + } } + } + + variablesCapturedInLambdas = inLambdaCaptures?.SelectAsArray( + static item => new VariableCapture(item.kind, item.symbol)) ?? []; + + inLambdaCaptures?.Free(); - return new CapturedParameterKey(ParameterKind.Explicit, GetSymbolDeclarationSyntax(parameter, cancellationToken), lambda); + // only primary constructor parameters can be captured outside of lambda bodies: + if (liftingPrimaryConstructor != null && !ignorePrimaryParameterCaptures) + { + primaryParametersCapturedViaThis = memberBody.GetCapturedVariables(model).SelectAsArray( + predicate: (capture, liftingPrimaryConstructor) => capture.ContainingSymbol == liftingPrimaryConstructor, + selector: (capture, _) => (IParameterSymbol)capture, + liftingPrimaryConstructor); + } + else + { + primaryParametersCapturedViaThis = []; } - private static bool TryMapParameter( - CapturedParameterKey parameterKey, - IReadOnlyDictionary? parameterMap, - IReadOnlyDictionary bodyMap, - out CapturedParameterKey mappedParameterKey) + inLambdaCapturesIndex?.Free(); + } + + private void ReportPrimaryParameterCaptureRudeEdits( + ArrayBuilder diagnostics, + IMethodSymbol? oldLiftingPrimaryConstructor, + ImmutableArray oldPrimaryCaptures, + IMethodSymbol? newLiftingPrimaryConstructor, + ImmutableArray newPrimaryCaptures, + ISymbol newMember, + CancellationToken cancellationToken) + { + foreach (var newCapture in newPrimaryCaptures) { - if (parameterKey.ContainingLambda == null) + if (oldLiftingPrimaryConstructor == null || !IsCapturedPrimaryParameterCapturedInType(newCapture, oldLiftingPrimaryConstructor.ContainingType)) { - // method or primary constructor parameter: - SyntaxNode? mappedParameter = null; - if (parameterKey.Syntax == null || parameterMap?.TryGetValue(parameterKey.Syntax, out mappedParameter) == true) - { - mappedParameterKey = parameterKey with { Syntax = mappedParameter }; - return true; - } + diagnostics.Add(new RudeEditDiagnostic( + RudeEditKind.CapturingPrimaryConstructorParameter, + GetSymbolLocationSpan(newCapture, cancellationToken), + node: null, + [GetLayoutKindDisplay(newCapture), newCapture.Name])); } - else if (bodyMap.TryGetValue(parameterKey.ContainingLambda, out var mappedContainingLambdaSyntax)) - { - // lambda or local function parameter: - Debug.Assert(parameterKey.Syntax != null); + } - if (bodyMap.TryGetValue(parameterKey.Syntax, out var mappedParameter)) - { - mappedParameterKey = parameterKey with { Syntax = mappedParameter, ContainingLambda = mappedContainingLambdaSyntax }; - return true; - } - } + // Disallow uncapturing primary parameters. We could allow it but would need to be sure the compiler is going to reuse + // the backing field if the parameter is later captured again, rather then emitting a new one of the same name. - // no mapping - mappedParameterKey = default; - return false; + foreach (var oldCapture in oldPrimaryCaptures) + { + if (newLiftingPrimaryConstructor == null || !IsCapturedPrimaryParameterCapturedInType(oldCapture, newLiftingPrimaryConstructor.ContainingType)) + { + diagnostics.Add(new RudeEditDiagnostic( + RudeEditKind.NotCapturingPrimaryConstructorParameter, + GetSymbolLocationSpan(newMember, cancellationToken), + node: null, + [GetLayoutKindDisplay(oldCapture), oldCapture.Name])); + } } - private enum ParameterKind + static bool IsCapturedPrimaryParameterCapturedInType(IParameterSymbol capture, INamedTypeSymbol otherType) { - Explicit, - Value, - TopLevelMainArgs + var oldBackingField = capture.GetPrimaryParameterBackingField(); + + // captured parameter must have a backing field: + Contract.ThrowIfNull(oldBackingField); + + // The backing field still exists in the new type: + return otherType.GetMembers(oldBackingField.Name).Any(); } - private readonly record struct CapturedParameterKey(ParameterKind Kind, SyntaxNode? Syntax = null, SyntaxNode? ContainingLambda = null); + static string GetLayoutKindDisplay(IParameterSymbol parameter) + => (parameter.ContainingType.TypeKind == TypeKind.Struct) ? FeaturesResources.struct_ : FeaturesResources.class_with_explicit_or_sequential_layout; + } - private void CalculateCapturedVariablesMaps( - ImmutableArray oldCaptures, - SyntaxNode? oldDeclaration, - IMethodSymbol? oldPrimaryConstructor, - ImmutableArray newCaptures, - SyntaxNode? newDeclaration, - IMethodSymbol? newPrimaryConstructor, - DeclarationBodyMap bodyMap, - [Out] ArrayBuilder reverseCapturesMap, // {new capture index -> old capture index} - [Out] ArrayBuilder newCapturesToClosureScopes, // {new capture index -> new closure scope} - [Out] ArrayBuilder oldCapturesToClosureScopes, // {old capture index -> old closure scope} - [Out] Dictionary closureRudeEdits, - CancellationToken cancellationToken) - { - BidirectionalMap? parameterMap = null; - if (oldDeclaration != null && newDeclaration != null) - { - parameterMap = ComputeParameterMap(oldDeclaration, newDeclaration); - - // In context where primary parameters are accessed directly but they are not part of the member body match (i.e. in initializers), - // calculate mapping of the primary constructor parameters and merge it into parameter map. - if (oldPrimaryConstructor != null && - newPrimaryConstructor != null && - IsDeclarationWithInitializer(oldDeclaration) && - IsDeclarationWithInitializer(newDeclaration)) - { - var primaryParameterMap = ComputeParameterMap( - GetSymbolDeclarationSyntax(oldPrimaryConstructor, cancellationToken), - GetSymbolDeclarationSyntax(newPrimaryConstructor, cancellationToken)); + private static BitVector GetAccessedCaptures( + LambdaBody lambdaBody, + SemanticModel model, + ImmutableArray captures, + PooledDictionary capturesIndex, + IMethodSymbol? liftingPrimaryConstructor) + { + var result = BitVector.Create(captures.Length); - Contract.ThrowIfNull(primaryParameterMap); + foreach (var expressionOrStatement in lambdaBody.GetExpressionsAndStatements()) + { + var dataFlow = model.AnalyzeDataFlow(expressionOrStatement); + MarkVariables(dataFlow.ReadInside); + MarkVariables(dataFlow.WrittenInside); - parameterMap = (parameterMap != null) ? parameterMap.Value.With(primaryParameterMap.Value) : primaryParameterMap; + void MarkVariables(ImmutableArray variables) + { + foreach (var variable in variables) + { + if (capturesIndex.TryGetValue(VariableCaptureKey.Create(variable, liftingPrimaryConstructor), out var newCaptureIndex)) + { + result[newCaptureIndex] = true; + } } } + } - using var _1 = PooledDictionary.GetInstance(out var oldLocalCaptures); - using var _2 = PooledDictionary.GetInstance(out var oldParameterCaptures); + return result; + } - for (var oldCaptureIndex = 0; oldCaptureIndex < oldCaptures.Length; oldCaptureIndex++) - { - var oldCapture = oldCaptures[oldCaptureIndex]; + private static void BuildIndex(Dictionary index, ImmutableArray array) + { + for (var i = 0; i < array.Length; i++) + { + index.Add(array[i].Key, i); + } + } - if (oldCapture.IsThis) - { - continue; - } + internal static ISymbol? GetAssociatedMember(ISymbol symbol) + => symbol switch + { + IMethodSymbol method => method.AssociatedSymbol, + ITypeParameterSymbol or IParameterSymbol => symbol.ContainingSymbol, + _ => null + }; - if (oldCapture.Symbol is IParameterSymbol oldParameterCapture) - { - oldParameterCaptures.Add(GetParameterKey(oldParameterCapture, cancellationToken), oldCaptureIndex); - } - else - { - oldLocalCaptures.Add(GetSymbolDeclarationSyntax(oldCapture.Symbol, cancellationToken), oldCaptureIndex); - } - } + /// + /// Returns node that represents a declaration of the symbol. + /// + protected abstract SyntaxNode? GetSymbolDeclarationSyntax(ISymbol symbol, Func, SyntaxReference?> selector, CancellationToken cancellationToken); - for (var newCaptureIndex = 0; newCaptureIndex < newCaptures.Length; newCaptureIndex++) - { - var newCapture = newCaptures[newCaptureIndex]; - int oldCaptureIndex; + protected SyntaxNode GetSymbolDeclarationSyntax(ISymbol symbol, CancellationToken cancellationToken) + => GetSymbolDeclarationSyntax(symbol, selector: System.Linq.ImmutableArrayExtensions.First, cancellationToken)!; - if (newCapture.IsThis) - { - continue; - } + protected SyntaxNode? GetSingleSymbolDeclarationSyntax(ISymbol symbol, CancellationToken cancellationToken) + => GetSymbolDeclarationSyntax(symbol, selector: refs => refs is [var single] ? single : null, cancellationToken)!; - if (newCapture.Symbol is IParameterSymbol newParameterCapture) - { - var newParameterKey = GetParameterKey(newParameterCapture, cancellationToken); - if (!TryMapParameter(newParameterKey, parameterMap?.Reverse, bodyMap.Reverse, out var oldParameterKey) || - !oldParameterCaptures.TryGetValue(oldParameterKey, out oldCaptureIndex)) - { - continue; - } - } - else - { - var local = newCapture.Symbol; - var newCaptureSyntax = GetSymbolDeclarationSyntax(local, cancellationToken); + protected SyntaxNode? GetSymbolDeclarationSyntax(ISymbol symbol, SyntaxTree tree, CancellationToken cancellationToken) + => GetSymbolDeclarationSyntax(symbol, syntaxRefs => syntaxRefs.FirstOrDefault(r => r.SyntaxTree == tree), cancellationToken); - // variable doesn't exists in the old method or has not been captured prior the edit: - if (!bodyMap.Reverse.TryGetValue(newCaptureSyntax, out var mappedOldSyntax) || - !oldLocalCaptures.TryGetValue(mappedOldSyntax, out oldCaptureIndex)) - { - continue; - } - } + protected abstract ISymbol? GetDeclaredSymbol(SemanticModel model, SyntaxNode declaration, CancellationToken cancellationToken); - reverseCapturesMap[newCaptureIndex] = oldCaptureIndex; + protected ISymbol GetRequiredDeclaredSymbol(SemanticModel model, SyntaxNode declaration, CancellationToken cancellationToken) + { + var symbol = GetDeclaredSymbol(model, declaration, cancellationToken); + Contract.ThrowIfNull(symbol); + return symbol; + } - var oldCapture = oldCaptures[oldCaptureIndex]; - Contract.ThrowIfTrue(oldCapture.IsThis); + private TextSpan GetSymbolLocationSpan(ISymbol symbol, CancellationToken cancellationToken) + { + // Note that in VB implicit value parameter in property setter doesn't have a location. + // In C# its location is the location of the setter. + // See https://github.com/dotnet/roslyn/issues/14273 + return IsGlobalMain(symbol) ? GetDiagnosticSpan(GetSymbolDeclarationSyntax(symbol, cancellationToken), EditKind.Update) : + symbol is IParameterSymbol && IsGlobalMain(symbol.ContainingSymbol) ? GetDiagnosticSpan(GetSymbolDeclarationSyntax(symbol.ContainingSymbol, cancellationToken), EditKind.Update) : + symbol.Locations.FirstOrDefault()?.SourceSpan ?? symbol.ContainingSymbol.Locations.First().SourceSpan; + } - // If new parameter/local capture and does not have a corresponding old parameter/local capture a rude edit is reported above. - // Also range variables can't be mapped to other variables since they have - // different kinds of declarator syntax nodes. - Debug.Assert(oldCapture.Kind == newCapture.Kind); + private CapturedParameterKey GetParameterKey(IParameterSymbol parameter, CancellationToken cancellationToken) + { + Debug.Assert(!parameter.IsThis); - var oldSymbol = oldCapture.Symbol; - var newSymbol = newCapture.Symbol; + if (parameter.IsImplicitValueParameter()) + { + return new CapturedParameterKey(ParameterKind.Value); + } - // Range variables don't have types. Each transparent identifier (range variable use) - // might have a different type. Changing these types is ok as long as the containing lambda - // signatures remain unchanged, which we validate for all lambdas in general. - // - // The scope of a transparent identifier is the containing lambda body. Since we verify that - // each lambda body accesses the same captured variables (including range variables) - // the corresponding scopes are guaranteed to be preserved as well. - if (oldSymbol.Kind == SymbolKind.RangeVariable) - { - continue; - } + if (IsGlobalMain(parameter.ContainingSymbol)) + { + return new CapturedParameterKey(ParameterKind.TopLevelMainArgs); + } - // rename: - // Note that the name has to match exactly even in VB, since we can't rename a field. - if (newSymbol.Name != oldSymbol.Name) - { - AddRuntimeRudeEdit(newSymbol, new RudeEditDiagnostic( - RudeEditKind.RenamingCapturedVariable, - GetSymbolLocationSpan(newSymbol, cancellationToken), - null, - [oldSymbol.Name, newSymbol.Name])); + var lambda = parameter.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.LambdaMethod or MethodKind.LocalFunction } containingLambda ? + GetSymbolDeclarationSyntax(containingLambda, cancellationToken) : null; + + // Indexer parameters in the getter/setter are implicitly declared. + // We need to find the corresponding parameter of the indexer itself. + if (parameter is { IsImplicitlyDeclared: true, ContainingSymbol: IMethodSymbol { AssociatedSymbol: { } associatedSymbol } }) + { + parameter = associatedSymbol.GetParameters().Single(p => p.Name == parameter.Name); + } + + return new CapturedParameterKey(ParameterKind.Explicit, GetSymbolDeclarationSyntax(parameter, cancellationToken), lambda); + } + + private static bool TryMapParameter( + CapturedParameterKey parameterKey, + IReadOnlyDictionary? parameterMap, + IReadOnlyDictionary bodyMap, + out CapturedParameterKey mappedParameterKey) + { + if (parameterKey.ContainingLambda == null) + { + // method or primary constructor parameter: + SyntaxNode? mappedParameter = null; + if (parameterKey.Syntax == null || parameterMap?.TryGetValue(parameterKey.Syntax, out mappedParameter) == true) + { + mappedParameterKey = parameterKey with { Syntax = mappedParameter }; + return true; + } + } + else if (bodyMap.TryGetValue(parameterKey.ContainingLambda, out var mappedContainingLambdaSyntax)) + { + // lambda or local function parameter: + Debug.Assert(parameterKey.Syntax != null); - continue; - } + if (bodyMap.TryGetValue(parameterKey.Syntax, out var mappedParameter)) + { + mappedParameterKey = parameterKey with { Syntax = mappedParameter, ContainingLambda = mappedContainingLambdaSyntax }; + return true; + } + } - // If a parameter type changes then the containing method is going to be deleted - // along with all its closures. No need to issue rude edits for the closures. - if (oldSymbol.Kind == SymbolKind.Parameter) - { - continue; - } + // no mapping + mappedParameterKey = default; + return false; + } - var oldType = GetType(oldSymbol); - var newType = GetType(newSymbol); + private enum ParameterKind + { + Explicit, + Value, + TopLevelMainArgs + } - if (!TypesEquivalent(oldType, newType, exact: false)) - { - AddRuntimeRudeEdit(newSymbol, new RudeEditDiagnostic( - RudeEditKind.ChangingCapturedVariableType, - GetSymbolLocationSpan(newSymbol, cancellationToken), - node: null, - [newSymbol.Name, oldType.ToDisplayString(ErrorDisplayFormat)])); + private readonly record struct CapturedParameterKey(ParameterKind Kind, SyntaxNode? Syntax = null, SyntaxNode? ContainingLambda = null); + + private void CalculateCapturedVariablesMaps( + ImmutableArray oldCaptures, + SyntaxNode? oldDeclaration, + IMethodSymbol? oldPrimaryConstructor, + ImmutableArray newCaptures, + SyntaxNode? newDeclaration, + IMethodSymbol? newPrimaryConstructor, + DeclarationBodyMap bodyMap, + [Out] ArrayBuilder reverseCapturesMap, // {new capture index -> old capture index} + [Out] ArrayBuilder newCapturesToClosureScopes, // {new capture index -> new closure scope} + [Out] ArrayBuilder oldCapturesToClosureScopes, // {old capture index -> old closure scope} + [Out] Dictionary closureRudeEdits, + CancellationToken cancellationToken) + { + BidirectionalMap? parameterMap = null; + if (oldDeclaration != null && newDeclaration != null) + { + parameterMap = ComputeParameterMap(oldDeclaration, newDeclaration); - continue; - } + // In context where primary parameters are accessed directly but they are not part of the member body match (i.e. in initializers), + // calculate mapping of the primary constructor parameters and merge it into parameter map. + if (oldPrimaryConstructor != null && + newPrimaryConstructor != null && + IsDeclarationWithInitializer(oldDeclaration) && + IsDeclarationWithInitializer(newDeclaration)) + { + var primaryParameterMap = ComputeParameterMap( + GetSymbolDeclarationSyntax(oldPrimaryConstructor, cancellationToken), + GetSymbolDeclarationSyntax(newPrimaryConstructor, cancellationToken)); - var oldScope = GetCapturedVariableScope(oldSymbol, cancellationToken); - var newScope = GetCapturedVariableScope(newSymbol, cancellationToken); - if (!AreEquivalentClosureScopes(oldScope, newScope, bodyMap.Reverse)) - { - continue; - } + Contract.ThrowIfNull(primaryParameterMap); - newCapturesToClosureScopes[newCaptureIndex] = newScope; - oldCapturesToClosureScopes[oldCaptureIndex] = oldScope; + parameterMap = (parameterMap != null) ? parameterMap.Value.With(primaryParameterMap.Value) : primaryParameterMap; } - - void AddRuntimeRudeEdit(ISymbol newSymbol, RudeEditDiagnostic diagnostic) - => closureRudeEdits.TryAdd(GetCapturedVariableScope(newSymbol, cancellationToken), diagnostic); } - private void ReportLambdaSignatureRudeEdits( - DiagnosticContext diagnosticContext, - SyntaxNode oldLambda, - SyntaxNode newLambda, - EditAndContinueCapabilitiesGrantor capabilities, - out bool hasSignatureErrors, - CancellationToken cancellationToken) - { - hasSignatureErrors = false; + using var _1 = PooledDictionary.GetInstance(out var oldLocalCaptures); + using var _2 = PooledDictionary.GetInstance(out var oldParameterCaptures); - Debug.Assert(IsNestedFunction(newLambda)); - Debug.Assert(IsNestedFunction(oldLambda)); + for (var oldCaptureIndex = 0; oldCaptureIndex < oldCaptures.Length; oldCaptureIndex++) + { + var oldCapture = oldCaptures[oldCaptureIndex]; - if (IsLocalFunction(oldLambda) != IsLocalFunction(newLambda)) + if (oldCapture.IsThis) { - diagnosticContext.Report(RudeEditKind.SwitchBetweenLambdaAndLocalFunction, cancellationToken); - hasSignatureErrors = true; - return; + continue; } - var oldLambdaSymbol = (IMethodSymbol)diagnosticContext.RequiredOldSymbol; - var newLambdaSymbol = (IMethodSymbol)diagnosticContext.RequiredNewSymbol; - - // signature validation: - if (!ParameterTypesEquivalent(oldLambdaSymbol.Parameters, newLambdaSymbol.Parameters, exact: false)) + if (oldCapture.Symbol is IParameterSymbol oldParameterCapture) { - diagnosticContext.Report(RudeEditKind.ChangingLambdaParameters, cancellationToken); - hasSignatureErrors = true; + oldParameterCaptures.Add(GetParameterKey(oldParameterCapture, cancellationToken), oldCaptureIndex); } - else if (!ReturnTypesEquivalent(oldLambdaSymbol, newLambdaSymbol, exact: false)) + else { - diagnosticContext.Report(RudeEditKind.ChangingLambdaReturnType, cancellationToken); - hasSignatureErrors = true; + oldLocalCaptures.Add(GetSymbolDeclarationSyntax(oldCapture.Symbol, cancellationToken), oldCaptureIndex); } - else if (!TypeParametersEquivalent(oldLambdaSymbol.TypeParameters, newLambdaSymbol.TypeParameters, exact: false) || - !oldLambdaSymbol.TypeParameters.SequenceEqual(newLambdaSymbol.TypeParameters, static (p, q) => p.Name == q.Name)) + } + + for (var newCaptureIndex = 0; newCaptureIndex < newCaptures.Length; newCaptureIndex++) + { + var newCapture = newCaptures[newCaptureIndex]; + int oldCaptureIndex; + + if (newCapture.IsThis) { - diagnosticContext.Report(RudeEditKind.ChangingTypeParameters, cancellationToken); - hasSignatureErrors = true; + continue; } - if (hasSignatureErrors) + if (newCapture.Symbol is IParameterSymbol newParameterCapture) { - return; + var newParameterKey = GetParameterKey(newParameterCapture, cancellationToken); + if (!TryMapParameter(newParameterKey, parameterMap?.Reverse, bodyMap.Reverse, out var oldParameterKey) || + !oldParameterCaptures.TryGetValue(oldParameterKey, out oldCaptureIndex)) + { + continue; + } + } + else + { + var local = newCapture.Symbol; + var newCaptureSyntax = GetSymbolDeclarationSyntax(local, cancellationToken); + + // variable doesn't exists in the old method or has not been captured prior the edit: + if (!bodyMap.Reverse.TryGetValue(newCaptureSyntax, out var mappedOldSyntax) || + !oldLocalCaptures.TryGetValue(mappedOldSyntax, out oldCaptureIndex)) + { + continue; + } } - // custom attributes + reverseCapturesMap[newCaptureIndex] = oldCaptureIndex; - ReportCustomAttributeRudeEdits(diagnosticContext, capabilities, out _, out _, cancellationToken); + var oldCapture = oldCaptures[oldCaptureIndex]; + Contract.ThrowIfTrue(oldCapture.IsThis); - for (var i = 0; i < oldLambdaSymbol.Parameters.Length; i++) - { - ReportCustomAttributeRudeEdits(diagnosticContext.WithSymbols(oldLambdaSymbol.Parameters[i], newLambdaSymbol.Parameters[i]), capabilities, out _, out _, cancellationToken); - } + // If new parameter/local capture and does not have a corresponding old parameter/local capture a rude edit is reported above. + // Also range variables can't be mapped to other variables since they have + // different kinds of declarator syntax nodes. + Debug.Assert(oldCapture.Kind == newCapture.Kind); + + var oldSymbol = oldCapture.Symbol; + var newSymbol = newCapture.Symbol; - for (var i = 0; i < oldLambdaSymbol.TypeParameters.Length; i++) + // Range variables don't have types. Each transparent identifier (range variable use) + // might have a different type. Changing these types is ok as long as the containing lambda + // signatures remain unchanged, which we validate for all lambdas in general. + // + // The scope of a transparent identifier is the containing lambda body. Since we verify that + // each lambda body accesses the same captured variables (including range variables) + // the corresponding scopes are guaranteed to be preserved as well. + if (oldSymbol.Kind == SymbolKind.RangeVariable) { - ReportCustomAttributeRudeEdits(diagnosticContext.WithSymbols(oldLambdaSymbol.TypeParameters[i], newLambdaSymbol.TypeParameters[i]), capabilities, out _, out _, cancellationToken); + continue; } - } - private static ITypeSymbol GetType(ISymbol localOrParameter) - => localOrParameter.Kind switch + // rename: + // Note that the name has to match exactly even in VB, since we can't rename a field. + if (newSymbol.Name != oldSymbol.Name) { - SymbolKind.Parameter => ((IParameterSymbol)localOrParameter).Type, - SymbolKind.Local => ((ILocalSymbol)localOrParameter).Type, - _ => throw ExceptionUtilities.UnexpectedValue(localOrParameter.Kind), - }; + AddRuntimeRudeEdit(newSymbol, new RudeEditDiagnostic( + RudeEditKind.RenamingCapturedVariable, + GetSymbolLocationSpan(newSymbol, cancellationToken), + null, + [oldSymbol.Name, newSymbol.Name])); - private SyntaxNode GetCapturedVariableScope(ISymbol local, CancellationToken cancellationToken) - { - Debug.Assert(local.Kind is not SymbolKind.RangeVariable); + continue; + } - if (local is IParameterSymbol) + // If a parameter type changes then the containing method is going to be deleted + // along with all its closures. No need to issue rude edits for the closures. + if (oldSymbol.Kind == SymbolKind.Parameter) { - var scope = GetCapturedParameterScope(GetSymbolDeclarationSyntax(local.ContainingSymbol, cancellationToken)); - Contract.ThrowIfFalse(IsClosureScope(scope)); - return scope; + continue; } - var node = GetSymbolDeclarationSyntax(local, cancellationToken); - while (true) + var oldType = GetType(oldSymbol); + var newType = GetType(newSymbol); + + if (!TypesEquivalent(oldType, newType, exact: false)) { - Debug.Assert(node != null); - if (IsClosureScope(node)) - { - return node; - } + AddRuntimeRudeEdit(newSymbol, new RudeEditDiagnostic( + RudeEditKind.ChangingCapturedVariableType, + GetSymbolLocationSpan(newSymbol, cancellationToken), + node: null, + [newSymbol.Name, oldType.ToDisplayString(ErrorDisplayFormat)])); - node = node.Parent; + continue; } - } - private static bool AreEquivalentClosureScopes(SyntaxNode? oldScope, SyntaxNode? newScope, IReadOnlyDictionary reverseMap) - { - if (oldScope == null || newScope == null) + var oldScope = GetCapturedVariableScope(oldSymbol, cancellationToken); + var newScope = GetCapturedVariableScope(newSymbol, cancellationToken); + if (!AreEquivalentClosureScopes(oldScope, newScope, bodyMap.Reverse)) { - return oldScope == newScope; + continue; } - return reverseMap.TryGetValue(newScope, out var mappedScope) && mappedScope == oldScope; + newCapturesToClosureScopes[newCaptureIndex] = newScope; + oldCapturesToClosureScopes[oldCaptureIndex] = oldScope; } - #endregion + void AddRuntimeRudeEdit(ISymbol newSymbol, RudeEditDiagnostic diagnostic) + => closureRudeEdits.TryAdd(GetCapturedVariableScope(newSymbol, cancellationToken), diagnostic); + } + + private void ReportLambdaSignatureRudeEdits( + DiagnosticContext diagnosticContext, + SyntaxNode oldLambda, + SyntaxNode newLambda, + EditAndContinueCapabilitiesGrantor capabilities, + out bool hasSignatureErrors, + CancellationToken cancellationToken) + { + hasSignatureErrors = false; - #region State Machines + Debug.Assert(IsNestedFunction(newLambda)); + Debug.Assert(IsNestedFunction(oldLambda)); - private static void ReportMissingStateMachineAttribute( - in DiagnosticContext diagnosticContext, - Compilation oldCompilation, - StateMachineInfo kinds, - CancellationToken cancellationToken) + if (IsLocalFunction(oldLambda) != IsLocalFunction(newLambda)) { - var stateMachineAttributeQualifiedName = kinds switch - { - { IsIterator: true } and { IsAsync: true } => "System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute", - { IsIterator: true } => "System.Runtime.CompilerServices.IteratorStateMachineAttribute", - { IsAsync: true } => "System.Runtime.CompilerServices.AsyncStateMachineAttribute", - _ => throw ExceptionUtilities.UnexpectedValue(kinds) - }; + diagnosticContext.Report(RudeEditKind.SwitchBetweenLambdaAndLocalFunction, cancellationToken); + hasSignatureErrors = true; + return; + } - // We assume that the attributes, if exist, are well formed. - // If not an error will be reported during EnC delta emit. + var oldLambdaSymbol = (IMethodSymbol)diagnosticContext.RequiredOldSymbol; + var newLambdaSymbol = (IMethodSymbol)diagnosticContext.RequiredNewSymbol; - // Report rude edit if the type is not found in the compilation. - // Consider: This diagnostic is cached in the document analysis, - // so it could happen that the attribute type is added later to - // the compilation and we continue to report the diagnostic. - // We could report rude edit when adding these types or flush all - // (or specific) document caches. This is not a common scenario though, - // since the attribute has been long defined in the BCL. - if (oldCompilation.GetTypeByMetadataName(stateMachineAttributeQualifiedName) == null) - { - diagnosticContext.Report(RudeEditKind.UpdatingStateMachineMethodMissingAttribute, cancellationToken, arguments: [stateMachineAttributeQualifiedName]); - } + // signature validation: + if (!ParameterTypesEquivalent(oldLambdaSymbol.Parameters, newLambdaSymbol.Parameters, exact: false)) + { + diagnosticContext.Report(RudeEditKind.ChangingLambdaParameters, cancellationToken); + hasSignatureErrors = true; + } + else if (!ReturnTypesEquivalent(oldLambdaSymbol, newLambdaSymbol, exact: false)) + { + diagnosticContext.Report(RudeEditKind.ChangingLambdaReturnType, cancellationToken); + hasSignatureErrors = true; + } + else if (!TypeParametersEquivalent(oldLambdaSymbol.TypeParameters, newLambdaSymbol.TypeParameters, exact: false) || + !oldLambdaSymbol.TypeParameters.SequenceEqual(newLambdaSymbol.TypeParameters, static (p, q) => p.Name == q.Name)) + { + diagnosticContext.Report(RudeEditKind.ChangingTypeParameters, cancellationToken); + hasSignatureErrors = true; } - #endregion + if (hasSignatureErrors) + { + return; + } - #endregion + // custom attributes - #region Helpers + ReportCustomAttributeRudeEdits(diagnosticContext, capabilities, out _, out _, cancellationToken); - private static SyntaxNode? FindPartner(OneOrMany rootNodes, OneOrMany otherRootNodes, SyntaxNode otherNode) + for (var i = 0; i < oldLambdaSymbol.Parameters.Length; i++) { - Debug.Assert(rootNodes.Count == otherRootNodes.Count); - - for (var i = 0; i < rootNodes.Count; i++) - { - var otherRootNode = otherRootNodes[i]; - if (otherRootNode.FullSpan.Contains(otherNode.SpanStart)) - { - return FindPartner(rootNodes[i], otherRootNode, otherNode); - } - } + ReportCustomAttributeRudeEdits(diagnosticContext.WithSymbols(oldLambdaSymbol.Parameters[i], newLambdaSymbol.Parameters[i]), capabilities, out _, out _, cancellationToken); + } - return null; + for (var i = 0; i < oldLambdaSymbol.TypeParameters.Length; i++) + { + ReportCustomAttributeRudeEdits(diagnosticContext.WithSymbols(oldLambdaSymbol.TypeParameters[i], newLambdaSymbol.TypeParameters[i]), capabilities, out _, out _, cancellationToken); } + } - internal static SyntaxNode FindPartner(SyntaxNode root, SyntaxNode otherRoot, SyntaxNode otherNode) + private static ITypeSymbol GetType(ISymbol localOrParameter) + => localOrParameter.Kind switch { - Debug.Assert(otherNode.SyntaxTree == otherRoot.SyntaxTree); - Debug.Assert(otherRoot.FullSpan.Contains(otherNode.SpanStart)); + SymbolKind.Parameter => ((IParameterSymbol)localOrParameter).Type, + SymbolKind.Local => ((ILocalSymbol)localOrParameter).Type, + _ => throw ExceptionUtilities.UnexpectedValue(localOrParameter.Kind), + }; - // Finding a partner of a zero-width node is complicated and not supported atm: - Debug.Assert(otherNode.FullSpan.Length > 0); + private SyntaxNode GetCapturedVariableScope(ISymbol local, CancellationToken cancellationToken) + { + Debug.Assert(local.Kind is not SymbolKind.RangeVariable); - var originalLeftNode = otherNode; - var leftPosition = otherNode.SpanStart; - otherNode = otherRoot; - var rightNode = root; + if (local is IParameterSymbol) + { + var scope = GetCapturedParameterScope(GetSymbolDeclarationSyntax(local.ContainingSymbol, cancellationToken)); + Contract.ThrowIfFalse(IsClosureScope(scope)); + return scope; + } - while (otherNode != originalLeftNode) + var node = GetSymbolDeclarationSyntax(local, cancellationToken); + while (true) + { + Debug.Assert(node != null); + if (IsClosureScope(node)) { - Debug.Assert(otherNode.RawKind == rightNode.RawKind); - var leftChild = ChildThatContainsPosition(otherNode, leftPosition, out var childIndex); - - // Can only happen when searching for zero-width node. - Debug.Assert(!leftChild.IsToken); - - rightNode = rightNode.ChildNodesAndTokens()[childIndex].AsNode()!; - otherNode = leftChild.AsNode()!; + return node; } - return rightNode; + node = node.Parent; } + } - /// - /// Returns child node or token that contains given position. - /// - /// - /// This is a copy of that also returns the index of the child node. - /// - internal static SyntaxNodeOrToken ChildThatContainsPosition(SyntaxNode self, int position, out int childIndex) + private static bool AreEquivalentClosureScopes(SyntaxNode? oldScope, SyntaxNode? newScope, IReadOnlyDictionary reverseMap) + { + if (oldScope == null || newScope == null) { - var childList = self.ChildNodesAndTokens(); - - var left = 0; - var right = childList.Count - 1; + return oldScope == newScope; + } - while (left <= right) - { - var middle = left + ((right - left) / 2); - var node = childList[middle]; + return reverseMap.TryGetValue(newScope, out var mappedScope) && mappedScope == oldScope; + } - var span = node.FullSpan; - if (position < span.Start) - { - right = middle - 1; - } - else if (position >= span.End) - { - left = middle + 1; - } - else - { - childIndex = middle; - return node; - } - } + #endregion - // we could check up front that index is within FullSpan, - // but we wan to optimize for the common case where position is valid. - Debug.Assert(!self.FullSpan.Contains(position), "Position is valid. How could we not find a child?"); - throw new ArgumentOutOfRangeException(nameof(position)); - } + #region State Machines - internal static void FindLeafNodeAndPartner(SyntaxNode leftRoot, int leftPosition, SyntaxNode rightRoot, out SyntaxNode leftNode, out SyntaxNode? rightNode) + private static void ReportMissingStateMachineAttribute( + in DiagnosticContext diagnosticContext, + Compilation oldCompilation, + StateMachineInfo kinds, + CancellationToken cancellationToken) + { + var stateMachineAttributeQualifiedName = kinds switch { - leftNode = leftRoot; - rightNode = rightRoot; - while (true) - { - if (rightNode != null && leftNode.RawKind != rightNode.RawKind) - { - rightNode = null; - } + { IsIterator: true } and { IsAsync: true } => "System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute", + { IsIterator: true } => "System.Runtime.CompilerServices.IteratorStateMachineAttribute", + { IsAsync: true } => "System.Runtime.CompilerServices.AsyncStateMachineAttribute", + _ => throw ExceptionUtilities.UnexpectedValue(kinds) + }; + + // We assume that the attributes, if exist, are well formed. + // If not an error will be reported during EnC delta emit. + + // Report rude edit if the type is not found in the compilation. + // Consider: This diagnostic is cached in the document analysis, + // so it could happen that the attribute type is added later to + // the compilation and we continue to report the diagnostic. + // We could report rude edit when adding these types or flush all + // (or specific) document caches. This is not a common scenario though, + // since the attribute has been long defined in the BCL. + if (oldCompilation.GetTypeByMetadataName(stateMachineAttributeQualifiedName) == null) + { + diagnosticContext.Report(RudeEditKind.UpdatingStateMachineMethodMissingAttribute, cancellationToken, arguments: [stateMachineAttributeQualifiedName]); + } + } - var leftChild = ChildThatContainsPosition(leftNode, leftPosition, out var childIndex); - if (leftChild.IsToken) - { - return; - } + #endregion - if (rightNode != null) - { - var rightNodeChildNodesAndTokens = rightNode.ChildNodesAndTokens(); - if (childIndex >= 0 && childIndex < rightNodeChildNodesAndTokens.Count) - { - rightNode = rightNodeChildNodesAndTokens[childIndex].AsNode(); - } - else - { - rightNode = null; - } - } + #endregion - leftNode = leftChild.AsNode()!; - } - } + #region Helpers - private static SyntaxNode? TryGetNode(SyntaxNode root, int position) - => root.FullSpan.Contains(position) ? root.FindToken(position).Parent : null; + private static SyntaxNode? FindPartner(OneOrMany rootNodes, OneOrMany otherRootNodes, SyntaxNode otherNode) + { + Debug.Assert(rootNodes.Count == otherRootNodes.Count); - internal static void AddNodes(ArrayBuilder nodes, SyntaxList list) - where T : SyntaxNode + for (var i = 0; i < rootNodes.Count; i++) { - foreach (var node in list) + var otherRootNode = otherRootNodes[i]; + if (otherRootNode.FullSpan.Contains(otherNode.SpanStart)) { - nodes.Add(node); + return FindPartner(rootNodes[i], otherRootNode, otherNode); } } - internal static void AddNodes(ArrayBuilder nodes, SeparatedSyntaxList? list) - where T : SyntaxNode - { - if (list.HasValue) - { - foreach (var node in list.Value) - { - nodes.Add(node); - } - } - } + return null; + } - private sealed class TypedConstantComparer : IEqualityComparer + internal static SyntaxNode FindPartner(SyntaxNode root, SyntaxNode otherRoot, SyntaxNode otherNode) + { + Debug.Assert(otherNode.SyntaxTree == otherRoot.SyntaxTree); + Debug.Assert(otherRoot.FullSpan.Contains(otherNode.SpanStart)); + + // Finding a partner of a zero-width node is complicated and not supported atm: + Debug.Assert(otherNode.FullSpan.Length > 0); + + var originalLeftNode = otherNode; + var leftPosition = otherNode.SpanStart; + otherNode = otherRoot; + var rightNode = root; + + while (otherNode != originalLeftNode) { - public static readonly TypedConstantComparer Instance = new(); + Debug.Assert(otherNode.RawKind == rightNode.RawKind); + var leftChild = ChildThatContainsPosition(otherNode, leftPosition, out var childIndex); - public bool Equals(TypedConstant x, TypedConstant y) - => x.Kind == y.Kind && - x.IsNull == y.IsNull && - SymbolEquivalenceComparer.Instance.Equals(x.Type, y.Type) && - x.Kind switch - { - TypedConstantKind.Array => x.Values.IsDefault || x.Values.SequenceEqual(y.Values, Instance), - TypedConstantKind.Type => TypesEquivalent((ITypeSymbol?)x.Value, (ITypeSymbol?)y.Value, exact: false), - _ => Equals(x.Value, y.Value) - }; + // Can only happen when searching for zero-width node. + Debug.Assert(!leftChild.IsToken); - public int GetHashCode(TypedConstant obj) - => obj.GetHashCode(); + rightNode = rightNode.ChildNodesAndTokens()[childIndex].AsNode()!; + otherNode = leftChild.AsNode()!; } - private sealed class NamedArgumentComparer : IEqualityComparer> - { - public static readonly NamedArgumentComparer Instance = new(); + return rightNode; + } + + /// + /// Returns child node or token that contains given position. + /// + /// + /// This is a copy of that also returns the index of the child node. + /// + internal static SyntaxNodeOrToken ChildThatContainsPosition(SyntaxNode self, int position, out int childIndex) + { + var childList = self.ChildNodesAndTokens(); - public bool Equals(KeyValuePair x, KeyValuePair y) - => x.Key.Equals(y.Key) && - TypedConstantComparer.Instance.Equals(x.Value, y.Value); + var left = 0; + var right = childList.Count - 1; - public int GetHashCode(KeyValuePair obj) - => obj.GetHashCode(); + while (left <= right) + { + var middle = left + ((right - left) / 2); + var node = childList[middle]; + + var span = node.FullSpan; + if (position < span.Start) + { + right = middle - 1; + } + else if (position >= span.End) + { + left = middle + 1; + } + else + { + childIndex = middle; + return node; + } } - private static bool IsGlobalMain(ISymbol symbol) - => symbol is IMethodSymbol { Name: WellKnownMemberNames.TopLevelStatementsEntryPointMethodName }; + // we could check up front that index is within FullSpan, + // but we wan to optimize for the common case where position is valid. + Debug.Assert(!self.FullSpan.Contains(position), "Position is valid. How could we not find a child?"); + throw new ArgumentOutOfRangeException(nameof(position)); + } - private static bool InGenericContext(ISymbol symbol) + internal static void FindLeafNodeAndPartner(SyntaxNode leftRoot, int leftPosition, SyntaxNode rightRoot, out SyntaxNode leftNode, out SyntaxNode? rightNode) + { + leftNode = leftRoot; + rightNode = rightRoot; + while (true) { - var current = symbol; + if (rightNode != null && leftNode.RawKind != rightNode.RawKind) + { + rightNode = null; + } - while (true) + var leftChild = ChildThatContainsPosition(leftNode, leftPosition, out var childIndex); + if (leftChild.IsToken) { - if (current is IMethodSymbol { Arity: > 0 }) - { - return true; - } + return; + } - if (current is INamedTypeSymbol { Arity: > 0 }) + if (rightNode != null) + { + var rightNodeChildNodesAndTokens = rightNode.ChildNodesAndTokens(); + if (childIndex >= 0 && childIndex < rightNodeChildNodesAndTokens.Count) { - return true; + rightNode = rightNodeChildNodesAndTokens[childIndex].AsNode(); } - - current = current.ContainingSymbol; - if (current == null) + else { - return false; + rightNode = null; } } + + leftNode = leftChild.AsNode()!; } + } - private bool InGenericLocalContext(SyntaxNode node, OneOrMany roots) + private static SyntaxNode? TryGetNode(SyntaxNode root, int position) + => root.FullSpan.Contains(position) ? root.FindToken(position).Parent : null; + + internal static void AddNodes(ArrayBuilder nodes, SyntaxList list) + where T : SyntaxNode + { + foreach (var node in list) { - var current = node; + nodes.Add(node); + } + } - while (true) + internal static void AddNodes(ArrayBuilder nodes, SeparatedSyntaxList? list) + where T : SyntaxNode + { + if (list.HasValue) + { + foreach (var node in list.Value) { - if (IsGenericLocalFunction(current)) - { - return true; - } - - if (roots.Contains(current)) - { - break; - } - - current = current.Parent; - Contract.ThrowIfNull(current); + nodes.Add(node); } - - return false; } + } - public IMethodSymbol? GetPrimaryConstructor(INamedTypeSymbol typeSymbol, CancellationToken cancellationToken) - => typeSymbol.InstanceConstructors.FirstOrDefault(IsPrimaryConstructor, cancellationToken); + private sealed class TypedConstantComparer : IEqualityComparer + { + public static readonly TypedConstantComparer Instance = new(); + + public bool Equals(TypedConstant x, TypedConstant y) + => x.Kind == y.Kind && + x.IsNull == y.IsNull && + SymbolEquivalenceComparer.Instance.Equals(x.Type, y.Type) && + x.Kind switch + { + TypedConstantKind.Array => x.Values.IsDefault || x.Values.SequenceEqual(y.Values, Instance), + TypedConstantKind.Type => TypesEquivalent((ITypeSymbol?)x.Value, (ITypeSymbol?)y.Value, exact: false), + _ => Equals(x.Value, y.Value) + }; + + public int GetHashCode(TypedConstant obj) + => obj.GetHashCode(); + } - // TODO: should be compiler API: https://github.com/dotnet/roslyn/issues/53092 - public bool IsPrimaryConstructor(ISymbol symbol, CancellationToken cancellationToken) - => symbol is IMethodSymbol { IsStatic: false, MethodKind: MethodKind.Constructor, DeclaringSyntaxReferences: [_] } && IsPrimaryConstructorDeclaration(GetSymbolDeclarationSyntax(symbol, cancellationToken)); + private sealed class NamedArgumentComparer : IEqualityComparer> + { + public static readonly NamedArgumentComparer Instance = new(); - /// - /// True if is a property or a field whose name matches one of the primary constructor parameter names. - /// TODO: should be compiler API: https://github.com/dotnet/roslyn/issues/54286 - /// - public bool IsPrimaryConstructorParameterMatchingSymbol(ISymbol symbol, CancellationToken cancellationToken) - => symbol is { IsStatic: false } and (IPropertySymbol or IFieldSymbol) && - GetPrimaryConstructor(symbol.ContainingType, cancellationToken) is { } primaryCtor && - primaryCtor.Parameters.Any(static (parameter, name) => parameter.Name == name, symbol.Name); + public bool Equals(KeyValuePair x, KeyValuePair y) + => x.Key.Equals(y.Key) && + TypedConstantComparer.Instance.Equals(x.Value, y.Value); - /// - /// Primary constructor that the participates in (if any), - /// i.e. the itself if it is a primary constructor, - /// or the primary constructor the member initializer of contributes to. - /// - public IMethodSymbol? GetEncompassingPrimaryConstructor(SyntaxNode declaration, ISymbol symbol, CancellationToken cancellationToken) - => IsPrimaryConstructorDeclaration(declaration) ? (IMethodSymbol)symbol : - IsDeclarationWithInitializer(declaration) ? symbol.ContainingType.InstanceConstructors.FirstOrDefault(IsPrimaryConstructor, cancellationToken) : - null; + public int GetHashCode(KeyValuePair obj) + => obj.GetHashCode(); + } - private static IPropertySymbol? GetPropertySynthesizedForRecordPrimaryConstructorParameter(IParameterSymbol parameter) - => (IPropertySymbol?)parameter.ContainingType.GetMembers(parameter.Name) - .FirstOrDefault(static m => m is IPropertySymbol { IsImplicitlyDeclared: false, GetMethod.IsImplicitlyDeclared: true, SetMethod.IsImplicitlyDeclared: true }); + private static bool IsGlobalMain(ISymbol symbol) + => symbol is IMethodSymbol { Name: WellKnownMemberNames.TopLevelStatementsEntryPointMethodName }; - /// - /// True if being inserted or deleted affects the bodies of synthesized record members. - /// - private static bool SymbolPresenceAffectsSynthesizedRecordMembers(ISymbol symbol) - => symbol is { IsStatic: false, ContainingType.IsRecord: true } and - (IPropertySymbol { GetMethod.IsImplicitlyDeclared: false, SetMethod: null or { IsImplicitlyDeclared: false } } or IFieldSymbol); + private static bool InGenericContext(ISymbol symbol) + { + var current = symbol; - /// - /// True if a syntactic delete edit of an in - /// that has a corresponding in the new compilation implies an existance - /// of a matching syntactic insert edit (either in the currently analyzed document or another one). - /// - /// The old symbol has to be explicitly declared, otherwise it couldn't have been deleted via syntactic delete edit. - /// Only detects scenarios where an insert must have occurred. False doesn't mean an insert does not exist. - /// - private bool DeleteEditImpliesInsertEdit(ISymbol oldSymbol, ISymbol newSymbol, Compilation oldCompilation, CancellationToken cancellationToken) + while (true) { - if (!newSymbol.IsSynthesized()) + if (current is IMethodSymbol { Arity: > 0 }) { return true; } - // new symbol is synthesized - check if there is an insert of another symbol that triggers the synthesis - - // Primary deconstructor is synthesized based on presence of primary constructor: - if (newSymbol is IMethodSymbol { IsStatic: false, ContainingType.IsRecord: true, ReturnsVoid: true, Name: WellKnownMemberNames.DeconstructMethodName } method && - GetPrimaryConstructor(newSymbol.ContainingType, cancellationToken) is { } newPrimaryConstructor && - method.HasDeconstructorSignature(newPrimaryConstructor)) + if (current is INamedTypeSymbol { Arity: > 0 }) { - var oldConstructor = SymbolKey.Create(newPrimaryConstructor, cancellationToken).Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; - - // An insert exists if the new primary constructor is explicitly declared and - // the old one doesn't exist, is synthesized, or is not a primary constructor parameter. - return !newPrimaryConstructor.IsSynthesized() && - (oldConstructor == null || oldConstructor.IsSynthesized() || !IsPrimaryConstructor(oldConstructor, cancellationToken)); + return true; } - // Primary property is synthesized based on presence of primary constructor parameter: - if (newSymbol is IPropertySymbol { IsStatic: false, ContainingType.IsRecord: true } && - GetPrimaryConstructor(newSymbol.ContainingType, cancellationToken)?.Parameters.FirstOrDefault( - static (parameter, name) => parameter.Name == name, newSymbol.Name) is { } newPrimaryParameter) + current = current.ContainingSymbol; + if (current == null) { - var oldParameter = SymbolKey.Create(newPrimaryParameter, cancellationToken).Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; - var oldProperty = (IPropertySymbol)oldSymbol; + return false; + } + } + } - // An insert exists if the new primary parameter is explicitly declared and - // the old one doesn't exist, is synthesized, or is not a primary constructor parameter. - // A syntax change is causing the old synthesized property to be deleted, - // so there has to be an insert inserting the new one. - // - // old: - // - // record R() { int P { get; init; } } // insert exists: oldParameter == null - // record R() { R(int P) {} int P { get; init; } } // insert exists: old constructor is not primary - // record R(int P) { int P { get; init; } } // no insert - // record R(int P); // insert exists: oldProperty is synthesized auto-prop - // - // new: - // - // record R(int P); + private bool InGenericLocalContext(SyntaxNode node, OneOrMany roots) + { + var current = node; - return !newPrimaryParameter.IsSynthesized() && - (oldParameter == null || oldParameter.IsSynthesized() || !IsPrimaryConstructor(oldParameter.ContainingSymbol, cancellationToken) || oldProperty.IsSynthesizedAutoProperty()); + while (true) + { + if (IsGenericLocalFunction(current)) + { + return true; } - // Accessor of a property is synthesized based on presence of the property: - if (newSymbol is IMethodSymbol { AssociatedSymbol: IPropertySymbol { } newProperty }) + if (roots.Contains(current)) { - var oldProperty = ((IMethodSymbol)oldSymbol).AssociatedSymbol; - Contract.ThrowIfNull(oldProperty); - - // An insert exists if an insert exists for the new property - return DeleteEditImpliesInsertEdit(oldProperty, newProperty, oldCompilation, cancellationToken); + break; } - return false; + current = current.Parent; + Contract.ThrowIfNull(current); } - private static bool HasPrintMembersSignature(IMethodSymbol method, Compilation compilation) - => method.Parameters is [var parameter] && SymbolEqualityComparer.Default.Equals(parameter.Type, compilation.GetTypeByMetadataName(typeof(StringBuilder).FullName!)); - - private static bool HasIEquatableEqualsSignature(IMethodSymbol method) - => method.Parameters is [var parameter] && SymbolEqualityComparer.Default.Equals(parameter.Type, method.ContainingType); + return false; + } - private static bool HasGetHashCodeSignature(IMethodSymbol method) - => method.Parameters is []; + public IMethodSymbol? GetPrimaryConstructor(INamedTypeSymbol typeSymbol, CancellationToken cancellationToken) + => typeSymbol.InstanceConstructors.FirstOrDefault(IsPrimaryConstructor, cancellationToken); + + // TODO: should be compiler API: https://github.com/dotnet/roslyn/issues/53092 + public bool IsPrimaryConstructor(ISymbol symbol, CancellationToken cancellationToken) + => symbol is IMethodSymbol { IsStatic: false, MethodKind: MethodKind.Constructor, DeclaringSyntaxReferences: [_] } && IsPrimaryConstructorDeclaration(GetSymbolDeclarationSyntax(symbol, cancellationToken)); + + /// + /// True if is a property or a field whose name matches one of the primary constructor parameter names. + /// TODO: should be compiler API: https://github.com/dotnet/roslyn/issues/54286 + /// + public bool IsPrimaryConstructorParameterMatchingSymbol(ISymbol symbol, CancellationToken cancellationToken) + => symbol is { IsStatic: false } and (IPropertySymbol or IFieldSymbol) && + GetPrimaryConstructor(symbol.ContainingType, cancellationToken) is { } primaryCtor && + primaryCtor.Parameters.Any(static (parameter, name) => parameter.Name == name, symbol.Name); + + /// + /// Primary constructor that the participates in (if any), + /// i.e. the itself if it is a primary constructor, + /// or the primary constructor the member initializer of contributes to. + /// + public IMethodSymbol? GetEncompassingPrimaryConstructor(SyntaxNode declaration, ISymbol symbol, CancellationToken cancellationToken) + => IsPrimaryConstructorDeclaration(declaration) ? (IMethodSymbol)symbol : + IsDeclarationWithInitializer(declaration) ? symbol.ContainingType.InstanceConstructors.FirstOrDefault(IsPrimaryConstructor, cancellationToken) : + null; + + private static IPropertySymbol? GetPropertySynthesizedForRecordPrimaryConstructorParameter(IParameterSymbol parameter) + => (IPropertySymbol?)parameter.ContainingType.GetMembers(parameter.Name) + .FirstOrDefault(static m => m is IPropertySymbol { IsImplicitlyDeclared: false, GetMethod.IsImplicitlyDeclared: true, SetMethod.IsImplicitlyDeclared: true }); + + /// + /// True if being inserted or deleted affects the bodies of synthesized record members. + /// + private static bool SymbolPresenceAffectsSynthesizedRecordMembers(ISymbol symbol) + => symbol is { IsStatic: false, ContainingType.IsRecord: true } and + (IPropertySymbol { GetMethod.IsImplicitlyDeclared: false, SetMethod: null or { IsImplicitlyDeclared: false } } or IFieldSymbol); + + /// + /// True if a syntactic delete edit of an in + /// that has a corresponding in the new compilation implies an existance + /// of a matching syntactic insert edit (either in the currently analyzed document or another one). + /// + /// The old symbol has to be explicitly declared, otherwise it couldn't have been deleted via syntactic delete edit. + /// Only detects scenarios where an insert must have occurred. False doesn't mean an insert does not exist. + /// + private bool DeleteEditImpliesInsertEdit(ISymbol oldSymbol, ISymbol newSymbol, Compilation oldCompilation, CancellationToken cancellationToken) + { + if (!newSymbol.IsSynthesized()) + { + return true; + } - #endregion + // new symbol is synthesized - check if there is an insert of another symbol that triggers the synthesis - #region Testing + // Primary deconstructor is synthesized based on presence of primary constructor: + if (newSymbol is IMethodSymbol { IsStatic: false, ContainingType.IsRecord: true, ReturnsVoid: true, Name: WellKnownMemberNames.DeconstructMethodName } method && + GetPrimaryConstructor(newSymbol.ContainingType, cancellationToken) is { } newPrimaryConstructor && + method.HasDeconstructorSignature(newPrimaryConstructor)) + { + var oldConstructor = SymbolKey.Create(newPrimaryConstructor, cancellationToken).Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; - internal TestAccessor GetTestAccessor() - => new(this); + // An insert exists if the new primary constructor is explicitly declared and + // the old one doesn't exist, is synthesized, or is not a primary constructor parameter. + return !newPrimaryConstructor.IsSynthesized() && + (oldConstructor == null || oldConstructor.IsSynthesized() || !IsPrimaryConstructor(oldConstructor, cancellationToken)); + } - internal readonly struct TestAccessor(AbstractEditAndContinueAnalyzer abstractEditAndContinueAnalyzer) + // Primary property is synthesized based on presence of primary constructor parameter: + if (newSymbol is IPropertySymbol { IsStatic: false, ContainingType.IsRecord: true } && + GetPrimaryConstructor(newSymbol.ContainingType, cancellationToken)?.Parameters.FirstOrDefault( + static (parameter, name) => parameter.Name == name, newSymbol.Name) is { } newPrimaryParameter) { - private readonly AbstractEditAndContinueAnalyzer _abstractEditAndContinueAnalyzer = abstractEditAndContinueAnalyzer; + var oldParameter = SymbolKey.Create(newPrimaryParameter, cancellationToken).Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + var oldProperty = (IPropertySymbol)oldSymbol; + + // An insert exists if the new primary parameter is explicitly declared and + // the old one doesn't exist, is synthesized, or is not a primary constructor parameter. + // A syntax change is causing the old synthesized property to be deleted, + // so there has to be an insert inserting the new one. + // + // old: + // + // record R() { int P { get; init; } } // insert exists: oldParameter == null + // record R() { R(int P) {} int P { get; init; } } // insert exists: old constructor is not primary + // record R(int P) { int P { get; init; } } // no insert + // record R(int P); // insert exists: oldProperty is synthesized auto-prop + // + // new: + // + // record R(int P); - internal void ReportTopLevelSyntacticRudeEdits(ArrayBuilder diagnostics, EditScript syntacticEdits, Dictionary editMap) - => _abstractEditAndContinueAnalyzer.ReportTopLevelSyntacticRudeEdits(diagnostics, syntacticEdits, editMap); + return !newPrimaryParameter.IsSynthesized() && + (oldParameter == null || oldParameter.IsSynthesized() || !IsPrimaryConstructor(oldParameter.ContainingSymbol, cancellationToken) || oldProperty.IsSynthesizedAutoProperty()); + } - internal DeclarationBodyMap IncludeLambdaBodyMaps( - DeclarationBodyMap bodyMap, - ArrayBuilder memberBodyActiveNodes, - ref Dictionary? lazyActiveOrMatchedLambdas) - { - return _abstractEditAndContinueAnalyzer.IncludeLambdaBodyMaps(bodyMap, memberBodyActiveNodes, ref lazyActiveOrMatchedLambdas); - } + // Accessor of a property is synthesized based on presence of the property: + if (newSymbol is IMethodSymbol { AssociatedSymbol: IPropertySymbol { } newProperty }) + { + var oldProperty = ((IMethodSymbol)oldSymbol).AssociatedSymbol; + Contract.ThrowIfNull(oldProperty); + + // An insert exists if an insert exists for the new property + return DeleteEditImpliesInsertEdit(oldProperty, newProperty, oldCompilation, cancellationToken); } - #endregion + return false; + } + + private static bool HasPrintMembersSignature(IMethodSymbol method, Compilation compilation) + => method.Parameters is [var parameter] && SymbolEqualityComparer.Default.Equals(parameter.Type, compilation.GetTypeByMetadataName(typeof(StringBuilder).FullName!)); + + private static bool HasIEquatableEqualsSignature(IMethodSymbol method) + => method.Parameters is [var parameter] && SymbolEqualityComparer.Default.Equals(parameter.Type, method.ContainingType); + + private static bool HasGetHashCodeSignature(IMethodSymbol method) + => method.Parameters is []; + + #endregion + + #region Testing + + internal TestAccessor GetTestAccessor() + => new(this); + + internal readonly struct TestAccessor(AbstractEditAndContinueAnalyzer abstractEditAndContinueAnalyzer) + { + private readonly AbstractEditAndContinueAnalyzer _abstractEditAndContinueAnalyzer = abstractEditAndContinueAnalyzer; + + internal void ReportTopLevelSyntacticRudeEdits(ArrayBuilder diagnostics, EditScript syntacticEdits, Dictionary editMap) + => _abstractEditAndContinueAnalyzer.ReportTopLevelSyntacticRudeEdits(diagnostics, syntacticEdits, editMap); + + internal DeclarationBodyMap IncludeLambdaBodyMaps( + DeclarationBodyMap bodyMap, + ArrayBuilder memberBodyActiveNodes, + ref Dictionary? lazyActiveOrMatchedLambdas) + { + return _abstractEditAndContinueAnalyzer.IncludeLambdaBodyMaps(bodyMap, memberBodyActiveNodes, ref lazyActiveOrMatchedLambdas); + } } + + #endregion } diff --git a/src/Features/Core/Portable/EditAndContinue/ActiveStatement.cs b/src/Features/Core/Portable/EditAndContinue/ActiveStatement.cs index cd62b557e4385..19701f66dfe94 100644 --- a/src/Features/Core/Portable/EditAndContinue/ActiveStatement.cs +++ b/src/Features/Core/Portable/EditAndContinue/ActiveStatement.cs @@ -6,90 +6,89 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Represents an instruction range in the code that contains an active instruction of at least one thread and that is delimited by consecutive sequence points. +/// More than one thread can share the same instance of . +/// +[DebuggerDisplay("{GetDebuggerDisplay(), nq}")] +internal sealed class ActiveStatement { /// - /// Represents an instruction range in the code that contains an active instruction of at least one thread and that is delimited by consecutive sequence points. - /// More than one thread can share the same instance of . + /// Ordinal of the active statement within the set of all active statements. /// - [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] - internal sealed class ActiveStatement + public readonly int Ordinal; + + /// + /// The instruction of the active statement that is being executed. + /// The executing version of the method might be several generations old. + /// E.g. when the thread is executing an exception handling region and hasn't been remapped yet. + /// + public readonly ManagedInstructionId InstructionId; + + /// + /// The current source span. + /// + public readonly SourceFileSpan FileSpan; + + /// + /// Aggregated across all threads. + /// + public readonly ActiveStatementFlags Flags; + + public ActiveStatement(int ordinal, ActiveStatementFlags flags, SourceFileSpan span, ManagedInstructionId instructionId) { - /// - /// Ordinal of the active statement within the set of all active statements. - /// - public readonly int Ordinal; - - /// - /// The instruction of the active statement that is being executed. - /// The executing version of the method might be several generations old. - /// E.g. when the thread is executing an exception handling region and hasn't been remapped yet. - /// - public readonly ManagedInstructionId InstructionId; - - /// - /// The current source span. - /// - public readonly SourceFileSpan FileSpan; - - /// - /// Aggregated across all threads. - /// - public readonly ActiveStatementFlags Flags; - - public ActiveStatement(int ordinal, ActiveStatementFlags flags, SourceFileSpan span, ManagedInstructionId instructionId) - { - Debug.Assert(ordinal >= 0); - - Ordinal = ordinal; - Flags = flags; - FileSpan = span; - InstructionId = instructionId; - - // IsStale implies !IsMethodUpToDate - Debug.Assert(!IsStale || !IsMethodUpToDate); - } - - public ActiveStatement WithSpan(LinePositionSpan span) - => WithFileSpan(FileSpan.WithSpan(span)); - - public ActiveStatement WithFileSpan(SourceFileSpan span) - => new(Ordinal, Flags, span, InstructionId); - - public ActiveStatement WithFlags(ActiveStatementFlags flags) - => new(Ordinal, flags, FileSpan, InstructionId); - - public LinePositionSpan Span - => FileSpan.Span; - - public string FilePath - => FileSpan.Path; - - /// - /// True if at least one of the threads whom this active statement belongs to is in a leaf frame. - /// - public bool IsLeaf - => (Flags & ActiveStatementFlags.LeafFrame) != 0; - - /// - /// True if at least one of the threads whom this active statement belongs to is in a non-leaf frame. - /// - public bool IsNonLeaf - => (Flags & ActiveStatementFlags.NonLeafFrame) != 0; - - /// - /// True if the active statement is located in a version of the method that's not the latest version of the method. - /// - public bool IsMethodUpToDate - => (Flags & ActiveStatementFlags.MethodUpToDate) != 0; - - /// - /// True if the active statement is located in a version of the method that precedes a later version that was created by Hot Reload update. - /// - public bool IsStale - => (Flags & ActiveStatementFlags.Stale) != 0; - - private string GetDebuggerDisplay() - => $"{Ordinal}: {Span}"; + Debug.Assert(ordinal >= 0); + + Ordinal = ordinal; + Flags = flags; + FileSpan = span; + InstructionId = instructionId; + + // IsStale implies !IsMethodUpToDate + Debug.Assert(!IsStale || !IsMethodUpToDate); } + + public ActiveStatement WithSpan(LinePositionSpan span) + => WithFileSpan(FileSpan.WithSpan(span)); + + public ActiveStatement WithFileSpan(SourceFileSpan span) + => new(Ordinal, Flags, span, InstructionId); + + public ActiveStatement WithFlags(ActiveStatementFlags flags) + => new(Ordinal, flags, FileSpan, InstructionId); + + public LinePositionSpan Span + => FileSpan.Span; + + public string FilePath + => FileSpan.Path; + + /// + /// True if at least one of the threads whom this active statement belongs to is in a leaf frame. + /// + public bool IsLeaf + => (Flags & ActiveStatementFlags.LeafFrame) != 0; + + /// + /// True if at least one of the threads whom this active statement belongs to is in a non-leaf frame. + /// + public bool IsNonLeaf + => (Flags & ActiveStatementFlags.NonLeafFrame) != 0; + + /// + /// True if the active statement is located in a version of the method that's not the latest version of the method. + /// + public bool IsMethodUpToDate + => (Flags & ActiveStatementFlags.MethodUpToDate) != 0; + + /// + /// True if the active statement is located in a version of the method that precedes a later version that was created by Hot Reload update. + /// + public bool IsStale + => (Flags & ActiveStatementFlags.Stale) != 0; + + private string GetDebuggerDisplay() + => $"{Ordinal}: {Span}"; } diff --git a/src/Features/Core/Portable/EditAndContinue/ActiveStatementExceptionRegions.cs b/src/Features/Core/Portable/EditAndContinue/ActiveStatementExceptionRegions.cs index f6eb6aaa01fcd..1e81372175f8b 100644 --- a/src/Features/Core/Portable/EditAndContinue/ActiveStatementExceptionRegions.cs +++ b/src/Features/Core/Portable/EditAndContinue/ActiveStatementExceptionRegions.cs @@ -5,26 +5,25 @@ using System.Collections.Immutable; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal readonly struct ActiveStatementExceptionRegions { - internal readonly struct ActiveStatementExceptionRegions - { - /// - /// Exception region spans corresponding to an active statement. - /// - public readonly ImmutableArray Spans; + /// + /// Exception region spans corresponding to an active statement. + /// + public readonly ImmutableArray Spans; - /// - /// True if the active statement is covered by any of the exception region spans. - /// - public readonly bool IsActiveStatementCovered; + /// + /// True if the active statement is covered by any of the exception region spans. + /// + public readonly bool IsActiveStatementCovered; - public ActiveStatementExceptionRegions(ImmutableArray spans, bool isActiveStatementCovered) - { - Contract.ThrowIfTrue(spans.IsDefault); + public ActiveStatementExceptionRegions(ImmutableArray spans, bool isActiveStatementCovered) + { + Contract.ThrowIfTrue(spans.IsDefault); - Spans = spans; - IsActiveStatementCovered = isActiveStatementCovered; - } + Spans = spans; + IsActiveStatementCovered = isActiveStatementCovered; } } diff --git a/src/Features/Core/Portable/EditAndContinue/ActiveStatementId.cs b/src/Features/Core/Portable/EditAndContinue/ActiveStatementId.cs index a5da8bb94dc2d..ff18daeb358e4 100644 --- a/src/Features/Core/Portable/EditAndContinue/ActiveStatementId.cs +++ b/src/Features/Core/Portable/EditAndContinue/ActiveStatementId.cs @@ -4,11 +4,10 @@ #nullable disable -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal readonly struct ActiveStatementId(DocumentId documentId, int ordinal) { - internal readonly struct ActiveStatementId(DocumentId documentId, int ordinal) - { - public readonly DocumentId DocumentId = documentId; - public readonly int Ordinal = ordinal; - } + public readonly DocumentId DocumentId = documentId; + public readonly int Ordinal = ordinal; } diff --git a/src/Features/Core/Portable/EditAndContinue/ActiveStatementProvider.cs b/src/Features/Core/Portable/EditAndContinue/ActiveStatementProvider.cs index cc8c99debd4c4..d6c453cca23c0 100644 --- a/src/Features/Core/Portable/EditAndContinue/ActiveStatementProvider.cs +++ b/src/Features/Core/Portable/EditAndContinue/ActiveStatementProvider.cs @@ -6,10 +6,9 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.EditAndContinue -{ - /// - /// Provides active statement spans within the specified document of a solution. - /// - internal delegate ValueTask> ActiveStatementSpanProvider(DocumentId? documentId, string filePath, CancellationToken cancellationToken); -} +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Provides active statement spans within the specified document of a solution. +/// +internal delegate ValueTask> ActiveStatementSpanProvider(DocumentId? documentId, string filePath, CancellationToken cancellationToken); diff --git a/src/Features/Core/Portable/EditAndContinue/ActiveStatementSpan.cs b/src/Features/Core/Portable/EditAndContinue/ActiveStatementSpan.cs index ae4c823ecdc90..105af64b2ca55 100644 --- a/src/Features/Core/Portable/EditAndContinue/ActiveStatementSpan.cs +++ b/src/Features/Core/Portable/EditAndContinue/ActiveStatementSpan.cs @@ -11,59 +11,58 @@ using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Represents a span of an active statement tracked by the client editor. +/// +[DataContract] +internal readonly struct ActiveStatementSpan : IEquatable { /// - /// Represents a span of an active statement tracked by the client editor. + /// The corresponding . /// - [DataContract] - internal readonly struct ActiveStatementSpan : IEquatable - { - /// - /// The corresponding . - /// - [DataMember(Order = 0)] - public readonly int Ordinal; + [DataMember(Order = 0)] + public readonly int Ordinal; - /// - /// Line span in the mapped document. - /// - [DataMember(Order = 1)] - public readonly LinePositionSpan LineSpan; + /// + /// Line span in the mapped document. + /// + [DataMember(Order = 1)] + public readonly LinePositionSpan LineSpan; - /// - /// Flags. - /// - [DataMember(Order = 2)] - public readonly ActiveStatementFlags Flags; + /// + /// Flags. + /// + [DataMember(Order = 2)] + public readonly ActiveStatementFlags Flags; - /// - /// The id of the unmapped document where the source of the active statement is and from where the statement might be mapped to via #line directive. - /// Null if unknown (not determined yet). - /// - [DataMember(Order = 3)] - public readonly DocumentId? UnmappedDocumentId; + /// + /// The id of the unmapped document where the source of the active statement is and from where the statement might be mapped to via #line directive. + /// Null if unknown (not determined yet). + /// + [DataMember(Order = 3)] + public readonly DocumentId? UnmappedDocumentId; - public ActiveStatementSpan(int ordinal, LinePositionSpan lineSpan, ActiveStatementFlags flags, DocumentId? unmappedDocumentId) - { - Debug.Assert(ordinal >= 0); + public ActiveStatementSpan(int ordinal, LinePositionSpan lineSpan, ActiveStatementFlags flags, DocumentId? unmappedDocumentId) + { + Debug.Assert(ordinal >= 0); - Ordinal = ordinal; - LineSpan = lineSpan; - Flags = flags; - UnmappedDocumentId = unmappedDocumentId; - } + Ordinal = ordinal; + LineSpan = lineSpan; + Flags = flags; + UnmappedDocumentId = unmappedDocumentId; + } - public override bool Equals(object? obj) - => obj is ActiveStatementSpan other && Equals(other); + public override bool Equals(object? obj) + => obj is ActiveStatementSpan other && Equals(other); - public bool Equals(ActiveStatementSpan other) - => Ordinal.Equals(other.Ordinal) && - LineSpan.Equals(other.LineSpan) && - Flags == other.Flags && - UnmappedDocumentId == other.UnmappedDocumentId; + public bool Equals(ActiveStatementSpan other) + => Ordinal.Equals(other.Ordinal) && + LineSpan.Equals(other.LineSpan) && + Flags == other.Flags && + UnmappedDocumentId == other.UnmappedDocumentId; - public override int GetHashCode() - => Hash.Combine(Ordinal, Hash.Combine(LineSpan.GetHashCode(), Hash.Combine(UnmappedDocumentId, (int)Flags))); - } + public override int GetHashCode() + => Hash.Combine(Ordinal, Hash.Combine(LineSpan.GetHashCode(), Hash.Combine(UnmappedDocumentId, (int)Flags))); } diff --git a/src/Features/Core/Portable/EditAndContinue/ActiveStatementsMap.cs b/src/Features/Core/Portable/EditAndContinue/ActiveStatementsMap.cs index 3ea9546ebbc30..f2f503d49ee6d 100644 --- a/src/Features/Core/Portable/EditAndContinue/ActiveStatementsMap.cs +++ b/src/Features/Core/Portable/EditAndContinue/ActiveStatementsMap.cs @@ -14,320 +14,319 @@ using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal sealed class ActiveStatementsMap { - internal sealed class ActiveStatementsMap + public static readonly ActiveStatementsMap Empty = + new(ImmutableDictionary>.Empty, + ImmutableDictionary.Empty); + + public static readonly Comparer Comparer = + Comparer.Create((x, y) => x.FileSpan.Start.CompareTo(y.FileSpan.Start)); + + private static readonly Comparer<(ManagedActiveStatementDebugInfo, SourceFileSpan, int)> s_infoSpanComparer = + Comparer<(ManagedActiveStatementDebugInfo, SourceFileSpan span, int)>.Create((x, y) => x.span.Start.CompareTo(y.span.Start)); + + /// + /// Groups active statements by document path as listed in the PDB. + /// Within each group the statements are ordered by their start position. + /// + public readonly IReadOnlyDictionary> DocumentPathMap; + + /// + /// Active statements by instruction id. + /// + public readonly IReadOnlyDictionary InstructionMap; + + /// + /// Maps syntax tree to active statements with calculated unmapped spans. + /// + private ImmutableDictionary> _lazyOldDocumentActiveStatements; + + public ActiveStatementsMap( + IReadOnlyDictionary> documentPathMap, + IReadOnlyDictionary instructionMap) { - public static readonly ActiveStatementsMap Empty = - new(ImmutableDictionary>.Empty, - ImmutableDictionary.Empty); - - public static readonly Comparer Comparer = - Comparer.Create((x, y) => x.FileSpan.Start.CompareTo(y.FileSpan.Start)); - - private static readonly Comparer<(ManagedActiveStatementDebugInfo, SourceFileSpan, int)> s_infoSpanComparer = - Comparer<(ManagedActiveStatementDebugInfo, SourceFileSpan span, int)>.Create((x, y) => x.span.Start.CompareTo(y.span.Start)); - - /// - /// Groups active statements by document path as listed in the PDB. - /// Within each group the statements are ordered by their start position. - /// - public readonly IReadOnlyDictionary> DocumentPathMap; - - /// - /// Active statements by instruction id. - /// - public readonly IReadOnlyDictionary InstructionMap; - - /// - /// Maps syntax tree to active statements with calculated unmapped spans. - /// - private ImmutableDictionary> _lazyOldDocumentActiveStatements; - - public ActiveStatementsMap( - IReadOnlyDictionary> documentPathMap, - IReadOnlyDictionary instructionMap) - { - Debug.Assert(documentPathMap.All(entry => entry.Value.IsSorted(Comparer))); - - DocumentPathMap = documentPathMap; - InstructionMap = instructionMap; - - _lazyOldDocumentActiveStatements = ImmutableDictionary>.Empty; - } - - public static ActiveStatementsMap Create( - ImmutableArray debugInfos, - ImmutableDictionary> remapping) - { - using var _1 = PooledDictionary>.GetInstance(out var updatedSpansByDocumentPath); + Debug.Assert(documentPathMap.All(entry => entry.Value.IsSorted(Comparer))); - var ordinal = 0; - foreach (var debugInfo in debugInfos) - { - var documentName = debugInfo.DocumentName; - if (documentName == null) - { - // Ignore active statements that do not have a source location. - continue; - } - - if (!TryGetUpToDateSpan(debugInfo, remapping, out var baseSpan)) - { - continue; - } + DocumentPathMap = documentPathMap; + InstructionMap = instructionMap; - if (!updatedSpansByDocumentPath.TryGetValue(documentName, out var documentInfos)) - { - updatedSpansByDocumentPath.Add(documentName, documentInfos = ArrayBuilder<(ManagedActiveStatementDebugInfo, SourceFileSpan, int)>.GetInstance()); - } + _lazyOldDocumentActiveStatements = ImmutableDictionary>.Empty; + } - documentInfos.Add((debugInfo, new SourceFileSpan(documentName, baseSpan), ordinal++)); - } + public static ActiveStatementsMap Create( + ImmutableArray debugInfos, + ImmutableDictionary> remapping) + { + using var _1 = PooledDictionary>.GetInstance(out var updatedSpansByDocumentPath); - foreach (var (_, infos) in updatedSpansByDocumentPath) + var ordinal = 0; + foreach (var debugInfo in debugInfos) + { + var documentName = debugInfo.DocumentName; + if (documentName == null) { - infos.Sort(s_infoSpanComparer); + // Ignore active statements that do not have a source location. + continue; } - var byDocumentPath = updatedSpansByDocumentPath.ToImmutableDictionary( - keySelector: entry => entry.Key, - elementSelector: entry => entry.Value.SelectAsArray(item => new ActiveStatement( - ordinal: item.ordinal, - flags: item.info.Flags, - span: item.span, - instructionId: item.info.ActiveInstruction))); - - using var _2 = PooledDictionary.GetInstance(out var byInstruction); - - foreach (var (_, statements) in byDocumentPath) + if (!TryGetUpToDateSpan(debugInfo, remapping, out var baseSpan)) { - foreach (var statement in statements) - { - try - { - byInstruction.Add(statement.InstructionId, statement); - } - catch (ArgumentException) - { - throw new InvalidOperationException($"Multiple active statements with the same instruction id returned by Active Statement Provider"); - } - } + continue; } - // TODO: Remove. Workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1830914. - if (EditAndContinueMethodDebugInfoReader.IgnoreCaseWhenComparingDocumentNames) + if (!updatedSpansByDocumentPath.TryGetValue(documentName, out var documentInfos)) { - byDocumentPath = byDocumentPath.WithComparers(keyComparer: StringComparer.OrdinalIgnoreCase); + updatedSpansByDocumentPath.Add(documentName, documentInfos = ArrayBuilder<(ManagedActiveStatementDebugInfo, SourceFileSpan, int)>.GetInstance()); } - return new ActiveStatementsMap(byDocumentPath, byInstruction.ToImmutableDictionary()); + documentInfos.Add((debugInfo, new SourceFileSpan(documentName, baseSpan), ordinal++)); } - private static bool TryGetUpToDateSpan(ManagedActiveStatementDebugInfo activeStatementInfo, ImmutableDictionary> remapping, out LinePositionSpan newSpan) + foreach (var (_, infos) in updatedSpansByDocumentPath) { - // Drop stale active statements - their location in the current snapshot is unknown. - if (activeStatementInfo.Flags.HasFlag(ActiveStatementFlags.Stale)) - { - newSpan = default; - return false; - } + infos.Sort(s_infoSpanComparer); + } - var activeSpan = activeStatementInfo.SourceSpan.ToLinePositionSpan(); - if (activeStatementInfo.Flags.HasFlag(ActiveStatementFlags.MethodUpToDate)) - { - newSpan = activeSpan; - return true; - } + var byDocumentPath = updatedSpansByDocumentPath.ToImmutableDictionary( + keySelector: entry => entry.Key, + elementSelector: entry => entry.Value.SelectAsArray(item => new ActiveStatement( + ordinal: item.ordinal, + flags: item.info.Flags, + span: item.span, + instructionId: item.info.ActiveInstruction))); - var instructionId = activeStatementInfo.ActiveInstruction; + using var _2 = PooledDictionary.GetInstance(out var byInstruction); - // Map active statement spans in non-remappable regions to the latest source locations. - if (remapping.TryGetValue(instructionId.Method, out var regionsInMethod)) + foreach (var (_, statements) in byDocumentPath) + { + foreach (var statement in statements) { - // Note that active statement spans can be nested. For example, - // [|var x = y switch { 1 => 0, _ => [|1|] };|] - - foreach (var region in regionsInMethod) + try { - if (!region.IsExceptionRegion && - region.OldSpan.Span == activeSpan && - activeStatementInfo.DocumentName == region.OldSpan.Path) - { - newSpan = region.NewSpan.Span; - return true; - } + byInstruction.Add(statement.InstructionId, statement); + } + catch (ArgumentException) + { + throw new InvalidOperationException($"Multiple active statements with the same instruction id returned by Active Statement Provider"); } } + } - // The active statement is in a method instance that was updated during Hot Reload session, - // at which point the location of the span was not known. - newSpan = default; - return false; + // TODO: Remove. Workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1830914. + if (EditAndContinueMethodDebugInfoReader.IgnoreCaseWhenComparingDocumentNames) + { + byDocumentPath = byDocumentPath.WithComparers(keyComparer: StringComparer.OrdinalIgnoreCase); } - public bool IsEmpty - => InstructionMap.IsEmpty(); + return new ActiveStatementsMap(byDocumentPath, byInstruction.ToImmutableDictionary()); + } - internal async ValueTask> GetOldActiveStatementsAsync(IEditAndContinueAnalyzer analyzer, Document oldDocument, CancellationToken cancellationToken) + private static bool TryGetUpToDateSpan(ManagedActiveStatementDebugInfo activeStatementInfo, ImmutableDictionary> remapping, out LinePositionSpan newSpan) + { + // Drop stale active statements - their location in the current snapshot is unknown. + if (activeStatementInfo.Flags.HasFlag(ActiveStatementFlags.Stale)) { - var oldTree = await oldDocument.DocumentState.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var oldRoot = await oldTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var oldText = await oldTree.GetTextAsync(cancellationToken).ConfigureAwait(false); - return GetOldActiveStatements(analyzer, oldTree, oldText, oldRoot, cancellationToken); + newSpan = default; + return false; } - internal ImmutableArray GetOldActiveStatements(IEditAndContinueAnalyzer analyzer, SyntaxTree oldSyntaxTree, SourceText oldText, SyntaxNode oldRoot, CancellationToken cancellationToken) + var activeSpan = activeStatementInfo.SourceSpan.ToLinePositionSpan(); + if (activeStatementInfo.Flags.HasFlag(ActiveStatementFlags.MethodUpToDate)) { - Debug.Assert(oldRoot.SyntaxTree == oldSyntaxTree); - - return ImmutableInterlocked.GetOrAdd( - ref _lazyOldDocumentActiveStatements, - oldSyntaxTree, - oldSyntaxTree => CalculateOldActiveStatementsAndExceptionRegions(analyzer, oldSyntaxTree, oldText, oldRoot, cancellationToken)); + newSpan = activeSpan; + return true; } - private ImmutableArray CalculateOldActiveStatementsAndExceptionRegions(IEditAndContinueAnalyzer analyzer, SyntaxTree oldTree, SourceText oldText, SyntaxNode oldRoot, CancellationToken cancellationToken) + var instructionId = activeStatementInfo.ActiveInstruction; + + // Map active statement spans in non-remappable regions to the latest source locations. + if (remapping.TryGetValue(instructionId.Method, out var regionsInMethod)) { - using var _1 = ArrayBuilder.GetInstance(out var builder); - using var _2 = PooledHashSet.GetInstance(out var mappedStatements); + // Note that active statement spans can be nested. For example, + // [|var x = y switch { 1 => 0, _ => [|1|] };|] - void AddStatement(LinePositionSpan unmappedLineSpan, ActiveStatement activeStatement) + foreach (var region in regionsInMethod) { - // Protect against stale/invalid active statement spans read from the PDB. - // Also guard against active statements unmapped to multiple locations in the unmapped file - // (when multiple #line directives map to the same span that overlaps with the active statement). - if (TryGetTextSpan(oldText.Lines, unmappedLineSpan, out var unmappedSpan) && - oldRoot.FullSpan.Contains(unmappedSpan.Start) && - mappedStatements.Add(activeStatement)) + if (!region.IsExceptionRegion && + region.OldSpan.Span == activeSpan && + activeStatementInfo.DocumentName == region.OldSpan.Path) { - var exceptionRegions = analyzer.GetExceptionRegions(oldRoot, unmappedSpan, activeStatement.IsNonLeaf, cancellationToken); - builder.Add(new UnmappedActiveStatement(unmappedSpan, activeStatement, exceptionRegions)); + newSpan = region.NewSpan.Span; + return true; } } + } + + // The active statement is in a method instance that was updated during Hot Reload session, + // at which point the location of the span was not known. + newSpan = default; + return false; + } + + public bool IsEmpty + => InstructionMap.IsEmpty(); + + internal async ValueTask> GetOldActiveStatementsAsync(IEditAndContinueAnalyzer analyzer, Document oldDocument, CancellationToken cancellationToken) + { + var oldTree = await oldDocument.DocumentState.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var oldRoot = await oldTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var oldText = await oldTree.GetTextAsync(cancellationToken).ConfigureAwait(false); + return GetOldActiveStatements(analyzer, oldTree, oldText, oldRoot, cancellationToken); + } + + internal ImmutableArray GetOldActiveStatements(IEditAndContinueAnalyzer analyzer, SyntaxTree oldSyntaxTree, SourceText oldText, SyntaxNode oldRoot, CancellationToken cancellationToken) + { + Debug.Assert(oldRoot.SyntaxTree == oldSyntaxTree); - var hasAnyLineDirectives = false; - foreach (var lineMapping in oldTree.GetLineMappings(cancellationToken)) + return ImmutableInterlocked.GetOrAdd( + ref _lazyOldDocumentActiveStatements, + oldSyntaxTree, + oldSyntaxTree => CalculateOldActiveStatementsAndExceptionRegions(analyzer, oldSyntaxTree, oldText, oldRoot, cancellationToken)); + } + + private ImmutableArray CalculateOldActiveStatementsAndExceptionRegions(IEditAndContinueAnalyzer analyzer, SyntaxTree oldTree, SourceText oldText, SyntaxNode oldRoot, CancellationToken cancellationToken) + { + using var _1 = ArrayBuilder.GetInstance(out var builder); + using var _2 = PooledHashSet.GetInstance(out var mappedStatements); + + void AddStatement(LinePositionSpan unmappedLineSpan, ActiveStatement activeStatement) + { + // Protect against stale/invalid active statement spans read from the PDB. + // Also guard against active statements unmapped to multiple locations in the unmapped file + // (when multiple #line directives map to the same span that overlaps with the active statement). + if (TryGetTextSpan(oldText.Lines, unmappedLineSpan, out var unmappedSpan) && + oldRoot.FullSpan.Contains(unmappedSpan.Start) && + mappedStatements.Add(activeStatement)) { - var unmappedSection = lineMapping.Span; - var mappedSection = lineMapping.MappedSpan; + var exceptionRegions = analyzer.GetExceptionRegions(oldRoot, unmappedSpan, activeStatement.IsNonLeaf, cancellationToken); + builder.Add(new UnmappedActiveStatement(unmappedSpan, activeStatement, exceptionRegions)); + } + } - hasAnyLineDirectives = true; + var hasAnyLineDirectives = false; + foreach (var lineMapping in oldTree.GetLineMappings(cancellationToken)) + { + var unmappedSection = lineMapping.Span; + var mappedSection = lineMapping.MappedSpan; + + hasAnyLineDirectives = true; + + var targetPath = mappedSection.HasMappedPath ? mappedSection.Path : oldTree.FilePath; - var targetPath = mappedSection.HasMappedPath ? mappedSection.Path : oldTree.FilePath; + if (DocumentPathMap.TryGetValue(targetPath, out var activeStatementsInMappedFile)) + { + var range = GetSpansStartingInSpan( + mappedSection.Span.Start, + mappedSection.Span.End, + activeStatementsInMappedFile, + startPositionComparer: (x, y) => x.Span.Start.CompareTo(y)); - if (DocumentPathMap.TryGetValue(targetPath, out var activeStatementsInMappedFile)) + for (var i = range.Start.Value; i < range.End.Value; i++) { - var range = GetSpansStartingInSpan( - mappedSection.Span.Start, - mappedSection.Span.End, - activeStatementsInMappedFile, - startPositionComparer: (x, y) => x.Span.Start.CompareTo(y)); - - for (var i = range.Start.Value; i < range.End.Value; i++) - { - var activeStatement = activeStatementsInMappedFile[i]; - var unmappedLineSpan = ReverseMapLinePositionSpan(unmappedSection, mappedSection.Span, activeStatement.Span); - - AddStatement(unmappedLineSpan, activeStatement); - } + var activeStatement = activeStatementsInMappedFile[i]; + var unmappedLineSpan = ReverseMapLinePositionSpan(unmappedSection, mappedSection.Span, activeStatement.Span); + + AddStatement(unmappedLineSpan, activeStatement); } } + } - if (!hasAnyLineDirectives) - { - Debug.Assert(builder.IsEmpty()); + if (!hasAnyLineDirectives) + { + Debug.Assert(builder.IsEmpty()); - if (DocumentPathMap.TryGetValue(oldTree.FilePath, out var activeStatements)) + if (DocumentPathMap.TryGetValue(oldTree.FilePath, out var activeStatements)) + { + foreach (var activeStatement in activeStatements) { - foreach (var activeStatement in activeStatements) - { - AddStatement(activeStatement.Span, activeStatement); - } + AddStatement(activeStatement.Span, activeStatement); } } + } - Debug.Assert(builder.IsSorted(Comparer.Create((x, y) => x.UnmappedSpan.Start.CompareTo(y.UnmappedSpan.End)))); + Debug.Assert(builder.IsSorted(Comparer.Create((x, y) => x.UnmappedSpan.Start.CompareTo(y.UnmappedSpan.End)))); - return builder.ToImmutable(); - } + return builder.ToImmutable(); + } - private static LinePositionSpan ReverseMapLinePositionSpan(LinePositionSpan unmappedSection, LinePositionSpan mappedSection, LinePositionSpan mappedSpan) - { - var lineDifference = unmappedSection.Start.Line - mappedSection.Start.Line; - var unmappedStartLine = mappedSpan.Start.Line + lineDifference; - var unmappedEndLine = mappedSpan.End.Line + lineDifference; + private static LinePositionSpan ReverseMapLinePositionSpan(LinePositionSpan unmappedSection, LinePositionSpan mappedSection, LinePositionSpan mappedSpan) + { + var lineDifference = unmappedSection.Start.Line - mappedSection.Start.Line; + var unmappedStartLine = mappedSpan.Start.Line + lineDifference; + var unmappedEndLine = mappedSpan.End.Line + lineDifference; - var unmappedStartColumn = (mappedSpan.Start.Line == mappedSection.Start.Line) - ? unmappedSection.Start.Character + mappedSpan.Start.Character - mappedSection.Start.Character - : mappedSpan.Start.Character; + var unmappedStartColumn = (mappedSpan.Start.Line == mappedSection.Start.Line) + ? unmappedSection.Start.Character + mappedSpan.Start.Character - mappedSection.Start.Character + : mappedSpan.Start.Character; - var unmappedEndColumn = (mappedSpan.End.Line == mappedSection.Start.Line) - ? unmappedSection.Start.Character + mappedSpan.End.Character - mappedSection.Start.Character - : mappedSpan.End.Character; + var unmappedEndColumn = (mappedSpan.End.Line == mappedSection.Start.Line) + ? unmappedSection.Start.Character + mappedSpan.End.Character - mappedSection.Start.Character + : mappedSpan.End.Character; - return new(new(unmappedStartLine, unmappedStartColumn), new(unmappedEndLine, unmappedEndColumn)); - } + return new(new(unmappedStartLine, unmappedStartColumn), new(unmappedEndLine, unmappedEndColumn)); + } - private static bool TryGetTextSpan(TextLineCollection lines, LinePositionSpan lineSpan, out TextSpan span) + private static bool TryGetTextSpan(TextLineCollection lines, LinePositionSpan lineSpan, out TextSpan span) + { + if (lineSpan.Start.Line >= lines.Count || lineSpan.End.Line >= lines.Count) { - if (lineSpan.Start.Line >= lines.Count || lineSpan.End.Line >= lines.Count) - { - span = default; - return false; - } - - var start = lines[lineSpan.Start.Line].Start + lineSpan.Start.Character; - var end = lines[lineSpan.End.Line].Start + lineSpan.End.Character; - span = TextSpan.FromBounds(start, end); - return true; + span = default; + return false; } - /// - /// Since an active statement represents a range between two sequence points and its span is associated with the first of these sequence points, - /// we decide whether the active statement is relevant within given span by checking whether its start location is within that span. - /// An active statement may overlap a span even if its starting location is not in the span, but such active statement is not relevant - /// for analysis of code within the given span. - /// - /// Assumes that are sorted by their start position. - /// - internal static Range GetSpansStartingInSpan( - TPosition spanStart, - TPosition spanEnd, - ImmutableArray spans, - Func startPositionComparer) - { - var start = spans.BinarySearch(spanStart, startPositionComparer); - if (start < 0) - { - // ~start points to the next span whose start position is greater than span start position: - start = ~start; - } + var start = lines[lineSpan.Start.Line].Start + lineSpan.Start.Character; + var end = lines[lineSpan.End.Line].Start + lineSpan.End.Character; + span = TextSpan.FromBounds(start, end); + return true; + } - if (start == spans.Length) - { - return default; - } + /// + /// Since an active statement represents a range between two sequence points and its span is associated with the first of these sequence points, + /// we decide whether the active statement is relevant within given span by checking whether its start location is within that span. + /// An active statement may overlap a span even if its starting location is not in the span, but such active statement is not relevant + /// for analysis of code within the given span. + /// + /// Assumes that are sorted by their start position. + /// + internal static Range GetSpansStartingInSpan( + TPosition spanStart, + TPosition spanEnd, + ImmutableArray spans, + Func startPositionComparer) + { + var start = spans.BinarySearch(spanStart, startPositionComparer); + if (start < 0) + { + // ~start points to the next span whose start position is greater than span start position: + start = ~start; + } - var length = spans.AsSpan()[start..].BinarySearch(spanEnd, startPositionComparer); - if (length < 0) - { - // ~length points to the next span whose start position is greater than span start position: - length = ~length; - } + if (start == spans.Length) + { + return default; + } - while (start > 0 && startPositionComparer(spans[start - 1], spanStart) == 0) - { - start--; - } + var length = spans.AsSpan()[start..].BinarySearch(spanEnd, startPositionComparer); + if (length < 0) + { + // ~length points to the next span whose start position is greater than span start position: + length = ~length; + } - var end = start + length; - while (end < spans.Length && startPositionComparer(spans[end], spanStart) == 0) - { - end++; - } + while (start > 0 && startPositionComparer(spans[start - 1], spanStart) == 0) + { + start--; + } - return new Range(start, end); + var end = start + length; + while (end < spans.Length && startPositionComparer(spans[end], spanStart) == 0) + { + end++; } + + return new Range(start, end); } } diff --git a/src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs b/src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs index 57e287e327b3b..7f07707a5c27c 100644 --- a/src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs +++ b/src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs @@ -18,509 +18,508 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Encapsulates access to the last committed solution. +/// We don't want to expose the solution directly since access to documents must be gated by out-of-sync checks. +/// +internal sealed class CommittedSolution { - /// - /// Encapsulates access to the last committed solution. - /// We don't want to expose the solution directly since access to documents must be gated by out-of-sync checks. - /// - internal sealed class CommittedSolution + private readonly DebuggingSession _debuggingSession; + + private Solution _solution; + + internal enum DocumentState { - private readonly DebuggingSession _debuggingSession; + None = 0, - private Solution _solution; + /// + /// The current document content does not match the content the module was compiled with. + /// This document state may change to or . + /// + OutOfSync = 1, - internal enum DocumentState - { - None = 0, - - /// - /// The current document content does not match the content the module was compiled with. - /// This document state may change to or . - /// - OutOfSync = 1, - - /// - /// It hasn't been possible to determine whether the current document content does matches the content - /// the module was compiled with due to error while reading the PDB or the source file. - /// This document state may change to or . - /// - Indeterminate = 2, - - /// - /// The document is not compiled into the module. It's only included in the project - /// to support design-time features such as completion, etc. - /// This is a final state. Once a document is in this state it won't switch to a different one. - /// - DesignTimeOnly = 3, - - /// - /// The current document content matches the content the built module was compiled with. - /// This is a final state. Once a document is in this state it won't switch to a different one. - /// - MatchesBuildOutput = 4 - } + /// + /// It hasn't been possible to determine whether the current document content does matches the content + /// the module was compiled with due to error while reading the PDB or the source file. + /// This document state may change to or . + /// + Indeterminate = 2, /// - /// Implements workaround for https://github.com/dotnet/project-system/issues/5457. - /// - /// When debugging is started we capture the current solution snapshot. - /// The documents in this snapshot might not match exactly to those that the compiler used to build the module - /// that's currently loaded into the debuggee. This is because there is no reliable synchronization between - /// the (design-time) build and Roslyn workspace. Although Roslyn uses file-watchers to watch for changes in - /// the files on disk, the file-changed events raised by the build might arrive to Roslyn after the debugger - /// has attached to the debuggee and EnC service captured the solution. - /// - /// Ideally, the Project System would notify Roslyn at the end of each build what the content of the source - /// files generated by various targets is. Roslyn would then apply these changes to the workspace and - /// the EnC service would capture a solution snapshot that includes these changes. - /// - /// Since this notification is currently not available we check the current content of source files against - /// the corresponding checksums stored in the PDB. Documents for which we have not observed source file content - /// that maches the PDB checksum are considered . - /// - /// Some documents in the workspace are added for design-time-only purposes and are not part of the compilation - /// from which the assembly is built. These documents won't have a record in the PDB and will be tracked as - /// . - /// - /// A document state can only change from to . - /// Once a document state is or - /// it will never change. + /// The document is not compiled into the module. It's only included in the project + /// to support design-time features such as completion, etc. + /// This is a final state. Once a document is in this state it won't switch to a different one. /// - private readonly Dictionary _documentState = []; + DesignTimeOnly = 3, - private readonly object _guard = new(); + /// + /// The current document content matches the content the built module was compiled with. + /// This is a final state. Once a document is in this state it won't switch to a different one. + /// + MatchesBuildOutput = 4 + } - public CommittedSolution(DebuggingSession debuggingSession, Solution solution, IEnumerable> initialDocumentStates) - { - _solution = solution; - _debuggingSession = debuggingSession; - _documentState.AddRange(initialDocumentStates); - } + /// + /// Implements workaround for https://github.com/dotnet/project-system/issues/5457. + /// + /// When debugging is started we capture the current solution snapshot. + /// The documents in this snapshot might not match exactly to those that the compiler used to build the module + /// that's currently loaded into the debuggee. This is because there is no reliable synchronization between + /// the (design-time) build and Roslyn workspace. Although Roslyn uses file-watchers to watch for changes in + /// the files on disk, the file-changed events raised by the build might arrive to Roslyn after the debugger + /// has attached to the debuggee and EnC service captured the solution. + /// + /// Ideally, the Project System would notify Roslyn at the end of each build what the content of the source + /// files generated by various targets is. Roslyn would then apply these changes to the workspace and + /// the EnC service would capture a solution snapshot that includes these changes. + /// + /// Since this notification is currently not available we check the current content of source files against + /// the corresponding checksums stored in the PDB. Documents for which we have not observed source file content + /// that maches the PDB checksum are considered . + /// + /// Some documents in the workspace are added for design-time-only purposes and are not part of the compilation + /// from which the assembly is built. These documents won't have a record in the PDB and will be tracked as + /// . + /// + /// A document state can only change from to . + /// Once a document state is or + /// it will never change. + /// + private readonly Dictionary _documentState = []; - // test only - internal void Test_SetDocumentState(DocumentId documentId, DocumentState state) + private readonly object _guard = new(); + + public CommittedSolution(DebuggingSession debuggingSession, Solution solution, IEnumerable> initialDocumentStates) + { + _solution = solution; + _debuggingSession = debuggingSession; + _documentState.AddRange(initialDocumentStates); + } + + // test only + internal void Test_SetDocumentState(DocumentId documentId, DocumentState state) + { + lock (_guard) { - lock (_guard) - { - _documentState[documentId] = state; - } + _documentState[documentId] = state; } + } - // test only - internal ImmutableArray<(DocumentId id, DocumentState state)> Test_GetDocumentStates() + // test only + internal ImmutableArray<(DocumentId id, DocumentState state)> Test_GetDocumentStates() + { + lock (_guard) { - lock (_guard) - { - return _documentState.SelectAsArray(e => (e.Key, e.Value)); - } + return _documentState.SelectAsArray(e => (e.Key, e.Value)); } + } - public bool HasNoChanges(Solution solution) - => _solution == solution; + public bool HasNoChanges(Solution solution) + => _solution == solution; - public Project? GetProject(ProjectId id) - => _solution.GetProject(id); + public Project? GetProject(ProjectId id) + => _solution.GetProject(id); - public Project GetRequiredProject(ProjectId id) - => _solution.GetRequiredProject(id); + public Project GetRequiredProject(ProjectId id) + => _solution.GetRequiredProject(id); - public ImmutableArray GetDocumentIdsWithFilePath(string path) - => _solution.GetDocumentIdsWithFilePath(path); + public ImmutableArray GetDocumentIdsWithFilePath(string path) + => _solution.GetDocumentIdsWithFilePath(path); - public bool ContainsDocument(DocumentId documentId) - => _solution.ContainsDocument(documentId); + public bool ContainsDocument(DocumentId documentId) + => _solution.ContainsDocument(documentId); - /// - /// Returns a document snapshot for given whose content exactly matches - /// the source file used to compile the binary currently loaded in the debuggee. Returns null - /// if it fails to find a document snapshot whose content hash maches the one recorded in the PDB. - /// - /// The result is cached and the next lookup uses the cached value, including failures unless is true. - /// - public async Task<(Document? Document, DocumentState State)> GetDocumentAndStateAsync(DocumentId documentId, Document? currentDocument, CancellationToken cancellationToken, bool reloadOutOfSyncDocument = false) + /// + /// Returns a document snapshot for given whose content exactly matches + /// the source file used to compile the binary currently loaded in the debuggee. Returns null + /// if it fails to find a document snapshot whose content hash maches the one recorded in the PDB. + /// + /// The result is cached and the next lookup uses the cached value, including failures unless is true. + /// + public async Task<(Document? Document, DocumentState State)> GetDocumentAndStateAsync(DocumentId documentId, Document? currentDocument, CancellationToken cancellationToken, bool reloadOutOfSyncDocument = false) + { + Contract.ThrowIfFalse(currentDocument == null || documentId == currentDocument.Id); + + Solution solution; + var documentState = DocumentState.None; + + lock (_guard) { - Contract.ThrowIfFalse(currentDocument == null || documentId == currentDocument.Id); + solution = _solution; + _documentState.TryGetValue(documentId, out documentState); + } - Solution solution; - var documentState = DocumentState.None; + var committedDocument = solution.GetDocument(documentId); - lock (_guard) - { - solution = _solution; - _documentState.TryGetValue(documentId, out documentState); - } + switch (documentState) + { + case DocumentState.MatchesBuildOutput: + // Note: committedDocument is null if we previously validated that a document that is not in + // the committed solution is also not in the PDB. This means the document has been added during debugging. + return (committedDocument, documentState); - var committedDocument = solution.GetDocument(documentId); + case DocumentState.DesignTimeOnly: + return (null, documentState); - switch (documentState) - { - case DocumentState.MatchesBuildOutput: - // Note: committedDocument is null if we previously validated that a document that is not in - // the committed solution is also not in the PDB. This means the document has been added during debugging. - return (committedDocument, documentState); + case DocumentState.OutOfSync: + if (reloadOutOfSyncDocument) + { + break; + } + + return (null, documentState); + + case DocumentState.Indeterminate: + // Previous attempt resulted in a read error. Try again. + break; + + case DocumentState.None: + // Have not seen the document before, the document is not in the solution, or the document is source generated. - case DocumentState.DesignTimeOnly: - return (null, documentState); + if (committedDocument == null) + { + var sourceGeneratedDocument = await solution.GetSourceGeneratedDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); + if (sourceGeneratedDocument != null) + { + // source generated files are never out-of-date: + return (sourceGeneratedDocument, DocumentState.MatchesBuildOutput); + } - case DocumentState.OutOfSync: - if (reloadOutOfSyncDocument) + // The current document is source-generated therefore the corresponding one is not present in the base solution. + if (currentDocument is SourceGeneratedDocument) { - break; + return (null, DocumentState.MatchesBuildOutput); } + } - return (null, documentState); + break; + } - case DocumentState.Indeterminate: - // Previous attempt resulted in a read error. Try again. - break; + // Document compiled into the baseline DLL/PDB may have been added to the workspace + // after the committed solution snapshot was taken. + var document = committedDocument ?? currentDocument; + if (document == null) + { + // Document has been deleted. + return (null, DocumentState.None); + } - case DocumentState.None: - // Have not seen the document before, the document is not in the solution, or the document is source generated. + // TODO: Handle case when the old project does not exist and needs to be added. https://github.com/dotnet/roslyn/issues/1204 + if (committedDocument == null && !solution.ContainsProject(document.Project.Id)) + { + // Document in a new project that does not exist in the committed solution. + // Pretend this document is design-time-only and ignore it. + return (null, DocumentState.DesignTimeOnly); + } - if (committedDocument == null) - { - var sourceGeneratedDocument = await solution.GetSourceGeneratedDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); - if (sourceGeneratedDocument != null) - { - // source generated files are never out-of-date: - return (sourceGeneratedDocument, DocumentState.MatchesBuildOutput); - } - - // The current document is source-generated therefore the corresponding one is not present in the base solution. - if (currentDocument is SourceGeneratedDocument) - { - return (null, DocumentState.MatchesBuildOutput); - } - } + if (!document.DocumentState.SupportsEditAndContinue()) + { + return (null, DocumentState.DesignTimeOnly); + } - break; - } + Contract.ThrowIfNull(document.FilePath); - // Document compiled into the baseline DLL/PDB may have been added to the workspace - // after the committed solution snapshot was taken. - var document = committedDocument ?? currentDocument; - if (document == null) + var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var sourceTextVersion = (committedDocument == null) ? await document.GetTextVersionAsync(cancellationToken).ConfigureAwait(false) : default; + + var (maybeMatchingSourceText, maybePdbHasDocument) = await TryGetMatchingSourceTextAsync(document, sourceText, currentDocument, cancellationToken).ConfigureAwait(false); + + lock (_guard) + { + // only listed document states can be changed: + if (_documentState.TryGetValue(documentId, out documentState) && + documentState != DocumentState.OutOfSync && + documentState != DocumentState.Indeterminate) { - // Document has been deleted. - return (null, DocumentState.None); + return (document, documentState); } - // TODO: Handle case when the old project does not exist and needs to be added. https://github.com/dotnet/roslyn/issues/1204 - if (committedDocument == null && !solution.ContainsProject(document.Project.Id)) + DocumentState newState; + Document? matchingDocument; + + if (!maybePdbHasDocument.HasValue) { - // Document in a new project that does not exist in the committed solution. - // Pretend this document is design-time-only and ignore it. - return (null, DocumentState.DesignTimeOnly); + // Unable to determine due to error reading the PDB. + return (document, DocumentState.Indeterminate); } - if (!document.DocumentState.SupportsEditAndContinue()) + if (!maybePdbHasDocument.Value) { - return (null, DocumentState.DesignTimeOnly); + // Source file is not listed in the PDB. + // It could either be a newly added document or a design-time-only document (e.g. WPF .g.i.cs files). + // We can't distinguish between newly added document and newly added design-time-only document. + matchingDocument = null; + newState = (committedDocument != null) ? DocumentState.DesignTimeOnly : DocumentState.MatchesBuildOutput; } - - Contract.ThrowIfNull(document.FilePath); - - var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var sourceTextVersion = (committedDocument == null) ? await document.GetTextVersionAsync(cancellationToken).ConfigureAwait(false) : default; - - var (maybeMatchingSourceText, maybePdbHasDocument) = await TryGetMatchingSourceTextAsync(document, sourceText, currentDocument, cancellationToken).ConfigureAwait(false); - - lock (_guard) + else if (!maybeMatchingSourceText.HasValue) { - // only listed document states can be changed: - if (_documentState.TryGetValue(documentId, out documentState) && - documentState != DocumentState.OutOfSync && - documentState != DocumentState.Indeterminate) - { - return (document, documentState); - } - - DocumentState newState; - Document? matchingDocument; - - if (!maybePdbHasDocument.HasValue) + // Unable to determine due to error reading the source file. + return (document, DocumentState.Indeterminate); + } + else + { + // Document exists in the PDB but not in the committed solution. + // Add the document to the committed solution with its current (possibly out-of-sync) text. + if (committedDocument == null) { - // Unable to determine due to error reading the PDB. - return (document, DocumentState.Indeterminate); + // TODO: Handle case when the old project does not exist and needs to be added. https://github.com/dotnet/roslyn/issues/1204 + Debug.Assert(_solution.ContainsProject(documentId.ProjectId)); + + // TODO: Use API proposed in https://github.com/dotnet/roslyn/issues/56253. + _solution = _solution.AddDocument(DocumentInfo.Create( + documentId, + name: document.Name, + sourceCodeKind: document.SourceCodeKind, + folders: document.Folders, + loader: TextLoader.From(TextAndVersion.Create(sourceText, sourceTextVersion, document.Name)), + filePath: document.FilePath, + isGenerated: document.State.Attributes.IsGenerated) + .WithDesignTimeOnly(document.State.Attributes.DesignTimeOnly) + .WithDocumentServiceProvider(document.State.Services)); } - if (!maybePdbHasDocument.Value) - { - // Source file is not listed in the PDB. - // It could either be a newly added document or a design-time-only document (e.g. WPF .g.i.cs files). - // We can't distinguish between newly added document and newly added design-time-only document. - matchingDocument = null; - newState = (committedDocument != null) ? DocumentState.DesignTimeOnly : DocumentState.MatchesBuildOutput; - } - else if (!maybeMatchingSourceText.HasValue) - { - // Unable to determine due to error reading the source file. - return (document, DocumentState.Indeterminate); - } - else + var matchingSourceText = maybeMatchingSourceText.Value; + if (matchingSourceText != null) { - // Document exists in the PDB but not in the committed solution. - // Add the document to the committed solution with its current (possibly out-of-sync) text. - if (committedDocument == null) - { - // TODO: Handle case when the old project does not exist and needs to be added. https://github.com/dotnet/roslyn/issues/1204 - Debug.Assert(_solution.ContainsProject(documentId.ProjectId)); - - // TODO: Use API proposed in https://github.com/dotnet/roslyn/issues/56253. - _solution = _solution.AddDocument(DocumentInfo.Create( - documentId, - name: document.Name, - sourceCodeKind: document.SourceCodeKind, - folders: document.Folders, - loader: TextLoader.From(TextAndVersion.Create(sourceText, sourceTextVersion, document.Name)), - filePath: document.FilePath, - isGenerated: document.State.Attributes.IsGenerated) - .WithDesignTimeOnly(document.State.Attributes.DesignTimeOnly) - .WithDocumentServiceProvider(document.State.Services)); - } - - var matchingSourceText = maybeMatchingSourceText.Value; - if (matchingSourceText != null) + if (committedDocument != null && sourceText.ContentEquals(matchingSourceText)) { - if (committedDocument != null && sourceText.ContentEquals(matchingSourceText)) - { - matchingDocument = document; - } - else - { - _solution = _solution.WithDocumentText(documentId, matchingSourceText, PreservationMode.PreserveValue); - matchingDocument = _solution.GetDocument(documentId); - } - - newState = DocumentState.MatchesBuildOutput; + matchingDocument = document; } else { - matchingDocument = null; - newState = DocumentState.OutOfSync; + _solution = _solution.WithDocumentText(documentId, matchingSourceText, PreservationMode.PreserveValue); + matchingDocument = _solution.GetDocument(documentId); } - } - _documentState[documentId] = newState; - return (matchingDocument, newState); + newState = DocumentState.MatchesBuildOutput; + } + else + { + matchingDocument = null; + newState = DocumentState.OutOfSync; + } } + + _documentState[documentId] = newState; + return (matchingDocument, newState); } + } - private async ValueTask<(Optional matchingSourceText, bool? hasDocument)> TryGetMatchingSourceTextAsync(Document document, SourceText sourceText, Document? currentDocument, CancellationToken cancellationToken) - { - Contract.ThrowIfNull(document.FilePath); + private async ValueTask<(Optional matchingSourceText, bool? hasDocument)> TryGetMatchingSourceTextAsync(Document document, SourceText sourceText, Document? currentDocument, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(document.FilePath); - var maybePdbHasDocument = TryReadSourceFileChecksumFromPdb(document, out var requiredChecksum, out var checksumAlgorithm); + var maybePdbHasDocument = TryReadSourceFileChecksumFromPdb(document, out var requiredChecksum, out var checksumAlgorithm); - var maybeMatchingSourceText = (maybePdbHasDocument == true) ? - await TryGetMatchingSourceTextAsync(sourceText, document.FilePath, currentDocument, _debuggingSession.SourceTextProvider, requiredChecksum, checksumAlgorithm, cancellationToken).ConfigureAwait(false) : default; + var maybeMatchingSourceText = (maybePdbHasDocument == true) ? + await TryGetMatchingSourceTextAsync(sourceText, document.FilePath, currentDocument, _debuggingSession.SourceTextProvider, requiredChecksum, checksumAlgorithm, cancellationToken).ConfigureAwait(false) : default; - return (maybeMatchingSourceText, maybePdbHasDocument); + return (maybeMatchingSourceText, maybePdbHasDocument); + } + + private static async ValueTask> TryGetMatchingSourceTextAsync( + SourceText sourceText, string filePath, Document? currentDocument, IPdbMatchingSourceTextProvider sourceTextProvider, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + { + if (IsMatchingSourceText(sourceText, requiredChecksum, checksumAlgorithm)) + { + return sourceText; } - private static async ValueTask> TryGetMatchingSourceTextAsync( - SourceText sourceText, string filePath, Document? currentDocument, IPdbMatchingSourceTextProvider sourceTextProvider, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + if (currentDocument != null) { - if (IsMatchingSourceText(sourceText, requiredChecksum, checksumAlgorithm)) + var currentDocumentSourceText = await currentDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (IsMatchingSourceText(currentDocumentSourceText, requiredChecksum, checksumAlgorithm)) { - return sourceText; + return currentDocumentSourceText; } + } - if (currentDocument != null) + var text = await sourceTextProvider.TryGetMatchingSourceTextAsync(filePath, requiredChecksum, checksumAlgorithm, cancellationToken).ConfigureAwait(false); + if (text != null) + { + return SourceText.From(text, sourceText.Encoding, checksumAlgorithm); + } + + return await Task.Run(() => TryGetPdbMatchingSourceTextFromDisk(filePath, sourceText.Encoding, requiredChecksum, checksumAlgorithm), cancellationToken).ConfigureAwait(false); + } + + internal static async Task>> GetMatchingDocumentsAsync( + IEnumerable<(Project, IEnumerable)> documentsByProject, + Func compilationOutputsProvider, + IPdbMatchingSourceTextProvider sourceTextProvider, + CancellationToken cancellationToken) + { + var projectTasks = documentsByProject.Select(async projectDocumentStates => + { + cancellationToken.ThrowIfCancellationRequested(); + + var (project, documentStates) = projectDocumentStates; + + // Skip projects that do not support Roslyn EnC (e.g. F#, etc). + // Source files of these may not even be captured in the solution snapshot. + if (!project.SupportsEditAndContinue()) { - var currentDocumentSourceText = await currentDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - if (IsMatchingSourceText(currentDocumentSourceText, requiredChecksum, checksumAlgorithm)) - { - return currentDocumentSourceText; - } + return Array.Empty(); } - var text = await sourceTextProvider.TryGetMatchingSourceTextAsync(filePath, requiredChecksum, checksumAlgorithm, cancellationToken).ConfigureAwait(false); - if (text != null) + using var debugInfoReaderProvider = GetMethodDebugInfoReader(compilationOutputsProvider(project), project.Name); + if (debugInfoReaderProvider == null) { - return SourceText.From(text, sourceText.Encoding, checksumAlgorithm); + return Array.Empty(); } - return await Task.Run(() => TryGetPdbMatchingSourceTextFromDisk(filePath, sourceText.Encoding, requiredChecksum, checksumAlgorithm), cancellationToken).ConfigureAwait(false); - } + var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader(); - internal static async Task>> GetMatchingDocumentsAsync( - IEnumerable<(Project, IEnumerable)> documentsByProject, - Func compilationOutputsProvider, - IPdbMatchingSourceTextProvider sourceTextProvider, - CancellationToken cancellationToken) - { - var projectTasks = documentsByProject.Select(async projectDocumentStates => + var documentTasks = documentStates.Select(async documentState => { cancellationToken.ThrowIfCancellationRequested(); - var (project, documentStates) = projectDocumentStates; - - // Skip projects that do not support Roslyn EnC (e.g. F#, etc). - // Source files of these may not even be captured in the solution snapshot. - if (!project.SupportsEditAndContinue()) + if (documentState.SupportsEditAndContinue()) { - return Array.Empty(); - } + var sourceFilePath = documentState.FilePath; + Contract.ThrowIfNull(sourceFilePath); - using var debugInfoReaderProvider = GetMethodDebugInfoReader(compilationOutputsProvider(project), project.Name); - if (debugInfoReaderProvider == null) - { - return Array.Empty(); - } - - var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader(); - - var documentTasks = documentStates.Select(async documentState => - { - cancellationToken.ThrowIfCancellationRequested(); + // Hydrate the solution snapshot with the content of the file. + // It's important to do this before we start watching for changes so that we have a baseline we can compare future snapshots to. + var sourceText = await documentState.GetTextAsync(cancellationToken).ConfigureAwait(false); - if (documentState.SupportsEditAndContinue()) + // TODO: https://github.com/dotnet/roslyn/issues/51993 + // avoid rereading the file in common case - the workspace should create source texts with the right checksum algorithm and encoding + if (TryReadSourceFileChecksumFromPdb(debugInfoReader, sourceFilePath, out var requiredChecksum, out var checksumAlgorithm) == true && + await TryGetMatchingSourceTextAsync(sourceText, sourceFilePath, currentDocument: null, sourceTextProvider, requiredChecksum, checksumAlgorithm, cancellationToken).ConfigureAwait(false) is { HasValue: true, Value: not null }) { - var sourceFilePath = documentState.FilePath; - Contract.ThrowIfNull(sourceFilePath); - - // Hydrate the solution snapshot with the content of the file. - // It's important to do this before we start watching for changes so that we have a baseline we can compare future snapshots to. - var sourceText = await documentState.GetTextAsync(cancellationToken).ConfigureAwait(false); - - // TODO: https://github.com/dotnet/roslyn/issues/51993 - // avoid rereading the file in common case - the workspace should create source texts with the right checksum algorithm and encoding - if (TryReadSourceFileChecksumFromPdb(debugInfoReader, sourceFilePath, out var requiredChecksum, out var checksumAlgorithm) == true && - await TryGetMatchingSourceTextAsync(sourceText, sourceFilePath, currentDocument: null, sourceTextProvider, requiredChecksum, checksumAlgorithm, cancellationToken).ConfigureAwait(false) is { HasValue: true, Value: not null }) - { - return documentState.Id; - } + return documentState.Id; } + } - return null; - }); - - return await Task.WhenAll(documentTasks).ConfigureAwait(false); + return null; }); - var documentIdArrays = await Task.WhenAll(projectTasks).ConfigureAwait(false); + return await Task.WhenAll(documentTasks).ConfigureAwait(false); + }); - return documentIdArrays.SelectMany(ids => ids.WhereNotNull()).Select(id => KeyValuePairUtil.Create(id, DocumentState.MatchesBuildOutput)); - } + var documentIdArrays = await Task.WhenAll(projectTasks).ConfigureAwait(false); - private static DebugInformationReaderProvider? GetMethodDebugInfoReader(CompilationOutputs compilationOutputs, string projectName) - { - DebugInformationReaderProvider? debugInfoReaderProvider; - try - { - debugInfoReaderProvider = compilationOutputs.OpenPdb(); + return documentIdArrays.SelectMany(ids => ids.WhereNotNull()).Select(id => KeyValuePairUtil.Create(id, DocumentState.MatchesBuildOutput)); + } - if (debugInfoReaderProvider == null) - { - EditAndContinueService.Log.Write("Source file of project '{0}' doesn't match output PDB: PDB '{1}' (assembly: '{2}') not found", projectName, compilationOutputs.PdbDisplayPath, compilationOutputs.AssemblyDisplayPath); - } + private static DebugInformationReaderProvider? GetMethodDebugInfoReader(CompilationOutputs compilationOutputs, string projectName) + { + DebugInformationReaderProvider? debugInfoReaderProvider; + try + { + debugInfoReaderProvider = compilationOutputs.OpenPdb(); - return debugInfoReaderProvider; - } - catch (Exception e) + if (debugInfoReaderProvider == null) { - EditAndContinueService.Log.Write("Source file of project '{0}' doesn't match output PDB: error opening PDB '{1}' (assembly: '{2}'): {3}", projectName, compilationOutputs.PdbDisplayPath, compilationOutputs.AssemblyDisplayPath, e.Message); - return null; + EditAndContinueService.Log.Write("Source file of project '{0}' doesn't match output PDB: PDB '{1}' (assembly: '{2}') not found", projectName, compilationOutputs.PdbDisplayPath, compilationOutputs.AssemblyDisplayPath); } - } - public void CommitSolution(Solution solution) + return debugInfoReaderProvider; + } + catch (Exception e) { - lock (_guard) - { - _solution = solution; - } + EditAndContinueService.Log.Write("Source file of project '{0}' doesn't match output PDB: error opening PDB '{1}' (assembly: '{2}'): {3}", projectName, compilationOutputs.PdbDisplayPath, compilationOutputs.AssemblyDisplayPath, e.Message); + return null; } + } - private static bool IsMatchingSourceText(SourceText sourceText, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm) - => checksumAlgorithm == sourceText.ChecksumAlgorithm && sourceText.GetChecksum().SequenceEqual(requiredChecksum); - - private static Optional TryGetPdbMatchingSourceTextFromDisk(string sourceFilePath, Encoding? encoding, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm) + public void CommitSolution(Solution solution) + { + lock (_guard) { - try - { - using var fileStream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete); + _solution = solution; + } + } - // We must use the encoding of the document as determined by the IDE (the editor). - // This might differ from the encoding that the compiler chooses, so if we just relied on the compiler we - // might end up updating the committed solution with a document that has a different encoding than - // the one that's in the workspace, resulting in false document changes when we compare the two. - var sourceText = SourceText.From(fileStream, encoding, checksumAlgorithm); + private static bool IsMatchingSourceText(SourceText sourceText, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm) + => checksumAlgorithm == sourceText.ChecksumAlgorithm && sourceText.GetChecksum().SequenceEqual(requiredChecksum); - if (IsMatchingSourceText(sourceText, requiredChecksum, checksumAlgorithm)) - { - return sourceText; - } + private static Optional TryGetPdbMatchingSourceTextFromDisk(string sourceFilePath, Encoding? encoding, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm) + { + try + { + using var fileStream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete); - EditAndContinueService.Log.Write("Checksum differs for source file '{0}'", sourceFilePath); + // We must use the encoding of the document as determined by the IDE (the editor). + // This might differ from the encoding that the compiler chooses, so if we just relied on the compiler we + // might end up updating the committed solution with a document that has a different encoding than + // the one that's in the workspace, resulting in false document changes when we compare the two. + var sourceText = SourceText.From(fileStream, encoding, checksumAlgorithm); - // does not match: - return null; - } - catch (Exception e) + if (IsMatchingSourceText(sourceText, requiredChecksum, checksumAlgorithm)) { - EditAndContinueService.Log.Write("Error calculating checksum for source file '{0}': '{1}'", sourceFilePath, e.Message); - - // unable to determine: - return default; + return sourceText; } - } - private bool? TryReadSourceFileChecksumFromPdb(Document document, out ImmutableArray requiredChecksum, out SourceHashAlgorithm checksumAlgorithm) - { - Contract.ThrowIfNull(document.FilePath); + EditAndContinueService.Log.Write("Checksum differs for source file '{0}'", sourceFilePath); - var compilationOutputs = _debuggingSession.GetCompilationOutputs(document.Project); - using var debugInfoReaderProvider = GetMethodDebugInfoReader(compilationOutputs, document.Project.Name); - if (debugInfoReaderProvider == null) - { - // unable to determine whether document is in the PDB - requiredChecksum = default; - checksumAlgorithm = default; - return null; - } + // does not match: + return null; + } + catch (Exception e) + { + EditAndContinueService.Log.Write("Error calculating checksum for source file '{0}': '{1}'", sourceFilePath, e.Message); - var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader(); - return TryReadSourceFileChecksumFromPdb(debugInfoReader, document.FilePath, out requiredChecksum, out checksumAlgorithm); + // unable to determine: + return default; } + } - /// - /// Returns true if the PDB contains a document record for given , - /// in which case and contain its checksum. - /// False if the document is not found in the PDB. - /// Null if it can't be determined because the PDB is not available or an error occurred while reading the PDB. - /// - private static bool? TryReadSourceFileChecksumFromPdb(EditAndContinueMethodDebugInfoReader debugInfoReader, string sourceFilePath, out ImmutableArray checksum, out SourceHashAlgorithm algorithm) + private bool? TryReadSourceFileChecksumFromPdb(Document document, out ImmutableArray requiredChecksum, out SourceHashAlgorithm checksumAlgorithm) + { + Contract.ThrowIfNull(document.FilePath); + + var compilationOutputs = _debuggingSession.GetCompilationOutputs(document.Project); + using var debugInfoReaderProvider = GetMethodDebugInfoReader(compilationOutputs, document.Project.Name); + if (debugInfoReaderProvider == null) { - checksum = default; - algorithm = default; + // unable to determine whether document is in the PDB + requiredChecksum = default; + checksumAlgorithm = default; + return null; + } - try - { - if (!debugInfoReader.TryGetDocumentChecksum(sourceFilePath, out checksum, out var algorithmId)) - { - EditAndContinueService.Log.Write("Source '{0}' doesn't match output PDB: no document", sourceFilePath); - return false; - } + var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader(); + return TryReadSourceFileChecksumFromPdb(debugInfoReader, document.FilePath, out requiredChecksum, out checksumAlgorithm); + } - algorithm = SourceHashAlgorithms.GetSourceHashAlgorithm(algorithmId); - if (algorithm == SourceHashAlgorithm.None) - { - // This can only happen if the PDB was post-processed by a misbehaving tool. - EditAndContinueService.Log.Write("Source '{0}' doesn't match PDB: unknown checksum alg", sourceFilePath); - } + /// + /// Returns true if the PDB contains a document record for given , + /// in which case and contain its checksum. + /// False if the document is not found in the PDB. + /// Null if it can't be determined because the PDB is not available or an error occurred while reading the PDB. + /// + private static bool? TryReadSourceFileChecksumFromPdb(EditAndContinueMethodDebugInfoReader debugInfoReader, string sourceFilePath, out ImmutableArray checksum, out SourceHashAlgorithm algorithm) + { + checksum = default; + algorithm = default; - return true; + try + { + if (!debugInfoReader.TryGetDocumentChecksum(sourceFilePath, out checksum, out var algorithmId)) + { + EditAndContinueService.Log.Write("Source '{0}' doesn't match output PDB: no document", sourceFilePath); + return false; } - catch (Exception e) + + algorithm = SourceHashAlgorithms.GetSourceHashAlgorithm(algorithmId); + if (algorithm == SourceHashAlgorithm.None) { - EditAndContinueService.Log.Write("Source '{0}' doesn't match output PDB: error reading symbols: {1}", sourceFilePath, e.Message); + // This can only happen if the PDB was post-processed by a misbehaving tool. + EditAndContinueService.Log.Write("Source '{0}' doesn't match PDB: unknown checksum alg", sourceFilePath); } - // unable to determine - return null; + return true; } + catch (Exception e) + { + EditAndContinueService.Log.Write("Source '{0}' doesn't match output PDB: error reading symbols: {1}", sourceFilePath, e.Message); + } + + // unable to determine + return null; } } diff --git a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs index fc6969c393f89..03cbd749dd828 100644 --- a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs @@ -22,840 +22,839 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Represents a debugging session. +/// +internal sealed class DebuggingSession : IDisposable { + private readonly Func _compilationOutputsProvider; + internal readonly IPdbMatchingSourceTextProvider SourceTextProvider; + private readonly CancellationTokenSource _cancellationSource = new(); + /// - /// Represents a debugging session. + /// MVIDs read from the assembly built for given project id. + /// Only contains ids for projects that support EnC. /// - internal sealed class DebuggingSession : IDisposable - { - private readonly Func _compilationOutputsProvider; - internal readonly IPdbMatchingSourceTextProvider SourceTextProvider; - private readonly CancellationTokenSource _cancellationSource = new(); - - /// - /// MVIDs read from the assembly built for given project id. - /// Only contains ids for projects that support EnC. - /// - private readonly Dictionary _projectModuleIds = []; - private readonly Dictionary _moduleIds = []; - private readonly object _projectModuleIdsGuard = new(); - - /// - /// The current baseline for given project id. - /// The baseline is updated when changes are committed at the end of edit session. - /// The backing module readers of initial baselines need to be kept alive -- store them in - /// and dispose them at the end of the debugging session. - /// - /// - /// The baseline of each updated project is linked to its initial baseline that reads from the on-disk metadata and PDB. - /// Therefore once an initial baseline is created it needs to be kept alive till the end of the debugging session, - /// even when it's replaced in by a newer baseline. - /// - private readonly Dictionary _projectBaselines = []; - private readonly List _initialBaselineModuleReaders = []; - private readonly object _projectEmitBaselinesGuard = new(); - - /// - /// To avoid accessing metadata/symbol readers that have been disposed, - /// read lock is acquired before every operation that may access a baseline module/symbol reader - /// and write lock when the baseline readers are being disposed. - /// - private readonly ReaderWriterLockSlim _baselineAccessLock = new(); - private bool _isDisposed; - - internal EditSession EditSession { get; private set; } - - private readonly HashSet _modulesPreparedForUpdate = []; - private readonly object _modulesPreparedForUpdateGuard = new(); - - internal readonly DebuggingSessionId Id; - - /// - /// Incremented on every emit update invocation. Used by logging. - /// - private int _updateOrdinal; - - /// - /// The solution captured when the debugging session entered run mode (application debugging started), - /// or the solution which the last changes committed to the debuggee at the end of edit session were calculated from. - /// The solution reflecting the current state of the modules loaded in the debugee. - /// - internal readonly CommittedSolution LastCommittedSolution; - - internal readonly IManagedHotReloadService DebuggerService; - - /// - /// True if the diagnostics produced by the session should be reported to the diagnotic analyzer. - /// - internal readonly bool ReportDiagnostics; - - private readonly DebuggingSessionTelemetry _telemetry; - private readonly EditSessionTelemetry _editSessionTelemetry = new(); - - private PendingUpdate? _pendingUpdate; - private Action _reportTelemetry; - - /// - /// Last array of module updates generated during the debugging session. - /// Useful for crash dump diagnostics. - /// - private ImmutableArray _lastModuleUpdatesLog; - - internal DebuggingSession( - DebuggingSessionId id, - Solution solution, - IManagedHotReloadService debuggerService, - Func compilationOutputsProvider, - IPdbMatchingSourceTextProvider sourceTextProvider, - IEnumerable> initialDocumentStates, - bool reportDiagnostics) - { - _compilationOutputsProvider = compilationOutputsProvider; - SourceTextProvider = sourceTextProvider; - _reportTelemetry = ReportTelemetry; - _telemetry = new DebuggingSessionTelemetry(solution.SolutionState.SolutionAttributes.TelemetryId); - - Id = id; - DebuggerService = debuggerService; - LastCommittedSolution = new CommittedSolution(this, solution, initialDocumentStates); - - EditSession = new EditSession( - this, - nonRemappableRegions: ImmutableDictionary>.Empty, - _editSessionTelemetry, - lazyActiveStatementMap: null, - inBreakState: false); - - ReportDiagnostics = reportDiagnostics; - } + private readonly Dictionary _projectModuleIds = []; + private readonly Dictionary _moduleIds = []; + private readonly object _projectModuleIdsGuard = new(); - public void Dispose() - { - Debug.Assert(!_isDisposed); + /// + /// The current baseline for given project id. + /// The baseline is updated when changes are committed at the end of edit session. + /// The backing module readers of initial baselines need to be kept alive -- store them in + /// and dispose them at the end of the debugging session. + /// + /// + /// The baseline of each updated project is linked to its initial baseline that reads from the on-disk metadata and PDB. + /// Therefore once an initial baseline is created it needs to be kept alive till the end of the debugging session, + /// even when it's replaced in by a newer baseline. + /// + private readonly Dictionary _projectBaselines = []; + private readonly List _initialBaselineModuleReaders = []; + private readonly object _projectEmitBaselinesGuard = new(); - _isDisposed = true; - _cancellationSource.Cancel(); - _cancellationSource.Dispose(); + /// + /// To avoid accessing metadata/symbol readers that have been disposed, + /// read lock is acquired before every operation that may access a baseline module/symbol reader + /// and write lock when the baseline readers are being disposed. + /// + private readonly ReaderWriterLockSlim _baselineAccessLock = new(); + private bool _isDisposed; - // Wait for all operations on baseline to finish before we dispose the readers. - _baselineAccessLock.EnterWriteLock(); + internal EditSession EditSession { get; private set; } - foreach (var reader in GetBaselineModuleReaders()) - { - reader.Dispose(); - } + private readonly HashSet _modulesPreparedForUpdate = []; + private readonly object _modulesPreparedForUpdateGuard = new(); - _baselineAccessLock.ExitWriteLock(); - _baselineAccessLock.Dispose(); + internal readonly DebuggingSessionId Id; - if (Interlocked.Exchange(ref _pendingUpdate, null) != null) - { - throw new InvalidOperationException($"Pending update has not been committed or discarded."); - } - } + /// + /// Incremented on every emit update invocation. Used by logging. + /// + private int _updateOrdinal; + + /// + /// The solution captured when the debugging session entered run mode (application debugging started), + /// or the solution which the last changes committed to the debuggee at the end of edit session were calculated from. + /// The solution reflecting the current state of the modules loaded in the debugee. + /// + internal readonly CommittedSolution LastCommittedSolution; + + internal readonly IManagedHotReloadService DebuggerService; - internal void ThrowIfDisposed() + /// + /// True if the diagnostics produced by the session should be reported to the diagnotic analyzer. + /// + internal readonly bool ReportDiagnostics; + + private readonly DebuggingSessionTelemetry _telemetry; + private readonly EditSessionTelemetry _editSessionTelemetry = new(); + + private PendingUpdate? _pendingUpdate; + private Action _reportTelemetry; + + /// + /// Last array of module updates generated during the debugging session. + /// Useful for crash dump diagnostics. + /// + private ImmutableArray _lastModuleUpdatesLog; + + internal DebuggingSession( + DebuggingSessionId id, + Solution solution, + IManagedHotReloadService debuggerService, + Func compilationOutputsProvider, + IPdbMatchingSourceTextProvider sourceTextProvider, + IEnumerable> initialDocumentStates, + bool reportDiagnostics) + { + _compilationOutputsProvider = compilationOutputsProvider; + SourceTextProvider = sourceTextProvider; + _reportTelemetry = ReportTelemetry; + _telemetry = new DebuggingSessionTelemetry(solution.SolutionState.SolutionAttributes.TelemetryId); + + Id = id; + DebuggerService = debuggerService; + LastCommittedSolution = new CommittedSolution(this, solution, initialDocumentStates); + + EditSession = new EditSession( + this, + nonRemappableRegions: ImmutableDictionary>.Empty, + _editSessionTelemetry, + lazyActiveStatementMap: null, + inBreakState: false); + + ReportDiagnostics = reportDiagnostics; + } + + public void Dispose() + { + Debug.Assert(!_isDisposed); + + _isDisposed = true; + _cancellationSource.Cancel(); + _cancellationSource.Dispose(); + + // Wait for all operations on baseline to finish before we dispose the readers. + _baselineAccessLock.EnterWriteLock(); + + foreach (var reader in GetBaselineModuleReaders()) { - if (_isDisposed) - throw new ObjectDisposedException(nameof(DebuggingSession)); + reader.Dispose(); } - private void StorePendingUpdate(PendingUpdate update) - { - var previousPendingUpdate = Interlocked.Exchange(ref _pendingUpdate, update); + _baselineAccessLock.ExitWriteLock(); + _baselineAccessLock.Dispose(); - // commit/discard was not called: - if (previousPendingUpdate != null) - { - throw new InvalidOperationException($"Previous update has not been committed or discarded."); - } + if (Interlocked.Exchange(ref _pendingUpdate, null) != null) + { + throw new InvalidOperationException($"Pending update has not been committed or discarded."); } + } - private PendingUpdate RetrievePendingUpdate() - { - var pendingUpdate = Interlocked.Exchange(ref _pendingUpdate, null); - if (pendingUpdate == null) - { - throw new InvalidOperationException($"No pending update."); - } + internal void ThrowIfDisposed() + { + if (_isDisposed) + throw new ObjectDisposedException(nameof(DebuggingSession)); + } - return pendingUpdate; - } + private void StorePendingUpdate(PendingUpdate update) + { + var previousPendingUpdate = Interlocked.Exchange(ref _pendingUpdate, update); - private void EndEditSession(out ImmutableArray documentsToReanalyze) + // commit/discard was not called: + if (previousPendingUpdate != null) { - documentsToReanalyze = EditSession.GetDocumentsWithReportedDiagnostics(); - - var editSessionTelemetryData = EditSession.Telemetry.GetDataAndClear(); - _telemetry.LogEditSession(editSessionTelemetryData); + throw new InvalidOperationException($"Previous update has not been committed or discarded."); } + } - public void EndSession(out ImmutableArray documentsToReanalyze, out DebuggingSessionTelemetry.Data telemetryData) + private PendingUpdate RetrievePendingUpdate() + { + var pendingUpdate = Interlocked.Exchange(ref _pendingUpdate, null); + if (pendingUpdate == null) { - ThrowIfDisposed(); + throw new InvalidOperationException($"No pending update."); + } - EndEditSession(out documentsToReanalyze); - telemetryData = _telemetry.GetDataAndClear(); - _reportTelemetry(telemetryData); + return pendingUpdate; + } - Dispose(); - } + private void EndEditSession(out ImmutableArray documentsToReanalyze) + { + documentsToReanalyze = EditSession.GetDocumentsWithReportedDiagnostics(); - public void BreakStateOrCapabilitiesChanged(bool? inBreakState, out ImmutableArray documentsToReanalyze) - => RestartEditSession(nonRemappableRegions: null, inBreakState, out documentsToReanalyze); + var editSessionTelemetryData = EditSession.Telemetry.GetDataAndClear(); + _telemetry.LogEditSession(editSessionTelemetryData); + } - internal void RestartEditSession(ImmutableDictionary>? nonRemappableRegions, bool? inBreakState, out ImmutableArray documentsToReanalyze) - { - ThrowIfDisposed(); + public void EndSession(out ImmutableArray documentsToReanalyze, out DebuggingSessionTelemetry.Data telemetryData) + { + ThrowIfDisposed(); - EndEditSession(out documentsToReanalyze); + EndEditSession(out documentsToReanalyze); + telemetryData = _telemetry.GetDataAndClear(); + _reportTelemetry(telemetryData); + + Dispose(); + } + + public void BreakStateOrCapabilitiesChanged(bool? inBreakState, out ImmutableArray documentsToReanalyze) + => RestartEditSession(nonRemappableRegions: null, inBreakState, out documentsToReanalyze); + + internal void RestartEditSession(ImmutableDictionary>? nonRemappableRegions, bool? inBreakState, out ImmutableArray documentsToReanalyze) + { + ThrowIfDisposed(); - EditSession = new EditSession( - this, - nonRemappableRegions ?? EditSession.NonRemappableRegions, - EditSession.Telemetry, - (inBreakState == null) ? EditSession.BaseActiveStatements : null, - inBreakState ?? EditSession.InBreakState); + EndEditSession(out documentsToReanalyze); + + EditSession = new EditSession( + this, + nonRemappableRegions ?? EditSession.NonRemappableRegions, + EditSession.Telemetry, + (inBreakState == null) ? EditSession.BaseActiveStatements : null, + inBreakState ?? EditSession.InBreakState); + } + + private ImmutableArray GetBaselineModuleReaders() + { + lock (_projectEmitBaselinesGuard) + { + return _initialBaselineModuleReaders.ToImmutableArrayOrEmpty(); } + } + + internal CompilationOutputs GetCompilationOutputs(Project project) + => _compilationOutputsProvider(project); - private ImmutableArray GetBaselineModuleReaders() + private bool AddModulePreparedForUpdate(Guid mvid) + { + lock (_modulesPreparedForUpdateGuard) { - lock (_projectEmitBaselinesGuard) - { - return _initialBaselineModuleReaders.ToImmutableArrayOrEmpty(); - } + return _modulesPreparedForUpdate.Add(mvid); } + } - internal CompilationOutputs GetCompilationOutputs(Project project) - => _compilationOutputsProvider(project); + /// + /// Reads the MVID of a compiled project. + /// + /// + /// An MVID and an error message to report, in case an IO exception occurred while reading the binary. + /// The MVID is if either the project is not built, or the MVID can't be read from the module binary. + /// + internal async Task<(Guid Mvid, Diagnostic? Error)> GetProjectModuleIdAsync(Project project, CancellationToken cancellationToken) + { + Debug.Assert(project.SupportsEditAndContinue()); - private bool AddModulePreparedForUpdate(Guid mvid) + lock (_projectModuleIdsGuard) { - lock (_modulesPreparedForUpdateGuard) + if (_projectModuleIds.TryGetValue(project.Id, out var id)) { - return _modulesPreparedForUpdate.Add(mvid); + return id; } } - /// - /// Reads the MVID of a compiled project. - /// - /// - /// An MVID and an error message to report, in case an IO exception occurred while reading the binary. - /// The MVID is if either the project is not built, or the MVID can't be read from the module binary. - /// - internal async Task<(Guid Mvid, Diagnostic? Error)> GetProjectModuleIdAsync(Project project, CancellationToken cancellationToken) + (Guid Mvid, Diagnostic? Error) ReadMvid() { - Debug.Assert(project.SupportsEditAndContinue()); + var outputs = GetCompilationOutputs(project); - lock (_projectModuleIdsGuard) + try { - if (_projectModuleIds.TryGetValue(project.Id, out var id)) - { - return id; - } + return (outputs.ReadAssemblyModuleVersionId(), Error: null); } - - (Guid Mvid, Diagnostic? Error) ReadMvid() + catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException) { - var outputs = GetCompilationOutputs(project); - - try - { - return (outputs.ReadAssemblyModuleVersionId(), Error: null); - } - catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException) - { - return (Mvid: Guid.Empty, Error: null); - } - catch (Exception e) - { - var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile); - return (Mvid: Guid.Empty, Error: Diagnostic.Create(descriptor, Location.None, new[] { outputs.AssemblyDisplayPath, e.Message })); - } + return (Mvid: Guid.Empty, Error: null); } + catch (Exception e) + { + var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile); + return (Mvid: Guid.Empty, Error: Diagnostic.Create(descriptor, Location.None, new[] { outputs.AssemblyDisplayPath, e.Message })); + } + } - var newId = await Task.Run(ReadMvid, cancellationToken).ConfigureAwait(false); + var newId = await Task.Run(ReadMvid, cancellationToken).ConfigureAwait(false); - lock (_projectModuleIdsGuard) + lock (_projectModuleIdsGuard) + { + if (_projectModuleIds.TryGetValue(project.Id, out var id)) { - if (_projectModuleIds.TryGetValue(project.Id, out var id)) - { - return id; - } - - // Do not cache failures. The error might be intermittent and might be corrected next time we attempt to read the MVID. - if (newId.Mvid != Guid.Empty) - { - _moduleIds[newId.Mvid] = project.Id; - _projectModuleIds[project.Id] = newId; - } + return id; + } - return newId; + // Do not cache failures. The error might be intermittent and might be corrected next time we attempt to read the MVID. + if (newId.Mvid != Guid.Empty) + { + _moduleIds[newId.Mvid] = project.Id; + _projectModuleIds[project.Id] = newId; } + + return newId; } + } - internal bool TryGetProjectId(Guid moduleId, [NotNullWhen(true)] out ProjectId? projectId) + internal bool TryGetProjectId(Guid moduleId, [NotNullWhen(true)] out ProjectId? projectId) + { + lock (_projectModuleIdsGuard) { - lock (_projectModuleIdsGuard) + return _moduleIds.TryGetValue(moduleId, out projectId); + } + } + + /// + /// Get for given project. + /// + /// Project used to create the initial baseline, if the baseline does not exist yet. + /// Compilation used to create the initial baseline, if the baseline does not exist yet. + /// True unless the project outputs can't be read. + internal bool TryGetOrCreateEmitBaseline( + Project baselineProject, + Compilation baselineCompilation, + out ImmutableArray diagnostics, + [NotNullWhen(true)] out ProjectBaseline? baseline, + [NotNullWhen(true)] out ReaderWriterLockSlim? baselineAccessLock) + { + baselineAccessLock = _baselineAccessLock; + + lock (_projectEmitBaselinesGuard) + { + if (_projectBaselines.TryGetValue(baselineProject.Id, out baseline)) { - return _moduleIds.TryGetValue(moduleId, out projectId); + diagnostics = []; + return true; } } - /// - /// Get for given project. - /// - /// Project used to create the initial baseline, if the baseline does not exist yet. - /// Compilation used to create the initial baseline, if the baseline does not exist yet. - /// True unless the project outputs can't be read. - internal bool TryGetOrCreateEmitBaseline( - Project baselineProject, - Compilation baselineCompilation, - out ImmutableArray diagnostics, - [NotNullWhen(true)] out ProjectBaseline? baseline, - [NotNullWhen(true)] out ReaderWriterLockSlim? baselineAccessLock) + var outputs = GetCompilationOutputs(baselineProject); + if (!TryCreateInitialBaseline(baselineCompilation, outputs, baselineProject.Id, out diagnostics, out var initialBaseline, out var debugInfoReaderProvider, out var metadataReaderProvider)) { - baselineAccessLock = _baselineAccessLock; + // Unable to read the DLL/PDB at this point (it might be open by another process). + // Don't cache the failure so that the user can attempt to apply changes again. + baseline = null; + return false; + } - lock (_projectEmitBaselinesGuard) + lock (_projectEmitBaselinesGuard) + { + if (_projectBaselines.TryGetValue(baselineProject.Id, out baseline)) { - if (_projectBaselines.TryGetValue(baselineProject.Id, out baseline)) - { - diagnostics = []; - return true; - } + metadataReaderProvider.Dispose(); + debugInfoReaderProvider.Dispose(); + return true; } - var outputs = GetCompilationOutputs(baselineProject); - if (!TryCreateInitialBaseline(baselineCompilation, outputs, baselineProject.Id, out diagnostics, out var initialBaseline, out var debugInfoReaderProvider, out var metadataReaderProvider)) + baseline = new ProjectBaseline(baselineProject.Id, initialBaseline, generation: 0); + + _projectBaselines.Add(baselineProject.Id, baseline); + _initialBaselineModuleReaders.Add(metadataReaderProvider); + _initialBaselineModuleReaders.Add(debugInfoReaderProvider); + } + + return true; + } + + private static unsafe bool TryCreateInitialBaseline( + Compilation compilation, + CompilationOutputs compilationOutputs, + ProjectId projectId, + out ImmutableArray diagnostics, + [NotNullWhen(true)] out EmitBaseline? baseline, + [NotNullWhen(true)] out DebugInformationReaderProvider? debugInfoReaderProvider, + [NotNullWhen(true)] out MetadataReaderProvider? metadataReaderProvider) + { + // Read the metadata and symbols from the disk. Close the files as soon as we are done emitting the delta to minimize + // the time when they are being locked. Since we need to use the baseline that is produced by delta emit for the subsequent + // delta emit we need to keep the module metadata and symbol info backing the symbols of the baseline alive in memory. + // Alternatively, we could drop the data once we are done with emitting the delta and re-emit the baseline again + // when we need it next time and the module is loaded. + + diagnostics = default; + baseline = null; + debugInfoReaderProvider = null; + metadataReaderProvider = null; + + var success = false; + var fileBeingRead = compilationOutputs.PdbDisplayPath; + try + { + debugInfoReaderProvider = compilationOutputs.OpenPdb(); + if (debugInfoReaderProvider == null) { - // Unable to read the DLL/PDB at this point (it might be open by another process). - // Don't cache the failure so that the user can attempt to apply changes again. - baseline = null; - return false; + throw new FileNotFoundException(); } - lock (_projectEmitBaselinesGuard) - { - if (_projectBaselines.TryGetValue(baselineProject.Id, out baseline)) - { - metadataReaderProvider.Dispose(); - debugInfoReaderProvider.Dispose(); - return true; - } + var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader(); - baseline = new ProjectBaseline(baselineProject.Id, initialBaseline, generation: 0); + fileBeingRead = compilationOutputs.AssemblyDisplayPath; - _projectBaselines.Add(baselineProject.Id, baseline); - _initialBaselineModuleReaders.Add(metadataReaderProvider); - _initialBaselineModuleReaders.Add(debugInfoReaderProvider); + metadataReaderProvider = compilationOutputs.OpenAssemblyMetadata(prefetch: true); + if (metadataReaderProvider == null) + { + throw new FileNotFoundException(); } + var metadataReader = metadataReaderProvider.GetMetadataReader(); + var moduleMetadata = ModuleMetadata.CreateFromMetadata((IntPtr)metadataReader.MetadataPointer, metadataReader.MetadataLength); + + baseline = EmitBaseline.CreateInitialBaseline( + compilation, + moduleMetadata, + debugInfoReader.GetDebugInfo, + debugInfoReader.GetLocalSignature, + debugInfoReader.IsPortable); + + success = true; return true; } - - private static unsafe bool TryCreateInitialBaseline( - Compilation compilation, - CompilationOutputs compilationOutputs, - ProjectId projectId, - out ImmutableArray diagnostics, - [NotNullWhen(true)] out EmitBaseline? baseline, - [NotNullWhen(true)] out DebugInformationReaderProvider? debugInfoReaderProvider, - [NotNullWhen(true)] out MetadataReaderProvider? metadataReaderProvider) + catch (Exception e) { - // Read the metadata and symbols from the disk. Close the files as soon as we are done emitting the delta to minimize - // the time when they are being locked. Since we need to use the baseline that is produced by delta emit for the subsequent - // delta emit we need to keep the module metadata and symbol info backing the symbols of the baseline alive in memory. - // Alternatively, we could drop the data once we are done with emitting the delta and re-emit the baseline again - // when we need it next time and the module is loaded. + EditAndContinueService.Log.Write("Failed to create baseline for '{0}': {1}", projectId, e.Message); - diagnostics = default; - baseline = null; - debugInfoReaderProvider = null; - metadataReaderProvider = null; - - var success = false; - var fileBeingRead = compilationOutputs.PdbDisplayPath; - try + var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile); + diagnostics = [Diagnostic.Create(descriptor, Location.None, new[] { fileBeingRead, e.Message })]; + } + finally + { + if (!success) { - debugInfoReaderProvider = compilationOutputs.OpenPdb(); - if (debugInfoReaderProvider == null) - { - throw new FileNotFoundException(); - } - - var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader(); + debugInfoReaderProvider?.Dispose(); + metadataReaderProvider?.Dispose(); + } + } - fileBeingRead = compilationOutputs.AssemblyDisplayPath; + return false; + } - metadataReaderProvider = compilationOutputs.OpenAssemblyMetadata(prefetch: true); - if (metadataReaderProvider == null) - { - throw new FileNotFoundException(); - } + private static ImmutableDictionary> GroupToImmutableDictionary(IEnumerable> items) + where K : notnull + { + var builder = ImmutableDictionary.CreateBuilder>(); - var metadataReader = metadataReaderProvider.GetMetadataReader(); - var moduleMetadata = ModuleMetadata.CreateFromMetadata((IntPtr)metadataReader.MetadataPointer, metadataReader.MetadataLength); + foreach (var item in items) + { + builder.Add(item.Key, item.ToImmutableArray()); + } - baseline = EmitBaseline.CreateInitialBaseline( - compilation, - moduleMetadata, - debugInfoReader.GetDebugInfo, - debugInfoReader.GetLocalSignature, - debugInfoReader.IsPortable); + return builder.ToImmutable(); + } - success = true; - return true; - } - catch (Exception e) + public async ValueTask> GetDocumentDiagnosticsAsync(Document document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + { + try + { + if (_isDisposed) { - EditAndContinueService.Log.Write("Failed to create baseline for '{0}': {1}", projectId, e.Message); - - var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile); - diagnostics = [Diagnostic.Create(descriptor, Location.None, new[] { fileBeingRead, e.Message })]; + return []; } - finally + + // Not a C# or VB project. + var project = document.Project; + if (!project.SupportsEditAndContinue()) { - if (!success) - { - debugInfoReaderProvider?.Dispose(); - metadataReaderProvider?.Dispose(); - } + return []; } - return false; - } - - private static ImmutableDictionary> GroupToImmutableDictionary(IEnumerable> items) - where K : notnull - { - var builder = ImmutableDictionary.CreateBuilder>(); + // Document does not compile to the assembly (e.g. cshtml files, .g.cs files generated for completion only) + if (!document.DocumentState.SupportsEditAndContinue()) + { + return []; + } - foreach (var item in items) + // Do not analyze documents (and report diagnostics) of projects that have not been built. + // Allow user to make any changes in these documents, they won't be applied within the current debugging session. + // Do not report the file read error - it might be an intermittent issue. The error will be reported when the + // change is attempted to be applied. + var (mvid, _) = await GetProjectModuleIdAsync(project, cancellationToken).ConfigureAwait(false); + if (mvid == Guid.Empty) { - builder.Add(item.Key, item.ToImmutableArray()); + return []; } - return builder.ToImmutable(); - } + var (oldDocument, oldDocumentState) = await LastCommittedSolution.GetDocumentAndStateAsync(document.Id, document, cancellationToken).ConfigureAwait(false); + if (oldDocumentState is CommittedSolution.DocumentState.OutOfSync or + CommittedSolution.DocumentState.Indeterminate or + CommittedSolution.DocumentState.DesignTimeOnly) + { + // Do not report diagnostics for existing out-of-sync documents or design-time-only documents. + return []; + } - public async ValueTask> GetDocumentDiagnosticsAsync(Document document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) - { - try + var analysis = await EditSession.Analyses.GetDocumentAnalysisAsync(LastCommittedSolution, oldDocument, document, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); + if (analysis.HasChanges) { - if (_isDisposed) + // Once we detected a change in a document let the debugger know that the corresponding loaded module + // is about to be updated, so that it can start initializing it for EnC update, reducing the amount of time applying + // the change blocks the UI when the user "continues". + if (AddModulePreparedForUpdate(mvid)) { - return []; + // fire and forget: + _ = Task.Run(() => DebuggerService.PrepareModuleForUpdateAsync(mvid, cancellationToken), cancellationToken); } + } - // Not a C# or VB project. - var project = document.Project; - if (!project.SupportsEditAndContinue()) - { - return []; - } + if (analysis.RudeEditErrors.IsEmpty) + { + return []; + } - // Document does not compile to the assembly (e.g. cshtml files, .g.cs files generated for completion only) - if (!document.DocumentState.SupportsEditAndContinue()) - { - return []; - } + EditSession.Telemetry.LogRudeEditDiagnostics(analysis.RudeEditErrors, project.State.Attributes.TelemetryId); - // Do not analyze documents (and report diagnostics) of projects that have not been built. - // Allow user to make any changes in these documents, they won't be applied within the current debugging session. - // Do not report the file read error - it might be an intermittent issue. The error will be reported when the - // change is attempted to be applied. - var (mvid, _) = await GetProjectModuleIdAsync(project, cancellationToken).ConfigureAwait(false); - if (mvid == Guid.Empty) - { - return []; - } + // track the document, so that we can refresh or clean diagnostics at the end of edit session: + EditSession.TrackDocumentWithReportedDiagnostics(document.Id); - var (oldDocument, oldDocumentState) = await LastCommittedSolution.GetDocumentAndStateAsync(document.Id, document, cancellationToken).ConfigureAwait(false); - if (oldDocumentState is CommittedSolution.DocumentState.OutOfSync or - CommittedSolution.DocumentState.Indeterminate or - CommittedSolution.DocumentState.DesignTimeOnly) - { - // Do not report diagnostics for existing out-of-sync documents or design-time-only documents. - return []; - } + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + return analysis.RudeEditErrors.SelectAsArray((e, t) => e.ToDiagnostic(t), tree); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + return []; + } + } - var analysis = await EditSession.Analyses.GetDocumentAnalysisAsync(LastCommittedSolution, oldDocument, document, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); - if (analysis.HasChanges) - { - // Once we detected a change in a document let the debugger know that the corresponding loaded module - // is about to be updated, so that it can start initializing it for EnC update, reducing the amount of time applying - // the change blocks the UI when the user "continues". - if (AddModulePreparedForUpdate(mvid)) - { - // fire and forget: - _ = Task.Run(() => DebuggerService.PrepareModuleForUpdateAsync(mvid, cancellationToken), cancellationToken); - } - } + public async ValueTask EmitSolutionUpdateAsync( + Solution solution, + ActiveStatementSpanProvider activeStatementSpanProvider, + CancellationToken cancellationToken) + { + ThrowIfDisposed(); - if (analysis.RudeEditErrors.IsEmpty) - { - return []; - } + var updateId = new UpdateId(Id, Interlocked.Increment(ref _updateOrdinal)); - EditSession.Telemetry.LogRudeEditDiagnostics(analysis.RudeEditErrors, project.State.Attributes.TelemetryId); + var solutionUpdate = await EditSession.EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, updateId, cancellationToken).ConfigureAwait(false); - // track the document, so that we can refresh or clean diagnostics at the end of edit session: - EditSession.TrackDocumentWithReportedDiagnostics(document.Id); + solutionUpdate.Log(EditAndContinueService.Log, updateId); + _lastModuleUpdatesLog = solutionUpdate.ModuleUpdates.Updates; - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - return analysis.RudeEditErrors.SelectAsArray((e, t) => e.ToDiagnostic(t), tree); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return []; - } + if (solutionUpdate.ModuleUpdates.Status == ModuleUpdateStatus.Ready) + { + StorePendingUpdate(new PendingSolutionUpdate( + solution, + solutionUpdate.ProjectBaselines, + solutionUpdate.ModuleUpdates.Updates, + solutionUpdate.NonRemappableRegions)); } - public async ValueTask EmitSolutionUpdateAsync( - Solution solution, - ActiveStatementSpanProvider activeStatementSpanProvider, - CancellationToken cancellationToken) + // Note that we may return empty deltas if all updates have been deferred. + // The debugger will still call commit or discard on the update batch. + return new EmitSolutionUpdateResults() { - ThrowIfDisposed(); + ModuleUpdates = solutionUpdate.ModuleUpdates, + Diagnostics = solutionUpdate.Diagnostics, + RudeEdits = solutionUpdate.DocumentsWithRudeEdits, + SyntaxError = solutionUpdate.SyntaxError, + }; + } - var updateId = new UpdateId(Id, Interlocked.Increment(ref _updateOrdinal)); + public void CommitSolutionUpdate(out ImmutableArray documentsToReanalyze) + { + ThrowIfDisposed(); - var solutionUpdate = await EditSession.EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, updateId, cancellationToken).ConfigureAwait(false); + ImmutableDictionary>? newNonRemappableRegions = null; - solutionUpdate.Log(EditAndContinueService.Log, updateId); - _lastModuleUpdatesLog = solutionUpdate.ModuleUpdates.Updates; + var pendingUpdate = RetrievePendingUpdate(); + if (pendingUpdate is PendingSolutionUpdate pendingSolutionUpdate) + { + // Save new non-remappable regions for the next edit session. + // If no edits were made the pending list will be empty and we need to keep the previous regions. - if (solutionUpdate.ModuleUpdates.Status == ModuleUpdateStatus.Ready) - { - StorePendingUpdate(new PendingSolutionUpdate( - solution, - solutionUpdate.ProjectBaselines, - solutionUpdate.ModuleUpdates.Updates, - solutionUpdate.NonRemappableRegions)); - } + newNonRemappableRegions = GroupToImmutableDictionary( + from moduleRegions in pendingSolutionUpdate.NonRemappableRegions + from region in moduleRegions.Regions + group region.Region by new ManagedMethodId(moduleRegions.ModuleId, region.Method)); - // Note that we may return empty deltas if all updates have been deferred. - // The debugger will still call commit or discard on the update batch. - return new EmitSolutionUpdateResults() - { - ModuleUpdates = solutionUpdate.ModuleUpdates, - Diagnostics = solutionUpdate.Diagnostics, - RudeEdits = solutionUpdate.DocumentsWithRudeEdits, - SyntaxError = solutionUpdate.SyntaxError, - }; + if (newNonRemappableRegions.IsEmpty) + newNonRemappableRegions = null; + + LastCommittedSolution.CommitSolution(pendingSolutionUpdate.Solution); } - public void CommitSolutionUpdate(out ImmutableArray documentsToReanalyze) + // update baselines: + lock (_projectEmitBaselinesGuard) { - ThrowIfDisposed(); - - ImmutableDictionary>? newNonRemappableRegions = null; - - var pendingUpdate = RetrievePendingUpdate(); - if (pendingUpdate is PendingSolutionUpdate pendingSolutionUpdate) + foreach (var baseline in pendingUpdate.ProjectBaselines) { - // Save new non-remappable regions for the next edit session. - // If no edits were made the pending list will be empty and we need to keep the previous regions. + _projectBaselines[baseline.ProjectId] = baseline; + } + } - newNonRemappableRegions = GroupToImmutableDictionary( - from moduleRegions in pendingSolutionUpdate.NonRemappableRegions - from region in moduleRegions.Regions - group region.Region by new ManagedMethodId(moduleRegions.ModuleId, region.Method)); + _editSessionTelemetry.LogCommitted(); - if (newNonRemappableRegions.IsEmpty) - newNonRemappableRegions = null; + // Restart edit session with no active statements (switching to run mode). + RestartEditSession(newNonRemappableRegions, inBreakState: false, out documentsToReanalyze); + } - LastCommittedSolution.CommitSolution(pendingSolutionUpdate.Solution); - } + public void DiscardSolutionUpdate() + { + ThrowIfDisposed(); + _ = RetrievePendingUpdate(); + } - // update baselines: - lock (_projectEmitBaselinesGuard) + public async ValueTask>> GetBaseActiveStatementSpansAsync(Solution solution, ImmutableArray documentIds, CancellationToken cancellationToken) + { + try + { + if (_isDisposed || !EditSession.InBreakState) { - foreach (var baseline in pendingUpdate.ProjectBaselines) - { - _projectBaselines[baseline.ProjectId] = baseline; - } + return default; } - _editSessionTelemetry.LogCommitted(); + var baseActiveStatements = await EditSession.BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false); + using var _1 = PooledDictionary>.GetInstance(out var documentIndicesByMappedPath); + using var _2 = PooledHashSet.GetInstance(out var projectIds); - // Restart edit session with no active statements (switching to run mode). - RestartEditSession(newNonRemappableRegions, inBreakState: false, out documentsToReanalyze); - } - - public void DiscardSolutionUpdate() - { - ThrowIfDisposed(); - _ = RetrievePendingUpdate(); - } - - public async ValueTask>> GetBaseActiveStatementSpansAsync(Solution solution, ImmutableArray documentIds, CancellationToken cancellationToken) - { - try + // Construct map of mapped file path to a text document in the current solution + // and a set of projects these documents are contained in. + for (var i = 0; i < documentIds.Length; i++) { - if (_isDisposed || !EditSession.InBreakState) + var documentId = documentIds[i]; + + var document = await solution.GetTextDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); + if (document?.State.SupportsEditAndContinue() != true) { - return default; + // document has been deleted or doesn't support EnC (can't have an active statement anymore): + continue; } - var baseActiveStatements = await EditSession.BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false); - using var _1 = PooledDictionary>.GetInstance(out var documentIndicesByMappedPath); - using var _2 = PooledHashSet.GetInstance(out var projectIds); - - // Construct map of mapped file path to a text document in the current solution - // and a set of projects these documents are contained in. - for (var i = 0; i < documentIds.Length; i++) + if (!document.Project.SupportsEditAndContinue()) { - var documentId = documentIds[i]; + // document is in a project that does not support EnC + continue; + } - var document = await solution.GetTextDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); - if (document?.State.SupportsEditAndContinue() != true) - { - // document has been deleted or doesn't support EnC (can't have an active statement anymore): - continue; - } + Contract.ThrowIfNull(document.FilePath); - if (!document.Project.SupportsEditAndContinue()) - { - // document is in a project that does not support EnC - continue; - } + // Multiple documents may have the same path (linked file). + // The documents represent the files that #line directives map to. + // Documents that have the same path must have different project id. + documentIndicesByMappedPath.MultiAdd(document.FilePath, (documentId.ProjectId, i)); + projectIds.Add(documentId.ProjectId); + } - Contract.ThrowIfNull(document.FilePath); + using var _3 = PooledDictionary>.GetInstance( + out var activeStatementsInChangedDocuments); - // Multiple documents may have the same path (linked file). - // The documents represent the files that #line directives map to. - // Documents that have the same path must have different project id. - documentIndicesByMappedPath.MultiAdd(document.FilePath, (documentId.ProjectId, i)); - projectIds.Add(documentId.ProjectId); + // Analyze changed documents in projects containing active statements: + foreach (var projectId in projectIds) + { + var oldProject = LastCommittedSolution.GetProject(projectId); + if (oldProject == null) + { + // document is in a project that's been added to the solution + continue; } - using var _3 = PooledDictionary>.GetInstance( - out var activeStatementsInChangedDocuments); + var newProject = solution.GetRequiredProject(projectId); - // Analyze changed documents in projects containing active statements: - foreach (var projectId in projectIds) - { - var oldProject = LastCommittedSolution.GetProject(projectId); - if (oldProject == null) - { - // document is in a project that's been added to the solution - continue; - } + Debug.Assert(oldProject.SupportsEditAndContinue()); + Debug.Assert(newProject.SupportsEditAndContinue()); - var newProject = solution.GetRequiredProject(projectId); + var analyzer = newProject.Services.GetRequiredService(); - Debug.Assert(oldProject.SupportsEditAndContinue()); - Debug.Assert(newProject.SupportsEditAndContinue()); + await foreach (var documentId in EditSession.GetChangedDocumentsAsync(oldProject, newProject, cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); - var analyzer = newProject.Services.GetRequiredService(); + var newDocument = await solution.GetRequiredDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - await foreach (var documentId in EditSession.GetChangedDocumentsAsync(oldProject, newProject, cancellationToken).ConfigureAwait(false)) + var (oldDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newDocument.Id, newDocument, cancellationToken).ConfigureAwait(false); + if (oldDocument == null) { - cancellationToken.ThrowIfCancellationRequested(); - - var newDocument = await solution.GetRequiredDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - - var (oldDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newDocument.Id, newDocument, cancellationToken).ConfigureAwait(false); - if (oldDocument == null) - { - // Document is out-of-sync, can't reason about its content with respect to the binaries loaded in the debuggee. - continue; - } + // Document is out-of-sync, can't reason about its content with respect to the binaries loaded in the debuggee. + continue; + } - var oldDocumentActiveStatements = await baseActiveStatements.GetOldActiveStatementsAsync(analyzer, oldDocument, cancellationToken).ConfigureAwait(false); + var oldDocumentActiveStatements = await baseActiveStatements.GetOldActiveStatementsAsync(analyzer, oldDocument, cancellationToken).ConfigureAwait(false); - var analysis = await analyzer.AnalyzeDocumentAsync( - oldProject, - EditSession.BaseActiveStatements, - newDocument, - newActiveStatementSpans: [], - EditSession.Capabilities, - cancellationToken).ConfigureAwait(false); + var analysis = await analyzer.AnalyzeDocumentAsync( + oldProject, + EditSession.BaseActiveStatements, + newDocument, + newActiveStatementSpans: [], + EditSession.Capabilities, + cancellationToken).ConfigureAwait(false); - // Document content did not change or unable to determine active statement spans in a document with syntax errors: - if (!analysis.ActiveStatements.IsDefault) + // Document content did not change or unable to determine active statement spans in a document with syntax errors: + if (!analysis.ActiveStatements.IsDefault) + { + for (var i = 0; i < oldDocumentActiveStatements.Length; i++) { - for (var i = 0; i < oldDocumentActiveStatements.Length; i++) - { - // Note: It is possible that one active statement appears in multiple documents if the documents represent a linked file. - // Example (old and new contents): - // #if Condition #if Condition - // #line 1 a.txt #line 1 a.txt - // [|F(1);|] [|F(1000);|] - // #else #else - // #line 1 a.txt #line 1 a.txt - // [|F(2);|] [|F(2);|] - // #endif #endif - // - // In the new solution the AS spans are different depending on which document view of the same file we are looking at. - // Different views correspond to different projects. - activeStatementsInChangedDocuments.MultiAdd(oldDocumentActiveStatements[i].Statement, (analysis.DocumentId, analysis.ActiveStatements[i].Span)); - } + // Note: It is possible that one active statement appears in multiple documents if the documents represent a linked file. + // Example (old and new contents): + // #if Condition #if Condition + // #line 1 a.txt #line 1 a.txt + // [|F(1);|] [|F(1000);|] + // #else #else + // #line 1 a.txt #line 1 a.txt + // [|F(2);|] [|F(2);|] + // #endif #endif + // + // In the new solution the AS spans are different depending on which document view of the same file we are looking at. + // Different views correspond to different projects. + activeStatementsInChangedDocuments.MultiAdd(oldDocumentActiveStatements[i].Statement, (analysis.DocumentId, analysis.ActiveStatements[i].Span)); } } } + } - using var _4 = ArrayBuilder>.GetInstance(out var spans); - spans.AddMany([], documentIds.Length); + using var _4 = ArrayBuilder>.GetInstance(out var spans); + spans.AddMany([], documentIds.Length); - foreach (var (mappedPath, documentBaseActiveStatements) in baseActiveStatements.DocumentPathMap) + foreach (var (mappedPath, documentBaseActiveStatements) in baseActiveStatements.DocumentPathMap) + { + if (documentIndicesByMappedPath.TryGetValue(mappedPath, out var indices)) { - if (documentIndicesByMappedPath.TryGetValue(mappedPath, out var indices)) + // translate active statements from base solution to the new solution, if the documents they are contained in changed: + foreach (var (projectId, index) in indices) { - // translate active statements from base solution to the new solution, if the documents they are contained in changed: - foreach (var (projectId, index) in indices) - { - spans[index] = documentBaseActiveStatements.SelectAsArray( - activeStatement => + spans[index] = documentBaseActiveStatements.SelectAsArray( + activeStatement => + { + LinePositionSpan span; + DocumentId? unmappedDocumentId; + + if (activeStatementsInChangedDocuments.TryGetValue(activeStatement, out var newSpans)) { - LinePositionSpan span; - DocumentId? unmappedDocumentId; - - if (activeStatementsInChangedDocuments.TryGetValue(activeStatement, out var newSpans)) - { - (unmappedDocumentId, span) = newSpans.Single(ns => ns.unmappedDocumentId.ProjectId == projectId); - } - else - { - span = activeStatement.Span; - unmappedDocumentId = null; - } - - return new ActiveStatementSpan(activeStatement.Ordinal, span, activeStatement.Flags, unmappedDocumentId); - }); - } + (unmappedDocumentId, span) = newSpans.Single(ns => ns.unmappedDocumentId.ProjectId == projectId); + } + else + { + span = activeStatement.Span; + unmappedDocumentId = null; + } + + return new ActiveStatementSpan(activeStatement.Ordinal, span, activeStatement.Flags, unmappedDocumentId); + }); } } + } - documentIndicesByMappedPath.FreeValues(); - activeStatementsInChangedDocuments.FreeValues(); + documentIndicesByMappedPath.FreeValues(); + activeStatementsInChangedDocuments.FreeValues(); - return spans.ToImmutable(); - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) - { - throw ExceptionUtilities.Unreachable(); - } + return spans.ToImmutable(); + } + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) + { + throw ExceptionUtilities.Unreachable(); } + } - public async ValueTask> GetAdjustedActiveStatementSpansAsync(TextDocument mappedDocument, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + public async ValueTask> GetAdjustedActiveStatementSpansAsync(TextDocument mappedDocument, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + { + try { - try + if (_isDisposed || !EditSession.InBreakState || !mappedDocument.State.SupportsEditAndContinue() || !mappedDocument.Project.SupportsEditAndContinue()) { - if (_isDisposed || !EditSession.InBreakState || !mappedDocument.State.SupportsEditAndContinue() || !mappedDocument.Project.SupportsEditAndContinue()) - { - return []; - } + return []; + } - Contract.ThrowIfNull(mappedDocument.FilePath); + Contract.ThrowIfNull(mappedDocument.FilePath); - var newProject = mappedDocument.Project; - var newSolution = newProject.Solution; - var oldProject = LastCommittedSolution.GetProject(newProject.Id); - if (oldProject == null) - { - // TODO: https://github.com/dotnet/roslyn/issues/1204 - // Enumerate all documents of the new project. - return []; - } + var newProject = mappedDocument.Project; + var newSolution = newProject.Solution; + var oldProject = LastCommittedSolution.GetProject(newProject.Id); + if (oldProject == null) + { + // TODO: https://github.com/dotnet/roslyn/issues/1204 + // Enumerate all documents of the new project. + return []; + } - var baseActiveStatements = await EditSession.BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false); - if (!baseActiveStatements.DocumentPathMap.TryGetValue(mappedDocument.FilePath, out var oldMappedDocumentActiveStatements)) - { - // no active statements in this document - return []; - } + var baseActiveStatements = await EditSession.BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false); + if (!baseActiveStatements.DocumentPathMap.TryGetValue(mappedDocument.FilePath, out var oldMappedDocumentActiveStatements)) + { + // no active statements in this document + return []; + } - var newDocumentActiveStatementSpans = await activeStatementSpanProvider(mappedDocument.Id, mappedDocument.FilePath, cancellationToken).ConfigureAwait(false); - if (newDocumentActiveStatementSpans.IsEmpty) - { - return []; - } + var newDocumentActiveStatementSpans = await activeStatementSpanProvider(mappedDocument.Id, mappedDocument.FilePath, cancellationToken).ConfigureAwait(false); + if (newDocumentActiveStatementSpans.IsEmpty) + { + return []; + } - var analyzer = newProject.Services.GetRequiredService(); + var analyzer = newProject.Services.GetRequiredService(); - using var _ = ArrayBuilder.GetInstance(out var adjustedMappedSpans); + using var _ = ArrayBuilder.GetInstance(out var adjustedMappedSpans); - // Start with the current locations of the tracking spans. - adjustedMappedSpans.AddRange(newDocumentActiveStatementSpans); + // Start with the current locations of the tracking spans. + adjustedMappedSpans.AddRange(newDocumentActiveStatementSpans); - // Update tracking spans to the latest known locations of the active statements contained in changed documents based on their analysis. - await foreach (var unmappedDocumentId in EditSession.GetChangedDocumentsAsync(oldProject, newProject, cancellationToken).ConfigureAwait(false)) - { - var newUnmappedDocument = await newSolution.GetRequiredDocumentAsync(unmappedDocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + // Update tracking spans to the latest known locations of the active statements contained in changed documents based on their analysis. + await foreach (var unmappedDocumentId in EditSession.GetChangedDocumentsAsync(oldProject, newProject, cancellationToken).ConfigureAwait(false)) + { + var newUnmappedDocument = await newSolution.GetRequiredDocumentAsync(unmappedDocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - var (oldUnmappedDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newUnmappedDocument.Id, newUnmappedDocument, cancellationToken).ConfigureAwait(false); - if (oldUnmappedDocument == null) - { - // document out-of-date - continue; - } + var (oldUnmappedDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newUnmappedDocument.Id, newUnmappedDocument, cancellationToken).ConfigureAwait(false); + if (oldUnmappedDocument == null) + { + // document out-of-date + continue; + } - var analysis = await EditSession.Analyses.GetDocumentAnalysisAsync(LastCommittedSolution, oldUnmappedDocument, newUnmappedDocument, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); + var analysis = await EditSession.Analyses.GetDocumentAnalysisAsync(LastCommittedSolution, oldUnmappedDocument, newUnmappedDocument, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); - // Document content did not change or unable to determine active statement spans in a document with syntax errors: - if (!analysis.ActiveStatements.IsDefault) + // Document content did not change or unable to determine active statement spans in a document with syntax errors: + if (!analysis.ActiveStatements.IsDefault) + { + foreach (var activeStatement in analysis.ActiveStatements) { - foreach (var activeStatement in analysis.ActiveStatements) + var i = adjustedMappedSpans.FindIndex((s, ordinal) => s.Ordinal == ordinal, activeStatement.Ordinal); + if (i >= 0) { - var i = adjustedMappedSpans.FindIndex((s, ordinal) => s.Ordinal == ordinal, activeStatement.Ordinal); - if (i >= 0) - { - adjustedMappedSpans[i] = new ActiveStatementSpan(activeStatement.Ordinal, activeStatement.Span, activeStatement.Flags, unmappedDocumentId); - } + adjustedMappedSpans[i] = new ActiveStatementSpan(activeStatement.Ordinal, activeStatement.Span, activeStatement.Flags, unmappedDocumentId); } } } - - return adjustedMappedSpans.ToImmutable(); - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) - { - throw ExceptionUtilities.Unreachable(); } - } - private static void ReportTelemetry(DebuggingSessionTelemetry.Data data) + return adjustedMappedSpans.ToImmutable(); + } + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) { - // report telemetry (fire and forget): - _ = Task.Run(() => DebuggingSessionTelemetry.Log(data, Logger.Log, CorrelationIdFactory.GetNextId)); + throw ExceptionUtilities.Unreachable(); } + } - internal TestAccessor GetTestAccessor() - => new(this); + private static void ReportTelemetry(DebuggingSessionTelemetry.Data data) + { + // report telemetry (fire and forget): + _ = Task.Run(() => DebuggingSessionTelemetry.Log(data, Logger.Log, CorrelationIdFactory.GetNextId)); + } - internal readonly struct TestAccessor(DebuggingSession instance) - { - private readonly DebuggingSession _instance = instance; + internal TestAccessor GetTestAccessor() + => new(this); + + internal readonly struct TestAccessor(DebuggingSession instance) + { + private readonly DebuggingSession _instance = instance; - public ImmutableHashSet GetModulesPreparedForUpdate() + public ImmutableHashSet GetModulesPreparedForUpdate() + { + lock (_instance._modulesPreparedForUpdateGuard) { - lock (_instance._modulesPreparedForUpdateGuard) - { - return _instance._modulesPreparedForUpdate.ToImmutableHashSet(); - } + return _instance._modulesPreparedForUpdate.ToImmutableHashSet(); } + } - public EmitBaseline GetProjectEmitBaseline(ProjectId id) + public EmitBaseline GetProjectEmitBaseline(ProjectId id) + { + lock (_instance._projectEmitBaselinesGuard) { - lock (_instance._projectEmitBaselinesGuard) - { - return _instance._projectBaselines[id].EmitBaseline; - } + return _instance._projectBaselines[id].EmitBaseline; } + } - public ImmutableArray GetBaselineModuleReaders() - => _instance.GetBaselineModuleReaders(); + public ImmutableArray GetBaselineModuleReaders() + => _instance.GetBaselineModuleReaders(); - public PendingUpdate? GetPendingSolutionUpdate() - => _instance._pendingUpdate; + public PendingUpdate? GetPendingSolutionUpdate() + => _instance._pendingUpdate; - public void SetTelemetryLogger(Action logger, Func getNextId) - => _instance._reportTelemetry = data => DebuggingSessionTelemetry.Log(data, logger, getNextId); - } + public void SetTelemetryLogger(Action logger, Func getNextId) + => _instance._reportTelemetry = data => DebuggingSessionTelemetry.Log(data, logger, getNextId); } } diff --git a/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs b/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs index 49ff8dffe037b..2b3d5186583d6 100644 --- a/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs +++ b/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs @@ -8,151 +8,150 @@ using System.Linq; using Microsoft.CodeAnalysis.Internal.Log; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal sealed class DebuggingSessionTelemetry(Guid solutionSessionId) { - internal sealed class DebuggingSessionTelemetry(Guid solutionSessionId) + internal readonly struct Data(DebuggingSessionTelemetry telemetry) { - internal readonly struct Data(DebuggingSessionTelemetry telemetry) - { - public readonly Guid SolutionSessionId = telemetry._solutionSessionId; - public readonly ImmutableArray EditSessionData = telemetry._editSessionData.ToImmutableArray(); - public readonly int EmptyEditSessionCount = telemetry._emptyEditSessionCount; - public readonly int EmptyHotReloadEditSessionCount = telemetry._emptyHotReloadEditSessionCount; - } + public readonly Guid SolutionSessionId = telemetry._solutionSessionId; + public readonly ImmutableArray EditSessionData = telemetry._editSessionData.ToImmutableArray(); + public readonly int EmptyEditSessionCount = telemetry._emptyEditSessionCount; + public readonly int EmptyHotReloadEditSessionCount = telemetry._emptyHotReloadEditSessionCount; + } - private readonly object _guard = new(); + private readonly object _guard = new(); - private readonly Guid _solutionSessionId = solutionSessionId; - private readonly List _editSessionData = []; - private int _emptyEditSessionCount; - private int _emptyHotReloadEditSessionCount; + private readonly Guid _solutionSessionId = solutionSessionId; + private readonly List _editSessionData = []; + private int _emptyEditSessionCount; + private int _emptyHotReloadEditSessionCount; - public Data GetDataAndClear() + public Data GetDataAndClear() + { + lock (_guard) { - lock (_guard) - { - var data = new Data(this); - _editSessionData.Clear(); - _emptyEditSessionCount = 0; - _emptyHotReloadEditSessionCount = 0; - return data; - } + var data = new Data(this); + _editSessionData.Clear(); + _emptyEditSessionCount = 0; + _emptyHotReloadEditSessionCount = 0; + return data; } + } - public void LogEditSession(EditSessionTelemetry.Data editSessionTelemetryData) + public void LogEditSession(EditSessionTelemetry.Data editSessionTelemetryData) + { + lock (_guard) { - lock (_guard) + if (editSessionTelemetryData.IsEmpty) { - if (editSessionTelemetryData.IsEmpty) - { - if (editSessionTelemetryData.InBreakState) - _emptyEditSessionCount++; - else - _emptyHotReloadEditSessionCount++; - } + if (editSessionTelemetryData.InBreakState) + _emptyEditSessionCount++; else - { - _editSessionData.Add(editSessionTelemetryData); - } + _emptyHotReloadEditSessionCount++; + } + else + { + _editSessionData.Add(editSessionTelemetryData); } } + } - // Example query: - // - // RawEventsVS - // | where EventName == "vs/ide/vbcs/debugging/encsession/editsession" - // | project EventId, EventName, Properties, Measures, MacAddressHash - // | where Measures["vs.ide.vbcs.debugging.encsession.editsession.emitdeltaerroridcount"] == 0 - // | extend HasValidChanges = Properties["vs.ide.vbcs.debugging.encsession.editsession.hadvalidchanges"] == "True" - // | where HasValidChanges - // | extend IsHotReload = Properties["vs.ide.vbcs.debugging.encsession.editsession.inbreakstate"] == "False" - // | extend IsEnC = not(IsHotReload) - // | summarize HotReloadUsers = dcountif(MacAddressHash, IsHotReload), - // EncUsers = dcountif(MacAddressHash, IsEnC) - public static void Log(Data data, Action log, Func getNextId) - { - const string SessionId = nameof(SessionId); - const string EditSessionId = nameof(EditSessionId); + // Example query: + // + // RawEventsVS + // | where EventName == "vs/ide/vbcs/debugging/encsession/editsession" + // | project EventId, EventName, Properties, Measures, MacAddressHash + // | where Measures["vs.ide.vbcs.debugging.encsession.editsession.emitdeltaerroridcount"] == 0 + // | extend HasValidChanges = Properties["vs.ide.vbcs.debugging.encsession.editsession.hadvalidchanges"] == "True" + // | where HasValidChanges + // | extend IsHotReload = Properties["vs.ide.vbcs.debugging.encsession.editsession.inbreakstate"] == "False" + // | extend IsEnC = not(IsHotReload) + // | summarize HotReloadUsers = dcountif(MacAddressHash, IsHotReload), + // EncUsers = dcountif(MacAddressHash, IsEnC) + public static void Log(Data data, Action log, Func getNextId) + { + const string SessionId = nameof(SessionId); + const string EditSessionId = nameof(EditSessionId); - var debugSessionId = getNextId(); + var debugSessionId = getNextId(); - log(FunctionId.Debugging_EncSession, KeyValueLogMessage.Create(map => - { - map["SolutionSessionId"] = data.SolutionSessionId.ToString("B").ToUpperInvariant(); - map[SessionId] = debugSessionId; - map["SessionCount"] = data.EditSessionData.Count(session => session.InBreakState); - map["EmptySessionCount"] = data.EmptyEditSessionCount; - map["HotReloadSessionCount"] = data.EditSessionData.Count(session => !session.InBreakState); - map["EmptyHotReloadSessionCount"] = data.EmptyHotReloadEditSessionCount; - })); + log(FunctionId.Debugging_EncSession, KeyValueLogMessage.Create(map => + { + map["SolutionSessionId"] = data.SolutionSessionId.ToString("B").ToUpperInvariant(); + map[SessionId] = debugSessionId; + map["SessionCount"] = data.EditSessionData.Count(session => session.InBreakState); + map["EmptySessionCount"] = data.EmptyEditSessionCount; + map["HotReloadSessionCount"] = data.EditSessionData.Count(session => !session.InBreakState); + map["EmptyHotReloadSessionCount"] = data.EmptyHotReloadEditSessionCount; + })); + + foreach (var editSessionData in data.EditSessionData) + { + var editSessionId = getNextId(); - foreach (var editSessionData in data.EditSessionData) + log(FunctionId.Debugging_EncSession_EditSession, KeyValueLogMessage.Create(map => { - var editSessionId = getNextId(); + map[SessionId] = debugSessionId; + map[EditSessionId] = editSessionId; - log(FunctionId.Debugging_EncSession_EditSession, KeyValueLogMessage.Create(map => - { - map[SessionId] = debugSessionId; - map[EditSessionId] = editSessionId; + map["HadCompilationErrors"] = editSessionData.HadCompilationErrors; + map["HadRudeEdits"] = editSessionData.HadRudeEdits; - map["HadCompilationErrors"] = editSessionData.HadCompilationErrors; - map["HadRudeEdits"] = editSessionData.HadRudeEdits; + // Changes made to source code during the edit session were valid - they were significant and no rude edits were reported. + // The changes still might fail to emit (see EmitDeltaErrorIdCount). + map["HadValidChanges"] = editSessionData.HadValidChanges; + map["HadValidInsignificantChanges"] = editSessionData.HadValidInsignificantChanges; - // Changes made to source code during the edit session were valid - they were significant and no rude edits were reported. - // The changes still might fail to emit (see EmitDeltaErrorIdCount). - map["HadValidChanges"] = editSessionData.HadValidChanges; - map["HadValidInsignificantChanges"] = editSessionData.HadValidInsignificantChanges; + map["RudeEditsCount"] = editSessionData.RudeEdits.Length; - map["RudeEditsCount"] = editSessionData.RudeEdits.Length; + // Number of emit errors. + map["EmitDeltaErrorIdCount"] = editSessionData.EmitErrorIds.Length; - // Number of emit errors. - map["EmitDeltaErrorIdCount"] = editSessionData.EmitErrorIds.Length; + // False for Hot Reload session, true or missing for EnC session (missing in older data that did not have this property). + map["InBreakState"] = editSessionData.InBreakState; - // False for Hot Reload session, true or missing for EnC session (missing in older data that did not have this property). - map["InBreakState"] = editSessionData.InBreakState; + map["Capabilities"] = (int)editSessionData.Capabilities; - map["Capabilities"] = (int)editSessionData.Capabilities; + // Ids of all projects whose binaries were successfully updated during the session. + map["ProjectIdsWithAppliedChanges"] = editSessionData.Committed ? editSessionData.ProjectsWithValidDelta.Select(ProjectIdToPii) : ""; - // Ids of all projects whose binaries were successfully updated during the session. - map["ProjectIdsWithAppliedChanges"] = editSessionData.Committed ? editSessionData.ProjectsWithValidDelta.Select(ProjectIdToPii) : ""; + // Total milliseconds it took to emit the delta in this edit session. + map["EmitDifferenceMilliseconds"] = (long)editSessionData.EmitDifferenceTime.TotalMilliseconds; - // Total milliseconds it took to emit the delta in this edit session. - map["EmitDifferenceMilliseconds"] = (long)editSessionData.EmitDifferenceTime.TotalMilliseconds; + // Total milliseconds it took to analyze all documents that contributed to the changes that were + // attempted to be applied (whether or not the applications was successful) in this edit session. + // Includes analysis that had been performed asynchronously before "apply changes" was triggered + // (if we reused analysis results that were calculated by EnC analyzer for rude edit reporting). + map["TotalAnalysisMilliseconds"] = (long)editSessionData.AnalysisTime.TotalMilliseconds; + })); - // Total milliseconds it took to analyze all documents that contributed to the changes that were - // attempted to be applied (whether or not the applications was successful) in this edit session. - // Includes analysis that had been performed asynchronously before "apply changes" was triggered - // (if we reused analysis results that were calculated by EnC analyzer for rude edit reporting). - map["TotalAnalysisMilliseconds"] = (long)editSessionData.AnalysisTime.TotalMilliseconds; + foreach (var errorId in editSessionData.EmitErrorIds) + { + log(FunctionId.Debugging_EncSession_EditSession_EmitDeltaErrorId, KeyValueLogMessage.Create(map => + { + map[SessionId] = debugSessionId; + map[EditSessionId] = editSessionId; + map["ErrorId"] = errorId; })); + } - foreach (var errorId in editSessionData.EmitErrorIds) - { - log(FunctionId.Debugging_EncSession_EditSession_EmitDeltaErrorId, KeyValueLogMessage.Create(map => - { - map[SessionId] = debugSessionId; - map[EditSessionId] = editSessionId; - map["ErrorId"] = errorId; - })); - } - - foreach (var (editKind, syntaxKind, projectId) in editSessionData.RudeEdits) + foreach (var (editKind, syntaxKind, projectId) in editSessionData.RudeEdits) + { + log(FunctionId.Debugging_EncSession_EditSession_RudeEdit, KeyValueLogMessage.Create(map => { - log(FunctionId.Debugging_EncSession_EditSession_RudeEdit, KeyValueLogMessage.Create(map => - { - map[SessionId] = debugSessionId; - map[EditSessionId] = editSessionId; - - map["RudeEditKind"] = editKind; - map["RudeEditSyntaxKind"] = syntaxKind; - map["RudeEditBlocking"] = editSessionData.HadRudeEdits; - map["RudeEditProjectId"] = ProjectIdToPii(projectId); - })); - } - - static PiiValue ProjectIdToPii(Guid projectId) - => new(projectId.ToString("B").ToUpperInvariant()); + map[SessionId] = debugSessionId; + map[EditSessionId] = editSessionId; + + map["RudeEditKind"] = editKind; + map["RudeEditSyntaxKind"] = syntaxKind; + map["RudeEditBlocking"] = editSessionData.HadRudeEdits; + map["RudeEditProjectId"] = ProjectIdToPii(projectId); + })); } + + static PiiValue ProjectIdToPii(Guid projectId) + => new(projectId.ToString("B").ToUpperInvariant()); } } } diff --git a/src/Features/Core/Portable/EditAndContinue/DocumentActiveStatementChanges.cs b/src/Features/Core/Portable/EditAndContinue/DocumentActiveStatementChanges.cs index a57b97bf4d3c8..3d77a22e13c1d 100644 --- a/src/Features/Core/Portable/EditAndContinue/DocumentActiveStatementChanges.cs +++ b/src/Features/Core/Portable/EditAndContinue/DocumentActiveStatementChanges.cs @@ -6,43 +6,42 @@ using System.Diagnostics; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal readonly struct DocumentActiveStatementChanges { - internal readonly struct DocumentActiveStatementChanges - { - public readonly ImmutableArray OldStatements; - public readonly ImmutableArray NewStatements; - public readonly ImmutableArray> NewExceptionRegions; + public readonly ImmutableArray OldStatements; + public readonly ImmutableArray NewStatements; + public readonly ImmutableArray> NewExceptionRegions; - public DocumentActiveStatementChanges( - ImmutableArray oldSpans, - ImmutableArray newStatements, - ImmutableArray> newExceptionRegions) - { - Contract.ThrowIfFalse(oldSpans.Length == newStatements.Length); - Contract.ThrowIfFalse(oldSpans.Length == newExceptionRegions.Length); + public DocumentActiveStatementChanges( + ImmutableArray oldSpans, + ImmutableArray newStatements, + ImmutableArray> newExceptionRegions) + { + Contract.ThrowIfFalse(oldSpans.Length == newStatements.Length); + Contract.ThrowIfFalse(oldSpans.Length == newExceptionRegions.Length); #if DEBUG - for (var i = 0; i < oldSpans.Length; i++) - { - // old and new exception region counts must match: - Debug.Assert(oldSpans[i].ExceptionRegions.Spans.Length == newExceptionRegions[i].Length); - } + for (var i = 0; i < oldSpans.Length; i++) + { + // old and new exception region counts must match: + Debug.Assert(oldSpans[i].ExceptionRegions.Spans.Length == newExceptionRegions[i].Length); + } #endif - OldStatements = oldSpans; - NewStatements = newStatements; - NewExceptionRegions = newExceptionRegions; - } + OldStatements = oldSpans; + NewStatements = newStatements; + NewExceptionRegions = newExceptionRegions; + } - public void Deconstruct( - out ImmutableArray oldStatements, - out ImmutableArray newStatements, - out ImmutableArray> newExceptionRegions) - { - oldStatements = OldStatements; - newStatements = NewStatements; - newExceptionRegions = NewExceptionRegions; - } + public void Deconstruct( + out ImmutableArray oldStatements, + out ImmutableArray newStatements, + out ImmutableArray> newExceptionRegions) + { + oldStatements = OldStatements; + newStatements = NewStatements; + newExceptionRegions = NewExceptionRegions; } } diff --git a/src/Features/Core/Portable/EditAndContinue/DocumentAnalysisResults.cs b/src/Features/Core/Portable/EditAndContinue/DocumentAnalysisResults.cs index ecfa08de484db..29b2995679e37 100644 --- a/src/Features/Core/Portable/EditAndContinue/DocumentAnalysisResults.cs +++ b/src/Features/Core/Portable/EditAndContinue/DocumentAnalysisResults.cs @@ -10,208 +10,207 @@ using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using System; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal sealed class DocumentAnalysisResults { - internal sealed class DocumentAnalysisResults + /// + /// The state of the document the results are calculated for. + /// + public DocumentId DocumentId { get; } + + /// + /// Document file path for logging. + /// + public string FilePath; + + /// + /// Spans of active statements in the document, or null if the document has syntax errors or has not changed. + /// Calculated even in presence of rude edits so that the active statements can be rendered in the editor. + /// + public ImmutableArray ActiveStatements { get; } + + /// + /// Diagnostics for rude edits in the document, or empty if the document is unchanged or has syntax errors. + /// If the compilation has semantic errors only syntactic rude edits are calculated. + /// + public ImmutableArray RudeEditErrors { get; } + + /// + /// The first syntax error, or null if the document does not have syntax errors reported by the compiler. + /// + public Diagnostic? SyntaxError { get; } + + /// + /// Edits made in the document, or null if the document is unchanged, has syntax errors or rude edits. + /// + public ImmutableArray SemanticEdits { get; } + + /// + /// Exception regions -- spans of catch and finally handlers that surround the active statements. + /// + /// Null if the document has syntax errors, rude edits or has not changed. + /// + /// + /// Null if there are any rude edit diagnostics. + /// + /// Otherwise, each active statement in has a corresponding slot in . + /// + /// Exception regions for each EH block/clause are marked as |...|. + /// try { ... AS ... } |catch { } finally { }| + /// try { } |catch { ... AS ... }| finally { } + /// try { } catch { } |finally { ... AS ... }| + /// + /// Contains a minimal set of spans that cover the handlers. + /// For example: + /// try { } |finally { try { ... AS ... } catch { } }| + /// try { } |finally { try { } catch { ... AS ... } }| + /// try { try { } |finally { ... AS ... }| } |catch { } catch { } finally { }| + /// + public ImmutableArray> ExceptionRegions { get; } + + /// + /// Line edits in the document (or mapped documents), or null if the document has syntax errors, rude edits or has not changed. + /// + /// + /// Grouped by file name and updates in each group are ordered by . + /// Each entry in the group applies the delta of - + /// to all lines in range [, next entry's ). + /// + public ImmutableArray LineEdits { get; } + + /// + /// Capabilities that are required for the updates made in this document. + /// if the document does not have valid changes. + /// + public EditAndContinueCapabilities RequiredCapabilities { get; } + + /// + /// Time span it took to perform the analysis. + /// + public TimeSpan ElapsedTime { get; } + + /// + /// Document contains errors that block EnC analysis. + /// + public bool HasSyntaxErrors { get; } + + /// + /// Document contains changes. + /// + public bool HasChanges { get; } + + public DocumentAnalysisResults( + DocumentId documentId, + string filePath, + ImmutableArray activeStatementsOpt, + ImmutableArray rudeEdits, + Diagnostic? syntaxError, + ImmutableArray semanticEditsOpt, + ImmutableArray> exceptionRegionsOpt, + ImmutableArray lineEditsOpt, + EditAndContinueCapabilities requiredCapabilities, + TimeSpan elapsedTime, + bool hasChanges, + bool hasSyntaxErrors) { - /// - /// The state of the document the results are calculated for. - /// - public DocumentId DocumentId { get; } - - /// - /// Document file path for logging. - /// - public string FilePath; - - /// - /// Spans of active statements in the document, or null if the document has syntax errors or has not changed. - /// Calculated even in presence of rude edits so that the active statements can be rendered in the editor. - /// - public ImmutableArray ActiveStatements { get; } - - /// - /// Diagnostics for rude edits in the document, or empty if the document is unchanged or has syntax errors. - /// If the compilation has semantic errors only syntactic rude edits are calculated. - /// - public ImmutableArray RudeEditErrors { get; } - - /// - /// The first syntax error, or null if the document does not have syntax errors reported by the compiler. - /// - public Diagnostic? SyntaxError { get; } - - /// - /// Edits made in the document, or null if the document is unchanged, has syntax errors or rude edits. - /// - public ImmutableArray SemanticEdits { get; } - - /// - /// Exception regions -- spans of catch and finally handlers that surround the active statements. - /// - /// Null if the document has syntax errors, rude edits or has not changed. - /// - /// - /// Null if there are any rude edit diagnostics. - /// - /// Otherwise, each active statement in has a corresponding slot in . - /// - /// Exception regions for each EH block/clause are marked as |...|. - /// try { ... AS ... } |catch { } finally { }| - /// try { } |catch { ... AS ... }| finally { } - /// try { } catch { } |finally { ... AS ... }| - /// - /// Contains a minimal set of spans that cover the handlers. - /// For example: - /// try { } |finally { try { ... AS ... } catch { } }| - /// try { } |finally { try { } catch { ... AS ... } }| - /// try { try { } |finally { ... AS ... }| } |catch { } catch { } finally { }| - /// - public ImmutableArray> ExceptionRegions { get; } - - /// - /// Line edits in the document (or mapped documents), or null if the document has syntax errors, rude edits or has not changed. - /// - /// - /// Grouped by file name and updates in each group are ordered by . - /// Each entry in the group applies the delta of - - /// to all lines in range [, next entry's ). - /// - public ImmutableArray LineEdits { get; } - - /// - /// Capabilities that are required for the updates made in this document. - /// if the document does not have valid changes. - /// - public EditAndContinueCapabilities RequiredCapabilities { get; } - - /// - /// Time span it took to perform the analysis. - /// - public TimeSpan ElapsedTime { get; } - - /// - /// Document contains errors that block EnC analysis. - /// - public bool HasSyntaxErrors { get; } - - /// - /// Document contains changes. - /// - public bool HasChanges { get; } - - public DocumentAnalysisResults( - DocumentId documentId, - string filePath, - ImmutableArray activeStatementsOpt, - ImmutableArray rudeEdits, - Diagnostic? syntaxError, - ImmutableArray semanticEditsOpt, - ImmutableArray> exceptionRegionsOpt, - ImmutableArray lineEditsOpt, - EditAndContinueCapabilities requiredCapabilities, - TimeSpan elapsedTime, - bool hasChanges, - bool hasSyntaxErrors) + Debug.Assert(!rudeEdits.IsDefault); + + if (hasSyntaxErrors || !hasChanges) { - Debug.Assert(!rudeEdits.IsDefault); + Debug.Assert(activeStatementsOpt.IsDefault); + Debug.Assert(semanticEditsOpt.IsDefault); + Debug.Assert(exceptionRegionsOpt.IsDefault); + Debug.Assert(lineEditsOpt.IsDefault); + Debug.Assert(syntaxError != null || !rudeEdits.IsEmpty || !hasChanges); + Debug.Assert(requiredCapabilities == EditAndContinueCapabilities.None); + } + else + { + Debug.Assert(!activeStatementsOpt.IsDefault); + Debug.Assert(syntaxError == null); - if (hasSyntaxErrors || !hasChanges) + if (!rudeEdits.IsEmpty) { - Debug.Assert(activeStatementsOpt.IsDefault); Debug.Assert(semanticEditsOpt.IsDefault); Debug.Assert(exceptionRegionsOpt.IsDefault); Debug.Assert(lineEditsOpt.IsDefault); - Debug.Assert(syntaxError != null || !rudeEdits.IsEmpty || !hasChanges); Debug.Assert(requiredCapabilities == EditAndContinueCapabilities.None); } else { - Debug.Assert(!activeStatementsOpt.IsDefault); - Debug.Assert(syntaxError == null); - - if (!rudeEdits.IsEmpty) - { - Debug.Assert(semanticEditsOpt.IsDefault); - Debug.Assert(exceptionRegionsOpt.IsDefault); - Debug.Assert(lineEditsOpt.IsDefault); - Debug.Assert(requiredCapabilities == EditAndContinueCapabilities.None); - } - else - { - Debug.Assert(!semanticEditsOpt.IsDefault); - Debug.Assert(!exceptionRegionsOpt.IsDefault); - Debug.Assert(!lineEditsOpt.IsDefault); - - // no duplicate files in line edits: - Debug.Assert(lineEditsOpt.Select(edit => edit.FileName).Distinct().Count() == lineEditsOpt.Length); - - // line updates are sorted: - Debug.Assert(lineEditsOpt.All(documentLineEdits => documentLineEdits.LineUpdates.IsSorted(Comparer.Create( - (x, y) => x.OldLine.CompareTo(y.OldLine))))); - - Debug.Assert(exceptionRegionsOpt.Length == activeStatementsOpt.Length); - Debug.Assert(requiredCapabilities != EditAndContinueCapabilities.None); - } - } + Debug.Assert(!semanticEditsOpt.IsDefault); + Debug.Assert(!exceptionRegionsOpt.IsDefault); + Debug.Assert(!lineEditsOpt.IsDefault); - DocumentId = documentId; - FilePath = filePath; - RudeEditErrors = rudeEdits; - SyntaxError = syntaxError; - SemanticEdits = semanticEditsOpt; - ActiveStatements = activeStatementsOpt; - ExceptionRegions = exceptionRegionsOpt; - LineEdits = lineEditsOpt; - RequiredCapabilities = requiredCapabilities; - ElapsedTime = elapsedTime; - HasSyntaxErrors = hasSyntaxErrors; - HasChanges = hasChanges; + // no duplicate files in line edits: + Debug.Assert(lineEditsOpt.Select(edit => edit.FileName).Distinct().Count() == lineEditsOpt.Length); + + // line updates are sorted: + Debug.Assert(lineEditsOpt.All(documentLineEdits => documentLineEdits.LineUpdates.IsSorted(Comparer.Create( + (x, y) => x.OldLine.CompareTo(y.OldLine))))); + + Debug.Assert(exceptionRegionsOpt.Length == activeStatementsOpt.Length); + Debug.Assert(requiredCapabilities != EditAndContinueCapabilities.None); + } } - public bool HasChangesAndErrors - => HasChanges && (HasSyntaxErrors || !RudeEditErrors.IsEmpty); - - public bool HasChangesAndSyntaxErrors - => HasChanges && HasSyntaxErrors; - - public bool HasSignificantValidChanges - => HasChanges && (!SemanticEdits.IsDefaultOrEmpty || !LineEdits.IsDefaultOrEmpty); - - /// - /// Report errors blocking the document analysis. - /// - public static DocumentAnalysisResults SyntaxErrors(DocumentId documentId, string filePath, ImmutableArray rudeEdits, Diagnostic? syntaxError, TimeSpan elapsedTime, bool hasChanges) - => new( - documentId, - filePath, - activeStatementsOpt: default, - rudeEdits, - syntaxError, - semanticEditsOpt: default, - exceptionRegionsOpt: default, - lineEditsOpt: default, - EditAndContinueCapabilities.None, - elapsedTime, - hasChanges, - hasSyntaxErrors: true); - - /// - /// Report unchanged document results. - /// - public static DocumentAnalysisResults Unchanged(DocumentId documentId, string filePath, TimeSpan elapsedTime) - => new( - documentId, - filePath, - activeStatementsOpt: default, - rudeEdits: [], - syntaxError: null, - semanticEditsOpt: default, - exceptionRegionsOpt: default, - lineEditsOpt: default, - EditAndContinueCapabilities.None, - elapsedTime, - hasChanges: false, - hasSyntaxErrors: false); + DocumentId = documentId; + FilePath = filePath; + RudeEditErrors = rudeEdits; + SyntaxError = syntaxError; + SemanticEdits = semanticEditsOpt; + ActiveStatements = activeStatementsOpt; + ExceptionRegions = exceptionRegionsOpt; + LineEdits = lineEditsOpt; + RequiredCapabilities = requiredCapabilities; + ElapsedTime = elapsedTime; + HasSyntaxErrors = hasSyntaxErrors; + HasChanges = hasChanges; } + + public bool HasChangesAndErrors + => HasChanges && (HasSyntaxErrors || !RudeEditErrors.IsEmpty); + + public bool HasChangesAndSyntaxErrors + => HasChanges && HasSyntaxErrors; + + public bool HasSignificantValidChanges + => HasChanges && (!SemanticEdits.IsDefaultOrEmpty || !LineEdits.IsDefaultOrEmpty); + + /// + /// Report errors blocking the document analysis. + /// + public static DocumentAnalysisResults SyntaxErrors(DocumentId documentId, string filePath, ImmutableArray rudeEdits, Diagnostic? syntaxError, TimeSpan elapsedTime, bool hasChanges) + => new( + documentId, + filePath, + activeStatementsOpt: default, + rudeEdits, + syntaxError, + semanticEditsOpt: default, + exceptionRegionsOpt: default, + lineEditsOpt: default, + EditAndContinueCapabilities.None, + elapsedTime, + hasChanges, + hasSyntaxErrors: true); + + /// + /// Report unchanged document results. + /// + public static DocumentAnalysisResults Unchanged(DocumentId documentId, string filePath, TimeSpan elapsedTime) + => new( + documentId, + filePath, + activeStatementsOpt: default, + rudeEdits: [], + syntaxError: null, + semanticEditsOpt: default, + exceptionRegionsOpt: default, + lineEditsOpt: default, + EditAndContinueCapabilities.None, + elapsedTime, + hasChanges: false, + hasSyntaxErrors: false); } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueCapabilities.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueCapabilities.cs index 5f8cb19b5d43a..69800c49f88d5 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueCapabilities.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueCapabilities.cs @@ -6,124 +6,123 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.PooledObjects; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// The capabilities that the runtime has with respect to edit and continue +/// +[Flags] +internal enum EditAndContinueCapabilities { + None = 0, + /// - /// The capabilities that the runtime has with respect to edit and continue + /// Edit and continue is generally available with the set of capabilities that Mono 6, .NET Framework and .NET 5 have in common. /// - [Flags] - internal enum EditAndContinueCapabilities - { - None = 0, - - /// - /// Edit and continue is generally available with the set of capabilities that Mono 6, .NET Framework and .NET 5 have in common. - /// - Baseline = 1 << 0, - - /// - /// Adding a static or instance method to an existing type. - /// - AddMethodToExistingType = 1 << 1, - - /// - /// Adding a static field to an existing type. - /// - AddStaticFieldToExistingType = 1 << 2, - - /// - /// Adding an instance field to an existing type. - /// - AddInstanceFieldToExistingType = 1 << 3, - - /// - /// Creating a new type definition. - /// - NewTypeDefinition = 1 << 4, - - /// - /// Adding, updating and deleting of custom attributes (as distinct from pseudo-custom attributes) - /// - ChangeCustomAttributes = 1 << 5, - - /// - /// Whether the runtime supports updating the Param table, and hence related edits (eg parameter renames) - /// - UpdateParameters = 1 << 6, - - /// - /// Adding a static or instance method, property or event to an existing type (without backing fields), such that the method and/or the type are generic. - /// - GenericAddMethodToExistingType = 1 << 7, - - /// - /// Updating an existing static or instance method, property or event (without backing fields) that is generic and/or contained in a generic type. - /// - GenericUpdateMethod = 1 << 8, - - /// - /// Adding a static or instance field to an existing generic type. - /// - GenericAddFieldToExistingType = 1 << 9, - } + Baseline = 1 << 0, + + /// + /// Adding a static or instance method to an existing type. + /// + AddMethodToExistingType = 1 << 1, + + /// + /// Adding a static field to an existing type. + /// + AddStaticFieldToExistingType = 1 << 2, + + /// + /// Adding an instance field to an existing type. + /// + AddInstanceFieldToExistingType = 1 << 3, + + /// + /// Creating a new type definition. + /// + NewTypeDefinition = 1 << 4, + + /// + /// Adding, updating and deleting of custom attributes (as distinct from pseudo-custom attributes) + /// + ChangeCustomAttributes = 1 << 5, + + /// + /// Whether the runtime supports updating the Param table, and hence related edits (eg parameter renames) + /// + UpdateParameters = 1 << 6, + + /// + /// Adding a static or instance method, property or event to an existing type (without backing fields), such that the method and/or the type are generic. + /// + GenericAddMethodToExistingType = 1 << 7, + + /// + /// Updating an existing static or instance method, property or event (without backing fields) that is generic and/or contained in a generic type. + /// + GenericUpdateMethod = 1 << 8, + + /// + /// Adding a static or instance field to an existing generic type. + /// + GenericAddFieldToExistingType = 1 << 9, +} - internal static class EditAndContinueCapabilitiesParser +internal static class EditAndContinueCapabilitiesParser +{ + public static EditAndContinueCapabilities Parse(ImmutableArray capabilities) { - public static EditAndContinueCapabilities Parse(ImmutableArray capabilities) - { - var caps = EditAndContinueCapabilities.None; + var caps = EditAndContinueCapabilities.None; - foreach (var capability in capabilities) + foreach (var capability in capabilities) + { + caps |= capability switch { - caps |= capability switch - { - nameof(EditAndContinueCapabilities.Baseline) => EditAndContinueCapabilities.Baseline, - nameof(EditAndContinueCapabilities.AddMethodToExistingType) => EditAndContinueCapabilities.AddMethodToExistingType, - nameof(EditAndContinueCapabilities.AddStaticFieldToExistingType) => EditAndContinueCapabilities.AddStaticFieldToExistingType, - nameof(EditAndContinueCapabilities.AddInstanceFieldToExistingType) => EditAndContinueCapabilities.AddInstanceFieldToExistingType, - nameof(EditAndContinueCapabilities.NewTypeDefinition) => EditAndContinueCapabilities.NewTypeDefinition, - nameof(EditAndContinueCapabilities.ChangeCustomAttributes) => EditAndContinueCapabilities.ChangeCustomAttributes, - nameof(EditAndContinueCapabilities.UpdateParameters) => EditAndContinueCapabilities.UpdateParameters, - nameof(EditAndContinueCapabilities.GenericAddMethodToExistingType) => EditAndContinueCapabilities.GenericAddMethodToExistingType, - nameof(EditAndContinueCapabilities.GenericUpdateMethod) => EditAndContinueCapabilities.GenericUpdateMethod, - nameof(EditAndContinueCapabilities.GenericAddFieldToExistingType) => EditAndContinueCapabilities.GenericAddFieldToExistingType, - - // To make it eaiser for runtimes to specify more broad capabilities - "AddDefinitionToExistingType" => EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddStaticFieldToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType, - - _ => EditAndContinueCapabilities.None - }; - } - - return caps; + nameof(EditAndContinueCapabilities.Baseline) => EditAndContinueCapabilities.Baseline, + nameof(EditAndContinueCapabilities.AddMethodToExistingType) => EditAndContinueCapabilities.AddMethodToExistingType, + nameof(EditAndContinueCapabilities.AddStaticFieldToExistingType) => EditAndContinueCapabilities.AddStaticFieldToExistingType, + nameof(EditAndContinueCapabilities.AddInstanceFieldToExistingType) => EditAndContinueCapabilities.AddInstanceFieldToExistingType, + nameof(EditAndContinueCapabilities.NewTypeDefinition) => EditAndContinueCapabilities.NewTypeDefinition, + nameof(EditAndContinueCapabilities.ChangeCustomAttributes) => EditAndContinueCapabilities.ChangeCustomAttributes, + nameof(EditAndContinueCapabilities.UpdateParameters) => EditAndContinueCapabilities.UpdateParameters, + nameof(EditAndContinueCapabilities.GenericAddMethodToExistingType) => EditAndContinueCapabilities.GenericAddMethodToExistingType, + nameof(EditAndContinueCapabilities.GenericUpdateMethod) => EditAndContinueCapabilities.GenericUpdateMethod, + nameof(EditAndContinueCapabilities.GenericAddFieldToExistingType) => EditAndContinueCapabilities.GenericAddFieldToExistingType, + + // To make it eaiser for runtimes to specify more broad capabilities + "AddDefinitionToExistingType" => EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddStaticFieldToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType, + + _ => EditAndContinueCapabilities.None + }; } - public static ImmutableArray ToStringArray(this EditAndContinueCapabilities capabilities) - { - using var _ = ArrayBuilder.GetInstance(out var builder); + return caps; + } - if (capabilities.HasFlag(EditAndContinueCapabilities.Baseline)) - builder.Add(nameof(EditAndContinueCapabilities.Baseline)); + public static ImmutableArray ToStringArray(this EditAndContinueCapabilities capabilities) + { + using var _ = ArrayBuilder.GetInstance(out var builder); - if (capabilities.HasFlag(EditAndContinueCapabilities.AddMethodToExistingType)) - builder.Add(nameof(EditAndContinueCapabilities.AddMethodToExistingType)); + if (capabilities.HasFlag(EditAndContinueCapabilities.Baseline)) + builder.Add(nameof(EditAndContinueCapabilities.Baseline)); - if (capabilities.HasFlag(EditAndContinueCapabilities.AddStaticFieldToExistingType)) - builder.Add(nameof(EditAndContinueCapabilities.AddStaticFieldToExistingType)); + if (capabilities.HasFlag(EditAndContinueCapabilities.AddMethodToExistingType)) + builder.Add(nameof(EditAndContinueCapabilities.AddMethodToExistingType)); - if (capabilities.HasFlag(EditAndContinueCapabilities.AddInstanceFieldToExistingType)) - builder.Add(nameof(EditAndContinueCapabilities.AddInstanceFieldToExistingType)); + if (capabilities.HasFlag(EditAndContinueCapabilities.AddStaticFieldToExistingType)) + builder.Add(nameof(EditAndContinueCapabilities.AddStaticFieldToExistingType)); - if (capabilities.HasFlag(EditAndContinueCapabilities.NewTypeDefinition)) - builder.Add(nameof(EditAndContinueCapabilities.NewTypeDefinition)); + if (capabilities.HasFlag(EditAndContinueCapabilities.AddInstanceFieldToExistingType)) + builder.Add(nameof(EditAndContinueCapabilities.AddInstanceFieldToExistingType)); - if (capabilities.HasFlag(EditAndContinueCapabilities.ChangeCustomAttributes)) - builder.Add(nameof(EditAndContinueCapabilities.ChangeCustomAttributes)); + if (capabilities.HasFlag(EditAndContinueCapabilities.NewTypeDefinition)) + builder.Add(nameof(EditAndContinueCapabilities.NewTypeDefinition)); - if (capabilities.HasFlag(EditAndContinueCapabilities.UpdateParameters)) - builder.Add(nameof(EditAndContinueCapabilities.UpdateParameters)); + if (capabilities.HasFlag(EditAndContinueCapabilities.ChangeCustomAttributes)) + builder.Add(nameof(EditAndContinueCapabilities.ChangeCustomAttributes)); - return builder.ToImmutable(); - } + if (capabilities.HasFlag(EditAndContinueCapabilities.UpdateParameters)) + builder.Add(nameof(EditAndContinueCapabilities.UpdateParameters)); + + return builder.ToImmutable(); } } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueCapabilitiesGrantor.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueCapabilitiesGrantor.cs index 63cd11d803504..e5eda77eb92a3 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueCapabilitiesGrantor.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueCapabilitiesGrantor.cs @@ -2,21 +2,20 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Grants capabilities. +/// +internal sealed class EditAndContinueCapabilitiesGrantor(EditAndContinueCapabilities availableCapabilities) { - /// - /// Grants capabilities. - /// - internal sealed class EditAndContinueCapabilitiesGrantor(EditAndContinueCapabilities availableCapabilities) - { - private readonly EditAndContinueCapabilities _availableCapabilities = availableCapabilities; + private readonly EditAndContinueCapabilities _availableCapabilities = availableCapabilities; - public EditAndContinueCapabilities GrantedCapabilities { get; private set; } = 0; + public EditAndContinueCapabilities GrantedCapabilities { get; private set; } = 0; - public bool Grant(EditAndContinueCapabilities capabilities) - { - GrantedCapabilities |= capabilities; - return (_availableCapabilities & capabilities) == capabilities; - } + public bool Grant(EditAndContinueCapabilities capabilities) + { + GrantedCapabilities |= capabilities; + return (_availableCapabilities & capabilities) == capabilities; } } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs index 6b047e1a09fe9..f63f39b3cee2b 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs @@ -9,202 +9,201 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal static class EditAndContinueDiagnosticDescriptors { - internal static class EditAndContinueDiagnosticDescriptors - { - private const int GeneralDiagnosticBaseId = 1000; - private const int ModuleDiagnosticBaseId = 2000; - private static readonly int s_diagnosticBaseIndex; + private const int GeneralDiagnosticBaseId = 1000; + private const int ModuleDiagnosticBaseId = 2000; + private static readonly int s_diagnosticBaseIndex; - private static readonly LocalizableResourceString s_rudeEditLocString; - private static readonly LocalizableResourceString s_encLocString; - private static readonly LocalizableResourceString s_encDisallowedByProjectLocString; + private static readonly LocalizableResourceString s_rudeEditLocString; + private static readonly LocalizableResourceString s_encLocString; + private static readonly LocalizableResourceString s_encDisallowedByProjectLocString; - private static readonly ImmutableArray s_descriptors; + private static readonly ImmutableArray s_descriptors; - // descriptors for diagnostics reported by the debugger: - private static Dictionary s_lazyModuleDiagnosticDescriptors; - private static readonly object s_moduleDiagnosticDescriptorsGuard; + // descriptors for diagnostics reported by the debugger: + private static Dictionary s_lazyModuleDiagnosticDescriptors; + private static readonly object s_moduleDiagnosticDescriptorsGuard; - static EditAndContinueDiagnosticDescriptors() - { - s_moduleDiagnosticDescriptorsGuard = new object(); + static EditAndContinueDiagnosticDescriptors() + { + s_moduleDiagnosticDescriptorsGuard = new object(); - s_rudeEditLocString = new LocalizableResourceString(nameof(FeaturesResources.RudeEdit), FeaturesResources.ResourceManager, typeof(FeaturesResources)); - s_encLocString = new LocalizableResourceString(nameof(FeaturesResources.EditAndContinue), FeaturesResources.ResourceManager, typeof(FeaturesResources)); - s_encDisallowedByProjectLocString = new LocalizableResourceString(nameof(FeaturesResources.EditAndContinueDisallowedByProject), FeaturesResources.ResourceManager, typeof(FeaturesResources)); + s_rudeEditLocString = new LocalizableResourceString(nameof(FeaturesResources.RudeEdit), FeaturesResources.ResourceManager, typeof(FeaturesResources)); + s_encLocString = new LocalizableResourceString(nameof(FeaturesResources.EditAndContinue), FeaturesResources.ResourceManager, typeof(FeaturesResources)); + s_encDisallowedByProjectLocString = new LocalizableResourceString(nameof(FeaturesResources.EditAndContinueDisallowedByProject), FeaturesResources.ResourceManager, typeof(FeaturesResources)); - var builder = ImmutableArray.CreateBuilder(); + var builder = ImmutableArray.CreateBuilder(); - void add(int index, int id, string resourceName, LocalizableResourceString title, DiagnosticSeverity severity) + void add(int index, int id, string resourceName, LocalizableResourceString title, DiagnosticSeverity severity) + { + if (index >= builder.Count) { - if (index >= builder.Count) - { - builder.Count = index + 1; - } - - builder[index] = new DiagnosticDescriptor( - $"ENC{id:D4}", - title, - messageFormat: new LocalizableResourceString(resourceName, FeaturesResources.ResourceManager, typeof(FeaturesResources)), - DiagnosticCategory.EditAndContinue, - severity, - isEnabledByDefault: true, - customTags: DiagnosticCustomTags.EditAndContinue); + builder.Count = index + 1; } - void AddRudeEdit(RudeEditKind kind, string resourceName) - => add(GetDescriptorIndex(kind), (int)kind, resourceName, s_rudeEditLocString, DiagnosticSeverity.Error); - - void AddGeneralDiagnostic(EditAndContinueErrorCode code, string resourceName, DiagnosticSeverity severity = DiagnosticSeverity.Error) - => add(GetDescriptorIndex(code), GeneralDiagnosticBaseId + (int)code, resourceName, s_encLocString, severity); - - // - // rude edits - // - - AddRudeEdit(RudeEditKind.InsertAroundActiveStatement, nameof(FeaturesResources.Adding_0_around_an_active_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.DeleteAroundActiveStatement, nameof(FeaturesResources.Deleting_0_around_an_active_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.DeleteActiveStatement, nameof(FeaturesResources.Removing_0_that_contains_an_active_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.UpdateAroundActiveStatement, nameof(FeaturesResources.Updating_a_0_around_an_active_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.UpdateExceptionHandlerOfActiveTry, nameof(FeaturesResources.Modifying_a_catch_finally_handler_with_an_active_statement_in_the_try_block_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.UpdateTryOrCatchWithActiveFinally, nameof(FeaturesResources.Modifying_a_try_catch_finally_statement_when_the_finally_block_is_active_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.UpdateCatchHandlerAroundActiveStatement, nameof(FeaturesResources.Modifying_a_catch_handler_around_an_active_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.Update, nameof(FeaturesResources.Updating_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ModifiersUpdate, nameof(FeaturesResources.Updating_the_modifiers_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.VarianceUpdate, nameof(FeaturesResources.Updating_the_variance_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.TypeUpdate, nameof(FeaturesResources.Updating_the_type_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InitializerUpdate, nameof(FeaturesResources.Updating_the_initializer_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.FixedSizeFieldUpdate, nameof(FeaturesResources.Updating_the_size_of_a_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.EnumUnderlyingTypeUpdate, nameof(FeaturesResources.Updating_the_underlying_type_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.BaseTypeOrInterfaceUpdate, nameof(FeaturesResources.Updating_the_base_class_and_or_base_interface_s_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.TypeKindUpdate, nameof(FeaturesResources.Updating_the_kind_of_a_type_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.AccessorKindUpdate, nameof(FeaturesResources.Updating_the_kind_of_a_property_event_accessor_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.DeclareAliasUpdate, nameof(FeaturesResources.Updating_the_alias_of_Declare_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.DeclareLibraryUpdate, nameof(FeaturesResources.Updating_the_library_name_of_Declare_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.FieldKindUpdate, nameof(FeaturesResources.Changing_a_field_to_an_event_or_vice_versa_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.Renamed, nameof(FeaturesResources.Renaming_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.Insert, nameof(FeaturesResources.Adding_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertVirtual, nameof(FeaturesResources.Adding_an_abstract_0_or_overriding_an_inherited_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertOverridable, nameof(FeaturesResources.Adding_a_MustOverride_0_or_overriding_an_inherited_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertExtern, nameof(FeaturesResources.Adding_an_extern_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertDllImport, nameof(FeaturesResources.Adding_an_imported_method_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertOperator, nameof(FeaturesResources.Adding_a_user_defined_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertIntoStruct, nameof(FeaturesResources.Adding_0_into_a_1_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertIntoClassWithLayout, nameof(FeaturesResources.Adding_0_into_a_class_with_explicit_or_sequential_layout_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.Move, nameof(FeaturesResources.Moving_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.Delete, nameof(FeaturesResources.Deleting_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.GenericMethodUpdate, nameof(FeaturesResources.Modifying_a_generic_method_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.GenericTypeUpdate, nameof(FeaturesResources.Modifying_a_method_inside_the_context_of_a_generic_type_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertConstructorToTypeWithInitializersWithLambdas, nameof(FeaturesResources.Adding_a_constructor_to_a_type_with_a_field_or_property_initializer_that_contains_an_anonymous_function_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.RenamingCapturedVariable, nameof(FeaturesResources.Renaming_a_captured_variable_from_0_to_1_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.StackAllocUpdate, nameof(FeaturesResources.Modifying_0_which_contains_the_stackalloc_operator_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ExperimentalFeaturesEnabled, nameof(FeaturesResources.Modifying_source_with_experimental_language_features_enabled_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.AwaitStatementUpdate, nameof(FeaturesResources.Updating_a_complex_statement_containing_an_await_expression_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingAccessibility, nameof(FeaturesResources.Changing_visibility_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingCapturedVariableType, nameof(FeaturesResources.Changing_the_type_of_a_captured_variable_0_previously_of_type_1_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingCapturedVariableScope, nameof(FeaturesResources.Changing_the_declaration_scope_of_a_captured_variable_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingLambdaParameters, nameof(FeaturesResources.Changing_the_parameters_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingLambdaReturnType, nameof(FeaturesResources.Changing_the_return_type_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingQueryLambdaType, nameof(FeaturesResources.Changing_the_signature_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.ActiveStatementUpdate, nameof(FeaturesResources.Updating_an_active_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ActiveStatementLambdaRemoved, nameof(FeaturesResources.Removing_0_that_contains_an_active_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.PartiallyExecutedActiveStatementUpdate, nameof(FeaturesResources.Updating_an_active_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.PartiallyExecutedActiveStatementDelete, nameof(FeaturesResources.Removing_0_that_contains_an_active_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertFile, nameof(FeaturesResources.Adding_a_new_file_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.UpdatingStateMachineMethodAroundActiveStatement, nameof(FeaturesResources.Updating_async_or_iterator_modifier_around_an_active_statement_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.UpdatingStateMachineMethodMissingAttribute, nameof(FeaturesResources.Attribute_0_is_missing_Updating_an_async_method_or_an_iterator_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.SwitchBetweenLambdaAndLocalFunction, nameof(FeaturesResources.Switching_between_lambda_and_local_function_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertMethodWithExplicitInterfaceSpecifier, nameof(FeaturesResources.Adding_a_method_with_an_explicit_interface_specifier_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertIntoInterface, nameof(FeaturesResources.Adding_0_into_an_interface_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertLocalFunctionIntoInterfaceMethod, nameof(FeaturesResources.Adding_0_into_an_interface_method_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InternalError, nameof(FeaturesResources.Modifying_source_file_0_requires_restarting_the_application_due_to_internal_error_1)); - AddRudeEdit(RudeEditKind.ChangingFromAsynchronousToSynchronous, nameof(FeaturesResources.Changing_0_from_asynchronous_to_synchronous_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingStateMachineShape, nameof(FeaturesResources.Changing_0_to_1_requires_restarting_the_application_because_it_changes_the_shape_of_the_state_machine)); - AddRudeEdit(RudeEditKind.ComplexQueryExpression, nameof(FeaturesResources.Modifying_0_which_contains_an_Aggregate_Group_By_or_Join_query_clauses_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.MemberBodyInternalError, nameof(FeaturesResources.Modifying_body_of_0_requires_restarting_the_application_due_to_internal_error_1)); - AddRudeEdit(RudeEditKind.MemberBodyTooBig, nameof(FeaturesResources.Modifying_body_of_0_requires_restarting_the_application_because_the_body_has_too_many_statements)); - AddRudeEdit(RudeEditKind.SourceFileTooBig, nameof(FeaturesResources.Modifying_source_file_0_requires_restarting_the_application_because_the_file_is_too_big)); - AddRudeEdit(RudeEditKind.NotSupportedByRuntime, nameof(FeaturesResources.Applying_source_changes_while_the_application_is_running_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.MakeMethodAsyncNotSupportedByRuntime, nameof(FeaturesResources.Making_a_method_asynchronous_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.MakeMethodIteratorNotSupportedByRuntime, nameof(FeaturesResources.Making_a_method_an_iterator_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.InsertNotSupportedByRuntime, nameof(FeaturesResources.Adding_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingAttributesNotSupportedByRuntime, nameof(FeaturesResources.Updating_the_attributes_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime, nameof(FeaturesResources.Updating_reloadable_type_marked_by_0_attribute_or_its_member_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.ChangingParameterTypes, nameof(FeaturesResources.Changing_parameter_types_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingTypeParameters, nameof(FeaturesResources.Changing_type_parameters_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingConstraints, nameof(FeaturesResources.Changing_constraints_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangeImplicitMainReturnType, nameof(FeaturesResources.An_update_that_causes_the_return_type_of_implicit_main_to_change_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.RenamingNotSupportedByRuntime, nameof(FeaturesResources.Renaming_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.ChangingNonCustomAttribute, nameof(FeaturesResources.Changing_pseudo_custom_attribute_0_of_1_requires_restarting_th_application)); - AddRudeEdit(RudeEditKind.ChangingNamespace, nameof(FeaturesResources.Changing_the_containing_namespace_of_0_from_1_to_2_requires_restarting_th_application)); - AddRudeEdit(RudeEditKind.ChangingSignatureNotSupportedByRuntime, nameof(FeaturesResources.Changing_the_signature_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.DeleteNotSupportedByRuntime, nameof(FeaturesResources.Deleting_0_requires_restarting_the_application_because_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.UpdatingStateMachineMethodNotSupportedByRuntime, nameof(FeaturesResources.Updating_async_or_iterator_requires_restarting_the_application_because_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.UpdatingGenericNotSupportedByRuntime, nameof(FeaturesResources.Updating_0_within_generic_type_or_method_requires_restarting_the_application_because_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.CapturingPrimaryConstructorParameter, nameof(FeaturesResources.Capturing_primary_constructor_parameter_0_that_hasn_t_been_captured_before_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.NotCapturingPrimaryConstructorParameter, nameof(FeaturesResources.Ceasing_to_capture_primary_constructor_parameter_0_of_1_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingAttribute, nameof(FeaturesResources.Changing_attribute_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ChangingNameOrSignatureOfActiveMember, nameof(FeaturesResources.Changing_name_or_signature_of_0_that_contains_an_active_statement_requires_restarting_the_application)); - - // VB specific - AddRudeEdit(RudeEditKind.HandlesClauseUpdate, nameof(FeaturesResources.Updating_the_Handles_clause_of_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.ImplementsClauseUpdate, nameof(FeaturesResources.Updating_the_Implements_clause_of_a_0_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.InsertHandlesClause, nameof(FeaturesResources.Adding_0_with_the_Handles_clause_requires_restarting_the_application)); - AddRudeEdit(RudeEditKind.UpdateStaticLocal, nameof(FeaturesResources.Modifying_0_which_contains_a_static_variable_requires_restarting_the_application)); - - // - // other Roslyn reported diagnostics: - // - - s_diagnosticBaseIndex = builder.Count; - - AddGeneralDiagnostic(EditAndContinueErrorCode.ErrorReadingFile, nameof(FeaturesResources.ErrorReadingFile)); - AddGeneralDiagnostic(EditAndContinueErrorCode.CannotApplyChangesUnexpectedError, nameof(FeaturesResources.CannotApplyChangesUnexpectedError)); - AddGeneralDiagnostic(EditAndContinueErrorCode.ChangesDisallowedWhileStoppedAtException, nameof(FeaturesResources.ChangesDisallowedWhileStoppedAtException)); - AddGeneralDiagnostic(EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee, nameof(FeaturesResources.DocumentIsOutOfSyncWithDebuggee), DiagnosticSeverity.Warning); - AddGeneralDiagnostic(EditAndContinueErrorCode.UnableToReadSourceFileOrPdb, nameof(FeaturesResources.UnableToReadSourceFileOrPdb), DiagnosticSeverity.Warning); - AddGeneralDiagnostic(EditAndContinueErrorCode.AddingTypeRuntimeCapabilityRequired, nameof(FeaturesResources.ChangesRequiredSynthesizedType)); - - s_descriptors = builder.ToImmutable(); + builder[index] = new DiagnosticDescriptor( + $"ENC{id:D4}", + title, + messageFormat: new LocalizableResourceString(resourceName, FeaturesResources.ResourceManager, typeof(FeaturesResources)), + DiagnosticCategory.EditAndContinue, + severity, + isEnabledByDefault: true, + customTags: DiagnosticCustomTags.EditAndContinue); } - internal static ImmutableArray GetDescriptors() - => s_descriptors.WhereAsArray(d => d != null); + void AddRudeEdit(RudeEditKind kind, string resourceName) + => add(GetDescriptorIndex(kind), (int)kind, resourceName, s_rudeEditLocString, DiagnosticSeverity.Error); + + void AddGeneralDiagnostic(EditAndContinueErrorCode code, string resourceName, DiagnosticSeverity severity = DiagnosticSeverity.Error) + => add(GetDescriptorIndex(code), GeneralDiagnosticBaseId + (int)code, resourceName, s_encLocString, severity); + + // + // rude edits + // + + AddRudeEdit(RudeEditKind.InsertAroundActiveStatement, nameof(FeaturesResources.Adding_0_around_an_active_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.DeleteAroundActiveStatement, nameof(FeaturesResources.Deleting_0_around_an_active_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.DeleteActiveStatement, nameof(FeaturesResources.Removing_0_that_contains_an_active_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.UpdateAroundActiveStatement, nameof(FeaturesResources.Updating_a_0_around_an_active_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.UpdateExceptionHandlerOfActiveTry, nameof(FeaturesResources.Modifying_a_catch_finally_handler_with_an_active_statement_in_the_try_block_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.UpdateTryOrCatchWithActiveFinally, nameof(FeaturesResources.Modifying_a_try_catch_finally_statement_when_the_finally_block_is_active_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.UpdateCatchHandlerAroundActiveStatement, nameof(FeaturesResources.Modifying_a_catch_handler_around_an_active_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.Update, nameof(FeaturesResources.Updating_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ModifiersUpdate, nameof(FeaturesResources.Updating_the_modifiers_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.VarianceUpdate, nameof(FeaturesResources.Updating_the_variance_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.TypeUpdate, nameof(FeaturesResources.Updating_the_type_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InitializerUpdate, nameof(FeaturesResources.Updating_the_initializer_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.FixedSizeFieldUpdate, nameof(FeaturesResources.Updating_the_size_of_a_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.EnumUnderlyingTypeUpdate, nameof(FeaturesResources.Updating_the_underlying_type_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.BaseTypeOrInterfaceUpdate, nameof(FeaturesResources.Updating_the_base_class_and_or_base_interface_s_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.TypeKindUpdate, nameof(FeaturesResources.Updating_the_kind_of_a_type_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.AccessorKindUpdate, nameof(FeaturesResources.Updating_the_kind_of_a_property_event_accessor_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.DeclareAliasUpdate, nameof(FeaturesResources.Updating_the_alias_of_Declare_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.DeclareLibraryUpdate, nameof(FeaturesResources.Updating_the_library_name_of_Declare_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.FieldKindUpdate, nameof(FeaturesResources.Changing_a_field_to_an_event_or_vice_versa_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.Renamed, nameof(FeaturesResources.Renaming_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.Insert, nameof(FeaturesResources.Adding_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertVirtual, nameof(FeaturesResources.Adding_an_abstract_0_or_overriding_an_inherited_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertOverridable, nameof(FeaturesResources.Adding_a_MustOverride_0_or_overriding_an_inherited_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertExtern, nameof(FeaturesResources.Adding_an_extern_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertDllImport, nameof(FeaturesResources.Adding_an_imported_method_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertOperator, nameof(FeaturesResources.Adding_a_user_defined_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertIntoStruct, nameof(FeaturesResources.Adding_0_into_a_1_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertIntoClassWithLayout, nameof(FeaturesResources.Adding_0_into_a_class_with_explicit_or_sequential_layout_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.Move, nameof(FeaturesResources.Moving_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.Delete, nameof(FeaturesResources.Deleting_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.GenericMethodUpdate, nameof(FeaturesResources.Modifying_a_generic_method_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.GenericTypeUpdate, nameof(FeaturesResources.Modifying_a_method_inside_the_context_of_a_generic_type_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertConstructorToTypeWithInitializersWithLambdas, nameof(FeaturesResources.Adding_a_constructor_to_a_type_with_a_field_or_property_initializer_that_contains_an_anonymous_function_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.RenamingCapturedVariable, nameof(FeaturesResources.Renaming_a_captured_variable_from_0_to_1_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.StackAllocUpdate, nameof(FeaturesResources.Modifying_0_which_contains_the_stackalloc_operator_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ExperimentalFeaturesEnabled, nameof(FeaturesResources.Modifying_source_with_experimental_language_features_enabled_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.AwaitStatementUpdate, nameof(FeaturesResources.Updating_a_complex_statement_containing_an_await_expression_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingAccessibility, nameof(FeaturesResources.Changing_visibility_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingCapturedVariableType, nameof(FeaturesResources.Changing_the_type_of_a_captured_variable_0_previously_of_type_1_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingCapturedVariableScope, nameof(FeaturesResources.Changing_the_declaration_scope_of_a_captured_variable_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingLambdaParameters, nameof(FeaturesResources.Changing_the_parameters_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingLambdaReturnType, nameof(FeaturesResources.Changing_the_return_type_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingQueryLambdaType, nameof(FeaturesResources.Changing_the_signature_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); + AddRudeEdit(RudeEditKind.ActiveStatementUpdate, nameof(FeaturesResources.Updating_an_active_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ActiveStatementLambdaRemoved, nameof(FeaturesResources.Removing_0_that_contains_an_active_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.PartiallyExecutedActiveStatementUpdate, nameof(FeaturesResources.Updating_an_active_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.PartiallyExecutedActiveStatementDelete, nameof(FeaturesResources.Removing_0_that_contains_an_active_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertFile, nameof(FeaturesResources.Adding_a_new_file_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.UpdatingStateMachineMethodAroundActiveStatement, nameof(FeaturesResources.Updating_async_or_iterator_modifier_around_an_active_statement_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.UpdatingStateMachineMethodMissingAttribute, nameof(FeaturesResources.Attribute_0_is_missing_Updating_an_async_method_or_an_iterator_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.SwitchBetweenLambdaAndLocalFunction, nameof(FeaturesResources.Switching_between_lambda_and_local_function_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertMethodWithExplicitInterfaceSpecifier, nameof(FeaturesResources.Adding_a_method_with_an_explicit_interface_specifier_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertIntoInterface, nameof(FeaturesResources.Adding_0_into_an_interface_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertLocalFunctionIntoInterfaceMethod, nameof(FeaturesResources.Adding_0_into_an_interface_method_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InternalError, nameof(FeaturesResources.Modifying_source_file_0_requires_restarting_the_application_due_to_internal_error_1)); + AddRudeEdit(RudeEditKind.ChangingFromAsynchronousToSynchronous, nameof(FeaturesResources.Changing_0_from_asynchronous_to_synchronous_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingStateMachineShape, nameof(FeaturesResources.Changing_0_to_1_requires_restarting_the_application_because_it_changes_the_shape_of_the_state_machine)); + AddRudeEdit(RudeEditKind.ComplexQueryExpression, nameof(FeaturesResources.Modifying_0_which_contains_an_Aggregate_Group_By_or_Join_query_clauses_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.MemberBodyInternalError, nameof(FeaturesResources.Modifying_body_of_0_requires_restarting_the_application_due_to_internal_error_1)); + AddRudeEdit(RudeEditKind.MemberBodyTooBig, nameof(FeaturesResources.Modifying_body_of_0_requires_restarting_the_application_because_the_body_has_too_many_statements)); + AddRudeEdit(RudeEditKind.SourceFileTooBig, nameof(FeaturesResources.Modifying_source_file_0_requires_restarting_the_application_because_the_file_is_too_big)); + AddRudeEdit(RudeEditKind.NotSupportedByRuntime, nameof(FeaturesResources.Applying_source_changes_while_the_application_is_running_is_not_supported_by_the_runtime)); + AddRudeEdit(RudeEditKind.MakeMethodAsyncNotSupportedByRuntime, nameof(FeaturesResources.Making_a_method_asynchronous_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); + AddRudeEdit(RudeEditKind.MakeMethodIteratorNotSupportedByRuntime, nameof(FeaturesResources.Making_a_method_an_iterator_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); + AddRudeEdit(RudeEditKind.InsertNotSupportedByRuntime, nameof(FeaturesResources.Adding_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingAttributesNotSupportedByRuntime, nameof(FeaturesResources.Updating_the_attributes_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); + AddRudeEdit(RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime, nameof(FeaturesResources.Updating_reloadable_type_marked_by_0_attribute_or_its_member_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); + AddRudeEdit(RudeEditKind.ChangingParameterTypes, nameof(FeaturesResources.Changing_parameter_types_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingTypeParameters, nameof(FeaturesResources.Changing_type_parameters_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingConstraints, nameof(FeaturesResources.Changing_constraints_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangeImplicitMainReturnType, nameof(FeaturesResources.An_update_that_causes_the_return_type_of_implicit_main_to_change_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.RenamingNotSupportedByRuntime, nameof(FeaturesResources.Renaming_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); + AddRudeEdit(RudeEditKind.ChangingNonCustomAttribute, nameof(FeaturesResources.Changing_pseudo_custom_attribute_0_of_1_requires_restarting_th_application)); + AddRudeEdit(RudeEditKind.ChangingNamespace, nameof(FeaturesResources.Changing_the_containing_namespace_of_0_from_1_to_2_requires_restarting_th_application)); + AddRudeEdit(RudeEditKind.ChangingSignatureNotSupportedByRuntime, nameof(FeaturesResources.Changing_the_signature_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); + AddRudeEdit(RudeEditKind.DeleteNotSupportedByRuntime, nameof(FeaturesResources.Deleting_0_requires_restarting_the_application_because_is_not_supported_by_the_runtime)); + AddRudeEdit(RudeEditKind.UpdatingStateMachineMethodNotSupportedByRuntime, nameof(FeaturesResources.Updating_async_or_iterator_requires_restarting_the_application_because_is_not_supported_by_the_runtime)); + AddRudeEdit(RudeEditKind.UpdatingGenericNotSupportedByRuntime, nameof(FeaturesResources.Updating_0_within_generic_type_or_method_requires_restarting_the_application_because_is_not_supported_by_the_runtime)); + AddRudeEdit(RudeEditKind.CapturingPrimaryConstructorParameter, nameof(FeaturesResources.Capturing_primary_constructor_parameter_0_that_hasn_t_been_captured_before_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.NotCapturingPrimaryConstructorParameter, nameof(FeaturesResources.Ceasing_to_capture_primary_constructor_parameter_0_of_1_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingAttribute, nameof(FeaturesResources.Changing_attribute_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingNameOrSignatureOfActiveMember, nameof(FeaturesResources.Changing_name_or_signature_of_0_that_contains_an_active_statement_requires_restarting_the_application)); + + // VB specific + AddRudeEdit(RudeEditKind.HandlesClauseUpdate, nameof(FeaturesResources.Updating_the_Handles_clause_of_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ImplementsClauseUpdate, nameof(FeaturesResources.Updating_the_Implements_clause_of_a_0_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.InsertHandlesClause, nameof(FeaturesResources.Adding_0_with_the_Handles_clause_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.UpdateStaticLocal, nameof(FeaturesResources.Modifying_0_which_contains_a_static_variable_requires_restarting_the_application)); + + // + // other Roslyn reported diagnostics: + // + + s_diagnosticBaseIndex = builder.Count; + + AddGeneralDiagnostic(EditAndContinueErrorCode.ErrorReadingFile, nameof(FeaturesResources.ErrorReadingFile)); + AddGeneralDiagnostic(EditAndContinueErrorCode.CannotApplyChangesUnexpectedError, nameof(FeaturesResources.CannotApplyChangesUnexpectedError)); + AddGeneralDiagnostic(EditAndContinueErrorCode.ChangesDisallowedWhileStoppedAtException, nameof(FeaturesResources.ChangesDisallowedWhileStoppedAtException)); + AddGeneralDiagnostic(EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee, nameof(FeaturesResources.DocumentIsOutOfSyncWithDebuggee), DiagnosticSeverity.Warning); + AddGeneralDiagnostic(EditAndContinueErrorCode.UnableToReadSourceFileOrPdb, nameof(FeaturesResources.UnableToReadSourceFileOrPdb), DiagnosticSeverity.Warning); + AddGeneralDiagnostic(EditAndContinueErrorCode.AddingTypeRuntimeCapabilityRequired, nameof(FeaturesResources.ChangesRequiredSynthesizedType)); + + s_descriptors = builder.ToImmutable(); + } + + internal static ImmutableArray GetDescriptors() + => s_descriptors.WhereAsArray(d => d != null); - internal static DiagnosticDescriptor GetDescriptor(RudeEditKind kind) - => s_descriptors[GetDescriptorIndex(kind)]; + internal static DiagnosticDescriptor GetDescriptor(RudeEditKind kind) + => s_descriptors[GetDescriptorIndex(kind)]; - internal static DiagnosticDescriptor GetDescriptor(EditAndContinueErrorCode errorCode) - => s_descriptors[GetDescriptorIndex(errorCode)]; + internal static DiagnosticDescriptor GetDescriptor(EditAndContinueErrorCode errorCode) + => s_descriptors[GetDescriptorIndex(errorCode)]; - internal static DiagnosticDescriptor GetModuleDiagnosticDescriptor(ManagedHotReloadAvailabilityStatus status) + internal static DiagnosticDescriptor GetModuleDiagnosticDescriptor(ManagedHotReloadAvailabilityStatus status) + { + lock (s_moduleDiagnosticDescriptorsGuard) { - lock (s_moduleDiagnosticDescriptorsGuard) + s_lazyModuleDiagnosticDescriptors ??= []; + + if (!s_lazyModuleDiagnosticDescriptors.TryGetValue(status, out var descriptor)) { - s_lazyModuleDiagnosticDescriptors ??= []; - - if (!s_lazyModuleDiagnosticDescriptors.TryGetValue(status, out var descriptor)) - { - s_lazyModuleDiagnosticDescriptors.Add(status, descriptor = new DiagnosticDescriptor( - $"ENC{ModuleDiagnosticBaseId + (int)status:D4}", - s_encLocString, - s_encDisallowedByProjectLocString, - DiagnosticCategory.EditAndContinue, - DiagnosticSeverity.Error, - isEnabledByDefault: true, - customTags: DiagnosticCustomTags.EditAndContinue)); - } - - return descriptor; + s_lazyModuleDiagnosticDescriptors.Add(status, descriptor = new DiagnosticDescriptor( + $"ENC{ModuleDiagnosticBaseId + (int)status:D4}", + s_encLocString, + s_encDisallowedByProjectLocString, + DiagnosticCategory.EditAndContinue, + DiagnosticSeverity.Error, + isEnabledByDefault: true, + customTags: DiagnosticCustomTags.EditAndContinue)); } + + return descriptor; } + } - private static int GetDescriptorIndex(RudeEditKind kind) - => (int)kind; + private static int GetDescriptorIndex(RudeEditKind kind) + => (int)kind; - private static int GetDescriptorIndex(EditAndContinueErrorCode errorCode) - => s_diagnosticBaseIndex + (int)errorCode; - } + private static int GetDescriptorIndex(EditAndContinueErrorCode errorCode) + => s_diagnosticBaseIndex + (int)errorCode; } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticUpdateSource.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticUpdateSource.cs index b94e7f3207ec3..ca77fa41c5173 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticUpdateSource.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticUpdateSource.cs @@ -14,144 +14,143 @@ using Microsoft.CodeAnalysis.Shared.Collections; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[Export(typeof(EditAndContinueDiagnosticUpdateSource))] +[Shared] +internal sealed class EditAndContinueDiagnosticUpdateSource : IDiagnosticUpdateSource { - [Export(typeof(EditAndContinueDiagnosticUpdateSource))] - [Shared] - internal sealed class EditAndContinueDiagnosticUpdateSource : IDiagnosticUpdateSource + private int _diagnosticsVersion; + private bool _previouslyHadDiagnostics; + + /// + /// Represents an increasing integer version of diagnostics from Edit and Continue, which increments + /// when diagnostics might have changed even if there is no associated document changes (eg a restart + /// of an app during Hot Reload) + /// + public int Version => _diagnosticsVersion; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public EditAndContinueDiagnosticUpdateSource(IDiagnosticUpdateSourceRegistrationService registrationService) + => registrationService.Register(this); + + // for testing + [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] + internal EditAndContinueDiagnosticUpdateSource() { - private int _diagnosticsVersion; - private bool _previouslyHadDiagnostics; - - /// - /// Represents an increasing integer version of diagnostics from Edit and Continue, which increments - /// when diagnostics might have changed even if there is no associated document changes (eg a restart - /// of an app during Hot Reload) - /// - public int Version => _diagnosticsVersion; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EditAndContinueDiagnosticUpdateSource(IDiagnosticUpdateSourceRegistrationService registrationService) - => registrationService.Register(this); - - // for testing - [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] - internal EditAndContinueDiagnosticUpdateSource() - { - } + } - public event EventHandler>? DiagnosticsUpdated; - public event EventHandler? DiagnosticsCleared; + public event EventHandler>? DiagnosticsUpdated; + public event EventHandler? DiagnosticsCleared; - /// - /// This implementation reports diagnostics via event. - /// - public bool SupportGetDiagnostics => false; + /// + /// This implementation reports diagnostics via event. + /// + public bool SupportGetDiagnostics => false; - public ValueTask> GetDiagnosticsAsync(Workspace workspace, ProjectId? projectId, DocumentId? documentId, object? id, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default) - => new([]); + public ValueTask> GetDiagnosticsAsync(Workspace workspace, ProjectId? projectId, DocumentId? documentId, object? id, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default) + => new([]); - /// - /// Clears all diagnostics reported thru this source. - /// We do not track the particular reported diagnostics here since we can just clear all of them at once. - /// - public void ClearDiagnostics(bool isSessionEnding = false) + /// + /// Clears all diagnostics reported thru this source. + /// We do not track the particular reported diagnostics here since we can just clear all of them at once. + /// + public void ClearDiagnostics(bool isSessionEnding = false) + { + // If ClearDiagnostics is called and there weren't any diagnostics previously, then there is no point incrementing + // our version number and potentially invalidating caches unnecessarily. + // If the debug session is ending, however, we want to always increment otherwise we can get stuck. eg if the user + // makes a rude edit during a debug session, but doesn't apply the changes, the rude edit will be raised without + // this class knowing about it, and then if the debug session is stopped, we have no knowledge of any diagnostics here + // so don't bump our version number, but the document checksum also doesn't change, so we get stuck with the rude edit. + if (isSessionEnding || _previouslyHadDiagnostics) { - // If ClearDiagnostics is called and there weren't any diagnostics previously, then there is no point incrementing - // our version number and potentially invalidating caches unnecessarily. - // If the debug session is ending, however, we want to always increment otherwise we can get stuck. eg if the user - // makes a rude edit during a debug session, but doesn't apply the changes, the rude edit will be raised without - // this class knowing about it, and then if the debug session is stopped, we have no knowledge of any diagnostics here - // so don't bump our version number, but the document checksum also doesn't change, so we get stuck with the rude edit. - if (isSessionEnding || _previouslyHadDiagnostics) - { - _previouslyHadDiagnostics = false; - _diagnosticsVersion++; - } - - DiagnosticsCleared?.Invoke(this, EventArgs.Empty); + _previouslyHadDiagnostics = false; + _diagnosticsVersion++; } - /// - /// Reports given set of project or solution level diagnostics. - /// - public void ReportDiagnostics(Workspace workspace, Solution solution, ImmutableArray diagnostics, ImmutableArray<(DocumentId, ImmutableArray Diagnostics)> rudeEdits) + DiagnosticsCleared?.Invoke(this, EventArgs.Empty); + } + + /// + /// Reports given set of project or solution level diagnostics. + /// + public void ReportDiagnostics(Workspace workspace, Solution solution, ImmutableArray diagnostics, ImmutableArray<(DocumentId, ImmutableArray Diagnostics)> rudeEdits) + { + RoslynDebug.Assert(solution != null); + + // Even though we only report diagnostics, and not rude edits, we still need to + // ensure that the presence of rude edits are considered when we decide to update + // our version number. + // The array inside rudeEdits won't ever be empty for a given document so we can just + // check the outer array. + if (diagnostics.Any() || rudeEdits.Any()) { - RoslynDebug.Assert(solution != null); - - // Even though we only report diagnostics, and not rude edits, we still need to - // ensure that the presence of rude edits are considered when we decide to update - // our version number. - // The array inside rudeEdits won't ever be empty for a given document so we can just - // check the outer array. - if (diagnostics.Any() || rudeEdits.Any()) - { - _previouslyHadDiagnostics = true; - _diagnosticsVersion++; - } + _previouslyHadDiagnostics = true; + _diagnosticsVersion++; + } - var updateEvent = DiagnosticsUpdated; - if (updateEvent == null) - { - return; - } + var updateEvent = DiagnosticsUpdated; + if (updateEvent == null) + { + return; + } - var documentDiagnostics = diagnostics.WhereAsArray(d => d.DocumentId != null); - var projectDiagnostics = diagnostics.WhereAsArray(d => d.DocumentId == null && d.ProjectId != null); - var solutionDiagnostics = diagnostics.WhereAsArray(d => d.DocumentId == null && d.ProjectId == null); + var documentDiagnostics = diagnostics.WhereAsArray(d => d.DocumentId != null); + var projectDiagnostics = diagnostics.WhereAsArray(d => d.DocumentId == null && d.ProjectId != null); + var solutionDiagnostics = diagnostics.WhereAsArray(d => d.DocumentId == null && d.ProjectId == null); - using var argsBuilder = TemporaryArray.Empty; + using var argsBuilder = TemporaryArray.Empty; - if (documentDiagnostics.Length > 0) + if (documentDiagnostics.Length > 0) + { + foreach (var (documentId, diagnosticData) in documentDiagnostics.GroupBy(static data => data.DocumentId!)) { - foreach (var (documentId, diagnosticData) in documentDiagnostics.GroupBy(static data => data.DocumentId!)) - { - var diagnosticGroupId = (this, documentId); - - argsBuilder.Add(DiagnosticsUpdatedArgs.DiagnosticsCreated( - diagnosticGroupId, - workspace, - solution, - documentId.ProjectId, - documentId: documentId, - diagnostics: diagnosticData.ToImmutableArray())); - } - } + var diagnosticGroupId = (this, documentId); - if (projectDiagnostics.Length > 0) - { - foreach (var (projectId, diagnosticData) in projectDiagnostics.GroupBy(static data => data.ProjectId!)) - { - var diagnosticGroupId = (this, projectId); - - argsBuilder.Add(DiagnosticsUpdatedArgs.DiagnosticsCreated( - diagnosticGroupId, - workspace, - solution, - projectId, - documentId: null, - diagnostics: diagnosticData.ToImmutableArray())); - } + argsBuilder.Add(DiagnosticsUpdatedArgs.DiagnosticsCreated( + diagnosticGroupId, + workspace, + solution, + documentId.ProjectId, + documentId: documentId, + diagnostics: diagnosticData.ToImmutableArray())); } + } - if (solutionDiagnostics.Length > 0) + if (projectDiagnostics.Length > 0) + { + foreach (var (projectId, diagnosticData) in projectDiagnostics.GroupBy(static data => data.ProjectId!)) { - var diagnosticGroupId = this; + var diagnosticGroupId = (this, projectId); argsBuilder.Add(DiagnosticsUpdatedArgs.DiagnosticsCreated( diagnosticGroupId, workspace, solution, - projectId: null, + projectId, documentId: null, - diagnostics: solutionDiagnostics)); + diagnostics: diagnosticData.ToImmutableArray())); } + } - if (argsBuilder.Count > 0) - { - updateEvent(this, argsBuilder.ToImmutableAndClear()); - } + if (solutionDiagnostics.Length > 0) + { + var diagnosticGroupId = this; + + argsBuilder.Add(DiagnosticsUpdatedArgs.DiagnosticsCreated( + diagnosticGroupId, + workspace, + solution, + projectId: null, + documentId: null, + diagnostics: solutionDiagnostics)); + } + + if (argsBuilder.Count > 0) + { + updateEvent(this, argsBuilder.ToImmutableAndClear()); } } } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs index 014a7a514be6e..547610c7cea89 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs @@ -15,194 +15,193 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Calculates and caches results of changed documents analysis. +/// The work is triggered by an incremental analyzer on idle or explicitly when "continue" operation is executed. +/// Contains analyses of the latest observed document versions. +/// +internal sealed class EditAndContinueDocumentAnalysesCache(AsyncLazy baseActiveStatements, AsyncLazy capabilities) { + private readonly object _guard = new(); + private readonly Dictionary results, Project baseProject, Document document, ImmutableArray activeStatementSpans)> _analyses = []; + private readonly AsyncLazy _baseActiveStatements = baseActiveStatements; + private readonly AsyncLazy _capabilities = capabilities; + + public async ValueTask> GetDocumentAnalysesAsync( + CommittedSolution oldSolution, + IReadOnlyList<(Document? oldDocument, Document newDocument)> documents, + ActiveStatementSpanProvider activeStatementSpanProvider, + CancellationToken cancellationToken) + { + try + { + if (documents.IsEmpty()) + { + return []; + } + + var tasks = documents.Select(document => Task.Run(() => GetDocumentAnalysisAsync(oldSolution, document.oldDocument, document.newDocument, activeStatementSpanProvider, cancellationToken).AsTask(), cancellationToken)); + var allResults = await Task.WhenAll(tasks).ConfigureAwait(false); + + return allResults.ToImmutableArray(); + } + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) + { + throw ExceptionUtilities.Unreachable(); + } + } + /// - /// Calculates and caches results of changed documents analysis. - /// The work is triggered by an incremental analyzer on idle or explicitly when "continue" operation is executed. - /// Contains analyses of the latest observed document versions. + /// Returns a document analysis or kicks off a new one if one is not available for the specified document snapshot. /// - internal sealed class EditAndContinueDocumentAnalysesCache(AsyncLazy baseActiveStatements, AsyncLazy capabilities) + /// Committed solution. + /// Document snapshot to analyze. + /// Provider of active statement spans tracked by the editor for the solution snapshot of the . + public async ValueTask GetDocumentAnalysisAsync( + CommittedSolution oldSolution, + Document? oldDocument, + Document newDocument, + ActiveStatementSpanProvider activeStatementSpanProvider, + CancellationToken cancellationToken) { - private readonly object _guard = new(); - private readonly Dictionary results, Project baseProject, Document document, ImmutableArray activeStatementSpans)> _analyses = []; - private readonly AsyncLazy _baseActiveStatements = baseActiveStatements; - private readonly AsyncLazy _capabilities = capabilities; - - public async ValueTask> GetDocumentAnalysesAsync( - CommittedSolution oldSolution, - IReadOnlyList<(Document? oldDocument, Document newDocument)> documents, - ActiveStatementSpanProvider activeStatementSpanProvider, - CancellationToken cancellationToken) + try { - try - { - if (documents.IsEmpty()) - { - return []; - } + var unmappedActiveStatementSpans = await GetLatestUnmappedActiveStatementSpansAsync(oldDocument, newDocument, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); - var tasks = documents.Select(document => Task.Run(() => GetDocumentAnalysisAsync(oldSolution, document.oldDocument, document.newDocument, activeStatementSpanProvider, cancellationToken).AsTask(), cancellationToken)); - var allResults = await Task.WhenAll(tasks).ConfigureAwait(false); + // The base project may have been updated as documents were brought up-to-date in the committed solution. + // Get the latest available snapshot of the base project from the committed solution and use it for analyses of all documents, + // so that we use a single compilation for the base project (for efficiency). + // Note that some other request might be updating documents in the committed solution that were not changed (not in changedOrAddedDocuments) + // but are not up-to-date. These documents do not have impact on the analysis unless we read semantic information + // from the project compilation. When reading such information we need to be aware of its potential incompleteness + // and consult the compiler output binary (see https://github.com/dotnet/roslyn/issues/51261). + var oldProject = oldDocument?.Project ?? oldSolution.GetRequiredProject(newDocument.Project.Id); - return allResults.ToImmutableArray(); - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) + AsyncLazy lazyResults; + + lock (_guard) { - throw ExceptionUtilities.Unreachable(); + lazyResults = GetDocumentAnalysisNoLock(oldProject, newDocument, unmappedActiveStatementSpans); } + + return await lazyResults.GetValueAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) + { + throw ExceptionUtilities.Unreachable(); } + } - /// - /// Returns a document analysis or kicks off a new one if one is not available for the specified document snapshot. - /// - /// Committed solution. - /// Document snapshot to analyze. - /// Provider of active statement spans tracked by the editor for the solution snapshot of the . - public async ValueTask GetDocumentAnalysisAsync( - CommittedSolution oldSolution, - Document? oldDocument, - Document newDocument, - ActiveStatementSpanProvider activeStatementSpanProvider, - CancellationToken cancellationToken) + /// + /// Calculates unmapped active statement spans in the from spans provided by . + /// + private async Task> GetLatestUnmappedActiveStatementSpansAsync(Document? oldDocument, Document newDocument, ActiveStatementSpanProvider newActiveStatementSpanProvider, CancellationToken cancellationToken) + { + if (oldDocument == null) { - try - { - var unmappedActiveStatementSpans = await GetLatestUnmappedActiveStatementSpansAsync(oldDocument, newDocument, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); + // document has been deleted or is out-of-sync + return []; + } - // The base project may have been updated as documents were brought up-to-date in the committed solution. - // Get the latest available snapshot of the base project from the committed solution and use it for analyses of all documents, - // so that we use a single compilation for the base project (for efficiency). - // Note that some other request might be updating documents in the committed solution that were not changed (not in changedOrAddedDocuments) - // but are not up-to-date. These documents do not have impact on the analysis unless we read semantic information - // from the project compilation. When reading such information we need to be aware of its potential incompleteness - // and consult the compiler output binary (see https://github.com/dotnet/roslyn/issues/51261). - var oldProject = oldDocument?.Project ?? oldSolution.GetRequiredProject(newDocument.Project.Id); + if (newDocument.FilePath == null) + { + // document has been added, or doesn't have a file path - we do not have tracking spans for it + return []; + } - AsyncLazy lazyResults; + var newTree = await newDocument.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var newLineMappings = newTree.GetLineMappings(cancellationToken); - lock (_guard) - { - lazyResults = GetDocumentAnalysisNoLock(oldProject, newDocument, unmappedActiveStatementSpans); - } - - return await lazyResults.GetValueAsync(cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) - { - throw ExceptionUtilities.Unreachable(); - } + // No #line directives -- retrieve the current location of tracking spans directly for this document: + if (!newLineMappings.Any()) + { + var newMappedDocumentSpans = await newActiveStatementSpanProvider(newDocument.Id, newDocument.FilePath, cancellationToken).ConfigureAwait(false); + return newMappedDocumentSpans.SelectAsArray(s => s.LineSpan); } - /// - /// Calculates unmapped active statement spans in the from spans provided by . - /// - private async Task> GetLatestUnmappedActiveStatementSpansAsync(Document? oldDocument, Document newDocument, ActiveStatementSpanProvider newActiveStatementSpanProvider, CancellationToken cancellationToken) + // The document has #line directives. In order to determine all active statement spans in the document + // we need to find all documents that #line directives in this document map to. + // We retrieve the tracking spans for all such documents and then map them back to this document. + + using var _1 = PooledDictionary>.GetInstance(out var mappedSpansByDocumentPath); + using var _2 = ArrayBuilder.GetInstance(out var activeStatementSpansBuilder); + + var baseActiveStatements = await _baseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false); + var analyzer = newDocument.Project.Services.GetRequiredService(); + var oldActiveStatements = await baseActiveStatements.GetOldActiveStatementsAsync(analyzer, oldDocument, cancellationToken).ConfigureAwait(false); + + foreach (var oldActiveStatement in oldActiveStatements) { - if (oldDocument == null) + var mappedFilePath = oldActiveStatement.Statement.FileSpan.Path; + if (!mappedSpansByDocumentPath.TryGetValue(mappedFilePath, out var newMappedDocumentSpans)) { - // document has been deleted or is out-of-sync - return []; + newMappedDocumentSpans = await newActiveStatementSpanProvider((newDocument.FilePath == mappedFilePath) ? newDocument.Id : null, mappedFilePath, cancellationToken).ConfigureAwait(false); + mappedSpansByDocumentPath.Add(mappedFilePath, newMappedDocumentSpans); } - if (newDocument.FilePath == null) + // Spans not tracked in the document (e.g. the document has been closed): + if (newMappedDocumentSpans.IsEmpty) { - // document has been added, or doesn't have a file path - we do not have tracking spans for it - return []; + continue; } - var newTree = await newDocument.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var newLineMappings = newTree.GetLineMappings(cancellationToken); + // all baseline spans are being tracked in their corresponding mapped documents (if a span is deleted it's still tracked as empty): + var newMappedDocumentActiveSpan = newMappedDocumentSpans.GetStatement(oldActiveStatement.Statement.Ordinal); + Debug.Assert(newMappedDocumentActiveSpan.UnmappedDocumentId == null || newMappedDocumentActiveSpan.UnmappedDocumentId == newDocument.Id); - // No #line directives -- retrieve the current location of tracking spans directly for this document: - if (!newLineMappings.Any()) - { - var newMappedDocumentSpans = await newActiveStatementSpanProvider(newDocument.Id, newDocument.FilePath, cancellationToken).ConfigureAwait(false); - return newMappedDocumentSpans.SelectAsArray(s => s.LineSpan); - } + // TODO: optimize + var newLineMappingContainingActiveSpan = newLineMappings.FirstOrDefault(mapping => mapping.MappedSpan.Span.Contains(newMappedDocumentActiveSpan.LineSpan)); - // The document has #line directives. In order to determine all active statement spans in the document - // we need to find all documents that #line directives in this document map to. - // We retrieve the tracking spans for all such documents and then map them back to this document. + var unmappedSpan = newLineMappingContainingActiveSpan.MappedSpan.IsValid ? newLineMappingContainingActiveSpan.Span : default; + activeStatementSpansBuilder.Add(unmappedSpan); + } - using var _1 = PooledDictionary>.GetInstance(out var mappedSpansByDocumentPath); - using var _2 = ArrayBuilder.GetInstance(out var activeStatementSpansBuilder); + return activeStatementSpansBuilder.ToImmutable(); + } - var baseActiveStatements = await _baseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false); - var analyzer = newDocument.Project.Services.GetRequiredService(); - var oldActiveStatements = await baseActiveStatements.GetOldActiveStatementsAsync(analyzer, oldDocument, cancellationToken).ConfigureAwait(false); + private AsyncLazy GetDocumentAnalysisNoLock(Project baseProject, Document document, ImmutableArray activeStatementSpans) + { + // Do not reuse an analysis of the document unless its snasphot is exactly the same as was used to calculate the results. + // Note that comparing document snapshots in effect compares the entire solution snapshots (when another document is changed a new solution snapshot is created + // that creates new document snapshots for all queried documents). + // Also check the base project snapshot since the analysis uses semantic information from the base project as well. + // + // It would be possible to reuse analysis results of documents whose content does not change in between two solution snapshots. + // However, we'd need rather sophisticated caching logic. The semantic analysis gathers information from other documents when + // calculating results for a specific document. In some cases it's easy to record the set of documents the analysis depends on. + // For example, when analyzing a partial class we can record all documents its declaration spans. However, in other cases the analysis + // checks for absence of a top-level type symbol. Adding a symbol to any document thus invalidates such analysis. It'd be possible + // to keep track of which type symbols an analysis is conditional upon, if it was worth the extra complexity. + if (_analyses.TryGetValue(document.Id, out var analysis) && + analysis.baseProject == baseProject && + analysis.document == document && + analysis.activeStatementSpans.SequenceEqual(activeStatementSpans)) + { + return analysis.results; + } - foreach (var oldActiveStatement in oldActiveStatements) + var lazyResults = AsyncLazy.Create( + asynchronousComputeFunction: async cancellationToken => { - var mappedFilePath = oldActiveStatement.Statement.FileSpan.Path; - if (!mappedSpansByDocumentPath.TryGetValue(mappedFilePath, out var newMappedDocumentSpans)) + try { - newMappedDocumentSpans = await newActiveStatementSpanProvider((newDocument.FilePath == mappedFilePath) ? newDocument.Id : null, mappedFilePath, cancellationToken).ConfigureAwait(false); - mappedSpansByDocumentPath.Add(mappedFilePath, newMappedDocumentSpans); + var analyzer = document.Project.Services.GetRequiredService(); + return await analyzer.AnalyzeDocumentAsync(baseProject, _baseActiveStatements, document, activeStatementSpans, _capabilities, cancellationToken).ConfigureAwait(false); } - - // Spans not tracked in the document (e.g. the document has been closed): - if (newMappedDocumentSpans.IsEmpty) + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) { - continue; + throw ExceptionUtilities.Unreachable(); } + }); - // all baseline spans are being tracked in their corresponding mapped documents (if a span is deleted it's still tracked as empty): - var newMappedDocumentActiveSpan = newMappedDocumentSpans.GetStatement(oldActiveStatement.Statement.Ordinal); - Debug.Assert(newMappedDocumentActiveSpan.UnmappedDocumentId == null || newMappedDocumentActiveSpan.UnmappedDocumentId == newDocument.Id); - - // TODO: optimize - var newLineMappingContainingActiveSpan = newLineMappings.FirstOrDefault(mapping => mapping.MappedSpan.Span.Contains(newMappedDocumentActiveSpan.LineSpan)); - - var unmappedSpan = newLineMappingContainingActiveSpan.MappedSpan.IsValid ? newLineMappingContainingActiveSpan.Span : default; - activeStatementSpansBuilder.Add(unmappedSpan); - } - - return activeStatementSpansBuilder.ToImmutable(); - } + // Previous results for this document id are discarded as they are no longer relevant. + // The only relevant analysis is for the latest base and document snapshots. + // Note that the base snapshot may evolve if documents are dicovered that were previously + // out-of-sync with the compiled outputs and are now up-to-date. + _analyses[document.Id] = (lazyResults, baseProject, document, activeStatementSpans); - private AsyncLazy GetDocumentAnalysisNoLock(Project baseProject, Document document, ImmutableArray activeStatementSpans) - { - // Do not reuse an analysis of the document unless its snasphot is exactly the same as was used to calculate the results. - // Note that comparing document snapshots in effect compares the entire solution snapshots (when another document is changed a new solution snapshot is created - // that creates new document snapshots for all queried documents). - // Also check the base project snapshot since the analysis uses semantic information from the base project as well. - // - // It would be possible to reuse analysis results of documents whose content does not change in between two solution snapshots. - // However, we'd need rather sophisticated caching logic. The semantic analysis gathers information from other documents when - // calculating results for a specific document. In some cases it's easy to record the set of documents the analysis depends on. - // For example, when analyzing a partial class we can record all documents its declaration spans. However, in other cases the analysis - // checks for absence of a top-level type symbol. Adding a symbol to any document thus invalidates such analysis. It'd be possible - // to keep track of which type symbols an analysis is conditional upon, if it was worth the extra complexity. - if (_analyses.TryGetValue(document.Id, out var analysis) && - analysis.baseProject == baseProject && - analysis.document == document && - analysis.activeStatementSpans.SequenceEqual(activeStatementSpans)) - { - return analysis.results; - } - - var lazyResults = AsyncLazy.Create( - asynchronousComputeFunction: async cancellationToken => - { - try - { - var analyzer = document.Project.Services.GetRequiredService(); - return await analyzer.AnalyzeDocumentAsync(baseProject, _baseActiveStatements, document, activeStatementSpans, _capabilities, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) - { - throw ExceptionUtilities.Unreachable(); - } - }); - - // Previous results for this document id are discarded as they are no longer relevant. - // The only relevant analysis is for the latest base and document snapshots. - // Note that the base snapshot may evolve if documents are dicovered that were previously - // out-of-sync with the compiled outputs and are now up-to-date. - _analyses[document.Id] = (lazyResults, baseProject, document, activeStatementSpans); - - return lazyResults; - } + return lazyResults; } } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs index 2bf0279cb1ed6..661dce884c5ef 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs @@ -2,16 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal enum EditAndContinueErrorCode { - internal enum EditAndContinueErrorCode - { - ErrorReadingFile = 1, - CannotApplyChangesUnexpectedError = 2, - // ChangesNotAppliedWhileRunning = 3, // obsolete - ChangesDisallowedWhileStoppedAtException = 4, - DocumentIsOutOfSyncWithDebuggee = 5, - UnableToReadSourceFileOrPdb = 6, - AddingTypeRuntimeCapabilityRequired = 7, - } + ErrorReadingFile = 1, + CannotApplyChangesUnexpectedError = 2, + // ChangesNotAppliedWhileRunning = 3, // obsolete + ChangesDisallowedWhileStoppedAtException = 4, + DocumentIsOutOfSyncWithDebuggee = 5, + UnableToReadSourceFileOrPdb = 6, + AddingTypeRuntimeCapabilityRequired = 7, } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueMethodDebugInfoReader.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueMethodDebugInfoReader.cs index 8e589f83706a5..1d69bf53c5dd6 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueMethodDebugInfoReader.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueMethodDebugInfoReader.cs @@ -16,228 +16,227 @@ using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.DiaSymReader; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Reader of debug information needed for EnC. +/// This object does not own the underlying memory (SymReader/MetadataReader). +/// +internal abstract class EditAndContinueMethodDebugInfoReader { + // TODO: Remove, the path should match exactly. Workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1830914. + internal static bool IgnoreCaseWhenComparingDocumentNames; + + public abstract bool IsPortable { get; } + public abstract EditAndContinueMethodDebugInformation GetDebugInfo(MethodDefinitionHandle methodHandle); + public abstract StandaloneSignatureHandle GetLocalSignature(MethodDefinitionHandle methodHandle); + /// - /// Reader of debug information needed for EnC. - /// This object does not own the underlying memory (SymReader/MetadataReader). + /// Reads document checksum. /// - internal abstract class EditAndContinueMethodDebugInfoReader + /// True if a document with given path is listed in the PDB. + /// Error reading debug information from the PDB. + public abstract bool TryGetDocumentChecksum(string documentPath, out ImmutableArray checksum, out Guid algorithmId); + + private sealed class Native : EditAndContinueMethodDebugInfoReader { - // TODO: Remove, the path should match exactly. Workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1830914. - internal static bool IgnoreCaseWhenComparingDocumentNames; + private readonly ISymUnmanagedReader5 _symReader; + private readonly int _version; + + public Native(ISymUnmanagedReader5 symReader, int version) + { + Debug.Assert(symReader != null); + Debug.Assert(version >= 1); - public abstract bool IsPortable { get; } - public abstract EditAndContinueMethodDebugInformation GetDebugInfo(MethodDefinitionHandle methodHandle); - public abstract StandaloneSignatureHandle GetLocalSignature(MethodDefinitionHandle methodHandle); + _symReader = symReader; + _version = version; + } - /// - /// Reads document checksum. - /// - /// True if a document with given path is listed in the PDB. - /// Error reading debug information from the PDB. - public abstract bool TryGetDocumentChecksum(string documentPath, out ImmutableArray checksum, out Guid algorithmId); + public override bool IsPortable => false; - private sealed class Native : EditAndContinueMethodDebugInfoReader + public override StandaloneSignatureHandle GetLocalSignature(MethodDefinitionHandle methodHandle) { - private readonly ISymUnmanagedReader5 _symReader; - private readonly int _version; + var symMethod = (ISymUnmanagedMethod2)_symReader.GetMethodByVersion(MetadataTokens.GetToken(methodHandle), _version); - public Native(ISymUnmanagedReader5 symReader, int version) - { - Debug.Assert(symReader != null); - Debug.Assert(version >= 1); - - _symReader = symReader; - _version = version; - } + // Compiler generated methods (e.g. async kick-off methods) might not have debug information. + return symMethod == null ? default : MetadataTokens.StandaloneSignatureHandle(symMethod.GetLocalSignatureToken()); + } - public override bool IsPortable => false; + public override EditAndContinueMethodDebugInformation GetDebugInfo(MethodDefinitionHandle methodHandle) + { + var methodToken = MetadataTokens.GetToken(methodHandle); - public override StandaloneSignatureHandle GetLocalSignature(MethodDefinitionHandle methodHandle) + byte[] debugInfo; + try { - var symMethod = (ISymUnmanagedMethod2)_symReader.GetMethodByVersion(MetadataTokens.GetToken(methodHandle), _version); - - // Compiler generated methods (e.g. async kick-off methods) might not have debug information. - return symMethod == null ? default : MetadataTokens.StandaloneSignatureHandle(symMethod.GetLocalSignatureToken()); + debugInfo = _symReader.GetCustomDebugInfo(methodToken, _version); } - - public override EditAndContinueMethodDebugInformation GetDebugInfo(MethodDefinitionHandle methodHandle) + catch (ArgumentOutOfRangeException) { - var methodToken = MetadataTokens.GetToken(methodHandle); + // Sometimes the debugger returns the HRESULT for ArgumentOutOfRangeException, rather than E_FAIL, + // for methods without custom debug info (https://github.com/dotnet/roslyn/issues/4138). + debugInfo = null; + } + catch (Exception e) when (FatalError.ReportAndCatch(e)) // likely a bug in the compiler/debugger + { + throw new InvalidDataException(e.Message, e); + } - byte[] debugInfo; - try - { - debugInfo = _symReader.GetCustomDebugInfo(methodToken, _version); - } - catch (ArgumentOutOfRangeException) + try + { + ImmutableArray localSlots, lambdaMap, stateMachineSuspensionPoints; + if (debugInfo != null) { - // Sometimes the debugger returns the HRESULT for ArgumentOutOfRangeException, rather than E_FAIL, - // for methods without custom debug info (https://github.com/dotnet/roslyn/issues/4138). - debugInfo = null; + localSlots = CustomDebugInfoReader.TryGetCustomDebugInfoRecord(debugInfo, CustomDebugInfoKind.EditAndContinueLocalSlotMap); + lambdaMap = CustomDebugInfoReader.TryGetCustomDebugInfoRecord(debugInfo, CustomDebugInfoKind.EditAndContinueLambdaMap); + stateMachineSuspensionPoints = CustomDebugInfoReader.TryGetCustomDebugInfoRecord(debugInfo, CustomDebugInfoKind.EditAndContinueStateMachineStateMap); } - catch (Exception e) when (FatalError.ReportAndCatch(e)) // likely a bug in the compiler/debugger + else { - throw new InvalidDataException(e.Message, e); + localSlots = lambdaMap = stateMachineSuspensionPoints = default; } - try - { - ImmutableArray localSlots, lambdaMap, stateMachineSuspensionPoints; - if (debugInfo != null) - { - localSlots = CustomDebugInfoReader.TryGetCustomDebugInfoRecord(debugInfo, CustomDebugInfoKind.EditAndContinueLocalSlotMap); - lambdaMap = CustomDebugInfoReader.TryGetCustomDebugInfoRecord(debugInfo, CustomDebugInfoKind.EditAndContinueLambdaMap); - stateMachineSuspensionPoints = CustomDebugInfoReader.TryGetCustomDebugInfoRecord(debugInfo, CustomDebugInfoKind.EditAndContinueStateMachineStateMap); - } - else - { - localSlots = lambdaMap = stateMachineSuspensionPoints = default; - } - - return EditAndContinueMethodDebugInformation.Create(localSlots, lambdaMap, stateMachineSuspensionPoints); - } - catch (InvalidOperationException e) when (FatalError.ReportAndCatch(e)) // likely a bug in the compiler/debugger - { - // TODO: CustomDebugInfoReader should throw InvalidDataException - throw new InvalidDataException(e.Message, e); - } + return EditAndContinueMethodDebugInformation.Create(localSlots, lambdaMap, stateMachineSuspensionPoints); + } + catch (InvalidOperationException e) when (FatalError.ReportAndCatch(e)) // likely a bug in the compiler/debugger + { + // TODO: CustomDebugInfoReader should throw InvalidDataException + throw new InvalidDataException(e.Message, e); } - - public override bool TryGetDocumentChecksum(string documentPath, out ImmutableArray checksum, out Guid algorithmId) - => TryGetDocumentChecksum(_symReader, documentPath, out checksum, out algorithmId); } - private sealed class Portable(MetadataReader pdbReader) : EditAndContinueMethodDebugInfoReader - { - private readonly MetadataReader _pdbReader = pdbReader; + public override bool TryGetDocumentChecksum(string documentPath, out ImmutableArray checksum, out Guid algorithmId) + => TryGetDocumentChecksum(_symReader, documentPath, out checksum, out algorithmId); + } - public override bool IsPortable => true; + private sealed class Portable(MetadataReader pdbReader) : EditAndContinueMethodDebugInfoReader + { + private readonly MetadataReader _pdbReader = pdbReader; + + public override bool IsPortable => true; - public override StandaloneSignatureHandle GetLocalSignature(MethodDefinitionHandle methodHandle) - => _pdbReader.GetMethodDebugInformation(methodHandle.ToDebugInformationHandle()).LocalSignature; + public override StandaloneSignatureHandle GetLocalSignature(MethodDefinitionHandle methodHandle) + => _pdbReader.GetMethodDebugInformation(methodHandle.ToDebugInformationHandle()).LocalSignature; - public override EditAndContinueMethodDebugInformation GetDebugInfo(MethodDefinitionHandle methodHandle) - => EditAndContinueMethodDebugInformation.Create( - compressedSlotMap: GetCdiBytes(methodHandle, PortableCustomDebugInfoKinds.EncLocalSlotMap), - compressedLambdaMap: GetCdiBytes(methodHandle, PortableCustomDebugInfoKinds.EncLambdaAndClosureMap), - compressedStateMachineStateMap: GetCdiBytes(methodHandle, PortableCustomDebugInfoKinds.EncStateMachineStateMap)); + public override EditAndContinueMethodDebugInformation GetDebugInfo(MethodDefinitionHandle methodHandle) + => EditAndContinueMethodDebugInformation.Create( + compressedSlotMap: GetCdiBytes(methodHandle, PortableCustomDebugInfoKinds.EncLocalSlotMap), + compressedLambdaMap: GetCdiBytes(methodHandle, PortableCustomDebugInfoKinds.EncLambdaAndClosureMap), + compressedStateMachineStateMap: GetCdiBytes(methodHandle, PortableCustomDebugInfoKinds.EncStateMachineStateMap)); - private ImmutableArray GetCdiBytes(MethodDefinitionHandle methodHandle, Guid kind) - => TryGetCustomDebugInformation(_pdbReader, methodHandle, kind, out var cdi) ? - _pdbReader.GetBlobContent(cdi.Value) : default; + private ImmutableArray GetCdiBytes(MethodDefinitionHandle methodHandle, Guid kind) + => TryGetCustomDebugInformation(_pdbReader, methodHandle, kind, out var cdi) ? + _pdbReader.GetBlobContent(cdi.Value) : default; - /// Invalid data format. - private static bool TryGetCustomDebugInformation(MetadataReader reader, EntityHandle handle, Guid kind, out CustomDebugInformation customDebugInfo) + /// Invalid data format. + private static bool TryGetCustomDebugInformation(MetadataReader reader, EntityHandle handle, Guid kind, out CustomDebugInformation customDebugInfo) + { + var foundAny = false; + customDebugInfo = default; + foreach (var infoHandle in reader.GetCustomDebugInformation(handle)) { - var foundAny = false; - customDebugInfo = default; - foreach (var infoHandle in reader.GetCustomDebugInformation(handle)) + var info = reader.GetCustomDebugInformation(infoHandle); + var id = reader.GetGuid(info.Kind); + if (id == kind) { - var info = reader.GetCustomDebugInformation(infoHandle); - var id = reader.GetGuid(info.Kind); - if (id == kind) + if (foundAny) { - if (foundAny) - { - throw new BadImageFormatException(); - } - - customDebugInfo = info; - foundAny = true; + throw new BadImageFormatException(); } - } - return foundAny; + customDebugInfo = info; + foundAny = true; + } } - public override bool TryGetDocumentChecksum(string documentPath, out ImmutableArray checksum, out Guid algorithmId) + return foundAny; + } + + public override bool TryGetDocumentChecksum(string documentPath, out ImmutableArray checksum, out Guid algorithmId) + { + foreach (var documentHandle in _pdbReader.Documents) { - foreach (var documentHandle in _pdbReader.Documents) - { - var document = _pdbReader.GetDocument(documentHandle); + var document = _pdbReader.GetDocument(documentHandle); - if (_pdbReader.StringComparer.Equals(document.Name, documentPath, IgnoreCaseWhenComparingDocumentNames)) - { - checksum = _pdbReader.GetBlobContent(document.Hash); - algorithmId = _pdbReader.GetGuid(document.HashAlgorithm); - return true; - } + if (_pdbReader.StringComparer.Equals(document.Name, documentPath, IgnoreCaseWhenComparingDocumentNames)) + { + checksum = _pdbReader.GetBlobContent(document.Hash); + algorithmId = _pdbReader.GetGuid(document.HashAlgorithm); + return true; } - - checksum = default; - algorithmId = default; - return false; } + + checksum = default; + algorithmId = default; + return false; } + } - /// - /// Creates backed by a given . - /// - /// SymReader open on a Portable or Windows PDB. - /// The version of the PDB to read. - /// is null. - /// is less than 1. - /// Error reading debug information. - /// - /// The resulting reader does not take ownership of the or the memory it reads. - /// - /// - /// Automatically detects the underlying PDB format and returns the appropriate reader. - /// - public static unsafe EditAndContinueMethodDebugInfoReader Create(ISymUnmanagedReader5 symReader, int version = 1) + /// + /// Creates backed by a given . + /// + /// SymReader open on a Portable or Windows PDB. + /// The version of the PDB to read. + /// is null. + /// is less than 1. + /// Error reading debug information. + /// + /// The resulting reader does not take ownership of the or the memory it reads. + /// + /// + /// Automatically detects the underlying PDB format and returns the appropriate reader. + /// + public static unsafe EditAndContinueMethodDebugInfoReader Create(ISymUnmanagedReader5 symReader, int version = 1) + { + if (symReader == null) { - if (symReader == null) - { - throw new ArgumentNullException(nameof(symReader)); - } + throw new ArgumentNullException(nameof(symReader)); + } - if (version <= 0) - { - throw new ArgumentOutOfRangeException(nameof(version)); - } + if (version <= 0) + { + throw new ArgumentOutOfRangeException(nameof(version)); + } - var hr = symReader.GetPortableDebugMetadataByVersion(version, metadata: out var metadata, size: out var size); - Marshal.ThrowExceptionForHR(hr); + var hr = symReader.GetPortableDebugMetadataByVersion(version, metadata: out var metadata, size: out var size); + Marshal.ThrowExceptionForHR(hr); - if (hr == 0) - { - return new Portable(new MetadataReader(metadata, size)); - } - else - { - return new Native(symReader, version); - } + if (hr == 0) + { + return new Portable(new MetadataReader(metadata, size)); } - - /// - /// Creates back by a given . - /// - /// open on a Portable PDB. - /// is null. - /// - /// The resulting reader does not take ownership of the or the memory it reads. - /// - public static unsafe EditAndContinueMethodDebugInfoReader Create(MetadataReader pdbReader) - => new Portable(pdbReader ?? throw new ArgumentNullException(nameof(pdbReader))); - - internal static bool TryGetDocumentChecksum(ISymUnmanagedReader5 symReader, string documentPath, out ImmutableArray checksum, out Guid algorithmId) + else { - var symDocument = symReader.GetDocument(documentPath); + return new Native(symReader, version); + } + } - // Make sure the full path matches. - // Native SymReader allows partial match on the document file name. - if (symDocument == null || !StringComparer.Ordinal.Equals(symDocument.GetName(), documentPath)) - { - checksum = default; - algorithmId = default; - return false; - } + /// + /// Creates back by a given . + /// + /// open on a Portable PDB. + /// is null. + /// + /// The resulting reader does not take ownership of the or the memory it reads. + /// + public static unsafe EditAndContinueMethodDebugInfoReader Create(MetadataReader pdbReader) + => new Portable(pdbReader ?? throw new ArgumentNullException(nameof(pdbReader))); + + internal static bool TryGetDocumentChecksum(ISymUnmanagedReader5 symReader, string documentPath, out ImmutableArray checksum, out Guid algorithmId) + { + var symDocument = symReader.GetDocument(documentPath); - algorithmId = symDocument.GetHashAlgorithm(); - checksum = symDocument.GetChecksum().ToImmutableArray(); - return true; + // Make sure the full path matches. + // Native SymReader allows partial match on the document file name. + if (symDocument == null || !StringComparer.Ordinal.Equals(symDocument.GetName(), documentPath)) + { + checksum = default; + algorithmId = default; + return false; } + + algorithmId = symDocument.GetHashAlgorithm(); + checksum = symDocument.GetChecksum().ToImmutableArray(); + return true; } } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs index 05b8980866329..4c3265462f7e7 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs @@ -18,261 +18,260 @@ using Roslyn.Utilities; using Microsoft.CodeAnalysis.ErrorReporting; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Implements core of Edit and Continue orchestration: management of edit sessions and connecting EnC related services. +/// +[Export(typeof(IEditAndContinueService)), Shared] +internal sealed class EditAndContinueService : IEditAndContinueService { - /// - /// Implements core of Edit and Continue orchestration: management of edit sessions and connecting EnC related services. - /// - [Export(typeof(IEditAndContinueService)), Shared] - internal sealed class EditAndContinueService : IEditAndContinueService + [ExportWorkspaceService(typeof(IEditAndContinueWorkspaceService)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + internal sealed class WorkspaceService(IEditAndContinueService service) : IEditAndContinueWorkspaceService { - [ExportWorkspaceService(typeof(IEditAndContinueWorkspaceService)), Shared] - [method: ImportingConstructor] - [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - internal sealed class WorkspaceService(IEditAndContinueService service) : IEditAndContinueWorkspaceService - { - public IEditAndContinueService Service { get; } = service; - } + public IEditAndContinueService Service { get; } = service; + } - internal static readonly TraceLog Log; - internal static readonly TraceLog AnalysisLog; + internal static readonly TraceLog Log; + internal static readonly TraceLog AnalysisLog; - private Func _compilationOutputsProvider; + private Func _compilationOutputsProvider; - /// - /// List of active debugging sessions (small number of simoultaneously active sessions is expected). - /// - private readonly List _debuggingSessions = []; - private static int s_debuggingSessionId; + /// + /// List of active debugging sessions (small number of simoultaneously active sessions is expected). + /// + private readonly List _debuggingSessions = []; + private static int s_debuggingSessionId; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EditAndContinueService() - { - _compilationOutputsProvider = GetCompilationOutputs; - } + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public EditAndContinueService() + { + _compilationOutputsProvider = GetCompilationOutputs; + } - static EditAndContinueService() - { - Log = new(2048, "EnC", "Trace.log"); - AnalysisLog = new(1024, "EnC", "Analysis.log"); + static EditAndContinueService() + { + Log = new(2048, "EnC", "Trace.log"); + AnalysisLog = new(1024, "EnC", "Analysis.log"); - var logDir = GetLogDirectory(); - if (logDir != null) - { - Log.SetLogDirectory(logDir); - AnalysisLog.SetLogDirectory(logDir); - } + var logDir = GetLogDirectory(); + if (logDir != null) + { + Log.SetLogDirectory(logDir); + AnalysisLog.SetLogDirectory(logDir); } + } - private static string? GetLogDirectory() + private static string? GetLogDirectory() + { + try { - try - { - var path = Environment.GetEnvironmentVariable("Microsoft_CodeAnalysis_EditAndContinue_LogDir"); - if (string.IsNullOrWhiteSpace(path)) - { - return null; - } - - Directory.CreateDirectory(path); - return path; - } - catch + var path = Environment.GetEnvironmentVariable("Microsoft_CodeAnalysis_EditAndContinue_LogDir"); + if (string.IsNullOrWhiteSpace(path)) { return null; } - } - public void SetFileLoggingDirectory(string? logDirectory) - { - Log.SetLogDirectory(logDirectory ?? GetLogDirectory()); + Directory.CreateDirectory(path); + return path; } - - private static CompilationOutputs GetCompilationOutputs(Project project) + catch { - // The Project System doesn't always indicate whether we emit PDB, what kind of PDB we emit nor the path of the PDB. - // To work around we look for the PDB on the path specified in the PDB debug directory. - // https://github.com/dotnet/roslyn/issues/35065 - return new CompilationOutputFilesWithImplicitPdbPath(project.CompilationOutputInfo.AssemblyPath); + return null; } + } - private DebuggingSession? TryGetDebuggingSession(DebuggingSessionId sessionId) + public void SetFileLoggingDirectory(string? logDirectory) + { + Log.SetLogDirectory(logDirectory ?? GetLogDirectory()); + } + + private static CompilationOutputs GetCompilationOutputs(Project project) + { + // The Project System doesn't always indicate whether we emit PDB, what kind of PDB we emit nor the path of the PDB. + // To work around we look for the PDB on the path specified in the PDB debug directory. + // https://github.com/dotnet/roslyn/issues/35065 + return new CompilationOutputFilesWithImplicitPdbPath(project.CompilationOutputInfo.AssemblyPath); + } + + private DebuggingSession? TryGetDebuggingSession(DebuggingSessionId sessionId) + { + lock (_debuggingSessions) { - lock (_debuggingSessions) - { - return _debuggingSessions.SingleOrDefault(s => s.Id == sessionId); - } + return _debuggingSessions.SingleOrDefault(s => s.Id == sessionId); } + } - private ImmutableArray GetActiveDebuggingSessions() + private ImmutableArray GetActiveDebuggingSessions() + { + lock (_debuggingSessions) { - lock (_debuggingSessions) - { - return _debuggingSessions.ToImmutableArray(); - } + return _debuggingSessions.ToImmutableArray(); } + } - private ImmutableArray GetDiagnosticReportingDebuggingSessions() + private ImmutableArray GetDiagnosticReportingDebuggingSessions() + { + lock (_debuggingSessions) { - lock (_debuggingSessions) - { - return _debuggingSessions.Where(s => s.ReportDiagnostics).ToImmutableArray(); - } + return _debuggingSessions.Where(s => s.ReportDiagnostics).ToImmutableArray(); } + } - public async ValueTask StartDebuggingSessionAsync( - Solution solution, - IManagedHotReloadService debuggerService, - IPdbMatchingSourceTextProvider sourceTextProvider, - ImmutableArray captureMatchingDocuments, - bool captureAllMatchingDocuments, - bool reportDiagnostics, - CancellationToken cancellationToken) + public async ValueTask StartDebuggingSessionAsync( + Solution solution, + IManagedHotReloadService debuggerService, + IPdbMatchingSourceTextProvider sourceTextProvider, + ImmutableArray captureMatchingDocuments, + bool captureAllMatchingDocuments, + bool reportDiagnostics, + CancellationToken cancellationToken) + { + try { - try - { - Contract.ThrowIfTrue(captureAllMatchingDocuments && !captureMatchingDocuments.IsEmpty); - - IEnumerable> initialDocumentStates; - - if (captureAllMatchingDocuments || !captureMatchingDocuments.IsEmpty) - { - var documentsByProject = captureAllMatchingDocuments - ? solution.Projects.Select(project => (project, project.State.DocumentStates.States.Values)) - : GetDocumentStatesGroupedByProject(solution, captureMatchingDocuments); + Contract.ThrowIfTrue(captureAllMatchingDocuments && !captureMatchingDocuments.IsEmpty); - initialDocumentStates = await CommittedSolution.GetMatchingDocumentsAsync(documentsByProject, _compilationOutputsProvider, sourceTextProvider, cancellationToken).ConfigureAwait(false); - } - else - { - initialDocumentStates = SpecializedCollections.EmptyEnumerable>(); - } + IEnumerable> initialDocumentStates; - var sessionId = new DebuggingSessionId(Interlocked.Increment(ref s_debuggingSessionId)); - var session = new DebuggingSession(sessionId, solution, debuggerService, _compilationOutputsProvider, sourceTextProvider, initialDocumentStates, reportDiagnostics); - - lock (_debuggingSessions) - { - _debuggingSessions.Add(session); - } - - Log.Write("Session #{0} started.", sessionId.Ordinal); - return sessionId; + if (captureAllMatchingDocuments || !captureMatchingDocuments.IsEmpty) + { + var documentsByProject = captureAllMatchingDocuments + ? solution.Projects.Select(project => (project, project.State.DocumentStates.States.Values)) + : GetDocumentStatesGroupedByProject(solution, captureMatchingDocuments); + initialDocumentStates = await CommittedSolution.GetMatchingDocumentsAsync(documentsByProject, _compilationOutputsProvider, sourceTextProvider, cancellationToken).ConfigureAwait(false); } - catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, cancellationToken)) + else { - throw ExceptionUtilities.Unreachable(); + initialDocumentStates = SpecializedCollections.EmptyEnumerable>(); } - } - private static IEnumerable<(Project, IEnumerable)> GetDocumentStatesGroupedByProject(Solution solution, ImmutableArray documentIds) - => from documentId in documentIds - where solution.ContainsDocument(documentId) - group documentId by documentId.ProjectId into projectDocumentIds - let project = solution.GetRequiredProject(projectDocumentIds.Key) - select (project, from documentId in projectDocumentIds select project.State.DocumentStates.GetState(documentId)); + var sessionId = new DebuggingSessionId(Interlocked.Increment(ref s_debuggingSessionId)); + var session = new DebuggingSession(sessionId, solution, debuggerService, _compilationOutputsProvider, sourceTextProvider, initialDocumentStates, reportDiagnostics); - public void EndDebuggingSession(DebuggingSessionId sessionId, out ImmutableArray documentsToReanalyze) - { - DebuggingSession? debuggingSession; lock (_debuggingSessions) { - _debuggingSessions.TryRemoveFirst((s, sessionId) => s.Id == sessionId, sessionId, out debuggingSession); + _debuggingSessions.Add(session); } - Contract.ThrowIfNull(debuggingSession, "Debugging session has not started."); - - debuggingSession.EndSession(out documentsToReanalyze, out var telemetryData); + Log.Write("Session #{0} started.", sessionId.Ordinal); + return sessionId; - Log.Write("Session #{0} ended.", debuggingSession.Id.Ordinal); } - - public void BreakStateOrCapabilitiesChanged(DebuggingSessionId sessionId, bool? inBreakState, out ImmutableArray documentsToReanalyze) + catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, cancellationToken)) { - var debuggingSession = TryGetDebuggingSession(sessionId); - Contract.ThrowIfNull(debuggingSession); - debuggingSession.BreakStateOrCapabilitiesChanged(inBreakState, out documentsToReanalyze); + throw ExceptionUtilities.Unreachable(); } + } + + private static IEnumerable<(Project, IEnumerable)> GetDocumentStatesGroupedByProject(Solution solution, ImmutableArray documentIds) + => from documentId in documentIds + where solution.ContainsDocument(documentId) + group documentId by documentId.ProjectId into projectDocumentIds + let project = solution.GetRequiredProject(projectDocumentIds.Key) + select (project, from documentId in projectDocumentIds select project.State.DocumentStates.GetState(documentId)); - public ValueTask> GetDocumentDiagnosticsAsync(Document document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + public void EndDebuggingSession(DebuggingSessionId sessionId, out ImmutableArray documentsToReanalyze) + { + DebuggingSession? debuggingSession; + lock (_debuggingSessions) { - return GetDiagnosticReportingDebuggingSessions().SelectManyAsArrayAsync( - (s, arg, cancellationToken) => s.GetDocumentDiagnosticsAsync(arg.document, arg.activeStatementSpanProvider, cancellationToken), - (document, activeStatementSpanProvider), - cancellationToken); + _debuggingSessions.TryRemoveFirst((s, sessionId) => s.Id == sessionId, sessionId, out debuggingSession); } - public ValueTask EmitSolutionUpdateAsync( - DebuggingSessionId sessionId, - Solution solution, - ActiveStatementSpanProvider activeStatementSpanProvider, - CancellationToken cancellationToken) - { - var debuggingSession = TryGetDebuggingSession(sessionId); - if (debuggingSession == null) - { - return ValueTaskFactory.FromResult(EmitSolutionUpdateResults.Empty); - } + Contract.ThrowIfNull(debuggingSession, "Debugging session has not started."); - return debuggingSession.EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, cancellationToken); - } + debuggingSession.EndSession(out documentsToReanalyze, out var telemetryData); - public void CommitSolutionUpdate(DebuggingSessionId sessionId, out ImmutableArray documentsToReanalyze) - { - var debuggingSession = TryGetDebuggingSession(sessionId); - Contract.ThrowIfNull(debuggingSession); + Log.Write("Session #{0} ended.", debuggingSession.Id.Ordinal); + } - debuggingSession.CommitSolutionUpdate(out documentsToReanalyze); - } + public void BreakStateOrCapabilitiesChanged(DebuggingSessionId sessionId, bool? inBreakState, out ImmutableArray documentsToReanalyze) + { + var debuggingSession = TryGetDebuggingSession(sessionId); + Contract.ThrowIfNull(debuggingSession); + debuggingSession.BreakStateOrCapabilitiesChanged(inBreakState, out documentsToReanalyze); + } - public void DiscardSolutionUpdate(DebuggingSessionId sessionId) - { - var debuggingSession = TryGetDebuggingSession(sessionId); - Contract.ThrowIfNull(debuggingSession); + public ValueTask> GetDocumentDiagnosticsAsync(Document document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + { + return GetDiagnosticReportingDebuggingSessions().SelectManyAsArrayAsync( + (s, arg, cancellationToken) => s.GetDocumentDiagnosticsAsync(arg.document, arg.activeStatementSpanProvider, cancellationToken), + (document, activeStatementSpanProvider), + cancellationToken); + } - debuggingSession.DiscardSolutionUpdate(); + public ValueTask EmitSolutionUpdateAsync( + DebuggingSessionId sessionId, + Solution solution, + ActiveStatementSpanProvider activeStatementSpanProvider, + CancellationToken cancellationToken) + { + var debuggingSession = TryGetDebuggingSession(sessionId); + if (debuggingSession == null) + { + return ValueTaskFactory.FromResult(EmitSolutionUpdateResults.Empty); } - public ValueTask>> GetBaseActiveStatementSpansAsync(DebuggingSessionId sessionId, Solution solution, ImmutableArray documentIds, CancellationToken cancellationToken) - { - var debuggingSession = TryGetDebuggingSession(sessionId); - if (debuggingSession == null) - { - return default; - } + return debuggingSession.EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, cancellationToken); + } - return debuggingSession.GetBaseActiveStatementSpansAsync(solution, documentIds, cancellationToken); - } + public void CommitSolutionUpdate(DebuggingSessionId sessionId, out ImmutableArray documentsToReanalyze) + { + var debuggingSession = TryGetDebuggingSession(sessionId); + Contract.ThrowIfNull(debuggingSession); - public ValueTask> GetAdjustedActiveStatementSpansAsync(DebuggingSessionId sessionId, TextDocument mappedDocument, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) - { - var debuggingSession = TryGetDebuggingSession(sessionId); - if (debuggingSession == null) - { - return ValueTaskFactory.FromResult(ImmutableArray.Empty); - } + debuggingSession.CommitSolutionUpdate(out documentsToReanalyze); + } + + public void DiscardSolutionUpdate(DebuggingSessionId sessionId) + { + var debuggingSession = TryGetDebuggingSession(sessionId); + Contract.ThrowIfNull(debuggingSession); + + debuggingSession.DiscardSolutionUpdate(); + } - return debuggingSession.GetAdjustedActiveStatementSpansAsync(mappedDocument, activeStatementSpanProvider, cancellationToken); + public ValueTask>> GetBaseActiveStatementSpansAsync(DebuggingSessionId sessionId, Solution solution, ImmutableArray documentIds, CancellationToken cancellationToken) + { + var debuggingSession = TryGetDebuggingSession(sessionId); + if (debuggingSession == null) + { + return default; } - internal TestAccessor GetTestAccessor() - => new(this); + return debuggingSession.GetBaseActiveStatementSpansAsync(solution, documentIds, cancellationToken); + } - internal readonly struct TestAccessor(EditAndContinueService service) + public ValueTask> GetAdjustedActiveStatementSpansAsync(DebuggingSessionId sessionId, TextDocument mappedDocument, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + { + var debuggingSession = TryGetDebuggingSession(sessionId); + if (debuggingSession == null) { - private readonly EditAndContinueService _service = service; + return ValueTaskFactory.FromResult(ImmutableArray.Empty); + } - public void SetOutputProvider(Func value) - => _service._compilationOutputsProvider = value; + return debuggingSession.GetAdjustedActiveStatementSpansAsync(mappedDocument, activeStatementSpanProvider, cancellationToken); + } - public DebuggingSession GetDebuggingSession(DebuggingSessionId id) - => _service.TryGetDebuggingSession(id) ?? throw ExceptionUtilities.UnexpectedValue(id); + internal TestAccessor GetTestAccessor() + => new(this); - public ImmutableArray GetActiveDebuggingSessions() - => _service.GetActiveDebuggingSessions(); + internal readonly struct TestAccessor(EditAndContinueService service) + { + private readonly EditAndContinueService _service = service; + + public void SetOutputProvider(Func value) + => _service._compilationOutputsProvider = value; + + public DebuggingSession GetDebuggingSession(DebuggingSessionId id) + => _service.TryGetDebuggingSession(id) ?? throw ExceptionUtilities.UnexpectedValue(id); + + public ImmutableArray GetActiveDebuggingSessions() + => _service.GetActiveDebuggingSessions(); - } } } diff --git a/src/Features/Core/Portable/EditAndContinue/EditSession.cs b/src/Features/Core/Portable/EditAndContinue/EditSession.cs index 713a9192cd2d5..89ed4e4c87244 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditSession.cs @@ -20,1325 +20,1324 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal sealed class EditSession { - internal sealed class EditSession + internal readonly DebuggingSession DebuggingSession; + internal readonly EditSessionTelemetry Telemetry; + + // Maps active statement instructions reported by the debugger to their latest spans that might not yet have been applied + // (remapping not triggered yet). Consumed by the next edit session and updated when changes are committed at the end of the edit session. + // + // Consider a function F containing a call to function G. While G is being executed, F is updated a couple of times (in two edit sessions) + // before the thread returns from G and is remapped to the latest version of F. At the start of the second edit session, + // the active instruction reported by the debugger is still at the original location since function F has not been remapped yet (G has not returned yet). + // + // '>' indicates an active statement instruction for non-leaf frame reported by the debugger. + // v1 - before first edit, G executing + // v2 - after first edit, G still executing + // v3 - after second edit and G returned + // + // F v1: F v2: F v3: + // 0: nop 0: nop 0: nop + // 1> G() 1> nop 1: nop + // 2: nop 2: G() 2: nop + // 3: nop 3: nop 3> G() + // + // When entering a break state we query the debugger for current active statements. + // The returned statements reflect the current state of the threads in the runtime. + // When a change is successfully applied we remember changes in active statement spans. + // These changes are passed to the next edit session. + // We use them to map the spans for active statements returned by the debugger. + // + // In the above case the sequence of events is + // 1st break: get active statements returns (F, v=1, il=1, span1) the active statement is up-to-date + // 1st apply: detected span change for active statement (F, v=1, il=1): span1->span2 + // 2nd break: previously updated statements contains (F, v=1, il=1)->span2 + // get active statements returns (F, v=1, il=1, span1) which is mapped to (F, v=1, il=1, span2) using previously updated statements + // 2nd apply: detected span change for active statement (F, v=1, il=1): span2->span3 + // 3rd break: previously updated statements contains (F, v=1, il=1)->span3 + // get active statements returns (F, v=3, il=3, span3) the active statement is up-to-date + // + internal readonly ImmutableDictionary> NonRemappableRegions; + + /// + /// Gets the capabilities of the runtime with respect to applying code changes. + /// Retrieved lazily from since they are only needed when changes are detected in the solution. + /// + internal readonly AsyncLazy Capabilities; + + /// + /// Map of base active statements. + /// Calculated lazily based on info retrieved from since it is only needed when changes are detected in the solution. + /// + internal readonly AsyncLazy BaseActiveStatements; + + /// + /// Cache of document EnC analyses. + /// + internal readonly EditAndContinueDocumentAnalysesCache Analyses; + + /// + /// True for Edit and Continue edit sessions - when the application is in break state. + /// False for Hot Reload edit sessions - when the application is running. + /// + internal readonly bool InBreakState; + + /// + /// A is added whenever EnC analyzer reports + /// rude edits or module diagnostics. At the end of the session we ask the diagnostic analyzer to reanalyze + /// the documents to clean up the diagnostics. + /// + private readonly HashSet _documentsWithReportedDiagnostics = []; + private readonly object _documentsWithReportedDiagnosticsGuard = new(); + + internal EditSession( + DebuggingSession debuggingSession, + ImmutableDictionary> nonRemappableRegions, + EditSessionTelemetry telemetry, + AsyncLazy? lazyActiveStatementMap, + bool inBreakState) { - internal readonly DebuggingSession DebuggingSession; - internal readonly EditSessionTelemetry Telemetry; + DebuggingSession = debuggingSession; + NonRemappableRegions = nonRemappableRegions; + Telemetry = telemetry; + InBreakState = inBreakState; - // Maps active statement instructions reported by the debugger to their latest spans that might not yet have been applied - // (remapping not triggered yet). Consumed by the next edit session and updated when changes are committed at the end of the edit session. - // - // Consider a function F containing a call to function G. While G is being executed, F is updated a couple of times (in two edit sessions) - // before the thread returns from G and is remapped to the latest version of F. At the start of the second edit session, - // the active instruction reported by the debugger is still at the original location since function F has not been remapped yet (G has not returned yet). - // - // '>' indicates an active statement instruction for non-leaf frame reported by the debugger. - // v1 - before first edit, G executing - // v2 - after first edit, G still executing - // v3 - after second edit and G returned - // - // F v1: F v2: F v3: - // 0: nop 0: nop 0: nop - // 1> G() 1> nop 1: nop - // 2: nop 2: G() 2: nop - // 3: nop 3: nop 3> G() - // - // When entering a break state we query the debugger for current active statements. - // The returned statements reflect the current state of the threads in the runtime. - // When a change is successfully applied we remember changes in active statement spans. - // These changes are passed to the next edit session. - // We use them to map the spans for active statements returned by the debugger. - // - // In the above case the sequence of events is - // 1st break: get active statements returns (F, v=1, il=1, span1) the active statement is up-to-date - // 1st apply: detected span change for active statement (F, v=1, il=1): span1->span2 - // 2nd break: previously updated statements contains (F, v=1, il=1)->span2 - // get active statements returns (F, v=1, il=1, span1) which is mapped to (F, v=1, il=1, span2) using previously updated statements - // 2nd apply: detected span change for active statement (F, v=1, il=1): span2->span3 - // 3rd break: previously updated statements contains (F, v=1, il=1)->span3 - // get active statements returns (F, v=3, il=3, span3) the active statement is up-to-date - // - internal readonly ImmutableDictionary> NonRemappableRegions; - - /// - /// Gets the capabilities of the runtime with respect to applying code changes. - /// Retrieved lazily from since they are only needed when changes are detected in the solution. - /// - internal readonly AsyncLazy Capabilities; - - /// - /// Map of base active statements. - /// Calculated lazily based on info retrieved from since it is only needed when changes are detected in the solution. - /// - internal readonly AsyncLazy BaseActiveStatements; - - /// - /// Cache of document EnC analyses. - /// - internal readonly EditAndContinueDocumentAnalysesCache Analyses; - - /// - /// True for Edit and Continue edit sessions - when the application is in break state. - /// False for Hot Reload edit sessions - when the application is running. - /// - internal readonly bool InBreakState; - - /// - /// A is added whenever EnC analyzer reports - /// rude edits or module diagnostics. At the end of the session we ask the diagnostic analyzer to reanalyze - /// the documents to clean up the diagnostics. - /// - private readonly HashSet _documentsWithReportedDiagnostics = []; - private readonly object _documentsWithReportedDiagnosticsGuard = new(); - - internal EditSession( - DebuggingSession debuggingSession, - ImmutableDictionary> nonRemappableRegions, - EditSessionTelemetry telemetry, - AsyncLazy? lazyActiveStatementMap, - bool inBreakState) - { - DebuggingSession = debuggingSession; - NonRemappableRegions = nonRemappableRegions; - Telemetry = telemetry; - InBreakState = inBreakState; + telemetry.SetBreakState(inBreakState); - telemetry.SetBreakState(inBreakState); + BaseActiveStatements = lazyActiveStatementMap ?? (inBreakState + ? AsyncLazy.Create(GetBaseActiveStatementsAsync) + : new AsyncLazy(ActiveStatementsMap.Empty)); - BaseActiveStatements = lazyActiveStatementMap ?? (inBreakState - ? AsyncLazy.Create(GetBaseActiveStatementsAsync) - : new AsyncLazy(ActiveStatementsMap.Empty)); + Capabilities = AsyncLazy.Create(GetCapabilitiesAsync); + Analyses = new EditAndContinueDocumentAnalysesCache(BaseActiveStatements, Capabilities); + } + + /// + /// The compiler has various scenarios that will cause it to synthesize things that might not be covered + /// by existing rude edits, but we still need to ensure the runtime supports them before we proceed. + /// + private async Task GetUnsupportedChangesDiagnosticAsync(EmitDifferenceResult emitResult, CancellationToken cancellationToken) + { + Debug.Assert(emitResult.Success); + Debug.Assert(emitResult.Baseline is not null); - Capabilities = AsyncLazy.Create(GetCapabilitiesAsync); - Analyses = new EditAndContinueDocumentAnalysesCache(BaseActiveStatements, Capabilities); + // if there were no changed types then there is nothing to check + if (emitResult.ChangedTypes.Length == 0) + { + return null; } - /// - /// The compiler has various scenarios that will cause it to synthesize things that might not be covered - /// by existing rude edits, but we still need to ensure the runtime supports them before we proceed. - /// - private async Task GetUnsupportedChangesDiagnosticAsync(EmitDifferenceResult emitResult, CancellationToken cancellationToken) + var capabilities = await Capabilities.GetValueAsync(cancellationToken).ConfigureAwait(false); + if (!capabilities.HasFlag(EditAndContinueCapabilities.NewTypeDefinition)) { - Debug.Assert(emitResult.Success); - Debug.Assert(emitResult.Baseline is not null); + // If the runtime doesn't support adding new types then we expect every row number for any type that is + // emitted will be less than or equal to the number of rows in the original metadata. + var highestEmittedTypeDefRow = emitResult.ChangedTypes.Max(t => MetadataTokens.GetRowNumber(t)); + var highestExistingTypeDefRow = emitResult.Baseline.OriginalMetadata.GetMetadataReader().GetTableRowCount(TableIndex.TypeDef); - // if there were no changed types then there is nothing to check - if (emitResult.ChangedTypes.Length == 0) + if (highestEmittedTypeDefRow > highestExistingTypeDefRow) { - return null; + var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.AddingTypeRuntimeCapabilityRequired); + return Diagnostic.Create(descriptor, Location.None); } + } - var capabilities = await Capabilities.GetValueAsync(cancellationToken).ConfigureAwait(false); - if (!capabilities.HasFlag(EditAndContinueCapabilities.NewTypeDefinition)) - { - // If the runtime doesn't support adding new types then we expect every row number for any type that is - // emitted will be less than or equal to the number of rows in the original metadata. - var highestEmittedTypeDefRow = emitResult.ChangedTypes.Max(t => MetadataTokens.GetRowNumber(t)); - var highestExistingTypeDefRow = emitResult.Baseline.OriginalMetadata.GetMetadataReader().GetTableRowCount(TableIndex.TypeDef); + return null; + } - if (highestEmittedTypeDefRow > highestExistingTypeDefRow) - { - var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.AddingTypeRuntimeCapabilityRequired); - return Diagnostic.Create(descriptor, Location.None); - } - } + /// + /// Errors to be reported when a project is updated but the corresponding module does not support EnC. + /// + /// if the module is not loaded. + public async Task?> GetModuleDiagnosticsAsync(Guid mvid, Project oldProject, Project newProject, ImmutableArray documentAnalyses, CancellationToken cancellationToken) + { + Contract.ThrowIfTrue(documentAnalyses.IsEmpty); + var availability = await DebuggingSession.DebuggerService.GetAvailabilityAsync(mvid, cancellationToken).ConfigureAwait(false); + if (availability.Status == ManagedHotReloadAvailabilityStatus.ModuleNotLoaded) + { return null; } - /// - /// Errors to be reported when a project is updated but the corresponding module does not support EnC. - /// - /// if the module is not loaded. - public async Task?> GetModuleDiagnosticsAsync(Guid mvid, Project oldProject, Project newProject, ImmutableArray documentAnalyses, CancellationToken cancellationToken) + if (availability.Status == ManagedHotReloadAvailabilityStatus.Available) { - Contract.ThrowIfTrue(documentAnalyses.IsEmpty); + return ImmutableArray.Empty; + } - var availability = await DebuggingSession.DebuggerService.GetAvailabilityAsync(mvid, cancellationToken).ConfigureAwait(false); - if (availability.Status == ManagedHotReloadAvailabilityStatus.ModuleNotLoaded) - { - return null; - } + var descriptor = EditAndContinueDiagnosticDescriptors.GetModuleDiagnosticDescriptor(availability.Status); + var messageArgs = new[] { newProject.Name, availability.LocalizedMessage }; - if (availability.Status == ManagedHotReloadAvailabilityStatus.Available) - { - return ImmutableArray.Empty; - } + using var _ = ArrayBuilder.GetInstance(out var diagnostics); + + await foreach (var location in CreateChangedLocationsAsync(oldProject, newProject, documentAnalyses, cancellationToken).ConfigureAwait(false)) + { + diagnostics.Add(Diagnostic.Create(descriptor, location, messageArgs)); + } - var descriptor = EditAndContinueDiagnosticDescriptors.GetModuleDiagnosticDescriptor(availability.Status); - var messageArgs = new[] { newProject.Name, availability.LocalizedMessage }; + return diagnostics.ToImmutable(); + } - using var _ = ArrayBuilder.GetInstance(out var diagnostics); + private static async IAsyncEnumerable CreateChangedLocationsAsync(Project oldProject, Project newProject, ImmutableArray documentAnalyses, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var hasRemovedOrAddedDocument = false; + foreach (var documentAnalysis in documentAnalyses) + { + if (!documentAnalysis.HasChanges) + { + continue; + } - await foreach (var location in CreateChangedLocationsAsync(oldProject, newProject, documentAnalyses, cancellationToken).ConfigureAwait(false)) + var oldDocument = await oldProject.GetDocumentAsync(documentAnalysis.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + var newDocument = await newProject.GetDocumentAsync(documentAnalysis.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + if (oldDocument == null || newDocument == null) { - diagnostics.Add(Diagnostic.Create(descriptor, location, messageArgs)); + hasRemovedOrAddedDocument = true; + continue; } - return diagnostics.ToImmutable(); + var oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var newTree = await newDocument.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + + // document location: + yield return Location.Create(newTree, GetFirstLineDifferenceSpan(oldText, newText)); } - private static async IAsyncEnumerable CreateChangedLocationsAsync(Project oldProject, Project newProject, ImmutableArray documentAnalyses, [EnumeratorCancellation] CancellationToken cancellationToken) + // project location: + if (hasRemovedOrAddedDocument) { - var hasRemovedOrAddedDocument = false; - foreach (var documentAnalysis in documentAnalyses) - { - if (!documentAnalysis.HasChanges) - { - continue; - } - - var oldDocument = await oldProject.GetDocumentAsync(documentAnalysis.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - var newDocument = await newProject.GetDocumentAsync(documentAnalysis.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - if (oldDocument == null || newDocument == null) - { - hasRemovedOrAddedDocument = true; - continue; - } - - var oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var newTree = await newDocument.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + yield return Location.None; + } + } - // document location: - yield return Location.Create(newTree, GetFirstLineDifferenceSpan(oldText, newText)); - } + private static TextSpan GetFirstLineDifferenceSpan(SourceText oldText, SourceText newText) + { + var oldLineCount = oldText.Lines.Count; + var newLineCount = newText.Lines.Count; - // project location: - if (hasRemovedOrAddedDocument) + for (var i = 0; i < Math.Min(oldLineCount, newLineCount); i++) + { + var oldLineSpan = oldText.Lines[i].Span; + var newLineSpan = newText.Lines[i].Span; + if (oldLineSpan != newLineSpan || !oldText.GetSubText(oldLineSpan).ContentEquals(newText.GetSubText(newLineSpan))) { - yield return Location.None; + return newText.Lines[i].Span; } } - private static TextSpan GetFirstLineDifferenceSpan(SourceText oldText, SourceText newText) + return (oldLineCount == newLineCount) ? default : + (newLineCount > oldLineCount) ? newText.Lines[oldLineCount].Span : + TextSpan.FromBounds(newText.Lines[newLineCount - 1].End, newText.Lines[newLineCount - 1].EndIncludingLineBreak); + } + + private async Task GetCapabilitiesAsync(CancellationToken cancellationToken) + { + try + { + var capabilities = await DebuggingSession.DebuggerService.GetCapabilitiesAsync(cancellationToken).ConfigureAwait(false); + return EditAndContinueCapabilitiesParser.Parse(capabilities); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - var oldLineCount = oldText.Lines.Count; - var newLineCount = newText.Lines.Count; + return EditAndContinueCapabilities.Baseline; + } + } - for (var i = 0; i < Math.Min(oldLineCount, newLineCount); i++) - { - var oldLineSpan = oldText.Lines[i].Span; - var newLineSpan = newText.Lines[i].Span; - if (oldLineSpan != newLineSpan || !oldText.GetSubText(oldLineSpan).ContentEquals(newText.GetSubText(newLineSpan))) - { - return newText.Lines[i].Span; - } - } + private async Task GetBaseActiveStatementsAsync(CancellationToken cancellationToken) + { + try + { + // Last committed solution reflects the state of the source that is in sync with the binaries that are loaded in the debuggee. + var debugInfos = await DebuggingSession.DebuggerService.GetActiveStatementsAsync(cancellationToken).ConfigureAwait(false); + return ActiveStatementsMap.Create(debugInfos, NonRemappableRegions); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + return ActiveStatementsMap.Empty; + } + } - return (oldLineCount == newLineCount) ? default : - (newLineCount > oldLineCount) ? newText.Lines[oldLineCount].Span : - TextSpan.FromBounds(newText.Lines[newLineCount - 1].End, newText.Lines[newLineCount - 1].EndIncludingLineBreak); + public static async ValueTask HasChangesAsync(Solution oldSolution, Solution newSolution, string sourceFilePath, CancellationToken cancellationToken) + { + // Note that this path look up does not work for source-generated files: + var newDocumentIds = newSolution.GetDocumentIdsWithFilePath(sourceFilePath); + if (newDocumentIds.IsEmpty) + { + // file does not exist or has been removed: + return !oldSolution.GetDocumentIdsWithFilePath(sourceFilePath).IsEmpty; } - private async Task GetCapabilitiesAsync(CancellationToken cancellationToken) + // it suffices to check the content of one of the document if there are multiple linked ones: + var documentId = newDocumentIds.First(); + var oldDocument = oldSolution.GetTextDocument(documentId); + if (oldDocument == null) { - try - { - var capabilities = await DebuggingSession.DebuggerService.GetCapabilitiesAsync(cancellationToken).ConfigureAwait(false); - return EditAndContinueCapabilitiesParser.Parse(capabilities); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return EditAndContinueCapabilities.Baseline; - } + // file has been added + return true; } - private async Task GetBaseActiveStatementsAsync(CancellationToken cancellationToken) + var newDocument = newSolution.GetRequiredTextDocument(documentId); + return oldDocument != newDocument && !await ContentEqualsAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false); + } + + public static async ValueTask HasChangesAsync(Solution oldSolution, Solution newSolution, CancellationToken cancellationToken) + { + if (oldSolution == newSolution) { - try - { - // Last committed solution reflects the state of the source that is in sync with the binaries that are loaded in the debuggee. - var debugInfos = await DebuggingSession.DebuggerService.GetActiveStatementsAsync(cancellationToken).ConfigureAwait(false); - return ActiveStatementsMap.Create(debugInfos, NonRemappableRegions); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return ActiveStatementsMap.Empty; - } + return false; } - public static async ValueTask HasChangesAsync(Solution oldSolution, Solution newSolution, string sourceFilePath, CancellationToken cancellationToken) + foreach (var newProject in newSolution.Projects) { - // Note that this path look up does not work for source-generated files: - var newDocumentIds = newSolution.GetDocumentIdsWithFilePath(sourceFilePath); - if (newDocumentIds.IsEmpty) + if (!newProject.SupportsEditAndContinue()) { - // file does not exist or has been removed: - return !oldSolution.GetDocumentIdsWithFilePath(sourceFilePath).IsEmpty; + continue; } - // it suffices to check the content of one of the document if there are multiple linked ones: - var documentId = newDocumentIds.First(); - var oldDocument = oldSolution.GetTextDocument(documentId); - if (oldDocument == null) + var oldProject = oldSolution.GetProject(newProject.Id); + if (oldProject == null || await HasChangedOrAddedDocumentsAsync(oldProject, newProject, changedOrAddedDocuments: null, cancellationToken).ConfigureAwait(false)) { - // file has been added + // project added or has changes return true; } - - var newDocument = newSolution.GetRequiredTextDocument(documentId); - return oldDocument != newDocument && !await ContentEqualsAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false); } - public static async ValueTask HasChangesAsync(Solution oldSolution, Solution newSolution, CancellationToken cancellationToken) + foreach (var oldProject in oldSolution.Projects) { - if (oldSolution == newSolution) + if (!oldProject.SupportsEditAndContinue()) { - return false; + continue; } - foreach (var newProject in newSolution.Projects) + var newProject = newSolution.GetProject(oldProject.Id); + if (newProject == null) { - if (!newProject.SupportsEditAndContinue()) - { - continue; - } - - var oldProject = oldSolution.GetProject(newProject.Id); - if (oldProject == null || await HasChangedOrAddedDocumentsAsync(oldProject, newProject, changedOrAddedDocuments: null, cancellationToken).ConfigureAwait(false)) - { - // project added or has changes - return true; - } + // project removed + return true; } + } - foreach (var oldProject in oldSolution.Projects) - { - if (!oldProject.SupportsEditAndContinue()) - { - continue; - } + return false; + } - var newProject = newSolution.GetProject(oldProject.Id); - if (newProject == null) - { - // project removed - return true; - } - } + private static async ValueTask ContentEqualsAsync(TextDocument oldDocument, TextDocument newDocument, CancellationToken cancellationToken) + { + // Check if the currently observed document content has changed compared to the base document content. + // This is an important optimization that aims to avoid IO while stepping in sources that have not changed. + // + // We may be comparing out-of-date committed document content but we only make a decision based on that content + // if it matches the current content. If the current content is equal to baseline content that does not match + // the debuggee then the workspace has not observed the change made to the file on disk since baseline was captured + // (there had to be one as the content doesn't match). When we are about to apply changes it is ok to ignore this + // document because the user does not see the change yet in the buffer (if the doc is open) and won't be confused + // if it is not applied yet. The change will be applied later after it's observed by the workspace. + var oldSource = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var newSource = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + return oldSource.ContentEquals(newSource); + } + internal static async ValueTask HasChangedOrAddedDocumentsAsync(Project oldProject, Project newProject, ArrayBuilder? changedOrAddedDocuments, CancellationToken cancellationToken) + { + if (!newProject.SupportsEditAndContinue()) + { return false; } - private static async ValueTask ContentEqualsAsync(TextDocument oldDocument, TextDocument newDocument, CancellationToken cancellationToken) + if (oldProject.State == newProject.State) { - // Check if the currently observed document content has changed compared to the base document content. - // This is an important optimization that aims to avoid IO while stepping in sources that have not changed. - // - // We may be comparing out-of-date committed document content but we only make a decision based on that content - // if it matches the current content. If the current content is equal to baseline content that does not match - // the debuggee then the workspace has not observed the change made to the file on disk since baseline was captured - // (there had to be one as the content doesn't match). When we are about to apply changes it is ok to ignore this - // document because the user does not see the change yet in the buffer (if the doc is open) and won't be confused - // if it is not applied yet. The change will be applied later after it's observed by the workspace. - var oldSource = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var newSource = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - return oldSource.ContentEquals(newSource); + return false; } - internal static async ValueTask HasChangedOrAddedDocumentsAsync(Project oldProject, Project newProject, ArrayBuilder? changedOrAddedDocuments, CancellationToken cancellationToken) + foreach (var documentId in newProject.State.DocumentStates.GetChangedStateIds(oldProject.State.DocumentStates, ignoreUnchangedContent: true)) { - if (!newProject.SupportsEditAndContinue()) + var document = newProject.GetRequiredDocument(documentId); + if (document.State.Attributes.DesignTimeOnly) { - return false; + continue; } - if (oldProject.State == newProject.State) + if (await ContentEqualsAsync(oldProject.GetRequiredDocument(documentId), document, cancellationToken).ConfigureAwait(false)) { - return false; + continue; } - foreach (var documentId in newProject.State.DocumentStates.GetChangedStateIds(oldProject.State.DocumentStates, ignoreUnchangedContent: true)) + if (changedOrAddedDocuments is null) { - var document = newProject.GetRequiredDocument(documentId); - if (document.State.Attributes.DesignTimeOnly) - { - continue; - } - - if (await ContentEqualsAsync(oldProject.GetRequiredDocument(documentId), document, cancellationToken).ConfigureAwait(false)) - { - continue; - } - - if (changedOrAddedDocuments is null) - { - return true; - } - - changedOrAddedDocuments.Add(document); + return true; } - foreach (var documentId in newProject.State.DocumentStates.GetAddedStateIds(oldProject.State.DocumentStates)) - { - var document = newProject.GetRequiredDocument(documentId); - if (document.State.Attributes.DesignTimeOnly) - { - continue; - } - - if (changedOrAddedDocuments is null) - { - return true; - } + changedOrAddedDocuments.Add(document); + } - changedOrAddedDocuments.Add(document); + foreach (var documentId in newProject.State.DocumentStates.GetAddedStateIds(oldProject.State.DocumentStates)) + { + var document = newProject.GetRequiredDocument(documentId); + if (document.State.Attributes.DesignTimeOnly) + { + continue; } - // Any changes in non-generated document content might affect source generated documents as well, - // no need to check further in that case. - - if (changedOrAddedDocuments is { Count: > 0 }) + if (changedOrAddedDocuments is null) { return true; } - foreach (var documentId in newProject.State.AdditionalDocumentStates.GetChangedStateIds(oldProject.State.AdditionalDocumentStates, ignoreUnchangedContent: true)) - { - var document = newProject.GetRequiredAdditionalDocument(documentId); - if (!await ContentEqualsAsync(oldProject.GetRequiredAdditionalDocument(documentId), document, cancellationToken).ConfigureAwait(false)) - { - return true; - } - } + changedOrAddedDocuments.Add(document); + } - foreach (var documentId in newProject.State.AnalyzerConfigDocumentStates.GetChangedStateIds(oldProject.State.AnalyzerConfigDocumentStates, ignoreUnchangedContent: true)) - { - var document = newProject.GetRequiredAnalyzerConfigDocument(documentId); - if (!await ContentEqualsAsync(oldProject.GetRequiredAnalyzerConfigDocument(documentId), document, cancellationToken).ConfigureAwait(false)) - { - return true; - } - } + // Any changes in non-generated document content might affect source generated documents as well, + // no need to check further in that case. + + if (changedOrAddedDocuments is { Count: > 0 }) + { + return true; + } - // TODO: should handle removed documents above (detect them as edits) https://github.com/dotnet/roslyn/issues/62848 - if (newProject.State.DocumentStates.GetRemovedStateIds(oldProject.State.DocumentStates).Any() || - newProject.State.AdditionalDocumentStates.GetRemovedStateIds(oldProject.State.AdditionalDocumentStates).Any() || - newProject.State.AdditionalDocumentStates.GetAddedStateIds(oldProject.State.AdditionalDocumentStates).Any() || - newProject.State.AnalyzerConfigDocumentStates.GetRemovedStateIds(oldProject.State.AnalyzerConfigDocumentStates).Any() || - newProject.State.AnalyzerConfigDocumentStates.GetAddedStateIds(oldProject.State.AnalyzerConfigDocumentStates).Any()) + foreach (var documentId in newProject.State.AdditionalDocumentStates.GetChangedStateIds(oldProject.State.AdditionalDocumentStates, ignoreUnchangedContent: true)) + { + var document = newProject.GetRequiredAdditionalDocument(documentId); + if (!await ContentEqualsAsync(oldProject.GetRequiredAdditionalDocument(documentId), document, cancellationToken).ConfigureAwait(false)) { return true; } - - return false; } - internal static async Task PopulateChangedAndAddedDocumentsAsync(Project oldProject, Project newProject, ArrayBuilder changedOrAddedDocuments, CancellationToken cancellationToken) + foreach (var documentId in newProject.State.AnalyzerConfigDocumentStates.GetChangedStateIds(oldProject.State.AnalyzerConfigDocumentStates, ignoreUnchangedContent: true)) { - changedOrAddedDocuments.Clear(); - - if (!await HasChangedOrAddedDocumentsAsync(oldProject, newProject, changedOrAddedDocuments, cancellationToken).ConfigureAwait(false)) + var document = newProject.GetRequiredAnalyzerConfigDocument(documentId); + if (!await ContentEqualsAsync(oldProject.GetRequiredAnalyzerConfigDocument(documentId), document, cancellationToken).ConfigureAwait(false)) { - return; + return true; } + } - cancellationToken.ThrowIfCancellationRequested(); + // TODO: should handle removed documents above (detect them as edits) https://github.com/dotnet/roslyn/issues/62848 + if (newProject.State.DocumentStates.GetRemovedStateIds(oldProject.State.DocumentStates).Any() || + newProject.State.AdditionalDocumentStates.GetRemovedStateIds(oldProject.State.AdditionalDocumentStates).Any() || + newProject.State.AdditionalDocumentStates.GetAddedStateIds(oldProject.State.AdditionalDocumentStates).Any() || + newProject.State.AnalyzerConfigDocumentStates.GetRemovedStateIds(oldProject.State.AnalyzerConfigDocumentStates).Any() || + newProject.State.AnalyzerConfigDocumentStates.GetAddedStateIds(oldProject.State.AnalyzerConfigDocumentStates).Any()) + { + return true; + } - var oldSourceGeneratedDocumentStates = await oldProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(oldProject.State, cancellationToken).ConfigureAwait(false); + return false; + } - cancellationToken.ThrowIfCancellationRequested(); + internal static async Task PopulateChangedAndAddedDocumentsAsync(Project oldProject, Project newProject, ArrayBuilder changedOrAddedDocuments, CancellationToken cancellationToken) + { + changedOrAddedDocuments.Clear(); - var newSourceGeneratedDocumentStates = await newProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(newProject.State, cancellationToken).ConfigureAwait(false); + if (!await HasChangedOrAddedDocumentsAsync(oldProject, newProject, changedOrAddedDocuments, cancellationToken).ConfigureAwait(false)) + { + return; + } - foreach (var documentId in newSourceGeneratedDocumentStates.GetChangedStateIds(oldSourceGeneratedDocumentStates, ignoreUnchangedContent: true)) - { - var newState = newSourceGeneratedDocumentStates.GetRequiredState(documentId); - if (newState.Attributes.DesignTimeOnly) - { - continue; - } + cancellationToken.ThrowIfCancellationRequested(); - changedOrAddedDocuments.Add(newProject.GetOrCreateSourceGeneratedDocument(newState)); - } + var oldSourceGeneratedDocumentStates = await oldProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(oldProject.State, cancellationToken).ConfigureAwait(false); - foreach (var documentId in newSourceGeneratedDocumentStates.GetAddedStateIds(oldSourceGeneratedDocumentStates)) - { - var newState = newSourceGeneratedDocumentStates.GetRequiredState(documentId); - if (newState.Attributes.DesignTimeOnly) - { - continue; - } + cancellationToken.ThrowIfCancellationRequested(); - changedOrAddedDocuments.Add(newProject.GetOrCreateSourceGeneratedDocument(newState)); - } - } + var newSourceGeneratedDocumentStates = await newProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(newProject.State, cancellationToken).ConfigureAwait(false); - /// - /// Enumerates s of changed (not added or removed) s (not additional nor analyzer config). - /// - internal static async IAsyncEnumerable GetChangedDocumentsAsync(Project oldProject, Project newProject, [EnumeratorCancellation] CancellationToken cancellationToken) + foreach (var documentId in newSourceGeneratedDocumentStates.GetChangedStateIds(oldSourceGeneratedDocumentStates, ignoreUnchangedContent: true)) { - Debug.Assert(oldProject.Id == newProject.Id); - - if (!newProject.SupportsEditAndContinue() || oldProject.State == newProject.State) + var newState = newSourceGeneratedDocumentStates.GetRequiredState(documentId); + if (newState.Attributes.DesignTimeOnly) { - yield break; + continue; } - foreach (var documentId in newProject.State.DocumentStates.GetChangedStateIds(oldProject.State.DocumentStates, ignoreUnchangedContent: true)) - { - yield return documentId; - } + changedOrAddedDocuments.Add(newProject.GetOrCreateSourceGeneratedDocument(newState)); + } - // Given the following assumptions: - // - source generators are deterministic, - // - metadata references and compilation options have not changed (TODO -- need to check), - // - source documents have not changed, - // - additional documents have not changed, - // - analyzer config documents have not changed, - // the outputs of source generators will not change. - - if (!newProject.State.DocumentStates.HasAnyStateChanges(oldProject.State.DocumentStates) && - !newProject.State.AdditionalDocumentStates.HasAnyStateChanges(oldProject.State.AdditionalDocumentStates) && - !newProject.State.AnalyzerConfigDocumentStates.HasAnyStateChanges(oldProject.State.AnalyzerConfigDocumentStates)) + foreach (var documentId in newSourceGeneratedDocumentStates.GetAddedStateIds(oldSourceGeneratedDocumentStates)) + { + var newState = newSourceGeneratedDocumentStates.GetRequiredState(documentId); + if (newState.Attributes.DesignTimeOnly) { - // Based on the above assumption there are no changes in source generated files. - yield break; + continue; } - cancellationToken.ThrowIfCancellationRequested(); - - var oldSourceGeneratedDocumentStates = await oldProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(oldProject.State, cancellationToken).ConfigureAwait(false); + changedOrAddedDocuments.Add(newProject.GetOrCreateSourceGeneratedDocument(newState)); + } + } - cancellationToken.ThrowIfCancellationRequested(); + /// + /// Enumerates s of changed (not added or removed) s (not additional nor analyzer config). + /// + internal static async IAsyncEnumerable GetChangedDocumentsAsync(Project oldProject, Project newProject, [EnumeratorCancellation] CancellationToken cancellationToken) + { + Debug.Assert(oldProject.Id == newProject.Id); - var newSourceGeneratedDocumentStates = await newProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(newProject.State, cancellationToken).ConfigureAwait(false); + if (!newProject.SupportsEditAndContinue() || oldProject.State == newProject.State) + { + yield break; + } - foreach (var documentId in newSourceGeneratedDocumentStates.GetChangedStateIds(oldSourceGeneratedDocumentStates, ignoreUnchangedContent: true)) - { - yield return documentId; - } + foreach (var documentId in newProject.State.DocumentStates.GetChangedStateIds(oldProject.State.DocumentStates, ignoreUnchangedContent: true)) + { + yield return documentId; } - private async Task<(ImmutableArray results, ImmutableArray diagnostics)> AnalyzeDocumentsAsync( - ArrayBuilder changedOrAddedDocuments, - ActiveStatementSpanProvider newDocumentActiveStatementSpanProvider, - CancellationToken cancellationToken) + // Given the following assumptions: + // - source generators are deterministic, + // - metadata references and compilation options have not changed (TODO -- need to check), + // - source documents have not changed, + // - additional documents have not changed, + // - analyzer config documents have not changed, + // the outputs of source generators will not change. + + if (!newProject.State.DocumentStates.HasAnyStateChanges(oldProject.State.DocumentStates) && + !newProject.State.AdditionalDocumentStates.HasAnyStateChanges(oldProject.State.AdditionalDocumentStates) && + !newProject.State.AnalyzerConfigDocumentStates.HasAnyStateChanges(oldProject.State.AnalyzerConfigDocumentStates)) { - using var _1 = ArrayBuilder.GetInstance(out var documentDiagnostics); - using var _2 = ArrayBuilder<(Document? oldDocument, Document newDocument)>.GetInstance(out var documents); + // Based on the above assumption there are no changes in source generated files. + yield break; + } - foreach (var newDocument in changedOrAddedDocuments) - { - var (oldDocument, oldDocumentState) = await DebuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(newDocument.Id, newDocument, cancellationToken, reloadOutOfSyncDocument: true).ConfigureAwait(false); - switch (oldDocumentState) - { - case CommittedSolution.DocumentState.DesignTimeOnly: - break; - - case CommittedSolution.DocumentState.Indeterminate: - case CommittedSolution.DocumentState.OutOfSync: - var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor((oldDocumentState == CommittedSolution.DocumentState.Indeterminate) ? - EditAndContinueErrorCode.UnableToReadSourceFileOrPdb : EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee); - documentDiagnostics.Add(Diagnostic.Create(descriptor, Location.Create(newDocument.FilePath!, textSpan: default, lineSpan: default), new[] { newDocument.FilePath })); - break; - - case CommittedSolution.DocumentState.MatchesBuildOutput: - // Include the document regardless of whether the module it was built into has been loaded or not. - // If the module has been built it might get loaded later during the debugging session, - // at which point we apply all changes that have been made to the project so far. - - documents.Add((oldDocument, newDocument)); - break; - - default: - throw ExceptionUtilities.UnexpectedValue(oldDocumentState); - } - } + cancellationToken.ThrowIfCancellationRequested(); - var analyses = await Analyses.GetDocumentAnalysesAsync(DebuggingSession.LastCommittedSolution, documents, newDocumentActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false); - return (analyses, documentDiagnostics.ToImmutable()); - } + var oldSourceGeneratedDocumentStates = await oldProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(oldProject.State, cancellationToken).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); - internal ImmutableArray GetDocumentsWithReportedDiagnostics() + var newSourceGeneratedDocumentStates = await newProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(newProject.State, cancellationToken).ConfigureAwait(false); + + foreach (var documentId in newSourceGeneratedDocumentStates.GetChangedStateIds(oldSourceGeneratedDocumentStates, ignoreUnchangedContent: true)) { - lock (_documentsWithReportedDiagnosticsGuard) - { - return ImmutableArray.CreateRange(_documentsWithReportedDiagnostics); - } + yield return documentId; } + } + + private async Task<(ImmutableArray results, ImmutableArray diagnostics)> AnalyzeDocumentsAsync( + ArrayBuilder changedOrAddedDocuments, + ActiveStatementSpanProvider newDocumentActiveStatementSpanProvider, + CancellationToken cancellationToken) + { + using var _1 = ArrayBuilder.GetInstance(out var documentDiagnostics); + using var _2 = ArrayBuilder<(Document? oldDocument, Document newDocument)>.GetInstance(out var documents); - internal void TrackDocumentWithReportedDiagnostics(DocumentId documentId) + foreach (var newDocument in changedOrAddedDocuments) { - lock (_documentsWithReportedDiagnosticsGuard) + var (oldDocument, oldDocumentState) = await DebuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(newDocument.Id, newDocument, cancellationToken, reloadOutOfSyncDocument: true).ConfigureAwait(false); + switch (oldDocumentState) { - _documentsWithReportedDiagnostics.Add(documentId); + case CommittedSolution.DocumentState.DesignTimeOnly: + break; + + case CommittedSolution.DocumentState.Indeterminate: + case CommittedSolution.DocumentState.OutOfSync: + var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor((oldDocumentState == CommittedSolution.DocumentState.Indeterminate) ? + EditAndContinueErrorCode.UnableToReadSourceFileOrPdb : EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee); + documentDiagnostics.Add(Diagnostic.Create(descriptor, Location.Create(newDocument.FilePath!, textSpan: default, lineSpan: default), new[] { newDocument.FilePath })); + break; + + case CommittedSolution.DocumentState.MatchesBuildOutput: + // Include the document regardless of whether the module it was built into has been loaded or not. + // If the module has been built it might get loaded later during the debugging session, + // at which point we apply all changes that have been made to the project so far. + + documents.Add((oldDocument, newDocument)); + break; + + default: + throw ExceptionUtilities.UnexpectedValue(oldDocumentState); } } - private static ProjectAnalysisSummary GetProjectAnalysisSummary(ImmutableArray documentAnalyses) - { - var hasChanges = false; - var hasSignificantValidChanges = false; + var analyses = await Analyses.GetDocumentAnalysesAsync(DebuggingSession.LastCommittedSolution, documents, newDocumentActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false); + return (analyses, documentDiagnostics.ToImmutable()); + } - foreach (var analysis in documentAnalyses) - { - // skip documents that actually were not changed: - if (!analysis.HasChanges) - { - continue; - } + internal ImmutableArray GetDocumentsWithReportedDiagnostics() + { + lock (_documentsWithReportedDiagnosticsGuard) + { + return ImmutableArray.CreateRange(_documentsWithReportedDiagnostics); + } + } - // rude edit detection wasn't completed due to errors that prevent us from analyzing the document: - if (analysis.HasChangesAndSyntaxErrors) - { - return ProjectAnalysisSummary.CompilationErrors; - } + internal void TrackDocumentWithReportedDiagnostics(DocumentId documentId) + { + lock (_documentsWithReportedDiagnosticsGuard) + { + _documentsWithReportedDiagnostics.Add(documentId); + } + } - // rude edits detected: - if (!analysis.RudeEditErrors.IsEmpty) - { - return ProjectAnalysisSummary.RudeEdits; - } + private static ProjectAnalysisSummary GetProjectAnalysisSummary(ImmutableArray documentAnalyses) + { + var hasChanges = false; + var hasSignificantValidChanges = false; - hasChanges = true; - hasSignificantValidChanges |= analysis.HasSignificantValidChanges; + foreach (var analysis in documentAnalyses) + { + // skip documents that actually were not changed: + if (!analysis.HasChanges) + { + continue; } - if (!hasChanges) + // rude edit detection wasn't completed due to errors that prevent us from analyzing the document: + if (analysis.HasChangesAndSyntaxErrors) { - // we get here if a document is closed and reopen without any actual change: - return ProjectAnalysisSummary.NoChanges; + return ProjectAnalysisSummary.CompilationErrors; } - if (!hasSignificantValidChanges) + // rude edits detected: + if (!analysis.RudeEditErrors.IsEmpty) { - return ProjectAnalysisSummary.ValidInsignificantChanges; + return ProjectAnalysisSummary.RudeEdits; } - return ProjectAnalysisSummary.ValidChanges; + hasChanges = true; + hasSignificantValidChanges |= analysis.HasSignificantValidChanges; } - internal static async ValueTask GetProjectChangesAsync( - ActiveStatementsMap baseActiveStatements, - Compilation oldCompilation, - Compilation newCompilation, - Project oldProject, - Project newProject, - ImmutableArray changedDocumentAnalyses, - CancellationToken cancellationToken) + if (!hasChanges) { - try - { - using var _1 = ArrayBuilder.GetInstance(out var allEdits); - using var _2 = ArrayBuilder.GetInstance(out var allLineEdits); - using var _3 = ArrayBuilder.GetInstance(out var activeStatementsInChangedDocuments); + // we get here if a document is closed and reopen without any actual change: + return ProjectAnalysisSummary.NoChanges; + } + + if (!hasSignificantValidChanges) + { + return ProjectAnalysisSummary.ValidInsignificantChanges; + } - var analyzer = newProject.Services.GetRequiredService(); - var requiredCapabilities = EditAndContinueCapabilities.None; + return ProjectAnalysisSummary.ValidChanges; + } - foreach (var analysis in changedDocumentAnalyses) + internal static async ValueTask GetProjectChangesAsync( + ActiveStatementsMap baseActiveStatements, + Compilation oldCompilation, + Compilation newCompilation, + Project oldProject, + Project newProject, + ImmutableArray changedDocumentAnalyses, + CancellationToken cancellationToken) + { + try + { + using var _1 = ArrayBuilder.GetInstance(out var allEdits); + using var _2 = ArrayBuilder.GetInstance(out var allLineEdits); + using var _3 = ArrayBuilder.GetInstance(out var activeStatementsInChangedDocuments); + + var analyzer = newProject.Services.GetRequiredService(); + var requiredCapabilities = EditAndContinueCapabilities.None; + + foreach (var analysis in changedDocumentAnalyses) + { + if (!analysis.HasSignificantValidChanges) { - if (!analysis.HasSignificantValidChanges) - { - continue; - } + continue; + } - // we shouldn't be asking for deltas in presence of errors: - Contract.ThrowIfTrue(analysis.HasChangesAndErrors); + // we shouldn't be asking for deltas in presence of errors: + Contract.ThrowIfTrue(analysis.HasChangesAndErrors); - // Active statements are calculated if document changed and has no syntax errors: - Contract.ThrowIfTrue(analysis.ActiveStatements.IsDefault); + // Active statements are calculated if document changed and has no syntax errors: + Contract.ThrowIfTrue(analysis.ActiveStatements.IsDefault); - allEdits.AddRange(analysis.SemanticEdits); - allLineEdits.AddRange(analysis.LineEdits); - requiredCapabilities |= analysis.RequiredCapabilities; + allEdits.AddRange(analysis.SemanticEdits); + allLineEdits.AddRange(analysis.LineEdits); + requiredCapabilities |= analysis.RequiredCapabilities; - if (analysis.ActiveStatements.Length > 0) - { - var oldDocument = await oldProject.GetDocumentAsync(analysis.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + if (analysis.ActiveStatements.Length > 0) + { + var oldDocument = await oldProject.GetDocumentAsync(analysis.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - var oldActiveStatements = (oldDocument == null) ? [] : - await baseActiveStatements.GetOldActiveStatementsAsync(analyzer, oldDocument, cancellationToken).ConfigureAwait(false); + var oldActiveStatements = (oldDocument == null) ? [] : + await baseActiveStatements.GetOldActiveStatementsAsync(analyzer, oldDocument, cancellationToken).ConfigureAwait(false); - activeStatementsInChangedDocuments.Add(new(oldActiveStatements, analysis.ActiveStatements, analysis.ExceptionRegions)); - } + activeStatementsInChangedDocuments.Add(new(oldActiveStatements, analysis.ActiveStatements, analysis.ExceptionRegions)); } + } + + MergePartialEdits(oldCompilation, newCompilation, allEdits, out var mergedEdits, out var addedSymbols, cancellationToken); + + return new ProjectChanges( + mergedEdits, + allLineEdits.ToImmutable(), + addedSymbols, + activeStatementsInChangedDocuments.ToImmutable(), + requiredCapabilities); + } + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) + { + throw ExceptionUtilities.Unreachable(); + } + } - MergePartialEdits(oldCompilation, newCompilation, allEdits, out var mergedEdits, out var addedSymbols, cancellationToken); + internal static void MergePartialEdits( + Compilation oldCompilation, + Compilation newCompilation, + IReadOnlyList edits, + out ImmutableArray mergedEdits, + out ImmutableHashSet addedSymbols, + CancellationToken cancellationToken) + { + using var _0 = ArrayBuilder.GetInstance(edits.Count, out var mergedEditsBuilder); + using var _1 = PooledHashSet.GetInstance(out var addedSymbolsBuilder); + using var _2 = ArrayBuilder<(ISymbol? oldSymbol, ISymbol? newSymbol)>.GetInstance(edits.Count, out var resolvedSymbols); - return new ProjectChanges( - mergedEdits, - allLineEdits.ToImmutable(), - addedSymbols, - activeStatementsInChangedDocuments.ToImmutable(), - requiredCapabilities); + foreach (var edit in edits) + { + SymbolKeyResolution oldResolution; + if (edit.Kind is SemanticEditKind.Update or SemanticEditKind.Delete) + { + oldResolution = edit.Symbol.Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken); + Contract.ThrowIfNull(oldResolution.Symbol); + } + else + { + oldResolution = default; + } + + SymbolKeyResolution newResolution; + if (edit.Kind is SemanticEditKind.Update or SemanticEditKind.Insert or SemanticEditKind.Replace) + { + newResolution = edit.Symbol.Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken); + Contract.ThrowIfNull(newResolution.Symbol); } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) + else if (edit.Kind == SemanticEditKind.Delete && edit.DeletedSymbolContainer is not null) { - throw ExceptionUtilities.Unreachable(); + // For deletes, we use NewSymbol to reference the containing type of the deleted member + newResolution = edit.DeletedSymbolContainer.Value.Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken); + Contract.ThrowIfNull(newResolution.Symbol); } + else + { + newResolution = default; + } + + resolvedSymbols.Add((oldResolution.Symbol, newResolution.Symbol)); } - internal static void MergePartialEdits( - Compilation oldCompilation, - Compilation newCompilation, - IReadOnlyList edits, - out ImmutableArray mergedEdits, - out ImmutableHashSet addedSymbols, - CancellationToken cancellationToken) + for (var i = 0; i < edits.Count; i++) { - using var _0 = ArrayBuilder.GetInstance(edits.Count, out var mergedEditsBuilder); - using var _1 = PooledHashSet.GetInstance(out var addedSymbolsBuilder); - using var _2 = ArrayBuilder<(ISymbol? oldSymbol, ISymbol? newSymbol)>.GetInstance(edits.Count, out var resolvedSymbols); + var edit = edits[i]; - foreach (var edit in edits) + if (edit.PartialType == null) { - SymbolKeyResolution oldResolution; - if (edit.Kind is SemanticEditKind.Update or SemanticEditKind.Delete) - { - oldResolution = edit.Symbol.Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken); - Contract.ThrowIfNull(oldResolution.Symbol); - } - else - { - oldResolution = default; - } + var (oldSymbol, newSymbol) = resolvedSymbols[i]; - SymbolKeyResolution newResolution; - if (edit.Kind is SemanticEditKind.Update or SemanticEditKind.Insert or SemanticEditKind.Replace) - { - newResolution = edit.Symbol.Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken); - Contract.ThrowIfNull(newResolution.Symbol); - } - else if (edit.Kind == SemanticEditKind.Delete && edit.DeletedSymbolContainer is not null) + if (edit.Kind == SemanticEditKind.Insert) { - // For deletes, we use NewSymbol to reference the containing type of the deleted member - newResolution = edit.DeletedSymbolContainer.Value.Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken); - Contract.ThrowIfNull(newResolution.Symbol); - } - else - { - newResolution = default; + Contract.ThrowIfNull(newSymbol); + addedSymbolsBuilder.Add(newSymbol); } - resolvedSymbols.Add((oldResolution.Symbol, newResolution.Symbol)); + mergedEditsBuilder.Add(new SemanticEdit( + edit.Kind, + oldSymbol: oldSymbol, + newSymbol: newSymbol, + syntaxMap: edit.SyntaxMaps.MatchingNodes, + runtimeRudeEdit: edit.SyntaxMaps.RuntimeRudeEdits)); } + } - for (var i = 0; i < edits.Count; i++) - { - var edit = edits[i]; - - if (edit.PartialType == null) - { - var (oldSymbol, newSymbol) = resolvedSymbols[i]; - - if (edit.Kind == SemanticEditKind.Insert) - { - Contract.ThrowIfNull(newSymbol); - addedSymbolsBuilder.Add(newSymbol); - } - - mergedEditsBuilder.Add(new SemanticEdit( - edit.Kind, - oldSymbol: oldSymbol, - newSymbol: newSymbol, - syntaxMap: edit.SyntaxMaps.MatchingNodes, - runtimeRudeEdit: edit.SyntaxMaps.RuntimeRudeEdits)); - } - } + // no partial type merging needed: + if (edits.Count == mergedEditsBuilder.Count) + { + mergedEdits = mergedEditsBuilder.ToImmutable(); + addedSymbols = addedSymbolsBuilder.ToImmutableHashSet(); + return; + } - // no partial type merging needed: - if (edits.Count == mergedEditsBuilder.Count) - { - mergedEdits = mergedEditsBuilder.ToImmutable(); - addedSymbols = addedSymbolsBuilder.ToImmutableHashSet(); - return; - } + // Calculate merged syntax map for each partial type symbol: - // Calculate merged syntax map for each partial type symbol: + var symbolKeyComparer = SymbolKey.GetComparer(ignoreCase: false, ignoreAssemblyKeys: true); + var mergedUpdateEditSyntaxMaps = new Dictionary? matchingNodes, Func? runtimeRudeEdits)>(symbolKeyComparer); - var symbolKeyComparer = SymbolKey.GetComparer(ignoreCase: false, ignoreAssemblyKeys: true); - var mergedUpdateEditSyntaxMaps = new Dictionary? matchingNodes, Func? runtimeRudeEdits)>(symbolKeyComparer); + var updatesByPartialType = edits + .Where(edit => edit is { PartialType: not null, Kind: SemanticEditKind.Update }) + .GroupBy(edit => edit.PartialType!.Value, symbolKeyComparer); - var updatesByPartialType = edits - .Where(edit => edit is { PartialType: not null, Kind: SemanticEditKind.Update }) - .GroupBy(edit => edit.PartialType!.Value, symbolKeyComparer); + foreach (var partialTypeEdits in updatesByPartialType) + { + Func? mergedMatchingNodes; + Func? mergedRuntimeRudeEdits; - foreach (var partialTypeEdits in updatesByPartialType) + if (partialTypeEdits.Any(static e => e.SyntaxMaps.HasMap)) { - Func? mergedMatchingNodes; - Func? mergedRuntimeRudeEdits; + var newMaps = partialTypeEdits.Where(static edit => edit.SyntaxMaps.HasMap).SelectAsArray(static edit => edit.SyntaxMaps); - if (partialTypeEdits.Any(static e => e.SyntaxMaps.HasMap)) - { - var newMaps = partialTypeEdits.Where(static edit => edit.SyntaxMaps.HasMap).SelectAsArray(static edit => edit.SyntaxMaps); + mergedMatchingNodes = node => newMaps[newMaps.IndexOf(static (m, node) => m.NewTree == node.SyntaxTree, node)].MatchingNodes!(node); + mergedRuntimeRudeEdits = node => newMaps[newMaps.IndexOf(static (m, node) => m.NewTree == node.SyntaxTree, node)].RuntimeRudeEdits?.Invoke(node); + } + else + { + mergedMatchingNodes = null; + mergedRuntimeRudeEdits = null; + } - mergedMatchingNodes = node => newMaps[newMaps.IndexOf(static (m, node) => m.NewTree == node.SyntaxTree, node)].MatchingNodes!(node); - mergedRuntimeRudeEdits = node => newMaps[newMaps.IndexOf(static (m, node) => m.NewTree == node.SyntaxTree, node)].RuntimeRudeEdits?.Invoke(node); - } - else - { - mergedMatchingNodes = null; - mergedRuntimeRudeEdits = null; - } + mergedUpdateEditSyntaxMaps.Add(partialTypeEdits.Key, (mergedMatchingNodes, mergedRuntimeRudeEdits)); + } - mergedUpdateEditSyntaxMaps.Add(partialTypeEdits.Key, (mergedMatchingNodes, mergedRuntimeRudeEdits)); - } + // Deduplicate edits based on their target symbol and use merged syntax map calculated above for a given partial type. - // Deduplicate edits based on their target symbol and use merged syntax map calculated above for a given partial type. + using var _3 = PooledHashSet.GetInstance(out var visitedSymbols); - using var _3 = PooledHashSet.GetInstance(out var visitedSymbols); + for (var i = 0; i < edits.Count; i++) + { + var edit = edits[i]; - for (var i = 0; i < edits.Count; i++) + if (edit.PartialType != null) { - var edit = edits[i]; - - if (edit.PartialType != null) + var (oldSymbol, newSymbol) = resolvedSymbols[i]; + if (visitedSymbols.Add(newSymbol ?? oldSymbol!)) { - var (oldSymbol, newSymbol) = resolvedSymbols[i]; - if (visitedSymbols.Add(newSymbol ?? oldSymbol!)) - { - var syntaxMaps = (edit.Kind == SemanticEditKind.Update) ? mergedUpdateEditSyntaxMaps[edit.PartialType.Value] : default; - mergedEditsBuilder.Add(new SemanticEdit(edit.Kind, oldSymbol, newSymbol, syntaxMaps.matchingNodes, syntaxMaps.runtimeRudeEdits)); - } + var syntaxMaps = (edit.Kind == SemanticEditKind.Update) ? mergedUpdateEditSyntaxMaps[edit.PartialType.Value] : default; + mergedEditsBuilder.Add(new SemanticEdit(edit.Kind, oldSymbol, newSymbol, syntaxMaps.matchingNodes, syntaxMaps.runtimeRudeEdits)); } } - - mergedEdits = mergedEditsBuilder.ToImmutable(); - addedSymbols = addedSymbolsBuilder.ToImmutableHashSet(); } - public async ValueTask EmitSolutionUpdateAsync(Solution solution, ActiveStatementSpanProvider solutionActiveStatementSpanProvider, UpdateId updateId, CancellationToken cancellationToken) + mergedEdits = mergedEditsBuilder.ToImmutable(); + addedSymbols = addedSymbolsBuilder.ToImmutableHashSet(); + } + + public async ValueTask EmitSolutionUpdateAsync(Solution solution, ActiveStatementSpanProvider solutionActiveStatementSpanProvider, UpdateId updateId, CancellationToken cancellationToken) + { + var log = EditAndContinueService.Log; + + try { - var log = EditAndContinueService.Log; + log.Write("EmitSolutionUpdate {0}.{1}: '{2}'", updateId.SessionId.Ordinal, updateId.Ordinal, solution.FilePath); - try + using var _1 = ArrayBuilder.GetInstance(out var deltas); + using var _2 = ArrayBuilder<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)>.GetInstance(out var nonRemappableRegions); + using var _3 = ArrayBuilder.GetInstance(out var newProjectBaselines); + using var _4 = ArrayBuilder.GetInstance(out var diagnostics); + using var _5 = ArrayBuilder.GetInstance(out var changedOrAddedDocuments); + using var _6 = ArrayBuilder<(DocumentId, ImmutableArray)>.GetInstance(out var documentsWithRudeEdits); + Diagnostic? syntaxError = null; + + var oldSolution = DebuggingSession.LastCommittedSolution; + + var isBlocked = false; + var hasEmitErrors = false; + foreach (var newProject in solution.Projects) { - log.Write("EmitSolutionUpdate {0}.{1}: '{2}'", updateId.SessionId.Ordinal, updateId.Ordinal, solution.FilePath); + var oldProject = oldSolution.GetProject(newProject.Id); + if (oldProject == null) + { + log.Write("EnC state of '{0}' queried: project not loaded", newProject.Id); - using var _1 = ArrayBuilder.GetInstance(out var deltas); - using var _2 = ArrayBuilder<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)>.GetInstance(out var nonRemappableRegions); - using var _3 = ArrayBuilder.GetInstance(out var newProjectBaselines); - using var _4 = ArrayBuilder.GetInstance(out var diagnostics); - using var _5 = ArrayBuilder.GetInstance(out var changedOrAddedDocuments); - using var _6 = ArrayBuilder<(DocumentId, ImmutableArray)>.GetInstance(out var documentsWithRudeEdits); - Diagnostic? syntaxError = null; + // TODO (https://github.com/dotnet/roslyn/issues/1204): + // + // When debugging session is started some projects might not have been loaded to the workspace yet (may be explicitly unloaded by the user). + // We capture the base solution. Edits in files that are in projects that haven't been loaded won't be applied + // and will result in source mismatch when the user steps into them. + // + // We can allow project to be added by including all its documents here. + // When we analyze these documents later on we'll check if they match the PDB. + // If so we can add them to the committed solution and detect further changes. + // It might be more efficient though to track added projects separately. - var oldSolution = DebuggingSession.LastCommittedSolution; + continue; + } - var isBlocked = false; - var hasEmitErrors = false; - foreach (var newProject in solution.Projects) + await PopulateChangedAndAddedDocumentsAsync(oldProject, newProject, changedOrAddedDocuments, cancellationToken).ConfigureAwait(false); + if (changedOrAddedDocuments.IsEmpty()) { - var oldProject = oldSolution.GetProject(newProject.Id); - if (oldProject == null) - { - log.Write("EnC state of '{0}' queried: project not loaded", newProject.Id); - - // TODO (https://github.com/dotnet/roslyn/issues/1204): - // - // When debugging session is started some projects might not have been loaded to the workspace yet (may be explicitly unloaded by the user). - // We capture the base solution. Edits in files that are in projects that haven't been loaded won't be applied - // and will result in source mismatch when the user steps into them. - // - // We can allow project to be added by including all its documents here. - // When we analyze these documents later on we'll check if they match the PDB. - // If so we can add them to the committed solution and detect further changes. - // It might be more efficient though to track added projects separately. - - continue; - } + continue; + } - await PopulateChangedAndAddedDocumentsAsync(oldProject, newProject, changedOrAddedDocuments, cancellationToken).ConfigureAwait(false); - if (changedOrAddedDocuments.IsEmpty()) - { - continue; - } + log.Write("Found {0} potentially changed document(s) in project '{1}'", changedOrAddedDocuments.Count, newProject.Id); - log.Write("Found {0} potentially changed document(s) in project '{1}'", changedOrAddedDocuments.Count, newProject.Id); + var (mvid, mvidReadError) = await DebuggingSession.GetProjectModuleIdAsync(newProject, cancellationToken).ConfigureAwait(false); + if (mvidReadError != null) + { + // The error hasn't been reported by GetDocumentDiagnosticsAsync since it might have been intermittent. + // The MVID is required for emit so we consider the error permanent and report it here. + // Bail before analyzing documents as the analysis needs to read the PDB which will likely fail if we can't even read the MVID. + diagnostics.Add(new(newProject.Id, [mvidReadError])); - var (mvid, mvidReadError) = await DebuggingSession.GetProjectModuleIdAsync(newProject, cancellationToken).ConfigureAwait(false); - if (mvidReadError != null) - { - // The error hasn't been reported by GetDocumentDiagnosticsAsync since it might have been intermittent. - // The MVID is required for emit so we consider the error permanent and report it here. - // Bail before analyzing documents as the analysis needs to read the PDB which will likely fail if we can't even read the MVID. - diagnostics.Add(new(newProject.Id, [mvidReadError])); + Telemetry.LogProjectAnalysisSummary(ProjectAnalysisSummary.ValidChanges, newProject.State.ProjectInfo.Attributes.TelemetryId, ImmutableArray.Create(mvidReadError.Descriptor.Id)); + isBlocked = true; + continue; + } - Telemetry.LogProjectAnalysisSummary(ProjectAnalysisSummary.ValidChanges, newProject.State.ProjectInfo.Attributes.TelemetryId, ImmutableArray.Create(mvidReadError.Descriptor.Id)); - isBlocked = true; - continue; - } + if (mvid == Guid.Empty) + { + log.Write("Emitting update of '{0}': project not built", newProject.Id); + continue; + } - if (mvid == Guid.Empty) - { - log.Write("Emitting update of '{0}': project not built", newProject.Id); - continue; - } + // Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync. + // Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by + // incoming events updating the content of out-of-sync documents. + // + // If in past we concluded that a document is out-of-sync, attempt to check one more time before we block apply. + // The source file content might have been updated since the last time we checked. + // + // TODO (investigate): https://github.com/dotnet/roslyn/issues/38866 + // It is possible that the result of Rude Edit semantic analysis of an unchanged document will change if there + // another document is updated. If we encounter a significant case of this we should consider caching such a result per project, + // rather then per document. Also, we might be observing an older semantics if the document that is causing the change is out-of-sync -- + // e.g. the binary was built with an overload C.M(object), but a generator updated class C to also contain C.M(string), + // which change we have not observed yet. Then call-sites of C.M in a changed document observed by the analysis will be seen as C.M(object) + // instead of the true C.M(string). + + var (changedDocumentAnalyses, documentDiagnostics) = await AnalyzeDocumentsAsync(changedOrAddedDocuments, solutionActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false); + if (documentDiagnostics.Any()) + { + // The diagnostic hasn't been reported by GetDocumentDiagnosticsAsync since out-of-sync documents are likely to be synchronized + // before the changes are attempted to be applied. If we still have any out-of-sync documents we report warnings and ignore changes in them. + // If in future the file is updated so that its content matches the PDB checksum, the document transitions to a matching state, + // and we consider any further changes to it for application. + diagnostics.Add(new(newProject.Id, documentDiagnostics)); + } - // Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync. - // Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by - // incoming events updating the content of out-of-sync documents. - // - // If in past we concluded that a document is out-of-sync, attempt to check one more time before we block apply. - // The source file content might have been updated since the last time we checked. - // - // TODO (investigate): https://github.com/dotnet/roslyn/issues/38866 - // It is possible that the result of Rude Edit semantic analysis of an unchanged document will change if there - // another document is updated. If we encounter a significant case of this we should consider caching such a result per project, - // rather then per document. Also, we might be observing an older semantics if the document that is causing the change is out-of-sync -- - // e.g. the binary was built with an overload C.M(object), but a generator updated class C to also contain C.M(string), - // which change we have not observed yet. Then call-sites of C.M in a changed document observed by the analysis will be seen as C.M(object) - // instead of the true C.M(string). - - var (changedDocumentAnalyses, documentDiagnostics) = await AnalyzeDocumentsAsync(changedOrAddedDocuments, solutionActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false); - if (documentDiagnostics.Any()) + foreach (var changedDocumentAnalysis in changedDocumentAnalyses) + { + if (changedDocumentAnalysis.HasChanges) { - // The diagnostic hasn't been reported by GetDocumentDiagnosticsAsync since out-of-sync documents are likely to be synchronized - // before the changes are attempted to be applied. If we still have any out-of-sync documents we report warnings and ignore changes in them. - // If in future the file is updated so that its content matches the PDB checksum, the document transitions to a matching state, - // and we consider any further changes to it for application. - diagnostics.Add(new(newProject.Id, documentDiagnostics)); + log.Write("Document changed, added, or deleted: '{0}'", changedDocumentAnalysis.FilePath); } - foreach (var changedDocumentAnalysis in changedDocumentAnalyses) - { - if (changedDocumentAnalysis.HasChanges) - { - log.Write("Document changed, added, or deleted: '{0}'", changedDocumentAnalysis.FilePath); - } - - Telemetry.LogAnalysisTime(changedDocumentAnalysis.ElapsedTime); - } + Telemetry.LogAnalysisTime(changedDocumentAnalysis.ElapsedTime); + } - var projectSummary = GetProjectAnalysisSummary(changedDocumentAnalyses); - log.Write("Project summary for '{0}': {1}", newProject.Id, projectSummary); + var projectSummary = GetProjectAnalysisSummary(changedDocumentAnalyses); + log.Write("Project summary for '{0}': {1}", newProject.Id, projectSummary); - if (projectSummary == ProjectAnalysisSummary.NoChanges) - { - continue; - } + if (projectSummary == ProjectAnalysisSummary.NoChanges) + { + continue; + } - // The capability of a module to apply edits may change during edit session if the user attaches debugger to - // an additional process that doesn't support EnC (or detaches from such process). Before we apply edits - // we need to check with the debugger. - var (moduleDiagnostics, isModuleLoaded) = await GetModuleDiagnosticsAsync(mvid, oldProject, newProject, changedDocumentAnalyses, cancellationToken).ConfigureAwait(false); + // The capability of a module to apply edits may change during edit session if the user attaches debugger to + // an additional process that doesn't support EnC (or detaches from such process). Before we apply edits + // we need to check with the debugger. + var (moduleDiagnostics, isModuleLoaded) = await GetModuleDiagnosticsAsync(mvid, oldProject, newProject, changedDocumentAnalyses, cancellationToken).ConfigureAwait(false); - var isModuleEncBlocked = isModuleLoaded && !moduleDiagnostics.IsEmpty; - if (isModuleEncBlocked) - { - diagnostics.Add(new(newProject.Id, moduleDiagnostics)); - isBlocked = true; - } + var isModuleEncBlocked = isModuleLoaded && !moduleDiagnostics.IsEmpty; + if (isModuleEncBlocked) + { + diagnostics.Add(new(newProject.Id, moduleDiagnostics)); + isBlocked = true; + } - if (projectSummary == ProjectAnalysisSummary.CompilationErrors) - { - // only remember the first syntax error we encounter: - syntaxError ??= changedDocumentAnalyses.FirstOrDefault(a => a.SyntaxError != null)?.SyntaxError; - isBlocked = true; - } - else if (projectSummary == ProjectAnalysisSummary.RudeEdits) + if (projectSummary == ProjectAnalysisSummary.CompilationErrors) + { + // only remember the first syntax error we encounter: + syntaxError ??= changedDocumentAnalyses.FirstOrDefault(a => a.SyntaxError != null)?.SyntaxError; + isBlocked = true; + } + else if (projectSummary == ProjectAnalysisSummary.RudeEdits) + { + foreach (var analysis in changedDocumentAnalyses) { - foreach (var analysis in changedDocumentAnalyses) + if (analysis.RudeEditErrors.Length > 0) { - if (analysis.RudeEditErrors.Length > 0) - { - documentsWithRudeEdits.Add((analysis.DocumentId, analysis.RudeEditErrors)); - Telemetry.LogRudeEditDiagnostics(analysis.RudeEditErrors, newProject.State.Attributes.TelemetryId); - } + documentsWithRudeEdits.Add((analysis.DocumentId, analysis.RudeEditErrors)); + Telemetry.LogRudeEditDiagnostics(analysis.RudeEditErrors, newProject.State.Attributes.TelemetryId); } - - isBlocked = true; } - if (isModuleEncBlocked || projectSummary != ProjectAnalysisSummary.ValidChanges) - { - Telemetry.LogProjectAnalysisSummary(projectSummary, newProject.State.ProjectInfo.Attributes.TelemetryId, moduleDiagnostics.NullToEmpty().SelectAsArray(d => d.Descriptor.Id)); + isBlocked = true; + } - await LogDocumentChangesAsync(generation: null, cancellationToken).ConfigureAwait(false); - continue; - } + if (isModuleEncBlocked || projectSummary != ProjectAnalysisSummary.ValidChanges) + { + Telemetry.LogProjectAnalysisSummary(projectSummary, newProject.State.ProjectInfo.Attributes.TelemetryId, moduleDiagnostics.NullToEmpty().SelectAsArray(d => d.Descriptor.Id)); - var oldCompilation = await oldProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - Contract.ThrowIfNull(oldCompilation); + await LogDocumentChangesAsync(generation: null, cancellationToken).ConfigureAwait(false); + continue; + } - if (!DebuggingSession.TryGetOrCreateEmitBaseline(oldProject, oldCompilation, out var createBaselineDiagnostics, out var projectBaseline, out var baselineAccessLock)) - { - Debug.Assert(!createBaselineDiagnostics.IsEmpty); + var oldCompilation = await oldProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(oldCompilation); - // Report diagnosics even when the module is never going to be loaded (e.g. in multi-targeting scenario, where only one framework being debugged). - // This is consistent with reporting compilation errors - the IDE reports them for all TFMs regardless of what framework the app is running on. - diagnostics.Add(new(newProject.Id, createBaselineDiagnostics)); - Telemetry.LogProjectAnalysisSummary(projectSummary, newProject.State.ProjectInfo.Attributes.TelemetryId, createBaselineDiagnostics); - isBlocked = true; + if (!DebuggingSession.TryGetOrCreateEmitBaseline(oldProject, oldCompilation, out var createBaselineDiagnostics, out var projectBaseline, out var baselineAccessLock)) + { + Debug.Assert(!createBaselineDiagnostics.IsEmpty); - await LogDocumentChangesAsync(generation: null, cancellationToken).ConfigureAwait(false); - continue; - } + // Report diagnosics even when the module is never going to be loaded (e.g. in multi-targeting scenario, where only one framework being debugged). + // This is consistent with reporting compilation errors - the IDE reports them for all TFMs regardless of what framework the app is running on. + diagnostics.Add(new(newProject.Id, createBaselineDiagnostics)); + Telemetry.LogProjectAnalysisSummary(projectSummary, newProject.State.ProjectInfo.Attributes.TelemetryId, createBaselineDiagnostics); + isBlocked = true; - await LogDocumentChangesAsync(projectBaseline.Generation + 1, cancellationToken).ConfigureAwait(false); + await LogDocumentChangesAsync(generation: null, cancellationToken).ConfigureAwait(false); + continue; + } - async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cancellationToken) + await LogDocumentChangesAsync(projectBaseline.Generation + 1, cancellationToken).ConfigureAwait(false); + + async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cancellationToken) + { + var fileLog = log.FileLog; + if (fileLog != null) { - var fileLog = log.FileLog; - if (fileLog != null) + foreach (var changedDocumentAnalysis in changedDocumentAnalyses) { - foreach (var changedDocumentAnalysis in changedDocumentAnalyses) + if (changedDocumentAnalysis.HasChanges) { - if (changedDocumentAnalysis.HasChanges) - { - var oldDocument = await oldProject.GetDocumentAsync(changedDocumentAnalysis.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - var newDocument = await newProject.GetDocumentAsync(changedDocumentAnalysis.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - await fileLog.WriteDocumentChangeAsync(oldDocument, newDocument, updateId, generation, cancellationToken).ConfigureAwait(false); - } + var oldDocument = await oldProject.GetDocumentAsync(changedDocumentAnalysis.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + var newDocument = await newProject.GetDocumentAsync(changedDocumentAnalysis.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + await fileLog.WriteDocumentChangeAsync(oldDocument, newDocument, updateId, generation, cancellationToken).ConfigureAwait(false); } } } + } - log.Write("Emitting update of '{0}'", newProject.Id); + log.Write("Emitting update of '{0}'", newProject.Id); - var newCompilation = await newProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - Contract.ThrowIfNull(newCompilation); + var newCompilation = await newProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(newCompilation); - var oldActiveStatementsMap = await BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false); - var projectChanges = await GetProjectChangesAsync(oldActiveStatementsMap, oldCompilation, newCompilation, oldProject, newProject, changedDocumentAnalyses, cancellationToken).ConfigureAwait(false); + var oldActiveStatementsMap = await BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false); + var projectChanges = await GetProjectChangesAsync(oldActiveStatementsMap, oldCompilation, newCompilation, oldProject, newProject, changedDocumentAnalyses, cancellationToken).ConfigureAwait(false); - using var pdbStream = SerializableBytes.CreateWritableStream(); - using var metadataStream = SerializableBytes.CreateWritableStream(); - using var ilStream = SerializableBytes.CreateWritableStream(); + using var pdbStream = SerializableBytes.CreateWritableStream(); + using var metadataStream = SerializableBytes.CreateWritableStream(); + using var ilStream = SerializableBytes.CreateWritableStream(); - // project must support compilations since it supports EnC - Contract.ThrowIfNull(newCompilation); + // project must support compilations since it supports EnC + Contract.ThrowIfNull(newCompilation); - // The compiler only uses this predicate to determine if CS7101: "Member 'X' added during the current debug session - // can only be accessed from within its declaring assembly 'Lib'" should be reported. - // Prior to .NET 8 Preview 4 the runtime failed to apply such edits. - // This was fixed in Preview 4 along with support for generics. If we see a generic capability we can disable reporting - // this compiler error. Otherwise, we leave the check as is in order to detect at least some runtime failures on .NET Framework. - // Note that the analysis in the compiler detecting the circumstances under which the runtime fails - // to apply the change has both false positives (flagged generic updates that shouldn't be flagged) and negatives - // (didn't flag cases like https://github.com/dotnet/roslyn/issues/68293). - var capabilities = await Capabilities.GetValueAsync(cancellationToken).ConfigureAwait(false); - var isAddedSymbolPredicate = capabilities.HasFlag(EditAndContinueCapabilities.GenericAddMethodToExistingType) ? - static _ => false : (Func)projectChanges.AddedSymbols.Contains; + // The compiler only uses this predicate to determine if CS7101: "Member 'X' added during the current debug session + // can only be accessed from within its declaring assembly 'Lib'" should be reported. + // Prior to .NET 8 Preview 4 the runtime failed to apply such edits. + // This was fixed in Preview 4 along with support for generics. If we see a generic capability we can disable reporting + // this compiler error. Otherwise, we leave the check as is in order to detect at least some runtime failures on .NET Framework. + // Note that the analysis in the compiler detecting the circumstances under which the runtime fails + // to apply the change has both false positives (flagged generic updates that shouldn't be flagged) and negatives + // (didn't flag cases like https://github.com/dotnet/roslyn/issues/68293). + var capabilities = await Capabilities.GetValueAsync(cancellationToken).ConfigureAwait(false); + var isAddedSymbolPredicate = capabilities.HasFlag(EditAndContinueCapabilities.GenericAddMethodToExistingType) ? + static _ => false : (Func)projectChanges.AddedSymbols.Contains; - EmitDifferenceResult emitResult; + EmitDifferenceResult emitResult; - // The lock protects underlying baseline readers from being disposed while emitting delta. - // If the lock is disposed at this point the session has been incorrectly disposed while operations on it are in progress. - using (baselineAccessLock.DisposableRead()) - { - DebuggingSession.ThrowIfDisposed(); + // The lock protects underlying baseline readers from being disposed while emitting delta. + // If the lock is disposed at this point the session has been incorrectly disposed while operations on it are in progress. + using (baselineAccessLock.DisposableRead()) + { + DebuggingSession.ThrowIfDisposed(); - var emitDifferenceTimer = SharedStopwatch.StartNew(); + var emitDifferenceTimer = SharedStopwatch.StartNew(); - emitResult = newCompilation.EmitDifference( - projectBaseline.EmitBaseline, - projectChanges.SemanticEdits, - isAddedSymbolPredicate, - metadataStream, - ilStream, - pdbStream, - cancellationToken); + emitResult = newCompilation.EmitDifference( + projectBaseline.EmitBaseline, + projectChanges.SemanticEdits, + isAddedSymbolPredicate, + metadataStream, + ilStream, + pdbStream, + cancellationToken); - Telemetry.LogEmitDifferenceTime(emitDifferenceTimer.Elapsed); - } + Telemetry.LogEmitDifferenceTime(emitDifferenceTimer.Elapsed); + } - if (emitResult.Success) - { - Contract.ThrowIfNull(emitResult.Baseline); + if (emitResult.Success) + { + Contract.ThrowIfNull(emitResult.Baseline); - var unsupportedChangesDiagnostic = await GetUnsupportedChangesDiagnosticAsync(emitResult, cancellationToken).ConfigureAwait(false); - if (unsupportedChangesDiagnostic is not null) - { - diagnostics.Add(new(newProject.Id, [unsupportedChangesDiagnostic])); - isBlocked = true; - } - else - { - var updatedMethodTokens = emitResult.UpdatedMethods.SelectAsArray(h => MetadataTokens.GetToken(h)); - var changedTypeTokens = emitResult.ChangedTypes.SelectAsArray(h => MetadataTokens.GetToken(h)); - - // Determine all active statements whose span changed and exception region span deltas. - GetActiveStatementAndExceptionRegionSpans( - mvid, - oldActiveStatementsMap, - updatedMethodTokens, - NonRemappableRegions, - projectChanges.ActiveStatementChanges, - out var activeStatementsInUpdatedMethods, - out var moduleNonRemappableRegions, - out var exceptionRegionUpdates); - - var delta = new ManagedHotReloadUpdate( - mvid, - newCompilation.AssemblyName ?? newProject.Name, // used for display in debugger diagnostics - ilStream.ToImmutableArray(), - metadataStream.ToImmutableArray(), - pdbStream.ToImmutableArray(), - changedTypeTokens, - projectChanges.RequiredCapabilities.ToStringArray(), - updatedMethodTokens, - projectChanges.LineChanges, - activeStatementsInUpdatedMethods, - exceptionRegionUpdates); - - deltas.Add(delta); - - nonRemappableRegions.Add((mvid, moduleNonRemappableRegions)); - newProjectBaselines.Add(new ProjectBaseline(projectBaseline.ProjectId, emitResult.Baseline, projectBaseline.Generation + 1)); - - var fileLog = log.FileLog; - if (fileLog != null) - { - await LogDeltaFilesAsync(fileLog, delta, projectBaseline.Generation, oldProject, newProject, cancellationToken).ConfigureAwait(false); - } - } + var unsupportedChangesDiagnostic = await GetUnsupportedChangesDiagnosticAsync(emitResult, cancellationToken).ConfigureAwait(false); + if (unsupportedChangesDiagnostic is not null) + { + diagnostics.Add(new(newProject.Id, [unsupportedChangesDiagnostic])); + isBlocked = true; } else { - // error - isBlocked = hasEmitErrors = true; - } + var updatedMethodTokens = emitResult.UpdatedMethods.SelectAsArray(h => MetadataTokens.GetToken(h)); + var changedTypeTokens = emitResult.ChangedTypes.SelectAsArray(h => MetadataTokens.GetToken(h)); + + // Determine all active statements whose span changed and exception region span deltas. + GetActiveStatementAndExceptionRegionSpans( + mvid, + oldActiveStatementsMap, + updatedMethodTokens, + NonRemappableRegions, + projectChanges.ActiveStatementChanges, + out var activeStatementsInUpdatedMethods, + out var moduleNonRemappableRegions, + out var exceptionRegionUpdates); + + var delta = new ManagedHotReloadUpdate( + mvid, + newCompilation.AssemblyName ?? newProject.Name, // used for display in debugger diagnostics + ilStream.ToImmutableArray(), + metadataStream.ToImmutableArray(), + pdbStream.ToImmutableArray(), + changedTypeTokens, + projectChanges.RequiredCapabilities.ToStringArray(), + updatedMethodTokens, + projectChanges.LineChanges, + activeStatementsInUpdatedMethods, + exceptionRegionUpdates); + + deltas.Add(delta); + + nonRemappableRegions.Add((mvid, moduleNonRemappableRegions)); + newProjectBaselines.Add(new ProjectBaseline(projectBaseline.ProjectId, emitResult.Baseline, projectBaseline.Generation + 1)); - // TODO: https://github.com/dotnet/roslyn/issues/36061 - // We should only report diagnostics from emit phase. - // Syntax and semantic diagnostics are already reported by the diagnostic analyzer. - // Currently we do not have means to distinguish between diagnostics reported from compilation and emit phases. - // Querying diagnostics of the entire compilation or just the updated files migth be slow. - // In fact, it is desirable to allow emitting deltas for symbols affected by the change while allowing untouched - // method bodies to have errors. - if (!emitResult.Diagnostics.IsEmpty) - { - diagnostics.Add(new(newProject.Id, emitResult.Diagnostics)); + var fileLog = log.FileLog; + if (fileLog != null) + { + await LogDeltaFilesAsync(fileLog, delta, projectBaseline.Generation, oldProject, newProject, cancellationToken).ConfigureAwait(false); + } } - - Telemetry.LogProjectAnalysisSummary(projectSummary, newProject.State.ProjectInfo.Attributes.TelemetryId, emitResult.Diagnostics); + } + else + { + // error + isBlocked = hasEmitErrors = true; } - // log capabilities for edit sessions with changes or reported errors: - if (isBlocked || deltas.Count > 0) + // TODO: https://github.com/dotnet/roslyn/issues/36061 + // We should only report diagnostics from emit phase. + // Syntax and semantic diagnostics are already reported by the diagnostic analyzer. + // Currently we do not have means to distinguish between diagnostics reported from compilation and emit phases. + // Querying diagnostics of the entire compilation or just the updated files migth be slow. + // In fact, it is desirable to allow emitting deltas for symbols affected by the change while allowing untouched + // method bodies to have errors. + if (!emitResult.Diagnostics.IsEmpty) { - Telemetry.LogRuntimeCapabilities(await Capabilities.GetValueAsync(cancellationToken).ConfigureAwait(false)); + diagnostics.Add(new(newProject.Id, emitResult.Diagnostics)); } - var update = isBlocked - ? SolutionUpdate.Blocked(diagnostics.ToImmutable(), documentsWithRudeEdits.ToImmutable(), syntaxError, hasEmitErrors) - : new SolutionUpdate( - new ModuleUpdates( - (deltas.Count > 0) ? ModuleUpdateStatus.Ready : ModuleUpdateStatus.None, - deltas.ToImmutable()), - nonRemappableRegions.ToImmutable(), - newProjectBaselines.ToImmutable(), - diagnostics.ToImmutable(), - documentsWithRudeEdits.ToImmutable(), - syntaxError); - - return update; - } - catch (Exception e) when (LogException(e) && FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) - { - throw ExceptionUtilities.Unreachable(); + Telemetry.LogProjectAnalysisSummary(projectSummary, newProject.State.ProjectInfo.Attributes.TelemetryId, emitResult.Diagnostics); } - bool LogException(Exception e) + // log capabilities for edit sessions with changes or reported errors: + if (isBlocked || deltas.Count > 0) { - log.Write("Exception while emitting update: {0}", e.ToString()); - return true; + Telemetry.LogRuntimeCapabilities(await Capabilities.GetValueAsync(cancellationToken).ConfigureAwait(false)); } + + var update = isBlocked + ? SolutionUpdate.Blocked(diagnostics.ToImmutable(), documentsWithRudeEdits.ToImmutable(), syntaxError, hasEmitErrors) + : new SolutionUpdate( + new ModuleUpdates( + (deltas.Count > 0) ? ModuleUpdateStatus.Ready : ModuleUpdateStatus.None, + deltas.ToImmutable()), + nonRemappableRegions.ToImmutable(), + newProjectBaselines.ToImmutable(), + diagnostics.ToImmutable(), + documentsWithRudeEdits.ToImmutable(), + syntaxError); + + return update; + } + catch (Exception e) when (LogException(e) && FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) + { + throw ExceptionUtilities.Unreachable(); } - private async ValueTask LogDeltaFilesAsync(TraceLog.FileLogger log, ManagedHotReloadUpdate delta, int baselineGeneration, Project oldProject, Project newProject, CancellationToken cancellationToken) + bool LogException(Exception e) { - var sessionId = DebuggingSession.Id; + log.Write("Exception while emitting update: {0}", e.ToString()); + return true; + } + } - if (baselineGeneration == 0) - { - var oldCompilationOutputs = DebuggingSession.GetCompilationOutputs(oldProject); - - await log.WriteAsync( - async (stream, cancellationToken) => await oldCompilationOutputs.TryCopyAssemblyToAsync(stream, cancellationToken).ConfigureAwait(false), - sessionId, - newProject.Name, - PathUtilities.GetFileName(oldCompilationOutputs.AssemblyDisplayPath) ?? oldProject.Name + ".dll", - cancellationToken).ConfigureAwait(false); - - await log.WriteAsync( - async (stream, cancellationToken) => await oldCompilationOutputs.TryCopyPdbToAsync(stream, cancellationToken).ConfigureAwait(false), - sessionId, - newProject.Name, - PathUtilities.GetFileName(oldCompilationOutputs.PdbDisplayPath) ?? oldProject.Name + ".pdb", - cancellationToken).ConfigureAwait(false); - } + private async ValueTask LogDeltaFilesAsync(TraceLog.FileLogger log, ManagedHotReloadUpdate delta, int baselineGeneration, Project oldProject, Project newProject, CancellationToken cancellationToken) + { + var sessionId = DebuggingSession.Id; - var generation = baselineGeneration + 1; - log.Write(sessionId, delta.ILDelta, newProject.Name, generation + ".il"); - log.Write(sessionId, delta.MetadataDelta, newProject.Name, generation + ".meta"); - log.Write(sessionId, delta.PdbDelta, newProject.Name, generation + ".pdb"); + if (baselineGeneration == 0) + { + var oldCompilationOutputs = DebuggingSession.GetCompilationOutputs(oldProject); + + await log.WriteAsync( + async (stream, cancellationToken) => await oldCompilationOutputs.TryCopyAssemblyToAsync(stream, cancellationToken).ConfigureAwait(false), + sessionId, + newProject.Name, + PathUtilities.GetFileName(oldCompilationOutputs.AssemblyDisplayPath) ?? oldProject.Name + ".dll", + cancellationToken).ConfigureAwait(false); + + await log.WriteAsync( + async (stream, cancellationToken) => await oldCompilationOutputs.TryCopyPdbToAsync(stream, cancellationToken).ConfigureAwait(false), + sessionId, + newProject.Name, + PathUtilities.GetFileName(oldCompilationOutputs.PdbDisplayPath) ?? oldProject.Name + ".pdb", + cancellationToken).ConfigureAwait(false); } - // internal for testing - internal static void GetActiveStatementAndExceptionRegionSpans( - Guid moduleId, - ActiveStatementsMap oldActiveStatementMap, - ImmutableArray updatedMethodTokens, - ImmutableDictionary> previousNonRemappableRegions, - ImmutableArray activeStatementsInChangedDocuments, - out ImmutableArray activeStatementsInUpdatedMethods, - out ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)> nonRemappableRegions, - out ImmutableArray exceptionRegionUpdates) + var generation = baselineGeneration + 1; + log.Write(sessionId, delta.ILDelta, newProject.Name, generation + ".il"); + log.Write(sessionId, delta.MetadataDelta, newProject.Name, generation + ".meta"); + log.Write(sessionId, delta.PdbDelta, newProject.Name, generation + ".pdb"); + } + + // internal for testing + internal static void GetActiveStatementAndExceptionRegionSpans( + Guid moduleId, + ActiveStatementsMap oldActiveStatementMap, + ImmutableArray updatedMethodTokens, + ImmutableDictionary> previousNonRemappableRegions, + ImmutableArray activeStatementsInChangedDocuments, + out ImmutableArray activeStatementsInUpdatedMethods, + out ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)> nonRemappableRegions, + out ImmutableArray exceptionRegionUpdates) + { + using var _1 = PooledDictionary<(ManagedModuleMethodId MethodId, SourceFileSpan BaseSpan), SourceFileSpan>.GetInstance(out var changedNonRemappableSpans); + var activeStatementsInUpdatedMethodsBuilder = ArrayBuilder.GetInstance(); + var nonRemappableRegionsBuilder = ArrayBuilder<(ManagedModuleMethodId Method, NonRemappableRegion Region)>.GetInstance(); + + // Process active statements and their exception regions in changed documents of this project/module: + foreach (var (oldActiveStatements, newActiveStatements, newExceptionRegions) in activeStatementsInChangedDocuments) { - using var _1 = PooledDictionary<(ManagedModuleMethodId MethodId, SourceFileSpan BaseSpan), SourceFileSpan>.GetInstance(out var changedNonRemappableSpans); - var activeStatementsInUpdatedMethodsBuilder = ArrayBuilder.GetInstance(); - var nonRemappableRegionsBuilder = ArrayBuilder<(ManagedModuleMethodId Method, NonRemappableRegion Region)>.GetInstance(); + Debug.Assert(oldActiveStatements.Length == newExceptionRegions.Length); + Debug.Assert(newActiveStatements.Length == newExceptionRegions.Length); - // Process active statements and their exception regions in changed documents of this project/module: - foreach (var (oldActiveStatements, newActiveStatements, newExceptionRegions) in activeStatementsInChangedDocuments) + for (var i = 0; i < newActiveStatements.Length; i++) { - Debug.Assert(oldActiveStatements.Length == newExceptionRegions.Length); - Debug.Assert(newActiveStatements.Length == newExceptionRegions.Length); + var (_, oldActiveStatement, oldActiveStatementExceptionRegions) = oldActiveStatements[i]; + var newActiveStatement = newActiveStatements[i]; + var newActiveStatementExceptionRegions = newExceptionRegions[i]; + + var instructionId = newActiveStatement.InstructionId; + var methodId = instructionId.Method.Method; - for (var i = 0; i < newActiveStatements.Length; i++) + var isMethodUpdated = updatedMethodTokens.Contains(methodId.Token); + if (isMethodUpdated) { - var (_, oldActiveStatement, oldActiveStatementExceptionRegions) = oldActiveStatements[i]; - var newActiveStatement = newActiveStatements[i]; - var newActiveStatementExceptionRegions = newExceptionRegions[i]; + activeStatementsInUpdatedMethodsBuilder.Add(new ManagedActiveStatementUpdate(methodId, instructionId.ILOffset, newActiveStatement.Span.ToSourceSpan())); + } - var instructionId = newActiveStatement.InstructionId; - var methodId = instructionId.Method.Method; + Debug.Assert(!oldActiveStatement.IsStale); - var isMethodUpdated = updatedMethodTokens.Contains(methodId.Token); - if (isMethodUpdated) - { - activeStatementsInUpdatedMethodsBuilder.Add(new ManagedActiveStatementUpdate(methodId, instructionId.ILOffset, newActiveStatement.Span.ToSourceSpan())); - } + // Adds a region with specified PDB spans. + void AddNonRemappableRegion(SourceFileSpan oldSpan, SourceFileSpan newSpan, bool isExceptionRegion) + { + // TODO: Remove comparer, the path should match exactly. Workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1830914. + Debug.Assert(string.Equals(oldSpan.Path, newSpan.Path, + EditAndContinueMethodDebugInfoReader.IgnoreCaseWhenComparingDocumentNames ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)); - Debug.Assert(!oldActiveStatement.IsStale); + // The up-to-date flag is copied when new active statement is created from the corresponding old one. + Debug.Assert(oldActiveStatement.IsMethodUpToDate == newActiveStatement.IsMethodUpToDate); - // Adds a region with specified PDB spans. - void AddNonRemappableRegion(SourceFileSpan oldSpan, SourceFileSpan newSpan, bool isExceptionRegion) + if (oldActiveStatement.IsMethodUpToDate) { - // TODO: Remove comparer, the path should match exactly. Workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1830914. - Debug.Assert(string.Equals(oldSpan.Path, newSpan.Path, - EditAndContinueMethodDebugInfoReader.IgnoreCaseWhenComparingDocumentNames ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)); - - // The up-to-date flag is copied when new active statement is created from the corresponding old one. - Debug.Assert(oldActiveStatement.IsMethodUpToDate == newActiveStatement.IsMethodUpToDate); - - if (oldActiveStatement.IsMethodUpToDate) + // Start tracking non-remappable regions for active statements in methods that were up-to-date + // when break state was entered and now being updated (regardless of whether the active span changed or not). + if (isMethodUpdated) { - // Start tracking non-remappable regions for active statements in methods that were up-to-date - // when break state was entered and now being updated (regardless of whether the active span changed or not). - if (isMethodUpdated) - { - nonRemappableRegionsBuilder.Add((methodId, new NonRemappableRegion(oldSpan, newSpan, isExceptionRegion))); - } - else if (!isExceptionRegion) - { - // If the method has been up-to-date and it is not updated now then either the active statement span has not changed, - // or the entire method containing it moved. In neither case do we need to start tracking non-remapable region - // for the active statement since movement of whole method bodies (if any) is handled only on PDB level without - // triggering any remapping on the IL level. - // - // That said, we still add a non-remappable region for this active statement, so that we know in future sessions - // that this active statement existed and its span has not changed. We don't report these regions to the debugger, - // but we use them to map active statement spans to the baseline snapshots of following edit sessions. - nonRemappableRegionsBuilder.Add((methodId, new NonRemappableRegion(oldSpan, oldSpan, isExceptionRegion: false))); - } + nonRemappableRegionsBuilder.Add((methodId, new NonRemappableRegion(oldSpan, newSpan, isExceptionRegion))); } - else if (oldSpan.Span != newSpan.Span) + else if (!isExceptionRegion) { - // The method is not up-to-date hence we might have a previous non-remappable span mapping that needs to be brought forward to the new snapshot. - changedNonRemappableSpans[(methodId, oldSpan)] = newSpan; + // If the method has been up-to-date and it is not updated now then either the active statement span has not changed, + // or the entire method containing it moved. In neither case do we need to start tracking non-remapable region + // for the active statement since movement of whole method bodies (if any) is handled only on PDB level without + // triggering any remapping on the IL level. + // + // That said, we still add a non-remappable region for this active statement, so that we know in future sessions + // that this active statement existed and its span has not changed. We don't report these regions to the debugger, + // but we use them to map active statement spans to the baseline snapshots of following edit sessions. + nonRemappableRegionsBuilder.Add((methodId, new NonRemappableRegion(oldSpan, oldSpan, isExceptionRegion: false))); } } + else if (oldSpan.Span != newSpan.Span) + { + // The method is not up-to-date hence we might have a previous non-remappable span mapping that needs to be brought forward to the new snapshot. + changedNonRemappableSpans[(methodId, oldSpan)] = newSpan; + } + } - AddNonRemappableRegion(oldActiveStatement.FileSpan, newActiveStatement.FileSpan, isExceptionRegion: false); + AddNonRemappableRegion(oldActiveStatement.FileSpan, newActiveStatement.FileSpan, isExceptionRegion: false); - // The spans of the exception regions are known (non-default) for active statements in changed documents - // as we ensured earlier that all changed documents are in-sync. + // The spans of the exception regions are known (non-default) for active statements in changed documents + // as we ensured earlier that all changed documents are in-sync. - for (var j = 0; j < oldActiveStatementExceptionRegions.Spans.Length; j++) - { - AddNonRemappableRegion(oldActiveStatementExceptionRegions.Spans[j], newActiveStatementExceptionRegions[j], isExceptionRegion: true); - } + for (var j = 0; j < oldActiveStatementExceptionRegions.Spans.Length; j++) + { + AddNonRemappableRegion(oldActiveStatementExceptionRegions.Spans[j], newActiveStatementExceptionRegions[j], isExceptionRegion: true); } } + } - activeStatementsInUpdatedMethods = activeStatementsInUpdatedMethodsBuilder.ToImmutableAndFree(); + activeStatementsInUpdatedMethods = activeStatementsInUpdatedMethodsBuilder.ToImmutableAndFree(); - // Gather all active method instances contained in this project/module that are not up-to-date: - using var _2 = PooledHashSet.GetInstance(out var unremappedActiveMethods); - foreach (var (instruction, baseActiveStatement) in oldActiveStatementMap.InstructionMap) + // Gather all active method instances contained in this project/module that are not up-to-date: + using var _2 = PooledHashSet.GetInstance(out var unremappedActiveMethods); + foreach (var (instruction, baseActiveStatement) in oldActiveStatementMap.InstructionMap) + { + if (moduleId == instruction.Method.Module && !baseActiveStatement.IsMethodUpToDate) { - if (moduleId == instruction.Method.Module && !baseActiveStatement.IsMethodUpToDate) - { - unremappedActiveMethods.Add(instruction.Method.Method); - } + unremappedActiveMethods.Add(instruction.Method.Method); } + } - // Update previously calculated non-remappable region mappings. - // These map to the old snapshot and we need them to map to the new snapshot, which will be the baseline for the next session. - if (unremappedActiveMethods.Count > 0) + // Update previously calculated non-remappable region mappings. + // These map to the old snapshot and we need them to map to the new snapshot, which will be the baseline for the next session. + if (unremappedActiveMethods.Count > 0) + { + foreach (var (methodInstance, regionsInMethod) in previousNonRemappableRegions) { - foreach (var (methodInstance, regionsInMethod) in previousNonRemappableRegions) + // Skip non-remappable regions that belong to method instances that are from a different module. + if (methodInstance.Module != moduleId) { - // Skip non-remappable regions that belong to method instances that are from a different module. - if (methodInstance.Module != moduleId) - { - continue; - } - - // Skip no longer active methods - all active statements in these method instances have been remapped to newer versions. - // New active statement can't appear in a stale method instance since such instance can't be invoked. - if (!unremappedActiveMethods.Contains(methodInstance.Method)) - { - continue; - } + continue; + } - foreach (var region in regionsInMethod) - { - // We have calculated changes against a base snapshot (last break state): - var baseSpan = region.NewSpan; + // Skip no longer active methods - all active statements in these method instances have been remapped to newer versions. + // New active statement can't appear in a stale method instance since such instance can't be invoked. + if (!unremappedActiveMethods.Contains(methodInstance.Method)) + { + continue; + } - NonRemappableRegion newRegion; - if (changedNonRemappableSpans.TryGetValue((methodInstance.Method, baseSpan), out var newSpan)) - { - // all spans must be of the same size: - Debug.Assert(newSpan.Span.End.Line - newSpan.Span.Start.Line == baseSpan.Span.End.Line - baseSpan.Span.Start.Line); - Debug.Assert(region.OldSpan.Span.End.Line - region.OldSpan.Span.Start.Line == baseSpan.Span.End.Line - baseSpan.Span.Start.Line); - Debug.Assert(newSpan.Path == region.OldSpan.Path); + foreach (var region in regionsInMethod) + { + // We have calculated changes against a base snapshot (last break state): + var baseSpan = region.NewSpan; - newRegion = region.WithNewSpan(newSpan); - } - else - { - newRegion = region; - } + NonRemappableRegion newRegion; + if (changedNonRemappableSpans.TryGetValue((methodInstance.Method, baseSpan), out var newSpan)) + { + // all spans must be of the same size: + Debug.Assert(newSpan.Span.End.Line - newSpan.Span.Start.Line == baseSpan.Span.End.Line - baseSpan.Span.Start.Line); + Debug.Assert(region.OldSpan.Span.End.Line - region.OldSpan.Span.Start.Line == baseSpan.Span.End.Line - baseSpan.Span.Start.Line); + Debug.Assert(newSpan.Path == region.OldSpan.Path); - nonRemappableRegionsBuilder.Add((methodInstance.Method, newRegion)); + newRegion = region.WithNewSpan(newSpan); + } + else + { + newRegion = region; } + + nonRemappableRegionsBuilder.Add((methodInstance.Method, newRegion)); } } - - nonRemappableRegions = nonRemappableRegionsBuilder.ToImmutableAndFree(); - - // Note: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1319289 - // - // The update should include the file name, otherwise it is not possible for the debugger to find - // the right IL span of the exception handler in case when multiple handlers in the same method - // have the same mapped span but different mapped file name: - // - // try { active statement } - // #line 20 "bar" - // catch (IOException) { } - // #line 20 "baz" - // catch (Exception) { } - // - // The range span in exception region updates is the new span. Deltas are inverse. - // old = new + delta - // new = old – delta - exceptionRegionUpdates = nonRemappableRegions.SelectAsArray( - r => r.Region.IsExceptionRegion, - r => new ManagedExceptionRegionUpdate( - r.Method, - -r.Region.OldSpan.Span.GetLineDelta(r.Region.NewSpan.Span), - r.Region.NewSpan.Span.ToSourceSpan())); } + + nonRemappableRegions = nonRemappableRegionsBuilder.ToImmutableAndFree(); + + // Note: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1319289 + // + // The update should include the file name, otherwise it is not possible for the debugger to find + // the right IL span of the exception handler in case when multiple handlers in the same method + // have the same mapped span but different mapped file name: + // + // try { active statement } + // #line 20 "bar" + // catch (IOException) { } + // #line 20 "baz" + // catch (Exception) { } + // + // The range span in exception region updates is the new span. Deltas are inverse. + // old = new + delta + // new = old – delta + exceptionRegionUpdates = nonRemappableRegions.SelectAsArray( + r => r.Region.IsExceptionRegion, + r => new ManagedExceptionRegionUpdate( + r.Method, + -r.Region.OldSpan.Span.GetLineDelta(r.Region.NewSpan.Span), + r.Region.NewSpan.Span.ToSourceSpan())); } } diff --git a/src/Features/Core/Portable/EditAndContinue/EditSessionTelemetry.cs b/src/Features/Core/Portable/EditAndContinue/EditSessionTelemetry.cs index e91f9f490fd5f..56e5ad9da9187 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditSessionTelemetry.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditSessionTelemetry.cs @@ -8,142 +8,141 @@ using System.Diagnostics; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +// EncEditSessionInfo is populated on a background thread and then read from the UI thread +internal sealed class EditSessionTelemetry { - // EncEditSessionInfo is populated on a background thread and then read from the UI thread - internal sealed class EditSessionTelemetry + internal readonly struct Data(EditSessionTelemetry telemetry) { - internal readonly struct Data(EditSessionTelemetry telemetry) - { - public readonly ImmutableArray<(ushort EditKind, ushort SyntaxKind, Guid projectId)> RudeEdits = telemetry._rudeEdits.AsImmutable(); - public readonly ImmutableArray EmitErrorIds = telemetry._emitErrorIds.AsImmutable(); - public readonly ImmutableArray ProjectsWithValidDelta = telemetry._projectsWithValidDelta.AsImmutable(); - public readonly EditAndContinueCapabilities Capabilities = telemetry._capabilities; - public readonly bool HadCompilationErrors = telemetry._hadCompilationErrors; - public readonly bool HadRudeEdits = telemetry._hadRudeEdits; - public readonly bool HadValidChanges = telemetry._hadValidChanges; - public readonly bool HadValidInsignificantChanges = telemetry._hadValidInsignificantChanges; - public readonly bool InBreakState = telemetry._inBreakState!.Value; - public readonly bool IsEmpty = telemetry.IsEmpty; - public readonly bool Committed = telemetry._committed; - public readonly TimeSpan EmitDifferenceTime = telemetry._emitDifferenceTime; - public readonly TimeSpan AnalysisTime = telemetry._analysisTime; - } + public readonly ImmutableArray<(ushort EditKind, ushort SyntaxKind, Guid projectId)> RudeEdits = telemetry._rudeEdits.AsImmutable(); + public readonly ImmutableArray EmitErrorIds = telemetry._emitErrorIds.AsImmutable(); + public readonly ImmutableArray ProjectsWithValidDelta = telemetry._projectsWithValidDelta.AsImmutable(); + public readonly EditAndContinueCapabilities Capabilities = telemetry._capabilities; + public readonly bool HadCompilationErrors = telemetry._hadCompilationErrors; + public readonly bool HadRudeEdits = telemetry._hadRudeEdits; + public readonly bool HadValidChanges = telemetry._hadValidChanges; + public readonly bool HadValidInsignificantChanges = telemetry._hadValidInsignificantChanges; + public readonly bool InBreakState = telemetry._inBreakState!.Value; + public readonly bool IsEmpty = telemetry.IsEmpty; + public readonly bool Committed = telemetry._committed; + public readonly TimeSpan EmitDifferenceTime = telemetry._emitDifferenceTime; + public readonly TimeSpan AnalysisTime = telemetry._analysisTime; + } - private readonly object _guard = new(); + private readonly object _guard = new(); - // Limit the number of reported items to limit the size of the telemetry event (max total size is 64K). - private const int MaxReportedProjectIds = 20; + // Limit the number of reported items to limit the size of the telemetry event (max total size is 64K). + private const int MaxReportedProjectIds = 20; - private readonly HashSet<(ushort, ushort, Guid)> _rudeEdits = []; - private readonly HashSet _emitErrorIds = []; - private readonly HashSet _projectsWithValidDelta = []; + private readonly HashSet<(ushort, ushort, Guid)> _rudeEdits = []; + private readonly HashSet _emitErrorIds = []; + private readonly HashSet _projectsWithValidDelta = []; - private bool _hadCompilationErrors; - private bool _hadRudeEdits; - private bool _hadValidChanges; - private bool _hadValidInsignificantChanges; - private bool? _inBreakState; - private bool _committed; - private TimeSpan _emitDifferenceTime; - private TimeSpan _analysisTime; + private bool _hadCompilationErrors; + private bool _hadRudeEdits; + private bool _hadValidChanges; + private bool _hadValidInsignificantChanges; + private bool? _inBreakState; + private bool _committed; + private TimeSpan _emitDifferenceTime; + private TimeSpan _analysisTime; - private EditAndContinueCapabilities _capabilities; + private EditAndContinueCapabilities _capabilities; - public Data GetDataAndClear() + public Data GetDataAndClear() + { + lock (_guard) { - lock (_guard) - { - var data = new Data(this); - _rudeEdits.Clear(); - _emitErrorIds.Clear(); - _projectsWithValidDelta.Clear(); - _hadCompilationErrors = false; - _hadRudeEdits = false; - _hadValidChanges = false; - _hadValidInsignificantChanges = false; - _inBreakState = null; - _capabilities = EditAndContinueCapabilities.None; - _committed = false; - _emitDifferenceTime = TimeSpan.Zero; - return data; - } + var data = new Data(this); + _rudeEdits.Clear(); + _emitErrorIds.Clear(); + _projectsWithValidDelta.Clear(); + _hadCompilationErrors = false; + _hadRudeEdits = false; + _hadValidChanges = false; + _hadValidInsignificantChanges = false; + _inBreakState = null; + _capabilities = EditAndContinueCapabilities.None; + _committed = false; + _emitDifferenceTime = TimeSpan.Zero; + return data; } + } - public bool IsEmpty => !(_hadCompilationErrors || _hadRudeEdits || _hadValidChanges || _hadValidInsignificantChanges); + public bool IsEmpty => !(_hadCompilationErrors || _hadRudeEdits || _hadValidChanges || _hadValidInsignificantChanges); - public void SetBreakState(bool value) - => _inBreakState = value; + public void SetBreakState(bool value) + => _inBreakState = value; - public void LogEmitDifferenceTime(TimeSpan span) - => _emitDifferenceTime += span; + public void LogEmitDifferenceTime(TimeSpan span) + => _emitDifferenceTime += span; - public void LogAnalysisTime(TimeSpan span) - => _analysisTime += span; + public void LogAnalysisTime(TimeSpan span) + => _analysisTime += span; - public void LogProjectAnalysisSummary(ProjectAnalysisSummary summary, Guid projectTelemetryId, ImmutableArray errorsIds) + public void LogProjectAnalysisSummary(ProjectAnalysisSummary summary, Guid projectTelemetryId, ImmutableArray errorsIds) + { + lock (_guard) { - lock (_guard) - { - _emitErrorIds.AddRange(errorsIds); + _emitErrorIds.AddRange(errorsIds); - switch (summary) - { - case ProjectAnalysisSummary.NoChanges: - break; + switch (summary) + { + case ProjectAnalysisSummary.NoChanges: + break; - case ProjectAnalysisSummary.CompilationErrors: - _hadCompilationErrors = true; - break; + case ProjectAnalysisSummary.CompilationErrors: + _hadCompilationErrors = true; + break; - case ProjectAnalysisSummary.RudeEdits: - _hadRudeEdits = true; - break; + case ProjectAnalysisSummary.RudeEdits: + _hadRudeEdits = true; + break; - case ProjectAnalysisSummary.ValidChanges: - _hadValidChanges = true; + case ProjectAnalysisSummary.ValidChanges: + _hadValidChanges = true; - if (errorsIds.IsEmpty && _projectsWithValidDelta.Count < MaxReportedProjectIds) - { - _projectsWithValidDelta.Add(projectTelemetryId); - } + if (errorsIds.IsEmpty && _projectsWithValidDelta.Count < MaxReportedProjectIds) + { + _projectsWithValidDelta.Add(projectTelemetryId); + } - break; + break; - case ProjectAnalysisSummary.ValidInsignificantChanges: - _hadValidInsignificantChanges = true; - break; + case ProjectAnalysisSummary.ValidInsignificantChanges: + _hadValidInsignificantChanges = true; + break; - default: - throw ExceptionUtilities.UnexpectedValue(summary); - } + default: + throw ExceptionUtilities.UnexpectedValue(summary); } } + } - public void LogProjectAnalysisSummary(ProjectAnalysisSummary summary, Guid projectTelemetryId, ImmutableArray emitDiagnostics) - => LogProjectAnalysisSummary(summary, projectTelemetryId, emitDiagnostics.SelectAsArray(d => d.Severity == DiagnosticSeverity.Error, d => d.Id)); + public void LogProjectAnalysisSummary(ProjectAnalysisSummary summary, Guid projectTelemetryId, ImmutableArray emitDiagnostics) + => LogProjectAnalysisSummary(summary, projectTelemetryId, emitDiagnostics.SelectAsArray(d => d.Severity == DiagnosticSeverity.Error, d => d.Id)); - public void LogRudeEditDiagnostics(ImmutableArray diagnostics, Guid projectTelemetryId) + public void LogRudeEditDiagnostics(ImmutableArray diagnostics, Guid projectTelemetryId) + { + lock (_guard) { - lock (_guard) + foreach (var diagnostic in diagnostics) { - foreach (var diagnostic in diagnostics) - { - _rudeEdits.Add(((ushort)diagnostic.Kind, diagnostic.SyntaxKind, projectTelemetryId)); - } + _rudeEdits.Add(((ushort)diagnostic.Kind, diagnostic.SyntaxKind, projectTelemetryId)); } } + } - public void LogRuntimeCapabilities(EditAndContinueCapabilities capabilities) + public void LogRuntimeCapabilities(EditAndContinueCapabilities capabilities) + { + lock (_guard) { - lock (_guard) - { - Debug.Assert(_capabilities == EditAndContinueCapabilities.None || _capabilities == capabilities); - _capabilities = capabilities; - } + Debug.Assert(_capabilities == EditAndContinueCapabilities.None || _capabilities == capabilities); + _capabilities = capabilities; } - - public void LogCommitted() - => _committed = true; } + + public void LogCommitted() + => _committed = true; } diff --git a/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs b/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs index bcc77fa3b3368..d27a67d59ed99 100644 --- a/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs +++ b/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs @@ -14,157 +14,156 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal readonly struct EmitSolutionUpdateResults { - internal readonly struct EmitSolutionUpdateResults + [DataContract] + internal readonly struct Data { - [DataContract] - internal readonly struct Data - { - [DataMember] - public required ModuleUpdates ModuleUpdates { get; init; } + [DataMember] + public required ModuleUpdates ModuleUpdates { get; init; } - [DataMember] - public required ImmutableArray Diagnostics { get; init; } + [DataMember] + public required ImmutableArray Diagnostics { get; init; } - [DataMember] - public required ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> RudeEdits { get; init; } + [DataMember] + public required ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> RudeEdits { get; init; } - [DataMember] - public required DiagnosticData? SyntaxError { get; init; } - } + [DataMember] + public required DiagnosticData? SyntaxError { get; init; } + } - public static readonly EmitSolutionUpdateResults Empty = new() + public static readonly EmitSolutionUpdateResults Empty = new() + { + ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.None, []), + Diagnostics = [], + RudeEdits = [], + SyntaxError = null + }; + + public required ModuleUpdates ModuleUpdates { get; init; } + public required ImmutableArray Diagnostics { get; init; } + public required ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> RudeEdits { get; init; } + public required Diagnostic? SyntaxError { get; init; } + + public Data Dehydrate(Solution solution) + => new() { - ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.None, []), - Diagnostics = [], - RudeEdits = [], - SyntaxError = null + ModuleUpdates = ModuleUpdates, + Diagnostics = Diagnostics.ToDiagnosticData(solution), + RudeEdits = RudeEdits, + SyntaxError = GetSyntaxErrorData(solution) }; - public required ModuleUpdates ModuleUpdates { get; init; } - public required ImmutableArray Diagnostics { get; init; } - public required ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> RudeEdits { get; init; } - public required Diagnostic? SyntaxError { get; init; } + public DiagnosticData? GetSyntaxErrorData(Solution solution) + { + if (SyntaxError == null) + { + return null; + } - public Data Dehydrate(Solution solution) - => new() - { - ModuleUpdates = ModuleUpdates, - Diagnostics = Diagnostics.ToDiagnosticData(solution), - RudeEdits = RudeEdits, - SyntaxError = GetSyntaxErrorData(solution) - }; + Debug.Assert(SyntaxError.Location.SourceTree != null); + return DiagnosticData.Create(SyntaxError, solution.GetRequiredDocument(SyntaxError.Location.SourceTree)); + } + + public async Task> GetAllDiagnosticsAsync(Solution solution, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var diagnostics); - public DiagnosticData? GetSyntaxErrorData(Solution solution) + // add rude edits: + foreach (var (documentId, documentRudeEdits) in RudeEdits) { - if (SyntaxError == null) + var document = await solution.GetDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(document); + + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + foreach (var documentRudeEdit in documentRudeEdits) { - return null; + diagnostics.Add(documentRudeEdit.ToDiagnostic(tree)); } - - Debug.Assert(SyntaxError.Location.SourceTree != null); - return DiagnosticData.Create(SyntaxError, solution.GetRequiredDocument(SyntaxError.Location.SourceTree)); } - public async Task> GetAllDiagnosticsAsync(Solution solution, CancellationToken cancellationToken) + // add emit diagnostics: + foreach (var (_, projectEmitDiagnostics) in Diagnostics) { - using var _ = ArrayBuilder.GetInstance(out var diagnostics); + diagnostics.AddRange(projectEmitDiagnostics); + } - // add rude edits: - foreach (var (documentId, documentRudeEdits) in RudeEdits) - { - var document = await solution.GetDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - Contract.ThrowIfNull(document); + return diagnostics.ToImmutable(); + } - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - foreach (var documentRudeEdit in documentRudeEdits) - { - diagnostics.Add(documentRudeEdit.ToDiagnostic(tree)); - } - } + internal static async ValueTask> GetHotReloadDiagnosticsAsync( + Solution solution, + ImmutableArray diagnosticData, + ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> rudeEdits, + DiagnosticData? syntaxError, + ModuleUpdateStatus updateStatus, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var builder); - // add emit diagnostics: - foreach (var (_, projectEmitDiagnostics) in Diagnostics) + // Add the first compiler emit error. Do not report warnings - they do not block applying the edit. + // It's unnecessary to report more then one error since all the diagnostics are already reported in the Error List + // and this is just messaging to the agent. + + foreach (var data in diagnosticData) + { + if (data.Severity != DiagnosticSeverity.Error) { - diagnostics.AddRange(projectEmitDiagnostics); + continue; } - return diagnostics.ToImmutable(); + builder.Add(data.ToHotReloadDiagnostic(updateStatus)); + + // only report first error + break; } - internal static async ValueTask> GetHotReloadDiagnosticsAsync( - Solution solution, - ImmutableArray diagnosticData, - ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> rudeEdits, - DiagnosticData? syntaxError, - ModuleUpdateStatus updateStatus, - CancellationToken cancellationToken) + if (syntaxError != null) { - using var _ = ArrayBuilder.GetInstance(out var builder); + Debug.Assert(syntaxError.DataLocation != null); + Debug.Assert(syntaxError.Message != null); - // Add the first compiler emit error. Do not report warnings - they do not block applying the edit. - // It's unnecessary to report more then one error since all the diagnostics are already reported in the Error List - // and this is just messaging to the agent. + var fileSpan = syntaxError.DataLocation.MappedFileSpan; - foreach (var data in diagnosticData) - { - if (data.Severity != DiagnosticSeverity.Error) - { - continue; - } + builder.Add(new ManagedHotReloadDiagnostic( + syntaxError.Id, + syntaxError.Message, + ManagedHotReloadDiagnosticSeverity.Error, + fileSpan.Path, + fileSpan.Span.ToSourceSpan())); + } - builder.Add(data.ToHotReloadDiagnostic(updateStatus)); + // Report all rude edits. - // only report first error - break; - } + foreach (var (documentId, diagnostics) in rudeEdits) + { + var document = await solution.GetRequiredDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (syntaxError != null) + foreach (var diagnostic in diagnostics) { - Debug.Assert(syntaxError.DataLocation != null); - Debug.Assert(syntaxError.Message != null); + var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(diagnostic.Kind); - var fileSpan = syntaxError.DataLocation.MappedFileSpan; + var severity = descriptor.DefaultSeverity switch + { + DiagnosticSeverity.Error => ManagedHotReloadDiagnosticSeverity.RestartRequired, + DiagnosticSeverity.Warning => ManagedHotReloadDiagnosticSeverity.Warning, + _ => throw ExceptionUtilities.UnexpectedValue(descriptor.DefaultSeverity) + }; + + var fileSpan = tree.GetMappedLineSpan(diagnostic.Span, cancellationToken); builder.Add(new ManagedHotReloadDiagnostic( - syntaxError.Id, - syntaxError.Message, - ManagedHotReloadDiagnosticSeverity.Error, - fileSpan.Path, + descriptor.Id, + string.Format(descriptor.MessageFormat.ToString(CultureInfo.CurrentUICulture), diagnostic.Arguments), + severity, + fileSpan.Path ?? "", fileSpan.Span.ToSourceSpan())); } - - // Report all rude edits. - - foreach (var (documentId, diagnostics) in rudeEdits) - { - var document = await solution.GetRequiredDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - - foreach (var diagnostic in diagnostics) - { - var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(diagnostic.Kind); - - var severity = descriptor.DefaultSeverity switch - { - DiagnosticSeverity.Error => ManagedHotReloadDiagnosticSeverity.RestartRequired, - DiagnosticSeverity.Warning => ManagedHotReloadDiagnosticSeverity.Warning, - _ => throw ExceptionUtilities.UnexpectedValue(descriptor.DefaultSeverity) - }; - - var fileSpan = tree.GetMappedLineSpan(diagnostic.Span, cancellationToken); - - builder.Add(new ManagedHotReloadDiagnostic( - descriptor.Id, - string.Format(descriptor.MessageFormat.ToString(CultureInfo.CurrentUICulture), diagnostic.Arguments), - severity, - fileSpan.Path ?? "", - fileSpan.Span.ToSourceSpan())); - } - } - - return builder.ToImmutable(); } + + return builder.ToImmutable(); } } diff --git a/src/Features/Core/Portable/EditAndContinue/IActiveStatementSpanProvider.cs b/src/Features/Core/Portable/EditAndContinue/IActiveStatementSpanProvider.cs index e1326854caaef..19de6d28cc9ef 100644 --- a/src/Features/Core/Portable/EditAndContinue/IActiveStatementSpanProvider.cs +++ b/src/Features/Core/Portable/EditAndContinue/IActiveStatementSpanProvider.cs @@ -6,33 +6,32 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal interface IActiveStatementSpanProvider { - internal interface IActiveStatementSpanProvider - { - /// - /// Returns base mapped active statement spans contained in each specified document projected to a given solution snapshot - /// (i.e. the solution snapshot the base active statements are current for could be different from the given ). - /// - /// - /// if called outside of an edit session. - /// The length of the returned array matches the length of otherwise. - /// - /// - /// The document may be any text document. - /// may not correspond to any document in the given (an empty array of spans is returned for such document). - /// Returns of the unmapped document containing the active statement (i.e. the document that has the #line directive mapping the statement to one of the specified ), - /// or null the unmapped document has not been determined (the active statement has not changed from the baseline). - /// - ValueTask>> GetBaseActiveStatementSpansAsync(Solution solution, ImmutableArray documentIds, CancellationToken cancellationToken); + /// + /// Returns base mapped active statement spans contained in each specified document projected to a given solution snapshot + /// (i.e. the solution snapshot the base active statements are current for could be different from the given ). + /// + /// + /// if called outside of an edit session. + /// The length of the returned array matches the length of otherwise. + /// + /// + /// The document may be any text document. + /// may not correspond to any document in the given (an empty array of spans is returned for such document). + /// Returns of the unmapped document containing the active statement (i.e. the document that has the #line directive mapping the statement to one of the specified ), + /// or null the unmapped document has not been determined (the active statement has not changed from the baseline). + /// + ValueTask>> GetBaseActiveStatementSpansAsync(Solution solution, ImmutableArray documentIds, CancellationToken cancellationToken); - /// - /// Returns adjusted active statements in the specified mapped snapshot. - /// - /// - /// if called outside of an edit session, or active statements for the document can't be determined for some reason - /// (e.g. the document has syntax errors or is out-of-sync). - /// - ValueTask> GetAdjustedActiveStatementSpansAsync(TextDocument document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken); - } + /// + /// Returns adjusted active statements in the specified mapped snapshot. + /// + /// + /// if called outside of an edit session, or active statements for the document can't be determined for some reason + /// (e.g. the document has syntax errors or is out-of-sync). + /// + ValueTask> GetAdjustedActiveStatementSpansAsync(TextDocument document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueAnalyzer.cs b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueAnalyzer.cs index e09687a8d63e5..03a68cfd19c34 100644 --- a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueAnalyzer.cs +++ b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueAnalyzer.cs @@ -9,18 +9,17 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal interface IEditAndContinueAnalyzer : ILanguageService { - internal interface IEditAndContinueAnalyzer : ILanguageService - { - Task AnalyzeDocumentAsync( - Project baseProject, - AsyncLazy lazyBaseActiveStatements, - Document document, - ImmutableArray newActiveStatementSpans, - AsyncLazy lazyCapabilities, - CancellationToken cancellationToken); + Task AnalyzeDocumentAsync( + Project baseProject, + AsyncLazy lazyBaseActiveStatements, + Document document, + ImmutableArray newActiveStatementSpans, + AsyncLazy lazyCapabilities, + CancellationToken cancellationToken); - ActiveStatementExceptionRegions GetExceptionRegions(SyntaxNode syntaxRoot, TextSpan unmappedActiveStatementSpan, bool isNonLeaf, CancellationToken cancellationToken); - } + ActiveStatementExceptionRegions GetExceptionRegions(SyntaxNode syntaxRoot, TextSpan unmappedActiveStatementSpan, bool isNonLeaf, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueService.cs b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueService.cs index 67acc65626ec8..322a5f20f6634 100644 --- a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueService.cs +++ b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueService.cs @@ -9,28 +9,27 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal interface IEditAndContinueWorkspaceService : IWorkspaceService { - internal interface IEditAndContinueWorkspaceService : IWorkspaceService - { - IEditAndContinueService Service { get; } - } + IEditAndContinueService Service { get; } +} - internal interface IEditAndContinueService - { - ValueTask> GetDocumentDiagnosticsAsync(Document document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken); - ValueTask EmitSolutionUpdateAsync(DebuggingSessionId sessionId, Solution solution, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken); +internal interface IEditAndContinueService +{ + ValueTask> GetDocumentDiagnosticsAsync(Document document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken); + ValueTask EmitSolutionUpdateAsync(DebuggingSessionId sessionId, Solution solution, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken); - void CommitSolutionUpdate(DebuggingSessionId sessionId, out ImmutableArray documentsToReanalyze); - void DiscardSolutionUpdate(DebuggingSessionId sessionId); + void CommitSolutionUpdate(DebuggingSessionId sessionId, out ImmutableArray documentsToReanalyze); + void DiscardSolutionUpdate(DebuggingSessionId sessionId); - ValueTask StartDebuggingSessionAsync(Solution solution, IManagedHotReloadService debuggerService, IPdbMatchingSourceTextProvider sourceTextProvider, ImmutableArray captureMatchingDocuments, bool captureAllMatchingDocuments, bool reportDiagnostics, CancellationToken cancellationToken); - void BreakStateOrCapabilitiesChanged(DebuggingSessionId sessionId, bool? inBreakState, out ImmutableArray documentsToReanalyze); - void EndDebuggingSession(DebuggingSessionId sessionId, out ImmutableArray documentsToReanalyze); + ValueTask StartDebuggingSessionAsync(Solution solution, IManagedHotReloadService debuggerService, IPdbMatchingSourceTextProvider sourceTextProvider, ImmutableArray captureMatchingDocuments, bool captureAllMatchingDocuments, bool reportDiagnostics, CancellationToken cancellationToken); + void BreakStateOrCapabilitiesChanged(DebuggingSessionId sessionId, bool? inBreakState, out ImmutableArray documentsToReanalyze); + void EndDebuggingSession(DebuggingSessionId sessionId, out ImmutableArray documentsToReanalyze); - ValueTask>> GetBaseActiveStatementSpansAsync(DebuggingSessionId sessionId, Solution solution, ImmutableArray documentIds, CancellationToken cancellationToken); - ValueTask> GetAdjustedActiveStatementSpansAsync(DebuggingSessionId sessionId, TextDocument document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken); + ValueTask>> GetBaseActiveStatementSpansAsync(DebuggingSessionId sessionId, Solution solution, ImmutableArray documentIds, CancellationToken cancellationToken); + ValueTask> GetAdjustedActiveStatementSpansAsync(DebuggingSessionId sessionId, TextDocument document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken); - void SetFileLoggingDirectory(string? logDirectory); - } + void SetFileLoggingDirectory(string? logDirectory); } diff --git a/src/Features/Core/Portable/EditAndContinue/NonRemappableRegion.cs b/src/Features/Core/Portable/EditAndContinue/NonRemappableRegion.cs index 519045cf40865..2fafa438bdc71 100644 --- a/src/Features/Core/Portable/EditAndContinue/NonRemappableRegion.cs +++ b/src/Features/Core/Portable/EditAndContinue/NonRemappableRegion.cs @@ -6,52 +6,51 @@ using System.Diagnostics; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[DebuggerDisplay("{GetDebuggerDisplay(),nq}")] +internal readonly struct NonRemappableRegion(SourceFileSpan oldSpan, SourceFileSpan newSpan, bool isExceptionRegion) : IEquatable { - [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] - internal readonly struct NonRemappableRegion(SourceFileSpan oldSpan, SourceFileSpan newSpan, bool isExceptionRegion) : IEquatable - { - /// - /// PDB span in pre-remap method version. - /// - /// - /// When a thread is executing in an old version of a method before it is remapped to the new version - /// its active statement needs to be mapped from in the old version - /// to in the new version of the method. - /// - public readonly SourceFileSpan OldSpan = oldSpan; - - /// - /// PDB span in the new method version. - /// - public readonly SourceFileSpan NewSpan = newSpan; - - /// - /// True if the region represents an exception region, false if it represents an active statement. - /// - public readonly bool IsExceptionRegion = isExceptionRegion; - - public override bool Equals(object? obj) - => obj is NonRemappableRegion region && Equals(region); - - public bool Equals(NonRemappableRegion other) - => OldSpan.Equals(other.OldSpan) && - NewSpan.Equals(other.NewSpan) && - IsExceptionRegion == other.IsExceptionRegion; - - public override int GetHashCode() - => Hash.Combine(OldSpan.GetHashCode(), Hash.Combine(IsExceptionRegion, NewSpan.GetHashCode())); - - public static bool operator ==(NonRemappableRegion left, NonRemappableRegion right) - => left.Equals(right); - - public static bool operator !=(NonRemappableRegion left, NonRemappableRegion right) - => !(left == right); - - public NonRemappableRegion WithNewSpan(SourceFileSpan newSpan) - => new(OldSpan, newSpan, IsExceptionRegion); - - internal string GetDebuggerDisplay() - => $"{(IsExceptionRegion ? "ER" : "AS")} {OldSpan} => {NewSpan.Span}"; - } + /// + /// PDB span in pre-remap method version. + /// + /// + /// When a thread is executing in an old version of a method before it is remapped to the new version + /// its active statement needs to be mapped from in the old version + /// to in the new version of the method. + /// + public readonly SourceFileSpan OldSpan = oldSpan; + + /// + /// PDB span in the new method version. + /// + public readonly SourceFileSpan NewSpan = newSpan; + + /// + /// True if the region represents an exception region, false if it represents an active statement. + /// + public readonly bool IsExceptionRegion = isExceptionRegion; + + public override bool Equals(object? obj) + => obj is NonRemappableRegion region && Equals(region); + + public bool Equals(NonRemappableRegion other) + => OldSpan.Equals(other.OldSpan) && + NewSpan.Equals(other.NewSpan) && + IsExceptionRegion == other.IsExceptionRegion; + + public override int GetHashCode() + => Hash.Combine(OldSpan.GetHashCode(), Hash.Combine(IsExceptionRegion, NewSpan.GetHashCode())); + + public static bool operator ==(NonRemappableRegion left, NonRemappableRegion right) + => left.Equals(right); + + public static bool operator !=(NonRemappableRegion left, NonRemappableRegion right) + => !(left == right); + + public NonRemappableRegion WithNewSpan(SourceFileSpan newSpan) + => new(OldSpan, newSpan, IsExceptionRegion); + + internal string GetDebuggerDisplay() + => $"{(IsExceptionRegion ? "ER" : "AS")} {OldSpan} => {NewSpan.Span}"; } diff --git a/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs b/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs index e3816eac25102..e2204f8f15d41 100644 --- a/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs +++ b/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs @@ -7,23 +7,22 @@ using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal abstract class PendingUpdate( + ImmutableArray projectBaselines, + ImmutableArray deltas) { - internal abstract class PendingUpdate( - ImmutableArray projectBaselines, - ImmutableArray deltas) - { - public readonly ImmutableArray ProjectBaselines = projectBaselines; - public readonly ImmutableArray Deltas = deltas; - } + public readonly ImmutableArray ProjectBaselines = projectBaselines; + public readonly ImmutableArray Deltas = deltas; +} - internal sealed class PendingSolutionUpdate( - Solution solution, - ImmutableArray projectBaselines, - ImmutableArray deltas, - ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)> nonRemappableRegions) : PendingUpdate(projectBaselines, deltas) - { - public readonly Solution Solution = solution; - public readonly ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)> Regions)> NonRemappableRegions = nonRemappableRegions; - } +internal sealed class PendingSolutionUpdate( + Solution solution, + ImmutableArray projectBaselines, + ImmutableArray deltas, + ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)> nonRemappableRegions) : PendingUpdate(projectBaselines, deltas) +{ + public readonly Solution Solution = solution; + public readonly ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)> Regions)> NonRemappableRegions = nonRemappableRegions; } diff --git a/src/Features/Core/Portable/EditAndContinue/ProjectAnalysisSummary.cs b/src/Features/Core/Portable/EditAndContinue/ProjectAnalysisSummary.cs index d25577fead4db..57b194c1e1de2 100644 --- a/src/Features/Core/Portable/EditAndContinue/ProjectAnalysisSummary.cs +++ b/src/Features/Core/Portable/EditAndContinue/ProjectAnalysisSummary.cs @@ -2,33 +2,32 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal enum ProjectAnalysisSummary { - internal enum ProjectAnalysisSummary - { - /// - /// Project hasn't been changed. - /// - NoChanges, + /// + /// Project hasn't been changed. + /// + NoChanges, - /// - /// Project contains compilation errors that block EnC analysis. - /// - CompilationErrors, + /// + /// Project contains compilation errors that block EnC analysis. + /// + CompilationErrors, - /// - /// Project contains rude edits. - /// - RudeEdits, + /// + /// Project contains rude edits. + /// + RudeEdits, - /// - /// The project only changed in comments, whitespaces, etc. that don't require compilation. - /// - ValidInsignificantChanges, + /// + /// The project only changed in comments, whitespaces, etc. that don't require compilation. + /// + ValidInsignificantChanges, - /// - /// The project contains valid changes that require application of a delta. - /// - ValidChanges - } + /// + /// The project contains valid changes that require application of a delta. + /// + ValidChanges } diff --git a/src/Features/Core/Portable/EditAndContinue/ProjectChanges.cs b/src/Features/Core/Portable/EditAndContinue/ProjectChanges.cs index 653e9aa1a590e..d9b5c6a00baee 100644 --- a/src/Features/Core/Portable/EditAndContinue/ProjectChanges.cs +++ b/src/Features/Core/Portable/EditAndContinue/ProjectChanges.cs @@ -7,52 +7,51 @@ using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal readonly struct ProjectChanges { - internal readonly struct ProjectChanges + /// + /// All semantic changes made in changed documents. + /// + public readonly ImmutableArray SemanticEdits; + + /// + /// All line changes made in changed documents. + /// + public readonly ImmutableArray LineChanges; + + /// + /// All symbols added in changed documents. + /// + public readonly ImmutableHashSet AddedSymbols; + + /// + /// All active statements and the corresponding exception regions in changed documents. + /// + public readonly ImmutableArray ActiveStatementChanges; + + /// + /// Runtime capabilities required to apply the changes. + /// + public readonly EditAndContinueCapabilities RequiredCapabilities; + + public ProjectChanges( + ImmutableArray semanticEdits, + ImmutableArray lineChanges, + ImmutableHashSet addedSymbols, + ImmutableArray activeStatementChanges, + EditAndContinueCapabilities requiredCapabilities) { - /// - /// All semantic changes made in changed documents. - /// - public readonly ImmutableArray SemanticEdits; - - /// - /// All line changes made in changed documents. - /// - public readonly ImmutableArray LineChanges; - - /// - /// All symbols added in changed documents. - /// - public readonly ImmutableHashSet AddedSymbols; - - /// - /// All active statements and the corresponding exception regions in changed documents. - /// - public readonly ImmutableArray ActiveStatementChanges; - - /// - /// Runtime capabilities required to apply the changes. - /// - public readonly EditAndContinueCapabilities RequiredCapabilities; - - public ProjectChanges( - ImmutableArray semanticEdits, - ImmutableArray lineChanges, - ImmutableHashSet addedSymbols, - ImmutableArray activeStatementChanges, - EditAndContinueCapabilities requiredCapabilities) - { - Debug.Assert(!semanticEdits.IsDefault); - Debug.Assert(!lineChanges.IsDefault); - Debug.Assert(!activeStatementChanges.IsDefault); - Debug.Assert(requiredCapabilities != EditAndContinueCapabilities.None); - - SemanticEdits = semanticEdits; - LineChanges = lineChanges; - AddedSymbols = addedSymbols; - ActiveStatementChanges = activeStatementChanges; - RequiredCapabilities = requiredCapabilities; - } + Debug.Assert(!semanticEdits.IsDefault); + Debug.Assert(!lineChanges.IsDefault); + Debug.Assert(!activeStatementChanges.IsDefault); + Debug.Assert(requiredCapabilities != EditAndContinueCapabilities.None); + + SemanticEdits = semanticEdits; + LineChanges = lineChanges; + AddedSymbols = addedSymbols; + ActiveStatementChanges = activeStatementChanges; + RequiredCapabilities = requiredCapabilities; } } diff --git a/src/Features/Core/Portable/EditAndContinue/Remote/ActiveStatementSpanProviderCallback.cs b/src/Features/Core/Portable/EditAndContinue/Remote/ActiveStatementSpanProviderCallback.cs index 1839acd2fb7a0..811315a752577 100644 --- a/src/Features/Core/Portable/EditAndContinue/Remote/ActiveStatementSpanProviderCallback.cs +++ b/src/Features/Core/Portable/EditAndContinue/Remote/ActiveStatementSpanProviderCallback.cs @@ -8,25 +8,24 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.ErrorReporting; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal sealed class ActiveStatementSpanProviderCallback(ActiveStatementSpanProvider provider) { - internal sealed class ActiveStatementSpanProviderCallback(ActiveStatementSpanProvider provider) - { - private readonly ActiveStatementSpanProvider _provider = provider; + private readonly ActiveStatementSpanProvider _provider = provider; - /// - /// Remote API. - /// - public async ValueTask> GetSpansAsync(DocumentId? documentId, string filePath, CancellationToken cancellationToken) + /// + /// Remote API. + /// + public async ValueTask> GetSpansAsync(DocumentId? documentId, string filePath, CancellationToken cancellationToken) + { + try + { + return await _provider(documentId, filePath, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - try - { - return await _provider(documentId, filePath, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return []; - } + return []; } } } diff --git a/src/Features/Core/Portable/EditAndContinue/Remote/IRemoteEditAndContinueService.cs b/src/Features/Core/Portable/EditAndContinue/Remote/IRemoteEditAndContinueService.cs index c8754830204e6..25372a0ed071c 100644 --- a/src/Features/Core/Portable/EditAndContinue/Remote/IRemoteEditAndContinueService.cs +++ b/src/Features/Core/Portable/EditAndContinue/Remote/IRemoteEditAndContinueService.cs @@ -11,44 +11,43 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal interface IRemoteEditAndContinueService { - internal interface IRemoteEditAndContinueService + internal interface ICallback { - internal interface ICallback - { - ValueTask> GetActiveStatementsAsync(RemoteServiceCallbackId callbackId, CancellationToken cancellationToken); - ValueTask GetAvailabilityAsync(RemoteServiceCallbackId callbackId, Guid mvid, CancellationToken cancellationToken); - ValueTask> GetCapabilitiesAsync(RemoteServiceCallbackId callbackId, CancellationToken cancellationToken); - ValueTask PrepareModuleForUpdateAsync(RemoteServiceCallbackId callbackId, Guid mvid, CancellationToken cancellationToken); - - ValueTask> GetSpansAsync(RemoteServiceCallbackId callbackId, DocumentId? documentId, string filePath, CancellationToken cancellationToken); - ValueTask TryGetMatchingSourceTextAsync(RemoteServiceCallbackId callbackId, string filePath, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken); - } - - ValueTask> GetDocumentDiagnosticsAsync(Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DocumentId documentId, CancellationToken cancellationToken); - ValueTask EmitSolutionUpdateAsync(Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DebuggingSessionId sessionId, CancellationToken cancellationToken); - - /// - /// Returns ids of documents for which diagnostics need to be refreshed in-proc. - /// - ValueTask> CommitSolutionUpdateAsync(DebuggingSessionId sessionId, CancellationToken cancellationToken); - ValueTask DiscardSolutionUpdateAsync(DebuggingSessionId sessionId, CancellationToken cancellationToken); - - ValueTask StartDebuggingSessionAsync(Checksum solutionChecksum, RemoteServiceCallbackId callbackId, ImmutableArray captureMatchingDocuments, bool captureAllMatchingDocuments, bool reportDiagnostics, CancellationToken cancellationToken); - - /// - /// Returns ids of documents for which diagnostics need to be refreshed in-proc. - /// - ValueTask> BreakStateOrCapabilitiesChangedAsync(DebuggingSessionId sessionId, bool? isBreakState, CancellationToken cancellationToken); - - /// - /// Returns ids of documents for which diagnostics need to be refreshed in-proc. - /// - ValueTask> EndDebuggingSessionAsync(DebuggingSessionId sessionId, CancellationToken cancellationToken); - ValueTask>> GetBaseActiveStatementSpansAsync(Checksum solutionChecksum, DebuggingSessionId sessionId, ImmutableArray documentIds, CancellationToken cancellationToken); - ValueTask> GetAdjustedActiveStatementSpansAsync(Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DebuggingSessionId sessionId, DocumentId documentId, CancellationToken cancellationToken); - - ValueTask SetFileLoggingDirectoryAsync(string? logDirectory, CancellationToken cancellationToken); + ValueTask> GetActiveStatementsAsync(RemoteServiceCallbackId callbackId, CancellationToken cancellationToken); + ValueTask GetAvailabilityAsync(RemoteServiceCallbackId callbackId, Guid mvid, CancellationToken cancellationToken); + ValueTask> GetCapabilitiesAsync(RemoteServiceCallbackId callbackId, CancellationToken cancellationToken); + ValueTask PrepareModuleForUpdateAsync(RemoteServiceCallbackId callbackId, Guid mvid, CancellationToken cancellationToken); + + ValueTask> GetSpansAsync(RemoteServiceCallbackId callbackId, DocumentId? documentId, string filePath, CancellationToken cancellationToken); + ValueTask TryGetMatchingSourceTextAsync(RemoteServiceCallbackId callbackId, string filePath, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken); } + + ValueTask> GetDocumentDiagnosticsAsync(Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DocumentId documentId, CancellationToken cancellationToken); + ValueTask EmitSolutionUpdateAsync(Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DebuggingSessionId sessionId, CancellationToken cancellationToken); + + /// + /// Returns ids of documents for which diagnostics need to be refreshed in-proc. + /// + ValueTask> CommitSolutionUpdateAsync(DebuggingSessionId sessionId, CancellationToken cancellationToken); + ValueTask DiscardSolutionUpdateAsync(DebuggingSessionId sessionId, CancellationToken cancellationToken); + + ValueTask StartDebuggingSessionAsync(Checksum solutionChecksum, RemoteServiceCallbackId callbackId, ImmutableArray captureMatchingDocuments, bool captureAllMatchingDocuments, bool reportDiagnostics, CancellationToken cancellationToken); + + /// + /// Returns ids of documents for which diagnostics need to be refreshed in-proc. + /// + ValueTask> BreakStateOrCapabilitiesChangedAsync(DebuggingSessionId sessionId, bool? isBreakState, CancellationToken cancellationToken); + + /// + /// Returns ids of documents for which diagnostics need to be refreshed in-proc. + /// + ValueTask> EndDebuggingSessionAsync(DebuggingSessionId sessionId, CancellationToken cancellationToken); + ValueTask>> GetBaseActiveStatementSpansAsync(Checksum solutionChecksum, DebuggingSessionId sessionId, ImmutableArray documentIds, CancellationToken cancellationToken); + ValueTask> GetAdjustedActiveStatementSpansAsync(Checksum solutionChecksum, RemoteServiceCallbackId callbackId, DebuggingSessionId sessionId, DocumentId documentId, CancellationToken cancellationToken); + + ValueTask SetFileLoggingDirectoryAsync(string? logDirectory, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/EditAndContinue/Remote/RemoteDebuggingSessionProxy.cs b/src/Features/Core/Portable/EditAndContinue/Remote/RemoteDebuggingSessionProxy.cs index f93b8f481f6b8..d2b0067d344fb 100644 --- a/src/Features/Core/Portable/EditAndContinue/Remote/RemoteDebuggingSessionProxy.cs +++ b/src/Features/Core/Portable/EditAndContinue/Remote/RemoteDebuggingSessionProxy.cs @@ -15,233 +15,232 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal sealed class RemoteDebuggingSessionProxy(Workspace workspace, IDisposable? connection, DebuggingSessionId sessionId) : IActiveStatementSpanProvider, IDisposable { - internal sealed class RemoteDebuggingSessionProxy(Workspace workspace, IDisposable? connection, DebuggingSessionId sessionId) : IActiveStatementSpanProvider, IDisposable + private readonly IDisposable? _connection = connection; + private readonly DebuggingSessionId _sessionId = sessionId; + private readonly Workspace _workspace = workspace; + + public void Dispose() { - private readonly IDisposable? _connection = connection; - private readonly DebuggingSessionId _sessionId = sessionId; - private readonly Workspace _workspace = workspace; + _connection?.Dispose(); + } + + private IEditAndContinueService GetLocalService() + => _workspace.Services.GetRequiredService().Service; - public void Dispose() + public async ValueTask BreakStateOrCapabilitiesChangedAsync(IDiagnosticAnalyzerService diagnosticService, EditAndContinueDiagnosticUpdateSource diagnosticUpdateSource, bool? inBreakState, CancellationToken cancellationToken) + { + ImmutableArray documentsToReanalyze; + + var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); + if (client == null) { - _connection?.Dispose(); + GetLocalService().BreakStateOrCapabilitiesChanged(_sessionId, inBreakState, out documentsToReanalyze); } + else + { + var documentsToReanalyzeOpt = await client.TryInvokeAsync>( + (service, cancallationToken) => service.BreakStateOrCapabilitiesChangedAsync(_sessionId, inBreakState, cancellationToken), + cancellationToken).ConfigureAwait(false); - private IEditAndContinueService GetLocalService() - => _workspace.Services.GetRequiredService().Service; + documentsToReanalyze = documentsToReanalyzeOpt.HasValue ? documentsToReanalyzeOpt.Value : []; + } - public async ValueTask BreakStateOrCapabilitiesChangedAsync(IDiagnosticAnalyzerService diagnosticService, EditAndContinueDiagnosticUpdateSource diagnosticUpdateSource, bool? inBreakState, CancellationToken cancellationToken) - { - ImmutableArray documentsToReanalyze; + // clear all reported rude edits: + diagnosticService.Reanalyze(_workspace, projectIds: null, documentIds: documentsToReanalyze, highPriority: false); - var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); - if (client == null) - { - GetLocalService().BreakStateOrCapabilitiesChanged(_sessionId, inBreakState, out documentsToReanalyze); - } - else - { - var documentsToReanalyzeOpt = await client.TryInvokeAsync>( - (service, cancallationToken) => service.BreakStateOrCapabilitiesChangedAsync(_sessionId, inBreakState, cancellationToken), - cancellationToken).ConfigureAwait(false); + // clear emit/apply diagnostics reported previously: + diagnosticUpdateSource.ClearDiagnostics(isSessionEnding: false); + } - documentsToReanalyze = documentsToReanalyzeOpt.HasValue ? documentsToReanalyzeOpt.Value : []; - } + public async ValueTask EndDebuggingSessionAsync(Solution compileTimeSolution, EditAndContinueDiagnosticUpdateSource diagnosticUpdateSource, IDiagnosticAnalyzerService diagnosticService, CancellationToken cancellationToken) + { + ImmutableArray documentsToReanalyze; - // clear all reported rude edits: - diagnosticService.Reanalyze(_workspace, projectIds: null, documentIds: documentsToReanalyze, highPriority: false); + var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); + if (client == null) + { + GetLocalService().EndDebuggingSession(_sessionId, out documentsToReanalyze); + } + else + { + var documentsToReanalyzeOpt = await client.TryInvokeAsync>( + (service, cancallationToken) => service.EndDebuggingSessionAsync(_sessionId, cancellationToken), + cancellationToken).ConfigureAwait(false); - // clear emit/apply diagnostics reported previously: - diagnosticUpdateSource.ClearDiagnostics(isSessionEnding: false); + documentsToReanalyze = documentsToReanalyzeOpt.HasValue ? documentsToReanalyzeOpt.Value : []; } - public async ValueTask EndDebuggingSessionAsync(Solution compileTimeSolution, EditAndContinueDiagnosticUpdateSource diagnosticUpdateSource, IDiagnosticAnalyzerService diagnosticService, CancellationToken cancellationToken) - { - ImmutableArray documentsToReanalyze; + var designTimeDocumentsToReanalyze = await CompileTimeSolutionProvider.GetDesignTimeDocumentsAsync( + compileTimeSolution, documentsToReanalyze, designTimeSolution: _workspace.CurrentSolution, cancellationToken).ConfigureAwait(false); + // clear all reported rude edits: + diagnosticService.Reanalyze(_workspace, projectIds: null, documentIds: designTimeDocumentsToReanalyze, highPriority: false); + + // clear emit/apply diagnostics reported previously: + diagnosticUpdateSource.ClearDiagnostics(isSessionEnding: true); + + Dispose(); + } + + public async ValueTask<( + ModuleUpdates updates, + ImmutableArray diagnostics, + ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> rudeEdits, + DiagnosticData? syntaxError)> EmitSolutionUpdateAsync( + Solution solution, + ActiveStatementSpanProvider activeStatementSpanProvider, + IDiagnosticAnalyzerService diagnosticService, + EditAndContinueDiagnosticUpdateSource diagnosticUpdateSource, + CancellationToken cancellationToken) + { + ModuleUpdates moduleUpdates; + ImmutableArray diagnosticData; + ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> rudeEdits; + DiagnosticData? syntaxError; + + try + { var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); if (client == null) { - GetLocalService().EndDebuggingSession(_sessionId, out documentsToReanalyze); + var results = await GetLocalService().EmitSolutionUpdateAsync(_sessionId, solution, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); + moduleUpdates = results.ModuleUpdates; + diagnosticData = results.Diagnostics.ToDiagnosticData(solution); + rudeEdits = results.RudeEdits; + syntaxError = results.GetSyntaxErrorData(solution); } else { - var documentsToReanalyzeOpt = await client.TryInvokeAsync>( - (service, cancallationToken) => service.EndDebuggingSessionAsync(_sessionId, cancellationToken), + var result = await client.TryInvokeAsync( + solution, + (service, solutionInfo, callbackId, cancellationToken) => service.EmitSolutionUpdateAsync(solutionInfo, callbackId, _sessionId, cancellationToken), + callbackTarget: new ActiveStatementSpanProviderCallback(activeStatementSpanProvider), cancellationToken).ConfigureAwait(false); - documentsToReanalyze = documentsToReanalyzeOpt.HasValue ? documentsToReanalyzeOpt.Value : []; - } - - var designTimeDocumentsToReanalyze = await CompileTimeSolutionProvider.GetDesignTimeDocumentsAsync( - compileTimeSolution, documentsToReanalyze, designTimeSolution: _workspace.CurrentSolution, cancellationToken).ConfigureAwait(false); - - // clear all reported rude edits: - diagnosticService.Reanalyze(_workspace, projectIds: null, documentIds: designTimeDocumentsToReanalyze, highPriority: false); - - // clear emit/apply diagnostics reported previously: - diagnosticUpdateSource.ClearDiagnostics(isSessionEnding: true); - - Dispose(); - } - - public async ValueTask<( - ModuleUpdates updates, - ImmutableArray diagnostics, - ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> rudeEdits, - DiagnosticData? syntaxError)> EmitSolutionUpdateAsync( - Solution solution, - ActiveStatementSpanProvider activeStatementSpanProvider, - IDiagnosticAnalyzerService diagnosticService, - EditAndContinueDiagnosticUpdateSource diagnosticUpdateSource, - CancellationToken cancellationToken) - { - ModuleUpdates moduleUpdates; - ImmutableArray diagnosticData; - ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> rudeEdits; - DiagnosticData? syntaxError; - - try - { - var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); - if (client == null) + if (result.HasValue) { - var results = await GetLocalService().EmitSolutionUpdateAsync(_sessionId, solution, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); - moduleUpdates = results.ModuleUpdates; - diagnosticData = results.Diagnostics.ToDiagnosticData(solution); - rudeEdits = results.RudeEdits; - syntaxError = results.GetSyntaxErrorData(solution); + moduleUpdates = result.Value.ModuleUpdates; + diagnosticData = result.Value.Diagnostics; + rudeEdits = result.Value.RudeEdits; + syntaxError = result.Value.SyntaxError; } else { - var result = await client.TryInvokeAsync( - solution, - (service, solutionInfo, callbackId, cancellationToken) => service.EmitSolutionUpdateAsync(solutionInfo, callbackId, _sessionId, cancellationToken), - callbackTarget: new ActiveStatementSpanProviderCallback(activeStatementSpanProvider), - cancellationToken).ConfigureAwait(false); - - if (result.HasValue) - { - moduleUpdates = result.Value.ModuleUpdates; - diagnosticData = result.Value.Diagnostics; - rudeEdits = result.Value.RudeEdits; - syntaxError = result.Value.SyntaxError; - } - else - { - moduleUpdates = new ModuleUpdates(ModuleUpdateStatus.RestartRequired, []); - diagnosticData = []; - rudeEdits = []; - syntaxError = null; - } + moduleUpdates = new ModuleUpdates(ModuleUpdateStatus.RestartRequired, []); + diagnosticData = []; + rudeEdits = []; + syntaxError = null; } } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - diagnosticData = GetInternalErrorDiagnosticData(solution, e); - rudeEdits = []; - moduleUpdates = new ModuleUpdates(ModuleUpdateStatus.RestartRequired, []); - syntaxError = null; - } + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + diagnosticData = GetInternalErrorDiagnosticData(solution, e); + rudeEdits = []; + moduleUpdates = new ModuleUpdates(ModuleUpdateStatus.RestartRequired, []); + syntaxError = null; + } - // clear emit/apply diagnostics reported previously: - diagnosticUpdateSource.ClearDiagnostics(isSessionEnding: false); + // clear emit/apply diagnostics reported previously: + diagnosticUpdateSource.ClearDiagnostics(isSessionEnding: false); - // clear all reported rude edits: - diagnosticService.Reanalyze(_workspace, projectIds: null, documentIds: rudeEdits.Select(d => d.DocumentId), highPriority: false); + // clear all reported rude edits: + diagnosticService.Reanalyze(_workspace, projectIds: null, documentIds: rudeEdits.Select(d => d.DocumentId), highPriority: false); - // report emit/apply diagnostics: - diagnosticUpdateSource.ReportDiagnostics(_workspace, solution, diagnosticData, rudeEdits); + // report emit/apply diagnostics: + diagnosticUpdateSource.ReportDiagnostics(_workspace, solution, diagnosticData, rudeEdits); - return (moduleUpdates, diagnosticData, rudeEdits, syntaxError); - } + return (moduleUpdates, diagnosticData, rudeEdits, syntaxError); + } - private static ImmutableArray GetInternalErrorDiagnosticData(Solution solution, Exception e) - { - var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(RudeEditKind.InternalError); + private static ImmutableArray GetInternalErrorDiagnosticData(Solution solution, Exception e) + { + var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(RudeEditKind.InternalError); - var diagnostic = Diagnostic.Create( - descriptor, - Location.None, - string.Format(descriptor.MessageFormat.ToString(), "", e.Message)); + var diagnostic = Diagnostic.Create( + descriptor, + Location.None, + string.Format(descriptor.MessageFormat.ToString(), "", e.Message)); - return [DiagnosticData.Create(solution, diagnostic, project: null)]; - } + return [DiagnosticData.Create(solution, diagnostic, project: null)]; + } - public async ValueTask CommitSolutionUpdateAsync(IDiagnosticAnalyzerService diagnosticService, CancellationToken cancellationToken) + public async ValueTask CommitSolutionUpdateAsync(IDiagnosticAnalyzerService diagnosticService, CancellationToken cancellationToken) + { + ImmutableArray documentsToReanalyze; + + var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); + if (client == null) { - ImmutableArray documentsToReanalyze; + GetLocalService().CommitSolutionUpdate(_sessionId, out documentsToReanalyze); + } + else + { + var documentsToReanalyzeOpt = await client.TryInvokeAsync>( + (service, cancallationToken) => service.CommitSolutionUpdateAsync(_sessionId, cancellationToken), + cancellationToken).ConfigureAwait(false); - var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); - if (client == null) - { - GetLocalService().CommitSolutionUpdate(_sessionId, out documentsToReanalyze); - } - else - { - var documentsToReanalyzeOpt = await client.TryInvokeAsync>( - (service, cancallationToken) => service.CommitSolutionUpdateAsync(_sessionId, cancellationToken), - cancellationToken).ConfigureAwait(false); + documentsToReanalyze = documentsToReanalyzeOpt.HasValue ? documentsToReanalyzeOpt.Value : []; + } - documentsToReanalyze = documentsToReanalyzeOpt.HasValue ? documentsToReanalyzeOpt.Value : []; - } + // clear all reported rude edits: + diagnosticService.Reanalyze(_workspace, projectIds: null, documentIds: documentsToReanalyze, highPriority: false); + } - // clear all reported rude edits: - diagnosticService.Reanalyze(_workspace, projectIds: null, documentIds: documentsToReanalyze, highPriority: false); + public async ValueTask DiscardSolutionUpdateAsync(CancellationToken cancellationToken) + { + var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); + if (client == null) + { + GetLocalService().DiscardSolutionUpdate(_sessionId); + return; } - public async ValueTask DiscardSolutionUpdateAsync(CancellationToken cancellationToken) - { - var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); - if (client == null) - { - GetLocalService().DiscardSolutionUpdate(_sessionId); - return; - } + await client.TryInvokeAsync( + (service, cancellationToken) => service.DiscardSolutionUpdateAsync(_sessionId, cancellationToken), + cancellationToken).ConfigureAwait(false); + } - await client.TryInvokeAsync( - (service, cancellationToken) => service.DiscardSolutionUpdateAsync(_sessionId, cancellationToken), - cancellationToken).ConfigureAwait(false); + public async ValueTask>> GetBaseActiveStatementSpansAsync(Solution solution, ImmutableArray documentIds, CancellationToken cancellationToken) + { + var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); + if (client == null) + { + return await GetLocalService().GetBaseActiveStatementSpansAsync(_sessionId, solution, documentIds, cancellationToken).ConfigureAwait(false); } - public async ValueTask>> GetBaseActiveStatementSpansAsync(Solution solution, ImmutableArray documentIds, CancellationToken cancellationToken) - { - var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); - if (client == null) - { - return await GetLocalService().GetBaseActiveStatementSpansAsync(_sessionId, solution, documentIds, cancellationToken).ConfigureAwait(false); - } + var result = await client.TryInvokeAsync>>( + solution, + (service, solutionInfo, cancellationToken) => service.GetBaseActiveStatementSpansAsync(solutionInfo, _sessionId, documentIds, cancellationToken), + cancellationToken).ConfigureAwait(false); - var result = await client.TryInvokeAsync>>( - solution, - (service, solutionInfo, cancellationToken) => service.GetBaseActiveStatementSpansAsync(solutionInfo, _sessionId, documentIds, cancellationToken), - cancellationToken).ConfigureAwait(false); + return result.HasValue ? result.Value : []; + } - return result.HasValue ? result.Value : []; + public async ValueTask> GetAdjustedActiveStatementSpansAsync(TextDocument document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + { + // filter out documents that are not synchronized to remote process before we attempt remote invoke: + if (!RemoteSupportedLanguages.IsSupported(document.Project.Language)) + { + return []; } - public async ValueTask> GetAdjustedActiveStatementSpansAsync(TextDocument document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); + if (client == null) { - // filter out documents that are not synchronized to remote process before we attempt remote invoke: - if (!RemoteSupportedLanguages.IsSupported(document.Project.Language)) - { - return []; - } - - var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); - if (client == null) - { - return await GetLocalService().GetAdjustedActiveStatementSpansAsync(_sessionId, document, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); - } + return await GetLocalService().GetAdjustedActiveStatementSpansAsync(_sessionId, document, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); + } - var result = await client.TryInvokeAsync>( - document.Project.Solution, - (service, solutionInfo, callbackId, cancellationToken) => service.GetAdjustedActiveStatementSpansAsync(solutionInfo, callbackId, _sessionId, document.Id, cancellationToken), - callbackTarget: new ActiveStatementSpanProviderCallback(activeStatementSpanProvider), - cancellationToken).ConfigureAwait(false); + var result = await client.TryInvokeAsync>( + document.Project.Solution, + (service, solutionInfo, callbackId, cancellationToken) => service.GetAdjustedActiveStatementSpansAsync(solutionInfo, callbackId, _sessionId, document.Id, cancellationToken), + callbackTarget: new ActiveStatementSpanProviderCallback(activeStatementSpanProvider), + cancellationToken).ConfigureAwait(false); - return result.HasValue ? result.Value : []; - } + return result.HasValue ? result.Value : []; } } diff --git a/src/Features/Core/Portable/EditAndContinue/Remote/RemoteEditAndContinueServiceProxy.cs b/src/Features/Core/Portable/EditAndContinue/Remote/RemoteEditAndContinueServiceProxy.cs index 74983553f7c9c..9e93e6c963a0f 100644 --- a/src/Features/Core/Portable/EditAndContinue/Remote/RemoteEditAndContinueServiceProxy.cs +++ b/src/Features/Core/Portable/EditAndContinue/Remote/RemoteEditAndContinueServiceProxy.cs @@ -16,243 +16,242 @@ using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Facade used to call remote methods. +/// Encapsulates all RPC logic as well as dispatching to the local service if the remote service is disabled. +/// THe facade is useful for targeted testing of serialization/deserialization of EnC service calls. +/// +internal readonly partial struct RemoteEditAndContinueServiceProxy(Workspace workspace) { - /// - /// Facade used to call remote methods. - /// Encapsulates all RPC logic as well as dispatching to the local service if the remote service is disabled. - /// THe facade is useful for targeted testing of serialization/deserialization of EnC service calls. - /// - internal readonly partial struct RemoteEditAndContinueServiceProxy(Workspace workspace) + [ExportRemoteServiceCallbackDispatcher(typeof(IRemoteEditAndContinueService)), Shared] + internal sealed class CallbackDispatcher : RemoteServiceCallbackDispatcher, IRemoteEditAndContinueService.ICallback { - [ExportRemoteServiceCallbackDispatcher(typeof(IRemoteEditAndContinueService)), Shared] - internal sealed class CallbackDispatcher : RemoteServiceCallbackDispatcher, IRemoteEditAndContinueService.ICallback + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CallbackDispatcher() { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CallbackDispatcher() - { - } + } - public ValueTask> GetSpansAsync(RemoteServiceCallbackId callbackId, DocumentId? documentId, string filePath, CancellationToken cancellationToken) - => ((ActiveStatementSpanProviderCallback)GetCallback(callbackId)).GetSpansAsync(documentId, filePath, cancellationToken); + public ValueTask> GetSpansAsync(RemoteServiceCallbackId callbackId, DocumentId? documentId, string filePath, CancellationToken cancellationToken) + => ((ActiveStatementSpanProviderCallback)GetCallback(callbackId)).GetSpansAsync(documentId, filePath, cancellationToken); - public ValueTask TryGetMatchingSourceTextAsync(RemoteServiceCallbackId callbackId, string filePath, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) - => ((DebuggingSessionCallback)GetCallback(callbackId)).TryGetMatchingSourceTextAsync(filePath, requiredChecksum, checksumAlgorithm, cancellationToken); + public ValueTask TryGetMatchingSourceTextAsync(RemoteServiceCallbackId callbackId, string filePath, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + => ((DebuggingSessionCallback)GetCallback(callbackId)).TryGetMatchingSourceTextAsync(filePath, requiredChecksum, checksumAlgorithm, cancellationToken); - public ValueTask> GetActiveStatementsAsync(RemoteServiceCallbackId callbackId, CancellationToken cancellationToken) - => ((DebuggingSessionCallback)GetCallback(callbackId)).GetActiveStatementsAsync(cancellationToken); + public ValueTask> GetActiveStatementsAsync(RemoteServiceCallbackId callbackId, CancellationToken cancellationToken) + => ((DebuggingSessionCallback)GetCallback(callbackId)).GetActiveStatementsAsync(cancellationToken); - public ValueTask GetAvailabilityAsync(RemoteServiceCallbackId callbackId, Guid mvid, CancellationToken cancellationToken) - => ((DebuggingSessionCallback)GetCallback(callbackId)).GetAvailabilityAsync(mvid, cancellationToken); + public ValueTask GetAvailabilityAsync(RemoteServiceCallbackId callbackId, Guid mvid, CancellationToken cancellationToken) + => ((DebuggingSessionCallback)GetCallback(callbackId)).GetAvailabilityAsync(mvid, cancellationToken); - public ValueTask> GetCapabilitiesAsync(RemoteServiceCallbackId callbackId, CancellationToken cancellationToken) - => ((DebuggingSessionCallback)GetCallback(callbackId)).GetCapabilitiesAsync(cancellationToken); + public ValueTask> GetCapabilitiesAsync(RemoteServiceCallbackId callbackId, CancellationToken cancellationToken) + => ((DebuggingSessionCallback)GetCallback(callbackId)).GetCapabilitiesAsync(cancellationToken); - public ValueTask PrepareModuleForUpdateAsync(RemoteServiceCallbackId callbackId, Guid mvid, CancellationToken cancellationToken) - => ((DebuggingSessionCallback)GetCallback(callbackId)).PrepareModuleForUpdateAsync(mvid, cancellationToken); - } + public ValueTask PrepareModuleForUpdateAsync(RemoteServiceCallbackId callbackId, Guid mvid, CancellationToken cancellationToken) + => ((DebuggingSessionCallback)GetCallback(callbackId)).PrepareModuleForUpdateAsync(mvid, cancellationToken); + } - private sealed class DebuggingSessionCallback(IManagedHotReloadService debuggerService, IPdbMatchingSourceTextProvider sourceTextProvider) - { - private readonly IManagedHotReloadService _debuggerService = debuggerService; - private readonly IPdbMatchingSourceTextProvider _sourceTextProvider = sourceTextProvider; + private sealed class DebuggingSessionCallback(IManagedHotReloadService debuggerService, IPdbMatchingSourceTextProvider sourceTextProvider) + { + private readonly IManagedHotReloadService _debuggerService = debuggerService; + private readonly IPdbMatchingSourceTextProvider _sourceTextProvider = sourceTextProvider; - public async ValueTask TryGetMatchingSourceTextAsync(string filePath, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + public async ValueTask TryGetMatchingSourceTextAsync(string filePath, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + { + try { - try - { - return await _sourceTextProvider.TryGetMatchingSourceTextAsync(filePath, requiredChecksum, checksumAlgorithm, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return null; - } + return await _sourceTextProvider.TryGetMatchingSourceTextAsync(filePath, requiredChecksum, checksumAlgorithm, cancellationToken).ConfigureAwait(false); } - - public async ValueTask> GetActiveStatementsAsync(CancellationToken cancellationToken) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - try - { - return await _debuggerService.GetActiveStatementsAsync(cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return []; - } + return null; } + } - public async ValueTask GetAvailabilityAsync(Guid mvid, CancellationToken cancellationToken) + public async ValueTask> GetActiveStatementsAsync(CancellationToken cancellationToken) + { + try { - try - { - return await _debuggerService.GetAvailabilityAsync(mvid, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return new ManagedHotReloadAvailability(ManagedHotReloadAvailabilityStatus.InternalError, e.Message); - } + return await _debuggerService.GetActiveStatementsAsync(cancellationToken).ConfigureAwait(false); } - - public async ValueTask PrepareModuleForUpdateAsync(Guid mvid, CancellationToken cancellationToken) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - try - { - await _debuggerService.PrepareModuleForUpdateAsync(mvid, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - // nop - } + return []; } + } - public async ValueTask> GetCapabilitiesAsync(CancellationToken cancellationToken) + public async ValueTask GetAvailabilityAsync(Guid mvid, CancellationToken cancellationToken) + { + try { - try - { - return await _debuggerService.GetCapabilitiesAsync(cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return []; - } + return await _debuggerService.GetAvailabilityAsync(mvid, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + return new ManagedHotReloadAvailability(ManagedHotReloadAvailabilityStatus.InternalError, e.Message); } } - public readonly Workspace Workspace = workspace; - - private IEditAndContinueService GetLocalService() - => Workspace.Services.GetRequiredService().Service; - - public async ValueTask StartDebuggingSessionAsync( - Solution solution, - IManagedHotReloadService debuggerService, - IPdbMatchingSourceTextProvider sourceTextProvider, - ImmutableArray captureMatchingDocuments, - bool captureAllMatchingDocuments, - bool reportDiagnostics, - CancellationToken cancellationToken) + public async ValueTask PrepareModuleForUpdateAsync(Guid mvid, CancellationToken cancellationToken) { - var client = await RemoteHostClient.TryGetClientAsync(Workspace, cancellationToken).ConfigureAwait(false); - if (client == null) + try { - var sessionId = await GetLocalService().StartDebuggingSessionAsync(solution, debuggerService, sourceTextProvider, captureMatchingDocuments, captureAllMatchingDocuments, reportDiagnostics, cancellationToken).ConfigureAwait(false); - return new RemoteDebuggingSessionProxy(Workspace, LocalConnection.Instance, sessionId); + await _debuggerService.PrepareModuleForUpdateAsync(mvid, cancellationToken).ConfigureAwait(false); } - - // need to keep the providers alive until the session ends: - var connection = client.CreateConnection( - callbackTarget: new DebuggingSessionCallback(debuggerService, sourceTextProvider)); - - var sessionIdOpt = await connection.TryInvokeAsync( - solution, - async (service, solutionInfo, callbackId, cancellationToken) => await service.StartDebuggingSessionAsync(solutionInfo, callbackId, captureMatchingDocuments, captureAllMatchingDocuments, reportDiagnostics, cancellationToken).ConfigureAwait(false), - cancellationToken).ConfigureAwait(false); - - if (sessionIdOpt.HasValue) + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - return new RemoteDebuggingSessionProxy(Workspace, connection, sessionIdOpt.Value); + // nop } - - connection.Dispose(); - return null; } - public async ValueTask> GetDocumentDiagnosticsAsync(Document document, Document designTimeDocument, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + public async ValueTask> GetCapabilitiesAsync(CancellationToken cancellationToken) { - // filter out documents that are not synchronized to remote process before we attempt remote invoke: - if (!RemoteSupportedLanguages.IsSupported(document.Project.Language)) + try + { + return await _debuggerService.GetCapabilitiesAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { return []; } + } + } - var client = await RemoteHostClient.TryGetClientAsync(Workspace, cancellationToken).ConfigureAwait(false); - if (client == null) - { - var diagnostics = await GetLocalService().GetDocumentDiagnosticsAsync(document, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); + public readonly Workspace Workspace = workspace; - if (designTimeDocument != document) - { - diagnostics = diagnostics.SelectAsArray( - diagnostic => RemapLocation(designTimeDocument, DiagnosticData.Create(document.Project.Solution, diagnostic, document.Project))); - } + private IEditAndContinueService GetLocalService() + => Workspace.Services.GetRequiredService().Service; - return diagnostics; - } + public async ValueTask StartDebuggingSessionAsync( + Solution solution, + IManagedHotReloadService debuggerService, + IPdbMatchingSourceTextProvider sourceTextProvider, + ImmutableArray captureMatchingDocuments, + bool captureAllMatchingDocuments, + bool reportDiagnostics, + CancellationToken cancellationToken) + { + var client = await RemoteHostClient.TryGetClientAsync(Workspace, cancellationToken).ConfigureAwait(false); + if (client == null) + { + var sessionId = await GetLocalService().StartDebuggingSessionAsync(solution, debuggerService, sourceTextProvider, captureMatchingDocuments, captureAllMatchingDocuments, reportDiagnostics, cancellationToken).ConfigureAwait(false); + return new RemoteDebuggingSessionProxy(Workspace, LocalConnection.Instance, sessionId); + } - var diagnosticData = await client.TryInvokeAsync>( - document.Project.Solution, - (service, solutionInfo, callbackId, cancellationToken) => service.GetDocumentDiagnosticsAsync(solutionInfo, callbackId, document.Id, cancellationToken), - callbackTarget: new ActiveStatementSpanProviderCallback(activeStatementSpanProvider), - cancellationToken).ConfigureAwait(false); + // need to keep the providers alive until the session ends: + var connection = client.CreateConnection( + callbackTarget: new DebuggingSessionCallback(debuggerService, sourceTextProvider)); - if (!diagnosticData.HasValue) - { - return []; - } + var sessionIdOpt = await connection.TryInvokeAsync( + solution, + async (service, solutionInfo, callbackId, cancellationToken) => await service.StartDebuggingSessionAsync(solutionInfo, callbackId, captureMatchingDocuments, captureAllMatchingDocuments, reportDiagnostics, cancellationToken).ConfigureAwait(false), + cancellationToken).ConfigureAwait(false); - var project = document.Project; + if (sessionIdOpt.HasValue) + { + return new RemoteDebuggingSessionProxy(Workspace, connection, sessionIdOpt.Value); + } - using var _ = ArrayBuilder.GetInstance(out var result); - foreach (var data in diagnosticData.Value) - { - Debug.Assert(data.DataLocation != null); - - Diagnostic diagnostic; - - // Workaround for solution crawler not supporting mapped locations to make Razor work. - // We pretend the diagnostic is in the original document, but use the mapped line span. - // Razor will ignore the column (which will be off because #line directives can't currently map columns) and only use the line number. - if (designTimeDocument != document) - { - diagnostic = RemapLocation(designTimeDocument, data); - } - else - { - diagnostic = await data.ToDiagnosticAsync(document.Project, cancellationToken).ConfigureAwait(false); - } - - result.Add(diagnostic); - } + connection.Dispose(); + return null; + } - return result.ToImmutable(); + public async ValueTask> GetDocumentDiagnosticsAsync(Document document, Document designTimeDocument, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + { + // filter out documents that are not synchronized to remote process before we attempt remote invoke: + if (!RemoteSupportedLanguages.IsSupported(document.Project.Language)) + { + return []; } - private static Diagnostic RemapLocation(Document designTimeDocument, DiagnosticData data) + var client = await RemoteHostClient.TryGetClientAsync(Workspace, cancellationToken).ConfigureAwait(false); + if (client == null) { - Debug.Assert(data.DataLocation != null); - Debug.Assert(designTimeDocument.FilePath != null); + var diagnostics = await GetLocalService().GetDocumentDiagnosticsAsync(document, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); - // If the location in the generated document is in a scope of user-visible #line mapping use the mapped span, - // otherwise (if it's hidden) display the diagnostic at the start of the file. - var span = data.DataLocation.UnmappedFileSpan != data.DataLocation.MappedFileSpan ? data.DataLocation.MappedFileSpan.Span : default; - var location = Location.Create(designTimeDocument.FilePath, textSpan: default, span); + if (designTimeDocument != document) + { + diagnostics = diagnostics.SelectAsArray( + diagnostic => RemapLocation(designTimeDocument, DiagnosticData.Create(document.Project.Solution, diagnostic, document.Project))); + } - return data.ToDiagnostic(location, []); + return diagnostics; } - public async ValueTask SetFileLoggingDirectoryAsync(string? logDirectory, CancellationToken cancellationToken) + var diagnosticData = await client.TryInvokeAsync>( + document.Project.Solution, + (service, solutionInfo, callbackId, cancellationToken) => service.GetDocumentDiagnosticsAsync(solutionInfo, callbackId, document.Id, cancellationToken), + callbackTarget: new ActiveStatementSpanProviderCallback(activeStatementSpanProvider), + cancellationToken).ConfigureAwait(false); + + if (!diagnosticData.HasValue) + { + return []; + } + + var project = document.Project; + + using var _ = ArrayBuilder.GetInstance(out var result); + foreach (var data in diagnosticData.Value) { - var client = await RemoteHostClient.TryGetClientAsync(Workspace, cancellationToken).ConfigureAwait(false); - if (client == null) + Debug.Assert(data.DataLocation != null); + + Diagnostic diagnostic; + + // Workaround for solution crawler not supporting mapped locations to make Razor work. + // We pretend the diagnostic is in the original document, but use the mapped line span. + // Razor will ignore the column (which will be off because #line directives can't currently map columns) and only use the line number. + if (designTimeDocument != document) { - GetLocalService().SetFileLoggingDirectory(logDirectory); + diagnostic = RemapLocation(designTimeDocument, data); } else { - await client.TryInvokeAsync( - (service, cancellationToken) => service.SetFileLoggingDirectoryAsync(logDirectory, cancellationToken), - cancellationToken).ConfigureAwait(false); + diagnostic = await data.ToDiagnosticAsync(document.Project, cancellationToken).ConfigureAwait(false); } + + result.Add(diagnostic); } - private sealed class LocalConnection : IDisposable + return result.ToImmutable(); + } + + private static Diagnostic RemapLocation(Document designTimeDocument, DiagnosticData data) + { + Debug.Assert(data.DataLocation != null); + Debug.Assert(designTimeDocument.FilePath != null); + + // If the location in the generated document is in a scope of user-visible #line mapping use the mapped span, + // otherwise (if it's hidden) display the diagnostic at the start of the file. + var span = data.DataLocation.UnmappedFileSpan != data.DataLocation.MappedFileSpan ? data.DataLocation.MappedFileSpan.Span : default; + var location = Location.Create(designTimeDocument.FilePath, textSpan: default, span); + + return data.ToDiagnostic(location, []); + } + + public async ValueTask SetFileLoggingDirectoryAsync(string? logDirectory, CancellationToken cancellationToken) + { + var client = await RemoteHostClient.TryGetClientAsync(Workspace, cancellationToken).ConfigureAwait(false); + if (client == null) + { + GetLocalService().SetFileLoggingDirectory(logDirectory); + } + else { - public static readonly LocalConnection Instance = new(); + await client.TryInvokeAsync( + (service, cancellationToken) => service.SetFileLoggingDirectoryAsync(logDirectory, cancellationToken), + cancellationToken).ConfigureAwait(false); + } + } - public void Dispose() - { - } + private sealed class LocalConnection : IDisposable + { + public static readonly LocalConnection Instance = new(); + + public void Dispose() + { } } } diff --git a/src/Features/Core/Portable/EditAndContinue/RudeEditDiagnostic.cs b/src/Features/Core/Portable/EditAndContinue/RudeEditDiagnostic.cs index d5c61186a49fe..bc02952a2a205 100644 --- a/src/Features/Core/Portable/EditAndContinue/RudeEditDiagnostic.cs +++ b/src/Features/Core/Portable/EditAndContinue/RudeEditDiagnostic.cs @@ -6,40 +6,39 @@ using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[DataContract] +internal readonly struct RudeEditDiagnostic { - [DataContract] - internal readonly struct RudeEditDiagnostic + [DataMember(Order = 0)] + public readonly RudeEditKind Kind; + + [DataMember(Order = 1)] + public readonly TextSpan Span; + + [DataMember(Order = 2)] + public readonly ushort SyntaxKind; + + [DataMember(Order = 3)] + public readonly string?[] Arguments; + + internal RudeEditDiagnostic(RudeEditKind kind, TextSpan span, ushort syntaxKind, string?[] arguments) + { + Kind = kind; + Span = span; + SyntaxKind = syntaxKind; + Arguments = arguments; + } + + internal RudeEditDiagnostic(RudeEditKind kind, TextSpan span, SyntaxNode? node = null, string?[]? arguments = null) + : this(kind, span, (ushort)(node != null ? node.RawKind : 0), arguments ?? []) + { + } + + internal Diagnostic ToDiagnostic(SyntaxTree tree) { - [DataMember(Order = 0)] - public readonly RudeEditKind Kind; - - [DataMember(Order = 1)] - public readonly TextSpan Span; - - [DataMember(Order = 2)] - public readonly ushort SyntaxKind; - - [DataMember(Order = 3)] - public readonly string?[] Arguments; - - internal RudeEditDiagnostic(RudeEditKind kind, TextSpan span, ushort syntaxKind, string?[] arguments) - { - Kind = kind; - Span = span; - SyntaxKind = syntaxKind; - Arguments = arguments; - } - - internal RudeEditDiagnostic(RudeEditKind kind, TextSpan span, SyntaxNode? node = null, string?[]? arguments = null) - : this(kind, span, (ushort)(node != null ? node.RawKind : 0), arguments ?? []) - { - } - - internal Diagnostic ToDiagnostic(SyntaxTree tree) - { - var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(Kind); - return Diagnostic.Create(descriptor, tree.GetLocation(Span), Arguments); - } + var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(Kind); + return Diagnostic.Create(descriptor, tree.GetLocation(Span), Arguments); } } diff --git a/src/Features/Core/Portable/EditAndContinue/RudeEditKind.cs b/src/Features/Core/Portable/EditAndContinue/RudeEditKind.cs index eb8abc1410ea5..e7aeb09fff695 100644 --- a/src/Features/Core/Portable/EditAndContinue/RudeEditKind.cs +++ b/src/Features/Core/Portable/EditAndContinue/RudeEditKind.cs @@ -2,145 +2,144 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +// TELEMETRY: DO NOT MODIFY ANY ENUM VALUES OF THIS ENUM. +// IT WILL BREAK OUR SQM VARIABLE MAPPINGS. + +internal enum RudeEditKind : ushort { - // TELEMETRY: DO NOT MODIFY ANY ENUM VALUES OF THIS ENUM. - // IT WILL BREAK OUR SQM VARIABLE MAPPINGS. - - internal enum RudeEditKind : ushort - { - None = 0, - - ActiveStatementUpdate = 1, - ActiveStatementLambdaRemoved = 2, - - Update = 3, - ModifiersUpdate = 4, - HandlesClauseUpdate = 5, - ImplementsClauseUpdate = 6, - VarianceUpdate = 7, - FieldKindUpdate = 8, - TypeUpdate = 9, - //ConstraintKindUpdate = 10, - InitializerUpdate = 11, - FixedSizeFieldUpdate = 12, - EnumUnderlyingTypeUpdate = 13, - BaseTypeOrInterfaceUpdate = 14, - TypeKindUpdate = 15, - AccessorKindUpdate = 16, - //MethodKindUpdate = 17, - DeclareLibraryUpdate = 18, - DeclareAliasUpdate = 19, - Renamed = 20, - Insert = 21, - // InsertNonPrivate = 22, - InsertVirtual = 23, - InsertOverridable = 24, - InsertExtern = 25, - InsertOperator = 26, - // InsertNonPublicConstructor = 27, - // InsertGenericMethod = 28, - InsertDllImport = 29, - InsertIntoStruct = 30, - InsertIntoClassWithLayout = 31, - Move = 32, - Delete = 33, - // MethodBodyAdd = 34, - // MethodBodyDelete = 35, - GenericMethodUpdate = 36, - // GenericMethodTriviaUpdate = 37, - GenericTypeUpdate = 38, - // GenericTypeTriviaUpdate = 39, - // GenericTypeInitializerUpdate = 40, - // PartialTypeInitializerUpdate = 41, - // AsyncMethodUpdate = 42, - // AsyncMethodTriviaUpdate = 43, - StackAllocUpdate = 44, - - ExperimentalFeaturesEnabled = 45, - - AwaitStatementUpdate = 46, - ChangingAccessibility = 47, - - // CapturingVariable = 48, - // NotCapturingVariable = 49, - // DeletingCapturedVariable = 50, - ChangingCapturedVariableType = 51, - ChangingCapturedVariableScope = 52, - ChangingLambdaParameters = 53, - ChangingLambdaReturnType = 54, - // AccessingCapturedVariableInLambda = 55, - // NotAccessingCapturedVariableInLambda = 56, - // InsertLambdaWithMultiScopeCapture = 57, - // DeleteLambdaWithMultiScopeCapture = 58, - ChangingQueryLambdaType = 59, - - InsertAroundActiveStatement = 60, - DeleteAroundActiveStatement = 61, - DeleteActiveStatement = 62, - UpdateAroundActiveStatement = 63, - UpdateExceptionHandlerOfActiveTry = 64, - UpdateTryOrCatchWithActiveFinally = 65, - UpdateCatchHandlerAroundActiveStatement = 66, - UpdateStaticLocal = 67, - - InsertConstructorToTypeWithInitializersWithLambdas = 68, - RenamingCapturedVariable = 69, - - InsertHandlesClause = 70, - InsertFile = 71, - PartiallyExecutedActiveStatementUpdate = 72, - PartiallyExecutedActiveStatementDelete = 73, - UpdatingStateMachineMethodAroundActiveStatement = 74, - UpdatingStateMachineMethodMissingAttribute = 75, - - SwitchBetweenLambdaAndLocalFunction = 76, - //RefStruct = 77, - //ReadOnlyStruct = 78, - //ReadOnlyReferences = 79, - - InternalError = 80, - - InsertMethodWithExplicitInterfaceSpecifier = 81, - InsertIntoInterface = 82, - InsertLocalFunctionIntoInterfaceMethod = 83, - //SwitchExpressionUpdate = 84, - ChangingFromAsynchronousToSynchronous = 85, - ChangingStateMachineShape = 86, - - // Chagned from 0x103 in 16.1 and from 82 to 87 in 16.8 - ComplexQueryExpression = 87, - - MemberBodyInternalError = 88, - SourceFileTooBig = 89, - MemberBodyTooBig = 90, - // InsertIntoGenericType = 91, - - //ImplementRecordParameterAsReadOnly = 92, - //ImplementRecordParameterWithSet = 93, - //AddRecordPositionalParameter = 94, - //DeleteRecordPositionalParameter = 95, - //ExplicitRecordMethodParameterNamesMustMatch = 96, - NotSupportedByRuntime = 97, - MakeMethodAsyncNotSupportedByRuntime = 98, - MakeMethodIteratorNotSupportedByRuntime = 99, - InsertNotSupportedByRuntime = 100, - ChangingAttributesNotSupportedByRuntime = 101, - ChangeImplicitMainReturnType = 102, - ChangingParameterTypes = 103, - ChangingTypeParameters = 104, - ChangingConstraints = 105, - ChangingReloadableTypeNotSupportedByRuntime = 106, - RenamingNotSupportedByRuntime = 107, - ChangingNonCustomAttribute = 108, - ChangingNamespace = 109, - ChangingSignatureNotSupportedByRuntime = 110, - DeleteNotSupportedByRuntime = 111, - UpdatingStateMachineMethodNotSupportedByRuntime = 112, - UpdatingGenericNotSupportedByRuntime = 113, - CapturingPrimaryConstructorParameter = 114, - NotCapturingPrimaryConstructorParameter = 115, - ChangingAttribute = 116, - ChangingNameOrSignatureOfActiveMember = 117, - } + None = 0, + + ActiveStatementUpdate = 1, + ActiveStatementLambdaRemoved = 2, + + Update = 3, + ModifiersUpdate = 4, + HandlesClauseUpdate = 5, + ImplementsClauseUpdate = 6, + VarianceUpdate = 7, + FieldKindUpdate = 8, + TypeUpdate = 9, + //ConstraintKindUpdate = 10, + InitializerUpdate = 11, + FixedSizeFieldUpdate = 12, + EnumUnderlyingTypeUpdate = 13, + BaseTypeOrInterfaceUpdate = 14, + TypeKindUpdate = 15, + AccessorKindUpdate = 16, + //MethodKindUpdate = 17, + DeclareLibraryUpdate = 18, + DeclareAliasUpdate = 19, + Renamed = 20, + Insert = 21, + // InsertNonPrivate = 22, + InsertVirtual = 23, + InsertOverridable = 24, + InsertExtern = 25, + InsertOperator = 26, + // InsertNonPublicConstructor = 27, + // InsertGenericMethod = 28, + InsertDllImport = 29, + InsertIntoStruct = 30, + InsertIntoClassWithLayout = 31, + Move = 32, + Delete = 33, + // MethodBodyAdd = 34, + // MethodBodyDelete = 35, + GenericMethodUpdate = 36, + // GenericMethodTriviaUpdate = 37, + GenericTypeUpdate = 38, + // GenericTypeTriviaUpdate = 39, + // GenericTypeInitializerUpdate = 40, + // PartialTypeInitializerUpdate = 41, + // AsyncMethodUpdate = 42, + // AsyncMethodTriviaUpdate = 43, + StackAllocUpdate = 44, + + ExperimentalFeaturesEnabled = 45, + + AwaitStatementUpdate = 46, + ChangingAccessibility = 47, + + // CapturingVariable = 48, + // NotCapturingVariable = 49, + // DeletingCapturedVariable = 50, + ChangingCapturedVariableType = 51, + ChangingCapturedVariableScope = 52, + ChangingLambdaParameters = 53, + ChangingLambdaReturnType = 54, + // AccessingCapturedVariableInLambda = 55, + // NotAccessingCapturedVariableInLambda = 56, + // InsertLambdaWithMultiScopeCapture = 57, + // DeleteLambdaWithMultiScopeCapture = 58, + ChangingQueryLambdaType = 59, + + InsertAroundActiveStatement = 60, + DeleteAroundActiveStatement = 61, + DeleteActiveStatement = 62, + UpdateAroundActiveStatement = 63, + UpdateExceptionHandlerOfActiveTry = 64, + UpdateTryOrCatchWithActiveFinally = 65, + UpdateCatchHandlerAroundActiveStatement = 66, + UpdateStaticLocal = 67, + + InsertConstructorToTypeWithInitializersWithLambdas = 68, + RenamingCapturedVariable = 69, + + InsertHandlesClause = 70, + InsertFile = 71, + PartiallyExecutedActiveStatementUpdate = 72, + PartiallyExecutedActiveStatementDelete = 73, + UpdatingStateMachineMethodAroundActiveStatement = 74, + UpdatingStateMachineMethodMissingAttribute = 75, + + SwitchBetweenLambdaAndLocalFunction = 76, + //RefStruct = 77, + //ReadOnlyStruct = 78, + //ReadOnlyReferences = 79, + + InternalError = 80, + + InsertMethodWithExplicitInterfaceSpecifier = 81, + InsertIntoInterface = 82, + InsertLocalFunctionIntoInterfaceMethod = 83, + //SwitchExpressionUpdate = 84, + ChangingFromAsynchronousToSynchronous = 85, + ChangingStateMachineShape = 86, + + // Chagned from 0x103 in 16.1 and from 82 to 87 in 16.8 + ComplexQueryExpression = 87, + + MemberBodyInternalError = 88, + SourceFileTooBig = 89, + MemberBodyTooBig = 90, + // InsertIntoGenericType = 91, + + //ImplementRecordParameterAsReadOnly = 92, + //ImplementRecordParameterWithSet = 93, + //AddRecordPositionalParameter = 94, + //DeleteRecordPositionalParameter = 95, + //ExplicitRecordMethodParameterNamesMustMatch = 96, + NotSupportedByRuntime = 97, + MakeMethodAsyncNotSupportedByRuntime = 98, + MakeMethodIteratorNotSupportedByRuntime = 99, + InsertNotSupportedByRuntime = 100, + ChangingAttributesNotSupportedByRuntime = 101, + ChangeImplicitMainReturnType = 102, + ChangingParameterTypes = 103, + ChangingTypeParameters = 104, + ChangingConstraints = 105, + ChangingReloadableTypeNotSupportedByRuntime = 106, + RenamingNotSupportedByRuntime = 107, + ChangingNonCustomAttribute = 108, + ChangingNamespace = 109, + ChangingSignatureNotSupportedByRuntime = 110, + DeleteNotSupportedByRuntime = 111, + UpdatingStateMachineMethodNotSupportedByRuntime = 112, + UpdatingGenericNotSupportedByRuntime = 113, + CapturingPrimaryConstructorParameter = 114, + NotCapturingPrimaryConstructorParameter = 115, + ChangingAttribute = 116, + ChangingNameOrSignatureOfActiveMember = 117, } diff --git a/src/Features/Core/Portable/EditAndContinue/SemanticEditInfo.cs b/src/Features/Core/Portable/EditAndContinue/SemanticEditInfo.cs index cdcc15717bbce..f8b0ee33f870f 100644 --- a/src/Features/Core/Portable/EditAndContinue/SemanticEditInfo.cs +++ b/src/Features/Core/Portable/EditAndContinue/SemanticEditInfo.cs @@ -7,100 +7,99 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.Emit; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal readonly record struct SyntaxMaps { - internal readonly record struct SyntaxMaps + /// + /// The tree the maps operate on (the new tree, since the maps are mapping from new nodes to old nodes/rude edits). + /// + public readonly SyntaxTree NewTree; + + public readonly Func? MatchingNodes; + public readonly Func? RuntimeRudeEdits; + + public SyntaxMaps( + SyntaxTree newTree, + Func? matchingNodes = null, + Func? runtimeRudeEdits = null) { - /// - /// The tree the maps operate on (the new tree, since the maps are mapping from new nodes to old nodes/rude edits). - /// - public readonly SyntaxTree NewTree; - - public readonly Func? MatchingNodes; - public readonly Func? RuntimeRudeEdits; - - public SyntaxMaps( - SyntaxTree newTree, - Func? matchingNodes = null, - Func? runtimeRudeEdits = null) - { - // if we have runtime rude edit map we should also have matching node map: - Debug.Assert(runtimeRudeEdits == null || matchingNodes != null); - - NewTree = newTree; - MatchingNodes = matchingNodes; - RuntimeRudeEdits = runtimeRudeEdits; - } - - [MemberNotNullWhen(true, nameof(MatchingNodes))] - public bool HasMap => MatchingNodes != null; + // if we have runtime rude edit map we should also have matching node map: + Debug.Assert(runtimeRudeEdits == null || matchingNodes != null); + + NewTree = newTree; + MatchingNodes = matchingNodes; + RuntimeRudeEdits = runtimeRudeEdits; } - internal readonly struct SemanticEditInfo + [MemberNotNullWhen(true, nameof(MatchingNodes))] + public bool HasMap => MatchingNodes != null; +} + +internal readonly struct SemanticEditInfo +{ + public SemanticEditInfo( + SemanticEditKind kind, + SymbolKey symbol, + SyntaxMaps syntaxMaps, + SymbolKey? partialType, + SymbolKey? deletedSymbolContainer) { - public SemanticEditInfo( - SemanticEditKind kind, - SymbolKey symbol, - SyntaxMaps syntaxMaps, - SymbolKey? partialType, - SymbolKey? deletedSymbolContainer) - { - Debug.Assert(kind == SemanticEditKind.Delete || deletedSymbolContainer == null); - - Kind = kind; - Symbol = symbol; - SyntaxMaps = syntaxMaps; - PartialType = partialType; - DeletedSymbolContainer = deletedSymbolContainer; - } - - public static SemanticEditInfo CreateInsert(SymbolKey symbol, SymbolKey? partialType) - => new(SemanticEditKind.Insert, symbol, syntaxMaps: default, partialType, deletedSymbolContainer: null); - - public static SemanticEditInfo CreateUpdate(SymbolKey symbol, SyntaxMaps syntaxMaps, SymbolKey? partialType) - => new(SemanticEditKind.Update, symbol, syntaxMaps, partialType, deletedSymbolContainer: null); - - public static SemanticEditInfo CreateReplace(SymbolKey symbol, SymbolKey? partialType) - => new(SemanticEditKind.Replace, symbol, syntaxMaps: default, partialType, deletedSymbolContainer: null); - - public static SemanticEditInfo CreateDelete(SymbolKey symbol, SymbolKey deletedSymbolContainer, SymbolKey? partialType) - => new(SemanticEditKind.Delete, symbol, syntaxMaps: default, partialType, deletedSymbolContainer); - - /// - /// or or . - /// - public SemanticEditKind Kind { get; } - - /// - /// If is represents the inserted symbol in the new compilation. - /// If is represents the updated symbol in both compilations. - /// If is represents the deleted symbol in the old compilation. - /// - /// We use to represent the symbol rather then , - /// since different semantic edits might have been calculated against different solution snapshot and thus symbols are not directly comparable. - /// When the edits are processed we map the to the current compilation. - /// - public SymbolKey Symbol { get; } - - /// - /// If is represents the containing symbol in the new compilation. - /// - /// We use to represent the symbol rather then , - /// since different semantic edits might have been calculated against different solution snapshot and thus symbols are not directly comparable. - /// When the edits are processed we map the to the current compilation. - /// - public SymbolKey? DeletedSymbolContainer { get; } - - /// - /// Syntax maps for nodes in the tree for this edit, which will be merged with other maps from other trees for this type. - /// - public SyntaxMaps SyntaxMaps { get; } - - /// - /// Specified if the edit needs to be merged with other edits of the same . - /// - /// If specified, the is either null or incomplete: it only provides mapping of the changed members of a single partial type declaration. - /// - public SymbolKey? PartialType { get; } + Debug.Assert(kind == SemanticEditKind.Delete || deletedSymbolContainer == null); + + Kind = kind; + Symbol = symbol; + SyntaxMaps = syntaxMaps; + PartialType = partialType; + DeletedSymbolContainer = deletedSymbolContainer; } + + public static SemanticEditInfo CreateInsert(SymbolKey symbol, SymbolKey? partialType) + => new(SemanticEditKind.Insert, symbol, syntaxMaps: default, partialType, deletedSymbolContainer: null); + + public static SemanticEditInfo CreateUpdate(SymbolKey symbol, SyntaxMaps syntaxMaps, SymbolKey? partialType) + => new(SemanticEditKind.Update, symbol, syntaxMaps, partialType, deletedSymbolContainer: null); + + public static SemanticEditInfo CreateReplace(SymbolKey symbol, SymbolKey? partialType) + => new(SemanticEditKind.Replace, symbol, syntaxMaps: default, partialType, deletedSymbolContainer: null); + + public static SemanticEditInfo CreateDelete(SymbolKey symbol, SymbolKey deletedSymbolContainer, SymbolKey? partialType) + => new(SemanticEditKind.Delete, symbol, syntaxMaps: default, partialType, deletedSymbolContainer); + + /// + /// or or . + /// + public SemanticEditKind Kind { get; } + + /// + /// If is represents the inserted symbol in the new compilation. + /// If is represents the updated symbol in both compilations. + /// If is represents the deleted symbol in the old compilation. + /// + /// We use to represent the symbol rather then , + /// since different semantic edits might have been calculated against different solution snapshot and thus symbols are not directly comparable. + /// When the edits are processed we map the to the current compilation. + /// + public SymbolKey Symbol { get; } + + /// + /// If is represents the containing symbol in the new compilation. + /// + /// We use to represent the symbol rather then , + /// since different semantic edits might have been calculated against different solution snapshot and thus symbols are not directly comparable. + /// When the edits are processed we map the to the current compilation. + /// + public SymbolKey? DeletedSymbolContainer { get; } + + /// + /// Syntax maps for nodes in the tree for this edit, which will be merged with other maps from other trees for this type. + /// + public SyntaxMaps SyntaxMaps { get; } + + /// + /// Specified if the edit needs to be merged with other edits of the same . + /// + /// If specified, the is either null or incomplete: it only provides mapping of the changed members of a single partial type declaration. + /// + public SymbolKey? PartialType { get; } } diff --git a/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs b/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs index 97365d58f95e9..11efbf736617d 100644 --- a/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs +++ b/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs @@ -8,65 +8,64 @@ using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using Microsoft.CodeAnalysis.Diagnostics; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal readonly struct SolutionUpdate( + ModuleUpdates moduleUpdates, + ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)> nonRemappableRegions, + ImmutableArray projectBaselines, + ImmutableArray diagnostics, + ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> documentsWithRudeEdits, + Diagnostic? syntaxError) { - internal readonly struct SolutionUpdate( - ModuleUpdates moduleUpdates, - ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)> nonRemappableRegions, - ImmutableArray projectBaselines, + public readonly ModuleUpdates ModuleUpdates = moduleUpdates; + public readonly ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)> NonRemappableRegions = nonRemappableRegions; + public readonly ImmutableArray ProjectBaselines = projectBaselines; + public readonly ImmutableArray Diagnostics = diagnostics; + public readonly ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> DocumentsWithRudeEdits = documentsWithRudeEdits; + public readonly Diagnostic? SyntaxError = syntaxError; + + public static SolutionUpdate Blocked( ImmutableArray diagnostics, - ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> documentsWithRudeEdits, - Diagnostic? syntaxError) - { - public readonly ModuleUpdates ModuleUpdates = moduleUpdates; - public readonly ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)> NonRemappableRegions = nonRemappableRegions; - public readonly ImmutableArray ProjectBaselines = projectBaselines; - public readonly ImmutableArray Diagnostics = diagnostics; - public readonly ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> DocumentsWithRudeEdits = documentsWithRudeEdits; - public readonly Diagnostic? SyntaxError = syntaxError; + ImmutableArray<(DocumentId, ImmutableArray)> documentsWithRudeEdits, + Diagnostic? syntaxError, + bool hasEmitErrors) + => new( + new(syntaxError != null || hasEmitErrors ? ModuleUpdateStatus.Blocked : ModuleUpdateStatus.RestartRequired, []), + ImmutableArray<(Guid, ImmutableArray<(ManagedModuleMethodId, NonRemappableRegion)>)>.Empty, + [], + diagnostics, + documentsWithRudeEdits, + syntaxError); - public static SolutionUpdate Blocked( - ImmutableArray diagnostics, - ImmutableArray<(DocumentId, ImmutableArray)> documentsWithRudeEdits, - Diagnostic? syntaxError, - bool hasEmitErrors) - => new( - new(syntaxError != null || hasEmitErrors ? ModuleUpdateStatus.Blocked : ModuleUpdateStatus.RestartRequired, []), - ImmutableArray<(Guid, ImmutableArray<(ManagedModuleMethodId, NonRemappableRegion)>)>.Empty, - [], - diagnostics, - documentsWithRudeEdits, - syntaxError); + internal void Log(TraceLog log, UpdateId updateId) + { + log.Write("Solution update {0}.{1} status: {2}", updateId.SessionId.Ordinal, updateId.Ordinal, ModuleUpdates.Status); - internal void Log(TraceLog log, UpdateId updateId) + foreach (var moduleUpdate in ModuleUpdates.Updates) { - log.Write("Solution update {0}.{1} status: {2}", updateId.SessionId.Ordinal, updateId.Ordinal, ModuleUpdates.Status); - - foreach (var moduleUpdate in ModuleUpdates.Updates) - { - log.Write("Module update: capabilities=[{0}], types=[{1}], methods=[{2}]", - moduleUpdate.RequiredCapabilities, - moduleUpdate.UpdatedTypes, - moduleUpdate.UpdatedMethods); - } + log.Write("Module update: capabilities=[{0}], types=[{1}], methods=[{2}]", + moduleUpdate.RequiredCapabilities, + moduleUpdate.UpdatedTypes, + moduleUpdate.UpdatedMethods); + } - foreach (var projectDiagnostics in Diagnostics) + foreach (var projectDiagnostics in Diagnostics) + { + foreach (var diagnostic in projectDiagnostics.Diagnostics) { - foreach (var diagnostic in projectDiagnostics.Diagnostics) + if (diagnostic.Severity == DiagnosticSeverity.Error) { - if (diagnostic.Severity == DiagnosticSeverity.Error) - { - log.Write("Project {0} update error: {1}", projectDiagnostics.ProjectId, diagnostic); - } + log.Write("Project {0} update error: {1}", projectDiagnostics.ProjectId, diagnostic); } } + } - foreach (var documentWithRudeEdits in DocumentsWithRudeEdits) + foreach (var documentWithRudeEdits in DocumentsWithRudeEdits) + { + foreach (var rudeEdit in documentWithRudeEdits.Diagnostics) { - foreach (var rudeEdit in documentWithRudeEdits.Diagnostics) - { - log.Write("Document {0} rude edit: {1} {2}", documentWithRudeEdits.DocumentId, rudeEdit.Kind, rudeEdit.SyntaxKind); - } + log.Write("Document {0} rude edit: {1} {2}", documentWithRudeEdits.DocumentId, rudeEdit.Kind, rudeEdit.SyntaxKind); } } } diff --git a/src/Features/Core/Portable/EditAndContinue/SourceFileSpan.cs b/src/Features/Core/Portable/EditAndContinue/SourceFileSpan.cs index b915a0b07cb63..f32bc34bc75fd 100644 --- a/src/Features/Core/Portable/EditAndContinue/SourceFileSpan.cs +++ b/src/Features/Core/Portable/EditAndContinue/SourceFileSpan.cs @@ -7,82 +7,81 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Represents a span of text in a source code file in terms of file name, line number, and offset within line. +/// An alternative for without bit. +/// +/// +/// Initializes the instance. +/// +/// The file identifier - typically a relative or absolute path. +/// The span. +/// is null. +[DataContract] +internal readonly struct SourceFileSpan(string path, LinePositionSpan span) : IEquatable { /// - /// Represents a span of text in a source code file in terms of file name, line number, and offset within line. - /// An alternative for without bit. + /// Path, or null if the span represents an invalid value. /// /// - /// Initializes the instance. + /// Path may be if not available. /// - /// The file identifier - typically a relative or absolute path. - /// The span. - /// is null. - [DataContract] - internal readonly struct SourceFileSpan(string path, LinePositionSpan span) : IEquatable - { - /// - /// Path, or null if the span represents an invalid value. - /// - /// - /// Path may be if not available. - /// - [DataMember(Order = 0)] - public string Path { get; } = path ?? throw new ArgumentNullException(nameof(path)); - - /// - /// Gets the span. - /// - [DataMember(Order = 1)] - public LinePositionSpan Span { get; } = span; - - public SourceFileSpan WithSpan(LinePositionSpan span) - => new(Path, span); - - public SourceFileSpan WithPath(string path) - => new(path, Span); - - /// - /// Returns true if the span represents a valid location. - /// - public bool IsValid - => Path != null; // invalid span can be constructed by new SourceFileSpan() - - /// - /// Gets the of the start of the span. - /// - public LinePosition Start - => Span.Start; - - /// - /// Gets the of the end of the span. - /// - public LinePosition End - => Span.End; - - public bool Equals(SourceFileSpan other) - => Span.Equals(other.Span) && string.Equals(Path, other.Path, StringComparison.Ordinal); - - public override bool Equals(object? other) - => other is SourceFileSpan span && Equals(span); - - public override int GetHashCode() - => Hash.Combine(Path, Span.GetHashCode()); - - public override string ToString() - => string.IsNullOrEmpty(Path) ? Span.ToString() : $"{Path}: {Span}"; - - public static implicit operator SourceFileSpan(FileLinePositionSpan span) - => new(span.Path, span.Span); - - public static bool operator ==(SourceFileSpan left, SourceFileSpan right) - => left.Equals(right); - - public static bool operator !=(SourceFileSpan left, SourceFileSpan right) - => !(left == right); - - public bool Contains(SourceFileSpan span) - => Span.Contains(span.Span) && string.Equals(Path, span.Path, StringComparison.Ordinal); - } + [DataMember(Order = 0)] + public string Path { get; } = path ?? throw new ArgumentNullException(nameof(path)); + + /// + /// Gets the span. + /// + [DataMember(Order = 1)] + public LinePositionSpan Span { get; } = span; + + public SourceFileSpan WithSpan(LinePositionSpan span) + => new(Path, span); + + public SourceFileSpan WithPath(string path) + => new(path, Span); + + /// + /// Returns true if the span represents a valid location. + /// + public bool IsValid + => Path != null; // invalid span can be constructed by new SourceFileSpan() + + /// + /// Gets the of the start of the span. + /// + public LinePosition Start + => Span.Start; + + /// + /// Gets the of the end of the span. + /// + public LinePosition End + => Span.End; + + public bool Equals(SourceFileSpan other) + => Span.Equals(other.Span) && string.Equals(Path, other.Path, StringComparison.Ordinal); + + public override bool Equals(object? other) + => other is SourceFileSpan span && Equals(span); + + public override int GetHashCode() + => Hash.Combine(Path, Span.GetHashCode()); + + public override string ToString() + => string.IsNullOrEmpty(Path) ? Span.ToString() : $"{Path}: {Span}"; + + public static implicit operator SourceFileSpan(FileLinePositionSpan span) + => new(span.Path, span.Span); + + public static bool operator ==(SourceFileSpan left, SourceFileSpan right) + => left.Equals(right); + + public static bool operator !=(SourceFileSpan left, SourceFileSpan right) + => !(left == right); + + public bool Contains(SourceFileSpan span) + => Span.Contains(span.Span) && string.Equals(Path, span.Path, StringComparison.Ordinal); } diff --git a/src/Features/Core/Portable/EditAndContinue/TraceLog.cs b/src/Features/Core/Portable/EditAndContinue/TraceLog.cs index f89486fe9ba13..8c0afaf35a6fd 100644 --- a/src/Features/Core/Portable/EditAndContinue/TraceLog.cs +++ b/src/Features/Core/Portable/EditAndContinue/TraceLog.cs @@ -14,260 +14,259 @@ using System.Threading.Tasks; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +/// +/// Fixed size rolling tracing log. +/// +/// +/// Recent entries are captured in a memory dump. +/// If DEBUG is defined, all entries written to or +/// are print to output. +/// +internal sealed class TraceLog(int logSize, string id, string fileName) { - /// - /// Fixed size rolling tracing log. - /// - /// - /// Recent entries are captured in a memory dump. - /// If DEBUG is defined, all entries written to or - /// are print to output. - /// - internal sealed class TraceLog(int logSize, string id, string fileName) + internal readonly struct Arg { - internal readonly struct Arg + // To display enums in Expression Evaluator we need to remember the type of the enum. + // The debugger currently does not support evaluating expressions that involve Type instances nor lambdas, + // so we need to manually special case the types of enums we care about displaying. + + private enum EnumType { - // To display enums in Expression Evaluator we need to remember the type of the enum. - // The debugger currently does not support evaluating expressions that involve Type instances nor lambdas, - // so we need to manually special case the types of enums we care about displaying. + ProjectAnalysisSummary, + RudeEditKind, + ModuleUpdateStatus, + EditAndContinueCapabilities, + } - private enum EnumType - { - ProjectAnalysisSummary, - RudeEditKind, - ModuleUpdateStatus, - EditAndContinueCapabilities, - } + private static readonly StrongBox s_ProjectAnalysisSummary = new(EnumType.ProjectAnalysisSummary); + private static readonly StrongBox s_RudeEditKind = new(EnumType.RudeEditKind); + private static readonly StrongBox s_ModuleUpdateStatus = new(EnumType.ModuleUpdateStatus); + private static readonly StrongBox s_EditAndContinueCapabilities = new(EnumType.EditAndContinueCapabilities); - private static readonly StrongBox s_ProjectAnalysisSummary = new(EnumType.ProjectAnalysisSummary); - private static readonly StrongBox s_RudeEditKind = new(EnumType.RudeEditKind); - private static readonly StrongBox s_ModuleUpdateStatus = new(EnumType.ModuleUpdateStatus); - private static readonly StrongBox s_EditAndContinueCapabilities = new(EnumType.EditAndContinueCapabilities); + public readonly object? Object; + public readonly int Int32; + public readonly ImmutableArray Tokens; - public readonly object? Object; - public readonly int Int32; - public readonly ImmutableArray Tokens; + public Arg(object? value) + { + Int32 = -1; + Object = value ?? ""; + Tokens = default; + } - public Arg(object? value) - { - Int32 = -1; - Object = value ?? ""; - Tokens = default; - } + public Arg(ImmutableArray tokens) + { + Int32 = -1; + Object = null; + Tokens = tokens; + } + + private Arg(int value, StrongBox enumKind) + { + Int32 = value; + Object = enumKind; + Tokens = default; + } + + public object? GetDebuggerDisplay() + => (!Tokens.IsDefault) ? string.Join(",", Tokens.Select(token => token.ToString("X8"))) : + (Object is ImmutableArray array) ? string.Join(",", array) : + (Object is null) ? Int32 : + (Object is StrongBox { Value: var enumType }) ? enumType switch + { + EnumType.ProjectAnalysisSummary => (ProjectAnalysisSummary)Int32, + EnumType.RudeEditKind => (RudeEditKind)Int32, + EnumType.ModuleUpdateStatus => (ModuleUpdateStatus)Int32, + EnumType.EditAndContinueCapabilities => (EditAndContinueCapabilities)Int32, + _ => throw ExceptionUtilities.UnexpectedValue(enumType) + } : + Object; + + public static implicit operator Arg(string? value) => new(value); + public static implicit operator Arg(int value) => new(value); + public static implicit operator Arg(bool value) => new(value ? "true" : "false"); + public static implicit operator Arg(ProjectId value) => new(value.DebugName); + public static implicit operator Arg(DocumentId value) => new(value.DebugName); + public static implicit operator Arg(Diagnostic value) => new(value.ToString()); + public static implicit operator Arg(ProjectAnalysisSummary value) => new((int)value, s_ProjectAnalysisSummary); + public static implicit operator Arg(RudeEditKind value) => new((int)value, s_RudeEditKind); + public static implicit operator Arg(ModuleUpdateStatus value) => new((int)value, s_ModuleUpdateStatus); + public static implicit operator Arg(EditAndContinueCapabilities value) => new((int)value, s_EditAndContinueCapabilities); + public static implicit operator Arg(ImmutableArray tokens) => new(tokens); + public static implicit operator Arg(ImmutableArray items) => new(items); + } + + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] + internal readonly struct Entry(string format, Arg[]? args) + { + public readonly string MessageFormat = format; + public readonly Arg[]? Args = args; + + internal string GetDebuggerDisplay() + => (MessageFormat == null) ? "" : string.Format(MessageFormat, Args?.Select(a => a.GetDebuggerDisplay()).ToArray() ?? []); + } + + internal sealed class FileLogger(string logDirectory, TraceLog traceLog) + { + private readonly string _logDirectory = logDirectory; + private readonly TraceLog _traceLog = traceLog; + + public void Append(Entry entry) + { + string? path = null; - public Arg(ImmutableArray tokens) + try { - Int32 = -1; - Object = null; - Tokens = tokens; + path = Path.Combine(_logDirectory, _traceLog._fileName); + File.AppendAllLines(path, [entry.GetDebuggerDisplay()]); } - - private Arg(int value, StrongBox enumKind) + catch (Exception e) { - Int32 = value; - Object = enumKind; - Tokens = default; + _traceLog.AppendFileLoggingErrorInMemory(path, e); } + } - public object? GetDebuggerDisplay() - => (!Tokens.IsDefault) ? string.Join(",", Tokens.Select(token => token.ToString("X8"))) : - (Object is ImmutableArray array) ? string.Join(",", array) : - (Object is null) ? Int32 : - (Object is StrongBox { Value: var enumType }) ? enumType switch - { - EnumType.ProjectAnalysisSummary => (ProjectAnalysisSummary)Int32, - EnumType.RudeEditKind => (RudeEditKind)Int32, - EnumType.ModuleUpdateStatus => (ModuleUpdateStatus)Int32, - EnumType.EditAndContinueCapabilities => (EditAndContinueCapabilities)Int32, - _ => throw ExceptionUtilities.UnexpectedValue(enumType) - } : - Object; - - public static implicit operator Arg(string? value) => new(value); - public static implicit operator Arg(int value) => new(value); - public static implicit operator Arg(bool value) => new(value ? "true" : "false"); - public static implicit operator Arg(ProjectId value) => new(value.DebugName); - public static implicit operator Arg(DocumentId value) => new(value.DebugName); - public static implicit operator Arg(Diagnostic value) => new(value.ToString()); - public static implicit operator Arg(ProjectAnalysisSummary value) => new((int)value, s_ProjectAnalysisSummary); - public static implicit operator Arg(RudeEditKind value) => new((int)value, s_RudeEditKind); - public static implicit operator Arg(ModuleUpdateStatus value) => new((int)value, s_ModuleUpdateStatus); - public static implicit operator Arg(EditAndContinueCapabilities value) => new((int)value, s_EditAndContinueCapabilities); - public static implicit operator Arg(ImmutableArray tokens) => new(tokens); - public static implicit operator Arg(ImmutableArray items) => new(items); + private string CreateSessionDirectory(DebuggingSessionId sessionId, string relativePath) + { + Contract.ThrowIfNull(_logDirectory); + var directory = Path.Combine(_logDirectory, sessionId.Ordinal.ToString(), relativePath); + Directory.CreateDirectory(directory); + return directory; } - [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] - internal readonly struct Entry(string format, Arg[]? args) + private string MakeSourceFileLogPath(Document document, string suffix, UpdateId updateId, int? generation) { - public readonly string MessageFormat = format; - public readonly Arg[]? Args = args; + Debug.Assert(document.FilePath != null); + Debug.Assert(document.Project.FilePath != null); + + var projectDir = PathUtilities.GetDirectoryName(document.Project.FilePath)!; + var documentDir = PathUtilities.GetDirectoryName(document.FilePath)!; + var extension = PathUtilities.GetExtension(document.FilePath); + var fileName = PathUtilities.GetFileName(document.FilePath, includeExtension: false); + + var relativeDir = PathUtilities.IsSameDirectoryOrChildOf(documentDir, projectDir) ? PathUtilities.GetRelativePath(projectDir, documentDir) : documentDir; + relativeDir = relativeDir.Replace('\\', '_').Replace('/', '_'); - internal string GetDebuggerDisplay() - => (MessageFormat == null) ? "" : string.Format(MessageFormat, Args?.Select(a => a.GetDebuggerDisplay()).ToArray() ?? []); + var directory = CreateSessionDirectory(updateId.SessionId, Path.Combine(document.Project.Name, relativeDir)); + return Path.Combine(directory, $"{fileName}.{updateId.Ordinal}.{generation?.ToString() ?? "-"}.{suffix}{extension}"); } - internal sealed class FileLogger(string logDirectory, TraceLog traceLog) + public void Write(DebuggingSessionId sessionId, ImmutableArray bytes, string directory, string fileName) { - private readonly string _logDirectory = logDirectory; - private readonly TraceLog _traceLog = traceLog; - - public void Append(Entry entry) + string? path = null; + try { - string? path = null; - - try - { - path = Path.Combine(_logDirectory, _traceLog._fileName); - File.AppendAllLines(path, [entry.GetDebuggerDisplay()]); - } - catch (Exception e) - { - _traceLog.AppendFileLoggingErrorInMemory(path, e); - } + path = Path.Combine(CreateSessionDirectory(sessionId, directory), fileName); + File.WriteAllBytes(path, bytes.ToArray()); } - - private string CreateSessionDirectory(DebuggingSessionId sessionId, string relativePath) + catch (Exception e) { - Contract.ThrowIfNull(_logDirectory); - var directory = Path.Combine(_logDirectory, sessionId.Ordinal.ToString(), relativePath); - Directory.CreateDirectory(directory); - return directory; + _traceLog.AppendFileLoggingErrorInMemory(path, e); } + } - private string MakeSourceFileLogPath(Document document, string suffix, UpdateId updateId, int? generation) + public async ValueTask WriteAsync(Func writer, DebuggingSessionId sessionId, string directory, string fileName, CancellationToken cancellationToken) + { + string? path = null; + try { - Debug.Assert(document.FilePath != null); - Debug.Assert(document.Project.FilePath != null); - - var projectDir = PathUtilities.GetDirectoryName(document.Project.FilePath)!; - var documentDir = PathUtilities.GetDirectoryName(document.FilePath)!; - var extension = PathUtilities.GetExtension(document.FilePath); - var fileName = PathUtilities.GetFileName(document.FilePath, includeExtension: false); - - var relativeDir = PathUtilities.IsSameDirectoryOrChildOf(documentDir, projectDir) ? PathUtilities.GetRelativePath(projectDir, documentDir) : documentDir; - relativeDir = relativeDir.Replace('\\', '_').Replace('/', '_'); - - var directory = CreateSessionDirectory(updateId.SessionId, Path.Combine(document.Project.Name, relativeDir)); - return Path.Combine(directory, $"{fileName}.{updateId.Ordinal}.{generation?.ToString() ?? "-"}.{suffix}{extension}"); + path = Path.Combine(CreateSessionDirectory(sessionId, directory), fileName); + using var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write | FileShare.Delete); + await writer(file, cancellationToken).ConfigureAwait(false); } - - public void Write(DebuggingSessionId sessionId, ImmutableArray bytes, string directory, string fileName) + catch (Exception e) { - string? path = null; - try - { - path = Path.Combine(CreateSessionDirectory(sessionId, directory), fileName); - File.WriteAllBytes(path, bytes.ToArray()); - } - catch (Exception e) - { - _traceLog.AppendFileLoggingErrorInMemory(path, e); - } + _traceLog.AppendFileLoggingErrorInMemory(path, e); } + } + + public async ValueTask WriteDocumentAsync(Document document, string fileNameSuffix, UpdateId updateId, int? generation, CancellationToken cancellationToken) + { + Debug.Assert(document.FilePath != null); - public async ValueTask WriteAsync(Func writer, DebuggingSessionId sessionId, string directory, string fileName, CancellationToken cancellationToken) + string? path = null; + try + { + path = MakeSourceFileLogPath(document, fileNameSuffix, updateId, generation); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + using var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write | FileShare.Delete); + using var writer = new StreamWriter(file, text.Encoding ?? Encoding.UTF8); + text.Write(writer, cancellationToken); + } + catch (Exception e) { - string? path = null; - try - { - path = Path.Combine(CreateSessionDirectory(sessionId, directory), fileName); - using var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write | FileShare.Delete); - await writer(file, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) - { - _traceLog.AppendFileLoggingErrorInMemory(path, e); - } + _traceLog.AppendFileLoggingErrorInMemory(path, e); } + } - public async ValueTask WriteDocumentAsync(Document document, string fileNameSuffix, UpdateId updateId, int? generation, CancellationToken cancellationToken) + public async ValueTask WriteDocumentChangeAsync(Document? oldDocument, Document? newDocument, UpdateId updateId, int? generation, CancellationToken cancellationToken) + { + if (oldDocument?.FilePath != null) { - Debug.Assert(document.FilePath != null); - - string? path = null; - try - { - path = MakeSourceFileLogPath(document, fileNameSuffix, updateId, generation); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - using var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write | FileShare.Delete); - using var writer = new StreamWriter(file, text.Encoding ?? Encoding.UTF8); - text.Write(writer, cancellationToken); - } - catch (Exception e) - { - _traceLog.AppendFileLoggingErrorInMemory(path, e); - } + await WriteDocumentAsync(oldDocument, fileNameSuffix: "old", updateId, generation, cancellationToken).ConfigureAwait(false); } - public async ValueTask WriteDocumentChangeAsync(Document? oldDocument, Document? newDocument, UpdateId updateId, int? generation, CancellationToken cancellationToken) + if (newDocument?.FilePath != null) { - if (oldDocument?.FilePath != null) - { - await WriteDocumentAsync(oldDocument, fileNameSuffix: "old", updateId, generation, cancellationToken).ConfigureAwait(false); - } - - if (newDocument?.FilePath != null) - { - await WriteDocumentAsync(newDocument, fileNameSuffix: "new", updateId, generation, cancellationToken).ConfigureAwait(false); - } + await WriteDocumentAsync(newDocument, fileNameSuffix: "new", updateId, generation, cancellationToken).ConfigureAwait(false); } } + } - private readonly Entry[] _log = new Entry[logSize]; - private readonly string _id = id; - private readonly string _fileName = fileName; - private int _currentLine; + private readonly Entry[] _log = new Entry[logSize]; + private readonly string _id = id; + private readonly string _fileName = fileName; + private int _currentLine; - public FileLogger? FileLog { get; private set; } + public FileLogger? FileLog { get; private set; } - public void SetLogDirectory(string? logDirectory) - { - FileLog = (logDirectory != null) ? new FileLogger(logDirectory, this) : null; - } + public void SetLogDirectory(string? logDirectory) + { + FileLog = (logDirectory != null) ? new FileLogger(logDirectory, this) : null; + } - private void AppendInMemory(Entry entry) - { - var index = Interlocked.Increment(ref _currentLine); - _log[(index - 1) % _log.Length] = entry; - } + private void AppendInMemory(Entry entry) + { + var index = Interlocked.Increment(ref _currentLine); + _log[(index - 1) % _log.Length] = entry; + } - private void AppendFileLoggingErrorInMemory(string? path, Exception e) - => AppendInMemory(new Entry("Error writing log file '{0}': {1}", [new Arg(path), new Arg(e.Message)])); + private void AppendFileLoggingErrorInMemory(string? path, Exception e) + => AppendInMemory(new Entry("Error writing log file '{0}': {1}", [new Arg(path), new Arg(e.Message)])); - private void Append(Entry entry) - { - AppendInMemory(entry); - FileLog?.Append(entry); - } + private void Append(Entry entry) + { + AppendInMemory(entry); + FileLog?.Append(entry); + } - public void Write(string str) - => Write(str, args: null); + public void Write(string str) + => Write(str, args: null); - public void Write(string format, params Arg[]? args) - => Append(new Entry(format, args)); + public void Write(string format, params Arg[]? args) + => Append(new Entry(format, args)); - [Conditional("DEBUG")] - public void DebugWrite(string str) - => DebugWrite(str, args: null); + [Conditional("DEBUG")] + public void DebugWrite(string str) + => DebugWrite(str, args: null); - [Conditional("DEBUG")] - public void DebugWrite(string format, params Arg[]? args) - { - var entry = new Entry(format, args); - Append(entry); - Debug.WriteLine(entry.ToString(), _id); - } + [Conditional("DEBUG")] + public void DebugWrite(string format, params Arg[]? args) + { + var entry = new Entry(format, args); + Append(entry); + Debug.WriteLine(entry.ToString(), _id); + } - internal TestAccessor GetTestAccessor() - => new(this); + internal TestAccessor GetTestAccessor() + => new(this); - internal readonly struct TestAccessor(TraceLog traceLog) - { - private readonly TraceLog _traceLog = traceLog; + internal readonly struct TestAccessor(TraceLog traceLog) + { + private readonly TraceLog _traceLog = traceLog; - internal Entry[] Entries => _traceLog._log; - } + internal Entry[] Entries => _traceLog._log; } } diff --git a/src/Features/Core/Portable/EditAndContinue/UnmappedActiveStatement.cs b/src/Features/Core/Portable/EditAndContinue/UnmappedActiveStatement.cs index c92e721dd8f43..6f3fc9980050b 100644 --- a/src/Features/Core/Portable/EditAndContinue/UnmappedActiveStatement.cs +++ b/src/Features/Core/Portable/EditAndContinue/UnmappedActiveStatement.cs @@ -7,31 +7,30 @@ using System.Diagnostics; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal readonly struct UnmappedActiveStatement(TextSpan unmappedSpan, ActiveStatement statement, ActiveStatementExceptionRegions exceptionRegions) { - internal readonly struct UnmappedActiveStatement(TextSpan unmappedSpan, ActiveStatement statement, ActiveStatementExceptionRegions exceptionRegions) - { - /// - /// Unmapped span of the active statement - /// (span within the file that contains #line directive that has an effect on the active statement, if there is any). - /// - public TextSpan UnmappedSpan { get; } = unmappedSpan; + /// + /// Unmapped span of the active statement + /// (span within the file that contains #line directive that has an effect on the active statement, if there is any). + /// + public TextSpan UnmappedSpan { get; } = unmappedSpan; - /// - /// Active statement - its is mapped. - /// - public ActiveStatement Statement { get; } = statement; + /// + /// Active statement - its is mapped. + /// + public ActiveStatement Statement { get; } = statement; - /// - /// Mapped exception regions around the active statement. - /// - public ActiveStatementExceptionRegions ExceptionRegions { get; } = exceptionRegions; + /// + /// Mapped exception regions around the active statement. + /// + public ActiveStatementExceptionRegions ExceptionRegions { get; } = exceptionRegions; - public void Deconstruct(out TextSpan unmappedSpan, out ActiveStatement statement, out ActiveStatementExceptionRegions exceptionRegions) - { - unmappedSpan = UnmappedSpan; - statement = Statement; - exceptionRegions = ExceptionRegions; - } + public void Deconstruct(out TextSpan unmappedSpan, out ActiveStatement statement, out ActiveStatementExceptionRegions exceptionRegions) + { + unmappedSpan = UnmappedSpan; + statement = Statement; + exceptionRegions = ExceptionRegions; } } diff --git a/src/Features/Core/Portable/EditAndContinue/Utilities/BidirectionalMap.cs b/src/Features/Core/Portable/EditAndContinue/Utilities/BidirectionalMap.cs index 34d592e3a75ed..b8b9e320ce4a1 100644 --- a/src/Features/Core/Portable/EditAndContinue/Utilities/BidirectionalMap.cs +++ b/src/Features/Core/Portable/EditAndContinue/Utilities/BidirectionalMap.cs @@ -6,69 +6,68 @@ using Microsoft.CodeAnalysis.Differencing; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal readonly struct BidirectionalMap + where T : notnull { - internal readonly struct BidirectionalMap - where T : notnull + public readonly IReadOnlyDictionary Forward; + public readonly IReadOnlyDictionary Reverse; + + public static readonly BidirectionalMap Empty = new(SpecializedCollections.EmptyReadOnlyDictionary(), SpecializedCollections.EmptyReadOnlyDictionary()); + + public BidirectionalMap(IReadOnlyDictionary forward, IReadOnlyDictionary reverse) { - public readonly IReadOnlyDictionary Forward; - public readonly IReadOnlyDictionary Reverse; + Contract.ThrowIfFalse(forward.Count == reverse.Count); + Forward = forward; + Reverse = reverse; + } - public static readonly BidirectionalMap Empty = new(SpecializedCollections.EmptyReadOnlyDictionary(), SpecializedCollections.EmptyReadOnlyDictionary()); + public BidirectionalMap With(T source, T target) + { + var forward = new Dictionary(Forward.Count + 1); + var reverse = new Dictionary(Reverse.Count + 1); - public BidirectionalMap(IReadOnlyDictionary forward, IReadOnlyDictionary reverse) + foreach (var entry in Forward) { - Contract.ThrowIfFalse(forward.Count == reverse.Count); - Forward = forward; - Reverse = reverse; + forward.Add(entry.Key, entry.Value); + reverse.Add(entry.Value, entry.Key); } - public BidirectionalMap With(T source, T target) + forward.Add(source, target); + reverse.Add(target, source); + return new(forward, reverse); + } + + public BidirectionalMap With(BidirectionalMap map) + { + if (map.Forward.Count == 0) { - var forward = new Dictionary(Forward.Count + 1); - var reverse = new Dictionary(Reverse.Count + 1); + return this; + } - foreach (var entry in Forward) - { - forward.Add(entry.Key, entry.Value); - reverse.Add(entry.Value, entry.Key); - } + var count = Forward.Count + map.Forward.Count; + var forward = new Dictionary(count); + var reverse = new Dictionary(count); - forward.Add(source, target); - reverse.Add(target, source); - return new(forward, reverse); + foreach (var entry in Forward) + { + forward.Add(entry.Key, entry.Value); + reverse.Add(entry.Value, entry.Key); } - public BidirectionalMap With(BidirectionalMap map) + foreach (var entry in map.Forward) { - if (map.Forward.Count == 0) - { - return this; - } - - var count = Forward.Count + map.Forward.Count; - var forward = new Dictionary(count); - var reverse = new Dictionary(count); - - foreach (var entry in Forward) - { - forward.Add(entry.Key, entry.Value); - reverse.Add(entry.Value, entry.Key); - } - - foreach (var entry in map.Forward) - { - forward.Add(entry.Key, entry.Value); - reverse.Add(entry.Value, entry.Key); - } - - return new BidirectionalMap(forward, reverse); + forward.Add(entry.Key, entry.Value); + reverse.Add(entry.Value, entry.Key); } - public BidirectionalMap WithMatch(Match match) - => With(BidirectionalMap.FromMatch(match)); - - public static BidirectionalMap FromMatch(Match match) - => new(match.Matches, match.ReverseMatches); + return new BidirectionalMap(forward, reverse); } + + public BidirectionalMap WithMatch(Match match) + => With(BidirectionalMap.FromMatch(match)); + + public static BidirectionalMap FromMatch(Match match) + => new(match.Matches, match.ReverseMatches); } diff --git a/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs b/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs index d5fea92baced4..e8f40615816a0 100644 --- a/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs +++ b/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs @@ -13,218 +13,217 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.EditAndContinue +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal static partial class Extensions { - internal static partial class Extensions - { - internal static LinePositionSpan AddLineDelta(this LinePositionSpan span, int lineDelta) - => new(new LinePosition(span.Start.Line + lineDelta, span.Start.Character), new LinePosition(span.End.Line + lineDelta, span.End.Character)); + internal static LinePositionSpan AddLineDelta(this LinePositionSpan span, int lineDelta) + => new(new LinePosition(span.Start.Line + lineDelta, span.Start.Character), new LinePosition(span.End.Line + lineDelta, span.End.Character)); - internal static SourceFileSpan AddLineDelta(this SourceFileSpan span, int lineDelta) - => new(span.Path, span.Span.AddLineDelta(lineDelta)); + internal static SourceFileSpan AddLineDelta(this SourceFileSpan span, int lineDelta) + => new(span.Path, span.Span.AddLineDelta(lineDelta)); - internal static int GetLineDelta(this LinePositionSpan oldSpan, LinePositionSpan newSpan) - => newSpan.Start.Line - oldSpan.Start.Line; + internal static int GetLineDelta(this LinePositionSpan oldSpan, LinePositionSpan newSpan) + => newSpan.Start.Line - oldSpan.Start.Line; - internal static bool Contains(this LinePositionSpan container, LinePositionSpan span) - => span.Start >= container.Start && span.End <= container.End; + internal static bool Contains(this LinePositionSpan container, LinePositionSpan span) + => span.Start >= container.Start && span.End <= container.End; - public static LinePositionSpan ToLinePositionSpan(this SourceSpan span) - => new(new(span.StartLine, span.StartColumn), new(span.EndLine, span.EndColumn)); + public static LinePositionSpan ToLinePositionSpan(this SourceSpan span) + => new(new(span.StartLine, span.StartColumn), new(span.EndLine, span.EndColumn)); - public static SourceSpan ToSourceSpan(this LinePositionSpan span) - => new(span.Start.Line, span.Start.Character, span.End.Line, span.End.Character); + public static SourceSpan ToSourceSpan(this LinePositionSpan span) + => new(span.Start.Line, span.Start.Character, span.End.Line, span.End.Character); - public static ActiveStatement GetStatement(this ImmutableArray statements, int ordinal) + public static ActiveStatement GetStatement(this ImmutableArray statements, int ordinal) + { + foreach (var item in statements) { - foreach (var item in statements) + if (item.Ordinal == ordinal) { - if (item.Ordinal == ordinal) - { - return item; - } + return item; } - - throw ExceptionUtilities.UnexpectedValue(ordinal); } - public static ActiveStatementSpan GetStatement(this ImmutableArray statements, int ordinal) + throw ExceptionUtilities.UnexpectedValue(ordinal); + } + + public static ActiveStatementSpan GetStatement(this ImmutableArray statements, int ordinal) + { + foreach (var item in statements) { - foreach (var item in statements) + if (item.Ordinal == ordinal) { - if (item.Ordinal == ordinal) - { - return item; - } + return item; } - - throw ExceptionUtilities.UnexpectedValue(ordinal); } - public static UnmappedActiveStatement GetStatement(this ImmutableArray statements, int ordinal) + throw ExceptionUtilities.UnexpectedValue(ordinal); + } + + public static UnmappedActiveStatement GetStatement(this ImmutableArray statements, int ordinal) + { + foreach (var item in statements) { - foreach (var item in statements) + if (item.Statement.Ordinal == ordinal) { - if (item.Statement.Ordinal == ordinal) - { - return item; - } + return item; } + } - throw ExceptionUtilities.UnexpectedValue(ordinal); + throw ExceptionUtilities.UnexpectedValue(ordinal); + } + + /// + /// True if the project supports Edit and Continue. + /// Only depends on the language of the project and never changes. + /// + public static bool SupportsEditAndContinue(this Project project) + => project.Services.GetService() != null; + + // Note: source generated files have relative paths: https://github.com/dotnet/roslyn/issues/51998 + public static bool SupportsEditAndContinue(this TextDocumentState textDocumentState) + { + if (textDocumentState.Attributes.DesignTimeOnly) + { + return false; } - /// - /// True if the project supports Edit and Continue. - /// Only depends on the language of the project and never changes. - /// - public static bool SupportsEditAndContinue(this Project project) - => project.Services.GetService() != null; + if (textDocumentState is SourceGeneratedDocumentState { FilePath: not null }) + { + return true; + } + + if (!PathUtilities.IsAbsolute(textDocumentState.FilePath)) + { + return false; + } - // Note: source generated files have relative paths: https://github.com/dotnet/roslyn/issues/51998 - public static bool SupportsEditAndContinue(this TextDocumentState textDocumentState) + if (textDocumentState is DocumentState documentState) { - if (textDocumentState.Attributes.DesignTimeOnly) + if (!documentState.SupportsSyntaxTree) { return false; } - if (textDocumentState is SourceGeneratedDocumentState { FilePath: not null }) + // WPF design time documents are added to the Workspace by the Project System as regular documents, + // although they are not compiled into the binary. + if (IsWpfDesignTimeOnlyDocument(textDocumentState.FilePath, documentState.LanguageServices.Language)) { - return true; + return false; } - if (!PathUtilities.IsAbsolute(textDocumentState.FilePath)) + // Razor generated documents are added to the Workspace by the Web Tools editor but aren't used at runtime, + // so don't need to be considered for edit and continue. + if (IsRazorDesignTimeOnlyDocument(textDocumentState.FilePath)) { return false; } + } - if (textDocumentState is DocumentState documentState) - { - if (!documentState.SupportsSyntaxTree) - { - return false; - } - - // WPF design time documents are added to the Workspace by the Project System as regular documents, - // although they are not compiled into the binary. - if (IsWpfDesignTimeOnlyDocument(textDocumentState.FilePath, documentState.LanguageServices.Language)) - { - return false; - } - - // Razor generated documents are added to the Workspace by the Web Tools editor but aren't used at runtime, - // so don't need to be considered for edit and continue. - if (IsRazorDesignTimeOnlyDocument(textDocumentState.FilePath)) - { - return false; - } - } + return true; + } - return true; - } + private static bool IsWpfDesignTimeOnlyDocument(string filePath, string language) + => language switch + { + LanguageNames.CSharp => filePath.EndsWith(".g.i.cs", StringComparison.OrdinalIgnoreCase), + LanguageNames.VisualBasic => filePath.EndsWith(".g.i.vb", StringComparison.OrdinalIgnoreCase), + _ => false + }; - private static bool IsWpfDesignTimeOnlyDocument(string filePath, string language) - => language switch - { - LanguageNames.CSharp => filePath.EndsWith(".g.i.cs", StringComparison.OrdinalIgnoreCase), - LanguageNames.VisualBasic => filePath.EndsWith(".g.i.vb", StringComparison.OrdinalIgnoreCase), - _ => false - }; + private static bool IsRazorDesignTimeOnlyDocument(string filePath) + => filePath.EndsWith(".razor.g.cs", StringComparison.OrdinalIgnoreCase) || + filePath.EndsWith(".cshtml.g.cs", StringComparison.OrdinalIgnoreCase); - private static bool IsRazorDesignTimeOnlyDocument(string filePath) - => filePath.EndsWith(".razor.g.cs", StringComparison.OrdinalIgnoreCase) || - filePath.EndsWith(".cshtml.g.cs", StringComparison.OrdinalIgnoreCase); + public static ManagedHotReloadDiagnostic ToHotReloadDiagnostic(this DiagnosticData data, ModuleUpdateStatus updateStatus) + { + var fileSpan = data.DataLocation.MappedFileSpan; + + return new( + data.Id, + data.Message ?? FeaturesResources.Unknown_error_occurred, + updateStatus == ModuleUpdateStatus.RestartRequired + ? ManagedHotReloadDiagnosticSeverity.RestartRequired + : (data.Severity == DiagnosticSeverity.Error) + ? ManagedHotReloadDiagnosticSeverity.Error + : ManagedHotReloadDiagnosticSeverity.Warning, + fileSpan.Path ?? "", + fileSpan.Span.ToSourceSpan()); + } - public static ManagedHotReloadDiagnostic ToHotReloadDiagnostic(this DiagnosticData data, ModuleUpdateStatus updateStatus) + public static bool IsSynthesized(this ISymbol symbol) + => symbol.IsImplicitlyDeclared || symbol.IsSynthesizedAutoProperty() || symbol.IsSynthesizedParameter(); + + public static bool IsSynthesizedAutoProperty(this IPropertySymbol property) + => property is { GetMethod.IsImplicitlyDeclared: true, SetMethod.IsImplicitlyDeclared: true }; + + public static bool IsSynthesizedAutoProperty(this ISymbol symbol) + => symbol is IPropertySymbol property && property.IsSynthesizedAutoProperty(); + + public static bool IsSynthesizedParameter(this ISymbol symbol) + => symbol is IParameterSymbol parameter && parameter.IsSynthesizedParameter(); + + /// + /// True if the parameter is synthesized based on some other symbol (origin). + /// In some cases of parameters of synthezied methods might be false. + /// The parameter syntax in these cases is associated with multiple symbols. + /// We pick one that is considered the origin and the others are considered synthesized based on it. + /// + /// 1) Parameter of a record deconstructor + /// Considered synthesized since the primary parameter syntax represents the parameter of the primary constructor. + /// The deconstructor is synthesized based on the primary constructor. + /// 2) Parameter of an Invoke method of a delegate type + /// The Invoke method is synthesized but its parameters represent the parameters of the delegate. + /// The parameters of BeginInvoke and EndInvoke are synthesized based on the Invoke method parameters. + /// + public static bool IsSynthesizedParameter(this IParameterSymbol parameter) + => parameter.IsImplicitlyDeclared || parameter.ContainingSymbol.IsSynthesized() && parameter.ContainingSymbol != parameter.ContainingType.DelegateInvokeMethod; + + public static bool IsAutoProperty(this ISymbol symbol) + => symbol is IPropertySymbol property && IsAutoProperty(property); + + public static bool IsAutoProperty(this IPropertySymbol property) + => property.ContainingType.GetMembers().Any(static (member, property) => member is IFieldSymbol field && field.AssociatedSymbol == property, property); + + public static bool HasSynthesizedDefaultConstructor(this INamedTypeSymbol type) + => !type.InstanceConstructors.Any(static c => !(c.Parameters is [] || c.ContainingType.IsRecord && c.IsCopyConstructor())); + + public static bool IsCopyConstructor(this ISymbol symbol) + => symbol is IMethodSymbol { Parameters: [var parameter] } && SymbolEqualityComparer.Default.Equals(parameter.Type, symbol.ContainingType); + + public static bool HasDeconstructorSignature(this IMethodSymbol method, IMethodSymbol constructor) + => method.Parameters.Length > 0 && + method.Parameters.Length == constructor.Parameters.Length && + method.Parameters.All( + static (param, constructor) => param.RefKind == RefKind.Out && param.Type.Equals(constructor.Parameters[param.Ordinal].Type, SymbolEqualityComparer.Default), + constructor); + + // TODO: use AssociatedSymbol to tie field to the parameter (see https://github.com/dotnet/roslyn/issues/69115) + public static IFieldSymbol? GetPrimaryParameterBackingField(this IParameterSymbol parameter) + => (IFieldSymbol?)parameter.ContainingType.GetMembers().FirstOrDefault( + static (member, parameter) => member is IFieldSymbol field && ParsePrimaryParameterBackingFieldName(field.Name, out var paramName) && paramName == parameter.Name, parameter); + + private static bool ParsePrimaryParameterBackingFieldName(string fieldName, [NotNullWhen(true)] out string? parameterName) + { + int closing; + if (fieldName.StartsWith("<") && (closing = fieldName.IndexOf(">P")) > 1) { - var fileSpan = data.DataLocation.MappedFileSpan; - - return new( - data.Id, - data.Message ?? FeaturesResources.Unknown_error_occurred, - updateStatus == ModuleUpdateStatus.RestartRequired - ? ManagedHotReloadDiagnosticSeverity.RestartRequired - : (data.Severity == DiagnosticSeverity.Error) - ? ManagedHotReloadDiagnosticSeverity.Error - : ManagedHotReloadDiagnosticSeverity.Warning, - fileSpan.Path ?? "", - fileSpan.Span.ToSourceSpan()); + parameterName = fieldName.Substring(1, closing - 1); + return true; } - public static bool IsSynthesized(this ISymbol symbol) - => symbol.IsImplicitlyDeclared || symbol.IsSynthesizedAutoProperty() || symbol.IsSynthesizedParameter(); - - public static bool IsSynthesizedAutoProperty(this IPropertySymbol property) - => property is { GetMethod.IsImplicitlyDeclared: true, SetMethod.IsImplicitlyDeclared: true }; - - public static bool IsSynthesizedAutoProperty(this ISymbol symbol) - => symbol is IPropertySymbol property && property.IsSynthesizedAutoProperty(); - - public static bool IsSynthesizedParameter(this ISymbol symbol) - => symbol is IParameterSymbol parameter && parameter.IsSynthesizedParameter(); - - /// - /// True if the parameter is synthesized based on some other symbol (origin). - /// In some cases of parameters of synthezied methods might be false. - /// The parameter syntax in these cases is associated with multiple symbols. - /// We pick one that is considered the origin and the others are considered synthesized based on it. - /// - /// 1) Parameter of a record deconstructor - /// Considered synthesized since the primary parameter syntax represents the parameter of the primary constructor. - /// The deconstructor is synthesized based on the primary constructor. - /// 2) Parameter of an Invoke method of a delegate type - /// The Invoke method is synthesized but its parameters represent the parameters of the delegate. - /// The parameters of BeginInvoke and EndInvoke are synthesized based on the Invoke method parameters. - /// - public static bool IsSynthesizedParameter(this IParameterSymbol parameter) - => parameter.IsImplicitlyDeclared || parameter.ContainingSymbol.IsSynthesized() && parameter.ContainingSymbol != parameter.ContainingType.DelegateInvokeMethod; - - public static bool IsAutoProperty(this ISymbol symbol) - => symbol is IPropertySymbol property && IsAutoProperty(property); - - public static bool IsAutoProperty(this IPropertySymbol property) - => property.ContainingType.GetMembers().Any(static (member, property) => member is IFieldSymbol field && field.AssociatedSymbol == property, property); - - public static bool HasSynthesizedDefaultConstructor(this INamedTypeSymbol type) - => !type.InstanceConstructors.Any(static c => !(c.Parameters is [] || c.ContainingType.IsRecord && c.IsCopyConstructor())); - - public static bool IsCopyConstructor(this ISymbol symbol) - => symbol is IMethodSymbol { Parameters: [var parameter] } && SymbolEqualityComparer.Default.Equals(parameter.Type, symbol.ContainingType); - - public static bool HasDeconstructorSignature(this IMethodSymbol method, IMethodSymbol constructor) - => method.Parameters.Length > 0 && - method.Parameters.Length == constructor.Parameters.Length && - method.Parameters.All( - static (param, constructor) => param.RefKind == RefKind.Out && param.Type.Equals(constructor.Parameters[param.Ordinal].Type, SymbolEqualityComparer.Default), - constructor); - - // TODO: use AssociatedSymbol to tie field to the parameter (see https://github.com/dotnet/roslyn/issues/69115) - public static IFieldSymbol? GetPrimaryParameterBackingField(this IParameterSymbol parameter) - => (IFieldSymbol?)parameter.ContainingType.GetMembers().FirstOrDefault( - static (member, parameter) => member is IFieldSymbol field && ParsePrimaryParameterBackingFieldName(field.Name, out var paramName) && paramName == parameter.Name, parameter); - - private static bool ParsePrimaryParameterBackingFieldName(string fieldName, [NotNullWhen(true)] out string? parameterName) - { - int closing; - if (fieldName.StartsWith("<") && (closing = fieldName.IndexOf(">P")) > 1) - { - parameterName = fieldName.Substring(1, closing - 1); - return true; - } - - parameterName = null; - return false; - } + parameterName = null; + return false; + } - /// - /// Returns a deconstructor that matches the parameters of the given , or null if there is none. - /// - public static IMethodSymbol? GetMatchingDeconstructor(this IMethodSymbol constructor) - => (IMethodSymbol?)constructor.ContainingType.GetMembers(WellKnownMemberNames.DeconstructMethodName).FirstOrDefault( - static (symbol, constructor) => symbol is IMethodSymbol method && HasDeconstructorSignature(method, constructor), constructor)?.PartialAsImplementation(); + /// + /// Returns a deconstructor that matches the parameters of the given , or null if there is none. + /// + public static IMethodSymbol? GetMatchingDeconstructor(this IMethodSymbol constructor) + => (IMethodSymbol?)constructor.ContainingType.GetMembers(WellKnownMemberNames.DeconstructMethodName).FirstOrDefault( + static (symbol, constructor) => symbol is IMethodSymbol method && HasDeconstructorSignature(method, constructor), constructor)?.PartialAsImplementation(); - public static ISymbol PartialAsImplementation(this ISymbol symbol) - => symbol is IMethodSymbol { PartialImplementationPart: { } impl } ? impl : symbol; - } + public static ISymbol PartialAsImplementation(this ISymbol symbol) + => symbol is IMethodSymbol { PartialImplementationPart: { } impl } ? impl : symbol; } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/AbstractEmbeddedLanguagesProvider.cs b/src/Features/Core/Portable/EmbeddedLanguages/AbstractEmbeddedLanguagesProvider.cs index 0e7daac1d7435..e515713b6c883 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/AbstractEmbeddedLanguagesProvider.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/AbstractEmbeddedLanguagesProvider.cs @@ -7,29 +7,28 @@ using Microsoft.CodeAnalysis.Features.EmbeddedLanguages.Json.LanguageServices; using Microsoft.CodeAnalysis.Features.EmbeddedLanguages.RegularExpressions.LanguageServices; -namespace Microsoft.CodeAnalysis.EmbeddedLanguages -{ - /// - /// Abstract implementation of the C# and VB embedded language providers. - /// - internal abstract class AbstractEmbeddedLanguagesProvider : IEmbeddedLanguagesProvider - { - public EmbeddedLanguageInfo EmbeddedLanguageInfo { get; } - public ImmutableArray Languages { get; } +namespace Microsoft.CodeAnalysis.EmbeddedLanguages; - protected AbstractEmbeddedLanguagesProvider(EmbeddedLanguageInfo info) - { - EmbeddedLanguageInfo = info; - Languages = [new DateAndTimeEmbeddedLanguage(info), new RegexEmbeddedLanguage(this, info), new JsonEmbeddedLanguage()]; - } +/// +/// Abstract implementation of the C# and VB embedded language providers. +/// +internal abstract class AbstractEmbeddedLanguagesProvider : IEmbeddedLanguagesProvider +{ + public EmbeddedLanguageInfo EmbeddedLanguageInfo { get; } + public ImmutableArray Languages { get; } - /// Escapes appropriately so it can be inserted into - /// . For example if inserting `\p{Number}` into a normal C# - /// string token, the `\` would have to be escaped into `\\`. However in a verbatim-string - /// literal (i.e. `@"..."`) it would not have to be escaped. - /// - /// The original string token that is being - /// inserted into. - public abstract string EscapeText(string text, SyntaxToken token); + protected AbstractEmbeddedLanguagesProvider(EmbeddedLanguageInfo info) + { + EmbeddedLanguageInfo = info; + Languages = [new DateAndTimeEmbeddedLanguage(info), new RegexEmbeddedLanguage(this, info), new JsonEmbeddedLanguage()]; } + + /// Escapes appropriately so it can be inserted into + /// . For example if inserting `\p{Number}` into a normal C# + /// string token, the `\` would have to be escaped into `\\`. However in a verbatim-string + /// literal (i.e. `@"..."`) it would not have to be escaped. + /// + /// The original string token that is being + /// inserted into. + public abstract string EscapeText(string text, SyntaxToken token); } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/AbstractLanguageDetector.cs b/src/Features/Core/Portable/EmbeddedLanguages/AbstractLanguageDetector.cs index 0f744b937e363..cb22f05ded3b3 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/AbstractLanguageDetector.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/AbstractLanguageDetector.cs @@ -11,229 +11,228 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages +namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages; + +internal abstract class AbstractLanguageDetector + where TOptions : struct, Enum { - internal abstract class AbstractLanguageDetector - where TOptions : struct, Enum - { - protected readonly EmbeddedLanguageInfo Info; + protected readonly EmbeddedLanguageInfo Info; - private readonly EmbeddedLanguageDetector _detector; + private readonly EmbeddedLanguageDetector _detector; - protected AbstractLanguageDetector( - EmbeddedLanguageInfo info, - ImmutableArray languageIdentifiers, - EmbeddedLanguageCommentDetector commentDetector) - { - Info = info; - _detector = new EmbeddedLanguageDetector(info, languageIdentifiers, commentDetector); - } + protected AbstractLanguageDetector( + EmbeddedLanguageInfo info, + ImmutableArray languageIdentifiers, + EmbeddedLanguageCommentDetector commentDetector) + { + Info = info; + _detector = new EmbeddedLanguageDetector(info, languageIdentifiers, commentDetector); + } + + /// + /// Whether or not this is an argument to a well known api for this language (like Regex.Match or JToken.Parse). + /// We light up support if we detect these, even if these APIs don't have the StringSyntaxAttribute attribute on + /// them. That way users can get a decent experience even on downlevel frameworks. + /// + protected abstract bool IsArgumentToWellKnownAPI(SyntaxToken token, SyntaxNode argumentNode, SemanticModel semanticModel, CancellationToken cancellationToken, out TOptions options); + + /// + /// Giving a sibling argument expression to the string literal, attempts to determine if they correspond to + /// options for that language. For example with new Regex("[a-z]", RegexOptions.CaseInsensitive) the + /// second argument's expression defines options that control how the literal is parsed. + /// + protected abstract bool TryGetOptions(SemanticModel semanticModel, ITypeSymbol exprType, SyntaxNode expr, CancellationToken cancellationToken, out TOptions options); + + // Most embedded languages don't support being in an interpolated string text token. + protected virtual bool IsEmbeddedLanguageInterpolatedStringTextToken(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken) + => false; + + /// + /// What options we should assume by default if we're matched up against a symbol that has a [StringSyntax] + /// attribute on it. + /// + protected virtual TOptions GetStringSyntaxDefaultOptions() + => default; + + public bool IsEmbeddedLanguageToken( + SyntaxToken token, + SemanticModel semanticModel, + CancellationToken cancellationToken, + out TOptions options) + { + options = default; - /// - /// Whether or not this is an argument to a well known api for this language (like Regex.Match or JToken.Parse). - /// We light up support if we detect these, even if these APIs don't have the StringSyntaxAttribute attribute on - /// them. That way users can get a decent experience even on downlevel frameworks. - /// - protected abstract bool IsArgumentToWellKnownAPI(SyntaxToken token, SyntaxNode argumentNode, SemanticModel semanticModel, CancellationToken cancellationToken, out TOptions options); - - /// - /// Giving a sibling argument expression to the string literal, attempts to determine if they correspond to - /// options for that language. For example with new Regex("[a-z]", RegexOptions.CaseInsensitive) the - /// second argument's expression defines options that control how the literal is parsed. - /// - protected abstract bool TryGetOptions(SemanticModel semanticModel, ITypeSymbol exprType, SyntaxNode expr, CancellationToken cancellationToken, out TOptions options); - - // Most embedded languages don't support being in an interpolated string text token. - protected virtual bool IsEmbeddedLanguageInterpolatedStringTextToken(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken) - => false; - - /// - /// What options we should assume by default if we're matched up against a symbol that has a [StringSyntax] - /// attribute on it. - /// - protected virtual TOptions GetStringSyntaxDefaultOptions() - => default; - - public bool IsEmbeddedLanguageToken( - SyntaxToken token, - SemanticModel semanticModel, - CancellationToken cancellationToken, - out TOptions options) + // First check for the standard pattern of either a `// lang=...` comment or an API annotated with the + // [StringSyntax] attribute. + if (_detector.IsEmbeddedLanguageToken(token, semanticModel, cancellationToken, out _, out var stringOptions)) { - options = default; + // If we got string-options back, then we were on a comment string (e.g. `// lang=regex,option1,option2`). + // Attempt to convert the string options to actual options requested. + if (stringOptions != null) + return EmbeddedLanguageCommentOptions.TryGetOptions(stringOptions, out options); - // First check for the standard pattern of either a `// lang=...` comment or an API annotated with the - // [StringSyntax] attribute. - if (_detector.IsEmbeddedLanguageToken(token, semanticModel, cancellationToken, out _, out var stringOptions)) + // If we weren't on a comment, then we were on an API with StringSyntaxAttribute on it. Attempt to grab + // API specific options for the client to use. + var syntaxFacts = Info.SyntaxFacts; + if (syntaxFacts.IsLiteralExpression(token.Parent) && + syntaxFacts.IsArgument(token.Parent.Parent)) { - // If we got string-options back, then we were on a comment string (e.g. `// lang=regex,option1,option2`). - // Attempt to convert the string options to actual options requested. - if (stringOptions != null) - return EmbeddedLanguageCommentOptions.TryGetOptions(stringOptions, out options); - - // If we weren't on a comment, then we were on an API with StringSyntaxAttribute on it. Attempt to grab - // API specific options for the client to use. - var syntaxFacts = Info.SyntaxFacts; - if (syntaxFacts.IsLiteralExpression(token.Parent) && - syntaxFacts.IsArgument(token.Parent.Parent)) - { - options = GetOptionsFromSiblingArgument(token.Parent.Parent, semanticModel, cancellationToken) ?? - GetStringSyntaxDefaultOptions(); - } - - return true; + options = GetOptionsFromSiblingArgument(token.Parent.Parent, semanticModel, cancellationToken) ?? + GetStringSyntaxDefaultOptions(); } - else - { - // We did not have a comment, and we didn't have a StringSyntax API. See if this is an unannotated API - // (e.g. Regex/Json prior to .net core). - if (Info.IsAnyStringLiteral(token.RawKind)) - return IsEmbeddedLanguageStringLiteralToken(token, semanticModel, cancellationToken, out options); + return true; + } + else + { + // We did not have a comment, and we didn't have a StringSyntax API. See if this is an unannotated API + // (e.g. Regex/Json prior to .net core). - if (token.RawKind == Info.SyntaxKinds.InterpolatedStringTextToken) - { - options = default; - return IsEmbeddedLanguageInterpolatedStringTextToken(token, semanticModel, cancellationToken); - } + if (Info.IsAnyStringLiteral(token.RawKind)) + return IsEmbeddedLanguageStringLiteralToken(token, semanticModel, cancellationToken, out options); - return false; + if (token.RawKind == Info.SyntaxKinds.InterpolatedStringTextToken) + { + options = default; + return IsEmbeddedLanguageInterpolatedStringTextToken(token, semanticModel, cancellationToken); } + + return false; } + } - private bool IsEmbeddedLanguageStringLiteralToken(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken, out TOptions options) - { - options = default; - var syntaxFacts = Info.SyntaxFacts; + private bool IsEmbeddedLanguageStringLiteralToken(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken, out TOptions options) + { + options = default; + var syntaxFacts = Info.SyntaxFacts; - return syntaxFacts.IsLiteralExpression(token.Parent) && - syntaxFacts.IsArgument(token.Parent.Parent) && - IsArgumentToWellKnownAPI(token, token.Parent.Parent, semanticModel, cancellationToken, out options); - } + return syntaxFacts.IsLiteralExpression(token.Parent) && + syntaxFacts.IsArgument(token.Parent.Parent) && + IsArgumentToWellKnownAPI(token, token.Parent.Parent, semanticModel, cancellationToken, out options); + } - protected TOptions? GetOptionsFromSiblingArgument( - SyntaxNode argument, - SemanticModel semanticModel, - CancellationToken cancellationToken) + protected TOptions? GetOptionsFromSiblingArgument( + SyntaxNode argument, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var syntaxFacts = Info.SyntaxFacts; + var argumentList = argument.GetRequiredParent(); + var arguments = syntaxFacts.IsArgument(argument) + ? syntaxFacts.GetArgumentsOfArgumentList(argumentList) + : syntaxFacts.GetArgumentsOfAttributeArgumentList(argumentList); + foreach (var siblingArg in arguments) { - var syntaxFacts = Info.SyntaxFacts; - var argumentList = argument.GetRequiredParent(); - var arguments = syntaxFacts.IsArgument(argument) - ? syntaxFacts.GetArgumentsOfArgumentList(argumentList) - : syntaxFacts.GetArgumentsOfAttributeArgumentList(argumentList); - foreach (var siblingArg in arguments) + if (siblingArg != argument) { - if (siblingArg != argument) + var expr = syntaxFacts.IsArgument(argument) + ? syntaxFacts.GetExpressionOfArgument(siblingArg) + : syntaxFacts.GetExpressionOfAttributeArgument(siblingArg); + if (expr != null) { - var expr = syntaxFacts.IsArgument(argument) - ? syntaxFacts.GetExpressionOfArgument(siblingArg) - : syntaxFacts.GetExpressionOfAttributeArgument(siblingArg); - if (expr != null) + var exprType = semanticModel.GetTypeInfo(expr, cancellationToken); + if (exprType.Type != null && + TryGetOptions(semanticModel, exprType.Type, expr, cancellationToken, out var options)) { - var exprType = semanticModel.GetTypeInfo(expr, cancellationToken); - if (exprType.Type != null && - TryGetOptions(semanticModel, exprType.Type, expr, cancellationToken, out var options)) - { - return options; - } + return options; } } } - - return null; } - protected static string? GetNameOfType(SyntaxNode? typeNode, ISyntaxFacts syntaxFacts) - { - if (syntaxFacts.IsQualifiedName(typeNode)) - { - return GetNameOfType(syntaxFacts.GetRightSideOfDot(typeNode), syntaxFacts); - } - else if (syntaxFacts.IsIdentifierName(typeNode)) - { - return syntaxFacts.GetIdentifierOfSimpleName(typeNode).ValueText; - } + return null; + } - return null; + protected static string? GetNameOfType(SyntaxNode? typeNode, ISyntaxFacts syntaxFacts) + { + if (syntaxFacts.IsQualifiedName(typeNode)) + { + return GetNameOfType(syntaxFacts.GetRightSideOfDot(typeNode), syntaxFacts); } - - protected string? GetNameOfInvokedExpression(SyntaxNode invokedExpression) + else if (syntaxFacts.IsIdentifierName(typeNode)) { - var syntaxFacts = Info.SyntaxFacts; - if (syntaxFacts.IsSimpleMemberAccessExpression(invokedExpression)) - { - return syntaxFacts.GetIdentifierOfSimpleName(syntaxFacts.GetNameOfMemberAccessExpression(invokedExpression)).ValueText; - } - else if (syntaxFacts.IsIdentifierName(invokedExpression)) - { - return syntaxFacts.GetIdentifierOfSimpleName(invokedExpression).ValueText; - } - - return null; + return syntaxFacts.GetIdentifierOfSimpleName(typeNode).ValueText; } - } - internal interface ILanguageDetectorInfo - { - ImmutableArray LanguageIdentifiers { get; } - TDetector Create(Compilation compilation, EmbeddedLanguageInfo info); + return null; } - internal abstract class AbstractLanguageDetector : - AbstractLanguageDetector - where TOptions : struct, Enum - where TTree : class - where TDetector : AbstractLanguageDetector - where TDetectorInfo : struct, ILanguageDetectorInfo + protected string? GetNameOfInvokedExpression(SyntaxNode invokedExpression) { - public static readonly ImmutableArray LanguageIdentifiers = default(TDetectorInfo).LanguageIdentifiers; - public static readonly EmbeddedLanguageCommentDetector CommentDetector = new(LanguageIdentifiers); - - /// - /// Cache so that we can reuse the same TDetector when analyzing a particular compilation model. This saves the - /// time from having to recreate this for every string literal that features examine for a particular - /// compilation. - /// - private static readonly ConditionalWeakTable s_compilationToDetector = new(); - - protected AbstractLanguageDetector( - EmbeddedLanguageInfo info, - ImmutableArray languageIdentifiers, - EmbeddedLanguageCommentDetector commentDetector) - : base(info, languageIdentifiers, commentDetector) + var syntaxFacts = Info.SyntaxFacts; + if (syntaxFacts.IsSimpleMemberAccessExpression(invokedExpression)) { + return syntaxFacts.GetIdentifierOfSimpleName(syntaxFacts.GetNameOfMemberAccessExpression(invokedExpression)).ValueText; } - - public static TDetector GetOrCreate( - Compilation compilation, EmbeddedLanguageInfo info) + else if (syntaxFacts.IsIdentifierName(invokedExpression)) { - // Do a quick non-allocating check first. If not already created, go the path that will have to allocate - // the lambda closure. - return s_compilationToDetector.TryGetValue(compilation, out var detector) - ? detector - : Create(compilation, info); - - static TDetector Create(Compilation compilation, EmbeddedLanguageInfo info) - => s_compilationToDetector.GetValue(compilation, _ => default(TDetectorInfo).Create(compilation, info)); + return syntaxFacts.GetIdentifierOfSimpleName(invokedExpression).ValueText; } - /// - /// Tries to parse out an appropriate language tree given the characters in this string literal. - /// - protected abstract TTree? TryParse(VirtualCharSequence chars, TOptions options); - - /// - /// Attempts to parse the string-literal-like into an embedded language tree. The - /// token must either be in a location semantically known to accept this language, or it must have an - /// appropriate comment on it stating that it should be interpreted as this language. - /// - public TTree? TryParseString(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken) - { - if (!this.IsEmbeddedLanguageToken(token, semanticModel, cancellationToken, out var options)) - return null; + return null; + } +} - var chars = Info.VirtualCharService.TryConvertToVirtualChars(token); - return TryParse(chars, options); - } +internal interface ILanguageDetectorInfo +{ + ImmutableArray LanguageIdentifiers { get; } + TDetector Create(Compilation compilation, EmbeddedLanguageInfo info); +} + +internal abstract class AbstractLanguageDetector : + AbstractLanguageDetector + where TOptions : struct, Enum + where TTree : class + where TDetector : AbstractLanguageDetector + where TDetectorInfo : struct, ILanguageDetectorInfo +{ + public static readonly ImmutableArray LanguageIdentifiers = default(TDetectorInfo).LanguageIdentifiers; + public static readonly EmbeddedLanguageCommentDetector CommentDetector = new(LanguageIdentifiers); + + /// + /// Cache so that we can reuse the same TDetector when analyzing a particular compilation model. This saves the + /// time from having to recreate this for every string literal that features examine for a particular + /// compilation. + /// + private static readonly ConditionalWeakTable s_compilationToDetector = new(); + + protected AbstractLanguageDetector( + EmbeddedLanguageInfo info, + ImmutableArray languageIdentifiers, + EmbeddedLanguageCommentDetector commentDetector) + : base(info, languageIdentifiers, commentDetector) + { + } + + public static TDetector GetOrCreate( + Compilation compilation, EmbeddedLanguageInfo info) + { + // Do a quick non-allocating check first. If not already created, go the path that will have to allocate + // the lambda closure. + return s_compilationToDetector.TryGetValue(compilation, out var detector) + ? detector + : Create(compilation, info); + + static TDetector Create(Compilation compilation, EmbeddedLanguageInfo info) + => s_compilationToDetector.GetValue(compilation, _ => default(TDetectorInfo).Create(compilation, info)); + } + + /// + /// Tries to parse out an appropriate language tree given the characters in this string literal. + /// + protected abstract TTree? TryParse(VirtualCharSequence chars, TOptions options); + + /// + /// Attempts to parse the string-literal-like into an embedded language tree. The + /// token must either be in a location semantically known to accept this language, or it must have an + /// appropriate comment on it stating that it should be interpreted as this language. + /// + public TTree? TryParseString(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken) + { + if (!this.IsEmbeddedLanguageToken(token, semanticModel, cancellationToken, out var options)) + return null; + + var chars = Info.VirtualCharService.TryConvertToVirtualChars(token); + return TryParse(chars, options); } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractEmbeddedLanguageClassificationService.cs b/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractEmbeddedLanguageClassificationService.cs index f47ac8bd42e69..2aa588647f843 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractEmbeddedLanguageClassificationService.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractEmbeddedLanguageClassificationService.cs @@ -14,141 +14,147 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal abstract class AbstractEmbeddedLanguageClassificationService : + AbstractEmbeddedLanguageFeatureService, IEmbeddedLanguageClassificationService { - internal abstract class AbstractEmbeddedLanguageClassificationService : - AbstractEmbeddedLanguageFeatureService, IEmbeddedLanguageClassificationService + /// + /// Finally classifier to run if there is no embedded language in a string. It will just classify escape sequences. + /// + private readonly IEmbeddedLanguageClassifier _fallbackClassifier; + + protected AbstractEmbeddedLanguageClassificationService( + string languageName, + EmbeddedLanguageInfo info, + ISyntaxKinds syntaxKinds, + IEmbeddedLanguageClassifier fallbackClassifier, + IEnumerable> allClassifiers) + : base(languageName, info, syntaxKinds, allClassifiers) { - /// - /// Finally classifier to run if there is no embedded language in a string. It will just classify escape sequences. - /// - private readonly IEmbeddedLanguageClassifier _fallbackClassifier; - - protected AbstractEmbeddedLanguageClassificationService( - string languageName, - EmbeddedLanguageInfo info, - ISyntaxKinds syntaxKinds, - IEmbeddedLanguageClassifier fallbackClassifier, - IEnumerable> allClassifiers) - : base(languageName, info, syntaxKinds, allClassifiers) - { - _fallbackClassifier = fallbackClassifier; - } + _fallbackClassifier = fallbackClassifier; + } - public async Task AddEmbeddedLanguageClassificationsAsync( - Document document, ImmutableArray textSpans, ClassificationOptions options, SegmentedList result, CancellationToken cancellationToken) - { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var project = document.Project; - AddEmbeddedLanguageClassifications( - project.Solution.Services, project, semanticModel, textSpans, options, result, cancellationToken); - } + public async Task AddEmbeddedLanguageClassificationsAsync( + Document document, ImmutableArray textSpans, ClassificationOptions options, SegmentedList result, CancellationToken cancellationToken) + { + var project = document.Project; + SemanticModel semanticModel; +#pragma warning disable RSEXPERIMENTAL001 // Internal usage of experimental API + semanticModel = await document.GetRequiredNullableDisabledSemanticModelAsync(cancellationToken).ConfigureAwait(false); +#pragma warning restore RSEXPERIMENTAL001 - public void AddEmbeddedLanguageClassifications( - SolutionServices services, Project? project, SemanticModel semanticModel, ImmutableArray textSpans, ClassificationOptions options, SegmentedList result, CancellationToken cancellationToken) - { - if (project is null) - return; + AddEmbeddedLanguageClassifications( + project.Solution.Services, project, semanticModel, textSpans, options, result, cancellationToken); + } - var root = semanticModel.SyntaxTree.GetRoot(cancellationToken); - foreach (var textSpan in textSpans) - { - var worker = new Worker(this, services, project, semanticModel, textSpan, options, result, cancellationToken); - worker.VisitTokens(root); - } + public void AddEmbeddedLanguageClassifications( + SolutionServices services, Project? project, SemanticModel semanticModel, ImmutableArray textSpans, ClassificationOptions options, SegmentedList result, CancellationToken cancellationToken) + { + if (project is null) + return; + + var root = semanticModel.SyntaxTree.GetRoot(cancellationToken); + foreach (var textSpan in textSpans) + { + var worker = new Worker(this, services, project, semanticModel, textSpan, options, result, cancellationToken); + worker.VisitTokens(root); } + } - private readonly ref struct Worker( - AbstractEmbeddedLanguageClassificationService service, - SolutionServices solutionServices, - Project project, - SemanticModel semanticModel, - TextSpan textSpan, - ClassificationOptions options, - SegmentedList result, - CancellationToken cancellationToken) + private readonly ref struct Worker( + AbstractEmbeddedLanguageClassificationService service, + SolutionServices solutionServices, + Project project, + SemanticModel semanticModel, + TextSpan textSpan, + ClassificationOptions options, + SegmentedList result, + CancellationToken cancellationToken) + { + private readonly AbstractEmbeddedLanguageClassificationService _owner = service; + private readonly SolutionServices _solutionServices = solutionServices; + private readonly Project _project = project; + private readonly SemanticModel _semanticModel = semanticModel; + private readonly TextSpan _textSpan = textSpan; + private readonly ClassificationOptions _options = options; + private readonly SegmentedList _result = result; + private readonly CancellationToken _cancellationToken = cancellationToken; + + public void VisitTokens(SyntaxNode node) { - private readonly AbstractEmbeddedLanguageClassificationService _owner = service; - private readonly SolutionServices _solutionServices = solutionServices; - private readonly Project _project = project; - private readonly SemanticModel _semanticModel = semanticModel; - private readonly TextSpan _textSpan = textSpan; - private readonly ClassificationOptions _options = options; - private readonly SegmentedList _result = result; - private readonly CancellationToken _cancellationToken = cancellationToken; - - public void VisitTokens(SyntaxNode node) + using var pooledStack = SharedPools.Default>().GetPooledObject(); + var stack = pooledStack.Object; + stack.Push(node); + while (stack.Count > 0) { - using var pooledStack = SharedPools.Default>().GetPooledObject(); - var stack = pooledStack.Object; - stack.Push(node); - while (stack.Count > 0) + _cancellationToken.ThrowIfCancellationRequested(); + var currentNodeOrToken = stack.Pop(); + if (currentNodeOrToken.Span.IntersectsWith(_textSpan)) { - _cancellationToken.ThrowIfCancellationRequested(); - var currentNodeOrToken = stack.Pop(); - if (currentNodeOrToken.Span.IntersectsWith(_textSpan)) + if (currentNodeOrToken.IsNode) { - if (currentNodeOrToken.IsNode) + foreach (var child in currentNodeOrToken.ChildNodesAndTokens().Reverse()) { - foreach (var child in currentNodeOrToken.ChildNodesAndTokens().Reverse()) - { - stack.Push(child); - } - } - else - { - ProcessToken(currentNodeOrToken.AsToken()); + stack.Push(child); } } + else + { + ProcessToken(currentNodeOrToken.AsToken()); + } } } + } - private void ProcessToken(SyntaxToken token) - { - _cancellationToken.ThrowIfCancellationRequested(); + private void ProcessToken(SyntaxToken token) + { + _cancellationToken.ThrowIfCancellationRequested(); + + // Directives need to be processes as they can contain strings, which then have escapes in them. + if (token.ContainsDirectives) ProcessTriviaList(token.LeadingTrivia); - ClassifyToken(token); - ProcessTriviaList(token.TrailingTrivia); - } - private void ClassifyToken(SyntaxToken token) - { - if (token.Span.IntersectsWith(_textSpan) && _owner.SyntaxTokenKinds.Contains(token.RawKind)) - { - var context = new EmbeddedLanguageClassificationContext( - _solutionServices, _project, _semanticModel, token, _options, _owner.Info.VirtualCharService, _result, _cancellationToken); + ClassifyToken(token); + } - var classifiers = _owner.GetServices(_semanticModel, token, _cancellationToken); - foreach (var classifier in classifiers) - { - // If this classifier added values then need to check the other ones. - if (TryClassify(classifier.Value, context)) - return; - } + private void ClassifyToken(SyntaxToken token) + { + if (token.Span.IntersectsWith(_textSpan) && _owner.SyntaxTokenKinds.Contains(token.RawKind)) + { + var context = new EmbeddedLanguageClassificationContext( + _solutionServices, _project, _semanticModel, token, _textSpan, _options, _owner.Info.VirtualCharService, _result, _cancellationToken); - // If not other classifier classified this, then give the fallback classifier a chance to classify basic language escapes. - TryClassify(_owner._fallbackClassifier, context); + var classifiers = _owner.GetServices(_semanticModel, token, _cancellationToken); + foreach (var classifier in classifiers) + { + // If this classifier added values then need to check the other ones. + if (TryClassify(classifier.Value, context)) + return; } - } - private bool TryClassify(IEmbeddedLanguageClassifier classifier, EmbeddedLanguageClassificationContext context) - { - var count = _result.Count; - classifier.RegisterClassifications(context); - return _result.Count != count; + // If not other classifier classified this, then give the fallback classifier a chance to classify basic language escapes. + TryClassify(_owner._fallbackClassifier, context); } + } - private void ProcessTriviaList(SyntaxTriviaList triviaList) - { - foreach (var trivia in triviaList) - ProcessTrivia(trivia); - } + private bool TryClassify(IEmbeddedLanguageClassifier classifier, EmbeddedLanguageClassificationContext context) + { + var count = _result.Count; + classifier.RegisterClassifications(context); + return _result.Count != count; + } - private void ProcessTrivia(SyntaxTrivia trivia) - { - if (trivia.HasStructure && trivia.FullSpan.IntersectsWith(_textSpan)) - VisitTokens(trivia.GetStructure()!); - } + private void ProcessTriviaList(SyntaxTriviaList triviaList) + { + foreach (var trivia in triviaList) + ProcessTrivia(trivia); + } + + private void ProcessTrivia(SyntaxTrivia trivia) + { + if (trivia.IsDirective && trivia.FullSpan.IntersectsWith(_textSpan)) + VisitTokens(trivia.GetStructure()!); } } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractFallbackEmbeddedLanguageClassifier.cs b/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractFallbackEmbeddedLanguageClassifier.cs index 7364c72ef2065..fd14266a8d1b8 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractFallbackEmbeddedLanguageClassifier.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractFallbackEmbeddedLanguageClassifier.cs @@ -6,49 +6,48 @@ using Microsoft.CodeAnalysis.EmbeddedLanguages; using Microsoft.CodeAnalysis.Shared.Collections; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal abstract class AbstractFallbackEmbeddedLanguageClassifier : IEmbeddedLanguageClassifier { - internal abstract class AbstractFallbackEmbeddedLanguageClassifier : IEmbeddedLanguageClassifier + private readonly EmbeddedLanguageInfo _info; + private readonly ImmutableArray _supportedKinds; + + protected AbstractFallbackEmbeddedLanguageClassifier(EmbeddedLanguageInfo info) { - private readonly EmbeddedLanguageInfo _info; - private readonly ImmutableArray _supportedKinds; + _info = info; - protected AbstractFallbackEmbeddedLanguageClassifier(EmbeddedLanguageInfo info) - { - _info = info; + using var array = TemporaryArray.Empty; + + array.Add(info.SyntaxKinds.CharacterLiteralToken); + array.Add(info.SyntaxKinds.StringLiteralToken); + array.Add(info.SyntaxKinds.InterpolatedStringTextToken); - using var array = TemporaryArray.Empty; + array.AsRef().AddIfNotNull(info.SyntaxKinds.Utf8StringLiteralToken); - array.Add(info.SyntaxKinds.CharacterLiteralToken); - array.Add(info.SyntaxKinds.StringLiteralToken); - array.Add(info.SyntaxKinds.InterpolatedStringTextToken); + _supportedKinds = array.ToImmutableAndClear(); + } + + public void RegisterClassifications(EmbeddedLanguageClassificationContext context) + { + var token = context.SyntaxToken; + if (!_supportedKinds.Contains(token.RawKind)) + return; - array.AsRef().AddIfNotNull(info.SyntaxKinds.Utf8StringLiteralToken); + var virtualChars = _info.VirtualCharService.TryConvertToVirtualChars(token); + if (virtualChars.IsDefaultOrEmpty) + return; - _supportedKinds = array.ToImmutableAndClear(); - } + // Can avoid any work if we got the same number of virtual characters back as characters in the string. In + // that case, there are clearly no escaped characters. + if (virtualChars.Length == token.Text.Length) + return; - public void RegisterClassifications(EmbeddedLanguageClassificationContext context) + foreach (var vc in virtualChars) { - var token = context.SyntaxToken; - if (!_supportedKinds.Contains(token.RawKind)) - return; - - var virtualChars = _info.VirtualCharService.TryConvertToVirtualChars(token); - if (virtualChars.IsDefaultOrEmpty) - return; - - // Can avoid any work if we got the same number of virtual characters back as characters in the string. In - // that case, there are clearly no escaped characters. - if (virtualChars.Length == token.Text.Length) - return; - - foreach (var vc in virtualChars) - { - // utf-16 virtual char might have either one or two in text span - if (vc.Span.Length > vc.Rune.Utf16SequenceLength) - context.AddClassification(ClassificationTypeNames.StringEscapeCharacter, vc.Span); - } + // utf-16 virtual char might have either one or two in text span + if (vc.Span.Length > vc.Rune.Utf16SequenceLength) + context.AddClassification(ClassificationTypeNames.StringEscapeCharacter, vc.Span); } } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/Classification/EmbeddedLanguageClassifierContext.cs b/src/Features/Core/Portable/EmbeddedLanguages/Classification/EmbeddedLanguageClassifierContext.cs index e19607c9f30be..629b8dd02be56 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/Classification/EmbeddedLanguageClassifierContext.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/Classification/EmbeddedLanguageClassifierContext.cs @@ -8,52 +8,63 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal readonly struct EmbeddedLanguageClassificationContext { - internal readonly struct EmbeddedLanguageClassificationContext + internal readonly SolutionServices SolutionServices; + + private readonly SegmentedList _result; + + /// + /// The portion of the string or character token to classify. + /// + private readonly TextSpan _spanToClassify; + + public Project Project { get; } + + /// + /// The string or character token to classify. + /// + public SyntaxToken SyntaxToken { get; } + + /// + /// SemanticModel that is contained in. + /// + public SemanticModel SemanticModel { get; } + + public CancellationToken CancellationToken { get; } + + internal readonly ClassificationOptions Options; + internal readonly IVirtualCharService VirtualCharService; + + internal EmbeddedLanguageClassificationContext( + SolutionServices solutionServices, + Project project, + SemanticModel semanticModel, + SyntaxToken syntaxToken, + TextSpan spanToClassify, + ClassificationOptions options, + IVirtualCharService virtualCharService, + SegmentedList result, + CancellationToken cancellationToken) + { + SolutionServices = solutionServices; + Project = project; + SemanticModel = semanticModel; + SyntaxToken = syntaxToken; + _spanToClassify = spanToClassify; + Options = options; + VirtualCharService = virtualCharService; + _result = result; + CancellationToken = cancellationToken; + } + + public void AddClassification(string classificationType, TextSpan span) { - internal readonly SolutionServices SolutionServices; - - private readonly SegmentedList _result; - - public Project Project { get; } - - /// - /// The string or character token to classify. - /// - public SyntaxToken SyntaxToken { get; } - - /// - /// SemanticModel that is contained in. - /// - public SemanticModel SemanticModel { get; } - - public CancellationToken CancellationToken { get; } - - internal readonly ClassificationOptions Options; - internal readonly IVirtualCharService VirtualCharService; - - internal EmbeddedLanguageClassificationContext( - SolutionServices solutionServices, - Project project, - SemanticModel semanticModel, - SyntaxToken syntaxToken, - ClassificationOptions options, - IVirtualCharService virtualCharService, - SegmentedList result, - CancellationToken cancellationToken) - { - SolutionServices = solutionServices; - Project = project; - SemanticModel = semanticModel; - SyntaxToken = syntaxToken; - Options = options; - VirtualCharService = virtualCharService; - _result = result; - CancellationToken = cancellationToken; - } - - public void AddClassification(string classificationType, TextSpan span) - => _result.Add(new ClassifiedSpan(classificationType, span)); + // Ignore characters that don't intersect with the requested span. That avoids potentially adding lots of + // classifications for portions of a large string that are out of view. + if (span.IntersectsWith(_spanToClassify)) + _result.Add(new ClassifiedSpan(classificationType, span)); } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/Classification/ExportEmbeddedLanguageClassifier.cs b/src/Features/Core/Portable/EmbeddedLanguages/Classification/ExportEmbeddedLanguageClassifier.cs index 621df95ebfff0..735645b0a0c51 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/Classification/ExportEmbeddedLanguageClassifier.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/Classification/ExportEmbeddedLanguageClassifier.cs @@ -4,18 +4,17 @@ using Microsoft.CodeAnalysis.EmbeddedLanguages; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +/// +/// Use this attribute to export a . +/// +internal class ExportEmbeddedLanguageClassifierAttribute( + string name, string[] languages, bool supportsUnannotatedAPIs, params string[] identifiers) : ExportEmbeddedLanguageFeatureServiceAttribute(typeof(IEmbeddedLanguageClassifier), name, languages, supportsUnannotatedAPIs, identifiers) { - /// - /// Use this attribute to export a . - /// - internal class ExportEmbeddedLanguageClassifierAttribute( - string name, string[] languages, bool supportsUnannotatedAPIs, params string[] identifiers) : ExportEmbeddedLanguageFeatureServiceAttribute(typeof(IEmbeddedLanguageClassifier), name, languages, supportsUnannotatedAPIs, identifiers) + public ExportEmbeddedLanguageClassifierAttribute( + string name, string[] languages, params string[] identifiers) + : this(name, languages, supportsUnannotatedAPIs: false, identifiers) { - public ExportEmbeddedLanguageClassifierAttribute( - string name, string[] languages, params string[] identifiers) - : this(name, languages, supportsUnannotatedAPIs: false, identifiers) - { - } } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/Classification/IEmbeddedLanguageClassifier.cs b/src/Features/Core/Portable/EmbeddedLanguages/Classification/IEmbeddedLanguageClassifier.cs index f701497607ee7..a0086f1bc5873 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/Classification/IEmbeddedLanguageClassifier.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/Classification/IEmbeddedLanguageClassifier.cs @@ -6,14 +6,13 @@ using System.Composition; using Microsoft.CodeAnalysis.EmbeddedLanguages; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal interface IEmbeddedLanguageClassifier : IEmbeddedLanguageFeatureService { - internal interface IEmbeddedLanguageClassifier : IEmbeddedLanguageFeatureService - { - /// - /// This method will be called for all string and character tokens in a file to determine if there are special - /// embedded language strings to classify. - /// - void RegisterClassifications(EmbeddedLanguageClassificationContext context); - } + /// + /// This method will be called for all string and character tokens in a file to determine if there are special + /// embedded language strings to classify. + /// + void RegisterClassifications(EmbeddedLanguageClassificationContext context); } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeEmbeddedCompletionProvider.cs b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeEmbeddedCompletionProvider.cs index 38729b5f3d168..9f89c0e96515a 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeEmbeddedCompletionProvider.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeEmbeddedCompletionProvider.cs @@ -15,225 +15,224 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime +namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime; + +internal sealed partial class DateAndTimeEmbeddedCompletionProvider(DateAndTimeEmbeddedLanguage language) : EmbeddedLanguageCompletionProvider { - internal sealed partial class DateAndTimeEmbeddedCompletionProvider(DateAndTimeEmbeddedLanguage language) : EmbeddedLanguageCompletionProvider - { - private const string StartKey = nameof(StartKey); - private const string LengthKey = nameof(LengthKey); - private const string NewTextKey = nameof(NewTextKey); - private const string DescriptionKey = nameof(DescriptionKey); + private const string StartKey = nameof(StartKey); + private const string LengthKey = nameof(LengthKey); + private const string NewTextKey = nameof(NewTextKey); + private const string DescriptionKey = nameof(DescriptionKey); - // Always soft-select these completion items. Also, never filter down. - private static readonly CompletionItemRules s_rules = - CompletionItemRules.Default.WithSelectionBehavior(CompletionItemSelectionBehavior.SoftSelection) - .WithFilterCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, new char[] { })); + // Always soft-select these completion items. Also, never filter down. + private static readonly CompletionItemRules s_rules = + CompletionItemRules.Default.WithSelectionBehavior(CompletionItemSelectionBehavior.SoftSelection) + .WithFilterCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, new char[] { })); - private readonly DateAndTimeEmbeddedLanguage _language = language; + private readonly DateAndTimeEmbeddedLanguage _language = language; - public override ImmutableHashSet TriggerCharacters { get; } = ['"', ':']; + public override ImmutableHashSet TriggerCharacters { get; } = ['"', ':']; - public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger) + public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger) + { + if (trigger.Kind is CompletionTriggerKind.Invoke or + CompletionTriggerKind.InvokeAndCommitIfUnique) { - if (trigger.Kind is CompletionTriggerKind.Invoke or - CompletionTriggerKind.InvokeAndCommitIfUnique) - { - return true; - } + return true; + } - if (trigger.Kind == CompletionTriggerKind.Insertion) + if (trigger.Kind == CompletionTriggerKind.Insertion) + { + if (TriggerCharacters.Contains(trigger.Character)) { - if (TriggerCharacters.Contains(trigger.Character)) - { - return true; - } - - // Only trigger if it's the first character of a sequence - return char.IsLetter(trigger.Character) && - caretPosition >= 2 && - !char.IsLetter(text[caretPosition - 2]); + return true; } - return false; + // Only trigger if it's the first character of a sequence + return char.IsLetter(trigger.Character) && + caretPosition >= 2 && + !char.IsLetter(text[caretPosition - 2]); } - public override async Task ProvideCompletionsAsync(CompletionContext context) - { - if (!context.CompletionOptions.ProvideDateAndTimeCompletions) - return; - - if (context.Trigger.Kind is not CompletionTriggerKind.Invoke and - not CompletionTriggerKind.InvokeAndCommitIfUnique and - not CompletionTriggerKind.Insertion) - { - return; - } + return false; + } - var document = context.Document; - var position = context.Position; - var cancellationToken = context.CancellationToken; + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + if (!context.CompletionOptions.ProvideDateAndTimeCompletions) + return; - var stringTokenOpt = await _language.TryGetDateAndTimeTokenAtPositionAsync( - document, position, cancellationToken).ConfigureAwait(false); + if (context.Trigger.Kind is not CompletionTriggerKind.Invoke and + not CompletionTriggerKind.InvokeAndCommitIfUnique and + not CompletionTriggerKind.Insertion) + { + return; + } - if (stringTokenOpt == null) - return; + var document = context.Document; + var position = context.Position; + var cancellationToken = context.CancellationToken; - var syntaxFacts = document.GetRequiredLanguageService(); - var stringToken = stringTokenOpt.Value; + var stringTokenOpt = await _language.TryGetDateAndTimeTokenAtPositionAsync( + document, position, cancellationToken).ConfigureAwait(false); - // If we're not in an interpolation, at least make sure we're within the bounds of the string. - if (stringToken.RawKind != syntaxFacts.SyntaxKinds.InterpolatedStringTextToken) - { - if (position <= stringToken.SpanStart || position >= stringToken.Span.End) - return; - } + if (stringTokenOpt == null) + return; - // Note: it's acceptable if this fails to convert. We just won't show the example in that case. - var virtualChars = _language.Info.VirtualCharService.TryConvertToVirtualChars(stringToken); + var syntaxFacts = document.GetRequiredLanguageService(); + var stringToken = stringTokenOpt.Value; - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + // If we're not in an interpolation, at least make sure we're within the bounds of the string. + if (stringToken.RawKind != syntaxFacts.SyntaxKinds.InterpolatedStringTextToken) + { + if (position <= stringToken.SpanStart || position >= stringToken.Span.End) + return; + } - using var _1 = ArrayBuilder.GetInstance(out var items); + // Note: it's acceptable if this fails to convert. We just won't show the example in that case. + var virtualChars = _language.Info.VirtualCharService.TryConvertToVirtualChars(stringToken); - var embeddedContext = new EmbeddedCompletionContext(text, context, virtualChars, items); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - ProvideStandardFormats(embeddedContext); - ProvideCustomFormats(embeddedContext); - if (items.Count == 0) - return; + using var _1 = ArrayBuilder.GetInstance(out var items); - using var _2 = ArrayBuilder>.GetInstance(out var properties); - foreach (var embeddedItem in items) - { - properties.Clear(); - - var textChange = embeddedItem.Change.TextChange; - - properties.Add(new(StartKey, textChange.Span.Start.ToString())); - properties.Add(new(LengthKey, textChange.Span.Length.ToString())); - properties.Add(new(NewTextKey, textChange.NewText!)); - properties.Add(new(DescriptionKey, embeddedItem.FullDescription)); - properties.Add(new(AbstractAggregateEmbeddedLanguageCompletionProvider.EmbeddedProviderName, Name)); - - // Keep everything sorted in the order we just produced the items in. - var sortText = context.Items.Count.ToString("0000"); - context.AddItem(CompletionItem.CreateInternal( - displayText: embeddedItem.DisplayText, - inlineDescription: embeddedItem.InlineDescription, - sortText: sortText, - properties: properties.ToImmutable(), - rules: embeddedItem.IsDefault - ? s_rules.WithMatchPriority(MatchPriority.Preselect) - : s_rules, - isComplexTextEdit: context.CompletionListSpan != textChange.Span)); - } + var embeddedContext = new EmbeddedCompletionContext(text, context, virtualChars, items); - context.IsExclusive = true; - } + ProvideStandardFormats(embeddedContext); + ProvideCustomFormats(embeddedContext); + if (items.Count == 0) + return; - private static void ProvideStandardFormats(EmbeddedCompletionContext context) + using var _2 = ArrayBuilder>.GetInstance(out var properties); + foreach (var embeddedItem in items) { - context.AddStandard("d", FeaturesResources.short_date, FeaturesResources.short_date_description); - context.AddStandard("D", FeaturesResources.long_date, FeaturesResources.long_date_description); - context.AddStandard("f", FeaturesResources.full_short_date_time, FeaturesResources.full_short_date_time_description); - context.AddStandard("F", FeaturesResources.full_long_date_time, FeaturesResources.full_long_date_time_description); - context.AddStandard("g", FeaturesResources.general_short_date_time, FeaturesResources.general_short_date_time_description); - context.AddStandard("G", FeaturesResources.general_long_date_time, FeaturesResources.general_long_date_time_description, isDefault: true); // This is what DateTime.ToString() uses - context.AddStandard("M", FeaturesResources.month_day, FeaturesResources.month_day_description); - context.AddStandard("O", FeaturesResources.round_trip_date_time, FeaturesResources.round_trip_date_time_description); - context.AddStandard("R", FeaturesResources.rfc1123_date_time, FeaturesResources.rfc1123_date_time_description); - context.AddStandard("s", FeaturesResources.sortable_date_time, FeaturesResources.sortable_date_time_description); - context.AddStandard("t", FeaturesResources.short_time, FeaturesResources.short_time_description); - context.AddStandard("T", FeaturesResources.long_time, FeaturesResources.long_time_description); - context.AddStandard("u", FeaturesResources.universal_sortable_date_time, FeaturesResources.universal_sortable_date_time_description); - context.AddStandard("U", FeaturesResources.universal_full_date_time, FeaturesResources.universal_full_date_time_description); - context.AddStandard("Y", FeaturesResources.year_month, FeaturesResources.year_month_description); + properties.Clear(); + + var textChange = embeddedItem.Change.TextChange; + + properties.Add(new(StartKey, textChange.Span.Start.ToString())); + properties.Add(new(LengthKey, textChange.Span.Length.ToString())); + properties.Add(new(NewTextKey, textChange.NewText!)); + properties.Add(new(DescriptionKey, embeddedItem.FullDescription)); + properties.Add(new(AbstractAggregateEmbeddedLanguageCompletionProvider.EmbeddedProviderName, Name)); + + // Keep everything sorted in the order we just produced the items in. + var sortText = context.Items.Count.ToString("0000"); + context.AddItem(CompletionItem.CreateInternal( + displayText: embeddedItem.DisplayText, + inlineDescription: embeddedItem.InlineDescription, + sortText: sortText, + properties: properties.ToImmutable(), + rules: embeddedItem.IsDefault + ? s_rules.WithMatchPriority(MatchPriority.Preselect) + : s_rules, + isComplexTextEdit: context.CompletionListSpan != textChange.Span)); } - private static void ProvideCustomFormats(EmbeddedCompletionContext context) - { - context.AddCustom("d", FeaturesResources.day_of_the_month_1_2_digits, FeaturesResources.day_of_the_month_1_2_digits_description); - context.AddCustom("dd", FeaturesResources.day_of_the_month_2_digits, FeaturesResources.day_of_the_month_2_digits_description); - context.AddCustom("ddd", FeaturesResources.day_of_the_week_abbreviated, FeaturesResources.day_of_the_week_abbreviated_description); - context.AddCustom("dddd", FeaturesResources.day_of_the_week_full, FeaturesResources.day_of_the_week_full_description); - - context.AddCustom("f", FeaturesResources._10ths_of_a_second, FeaturesResources._10ths_of_a_second_description); - context.AddCustom("ff", FeaturesResources._100ths_of_a_second, FeaturesResources._100ths_of_a_second_description); - context.AddCustom("fff", FeaturesResources._1000ths_of_a_second, FeaturesResources._1000ths_of_a_second_description); - context.AddCustom("ffff", FeaturesResources._10000ths_of_a_second, FeaturesResources._10000ths_of_a_second_description); - context.AddCustom("fffff", FeaturesResources._100000ths_of_a_second, FeaturesResources._100000ths_of_a_second_description); - context.AddCustom("ffffff", FeaturesResources._1000000ths_of_a_second, FeaturesResources._1000000ths_of_a_second_description); - context.AddCustom("fffffff", FeaturesResources._10000000ths_of_a_second, FeaturesResources._10000000ths_of_a_second_description); - - context.AddCustom("F", FeaturesResources._10ths_of_a_second_non_zero, FeaturesResources._10ths_of_a_second_non_zero_description); - context.AddCustom("FF", FeaturesResources._100ths_of_a_second_non_zero, FeaturesResources._100ths_of_a_second_non_zero_description); - context.AddCustom("FFF", FeaturesResources._1000ths_of_a_second_non_zero, FeaturesResources._1000ths_of_a_second_non_zero_description); - context.AddCustom("FFFF", FeaturesResources._10000ths_of_a_second_non_zero, FeaturesResources._10000ths_of_a_second_non_zero_description); - context.AddCustom("FFFFF", FeaturesResources._100000ths_of_a_second_non_zero, FeaturesResources._100000ths_of_a_second_non_zero_description); - context.AddCustom("FFFFFF", FeaturesResources._1000000ths_of_a_second_non_zero, FeaturesResources._1000000ths_of_a_second_non_zero_description); - context.AddCustom("FFFFFFF", FeaturesResources._10000000ths_of_a_second_non_zero, FeaturesResources._10000000ths_of_a_second_non_zero_description); - - context.AddCustom("gg", FeaturesResources.period_era, FeaturesResources.period_era_description); - - context.AddCustom("h", FeaturesResources._12_hour_clock_1_2_digits, FeaturesResources._12_hour_clock_1_2_digits_description); - context.AddCustom("hh", FeaturesResources._12_hour_clock_2_digits, FeaturesResources._12_hour_clock_2_digits_description); - - context.AddCustom("H", FeaturesResources._24_hour_clock_1_2_digits, FeaturesResources._24_hour_clock_1_2_digits_description); - context.AddCustom("HH", FeaturesResources._24_hour_clock_2_digits, FeaturesResources._24_hour_clock_2_digits_description); - - context.AddCustom("K", FeaturesResources.time_zone, FeaturesResources.time_zone_description); - - context.AddCustom("m", FeaturesResources.minute_1_2_digits, FeaturesResources.minute_1_2_digits_description); - context.AddCustom("mm", FeaturesResources.minute_2_digits, FeaturesResources.minute_2_digits_description); - - context.AddCustom("M", FeaturesResources.month_1_2_digits, FeaturesResources.month_1_2_digits_description); - context.AddCustom("MM", FeaturesResources.month_2_digits, FeaturesResources.month_2_digits_description); - context.AddCustom("MMM", FeaturesResources.month_abbreviated, FeaturesResources.month_abbreviated_description); - context.AddCustom("MMMM", FeaturesResources.month_full, FeaturesResources.month_full_description); - - context.AddCustom("s", FeaturesResources.second_1_2_digits, FeaturesResources.second_1_2_digits_description); - context.AddCustom("ss", FeaturesResources.second_2_digits, FeaturesResources.second_2_digits_description); - - context.AddCustom("t", FeaturesResources.AM_PM_abbreviated, FeaturesResources.AM_PM_abbreviated_description); - context.AddCustom("tt", FeaturesResources.AM_PM_full, FeaturesResources.AM_PM_full_description); - - context.AddCustom("y", FeaturesResources.year_1_2_digits, FeaturesResources.year_1_2_digits_description); - context.AddCustom("yy", FeaturesResources.year_2_digits, FeaturesResources.year_2_digits_description); - context.AddCustom("yyy", FeaturesResources.year_3_4_digits, FeaturesResources.year_3_4_digits_description); - context.AddCustom("yyyy", FeaturesResources.year_4_digits, FeaturesResources.year_4_digits_description); - context.AddCustom("yyyyy", FeaturesResources.year_5_digits, FeaturesResources.year_5_digits_description); - - context.AddCustom("z", FeaturesResources.utc_hour_offset_1_2_digits, FeaturesResources.utc_hour_offset_1_2_digits_description); - context.AddCustom("zz", FeaturesResources.utc_hour_offset_2_digits, FeaturesResources.utc_hour_offset_2_digits_description); - context.AddCustom("zzz", FeaturesResources.utc_hour_and_minute_offset, FeaturesResources.utc_hour_and_minute_offset_description); - - context.AddCustom(":", FeaturesResources.time_separator, FeaturesResources.time_separator_description); - context.AddCustom("/", FeaturesResources.date_separator, FeaturesResources.date_separator_description); - } + context.IsExclusive = true; + } - public override Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) - { - // These values have always been added by us. - var startString = item.GetProperty(StartKey); - var lengthString = item.GetProperty(LengthKey); - var newText = item.GetProperty(NewTextKey); + private static void ProvideStandardFormats(EmbeddedCompletionContext context) + { + context.AddStandard("d", FeaturesResources.short_date, FeaturesResources.short_date_description); + context.AddStandard("D", FeaturesResources.long_date, FeaturesResources.long_date_description); + context.AddStandard("f", FeaturesResources.full_short_date_time, FeaturesResources.full_short_date_time_description); + context.AddStandard("F", FeaturesResources.full_long_date_time, FeaturesResources.full_long_date_time_description); + context.AddStandard("g", FeaturesResources.general_short_date_time, FeaturesResources.general_short_date_time_description); + context.AddStandard("G", FeaturesResources.general_long_date_time, FeaturesResources.general_long_date_time_description, isDefault: true); // This is what DateTime.ToString() uses + context.AddStandard("M", FeaturesResources.month_day, FeaturesResources.month_day_description); + context.AddStandard("O", FeaturesResources.round_trip_date_time, FeaturesResources.round_trip_date_time_description); + context.AddStandard("R", FeaturesResources.rfc1123_date_time, FeaturesResources.rfc1123_date_time_description); + context.AddStandard("s", FeaturesResources.sortable_date_time, FeaturesResources.sortable_date_time_description); + context.AddStandard("t", FeaturesResources.short_time, FeaturesResources.short_time_description); + context.AddStandard("T", FeaturesResources.long_time, FeaturesResources.long_time_description); + context.AddStandard("u", FeaturesResources.universal_sortable_date_time, FeaturesResources.universal_sortable_date_time_description); + context.AddStandard("U", FeaturesResources.universal_full_date_time, FeaturesResources.universal_full_date_time_description); + context.AddStandard("Y", FeaturesResources.year_month, FeaturesResources.year_month_description); + } - Contract.ThrowIfNull(startString); - Contract.ThrowIfNull(lengthString); - Contract.ThrowIfNull(newText); + private static void ProvideCustomFormats(EmbeddedCompletionContext context) + { + context.AddCustom("d", FeaturesResources.day_of_the_month_1_2_digits, FeaturesResources.day_of_the_month_1_2_digits_description); + context.AddCustom("dd", FeaturesResources.day_of_the_month_2_digits, FeaturesResources.day_of_the_month_2_digits_description); + context.AddCustom("ddd", FeaturesResources.day_of_the_week_abbreviated, FeaturesResources.day_of_the_week_abbreviated_description); + context.AddCustom("dddd", FeaturesResources.day_of_the_week_full, FeaturesResources.day_of_the_week_full_description); + + context.AddCustom("f", FeaturesResources._10ths_of_a_second, FeaturesResources._10ths_of_a_second_description); + context.AddCustom("ff", FeaturesResources._100ths_of_a_second, FeaturesResources._100ths_of_a_second_description); + context.AddCustom("fff", FeaturesResources._1000ths_of_a_second, FeaturesResources._1000ths_of_a_second_description); + context.AddCustom("ffff", FeaturesResources._10000ths_of_a_second, FeaturesResources._10000ths_of_a_second_description); + context.AddCustom("fffff", FeaturesResources._100000ths_of_a_second, FeaturesResources._100000ths_of_a_second_description); + context.AddCustom("ffffff", FeaturesResources._1000000ths_of_a_second, FeaturesResources._1000000ths_of_a_second_description); + context.AddCustom("fffffff", FeaturesResources._10000000ths_of_a_second, FeaturesResources._10000000ths_of_a_second_description); + + context.AddCustom("F", FeaturesResources._10ths_of_a_second_non_zero, FeaturesResources._10ths_of_a_second_non_zero_description); + context.AddCustom("FF", FeaturesResources._100ths_of_a_second_non_zero, FeaturesResources._100ths_of_a_second_non_zero_description); + context.AddCustom("FFF", FeaturesResources._1000ths_of_a_second_non_zero, FeaturesResources._1000ths_of_a_second_non_zero_description); + context.AddCustom("FFFF", FeaturesResources._10000ths_of_a_second_non_zero, FeaturesResources._10000ths_of_a_second_non_zero_description); + context.AddCustom("FFFFF", FeaturesResources._100000ths_of_a_second_non_zero, FeaturesResources._100000ths_of_a_second_non_zero_description); + context.AddCustom("FFFFFF", FeaturesResources._1000000ths_of_a_second_non_zero, FeaturesResources._1000000ths_of_a_second_non_zero_description); + context.AddCustom("FFFFFFF", FeaturesResources._10000000ths_of_a_second_non_zero, FeaturesResources._10000000ths_of_a_second_non_zero_description); + + context.AddCustom("gg", FeaturesResources.period_era, FeaturesResources.period_era_description); + + context.AddCustom("h", FeaturesResources._12_hour_clock_1_2_digits, FeaturesResources._12_hour_clock_1_2_digits_description); + context.AddCustom("hh", FeaturesResources._12_hour_clock_2_digits, FeaturesResources._12_hour_clock_2_digits_description); + + context.AddCustom("H", FeaturesResources._24_hour_clock_1_2_digits, FeaturesResources._24_hour_clock_1_2_digits_description); + context.AddCustom("HH", FeaturesResources._24_hour_clock_2_digits, FeaturesResources._24_hour_clock_2_digits_description); + + context.AddCustom("K", FeaturesResources.time_zone, FeaturesResources.time_zone_description); + + context.AddCustom("m", FeaturesResources.minute_1_2_digits, FeaturesResources.minute_1_2_digits_description); + context.AddCustom("mm", FeaturesResources.minute_2_digits, FeaturesResources.minute_2_digits_description); + + context.AddCustom("M", FeaturesResources.month_1_2_digits, FeaturesResources.month_1_2_digits_description); + context.AddCustom("MM", FeaturesResources.month_2_digits, FeaturesResources.month_2_digits_description); + context.AddCustom("MMM", FeaturesResources.month_abbreviated, FeaturesResources.month_abbreviated_description); + context.AddCustom("MMMM", FeaturesResources.month_full, FeaturesResources.month_full_description); + + context.AddCustom("s", FeaturesResources.second_1_2_digits, FeaturesResources.second_1_2_digits_description); + context.AddCustom("ss", FeaturesResources.second_2_digits, FeaturesResources.second_2_digits_description); + + context.AddCustom("t", FeaturesResources.AM_PM_abbreviated, FeaturesResources.AM_PM_abbreviated_description); + context.AddCustom("tt", FeaturesResources.AM_PM_full, FeaturesResources.AM_PM_full_description); + + context.AddCustom("y", FeaturesResources.year_1_2_digits, FeaturesResources.year_1_2_digits_description); + context.AddCustom("yy", FeaturesResources.year_2_digits, FeaturesResources.year_2_digits_description); + context.AddCustom("yyy", FeaturesResources.year_3_4_digits, FeaturesResources.year_3_4_digits_description); + context.AddCustom("yyyy", FeaturesResources.year_4_digits, FeaturesResources.year_4_digits_description); + context.AddCustom("yyyyy", FeaturesResources.year_5_digits, FeaturesResources.year_5_digits_description); + + context.AddCustom("z", FeaturesResources.utc_hour_offset_1_2_digits, FeaturesResources.utc_hour_offset_1_2_digits_description); + context.AddCustom("zz", FeaturesResources.utc_hour_offset_2_digits, FeaturesResources.utc_hour_offset_2_digits_description); + context.AddCustom("zzz", FeaturesResources.utc_hour_and_minute_offset, FeaturesResources.utc_hour_and_minute_offset_description); + + context.AddCustom(":", FeaturesResources.time_separator, FeaturesResources.time_separator_description); + context.AddCustom("/", FeaturesResources.date_separator, FeaturesResources.date_separator_description); + } - return Task.FromResult(CompletionChange.Create( - new TextChange(new TextSpan(int.Parse(startString), int.Parse(lengthString)), newText))); - } + public override Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) + { + // These values have always been added by us. + var startString = item.GetProperty(StartKey); + var lengthString = item.GetProperty(LengthKey); + var newText = item.GetProperty(NewTextKey); - public override Task GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken) - { - if (!item.TryGetProperty(DescriptionKey, out var description)) - return SpecializedTasks.Null(); + Contract.ThrowIfNull(startString); + Contract.ThrowIfNull(lengthString); + Contract.ThrowIfNull(newText); - return Task.FromResult((CompletionDescription?)CompletionDescription.Create( - [new TaggedText(TextTags.Text, description)])); - } + return Task.FromResult(CompletionChange.Create( + new TextChange(new TextSpan(int.Parse(startString), int.Parse(lengthString)), newText))); + } + + public override Task GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken) + { + if (!item.TryGetProperty(DescriptionKey, out var description)) + return SpecializedTasks.Null(); + + return Task.FromResult((CompletionDescription?)CompletionDescription.Create( + [new TaggedText(TextTags.Text, description)])); } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeItem.cs b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeItem.cs index 180068ed32a1d..57ca43092a55a 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeItem.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeItem.cs @@ -4,18 +4,17 @@ using Microsoft.CodeAnalysis.Completion; -namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime +namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime; + +internal partial class DateAndTimeEmbeddedCompletionProvider { - internal partial class DateAndTimeEmbeddedCompletionProvider + private readonly struct DateAndTimeItem( + string displayText, string inlineDescription, string fullDescription, CompletionChange change, bool isDefault) { - private readonly struct DateAndTimeItem( - string displayText, string inlineDescription, string fullDescription, CompletionChange change, bool isDefault) - { - public readonly string DisplayText = displayText; - public readonly string InlineDescription = inlineDescription; - public readonly string FullDescription = fullDescription; - public readonly CompletionChange Change = change; - public readonly bool IsDefault = isDefault; - } + public readonly string DisplayText = displayText; + public readonly string InlineDescription = inlineDescription; + public readonly string FullDescription = fullDescription; + public readonly CompletionChange Change = change; + public readonly bool IsDefault = isDefault; } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeOptions.cs b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeOptions.cs index 110e786332809..83e1e360fed58 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeOptions.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateAndTimeOptions.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime +namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime; + +/// +/// No actual options for DateAndTime. But we use this to fit into the common pattern of embedded languages. +/// +internal enum DateAndTimeOptions { - /// - /// No actual options for DateAndTime. But we use this to fit into the common pattern of embedded languages. - /// - internal enum DateAndTimeOptions - { - } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateTimeTree.cs b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateTimeTree.cs index 8bd799bea183a..eae16db26deae 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateTimeTree.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/DateTimeTree.cs @@ -2,17 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime +namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime; + +/// +/// No actual tree for DateAndTime. But we use this to fit into the common pattern of embedded languages. +/// +internal class DateTimeTree { - /// - /// No actual tree for DateAndTime. But we use this to fit into the common pattern of embedded languages. - /// - internal class DateTimeTree - { - public static readonly DateTimeTree Instance = new(); + public static readonly DateTimeTree Instance = new(); - private DateTimeTree() - { - } + private DateTimeTree() + { } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/EmbeddedCompletionContext.cs b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/EmbeddedCompletionContext.cs index bad046b6814a1..38a0ab4c4ac88 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/EmbeddedCompletionContext.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/EmbeddedCompletionContext.cs @@ -9,142 +9,141 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime +namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime; + +internal partial class DateAndTimeEmbeddedCompletionProvider { - internal partial class DateAndTimeEmbeddedCompletionProvider + private readonly struct EmbeddedCompletionContext { - private readonly struct EmbeddedCompletionContext + private static readonly DateTime s_exampleDateTime = DateTime.Parse("2009-06-15T13:45:30.1234567"); + private static readonly CultureInfo s_enUsCulture = CultureInfo.GetCultureInfo("en-US"); + + private readonly ArrayBuilder _items; + private readonly TextSpan _replacementSpan; + + /// + /// The portion of the user string token prior to the section we're replacing. Used for building the + /// example format to present. + /// + private readonly string _userFormatPrefix; + + /// + /// The portion of the user string token after to the section we're replacing. Used for building the + /// example format to present. + /// + private readonly string _userFormatSuffix; + + public EmbeddedCompletionContext( + SourceText text, + CompletionContext context, + VirtualCharSequence virtualChars, + ArrayBuilder items) { - private static readonly DateTime s_exampleDateTime = DateTime.Parse("2009-06-15T13:45:30.1234567"); - private static readonly CultureInfo s_enUsCulture = CultureInfo.GetCultureInfo("en-US"); - - private readonly ArrayBuilder _items; - private readonly TextSpan _replacementSpan; - - /// - /// The portion of the user string token prior to the section we're replacing. Used for building the - /// example format to present. - /// - private readonly string _userFormatPrefix; - - /// - /// The portion of the user string token after to the section we're replacing. Used for building the - /// example format to present. - /// - private readonly string _userFormatSuffix; - - public EmbeddedCompletionContext( - SourceText text, - CompletionContext context, - VirtualCharSequence virtualChars, - ArrayBuilder items) + _items = items; + + var startPosition = context.Position; + while (char.IsLetter(text[startPosition - 1])) { - _items = items; + startPosition--; + } - var startPosition = context.Position; - while (char.IsLetter(text[startPosition - 1])) - { - startPosition--; - } + _replacementSpan = TextSpan.FromBounds(startPosition, context.Position); - _replacementSpan = TextSpan.FromBounds(startPosition, context.Position); + (_userFormatPrefix, _userFormatSuffix) = GetUserFormatParts(virtualChars, startPosition, context.Position); + } - (_userFormatPrefix, _userFormatSuffix) = GetUserFormatParts(virtualChars, startPosition, context.Position); - } + private static (string prefix, string suffix) GetUserFormatParts( + VirtualCharSequence virtualChars, int startPosition, int endPosition) + { + virtualChars = virtualChars.IsDefault ? VirtualCharSequence.Empty : virtualChars; - private static (string prefix, string suffix) GetUserFormatParts( - VirtualCharSequence virtualChars, int startPosition, int endPosition) + using var _1 = PooledStringBuilder.GetInstance(out var prefix); + using var _2 = PooledStringBuilder.GetInstance(out var suffix); + foreach (var ch in virtualChars) { - virtualChars = virtualChars.IsDefault ? VirtualCharSequence.Empty : virtualChars; - - using var _1 = PooledStringBuilder.GetInstance(out var prefix); - using var _2 = PooledStringBuilder.GetInstance(out var suffix); - foreach (var ch in virtualChars) - { - if (ch.Span.End <= startPosition) - ch.AppendTo(prefix); - else if (ch.Span.Start >= endPosition) - ch.AppendTo(suffix); - } - - return (prefix.ToString(), suffix.ToString()); + if (ch.Span.End <= startPosition) + ch.AppendTo(prefix); + else if (ch.Span.Start >= endPosition) + ch.AppendTo(suffix); } - private void AddExamples(ArrayBuilder examples, bool standard, string displayText) - { - var userFormat = _userFormatPrefix + displayText + _userFormatSuffix; + return (prefix.ToString(), suffix.ToString()); + } - var primaryCulture = CultureInfo.CurrentCulture; - var secondaryCulture = s_enUsCulture; - var hideCulture = primaryCulture.Equals(secondaryCulture); + private void AddExamples(ArrayBuilder examples, bool standard, string displayText) + { + var userFormat = _userFormatPrefix + displayText + _userFormatSuffix; - AddExample(examples, standard, userFormat, primaryCulture, hideCulture); - AddExample(examples, standard, userFormat, secondaryCulture, hideCulture); - AddExample(examples, standard, displayText, primaryCulture, hideCulture); - AddExample(examples, standard, displayText, secondaryCulture, hideCulture); - } + var primaryCulture = CultureInfo.CurrentCulture; + var secondaryCulture = s_enUsCulture; + var hideCulture = primaryCulture.Equals(secondaryCulture); + + AddExample(examples, standard, userFormat, primaryCulture, hideCulture); + AddExample(examples, standard, userFormat, secondaryCulture, hideCulture); + AddExample(examples, standard, displayText, primaryCulture, hideCulture); + AddExample(examples, standard, displayText, secondaryCulture, hideCulture); + } - private static void AddExample( - ArrayBuilder examples, bool standard, string displayText, CultureInfo culture, bool hideCulture) + private static void AddExample( + ArrayBuilder examples, bool standard, string displayText, CultureInfo culture, bool hideCulture) + { + // Single letter custom strings need a %, or else they're interpreted as a format + // standard format string (and will throw a format exception). + var formatString = !standard && displayText.Length == 1 + ? "%" + displayText + : displayText; + + if (formatString == "") + return; + + // Format string may be invalid. Just don't show anything in that case. + string formattedDate; + try + { + formattedDate = s_exampleDateTime.ToString(formatString, culture); + } + catch (FormatException) + { + return; + } + catch (ArgumentOutOfRangeException) { - // Single letter custom strings need a %, or else they're interpreted as a format - // standard format string (and will throw a format exception). - var formatString = !standard && displayText.Length == 1 - ? "%" + displayText - : displayText; - - if (formatString == "") - return; - - // Format string may be invalid. Just don't show anything in that case. - string formattedDate; - try - { - formattedDate = s_exampleDateTime.ToString(formatString, culture); - } - catch (FormatException) - { - return; - } - catch (ArgumentOutOfRangeException) - { - return; - } - - var example = hideCulture - ? $" {displayText} → {formattedDate}" - : $" {displayText} ({culture}) → {formattedDate}"; - if (!examples.Contains(example)) - examples.Add(example); + return; } - public void AddStandard(string displayText, string suffix, string description, bool isDefault = false) - => Add(displayText, suffix, description, standard: true, isDefault); + var example = hideCulture + ? $" {displayText} → {formattedDate}" + : $" {displayText} ({culture}) → {formattedDate}"; + if (!examples.Contains(example)) + examples.Add(example); + } - public void AddCustom(string displayText, string suffix, string description) - => Add(displayText, suffix, description, standard: false, isDefault: false); + public void AddStandard(string displayText, string suffix, string description, bool isDefault = false) + => Add(displayText, suffix, description, standard: true, isDefault); - private void Add(string displayText, string suffix, string description, bool standard, bool isDefault) - { - using var _1 = PooledStringBuilder.GetInstance(out var descriptionBuilder); - using var _2 = ArrayBuilder.GetInstance(out var examples); + public void AddCustom(string displayText, string suffix, string description) + => Add(displayText, suffix, description, standard: false, isDefault: false); - AddExamples(examples, standard, displayText); + private void Add(string displayText, string suffix, string description, bool standard, bool isDefault) + { + using var _1 = PooledStringBuilder.GetInstance(out var descriptionBuilder); + using var _2 = ArrayBuilder.GetInstance(out var examples); - descriptionBuilder.AppendLine( - examples.Count == 1 ? FeaturesResources.Example : FeaturesResources.Examples); - foreach (var example in examples) - descriptionBuilder.AppendLine(example); + AddExamples(examples, standard, displayText); - descriptionBuilder.AppendLine(); - descriptionBuilder.Append(description); + descriptionBuilder.AppendLine( + examples.Count == 1 ? FeaturesResources.Example : FeaturesResources.Examples); + foreach (var example in examples) + descriptionBuilder.AppendLine(example); - _items.Add(new DateAndTimeItem( - displayText, suffix, descriptionBuilder.ToString(), - CompletionChange.Create( - new TextChange(_replacementSpan, displayText)), - isDefault)); - } + descriptionBuilder.AppendLine(); + descriptionBuilder.Append(description); + + _items.Add(new DateAndTimeItem( + displayText, suffix, descriptionBuilder.ToString(), + CompletionChange.Create( + new TextChange(_replacementSpan, displayText)), + isDefault)); } } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/LanguageServices/DateAndTimeEmbeddedLanguage.cs b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/LanguageServices/DateAndTimeEmbeddedLanguage.cs index 7ca3818467e11..a42b7a39d6962 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/LanguageServices/DateAndTimeEmbeddedLanguage.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/DateAndTime/LanguageServices/DateAndTimeEmbeddedLanguage.cs @@ -9,54 +9,53 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime.LanguageServices +namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.DateAndTime.LanguageServices; + +internal class DateAndTimeEmbeddedLanguage : IEmbeddedLanguage { - internal class DateAndTimeEmbeddedLanguage : IEmbeddedLanguage + public readonly EmbeddedLanguageInfo Info; + + public DateAndTimeEmbeddedLanguage(EmbeddedLanguageInfo info) { - public readonly EmbeddedLanguageInfo Info; + Info = info; + CompletionProvider = new DateAndTimeEmbeddedCompletionProvider(this); + } - public DateAndTimeEmbeddedLanguage(EmbeddedLanguageInfo info) - { - Info = info; - CompletionProvider = new DateAndTimeEmbeddedCompletionProvider(this); - } + public EmbeddedLanguageCompletionProvider CompletionProvider { get; } - public EmbeddedLanguageCompletionProvider CompletionProvider { get; } + public async Task TryGetDateAndTimeTokenAtPositionAsync( + Document document, int position, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); - public async Task TryGetDateAndTimeTokenAtPositionAsync( - Document document, int position, CancellationToken cancellationToken) - { - var syntaxFacts = document.GetRequiredLanguageService(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = GetToken(syntaxFacts, root, position); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = GetToken(syntaxFacts, root, position); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var detector = DateAndTimeLanguageDetector.GetOrCreate(semanticModel.Compilation, this.Info); + return detector.TryParseString(token, semanticModel, cancellationToken) != null + ? token : null; + } - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var detector = DateAndTimeLanguageDetector.GetOrCreate(semanticModel.Compilation, this.Info); - return detector.TryParseString(token, semanticModel, cancellationToken) != null - ? token : null; - } + private static SyntaxToken GetToken(ISyntaxFactsService syntaxFacts, SyntaxNode root, int position) + { + var token = root.FindToken(position); + var syntaxKinds = syntaxFacts.SyntaxKinds; - private static SyntaxToken GetToken(ISyntaxFactsService syntaxFacts, SyntaxNode root, int position) + if (token.RawKind == syntaxKinds.CloseBraceToken) { - var token = root.FindToken(position); - var syntaxKinds = syntaxFacts.SyntaxKinds; - - if (token.RawKind == syntaxKinds.CloseBraceToken) - { - // Might be here: $"Date is: {date:$$}" or - // $"Date is: {date:G$$}" - // - // If so, we want to return the InterpolatedStringTextToken following the `:` - var previous = token.GetPreviousToken(); - if (previous.RawKind == syntaxKinds.InterpolatedStringTextToken) - return previous; - - if (previous.RawKind == syntaxKinds.ColonToken) - return previous.GetNextToken(includeZeroWidth: true); - } - - return token; + // Might be here: $"Date is: {date:$$}" or + // $"Date is: {date:G$$}" + // + // If so, we want to return the InterpolatedStringTextToken following the `:` + var previous = token.GetPreviousToken(); + if (previous.RawKind == syntaxKinds.InterpolatedStringTextToken) + return previous; + + if (previous.RawKind == syntaxKinds.ColonToken) + return previous.GetNextToken(includeZeroWidth: true); } + + return token; } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/EmbeddedLanguageCommentDetector.cs b/src/Features/Core/Portable/EmbeddedLanguages/EmbeddedLanguageCommentDetector.cs index 5dff6294d937d..48e18827cfef8 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/EmbeddedLanguageCommentDetector.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/EmbeddedLanguageCommentDetector.cs @@ -8,47 +8,46 @@ using System.Linq; using System.Text.RegularExpressions; -namespace Microsoft.CodeAnalysis.EmbeddedLanguages +namespace Microsoft.CodeAnalysis.EmbeddedLanguages; + +/// +/// Helps match patterns of the form: language=name,option1,option2,option3 +/// +/// All matching is case insensitive, with spaces allowed between the punctuation. Option values are +/// returned as strings. +/// +/// Option names are the values from the TOptions enum. +/// +internal readonly struct EmbeddedLanguageCommentDetector { - /// - /// Helps match patterns of the form: language=name,option1,option2,option3 - /// - /// All matching is case insensitive, with spaces allowed between the punctuation. Option values are - /// returned as strings. - /// - /// Option names are the values from the TOptions enum. - /// - internal readonly struct EmbeddedLanguageCommentDetector - { - private readonly Regex _regex; + private readonly Regex _regex; - public EmbeddedLanguageCommentDetector(ImmutableArray identifiers) - { - var namePortion = string.Join("|", identifiers.Select(n => $"({Regex.Escape(n)})")); - _regex = new Regex($@"^((//)|(')|(/\*))\s*lang(uage)?\s*=\s*(?{namePortion})\b((\s*,\s*)(?